DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 "ServerConfig.h"
14
15#if DETWS_ENABLE_PCA9685
16
17namespace
18{
19const uint32_t PCA9685_OSC_HZ = 25000000u;
20const uint8_t PCA9685_PRESCALE_MIN = 3;
21const uint8_t PCA9685_PRESCALE_MAX = 255;
22} // namespace
23
24uint8_t pca9685_prescale(uint32_t freq_hz)
25{
26 if (freq_hz == 0)
27 return PCA9685_PRESCALE_MAX;
28 uint32_t denom = 4096u * freq_hz;
29 uint32_t pre = (PCA9685_OSC_HZ + denom / 2) / denom; // round(25e6 / (4096*freq))
30 pre = pre ? pre - 1 : 0;
31 if (pre < PCA9685_PRESCALE_MIN)
32 pre = PCA9685_PRESCALE_MIN;
33 if (pre > PCA9685_PRESCALE_MAX)
34 pre = PCA9685_PRESCALE_MAX;
35 return (uint8_t)pre;
36}
37
38uint8_t pca9685_channel_reg(uint8_t channel)
39{
40 if (channel >= PCA9685_CHANNELS)
41 return 0;
42 return (uint8_t)(PCA9685_REG_LED0_ON_L + 4 * channel);
43}
44
45uint16_t pca9685_us_to_count(uint32_t microseconds, uint32_t freq_hz)
46{
47 // count = round(us * 4096 * freq / 1e6); 64-bit to avoid overflow at high pulse widths.
48 uint64_t num = (uint64_t)microseconds * 4096u * freq_hz;
49 uint32_t count = (uint32_t)((num + 500000u) / 1000000u);
50 return count > PCA9685_COUNT_MAX ? (uint16_t)PCA9685_COUNT_MAX : (uint16_t)count;
51}
52
53size_t pca9685_set_pwm_bytes(uint8_t *buf, size_t cap, uint8_t channel, uint16_t on, uint16_t off)
54{
55 if (!buf || cap < 5 || channel >= PCA9685_CHANNELS)
56 return 0;
57 buf[0] = pca9685_channel_reg(channel);
58 // Bit 4 of each _H register is the full-ON / full-OFF flag (count bit 12), so keep bits 4:0.
59 buf[1] = (uint8_t)(on & 0xFF);
60 buf[2] = (uint8_t)((on >> 8) & 0x1F);
61 buf[3] = (uint8_t)(off & 0xFF);
62 buf[4] = (uint8_t)((off >> 8) & 0x1F);
63 return 5;
64}
65
66// ---------------------------------------------------------------------------
67// I2C binding
68// ---------------------------------------------------------------------------
69
70#if defined(ARDUINO)
71
72#include "services/i2c.h"
73#include <Arduino.h>
74#include <Wire.h>
75
76namespace
77{
78// All PCA9685 I2C-binding state, owned by one instance (internal linkage): the device address
79// and the configured PWM frequency, grouped so it is one named owner, unreachable cross-TU.
80struct Pca9685Ctx
81{
82 uint8_t addr = DETWS_PCA9685_I2C_ADDR;
83 uint32_t freq = DETWS_PCA9685_FREQ;
84};
85Pca9685Ctx s_pca;
86
87bool wr(uint8_t reg, uint8_t val)
88{
89 Wire.beginTransmission(s_pca.addr);
90 Wire.write(reg);
91 Wire.write(val);
92 return Wire.endTransmission() == 0;
93}
94} // namespace
95
96bool pca9685_begin(uint8_t addr, uint32_t freq_hz)
97{
98 s_pca.addr = addr ? addr : (uint8_t)DETWS_PCA9685_I2C_ADDR;
99 s_pca.freq = freq_hz ? freq_hz : (uint32_t)DETWS_PCA9685_FREQ;
101 bool ok = true;
102 ok &= wr(PCA9685_REG_MODE1, 0x10); // SLEEP (required before changing PRESCALE)
103 ok &= wr(PCA9685_REG_PRESCALE, pca9685_prescale(s_pca.freq));
104 ok &= wr(PCA9685_REG_MODE1, 0x20); // wake, auto-increment (AI)
105 delayMicroseconds(500); // oscillator settle
106 ok &= wr(PCA9685_REG_MODE1, 0xA0); // AI + RESTART
107 ok &= wr(PCA9685_REG_MODE2, 0x04); // OUTDRV: totem-pole outputs
108 return ok;
109}
110
111bool pca9685_set_pwm(uint8_t channel, uint16_t on, uint16_t off)
112{
113 uint8_t b[5];
114 if (pca9685_set_pwm_bytes(b, sizeof(b), channel, on, off) != 5)
115 return false;
116 Wire.beginTransmission(s_pca.addr);
117 Wire.write(b, 5);
118 return Wire.endTransmission() == 0;
119}
120
121bool pca9685_set_servo_us(uint8_t channel, uint32_t microseconds)
122{
123 return pca9685_set_pwm(channel, 0, pca9685_us_to_count(microseconds, s_pca.freq));
124}
125
126#else // host build: no I2C. The prescale / count math + encoder above are host-tested.
127
128bool pca9685_begin(uint8_t, uint32_t)
129{
130 return false;
131}
132bool pca9685_set_pwm(uint8_t, uint16_t, uint16_t)
133{
134 return false;
135}
136bool pca9685_set_servo_us(uint8_t, uint32_t)
137{
138 return false;
139}
140
141#endif // ARDUINO
142
143#endif // DETWS_ENABLE_PCA9685
User-facing configuration for DeterministicESPAsyncWebServer.
#define DETWS_PCA9685_FREQ
Default PWM output frequency in Hz (50 Hz suits hobby servos).
#define DETWS_PCA9685_I2C_ADDR
I2C address of the PCA9685 (0x40 default; the six address pins select 0x40..0x7F).
The one owner of the shared I2C bus bring-up for the peripheral drivers.
void detws_i2c_begin()
Bring up the shared I2C bus on DETWS_I2C_SDA_PIN / DETWS_I2C_SCL_PIN (-1 = default).
Definition i2c.h:33
NXP PCA9685 16-channel 12-bit PWM / servo driver codec (DETWS_ENABLE_PCA9685).
bool pca9685_begin(uint8_t addr, uint32_t freq_hz)
Reset the PCA9685 at addr and set the PWM frequency freq_hz.
#define PCA9685_CHANNELS
PWM output channels.
Definition pca9685.h:31
uint16_t 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),...
uint8_t pca9685_prescale(uint32_t freq_hz)
Compute the PRESCALE register value for a PWM output frequency (25 MHz oscillator): round(25e6 / (409...
#define PCA9685_REG_MODE2
Definition pca9685.h:38
bool 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_MODE1
Definition pca9685.h:37
bool pca9685_set_pwm(uint8_t channel, uint16_t on, uint16_t off)
Set channel's raw 12-bit ON / OFF counts.
uint8_t pca9685_channel_reg(uint8_t channel)
The register base (LED_ON_L) for channel (0..15); 0 for an out-of-range channel.
size_t 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...
#define PCA9685_COUNT_MAX
a PWM count is 12-bit (0..4095)
Definition pca9685.h:32
#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