news 2026/9/17 7:06:26

OpenMV+STM32视觉循迹小车实战:图像处理与PID控制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenMV+STM32视觉循迹小车实战:图像处理与PID控制详解

简介:基于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 固件,可以直接调用sensorimage等模块完成图像采集和处理。在循迹小车里,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表示使用更抗离群点的回归方式,代价是计算量稍大,但对光照不均、反光斑等情况更稳。得到的rhotheta可以换算成与画面中心线的横向偏差:

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.jpgleft_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 字节协议:帧头0xAAerr一个字节,车速一个字节,最后是校验字节。

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 * err

Kp是转向比例系数,err是 OpenMV 计算出的横向偏差,单位映射到 -127 到 127。当车偏左时,err为负,左边减速右边加速,车辆自动向右修正。这个公式虽然简单,却是视觉循迹控制系统的核心骨架。关键在于base_speedKp需要匹配: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。帧率够高之后再去调KpKd,观察抖动的频率:高频抖动优先减小Kd,低频大幅摆动优先减小Kp

5.3 如何用 OpenMV 的帧日志做回归验证

跑车过程中 OpenMV 的 IDE 无法随时在线,因为数据线会影响车体配重。常见做法是用 TF 卡记录日志,OpenMV 支持将调试信息写入 TF 卡文本文件。每次试跑后把 TF 卡取出来,查看每一帧的err值、回归置信度和耗时,找出失控瞬间的图像特征。比如直线路段突然出现err跳变到 127,回看日志如果那几帧的theta接近 90 度,大概率是车身经过强反光区域导致二值化失效,这时候去调整阈值而不是继续调 PID。日志驱动的调试方式让每一次修改都有据可查,也方便换一个场地后快速回归验证参数是否仍然有效。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 7:05:41

基于.NET与微信小程序的市容监察管理系统设计与实现

又是一年毕设季&#xff0c;后台私信里问得最多的还是那句话&#xff1a;“老师/学长&#xff0c;系统类的题目到底怎么选才不踩坑&#xff1f;”其实系统类选题只要业务线清晰、技术栈主流、有完整的闭环&#xff0c;就是最稳妥的方向。今天我就拿一个非常有代表性的题目来拆—…

作者头像 李华
网站建设 2026/9/17 7:05:25

Qt for MCUs 2.11 LTS:MCU级矢量地图渲染实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 7:05:10

Flutter文本处理库copywriter的鸿蒙适配指南

1. Flutter 三方库 copywriter 的鸿蒙化适配指南在移动应用开发领域&#xff0c;文本展示质量直接影响用户体验。作为开发者&#xff0c;我们经常面临中英文混排不美观、标点符号不规范等问题。copywriter 库的出现&#xff0c;为 Flutter 开发者提供了一套优雅的解决方案。本文…

作者头像 李华
网站建设 2026/9/17 7:04:24

SVN从零到实战:安装配置、分支合并与团队权限管理

不论你现在用的是单兵作战还是几十人的研发团队&#xff0c;代码版本管理这件事迟早要面对。Git这几年确实风头很盛&#xff0c;但SVN&#xff08;Subversion&#xff09;从来没有退出过主流视野。我见过不少传统企业、外包项目、甚至银行和制造业的研发部门&#xff0c;到现在…

作者头像 李华
网站建设 2026/9/17 7:03:19

Folo上下文菜单:React Native iOS菜单

Folo上下文菜单&#xff1a;React Native iOS菜单 上下文菜单&#xff08;Context Menu&#xff09;是移动应用中提升用户体验的关键交互组件&#xff0c;尤其在iOS平台上&#xff0c;它通过长按操作展示与当前内容相关的快捷功能&#xff0c;帮助用户快速完成任务。本文将详细…

作者头像 李华