第 7 篇:给小车装上眼睛——OpenMV 视觉识别与串口通信
第 7 篇给小车装上眼睛——OpenMV 视觉识别与串口通信回顾全套小车工厂模型第1篇看懂main入口、头文件工具箱、开机初始化、while(1)主线流水线第2篇掌握变量储物盒、嵌入式专用定宽数据类型第3篇学会分支循环控制流、函数模块化、枚举状态机第4篇搞懂中断定时闹钟、volatile关键字、中断标准三步写法第5篇吃透PWM油门、编码器测速、PID速度闭环、陀螺仪角度纠偏小车稳直准行驶第6篇打通无线通信NRF24L01无线收发、SPI通道、自定义通信帧、异或校验、断线保护遥控器按键变小车指令本篇给小车装上眼睛OpenMV摄像头识别色块/二维码通过串口(USART3)把看到的结果发给STM32。你会发现串口帧设计和第6篇NRF帧一脉相承最终同样汇入Set_Motion_Mode。彻底打通「摄像头看到 → 小车自动行动」自主视觉链路。0. 引子遥控器再强也得人一直按第6篇之后小车已经能听你指挥了。但有个局限遥控器要人一直拿着按。人一松手车就停人不在现场车就是块砖。真正智能的小车要能自己看路、自己决策看见地上的红线/目标色块自己追上去扫到二维码自己按二维码里的指令行动相当于摄像头当遥控器用。这件事靠一个带处理能力的摄像头OpenMV来实现它和 STM32 分工协作OpenMV 负责看和认STM32 负责算和动。1. OpenMV小车的眼睛 视觉大脑1.1 它是什么一句话看懂OpenMV 是一颗自带图像处理能力的摄像头模组跑 MicroPython和 STM32 的 C 语言不同能直接在模块上跑代码、识别图像。本项目里它的角色STM32 是身体和肌肉驱动、PID、遥控OpenMV 是眼睛和视觉脑看画面、找目标、出结论。两者是两块独立芯片各干各的靠一根串口线对话。1.2 它能看懂什么本项目和入门最常用两类识别色块追踪find_blobs在画面里找出指定颜色的连通区域返回色块中心坐标、大小。小车用来追着某个颜色跑二维码识别find_qrcodes扫到二维码直接解出里面的文字/数字当成命令字发给小车相当于视觉版遥控按键。之所以选这两个是因为它们计算量小、OpenMV 实时跑得动非常适合新手上手。1.3 眼睛和大脑怎么连线串口 UARTOpenMV 处理完图像要把结论色块坐标 / 二维码命令告诉 STM32。两边用串口 UART通信对比第6篇 NRF本篇 串口介质2.4G 无线杜邦线直连有线STM32外设SPI 指挥 NRFUSART3 直接收发特点隔空、可能丢包稳定、几乎不丢但需要自己界定帧边界串口就是两根线交叉连OpenMV 的 TX → STM32 的 RXUSART3OpenMV 的 RX → STM32 的 TX本项目主要用 OpenMV→STM32 单向发结论。两边波特率必须一致本项目设为115200呼应第1篇USART3_Init(115200)。2. 串口通信帧和 NRF 一脉相承的设计2.1 串口是流式的比无线更需要帧边界第6篇的 NRF 是收一整包触发中断硬件帮我们界定了包。串口不同它一个字节一个字节往外吐没有天然的一帧概念。如果只约定每次发 4 个字节一旦中间丢掉 1 个字节接收方后面的所有解析全部错位、且永远对不回来除非重新对齐。所以串口通信比无线更依赖帧头和帧尾来标出边界。本项目直接复用第6篇的设计思想视觉帧也是 6 字节、头/尾/校验齐全typedefstruct{uint8_thead;// 帧头 0xAA标记一包开始uint8_ttype;// 类型0x01 色块追踪结果 / 0x02 二维码指令uint8_tdata1;// 主数据色块中心x(0~160) 或 二维码命令字uint8_tdata2;// 辅数据色块大小/可信度 或 速度档位uint8_tcheck;// 异或校验码和第6篇算法完全一致uint8_ttail;// 帧尾 0x55标记一包结束}CAM_Frame_t;全部uint8_t定宽类型呼应第2篇字节数固定双方解析不错位。2.2 异或校验直接复用第6篇发送端OpenMV和接收端STM32的校验算法和第6篇 NRF 完全相同checkhead^type^data1^data2;// 改任意一个字节校验必失败 → 丢弃校验不过的一律不执行杜绝干扰导致的乱动。2.3 OpenMV 端怎么发MicroPython 示例OpenMV 跑的是 Python但组帧思想和 STM32 一模一样importpyb uartpyb.UART(3,115200)# OpenMV 的 UART3和 STM32 的 USART3 波特率对应defsend_frame(cmd,d1,d2):head,tail0xAA,0x55checkhead^cmd^d1^d2 uart.write(bytes([head,cmd,d1,d2,check,tail]))# 色块追踪把最大色块的中心 x 发过去blobmax_blobs# 找到目标颜色里最大的色块send_frame(0x01,blob.cx(),blob.pixels())# type0x01data1中心x# 二维码把二维码内容当命令字发过去forqinimg.find_qrcodes():send_frame(0x02,int(q.payload()),0)# type0x02data1命令字可以看到OpenMV 只负责组好一帧发出去含义解释全在 STM32 端分工和第6篇 NRF 完全一致。3. 串口接收与解析状态机 中断 volatile3.1 USART3 接收中断呼应第4篇中断三步STM32 用 USART3 的收到一个字节中断来收数据严格走第4篇的三步判断标志、处理、清标志。volatileuint8_tring_buf[32];// 环形缓冲中断写、主循环读volatileuint16_tring_w0;// 写指针中断里自增voidUSART3_IRQHandler(void){if(USART_GetITStatus(USART3,USART_IT_RXNE))// 1. 判断是接收中断吗{uint8_tbUSART_ReceiveData(USART3);// 2. 处理取走这个字节ring_buf[ring_w%32]b;ring_w;USART_ClearITPendingBit(USART3,USART_IT_RXNE);// 3. 清标志否则反复进中断}}3.2 为什么用环形缓冲 状态机串口字节是零散到达的主循环不能每次只处理一个字节就下结论。正确做法中断里只负责把字节塞进环形缓冲ring_buf快进快出绝不拖延呼应第4篇中断要短平快主循环里用状态机从缓冲中拼出完整的一帧先等0xAA再收满数据最后看0x55收尾。状态机解析核心逻辑typedefenum{S_HEAD,S_DATA}state_t;state_tstS_HEAD;uint8_tbuf[6],idx0;volatileuint16_tring_r0;// 读指针主循环里移动voidCam_Parse(void){while(ring_r!ring_w)// 缓冲里还有未处理字节{uint8_tbring_buf[ring_r%32];ring_r;if(stS_HEAD){if(b0xAA){buf[0]b;idx1;stS_DATA;}// 命中帧头开始收}else{buf[idx]b;if(idx6)// 收满 6 字节{if(buf[5]0x55)// 帧尾对{uint8_tcbuf[0]^buf[1]^buf[2]^buf[3];if(cbuf[4])cam_frame_ready1;// 校验过 → 标记一帧就绪}stS_HEAD;idx0;// 无论成败都回到找帧头状态自动重同步}}}}关键优势呼应第6篇本篇思考题即使中途丢了 1 个字节状态机下次遇到0xAA会重新对齐不会永久错位。这正是帧头帧尾存在的意义。3.3 volatile 不能少呼应第4、6篇ring_buf、ring_w、cam_frame_ready都是中断写、主循环读的共享变量必须加volatile否则编译器缓存优化会让主循环读不到新数据和第6篇new_frame_flag是同一个坑。3.4 主循环解析流程主循环里先收串口、再判断线参考第6篇断线保护思路太久没收到视觉帧也可降级为停车或切回遥控最后按帧含义行动while(1){Cam_Parse();// 1. 从缓冲里拼帧if(cam_frame_ready){cam_frame_ready0;HandleCamFrame();// 2. 按 type 分派色块追踪 / 二维码指令}HandleNRFCommands();// 3. 遥控照常处理遥控优先或视觉优先自己定// 4. 运动计算、屏幕刷新……}4. 两种视觉玩法色块追踪 二维码指令4.1 色块追踪看见目标就追位置闭环呼应第5篇 PIDOpenMV 把色块中心 x发过来。画面宽 160正中央是 80。色块偏左说明车该往左转把它拉回中央——这本质是一个位置闭环思路和速度 PID 完全一致偏差大 → 修正强。voidHandleCamFrame(void){CAM_Frame_t*f(CAM_Frame_t*)cam_buf;if(f-type0x01)// 色块追踪结果{int16_terr(int16_t)f-data1-80;// 距画面中心的偏差int16_tturnerr*6;// 简化 P 控制偏差越大转越强Set_Tracking(turn,f-data2);// 让车转向把色块拉回中央}elseif(f-type0x02)// 二维码指令{Set_Motion_Mode(f-data1,f-data2);// 直接复用第6篇的运动切换}}注意Set_Motion_Mode就是第6篇那个函数——二维码指令走和遥控按键完全相同的执行路径摄像头等于会扫码的遥控器。4.2 完整链路图[OpenMV 看画面] → find_blobs / find_qrcodes 识别 → 组 6 字节帧(头/类型/数据/校验/尾) → UART 串口 115200 发出 → STM32 USART3 接收中断 → 塞进环形缓冲 → 主循环状态机拼帧 → 校验帧头帧尾异或 → HandleCamFrame 分派 → Set_Tracking / Set_Motion_Mode复用第6篇 → 四轮 PID 算 PWM → 电机转小车自动行动到这里第1~6篇的所有技术——初始化、变量、函数、中断、volatile、PWM、编码器、PID、NRF 通信帧——在自主视觉这条链路里全部再次被串起来。你扫一个二维码背后是几十个知识点在协同。5. 本篇小结知识点通俗理解OpenMV带处理能力的摄像头负责看画面、认目标串口UARTOpenMV 与 STM32 的对话线TX→RX115200 波特率视觉帧复用第6篇思想头/类型/数据/校验/尾6字节定长异或校验和 NRF 同款算法脏数据一律丢弃接收中断USART3 每收一字节进中断呼应第4篇三步环形缓冲中断快存、主循环慢取互不拖沓状态机拼帧用帧头帧尾从字节流里还原完整帧丢字节能自动重同步volatilering_buf/ring_w/标志位必须加否则主循环读不到色块追踪位置闭环P控制把目标拉回画面中央二维码指令摄像头当遥控器用直连 Set_Motion_Mode6. 第6篇趣味思考标准答案思考1通信帧删掉帧头 0xAA、帧尾 0x55只留命令/速度/校验连续传输会出什么故障串口是字节流没有帧头帧尾就等于没有标点的一长串字接收方只能硬猜每几个字节是一句。一旦中间因为干扰丢掉 1 个字节后面所有帧都会整体错位一位原本的速度被当成命令、“校验被当成速度”……小车会执行完全错误的动作而且再也自我纠正不回来只能一直错下去。有了0xAA/0x55即使丢掉一字节下一次命中帧头就能重新对齐正是本篇状态机的做法。所以帧头帧尾是防失步、能自愈的关键。思考2遥控 NRF 与小车 NRF 地址一致、频道不同小车能否收到为什么收不到。NRF24L01 先靠频道决定收听哪个频率窗口——接收端必须调谐到和发射端完全相同的频率才能听到那一段电波地址是在同一频率上进一步筛选这包是不是发给我的。频道不同 根本不在同一个频率上收听电波完全收不到地址匹不匹配毫无意义。呼应第6篇 1.3 节错一个频道直接收不到数据。6.1 本篇趣味思考答案见第8篇末尾本项目 OpenMV 通过串口只发结论、不发整张图给 STM32。如果改成把整张 160x120 的彩色图传过去让 STM32 处理会遇到什么现实问题色块追踪里如果err 色块中心x - 80算出负数色块偏左按本篇turn err * 6的逻辑小车应该往哪边转才能把色块拉回中央为什么7. 交流我目前也是嵌入式自学新手本篇内容是结合自己麦轮小车项目实操总结而来部分知识点理解可能存在偏差讲解也有不够严谨的地方。技术大佬如果发现文中存在错误、逻辑漏洞欢迎评论区指正我会及时修正文章零基础小白有看不懂的知识点、实操踩坑问题都可以留言提问我看到会逐条回复大家也可以分享自己做智能小车的经验、代码优化思路一起交流学习共同进步祝愿各位大佬项目开发一路顺畅调试零bug思路源源不断祝愿所有入门小白少踩弯路快速吃透STM32早日做出属于自己的智能小车下篇预告《第 8 篇把一切串起来——整车联调、OLED 显示与调试避坑》前 7 篇把小车的身体、肌肉、耳朵、眼睛全讲完了。最后一篇做总装主循环如何统筹遥控 视觉 屏幕OLED 怎么把车速/模式/状态显示出来以及新手联调时最常踩的坑上电顺序、中断优先级、变量共享、PID 调参和一套先动哪、再看哪的排查套路帮你把零散知识真正拧成一台能跑的小车。
