公司动态

智能小车系统设计:STM32与OpenMV在电赛中的工程实践与优化

📅 2026/8/5 10:43:05
智能小车系统设计:STM32与OpenMV在电赛中的工程实践与优化
1. 项目背景与核心挑战复盘2021年的全国大学生电子设计竞赛电赛已经过去几年了但F题“智能送药小车”至今仍是许多同学备赛、学习和复现的热门项目。我当年作为指导老师带着学生完整地走了一遍从方案设计、硬件搭建、软件调试到最终封箱的全过程期间踩过的坑、掉过的头发现在回想起来依然历历在目。这个题目之所以经典在于它综合考察了运动控制、视觉识别、机械结构与系统联调四大能力任何一个环节的短板都会在联调阶段被无限放大。网上能找到的源码和工程文件不少但大多只是“能用”缺乏对设计思路、参数调校逻辑和调试血泪史的深度剖析。今天我就以我们当年的方案为基础结合这几年指导新队伍的经验把整个项目的“里子”和“面子”都拆开来讲透并附上我们优化后的完整工程文件。希望这份总结能帮你不仅“抄”到代码更能理解背后的“为什么”在未来的项目中举一反三。F题的核心任务很明确一辆小车需要从病区起点出发在模拟医院走廊的赛道上循迹行进识别两侧病房门牌上的数字或二维码将对应的药品用不同颜色的物块模拟准确送达指定病房门口并完成声光提示。听起来像是“循迹小车OpenMV”的简单组合实际做起来你会发现从“车能动”到“稳定拿分”之间隔着一条东非大裂谷。关键挑战集中在三点第一复杂光照下的鲁棒循迹。赛场灯光、窗户自然光、其他队伍设备的补光交织在一起对灰度传感器或摄像头的阈值设定是巨大考验。第二快速准确的视觉识别与决策。小车在运动中进行识别图像模糊、光照变化、数字倾斜都会导致误判一旦送错药就是零分。第三多任务实时调度与稳定执行。小车需要同时处理电机PID控制、传感器数据采集、图像处理、逻辑判断和机械臂控制如果有如何让这些任务和谐共处不卡顿、不冲突是软件架构设计的核心。我们的方案主体基于STM32F4系列MCU作为主控搭配OpenMV作为视觉模块电机驱动采用经典的TB6612循迹方案则根据赛场环境测试后选择了抗干扰更强的“灰度传感器阵列”而非纯摄像头方案。下文我将分模块深入所有代码和工程文件都会在文末提供链接。2. 硬件系统设计与关键器件选型逻辑硬件是系统的骨架选型不当后期软件再怎么优化也事倍功半。我们的硬件架构可以概括为“一个大脑两只眼睛四条腿”。2.1 主控MCU为什么是STM32F407而不是F103或H7这是第一个需要权衡的点。F103价格低廉、资源够用是很多入门小车的选择。但对于F题我们面临的是需要处理来自多个串口OpenMV、可能的上位机调试、多个定时器电机PWM、编码器输入捕获、外部中断传感器触发的数据同时还要运行一套状态机逻辑。F103的CPU主频和内存特别是SRAM在任务繁重时容易捉襟见肘例如当图像数据通过串口发送稍快就可能堵塞其他任务。H7性能强大但成本高开发复杂度也更高对于有限备赛时间的学生来说性价比不高。STM32F407系列我们用的是F407ZGT6成为了甜点选择。主频168MHz192KB的SRAM足够应对多路数据吞吐。它拥有多个DMA控制器这是关键我们可以将编码器读数、串口通信、甚至ADC采样如果用到模拟灰度传感器都配置为DMA模式极大减轻CPU负担让CPU专注于决策逻辑。此外F4的浮点运算单元FPU是硬件级的在进行电机PID的浮点运算时速度远超F103的软件浮点模拟。因此在资源允许的情况下为电赛小车选择带FPU和充足SRAM的F4系列是为软件稳定性买的一份重要保险。2.2 视觉模块OpenMV的实战配置与避坑OpenMV Cam H7是当时的热门选择其集成的MicroPython环境和丰富的图像处理库大大降低了开发门槛。但直接套用例程在赛场上大概率会翻车。镜头与安装我们选择了焦距更长的镜头以便在更远的距离识别门牌给小车预留反应时间。安装位置必须绝对稳固且无振动。我们最初用扎带固定小车一加减速镜头就轻微抖动导致图像模糊。后来改用3D打印的刚性结构件配合螺丝锁死问题才解决。镜头光轴要与小车前进方向平行且高度需提前在赛道上测试确保数字在图像中的位置相对固定。光照补偿与阈值设定这是最大的坑。实验室光线均匀阈值调得很好。赛场灯光可能从顶部直射形成高光也可能有侧光造成阴影。我们的策略是不做固定的颜色阈值。对于识别色块药品我们采用image.find_blobs()函数但其阈值参数需要在赛场实际光照下校准。我们编写了一个简单的脚本让OpenMV在赛道上实时显示识别到的色块和其LAB颜色空间的阈值范围现场快速调整并保存多组参数备用。对于数字识别我们放弃了颜色识别改用灰度图像数字模板匹配。先通过image.binary()进行二值化但二值化阈值不能写死。我们采用了image.get_histogram()统计图像灰度直方图动态计算出一个阈值例如取波谷值以适应不同光照。然后使用image.find_template()匹配预先训练好的0-9数字模板。虽然速度比颜色识别慢一点但抗光照干扰能力显著增强。开补光灯在OpenMV模块上方加装一圈LED补光灯并串联一个电位器手动调节亮度。在赛场调试时根据环境光调节补光强度确保图像质量稳定。补光灯的电源必须独立于电机电源防止电机大电流拉低电压导致灯光闪烁。2.3 运动底盘与传感器布局电机与编码器我们使用了带AB相增量式编码器的直流减速电机。编码器精度线数不必盲目求高200-500线足以。关键在于安装要同心接线要屏蔽。编码器信号线最好用双绞线并远离电机驱动线防止PWM噪声干扰导致计数错误。我们曾因编码器线平行于电机电源线导致小车在高速时编码器计数疯狂跳变PID失控。循迹传感器我们放弃了纯OpenMV循迹因为其处理延时和赛道光线变化对颜色阈值影响太大。最终方案是**“灰度传感器阵列为主OpenMV辅助校验”**。使用了8路数字式灰度传感器TCRT5000排成一排。它的优势是响应速度快、受环境光影响相对较小通过电位器可调灵敏度。布局上传感器间距需要略小于赛道黑线的宽度确保任何时候至少有1-2个传感器压在黑线上。供电系统强烈建议分路供电。我们用了三块电池一块大容量2S锂电池7.4V单独给电机驱动供电。一块小容量2S电池给主控STM32、传感器、OpenMV、舵机等核心逻辑部件供电。OpenMV的补光灯单独由一块9V电池通过降压模块供电。 这样做彻底隔离了电机启停时的大电流冲击对控制电路的干扰系统稳定性飙升。电机电源和控制电源之间共地即可。3. 软件架构状态机与多任务协同软件的核心思想是“化整为零分而治之”。我们采用了基于定时器中断的裸机多任务框架配合一个清晰的有限状态机来调度全局流程。3.1 主循环与任务调度我们没有使用RTOS因为对于这个确定性的小车任务一个精心设计的裸机调度器更轻量、可控。// 伪代码示意 int main(void) { // 硬件初始化 All_Init(); // 主循环 while(1) { // 任务110ms执行一次处理电机PID计算与控制 if(task_10ms_flag) { task_10ms_flag 0; Motor_PID_Calculate(); Motor_Output(); } // 任务250ms执行一次处理传感器数据采集与滤波 if(task_50ms_flag) { task_50ms_flag 0; Trace_Sensor_Update(); // 读取灰度传感器 Encoder_Update(); // 更新编码器值 } // 任务3100ms执行一次处理视觉通信与状态机 if(task_100ms_flag) { task_100ms_flag 0; OpenMV_Data_Receive(); // 解析串口数据 FSM_Handler(); // 状态机处理 } // 其他低优先级或事件驱动任务 // ... } }定时器中断负责设置这些task_xxms_flag标志位。这种方式的优点是任务执行周期稳定不会因为某个任务耗时过长而阻塞其他任务除非它超过了自己的周期。所有耗时操作如等待串口数据、复杂的图像处理都放在OpenMV端STM32只做快速的决策和控制。3.2 核心状态机设计小车的整个流程用一个状态机来管理状态清晰调试方便。typedef enum { STATE_INIT, // 初始化 STATE_STANDBY, // 等待启动 STATE_TRACING, // 循迹模式 STATE_IDENTIFYING, // 识别门牌减速 STATE_DECISION, // 决策是否送药 STATE_DELIVERING, // 执行送药动作转弯、伸机械臂等 STATE_RETURNING, // 返回赛道或继续前进 STATE_ERROR // 错误处理 } SystemState_t;FSM_Handler()函数根据当前状态、传感器输入和视觉信息决定状态迁移。例如在STATE_TRACING状态下如果灰度传感器检测到特定图案如十字路口并且OpenMV识别到了门牌数字则迁移到STATE_IDENTIFYING小车减速。在STATE_IDENTIFYING下等待OpenMV发送稳定的识别结果然后与任务单比对决定进入STATE_DELIVERING送药还是STATE_RETURNING忽略继续前进。3.3 循迹控制算法不仅仅是PID灰度传感器阵列读回的是一个8位的二进制序列如0b00111000表示黑线在传感器下的位置。最简单的PD控制算法是根据这个位置偏差来计算转向。// 简单PD循迹示例 int Calculate_Steering(uint8_t sensor_value) { int position Map_Sensor_to_Position(sensor_value); // 将传感器值映射为-100到100的位置 int error position - setpoint; // setpoint通常为0表示居中 static int last_error 0; int steering Kp * error Kd * (error - last_error); last_error error; return steering; // 正值右转负值左转 }但实际赛道有直道、弯道、十字路口。我们做了优化动态PID参数在误差绝对值大时比如冲出赛道采用更大的Kp和Kd快速拉回在误差小时采用更小的参数平滑运行防止震荡。这可以通过查表或简单的分段函数实现。十字路口处理当传感器检测到类似0b11111111全白可能已出赛道或0b00011000中间黑两边白可能是十字路口中心的特殊模式时状态机会介入。此时小车会短暂进入一个“路口处理子状态”根据任务要求执行直行、左转或右转。关键在于转弯动作完成后必须有一个“回正并对齐黑线”的步骤否则很容易在下个弯道跑偏。我们采用的方法是转弯完成后进入一个缓慢前进并强力纠偏的模式直到传感器重新检测到稳定的黑线位置。速度规划直道加速入弯前减速识别门牌时大幅减速。我们根据当前误差和前方路径预判如果知道赛道地图来动态设定小车的目标速度。这能显著提升整体用时和稳定性。4. 视觉识别与通信协议设计OpenMV与STM32通过串口通信设计一个可靠的协议至关重要。4.1 通信协议格式我们自定义了简单的帧结构包含帧头、数据类型、数据长度、数据内容、校验和。 例如对于数字识别结果[0xAA, 0xBB, 0x01, 0x01, 0x09, 0xCC]解释帧头0xAA 0xBB数据类型0x01数字识别数据长度0x011个字节数据0x09数字9校验和0xCC。STM32端使用一个状态机解析串口数据防止因数据错位或干扰导致解析错误。4.2 OpenMV端脚本要点# OpenMV 主循环部分伪代码 import sensor, image, time, pyb from pyb import UART # 初始化... uart UART(3, 115200) # 串口3波特率115200 while(True): img sensor.snapshot() # 1. 循迹辅助如果使用 # ... 分析图像计算偏差通过串口发送 # 2. 检测是否到达识别区域例如通过检测特定颜色或形状的标记 # 如果到达则进入高精度识别模式 if need_identify: # 动态二值化 hist img.get_histogram() threshold hist.get_threshold().value() # 获取一个建议阈值 img.binary([(threshold-20, threshold20)]) # 根据阈值做二值化 # 模板匹配找数字 for n in range(10): r img.find_template(template_list[n], 0.7) # 0.7是相似度阈值 if r: uart.write(protocol_pack(numbern)) # 打包发送 break # 找到一个就跳出 time.sleep_ms(50) # 控制循环频率4.3 识别策略的容错设计多次识别取众数OpenMV在识别阶段连续识别5次将结果如数字发回STM32。STM32统计出现次数最多的结果作为最终结果避免单次误判。超时与默认行为设定一个识别超时时间如2秒。如果超时仍未收到稳定结果STM32根据预设策略决定是放弃该任务继续前进还是尝试第二次识别例如稍微移动小车位置。数据校验与心跳包除了每帧数据有校验和OpenMV还会每隔一秒发送一个心跳包0xFF。STM32如果超过3秒收不到心跳则认为视觉模块异常进入安全模式停车或低速循迹返回。5. 调试心得与赛场应急策略调试是电赛的精髓也是崩溃的主要来源。5.1 分模块调试逐级联调底盘单独调不装任何传感器先让小车能通过遥控或按键前后左右走直线。测试电机驱动、编码器读数是否正常。循迹单独调装上灰度传感器在简单直道和弯道上调试PID参数直到能稳定循迹。记录下不同速度下的最优PID参数。视觉单独调将OpenMV固定在三脚架上模拟小车运动轨迹测试在不同光照、不同距离下识别数字和色块的准确率。保存多组环境参数。联调将两者结合。先让小车纯循迹在特定点如模拟病房触发视觉识别。这里最大的问题是时序小车到达识别点时速度是否足够慢OpenMV是否已经准备好并完成了对焦我们的解决方案是在距离识别点前约20cm处设置一个“预识别点”比如赛道上的一个白色横条用灰度传感器检测。检测到横条后小车立即减速到爬行速度同时通过串口发送指令唤醒OpenMV进入高精度识别模式。5.2 参数保存与现场校准所有关键参数PID参数、灰度传感器阈值、视觉识别阈值、速度规划表都不应写死在代码里。我们将其保存在STM32的Flash中并编写了一个简单的上位机用Python的Tkinter或Qt快速写一个通过串口进行读写。这样在赛场调试时可以快速调整参数并保存无需重新烧录程序。5.3 赛场应急清单电池电量准备多组充满电的电池并标记好。比赛前每块电池都用在赛道上全速跑一次记录续航时间。备用小车如果条件允许核心模块主控、驱动、传感器准备两套。一套作为主力一套作为“器官捐献者”。干扰应对赛场Wi-Fi、对讲机、其他队伍的电机都可能造成干扰。准备锡箔纸或铜箔关键信号线可以做临时屏蔽。编码器、串口通信的波特率不要设在常见值如9600可以设为115200或921600减少被误触发的概率。软件看门狗务必开启STM32的独立看门狗IWDG和窗口看门狗WWDG。在关键任务循环中定期“喂狗”。这样即使程序跑飞也能自动复位而不是瘫在赛道上。一键初始化在代码中设置一个秘密按键组合比如长按两个键按下后强制小车回到初始状态停止所有电机、复位状态机。用于处理赛场上可能出现的死锁状态。6. 源码与工程文件结构说明随文提供的工程文件基于STM32CubeMX生成使用HAL库开发IDE为Keil MDK。工程结构清晰模块化程度高方便大家理解和移植。F103_F4_Smart_Car_2021/ ├── Core/ │ ├── Inc/ // 主要头文件 │ │ ├── bsp_motor.h // 电机驱动 │ │ ├── bsp_encoder.h // 编码器 │ │ ├── bsp_trace.h // 循迹传感器 │ │ ├── bsp_uart.h // 串口通信与OpenMV │ │ ├── pid.h // PID算法库 │ │ ├── fsm.h // 状态机头文件 │ │ └── task_scheduler.h // 任务调度器 │ └── Src/ // 主要源文件对应上述头文件 ├── Drivers/ // STM32 HAL库 ├── OpenMV_Scripts/ // OpenMV端Python脚本 │ ├── main_find_number.py // 数字识别主脚本 │ ├── main_trace_assist.py // 循迹辅助脚本可选 │ └── templates/ // 存放0-9的数字模板图片 ├── Documentation/ // 一些说明文档 │ ├── 硬件接线图.pdf │ └── 参数配置指南.md └── README.md // 工程总说明包含如何编译、下载、配置关键代码片段导读pid.c实现了增量式和位置式两种PID并做了积分限幅和输出限幅防止积分饱和和电机过冲。fsm.c状态机的实现。每个状态都是一个函数函数内部判断条件并返回下一个状态号。逻辑清晰易于调试。task_scheduler.c利用基本定时器如TIM6产生1ms中断在中断里维护多个软件定时器用于设置task_xxms_flag。bsp_uart.c包含了一个完整的串口协议解析器使用状态机方式解析来自OpenMV的数据包稳定可靠。注意提供的源码是经过整理和脱敏的“教学版”去除了我们队伍特定的调试信息和一些过于复杂的优化。它能够完整实现F题的基本要求并包含了上述提到的大部分稳定性和容错设计。你可以以此为基础根据自己小车的硬件差异如传感器间距、轮距、电机减速比调整参数并添加更高级的功能如使用陀螺仪进行更精确的转角控制。最后想说的是电赛小车项目是一个典型的系统工程它考验的不仅仅是编程或焊板子的能力更是系统思维、调试耐心和团队协作。硬件上的一个小疏漏可能需要软件写几百行代码来弥补软件逻辑的一个小瑕疵可能在测试一百次中只出现一次但在赛场上就是百分之百。这份总结和源码希望能为你铺平一些道路但真正的成长来自于你亲手解决每一个报错、调参调到凌晨、以及小车终于稳稳跑完全程那一刻的喜悦。工程文件的下载链接我会放在评论区如果大家在复现过程中遇到任何问题也欢迎留言讨论。