
简介面向2024全国大学生电子设计竞赛H题自动行驶小车参赛选手将完整方案整理开源适合备战电赛或学习MSPM0系列单片机的开发者尤其适合已有单片机基础、希望在赛题框架下快速上手的同学。资源包共89个文件以C语言源码与头文件为核心配套CCS工程配置、syscfg驱动配置、链接脚本、编译中间文件及README说明文档体积仅386KB目录结构清晰。方案围绕TI MSPM0G3507主控实现涵盖OLED显示、MPU6050姿态解算、JY61P串口传感器接入、PWM电机调速等模块可帮助读者还原小车运动控制逻辑理解外设初始化、定时器与串口的中断处理流程。目前已有3162人学习适合对照源码复现和改造也能作为后续电赛小车类题目的基础工程。 电赛H题这名字参加过2024年国赛的同学应该都不陌生。当时赛题一出来实验室几个人盯着“自动行驶小车”这六个字沉默了半分钟——这题看着门槛不高但真做起来考的是从传感器到控制算法再到系统联调的一整条链路。我最后拿的是省一等奖但备赛最后一周炸车炸到怀疑人生。这篇就把我们从选型到调参的完整思路写出来给准备参加电赛或者做类似智能车项目的朋友一个参考。1. 赛题翻译H题考的不是“跑得快”而是“跑得对”1.1 题目到底要求小车干什么2024年H题的核心场景是模拟一辆在“城市道路”上自动行驶的小车。赛道上画有引导线模拟车道沿途布置了红绿灯、禁行区域、障碍物等交通元素小车需要完成按规定路线行驶、遇红灯停车、绿灯再启动、绕开障碍、按指定路口转向、最终在终点区域停车等一系列动作。把题目翻译成工程语言就是这样几个子需求循迹行驶通过传感器获取赛道引导线的偏差信息控制电机转速让小车稳定沿着线走。交通元素识别识别红绿灯状态、禁行区色块、障碍物位置并做出对应动作。路口决策在十字或T型路口根据赛前设定的任务指令选择直行、左转或右转。精确停车在指定终点区域内停车对停车位置精度有要求。很多队伍一开始把注意力放在了“小车跑得快不快”上结果调了很久速度发现快根本不解决核心问题。H题真正的难点在于状态判断和动作切换的可靠性——你永远不知道现场光线什么样、赛道摩擦系数什么样、检测元素会以什么角度出现在镜头里。跑得快的车到处乱冲做得“对”的车才是拿分关键。1.2 隐含考点状态机思维和系统鲁棒性这道题与其说在考单片机编程不如说在考状态机设计能力和系统鲁棒性。因为小车的每个动作都不是孤立的红灯停车之后要能自动切回循迹避障结束要能重新找线一个路口判断失误整圈就废了。所以从第一天开始我们的代码架构就完全围绕“状态”来组织。小车每一时刻都处于一个明确的状态找线、循迹、停车等待、避障、路口转向、终点停止每个状态有明确的进入条件和退出条件。后来复盘时发现正是这个决定让我们在最后联调阶段省了大量时间——出问题时能立刻定位到是哪个状态切换出了bug而不是一团乱麻地瞎试。2. 硬件选型这套配置让我少交了三次“学费”2.1 底盘四轮差速还是舵机转向底盘决定了整车的运动模型这是选型最开始就要定的事。H题赛道有弧线、十字路口、直角弯所以底盘方案主要两个方向四轮差速底盘左右两路电机分别驱动通过转速差实现转向。优点是结构简单、转向灵活、可以在原地调整朝向缺点是直线循迹时需要频繁修正控制频率要求高。前轮舵机后轮电机底盘车模底盘转向由舵机带动前轮摆角完成运动模型接近真实汽车。优点是直线行驶稳定、循迹视觉效果好缺点是转弯半径大路口转向时对时机要求高。我们最终用的是后轮双电机差速前轮万向轮的三轮结构也就是常见的差速驱动布局。理由很直接H题的赛道不要求高速差速底盘控制简单加分项里的直角转向也能轻松完成。你如果选定舵机转向方案也可以但务必提前验证最小转弯半径是否能在赛道允许范围内完成掉头和直角弯这个我们组另一队就翻了车。2.2 主控和视觉模块的搭配主控我们选了STM32F103C8T6也就是“蓝板”。说实话很多队伍纠结要不要上F407我的看法是没必要。自动行驶小车的主控任务就是处理编码器数据、跑PID、解析串口数据、输出PWMF103的72MHz主频完全够用而且资料多出了问题好查。视觉模块选了OpenMV Cam H7 Plus。加上K210也考虑过但OpenMV在颜色阈值调试上有IDE实时预览现场调参效率高。如果你自己熟悉K210的MaixPy也完全可以核心逻辑都一样。不过强烈不建议用树莓派——启动慢、功耗高、环境依赖重电赛现场你根本不想折腾OpenCV环境。2.3 传感器与电源容易踩的坑传感器方面编码器必须带我用的是JGB37-520直流减速电机自带的霍尔编码器13线其实就是每转13个脉冲的常见规格。另外还要准备灰度传感器作为视觉失效时的兜底——我们只在起点和终点区用了两路灰度来做粗定位实际证明这个决定帮我们避免了好几次视觉误判导致的位置丢失。电源是很多人忽视的重灾区。电机启动瞬间电流很大如果和主控、摄像头共用电源电压跌落会让OpenMV直接重启。我们总共烧过两块OpenMV原因都是电源纹波问题。最后方案是7.4V两节锂电池→降压模块→5V给主控和摄像头电机驱动直接吃电池电压主控和电机之间完全隔离才解决。部分选型备注主控STM32F103C8T6性价比高、资料多视觉OpenMV H7 Plus调参效率高、IDE好用电机JGB37-520带霍尔编码器扭矩够、自带测速驱动TB6612FNG压降小、发热低电源7.4V锂电独立降压主控和电机必须隔离辅助传感器2路灰度起点和终点定位兜底3. 视觉识别OpenMV如何从满屏白底里找到那条线3.1 图像预处理与线偏差求解视觉部分是全车的大脑入口。OpenMV上的处理流程简单说就是拍图→阈值过滤→找目标色块→算偏差→发送给STM32。赛道通常是白底黑线所以第一步就是设定LAB色彩空间下的黑色阈值把黑色引导线从背景中分离出来。我们在IDE里调试时会实时打开阈值预览窗口把L通道调低、A和B通道压缩得到只覆盖引导线的mask。拿到色块后核心是求偏差值error。这个偏差定义为目标引导线中心点的x坐标与图像中心点x坐标的差范围大约在-160到160像素之间。常用的做法是取色块blob.cx()作为引导线的水平中心import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QQVGA) # 160x120算力开销小 sensor.skip_frames(time2000) sensor.set_auto_gain(False) # 关闭自动增益防止亮度波动 sensor.set_auto_whitebal(False) red_threshold (0, 40, -20, 20, -20, 20) # 黑色阈值按现场微调 while True: img sensor.snapshot() blobs img.find_blobs([red_threshold], roi(0, 20, 160, 80), pixels_threshold30) if blobs: blob max(blobs, keylambda b: b.pixels()) error blob.cx() - img.width() // 2 data b\xaa\x55 b\x01 int(error).to_bytes(2, signed) b\x00 uart.write(data)一个非常关键的细节ROI感兴趣区域不要覆盖全图我们只取了图像底部到中部的一块矩形区域。因为引导线在小车正前方时这一块区域最能反映“当前偏差”而图像顶部往往是远处景观或赛道外的干扰不加ROI的话偏差会剧烈跳变。3.2 红绿灯和禁行区域识别逻辑识别红绿灯本质上就是设定红、黄、绿三种颜色的LAB阈值然后对每个色块做面积过滤。红色和绿色的阈值很好调麻烦的是黄色容易和白色赛道混在一起这时候需要加一个条件色块的宽高比必须在一定范围内比如灯体是扁的或接近方形以及色块的中心位置应当在中上方ROI区域。禁行区通常用红色胶带或色块贴纸标出处理逻辑也是颜色阈值。但真正容易出错的是误判——赛道旁边的红色杂物、灯光的红色反光都算干扰。我们的解决方式是多重过滤先看色块面积是否超过阈值再看色块是否出现在预期的ROI横向区间内最后还要满足连续N帧检测到才认为是有效目标。这里说明一下电赛现场每个赛区的赛道尺寸、标识样式可能略有差异所以颜色阈值和ROI位置一定要到现场后重新标定不能拿着实验室的参数直接用。3.3 路口识别到底什么时候该转向路口识别是整个视觉部分最容易翻车的地方。十字形和T字形的特征在线阵灰度传感器下非常难判断但在摄像头画面里有明显特征引导线在画面底部是正常的一条到了画面中部会分叉成多段色块或者在底部突然中断。我们用的是“多色块检测法”对画面底部ROI和中部ROI分别找色块。如果中部ROI出现了2个及以上面积相近的独立色块就判定为路口。再加一个保险——连读三帧都检测到路口才触发转向避免单帧噪声误判。4. 运动控制从“乱扭”到“走直线”的PID调参之路4.1 速度环PID让两个轮子转速一致差速底盘最基础的控制是速度闭环。左右两个电机在同样PWM下实际转速未必相同电机个体差异、摩擦差异所以必须用编码器测速反馈每路电机跑一个PID。速度环用增量式PID实现输出的是PWM增量好处是控制平稳、不容易积分饱和。目标是让左右轮都稳定在设定转速附近这是后续一切循迹、转向的基础int IncrementalPID(int target_speed, int actual_speed) { static int error, last_error, prev_error; float Kp 8.0f, Ki 0.2f, Kd 0.5f; error target_speed - actual_speed; int output Kp*(error - last_error) Ki*error Kd*(error - 2*last_error prev_error); prev_error last_error; last_error error; return output; }调这个PID有个笨办法把一个轮子架空让另一个轮子也悬空给固定目标转速观察编码器反馈是否稳定。如果转速在目标值附近震荡就降低Kp如果响应太慢就适当加大Kp和Ki。总之先让两轮各自“听话”再谈协同。4.2 转向环把视觉偏差变成转速差有了速度环转向环就好办多了。我们用了最经典的直线式映射视觉模块给出的error经过一个比例系数转换成左右轮的转速差。大偏差时猛打方向小偏差时微微修正。实际实现时我用的是位置式PD控制来输出左右转速差turn_output Kp_turn * error Kd_turn * (error - last_error) left_speed base_speed - turn_output right_speed base_speed turn_output其中base_speed是基础行驶速度我们最终设在350rpm左右电机减速后的实际轮速这个速度既能保证识别处理跟得上也不会因为太快导致过弯甩出赛道。Kp_turn从0.3开始试逐渐加大直到小车在直线段不再明显左右摆动弯道不冲出为止。4.3 调参顺序的实操经验我总结的调参顺序给新手一个参考先调速度环两轮转速稳→ 再调直线循迹Kp_turn从小到大→ 加Kd抑制过弯震荡 → 最后调路口转向的PWM持续时间。一个血泪教训不要一开始就在完整赛道上调。先在桌面用黑色胶带贴一条简单的直线把直线走稳了再逐步加圆弧、加直角、加路口。直接在完整赛道上调参出问题了你根本分不清是视觉问题还是控制问题。D项尤其要注意——太大的D会让小车变得“僵硬”在S弯道里表现为不断高频抖动。调Kd时用小步长慢慢加每次只加0.01级别直到弯道平滑为止。5. 上下位机协作OpenMV与STM32之间的通信协议设计5.1 数据帧格式简单可靠最重要OpenMV负责“看”STM32负责“动”两者之间通过串口UART通信。协议设计原则是帧头数据校验不能裸发一个整数就完事。我们用的帧格式帧头(2字节)模式(1字节)偏差(2字节,有符号)标志位(1字节)校验(1字节) 0xAA 0x55 0x01 error低8位 高8位 flags sumflags字节用来表示红绿灯状态和路口信息每一位代表一个状态bit0红灯、bit1绿灯、bit2检测到路口、bit3检测到禁行区。这样一个字节就能传递所有视觉判断结果。5.2 主控解析与决策策略STM32端用串口中断接收收满7个字节后先校验帧头和校验和通过之后才更新全局变量。这里有个经验数据加一个简单的时间戳或者监测接收时间间隔如果超过200ms没有新帧就认为视觉模块异常让小车进入安全停车状态防止失控冲撞。上位机发送频率我们设在20Hz左右也就是每50ms发一帧。这个频率和STM32控制周期50ms匹配保证每一帧控制指令都有对应的新视觉数据。频率太高没意义反而占用CPU太低会导致转向反应迟钝。OpenMV端的发送代码from pyb import UART uart UART(3, 115200, timeout_char100) def send_frame(mode, error, flags): data bytearray([0xAA, 0x55, mode]) data.append((error 8) 0xFF) data.append(error 0xFF) data.append(flags) checksum sum(data[2:]) 0xFF data.append(checksum) uart.write(data)6. 状态机与任务决策让小车知道“下一步去哪”6.1 状态定义与切换条件把整辆车的行为拆成状态是H题实现不乱的根基。我定义的状态不多就7个STATE_INIT上电自检等待启动信号STATE_FIND_LINE在原地调整方向寻找引导线STATE_FOLLOW正常循迹行驶STATE_WAIT红灯停车等待或避障停车等待STATE_TURN路口转向动作执行STATE_AVOID绕行障碍物STATE_STOP到达终点彻底停车状态切换的核心原则任何状态都有超时保护。比如STATE_TURN在转向动作执行2秒后如果还没回到循迹状态主动切回STATE_FIND_LINE重新找线而不是卡死在原地。6.2 从视觉输入到动作指令的整条链路拿“红灯停车”举例整条链路是OpenMV识别的flags里bit0置1 → 串口帧发到STM32 → 主控解析出红灯标志 → 状态机从STATE_FOLLOW切入STATE_WAIT→ 电机PWM输出为0 → 轮子停下来并保持位置。绿灯亮起时bit1置1状态机切回STATE_FOLLOW继续走。路口转向稍微复杂OpenMV检测到路口后置bit2小车先直行一小段确保前轮过线再根据预设任务执行左转或右转。我们是靠定时器控制转向PWM持续时间实现的左转时右轮前进、左轮回转或停止持续600ms左右然后自动回到循迹状态通过视觉偏差重新微调方向。关于避障很多方案是“检测到障碍→绕过”。H题的避障判定逻辑可以简化为OpenMV在ROI内检测到面积很大的、颜色与赛道不匹配的色块时先停车判断是绕行还是等待再继续。我们最后用了“先停再绕”策略稳定但不追求速度因为H题没有时间加分项稳定才是满分基础。7. 实测避坑赛前三天那些让小车“发疯”的瞬间7.1 供电不稳导致OpenMV静默重启这个问题排查了整整一天。现象是小车跑了一圈后突然失去循迹能力直冲向赛道外。用串口监视器一看OpenMV只有启动信息没有正常运行数据说明它重启了。排查链路是用万用表量电机启动瞬间的电压波形发现7.4V电池经过降压模块后在电机急加速时电压骤降到3.3V以下。最初方案是电池→5V稳压→给OpenMV和STM32供电电机驱动直接接电池。但稳压芯片在输入电压跳动时输出也跟着跳最终把OpenMV的供电改为独立5V稳压模块且在OpenMV电源脚并联一个470uF电解电容0.1uF瓷片电容问题才彻底消失。7.2 光线变化导致颜色阈值突然失效电赛现场光线和实验室差别很大尤其是中午阳光直接照在赛道上的时候白色反光让黑色阈值全乱了。最严重一次OpenMV把黑色引导线识别成了白的一部分小车在直线段就丢线。当时我们的处理办法是在代码里把摄像头的自动增益和自动白平衡关闭固定曝光时间并在比赛前留出10分钟的阈值重新标定时间。另外写了一个“多场景阈值数组”在找线失败时自动切换备用阈值组再找一次。这个方法虽然不算优雅但在现场真的救过命。7.3 编码器受电机干扰导致测速乱跳最后一个坑出现在联调后期速度环PID无论怎么调左轮转速都剧烈震荡。最终发现是编码器信号线离电机供电线太近电机转动时产生电磁干扰编码器脉冲丢失。解决方案也很简单把编码器信号线换成屏蔽线外层接地在STM32的编码器输入引脚加100Ω串联电阻和10nF对地电容做硬件滤波。所以做这类系统布线的规划要提前想好不要等到干扰出现了才后悔。说回这套方案本身其实每个模块单独拿出来都不算难难的是把它们组合成一个在陌生环境里也能稳定跑完的系统。2024年H题让我最大的收获不是那个省一等奖而是学会了一种调试思路——永远先怀疑通信和供电再怀疑算法永远给每个状态加上超时兜底永远在现场留出重新标定的时间。如果你也准备参加下一届电赛或其他智能车赛希望这篇能给你搭一个可用的底子剩下的路还得自己在调试点位上一个一个踩过来。本文还有配套的精品资源点击获取