ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
pca9685.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file pca9685.cpp
6 * @brief NXP PCA9685 PWM / servo driver codec - implementation. See pca9685.h.
7 *
8 * Prescale changes require the oscillator to be asleep, so begin() sleeps, writes PRESCALE,
9 * wakes with auto-increment, then restarts and selects totem-pole outputs.
10 */
11
13#include "protocore_config.h"
14
15#if PC_ENABLE_PCA9685
16
17#if defined(ARDUINO)
19#include <Arduino.h>
20#include <Wire.h>
21#endif
22namespace
23{
24const uint32_t PCA9685_OSC_HZ = 25000000u;
25const uint8_t PCA9685_PRESCALE_MIN = 3;
26const uint8_t PCA9685_PRESCALE_MAX = 255;
27} // namespace
28
29uint8_t pc_pca9685_prescale(uint32_t freq_hz)
30{
31 if (freq_hz == 0)
32 {
33 return PCA9685_PRESCALE_MAX;
34 }
35 uint32_t denom = 4096u * freq_hz;
36 uint32_t pre = (PCA9685_OSC_HZ + denom / 2) / denom; // round(25e6 / (4096*freq))
37 pre = pre ? pre - 1 : 0;
38 if (pre < PCA9685_PRESCALE_MIN)
39 {
40 pre = PCA9685_PRESCALE_MIN;
41 }
42 if (pre > PCA9685_PRESCALE_MAX)
43 {
44 pre = PCA9685_PRESCALE_MAX;
45 }
46 return (uint8_t)pre;
47}
48
49uint8_t pc_pca9685_channel_reg(uint8_t channel)
50{
51 if (channel >= PCA9685_CHANNELS)
52 {
53 return 0;
54 }
55 return (uint8_t)(PCA9685_REG_LED0_ON_L + 4 * channel);
56}
57
58uint16_t pc_pca9685_us_to_count(uint32_t microseconds, uint32_t freq_hz)
59{
60 // count = round(us * 4096 * freq / 1e6); 64-bit to avoid overflow at high pulse widths.
61 uint64_t num = (uint64_t)microseconds * 4096u * freq_hz;
62 uint32_t count = (uint32_t)((num + 500000u) / 1000000u);
63 return count > PCA9685_COUNT_MAX ? (uint16_t)PCA9685_COUNT_MAX : (uint16_t)count;
64}
65
66size_t pc_pca9685_set_pwm_bytes(uint8_t *buf, size_t cap, uint8_t channel, uint16_t on, uint16_t off)
67{
68 if (!buf || cap < 5 || channel >= PCA9685_CHANNELS)
69 {
70 return 0;
71 }
72 buf[0] = pc_pca9685_channel_reg(channel);
73 // Bit 4 of each _H register is the full-ON / full-OFF flag (count bit 12), so keep bits 4:0.
74 buf[1] = (uint8_t)(on & 0xFF);
75 buf[2] = (uint8_t)((on >> 8) & 0x1F);
76 buf[3] = (uint8_t)(off & 0xFF);
77 buf[4] = (uint8_t)((off >> 8) & 0x1F);
78 return 5;
79}
80
81// ---------------------------------------------------------------------------
82// I2C binding
83// ---------------------------------------------------------------------------
84
85#if defined(ARDUINO)
86
87namespace
88{
89// All PCA9685 I2C-binding state, owned by one instance (internal linkage): the device address
90// and the configured PWM frequency, grouped so it is one named owner, unreachable cross-TU.
91struct Pca9685Ctx
92{
93 uint8_t addr = PC_PCA9685_I2C_ADDR;
94 uint32_t freq = PC_PCA9685_FREQ;
95};
96Pca9685Ctx s_pca;
97
98bool wr(uint8_t reg, uint8_t val)
99{
100 Wire.beginTransmission(s_pca.addr);
101 Wire.write(reg);
102 Wire.write(val);
103 return Wire.endTransmission() == 0;
104}
105} // namespace
106
107bool pc_pca9685_begin(uint8_t addr, uint32_t freq_hz)
108{
109 s_pca.addr = addr ? addr : (uint8_t)PC_PCA9685_I2C_ADDR;
110 s_pca.freq = freq_hz ? freq_hz : (uint32_t)PC_PCA9685_FREQ;
111 pc_i2c_begin();
112 bool ok = true;
113 ok &= wr(PCA9685_REG_MODE1, 0x10); // SLEEP (required before changing PRESCALE)
114 ok &= wr(PCA9685_REG_PRESCALE, pc_pca9685_prescale(s_pca.freq));
115 ok &= wr(PCA9685_REG_MODE1, 0x20); // wake, auto-increment (AI)
116 delayMicroseconds(500); // oscillator settle
117 ok &= wr(PCA9685_REG_MODE1, 0xA0); // AI + RESTART
118 ok &= wr(PCA9685_REG_MODE2, 0x04); // OUTDRV: totem-pole outputs
119 return ok;
120}
121
122bool pc_pca9685_set_pwm(uint8_t channel, uint16_t on, uint16_t off)
123{
124 uint8_t b[5];
125 if (pc_pca9685_set_pwm_bytes(b, sizeof(b), channel, on, off) != 5)
126 {
127 return false;
128 }
129 Wire.beginTransmission(s_pca.addr);
130 Wire.write(b, 5);
131 return Wire.endTransmission() == 0;
132}
133
134bool pc_pca9685_set_servo_us(uint8_t channel, uint32_t microseconds)
135{
136 return pc_pca9685_set_pwm(channel, 0, pc_pca9685_us_to_count(microseconds, s_pca.freq));
137}
138
139#else // host build: no I2C. The prescale / count math + encoder above are host-tested.
140
141bool pc_pca9685_begin(uint8_t, uint32_t)
142{
143 return false;
144}
145bool pc_pca9685_set_pwm(uint8_t, uint16_t, uint16_t)
146{
147 return false;
148}
149bool pc_pca9685_set_servo_us(uint8_t, uint32_t)
150{
151 return false;
152}
153
154#endif // ARDUINO
155
156#endif // PC_ENABLE_PCA9685
The one owner of the shared I2C bus bring-up for the peripheral drivers.
void pc_i2c_begin()
Bring up the shared I2C bus on PC_I2C_SDA_PIN / PC_I2C_SCL_PIN (-1 = default).
Definition i2c.h:33
NXP PCA9685 16-channel 12-bit PWM / servo driver codec (PC_ENABLE_PCA9685).
#define PCA9685_CHANNELS
PWM output channels.
Definition pca9685.h:31
size_t pc_pca9685_set_pwm_bytes(uint8_t *buf, size_t cap, uint8_t channel, uint16_t on, uint16_t off)
Emit the 5-byte channel PWM write: [LED_ON_L(channel), ON_L, ON_H, OFF_L, OFF_H] (each count 12-bit l...
uint16_t pc_pca9685_us_to_count(uint32_t microseconds, uint32_t freq_hz)
Convert a servo pulse width (microseconds) at freq_hz to a 12-bit OFF count (rounded),...
bool pc_pca9685_set_servo_us(uint8_t channel, uint32_t microseconds)
Drive a servo on channel to a microseconds pulse (uses the configured frequency).
#define PCA9685_REG_MODE2
Definition pca9685.h:38
#define PCA9685_REG_MODE1
Definition pca9685.h:37
#define PCA9685_COUNT_MAX
a PWM count is 12-bit (0..4095)
Definition pca9685.h:32
bool pc_pca9685_set_pwm(uint8_t channel, uint16_t on, uint16_t off)
Set channel's raw 12-bit ON / OFF counts.
#define PCA9685_REG_LED0_ON_L
channel 0 base; channel n is this + 4*n
Definition pca9685.h:39
#define PCA9685_REG_PRESCALE
Definition pca9685.h:40
uint8_t pc_pca9685_prescale(uint32_t freq_hz)
Compute the PRESCALE register value for a PWM output frequency (25 MHz oscillator): round(25e6 / (409...
uint8_t pc_pca9685_channel_reg(uint8_t channel)
The register base (LED_ON_L) for channel (0..15); 0 for an out-of-range channel.
bool pc_pca9685_begin(uint8_t addr, uint32_t freq_hz)
Reset the PCA9685 at addr and set the PWM frequency freq_hz.
User-facing configuration for ProtoCore.
#define PC_PCA9685_I2C_ADDR
I2C address of the PCA9685 (0x40 default; the six address pins select 0x40..0x7F).
#define PC_PCA9685_FREQ
Default PWM output frequency in Hz (50 Hz suits hobby servos).