做自动驾驶小车这几个月,我最大的体会是:比赛题目越炫,落到地上越要耐得住性子。第十六届研电赛Arm命题叫“自动驾驶,一起驶向未来!”,听着像科技发布会,拿到任务手册那一刻,看到的其实是模拟赛道地图、红绿灯模型、斑马线贴纸,还有一辆需要从零攒出来的小车。
这篇文章记录的是我们完成这个Arm命题的完整过程:怎么拆题、怎么选硬件架构、怎么调PID、现场条件会怎样坑你,以及那些在答辩PPT上看不到的教训。如果你准备参加研电赛、智能车竞赛,或者想入门嵌入式视觉与控制,这篇文章应该能帮你少走一大截弯路;就算只是好奇这类比赛在做什么,也可以看到“自动驾驶”被压缩到一条赛道上之后,到底是怎么跑起来的。
1. 命题拆解与整体设计思路
1.1 先看题目到底考什么
研电赛的Arm命题,本质上是ARM企业命题,题目会要求使用基于Arm架构的处理器,完成一个有明确场景的嵌入式系统。第十六届的“自动驾驶”也不例外:场地是一个模拟城市道路的赛道,车辆需要沿车道线行驶,遇到红绿灯要停车等待,遇到斑马线要减速,最后停在指定区域或完成相应任务。评分点很直白:完赛用时、是否压线、识别是否准确、停车精度、系统稳定性。
翻译成工程需求,就是三块:
- 感知:从摄像头图像里提取车道线、红绿灯、斑马线这些语义信息;
- 决策:根据感知结果决定速度怎么调、往左还是往右、要不要停车;
- 执行:电机、舵机、制动器要把决策准确执行出来,误差越小越好。
这个闭环看起来简单,难的是它必须在几十毫秒内反复完成,而且不能出错。跑得慢谁都会,跑得又快又稳才是分数差距的来源。
1.2 整体架构怎么选
我们最开始也想过:能不能用一块高性能MCU全部搞定?答案是能跑,但非常难受。图像处理要占用大量CPU时间,而电机和舵机的PWM控制需要稳定的小周期输出。一边做耗时长的图像计算,一边还要维持10ms周期的速度环控制,中断优先级稍微没调好,电机就容易一顿一顿,车跑起来跟踩刹车一样。
后来我们定下的方案是双核心:
- 主控端:基于Cortex-M7内核的MCU,负责电机控制、舵机控制、编码器读取、任务状态机调度;
- 视觉端:基于Cortex-A或高性能协处理器模块,负责摄像头采集、图像处理、赛道线提取、颜色识别;
- 两核之间通过串口通信,视觉端把“赛道偏差、目标速度、红绿灯状态”这些高层信息发给主控端。
这个分法和实际汽车行业的域控制器思路很像:一个域管感知,一个域管控制,中间通过总线通信。它带来的直接好处是控制周期稳定,视觉处理慢一点也不影响电机控制的实时性,调起车来特别舒服。
对比过几种主流方案后,我更推荐这种分工:
| 方案 | 实现难度 | 稳定性 | 扩展性 | 适合场景 |
|---|---|---|---|---|
| 单MCU全包 | 中 | 低 | 低 | 入门尝试 |
| 视觉模块+主控MCU | 低 | 高 | 中 | 多数参赛队伍首选 |
| MCU+Linux板 | 高 | 中 | 高 | 想做复杂识别或SLAM |
| 多MCU冗余 | 高 | 高 | 高 | 追求极致稳定性 |
1.3 为什么我们首选摄像头而不是红外循迹
以前智能车竞赛最经典的方案是红外灰度传感器排成一排,靠反射强度判断黑色车道线。这个方案简单、抗光照干扰强,我们在早期快速验证时也用过,但它有个致命短板:前瞻距离太近,车速一上来,弯道根本来不及反应。
摄像头方案的优势是前瞻距离远,能看到前方20到50厘米的路况,而且能直接完成红绿灯、斑马线这类语义识别,这是红外传感器完全做不到的。缺点也很明显:图像数据量大、处理需要算力、对光照敏感。
所以我们的策略是:摄像头为主传感器,红外灰度传感器作为备份和过线判断辅助,编码器负责速度反馈。三套信息来源互为冗余,任何一个传感器突然抽风,车都不会立刻失控。
2. 核心模块与关键算法实现
2.1 图像采集与赛道线提取
摄像头画面我们只取灰度图处理,分辨率定在160x120左右。为什么不用更高分辨率?因为赛道线提取只需要边缘信息,分辨率越高,处理越慢,对控制来说是负收益。视觉端的处理帧率反而比分辨率更重要,我们最后稳定在40到50帧。
赛道线提取的核心流程是:
- 灰度化:彩色图像转灰度,减少计算量;
- 二值化:把灰度图转成只有黑白两色的图,黑线变白、背景变黑,或者反过来;
- 列扫描:对图像每一列或隔几列扫描,找黑白交界点;
- 拟合中线:把左右边界的中点连起来,得到这一帧的赛道参考线。
这里必须强调二值化的“阈值”问题。我们最开始用固定阈值120,在实验室日光灯下跑得好好的,一搬到场地就废了——窗外阳光一照,阈值完全失效。后来换成大津法,也就是Otsu算法,根据整幅图像的灰度分布自动算阈值,适应性明显好很多。
核心扫描部分用C写出来大概是这样:
uint8_t get_line_center(uint8_t *image, int width, int height) { int col, row; int center_sum = 0; int valid_cols = 0; for (col = width / 4; col < width * 3 / 4; col += 2) { int left_edge = -1, right_edge = -1; for (row = height / 2; row < height; row++) { if (image[row * width + col] > 0) { if (left_edge == -1) left_edge = row; right_edge = row; } } if (left_edge != -1 && right_edge != -1) { center_sum += (left_edge + right_edge) / 2; valid_cols++; } } if (valid_cols == 0) return 0xFF; // 丢线 return center_sum / valid_cols; }这个函数输出的就是一个通过方向盘控制的偏差信号。丢了线要返回特殊值,让上层状态机去处理,而不是硬算出一个错误值。
2.2 从赛道中线到转向控制
有了中线偏差,接下来就是转向控制。我们用位置式PD控制器:偏差经过比例和微分运算后,映射成舵机PWM脉宽。
公式不复杂:
steer = KP * error + KD * (error - last_error)其中error是两个值:一个是图像中线相对车体中心的位置偏差,另一个可以是曲率估算。曲率的近似做法是对连续几帧中线做直线拟合,斜率越大代表弯越急。急弯时增大前馈补偿,转向会更早更快。
PD参数的经验值是:P先调到车能在直道上稳定走,再一点点加D抑制抖动。D过大时,舵机会像人得了帕金森一样高频抖动,轮胎磨地吱吱响,这时候就要把D缩回来。还需要注意舵机PWM的死区和中位,让舵机归中后车轮确实朝正前方。
2.3 速度闭环与弯道策略
速度控制我们用的是增量式PID,控制周期10ms。编码器用M法测速,也就是在固定时间窗口内统计脉冲个数,再换算成线速度。
增量式PID的实现很经典:
float speed_pid(float target_speed, float current_speed) { static float err[3] = {0, 0, 0}; float output; err[2] = err[1]; err[1] = err[0]; err[0] = target_speed - current_speed; output = err[0] - err[1]; output = KPS * output; output += KIS * err[0]; output += KDS * (err[0] - 2 * err[1] + err[2]); // 输出限幅 if (output > MAX_OUTPUT) output = MAX_OUTPUT; if (output < -MAX_OUTPUT) output = -MAX_OUTPUT; return output; }它输出的PWM增量叠加到当前占空比上,好处是启动平滑、不会积分饱和,适合嵌入式环境。
速度策略才是拉开差距的地方:
- 直道:加速,目标速度可以顶到每秒1.8米上下;
- 入弯前一个车身:根据曲率提前减速到安全速度;
- 出弯后:再恢复直道速度。
这个“提前”很重要。很多队伍弯道里再刹车,结果就是高速入弯直接冲出赛道。我们的做法是在状态机里维护一个“前方弯道曲率”缓存,提前两个控制周期把目标速度切下来。
2.4 红绿灯、斑马线这些交通元素怎么识别
在嵌入式环境里做交通元素识别,我建议先别上神经网络,先用颜色阈值+几何特征老老实实做。
红绿灯识别:摄像头画面是彩色的,转到HSL色空间后,红色在特定色相区间、亮度区间内非常集中。算出图像中红色区域的面积、外接矩形宽高比、在图像中的位置,就能判断是不是红灯。绿灯同理换一组阈值。
- 红灯亮:进入停车状态机,车停住,计时等待;
- 绿灯亮:正常行驶;
- 没识别到灯:保持上一状态。
斑马线识别:斑马线是连续的黑白条纹,在图像底部区域扫描,统计黑白交替的跳变次数,连续超过5到6次就判定为斑马线,然后执行减速流程。停车标志可以做成红色圆形区域检测,或者直接在赛道上利用色块触发。
这套基于规则的方案,识别速度快、逻辑可控、现场好调,对比赛场景完全够用。实验数据表明,在固定光照下识别准确率能到98%以上。
3. 从代码到赛道,调试流程与参数整定
3.1 先分模块打通,再合体联调
我们第一次调试就犯了“把所有东西一起调”的错:图像在抖动,电机在抽搐,舵机在乱摆,根本没法判断问题出在哪一套环节。后来总结出一套顺序,强烈建议照着来:
- 点亮最小系统:GPIO控制LED,确认主控能烧写、能跑;
- 电机开环测试:直接给固定PWM占空比,确认轮子转、方向正确;
- 编码器标定:用手匀速推车,看速度读数是否合理;
- 摄像头直出:把二值化后的图像实时显示出来,确认赛道线清晰可见;
- 提取赛道中线:把中线坐标发到上位机,配合远端测试;
- 转向闭环:手推车过弯,观察舵机打角方向和力度是否自然;
- 速度闭环:空载跑,看速度阶跃响应;
- 最后才把视觉、控制、状态机接在一起跑全赛道。
每一步都要有明确的验收标准,否则后边全是坑。
3.2 PID参数整定的实操经验
PID参数网上全是理论,我这里写点实操经验。转向PD先调P:从小往大加,车能稳定直行就停下来,再继续加就会开始左右摆。出现低频摆头,一般是P偏大或D不足;出现高频抖动,一般是D过大引入的噪声。
速度PID则反着来,先给一个中等目标速度,把P拉起来,让速度环响应快,再加入I消除稳态误差,D在这种电机系统里通常需要很小,尤其在编码器精度不高时,D大了反而会把量化噪声放大成抖动。
我把调试中常见的现象整理成了表格:
| 现象 | 可能原因 | 调参方向 |
|---|---|---|
| 直道左右摆头 | P过大 | 减小P,或增加D |
| 弯道转向不足 | P过小或前瞻太近 | 增大P,或提高图像前瞻行数 |
| 高速抖动 | D过大 | 减小D,检查编码器数据 |
| 速度起不来 | I太小或目标速度低 | 增大I,提高直道目标速度 |
| 停车点头 | 刹车减速过快 | 增加预减速距离,平滑目标速度 |
3.3 光照、曝光与白平衡
摄像头最怕的就是光照突变。我们调试时遇到过:同一块场地,中午和下午的太阳角度不一样,二值化效果就完全不同。
处理办法有两层。第一层是硬件:给镜头加偏振片,减弱地面反光;第二层是软件:用大津法自适应阈值,同时固定曝光时间和增益,不让自动曝光在行驶中反复跳变。自动曝光如果开着,过曝光和欠曝光会在画面里交替出现,赛道线提取结果也会跟着抖。
3.4 机械结构和电气系统同样不能忽视
很多人以为调好代码车就能跑,实际上机械和电气对稳定的影响一点不比算法小。我们整车底盘是四轮结构,前轮转向用舵机拉杆,后轮驱动用带减速箱的电机。
- 舵机中位必须校准,装舵臂时没对准,打角天生偏一边,算法怎么调都别扭;
- 电池电压会随电量下降,从8.4V降到7.2V,电机转速差别极大,需要用电压补偿或者闭环控制来吸收;
- 电机存在死区电压,占空比低于某个值时轮子完全不转,速度PID的输出如果落在这个区间,要先跳过死区。
4. 比赛现场的常见问题与应急预案
4.1 现场光照条件让人崩溃怎么办
比赛当天的场地光照我们提前一天去踩过点,但真正跑的时候,裁判区上方的强光灯一开,整个赛道表面的反射情况还是和我们调车时不一样。
我们准备了多套阈值参数,在视觉端存了“阴天模式”、“强光模式”、“灯光补光模式”,开赛前根据现场画面手动切一遍,测试跑一圈确认没问题再正式跑。另外在赛道上方加了一块遮光板,物理上减少环境光的直射,比纯软件扛光要实在得多。
4.2 程序卡死和跑飞怎么防
MCU跑着跑着死机、图像处理卡死导致控制中断,这是比赛翻车率最高的原因之一。我们做三层防护:
- 硬件看门狗,主循环里定时喂狗,一旦程序跑飞就自动复位;
- 关键状态机加超时判定,比如红灯等待超过设定时间就强制恢复行驶;
- 坏帧保护:图像数据校验失败时直接丢掉,而不是拿一帧烂数据去算控制。
这三层加完之后,连续跑50圈没有出现过一次死机。
4.3 对赛道规则的解读,差一点就完蛋
我们曾经对“压线”的理解错了。规则里说“车身投影不得超出赛道边缘”,我们以为是轮胎压线才算违规,结果测试时整车压着线跑了好几天。直到一次模拟评审提醒,我们才去重新研读裁判手册。
这件事的教训是:比赛规则一定要在动工之前逐条读懂,并且把关键条款打印出来贴在工位上。对判罚标准有疑问,第一时间咨询主办方或承办方,不要自己猜,猜错了浪费整个备赛周期。
4.4 遥测是联调的最后一根救命稻草
小车已经跑起来了,怎么知道它在想什么?靠看轮子猜根本不行。我们做了个最简单的遥测方案:主控通过串口把当前状态、目标速度、实际速度、舵机PWM值、图像偏差、看门狗复位次数全部发出来,通过USB转串口发到电脑上的上位机,实时画曲线。
有了曲线之后,很多问题直接一目了然。比如出弯时车速先掉下去再恢复,说明速度规划里的曲率预判不够;比如舵机响应滞后,就能看到PWM输出和实际转弯动作之间的时间差。强烈建议每个参赛队伍都做这套遥测,哪怕只是文本logger都行。
5. 完赛复盘与自动驾驶的延伸思考
5.1 我们的迭代时间线
整个备赛过程大约花了三个月,现在回头看:
- 第1到2周:读懂命题,确定双核心架构,搭好最小系统板;
- 第3到4周:摄像头采集和二值化流程跑通;
- 第5到6周:转向闭环能让人推着车走;
- 第7周:速度闭环跑通,车能自己走完半圈直道;
- 第8到10周:加入红绿灯、斑马线、停车任务,全赛道跑通;
- 最后两周:疯狂做鲁棒性测试,连续跑圈、换场地、换光照、换电池,把所有能想到的意外都模拟了一遍。
最致命的一次迭代是第9周:我们改了图像处理的行扫描范围,结果速度状态机没同步更新,导致车在第一个弯道直接直行冲了出去,把前保险杠撞坏了。从那以后,每次改参数都强制两个人确认,几乎成了我们的软性约束。
5.2 从竞速小车到真实自动驾驶的距离
做完这个比赛,我最直观的感受是:竞赛小车和真实自动驾驶在框架上是一致的,但复杂度差了不止几个量级。
赛道上我们的感知只做黑白线和红绿颜色,真实自动驾驶要把激光雷达点云、摄像头图像、毫米波雷达数据做多传感器融合;赛道上我们的决策是几条状态机和PID,真实场景需要做预测、规划、控制一体化;赛道上我们的失误代价是重跑一圈,真实场景里代价是生命。
但这不代表竞赛没有价值。恰恰是这种被简化后的环境,让我们能专注理解“感知-决策-执行”闭环本身是怎么运转的。车为什么走弯道会偏、为什么速度上去了反而更难控制、为什么传感器延迟对稳定性影响这么大,这些底层问题,在竞赛里体会得越深,将来做真实系统时就越有感觉。
5.3 给下届选手的话
如果让我给即将参赛的团队提建议,就六条:
- 组队不要只看会写代码的,控制、机械、视觉各配一个人,三样缺一不可;
- 硬件尽量用成熟方案,把时间留给算法和调试,别在底板上硬啃;
- 从第一天就用Git管理代码,每天提交,出了新问题可以随时回退到能跑的版本;
- 预留两套整车备件,电机、舵机、传感器、电池都买双份,现场坏了直接换;
- 比赛前两周就要停止加新功能,专心做鲁棒性和防呆测试;
- 最重要的一条:稳定完赛永远好过快但翻车,跑完没成绩的大神队伍每年都有很多。
这次比赛对我个人来说最大的收获不是名次,而是真正理解了工程上常说的一句话:自动驾驶的本质不是在理想条件下跑得有多快,而是不确定条件下还能不能做对决策。每一次现场翻车,本质上都是在帮你补上系统里漏掉的一个条件。如果你正在备赛,别急着追求速度,先把每一次跑圈的数据留下来,把每一个“莫名其妙”的问题查到底。等你的车能在不同光照、不同电池电量、不同赛道纹理下都稳定跑完时,你就已经完成这场比赛真正想让你学会的东西了。