Skip to content

Quickstart

Centre a hobby servo from your MCU: wire four signals, write a small port class, call three driver functions.

Before you power the servo

Power servos from their own 5 V supply, not from the PCA9685's VDD or your MCU's regulator, and join the grounds. A stalled servo draws far more than a logic rail can give.

1. Wire it

MCU PCA9685 Notes
I2C SCL SCL (pin 26, TSSOP28) Pull-up to VDD; 4.7 kΩ suits 100 kHz and 400 kHz
I2C SDA SDA (pin 27) Pull-up to VDD
3.3 V or 5 V VDD (pin 28) 2.3 V to 5.5 V; 100 nF to VSS close to the pin
GND VSS (pin 14) Common with the servo supply ground
GND or a GPIO OE (pin 23) Active low: tie it low, or drive it to blank all outputs
GND A0 to A5, EXTCLK All low: address 0x40; EXTCLK must be grounded when unused
LED0 (pin 6) Servo signal wire

The Hardware page has the full pin list, the address table and the electrical limits. Breakout boards usually fit the pull-ups and tie OE and the address pins already: check yours.

2. Add the driver

Add the repository as a submodule and link the hf_pca9685 CMake target. Installation covers CMake and ESP-IDF.

git submodule add https://github.com/hardfoc/hf-pca9685-driver.git external/hf-pca9685-driver

3. Write the port

The port is the only platform code. It brings the bus up and moves register bytes. Replace the three platform_* calls with your SDK's.

my_i2c.hpp
class MyI2c : public pca9685::I2cInterface<MyI2c> {
public:
  // Bring the bus up once; later calls return true at once.
  bool EnsureInitialized() noexcept {
    if (!ready_) {
      ready_ = platform_i2c_init();
    }
    return ready_;
  }

  // One write transaction: START, address + W, register, data bytes, STOP.
  bool Write(uint8_t addr, uint8_t reg, const uint8_t* data, size_t len) noexcept {
    uint8_t frame[1 + 4];                           // the driver writes at most 4 data bytes
    if (len > sizeof(frame) - 1) {
      return false;
    }
    frame[0] = reg;
    for (size_t i = 0; i < len; ++i) {
      frame[1 + i] = data[i];
    }
    return platform_i2c_write(addr, frame, len + 1);
  }

  // Register read: START, address + W, register, repeated START, address + R, data, STOP.
  bool Read(uint8_t addr, uint8_t reg, uint8_t* data, size_t len) noexcept {
    return platform_i2c_write_read(addr, &reg, 1, data, len);
  }

  // Optional: wait at least `us` microseconds. The driver uses it for the 500 us oscillator start-up.
  void DelayUs(uint32_t us) noexcept { platform_delay_us(us); }

private:
  bool ready_ = false;
};

Porting explains each method and the transfer rules.

4. Centre the servo

main.cpp
#include "pca9685.hpp"

int center_servo_on_channel_0(MyI2c& bus) {
  pca9685::PCA9685<MyI2c> pwm(&bus, 0x40);        // A0..A5 tied low

  if (!pwm.EnsureInitialized()) {                 // 1. bus up, MODE1 reset
    return -1;                                    // wiring, pull-ups or address
  }
  if (!pwm.SetPwmFreq(50.0f)) {                   // 2. 50 Hz: a 20 ms servo frame
    return -2;
  }
  // 3. 1.5 ms high out of 20 ms = 7.5 % duty: servo centre
  if (!pwm.SetDuty(0, 1.5f / 20.0f)) {
    return -3;
  }
  return 0;
}

What each step does:

  1. EnsureInitialized() calls your port's EnsureInitialized(), then writes MODE1 = 0x20, which starts the oscillator and turns on register auto-increment. Every other call runs it for you if you skip it.
  2. SetPwmFreq(50.0f) writes PRE_SCALE = 121 (the chip only accepts it while asleep; the driver handles that), giving a 20 ms frame.
  3. SetDuty(0, 0.075f) sets channel 0 high for 7.5 % of each frame: 1.5 ms, the centre of a typical servo.

This example is built by the host CI, so it compiles against the current headers.

If it does not work

Symptom First thing to check
EnsureInitialized() returns false Pull-ups on SCL and SDA, VDD, the address (A0 to A5)
Calls succeed, output stays low OE must be low; the chip is not asleep (Wake())
Output wrong on a channel Your port's transfers (Troubleshooting)
Servo jitters or sits off centre Frequency and pulse width (PWM timing)

Next

  • API guide: frequency, channels, sleep, output stage, errors.
  • Examples: an ESP32-S3 test suite and a 16-servo demo.
  • PWM timing: how the 12-bit counter, ON and OFF ticks and the prescaler fit together.