* [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition
@ 2026-06-17 16:47 Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 1/9] leds: Add callback offloaded() to query the state of hardware control trigger Rong Zhang
` (9 more replies)
0 siblings, 10 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:47 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
Some laptops can tune their keyboard backlight according to ambient
light sensors (auto mode). This capability is essentially a hardware
control trigger. Meanwhile, such laptops also offer a shrotcut for
cycling through brightness levels and auto mode. For example, on
ThinkBook, pressing Fn+Space cycles keyboard backlight levels in the
following sequence:
1 => 2 => 0 => auto => 1 ...
Recent ThinkPad models should have similar sequence too.
However, there are some issues preventing us from using a private
hardware control trigger:
1. We want a mechanism to tell userspace which trigger is the hardware
control one, so that userspace can determine if auto mode is on/off,
as well as turing it on/off programmatically without obtaining the
trigger's name via other channels
2. Turing on/off auto mode via the shortcut cannot activate/deactivate
the corresponding hardware control trigger, making the software state
out of sync
3. Even with #1 resolved, deactivating the hardware control trigger has
a side effect of emitting LED_OFF, breaking the shortcut cycle, where
"auto => 1" requires the driver to deactivate the trigger
This RFC series tries to demonstrate a path on solving these issues:
- Introduce an attribute "trigger_may_offload", 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
- if the trigger is selected ("<foo_trigger>")
- if the trigger is offloaded ("[foo_trigger]")
- A callback offloaded() is added so that LED triggers can report
their hardware control state
- Add led_trigger_notify_hw_control_changed() interface, so that LED
drivers can notify the LED core about hardware-initiated hardware
control transitions. The LED core will then determine if the
transition is allowed and switching between "none" (i.e., no trigger)
and the device's private trigger accordingly
- This capability is restricted to the device's private trigger. If
the current trigger is neither the private trigger nor "none", no
transition will be made
- This interface is gated behind Kconfig LEDS_TRIGGERS_HW_CHANGED and
LED device flag LED_TRIG_HW_CHANGED
- Tune the logic of trigger deactivation so that it won't emit LED_OFF
when the deactivation is triggered by hardware
The last three patches are included in the RFC series to demonstrate how
to these interfaces are supposed to be utilized, so that ideapad-laptop
can expose the auto mode of ThinkBook's keyboard backlight. They can be
submitted separately once the dust settles, if preferred.
[ Summary of other approaches ]
< custom attribute >
Pros:
- simplicity, KISS
- no need to touch the LED core
- extensible as long as it has a sensor-neutral name
- a sensor-related name could potentially lead to a mess if a future
device implements auto mode based on multiple different sensors
Cons:
- must have zero influence on brightness_set[_blocking] callbacks
in order not to break triggers
- potential interference with triggers and the brightness attribute
- weird semantic (an attribute other than "brightness" and "trigger"
changes the brightness)
< private hardware control trigger (this series) >
Pros:
- mutually exclusive with other triggers (hence less chaos)
- semantic correctness
- acts as an aggregate switch to turn on/off auto mode even a future
device implements auto mode based on multiple different sensors
- extensibility (through trigger attributes)
Cons:
- complexity
[ Previous discussion threads ]
https://lore.kernel.org/r/08580ec5-1d7b-4612-8a3f-75bc2f40aad2@app.fastmail.com
https://lore.kernel.org/r/1dbfcf656cdb4af0299f90d7426d2ec7e2b8ac9e.camel@rong.moe
Signed-off-by: Rong Zhang <i@rong.moe>
---
Changes in v2:
- Restrict the led_trigger_notify_hw_control_changed() interface to
private triggers only
- Drop PATCH v1 1/9 ("leds: Load trigger modules on-demand if used as
hw control trigger"), not relavant any more
- Gate the led_trigger_notify_hw_control_changed() interface behind
Kconfig LEDS_TRIGGERS_HW_CHANGED and LED device flag
LED_TRIG_HW_CHANGED
- Fix lock ordering inversion
- ideapad-laptop:
- Only call led_trigger_notify_hw_control_changed() when needed
- Serialize keyboard backlight notifications
- Reword commit messages and documentations
- Link to v1: https://patch.msgid.link/20260227190617.271388-1-i@rong.moe
---
Rong Zhang (9):
leds: Add callback offloaded() to query the state of hardware control trigger
leds: cros_ec: Implement offloaded() callback for trigger
leds: turris-omnia: Implement offloaded() callback for trigger
leds: trigger: netdev: Implement offloaded() callback
leds: Add trigger_may_offload attribute
leds: trigger: Add led_trigger_notify_hw_control_changed() interface
platform/x86: ideapad-laptop: Decouple hardware & classdev brightness for keyboard backlight
platform/x86: ideapad-laptop: Serialize keyboard backlight notifications
platform/x86: ideapad-laptop: Fully support auto keyboard backlight
.../ABI/obsolete/sysfs-class-led-trigger-netdev | 16 ++
Documentation/ABI/testing/sysfs-class-led | 22 +++
.../ABI/testing/sysfs-class-led-trigger-netdev | 13 --
Documentation/leds/leds-class.rst | 74 +++++++
drivers/leds/led-class.c | 23 +++
drivers/leds/led-triggers.c | 131 +++++++++++-
drivers/leds/leds-cros_ec.c | 6 +
drivers/leds/leds-turris-omnia.c | 7 +
drivers/leds/leds.h | 2 +
drivers/leds/trigger/Kconfig | 9 +
drivers/leds/trigger/ledtrig-netdev.c | 10 +
drivers/platform/x86/lenovo/Kconfig | 1 +
drivers/platform/x86/lenovo/ideapad-laptop.c | 219 ++++++++++++++++-----
include/linux/leds.h | 9 +
14 files changed, 481 insertions(+), 61 deletions(-)
---
base-commit: 66affa37cfac0aec061cc4bcf4a065b0c52f7e19
change-id: 20260506-leds-trigger-hw-changed-96a62188cbdf
Thanks,
Rong
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH RFC v2 1/9] leds: Add callback offloaded() to query the state of hardware control trigger
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
@ 2026-06-17 16:47 ` Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 2/9] leds: cros_ec: Implement offloaded() callback for trigger Rong Zhang
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:47 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
There are multiple triggers implementing hardware control. However, the
LED core doesn't really know the hardware control state since the
coordination is done directly between the trigger and the LED device.
Add an offloaded() callback so that the LED core can query the hardware
control state.
Signed-off-by: Rong Zhang <i@rong.moe>
---
Documentation/leds/leds-class.rst | 5 +++++
include/linux/leds.h | 1 +
2 files changed, 6 insertions(+)
diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
index 5db620ed27aa..84665200a88d 100644
--- a/Documentation/leds/leds-class.rst
+++ b/Documentation/leds/leds-class.rst
@@ -235,6 +235,11 @@ 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 must implement the following API to support hw control:
+ - offloaded:
+ return a boolean indicating if the trigger is offloaded to
+ hardware.
+
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
diff --git a/include/linux/leds.h b/include/linux/leds.h
index b16b803cc1ac..7332034a43c8 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 (*offloaded)(struct led_classdev *led_cdev);
/* Brightness set by led_trigger_event */
enum led_brightness brightness;
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC v2 2/9] leds: cros_ec: Implement offloaded() callback for trigger
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 1/9] leds: Add callback offloaded() to query the state of hardware control trigger Rong Zhang
@ 2026-06-17 16:47 ` Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 3/9] leds: turris-omnia: " Rong Zhang
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:47 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
"chromeos-auto" is a private hardware control trigger which always stays
in hardware control. Implement offloaded() callback with its return
value to be always true to reflect this.
Signed-off-by: Rong Zhang <i@rong.moe>
---
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 bea3cc3fbfd2..f48e3cf6ccf6 100644
--- a/drivers/leds/leds-cros_ec.c
+++ b/drivers/leds/leds-cros_ec.c
@@ -86,12 +86,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_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,
+ .offloaded = cros_ec_led_trigger_offloaded,
};
static int cros_ec_led_brightness_set_blocking(struct led_classdev *led_cdev,
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC v2 3/9] leds: turris-omnia: Implement offloaded() callback for trigger
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 1/9] leds: Add callback offloaded() to query the state of hardware control trigger Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 2/9] leds: cros_ec: Implement offloaded() callback for trigger Rong Zhang
@ 2026-06-17 16:47 ` Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 4/9] leds: trigger: netdev: Implement offloaded() callback Rong Zhang
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:47 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
"omnia-mcu" is a private hardware control trigger which always stays in
hardware control mode. Implement 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.
Signed-off-by: Rong Zhang <i@rong.moe>
---
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 25ee5c1eb820..8e016ca86403 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_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,
+ .offloaded = omnia_hwtrig_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) |
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC v2 4/9] leds: trigger: netdev: Implement offloaded() callback
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
` (2 preceding siblings ...)
2026-06-17 16:47 ` [PATCH RFC v2 3/9] leds: turris-omnia: " Rong Zhang
@ 2026-06-17 16:47 ` Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 5/9] leds: Add trigger_may_offload attribute Rong Zhang
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:47 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
"netdev" can run in hardware control according to hardware capabilities
and trigger options. Implement offloaded() callback to provide its
hardware control state to the LED core.
Signed-off-by: Rong Zhang <i@rong.moe>
---
drivers/leds/trigger/ledtrig-netdev.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c
index 64c078e997f2..a26109ca4b1c 100644
--- a/drivers/leds/trigger/ledtrig-netdev.c
+++ b/drivers/leds/trigger/ledtrig-netdev.c
@@ -754,10 +754,18 @@ static void netdev_trig_deactivate(struct led_classdev *led_cdev)
kfree(trigger_data);
}
+static bool netdev_trig_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,
+ .offloaded = netdev_trig_offloaded,
.groups = netdev_trig_groups,
};
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC v2 5/9] leds: Add trigger_may_offload attribute
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
` (3 preceding siblings ...)
2026-06-17 16:47 ` [PATCH RFC v2 4/9] leds: trigger: netdev: Implement offloaded() callback Rong Zhang
@ 2026-06-17 16:47 ` Rong Zhang
2026-06-17 16:48 ` [PATCH RFC v2 6/9] leds: trigger: Add led_trigger_notify_hw_control_changed() interface Rong Zhang
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:47 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
There are multiple triggers implementing hardware control. Only "netdev"
provides a custom attribute to determine if it's offloaded to hardware
(i.e., in hardware control). For other triggers, there is no obvious way
for userspace to determine the trigger state programmatically. Moreover,
userspace can't query if an LED device supports hardware control or
identifies these triggers.
Add a new attribute "trigger_may_offload" to the LED core, 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
- if the trigger is selected ("<foo_trigger>")
- if the trigger is offloaded ("[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.
Signed-off-by: Rong Zhang <i@rong.moe>
---
.../ABI/obsolete/sysfs-class-led-trigger-netdev | 16 ++++++++
Documentation/ABI/testing/sysfs-class-led | 22 +++++++++++
.../ABI/testing/sysfs-class-led-trigger-netdev | 13 -------
Documentation/leds/leds-class.rst | 8 ++++
drivers/leds/led-class.c | 23 +++++++++++
drivers/leds/led-triggers.c | 45 ++++++++++++++++++++++
drivers/leds/leds.h | 2 +
drivers/leds/trigger/ledtrig-netdev.c | 2 +
8 files changed, 118 insertions(+), 13 deletions(-)
diff --git a/Documentation/ABI/obsolete/sysfs-class-led-trigger-netdev b/Documentation/ABI/obsolete/sysfs-class-led-trigger-netdev
new file mode 100644
index 000000000000..8d2fbfaf50c3
--- /dev/null
+++ b/Documentation/ABI/obsolete/sysfs-class-led-trigger-netdev
@@ -0,0 +1,16 @@
+What: /sys/class/leds/<led>/offloaded
+Date: June 2026
+KernelVersion: 7.3
+Contact: linux-leds@vger.kernel.org
+Description:
+ Communicate whether the LED trigger modes are offloaded to
+ hardware or whether software fallback is used.
+
+ If 0, the LED is using software fallback to blink.
+
+ If 1, the LED blinking in requested mode is offloaded to
+ hardware.
+
+ /sys/class/leds/<led>/trigger_may_offload provides a generic
+ method to query the offloaded state of supported triggers,
+ superseding this attribute.
diff --git a/Documentation/ABI/testing/sysfs-class-led b/Documentation/ABI/testing/sysfs-class-led
index 0313b82644f2..edd5a9a74dfd 100644
--- a/Documentation/ABI/testing/sysfs-class-led
+++ b/Documentation/ABI/testing/sysfs-class-led
@@ -78,6 +78,28 @@ Description:
(which would often be configured in the device tree for the
hardware).
+What: /sys/class/leds/<led>/trigger_may_offload
+Date: June 2026
+KernelVersion: 7.3
+Contact: linux-leds@vger.kernel.org
+Description:
+ Names and states of triggers that may be offloaded to hardware.
+ Such triggers are also called "hw 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.
+ - `<foo_trigger>`: the trigger is selected, but falls back to
+ software blink for some reason (e.g., incompatible trigger
+ parameters)
+ - `[foo_trigger]`: the trigger is selected and offloaded to
+ hardware.
+
What: /sys/class/leds/<led>/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..396d37a4b820 100644
--- a/Documentation/ABI/testing/sysfs-class-led-trigger-netdev
+++ b/Documentation/ABI/testing/sysfs-class-led-trigger-netdev
@@ -62,19 +62,6 @@ Description:
When offloaded is true, the blink interval is controlled by
hardware and won't reflect the value set in interval.
-What: /sys/class/leds/<led>/offloaded
-Date: Jun 2023
-KernelVersion: 6.5
-Contact: linux-leds@vger.kernel.org
-Description:
- Communicate whether the LED trigger modes are offloaded to
- hardware or whether software fallback is used.
-
- If 0, the LED is using software fallback to blink.
-
- If 1, the LED blinking in requested mode is offloaded to
- hardware.
-
What: /sys/class/leds/<led>/link_10
Date: Jun 2023
KernelVersion: 6.5
diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
index 84665200a88d..41342ecb5f6b 100644
--- a/Documentation/leds/leds-class.rst
+++ b/Documentation/leds/leds-class.rst
@@ -179,6 +179,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` 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:
@@ -240,6 +243,11 @@ LED trigger must implement the following API to support hw control:
return a boolean indicating if the trigger is offloaded to
hardware.
+ If an LED driver specifies a hw control trigger but the
+ latter doesn't implement this callback, a dev_err_once will
+ be emitted and the LED trigger will be assumed to be not
+ offloaded.
+
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
diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c
index 9e14ae588f78..0ac80b93b8b5 100644
--- a/drivers/leds/led-class.c
+++ b/drivers/leds/led-class.c
@@ -90,8 +90,31 @@ static const struct bin_attribute *const led_trigger_bin_attrs[] = {
&bin_attr_trigger,
NULL,
};
+
+static DEVICE_ATTR(trigger_may_offload, 0444, led_trigger_may_offload_show, NULL);
+static struct attribute *led_trigger_attrs[] = {
+ &dev_attr_trigger_may_offload.attr,
+ NULL,
+};
+
+static umode_t led_trigger_is_visible(struct kobject *kobj,
+ 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.attr &&
+ !led_cdev->hw_control_trigger)
+ return 0;
+
+ return attr->mode;
+}
+
static const struct attribute_group led_trigger_group = {
.bin_attrs = led_trigger_bin_attrs,
+ .attrs = led_trigger_attrs,
+ .is_visible = led_trigger_is_visible,
};
#endif
diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c
index b1223218bda1..c43229d9c4c1 100644
--- a/drivers/leds/led-triggers.c
+++ b/drivers/leds/led-triggers.c
@@ -313,6 +313,51 @@ void led_trigger_set_default(struct led_classdev *led_cdev)
}
EXPORT_SYMBOL_GPL(led_trigger_set_default);
+/*
+ * Caller must ensure led_cdev->trigger_lock held,
+ * and led_cdev->trigger->name must match led_cdev->hw_control_trigger.
+ */
+static bool led_trigger_get_offloaded(struct led_classdev *led_cdev)
+{
+ if (likely(led_cdev->trigger->offloaded))
+ return led_cdev->trigger->offloaded(led_cdev);
+
+ dev_err_once(led_cdev->dev,
+ "hw control trigger %s doesn't implement offloaded(), this is a bug\n",
+ led_cdev->trigger->name);
+ return false;
+}
+
+ssize_t led_trigger_may_offload_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ bool hit, offloaded = false;
+ struct led_trigger *trig;
+ int len;
+
+ mutex_lock(&led_cdev->led_access);
+ down_read(&led_cdev->trigger_lock);
+
+ trig = led_cdev->trigger;
+
+ hit = trig && !strcmp(led_cdev->hw_control_trigger, trig->name);
+ if (hit)
+ offloaded = led_trigger_get_offloaded(led_cdev);
+
+ /* [offloaded] <active_but_not_offloaded> inactive */
+ len = sysfs_emit(buf, "%s%s%s\n",
+ offloaded ? "[" : (hit ? "<" : ""),
+ led_cdev->hw_control_trigger,
+ offloaded ? "]" : (hit ? ">" : ""));
+
+ up_read(&led_cdev->trigger_lock);
+ mutex_unlock(&led_cdev->led_access);
+
+ return len;
+}
+EXPORT_SYMBOL_GPL(led_trigger_may_offload_show);
+
/* LED Trigger Interface */
int led_trigger_register(struct led_trigger *trig)
diff --git a/drivers/leds/leds.h b/drivers/leds/leds.h
index bee46651e068..9177e098989b 100644
--- a/drivers/leds/leds.h
+++ b/drivers/leds/leds.h
@@ -27,6 +27,8 @@ ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,
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);
+ssize_t led_trigger_may_offload_show(struct device *dev,
+ struct device_attribute *attr, char *buf);
extern struct rw_semaphore leds_list_lock;
extern struct list_head leds_list;
diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c
index a26109ca4b1c..21f22eea4ab8 100644
--- a/drivers/leds/trigger/ledtrig-netdev.c
+++ b/drivers/leds/trigger/ledtrig-netdev.c
@@ -487,6 +487,8 @@ static ssize_t offloaded_show(struct device *dev,
{
struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev);
+ dev_warn_once(dev, "offloaded attribute has been deprecated, see trigger_may_offload.\n");
+
return sprintf(buf, "%d\n", trigger_data->hw_control);
}
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC v2 6/9] leds: trigger: Add led_trigger_notify_hw_control_changed() interface
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
` (4 preceding siblings ...)
2026-06-17 16:47 ` [PATCH RFC v2 5/9] leds: Add trigger_may_offload attribute Rong Zhang
@ 2026-06-17 16:48 ` Rong Zhang
2026-06-17 16:48 ` [PATCH RFC v2 7/9] platform/x86: ideapad-laptop: Decouple hardware & classdev brightness for keyboard backlight Rong Zhang
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:48 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
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, LEDS_TRIGGERS_HW_CHANGED must be enabled in
Kconfig, and the LED driver must set the LED_TRIG_HW_CHANGED flag for
the classdev.
Signed-off-by: Rong Zhang <i@rong.moe>
---
Documentation/leds/leds-class.rst | 61 +++++++++++++++++++++++++++
drivers/leds/led-triggers.c | 86 +++++++++++++++++++++++++++++++++++++--
drivers/leds/trigger/Kconfig | 9 ++++
include/linux/leds.h | 8 ++++
4 files changed, 161 insertions(+), 3 deletions(-)
diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
index 41342ecb5f6b..f250dc938e1f 100644
--- a/Documentation/leds/leds-class.rst
+++ b/Documentation/leds/leds-class.rst
@@ -261,9 +261,70 @@ 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 hw control doesn't fit any generic trigger. To associate the private
+trigger with the LED classdev, their `trigger_type` must be the same. The name
+of the private trigger must be the same as `hw_control_trigger`. 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.
+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. Calling the method
+on a classdev not registered with the `LED_TRIG_HW_CHANGED` flag or an
+appropriate `hw_control_trigger` string is a bug and will trigger a WARN_ON.
+
+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`, or else a dev_err() will be triggered.
+
+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-triggers.c b/drivers/leds/led-triggers.c
index c43229d9c4c1..73e9ce376d02 100644
--- a/drivers/leds/led-triggers.c
+++ b/drivers/leds/led-triggers.c
@@ -7,6 +7,7 @@
* Author: Richard Purdie <rpurdie@openedhand.com>
*/
+#include <linux/bug.h>
#include <linux/export.h>
#include <linux/kernel.h>
#include <linux/list.h>
@@ -162,8 +163,8 @@ ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,
}
EXPORT_SYMBOL_GPL(led_trigger_read);
-/* 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];
@@ -194,7 +195,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 don't bother
+ * leaving the LED in an undefined state.
+ */
+ if (trig || !hw_triggered)
+ led_set_brightness(led_cdev, LED_OFF);
}
if (trig) {
spin_lock(&trig->leddev_list_lock);
@@ -258,6 +273,12 @@ int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
return ret;
}
+
+/* Caller must ensure led_cdev->trigger_lock held */
+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)
@@ -448,6 +469,65 @@ 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)
+{
+ int err = 0;
+
+ if (!led_cdev->trigger) {
+ /* "none" => private trigger. */
+ if (activate)
+ err = __led_trigger_set(led_cdev, hc_trig, true);
+ } else if (led_cdev->trigger == hc_trig) {
+ /* private trigger => "none". */
+ if (!activate)
+ err = __led_trigger_set(led_cdev, NULL, true);
+ } else {
+ /* Other trigger is active. */
+ dev_dbg(led_cdev->dev,
+ "Ignoring hw control transition (%s %s) while %s is active",
+ activate ? "activate" : "deactivate", hc_trig->name,
+ led_cdev->trigger->name);
+
+ return;
+ }
+
+ if (err)
+ dev_warn(led_cdev->dev, "Failed to %s %s in hw control transition: %d",
+ activate ? "activate" : "deactivate", hc_trig->name, err);
+}
+
+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate)
+{
+ struct led_trigger *trig;
+
+ /* Restricted to private triggers. */
+ if (WARN_ON(!(led_cdev->flags & LED_TRIG_HW_CHANGED) ||
+ !led_cdev->hw_control_trigger || !led_cdev->trigger_type))
+ return;
+
+ down_read(&triggers_list_lock);
+ 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)) {
+ down_write(&led_cdev->trigger_lock);
+ led_trigger_do_hw_control_transition(led_cdev, activate, trig);
+ up_write(&led_cdev->trigger_lock);
+
+ up_read(&triggers_list_lock);
+ return;
+ }
+ }
+ up_read(&triggers_list_lock);
+
+ dev_err(led_cdev->dev,
+ "%s() is called, but the private trigger (%s) is never registered\n",
+ __func__, led_cdev->hw_control_trigger);
+}
+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/trigger/Kconfig b/drivers/leds/trigger/Kconfig
index c11282a74b5a..798122154049 100644
--- a/drivers/leds/trigger/Kconfig
+++ b/drivers/leds/trigger/Kconfig
@@ -9,6 +9,15 @@ menuconfig LEDS_TRIGGERS
if LEDS_TRIGGERS
+config LEDS_TRIGGERS_HW_CHANGED
+ bool "LED hardware-initiated trigger transition support"
+ 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 7332034a43c8..479391ddf5e5 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;
@@ -599,6 +600,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,
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC v2 7/9] platform/x86: ideapad-laptop: Decouple hardware & classdev brightness for keyboard backlight
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
` (5 preceding siblings ...)
2026-06-17 16:48 ` [PATCH RFC v2 6/9] leds: trigger: Add led_trigger_notify_hw_control_changed() interface Rong Zhang
@ 2026-06-17 16:48 ` Rong Zhang
2026-06-17 16:48 ` [PATCH RFC v2 8/9] platform/x86: ideapad-laptop: Serialize keyboard backlight notifications Rong Zhang
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:48 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
Some recent models come with an ambient light sensor (ALS). On these
models, their EC will automatically set the keyboard backlight to an
appropriate brightness when the effective "hardware brightness" is 3.
"Hardware brightness" can't be perfectly mapped to an LED classdev
brightness, but the EC does use this predefined brightness value to
represent auto mode.
Currently, the code processing keyboard backlight is coupled with LED
classdev, making it hard to expose the auto brightness (ALS) mode to the
userspace.
As the first step toward the goal, decouple hardware brightness from LED
classdev brightness, and update comments about corresponding backlight
modes.
Since upcoming changes will heavily rely on kbd_bl.last_hw_brightness,
also convert it into an atomic_t to prevent potential race conditions.
To minimalize the diff set in upcoming changes, a trivial refactor
also converts the initialization path into another equivalent form.
Signed-off-by: Rong Zhang <i@rong.moe>
---
drivers/platform/x86/lenovo/Kconfig | 1 +
drivers/platform/x86/lenovo/ideapad-laptop.c | 148 ++++++++++++++++++---------
2 files changed, 103 insertions(+), 46 deletions(-)
diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index 09b1b055d2e0..76ed1593e2aa 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -16,6 +16,7 @@ config IDEAPAD_LAPTOP
select INPUT_SPARSEKMAP
select NEW_LEDS
select LEDS_CLASS
+ select LEDS_TRIGGERS
help
This is a driver for Lenovo IdeaPad netbooks contains drivers for
rfkill switch, hotkey, fan control and backlight control.
diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c
index 4fbc904f1fc3..40153dc9a5f2 100644
--- a/drivers/platform/x86/lenovo/ideapad-laptop.c
+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c
@@ -9,6 +9,7 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
+#include <linux/atomic.h>
#include <linux/backlight.h>
#include <linux/bitfield.h>
#include <linux/bitops.h>
@@ -134,10 +135,31 @@ enum {
};
/*
- * These correspond to the number of supported states - 1
- * Future keyboard types may need a new system, if there's a collision
- * KBD_BL_TRISTATE_AUTO has no way to report or set the auto state
- * so it effectively has 3 states, but needs to handle 4
+ * The enumeration has two purposes:
+ * - as an internal identifier for all known types of keyboard backlight
+ * - as a mandatory parameter of the KBLC command
+ *
+ * For each type, the hardware brightness values are defined as follows:
+ * +--------------------------+----------+-----+------+------+
+ * | Hardware brightness | 0 | 1 | 2 | 3 |
+ * | Type | | | | |
+ * +--------------------------+----------+-----+------+------+
+ * | KBD_BL_STANDARD | off | on | N/A | N/A |
+ * +--------------------------+----------+-----+------+------+
+ * | KBD_BL_TRISTATE | off | low | high | N/A |
+ * +--------------------------+----------+-----+------+------+
+ * | KBD_BL_TRISTATE_AUTO | off | low | high | auto |
+ * +--------------------------+----------+-----+------+------+
+ *
+ * We map LED classdev brightness for KBD_BL_TRISTATE_AUTO as follows:
+ * +--------------------------+----------+-----+------+
+ * | LED classdev brightness | 0 | 1 | 2 |
+ * | Operation | | | |
+ * +--------------------------+----------+-----+------+
+ * | Read | off/auto | low | high |
+ * +--------------------------+----------+-----+------+
+ * | Write | off | low | high |
+ * +--------------------------+----------+-----+------+
*/
enum {
KBD_BL_STANDARD = 1,
@@ -145,6 +167,8 @@ enum {
KBD_BL_TRISTATE_AUTO = 3,
};
+#define KBD_BL_AUTO_MODE_HW_BRIGHTNESS 3
+
#define KBD_BL_QUERY_TYPE 0x1
#define KBD_BL_TRISTATE_TYPE 0x5
#define KBD_BL_TRISTATE_AUTO_TYPE 0x7
@@ -203,7 +227,7 @@ struct ideapad_private {
bool initialized;
int type;
struct led_classdev led;
- unsigned int last_brightness;
+ atomic_t last_hw_brightness;
} kbd_bl;
struct {
bool initialized;
@@ -1592,7 +1616,24 @@ static int ideapad_kbd_bl_check_tristate(int type)
return (type == KBD_BL_TRISTATE) || (type == KBD_BL_TRISTATE_AUTO);
}
-static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)
+static int ideapad_kbd_bl_brightness_parse(struct ideapad_private *priv, int hw_brightness)
+{
+ /* Off, low or high */
+ if (hw_brightness <= priv->kbd_bl.led.max_brightness)
+ return hw_brightness;
+
+ /* Auto (controlled by EC according to ALS), report as off */
+ if (priv->kbd_bl.type == KBD_BL_TRISTATE_AUTO &&
+ hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS)
+ return 0;
+
+ /* Unknown value */
+ dev_warn(&priv->platform_device->dev,
+ "Unknown keyboard backlight value: %u", hw_brightness);
+ return -EINVAL;
+}
+
+static int ideapad_kbd_bl_hw_brightness_get(struct ideapad_private *priv)
{
unsigned long value;
int err;
@@ -1606,21 +1647,7 @@ static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)
if (err)
return err;
- /* Convert returned value to brightness level */
- value = FIELD_GET(KBD_BL_GET_BRIGHTNESS, value);
-
- /* Off, low or high */
- if (value <= priv->kbd_bl.led.max_brightness)
- return value;
-
- /* Auto, report as off */
- if (value == priv->kbd_bl.led.max_brightness + 1)
- return 0;
-
- /* Unknown value */
- dev_warn(&priv->platform_device->dev,
- "Unknown keyboard backlight value: %lu", value);
- return -EINVAL;
+ return FIELD_GET(KBD_BL_GET_BRIGHTNESS, value);
}
err = eval_hals(priv->adev->handle, &value);
@@ -1630,6 +1657,16 @@ static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)
return !!test_bit(HALS_KBD_BL_STATE_BIT, &value);
}
+static int ideapad_kbd_bl_brightness_get(struct ideapad_private *priv)
+{
+ int hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
+
+ if (hw_brightness < 0)
+ return hw_brightness;
+
+ return ideapad_kbd_bl_brightness_parse(priv, hw_brightness);
+}
+
static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_classdev *led_cdev)
{
struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
@@ -1637,32 +1674,37 @@ static enum led_brightness ideapad_kbd_bl_led_cdev_brightness_get(struct led_cla
return ideapad_kbd_bl_brightness_get(priv);
}
-static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, unsigned int brightness)
+static int ideapad_kbd_bl_hw_brightness_set(struct ideapad_private *priv, int hw_brightness)
{
- int err;
unsigned long value;
int type = priv->kbd_bl.type;
+ int err;
if (ideapad_kbd_bl_check_tristate(type)) {
- if (brightness > priv->kbd_bl.led.max_brightness)
- return -EINVAL;
-
- value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, brightness) |
+ value = FIELD_PREP(KBD_BL_SET_BRIGHTNESS, hw_brightness) |
FIELD_PREP(KBD_BL_COMMAND_TYPE, type) |
KBD_BL_COMMAND_SET;
err = exec_kblc(priv->adev->handle, value);
} else {
- err = exec_sals(priv->adev->handle, brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF);
+ value = hw_brightness ? SALS_KBD_BL_ON : SALS_KBD_BL_OFF;
+ err = exec_sals(priv->adev->handle, value);
}
-
if (err)
return err;
- priv->kbd_bl.last_brightness = brightness;
+ atomic_set(&priv->kbd_bl.last_hw_brightness, hw_brightness);
return 0;
}
+static int ideapad_kbd_bl_brightness_set(struct ideapad_private *priv, int brightness)
+{
+ if (brightness > priv->kbd_bl.led.max_brightness)
+ return -EINVAL;
+
+ return ideapad_kbd_bl_hw_brightness_set(priv, brightness);
+}
+
static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
@@ -1673,26 +1715,31 @@ static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev,
static void ideapad_kbd_bl_notify(struct ideapad_private *priv)
{
- int brightness;
+ int hw_brightness, brightness, last_brightness, last_hw_brightness;
if (!priv->kbd_bl.initialized)
return;
- brightness = ideapad_kbd_bl_brightness_get(priv);
- if (brightness < 0)
+ hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
+ if (hw_brightness < 0)
return;
- if (brightness == priv->kbd_bl.last_brightness)
- return;
+ brightness = ideapad_kbd_bl_brightness_parse(priv, hw_brightness);
+ if (brightness < 0)
+ return; /* Reject insane values early. */
- priv->kbd_bl.last_brightness = brightness;
+ last_hw_brightness = atomic_xchg(&priv->kbd_bl.last_hw_brightness, hw_brightness);
+ if (hw_brightness == last_hw_brightness)
+ return;
- led_classdev_notify_brightness_hw_changed(&priv->kbd_bl.led, brightness);
+ last_brightness = ideapad_kbd_bl_brightness_parse(priv, last_hw_brightness);
+ if (last_brightness < 0 || brightness != last_brightness)
+ led_classdev_notify_brightness_hw_changed(&priv->kbd_bl.led, brightness);
}
static int ideapad_kbd_bl_init(struct ideapad_private *priv)
{
- int brightness, err;
+ int hw_brightness, err;
if (!priv->features.kbd_bl)
return -ENODEV;
@@ -1700,21 +1747,30 @@ static int ideapad_kbd_bl_init(struct ideapad_private *priv)
if (WARN_ON(priv->kbd_bl.initialized))
return -EEXIST;
- if (ideapad_kbd_bl_check_tristate(priv->kbd_bl.type))
- priv->kbd_bl.led.max_brightness = 2;
- else
- priv->kbd_bl.led.max_brightness = 1;
+ hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
+ if (hw_brightness < 0)
+ return hw_brightness;
- brightness = ideapad_kbd_bl_brightness_get(priv);
- if (brightness < 0)
- return brightness;
+ atomic_set(&priv->kbd_bl.last_hw_brightness, hw_brightness);
- priv->kbd_bl.last_brightness = brightness;
priv->kbd_bl.led.name = "platform::" LED_FUNCTION_KBD_BACKLIGHT;
priv->kbd_bl.led.brightness_get = ideapad_kbd_bl_led_cdev_brightness_get;
priv->kbd_bl.led.brightness_set_blocking = ideapad_kbd_bl_led_cdev_brightness_set;
priv->kbd_bl.led.flags = LED_BRIGHT_HW_CHANGED | LED_RETAIN_AT_SHUTDOWN;
+ switch (priv->kbd_bl.type) {
+ case KBD_BL_TRISTATE_AUTO:
+ case KBD_BL_TRISTATE:
+ priv->kbd_bl.led.max_brightness = 2;
+ break;
+ case KBD_BL_STANDARD:
+ priv->kbd_bl.led.max_brightness = 1;
+ break;
+ default:
+ /* This has already been validated by ideapad_check_features(). */
+ unreachable();
+ }
+
err = led_classdev_register(&priv->platform_device->dev, &priv->kbd_bl.led);
if (err)
return err;
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC v2 8/9] platform/x86: ideapad-laptop: Serialize keyboard backlight notifications
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
` (6 preceding siblings ...)
2026-06-17 16:48 ` [PATCH RFC v2 7/9] platform/x86: ideapad-laptop: Decouple hardware & classdev brightness for keyboard backlight Rong Zhang
@ 2026-06-17 16:48 ` Rong Zhang
2026-06-17 16:48 ` [PATCH RFC v2 9/9] platform/x86: ideapad-laptop: Fully support auto keyboard backlight Rong Zhang
2026-07-02 13:41 ` [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Lee Jones
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:48 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
ACPI notifications are delivered in dedicated work contexts and may
arrive simultaneously. In the following change, much work will be done
while handling the notification, which could lead to potential race
conditions.
Introduce a new mutex to serialize keyboard backlight notifications to
prevent potential race conditions.
Signed-off-by: Rong Zhang <i@rong.moe>
---
drivers/platform/x86/lenovo/ideapad-laptop.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c
index 40153dc9a5f2..97949094ead4 100644
--- a/drivers/platform/x86/lenovo/ideapad-laptop.c
+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c
@@ -26,7 +26,9 @@
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/leds.h>
+#include <linux/lockdep.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/platform_profile.h>
#include <linux/power_supply.h>
@@ -228,6 +230,8 @@ struct ideapad_private {
int type;
struct led_classdev led;
atomic_t last_hw_brightness;
+
+ struct mutex notif_mutex; /* protects notifications */
} kbd_bl;
struct {
bool initialized;
@@ -1720,6 +1724,8 @@ static void ideapad_kbd_bl_notify(struct ideapad_private *priv)
if (!priv->kbd_bl.initialized)
return;
+ guard(mutex)(&priv->kbd_bl.notif_mutex);
+
hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
if (hw_brightness < 0)
return;
@@ -1747,6 +1753,10 @@ static int ideapad_kbd_bl_init(struct ideapad_private *priv)
if (WARN_ON(priv->kbd_bl.initialized))
return -EEXIST;
+ err = devm_mutex_init(&priv->platform_device->dev, &priv->kbd_bl.notif_mutex);
+ if (err)
+ return err;
+
hw_brightness = ideapad_kbd_bl_hw_brightness_get(priv);
if (hw_brightness < 0)
return hw_brightness;
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH RFC v2 9/9] platform/x86: ideapad-laptop: Fully support auto keyboard backlight
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
` (7 preceding siblings ...)
2026-06-17 16:48 ` [PATCH RFC v2 8/9] platform/x86: ideapad-laptop: Serialize keyboard backlight notifications Rong Zhang
@ 2026-06-17 16:48 ` Rong Zhang
2026-07-02 13:41 ` [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Lee Jones
9 siblings, 0 replies; 11+ messages in thread
From: Rong Zhang @ 2026-06-17 16:48 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Jonathan Corbet, Shuah Khan,
Thomas Weißschuh, Benson Leung, Guenter Roeck,
Marek Behún, Mark Pearson, Derek J. Clark, Hans de Goede,
Ilpo Järvinen, Ike Panhc
Cc: Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86, Rong Zhang
Currently, the auto brightness mode of keyboard backlight maps to
brightness=0 in LED classdev. The only method to switch to such a mode
is by pressing the manufacturer-defined shortcut (Fn+Space). However, 0
is a multiplexed brightness value; writing 0 simply results in the
backlight being turned off.
With brightness processing code decoupled from LED classdev, we can now
fully support the auto brightness mode. In this mode, the keyboard
backlight is controlled by the EC according to the ambient light sensor
(ALS).
To utilize this, a private hardware control trigger "ideapad-auto" is
added, with the event handling procedure calling the
led_trigger_notify_hw_control_changed() interface to activate/deactivate
the private trigger according to the current LED trigger state.
Meanwhile, block brightness changes on exit to prevent the side effect
of LED device unregistration when the private trigger is active from
resetting the brightness to zero, so that we can retain the state of
auto mode among boots.
Signed-off-by: Rong Zhang <i@rong.moe>
---
drivers/platform/x86/lenovo/ideapad-laptop.c | 63 ++++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c
index 97949094ead4..a83af9bf843c 100644
--- a/drivers/platform/x86/lenovo/ideapad-laptop.c
+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c
@@ -1714,9 +1714,56 @@ static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev,
{
struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
+ /*
+ * When deinitializing: It must be the side effect of led_cdev
+ * unregistration when our private trigger is active. We've set
+ * LED_RETAIN_AT_SHUTDOWN to retain led_cdev brightness level.
+ * To do the same for auto mode, gate changes and return early.
+ */
+ if (unlikely(!priv->kbd_bl.initialized))
+ return 0;
+
return ideapad_kbd_bl_brightness_set(priv, brightness);
}
+static bool ideapad_kbd_bl_auto_trigger_offloaded(struct led_classdev *led_cdev)
+{
+ struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
+
+ return atomic_read(&priv->kbd_bl.last_hw_brightness) == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+}
+
+static int ideapad_kbd_bl_auto_trigger_activate(struct led_classdev *led_cdev)
+{
+ struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
+
+ return ideapad_kbd_bl_hw_brightness_set(priv, KBD_BL_AUTO_MODE_HW_BRIGHTNESS);
+}
+
+static struct led_hw_trigger_type ideapad_kbd_bl_auto_trigger_type;
+
+static struct led_trigger ideapad_kbd_bl_auto_trigger = {
+ .name = "ideapad-auto",
+ .trigger_type = &ideapad_kbd_bl_auto_trigger_type,
+ .activate = ideapad_kbd_bl_auto_trigger_activate,
+ .offloaded = ideapad_kbd_bl_auto_trigger_offloaded,
+};
+
+static void ideapad_kbd_bl_notify_hw_control(struct ideapad_private *priv,
+ int hw_brightness, int last_hw_brightness)
+{
+ bool hw_control, last_hw_control;
+
+ if (priv->kbd_bl.type != KBD_BL_TRISTATE_AUTO)
+ return;
+
+ hw_control = hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+ last_hw_control = last_hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+
+ if (hw_control != last_hw_control)
+ led_trigger_notify_hw_control_changed(&priv->kbd_bl.led, hw_control);
+}
+
static void ideapad_kbd_bl_notify(struct ideapad_private *priv)
{
int hw_brightness, brightness, last_brightness, last_hw_brightness;
@@ -1738,6 +1785,8 @@ static void ideapad_kbd_bl_notify(struct ideapad_private *priv)
if (hw_brightness == last_hw_brightness)
return;
+ ideapad_kbd_bl_notify_hw_control(priv, hw_brightness, last_hw_brightness);
+
last_brightness = ideapad_kbd_bl_brightness_parse(priv, last_hw_brightness);
if (last_brightness < 0 || brightness != last_brightness)
led_classdev_notify_brightness_hw_changed(&priv->kbd_bl.led, brightness);
@@ -1770,6 +1819,20 @@ static int ideapad_kbd_bl_init(struct ideapad_private *priv)
switch (priv->kbd_bl.type) {
case KBD_BL_TRISTATE_AUTO:
+ err = devm_led_trigger_register(&priv->platform_device->dev,
+ &ideapad_kbd_bl_auto_trigger);
+ if (err)
+ return err;
+
+ priv->kbd_bl.led.flags |= LED_TRIG_HW_CHANGED;
+ priv->kbd_bl.led.hw_control_trigger = ideapad_kbd_bl_auto_trigger.name;
+ priv->kbd_bl.led.trigger_type = &ideapad_kbd_bl_auto_trigger_type;
+
+ /* Hardware remembers the last brightness level, including auto mode. */
+ if (hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS)
+ priv->kbd_bl.led.default_trigger = ideapad_kbd_bl_auto_trigger.name;
+
+ fallthrough;
case KBD_BL_TRISTATE:
priv->kbd_bl.led.max_brightness = 2;
break;
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
` (8 preceding siblings ...)
2026-06-17 16:48 ` [PATCH RFC v2 9/9] platform/x86: ideapad-laptop: Fully support auto keyboard backlight Rong Zhang
@ 2026-07-02 13:41 ` Lee Jones
9 siblings, 0 replies; 11+ messages in thread
From: Lee Jones @ 2026-07-02 13:41 UTC (permalink / raw)
To: Rong Zhang
Cc: Pavel Machek, Jonathan Corbet, Shuah Khan, Thomas Weißschuh,
Benson Leung, Guenter Roeck, Marek Behún, Mark Pearson,
Derek J. Clark, Hans de Goede, Ilpo Järvinen, Ike Panhc,
Andrew Lunn, Jakub Kicinski, Vishnu Sankar, Vishnu Sankar,
linux-leds, netdev, linux-doc, linux-kernel, chrome-platform,
platform-driver-x86
On Thu, 18 Jun 2026, Rong Zhang wrote:
> Some laptops can tune their keyboard backlight according to ambient
> light sensors (auto mode). This capability is essentially a hardware
> control trigger. Meanwhile, such laptops also offer a shrotcut for
> cycling through brightness levels and auto mode. For example, on
> ThinkBook, pressing Fn+Space cycles keyboard backlight levels in the
> following sequence:
So we're effectively lifting something out of netdev and making it more
generic. I don't generally have an issue with this, but the idea will
need more eyes on before I feel confident enough to review (for my
own little quirks) and merge it.
--
Lee Jones
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-07-02 13:41 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-17 16:47 [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 1/9] leds: Add callback offloaded() to query the state of hardware control trigger Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 2/9] leds: cros_ec: Implement offloaded() callback for trigger Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 3/9] leds: turris-omnia: " Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 4/9] leds: trigger: netdev: Implement offloaded() callback Rong Zhang
2026-06-17 16:47 ` [PATCH RFC v2 5/9] leds: Add trigger_may_offload attribute Rong Zhang
2026-06-17 16:48 ` [PATCH RFC v2 6/9] leds: trigger: Add led_trigger_notify_hw_control_changed() interface Rong Zhang
2026-06-17 16:48 ` [PATCH RFC v2 7/9] platform/x86: ideapad-laptop: Decouple hardware & classdev brightness for keyboard backlight Rong Zhang
2026-06-17 16:48 ` [PATCH RFC v2 8/9] platform/x86: ideapad-laptop: Serialize keyboard backlight notifications Rong Zhang
2026-06-17 16:48 ` [PATCH RFC v2 9/9] platform/x86: ideapad-laptop: Fully support auto keyboard backlight Rong Zhang
2026-07-02 13:41 ` [PATCH RFC v2 0/9] leds: Add support for hardware-initiated hardware control trigger transition Lee Jones
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox