☰
STM32+OpenMV视觉巡线小车实战:从串口通信到PID控制
2026/10/3 13:19:49 网站建设 项目流程

做视觉巡线小车,是很多嵌入式爱好者入手机器视觉的第一个完整项目。STM32负责“动”,OpenMV负责“看”,两者通过串口把视觉信息和运动控制串起来,整套系统麻雀虽小,五脏俱全。项目做完我最大的感受是:视觉巡线的难点不在于某个单一模块,而在于“看”和“动”的配合。OpenMV看到的偏差怎么变成电机的转速差、弯道怎么不冲出赛道、十字路口怎么不迷路,这里面每一步都有讲究。这篇文章把我做这个项目的过程、代码思路、调参经历和踩过的坑完整记录下来,给准备做或者正在做类似项目的朋友一个参考。

1. 项目概览与整体方案选型

1.1 项目目标与功能拆解

先明确我要做的东西是什么。视觉巡线小车,最朴素的场景就是:一辆两轮差速小车,车头装一个摄像头,在白色底板上沿着黑色赛道自动行驶,能过弯道、能走直线、能处理十字路口和断线区域。听起来简单,但仔细拆解下来,整个系统要解决四个问题:第一是“看得到”,摄像头要能稳定识别出赛道线;第二是“算得准”,要把赛道线的位置转换成准确的偏差数据;第三是“传得快”,视觉数据和运动控制之间的通信不能拖后腿;第四是“控得住”,电机要根据偏差实时调整转速,让小车不偏不倚地跑下去。

从功能模块上分,这套系统由三大部分组成:OpenMV视觉模块负责图像采集、赛道识别和偏差计算;STM32主控模块负责接收视觉数据、运行控制算法、输出PWM驱动电机;底盘动力模块包含电池、电机驱动板和两个直流减速电机。三个模块各司其职,靠一条串口线和几条电源线连在一起。我选择的是最简单也最经典的两轮差速底盘方案,两个驱动轮加一个万向轮,控制灵活而且算法直观。

1.2 核心器件选型:为什么是STM32+OpenMV

选型这块,我一开始其实纠结过一段时间。视觉模块在市面上有很多选择:树莓派加摄像头、K210、OpenMV,甚至用手机加蓝牙。每种方案都有各自的长处,但结合“嵌入式学习”和“视觉巡线”这两个核心需求,OpenMV是最合适的。

原因有三点。第一,OpenMV是基于MicroPython的,写起来比C语言快太多。图像处理算法在OpenMV IDE里调试非常直观,可以实时看画面、看阈值效果、看色块识别结果,这对前期开发效率帮助巨大。相比之下树莓派虽然性能强,但Linux环境的启动速度、图像处理管线的复杂度,对一个巡线项目来说有点杀鸡用牛刀。K210的话性能也够,但生态和资料相对OpenMV少一些。第二,OpenMV自带摄像头接口、SD卡槽、LED补光灯和完整的例程库,尤其是find_blobs和regression这两个图像算法函数就是为色块追踪和线段检测量身定做的,不需要自己从零写图像处理算法。第三,OpenMV的UART、I2C、GPIO都引出来了,和STM32通信非常方便。

STM32这边,我用的是一块STM32F103C8T6的最小系统板。这颗芯片虽然是一颗老将,但用在巡线小车上绰绰有余。72MHz主频、3个定时器、3个USART,刚好覆盖PWM输出、串口通信和逻辑控制的需求。Keil5写代码、ST-Link下载调试,这套开发流程在校园和工业嵌入式领域都非常通用,学到的经验也具备迁移价值。

1.3 系统架构与数据流解析

把整体架构画出来就清晰了。传感器端是OpenMV摄像头,拍摄前方地面画面,每帧图像经过算法处理后计算出赛道线和画面中线的偏差值,然后通过串口把偏差值按固定格式发送给STM32。STM32收到数据后做两层处理:第一层是解析校验,确认数据没有丢帧错位;第二层是执行控制算法,也就是PD/PID计算,把偏差值映射成左右两个电机的转速差,最终通过定时器PWM通道输出到电机驱动模块,驱动电机转动。

整个数据流是单向的:OpenMV负责“感知”,STM32负责“决策和执行”。这种架构的好处很直接——视觉部分的运算量不会占用主控的资源,主控可以专心地跑控制逻辑。另一层好处是模块化程度高,OpenMV那边的算法和STM32这边的控制算法可以独立开发、独立调试,最后再联调通信协议。我在做的时候就是先分别调好了视觉模块和电机模块,才把它们接在一起的。

2. 视觉端OpenMV巡线算法与实现细节

2.1 图像预处理与LAB阈值设定

OpenMV上的巡线实现,核心是色块追踪。黑色赛道线的RGB值几乎接近0,而白色底板的RGB值比较高,两者在LAB色空间里区分度很明显。我是在OpenMV IDE里打开阈值编辑器,实时调节L通道的上下限,把画面里赛道线单独抠出来。阈值这块看着简单,其实有个最容易被忽略的问题:光照。

OpenMV的镜头有一个自动曝光和自动白平衡机制,环境光忽亮忽暗的时候,图像亮度会自动调整,这会导致原本调好的阈值在某些时刻失效。我的处理方式是把auto_exposure和auto_whitebal这两个参数固定住,手动设置曝光时间和增益值。在正常室内灯光下,我最后用的阈值大概是(20, 60, -20, 20, -20, 20)这样的一个范围,但每个人实际环境不同,还是要在现场重新取。这里有个小技巧,调阈值的时候不要只看一帧画面,要拿着小车在赛道的不同位置、不同角度多动一动,观察日照变化、反光对色块提取的影响。

图像分辨率我设置成QQVGA,也就是160x120。为什么不用更高的分辨率?因为巡线小车只需要知道赛道线的横向位置偏差,不需要看清赛道纹理。分辨率每提高一倍,图像处理的耗时大约会增加一倍,帧率就直接腰斩。160x120在OpenMV上跑find_blobs,帧率可以稳定在50FPS左右,这个刷新率对电机控制来说已经绰绰有余了。

2.2 偏差值计算的两种方法对比

OpenMV官方例程里提供了两种巡线思路:一种是find_blobs找色块,取色块的中心X坐标作为赛道位置;另一种是find_lines或regression直线回归,取回归直线的中心位置。这两种我都试过,各自有适用的场景。

find_blobs的思路是把黑色赛道线看成一个连通的色块,然后取这个色块的外接矩形,矩形的中心点就是赛道位置。这种方法在赛道线比较宽、比较连续的时候效果很好,计算快,稳定性高。但对于弯道比较大的情况,色块的中心可能偏向弯道内侧,导致小车转向不足。

regression直线回归的思路是提取所有黑色像素点,用最小二乘法拟合出一条直线,直线的方向和位置代表赛道方向。它的优势在于弯道预判——回归直线带有角度信息,OpenMV甚至可以直接返回直线的角度和中心坐标。我最后采用的是“色块提取+回归直线”的组合:先用find_blobs找到赛道线区域,再在区域内做回归,这样既保留了色块的稳定性,又拿到了角度信息作为转向参考。不过为了控制逻辑简单,最终发给STM32的核心数据依然是“偏差值”。

2.3 串口数据打包与发送格式设计

OpenMV算出了偏差,接下来就是把它打包发出去。串口通信的格式我折腾过好几版,最初图省事直接发ASCII字符串,像"dev:25\n"这样。这样做的优点是调试方便,串口助手直接能看明白内容。但问题也很明显:发送效率低,一个整型偏差值要占用好几个字节,而且STM32那边解析字符串需要处理字符到数字的转换,稍微麻烦一些。

后面我改成了二进制报文格式。一帧数据由5个字节组成:帧头0xAA、0x55,接着是数据字节偏差值,第五个字节是校验和。整个发送逻辑用MicroPython写起来非常简洁,关键代码如下:

import ustruct from pyb import UART uart = UART(3, 115200) # 每一帧:帧头0xAA 0x55 + 偏差值1字节 + 校验1字节 def send_deviation(dev): dev = max(-128, min(127, int(dev))) buf = bytearray([0xAA, 0x55, dev & 0xFF, (0xAA + 0x55 + dev) & 0xFF]) uart.write(buf)

把偏差值限制在-128到127之间,是为了方便用一个字节表示。校验和用来在STM32端验证数据正确性,防止通信干扰导致的错误帧。这个协议虽然简单,但实际用下来非常可靠。

3. STM32端控制核心:从串口解析到PWM输出

3.1 串口接收与解析的中断处理

STM32端的核心任务是实时、准确地拿到OpenMV发来的偏差值。我使用的是USART1,配置为115200波特率、8位数据位、1位停止位,无校验。接收方式用的是串口中断加状态机解析。

为什么不用轮询方式?因为主循环里还要跑PID算法和PWM更新,如果串口数据到了但循环没有及时去读,数据就会覆盖丢失。用中断接收的好处是数据一到就立刻存入环形缓冲区,主循环只需要从缓冲区里取数据解析即可,两边互不干扰。

解析状态机也比较经典,核心逻辑是:先找帧头0xAA 0x55,找到后接收第3个字节作为偏差值,再接收第4个字节作为校验值,最后判断校验和是否正确。说得直白一点,就是“先对暗号,再收货”。我贴一段简化后的C代码:

uint8_t rx_buffer[4]; uint8_t rx_index = 0; int16_t deviation = 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE)) { uint8_t data = USART_ReceiveData(USART1); if (rx_index == 0 && data != 0xAA) return; // 等待帧头 rx_buffer[rx_index++] = data; if (rx_index >= 4) { if ((rx_buffer[0] + rx_buffer[1] + rx_buffer[2]) == rx_buffer[3]) { deviation = (int8_t)rx_buffer[2]; } rx_index = 0; } } }

这段代码的核心是“非0xAA的数据直接丢弃”,这种做法能快速跳出乱帧,重新对齐数据流。如果校验失败,直接把索引清零,重新开始找帧头,不会卡死在半截数据上。

3.2 定时器PWM配置与电机驱动细节

STM32驱动直流电机,本质上是输出PWM信号控制电机两端电压的平均值,从而控制转速。我用TIM2的两个通道输出PWM,分别控制左右两个电机的PWM输入脚。还有一个方向引脚用普通GPIO控制,因为直流减速电机驱动模块通常需要两路信号:一路PWM调速,一路高低电平换向。

电机驱动模块我选的是TB6612FNG,相比L298N,它体积小、压降小、发热低,而且逻辑电平兼容3.3V,可以直接和STM32对接,不需要额外的电平转换。PWM频率我设置为10kHz,这个频率高于人耳听觉范围,不会听到电机高频啸叫,同时对于直流电机的电感特性来说也能获得比较平滑的电流波形。

关键的接线逻辑大致是这样:TIM2_CH1,也就是PA0引脚输出左电机PWM;TIM2_CH2,也就是PA1引脚输出右电机PWM;PB0和PB1控制左电机方向;PB10和PB11控制右电机方向。电源方面,电池7.4V直接接TB6612的VM引脚,它内部稳压后给逻辑部分供电,STM32那边单独用一个AMS1117-3.3模块降压供电。别忘了全部模块必须共地,这个问题我在调试初期踩过一次,后面会详细说。

3.3 两轮差速模型与转向控制逻辑

小车转弯靠的是左右轮速不一样,这就是两轮差速的核心思想。差速模型写起来很直白:一个基础速度base_speed负责让小车前进,偏差值再叠加到两个轮子上产生差速。我的映射规则是:偏差为正,说明赛道线在中线的右侧,小车需要向右转,这时右轮减速、左轮加速;偏差为负则相反,左轮减速、右轮加速。

在代码里体现为:

int16_t speed_left = base_speed - kp * deviation; int16_t speed_right = base_speed + kp * deviation;

这个公式是纯比例控制,如果kp设置不合适,小车要么反应迟钝,要么左右剧烈震荡。实际使用中我很快就加了微分项,也就是PD控制,用来抑制转向过冲,这个我们下一节详细展开。差速模型本身也有限制:如果偏差值太大,比如赛道线几乎在画面边缘,差速导致一侧电机反转,小车会原地打转。为了避免这种情况,我给左右电机的PWM值分别做了限幅,保持在最大值为1000、最小值为50(归一化占空比对应值)的范围内。

4. PID控制器设计与现场调参实战

4.1 为什么巡线小车离不开PID

从纯比例控制起步,当我第一次把kp调大想让小车反应更灵敏时,发现小车在直道上开始左右画龙,高速时甚至摇摆幅度越来越大,直到冲出赛道。原因很简单,比例控制只根据当前偏差输出纠偏量,没考虑小车的惯性。小车以一定速度向前冲,你给它一个瞬间的大转向,它会因为惯性冲过头,超过中线后又反向纠偏,结果就是来回震荡。

这就是PID控制中“D”项要解决的问题。D项根据偏差的变化率来输出一个相反方向的修正力,相当于一个阻尼器。偏差变化越快,阻尼力越大,从而抑制振荡。对于巡线小车这种典型的“一阶惯性+延迟”系统,PD控制通常就够用了。I项积分一般可以不给,因为巡线任务本身是持续动态的,目标线一直在变,过大的I项反而容易引起超调。

4.2 PD控制器代码实现与参数调优

我最后实现的PD控制算法如下:

#define BASE_SPEED 400 #define KP 32 #define KD 18 float last_deviation = 0; void control_loop(void) { if (rx_updated) { float P = KP * deviation; float D = KD * (deviation - last_deviation); int16_t output = (int16_t)(P + D); // 限幅 if (output > 300) output = 300; if (output < -300) output = -300; int16_t speed_left = BASE_SPEED - output; int16_t speed_right = BASE_SPEED + output; set_motor_speed(LEFT, speed_left); set_motor_speed(RIGHT, speed_right); last_deviation = deviation; } }

参数整定我采用了一个比较实用的流程:先把KD设成0,只用KP,从小到大慢慢加,直到小车能在直道上基本稳定行走但又开始出现轻微摆动时,记下这个KP值;然后逐渐增加KD,观察摆动是否被抑制住。我的经验是,KP大约在30左右,KD在15到20之间,小车就能跑得比较稳了。当然不同电机的响应速度、不同底盘的摩擦力都会影响这个数值,建议在自己的平台上重新整定。另外BASE_SPEED也不要一开始就给满,我调试时先给300,跑顺了再逐步提到400、500。

4.3 转向延迟与丢线处理策略

调试过程中最烦的一个问题是:小车在连续弯道中,OpenMV偶尔会一瞬间丢失赛道线。摄像头画面对着一片白色,find_blobs找不到任何色块。如果这时直接发一个偏差0给STM32,小车会以为自己在直线,按基础速度猛冲,结果就是冲出赛道。

针对这个问题,我在OpenMV端加了一个“丢线保持”逻辑:如果连续几帧都找不到色块,就根据上一次的偏差方向给出一个修正值,让OpenMV主动朝丢失方向搜索。同时STM32端也做了类似的保护,如果串口数据超过200毫秒没更新,就判定为通信超时,电机直接停止,防止小车失控。

丢线处理这块还有另一种思路,就是OpenMV把图像搜索区域改大,或者把阈值调宽一些,让系统对轻微反光、阴影变化不那么敏感,但这样做有可能把赛道附近的杂物也识别成赛道线,属于“饮鸩止渴”。我最后选择的是“丢线保持+超时停车”的双保险策略。

5. 硬件设计与接线避坑指南

5.1 电源系统分配与稳压方案

电源是整个项目里最容易被低估、也最容易出问题的一环。我的小车使用2节18650锂电池串联,标称电压7.4V。这个电压直接给电机驱动模块供电是没问题的,但不能直接给STM32供电——STM32的额定电压是3.3V。我用了两条降压路径:7.4V经过一个LM2596降压模块降到5V,再通过AMS1117-3.3降到3.3V给STM32供电。OpenMV则直接使用5V供电。

这里有一个非常关键的经验:电机是系统里最大的干扰源。电机启动和堵转瞬间电流变化剧烈,会在电源线上产生很大的纹波噪声。如果直接用同一个5V给OpenMV和STM32供电,图像处理时OpenMV的模拟电路很容易被干扰,表现为画面闪烁、阈值漂移甚至花屏。解决办法是让电机电源和逻辑电源在物理上分开走线,或者在主电源处加一个大容量的电解电容和一个高频陶瓷电容做滤波。我实测下来,在电池两端并联一个470uF电解电容和0.1uF陶瓷电容后,干扰问题基本消失。

5.2 电机驱动接线与方向电平逻辑

TB6612的接线逻辑并不复杂,但有一个特别容易出错的点:AIN1、AIN2和PWMA这三个引脚决定了一个电机的全部行为。AIN1和AIN2控制方向,PWMA控制速度,这三者必须配合使用。比如要让左电机正转,需要设置AIN1=1、AIN2=0,然后给PWMA一个PWM占空比;要让左电机反转,则设置AIN1=0、AIN2=1。如果两个方向引脚都是0或者都是1,电机就会停止(刹车或滑行,取决于驱动芯片内部逻辑)。

我在代码里封装了这样一个函数:

void set_motor_speed(uint8_t motor, int16_t speed) { if (speed > 0) { // 正转 if (motor == LEFT) { GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_SET); GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_RESET); TIM_SetCompare1(TIM2, speed); } else { GPIO_WriteBit(GPIOB, GPIO_Pin_10, Bit_SET); GPIO_WriteBit(GPIOB, GPIO_Pin_11, Bit_RESET); TIM_SetCompare2(TIM2, speed); } } else { // 反转 speed = -speed; // 对应的方向引脚反过来设置 } }

注意这里如果speed给了一个负值,不仅要反转方向,还要对速度值取绝对值,否则TIM_SetCompare收到负数会导致PWM输出异常。我在调试时因为这个问题折腾了很久,小车一会儿往前冲一会儿突然停住,后来才发现是负值没有取绝对值。

5.3 摄像头安装角度与镜头调焦

摄像头安装的位置和角度对巡线效果影响非常大。我一开始把OpenMV固定在底盘正前方,镜头几乎水平朝向正前方,结果发现OpenMV看到的是远处的赛道而不是车正前方的赛道,巡线响应非常迟钝,弯道处总是“看到得太晚”。后来把安装支架抬高,让镜头向下倾斜大约45度,这样画面上看到的区域就是车前方20到40厘米左右的一段赛道线。

需要注意另一个细节是镜头调焦。OpenMV标配的镜头是可变焦的,出厂时通常对焦在较远的距离。如果安装高度只有十几厘米,看到的图像有可能是模糊的。我的解决办法是:把小车放在赛道上,OpenMV连接到IDE,打开实时画面看赛道线是否清晰,然后用手慢慢旋转镜头底座,直到画面中的赛道线边缘出现清晰的锐利边界。这个过程需要耐心,最好用一个小把手去旋镜头,不然手指容易弄脏镜片。

6. 常见问题排查与调测经验汇总

6.1 串口通信乱码和数据错位的根治方法

串口乱码是联调阶段最容易遇到的问题。我遇到过的乱码原因包括:波特率不一致、收发引脚接反、没有共地、干扰导致数据帧错位。逐一排查的思路是这样的:先用USB转TTL模块把OpenMV连接到电脑,用串口助手查看OpenMV发的原始数据,确认OpenMV端数据格式正确;然后再把STM32端的串口通过另一个USB转TTL模块也接到电脑,由电脑模拟OpenMV发送数据,确认STM32端解析正确。两边分开测试都通过之后,再把两个设备对接。这个“分而治之”的排查方法帮我快速定位到问题出在那一端。

对于共地问题,有个很简单直观的判断方法:如果OpenMV和STM32各自单独接电源时通信正常,但一起供电后通信就乱,大概率是共地问题。解决办法就是在两个模块的GND引脚之间额外接一根粗导线。

6.2 小车左右画龙与震荡的分析

小车的“画龙”现象,我之前提到过,是PD参数配合不好的典型症状。但“左右震荡”这个症状其实还得分几种情况来讨论。第一种是P过大引起的持续震荡,特点是小幅高频的左右摆动,解决方法是降低KP;第二种是D过小引起的过冲型震荡,特点是振幅较大且逐渐衰减,解决方法是增大KD;第三种是机械结构问题,比如两个驱动轮转速不完全一致、万向轮摩擦力过大、底盘重心不稳,这种情况即使PID参数调得再好也没用。我在调车时发现把万向轮换成一颗更顺滑的滚珠后,直道稳定性提升了一个档次,这个细节很容易被忽略。

6.3 OpenMV识别不稳定的几个隐藏原因

OpenMV识别不稳定,并不总是算法的问题。我总结出了三个隐藏原因,都在实际项目中遇到过。第一是镜头焦距松了,小车跑着跑着画面变模糊,重新对焦就好。第二是OpenMV内部的LDO稳压芯片过热,长时间运行后图像出现条纹,这和环境温度以及供电电压都有关系,我的解决方法是给OpenMV的散热片贴了一片小的铝散热片。第三是最隐蔽的:LED补光灯引起的反光。如果OpenMV自带的白色LED补光灯亮度太高,在某些光洁的赛道表面会产生反光,把原本黑色的赛道线照成浅色,导致阈值提取失败。后来我在OpenMV代码里直接把LED(2).off()关掉了补光灯,改用室内自然光,识别反而更稳定。

6.4 电机响应迟滞与PWM频率调整

还有一个容易被忽略的是PWM频率和电机响应速度的关系。早期我把PWM频率配置成200Hz,电机在低占空比时会出现明显的“咔咔”声和步进式的转动,这是PWM频率过低导致的电流不连续。提高到10kHz后,电机运行平滑多了。实际上,直流电机的PWM频率一般推荐在10kHz到20kHz之间,太低了噪声大,太高了驱动芯片的开关损耗会增加。我最终用的是10kHz,实测下来是最平衡的。

另外电机驱动板的使能引脚也要检查。TB6612的STBY引脚如果接低电平,整个芯片就会进入待机状态,电机完全不动。我的代码里初始化时就直接把STBY拉高到3.3V,否则会陷入“PWM明明有输出但电机就是不动”的假象中。

6.5 常见问题速查表

现象可能原因排查与解决方法
串口收到乱码波特率不一致 / 收发接反 / 未共地分端排查;统一波特率;GND相连
小车左右画龙KP过大 / KD过小 / 机械不对称降低KP;增大KD;检查轮子与万向轮
OpenMV画面花屏供电不稳 / 镜头松动改善电源滤波;重新对焦并固定镜头
电机不转使能脚未拉高 / PWM频率过低 / 方向引脚错误拉高STBY;提高PWM频率;检查方向逻辑
小车丢线冲出赛道丢线处理缺失 / 弯道太急增加丢线保持逻辑;降低BASE_SPEED
OpenMV帧率低分辨率太高 / 自动曝光耗时降到QQVGA;固定曝光和增益
舵机或电机死机电源噪声干扰逻辑电路加电解电容滤波;动力与逻辑电源分开走线

7. 项目调试心得与可扩展方向

整个项目做下来,我最深的体会是:嵌入式项目里“稳定性”比“花哨”重要得多。代码写得再漂亮、算法看起来再高级,如果小车跑三圈就冲出赛道一次,那依然不能算一个合格的项目。所以我把大量时间花在了数据校验、超时保护、丢线处理和电源滤波这些“看不见”的地方。这些细节不直接体现在功能上,但恰恰决定了整个系统能不能长时间稳定运行。

另外一点感触是,调参要有方法论。不要靠“瞎猜参数-跑一下-再看效果”的循环,那是碰运气。先让每个模块独立工作正常,再联调;调PID时先P后D,每次只改一个变量;记录每一组参数对应的小车表现,形成数据表,你才能逐渐找到规律。这个方法不仅适用于巡线小车,往后的飞控、小车、机械臂项目,我都沿用了同一套流程。

这个项目的可扩展空间其实很大。比如在OpenMV端加入对赛道分支的判断,可以实现巡线小车在三岔路口自主转向;在STM32端加入蓝牙模块或者无线模块,可以把遥测数据发到电脑上画实时曲线;再加上光电编码器测速,把开环控制升级成闭环PID速度控制,小车过弯的稳定性还会有明显提升。每一个方向都足够展开做一个新的项目,但基础的“视觉+主控+驱动”这套架构,已经通过这个巡线小车全部打通了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询