kernel: pse-pd: add poll helper and LED trigger support

Backport the pending v6 poll-based event detection (devm_pse_poll_helper)
and per-PI LED triggers via the notification path, plus the
poll-interval-ms binding. Unblocks the hs104 poll path and PoE LED
triggers.

Pending upstream as [PATCH net-next v5 0/2] (reviewed; v6 pending,
merge expected soon).
Link: https://lore.kernel.org/all/20260429213224.1747410-1-github@szelinsky.de/

Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
Link: https://github.com/openwrt/openwrt/pull/22245
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
This commit is contained in:
Carlo Szelinsky
2026-07-21 18:59:25 +02:00
committed by Jonas Jelonek
parent d060557066
commit 7926c03b4d
3 changed files with 724 additions and 0 deletions
@@ -0,0 +1,30 @@
From af1b9d9d0b357d14e54abff6ec9335e5fb426f75 Mon Sep 17 00:00:00 2001
From: Carlo Szelinsky <github@szelinsky.de>
Date: Sun, 29 Mar 2026 16:24:08 +0200
Subject: dt-bindings: net: pse-pd: add poll-interval-ms property
Add the optional poll-interval-ms property for PSE controllers that
use poll-based event detection instead of interrupts. Defaults to
500ms if not specified.
Reviewed-by: Kory Maincent <kory.maincent@bootlin.com>
Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
---
--- a/Documentation/devicetree/bindings/net/pse-pd/pse-controller.yaml
+++ b/Documentation/devicetree/bindings/net/pse-pd/pse-controller.yaml
@@ -27,6 +27,14 @@ properties:
subnode. This property is deprecated, please use pse-pis instead.
enum: [0, 1]
+ poll-interval-ms:
+ description:
+ Polling interval in milliseconds for PSE controllers using
+ poll-based event detection instead of interrupts. Used when the
+ controller lacks IRQ support or the IRQ line is not wired.
+ default: 500
+ minimum: 50
+
pse-pis:
type: object
description:
@@ -0,0 +1,330 @@
From 34551486030a0b5b3a1bac4bb293633f0e5e3e0e Mon Sep 17 00:00:00 2001
From: Carlo Szelinsky <github@szelinsky.de>
Date: Sun, 29 Mar 2026 16:26:08 +0200
Subject: net: pse-pd: add devm_pse_poll_helper()
Extract the common event handling loop from pse_isr() into a shared
pse_handle_events() function, and add a generic poll-based alternative
to the IRQ path for PSE controllers that lack interrupt support or
have IRQ lines not wired on the board.
The new devm_pse_poll_helper() function sets up a delayed work that
periodically calls the driver's map_event callback to detect state
changes, feeding events into the existing ntf_fifo / pse_send_ntf_worker
notification pipeline. This reuses the same pse_irq_desc interface as
the IRQ path: the driver provides a map_event callback that populates
per-PI notification arrays.
The poll worker uses system_freezable_wq to avoid running during system
suspend when the underlying hardware (e.g. I2C bus) may be inaccessible.
devm_pse_poll_helper() only allocates the per-PI state and initializes
the delayed work; the work is armed by pse_controller_register() as the
very last step on success, gated on pcdev->polling. Drivers set up the
helper before registering the controller, so arming earlier would let
pse_poll_worker() acquire pcdev->lock and push events into the ntf_fifo
before they are initialized; and if probe failed after the helper but
before registration, the work would fire against freed devres memory.
Arming after a successful register closes both. Probe is a preemptible
process and can be outscheduled at any point, so this is reachable in
practice, not just in theory (e.g. a single-core SoC with the pse-pd
core built as a module and a squashfs rootfs makes the boot heavy
enough to lose the race).
The poll work is cancelled in pse_controller_unregister() before
pse_release_pis() frees pcdev->pi, next to the IRQ disable, so neither
the poll worker nor the IRQ handler can dereference the freed PI array.
The notifs_mask is allocated as a real bitmap via devm_bitmap_zalloc()
and passed to pse_handle_events() and the driver's map_event callback
as a pointer (mirroring the pattern from commit 5099807f335c
("net: pse-pd: fix out-of-bounds bitmap access in pse_isr() on 32-bit"))
so for_each_set_bit() does not read past a single unsigned long when
nr_lines > BITS_PER_LONG.
The poll interval defaults to 500ms, balancing responsiveness against
bus load (e.g. I2C).
Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
---
--- a/drivers/net/pse-pd/pse_core.c
+++ b/drivers/net/pse-pd/pse_core.c
@@ -15,10 +15,17 @@
#include <linux/regulator/driver.h>
#include <linux/regulator/machine.h>
#include <linux/rtnetlink.h>
+#include <linux/workqueue.h>
#include <net/net_trackers.h>
#define PSE_PW_D_LIMIT INT_MAX
+/* Default poll interval for controllers without IRQ support.
+ * 500ms provides a reasonable trade-off between responsiveness
+ * (event detection, PD detection) and I2C bus utilization.
+ */
+#define PSE_DEFAULT_POLL_INTERVAL_MS 500
+
static DEFINE_MUTEX(pse_list_mutex);
static LIST_HEAD(pse_controller_list);
static DEFINE_XARRAY_ALLOC(pse_pw_d_map);
@@ -1148,6 +1155,15 @@ int pse_controller_register(struct pse_c
blocking_notifier_call_chain(&pse_controller_notifier,
PSE_REGISTERED, pcdev);
+ /* Arm the poll work last, once everything is initialized and
+ * registration has succeeded. devm_pse_poll_helper() only sets up the
+ * delayed work; queuing it here ensures pse_poll_worker() never runs
+ * against a half-initialized controller or one whose probe later fails.
+ */
+ if (pcdev->polling)
+ queue_delayed_work(system_freezable_wq, &pcdev->poll_work,
+ msecs_to_jiffies(pcdev->poll_interval_ms));
+
return 0;
}
EXPORT_SYMBOL_GPL(pse_controller_register);
@@ -1164,6 +1180,8 @@ void pse_controller_unregister(struct ps
pse_flush_pw_ds(pcdev);
if (pcdev->irq)
disable_irq(pcdev->irq);
+ if (pcdev->polling)
+ cancel_delayed_work_sync(&pcdev->poll_work);
pse_release_pis(pcdev);
cancel_work_sync(&pcdev->ntf_work);
kfifo_free(&pcdev->ntf_fifo);
@@ -1286,66 +1304,104 @@ static int pse_set_config_isr(struct pse
}
/**
- * pse_isr - IRQ handler for PSE
- * @irq: irq number
- * @data: pointer to user interrupt structure
+ * pse_handle_events - Process PSE events for all PIs
+ * @pcdev: a pointer to the PSE controller device
+ * @notifs: per-PI notification array
+ * @notifs_mask: bitmap of PIs with events (sized for pcdev->nr_lines)
*
- * Return: irqreturn_t - status of IRQ
+ * Common event handling shared between IRQ and poll paths.
+ * Caller must hold pcdev->lock.
*/
-static irqreturn_t pse_isr(int irq, void *data)
+static void pse_handle_events(struct pse_controller_dev *pcdev,
+ unsigned long *notifs,
+ unsigned long *notifs_mask)
{
- struct pse_controller_dev *pcdev;
- struct pse_irq_desc *desc;
- struct pse_irq *h = data;
- int ret, i;
-
- desc = &h->desc;
- pcdev = h->pcdev;
-
- /* Clear notifs mask */
- memset(h->notifs, 0, pcdev->nr_lines * sizeof(*h->notifs));
- bitmap_zero(h->notifs_mask, pcdev->nr_lines);
- mutex_lock(&pcdev->lock);
- ret = desc->map_event(irq, pcdev, h->notifs, h->notifs_mask);
- if (ret || bitmap_empty(h->notifs_mask, pcdev->nr_lines)) {
- mutex_unlock(&pcdev->lock);
- return IRQ_NONE;
- }
+ int i;
- for_each_set_bit(i, h->notifs_mask, pcdev->nr_lines) {
- unsigned long notifs, rnotifs;
+ for_each_set_bit(i, notifs_mask, pcdev->nr_lines) {
+ unsigned long pi_notifs, rnotifs;
struct pse_ntf ntf = {};
+ int ret;
/* Do nothing PI not described */
if (!pcdev->pi[i].rdev)
continue;
- notifs = h->notifs[i];
+ pi_notifs = notifs[i];
if (pse_pw_d_is_sw_pw_control(pcdev, pcdev->pi[i].pw_d)) {
- ret = pse_set_config_isr(pcdev, i, notifs);
+ ret = pse_set_config_isr(pcdev, i, pi_notifs);
if (ret)
- notifs |= ETHTOOL_PSE_EVENT_SW_PW_CONTROL_ERROR;
+ pi_notifs |= ETHTOOL_PSE_EVENT_SW_PW_CONTROL_ERROR;
}
- dev_dbg(h->pcdev->dev,
- "Sending PSE notification EVT 0x%lx\n", notifs);
+ dev_dbg(pcdev->dev,
+ "Sending PSE notification EVT 0x%lx\n", pi_notifs);
- ntf.notifs = notifs;
+ ntf.notifs = pi_notifs;
ntf.id = i;
kfifo_in_spinlocked(&pcdev->ntf_fifo, &ntf, 1,
&pcdev->ntf_fifo_lock);
schedule_work(&pcdev->ntf_work);
- rnotifs = pse_to_regulator_notifs(notifs);
+ rnotifs = pse_to_regulator_notifs(pi_notifs);
regulator_notifier_call_chain(pcdev->pi[i].rdev, rnotifs,
NULL);
}
+}
+/**
+ * pse_isr - IRQ handler for PSE
+ * @irq: irq number
+ * @data: pointer to user interrupt structure
+ *
+ * Return: irqreturn_t - status of IRQ
+ */
+static irqreturn_t pse_isr(int irq, void *data)
+{
+ struct pse_controller_dev *pcdev;
+ struct pse_irq *h = data;
+ int ret;
+
+ pcdev = h->pcdev;
+
+ /* Clear notifs mask */
+ memset(h->notifs, 0, pcdev->nr_lines * sizeof(*h->notifs));
+ bitmap_zero(h->notifs_mask, pcdev->nr_lines);
+ mutex_lock(&pcdev->lock);
+ ret = h->desc.map_event(irq, pcdev, h->notifs, h->notifs_mask);
+ if (ret || bitmap_empty(h->notifs_mask, pcdev->nr_lines)) {
+ mutex_unlock(&pcdev->lock);
+ return IRQ_NONE;
+ }
+
+ pse_handle_events(pcdev, h->notifs, h->notifs_mask);
mutex_unlock(&pcdev->lock);
return IRQ_HANDLED;
}
+static void pse_poll_worker(struct work_struct *work)
+{
+ struct pse_controller_dev *pcdev =
+ container_of(work, struct pse_controller_dev,
+ poll_work.work);
+ int ret;
+
+ memset(pcdev->poll_notifs, 0,
+ pcdev->nr_lines * sizeof(*pcdev->poll_notifs));
+ bitmap_zero(pcdev->poll_notifs_mask, pcdev->nr_lines);
+ mutex_lock(&pcdev->lock);
+ ret = pcdev->poll_desc.map_event(0, pcdev, pcdev->poll_notifs,
+ pcdev->poll_notifs_mask);
+ if (!ret && !bitmap_empty(pcdev->poll_notifs_mask, pcdev->nr_lines))
+ pse_handle_events(pcdev, pcdev->poll_notifs,
+ pcdev->poll_notifs_mask);
+ mutex_unlock(&pcdev->lock);
+
+ queue_delayed_work(system_freezable_wq, &pcdev->poll_work,
+ msecs_to_jiffies(pcdev->poll_interval_ms));
+}
+
/**
* devm_pse_irq_helper - Register IRQ based PSE event notifier
* @pcdev: a pointer to the PSE
@@ -1403,6 +1459,61 @@ int devm_pse_irq_helper(struct pse_contr
}
EXPORT_SYMBOL_GPL(devm_pse_irq_helper);
+/**
+ * devm_pse_poll_helper - Register poll-based PSE event notifier
+ * @pcdev: a pointer to the PSE controller device
+ * @d: PSE event description (uses same pse_irq_desc as IRQ path)
+ *
+ * For PSE controllers without IRQ support or with IRQ not wired. Sets
+ * up a delayed work that periodically calls the driver's map_event
+ * callback to detect state changes, feeding events into the standard
+ * notification pipeline.
+ *
+ * The poll worker uses system_freezable_wq to ensure it does not run
+ * during system suspend while the hardware may be inaccessible.
+ *
+ * Must be called before devm_pse_controller_register(): this helper only
+ * sets up the delayed work, which pse_controller_register() arms as its
+ * last step on success. Calling it afterwards leaves the work unarmed and
+ * polling never starts.
+ *
+ * Return: 0 on success and errno on failure
+ */
+int devm_pse_poll_helper(struct pse_controller_dev *pcdev,
+ const struct pse_irq_desc *d)
+{
+ struct device *dev = pcdev->dev;
+
+ if (!d || !d->map_event || !d->name)
+ return -EINVAL;
+
+ pcdev->poll_desc = *d;
+ pcdev->poll_notifs = devm_kcalloc(dev, pcdev->nr_lines,
+ sizeof(*pcdev->poll_notifs),
+ GFP_KERNEL);
+ if (!pcdev->poll_notifs)
+ return -ENOMEM;
+
+ pcdev->poll_notifs_mask = devm_bitmap_zalloc(dev, pcdev->nr_lines,
+ GFP_KERNEL);
+ if (!pcdev->poll_notifs_mask)
+ return -ENOMEM;
+
+ if (!pcdev->poll_interval_ms)
+ pcdev->poll_interval_ms = PSE_DEFAULT_POLL_INTERVAL_MS;
+
+ INIT_DELAYED_WORK(&pcdev->poll_work, pse_poll_worker);
+ pcdev->polling = true;
+
+ /* The poll work is armed by pse_controller_register() as the last
+ * step on success, so it never runs against a half-initialized or
+ * never-registered controller.
+ */
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(devm_pse_poll_helper);
+
/* PSE control section */
static void __pse_control_release(struct kref *kref)
--- a/include/linux/pse-pd/pse.h
+++ b/include/linux/pse-pd/pse.h
@@ -293,6 +293,12 @@ struct pse_ntf {
* @pi: table of PSE PIs described in this controller device
* @no_of_pse_pi: flag set if the pse_pis devicetree node is not used
* @irq: PSE interrupt
+ * @polling: flag indicating poll-based event detection is active
+ * @poll_interval_ms: poll interval in milliseconds
+ * @poll_work: delayed work for poll-based event detection
+ * @poll_desc: copy of the driver's event descriptor for polling
+ * @poll_notifs: per-PI notification scratch space for poll worker
+ * @poll_notifs_mask: bitmap of PIs with events for poll worker
* @pis_prio_max: Maximum value allowed for the PSE PIs priority
* @supp_budget_eval_strategies: budget evaluation strategies supported
* by the PSE
@@ -313,6 +319,12 @@ struct pse_controller_dev {
struct pse_pi *pi;
bool no_of_pse_pi;
int irq;
+ bool polling;
+ unsigned int poll_interval_ms;
+ struct delayed_work poll_work;
+ struct pse_irq_desc poll_desc;
+ unsigned long *poll_notifs;
+ unsigned long *poll_notifs_mask;
unsigned int pis_prio_max;
u32 supp_budget_eval_strategies;
struct work_struct ntf_work;
@@ -364,6 +376,8 @@ int devm_pse_controller_register(struct
struct pse_controller_dev *pcdev);
int devm_pse_irq_helper(struct pse_controller_dev *pcdev, int irq,
int irq_flags, const struct pse_irq_desc *d);
+int devm_pse_poll_helper(struct pse_controller_dev *pcdev,
+ const struct pse_irq_desc *d);
struct pse_control *of_pse_control_get(struct device_node *node,
struct phy_device *phydev);
@@ -0,0 +1,364 @@
From 547266c0e2d887b338dee1d6e6693e987b2868cd Mon Sep 17 00:00:00 2001
From: Carlo Szelinsky <github@szelinsky.de>
Date: Sun, 29 Mar 2026 16:26:49 +0200
Subject: net: pse-pd: add LED trigger support via notification path
Add per-PI "delivering" and "enabled" LED triggers to the PSE core
subsystem. LED state is updated from the shared pse_handle_events()
function whenever the IRQ or poll path detects a state change, as well
as from the regulator enable/disable paths so that host-initiated
admin state changes via ethtool are immediately reflected.
Each trigger registers an .activate callback that syncs a freshly-bound
LED to the cached state, so an LED bound after pse_controller_register()
(e.g. via sysfs) does not stay dark until the next hardware event.
The triggers are registered before the PI regulators are exposed in
pse_controller_register(). of_load_pse_pis() has already populated
pi[]/pi[i].np by then, which is all the trigger loop needs, and
registering first closes the window where a consumer calling
regulator_enable() as soon as the regulators appear could fire
led_trigger_event() on a trigger whose led_cdevs list head is not yet
initialized. A registration failure is fatal to the probe, matching the
other allocations in the function.
The post-registration initial-state pass is run under pcdev->lock,
matching pse_led_update()'s documented locking contract and avoiding
races with concurrent regulator_enable() paths that share last_delivering
/ last_enabled and the hardware ops. pse_handle_events() gains a
lockdep_assert_held() to make that contract explicit and catch any
future caller that forgets the lock.
pse_controller_unregister() clears pcdev->pi_led_trigs before returning,
so a deferred regulator disable flushed during the regulator core's
later teardown finds pse_led_update() short-circuited rather than
walking soon-to-be-freed trigger state.
Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
---
--- a/drivers/net/pse-pd/pse_core.c
+++ b/drivers/net/pse-pd/pse_core.c
@@ -8,6 +8,7 @@
#include <linux/device.h>
#include <linux/ethtool.h>
#include <linux/ethtool_netlink.h>
+#include <linux/leds.h>
#include <linux/notifier.h>
#include <linux/of.h>
#include <linux/phy.h>
@@ -704,6 +705,168 @@ static int _pse_pi_delivery_power_sw_pw_
return 0;
}
+#if IS_ENABLED(CONFIG_LEDS_TRIGGERS)
+/**
+ * pse_pi_get_states - Fetch current delivering/enabled state for a PI
+ * @pcdev: PSE controller device
+ * @id: PI index
+ * @delivering: out, set to true if PI is currently delivering power
+ * @enabled: out, set to true if PI is administratively enabled
+ *
+ * Queries hardware via the controller ops. Caller must hold pcdev->lock.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int pse_pi_get_states(struct pse_controller_dev *pcdev, int id,
+ bool *delivering, bool *enabled)
+{
+ struct pse_pw_status pw_status = {};
+ struct pse_admin_state admin_state = {};
+ int ret;
+
+ ret = pcdev->ops->pi_get_pw_status(pcdev, id, &pw_status);
+ if (ret)
+ return ret;
+ ret = pcdev->ops->pi_get_admin_state(pcdev, id, &admin_state);
+ if (ret)
+ return ret;
+
+ *delivering = pw_status.c33_pw_status ==
+ ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING ||
+ pw_status.podl_pw_status ==
+ ETHTOOL_PODL_PSE_PW_D_STATUS_DELIVERING;
+ *enabled = admin_state.c33_admin_state ==
+ ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED ||
+ admin_state.podl_admin_state ==
+ ETHTOOL_PODL_PSE_ADMIN_STATE_ENABLED;
+
+ return 0;
+}
+
+/**
+ * pse_led_update - Update LED triggers for a PI based on current state
+ * @pcdev: PSE controller device
+ * @id: PI index
+ *
+ * Queries the current power status and admin state of the PI and
+ * fires LED trigger events on state changes. Called from the
+ * notification path and the regulator enable/disable paths.
+ *
+ * Must be called with pcdev->lock held.
+ */
+static void pse_led_update(struct pse_controller_dev *pcdev, int id)
+{
+ struct pse_pi_led_triggers *trigs;
+ bool delivering, enabled;
+
+ if (!pcdev->pi_led_trigs)
+ return;
+
+ trigs = &pcdev->pi_led_trigs[id];
+ if (!trigs->delivering.name)
+ return;
+
+ if (pse_pi_get_states(pcdev, id, &delivering, &enabled))
+ return;
+
+ if (trigs->last_delivering != delivering) {
+ trigs->last_delivering = delivering;
+ led_trigger_event(&trigs->delivering,
+ delivering ? LED_FULL : LED_OFF);
+ }
+
+ if (trigs->last_enabled != enabled) {
+ trigs->last_enabled = enabled;
+ led_trigger_event(&trigs->enabled,
+ enabled ? LED_FULL : LED_OFF);
+ }
+}
+
+/* Sync a freshly-bound LED to the cached trigger state. Without these
+ * .activate callbacks, an LED bound to the trigger after
+ * pse_controller_register() (e.g. via sysfs) would stay dark until the
+ * next hardware event toggles state.
+ */
+static int pse_led_delivering_activate(struct led_classdev *led_cdev)
+{
+ struct pse_pi_led_triggers *trigs =
+ container_of(led_cdev->trigger, struct pse_pi_led_triggers,
+ delivering);
+
+ led_set_brightness(led_cdev,
+ trigs->last_delivering ? LED_FULL : LED_OFF);
+ return 0;
+}
+
+static int pse_led_enabled_activate(struct led_classdev *led_cdev)
+{
+ struct pse_pi_led_triggers *trigs =
+ container_of(led_cdev->trigger, struct pse_pi_led_triggers,
+ enabled);
+
+ led_set_brightness(led_cdev,
+ trigs->last_enabled ? LED_FULL : LED_OFF);
+ return 0;
+}
+
+static int pse_led_triggers_register(struct pse_controller_dev *pcdev)
+{
+ struct device *dev = pcdev->dev;
+ const char *dev_id;
+ int i, ret;
+
+ dev_id = dev_name(dev);
+
+ pcdev->pi_led_trigs = devm_kcalloc(dev, pcdev->nr_lines,
+ sizeof(*pcdev->pi_led_trigs),
+ GFP_KERNEL);
+ if (!pcdev->pi_led_trigs)
+ return -ENOMEM;
+
+ for (i = 0; i < pcdev->nr_lines; i++) {
+ struct pse_pi_led_triggers *trigs = &pcdev->pi_led_trigs[i];
+
+ /* Skip PIs not described in device tree */
+ if (!pcdev->no_of_pse_pi && !pcdev->pi[i].np)
+ continue;
+
+ trigs->delivering.name = devm_kasprintf(dev, GFP_KERNEL,
+ "pse-%s:port%d:delivering",
+ dev_id, i);
+ if (!trigs->delivering.name)
+ return -ENOMEM;
+ trigs->delivering.activate = pse_led_delivering_activate;
+
+ ret = devm_led_trigger_register(dev, &trigs->delivering);
+ if (ret) {
+ trigs->delivering.name = NULL;
+ return ret;
+ }
+
+ trigs->enabled.name = devm_kasprintf(dev, GFP_KERNEL,
+ "pse-%s:port%d:enabled",
+ dev_id, i);
+ if (!trigs->enabled.name)
+ return -ENOMEM;
+ trigs->enabled.activate = pse_led_enabled_activate;
+
+ ret = devm_led_trigger_register(dev, &trigs->enabled);
+ if (ret) {
+ trigs->enabled.name = NULL;
+ return ret;
+ }
+ }
+
+ return 0;
+}
+#else
+static inline void pse_led_update(struct pse_controller_dev *pcdev, int id) {}
+static int pse_led_triggers_register(struct pse_controller_dev *pcdev)
+{
+ return 0;
+}
+#endif /* CONFIG_LEDS_TRIGGERS */
+
static int pse_pi_enable(struct regulator_dev *rdev)
{
struct pse_controller_dev *pcdev = rdev_get_drvdata(rdev);
@@ -729,6 +892,7 @@ static int pse_pi_enable(struct regulato
pcdev->pi[id].admin_state_enabled = 1;
ret = 0;
}
+ pse_led_update(pcdev, id);
mutex_unlock(&pcdev->lock);
return ret;
}
@@ -736,6 +900,7 @@ static int pse_pi_enable(struct regulato
ret = ops->pi_enable(pcdev, id);
if (!ret)
pcdev->pi[id].admin_state_enabled = 1;
+ pse_led_update(pcdev, id);
mutex_unlock(&pcdev->lock);
return ret;
@@ -759,6 +924,7 @@ static int pse_pi_disable(struct regulat
ret = _pse_pi_disable(pcdev, id);
if (!ret)
pcdev->pi[id].admin_state_enabled = 0;
+ pse_led_update(pcdev, id);
mutex_unlock(&pcdev->lock);
return 0;
@@ -1119,6 +1285,17 @@ int pse_controller_register(struct pse_c
return ret;
}
+ /* Register the LED triggers before exposing the regulators. The
+ * trigger loop only needs pi[]/pi[i].np, which of_load_pse_pis()
+ * has already populated. Registering first means a consumer that
+ * calls regulator_enable() as soon as the regulators appear cannot
+ * race against a half-initialized led_trigger (whose led_cdevs list
+ * head is not yet set up).
+ */
+ ret = pse_led_triggers_register(pcdev);
+ if (ret)
+ return ret;
+
/* Each regulator name len is pcdev dev name + 7 char +
* int max digit number (10) + 1
*/
@@ -1148,6 +1325,19 @@ int pse_controller_register(struct pse_c
if (ret)
return ret;
+ /* Query initial LED state for all PIs so already-active ports
+ * are reflected immediately without waiting for a hardware event.
+ * Hold pcdev->lock: regulators are already exposed and a
+ * concurrent regulator_enable() would race on the hw callbacks
+ * and on last_delivering / last_enabled.
+ */
+ mutex_lock(&pcdev->lock);
+ for (i = 0; i < pcdev->nr_lines; i++) {
+ if (pcdev->no_of_pse_pi || pcdev->pi[i].np)
+ pse_led_update(pcdev, i);
+ }
+ mutex_unlock(&pcdev->lock);
+
mutex_lock(&pse_list_mutex);
list_add(&pcdev->list, &pse_controller_list);
mutex_unlock(&pse_list_mutex);
@@ -1183,6 +1373,12 @@ void pse_controller_unregister(struct ps
if (pcdev->polling)
cancel_delayed_work_sync(&pcdev->poll_work);
pse_release_pis(pcdev);
+ /* The pi_led_trigs array is devm-allocated and freed only after this
+ * function returns. Clear the pointer now so a deferred regulator
+ * disable flushed during regulator_unregister() makes pse_led_update()
+ * short-circuit instead of walking soon-to-be-freed trigger state.
+ */
+ pcdev->pi_led_trigs = NULL;
cancel_work_sync(&pcdev->ntf_work);
kfifo_free(&pcdev->ntf_fifo);
mutex_lock(&pse_list_mutex);
@@ -1318,12 +1514,21 @@ static void pse_handle_events(struct pse
{
int i;
+ lockdep_assert_held(&pcdev->lock);
+
for_each_set_bit(i, notifs_mask, pcdev->nr_lines) {
unsigned long pi_notifs, rnotifs;
struct pse_ntf ntf = {};
int ret;
- /* Do nothing PI not described */
+ /* Update LEDs for described PIs regardless of consumer state.
+ * LED triggers are registered at controller init, before any
+ * PHY claims a PSE control, so rdev may still be NULL here.
+ */
+ if (pcdev->no_of_pse_pi || pcdev->pi[i].np)
+ pse_led_update(pcdev, i);
+
+ /* Skip regulator/netlink path for PIs without consumers */
if (!pcdev->pi[i].rdev)
continue;
--- a/include/linux/pse-pd/pse.h
+++ b/include/linux/pse-pd/pse.h
@@ -10,6 +10,7 @@
#include <linux/kfifo.h>
#include <uapi/linux/ethtool.h>
#include <uapi/linux/ethtool_netlink_generated.h>
+#include <linux/leds.h>
#include <linux/regulator/driver.h>
/* Maximum current in uA according to IEEE 802.3-2022 Table 145-1 */
@@ -268,6 +269,21 @@ struct pse_pi {
};
/**
+ * struct pse_pi_led_triggers - LED trigger state for a PSE PI
+ *
+ * @delivering: LED trigger for power delivering state
+ * @enabled: LED trigger for admin enabled state
+ * @last_delivering: cached delivering state for change detection
+ * @last_enabled: cached enabled state for change detection
+ */
+struct pse_pi_led_triggers {
+ struct led_trigger delivering;
+ struct led_trigger enabled;
+ bool last_delivering;
+ bool last_enabled;
+};
+
+/**
* struct pse_ntf - PSE notification element
*
* @id: ID of the PSE control
@@ -305,6 +321,7 @@ struct pse_ntf {
* @ntf_work: workqueue for PSE notification management
* @ntf_fifo: PSE notifications FIFO
* @ntf_fifo_lock: protect @ntf_fifo writer
+ * @pi_led_trigs: per-PI LED trigger state array
*/
struct pse_controller_dev {
const struct pse_controller_ops *ops;
@@ -330,6 +347,7 @@ struct pse_controller_dev {
struct work_struct ntf_work;
DECLARE_KFIFO_PTR(ntf_fifo, struct pse_ntf);
spinlock_t ntf_fifo_lock; /* Protect @ntf_fifo writer */
+ struct pse_pi_led_triggers *pi_led_trigs;
};
/**