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