From 7926c03b4da33677886872d16eaf6d5de82c22c8 Mon Sep 17 00:00:00 2001 From: Carlo Szelinsky Date: Sat, 18 Jul 2026 15:53:20 +0200 Subject: [PATCH] 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 Link: https://github.com/openwrt/openwrt/pull/22245 Signed-off-by: Jonas Jelonek --- ...ings-net-pse-pd-add-poll-interval-ms.patch | 30 ++ ...-net-pse-pd-add-devm_pse_poll_helper.patch | 330 ++++++++++++++++ ...2-net-pse-pd-add-LED-trigger-support.patch | 364 ++++++++++++++++++ 3 files changed, 724 insertions(+) create mode 100644 target/linux/generic/pending-6.18/897-00-dt-bindings-net-pse-pd-add-poll-interval-ms.patch create mode 100644 target/linux/generic/pending-6.18/897-01-net-pse-pd-add-devm_pse_poll_helper.patch create mode 100644 target/linux/generic/pending-6.18/897-02-net-pse-pd-add-LED-trigger-support.patch diff --git a/target/linux/generic/pending-6.18/897-00-dt-bindings-net-pse-pd-add-poll-interval-ms.patch b/target/linux/generic/pending-6.18/897-00-dt-bindings-net-pse-pd-add-poll-interval-ms.patch new file mode 100644 index 0000000000..3f64deafe8 --- /dev/null +++ b/target/linux/generic/pending-6.18/897-00-dt-bindings-net-pse-pd-add-poll-interval-ms.patch @@ -0,0 +1,30 @@ +From af1b9d9d0b357d14e54abff6ec9335e5fb426f75 Mon Sep 17 00:00:00 2001 +From: Carlo Szelinsky +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 +Signed-off-by: Carlo Szelinsky + +--- +--- 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: diff --git a/target/linux/generic/pending-6.18/897-01-net-pse-pd-add-devm_pse_poll_helper.patch b/target/linux/generic/pending-6.18/897-01-net-pse-pd-add-devm_pse_poll_helper.patch new file mode 100644 index 0000000000..d4f2559555 --- /dev/null +++ b/target/linux/generic/pending-6.18/897-01-net-pse-pd-add-devm_pse_poll_helper.patch @@ -0,0 +1,330 @@ +From 34551486030a0b5b3a1bac4bb293633f0e5e3e0e Mon Sep 17 00:00:00 2001 +From: Carlo Szelinsky +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 + +--- +--- a/drivers/net/pse-pd/pse_core.c ++++ b/drivers/net/pse-pd/pse_core.c +@@ -15,10 +15,17 @@ + #include + #include + #include ++#include + #include + + #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); diff --git a/target/linux/generic/pending-6.18/897-02-net-pse-pd-add-LED-trigger-support.patch b/target/linux/generic/pending-6.18/897-02-net-pse-pd-add-LED-trigger-support.patch new file mode 100644 index 0000000000..751e0a3a7b --- /dev/null +++ b/target/linux/generic/pending-6.18/897-02-net-pse-pd-add-LED-trigger-support.patch @@ -0,0 +1,364 @@ +From 547266c0e2d887b338dee1d6e6693e987b2868cd Mon Sep 17 00:00:00 2001 +From: Carlo Szelinsky +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 + +--- +--- a/drivers/net/pse-pd/pse_core.c ++++ b/drivers/net/pse-pd/pse_core.c +@@ -8,6 +8,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -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 + #include + #include ++#include + #include + + /* 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; + }; + + /**