简介:基于OpenMV与STM32C8T6的循迹小车完整项目资料,面向嵌入式视觉与运动控制方向的学习者,适合零基础入门,也可作为毕设、课程设计或工程实训参考。实现思路完整:OpenMV端选取画面中部靠下的ROI区域做阈值化,提取黑线中心后运行PID计算,再按自定义协议将控制量发送至STM32;STM32对PID结果平方处理,与基准速度叠加得到最终占空比,完成循迹闭环。压缩包共157个文件,大小约7.79MB,主要包含.c/.h固件源码、.uvprojx工程文件、.ioc初始化配置、编译中间文件以及OpenMV的.py脚本,STM32部分采用HAL库编写,目录结构清晰,便于阅读和二次开发。目前已有3693人学习下载。资料覆盖图像阈值化、黑线中心提取、PID参数调整、串口通信协议和PWM输出等关键环节,可帮助快速搭建小车原型、理解完整视觉循迹方案,也便于后期扩展运控算法。
1. 双芯片分工:为什么循迹小车需要 OpenMV 和 STM32 搭配
很多入门者做循迹小车第一反应是用灰度传感器排成一排,靠地面反射差判断黑线。这套方案确实能跑,但遇到十字路口、直角弯、断线或者光线变化剧烈的场地,调参的时间和精力往往超过写代码本身。视觉循迹之所以逐渐成为主流,是因为摄像头能一次性拿到整条轨迹的上下文,而不是只看到车底那几厘米的地面。可摄像头采集一帧图像的原始数据量很大,用普通单片机做实时处理会耗尽 CPU,更别提同时还要处理电机 PID、编码器反馈和通信协议。于是把视觉识别交给 OpenMV,把运动控制交给 STM32,两者通过串口一帧帧交换结果,成为最常见的工程分工。OpenMV 负责“看清线在哪”,STM32 负责“让车跟住线”,这种解耦让每一端的代码都足够简单,也方便后续把 OpenMV 换成 K210 或树莓派。本文就按这条路,把硬件接线、图像处理、通信协议和电机控制串起来讲透。
2. 图像侧实现:OpenMV 上的二值化、巡线算法与数据集调参思路
2.1 OpenMV 在视觉循迹里到底做了什么
OpenMV 是一块自带摄像头和 MicroPython 解释器的嵌入式视觉开发板,典型型号如 OpenMV4 H7 Plus。它内部运行着 MicroPython 固件,可以直接调用sensor、image等模块完成图像采集和处理。在循迹小车里,OpenMV 承担的工作通常只有三件:采集图像、提取轨迹特征坐标、把坐标结果通过串口发出去。至于电机怎么转、转向多大角度,OpenMV 不做,也不需要做。这样的职责划分让图像处理频率能维持在 30 到 50 帧每秒,足够支撑校园赛道里的常规车速。
OpenMV 的 IDE 就是官方提供的 OpenMV IDE,下载后通过 USB 连接板子即可烧录程序。第一次使用建议先跑官方例程hello_world,确认摄像头能正常出图,再逐步把处理逻辑加进去。调试时 OpenMV IDE 里的帧缓冲区视图非常关键,因为它能实时显示当前画面、二值化结果和绘制出的辅助线,调参效率比纯粹看串口日志高很多。
2.2 灰度图与二值化阈值选择
赛道线的颜色一般与地面有明显差异,黑色胶带配浅色地面是常见组合。处理流程通常分三步:从 RGB 图转灰度图、对灰度图做二值化、在二值化结果上寻找黑色像素的重心或者色块区域。二值化的目的是把灰度图变成纯黑白的图像,前景为黑线、背景为白色,这样后面做阈值判断就非常快速可靠。
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) red_threshold = (0, 60) # 根据实际赛道调整,灰度值低于60视为黑线 while True: img = sensor.snapshot() img.binary([red_threshold]) # 二值化,黑线变白,背景变黑 blobs = img.find_blobs([(255, 255)], pixels_threshold=50, area_threshold=50)这段代码里,binary([red_threshold])会做原地二值化,把灰度值落在 0 到 60 之间的像素点变成白色,其余变成黑色。find_blobs在二值化后的画面里搜索白色连通域,也就是寻找黑线形成的白色区域。pixels_threshold用来过滤掉过小的噪点,太低的阈值会把反光白点也算进来。area_threshold作用于连通域面积,两个参数一起使用时先过滤像素数,再过滤面积,避免摄像头抖动造成的单帧闪烁。
这里容易踩的第一个坑是阈值写死。环境光线变化后,同样一条黑线的灰度值会整体漂移,阈值偏大或偏小都会让二值化结果崩掉。常见做法是现场用 OpenMV IDE 的“阈值编辑器”窗口观察直方图,手动找出合适的上下限,然后用代码固定下来。更稳妥的办法是每次开机做几秒钟自动校准,扫描整幅图像的灰度分布,把最小值加上一个偏移量当上限。
2.3 线性回归巡线与偏差计算
色块方案在直线和缓弯上表现不错,但到了急弯或十字路口,单纯找色块重心容易丢线。更好的方案是用img.get_regression()对二值化后的白线做线性回归拟合。线性回归会返回一条直线的角度和偏移量,OpenMV 官方文档里也推荐这条路,因为回归算法对断线和遮挡有一定鲁棒性。即使线只有一半可见,回归结果依然能给出合理的走向。
line = img.get_regression(thresholds=[(255, 255)], robust=True) if line: rho = line.rho() # 线到原点的距离 theta = line.theta() # 角度,0度表示水平 img.draw_line(line.line(), color=127)robust=True表示使用更抗离群点的回归方式,代价是计算量稍大,但对光照不均、反光斑等情况更稳。得到的rho和theta可以换算成与画面中心线的横向偏差:
import math if line: err = line.rho() / math.cos(line.theta())这个err就是黑线相对画面中心的偏移量,正值表示线在右侧,负值在左侧。把这个err映射到 0 到 255 之间,就是后面要发给 STM32 的方向信息。OpenMV 的 ROI 区域也可以设置,例如只关注画面下半部分,减少远处背景的干扰。ROI 设置后get_regression只在该区域内搜索,定位更稳定,计算量也降低。
2.4 OpenMV 数据集与小数据集调优技巧
很多做 OpenMV 的人会在训练神经网络时提到“数据集”,比如收集赛道图像打标签喂给 CNN。但对传统图像处理方案来说,“数据集”更多指一组代表性的场景截图,用来反复调试阈值和回归参数。实际做法是先把车辆停在赛道的直线、弯道、十字路口、断线处,用 OpenMV IDE 分别截图保存,然后离线逐张查看二值化效果,用这些截图验证参数是否在所有场景下都成立。每张截图都命名成场景名加光线条件,比如cross_afternoon.jpg、left_turn_shadow.jpg。
小数据集的用法不止于调阈值,还可以用来设计消融实验:固定其他参数,只改pixels_threshold,观察哪一档让十字路口识别最稳定。这类离线验证能显著减少到场地后反复烧录的次数。OpenMV 也支持 TF 卡,可以把截图直接存卡里,或者用 UART 把图像发送到电脑保存。我自己习惯写一段按按钮截图的小脚本,按一下腰杆存一张,回办公室慢慢分析。
3. 两机通信:OpenMV 与 STM32 的 UART 协议设计与常见坑
3.1 通信方式选型:UART、I2C 还是 SPI
OpenMV 与 STM32 之间的数据量很小,每帧只需要几个字节的方向偏差和车速标志,因此最常用也最不容易出错的方案是 UART。UART 是异步串口,两根线就能通信,速率设置为 115200 或者 256000 都足够。SPI 和 I2C 在原理上也能用,但 SPI 需要额外管理片选和时钟极性,I2C 的地址冲突和上拉电阻在调试期并不省心。唯一要注意的是电平匹配,OpenMV 的 IO 口多为 3.3V 电平,STM32 若工作在 5V 供电,需要确认引脚是否兼容 3.3V,或者直接选 3.3V 供电的 STM32 最小系统板。用 USB 转 TTL 调试时,TX、RX 要交叉连接,同时共地,否则通信会出现乱码。
3.2 自定义协议:帧头、数据位、校验位
裸发裸收的方式不可取,OpenMV 发一个err值,STM32 就直接用,看似简单,实际上任何一帧被干扰错位,后面的数据全会乱掉。工程上至少需要一个带帧头的协议,最简单的方法是帧头加数据加校验。下面的代码定义了一个 5 字节协议:帧头0xAA,err一个字节,车速一个字节,最后是校验字节。
from pyb import UART uart = UART(3, 115200, timeout_char=1000) uart.init(115200, bits=8, parity=None, stop=1) def pack_and_send(err, speed): header = 0xAA checksum = (header + err + speed) & 0xFF data = bytes([header, err, speed, checksum, 0x55]) uart.write(data)校验字节用简单累加和,0x55作为帧尾。keysum计算时把帧头和所有数据加在一起取低 8 位,接收端做同样的累加对比,如果发现不一致就丢弃这一帧。这样发过去的每一条数据都有明确的边界和自校验能力,STM32 解析起来非常直接。
如果后续发现偶尔有帧错位,可以在协议里加一个frame_id序号,递增发送,STM32 端维护一个 last_id,连续多次不递增就判断链路异常。这个方法在电磁环境比较差的电机驱动旁边尤其有用。
3.3 STM32 端串口接收解析与状态机
STM32 端用 HAL 库的HAL_UART_Receive_IT中断接收,每收一个字节进一次中断,在中断里跑一个简单的状态机判断帧头、收集数据、做校验。这样不会因为主循环忙于 PID 计算而丢帧。
typedef enum { FRAME_WAIT_HEADER, FRAME_GET_ERR, FRAME_GET_SPEED, FRAME_GET_CHECK } FrameState; uint8_t rx_buf[3]; uint8_t rx_index = 0; FrameState rx_state = FRAME_WAIT_HEADER; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { uint8_t byte = rx_byte; switch (rx_state) { case FRAME_WAIT_HEADER: if (byte == 0xAA) { rx_state = FRAME_GET_ERR; rx_index = 0; } break; case FRAME_GET_ERR: rx_buf[0] = byte; rx_state = FRAME_GET_SPEED; break; case FRAME_GET_SPEED: rx_buf[1] = byte; rx_state = FRAME_GET_CHECK; break; case FRAME_GET_CHECK: uint8_t sum = (0xAA + rx_buf[0] + rx_buf[1]) & 0xFF; if (sum == byte) { target_err = rx_buf[0] - 127; target_speed = rx_buf[1]; } rx_state = FRAME_WAIT_HEADER; break; } HAL_UART_Receive_IT(huart, &rx_byte, 1); } }状态机的每一个分支都对应协议里的一类字节,接收错误时会自动回到等待帧头的状态,不会影响下一次通信。需要注意的是在中断回调里尽量不要做耗时操作,解析完成的target_err只是一个全局变量的赋值,PID 计算放到主循环里执行即可。很多人把 PID 也放在回调里,中断时间过长会直接导致串口丢字节。
4. STM32 侧运动控制:PWM 调速、差速转向与 PID 参数整定
4.1 电机驱动与 PWM 通道分配
小车底盘通常是两轮差速结构,两个直流减速电机分别由驱动模块供电,常见驱动芯片有 TB6612、L298N 和 DRV8833 等。TB6612 因为压降小、发热低,更适合小车的电池供电场景。每个电机需要两个方向引脚加一个 PWM 引脚,因此 STM32 上至少需要两路 PWM 输出和四个 GPIO。用定时器的两个通道输出 PWM,例如 TIM2 的 CH1 和 CH2,频率设置为 10kHz 到 20kHz,既能避开电机噪声也能获得更平滑的调速。方向引脚接普通 GPIO,高电平正转,低电平反转。实际接线时最好在电机电源端并联大电容,否则电机启停瞬间的电流尖峰可能导致 STM32 复位。
4.2 差速转向模型与偏差映射
差速转向的核心是让左右轮转速差与 OpenMV 返回的偏差量建立映射关系。最简单可靠的方式是设定一个基础速度base_speed,左右轮速度在基础速度上做加减:
l_speed = base_speed - Kp * err r_speed = base_speed + Kp * errKp是转向比例系数,err是 OpenMV 计算出的横向偏差,单位映射到 -127 到 127。当车偏左时,err为负,左边减速右边加速,车辆自动向右修正。这个公式虽然简单,却是视觉循迹控制系统的核心骨架。关键在于base_speed和Kp需要匹配:base_speed太大,小车冲进弯道的速度让Kp来不及修正;Kp太大,左右轮一会儿全速一会儿全刹,车会左右抖动。经验法则是先把base_speed固定在一个低速度,比如满占空比的 30%,然后逐渐增大Kp直到出现摆动,再退回 70% 的数值。
4.3 PID 控制代码框架与整定方法
纯比例控制在缓弯表现尚可,但遇到连续 S 弯会因为转向滞后而冲出赛道。给转向加一点微分项会显著改善:D项对偏差变化率做出提前反应,让转向更“跟手”。积分项在电机控制里要格外谨慎,因为小车转向系统的静差不大,积分过大反而造成震荡。我的默认做法是只使用 PD,只在赛道明显偏向一侧时才考虑引入积分限制在很小范围内的 I 项。
float kp = 0.8f, ki = 0.0f, kd = 2.0f; float err_prev = 0.0f; float integral = 0.0f; void pid_update(float err) { float p = kp * err; integral += ki * err; if (integral > 20) integral = 20; if (integral < -20) integral = -20; float d = kd * (err - err_prev); float output = p + integral + d; err_prev = err; int base_speed = 60; int left_speed = base_speed - output; int right_speed = base_speed + output; left_speed = CLAMP(left_speed, 0, 100); right_speed = CLAMP(right_speed, 0, 100); set_motor_speed(left_speed, right_speed); }代码里CLAMP把输出限制在 0 到 100 的占空比范围,防止 PWM 超限导致驱动芯片行为异常。kd的数值选择要看控制周期:如果主循环跑得很快,比如 10ms 一次,kd可以取较小值;如果控制周期拉长到 50ms,kd需要相应提高。整定顺序是先把kd设为 0,调kp让小车能走完一个直道,然后加kd消除弯道中的摆动,整个过程在 5 到 10 米的赛道上来回验证,每次只改一个参数。
4.4 STM32 环境搭建与调试注意点
STM32 开发环境目前主流的组合是 Keil MDK + STM32CubeMX,或者直接用 STM32CubeIDE。Keil 需要单独安装对应芯片的器件支持包,比如在 Pack Installer 里安装Keil.STM32F1xx_DFP,如果下载比较慢可以手动从官网下载离线包安装。新建工程时用 CubeMX 生成初始化代码是最不容易出错的方式,串口、PWM、GPIO 的时钟树和引脚复用都由工具生成,省去查 datasheet 的步骤。
如果下载时报error: no stm32 target found! if your product embedsdebug authentication, pl...,常见原因有两个:一是 ST-Link 和板子之间的 SWDIO/SWCLK 接线没接对,检查复位引脚和 GND 是否共地;二是 STM32 开启了读保护,先用 ST-Link Utility 做全片擦除再重新烧录。Windows 下如果设备管理器里 ST-Link 出现黄色感叹号,需要安装对应版本的 ST-Link 驱动,或者换一根能传数据的 USB 线而不是仅有充电功能的线。
5. 联调流程与三个提升稳定性的实战技巧
5.1 分段联调:先看图像,再看通信,最后跑车
联调阶段严格按三步走。第一步,只接 OpenMV 和电脑,打开 IDE 观察回归线是否贴合赛道线,确认输出的err在直道接近 0,弯道大小合适。第二步,把 OpenMV 和 STM32 连好,STM32 接上一个虚拟串口监控工具或者用 OLED 显示收到的err值,人手工按住小车沿赛道走一圈,观察偏差数值变化是否符合预期。这一步能在不跑车的情况下验证协议正确性。第三步,架起小车,用绳子拉着低速行驶,确认转向方向和偏差的符号映射正确,最后放开全速跑。顺序不能变,否则出了问题根本说不清是视觉的问题还是电机控制的问题。
5.2 调整 PID 时为什么车会抖,以及抖了先查什么
车体抖动不一定是Kp太大引起的。先检查机械结构,轮胎是否打滑、重心是否太靠后,这些因素会放大控制系统的震荡。然后看 OpenMV 的帧率,如果画面只有 15 帧每秒,控制输出的更新频率太低会让系统变得“迟钝”,这时候应该降低图像分辨率或者缩小 ROI 来提速。sensor.set_framesize(sensor.QQVGA)的分辨率是 160x120,图像处理量比 VGA 小很多,但流畅度远高于一片一算的 QVGA。帧率够高之后再去调Kp和Kd,观察抖动的频率:高频抖动优先减小Kd,低频大幅摆动优先减小Kp。
5.3 如何用 OpenMV 的帧日志做回归验证
跑车过程中 OpenMV 的 IDE 无法随时在线,因为数据线会影响车体配重。常见做法是用 TF 卡记录日志,OpenMV 支持将调试信息写入 TF 卡文本文件。每次试跑后把 TF 卡取出来,查看每一帧的err值、回归置信度和耗时,找出失控瞬间的图像特征。比如直线路段突然出现err跳变到 127,回看日志如果那几帧的theta接近 90 度,大概率是车身经过强反光区域导致二值化失效,这时候去调整阈值而不是继续调 PID。日志驱动的调试方式让每一次修改都有据可查,也方便换一个场地后快速回归验证参数是否仍然有效。
本文还有配套的精品资源,点击获取