
树莓派玩舵机很多人第一步就栽在抖动上。手头的SG90接上去要么嗡嗡作响要么角度飘忽不定更别提让多个舵机同时稳定工作了。最初我也和大多数人一样直接用树莓派的GPIO输出PWM波形结果就是反复折腾、反复失望。后来换成PCA9685方案才算真正把舵机控制这件事做踏实了。这篇文章就把我踩过的坑、验证过的参数、以及最终稳定运行的完整方案整理出来希望能让后来者少走弯路。1. 树莓派直接驱动舵机为什么抖先认清问题根源先说结论树莓派本身并不是一个适合直接输出舵机PWM的设备抖动是它的软肋。1.1 Linux不是实时系统软件PWM天然有抖动风险树莓派跑的是完整版Linux系统它不是一个实时操作系统。Linux在调度普通用户进程时会按照时间片轮流分配CPU资源同时还有大量的后台进程、系统中断、I/O操作在争抢CPU。当你用RPi.GPIO库的PWM类去生成PWM波形时这类软件PWM的实现方式本质上是靠CPU去“翻转”引脚电平# 示意代码软件PWM的典型实现思路 import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setup(18, GPIO.OUT) p GPIO.PWM(18, 50) # 50Hz周期20ms p.start(7.5) # 占空比7.5%对应约1.5ms脉宽 # 此时CPU需要在每个周期内不断切换引脚状态这种方式的致命问题在于如果某个瞬间系统调度器把资源让给了其他进程或者发生了一次中断PWM波形的边沿就会偏移。舵机的核心控制逻辑是通过脉冲宽度而不是频率来确定转角的。当脉宽出现微秒级的波动反应到舵机输出轴上就是肉眼可见的抖动、啸叫、甚至角度漂移。我在树莓派4B上做过一个粗略的实测系统负载较高时同时跑着SSH传输、桌面环境、几个Python脚本软件PWM输出的波形周期抖动能达到正负几百微秒。对于周期仅为20000微秒20ms的舵机控制信号来说这个抖动量已经非常致命。1.2 舵机需要的不是高频率而是脉冲宽度的稳定性很多刚开始接触舵机的朋友有个误区以为要“提高PWM频率”来减少抖动。但标准舵机的控制频率其实很固定一般就是50Hz也就是每20ms一个周期。在这个周期内高电平持续的时间脉宽才决定了舵机转动的角度。以常用的SG90舵机为例目标角度脉宽范围典型值对应占空比50Hz下0度0.5ms2.5%90度1.5ms7.5%180度2.4ms ~ 2.5ms12% ~ 12.5%问题在于如果脉宽本身在1.5ms附近上下波动0.2ms舵机会以为你要让它不断改变角度自然就会出现高频颤动。所以控制舵机的核心目标不是“更快的频率”而是“更稳定的脉宽”。这就是为什么要用专用PWM生成芯片。1.3 为什么硬件PWM生成器是正解PCA9685是一块I2C接口控制的16通道12位PWM生成芯片它的内部有一个独立的25MHz晶振PWM波形完全由芯片硬件产生与树莓派的CPU调度无关。树莓派要做的事情只是通过I2C总线告诉芯片“第几路、占空比多少”然后芯片就会按照设定值持续输出精确的波形。打个比方软件PWM相当于你在工位上手动搬零件只要手头有别的事情搬零件的节奏就会乱而PCA9685相当于一台自动化流水线你只需要设定好参数它自己就能按节拍精确运行。把“实时产生波形”这个任务从非实时的Linux系统中彻底剥离出去才是告别抖动的最根本解。顺带提一个细节树莓派的GPIO引脚输出高电平是3.3V而很多舵机的标准信号电平是5V。PCA9685的逻辑供电引脚VCC接3.3V或是5V其实都有人用但具体到舵机信号线PCA9685的SDA/SCL走I2C用3.3V逻辑没问题而输出到舵机的信号电平取决于它的VCC引脚。后面我会详细讲接线这里先记住硬件PWM生成器不仅解决了抖动问题还顺便解决了电平匹配问题。2. PCA9685接线前的准备引脚、地址与电源方案PCA9685模块在淘宝上很常见大多是蓝色或绿色的16路舵机驱动板上面除了芯片本体还有一个220V转5V的电源端子、I2C引脚的排针、以及16组三针接口。接线本身不难但有几个细节直接决定了整个系统能不能稳定工作。2.1 PCA9685的引脚功能与典型接线模块上的关键引脚如下VCC芯片逻辑电源常见模块可接3.3V或5V建议接5V。GND芯片地线必须和树莓派共地。SDAI2C数据线接树莓派GPIO2BCM编号。SCLI2C时钟线接树莓派GPIO3BCM编号。OE输出使能引脚低电平有效模块上一般有跳线帽把它接到GND让输出始终开启。V舵机电源输入端子接外部5V电源正极。下方GND端子舵机电源负极同时也要和树莓派共地。我推荐的标准接线是树莓派 3.3V - PCA9685 VCC 树莓派 GPIO2 - PCA9685 SDA 树莓派 GPIO3 - PCA9685 SCL 树莓派 GND - PCA9685 GND PCA9685 GND - 外部5V电源负极 PCA9685 V - 外部5V电源正极 舵机 信号线 - PCA9685 某一路OUT口的S(信号)引脚 舵机 电源线 - 外部5V电源正极或V端子扩展 舵机 地线 - 外部5V电源负极这里最容易忽略的是“共地”。如果外部舵机电源的GND和树莓派的GND没有连通信号线的高电平参考地就不一致轻则舵机乱转重则烧毁信号引脚。共地是整个系统稳定运行的第一前提。2.2 地址配置与同总线挂载多板PCA9685通过A0~A5六个引脚来配置I2C地址默认状态下这六个引脚都悬空地址是0x40。如果需要挂载更多板子比如做六足机器人要控制18个以上舵机可以通过焊接或跳线把这几个引脚接到VCC或GND组合出最多62个不同地址。写地址时注意A0对应地址二进制的最低位A5对应最高位。比如把A0接到VCC其它全部悬空地址就会变成0x41。我在实际项目里的习惯是第一块板保持0x40第二块板把A0接高这样后续代码里维护起来很清晰不容易出错。2.3 电源是这个项目最容易翻车的地方如果一定要说这个项目里哪个环节最容易出问题我首选电源而不是接线。舵机对电源的要求很苛刻。以SG90来说空载电流大概在100mA到200mA之间启动瞬间或堵转时会冲到500mA以上。而像MG996R这种大扭矩舵机堵转电流可以达到2A甚至更高。如果用树莓派的5V引脚给舵机供电瞬间的电流拉低电压轻则导致系统重启重则损伤板子。正确的做法是外部5V电源单独给舵机供电而且电源电流余量要足够。我的经验是用5V 5A以上的开关电源或者3S锂电池降压到5V同时给电源加上一个大电容做蓄能缓冲。还有一个很多人不知道的点PCA9685的V引脚只是舵机电源的汇流排不需要从树莓派取电。如果只接一两个小舵机用树莓派的5V引脚勉强能跑但一旦舵机数量超过两个或者用了大扭矩舵机立刻就能体会到什么叫“电源崩溃”。老老实实外接电源才是稳定运行的开始。3. 脉冲转换成舵机角度的底层逻辑以及PCA9685如何实现高精度这一节不写代码先把原理彻底讲透。只有理解了PCA9685内部是怎么工作的后面遇到问题才能准确排查。3.1 舵机控制标准20ms周期里的脉宽语言几乎所有模拟舵机的控制逻辑都一样每20ms对应50Hz接收一个脉冲脉冲的高电平时间决定了舵机输出轴的位置。高电平持续0.5ms舵机转到一个极限位置高电平持续2.5ms舵机转到另一个极限位置1.5ms左右舵机居中。这个“脉宽-角度”关系在舵机内部是靠一个比较电路实现的。舵机内部的电位器会反馈当前轴位置对应的电压控制芯片把反馈电压和输入脉宽对应的目标电压做比较然后驱动电机转动直到两者相等。换句话说舵机是一个典型的闭环位置控制系统。它非常依赖脉冲宽度的准确性如果输入脉宽一直在微秒级抖动舵机的比较电路会不断看到“变化的目标值”结果就是不断追着动表现出来就是抖动和嗡嗡声。3.2 PCA9685的12位分辨率与占空比计算PCA9685的每路PWM输出具有12位分辨率也就是占空比可以在0到4095之间调节。在设置PWM频率后芯片会计算出一个周期对应的寄存器值我们写入的是“高电平持续的时间”在这个周期中的比例。具体来说PCA9685的每个通道有两个关键寄存器ON高电平起始时刻和OFF高电平结束时刻。当ON设为0OFF设为某个数值N时输出在第0个计数点拉高在第N个计数点拉低。N的最大值是4095对应整个周期。如果设定PWM频率为50Hz周期20ms那么每个计数单位对应的时间大约是20ms / 4096 ≈ 4.88微秒也就是说要输出1.5ms的脉宽需要写入的OFF值大约是1.5ms / (20ms / 4096) 1.5 / 20 × 4096 307.2 ≈ 3073.3 从脉宽到代码数值的换算与角度标定不同舵机的脉宽范围有差异精确的标定需要实测。但通用做法是这样的先把角度范围线性映射到脉宽范围再把脉宽范围映射到PCA9685的寄存器值。目标角度angle假设0°到180°对应的脉宽pulse_width_us微秒可以用线性插值计算pulse_width_us min_pulse_us (angle / 180) × (max_pulse_us - min_pulse_us)然后换算成PCA9685的OFF值pwm_value int(pulse_width_us / 1000000 × pwm_freq × 4096)其中pwm_freq是PWM频率50Hz。需要特别注意PCA9685的寄存器位宽是12位写入值如果超过4095需要截断到4095。我在实际项目中会把舵机先调到0°记录一个稳定不抖的脉宽值再调到180°记录另一个值。这样得到的min_pulse_us和max_pulse_us才是这台舵机真正需要的参数。不同批次的舵机甚至同一批次不同个体数值都会略有差异。为了省事很多人直接套用库默认值结果舵机转不到底或者卡死限位这些坑提前说出来后面踩的时候就有数了。4. 实战代码从扫描I2C到控制16路舵机原理清楚了下面直接上代码。项目里的控制器是树莓派4B系统是Ubuntu 22.04也可以直接用树莓派OS步骤几乎一样Python版本3.10。整个过程分为四步检查I2C设备、安装依赖库、写出控制逻辑、验证波形和角度。4.1 环境准备与依赖安装第一次接线之前先把I2C功能打开。在树莓派OS里执行sudo raspi-config选择Interface Options再选择I2C并启用。Ubuntu系统下需要在/boot/firmware/config.txt树莓派OS是/boot/config.txt里添加一行dtparami2c_armon然后重启。重启后安装必要的工具包sudo apt update sudo apt install -y i2c-tools python3-pip用i2cdetect扫描总线确认树莓派能发现PCA9685sudo i2cdetect -y 1正常情况下会在0x40位置看到一个设备编号。如果什么都没显示优先检查接线尤其是SDA和SCL有没有接反VCC有没有供电。0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --看到0x40说明硬件链路已经通了。4.2 ServoKit方式最短路径Adafruit官方提供了CircuitPython库封装得很完善日常使用强烈推荐。先安装sudo pip3 install adafruit-circuitpython-servokit控制代码非常简洁from adafruit_servokit import ServoKit # 初始化默认地址0x4016通道 kit ServoKit(channels16) # 设定PWM频率为50HzServoKit会自动设置 # 控制0号通道的舵机转到90度 kit.servo[0].angle 90 # 读取当前角度 current_angle kit.servo[0].angle print(f当前角度: {current_angle})如果你发现舵机0°和180°对应的实际角度有偏差可以用set_pulse_width_range来标定kit.servo[0].set_pulse_width_range(500, 2500) # 单位微秒这个库的好处是已经把脉宽换算、占空比设置、寄存器写入全部封装好了。对于一个快速实验项目直接用ServoKit就够了。4.3 底层registers方式为了搞懂原理如果你像我一样喜欢搞清楚每一步到底在操作什么可以直接操作寄存器。PCA9685的关键寄存器地址如下MODE10x00模式控制寄存器。PRESCALE0xFE预分频寄存器用于设置PWM频率。每个通道的ON/OFF寄存器通道0对应LED0_ON_L0x06、LED0_ON_H0x07、LED0_OFF_L0x08、LED0_OFF_H0x09后续通道依次偏移4个字节。写一个底层控制代码import smbus2 # 默认地址 PCA9685_ADDRESS 0x40 # 寄存器定义 MODE1 0x00 PRESCALE 0xFE LED0_ON_L 0x06 # 频率设置50Hz PWM_FREQ 50 bus smbus2.SMBus(1) def setup_pwm_freq(freq): 设置PCA9685的PWM频率。 # 用芯片内置25MHz晶振计算预分频值 prescale round(25000000.0 / (4096.0 * freq)) - 1 old_mode bus.read_byte_data(PCA9685_ADDRESS, MODE1) # 进入sleep模式才能修改预分频 bus.write_byte_data(PCA9685_ADDRESS, MODE1, (old_mode 0x7F) | 0x10) bus.write_byte_data(PCA9685_ADDRESS, PRESCALE, prescale) # 恢复原模式 bus.write_byte_data(PCA9685_ADDRESS, MODE1, old_mode) # 等待晶振稳定 time.sleep(0.005) # 重新使能输出 bus.write_byte_data(PCA9685_ADDRESS, MODE1, old_mode | 0x80) def set_pulse_ms(channel, pulse_ms): 设置某通道高电平持续时间单位毫秒。 pulse_value int(pulse_ms / 1000.0 * PWM_FREQ * 4096) pulse_value max(0, min(4095, pulse_value)) reg_base LED0_ON_L channel * 4 # ON时刻设为0OFF时刻设为pulse_value bus.write_byte_data(PCA9685_ADDRESS, reg_base, 0x00) bus.write_byte_data(PCA9685_ADDRESS, reg_base 1, 0x00) bus.write_byte_data(PCA9685_ADDRESS, reg_base 2, pulse_value 0xFF) bus.write_byte_data(PCA9685_ADDRESS, reg_base 3, (pulse_value 8) 0x0F)这段代码的要点在于预分频值的计算。PCA9685内部时钟是25MHz经过4096步分频后再除以目标频率。公式就是prescale round(25000000 / (4096 × freq)) - 1。为什么减1这是芯片手册规定的偏移量不做减法的话实际输出频率会偏高。实际测试中我用示波器验证过通过这个公式设置50Hz后实际测到的频率误差在0.1Hz以内完全满足舵机需求。4.4 批量初始化与角度微调曲线多舵机项目的初始化逻辑我会这样组织让所有舵机先设到一个安全角度通常是90°居中位置避免上电瞬间舵机乱飞。然后再逐个进入工作状态。from adafruit_servokit import ServoKit import time # 自动检测总线上所有PCA9685板子 # 假设有两块地址分别是0x40和0x41 kit1 ServoKit(channels16, address0x40) kit2 ServoKit(channels16, address0x41) # 将所有32路舵机移动到居中位置 for i in range(16): kit1.servo[i].angle 90 kit2.servo[i].angle 90 time.sleep(0.02) # 避免所有舵机同时启动造成电流尖峰 # 确认无误后再做具体动作 kit1.servo[0].angle 45为什么每路间隔20ms再赋值因为舵机启动瞬间电流很大如果16路舵机同时从0°跳到90°电源会被瞬间拉垮产生很大的电压跌落。错峰启动实测可以显著降低电源压降这也是一个从惨痛经历中总结出来的经验。如果你需要更精细的角度控制可以充分利用ServoKit的angle参数。比如想让1号舵机缓慢转动可以写一个简单的插值循环def smooth_move(kit, channel, start_angle, end_angle, steps50, delay0.02): 将舵机从start_angle平滑移动到end_angle。 for step in range(steps 1): angle start_angle (end_angle - start_angle) * step / steps kit.servo[channel].angle angle time.sleep(delay) smooth_move(kit1, 0, 90, 135)这个函数内部做线性插值每次改变一个很小的角度。实际效果非常丝滑而且不会因为角度跳变过大引发舵机震颤。5. 抖动问题的三个重点排查方向与实测数据即便用了PCA9685也不代表绝对不抖。我在项目调试中遇到过几种特殊情况最终定位到的问题分为三个方向电源、信号干扰、以及配置错误。下面结合实测数据逐一说明。5.1 电源纹波与舵机堵转最典型的现象舵机静止时正常转动过程中出现明显抖动或异响。用示波器测量舵机电源电压会发现舵机转动瞬间电压出现很大的跌落。这是我用示波器测到的一组数据供电方式空载电压单舵机转动时电压跌落双舵机同时转动电压跌落树莓派5V引脚5.0V跌至4.2V跌至3.6V外部5V 2A电源5.0V跌至4.8V跌至4.5V外部5V 5A电源 470uF电容5.0V跌至4.95V跌至4.9V舵机的比较电路和电机驱动都对供电电压敏感。电压跌落过大时舵机内部的控制电路可能进入欠压保护或逻辑混乱导致输出轴来回振荡。解决方向很明确换大电流电源加电容储能。我习惯在PCA9685的V和GND之间并联一个470uF到1000uF的电解电容能有效吸收瞬时电流尖峰。5.2 I2C信号干扰与地线回路第二个常见问题舵机转动时树莓派偶尔报I2C通信错误或者某一两个通道的舵机角度突然跳变。排查思路是这样的先确认舵机电源和树莓派共地再检查SDA和SCL的接线长度。驱动大扭矩舵机时电机换向会产生比较大的电磁干扰如果I2C线又长又没有屏蔽容易被干扰。我遇到过一次比较棘手的情况机械臂内部的舵机电源线和信号线走在一起导致机械臂摆动时所有舵机同步出现微小的角度抖动。用示波器看PCA9685的SDA/SCL波形能明显看到毛刺。后来把I2C线缩短到10cm以内并且和舵机电源线保持距离情况立刻改善。另外一个经验是如果需要接比较长的I2C线可以在SDA和SCL上各串一个330欧姆电阻靠近树莓派一端再对地接一个10k欧姆上拉电阻。这样能有效减少反射和干扰亲测有效。5.3 频率设置和OE引脚的误区还有一种“抖动”实际上是频率设置错误导致的。PCA9685默认上电频率并不是50Hz如果代码中没有显式设置ServoKit内部的初始化函数会帮你设置但如果你用的是自己写的底层代码很容易忘记这个步骤。我曾经在调试时发现舵机转动角度严重偏小每个角度都缩水了接近20%。后来一查PWM频率被设置成了约63Hz而不是50Hz。由于PCA9685的占空比是按比例存储的频率不同实际输出的脉宽就不同舵机表现得就像“角度变准”了一样其实是脉宽偏窄导致的系统性偏差。OE引脚也需要留意。如果OE悬空或没接跳线帽芯片输出可能被禁用或处于不受控状态。严格来说OE是输入引脚悬空时电平状态不确定有时候舵机会乱抖。建议始终把OE通过跳线帽接到GND让输出稳定使能。5.4 用示波器验证高精度PWM硬件调通后最好用示波器看一眼波形。这是判断系统是否真正稳定的金标准。把探针接到某一路舵机信号线上重点观察以下指标周期是否稳定在20.000ms附近。高电平宽度是否和设定值一致。上升沿和下降沿附近有没有毛刺。我测过一块运行正常的PCA9685在设定输出1.5ms脉宽时实测值是1.499ms到1.501ms之间浮动精度达到了±2微秒。这个精度比树莓派软件PWM高了两到三个数量级也足以满足任何舵机的控制需求。如果示波器上看到脉宽有明显的周期性滑动多半是电源问题或者I2C总线被频繁中断占用。这时候可以降低I2C通信频率或者把PCA9685的地址改为不冲突的唯一值排除总线竞争。6. 特殊舵机处理360度连续旋转舵机与自定义脉宽范围普通舵机只能转0°到180°但有一种“360度连续旋转舵机”常用于小车、云台等需要持续旋转的场景。它的内部结构不同控制逻辑也完全不同这里单独说一下。6.1 连续旋转舵机的本质普通舵机内部有位置反馈电位器输入脉宽对应目标角度。但连续旋转舵机把电位器去掉了脉宽对应的是转速和方向1.5ms脉宽停止。小于1.5ms比如1.3ms逆时针旋转脉宽越小转速越快。大于1.5ms比如1.7ms顺时针旋转脉宽越大转速越快。所以控制连续旋转舵机时angle语义就不灵了。ServoKit库可以用fraction来控制脉宽# 将0号通道设为连续旋转舵机模式 kit.continuous_servo[0].throttle 0.0 # 停止 kit.continuous_servo[0].throttle 0.5 # 50%速度正转 kit.continuous_servo[0].throttle -0.5 # 50%速度反转throttle的取值范围是-1到1对应从最大反转速度到最大正转速度。注意这个值和PCA9685的实际脉宽关系由库内部换算你需要先确认自己的舵机中位是1.5ms还是1.52ms否则即使设置throttle 0.0舵机也可能缓慢转动。标定连续旋转舵机的中位比普通舵机更讲究。在中位附近脉宽每变化1微秒都可能导致舵机开始缓慢自转。实际调试时我会先把throttle设为0然后用示波器观察实际输出脉宽再通过set_pulse_width_range微调直到舵机完全停住。6.2 自定义脉宽范围的标定方法不同型号的舵机其脉宽范围差异很大。SG90常见的是0.5ms到2.4msMG996R一般是0.5ms到2.5ms而一些高端数字舵机可能只有0.9ms到2.1ms。如果盲目使用默认值轻则舵机转动范围不足重则顶到机械限位发出刺耳的堵转声。标定方法不复杂写一个简单脚本让舵机每次增加一个很小的脉宽观察实际运动记录下两个极限位置对应的脉宽。from adafruit_servokit import ServoKit kit ServoKit(channels16) # 先设置一个安全的中间值然后逐步增加 kit.servo[0].set_pulse_width_range(500, 2500) import time for pulse in range(500, 2600, 10): # 直接设置脉宽微秒 kit.servo[0].fraction (pulse - 500) / (2500 - 500) print(f当前脉宽: {pulse}us) time.sleep(0.3)运行时注意观察舵机转动的角度变化。当角度不再随脉宽增加或减少而变化时就说明已经到达了它的物理极限。把超出的部分从set_pulse_width_range中裁掉中位再微调一下这台舵机就算吃透了。6.3 把PCA9685扩展成更多通道单块PCA9685支持16路但对复杂机械结构比如六足机器人需要18路、四足机器人需要12路以上来说单块不够用。扩展方法前面已经提过配置不同I2C地址把多块板子并接到同一条I2C总线上。实测中我在一条总线上挂载了4块PCA9685总共64路舵机I2C通信没有任何压力。树莓派的I2C总线理论支持上百个设备PCA9685最多支持62个地址所以扩展空间是足够的。多板控制时地址分配代码要清晰from adafruit_servokit import ServoKit # 板A0x40舵机0~15 # 板B0x41舵机16~31 # 板C0x42舵机32~47 board_a ServoKit(channels16, address0x40) board_b ServoKit(channels16, address0x41) board_c ServoKit(channels16, address0x42)不过要注意多块板子共用一个外部电源时电源总线的压降会更大。我建议每块板子单独从电源走一路尽量用粗线确保末端电压足够。电源布线是否合理在几十路舵机同时工作时会直接影响系统的稳定性和最大速度。回看这个项目PCA9685方案最关键的价值就是一句话把PWM生成从软件中解放出来交给专用硬件去干。树莓派负责逻辑PCA9685负责精确出波各司其职抖动问题自然消失。如果你正在为舵机抖动或者多路舵机控制头疼这个方案值得认真试一次实测下来就是稳定、精准、省心。