多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

滚球控制系统实战:视觉反馈与PID控制的工程实现

滚球控制系统实战:视觉反馈与PID控制的工程实现 简介2017年全国大学生电子设计竞赛B题滚球控制系统的完整工程代码包面向参加电赛或学习嵌入式视觉控制的学生与开发者。系统以OpenMV摄像头追踪小球位置通过蓝牙将坐标发送至STM32控制端结合PID算法驱动电机实现滚球控制覆盖图像处理、无线通信与闭环控制等关键环节。压缩包共254个文件约8.76MB主要包括Keil工程文件uvproj/uvopt、C语言源码与头文件c/h、编译生成的目标文件与汇编o/axf/s、烧录固件hex以及脚本和备份文件bat/bak目录结构便于直接打开工程查看或重新编译。目前已有1810人学习下载适合希望快速上手完整赛题方案、参考硬件驱动与图像识别代码、梳理PID控制流程的参赛者和嵌入式爱好者。1. 题面回顾与整体方案选型1.1 这一题到底在考什么2017年电赛B题“滚球控制系统”题面核心一句话概括让一个金属小球在一个可双向倾斜的平板上稳定到指定位置并能沿给定轨迹运动。听起来像玩具实际做起来是集机械、视觉、控制、嵌入式于一身的综合题。先说清楚系统工作逻辑平板绕X轴和Y轴两个方向倾斜小球受重力分量驱动在平板上滚动。控制系统需要实时获取小球的位置坐标和期望位置求偏差通过控制算法计算出平板应该倾斜的角度再由舵机或电机驱动平板动作形成闭环。整个过程对实时性要求很高典型的视觉反馈闭环系统。从当年参赛队伍的方案分布来看大致分三类方案类型视觉方案主控方案优缺点A类OpenMV摄像头模块STM32F4系列开发快视觉端独立处理串口通信适合赶时间B类普通摄像头上位机图像处理上位机STM32图像处理灵活但需要多设备联调现场稳定性考验大C类摄像头直连高性能MCU树莓派/全志等嵌入式Linux算力强可跑复杂算法但功耗和实时性要平衡我当年采用的是A类方案OpenMV做图像识别输出小球坐标STM32做主控跑PID控制算法驱动舵机改变平板角度。这套方案最大的好处是视觉和控制解耦调试时可以分别验证不用一上来就面对“全链路不工作”的噩梦。1.2 系统构成与机械结构要点整个系统拆开看核心模块就四个视觉检测模块、主控计算模块、执行机构模块、机械平台结构。机械结构是我第一个踩坑的地方。平板和底座之间必须用万向节或者类似结构连接确保平板能以中心为支点做两个自由度的旋转。很多队伍用两个舵机叠加的云台结构一个舵机控制俯仰一个控制横滚下面一个舵机要驮着另一个舵机结构重量大容易产生机械抖动。我当时的做法是底座固定一个数字舵机通过连杆结构驱动平板绕X轴旋转另一个舵机垂直安装驱动平板绕Y轴旋转。这样两个舵机都固定在底座上平板上的负载很小转动惯量低响应快得多。连杆结构需要自己用3D打印或者铝合金件加工精度要求挺高如果连杆虚位大了控制死区会让你怀疑人生。机械部分的经验总结第一舵机选型至少用20kg级别的数字舵机金属齿轮不然平板带球运行时根本稳不住第二所有连接件尽量用轴承减少摩擦阻力第三平板的平面度非常关键如果平板本身是翘的PID参数调到天边也救不回来。2. 代码工程结构与数据流设计2.1 代码模块划分的底层逻辑写这套代码最大的体会是竞赛代码和平时练手的例程完全是两码事。平时写代码追求“能跑”竞赛代码追求“在规定的几十秒内稳定跑完整个赛程”。所以代码结构设计的第一步不是急着写逻辑而是先把数据流理清楚。我的代码整体分三大模块视觉模块OpenMV端、通信协议层、控制执行模块STM32端。视觉模块负责的事情比较纯粹通过摄像头采集图像识别小球位置把坐标算出来。OpenMV上的Micropython代码简单直接就是初始化传感器、设置阈值、找色块、计算重心。通信协议层是最容易被低估的部分。因为OpenMV和STM32通过串口通信数据帧格式必须自己定。我定义的数据帧是帧头0xAA 数据长度 数据位X坐标、Y坐标、标志位 校验位。X和Y坐标各占两个字节先高后低这样可以在一次传输里同时传递坐标和当前状态。控制执行模块是STM32端的核心包含三个子模块串口解析、期望轨迹生成、PID控制器。数据流向 摄像头采集 - OpenMV图像处理 - 小球坐标(x,y) - 串口协议帧 - STM32串口中断接收 - 数据校验解析 - 与期望坐标(x_ref, y_ref)求偏差 - PID计算 - 舵机PWM输出 - 平板倾斜 - 小球滚动 - 摄像头再次采集闭环理清数据流之后写代码就变成填空了。每个模块的输入输出都明确测试的时候可以单独喂数据验证不用每次都要整个系统跑起来。2.2 控制周期与任务调度设计控制系统的实时性是所有代码设计中优先级最高的一件事。我的系统采用的是固定控制周期设计核心控制环运行在5ms定时器中断中也就是200Hz的控制频率。这个频率怎么定的主要考虑因素有三个舵机响应速度数字舵机典型响应时间在50ms到200ms级别控制频率太高意义不大、OpenMV的帧率实际稳定跑30fps左右即大约33ms一帧新数据、PID微分项的噪声敏感性频率太高会放大噪声。这里有个重要的实际经验控制周期和视觉数据的到达周期是不匹配的200Hz的控制率但视觉数据只有30Hz左右。如果直接把上一帧坐标用在控制里小球可能已经跑出去很远了控制必然滞后。我的做法是在主控里做坐标缓存和外推预测当收到最新一帧坐标时记录该帧的时间戳控制环计算时读取最近一帧坐标并乘以一个根据实际调试得出的速度修正系数相当于简单预测了小球在当前时刻的位置。这个优化看似不起眼实测对系统稳定性的提升非常大。整个任务的调度结构主循环非阻塞 while(1) { 处理串口接收数据帧中断环形缓冲区 更新期望轨迹点根据任务状态机 非实时状态刷新OLED显示、按键扫描 } 定时器中断5ms 读取最近状态缓存 计算PID输出 更新舵机PWM占空比状态机也是很关键的设计。赛题有多个任务定点停留、直线运动、圆周运动等。我把这些做成一个简单的枚举状态机主循环根据状态决定当前执行的轨迹类型定时器中断只负责执行“位置偏差-PID-输出”这条固定流水线不关心现在跑什么轨迹。这样轨迹切换的时候控制环完全不需要改动。3. 核心算法模块拆解与代码实现3.1 图像处理从像素到坐标OpenMV端识别小球最常用的是色块识别函数find_blobs()。但直接调用API只能拿到一个色块的矩形框我们需要的是小球在图像中的具体中心坐标。小球一般选择橙色或者红色和白色平板背景对比度高阈值设置也相对容易。图像处理的几个关键点第一ROI区域裁剪。平板在摄像头视野内是固定区域我直接在初始化时获取平板四个角的图像坐标设为ROI。只在这个区域内做色块查找能大幅减少误识别也能提高帧率。第二用色块的中心作为小球位置。blob.cx()和blob.cy()返回的是色块中心对小圆球来说这和球心位置偏差很小可以接受。但如果小球快速运动图像会有运动模糊色块区域会拉长这时候取中心点会有几个像素的误差在控制上体现为几毫米的偏差。解决方法是适当增大曝光时间换取更亮更清晰的图像或者改用边缘检测加灰度重心法。第三多目标干扰处理。如果赛场环境有其他橙红色物体混进ROIfind_blobs会返回多个结果。我加了面积和圆度双重过滤条件只保留面积在合理区间且圆度接近1的目标。核心代码片段import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time2000) sensor.set_auto_gain(False) # 关闭自动增益防止亮度变化导致阈值漂移 sensor.set_auto_whitebal(False) uart UART(3, 115200, timeout_char1000) THRESHOLD (30, 80, 20, 80, 30, 80) # 橙色小球LAB阈值 ROI (60, 40, 200, 120) # 平板区域需要根据实际标定 while True: img sensor.snapshot() blobs img.find_blobs([THRESHOLD], roiROI, pixels_threshold20, area_threshold20) if blobs: # 面积过滤 圆度过滤 best_blob max(blobs, keylambda b: b.area()) if best_blob.area() 100 and best_blob.roundness() 0.5: x best_blob.cx() y best_blob.cy() # 组帧发送帧头 0xAAX高字节X低字节Y高字节Y低字节校验 data bytearray([0xAA, x 8, x 0xFF, y 8, y 0xFF, (x ^ y) 0xFF]) uart.write(data)这里特别提一下自动增益和自动白平衡这是新手最容易忽略的坑。如果不关闭当环境亮度或者小球位置变化时摄像头的增益会自己调整同样的颜色在不同时刻测出的阈值完全不一致导致时而识别不到、时而误识别。调好一次阈值后固定参数是最稳妥的做法。3.2 坐标映射图像坐标到物理坐标的标定摄像头拿到的是图像像素坐标几百x几百但PID控制需要的是平板上的物理坐标或者归一化坐标。如果直接用像素坐标做控制镜头畸变在边缘区域会造成很大的控制误差小球在平板中间稳了到边缘就明显跑偏。我采用的方案是四点标定法在平板的四个角各做一个标记记录它们在图像中的像素坐标同时知道它们的物理坐标比如定义平板中心为原点长宽方向上归一化到-100到100。用透视变换把图像坐标转换到物理坐标。数学上这是一个单应性变换矩阵求解问题。四个点对可以求解8个自由度的单应矩阵。OpenMV的库函数里没有直接封装透视变换的API我是在STM32端实现的坐标解算。事先在电脑上用Python离线算出变换矩阵系数固化在代码常量里。// STM32端简化实现假设已通过标定得到系数 a0~a7 // 物理坐标X_phys (a0*x_img a1*y_img a2) / (a6*x_img a7*y_img 1) // Y_phys (a3*x_img a4*y_img a5) / (a6*x_img a7*y_img 1) float x_phys (a0*x a1*y a2) / (a6*x a7*y 1); float y_phys (a3*x a4*y a5) / (a6*x a7*y 1);标定的操作要注意几点四个标定点尽量覆盖整个平板区域不要集中在中间否则外推边缘误差很大标定时平板必须处于水平状态每次挪动摄像头或者平板位置后需要重新标定。这个矩阵的关键作用不只是矫正畸变还能把图像坐标系的原点对齐到平板中心让控制变量的物理意义更清晰。3.3 PID控制位置环加前馈控制算法的核心是一个双通道PIDX轴和Y轴独立控制。最朴素的想法就是两个独立的PID分别把小球拉向目标坐标。但实际跑起来你会发现纯位置PID在小球接近目标时会反复震荡难以稳定。原因在于小球是无自平衡能力的对象没有阻尼平板一倾斜就加速它相当于一个二阶积分环节位置PID要稳住这种对象本质上需要靠微分项的阻尼作用。单纯调大比例增益会出现极限环震荡调大微分项会放大噪声。我的解决方案是位置式PID加微分先行并加入速度前馈typedef struct { float kp, ki, kd; float integral; float last_err; float target; } PID_t; float PID_Update(PID_t *pid, float measured, float dt) { float err pid-target - measured; pid-integral err * dt; // 积分限幅 if (pid-integral 300) pid-integral 300; if (pid-integral -300) pid-integral -300; float derivative (measured - pid-last_measured) / dt; // 微分先行的关键 float output pid-kp * err pid-ki * pid-integral - pid-kd * derivative; pid-last_err err; pid-last_measured measured; return output; }微分先行和标准PID的区别在于微分项作用于被控量小球位置的变化率而不是偏差的变化率。这样目标值突变比如从点A切换到点B时微分项不会产生巨大的冲击系统不会猛地抖一下。速度前馈是这样加的当执行圆轨迹或直线轨迹时目标点本身在运动小球的期望速度不为零。单纯的位置PID输出的是把小球拉向目标点的力但如果目标点本身在移动就相当于一直在追一个动点。我的做法是根据期望轨迹计算出目标点的运动速度乘以一个前馈系数加到输出上。这个前馈系数只有纯位置PID输出的一半左右但对轨迹跟踪精度的提升很明显跟踪圆形轨迹的半径误差能缩小到一个很小的范围。4. 调试实战从能跑到跑稳的完整链条4.1 分步调试法先开环后闭环拿到代码的第一件事我强烈建议先做开环测试不要一上来就跑闭环。第一步测试视觉链路。把OpenMV单独上电在OLED或者串口输出里看小球坐标是否连续、平滑坐标在平板不同位置是否和预想一致。这个环节最容易发现的是图像阈值问题、标定矩阵用错的问题、串口通信丢帧的问题。第二步测试舵机执行链路。用STM32直接发不同脉宽的PWM信号观察平板倾斜方向是否正确、响应是否灵敏。这里注意舵机的脉宽范围和旋转方向同一型号的舵机不同批次可能反着转直接写死很容易翻车。第三步纯P控制测试。把KI和KD设为0只保留比例项从很小的KP开始加。观察小球在平板上是否被“推”向目标如果有来回震荡就降低KP如果响应太慢就增大一点。纯P能让小球稳定在一个范围内就算是第一步成功。第四步逐步加D和I。先加微分项压制震荡再加积分项消除静差。积分项一定要限幅这一点前面代码里已经体现了。4.2 现场调试的实战经验比赛现场和实验室环境差别很大灯光的位置、亮度完全不一样摄像头看到的画面可能和调参时完全不同。这是每年电赛翻车最多的地方。我当时的做法是在OpenMV初始化时加入一个阈值自动适配逻辑。上电后先读取当前环境光下的图像取ROI区域内最亮和最暗的像素值范围结合预设的色差偏移动态生成阈值。这个逻辑只在上电时执行一次运行期间阈值不变化避免图像处理在循环中自适应带来的不确定抖动。还有现场电磁干扰问题。舵机是强干扰源大电流变化会通过电源线影响主控和摄像头。我的做法是每个模块独立供电舵机用单独的7.4V电池主控和摄像头用5V电源通过光耦隔离或者是至少用共地加粗导线来缓解。实测下来如果共用一个电源舵机启动瞬间电压跌落非常明显摄像头画面会闪甚至直接重启。4.3 PID参数整定的实用技巧PID参数整定很多人喜欢用试凑法一顿乱调最后调出来的系统可能刚好能过测试但稍微换个环境就垮了。我建议采用临界比例度法的思路来做系统性的参数整定。做法先只保留比例项逐步增大KP直到系统出现等幅震荡记录此时的KP值为Ku震荡周期为Tu。然后根据经验公式计算初始参数再在这组参数附近微调。当年调试出来的参数大概在一个范围内但不同机械结构的系统差异很大——比如我后期换了一组舵机参数就必须重新调。给一个参考方向比例系数一般在2到8之间微分系数在比例系数的5到10倍之间积分系数很小主要用来消除静态误差调大了很容易引发低频震荡。在整定过程中有两个细节值得特别留意。一个是在线修改参数的功能调试时用按键或者串口命令实时调整参数比重新烧录程序快太多了。另一个是用OLED实时显示PID的各个环节的值定位问题快比如看到输出一直顶到上限但小球纹丝不动基本可以判断是机械卡死而不是PID参数问题。5. 高频故障与排查手册5.1 常见故障速查表现象可能原因优先级排查项小球静止时来回震荡微分项过小阻尼不足增大KD检查微分项是否有效小球偏向一侧无法居中积分项过小时有静差或机械零点偏了检查平板水平度增加KI小球跑飞完全不受控坐标映射错误或舵机方向反了先检查跟踪的坐标是否正确再看PWM方向摄像头找不到小球阈值不对、光照变化、小球颜色被反光显示二值化图像确认关闭自动增益控制反应明显迟钝控制周期太长或串口丢帧严重检查串口波特率确认帧校验机制是否有效舵机嗡嗡响且发烫控制频率过高或输出抖动频繁降低控制频率输出加死区5.2 独家避坑经验写代码时最大的一个教训串口中断接收里千万不要做耗时操作。我早期在串口中断处理里直接解析数据并更新PID目标值结果导致中断函数执行时间太长主循环被阻塞得厉害。正确做法是中断里只把数据扔进环形缓冲区在主循环里做解析。还有一个我花了一整天才解决的诡异问题小球在某一个特定位置附近总是出现规律性偏转。排查到最后发现是摄像头支架的共振造成的。摄像头固定在一个较长的金属支架上舵机工作时支架产生高频微振动导致图像坐标周期性漂移。后来把摄像头支架加粗加固后问题彻底消失。调试时建议把重要数据通过串口打印或者无线模块发到电脑上做成实时曲线。很多队伍在实验室里看OLED上的数字变化完全看不出趋势。我在笔记本上用匿名上位机或者直接用Python的matplotlib做实时绘图把小球的实际轨迹画出来调试效率提升非常明显。5.3 功能扩展思路如果这套系统想继续深入做有两个方向很值得扩展。一个是把控制算法升级为串级PID结构内环控制平板角速度外环控制小球位置响应速度会有质的提升。另一个是在轨迹规划上下功夫赛题要求画圆或走直线时目前的简单逐点逼近方案在高速下精度有限可以考虑前瞻加轨迹插值让运动更顺滑。比赛的代码就是这样一套东西从原理到落地每一步都有坑每一步也都有收获。希望这篇内容能帮到准备电赛的朋友们特别是那些正在为B题代码头疼的队友们。调试的时候保持耐心分模块去验证不要指望一次点亮剩下来的事情就交给时间和手感了。本文还有配套的精品资源点击获取
返回列表