1. 项目概述与核心思路
看到“智能送药小车”这个题目,再结合“我是怎样水过的”这个略带调侃的副标题,相信很多参加过电子设计竞赛(电赛)的朋友都会心一笑。这背后反映的,绝不是一个简单的“水”字,而是在有限的时间、预算和知识储备下,如何做出一个“能用、能跑、能演示”的系统的真实写照。2021年电赛的这道题,本质上是一个典型的室内移动机器人综合应用,它融合了路径规划、视觉识别、机械控制、无线通信等多个技术模块,对参赛者的系统集成能力和临场应变能力提出了很高的要求。
我当时和队友拿到题目后,第一反应也是头大。题目要求小车能在模拟的医院病房环境中,自主从药房出发,识别病房门牌号,将药品送达指定病房,并完成一系列交互动作。听起来很酷,但细想下来,从零开始造轮子几乎不可能。所以,我们的核心思路从一开始就非常明确:在保证基础功能稳定实现的前提下,尽可能采用成熟、可靠的模块化方案,将开发风险降到最低,把有限的精力集中在系统联调和策略优化上。这不是偷懒,而是在竞赛这种高压、限时的环境下,最务实、最有可能出成绩的策略。我们的目标不是做出一个技术炫酷但稳定性差的“花瓶”,而是打造一个在评委面前能稳定跑完全程的“战士”。
整个项目,我们将其拆解为三个核心层次:感知层、决策层和执行层。感知层负责“看清”和“知道自己在哪”,主要依赖摄像头和编码器;决策层是大脑,我们选择了性能足够且生态成熟的STM32系列单片机;执行层则是小车底盘、机械臂和通信模块。这种分层架构的好处是,各层之间通过清晰的接口(如串口、PWM、IO)耦合,可以并行开发,最后联调,极大地提高了开发效率。下面,我就按照这个架构,结合我们当时“踩过的坑”和“水过的技巧”,把整个项目的实现过程拆开揉碎了讲清楚。
2. 系统整体设计与核心模块选型
2.1 主控与核心架构选择
主控芯片是项目的心脏。当时主流的选择有STM32、树莓派、K210,甚至还有用OpenMV直接当主控的。我们经过一番权衡,最终选择了STM32F407ZGT6作为核心主控。理由很直接:首先,我们团队对STM32的HAL库和标准库比较熟悉,开发速度快;其次,F407主频168MHz,带有FPU,处理摄像头图像、进行简单的路径计算完全够用;最重要的是,它的外设资源(多个串口、定时器、IO口)非常丰富,可以轻松连接电机驱动、编码器、摄像头、无线模块等多个设备,无需担心资源冲突。
注意:很多队伍为了追求视觉处理的便捷,会选用树莓派。但树莓派运行Linux系统,在电机控制实时性、电源管理(功耗较大)和系统稳定性(突然死机)上存在风险。电赛现场环境复杂,一个稳定的核心往往比一个功能强大的核心更重要。
我们采用了前后台系统的软件架构,而非实时操作系统(RTOS)。对于这个项目来说,任务相对固定:定时采集摄像头图像并处理、读取编码器计算里程、根据状态机执行任务、通过PID控制电机。这些任务通过一个精心设计的主循环和定时器中断就能很好地调度。引入RTOS会增加学习成本和调试复杂度,在时间紧迫的情况下,简单可靠的方案就是好方案。
2.2 视觉方案:如何“看清”门牌和路
视觉是整个小车的“眼睛”,也是最容易出问题的环节。题目要求识别数字门牌和巡线。我们放弃了当时听起来很“高大上”的神经网络方案,因为数据集制作、模型训练和部署太耗时,且在当时嵌入式设备上推理速度是个问题。我们选择了最经典也最稳妥的OpenMV摄像头模块。
OpenMV的优势在于它内置了MicroPython解释器和丰富的机器视觉库,比如find_blobs(找色块)、find_template(模板匹配)、apriltag识别等。我们用它来完成两件事:
- 巡线:通过阈值化,将赛道(通常是黑色电工胶带)与背景(白色KT板)区分开,计算赛道中心的偏移量,转化为小车的转向控制量。
- 数字识别:这是关键。我们采用了特征匹配+模板匹配的“土办法”。首先,在赛前,我们用OpenMV对着可能出现的数字(如1,2,3,4)在比赛现场光照条件下拍下多组模板图片,存入SD卡。比赛时,小车到达病房区域后,摄像头会先进行颜色滤波,定位出门牌的大致区域(比如一个蓝色的矩形区域),然后对这个区域进行二值化和裁剪。最后,将裁剪后的图像与我们预存的数字模板进行一一比对(使用
image.find_template函数),计算相似度,取相似度最高的作为识别结果。
这个方法听起来很“笨”,但极其有效。它避免了复杂的字符分割和识别算法,将问题转化为图像匹配问题,稳定性非常高。当然,前提是模板图片要在与实际环境光照相近的条件下采集。
2.3 定位与导航:如何“知道自己在哪”
在室内平坦的赛道上,我们采用了最经典的“编码器+巡线”的相对定位方法。绝对定位(如UWB、激光SLAM)成本高、算法复杂,不适合电赛。
- 里程计:小车底盘的两个驱动轮上安装了光电编码器(500线)。通过定时器输入捕获模式,读取编码器的脉冲数,根据轮径和减速比,可以计算出每个轮子实际转动的距离。通过差速里程计模型,就能估算出小车相对于起点的位置(x, y)和航向角(θ)。这是路径规划和回退的基础。
- 巡线纠偏:这是主要的横向定位手段。OpenMV计算出赛道中心线偏离图像中心的像素偏差,通过一个比例系数转换为PWM占空比,调整左右轮差速,实现转向。我们将其设计为一个独立的、高频率运行的PID控制环。
- 路标识别:赛道中会有十字路口、丁字路口等特殊标志。我们通过识别赛道线的中断、分支等特征,作为全局路径中的关键“路标”,用于修正累积的里程计误差和触发状态切换。例如,识别到一个十字路口,就意味着小车到达了病房区的中心走廊。
2.4 执行机构与车体设计
车体是我们自己用亚克力板激光切割后组装的,结构坚固且方便打孔安装各种模块。执行机构主要包括:
- 底盘驱动:采用了常见的MG513P30直流减速电机,搭配TB6612FNG电机驱动模块。TB6612比传统的L298N效率高、发热小,驱动我们的小车绰绰有余。电机一定要配对,我们赛前用同一PWM值测试,筛选出空载转速相近的两个电机,以减少直行时的固有偏差。
- 机械臂/送药机构:这是体现“送药”功能的关键。题目通常要求将一个小药瓶(或模拟物)放入病房门口的篮子里。我们设计了一个极其简单的“推杆式”机构:用一个9g舵机控制一个可以伸缩的推板。小车到达指定位置后,舵机转动,将放置在车头托盘上的药瓶推入篮子。复杂的三自由度机械臂不仅控制难,而且容易卡死,简单的机构在关键时刻更可靠。
- 无线通信:用于接收上位机(可能是PC或Pad)发出的病房号指令。我们用了最普及的HC-05蓝牙模块。在STM32上实现一个简单的串口协议,如“#Room:3*”,就能可靠接收指令。避免使用Wi-Fi,因为现场无线环境复杂,容易干扰。
3. 核心算法与软件实现细节
3.1 巡线PID算法的调试心得
巡线是小车能跑起来的基础。我们用的是最简单的单目摄像头巡线,算法核心是一个位置式PID控制器。
- 误差获取:OpenMV图像宽度为320像素,我们将图像ROI(感兴趣区域)设置在下方一条横带内。找出该区域内黑色赛道部分的中心点横坐标
line_center。误差error = 160 - line_center(假设图像中心是160)。 - PID计算:
output = Kp * error + Ki * integral + Kd * derivative。这个output就是左右电机的速度差补偿值。 - 参数整定:这是调参的“玄学”部分。我们的经验是:
- 先P后I最后D:先把Ki和Kd设为0,逐渐增大Kp,让小车能快速响应弯道,但会出现左右振荡。
- 加D抑振:加入微分项Kd,可以有效抑制振荡,让过弯更平滑。D值太大会导致系统对噪声敏感,小车抖动。
- 慎用I:积分项Ki用于消除静态误差(比如长期偏向一侧)。但在巡线中,赛道是变化的,积分容易累积导致系统滞后甚至失控。我们最终给的Ki值非常小,或者只在直道段启用积分。
- 输出限幅:计算出的
output必须限制在一个合理范围(如±50),再叠加到电机的基础速度上,防止速度突变翻车。
实操心得:PID参数没有“黄金值”,必须在实际赛道上调试。我们准备了几个不同曲率的弯道,反复调试,记录下参数。最重要的是鲁棒性,即一套参数应能适应直道、弯道、十字路口等多种情况,即使性能不是最优,但绝不能跑飞。
3.2 基于状态机的任务调度
小车的整个送药流程是一个顺序+分支的过程,非常适合用有限状态机(FSM)来实现。这能让程序逻辑无比清晰,易于调试和维护。
我们定义了以下几个主要状态:
STATE_IDLE:空闲状态,等待蓝牙指令。STATE_GO_TO_CORRIDOR:从药房出发,巡线前往中心走廊。STATE_FIND_ROOM:在走廊巡线,结合编码器里程和路口识别,寻找目标病房门口。STATE_ALIGN_DOOR:到达大致位置后,进行横向微调,对准病房门。STATE_DELIVER:控制舵机,推出药品。STATE_RETURN:送药完成后,原路返回或前往下一个点。
状态机的切换条件非常明确,比如里程到达某个值、摄像头识别到路口、接收到某个传感器信号等。在main函数的大循环里,就是一个switch(state)语句,每个case里执行该状态下的操作并检查切换条件。
// 伪代码示例 switch(current_state) { case STATE_GO_TO_CORRIDOR: follow_line(); // 巡线 update_odometry(); // 更新里程 if (odometry.y > 2000) { // 假设走廊在y轴2000mm处 current_state = STATE_FIND_ROOM; target_door = bluetooth_received_room_number; // 从蓝牙获取目标病房号 } break; case STATE_FIND_ROOM: follow_line(); if (detect_crossroad()) { // 识别到路口 crossroads_count++; if (crossroads_count == target_door) { // 第n个路口就是目标病房 current_state = STATE_ALIGN_DOOR; stop_motors(); } } break; // ... 其他状态 }这种方法的好处是,当某个环节出问题时(比如某个路口没识别到),我们可以很容易地通过串口打印当前状态和变量,快速定位问题所在。
3.3 数字识别的具体实现与优化
如前所述,我们的数字识别基于OpenMV的模板匹配。但直接匹配很容易受光照和角度影响。我们做了几点优化:
- ROI动态定位:不是在整个图像里找数字,那样太慢且容易误匹配。我们先找蓝色的门牌区域(
find_blobs找蓝色色块),以这个色块的边界作为ROI,大大缩小了搜索范围。 - 图像预处理:对ROI内的图像进行灰度化、高斯滤波(去噪)、二值化(阈值可调,以应对不同光照)。二值化是关键一步,我们预留了一个按键,可以在现场实时调整二值化阈值,确保数字轮廓清晰。
- 多模板与置信度:每个数字(0-9)我们准备了3-5个不同角度、轻微光照差异的模板。匹配时,遍历所有模板,记录最高得分。同时设定一个置信度阈值(比如0.7),只有最高得分超过阈值,才认为识别有效,否则视为识别失败,触发重试或使用备用策略(如根据里程估算)。
- 识别结果滤波:连续识别5帧,取出现次数最多的结果作为最终结果,避免单帧误识别。
4. 系统集成、调试与现场应对策略
4.1 模块化联调与测试清单
所有模块单独测试通过后,集成是最考验人的。我们制定了一个严格的联调测试清单,确保每次改动后都跑一遍:
- 电源测试:带上所有负载(电机堵转、舵机动作、摄像头开启),用万用表测量主板和各模块供电电压是否稳定(尤其是5V和3.3V)。电机启动瞬间的电压跌落是很多诡异问题的根源。
- 通信测试:蓝牙模块与手机APP能否正常收发指令?指令格式是否正确解析?OpenMV与STM32的串口通信是否畅通(图像数据、识别结果)?
- 基础运动测试:发送前进、后退、左转、右转指令,小车是否准确执行?编码器读数是否正常递增/递减?
- 巡线闭环测试:将小车放在赛道上,能否稳定巡线?过弯是否平滑?从不同位置放下,能否都回归到赛道?
- 全流程模拟测试:模拟完整任务,从接收指令开始,到寻路、识别、送药、返回。不要求一次成功,但每个环节的问题要记录。
4.2 现场环境适应与参数快速调整
比赛现场的光照、赛道摩擦系数、场地平整度都可能和实验室不同。我们必须准备快速调整的能力。
- 光照应对:我们为OpenMV制作了一个简单的遮光罩(用纸板卷成筒),减少侧面杂光干扰。二值化阈值、颜色识别阈值都做成可通过串口指令或按键实时调整的变量。
- 赛道摩擦应对:准备不同材质的轮胎(如硅胶轮、橡胶轮)。现场测试哪种抓地力更好且不打滑。电机的PID参数也准备2-3组,一组用于高摩擦地面(参数可激进些),一组用于低摩擦地面(参数保守些)。
- 机械结构加固:用扎带、热熔胶对所有线缆和可能松动的模块进行加固。赛前反复检查螺丝是否拧紧,舵机臂是否安装牢固。
4.3 故障预案与降级策略
再完美的系统也可能出意外。我们设计了几个降级策略,确保在主要方案失效时,小车仍能完成基本任务,拿到基础分。
- 视觉失效预案:如果摄像头突然损坏或光线极差,无法巡线和识别数字。我们启用了基于编码器的盲跑程序。提前测量好赛道关键点间的精确距离(如药房到走廊中心距离、走廊中心到各病房距离),小车完全依靠编码器里程和陀螺仪(如果装了的话)走预设的固定路径和转角。虽然无法应对赛道偏差,但能完成“到达大致位置”的动作。
- 数字识别失效预案:如果模板匹配始终失败。我们启用了备用方案:路径计数法。我们知道每个病房对应走廊上的第几个路口。小车只需要巡线,并利用OpenMV识别十字路口(通过检测横向赛道线)进行计数,到达指定数量后即执行送药动作。这不需要识别具体数字。
- 通信失效预案:如果蓝牙连接不上。我们在小车上设置了一个拨码开关或几个按键,用于手动输入目标病房号(比如按3下按键代表3号病房)。上电后,小车检测到手动输入,则开始任务。
5. 常见问题排查与实战心得
5.1 硬件层面典型问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 电机不转或单边转 | 1. 电机驱动模块供电不足或损坏。 2. STM32 PWM输出引脚配置错误或未初始化。 3. 电机线虚焊或断开。 4. 程序初始化后未使能电机驱动(如TB6612的STBY引脚未拉高)。 | 1. 用万用表测量驱动模块VM(电机电源)电压,确保在额定范围内(如12V)。 2. 用示波器或逻辑分析仪检查STM32对应引脚是否有PWM波形输出。 3. 拔下电机线,直接用电池点触电机,看是否转动。 4. 检查代码,确认初始化序列中已将驱动芯片的使能引脚置高。 |
| 编码器读数不准或为0 | 1. 编码器供电不正常。 2. 定时器编码器模式配置错误。 3. 接线错误(A、B相序接反)。 4. 脉冲数过多,定时器溢出。 | 1. 测量编码器VCC和GND间电压(通常5V)。 2. 检查定时器初始化代码,是否设置为编码器模式(Encoder Mode),并正确映射通道。 3. 交换A、B相线序试试。 4. 增加定时器位数(如用32位定时器)或提高读取频率,在中断里及时读取计数值并清零。 |
| 摄像头无图像或通信异常 | 1. 串口波特率不匹配。 2. 摄像头供电电流不足(尤其是点亮LED时)。 3. 串口线RX/TX接反。 4. OpenMV脚本未成功运行。 | 1. 确认STM32和OpenMV脚本中设置的串口波特率一致(常用115200)。 2. 单独给OpenMV用一路稳压电源供电,或检查主电源能否提供足够电流(建议1A以上)。 3. 检查接线:STM32的TX接OpenMV的RX,STM32的RX接OpenMV的TX。 4. 通过OpenMV IDE连接摄像头,看能否运行脚本并看到图像。 |
| 小车跑偏(不巡线时) | 1. 左右轮电机转速固有差异。 2. 左右轮直径或轮胎摩擦力有差异。 3. 车体重心严重偏向一侧。 | 1. 空载时,给相同占空比的PWM,用手机测速APP测量轮子转速,微调PWM值进行软件补偿。 2. 挑选直径一致的轮子,轮胎表面清洁。 3. 重新布局电池、主板等重物,使左右重量平衡。 |
5.2 软件与算法层面典型问题
- 巡线时小车剧烈振荡或冲出赛道:这是PID参数不合理,尤其是微分项D过大或积分项I累积造成的。降低P和D值,或将I项清零。先确保小车在直道上能缓慢修正,再调弯道性能。
- 数字识别时好时坏:主要是光照和阈值问题。务必在现场光线条件下重新采集模板图片。增加图像预处理的对比度增强步骤。采用多帧投票决策,避免单帧误判。
- 状态机卡在某个状态不切换:最常用的调试方法:串口打印大法。在每个状态入口和条件判断处,通过串口打印当前状态、关键传感器数据和条件变量值。可以迅速定位是传感器数据未达到预期,还是条件判断逻辑有误。
- 里程计累积误差越来越大:这是无法避免的。我们的策略是利用路标进行重置。例如,每次识别到一个十字路口,就将里程计的坐标(x, y)重置为这个路口的已知坐标,航向角θ也根据赛道方向进行修正。这样就把长距离导航分解为多个短距离的、误差可控的段落。
5.3 团队协作与时间管理心得
电赛是团队战,合理分工至关重要。我们队三人分工如下:一人负责硬件搭建、电源管理和电机驱动(硬件岗);一人负责OpenMV视觉算法和图像处理(算法岗);一人负责STM32主控程序、状态机和PID调试(软件岗)。前期分头开发,中后期每天至少进行一次集成联调。
时间管理上,我们遵循“先完成,再完美”的原则。第一周,目标是让小车能巡线走起来,能收到蓝牙指令。第二周,实现基本的寻路和数字识别。第三周,完善所有功能,并疯狂进行稳定性测试和制定故障预案。最后一天,不再添加新功能,只做稳定性优化和演练。
“水过”的真谛,不在于技术有多高超,而在于对项目风险的清醒认知、对成熟方案的灵活运用、以及面对问题时快速有效的解决能力。用一个稳定可靠的方案拿到80%的分数,远比用一个高风险的前沿方案去搏100%要明智。这个项目带给我们的,不仅仅是奖状,更是在压力下进行系统工程开发的宝贵经验。直到现在,我依然觉得那些为了调一个PID参数在实验室熬到深夜、为了一次成功的全程演示而欢呼雀跃的日子,是大学生活里最闪光的记忆。