简介:一份基于STM32F103C8T6与OpenMV的色块追踪云台毕业设计源码及项目说明;系统由STM32主控实时解析OpenMV串口发送的色块坐标,采用PID算法计算偏差,精准控制双舵机云台实现目标跟踪。文档方案对舵机脉冲角度换算做了详细说明,例如0.5ms对应0°、2.5ms对应180°,并定义了串口数据帧格式,OpenMV端通过调用色块追踪库函数获取目标坐标后周期上传。资源共1009个文件,包含561个C源文件与245个H头文件,还有汇编启动文件、IAR/Keil/MDK工程配置、文本说明、OpenMV脚本、链接映射文件等,整体约24.65MB,目录结构清晰,便于按模块查找。整套代码可直接作为课程设计、毕业设计或电子设计竞赛的参考模板,能帮助读者掌握嵌入式主控与视觉模块协同工作的完整链路,包括串口通信、PID闭环控制、舵机驱动等常见工程环节。目前已有982人学习下载。 坐标(160,120),目标偏右上,偏航角加15度,俯仰角降8度……如果你正在折腾毕业设计,大概率已经和这段画面打过照面了。基于STM32和OpenMV的色块追踪云台,算是机器视觉入门里最经典也最能打的组合:OpenMV负责“看”,STM32负责“动”,一套下来既覆盖了图像处理、串口通信,又牵扯到机械结构、闭环控制,知识点密度足够撑起一份拿得出手的毕设,也可以当工作后的练手项目。
这篇文章不打算把源码贴一遍就算完,我按实际做项目的顺序,把整体设计、硬件连接、色块识别原理、PID调参、通信协议、踩坑记录全部拆开讲。无论你是刚拿到工程文件的小白,还是想从零复现的进阶玩家,都建议跟着走一遍,很多细节是你翻官方文档翻不出来的。
1. 项目到底在做什么,为什么这么搭
1.1 这个项目的核心目标
色块追踪云台,说白了就是做一个“眼睛长在转台上”的小系统:OpenMV摄像头盯着画面,只要出现设定颜色的物体(比如红色小球、黄色胶带、绿色激光笔光斑),就把它在图像里的位置换算成云台转动的角度,让两个舵机带着摄像头一直对准这个目标。
听起来不复杂,但拆开之后至少涉及三个层面的能力:
- 视觉层面:颜色空间转换、阈值分割、色块提取、目标坐标输出;
- 通信层面:OpenMV和STM32之间通过串口交换数据,要有帧格式、要有校验、要处理粘包;
- 控制层面:根据目标偏移量计算舵机角度,用PID做平滑跟随,还要防止云台抖动和丢目标乱转。
这三块正好对应嵌入式开发里最常见的三类问题,所以它才会成为毕业设计常青树。你做完这个项目,简历上可以写“独立完成视觉识别与运动控制闭环”,面试时是有东西可以展开聊的。
1.2 为什么是OpenMV加STM32,而不是其他方案
我在做方案预研的时候,其实把市面上常见的组合都扫了一圈,先说结论:OpenMV加STM32在毕业设计这个场景里,是综合成本最低、容错率最高的选择。
- OpenMV本身是一个带摄像头、带处理器、跑MicroPython的视觉开发板,IDE开箱即用,find_blobs这种功能几行代码就能调出来,比用树莓派加OpenCV轻量得多,也比纯STM32加OV7670硬怼图像处理省太多时间。
- 用STM32做主控,是因为云台控制、传感器扩展、无线模块对接都需要一个实时性好的MCU。视觉开发板只管出坐标,控制板只管执行,分工明确,出问题也好定位。
- 有人会问那用K210不是更便宜吗?K210确实性价比高,但资料成熟度和MicroPython生态不如OpenMV,毕设周期内想稳一点,OpenMV是更稳妥的选项。
这套架构的另一个好处是以后扩展方便。你后续想加个激光测距模块测目标距离,或者加个ESP8266把坐标上报到服务器,都是在STM32这边挂外设的事情,不会动到视觉部分的代码。
2. 硬件细节与组装前必须整明白的事
2.1 物料清单和连接拓扑
做云台追踪,标准配置大概是这些:
| 部件 | 型号/规格 | 数量 | 用途 |
|---|---|---|---|
| 主控板 | STM32F103C8T6最小系统板 | 1 | 接收坐标、计算PID、输出PWM |
| 视觉模块 | OpenMV Cam H7 Plus / M7 | 1 | 图像采集、色块识别、串口发送 |
| 云台 | 二自由度舵机云台支架 | 1 | 承载摄像头,提供偏航、俯仰转动 |
| 舵机 | SG90 或 MG90S | 2 | 云台动力 |
| 电源 | 5V/2A以上稳压模块或电池组 | 1 | 舵机供电(关键!) |
| 连接线 | 杜邦线若干、USB转TTL调试线 | 若干 | 程序下载、串口调试 |
接线不复杂,但有几个点必须提前知道:
- OpenMV的P4(TX)接STM32的PA3(RX1),OpenMV的P5(RX)接STM32的PA2(TX1),交叉连接,别忘了共地;
- 舵机信号线接STM32的TIM定时器通道引脚,我常用PA0和PA1,对应TIM2_CH1和TIM2_CH2;
- 舵机电源一定要单独供电,至少5V/2A,不能直接吃STM32最小系统板的3.3V输出。SG90虽然是小舵机,堵转峰值电流能到700mA以上,两个舵机同时动作如果全走板子上的LDO,板子必挂。
2.2 机械结构里的重心问题
很多人第一次装云台会忽略重心。摄像头要尽量固定在云台转轴的“正上方”,让摄像头模组的重心和俯仰舵机转轴大致重合。
如果摄像头伸得太远,俯仰舵机每动一下都要承担很大的重力矩,刚开始还转得动,几分钟后就开始发抖,甚至直接发热烧掉。我当时图省事把OpenMV用长铜柱架出去一大截,结果一调到俯仰就咔咔响,后来换了短支架让镜头贴近转轴,问题马上消失。
另外支架螺丝不要拧得过紧,特别是舵机臂和云台托盘的连接处,拧太死会导致舵机堵转,一上电就嗡嗡叫,这个状态下电流飙得很快。
3. 色块识别:从原理到参数调优
3.1 OpenMV识别色块的逻辑,其实很直白
色块追踪这个功能,OpenMV官方库里已经封装好了,但你要真正调好它,还是得明白底层逻辑。OpenMV拿到的是RGB图像,但RGB颜色空间受光照影响太大,同一个红色物体在阳光下和灯光下数值差很多。所以OpenMV默认把图像转成LAB颜色空间再处理。
LAB的三个分量分别是:L代表亮度,A代表从绿色到红色的分量,B代表从蓝色到黄色的分量。做颜色识别时我们最关心的是A和B,因为这两个分量受亮度影响相对小。你打开IDE的帧缓冲区,用“工具”里的阈值编辑器拖动LAB阈值,就是在选一个“颜色范围”,图像里落在这个范围内的像素会被变成白色,范围外的变成黑色,再用find_blobs找出白色区域的最小外接矩形,这就是色块。
核心代码长这样:
import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) uart = UART(3, 115200, timeout_char=1000) red_threshold = (30, 100, 15, 127, 15, 127) # LAB阈值,用阈值编辑器获取 while True: img = sensor.snapshot() blobs = img.find_blobs([red_threshold], pixels_threshold=200, area_threshold=200, merge=True) if blobs: largest_blob = max(blobs, key=lambda b: b.pixels()) img.draw_rectangle(largest_blob.rect()) img.draw_cross(largest_blob.cx(), largest_blob.cy()) # 发送目标中心坐标 msg = bytearray([0xAA, 0x55, largest_blob.cx() >> 8, largest_blob.cx() & 0xFF, largest_blob.cy() >> 8, largest_blob.cy() & 0xFF]) uart.write(msg)3.2 find_blobs参数的经验值
很多人拿官方例程就开跑,结果发现识别不稳定,问题多半出在参数上:
- pixels_threshold和area_threshold太小:画面里的噪点会被当目标,云台抽风式乱转;
- 太大:目标稍小或距离稍远就识别不到,云台全程发呆;
- merge=True:相邻的色块会合并成一个大块,适合目标被遮挡一部分的场景;如果画面里有多个同色目标且你想只追最近那个,merge之后取pixels最大的块,效果比不合并好得多;
- roi:如果追踪场景固定,比如只追踪桌子上的球,可以把识别区域限定在画面中央一个矩形内,能大幅减少背景干扰。
我的经验值是在QVGA分辨率下,红色球体大概占画面1/20时,pixels_threshold设200、area_threshold设200是起步值。如果你发现识别框忽大忽小,可以往上调到300到500,牺牲一点灵敏度换取稳定性。
3.3 光照是这个项目最大的敌人
调阈值的时候你可能会发现一个规律:白天调到完美的阈值,到了傍晚打开灯再跑就“瞎”了。这是正常现象,LAB虽然比RGB抗光照,但抗的是“光照强度变化”,扛不住色温变化。
我的做法有三个:
- 尽量用白光LED补光,保持环境光色温稳定;
- 阈值采集在“正式运行时的实际光线条件”下做,不要在纯黑房间里调;
- 如果场景允许,在OpenMV前面加一个红外截止滤镜,能进一步压掉红光干扰,红色目标识别会稳定不少。
4. 云台控制:PID让云台“咬住”目标
4.1 为什么必须上PID,不能直接等于
最粗暴的控制逻辑是这样:目标在画面中心偏右,那就让偏航舵机往右转一点;目标在中心偏上,就让俯仰舵机往上抬一点。但这种“有误差就动,没误差就停”的控制方式,在实际系统里会有个问题——舵机是机械结构,有惯性、有摩擦、有响应延迟,你让它动一点它就冲过头,冲过头了误差反向了又往回调,结果就是云台一直在目标附近小幅振荡,看起来就像“点头病”。
PID的作用简单理解就是:根据当前的偏差、过去的偏差积累、以及偏差的变化趋势,综合算出舵机应该转多少。对应到代码里就是比例项、积分项、微分项三项相加。
位置式PID的核心公式是:
output = Kp * error + Ki * integral + Kd * derivative;其中error是目标角度和当前角度的差值,integral是误差的累加,derivative是误差的变化率。
4.2 PID参数整定的实操记录
我调PID从来不用理论公式硬推,直接上工程经验法:
- 先把Ki和Kd设成0,只保留Kp,从小往大加,看到云台能“追上”目标但略有回摆,Kp就差不多了;
- 加入Kd,抑制回摆,Kd从0.1开始慢慢加,直到云台在目标静止时基本不抖;
- 如果云台静止时对目标有固定角度的偏差,比如总是停在偏左一格的位置,这时候才需要慢慢加Ki。
我用SG90舵机时的参考值:Kp=1.2,Ki=0.05,Kd=0.15。要注意这只是参考,舵机不同、云台重量不同、PID周期不同,参数会差很多。如果你换MG90S或者金属舵机,惯量变大,Kd通常还要往上调。
还有一个很多人忽略的细节——输出限幅。STM32的PWM控制舵机一般是50Hz频率,角度映射到占空比,比如0.5ms到2.5ms对应0度到180度。计算出来的PID输出如果直接往PWM里塞,就会遇到超范围问题。正确做法是把输出限幅在舵机实际能转的角度范围内,比如限制在5到175度,避免舵机撞到机械限位咔咔响。
4.3 死区:防抖的另一个关键点
PID参数调好之后,云台可能还有一个问题:目标明明已经处于画面中心附近,误差只有两三个像素,舵机却还在小幅度修正,导致画面总是有一点点漂移。
解决方法是加死区。把误差绝对值小于死区阈值的目标偏差直接当作0,不改变输出:
if (abs(error) < deadzone) { output = current_angle; // 保持当前位置 } else { output = current_angle + pid_compute(error); }死区阈值我习惯设2到3个像素。太小没效果,太大云台会显得迟钝,目标在中心附近小幅移动时完全不跟随。
5. STM32与OpenMV的通信设计:别让数据“粘包”坑了你
5.1 自定义协议帧的格式设计
OpenMV算出来的坐标要发给STM32,看似简单,但直接用uart.write发字符串是最容易踩坑的做法。比如你发“160,120\n”,接收端每收到一个字符就处理一次,一旦串口中断处理不及时,数据就串了。
我的做法是定义一套固定格式的二进制帧:
| 字节位置 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头1 |
| 1 | 0x55 | 帧头2 |
| 2 | 数据长度 | 有效数据字节数 |
| 3 | 目标中心X高字节 | 坐标值大端序 |
| 4 | 目标中心X低字节 | |
| 5 | 目标中心Y高字节 | |
| 6 | 目标中心Y低字节 | |
| 7 | 校验和 | 前面所有字节累加取低8位 |
用双字节帧头是为了防止粘包时误判数据起始位置。接收端只有连续收到0xAA、0x55才认为是新帧的开始,其它字节一律丢弃。校验和用来过滤脏数据,这个在实验室环境可能感受不深,但到了无线串口或者长线缆连接时能救你半条命。
STM32端的解析核心逻辑写起来不复杂:
uint8_t rx_buf[16]; uint8_t rx_index = 0; uint8_t frame_flag = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { rx_buf[rx_index++] = received_byte; if (rx_index == 1 && rx_buf[0] != 0xAA) { rx_index = 0; // 没等来帧头1,重新等 } else if (rx_index == 2 && rx_buf[1] != 0x55) { rx_index = 0; // 帧头2不对,重新等 } else if (rx_index == rx_buf[2] + 4) { // 收完一整帧,做校验和判断 uint8_t sum = 0; for (int i = 0; i < rx_index - 1; i++) sum += rx_buf[i]; if (sum == rx_buf[rx_index - 1]) { frame_flag = 1; } rx_index = 0; } } }这段代码的精髓在于:收到第一个字节发现不是0xAA就直接丢弃,收到第二个字节发现不是0x55也重新等,这样就天然处理了粘包和半包的问题。
5.2 波特率选多少合适
OpenMV和STM32之间的串口波特率,我建议直接用115200。OpenMV的帧率在QVGA分辨率下大概能跑到30到50帧,每一帧发一个坐标,数据量很小,115200波特率完全够用,跑115200还能兼容大多数USB转TTL调试工具,排查问题方便。
有个细节:OpenMV的UART默认是UART3,对应的引脚是P4和P5,波特率在构造函数里设好之后,STM32那边必须一致,否则收到的全是乱码。
5.3 如果画面里没有目标怎么办
色块丢失是追踪项目里最常被忽视的场景。如果OpenMV没有找到目标,还按上一帧的坐标发,云台就会对着一个不存在的目标使劲转。我的方案是:找不到目标时给STM32发一帧“无效数据”,X和Y都置为0xFFFF,STM32收到后进入“搜索模式”,让云台按固定节奏左右小幅摆动扫描,直到重新看到目标。
这个逻辑对演示效果提升非常明显。评审老师或者同学在旁边看的时候,随手把球移出画面,云台不会乱抽,而是缓慢搜索,这种细节很加分。
6. 调试实录:常见问题与排查速查表
做这个项目,说实话有一半时间都花在“眼下这个小毛病到底出在哪”上。我把踩过的坑整理成一张表,方便你对照排查:
| 问题现象 | 可能原因 | 解决方向 |
|---|---|---|
| OpenMV识别框乱跳 | 阈值范围太宽、roi里干扰物太多 | 重新收紧阈值,限定roi,加merge=True |
| 云台快速来回抖动 | Kp太大或Kd太小 | 降低Kp,增大Kd,检查舵机供电是否稳定 |
| 云台转到某个位置后卡死不动 | 角度超出机械限位 | 加PID输出限幅,限制目标角度范围 |
| 串口收到的坐标全是乱码 | 波特率不一致、共地没接 | 确认两端波特率,接上GND共地线 |
| OpenMV运行一段时间后死机 | 供电不足、MicroPython堆栈溢出 | 换独立5V电源,把递归改迭代,降低帧率 |
| STM32下载程序报“No STM32 Target Found” | ST-Link接线问题或芯片被锁 | 检查SWDIO/SWCLK接线,按住复位键再点击下载,必要时用ST-Link Utility做整片擦除 |
| 舵机在目标静止时仍有“嗡嗡”声 | 舵机在死区附近反复修正 | 加死区判断,降低PID输出分辨率 |
最后一个问题多啰嗦两句:舵机滋滋响不一定是坏了,更多时候是PID在极小误差区间内反复调整占空比,舵机一直在“想动又动不了”的状态。这时候把死区从2个像素调到4个像素,安静很多。
另外调试串口一定要用带CH340或者CP2102的USB转TTL工具,不要用PL2303,它对5V和3.3V逻辑电平的兼容性参差不齐,之前遇到过PL2303把OpenMV的TX直接拉挂的情况,排查了大半天。
7. 个人体会与可扩展方向
这个项目做完,我的一个很深的感受是:硬件和算法的边界感一定要清晰。OpenMV负责出坐标,STM32负责执行,哪个环节出问题就单独测哪个环节,千万不要“两头一起查”。我的调试流程是先写一个固定发坐标的OpenMV脚本,让STM32云台跟着固定的轨迹转,确认控制链路通了,再反过来把OpenMV的识别打开,这样问题永远只会出现在半条链路上,而不是全部。
如果你做完基础版想再往上走一走,有几个方向性价比很高:
- 把OpenMV换成正点原子的K210或者树莓派Pico加摄像头,对比一下视觉开发板的选型差异;
- 加一个测距模块,比如VL53L0X,把色块追踪升级成“追踪目标的同时保持固定距离”,这就有点机器跟随的意思了;
- 把STM32的串口数据通过ESP8266转发到手机端,做一个远程监控云台的Web页面;
- 用FreeRTOS把串口接收、PID计算、舵机PWM输出放到不同任务里,顺便练一下RTOS的队列和信号量用法。
毕业设计只是把一块敲门砖敲响,真正值钱的其实是你在调PID、拼协议、修电路过程中形成的那套排查问题的方法论。这套东西带不走,但它会在你以后做任何嵌入式项目时反复出现。
本文还有配套的精品资源,点击获取