简介:围绕线性CCD循迹的智能小车控制系统设计文档,面向参加NXP杯智能汽车竞赛的高校队伍、嵌入式与控制算法初学者,以及需要借鉴循迹方案的系统开发者。内容以MC9S12X S128单片机为控制核心,讲解利用蓝宙TSL1401线性CCD识别两侧黑色引导线白色道路的完整实现思路。硬件部分涵盖7.2V电池供电与5V、6V降压设计、CCD图像采集、$3010舵机转向、欧姆龙编码器测速,以及加装74LVC245隔离保护的MOS管双电机驱动;软件部分重点说明多个固定阈值滤波、跳变沿黑线边缘提取、分段非线性位置式PD转向控制与增量式PID速度闭环调速,并给出直道、起跑线、十字等赛道元素的识别与抗干扰处理经验。压缩包内为1个PDF文件,约139KB,篇幅精炼、便于打印研读。目前已有122人学习,适合希望快速掌握线性CCD智能车硬件架构与控制算法、用于课程设计或竞赛备赛的读者参考。
1. 线性CCD智能小车为什么比红外循迹更适合做控制系统
赛道上有反光接缝、环境灯管频闪、黑线边缘毛刺,红外对管只返回“有”或“无”,一旦车身偏出半厘米,控制系统拿到的仍是同一个开关量,转向舵机只能在最后一刻猛打。线性CCD把一维视场里的灰度分布连续采回来,哪怕黑线只有几个像素宽,也能算出中心位置、线宽和左右边缘斜率。对智能小车控制系统来说,控制量从离散开关变成了连续偏差,位置式PID才有发挥空间。这套方案适合电子设计竞赛、工创赛智能物流小车、智能搬运小车这类需要在分岔口和急弯提前判断的场合,也适合从51单片机智能小车升级到STM32智能小车的课程设计。
2. 线性CCD成像原理与主控选型:从像素输出到STM32F103采样
2.1 线性CCD的电荷积分与曝光时间怎么定
线性CCD内部是一排光敏像素,每个像素在积分时间内把光生电荷攒起来,再由转移栅依次搬到模拟输出脚。常见器件如TCD1206、TCD1304,输出是一串随光强变化的电压脉冲。曝光时间就是积分时间,它直接决定输出电压幅度:时间太短,白底和黑线都接近0,阈值没法分;时间太长,白底饱和,黑线也被周围散射光抬起来,边缘变糊。
实际调试时,先把CCD对准白纸,用示波器看模拟输出峰峰值。调整积分时间,让白底输出落在ADC量程的70%到85%,黑线落在地板附近。曝光时间通常在1ms到5ms之间,再长就会在高速过弯时产生运动模糊。帧率近似等于1/(曝光时间+读出时间),读出时间由像素数和时钟频率决定。
| 曝光时间 | 理论帧率 | 适用车速 | 现象 |
|---|---|---|---|
| 0.5ms | 约800Hz | 高速直道 | 光线暗时输出太小 |
| 1ms | 约500Hz | 中高速 | 多数室内赛道可用 |
| 2ms | 约300Hz | 中速 | 抗环境光稍好 |
| 5ms | 约150Hz | 低速调试 | 过弯拖影明显 |
提示:曝光时间不要只在静态下调好就固定,电池电压下降后红外补光或环境灯变化,白底输出会漂,需要留动态阈值余量。
2.2 主控选型:STM32F103、51单片机、Arduino谁更适合线性CCD智能小车
线性CCD的像素多、帧率高,主控要同时做ADC采样、二值化、中线提取、PID和电机PWM。51单片机智能小车常见的是STC89C52一类,机器周期长,ADC外设弱,跑128像素、200Hz以上时中断会挤占控制周期,适合低速电磁三轮智能小车循迹,不太适合线性CCD。Arduino智能小车开发快,analogRead加中断也能跑,但analogRead阻塞时间长,多路采样时抖动大,适合验证算法,不适合追求高帧率。
STM32F103系列更均衡,72MHz主频、12位ADC、定时器触发和DMA搬运可以把CPU从采样里解放出来。若选STM32F103ZET6这类大容量型号,定时器、串口和IO都宽裕,方便接OLED和编码器。选型时重点看三点:ADC是否支持外部触发和DMA;定时器能否输出CCD所需的转移时钟;串口是否够用,因为调CCD波形时串口比调试器直观。
| 平台 | ADC与DMA | 控制周期 | 适合场景 |
|---|---|---|---|
| STM32F103 | 12位,支持定时器触发+DMA | 1ms以内 | 线性CCD智能小车主控 |
| 51单片机 | 多数无ADC,需外扩 | 5ms以上 | 低速循迹、教学 |
| Arduino | 10位,阻塞式读取 | 2ms到5ms | 算法验证、快速原型 |
2.3 线性CCD驱动与ADC+DMA采样最小代码
下面以STM32 HAL库为例,思路是用定时器更新事件触发ADC,再让DMA把一帧像素搬到数组。CCD的转移时钟由另一个定时器PWM输出,这里不展开寄存器配置,只保留采样主干。
// STM32F103 + 线性CCD:定时器触发ADC,DMA搬运一帧像素 #define CCD_PIXELS 128 uint16_t ccd_raw[CCD_PIXELS]; void CCD_Sample_Init(void) { // ADC1 外部触发选择 TIM2_TRGO,上升沿启动 // DMA1_Channel1 循环模式,搬 CCD_PIXELS 个半字 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ccd_raw, CCD_PIXELS); } // 每次DMA传输完成中断里置位,主循环处理 volatile uint8_t ccd_frame_ready = 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { ccd_frame_ready = 1; // 一帧采集完成 } }逻辑说明:CCD每输出一个像素,ADC就采一次,DMA自动递增目标地址,不需要在中断里逐个读寄存器。参数说明:CCD_PIXELS要和实际使用的像素数一致,多采会浪费时间,少采会丢边缘;ADC采样时间要小于像素时钟周期的一半,否则相邻像素串扰;DMA用循环模式,主循环来不及处理时新帧会覆盖旧帧,调试阶段可以改成正常模式并在回调里重启。
3. 从一维灰度到黑线偏差:线性CCD二值化、边缘检测与中线提取
3.1 固定阈值二值化为什么在赛道翻车
固定阈值在实验室白纸上好用,一到赛场就出问题:灯管照到赛道一侧,白底从3000跳到3600,黑线从400升到900,同一个阈值会把半边白底判成黑线。常见做法是用一帧内的最亮和最暗做动态阈值,或者对每帧做归一化后再比较。动态阈值不能简单取中值,因为黑线只占少数像素,中值容易落在白底上。更稳的是取最大最小值的加权,再配合边缘梯度。
// 一帧内动态阈值二值化,white_is_high表示白底ADC值大 #define CCD_N 128 uint8_t ccd_bin[CCD_N]; void ccd_binarize(uint16_t *raw, uint8_t *bin, uint16_t n, uint8_t white_is_high) { uint16_t vmin = 4095, vmax = 0; for (uint16_t i = 0; i < n; i++) { if (raw[i] < vmin) vmin = raw[i]; if (raw[i] > vmax) vmax = raw[i]; } // 阈值偏向黑线一侧,避免白底噪声被误判 uint16_t th = vmin + (uint16_t)((vmax - vmin) * 0.35f); for (uint16_t i = 0; i < n; i++) { uint8_t is_white = white_is_high ? (raw[i] > th) : (raw[i] < th); bin[i] = is_white ? 1 : 0; // 1为白,0为黑 } }逻辑说明:先统计一帧的极值,再按比例取阈值,比固定值更能适应光照变化。参数说明:white_is_high根据CCD输出极性设置,TCD1206常见白底输出高;0.35f是经验系数,光线均匀时可取0.5,反光严重时降到0.25。如果一帧内全是白底,vmax-vmin很小,阈值会接近vmin,此时应当用上一帧阈值或直接判丢线。
3.2 边缘检测与中线计算:跳变沿、加权质心、宽度有效性
二值化后要从中线偏差。最简单的是找左右跳变沿:从左往右第一个白到黑的下降沿是左边缘,从右往左第一个白到黑的下降沿是右边缘,取平均就是黑线中心。这种方法在十字路口会找到多个沿,需要加宽度过滤。更抗噪的是对黑点做加权质心,把每个黑像素的坐标乘权重累加,权重可以用灰度值或到上一帧中心的距离。
| 方法 | 抗噪 | 计算量 | 适用 |
|---|---|---|---|
| 左右跳变沿 | 中 | 极小 | 单线、无十字 |
| 加权质心 | 高 | 小 | 弯道、虚线 |
| 多段聚类 | 高 | 中 | 十字、分岔 |
| 边缘梯度拟合 | 高 | 大 | 高速、模糊赛道 |
typedef struct { float center; // 黑线中心像素坐标 int left; // 左边缘 int right; // 右边缘 uint8_t valid; // 是否有效 } LineInfo; LineInfo ccd_get_line(uint8_t *bin, int n) { LineInfo info = {0, -1, -1, 0}; float sum = 0, cnt = 0; for (int i = 1; i < n - 1; i++) { if (bin[i] == 0) { // 黑点 sum += i; cnt += 1.0f; } // 记录第一个左沿和最后一个右沿,用于线宽判断 if (bin[i-1] == 1 && bin[i] == 0 && info.left < 0) info.left = i; if (bin[i] == 0 && bin[i+1] == 1) info.right = i; } if (cnt > 3) { // 黑点太少视为丢线 info.center = sum / cnt; info.valid = 1; } return info; }逻辑说明:先累加所有黑点坐标求质心,同时记录左右沿用于线宽判断。参数说明:cnt > 3是有效性门槛,像素少时质心会跳;如果赛道有虚线,门槛要降到2并配合上一帧预测。实际控制时偏差用info.center - CCD_N/2,再除以CCD_N/2归一化到-1到1,PID参数才好整定。
3.3 偏差映射到舵机与差速:位置式PID的C语言实现
线性CCD给出的偏差有正负,位置式PID输出给舵机或左右轮差速。舵机控制常见做法是输出PWM脉宽,中值1500us,左右限幅到1000us和2000us。差速控制则把PID输出加到左右轮PWM上,同时用速度环维持总速度。下面是一个带积分限幅和死区的PID。
typedef struct { float kp, ki, kd; float err, last_err, integral; float out_max; float dead_zone; } PID; float pid_calc(PID *p, float target, float actual) { p->err = target - actual; if (p->err > -p->dead_zone && p->err < p->dead_zone) { p->err = 0; // 小偏差不动作,防止舵机抖 } p->integral += p->err; if (p->integral > p->out_max) p->integral = p->out_max; if (p->integral < -p->out_max) p->integral = -p->out_max; float d = p->err - p->last_err; p->last_err = p->err; return p->kp * p->err + p->ki * p->integral + p->kd * d; }逻辑说明:误差是目标中线减当前中线,输出正负对应左右转。参数说明:kp先给1.0左右,能回中线但不过冲;ki通常给0到0.05,大了会在弯道积分饱和;kd给0.05到0.3,抑制过冲但会放大CCD噪声;dead_zone按像素定,常见1到2个像素;out_max对应舵机最大偏转或差速上限。调参顺序是先kp,再加kd,最后少量ki。
4. 线性CCD智能小车控制系统闭环调参:速度环、转向环与丢线保护
4.1 双环控制结构:转向环和速度环如何解耦
线性CCD智能小车如果只做转向PID,直道能跑,进弯会冲出去,因为速度没降。常见做法是串级:外环用中线偏差算目标速度,内环用编码器算实际速度。转向环单独跑,输出舵机角度或左右差速。速度环的目标值可以由偏差绝对值决定:偏差大就降速,偏差小就加速。这样直道全速,弯道自动刹车。
// 双环控制:偏差大则降速,转向环独立输出 float steer_pid(PID *sp, float err_norm) { return pid_calc(sp, 0.0f, err_norm); // 目标偏差为0 } float speed_target(float base_speed, float err_norm) { float abs_err = err_norm > 0 ? err_norm : -err_norm; float reduce = abs_err * 40.0f; // 偏差1.0时降40 float target = base_speed - reduce; return target < 15.0f ? 15.0f : target; }逻辑说明:转向环目标始终是0偏差,速度环目标随偏差收缩。参数说明:base_speed是直道编码器脉冲目标值,按电机减速比实测;40.0f是降速系数,赛道急弯多就加大;15.0f是最低速度,太低会在弯中失速。两个环的周期可以不同,转向环1ms到2ms,速度环5ms到10ms。
4.2 曝光时间、帧率与车速的匹配表
CCD帧率决定了控制系统能多快看到偏差。假设车重和轮胎抓地力固定,过弯时从看到线到打舵的延迟超过控制周期,PID再快也救不回来。下面给一个经验匹配表,实际以跑车测试为准。
| 车速 | 建议帧率 | 曝光时间 | 控制周期 | 备注 |
|---|---|---|---|---|
| 0.5m/s | 100Hz | 3ms | 5ms | 调试阶段 |
| 1.0m/s | 200Hz | 2ms | 3ms | 稳定循迹 |
| 1.5m/s | 300Hz | 1ms | 2ms | 需要补光 |
| 2.0m/s | 500Hz | 0.5ms | 1ms | 直道多、弯道少 |
注意:帧率不是越高越好,曝光时间缩短后信号变小,ADC噪声占比变大,二值化会抖。先保证白底输出稳定,再提帧率。
4.3 丢线、十字、急弯的工况状态机
线性CCD在十字路口会看到左右两条黑线,在急弯会丢掉一侧边缘,在虚线赛道会间歇丢线。如果只用一帧中线,控制器会来回甩。常见做法是加状态机:正常循迹、丢线预测、十字、急弯。丢线时用上一帧偏差和陀螺仪或编码器差速做短时预测,超过时间阈值就停车。
typedef enum { LINE_NORMAL, LINE_LOST, LINE_CROSS, LINE_CURVE } LineState; LineState line_state = LINE_NORMAL; float last_err_norm = 0; int lost_cnt = 0; void line_state_update(LineInfo *info, float err_norm) { if (!info->valid) { lost_cnt++; if (lost_cnt < 10) { line_state = LINE_LOST; // 短时丢线,用上次偏差 } else { line_state = LINE_NORMAL; // 长时间丢线由上层停车 } } else { lost_cnt = 0; line_state = LINE_NORMAL; last_err_norm = err_norm; } // 线宽超过阈值且左右沿都在,可判十字 if (info->valid && (info->right - info->left) > CCD_N * 0.6f) { line_state = LINE_CROSS; } }逻辑说明:丢线计数器区分短时遮挡和真正冲出赛道,十字用线宽判断。参数说明:lost_cnt < 10按帧率调整,200Hz下约50ms;线宽阈值0.6f要按赛道黑线宽度标定。急弯状态可以用偏差绝对值大于0.7并持续几帧来触发,触发后强制降速。
4.4 用串口波形调CCD:把一维像素发到上位机
调CCD最有效的手段不是盯着车跑,而是把一帧像素发到串口上位机看波形。波特率至少115200,一帧128像素用十六进制发送,帧头加0xAA 0x55,帧尾加校验。上位机画出来的波形能直接看出曝光是否合适、阈值是否切偏、边缘是否被反光干扰。
// 串口发送一帧CCD原始数据,帧头0xAA55,像素低字节在前 void ccd_send_frame(uint16_t *raw, int n) { uint8_t head[2] = {0xAA, 0x55}; HAL_UART_Transmit(&huart1, head, 2, 10); for (int i = 0; i < n; i++) { uint8_t buf[2]; buf[0] = raw[i] & 0xFF; buf[1] = (raw[i] >> 8) & 0xFF; HAL_UART_Transmit(&huart1, buf, 2, 10); } }逻辑说明:帧头用于上位机对齐,两个字节表示一个12位ADC值。参数说明:HAL_UART_Transmit的超时10ms在115200下够用,但会阻塞控制循环,实际可以改成DMA发送。发送频率不要等于控制频率,每5帧发一次即可,避免占用CPU。
5. 线性CCD智能小车的进阶技巧:标定、抗光干扰与故障定位
5.1 全白全黑标定与归一化
线性CCD个体差异和镜头安装角度会让同一块白底在不同像素上输出不同。跑车前做一次全白和全黑标定,把每个像素的white[i]和black[i]存下来,运行时用(raw[i]-black[i])/(white[i]-black[i])归一化到0到1,再统一二值化。这样即使左侧像素偏暗,也不会被误判成黑线。标定数据可以存在Flash里,每次上电读取。
5.2 抗光干扰与机械安装
环境光干扰主要来自顶部灯管和赛道反光。机械上给CCD加遮光罩,镜头离地高度控制在8cm到15cm,视场覆盖黑线两侧各两倍线宽。镜头前加红外滤光片配合红外补光,能压掉可见光频闪,但要注意补光均匀,否则中间亮两边暗。程序上可以在二值化前做相邻像素滑动平均,窗口3到5,代价是边缘变钝,高速时慎用。
5.3 线性CCD智能小车控制系统故障排查表
| 现象 | 可能原因 | 检查方法 | 处理 |
|---|---|---|---|
| 波形全高 | 曝光过长或没接转移时钟 | 示波器看CCD输出脚 | 缩短积分时间,查时钟 |
| 波形全低 | 曝光过短或镜头盖未开 | 对准白纸看峰峰值 | 加曝光,查补光 |
| 中线跳动 | 阈值切到噪声 | 串口看二值化结果 | 改动态阈值系数 |
| 过弯冲出 | 帧率低或降速不够 | 看串口帧周期 | 提帧率,加降速系数 |
| 十字误判 | 线宽阈值太宽 | 看左右沿距离 | 收紧线宽阈值 |
| 舵机抖 | PID的kd过大 | 看偏差波形 | 降kd,加死区 |
最后一条实用技巧:把CCD固定在车头后,先用手推车走完整赛道,同时用串口记录每帧的中线和偏差,回到电脑上离线跑PID参数,比直接上路撞车快得多。
本文还有配套的精品资源,点击获取