ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

线性CCD智能小车循迹:STM32F103与PID控制实战

线性CCD智能小车循迹:STM32F103与PID控制实战 简介围绕线性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控制周期适合场景STM32F10312位支持定时器触发DMA1ms以内线性CCD智能小车主控51单片机多数无ADC需外扩5ms以上低速循迹、教学Arduino10位阻塞式读取2ms到5ms算法验证、快速原型2.3 线性CCD驱动与ADCDMA采样最小代码下面以STM32 HAL库为例思路是用定时器更新事件触发ADC再让DMA把一帧像素搬到数组。CCD的转移时钟由另一个定时器PWM输出这里不展开寄存器配置只保留采样主干。// STM32F103 线性CCD定时器触发ADCDMA搬运一帧像素 #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[i1] 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到1PID参数才好整定。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/s100Hz3ms5ms调试阶段1.0m/s200Hz2ms3ms稳定循迹1.5m/s300Hz1ms2ms需要补光2.0m/s500Hz0.5ms1ms直道多、弯道少注意帧率不是越高越好曝光时间缩短后信号变小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参数比直接上路撞车快得多。本文还有配套的精品资源点击获取
返回列表