From 35cbd34a6a05cc53882b1b7ca6f338a93ab29fcb Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Fri, 14 Aug 2026 10:16:54 +0100 Subject: [PATCH 01/49] leds: st1202: Add hardware-accelerated blink support Implement blink_set() to enable hardware-accelerated blinking via the timer trigger. The LED1202 pattern engine is used to produce a two-step sequence: full brightness for delay_on, off for delay_off, repeating indefinitely. Requested delays are clamped to the hardware range [22ms, 5610ms] then rounded up to the nearest 22ms step. Clamping before rounding prevents integer overflow in roundup() for extreme input values; since ST1202_MILLIS_PATTERN_DUR_MAX is an exact multiple of ST1202_MILLIS_PATTERN_DUR_MIN, rounding a clamped value cannot exceed the maximum. A zero delay is replaced with the default of 500ms independently for each of delay_on and delay_off. The LED1202 pattern sequencer is global and its timing registers are shared across all channels, so only one blink configuration can be active at a time. Other active channels have their PWM slots zeroed for both pattern steps so they remain dark rather than outputting unintended values when the sequencer runs. The target channel's ILED register is set to full brightness and the channel is enabled, since the timer trigger deactivates the current trigger before calling blink_set which would otherwise leave the channel disabled. Signed-off-by: Manuel Fombuena Link: https://patch.msgid.link/GV1PR08MB84973F79E871176EB387C377C5DA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 89 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 168df5ecf27b..ead144155849 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -15,6 +15,7 @@ #include #include +#define ST1202_BLINK_DEFAULT_DELAY 500 #define ST1202_CHAN_DISABLE_ALL 0x00 #define ST1202_CHAN_ENABLE_HIGH 0x03 #define ST1202_CHAN_ENABLE_LOW 0x02 @@ -275,6 +276,93 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, return 0; } +static int st1202_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct st1202_led *led = cdev_to_st1202_led(led_cdev); + struct st1202_chip *chip = led->chip; + unsigned long on, off; + int ret; + + if (!*delay_on) + *delay_on = ST1202_BLINK_DEFAULT_DELAY; + if (!*delay_off) + *delay_off = ST1202_BLINK_DEFAULT_DELAY; + + on = *delay_on; + off = *delay_off; + + on = clamp_val(on, ST1202_MILLIS_PATTERN_DUR_MIN, ST1202_MILLIS_PATTERN_DUR_MAX); + off = clamp_val(off, ST1202_MILLIS_PATTERN_DUR_MIN, ST1202_MILLIS_PATTERN_DUR_MAX); + on = roundup(on, ST1202_MILLIS_PATTERN_DUR_MIN); + off = roundup(off, ST1202_MILLIS_PATTERN_DUR_MIN); + + guard(mutex)(&chip->lock); + + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_SHFT); + if (ret) + return ret; + + /* Zero out PWM for all other active channels to prevent them from blinking */ + for (int chan = 0; chan < ST1202_MAX_LEDS; chan++) { + if (!chip->leds[chan].is_active || chan == led->led_num) + continue; + + ret = st1202_pwm_pattern_write(chip, chan, 0, LED_OFF); + if (ret) + return ret; + + ret = st1202_pwm_pattern_write(chip, chan, 1, LED_OFF); + if (ret) + return ret; + } + + ret = st1202_pwm_pattern_write(chip, led->led_num, 0, ST1202_PATTERN_PWM_FULL); + if (ret) + return ret; + + ret = st1202_pwm_pattern_write(chip, led->led_num, 1, LED_OFF); + if (ret) + return ret; + + ret = st1202_duration_pattern_write(chip, 0, on); + if (ret) + return ret; + + ret = st1202_duration_pattern_write(chip, 1, off); + if (ret) + return ret; + + for (int pattern = 2; pattern < ST1202_MAX_PATTERNS; pattern++) { + ret = st1202_write_reg(chip, ST1202_PATTERN_DUR + pattern, 0); + if (ret) + return ret; + } + + ret = st1202_write_reg(chip, ST1202_PATTERN_REP, U8_MAX); + if (ret) + return ret; + + ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, U8_MAX); + if (ret) + return ret; + + ret = __st1202_channel_set(chip, led->led_num, true); + if (ret) + return ret; + + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, + ST1202_CONFIG_REG_PATSR | ST1202_CONFIG_REG_PATS | + ST1202_CONFIG_REG_SHFT); + if (ret) + return ret; + + *delay_on = on; + *delay_off = off; + + return 0; +} + static int st1202_dt_init(struct st1202_chip *chip) { struct device *dev = &chip->client->dev; @@ -301,6 +389,7 @@ static int st1202_dt_init(struct st1202_chip *chip) led->led_cdev.pattern_set = st1202_led_pattern_set; led->led_cdev.pattern_clear = st1202_led_pattern_clear; led->led_cdev.default_trigger = "pattern"; + led->led_cdev.blink_set = st1202_blink_set; led->led_cdev.brightness_set = st1202_brightness_set; led->led_cdev.brightness_get = st1202_brightness_get; } From 4805e5ec50d0b355370e3d1ea906af4fc758accf Mon Sep 17 00:00:00 2001 From: Manush Prajwal Date: Fri, 28 Aug 2026 14:30:13 +0530 Subject: [PATCH 02/49] leds: max77705: Use fwnode_for_each_child_node_scoped() in max77705_add_led() Rework the subled loop in max77705_add_led() around fwnode_for_each_child_node_scoped() instead of adding a manual fwnode_handle_put(child) before the early return on a max77705_parse_subled() failure, so the reference on the current child fwnode is released automatically on every exit path from the loop. child is never used outside the loop, so the now-unused outer child declaration is dropped along with it. Suggested-by: Krzysztof Kozlowski Fixes: 3b6eaa3db567 ("leds: Use fwnode_for_each_child_node() instead") Signed-off-by: Manush Prajwal Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/6a914e1e.7d702ed2.1273e8.2a52@mx.google.com Signed-off-by: Lee Jones --- drivers/leds/leds-max77705.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/leds/leds-max77705.c b/drivers/leds/leds-max77705.c index 1e2054c1bf80..4fd803c95989 100644 --- a/drivers/leds/leds-max77705.c +++ b/drivers/leds/leds-max77705.c @@ -160,7 +160,6 @@ static int max77705_add_led(struct device *dev, struct regmap *regmap, struct fw struct max77705_led *led; struct led_classdev *cdev; struct mc_subled *info; - struct fwnode_handle *child; struct led_init_data init_data = {}; led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); @@ -191,7 +190,7 @@ static int max77705_add_led(struct device *dev, struct regmap *regmap, struct fw cdev->brightness_set_blocking = max77705_led_brightness_set_multi; cdev->blink_set = max77705_rgb_blink; - fwnode_for_each_child_node(np, child) { + fwnode_for_each_child_node_scoped(np, child) { ret = max77705_parse_subled(dev, child, &info[i]); if (ret < 0) return ret; From 8b35269884491eb07669219468ad425bf9b9f455 Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Mon, 7 Sep 2026 10:24:36 +0530 Subject: [PATCH 03/49] leds: ss4200: Fix typo "retrive" in comment Correct "retrive" to "retrieve", reported by scripts/checkpatch.pl using the misspelling list in scripts/spelling.txt. Only touches a comment, no code changes. Since v1 the subject carries the driver name and a capitalised description, to match the LED subsystem convention. Assisted-by: Cursor:claude-opus-5 Signed-off-by: Hemanth Selam Link: https://patch.msgid.link/20260907045436.18039-1-hemanth.selam@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-ss4200.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/leds/leds-ss4200.c b/drivers/leds/leds-ss4200.c index f24ca75c7cb1..c782339b1330 100644 --- a/drivers/leds/leds-ss4200.c +++ b/drivers/leds/leds-ss4200.c @@ -339,7 +339,7 @@ static void ich7_lpc_cleanup(struct device *dev) /* * The OS has determined that the LPC of the Intel ICH7 Southbridge is present - * so we can retrive the required operational information and prepare the GPIO. + * so we can retrieve the required operational information and prepare the GPIO. */ static struct pci_dev *nas_gpio_pci_dev; static int ich7_lpc_probe(struct pci_dev *dev, From ceb3263b276ea6d9859e31ab6e556a9dc1649843 Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Mon, 7 Sep 2026 13:24:36 -0700 Subject: [PATCH 04/49] leds: syscon: Use device property API Replace the DT-specific of_property_read_u32() calls and the dev_of_node()/of_fwnode_handle() roundtrip with their device/fwnode equivalents. Behaviour is unchanged for device tree: device_property_read_u32() and dev_fwnode() operate on the same node, and the fallback from "reg" to "offset" is preserved. Use dev_fwnode() for init_data.fwnode; unlike a child node from a scoped iterator, the platform device's own node is held by the driver core for the device lifetime. Assisted-by: opencode:deepseek-v4-flash-free Signed-off-by: Rosen Penev Link: https://patch.msgid.link/20260907202436.42304-1-rosenp@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-syscon.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/drivers/leds/leds-syscon.c b/drivers/leds/leds-syscon.c index d633ad519d0c..741850f35f19 100644 --- a/drivers/leds/leds-syscon.c +++ b/drivers/leds/leds-syscon.c @@ -56,7 +56,6 @@ static int syscon_led_probe(struct platform_device *pdev) { struct led_init_data init_data = {}; struct device *dev = &pdev->dev; - struct device_node *np = dev_of_node(dev); struct device *parent; struct regmap *map; struct syscon_led *sled; @@ -81,13 +80,13 @@ static int syscon_led_probe(struct platform_device *pdev) sled->map = map; - if (of_property_read_u32(np, "reg", &sled->offset) && - of_property_read_u32(np, "offset", &sled->offset)) + if (device_property_read_u32(dev, "reg", &sled->offset) && + device_property_read_u32(dev, "offset", &sled->offset)) return -EINVAL; - if (of_property_read_u32(np, "mask", &sled->mask)) + if (device_property_read_u32(dev, "mask", &sled->mask)) return -EINVAL; - init_data.fwnode = of_fwnode_handle(np); + init_data.fwnode = dev_fwnode(dev); state = led_init_default_state_get(init_data.fwnode); switch (state) { From 57d0756f1cd0332b003aa2cbac16b9d7753acd59 Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Mon, 7 Sep 2026 13:19:40 -0700 Subject: [PATCH 05/49] leds: is31fl32xx: Use fwnode/device property APIs Convert the DT-specific helpers in is31fl32xx_parse_dt() and is31fl32xx_parse_child_dt() to their fwnode/device equivalents. Behaviour is unchanged for device tree: device_property_read_bool(), device_for_each_child_node_scoped() and device_get_child_node_count() operate on the same nodes and keep the skip of unavailable children. Use the %pfwP format specifier in error messages, as child is now a struct fwnode_handle and %pOF would misread it as a device_node. Assisted-by: opencode:deepseek-v4-flash-free Signed-off-by: Rosen Penev Link: https://patch.msgid.link/20260907201940.40524-1-rosenp@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-is31fl32xx.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/drivers/leds/leds-is31fl32xx.c b/drivers/leds/leds-is31fl32xx.c index 6c8d6b833260..285085d27453 100644 --- a/drivers/leds/leds-is31fl32xx.c +++ b/drivers/leds/leds-is31fl32xx.c @@ -378,23 +378,23 @@ static int is31fl32xx_init_regs(struct is31fl32xx_priv *priv) } static int is31fl32xx_parse_child_dt(const struct device *dev, - const struct device_node *child, + const struct fwnode_handle *child, struct is31fl32xx_led_data *led_data) { struct led_classdev *cdev = &led_data->cdev; int ret = 0; u32 reg; - ret = of_property_read_u32(child, "reg", ®); + ret = fwnode_property_read_u32(child, "reg", ®); if (ret || reg < 1 || reg > led_data->priv->cdef->channels) { dev_err(dev, - "Child node %pOF does not have a valid reg property\n", + "Child node %pfwP does not have a valid reg property\n", child); return -EINVAL; } led_data->channel = reg; - of_property_read_u32(child, "led-max-microamp", &led_data->max_microamp); + fwnode_property_read_u32(child, "led-max-microamp", &led_data->max_microamp); cdev->brightness_set_blocking = is31fl32xx_brightness_set; @@ -422,7 +422,7 @@ static int is31fl32xx_parse_dt(struct device *dev, int ret = 0; if ((cdef->output_frequency_setting_reg != IS31FL32XX_REG_NONE) && - of_property_read_bool(dev_of_node(dev), "issi,22khz-pwm")) { + device_property_read_bool(dev, "issi,22khz-pwm")) { ret = is31fl32xx_write(priv, cdef->output_frequency_setting_reg, IS31FL32XX_PWM_FREQUENCY_22KHZ); @@ -433,7 +433,7 @@ static int is31fl32xx_parse_dt(struct device *dev, } } - for_each_available_child_of_node_scoped(dev_of_node(dev), child) { + device_for_each_child_node_scoped(dev, child) { struct led_init_data init_data = {}; struct is31fl32xx_led_data *led_data = &priv->leds[priv->num_leds]; @@ -451,17 +451,17 @@ static int is31fl32xx_parse_dt(struct device *dev, led_data->channel); if (other_led_data) { dev_err(dev, - "Node %pOF 'reg' conflicts with another LED\n", + "Node %pfwP 'reg' conflicts with another LED\n", child); return -EINVAL; } - init_data.fwnode = of_fwnode_handle(child); + init_data.fwnode = child; ret = devm_led_classdev_register_ext(dev, &led_data->cdev, &init_data); if (ret) { - dev_err(dev, "Failed to register LED for %pOF: %d\n", + dev_err(dev, "Failed to register LED for %pfwP: %d\n", child, ret); return ret; } @@ -576,7 +576,7 @@ static int is31fl32xx_probe(struct i2c_client *client) cdef = device_get_match_data(dev); - count = of_get_available_child_count(dev_of_node(dev)); + count = device_get_child_node_count(dev); if (!count) return -EINVAL; From f9879086c4eb4b3b0f4baa80139a5720b99b229e Mon Sep 17 00:00:00 2001 From: Miles Krause Date: Thu, 10 Sep 2026 15:43:20 -0400 Subject: [PATCH 06/49] leds: lp8860: Fix device_node leak in lp8860_probe() lp8860_probe() looks up the driver's single LED child node with of_get_next_available_child() and hands it to the LED core as init_data.fwnode, but never drops the reference that lookup returned: child_node = of_get_next_available_child(np, NULL); if (!child_node) return -EINVAL; ... init_data.fwnode = of_fwnode_handle(child_node); of_get_next_available_child() returns the node with its refcount incremented, and the LED core does not take a reference of its own: led_classdev_register_ext() only reads properties out of init_data.fwnode and then stores the bare pointer with device_set_node(). The reference therefore stays owned by the driver for as long as it holds the node. Nothing in lp8860_probe() ever releases it, so it is leaked on every error return taken after the lookup - the enable GPIO, the vled regulator, devm_mutex_init(), the regmap allocation, the optional EEPROM programming and the LED class registration - and on a fully successful probe alike. A device that binds and unbinds repeatedly leaks one device_node reference per bind. Sibling drivers already get this right: leds-lm3692x.c, from the same TI LED family, calls fwnode_handle_put(init_data.fwnode) once after devm_led_classdev_register_ext() to cover both outcomes, while leds-ktd2692.c and leds-aat1290.c take the node with __free(device_node). Use __free(device_node) here too. Unlike a single of_node_put() at the end of probe it also covers the intermediate error returns, and it keeps the node alive for the whole function, which the LED core still needs when it reads the child's properties during registration. Fixes: 99ca0ea57309 ("leds: lp8860: Use generic support for composing LED names") Signed-off-by: Miles Krause Link: https://patch.msgid.link/20260910-leds-lp8860-of-node-leak-v1-1-d333cff4345b@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-lp8860.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/leds/leds-lp8860.c b/drivers/leds/leds-lp8860.c index 69f064781f69..f6e4227de903 100644 --- a/drivers/leds/leds-lp8860.c +++ b/drivers/leds/leds-lp8860.c @@ -7,6 +7,7 @@ * Author: Dan Murphy */ +#include #include #include #include @@ -274,7 +275,6 @@ static int lp8860_probe(struct i2c_client *client) int ret; struct lp8860_led *led; struct device_node *np = dev_of_node(&client->dev); - struct device_node *child_node; struct led_init_data init_data = {}; struct gpio_desc *enable_gpio; @@ -282,7 +282,8 @@ static int lp8860_probe(struct i2c_client *client) if (!led) return -ENOMEM; - child_node = of_get_next_available_child(np, NULL); + struct device_node *child_node __free(device_node) = + of_get_next_available_child(np, NULL); if (!child_node) return -EINVAL; From c73e9f04445f361d5f19ca2edfde742ce18348ef Mon Sep 17 00:00:00 2001 From: Myeonghun Pak Date: Thu, 10 Sep 2026 15:45:25 -0400 Subject: [PATCH 07/49] leds: ktd2692: Pass context to regulator cleanup regulator_disable_action() retrieves the driver context from device driver data. However, ktd2692_parse_dt() registers the action before ktd2692_probe() stores the context with platform_set_drvdata(). If devm_add_action_or_reset() cannot allocate the action, it invokes the callback immediately. A later probe failure also invokes it during managed-resource unwinding. Both paths dereference a NULL context. Pass the already allocated context directly to the action and retain the device pointer in it for error reporting. The context is allocated before the action is registered, so reverse-order devres unwinding keeps it alive through the callback. This issue was identified during our ongoing static-analysis research while reviewing kernel code. Fixes: ee78b9360e14 ("leds: ktd2692: Fix an error handling path") Cc: stable@vger.kernel.org Co-developed-by: Ijae Kim Signed-off-by: Ijae Kim Signed-off-by: Myeonghun Pak Link: https://patch.msgid.link/20260910194525.99654-1-mhun512@gmail.com Signed-off-by: Lee Jones --- drivers/leds/flash/leds-ktd2692.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/drivers/leds/flash/leds-ktd2692.c b/drivers/leds/flash/leds-ktd2692.c index 22fbfccd4873..43d4d55503ab 100644 --- a/drivers/leds/flash/leds-ktd2692.c +++ b/drivers/leds/flash/leds-ktd2692.c @@ -69,6 +69,8 @@ static const struct expresswire_timing ktd2692_timing = { }; struct ktd2692_context { + struct device *dev; + /* Common ExpressWire properties (ctrl GPIO and timing) */ struct expresswire_common_props props; @@ -196,13 +198,12 @@ static void ktd2692_setup(struct ktd2692_context *led) static void regulator_disable_action(void *_data) { - struct device *dev = _data; - struct ktd2692_context *led = dev_get_drvdata(dev); + struct ktd2692_context *led = _data; int ret; ret = regulator_disable(led->regulator); if (ret) - dev_err(dev, "Failed to disable supply: %d\n", ret); + dev_err(led->dev, "Failed to disable supply: %d\n", ret); } static int ktd2692_parse_dt(struct ktd2692_context *led, struct device *dev, @@ -233,7 +234,7 @@ static int ktd2692_parse_dt(struct ktd2692_context *led, struct device *dev, dev_err(dev, "Failed to enable supply: %d\n", ret); } else { ret = devm_add_action_or_reset(dev, - regulator_disable_action, dev); + regulator_disable_action, led); if (ret) return ret; } @@ -290,6 +291,7 @@ static int ktd2692_probe(struct platform_device *pdev) if (!led) return -ENOMEM; + led->dev = &pdev->dev; fled_cdev = &led->fled_cdev; led_cdev = &fled_cdev->led_cdev; led->props.timing = ktd2692_timing; From 1b75f038e5f71ed32ce6593b5763f9776ec58a25 Mon Sep 17 00:00:00 2001 From: Haibo Chen Date: Mon, 27 Jul 2026 17:57:54 +0800 Subject: [PATCH 08/49] leds: pca9532: Check i2c_smbus_read_byte_data() return value Coverity report INTEGER_OVERFLOW (tainted_data_return -> cast_underflow -> overflow) in pca9532_getled(). pca9532_getled() stored the return value of i2c_smbus_read_byte_data() into a char and used it directly to compute the LED state without checking for an error. i2c_smbus_read_byte_data() returns a negative error code on failure. On an I2C read error the negative value was assigned to an unsigned type, triggering an integer underflow/overflow, and a meaningless LED state was returned to the caller. Read the value into an int, check for the error, and fall back to PCA9532_OFF while emitting a warning when the read fails. Fixes: 28c5fe99016d ("leds: pca9532: Extend pca9532 device tree support") Assisted-by: VeroCoder:vero-1 [cli] Signed-off-by: Haibo Chen Reviewed-by: Frank Li Link: https://patch.msgid.link/20260727-led-fix-v1-1-d635c5037109@nxp.com Signed-off-by: Lee Jones --- drivers/leds/leds-pca9532.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drivers/leds/leds-pca9532.c b/drivers/leds/leds-pca9532.c index b2e081c8f139..1133fa348e24 100644 --- a/drivers/leds/leds-pca9532.c +++ b/drivers/leds/leds-pca9532.c @@ -298,12 +298,15 @@ static enum pca9532_state pca9532_getled(struct pca9532_led *led) struct i2c_client *client = led->client; struct pca9532_data *data = i2c_get_clientdata(client); u8 maxleds = data->chip_info->num_leds; - char reg; - enum pca9532_state ret; + int reg; + enum pca9532_state ret = PCA9532_OFF; mutex_lock(&data->update_lock); reg = i2c_smbus_read_byte_data(client, LED_REG(maxleds, led->id)); - ret = (reg & LED_MASK(led->id)) >> LED_SHIFT(led->id); + if (reg >= 0) + ret = (reg & LED_MASK(led->id)) >> LED_SHIFT(led->id); + else + dev_warn(&client->dev, "failed to read LED register: %d\n", reg); mutex_unlock(&data->update_lock); return ret; } From 145ca6bca2cc5fd8d9c7af216df58a9c75c0e84c Mon Sep 17 00:00:00 2001 From: Cong Nguyen Date: Tue, 15 Sep 2026 10:45:15 +0700 Subject: [PATCH 09/49] leds: lm3692x: Fix regulator/gpio leak and wrong enabled state on last write failure Every write in lm3692x_leds_enable() checks its return and jumps to out: on failure -- except the last one, which falls through and sets led->enabled = true regardless of whether it actually succeeded. Leaks the regulator on a failing probe, and short-circuits later brightness_set() calls without retrying. Route it through out: like every other write here, and only set enabled = true once it succeeds. Fixes: 260718b3a35d ("leds: lm3692x: Disable chip on brightness 0") Assisted-by: Claude:claude-opus-4 Signed-off-by: Cong Nguyen Link: https://patch.msgid.link/20260915034515.4147981-1-congnt264@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-lm3692x.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/leds/leds-lm3692x.c b/drivers/leds/leds-lm3692x.c index 8d2678dc9e4f..9c58d54c5783 100644 --- a/drivers/leds/leds-lm3692x.c +++ b/drivers/leds/leds-lm3692x.c @@ -276,9 +276,11 @@ static int lm3692x_leds_enable(struct lm3692x_led *led) ret = regmap_update_bits(led->regmap, LM3692X_EN, LM3692X_ENABLE_MASK, enable_state | LM3692X_DEVICE_EN); + if (ret) + goto out; led->enabled = true; - return ret; + return 0; out: dev_err(&led->client->dev, "Fail writing initialization values\n"); From 226f32543668960673596288a50191052dba3352 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:42:15 +0100 Subject: [PATCH 10/49] leds: st1202: Correct the name of the prescaler conversion helper st1202_prescalar_to_miliseconds() converts a duration in milliseconds into the prescaler value written to the pattern duration registers, so its name states the conversion the wrong way round. It also misspells both "prescaler" and "milliseconds". Rename it to st1202_milliseconds_to_prescaler(). No functional change. Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB8497AA602374777F1BA08E07C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index ead144155849..5a2202fb4c22 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -85,7 +85,7 @@ static int st1202_write_reg(struct st1202_chip *chip, int reg, uint8_t val) return ret; } -static uint8_t st1202_prescalar_to_miliseconds(unsigned int value) +static uint8_t st1202_milliseconds_to_prescaler(unsigned int value) { return value / ST1202_MILLIS_PATTERN_DUR_MIN; } @@ -126,7 +126,7 @@ static int st1202_duration_pattern_write(struct st1202_chip *chip, int pattern, unsigned int value) { return st1202_write_reg(chip, (ST1202_PATTERN_DUR + pattern), - st1202_prescalar_to_miliseconds(value)); + st1202_milliseconds_to_prescaler(value)); } static int __st1202_channel_set(struct st1202_chip *chip, int led_num, bool active) From 7520fc78c52ea55c2c83d0ecc11ab2355a753b7f Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:42:52 +0100 Subject: [PATCH 11/49] leds: st1202: Spell out the phase-shift configuration bit ST1202_CONFIG_REG_SHFT is abbreviated to the point of being unreadable and, unlike the neighbouring PATS and PATSR bits, carries no comment explaining what it controls. Rename it to ST1202_CONFIG_REG_PHASE_SHIFT and document it in the same style as the other configuration bits, retaining the datasheet name in the comment so it remains searchable. The longer name no longer fits the existing layout in st1202_led_pattern_set(), where the continuation line was already 106 columns wide. Re-wrap that register write to match the layout st1202_blink_set() uses for the same operation and drop the redundant parentheses around the bit mask. No functional change. Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB849716E21E082D3B93B5B683C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 5a2202fb4c22..d8a04e96fb46 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -24,7 +24,8 @@ #define ST1202_CONFIG_REG_PATS BIT(7) /* PATSR: Pattern sequence runs (self-clear when sequence is finished) */ #define ST1202_CONFIG_REG_PATSR BIT(6) -#define ST1202_CONFIG_REG_SHFT BIT(3) +/* SHFT: Phase-shift delay enable */ +#define ST1202_CONFIG_REG_PHASE_SHIFT BIT(3) #define ST1202_DEV_ENABLE 0x01 #define ST1202_DEV_ENABLE_ON BIT(0) #define ST1202_DEV_ENABLE_RESET BIT(7) @@ -210,7 +211,7 @@ 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); + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_PHASE_SHIFT); if (ret != 0) return ret; @@ -246,7 +247,7 @@ 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); + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_PHASE_SHIFT); if (ret != 0) return ret; @@ -268,8 +269,9 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, 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)); + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, + ST1202_CONFIG_REG_PATSR | ST1202_CONFIG_REG_PATS | + ST1202_CONFIG_REG_PHASE_SHIFT); if (ret != 0) return ret; @@ -299,7 +301,7 @@ static int st1202_blink_set(struct led_classdev *led_cdev, guard(mutex)(&chip->lock); - ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_SHFT); + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_PHASE_SHIFT); if (ret) return ret; @@ -353,7 +355,7 @@ static int st1202_blink_set(struct led_classdev *led_cdev, ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_PATSR | ST1202_CONFIG_REG_PATS | - ST1202_CONFIG_REG_SHFT); + ST1202_CONFIG_REG_PHASE_SHIFT); if (ret) return ret; @@ -419,7 +421,7 @@ static int st1202_setup(struct st1202_chip *chip) return ret; /* Enable phase-shift delay feature */ - ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_SHFT); + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_PHASE_SHIFT); if (ret < 0) return ret; From 0aed225f40613253617ad30a57492043a3448938 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:43:17 +0100 Subject: [PATCH 12/49] leds: st1202: Name pattern index variables consistently The driver already names pattern indices "pattern" in the signatures of st1202_pwm_pattern_write() and st1202_duration_pattern_write(), and st1202_blink_set() follows the same convention. The remaining loops abbreviate it to "patt", which matches nothing else in the file. Rename those loop variables to "pattern". In st1202_led_pattern_set() that name is taken by the array of pattern steps, so rename the parameter to "patterns", which also reflects better that it is an array. Two st1202_pwm_pattern_write() calls exceed 100 columns with the longer name and are wrapped accordingly. No functional change. Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB8497AD614B8686BBDA654674C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index d8a04e96fb46..ddb8c6027706 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -177,8 +177,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); + for (int pattern = 0; pattern < ST1202_MAX_PATTERNS; pattern++) + st1202_pwm_pattern_write(chip, led->led_num, pattern, ST1202_PATTERN_PWM_FULL); st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, value); __st1202_channel_set(chip, led->led_num, !!value); } @@ -215,12 +215,13 @@ static int st1202_led_pattern_clear(struct led_classdev *ldev) if (ret != 0) return ret; - for (int patt = 0; patt < ST1202_MAX_PATTERNS; patt++) { - ret = st1202_pwm_pattern_write(chip, led->led_num, patt, ST1202_PATTERN_PWM_FULL); + for (int pattern = 0; pattern < ST1202_MAX_PATTERNS; pattern++) { + ret = st1202_pwm_pattern_write(chip, led->led_num, pattern, + ST1202_PATTERN_PWM_FULL); if (ret != 0) return ret; - ret = st1202_write_reg(chip, ST1202_PATTERN_DUR + patt, 0); + ret = st1202_write_reg(chip, ST1202_PATTERN_DUR + pattern, 0); if (ret != 0) return ret; } @@ -229,7 +230,7 @@ static int st1202_led_pattern_clear(struct led_classdev *ldev) } static int st1202_led_pattern_set(struct led_classdev *ldev, - struct led_pattern *pattern, + struct led_pattern *patterns, u32 len, int repeat) { struct st1202_led *led = cdev_to_st1202_led(ldev); @@ -239,9 +240,9 @@ 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) + for (int pattern = 0; pattern < len; pattern++) { + if (patterns[pattern].delta_t < ST1202_MILLIS_PATTERN_DUR_MIN || + patterns[pattern].delta_t > ST1202_MILLIS_PATTERN_DUR_MAX) return -EINVAL; } @@ -251,12 +252,13 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, if (ret != 0) return ret; - for (int patt = 0; patt < len; patt++) { - ret = st1202_pwm_pattern_write(chip, led->led_num, patt, pattern[patt].brightness); + for (int pattern = 0; pattern < len; pattern++) { + ret = st1202_pwm_pattern_write(chip, led->led_num, pattern, + patterns[pattern].brightness); if (ret != 0) return ret; - ret = st1202_duration_pattern_write(chip, patt, pattern[patt].delta_t); + ret = st1202_duration_pattern_write(chip, pattern, patterns[pattern].delta_t); if (ret != 0) return ret; } From 38f98583bcac95a65774b73cbcc56280a71f5612 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:43:58 +0100 Subject: [PATCH 13/49] leds: st1202: Use u8 consistently for 8-bit values The driver spells 8-bit unsigned values both as uint8_t and as u8, a mix present since the driver was added. The register helpers and the prescaler conversion use uint8_t, while the rest of the driver uses u8, which is also the preferred type in kernel code. Convert the remaining uint8_t uses to u8. No functional change. Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB8497783FB573CD48491F9648C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index ddb8c6027706..90f69ac16714 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -59,7 +59,7 @@ static struct st1202_led *cdev_to_st1202_led(struct led_classdev *cdev) return container_of(cdev, struct st1202_led, led_cdev); } -static int st1202_read_reg(struct st1202_chip *chip, int reg, uint8_t *val) +static int st1202_read_reg(struct st1202_chip *chip, int reg, u8 *val) { struct device *dev = &chip->client->dev; int ret; @@ -70,11 +70,11 @@ static int st1202_read_reg(struct st1202_chip *chip, int reg, uint8_t *val) return ret; } - *val = (uint8_t)ret; + *val = (u8)ret; return 0; } -static int st1202_write_reg(struct st1202_chip *chip, int reg, uint8_t val) +static int st1202_write_reg(struct st1202_chip *chip, int reg, u8 val) { struct device *dev = &chip->client->dev; int ret; @@ -86,7 +86,7 @@ static int st1202_write_reg(struct st1202_chip *chip, int reg, uint8_t val) return ret; } -static uint8_t st1202_milliseconds_to_prescaler(unsigned int value) +static u8 st1202_milliseconds_to_prescaler(unsigned int value) { return value / ST1202_MILLIS_PATTERN_DUR_MIN; } From 119a7d375e5caedaf252198fa64e7df5d7269ab4 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:44:38 +0100 Subject: [PATCH 14/49] leds: st1202: Clear unused pattern slots in pattern_set() The pattern duration registers are chip-wide while the PWM registers are per-channel, so a channel can end up driven by a sequencer timeline it never programmed. st1202_led_pattern_set() programs only the first len slots. The pattern trigger calls pattern_clear() before pattern_set() only once a channel is already in PATTERN_TYPE_HW mode, and activating the trigger resets that mode, so the first hardware pattern written after every activation skips it. If another channel has already programmed a longer pattern, durations [len, 8) are still set, and this channel's PWM for those slots holds whatever was last written there. That used to be LED_OFF, so a channel stayed dark for the slots it had not programmed itself. Since commit d2ca0e2b6d64 ("leds: st1202: Set all pattern PWM slots to full after clearing pattern") pattern_clear() leaves ST1202_PATTERN_PWM_FULL there, and so does the brightness path since commit 7cbe470366bd ("leds: st1202: Fix brightness having no effect while pattern mode is active"). The channel now plays its own steps and then sits at full brightness for the rest of the sequence. Zero this channel's PWM for the unused slots so it stays dark for the remainder of the timeline, as st1202_blink_set() already does for the channels it is not driving. The duration registers are deliberately left alone. All twelve channels share them, so clearing them would truncate a longer pattern still running elsewhere on the same device. A channel whose pattern is shorter than the active timeline therefore keeps the longer period; that follows from having a single hardware sequencer and is not something this can correct. Fixes: d2ca0e2b6d64 ("leds: st1202: Set all pattern PWM slots to full after clearing pattern") Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB8497E9939E3C0D68401659D0C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 90f69ac16714..9985b4b60989 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -263,6 +263,12 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, return ret; } + for (int pattern = len; pattern < ST1202_MAX_PATTERNS; pattern++) { + ret = st1202_pwm_pattern_write(chip, led->led_num, pattern, LED_OFF); + if (ret != 0) + return ret; + } + ret = st1202_write_reg(chip, ST1202_PATTERN_REP, repeat); if (ret != 0) return ret; From 82f8b88d6ac2edf01651bbd84e03ede04b2ca80e Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:45:01 +0100 Subject: [PATCH 15/49] leds: st1202: Scale pattern brightness to the 12-bit PWM range The LED1202 PWM registers are 12 bits wide, but st1202_led_pattern_set() writes the 8-bit brightness value carried by struct led_pattern into them unscaled. A pattern step asking for full brightness therefore drives the channel at 255 out of 4095, about 6% duty cycle, so a pattern is dimmer than the same brightness set directly. Scale the value to the full PWM range before programming it. Scale against max_brightness rather than the register maximum. The LED core takes max_brightness from the DT max-brightness property, so a board can declare a ceiling below full scale, and the pattern trigger already rejects steps above it. Scaling against a hardcoded 255 would leave such a board unable to reach its own declared maximum from the pattern path while the brightness path reaches it. Scaling divides by max_brightness. The LED core replaces a zero max-brightness from the device tree with LED_FULL, so the divisor should never be zero, but the helper returns zero rather than rely on that. max_brightness is capped at U8_MAX, the driver's own maximum, so a device tree declaring more than the part supports is treated as full scale. struct led_pattern brightness is signed, so clamp both ends and floor a negative value to LED_OFF rather than let it wrap. Document that a step is relative to max_brightness, since the range given in the documentation no longer holds when the device tree sets max-brightness. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB8497B938D3592582E17CB757C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- Documentation/leds/leds-st1202.rst | 5 +++++ drivers/leds/leds-st1202.c | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Documentation/leds/leds-st1202.rst b/Documentation/leds/leds-st1202.rst index ed32eb3a27d4..28479a112389 100644 --- a/Documentation/leds/leds-st1202.rst +++ b/Documentation/leds/leds-st1202.rst @@ -20,6 +20,11 @@ brightness (PWM) and duration must be written to hw_pattern. - Min pattern duration: 22 ms - Max pattern duration: 5610 ms +If the device tree sets max-brightness, that value replaces 255 as the +upper limit, and a step equal to it drives the channel at full scale. +The hardware cannot go above 255, so with a larger max-brightness every +step from 255 upwards is full scale. + 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 9985b4b60989..5558ac9612a1 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -91,6 +91,16 @@ static u8 st1202_milliseconds_to_prescaler(unsigned int value) return value / ST1202_MILLIS_PATTERN_DUR_MIN; } +static u16 st1202_brightness_to_pwm(int brightness, unsigned int max_brightness) +{ + if (!max_brightness) + return 0; + + brightness = clamp_t(int, brightness, LED_OFF, max_brightness); + + return brightness * ST1202_PATTERN_PWM_FULL / max_brightness; +} + static int st1202_pwm_pattern_write(struct st1202_chip *chip, int led_num, int pattern, unsigned int value) { @@ -235,8 +245,11 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, { struct st1202_led *led = cdev_to_st1202_led(ldev); struct st1202_chip *chip = led->chip; + unsigned int max_brightness; int ret; + max_brightness = min_t(unsigned int, ldev->max_brightness, U8_MAX); + if (len > ST1202_MAX_PATTERNS) return -EINVAL; @@ -254,7 +267,8 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, for (int pattern = 0; pattern < len; pattern++) { ret = st1202_pwm_pattern_write(chip, led->led_num, pattern, - patterns[pattern].brightness); + st1202_brightness_to_pwm(patterns[pattern].brightness, + max_brightness)); if (ret != 0) return ret; From 878eec1475e77ad22059584bc617b6e671a536d9 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:45:30 +0100 Subject: [PATCH 16/49] leds: st1202: Program the channel current for hardware patterns A channel's output is ILED x Pattern_PWM / 4095, so a pattern is only visible while the analog current register holds a non-zero value. st1202_led_pattern_set() programs the PWM slots and starts the sequencer but never writes ILED, so a channel whose current is zero stays dark while pattern_set() reports success. Zero is reached by ordinary use. Writing 0 to brightness sets it, and so does switching away from the pattern trigger, because pattern_trig_deactivate() ends with led_set_brightness(LED_OFF). A sequence as simple as echo none > trigger echo pattern > trigger echo "255 500 0 500" > hw_pattern therefore programs a pattern that never lights up. Program the current in pattern_set() and release it in pattern_clear(), so the pattern path owns ILED for as long as a pattern is loaded. Releasing it matters because the trigger calls pattern_clear() before it parses a new pattern: without that, a rejected or empty hw_pattern write would leave the channel at full current with every PWM slot at full scale, latching the LED on. Clearing before the slots are raised also avoids a brief full-brightness flash on every reprogram. A current of zero is not enough to release the channel, though: the datasheet notes that it does not switch the channel off but leaves a small uncontrolled current, which shows as a faint glow. pattern_clear() therefore also disables the channel, and pattern_set() enables it again. Both are done before the sequencer is stopped, so an I2C error partway through a clear cannot leave the channel lit. Probe clears every channel, so the LEDs now reach user space switched off. Until now they came up lit at the chip's power-on current, because pattern_clear() fills every slot at full scale. Probe no longer enables the channels itself either: st1202_setup() has already disabled them all, and each path that lights a channel enables it when needed. The current is programmed to the same max_brightness the PWM values are scaled against, so a pattern step of N produces the same output as writing N to brightness, and a board declaring a lower ceiling through the max-brightness property keeps it on both paths. Note that this makes a hardware pattern take the channel to its maximum current, so brightness reads back as max_brightness once a pattern is loaded and as zero once it is cleared. The pattern's own values, not the last brightness written, decide what the channel shows while a pattern is running. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB84978D7C1CC0367D1818093DC5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 5558ac9612a1..41eac6195659 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -221,6 +221,14 @@ static int st1202_led_pattern_clear(struct led_classdev *ldev) guard(mutex)(&chip->lock); + ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, LED_OFF); + if (ret != 0) + return ret; + + ret = __st1202_channel_set(chip, led->led_num, false); + if (ret != 0) + return ret; + ret = st1202_write_reg(chip, ST1202_CONFIG_REG, ST1202_CONFIG_REG_PHASE_SHIFT); if (ret != 0) return ret; @@ -287,6 +295,10 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, if (ret != 0) return ret; + ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, max_brightness); + if (ret != 0) + return ret; + ret = __st1202_channel_set(chip, led->led_num, true); if (ret != 0) return ret; @@ -455,7 +467,7 @@ static int st1202_setup(struct st1202_chip *chip) /* Duration of initialization */ usleep_range(6500, 10000); - /* Deactivate all LEDS (channels) and activate only the ones found in Device Tree */ + /* Deactivate all LEDs (channels); each is enabled when it is lit */ ret = st1202_write_reg(chip, ST1202_CHAN_ENABLE_LOW, ST1202_CHAN_DISABLE_ALL); if (ret < 0) return ret; @@ -502,11 +514,6 @@ static int st1202_probe(struct i2c_client *client) if (!led->is_active) continue; - ret = st1202_channel_set(led->chip, led->led_num, true); - if (ret < 0) - return dev_err_probe(&client->dev, ret, - "Failed to activate LED channel\n"); - ret = st1202_led_pattern_clear(&led->led_cdev); if (ret < 0) return dev_err_probe(&client->dev, ret, From 5882de94edf7a53812a76a882a95e1e86d7105d9 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:46:05 +0100 Subject: [PATCH 17/49] leds: st1202: Take a reference on the LED firmware node st1202_dt_init() stores each LED's firmware node while walking the device tree with for_each_available_child_of_node_scoped(), which drops its reference to the node at the end of every iteration. The LED class stores the pointer with device_set_node() and does not take a reference of its own, so nothing keeps the node alive for as long as the LED device uses it. That has been the case since the driver was added, when the LED class devices were registered inside that same loop. Commit c72e455b89f2 ("leds: leds-st1202: Fix NULL pointer access on race condition") moved the registration into a separate loop in st1202_probe(), which also leaves the stored pointer unreferenced between the two loops. With CONFIG_OF_DYNAMIC the node can be freed while it is still in use, leaving led->fwnode and the LED device's firmware node dangling. Take a reference when the node is stored and release it through a devm action. The action is registered before the LED class devices are, so it runs after they have been unregistered. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB849710C564F23C21BDA3EA41C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 41eac6195659..ead7a3669312 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -399,6 +400,11 @@ static int st1202_blink_set(struct led_classdev *led_cdev, return 0; } +static void st1202_fwnode_put(void *data) +{ + fwnode_handle_put(data); +} + static int st1202_dt_init(struct st1202_chip *chip) { struct device *dev = &chip->client->dev; @@ -418,7 +424,11 @@ static int st1202_dt_init(struct st1202_chip *chip) led = &chip->leds[reg]; led->is_active = true; - led->fwnode = of_fwnode_handle(child); + led->fwnode = fwnode_handle_get(of_fwnode_handle(child)); + + err = devm_add_action_or_reset(dev, st1202_fwnode_put, led->fwnode); + if (err) + return err; led->led_cdev.max_brightness = U8_MAX; led->led_cdev.brightness_set_blocking = st1202_led_set; From 1ec35fa181981b4b9581b3dae967bac7ed82c89c Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:46:31 +0100 Subject: [PATCH 18/49] leds: st1202: Do not set brightness from atomic context st1202_brightness_set() takes the chip mutex and performs I2C transfers, both of which can sleep, but it is installed as the brightness_set callback. led_set_brightness_nopm() calls that op directly on the assumption that it never sleeps, including from softirq context, so the driver can sleep in atomic context. The driver also installs brightness_set_blocking, but the core only falls back to it when brightness_set is absent, so st1202_led_set() was never reached. It merely turned the channel on or off and ignored the requested brightness, so dropping brightness_set on its own would have lost brightness control entirely. Move the register programming into st1202_led_set() so the blocking callback honours the brightness value, and drop brightness_set. The core now defers the update to a work queue, where sleeping is allowed. Errors from the register writes are propagated rather than discarded. st1202_led_set() was the last user of the locking st1202_channel_set() wrapper, which is removed. Programming every PWM slot to full scale is kept: it is what makes the brightness visible while the global sequencer is running, without having to stop the sequencer and disturb the other channels. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB8497CF8FB57A272EB671F0C6C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 45 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index ead7a3669312..101468557f6c 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -173,27 +173,6 @@ static int __st1202_channel_set(struct st1202_chip *chip, int led_num, bool acti 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 pattern = 0; pattern < ST1202_MAX_PATTERNS; pattern++) - st1202_pwm_pattern_write(chip, led->led_num, pattern, 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); @@ -210,8 +189,29 @@ static enum led_brightness st1202_brightness_get(struct led_classdev *led_cdev) static int st1202_led_set(struct led_classdev *ldev, enum led_brightness value) { struct st1202_led *led = cdev_to_st1202_led(ldev); + struct st1202_chip *chip = led->chip; + int ret; - return st1202_channel_set(led->chip, led->led_num, !!value); + guard(mutex)(&chip->lock); + + /* + * The output of a channel is ILED x Pattern_PWM / 4095. Setting every + * PWM slot to full scale makes it equal ILED whatever the state of the + * sequencer, so the brightness takes effect without stopping the + * sequencer, which is global and would disturb the other channels. + */ + for (int pattern = 0; pattern < ST1202_MAX_PATTERNS; pattern++) { + ret = st1202_pwm_pattern_write(chip, led->led_num, pattern, + ST1202_PATTERN_PWM_FULL); + if (ret) + return ret; + } + + ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, value); + if (ret) + return ret; + + return __st1202_channel_set(chip, led->led_num, !!value); } static int st1202_led_pattern_clear(struct led_classdev *ldev) @@ -436,7 +436,6 @@ static int st1202_dt_init(struct st1202_chip *chip) led->led_cdev.pattern_clear = st1202_led_pattern_clear; led->led_cdev.default_trigger = "pattern"; led->led_cdev.blink_set = st1202_blink_set; - led->led_cdev.brightness_set = st1202_brightness_set; led->led_cdev.brightness_get = st1202_brightness_get; } From 3268b9e91db16f240ffe07ff6caf963ba8191ce8 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:47:16 +0100 Subject: [PATCH 19/49] leds: st1202: Clear unused pattern slots in blink_set() st1202_blink_set() programs slots 0 and 1 of its own channel and leaves slots 2 to 7 at ST1202_PATTERN_PWM_FULL, the value both st1202_led_set() and st1202_led_pattern_clear() leave behind. Nothing keeps those slots out of the output except the duration registers being zero, and those are shared by all twelve channels. Another channel programming a hardware pattern longer than two steps writes those durations again and re-arms the sequencer, so the blinking channel drives full scale for the extra steps. With two LEDs on one device, blinking one and giving the other a four step pattern, the blinking channel lights during the two steps it never programmed. Zero the channel's own unused slots, the same invariant st1202_led_pattern_set() applies to the slots beyond its pattern length. blink_set() already zeroes durations 2 to 7 when it runs; the slots are exposed only when another channel sets those shared durations again. A channel whose blink is shorter than the active timeline still loses its own timing, because the sequencer has one set of duration registers. This only stops it emitting output it never asked for. Fixes: 35cbd34a6a05 ("leds: st1202: Add hardware-accelerated blink support") Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB849770CF6D41F3DFF56B1AC3C5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 101468557f6c..42989b4fbaf3 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -358,9 +358,11 @@ static int st1202_blink_set(struct led_classdev *led_cdev, if (ret) return ret; - ret = st1202_pwm_pattern_write(chip, led->led_num, 1, LED_OFF); - if (ret) - return ret; + for (int pattern = 1; pattern < ST1202_MAX_PATTERNS; pattern++) { + ret = st1202_pwm_pattern_write(chip, led->led_num, pattern, LED_OFF); + if (ret) + return ret; + } ret = st1202_duration_pattern_write(chip, 0, on); if (ret) From 865d81a98602c828515fa6ac3c530efc4205f9f6 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:47:39 +0100 Subject: [PATCH 20/49] leds: st1202: Honour max_brightness when blinking st1202_blink_set() drives the channel current register to U8_MAX, the full scale of the part, regardless of the ceiling the board declared. led_classdev_register_ext() takes max_brightness from the device tree max-brightness property. The driver does not implement led-max-microamp, so that is the only way a board can ask for less than 20 mA. The brightness and pattern paths both respect it. Blinking is the one that does not, so a board that declares a lower ceiling holds it until something enables the timer trigger. Program the same value the other paths use, capped at the width of the current register so an out of range property cannot wrap it to zero. Fixes: 35cbd34a6a05 ("leds: st1202: Add hardware-accelerated blink support") Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB84979A5D536C71D319A06808C5BA2@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 42989b4fbaf3..39129206b78d 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -382,7 +382,8 @@ static int st1202_blink_set(struct led_classdev *led_cdev, if (ret) return ret; - ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, U8_MAX); + ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, + min_t(unsigned int, led_cdev->max_brightness, U8_MAX)); if (ret) return ret; From ca81ea108502817e3841e98f0d3651398d9e1bd9 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:48:22 +0100 Subject: [PATCH 21/49] leds: st1202: Cap the channel current in st1202_led_set() st1202_led_set() writes the requested brightness straight into the 8-bit channel current register. The LED core limits brightness to max_brightness, which comes from the device tree max-brightness property and has no upper bound, so on a board declaring more than 255 a brightness of 256 reaches the register as 0 and turns the LED off, and larger values wrap around. Cap the value at U8_MAX, the width of the register, so anything above it drives the channel at full current. The pattern and blink paths already cap the current they program the same way. Fixes: 259230378c65 ("leds: Add LED1202 I2C driver") Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB8497C60D1D2921DD0384926FC5BA2@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 39129206b78d..5e4a8b80cfe4 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -207,7 +207,8 @@ static int st1202_led_set(struct led_classdev *ldev, enum led_brightness value) return ret; } - ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, value); + ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, + min_t(unsigned int, value, U8_MAX)); if (ret) return ret; From e264a106d8fc45fd82891cb0c4f8bd9e2c48c152 Mon Sep 17 00:00:00 2001 From: Manuel Fombuena Date: Tue, 15 Sep 2026 13:48:51 +0100 Subject: [PATCH 22/49] leds: st1202: Give the channel current ceiling a single helper The channel current register is 8 bits wide, while max_brightness comes from the device tree and can be larger. The brightness, pattern and blink paths each apply that limit on their own, open-coding the same min_t() against U8_MAX, and the blink path previously drifted from the others by ignoring max_brightness altogether. Move the limit into st1202_iled_max() and use it in all three places. Each path programs the same value as before: the LED core already limits brightness to max_brightness, so capping it against st1202_iled_max() in st1202_led_set() gives the same result as capping it against U8_MAX. Signed-off-by: Manuel Fombuena Assisted-by: LLM Link: https://patch.msgid.link/GV1PR08MB8497935DA426302FB808A81EC5BA2@GV1PR08MB8497.eurprd08.prod.outlook.com Signed-off-by: Lee Jones --- drivers/leds/leds-st1202.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/drivers/leds/leds-st1202.c b/drivers/leds/leds-st1202.c index 5e4a8b80cfe4..8c2e1d58ba0c 100644 --- a/drivers/leds/leds-st1202.c +++ b/drivers/leds/leds-st1202.c @@ -92,6 +92,12 @@ static u8 st1202_milliseconds_to_prescaler(unsigned int value) return value / ST1202_MILLIS_PATTERN_DUR_MIN; } +/* The channel current register is 8 bits wide, whatever max-brightness says */ +static u8 st1202_iled_max(struct led_classdev *led_cdev) +{ + return min_t(unsigned int, led_cdev->max_brightness, U8_MAX); +} + static u16 st1202_brightness_to_pwm(int brightness, unsigned int max_brightness) { if (!max_brightness) @@ -208,7 +214,7 @@ static int st1202_led_set(struct led_classdev *ldev, enum led_brightness value) } ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, - min_t(unsigned int, value, U8_MAX)); + min_t(unsigned int, value, st1202_iled_max(ldev))); if (ret) return ret; @@ -258,7 +264,7 @@ static int st1202_led_pattern_set(struct led_classdev *ldev, unsigned int max_brightness; int ret; - max_brightness = min_t(unsigned int, ldev->max_brightness, U8_MAX); + max_brightness = st1202_iled_max(ldev); if (len > ST1202_MAX_PATTERNS) return -EINVAL; @@ -383,8 +389,7 @@ static int st1202_blink_set(struct led_classdev *led_cdev, if (ret) return ret; - ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, - min_t(unsigned int, led_cdev->max_brightness, U8_MAX)); + ret = st1202_write_reg(chip, ST1202_ILED_REG0 + led->led_num, st1202_iled_max(led_cdev)); if (ret) return ret; From d229695f2162a41bfa614f175798fa56a5f1c969 Mon Sep 17 00:00:00 2001 From: Dmitry Antipov Date: Wed, 16 Sep 2026 19:16:06 +0300 Subject: [PATCH 23/49] leds: flash: Prevent using uninitialized variable in flash_fault_show() In flash_fault_show(), address of uninitialized 'fault' variable is passed to device-specific .fault_get callbacks. This is not a problem if such a callback directly assigns the value like rt4505_fault_get(), but results in an undefined behavior if callback just performs bitwise ORs like as3645a_get_fault(). So just initialize 'fault' with zero. Reported-by: sashiko-bot@kernel.org Closes: https://sashiko.dev/#/patchset/20260904054437.461706-1-dmantipov@yandex.ru?part=1 Fixes: 7aea8389a77ab ("leds: Add LED Flash class extension to the LED subsystem") Signed-off-by: Dmitry Antipov Link: https://patch.msgid.link/20260916161607.90106-1-dmantipov@yandex.ru Signed-off-by: Lee Jones --- drivers/leds/led-class-flash.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/leds/led-class-flash.c b/drivers/leds/led-class-flash.c index 165035a8826c..3bb90823de1a 100644 --- a/drivers/leds/led-class-flash.c +++ b/drivers/leds/led-class-flash.c @@ -187,7 +187,7 @@ static ssize_t flash_fault_show(struct device *dev, { struct led_classdev *led_cdev = dev_get_drvdata(dev); struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); - u32 fault, mask = 0x1; + u32 fault = 0, mask = 0x1; char *pbuf = buf; int i, ret, buf_len; From b18158f705dcff53f76f37733dea5154f1b32444 Mon Sep 17 00:00:00 2001 From: Dmitry Antipov Date: Wed, 16 Sep 2026 19:16:07 +0300 Subject: [PATCH 24/49] leds: flash: Simplify flash_fault_show() Do not emit extra trailing space and use sysfs_emit_at() over sprintf()/strcat()/strlen() in flash_fault_show(). Suggested-by: Lee Jones Signed-off-by: Dmitry Antipov Link: https://patch.msgid.link/20260916161607.90106-2-dmantipov@yandex.ru Signed-off-by: Lee Jones --- drivers/leds/led-class-flash.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/drivers/leds/led-class-flash.c b/drivers/leds/led-class-flash.c index 3bb90823de1a..181dfd49c6ad 100644 --- a/drivers/leds/led-class-flash.c +++ b/drivers/leds/led-class-flash.c @@ -188,25 +188,22 @@ static ssize_t flash_fault_show(struct device *dev, struct led_classdev *led_cdev = dev_get_drvdata(dev); struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); u32 fault = 0, mask = 0x1; - char *pbuf = buf; - int i, ret, buf_len; + int i, ret; ret = led_get_flash_fault(fled_cdev, &fault); if (ret < 0) return -EINVAL; - *buf = '\0'; - - for (i = 0; i < LED_NUM_FLASH_FAULTS; ++i) { - if (fault & mask) { - buf_len = sprintf(pbuf, "%s ", - led_flash_fault_names[i]); - pbuf += buf_len; - } + for (ret = 0, i = 0; i < LED_NUM_FLASH_FAULTS; ++i) { + if (fault & mask) + ret += sysfs_emit_at(buf, ret, "%s%s", + (ret ? " " : ""), + led_flash_fault_names[i]); mask <<= 1; } - return strlen(strcat(buf, "\n")); + ret += sysfs_emit_at(buf, ret, "\n"); + return ret; } static DEVICE_ATTR_RO(flash_fault); From 6db5536b3bb71ab303fe7e9b95d77a09702c24a3 Mon Sep 17 00:00:00 2001 From: Jan Carlo Roleda Date: Wed, 23 Sep 2026 08:33:23 +0800 Subject: [PATCH 25/49] dt-bindings: leds: Document LTC3208 Multidisplay LED Driver Add Devicetree Documentation for LTC3208 Multidisplay LED Driver. Signed-off-by: Jan Carlo Roleda Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260923003325.61722-2-jancarlo.roleda@analog.com Signed-off-by: Lee Jones --- .../devicetree/bindings/leds/adi,ltc3208.yaml | 183 ++++++++++++++++++ MAINTAINERS | 7 + 2 files changed, 190 insertions(+) create mode 100644 Documentation/devicetree/bindings/leds/adi,ltc3208.yaml diff --git a/Documentation/devicetree/bindings/leds/adi,ltc3208.yaml b/Documentation/devicetree/bindings/leds/adi,ltc3208.yaml new file mode 100644 index 000000000000..c6697816b7d9 --- /dev/null +++ b/Documentation/devicetree/bindings/leds/adi,ltc3208.yaml @@ -0,0 +1,183 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +# Copyright (c) 2026 Analog Devices, Inc. +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/leds/adi,ltc3208.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: LTC3208 Multidisplay LED Controller from Linear Technologies (Now Analog Devices) + +maintainers: + - Jan Carlo Roleda + +description: + The LTC3208 is a multidisplay LED controller that can support up to 1A to all + connected LEDs. + + The datasheet for this device can be found in + https://www.analog.com/en/products/ltc3208.html + +properties: + compatible: + const: adi,ltc3208 + + reg: + maxItems: 1 + + "#address-cells": + const: 1 + + "#size-cells": + const: 0 + + adi,disable-camhl-pin: + type: boolean + description: + Configures whether the external CAMHL pin is disabled. + If disabled then the output pins associated with CAM will always select + the CAM register's high half-byte brightness. + + adi,cfg-enrgbs-pin: + type: boolean + description: + Configures which channel the ENRGBS pin toggles when it receives a signal. + ENRGBS pin controls the SUB channel's output pins if this is set, + or RGB channel's output pins if this is unset. + + adi,disable-rgb-aux4-dropout: + type: boolean + description: + Configures the RGB and AUX4 dropout signals to be disabled. + + adi,aux1-channel: + $ref: /schemas/types.yaml#/definitions/string + description: + LED Channel that the AUX1 output pin mirrors its brightness level from. + enum: [aux, main, sub, cam] + default: aux + + adi,aux2-channel: + $ref: /schemas/types.yaml#/definitions/string + description: + LED Channel that the AUX2 output pin mirrors its brightness level from. + enum: [aux, main, sub, cam] + default: aux + + adi,aux3-channel: + $ref: /schemas/types.yaml#/definitions/string + description: + LED Channel that the AUX3 output pin mirrors its brightness level from. + enum: [aux, main, sub, cam] + default: aux + + adi,aux4-channel: + $ref: /schemas/types.yaml#/definitions/string + description: + LED Channel that the AUX4 output pin mirrors its brightness level from. + enum: [aux, main, sub, cam] + default: aux + +patternProperties: + "^led@[0-7]$": + type: object + $ref: /schemas/leds/common.yaml# + unevaluatedProperties: false + properties: + reg: + description: + LED Channel Number. each channel maps to a specific channel group used + to configure the brightness level of the output pins corresponding to + the channel. + enum: + - 0 # Main Channel (8-bit brightness) + - 1 # Sub Channel (8-bit brightness) + - 2 # AUX Channel (4-bit brightness) + - 3 # Camera Channel, Low-side byte (4-bit brightness) + - 4 # Camera Channel, High-side byte (4-bit brightness) + - 5 # Red Channel (4-bit brightness) + - 6 # Blue Channel (4-bit brightness) + - 7 # Green Channel (4-bit brightness) + required: + - reg + +required: + - compatible + - reg + - "#address-cells" + - "#size-cells" + +additionalProperties: false + +examples: + - | + #include + i2c { + #address-cells = <1>; + #size-cells = <0>; + + led-controller@1b { + compatible = "adi,ltc3208"; + reg = <0x1b>; + #address-cells = <1>; + #size-cells = <0>; + adi,disable-camhl-pin; + adi,cfg-enrgbs-pin; + adi,disable-rgb-aux4-dropout; + + /* MAIN */ + led@0 { + reg = <0>; + function = LED_FUNCTION_ACTIVITY; + color = ; + }; + + /* SUB */ + led@1 { + reg = <1>; + function = LED_FUNCTION_ACTIVITY; + color = ; + }; + + /* AUX */ + led@2 { + reg = <2>; + function = LED_FUNCTION_ACTIVITY; + color = ; + }; + + /* CAMLO */ + led@3 { + reg = <3>; + function = LED_FUNCTION_FLASH; + color = ; + }; + + /* CAMHI */ + led@4 { + reg = <4>; + function = LED_FUNCTION_FLASH; + color = ; + }; + + /* RED */ + led@5 { + reg = <5>; + function = LED_FUNCTION_INDICATOR; + color = ; + }; + + /* BLUE */ + led@6 { + reg = <6>; + function = LED_FUNCTION_INDICATOR; + color = ; + }; + + /* GREEN */ + led@7 { + reg = <7>; + function = LED_FUNCTION_INDICATOR; + color = ; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 3a19da74d00c..c422939a8269 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15527,6 +15527,13 @@ W: https://ez.analog.com/linux-software-drivers F: Documentation/devicetree/bindings/iio/temperature/adi,ltc2983.yaml F: drivers/iio/temperature/ltc2983.c +LTC3208 LED DRIVER +M: Jan Carlo Roleda +L: linux-leds@vger.kernel.org +S: Maintained +W: https://ez.analog.com/linux-software-drivers +F: Documentation/devicetree/bindings/leds/adi,ltc3208.yaml + LTC3220 LED DRIVER M: Edelweise Escala L: linux-leds@vger.kernel.org From 83858f2b93922e1a6ddb77ed76b18711950f8b0d Mon Sep 17 00:00:00 2001 From: Jan Carlo Roleda Date: Wed, 23 Sep 2026 08:33:24 +0800 Subject: [PATCH 26/49] leds: ltc3208: Add driver for LTC3208 Multidisplay LED Driver Kernel driver implementation for LTC3208 Multidisplay LED Driver. The LTC3208 is a Multi-display LED driver, designed to control up to 7 distinct LED channels (MAIN, SUB, AUX, CAMHI, CAMLO, RED, GREEN, BLUE), each configurable with its own current level that is equally set to its respective output current source pins for external LEDs. It is programmed via the I2C serial interface. MAIN and SUB support 8-bit current level resolution, while AUX, CAMHI/LO, RED, GREEN, and BLUE support 4-bit levels. The AUX LED channel can be configured to mirror the CAM, SUB, and MAIN channel current levels, or as its own independent AUX channel. The CAM LED channel is configured as 2 separate CAMHI and CAMLO register sub-channels, which current is selected via the CAMHL pin, or set to CAMHI register only via setting the S_CAMHILO bit high in register G (0x7). Signed-off-by: Jan Carlo Roleda Link: https://patch.msgid.link/20260923003325.61722-3-jancarlo.roleda@analog.com Signed-off-by: Lee Jones --- MAINTAINERS | 1 + drivers/leds/Kconfig | 12 ++ drivers/leds/Makefile | 1 + drivers/leds/leds-ltc3208.c | 234 ++++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 drivers/leds/leds-ltc3208.c diff --git a/MAINTAINERS b/MAINTAINERS index c422939a8269..a1eb0937238d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15533,6 +15533,7 @@ L: linux-leds@vger.kernel.org S: Maintained W: https://ez.analog.com/linux-software-drivers F: Documentation/devicetree/bindings/leds/adi,ltc3208.yaml +F: drivers/leds/leds-ltc3208.c LTC3220 LED DRIVER M: Edelweise Escala diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index d0d3182236be..7ec2c8d78542 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -1042,6 +1042,18 @@ config LEDS_ACER_A500 This option enables support for the Power Button LED of Acer Iconia Tab A500. +config LEDS_LTC3208 + tristate "LED Driver for Analog Devices LTC3208" + depends on LEDS_CLASS && I2C + select REGMAP_I2C + help + Say Y to enable the LTC3208 LED driver. + This enables the LED device LTC3208, a 7-channel, 17-current source + multidisplay high-current LED driver, configured via I2C. + + To compile this driver as a module, choose M here: the module will + be called ltc3208. + source "drivers/leds/blink/Kconfig" comment "Flash and Torch LED drivers" diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index a68244bd50fb..4d4b089156e2 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_LTC3208) += leds-ltc3208.o obj-$(CONFIG_LEDS_LTC3220) += leds-ltc3220.o obj-$(CONFIG_LEDS_MAX5970) += leds-max5970.o obj-$(CONFIG_LEDS_MAX77650) += leds-max77650.o diff --git a/drivers/leds/leds-ltc3208.c b/drivers/leds/leds-ltc3208.c new file mode 100644 index 000000000000..64091f29f18a --- /dev/null +++ b/drivers/leds/leds-ltc3208.c @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * LED driver for Analog Devices LTC3208 Multi-Display Driver + * + * Copyright 2026 Analog Devices Inc. + * + * Author: Jan Carlo Roleda + * + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Registers */ +#define LTC3208_REG_A_GRNRED_CURRENT 0x1 /* Green and Red current DAC */ +#define LTC3208_REG_B_AUXBLU_CURRENT 0x2 /* AUX and Blue current DAC */ +#define LTC3208_REG_C_MAIN_CURRENT 0x3 /* Main current DAC */ +#define LTC3208_REG_D_SUB_CURRENT 0x4 /* Sub current DAC */ +#define LTC3208_REG_E_AUX_SELECT 0x5 /* AUX DAC Select */ +#define LTC3208_AUX1_MASK GENMASK(1, 0) +#define LTC3208_AUX2_MASK GENMASK(3, 2) +#define LTC3208_AUX3_MASK GENMASK(5, 4) +#define LTC3208_AUX4_MASK GENMASK(7, 6) +#define LTC3208_REG_F_CAM_CURRENT 0x6 /* CAM (High and Low) current DAC */ +#define LTC3208_REG_G_OPT 0x7 /* Device Options */ +#define LTC3208_OPT_CPO_MASK GENMASK(7, 6) +#define LTC3208_OPT_DIS_RGBDROP BIT(3) +#define LTC3208_OPT_DIS_CAMHILO BIT(2) +#define LTC3208_OPT_EN_RGBS BIT(1) + +#define LTC3208_MAX_BRIGHTNESS_4BIT 0xF +#define LTC3208_MAX_BRIGHTNESS_8BIT 0xFF + +#define LTC3208_NUM_AUX_LEDS 4 /* Number of configurable aux channels */ +#define LTC3208_NUM_REGS 7 /* Number of available registers to access */ + +enum ltc3208_aux_channel { + LTC3208_AUX_CHAN_AUX = 0, + LTC3208_AUX_CHAN_MAIN, + LTC3208_AUX_CHAN_SUB, + LTC3208_AUX_CHAN_CAM +}; + +enum ltc3208_channel { + LTC3208_CHAN_MAIN = 0, + LTC3208_CHAN_SUB, + LTC3208_CHAN_AUX, + LTC3208_CHAN_CAML, + LTC3208_CHAN_CAMH, + LTC3208_CHAN_RED, + LTC3208_CHAN_BLUE, + LTC3208_CHAN_GREEN, + LTC3208_CHAN_N_COUNT, +}; + +static const char *const ltc3208_dt_aux_channels[] = { + "adi,aux1-channel", + "adi,aux2-channel", + "adi,aux3-channel", + "adi,aux4-channel" +}; + +static const char *const ltc3208_aux_opt[] = { "aux", "main", "sub", "cam" }; + +struct ltc3208_led { + struct led_classdev cdev; + struct regmap_field *rfield; +}; + +static const struct reg_default ltc3208_reg_defaults[] = { + { LTC3208_REG_A_GRNRED_CURRENT, 0 }, + { LTC3208_REG_B_AUXBLU_CURRENT, 0 }, + { LTC3208_REG_C_MAIN_CURRENT, 0 }, + { LTC3208_REG_D_SUB_CURRENT, 0 }, + { LTC3208_REG_E_AUX_SELECT, 0 }, + { LTC3208_REG_F_CAM_CURRENT, 0 }, + { LTC3208_REG_G_OPT, 0 } +}; + +static const struct regmap_config ltc3208_regmap_cfg = { + .reg_bits = 8, + .val_bits = 8, + .max_register = LTC3208_REG_G_OPT, + .cache_type = REGCACHE_FLAT_S, + .reg_defaults = ltc3208_reg_defaults, + .num_reg_defaults = LTC3208_NUM_REGS, +}; + +static const struct reg_field ltc3208_led_reg_field[LTC3208_CHAN_N_COUNT] = { + [LTC3208_CHAN_MAIN] = REG_FIELD(LTC3208_REG_C_MAIN_CURRENT, 0, 7), + [LTC3208_CHAN_SUB] = REG_FIELD(LTC3208_REG_D_SUB_CURRENT, 0, 7), + [LTC3208_CHAN_BLUE] = REG_FIELD(LTC3208_REG_B_AUXBLU_CURRENT, 0, 3), + [LTC3208_CHAN_AUX] = REG_FIELD(LTC3208_REG_B_AUXBLU_CURRENT, 4, 7), + [LTC3208_CHAN_CAML] = REG_FIELD(LTC3208_REG_F_CAM_CURRENT, 0, 3), + [LTC3208_CHAN_CAMH] = REG_FIELD(LTC3208_REG_F_CAM_CURRENT, 4, 7), + [LTC3208_CHAN_RED] = REG_FIELD(LTC3208_REG_A_GRNRED_CURRENT, 0, 3), + [LTC3208_CHAN_GREEN] = REG_FIELD(LTC3208_REG_A_GRNRED_CURRENT, 4, 7), +}; + +static int ltc3208_led_set_brightness(struct led_classdev *led_cdev, enum led_brightness brightness) +{ + struct ltc3208_led *led = container_of(led_cdev, struct ltc3208_led, cdev); + u8 current_level = brightness; + + return regmap_field_write(led->rfield, current_level); +} + +static int ltc3208_probe(struct i2c_client *client) +{ + enum ltc3208_aux_channel aux_channels[LTC3208_NUM_AUX_LEDS]; + DECLARE_BITMAP(registered_chans, LTC3208_CHAN_N_COUNT) = { }; + struct regmap *regmap; + bool disable_rgb_aux4_dropout_signal; + bool disable_camhl_pin; + bool set_sub_control_pin; + int ret; + u8 dev_options; + + regmap = devm_regmap_init_i2c(client, <c3208_regmap_cfg); + if (IS_ERR(regmap)) + return dev_err_probe(&client->dev, PTR_ERR(regmap), "Failed to initialize regmap"); + + regcache_mark_dirty(regmap); + ret = regcache_sync(regmap); + if (ret) + return dev_err_probe(&client->dev, ret, "Failed to sync register cache"); + + disable_camhl_pin = device_property_read_bool(&client->dev, "adi,disable-camhl-pin"); + + set_sub_control_pin = device_property_read_bool(&client->dev, "adi,cfg-enrgbs-pin"); + + disable_rgb_aux4_dropout_signal = + device_property_read_bool(&client->dev, "adi,disable-rgb-aux4-dropout"); + + dev_options = FIELD_PREP(LTC3208_OPT_EN_RGBS, set_sub_control_pin) | + FIELD_PREP(LTC3208_OPT_DIS_CAMHILO, disable_camhl_pin) | + FIELD_PREP(LTC3208_OPT_CPO_MASK, 0) | + FIELD_PREP(LTC3208_OPT_DIS_RGBDROP, disable_rgb_aux4_dropout_signal); + + ret = regmap_write(regmap, LTC3208_REG_G_OPT, dev_options); + if (ret) + return dev_err_probe(&client->dev, ret, "Failed to set device options register"); + + /* Initialize AUX channel configurations */ + for (int i = 0; i < LTC3208_NUM_AUX_LEDS; i++) { + /* Default value (AUX) if property is not present in DT */ + if (!device_property_present(&client->dev, ltc3208_dt_aux_channels[i])) { + aux_channels[i] = LTC3208_AUX_CHAN_AUX; + continue; + } + + ret = device_property_match_property_string(&client->dev, + ltc3208_dt_aux_channels[i], + ltc3208_aux_opt, LTC3208_NUM_AUX_LEDS); + if (ret < 0) + return dev_err_probe(&client->dev, ret, "Error reading AUX Channel %d", i); + + aux_channels[i] = ret; + } + + dev_options = FIELD_PREP(LTC3208_AUX1_MASK, aux_channels[0]) | + FIELD_PREP(LTC3208_AUX2_MASK, aux_channels[1]) | + FIELD_PREP(LTC3208_AUX3_MASK, aux_channels[2]) | + FIELD_PREP(LTC3208_AUX4_MASK, aux_channels[3]); + + ret = regmap_write(regmap, LTC3208_REG_E_AUX_SELECT, dev_options); + if (ret) + return dev_err_probe(&client->dev, ret, "Error writing to aux channel register"); + + device_for_each_child_node_scoped(&client->dev, child) { + struct ltc3208_led *led; + struct led_init_data init_data = { }; + u32 chan; + + ret = fwnode_property_read_u32(child, "reg", &chan); + if (ret) + return dev_err_probe(&client->dev, ret, + "Failed to get register value of LED"); + if (chan >= LTC3208_CHAN_N_COUNT) + return dev_err_probe(&client->dev, -EINVAL, "%u is an invalid LED ID", + chan); + if (test_and_set_bit(chan, registered_chans)) + return dev_err_probe(&client->dev, -EINVAL, "%u is already registered", + chan); + + led = devm_kzalloc(&client->dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->rfield = devm_regmap_field_alloc(&client->dev, regmap, + ltc3208_led_reg_field[chan]); + if (IS_ERR(led->rfield)) + return dev_err_probe(&client->dev, PTR_ERR(led->rfield), + "Cannot allocate regmap field"); + led->cdev.brightness_set_blocking = ltc3208_led_set_brightness; + led->cdev.max_brightness = LTC3208_MAX_BRIGHTNESS_4BIT; + + if (chan == LTC3208_CHAN_MAIN || chan == LTC3208_CHAN_SUB) + led->cdev.max_brightness = LTC3208_MAX_BRIGHTNESS_8BIT; + + init_data.fwnode = child; + + ret = devm_led_classdev_register_ext(&client->dev, &led->cdev, &init_data); + if (ret) + return dev_err_probe(&client->dev, ret, "Failed to register LED %u", chan); + } + + return 0; +} + +static const struct of_device_id ltc3208_match_table[] = { + { .compatible = "adi,ltc3208" }, + { } +}; +MODULE_DEVICE_TABLE(of, ltc3208_match_table); + +static struct i2c_driver ltc3208_driver = { + .driver = { + .name = "ltc3208", + .of_match_table = ltc3208_match_table, + }, + .probe = ltc3208_probe, +}; +module_i2c_driver(ltc3208_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Jan Carlo Roleda "); +MODULE_DESCRIPTION("LTC3208 LED Driver"); From 4893d51ed4d7f3c0254543c0b5f2b307c8728eff Mon Sep 17 00:00:00 2001 From: Kees Cook Date: Thu, 17 Sep 2026 14:13:58 -0700 Subject: [PATCH 27/49] leds: aat1290: Allocate mm_current_scale with ARRAY_SIZE() In preparation for making the devm_kmalloc family of allocators type aware, we need to make sure that the returned type from the allocation matches the type of the variable being assigned. (Before, the allocator would always return "void *", which can be implicitly cast to any pointer type.) This allocates one int per entry of max_mm_current_percent, but the size was taken from the whole array, which would make the allocation type a pointer to the array rather than the "int *" being assigned. Allocate ARRAY_SIZE-many entries instead. The resulting allocation size is the same: AAT1290_MM_CURRENT_SCALE_SIZE and ARRAY_SIZE(max_mm_current_percent) are both 15. Build tested ARCH=x86_64 allmodconfig with GCC 16.2.0: drivers/leds/flash/leds-aat1290.o Assisted-by: LLM coccinelle Signed-off-by: Kees Cook Link: https://patch.msgid.link/20260917211357.i.412-kees@kernel.org Signed-off-by: Lee Jones --- drivers/leds/flash/leds-aat1290.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/leds/flash/leds-aat1290.c b/drivers/leds/flash/leds-aat1290.c index 49251cfd3350..4bde0b07fe78 100644 --- a/drivers/leds/flash/leds-aat1290.c +++ b/drivers/leds/flash/leds-aat1290.c @@ -311,8 +311,9 @@ static int init_mm_current_scale(struct aat1290_led *led, int i, max_mm_current = AAT1290_MAX_MM_CURRENT(cfg->max_flash_current); - led->mm_current_scale = devm_kzalloc(&led->pdev->dev, - sizeof(max_mm_current_percent), + led->mm_current_scale = devm_kcalloc(&led->pdev->dev, + ARRAY_SIZE(max_mm_current_percent), + sizeof(*led->mm_current_scale), GFP_KERNEL); if (!led->mm_current_scale) return -ENOMEM; From 52931e38d7eeb28bacc5e7fef2afddd08baa0ecb Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Fri, 18 Sep 2026 22:17:23 +0800 Subject: [PATCH 28/49] leds: Use assign_bit() where applicable Convert open-coded if/else with set_bit/clear_bit and their non-atomic __set_bit/__clear_bit variants to the assign_bit/__assign_bit API. Done with Coccinelle semantic patch and manual fixups. Signed-off-by: Peng Fan Link: https://patch.msgid.link/20260918141728.3022983-1-peng.fan@oss.nxp.com Signed-off-by: Lee Jones --- drivers/leds/led-core.c | 5 +---- drivers/leds/leds-pca963x.c | 5 +---- drivers/leds/trigger/ledtrig-netdev.c | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/drivers/leds/led-core.c b/drivers/leds/led-core.c index 073c547068cc..80e281dce195 100644 --- a/drivers/leds/led-core.c +++ b/drivers/leds/led-core.c @@ -267,10 +267,7 @@ void led_blink_set_oneshot(struct led_classdev *led_cdev, set_bit(LED_BLINK_ONESHOT, &led_cdev->work_flags); clear_bit(LED_BLINK_ONESHOT_STOP, &led_cdev->work_flags); - if (invert) - set_bit(LED_BLINK_INVERT, &led_cdev->work_flags); - else - clear_bit(LED_BLINK_INVERT, &led_cdev->work_flags); + assign_bit(LED_BLINK_INVERT, &led_cdev->work_flags, invert); led_blink_setup(led_cdev, delay_on, delay_off); } diff --git a/drivers/leds/leds-pca963x.c b/drivers/leds/leds-pca963x.c index 605cf7497e4c..5363afbe93ea 100644 --- a/drivers/leds/leds-pca963x.c +++ b/drivers/leds/leds-pca963x.c @@ -208,10 +208,7 @@ static void pca963x_track_power_state(struct pca963x_led *led, unsigned int led_ { unsigned long *leds_on = &led->chip->leds_on; - if (brightness) - set_bit(led_num, leds_on); - else - clear_bit(led_num, leds_on); + assign_bit(led_num, leds_on, brightness); } static int pca963x_sync_power_state(struct pca963x_led *led, unsigned long cached_leds) diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c index 5b4e92c14dbb..7e43fa30130c 100644 --- a/drivers/leds/trigger/ledtrig-netdev.c +++ b/drivers/leds/trigger/ledtrig-netdev.c @@ -414,10 +414,7 @@ static ssize_t netdev_led_attr_store(struct device *dev, const char *buf, return -EINVAL; } - if (state) - set_bit(bit, &mode); - else - clear_bit(bit, &mode); + assign_bit(bit, &mode, state); if (test_bit(TRIGGER_NETDEV_LINK, &mode) && (test_bit(TRIGGER_NETDEV_LINK_10, &mode) || From cfa0b7d98f3f5b731470ea9b6a6d4825aea1687e Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Fri, 18 Sep 2026 17:13:31 -0700 Subject: [PATCH 29/49] leds: rgb: qcom-lpg: Use fwnode APIs for LED parsing Convert LED and channel parsing in lpg_add_led() and lpg_parse_channel() from DT-specific helpers to the fwnode API. The probe loop now uses device_for_each_child_node_scoped(), dropping explicit node lifetime handling. Behaviour is unchanged for device tree: fwnode_get_child_node_count() and both child iteration helpers skip unavailable children on OF, and the fwnode handle is passed straight to LED registration. cdev->default_trigger is only assigned when fwnode_property_read_string() succeeds, so an absent linux,default-trigger property no longer yields an uninitialized pointer. Assisted-by: LLM Signed-off-by: Rosen Penev Link: https://patch.msgid.link/20260919001331.720072-1-rosenp@gmail.com Signed-off-by: Lee Jones --- drivers/leds/rgb/leds-qcom-lpg.c | 38 ++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/drivers/leds/rgb/leds-qcom-lpg.c b/drivers/leds/rgb/leds-qcom-lpg.c index d7d6518de30f..24ffe079f221 100644 --- a/drivers/leds/rgb/leds-qcom-lpg.c +++ b/drivers/leds/rgb/leds-qcom-lpg.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -1342,7 +1343,7 @@ static int lpg_add_pwm(struct lpg *lpg) return ret; } -static int lpg_parse_channel(struct lpg *lpg, struct device_node *np, +static int lpg_parse_channel(struct lpg *lpg, struct fwnode_handle *fw, struct lpg_channel **channel) { struct lpg_channel *chan; @@ -1350,17 +1351,18 @@ static int lpg_parse_channel(struct lpg *lpg, struct device_node *np, u32 reg; int ret; - ret = of_property_read_u32(np, "reg", ®); + ret = fwnode_property_read_u32(fw, "reg", ®); if (ret || !reg || reg > lpg->num_channels) - return dev_err_probe(lpg->dev, -EINVAL, "invalid \"reg\" of %pOFn\n", np); + return dev_err_probe(lpg->dev, -EINVAL, + "invalid \"reg\" of %s\n", fwnode_get_name(fw)); chan = &lpg->channels[reg - 1]; chan->in_use = true; - ret = of_property_read_u32(np, "color", &color); + ret = fwnode_property_read_u32(fw, "color", &color); if (ret < 0 && ret != -EINVAL) return dev_err_probe(lpg->dev, ret, - "failed to parse \"color\" of %pOF\n", np); + "failed to parse \"color\" of %s\n", fwnode_get_name(fw)); chan->color = color; @@ -1369,25 +1371,26 @@ static int lpg_parse_channel(struct lpg *lpg, struct device_node *np, return 0; } -static int lpg_add_led(struct lpg *lpg, struct device_node *np) +static int lpg_add_led(struct lpg *lpg, struct fwnode_handle *fw) { struct led_init_data init_data = {}; struct led_classdev *cdev; struct mc_subled *info; struct lpg_led *led; + const char *trigger; const char *state; int num_channels; u32 color = 0; int ret; int i; - ret = of_property_read_u32(np, "color", &color); + ret = fwnode_property_read_u32(fw, "color", &color); if (ret < 0 && ret != -EINVAL) return dev_err_probe(lpg->dev, ret, - "failed to parse \"color\" of %pOF\n", np); + "failed to parse \"color\" of %s\n", fwnode_get_name(fw)); if (color == LED_COLOR_ID_RGB || color == LED_COLOR_ID_MULTI) - num_channels = of_get_available_child_count(np); + num_channels = fwnode_get_child_node_count(fw); else num_channels = 1; @@ -1403,7 +1406,7 @@ static int lpg_add_led(struct lpg *lpg, struct device_node *np) if (!info) return -ENOMEM; i = 0; - for_each_available_child_of_node_scoped(np, child) { + fwnode_for_each_available_child_node_scoped(fw, child) { ret = lpg_parse_channel(lpg, child, &led->channels[i]); if (ret < 0) return ret; @@ -1414,7 +1417,7 @@ static int lpg_add_led(struct lpg *lpg, struct device_node *np) } led->mcdev.subled_info = info; - led->mcdev.num_colors = num_channels; + led->mcdev.num_colors = i; cdev = &led->mcdev.led_cdev; cdev->brightness_set_blocking = lpg_brightness_mc_set; @@ -1426,7 +1429,7 @@ static int lpg_add_led(struct lpg *lpg, struct device_node *np) cdev->pattern_clear = lpg_pattern_mc_clear; } } else { - ret = lpg_parse_channel(lpg, np, &led->channels[0]); + ret = lpg_parse_channel(lpg, fw, &led->channels[0]); if (ret < 0) return ret; @@ -1441,14 +1444,15 @@ static int lpg_add_led(struct lpg *lpg, struct device_node *np) } } - cdev->default_trigger = of_get_property(np, "linux,default-trigger", NULL); + if (!fwnode_property_read_string(fw, "linux,default-trigger", &trigger)) + cdev->default_trigger = trigger; if (lpg->lpg_chan_sdam) cdev->max_brightness = PPG_MAX_LED_BRIGHTNESS; else cdev->max_brightness = LPG_RESOLUTION_9BIT - 1; - if (!of_property_read_string(np, "default-state", &state) && + if (!fwnode_property_read_string(fw, "default-state", &state) && !strcmp(state, "on")) cdev->brightness = cdev->max_brightness; else @@ -1456,7 +1460,7 @@ static int lpg_add_led(struct lpg *lpg, struct device_node *np) cdev->brightness_set_blocking(cdev, cdev->brightness); - init_data.fwnode = of_fwnode_handle(np); + init_data.fwnode = fw; if (color == LED_COLOR_ID_RGB || color == LED_COLOR_ID_MULTI) ret = devm_led_classdev_multicolor_register_ext(lpg->dev, &led->mcdev, &init_data); @@ -1638,8 +1642,8 @@ static int lpg_probe(struct platform_device *pdev) if (ret < 0) return ret; - for_each_available_child_of_node_scoped(pdev->dev.of_node, np) { - ret = lpg_add_led(lpg, np); + device_for_each_child_node_scoped(&pdev->dev, child) { + ret = lpg_add_led(lpg, child); if (ret) return ret; } From d4948551280b1d52c1896b7c80d5383b7d8ad7c5 Mon Sep 17 00:00:00 2001 From: Svyatoslav Ryhel Date: Tue, 21 Jul 2026 12:56:48 +0300 Subject: [PATCH 30/49] dt-bindings: leds: leds-cpcap: Convert to DT schema Convert LEDs devicetree bindings for the Motorola CPCAP MFD from TXT to YAML format. This patch does not change any functionality; the bindings remain the same. Signed-off-by: Svyatoslav Ryhel Reviewed-by: Rob Herring (Arm) Link: https://patch.msgid.link/20260721095654.429346-2-clamor95@gmail.com Signed-off-by: Lee Jones --- .../devicetree/bindings/leds/leds-cpcap.txt | 29 ------------- .../bindings/leds/motorola,cpcap-leds.yaml | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 29 deletions(-) delete mode 100644 Documentation/devicetree/bindings/leds/leds-cpcap.txt create mode 100644 Documentation/devicetree/bindings/leds/motorola,cpcap-leds.yaml diff --git a/Documentation/devicetree/bindings/leds/leds-cpcap.txt b/Documentation/devicetree/bindings/leds/leds-cpcap.txt deleted file mode 100644 index ebf7cdc7f70c..000000000000 --- a/Documentation/devicetree/bindings/leds/leds-cpcap.txt +++ /dev/null @@ -1,29 +0,0 @@ -Motorola CPCAP PMIC LEDs ------------------------- - -This module is part of the CPCAP. For more details about the whole -chip see Documentation/devicetree/bindings/mfd/motorola-cpcap.txt. - -Requires node properties: -- compatible: should be one of - * "motorola,cpcap-led-mdl" (Main Display Lighting) - * "motorola,cpcap-led-kl" (Keyboard Lighting) - * "motorola,cpcap-led-adl" (Aux Display Lighting) - * "motorola,cpcap-led-red" (Red Triode) - * "motorola,cpcap-led-green" (Green Triode) - * "motorola,cpcap-led-blue" (Blue Triode) - * "motorola,cpcap-led-cf" (Camera Flash) - * "motorola,cpcap-led-bt" (Bluetooth) - * "motorola,cpcap-led-cp" (Camera Privacy LED) -- label: see Documentation/devicetree/bindings/leds/common.txt -- vdd-supply: A phandle to the regulator powering the LED - -Example: - -&cpcap { - cpcap_led_red: red-led { - compatible = "motorola,cpcap-led-red"; - label = "cpcap:red"; - vdd-supply = <&sw5>; - }; -}; diff --git a/Documentation/devicetree/bindings/leds/motorola,cpcap-leds.yaml b/Documentation/devicetree/bindings/leds/motorola,cpcap-leds.yaml new file mode 100644 index 000000000000..c8e7b88a05cc --- /dev/null +++ b/Documentation/devicetree/bindings/leds/motorola,cpcap-leds.yaml @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/leds/motorola,cpcap-leds.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Motorola CPCAP PMIC LEDs + +maintainers: + - Svyatoslav Ryhel + +description: + This module is part of the Motorola CPCAP MFD device. For more details + see Documentation/devicetree/bindings/mfd/motorola,cpcap.yaml. LEDs are + represented as sub-nodes of the PMIC node on the device tree. + +allOf: + - $ref: /schemas/leds/common.yaml# + +properties: + compatible: + enum: + - motorola,cpcap-led-adl # Display Lighting + - motorola,cpcap-led-blue # Blue Triode + - motorola,cpcap-led-bt # Bluetooth + - motorola,cpcap-led-cf # Camera Flash + - motorola,cpcap-led-cp # Camera Privacy LED + - motorola,cpcap-led-green # Green Triode + - motorola,cpcap-led-kl # Keyboard Lighting + - motorola,cpcap-led-mdl # Main Display Lighting + - motorola,cpcap-led-red # Red Triode + + vdd-supply: true + +required: + - compatible + - label + - vdd-supply + +unevaluatedProperties: false + +... From 6887f40dadb5a263de1d9e4b5517400c08089d35 Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:15 +0800 Subject: [PATCH 31/49] leds: class: Always protect brightness_show() with led_access The led_classdev_register_ext() protects transient states when registering with the led_access mutex and relies on all sensitive sysfs attribute callbacks holding the mutex to prevent exposing transient states. The Fixes: commit broke this as it accessed trigger_lock without holding led_access. Fix it by protecting brightness_show() with led_access in all code paths. Fixes: b819dc7d8fb2 ("leds: core: Report ENODATA for brightness of hardware controlled LED") Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-1-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- drivers/leds/led-class.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c index a51b0ed53886..4259533dafa1 100644 --- a/drivers/leds/led-class.c +++ b/drivers/leds/led-class.c @@ -43,13 +43,13 @@ static ssize_t brightness_show(struct device *dev, struct led_classdev *led_cdev = dev_get_drvdata(dev); unsigned int brightness; - if (led_trigger_is_hw_controlled(led_cdev)) - return -ENODATA; + scoped_guard(mutex, &led_cdev->led_access) { + if (led_trigger_is_hw_controlled(led_cdev)) + return -ENODATA; - mutex_lock(&led_cdev->led_access); - led_update_brightness(led_cdev); - brightness = led_cdev->brightness; - mutex_unlock(&led_cdev->led_access); + led_update_brightness(led_cdev); + brightness = led_cdev->brightness; + } return sysfs_emit(buf, "%u\n", brightness); } From 7879d6277485697cf37eb3c1b89ce260d8c67e4f Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:16 +0800 Subject: [PATCH 32/49] leds: trigger: Move led_trigger_is_hw_controlled() to the right place Currently led_trigger_is_hw_controlled() is placed at led-class.c, which is not an right place as it falls into the triggers namespace and does triggers stuff. Move it into led-triggers.c, and split it into locked and unlocked variant for convenience. Fixes: b819dc7d8fb2 ("leds: core: Report ENODATA for brightness of hardware controlled LED") Acked-by: Ike Panhc Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-2-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- drivers/leds/led-class.c | 10 ---------- drivers/leds/led-triggers.c | 19 +++++++++++++++++++ include/linux/leds.h | 8 ++++++++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c index 4259533dafa1..7110bfbe3b83 100644 --- a/drivers/leds/led-class.c +++ b/drivers/leds/led-class.c @@ -27,16 +27,6 @@ static LIST_HEAD(leds_lookup_list); static struct workqueue_struct *leds_wq; -static bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev) -{ -#ifdef CONFIG_LEDS_TRIGGERS - guard(rwsem_read)(&led_cdev->trigger_lock); - return led_cdev->trigger && led_cdev->trigger->trigger_type; -#else - return false; -#endif -} - static ssize_t brightness_show(struct device *dev, struct device_attribute *attr, char *buf) { diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c index b1223218bda1..bf2543538ed0 100644 --- a/drivers/leds/led-triggers.c +++ b/drivers/leds/led-triggers.c @@ -7,9 +7,11 @@ * Author: Richard Purdie */ +#include #include #include #include +#include #include #include #include @@ -33,6 +35,23 @@ trigger_relevant(struct led_classdev *led_cdev, struct led_trigger *trig) return !trig->trigger_type || trig->trigger_type == led_cdev->trigger_type; } +static bool __led_trigger_is_hw_controlled(struct led_classdev *led_cdev) +{ + lockdep_assert_held(&led_cdev->trigger_lock); + + if (!led_cdev->trigger) + return false; + + return led_cdev->trigger->trigger_type; +} + +bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev) +{ + guard(rwsem_read)(&led_cdev->trigger_lock); + return __led_trigger_is_hw_controlled(led_cdev); +} +EXPORT_SYMBOL_GPL(led_trigger_is_hw_controlled); + ssize_t led_trigger_write(struct file *filp, struct kobject *kobj, const struct bin_attribute *bin_attr, char *buf, loff_t pos, size_t count) diff --git a/include/linux/leds.h b/include/linux/leds.h index a515f075c29a..46ad2e03e1c6 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -534,6 +534,8 @@ void led_trigger_set_default(struct led_classdev *led_cdev); int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trigger); void led_trigger_remove(struct led_classdev *led_cdev); +bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev); + static inline void led_set_trigger_data(struct led_classdev *led_cdev, void *trigger_data) { @@ -584,6 +586,12 @@ static inline int led_trigger_set(struct led_classdev *led_cdev, } static inline void led_trigger_remove(struct led_classdev *led_cdev) {} + +static inline bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev) +{ + return false; +} + static inline void led_set_trigger_data(struct led_classdev *led_cdev) {} static inline void *led_get_trigger_data(struct led_classdev *led_cdev) { From 96b4ddd6d2d8c645d672f1f26130685d9f40cf2f Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:17 +0800 Subject: [PATCH 33/49] leds: class: Remove hardware control trigger when writing brightness Since commit b819dc7d8fb2 ("leds: core: Report ENODATA for brightness of hardware controlled LED"), the brightness attribute becomes write-only when the LED is controlled fully by the hardware. A write-only attribute is very confusing. Moreover, most LED drivers set hardware brightness innocently with the side effect of disabling hardware control, but the hardware control trigger remains active, resulting in the software and hardware being out of sync. Fix it by removing the hardware control trigger when writing the brightness attribute. This should also match the semantics of hardware control: When the LED is in hw control, no software blink is possible and doing so will effectively disable hw control. Fixes: b819dc7d8fb2 ("leds: core: Report ENODATA for brightness of hardware controlled LED") Acked-by: Ike Panhc Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-3-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- Documentation/ABI/testing/sysfs-class-led | 4 +++- drivers/leds/led-class.c | 3 +++ drivers/leds/led-triggers.c | 9 +++++++++ include/linux/leds.h | 2 ++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Documentation/ABI/testing/sysfs-class-led b/Documentation/ABI/testing/sysfs-class-led index d4c918cc11a1..f6d4e17e1a3b 100644 --- a/Documentation/ABI/testing/sysfs-class-led +++ b/Documentation/ABI/testing/sysfs-class-led @@ -28,7 +28,9 @@ Description: Writing 0 to this file clears active trigger. Writing non-zero to this file while trigger is active changes the - top brightness trigger is going to use. + top brightness trigger is going to use, unless the current + trigger is offloaded to hardware (in which case writing any + value will deactivate the trigger). diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c index 7110bfbe3b83..39cc2f3ea63f 100644 --- a/drivers/leds/led-class.c +++ b/drivers/leds/led-class.c @@ -64,6 +64,9 @@ static ssize_t brightness_store(struct device *dev, if (state == LED_OFF) led_trigger_remove(led_cdev); + else + led_trigger_remove_hw_control(led_cdev); + led_set_brightness(led_cdev, state); ret = size; diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c index bf2543538ed0..804a04b326c4 100644 --- a/drivers/leds/led-triggers.c +++ b/drivers/leds/led-triggers.c @@ -287,6 +287,15 @@ void led_trigger_remove(struct led_classdev *led_cdev) } EXPORT_SYMBOL_GPL(led_trigger_remove); +void led_trigger_remove_hw_control(struct led_classdev *led_cdev) +{ + guard(rwsem_write)(&led_cdev->trigger_lock); + + if (__led_trigger_is_hw_controlled(led_cdev)) + led_trigger_set(led_cdev, NULL); +} +EXPORT_SYMBOL_GPL(led_trigger_remove_hw_control); + static bool led_match_default_trigger(struct led_classdev *led_cdev, struct led_trigger *trig) { diff --git a/include/linux/leds.h b/include/linux/leds.h index 46ad2e03e1c6..d778709f5b1b 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -533,6 +533,7 @@ void led_trigger_blink_oneshot(struct led_trigger *trigger, void led_trigger_set_default(struct led_classdev *led_cdev); int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trigger); void led_trigger_remove(struct led_classdev *led_cdev); +void led_trigger_remove_hw_control(struct led_classdev *led_cdev); bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev); @@ -586,6 +587,7 @@ static inline int led_trigger_set(struct led_classdev *led_cdev, } static inline void led_trigger_remove(struct led_classdev *led_cdev) {} +static inline void led_trigger_remove_hw_control(struct led_classdev *led_cdev) {} static inline bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev) { From 43b83e7462e8e9b96e14262c0b6aa739f01c38f7 Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:18 +0800 Subject: [PATCH 34/49] leds: trigger: Move led_trigger_group to the right place Currently led_trigger_group and its members are placed at led-class.c, but they falls into the triggers namespace and does triggers stuff. Placing them at led-class.c also requires the read and write callback to be exported and results in an extra #ifdef block. Move it into led-triggers.c, so that it becomes the only symbol to be exported for sysfs attributes. Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-4-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- drivers/leds/led-class.c | 11 ----------- drivers/leds/led-triggers.c | 25 +++++++++++++++++-------- drivers/leds/leds.h | 7 +------ 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c index 39cc2f3ea63f..7f51715fac69 100644 --- a/drivers/leds/led-class.c +++ b/drivers/leds/led-class.c @@ -90,17 +90,6 @@ static ssize_t max_brightness_show(struct device *dev, } static DEVICE_ATTR_RO(max_brightness); -#ifdef CONFIG_LEDS_TRIGGERS -static const BIN_ATTR(trigger, 0644, led_trigger_read, led_trigger_write, 0); -static const struct bin_attribute *const led_trigger_bin_attrs[] = { - &bin_attr_trigger, - NULL, -}; -static const struct attribute_group led_trigger_group = { - .bin_attrs = led_trigger_bin_attrs, -}; -#endif - static struct attribute *led_class_attrs[] = { &dev_attr_brightness.attr, &dev_attr_max_brightness.attr, diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c index 804a04b326c4..de6056bc80e6 100644 --- a/drivers/leds/led-triggers.c +++ b/drivers/leds/led-triggers.c @@ -52,9 +52,9 @@ bool led_trigger_is_hw_controlled(struct led_classdev *led_cdev) } EXPORT_SYMBOL_GPL(led_trigger_is_hw_controlled); -ssize_t led_trigger_write(struct file *filp, struct kobject *kobj, - const struct bin_attribute *bin_attr, char *buf, - loff_t pos, size_t count) +static ssize_t trigger_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, char *buf, + loff_t pos, size_t count) { struct device *dev = kobj_to_dev(kobj); struct led_classdev *led_cdev = dev_get_drvdata(dev); @@ -97,7 +97,6 @@ ssize_t led_trigger_write(struct file *filp, struct kobject *kobj, mutex_unlock(&led_cdev->led_access); return ret; } -EXPORT_SYMBOL_GPL(led_trigger_write); __printf(3, 4) static int led_trigger_snprintf(char *buf, ssize_t size, const char *fmt, ...) @@ -149,9 +148,9 @@ static int led_trigger_format(char *buf, size_t size, * attribute, which is not limited by length. This is _not_ good design, do not * copy it. */ -ssize_t led_trigger_read(struct file *filp, struct kobject *kobj, - const struct bin_attribute *attr, char *buf, - loff_t pos, size_t count) +static ssize_t trigger_read(struct file *filp, struct kobject *kobj, + const struct bin_attribute *attr, char *buf, + loff_t pos, size_t count) { struct device *dev = kobj_to_dev(kobj); struct led_classdev *led_cdev = dev_get_drvdata(dev); @@ -179,7 +178,17 @@ ssize_t led_trigger_read(struct file *filp, struct kobject *kobj, return len; } -EXPORT_SYMBOL_GPL(led_trigger_read); +static const BIN_ATTR_RW(trigger, 0); + +static const struct bin_attribute *const led_trigger_bin_attrs[] = { + &bin_attr_trigger, + NULL +}; + +const struct attribute_group led_trigger_group = { + .bin_attrs = led_trigger_bin_attrs, +}; +EXPORT_SYMBOL_GPL(led_trigger_group); /* Caller must ensure led_cdev->trigger_lock held */ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig) diff --git a/drivers/leds/leds.h b/drivers/leds/leds.h index bee46651e068..c1db21e943b0 100644 --- a/drivers/leds/leds.h +++ b/drivers/leds/leds.h @@ -21,14 +21,9 @@ void led_init_core(struct led_classdev *led_cdev); void led_stop_software_blink(struct led_classdev *led_cdev); void led_set_brightness_nopm(struct led_classdev *led_cdev, unsigned int value); void led_set_brightness_nosleep(struct led_classdev *led_cdev, unsigned int value); -ssize_t led_trigger_read(struct file *filp, struct kobject *kobj, - const struct bin_attribute *attr, char *buf, - loff_t pos, size_t count); -ssize_t led_trigger_write(struct file *filp, struct kobject *kobj, - const struct bin_attribute *bin_attr, char *buf, - loff_t pos, size_t count); extern struct rw_semaphore leds_list_lock; extern struct list_head leds_list; +extern const struct attribute_group led_trigger_group; #endif /* __LEDS_H_INCLUDED */ From 8e04e5382aa989ae9a36dc0cb3f3c90347d5607f Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:19 +0800 Subject: [PATCH 35/49] leds: trigger: Add hw_offloaded() callback and provide trigger_may_offload_to_hw attribute There are multiple triggers implementing hardware control. However, the LED trigger core doesn't really know the hardware control (offloaded) state since the coordination is done directly between the trigger and the LED driver. It can only assume private triggers as offloaded and generic ones as not offloaded. Add an hw_offloaded() callback so that triggers can report their offloaded states to the LED trigger core. When unimplemented, it defaults to true for private triggers and false for generic ones to keep the current behavior unchanged. With that, provide a new attribute "trigger_may_offload_to_hw", so that userspace can determine: - if the LED device supports hardware control (supported => visible) - which trigger is the hardware control trigger selected by the LED device and offloaded to hardware ("[foo_trigger]") Note: the documentation describes the attribute as "returning a list" despite the LED core currently only supports one hardware control trigger per LED device. This is intentional to make the attribute extensible in the future without breaking userspace. IOW, userspace should parse the new attribute in the same way as /sys/class/leds//trigger. Acked-by: Ike Panhc Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-5-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- Documentation/ABI/testing/sysfs-class-led | 21 +++++++++++++ Documentation/leds/leds-class.rst | 20 +++++++++++++ drivers/leds/led-triggers.c | 36 +++++++++++++++++++++++ include/linux/leds.h | 1 + 4 files changed, 78 insertions(+) diff --git a/Documentation/ABI/testing/sysfs-class-led b/Documentation/ABI/testing/sysfs-class-led index f6d4e17e1a3b..123e3a15b7d6 100644 --- a/Documentation/ABI/testing/sysfs-class-led +++ b/Documentation/ABI/testing/sysfs-class-led @@ -80,6 +80,27 @@ Description: (which would often be configured in the device tree for the hardware). +What: /sys/class/leds//trigger_may_offload_to_hw +Date: September 2026 +KernelVersion: 7.4 +Contact: linux-leds@vger.kernel.org +Description: + Names and states of triggers that may be offloaded to hardware. + Such triggers are also called "hardware control trigger" in some + context. + + Only exists when the LED supports trigger offload. + + Reading this file returns a list of triggers that are capable to + be offloaded. The optional brackets around the trigger name + indicate the state of the current trigger: + + - `foo_trigger`: the trigger is not selected, or selected but + falls back to software blink for some reasons, e.g., + incompatible trigger parameters. + - `[foo_trigger]`: the trigger is selected and offloaded to + hardware. + What: /sys/class/leds//inverted Date: January 2011 KernelVersion: 2.6.38 diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst index 3913966cfdac..ea478989aae2 100644 --- a/Documentation/leds/leds-class.rst +++ b/Documentation/leds/leds-class.rst @@ -242,6 +242,9 @@ ops and needs to declare specific support for the supported triggers. With hw control we refer to the LED driven by hardware. +A sysfs attribute `trigger_may_offload_to_hw` is provided for userspace +to query supported triggers and their states. + LED driver must define the following value to support hw control: - hw_control_trigger: @@ -298,6 +301,15 @@ LED driver must implement the following API to support hw control: Returns a pointer to a struct device or NULL if nothing is currently attached. +LED trigger should implement the following API to indicate hw control: + - hw_offloaded: + return a boolean indicating if the trigger is currently + offloaded to hardware. + + If a trigger doesn't implement this callback, the default + value will be true for private triggers and false for generic + ones. + LED driver can activate additional modes by default to workaround the impossibility of supporting each different mode on the supported trigger. Examples are hardcoding the blink speed to a set interval, enable special @@ -311,6 +323,14 @@ the end use hw_control_set to activate hw control. A trigger can use hw_control_get to check if a LED is already in hw control and init their flags. +Alternatively, a private trigger can be implemented along with the LED driver if +the LED's hardware control doesn't fit any generic trigger. To associate the +private trigger with the LED classdev, their `trigger_type` must be the same. To +declare that the private trigger provides hardware control for the associated +LED classdev, set the `hw_control_trigger` string to the trigger's name. Since +both the LED classdev and the private trigger are in the same LED driver, it's +not necessary for them to coordinate via `hw_control_*` callbacks. + When the LED is in hw control, no software blink is possible and doing so will effectively disable hw control. diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c index de6056bc80e6..38ce3350c870 100644 --- a/drivers/leds/led-triggers.c +++ b/drivers/leds/led-triggers.c @@ -42,6 +42,10 @@ static bool __led_trigger_is_hw_controlled(struct led_classdev *led_cdev) if (!led_cdev->trigger) return false; + if (led_cdev->trigger->hw_offloaded) + return led_cdev->trigger->hw_offloaded(led_cdev); + + /* Otherwise assume private triggers are always offloaded. */ return led_cdev->trigger->trigger_type; } @@ -185,8 +189,40 @@ static const struct bin_attribute *const led_trigger_bin_attrs[] = { NULL }; +static ssize_t trigger_may_offload_to_hw_show(struct device *dev, + const struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + bool offloaded = led_trigger_is_hw_controlled(led_cdev); + + return sysfs_emit(buf, "%s%s%s\n", + offloaded ? "[" : "", + led_cdev->hw_control_trigger, + offloaded ? "]" : ""); +} +static const DEVICE_ATTR_RO(trigger_may_offload_to_hw); + +static const struct attribute *const led_trigger_attrs[] = { + &dev_attr_trigger_may_offload_to_hw.attr, + NULL +}; + +static umode_t led_trigger_is_visible(struct kobject *kobj, + const struct attribute *attr, int idx) +{ + struct device *dev = kobj_to_dev(kobj); + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + if (attr == &dev_attr_trigger_may_offload_to_hw.attr) + return led_cdev->hw_control_trigger ? attr->mode : 0; + + return attr->mode; +} + const struct attribute_group led_trigger_group = { .bin_attrs = led_trigger_bin_attrs, + .attrs_const = led_trigger_attrs, + .is_visible_const = led_trigger_is_visible, }; EXPORT_SYMBOL_GPL(led_trigger_group); diff --git a/include/linux/leds.h b/include/linux/leds.h index d778709f5b1b..9a0bfd985b46 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -485,6 +485,7 @@ struct led_trigger { const char *name; int (*activate)(struct led_classdev *led_cdev); void (*deactivate)(struct led_classdev *led_cdev); + bool (*hw_offloaded)(struct led_classdev *led_cdev); /* Brightness set by led_trigger_event */ enum led_brightness brightness; From b366a50d39d13f8230f959b457abcaeba1e7d121 Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:20 +0800 Subject: [PATCH 36/49] leds: cros_ec: Implement hw_offloaded() trigger callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "chromeos-auto" is a private hardware control trigger which always stays in hardware control. Implement hw_offloaded() callback with its return value to be always true to reflect this. Reviewed-by: Thomas Weißschuh Acked-by: Ike Panhc Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-6-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- drivers/leds/leds-cros_ec.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/leds/leds-cros_ec.c b/drivers/leds/leds-cros_ec.c index 1844d0cd5f52..0e93aa28a8b1 100644 --- a/drivers/leds/leds-cros_ec.c +++ b/drivers/leds/leds-cros_ec.c @@ -85,12 +85,18 @@ static int cros_ec_led_trigger_activate(struct led_classdev *led_cdev) return cros_ec_led_send_cmd(priv->cros_ec, &arg); } +static bool cros_ec_led_trigger_hw_offloaded(struct led_classdev *led_cdev) +{ + return true; +} + static struct led_hw_trigger_type cros_ec_led_trigger_type; static struct led_trigger cros_ec_led_trigger = { .name = "chromeos-auto", .trigger_type = &cros_ec_led_trigger_type, .activate = cros_ec_led_trigger_activate, + .hw_offloaded = cros_ec_led_trigger_hw_offloaded, }; static int cros_ec_led_brightness_set_blocking(struct led_classdev *led_cdev, From f5005c7cec7d4cff58f25442ed6dfca56e42b16d Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:21 +0800 Subject: [PATCH 37/49] leds: turris-omnia: Implement hw_offloaded() trigger callback and declare hw_control_trigger "omnia-mcu" is a private hardware control trigger which always stays in hardware control mode. Implement hw_offloaded() callback with its return value to be always true to reflect this. Meanwhile, declare it as a hardware control trigger as it's forgotten before. Acked-by: Ike Panhc Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-7-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- drivers/leds/leds-turris-omnia.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/leds/leds-turris-omnia.c b/drivers/leds/leds-turris-omnia.c index ed6a47bbb44f..57128ae7e8fd 100644 --- a/drivers/leds/leds-turris-omnia.c +++ b/drivers/leds/leds-turris-omnia.c @@ -195,10 +195,16 @@ static void omnia_hwtrig_deactivate(struct led_classdev *cdev) err); } +static bool omnia_hwtrig_hw_offloaded(struct led_classdev *cdev) +{ + return true; +} + static struct led_trigger omnia_hw_trigger = { .name = "omnia-mcu", .activate = omnia_hwtrig_activate, .deactivate = omnia_hwtrig_deactivate, + .hw_offloaded = omnia_hwtrig_hw_offloaded, .trigger_type = &omnia_hw_trigger_type, }; @@ -251,6 +257,7 @@ static int omnia_led_register(struct i2c_client *client, struct omnia_led *led, * by LED class from the linux,default-trigger property. */ cdev->default_trigger = omnia_hw_trigger.name; + cdev->hw_control_trigger = omnia_hw_trigger.name; /* Put the LED into software mode */ ret = omnia_cmd_write_u8(client, OMNIA_CMD_LED_MODE, OMNIA_CMD_LED_MODE_LED(led->reg) | From 072e0e799bc6b3909afc8fd7fac750b31e37b438 Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:22 +0800 Subject: [PATCH 38/49] leds: trigger: netdev: Implement hw_offloaded() callback "netdev" can run in hardware control according to hardware capabilities and trigger options. Implement hw_offloaded() callback to provide its hardware control state to the LED core, and document the relation between the custom "offloaded" attribute and the generic "trigger_may_offload_to_hw" attribute. The callback mimics how the existing "offloaded" attribute does, i.e., locklessly reads hw_control, as it's just a hint and don't need to be accurate. Acked-by: Ike Panhc Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-8-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- Documentation/ABI/testing/sysfs-class-led | 3 +++ Documentation/ABI/testing/sysfs-class-led-trigger-netdev | 3 +++ drivers/leds/trigger/ledtrig-netdev.c | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/Documentation/ABI/testing/sysfs-class-led b/Documentation/ABI/testing/sysfs-class-led index 123e3a15b7d6..ea113fed10ef 100644 --- a/Documentation/ABI/testing/sysfs-class-led +++ b/Documentation/ABI/testing/sysfs-class-led @@ -101,6 +101,9 @@ Description: - `[foo_trigger]`: the trigger is selected and offloaded to hardware. + The "netdev" trigger also provides a custom attribute to + indicate its state, see `/sys/class/leds//offloaded`. + What: /sys/class/leds//inverted Date: January 2011 KernelVersion: 2.6.38 diff --git a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev index ed46b37ab8a2..203ea58396ed 100644 --- a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev +++ b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev @@ -75,6 +75,9 @@ Description: If 1, the LED blinking in requested mode is offloaded to hardware. + LED trigger core also provides a generic attribute for this + purpose, see `/sys/class/leds//trigger_may_offload_to_hw`. + What: /sys/class/leds//link_10 Date: Jun 2023 KernelVersion: 6.5 diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c index 7e43fa30130c..5b0132484594 100644 --- a/drivers/leds/trigger/ledtrig-netdev.c +++ b/drivers/leds/trigger/ledtrig-netdev.c @@ -795,10 +795,18 @@ static void netdev_trig_deactivate(struct led_classdev *led_cdev) kfree(trigger_data); } +static bool netdev_trig_hw_offloaded(struct led_classdev *led_cdev) +{ + struct led_netdev_data *trigger_data = led_get_trigger_data(led_cdev); + + return trigger_data->hw_control; +} + static struct led_trigger netdev_led_trigger = { .name = "netdev", .activate = netdev_trig_activate, .deactivate = netdev_trig_deactivate, + .hw_offloaded = netdev_trig_hw_offloaded, .groups = netdev_trig_groups, }; From 7b9a609575615a3ba18a31a95282e756e34bd506 Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:23 +0800 Subject: [PATCH 39/49] leds: trigger: Enforce strict checks in led_trigger_is_hw_controlled() With all existing triggers adopting the new interface, strict checks could be enforced to make the semantics of hardware control triggers clearer. In detail, a hardware control trigger should: - Implement hw_offloaded() callback to indicate hardware control - Associate with the LED classdev's hw_control_trigger string The fallback path is kept, but a warning message is emitted when it reaches there. Acked-by: Ike Panhc Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-9-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- drivers/leds/led-triggers.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c index 38ce3350c870..6fee3145caab 100644 --- a/drivers/leds/led-triggers.c +++ b/drivers/leds/led-triggers.c @@ -42,9 +42,17 @@ static bool __led_trigger_is_hw_controlled(struct led_classdev *led_cdev) if (!led_cdev->trigger) return false; + if (!led_cdev->hw_control_trigger || + strcmp(led_cdev->hw_control_trigger, led_cdev->trigger->name)) + return false; + if (led_cdev->trigger->hw_offloaded) return led_cdev->trigger->hw_offloaded(led_cdev); + dev_warn_once(led_cdev->dev, + "Hardware control trigger %s doesn't provide offloaded state\n", + led_cdev->trigger->name); + /* Otherwise assume private triggers are always offloaded. */ return led_cdev->trigger->trigger_type; } From 05b4738b0078f7d6f154f68068a11c8a0635e9df Mon Sep 17 00:00:00 2001 From: Rong Zhang Date: Mon, 21 Sep 2026 03:40:24 +0800 Subject: [PATCH 40/49] leds: trigger: Add led_trigger_notify_hw_control_changed() interface Some hardware can autonomously activate/deactivate hardware control. After that, the LED hardware notifies the LED driver. Currently, there is no mechanism for LED drivers to notify the LED core about such events and initiate a trigger transition to reflect the hardware state. Add a new interface called led_trigger_notify_hw_control_changed(), so that LED drivers can call it to notify the LED core about the transition. The interface only allows two transitions: 1. "none" => private trigger 2. private trigger => "none" If the current trigger is neither the private trigger nor "none", no transition will be made. This protects the currently selected software trigger. Note that LED_OFF won't be emitted during the #2 transition, as some hardware may have selected a new brightness level during its hardware state transition (e.g., laptop keyboards with a shortcut cycling through different backlight brightnesses and auto mode). The interface is designed as a void function as any failure should be non-fatal and the result of transition should not have any impact on the LED drivers' event handling procedures. To use the interface, the config LEDS_TRIGGERS_HW_CHANGED must be enabled, and the LED driver must set the LED_TRIG_HW_CHANGED flag for the classdev. By default, the config is enabled when LEDS_BRIGHTNESS_HW_CHANGED is enabled. Acked-by: Ike Panhc Signed-off-by: Rong Zhang Link: https://patch.msgid.link/20260921-leds-trigger-hw-changed-v7-10-fe3cdb6dec51@rong.moe Signed-off-by: Lee Jones --- Documentation/leds/leds-class.rst | 52 ++++++++++++++++++++ drivers/leds/led-class.c | 10 ++++ drivers/leds/led-triggers.c | 80 ++++++++++++++++++++++++++++++- drivers/leds/leds.h | 1 + drivers/leds/trigger/Kconfig | 10 ++++ include/linux/leds.h | 13 +++++ 6 files changed, 164 insertions(+), 2 deletions(-) diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst index ea478989aae2..464e54cabf52 100644 --- a/Documentation/leds/leds-class.rst +++ b/Documentation/leds/leds-class.rst @@ -334,6 +334,58 @@ not necessary for them to coordinate via `hw_control_*` callbacks. When the LED is in hw control, no software blink is possible and doing so will effectively disable hw control. +Hardware-initiated trigger transition +===================================== + +Some hardware can autonomously activate/deactivate hardware control. After that, +the LED hardware notifies the LED driver. + +If the driver can detect such transitions and thus wants to notify the LED core +to update the current trigger then the `LED_TRIG_HW_CHANGED` flag must be set in +flags before registering. To update the current trigger accordingly, call +`led_trigger_notify_hw_control_changed` on the LED classdev. + +This capability is restricted to the LED device's private trigger. The private +trigger must have been properly registered (see above) and named after +`hw_control_trigger`. + +Only two transitions are defined: + +- "none" => private trigger: + This happens when the hardware autonomously activates hardware control + and when "none" (i.e., no trigger) is currently active. If the private + trigger is already active when the method is called, this is essentially + a no-op. + + The activation sequence for the private trigger will be executed as + normal. + + The LED driver and its private trigger must be able to handle the + activation sequence even if the hardware is currently in hardware + control. + + If error occurs in the activation sequence, the LED Trigger core reverts + the effective trigger to "none". + +- private trigger => "none" + This happens when the hardware autonomously deactivates hardware control + and when the private trigger is currently active. If "none" (i.e., no + trigger) is active when the method is called, this is essentially a + no-op. + + The deactivation sequence for the private trigger will be executed as + normal, except that the current LED brightness is retained. The reason + for keeping the brightness unchanged is that some hardware may choose a + specific brightness instead of simply turning off the LED after + autonomously deactivating hardware control. + + The LED driver and its private trigger must be able to handle the + deactivation sequence even if the hardware is not currently in hardware + control. + +If the current trigger is neither the private trigger nor "none", no transition +will be made. + Known Issues ============ diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c index 7f51715fac69..00be3cb96c00 100644 --- a/drivers/leds/led-class.c +++ b/drivers/leds/led-class.c @@ -559,6 +559,11 @@ int led_classdev_register_ext(struct device *parent, #endif #ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED led_cdev->brightness_hw_changed = -1; +#endif +#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED + if (led_cdev->flags & LED_TRIG_HW_CHANGED) + INIT_WORK(&led_cdev->trigger_hw_changed_work, + led_trigger_hw_control_changed_worker); #endif if (!led_cdev->max_brightness) led_cdev->max_brightness = LED_FULL; @@ -598,6 +603,11 @@ void led_classdev_unregister(struct led_classdev *led_cdev) if (IS_ERR_OR_NULL(led_cdev->dev)) return; +#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED + if (led_cdev->flags & LED_TRIG_HW_CHANGED) + disable_work_sync(&led_cdev->trigger_hw_changed_work); +#endif + #ifdef CONFIG_LEDS_TRIGGERS down_write(&led_cdev->trigger_lock); if (led_cdev->trigger) diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c index 6fee3145caab..f16603b5ff44 100644 --- a/drivers/leds/led-triggers.c +++ b/drivers/leds/led-triggers.c @@ -7,7 +7,9 @@ * Author: Richard Purdie */ +#include #include +#include #include #include #include @@ -235,7 +237,8 @@ const struct attribute_group led_trigger_group = { EXPORT_SYMBOL_GPL(led_trigger_group); /* Caller must ensure led_cdev->trigger_lock held */ -int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig) +static int __led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig, + bool hw_triggered) { char *event = NULL; char *envp[2]; @@ -266,7 +269,21 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig) led_cdev->trigger_data = NULL; led_cdev->activated = false; led_cdev->flags &= ~LED_INIT_DEFAULT_TRIGGER; - led_set_brightness(led_cdev, LED_OFF); + + /* + * Hardware may have selected a new brightness level during its + * hardware control transition, so only reset brightness if we + * are switching to another trigger or if the switching is not + * hardware triggered. + * + * Note that this does not apply to the error path, as running + * into the error path implies a none => private trigger + * transition. This hints that the LED driver and its private + * trigger must have some fundamental bugs, so the error path + * always turns off the LED to reset it to a certain state. + */ + if (trig || !hw_triggered) + led_set_brightness(led_cdev, LED_OFF); } if (trig) { spin_lock(&trig->leddev_list_lock); @@ -330,6 +347,11 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig) return ret; } + +int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig) +{ + return __led_trigger_set(led_cdev, trig, false); +} EXPORT_SYMBOL_GPL(led_trigger_set); void led_trigger_remove(struct led_classdev *led_cdev) @@ -484,6 +506,60 @@ int devm_led_trigger_register(struct device *dev, } EXPORT_SYMBOL_GPL(devm_led_trigger_register); +#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED + +static void led_trigger_do_hw_control_transition(struct led_classdev *led_cdev, bool activate, + struct led_trigger *hc_trig) +{ + if (activate && !led_cdev->trigger) /* "none" => private trigger. */ + __led_trigger_set(led_cdev, hc_trig, true); + else if (!activate && led_cdev->trigger == hc_trig) /* private trigger => "none". */ + __led_trigger_set(led_cdev, NULL, true); + + /* Already in the desired state, or another trigger is active, ignore. */ +} + +void led_trigger_hw_control_changed_worker(struct work_struct *work) +{ + struct led_classdev *led_cdev = + container_of(work, struct led_classdev, trigger_hw_changed_work); + bool activate = READ_ONCE(led_cdev->trigger_hw_changed); + + scoped_guard(rwsem_read, &triggers_list_lock) { + struct led_trigger *trig; + + list_for_each_entry(trig, &trigger_list, next_trig) { + if (trig->trigger_type == led_cdev->trigger_type && + !strcmp(trig->name, led_cdev->hw_control_trigger)) { + guard(rwsem_write)(&led_cdev->trigger_lock); + + led_trigger_do_hw_control_transition(led_cdev, activate, trig); + return; + } + } + } + + dev_warn(led_cdev->dev, + "Private trigger %s is not registered, can't toggle hardware control\n", + led_cdev->hw_control_trigger); +} +EXPORT_SYMBOL_GPL(led_trigger_hw_control_changed_worker); + +void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate) +{ + /* Restricted to private triggers. */ + if (WARN_ON(!(led_cdev->flags & LED_TRIG_HW_CHANGED) || + !led_cdev->hw_control_trigger || !led_cdev->trigger_type)) + return; + + WRITE_ONCE(led_cdev->trigger_hw_changed, activate); + + schedule_work(&led_cdev->trigger_hw_changed_work); +} +EXPORT_SYMBOL_GPL(led_trigger_notify_hw_control_changed); + +#endif /* CONFIG_LEDS_TRIGGERS_HW_CHANGED */ + /* Simple LED Trigger Interface */ void led_trigger_event(struct led_trigger *trig, diff --git a/drivers/leds/leds.h b/drivers/leds/leds.h index c1db21e943b0..6d00e6f44126 100644 --- a/drivers/leds/leds.h +++ b/drivers/leds/leds.h @@ -21,6 +21,7 @@ void led_init_core(struct led_classdev *led_cdev); void led_stop_software_blink(struct led_classdev *led_cdev); void led_set_brightness_nopm(struct led_classdev *led_cdev, unsigned int value); void led_set_brightness_nosleep(struct led_classdev *led_cdev, unsigned int value); +void led_trigger_hw_control_changed_worker(struct work_struct *work); extern struct rw_semaphore leds_list_lock; extern struct list_head leds_list; diff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig index c11282a74b5a..a11d04ce4ab2 100644 --- a/drivers/leds/trigger/Kconfig +++ b/drivers/leds/trigger/Kconfig @@ -9,6 +9,16 @@ menuconfig LEDS_TRIGGERS if LEDS_TRIGGERS +config LEDS_TRIGGERS_HW_CHANGED + bool "LED hardware-initiated trigger transition support" + default LEDS_BRIGHTNESS_HW_CHANGED + help + This option enables support for hardware initiated hardware control + transitions, where the LED hardware autonomously switches between + "none" (i.e., no trigger) and its private trigger. + + See Documentation/leds/leds-class.rst for details. + config LEDS_TRIGGER_TIMER tristate "LED Timer Trigger" help diff --git a/include/linux/leds.h b/include/linux/leds.h index 9a0bfd985b46..beaf23993063 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -109,6 +109,7 @@ struct led_classdev { #define LED_INIT_DEFAULT_TRIGGER BIT(23) #define LED_REJECT_NAME_CONFLICT BIT(24) #define LED_MULTI_COLOR BIT(25) +#define LED_TRIG_HW_CHANGED BIT(26) /* set_brightness_work / blink_timer flags, atomic, private. */ unsigned long work_flags; @@ -239,6 +240,11 @@ struct led_classdev { struct kernfs_node *brightness_hw_changed_kn; #endif +#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED + bool trigger_hw_changed; + struct work_struct trigger_hw_changed_work; +#endif + /* Ensures consistent access to the LED class device */ struct mutex led_access; }; @@ -609,6 +615,13 @@ led_trigger_get_brightness(const struct led_trigger *trigger) #endif /* CONFIG_LEDS_TRIGGERS */ +#ifdef CONFIG_LEDS_TRIGGERS_HW_CHANGED +void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate); +#else +static inline void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, + bool activate) {} +#endif + /* Trigger specific enum */ enum led_trigger_netdev_modes { TRIGGER_NETDEV_LINK = 0, From 755fd606097e7b225a9192e9227bfc3a6bb3643e Mon Sep 17 00:00:00 2001 From: Griffin Kroah-Hartman Date: Wed, 30 Sep 2026 17:20:26 +0200 Subject: [PATCH 41/49] MAINTAINERS: Add myself as a LED SUBSYSTEM reviewer I will be helping to review LED patches. Signed-off-by: Griffin Kroah-Hartman Link: https://patch.msgid.link/20260930-led-add-maintainers-v1-1-ebd45f8b6ab1@kroah.com Signed-off-by: Lee Jones --- MAINTAINERS | 1 + 1 file changed, 1 insertion(+) diff --git a/MAINTAINERS b/MAINTAINERS index a1eb0937238d..bccc0146f415 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14786,6 +14786,7 @@ F: drivers/scsi/leapraid/ LED SUBSYSTEM M: Lee Jones M: Pavel Machek +R: Griffin Kroah-Hartman L: linux-leds@vger.kernel.org S: Maintained T: git git://git.kernel.org/pub/scm/linux/kernel/git/lee/leds.git From 1c93924893871c517e54fefc1feb420af3eadcfb Mon Sep 17 00:00:00 2001 From: "Rob Herring (Arm)" Date: Mon, 21 Sep 2026 18:27:08 -0500 Subject: [PATCH 42/49] dt-bindings: leds: Allow PHY RX triggers The ZynqMP ZCU100 revC DTS uses separate phy0tx and phy0rx triggers for its WLAN activity LEDs. Allow the RX form alongside the existing TX form. Assisted-by: LLM Signed-off-by: Rob Herring (Arm) Link: https://patch.msgid.link/20260921232709.3133716-1-robh@kernel.org Signed-off-by: Lee Jones --- Documentation/devicetree/bindings/leds/common.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/leds/common.yaml b/Documentation/devicetree/bindings/leds/common.yaml index a0ef2f2f0724..2502e6f221f8 100644 --- a/Documentation/devicetree/bindings/leds/common.yaml +++ b/Documentation/devicetree/bindings/leds/common.yaml @@ -147,7 +147,7 @@ properties: # LED is triggered by SD/MMC activity - pattern: "^mmc[0-9]+$" # LED is triggered by WLAN activity - - pattern: "^phy[0-9]+tx$" + - pattern: "^phy[0-9]+(rx|tx)$" led-pattern: description: | From 186fc521f211a207c323b753659f28d3dda7bc45 Mon Sep 17 00:00:00 2001 From: David Heidelberg Date: Mon, 21 Sep 2026 12:46:22 +0200 Subject: [PATCH 43/49] backlight: qcom-wled: Fix NULL pointer dereference in wled_remove() wled_probe() doesn't set the driver data for the platform device. As a result, platform_get_drvdata() in wled_remove() will return NULL, and the first dereference of it crashes on unbind. Set the platform device driver data in wled_probe(). Cc: stable@vger.kernel.org Fixes: feeab87b3072 ("backlight: qcom-wled: Add support for short circuit handling") Signed-off-by: David Heidelberg Reviewed-by: Daniel Thompson (RISCstar) Reviewed-by: Konrad Dybcio Link: https://patch.msgid.link/20260921-qcom-wled-backlight-v4-1-bab8c7ef73cb@ixit.cz Signed-off-by: Lee Jones --- drivers/video/backlight/qcom-wled.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c index 650dd95f06ef..2281007bdd45 100644 --- a/drivers/video/backlight/qcom-wled.c +++ b/drivers/video/backlight/qcom-wled.c @@ -1680,6 +1680,7 @@ static int wled_probe(struct platform_device *pdev) wled->regmap = regmap; wled->dev = &pdev->dev; + platform_set_drvdata(pdev, wled); wled->version = (uintptr_t)of_device_get_match_data(&pdev->dev); if (!wled->version) { From e60dc9c05e0f415ef8bc63b3179abdd49830a619 Mon Sep 17 00:00:00 2001 From: David Heidelberg Date: Mon, 21 Sep 2026 12:46:23 +0200 Subject: [PATCH 44/49] backlight: qcom-wled: Fix WLED3 brightness register stride WLED3 has a 16-bit brightness register pair per string, at 0x40 + 2*n, which is what the pm8941-wled driver wrote to: rc = regmap_bulk_write(wled->regmap, wled->addr + WLED3_CTRL_REG_VAL_BASE + 2 * i, v, 2); The restructuring for WLED3 turned that into WLED3_SINK_REG_BRIGHT(n), defined as 0x40 + n, so the two byte writes for consecutive strings overlap: string 1 overwrites the MSB of string 0 with its own LSB, and with the default three strings only string 1 ends up with the requested value. Use the 2 byte stride. Fixes: 775d2ffb4af6 ("backlight: qcom-wled: Restructure the driver for WLED3") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: David Heidelberg Reviewed-by: Daniel Thompson (RISCstar) Reviewed-by: Konrad Dybcio Link: https://patch.msgid.link/20260921-qcom-wled-backlight-v4-2-bab8c7ef73cb@ixit.cz Signed-off-by: Lee Jones --- drivers/video/backlight/qcom-wled.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c index 2281007bdd45..f452d328997a 100644 --- a/drivers/video/backlight/qcom-wled.c +++ b/drivers/video/backlight/qcom-wled.c @@ -59,7 +59,7 @@ #define WLED3_SINK_REG_CURR_SINK_SHFT 5 /* WLED3 specific per-'string' registers below */ -#define WLED3_SINK_REG_BRIGHT(n) (0x40 + n) +#define WLED3_SINK_REG_BRIGHT(n) (0x40 + (n * 0x2)) #define WLED3_SINK_REG_STR_MOD_EN(n) (0x60 + (n * 0x10)) #define WLED3_SINK_REG_STR_MOD_MASK BIT(7) From b32a8c32fea3855e707717155f198f03fdbf6f1e Mon Sep 17 00:00:00 2001 From: David Heidelberg Date: Mon, 21 Sep 2026 12:46:24 +0200 Subject: [PATCH 45/49] backlight: qcom-wled: Fix unbalanced OVP IRQ enable at probe wled_configure_ovp_irq() derives the initial state of the OVP interrupt from the hardware: /* Keep OVP irq disabled until module is enabled */ if (!(val & WLED3_CTRL_REG_MOD_EN_MASK)) disable_irq(wled->ovp_irq); but wled->brightness, which is what the rest of the driver uses to tell whether the module is on, is left at zero. On boards where the bootloader hands the kernel a lit backlight the two disagree. MOD_EN is already set, so the interrupt is left enabled, while wled_update_status() still believes the backlight is off and takes if (!!brightness != !!wled->brightness) rc = wled_module_enable(wled, !!brightness); on the first backlight update. wled_module_enable() then schedules wled_ovp_work(), which calls enable_irq() on the already enabled interrupt: Unbalanced enable for IRQ 176 WARNING: CPU: 0 PID: 160 at kernel/irq/manage.c:774 __enable_irq+0x50/0x80 Hardware name: Xiaomi Pocophone F1 (DT) Workqueue: events wled_ovp_work Call trace: __enable_irq+0x50/0x80 enable_irq+0x48/0xa0 wled_ovp_work+0x18/0x24 process_one_work+0x1d0/0x350 worker_thread+0x13c/0x460 kthread+0x110/0x114 ret_from_fork+0x10/0x20 The bootloader is not the only way to get there. The readback runs after wledN_setup(), and wled4_setup() sets MOD_EN itself on the path where the sink configuration does not already match, as does the tail of wled_auto_string_detection(), which all three setup paths can reach through wled_auto_detection_at_init(). A cold-booted board with a dark panel can therefore reach the same disagreement. Move the MOD_EN readback into wled_probe() and use it to seed wled->brightness, so the driver starts out agreeing with the hardware, and key the OVP interrupt off wled->brightness instead. The first backlight update then only reprograms the brightness registers and leaves both the module and the interrupt alone. The OVP interrupt also stays armed from probe whenever the module is already enabled, rather than being disabled at probe and only enabled once something writes brightness. Note that a backlight update requesting brightness 0 before any non-zero one now really does turn the module off wherever MOD_EN was already set, where before it was silently ignored. wled->brightness is seeded with default-brightness rather than the level the bootloader actually programmed, so the first update can still step the brightness. Reading that level back is version specific and is done in a follow-up, to keep this fix small enough to backport. Assisted-by: LLM Fixes: 8663c188beea ("backlight: qcom-wled: Add auto string detection logic") Cc: stable@vger.kernel.org Signed-off-by: David Heidelberg Reviewed-by: Daniel Thompson (RISCstar) Link: https://patch.msgid.link/20260921-qcom-wled-backlight-v4-3-bab8c7ef73cb@ixit.cz Signed-off-by: Lee Jones --- drivers/video/backlight/qcom-wled.c | 32 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c index f452d328997a..3ce3bade3b8d 100644 --- a/drivers/video/backlight/qcom-wled.c +++ b/drivers/video/backlight/qcom-wled.c @@ -1627,7 +1627,6 @@ static int wled_configure_ovp_irq(struct wled *wled, struct platform_device *pdev) { int rc; - u32 val; wled->ovp_irq = platform_get_irq_byname(pdev, "ovp"); if (wled->ovp_irq < 0) { @@ -1643,13 +1642,8 @@ static int wled_configure_ovp_irq(struct wled *wled, return 0; } - rc = regmap_read(wled->regmap, wled->ctrl_addr + - WLED3_CTRL_REG_MOD_EN, &val); - if (rc < 0) - return rc; - - /* Keep OVP irq disabled until module is enabled */ - if (!(val & WLED3_CTRL_REG_MOD_EN_MASK)) + /* Keep the OVP irq disabled until the module is enabled */ + if (!wled->brightness) disable_irq(wled->ovp_irq); return 0; @@ -1665,6 +1659,7 @@ static int wled_probe(struct platform_device *pdev) struct backlight_device *bl; struct wled *wled; struct regmap *regmap; + u32 mod_en; u32 val; int rc; @@ -1735,6 +1730,24 @@ static int wled_probe(struct platform_device *pdev) INIT_DELAYED_WORK(&wled->ovp_work, wled_ovp_work); + val = WLED_DEFAULT_BRIGHTNESS; + of_property_read_u32(pdev->dev.of_node, "default-brightness", &val); + + /* + * The module may already be enabled, either by a bootloader that left + * the backlight lit or by the setup above. Record that, so that the + * first brightness update does not enable an already enabled module, + * and so that the OVP irq is armed from probe rather than from that + * first update. + */ + rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN, + &mod_en); + if (rc < 0) + return rc; + + if (mod_en & WLED3_CTRL_REG_MOD_EN_MASK) + wled->brightness = val; + rc = wled_configure_short_irq(wled, pdev); if (rc < 0) return rc; @@ -1743,9 +1756,6 @@ static int wled_probe(struct platform_device *pdev) if (rc < 0) return rc; - val = WLED_DEFAULT_BRIGHTNESS; - of_property_read_u32(pdev->dev.of_node, "default-brightness", &val); - memset(&props, 0, sizeof(struct backlight_properties)); props.type = BACKLIGHT_RAW; props.brightness = val; From 2ffcac916578f4a57347e717786d1e07e85cbf2b Mon Sep 17 00:00:00 2001 From: David Heidelberg Date: Mon, 21 Sep 2026 12:46:25 +0200 Subject: [PATCH 46/49] backlight: qcom-wled: Read back the programmed brightness at probe Since the previous change wled_probe() seeds wled->brightness from MOD_EN, but with default-brightness rather than the level the hardware is actually driving, so a backlight left lit by the bootloader still visibly steps on the first update, and actual_brightness reports the last value written rather than what the panel shows. Add wled_read_brightness(), which returns 0 when MOD_EN is clear and otherwise reads the brightness register the matching wledN_set_brightness() writes: the first enabled string on WLED3 and WLED4, the selected modulator on WLED5. Use it to seed both wled->brightness and the initial backlight property, and expose it as backlight_ops.get_brightness so that actual_brightness reflects the hardware too. A module that is enabled with the brightness registers at zero, which wled4_setup() and wled_auto_string_detection() can leave behind on a cold boot, now reads back as off. The OVP interrupt is then armed by the first brightness update rather than from probe, which is what happens on every other board where the module starts out disabled. Suggested-by: Konrad Dybcio Assisted-by: LLM Signed-off-by: David Heidelberg Reviewed-by: Daniel Thompson (RISCstar) Link: https://patch.msgid.link/20260921-qcom-wled-backlight-v4-4-bab8c7ef73cb@ixit.cz Signed-off-by: Lee Jones --- drivers/video/backlight/qcom-wled.c | 74 ++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/drivers/video/backlight/qcom-wled.c b/drivers/video/backlight/qcom-wled.c index 3ce3bade3b8d..ca350f320e17 100644 --- a/drivers/video/backlight/qcom-wled.c +++ b/drivers/video/backlight/qcom-wled.c @@ -290,6 +290,50 @@ static int wled5_set_brightness(struct wled *wled, u16 brightness) return rc; } +static int wled_read_brightness(struct wled *wled) +{ + u16 addr, mask; + __le16 v; + u32 val; + int rc; + + rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN, + &val); + if (rc < 0) + return rc; + + if (!(val & WLED3_CTRL_REG_MOD_EN_MASK)) + return 0; + + switch (wled->version) { + case 3: + addr = wled->sink_addr + + WLED3_SINK_REG_BRIGHT(wled->cfg.enabled_strings[0]); + mask = WLED3_SINK_REG_BRIGHT_MAX; + break; + case 4: + addr = wled->sink_addr + + WLED4_SINK_REG_BRIGHT(wled->cfg.enabled_strings[0]); + mask = WLED3_SINK_REG_BRIGHT_MAX; + break; + case 5: + addr = wled->sink_addr + (wled->cfg.mod_sel == MOD_A ? + WLED5_SINK_REG_MOD_A_BRIGHTNESS_LSB : + WLED5_SINK_REG_MOD_B_BRIGHTNESS_LSB); + mask = WLED5_SINK_REG_BRIGHT_MAX_15B; + break; + default: + dev_err(wled->dev, "Invalid WLED version\n"); + return -EINVAL; + } + + rc = regmap_bulk_read(wled->regmap, addr, &v, sizeof(v)); + if (rc < 0) + return rc; + + return min_t(u32, le16_to_cpu(v) & mask, wled->max_brightness); +} + static void wled_ovp_work(struct work_struct *work) { struct wled *wled = container_of(work, @@ -480,6 +524,18 @@ static int wled_update_status(struct backlight_device *bl) return rc; } +static int wled_get_brightness(struct backlight_device *bl) +{ + struct wled *wled = bl_get_data(bl); + int rc; + + mutex_lock(&wled->lock); + rc = wled_read_brightness(wled); + mutex_unlock(&wled->lock); + + return rc; +} + static int wled4_cabc_config(struct wled *wled, bool enable) { int i, j, rc; @@ -1651,6 +1707,7 @@ static int wled_configure_ovp_irq(struct wled *wled, static const struct backlight_ops wled_ops = { .update_status = wled_update_status, + .get_brightness = wled_get_brightness, }; static int wled_probe(struct platform_device *pdev) @@ -1659,7 +1716,6 @@ static int wled_probe(struct platform_device *pdev) struct backlight_device *bl; struct wled *wled; struct regmap *regmap; - u32 mod_en; u32 val; int rc; @@ -1734,19 +1790,15 @@ static int wled_probe(struct platform_device *pdev) of_property_read_u32(pdev->dev.of_node, "default-brightness", &val); /* - * The module may already be enabled, either by a bootloader that left - * the backlight lit or by the setup above. Record that, so that the - * first brightness update does not enable an already enabled module, - * and so that the OVP irq is armed from probe rather than from that - * first update. + * The module may already be lit, either by the bootloader or by the + * setup above. Start from what the hardware is driving, so that the + * first brightness update does not step the brightness. */ - rc = regmap_read(wled->regmap, wled->ctrl_addr + WLED3_CTRL_REG_MOD_EN, - &mod_en); + rc = wled_read_brightness(wled); if (rc < 0) return rc; - if (mod_en & WLED3_CTRL_REG_MOD_EN_MASK) - wled->brightness = val; + wled->brightness = rc; rc = wled_configure_short_irq(wled, pdev); if (rc < 0) @@ -1758,7 +1810,7 @@ static int wled_probe(struct platform_device *pdev) memset(&props, 0, sizeof(struct backlight_properties)); props.type = BACKLIGHT_RAW; - props.brightness = val; + props.brightness = wled->brightness ?: val; props.max_brightness = wled->max_brightness; bl = devm_backlight_device_register(&pdev->dev, wled->name, &pdev->dev, wled, From dc8aec7428160492252267cbc0f78b889f7cae01 Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Sun, 13 Sep 2026 14:33:02 +0200 Subject: [PATCH 47/49] dt-bindings: leds: Use consistent indentation in the example Correct indentation in the examples to consistent 2- or 4-spaces indentation to fix dt-check-style warnings ("example 0 [indent-consistent] indent mismatch ..."). Preferred is 4-spaces, but re-indenting entire example just for that is too much churn. Signed-off-by: Krzysztof Kozlowski Reviewed-by: Rob Herring (Arm) Link: https://patch.msgid.link/20260913123301.100129-2-krzysztof.kozlowski@oss.qualcomm.com Signed-off-by: Lee Jones --- .../leds/backlight/lm3630a-backlight.yaml | 52 +++++++++---------- .../leds/backlight/lp855x-backlight.yaml | 18 +++---- .../bindings/leds/backlight/qcom-wled.yaml | 2 +- .../bindings/leds/leds-consumer.yaml | 14 ++--- .../devicetree/bindings/leds/leds-lp50xx.yaml | 12 ++--- .../devicetree/bindings/leds/leds-lp5860.yaml | 12 ++--- .../devicetree/bindings/leds/nxp,pca963x.yaml | 24 ++++----- 7 files changed, 67 insertions(+), 67 deletions(-) diff --git a/Documentation/devicetree/bindings/leds/backlight/lm3630a-backlight.yaml b/Documentation/devicetree/bindings/leds/backlight/lm3630a-backlight.yaml index 3c9b4054ed9a..6393cbf3afdf 100644 --- a/Documentation/devicetree/bindings/leds/backlight/lm3630a-backlight.yaml +++ b/Documentation/devicetree/bindings/leds/backlight/lm3630a-backlight.yaml @@ -99,20 +99,20 @@ examples: #size-cells = <0>; led-controller@38 { - compatible = "ti,lm3630a"; - reg = <0x38>; - enable-gpios = <&gpio2 5 GPIO_ACTIVE_HIGH>; + compatible = "ti,lm3630a"; + reg = <0x38>; + enable-gpios = <&gpio2 5 GPIO_ACTIVE_HIGH>; - #address-cells = <1>; - #size-cells = <0>; + #address-cells = <1>; + #size-cells = <0>; - led@0 { - reg = <0>; - led-sources = <0 1>; - label = "lcd-backlight"; - default-brightness = <200>; - max-brightness = <255>; - }; + led@0 { + reg = <0>; + led-sources = <0 1>; + label = "lcd-backlight"; + default-brightness = <200>; + max-brightness = <255>; + }; }; }; - | @@ -121,22 +121,22 @@ examples: #size-cells = <0>; led-controller@38 { - compatible = "ti,lm3630a"; - reg = <0x38>; + compatible = "ti,lm3630a"; + reg = <0x38>; - #address-cells = <1>; - #size-cells = <0>; + #address-cells = <1>; + #size-cells = <0>; - led@0 { - reg = <0>; - default-brightness = <150>; - ti,linear-mapping-mode; - }; + led@0 { + reg = <0>; + default-brightness = <150>; + ti,linear-mapping-mode; + }; - led@1 { - reg = <1>; - default-brightness = <225>; - ti,linear-mapping-mode; - }; + led@1 { + reg = <1>; + default-brightness = <225>; + ti,linear-mapping-mode; + }; }; }; diff --git a/Documentation/devicetree/bindings/leds/backlight/lp855x-backlight.yaml b/Documentation/devicetree/bindings/leds/backlight/lp855x-backlight.yaml index 9416e1bfab92..2215ee1b2e5b 100644 --- a/Documentation/devicetree/bindings/leds/backlight/lp855x-backlight.yaml +++ b/Documentation/devicetree/bindings/leds/backlight/lp855x-backlight.yaml @@ -95,20 +95,20 @@ examples: /* 4V OV, 4 output LED0 string enabled */ rom-14h { - rom-addr = /bits/ 8 <0x14>; - rom-val = /bits/ 8 <0xcf>; + rom-addr = /bits/ 8 <0x14>; + rom-val = /bits/ 8 <0xcf>; }; /* Heavy smoothing, 24ms ramp time step */ rom-15h { - rom-addr = /bits/ 8 <0x15>; - rom-val = /bits/ 8 <0xc7>; + rom-addr = /bits/ 8 <0x15>; + rom-val = /bits/ 8 <0xc7>; }; /* 4 output LED1 string enabled */ rom-19h { - rom-addr = /bits/ 8 <0x19>; - rom-val = /bits/ 8 <0x0f>; + rom-addr = /bits/ 8 <0x19>; + rom-val = /bits/ 8 <0x0f>; }; }; }; @@ -125,7 +125,7 @@ examples: dev-ctrl = /bits/ 8 <0x85>; init-brt = /bits/ 8 <0x10>; }; - }; + }; - | i2c { #address-cells = <1>; @@ -142,8 +142,8 @@ examples: /* 4V OV, 4 output LED string enabled */ rom-14h { - rom-addr = /bits/ 8 <0x14>; - rom-val = /bits/ 8 <0xcf>; + rom-addr = /bits/ 8 <0x14>; + rom-val = /bits/ 8 <0xcf>; }; }; }; diff --git a/Documentation/devicetree/bindings/leds/backlight/qcom-wled.yaml b/Documentation/devicetree/bindings/leds/backlight/qcom-wled.yaml index a54448cfdb38..bca00b774bb0 100644 --- a/Documentation/devicetree/bindings/leds/backlight/qcom-wled.yaml +++ b/Documentation/devicetree/bindings/leds/backlight/qcom-wled.yaml @@ -280,4 +280,4 @@ examples: qcom,ovp = <29>; qcom,num-strings = <2>; qcom,enabled-strings = <0 1>; - }; + }; diff --git a/Documentation/devicetree/bindings/leds/leds-consumer.yaml b/Documentation/devicetree/bindings/leds/leds-consumer.yaml index fe6a0faa1d3b..b4065c9733a5 100644 --- a/Documentation/devicetree/bindings/leds/leds-consumer.yaml +++ b/Documentation/devicetree/bindings/leds/leds-consumer.yaml @@ -53,15 +53,15 @@ examples: }; i2c { - #address-cells = <1>; - #size-cells = <0>; + #address-cells = <1>; + #size-cells = <0>; - v4l2_node: camera@36 { - reg = <0x36>; + v4l2_node: camera@36 { + reg = <0x36>; - leds = <&privacy_led>; - led-names = "privacy"; - }; + leds = <&privacy_led>; + led-names = "privacy"; + }; }; ... diff --git a/Documentation/devicetree/bindings/leds/leds-lp50xx.yaml b/Documentation/devicetree/bindings/leds/leds-lp50xx.yaml index 23f809906ba7..85182ad7ad09 100644 --- a/Documentation/devicetree/bindings/leds/leds-lp50xx.yaml +++ b/Documentation/devicetree/bindings/leds/leds-lp50xx.yaml @@ -121,18 +121,18 @@ examples: function = LED_FUNCTION_CHARGING; led@0 { - reg = <0x0>; - color = ; + reg = <0x0>; + color = ; }; led@1 { - reg = <0x1>; - color = ; + reg = <0x1>; + color = ; }; led@2 { - reg = <0x2>; - color = ; + reg = <0x2>; + color = ; }; }; diff --git a/Documentation/devicetree/bindings/leds/leds-lp5860.yaml b/Documentation/devicetree/bindings/leds/leds-lp5860.yaml index 0e88c71c2d39..710fec3738ed 100644 --- a/Documentation/devicetree/bindings/leds/leds-lp5860.yaml +++ b/Documentation/devicetree/bindings/leds/leds-lp5860.yaml @@ -100,18 +100,18 @@ examples: color = ; led@0 { - reg = <0x0>; - color = ; + reg = <0x0>; + color = ; }; led@1 { - reg = <0x1>; - color = ; + reg = <0x1>; + color = ; }; led@2 { - reg = <0x2>; - color = ; + reg = <0x2>; + color = ; }; }; }; diff --git a/Documentation/devicetree/bindings/leds/nxp,pca963x.yaml b/Documentation/devicetree/bindings/leds/nxp,pca963x.yaml index a16ca11f630b..dac316741b9b 100644 --- a/Documentation/devicetree/bindings/leds/nxp,pca963x.yaml +++ b/Documentation/devicetree/bindings/leds/nxp,pca963x.yaml @@ -164,27 +164,27 @@ examples: #size-cells = <0>; led@0 { - reg = <0>; - color = ; - function = LED_FUNCTION_STATUS; + reg = <0>; + color = ; + function = LED_FUNCTION_STATUS; }; led@1 { - reg = <1>; - color = ; - function = LED_FUNCTION_STATUS; + reg = <1>; + color = ; + function = LED_FUNCTION_STATUS; }; led@2 { - reg = <2>; - color = ; - function = LED_FUNCTION_STATUS; + reg = <2>; + color = ; + function = LED_FUNCTION_STATUS; }; led@3 { - reg = <3>; - color = ; - function = LED_FUNCTION_STATUS; + reg = <3>; + color = ; + function = LED_FUNCTION_STATUS; }; }; }; From 7260db6364a2999f2f85bae8afb6b4d22e41fc8a Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Mon, 5 Oct 2026 13:09:06 -0700 Subject: [PATCH 48/49] leds: pca955x: Drop platform data and allocate LEDs with the device struct pca955x_platform_data has been private to this driver since commit ed1f4b9676a8 ("leds: pca955x: add device tree support"), so nothing can pass it in through dev_get_platdata() and it only serves as an intermediate copy of the per-LED type, default state and fwnode. That copy is a full array of struct pca955x_led, the same runtime structure that struct pca955x already holds, so the data is stored twice. Remove the platform data and parse the child firmware nodes directly into pca955x->leds[]. Make leds[] a flexible array member so it is allocated together with struct pca955x. This removes three allocations and the num_leds sanity check, which could no longer fail. struct pca955x_led moves above struct pca955x, as a flexible array member needs the complete element type. Assisted-by: LLM Signed-off-by: Rosen Penev Link: https://patch.msgid.link/20261005200906.188636-1-rosenp@gmail.com Signed-off-by: Lee Jones --- drivers/leds/leds-pca955x.c | 94 ++++++++++++------------------------- 1 file changed, 31 insertions(+), 63 deletions(-) diff --git a/drivers/leds/leds-pca955x.c b/drivers/leds/leds-pca955x.c index 273383351ba0..e4af7a3b639f 100644 --- a/drivers/leds/leds-pca955x.c +++ b/drivers/leds/leds-pca955x.c @@ -79,6 +79,17 @@ struct pca955x_chipdef { int blink_div; /* PSC divider */ }; +struct pca955x_led { + struct pca955x *pca955x; + struct led_classdev led_cdev; + int led_num; /* 0 .. 15 potentially */ + u32 type; + enum led_default_state default_state; + struct fwnode_handle *fwnode; +}; + +#define led_to_pca955x(l) container_of(l, struct pca955x_led, led_cdev) + static const struct pca955x_chipdef pca955x_chipdefs[] = { [pca9550] = { .bits = 2, @@ -114,7 +125,6 @@ static const struct pca955x_chipdef pca955x_chipdefs[] = { struct pca955x { struct mutex lock; - struct pca955x_led *leds; const struct pca955x_chipdef *chipdef; struct i2c_client *client; unsigned long active_blink; @@ -123,22 +133,7 @@ struct pca955x { #ifdef CONFIG_LEDS_PCA955X_GPIO struct gpio_chip gpio; #endif -}; - -struct pca955x_led { - struct pca955x *pca955x; - struct led_classdev led_cdev; - int led_num; /* 0 .. 15 potentially */ - u32 type; - enum led_default_state default_state; - struct fwnode_handle *fwnode; -}; - -#define led_to_pca955x(l) container_of(l, struct pca955x_led, led_cdev) - -struct pca955x_platform_data { - struct pca955x_led *leds; - int num_leds; + struct pca955x_led leds[]; }; /* 8 bits per input register */ @@ -530,37 +525,27 @@ static int pca955x_gpio_direction_output(struct gpio_chip *gc, } #endif /* CONFIG_LEDS_PCA955X_GPIO */ -static struct pca955x_platform_data * -pca955x_get_pdata(struct i2c_client *client, const struct pca955x_chipdef *chip) +static int pca955x_parse_fwnode(struct pca955x *pca955x) { - struct pca955x_platform_data *pdata; + struct device *dev = &pca955x->client->dev; + u8 bits = pca955x->chipdef->bits; struct pca955x_led *led; struct fwnode_handle *child; int count; - count = device_get_child_node_count(&client->dev); - if (count > chip->bits) - return ERR_PTR(-ENODEV); + count = device_get_child_node_count(dev); + if (count > bits) + return -ENODEV; - pdata = devm_kzalloc(&client->dev, sizeof(*pdata), GFP_KERNEL); - if (!pdata) - return ERR_PTR(-ENOMEM); - - pdata->leds = devm_kcalloc(&client->dev, - chip->bits, sizeof(struct pca955x_led), - GFP_KERNEL); - if (!pdata->leds) - return ERR_PTR(-ENOMEM); - - device_for_each_child_node(&client->dev, child) { + device_for_each_child_node(dev, child) { u32 reg; int res; res = fwnode_property_read_u32(child, "reg", ®); - if ((res != 0) || (reg >= chip->bits)) + if ((res != 0) || (reg >= bits)) continue; - led = &pdata->leds[reg]; + led = &pca955x->leds[reg]; led->type = PCA955X_TYPE_LED; led->fwnode = child; led->default_state = led_init_default_state_get(child); @@ -568,9 +553,7 @@ pca955x_get_pdata(struct i2c_client *client, const struct pca955x_chipdef *chip) fwnode_property_read_u32(child, "type", &led->type); } - pdata->num_leds = chip->bits; - - return pdata; + return 0; } static int pca955x_probe(struct i2c_client *client) @@ -584,7 +567,6 @@ static int pca955x_probe(struct i2c_client *client) u8 i, nls, psc0; u8 ls1[4]; u8 ls2[4]; - struct pca955x_platform_data *pdata; bool keep_psc0 = false; bool set_default_label = false; char default_label[4]; @@ -595,12 +577,6 @@ static int pca955x_probe(struct i2c_client *client) return dev_err_probe(&client->dev, -ENODEV, "unknown chip\n"); adapter = client->adapter; - pdata = dev_get_platdata(&client->dev); - if (!pdata) { - pdata = pca955x_get_pdata(client, chip); - if (IS_ERR(pdata)) - return PTR_ERR(pdata); - } /* Make sure the slave address / chip type combo given is possible */ if ((client->addr & ~((1 << chip->slv_addr_shift) - 1)) != @@ -616,22 +592,11 @@ static int pca955x_probe(struct i2c_client *client) if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) return -EIO; - if (pdata->num_leds != chip->bits) { - dev_err(&client->dev, - "board info claims %d LEDs on a %u-bit chip\n", - pdata->num_leds, chip->bits); - return -ENODEV; - } - - pca955x = devm_kzalloc(&client->dev, sizeof(*pca955x), GFP_KERNEL); + pca955x = devm_kzalloc(&client->dev, + struct_size(pca955x, leds, chip->bits), GFP_KERNEL); if (!pca955x) return -ENOMEM; - pca955x->leds = devm_kcalloc(&client->dev, chip->bits, - sizeof(*pca955x_led), GFP_KERNEL); - if (!pca955x->leds) - return -ENOMEM; - i2c_set_clientdata(client, pca955x); mutex_init(&pca955x->lock); @@ -639,6 +604,10 @@ static int pca955x_probe(struct i2c_client *client) pca955x->chipdef = chip; pca955x->blink_period = PCA955X_BLINK_DEFAULT_MS; + err = pca955x_parse_fwnode(pca955x); + if (err) + return err; + init_data.devname_mandatory = false; init_data.devicename = "pca955x"; @@ -657,7 +626,6 @@ static int pca955x_probe(struct i2c_client *client) pca955x_led = &pca955x->leds[i]; pca955x_led->led_num = i; pca955x_led->pca955x = pca955x; - pca955x_led->type = pdata->leds[i].type; switch (pca955x_led->type) { case PCA955X_TYPE_NONE: @@ -671,16 +639,16 @@ static int pca955x_probe(struct i2c_client *client) led->brightness_get = pca955x_led_get; led->blink_set = pca955x_led_blink; - if (pdata->leds[i].default_state == LEDS_DEFSTATE_OFF) + if (pca955x_led->default_state == LEDS_DEFSTATE_OFF) ls2[reg] = pca955x_ledsel(ls2[reg], bit, PCA955X_LS_LED_OFF); - else if (pdata->leds[i].default_state == LEDS_DEFSTATE_ON) + else if (pca955x_led->default_state == LEDS_DEFSTATE_ON) ls2[reg] = pca955x_ledsel(ls2[reg], bit, PCA955X_LS_LED_ON); else if (pca955x_ledstate(ls2[reg], bit) == PCA955X_LS_BLINK0) { keep_psc0 = true; set_bit(i, &pca955x->active_blink); } - init_data.fwnode = pdata->leds[i].fwnode; + init_data.fwnode = pca955x_led->fwnode; if (is_of_node(init_data.fwnode)) { if (to_of_node(init_data.fwnode)->name[0] == From 59cf91cf87abb67a49b1bb7b9d3b06486abde8cf Mon Sep 17 00:00:00 2001 From: Udaya Kiran Challa Date: Tue, 6 Oct 2026 00:14:28 +0530 Subject: [PATCH 49/49] dt-bindings: leds: lacie,netxbig-leds: Convert to DT schema Convert lacie,netxbig-leds devicetree binding from legacy text format to DT schema. Signed-off-by: Udaya Kiran Challa Reviewed-by: Rob Herring (Arm) Link: https://patch.msgid.link/20261005184428.251868-1-challauday369@gmail.com Signed-off-by: Lee Jones --- .../bindings/leds/lacie,netxbig-leds.yaml | 136 ++++++++++++++++++ .../devicetree/bindings/leds/leds-netxbig.txt | 92 ------------ 2 files changed, 136 insertions(+), 92 deletions(-) create mode 100644 Documentation/devicetree/bindings/leds/lacie,netxbig-leds.yaml delete mode 100644 Documentation/devicetree/bindings/leds/leds-netxbig.txt diff --git a/Documentation/devicetree/bindings/leds/lacie,netxbig-leds.yaml b/Documentation/devicetree/bindings/leds/lacie,netxbig-leds.yaml new file mode 100644 index 000000000000..fe47c12044da --- /dev/null +++ b/Documentation/devicetree/bindings/leds/lacie,netxbig-leds.yaml @@ -0,0 +1,136 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/leds/lacie,netxbig-leds.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: LaCie/Seagate netxbig CPLD LEDs + +maintainers: + - Simon Guinot + +description: | + Binding for the CPLD LEDs (GPIO extension bus) found on some LaCie/Seagate + boards (Example: 2Big/5Big Network v2, 2Big NAS). + +properties: + compatible: + const: lacie,netxbig-leds + + gpio-ext: + description: Phandle for the gpio-ext bus. + $ref: /schemas/types.yaml#/definitions/phandle + + timers: + description: + Timer array. Each timer entry is represented by three integers - + Mode (gpio-ext bus), delay_on and delay_off. + $ref: /schemas/types.yaml#/definitions/uint32-array + +patternProperties: + "^led-[a-z0-9]+$": + type: object + $ref: common.yaml# + unevaluatedProperties: false + + properties: + mode-addr: + description: Mode register address on gpio-ext bus. + $ref: /schemas/types.yaml#/definitions/uint32 + + mode-val: + description: + Mode to value mapping. Each entry is represented by two integers - + a mode and the corresponding value on the gpio-ext bus. + $ref: /schemas/types.yaml#/definitions/uint32-array + + bright-addr: + description: Brightness register address on gpio-ext bus. + $ref: /schemas/types.yaml#/definitions/uint32 + + max-brightness: + description: Maximum brightness value. + $ref: /schemas/types.yaml#/definitions/uint32 + + required: + - mode-addr + - mode-val + - bright-addr + - max-brightness + +required: + - compatible + - gpio-ext + +additionalProperties: false + +examples: + - | + #include + + leds { + compatible = "lacie,netxbig-leds"; + + gpio-ext = <&netxbig_gpio_ext>; + + timers = ; + + led-0 { + label = "netxbig:blue:power"; + mode-addr = <0>; + mode-val = ; + bright-addr = <1>; + max-brightness = <7>; + }; + led-1 { + label = "netxbig:red:power"; + mode-addr = <0>; + mode-val = ; + bright-addr = <1>; + max-brightness = <7>; + }; + led-2 { + label = "netxbig:blue:sata0"; + mode-addr = <3>; + mode-val = ; + bright-addr = <2>; + max-brightness = <7>; + }; + led-3 { + label = "netxbig:red:sata0"; + mode-addr = <3>; + mode-val = ; + bright-addr = <2>; + max-brightness = <7>; + }; + led-4 { + label = "netxbig:blue:sata1"; + mode-addr = <4>; + mode-val = ; + bright-addr = <2>; + max-brightness = <7>; + }; + led-5 { + label = "netxbig:red:sata1"; + mode-addr = <4>; + mode-val = ; + bright-addr = <2>; + max-brightness = <7>; + }; + }; diff --git a/Documentation/devicetree/bindings/leds/leds-netxbig.txt b/Documentation/devicetree/bindings/leds/leds-netxbig.txt deleted file mode 100644 index 5ef92a26d768..000000000000 --- a/Documentation/devicetree/bindings/leds/leds-netxbig.txt +++ /dev/null @@ -1,92 +0,0 @@ -Binding for the CPLD LEDs (GPIO extension bus) found on some LaCie/Seagate -boards (Example: 2Big/5Big Network v2, 2Big NAS). - -Required properties: -- compatible: "lacie,netxbig-leds". -- gpio-ext: Phandle for the gpio-ext bus. - -Optional properties: -- timers: Timer array. Each timer entry is represented by three integers: - Mode (gpio-ext bus), delay_on and delay_off. - -Each LED is represented as a sub-node of the netxbig-leds device. - -Required sub-node properties: -- mode-addr: Mode register address on gpio-ext bus. -- mode-val: Mode to value mapping. Each entry is represented by two integers: - A mode and the corresponding value on the gpio-ext bus. -- bright-addr: Brightness register address on gpio-ext bus. -- max-brightness: Maximum brightness value. - -Optional sub-node properties: -- label: Name for this LED. If omitted, the label is taken from the node name. -- linux,default-trigger: Trigger assigned to the LED. - -Example: - -netxbig-leds { - compatible = "lacie,netxbig-leds"; - - gpio-ext = &gpio_ext; - - timers = ; - - blue-power { - label = "netxbig:blue:power"; - mode-addr = <0>; - mode-val = ; - bright-addr = <1>; - max-brightness = <7>; - }; - red-power { - label = "netxbig:red:power"; - mode-addr = <0>; - mode-val = ; - bright-addr = <1>; - max-brightness = <7>; - }; - blue-sata0 { - label = "netxbig:blue:sata0"; - mode-addr = <3>; - mode-val = ; - bright-addr = <2>; - max-brightness = <7>; - }; - red-sata0 { - label = "netxbig:red:sata0"; - mode-addr = <3>; - mode-val = ; - bright-addr = <2>; - max-brightness = <7>; - }; - blue-sata1 { - label = "netxbig:blue:sata1"; - mode-addr = <4>; - mode-val = ; - bright-addr = <2>; - max-brightness = <7>; - }; - red-sata1 { - label = "netxbig:red:sata1"; - mode-addr = <4>; - mode-val = ; - bright-addr = <2>; - max-brightness = <7>; - }; -};