☰
OpenMV与STM32双芯片追踪云台:PID控制与串口通信实战
2026/10/20 4:39:19 网站建设 项目流程

简介:这份资源面向参加2023年全国大学生电子设计竞赛E题(运动目标控制与自动追踪系统)的本科组选手及嵌入式学习者,聚焦红色光斑位置控制与绿色光斑自动追踪的云台方案实现。压缩包共2个文件,以rar工程归档与py脚本为主,前者承载STM32云台控制代码,后者对应OpenMV视觉识别与色块追踪逻辑,整体约294KB,体积轻便便于快速部署与二次开发。资源围绕1米外白色屏幕上直径不超过1cm的红绿光斑展开,涉及二维电控云台驱动、视觉坐标解算与追踪闭环等关键环节,可帮助读者理解赛题结构并搭建基础验证平台。目前已有9053人学习下载,适合需要参考完整赛题实现思路、对照调试视觉与云台联动的中高级嵌入式开发者。

1. 从一块追着目标跑的云台说起:这套方案到底能干什么

如果你正在准备电子设计大赛,或者想找一个能同时练 OpenMV 图像处理和 STM32 实时控制的完整项目,这套追踪云台方案值得拆开看。它的核心逻辑很直接:OpenMV 负责“看”,识别画面里的色块或目标,把坐标偏差通过串口发给 STM32;STM32 负责“动”,用 PID 算法驱动两个舵机,让云台始终把目标压在画面中心。整套东西不依赖上位机,上电就能独立跑,属于典型的嵌入式视觉闭环系统。

它适合三类人:一是备赛学生,需要一套可复现、可调参的追踪框架;二是刚接触 OpenMV 和 STM32 联调的开发者,想搞懂图像坐标到舵机角度的映射链路;三是做云台、机器人视觉跟踪的爱好者,想拿一个能跑通的基线工程继续改。下面按“能干什么 → 怎么跑 → 怎么改”的顺序,把这份资源拆透。

2. 系统架构与硬件链路:OpenMV 和 STM32 到底怎么分工

2.1 双芯片架构的职责划分

这套方案最容易被新手搞混的地方,是“谁算、谁控”。常见做法是让 OpenMV 只做视觉前端,输出目标在画面中的像素坐标和尺寸;STM32 拿到坐标后,结合画面中心点算出偏差,再跑 PID 输出 PWM 给舵机。这样分工的好处是 OpenMV 的算力全部留给图像处理,STM32 的定时器和中断资源全部留给实时控制,两边都不憋屈。

如果反过来让 OpenMV 直接驱动舵机,帧率一波动,舵机就抖得厉害,这是血泪经验。OpenMV 的find_blobs单帧耗时在几毫秒到十几毫秒之间浮动,直接控舵机会把图像处理的抖动传导到机械结构上。所以架构上必须把“感知”和“控制”拆开,STM32 以固定周期跑 PID,OpenMV 异步更新目标坐标。

硬件链路大致是:

模块型号/接口作用
视觉模块OpenMV Cam色块识别、坐标输出
主控STM32F103/F407PID 运算、PWM 输出
执行二自由度舵机云台水平/俯仰追踪
通信UART 串口OpenMV → STM32 数据帧
供电独立 5V/6V舵机与主控分开供电

注意:舵机供电一定要和 STM32 分开,共地但不共电源。舵机堵转瞬间的电流跌落会把 STM32 拉复位,这个坑几乎每届比赛都有人踩。

2.2 串口通信协议设计

OpenMV 和 STM32 之间传什么、怎么传,决定了整套系统的稳定性。常见做法是定义一个定长帧,帧头 + 数据 + 帧尾校验。比如用0xAA 0xBB做帧头,后面跟目标 x 坐标、y 坐标、目标宽度,最后跟一个校验字节。

# OpenMV 端:打包目标坐标并通过串口发送 import struct from pyb import UART uart = UART(3, 115200) # 使用 UART3,波特率 115200 def send_target(cx, cy, w): # 帧头 0xAA 0xBB,数据为大端短整型,帧尾 0xCC frame = struct.pack('>BBhhhB', 0xAA, 0xBB, cx, cy, w, 0xCC) uart.write(frame)

这段代码里struct.pack的格式字符串>BBhhhB含义是:大端序,两个无符号字节帧头,三个短整型(x、y、宽度),一个无符号字节帧尾。用大端是为了和 STM32 端解析保持一致,避免字节序对不上导致坐标乱飞。波特率选 115200 是因为 OpenMV 默认串口在高速下更稳,太低会拖慢帧率。

STM32 端接收时,常见做法是用串口空闲中断 + DMA,把一帧数据收完再解析,而不是在接收中断里逐字节判断。逐字节判断容易在帧头误匹配时把整帧丢掉,空闲中断能保证一帧完整到达后再处理。

// STM32 端:串口空闲中断回调中解析帧 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (rx_buf[0] == 0xAA && rx_buf[1] == 0xBB && rx_buf[7] == 0xCC) { target_x = (int16_t)((rx_buf[2] << 8) | rx_buf[3]); target_y = (int16_t)((rx_buf[4] << 8) | rx_buf[5]); target_w = (int16_t)((rx_buf[6] << 8) | rx_buf[7 - 1]); target_updated = 1; // 标记新数据到达 } HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, RX_BUF_SIZE); }

这里target_updated标志位很关键。PID 控制循环在主循环里以固定周期跑,只有新数据到达时才更新目标值,避免用旧数据反复计算导致积分饱和。HAL_UARTEx_ReceiveToIdle_DMA是 STM32 HAL 库提供的空闲中断 DMA 接收函数,收完一帧自动触发回调,比手动管理缓冲区省心得多。

2.3 云台机械结构与舵机选型

二自由度云台一般用两个舵机:一个负责水平旋转(偏航),一个负责俯仰。常见做法是选用金属齿轮舵机,扭矩至少 15kg·cm 以上,因为 OpenMV 摄像头加支架有一定重量,塑料齿轮舵机容易扫齿。供电用 6V 独立电源,和 STM32 的 3.3V/5V 分开。

机械安装上,俯仰舵机装在偏航舵机上,摄像头固定在俯仰舵机输出轴上。安装时尽量让摄像头光轴和俯仰舵机旋转轴重合,否则画面中心和实际指向会有偏差,PID 调起来会一直有静差。这个细节在比赛现场没时间慢慢调,最好在搭建阶段就用水平仪对好。

3. 从零跑通:OpenMV 识别与 STM32 PID 控制实操

3.1 OpenMV 端色块识别与坐标输出

OpenMV 的色块识别核心是find_blobs,但直接调用默认参数很容易翻车。常见做法是先做阈值校准,用 IDE 里的阈值编辑器取 LAB 空间的阈值,然后设置最小像素面积和合并边缘。

import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240,兼顾帧率和精度 sensor.skip_frames(time=2000) # LAB 阈值,需根据实际目标颜色校准 red_threshold = (30, 100, 15, 127, 15, 127) uart = UART(3, 115200) while True: img = sensor.snapshot() blobs = img.find_blobs([red_threshold], pixels_threshold=100, # 最小像素面积 area_threshold=100, merge=True) # 合并相邻色块 if blobs: # 取最大色块作为目标 largest = max(blobs, key=lambda b: b.pixels()) img.draw_rectangle(largest.rect(), color=(0, 255, 0)) img.draw_cross(largest.cx(), largest.cy()) send_target(largest.cx(), largest.cy(), largest.w()) else: send_target(-1, -1, 0) # 丢失目标时发送无效坐标

pixels_threshold=100表示忽略小于 100 像素的色块,能过滤掉大部分噪点。merge=True把相邻的色块合并成一个,避免目标被分割成多块时坐标跳变。丢失目标时发送-1是常见做法,STM32 收到无效坐标后保持当前角度或缓慢回中,而不是猛追噪声。

帧率方面,QVGA 下find_blobs大概能跑到 25~30 FPS,足够舵机追踪。如果目标颜色和背景接近,可以缩小 ROI 区域只处理画面中心部分,帧率还能再提。

3.2 STM32 端 PID 控制器实现

PID 是这套系统的控制核心。常见做法是用位置式 PID,输入是目标坐标和画面中心的偏差,输出是舵机 PWM 的增量。

// 位置式 PID 结构体 typedef struct { float Kp, Ki, Kd; float integral; float prev_error; float integral_limit; // 积分限幅,防止饱和 float output_limit; // 输出限幅 } PID_TypeDef; float PID_Compute(PID_TypeDef *pid, float error) { pid->integral += error; // 积分限幅,这是防超调的关键 if (pid->integral > pid->integral_limit) pid->integral = pid->integral_limit; else if (pid->integral < -pid->integral_limit) pid->integral = -pid->integral_limit; float derivative = error - pid->prev_error; pid->prev_error = error; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; // 输出限幅 if (output > pid->output_limit) output = pid->output_limit; if (output < -pid->output_limit) output = -pid->output_limit; return output; }

integral_limit和output_limit这两个限幅是调参时最容易忽略的。没有积分限幅,目标丢失几秒后积分项会累积到很大,重新捕获目标时舵机会猛冲过去,俗称“积分爆炸”。输出限幅则限制单次 PWM 增量,防止舵机被大误差直接打满。

PID 参数整定顺序一般是先 P 后 I 再 D。先把 Ki 和 Kd 置零,调 Kp 到云台能快速响应但不过冲;然后加 Ki 消除静差,但 Ki 太大会震荡;最后加 Kd 抑制超调,D 太大会放大噪声。比赛现场常见做法是先用一组保守参数跑通,再根据实际追踪效果微调。

3.3 主循环调度与舵机 PWM 输出

STM32 主循环里,PID 计算和舵机 PWM 更新要放在固定周期里跑,常见做法是用定时器中断触发,周期 10ms 左右。周期太短舵机响应不过来,太长追踪会有明显延迟。

// 定时器中断回调,10ms 周期 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim == &htim2) { if (target_updated) { // 计算偏差:目标坐标 - 画面中心 float err_x = (float)(target_x - 160); // QVGA 中心 x=160 float err_y = (float)(target_y - 120); // QVGA 中心 y=120 float out_x = PID_Compute(&pid_yaw, err_x); float out_y = PID_Compute(&pid_pitch, err_y); // 更新舵机 PWM 比较值 servo_yaw_angle += out_x; servo_pitch_angle += out_y; // 角度限幅,防止舵机撞限位 servo_yaw_angle = clamp(servo_yaw_angle, 0, 180); servo_pitch_angle = clamp(servo_pitch_angle, 0, 180); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, angle_to_pwm(servo_yaw_angle)); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, angle_to_pwm(servo_pitch_angle)); target_updated = 0; } } }

angle_to_pwm是把 0~180 度映射到定时器比较值的函数,常见做法是 50Hz PWM,周期 20ms,0.5ms~2.5ms 脉宽对应 0~180 度。clamp函数做角度限幅,防止舵机转到机械限位后堵转烧毁。这个限幅值要根据实际云台结构测出来,不要直接写 0~180,很多云台实际可用范围只有 30~150 度。

4. 避坑与排查:追踪云台最常见的 5 个翻车现场

4.1 云台抖动严重,目标静止时也在晃

现象是目标不动,云台却持续小幅抖动。原因通常是 PID 的 D 项太大,把图像噪声放大了;或者 OpenMV 帧率不稳定,坐标更新周期和 PID 周期不同步。解决方法是先降低 Kd,再在 STM32 端对目标坐标做滑动平均滤波,比如取最近 5 帧坐标的中位数。另外检查 OpenMV 是否开了自动增益和自动白平衡,这两个功能会让画面亮度变化,导致色块识别不稳定,建议手动锁定。

4.2 目标丢失后重新捕获时舵机猛冲

这是典型的积分饱和。目标丢失期间,PID 的积分项持续累积,重新看到目标时积分项已经很大,输出直接打满。解决方法是在丢失目标时清空积分项,或者给积分项加限幅。常见做法是收到无效坐标时把pid->integral置零,同时让舵机保持当前角度而不是继续积分。

4.3 串口数据错位,坐标偶尔乱跳

现象是云台偶尔猛转一下又回来。原因多半是串口帧解析没有做帧头校验,或者 DMA 接收缓冲区被覆盖。解决方法是严格校验帧头和帧尾,帧头不匹配直接丢弃整帧;同时用空闲中断确保一帧完整接收后再解析。如果波特率高于 115200,还要检查 STM32 时钟配置是否准确,时钟偏差会导致累积误差。

4.4 舵机和 STM32 共电源导致复位

现象是舵机一动 STM32 就重启。原因是舵机启动电流大,把共用电源电压拉低到 STM32 的欠压复位阈值以下。解决方法是舵机用独立电源供电,STM32 和舵机只共地不共电源。如果必须共电源,要在舵机电源端并一个大电容(1000uF 以上)做缓冲。

4.5 色块识别受光照影响大,换个场地就失效

现象是实验室调好的阈值,到比赛场地就识别不到。原因是 LAB 阈值对光照敏感。解决方法是尽量用自动阈值或动态阈值,或者在 OpenMV 端加一个按键校准功能,现场重新取阈值。另一个技巧是改用 AprilTag 或圆形识别,对光照鲁棒性比色块强,但需要目标上贴标记。

5. 二次开发与调参技巧:把这套云台改成你自己的

拿到这套工程后,最值得先改的是目标识别方式。色块识别适合颜色固定的场景,但如果你的目标颜色会变,可以换成 AprilTag 识别,OpenMV 官方例程里有现成的find_apriltags,输出的是标签的 3D 姿态,比色块坐标更稳定。改的时候只需要替换 OpenMV 端的识别函数,串口协议和 STM32 端完全不用动,这也是双芯片架构的好处——感知和控制解耦,换传感器不影响控制层。

第二个改造点是 PID 参数的自整定。手动调参在比赛现场很耗时,可以加一个简单的继电反馈自整定:让系统在等幅振荡下运行,根据振荡周期和幅值算出临界增益,再用 Ziegler-Nichols 公式算 Kp、Ki、Kd。代码上只需要在 STM32 里加一个自整定模式,用按键触发,跑完后把参数存到 Flash 里。

第三个改造点是增加前馈控制。如果目标运动有规律(比如匀速直线),可以在 PID 输出上叠加一个速度前馈项,用目标坐标的差分作为速度估计,乘以一个前馈系数直接加到输出上。这样追踪运动目标时滞后会小很多,PID 只需要处理残差。

改造方向改动位置难度效果
换 AprilTag 识别OpenMV 端低抗光照干扰
PID 自整定STM32 端中省去手动调参
速度前馈STM32 端中减少追踪滞后
双目标切换OpenMV + STM32高多目标追踪

调参时我一般会先把云台固定,用串口打印目标坐标和 PID 输出,在电脑上画曲线看响应。只看云台动作很难判断是 P 大了还是 D 大了,有曲线一目了然。另外每次改完参数都要重新测一遍丢失目标再捕获的场景,这个场景最容易暴露积分饱和问题。

从那以后我每次调完 PID 都强制走一遍“丢失-重捕获”测试,确认积分项被正确清零才收工。希望这套拆解能帮你少走点弯路,把云台跑起来。

本文还有配套的精品资源,点击获取

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

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

立即咨询