教程 · 中级 · 阅读约 18 分钟

用 PCA9685(I²C PWM 驱动器)驱动舵机

如何通过 I²C 总线用一块 PCA9685 驱动多个舵机:设定 PWM 频率、把角度换算成经过标定的脉冲宽度,并安全地给舵机供电。

单片机的硬件 PWM 通道只有寥寥几个。一两个舵机没问题,但一条机械臂要四个,六足机器人要十八个,而 Arduino 在你用完关节之前就先耗尽了定时器。PCA9685解决的正是这件事:一块小板子在硬件上生成十六路独立 PWM 信号,而所有指令只走两根 I²C线。在真实硬件上驱动机械臂模拟器里的每一个关节,用的就是这个办法。

PCA9685 十六通道 PWM/舵机驱动板的标注图:顶部是十六组三针舵机排针,中央是 PCA9685 芯片,左边是五针 I²C 控制排针,地址焊跳线,右边是给舵机供电的 V+ 螺丝端子。
硬件里十六路 PWM 通道,由两根 I²C 引脚驱动;舵机电流从独立的 V+ 端子来,绝不经过单片机。 下载 SVG

为什么不干脆把舵机接到 Arduino 上

两个原因。第一是引脚:Uno 的定时器只够支撑几个 Servo 对象,再往上这个库就开始自己跟自己打架。第二条更要紧,是电源:舵机堵转时会拉 500–700 mA,四个同时动能拉到几安培。这股电流没法从 Arduino 的 5 V 引脚来——它会让板子欠压并在动作做到一半时复位。PCA9685 把两个问题都解决了。它把 PWM 生成卸载给一颗专用芯片,并从自己的 V+ 端子引入舵机电流,让大电流完全远离你的逻辑。

设置 PWM 频率

每个业余级舵机都期望 50 Hz 的控制信号——每 20 ms 一个脉冲。PCA9685 是整颗芯片设一个频率,所以在 setup() 里设一次:

#include <Adafruit_PWMServoDriver.h>

Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);  // default I²C address

void setup() {
  pwm.begin();
  pwm.setPWMFreq(50);   // 50 Hz — the standard servo refresh rate
}

现在十六个通道都以 50 Hz 脉冲。每个舵机保持的角度由脉冲的宽度决定,而不是频率——这是你能逐通道控制的那一部分。

角度到脉冲宽度——真正要紧的标定

舵机把脉冲宽度读作位置指令:大约1 ms 对应 0°,1.5 ms 对应居中,2 ms 对应 180°,每秒重复 50 次。SG90用的就是这套“脉宽即角度”的编码;PCA9685 只是把它挪到硬件里生成。

舵机控制示意图:一个 50 Hz 的 PWM 信号,其脉冲宽度决定舵机角度。1 毫秒脉冲指令 0 度,1.5 毫秒指令 90 度,2 毫秒指令 180 度,配以量角器表盘示意。
舵机读的是脉冲宽度而不是电压:1 ms 是 0°,1.5 ms 是居中,2 ms 是到底——每秒重复 50 次。 下载 SVG

PCA9685 不直接接受毫秒。它把每个 20 ms 周期切成 4096 个计数(12 位分辨率),你告诉它脉冲要维持高电平多少个计数。在 50 Hz 下,一个整周期就是 4096 个计数 = 20 ms,于是:

  • 1 ms ≈ 205 计数(0°)
  • 1.5 ms ≈ 307 计数(90°)
  • 2 ms ≈ 410 计数(180°)

这些是标称数值。真实舵机之间有差异——同一款 SG90 可能在 130 就到底、也可能到 490 才到底。**这是你必须标定的那一个数字。**给每个舵机存一对最小值和最大值,把你的角度映射进那个范围:

// Calibrate these two per servo by eye — see "Gotchas" below.
const int SERVO_MIN = 130;   // counts at the servo's 0° hard stop
const int SERVO_MAX = 490;   // counts at the servo's 180° hard stop

int angleToCount(int deg) {
  return map(deg, 0, 180, SERVO_MIN, SERVO_MAX);
}

跳过标定,正是“下 90°”的指令在一个关节落到 78°、在另一个落到 96° 的原因——也是一只用未标定舵机搭起来的机械臂,永远到不了逆解求解器所要求的位姿的原因。

接线:两条电源轨,一个地

  • 逻辑——来自 Arduino 的 VCC、GND、SDA、SCL。这只给芯片供电。
  • 舵机电源——一路独立的 5–6 V 电源接到 V+ 螺丝端子,按所有舵机同时堵转时的电流配容,而不是按它们的空闲消耗。
  • 共地——电源的地和 Arduino 的地必须连在一起,否则 PWM 信号没有参考,舵机会抽搐或者干脆不理你。承载指令的 I²C 线同样适用。

在 V+ 上并一只电容,吸收舵机启动时的电流尖峰;PCA9685 转接板上正好留了这么一块焊盘。

代码:让一个关节动起来

#include <Adafruit_PWMServoDriver.h>

Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);

const int SERVO_MIN = 130;
const int SERVO_MAX = 490;

int angleToCount(int deg) {
  return map(deg, 0, 180, SERVO_MIN, SERVO_MAX);
}

void setServo(uint8_t channel, int deg) {
  pwm.setPWM(channel, 0, angleToCount(deg));  // on at count 0, off at the angle count
}

void setup() {
  pwm.begin();
  pwm.setPWMFreq(50);
  setServo(0, 90);   // base to centre
  setServo(1, 45);   // shoulder
  setServo(2, 120);  // elbow
  setServo(3, 10);   // gripper open
}

void loop() {}

每次 setPWM(channel, 0, count) 的意思是“在周期开始时打开这个通道,经过 count 个计数后关闭”。要扫过一个关节,就在循环里按步长改变角度、每步之间加一个短 delay(),这样舵机有时间去动,而不是啪地跳过去。

坑

  • **给每个舵机分别标定 SERVO_MIN / SERVO_MAX。**下发 0°,记下舵盘实际停在哪里,微调计数直到对上。180° 同样做一遍。这里花十分钟,省下的是下游每一个位姿。
  • **永远不要用 Arduino 给 V+ 供电。**一个舵机也许撑得住,四个会让板子欠压复位。
  • **电源按堵转配,不按空闲配。**电源不够大时,几个舵机一起动就会塌,整块板子跟着抖。
  • **查地址。**默认是 0x40。跨接焊跳线可以把板子串起来,然后把新地址传给构造函数。
  • **温柔地动。**朝目标一步步走比直接跳过去好——它降低了电流尖峰,也对塑料齿轮更温柔。
  • **夹爪通道才是会把东西烧掉的那一路。**其他每个关节转到角度后就空转;夹爪夹着物体时,舵机是顶着一个东西堵转,持续吃大约半安培。零件一放到位就立刻松开,关于更细的 PWM 分辨率为什么买不到更细的力,见舵机夹爪力、堵转与柔顺。

一旦每个关节都听角度指挥,机械臂就可以处理几何问题了:把一个目标点变成够到它的关节角,而这正是二连杆臂逆解和机械臂模拟器存在的意义。

探索关联图谱

本教程所属的搭建

包含本教程的项目和学习路线。

延伸阅读

参考资料