Skip to content

Commit 533eae5

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 4532cbd commit 533eae5

File tree

4 files changed

+376
-0
lines changed

4 files changed

+376
-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: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
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 CLAMP(tmp - 1U, 0U, UINT8_MAX);
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+
uint8_t cur, duty, ch;
157+
int ret;
158+
159+
if (led >= PCA9533_CHANNELS) {
160+
LOG_ERR("Invalid LED index: %u", led);
161+
return -EINVAL;
162+
}
163+
164+
if (percent == 0) {
165+
LOG_DBG("LED%u -> OFF", led);
166+
ret = ls_update(&config->i2c, led, LS_FUNC_OFF);
167+
if (ret == 0) {
168+
engine_release(data, led);
169+
}
170+
return ret;
171+
}
172+
if (percent == LED_BRIGHTNESS_MAX) {
173+
LOG_DBG("LED%u -> ON", led);
174+
ret = ls_update(&config->i2c, led, LS_FUNC_ON);
175+
if (ret == 0) {
176+
engine_release(data, led);
177+
}
178+
return ret;
179+
}
180+
181+
duty = (percent * UINT8_MAX) / LED_BRIGHTNESS_MAX;
182+
cur = data->led_engine[led];
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+
uint8_t ch, duty, psc, cur;
239+
uint32_t period, duty32;
240+
241+
if (led >= PCA9533_CHANNELS) {
242+
LOG_ERR("Invalid LED index: %u", led);
243+
return -EINVAL;
244+
}
245+
246+
period = delay_on + delay_off;
247+
if (period < BLINK_MIN_MS || period > BLINK_MAX_MS) {
248+
LOG_ERR("Invalid blink period: %u ms (min: %u, max: %u)", period, BLINK_MIN_MS,
249+
BLINK_MAX_MS);
250+
return -ENOTSUP;
251+
}
252+
253+
/* Calculate duty cycle with overflow protection */
254+
duty32 = (delay_on * 256U) / period;
255+
duty = CLAMP(duty32, 0, UINT8_MAX);
256+
psc = ms_to_psc(period);
257+
cur = data->led_engine[led];
258+
259+
/* If LED is sole user of its engine, update in place */
260+
if (cur < PCA9533_ENGINES && data->engine_users[cur] == BIT(led)) {
261+
/* Only update if parameters changed */
262+
if (data->pwm_val[cur] != duty || data->psc_val[cur] != psc) {
263+
ret = i2c_reg_write_byte_dt(&config->i2c, cur ? PCA9533_PSC1 : PCA9533_PSC0,
264+
psc);
265+
if (ret == 0) {
266+
ret = i2c_reg_write_byte_dt(
267+
&config->i2c, cur ? PCA9533_PWM1 : PCA9533_PWM0, duty);
268+
}
269+
if (ret == 0) {
270+
data->psc_val[cur] = psc;
271+
data->pwm_val[cur] = duty;
272+
}
273+
}
274+
return 0;
275+
}
276+
277+
/* Acquire new engine with desired parameters */
278+
ret = engine_acquire(data, duty, psc, &ch);
279+
if (ret) {
280+
LOG_WRN("No PWM engine available for LED %u blink", led);
281+
return ret;
282+
}
283+
284+
/* If engine is new (no users), program it */
285+
if (data->engine_users[ch] == 0) {
286+
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PSC1 : PCA9533_PSC0, psc);
287+
if (ret == 0) {
288+
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PWM1 : PCA9533_PWM0,
289+
duty);
290+
}
291+
if (ret) {
292+
LOG_ERR("Failed to program engine %u: %d", ch, ret);
293+
return ret;
294+
}
295+
data->psc_val[ch] = psc;
296+
data->pwm_val[ch] = duty;
297+
}
298+
299+
LOG_DBG("LED%u now on engine %u (psc %u duty %u)", led, ch, psc, duty);
300+
engine_release(data, led);
301+
engine_bind(data, led, ch);
302+
return ls_update(&config->i2c, led, ch ? LS_FUNC_PWM1 : LS_FUNC_PWM0);
303+
}
304+
305+
static int pca9533_led_init_chip(const struct device *dev)
306+
{
307+
struct pca9533_data *data = dev->data;
308+
309+
for (uint8_t i = 0; i < PCA9533_CHANNELS; i++) {
310+
data->led_engine[i] = 0xFF;
311+
}
312+
data->engine_users[0] = 0;
313+
data->engine_users[1] = 0;
314+
315+
/* The Power-On Reset already initializes the registers to their default state
316+
* no need to write them here. We'll just reset bookkeeping
317+
*/
318+
319+
return 0;
320+
}
321+
322+
static int pca9533_pm_action(const struct device *dev, enum pm_device_action action)
323+
{
324+
switch (action) {
325+
case PM_DEVICE_ACTION_TURN_ON:
326+
return pca9533_led_init_chip(dev);
327+
case PM_DEVICE_ACTION_RESUME:
328+
case PM_DEVICE_ACTION_SUSPEND:
329+
case PM_DEVICE_ACTION_TURN_OFF:
330+
return 0;
331+
332+
default:
333+
return -ENOTSUP;
334+
}
335+
}
336+
337+
static int pca9533_led_init(const struct device *dev)
338+
{
339+
const struct pca9533_config *config = dev->config;
340+
341+
if (!i2c_is_ready_dt(&config->i2c)) {
342+
LOG_ERR("%s is not ready", config->i2c.bus->name);
343+
return -ENODEV;
344+
}
345+
346+
return pm_device_driver_init(dev, pca9533_pm_action);
347+
}
348+
349+
static const struct led_driver_api pca9533_led_api = {
350+
.blink = pca9533_led_blink,
351+
.set_brightness = pca9533_led_set_brightness,
352+
};
353+
354+
#define PCA9533_DEVICE(id) \
355+
static const struct pca9533_config pca9533_##id##_cfg = { \
356+
.i2c = I2C_DT_SPEC_INST_GET(id), \
357+
}; \
358+
static struct pca9533_data pca9533_##id##_data; \
359+
PM_DEVICE_DT_INST_DEFINE(id, pca9533_pm_action); \
360+
DEVICE_DT_INST_DEFINE(id, &pca9533_led_init, PM_DEVICE_DT_INST_GET(id), \
361+
&pca9533_##id##_data, &pca9533_##id##_cfg, POST_KERNEL, \
362+
CONFIG_LED_INIT_PRIORITY, &pca9533_led_api);
363+
364+
DT_INST_FOREACH_STATUS_OKAY(PCA9533_DEVICE)

0 commit comments

Comments
 (0)