
简介基于OpenMV与STM32C8T6的循迹小车完整项目资料面向嵌入式视觉与运动控制方向的学习者适合零基础入门也可作为毕设、课程设计或工程实训参考。实现思路完整OpenMV端选取画面中部靠下的ROI区域做阈值化提取黑线中心后运行PID计算再按自定义协议将控制量发送至STM32STM32对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(time2000) red_threshold (0, 60) # 根据实际赛道调整灰度值低于60视为黑线 while True: img sensor.snapshot() img.binary([red_threshold]) # 二值化黑线变白背景变黑 blobs img.find_blobs([(255, 255)], pixels_threshold50, area_threshold50)这段代码里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)], robustTrue) if line: rho line.rho() # 线到原点的距离 theta line.theta() # 角度0度表示水平 img.draw_line(line.line(), color127)robustTrue表示使用更抗离群点的回归方式代价是计算量稍大但对光照不均、反光斑等情况更稳。得到的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 还是 SPIOpenMV 与 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 字节协议帧头0xAAerr一个字节车速一个字节最后是校验字节。from pyb import UART uart UART(3, 115200, timeout_char1000) uart.init(115200, bits8, parityNone, stop1) 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可以取较小值如果控制周期拉长到 50mskd需要相应提高。整定顺序是先把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。日志驱动的调试方式让每一次修改都有据可查也方便换一个场地后快速回归验证参数是否仍然有效。本文还有配套的精品资源点击获取