[RFC PATCH 7/9] leds: trigger: Add led_trigger_notify_hw_control_changed() interface
From: Rong Zhang
Date: Fri Feb 27 2026 - 14:13:17 EST
Some LED devices can autonomously activate/deactivate hw control mode.
Currently, we have 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" => hw control trigger (offloaded)
2. hw control trigger (offloaded) => "none"
If the current trigger is neither hw control trigger nor "none", or if
hw control trigger is not offloaded, no trigger transition will be made.
This protects selected sw triggers.
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.
Signed-off-by: Rong Zhang <i@xxxxxxxx>
---
Documentation/leds/leds-class.rst | 63 +++++++++++++++++++
drivers/leds/led-triggers.c | 101 +++++++++++++++++++++++++++++-
include/linux/leds.h | 5 ++
3 files changed, 166 insertions(+), 3 deletions(-)
diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
index cf7733e30bace..4d84db1067b43 100644
--- a/Documentation/leds/leds-class.rst
+++ b/Documentation/leds/leds-class.rst
@@ -255,9 +255,72 @@ 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 hw control mode of the LED 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 hw control mode transition
+==========================================
+
+Some hardware can autonomously activate/deactivate hw control mode. After the
+mode transition, the LED hardware notifies the LED driver. To update the current
+trigger accordingly, call `led_trigger_notify_hw_control_changed` on the
+classdev. The driver must set `hw_control_trigger` before registering, or else
+calling this is a bug and will trigger a WARN_ON. An LED driver that implements
+a private trigger can pass a pointer to the private trigger as the last
+parameter, otherwise NULL should be passed. The private trigger must have been
+properly registered (see above) and named after `hw_control_trigger`, or else a
+WARN_ON will be triggered.
+
+For convenience, `hw_control_trigger` refers to a trigger name defined in LED
+classdev, while "hw control trigger" refers to a unique trigger with the
+same name as the former.
+
+Only two transitions are defined:
+
+- "none" => hw control trigger (offloaded):
+ This happens when the hardware autonomously activates hw control mode
+ and when "none" (i.e., no trigger) is currently active. If the hw
+ control trigger is already active and offloaded during the hw control
+ mode transition, this is essentially a no-op.
+
+ The activation sequence for the hw control trigger will be executed as
+ normal. After switching to the hw control trigger, its offloaded status
+ is checked and must be true. Failing to set the offloaded status
+ appropriately will trigger a WARN_ON.
+
+ The LED driver must be able to handle the activation sequence even if
+ the hardware is currently under hw control mode. If the hardware can
+ handle hw control mode transition idempotently, the LED driver probably
+ already has this capability. Otherwise, the LED driver should take extra
+ care to handle the transition.
+
+ If error occurs in the activation sequence, the LED Trigger core reverts
+ the effective trigger to "none".
+
+- hw control trigger (offloaded) => "none"
+ This happens when the hardware autonomously deactivates hw control mode
+ and when the hw control trigger is currently active and offloaded. If
+ "none" (i.e., no trigger) is active during the hw control mode
+ transition, this is essentially a no-op.
+
+ The deactivation sequence for the hw control 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 during its hw
+ control mode transition.
+
+ The idempotence rule also applies.
+
+If the current trigger is neither hw control trigger nor "none", or if hw
+control trigger is not offloaded, no transition will be made.
+
Known Issues
============
diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c
index f8100381fc684..0d0279ac8291b 100644
--- a/drivers/leds/led-triggers.c
+++ b/drivers/leds/led-triggers.c
@@ -7,6 +7,7 @@
* Author: Richard Purdie <rpurdie@xxxxxxxxxxxxxx>
*/
+#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,15 @@ 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 select a new brightness level during its
+ * hw control mode transition, so only reset brightness if we
+ * are switching to another trigger or if the switching is not
+ * hardware triggered.
+ */
+ if (trig || !hw_triggered)
+ led_set_brightness(led_cdev, LED_OFF);
}
if (trig) {
spin_lock(&trig->leddev_list_lock);
@@ -258,6 +267,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)
@@ -478,6 +493,86 @@ int devm_led_trigger_register(struct device *dev,
}
EXPORT_SYMBOL_GPL(devm_led_trigger_register);
+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" => hw control trigger (offloaded). */
+ if (activate) {
+ err = __led_trigger_set(led_cdev, hc_trig, true);
+
+ /*
+ * hw control trigger must recognize the current hw state during
+ * its activation and mark itself as offloaded.
+ */
+ WARN_ON(!err && !led_trigger_get_offloaded(led_cdev));
+ }
+ } else if (led_cdev->trigger == hc_trig && led_trigger_get_offloaded(led_cdev)) {
+ /* hw control trigger (offloaded) => "none". */
+ if (!activate)
+ err = __led_trigger_set(led_cdev, NULL, true);
+ } else {
+ /* Other trigger is active, or hw control trigger is not offloaded. */
+ dev_dbg(led_cdev->dev,
+ "Do not %s hw control trigger %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 hw control trigger %s: %d",
+ activate ? "activate" : "deactivate", hc_trig->name, err);
+}
+
+/* Caller must ensure led_cdev->led_access held */
+void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate,
+ struct led_trigger *priv_trig)
+{
+ struct led_trigger *trig;
+
+ down_write(&led_cdev->trigger_lock);
+
+ if (WARN_ON(!led_cdev->hw_control_trigger))
+ goto out;
+
+ /* Fast path: hw control trigger is a private trigger. */
+ if (priv_trig) {
+ if (WARN_ON(!led_match_hw_control_trigger(led_cdev, priv_trig)))
+ goto out;
+
+ led_trigger_do_hw_control_transition(led_cdev, activate, priv_trig);
+ goto out;
+ }
+
+ /* Fast path: hw control trigger is the current trigger. */
+ if (led_cdev->trigger && led_match_hw_control_trigger(led_cdev, led_cdev->trigger)) {
+ led_trigger_do_hw_control_transition(led_cdev, activate, led_cdev->trigger);
+ goto out;
+ }
+
+ down_read(&triggers_list_lock);
+ list_for_each_entry(trig, &trigger_list, next_trig) {
+ if (led_match_hw_control_trigger(led_cdev, trig)) {
+ led_trigger_do_hw_control_transition(led_cdev, activate, trig);
+
+ up_read(&triggers_list_lock);
+ goto out;
+ }
+ }
+ up_read(&triggers_list_lock);
+
+ dev_dbg(led_cdev->dev, "hw control trigger not found: %s",
+ led_cdev->hw_control_trigger);
+
+out:
+ up_write(&led_cdev->trigger_lock);
+}
+EXPORT_SYMBOL_GPL(led_trigger_notify_hw_control_changed);
+
/* Simple LED Trigger Interface */
void led_trigger_event(struct led_trigger *trig,
diff --git a/include/linux/leds.h b/include/linux/leds.h
index 7332034a43c85..82578724fd60c 100644
--- a/include/linux/leds.h
+++ b/include/linux/leds.h
@@ -533,6 +533,8 @@ void led_trigger_blink_oneshot(struct led_trigger *trigger,
int invert);
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_notify_hw_control_changed(struct led_classdev *led_cdev, bool activate,
+ struct led_trigger *priv_trig);
void led_trigger_remove(struct led_classdev *led_cdev);
static inline void led_set_trigger_data(struct led_classdev *led_cdev,
@@ -583,6 +585,9 @@ static inline int led_trigger_set(struct led_classdev *led_cdev,
{
return 0;
}
+static inline void led_trigger_notify_hw_control_changed(struct led_classdev *led_cdev,
+ bool activate,
+ struct led_trigger *priv_trig) {}
static inline void led_trigger_remove(struct led_classdev *led_cdev) {}
static inline void led_set_trigger_data(struct led_classdev *led_cdev) {}
--
2.51.0