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.