前言
想做一个“软硬结合”的项目,于是选择了当下最火的机器视觉方向。
市面上的视觉云台动辄几百上千,作为一个穷学生,我决定挑战极限:用最便宜的 SG90 舵机(3元/个)配合 STM32(hal库) 和 Python,手搓一个 AI 自动瞄准系统。
虽然硬件简陋,但通过引入 PID 控制算法 和 视差补偿逻辑,我成功克服了廉价舵机的抖动问题,实现了还不错的追踪效果。
一、 硬件清单:成本不到一杯咖啡 ☕️
这个项目的核心理念就是“低成本”。
| 硬件名称 | 型号/参数 | 参考价格 | 作用 |
| 主控芯片 | STM32F103C8T6 | 15元 | 控制核心,产生 PWM 波 |
| 执行器 | SG90 180度舵机 x2 | 6元 | 组成二自由度云台 (X轴/Y轴) |
| 视觉传感器 | 电脑自带摄像头 | 0元 | 图像采集 |
| 通信模块 | USB 转 TTL (CH340) | 5元 | 电脑与单片机的数据桥梁 |
| 其他 | 激光头 + 热熔胶 | 2元 | 模拟“发射器” |
| 总计 | — | < 30元 | — |


二、 系统架构设计 🛠️
整个系统分为 上位机 (电脑) 和 下位机 (STM32) 两部分,通过串口通讯。
- PC 端 (Python): 负责“眼睛”和“大脑”。使用 OpenCV 捕捉目标(橙色小球),计算坐标误差,并通过 PID 算法计算出舵机需要移动的角度,通过串口发送。
- STM32 端 (C语言): 负责“手”。接收串口数据,解析指令,输出 PWM 波控制舵机转动。
- 工作流程:摄像头 —> Python —> 串口 —> STM32 —> 舵机
三、 核心难点突破 (干货部分) ⚡️
1. 视觉识别:HSV 颜色空间提取
为了快速锁定目标,我使用了颜色阈值分割。相比于 RGB,HSV 空间对光照更具有鲁棒性。


2. 通信协议:如何防止数据“打架”
为了保证 STM32 能准确解析数据,我设计了简单的包格式:X增量,Y增量\n。
在 STM32 端,使用 sscanf 进行解析,只有检测到 \n 换行符才开始处理,有效防止了粘包问题。
3. 灵魂算法:PID 控制与消抖 (重点!)
这是本项目的最大挑战。 SG90 舵机存在极大的虚位和死区。
- 初期问题: 使用简单的比例控制 (P),云台在目标附近疯狂“摇头”(震荡)。
- 解决方案: 引入 PID 算法。
- P (比例): 提供响应速度。
- D (微分): 提供“阻尼感”,在接近目标时提前刹车,消除抖动。
- 软件死区: 当误差小于 40 像素时,强制停止调整,防止舵机在终点附近反复横跳。
# PID 代码 import time
class PID:
def __init__(self, kp, ki, kd):
self.kp = kp # 比例(动力)
self.ki = ki # 积分(很少用)
self.kd = kd # 微分(阻尼/防抖)
self.last_error = 0
self.last_time = time.time()
def compute(self, error):
current_time = time.time()
delta_time = current_time - self.last_time
# 防止时间过短导致除以0
if delta_time <= 0:
delta_time = 0.001 # P: 比例项
p_out = self.kp * error # D: 微分项 (本次误差 - 上次误差) / 时间
derivative = (error - self.last_error) / delta_time
d_out = self.kd * derivative # I: 积分项 (视觉追踪通常不需要,设为0即可)
# 如果需要,这里可以加 integral 逻辑
# 总输出
output = p_out + d_out # 更新状态
self.last_error = error
self.last_time = current_time
return output
四、 效果展示 & 遇到的坑 💣
1. 物理视差问题
现象: 摄像头识别准了,但激光总是打偏。
分析: 激光笔是粘在云台上的,两者物理上不重合。
解决: 在 Python 代码中加入 OFFSET_X 和 OFFSET_Y 进行软件补偿。
2. 成果展示
虽然受限于 SG90 的机械精度,无法做到指哪打哪的工业级精度,但它已经能灵敏地跟随小球运动。
五、 源码下载 📥
本项目的完整代码(Python上位机 + STM32源码)已上传。
Github链接:https://github.com/edythieajahgsgshwtvwywvwfd-sketch/S90_aim_ball/tree/master
