2024电赛H题自动行驶小车完整方案:从视觉识别到PID调参实战
2026/9/7 6:53:46 网站建设 项目流程

简介:面向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、解析串口数据、输出PWM,F103的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通道调低、AB通道压缩,得到只覆盖引导线的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(time=2000) 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_threshold=30) if blobs: blob = max(blobs, key=lambda 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 sum

flags字节用来表示红绿灯状态和路口信息,每一位代表一个状态:bit0=红灯、bit1=绿灯、bit2=检测到路口、bit3=检测到禁行区。这样一个字节就能传递所有视觉判断结果。

5.2 主控解析与决策策略

STM32端用串口中断接收,收满7个字节后先校验帧头和校验和,通过之后才更新全局变量。这里有个经验:数据加一个简单的时间戳或者监测接收时间间隔,如果超过200ms没有新帧,就认为视觉模块异常,让小车进入安全停车状态,防止失控冲撞。

上位机发送频率我们设在20Hz左右,也就是每50ms发一帧。这个频率和STM32控制周期50ms匹配,保证每一帧控制指令都有对应的新视觉数据。频率太高没意义,反而占用CPU;太低会导致转向反应迟钝。

OpenMV端的发送代码:

from pyb import UART uart = UART(3, 115200, timeout_char=100) 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题让我最大的收获,不是那个省一等奖,而是学会了一种调试思路——永远先怀疑通信和供电,再怀疑算法;永远给每个状态加上超时兜底;永远在现场留出重新标定的时间。如果你也准备参加下一届电赛或其他智能车赛,希望这篇能给你搭一个可用的底子,剩下的路,还得自己在调试点位上一个一个踩过来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询