Tutorial · Intermediate · 18 min read

Drive Servos with the PCA9685 (I²C PWM Driver)

How to drive many servos with a PCA9685 over I²C: set the PWM frequency, turn an angle into a calibrated pulse width, and power the servos safely.

Published

A microcontroller has only a handful of hardware PWM channels. That is fine for one or two servos, but a robot arm has four, a hexapod has eighteen, and the Arduino runs out of timers long before you run out of joints. The PCA9685 solves this: one small board generates sixteen independent PWM signals in hardware, and you command all of them over just two I²C wires. This is how you drive every joint of the robot arm simulator on real hardware.

Labelled diagram of a PCA9685 16-channel PWM/servo driver board: sixteen three-pin servo headers along the top, the PCA9685 chip in the centre, a five-pin I²C control header on the left, address solder jumpers, and a V+ screw terminal for servo power on the right.
Sixteen PWM channels in hardware, driven over two I²C pins; servo current comes from the separate V+ terminal, never the microcontroller. Download SVG

Why not just wire the servos to the Arduino?

Two reasons. First, pins: the Uno has enough timers for a few Servo objects before the library starts fighting itself. Second, and more important, power: a servo can pull 500–700 mA when it stalls, and four moving at once can pull several amps. That current cannot come from the Arduino’s 5 V pin — it browns out the board and resets it mid-move. The PCA9685 fixes both. It offloads the PWM generation to a dedicated chip, and it routes servo current from its own V+ terminal, keeping the high current away from your logic entirely.

Set the PWM frequency

Every hobby servo expects a 50 Hz control signal — one pulse every 20 ms. The PCA9685 sets one frequency for the whole chip, so you set it once in 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
}

All sixteen channels now pulse at 50 Hz. The angle each servo holds is set by the width of its pulse, not the frequency — that is the part you control per channel.

Angle to pulse width — the calibration that matters

A servo reads the pulse width as a position command: roughly 1 ms for 0°, 1.5 ms for centre, 2 ms for 180°, repeated 50 times a second. This is the same pulse-width-as-angle code the SG90 uses; the PCA9685 just generates it in hardware.

Servo control diagram: a 50 Hz PWM signal whose pulse width sets the servo angle. A 1 millisecond pulse commands 0 degrees, 1.5 milliseconds commands 90 degrees, and 2 milliseconds commands 180 degrees, shown on protractor dials.
The servo reads the pulse width, not the voltage: 1 ms is 0°, 1.5 ms is centre, 2 ms is full travel — repeated 50 times a second. Download SVG

The PCA9685 does not take milliseconds directly. It splits each 20 ms period into 4096 counts (12-bit resolution) and you tell it how many counts the pulse stays high. At 50 Hz, one full period is 4096 counts = 20 ms, so:

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

Those are the nominal numbers. Real servos vary — the same SG90 might bottom out at 130 and top out at 490. This is the one number you must calibrate. Store a min and max per servo and map your angle into that range:

// 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);
}

Skipping the calibration is the reason a “90°” command lands at 78° on one joint and 96° on another — and why an arm built on uncalibrated servos never quite reaches the pose the inverse-kinematics solver asked for.

Wiring: two power rails, one ground

  • LogicVCC, GND, SDA, SCL from the Arduino. This powers only the chip.
  • Servo power — a separate 5–6 V supply to the V+ screw terminal, sized for the stall current of every servo at once, not their idle draw.
  • Common ground — the supply’s ground and the Arduino’s ground must be tied together, or the PWM signal has no reference and the servos twitch or ignore you.

Add a capacitor across V+ to absorb the current spike as servos start moving; the PCA9685 breakout has a footprint for exactly this.

The code: move a joint

#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() {}

Each setPWM(channel, 0, count) says “turn this channel on at the start of the period and off after count ticks.” Sweep a joint by stepping the angle in a loop with a short delay() between steps, so the servo has time to move instead of snapping.

Gotchas

  • Calibrate SERVO_MIN / SERVO_MAX per servo. Command 0°, note where the horn actually sits, and nudge the count until it matches. Do the same at 180°. Ten minutes here saves every downstream pose.
  • Never drive V+ from the Arduino. One servo might survive it; four will brown out and reset the board.
  • Size the supply for stall, not idle. An undersized supply sags when several servos move together, and the whole board jitters.
  • Check the address. The default is 0x40. Bridge the solder jumpers to chain boards, and pass the new address to the constructor.
  • Move gently. Stepping toward a target beats snapping to it — it lowers the current spike and is kinder to plastic gears.

Once each joint answers to an angle, the arm is ready for the geometry: turning a target point into the joint angles that reach it, which is exactly what inverse kinematics of a two-link arm and the robot arm simulator are for.

Explore the graph

Part of these builds

Projects and learning paths that include this tutorial.

Further reading

References