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.
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.
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, ®, 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¶
#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:
EnsureInitialized()calls your port'sEnsureInitialized(), 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.SetPwmFreq(50.0f)writes PRE_SCALE = 121 (the chip only accepts it while asleep; the driver handles that), giving a 20 ms frame.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.