| /* |
| * Copyright (c) 2020 Seagate Technology LLC |
| * Copyright (c) 2022 Grinn |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| /** |
| * @file |
| * @brief LP50xx LED controller |
| */ |
| #include <errno.h> |
| #include <zephyr/device.h> |
| #include <zephyr/devicetree.h> |
| #include <zephyr/drivers/gpio.h> |
| #include <zephyr/drivers/i2c.h> |
| #include <zephyr/drivers/led.h> |
| #include <zephyr/drivers/led/lp50xx.h> |
| #include <zephyr/kernel.h> |
| #include <zephyr/pm/device.h> |
| #include <zephyr/sys/util.h> |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(lp50xx, CONFIG_LED_LOG_LEVEL); |
| |
| #define LP50XX_MAX_BRIGHTNESS 100U |
| |
| /* |
| * Number of supported RGB led modules per chipset. |
| * |
| * For each module, there are 4 associated registers: |
| * - 1 brightness register |
| * - 3 color registers (RGB) |
| * |
| * A chipset can have more modules than leds. In this case, the |
| * associated registers will simply be inactive. |
| */ |
| #define LP5012_NUM_MODULES 4 |
| #define LP5024_NUM_MODULES 8 |
| #define LP5036_NUM_MODULES 12 |
| |
| /* Maximum number of channels */ |
| #define LP50XX_MAX_CHANNELS(nmodules) \ |
| ((LP50XX_COLORS_PER_LED + 1) * ((nmodules) + 1)) |
| |
| #define LP50XX_DISABLE_DELAY_US 3 |
| #define LP50XX_ENABLE_DELAY_US 500 |
| |
| /* Base registers */ |
| #define LP50XX_DEVICE_CONFIG0 0x00 |
| #define LP50XX_DEVICE_CONFIG1 0x01 |
| #define LP50XX_LED_CONFIG0 0x02 |
| |
| #define LP50XX_BANK_BASE(nmodules) \ |
| (0x03 + (((nmodules) - 1) / 8)) |
| |
| #define LP50XX_LED0_BRIGHTNESS(nmodules) \ |
| ((LP50XX_BANK_BASE(nmodules)) + 4) |
| |
| #define LP50XX_OUT0_COLOR(nmodules) \ |
| (LP50XX_LED0_BRIGHTNESS(nmodules) + (nmodules)) |
| |
| #define LP50XX_RESET(nmodules) \ |
| (LP50XX_OUT0_COLOR(nmodules) + LP50XX_COLORS_PER_LED * (nmodules)) |
| |
| /* Register values */ |
| #define CONFIG0_CHIP_EN BIT(6) |
| |
| #define CONFIG1_LED_GLOBAL_OFF BIT(0) |
| #define CONFIG1_MAX_CURRENT_OPT BIT(1) |
| #define CONFIG1_PWM_DITHERING_EN BIT(2) |
| #define CONFIG1_AUTO_INCR_EN BIT(3) |
| #define CONFIG1_POWER_SAVE_EN BIT(4) |
| #define CONFIG1_LOG_SCALE_EN BIT(5) |
| |
| #define RESET_SW 0xFF |
| |
| struct lp50xx_config { |
| struct i2c_dt_spec bus; |
| const struct gpio_dt_spec gpio_enable; |
| uint8_t num_modules; |
| uint8_t max_leds; |
| uint8_t num_leds; |
| bool log_scale_en; |
| bool max_curr_opt; |
| const struct led_info *leds_info; |
| }; |
| |
| struct lp50xx_data { |
| uint8_t *chan_buf; |
| }; |
| |
| static const struct led_info *lp50xx_led_to_info( |
| const struct lp50xx_config *config, uint32_t led) |
| { |
| if (led < config->num_leds) { |
| return &config->leds_info[led]; |
| } |
| |
| return NULL; |
| } |
| |
| static int lp50xx_get_info(const struct device *dev, uint32_t led, |
| const struct led_info **info) |
| { |
| const struct lp50xx_config *config = dev->config; |
| const struct led_info *led_info = lp50xx_led_to_info(config, led); |
| |
| if (!led_info) { |
| return -EINVAL; |
| } |
| |
| *info = led_info; |
| |
| return 0; |
| } |
| |
| static int lp50xx_set_brightness(const struct device *dev, |
| uint32_t led, uint8_t value) |
| { |
| const struct lp50xx_config *config = dev->config; |
| const struct led_info *led_info = lp50xx_led_to_info(config, led); |
| uint8_t buf[2]; |
| |
| if (!led_info) { |
| return -ENODEV; |
| } |
| |
| if (value > LP50XX_MAX_BRIGHTNESS) { |
| LOG_ERR("%s: brightness value out of bounds: val=%d, max=%d", |
| dev->name, value, LP50XX_MAX_BRIGHTNESS); |
| return -EINVAL; |
| } |
| |
| buf[0] = LP50XX_LED0_BRIGHTNESS(config->num_modules) + led_info->index; |
| buf[1] = (value * 0xff) / 100; |
| |
| return i2c_write_dt(&config->bus, buf, sizeof(buf)); |
| } |
| |
| static int lp50xx_on(const struct device *dev, uint32_t led) |
| { |
| return lp50xx_set_brightness(dev, led, 100); |
| } |
| |
| static int lp50xx_off(const struct device *dev, uint32_t led) |
| { |
| return lp50xx_set_brightness(dev, led, 0); |
| } |
| |
| static int lp50xx_set_color(const struct device *dev, uint32_t led, |
| uint8_t num_colors, const uint8_t *color) |
| { |
| const struct lp50xx_config *config = dev->config; |
| const struct led_info *led_info = lp50xx_led_to_info(config, led); |
| uint8_t buf[4]; |
| |
| if (!led_info) { |
| return -ENODEV; |
| } |
| |
| if (num_colors != led_info->num_colors) { |
| LOG_ERR("%s: invalid number of colors: got=%d, expected=%d", |
| dev->name, |
| num_colors, |
| led_info->num_colors); |
| return -EINVAL; |
| } |
| |
| buf[0] = LP50XX_OUT0_COLOR(config->num_modules); |
| buf[0] += LP50XX_COLORS_PER_LED * led_info->index; |
| |
| buf[1] = color[0]; |
| buf[2] = color[1]; |
| buf[3] = color[2]; |
| |
| return i2c_write_dt(&config->bus, buf, sizeof(buf)); |
| } |
| |
| static int lp50xx_write_channels(const struct device *dev, |
| uint32_t start_channel, |
| uint32_t num_channels, const uint8_t *buf) |
| { |
| const struct lp50xx_config *config = dev->config; |
| struct lp50xx_data *data = dev->data; |
| uint8_t base_channel, end_channel, max_channels; |
| |
| base_channel = LP50XX_BANK_BASE(config->num_modules); |
| end_channel = base_channel + start_channel + num_channels; |
| max_channels = base_channel + LP50XX_MAX_CHANNELS(config->num_modules); |
| |
| if (end_channel > max_channels) { |
| return -EINVAL; |
| } |
| |
| /* |
| * Unfortunately this controller doesn't support commands split into |
| * two I2C messages. |
| */ |
| data->chan_buf[0] = base_channel + start_channel; |
| memcpy(data->chan_buf + 1, buf, num_channels); |
| |
| return i2c_write_dt(&config->bus, data->chan_buf, num_channels + 1); |
| } |
| |
| static int lp50xx_reset(const struct device *dev) |
| { |
| const struct lp50xx_config *config = dev->config; |
| uint8_t buf[2]; |
| int err; |
| |
| /* Software reset */ |
| buf[0] = LP50XX_RESET(config->num_modules); |
| buf[1] = RESET_SW; |
| err = i2c_write_dt(&config->bus, buf, 2); |
| if (err < 0) { |
| return err; |
| } |
| |
| /* After reset, apply configuration since all registers are reset. */ |
| buf[0] = LP50XX_DEVICE_CONFIG1; |
| buf[1] = CONFIG1_PWM_DITHERING_EN | CONFIG1_AUTO_INCR_EN |
| | CONFIG1_POWER_SAVE_EN; |
| if (config->max_curr_opt) { |
| buf[1] |= CONFIG1_MAX_CURRENT_OPT; |
| } |
| if (config->log_scale_en) { |
| buf[1] |= CONFIG1_LOG_SCALE_EN; |
| } |
| |
| return i2c_write_dt(&config->bus, buf, 2); |
| } |
| |
| static int lp50xx_hw_enable(const struct device *dev, bool enable) |
| { |
| const struct lp50xx_config *config = dev->config; |
| int err; |
| |
| if (config->gpio_enable.port == NULL) { |
| /* Nothing to do */ |
| return 0; |
| } |
| |
| err = gpio_pin_set_dt(&config->gpio_enable, enable); |
| if (err < 0) { |
| LOG_ERR("%s: failed to set enable gpio", dev->name); |
| return err; |
| } |
| |
| k_usleep(enable ? LP50XX_ENABLE_DELAY_US : LP50XX_DISABLE_DELAY_US); |
| |
| return 0; |
| } |
| |
| static int lp50xx_enable(const struct device *dev, bool enable) |
| { |
| const struct lp50xx_config *config = dev->config; |
| uint8_t value = enable ? CONFIG0_CHIP_EN : 0; |
| |
| return i2c_reg_update_byte_dt(&config->bus, |
| LP50XX_DEVICE_CONFIG0, |
| CONFIG0_CHIP_EN, |
| value); |
| } |
| |
| static int lp50xx_init(const struct device *dev) |
| { |
| const struct lp50xx_config *config = dev->config; |
| int err; |
| |
| if (!i2c_is_ready_dt(&config->bus)) { |
| LOG_ERR("%s: I2C device not ready", dev->name); |
| return -ENODEV; |
| } |
| |
| if (config->num_leds > config->max_leds) { |
| LOG_ERR("%s: invalid number of LEDs %d (max %d)", |
| dev->name, |
| config->num_leds, |
| config->max_leds); |
| return -EINVAL; |
| } |
| |
| /* Configure GPIO if present */ |
| if (config->gpio_enable.port != NULL) { |
| if (!gpio_is_ready_dt(&config->gpio_enable)) { |
| LOG_ERR("%s: enable gpio is not ready", dev->name); |
| return -ENODEV; |
| } |
| |
| err = gpio_pin_configure_dt(&config->gpio_enable, |
| GPIO_OUTPUT_INACTIVE); |
| if (err < 0) { |
| LOG_ERR("%s: failed to initialize enable gpio", |
| dev->name); |
| return err; |
| } |
| } |
| |
| /* Enable hardware */ |
| err = lp50xx_hw_enable(dev, true); |
| if (err < 0) { |
| LOG_ERR("%s: failed to enable hardware", dev->name); |
| return err; |
| } |
| |
| /* Reset device */ |
| err = lp50xx_reset(dev); |
| if (err < 0) { |
| LOG_ERR("%s: failed to reset", dev->name); |
| return err; |
| } |
| |
| /* Enable device */ |
| err = lp50xx_enable(dev, true); |
| if (err < 0) { |
| LOG_ERR("%s: failed to enable", dev->name); |
| return err; |
| } |
| |
| return 0; |
| } |
| |
| #ifdef CONFIG_PM_DEVICE |
| static int lp50xx_pm_action(const struct device *dev, |
| enum pm_device_action action) |
| { |
| switch (action) { |
| case PM_DEVICE_ACTION_SUSPEND: |
| return lp50xx_enable(dev, false); |
| case PM_DEVICE_ACTION_RESUME: |
| return lp50xx_enable(dev, true); |
| default: |
| return -ENOTSUP; |
| } |
| |
| return 0; |
| } |
| #endif /* CONFIG_PM_DEVICE */ |
| |
| static const struct led_driver_api lp50xx_led_api = { |
| .on = lp50xx_on, |
| .off = lp50xx_off, |
| .get_info = lp50xx_get_info, |
| .set_brightness = lp50xx_set_brightness, |
| .set_color = lp50xx_set_color, |
| .write_channels = lp50xx_write_channels, |
| }; |
| |
| #define COLOR_MAPPING(led_node_id) \ |
| const uint8_t color_mapping_##led_node_id[] = \ |
| DT_PROP(led_node_id, color_mapping); |
| |
| #define LED_INFO(led_node_id) \ |
| { \ |
| .label = DT_PROP(led_node_id, label), \ |
| .index = DT_PROP(led_node_id, index), \ |
| .num_colors = \ |
| DT_PROP_LEN(led_node_id, color_mapping), \ |
| .color_mapping = color_mapping_##led_node_id, \ |
| }, |
| |
| #define LP50XX_DEVICE(n, id, nmodules) \ |
| DT_INST_FOREACH_CHILD(n, COLOR_MAPPING) \ |
| \ |
| static const struct led_info lp##id##_leds_##n[] = { \ |
| DT_INST_FOREACH_CHILD(n, LED_INFO) \ |
| }; \ |
| \ |
| static const struct lp50xx_config lp##id##_config_##n = { \ |
| .bus = I2C_DT_SPEC_INST_GET(n), \ |
| .gpio_enable = \ |
| GPIO_DT_SPEC_INST_GET_OR(n, enable_gpios, {0}), \ |
| .num_modules = nmodules, \ |
| .max_leds = LP##id##_MAX_LEDS, \ |
| .num_leds = ARRAY_SIZE(lp##id##_leds_##n), \ |
| .log_scale_en = DT_INST_PROP(n, log_scale_en), \ |
| .max_curr_opt = DT_INST_PROP(n, max_curr_opt), \ |
| .leds_info = lp##id##_leds_##n, \ |
| }; \ |
| \ |
| static uint8_t lp##id##_chan_buf_##n[LP50XX_MAX_CHANNELS(nmodules) + 1];\ |
| \ |
| static struct lp50xx_data lp##id##_data_##n = { \ |
| .chan_buf = lp##id##_chan_buf_##n, \ |
| }; \ |
| \ |
| PM_DEVICE_DT_INST_DEFINE(n, lp50xx_pm_action); \ |
| \ |
| DEVICE_DT_INST_DEFINE(n, \ |
| lp50xx_init, \ |
| PM_DEVICE_DT_INST_GET(n), \ |
| &lp##id##_data_##n, \ |
| &lp##id##_config_##n, \ |
| POST_KERNEL, CONFIG_LED_INIT_PRIORITY, \ |
| &lp50xx_led_api); |
| |
| #undef DT_DRV_COMPAT |
| #define DT_DRV_COMPAT ti_lp5009 |
| DT_INST_FOREACH_STATUS_OKAY_VARGS(LP50XX_DEVICE, 5009, LP5012_NUM_MODULES) |
| |
| #undef DT_DRV_COMPAT |
| #define DT_DRV_COMPAT ti_lp5012 |
| DT_INST_FOREACH_STATUS_OKAY_VARGS(LP50XX_DEVICE, 5012, LP5012_NUM_MODULES) |
| |
| #undef DT_DRV_COMPAT |
| #define DT_DRV_COMPAT ti_lp5018 |
| DT_INST_FOREACH_STATUS_OKAY_VARGS(LP50XX_DEVICE, 5018, LP5024_NUM_MODULES) |
| |
| #undef DT_DRV_COMPAT |
| #define DT_DRV_COMPAT ti_lp5024 |
| DT_INST_FOREACH_STATUS_OKAY_VARGS(LP50XX_DEVICE, 5024, LP5024_NUM_MODULES) |
| |
| #undef DT_DRV_COMPAT |
| #define DT_DRV_COMPAT ti_lp5030 |
| DT_INST_FOREACH_STATUS_OKAY_VARGS(LP50XX_DEVICE, 5030, LP5036_NUM_MODULES) |
| |
| #undef DT_DRV_COMPAT |
| #define DT_DRV_COMPAT ti_lp5036 |
| DT_INST_FOREACH_STATUS_OKAY_VARGS(LP50XX_DEVICE, 5036, LP5036_NUM_MODULES) |