十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

STM32+OpenMV智能小车红绿灯识别与运动控制系统实战解析

STM32+OpenMV智能小车红绿灯识别与运动控制系统实战解析 简介基于STM32的交通信号识别智能小车项目面向毕业设计、课程设计、学科竞赛、项目实训及嵌入式学习等场景。系统以STM32C8T6为主控实现了红绿灯识别、超声波避障、编码器测速、OLED显示与限速控制等功能适合需要从零构建嵌入式项目的初、中级开发者。压缩包共419个文件整体压缩后10.34MB涵盖C源码、H头文件、Keil工程文件uvprojx、编译生成的hex/axf固件、map映射文件与说明文档并保留.o目标文件、.crf浏览信息等中间产物既可直接编译烧录也便于分析程序结构。目前已有211人学习资源经过严格测试可正常编译运行。除完整代码和工程外结合面包板与杜邦线的硬件搭建思路也已给出即使不具备PCB绘制能力也可采用面包板、杜邦线配合传感器模块快速搭建实物并在参考项目基础上扩展更多智能功能是课程设计与竞赛练手的实用案例。1. 从一颗红灯到一次刹车信号识别小车的真实瓶颈做交通信号识别智能小车时最常见的翻车点不在图像算法而在「图像识别结果到电机动作之间的那几百毫秒」。很多队伍用 OpenMV 或 K210 做视觉却把识别结果丢在串口里STM32 侧要么收不到、要么收到了但没来得及刹车于是红灯亮着车还停在斑马线中央。这其实是典型的「系统集成」问题不是视觉算法的错。这套基于 STM32 的智能小车方案适合三类人做毕设/课设需要完整系统闭环的学生、准备工创赛或智能车竞赛的参赛队以及想快速验证「视觉识别 运动控制」联调逻辑的工程师。它的核心思路是视觉模块负责看懂红绿灯STM32 负责执行刹车、转向、启停两者通过串口协议交换「语义化指令」而不是把图像一帧帧传给主控。这个拆分让算法与运动解耦也让每个模块都能单独调试。2. 为什么不让 STM32 直接跑视觉识别架构与硬件选型2.1 视觉模块与主控的分工逻辑STM32F103ZET6 或 C8T6 能不能直接做图像识别理论可以但性价比极低。以 320x240 的 RGB565 帧为例一帧裸数据约 150KB而 F103 的 RAM 只有 20KBC8T6或 64KBZET6连一帧都放不下。即便降分辨率到 160x120 灰度图约 19KB也要把帧缓冲压到极限留给算法堆栈的空间几乎没有。真正卡死的不是算力是内存带宽和帧缓冲。所以常见的可靠做法是「双芯片」架构用 OpenMV、K210 或树莓派做图像识别STM32 专职做运动控制。我一般推荐这三组组合视觉模块主控适用场景通信接口OpenMV H7STM32F103C8T6课设/毕设上手快MicroPython 生态成熟UARTK210Maix BitSTM32F103ZET6竞赛可跑轻量 CNN 模型算力更高UART / SPI树莓派 Zero 2WSTM32F407需要更复杂视觉车道线信号灯UART / USB这套架构下视觉模块把「红灯、绿灯、黄灯、无信号」的判定结果通过 UART 打包发给 STM32。STM32 只处理状态机收到红灯就减速停车收到绿灯就恢复循迹。这样视觉线程和运动线程互不阻塞调试时可以各自用串口独立观察。2.2 硬件清单与引脚分配以「ST-Link STM32F103ZET6 OpenMV TB6612 四路灰度传感器」这套为基础供电用 7.4V 2S 锂电池。清单和关键引脚如下STM32F103ZET6 最小系统板负责运动控制、逻辑判断OpenMV H7 Plus负责交通灯识别串口发送结果TB6612FNG 双路电机驱动驱动两路直流减速电机比 L298N 压降小、发热低四路 TCRT5000 灰度传感器用于车道循迹辅助信号灯识别两路霍尔编码器电机用于闭环调速非必须但推荐引脚分配上我习惯把资源按「电机、编码器、视觉串口、循迹」四个功能域切分避免 PWM 定时器冲突功能引脚外设左电机 PWMPA8TIM1_CH1右电机 PWMPA9TIM1_CH2左电机方向PB5 / PB6GPIO 输出右电机方向PB7 / PB8GPIO 输出视觉串口 TXPA2USART2_TX视觉串口 RXPA3USART2_RX灰度传感器PC0~PC3GPIO 输入这里最容易踩的坑是 PWM 定时器的通道复用。很多 STM32F1 开发板的板载 LED 或蜂鸣器会占用 TIM1/TIM2 的部分通道如果你发现某一个 PWM 输出死活没波形先查数据手册上这个引脚的复用功能再用逻辑分析仪看波形而不是怀疑代码。2.3 电平匹配串口和共地问题OpenMV 和 STM32 都是 3.3V 逻辑直接短接 TX/RX 即可不需要电平转换。但有两件事必须做一是共地OpenMV 的 GND 和 STM32 的 GND 必须连在一起否则串口数据会随机乱码二是交叉连接OpenMV 的 TX 接 STM32 的 RXOpenMV 的 RX 接 STM32 的 TX接反是串口调不通的第一大原因。3. 交通信号灯识别的核心算法与 OpenMV 实现3.1 为什么用 LAB 色彩空间而不是 RGB交通灯识别的本质是「按颜色找发光区域」。在 RGB 空间下红色灯的 R 分量高但同时阳光照射下的白色区域 R 也不低RGB 的亮度耦合让阈值很难设。LAB 色彩空间把亮度L和颜色A/B分离红灯的 A 分量红绿轴会显著偏正绿灯的 A 分量显著偏负不受光线明暗的直接影响。这就是 OpenMV 的find_blobs默认用 LAB 阈值的原因。实际标定时不要猜阈值。用 OpenMV IDE 的「帧缓冲区」工具先让小车停在灯前 30~50cm 处框选红灯区域IDE 会自动计算 LAB 范围。把这个范围抄到thresholds里。常见做法是设两组阈值一组是白天室外一组是室内/傍晚运行时根据曝光时间和平均亮度动态切换。3.2 找最大色块与面积过滤的最小代码下面是一段可直接烧录的 OpenMV 交通灯识别脚本任务识别红/绿/黄灯通过串口1发送状态码到 STM32import sensor, image, time from pyb import UART # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(30) sensor.set_auto_whitebal(False) # 固定白平衡防止颜色漂移 # UART3: TX(P4), RX(P5) uart UART(3, 115200, timeout_char1000) # LAB阈值: (L_min, L_max, A_min, A_max, B_min, B_max) # 分别在OpenMV IDE里用阈值编辑器框选得到 red_threshold (30, 100, 25, 80, -10, 50) # 红色灯 green_threshold (30, 100, -80, -20, -10, 50) # 绿色灯 yellow_threshold (50, 100, -10, 30, 30, 80) # 黄色灯 def find_light_color(img): # 1. 查找红色区域 for blob in img.find_blobs([red_threshold], pixels_threshold50, area_threshold50): if blob.area() 300: return 0x01 # 红色 # 2. 查找绿色区域 for blob in img.find_blobs([green_threshold], pixels_threshold50, area_threshold50): if blob.area() 300: return 0x02 # 绿色 # 3. 查找黄色区域 for blob in img.find_blobs([yellow_threshold], pixels_threshold50, area_threshold50): if blob.area() 200: return 0x03 # 黄色 return 0x00 # 未识别到信号灯 while True: img sensor.snapshot() color find_light_color(img) # 帧格式: 0xAA 0x55 0x01 color 0x0D 0x0A data bytes([0xAA, 0x55, 0x01, color, 0x0D, 0x0A]) uart.write(data) time.sleep_ms(50)这段代码的逻辑分三层find_blobs在整帧图像中搜索符合 LAB 阈值的像素连通域area_threshold过滤掉小于 50 像素的噪点blob.area() 300进一步排除远处的小色块。time.sleep_ms(50)把发送频率控制在 20Hz这个频率对车辆控制足够了太快反而会让 STM32 串口中断占用过多 CPU 时间。关键参数说明pixels_threshold50色块包含的像素总数下限设太小会捕获噪点设太大会漏掉远处的小灯。area_threshold50色块外接矩形面积下限和pixels_threshold是两层过滤防止密集噪点刚好凑够像素数。sensor.set_auto_whitebal(False)必须关自动白平衡。开着白平衡摄像头会把红灯的红色「纠正」成白色这是识别失败的头号原因。发送间隔 50ms和 STM32 端的串口接收超时时间要匹配STM32 侧建议设 100ms 超时避免临界情况下状态机抖动。3.3 信号灯防抖不能只靠单帧判断真实的交通灯有亮灭时序LED 灯还存在 50Hz 或 100Hz 频闪摄像头逐帧采样时可能抓到暗帧。如果每次都直接更新状态STM32 可能会在一秒内收到「红-无-红-无-红」的抖动序列导致小车在路口反复起步停车。我的处理方式是「连续 N 帧有效才切换状态」在 OpenMV 侧维护一个 5 帧的滑动窗口只有连续 5 帧都识别到同一颜色才更新发送状态同时间隔心跳帧仍然发送当前状态保证 STM32 知道视觉模块还活着。这在工创赛或智能车竞赛的场地里尤其重要强光下偶尔丢一两帧很正常但状态机不能被带着抖。另一种做法是把「防抖逻辑放在 STM32 侧」OpenMV 只发原始识别结果STM32 侧用状态机做require_count计数达到 N 次才切换执行状态。这比在 OpenMV 里做更灵活因为后续要调判断阈值时不用重新烧录视觉固件只改系统主控的代码就能完成验证。4. STM32 侧的运动控制与串口协议解析4.1 这才是两轮差速小车真正需要关心的视觉识别只是系统的眼睛STM32 控制电机运动才是小车的本体。常见的小车控制采用两轮差速左轮和右轮各自由一路 PWM 驱动转向靠两侧轮速差实现。计算公式为左轮速度 基础速度 转向修正 右轮速度 基础速度 - 转向修正这里的「修正量」可以是推导的 PID 输出也可以是根据偏离角度查表的经验值。如果小车上了编码器电机就可以做闭环控制用增量式 PID 让左右轮达到目标速度。我常用的参数是Kp 1.2Ki 0.05Kd 0.3这个组合在大多数 12V 以下的直流减速电机上都能直接用但启动时尽量加上限幅防积分饱和导致小车起步猛冲。4.2 自定义串口协议的帧格式设计OpenMV 的代码里我定义了帧格式0xAA 0x55 0x01 [状态码] 0x0D 0x0A。为什么不用裸发一个字节因为裸发无法解决两个问题一是识别结果丢帧后对方无法感知二是视觉模块重启后 STM32 不会知道。帧头0xAA 0x55是同步标记0x01是消息类型0x0D 0x0A是帧尾。这个协议只有 6 个字节短小且便于在小车这种低算力 MCU 上做状态机解析。4.3 状态机解析串口数据的 STM32 代码下面给出 STM32 标准库/HAL 通用的串口中断接收解析代码用状态机方式逐字节处理/* 定义协议状态机状态 */ typedef enum { FRAME_WAIT_AA 0, // 等待帧头1 FRAME_WAIT_55, // 等待帧头2 FRAME_WAIT_TYPE, // 等待消息类型 FRAME_WAIT_DATA, // 等待数据字节 FRAME_WAIT_0D, // 等待帧尾第一字节 FRAME_WAIT_0A // 等待帧尾第二字节 } frame_state_t; volatile uint8_t rx_state FRAME_WAIT_AA; volatile uint8_t signal_color 0x00; // 当前信号灯状态 volatile uint8_t rx_buf[6]; // 接收缓冲区 volatile uint8_t rx_index 0; /* 在串口中断回调中调用每收到一个字节执行一次 */ void uart_rx_sm(uint8_t byte) { switch (rx_state) { case FRAME_WAIT_AA: if (byte 0xAA) { rx_state FRAME_WAIT_55; rx_index 0; rx_buf[rx_index] byte; } break; case FRAME_WAIT_55: if (byte 0x55) { rx_state FRAME_WAIT_TYPE; rx_buf[rx_index] byte; } else { rx_state FRAME_WAIT_AA; // 帧头错误重新同步 } break; case FRAME_WAIT_TYPE: if (byte 0x01) { rx_state FRAME_WAIT_DATA; rx_buf[rx_index] byte; } else { rx_state FRAME_WAIT_AA; } break; case FRAME_WAIT_DATA: rx_buf[rx_index] byte; rx_state FRAME_WAIT_0D; break; case FRAME_WAIT_0D: if (byte 0x0D) { rx_state FRAME_WAIT_0A; rx_buf[rx_index] byte; } else { rx_state FRAME_WAIT_AA; } break; case FRAME_WAIT_0A: if (byte 0x0A) { /* 完整一帧接收完成提取数据 */ signal_color rx_buf[3]; rx_state FRAME_WAIT_AA; } else { rx_state FRAME_WAIT_AA; } break; } }这段状态机代码的逻辑是逐字节推进每个字节只做一次比较不阻塞串口中断也不依赖 DMA 长度判断。解析后的signal_color就作为运动控制状态机的输入。注意rx_index的作用是保留完整帧数据供后续提取正确帧必须严格满足AA 55 01 XX 0D 0A六字节结构任何一字节不符合都会让状态机回到FRAME_WAIT_AA重新同步。4.4 运动策略红灯停、绿灯行、黄灯减速有了signal_colorSTM32 的运动控制就是一个简单状态机信号灯状态运动控制逻辑0x01红立即停止 PWM 输出原地停车等待状态切换0x02绿恢复基础速度前进同时进入循迹模式0x03黄减速至基础速度的 50%不能急刹防翻车0x00未知保持当前速度但禁用「红转绿后起步」的快速切换这里有个细节值得注意红灯状态不能只在停车时生效一次就完事必须在每次控制周期持续检查。因为视觉模块可能在红灯中途丢失信号如强光干扰如果 STM32 用「只响应一次」的方式处理小车会在红灯期间重新起步。我一般在主循环里用一个last_signal_color变量做记忆红灯态下如果连续 2 秒收到未知状态才允许进入缓慢探索逻辑。5. 联调抓帧与三步排错法5.1 第一步用 USB 转 TTL 抓视觉模块的上行数据视觉模块和主控之间的协议是整个系统最容易出问题的地方。常见错误是 OpenMV 一直在发数据ST 端却一条指令都没收到。此时把 USB 转 TTL 模块的 RX 接到 OpenMV 的 TX 上打开串口助手波特率 115200选择十六进制显示观察是否稳定出现AA 55 01 XX 0D 0A的帧序列。如果帧结构断断续续优先排查视觉模块和 STM32 是否共地USB 转 TTL 模块是否选对了 3.3V 档位不少模块默认 5V 电平会把 OpenMV 的 TX 拉坏以及理线时是否把串口线和大电流的电机动力线捆在了一起。电机启停瞬间的 EMI 会把串口波形打乱我通常会让串口线远离电机线 5cm 以上并给视觉模块单独加 100uF 电解电容稳压。5.2 第二步用固定阈值表验证不同光照下的区间波动把 OpenMV 的识别结果和实际灯色对不上时别急着调代码先把摄像头固定分别拍摄红灯、绿灯、黄灯、无灯四种场景的截图在 IDE 里把每个场景的 LAB 均值记录成表格。你会发现白天阳光下的红灯 A 分量在 40~70 之间傍晚室内则降到 25~45。这个区间就是夜间误识别的根源。我最终用的方案是让 OpenMV 在开机时先跑 10 帧采样计算当前平均亮度然后切换阈值组。每组阈值是针对一个亮度区间标定的不做全局单一阈值。这也是很多智能车竞赛队伍的隐藏优化点不是算法多复杂而是针对环境有了自适应。5.3 给信号识别加上超时保护最后一个建议是在 STM32 的运动状态机里加超时保护。信号灯是周期性变化的但小车在场地里转圈时摄像头可能长时间看不到灯。此时若视觉模块因死机而停止发送心跳帧STM32 必须能感知并做出兜底动作。我用的是看门狗思路定义一个signal_timeout变量每次收到合法帧清零在主循环里递增当超过 500ms 未收到任何帧时强制将signal_color置为 0x00未知小车按未知状态处理——低速前进并持续尝试循迹。这样视觉模块就算死机重启STM32 不会停在路中间而是以安全速度继续运行同时把状态信息打印到调试串口。这个兜底逻辑既不干扰正常识别又避免了「视觉死机导致小车闯红灯」这种联调时最怕的事故。本文还有配套的精品资源点击获取
返回列表