ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32智能小车交通信号识别:OpenMV视觉与串口控制实战

STM32智能小车交通信号识别:OpenMV视觉与串口控制实战 简介这套基于STM32设计的交通信号识别智能小车项目资料面向毕业设计、课程设计、学科竞赛与项目实训等场景主控采用STM32F103C8T6实现了超声波避障、编码器测速、OLED显示、限速及红绿灯识别等完整功能。压缩包共419个文件约10.34MB以C源码、H头文件、Keil工程文件uvprojx、编译产物axf/hex以及说明文档等为主各模块代码与工程结构清晰方便直接编译烧录与二次开发。目前已有211人学习下载资料经过严格测试能够复现出功能正常的项目。配套内容包含源码、工程文件和说明并给出面包板加杜邦线加外设模块的接线思路适合不擅长PCB设计的初学者。基于此项目可快速实现功能演示也能扩展出更多智能车玩法。1. 先解决“识别”再谈“小车”——交通信号识别项目的第一道坎把一个摄像头装到 STM32 智能小车上容易难的是让小车在真实光照下分清红灯和绿灯并且做出对应动作。很多第一次做这个题目的同学会把精力全放在电机驱动和底盘组装上等到联调才发现STM32F103 那颗 72MHz 的 Cortex-M3 根本跑不动稍复杂一点的图像算法。我的建议是把视觉识别这块从主控里拆出去交给专门的视觉模块STM32 只做运动控制和人机交互。这个分工方式兼顾了开发速度、答辩可展示性和竞赛现场调参的灵活度也是这类项目里最稳妥的落地路径。2. 整体架构与硬件选型把“交通信号识别”拆成三层2.1 视觉识别方案的三种取舍交通信号识别智能小车核心是“看见信号灯—判断颜色—驱动小车”。常见的视觉方案有三类选型直接决定后续开发工作量。方案硬件载体识别能力开发复杂度适用场景摄像头 颜色阈值OpenMV、K210红绿灯颜色识别、简单形状判断低Python/C 快速迭代课设、毕设、中小型竞赛摄像头 模板匹配OpenMV、树莓派能识别箭头形状、数字倒计时中需要准备模板库有固定信号灯造型的比赛上位机 深度学习树莓派 / PC 端 YOLO通用目标检测抗干扰强高需要训练标注数据集工程实训、前沿项目开发对于 STM32 智能小车这个标题最常用的做法是第一种。OpenMV 本身集成了摄像头、处理器和 LED 补光灯固件里提供find_blobs这类现成接口把 LAB 颜色阈值设好就能拿到目标色块的中心坐标。不需要自己啃图像处理算法也不依赖上位机现场演示时拖着一条串口线或者用无线串口模块就能调参。K210 也是可选方案它的算力比 OpenMV 更强可以跑轻量级神经网络但对初学者来说模型训练和烧录流程要陡峭得多。如果目的是在两周内把一套完整系统跑起来我建议优先选 OpenMV。2.2 STM32 与视觉模块的分工协作这套系统的架构就三步OpenMV 负责看STM32 负责动两者之间通过串口通信。摄像头采集 → OpenMV 图像处理 → 判断红/绿/黄灯 → 串口发送指令 ↓ STM32 串口接收指令 ↓ 解析指令 → 控制电机驱动 → 差速转向/启停OpenMV 只发送三类短指令红灯停车、绿灯通行、黄灯减速。STM32 不需要关心图像算法只需要处理几个字节的串口协议这样两端的耦合度非常低。哪怕现场 OpenMV 程序崩了小车主控也不会跟着死机。关于 STM32 型号最常见的是 STM32F103C8T648 引脚性价比高如果考虑到后续要同时挂编码器、OLED、蓝牙模块、灰度传感器引脚会吃紧这时候选 STM32F103ZET6 这种 100 引脚的型号更从容。需要特别留意的是摄像头模块和 STM32 的供电隔离问题这个放到第 5 章细说。2.3 电机驱动与底盘设计要点底盘选择上两轮差速小车是最常见配置结构简单、转向灵活。两个直流减速电机加一个万向轮就能组成底盘本体比四轮阿克曼结构好控制得多转弯半径为零适合在模拟道路的小场地里跑。电机驱动芯片推荐 TB6612FNG而不是 L298N 模块。TB6612 的内阻更低发热小PWM 频率响应好不需要额外散热片。L298N 压降太大电池电压掉到一定程度电机就没劲了而且体积笨重。控制方式上STM32 定时器输出两路 PWM 分别控制左右电机方向引脚控制正反转。开环状态下直接调节 PWM 占空比就能让小车前进、停止和转向闭环状态下编码器反馈接入 STM32 的定时器输入捕获做速度环控制。第 6 章会专门讲编码器闭环怎么加。3. 交通信号识别的核心OpenMV 颜色阈值与识别逻辑3.1 为什么用 LAB 颜色空间而不是 RGB红绿灯识别最容易踩的坑是用 RGB 阈值切颜色。RGB 三个通道的值都随着光照强度成比例变化上午十点和下午四点同一盏红灯的 RGB 数值完全不同阈值没法写死。OpenMV 的sensor.snapshot()返回的图像可以直接在 LAB 空间做阈值分割L 表示亮度A 通道从绿到红B 通道从蓝到黄。红绿灯识别这一路关键是 A 通道。红色目标的 A 值显著为正绿色目标显著为负两者天然分在数轴两端用一个范围就能过滤掉大量背景。把自动增益和白平衡关掉再把曝光时间固定住颜色阈值基本就能稳定用一天。3.2 可复用的 OpenMV 识别代码下面这段代码是 OpenMV H7 Plus 上跑通的最小识别程序逻辑上做了三件事找红色块、找绿色块、连续三帧确认后通过串口发送指令。我把识别窗口限制在画面中心 160×120 区域因为信号灯大概率出现在视野中上部裁剪掉两侧的干扰物还能明显提高帧率。import sensor, image, time from pyb import UART # 初始化摄像头RGB565 格式中心窗口 160x120 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 内部仍是 320x240 sensor.set_windowing((80, 60, 160, 120)) # 取中心区域 sensor.skip_frames(time2000) # 关闭自动增益和白平衡否则阈值会随环境漂移 sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) # 串口 3TXP4(TX), RXP5(RX)波特率 115200 uart UART(3, 115200, timeout_char1000) # LAB 阈值按顺序为 (L_min, L_max, A_min, A_max, B_min, B_max) RED_THRESH (30, 100, 15, 127, -10, 40) GREEN_THRESH (40, 80, -60, -10, 10, 50) MIN_AREA 300 # 小于该面积的色块直接丢弃 def find_signal(thresholds, frame): blobs frame.find_blobs([thresholds], pixels_threshold80, area_threshold80, mergeTrue) max_blob None for b in blobs: # 只取画面上部区域避开路面红色物体和尾灯 if b.cy() frame.height() * 0.35: continue # density 是色块面积与最小外接矩形面积之比 if b.area() MIN_AREA and b.density() 0.3: if max_blob is None or b.area() max_blob.area(): max_blob b return max_blob def send_cmd(cmd): payload bytearray([0xFF, 0x01, ord(cmd)]) checksum (0x01 ord(cmd)) 0xFF payload.append(checksum) uart.write(payload) red_count 0 green_count 0 while True: frame sensor.snapshot() red_blob find_signal(RED_THRESH, frame) green_blob find_signal(GREEN_THRESH, frame) if red_blob and not green_blob: red_count 1 green_count 0 if red_count 3: send_cmd(R) red_count 0 elif green_blob and not red_blob: green_count 1 red_count 0 if green_count 3: send_cmd(G) green_count 0 else: red_count 0 green_count 0 time.sleep(20)代码逻辑拆开讲find_blobs里的pixels_threshold80表示色块像素总数少于 80 的候选目标直接剔除用于过滤图像噪点mergeTrue会把相邻的同一颜色小色块合并成一个大色块防止信号灯因为内部反光被拆成好几块。density()返回色块面积和它的外接矩形面积的比值圆形红灯的 density 接近 π/4 ≈ 0.785而条形红色物体的 density 通常低于 0.3这个参数是区分圆灯和红色栏杆、红色车贴的有效手段。连续三帧判定是防抖的关键。单帧识别可能因为汽车颠簸、遮挡、传感器噪声产生误判三帧连续命中才对外发送指令能过滤掉绝大多数偶发干扰。time.sleep(20)给传感器留出曝光时间也把循环帧率控制在合理范围。协议层面每个指令包是 4 字节0xFF作为帧头0x01表示负载长度第三字节是命令字符第四字节是校验和。校验和取长度和命令字节之和的低 8 位。接收端通过帧头定位通过长度约束再通过校验和丢弃损坏的帧保证 STM32 侧解析的正确性。3.3 黄灯识别的边界问题很多题目要求识别红黄绿三色。实操里黄灯是最难调的OpenMV 的 LAB 空间里黄色目标的 A 通道值和红色有重叠同一个阈值可能同时命中红灯和黄灯。我的处理建议是如果场地里黄灯是固定出现的单独调一套YELLOW_THRESH并用色块的宽高比做区分——圆灯接近 1:1箭头灯则一定是一个方向明显拉长。如果答辩现场没有硬性要求可以直接把黄色指令映射为“减速”视觉上识别到黄灯就发Y但优先级低于红灯。4. STM32 运动控制串口协议解析与差速转向4.1 串口中断里的状态机解析STM32 端收串口数据不要用if (data R)这种简单方式直接判断原因有两个OpenMV 发送的 4 字节帧可能在两次中断里到达第一次收到0xFF第二次才收到0x01和命令字节另外如果发送端程序异常没有帧头的数据流会让接收端不知所措。正确的做法是维护一个简单的状态机。typedef enum { WAIT_HEAD, WAIT_LEN, WAIT_CMD, WAIT_CHECK } parse_state_t; volatile parse_state_t parse_state WAIT_HEAD; volatile uint8_t rx_cmd 0; volatile uint8_t rx_len 0; volatile uint8_t rx_sum 0; void UART4_IRQHandler(void) { uint8_t byte USART_ReceiveData(UART4); switch (parse_state) { case WAIT_HEAD: if (byte 0xFF) { parse_state WAIT_LEN; rx_sum 0; } break; case WAIT_LEN: if (byte 0x01) { // 负载长度只有 1 字节 rx_len byte; rx_sum byte; parse_state WAIT_CMD; } else { parse_state WAIT_HEAD; // 长度不对重新找帧头 } break; case WAIT_CMD: rx_cmd byte; rx_sum byte; parse_state WAIT_CHECK; break; case WAIT_CHECK: if (byte rx_sum) { handle_signal_cmd(rx_cmd); // 校验通过才执行 } parse_state WAIT_HEAD; break; default: parse_state WAIT_HEAD; break; } }这个状态机的好处是具备自同步能力。哪怕收到一半的数据流打断只要遇到0xFF就能重新进入正确的解析轨道长度校验和和校验两重保险把物理链路误码造成的误动作概率降到极低。WAIT_CHECK状态在校验失败时直接丢弃整帧不执行任何命令避免小车收到半截指令后做出危险动作。handle_signal_cmd里的动作映射很简单R置停止标志位G清停止标志并恢复基准速度Y把目标速度降 40%。真正的 PWM 输出在定时器中断里根据这些标志位计算不在串口中断里直接改占空比防止占用串口中断时间过长。4.2 差速转向的 PD 控制实现OpenMV 发送的指令里只有颜色信息没有坐标信息。所以转向控制我做成了两档绿车直行、红灯停车。如果想进一步让小车沿着道路中线走可以让 OpenMV 把红灯或绿灯的blob.cx()目标横坐标也传下来。扩展协议把第三字节改成坐标值第四字节改成颜色标志一条帧就能同时携带“看到什么”和“在哪里”。STM32 侧拿到目标点横坐标后用它对图像中心160做误差计算误差驱动左右轮的 PWM 差速int error (int)(target_x - 160); // 目标点相对画面中心的偏差 int turn KP * error KD * (error - last_error); last_error error; int left_pwm BASE_PWM - turn; int right_pwm BASE_PWM turn; left_pwm CLAMP(left_pwm, MIN_PWM, MAX_PWM); right_pwm CLAMP(right_pwm, MIN_PWM, MAX_PWM); TIM_SetCompare1(TIM3, left_pwm); // CH1 - 左电机 TIM_SetCompare2(TIM3, right_pwm); // CH2 - 右电机误差为正代表目标在画面右侧turn为正右轮减速、左轮加速小车右转逻辑是闭合的。KP决定转向响应快慢KD决定阻尼大小。初始参数我习惯给KP 0.8、KD 0.15然后根据现场反应调整参数初始值现象调整方向KP0.8转弯太慢冲出弯道每次加 0.2KP0.8左右剧烈摆动每次减 0.1KD0.15出弯回正慢每次加 0.05KD0.15转向抖动、电机发烫每次减 0.05BASE_PWM60%直行太慢/太快依场地长度调整CLAMP宏把左右 PWM 限制在MIN_PWM到MAX_PWM之间MIN_PWM一般取整周期的 15%。低于这个值直流电机克服不了静摩擦力会发出嗡嗡声但不转PWM 容易掉进死区。与其让 PID 在小值区间反复震荡不如直接钳制到能转的最低值附近。4.3 PWM 频率与电机驱动细节PWM 频率对直流电机的控制体验影响很大。低于 5kHz 时电机会发出可闻的啸叫声而且电流纹波变大驱动芯片发热加剧高于 50kHz 时 MOSFET 开关损耗上升驱动芯片反而更容易烫手。我自己常用的配置是 20kHz听不到声音TB6612 的温度也比较正常。初始化时注意 STM32 定时器的预分频和自动重载值配合// 假设定时器时钟 72MHz目标 PWM 20kHz // PWM 频率 72MHz / (PSC1) / (ARR1) TIM_TimeBaseInitTypeDef.TIM_Prescaler 0; // PSC 0 TIM_TimeBaseInitTypeDef.TIM_Period 3599; // ARR 3599 - 20kHz占空比调节就是改TIM_SetCompare的 CCR 值范围 03600比例 0100%。这里有个新手容易踩的坑TIM_Prescaler设成 0 不代表没有分频72MHz / 1 / 3600 20kHz计算时要把三个值一起带进去验算。另外注意 STM32 的高级定时器 TIM1/TIM8 需要额外配置刹车和死区除非用到互补输出否则普通电机控制用 TIM2~TIM4 更省事。5. 联调排错从点灯到整车跑通的分步验证5.1 开发环境与工程初始化STM32 端工程建议用 STM32CubeMX 生成初始化代码省去手工配置时钟树和 GPIO 复用。生成后用 Keil MDK5 打开工程需要提前装好对应型号的器件支持包。第一次装完芯片包后新建工程时才能在器件列表里找到目标型号如果之前装过 C51 用的 Keil注意 MDK5 的安装目录和芯片包要选对避免出现找不到头文件的问题。CubeMX 里把串口波特率设为 115200、8 位数据、无校验、1 位停止位与 OpenMV 保持一致。GPIO 配置里重点确认电机方向引脚推挽输出、PWM 通道复用为定时器输出。5.2 分模块上电验证整车联调前我习惯按下面的顺序逐级验证跳过任何一步都可能让排错变得很痛苦步骤验证内容检查点失败排查方向1最小系统点灯LED 闪烁频率准确晶振起振、复位电路2电机开环转动左右轮分别正反转驱动芯片供电、IN 引脚电平3串口回环STM32 收到什么发回什么TX/RX 交叉连接、电平匹配4OpenMV 串口输出PC 串口助手看到FF 01 52 53波特率、USB 转串口模块5信号灯识别串口助手随红绿灯切换收到不同指令阈值、曝光、视场角6整车闭环红灯停车、绿灯通行线束、供电、共地第三步串口回环特别重要。先用一根杜邦线把 STM32 的 TX 和 RX 短接串口助手发什么收到什么证明单片机串口外设正常工作再连 OpenMV防止把摄像头模块的故障误判到 STM32 头上。如果用的是 USB 转 TTL 模块注意 RX 接 RX、TX 接 TX 的交叉关系不要两根线直连同名端。5.3 供电共地与串口乱码问题这是整套系统里最容易翻车的一环。OpenMV 摄像头启动瞬间电流很大如果和电机共用一个电源电机启动时电压跌落会导致摄像头重启表现为 OpenMV 的 LED 闪一下就灭串口完全无输出。正确做法是电机驱动单独用 7.4V 锂电池供电STM32 和 OpenMV 共用一路 5V 经过 LDO 输出的稳定电源两路电源的地线必须短接在一起。串口信号的电平参考地如果不一致即使波特率设对了也会出现乱码表现为串口助手能收到数据但全是0x00或0xFF的不规律组合。OpenMV 的 UART 是 3.3V 逻辑电平STM32F103 的串口引脚容忍 5V两者虽然可以直接相连但建议用万用表确认一下双方的地电位一致并且串口线上不要飞太长超过 20cm 就考虑降低波特率到 9600否则高速信号反射会造成偶发误码。5.4 识别不到红绿灯的排查顺序如果 OpenMV 画面里能看到红灯但find_blobs就是找不到目标先看两件事一是sensor.set_auto_whitebal(False)有没有执行白平衡不关的话暖色灯光下红色会偏白阈值空间被压缩二是曝光时间是否太长过曝会让红灯中心变成白色只有边缘一圈是红的。白平衡关掉后红灯的 A 通道值会明显上抬此时重新从 OpenMV IDE 的阈值编辑器里拷贝一份新阈值不要凭感觉手填。另外检查安装角度。摄像头如果装得太低信号灯在画面里的面积太小pxiel_threshold80可能把整个目标过滤掉。解决方法是把视觉模块支架抬高到距地面 2025cm让画面中上部留出足够空间给信号灯出现。6. 进阶玩法与离线验证技巧6.1 用指示灯先在桌面上验证完整信号链整车不上跑道、电机不转的情况下可以先做一套纯逻辑验证OpenMV 识别到红灯时让 STM32 板载 LED 快速闪烁绿灯时 LED 常亮无信号时 LED 慢闪。这个验证方式的好处是不依赖电机和底盘把视觉识别和协议解析这两个最容易出问题的环节先钉死。配套的逻辑可以写成一个独立函数之后替换成电机控制逻辑即可。void handle_signal_cmd(uint8_t cmd) { switch (cmd) { case R: GPIO_SetBits(LED_GPIO_PORT, LED_PIN); break; case G: GPIO_ResetBits(LED_GPIO_PORT, LED_PIN); break; case Y: // 黄灯翻转一次 LED模拟减速提示 GPIO_ToggleBits(LED_GPIO_PORT, LED_PIN); break; default: break; } }串口调试助手配合这套 LED 逻辑可以在不装车的情况下验证整条数据链路OpenMV 的画面里出现红灯PC 串口助手立刻收到FF 01 52 53STM32 板载 LED 亮起。任何一个环节没反应都能快速定位是识别问题、协议问题还是 GPIO 配置问题。6.2 编码器速度闭环把开环升级成稳定巡航如果竞赛现场的路面摩擦系数变化很大开环 PWM 会让小车在光滑地面跑得飞快、在粗糙地面爬不动。这时候给电机加上霍尔编码器STM32 定时器配置为编码器模式用 M 法测速每 20ms 读取一次计数增量除以时间得到速度值然后叠加一个增量式 PID 速度环。增量式 PID 的写法比位置式简洁输出的是 PWM 修正量天然适合嵌入式实现。参数整定上先只加 P让速度在目标值附近震荡时记录震荡周期再根据周期估算 I 和 D比盲目试参数快得多。这个升级对工创赛智能物流小车这类需要精确停车的场景同样适用——物流小车用灰度循迹而信号识别车用摄像头底层速度环完全可以复用。6.3 目标坐标的滑动平均与帧率瓶颈直接使用blob.cx()的原始值做闭环控制时画面抖动会让坐标在几个像素间跳动导致转向 PWM 高频振荡。加一个长度 5 的滑动窗口求平均值成本极低但转向平顺度提升非常明显。如果发现加了滤波之后转向明显变迟钝说明窗口开大了缩到 3 即可。帧率方面OpenMV 默认输出QVGA320×240但我们在初始化时用set_windowing把有效区域缩小到了 160×120实际处理的数据量只有原来的四分之一帧率可以稳定在 40fps 以上。如果还嫌慢把time.sleep(20)改成time.sleep(10)并保证曝光时间不超过 15ms。帧率低于 25fps 时小车的控制延迟会明显变大具体表现是转弯总是过冲——先检查曝光再检查窗口大小不要急着改 PID。本文还有配套的精品资源点击获取
返回列表