zephyr/drivers/led/lp50xx.c
Mathieu Anquetin ce674d9098 drivers: led: lp50xx: fix led index
The led identifer should refer to devicetree ordering, not to the index
used by the controller. This way, it will be possible to deactivate
some leds or to reorganize the indexing if necessary.

Signed-off-by: Mathieu Anquetin <mathieu.anquetin@groupe-cahors.com>
2023-08-04 13:18:01 +02:00

426 lines
10 KiB
C

/*
* 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_MIN_BRIGHTNESS 0U
#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 (!IN_RANGE(value, LP50XX_MIN_BRIGHTNESS, LP50XX_MAX_BRIGHTNESS)) {
LOG_ERR("%s: brightness value out of bounds: "
"val=%d, min=%d, max=%d",
dev->name,
value,
LP50XX_MIN_BRIGHTNESS,
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)