智能车摄像头循迹系统实战:从图像处理到PID控制与调试

智能车摄像头循迹系统实战:从图像处理到PID控制与调试 第21届智能汽车竞赛已经落下帷幕很多参赛队开始整理代码、设备、调试日志和赛后复盘。智能汽车竞赛虽然带有比赛属性但它本质上是一套完整的单片机嵌入式系统开发实践车模需要采集赛道信息、识别元素、计算控制量、驱动电机和舵机整个过程必须在几十毫秒甚至几毫秒内完成闭环。对于准备参加下一届比赛的学生来说最重要的不是记住某份现成代码而是理解一套智能车系统从环境搭建、模块实现、参数整定到现场排错的完整链路。这篇文章以一个典型的摄像头循迹车模为主线讲解智能车竞赛中常用的系统架构、软硬件环境、图像处理、PID 控制和现场调试方法。内容不会依赖某个特定车队或特定版本的主控板而是以常见的单片机平台为例说明实现思路。你把文中示例迁移到自己队伍的开发板上时只要替换引脚、寄存器库和部分底层接口即可。1. 先搞清智能车系统的组成和运行流程1.1 智能车系统整体模块一辆能在赛道上快速稳定行驶的智能车通常由以下几个模块组成车模底盘包括电机、舵机、轮胎、悬挂和车架。常见车模有 A 车、B 车、C 车、L 车等型号不同车模适合不同组别。主控板负责传感器数据采集、控制算法执行和输出 PWM。常用主控有 STM32 系列、英飞凌 TC264、灵动微、逐飞等品牌具体取决于赛题要求和队伍习惯。传感器模块用于识别赛道和元素。摄像头组使用摄像头采集图像电磁组使用电感采集磁场信号也有组别会结合编码器、陀螺仪、加速度计等。驱动模块包括电机驱动芯片、舵机驱动电路和稳压电源。电池电压经过稳压后给主控、传感器和驱动模块供电。调试接口串口、无线蓝牙、WiFi 或 SD 卡用于实时输出摄像头图像、控制量和运行日志。这些模块不是简单连接就结束。主控板需要在一个固定周期内完成“采集-处理-控制-输出”的闭环。一个典型的控制周期是 10ms 到 20ms。在这个周期内摄像头需要完成一帧图像采集和二值化控制算法需要根据赛道中线计算出偏差然后输出舵机转角 PWM 和电机速度 PWM。1.2 不同组别对传感器和控制要求的影响智能汽车竞赛每年会设置不同组别比如摄像头组、电磁组、完全模型组、越野组、独轮组等。组别不同传感器选型和控制策略差异很大。组别类型常用传感器控制重点调试难点摄像头组摄像头、编码器、陀螺仪图像识别、中线提取、差速转向图像抖动、逆光、曝光不足电磁组电磁电感、编码器磁场信号采集、归一化、差比和转向电磁值漂移、近场干扰平衡组陀螺仪、加速度计、编码器角度环、速度环、直立控制零点漂移、参数耦合越野组摄像头、GPS、编码器路径规划和速度控制地形变化、GPS 信号不稳无论哪个组别底层思路都类似测量当前状态和期望值计算偏差再通过控制算法让执行机构修正偏差。区别在于传感器数据形态和控制律复杂度。摄像头组处理的是二维图像电磁组处理的是多路 ADC 数值平衡组处理的是姿态角度和角速度。1.3 智能车运行的主状态机实际代码不能只写一个死循环。更合理的做法是把整车运行状态拆成多个阶段初始化初始化时钟、GPIO、PWM、ADC、摄像头、串口和中断。自检检查电池电压、按键状态、传感器是否连接正常。准备出发等待发车信号通常用按键或无线触发。运行循环执行图像采集、元素识别、速度控制、转向控制。停止检测到终点或人为干预后电机停止保留日志。这段状态划分非常重要。很多队伍在比赛现场发车时程序崩溃原因就是因为没有做初始化完成标志也没有等待稳定延时摄像头还没输出图像就直接进入了主循环。2. 环境准备从开发环境到最小系统验证2.1 软件开发环境选择智能车常用开发环境包括Keil MDK适合 STM32、NXP 等 ARM 内核单片机工程配置简单调试器支持好。IAR Embedded Workbench适合多种芯片编译优化好但工程配置较繁琐。VS Code GCC 工具链适合偏软件工程习惯的队伍但需要自己维护构建脚本。官方厂商 IDE比如英飞凌的 AURIX Development Studio、NXP 的 MCUXpresso IDE。对于新队伍建议从 Keil MDK 或官方 IDE 入手。它们对芯片启动文件、链接脚本和下载器支持最完整可以减少很多环境问题。环境安装完成后先做一次点灯测试。以 STM32 为例最小工程需要包含启动文件startup_stm32f10x_hd.s。系统时钟初始化文件system_stm32f10x.c。外设驱动库或 HAL 库。链接脚本STM32F103C8Tx_FLASH.ld。如果你使用的是逐飞库、龙邱库或自己封装库也要先确认库版本和芯片型号匹配。这里的关键点不是代码多复杂而是 GPIO 输出和串口输出能不能正常工作。2.2 串口输出和调试器连接串口是智能车调试最重要的通道。推荐在工程初始化完成后直接输出一行版本信息void debug_init(void) { uart_init(115200); printf([BOOT] Kalman ready. Build %s %s\r\n, __DATE__, __TIME__); }这段代码的作用是确认三件事主控已经正常运行。串口引脚接线正确波特率匹配。至少一个基础外设已经初始化成功。如果串口没有输出优先检查串口 TX 引脚是否接对、共地是否完成、波特率是否一致、芯片时钟是否起振。不要先怀疑程序逻辑先确认最小系统。2.3 稳压电源和电池电压检查车模使用电池供电时电压会随着负载快速变化。大多数单片机需要 3.3V 或 5V 稳压。推荐在上电前用万用表检查电池空载电压是否满足标称范围。稳压模块输出是否稳定在 3.3V 或 5.0V。电机驱动是否单独供电避免大电流导致主控复位。电源指示灯是否正常点亮。常见错误是电机驱动和主控共用一个电源芯片电机启动瞬间电压跌落主控直接复位或者摄像头图像变花。正确的做法是给电机驱动使用低压差、大电流的稳压模块并给主控传感器使用独立稳压或至少加大电容。2.4 最小闭环验证用按键控制舵机在接入摄像头之前先验证执行机构是否正常。可以写一个最简单程序while (1) { if (key_scan() KEY_PRESSED) { servo_pwm_set(750); // 中值 motor_pwm_set(1000); // 给定一个较小速度 } }这个例子的意义在于验证 PWM 波形、舵机电源和电机驱动是否工作。如果舵机没有反应用示波器或逻辑分析仪看 PWM 的频率和占空比是否正常如果电机不转检查 PWM 通道和方向引脚是否配置正确。注意不要一开始就把控制算法写进主循环。先确认每个硬件模块单独可控再逐层叠加逻辑。否则以后出现问题你不知道是传感器问题还是执行机构问题。3. 核心模块实现传感器采集、路径识别与控制量计算3.1 摄像头图像采集与二值化摄像头组最常用的传感器是灰度摄像头常见型号有 MT9V03X、OV7725 等。单片机通过 DVP 接口或 SPI 接口获取图像。原始图像通常是灰度图每个像素 8 bit。为了方便赛道识别一般先对图像做二值化把灰度值大于阈值的像素设为白色小于阈值的设为黑色。一个通用的图像采集流程如下摄像头输出 VSYNC 场中断信号表示一帧图像开始。单片机开启 DMA 接收图像数据。接收完成后在图像数组里进行二值化。根据二值图提取赛道边界和中线。保存一帧灰度图像的代码一般是#define IMG_W 188 #define IMG_H 120 uint8_t image_gray[IMG_H][IMG_W]; uint8_t image_binary[IMG_H][IMG_W]; void image_binary(uint8_t threshold) { for (int row 0; row IMG_H; row) { for (int col 0; col IMG_W; col) { if (image_gray[row][col] threshold) image_binary[row][col] 1; else image_binary[row][col] 0; } } }二值化看起来很简单但阈值太小会把赛道边缘噪声引入阈值太大会把赛道中间的高光当成丢线。更推荐使用动态阈值比如大津法Otsu根据整幅图像的灰度分布自动计算阈值。很多比赛现场的光线会突然变化比如灯光频闪、地面反光、阴影。如果使用固定阈值很可能出现同一辆车在实验室能跑、到赛场就冲出赛道的问题。动态阈值是摄像头组公认的基础优化。3.2 从图像中提取赛道中线二值化之后我们需要知道当前赛道在图像中的位置。常见做法是逐行扫描找出每一行的左右边界然后计算中线。for (int row 10; row IMG_H; row) { int left -1; int right -1; for (int col 0; col IMG_W; col) { if (image_binary[row][col] 1) { left col; break; } } for (int col IMG_W - 1; col 0; col--) { if (image_binary[row][col] 1) { right col; break; } } if (left ! -1 right ! -1) { center[row] (left right) / 2; } else { center[row] -1; // 该行丢线 } }这里的center[row]就是当前第row行的赛道中心线像素列坐标。把每一行的中心点和图像中心IMG_W / 2做差就能得到横向偏差。实际程序里通常用近处几行中心的平均值作为当前偏差用远处几行来估算弯道方向。只使用近处会导致转向迟滞只使用远处会导致弯道入弯太猛。3.3 电磁传感器信号与差比和算法电磁组识别赛道依靠的是导线中的交变电流产生的磁场。车模上一般放置水平电感和竖直电感通过 ADC 读取电感感应电压。原始 ADC 值受距离、车身高度、供电电压影响很大所以要先归一化。归一化公式float normalized (raw_value - min_raw) / (max_raw - min_raw);min_raw和max_raw需要在赛道上提前采集。如果没有提前采集也可以使用滑动窗口动态更新。转向偏差常用“差比和”计算float error (left_value - right_value) / (left_value right_value);当车在赛道正中时左右电感值接近error接近 0。当车偏右时左边电感值变大error为正控制器就会把车往左转。差比和的优点是抗共模干扰。如果两个电感同时受环境磁场影响比值的变化会小于绝对差值的变化。缺点是当两边电感值都很小时分母接近 0误差会放大所以需要加一个最小值保护。float error_safe (left_value - right_value) / (left_value right_value 1e-6f);3.4 舵机转向 PID 控制舵机控制使用位置式 PID 比较直观float pid_servo_calc(float error) { static float integral 0; static float last_error 0; integral error; if (integral INTEGRAL_LIMIT) integral INTEGRAL_LIMIT; if (integral -INTEGRAL_LIMIT) integral -INTEGRAL_LIMIT; float derivative error - last_error; last_error error; return kp * error ki * integral kd * derivative; }这里有三组参数kp决定转向对偏差的敏感程度。kp太小时过弯反应慢太大时车子会蛇形摆动。ki用来消除稳态误差。但智能车赛道偏差变化快积分用多了容易超调一般只给一个较小值或者直接不用积分。kd用来抑制偏差变化率。适当增大kd可以让车子更早对大弯做出反应但太大时会产生高频抖动舵机容易发烫。实际输出时需要把 PID 计算值映射到舵机 PWM 占空比范围内。uint16_t servo_pwm SERVO_MIDDLE (uint16_t)pid_output; servo_pwm clamp(servo_pwm, SERVO_MIN, SERVO_MAX);SERVO_MIDDLE是舵机中值需要通过实测获得。不同舵机、不同供电电压下中值可能不一样。跑车之前先用按键或串口指令把舵机打到中值确认前轮方向不是偏的。3.5 电机速度 PID 与差速控制电机控制通常采用速度闭环。编码器采集电机转速PID 把实际速度和目标速度做差得到 PWM 输出。速度环主要在启动和刹车时起作用。对于两驱车转向时还可以给内侧轮和外侧轮不同的速度也就是差速。差速可以让车在过弯时更顺滑。一个简单方法是float speed_left target_speed - steer_diff; float speed_right target_speed steer_diff;其中steer_diff可以由舵机 PID 的输出映射得到。过弯越大内外轮速度差越大。但如果差速太激进出弯时车身姿态反而不稳定需要配合编码器速度闭环来做限制。4. 参数调试与赛道元素处理4.1 PID 参数整定流程不要一开始在整圈赛道上调参数。推荐按下面的顺序先把舵机中值和正反向调正确。让车在直道上低速跑调整kp让车能沿直道稳定行驶。在弯道入口手动推车观察偏差变化确定kd的大概方向。逐步提高目标速度观察过弯时是转向不足还是转向过度。如果车身抖动减小kp或增大kd如果过弯太久增大kp。调参过程中必须记录每次修改的参数和结果。建议把参数做成一个结构体通过串口命令在线修改而不是每次改代码重新烧录。最少要有这样一组参数typedef struct { float kp; float ki; float kd; float target_speed; uint8_t image_threshold; } VehicleParam; VehicleParam param { .kp 1.2f, .ki 0.0f, .kd 8.0f, .target_speed 3.0f, .image_threshold 120, };在生产级别的代码里还应该把参数保存到 Flash避免断电丢失。比赛前用 U 盘或无线模块拷贝参数时你会发现这个设计非常省时间。4.2 常见赛道元素判断与处理智能车赛道通常包含十字路口、环岛、坡道、路肩、断路等元素。每个元素都要单独写状态判断和恢复逻辑。以十字路口为例最常见的问题是把十字误认为弯道。因为十字路口的左右边界会突然消失或出现大缺口。简单处理方法是如果图像中间行左右边界都缺失并且连续多帧都出现则进入十字状态。进入十字后保持上一次的转向方向或直接按当前偏差继续循迹。当左右边界恢复后退出十字状态。环岛则更复杂。入环时需要识别环岛入口进入环岛后要沿环岛内圈行驶出环时需要识别出口。部分队伍会用状态机来处理void island_state_machine(void) { switch (island_status) { case NO_ISLAND: if (is_island_entrance()) island_status ENTERING; break; case ENTERING: calculate_island_center(); if (is_fully_in_island()) island_status INSIDE; break; case INSIDE: track_inside_wall(); if (is_island_exit()) island_status EXITING; break; case EXITING: track_normal_line(); if (is_line_recovered()) island_status NO_ISLAND; break; } }元素判断最重要的是“状态切换条件”。条件太松会误触发条件太紧会漏检测。建议在调试阶段把当前状态变量、置信度计数通过串口传输到上位机实时观察状态切换时机。4.3 日志记录与上位机回放智能车跑起来很快人眼很难看清每一帧图像和控制量。所以在调试阶段必须把关键数据记录下来。记录方式有三种串口实时发送适合低速调试波特率建议 115200 或更高。无线蓝牙/WiFi适合赛道现场动态调试但可能有延迟和丢包。SD 卡或 Flash 存储适合高速跑完一圈后离线分析不丢数据。推荐的日志内容包括帧号和时间戳。当前状态机名称或编号。最近 N 行赛道中线偏差。舵机 PID 输出。左右电机速度。目标速度和当前速度。二值化阈值。上位机可以使用 VOFA、Serial Plot、匿名上位机或者自己用 Python 写一个简单的串口可视化。观察偏差曲线和 PID 输出曲线是最有效的调参方式。比如看到偏差从 0 突然跳到 50而舵机输出没有同步变化那说明图像处理阶段丢了帧或者控制周期太长。5. 竞赛现场常见问题与排查路径5.1 现场问题排查总表现场调试时间很短不能盲目试。下面这张表可以作为排查起点问题现象常见原因检查方式处理建议上电后主控无反应电源未接通、稳压损坏、芯片烧录失败量电池电压、稳压输出看电源灯先换 USB 供电确认最小系统按键发车没有反应GPIO 引脚接触不良、按键读到一直为高串口打印按键状态加内部上拉检查共地电机不转但舵机正常PWM 通道配置错、方向引脚反逻辑、驱动板故障用示波器看 PWM量电机两端电压单独测试开环 PWM电机抖动速度 PID 过强、编码器接线错误、供电不足看编码器数据是否突变降低目标速度断开速度环先开环测试摄像头图像满屏花摄像头排线松动、时钟配置不对、DMA 长度错误查看行中断和场中断频率重新初始化摄像头并检查 VSYNC图像过曝或全黑摄像头增益问题、曝光时间过大调用摄像头寄存器调整曝光在室内固定光源调一遍参数车身左右摆动转向 PID 的 kp 过大或 kd 过小查看偏差点和舵机输出曲线减小 kp增大 kd大弯转向不足kd 过小或远行参考点不可靠查看弯道中线提取结果增加前瞻距离或增大 kd出环岛后冲出赛道状态机退出条件错误回放环岛段日志调整出口识别条件增加恢复延时5.2 上电不复位陷阱现场最常见的问题是电池刚接上时电机突然转一下然后主控复位。这是因为电源刚建立时单片机 IO 状态不定电机驱动芯片输入引脚悬空导致驱动输出高电平。解决方案是增加“上电延时锁死电机”逻辑void motor_safe_init(void) { motor_pwm_disable(); for (volatile int i 0; i 100000; i); motor_pwm_enable(); }在上电后的一段时间里禁止 PWM 输出等所有外设初始化完成后再使能电机。更好的做法是使用电机驱动芯片的使能引脚默认拉低软件初始化完成后再拉高。5.3 摄像头现场花屏或丢帧现场电磁环境复杂摄像头排线长且没有屏蔽时容易丢帧。先确认摄像头场中断是否稳定。很多主控可以在场中断中翻转一个 GPIO然后连接 LED 看闪烁频率。如果图像偶尔整体偏移很可能不是摄像头坏了而是 DMA 接收的一帧图像不是从场有效开始。可以把 VSYNC 中断触发方式从上升沿改为下降沿或者在帧起始时丢弃前几行。如果图像有规则条纹可能是摄像头时钟 PCLK 配置太高或者引脚间串扰。适当降低 PCLK并使用短一点的排线。5.4 过弯抖动的参数排查顺序过弯时车身抖动不要第一反应是加大 PID。先按这个顺序排查检查舵机中值是否正确左右打角是否对称。用编码器数据看左右轮速度是否一致。回放偏差曲线看偏差是否因为图像丢线而带来毛刺。如果偏差曲线平滑再调 PID。如果偏差曲线有突变先优化图像提取不要用 PID 去补偿图像噪声。很多队伍在图像有毛刺时盲目增加 kd结果高频噪声被放大车身抖动更严重。正确做法是先平滑偏差比如对连续几帧偏差取滑动平均然后再进 PID。6. 从参赛到工程化代码规范、调试清单与赛后复盘6.1 代码模块化与参数外置临比赛前代码往往已经改得面目全非。建议从第一天开始就按模块划分文件project/ ├── main.c ├── control/ │ ├── pid.c │ ├── servo.c │ └── motor.c ├── sensor/ │ ├── camera.c │ ├── encoder.c │ └── adc.c ├── strategy/ │ ├── track_state.c │ ├── island.c │ └── cross.c ├── debug/ │ ├── uart.c │ └── logger.c └── config/ └── vehicle_config.hvehicle_config.h用来统一存放可调参数#define PI_KD 8.0f #define PI_KP 1.2f #define IMG_THRESHOLD 120 #define SERVO_MIDDLE 750不要让每个模块自己去修改参数。比赛现场调参时只需要打开这个文件就可以一次性调整大部分行为。6.2 参数备份和版本管理建议用 Git 管理代码。不要只在本地保存一份至少要有 U 盘和云仓库两个备份。比赛前经常出现“今天改了参数车子不如昨天快但忘了改了什么”的情况。解决办法是每次调参后把修改前的参数和修改后的参数记录在param_history.md中。每次代码改动提交一次 commitcommit message 写清楚改动点和测试结果。比赛前一天不要做大改动只保留已经验证过的版本。6.3 比赛前检查清单比赛现场忙碌时一张检查清单非常有用检查项操作结果电池电量实测电压是否在额定范围是/否轮胎胎压是否一致、有无磨损是/否镜头镜片是否清洁是/否摄像头排线是否插紧、有无弯折是/否编码器线是否松动是/否主控程序版本是否和调参记录一致是/否参数备份是否已复制到 U 盘是/否发车测试是否在赛道试跑至少一圈是/否备用电池是否已充满是/否工具包螺丝刀、扎带、胶带、万用表是/否这张表可以提前打印出来比赛前逐项打勾。能有效减少低级失误。6.4 赛后复盘不要只复盘成绩比赛结束不是终点而是下一轮技术提升的起点。建议赛后整理三份材料技术复盘哪些模块不稳定哪些代码临场修改过为什么改。数据复盘把比赛中保存的日志和调参记录重新回放找出速度损失点。管理复盘每个人的分工是否清晰、硬件备件是否充足、现场沟通是否顺畅。智能车竞赛最有价值的不是比赛名次而是真正理解了“一个复杂系统如何从零到一、如何在有限时间内被工程化”。下一届队伍如果能看到这些复盘材料起步速度会快很多。第21届智能汽车竞赛已经结束接下来的时间属于整理和沉淀。无论最终成绩如何把环境、代码、参数和调试经验保留下来才是这次比赛最值得带走的成果。对准备参加下一届比赛的队伍来说不妨从今天开始搭建自己的一套工程模板把一个一个模块跑通再逐步往上叠加复杂策略。只要最小闭环稳定后面所有优化都有基础可依。