从零组装电赛送药小车:OpenMV视觉核心+STM32控制,我的软硬件联调全记录
从零构建智能送药小车:OpenMV视觉与STM32的深度协同实战
项目背景与设计思路
去年电子设计竞赛的智能送药小车题目,让不少参赛队伍在视觉识别与控制协同上栽了跟头。作为全程参与该项目的开发者,我想分享一套经过实战检验的解决方案——如何让OpenMV的视觉识别与STM32的运动控制实现无缝协作。
这个项目的核心挑战在于实时性与可靠性的平衡。视觉模块需要快速准确地识别路径和十字路口,而主控芯片则要根据这些信息做出即时响应。我们最终采用的架构是:OpenMV负责图像采集与特征识别,通过串口向STM32发送指令编码;STM32解析指令后控制电机和舵机,同时处理避障等实时任务。
这种分工充分发挥了各自优势——OpenMV的专用图像处理库简化了开发流程,STM32的强大定时器资源确保了PWM控制的精确性。但真正让系统跑起来,还需要解决三大关键问题:
- 通信协议设计:如何确保指令传输的可靠性和实时性
- 控制逻辑实现:视觉识别结果到电机动作的映射关系
- 系统联调技巧:时序同步、电源管理等实战经验
硬件架构设计与选型要点
核心组件选型对比
| 组件类型 | 候选方案 | 最终选择 | 选择理由 |
|---|---|---|---|
| 视觉模块 | OpenMV vs Raspberry Pi | OpenMV H7 | 专用视觉库、低延迟、功耗优势 |
| 主控芯片 | STM32F103 vs STM32F407 | STM32F103C8T6 | 性价比高、外设够用 |
| 电机驱动 | L298N vs TB6612 | TB6612FNG | 发热量小、支持双路PWM |
| 电源管理 | 线性稳压 vs 开关电源 | MP2307DN+AMS1117 | 效率85%以上、纹波小 |
硬件连接有个容易忽视的细节:共地问题。OpenMV与STM32必须确保GND连通,否则串口通信会出现乱码。我们的接线方案是:
- OpenMV的UART3_TX(P4) → STM32的USART2_RX(PA3)
- OpenMV的UART3_RX(P5) → STM32的USART2_TX(PA2)
- 两模块的GND引脚直连
关键提示:务必在电源输入端加装470μF以上的电解电容,电机启停时的电压波动可能导致OpenMV意外重启。
供电系统优化方案
送药小车最头疼的就是电机干扰导致系统复位。经过多次测试,我们总结出这套供电方案:
- 7.4V锂电池作为主电源
- 通过MP2307DN降压至5V供给OpenMV
- AMS1117-3.3V为STM32供电
- 电机驱动单独一路电源,经1000μF电容滤波
# OpenMV电源监控代码(防止低压运行) import pyb def check_voltage(): v = pyb.ADC(pyb.Pin('P6')).read() * 3.3 / 4095 * 2 if v < 3.7: # 电压低于3.7V pyb.LED(1).on() pyb.LED(2).on() pyb.LED(3).on() raise Exception('电压过低!')视觉识别系统的深度优化
多ROI协同识别策略
原始方案采用固定阈值二值化,在实际场地中遇到光照变化就失效。改进后的识别系统有三个创新点:
- 动态阈值调整:根据环境光自动更新阈值范围
- 区域分级管理:将视野划分为5个ROI区域
- 中央区(巡线主区域)
- 左右预判区(提前检测弯道)
- 远场确认区(减少近处干扰)
- 十字路口特征区
- 状态机机制:不同场景使用不同识别策略
# 改进后的ROI定义(单位:像素) ROIs = { 'main': (30, 15, 20, 30), # 中央主区域 'left_pre': (0, 20, 15, 15), # 左侧预判 'right_pre': (65, 20, 15, 15), 'far': (25, 0, 30, 10), # 远场区域 'cross_L': (0, 40, 20, 20), # 十字路口特征 'cross_R': (60, 40, 20, 20) }十字路口识别的误判处理
初版代码遇到的最大问题是十字路口误识别。通过添加时空双重验证机制,准确率从70%提升到98%:
- 空间验证:左右特征区需同时检测到色块
- 时间验证:连续3帧确认才判定为十字路口
- 防抖处理:识别后500ms内不再检测
cross_check = 0 # 连续确认计数器 def check_crossing(img): global cross_check left_blobs = img.find_blobs([threshold], roi=ROIs['cross_L']) right_blobs = img.find_blobs([threshold], roi=ROIs['cross_R']) if left_blobs and right_blobs: cross_check += 1 if cross_check >= 3: return True else: cross_check = 0 return FalseSTM32控制系统的实现细节
通信协议设计规范
我们自定义了一套简洁高效的通信协议:
[指令类型][数据][结束符] 示例: "1-15.3\r\n" # 类型1指令,数据-15.3 "2\r\n" # 类型2指令(十字路口) "3\r\n" # 类型3指令(停止)STM32端的解析逻辑:
// USART2中断服务程序 void USART2_IRQHandler(void) { if(USART_GetITStatus(USART2, USART_IT_RXNE)) { char ch = USART_ReceiveData(USART2); if(ch == '\r') { parse_command(buffer); // 解析完整指令 buffer_index = 0; } else if(ch != '\n') { buffer[buffer_index++] = ch; } } }电机控制PID调参实战
通过实际测试得出的PID参数经验:
速度环PID(控制电机转速)
- Kp=0.8, Ki=0.05, Kd=0.1
- 采样周期10ms
方向环PID(控制舵机角度)
- Kp=1.2, Ki=0.01, Kd=0.3
- 采样周期20ms
调试时发现的黄金法则:
- 先调P至出现小幅震荡
- 然后加D抑制震荡
- 最后加I消除静差
- 野外测试时要预留20%的参数余量
系统联调中的典型问题解决
时序同步问题排查
遇到最棘手的bug:OpenMV发送的指令在STM32端出现随机丢失。通过逻辑分析仪捕获到的异常波形显示:
- 问题现象:每30-40个指令会丢失1个
- 根本原因:USART接收缓冲区溢出
- 解决方案:
- 将串口波特率从115200提升到230400
- 在STM32端启用DMA接收
- 添加指令序号校验机制
// DMA接收配置示例 DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel6); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART2->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uart_buffer; DMA_InitStructure.DMA_BufferSize = UART_BUF_SIZE; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_Init(DMA1_Channel6, &DMA_InitStructure); DMA_Cmd(DMA1_Channel6, ENABLE); USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE);电源干扰的终极解决方案
在决赛现场遇到的诡异现象:小车运行10分钟后必然失控。最终定位到是电机碳刷火花干扰导致,采取三重防护:
- 在所有电机引脚并联104瓷片电容
- 电源线改用双绞线并套磁环
- STM32的复位引脚增加0.1μF去耦电容
血泪教训:实验室测试正常不代表现场能稳定运行,务必进行4小时以上连续压力测试。
竞赛实战技巧与性能优化
场地自适应策略
省赛时因为场地光线变化导致识别失败,我们开发了自适应初始化流程:
- 上电后前5秒采集环境光参数
- 自动计算各区域阈值偏移量
- 保存基准值到Flash备用
def auto_calibration(): sensor.skip_frames(10) # 等待传感器稳定 hist = [0]*256 for i in range(30): # 采样30帧 img = sensor.snapshot() for p in img.get_histogram().get_threshold().get_data(): hist[p] += 1 # 找出占比最多的灰度值 peak = hist.index(max(hist)) # 计算新的阈值范围 new_threshold = (max(0, peak-20), min(255, peak+20)) return new_threshold运动控制的高级技巧
决赛前夜发现的速度-精度平衡法则:
- 直线段:全速运行(80% PWM)
- 弯道识别:降速至50%
- 十字路口前1米:预减速至30%
- 终点识别区:20%蠕行速度
对应的STM32代码实现:
void adjust_speed(uint8_t road_type) { static uint8_t last_type = 0; if(road_type != last_type) { switch(road_type) { case STRAIGHT: set_motor_pwm(80, 80); break; case CURVE: set_motor_pwm(50, 50); break; case CROSSING: set_motor_pwm(30, 30); break; case FINAL: set_motor_pwm(20, 20); break; } last_type = road_type; } }这套系统最终在省级竞赛中获得技术分第一名,最关键的成功因素是稳定性设计——在全部8次正式运行中零失误完成任务。现在回想起来,那些调试到凌晨三点的夜晚,那些被电机烫伤的手指,那些因为一个bug而重写了七遍的协议栈,都成了最珍贵的实战经验。
