要说智能车竞赛里最考验“多系统配合”的组别,蚂蚁搬家绝对算一个。它不是单纯比速度,而是把识别、抓取、搬运、放位这一串动作全部串起来,稍微有一个环节拖后腿,整圈时间就崩了。很多队伍在这个组别纠结主控芯片选NXP还是STC,我们刚开始也在这个问题上反复横跳,最后得出的结论是:不要二选一,让两颗芯片各干各的最擅长的事。NXP-MicroPython负责决策,STC单片机负责实时执行,两者配合好了,搬运效率能比单芯片方案稳定不少。这篇就完整复盘一下我们这套协同架构,从赛题拆解、代码分工、通信协议,到调试时踩过的那些坑,一次性讲清楚。
1. 蚂蚁搬家组到底考什么——从赛题拆出“看、想、动、稳”四个环节
很多队伍一看“搬运”两个字,就以为这是机械结构的主场,拼命堆舵机和机械臂,结果代码层面经常打架。我的经验是先别急着写代码,把赛题拆成具体的能力项,再决定用几个处理器、每颗芯片负责什么。
1.1 赛题里的三个核心子任务
蚂蚁搬家组无论具体赛规怎么变,核心任务一般离不开这三件事:从物品区把指定物体取走、搬运到目标区域、最后精准放位。听起来简单,但展开后就复杂了。取物阶段需要识别物体位置和类型,可能是靠颜色、形状标记区分;搬运阶段要保证小车走线又快又稳,电机轮速还得随时响应路径变化;放位阶段则需要慢速、对准、避免物体掉落,机械臂或推杆的动作要跟车体姿态配合上。
这三个子任务对硬件的能力要求完全不一样。识别可以接受几十毫秒的延迟,但抓取动作一旦触发,舵机和电机必须在几毫秒内响应,否则物体位置一偏就抓空。路径规划可以慢慢算,但轮速PID必须每个控制周期都稳定执行,不能让MicroPython的垃圾回收打断。这就是“分工”的根本原因。
1.2 双芯片分工的边界在哪里
我们最终采用的架构是:NXP平台跑MicroPython,作为决策层;STC单片机作为执行层。NXP负责摄像头或光电传感器的数据读取、物体识别、任务状态切换、路径规划这些“慢逻辑”;STC负责PWM输出、编码器计数、舵机控制、传感器边沿捕捉、PID运算这类“快逻辑”。
实际分配链路是这样的:NXP每次状态机切换时,通过串口向下发一条结构化命令;STC收到命令后,在当前控制周期内立刻执行具体的动作序列,比如“左轮速度40,右轮速度40,舵机到达120度”。执行过程中STC不会等NXP逐帧发指令,而是自己维护一个微秒级的时间表。这样即使上层MicroPython偶尔卡顿几十毫秒,车也不会失控。
1.3 为什么单芯片方案容易在蚂蚁搬家组翻车
用单一NXP跑MicroPython做全部事,最大的问题是实时性不可控。MicroPython的垃圾回收机制会在某个时刻暂停代码执行,如果你让它在那个瞬间去响应编码器中断或舵机PWM更新,动作就会顿一下。用纯C写NXP当然可以,但对大部分学生队伍来说,迭代速度太慢,视觉和流程逻辑开发周期会拉得很长。
单用STC则反过来,它擅长实时控制,但跑复杂的视觉识别、状态机,或者做稍微大一点的图像缓存处理,就非常吃力。STC的RAM往往只有几KB,存一帧小分辨率图像都勉强,更别说跑识别算法。所以最优解不是二选一,而是让两颗芯片的优势互补。我们的分工表格放在下面,方便对照。
| 能力维度 | NXP-MicroPython决策层 | STC执行层 |
|---|---|---|
| 视觉/识别 | 支持摄像头数据读取、色块识别、模板匹配 | 不适合,RAM和算力有限 |
| 路径规划 | 可维护状态机、航点列表 | 只执行具体指令 |
| 电机/舵机实时控制 | 不适合,受GC和解释执行影响 | 微秒级定时PWM、中断响应稳定 |
| 传感器消抖/编码器计数 | 有延迟风险 | 用外部中断或定时器捕捉,精准 |
| 开发迭代速度 | 快,MicroPython改逻辑方便 | 偏慢,但逻辑简单,改动不大 |
这张表是我们多轮测试后的真实结论,后面每个环节都会围绕这张表展开。
2. NXP-MicroPython决策层的工程实现——状态机、识别与内存驯化
确定了分工之后,NXP上跑什么、怎么跑,就成了决定整车智力的关键。我们的经验是:不要把NXP当成万能大脑,它的优势是逻辑表达清晰,劣势是性能和内存都有限,所以决策层代码必须做得非常“克制”。
2.1 任务状态机:待机-识别-抓取-搬运-放位-复位
MicroPython最舒服的编程模型就是状态机。我们定义了一个简单的枚举状态,主循环不断轮询当前状态,并根据传感器和通信结果跳转。这样写代码有一个好处:每个状态之间边界清楚,出问题的时候可以靠串口日志精确看到车卡在哪个环节。
一个典型的搬运循环是:上电后进入IDLE等待比赛信号;信号给到后进入SCAN状态,摄像头开始找目标物体,识别到就记录坐标和种类;接着进入PICK状态,向STC发送机械臂下降和夹爪闭合指令,同时根据物体位置做小幅车前移修正;抓到后进入MOVE状态,向STC发送左右轮差速数据,配合车头方向把物体搬运到目标区域;到了目标区域附近切换为PLACE状态,发送舵机放位动作;最后进入BACK状态,回到起点等待下一次搬运。整个循环看着简单,但真正跑起来后,最容易出的问题不是逻辑本身,而是状态切换时的时序等待。我们后来在每个状态入口都加了超时保护,比如SCAN超过3秒没识别到物体就自动降低车速重新扫描,防止小车原地死等。
2.2 视觉识别的选型与阈值标定
蚂蚁搬家组识别物体不一定要上很重的神经网络,大多数情况下用摄像头做颜色阈值分割就够了。我们用NXP接了一个摄像头模块,MicroPython里每次抓一帧图像,然后做颜色阈值过滤,提取目标色块的包围盒中心点。标定的过程比想象中磨人:不同光线下同一个红色的HSV阈值差别很大,我们在比赛前专门花了半天在赛场实地光线下标定,保存了三套阈值参数,在程序里根据当前环境动态切换。
这里要提醒一个容易被忽略的点:摄像头广角畸变。如果用广角镜头,画面边缘的物体位置误差很大,这时候再准的阈值也白搭。我们用了一个很土但有效的办法——在画面上画参考网格,把车放在不同位置,记录物体实际坐标和像素坐标的映射表,做简单的线性插值矫正。MicroPython里处理这种映射表非常方便,用数组一存就行,不用写复杂的相机标定代码。
2.3 MicroPython内存驯化:预分配、GC控制与帧率取舍
MicroPython跑在NXP上最大的坑是内存碎片和垃圾回收延迟。我们遇到过几次诡异的卡顿,后来定位到都是GC在作祟。解决办法有三条,非常实用。
第一,启动时预分配大缓冲区。比如把图像缓存、物体坐标数组、串口接收缓冲区全部在初始化阶段一次性分配好,避免运行中反复新建大对象。第二,手动控制垃圾回收时机,不在控制循环中间让GC自动触发。我们在关键时刻帧抓取前主动调用gc.collect(),把这个必然发生的暂停放在无关紧要的时间点。第三,降低帧率换取稳定性。视觉识别不需要每帧都处理,我们实际测试下来,每秒5到10帧完全足够蚂蚁搬家的场景,识别太快反而会增加误判和内存压力。这三点做到后,MicroPython决策层的运行稳定度提升非常明显。
3. STC执行层的硬实时控制——PWM、编码器与传感器去抖
如果说NXP是车的大脑,那STC就是小脑加脊髓。蚂蚁搬家组对动作执行的要求很高,机械臂下降多少、电机转几圈、舵机在哪个角度停留多久,都需要非常确定的时间控制。这部分用STC来做,可以说就是它的主场。
3.1 为什么电机闭环和舵机PWM要交给STC
电机驱动最怕的是PWM信号偶尔断一下或延迟一拍。STC的定时器可以做到微秒级中断,PWM输出稳定,不依赖操作系统的调度。我们用STC8系列的单片机,主频跑到24MHz以上,配合定时器中断做20ms周期的轮速控制,在这个周期里同时完成编码器读取、速度误差计算和PWM占空比更新,整个循环时间抖动可以控制在几十微秒以内,这在MicroPython上是很难实现的。
舵机控制同样如此。标准舵机的PWM周期一般是20ms,脉宽1ms到2ms对应不同角度。STC可以用定时器把高电平时间算得非常精准,这样舵机角度抖动就小。我们测试过,直接把舵机PWM命令放在STC上跑,角度重复精度能到1度以内,这对机械臂夹爪稳定抓取很重要。
3.2 编码器计数与传感器边沿捕捉的中断设计
蚂蚁搬家组的小车一般都需要知道轮子实际跑了多远,才能实现精准定位。我们在两个驱动轮上装了霍尔编码器或光电编码器,编码器信号接STC的外部中断引脚。STC在中断里做计数,主循环再根据计数值计算位移和速度。
这里有一个非常典型的坑:编码器中断频率很高,如果中断服务函数里处理太多事情,主循环就饿死了。我们的做法是中断里只做计数器累加,其他所有计算全部放到主循环里。另外,传感器边沿捕捉也需要用中断,比如检测物体是否到达夹爪位置、检测搬运区边界线,这些信号如果靠轮询,延迟不可控。STC配置成上升沿或下降沿触发中断,响应时间固定,逻辑上就踏实很多。
3.3 机械臂和舵机加减速策略
很多队伍把机械臂动作当成简单的“舵机转到位”来处理,结果发现物体在快速移动时容易掉落。我们后来总结出的经验是:舵机动作必须做加减速规划,尤其是垂直升降和夹爪开合。
具体操作是:STC收到NXP的“抓取”命令后,不是直接把舵机脉宽跳到目标值,而是把目标脉宽拆成很多个中间步骤,每10ms更新一次,形成一个S形加减速曲线。这样夹爪合拢时物体不会因为冲击太大被弹出来,机械臂下降到位时也更有缓冲。执行同样的逻辑,用NXP的MicroPython做这种细粒度定时会很吃力,因为Python层的循环间隔不稳定;用STC定时器中断就非常自然。
3.4 STC的ISP在线编程与调试技巧
顺带提一嘴STC在开发效率上的一个优势:支持ISP串口在线烧录,不用把芯片拆下来,也不用额外的仿真器。我们调试下位机固件时,直接用一块USB转TTL小板连接STC的串口引脚,按一下冷启动按钮就能写程序。蚂蚁搬家组的执行层逻辑改动频繁,比如调整加减速曲线、改编码器脉冲系数,每次改完几十秒就能重新烧录上车测试,这个效率对备赛时间来说太宝贵了。
4. 上下位机通信协议——一帧命令如何把两颗芯片拧成一根绳
分工再好,两颗芯片之间如果通信不可靠,整个系统就是散的。我们在这块吃过不少亏,最后沉淀出一套适合蚂蚁搬家场景的轻量通信协议,核心思路是“帧结构清晰、校验严格、状态可追踪”。
4.1 命令帧格式设计
NXP和STC之间用的是串口,波特率我们选了一个平衡点:115200。这个速率下数据稳定,传输一帧几十字节的命令在毫秒级完成,完全够用。帧格式固定为:帧头、命令类型、数据长度、数据域、校验和。
帧头我们用两个字节0xAA 0x55,用来在连续数据流里找帧起点。命令类型用一字节区分不同动作,比如0x01是电机速度指令、0x02是舵机角度指令、0x03是状态查询。数据长度表示后面数据域的字节数,数据域里放具体参数,比如左右轮速度值、舵机目标角度。最后加一个简单的CRC8校验或累加和校验。之所以坚持加校验,是因为电机驱动产生的电磁干扰会让串口偶尔出现误码,如果没有校验,STC可能把乱码当成控制指令,车就乱跑了。
4.2 STC返回状态与双向心跳机制
通信不能只从上往下单向走,STC也需要向上报状态。我们让STC每100ms主动回传一帧状态:当前轮速、编码器累计值、舵机位置、传感器电平。NXP收到这些数据后,可以判断执行层是否工作正常。比如NXP发了“抓取”命令,如果STC在超时时间内没有返回“夹爪到位”状态,NXP就会认为动作失败,切换到重试流程。
更关键的是心跳机制。NXP每100ms向STC发一个心跳命令,STC如果连续500ms没有收到心跳,就认为上层已经卡死,立刻执行安全停车——电机停转、机械臂回到安全位置。同样道理,NXP如果连续收不到STC回传,也会进入安全模式。这个双向看门狗逻辑,是我们整个系统最后能稳定跑完全程的底线保障。
4.3 串口通信实战坑:粘包、丢包与共地问题
我们调试过程中遇到过几个经典问题。第一个是粘包,STC发送速度很快时,NXP可能一次收到两帧数据,如果程序只按固定长度解析就会错位。我们的解法是在MicroPython里搞了一个简单的环形缓冲区和帧解析状态机,一字节一字节消化数据流,先找帧头再按长度收完整帧,这样就算数据挤在一起也能分得开。
第二个问题是串口丢包,排查到最后发现是NXP和STC两边电源没有共地。两套系统各用各的电池或者降压模块,地电位不一致,串口信号就飘。解决方式很简单,把NXP的地和STC的地用一根粗导线连在一起,再在通信线上加个上拉电阻。这类硬件层面的问题,只靠程序永远查不出来,但现象就是通信时好时坏,非常恼人。
5. 实测阶段踩过的五个大坑——从现象到根因的完整排查链路
这部分是我最想写的。我们调试了整整三周,被各种奇怪现象折磨过,这些坑如果不记录下来,后来者大概率还会再踩一遍。
5.1 电机反电动势导致NXP反复重启
第一次实车测试时,小车一加速,NXP那边就黑屏重启,整个系统全部归零。最初怀疑是供电不足,加了大电容,加了稳压模块,但问题依旧。后来用万用表去抓电机启动瞬间的电压波形,发现电机PWM切换时反电动势把电源电压瞬间拉到很低,NXP的复位阈值被触发。
根因找到了,解决思路就是物理隔离。我们在电机驱动板电源和NXP电源之间加了独立DC-DC隔离模块,同时所有大电流地线单独走一条粗线,信号地和控制地单点相连。改完之后,电机怎么加速NXP都非常稳定。这个坑提醒我们:双芯片架构里,电源域的隔离一定要从一开始就规划好,不能等出问题再补。
5.2 MicroPython垃圾回收引发的舵机抖动
有段时间小车在搬运途中舵机会偶尔抖一下,尤其是在NXP抓完图像数据之后。一开始一直查STC的PWM代码,后来把日志打出来才发现,舵机抖动的时刻和MicroPython里GC执行时间完全吻合。原因是NXP在GC暂停的瞬间,原本要发到STC的下一帧舵机指令被延后了几十毫秒,导致舵机在这个周期内保持在旧的目标角度,从外部看就是抖了一下。
解法分为两层。第一层,NXP侧做上文提过的手动GC控制,把垃圾回收放在状态切换的空档或小车停车的时候;第二层,STC侧做指令平滑,即使某段时间收不到新指令,也保持上一次舵机指令的终点值,不让舵机回到中间或上个位置。这样双保险之后,舵机抖动现象彻底消失。
5.3 STC复位电路与电源毛刺的诡异关系
我们有一块STC板子,跑着跑着会概率性复位,表现为车突然停下来,但马上又能重新启动。查了很久,最后发现和STC的复位电路设计有关。STC推荐复位引脚接一个10uF电容到地,上电瞬间维持复位电平。但我们的板子为了省事,复位电容只用了一个很小的瓷片电容,抗干扰能力不足。轮子碾过某些接缝时产生机械振动,间接带来电源毛刺,复位电路就被毛刺误触发了。
后来按照官方推荐重新搭了复位电路,用10uF电解电容并在复位引脚和地之间,现象立刻消失。这里要给所有用STC做执行板的队伍提个醒:复位电路不是随便接个电容就完事,容值太小、布线太长都会埋下隐患。尤其是比赛现场可能有其他队伍的大功率设备,电网环境复杂,复位电路的余量一定要留足。
5.4 搬运物体后的重心偏移让循迹跑偏
之前我们的决策层和STC执行层都工作正常了,但小车从物品区抓完物体往回走的时候,经常走着走着就往一边偏,直线都跑不直。排查发现是搬运物体让整车重心发生了偏移,重心偏了之后,两侧轮子对地面的正压力不一样,同样的PWM占空比下,两个轮子的实际打滑率和摩擦力不同,车就走歪了。
这个问题程序上很难完全消除,我们做了两个优化。第一,机械结构上尽可能让物品放在底盘正中间,缩小重心偏移量。第二,STC的PID控制里加入了一个前馈修正:根据当前是否持物,在PWM输出上叠加一个小的偏置量,让左右轮驱动力重新平衡。这个偏置量可以通过实测标定出来,比如空车跑三段取平均,满载跑三段取平均,差值就是修正量。
5.5 多任务并发卡死——用日志链还原现场
还有一次比较头疼的问题是整车上电后,偶尔会莫名其妙“死机”,仪表没有报警但车轮不动。NXP这边的MicroPython线程看起来正常,STC也显示在线,但就是不执行任何动作。为了找到问题,我们在两颗芯片的程序里都加了一串环形日志缓冲,把关键事件带时间戳记录在内存里,卡死之后用串口读出来。
日志还原后真相大白:原因是NXP在状态机进入搬运状态的同时,STC还停留在上一次搬运的放位状态,两颗芯片的状态不同步,双方都在等对方信号,形成了一个死等。我们的修复方案是在状态机设计里强制加入一个“准备就绪”握手命令,NXP必须在STC明确回报走到安全位置之后,才能发送下一阶段动作指令。从那之后,这种隐并发卡死再没出现过。
6. 搬运顺序与路线优化——从“能完成”到“稳定拿分”
蚂蚁搬家组很多时候比的不是谁的理论速度快,而是谁的失误率低。跑完一次全程拿10分,但如果中途掉一次物体,可能要扣掉大半。所以我们在基础功能稳定后,把重心放在了策略优化上。
6.1 搬运顺序的优先级规划
如果比赛是搬运多个物体到不同目标区,搬运顺序对总耗时影响很大。我们的原则很简单:先近后远、先易后难。先搬距离短的物体,一方面热身,另一方面积累成功率;再集中精力处理边角位置难拿的物体。在实际实现中,NXP决策层维护一个待办列表,每次识别完物体后按曼哈顿距离或欧氏距离算一个代价,自动决定下一个搬谁。这样写的好处是,即使比赛现场物体位置和预判不一样,程序也能动态调整,不会按照死顺序去硬搬。
6.2 放位阶段的微调与机械容错
放位是最容易掉链子的环节。我们发现,与其让小车快速冲到目标区然后猛地一下放,不如在目标区附近提前做一个“慢速对准区”。NXP通过在目标区域前两米处切换到低速模式,STC执行低速循迹和舵机微调,最后用光电传感器或机械限位确认物体到达正确高度,再松开夹爪。整个过程速度慢,但成功率极高,对得分而言非常划算。
机械容错上也有一点经验:夹爪的夹持力不要太紧,刚好能夹住但稍加震动不会掉落即可。太紧了放位时物体反而容易卡在夹爪里,导致物体无法落到指定区域。我们在夹爪内侧贴了一层防滑垫,既增加摩擦力又不至于过紧,放位成功率高了很多。
6.3 赛前稳定性的压测清单
最后建议每个队伍在赛前留出至少一天,做一波稳定性压测。我们的标准动作包括:连续空载跑10圈,看有没有死机或串口断连;满载搬运20次,看机械臂和舵机有没有疲劳衰减;故意用程序制造一次上层超时,验证STC的看门狗能否安全停车;还要在不同光线强度下测试视觉识别的成功率。每一轮测试结束后记录异常点,能改就改,不能改就规避,把赛场上可能出现的问题提前暴露掉。
这一套压测做下来,虽然不会让车变得更快,但能让你在发车之前心里有底。比赛比的不是偶然一次的高光表现,而是稳定输出。我们最后在正式比赛中的策略就是:不求最激进,只求每趟都稳稳把物体搬到位。
整个蚂蚁搬家项目做下来,NXP-MicroPython和STC的协同架构确实帮我们省了很多事。MicroPython让上层逻辑改起来非常顺手,状态机、识别、路径规划这些代码写起来就像写普通脚本;STC则在底层默默干着脏活累活,把每一个PWM脉冲、每一次编码器中断都处理得稳稳当当。两颗芯片各司其职,中间用一套可靠的串口协议拧在一起,整个车才算真正有了“脑子”和“手脚”。
如果让我再给后续参赛队伍一个最朴素的建议,那就是:先别急着调算法,先把通信、电源、复位电路这些底层的可靠性搞定,再往上堆功能。蚂蚁搬家组的胜负手,往往不在那些炫酷的视觉模型,而在于你的系统能不能连续跑完五趟不犯任何一个低级错误。