简介:这份资源是2017年全国大学生电子设计竞赛B题“滚球控制系统”的完整代码与工程文件,面向电赛参赛者、嵌入式开发者和机器视觉入门者。项目基于OpenMV摄像头识别小球坐标,通过蓝牙将数据传至主控,并配合PID算法实现精准控制,覆盖图像处理、无线通信、嵌入式编程等关键环节。压缩包共254个文件,以C语言源码、头文件、Keil工程配置(uvproj/uvopt)、编译生成的hex/axf及中间文件为主,包体约8.76MB,结构清晰,便于直接打开工程查看或烧录验证。已有1810人学习下载。资料内含完整的摄像头识别与蓝牙传输程序、控制算法实现、硬件驱动代码及工程构建脚本,可直接作为赛题方案参考,也可用于学习OpenMV视觉追踪、STM32嵌入式开发与运动控制的综合实践。 2017年电赛B题滚球控制系统,放到今天看依然是很有代表性的一个赛题。当年这道题刷掉了一大批队伍,不是因为题目本身多深奥,而是它把"视觉识别"和"运动控制"拧在了一起,任何一个环节掉链子,球就满板乱跑。我那年带队做这道题,从硬件搭建到代码调试前后折腾了一个多月,把能踩的坑基本踩了一遍。这篇就把我们当时的完整方案、代码思路和调参过程整理出来,给后面做类似项目的朋友一个参考。
1. 题目到底在考什么:从杆球到平板的控制维度跃升
1.1 2017年B题的控制对象与前几届的区别
电赛控制类的题目几乎每年都围绕"稳定"做文章。之前有倒立摆、风力摆、小球在杆上平衡,到了2017年变成了小球在平板上滚动稳定。杆上平衡是一维控制,平板是二维控制,控制维度直接翻倍。
更关键的变化在反馈环节。杆球系统可以用角度传感器直接测量杆的倾角,间接推算出小球位置。平板系统不行,你必须实时知道小球在平板上的具体坐标,这就逼着参赛队伍上摄像头做视觉识别。很多队在这里就卡住了——单片机上跑图像处理,放在今天都是个不小的工程,更别说在比赛现场那个环境下。
1.2 控制对象拆解:视觉、决策、执行三大子系统
滚球控制系统的本质是一个闭环反馈系统,拆开看是三层:
- 感知层:摄像头采集图像,通过颜色识别或其它方法提取小球中心坐标
- 决策层:根据小球当前位置和目标位置的偏差,计算平板的期望倾角
- 执行层:舵机带动平板运动,让小球朝目标方向滚动
这三个环节是串行关系,任何一个环节的延迟或者误差都会被后级放大。感知层每秒出多少帧、决策层PID计算周期多长、舵机响应速度多快,三者必须匹配。我们当时用的方案是摄像头30fps采集,PID以50Hz频率计算,舵机用金属齿轮数字舵机,整体系统延迟控制在80ms以内,这个指标基本够用。
1.3 设计指标倒推:为什么必须上摄像头
我记得题目要求是:能让小球从任意初始位置自动滚到目标点,并且稳定停留,误差不超过一定范围。这个指标用红外对管阵列或者触摸板来做理论上是可行的——把平板做成阵列式传感器,通过小球压住的位置判断坐标。但问题在于:
- 阵列式传感器分辨率有限,小球在两个传感器之间的位置无法精确判断
- 平板本身是活动的,传感器走线会受影响
- 比赛时间紧张,阵列式方案调试周期长
摄像头方案的优势是:非接触测量、分辨率高(320x240就够用)、不干扰平板运动。劣势是受光照影响大,这个后面专门讲。
2. 机械与硬件选型:这些钱和坑值得你知道
2.1 云台结构:两自由度还是三自由度
平板控制小球,需要让小球在X方向和Y方向都能加速,理论上两个自由度就够。市面上常见的两轴云台结构(一个舵机控制俯仰、一个舵机控制横滚)可以直接用。
但我们当时试下来发现一个隐蔽问题:两轴云台的转轴不在平板中心,导致平板倾斜时小球受力分析变得复杂。三自由度云台(加一个偏航轴)虽然能理论上修正,但多一个舵机多一份抖动源,控制器设计也复杂。
最终我们选了两轴云台,但在安装时把两个转轴的交点尽量对准平板中心。这个精度不用太苛刻,误差在2cm以内,后面的PID完全可以吃掉。
2.2 舵机选择:响应速度和死区才是关键
很多队用9g塑料舵机,便宜但抖得厉害。9g舵机的死区大概在5μs(对应PWM脉宽),扭矩小,平板稍微倾斜就能把它"顶回去",表现为球还没动,平板的实际角度已经偏离期望值。
我们用的是MG996R金属齿轮舵机,扭矩够大,死区约3μs,响应速度0.14s/60°。实测算下来,对于30cm×30cm的平板,这个响应速度刚好能跟上PID的输出节奏。再慢的话,PID的每次调整都会滞后一拍,系统容易震荡。
2.3 主控选型:STM32F4系列为什么够用
主控的选择直接决定了视觉算法的上限。我们用STM32F407VET6,168MHz主频,带DSP指令和硬件浮点单元。实际上跑OV7670摄像头采集(QVGA分辨率)+ 颜色阈值分割 + 连通域标记,CPU占用率大约60%,还有余量跑PID和串口通信。
如果你用F1系列,主频72MHz,跑同样的算法帧率会掉到15fps以下,控制效果明显变差。我的建议是电赛控制题最低起步F4,如果预算允许可以直接上F7或者带硬件JPEG编解码的芯片,处理raw RGB图像会更游刃有余。
3. 视觉定位:从像素坐标到平板坐标的换算全流程
视觉这块是滚球控制系统的核心难点,也是我们调试时间最长的地方。
3.1 颜色阈值提取球心:HSV比RGB抗光照
识别小球的方法很多:模板匹配、霍夫圆检测、颜色阈值。模板匹配太慢,霍夫圆检测对参数敏感,在单片机上跑不稳。最后我们选了颜色阈值+连通域分析。
阈值分割在RGB空间做有一个问题:光照一亮,红色小球的R通道值跟着变,阈值很难固定。我们改成HSV空间,色调(H)通道受光照影响小,饱和度(S)和亮度(V)做辅助条件。实测下来,只要光线不是极端变化,H通道阈值可以一整天不调。
具体流程:
- 将RGB565格式的摄像头图像转为HSV(用查找表优化,每像素转换一次大约需要几十个周期)
- 设定H通道范围,比如红色球是0~10和156~180(红色在色环上跨越0度,要分两段)
- 二值化后做膨胀腐蚀操作,把小球区域的噪声点去掉
- 遍历像素统计有效区域的最小外接矩形或质心,得到球心的像素坐标
质心计算最简单的方式是累加所有有效像素的横纵坐标,然后除以像素总数。代码大概是这样的:
uint32_t sum_x = 0, sum_y = 0; uint16_t count = 0; for (int y = 0; y < IMG_H; y++) { for (int x = 0; x < IMG_W; x++) { if (binary_image[y][x] == 255) { sum_x += x; sum_y += y; count++; } } } if (count > 100) { ball_x_pixel = sum_x / count; ball_y_pixel = sum_y / count; }注意count要设一个最小值,防止噪声点被当成小球。我们设的是100个像素,低于这个数就认为没检测到球。
3.2 摄像头标定:畸变对控制精度的影响
廉价摄像头的透镜畸变在画面边缘非常明显,边缘处的球心坐标会向四周偏移,导致球到了平板边缘时,视觉反馈的位置和真实位置对不上。
我们用的方法是棋盘格标定。打印一张棋盘格图片贴在平板上,采集图像后检测角点,计算畸变系数,然后用逆畸变模型对坐标做纠正。
不过在单片机上做完整畸变校正开销太大。实践中我们只校正径向畸变的前两项,公式简化后运算量不大:
x_corrected = x * (1 + k1*r^2 + k2*r^4) y_corrected = y * (1 + k1*r^2 + k2*r^4)其中r是像素点到图像中心的归一化距离。k1、k2在PC上用OpenCV标定好,然后直接写死在单片机里。这个校正让平板边缘的定位误差从20多个像素降到了5个像素以内,效果立竿见影。
3.3 像素坐标到平板物理坐标的映射
像素坐标到平板坐标的转换,理论上是透视变换,因为摄像头安装高度和角度导致画面存在透视变形。但实际测试发现,如果摄像头正对平板中心,且高度足够,透视变形可以忽略,直接用线性映射就能满足控制需求。
映射公式:
physical_x = (pixel_x - pixel_center_x) * scale_x physical_y = (pixel_y - pixel_center_y) * scale_yscale的标定方法:把球放在平板上距离中心已知距离的位置(比如10cm处),读取像素坐标,计算像素距离,scale = 10cm / 像素距离。X和Y方向分别标定,因为摄像头像素不一定方形。
3.4 提高识别帧率的工程技巧
我们开局用全图320×240处理,帧率只有20fps左右。后来优化了几个点:
- 把RGB565转HSV的查找表放在快速内存(CCM)里,访问速度更快
- 减少了膨胀腐蚀的迭代次数,从3次降到1次,小球区域噪声基本能接受
- 增加动态ROI:上一帧识别到小球后,下一帧只在以小球位置为中心的一个小区域内搜索。球不可能瞬间移动太远,这种方式帧率能翻倍
动态ROI有个坑:小球被快速推动时可能跳出ROI区域,导致目标丢失。解决方法是检查ROI边缘是否有目标,如果有,扩大搜索范围。这个逻辑虽然不复杂,但能显著提升系统的鲁棒性。
4. PID环路设计:为什么单级PID绝对不够用
4.1 单级PID的问题
我第一次做这个项目时,天真地想用一组PID直接把小球位置误差转成舵机PWM。结果球永远在目标点附近震荡,幅度越来越大,最后直接跑出平板。
原因在于:平板倾角控制的是小球的加速度,不是位置。平板倾角为θ时,小球在斜面上的加速度大约是g*sin(θ)(忽略摩擦力)。也就是说,位置环的输出应该是期望加速度,而不是期望角度。如果直接让位置环的输出控制舵机角度,相当于忽略了一个积分环节,系统的相角裕度下降,更容易震荡。
4.2 串级PID:位置环+速度环
正确的做法是串级PID:
- 外环(位置环):输入是位置偏差,输出是期望速度
- 内环(速度环):输入是速度偏差(期望速度-实际速度),输出是期望倾角/加速度
实际控制量:
期望速度 = Kp_pos * (目标位置-当前位置) + Kd_pos * (目标速度-当前速度) 期望加速度 = Kp_vel * (期望速度-当前速度) 平板倾角 = 期望加速度 / g * 比例系数(简化处理)位置环的Kd项需要小心,因为我们要的是位置能尽快到达目标,同时速度不能太大。直接对位置求导得到的当前速度往往噪声很大,建议用互质差分加低通滤波:
float velocity = (current_pos - last_pos) / dt; velocity = alpha * velocity + (1 - alpha) * filtered_velocity; // 一阶低通alpha取0.3左右比较合适。
4.3 摩擦力补偿和积分限幅
小球在平板上滚动存在静摩擦和滚动摩擦。当小球快到达目标点时,速度趋近于零,此时平板倾角很小,摩擦力可能让小球停在距离目标几毫米的地方。
位置环的积分项可以消除稳态误差,但如果积分过度,小球会在目标点来回冲。我们给积分项加了一个限幅,最大输出不超过位置环总输出的30%。这样小球慢速接近目标时,积分项能补偿摩擦力,但不会引起震荡。
另外,小球速度过低时摩擦力方向不确定,我们实验发现在最后阶段直接做一个"死区":当位置误差小于5mm时,强制平板回到水平,小球靠惯性停在目标附近。这个操作在比赛现场救过我们无数次。
5. 代码框架:状态机、数据流与关键实现
5.1 主循环架构
整个系统的代码结构不复杂,关键是数据流的时序要理顺。我们采用了简单的状态机+定时器中断调度:
启动 -> 自检 -> 待机 -> 跟踪模式 -> 完成标志主循环顺序:
- 摄像头DMA采集一帧图像,完成后触发中断
- 在中断或主循环中执行图像处理,得到球心像素坐标
- 像素坐标转物理坐标
- 计算位置环和速度环PID,得到期望倾角
- 期望倾角通过坐标分解,换算成两个舵机各自的目标角度
- 更新PWM占空比
DMA采集的好处是数据搬运不占CPU,图像数据直接在内存里,处理完一帧再启动下一帧采集,避免数据被覆盖。
5.2 PID控制器实现
串级PID的代码可以封装成结构体:
typedef struct { float kp, ki, kd; float integral; float last_error; float output_max; float integral_max; } PID_Controller; float PID_Update(PID_Controller *pid, float target, float measurement, float dt) { float error = target - measurement; pid->integral += error * dt; if (pid->integral > pid->integral_max) pid->integral = pid->integral_max; if (pid->integral < -pid->integral_max) pid->integral = -pid->integral_max; float derivative = (error - pid->last_error) / dt; float output = pid->kp * error + pid->ki * pid->integral + pid->kd * derivative; if (output > pid->output_max) output = pid->output_max; if (output < -pid->output_max) output = -pid->output_max; pid->last_error = error; return output; }两个环串联起来的样子:
float vel_target = PID_Update(&pos_pid, target_pos, current_pos, dt); float accel_target = PID_Update(&vel_pid, vel_target, current_velocity, dt); float tilt_angle_x = accel_target / 9.81f; // 简化为标准重力注意两个环的dt不一样:位置环的dt是图像帧间隔(约33ms),速度环可以跑得更快,但我们为了简单,两者共用同一个周期。
5.3 坐标分解:一个倾角怎么分给两个舵机
期望倾角是平板坐标系下的向量,需要分解到云台的两个轴上。假设云台的两个旋转轴分别控制平板绕X轴和Y轴旋转,那么直接把期望倾角分解成两个分量就可以:
float servo_angle_x = base_angle_x + K * tilt_angle_x; float servo_angle_y = base_angle_y + K * tilt_angle_y;系数K是通过实测舵机角度和平板倾角的比例关系得到的。每个舵机有一个机械零位,我们用软件做了标定:先把平板调水平,记录舵机角度为base_angle。
这里有个关键点:舵机的转向要和控制方向一致。如果球在右边,需要平板左边抬高(右边降低),如果舵机方向接反了,整个闭环变成正反馈,球会瞬间飞出平板。
6. 调参实录:从球满板跑到定点悬停的过程
6.1 第一阶段:让球别跑出平板
刚开始调参,最怕的是球直接滚出去。我的建议是先把位置环的P值调小,让球的运动反应"迟钝"一点。具体方法:
- 把KI和KD都置0,只留KP
- 手指轻推小球,观察它的反应速度
- KP从小到大慢慢加,直到小球被推离目标位置后能缓慢滚回来
这时候球会有点"懒",反应慢,但至少不会冲出去。这个阶段的目标是确认正反馈方向没接错、映射系数基本对。
我们当时KP初始值是0.3,最后调到约1.8。如果KP超过2.5,球就会在目标点附近明显震荡。
6.2 第二阶段:定点追踪与稳态误差
位置环稳定后,加上速度环。速度环的KP_vel先给到位置环KP的10倍左右,比如18。然后观察球接近目标点时的行为:
- 如果球经过目标点又冲出去,说明速度环不够,增加KP_vel
- 如果球还没到目标点就慢下来,说明速度环过大,减小KP_vel
- 如果球停在目标点附近2~3cm内不再动,说明摩擦力占了主导,这时加KI_vel或位置环KI
第二阶段的调参是最耗时的,因为两个环互相影响。我后来总结的经验是:位置环负责"去哪儿",速度环负责"怎么去"。每调整一次位置环参数,速度环参数就要重新微调一遍。
6.3 第三阶段:抗干扰和鲁棒性
比赛场地和调试场地不一样,光照、平板摩擦系数都有变化。这个阶段要做的:
- 测试不同初始位置下系统是否都能收敛
- 故意快速推球,看系统能否快速恢复
- 模拟光线变化(比如用手遮挡一部分光),看视觉识别是否稳定
我们在这个阶段给视觉加了一个"目标丢失"保护:连续3帧没识别到小球,舵机全部回中,平板恢复水平,防止小球以不可控的方式飞出。
7. 踩坑记录:赛场上最容易翻车的几个细节
7.1 光照突变导致目标丢失
比赛现场灯光和调试时不一样,尤其是有窗户的场地,日照变化会让小球的颜色范围偏移。
我们的应对策略是:在代码里做动态阈值修正。开机后先让摄像头自动曝光几秒,然后取画面中心区域的H通道直方图,找到小球颜色所在的峰,自动确定阈值范围。这个"自动阈值初始化"功能非常实用,比赛现场点按一个按键就能重新校准。
7.2 舵机供电不足引起的抖动
MG996R的峰值电流接近1A,如果用单片机板载的5V供电,电压瞬间跌落会导致舵机抖动,表现为平板在高频微颤,小球无法稳定。
解决方案是独立的5V电源模块给舵机供电,控制信号单独接一根公地线。这个细节很多队伍临场才发现,慌忙换电源,其实提前布线时就应该规划好。
7.3 坐标系方向搞反
听起来很基础,但确实有不少队伍在代码里把X和Y方向搞反,或者把正方向定义反了。排查方法很简单:小球往右推,看程序里输出的位置差是正还是负。如果是正,说明方向是对的。如果反了,要么调代码,要么把摄像头旋转180度。我们选择在代码里加一个标志位:
#define FLIP_X 1 // 1表示翻转,0表示不翻转 #define FLIP_Y -1 // 方向可以独立设置这样在调试时不用改接线,直接改宏定义就能切换方向,效率高很多。
7.4 串口调试的陷阱
我们把PID参数做成结构体数组,通过串口用最简单的文本协议在线修改。核心参数可以实时调整,不用反复烧录程序。
调试时我把目标点坐标、实际坐标、期望速度、实际速度、期望倾角、舵机占空比这些数据每50ms发一次到串口调试助手,画出曲线。没有这些曲线,PID调参基本是盲人摸象。
最后再说一个经验:做控制类赛题,代码写的漂亮不如逻辑清晰。我们最后把整个系统的核心逻辑压缩成不到500行,所有参数集中在一个配置文件里。比赛现场如果发现某个参数不对,能快速定位并修改,这种"代码的组织能力"在紧张的时候比什么都管用。滚球控制系统这道题,做完之后你对闭环控制、视觉识别和系统调优的理解会上一个台阶,这些能力在后来的工作和项目中一直受用。
本文还有配套的精品资源,点击获取