1. 项目概述一个能自主平衡小球的移动机器人平台几年前我在一个机器人爱好者社区里看到了一个用舵机控制平板保持小球平衡的创意当时就觉得这个想法既酷又充满了工程挑战。它不像简单的循迹小车而是将视觉感知、实时控制和机械结构紧密耦合在一起是一个检验综合能力的绝佳项目。于是我决定动手复现并深度优化这个“蝎子平衡板”Scorpion Balance Board目标不仅是让小球不掉下来还要让整个系统足够鲁棒、响应迅速并且能作为一个完整的移动平台被遥控。这个项目的核心简单说就是让一块板子“看见”球并“思考”如何动才能让球始终待在板子中央。它涉及三个关键层感知层摄像头捕捉图像识别小球位置、决策层PID算法根据位置误差计算舵机应有的角度、执行层舵机驱动平板在X和Y轴上倾斜。此外我们还为整个底盘增加了移动能力使其成为一个可以遥控的平衡机器人。整个过程从机械结构设计、电路焊接到图像处理算法调优、PID参数整定再到多进程服务管理每一步都充满了“坑”和“惊喜”。如果你对嵌入式系统、计算机视觉或控制理论感兴趣想找一个能串联起多个知识点的实战项目那么这篇详细的构建与调试笔记或许能给你提供一条清晰的路径。2. 系统整体架构与核心组件选型在动手之前清晰的顶层设计至关重要。这个项目不是一个单一的设备而是一个由感知、计算、控制、驱动和能源多个子系统协同工作的复合体。我的设计思路是高层的视觉处理与逻辑决策交给性能较强的 Raspberry Pi底层的实时电机脉宽调制PWM控制交给响应迅速的 Arduino两者通过串口通信各司其职。2.1 硬件架构解析硬件是项目的骨架选型直接决定了系统的性能和稳定性。以下是经过实际验证的组件清单及其选型理由主控制器大脑Raspberry Pi 3 Model B。选择它的原因很直接它有足够的计算能力运行 OpenCV 进行实时的图像处理尽管需要优化拥有丰富的 GPIO 引脚控制舵机并且原生支持 Raspberry Pi Camera 的 CSI 接口获取图像数据延迟低。Pi 4 当然更好但 Pi 3 的性能对此项目已绰绰有余且功耗和发热更低。视觉传感器眼睛Raspberry Pi Camera Module v3。相比之前的版本v3 在动态范围和低光性能上有提升。关键在于使用 CSI 接口而非 USB 摄像头可以大幅降低 CPU 占用率并获得更稳定的帧率这对于需要 30fps 实时处理的系统是必须的。实时控制器小脑Arduino Nano。为什么需要它虽然树莓派能产生 PWM 信号但其基于操作系统的软 PWM 在精度和稳定性上特别是在控制直流电机这种需要快速、精确换向的场景下不如 Arduino 的硬件 PWM 可靠。Arduino 专为实时控制而生代码循环极快确保对电机驱动指令的响应无延迟。执行机构肌肉舵机用于倾斜平板2个20kg 数字舵机如 SF3218MG。这是关键选择。平衡板需要快速、有力地响应。20kg 的扭矩足以驱动我们设计的机械臂和平板。数字舵机比模拟舵机具有更高的定位精度和更快的响应速度内部通常带有 PID 控制器能更准确地到达指定角度。直流电机用于底盘移动2个TT 减速电机配车轮。选择带减速箱的电机在 12V 供电下能提供足够的扭矩带动整个平台移动。具体型号需根据平台重量选择通常 1:48 或 1:120 减速比的电机较为常见。驱动与电源心脏与血管电机驱动L298N 双 H 桥驱动模块。这是经典选择可以同时驱动两个直流电机实现正反转和调速。它完全兼容 Arduino控制逻辑简单。电源管理一块12V 3000mAh 的 NiMH 电池组。为什么用镍氢相对于锂电它更安全无需复杂的保护板适合原型开发。使用两个LM2596 降压模块分别将 12V 降至 7.2V供舵机和 5V供树莓派和 Arduino。这里有个重要细节舵机在动作瞬间电流很大可能会引起电压骤降干扰树莓派。因此将舵机电源与计算单元电源分开降压可以有效避免这个问题。输入设备神经一个支持蓝牙的游戏手柄如 PlayStation 或 Xbox 手柄的蓝牙模式。无线操控是移动平台的必备功能蓝牙方案集成度高于传统的 2.4G RF 接收器。注意电源是整个系统稳定的基石。务必确保电池容量足够并且 LM2596 模块的散热良好。我曾因一个降压模块过热导致输出不稳使得树莓派意外重启排查了许久。2.2 软件架构与通信流程软件上我采用了“服务化”的架构将不同功能解耦提高系统的可靠性和可维护性。所有服务在树莓派启动时自动运行。摄像头与平衡服务这是核心循环。一个 Python 脚本持续抓取摄像头画面进行 HSV 颜色过滤和轮廓查找计算出小球的 (x, y) 像素坐标。随后两个独立的 PID 控制器一个管 X 轴一个管 Y 轴根据小球坐标与画面中心点的偏差计算出舵机需要转动的角度并通过 GPIO 输出 PWM 信号驱动舵机。手柄服务另一个 Python 脚本使用pygame或evdev库监听已配对的蓝牙手柄的按键事件。它将复杂的按键映射转化为简单的运动指令如前进‘F’ 左转‘L’。通信转发服务第三个 Python 脚本作为“桥梁”。它从手柄服务获取运动指令通过 USB-TTL 串口/dev/ttyUSB0发送给 Arduino Nano。同时它也可以设计为双向通信未来可以接收 Arduino 回传的传感器数据如电池电压。Arduino 固件Arduino 端的 C 代码非常简单。它循环监听串口一旦收到预定义的字符指令如 ‘F’, ‘L’就调用对应的函数通过 L298N 驱动两个直流电机执行前进、后退、左转、右转或停止的动作。通信流程图可以这样理解蓝牙手柄 --[蓝牙]-- 树莓派手柄服务 | v 树莓派摄像头/PID服务--[GPIO PWM]-- 舵机XY轴 | v 树莓派通信服务--[USB串口]-- Arduino Nano | v L298N驱动板 -- 直流电机左右轮这种架构确保了即使某一个服务比如手柄连接断开崩溃也不会影响最关键的平衡功能提升了系统的整体鲁棒性。3. 核心环节一基于HSV色彩空间的视觉定位让机器“看见”并“理解”小球的位置是整个反馈控制 loop 的起点。最直接的想法可能是用复杂的深度学习模型但对于这个颜色鲜明、背景相对固定的场景杀鸡焉用牛刀。我选择了HSV 色彩空间阈值分割这条经典而高效的路径。3.1 为什么是HSV而不是RGBRGB色彩空间对我们人类很直观但对光照变化极其敏感。早上阳光下的绿色小球和晚上台灯下的绿色小球其RGB值可能相差甚远。HSV色彩空间将颜色信息色调Hue、鲜艳程度饱和度Saturation和明暗亮度Value分离开来。其中色调H基本代表了物体的颜色受光照强度变化的影响相对较小。这意味着我们只要定义好小球颜色的H值范围就能在很大程度的光照变化下稳定地识别出它。实操步骤校准与获取HSV范围编写一个简单的校准脚本这个脚本实时显示摄像头画面并允许你用鼠标点击小球区域打印出该点的HSV值。import cv2 import numpy as np def mouse_callback(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: hsv_value hsv_frame[y, x] print(fClicked at ({x}, {y}): HSV {hsv_value}) cap cv2.VideoCapture(0) # 如果是Pi Camera需用picamera库 cv2.namedWindow(Calibration) cv2.setMouseCallback(Calibration, mouse_callback) while True: ret, frame cap.read() if not ret: break hsv_frame cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) cv2.imshow(Calibration, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()多次采样在不同光照条件下开灯、关灯、靠近窗户多次点击小球的不同部位记录下一系列H、S、V的值。确定阈值范围观察记录的数据。假设小球的H值大约在35-85之间绿色S值在50-255颜色较鲜艳V值在30-255不太暗。那么我们的阈值下限就是(35, 50, 30)上限是(85, 255, 255)。这里的经验是H的范围可以给得稍宽以防万一但S和V的下限不要设得太低否则容易把背景中灰暗的杂物也识别进来。3.2 图像处理流程与优化获取阈值后完整的处理流程如下捕获帧使用picamera库针对Pi Camera优化以30fps捕获图像并立即转换为NumPy数组。转换色彩空间cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)。阈值分割cv2.inRange(hsv_frame, lower_green, upper_green)得到一张二值图像白色区域即可能是小球。形态学操作这是去噪的关键步骤。先使用cv2.erode()腐蚀操作去除小的白色噪点再使用cv2.dilate()膨胀操作让小球的白色区域恢复连接。这个“先腐蚀后膨胀”的过程称为“开运算”。寻找轮廓cv2.findContours()在二值图像上寻找白色区域的轮廓。定位中心在所有轮廓中找到面积最大的那个假设它就是小球。用cv2.moments()计算该轮廓的矩进而得到其质心坐标(cx, cy)。这个坐标就是小球在图像像素坐标系中的位置。实操心得在树莓派上OpenCV的处理速度是瓶颈。为了达到稳定的30fps控制周期我做了以下优化降低分辨率将摄像头分辨率从默认的1920x1080降至640x480甚至320x240。对于这个应用低分辨率足以定位小球且处理速度能提升数倍。限制ROI感兴趣区域小球不会瞬间飞到画面边缘可以只对图像中央的一大块区域进行处理进一步减少运算量。使用imutils库的优化函数imutils库中的一些轮廓查找和定位函数有时比直接使用OpenCV原函数更高效。4. 核心环节二双轴PID控制器的设计与调参拿到小球的(cx, cy)坐标后我们需要将其转换为舵机的动作指令。这里PID控制器扮演了“大脑”的角色。我们需要两个完全独立的PID控制器PID_X控制X轴左右方向舵机PID_Y控制Y轴前后方向舵机。4.1 PID算法原理与代码实现PID是比例Proportional、积分Integral、微分Derivative控制的合称。其核心思想是根据当前误差P、过去一段时间的误差累积I和误差变化的趋势D共同计算出一个控制量。比例项P与当前误差成正比。误差越大控制动作越强。单纯P控制会产生静差小球停在离中心点不远的地方无法完全对准。积分项I累积历史误差。用来消除静差。但I太强会导致系统超调甚至振荡。微分项D与误差的变化率成正比。具有“预见性”能抑制系统振荡提高稳定性。在Python中我们可以实现一个简单的离散PID类class PID: def __init__(self, Kp, Ki, Kd, setpoint, output_limits(-100, 100)): self.Kp Kp self.Ki Ki self.Kd Kd self.setpoint setpoint self.output_limits output_limits self._prev_error 0 self._integral 0 self._last_time time.time() def compute(self, measured_value): current_time time.time() dt current_time - self._last_time if dt 0: dt 1e-16 # 避免除零 error self.setpoint - measured_value # 比例项 P self.Kp * error # 积分项带抗饱和 self._integral error * dt I self.Ki * self._integral # 微分项用误差的差分近似微分 derivative (error - self._prev_error) / dt D self.Kd * derivative # 计算总输出 output P I D # 输出限幅 output max(self.output_limits[0], min(self.output_limits[1], output)) # 更新状态 self._prev_error error self._last_time current_time return output对于我们的系统setpoint是图像的中心坐标如(320, 240)对于640x480的图像measured_value是小球当前的cx或cy。output就是舵机需要调整的角度增量或直接的PWM占空比。4.2 “手动整定”PID参数的实战过程PID调参是一门艺术也是这个项目最耗时、最考验耐心的部分。我遵循经典的“先P后I再D”的试凑法并在调参时关闭了另一个轴的控制器以隔离影响。初始化将所有参数Kp,Ki,Kd设为0。将小球手动放在板子中央附近。调比例P逐渐增大Kp。你会发现板子开始对小球的偏移有反应了。目标找到一个Kp值使得当小球被轻轻拨动后板子能将其快速拉回中心区域但会在中心点附近出现小幅、缓慢的振荡。如果振荡发散越来越大说明Kp太大了如果响应太慢小球总是掉出去说明Kp还不够。假设我们找到了一个初步的Kp 1.5。调积分I引入一个很小的Ki如 0.01。积分项的作用是消除静差。观察小球是否更能稳定在绝对中心。注意Ki太大会导致系统在中心点附近来回剧烈振荡甚至失控。需要非常小心地微调。我们的目标是让小球最终能稳稳停在中心没有静态偏差。调微分D在P和I使系统基本稳定但仍有轻微振荡时引入Kd。Kd能“阻尼”这种振荡。逐渐增大Kd观察振荡是否被抑制。Kd太大也会引入高频噪声可能导致舵机抖动。可能一个较小的值如Kd 0.05就能起到很好效果。微调与耦合单独调好一个轴后用同样的流程调另一个轴。然后让两个轴同时工作。由于机械结构上X轴和Y轴可能存在轻微的耦合动一个舵机会轻微影响另一个方向可能需要回头对四个参数Kp_x,Ki_x,Kd_x,Kp_y,Ki_y,Kd_y进行微小的整体微调。踩坑记录我的第一次调参失败是因为没有对PID的输出进行限幅。当小球突然滚到边缘误差极大PID计算出一个巨大的输出值直接驱动舵机打到极限位置产生剧烈的撞击差点损坏机械结构。务必在PID类中设置output_limits将其限制在舵机安全的角度范围内。5. 机械结构与系统集成一个好的算法需要一个稳固的“身体”来执行。机械结构的设计直接影响到控制的精度和系统的寿命。5.1 结构设计与3D打印我的设计包含以下几个核心部件均使用FDM 3D打印机如Creality Ender-3以PLA材料打印平衡板25cm x 25cm 的方形平板周边有一圈矮围栏防止小球瞬间滚落给予控制系统反应时间。板底中央有十字加强筋以增加刚度。舵机摇臂连接舵机轴和平板的L型臂。这里的核心是“杠杆比”。舵机本身的旋转角度有限通常180度或270度需要通过摇臂的长度将舵机的旋转运动转换为平板边缘足够的升降位移。需要计算好长度使得在舵机有效角度范围内平板能倾斜到足够的角度例如±15度来追回小球。十字轴支架万向节结构这是实现双轴自由度的关键。我使用了一个6mm 的十字方向节联轴器作为核心旋转关节。两个舵机分别控制这个十字轴的两个方向从而带动与之固连的平衡板实现前后、左右的倾斜。支架需要精确设计轴承座来固定舵机和十字轴确保转动顺滑且无虚位。主底盘容纳电池、树莓派、Arduino、驱动板等所有电子设备并安装两个驱动轮和一个万向轮。装配要点所有轴承连接处可涂抹少量润滑脂减少摩擦。舵机与摇臂、摇臂与平板之间的连接务必使用螺丝紧固并考虑使用螺丝胶防止振动松脱。电子设备用尼龙扎带或螺丝固定在底盘内线材用缠绕管收纳避免缠绕到运动部件中。5.2 电路连接与系统上电按照之前的架构图进行连接电源部分电池正负极接入第一个LM2596的Vin调节其输出至7.2V为两个舵机供电。再从电池正负极并联接入第二个LM2596调节输出至5.0V为树莓派和Arduino供电。务必确保地线GND共地即电池的GND、两个LM2596的GND、树莓派的GND、Arduino的GND最终都要连接在一起。树莓派部分CSI摄像头排线插入专用接口。两个舵机的信号线通常是橙色或白色分别连接到树莓派GPIO的指定引脚如GPIO17和GPIO18舵机的电源红色和地线棕色接入7.2V电源轨。Arduino部分L298N的输入引脚IN1, IN2, IN3, IN4分别连接到Arduino的D2, D3, D4, D5具体可根据程序定义调整。L298N的电源端接12V电池使能端ENA和ENB可以接PWM引脚如D9, D10进行调速或直接接5V使其全速。Arduino通过USB线供电也由5V电源轨提供。上电顺序建议先接通5V电源让树莓派和Arduino启动。待系统启动完毕再接通7.2V舵机电源。这样可以避免舵机在系统未初始化时产生误动作。6. 软件服务的部署与自启动为了让项目真正成为一个独立的“产品”我们需要让所有程序在树莓派上电后自动运行且能稳定地在后台工作。6.1 将Python脚本转化为系统服务我使用systemd来管理服务这是Linux系统的标准方式。创建服务文件例如为平衡服务创建balance.service。sudo nano /etc/systemd/system/balance.service编辑服务内容[Unit] DescriptionScorpion Balance Board PID Control Service Aftermulti-user.target [Service] Typesimple Userpi ExecStart/usr/bin/python3 /home/pi/scorpion_balance/balance_main.py WorkingDirectory/home/pi/scorpion_balance Restarton-failure RestartSec5 [Install] WantedBymulti-user.targetExecStart指定你的主平衡控制Python脚本的完整路径。Restarton-failure非常重要如果程序因任何原因崩溃如摄像头意外断开系统会自动在5秒后重启它极大增强了鲁棒性。Userpi以pi用户运行避免权限问题。同样地为手柄服务joystick.service和通信服务comms.service创建对应的服务文件。启用并启动服务sudo systemctl daemon-reload sudo systemctl enable balance.service joystick.service comms.service sudo systemctl start balance.service joystick.service comms.service检查服务状态sudo systemctl status balance.service可以查看服务是否正常运行以及最新的日志。6.2 蓝牙手柄的自动配对与连接这是实现无线遥控的关键也是另一个容易出问题的地方。目标是让树莓派在启动后自动搜索并连接指定的蓝牙手柄。手动配对一次首次使用通过树莓派桌面GUI或命令行bluetoothctl工具完成与手柄的配对和信任。编写自动连接脚本创建一个脚本例如auto_connect_joystick.sh使用bluetoothctl的命令模式进行连接。#!/bin/bash # 替换 XX:XX:XX:XX:XX:XX 为你的手柄蓝牙MAC地址 JOYSTICK_MACXX:XX:XX:XX:XX:XX while true; do # 检查手柄是否已连接 if ! bluetoothctl info $JOYSTICK_MAC | grep -q Connected: yes; then echo Joystick not connected. Attempting to connect... # 尝试连接 bluetoothctl connect $JOYSTICK_MAC fi sleep 10 # 每10秒检查一次 done将该脚本也设为系统服务参照上面的方法创建一个joystick-autoconnect.service让这个守护脚本在后台运行。这样即使手柄中途断开比如电量低它也会自动重连。7. 调试实录常见问题与解决方案在项目集成和调试阶段我遇到了无数问题。以下是其中最典型的一些及其解决方法希望能帮你节省大量时间。7.1 视觉识别不稳定小球时隐时现现象在图像中小球的轮廓有时能找到有时找不到导致PID控制中断板子乱动。排查检查HSV阈值光照条件变化是主因。在一天中的不同时间测试你的阈值。考虑使用自适应阈值算法或者增加一个简单的“自动白平衡”预处理步骤。检查形态学操作腐蚀和膨胀的核大小 (kernel) 很关键。核太小去不掉噪点太大会“吃”掉小球。通过实时调试窗口观察二值化图像的效果动态调整核大小如从3x3调到5x5。背景干扰确保摄像头视野内没有其他颜色与小球相近的物体。如果无法避免可以尝试在识别轮廓后不仅根据面积过滤还根据轮廓的圆度 (cv2.isContourConvex或计算轮廓面积与最小外接圆面积之比) 来筛选因为小球大概率是圆形的。解决我最终在代码中增加了一个“状态保持”逻辑。如果当前帧没找到轮廓则使用上一帧小球的位置。连续丢失多帧如10帧后才认为小球真的丢失此时将舵机回中或进入安全模式。7.2 舵机抖动或产生“嗡嗡”声现象即使小球静止在中心舵机也在轻微地、高频地抖动并发出噪音。排查PID微分项D过冲过大的Kd会放大图像噪声导致输出高频振荡。尝试降低Kd。输出限幅或死区设置为PID输出设置一个“死区”。例如当输出值的绝对值小于2时直接设为0。这样可以避免系统为了追求绝对零误差而进行无谓的、耗能的微调。电源问题舵机电源电压不稳或电流不足。用万用表测量舵机动作时的电压如果压降严重需要考虑更换更大功率的电源或增加电容缓冲。机械阻力检查十字轴、摇臂的连接处是否转动不顺畅增加了舵机的负载。解决我主要通过对PID输出增加死区并确保7.2V电源轨上有足够大的滤波电容如1000uF来缓解这个问题。7.3 底盘移动控制不精确或延迟大现象通过手柄控制底盘移动时响应慢或者左右轮速度不一致导致走不直。排查串口通信延迟检查树莓派与Arduino之间的串口波特率是否设置一致如115200并确保没有在循环中频繁打印大量调试信息到串口。电机驱动问题L298N模块的使能端ENA, ENB如果未启用PWM电机只能全速或停止无法调速。确保它们连接到了Arduino的PWM引脚带~标识并在代码中使用了analogWrite()。电池电量电量不足时电机转速会下降。确保电池充满电。机械差异两个TT电机即使型号相同也存在细微的性能差异。需要在Arduino代码中为每个电机设置一个微调系数。解决我在Arduino代码中实现了简单的“差速补偿”。通过手动测试如果发现小车总是右偏就在控制左轮速度的PWM值上乘以一个略大于1的系数如1.05直到它能基本走直线。7.4 系统整体延迟感明显现象从小球偏离到板子反应感觉有肉眼可见的延迟导致小球容易失控。排查与优化图像处理流水线这是最大的延迟来源。使用picamera库的capture_continuous函数并设置use_video_portTrue可以获得更低延迟的视频流。将处理循环中所有不必要的显示 (cv2.imshow) 和调试打印全部移除。控制周期在代码中计算并打印每次循环的时间。确保它稳定在33ms左右对应30fps。如果更长就需要进一步优化图像处理算法或降低分辨率。舵机响应速度检查舵机的响应速度参数。有些舵机从收到信号到转动到位需要几十到上百毫秒这本身就会引入延迟。选择“高速”型号的舵机会有帮助。多进程 vs 多线程我的三个服务平衡、手柄、通信最初在一个多线程程序中偶尔会相互阻塞。改为三个独立的进程即三个独立的Python脚本由systemd分别管理后响应性得到了改善。这个项目从构思到最终稳定运行花费了我近一个月的业余时间。它不仅仅是一个PID或OpenCV的练习而是一次完整的嵌入式系统集成挑战。最大的收获不是让小球稳稳停在板上那一刻的成就感而是在解决一个个具体问题过程中对硬件、软件、控制理论之间如何协同工作产生的深刻理解。如果你也打算尝试我的建议是分而治之。先确保摄像头能稳定识别小球再单独调试一个轴的PID让它能跟踪小球最后集成所有部分并处理耦合问题。耐心调试记录每一次参数变更的结果你最终一定能驯服这只“机械蝎子”。