-
Notifications
You must be signed in to change notification settings - Fork 7.7k
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
petrosyan-van
wants to merge
3
commits into
zephyrproject-rtos:main
Choose a base branch
from
petrosyan-van:drivers_led_pca9533
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+386
−0
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
fabiobaltieri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.