Skip to content

API guide

How to do the common jobs with the driver: initialise, set the frequency, drive outputs, configure the output stage, sleep and handle errors.

Every snippet on this page is compiled by the host CI from tests/docs/api_tasks.cpp, with Driver standing for pca9685::PCA9685<MyI2c>. The API reference lists every member.

The shape of the API

One PCA9685<I2cType> object per chip, constructed with a pointer to your port and the chip's 7-bit address. Every call that touches the chip is noexcept and returns bool.

Task Calls
Start up EnsureInitialized(), Reset(), IsInitialized()
Frequency SetPwmFreq(float hz), GetPrescale(uint8_t&)
One channel SetDuty(ch, duty), SetPwm(ch, on, off), SetChannelFullOn(ch), SetChannelFullOff(ch)
All channels SetAllPwm(on, off)
Output stage SetOutputDriverMode(bool totem_pole), SetOutputInvert(bool invert), SetOutputEnable(bool enable)
Power Sleep(), Wake()
Errors GetErrorFlags(), HasError(e), HasAnyError(), ClearError(e), ClearErrorFlags(mask), GetLastError()
Bus robustness SetRetries(int), SetRetryDelay(void (*)())
Version GetDriverVersion(), GetDriverVersionMajor(), GetDriverVersionMinor(), GetDriverVersionPatch()

Channels are 0 to 15 (MAX_CHANNELS_ = 16); ticks are 0 to 4095 (MAX_PWM_).

Initialise

Driver pwm(&bus, 0x40);

pwm.SetRetries(2);                              // up to 3 attempts per register access
pwm.SetRetryDelay([] { platform_delay_ms(1); }); // between attempts: let the bus recover

if (!pwm.EnsureInitialized()) {                 // bus up, then MODE1 = 0x20 (oscillator on, AI)
  return false;
}

EnsureInitialized() brings your bus up and writes MODE1 = 0x20: oscillator on, register auto-increment on (MODE1.AI, which the four-byte channel writes need), sub-addresses and All Call off. Every other call runs it on first use, so this step is optional, but calling it at start-up reports a wiring problem where you can act on it. Reset() writes MODE1 again at any time; it does not touch the channel registers, MODE2 or PRE_SCALE.

Set the frequency

if (!pwm.SetPwmFreq(1000.0f)) {                 // 24 .. 1526 Hz, same for all 16 channels
  return false;                                 // Error::OutOfRange outside that range
}
uint8_t prescale = 0;
if (!pwm.GetPrescale(prescale)) {               // 1000 Hz -> PRE_SCALE = 5
  return false;
}
const float actual_hz = 25'000'000.0f / (4096.0f * (prescale + 1));  // 1017 Hz
std::printf("PWM %.0f Hz (prescale %u)\n", static_cast<double>(actual_hz), prescale);

One frequency serves all 16 channels. The chip only accepts a new PRE_SCALE while asleep, so SetPwmFreq() briefly stops the oscillator and every output; call it once at start-up, not in a loop. PWM timing has the formula and a table of common frequencies.

Drive the outputs

if (!pwm.SetDuty(0, 0.25f)) {                   // channel 0: high for the first 25 % of the period
  return false;
}
if (!pwm.SetPwm(1, 1024, 3072)) {               // channel 1: 50 %, rising at tick 1024 (phase shift)
  return false;
}
if (!pwm.SetChannelFullOn(2) || !pwm.SetChannelFullOff(3)) {  // 2 constantly high, 3 constantly low
  return false;
}
return pwm.SetAllPwm(0, 2048);                  // all 16 channels: 50 %, overrides the above
Call Use it for
SetDuty(ch, duty) LED brightness, motor speed: duty from 0.0 to 1.0, clamped, rising edge at tick 0; 0.0 is full off, 1.0 full on
SetPwm(ch, on, off) Exact pulse widths (servos) and phase-shifted channels
SetChannelFullOn(ch), SetChannelFullOff(ch) A constant high or low, with no PWM edges
SetAllPwm(on, off) The same value on all 16 channels in one write

For a servo, compute ticks from the pulse width:

// Servo pulse width to PWM ticks: one period is 4096 ticks of 1 / f_pwm.
bool set_servo_us(Driver& pwm, uint8_t channel, float pulse_us, float pwm_hz) {
  const float period_us = 1'000'000.0f / pwm_hz;  // 20 000 us at 50 Hz
  const auto ticks = static_cast<uint16_t>(pulse_us / period_us * 4096.0f + 0.5f);
  return pwm.SetPwm(channel, 0, ticks);           // 1500 us at 50 Hz -> 307 ticks
}

Configure the output stage

if (!pwm.SetOutputDriverMode(false)) {          // MODE2.OUTDRV = 0: open-drain (sink only)
  return false;
}
return pwm.SetOutputInvert(true);               // MODE2.INVRT = 1: for LEDs wired to VDD

The power-up default is totem pole, not inverted: right for servo signals and logic inputs. Use open drain for LEDs with built-in Zener diodes, and inversion for LEDs wired from VDD to the pin. Hardware has the MODE2 bits.

With OE wired to a GPIO, SetOutputEnable() blanks or enables all 16 outputs at once through your port's GpioSet() (Porting); it sends nothing on the bus:

pwm.SetOutputEnable(false);                     // OE high: all outputs blanked (MODE2.OUTNE state)
if (!pwm.SetAllPwm(0, 2048)) {                  // program the channels while blanked
  return false;
}
return pwm.SetOutputEnable(true);               // OE low: outputs follow their registers

Sleep and wake

if (!pwm.Sleep()) {                             // MODE1.SLEEP = 1: oscillator off, outputs stop
  return false;
}
// ... low-power period ...
return pwm.Wake();                              // SLEEP = 0, 500 us start-up, then RESTART

Sleep() sets MODE1.SLEEP: the oscillator stops, the outputs stop and the chip draws 2.2 µA typical. The channel registers keep their values. Wake() follows the restart procedure of section 7.3.1.1: it clears SLEEP, waits the 500 µs oscillator start-up time and then, when the chip set RESTART on the way into sleep, writes RESTART to resume every channel where it was. When Wake() returns, output timing is valid again. The wait goes through your port's optional DelayUs() (Porting).

Handle errors

if (!pwm.SetPwm(16, 0, 100)) {                  // channel 16 does not exist
  if (pwm.HasError(Driver::Error::OutOfRange)) {
    std::printf("bad argument\n");
  }
  if (pwm.HasError(Driver::Error::I2cWrite) || pwm.HasError(Driver::Error::I2cRead)) {
    std::printf("bus failure after all retries\n");
  }
  pwm.ClearErrorFlags();                        // flags accumulate until you clear them
}
Flag Set when What to do
Error::I2cWrite A write failed on every attempt, or the port's EnsureInitialized() failed Check wiring, pull-ups and address; see Troubleshooting
Error::I2cRead A read failed on every attempt As above
Error::OutOfRange Channel above 15, tick above 4095, frequency outside 24 Hz to 1526 Hz Fix the argument; nothing was sent
Error::NotInitialized A call ran EnsureInitialized() and it failed Set together with the I2C flag that caused it

InvalidParam and DeviceNotFound are declared but the driver never sets them. Flags accumulate across calls: read them, act, then clear them.

Raw register access

The driver keeps its register helpers private. For a register it has no call for (SUBADR1 to SUBADR3, ALLCALLADR, MODE2.OCH or OUTNE), write through your port directly, as one transaction: bus.Write(0x40, 0x01, &mode2, 1). The register addresses are in the Register enum. Read MODE2 first and change only your bits: the driver's own MODE2 calls read-modify-write too, so the two stay consistent.

Next

Porting writes the port for your MCU; Troubleshooting maps symptoms to causes.