
去年第一次带着V1.0去赛场最尴尬的一幕我现在还记得预选赛第一趟跑到第9个格子右转时直接冲出迷宫撞在挡板上蜂鸣器响了一路。今年换了血用ESP32-S3主控的第二代NeuroMouse V2.0去参加AAMC 2026在Micromouse项目拿了个第三名。这篇复盘不是晒奖状而是想把这台小车从V1.0翻车到V2.0冲线的完整技术路线摊开讲清楚给做Micromouse、做轮式自主小车、或者单纯对ESP32-S3实时控制感兴趣的朋友一份能直接抄作业的参考。先说结论拿第三不是运气是V1.0攒下的哪里会翻车清单足够长。下面我按硬件架构、感知控制、迷宫算法、赛场实战四条线展开每步都会交代当时为什么这么选以及如果重来一次我会怎么改。1. 立项复盘V1.0翻车换来的需求清单1.1 V1.0的三次翻车现场V1.0用的是STM32F405方案。当时我天真地以为Micromouse嘛不就是检测墙、走格子、找中心三步结果第一次拿到标准16×16迷宫每格180mm壁高50mm实测直接被打脸。第一个问题是传感器。V1.0只用了三路红外反射对管分别看前、左、右理论方案是够的但红外反射信号强度跟墙的颜色、距离、环境光都有关系。练习场地是专用白底黑线迷宫板墙上贴白色泡沫板信号很干净。一到赛场墙面是浅灰色亮面材质同样距离反射回来的电压差了快一半。我用固定阈值判壁于是该亮的墙不亮不该亮的乱亮第一趟就在岔路口把左墙误判成右墙方向盘直接打反。第二个问题是转弯。V1.0的90度转向靠编码器计数左右轮差速转固定时间。低速凑合能跑速度一提到1m/s以上地面打滑、轮子空转编码器计数和实际角度就对不上了。跑三圈累计误差越来越大最后就是开篇那个冲出迷宫撞挡板的名场面。赛后我测了一下同样一个90度转弯低速和高速之间角度误差差了差不多8度。这个误差根本没法靠固定补偿修正。第三个问题很隐蔽STM32F405是单核Cortex-M4我把1kHz的PID中断、壁障扫描、Flood Fill、串口调试全塞在一个核上。平时跑着没事但Flood Fill更新网格一旦占用太长时间PID中断就开始抖。中断一抖电机声音立刻改变车身轻微打摆子。这个毛病在V1.0后期严重到我不敢开超过1.2m/s的直道速度因为一快就打摆一打摆就撞墙。1.2 V2.0的需求拆解立项时我先把拿奖这种模糊目标拆成可验收指标V2.0的每一步改动都能回溯到这张表后面调试时不会东一榔头西一棒子。指标V1.0实测V2.0目标验收方法直道最大巡航速度1.2 m/s再快就打摆子1.8 m/s直线连续跑50格90度转弯时间650ms300ms以内陀螺仪编码器记录曲线壁障误判次数平均每趟1.2次全程0次完整探索10趟统计起点到中心冲刺时间未完成卡壳首次冲刺25s以内实际计时迷宫数据更新延迟转弯后偶发乱码每次转弯后50ms内完成串口打印网格核对这份拆解很关键。比如稳定巡航1.8m/s直接决定了要不要换主控、要不要加陀螺仪、传感器要什么等级。没有这些数字后面所有优化都是感觉流。2. 硬件架构决策ESP32-S3、传感器和电机的取舍2.1 换主控的真正理由双核外设灵活性很多人问我为什么V2.0不用STM32F4或者G4反而选了ESP32-S3这种物联网芯片。说实话我也犹豫过。但把Micromouse的控制任务拆开看答案非常直观维度STM32F405ESP32-S3核心单核Cortex-M4 168MHz双核Xtensa LX7 240MHzSRAM192KB512KB引脚复用固定布线绕头几乎全引脚可映射IO矩阵无线无WiFiBLE5模块价格芯片约30-50元模组约20-30元最打动我的是双核。微鼠天然分两类任务一类是高频传感器采样和电机PID一类是低频但计算密集的Flood Fill和路径决策。单核时代两者互相抢时间双核时代我把它们钉在各自核心上——Core 0专门跑1kHz的控制中断Core 1跑迷宫算法和串口调试。实测控制中断的抖动从V1.0的±20μs降到了±3μs左右。这个稳定性对高速电机控制是决定性的我用示波器看过V1.0的PWM脉宽抖动比较明显V2.0基本是一条干净的线。另一个加分项是IO矩阵。ESP32-S3几乎所有GPIO都能映射到任意外设布线时不用像STM32那样死磕引脚复用表。微鼠板子面积紧张这个灵活性直接帮我把走线层数从四层压缩到两层。ESP32-S3的缺点也得说ADC精度一般电源波动大时模拟读数会飘。所以我的处理是ADC只用来做电池电压粗略监测所有对精度敏感的模拟量全部走数字接口编码器、I2C、串行输出这条路实测很稳。2.2 传感器布局红外为主、ToF为辅V1.0纯红外吃了大亏但纯ToF也不是银弹。VL53L0X能测绝对距离不受表面颜色影响可它的测量周期在20ms左右1kHz的控制循环根本喂不饱。而且ToF对目标表面角度敏感高速行驶车身一抖测距值就开始跳。V2.0最后用了混合方案五路红外反射对管做近距判壁有效距离5~12cm负责左右壁和前方壁的存在性判断前方加一颗VL53L1X做远距离壁检测和入格前预判。布局是这样的左壁两路红外一前一后斜向安装可以同时算距离和夹角右壁两路红外对称布局前方一路红外加一颗VL53L1X墙角加竖直挡光板削弱环境光影响这套布局最大的收益是平行走壁左前和左后两路读数差在阈值内说明车身和左壁平行读数差变大说明车头偏了。这个信号来自物理量响应比陀螺仪积分快得多是高速走直线的主力。2.3 电机、驱动和供电的细节电机选了N20微型减速直流电机减速比1:50带磁编码器每圈输出12脉冲4倍频后48计数/圈。配合直径32mm的轮子单圈编码器计数48×502400。满速1.8m/s时轮子转速约17.9转/s编码器频率大约43kHzESP32-S3的硬件正交解码没有任何压力。驱动芯片是TB6612FNG单路1.2A带N20这种堵转电流800mA左右的电机绰绰有余。别小看驱动选型V1.0用的L298N太老开关损耗大电池温度高不说PWM高电平的上升沿还特别钝电机响应慢了一拍。供电是另一个大坑。微鼠用2S锂电池7.4V标称电机启动瞬间电流能拉到1A以上。如果直接把电池电压分压给ESP32-S3的ADC噪声会大到怀疑人生。我做了三级处理电机直接吃电池逻辑电用一颗低压差LDO单独供模拟地和数字地在PCB上单点汇接。这件事做没做好直接决定后面调传感器的血压。3. 感知与运动控制把不撞墙练成肌肉记忆3.1 红外标定不是拧一个阈值那么简单V2.0红外的标定我做了个专门流程。每轮比赛前用一段固定在迷宫板上的滑轨让小车以固定高度和角度滑过墙壁记录不同距离下的红外输出ADC值生成距离-读数曲线。运行时不是定一个阈值而是用曲线拟合出距离再给距离加容差窗口。这样做的价值在于车离墙40mm、50mm、60mm时读数连续变化我可以估算车相对墙的距离和夹角而不只是有墙/没墙两个状态。平行走壁的原始数据就是这么来的。标定完还要做环境光补采把车放在赛场环境里关掉红外LED先采一遍环境底噪运行时代码里直接减掉。这里有个很多人忽视的细节红外发射LED和接收管吃同一个3.3V当电机PWM导致3.3V波动时发射光强和接收灵敏度同时变最后读数里带上周期性纹波。解决办法是LED供电独立稳压一劳永逸。至于给LED加独立稳压会不会占用太多板面积实测多一颗小LDO的体积换来的是传感器底层读数的干净很值。3.2 双环控制速度环在前航向校正在后V2.0电机控制是标准串级PID内环编码器速度环1kHz外环位置/航向环100Hz。速度环输出PWM占空比外环输出速度设定值的修正量。速度环整定有个实用经验先只用比例让车空转看能不能稳定在目标速度附近然后加一点积分消除稳态误差最后再加微分。微鼠这种轻负载积分项宜小不宜大——因为编码器和车轮之间隔着减速箱齿轮间隙会让积分项在换向时疯狂累积反而振荡。我把积分限幅设成输出范围的5%实测比不限幅稳定很多。外环才是MicroMouse跑直线的真正主角。直道上我的外环实际上是平行走壁修正量用左右四路红外算出的夹角和距离偏差转换成左右轮速度差叠加到速度设定值。这比看到偏差再转向快很多因为它纠正的是车头是否正对壁方向而不是是否在迷宫格中心。对称地前方墙体在即将入格时才参与控制用于减速和确认格中心。3.3 转弯与掉头陀螺仪短时积分转弯用MPU6050的Z轴陀螺仪做角速度积分得到航向角。陀螺仪积分会漂移所以我绝不拿它做长时累计航向只用在一次90度转向的短时窗口里。流程如下收到转向指令先按当前速度减速到0.3m/s以下纯差速原地转目标角速度先线性爬升再按剩余角度缩减陀螺仪累计转过85度时提前减速停止后用左右壁传感器校验夹角超1度就小角度修正// 简化版转向控制实际跑在1kHz中断里 void turn_update(float target_deg) { float est_deg integrate_gyro(dt); // 陀螺仪Z轴积分 float remain target_deg - est_deg; float w clamp(turn_kp * remain, 0, max_w); // 角速度设定 set_left_right_speed(-w * R_wheel, w * R_wheel); if (fabs(remain) 1.0f) stop_and_align(); // 停转壁检测修正 }翻过车的细节是原地差速转弯时如果地面不是完全干燥轮子会轻微打滑陀螺仪积分会比实际角度慢。所以我把最大角速度限制在电机理论上限的70%给抓地力留足余量。AAMC现场迷宫地板有点抛光感实测打滑率大约3%靠这个余量稳稳扛过去了。4. 迷宫探索与冲刺算法层怎么榨干硬件性能4.1 Flood Fill够用就没必要再造轮子迷宫求解主流方案是Flood Fill泛洪填充原理很简单从目标格开始给每格标一个到目标的距离像水波一样扩散。每到一个新位置根据现场测到的墙更新网格重新扩散一次选距离值最小的可行方向走。ESP32-S3跑16×16共256格一次完整Flood Fill在微秒级完成完全谈不上性能瓶颈。最朴素版本的实现思路长这样#define MAZE_W 16 #define MAZE_H 16 static int16_t dist[MAZE_H][MAZE_W]; void flood_fill_from(int tx, int ty) { for (int y 0; y MAZE_H; y) for (int x 0; x MAZE_W; x) dist[y][x] INT16_MAX; dist[ty][tx] 0; bool updated true; while (updated) { updated false; for (int y 0; y MAZE_H; y) for (int x 0; x MAZE_W; x) { if (dist[y][x] INT16_MAX) continue; int dirs[4][2] {{1,0},{-1,0},{0,1},{0,-1}}; for (int d 0; d 4; d) { int nx x dirs[d][0], ny y dirs[d][1]; if (nx 0 || nx MAZE_W || ny 0 || ny MAZE_H) continue; if (wall_between(x, y, d)) continue; if (dist[ny][nx] dist[y][x] 1) { dist[ny][nx] dist[y][x] 1; updated true; } } } } }优化经验Flood Fill从目标格做一次得到全图距离场后小车每一步只需查四个邻居的距离值选最小的不需要每次转弯都全图重算。只有当检测到预期中不存在的墙时才触发重算。我在代码里加了版本号机制迷宫网格每次变化才重算。这样探索阶段CPU占用几乎可以忽略从Core 0借调过来的时间也可以还给控制一举两得。4.2 探索阶段的决策逻辑探索目标不是最快而是尽量少走错路地收集墙信息。我用的是经典左手法则变体优先选距离场数值最小的方向两个方向距离接近时选当前车头顺时针一侧保持连续性死胡同只有来路可走就原地掉头每进入新格把四周墙数据写入网格这个方法在标准16×16迷宫表现不错缺点是开放区域会绕远。AAMC的迷宫墙密度比较高所以问题不大。真遇到大空场可以在探索完成后再做一次全局路径搜索找出遗漏区域时间不够就直接放弃毕竟比赛比的是冲刺成绩不是探索覆盖率。我这次就是按这个原则做的省下的时间全部用来调冲刺速度。4.3 冲刺阶段的速度规划冲刺阶段迷宫已经完整路线也定了剩下就是沿直线加速、提前减速、过弯、再加速的循环。微鼠直道通常不止一格我把多个连续格合并成一条直线段用梯形速度曲线规划加速度1.2m/s²地面摩擦低时降到0.8m/s²最大速度1.6m/s起步最终调到1.8m/s减速点提前计算保证到达转角前速度降到0.4m/s以下减速点计算有个关系式要看清楚v_end² v_start² - 2·a·s其中s是从当前位置到转向点的距离。如果编码器算出的当前位置比实际偏大减速就会偏晚冲进弯道才发现速度没降下来。所以我在弯道前加了个保险距离进入转弯前瞻区后强制查表给出一组目标速度vs剩余距离而不是只依赖线性公式。冲刺成绩从探索完稳定跑进25秒我花了大概两个晚上调这张减速表。参数和机器人重心、迷宫地板摩擦、轮胎磨损都有关没有一劳永逸的值每次换场地必须重新标定。AAMC赛前我踩了两条直道来拟合实际摩擦系数才敢把满速推到1.8。5. AAMC 2026实战从练习场到赛场的差距5.1 赛前模拟与现场突发正式比赛前一周我把能模拟的赛场干扰都过了一遍白墙换灰墙、底板换成组委会发的样品板、调整灯光亮度、甚至把空调风向对着跑道吹。红外对管最怕环境光我特意在下午强光时段和晚上灯光时段各跑三轮记录误判次数。真到了现场还是被上了一课。比赛迷宫板拼装后的拼接缝处墙面会有一个1~2mm的台阶。红外传感器跨越拼接缝的瞬间读数出现短暂跳变。练习场地面没有这道缝结果第一趟探索就在两块板交界处连续误判两次把墙画错了两格路线直接绕远。解决办法是加读数平滑采样窗口传感器连续采样20次去掉最大最小值取平均再和上一周期中值做一阶滤波跳变的影响就被抹平了。这个修改V1.0就该做我硬是拖到比赛现场才被逼着实现。好事是这次教训给了我一个非常明确的备赛流程赛前用拼接状态的实际迷宫板试跑而不是用整块金属模压板。5.2 三趟成绩背后的调试记录AAMC 2026决赛是三轮冲刺取最好成绩我的三轮数据大概是这样的轮次冲刺用时问题第一轮26.8s拼接缝附近减速过度浪费约2s第二轮25.1s转弯后传感器校准花掉0.8s路线稍长第三轮23.9s状态最好无额外纠错第三轮比第二轮快了1.2s主要贡献其实是信任墙数据。第二轮我冲刺阶段仍然对墙检测持怀疑态度每次路过弯道都额外做距离校验拖慢了速度。第三轮我告诉自己探索阶段已经把墙记下来了冲刺阶段除非传感器连续两次报冲突否则一律按已知迷宫走。这个心态调整直接省掉了每次进弯的校验时间。还有个现场教训第二轮结束以后裁判组检查小车发现一颗轮子的螺丝有点松。松螺丝对编码器读数是灾难性的码盘和轮轴相对位置会发生周期性偏移速度环会把这个偏移当成真实速度波动PWM疯狂修正车就开始抖。我当场拧紧螺丝第三轮丝滑了很多。这让我写进备赛清单的一句话是检查所有轮轴螺丝不是客气话。5.3 给后来者的三个忠告最后说三个从这届比赛带回来的忠告按重要程度排第一把传感器标定做到极致再谈算法和速度。Micromouse翻车九成是传感器误判不是算法不对。花一个下午做距离-读数曲线后面能省十个晚上。第二控制循环的稳定性比峰值性能重要。V2.0三轮成绩都在23~27秒之间差距来自偶发校正。如果你能保证每趟都不需要校正哪怕满速低一点总成绩也好过理论很快但经常要救回来的方案。第三备好心态调整清单。赛场突发状况永远比你排练的多与其现场乱改代码不如提前写好遇到误判怎么办、遇到打滑怎么办、遇到灯光变化怎么办的清单。按清单来手不抖脑子不乱。就我个人而言这个第三名最大的收获不是奖牌是让我重新相信一条老经验把基础环节做到没有短板比到处塞高级算法划算得多。NeuroMouse这个系列我还会继续改下一步想试试把探索阶段也做成自适应步长减少高速下的传感器平滑时间。等真跑出点新成绩再来更新这篇记录。