从三月份拿到赛题说明到七月底站上正式赛道,同舟拾队在三号工坊里泡了整整四个月。中间换过两次机械结构、推翻过一版图像算法、熬过不知道多少个凌晨两点。最后单车拉力组的成绩不算顶尖,但这一路踩出来的坑、总结出来的方法,我觉得比名次本身更值得写明白。这篇文章相当于我们队的技术报告加实战复盘,覆盖整车方案、PID与角速度输出、摄像头图像处理、环岛十字识别,还有几个典型的调试事故现场,写给正在备赛智能车竞赛的队伍,尤其是准备走摄像头循迹方向的朋友。全文没有保留任何不适合公开的部分,能抄的作业我都直接给了。
1. 赛制理解与整车方案设计:拉力组拼的不是单圈极速
1.1 先读题再动手:拉力模式对整车提出的要求
我们拿到规则后做的第一件事,不是买器件,而是把赛题逐条读了三遍,把扣分点和加分点全部标出来。单车拉力组从名字就能看出两个关键词:单车和拉力。
"单车"意味着这是一辆独立完成全程的智能车,不允许双车协作,整车所有的感知、决策、控制都要自洽。"拉力"则直接决定了比赛形态——它更像是一场长距离计时赛,小车从起点出发,在限定时间内尽可能稳定地完成更多有效圈数。想明白这一点之后,我们很快就放弃了"把单圈极速刷到最高"的思路。道理很简单:拉力赛里一次环岛误判损失的时间,可能大于你在所有直道上省下的时间;一次冲出赛道直接报废,再快的单圈也是零。所以我们把调车优先级定成:稳定性 > 单圈速度 > 极速。后期我们甚至主动把目标速度从3.2m/s降回3.0m/s,换来的是正式比赛三圈全部完赛,而不是拿着第一圈破纪录的数据在半路翻车。
1.2 整车硬件架构和选型逻辑
硬件选型是在"性能"和"调试成本"之间找平衡。我们最终定下来的架构是这样的:
| 模块 | 选型 | 选择理由 |
|---|---|---|
| 主控 | 英飞凌 TC264 | 双核 200MHz,可以一个核跑图像、一个核跑控制,中断优先级好分配,外设丰富,社区资料最多 |
| 图像传感器 | MT9V03X 灰度摄像头 | 全局曝光、帧率可选,室内外适应性强,SCCB 接口可以配曝光和增益 |
| 电磁传感器 | 5 路工字电感 + 运放电路 | 近距可靠、抗光照干扰,用于坡道起跑线和近距离兜底纠偏 |
| 转向执行器 | S3010 舵机 | 响应快、力矩足,支持 PWM 频率范围宽 |
| 驱动 | 双直流减速电机 + 编码器 | 编码器用于速度闭环,双电机差速方案方便做直道修正 |
| 姿态参考 | 六轴 IMU | 陀螺仪提供角速度给内环做负反馈,加速度计用于坡道判断 |
| 电源 | 7.4V 锂电池 + DC-DC | 舵机和电机分开供电,模拟地和数字地单点连接,减少干扰 |
主控选 TC264 是个纠结过的决定。ST 的芯片资料多、上手快,但英飞凌 TC264 的双核架构对智能车这种"图像处理 + 实时控制"的负载结构太合适了。我们实际把图像采集和处理跑在 CPU0,把控制逻辑和通讯跑在 CPU1,两个核在大部分时间里互不抢占。代价是双核调试比单核复杂,核间通信用 Mailbox 要非常小心死锁,这一点后面调试实录里会提到。
1.3 双传感器融合:摄像头为主、电磁为辅
摄像头看得远,但怕强光、怕逆光、怕反光;电磁不受光照影响,但只能感知贴近赛道表面的信号。我们队的融合原则很朴素:默认信任摄像头,电磁只做两件事——一是为摄像头的结果做置信度投票,二是辅助确认坡道和起跑线。比如环岛入口的判据里加一条"对应侧电磁幅值连续 N 帧偏大"的约束,误判率就肉眼可见地降下去了。很多队伍喜欢让两个传感器各自出赛道线再做加权融合,我们试过一个星期就放弃了,因为两套系统的坐标系本身就差很远,融合错了不如不融。
2. PID控制链路:从图像偏差到角速度输出
2.1 控制链条上每个环节都在干什么
很多人一上来就问"PID 参数是多少",但先得想清楚你的 PID 输出到底是什么。图像处理给出的只是"车相对赛道中心线的偏差",这个偏差不能直接变成舵机 PWM。我们中间经过了三层:
- 方向偏差 → 期望航向角:用中线的回归斜率和近端偏移量拟合;
- 期望航向角 → 期望角速度:外环 PID 算出来的输出量纲是 °/s,这就是"智能车 pid 输出角速度"这个常见做法的核心;
- 期望角速度 → 舵机 PWM:内环拿陀螺仪实测角速度做负反馈,输出最终占空比。
这样拆开有个非常实际的好处:外环的输出量纲统一成角速度之后,不管车速是 2m/s 还是 3m/s,同一组 PID 参数都能用,不用每个速度档位重新调一套转向参数。而且角速度内环天然能压住舵机的高频抖动——因为抖动本质上就是角速度在来回振荡,内环反馈一强,机械惯性就被压下去了。
2.2 串级 PID 的实现与整定顺序
核心控制节拍我们用的是 1ms,图像节拍 10ms。伪代码大概长这样:
// 1ms 控制节拍 float center_err = get_center_offset(); // 图像中线偏差,单位 cm float target_yaw = yaw_from_err(center_err); // 线性映射 + 限幅 float target_omega = PID_outer(target_yaw - current_yaw); // 位置式外环,输出 °/s float omega_now = imu_gyro_z(); // 陀螺仪实测角速度 float pwm = PID_inner(target_omega - omega_now); // 比例为主的内环 steer_set(pwm);整定顺序必须由内到外,这个顺序错了会非常折磨人。我们实际操作的顺序是:
- 先只调内环:给定一个固定 target_omega,让 PWM 响应快且不过冲;
- 再调外环的 P:保证直道稳定、弯道能跟上;
- 然后补外环的 I:解决环岛内长时间同向转向时的稳态残差;
- 最后加内环的 D:抑制回正过冲,但 D 加多了会放大陀螺仪噪声,所以只给一个很小的值。
| 环路 | P | I | D | 备注 |
|---|---|---|---|---|
| 速度环 | 15 | 0.8 | 0.3 | 增量式,带积分限幅 |
| 外环(方向→角速度) | 2.5 | 0.05 | 0.5 | 位置式,目标限幅±120°/s |
| 内环(角速度→PWM) | 0.6 | 0 | 0.02 | 只用 P + 轻微 D,周期 1ms |
这组参数是我们车上调出来的起点值,直接抄过去大概率要重新调,但整定顺序是对的。积分项一定要做限幅,否则在环岛里持续同向转向,积分一饱和,出环的时候车会猛打反向,这个坑我们踩过。
2.3 速度闭环和转向的耦合
速度和转向不是两个独立系统。过弯必须减速,但我们不做"到弯里才减"的被动方案,而是用摄像头天然的前瞻性做预减速:根据图像中远端区域的曲率估计,提前 0.3 秒把目标速度降下来。实际实现很简单,把外环输出的角速度绝对值映射成一个减速系数,乘以直道目标速度就行。跑出来的效果是进弯前车已经稳了,弯心反而可以松油门,出弯直接加速。提速阶段最大的经验:先把转向调稳,再把速度加上去。顺序反了,你就会得到一台过弯必飞、换了无数参数都不知道该怪谁的车。
3. 图像处理链路:逆透视变换与边线提取的工程细节
3.1 采图与预处理:动态阈值是场地光照的救命稻草
摄像头部分我们用的是 MT9V03X 灰度图,分辨率跑 188×120,帧率 60。图像处理能省则省,TC264 虽然快但也不是无上限的。第一个难点就出在二值化上。固定阈值在室内开灯和下午阳光直射下完全是两回事,第一天试车就把我们教训了。后来改成区块动态阈值:把图像横向分成 6 块,每块统计灰度直方图,取双峰之间的谷底作为这块的局部阈值,块与块之间做线性插值避免分界线跳变。实测下来从傍晚到正午的强光变化,都不用改任何参数。
预处理顺序大致是:SCCB 配置相机曝光和增益 → 灰度图 → 轻量滤波 → 分块动态二值化。滤波这块有人觉得没必要,但我们的灰度图在阳光下会冒一些椒盐噪点,这些噪点会让后面的边线搜索出现孤点。加上一个 2×2 均值滤波之后,边线连续性好了一个档次。别小看这一步,它直接决定了后面所有算法的输入质量。
3.2 逆透视变换:为什么让赛道宽度固定
原始图像里赛道近大远小,近处赛道占几十行像素,远处只占两三行。直接在这个图上算中线偏差,会有一个系统性误差:弯道里远处的边线权重被严重低估,转向响应慢半拍。在环岛这样的大曲率元素上,这个误差会被放大到直接冲出去。
逆透视变换(IPM)的思路,就是把图像"摊平"成俯视图。假设赛道平面是平面,摄像头位置和姿态固定,那么图像像素坐标和地面坐标之间存在一个单应矩阵(Homography)关系。我们在 PC 上用标定布拍一张图,取四组以上对应点对求解 8 参数单应矩阵,然后离线把映射算成一张查表,下载到单片机里。运行时对每个像素查表得到地面坐标,再重新采样成俯视栅格图。
// 离线阶段:求解并生成查表 H = solve_homography(src_points, dst_points); // 8 参数 for (u = 0; u < W; u++) { for (v = 0; v < H; v++) { p = inv(H) * [u, v, 1]; // 地面坐标 map_table[u][v] = round(p); } } // 运行时:只做查表重采样 for each (u, v) birdview[ map_table[u][v] ] = gray[u][v];查表比运行时逐像素算矩阵乘法快大约 8 倍,这是能稳住 60 帧的关键。IPM 之后,赛道左右边线在俯视图上基本等宽,中线、曲率、宽度约束全部变得非常好做。需要提醒的是:IPM 标定一定要把摄像头固定牢,螺丝松 1mm,远处误差会放大到几十像素。我们吃过这个亏,后来每次上场前都要用标定图做一次校验,确认映射没偏。
3.3 边线搜索与中线生成
俯视图上的边线提取,我们用经典的"逐行横向搜索":
- 从最近一行开始,以上一行的左右边线位置为中心,向两侧搜索黑白跳变点;
- 加两个约束:左右边线点之间的距离必须落在合理赛道宽度范围内;单行新边线位置相对上一行不能跳变太多;
- 如果某行只找到一条边线,就用另一条边线加赛道宽度外推补线;
- 中线取左右边线中点。
最后从近端向上取连续 15 行中点做线性回归,回归斜率作为航向偏差,截距作为横向偏差。这么做比只用底部几行中点抗噪能力强很多,远端的抖动在回归里被平均掉了。经验之谈:俯视图里的赛道宽度会随标定误差波动,所以宽度约束不要写死一个值,保留 ±30% 的自适应区间,否则遇到小坡道或者场地接缝,整行都会丢线。
4. 特殊赛道元素识别与处理策略
4.1 环岛:状态机比一个复杂条件判断靠谱
环岛是拉力组最容易翻车的元素,因为它在图像特征上和普通大弯道太接近了。我们最后用三段式状态机解决:
- 入库判定:连续 5~8 帧满足"一侧边线曲率持续增大 + 对侧远端出现缺口/入口特征",并且对应侧电磁幅值持续偏大,才置位 IN 状态。N 帧过滤能滤掉瞬态误判,但 N 也不能太大,否则车已经错过入库点了;
- 绕行阶段:进入环岛后给外环一个较大的同向角速度参考值,同时用另一侧边线做纠偏,速度按弯道减速处理;
- 出库判定:检测到内侧边线重新出现直线趋势,或者目标方向从"持续转向"变为"近似直行",置位 OUT,恢复直道逻辑。
状态机的最大好处是每段状态的参数可以单独调,互不污染。比如入库误判了,我们只调 IN 状态的阈值,不需要动绕行参数。这就是"智能车环岛识别"这类热词背后真正的工程做法——不是训练一个大模型,而是把过程拆成可调试的状态。
4.2 十字路口:补线是核心中的核心
十字在图像上的典型表现是:左右边线在某一行的位置同时"断开",远端出现两条属于对面赛道的直边线。如果不管,中线会被错误地带到对面赛道去,车就会一头扎进错误方向。这就是"智能车十字补线"解决的问题。
我们的处理方式分四步:
- 检测到左右边线在连续几行内同时消失,判定进入十字区域;
- 补线:左侧丢线就用右侧边线位置减去赛道宽度得出左线,右侧丢线同理,两边都丢就沿用上一帧的赛道中心方向;
- 显式屏蔽"远端对面赛道"的边线点,不让它参与中线计算;
- 十字区域强制直行意图:中线的航向偏差暂时置 0,横向偏差由近端补出来的边线维护。
十字补线的核心认知是:不要试图理解十字,只要保证车笔直穿过去。我们有一版算法试图在十字里识别三条通路,结果引入大量噪声分支,跑起来反而更不稳。砍掉之后,车速还提了 0.2m/s。
4.3 坡道、断路和起跑线
坡道识别靠两个信号:图像上有效赛道行数突然减少(近端大片白色占据视野),以及加速度计 Z 轴分量明显变化。策略很简单:上坡前降速 10%,坡顶保持油门,下坡用速度环压住不要冲。电磁兄弟车队对坡道的判断更依赖电磁幅值突变,我们是摄像头为主、电磁做确认。
起跑线是一组黑白条纹,图像上表现为若干行交替跳变。检测到起跑线之后,速度环目标直接置 0,还要加停车确认计数,连续 20ms 稳定才下电抱闸,防止冲线。断路元素我们之前遇到过,兜底逻辑是:图像连续超过 15 行完全丢线且电磁信号也大幅衰减,就按记忆方向保持直行 1.5 秒,等图像恢复后再切回正常循迹。
5. 调试排错实录:三个把我们拖进深夜的 bug
5.1 直道上的高频晃动
现象:直道上车速一上 2.5m/s,车头开始以十几 Hz 的频率左右摆动,轮胎发热,声音很明显。排查链路如下:
- 先排除机械:舵机连杆、底盘螺丝全部重新打紧,问题依然存在;
- 怀疑供电:示波器看舵机电源,纹波正常;
- 打印图像中线:发现图像处理出的中线本身就带高频抖动,主要来自近端几行二值化的噪点;
- 给中线加一阶低通滤波:晃动明显减弱,但弯道响应变肉了;
- 最终解法是两层联动:图像侧给中线加 0.02s 时间常数的低通,控制侧把角速度内环的 P 调上去。中线稳定了,外环输出不再乱抖;内环强反馈把机械惯性直接压掉。
这个 bug 给我们的启发是:这类问题几乎不可能靠单一环节解决,它是"传感器噪声 + 控制环增益 + 执行器惯性"三者耦合的结果。排查时要先定位变量的来源,不要一上来就调 PID 参数,否则就是盲调。
5.2 偶发花屏与瞬间打满舵
现象:跑几十秒出现一次图像花屏或半幅错位,紧接着舵机猛地打满,车直接飞出去。这个 bug 排查了很久。
- 第一反应是接线问题,重新压排线、加磁环,故障照旧复现;
- 后来在 CPU0 里加了一个调试计数器,发现花屏总是出现在控制核抢占图像核的关键中断之后;
- 根因终于找到:行中断里我们用 DMA 搬运图像数据并更新行指针,而 PIT 控制中断在高优先级下随时抢占。被抢占的行数据没写完,下一行就开始了,整个行缓冲区错位;
- 处理方案:把 DMA 行完成中断提到最高优先级,关键更新行指针的代码加临界段保护,同时把非必要的调试串口中断降级;
- 修完连续跑了两个小时,再无花屏。
这个 bug 充分体现了 TC264 双核使用的门槛:核间共享资源(行缓冲区)必须做同步,不是简单分个核就万事大吉。你以为是硬件问题,最后发现是双核调度问题,这种排查过程本身就是备赛最值钱的收获。
5.3 环岛被当成普通弯道
现象:有一版参数下,车进环岛完全不减速,外侧车轮直接压上路肩。排查过程:
- 打开状态机日志,发现入库状态根本没置位,特征量没有达到阈值;
- 打印特征量曲线,发现"边线曲率持续增大"这个判据在大曲率弯道和环岛入口之间的区分度不够;
- 加了附加条件解决:需要"对侧缺口出现 + 曲率持续 N 帧 + 电磁对应侧幅值偏大"三个条件同时满足;
- 代价是入库判定帧数变长,车必须提前降速,否则容易错过入口;
- 最终用前瞻速度规划配合:根据图像远端曲率预估,在进入环岛前 0.3 秒就开始降速。
环岛这类元素,识别准了之后性能瓶颈会转移到"识别太准但反应太慢"上。判据、状态机、速度规划必须一起调,只调其中任何一个都会顾此失彼。
6. 备赛节奏与现场发挥:一些个人体会
6.1 版本管理和测试日志比想象中重要
四个月里我们没少走弯路,能走出来很大程度上靠每天的测试日志。每改一个参数,记录日期、代码 commit 号、速度档位、环岛是否误判、十字是否稳定、关键截图和备注。日志贴在工位墙上,每个人都能看到前一天改了什么、效果如何。没有这个习惯的队伍,调车经常出现"上周明明还行,这周怎么全废了"的尴尬,然后谁也说不清到底改了什么。我们后期凡是遇到回归问题,先查日志定位改动点,再二分回退,效率高很多。
6.2 上赛场前最后一个小时的三条规矩
我们给自己定了三条,执行到最后一天:第一,上场前必须在新赛道图上连续跑满 5 圈零失误;第二,任何临时改代码必须先在新赛道图上验证,否则不许上车;第三,正式比赛三次机会的分配是第一次求稳完赛,第二次冲成绩,第三次留给意外。事实证明这三条帮我们至少避免了两次因为临时改参导致的现场事故。
调车最难的地方从来不是算法本身,而是定位问题。先把问题稳定复现出来,再用二分法和日志逐步缩小范围,比急着改参数有用十倍。这是我们同舟拾队四个月备赛下来最深的体会,也是这篇复盘最想传给下一届队伍的东西。