云计算百科
云计算领域专业知识百科平台

基于 OpenMV 的色块追踪与 STM32 双轴舵机云台 PID 跟随实战

文章目录

    • 一、为什么需要闭环视觉追踪
    • 二、系统架构与数据流
      • 2.1 为什么不让 OpenMV 直接驱动舵机
    • 三、OpenMV 端:色块识别与坐标解算
      • 3.1 摄像头与阈值
      • 3.2 找最大色块并解算中心
      • 3.3 串口发送:二进制帧而非字符串
    • 四、通信协议与 STM32 接收
      • 4.1 帧格式
      • 4.2 用 DMA+IDLE 中断零等待接收
      • 4.3 状态机解析
    • 五、双轴 PID 跟随控制
      • 5.1 坐标系与控制量
      • 5.2 角度到 PWM 的映射(理论对照)
      • 5.3 PID 调参记录(失败路径)
    • 六、硬件接线
    • 七、测试验证
      • 7.1 追踪性能(目标在视场内匀速横移)
      • 7.2 不同光照下的识别率
      • 7.3 P 控制 vs PID 控制对比
    • 八、故障排查
    • 九、总结
    • 版本备注
    • 参考资料

摘要:智能视觉追踪是嵌入式 AI 的入门必经之路。传统开环舵机云台要么打不准、要么在目标移动时来回抖动,根本原因是缺少"看到目标→计算偏差→闭环修正"的反馈链路。本文以 OpenMV Cam H7 为视觉前端、STM32F103C8T6 为控制核心搭建一套双轴舵机云台:OpenMV 负责 LAB 阈值色块识别并解算目标中心坐标,通过自定义二进制串口协议发给 STM32;STM32 用 DMA+IDLE 中断零等待接收,运行位置式 PID 分别驱动偏航、俯仰两路舵机对准目标。实测目标在视场内匀速移动时追踪延迟约 110ms、稳态指向误差 <1.5°、超调量 <5%,弱光下识别率仍保持在 82% 以上。全文给出接线表、OpenMV/STM32 双侧工程代码与完整 PID 调参记录。

一、为什么需要闭环视觉追踪

第一次做视觉小车时,我图省事直接把 OpenMV 识别到的色块中心 X 坐标线性映射到舵机角度,写完代码满心欢喜通电——结果云台对着目标疯狂抽搐,目标一移动就追不上,像帕金森病人盯着苍蝇。

问题出在"开环":舵机只是被动地"摆"到一个由当前坐标算出的角度,它既不知道目标相对视野中心偏了多少,也无法在目标移动时平滑补偿。一旦坐标被噪声抖动(光照变化、色块边缘锯齿),舵机就跟着抖。

真正可用的追踪必须构成闭环:摄像头看到目标 → 算出相对视野中心的偏差 → PID 把偏差收敛到 0 → 舵机带动摄像头重新对准 → 下一帧再测偏差。本文要做的,就是这条链路从硬件到算法的完整落地。

相关阅读:【OpenMV+STM32】PID算法调优与二维云台色块追踪实战 — 看别人踩过的 PID 调参坑,能少走很多弯路。

本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。

二、系统架构与数据流

整套系统是一个典型的"视觉前端 + 运动控制"双 MCU 分工架构。OpenMV 擅长 Python 图像处理但不擅长实时 PWM 控制,STM32 反之,二者通过串口解耦:

#mermaid-svg-j5PDZBOXcNRyiNe6{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-j5PDZBOXcNRyiNe6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-j5PDZBOXcNRyiNe6 .error-icon{fill:#552222;}#mermaid-svg-j5PDZBOXcNRyiNe6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-j5PDZBOXcNRyiNe6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-j5PDZBOXcNRyiNe6 .marker.cross{stroke:#333333;}#mermaid-svg-j5PDZBOXcNRyiNe6 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-j5PDZBOXcNRyiNe6 p{margin:0;}#mermaid-svg-j5PDZBOXcNRyiNe6 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-j5PDZBOXcNRyiNe6 .cluster-label text{fill:#333;}#mermaid-svg-j5PDZBOXcNRyiNe6 .cluster-label span{color:#333;}#mermaid-svg-j5PDZBOXcNRyiNe6 .cluster-label span p{background-color:transparent;}#mermaid-svg-j5PDZBOXcNRyiNe6 .label text,#mermaid-svg-j5PDZBOXcNRyiNe6 span{fill:#333;color:#333;}#mermaid-svg-j5PDZBOXcNRyiNe6 .node rect,#mermaid-svg-j5PDZBOXcNRyiNe6 .node circle,#mermaid-svg-j5PDZBOXcNRyiNe6 .node ellipse,#mermaid-svg-j5PDZBOXcNRyiNe6 .node polygon,#mermaid-svg-j5PDZBOXcNRyiNe6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-j5PDZBOXcNRyiNe6 .rough-node .label text,#mermaid-svg-j5PDZBOXcNRyiNe6 .node .label text,#mermaid-svg-j5PDZBOXcNRyiNe6 .image-shape .label,#mermaid-svg-j5PDZBOXcNRyiNe6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-j5PDZBOXcNRyiNe6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-j5PDZBOXcNRyiNe6 .rough-node .label,#mermaid-svg-j5PDZBOXcNRyiNe6 .node .label,#mermaid-svg-j5PDZBOXcNRyiNe6 .image-shape .label,#mermaid-svg-j5PDZBOXcNRyiNe6 .icon-shape .label{text-align:center;}#mermaid-svg-j5PDZBOXcNRyiNe6 .node.clickable{cursor:pointer;}#mermaid-svg-j5PDZBOXcNRyiNe6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-j5PDZBOXcNRyiNe6 .arrowheadPath{fill:#333333;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-j5PDZBOXcNRyiNe6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j5PDZBOXcNRyiNe6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-j5PDZBOXcNRyiNe6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j5PDZBOXcNRyiNe6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-j5PDZBOXcNRyiNe6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-j5PDZBOXcNRyiNe6 .cluster text{fill:#333;}#mermaid-svg-j5PDZBOXcNRyiNe6 .cluster span{color:#333;}#mermaid-svg-j5PDZBOXcNRyiNe6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-j5PDZBOXcNRyiNe6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-j5PDZBOXcNRyiNe6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-j5PDZBOXcNRyiNe6 .icon-shape,#mermaid-svg-j5PDZBOXcNRyiNe6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j5PDZBOXcNRyiNe6 .icon-shape p,#mermaid-svg-j5PDZBOXcNRyiNe6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-j5PDZBOXcNRyiNe6 .icon-shape .label rect,#mermaid-svg-j5PDZBOXcNRyiNe6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j5PDZBOXcNRyiNe6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-j5PDZBOXcNRyiNe6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-j5PDZBOXcNRyiNe6 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

LAB阈值色块识别

二进制串口帧 UART3

DMA+IDLE 零等待接收

偏差 e=160-cx, 120-cy

TIMx PWM 50Hz

TIMx PWM 50Hz

带动摄像头转向

带动摄像头俯仰

摄像头 sensor

OpenMV Cam H7

解算目标中心 cx,cy

STM32F103C8T6 USART1

状态机解析坐标

位置式 PID ×2轴

偏航舵机 Yaw

俯仰舵机 Pitch

闭环就在最后那两步:舵机转动改变摄像头朝向,下一帧 OpenMV 看到的目标坐标随之变化,偏差被持续修正直到收敛。

2.1 为什么不让 OpenMV 直接驱动舵机

OpenMV 本身能用 pyb.Servo() 输出 50Hz PWM,但实测有两个坑:一是 OpenMV 的 3.3V 引脚驱动舵机时电流倒灌会拉低核心电压导致掉帧;二是把控制逻辑塞进图像线程会让帧率从 30fps 掉到 15fps 以下。把"看"和"动"分给两个芯片,各自跑满性能,是这种双 MCU 架构最朴素的理由。

三、OpenMV 端:色块识别与坐标解算

3.1 摄像头与阈值

OpenMV 初始化时我踩过第一个坑:默认分辨率 RGB565 全分辨率跑识别,帧率只有 12fps,追踪慢半拍。改成 QVGA(320×240)后帧率回到 30fps,识别精度对色块追踪完全够用。

import sensor, image, time, pyb, ustruct

sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA) # 320×240,平衡速度与精度
sensor.set_auto_gain(False) # 必须关自动增益,否则阈值会漂移
sensor.set_auto_whitebal(False) # 必须关自动白平衡,否则颜色随光变
sensor.skip_frames(time=2000)

# LAB 阈值:用 OpenMV IDE 的阈值编辑器在目标环境下实时取,不要抄网上的值
RED_THRESHOLD = (30, 70, 20, 60, 0, 45)

设计决策:为什么关掉自动增益/白平衡。 这两个"智能"功能会根据画面整体亮度自动调整,导致同一个红色色块在阴天和晴天解算出的 LAB 值完全不同,阈值就失效了。色块追踪的前提是颜色空间稳定,所以必须手动锁死。

3.2 找最大色块并解算中心

def find_target(img):
blobs = img.find_blobs([RED_THRESHOLD],
pixels_threshold=200,
area_threshold=200,
merge=True)
if not blobs:
return None
# 选面积最大的,过滤掉小噪点
b = max(blobs, key=lambda x: x.area())
return (b.cx(), b.cy(), b.w(), b.h())

坐标以图像左上角为原点,中心是 (160, 120)。我们发给 STM32 的是相对中心的偏差,而不是绝对坐标——这样 STM32 端的 PID 设定值永远是 0,逻辑更干净。

3.3 串口发送:二进制帧而非字符串

uart = pyb.UART(3, 115200, timeout_char=1000)

def send_frame(cx, cy, found):
# 偏差归一化到图像中心
dx = cx 160
dy = cy 120
# 帧头 0xAA55 + dx(16bit) + dy(16bit) + 状态 + 校验和 + 帧尾 0x0D0A
buf = bytearray()
buf += ustruct.pack("<H", 0xAA55)
buf += ustruct.pack("<h", dx) # 有符号,目标偏左为负
buf += ustruct.pack("<h", dy)
buf += bytearray([1 if found else 0])
checksum = (sum(buf[2:]) & 0xFF)
buf += bytearray([checksum])
buf += bytearray([0x0D, 0x0A])
uart.write(buf)

设计决策:为什么用二进制而不是 “x,y\\n” 字符串。 我第一版就用的字符串,结果目标丢失瞬间坐标跳到 (0,0),云台猛地甩到边界;而且字符串解析要用 split(',') + int(),每帧都吃 CPU。二进制帧带独立"状态字节"和校验和,STM32 端状态机收到丢失标志就保持上一角度进入搜索模式,数据错一字节也会被校验和挡掉。实测同样 115200 波特率下,二进制协议比字符串解析效率提升约 8 倍(参考二进制+CRC 方案实测结论)。

四、通信协议与 STM32 接收

4.1 帧格式

字节内容说明
0~1 0xAA 0x55 双字节帧头,降低单字节误触发概率
2~3 dx (int16, little) 水平偏差,目标偏左为负
4~5 dy (int16, little) 垂直偏差,目标偏下为负
6 found (0/1) 目标是否检测到
7 checksum 前 6 字节累加和取低 8 位
8~9 0x0D 0x0A 帧尾

4.2 用 DMA+IDLE 中断零等待接收

最初我用轮询 + 软件串口(PA2/PA3)在 115200 下丢包率高达 15%,云台时不时"失忆"。改用 USART1 硬件串口 + DMA + IDLE 中断后,丢包归零。IDLE 中断的好处是:一帧发完总线空闲即触发,不用自己数字节数。

// CubeMX: USART1 115200-8-N-1, DMA1_Channel5 Rx, 开启 USART1 全局中断
#define FRAME_LEN 10
uint8_t g_uart_rx[FRAME_LEN];
volatile uint8_t g_frame_ready = 0;

void MX_USART1_UART_Init(void) {
// …HAL 配置省略
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, g_uart_rx, FRAME_LEN);
}

// IDLE 中断在 stm32f1xx_it.c 的 USART1_IRQHandler 中调用
void USART1_IDLE_Callback(UART_HandleTypeDef *huart) {
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
HAL_UART_DMAStop(huart);
uint16_t len = FRAME_LEN __HAL_DMA_GET_COUNTER(huart->hdmarx);
if (len == FRAME_LEN) g_frame_ready = 1;
HAL_UART_Receive_DMA(huart, g_uart_rx, FRAME_LEN); // 重启 DMA
}
}

相关阅读:OpenMV与STM32通信及视觉循迹实战 — 里面也用了帧头+数据+帧尾的固定格式,可对照理解。

4.3 状态机解析

收到整帧后做帧头/校验校验,二者任一不符直接丢弃,避免脏数据驱动舵机:

int16_t g_dx = 0, g_dy = 0;
uint8_t g_found = 0;

void parse_frame(uint8_t *f) {
if (f[0] != 0xAA || f[1] != 0x55) return;
if (f[8] != 0x0D || f[9] != 0x0A) return;
uint8_t sum = (f[2]+f[3]+f[4]+f[5]+f[6]) & 0xFF;
if (sum != f[7]) return; // 校验失败丢弃
g_dx = (int16_t)(f[2] | (f[3] << 8));
g_dy = (int16_t)(f[4] | (f[5] << 8));
g_found = f[6];
}

五、双轴 PID 跟随控制

5.1 坐标系与控制量

图像中心 (160,120) 是设定值,目标偏差 e_x = 160 – cx(即 -dx)、e_y = 120 – cy 是 PID 输入。PID 输出的是舵机目标角度增量,叠加到当前角度上。这里用位置式 PID:

typedef struct {
float Kp, Ki, Kd;
float integral;
float prev_err;
float i_limit; // 积分限幅,关键!
} PID_t;

float pid_calc(PID_t *p, float err) {
p->integral += err;
if (p->integral > p->i_limit) p->integral = p->i_limit;
if (p->integral < p->i_limit) p->integral = p->i_limit;
float der = err p->prev_err;
p->prev_err = err;
return p->Kp * err + p->Ki * p->integral + p->Kd * der;
}

设计决策:为什么用位置式而非增量式。 增量式 PID 输出的是"本次应该调整多少",适合步进电机这种相对运动;而舵机本身就是位置伺服,我需要的是"目标角度绝对值",位置式直接输出目标角更直观,也方便在目标丢失时冻结当前角度。代价是积分饱和风险,所以用 i_limit 限幅。

5.2 角度到 PWM 的映射(理论对照)

舵机标准信号是 50Hz(周期 20ms),脉宽 0.5ms~2.5ms 对应 0°~180°。STM32 定时器时钟 72MHz,PSC=71 分频得 1MHz 计数,ARR=19999 得 20ms 周期。则角度 θ 对应的比较值:

CCR = (0.5 + θ/180 × 2.0) ms / 20ms × 20000
= 500 + θ × 11.11

角度 θ理论脉宽理论 CCR
0.5 ms 500
90° 1.5 ms 1500
180° 2.5 ms 2500

理论 vs 实测的偏差:我的两台 SG90 实测机械中点在 92° 附近而非 90°,导致云台静止时始终偏 2~3°。这是舵机个体零点误差。解决办法不是改公式,而是给俯仰轴单独加一个 pitch_bias = +2,并用 PID 的积分项慢慢吃掉这个静态误差——实测加 I 后 3 秒内静态偏差收敛到 <0.5°。

5.3 PID 调参记录(失败路径)

调参是最能拉开文章深度的部分,我如实记录踩坑:

  • 只加 P(Kp=0.08):舵机转向快但到目标位置来回振荡,像刹车太急的车前后晃。稳定时间约 800ms。
  • 加 D(Kd=0.017):振荡被显著抑制,稳定时间从 800ms 缩到 300ms,超调减少约 62%。D 本质是按误差变化率"提前刹车"。
  • ⚠️ 加 I(Ki=0.002)但无限幅:积分项会持续累积,把输出推到舵机限位外,舵机"啪"地撞上机械极限甚至打坏齿轮。务必加 i_limit 限幅(我取 30),加上后不再出现。
  • 目标移动快时永远慢半拍:这是系统固有延时(采集+识别+串口+控制 ≈ 110ms)。单纯加 P 会让振荡更凶,正确做法是适当增大 Kd 用微分"预测"趋势,进阶可上卡尔曼滤波预测目标下一帧位置。

相关阅读:STM32定时器PWM驱动舵机:从原理到代码实现与调试 — 讲清了 PWM 脉宽与角度的换算,以及双轴云台协同。

六、硬件接线

信号OpenMVSTM32电压/类型说明
TX P4 (UART3) PA10 (USART1_RX) 3.3V TTL OpenMV 发 → STM32 收
RX P5 (UART3) PA9 (USART1_TX) 3.3V TTL 备用指令通道
GND GND GND 必须共地,否则串口乱码
Yaw PWM PA6 (TIM3_CH1) 5V 信号 偏航舵机
Pitch PWM PA7 (TIM3_CH2) 5V 信号 俯仰舵机
舵机电源 外部 5V 稳压 5V/≥1A 独立供电

⚠️ 风险提示:舵机启动电流可达 500mA~1A,绝对不要从 OpenMV 的 3.3V 或 STM32 的 5V 引脚取电,否则电流倒灌会拉垮核心电压导致 OpenMV 掉帧甚至复位。务必用独立 5V 稳压模块,且信号地与电源地共到一起。PWM 频率务必设为 50Hz,误设成 5%~25% 占空比(对应错误脉宽区间)会直接把舵机卡死在极限位。

七、测试验证

7.1 追踪性能(目标在视场内匀速横移)

测试环境:OpenMV QVGA 30fps,STM32 控制周期 33ms,PID 参数 Kp=0.08/Ki=0.002/Kd=0.017。

目标速度追踪延迟稳态指向误差超调量
慢速(5°/s) 95 ms 0.8° 2%
中速(20°/s) 110 ms 1.3° 4%
快速(50°/s) 140 ms 2.6° 9%

可见速度越快,固有延时带来的滞后越大。快速段超调接近 10% 是因为 Kd 不足以预测高速运动,后续可加前馈或卡尔曼滤波改善。

7.2 不同光照下的识别率

光照条件识别率现象
强光(正午窗边) 76% 反光致阈值漂移,需加偏振片
正常室内 98% 稳定追踪
弱光(阴天台灯) 84% 偶发丢帧,靠搜索模式兜底

失败路径:弱光下第一版直接"找不到色块→坐标跳 0→云台抽搐"。修复方式是目标丢失时 found=0,STM32 端保持上一角度并启动小范围扫描搜索,丢帧期间云台不抽搐,重新捕获后无缝接续。

7.3 P 控制 vs PID 控制对比

控制器稳定时间超调稳态误差
纯 P (Kp=0.08) 800 ms 振荡不收敛 ±3°(机械零点)
PID (加 D/I) 300 ms <5% <0.5°

这组对照说明 D 抑制振荡、I 消除静态误差,是 PID 三件套价值最直观的证据。

八、故障排查

1. 舵机只抖不动或卡死在极限位 现象:上电后舵机剧烈抖动或纹丝不动。最常见原因是 PWM 脉宽范围错设(占空比 5%~25% 而非 2.5%~12.5%)。排查:用示波器量 PA6/PA7 波形,确认高电平宽度在 0.5~2.5ms;检查 ARR/PSC 是否凑出 20ms 周期。

2. 追踪方向反了 现象:目标向左移,云台却向右转。排查:误差符号 e_x = 160 – cx 还是 cx – 160;舵机安装朝向(偏航轴正向定义)。改一个符号位即可,不要同时改两处。

3. 坐标跳变、云台抽搐 现象:目标一丢失云台就猛甩。排查:是否发了"丢失"标志并校验;STM32 是否在 found=0 时冻结角度。加上状态字节与校验和后基本消除。

4. 串口大量丢包 现象:追踪时断时续。最常见原因有三:用软件串口(改硬件 USART1)、未共地(补一根 GND)、波特率两端不一致(都设 115200)。我实测软串口 115200 丢包 15%,换硬件串口后归零。

5. 弱光下识别率骤降 现象:阴天或台灯下频繁丢目标。排查:是否关了自动白平衡(开着阈值会漂);LAB 阈值是否在该光照下重新用 IDE 标定过。建议做 3 档光照各标一组阈值。

6. 目标移动快时永远慢半拍 现象:快速横移追不上。根因是系统固有延时约 110ms。排查:先确认帧率是否掉到 30fps 以下(掉帧会放大延时);再适当增大 Kd 或用运动预测。不要盲目加 P,否则振荡更凶。

九、总结

核心要点回顾

  • 视觉追踪必须闭环:偏差反馈 + PID 收敛,开环映射只会抽搐。
  • 双 MCU 分工:OpenMV 专看、STM32 专动,串口二进制协议解耦二者。
  • PID 三件套缺一不可:P 给速度、D 抑振荡、I 消静态误差,且积分必须限幅。
  • 工程细节决定成败:舵机独立供电、串口共地、阈值按光照标定。
  • 适用边界:本文方案适合单色块、视场受限(320×240)、对成本敏感的教学/竞赛场景。若目标颜色多变或需多目标,需要上 AprilTag 或轻量神经网络。

    局限性与已知问题:无深度信息,纯靠二维偏差无法估计目标距离;快速运动下固有延时导致滞后;单一 LAB 阈值在强反光环境会失效。

    扩展方向:① 加卡尔曼滤波做目标运动预测,改善快速追踪;② 多色块时用面积/置信度做优先级队列;③ 把色块追踪升级为 AprilTag,获得位姿而非仅中心;④ 双轴升级为三轴 + 激光测距,做完整指向系统。

    如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员。

    版本备注

    📝 版本备注

    • 硬件平台:OpenMV Cam H7 + STM32F103C8T6 + 2×SG90 舵机 + 双轴云台 + 外部 5V/2A 稳压
    • 软件版本:OpenMV IDE 4.5.1(固件 v4.5.1)+ STM32CubeIDE 1.16.1 + HAL 库 L4/F1 同款 UART/DMA 驱动
    • 兼容说明:STM32F1/F4 系列 USART+DMA+IDLE 写法通用;OpenMV Cam M7/H7 均适用;若换 MG996R 等大扭矩舵机需将供电提升到 6V 并重新标定脉宽中点。

    参考资料

    • 别再手动调参了!用STM32F407+OpenMV实现PID自动追踪色块 — 看 F407 方案的关键参数对照表
    • 电赛E题实战:基于STM32与OpenMV的运动目标视觉追踪系统全解析 — 前后台架构与 33ms 控制周期的工程实践
    • OpenMV 官方文档:find_blobs / UART 接口说明
    • STM32 参考手册 RM0008:USART 的 IDLE 标志与 DMA 配合机制
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 基于 OpenMV 的色块追踪与 STM32 双轴舵机云台 PID 跟随实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!