1. 项目概述与赛题核心解读
2023年全国大学生电子设计竞赛(电赛)的E题,题目是“运动目标控制与自动追踪系统”。这个题目一出来,当时就在我们参赛圈子里炸开了锅,因为它完美地踩在了当前几个技术热点的交叉口上:机器视觉、运动控制和嵌入式系统。简单来说,就是让你做一个能自己“看”到移动目标,并且能控制一个云台(上面装着激光笔)去实时瞄准、追踪这个目标的装置。听起来是不是有点像电影里的自动瞄准?没错,其核心逻辑就是一套简易的、实时的闭环反馈系统。
这道题的魅力在于,它把一个复杂的工程问题,拆解成了几个非常具体、可实现的模块,但又留足了发挥空间。它不要求你用多高端的硬件,核心就是一块常见的微控制器(比如STM32)、一个摄像头模块(如OpenMV、K210或者树莓派搭配摄像头)、一个二维舵机云台,再加上一个激光笔。硬件门槛相对亲民,但软件和算法上的挑战直接拉满。你需要解决图像采集、目标识别、坐标转换、PID控制算法、系统延时补偿等一系列问题,任何一个环节没处理好,整个系统的追踪效果就会大打折扣,出现“跟不上”、“抖得厉害”或者“跑飞了”的情况。
这道题非常适合有一定嵌入式基础和编程能力,并且对机器视觉感兴趣的同学来挑战。它不仅能让你把单片机、传感器、电机驱动这些硬件知识串起来,更能让你亲身体验到从“感知”到“决策”再到“执行”的完整自动控制流程。接下来,我就结合我们团队的实战经历,把这个题从设计思路到代码实现,再到调试避坑,给你掰开揉碎了讲清楚。
2. 系统整体设计与核心思路拆解
做电赛题目,最忌讳的就是拿到题目就埋头焊板子、写代码。我们花了将近半天的时间来做方案论证和整体设计,事实证明,这半天的时间在后面省下了无数个通宵。
2.1 核心需求与指标分析
题目要求主要有几个关键点:1)系统能自动识别并锁定指定的运动目标(比如一个画有特定图案的小车);2)控制云台转动,使激光点持续照射在目标中心;3)目标做匀速直线、S形等规定运动时,追踪误差要小于一定范围(比如2厘米);4)有手动模式可以介入。这些要求翻译成技术语言就是:
- 感知层:需要一套视觉系统,能稳定、快速地从摄像头画面中提取出目标的位置坐标(X, Y)。
- 控制层:需要一套算法,能将目标坐标转换为云台两个舵机(俯仰和偏航)的转动角度,并且这个转换要能补偿摄像头安装位置带来的几何偏差。
- 执行层:需要能精确驱动舵机转到指定角度的硬件电路和驱动程序。
- 决策层:需要一套控制律(我们用的是PID),根据目标位置的实时变化,计算出舵机应该调整的速度或角度,让整个系统形成一个平滑、快速的闭环。
我们的设计思路也就此明确:以微控制器为大脑,摄像头为眼睛,舵机云台为手脚,构建一个视觉伺服系统。核心闭环流程是:摄像头采集图像 -> 图像处理识别目标 -> 计算目标在图像中的像素坐标 -> 通过坐标转换模型得到目标相对于云台的真实角度偏差 -> PID控制器根据角度偏差计算舵机调整量 -> 驱动舵机转动 -> 激光点移动 -> 下一帧图像采集……如此循环。
2.2 硬件方案选型与考量
硬件选型直接决定了系统的性能上限和开发难度。
主控MCU:我们选择了STM32F407。理由很充分:首先,F4系列带有FPU(浮点运算单元),在做坐标转换、PID运算等大量浮点计算时,速度比F1系列快一个数量级,这对保证控制周期至关重要。其次,它资源丰富,有足够的定时器来产生精准的PWM波控制舵机,也有足够的串口、SPI等接口与摄像头模块通信。
视觉模块:这是选型的重中之重。我们评估了三个主流方案:
- OpenMV:优点是开发简单,内置了丰富的图像处理函数(找色块、找直线、AprilTag识别等),直接用MicroPython编程,上手快。缺点是性能有限,处理复杂图像或高帧率时比较吃力,且与STM32通信通常用串口,传输处理后的结果数据有延时。
- Kendryte K210:这是一颗带AI加速的芯片,性能强悍,可以跑一些简单的神经网络模型进行目标检测。但如果只是识别颜色块或特定图案,有点杀鸡用牛刀,且双核开发、内存管理对新手不太友好。
- 树莓派+摄像头:性能最强,可以用OpenCV做非常复杂的处理。但问题是树莓派是Linux系统,实时性是个挑战,控制舵机的PWM精度可能不如单片机,而且系统复杂,功耗也高。
综合权衡,我们选择了OpenMV H7 Plus。它的性能比老款强不少,足够处理这个题目。最关键的是开发效率。电赛时间紧,OpenMV的快速原型开发能力能让我们把精力集中在控制算法上,而不是折腾摄像头驱动和底层图像处理。我们让OpenMV只负责一件事:以最高帧率找到目标,并输出其中心点的像素坐标(x, y)。这个坐标通过串口实时发送给STM32。
执行机构:云台我们用了两个数字舵机(MG996R这类),分别控制Yaw(偏航,水平转动)和Pitch(俯仰,垂直转动)。数字舵机比模拟舵机控制精度高,响应快。舵机由STM32的定时器产生PWM信号直接控制,注意要单独供电,电流要足,否则会出现抖动甚至重启。
其他:激光笔直接用个小功率的,固定在云台上。整个系统用一块12V锂电池供电,通过降压模块给各个部分提供合适的电压(STM32和OpenMV用5V或3.3V,舵机用6V或7.4V单独供电)。
注意:供电一定要隔离!电机(舵机)的电源必须和核心控制板(STM32, OpenMV)的电源分开,至少要用磁珠或二极管隔离,并用大电容稳压。否则舵机一动作,电压瞬间被拉低,会导致单片机复位,摄像头重启,这是新手最容易踩的坑。
3. 核心算法解析与实现要点
硬件是骨架,算法才是灵魂。这个题目的算法部分主要集中在坐标转换和PID控制上。
3.1 视觉识别与坐标获取
在OpenMV上,我们的脚本非常简单高效。假设目标是红色小球。
import sensor, image, time, pyb from pyb import UART # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240 sensor.skip_frames(time = 2000) sensor.set_auto_gain(False) # 必须关闭自动增益和白平衡,否则颜色阈值会变 sensor.set_auto_whitebal(False) # 定义红色阈值 (在RGB色彩空间下调整) red_threshold = (30, 100, 15, 127, -128, 127) # 根据实际环境调整 uart = UART(3, 115200) # 初始化串口3,波特率115200 while(True): img = sensor.snapshot() # 抓取一帧图像 # 寻找色块 blobs = img.find_blobs([red_threshold], pixels_threshold=100, area_threshold=100) 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()) # 通过串口发送中心坐标 data = bytearray([0xFF, 0xFE, largest_blob.cx()>>8, largest_blob.cx()&0xFF, largest_blob.cy()>>8, largest_blob.cy()&0xFF]) uart.write(data) else: # 没找到目标,发送一个特定值,比如(0, 0),并在前面加个标志位 uart.write(bytearray([0xFF, 0xFD, 0x00, 0x00, 0x00, 0x00])) # 控制帧率,不设延时,以最大速度运行这里的关键是稳定性和速度。阈值要调准,确保在不同光照下都能稳定识别。发送数据时我们加了一个简单的帧头(0xFF, 0xFE),方便STM32解析。没找到目标时发送另一个帧头(0xFF, 0xFD),让STM32知道目标丢失,可以进入搜索模式或保持上一位置。
3.2 从像素坐标到舵机角度:坐标转换模型
这是整个系统的核心数学模型,也是最容易出错的地方。OpenMV返回的是(cx, cy),这是目标在图像平面(320x240)上的像素坐标。我们需要把它转换成云台需要转动的角度(θ_yaw, θ_pitch)。
这里不能用一个简单的线性比例关系,因为摄像头是透视成像。我们采用小孔成像模型进行近似解算。假设:
- 摄像头光轴与云台转动中心对齐(理想情况,实际安装总有偏差)。
- 已知摄像头的焦距
f(以像素为单位)和图像中心(u0, v0)。这些参数可以通过摄像头标定获得,OpenMV也提供近似值。 - 目标在摄像头坐标系中的真实角度偏移(弧度制)可以通过以下公式计算:
Δθ_yaw ≈ (cx - u0) / fxΔθ_pitch ≈ (cy - v0) / fy其中,fx, fy是x轴和y轴方向的焦距,u0, v0是主点坐标(通常接近图像中心)。
在实际操作中,我们采用了一种更实用的两步法:
- 粗标定:将目标放在云台正前方足够远的位置,记录此时OpenMV返回的
(cx0, cy0),作为“零点”。 - 比例系数法:控制云台水平转动一个已知角度A(如30度),记录转动前后目标像素坐标的变化量Δcx。那么,像素-角度比例系数 Kyaw = A / Δcx。同理,可以标定出垂直方向的系数 Kpitch。
- 实时计算:在运行时,目标的角度偏差就可以近似计算为:
θ_yaw = (cx - cx0) * Kyawθ_pitch = (cy - cy0) * Kpitch
这个方法的优点是避免了复杂的相机内参测量,通过实验直接得到映射关系,虽然有一定误差,但在题目要求的距离和角度范围内完全够用,且非常容易理解和实现。
实操心得:标定一定要在实际比赛的场地光照和背景下进行!光线不同,摄像头成像的亮度和对比度不同,可能会轻微影响识别出的中心点位置。我们当时就在实验室的日光灯下标定得很好,结果比赛当天场地是暖色光,导致零点漂移,现场又重新微调了系数。
3.3 PID控制算法的实现与调参
得到角度偏差(θ_yaw, θ_pitch)后,我们的目标就是让这个偏差趋于0。这就是PID控制器的用武之地。我们为两个轴分别独立设置一个PID控制器。
在STM32上,我们实现了位置式PID。公式大家都很熟悉:Output = Kp * e(t) + Ki * ∫e(t)dt + Kd * de(t)/dt。但具体到代码里,有些细节至关重要:
typedef struct { float Kp, Ki, Kd; // PID参数 float integral; // 积分项 float prev_error; // 上一次误差 float integral_limit; // 积分限幅 float output_limit; // 输出限幅 } PID_Controller; float PID_Update(PID_Controller* pid, float error, float dt) { // 比例项 float proportional = pid->Kp * error; // 积分项(带限幅和抗饱和) pid->integral += error * dt; if (pid->integral > pid->integral_limit) pid->integral = pid->integral_limit; if (pid->integral < -pid->integral_limit) pid->integral = -pid->integral_limit; float integral = pid->Ki * pid->integral; // 微分项(用误差的差分近似) float derivative = pid->Kd * (error - pid->prev_error) / dt; pid->prev_error = error; // 计算总输出并限幅 float output = proportional + integral + derivative; if (output > pid->output_limit) output = pid->output_limit; if (output < -pid->output_limit) output = -pid->output_limit; return output; }调参是PID的灵魂,也是比赛调试时间的大头。我们的经验是“先P后I再D,从小到大慢慢试”:
- 调Kp(比例):先将Ki和Kd设为0。逐渐增大Kp,直到系统开始出现小幅度的、频率较快的振荡。此时系统响应快,但无法稳定在目标点,会在目标点附近来回抖。记录下这个临界Kp值。
- 调Kd(微分):引入Kd,目的是抑制振荡,增加系统阻尼。从0开始慢慢增加Kd,你会发现振荡幅度减小,变得平滑。Kd太大反而会引入高频噪声,使系统反应迟钝。
- 调Ki(积分):最后引入Ki,目的是消除静态误差(即始终差一点对准的情况)。Ki要非常小心地加,因为积分累积效应强,容易导致系统超调过大甚至震荡。一定要加上面代码中的
integral_limit(积分限幅)。
核心技巧:对于云台舵机系统,微分项(Kd)至关重要。因为舵机本身有惯性,如果没有微分项,仅用比例控制,云台很容易像钟摆一样晃来晃去,永远停不下来。合适的Kd能提供“阻尼”效果,让云台平滑、稳定地停下来。我们的参数最终大概在 Kp=1.2, Ki=0.05, Kd=0.8 左右(具体值取决于你的系统机械结构、舵机速度、控制周期等)。
控制周期(dt)也是一个关键参数。它取决于你的图像处理帧率。如果OpenMV每秒能处理30帧,那么理想控制周期就是33ms。在STM32中,我们用一个定时器中断,严格每隔33ms读取一次最新的目标坐标,并执行一次PID计算和舵机控制。周期不稳定会导致控制效果剧烈波动。
4. 系统集成与调试全流程
4.1 软件架构与多任务处理
STM32的程序我们采用“前后台”架构,配合中断,来保证实时性。
- 后台主循环:负责状态显示、参数设置(通过按键或串口)、模式切换等非实时任务。
- 定时器中断:这是核心节奏器。我们设置了一个1ms的定时器中断,用于计时。在中断服务函数里,我们设置一个33ms的“软定时器”。当33ms到达时,置位一个标志位
Vision_Update_Flag。 - 串口中断:用于接收OpenMV发来的坐标数据。一旦收到一帧完整数据(通过帧头判断),就立即解析,并将最新的
(cx, cy)坐标更新到一个全局变量中,同时记录接收时间戳。 - 主循环中的控制任务:在主循环中,不断检查
Vision_Update_Flag。如果标志位为1,则:- 清除标志位。
- 从全局变量中读取最新的目标坐标。
- 检查坐标是否有效(非零值且接收时间在最近100ms内,防止用陈旧数据)。
- 执行坐标转换,得到角度偏差。
- 调用两个轴的PID计算,得到舵机PWM的增量。
- 更新舵机的目标PWM值。
- 通过定时器PWM输出驱动舵机。
这种设计确保了控制周期基本稳定,并且图像数据的接收是即时的,不会被主循环中的其他任务阻塞。
4.2 通信协议与数据同步
OpenMV和STM32之间的串口通信必须可靠。我们设计了简单的协议:
- 数据帧格式:
帧头1帧头2数据1高字节数据1低字节数据2高字节数据2低字节。 0xFF, 0xFE代表找到目标,后面4个字节分别是cx和cy。0xFF, 0xFD代表目标丢失,后面4个字节忽略。- STM32端在串口中断中,用一个状态机来解析这个协议,确保能正确应对数据丢包和错位。
同步问题:摄像头采集、处理、发送需要时间,STM32接收、处理、控制也需要时间,这造成了系统延时。如果目标运动很快,这个延时会导致追踪永远慢半拍。我们的补偿方法是:在PID中适当增大微分项(Kd),因为微分项本质上是对未来趋势的预测。更高级的做法是使用“预测滤波”,如卡尔曼滤波,根据目标前几帧的运动速度来预测它当前的位置,但这在四天三夜的比赛中实现和调优风险较大。
4.3 现场调试与性能优化
调试阶段,我们准备了几个工具:
- 无线串口模块:将STM32的调试信息(如当前坐标、角度偏差、PID输出、帧率)实时发送到电脑,用串口绘图工具(如SerialPlot)可视化。这是调参的“眼睛”,没有它就是在盲调。
- 手机慢动作拍摄:用手机的慢动作视频(240帧)拍摄激光点追踪目标的整个过程,回放时可以一帧帧看,分析是反应慢、过冲还是抖动。
- 参数在线微调:我们在STM32程序里预留了接口,可以通过电脑串口助手实时发送指令修改PID参数,无需重新烧录程序,极大提升了调试效率。
优化点:
- 图像处理优化:在OpenMV上,将搜索区域(ROI)限制在上一帧目标位置附近,而不是全图搜索,能大幅提升帧率。
- 运动预测:当目标连续若干帧向一个方向运动时,可以给舵机目标角度加一个很小的提前量。
- 死区设置:当角度偏差小于某个很小阈值(如0.5度)时,不更新舵机位置,避免舵机一直在“微颤”,减少磨损和功耗。
5. 常见问题与故障排查实录
在实际制作和调试中,我们遇到了无数坑,这里把最典型的几个列出来,希望能帮你避雷。
| 问题现象 | 可能原因 | 排查与解决方法 |
|---|---|---|
| 激光点完全跟不上目标,滞后严重 | 1. 系统总延时过大。 2. PID参数过于保守(Kp太小)。 3. 舵机速度太慢。 | 1. 测量总延时:从目标移动,到激光点开始反应的时间。优化图像处理,提高帧率;检查通信是否拥堵。 2. 逐步增大比例系数Kp,观察反应速度。 3. 更换更快舵机,或提高舵机供电电压(在额定范围内)。 |
| 激光点在目标点附近高频剧烈抖动 | 1. 微分项Kd太小或为0,系统欠阻尼。 2. 比例项Kp过大。 3. 机械结构松动,云台固定不牢。 | 1. 首要任务是增加Kd,这是抑制振荡最有效的手段。 2. 适当减小Kp。 3. 紧固所有螺丝,检查云台轴承是否有间隙。 |
| 激光点缓慢漂移,无法稳定在中心 | 1. 存在静态误差,需要积分项Ki。 2. 坐标转换的“零点”标定不准。 3. 摄像头或激光笔安装有物理偏差。 | 1. 引入很小的Ki,并严格设置积分限幅。 2. 重新进行零点标定,确保目标在云台正前方。 3. 机械调整,确保激光笔光轴与摄像头光轴尽可能平行。 |
| 目标偶尔丢失,丢失后乱转 | 1. 图像识别阈值不适应环境光变化。 2. 目标运动出视野。 3. 通信误码或丢包。 | 1. 采用自适应阈值,或比赛前根据现场光微调。 2. 目标丢失后,让云台缓慢回到零点或进行小范围扫描搜索,而不是停住不动。 3. 加强通信协议校验,如增加CRC校验;降低波特率或检查接线。 |
| 舵机动作时,单片机或摄像头重启 | 电源问题!舵机启动瞬间电流极大,拉低了整个系统电压。 | 必须进行电源隔离!舵机使用独立电池或稳压模块供电,并与控制板电源共地。在舵机电源输入端并联一个大容量(如1000uF)的电解电容缓冲电流。 |
| 帧率不稳定,时快时慢 | 1. OpenMV图像处理复杂度波动。 2. STM32主循环被其他任务阻塞。 3. 串口数据传输时间不稳定。 | 1. 优化OpenMV代码,使用clock()函数测量各部分耗时,找出瓶颈。2. 确保控制任务在定时器触发下执行,优先级最高。 3. 确保串口波特率匹配,且中断服务函数执行时间尽可能短。 |
最后再分享一个比赛时的心态技巧:电赛到最后一天,系统可能还是不太稳定。这时不要盲目大改算法或硬件。“稳”字当头。检查所有接线是否牢固,电源是否充足,参数是否备份。做一个最基础、最稳定的功能版本保底。在这个基础上,再去尝试那些“锦上添花”的优化功能。我们当时就是先确保匀速直线追踪绝对稳定,拿到基础分,然后再去挑战S形曲线追踪的加分项。记住,一个能稳定拿到80分的系统,远胜于一个理论上能拿100分但随时会崩的系统。