
简介基于STM32与OpenMV的视觉云台追踪小车完整工程资料适合嵌入式、自动化、电子信息等专业学生用于课程设计、毕业设计或项目初期验证。整体围绕纯色物体识别与AprilTag标签识别讲解了从边缘检测、四边形筛选到标签解码的原理以及从图像坐标到二自由度云台角度换算的完整思路并给出可直接烧录的固件与源码工程。压缩包共246个文件主要包括.c/.h源码、Keil工程配置文件.uvprojx、编译生成的.hex/.axf固件、中间文件.o/.d/.crf及README说明文档整体约9.42MB目录结构清晰便于按模块对照学习。资源已有685人学习下载。通过源码可学习OpenMV视觉定位与STM32控制逻辑的串口通信方法掌握目标坐标到舵机角度的转换设计适合在现有代码基础上二次开发实现更多巡线、抓取等视觉追踪功能。1. 视觉云台追踪小车为什么必须用双芯片方案第一次看到“基于stm32以及openmv的视觉云台追踪小车”这个题目时很多人的第一反应是“OpenMV不就能直接输出坐标吗为什么还要加一块STM32”这个疑问恰恰是项目的核心。OpenMV的OpenMV Cam系列虽然集成了摄像头、处理器和MicroPython解释器但它本质上是视觉前端它的GPIO资源有限、定时器不够精准、PWM输出路数少直接驱动双路直流电机加两路舵机时会出现帧率波动导致PWM周期抖动、串口被图像处理占用导致控制指令延迟、电机反电动势干扰复位等一连串问题。而STM32F103系列在电机控制上经历了大量工业验证定时器输出比较模式配合DMA可以做到微秒级稳定PWM独立看门狗和硬件串口又能保证系统不出现死循环。所以常规做法是OpenMV负责“看”STM32负责“动”两者用串口通信建立一条低延迟指令通道。这套方案适合三类人做毕业设计或电赛的学生需要快速出效果但不想把整个视觉和控制逻辑揉在一个MCU上刚接触嵌入式视觉的工程师想理解图像坐标系到机械执行机构的完整映射流程以及想给现有STM32小车加视觉跟踪能力的开发者本文的串口协议和PID骨架可以直接复用。你不用先精通OpenMV的神经网络也不必研究复杂的图像处理算法只要掌握颜色识别和云台角度映射这两个基本功就能跑通一个“目标走到哪云台和小车跟到哪”的完整闭环。2. 系统架构与通信协议先让两个芯片“说上话”2.1 拆解硬件选型用一个最小可复现的物料清单常见做法是选STM32F103C8T6核心板OpenMV Cam H7 Plus作为视觉模块云台用两路SG90舵机组成二自由度结构底盘用两路带编码器的直流减速电机加TB6612驱动。电源必须分开OpenMV瞬时功耗可以达到250mA以上舵机启动瞬间电流接近500mA如果和电机共用一路供电摄像头会频繁花屏。我一般会准备两片18650锂电池组一组为STM32和OpenMV供电另一组专门给电机驱动两路电源只共地不共电。通信引脚接线时OpenMV的UART3_TXP4接STM32的PA3USART2_RXOpenMV的UART3_RXP5接STM32的PA2USART2_TX波特率设置为115200。STM32端选择USART2而不用USART1的原因是把USART1留着接蓝牙或上位机方便后期调试。关键的一个细节是两根通信线尽量短且远离电机驱动板的输出端电机的PWM斩波会产生强烈的电磁干扰在线材缠绕时不要把串口线和电机线扎在一起否则经常出现帧头校验错误。2.2 定义一帧指令不要让坐标数据“裸奔”OpenMV向STM32发送的数据需要包含三个信息目标在画面中的x坐标、y坐标、目标是否丢失。常见的做法是封装成七个字节的帧帧头0xAA、数据长度0x04、x高八位、x低八位、y高八位、y低八位、校验和。校验和取前六个字节的累加和低八位STM32收到后先验证帧头再算校验避免把噪声数据当成有效指令。以下是OpenMV端的发送代码import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time2000) sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) uart UART(3, 115200, timeout_char1000) red_threshold (30, 100, 15, 127, 15, 127) def pack_data(cx, cy, lost_flag): x int(cx) y int(cy) payload bytearray([0xAA, 0x04, (x 8) 0xFF, x 0xFF, (y 8) 0xFF, y 0xFF, 0x00]) checksum sum(payload[1:6]) 0xFF payload[6] checksum return payload while True: img sensor.snapshot() blobs img.find_blobs([red_threshold], pixels_threshold200, area_threshold200) if blobs: largest_blob max(blobs, keylambda b: b.area()) cx largest_blob.cx() cy largest_blob.cy() img.draw_cross(cx, cy, color(0, 255, 0)) uart.write(pack_data(cx, cy, 0)) else: uart.write(pack_data(160, 120, 1)) time.sleep(20)这段代码里find_blobs的第一个参数是LAB色彩空间的阈值元组不同光照环境下同一个红色物体的阈值变化很大调试时用OpenMV IDE的“阈值编辑器”工具拖动滑块找到实际环境的区间。pixels_threshold是用像素个数过滤掉画面中的噪点如果设成1摄像头的噪声点会让云台在小范围内高频抖动设得太大又可能漏掉远距离的小目标。time.sleep(20)控制发送帧率大约50Hz对于视觉云台来说这个频率刚好匹配舵机的响应速度不需要更快快了舵机也跟不上反而占用更多串口带宽。2.3 STM32端串口解析中断加状态机不要用阻塞延时STM32端如果写成在主循环里死等串口数据会有两个问题一是在执行电机PID计算时串口数据可能已经覆盖缓冲区导致丢帧二是如果通信线松动程序会卡死在等待循环里系统性崩溃。正确的做法是开启USART2接收中断在中断回调里按字节填充一个环形缓冲区主循环再解析帧。// 串口中断回调存入环形缓冲区 void USART2_IRQHandler(void) { uint8_t data; if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { data USART_ReceiveData(USART2); ring_buffer[ring_write_idx] data; ring_write_idx (ring_write_idx 1) % RING_BUFFER_SIZE; } } // 主循环中调用返回1表示解析到一帧完整指令 uint8_t parse_vision_frame(uint8_t *x_out, uint8_t *y_out, uint8_t *lost_out) { static uint8_t state 0; static uint8_t frame[7]; static uint8_t frame_idx 0; while (ring_read_idx ! ring_write_idx) { uint8_t byte ring_buffer[ring_read_idx]; ring_read_idx (ring_read_idx 1) % RING_BUFFER_SIZE; switch (state) { case 0: if (byte 0xAA) { frame_idx 0; frame[frame_idx] byte; state 1; } break; case 1: frame[frame_idx] byte; if (byte 0x04) { state 2; } else { state 0; } break; case 2: frame[frame_idx] byte; if (frame_idx 6) { uint8_t checksum 0; for (uint8_t i 1; i 6; i) checksum frame[i]; if (checksum frame[6]) { *x_out (frame[2] 8) | frame[3]; *y_out (frame[4] 8) | frame[5]; *lost_out (frame[4] 0 frame[5] 0) ? 1 : 0; state 0; return 1; } state 0; } break; } } return 0; }状态机解析的优点是每一帧数据的一个字节出错时不会影响下一帧数据的同步。判断丢目标的标准不是单独依赖lost_out标志而是同时检查x和y是否同时为0这样对异常数据有双重容错。如果帧头0xAA出现在数据段内部怎么办状态机的设计会直接丢弃当前帧并重新同步不产生虚假控制指令。3. 视觉处理与坐标映射从像素坐标到舵机角度3.1 OpenMV的色块识别——先理解LAB阈值分离的原理OpenMV默认的帧尺寸是QVGA320x240图像的坐标系原点在左上角x轴向右y轴向下。很多新手在写跟踪代码时直接用blob.cx()的原始值作为舵机目标值结果会发现云台“反着转”。原因是云台的云台转向舵机装在车体上镜头朝前当目标物向画面右侧移动时云台应该向右转但如果直接反向映射就会越追越偏。正确的做法是归一化处理。把cx除以320得到一个0到1之间的比值再乘以舵机控制量范围。SG90舵机的PWM占空比控制范围一般是500us到2500us对应0到180度但在实际机械结构中云台左右转动的机械限位可能是-90度到90度超过这个范围舵机会堵转。Limit值通常要留5度到10度的余量避免舵机顶着机械限位持续出力而烧毁电机。3.2 云台PID控制公式位置式PID为什么够用云台舵机不需要增量式PID因为舵机本身自带位置反馈电位器它的控制量是目标角度误差直接作用于输出位置式PID即可满足。公式为target_angle_x pid_x.update(center_x - blob.cx()) # 其中 center_x 160是画面中心点这里有一个容易被忽略的点目标角度不是直接等于blob.cx()而是等于画面中心与目标的偏差乘以比例系数。如果目标在画面正中心偏差为零此时云台应该保持当前角度不转。如果把blob.cx()直接当角度目标在正中时云台反而会偏转一个固定角度。我常用的一个经验值是水平方向的P系数取0.2到0.4垂直方向取0.15到0.25。因为云台的垂直舵机受到重力影响如果P值太大目标稍微一动云台就会“点头”振荡。调试时先在OpenMV IDE里打开“直方图”窗口观察目标物体的色块面积如果目标面积少于50像素就应该停止跟踪此时PID会因为噪声输入而产生抖动。加一个面积判断比任何滤波算法都有效。3.3 通过串口调试助手可视化PID曲线不确定PID参数是否合适时不要把OpenMV的数据直接发给STM32而是先在电脑上跑一遍纯Python仿真。OpenMV IDE自带串口终端把调试数据通过USB虚拟串口打印出来在Excel里画一条曲线观察超调量和稳态误差。这个习惯能节省大量联调时间。4. STM32端电机控制从PWM配置到双闭环4.1 定时器配置TIM1产生20ms周期TIM2产生PWM路舵机需要三路PWM信号左右转向、俯仰直流电机需要两路PWM左轮、右轮。SG90舵机的控制信号是50Hz周期即20ms周期脉宽500us对应0度2500us对应180度。STM32的定时器可以分频配置不需要每路都占一个定时器只要频率相同可以共用同一个TIM的多个通道。关键点是舵机PWM和电机PWM的频率不同舵机是50Hz直流电机一般是10kHz到20kHz。10kHz以上的PWM频率听不到电机啸叫而且电流纹波小电机运行更平滑。两者必须用不同的定时器。我一般这样做// 电机PWM初始化频率为10kHz void Motor_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 1000 - 1; // 10kHz TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC2Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }电机PWM占空比写死是不行的需要根据视觉目标的位置动态调整左右轮的差速。追踪小车在目标偏离画面中心时两轮速度应该相反这样车子原地旋转车头始终指向目标。比如目标在画面右侧50像素就让左轮正转、右轮反转速度与偏差成正比。4.2 电机速度闭环编码器只用来做“速度反馈”不做什么传感器融合底盘直流电机如果连编码器都不加用开环PWM驱动会出现一个明显问题电池电量充足时PWM 50%的占空比可以产生较快的转速电池电压降低后同样的占空比转速明显下降。视觉追踪中转速的绝对值不关键关键是左右两轮的转速差开环时即使发了相同的PWM值两个电机因为机械磨损和负载差异实际转速也有差别。所以必须加编码器闭环。常见的做法是STM32的定时器编码器接口模式读取电机AB相脉冲每10ms读取一次计数值通过M法测速得到转速。速度环PID的输出是PWM占空比。光有速度环还不够如果车子的重心偏了或前轮因为摩擦阻力转向速度环无法保证直线行驶这时候需要再加一个“线速度、角速度”的转换关系但这个项目里追踪逻辑足够简单不需要过分复杂的运动学解算。速度环PID的参数整定有一个通用规律比例项加大可以快速消除误差但超过临界值会震荡积分项的主要作用是消除静差但积分饱和在车辆系统中非常危险一个常见的坑是视觉目标丢失时PID的积分项还在累积导致目标重新出现时小车猛冲。解决办法是加积分限幅或者目标丢失时直接清空积分值。下面是一个简化版的增量式PID实现适用于电机速度环int16_t Speed_PID_Calc(int16_t target_speed, int16_t current_speed) { static int16_t previous_error 0; static int16_t integral 0; int16_t error target_speed - current_speed; integral error; if (integral 500) integral 500; if (integral -500) integral -500; int16_t output 8 * error integral / 20 15 * (error - previous_error); previous_error error; return output; }这里的参数是根据典型小型直流电机经验预设的P是8、I是0.05积分分离写法、D是15。注意D项的符号增量式PID中D系数带符号不要抄错。整定时先令I和D为0只调P观察电机是否出现周期性的音调变化那个音调频率就是系统震荡频率把P减小30%再继续调。4.3 目标丢失时的行为策略原地搜寻模式追踪小车必须有目标丢失的处理逻辑。常见做法是目标丢失后云台自动执行一个“扇形扫描”动作云台从-60度扫描到60度STM32在扫描过程中持续接收OpenMV的数据一旦收到有效坐标立即停止扫描并切换到追踪模式。这一策略的坑在于如果扫描速度过快目标从画面边缘掠过时OpenMV来不及识别就会错过了如果太慢目标可能已经从视野外的区域走掉。可接受的扫描速度是每50ms更新一次角度每次增加3度这样一次完整扫描链约2秒。5. 联调与排错按这个顺序上电问题最少5.1 分模块调通的顺序联调最忌讳的是把OpenMV和STM32同时上电然后发现问题不知道查哪边。我习惯的顺序是第一单独给OpenMV供电打开IDE的帧缓冲区窗口确认色块识别框能稳定框住目标第二OpenMV连接电脑USB串口发坐标数据到串口助手确认数据包格式正确第三STM32通过串口接USB转TTL工具用电脑假装OpenMV发固定坐标如(160, 120)看云台是否回正、电机是否停转第四最后再把两者用杜邦线连接。每一步花的时间不超过十分钟但能把问题的责任边界划清楚。5.2 常见故障现象与修复手段串口接好但STM32无响应时先用示波器看PA2和PA3引脚有没有波形。如果没有波形八成是杜邦线接触不良有波形但STM32不动作检查波特率是否与OpenMV端一致。如果云台角度对不上检查定时器重载值STM32的定时器计数频率是72MHz/(预分频1)SG90周期20ms要求计数值是20ms/(1/计数频率)这里有一个很多人踩过的坑——预分频设为71计数频率为1MHz但TIM_Period如果比20000 - 1小的话周期就小于20ms舵机会发热抖动。如果电机不转先量驱动板的VM和VCC电压再量STM32输出的PWM信号。如果电机会转但速度忽快忽慢多半是速度环的PID参数没有收敛把I项先清零P从小的值往上加。5.3 用逻辑分析仪验证PWM输出质量有条件时把逻辑分析仪的通道夹在舵机信号线和电机PWM输出上测量实际频率和脉宽。舵机信号线如果出现不规则的毛刺说明干扰源是电机PWM解决方法是给两组电源单独走线并共地另外在STM32的电源引脚上加100uF电解电容和0.1uF陶瓷电容去耦。6. 进阶玩法让追踪小车脱离“玩具”的四个改进追踪小车跑通之后的下一步常见的做法不是换更强的处理器而是优化数据和控制的四个细节。第一个是改善目标识别算法。find_blobs在颜色接近目标肤色的复杂背景下会把很多非目标的色块也框出来。改用Lab色彩空间的find_blobs其实是低配版方案上电环境变化后阈值需要重新整定。进阶的一种做法是用OpenMV的find_lines结合颜色阈值做边缘检测或者训练一个简单的CNN分类器OpenMV H7支持加载TensorFlow Lite模型但注意这会让帧率下降。若目标固定是特定颜色物块还有一种低成本方案调整镜头的自动增益和自动白平衡关闭时机让颜色阈值在一天内不同时段都精准。第二个是加入环形缓冲区的时间戳机制。现在STM32只是简单解析串口数据并没有记录每帧数据到达的时间。真实物理环境里OpenMV发来的数据帧与STM32执行指令之间存在一个可变的延迟。如果这个延迟超过50ms云台就会出现明显的滞后感。在数据帧中增加一字节的帧序列号STM32端记录当前系统滴答计时用两次连续帧之间的时间差来估计链路延迟再将PID系数乘以一个动态比例因子补偿。这里贴一个简单的延迟补偿思路uint32_t current_tick HAL_GetTick(); uint32_t link_delay current_tick - last_frame_tick; last_frame_tick current_tick; if (link_delay 60) // 超过60ms认为链路拥堵 { // 降低PID的D项系数防止振荡 speed_pid_d_factor 5; }第三个是云台角度平滑。OpenMV的坐标输出在目标稳定时也会有1到2个像素的抖动如果直接换算成舵机角度云台会不断小幅修正。最简单有效的平滑是低通滤波new_angle old_angle 0.3 * (target_angle - old_angle)。这个一阶惯性滤波器的截止频率大约在3Hz左右目标是去掉高频抖动而不引入明显延迟。第四个是用板载LED做状态指示。这是一个调试技巧STM32的一个GPIO连接LED不同闪烁频率表示不同状态——快闪表示目标丢失扫描中慢闪表示正常追踪常亮表示串口通信异常。十几行代码换来的排障效率提升非常明显不用每次出了问题都接串口打日志。最后说一个容易忽略但很重要的细节calc 一个较大的红色面积阈值。OpenMV官方的示例代码里pixels_threshold200是给近距离目标设计的但项目要求是在一米外追踪目标的像素面积会大幅缩小。改成pixels_threshold50并配合area_threshold50能明显推迟丢失目标的临界距离。要把追踪范围做大不要只改色块的“颜色”要同时调整色块的面积阈值、合并相邻色块的参数以及图像缩放的预处理逻辑。本文还有配套的精品资源点击获取