STM32智能小车交通信号识别:OpenMV视觉与串口控制实战
2026/9/15 14:59:27 网站建设 项目流程

简介:这套基于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 型号,最常见的是 STM32F103C8T6,48 引脚,性价比高;如果考虑到后续要同时挂编码器、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(time=2000) # 关闭自动增益和白平衡,否则阈值会随环境漂移 sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) # 串口 3:TX=P4(TX), RX=P5(RX),波特率 115200 uart = UART(3, 115200, timeout_char=1000) # 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_threshold=80, area_threshold=80, merge=True) 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_threshold=80表示色块像素总数少于 80 的候选目标直接剔除,用于过滤图像噪点;merge=True会把相邻的同一颜色小色块合并成一个大色块,防止信号灯因为内部反光被拆成好几块。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.8KD = 0.15,然后根据现场反应调整:

参数初始值现象调整方向
KP0.8转弯太慢,冲出弯道每次加 0.2
KP0.8左右剧烈摆动每次减 0.1
KD0.15出弯回正慢每次加 0.05
KD0.15转向抖动、电机发烫每次减 0.05
BASE_PWM60%直行太慢/太快依场地长度调整

CLAMP宏把左右 PWM 限制在MIN_PWMMAX_PWM之间,MIN_PWM一般取整周期的 15%。低于这个值直流电机克服不了静摩擦力,会发出嗡嗡声但不转,PWM 容易掉进死区。与其让 PID 在小值区间反复震荡,不如直接钳制到能转的最低值附近。

4.3 PWM 频率与电机驱动细节

PWM 频率对直流电机的控制体验影响很大。低于 5kHz 时电机会发出可闻的啸叫声,而且电流纹波变大,驱动芯片发热加剧;高于 50kHz 时 MOSFET 开关损耗上升,驱动芯片反而更容易烫手。我自己常用的配置是 20kHz,听不到声音,TB6612 的温度也比较正常。

初始化时注意 STM32 定时器的预分频和自动重载值配合:

// 假设定时器时钟 72MHz,目标 PWM 20kHz // PWM 频率 = 72MHz / (PSC+1) / (ARR+1) TIM_TimeBaseInitTypeDef.TIM_Prescaler = 0; // PSC = 0 TIM_TimeBaseInitTypeDef.TIM_Period = 3599; // ARR = 3599 -> 20kHz

占空比调节就是改TIM_SetCompare的 CCR 值,范围 0~3600,比例 0~100%。这里有个新手容易踩的坑: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 输出的稳定电源,两路电源的地线必须短接在一起。串口信号的电平参考地如果不一致,即使波特率设对了也会出现乱码,表现为串口助手能收到数据但全是0x000xFF的不规律组合。

OpenMV 的 UART 是 3.3V 逻辑电平,STM32F103 的串口引脚容忍 5V,两者虽然可以直接相连,但建议用万用表确认一下双方的地电位一致,并且串口线上不要飞太长,超过 20cm 就考虑降低波特率到 9600,否则高速信号反射会造成偶发误码。

5.4 识别不到红绿灯的排查顺序

如果 OpenMV 画面里能看到红灯,但find_blobs就是找不到目标,先看两件事:一是sensor.set_auto_whitebal(False)有没有执行,白平衡不关的话,暖色灯光下红色会偏白,阈值空间被压缩;二是曝光时间是否太长,过曝会让红灯中心变成白色,只有边缘一圈是红的。白平衡关掉后,红灯的 A 通道值会明显上抬,此时重新从 OpenMV IDE 的阈值编辑器里拷贝一份新阈值,不要凭感觉手填。

另外检查安装角度。摄像头如果装得太低,信号灯在画面里的面积太小,pxiel_threshold=80可能把整个目标过滤掉。解决方法是把视觉模块支架抬高到距地面 20~25cm,让画面中上部留出足够空间给信号灯出现。

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 53,STM32 板载 LED 亮起。任何一个环节没反应,都能快速定位是识别问题、协议问题还是 GPIO 配置问题。

6.2 编码器速度闭环:把开环升级成稳定巡航

如果竞赛现场的路面摩擦系数变化很大,开环 PWM 会让小车在光滑地面跑得飞快、在粗糙地面爬不动。这时候给电机加上霍尔编码器,STM32 定时器配置为编码器模式,用 M 法测速:每 20ms 读取一次计数增量,除以时间得到速度值,然后叠加一个增量式 PID 速度环。

增量式 PID 的写法比位置式简洁,输出的是 PWM 修正量,天然适合嵌入式实现。参数整定上,先只加 P,让速度在目标值附近震荡时记录震荡周期,再根据周期估算 I 和 D,比盲目试参数快得多。这个升级对工创赛智能物流小车这类需要精确停车的场景同样适用——物流小车用灰度循迹,而信号识别车用摄像头,底层速度环完全可以复用。

6.3 目标坐标的滑动平均与帧率瓶颈

直接使用blob.cx()的原始值做闭环控制时,画面抖动会让坐标在几个像素间跳动,导致转向 PWM 高频振荡。加一个长度 5 的滑动窗口求平均值,成本极低,但转向平顺度提升非常明显。如果发现加了滤波之后转向明显变迟钝,说明窗口开大了,缩到 3 即可。

帧率方面,OpenMV 默认输出QVGA(320×240),但我们在初始化时用set_windowing把有效区域缩小到了 160×120,实际处理的数据量只有原来的四分之一,帧率可以稳定在 40fps 以上。如果还嫌慢,把time.sleep(20)改成time.sleep(10)并保证曝光时间不超过 15ms。帧率低于 25fps 时,小车的控制延迟会明显变大,具体表现是转弯总是过冲——先检查曝光,再检查窗口大小,不要急着改 PID。

本文还有配套的精品资源,点击获取

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

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

立即咨询