From 101bc85a19436825e8c94a27e792cd9750aad708 Mon Sep 17 00:00:00 2001 From: Vivek BalachandharTN Date: Mon, 1 Dec 2025 04:26:12 +0000 Subject: leds: lm3692x: Fix kernel-doc for struct lm3692x_led Building with W=1 reports several kernel-doc warnings in drivers/leds/leds-lm3692x.c: Warning: leds-lm3692x.c:122 struct member 'boost_ctrl' not described in 'lm3692x_led' Warning: leds-lm3692x.c:122 struct member 'brightness_ctrl' not described in 'lm3692x_led' Warning: leds-lm3692x.c:122 struct member 'enabled' not described in 'lm3692x_led' These fields were added to struct lm3692x_led but the corresponding kernel-doc comment was not updated. Convert the kernel-doc block to use the "@member: description" style consistently and document the boost_ctrl, brightness_ctrl and enabled fields. This keeps the documentation in sync with the implementation and silences the W=1 warnings. Signed-off-by: Vivek BalachandharTN Link: https://patch.msgid.link/20251201042612.2099239-1-vivek.balachandhar@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-lm3692x.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'drivers') diff --git a/drivers/leds/leds-lm3692x.c b/drivers/leds/leds-lm3692x.c index c319ff4d70b2..1d64ceb5ac85 100644 --- a/drivers/leds/leds-lm3692x.c +++ b/drivers/leds/leds-lm3692x.c @@ -104,6 +104,9 @@ * @regulator: LED supply regulator pointer * @led_enable: LED sync to be enabled * @model_id: Current device model ID enumerated + * @boost_ctrl: Cached configuration for the boost control register + * @brightness_ctrl: Cached configuration for brightness/brightness control + * @enabled: Cached enable state of the device */ struct lm3692x_led { struct mutex lock; -- cgit v1.2.3 From f4b830a5371914239756b0599e5dc9d4c328e387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duje=20Mihanovi=C4=87?= Date: Wed, 17 Dec 2025 19:14:23 +0100 Subject: leds: expresswire: Fix chip state breakage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is possible to put the KTD2801 chip in an unknown/undefined state by changing the brightness very rapidly (for example, with a brightness slider). When this happens, the brightness is stuck on max and cannot be changed until the chip is power cycled. Fix this by disabling interrupts while talking to the chip. While at it, make expresswire_power_off() use fsleep() and also unexport some functions meant to be internal. Fixes: 1368d06dd2c9 ("leds: Introduce ExpressWire library") Tested-by: Karel Balej Signed-off-by: Duje Mihanović Link: https://patch.msgid.link/20251217-expresswire-fix-v2-1-4a02b10acd96@dujemihanovic.xyz Signed-off-by: Lee Jones --- drivers/leds/leds-expresswire.c | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'drivers') diff --git a/drivers/leds/leds-expresswire.c b/drivers/leds/leds-expresswire.c index bb69be228a6d..25c6b159a6ee 100644 --- a/drivers/leds/leds-expresswire.c +++ b/drivers/leds/leds-expresswire.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -16,37 +17,41 @@ void expresswire_power_off(struct expresswire_common_props *props) { gpiod_set_value_cansleep(props->ctrl_gpio, 0); - usleep_range(props->timing.poweroff_us, props->timing.poweroff_us * 2); + fsleep(props->timing.poweroff_us); } EXPORT_SYMBOL_NS_GPL(expresswire_power_off, "EXPRESSWIRE"); void expresswire_enable(struct expresswire_common_props *props) { + unsigned long flags; + + local_irq_save(flags); + gpiod_set_value(props->ctrl_gpio, 1); udelay(props->timing.detect_delay_us); gpiod_set_value(props->ctrl_gpio, 0); udelay(props->timing.detect_us); gpiod_set_value(props->ctrl_gpio, 1); + + local_irq_restore(flags); } EXPORT_SYMBOL_NS_GPL(expresswire_enable, "EXPRESSWIRE"); -void expresswire_start(struct expresswire_common_props *props) +static void expresswire_start(struct expresswire_common_props *props) { gpiod_set_value(props->ctrl_gpio, 1); udelay(props->timing.data_start_us); } -EXPORT_SYMBOL_NS_GPL(expresswire_start, "EXPRESSWIRE"); -void expresswire_end(struct expresswire_common_props *props) +static void expresswire_end(struct expresswire_common_props *props) { gpiod_set_value(props->ctrl_gpio, 0); udelay(props->timing.end_of_data_low_us); gpiod_set_value(props->ctrl_gpio, 1); udelay(props->timing.end_of_data_high_us); } -EXPORT_SYMBOL_NS_GPL(expresswire_end, "EXPRESSWIRE"); -void expresswire_set_bit(struct expresswire_common_props *props, bool bit) +static void expresswire_set_bit(struct expresswire_common_props *props, bool bit) { if (bit) { gpiod_set_value(props->ctrl_gpio, 0); @@ -60,13 +65,18 @@ void expresswire_set_bit(struct expresswire_common_props *props, bool bit) udelay(props->timing.short_bitset_us); } } -EXPORT_SYMBOL_NS_GPL(expresswire_set_bit, "EXPRESSWIRE"); void expresswire_write_u8(struct expresswire_common_props *props, u8 val) { + unsigned long flags; + + local_irq_save(flags); + expresswire_start(props); for (int i = 7; i >= 0; i--) expresswire_set_bit(props, val & BIT(i)); expresswire_end(props); + + local_irq_restore(flags); } EXPORT_SYMBOL_NS_GPL(expresswire_write_u8, "EXPRESSWIRE"); -- cgit v1.2.3 From 6f1bc4534f2144c8b1acc448b3c4441a6096095c Mon Sep 17 00:00:00 2001 From: Daniel Mack Date: Fri, 19 Dec 2025 16:45:20 +0100 Subject: leds: is31f132xx: Re-order code to remove forward declarations Move the chipdef structs after the functions they reference so that forward declarations become unnecessary. Signed-off-by: Daniel Mack Link: https://patch.msgid.link/20251219154521.643312-3-daniel@zonque.org Signed-off-by: Lee Jones --- drivers/leds/leds-is31fl32xx.c | 128 ++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 66 deletions(-) (limited to 'drivers') diff --git a/drivers/leds/leds-is31fl32xx.c b/drivers/leds/leds-is31fl32xx.c index dc9349f9d350..bd5323f2316e 100644 --- a/drivers/leds/leds-is31fl32xx.c +++ b/drivers/leds/leds-is31fl32xx.c @@ -88,72 +88,6 @@ struct is31fl32xx_chipdef { int (*sw_shutdown_func)(struct is31fl32xx_priv *priv, bool enable); }; -static const struct is31fl32xx_chipdef is31fl3236_cdef = { - .channels = 36, - .shutdown_reg = 0x00, - .pwm_update_reg = 0x25, - .global_control_reg = 0x4a, - .reset_reg = 0x4f, - .output_frequency_setting_reg = IS31FL32XX_REG_NONE, - .pwm_register_base = 0x01, - .led_control_register_base = 0x26, - .enable_bits_per_led_control_register = 1, -}; - -static const struct is31fl32xx_chipdef is31fl3236a_cdef = { - .channels = 36, - .shutdown_reg = 0x00, - .pwm_update_reg = 0x25, - .global_control_reg = 0x4a, - .reset_reg = 0x4f, - .output_frequency_setting_reg = 0x4b, - .pwm_register_base = 0x01, - .led_control_register_base = 0x26, - .enable_bits_per_led_control_register = 1, -}; - -static const struct is31fl32xx_chipdef is31fl3235_cdef = { - .channels = 28, - .shutdown_reg = 0x00, - .pwm_update_reg = 0x25, - .global_control_reg = 0x4a, - .reset_reg = 0x4f, - .output_frequency_setting_reg = IS31FL32XX_REG_NONE, - .pwm_register_base = 0x05, - .led_control_register_base = 0x2a, - .enable_bits_per_led_control_register = 1, -}; - -static const struct is31fl32xx_chipdef is31fl3218_cdef = { - .channels = 18, - .shutdown_reg = 0x00, - .pwm_update_reg = 0x16, - .global_control_reg = IS31FL32XX_REG_NONE, - .reset_reg = 0x17, - .output_frequency_setting_reg = IS31FL32XX_REG_NONE, - .pwm_register_base = 0x01, - .led_control_register_base = 0x13, - .enable_bits_per_led_control_register = 6, -}; - -static int is31fl3216_reset(struct is31fl32xx_priv *priv); -static int is31fl3216_software_shutdown(struct is31fl32xx_priv *priv, - bool enable); -static const struct is31fl32xx_chipdef is31fl3216_cdef = { - .channels = 16, - .shutdown_reg = IS31FL32XX_REG_NONE, - .pwm_update_reg = 0xB0, - .global_control_reg = IS31FL32XX_REG_NONE, - .reset_reg = IS31FL32XX_REG_NONE, - .output_frequency_setting_reg = IS31FL32XX_REG_NONE, - .pwm_register_base = 0x10, - .pwm_registers_reversed = true, - .led_control_register_base = 0x01, - .enable_bits_per_led_control_register = 8, - .reset_func = is31fl3216_reset, - .sw_shutdown_func = is31fl3216_software_shutdown, -}; - static int is31fl32xx_write(struct is31fl32xx_priv *priv, u8 reg, u8 val) { int ret; @@ -435,6 +369,68 @@ static int is31fl32xx_parse_dt(struct device *dev, return 0; } +static const struct is31fl32xx_chipdef is31fl3236_cdef = { + .channels = 36, + .shutdown_reg = 0x00, + .pwm_update_reg = 0x25, + .global_control_reg = 0x4a, + .reset_reg = 0x4f, + .output_frequency_setting_reg = IS31FL32XX_REG_NONE, + .pwm_register_base = 0x01, + .led_control_register_base = 0x26, + .enable_bits_per_led_control_register = 1, +}; + +static const struct is31fl32xx_chipdef is31fl3236a_cdef = { + .channels = 36, + .shutdown_reg = 0x00, + .pwm_update_reg = 0x25, + .global_control_reg = 0x4a, + .reset_reg = 0x4f, + .output_frequency_setting_reg = 0x4b, + .pwm_register_base = 0x01, + .led_control_register_base = 0x26, + .enable_bits_per_led_control_register = 1, +}; + +static const struct is31fl32xx_chipdef is31fl3235_cdef = { + .channels = 28, + .shutdown_reg = 0x00, + .pwm_update_reg = 0x25, + .global_control_reg = 0x4a, + .reset_reg = 0x4f, + .output_frequency_setting_reg = IS31FL32XX_REG_NONE, + .pwm_register_base = 0x05, + .led_control_register_base = 0x2a, + .enable_bits_per_led_control_register = 1, +}; + +static const struct is31fl32xx_chipdef is31fl3218_cdef = { + .channels = 18, + .shutdown_reg = 0x00, + .pwm_update_reg = 0x16, + .global_control_reg = IS31FL32XX_REG_NONE, + .reset_reg = 0x17, + .output_frequency_setting_reg = IS31FL32XX_REG_NONE, + .pwm_register_base = 0x01, + .led_control_register_base = 0x13, + .enable_bits_per_led_control_register = 6, +}; + +static const struct is31fl32xx_chipdef is31fl3216_cdef = { + .channels = 16, + .shutdown_reg = IS31FL32XX_REG_NONE, + .pwm_update_reg = 0xB0, + .global_control_reg = IS31FL32XX_REG_NONE, + .reset_reg = IS31FL32XX_REG_NONE, + .output_frequency_setting_reg = IS31FL32XX_REG_NONE, + .pwm_register_base = 0x10, + .pwm_registers_reversed = true, + .led_control_register_base = 0x01, + .enable_bits_per_led_control_register = 8, + .reset_func = is31fl3216_reset, + .sw_shutdown_func = is31fl3216_software_shutdown, +}; static const struct of_device_id of_is31fl32xx_match[] = { { .compatible = "issi,is31fl3236", .data = &is31fl3236_cdef, }, -- cgit v1.2.3 From a18983b95a61b093847b3ec37a5ac9c29ff6257d Mon Sep 17 00:00:00 2001 From: Daniel Mack Date: Fri, 19 Dec 2025 16:45:21 +0100 Subject: leds: is31f132xx: Add support for is31fl3293 This chip supports 3 LED channels with 4096 possible PWM values. Extend the driver to support this variant: * Make brightness steps configurable per device type * Handle dual-register brightness updates * Allow to specify values to write into the PWM update register * Add custom init and shutdown function for 3293 variant * Init registers after parsing DT properties Signed-off-by: Daniel Mack Link: https://patch.msgid.link/20251219154521.643312-4-daniel@zonque.org Signed-off-by: Lee Jones --- drivers/leds/leds-is31fl32xx.c | 138 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 8 deletions(-) (limited to 'drivers') diff --git a/drivers/leds/leds-is31fl32xx.c b/drivers/leds/leds-is31fl32xx.c index bd5323f2316e..fe07acbb103a 100644 --- a/drivers/leds/leds-is31fl32xx.c +++ b/drivers/leds/leds-is31fl32xx.c @@ -34,10 +34,26 @@ #define IS31FL32XX_PWM_FREQUENCY_22KHZ 0x01 +/* Registers for IS31FL3293 */ +#define IS31FL3293_SHUTDOWN_REG 0x01 +#define IS31FL3293_SHUTDOWN_SSD_DISABLE BIT(0) +#define IS31FL3293_SHUTDOWN_EN1 BIT(4) +#define IS31FL3293_SHUTDOWN_EN2 BIT(5) +#define IS31FL3293_SHUTDOWN_EN3 BIT(6) +#define IS31FL3293_GCC_REG 0x03 +#define IS31FL3293_GCC_LEVEL_MAX 0x3f +#define IS31FL3293_CL_REG 0x10 +#define IS31FL3293_COLOR_UPDATE_REG 0x27 +#define IS31FL3293_COLOR_UPDATE_MAGIC 0xc5 +#define IS31FL3293_RESET_REG 0x3c +#define IS31FL3293_RESET_MAGIC 0xc5 +#define IS31FL3293_MAX_MICROAMP 20000 + struct is31fl32xx_priv; struct is31fl32xx_led_data { struct led_classdev cdev; u8 channel; /* 1-based, max priv->cdef->channels */ + u32 max_microamp; struct is31fl32xx_priv *priv; }; @@ -53,6 +69,7 @@ struct is31fl32xx_priv { * @channels : Number of LED channels * @shutdown_reg : address of Shutdown register (optional) * @pwm_update_reg : address of PWM Update register + * @pwm_update_value : value to write to PWM Update register * @global_control_reg : address of Global Control register (optional) * @reset_reg : address of Reset register (optional) * @output_frequency_setting_reg: address of output frequency register (optional) @@ -60,6 +77,7 @@ struct is31fl32xx_priv { * @pwm_registers_reversed: : true if PWM registers count down instead of up * @led_control_register_base : address of first LED control register (optional) * @enable_bits_per_led_control_register: number of LEDs enable bits in each + * @brightness_steps : number of brightness steps supported by the chip * @reset_func : pointer to reset function * @sw_shutdown_func : pointer to software shutdown function * @@ -77,6 +95,7 @@ struct is31fl32xx_chipdef { u8 channels; u8 shutdown_reg; u8 pwm_update_reg; + u8 pwm_update_value; u8 global_control_reg; u8 reset_reg; u8 output_frequency_setting_reg; @@ -84,6 +103,7 @@ struct is31fl32xx_chipdef { bool pwm_registers_reversed; u8 led_control_register_base; u8 enable_bits_per_led_control_register; + u16 brightness_steps; int (*reset_func)(struct is31fl32xx_priv *priv); int (*sw_shutdown_func)(struct is31fl32xx_priv *priv, bool enable); }; @@ -152,6 +172,62 @@ static int is31fl3216_software_shutdown(struct is31fl32xx_priv *priv, return is31fl32xx_write(priv, IS31FL3216_CONFIG_REG, value); } +/* + * Custom Reset function for IS31FL3293. We need to set the global current limit + * and write to the color update register once. + */ +static int is31fl3293_reset(struct is31fl32xx_priv *priv) +{ + int i, ret; + + ret = is31fl32xx_write(priv, IS31FL3293_RESET_REG, + IS31FL3293_RESET_MAGIC); + if (ret) + return ret; + + /* Set the global current limit to maximum */ + ret = is31fl32xx_write(priv, IS31FL3293_GCC_REG, + IS31FL3293_GCC_LEVEL_MAX); + if (ret) + return ret; + + for (i = 0; i < priv->num_leds; i++) { + struct is31fl32xx_led_data *led_data = &priv->leds[i]; + int current_level_reg = IS31FL3293_CL_REG + led_data->channel - 1; + int microamp = max(led_data->max_microamp, IS31FL3293_MAX_MICROAMP); + int current_level = (microamp * 0xff) / IS31FL3293_MAX_MICROAMP; + + ret = is31fl32xx_write(priv, current_level_reg, current_level); + if (ret) + return ret; + } + + ret = is31fl32xx_write(priv, IS31FL3293_COLOR_UPDATE_REG, + IS31FL3293_COLOR_UPDATE_MAGIC); + if (ret) + return ret; + + return 0; +} + +/* + * Custom Software-Shutdown function for IS31FL3293 because the SHUTDOWN + * register of this device also has bits to enable the channels. + */ +static int is31fl3293_software_shutdown(struct is31fl32xx_priv *priv, + bool enable) +{ + u8 value = 0; + + if (!enable) + value = IS31FL3293_SHUTDOWN_SSD_DISABLE | + IS31FL3293_SHUTDOWN_EN1 | + IS31FL3293_SHUTDOWN_EN2 | + IS31FL3293_SHUTDOWN_EN3; + + return is31fl32xx_write(priv, IS31FL3293_SHUTDOWN_REG, value); +} + /* * NOTE: A mutex is not needed in this function because: * - All referenced data is read-only after probe() @@ -190,13 +266,36 @@ static int is31fl32xx_brightness_set(struct led_classdev *led_cdev, else pwm_register_offset = led_data->channel - 1; - ret = is31fl32xx_write(led_data->priv, - cdef->pwm_register_base + pwm_register_offset, - brightness); - if (ret) - return ret; + switch (cdef->brightness_steps) { + case 256: + ret = is31fl32xx_write(led_data->priv, + cdef->pwm_register_base + pwm_register_offset, + brightness); + if (ret) + return ret; + + break; + case 4096: + /* IS31FL329x devices use two registers to store 12 bits of brightness */ + pwm_register_offset *= 2; - return is31fl32xx_write(led_data->priv, cdef->pwm_update_reg, 0); + ret = is31fl32xx_write(led_data->priv, + cdef->pwm_register_base + pwm_register_offset, + brightness & 0xff); + if (ret) + return ret; + + ret = is31fl32xx_write(led_data->priv, + cdef->pwm_register_base + pwm_register_offset + 1, + (brightness >> 8) & 0xf); + if (ret) + return ret; + + break; + } + + return is31fl32xx_write(led_data->priv, cdef->pwm_update_reg, + cdef->pwm_update_value); } static int is31fl32xx_reset_regs(struct is31fl32xx_priv *priv) @@ -295,6 +394,8 @@ static int is31fl32xx_parse_child_dt(const struct device *dev, } led_data->channel = reg; + of_property_read_u32(child, "led-max-microamp", &led_data->max_microamp); + cdev->brightness_set_blocking = is31fl32xx_brightness_set; return 0; @@ -339,6 +440,7 @@ static int is31fl32xx_parse_dt(struct device *dev, const struct is31fl32xx_led_data *other_led_data; led_data->priv = priv; + led_data->cdev.max_brightness = priv->cdef->brightness_steps - 1; ret = is31fl32xx_parse_child_dt(dev, child, led_data); if (ret) @@ -391,6 +493,7 @@ static const struct is31fl32xx_chipdef is31fl3236a_cdef = { .pwm_register_base = 0x01, .led_control_register_base = 0x26, .enable_bits_per_led_control_register = 1, + .brightness_steps = 256, }; static const struct is31fl32xx_chipdef is31fl3235_cdef = { @@ -403,6 +506,7 @@ static const struct is31fl32xx_chipdef is31fl3235_cdef = { .pwm_register_base = 0x05, .led_control_register_base = 0x2a, .enable_bits_per_led_control_register = 1, + .brightness_steps = 256, }; static const struct is31fl32xx_chipdef is31fl3218_cdef = { @@ -415,6 +519,7 @@ static const struct is31fl32xx_chipdef is31fl3218_cdef = { .pwm_register_base = 0x01, .led_control_register_base = 0x13, .enable_bits_per_led_control_register = 6, + .brightness_steps = 256, }; static const struct is31fl32xx_chipdef is31fl3216_cdef = { @@ -430,9 +535,25 @@ static const struct is31fl32xx_chipdef is31fl3216_cdef = { .enable_bits_per_led_control_register = 8, .reset_func = is31fl3216_reset, .sw_shutdown_func = is31fl3216_software_shutdown, + .brightness_steps = 256, +}; + +static const struct is31fl32xx_chipdef is31fl3293_cdef = { + .channels = 3, + .shutdown_reg = IS31FL32XX_REG_NONE, + .pwm_update_reg = 0x28, + .pwm_update_value = 0xc5, + .global_control_reg = IS31FL32XX_REG_NONE, + .reset_reg = IS31FL32XX_REG_NONE, + .pwm_register_base = 0x19, + .led_control_register_base = IS31FL32XX_REG_NONE, + .brightness_steps = 4096, + .reset_func = is31fl3293_reset, + .sw_shutdown_func = is31fl3293_software_shutdown, }; static const struct of_device_id of_is31fl32xx_match[] = { + { .compatible = "issi,is31fl3293", .data = &is31fl3293_cdef, }, { .compatible = "issi,is31fl3236", .data = &is31fl3236_cdef, }, { .compatible = "issi,is31fl3236a", .data = &is31fl3236a_cdef, }, { .compatible = "issi,is31fl3235", .data = &is31fl3235_cdef, }, @@ -468,11 +589,11 @@ static int is31fl32xx_probe(struct i2c_client *client) priv->cdef = cdef; i2c_set_clientdata(client, priv); - ret = is31fl32xx_init_regs(priv); + ret = is31fl32xx_parse_dt(dev, priv); if (ret) return ret; - ret = is31fl32xx_parse_dt(dev, priv); + ret = is31fl32xx_init_regs(priv); if (ret) return ret; @@ -495,6 +616,7 @@ static void is31fl32xx_remove(struct i2c_client *client) * even though it is not used for DeviceTree based instantiation. */ static const struct i2c_device_id is31fl32xx_id[] = { + { "is31fl3293" }, { "is31fl3236" }, { "is31fl3236a" }, { "is31fl3235" }, -- cgit v1.2.3 From 207a693835d48f92994fc23d8c3e4f131acbb73a Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Wed, 24 Dec 2025 13:45:22 +0100 Subject: leds: lp55xx: Simplify with scoped for each OF child loop Use scoped for-each loop when iterating over device nodes to make code a bit simpler. Signed-off-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20251224124521.208635-2-krzysztof.kozlowski@oss.qualcomm.com Signed-off-by: Lee Jones --- drivers/leds/leds-lp55xx-common.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'drivers') diff --git a/drivers/leds/leds-lp55xx-common.c b/drivers/leds/leds-lp55xx-common.c index fd447eb7eb15..ea131177de96 100644 --- a/drivers/leds/leds-lp55xx-common.c +++ b/drivers/leds/leds-lp55xx-common.c @@ -1204,7 +1204,6 @@ static struct lp55xx_platform_data *lp55xx_of_populate_pdata(struct device *dev, struct device_node *np, struct lp55xx_chip *chip) { - struct device_node *child; struct lp55xx_platform_data *pdata; struct lp55xx_led_config *cfg; int num_channels; @@ -1229,12 +1228,10 @@ static struct lp55xx_platform_data *lp55xx_of_populate_pdata(struct device *dev, pdata->num_channels = num_channels; cfg->max_channel = chip->cfg->max_channel; - for_each_available_child_of_node(np, child) { + for_each_available_child_of_node_scoped(np, child) { ret = lp55xx_parse_logical_led(child, cfg, i); - if (ret) { - of_node_put(child); + if (ret) return ERR_PTR(-EINVAL); - } i++; } -- cgit v1.2.3 From f42033b5ce8c79c5db645916c9a72ee3e10cecfa Mon Sep 17 00:00:00 2001 From: Haotian Zhang Date: Fri, 9 Jan 2026 01:51:33 +0800 Subject: leds: qcom-lpg: Check the return value of regmap_bulk_write() The lpg_lut_store() function currently ignores the return value of regmap_bulk_write() and always returns 0. This can cause hardware write failures to go undetected, leading the caller to believe LUT programming succeeded when it may have failed. Check the return value of regmap_bulk_write() in lpg_lut_store and return the error to the caller on failure. Fixes: 24e2d05d1b68 ("leds: Add driver for Qualcomm LPG") Signed-off-by: Haotian Zhang Link: https://patch.msgid.link/20260108175133.638-1-vulab@iscas.ac.cn Signed-off-by: Lee Jones --- drivers/leds/rgb/leds-qcom-lpg.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'drivers') diff --git a/drivers/leds/rgb/leds-qcom-lpg.c b/drivers/leds/rgb/leds-qcom-lpg.c index 72da0bf469ad..f54851dfb42f 100644 --- a/drivers/leds/rgb/leds-qcom-lpg.c +++ b/drivers/leds/rgb/leds-qcom-lpg.c @@ -369,7 +369,7 @@ static int lpg_lut_store(struct lpg *lpg, struct led_pattern *pattern, { unsigned int idx; u16 val; - int i; + int i, ret; idx = bitmap_find_next_zero_area(lpg->lut_bitmap, lpg->lut_size, 0, len, 0); @@ -379,8 +379,10 @@ static int lpg_lut_store(struct lpg *lpg, struct led_pattern *pattern, for (i = 0; i < len; i++) { val = pattern[i].brightness; - regmap_bulk_write(lpg->map, lpg->lut_base + LPG_LUT_REG(idx + i), - &val, sizeof(val)); + ret = regmap_bulk_write(lpg->map, lpg->lut_base + LPG_LUT_REG(idx + i), + &val, sizeof(val)); + if (ret) + return ret; } bitmap_set(lpg->lut_bitmap, idx, len); -- cgit v1.2.3 From a0309dc699bc6434e3d269539e346f91f17036b8 Mon Sep 17 00:00:00 2001 From: Nam Tran Date: Thu, 15 Jan 2026 23:10:12 +0700 Subject: leds: Add basic support for TI/National Semiconductor LP5812 LED Driver The LP5812 is a 4x3 matrix RGB LED driver with an autonomous animation engine and time-cross-multiplexing (TCM) support for up to 12 LEDs or 4 RGB LEDs. Each LED can be configured through the related registers to realize vivid and fancy lighting effects. This patch adds minimal driver support for the LP5812, implementing only the essential functionality: I2C communication with the device, LED registration, brightness control in manual mode, and basic sysfs interfaces for LED configuration and fault monitoring. Signed-off-by: Nam Tran Link: https://patch.msgid.link/20260115161013.40706-2-trannamatk@gmail.com Signed-off-by: Lee Jones --- drivers/leds/rgb/Kconfig | 13 + drivers/leds/rgb/Makefile | 1 + drivers/leds/rgb/leds-lp5812.c | 642 +++++++++++++++++++++++++++++++++++++++++ drivers/leds/rgb/leds-lp5812.h | 172 +++++++++++ 4 files changed, 828 insertions(+) create mode 100644 drivers/leds/rgb/leds-lp5812.c create mode 100644 drivers/leds/rgb/leds-lp5812.h (limited to 'drivers') diff --git a/drivers/leds/rgb/Kconfig b/drivers/leds/rgb/Kconfig index 222d943d826a..28ef4c487367 100644 --- a/drivers/leds/rgb/Kconfig +++ b/drivers/leds/rgb/Kconfig @@ -26,6 +26,19 @@ config LEDS_KTD202X To compile this driver as a module, choose M here: the module will be called leds-ktd202x. +config LEDS_LP5812 + tristate "LED support for Texas Instruments LP5812" + depends on I2C + help + If you say Y here you get support for TI LP5812 LED driver. + The LP5812 is a 4x3 matrix RGB LED driver with autonomous + animation engine control. + + To compile this driver as a module, choose M here: the + module will be called leds-lp5812. + + If unsure, say N. + config LEDS_NCP5623 tristate "LED support for NCP5623" depends on I2C diff --git a/drivers/leds/rgb/Makefile b/drivers/leds/rgb/Makefile index a501fd27f179..be45991f63f5 100644 --- a/drivers/leds/rgb/Makefile +++ b/drivers/leds/rgb/Makefile @@ -2,6 +2,7 @@ obj-$(CONFIG_LEDS_GROUP_MULTICOLOR) += leds-group-multicolor.o obj-$(CONFIG_LEDS_KTD202X) += leds-ktd202x.o +obj-$(CONFIG_LEDS_LP5812) += leds-lp5812.o obj-$(CONFIG_LEDS_NCP5623) += leds-ncp5623.o obj-$(CONFIG_LEDS_PWM_MULTICOLOR) += leds-pwm-multicolor.o obj-$(CONFIG_LEDS_QCOM_LPG) += leds-qcom-lpg.o diff --git a/drivers/leds/rgb/leds-lp5812.c b/drivers/leds/rgb/leds-lp5812.c new file mode 100644 index 000000000000..ce6d703641e8 --- /dev/null +++ b/drivers/leds/rgb/leds-lp5812.c @@ -0,0 +1,642 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LP5812 LED driver + * + * Copyright (C) 2025 Texas Instruments + * + * Author: Jared Zhou + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "leds-lp5812.h" + +static const struct lp5812_mode_mapping chip_mode_map[] = { + {"direct_mode", 0, 0, 0, 0, 0, 0}, + {"tcm:1:0", 1, 0, 0, 0, 0, 0}, + {"tcm:1:1", 1, 1, 0, 0, 0, 0}, + {"tcm:1:2", 1, 2, 0, 0, 0, 0}, + {"tcm:1:3", 1, 3, 0, 0, 0, 0}, + {"tcm:2:0:1", 2, 0, 1, 0, 0, 0}, + {"tcm:2:0:2", 2, 0, 2, 0, 0, 0}, + {"tcm:2:0:3", 2, 0, 3, 0, 0, 0}, + {"tcm:2:1:2", 2, 1, 2, 0, 0, 0}, + {"tcm:2:1:3", 2, 1, 3, 0, 0, 0}, + {"tcm:2:2:3", 2, 2, 3, 0, 0, 0}, + {"tcm:3:0:1:2", 3, 0, 1, 2, 0, 0}, + {"tcm:3:0:1:3", 3, 0, 1, 3, 0, 0}, + {"tcm:3:0:2:3", 3, 0, 2, 3, 0, 0}, + {"tcm:4:0:1:2:3", 4, 0, 1, 2, 3, 0}, + {"mix:1:0:1", 5, 1, 0, 0, 0, 0}, + {"mix:1:0:2", 5, 2, 0, 0, 0, 0}, + {"mix:1:0:3", 5, 3, 0, 0, 0, 0}, + {"mix:1:1:0", 5, 0, 0, 0, 0, 1}, + {"mix:1:1:2", 5, 2, 0, 0, 0, 1}, + {"mix:1:1:3", 5, 3, 0, 0, 0, 1}, + {"mix:1:2:0", 5, 0, 0, 0, 0, 2}, + {"mix:1:2:1", 5, 1, 0, 0, 0, 2}, + {"mix:1:2:3", 5, 3, 0, 0, 0, 2}, + {"mix:1:3:0", 5, 0, 0, 0, 0, 3}, + {"mix:1:3:1", 5, 1, 0, 0, 0, 3}, + {"mix:1:3:2", 5, 2, 0, 0, 0, 3}, + {"mix:2:0:1:2", 6, 1, 2, 0, 0, 0}, + {"mix:2:0:1:3", 6, 1, 3, 0, 0, 0}, + {"mix:2:0:2:3", 6, 2, 3, 0, 0, 0}, + {"mix:2:1:0:2", 6, 0, 2, 0, 0, 1}, + {"mix:2:1:0:3", 6, 0, 3, 0, 0, 1}, + {"mix:2:1:2:3", 6, 2, 3, 0, 0, 1}, + {"mix:2:2:0:1", 6, 0, 1, 0, 0, 2}, + {"mix:2:2:0:3", 6, 0, 3, 0, 0, 2}, + {"mix:2:2:1:3", 6, 1, 3, 0, 0, 2}, + {"mix:2:3:0:1", 6, 0, 1, 0, 0, 3}, + {"mix:2:3:0:2", 6, 0, 2, 0, 0, 3}, + {"mix:2:3:1:2", 6, 1, 2, 0, 0, 3}, + {"mix:3:0:1:2:3", 7, 1, 2, 3, 0, 0}, + {"mix:3:1:0:2:3", 7, 0, 2, 3, 0, 1}, + {"mix:3:2:0:1:3", 7, 0, 1, 3, 0, 2}, + {"mix:3:3:0:1:2", 7, 0, 1, 2, 0, 3} +}; + +static int lp5812_write(struct lp5812_chip *chip, u16 reg, u8 val) +{ + struct device *dev = &chip->client->dev; + struct i2c_msg msg; + u8 buf[LP5812_DATA_LENGTH]; + u8 reg_addr_bit8_9; + int ret; + + /* Extract register address bits 9 and 8 for Address Byte 1 */ + reg_addr_bit8_9 = (reg >> LP5812_REG_ADDR_HIGH_SHIFT) & LP5812_REG_ADDR_BIT_8_9_MASK; + + /* Prepare payload: Address Byte 2 (bits [7:0]) and value to write */ + buf[LP5812_DATA_BYTE_0_IDX] = (u8)(reg & LP5812_REG_ADDR_LOW_MASK); + buf[LP5812_DATA_BYTE_1_IDX] = val; + + /* Construct I2C message for a write operation */ + msg.addr = (chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9; + msg.flags = 0; + msg.len = sizeof(buf); + msg.buf = buf; + + ret = i2c_transfer(chip->client->adapter, &msg, 1); + if (ret == 1) + return 0; + + dev_err(dev, "I2C write error, ret=%d\n", ret); + return ret < 0 ? ret : -EIO; +} + +static int lp5812_read(struct lp5812_chip *chip, u16 reg, u8 *val) +{ + struct device *dev = &chip->client->dev; + struct i2c_msg msgs[LP5812_READ_MSG_LENGTH]; + u8 ret_val; + u8 reg_addr_bit8_9; + u8 converted_reg; + int ret; + + /* Extract register address bits 9 and 8 for Address Byte 1 */ + reg_addr_bit8_9 = (reg >> LP5812_REG_ADDR_HIGH_SHIFT) & LP5812_REG_ADDR_BIT_8_9_MASK; + + /* Lower 8 bits go in Address Byte 2 */ + converted_reg = (u8)(reg & LP5812_REG_ADDR_LOW_MASK); + + /* Prepare I2C write message to set register address */ + msgs[LP5812_MSG_0_IDX].addr = + (chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9; + msgs[LP5812_MSG_0_IDX].flags = 0; + msgs[LP5812_MSG_0_IDX].len = 1; + msgs[LP5812_MSG_0_IDX].buf = &converted_reg; + + /* Prepare I2C read message to retrieve register value */ + msgs[LP5812_MSG_1_IDX].addr = + (chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9; + msgs[LP5812_MSG_1_IDX].flags = I2C_M_RD; + msgs[LP5812_MSG_1_IDX].len = 1; + msgs[LP5812_MSG_1_IDX].buf = &ret_val; + + ret = i2c_transfer(chip->client->adapter, msgs, LP5812_READ_MSG_LENGTH); + if (ret == LP5812_READ_MSG_LENGTH) { + *val = ret_val; + return 0; + } + + dev_err(dev, "I2C read error, ret=%d\n", ret); + *val = 0; + return ret < 0 ? ret : -EIO; +} + +static int lp5812_read_tsd_config_status(struct lp5812_chip *chip, u8 *reg_val) +{ + return lp5812_read(chip, LP5812_TSD_CONFIG_STATUS, reg_val); +} + +static int lp5812_update_regs_config(struct lp5812_chip *chip) +{ + u8 reg_val; + int ret; + + ret = lp5812_write(chip, LP5812_CMD_UPDATE, LP5812_UPDATE_CMD_VAL); + if (ret) + return ret; + + ret = lp5812_read_tsd_config_status(chip, ®_val); + if (ret) + return ret; + + return reg_val & LP5812_CFG_ERR_STATUS_MASK; +} + +static ssize_t parse_drive_mode(struct lp5812_chip *chip, const char *str) +{ + int i; + + chip->drive_mode.bits.mix_sel_led_0 = false; + chip->drive_mode.bits.mix_sel_led_1 = false; + chip->drive_mode.bits.mix_sel_led_2 = false; + chip->drive_mode.bits.mix_sel_led_3 = false; + + if (sysfs_streq(str, LP5812_MODE_DIRECT_NAME)) { + chip->drive_mode.bits.led_mode = LP5812_MODE_DIRECT_VALUE; + return 0; + } + + for (i = 0; i < ARRAY_SIZE(chip_mode_map); i++) { + if (!sysfs_streq(str, chip_mode_map[i].mode_name)) + continue; + + chip->drive_mode.bits.led_mode = chip_mode_map[i].mode; + chip->scan_order.bits.order0 = chip_mode_map[i].scan_order_0; + chip->scan_order.bits.order1 = chip_mode_map[i].scan_order_1; + chip->scan_order.bits.order2 = chip_mode_map[i].scan_order_2; + chip->scan_order.bits.order3 = chip_mode_map[i].scan_order_3; + + switch (chip_mode_map[i].selection_led) { + case LP5812_MODE_MIX_SELECT_LED_0: + chip->drive_mode.bits.mix_sel_led_0 = true; + break; + case LP5812_MODE_MIX_SELECT_LED_1: + chip->drive_mode.bits.mix_sel_led_1 = true; + break; + case LP5812_MODE_MIX_SELECT_LED_2: + chip->drive_mode.bits.mix_sel_led_2 = true; + break; + case LP5812_MODE_MIX_SELECT_LED_3: + chip->drive_mode.bits.mix_sel_led_3 = true; + break; + default: + return -EINVAL; + } + + return 0; + } + + return -EINVAL; +} + +static int lp5812_set_drive_mode_scan_order(struct lp5812_chip *chip) +{ + u8 val; + int ret; + + val = chip->drive_mode.val; + ret = lp5812_write(chip, LP5812_DEV_CONFIG1, val); + if (ret) + return ret; + + val = chip->scan_order.val; + ret = lp5812_write(chip, LP5812_DEV_CONFIG2, val); + + return ret; +} + +static int lp5812_set_led_mode(struct lp5812_chip *chip, int led_number, + enum control_mode mode) +{ + u8 reg_val; + u16 reg; + int ret; + + /* + * Select device configuration register. + * Reg3 for LED_0–LED_3, LED_A0–A2, LED_B0 + * Reg4 for LED_B1–B2, LED_C0–C2, LED_D0–D2 + */ + if (led_number < LP5812_NUMBER_LED_IN_REG) + reg = LP5812_DEV_CONFIG3; + else + reg = LP5812_DEV_CONFIG4; + + ret = lp5812_read(chip, reg, ®_val); + if (ret) + return ret; + + if (mode == LP5812_MODE_MANUAL) + reg_val &= ~(LP5812_ENABLE << (led_number % LP5812_NUMBER_LED_IN_REG)); + else + reg_val |= (LP5812_ENABLE << (led_number % LP5812_NUMBER_LED_IN_REG)); + + ret = lp5812_write(chip, reg, reg_val); + if (ret) + return ret; + + ret = lp5812_update_regs_config(chip); + + return ret; +} + +static int lp5812_manual_dc_pwm_control(struct lp5812_chip *chip, int led_number, + u8 val, enum dimming_type dimming_type) +{ + u16 led_base_reg; + int ret; + + if (dimming_type == LP5812_DIMMING_ANALOG) + led_base_reg = LP5812_MANUAL_DC_BASE; + else + led_base_reg = LP5812_MANUAL_PWM_BASE; + + ret = lp5812_write(chip, led_base_reg + led_number, val); + + return ret; +} + +static int lp5812_multicolor_brightness(struct lp5812_led *led) +{ + struct lp5812_chip *chip = led->chip; + int ret, i; + + guard(mutex)(&chip->lock); + for (i = 0; i < led->mc_cdev.num_colors; i++) { + ret = lp5812_manual_dc_pwm_control(chip, led->mc_cdev.subled_info[i].channel, + led->mc_cdev.subled_info[i].brightness, + LP5812_DIMMING_PWM); + if (ret) + return ret; + } + + return 0; +} + +static int lp5812_led_brightness(struct lp5812_led *led) +{ + struct lp5812_chip *chip = led->chip; + struct lp5812_led_config *led_cfg; + int ret; + + led_cfg = &chip->led_config[led->chan_nr]; + + guard(mutex)(&chip->lock); + ret = lp5812_manual_dc_pwm_control(chip, led_cfg->led_id[0], + led->brightness, LP5812_DIMMING_PWM); + + return ret; +} + +static int lp5812_set_brightness(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct lp5812_led *led = container_of(cdev, struct lp5812_led, cdev); + + led->brightness = (u8)brightness; + + return lp5812_led_brightness(led); +} + +static int lp5812_set_mc_brightness(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_dev = lcdev_to_mccdev(cdev); + struct lp5812_led *led = container_of(mc_dev, struct lp5812_led, mc_cdev); + + led_mc_calc_color_components(&led->mc_cdev, brightness); + + return lp5812_multicolor_brightness(led); +} + +static int lp5812_init_led(struct lp5812_led *led, struct lp5812_chip *chip, int chan) +{ + struct device *dev = &chip->client->dev; + struct mc_subled *mc_led_info; + struct led_classdev *led_cdev; + int i, ret; + + if (chip->led_config[chan].name) { + led->cdev.name = chip->led_config[chan].name; + } else { + led->cdev.name = devm_kasprintf(dev, GFP_KERNEL, "%s:channel%d", + chip->label ? : chip->client->name, chan); + if (!led->cdev.name) + return -ENOMEM; + } + + if (!chip->led_config[chan].is_sc_led) { + mc_led_info = devm_kcalloc(dev, chip->led_config[chan].num_colors, + sizeof(*mc_led_info), GFP_KERNEL); + if (!mc_led_info) + return -ENOMEM; + + led_cdev = &led->mc_cdev.led_cdev; + led_cdev->name = led->cdev.name; + led_cdev->brightness_set_blocking = lp5812_set_mc_brightness; + led->mc_cdev.num_colors = chip->led_config[chan].num_colors; + + for (i = 0; i < led->mc_cdev.num_colors; i++) { + mc_led_info[i].color_index = chip->led_config[chan].color_id[i]; + mc_led_info[i].channel = chip->led_config[chan].led_id[i]; + } + + led->mc_cdev.subled_info = mc_led_info; + } else { + led->cdev.brightness_set_blocking = lp5812_set_brightness; + } + + led->chan_nr = chan; + + if (chip->led_config[chan].is_sc_led) { + ret = devm_led_classdev_register(dev, &led->cdev); + if (ret == 0) + led->cdev.dev->platform_data = led; + } else { + ret = devm_led_classdev_multicolor_register(dev, &led->mc_cdev); + if (ret == 0) + led->mc_cdev.led_cdev.dev->platform_data = led; + } + + return ret; +} + +static int lp5812_register_leds(struct lp5812_led *leds, struct lp5812_chip *chip) +{ + struct lp5812_led *led; + int num_channels = chip->num_channels; + u8 reg_val; + u16 reg; + int ret, i, j; + + for (i = 0; i < num_channels; i++) { + led = &leds[i]; + ret = lp5812_init_led(led, chip, i); + if (ret) + goto err_init_led; + + led->chip = chip; + + for (j = 0; j < chip->led_config[i].num_colors; j++) { + ret = lp5812_write(chip, + LP5812_AUTO_DC_BASE + chip->led_config[i].led_id[j], + chip->led_config[i].max_current[j]); + if (ret) + goto err_init_led; + + ret = lp5812_manual_dc_pwm_control(chip, chip->led_config[i].led_id[j], + chip->led_config[i].max_current[j], + LP5812_DIMMING_ANALOG); + if (ret) + goto err_init_led; + + ret = lp5812_set_led_mode(chip, chip->led_config[i].led_id[j], + LP5812_MODE_MANUAL); + if (ret) + goto err_init_led; + + reg = (chip->led_config[i].led_id[j] < LP5812_NUMBER_LED_IN_REG) ? + LP5812_LED_EN_1 : LP5812_LED_EN_2; + + ret = lp5812_read(chip, reg, ®_val); + if (ret) + goto err_init_led; + + reg_val |= (LP5812_ENABLE << (chip->led_config[i].led_id[j] % + LP5812_NUMBER_LED_IN_REG)); + + ret = lp5812_write(chip, reg, reg_val); + if (ret) + goto err_init_led; + } + } + + return 0; + +err_init_led: + return ret; +} + +static int lp5812_init_device(struct lp5812_chip *chip) +{ + int ret; + + usleep_range(LP5812_WAIT_DEVICE_STABLE_MIN, LP5812_WAIT_DEVICE_STABLE_MAX); + + ret = lp5812_write(chip, LP5812_REG_ENABLE, LP5812_ENABLE); + if (ret) { + dev_err(&chip->client->dev, "failed to enable LP5812 device\n"); + return ret; + } + + ret = lp5812_write(chip, LP5812_DEV_CONFIG12, LP5812_LSD_LOD_START_UP); + if (ret) { + dev_err(&chip->client->dev, "failed to configure device safety thresholds\n"); + return ret; + } + + ret = parse_drive_mode(chip, chip->scan_mode); + if (ret) + return ret; + + ret = lp5812_set_drive_mode_scan_order(chip); + if (ret) + return ret; + + ret = lp5812_update_regs_config(chip); + if (ret) { + dev_err(&chip->client->dev, "failed to apply configuration updates\n"); + return ret; + } + + return 0; +} + +static void lp5812_deinit_device(struct lp5812_chip *chip) +{ + lp5812_write(chip, LP5812_LED_EN_1, LP5812_DISABLE); + lp5812_write(chip, LP5812_LED_EN_2, LP5812_DISABLE); + lp5812_write(chip, LP5812_REG_ENABLE, LP5812_DISABLE); +} + +static int lp5812_parse_led_channel(struct device_node *np, + struct lp5812_led_config *cfg, + int color_number) +{ + int color_id, reg, ret; + u32 max_cur; + + ret = of_property_read_u32(np, "reg", ®); + if (ret) + return ret; + + cfg->led_id[color_number] = reg; + + ret = of_property_read_u32(np, "led-max-microamp", &max_cur); + if (ret) + max_cur = 0; + /* Convert microamps to driver units */ + cfg->max_current[color_number] = max_cur / 100; + + ret = of_property_read_u32(np, "color", &color_id); + if (ret) + color_id = 0; + cfg->color_id[color_number] = color_id; + + return 0; +} + +static int lp5812_parse_led(struct device_node *np, + struct lp5812_led_config *cfg, + int led_index) +{ + int num_colors, ret; + + of_property_read_string(np, "label", &cfg[led_index].name); + + ret = of_property_read_u32(np, "reg", &cfg[led_index].chan_nr); + if (ret) + return ret; + + num_colors = 0; + for_each_available_child_of_node_scoped(np, child) { + ret = lp5812_parse_led_channel(child, &cfg[led_index], num_colors); + if (ret) + return ret; + + num_colors++; + } + + if (num_colors == 0) { + ret = lp5812_parse_led_channel(np, &cfg[led_index], 0); + if (ret) + return ret; + + num_colors = 1; + cfg[led_index].is_sc_led = true; + } else { + cfg[led_index].is_sc_led = false; + } + + cfg[led_index].num_colors = num_colors; + + return 0; +} + +static int lp5812_of_probe(struct device *dev, + struct device_node *np, + struct lp5812_chip *chip) +{ + struct lp5812_led_config *cfg; + int num_channels, i = 0, ret; + + num_channels = of_get_available_child_count(np); + if (num_channels == 0) { + dev_err(dev, "no LED channels\n"); + return -EINVAL; + } + + cfg = devm_kcalloc(dev, num_channels, sizeof(*cfg), GFP_KERNEL); + if (!cfg) + return -ENOMEM; + + chip->led_config = &cfg[0]; + chip->num_channels = num_channels; + + for_each_available_child_of_node_scoped(np, child) { + ret = lp5812_parse_led(child, cfg, i); + if (ret) + return -EINVAL; + i++; + } + + ret = of_property_read_string(np, "ti,scan-mode", &chip->scan_mode); + if (ret) + chip->scan_mode = LP5812_MODE_DIRECT_NAME; + + of_property_read_string(np, "label", &chip->label); + + return 0; +} + +static int lp5812_probe(struct i2c_client *client) +{ + struct lp5812_chip *chip; + struct device_node *np = dev_of_node(&client->dev); + struct lp5812_led *leds; + int ret; + + if (!np) + return -EINVAL; + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + ret = lp5812_of_probe(&client->dev, np, chip); + if (ret) + return ret; + + leds = devm_kcalloc(&client->dev, chip->num_channels, sizeof(*leds), GFP_KERNEL); + if (!leds) + return -ENOMEM; + + chip->client = client; + mutex_init(&chip->lock); + i2c_set_clientdata(client, chip); + + ret = lp5812_init_device(chip); + if (ret) + return ret; + + ret = lp5812_register_leds(leds, chip); + if (ret) + goto err_out; + + return 0; + +err_out: + lp5812_deinit_device(chip); + return ret; +} + +static void lp5812_remove(struct i2c_client *client) +{ + struct lp5812_chip *chip = i2c_get_clientdata(client); + + lp5812_deinit_device(chip); +} + +static const struct of_device_id of_lp5812_match[] = { + { .compatible = "ti,lp5812" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, of_lp5812_match); + +static struct i2c_driver lp5812_driver = { + .driver = { + .name = "lp5812", + .of_match_table = of_lp5812_match, + }, + .probe = lp5812_probe, + .remove = lp5812_remove, +}; +module_i2c_driver(lp5812_driver); + +MODULE_DESCRIPTION("Texas Instruments LP5812 LED Driver"); +MODULE_AUTHOR("Jared Zhou "); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/rgb/leds-lp5812.h b/drivers/leds/rgb/leds-lp5812.h new file mode 100644 index 000000000000..d8728ecd90b3 --- /dev/null +++ b/drivers/leds/rgb/leds-lp5812.h @@ -0,0 +1,172 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * LP5812 Driver Header + * + * Copyright (C) 2025 Texas Instruments + * + * Author: Jared Zhou + */ + +#ifndef _LP5812_H_ +#define _LP5812_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define LP5812_REG_ENABLE 0x0000 +#define LP5812_REG_RESET 0x0023 +#define LP5812_DEV_CONFIG0 0x0001 +#define LP5812_DEV_CONFIG1 0x0002 +#define LP5812_DEV_CONFIG2 0x0003 +#define LP5812_DEV_CONFIG3 0x0004 +#define LP5812_DEV_CONFIG4 0x0005 +#define LP5812_DEV_CONFIG5 0x0006 +#define LP5812_DEV_CONFIG6 0x0007 +#define LP5812_DEV_CONFIG7 0x0008 +#define LP5812_DEV_CONFIG8 0x0009 +#define LP5812_DEV_CONFIG9 0x000A +#define LP5812_DEV_CONFIG10 0x000B +#define LP5812_DEV_CONFIG11 0x000c +#define LP5812_DEV_CONFIG12 0x000D +#define LP5812_CMD_UPDATE 0x0010 +#define LP5812_LED_EN_1 0x0020 +#define LP5812_LED_EN_2 0x0021 +#define LP5812_FAULT_CLEAR 0x0022 +#define LP5812_MANUAL_DC_BASE 0x0030 +#define LP5812_AUTO_DC_BASE 0x0050 +#define LP5812_MANUAL_PWM_BASE 0x0040 + +#define LP5812_TSD_CONFIG_STATUS 0x0300 +#define LP5812_LOD_STATUS 0x0301 +#define LP5812_LSD_STATUS 0x0303 + +#define LP5812_ENABLE 0x01 +#define LP5812_DISABLE 0x00 +#define FAULT_CLEAR_ALL 0x07 +#define TSD_CLEAR_VAL 0x04 +#define LSD_CLEAR_VAL 0x02 +#define LOD_CLEAR_VAL 0x01 +#define LP5812_RESET 0x66 +#define LP5812_DEV_CONFIG12_DEFAULT 0x08 + +#define LP5812_UPDATE_CMD_VAL 0x55 +#define LP5812_REG_ADDR_HIGH_SHIFT 8 +#define LP5812_REG_ADDR_BIT_8_9_MASK 0x03 +#define LP5812_REG_ADDR_LOW_MASK 0xFF +#define LP5812_CHIP_ADDR_SHIFT 2 +#define LP5812_DATA_LENGTH 2 +#define LP5812_DATA_BYTE_0_IDX 0 +#define LP5812_DATA_BYTE_1_IDX 1 + +#define LP5812_READ_MSG_LENGTH 2 +#define LP5812_MSG_0_IDX 0 +#define LP5812_MSG_1_IDX 1 +#define LP5812_CFG_ERR_STATUS_MASK 0x01 +#define LP5812_CFG_TSD_STATUS_SHIFT 1 +#define LP5812_CFG_TSD_STATUS_MASK 0x01 + +#define LP5812_FAULT_CLEAR_LOD 0 +#define LP5812_FAULT_CLEAR_LSD 1 +#define LP5812_FAULT_CLEAR_TSD 2 +#define LP5812_FAULT_CLEAR_ALL 3 +#define LP5812_NUMBER_LED_IN_REG 8 + +#define LP5812_WAIT_DEVICE_STABLE_MIN 1000 +#define LP5812_WAIT_DEVICE_STABLE_MAX 1100 + +#define LP5812_LSD_LOD_START_UP 0x0B +#define LP5812_MODE_NAME_MAX_LEN 20 +#define LP5812_MODE_DIRECT_NAME "direct_mode" +#define LP5812_MODE_DIRECT_VALUE 0 +#define LP5812_MODE_MIX_SELECT_LED_0 0 +#define LP5812_MODE_MIX_SELECT_LED_1 1 +#define LP5812_MODE_MIX_SELECT_LED_2 2 +#define LP5812_MODE_MIX_SELECT_LED_3 3 + +enum control_mode { + LP5812_MODE_MANUAL = 0, + LP5812_MODE_AUTONOMOUS +}; + +enum dimming_type { + LP5812_DIMMING_ANALOG, + LP5812_DIMMING_PWM +}; + +union lp5812_scan_order { + struct { + u8 order0:2; + u8 order1:2; + u8 order2:2; + u8 order3:2; + } bits; + u8 val; +}; + +union lp5812_drive_mode { + struct { + u8 mix_sel_led_0:1; + u8 mix_sel_led_1:1; + u8 mix_sel_led_2:1; + u8 mix_sel_led_3:1; + u8 led_mode:3; + u8 pwm_fre:1; + } bits; + u8 val; +}; + +struct lp5812_reg { + u16 addr; + union { + u8 val; + u8 mask; + u8 shift; + }; +}; + +struct lp5812_mode_mapping { + char mode_name[LP5812_MODE_NAME_MAX_LEN]; + u8 mode; + u8 scan_order_0; + u8 scan_order_1; + u8 scan_order_2; + u8 scan_order_3; + u8 selection_led; +}; + +struct lp5812_led_config { + bool is_sc_led; + const char *name; + u8 color_id[LED_COLOR_ID_MAX]; + u32 max_current[LED_COLOR_ID_MAX]; + int chan_nr; + int num_colors; + int led_id[LED_COLOR_ID_MAX]; +}; + +struct lp5812_chip { + u8 num_channels; + struct i2c_client *client; + struct mutex lock; /* Protects register access */ + struct lp5812_led_config *led_config; + const char *label; + const char *scan_mode; + union lp5812_scan_order scan_order; + union lp5812_drive_mode drive_mode; +}; + +struct lp5812_led { + u8 brightness; + int chan_nr; + struct led_classdev cdev; + struct led_classdev_mc mc_cdev; + struct lp5812_chip *chip; +}; + +#endif /*_LP5812_H_*/ -- cgit v1.2.3 From c7dd343a37567e650c263d4c068418b0bb82bf79 Mon Sep 17 00:00:00 2001 From: Lukas Timmermann Date: Sun, 18 Jan 2026 17:50:10 +0100 Subject: leds: as3668: Driver for the ams Osram 4-channel i2c LED driver Since there were no existing drivers for the AS3668 or related devices, a new driver was introduced in a separate file. Similar devices were reviewed, but none shared enough characteristics to justify code reuse. As a result, this driver is written specifically for the AS3668. Signed-off-by: Lukas Timmermann Link: https://patch.msgid.link/20260118165010.902086-3-linux@timmermann.space Signed-off-by: Lee Jones --- drivers/leds/Kconfig | 13 +++ drivers/leds/Makefile | 1 + drivers/leds/leds-as3668.c | 202 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 drivers/leds/leds-as3668.c (limited to 'drivers') diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index 11e7282dc297..597d7a79c988 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -107,6 +107,19 @@ config LEDS_ARIEL Say Y to if your machine is a Dell Wyse 3020 thin client. +config LEDS_OSRAM_AMS_AS3668 + tristate "LED support for Osram AMS AS3668" + depends on LEDS_CLASS + depends on I2C + help + This option enables support for the Osram AMS AS3668 LED controller. + The AS3668 provides up to four LED channels and is controlled via + the I2C bus. This driver offers basic brightness control for each + channel, without support for blinking or other advanced features. + + To compile this driver as a module, choose M here: the module + will be called leds-as3668. + config LEDS_AW200XX tristate "LED support for Awinic AW20036/AW20054/AW20072/AW20108" depends on LEDS_CLASS diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index 9a0333ec1a86..8fdb45d5b439 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -15,6 +15,7 @@ obj-$(CONFIG_LEDS_ADP5520) += leds-adp5520.o obj-$(CONFIG_LEDS_AN30259A) += leds-an30259a.o obj-$(CONFIG_LEDS_APU) += leds-apu.o obj-$(CONFIG_LEDS_ARIEL) += leds-ariel.o +obj-$(CONFIG_LEDS_AS3668) += leds-as3668.o obj-$(CONFIG_LEDS_AW200XX) += leds-aw200xx.o obj-$(CONFIG_LEDS_AW2013) += leds-aw2013.o obj-$(CONFIG_LEDS_BCM6328) += leds-bcm6328.o diff --git a/drivers/leds/leds-as3668.c b/drivers/leds/leds-as3668.c new file mode 100644 index 000000000000..b2794492370e --- /dev/null +++ b/drivers/leds/leds-as3668.c @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Osram AMS AS3668 LED Driver IC + * + * Copyright (C) 2025 Lukas Timmermann + */ + +#include +#include +#include +#include +#include + +#define AS3668_MAX_LEDS 4 + +/* Chip Ident */ + +#define AS3668_CHIP_ID1_REG 0x3e +#define AS3668_CHIP_ID 0xa5 + +/* Current Control */ + +#define AS3668_CURR_MODE_REG 0x01 +#define AS3668_CURR_MODE_OFF 0x0 +#define AS3668_CURR_MODE_ON 0x1 +#define AS3668_CURR1_MODE_MASK GENMASK(1, 0) +#define AS3668_CURR2_MODE_MASK GENMASK(3, 2) +#define AS3668_CURR3_MODE_MASK GENMASK(5, 4) +#define AS3668_CURR4_MODE_MASK GENMASK(7, 6) +#define AS3668_CURR1_REG 0x02 +#define AS3668_CURR2_REG 0x03 +#define AS3668_CURR3_REG 0x04 +#define AS3668_CURR4_REG 0x05 + +#define AS3668_CURR_MODE_PACK(mode) (((mode) << 0) | \ + ((mode) << 2) | \ + ((mode) << 4) | \ + ((mode) << 6)) + +struct as3668_led { + struct led_classdev cdev; + struct as3668 *chip; + struct fwnode_handle *fwnode; + u8 mode_mask; + u8 current_reg; +}; + +struct as3668 { + struct i2c_client *client; + struct as3668_led leds[AS3668_MAX_LEDS]; +}; + +static int as3668_channel_mode_set(struct as3668_led *led, u8 mode) +{ + int ret; + u8 channel_modes; + + ret = i2c_smbus_read_byte_data(led->chip->client, AS3668_CURR_MODE_REG); + if (ret < 0) { + dev_err(led->cdev.dev, "failed to read channel modes\n"); + return ret; + } + channel_modes = (u8)ret; + + channel_modes &= ~led->mode_mask; + channel_modes |= led->mode_mask & (AS3668_CURR_MODE_PACK(mode)); + + return i2c_smbus_write_byte_data(led->chip->client, AS3668_CURR_MODE_REG, channel_modes); +} + +static enum led_brightness as3668_brightness_get(struct led_classdev *cdev) +{ + struct as3668_led *led = container_of(cdev, struct as3668_led, cdev); + + return i2c_smbus_read_byte_data(led->chip->client, led->current_reg); +} + +static void as3668_brightness_set(struct led_classdev *cdev, enum led_brightness brightness) +{ + struct as3668_led *led = container_of(cdev, struct as3668_led, cdev); + int err; + + err = as3668_channel_mode_set(led, !!brightness); + if (err) + dev_err(cdev->dev, "failed to set channel mode: %d\n", err); + + err = i2c_smbus_write_byte_data(led->chip->client, led->current_reg, brightness); + if (err) + dev_err(cdev->dev, "failed to set brightness: %d\n", err); +} + +static int as3668_dt_init(struct as3668 *as3668) +{ + struct device *dev = &as3668->client->dev; + struct as3668_led *led; + struct led_init_data init_data = {}; + int err; + u32 reg; + + for_each_available_child_of_node_scoped(dev_of_node(dev), child) { + err = of_property_read_u32(child, "reg", ®); + if (err) + return dev_err_probe(dev, err, "failed to read 'reg' property"); + + if (reg < 0 || reg >= AS3668_MAX_LEDS) + return dev_err_probe(dev, -EINVAL, + "unsupported LED: %d\n", reg); + + led = &as3668->leds[reg]; + led->fwnode = of_fwnode_handle(child); + + led->current_reg = reg + AS3668_CURR1_REG; + led->mode_mask = AS3668_CURR1_MODE_MASK << (reg * 2); + led->chip = as3668; + + led->cdev.max_brightness = U8_MAX; + led->cdev.brightness_get = as3668_brightness_get; + led->cdev.brightness_set = as3668_brightness_set; + + init_data.fwnode = led->fwnode; + init_data.default_label = ":"; + + err = devm_led_classdev_register_ext(dev, &led->cdev, &init_data); + if (err) + return dev_err_probe(dev, err, "failed to register LED %d\n", reg); + } + + return 0; +} + +static int as3668_probe(struct i2c_client *client) +{ + struct as3668 *as3668; + int err; + u8 chip_id; + + chip_id = i2c_smbus_read_byte_data(client, AS3668_CHIP_ID1_REG); + if (chip_id != AS3668_CHIP_ID) + return dev_err_probe(&client->dev, -ENODEV, + "expected chip ID 0x%02x, got 0x%02x\n", + AS3668_CHIP_ID, chip_id); + + as3668 = devm_kzalloc(&client->dev, sizeof(*as3668), GFP_KERNEL); + if (!as3668) + return -ENOMEM; + + as3668->client = client; + + err = as3668_dt_init(as3668); + if (err) + return err; + + /* Set all four channel modes to 'off' */ + err = i2c_smbus_write_byte_data(client, AS3668_CURR_MODE_REG, + FIELD_PREP(AS3668_CURR1_MODE_MASK, AS3668_CURR_MODE_OFF) | + FIELD_PREP(AS3668_CURR2_MODE_MASK, AS3668_CURR_MODE_OFF) | + FIELD_PREP(AS3668_CURR3_MODE_MASK, AS3668_CURR_MODE_OFF) | + FIELD_PREP(AS3668_CURR4_MODE_MASK, AS3668_CURR_MODE_OFF)); + + /* Set initial currents to 0mA */ + err |= i2c_smbus_write_byte_data(client, AS3668_CURR1_REG, 0); + err |= i2c_smbus_write_byte_data(client, AS3668_CURR2_REG, 0); + err |= i2c_smbus_write_byte_data(client, AS3668_CURR3_REG, 0); + err |= i2c_smbus_write_byte_data(client, AS3668_CURR4_REG, 0); + + if (err) + return dev_err_probe(&client->dev, -EIO, "failed to set zero initial current levels\n"); + + return 0; +} + +static void as3668_remove(struct i2c_client *client) +{ + i2c_smbus_write_byte_data(client, AS3668_CURR_MODE_REG, 0); +} + +static const struct i2c_device_id as3668_idtable[] = { + { "as3668" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, as3668_idtable); + +static const struct of_device_id as3668_match_table[] = { + { .compatible = "ams,as3668" }, + { } +}; +MODULE_DEVICE_TABLE(of, as3668_match_table); + +static struct i2c_driver as3668_driver = { + .driver = { + .name = "leds_as3668", + .of_match_table = as3668_match_table, + }, + .probe = as3668_probe, + .remove = as3668_remove, + .id_table = as3668_idtable, +}; +module_i2c_driver(as3668_driver); + +MODULE_AUTHOR("Lukas Timmermann "); +MODULE_DESCRIPTION("AS3668 LED driver"); +MODULE_LICENSE("GPL"); -- cgit v1.2.3