ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

OpenMV与STM32协同的舵机云台PID视觉追踪方案解析

OpenMV与STM32协同的舵机云台PID视觉追踪方案解析 简介这是基于STM32F103与OpenMV视觉模块的二维舵机云台追踪程序包适合做电磁炮、视觉瞄准或低成本追踪云台的电子爱好者与嵌入式开发者。程序主体为32端PID控制逻辑利用OpenMV颜色识别返回色块在x、y轴坐标驱动舵机完成二维实时追踪PID参数需按实际硬件微调舵机精度越高表现越好。包内共455个文件约11.86MB以C源文件151个c和头文件131个h为主同时包含编译过程产生的.o、.crf、.axf、.hex等工程文件以及png图片、txt说明和Keil工程配置uvprojx/uvoptx等目录涵盖标准库、HZ主工程与脚本便于直接查看代码结构。目前已有8233人学习下载适合具备一定STM32与PID基础、希望快速搭建视觉追踪原型的开发者参考。1. 项目定位一套把PID闭环跑在舵机云台上的二维视觉追踪代码调试追踪云台最常遇到的一个现象是色块识别没问题、坐标也送过来了但舵机就是不能把目标稳定锁在画面中心——要么追着目标来回扫要么目标稍微偏一点就整个过冲。这个基于STM32F103和OpenMV的工程恰好把二维追踪链路完整打通了OpenMV在画面里识别色块计算出目标与画面中心的偏移量通过串口发给F103F103跑PID算法输出PWM波驱动水平、垂直两颗舵机让云台实时跟随目标。整套代码的定位不是“能识别就行”而是“能稳定锁住”。适合正在做智能车、机械臂视觉跟随或想搞懂PID实物调试的开发者。你拿到手里可以直接烧录也可以把OpenMV端的色块追踪换成其他识别算法控制端不用动。2. 系统架构与通信协议OpenMV到F103的六字节数据链路这个项目拆开看是“三段式”结构视觉端OpenMV、控制端STM32F103、执行端双舵机云台。视觉端不是每帧都发完整图像而是只发目标偏移量串口负担极小控制端也不做图像处理只专注PID闭环。分离式设计有一个明显好处换识别算法不用动控制代码换舵机不用动视觉代码。2.1 硬件链路两路PWM加一路UART常见硬件选型是MG996R或SG90舵机电源需要5V左右电流能供2A以上最好。云台架通常是两自由度舵机云台水平pan、垂直tilt对应STM32F103的TIM2通道1/通道2一般接PA0和PA1。OpenMV的UART3P4作TX、P5作RX接到F103的USART1PA9是TX、PA10是RXTX/RX交叉对接。信号OpenMVSTM32F103UART3 TXP4PA10 (USART1_RX)UART3 RXP5PA9 (USART1_TX)GNDGNDGND必须共地PAN舵机信号—TIM2_CH1 (PA0)TILT舵机信号—TIM2_CH2 (PA1)舵机电源独立5V电源GND与F103共地同左有一点值得强调舵机供电禁止从OpenMV或F103的3.3V引脚取电舵机启动和堵转时的瞬时电流会直接拉崩主控电压导致串口波形畸变、程序反复复位。独立电源加粗线共地是最省心的接法。2.2 串口帧协议为什么固定六字节最省心协议设计的目标是简单可靠。帧格式固定为0xAA 0x55 Type Dx Dy ChecksumType字段0x01表示有目标0x00表示没有目标。Dx、Dy是目标中心相对于图像中心的偏移归一化到-100~100。Checksum是前面五个字节求和后取低8位。为什么用固定字节而不是JSON或字符串嵌入式里逐字节状态机解析比字符串split省内存、快得多而且OpenMV端发的是原始int不用做格式化转换。归一化还有一个好处OpenMV换分辨率时只需要改一个系数K协议本身不用动。比如320x240画面Dx原始范围是±160除以1.6变成±100换成640x480后K也跟着变但F103端无需感知。2.3 目录结构zip解压后的三段式布局压缩包解压后是典型的“OpenMV端 F103端 文档”三段结构01_OpenMV端main.py是主程序blob_track.py封装色块识别pid_pre.py预留了视觉端预处理的扩展位02_STM32F103端Core/Inc下是main.h、usart.h、pid.h、servo.hCore/Src下是main.c、usart.c、pid.c、servo.c是标准的MDK工程布局03_调参记录pids.md记录了几组实测参数test_log.txt是联调时的串口日志调试时可以直接对照。main.py负责sensor初始化和主循环usart.c里是串口中断接收状态机servo.c负责PWM初始化和角度映射。各部分职责清晰改哪一块都很容易定位。3. 核心代码拆解从色块坐标到舵机PWM的完整闭环这一章看关键代码。整条链路由四段组成OpenMV端识别出目标偏移 → 串口发送 → F103中断解析 → PID计算并驱动舵机。每一段我都拆开讲一遍。3.1 OpenMV端阈值标定、色块识别与偏差打包import sensor, image, time from pyb import UART # LAB色彩空间红色阈值按实际环境用IDE阈值编辑器标定 THRESHOLD (30, 80, 25, 80, -60, -30) sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240帧率优先 sensor.skip_frames(30) sensor.set_auto_gain(False) # 关自动增益 sensor.set_auto_whitebal(False) # 关白平衡 uart UART(3, 115200, timeout_char1000) # 归一化dx范围 -160..160 - -100..100 K 100.0 / (sensor.width() / 2) while True: img sensor.snapshot() blobs img.find_blobs([THRESHOLD], pixels_threshold100, area_threshold200, mergeTrue) if blobs: blob max(blobs, keylambda b: b.area()) img.draw_rectangle(blob.rect()) img.draw_cross(blob.cx(), blob.cy()) dx int((blob.cx() - sensor.width() / 2) * K) dy int((blob.cy() - sensor.height() / 2) * K) type_id 0x01 else: dx dy 0 type_id 0x00 b bytearray([0xAA, 0x55, type_id, dx 0xFF, dy 0xFF, (0xAA 0x55 type_id dx dy) 0xFF]) uart.write(b) time.sleep_ms(20) # 约50Hz的发送节奏这段代码有几个关键决策点。第一find_blobs里的pixels_threshold和area_threshold用来过滤噪声和小色块mergeTrue会把重叠的色块合并避免同一个目标被拆成多块第二max(blobs, keylambda b: b.area())是取最大面积色块作为主目标多目标场景下会一直锁住最大的那个第三关闭自动增益和白平衡很重要否则色块阈值在不同光线下会漂移。参数说明dx为正表示目标在画面中心右侧为负表示在左侧dy为正表示目标在中心下方。归一化后dx、dy的范围是-100~100恰好能塞进一个int8_t串口解析时不会遇到符号扩展问题。无目标时发type0F103收到后会走“丢目标保护”分支不会继续更新舵机角度。3.2 F103端串口中断六字节状态机解析typedef struct { uint8_t state; uint8_t type; volatile int8_t dx; volatile int8_t dy; volatile uint8_t valid; } Frame_t; Frame_t frame {0}; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t byte USART_ReceiveData(USART1); switch (frame.state) { case 0: if (byte 0xAA) frame.state 1; break; case 1: frame.state (byte 0x55) ? 2 : 0; break; case 2: frame.type byte; frame.state 3; break; case 3: frame.dx (int8_t)byte; frame.state 4; break; case 4: frame.dy (int8_t)byte; frame.state 5; break; case 5: if (byte (uint8_t)(0xAA 0x55 frame.type frame.dx frame.dy)) { frame.valid 1; } frame.state 0; break; default: frame.state 0; break; } } }状态机的逻辑每个新字节进入后都在五个状态之间流转任何一帧的字节对不上就回退到state0重新找帧头。dx、dy强转成int8_t是因为OpenMV发送的-100在字节流里是0x9C只有按有符号数解释才能还原成-100。校验和这里有个容易踩的坑中断里计算校验和时frame.dx已经是int8_t负数而OpenMV端计算时dx 0xFF是按无符号数算的。好在加法求模的结果不受符号解释影响只要两端都按“五个字节求和取低8位”这个规则算结果必然一致。这也是这个协议设计得聪明的地方——不需要在F103端做一次无符号还原。3.3 PID计算位置式PID、积分限幅与死区typedef struct { float kp; float ki; float kd; float integral; float last_err; float out_max; // 输出限幅 float dead_zone; // 死区 } PID_TypeDef; float PID_Calc(PID_TypeDef *pid, float measure_err) { if (fabsf(measure_err) pid-dead_zone) { measure_err 0.0f; } pid-integral measure_err; if (pid-integral pid-out_max * 0.3f) pid-integral pid-out_max * 0.3f; if (pid-integral -pid-out_max * 0.3f) pid-integral -pid-out_max * 0.3f; float out pid-kp * measure_err pid-ki * pid-integral pid-kd * (measure_err - pid-last_err); pid-last_err measure_err; if (out pid-out_max) out pid-out_max; if (out -pid-out_max) out -pid-out_max; return out; }这里用的是位置式PID输出直接是“云台这次要转多少角度”方便限幅、方便在日志里观察。为什么不选增量式增量式输出的是每次调整量累加过程放在舵机侧调试时看不到绝对角度丢目标后的状态也很难恢复。位置式把角度累积放在主循环里出问题时一眼能看出当前云台到哪里了。死区dead_zone对舵机云台很有用目标已经接近画面中心时误差可能只有两三个像素换算成角度不到0.1°此时让舵机反复微调只会制造高频抖动。误差小于死区就当作0给出去云台自然静止。积分限幅是防“起飞”的关键。如果目标突然快速移动误差很大积分项会一路累加等目标回到画面中心时积分还在输出一个很大的恢复量云台会冲过头。这里的out_max * 0.3f是把积分作用限制在输出上限的30%以内算一个比较稳妥的初值实际按调试效果调整。3.4 主循环与舵机映射误差怎么变成角度// 方向宏安装方向与图像坐标一致时保持1.0f #define PAN_DIR 1.0f #define TILT_DIR 1.0f while (1) { if (frame.valid) { frame.valid 0; // 丢目标复位PID内部状态避免恢复瞬间跳变 if (frame.type 0x00) { PID_Reset(pid_pan); PID_Reset(pid_tilt); continue; } float err_pan (float)frame.dx * PAN_DIR; float err_tilt (float)frame.dy * TILT_DIR; float out_pan PID_Calc(pid_pan, err_pan); float out_tilt PID_Calc(pid_tilt, err_tilt); pan_angle out_pan; tilt_angle out_tilt; clampf(pan_angle, -90.0f, 90.0f); clampf(tilt_angle, -90.0f, 90.0f); set_servo_angle(SERVO_PAN, pan_angle); set_servo_angle(SERVO_TILT, tilt_angle); } delay_ms(10); }方向宏这里值得单独解释。OpenMV图像坐标系原点在左上角x向右增大dx为正表示目标偏右云台理应向右转。但不同云台的舵机安装方向可能相反有的云台舵机正方向恰好和图像x轴反向此时把PAN_DIR改成-1.0f即可不需要动任何算法代码。这个宏就是给安装方向留的口子。丢目标保护的处理逻辑目标消失时frame.type 0x00直接复位PID内部状态并保留当前角度。如果不清积分目标恢复瞬间会产生一个巨大的误差跳变云台会猛甩一下复位后无缝衔接追踪表现会平滑很多。舵机角度映射的实现void set_servo_angle(uint8_t ch, float angle) { // 0度对应0.5ms脉宽180度对应2.5ms脉宽 float pulse 500.0f angle / 180.0f * 2000.0f; if (pulse 500.0f) pulse 500.0f; if (pulse 2500.0f) pulse 2500.0f; // 定时器预分频后计数频率1MHz1us脉宽1个计数 TIM_SetCompare1(TIM2, (uint16_t)pulse); // ch参数决定用TIM_SetCompare1还是TIM_SetCompare2 }定时器配置72MHz主频预分频71得到1MHz计数频率自动重装值19999即20ms周期对应50Hz这是模拟舵机标准频率。脉宽500us对应0度2500us对应180度角度到脉宽是线性映射。上电时最好先输出一次1500us中位脉宽让云台回到零位再进入主循环。4. 工程落地编译烧录、接线检查与阈值标定完整流程把工程跑起来真正麻烦的不是代码而是环境匹配和联调顺序。下面按我实际操作的顺序过一遍。4.1 环境准备与烧录OpenMV端安装OpenMV IDE打开01_OpenMV端/main.py用USB连接OpenMV点IDE左上角的绿色运行按钮脚本会直接跑在开发板上。F103端打开02_STM32F103端里现成的MDK工程编译后用SWD下载器烧录。SWD只需要四根线3.3V、SWDIO、SWCLK、GND下载速度设置1MHz比较稳太快有时会连不上这是老熟人翻车点。烧录完成后先别接舵机用逻辑分析仪或示波器看PA0和PA1上有没有20ms周期的PWM波确认初始脉宽在1.5ms左右。这一步能排除“代码烧了但PWM没配置对”的情况把问题限定在控制链路之前。4.2 硬件上电点检顺序上电检查按顺序走一遍能省大量排查时间先看OpenMV的电源指示灯亮不亮USB线是否带数据功能再看F103核心板指示灯串口接没接反OpenMV的P4必须接到F103的PA10P5必须接到PA9然后确认舵机独立电源已经供电且GND和F103共地最后确认两根舵机信号线分别接在PA0和PA1。提示第一次联调建议把“数据链”和“控制链”分开验证——先不接舵机只看串口数据正不正确确认没问题再接舵机跑闭环。4.3 阈值标定与串口数据验证OpenMV IDE自带的阈值编辑器是标定这个项目最顺手的工具采集一帧画面框选目标物体工具会自动生成一组LAB阈值把数值填回main.py里的THRESHOLD。标定的时候把自动增益和白平衡关掉代码里已经做了否则换一个光线环境阈值就废了。想在电脑上先看数据是否正常可以把OpenMV的USB口当串口跑一段Python脚本读取和F103端解析的是同一帧数据import serial ser serial.Serial(COM3, 115200, timeout1) while True: data ser.read(6) if len(data) 6 and data[0] 0xAA and data[1] 0x55: dx data[3] - 256 if data[3] 0x80 else data[3] dy data[4] - 256 if data[4] 0x80 else data[4] print(type%d dx%d dy%d chk%d % (data[2], dx, dy, data[5]))在Windows上跑需要先装pyserialLinux下把COM3换成/dev/ttyUSB0。读到的dx、dy应该是平滑变化的如果数值乱跳检查波特率是否一致以及OpenMV和电脑之间是否共地。数据验证通过后再接舵机跑闭环问题定位会快很多。5. 常见问题与避坑抖动、串口乱码与PID发散的排查实录这个章节记录我在调试中反复遇到的五类问题现象、原因、解决办法一条条列清楚调试时可以直接对照。5.1 高频抖动舵机在目标附近不停微颤现象目标静止时云台还在高频咯咯响角度输出曲线在±5度范围内颤抖。原因最大元凶是供电不足。USB的5V带不动舵机瞬时电流舵机每次脉冲都在拉垮电压导致PWM波形畸变其次是Kp调得过大输出随噪声放大。解决先换独立电源验证物理问题把供电问题排除后再把Kp降到0.8左右试一次。如果还抖把Kd直接归零微分环节对噪声最敏感。5.2 目标高速移动时跟不上积分饱和现象目标从画面一端快速移到另一端舵机飞快打到限位还停不下来目标消失后继续走一段才停。原因目标移动期间误差一直很大积分项持续累加超出安全范围后形成了“惯性”等误差变小时积分输出仍然很大。解决在PID_Calc里做积分限幅前述的out_max * 0.3f再把丢目标时的PID_Reset写进主循环。这两招配合后高速移动也能有效收敛。5.3 串口乱码与丢帧现象串口收不到完整帧或者dx、dy出现离谱的大数值。原因多半是OpenMV的TX/RX没有和F103交叉接或者没有共地其次是波特率不一致。还有一类隐蔽情况OpenMV的uart.write发送太频繁缓冲没写完又塞新数据。解决交叉接线、统一115200、GND共地。如果OpenMV端连续发帧时丢数据在uart.write(b)后加一句uart.wait_sent()等待发送完成再进入下一轮循环。5.4 阈值漂移导致误识别现象白天标定好晚上开灯或者阳光一照色块识别就抓空或者抓成别的物体。原因自动增益和白平衡开着阈值自身给得太宽。解决关闭自动增益和白平衡用LAB阈值重新标定阈值尽量收窄到一个区间宁可偶尔跟丢也不要误识别——PID对丢目标有保护逻辑但对误识别没有任何保护。5.5 舵机堵转发热现象调试中云台转到机械限位、舵机发出嗡嗡声外壳明显发热。原因主循环里没有角度限位或者限位范围超过了机械行程。解决把clampf的限位范围改成实际云台的机械行程多数两轴云台在±90度以内上电复位先回中位再开主循环避免上电瞬间跑到极限位置。5.6 PID参数速查表现象优先调整调整方向副作用高频抖动Kp减小响应变慢稳态存在固定偏差Ki增大超调变大来回过冲Kd增大噪声放大静止状态微颤dead_zone增大死区内的微小误差不响应目标消失后猛冲积分限幅收紧大误差下收敛变慢调参顺序固定先P后I再D每次只动一个量。改完一个参数观察至少5秒的追踪效果再动下一个不要同时改两个参数否则出问题根本不知道是哪个改坏的。6. 二次开发目标预测、前馈补偿与三轴扩展硬件链路完全复用代码结构预留了三个改造口按可移植性从低到高排列。6.1 目标预测一阶低通加微分前馈舵机机械响应有滞后目标快速移动时纯PID总是慢半拍。一个性价比很高的改法是在误差进入PID之前加一阶低通平滑再补一个前馈量float alpha dt / (0.10f dt); // tau约0.1秒 err_smooth alpha * (err_raw - err_smooth); float vff (err_smooth - err_last_smooth) / dt * 0.15f; out PID_Calc(pid, err_smooth) vff;时间常数tau0.1s会让误差曲线平滑很多vff对快速移动的目标提前给舵机一个“预判转角”。实测目标做匀速运动时前馈能把跟踪误差缩小一半以上。注意前馈对噪声敏感误差先过平滑否则噪声会被放大。6.2 三轴扩展与协议升级扩展Z轴距离或俯仰时需要把串口帧从6字节改成8字节新增dz字段校验和相应改变。这里有个血泪经验协议设计初期就把type字段的高两位预留给版本号低六位表示状态。这次改帧格式只需要在F103端按版本号走两条解析分支不用所有旧板子跟着报废。F103侧再加一路定时器通道驱动第三颗舵机主循环里多一个PID实例即可。整套代码结构对多路PID的扩展成本很低因为PID_Calc是结构体指针传入不依赖任何全局变量。6.3 让OpenMV直接输出PID如果只想做演示不想在F103里维护PID可以直接把PID_Calc逻辑搬到OpenMV端每帧算完误差后直接输出PWM。代价是闭环频率受视觉帧率限制通常只有20到30fpsF103端跑闭环可以做到100Hz左右动态响应明显更好。如果要拿这个项目练手PID建议保留F103数据链路比视觉端更稳定也更容易用串口日志画误差曲线。从那以后我每次改完PID参数都会顺手把误差曲线用串口打出来先看曲线形态再决定动Kp还是动Ki而不是凭感觉盲调。这个习惯帮我把调试时间几乎砍了一半希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表