简介:这份资源是2024年全国大学生电子设计竞赛H题巡线小车的完整工程,面向备赛电赛控制类赛题的学生与嵌入式爱好者。方案以STM32为主控、OpenMV摄像头巡线,核心思路是计算黑线与中心线的偏差进行转向控制,与官方要求的灰度传感器方案原理一致,可帮助读者理解巡线控制逻辑并迁移到自己的硬件平台。压缩包共1053个文件,以570个C源文件、256个头文件、51个汇编文件为主,另含工程配置、库文件与少量Python脚本,整体约27.45MB,目录结构完整,可直接用Keil打开编译。目前已有4259人学习下载。资源涵盖ABCD四点间丝滑衔接的控制难点、统一速度行驶的稳定性分析、MPU6050零漂问题及初始摆放角度对转向的影响等实战经验,读者可据此优化变速策略、缩短整体耗时,并参考四轮小车的同步微调思路。
1. 从一道赛题需求说起:为什么STM32+OpenMV是巡线小车的主流组合
每年电赛控制类题目公布后,总有一批队伍在方案选型上卡住:纯STM32方案跑得稳但视觉识别能力弱,纯OpenMV方案图像处理方便但实时控制响应跟不上。2024年电赛H题这类巡线小车任务,恰好把这两个矛盾点同时摆在了台面上——既要识别赛道元素(虚线、十字路口、路障),又要保证电机控制的实时性和稳定性。
这套完整工程给出的答案很直接:STM32负责底层运动控制和实时决策,OpenMV负责图像采集与赛道元素识别,两者通过串口通信分工协作。这个架构不是拍脑袋定的,而是经过大量队伍验证后沉淀下来的主流做法。它适合正在准备电赛控制类题目的学生、需要交巡线小车课程设计的同学,以及想参考一套完整工程结构的嵌入式开发者。接下来我会把这套方案从需求拆解、架构设计、模块实现到验收调试,完整地讲一遍。
2. 总体方案设计:双核分工与四个关键模块的职责划分
2.1 为什么选双核架构而不是单芯片方案
先算一笔账。巡线小车在运行过程中,OpenMV需要以至少30fps的帧率采集图像并做赛道识别,每次识别涉及颜色阈值分割、ROI区域提取、线性回归拟合中线等操作。如果把这些任务全部压到STM32上跑,即使是用H7系列,图像处理的耗时也会挤占电机PID控制的时间片,导致控制周期抖动。
双核架构的核心逻辑是:让OpenMV专注做它擅长的事——图像处理,输出的是结构化的赛道信息(比如中线偏移量、路口标志、障碍物方位),而不是原始图像数据。STM32则专注做实时控制,接收赛道信息后执行PID运算、舵机打角、电机调速。两者通过UART串口通信,波特率通常设在115200或更高。
这个分工带来的好处是:控制周期稳定在5ms以内,图像处理周期稳定在30ms左右,互不干扰。代价是需要额外处理串口通信的协议设计和数据校验,这部分后面会详细讲。
2.2 四个核心模块的职责与数据流
整套工程可以拆成四个模块来看:
视觉识别模块(OpenMV端):负责图像采集、赛道中线提取、元素识别(十字、虚线、障碍物)。输出格式通常是一帧包含多个字段的数据包,比如中线偏移量、赛道宽度、标志位。
通信模块(双端):定义数据帧格式,处理收发缓冲、校验和解析。这是最容易出问题的地方,后面避坑章节会重点讲。
运动控制模块(STM32端):接收视觉数据后,执行舵机PID和电机PID。舵机控制转向,电机控制速度。通常舵机用位置式PID,电机用增量式PID。
状态管理模块(STM32端):处理特殊赛道元素的逻辑,比如遇到十字路口时强制直行、遇到障碍物时减速绕行、出界时停车保护。
数据流是这样的:OpenMV采集图像 → 识别赛道 → 打包数据 → 串口发送 → STM32接收解析 → 状态机判断 → PID计算 → 输出PWM → 执行机构动作。整个链路中,任何一个环节的延迟都会影响小车的高速性能。
2.3 关键参数与选型参考
| 模块 | 选型建议 | 关键参数 |
|---|---|---|
| 主控 | STM32F103/F407 | 主频72MHz以上,至少2路UART |
| 视觉 | OpenMV H7/H7 Plus | 帧率≥30fps,支持UART输出 |
| 电机驱动 | TB6612或DRV8833 | 支持PWM调速,峰值电流≥2A |
| 舵机 | 模拟舵机或数字舵机 | 响应速度≤0.1s/60° |
| 电源 | 2S锂电+稳压模块 | 舵机独立供电,避免电压跌落 |
注意:舵机和电机最好分开供电。舵机堵转时电流冲击很大,如果和STM32共用一路LDO,容易导致MCU复位。这个坑每年都有队伍踩。
3. 视觉识别与串口通信的落地实现
3.1 OpenMV端赛道中线提取的核心逻辑
OpenMV的算法流程通常是:灰度化 → 阈值分割 → ROI提取 → 找色块或线性回归 → 计算中线偏移。下面是一段典型的实现代码:
import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) sensor.set_framesize(sensor.QQVGA) # 160x120,兼顾速度和精度 sensor.skip_frames(time=2000) uart = UART(3, 115200) # 只看图像下半部分,减少计算量 ROI = (0, 60, 160, 60) while True: img = sensor.snapshot() # 在ROI区域内做阈值分割,提取黑色赛道线 blobs = img.find_blobs([(0, 64)], roi=ROI, merge=True) if blobs: # 取最大的色块作为赛道线 line = max(blobs, key=lambda b: b.pixels()) cx = line.cx() # 中线x坐标 offset = cx - 80 # 相对于图像中心的偏移量 width = line.w() # 赛道宽度 flag = 0 # 标志位,后续扩展用 else: offset = 0 width = 0 flag = 1 # 丢失赛道 # 打包发送:帧头 + 偏移量 + 宽度 + 标志位 + 帧尾 uart.write(b'\xAA\xBB') uart.write(offset.to_bytes(2, 'little', signed=True)) uart.write(width.to_bytes(2, 'little')) uart.write(flag.to_bytes(1, 'little')) uart.write(b'\xCC\xDD')这段代码有几个设计点值得说清楚。第一,QQVGA分辨率是160x120,比QVGA快将近4倍,对于巡线任务来说精度完全够用。第二,ROI只取图像下半部分,因为赛道线在图像中永远出现在下方,上半部分是天空或远处背景,处理它纯属浪费算力。第三,find_blobs的阈值(0, 64)是针对灰度图的黑色赛道,如果赛道是白色或其它颜色,需要调整阈值范围。
3.2 串口通信协议的设计要点
上面代码里用的是自定义帧格式:0xAA 0xBB作为帧头,0xCC 0xDD作为帧尾,中间是有效数据。这种设计的好处是接收端可以通过帧头帧尾快速定位一帧数据的边界,避免因为丢包或错位导致解析混乱。
数据字段的字节数要提前约定好:偏移量用2字节有符号整数(范围-32768到32767,实际偏移量在-80到80之间,绰绰有余),宽度用2字节无符号整数,标志位用1字节。整帧长度固定为11字节。
STM32端的接收解析通常用状态机实现,或者用空闲中断+DMA的方式。状态机的方式更通用,适合初学者理解:
// STM32串口接收状态机(简化版) typedef enum { WAIT_HEAD1, WAIT_HEAD2, RECV_DATA, WAIT_TAIL1, WAIT_TAIL2 } RxState; RxState state = WAIT_HEAD1; uint8_t buf[7]; // 存储有效数据 uint8_t idx = 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t ch = USART_ReceiveData(USART1); switch (state) { case WAIT_HEAD1: if (ch == 0xAA) state = WAIT_HEAD2; break; case WAIT_HEAD2: state = (ch == 0xBB) ? RECV_DATA : WAIT_HEAD1; idx = 0; break; case RECV_DATA: buf[idx++] = ch; if (idx >= 7) state = WAIT_TAIL1; break; case WAIT_TAIL1: state = (ch == 0xCC) ? WAIT_TAIL2 : WAIT_HEAD1; break; case WAIT_TAIL2: if (ch == 0xDD) { // 一帧完整数据接收完毕,解析buf int16_t offset = (buf[1] << 8) | buf[0]; uint16_t width = (buf[3] << 8) | buf[2]; uint8_t flag = buf[4]; // 更新全局变量供控制循环使用 } state = WAIT_HEAD1; break; } } }这个状态机的逻辑是:只有完整收到帧头、7字节数据、帧尾后,才认为一帧有效。任何环节出错都回到初始状态重新等待帧头。这种设计能有效过滤掉噪声和错位数据。
3.3 控制周期的分配与中断优先级
STM32端的任务分配通常是这样的:串口接收用中断,PID控制用定时器中断,主循环处理状态机和逻辑判断。定时器中断的频率决定了控制周期,一般设在200Hz到500Hz之间,也就是2ms到5ms一次。
中断优先级要注意:串口接收中断的优先级应该高于定时器中断,否则可能出现数据还没收完就被PID中断打断的情况。但也不能太高,否则会影响系统的整体响应。通常的做法是串口接收设为抢占优先级1,定时器PID设为抢占优先级2。
4. 运动控制与状态机的工程实现
4.1 舵机与电机的PID参数整定
舵机控制用位置式PID,输入是中线偏移量,输出是舵机PWM占空比。电机控制用增量式PID,输入是目标速度和编码器反馈速度的差值,输出是电机PWM。
位置式PID的公式是:output = Kp * error + Ki * integral + Kd * derivative。对于舵机控制,通常只需要P和D,I项可以设为0,因为巡线对稳态误差要求不高,反而积分项容易导致超调。
增量式PID的公式是:delta = Kp * (e(k) - e(k-1)) + Ki * e(k) + Kd * (e(k) - 2e(k-1) + e(k-2))。增量式的好处是不需要累加积分项,天然抗积分饱和。
参数整定的顺序是:先调P,让小车能基本跟着线走但会有震荡;再加D,抑制震荡;最后如果稳态误差大再加少量I。舵机的P值通常在0.5到2.0之间,D值在0.1到0.5之间,具体要看舵机响应速度和赛道曲率。
4.2 特殊元素的识别与状态切换
巡线小车要处理的特殊元素包括:十字路口、虚线、障碍物、终点线。每种元素对应一个状态标志位,STM32收到标志位后切换到对应的处理逻辑。
十字路口的处理逻辑是:当OpenMV检测到赛道宽度突然增大(因为十字路口处横向也有线),标志位置1。STM32收到后,强制舵机回正、电机保持当前速度,持续一段时间(比如200ms)后恢复正常巡线。这段时间要足够让小车穿过路口。
虚线的处理逻辑是:当OpenMV连续几帧检测不到赛道线,但又不是完全丢失(比如宽度为0但偏移量在合理范围内),标志位置2。STM32收到后,保持上一次的舵机角度和电机速度,直到重新检测到赛道。
障碍物的处理逻辑是:OpenMV通过色块识别检测到障碍物,标志位置3。STM32收到后,减速并根据障碍物方位决定绕行方向。
状态切换的关键是防抖。不能因为一帧数据的异常就切换状态,通常需要连续3到5帧确认后才切换。这个防抖计数放在STM32端做,因为OpenMV端的帧率不稳定,放在STM32端更可靠。
4.3 代码结构的分层组织
一套可维护的工程代码应该分层组织:
Project/ ├── Core/ │ ├── main.c │ ├── stm32f1xx_it.c │ └── system_stm32f1xx.c ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── CMSIS/ ├── App/ │ ├── vision.c/h # 视觉数据处理 │ ├── motor.c/h # 电机控制 │ ├── servo.c/h # 舵机控制 │ ├── pid.c/h # PID算法 │ └── state.c/h # 状态机 └── Bsp/ ├── uart.c/h # 串口驱动 ├── timer.c/h # 定时器驱动 └── encoder.c/h # 编码器驱动App层放业务逻辑,Bsp层放硬件驱动,Core层放系统初始化。这样分层的好处是:换硬件平台时只需要改Bsp层,业务逻辑不用动;调PID时只改pid.c,不会影响其它模块。
5. 调试避坑:巡线小车最容易翻车的五个点
5.1 串口数据错位导致小车乱跑
现象:小车在直道上正常,一到弯道就突然猛打方向或者停车。
原因:串口通信没有帧头帧尾保护,或者帧头帧尾选得不好(比如用了0x00这种常见数据),导致接收端把数据中间的某个字节误判为帧头,解析出错误的偏移量。
解决:用双字节帧头(如0xAA 0xBB)和双字节帧尾,并且在接收状态机里严格校验。另外,偏移量用有符号整数时要注意字节序,OpenMV是小端模式,STM32也是小端模式,直接按小端解析即可。如果一端用了大端,数据就会完全错乱。
5.2 电源噪声导致MCU复位
现象:小车跑着跑着突然重启,或者舵机一动就死机。
原因:舵机和电机共用一路电源,舵机堵转时电流瞬间拉高,电压跌落导致MCU欠压复位。
解决:舵机单独供电,或者至少在电源入口处加一个大电容(1000uF以上)缓冲。电机驱动电源和MCU电源之间加磁珠或电感隔离。如果用的是锂电池,注意电池放电能力要足够,2S锂电持续放电能力至少10C。
5.3 PID参数整定方向搞反
现象:小车越调越震荡,减小P值反而更不稳定。
原因:PID输出的正负号和舵机转向的对应关系搞反了。比如偏移量为正时应该右转,但代码里写成了左转,形成正反馈。
解决:先确认舵机中值对应的PWM脉宽(通常是1500us),然后手动给一个固定偏移量,观察舵机转向是否正确。确认方向后再调PID参数。如果方向反了,把PID输出的符号取反即可。
5.4 OpenMV帧率不稳定导致控制抖动
现象:小车在直道上会轻微左右摆动,弯道时反应迟钝。
原因:OpenMV的帧率受光照影响很大,光照变化时帧率可能从30fps掉到15fps,导致STM32收到的数据更新频率不稳定。
解决:在STM32端做数据超时保护。如果超过一定时间(比如100ms)没有收到新数据,就保持上一次的有效数据,而不是用默认值。另外,OpenMV端要固定曝光和白平衡,避免自动调节导致的帧率波动。
5.5 赛道元素误识别
现象:小车在普通弯道处误判为十字路口,导致直行冲出赛道。
原因:十字路口的判断条件是赛道宽度突然增大,但某些急弯处由于图像畸变,赛道宽度也会增大。
解决:十字路口的判断不能只看宽度,还要结合赛道线的连续性。通常的做法是:检测到宽度增大后,再检查图像中是否存在横向的赛道线。如果只有宽度增大而没有横向线,就不触发十字路口逻辑。这个判断在OpenMV端做更合适,因为可以拿到原始图像信息。
6. 验收与答辩:怎么证明这套方案真的能跑
验收巡线小车方案,核心看三个指标:稳定性、速度和鲁棒性。稳定性看小车能不能连续跑完10圈不出界;速度看单圈用时;鲁棒性看在不同光照条件下能不能正常工作。
我一般会按下面的流程做验收测试:
| 测试项 | 测试方法 | 合格标准 |
|---|---|---|
| 直道稳定性 | 直道上连续跑10次 | 偏移量波动≤5个像素 |
| 弯道通过性 | 不同曲率弯道各跑10次 | 出界次数≤1次 |
| 十字路口 | 连续通过10个十字 | 误判次数≤1次 |
| 虚线处理 | 连续通过5段虚线 | 不停车、不跑偏 |
| 光照鲁棒性 | 室内/室外/阴影下各跑5圈 | 成功率≥80% |
| 连续运行 | 连续跑10分钟 | 无复位、无死机 |
答辩时,评委通常会问三个问题:为什么选这个方案、PID参数怎么整定的、遇到最棘手的问题是什么。第一个问题按第2章的逻辑回答就行。第二个问题要能说清楚整定顺序和每个参数的作用。第三个问题建议提前准备一个真实的调试案例,比如“串口数据错位导致小车乱跑,后来加了双字节帧头帧尾和状态机校验才解决”,这种具体的问题和解决过程比泛泛而谈更有说服力。
还有一个小技巧:答辩时带上调试工具。比如用串口助手实时显示OpenMV发送的数据,用示波器看PWM波形。评委看到你有完整的调试手段,对方案的可信度会加分不少。
从那以后我每次做控制类项目,都会在方案设计阶段就把调试接口预留出来——串口打印、LED状态指示、关键变量在线监控,这些看似麻烦的准备工作,在调试阶段能省下大量时间。希望这套方案能帮到你,少走一些弯路。
本文还有配套的精品资源,点击获取