From 4532cbd4f0c18dfb5ce52cebc0530090734a7da9 Mon Sep 17 00:00:00 2001 From: Van Petrosyan Date: Sun, 6 Jul 2025 21:47:54 +0200 Subject: [PATCH 1/3] drivers: led: Added dt-binding for pca9533 led dimmer Added DT binding for the PCA9533 LED Dimmer Signed-off-by: Van Petrosyan --- dts/bindings/led/nxp,pca9533.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 dts/bindings/led/nxp,pca9533.yaml diff --git a/dts/bindings/led/nxp,pca9533.yaml b/dts/bindings/led/nxp,pca9533.yaml new file mode 100644 index 000000000000..624bb11102c9 --- /dev/null +++ b/dts/bindings/led/nxp,pca9533.yaml @@ -0,0 +1,5 @@ +description: NXP PCA9533 4-bit LED dimmer + +compatible: "nxp,pca9533" + +include: ["i2c-device.yaml", "led-controller.yaml"] From 533eae5a408abfafab993e5052901cc657b6359a Mon Sep 17 00:00:00 2001 From: Van Petrosyan Date: Sun, 6 Jul 2025 22:00:39 +0200 Subject: [PATCH 2/3] 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 | 364 ++++++++++++++++++++++++++++++++++++ 4 files changed, 376 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 1d8b9d2aa960..bd3bed9e8775 100644 --- a/drivers/led/CMakeLists.txt +++ b/drivers/led/CMakeLists.txt @@ -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 diff --git a/drivers/led/Kconfig b/drivers/led/Kconfig index 0e411ce21f04..47f2dfa17262 100644 --- a/drivers/led/Kconfig +++ b/drivers/led/Kconfig @@ -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" diff --git a/drivers/led/Kconfig.pca9533 b/drivers/led/Kconfig.pca9533 new file mode 100644 index 000000000000..7ea220fe4406 --- /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 000000000000..2752c67151e4 --- /dev/null +++ b/drivers/led/pca9533.c @@ -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 +#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 */ + 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) From 09a7c6e0241b5f1bf84fb9e8ed2a3c138ed45fd6 Mon Sep 17 00:00:00 2001 From: Van Petrosyan Date: Sun, 6 Jul 2025 22:46:45 +0200 Subject: [PATCH 3/3] drivers: led: Added PCA9533 to build_all overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PCA9533 is added to the overlay so the new driver is compiled by the automated “build all LED drivers” test Signed-off-by: Van Petrosyan --- tests/drivers/build_all/led/app.overlay | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/drivers/build_all/led/app.overlay b/tests/drivers/build_all/led/app.overlay index ce0e7ee4ad62..61c1ff35a7e0 100644 --- a/tests/drivers/build_all/led/app.overlay +++ b/tests/drivers/build_all/led/app.overlay @@ -192,6 +192,11 @@ nordic,led2-mode = "host"; }; }; + + pca9533@15 { + compatible = "nxp,pca9533"; + reg = <0x15>; + }; }; test_pwm: pwm@12341234 {