From 782cca1ac6f43f5e6d0b1512b63fa517da223aca Mon Sep 17 00:00:00 2001 From: Jonas Rebmann Date: Fri, 5 Jun 2026 09:27:48 +0200 Subject: [PATCH 01/22] leds: pwm-multicolor: Introduce default-intensity property Like all LED drivers, pwm-multicolor supports turning on an LED on boot by setting linux,default-trigger, e.g. to "default-on". pwm-multicolor however scales the brightness of the color-component sub-LEDs with their individual intensity value. Since these intensities are zero-initialized, on boot a trigger is invisible until colors are set from userspace. Fix linux,default-trigger for pwm-multicolor by allowing for nonzero default-intensities but default to 0 for backwards-compatibility. Signed-off-by: Jonas Rebmann Link: https://patch.msgid.link/20260605-multicolor-default-v2-2-ed07271df6b0@pengutronix.de Signed-off-by: Lee Jones --- drivers/leds/rgb/leds-pwm-multicolor.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/leds/rgb/leds-pwm-multicolor.c b/drivers/leds/rgb/leds-pwm-multicolor.c index e0d7d3c9215c..3e515df40583 100644 --- a/drivers/leds/rgb/leds-pwm-multicolor.c +++ b/drivers/leds/rgb/leds-pwm-multicolor.c @@ -95,6 +95,8 @@ static int iterate_subleds(struct device *dev, struct pwm_mc_led *priv, } subled[priv->mc_cdev.num_colors].color_index = color; + fwnode_property_read_u32(fwnode, "default-intensity", + &subled[priv->mc_cdev.num_colors].intensity); priv->mc_cdev.num_colors++; } From 7ddc04d1bd08f80ffc1e2fb97f3fc6cacab0ffc0 Mon Sep 17 00:00:00 2001 From: "Mike Marciniszyn (Meta)" Date: Wed, 20 May 2026 16:03:35 -0400 Subject: [PATCH 02/22] leds: trigger: netdev: Extend speeds up to 100G Add 25G, 40G, 50G, and 100G as available speeds to the netdev LED trigger. Signed-off-by: Mike Marciniszyn (Meta) Reviewed-by: Andrew Lunn Link: https://patch.msgid.link/20260520200337.204431-2-mike.marciniszyn@gmail.com Signed-off-by: Lee Jones --- drivers/leds/trigger/ledtrig-netdev.c | 46 ++++++++++++++++++++++++++- include/linux/leds.h | 4 +++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c index 64c078e997f2..5b4e92c14dbb 100644 --- a/drivers/leds/trigger/ledtrig-netdev.c +++ b/drivers/leds/trigger/ledtrig-netdev.c @@ -129,6 +129,22 @@ static void set_baseline_state(struct led_netdev_data *trigger_data) trigger_data->link_speed == SPEED_10000) blink_on = true; + if (test_bit(TRIGGER_NETDEV_LINK_25000, &trigger_data->mode) && + trigger_data->link_speed == SPEED_25000) + blink_on = true; + + if (test_bit(TRIGGER_NETDEV_LINK_40000, &trigger_data->mode) && + trigger_data->link_speed == SPEED_40000) + blink_on = true; + + if (test_bit(TRIGGER_NETDEV_LINK_50000, &trigger_data->mode) && + trigger_data->link_speed == SPEED_50000) + blink_on = true; + + if (test_bit(TRIGGER_NETDEV_LINK_100000, &trigger_data->mode) && + trigger_data->link_speed == SPEED_100000) + blink_on = true; + if (test_bit(TRIGGER_NETDEV_HALF_DUPLEX, &trigger_data->mode) && trigger_data->duplex == DUPLEX_HALF) blink_on = true; @@ -342,6 +358,10 @@ static ssize_t netdev_led_attr_show(struct device *dev, char *buf, case TRIGGER_NETDEV_LINK_2500: case TRIGGER_NETDEV_LINK_5000: case TRIGGER_NETDEV_LINK_10000: + case TRIGGER_NETDEV_LINK_25000: + case TRIGGER_NETDEV_LINK_40000: + case TRIGGER_NETDEV_LINK_50000: + case TRIGGER_NETDEV_LINK_100000: case TRIGGER_NETDEV_HALF_DUPLEX: case TRIGGER_NETDEV_FULL_DUPLEX: case TRIGGER_NETDEV_TX: @@ -378,6 +398,10 @@ static ssize_t netdev_led_attr_store(struct device *dev, const char *buf, case TRIGGER_NETDEV_LINK_2500: case TRIGGER_NETDEV_LINK_5000: case TRIGGER_NETDEV_LINK_10000: + case TRIGGER_NETDEV_LINK_25000: + case TRIGGER_NETDEV_LINK_40000: + case TRIGGER_NETDEV_LINK_50000: + case TRIGGER_NETDEV_LINK_100000: case TRIGGER_NETDEV_HALF_DUPLEX: case TRIGGER_NETDEV_FULL_DUPLEX: case TRIGGER_NETDEV_TX: @@ -401,7 +425,11 @@ static ssize_t netdev_led_attr_store(struct device *dev, const char *buf, test_bit(TRIGGER_NETDEV_LINK_1000, &mode) || test_bit(TRIGGER_NETDEV_LINK_2500, &mode) || test_bit(TRIGGER_NETDEV_LINK_5000, &mode) || - test_bit(TRIGGER_NETDEV_LINK_10000, &mode))) + test_bit(TRIGGER_NETDEV_LINK_10000, &mode) || + test_bit(TRIGGER_NETDEV_LINK_25000, &mode) || + test_bit(TRIGGER_NETDEV_LINK_40000, &mode) || + test_bit(TRIGGER_NETDEV_LINK_50000, &mode) || + test_bit(TRIGGER_NETDEV_LINK_100000, &mode))) return -EINVAL; cancel_delayed_work_sync(&trigger_data->work); @@ -438,6 +466,10 @@ DEFINE_NETDEV_TRIGGER(link_1000, TRIGGER_NETDEV_LINK_1000); DEFINE_NETDEV_TRIGGER(link_2500, TRIGGER_NETDEV_LINK_2500); DEFINE_NETDEV_TRIGGER(link_5000, TRIGGER_NETDEV_LINK_5000); DEFINE_NETDEV_TRIGGER(link_10000, TRIGGER_NETDEV_LINK_10000); +DEFINE_NETDEV_TRIGGER(link_25000, TRIGGER_NETDEV_LINK_25000); +DEFINE_NETDEV_TRIGGER(link_40000, TRIGGER_NETDEV_LINK_40000); +DEFINE_NETDEV_TRIGGER(link_50000, TRIGGER_NETDEV_LINK_50000); +DEFINE_NETDEV_TRIGGER(link_100000, TRIGGER_NETDEV_LINK_100000); DEFINE_NETDEV_TRIGGER(half_duplex, TRIGGER_NETDEV_HALF_DUPLEX); DEFINE_NETDEV_TRIGGER(full_duplex, TRIGGER_NETDEV_FULL_DUPLEX); DEFINE_NETDEV_TRIGGER(tx, TRIGGER_NETDEV_TX); @@ -526,6 +558,10 @@ static umode_t netdev_trig_link_speed_visible(struct kobject *kobj, CHECK_LINK_MODE_ATTR(2500); CHECK_LINK_MODE_ATTR(5000); CHECK_LINK_MODE_ATTR(10000); + CHECK_LINK_MODE_ATTR(25000); + CHECK_LINK_MODE_ATTR(40000); + CHECK_LINK_MODE_ATTR(50000); + CHECK_LINK_MODE_ATTR(100000); } return 0; @@ -538,6 +574,10 @@ static struct attribute *netdev_trig_link_speed_attrs[] = { &dev_attr_link_2500.attr, &dev_attr_link_5000.attr, &dev_attr_link_10000.attr, + &dev_attr_link_25000.attr, + &dev_attr_link_40000.attr, + &dev_attr_link_50000.attr, + &dev_attr_link_100000.attr, NULL }; @@ -673,6 +713,10 @@ static void netdev_trig_work(struct work_struct *work) test_bit(TRIGGER_NETDEV_LINK_2500, &trigger_data->mode) || test_bit(TRIGGER_NETDEV_LINK_5000, &trigger_data->mode) || test_bit(TRIGGER_NETDEV_LINK_10000, &trigger_data->mode) || + test_bit(TRIGGER_NETDEV_LINK_25000, &trigger_data->mode) || + test_bit(TRIGGER_NETDEV_LINK_40000, &trigger_data->mode) || + test_bit(TRIGGER_NETDEV_LINK_50000, &trigger_data->mode) || + test_bit(TRIGGER_NETDEV_LINK_100000, &trigger_data->mode) || test_bit(TRIGGER_NETDEV_HALF_DUPLEX, &trigger_data->mode) || test_bit(TRIGGER_NETDEV_FULL_DUPLEX, &trigger_data->mode); interval = jiffies_to_msecs( diff --git a/include/linux/leds.h b/include/linux/leds.h index b16b803cc1ac..bf31c246d9e2 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -607,6 +607,10 @@ enum led_trigger_netdev_modes { TRIGGER_NETDEV_LINK_2500, TRIGGER_NETDEV_LINK_5000, TRIGGER_NETDEV_LINK_10000, + TRIGGER_NETDEV_LINK_25000, + TRIGGER_NETDEV_LINK_40000, + TRIGGER_NETDEV_LINK_50000, + TRIGGER_NETDEV_LINK_100000, TRIGGER_NETDEV_HALF_DUPLEX, TRIGGER_NETDEV_FULL_DUPLEX, TRIGGER_NETDEV_TX, From 10a5a70c02277a1c12999b669a1bd1922558338a Mon Sep 17 00:00:00 2001 From: Christophe JAILLET Date: Sat, 20 Jun 2026 17:51:53 +0200 Subject: [PATCH 03/22] leds: lp5860: Fix a potential double-unlock In lp5860_device_init(), if lp5860_init_dt() fails, an already unlocked mutex is unlocked another time. Slightly rework how the lock is taken/released to avoid this potential double unlock. Fixes: f0a66563aa2d ("leds: Add support for TI LP5860 LED driver chip") Signed-off-by: Christophe JAILLET Link: https://patch.msgid.link/0f4d556e0532bfa881d7d83c1e244572117a89e3.1781970674.git.christophe.jaillet@wanadoo.fr Signed-off-by: Lee Jones --- drivers/leds/rgb/leds-lp5860-core.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/leds/rgb/leds-lp5860-core.c b/drivers/leds/rgb/leds-lp5860-core.c index fd0e2f6e6e0f..e21d5f2302be 100644 --- a/drivers/leds/rgb/leds-lp5860-core.c +++ b/drivers/leds/rgb/leds-lp5860-core.c @@ -204,9 +204,9 @@ int lp5860_device_init(struct device *dev) mutex_lock(&lp->lock); ret = regmap_update_bits(lp->regmap, LP5860_REG_DEV_INITIAL, LP5860_MODE_MASK, LP5860_MODE_1 << LP5860_MODE_SHIFT); + mutex_unlock(&lp->lock); if (ret) goto err_disable; - mutex_unlock(&lp->lock); ret = lp5860_init_dt(lp); if (ret) @@ -215,7 +215,6 @@ int lp5860_device_init(struct device *dev) return 0; err_disable: - mutex_unlock(&lp->lock); lp5860_chip_enable(lp, LP5860_CHIP_DISABLE); return ret; } From f647a2266289a35eaa4865f629f2ab7046900d9b Mon Sep 17 00:00:00 2001 From: Christophe JAILLET Date: Sat, 20 Jun 2026 17:53:22 +0200 Subject: [PATCH 04/22] leds: lp5860-spi: Fix an error handling path If lp5860_device_init() fails, a missing mutex_destroy() should be called. Use devm_mutex_init() instead of mutex_init() to fix it. This also simplifies the remove function. Fixes: f0a66563aa2d ("leds: Add support for TI LP5860 LED driver chip") Signed-off-by: Christophe JAILLET Link: https://patch.msgid.link/311792e767ab803d4744bc26155e6dac253d9b45.1781970783.git.christophe.jaillet@wanadoo.fr Signed-off-by: Lee Jones --- drivers/leds/rgb/leds-lp5860-spi.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/leds/rgb/leds-lp5860-spi.c b/drivers/leds/rgb/leds-lp5860-spi.c index 5e0c44854a68..6bf6a625c28a 100644 --- a/drivers/leds/rgb/leds-lp5860-spi.c +++ b/drivers/leds/rgb/leds-lp5860-spi.c @@ -38,6 +38,7 @@ static int lp5860_probe(struct spi_device *spi) struct device *dev = &spi->dev; struct lp5860 *lp5860; unsigned int multi_leds; + int ret; multi_leds = device_get_child_node_count(dev); if (!multi_leds) { @@ -61,7 +62,10 @@ static int lp5860_probe(struct spi_device *spi) "Failed to initialise Regmap.\n"); lp5860->dev = dev; - mutex_init(&lp5860->lock); + + ret = devm_mutex_init(dev, &lp5860->lock); + if (ret) + return ret; spi_set_drvdata(spi, lp5860); @@ -70,10 +74,6 @@ static int lp5860_probe(struct spi_device *spi) static void lp5860_remove(struct spi_device *spi) { - struct lp5860 *lp5860 = spi_get_drvdata(spi); - - mutex_destroy(&lp5860->lock); - lp5860_device_remove(&spi->dev); } From 65a38a28a0b04af19a5e1fbf3869051412eeac96 Mon Sep 17 00:00:00 2001 From: Cosmo Chou Date: Fri, 3 Jul 2026 09:42:01 +0800 Subject: [PATCH 05/22] leds: pca9532: Fix inverted GPIO output polarity The pca9532_gpio_set_value() function incorrectly mapped the requested value to PCA9532_ON and PCA9532_OFF, inverting the GPIO output polarity. A requested logical high (val=1) incorrectly enabled the LED output driver, which on this open-drain device pulls the pin low, while a requested logical low (val=0) released the pin. Correct the mapping so that val=1 yields PCA9532_OFF (pin released / high-impedance) and val=0 yields PCA9532_ON (pin driven low). pca9532_gpio_direction_input() is also updated to pass val=1 to pca9532_gpio_set_value() to align with the corrected polarity mapping, ensuring the pin remains not driven when configured as an input. Fixes: 3c1ab50d0a31 ("drivers/leds/leds-pca9532.c: add gpio capability") Signed-off-by: Cosmo Chou Reviewed-by: Bartosz Golaszewski Link: https://patch.msgid.link/20260703014201.69829-1-chou.cosmo@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-pca9532.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/leds/leds-pca9532.c b/drivers/leds/leds-pca9532.c index f3bf59495b68..2d37e00e459d 100644 --- a/drivers/leds/leds-pca9532.c +++ b/drivers/leds/leds-pca9532.c @@ -327,9 +327,9 @@ static int pca9532_gpio_set_value(struct gpio_chip *gc, unsigned int offset, struct pca9532_led *led = &data->leds[offset]; if (val) - led->state = PCA9532_ON; - else led->state = PCA9532_OFF; + else + led->state = PCA9532_ON; pca9532_setled(led); @@ -349,7 +349,7 @@ static int pca9532_gpio_get_value(struct gpio_chip *gc, unsigned offset) static int pca9532_gpio_direction_input(struct gpio_chip *gc, unsigned offset) { /* To use as input ensure pin is not driven */ - pca9532_gpio_set_value(gc, offset, 0); + pca9532_gpio_set_value(gc, offset, 1); return 0; } From 9c019a8cb95d820e0bd03e75cfbad2c5b13941b7 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:27:22 +0100 Subject: [PATCH 06/22] leds: st1202: Stop pattern sequence before reprogramming The LED1202 datasheet (section 4.8) states that modifications to the Pattern Sequence Repetition register (PAT_REP) and pattern duration registers are only applied after the sequence has completed or been stopped. When the device is running in infinite loop mode (PAT_REP = 0xFF) the sequence never completes on its own, so these writes are silently ignored by the hardware. Neither pattern_clear() nor pattern_set() stop the running sequence before modifying pattern registers, causing any subsequent pattern reprogramming to have no effect when the previous pattern was set to infinite repeat. Fix this by clearing PATS in the Configuration register before touching any pattern registers in both functions, ensuring the hardware accepts the new values immediately. Note that the LED1202 has a single global pattern sequencer shared by all channels: PATS, PATSR, the duration registers, and PAT_REP are chip-wide. Stopping the sequencer in pattern_clear() therefore halts any pattern running on other channels. This is an inherent hardware constraint; pattern_set() restarts the sequencer when a new pattern is programmed. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB84978D0F499774773C7DA1FCC5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 7f68d956f694..316ed8eb054f 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -200,6 +200,10 @@ static int st1202_led_pattern_clear(struct led_classdev *ldev) guard(mutex)(&chip->lock); + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_SHFT); + if (ret != 0) + return ret; + for (int patt = 0; patt < ST1202_MAX_PATTERNS; patt++) { ret = st1202_pwm_pattern_write(chip, led->led_num, patt, LED_OFF); if (ret != 0) @@ -226,6 +230,10 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, guard(mutex)(&chip->lock); + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_SHFT); + if (ret != 0) + return ret; + for (int patt = 0; patt < len; patt++) { if (pattern[patt].delta_t < ST1202_MILLIS_PATTERN_DUR_MIN || pattern[patt].delta_t > ST1202_MILLIS_PATTERN_DUR_MAX) From fd2529ba8fb44cd4b56f1069363b949644b42cec Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:28:20 +0100 Subject: [PATCH 07/22] leds: st1202: Validate pattern input before stopping the sequence Input validation for pattern duration is performed inside the write loop, after the pattern sequence has already been stopped. If validation fails mid-loop the chip is left with the sequence stopped and partially written pattern data, with no recovery. Move all input validation before the mutex and before any hardware interaction, so an invalid input leaves the chip state unchanged. Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB84975929B6ED7CDFBCEB7D76C5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 316ed8eb054f..a750c95ef3f8 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -228,6 +228,12 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, if (len > ST1202_MAX_PATTERNS) return -EINVAL; + for (int patt = 0; patt < len; patt++) { + if (pattern[patt].delta_t < ST1202_MILLIS_PATTERN_DUR_MIN || + pattern[patt].delta_t > ST1202_MILLIS_PATTERN_DUR_MAX) + return -EINVAL; + } + guard(mutex)(&chip->lock); ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_SHFT); @@ -235,10 +241,6 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, return ret; for (int patt = 0; patt < len; patt++) { - if (pattern[patt].delta_t < ST1202_MILLIS_PATTERN_DUR_MIN || - pattern[patt].delta_t > ST1202_MILLIS_PATTERN_DUR_MAX) - return -EINVAL; - ret = st1202_pwm_pattern_write(chip, led->led_num, patt, pattern[patt].brightness); if (ret != 0) return ret; From d32f8bdc2b417a3013e1316a54a0b314f973bbc1 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:29:39 +0100 Subject: [PATCH 08/22] leds: st1202: Fix pattern duration prescaler and pattern_clear skip marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PATy_DUR register encodes duration as N × 22.2 ms, with register value 0 reserved as a pattern skip indicator (§7.10). The driver incorrectly subtracted 1 from the register value: value / ST1202_MILLIS_PATTERN_DUR_MIN - 1 This caused two problems: - All programmed durations were off by one step (~22 ms too short). - Writing the minimum duration (22 ms) produced register value 0, silently skipping the pattern step instead of setting a 22 ms duration. The maximum duration constant was also wrong at 5660 ms. The 8-bit register saturates at 255, giving a maximum of 5610 ms (22 ms × 255). Values above 5653 ms were already producing a uint8_t overflow and writing 0 to the hardware. Fix the formula by removing the erroneous subtraction, and derive the maximum from the register width so the relationship is explicit. Update the documentation to reflect the correct maximum. This exposes a secondary issue: pattern_clear() was calling st1202_duration_pattern_write() with ST1202_MILLIS_PATTERN_DUR_MIN to reset unused slots, accidentally relying on the broken formula to produce register value 0. With the corrected formula, the same call writes 0x01 (22 ms), leaving unused slots as valid 22 ms zero-PWM steps and making the LED appear off for 7 × 22 ms out of every cycle. Write 0 directly to the duration registers in pattern_clear() so unused slots are always explicitly marked as skip, independently of the conversion formula. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB84971D3AF982F4F707A378F0C5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- Documentation/leds/leds-st1202.rst | 2 +- drivers/leds/leds-st1202.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Documentation/leds/leds-st1202.rst b/Documentation/leds/leds-st1202.rst index 1a09fbfcedcf..a2353549469e 100644 --- a/Documentation/leds/leds-st1202.rst +++ b/Documentation/leds/leds-st1202.rst @@ -17,7 +17,7 @@ To be compatible with the hardware pattern format, maximum 8 tuples of brightness (PWM) and duration must be written to hw_pattern. - Min pattern duration: 22 ms -- Max pattern duration: 5660 ms +- Max pattern duration: 5610 ms The format of the hardware pattern values should be: "brightness duration brightness duration ..." diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index a750c95ef3f8..b4adade49898 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -31,7 +31,7 @@ #define ST1202_ILED_REG0 0x09 #define ST1202_MAX_LEDS 12 #define ST1202_MAX_PATTERNS 8 -#define ST1202_MILLIS_PATTERN_DUR_MAX 5660 +#define ST1202_MILLIS_PATTERN_DUR_MAX (ST1202_MILLIS_PATTERN_DUR_MIN * U8_MAX) #define ST1202_MILLIS_PATTERN_DUR_MIN 22 #define ST1202_PATTERN_DUR 0x16 #define ST1202_PATTERN_PWM 0x1E @@ -85,7 +85,7 @@ static int st1202_write_reg(struct st1202_chip *chip, int reg, uint8_t val) static uint8_t st1202_prescalar_to_miliseconds(unsigned int value) { - return value / ST1202_MILLIS_PATTERN_DUR_MIN - 1; + return value / ST1202_MILLIS_PATTERN_DUR_MIN; } static int st1202_pwm_pattern_write(struct st1202_chip *chip, int led_num, @@ -209,7 +209,7 @@ static int st1202_led_pattern_clear(struct led_classdev *ldev) if (ret != 0) return ret; - ret = st1202_duration_pattern_write(chip, patt, ST1202_MILLIS_PATTERN_DUR_MIN); + ret = st1202_write_reg(chip, ST1202_PATTERN_DUR + patt, 0); if (ret != 0) return ret; } From d2ca0e2b6d6430f9c60bb2e0ee0b2b3dc4e5d86a Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:30:50 +0100 Subject: [PATCH 09/22] leds: st1202: Set all pattern PWM slots to full after clearing pattern pattern_clear() sets all PWM registers for the channel to LED_OFF (0). In static mode (PATS=0), the LED output is ILED x Pattern0_PWM / 4095; with Pattern0 at zero the LED remains dark regardless of the ILED value. The LED1202 has a single global sequencer shared across all channels. If another channel starts the sequencer after this one has been cleared, the cleared channel runs through all 8 steps at zero duty cycle and stays dark regardless of ILED. Set all 8 PWM slots to ST1202_PATTERN_PWM_FULL so that ILED alone controls the channel brightness in both static and sequencer modes. Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB849732C162CFE9E2C525AC16C5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index b4adade49898..3203b3768ec5 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -35,6 +35,7 @@ #define ST1202_MILLIS_PATTERN_DUR_MIN 22 #define ST1202_PATTERN_DUR 0x16 #define ST1202_PATTERN_PWM 0x1E +#define ST1202_PATTERN_PWM_FULL 0x0FFF #define ST1202_PATTERN_REP 0x15 struct st1202_led { @@ -205,7 +206,7 @@ static int st1202_led_pattern_clear(struct led_classdev *ldev) return ret; for (int patt = 0; patt < ST1202_MAX_PATTERNS; patt++) { - ret = st1202_pwm_pattern_write(chip, led->led_num, patt, LED_OFF); + ret = st1202_pwm_pattern_write(chip, led->led_num, patt, ST1202_PATTERN_PWM_FULL); if (ret != 0) return ret; From dcc31246aaf0d330a3ba9a725f56c33e6d634caa Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:31:55 +0100 Subject: [PATCH 10/22] leds: st1202: Fix spurious pattern sequence start in setup st1202_setup() writes PATS and PATSR to the Configuration register as its final step, which starts the hardware pattern sequencer during device probe before any patterns have been programmed. This causes the device to run a sequence with whatever values happen to be in the pattern registers at the time. Remove the write. The device reset at the start of setup restores all registers to their power-on defaults, leaving PATS and PATSR cleared. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB849724B0FF00255F4760FAE0C5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 3203b3768ec5..b8a571282900 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -333,11 +333,6 @@ static int st1202_setup(struct st1202_chip *chip) if (ret < 0) return ret; - ret = st1202_write_reg(chip, ST1202_CONFIG_REG, - ST1202_CONFIG_REG_PATS | ST1202_CONFIG_REG_PATSR); - if (ret < 0) - return ret; - return 0; } From 7cbe470366bdd43c7e8114fb2c4d74fa69093121 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:33:16 +0100 Subject: [PATCH 11/22] leds: st1202: Fix brightness having no effect while pattern mode is active Once a hardware pattern is running (PATS=1), writing to the brightness sysfs attribute only updates the ILED register. The visible output is ILED x Pattern_PWM / 4095, so the change has little effect and the LED never returns to steady static operation as the user expects. The LED1202 has a single global sequencer shared across all channels. Stopping it in brightness_set() to force static mode would halt running patterns on all other active LEDs. Instead, set all 8 PWM slots for the channel to ST1202_PATTERN_PWM_FULL before writing ILED. With every step at full duty cycle, the output is ILED x FULL / 4095 = ILED regardless of the sequencer state, without disturbing other channels. This also enables basic LED operation without the pattern trigger: with the trigger set to none, the brightness sysfs attribute fully controls the LED as a simple on/off device. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB8497570FD162D0D42A9864E3C5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index b8a571282900..4909985ade04 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -136,6 +136,8 @@ static void st1202_brightness_set(struct led_classdev *led_cdev, guard(mutex)(&chip->lock); + for (int patt = 0; patt < ST1202_MAX_PATTERNS; patt++) + st1202_pwm_pattern_write(chip, led->led_num, patt, ST1202_PATTERN_PWM_FULL); st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, value); } From 0767335233a8cbab00bbe260a4e4bd380c7677fd Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:34:37 +0100 Subject: [PATCH 12/22] leds: st1202: Disable channel when brightness is set to zero When brightness_set() is called with LED_OFF, only the ILED register is zeroed; the channel enable bit is left set from probe time. A hardware channel enabled with ILED=0 still draws a small residual current, causing a dim glow even when the LED is supposed to be off. Fix this by splitting st1202_channel_set() into a lockless inner function __st1202_channel_set() and a locking wrapper, then calling the inner function from brightness_set() while it already holds the mutex. The channel is now disabled when value is zero and re-enabled when non-zero, in the same lock region as the ILED write. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB8497F11B30FE7D74CAA25135C5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 68 ++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 4909985ade04..2ecd36aae336 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -128,39 +128,11 @@ static int st1202_duration_pattern_write(struct st1202_chip *chip, int pattern, st1202_prescalar_to_miliseconds(value)); } -static void st1202_brightness_set(struct led_classdev *led_cdev, - enum led_brightness value) -{ - struct st1202_led *led = cdev_to_st1202_led(led_cdev); - struct st1202_chip *chip = led->chip; - - guard(mutex)(&chip->lock); - - for (int patt = 0; patt < ST1202_MAX_PATTERNS; patt++) - st1202_pwm_pattern_write(chip, led->led_num, patt, ST1202_PATTERN_PWM_FULL); - st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, value); -} - -static enum led_brightness st1202_brightness_get(struct led_classdev *led_cdev) -{ - struct st1202_led *led = cdev_to_st1202_led(led_cdev); - struct st1202_chip *chip = led->chip; - u8 value = 0; - - guard(mutex)(&chip->lock); - - st1202_read_reg(chip, ST1202_ILED_REG0 + led->led_num, &value); - - return value; -} - -static int st1202_channel_set(struct st1202_chip *chip, int led_num, bool active) +static int __st1202_channel_set(struct st1202_chip *chip, int led_num, bool active) { u8 chan_low, chan_high; int ret; - guard(mutex)(&chip->lock); - if (led_num <= 7) { ret = st1202_read_reg(chip, ST1202_CHAN_ENABLE_LOW, &chan_low); if (ret < 0) @@ -188,6 +160,40 @@ static int st1202_channel_set(struct st1202_chip *chip, int led_num, bool active return 0; } +static int st1202_channel_set(struct st1202_chip *chip, int led_num, bool active) +{ + guard(mutex)(&chip->lock); + + return __st1202_channel_set(chip, led_num, active); +} + +static void st1202_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct st1202_led *led = cdev_to_st1202_led(led_cdev); + struct st1202_chip *chip = led->chip; + + guard(mutex)(&chip->lock); + + for (int patt = 0; patt < ST1202_MAX_PATTERNS; patt++) + st1202_pwm_pattern_write(chip, led->led_num, patt, ST1202_PATTERN_PWM_FULL); + st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, value); + __st1202_channel_set(chip, led->led_num, !!value); +} + +static enum led_brightness st1202_brightness_get(struct led_classdev *led_cdev) +{ + struct st1202_led *led = cdev_to_st1202_led(led_cdev); + struct st1202_chip *chip = led->chip; + u8 value = 0; + + guard(mutex)(&chip->lock); + + st1202_read_reg(chip, ST1202_ILED_REG0 + led->led_num, &value); + + return value; +} + static int st1202_led_set(struct led_classdev *ldev, enum led_brightness value) { struct st1202_led *led = cdev_to_st1202_led(ldev); @@ -257,6 +263,10 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, if (ret != 0) return ret; + ret = __st1202_channel_set(chip, led->led_num, true); + if (ret != 0) + return ret; + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, (ST1202_CONFIG_REG_PATSR | ST1202_CONFIG_REG_PATS | ST1202_CONFIG_REG_SHFT)); if (ret != 0) From cf197514bdfd3877f42b5dce1efd40b7b686547e Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:35:58 +0100 Subject: [PATCH 13/22] leds: st1202: Validate LED reg property against channel count The reg property from the device tree is used directly as an array index into chip->leds[] without bounds checking. A value >= ST1202_MAX_LEDS would cause an out-of-bounds write during probe. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB849718B43321DB7E5A05D17BC5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 2ecd36aae336..168df5ecf27b 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -279,13 +279,19 @@ static int st1202_dt_init(struct st1202_chip *chip) { struct device *dev = &chip->client->dev; struct st1202_led *led; - int err, reg; + 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, "Invalid register\n"); + if (reg >= ST1202_MAX_LEDS) + return dev_err_probe(dev, -EINVAL, + "LED reg %u out of range [0, %d]\n", + reg, ST1202_MAX_LEDS - 1); + led = &chip->leds[reg]; led->is_active = true; led->fwnode = of_fwnode_handle(child); From 96fa4d69958cf90740d756d1e45cdb02cface41b Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Thu, 2 Jul 2026 23:37:03 +0100 Subject: [PATCH 14/22] leds: st1202: Correct and extend hw_pattern documentation Fix the repeat section: -1 is a valid value meaning infinite repeat, as accepted by the ledtrig-pattern sysfs interface; only 0 and values below -1 are rejected. The previous text incorrectly stated all negative numbers were invalid. Also remove the redundant trailing sentence since the behaviour is now covered inline. Add the brightness range (0-255) to the hw_pattern section, which was previously undocumented. Fixes: b1816b22381b ("Documentation:leds: Add leds-st1202.rst") Signed-off-by: Manuel Fombuena Assisted-by: Claude:claude-sonnet-4-6 Link: https://patch.msgid.link/GV1PR08MB8497B2FB4F5AC4B142790CE5C5F52@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- Documentation/leds/leds-st1202.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Documentation/leds/leds-st1202.rst b/Documentation/leds/leds-st1202.rst index a2353549469e..ed32eb3a27d4 100644 --- a/Documentation/leds/leds-st1202.rst +++ b/Documentation/leds/leds-st1202.rst @@ -16,6 +16,7 @@ in terms of PWM duty-cycle and duration (ms). To be compatible with the hardware pattern format, maximum 8 tuples of brightness (PWM) and duration must be written to hw_pattern. +- Brightness range: 0-255 - Min pattern duration: 22 ms - Max pattern duration: 5610 ms @@ -26,9 +27,7 @@ The format of the hardware pattern values should be: ---------------------------- Specify a pattern repeat number, which is common for all channels. -Default is 1; negative numbers and 0 are invalid. +Default is 1. Writing 0 is invalid. Writing -1 or 255 repeats the +pattern indefinitely. This file will always return the originally written repeat number. - -When the 255 value is written to it, all patterns will repeat -indefinitely. From 4d8da7e3dc0d4512e5e6f29df3ca136d7a90d464 Mon Sep 17 00:00:00 2001 From: Samuel Holland Date: Sat, 11 Jul 2026 14:28:25 -0500 Subject: [PATCH 15/22] dt-bindings: leds: Document "gpio" trigger The "gpio" trigger is used to control LEDs by the level of a GPIO input referenced by the trigger-sources property. This is already documented in the trigger-sources property description and is used in at least one devicetree (am335x-regor.dtsi). Document it so validation of those and future DTs will pass. Signed-off-by: Samuel Holland Acked-by: Conor Dooley Link: https://patch.msgid.link/20260711192842.845048-2-samuel@sholland.org Signed-off-by: Lee Jones --- Documentation/devicetree/bindings/leds/common.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/leds/common.yaml b/Documentation/devicetree/bindings/leds/common.yaml index f4e44b33f56d..e148b4277a6f 100644 --- a/Documentation/devicetree/bindings/leds/common.yaml +++ b/Documentation/devicetree/bindings/leds/common.yaml @@ -106,6 +106,8 @@ properties: - bluetooth-power # LED indicates camera flash state - flash + # LED indicates level of GPIO input referenced by trigger-sources + - gpio # LED indicated keyboard capslock - kbd-capslock # LED indicates MTD memory activity From 627666f7c9cd89e7c4023ba790a44610ccfb9471 Mon Sep 17 00:00:00 2001 From: Ninad Naik Date: Wed, 8 Jul 2026 00:14:28 +0530 Subject: [PATCH 16/22] dt-bindings: leds: bcm6358: Convert to DT schema Convert the brcm,bcm6358 to DT schema. Signed-off-by: Ninad Naik Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260707184437.500278-1-ninadnaik07@gmail.com Signed-off-by: Lee Jones --- .../bindings/leds/brcm,bcm6358-leds.yaml | 96 ++++++++++++ .../devicetree/bindings/leds/leds-bcm6358.txt | 143 ------------------ 2 files changed, 96 insertions(+), 143 deletions(-) create mode 100644 Documentation/devicetree/bindings/leds/brcm,bcm6358-leds.yaml delete mode 100644 Documentation/devicetree/bindings/leds/leds-bcm6358.txt diff --git a/Documentation/devicetree/bindings/leds/brcm,bcm6358-leds.yaml b/Documentation/devicetree/bindings/leds/brcm,bcm6358-leds.yaml new file mode 100644 index 000000000000..95fa311fddb5 --- /dev/null +++ b/Documentation/devicetree/bindings/leds/brcm,bcm6358-leds.yaml @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/leds/brcm,bcm6358-leds.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: LEDs connected to Broadcom BCM6358 controller + +description: | + This controller is present on BCM6358 and BCM6368. + In these SoCs there are Serial LEDs (LEDs connected to a 74x164 controller), + which can either be controlled by software (exporting the 74x164 as spi-gpio. + See Documentation/devicetree/bindings/gpio/fairchild,74hc595.yaml), or + by hardware using this driver. + +maintainers: + - Álvaro Fernández Rojas + +properties: + compatible: + const: brcm,bcm6358-leds + + reg: + maxItems: 1 + + "#address-cells": + const: 1 + + "#size-cells": + const: 0 + + brcm,clk-div: + description: SCK signal divider. + default: 1 + $ref: /schemas/types.yaml#/definitions/uint32 + enum: [1, 2, 4, 8] + + brcm,clk-dat-low: + description: Makes clock and data signals active low. + type: boolean + +patternProperties: + "^led@1?[0-9a-f]$": + type: object + $ref: common.yaml# + unevaluatedProperties: false + description: Each LED is represented as a sub-node of + this device. + + properties: + reg: + description: LED pin number (0 to 31). + maxItems: 1 + + required: + - reg + +required: + - compatible + - reg + - "#address-cells" + - "#size-cells" + +additionalProperties: false + +examples: + - | + #include + led-controller@fffe00d0 { + compatible = "brcm,bcm6358-leds"; + #address-cells = <1>; + #size-cells = <0>; + reg = <0xfffe00d0 0x8>; + + led@0 { + reg = <0>; + active-low; + label = "white:alarm"; + }; + led@2 { + reg = <2>; + active-low; + label = "white:tv"; + }; + led@3 { + reg = <3>; + active-low; + label = "white:tel"; + }; + led@4 { + reg = <4>; + active-low; + label = "white:adsl"; + }; + }; +... diff --git a/Documentation/devicetree/bindings/leds/leds-bcm6358.txt b/Documentation/devicetree/bindings/leds/leds-bcm6358.txt deleted file mode 100644 index 211ffc3c4a20..000000000000 --- a/Documentation/devicetree/bindings/leds/leds-bcm6358.txt +++ /dev/null @@ -1,143 +0,0 @@ -LEDs connected to Broadcom BCM6358 controller - -This controller is present on BCM6358 and BCM6368. -In these SoCs there are Serial LEDs (LEDs connected to a 74x164 controller), -which can either be controlled by software (exporting the 74x164 as spi-gpio. -See Documentation/devicetree/bindings/gpio/fairchild,74hc595.yaml), or -by hardware using this driver. - -Required properties: - - compatible : should be "brcm,bcm6358-leds". - - #address-cells : must be 1. - - #size-cells : must be 0. - - reg : BCM6358 LED controller address and size. - -Optional properties: - - brcm,clk-div : SCK signal divider. Possible values are 1, 2, 4 and 8. - Default : 1 - - brcm,clk-dat-low : Boolean, makes clock and data signals active low. - Default : false - -Each LED is represented as a sub-node of the brcm,bcm6358-leds device. - -LED sub-node required properties: - - reg : LED pin number (only LEDs 0 to 31 are valid). - -LED sub-node optional properties: - - label : see Documentation/devicetree/bindings/leds/common.txt - - default-state : see - Documentation/devicetree/bindings/leds/common.txt - - linux,default-trigger : see - Documentation/devicetree/bindings/leds/common.txt - -Examples: -Scenario 1 : BCM6358 - leds0: led-controller@fffe00d0 { - compatible = "brcm,bcm6358-leds"; - #address-cells = <1>; - #size-cells = <0>; - reg = <0xfffe00d0 0x8>; - - alarm_white { - reg = <0>; - active-low; - label = "white:alarm"; - }; - tv_white { - reg = <2>; - active-low; - label = "white:tv"; - }; - tel_white { - reg = <3>; - active-low; - label = "white:tel"; - }; - adsl_white { - reg = <4>; - active-low; - label = "white:adsl"; - }; - }; - -Scenario 2 : BCM6368 - leds0: led-controller@100000d0 { - compatible = "brcm,bcm6358-leds"; - #address-cells = <1>; - #size-cells = <0>; - reg = <0x100000d0 0x8>; - brcm,pol-low; - brcm,clk-div = <4>; - - power_red { - reg = <0>; - active-low; - label = "red:power"; - }; - power_green { - reg = <1>; - active-low; - label = "green:power"; - default-state = "on"; - }; - power_blue { - reg = <2>; - label = "blue:power"; - }; - broadband_red { - reg = <3>; - active-low; - label = "red:broadband"; - }; - broadband_green { - reg = <4>; - label = "green:broadband"; - }; - broadband_blue { - reg = <5>; - active-low; - label = "blue:broadband"; - }; - wireless_red { - reg = <6>; - active-low; - label = "red:wireless"; - }; - wireless_green { - reg = <7>; - active-low; - label = "green:wireless"; - }; - wireless_blue { - reg = <8>; - label = "blue:wireless"; - }; - phone_red { - reg = <9>; - active-low; - label = "red:phone"; - }; - phone_green { - reg = <10>; - active-low; - label = "green:phone"; - }; - phone_blue { - reg = <11>; - label = "blue:phone"; - }; - upgrading_red { - reg = <12>; - active-low; - label = "red:upgrading"; - }; - upgrading_green { - reg = <13>; - active-low; - label = "green:upgrading"; - }; - upgrading_blue { - reg = <14>; - label = "blue:upgrading"; - }; - }; From 3c28d9d0b7e0890df3c16dc7ad5f0fd17e775ab4 Mon Sep 17 00:00:00 2001 From: Edelweise Escala Date: Thu, 9 Jul 2026 08:41:19 +0800 Subject: [PATCH 17/22] dt-bindings: leds: Add LTC3220 18 channel LED Driver LTC3220 is a multi-display LED driver with I2C interface. The LTC3220 provides individual brightness control (64-step), blinking, and gradation features for up to 18 LED outputs. Signed-off-by: Edelweise Escala Reviewed-by: Conor Dooley Link: https://patch.msgid.link/20260709-ltc3220-driver-v13-1-9b53237642eb@analog.com Signed-off-by: Lee Jones --- .../devicetree/bindings/leds/adi,ltc3220.yaml | 133 ++++++++++++++++++ MAINTAINERS | 7 + 2 files changed, 140 insertions(+) create mode 100644 Documentation/devicetree/bindings/leds/adi,ltc3220.yaml diff --git a/Documentation/devicetree/bindings/leds/adi,ltc3220.yaml b/Documentation/devicetree/bindings/leds/adi,ltc3220.yaml new file mode 100644 index 000000000000..48215ad82d9a --- /dev/null +++ b/Documentation/devicetree/bindings/leds/adi,ltc3220.yaml @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/leds/adi,ltc3220.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Analog Devices LTC3220 LED Driver + +maintainers: + - Edelweise Escala + +description: > + The LTC3220 is a multi-display LED driver, which contains a high-efficiency, + low-noise charge pump to provide power to up to 18 LED current sources. + The LEDs are individually configurable to 64-step linear brightness control, + blinking and gradation control via 2-wire I2C interface. + + For more product information please see the link below + https://www.analog.com/en/products/ltc3220.html + +properties: + compatible: + const: adi,ltc3220 + + reg: + maxItems: 1 + + '#address-cells': + const: 1 + + '#size-cells': + const: 0 + + reset-gpios: + maxItems: 1 + +patternProperties: + '^led@([1-9a-f]|1[0-2])$': + type: object + $ref: /schemas/leds/common.yaml# + unevaluatedProperties: false + properties: + reg: + description: + Output channel for the LED (1-18 maps to LED outputs D1-D18). + Unit-address must be in hexadecimal (1-12 hex = 1-18 decimal). + For aggregated LED control, define only one LED node with reg = <0x1> + and use led-sources to list all controlled outputs. Only reg 1 should + be present when using led-sources. + items: + - minimum: 1 + maximum: 18 + + required: + - reg + + if: + required: + - led-sources + then: + properties: + reg: + items: + - const: 1 + +required: + - compatible + - reg + - '#address-cells' + - '#size-cells' + +additionalProperties: false + +examples: + - | + // Independent LEDs + #include + #include + + i2c { + #address-cells = <1>; + #size-cells = <0>; + + led-controller@1c { + compatible = "adi,ltc3220"; + reg = <0x1c>; + #address-cells = <1>; + #size-cells = <0>; + reset-gpios = <&gpio 17 GPIO_ACTIVE_LOW>; + + led@1 { + reg = <0x1>; + function = LED_FUNCTION_INDICATOR; + function-enumerator = <1>; + }; + + led@2 { + reg = <0x2>; + function = LED_FUNCTION_INDICATOR; + function-enumerator = <2>; + }; + + led@3 { + reg = <0x3>; + function = LED_FUNCTION_INDICATOR; + function-enumerator = <3>; + }; + }; + }; + + - | + // Aggregated LED + #include + + i2c { + #address-cells = <1>; + #size-cells = <0>; + + led-controller@1c { + compatible = "adi,ltc3220"; + reg = <0x1c>; + #address-cells = <1>; + #size-cells = <0>; + + led@1 { + reg = <0x1>; + led-sources = <0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xa 0xb 0xc 0xd 0xe 0xf 0x10 0x11 0x12>; + function = LED_FUNCTION_BACKLIGHT; + }; + }; + }; + +... diff --git a/MAINTAINERS b/MAINTAINERS index 15011f5752a9..69bd487d3076 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15380,6 +15380,13 @@ W: https://ez.analog.com/linux-software-drivers F: Documentation/devicetree/bindings/iio/temperature/adi,ltc2983.yaml F: drivers/iio/temperature/ltc2983.c +LTC3220 LED DRIVER +M: Edelweise Escala +L: linux-leds@vger.kernel.org +S: Maintained +W: https://ez.analog.com/linux-software-drivers +F: Documentation/devicetree/bindings/leds/adi,ltc3220.yaml + LTC4282 HARDWARE MONITOR DRIVER M: Nuno Sa L: linux-hwmon@vger.kernel.org From 07e028d93a57fa6217cf8eb1375ec29e7d1e4b27 Mon Sep 17 00:00:00 2001 From: Edelweise Escala Date: Thu, 9 Jul 2026 08:41:20 +0800 Subject: [PATCH 18/22] leds: ltc3220: Add Support for LTC3220 18 channel LED Driver Add driver for the LTC3220 18-channel LED driver with I2C interface, individual brightness control, and hardware-assisted blink/gradation features. Signed-off-by: Edelweise Escala Link: https://patch.msgid.link/20260709-ltc3220-driver-v13-2-9b53237642eb@analog.com Signed-off-by: Lee Jones --- MAINTAINERS | 1 + drivers/leds/Kconfig | 13 + drivers/leds/Makefile | 1 + drivers/leds/leds-ltc3220.c | 586 ++++++++++++++++++++++++++++++++++++ 4 files changed, 601 insertions(+) create mode 100644 drivers/leds/leds-ltc3220.c diff --git a/MAINTAINERS b/MAINTAINERS index 69bd487d3076..747ef98f21e8 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15386,6 +15386,7 @@ L: linux-leds@vger.kernel.org S: Maintained W: https://ez.analog.com/linux-software-drivers F: Documentation/devicetree/bindings/leds/adi,ltc3220.yaml +F: drivers/leds/leds-ltc3220.c LTC4282 HARDWARE MONITOR DRIVER M: Nuno Sa diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index f4a0a3c8c870..31b1e3ff094c 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -1000,6 +1000,19 @@ config LEDS_ST1202 Say Y to enable support for LEDs connected to LED1202 LED driver chips accessed via the I2C bus. +config LEDS_LTC3220 + tristate "LED Driver for Analog Devices Inc. LTC3220" + depends on I2C && LEDS_CLASS + select REGMAP_I2C + help + Say Y to enable support for the Analog Devices LTC3220 + 18-channel LED controller with I2C interface. + The driver supports individual LED brightness control (64 steps), + hardware-assisted blinking and gradation effects. + + To compile this driver as a module, choose M here: the module will + be called leds-ltc3220. + config LEDS_TPS6105X tristate "LED support for TI TPS6105X" depends on LEDS_CLASS diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index 7db3768912ca..a68244bd50fb 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -61,6 +61,7 @@ obj-$(CONFIG_LEDS_LP8788) += leds-lp8788.o obj-$(CONFIG_LEDS_LP8860) += leds-lp8860.o obj-$(CONFIG_LEDS_LP8864) += leds-lp8864.o obj-$(CONFIG_LEDS_LT3593) += leds-lt3593.o +obj-$(CONFIG_LEDS_LTC3220) += leds-ltc3220.o obj-$(CONFIG_LEDS_MAX5970) += leds-max5970.o obj-$(CONFIG_LEDS_MAX77650) += leds-max77650.o obj-$(CONFIG_LEDS_MAX77705) += leds-max77705.o diff --git a/drivers/leds/leds-ltc3220.c b/drivers/leds/leds-ltc3220.c new file mode 100644 index 000000000000..5060d5042fe1 --- /dev/null +++ b/drivers/leds/leds-ltc3220.c @@ -0,0 +1,586 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * LTC3220 18-Channel LED Driver + * + * Copyright 2026 Analog Devices Inc. + * + * Author: Edelweise Escala + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* LTC3220 Registers */ +#define LTC3220_COMMAND_REG 0x00 +#define LTC3220_QUICK_WRITE_MASK BIT(0) +#define LTC3220_SHUTDOWN_MASK BIT(3) + +#define LTC3220_ULED_REG(x) (0x01 + (x)) +#define LTC3220_LED_CURRENT_MASK GENMASK(5, 0) +#define LTC3220_LED_MODE_MASK GENMASK(7, 6) + +#define LTC3220_GRAD_BLINK_REG 0x13 +#define LTC3220_GRADATION_MASK GENMASK(2, 0) +#define LTC3220_GRADATION_DIRECTION_MASK BIT(0) +#define LTC3220_GRADATION_PERIOD_MASK GENMASK(2, 1) +#define LTC3220_BLINK_MASK GENMASK(4, 3) + +#define LTC3220_NUM_LEDS 18 +#define LTC3220_MAX_BRIGHTNESS 63 + +#define LTC3220_GRADATION_RAMP_TIME_240MS 240 +#define LTC3220_GRADATION_RAMP_TIME_480MS 480 + +#define LTC3220_BLINK_ON_156MS 156 +#define LTC3220_BLINK_ON_625MS 625 +#define LTC3220_BLINK_PERIOD_1250MS 1250 +#define LTC3220_BLINK_PERIOD_2500MS 2500 + +#define LTC3220_BLINK_SHORT_ON_TIME BIT(0) +#define LTC3220_BLINK_LONG_PERIOD BIT(1) + +enum ltc3220_led_mode { + LTC3220_NORMAL_MODE, + LTC3220_BLINK_MODE, + LTC3220_GRADATION_MODE, +}; + +enum ltc3220_blink_mode { + LTC3220_BLINK_MODE_625MS_1250MS, + LTC3220_BLINK_MODE_156MS_1250MS, + LTC3220_BLINK_MODE_625MS_2500MS, + LTC3220_BLINK_MODE_156MS_2500MS +}; + +enum ltc3220_gradation_mode { + LTC3220_GRADATION_MODE_DISABLED, + LTC3220_GRADATION_MODE_240MS_RAMP_TIME, + LTC3220_GRADATION_MODE_480MS_RAMP_TIME, + LTC3220_GRADATION_MODE_960MS_RAMP_TIME +}; + +static const struct regmap_config ltc3220_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = LTC3220_GRAD_BLINK_REG, + .cache_type = REGCACHE_FLAT_S, +}; + +struct ltc3220_uled_cfg { + struct led_classdev led_cdev; + u8 reg_value; + u8 led_index; + bool registered; +}; + +struct ltc3220 { + struct ltc3220_uled_cfg uled_cfg[LTC3220_NUM_LEDS]; + struct regmap *regmap; + struct mutex lock; +}; + +/* + * Set LED brightness. Hardware supports 0-63 brightness levels. + * Mode switching (blink/gradation) is handled through dedicated callbacks. + * + * In aggregated mode only a single LED (reg = 1) is registered and the + * hardware quick-write feature propagates the write to all 18 channels, so + * there is no need to update the other registers explicitly. + */ +static int __ltc3220_set_led_data(struct ltc3220 *ltc3220, + struct ltc3220_uled_cfg *uled_cfg, + enum led_brightness brightness) +{ + int ret; + + brightness &= LTC3220_LED_CURRENT_MASK; + + ret = regmap_write(ltc3220->regmap, LTC3220_ULED_REG(uled_cfg->led_index), + brightness); + if (ret) + return ret; + + uled_cfg->reg_value = brightness; + + return 0; +} + +static int ltc3220_set_led_data(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct ltc3220_uled_cfg *uled_cfg = container_of(led_cdev, struct ltc3220_uled_cfg, + led_cdev); + struct ltc3220 *ltc3220 = container_of(uled_cfg - uled_cfg->led_index, struct ltc3220, + uled_cfg[0]); + int ret; + + mutex_lock(<c3220->lock); + ret = __ltc3220_set_led_data(ltc3220, uled_cfg, brightness); + mutex_unlock(<c3220->lock); + + return ret; +} + +static enum led_brightness ltc3220_get_led_data(struct led_classdev *led_cdev) +{ + struct ltc3220_uled_cfg *uled_cfg = container_of(led_cdev, struct ltc3220_uled_cfg, + led_cdev); + + return uled_cfg->reg_value; +} + +/* + * LTC3220 pattern support for hardware-assisted breathing/gradation. + * The hardware supports 3 gradation ramp times (240ms, 480ms, 960ms) + * and can ramp up or down. The gradation period and direction are chip-global + * registers (LTC3220_GRAD_BLINK_REG), affecting all 18 channels simultaneously. + * This is a hardware limitation, not a driver bug. + * + * Pattern array interpretation: + * pattern[0].brightness = start brightness (0-63) + * pattern[0].delta_t = ramp time in milliseconds + * pattern[1].brightness = end brightness (0-63) + * pattern[1].delta_t = (optional, can be 0 or same as pattern[0].delta_t) + */ +static int ltc3220_pattern_set(struct led_classdev *led_cdev, + struct led_pattern *pattern, + u32 len, int repeat) +{ + struct ltc3220_uled_cfg *uled_cfg = container_of(led_cdev, struct ltc3220_uled_cfg, + led_cdev); + struct ltc3220 *ltc3220 = container_of(uled_cfg - uled_cfg->led_index, struct ltc3220, + uled_cfg[0]); + u8 gradation_period; + u8 start_brightness; + u8 end_brightness; + u8 gradation_val; + u8 led_mode; + bool is_increasing; + int ret; + + if (len != 2) + return -EINVAL; + + start_brightness = clamp_val(pattern[0].brightness, 0, LTC3220_LED_CURRENT_MASK); + end_brightness = clamp_val(pattern[1].brightness, 0, LTC3220_LED_CURRENT_MASK); + + is_increasing = end_brightness > start_brightness; + + if (pattern[0].delta_t == 0) + gradation_period = LTC3220_GRADATION_MODE_DISABLED; + else if (pattern[0].delta_t <= LTC3220_GRADATION_RAMP_TIME_240MS) + gradation_period = LTC3220_GRADATION_MODE_240MS_RAMP_TIME; + else if (pattern[0].delta_t <= LTC3220_GRADATION_RAMP_TIME_480MS) + gradation_period = LTC3220_GRADATION_MODE_480MS_RAMP_TIME; + else + gradation_period = LTC3220_GRADATION_MODE_960MS_RAMP_TIME; + + gradation_val = FIELD_PREP(LTC3220_GRADATION_PERIOD_MASK, gradation_period); + gradation_val |= FIELD_PREP(LTC3220_GRADATION_DIRECTION_MASK, is_increasing); + + /* + * With the ramp disabled (delta_t == 0) there is no gradation to run, + * so apply the end brightness directly in NORMAL mode instead of + * leaving the channel in gradation mode with a disabled ramp. + */ + led_mode = gradation_period == LTC3220_GRADATION_MODE_DISABLED ? + LTC3220_NORMAL_MODE : LTC3220_GRADATION_MODE; + + mutex_lock(<c3220->lock); + + ret = regmap_update_bits(ltc3220->regmap, LTC3220_GRAD_BLINK_REG, + LTC3220_GRADATION_MASK, gradation_val); + if (ret) + goto unlock; + + if (led_mode == LTC3220_GRADATION_MODE) { + ret = regmap_write(ltc3220->regmap, LTC3220_ULED_REG(uled_cfg->led_index), + start_brightness & LTC3220_LED_CURRENT_MASK); + if (ret) + goto unlock; + + ret = regmap_write(ltc3220->regmap, LTC3220_ULED_REG(uled_cfg->led_index), + FIELD_PREP(LTC3220_LED_MODE_MASK, led_mode) | + (end_brightness & LTC3220_LED_CURRENT_MASK)); + if (ret) + goto unlock; + + uled_cfg->reg_value = end_brightness; + } else { + ret = __ltc3220_set_led_data(ltc3220, uled_cfg, end_brightness); + if (ret) + goto unlock; + } + +unlock: + mutex_unlock(<c3220->lock); + return ret; +} + +static int ltc3220_pattern_clear(struct led_classdev *led_cdev) +{ + struct ltc3220_uled_cfg *uled_cfg = container_of(led_cdev, struct ltc3220_uled_cfg, + led_cdev); + struct ltc3220 *ltc3220 = container_of(uled_cfg - uled_cfg->led_index, struct ltc3220, + uled_cfg[0]); + int ret; + + mutex_lock(<c3220->lock); + + ret = regmap_update_bits(ltc3220->regmap, LTC3220_ULED_REG(uled_cfg->led_index), + LTC3220_LED_MODE_MASK, LTC3220_NORMAL_MODE); + if (ret) + goto unlock; + + ret = __ltc3220_set_led_data(ltc3220, uled_cfg, LED_OFF); + +unlock: + mutex_unlock(<c3220->lock); + return ret; +} + +/* + * LTC3220 has a global blink configuration that affects all LEDs. + * This implementation allows per-LED blink requests via sysfs, but setting + * blink on any LED reprograms the timing for all 18 channels simultaneously. + * The delay values are mapped to the hardware's discrete blink rates. + * + * HARDWARE LIMITATION: This is not a driver bug. Per-LED blink timing control + * is not possible with this hardware due to the global blink register. + */ +static int ltc3220_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct ltc3220_uled_cfg *uled_cfg = container_of(led_cdev, struct ltc3220_uled_cfg, + led_cdev); + struct ltc3220 *ltc3220 = container_of(uled_cfg - uled_cfg->led_index, struct ltc3220, + uled_cfg[0]); + u8 blink_brightness; + u8 blink_mode = 0; + int ret; + + if (*delay_on <= LTC3220_BLINK_ON_156MS) + blink_mode = LTC3220_BLINK_SHORT_ON_TIME; + + if (*delay_on + *delay_off > LTC3220_BLINK_PERIOD_1250MS) + blink_mode |= LTC3220_BLINK_LONG_PERIOD; + + switch (blink_mode) { + case LTC3220_BLINK_MODE_625MS_1250MS: + *delay_on = LTC3220_BLINK_ON_625MS; + *delay_off = LTC3220_BLINK_PERIOD_1250MS - LTC3220_BLINK_ON_625MS; + break; + case LTC3220_BLINK_MODE_156MS_1250MS: + *delay_on = LTC3220_BLINK_ON_156MS; + *delay_off = LTC3220_BLINK_PERIOD_1250MS - LTC3220_BLINK_ON_156MS; + break; + case LTC3220_BLINK_MODE_625MS_2500MS: + *delay_on = LTC3220_BLINK_ON_625MS; + *delay_off = LTC3220_BLINK_PERIOD_2500MS - LTC3220_BLINK_ON_625MS; + break; + case LTC3220_BLINK_MODE_156MS_2500MS: + *delay_on = LTC3220_BLINK_ON_156MS; + *delay_off = LTC3220_BLINK_PERIOD_2500MS - LTC3220_BLINK_ON_156MS; + break; + } + + mutex_lock(<c3220->lock); + + ret = regmap_update_bits(ltc3220->regmap, LTC3220_GRAD_BLINK_REG, + LTC3220_BLINK_MASK, FIELD_PREP(LTC3220_BLINK_MASK, blink_mode)); + if (ret) + goto unlock; + + blink_brightness = uled_cfg->reg_value ? : led_cdev->max_brightness; + + ret = regmap_write(ltc3220->regmap, LTC3220_ULED_REG(uled_cfg->led_index), + FIELD_PREP(LTC3220_LED_MODE_MASK, LTC3220_BLINK_MODE) | + (blink_brightness & LTC3220_LED_CURRENT_MASK)); + if (ret) + goto unlock; + + uled_cfg->reg_value = blink_brightness; + +unlock: + mutex_unlock(<c3220->lock); + return ret; +} + +static void ltc3220_reset_gpio_action(void *data) +{ + struct gpio_desc *reset_gpio = data; + + gpiod_set_value_cansleep(reset_gpio, 1); +} + +static int ltc3220_reset(struct ltc3220 *ltc3220, struct i2c_client *client) +{ + struct gpio_desc *reset_gpio; + int ret; + + reset_gpio = devm_gpiod_get_optional(&client->dev, "reset", GPIOD_OUT_HIGH); + if (IS_ERR(reset_gpio)) + return dev_err_probe(&client->dev, PTR_ERR(reset_gpio), "Failed on reset GPIO\n"); + + if (reset_gpio) { + usleep_range(10000, 12000); + gpiod_set_value_cansleep(reset_gpio, 0); + usleep_range(10000, 12000); + + ret = devm_add_action_or_reset(&client->dev, ltc3220_reset_gpio_action, + reset_gpio); + if (ret) + return ret; + } + + ret = regmap_write(ltc3220->regmap, LTC3220_COMMAND_REG, 0); + if (ret) + return ret; + + for (int i = 0; i < LTC3220_NUM_LEDS; i++) { + ret = regmap_write(ltc3220->regmap, LTC3220_ULED_REG(i), 0); + if (ret) + return ret; + } + + return regmap_write(ltc3220->regmap, LTC3220_GRAD_BLINK_REG, 0); +} + +static int ltc3220_suspend(struct device *dev) +{ + struct ltc3220 *ltc3220 = i2c_get_clientdata(to_i2c_client(dev)); + int ret; + + ret = regmap_update_bits(ltc3220->regmap, LTC3220_COMMAND_REG, + LTC3220_SHUTDOWN_MASK, LTC3220_SHUTDOWN_MASK); + if (ret) + return ret; + + regcache_mark_dirty(ltc3220->regmap); + + return 0; +} + +static int ltc3220_resume(struct device *dev) +{ + struct ltc3220 *ltc3220 = i2c_get_clientdata(to_i2c_client(dev)); + bool quick_write_enabled; + unsigned int command_reg; + int ret; + + ret = regmap_read(ltc3220->regmap, LTC3220_COMMAND_REG, &command_reg); + if (ret) + return ret; + + quick_write_enabled = command_reg & LTC3220_QUICK_WRITE_MASK; + + if (quick_write_enabled) { + ret = regmap_update_bits(ltc3220->regmap, LTC3220_COMMAND_REG, + LTC3220_QUICK_WRITE_MASK, 0); + if (ret) + return ret; + } + + ret = regmap_update_bits(ltc3220->regmap, LTC3220_COMMAND_REG, + LTC3220_SHUTDOWN_MASK, 0); + if (ret) + return ret; + + usleep_range(10000, 12000); + + ret = regcache_sync(ltc3220->regmap); + if (ret) + return ret; + + if (quick_write_enabled) { + ret = regmap_update_bits(ltc3220->regmap, LTC3220_COMMAND_REG, + LTC3220_QUICK_WRITE_MASK, + LTC3220_QUICK_WRITE_MASK); + if (ret) + return ret; + } + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(ltc3220_pm_ops, ltc3220_suspend, ltc3220_resume); + +static int ltc3220_probe(struct i2c_client *client) +{ + struct ltc3220 *ltc3220; + bool aggregated_led_found = false; + int num_leds = 0; + u8 led_index = 0; + int ret; + + ltc3220 = devm_kzalloc(&client->dev, sizeof(*ltc3220), GFP_KERNEL); + if (!ltc3220) + return -ENOMEM; + + ltc3220->regmap = devm_regmap_init_i2c(client, <c3220_regmap_config); + if (IS_ERR(ltc3220->regmap)) + return dev_err_probe(&client->dev, PTR_ERR(ltc3220->regmap), + "Failed to initialize regmap\n"); + + ret = devm_mutex_init(&client->dev, <c3220->lock); + if (ret) + return ret; + + i2c_set_clientdata(client, ltc3220); + + ret = ltc3220_reset(ltc3220, client); + if (ret) + return dev_err_probe(&client->dev, ret, "Failed to reset device\n"); + + /* First pass: validate configuration and set up LED structures */ + device_for_each_child_node_scoped(&client->dev, child) { + struct ltc3220_uled_cfg *led; + u32 source; + + ret = fwnode_property_read_u32(child, "reg", &source); + if (ret) + return dev_err_probe(&client->dev, ret, "Couldn't read LED address\n"); + + if (!source || source > LTC3220_NUM_LEDS) + return dev_err_probe(&client->dev, -EINVAL, "LED address out of range\n"); + + if (fwnode_property_present(child, "led-sources")) { + u32 led_sources[LTC3220_NUM_LEDS]; + int count; + + if (source != 1) + return dev_err_probe(&client->dev, -EINVAL, + "Aggregated LED out of range\n"); + + if (aggregated_led_found) + return dev_err_probe(&client->dev, -EINVAL, + "One Aggregated LED only\n"); + + count = fwnode_property_count_u32(child, "led-sources"); + if (count != LTC3220_NUM_LEDS) + return dev_err_probe(&client->dev, -EINVAL, + "Aggregated mode requires all %d outputs in led-sources, got %d\n", + LTC3220_NUM_LEDS, count); + + ret = fwnode_property_read_u32_array(child, "led-sources", + led_sources, LTC3220_NUM_LEDS); + if (ret) + return dev_err_probe(&client->dev, ret, + "Failed to read led-sources array\n"); + + /* + * Validate array contents for DT correctness. The hardware + * quick-write broadcasts to all 18 channels regardless of + * array contents, but checking helps catch DT mistakes. + */ + for (int i = 0; i < LTC3220_NUM_LEDS; i++) { + if (led_sources[i] < 1 || led_sources[i] > LTC3220_NUM_LEDS) + return dev_err_probe(&client->dev, -EINVAL, + "Invalid output %u in led-sources\n", + led_sources[i]); + } + + aggregated_led_found = true; + } + + num_leds++; + + /* LED node reg/index/address goes from 1 to 18 */ + led_index = source - 1; + led = <c3220->uled_cfg[led_index]; + + if (led->registered) + return dev_err_probe(&client->dev, -EINVAL, + "Duplicate LED reg %u found\n", source); + + led->registered = true; + led->led_index = led_index; + led->reg_value = 0; + led->led_cdev.brightness_set_blocking = ltc3220_set_led_data; + led->led_cdev.brightness_get = ltc3220_get_led_data; + led->led_cdev.max_brightness = LTC3220_MAX_BRIGHTNESS; + led->led_cdev.blink_set = ltc3220_blink_set; + led->led_cdev.pattern_set = ltc3220_pattern_set; + led->led_cdev.pattern_clear = ltc3220_pattern_clear; + } + + /* + * Aggregated LED mode uses hardware quick-write to control all 18 LEDs + * simultaneously. This is mutually exclusive with individual LED control. + * See Documentation/devicetree/bindings/leds/adi,ltc3220.yaml for details + * on how to configure aggregated LED mode. + */ + if (aggregated_led_found && num_leds > 1) + return dev_err_probe(&client->dev, -EINVAL, + "Aggregated LED must be the only LED node\n"); + + if (num_leds == 0) + return dev_err_probe(&client->dev, -EINVAL, + "No LED nodes found in device tree\n"); + + if (aggregated_led_found) { + ret = regmap_update_bits(ltc3220->regmap, + LTC3220_COMMAND_REG, + LTC3220_QUICK_WRITE_MASK, + LTC3220_QUICK_WRITE_MASK); + if (ret) + return dev_err_probe(&client->dev, ret, + "Failed to set quick write mode\n"); + } + + /* Second pass: register LEDs after validation */ + device_for_each_child_node_scoped(&client->dev, child) { + struct led_init_data init_data = {}; + struct ltc3220_uled_cfg *led; + u32 source; + + ret = fwnode_property_read_u32(child, "reg", &source); + if (ret) + return ret; + + if (!source || source > LTC3220_NUM_LEDS) + return dev_err_probe(&client->dev, -EINVAL, + "LED address out of range in second pass\n"); + + init_data.fwnode = child; + init_data.devicename = "ltc3220"; + + led_index = source - 1; + led = <c3220->uled_cfg[led_index]; + + ret = devm_led_classdev_register_ext(&client->dev, &led->led_cdev, &init_data); + if (ret) + return dev_err_probe(&client->dev, ret, "Failed to register LED class\n"); + } + + return 0; +} + +static const struct of_device_id ltc3220_of_match[] = { + { .compatible = "adi,ltc3220" }, + { } +}; +MODULE_DEVICE_TABLE(of, ltc3220_of_match); + +static struct i2c_driver ltc3220_led_driver = { + .driver = { + .name = "ltc3220", + .of_match_table = ltc3220_of_match, + .pm = pm_sleep_ptr(<c3220_pm_ops), + }, + .probe = ltc3220_probe, +}; +module_i2c_driver(ltc3220_led_driver); + +MODULE_AUTHOR("Edelweise Escala "); +MODULE_DESCRIPTION("LED driver for LTC3220 controllers"); +MODULE_LICENSE("GPL"); From 2ebd1cb772b00d1223895718155a03af4c56f396 Mon Sep 17 00:00:00 2001 From: Jonas Rebmann Date: Mon, 13 Jul 2026 10:50:28 +0200 Subject: [PATCH 19/22] dt-bindings: leds: Add default-intensity property Document the default-intensity property to set a default color on multicolor LEDs. Update pwm-multicolor to support it and update the example to turn the LED red on boot. Signed-off-by: Jonas Rebmann Acked-by: Conor Dooley Link: https://patch.msgid.link/20260713-multicolor-default-v3-1-fb43f3f789cf@pengutronix.de Signed-off-by: Lee Jones --- Documentation/devicetree/bindings/leds/common.yaml | 10 ++++++++++ .../devicetree/bindings/leds/leds-pwm-multicolor.yaml | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/Documentation/devicetree/bindings/leds/common.yaml b/Documentation/devicetree/bindings/leds/common.yaml index e148b4277a6f..a0ef2f2f0724 100644 --- a/Documentation/devicetree/bindings/leds/common.yaml +++ b/Documentation/devicetree/bindings/leds/common.yaml @@ -73,6 +73,16 @@ properties: - keep default: off + default-intensity: + description: + The initial intensity of the LED color component. As the intensity of + each sub-LED is multiplied with the overall brightness, without this + property on a sub-LED, it may effectively be initialized at minimum + brightness regardless of its linux,default-trigger and default-brightness + properties. + $ref: /schemas/types.yaml#/definitions/uint32 + default: 0 + linux,default-trigger: description: This parameter, if present, is a string defining the trigger assigned to diff --git a/Documentation/devicetree/bindings/leds/leds-pwm-multicolor.yaml b/Documentation/devicetree/bindings/leds/leds-pwm-multicolor.yaml index a31a202afe5c..7bf687c89411 100644 --- a/Documentation/devicetree/bindings/leds/leds-pwm-multicolor.yaml +++ b/Documentation/devicetree/bindings/leds/leds-pwm-multicolor.yaml @@ -45,6 +45,8 @@ properties: color: true + default-intensity: true + required: - pwms - color @@ -63,12 +65,14 @@ examples: multi-led { color = ; + linux,default-trigger = "default-on"; function = LED_FUNCTION_INDICATOR; max-brightness = <65535>; led-red { pwms = <&pwm1 0 1000000>; color = ; + default-intensity = <65535>; }; led-green { From b6e08e0ad4cfafab2c2070456e7eeba17608a35d Mon Sep 17 00:00:00 2001 From: Ninad Naik Date: Tue, 14 Jul 2026 01:37:32 +0530 Subject: [PATCH 20/22] leds: bcm63138: Use %pe to print pinctrl error instead of %ld Pass the pinctrl error pointer directly to %pe to print the symbolic error name. Detected by Coccinelle. Tested by compiling. Signed-off-by: Ninad Naik Link: https://patch.msgid.link/20260713200732.2470666-1-ninadnaik07@gmail.com Signed-off-by: Lee Jones --- drivers/leds/blink/leds-bcm63138.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/leds/blink/leds-bcm63138.c b/drivers/leds/blink/leds-bcm63138.c index 45c0662df933..12b70ee31b9a 100644 --- a/drivers/leds/blink/leds-bcm63138.c +++ b/drivers/leds/blink/leds-bcm63138.c @@ -236,8 +236,8 @@ static void bcm63138_leds_create_led(struct bcm63138_leds *leds, pinctrl = devm_pinctrl_get_select_default(led->cdev.dev); if (IS_ERR(pinctrl) && PTR_ERR(pinctrl) != -ENODEV) { - dev_warn(led->cdev.dev, "Failed to select %pOF pinctrl: %ld\n", - np, PTR_ERR(pinctrl)); + dev_warn(led->cdev.dev, "Failed to select %pOF pinctrl: %pe\n", + np, pinctrl); } bit = BIT(led->pin); From 98c5c7b0d4269ecabfc86b8b49ffcfa47979e99d Mon Sep 17 00:00:00 2001 From: Arnd Bergmann Date: Fri, 10 Jul 2026 23:18:51 +0200 Subject: [PATCH 21/22] leds: gpio: Make legacy gpiolib interface optional There are still a handful of ancient mips/armv5/sh boards that use the gpio_led:gpio member to pass an old-style gpio number, but all modern users have been converted to gpio descriptors. While the CONFIG_GPIOLIB_LEGACY option that guards devm_gpio_request_one() and related helpers is currently turned on in all kernel builds, the plan is to only enable it on the few platforms that actually pass gpio numbers in any platform_data. Split out the legacy portion of the platform_data handling into a custom helper function that is guarded with in #ifdef block, to allow the the leds-gpio driver to compile cleanly when CONFIG_GPIOLIB_LEGACY gets turned off. Once the last user is converted, this function can be removed. Link: https://lore.kernel.org/all/e9252384-a55c-4a91-9c61-06e05a0b2ce4@app.fastmail.com/ Signed-off-by: Arnd Bergmann Reviewed-by: Linus Walleij Reviewed-by: Bartosz Golaszewski Reviewed-by: Andy Shevchenko Acked-by: Dmitry Torokhov # for input Link: https://patch.msgid.link/20260710211854.1371746-4-arnd@kernel.org Signed-off-by: Lee Jones --- drivers/leds/leds-gpio.c | 52 +++++++++++++++++++++++++++------------- include/linux/leds.h | 2 ++ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/drivers/leds/leds-gpio.c b/drivers/leds/leds-gpio.c index a3428b22de3a..9cbcf7e40a15 100644 --- a/drivers/leds/leds-gpio.c +++ b/drivers/leds/leds-gpio.c @@ -9,8 +9,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -212,7 +212,6 @@ static struct gpio_desc *gpio_led_get_gpiod(struct device *dev, int idx, const struct gpio_led *template) { struct gpio_desc *gpiod; - int ret; /* * This means the LED does not come from the device tree @@ -223,16 +222,29 @@ static struct gpio_desc *gpio_led_get_gpiod(struct device *dev, int idx, gpiod = devm_gpiod_get_index_optional(dev, NULL, idx, GPIOD_OUT_LOW); if (IS_ERR(gpiod)) return gpiod; - if (gpiod) { - gpiod_set_consumer_name(gpiod, template->name); - return gpiod; - } - /* - * This is the legacy code path for platform code that - * still uses GPIO numbers. Ultimately we would like to get - * rid of this block completely. - */ + gpiod_set_consumer_name(gpiod, template->name); + return gpiod; +} + +#ifdef CONFIG_GPIOLIB_LEGACY +/* + * This is the legacy code path for platform code that still uses + * GPIO numbers, mainly MIPS and SuperH board files. + * Ultimately we would like to get rid of this block completely. + * + * ppc44x-warp sets the template->gpiod directly instead of + * adding a lookup table or device properties. This is not + * much better. + */ +static struct gpio_desc *gpio_led_get_legacy_gpiod(struct device *dev, int idx, + const struct gpio_led *template) +{ + struct gpio_desc *gpiod; + int ret; + + if (template->gpiod) + return template->gpiod; /* skip leds that aren't available */ if (!gpio_is_valid(template->gpio)) @@ -252,6 +264,13 @@ static struct gpio_desc *gpio_led_get_gpiod(struct device *dev, int idx, return gpiod; } +#else +static struct gpio_desc *gpio_led_get_legacy_gpiod(struct device *dev, int idx, + const struct gpio_led *template) +{ + return template->gpiod ?: ERR_PTR(-ENOENT); +} +#endif static int gpio_led_probe(struct platform_device *pdev) { @@ -270,14 +289,13 @@ static int gpio_led_probe(struct platform_device *pdev) const struct gpio_led *template = &pdata->leds[i]; struct gpio_led_data *led_dat = &priv->leds[i]; - if (template->gpiod) - led_dat->gpiod = template->gpiod; - else + led_dat->gpiod = gpio_led_get_gpiod(dev, i, template); + if (!led_dat->gpiod) led_dat->gpiod = - gpio_led_get_gpiod(dev, i, template); + gpio_led_get_legacy_gpiod(dev, i, template); if (IS_ERR(led_dat->gpiod)) { - dev_info(dev, "Skipping unavailable LED gpio %d (%s)\n", - template->gpio, template->name); + dev_info(dev, "Skipping unavailable LED gpio %s\n", + template->name); continue; } diff --git a/include/linux/leds.h b/include/linux/leds.h index bf31c246d9e2..a515f075c29a 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -680,8 +680,10 @@ typedef int (*gpio_blink_set_t)(struct gpio_desc *desc, int state, struct gpio_led { const char *name; const char *default_trigger; +#ifdef CONFIG_GPIOLIB_LEGACY unsigned gpio; unsigned active_low : 1; +#endif unsigned retain_state_suspended : 1; unsigned panic_indicator : 1; unsigned default_state : 2; From 8d6b6c05b8e33d11e3fb3203309385e1a9cceecd Mon Sep 17 00:00:00 2001 From: Cosmo Chou Date: Wed, 15 Jul 2026 16:07:47 +0800 Subject: [PATCH 22/22] leds: pca9532: Fix phantom device registration on missing hardware The initial PWM and PSC register writes in pca9532_configure() do not check the return values of i2c_smbus_write_byte_data(). If the I2C device is physically absent from the bus, the write fails with -ENXIO. However, the driver ignores this error and allows probe() to complete successfully. This results in the registration of phantom LED class devices and gpiochips backed by non-existent hardware. Subsequent GPIO reads from these phantom chips return bogus values (due to -ENXIO being truncated to an unsigned char in pca9532_gpio_get_value()), silently corrupting hardware state tracking in userspace. Propagate the I2C write failures back to probe() so the driver core can gracefully abort binding and release devres-managed resources. Fixes: e14fa82439d3 ("leds: Add pca9532 led driver") Signed-off-by: Cosmo Chou Reviewed-by: Bartosz Golaszewski Link: https://patch.msgid.link/20260715080747.1638097-1-chou.cosmo@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-pca9532.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/drivers/leds/leds-pca9532.c b/drivers/leds/leds-pca9532.c index 2d37e00e459d..b2e081c8f139 100644 --- a/drivers/leds/leds-pca9532.c +++ b/drivers/leds/leds-pca9532.c @@ -397,10 +397,14 @@ static int pca9532_configure(struct i2c_client *client, for (i = 0; i < 2; i++) { data->pwm[i] = pdata->pwm[i]; data->psc[i] = pdata->psc[i]; - i2c_smbus_write_byte_data(client, PCA9532_REG_PWM(maxleds, i), - data->pwm[i]); - i2c_smbus_write_byte_data(client, PCA9532_REG_PSC(maxleds, i), - data->psc[i]); + err = i2c_smbus_write_byte_data(client, PCA9532_REG_PWM(maxleds, i), + data->pwm[i]); + if (err < 0) + return err; + err = i2c_smbus_write_byte_data(client, PCA9532_REG_PSC(maxleds, i), + data->psc[i]); + if (err < 0) + return err; } data->hw_blink = true;