Skip to content

drivers: led: Added driver for PCA9533 LED dimmer #92752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions drivers/led/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ zephyr_library_sources_ifdef(CONFIG_LP5562 lp5562.c)
zephyr_library_sources_ifdef(CONFIG_LP5569 lp5569.c)
zephyr_library_sources_ifdef(CONFIG_MODULINO_BUTTONS_LEDS modulino_buttons_leds.c)
zephyr_library_sources_ifdef(CONFIG_NCP5623 ncp5623.c)
zephyr_library_sources_ifdef(CONFIG_PCA9533 pca9533.c)
zephyr_library_sources_ifdef(CONFIG_PCA9633 pca9633.c)
zephyr_library_sources_ifdef(CONFIG_TLC59108 tlc59108.c)
# zephyr-keep-sorted-stop
Expand Down
1 change: 1 addition & 0 deletions drivers/led/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ source "drivers/led/Kconfig.lp5569"
source "drivers/led/Kconfig.modulino"
source "drivers/led/Kconfig.ncp5623"
source "drivers/led/Kconfig.npm13xx"
source "drivers/led/Kconfig.pca9533"
source "drivers/led/Kconfig.pca9633"
source "drivers/led/Kconfig.pwm"
source "drivers/led/Kconfig.tlc59108"
Expand Down
10 changes: 10 additions & 0 deletions drivers/led/Kconfig.pca9533
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (c) 2025 Van Petrosyan
# SPDX-License-Identifier: Apache-2.0

config PCA9533
bool "PCA9533 LED driver"
default y
depends on DT_HAS_NXP_PCA9533_ENABLED
select I2C
help
Enable driver support for the NXP PCA9533 4-bit LED dimmer.
364 changes: 364 additions & 0 deletions drivers/led/pca9533.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
/*
* Copyright (c) 2025 Van Petrosyan.
*
* SPDX-License-Identifier: Apache-2.0
*/

#define DT_DRV_COMPAT nxp_pca9533

/**
* @file
* @brief LED driver for the PCA9533 I2C LED driver (7-bit slave address 0x62)
*/

#include <zephyr/drivers/i2c.h>
#include <zephyr/drivers/led.h>
#include <zephyr/pm/device.h>
#include <zephyr/sys/util.h>
#include <zephyr/kernel.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(pca9533, CONFIG_LED_LOG_LEVEL);

#define PCA9533_CHANNELS 4U /* LED0…LED3 */
#define PCA9533_ENGINES 2U

#define PCA9533_INPUT 0x00 /* read-only pin state */
#define PCA9533_PSC0 0x01 /* BLINK0 period prescaler */
#define PCA9533_PWM0 0x02 /* BLINK0 duty */
#define PCA9533_PSC1 0x03
#define PCA9533_PWM1 0x04
#define PCA9533_LS0 0x05 /* LED selector (2 bits per LED) */

/* LS register bit fields (§6.3.6, Table 10) */
#define LS_FUNC_OFF 0x0 /* high-Z → LED off */
#define LS_FUNC_ON 0x1 /* output LOW → LED on */
#define LS_FUNC_PWM0 0x2
#define LS_FUNC_PWM1 0x3
#define LS_SHIFT(ch) ((ch) * 2) /* 2 bits per LED in LS register */
#define LS_MASK(ch) (0x3u << LS_SHIFT(ch))

/* Blink period limits derived from PSC range 0…255 (§6.3.2/6.3.4) */
#define BLINK_MIN_MS 7U /* (0+1)/152 ≈ 6.58 ms → ceil */
#define BLINK_MAX_MS 1685U /* (255+1)/152 ≈ 1.684 s → ceil */

/* Default PWM frequency when using set_brightness (152 Hz) */
#define PCA9533_DEFAULT_PSC 0x00

struct pca9533_config {
struct i2c_dt_spec i2c;
};

struct pca9533_data {
/* run-time bookkeeping for the two PWM engines */
uint8_t pwm_val[PCA9533_ENGINES]; /* duty (0-255) programmed into PWMx */
uint8_t psc_val[PCA9533_ENGINES]; /* prescaler programmed into PSCx */
uint8_t engine_users[PCA9533_ENGINES]; /* bitmask of LEDs using engine 0 / 1 */
uint8_t led_engine[PCA9533_CHANNELS]; /* map LED→engine (0xFF = not used) */
};

/**
* @brief Convert period in ms to PSC register value
*
* Formula: psc = round(period_ms * 152 / 1000) - 1
*
* @param period_ms Blink period in milliseconds
* @return uint8_t PSC register value (clamped to 0-255)
*/
static uint8_t ms_to_psc(uint32_t period_ms)
{
uint32_t tmp = (period_ms * 152U + 500U) / 1000U;

return CLAMP(tmp - 1U, 0U, UINT8_MAX);
}

/**
* @brief Update LS bits for one LED (RMW operation)
*
* @param i2c I2C device specification
* @param led LED index (0-3)
* @param func Desired function (LS_FUNC_*)
* @return int 0 on success, negative errno on error
*/
static int ls_update(const struct i2c_dt_spec *i2c, uint8_t led, uint8_t func)
{
return i2c_reg_update_byte_dt(i2c, PCA9533_LS0, LS_MASK(led), func << LS_SHIFT(led));
}

/**
* @brief Claim a PWM engine matching (psc,duty) or find a free one
*
* Engine allocation strategy:
* 1. Reuse engine with exact (duty, psc) if exists
* 2. Use free engine if available
* 3. Return -EBUSY if no match
*
* @param data Driver data
* @param duty Desired duty cycle (0-255)
* @param psc Desired prescaler value
* @param[out] out_ch Acquired engine ID (0 or 1)
* @return int 0 on success, -EBUSY if no engine available
*/
static int engine_acquire(struct pca9533_data *data, uint8_t duty, uint8_t psc, uint8_t *out_ch)
{
/* Check for existing engine with matching parameters */
for (uint8_t ch = 0; ch < PCA9533_ENGINES; ch++) {
if (data->engine_users[ch] && data->pwm_val[ch] == duty &&
data->psc_val[ch] == psc) {
*out_ch = ch;
return 0;
}
}
/* Find free engine */
for (uint8_t ch = 0; ch < PCA9533_ENGINES; ch++) {
if (data->engine_users[ch] == 0) {
*out_ch = ch;
return 0;
}
}

return -EBUSY;
}

/**
* @brief Bind LED to a PWM engine
*
* @param data Driver data
* @param led LED index (0-3)
* @param ch Engine ID (0 or 1)
*/
static void engine_bind(struct pca9533_data *data, uint8_t led, uint8_t ch)
{
data->led_engine[led] = ch;
data->engine_users[ch] |= BIT(led);
}

/**
* @brief Release LED from its current PWM engine
*
* @param data Driver data
* @param led LED index (0-3)
*/
static void engine_release(struct pca9533_data *data, uint8_t led)
{
uint8_t ch = data->led_engine[led];

if (ch < PCA9533_ENGINES) {
data->engine_users[ch] &= ~BIT(led);
}
data->led_engine[led] = 0xFF;
}

static int pca9533_led_set_brightness(const struct device *dev, uint32_t led, uint8_t percent)
{
const struct pca9533_config *config = dev->config;
struct pca9533_data *data = dev->data;
uint8_t cur, duty, ch;
int ret;

if (led >= PCA9533_CHANNELS) {
LOG_ERR("Invalid LED index: %u", led);
return -EINVAL;
}

if (percent == 0) {
LOG_DBG("LED%u -> OFF", led);
ret = ls_update(&config->i2c, led, LS_FUNC_OFF);
if (ret == 0) {
engine_release(data, led);
}
return ret;
}
if (percent == LED_BRIGHTNESS_MAX) {
LOG_DBG("LED%u -> ON", led);
ret = ls_update(&config->i2c, led, LS_FUNC_ON);
if (ret == 0) {
engine_release(data, led);
}
return ret;
}

duty = (percent * UINT8_MAX) / LED_BRIGHTNESS_MAX;
cur = data->led_engine[led];

/*
* If LED is sole user of its current engine, we can retune it in place.
* This avoids engine thrashing when adjusting brightness.
*/
if (cur < PCA9533_ENGINES && data->engine_users[cur] == BIT(led)) {
/* Only update if duty has changed */
if (data->pwm_val[cur] != duty) {
LOG_DBG("LED%u retune duty %u on engine %u", led, duty, cur);
ret = i2c_reg_write_byte_dt(&config->i2c, cur ? PCA9533_PWM1 : PCA9533_PWM0,
duty);
if (ret == 0) {
data->pwm_val[cur] = duty;
}
}
return 0;
}

/* Acquire new engine - use default PSC for brightness control */
ret = engine_acquire(data, duty, PCA9533_DEFAULT_PSC, &ch);
if (ret) {
LOG_WRN("No PWM engine available for LED %u", led);
return ret;
}

/* If engine is new (no users), program its registers */
if (data->engine_users[ch] == 0) {
/* Set default period (152 Hz) */
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PSC1 : PCA9533_PSC0,
PCA9533_DEFAULT_PSC);
if (ret == 0) {
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PWM1 : PCA9533_PWM0,
duty);
}
if (ret) {
LOG_ERR("Failed to program engine %u: %d", ch, ret);
return ret;
}
data->psc_val[ch] = PCA9533_DEFAULT_PSC;
data->pwm_val[ch] = duty;
}

/* Bind LED to new engine and update hardware */
LOG_DBG("LED%u uses engine %u (duty %u)", led, ch, duty);
engine_release(data, led);
engine_bind(data, led, ch);
return ls_update(&config->i2c, led, ch ? LS_FUNC_PWM1 : LS_FUNC_PWM0);
}

static int pca9533_led_blink(const struct device *dev, uint32_t led, uint32_t delay_on,
uint32_t delay_off)
{
const struct pca9533_config *config = dev->config;
struct pca9533_data *data = dev->data;
int ret;
uint8_t ch, duty, psc, cur;
uint32_t period, duty32;

if (led >= PCA9533_CHANNELS) {
LOG_ERR("Invalid LED index: %u", led);
return -EINVAL;
}

period = delay_on + delay_off;
if (period < BLINK_MIN_MS || period > BLINK_MAX_MS) {
LOG_ERR("Invalid blink period: %u ms (min: %u, max: %u)", period, BLINK_MIN_MS,
BLINK_MAX_MS);
return -ENOTSUP;
}

/* Calculate duty cycle with overflow protection */
duty32 = (delay_on * 256U) / period;
duty = CLAMP(duty32, 0, UINT8_MAX);
psc = ms_to_psc(period);
cur = data->led_engine[led];

/* If LED is sole user of its engine, update in place */
if (cur < PCA9533_ENGINES && data->engine_users[cur] == BIT(led)) {
/* Only update if parameters changed */
if (data->pwm_val[cur] != duty || data->psc_val[cur] != psc) {
ret = i2c_reg_write_byte_dt(&config->i2c, cur ? PCA9533_PSC1 : PCA9533_PSC0,
psc);
if (ret == 0) {
ret = i2c_reg_write_byte_dt(
&config->i2c, cur ? PCA9533_PWM1 : PCA9533_PWM0, duty);
}
if (ret == 0) {
data->psc_val[cur] = psc;
data->pwm_val[cur] = duty;
}
}
return 0;
}

/* Acquire new engine with desired parameters */
ret = engine_acquire(data, duty, psc, &ch);
if (ret) {
LOG_WRN("No PWM engine available for LED %u blink", led);
return ret;
}

/* If engine is new (no users), program it */
if (data->engine_users[ch] == 0) {
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PSC1 : PCA9533_PSC0, psc);
if (ret == 0) {
ret = i2c_reg_write_byte_dt(&config->i2c, ch ? PCA9533_PWM1 : PCA9533_PWM0,
duty);
}
if (ret) {
LOG_ERR("Failed to program engine %u: %d", ch, ret);
return ret;
}
data->psc_val[ch] = psc;
data->pwm_val[ch] = duty;
}

LOG_DBG("LED%u now on engine %u (psc %u duty %u)", led, ch, psc, duty);
engine_release(data, led);
engine_bind(data, led, ch);
return ls_update(&config->i2c, led, ch ? LS_FUNC_PWM1 : LS_FUNC_PWM0);
}

static int pca9533_led_init_chip(const struct device *dev)
{
struct pca9533_data *data = dev->data;

for (uint8_t i = 0; i < PCA9533_CHANNELS; i++) {
data->led_engine[i] = 0xFF;
}
data->engine_users[0] = 0;
data->engine_users[1] = 0;

/* The Power-On Reset already initializes the registers to their default state
* no need to write them here. We'll just reset bookkeeping
*/

return 0;
}

static int pca9533_pm_action(const struct device *dev, enum pm_device_action action)
{
switch (action) {
case PM_DEVICE_ACTION_TURN_ON:
return pca9533_led_init_chip(dev);
case PM_DEVICE_ACTION_RESUME:
case PM_DEVICE_ACTION_SUSPEND:
case PM_DEVICE_ACTION_TURN_OFF:
return 0;

default:
return -ENOTSUP;
}
}

static int pca9533_led_init(const struct device *dev)
{
const struct pca9533_config *config = dev->config;

if (!i2c_is_ready_dt(&config->i2c)) {
LOG_ERR("%s is not ready", config->i2c.bus->name);
return -ENODEV;
}

return pm_device_driver_init(dev, pca9533_pm_action);
}

static const struct led_driver_api pca9533_led_api = {
.blink = pca9533_led_blink,
.set_brightness = pca9533_led_set_brightness,
};

#define PCA9533_DEVICE(id) \
static const struct pca9533_config pca9533_##id##_cfg = { \
.i2c = I2C_DT_SPEC_INST_GET(id), \
}; \
static struct pca9533_data pca9533_##id##_data; \
PM_DEVICE_DT_INST_DEFINE(id, pca9533_pm_action); \
DEVICE_DT_INST_DEFINE(id, &pca9533_led_init, PM_DEVICE_DT_INST_GET(id), \
&pca9533_##id##_data, &pca9533_##id##_cfg, POST_KERNEL, \
CONFIG_LED_INIT_PRIORITY, &pca9533_led_api);

DT_INST_FOREACH_STATUS_OKAY(PCA9533_DEVICE)
5 changes: 5 additions & 0 deletions dts/bindings/led/nxp,pca9533.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
description: NXP PCA9533 4-bit LED dimmer

compatible: "nxp,pca9533"

include: ["i2c-device.yaml", "led-controller.yaml"]
Loading