
1. 项目概述从“智能送药小车”到国赛一等奖的完整路径看到“智能送药小车”这个题目很多参加过电子设计竞赛的朋友应该会心一笑。这几乎是电赛里一个经典到不能再经典的赛题方向每年都能看到它的身影但每年也都能涌现出让人眼前一亮的创新方案。我们团队不眠者①队在国电-F题中正是凭借对这个“老题目”的深度挖掘和扎实实现最终拿下了广东赛区一等奖并成功推向了国赛。现在我们决定将整个项目的代码和设计方案完全开源希望能给后来者提供一个扎实的参考也希望能和大家一起探讨如何把“经典”做出“新意”。这个项目的核心远不止是让一个小车从A点跑到B点那么简单。它模拟的是一个真实的医院或养老院场景下的药品配送任务小车需要自主识别病房编号准确接收药品指令规划路径避开障碍最终将药品送达指定病房门口。整个过程要求全自主运行对系统的环境感知、决策控制、执行精度和稳定性提出了极高的要求。我们选择了以STM32作为主控大脑搭配OpenMV进行视觉识别构建了一套稳定、高效且易于调试的解决方案。开源的目的是希望将我们在备赛、调试、比赛中踩过的坑、总结的经验以及那些在技术报告里往往一笔带过但至关重要的“软细节”完整地呈现出来。2. 核心需求解析与系统顶层设计2.1 赛题要求与功能拆解拿到赛题后第一步不是急着画电路图或写代码而是把厚厚的赛题文档“嚼碎”。国电-F题“智能送药小车”通常包含以下几个核心考核点病房识别与定位小车需要识别贴在病房门上的特定标识如数字、二维码或颜色标签并准确停靠在门前。路径规划与循迹场地内会有引导线可能是黑线、电磁线或二维码路标小车需沿指定路径行驶并能处理十字路口、弯道等。障碍物检测与避障路径上会随机出现静态或动态障碍物小车需能检测并安全绕行。药品信息接收与确认通过无线通信如蓝牙、Wi-Fi或上位机下发送药任务病房号、药品信息小车需能接收并显示。执行精度与稳定性在规定时间内完成多次送药任务要求停靠位置准、任务成功率高、系统运行稳定不宕机。基于这些点我们决定采用“视觉主导多传感器融合”的架构。视觉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脚本。病房门牌识别我们采用颜色形状的复合识别。首先病房门牌使用特定颜色的矩形色块比如蓝色。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_threshold200, pixel_threshold200) 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(roiroi, threshold2500) if circles: # 确认找到门牌计算中心坐标并发送给STM32 # ... 通过UART发送坐标数据调试心得现场光线是视觉项目的头号杀手。我们准备了可调亮度的补光灯并在赛前花了大量时间调整颜色阈值。一个技巧是编写一个阈值调试工具能在OpenMV IDE里实时查看识别效果并调整LAB阈值将最佳阈值保存下来固化到代码中。循迹策略我们采用“双线巡迹”方案。在赛道两侧贴有黑色引导线。OpenMV识别这两条线并计算两条线中点的位置。将这个中点与图像中心点的偏差作为方向控制的误差量发送给STM32。这种方法比单线巡迹更稳定尤其在弯道时能提供更早的转向预判。# 双线巡迹示例 black_threshold (0, 50, -30, 30, -30, 30) # 黑色阈值 blobs img.find_blobs([black_threshold], area_threshold100, mergeTrue) # 区分左右线根据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给STM323.2 STM32控制核心多任务调度与通信STM32的程序我们使用HAL库开发结构清晰。核心是建立一个高效的多任务调度系统。定时器中断作为系统心跳我们配置了一个1ms的定时器中断。在这个中断里不进行复杂运算只执行以下操作更新系统时间戳。置位各个任务如电机控制、传感器读取、通信处理的“允许执行”标志位。这是一种简单的时间片轮询调度思想。主循环中的任务执行在主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发来的数据 } // ... 其他即时性任务如按键扫描 }与OpenMV的通信协议我们定义了一个简单的串口通信协议。OpenMV周期性地如每50ms发送一帧数据包含帧头、数据长度、病房识别状态/坐标误差、校验和等。STM32端接收到后进行校验解析出有效数据用于控制。帧格式0xAA 0x55 [数据长度] [数据1] ... [数据N] [校验和]校验和通常采用前面所有字节的累加和取低8位或CRC8确保数据传输的可靠性。3.3 运动控制算法PID与状态机小车的平稳运动是得分的关键。差速控制模型我们的小车是两轮差速驱动。运动学模型很简单线速度 V (V_left V_right) / 2角速度 ω (V_right - V_left) / LL为轮距。通过控制左右轮的速度就能实现前进、后退、转弯和自转。双闭环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参数并观察效果这节省了大量时间。任务状态机设计小车的送药流程是一个典型的状态机。我们设计了如下状态IDLE-RECV_TASK接收任务-NAV_TO_ROOM导航至病房-AVOID_OBSTACLE如遇障碍-ARRIVE_CHECK到达检查-DELIVERY执行递送-RETURN返回充电区/待命区。 STM32中用一个全局变量g_car_state记录当前状态每个状态有对应的处理函数和状态转移条件。这种设计逻辑清晰易于调试和扩展。4. 系统集成、调试与现场实战4.1 模块联调与系统标定当各个模块单独测试都OK后集成才是真正的挑战。电源与地噪声排查所有模块共地但电机驱动部分的大电流地线要粗并单点连接到电源地。用示波器观察单片机电源引脚在电机启停时是否有大的毛刺。我们通过增加电源滤波电容和采用磁珠隔离解决了复位问题。传感器数据融合与滤波超声波传感器数据跳动大我们采用中值滤波结合一阶低通滤波。例如连续采样5次去掉最大最小值再取平均最后进行低通滤波得到稳定可靠的距离值。视觉与控制器的延时校准OpenMV处理图像、STM32接收串口数据都存在毫秒级的延时。这会导致小车根据“过去”的位置信息做出控制反应可能引发振荡。我们在代码中加入了简单的预测补偿根据小车当前速度估算出延时期间小车移动的距离在计算误差时将其补偿回去。机械结构优化重心要低轮子要正。我们最初的车体较高快速转弯时容易晃动甚至侧翻。后来降低了电池和主板的位置并仔细调整了轮子与电机的连接确保同轴减少了不必要的机械抖动对编码器读数的影响。4.2 现场部署与适应性调整电赛现场环境多变必须做好预案。光线应对我们准备了小型LED补光灯并准备了多套针对强光、弱光、侧光的颜色阈值参数赛前根据现场光线快速切换。赛道适应性赛道的摩擦系数、平整度可能和实验室不同。我们准备了不同硬度的轮胎海绵轮、橡胶轮并现场微调了PID参数。一个关键技巧在赛前练习时记录下小车在标准速度下跑完一圈的编码器计数作为一个“标准距离”。现场如果发现同样编码器计数下跑的距离有偏差可以微调编码器计数与实际距离的换算系数。通信抗干扰比赛现场无线环境复杂。我们除了在通信协议中加入校验还设置了应答机制。对于重要的任务指令如病房号上位机发送后小车必须回复确认否则上位机会重发。调试接口预留我们在小车上预留了一个蓝牙模块接口并通过一个拨码开关控制其是否启用。在调试阶段打开蓝牙可以用手机实时查看小车传感器数据、状态和修改参数。比赛时关闭拨码开关确保系统纯净。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和源文件、以及详细的说明文档。给使用者的建议不要直接复制粘贴我们的代码和方案是针对特定赛题、特定硬件和特定场景优化的。请务必理解其背后的逻辑然后根据你自己的硬件平台可能电机不同、传感器型号不同和具体任务进行调整。重点关注框架比起具体的PID参数值更值得学习的是我们如何组织代码多任务调度、状态机、如何设计通信协议、如何进行传感器数据融合。这些框架性的东西具有通用性。硬件是基础再好的算法也跑在一个糟糕的硬件平台上。请花时间打磨你的小车底盘确保其机械结构的稳固性和电路连接的可靠性。一个晃动的小车会让所有精密的控制算法失去意义。关于项目演进的思考 这个项目获得一等奖但远非终点。赛后我们复盘认为还可以从以下几个方向深化更高级的路径规划目前是简单的巡线可以尝试集成红外光电对管阵列实现“离线”路径记忆或者探索基于摄像头AprilTag的全局定位与SLAM同步定位与地图构建雏形。更强的环境感知可以尝试使用TOF飞行时间激光雷达替代超声波获得更精确、快速的360度环境信息。更智能的决策引入更复杂的行为树或轻量级AI模型如TinyML让小车能处理更复杂的突发情况比如多动态障碍物、临时交通管制模拟等。无线通信与组网如果赛题允许可以设计多车协同送药系统通过ZigBee或Wi-Fi Mesh网络通信由一台主机调度多台小车这将是质的飞跃。开源这个项目是希望将我们“不眠之夜”的成果转化为社区共同进步的阶梯。电赛的魅力不仅在于结果更在于那个从无到有、不断调试、最终看到小车完美执行任务的过程。希望我们的代码和文档能帮你少走一些我们走过的弯路更高效地享受这个创造的过程。如果你在使用中发现了问题或者有更好的想法非常欢迎在GitHub上提交Issue或Pull Request。技术之路开源共行。