简介这份资源是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系列为什么够用主控的选择直接决定了视觉算法的上限。我们用STM32F407VET6168MHz主频带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行所有参数集中在一个配置文件里。比赛现场如果发现某个参数不对能快速定位并修改这种代码的组织能力在紧张的时候比什么都管用。滚球控制系统这道题做完之后你对闭环控制、视觉识别和系统调优的理解会上一个台阶这些能力在后来的工作和项目中一直受用。本文还有配套的精品资源点击获取