简介:这份资源是2024年全国大学生电子设计竞赛H题巡线小车的完整工程,面向备赛电赛控制类赛题的学生与嵌入式爱好者。方案以STM32为主控、OpenMV摄像头实现巡线,核心思路是计算黑线与中心线的偏差并据此转向,与官方要求的灰度传感器方案原理一致,便于替换移植。压缩包共1053个文件,约27.45MB,以570个C源文件、256个头文件、51个汇编文件为主,另含工程配置、库文件与少量Python脚本,构成可直接编译的完整工程。目前已有4259人学习下载。读者可从中获取巡线控制逻辑、ABCD四点间丝滑衔接的调试思路、MPU6050零漂处理与初始摆放角度对转向影响的参考经验,以及变速行驶优化稳定性和耗时的改进方向,适合在此基础上二次开发与参数微调。
1. 从零拆解一套电赛巡线小车工程:STM32+OpenMV 到底怎么配合
如果你正在准备 2024 电赛 H 题这类巡线小车赛项,大概率已经翻过不少帖子,最后发现真正能跑通的完整工程少之又少。我手上这套 STM32+OpenMV 的工程,核心不是炫技,而是把「视觉识别—串口通信—电机闭环」这条链路做扎实。它适合两类人:一是第一次打电赛、需要一份能直接编译下载的参考框架的新手;二是做过小车但视觉和主控总是配合不好的老手。整套方案里 OpenMV 负责找线和识别元素,STM32 负责决策和控速,两者通过串口协议交换数据。下面我按实际复现顺序,把选型理由、接线、代码和踩坑点全部拆开讲。
2. 硬件选型与串口通信协议:为什么不用纯 STM32 方案
2.1 视觉方案选型:OpenMV 与纯灰度传感器的边界
纯灰度传感器方案在直道和缓弯上确实够用,但 2024 电赛 H 题的赛道元素里出现了断线、十字、锐角弯,灰度方案一旦丢线就只能靠编码器盲走,容错率极低。OpenMV 的优势在于它能输出线的角度和偏移量,而不是简单的开关量。我一般会把 OpenMV 的 ROI 设在图像下半部分,减少远处畸变干扰,同时把分辨率降到 QQVGA(160×120),帧率能稳在 40fps 以上。这里有个反直觉的点:分辨率越低,巡线反而越稳,因为噪点少了,find_blobs 的阈值也更好调。
选 OpenMV 的另一个理由是它的 UART 输出可以直接对接 STM32 的串口,不需要额外的电平转换。常见做法是把 OpenMV 的 P4(TX)接 STM32 的 PA10(RX),P5(RX)接 PA9(TX),共地。注意 OpenMV 的 IO 是 3.3V 电平,STM32 也是 3.3V,直接连没问题,但如果你用的是 5V 的 Arduino 类主控,就必须加电平转换,否则 OpenMV 的 RX 脚会被拉坏。
2.2 串口帧格式设计:定长帧比变长帧更抗干扰
很多新手喜欢用\n结尾的变长字符串传数据,比如"120,30\n",这种格式在实验室里没问题,但赛场上电机一启动,串口误码率立刻上升,变长帧一旦丢一个字节,后面全部错位。我在这套工程里用的是定长帧:帧头 0xAA、帧尾 0x55,中间 4 个字节分别是偏移量高八位、偏移量低八位、角度、校验和。STM32 端用状态机接收,收到帧头才开始存,收到帧尾才校验。
// STM32 串口接收状态机(放在 USART1_IRQHandler 里) #define FRAME_HEAD 0xAA #define FRAME_TAIL 0x55 uint8_t rx_buf[6]; uint8_t rx_cnt = 0; uint8_t frame_ready = 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t ch = USART_ReceiveData(USART1); if (rx_cnt == 0 && ch != FRAME_HEAD) return; // 没收到帧头就丢弃 rx_buf[rx_cnt++] = ch; if (rx_cnt >= 6) { if (rx_buf[5] == FRAME_TAIL) { uint8_t sum = rx_buf[1] + rx_buf[2] + rx_buf[3]; if (sum == rx_buf[4]) frame_ready = 1; // 校验通过 } rx_cnt = 0; // 无论对错都复位,防止卡死 } } }这段代码的关键在rx_cnt == 0 && ch != FRAME_HEAD这一句,它保证状态机只在帧头处启动,中间任何干扰字节都会被跳过。校验和用简单的累加和就够了,不用上 CRC16,因为串口线很短,误码主要是偶发跳变,累加和足够过滤。参数上,波特率我设的是 115200,再高的话 OpenMV 的串口发送会丢包,再低则帧率跟不上车速。
2.3 电机驱动与编码器接口分配
电机驱动我用的是 TB6612,比 L298N 效率高、发热小。STM32 这边用 TIM1 的 CH1/CH2 输出 PWM 给左右电机,TIM2 和 TIM3 的编码器模式读左右轮速。注意编码器模式要配置成「编码器接口模式」,而不是普通的输入捕获,否则计数方向会乱。接线时 A 相接 PA0、B 相接 PA1,对应 TIM2 的 CH1 和 CH2。如果你用的是霍尔编码器,上拉电阻别忘了加,否则波形上升沿会变缓,高速时丢计数。
3. OpenMV 端巡线算法:ROI、阈值与角度解算
3.1 图像预处理:灰度化与自适应阈值
OpenMV 的find_blobs对光照非常敏感,赛场顶灯和窗户光混在一起,固定阈值基本必翻车。我一般用sensor.set_auto_gain(False)和sensor.set_auto_whitebal(False)先把自动增益和白平衡关掉,然后在赛道现场用sensor.snapshot()取一帧,手动调 LAB 阈值。更稳的做法是跑一个简单的自适应:取图像底部 20 行的灰度均值,动态调整黑线阈值。
# OpenMV 端巡线主循环(简化版) import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) sensor.set_framesize(sensor.QQVGA) sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) sensor.skip_frames(time=500) uart = UART(3, 115200) while True: img = sensor.snapshot() # ROI 取图像下半部分,减少远处干扰 roi = (0, 60, 160, 60) # 灰度阈值:黑线在 0-80 之间,具体现场调 blobs = img.find_blobs([(0, 80)], roi=roi, pixels_threshold=100, area_threshold=100) if blobs: b = max(blobs, key=lambda x: x.pixels()) # 取最大色块 offset = b.cx() - 80 # 中心偏移,范围 -80 到 80 angle = b.rotation() # 色块旋转角,单位弧度 # 打包成定长帧 frame = bytearray([0xAA, (offset >> 8) & 0xFF, offset & 0xFF, int(angle * 100) & 0xFF, 0, 0x55]) frame[4] = (frame[1] + frame[2] + frame[3]) & 0xFF uart.write(frame) else: # 丢线时发一个特殊值,STM32 端做保护 uart.write(bytearray([0xAA, 0x7F, 0xFF, 0x00, 0x00, 0x55])) time.sleep_ms(10)这里pixels_threshold和area_threshold都设成 100,是为了过滤掉赛道上的小污点。b.rotation()返回的是色块的旋转角,对于细长黑线来说,这个角度直接反映线的倾斜程度。注意rotation()在色块接近正方形时不稳定,所以我在 STM32 端加了判断:如果色块宽高比小于 1.5,就忽略角度,只用偏移量。
3.2 偏移量与角度的融合决策
STM32 收到偏移量和角度后,不能直接拿偏移量做 PID,否则在锐角弯会震荡。我的做法是:直道和缓弯用偏移量做位置环 PID,锐角弯用角度做前馈。具体来说,当abs(angle) > 30度时,把角度乘以一个系数直接叠加到转向量上,同时降低车速。这个系数我一般从 0.5 开始试,太大车会抖,太小入弯不及时。
// STM32 端转向决策(放在主循环里) float offset = (int16_t)((rx_buf[1] << 8) | rx_buf[2]); float angle = (int8_t)rx_buf[3] / 100.0f; float steer = 0; if (fabs(angle) > 0.5f) { // 约 30 度 steer = offset * 0.3f + angle * 80.0f; // 角度前馈 set_speed(300); // 入弯降速 } else { steer = offset * 0.8f; // 纯位置环 set_speed(500); } // steer 限幅后给电机 if (steer > 200) steer = 200; if (steer < -200) steer = -200;offset * 0.3f和angle * 80.0f这两个系数不是拍脑袋来的,offset 范围是 -80 到 80,乘 0.3 后转向量在 -24 到 24 之间;angle 范围是 -1.5 到 1.5 弧度,乘 80 后是 -120 到 120,两者叠加后锐角弯的转向量明显大于直道,符合预期。set_speed里的 300 和 500 是 PWM 占空比,具体值要根据你的电机和电池电压调,电池从 8.4V 掉到 7.4V 时,同样的占空比车速会慢 15% 左右,所以赛前最好用稳压源或者做电压补偿。
3.3 丢线保护与编码器辅助
OpenMV 丢线时发0x7F 0xFF,STM32 收到后不能立刻停车,否则车会顿挫。我的策略是:丢线后先保持上一次的转向量,同时用编码器差值判断车是否在直行。如果左右编码器差值很小,说明车还在直线上,继续走;如果差值突然变大,说明车在偏,立刻反向修正。这个逻辑能救回大部分因为反光导致的瞬时丢线。
4. 避坑与排查:调车现场最常见的五个翻车点
4.1 串口数据错位,车一启动就乱转
现象:电机不转时串口数据正常,电机一启动,STM32 收到的偏移量变成乱码,车原地打转。 原因:电机启动瞬间电流突变,串口地线被抬升,导致电平误判。另外 OpenMV 和 STM32 如果分别供电,共地没做好也会这样。 解决:电机电源和主控电源分开走线,在 TB6612 的 VM 和 GND 之间并一个 470uF 电解电容,串口线用双绞线或者屏蔽线。如果还不行,把波特率降到 57600,牺牲一点帧率换稳定。
4.2 OpenMV 帧率虚标,实际只有 15fps
现象:代码里设了 QQVGA,但串口收到的帧间隔明显大于 10ms,车高速时反应迟钝。 原因:find_blobs的pixels_threshold设得太小,图像里噪点全被当成色块,算法耗时暴涨。另外sensor.skip_frames没加够,自动增益没稳定。 解决:把pixels_threshold提到 200 以上,area_threshold同步提高。在sensor.reset()后加sensor.skip_frames(time=1000),确保第一帧就是稳定图像。实测这样改完,QQVGA 下能稳在 35fps 以上。
4.3 PID 参数整定:P 大了震荡,P 小了画龙
现象:车在直道上左右摆,或者入弯时切内道切得太晚。 原因:位置环 PID 的 P 和 D 没配合好,或者采样周期和串口帧率不匹配。 解决:先把 I 和 D 置零,只调 P,让车能勉强走直线但有点摆;然后加 D,摆会明显减小;最后加一点点 I 消除稳态误差。采样周期必须和串口帧率一致,OpenMV 发 10ms 一帧,STM32 的 PID 就 10ms 算一次,不要用定时器另开一个 5ms 的周期,否则数据没更新,PID 在算旧值。
4.4 电池电压下降导致后半程跑偏
现象:刚上场时车很猛,跑两圈后入弯速度明显不够,甚至丢线。 原因:锂电池从 8.4V 掉到 7.4V,电机转速下降,但 PID 参数是按满电调的。 解决:在 STM32 里用 ADC 读电池电压,做一个简单的占空比补偿:pwm_out = pwm_base * (8.4 / v_bat)。注意补偿系数不要超过 1.3,否则低压时占空比过大,电机电流会超限。
4.5 编码器计数方向反了,速度环变正反馈
现象:速度环一开,车就飞车,轮子越转越快。 原因:编码器 A/B 相接反,或者 TIM 的编码器模式极性配错,导致速度反馈符号反了。 解决:先不开速度环,用手转轮子,看串口打印的编码器值是不是正转增加、反转减小。如果反了,把 A/B 相接线对调,或者在代码里把计数取反。这个坑很隐蔽,因为位置环不开速度环时表现正常,一开速度环就炸。
5. 进阶技巧:用 VOFA+ 做实时波形调参
调 PID 最痛苦的是盲调,改一个参数下载一次,效率极低。我后来养成一个习惯:用 STM32 的另一个串口把偏移量、角度、转向量、左右轮速打包发给 VOFA+,用 JustFloat 协议实时看波形。这样 P 大了还是 D 大了,一眼就能看出来。具体做法是定义一个float data[4],每 10ms 发一次,VOFA+ 里选 JustFloat 模式,波特率 115200 或更高。
// VOFA+ JustFloat 协议发送(放在 10ms 定时器里) float data[4]; data[0] = offset; data[1] = angle; data[2] = steer; data[3] = encoder_diff; uint8_t tail[4] = {0x00, 0x00, 0x80, 0x7f}; // JustFloat 帧尾 USART_SendData(USART2, (uint8_t*)data, 16); // 发 4 个 float USART_SendData(USART2, tail, 4); // 发帧尾注意USART_SendData这里我简化写了,实际要用 DMA 或者阻塞发送,否则会丢字节。VOFA+ 的 JustFloat 协议要求帧尾是0x00 0x00 0x80 0x7f,这个不能错,错了波形出不来。调参时我一般把steer和offset放一起看,如果steer明显滞后于offset,说明 D 太小;如果steer在offset附近高频抖动,说明 P 太大。
还有一个技巧是给 OpenMV 加一个「调试模式」:按住 OpenMV 上的按键时,串口只发数据不控车,同时 IDE 里显示 ROI 框和色块。这样你可以一边推车一边看识别效果,比盲猜阈值快得多。从那以后我每次调新车,都强制先跑一遍 VOFA+ 波形和 OpenMV 调试模式,确认数据链路没问题再上电机。希望帮到你。
本文还有配套的精品资源,点击获取