
简介2024年电赛H题巡线小车演示工程以STM32为主控、OpenMV摄像头巡线完整展示偏差控制与小车运动控制方案。工程虽采用摄像头获取赛道黑线但控制原理与电赛要求的灰度传感器一致可通过替换传感器直接迁移到赛题场景适合参加电赛的学生、嵌入式开发爱好者参考。压缩包共1053个文件以C源文件、头文件、链接脚本、配置文件及Keil/STM32CubeMX工程文件为主并包含OpenMV端Python脚本整体体积27.45MB目录结构规整便于检索与二次开发。工程中针对巡线偏差、MPU6050姿态数据零漂、初始摆放角度、四轮同步等关键问题给出了实现代码与注释也保留了统一速度行驶的逻辑留出变速优化空间。包内还附带编译产物与依赖库可快速烧录验证已有4246人学习适合用来理解电赛巡线小车从传感器数据到电机控制的完整链路。 2024年电赛H题做的是自动行驶小车我们队最后用的方案是STM32 OpenMVSTM32负责运动控制和整机状态管理OpenMV负责视觉识别和寻迹。这套完整工程赛后被好几个学弟要过干脆写一篇梳理文章讲清楚每一块为什么这么做、代码怎么组织、现场最容易在哪里翻车。如果你正在备赛电赛或者想把手里的OpenMV和STM32真正连起来做一辆能跑的小车这篇内容应该能直接拿来抄作业。先说结论这套方案不算惊艳但胜在稳定、好调、容错率高。视觉部分偶尔抽风底层控制不会崩底层控制再乱状态机也能把车拉回安全状态。比赛比的不是谁的理论更花哨而是4天3夜之后谁的车还能稳稳跑完全程。1. 方案选型为什么是STM32OpenMV的组合1.1 视觉与控制分工OpenMV 的处理核心是 MicroPython 环境写图像算法确实方便跑个找色块、模板匹配都是几行代码的事。但它的算力放到自动行驶小车上是不够看的跑一帧图像处理再加上数字识别模型推理CPU已经接近满载。这时候你再让它去输出PWM、读编码器、跑PID控制帧率一波动小车立刻开始画龙控制周期完全没法保证。STM32的强项正好补上这块短板。F103系列价格便宜外设丰富实时性有硬件保障。我们把所有跟时间强相关的任务全部丢给STM32定时器PWM输出、编码器计数、速度闭环、串口解析、比赛流程的状态机管理。OpenMV只负责一件事——把“我看到了什么”这个结论通过串口告诉STM32。视觉可以慢半拍但控制必须每个周期都不掉链子这就是这套方案的核心分工逻辑。K210也提一句。它算力比OpenMV强还便宜不少队也用它。但K210的工具链和IDE调试体验确实不如OpenMV顺滑OpenMV IDE里能直接看实时画面、拖动阈值条、一键采集数据集比赛时间紧张的时候这个调试效率差异会被放大。所以我个人还是推荐OpenMV做视觉端这不是说K210不好而是电赛场景下“好调试”比“算力强”更值钱。1.2 通信链路与数据流视觉和控制分开之后两块板子之间怎么通信是关键。我们选用串口UART波特率1152008位数据位、1位停止位、无校验。为什么不选I2C或SPIOpenMV对这两者的封装在长期运行下不够稳定而且串口调试起来太方便了只要一个USB转TTL模块就能在电脑上直接监听OpenMV到底发了什么排查问题效率极高。整个系统的数据流是这样的OpenMV采集图像经过寻迹算法计算出偏差值再经过数字识别得到目标编号然后把这两类关键信息打包成固定格式的数据帧通过串口发送给STM32STM32在串口接收中断里逐字节解析出偏差值和目标编号喂给PID控制器计算左右轮目标速度最后通过PWM驱动电机同时用编码器做速度反馈。信息单向流动逻辑清晰基本没有循环依赖。2. 硬件配置与供电设计2.1 车体、电机与驱动模块车体选择上两轮差速底盘是最省心的方案转弯半径小控制模型简单。电机我们用的是带霍尔编码器的N20减速电机减速比30:1左右扭矩够用编码器线数越高分辨率越好。没有编码器反馈的电机不适合做闭环新手容易忽略这一点总想着开环也能跑但稍微有点负载变化或者电池电压下降车速就会漂巡线稳定性大打折扣。电机驱动模块强烈建议TB6612FNG别用经典的L298N。L298N的饱和压降太大同样的电池电压实际到电机上的电压少一大截发热还严重。TB6612内阻小PWM响应快体积也小很适合小车。接线上注意STBY引脚要拉高不然驱动芯片一直处于待机状态电机怎么都不转这个问题排查起来最浪费时间。2.2 供电系统最容易翻车的坑电赛小车最常见的诡异问题——OpenMV突然重启、STM32无缘无故复位、图像上出现横向条纹十有八九就是供电没做好。电机启动瞬间的电流峰值非常大会把整块电池电压瞬间拉低如果视觉模块和控制板共用同一条5V电源线压降冲击直接就把OpenMV干重启了。我们的供电方案是2S锂电池出来分两路一路经过MP1584降压模块输出5V单独给OpenMV供电另一路经过AMS1117-3.3给STM32控制板供电。电机驱动直接由电池供电不经过稳压模块。这样电机大电流波动不会直接冲击逻辑电源。除此之外在电机两端并联一个100uF以上电解电容加一个104陶瓷电容抑制换相火花干扰这个细节能解决掉图像上很多莫名奇妙的噪声。2.3 传感器安装与视角OpenMV的安装高度、镜头倾角直接影响视觉算法效果。装太高视野大但数字目标变小识别难度增加装太低地面线条在画面里变形严重。我们最终把OpenMV装在车头偏中间位置镜头略微下压15度左右视野覆盖车前0.3米范围既能看清引导线数字出现在画面下方时也有足够的分辨率。固定用铜柱加3D打印底座千万别用双面胶凑合比赛现场跑起来的震动会让镜头角度慢慢偏移图像阈值突然全部失效。3. OpenMV端巡线识别与数据打包3.1 巡线偏差计算巡线的基本原理不复杂把图像转成灰度设定阈值把引导线从背景中分割出来然后找到最大的连通区域计算它的中心横坐标和画面中心线的差值这个差值就是偏差。偏差为负说明车偏左了为正说明偏右STM32拿到这个值就能计算转向量。实际代码里有两个关键优化。第一处理区域限制在ROI内只看画面下方2/3区域不要全图扫描处理时间直接砍半帧率明显提升。第二阈值不要写死比赛现场灯光、反光、地面材质都会变化固定阈值很容易翻车。我们是用OpenMV IDE的阈值编辑器在现场跑几圈分别记录亮、暗、反光条件下的几组最佳阈值然后根据环境光线做切换。比赛现场环境变化快这个准备工作能做多少就做多少。OpenMV巡线的核心代码大概是这样import sensor, image, time from pyb import UART import ustruct sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_vflip(True) sensor.skip_frames(30) THRESHOLD (30, 100) # 在现场标定这里只是示例 ROI (0, 60, 160, 60) # 只看画面下方区域 uart UART(3, 115200, timeout_char1000) while True: img sensor.snapshot().binary([THRESHOLD]) blobs img.find_blobs([(255, 255, 255)], roiROI, pixels_threshold20) error 0 if blobs: b max(blobs, keylambda x: x.pixels()) error b.cx() - (img.width() // 2) data ustruct.pack(bbb, 0xAA, 0x55, error) uart.write(data) time.sleep_ms(25)注意blobs为空的时候要返回一个默认的偏差值并附加状态标志让STM32知道当前没有看到线。实际工程里这个状态处理比代码本身重要得多后面通信协议部分会细说。3.2 数字识别的工程化思路H题需要识别数字这块我们试过模板匹配NCC现场直接放弃。模板匹配对光照、角度、大小太敏感稍微换个距离识别率就崩。最终用的是OpenMV官方提供的数字识别模型配合自采数据集做微调。数据集采集是个体力活但直接决定识别率的上限。我们把数字卡片放在跑道上不同位置每个数字拍三五十张覆盖不同距离、不同角度、不同光线条件尽量模拟比赛现场的视角。然后用TensorFlow训练导出tflite模型放进OpenMV的SD卡。整个流程OpenMV官方文档有详细教程照着做就行。这里有个容易忽略的关键点输入模型前图像要预处理成模型要求的尺寸并做灰度化不做预处理的话识别效果会差非常多。很多人训完模型直接喂原图识别率上不去就是这个原因。3.3 通信协议与状态位设计OpenMV发给STM32的数据不能只发一个偏差值还要带上识别结果、状态标志、置信度等信息。我们定义的帧结构是字段长度说明帧头2字节0xAA 0x55state1字节OpenMV当前状态0寻迹、1识别目标、2停车target1字节识别到的数字编号error1字节巡线偏差有符号数confidence1字节识别置信度0-100sum1字节前面所有字节累加和校验直接用累加和挡住串口误码足够用了。发送频率固定在20Hz到50Hz之间不要每帧图像都发STM32控制周期是固定的视觉数据只要足够新就行20Hz实测下来完全够用还能降低串口负担。4. STM32端串口解析与差速控制4.1 CubeMX配置要点STM32端我们用STM32CubeMX生成初始化工程HAL库开发。需要开启的外设从功能上分是这几类一路定时器输出两路PWM驱动左右电机两路编码器模式定时器读取左右轮编码器一路串口接收OpenMV数据一路串口做调试输出再加几个GPIO控制电机方向和状态LED。PWM频率建议10kHz左右这个范围电机电感响应平滑噪声也小。编码器模式要在定时器的两个通道上同时使能Encoder Mode这样TI1和TI2的边沿跳变都会计数分辨率翻倍。串口接收用中断方式逐字节接收方便做状态机解析。CubeMX里需要检查的配置点是系统时钟用外部晶振保证串口波特率准确内部RC振荡器跑串口容易有偏差PWM定时器的Prescaler和Period算好输出10kHz编码器定时器配置为Encoder Mode编码器极性选对UART使能接收中断波特率1152008N14.2 串口数据解析状态机串口解析是STM32端最容易写崩的地方。新手常见的写法是在主循环里判断读到的字节是否为0xAA是就认为帧头来了结果数据错位一个字节整帧就废了后面再收到什么都是乱的。正确做法是状态机逐字节处理状态0等待0xAA状态1等待0x55状态2按长度接收数据域并累加校验收满后校验通过就更新全局变量。核心逻辑简化代码如下uint8_t rx_state 0; uint8_t rx_buf[8]; uint8_t rx_cnt 0; uint8_t rx_sum 0; void UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { uint8_t byte rx_byte; switch (rx_state) { case 0: if (byte 0xAA) rx_state 1; else rx_state 0; break; case 1: if (byte 0x55) { rx_state 2; rx_cnt 0; rx_sum 0; } else rx_state 0; break; case 2: if (rx_cnt 4) // 前4字节是数据 { rx_buf[rx_cnt] byte; rx_sum byte; } else // 第5字节是校验 { if (rx_sum byte) process_frame(rx_buf); rx_state 0; } rx_cnt; if (rx_cnt 5) rx_state 0; break; } HAL_UART_Receive_IT(huart2, rx_byte, 1); } }这段代码的核心是“不允许中间状态停留太久”。如果连续几百毫秒没有收到完整帧就把状态强行清零重新开始找帧头。另外我们还加了心跳机制STM32每次收到合法帧就更新一个时间戳主循环里检查这个时间戳超过500毫秒没刷新就自动停车。这个保护非常重要因为OpenMV一旦跑飞或者程序卡死视觉数据断了如果小车还在按最后一条指令狂奔后果很严重。自动停车能保证系统就算出问题也是停在原地而不是冲出去。4.3 差速模型与PID调参差速小车的转向控制核心公式只有一行int16_t left_speed base_speed turn; int16_t right_speed base_speed - turn;base_speed是基础前进速度turn是根据偏差算出来的转向修正量。偏差为正说明车偏右转向修正量就让左轮加速、右轮减速车自然向左修正。正负号要看电机实际安装方向装反了车会越修越偏调试时先确认方向。turn的计算我们用的是PD控制不是标准PID。巡线系统本身没有稳态误差I项加了反而容易积分饱和转弯时过冲。调参顺序有个口诀先P后DP太小转弯不积极车会冲出线P太大车在线上来回抖D用来抑制抖动的趋势但如果地面线条本身有噪声D加太大反而放大抖动。实际调试时先把I和D都设为0只调P让车能勉强跟线然后逐渐加D让运动更平滑。速度环是另一套PID用编码器反馈把左右轮速度稳定在目标值上。速度环放在定时器中断里1ms执行一次转向环放在主循环里10ms执行一次两级控制互不干扰。5. 联调中踩过的坑与现场应对5.1 烧录与连接报错比赛现场最让人崩溃的是STM32突然连不上调试器。最常见的报错error: no stm32 target found! if your product embeds debug authentication这一大串英文看着吓人其实原因比较集中SWDIO和SWCLK两根线接反了、目标板供电不正常、芯片被开了读保护。排查顺序建议这样先量一下目标板3.3V是否正常再看四根线SWDIO、SWCLK、GND、3.3V的连接是否和调试器定义一致还不行的话按住复位键点连接联上的瞬间松开手不少初始化失败的芯片都能这样救回来。如果芯片确实被开了读保护用STM32 ST-LINK Utility或者CubeProgrammer做一次Full Chip Erase把Option Bytes里的读保护位清掉。这个操作会把Flash程序抹掉但总比无法下载强。写代码前先把调试环境的问题解决不然比赛现场才来折腾这些心态直接崩。ST-LINK的虚拟串口在设备管理器里显示黄色感叹号这个也比较常见。原因是ST-LINK驱动不对需要装官方驱动STSW-LINK009装完就能识别出STM32 Virtual Com Port。有些系统装不上是因为驱动签名问题用管理员模式安装基本能解决。5.2 通信乱码与丢帧OpenMV和STM32串口不通、乱码、丢数据第一步永远先查共地。两块板子不共地串口电平没有参考点什么数据都可能出来。排查完共地再看波特率两边都是115200最好再确认一遍。如果这些都没问题还乱码直接降波特率到9600试一下比赛现场各种电机、电源模块产生的电磁干扰很大低波特率的抗干扰能力确实强不少用稳定性换速度一局比赛跑下来完全够用。丢帧的话多数是帧头同步问题。这里有个经验帧头用0xAA 0x55这两个字节的组合在二进制里特征比较明显不容易和随机噪声混淆。如果还是偶尔丢帧可以在SM32端加上文说的超时重同步机制错几帧没问题只要能在下一帧恢复就行。5.3 图像干扰与数字误判供电没隔离的时候电机一加速OpenMV画面就会出现横向条纹严重时直接黑屏重启。解决方案前面已经说过了电源分开、电机端加电容。还有一个更隐蔽的问题OpenMV的镜头会因为整车震动而轻微移位导致对焦变化阈值突然全部失效。比赛前用胶带或者螺纹胶把镜头固定死千万不要觉得这步无所谓我们联调时就因为这个吃了大亏跑着跑着图像突然发灰排查了很久才发现是镜头松了。现场如果某个数字老是识别错最快的办法不是重新训练模型而是检查数字卡片的摆放距离和曝光。数字太小经常是因为OpenMV的自动曝光把地面照得太亮把白平衡固定、手动设置曝光时间后识别率会稳定很多。识别不稳定的时候可以在OpenMV端做多帧统计比如连续5帧里同一个数字出现3次以上才确认STM32端也做一次确认才执行停车动作两道保险能把误判率压得很低。最后说点比赛层面的心得。电赛四天三夜真正写代码的时间很少大量时间都在跟供电、机械、通信作斗争。能提前准备的东西一定要提前弄好PID初值、通信协议、数据集采集、阈值标定这些都是可以提前磨刀的功夫。现场最忌讳的是临时改架构。这套STM32OpenMV方案最大的优点就是模块之间耦合弱OpenMV的视觉逻辑改了STM32不用动STM32控制策略改了OpenMV不用动。甚至在比赛现场发现规则细节有调整也能快速在某一端单独改代码接住这种余量比任何花哨算法都值钱。本文还有配套的精品资源点击获取