
做自动驾驶小车这几个月我最大的体会是比赛题目越炫落到地上越要耐得住性子。第十六届研电赛Arm命题叫“自动驾驶一起驶向未来”听着像科技发布会拿到任务手册那一刻看到的其实是模拟赛道地图、红绿灯模型、斑马线贴纸还有一辆需要从零攒出来的小车。这篇文章记录的是我们完成这个Arm命题的完整过程怎么拆题、怎么选硬件架构、怎么调PID、现场条件会怎样坑你以及那些在答辩PPT上看不到的教训。如果你准备参加研电赛、智能车竞赛或者想入门嵌入式视觉与控制这篇文章应该能帮你少走一大截弯路就算只是好奇这类比赛在做什么也可以看到“自动驾驶”被压缩到一条赛道上之后到底是怎么跑起来的。1. 命题拆解与整体设计思路1.1 先看题目到底考什么研电赛的Arm命题本质上是ARM企业命题题目会要求使用基于Arm架构的处理器完成一个有明确场景的嵌入式系统。第十六届的“自动驾驶”也不例外场地是一个模拟城市道路的赛道车辆需要沿车道线行驶遇到红绿灯要停车等待遇到斑马线要减速最后停在指定区域或完成相应任务。评分点很直白完赛用时、是否压线、识别是否准确、停车精度、系统稳定性。翻译成工程需求就是三块感知从摄像头图像里提取车道线、红绿灯、斑马线这些语义信息决策根据感知结果决定速度怎么调、往左还是往右、要不要停车执行电机、舵机、制动器要把决策准确执行出来误差越小越好。这个闭环看起来简单难的是它必须在几十毫秒内反复完成而且不能出错。跑得慢谁都会跑得又快又稳才是分数差距的来源。1.2 整体架构怎么选我们最开始也想过能不能用一块高性能MCU全部搞定答案是能跑但非常难受。图像处理要占用大量CPU时间而电机和舵机的PWM控制需要稳定的小周期输出。一边做耗时长的图像计算一边还要维持10ms周期的速度环控制中断优先级稍微没调好电机就容易一顿一顿车跑起来跟踩刹车一样。后来我们定下的方案是双核心主控端基于Cortex-M7内核的MCU负责电机控制、舵机控制、编码器读取、任务状态机调度视觉端基于Cortex-A或高性能协处理器模块负责摄像头采集、图像处理、赛道线提取、颜色识别两核之间通过串口通信视觉端把“赛道偏差、目标速度、红绿灯状态”这些高层信息发给主控端。这个分法和实际汽车行业的域控制器思路很像一个域管感知一个域管控制中间通过总线通信。它带来的直接好处是控制周期稳定视觉处理慢一点也不影响电机控制的实时性调起车来特别舒服。对比过几种主流方案后我更推荐这种分工方案实现难度稳定性扩展性适合场景单MCU全包中低低入门尝试视觉模块主控MCU低高中多数参赛队伍首选MCULinux板高中高想做复杂识别或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管理代码每天提交出了新问题可以随时回退到能跑的版本预留两套整车备件电机、舵机、传感器、电池都买双份现场坏了直接换比赛前两周就要停止加新功能专心做鲁棒性和防呆测试最重要的一条稳定完赛永远好过快但翻车跑完没成绩的大神队伍每年都有很多。这次比赛对我个人来说最大的收获不是名次而是真正理解了工程上常说的一句话自动驾驶的本质不是在理想条件下跑得有多快而是不确定条件下还能不能做对决策。每一次现场翻车本质上都是在帮你补上系统里漏掉的一个条件。如果你正在备赛别急着追求速度先把每一次跑圈的数据留下来把每一个“莫名其妙”的问题查到底。等你的车能在不同光照、不同电池电量、不同赛道纹理下都稳定跑完时你就已经完成这场比赛真正想让你学会的东西了。