STM32与OpenMV融合的智能小车设计:从PID控制到视觉识别实战
2026/8/29 2:09:24 网站建设 项目流程

1. 项目概述:从“智能送药小车”到国赛一等奖的完整路径

看到“智能送药小车”这个题目,很多参加过电子设计竞赛的朋友应该会心一笑。这几乎是电赛里一个经典到不能再经典的赛题方向,每年都能看到它的身影,但每年也都能涌现出让人眼前一亮的创新方案。我们团队(不眠者①队)在国电-F题中,正是凭借对这个“老题目”的深度挖掘和扎实实现,最终拿下了广东赛区一等奖,并成功推向了国赛。现在,我们决定将整个项目的代码和设计方案完全开源,希望能给后来者提供一个扎实的参考,也希望能和大家一起探讨如何把“经典”做出“新意”。

这个项目的核心,远不止是让一个小车从A点跑到B点那么简单。它模拟的是一个真实的医院或养老院场景下的药品配送任务:小车需要自主识别病房编号,准确接收药品指令,规划路径避开障碍,最终将药品送达指定病房门口。整个过程要求全自主运行,对系统的环境感知、决策控制、执行精度和稳定性提出了极高的要求。我们选择了以STM32作为主控大脑,搭配OpenMV进行视觉识别,构建了一套稳定、高效且易于调试的解决方案。开源的目的,是希望将我们在备赛、调试、比赛中踩过的坑、总结的经验,以及那些在技术报告里往往一笔带过但至关重要的“软细节”,完整地呈现出来。

2. 核心需求解析与系统顶层设计

2.1 赛题要求与功能拆解

拿到赛题后,第一步不是急着画电路图或写代码,而是把厚厚的赛题文档“嚼碎”。国电-F题“智能送药小车”通常包含以下几个核心考核点:

  1. 病房识别与定位:小车需要识别贴在病房门上的特定标识(如数字、二维码或颜色标签),并准确停靠在门前。
  2. 路径规划与循迹:场地内会有引导线(可能是黑线、电磁线或二维码路标),小车需沿指定路径行驶,并能处理十字路口、弯道等。
  3. 障碍物检测与避障:路径上会随机出现静态或动态障碍物,小车需能检测并安全绕行。
  4. 药品信息接收与确认:通过无线通信(如蓝牙、Wi-Fi)或上位机下发送药任务(病房号、药品信息),小车需能接收并显示。
  5. 执行精度与稳定性:在规定时间内完成多次送药任务,要求停靠位置准、任务成功率高、系统运行稳定不宕机。

基于这些点,我们决定采用“视觉主导,多传感器融合”的架构。视觉(OpenMV)负责最核心的病房识别和部分循迹,超声波/红外传感器负责避障,编码器负责里程计和速度闭环,STM32作为总调度中心。

2.2 硬件系统架构设计

硬件选型是项目的地基,选对了事半功倍。

  • 主控制器:STM32F407ZGT6。选择它基于几点考量:充足的IO口和定时器资源以驱动电机、伺服舵机并读取多个传感器;强大的计算性能(Cortex-M4内核,带FPU)足以处理复杂的控制算法和传感器数据融合;丰富的通信接口(多个UART、SPI、I2C)便于连接各模块。相比F1系列,F4在浮点运算和内存上的优势,在处理OpenMV传回的图像坐标数据时感受尤为明显。
  • 视觉模块:OpenMV Cam H7。这是项目的“眼睛”。为什么不用更便宜的OV7670摄像头+单片机自己处理?因为电赛时间紧、任务重,OpenMV集成了MicroPython环境和强大的机器视觉库,可以快速实现颜色识别、二维码识别、AprilTag识别等,大大降低了开发门槛和调试周期。我们将它用于识别彩色病房门牌和地面引导线。
  • 运动执行单元
    • 电机驱动:采用TB6612FNG双路电机驱动芯片。它比传统的L298N效率高、发热小,驱动我们的N20减速电机(带编码器)绰绰有余。
    • 舵机:用于控制一个简单的机械臂或拨杆,模拟“递送药品”的动作。选用SG90舵机,通过STM32的定时器产生PWM信号控制。
  • 感知与避障单元
    • 超声波传感器(HC-SR04):布置在小车前方和侧方,用于检测中远距离的障碍物。它的缺点是刷新率较低且易受软质物体干扰,但成本低、接口简单。
    • 红外避障传感器(TCRT5000):布置在小车底盘边缘,用于检测悬崖(桌边)或近距离的障碍物,反应速度快。
  • 电源管理:这是极其重要却常被忽视的一环。我们使用了多个稳压模块:一个12V转5V/3A的DCDC模块给电机驱动和舵机供电;另一个干净的12V转5V/1A的LDO模块给STM32、OpenMV和传感器供电。电机电源和控制电源必须隔离,否则电机启停时产生的电流冲击会导致单片机复位或传感器读数异常。我们还在电源入口处加入了大的电解电容进行缓冲。

注意:电赛现场提供的电源可能不稳定,务必在自己的电源设计上留足余量,并做好滤波。我们曾因电源问题在调试时莫名其妙死机,排查了很久。

3. 核心模块实现与算法剖析

3.1 OpenMV视觉识别策略

OpenMV的编程思路和单片机略有不同,它更像是写一个在摄像头模块上运行的Python脚本。

  1. 病房门牌识别:我们采用颜色+形状的复合识别。首先,病房门牌使用特定颜色的矩形色块(比如蓝色)。OpenMV脚本通过find_blobs()函数寻找画面中所有蓝色色块,然后根据色块的宽度、高度、宽高比等参数过滤出可能是门牌的矩形区域。为了增加抗干扰能力,我们还在门牌中心贴了一个小的黑色圆形作为定位点,通过find_circles()进行二次确认。

    # OpenMV 示例代码片段 (病房门牌识别) import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time = 2000) blue_threshold = (30, 70, -20, 10, -30, 10) # 根据现场灯光调整LAB阈值 while(True): img = sensor.snapshot() blobs = img.find_blobs([blue_threshold], area_threshold=200, pixel_threshold=200) for b in blobs: # 过滤:宽高比接近矩形,面积在一定范围内 if 0.8 < b.w()/b.h() < 1.2 and 2000 < b.area() < 8000: img.draw_rectangle(b.rect(), color=(255,0,0)) # 在色块中心找黑色圆点 roi = (b.cx()-10, b.cy()-10, 20, 20) circles = img.find_circles(roi=roi, threshold=2500) if circles: # 确认找到门牌,计算中心坐标并发送给STM32 # ... 通过UART发送坐标数据

    调试心得:现场光线是视觉项目的头号杀手。我们准备了可调亮度的补光灯,并在赛前花了大量时间调整颜色阈值。一个技巧是:编写一个阈值调试工具,能在OpenMV IDE里实时查看识别效果并调整LAB阈值,将最佳阈值保存下来固化到代码中。

  2. 循迹策略:我们采用“双线巡迹”方案。在赛道两侧贴有黑色引导线。OpenMV识别这两条线,并计算两条线中点的位置。将这个中点与图像中心点的偏差,作为方向控制的误差量发送给STM32。这种方法比单线巡迹更稳定,尤其在弯道时能提供更早的转向预判。

    # 双线巡迹示例 black_threshold = (0, 50, -30, 30, -30, 30) # 黑色阈值 blobs = img.find_blobs([black_threshold], area_threshold=100, merge=True) # 区分左右线:根据blob的x坐标位置 left_line, right_line = None, None for b in blobs: if b.cx() < img.width()//2: left_line = b else: right_line = b if left_line and right_line: center_x = (left_line.cx() + right_line.cx()) // 2 error = center_x - img.width()//2 # 发送error给STM32

3.2 STM32控制核心:多任务调度与通信

STM32的程序我们使用HAL库开发,结构清晰。核心是建立一个高效的多任务调度系统。

  1. 定时器中断作为系统心跳:我们配置了一个1ms的定时器中断。在这个中断里,不进行复杂运算,只执行以下操作:
    • 更新系统时间戳。
    • 置位各个任务(如电机控制、传感器读取、通信处理)的“允许执行”标志位。这是一种简单的时间片轮询调度思想。
  2. 主循环中的任务执行:在主while(1)循环中,不断检查各任务的标志位。如果标志位被置起,则执行相应的任务函数,执行完毕后清除标志位。例如:
    // 伪代码示例 volatile uint8_t flag_10ms = 0; // 10ms任务标志 volatile uint8_t flag_50ms = 0; // 50ms任务标志 // 在1ms定时器中断中 static uint16_t cnt_10ms = 0, cnt_50ms = 0; if(++cnt_10ms >= 10) { flag_10ms = 1; cnt_10ms = 0; } if(++cnt_50ms >= 50) { flag_50ms = 1; cnt_50ms = 0; } // 在主循环中 while(1) { if(flag_10ms) { flag_10ms = 0; Task_MotorControl(); // 电机PID控制 Task_ReadEncoder(); // 读取编码器 } if(flag_50ms) { flag_50ms = 0; Task_ReadUltrasonic(); // 读取超声波 Task_CheckUART(); // 处理OpenMV发来的数据 } // ... 其他即时性任务,如按键扫描 }
  3. 与OpenMV的通信协议:我们定义了一个简单的串口通信协议。OpenMV周期性地(如每50ms)发送一帧数据,包含帧头、数据长度、病房识别状态/坐标误差、校验和等。STM32端接收到后进行校验,解析出有效数据用于控制。
    • 帧格式0xAA 0x55 [数据长度] [数据1] ... [数据N] [校验和]
    • 校验和:通常采用前面所有字节的累加和取低8位,或CRC8,确保数据传输的可靠性。

3.3 运动控制算法:PID与状态机

小车的平稳运动是得分的关键。

  1. 差速控制模型:我们的小车是两轮差速驱动。运动学模型很简单:线速度 V = (V_left + V_right) / 2角速度 ω = (V_right - V_left) / L(L为轮距)。通过控制左右轮的速度,就能实现前进、后退、转弯和自转。
  2. 双闭环PID控制
    • 速度环(内环):通过编码器反馈的脉冲频率计算电机实时转速,与目标转速比较,通过PID运算输出PWM占空比。这保证了即使负载变化(如上坡),电机也能保持设定速度。
    • 位置环/方向环(外环):对于循迹,外环的输入是OpenMV计算出的路径偏差(error),输出是目标角速度或左右轮的速度差。对于定点停车,外环的输入是编码器累积的行程与目标距离的偏差。
    // 简化版PID结构体与计算函数 typedef struct { float Kp, Ki, Kd; float integral; float prev_error; } PID_Controller; float PID_Calculate(PID_Controller *pid, float error, float dt) { pid->integral += error * dt; float derivative = (error - pid->prev_error) / dt; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; pid->prev_error = error; // 输出限幅 if(output > MAX_OUTPUT) output = MAX_OUTPUT; if(output < -MAX_OUTPUT) output = -MAX_OUTPUT; return output; }
    调参经验:先调P,让系统快速响应但不要震荡;再调D,抑制超调和震荡;最后调I,消除静态误差。在电赛现场,由于地面摩擦、电池电压等变化,赛前调好的参数可能需要微调。我们准备了蓝牙模块,可以通过手机APP实时调整PID参数并观察效果,这节省了大量时间
  3. 任务状态机设计:小车的送药流程是一个典型的状态机。我们设计了如下状态:IDLE->RECV_TASK(接收任务)->NAV_TO_ROOM(导航至病房)->AVOID_OBSTACLE(如遇障碍)->ARRIVE_CHECK(到达检查)->DELIVERY(执行递送)->RETURN(返回充电区/待命区)。 STM32中用一个全局变量g_car_state记录当前状态,每个状态有对应的处理函数和状态转移条件。这种设计逻辑清晰,易于调试和扩展。

4. 系统集成、调试与现场实战

4.1 模块联调与系统标定

当各个模块单独测试都OK后,集成才是真正的挑战。

  1. 电源与地噪声排查:所有模块共地,但电机驱动部分的大电流地线要粗,并单点连接到电源地。用示波器观察单片机电源引脚,在电机启停时是否有大的毛刺。我们通过增加电源滤波电容和采用磁珠隔离,解决了复位问题。
  2. 传感器数据融合与滤波:超声波传感器数据跳动大,我们采用中值滤波结合一阶低通滤波。例如,连续采样5次,去掉最大最小值,再取平均,最后进行低通滤波,得到稳定可靠的距离值。
  3. 视觉与控制器的延时校准:OpenMV处理图像、STM32接收串口数据都存在毫秒级的延时。这会导致小车根据“过去”的位置信息做出控制反应,可能引发振荡。我们在代码中加入了简单的预测补偿:根据小车当前速度,估算出延时期间小车移动的距离,在计算误差时将其补偿回去。
  4. 机械结构优化:重心要低,轮子要正。我们最初的车体较高,快速转弯时容易晃动甚至侧翻。后来降低了电池和主板的位置,并仔细调整了轮子与电机的连接,确保同轴,减少了不必要的机械抖动对编码器读数的影响。

4.2 现场部署与适应性调整

电赛现场环境多变,必须做好预案。

  1. 光线应对:我们准备了小型LED补光灯,并准备了多套针对强光、弱光、侧光的颜色阈值参数,赛前根据现场光线快速切换。
  2. 赛道适应性:赛道的摩擦系数、平整度可能和实验室不同。我们准备了不同硬度的轮胎(海绵轮、橡胶轮),并现场微调了PID参数。一个关键技巧:在赛前练习时,记录下小车在标准速度下跑完一圈的编码器计数,作为一个“标准距离”。现场如果发现同样编码器计数下跑的距离有偏差,可以微调编码器计数与实际距离的换算系数。
  3. 通信抗干扰:比赛现场无线环境复杂。我们除了在通信协议中加入校验,还设置了应答机制。对于重要的任务指令(如病房号),上位机发送后,小车必须回复确认,否则上位机会重发。
  4. 调试接口预留:我们在小车上预留了一个蓝牙模块接口,并通过一个拨码开关控制其是否启用。在调试阶段,打开蓝牙,可以用手机实时查看小车传感器数据、状态和修改参数。比赛时,关闭拨码开关,确保系统纯净。

4.3 常见问题排查速查表

在开发和比赛过程中,我们遇到了无数问题。下面这个表格总结了一些典型问题及排查思路:

问题现象可能原因排查步骤与解决方案
小车启动后原地抖动或画圈1. 左右电机线接反。
2. 编码器A/B相序接反。
3. PID参数中P值过大产生振荡。
1. 交换任意一个电机的两根线。
2. 交换编码器的A、B相接线。
3. 逐步减小P值,观察现象。
OpenMV识别时有时无1. 光线变化导致阈值不匹配。
2. 电源供电不足,摄像头重启。
3. 镜头焦距未调好,图像模糊。
1. 使用补光灯,重新校准阈值。
2. 检查OpenMV供电电压电流,确保使用独立LDO供电。
3. 手动调节镜头,直到图像清晰。
串口通信收不到数据或乱码1. 波特率设置不一致。
2. TX/RX线接反。
3. 共地问题。
4. 程序未及时读取接收缓冲区导致溢出。
1. 确认STM32与OpenMV波特率完全相同。
2. 交叉连接TX和RX。
3. 确保两者有共同的GND连接。
4. 在中断或主循环中提高数据读取频率。
超声波测距值跳变巨大1. 传感器前方有干扰物(如其他队的小车)。
2. 测量周期太短,上次回波未结束就触发下一次。
3. 电源噪声。
1. 增加软件滤波(中值+低通)。
2. 拉长两次测量的间隔(如100ms以上)。
3. 在传感器VCC和GND间并联10uF和0.1uF电容。
小车在直道上跑偏1. 左右轮实际直径/摩擦力有细微差异。
2. 编码器计数不准。
3. 车体装配不对称。
1. 在速度环PID中为左右轮设置微小的速度补偿系数。
2. 检查编码器接线和计数代码,确保正反转计数都正确。
3. 重新调整车体结构,确保对称。
任务执行到一半程序跑飞1. 栈溢出或堆溢出。
2. 中断服务程序执行时间过长。
3. 内存访问越界(数组溢出)。
1. 增大启动文件中的栈堆大小。
2. 中断里只做标记,复杂操作放到主循环。
3. 使用编译器的数组边界检查功能,或仔细审查代码。

5. 开源仓库使用指南与项目演进思考

我们的所有代码、原理图、设计报告都已开源在GitHub上。仓库结构清晰,包含了STM32的Keil工程、OpenMV的Python脚本、硬件原理图(PDF和源文件)、以及详细的说明文档。

给使用者的建议

  1. 不要直接复制粘贴:我们的代码和方案是针对特定赛题、特定硬件和特定场景优化的。请务必理解其背后的逻辑,然后根据你自己的硬件平台(可能电机不同、传感器型号不同)和具体任务进行调整。
  2. 重点关注框架:比起具体的PID参数值,更值得学习的是我们如何组织代码(多任务调度、状态机)、如何设计通信协议、如何进行传感器数据融合。这些框架性的东西具有通用性。
  3. 硬件是基础:再好的算法也跑在一个糟糕的硬件平台上。请花时间打磨你的小车底盘,确保其机械结构的稳固性和电路连接的可靠性。一个晃动的小车会让所有精密的控制算法失去意义。

关于项目演进的思考: 这个项目获得一等奖,但远非终点。赛后我们复盘,认为还可以从以下几个方向深化:

  • 更高级的路径规划:目前是简单的巡线,可以尝试集成红外光电对管阵列实现“离线”路径记忆,或者探索基于摄像头+AprilTag的全局定位与SLAM(同步定位与地图构建)雏形。
  • 更强的环境感知:可以尝试使用TOF(飞行时间)激光雷达替代超声波,获得更精确、快速的360度环境信息。
  • 更智能的决策:引入更复杂的行为树或轻量级AI模型(如TinyML),让小车能处理更复杂的突发情况,比如多动态障碍物、临时交通管制(模拟)等。
  • 无线通信与组网:如果赛题允许,可以设计多车协同送药系统,通过ZigBee或Wi-Fi Mesh网络通信,由一台主机调度多台小车,这将是质的飞跃。

开源这个项目,是希望将我们“不眠之夜”的成果转化为社区共同进步的阶梯。电赛的魅力不仅在于结果,更在于那个从无到有、不断调试、最终看到小车完美执行任务的过程。希望我们的代码和文档,能帮你少走一些我们走过的弯路,更高效地享受这个创造的过程。如果你在使用中发现了问题,或者有更好的想法,非常欢迎在GitHub上提交Issue或Pull Request。技术之路,开源共行。

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

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

立即咨询