From 7c845f14d4f791a9f9bf480038fe1064a25a998a Mon Sep 17 00:00:00 2001 From: Van Petrosyan Date: Sun, 6 Jul 2025 22:00:39 +0200 Subject: [PATCH] drivers: led: Implemented PCA9533 driver with PM support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • 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 --- drivers/led/CMakeLists.txt | 1 + drivers/led/Kconfig | 1 + drivers/led/Kconfig.pca9533 | 10 + drivers/led/pca9533.c | 415 ++++++++++++++++++++++++++++++++++++ 4 files changed, 427 insertions(+) create mode 100644 drivers/led/Kconfig.pca9533 create mode 100644 drivers/led/pca9533.c diff --git a/drivers/led/CMakeLists.txt b/drivers/led/CMakeLists.txt index 6cbfb493fdf..9a53ac7c282 100644 --- a/drivers/led/CMakeLists.txt +++ b/drivers/led/CMakeLists.txt @@ -22,6 +22,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 diff --git a/drivers/led/Kconfig b/drivers/led/Kconfig index c9139846c34..f6809fc7c9d 100644 --- a/drivers/led/Kconfig +++ b/drivers/led/Kconfig @@ -42,6 +42,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" diff --git a/drivers/led/Kconfig.pca9533 b/drivers/led/Kconfig.pca9533 new file mode 100644 index 00000000000..7ea220fe440 --- /dev/null +++ b/drivers/led/Kconfig.pca9533 @@ -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. diff --git a/drivers/led/pca9533.c b/drivers/led/pca9533.c new file mode 100644 index 00000000000..e54c2d7af2d --- /dev/null +++ b/drivers/led/pca9533.c @@ -0,0 +1,415 @@ +/* + * 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 +#include +#include +#include + +#include +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 */ +}; + +/** + * @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)); +} + +/** + * Return engine index (0 - PCA9533_ENGINES-1) that currently drives @p led, + * or 0xFF if the LED is not routed to any engine (OFF / ON state). + */ +static uint8_t find_engine_for_led(const struct pca9533_data *data, uint8_t led) +{ + for (uint8_t ch = 0; ch < PCA9533_ENGINES; ch++) { + if (data->engine_users[ch] & BIT(led)) { + return ch; + } + } + return 0xFF; +} + +/** + * @brief Find an in-use engine whose parameters already match (duty, psc) + * + * @param data Driver data + * @param duty Desired duty + * @param psc Desired prescaler + * @param[out] out_ch Matching engine ID + * @return 0 if found, -ENOENT otherwise + */ +static int engine_find_match(const struct pca9533_data *data, uint8_t duty, uint8_t psc, + uint8_t *out_ch) +{ + 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; + } + } + return -ENOENT; +} + +/** + * @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->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 = find_engine_for_led(data, led); + + if (ch < PCA9533_ENGINES) { + data->engine_users[ch] &= ~BIT(led); + } +} + +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 = find_engine_for_led(data, led); + + /* Sole-user fast-path, with reuse-check */ + if (cur < PCA9533_ENGINES && data->engine_users[cur] == BIT(led)) { + uint8_t match; + + /* Can we piggy-back on an existing engine already at (duty, default psc)? */ + if (!engine_find_match(data, duty, PCA9533_DEFAULT_PSC, &match) && match != cur) { + LOG_DBG("LED%u moves from engine %u to matching engine %u", led, cur, + match); + engine_release(data, led); + engine_bind(data, led, match); + return ls_update(&config->i2c, led, match ? LS_FUNC_PWM1 : LS_FUNC_PWM0); + } + + /* Otherwise retune in place */ + ret = 0; + 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 ret; + } + + /* 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 = find_engine_for_led(data, led); + + /* Sole-user fast-path with reuse-check */ + if (cur < PCA9533_ENGINES && data->engine_users[cur] == BIT(led)) { + uint8_t match; + + /* Look for another engine already at the desired (psc,duty) */ + if (!engine_find_match(data, duty, psc, &match) && match != cur) { + LOG_DBG("LED%u moves from engine %u to matching engine %u (blink)", led, + cur, match); + engine_release(data, led); + engine_bind(data, led, match); + return ls_update(&config->i2c, led, match ? LS_FUNC_PWM1 : LS_FUNC_PWM0); + } + + /* Otherwise update this engine in place */ + ret = 0; + 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 ret; + } + + /* 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_ENGINES; i++) { + data->engine_users[i] = 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)