Skip to content

Commit 2c6ca00

Browse files
committed
drivers: led: Implemented PCA9533 driver with PM support
• Supports led_on/off, led_set_brightness (0–100 %, 152 Hz default), and led_blink (7 ms – 1.685 s) with automatic sharing of the two on-chip PWM engines; returns –EBUSY when a third distinct pair is requested. • Includes basic runtime-PM boilerplate to honour power-domain control; the device itself has no dedicated low-power states. Signed-off-by: Van Petrosyan <van.petrosyan@sensirion.com>
1 parent 10aaf88 commit 2c6ca00

File tree

4 files changed

+377
-0
lines changed

4 files changed

+377
-0
lines changed

drivers/led/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ zephyr_library_sources_ifdef(CONFIG_LP5562 lp5562.c)
2121
zephyr_library_sources_ifdef(CONFIG_LP5569 lp5569.c)
2222
zephyr_library_sources_ifdef(CONFIG_MODULINO_BUTTONS_LEDS modulino_buttons_leds.c)
2323
zephyr_library_sources_ifdef(CONFIG_NCP5623 ncp5623.c)
24+
zephyr_library_sources_ifdef(CONFIG_PCA9533 pca9533.c)
2425
zephyr_library_sources_ifdef(CONFIG_PCA9633 pca9633.c)
2526
zephyr_library_sources_ifdef(CONFIG_TLC59108 tlc59108.c)
2627
# zephyr-keep-sorted-stop

drivers/led/Kconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ source "drivers/led/Kconfig.lp5569"
4141
source "drivers/led/Kconfig.modulino"
4242
source "drivers/led/Kconfig.ncp5623"
4343
source "drivers/led/Kconfig.npm13xx"
44+
source "drivers/led/Kconfig.pca9533"
4445
source "drivers/led/Kconfig.pca9633"
4546
source "drivers/led/Kconfig.pwm"
4647
source "drivers/led/Kconfig.tlc59108"

drivers/led/Kconfig.pca9533

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright (c) 2025 Van Petrosyan
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
config PCA9533
5+
bool "PCA9533 LED driver"
6+
default y
7+
depends on DT_HAS_NXP_PCA9533_ENABLED
8+
select I2C
9+
help
10+
Enable driver support for the NXP PCA9533 4-bit LED dimmer.

drivers/led/pca9533.c

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
/*
2+
* Copyright (c) 2025 Van Petrosyan.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#define DT_DRV_COMPAT nxp_pca9533
8+
9+
/**
10+
* @file
11+
* @brief LED driver for the PCA9533 I2C LED driver (7-bit slave address 0x62)
12+
*/
13+
14+
#include <zephyr/drivers/i2c.h>
15+
#include <zephyr/drivers/led.h>
16+
#include <zephyr/pm/device.h>
17+
#include <zephyr/sys/util.h>
18+
#include <zephyr/kernel.h>
19+
20+
#include <zephyr/logging/log.h>
21+
LOG_MODULE_REGISTER(pca9533, CONFIG_LED_LOG_LEVEL);
22+
23+
#define PCA9533_CHANNELS 4U /* LED0…LED3 */
24+
#define PCA9533_ENGINES 2U
25+
26+
#define PCA9533_INPUT 0x00 /* read-only pin state */
27+
#define PCA9533_PSC0 0x01 /* BLINK0 period prescaler */
28+
#define PCA9533_PWM0 0x02 /* BLINK0 duty */
29+
#define PCA9533_PSC1 0x03
30+
#define PCA9533_PWM1 0x04
31+
#define PCA9533_LS0 0x05 /* LED selector (2 bits per LED) */
32+
33+
/* LS register bit fields (§6.3.6, Table 10) */
34+
#define LS_FUNC_OFF 0x0 /* high-Z → LED off */
35+
#define LS_FUNC_ON 0x1 /* output LOW → LED on */
36+
#define LS_FUNC_PWM0 0x2
37+
#define LS_FUNC_PWM1 0x3
38+
#define LS_SHIFT(ch) ((ch) * 2) /* 2 bits per LED in LS register */
39+
#define LS_MASK(ch) (0x3u << LS_SHIFT(ch))
40+
41+
/* Blink period limits derived from PSC range 0…255 (§6.3.2/6.3.4) */
42+
#define BLINK_MIN_MS 7U /* (0+1)/152 ≈ 6.58 ms → ceil */
43+
#define BLINK_MAX_MS 1685U /* (255+1)/152 ≈ 1.684 s → ceil */
44+
45+
/* Default PWM frequency when using set_brightness (152 Hz) */
46+
#define PCA9533_DEFAULT_PSC 0x00
47+
48+
struct pca9533_config {
49+
struct i2c_dt_spec i2c;
50+
};
51+
52+
struct pca9533_data {
53+
/* run-time bookkeeping for the two PWM engines */
54+
uint8_t pwm_val[PCA9533_ENGINES]; /* duty (0-255) programmed into PWMx */
55+
uint8_t psc_val[PCA9533_ENGINES]; /* prescaler programmed into PSCx */
56+
uint8_t engine_users[PCA9533_ENGINES]; /* bitmask of LEDs using engine 0 / 1 */
57+
uint8_t led_engine[PCA9533_CHANNELS]; /* map LED→engine (0xFF = not used) */
58+
};
59+
60+
/**
61+
* @brief Convert period in ms to PSC register value
62+
*
63+
* Formula: psc = round(period_ms * 152 / 1000) - 1
64+
*
65+
* @param period_ms Blink period in milliseconds
66+
* @return uint8_t PSC register value (clamped to 0-255)
67+
*/
68+
static uint8_t ms_to_psc(uint32_t period_ms)
69+
{
70+
uint32_t tmp = (period_ms * 152U + 500U) / 1000U;
71+
72+
return (uint8_t)CLAMP(tmp - 1U, 0U, 255U);
73+
}
74+
75+
/**
76+
* @brief Update LS bits for one LED (RMW operation)
77+
*
78+
* @param i2c I2C device specification
79+
* @param led LED index (0-3)
80+
* @param func Desired function (LS_FUNC_*)
81+
* @return int 0 on success, negative errno on error
82+
*/
83+
static int ls_update(const struct i2c_dt_spec *i2c, uint8_t led, uint8_t func)
84+
{
85+
return i2c_reg_update_byte_dt(i2c, PCA9533_LS0, LS_MASK(led), func << LS_SHIFT(led));
86+
}
87+
88+
/**
89+
* @brief Claim a PWM engine matching (psc,duty) or find a free one
90+
*
91+
* Engine allocation strategy:
92+
* 1. Reuse engine with exact (duty, psc) if exists
93+
* 2. Use free engine if available
94+
* 3. Return -EBUSY if no match
95+
*
96+
* @param data Driver data
97+
* @param duty Desired duty cycle (0-255)
98+
* @param psc Desired prescaler value
99+
* @param[out] out_ch Acquired engine ID (0 or 1)
100+
* @return int 0 on success, -EBUSY if no engine available
101+
*/
102+
static int engine_acquire(struct pca9533_data *data, uint8_t duty, uint8_t psc, uint8_t *out_ch)
103+
{
104+
/* Check for existing engine with matching parameters */
105+
for (uint8_t ch = 0; ch < PCA9533_ENGINES; ch++) {
106+
if (data->engine_users[ch] && data->pwm_val[ch] == duty &&
107+
data->psc_val[ch] == psc) {
108+
*out_ch = ch;
109+
return 0;
110+
}
111+
}
112+
/* Find free engine */
113+
for (uint8_t ch = 0; ch < PCA9533_ENGINES; ch++) {
114+
if (data->engine_users[ch] == 0) {
115+
*out_ch = ch;
116+
return 0;
117+
}
118+
}
119+
120+
return -EBUSY;
121+
}
122+
123+
/**
124+
* @brief Bind LED to a PWM engine
125+
*
126+
* @param data Driver data
127+
* @param led LED index (0-3)
128+
* @param ch Engine ID (0 or 1)
129+
*/
130+
static void engine_bind(struct pca9533_data *data, uint8_t led, uint8_t ch)
131+
{
132+
data->led_engine[led] = ch;
133+
data->engine_users[ch] |= BIT(led);
134+
}
135+
136+
/**
137+
* @brief Release LED from its current PWM engine
138+
*
139+
* @param data Driver data
140+
* @param led LED index (0-3)
141+
*/
142+
static void engine_release(struct pca9533_data *data, uint8_t led)
143+
{
144+
uint8_t ch = data->led_engine[led];
145+
146+
if (ch < PCA9533_ENGINES) {
147+
data->engine_users[ch] &= ~BIT(led);
148+
}
149+
data->led_engine[led] = 0xFF;
150+
}
151+
152+
static int pca9533_led_set_brightness(const struct device *dev, uint32_t led, uint8_t percent)
153+
{
154+
const struct pca9533_config *config = dev->config;
155+
struct pca9533_data *data = dev->data;
156+
int ret;
157+
uint8_t ch;
158+
159+
uint8_t duty = (percent * 255U) / LED_BRIGHTNESS_MAX;
160+
uint8_t cur = data->led_engine[led];
161+
162+
if (led >= PCA9533_CHANNELS) {
163+
LOG_ERR("Invalid LED index: %u", led);
164+
return -EINVAL;
165+
}
166+
167+
if (percent == 0) {
168+
LOG_DBG("LED%u -> OFF", led);
169+
ret = ls_update(&config->i2c, led, LS_FUNC_OFF);
170+
if (ret == 0) {
171+
engine_release(data, led);
172+
}
173+
return ret;
174+
}
175+
if (percent == LED_BRIGHTNESS_MAX) {
176+
LOG_DBG("LED%u -> ON", led);
177+
ret = ls_update(&config->i2c, led, LS_FUNC_ON);
178+
if (ret == 0) {
179+
engine_release(data, led);
180+
}
181+
return ret;
182+
}
183+
184+
/*
185+
* If LED is sole user of its current engine, we can retune it in place.
186+
* This avoids engine thrashing when adjusting brightness.
187+
*/
188+
if (cur < PCA9533_ENGINES && data->engine_users[cur] == BIT(led)) {
189+
/* Only update if duty has changed */
190+
if (data->pwm_val[cur] != duty) {
191+
LOG_DBG("LED%u retune duty %u on engine %u", led, duty, cur);
192+
ret = i2c_reg_write_byte_dt(&config->i2c, cur ? PCA9533_PWM1 : PCA9533_PWM0,
193+
duty);
194+
if (ret == 0) {
195+
data->pwm_val[cur] = duty;
196+
}
197+
}
198+
return 0;
199+
}
200+
201+
/* Acquire new engine - use default PSC for brightness control */
202+
ret = engine_acquire(data, duty, PCA9533_DEFAULT_PSC, &ch);
203+
if (ret) {
204+
LOG_WRN("No PWM engine available for LED %u", led);
205+
return ret;
206+
}
207+
208+
/* If engine is new (no users), program its registers */
209+
if (data->engine_users[ch] == 0) {
210+
/* Set default period (152 Hz) */
211+
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PSC1 : PCA9533_PSC0,
212+
PCA9533_DEFAULT_PSC);
213+
if (ret == 0) {
214+
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PWM1 : PCA9533_PWM0,
215+
duty);
216+
}
217+
if (ret) {
218+
LOG_ERR("Failed to program engine %u: %d", ch, ret);
219+
return ret;
220+
}
221+
data->psc_val[ch] = PCA9533_DEFAULT_PSC;
222+
data->pwm_val[ch] = duty;
223+
}
224+
225+
/* Bind LED to new engine and update hardware */
226+
LOG_DBG("LED%u uses engine %u (duty %u)", led, ch, duty);
227+
engine_release(data, led);
228+
engine_bind(data, led, ch);
229+
return ls_update(&config->i2c, led, ch ? LS_FUNC_PWM1 : LS_FUNC_PWM0);
230+
}
231+
232+
static int pca9533_led_blink(const struct device *dev, uint32_t led, uint32_t delay_on,
233+
uint32_t delay_off)
234+
{
235+
const struct pca9533_config *config = dev->config;
236+
struct pca9533_data *data = dev->data;
237+
int ret;
238+
239+
uint32_t period = delay_on + delay_off;
240+
241+
if (led >= PCA9533_CHANNELS) {
242+
LOG_ERR("Invalid LED index: %u", led);
243+
return -EINVAL;
244+
}
245+
246+
if (period < BLINK_MIN_MS || period > BLINK_MAX_MS) {
247+
LOG_ERR("Invalid blink period: %u ms (min: %u, max: %u)", period, BLINK_MIN_MS,
248+
BLINK_MAX_MS);
249+
return -ENOTSUP;
250+
}
251+
252+
/* Calculate duty cycle with overflow protection */
253+
uint32_t duty32 = (delay_on * 256U) / period;
254+
uint8_t duty = CLAMP(duty32, 0, 255);
255+
uint8_t psc = ms_to_psc(period);
256+
uint8_t cur = data->led_engine[led];
257+
258+
/* If LED is sole user of its engine, update in place */
259+
if (cur < PCA9533_ENGINES && data->engine_users[cur] == BIT(led)) {
260+
/* Only update if parameters changed */
261+
if (data->pwm_val[cur] != duty || data->psc_val[cur] != psc) {
262+
ret = i2c_reg_write_byte_dt(&config->i2c, cur ? PCA9533_PSC1 : PCA9533_PSC0,
263+
psc);
264+
if (ret == 0) {
265+
ret = i2c_reg_write_byte_dt(
266+
&config->i2c, cur ? PCA9533_PWM1 : PCA9533_PWM0, duty);
267+
}
268+
if (ret == 0) {
269+
data->psc_val[cur] = psc;
270+
data->pwm_val[cur] = duty;
271+
}
272+
}
273+
return 0;
274+
}
275+
276+
/* Acquire new engine with desired parameters */
277+
uint8_t ch;
278+
279+
ret = engine_acquire(data, duty, psc, &ch);
280+
if (ret) {
281+
LOG_WRN("No PWM engine available for LED %u blink", led);
282+
return ret;
283+
}
284+
285+
/* If engine is new (no users), program it */
286+
if (data->engine_users[ch] == 0) {
287+
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PSC1 : PCA9533_PSC0, psc);
288+
if (ret == 0) {
289+
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PWM1 : PCA9533_PWM0,
290+
duty);
291+
}
292+
if (ret) {
293+
LOG_ERR("Failed to program engine %u: %d", ch, ret);
294+
return ret;
295+
}
296+
data->psc_val[ch] = psc;
297+
data->pwm_val[ch] = duty;
298+
}
299+
300+
LOG_DBG("LED%u now on engine %u (psc %u duty %u)", led, ch, psc, duty);
301+
engine_release(data, led);
302+
engine_bind(data, led, ch);
303+
return ls_update(&config->i2c, led, ch ? LS_FUNC_PWM1 : LS_FUNC_PWM0);
304+
}
305+
306+
static int pca9533_led_init_chip(const struct device *dev)
307+
{
308+
struct pca9533_data *data = dev->data;
309+
310+
for (uint8_t i = 0; i < PCA9533_CHANNELS; i++) {
311+
data->led_engine[i] = 0xFF;
312+
}
313+
data->engine_users[0] = 0;
314+
data->engine_users[1] = 0;
315+
316+
/* The Power-On Reset already initializes the registers to their default state
317+
* no need to write them here. We'll just reset bookkeeping
318+
*/
319+
320+
return 0;
321+
}
322+
323+
static int pca9533_pm_action(const struct device *dev, enum pm_device_action action)
324+
{
325+
switch (action) {
326+
case PM_DEVICE_ACTION_TURN_ON:
327+
return pca9533_led_init_chip(dev);
328+
case PM_DEVICE_ACTION_RESUME:
329+
case PM_DEVICE_ACTION_SUSPEND:
330+
case PM_DEVICE_ACTION_TURN_OFF:
331+
return 0;
332+
333+
default:
334+
return -ENOTSUP;
335+
}
336+
}
337+
338+
static int pca9533_led_init(const struct device *dev)
339+
{
340+
const struct pca9533_config *config = dev->config;
341+
342+
if (!device_is_ready(config->i2c.bus)) {
343+
LOG_ERR("I²C bus not ready");
344+
return -ENODEV;
345+
}
346+
347+
return pm_device_driver_init(dev, pca9533_pm_action);
348+
}
349+
350+
static const struct led_driver_api pca9533_led_api = {
351+
.blink = pca9533_led_blink,
352+
.set_brightness = pca9533_led_set_brightness,
353+
};
354+
355+
#define PCA9533_DEVICE(id) \
356+
static const struct pca9533_config pca9533_##id##_cfg = { \
357+
.i2c = I2C_DT_SPEC_INST_GET(id), \
358+
}; \
359+
static struct pca9533_data pca9533_##id##_data; \
360+
PM_DEVICE_DT_INST_DEFINE(id, pca9533_pm_action); \
361+
DEVICE_DT_INST_DEFINE(id, &pca9533_led_init, PM_DEVICE_DT_INST_GET(id), \
362+
&pca9533_##id##_data, &pca9533_##id##_cfg, POST_KERNEL, \
363+
CONFIG_LED_INIT_PRIORITY, &pca9533_led_api);
364+
365+
DT_INST_FOREACH_STATUS_OKAY(PCA9533_DEVICE)

0 commit comments

Comments
 (0)