ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenMV+STM32视觉巡线小车实战:从图像识别到差速控制

OpenMV+STM32视觉巡线小车实战:从图像识别到差速控制 视觉巡线小车这个项目我前后折腾了两个多月才算是真正跑稳了。网上的资料看着都很全OpenMV有寻线例程STM32有电机驱动例程可真把两套东西拼到一起的时候各种问题才冒出来——通信对不上、图像突然丢线、弯道转不过去、电池电压一降小车就像喝醉了一样。这篇文章是把整个项目的技术路线从头到尾捋一遍重点放在系统架构、硬件接线、视觉处理、通信协议、差速控制和联调排错上把我调通的方案和踩过的坑都写清楚。适合正在做智能车竞赛、课程设计或者想用低成本方案学习视觉闭环控制的朋友。不说废话直接进正题。1. 系统架构谁当眼、谁当脑、怎么分工1.1 OpenMV和STM32为什么各干各的做视觉巡线第一件事不是写代码而是把架构想清楚。市面上有两类常见方案一类是OpenMV单独干完所有事——图像识别、PID计算、PWM输出全都塞给OpenMV靠它的IO口直接驱动电机另一类是OpenMV只做图像识别把结果通过串口发给STM32由STM32完成运动控制和PWM输出。我最终选了第二种。OpenMV的理想虽然不错但它本身也是一块基于STM32H7的小板子把CPU大量花在图像处理上没问题可一旦同时做高频PID控制和PWM刷新实时性很容易被打折扣。巡线车对控制频率的要求并不低我的控制周期设计在20ms左右也就是50Hz如果图像处理偶尔卡一下整个控制链路就跟着抖。更重要的是分工边界清晰。OpenMV干它最擅长的图像采集、预处理、特征提取、输出一个结构化的偏差结果。STM32干它最擅长的接收指令、做PID计算、输出PWM、处理电机状态。两边各管一摊调试的时候也能分开定位问题——车不动了先看OpenMV有没有发再看STM32有没有收不用全堆在一起查。1.2 数据流与任务边界划分整个系统的数据流是这样的OpenMV摄像头采集画面在ROI区域内做色块识别对目标色块做线性回归算出线条相对画面中心的偏角theta值然后把偏角打包成一帧数据通过UART串口发给STM32。STM32在串口中断里接收并解析出偏角数据把它作为PID控制器的输入偏差计算出一个转向修正量叠加到基础速度上得到左右两个电机的目标PWM占空比通过定时器输出到电机驱动芯片驱动直流减速电机转动。这个链路里有两个隐含的设计要点。第一OpenMV只往上抛“偏差”而不抛原始图像。很多人初学者喜欢把整张JPEG图通过串口发给STM32让单片机去处理这个思路在窄带宽下完全不现实。QVGA分辨率一张JPEG图轻轻松松几十KB115200波特率下传一张要好几秒钟小车早就冲出赛道了。正确做法是在前端把图像压缩成一个浮点数比如偏角-45到45度串口每帧只需要传输几个字节效率完全不在一个量级。第二STM32不需要理解“图像”这个概念。它眼里只有一串数字当前偏差是多少、目标偏差是0正对线条中间。所有和图像有关的逻辑全放在OpenMV端所有和运动有关的逻辑全放在STM32端。这种解耦让两边可以独立升级比如以后想从巡线改成巡十字路口只需要改OpenMV端的识别策略STM32端完全不用动。2. 硬件选型与接线少走弯路的几个决定2.1 电机驱动方案L298N、TB6612怎么选电机驱动芯片的选择直接影响整个系统的稳定性和体积。做视觉巡线小车市面上最常见的三个选择是L298N模块、TB6612FNG模块以及DRV8833等小电流驱动。先说我用过的L298N。它的优势是便宜、功率裕量大、接法简单网上教程铺天盖地。但它的劣势在实际用起来相当明显内部是双H桥电路搭配PN结三极管导通压降很大实测在1.5V到2V左右。这意味着7.4V电池经过L298N送到电机两端实际有效电压可能只有5.5V到6V。而且压降随着电流增加还会变大电机转速和你预期的差一大截。更头疼的是L298N模块上的线性稳压芯片78M05输入7.4V时它自身发热就很厉害我用手摸过烫得不能久放。后来我换成TB6612FNG这个芯片使用MOSFET做H桥导通压降只有0.5V左右同样电池电压下电机能获得更高有效电压。它本身体积只有L298N模块的几分之一还支持PWM频率直接输入。TB6612的逻辑电源和电机电源是分开的逻辑部分用3.3V供电时输入引脚可以直接兼容STM32的3.3V电平不用额外加电平转换。综合来看在电流需求不大的场景下比如N20减速电机堵转电流在2A以内TB6612是明显更优的选择。如果用的是大功率电机或者要求特别皮实那再考虑L298N或者分立MOSFET驱动方案。2.2 串口连接与电平匹配最容易翻车的地方OpenMV和STM32之间的串口连接看似就是两根线但翻车率非常高。OpenMV的UART引脚是3.3V逻辑电平STM32F103的IO口也是3.3V逻辑电平理论上可以直接相连。但有一个细节很多人忽略——必须共地。两块板子如果不共地TX和RX引脚的参考电位就不同出来的波形完全乱套。我见过好几个人说“串口收到的全是乱码”最后发现是只接了两根信号线、没接GND。这是新手最容易犯的错误也是最容易解决但最难发现的。接线方案上我用的是OpenMV的UART3P4为TXP5为RX对STM32的USART1PA9为TXPA10为RX交叉连接OpenMV的TX接STM32的RXOpenMV的RX接STM32的TX。注意一定要交叉接反了就成了两边都发不收毫无反应。还有一个容易踩的坑OpenMV的P0到P5这些引脚有些是复用的如果之前做过其他实验引脚可能被配置成其他外设功能。我建议在代码里明确初始化UART引脚别依赖默认状态。2.3 电源分配共地与压降问题供电方案我踩过不少坑最终定型为电池7.4V 2S锂聚合物电池容量1800mAh电机电源直接取自电池正负极经TB6612的VM引脚供电逻辑电源电池电压经过MP1584降压模块输出5V一路给OpenMV供电一路给STM32的5V引脚板载稳压到3.3V共地所有电源地必须连在一起这里最关键的一点是电机不要和单片机共用同一路电源。直流电机的启动电流和堵转电流波动非常大如果从同一个降压模块取电电机一转电压就往下掉STM32和OpenMV瞬间复位这是很多“小车一启动就重启”问题的根源。还有个细节MP1584这类降压模块输出纹波大小和负载有关给OpenMV供电时OpenMV的CMOS传感器对电源纹波比较敏感我实测过纹波偏大会导致画面出现横条纹。解决办法是在OpenMV电源输入端并联一个100uF电解电容和0.1uF陶瓷电容效果立竿见影。3. OpenMV视觉识别从像素到转向指令3.1 图像预处理为什么盯着灰度阈值而不是实际颜色打开OpenMV IDE很多人第一个动作就是打开阈值编辑器对着一块黑色电工胶布调LAB阈值然后把阈值写进代码就完事了。这样做确实能在当前光照下跑起来但换个环境、换个时间段阈值就失效了。我的做法是分两步。第一步固定传感器参数关闭自动增益、关闭自动白平衡、固定曝光时间。原因很简单自动曝光会根据画面亮度动态调整曝光时间一旦调整同一块黑胶布在不同画面帧里可能呈现完全不同的LAB值。在室内灯光比较稳定的条件下完全可以用固定参数。我在教室日光灯下的曝光时间设在20000us左右增益固定白平衡固定画面稳定得很。第二步在固定参数下重新标定阈值。具体操作是把OpenMV对着赛道用IDE自带的阈值编辑器Tools - Machine Vision - Threshold Editor框选线条区域观察LAB三通道的取值范围。比如我调出来的一条黑色胶布阈值是(0, 30, -20, 20, -20, 20)意思是L通道0到30很暗绿色分量-20到20蓝色分量-20到20。实际保存时我会留一点余量把范围稍微放宽避免光线轻微波动就丢线。3.2 get_regression线性回归为什么比找色块中心更稳OpenMV巡线有两大流派一个是find_blobs找出所有色块取最大色块的中心坐标作为偏差另一个是get_regression直接在线条上做线性回归输出线条的角度和偏移量。我一开始用的是find_blobs思路找到线条的色块中心x坐标和画面中心比较得出横向偏差然后传给PID做转向。在直道上这个方案没问题但到了弯道就很吃力。因为弯道上色块是弯曲的色块中心并不等于弯道出口方向。再加上反光、断线、光照不均色块中心会乱跳小车表现就是左晃右晃。换成get_regression之后稳定性提升了非常多。这个函数对满足阈值条件的像素做鲁棒线性回归返回一个包含角度theta和偏移量rho的Line对象。theta是线条相对垂直方向的角度范围0到180度rho是原点画面左上角到回归直线的垂直距离。实际控制中我最关心的是线条在当前画面中的角度。小车在直道上时theta接近90度线条基本垂直在右弯时theta会偏大或偏小根据摄像头安装方向而定。把theta经过归一化映射到-90到90之后直接作为PID的输入偏差使用。相比色块中心坐标角度信息对未来趋势的预判性更强弯道表现好很多。3.3 ROI裁剪与姿态标定ROI感兴趣区域的设置不是随便框一块就行。我的经验是把ROI放在画面下半部分大约占整个画面高度的60%宽度取全宽。为什么这样做首先画面下方的线条是离车最近的部分也是控制最需要关注的前瞻区域画面上方远处的线条容易因为透视关系变得很细很模糊反而干扰回归结果。其次ROI越小需要处理的像素越少帧率越高控制延迟越低。OpenMV在QVGA分辨率下全画面线性回归大概能跑30到40帧裁掉上半部分后能稳定在50帧以上。姿态标定是另一个容易忽略的点。把小车放在直线上让车身完全回正然后看OpenMV输出的theta值是多少。绝大多数情况下不是整数90可能是92或者88。这个偏差需要在代码里做零点修正。我用一个offset变量保存标定值每次计算偏差时减去它error theta - 90 - offset。如果不做这一步小车在直道上就会一直有一个固定的侧向力表现为走不直。另外还要注意摄像头的安装方向。我习惯让OpenMV的Y轴正方向指向小车前方这样线条在画面里呈现的角度和小车的转向方向是直观一致的。如果摄像头装反了theta的符号和实际转向会相反PID一输出就是反向打方向小车会直接冲出去。4. 通信协议一帧数据怎么做到既简单又可靠4.1 帧格式设计帧头、载荷、校验OpenMV到STM32的数据量很小本质上每次只需要传一个角度值但通信协议的健壮性不能省。如果直接uart.write(char(theta))一个字节发过去看起来简单问题在于STM32收数据时根本不知道这个字节什么时候到、是不是完整的一帧、中间有没有丢字节。我的做法是定义了一个非常简洁的帧格式字段长度字节说明帧头20xAA 0x55固定值数据长度1载荷字节数这里固定为2角度高字节1偏角的补码高位角度低字节1偏角的补码低位校验和1从帧头到数据低字节的累加和取低8位帧尾10x0D 0x0A角度值我用int16表示单位是0.1度。比如45.3度就编码为453这样保留了一位小数的精度又不使用浮点数传输。整帧一共8个字节在115200波特率下传输时间不到0.7ms对20ms控制周期来说完全可忽略。帧头的选择有讲究。我不用单字节0xAA做帧头而是用0xAA 0x55双字节。原因是数据载荷里的字节可能恰好是0xAA或0x55双帧头加上校验和能把误同步的概率压到非常低。校验和算法也不用CRC累加和就够用了——通信距离短、环境干扰小单字节累加和的检错能力在这个场景下足够而且STM32端实现起来只有一条循环。4.2 STM32端状态机解析STM32端的接收逻辑我建议不要用简单的if(USART_GetITStatus)里一个字节一个字节地缓存再统一处理而是用状态机解析。状态机的好处是天然处理粘包和半包问题。我定义了一个状态枚举typedef enum { FRAME_STATE_WAIT_HEAD1, // 等待第一个帧头 0xAA FRAME_STATE_WAIT_HEAD2, // 等待第二个帧头 0x55 FRAME_STATE_WAIT_LEN, // 等待数据长度 FRAME_STATE_WAIT_DATA, // 等待数据载荷 FRAME_STATE_WAIT_CHECK, // 等待校验和 FRAME_STATE_WAIT_END // 等待帧尾 } FrameState;串口中断每收到一个字节就驱动状态机向前走。在WAIT_DATA状态里按照长度字段把载荷字节存进数组。收完载荷后进入校验状态计算累加和并与收到的校验字节比较一致则再检查帧尾全部通过就把角度值提取出来存入一个全局变量同时置一个“新数据到达”标志位。主循环检测到这个标志位后直接读取角度值并清零标志。这套逻辑的核心价值在于即使某一帧丢了一个字节或者错了一个字节状态机不会一直卡死。错误字节会导致校验失败状态机自动回到等待帧头的初始状态从下一帧重新开始同步。我刚开始用傻瓜式接收时一帧错了后面全乱必须复位才能恢复换成状态机后鲁棒性好了很多。4.3 串口调试中遇到的数据错位与粘包调试通信时最容易遇到的问题有三个乱码、错位、粘包。乱码大概率是波特率不匹配或者电平/共地问题这个排查起来相对直接。错位则表现为帧头能对上但数据明显不对比如角度值突然跳到几千原因可能是OpenMV发送端在初始化时先发了一堆未定义数据或者两帧之间间隔太短导致STM32一次中断处理时缓存里挤了多帧数据。粘包其实是串口通信的正常现象串口把短时间内到达的字节全放在缓冲里应用层拿到的数据可能是“半帧整帧半帧”的混合。处理粘包的思路不是让发送端慢一点那会牺牲帧率而是在解析端做状态机容错。上面说的状态机天然能应付每帧之间即使没有间隔也不怕因为帧尾和帧头是明确区分的状态走完一帧紧接着从WAIT_HEAD1开始处理下一帧。另一个实用技巧是调试阶段在OpenMV端每发送完一帧后用IDE的串口终端打印一个带帧序号的调试信息STM32端也把解析结果通过另一个串口发到电脑两边同时看非常有利于快速定位是哪一端出了问题。5. 转向控制差速模型与PID整定实操5.1 两轮差速如何换算成PWM小车底盘用的是两轮差速结构——左右两个独立驱动的轮子加上前后各一个万向轮或牛眼轮。转向靠左右轮速度差实现。控制量换算的核心公式非常直观left_pwm BASE_SPEED steering_output; right_pwm BASE_SPEED - steering_output;其中BASE_SPEED是基础速度对应小车直线行驶的PWM占空比我一般设在50左右PWM周期设1000即50%占空比。steering_output是PID控制器输出的转向修正量范围限制在-40到40之间避免修正量过大导致某一侧反转反转或者另一侧占空比超过100%。这个公式隐含了一个假设左右轮在相同占空比下转速相同。实际上由于电机个体差异、轮胎磨损、地面摩擦不同左右轮不可能完全一致。我在代码里加了一个静态标定系数实测在直道上让小车跑起来微调某个轮的增益直到它走直线。这个标定值在不同电量下要重新检一次后面讲掉电问题时再说。PWM输出用STM32定时器的PWM模式实现。我用TIM2的CH1和CH2作为左右轮PWM输出ARR设为999也就是输出频率72kHz/100072kHz对于直流减速电机来说这个频率远超人耳可闻范围电机运行安静且没有啸叫。控制方向用TB6612的AIN1/AIN2和BIN1/BIN2两个引脚一个设为高一个设为低就决定正转反转两个都低就是刹车。5.2 PID三参数分工和调参顺序视觉巡线的PID控制用角度偏差作为输入是很自然的目标值是0小车正对线条方向实际值是当前偏角。这里我用的是增量式PID输出是转向修正量。先简单回顾三个参数的分工Kp比例偏差越大修正越猛。只加Kp时小车在直道上会来回摆动因为修正量总有滞后冲过头了又要反方向修回来。Ki积分消除静差。在巡线场景中如果光照变化导致线条识别系统偏差或者底盘左右轮标定不准比例控制会留下一个稳定偏差积分项可以慢慢补上。Kd微分抑制振荡。偏差变化越快输出的反向阻尼越大。在弯道入弯时角度突然变大微分项能预判趋势减少超调。我调参的顺序是先把Ki和Kd设成0只加Kp从小往大加。设定Kp10跑一圈小车在直道上来回晃Kp加到40左右直道基本稳住但弯道入口会有明显抖动。然后加Kd从Kd20开始逐步加大到80弯道抖动明显改善入弯时更顺滑。最后加很小的KiKi1到3就够了用来补偿静态偏差。实际调的时候有个观察技巧让OpenMV通过串口把theta值实时发给电脑用串口绘图软件比如VOFA或者SerialPlot画出偏差曲线。直道行驶时看曲线的振荡频率和振幅弯道行驶时看曲线的超调量。一味调大Kp会让曲线变成高频率低幅度的锯齿状适当增加Kd可以让曲线更平滑但如果Kd过大整个系统会变得迟钝弯道响应变慢。5.3 弯道策略偏角大时先减速再转向这是整个项目里提高完赛率最有效的一个优化。巡线车的死法大多数不是直道翻车而是弯道——入弯速度太快转向修正来不及跟上车直接冲出赛道。我在PID调了差不多之后加了一个简单的分层策略if (abs(error) 45) { base_speed 25; // 大角度急弯降速 } else if (abs(error) 20) { base_speed 38; // 中等弯道 } else { base_speed 50; // 直道或小偏差 }这里的逻辑并不复杂偏角越大说明弯道越急越需要降低前进速度提高转向修正量的相对权重。实测下来加了这道速度分层后急弯过弯成功率明显提升。要注意的是切换速度时不能跳变否则会产生顿挫感。我在代码里加了一阶低通滤波对base_speed做了平滑过渡每次调节步长不超过3。还有一种进阶玩法是“前瞻距离”动态调整ROI速度高时把ROI放远一点提前看到弯道趋势速度低时ROI拉近减少干扰。这个我在后期调过效果有但对赛道环境依赖比较大如果赛道颜色反光严重远端ROI反而容易误检。如果你的赛道比较简单干净可以试试这个方向。6. 联调阶段的典型故障与完整排查链路6.1 画面正常但小车不动逐级定位法这是最常见的问题也是最值得认真捋一遍排查思路的问题。我的办法是从信号链路的源头到末端逐级检测。第一级确认OpenMV是否在发送。用OpenMV IDE的串口终端在代码里加一条print(theta)打印语句跑起来看IDE终端是否有数据滚动。如果终端有数据说明OpenMV的识别和打印逻辑正常问题可能在UART发送管道。注意检查uart UART(3, 115200)的初始化是否在sensor.skip_frames()之后一些复用引脚的配置冲突会在初始化时报错。第二级确认OpenMV物理上是否发出来了。用USB转TTL工具接在OpenMV的TX引脚上在电脑串口助手里看有没有帧数据。如果这里没有数据很可能是引脚定义错了。OpenMV的UART3对应引脚是P4TX和P5RX这个一定要查芯片手册核对不同型号OpenMV的引脚映射可能有差异。第三级确认STM32是否收到了数据。在STM32串口中断里加一个计数器每收到一字节计数加1接上OpenMV后看计数是否增长。如果计数不动检查TX到RX的线是否接好、是否交叉、共地是否正常、两块板子的3.3V参考是否一致。第四级确认数据是否解析成功。在状态机解析通过后置位一个LED如果LED不亮说明协议封装不一致——两边对帧头、帧尾、校验的定义必须完全一致一个字节都不能差。这里最容易出问题的是OpenMV端写了一个整帧字符串发送而STM32按逐字节状态机解析两边对“数据长度”的理解不一致。第五级确认PWM是否输出。用示波器或万用表频率档测STM32定时器PWM引脚如果无波形检查定时器初始化、GPIO复用配置。如果有波形但在电机端无反应测一下TB6612输入侧PWMA/PWMB有没有波形、方向脚的电平对不对、VM电源是否到位。快速定位原则是动作没发生先从源头往下游推数据不对先用逻辑分析仪抓波形看帧格式。我那次调了很久才发现是OpenMV的TX接到了STM32的PA9上而PA9正好是USART1的TX等于两边都在发信号完全冲突。6.2 直道正常弯道抖问题可能出在曝光直道稳、弯道抖甚至丢线这个现象排查到最后很多人会去调PID但一次很偶然的机会让我意识到问题在图像侧。当时我在室内日光灯下测试跑直道时OpenMV的线性回归输出基本稳定在90度附近。但一进弯道由于弯道处线条在画面中会形成更宽的反射区或者日光灯的频闪效应导致两帧画面的亮度差异大get_regression输出的theta在弯道边缘出现了跳变比如从70度突然跳到92度然后再跳回75度。PID拿到这个跳变的输入输出自然疯狂抖动。解决办法有两层。第一层是把曝光时间固定下来。50Hz交流电的日光灯频闪是100Hz如果曝光时间不是灯光周期的整数倍每帧画面亮度就会周期性波动。我把曝光时间设在10ms20ms半周期的整数倍亮度波动明显减小。第二层是在OpenMV端对输出做滤波我用简单的递推平均连续存5个theta值取平均作为真正发送到STM32的结果。这个滤波会引入一点延迟但换来的是控制输入平滑PID的压力小很多。另外补充一个排查弯道抖动的技巧把OpenMV的img.draw_line(line.line(), color(255,0,0))画线结果通过IDE的帧缓冲区显示出来录一段像逐帧看弯道处回归线和实际线条的贴合情况。你会很直观地看到是不是识别本身在跳而不是控制端在抖。6.3 电量下降后行为漂移的根治思路锂电池从满电4.2V每节到放电截止3.7V左右总压会从8.4V降到7.4V幅度超过10%。对直流电机来说这个压降会导致同样PWM占空比下转速明显下降。因此小车充满电时跑得好好的跑了五六分钟后开始出现转向不足过弯越来越费劲。如果只靠PID本身理论上能自适应一部分——因为PID是基于误差反馈的误差大了输出就大。但问题是PWM已经接近限幅当基础速度设得太高转向修正量被推到了40的极限仍然不足以产生足够的差速于是车就出赛道了。我的根治方案有两个层面。第一把基础速度设在一个宽电压范围内都留有余量的值。以7.4V满电时测试如果最大合适占空比是60%那么基础速度设到45%而不是48%作为满电状态的初始值这样即使电压降到7.0V系统还有调节空间。第二在STM32端用ADC实时采集电池电压做一个分段补偿电压每下降0.2V基础速度增加2到3个PWM位。这个方案实测很有效整块电池的放电过程中小车表现都趋于一致。还有一个细节很多人不注意电量低时TB6612的VCC逻辑电压虽然是3.3V稳压出来的但如果电池电压低于某个值降压模块的输出也会跌导致STM32供电不稳开始复位。我现在会在代码里加一个低电压警告电池电压低于6.8V时蜂鸣器响一声提醒该换电池了。别等到完全没电再换对锂电池寿命和系统稳定性都有好处。最后再分享一条个人体会。视觉巡线这个项目从能跑变成稳定跑差距往往不在某一个模块多复杂而在于每个环节的余量。图像识别留一点阈值余量通信协议留一点校验余量PID留一点调节余量电源留一点压降余量每个环节都松一点整辆车就稳一大截。我一开始每个环节都卡着极限调结果全链路一联动就开始共振、丢帧、乱抖后来学会在每个关键参数上留出20%的裕量许多问题自己就消失了。如果你的小车也遇到“单独调都行连起来就废”的情况不妨从余量的角度重新审视一遍每个环节。
返回列表