generic: pse-pd: add fault LED trigger and Si3474 IRQ

Add a per-PI fault LED trigger to the PSE core, an interrupts
property to the Si3474 binding, and INTb handling to the Si3474
driver. Over-current events latch a fault state that the driver
reports as FAULT, which lights the fault trigger. Power good and
power enable changes update the delivering and enabled triggers
without waiting for a regulator operation.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
This commit is contained in:
Daniel Golle committed 2026-10-07 00:03:51 +01:00
1 parent 6e55a3c331
commit b2a6ce658d
3 files changed
+457

No files matched your search

@@ -0,0 +1,142 @@
From 631a975306fb701e2fe656883f4df6e3c06d7d85 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sun, 4 Oct 2026 08:27:31 +0100
Subject: [PATCH 1/3] net: pse-pd: add fault LED trigger
Add a per-PI "fault" LED trigger next to the existing "delivering"
and "enabled" ones. It is lit while the controller reports the PI's
power status as fault or otherfault, or error for PoDL, so a board
can show an over-current or short-circuit shutdown on a dedicated
LED.
The state is refreshed from the places that already update the other
triggers: the event path and the regulator enable and disable paths.
Assisted-by: LLM
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/pse-pd/pse_core.c | 39 +++++++++++++++++++++++++++++++----
include/linux/pse-pd/pse.h | 4 ++++
2 files changed, 39 insertions(+), 4 deletions(-)
--- a/drivers/net/pse-pd/pse_core.c
+++ b/drivers/net/pse-pd/pse_core.c
@@ -753,18 +753,19 @@ static int _pse_pi_delivery_power_sw_pw_
#if IS_ENABLED(CONFIG_LEDS_TRIGGERS)
/**
- * pse_pi_get_states - Fetch current delivering/enabled state for a PI
+ * pse_pi_get_states - Fetch current delivering/enabled/fault 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
+ * @fault: out, set to true if PI reports a power fault
*
* 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)
+ bool *delivering, bool *enabled, bool *fault)
{
struct pse_pw_status pw_status = {};
struct pse_admin_state admin_state = {};
@@ -785,6 +786,10 @@ static int pse_pi_get_states(struct pse_
ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED ||
admin_state.podl_admin_state ==
ETHTOOL_PODL_PSE_ADMIN_STATE_ENABLED;
+ *fault = pw_status.c33_pw_status == ETHTOOL_C33_PSE_PW_D_STATUS_FAULT ||
+ pw_status.c33_pw_status ==
+ ETHTOOL_C33_PSE_PW_D_STATUS_OTHERFAULT ||
+ pw_status.podl_pw_status == ETHTOOL_PODL_PSE_PW_D_STATUS_ERROR;
return 0;
}
@@ -803,7 +808,7 @@ static int pse_pi_get_states(struct pse_
static void pse_led_update(struct pse_controller_dev *pcdev, int id)
{
struct pse_pi_led_triggers *trigs;
- bool delivering, enabled;
+ bool delivering, enabled, fault;
if (!pcdev->pi_led_trigs)
return;
@@ -812,7 +817,7 @@ static void pse_led_update(struct pse_co
if (!trigs->delivering.name)
return;
- if (pse_pi_get_states(pcdev, id, &delivering, &enabled))
+ if (pse_pi_get_states(pcdev, id, &delivering, &enabled, &fault))
return;
if (trigs->last_delivering != delivering) {
@@ -826,6 +831,11 @@ static void pse_led_update(struct pse_co
led_trigger_event(&trigs->enabled,
enabled ? LED_FULL : LED_OFF);
}
+
+ if (trigs->last_fault != fault) {
+ trigs->last_fault = fault;
+ led_trigger_event(&trigs->fault, fault ? LED_FULL : LED_OFF);
+ }
}
/* Sync a freshly-bound LED to the cached trigger state. Without these
@@ -855,6 +865,15 @@ static int pse_led_enabled_activate(stru
return 0;
}
+static int pse_led_fault_activate(struct led_classdev *led_cdev)
+{
+ struct pse_pi_led_triggers *trigs = container_of(
+ led_cdev->trigger, struct pse_pi_led_triggers, fault);
+
+ led_set_brightness(led_cdev, trigs->last_fault ? LED_FULL : LED_OFF);
+ return 0;
+}
+
static int pse_led_triggers_register(struct pse_controller_dev *pcdev)
{
struct device *dev = pcdev->dev;
@@ -901,6 +920,18 @@ static int pse_led_triggers_register(str
trigs->enabled.name = NULL;
return ret;
}
+
+ trigs->fault.name = devm_kasprintf(
+ dev, GFP_KERNEL, "pse-%s:port%d:fault", dev_id, i);
+ if (!trigs->fault.name)
+ return -ENOMEM;
+ trigs->fault.activate = pse_led_fault_activate;
+
+ ret = devm_led_trigger_register(dev, &trigs->fault);
+ if (ret) {
+ trigs->fault.name = NULL;
+ return ret;
+ }
}
return 0;
--- a/include/linux/pse-pd/pse.h
+++ b/include/linux/pse-pd/pse.h
@@ -273,14 +273,18 @@ struct pse_pi {
*
* @delivering: LED trigger for power delivering state
* @enabled: LED trigger for admin enabled state
+ * @fault: LED trigger for power fault state
* @last_delivering: cached delivering state for change detection
* @last_enabled: cached enabled state for change detection
+ * @last_fault: cached fault state for change detection
*/
struct pse_pi_led_triggers {
struct led_trigger delivering;
struct led_trigger enabled;
+ struct led_trigger fault;
bool last_delivering;
bool last_enabled;
+ bool last_fault;
};
/**
@@ -0,0 +1,30 @@
From 3b57fcfd2ba85f59786904da3ea8a8416dfa2d2e Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sun, 4 Oct 2026 08:27:35 +0100
Subject: [PATCH 2/3] dt-bindings: net: pse-pd: skyworks,si3474: add interrupts
The Si3474 has a single open-drain INTb output that signals events
from either of its two quads. Describe it so a board can wire it to
a GPIO and let the driver react to over-current faults and power
state changes.
Assisted-by: LLM
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
.../devicetree/bindings/net/pse-pd/skyworks,si3474.yaml | 5 +++++
1 file changed, 5 insertions(+)
--- a/Documentation/devicetree/bindings/net/pse-pd/skyworks,si3474.yaml
+++ b/Documentation/devicetree/bindings/net/pse-pd/skyworks,si3474.yaml
@@ -25,6 +25,11 @@ properties:
- const: main
- const: secondary
+ interrupts:
+ maxItems: 1
+ description: INTb output, asserted low by an unserved event in either
+ quad.
+
channels:
description: The Si3474 is a single-chip PoE PSE controller managing
8 physical power delivery channels. Internally, it's structured
@@ -0,0 +1,285 @@
From 3a0bb2b94a94d304890077d72c48340ca8ffd05f Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sun, 4 Oct 2026 08:27:36 +0100
Subject: [PATCH 3/3] net: pse-pd: si3474: report over-current faults via INTb
When an interrupt is described, enable the START_EVENT, P_I_FAULT,
POWER_GOOD_CHANGE and POWER_ENABLE_CHANGE sources and service them
through the PSE interrupt helper. On every interrupt the event
registers of both quads are read, which clears the events and
negates INTb.
START_FAULT, ILIM_FAULT and the 2-pair and 4-pair PCUT_FAULT events
raise ETHTOOL_PSE_EVENT_OVER_CURRENT for the affected PI and latch a
fault state. pi_get_pw_status() reports that latch as FAULT until the
PI delivers power again or is enabled or disabled, which drives the
fault LED trigger. Power good and power enable changes are mapped to
their PI so the delivering and enabled LED triggers follow the
hardware without waiting for a regulator operation.
Assisted-by: LLM
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/pse-pd/si3474.c | 179 +++++++++++++++++++++++++++++++++++-
1 file changed, 175 insertions(+), 4 deletions(-)
--- a/drivers/net/pse-pd/si3474.c
+++ b/drivers/net/pse-pd/si3474.c
@@ -86,6 +86,8 @@
* (Note: Actual mapping depends on Device Tree and PORT_REMAP config)
*/
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/of.h>
@@ -104,6 +106,25 @@
#define FIRMWARE_REVISION_REG 0x41
#define CHIP_REVISION_REG 0x43
+/* Interrupt registers */
+#define INTERRUPT_REG 0x00
+#define INTERRUPT_MASK_REG 0x01
+#define POWER_EVENT_COR_REG 0x03
+#define DISCONNECT_PCUT_FAULT_COR_REG 0x07
+#define ILIM_START_FAULT_COR_REG 0x09
+#define SUPPLY_EVENT_COR_REG 0x0B
+#define POWER_ON_FAULT_COR_REG 0x25
+
+/* INTERRUPT and INTERRUPT_MASK bits */
+#define INT_START_EVENT BIT(6)
+#define INT_P_I_FAULT BIT(5)
+#define INT_POWER_GOOD_CHANGE BIT(1)
+#define INT_POWER_ENABLE_CHANGE BIT(0)
+
+#define SI3474_INT_MASK \
+ (INT_START_EVENT | INT_P_I_FAULT | INT_POWER_GOOD_CHANGE | \
+ INT_POWER_ENABLE_CHANGE)
+
/* Main status registers */
#define POWER_STATUS_REG 0x10
#define PORT_MODE_REG 0x12
@@ -135,16 +156,28 @@
#define CHAN_MASK(chan) (0x03U << (2 * CHAN_IDX(chan)))
#define CHAN_REG(base, chan) ((base) + (CHAN_IDX(chan) * 4))
+/* PORT12 / PORT34 PCUT_FAULT_4P bits of SUPPLY_EVENT */
+#define CHAN_PCUT_FAULT_4P_BIT(chan) BIT(2 + CHAN_IDX(chan) / 2)
+
struct si3474_pi_desc {
u8 chan[2];
bool is_4p;
};
+/* Per-quad snapshot of the event registers, read clear-on-read */
+struct si3474_events {
+ u8 power;
+ u8 pcut;
+ u8 ilim_start;
+ u8 supply;
+};
+
struct si3474_priv {
struct i2c_client *client[2];
struct pse_controller_dev pcdev;
struct device_node *np;
struct si3474_pi_desc pi[SI3474_MAX_CHANS];
+ unsigned long fault_pis;
};
static struct si3474_priv *to_si3474_priv(struct pse_controller_dev *pcdev)
@@ -159,10 +192,15 @@ static void si3474_get_channels(struct s
*chan1 = priv->pi[id].chan[1];
}
+static unsigned int si3474_chan_quad(u8 chan)
+{
+ return chan < 4 ? 0 : 1;
+}
+
static struct i2c_client *si3474_get_chan_client(struct si3474_priv *priv,
u8 chan)
{
- return (chan < 4) ? priv->client[0] : priv->client[1];
+ return priv->client[si3474_chan_quad(chan)];
}
static int si3474_pi_get_admin_state(struct pse_controller_dev *pcdev, int id,
@@ -216,11 +254,15 @@ static int si3474_pi_get_pw_status(struc
delivering = ret & (CHAN_UPPER_BIT(chan0) | CHAN_UPPER_BIT(chan1));
- if (delivering)
+ if (delivering) {
+ clear_bit(id, &priv->fault_pis);
pw_status->c33_pw_status =
ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
- else
+ } else if (test_bit(id, &priv->fault_pis)) {
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;
+ } else {
pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
+ }
return 0;
}
@@ -309,6 +351,8 @@ static int si3474_pi_enable(struct pse_c
if (ret)
return ret;
+ clear_bit(id, &priv->fault_pis);
+
/* DETECT_CLASS_ENABLE must be set when using AUTO mode,
* otherwise PI does not power up - datasheet section 2.10.2
*/
@@ -346,6 +390,8 @@ static int si3474_pi_disable(struct pse_
if (ret)
return ret;
+ clear_bit(id, &priv->fault_pis);
+
return 0;
}
@@ -453,6 +499,128 @@ static int si3474_pi_get_actual_pw(struc
return DIV_ROUND_CLOSEST_ULL(tmp_64, 1000000000);
}
+static int si3474_events_read(struct i2c_client *client,
+ struct si3474_events *ev)
+{
+ s32 ret;
+
+ ret = i2c_smbus_read_byte_data(client, INTERRUPT_REG);
+ if (ret < 0)
+ return ret;
+
+ if (!(ret & SI3474_INT_MASK))
+ return 0;
+
+ ret = i2c_smbus_read_byte_data(client, POWER_EVENT_COR_REG);
+ if (ret < 0)
+ return ret;
+ ev->power = ret;
+
+ ret = i2c_smbus_read_byte_data(client, DISCONNECT_PCUT_FAULT_COR_REG);
+ if (ret < 0)
+ return ret;
+ ev->pcut = ret;
+
+ ret = i2c_smbus_read_byte_data(client, ILIM_START_FAULT_COR_REG);
+ if (ret < 0)
+ return ret;
+ ev->ilim_start = ret;
+
+ ret = i2c_smbus_read_byte_data(client, SUPPLY_EVENT_COR_REG);
+ if (ret < 0)
+ return ret;
+ ev->supply = ret;
+
+ /* START_EVENT stays set until POWER_ON_FAULT is read as well */
+ ret = i2c_smbus_read_byte_data(client, POWER_ON_FAULT_COR_REG);
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+static bool si3474_chan_over_current(const struct si3474_events *ev, u8 chan)
+{
+ return (ev->pcut & CHAN_BIT(chan)) ||
+ (ev->ilim_start & (CHAN_BIT(chan) | CHAN_UPPER_BIT(chan))) ||
+ (ev->supply & CHAN_PCUT_FAULT_4P_BIT(chan));
+}
+
+static void si3474_pi_map_event(struct si3474_priv *priv, int id,
+ const struct si3474_events *ev,
+ unsigned long *notifs,
+ unsigned long *notifs_mask)
+{
+ bool over_current = false, power_change = false;
+ const struct si3474_events *quad_ev;
+ u8 chan[2];
+ int i;
+
+ if (!priv->pi[id].is_4p)
+ return;
+
+ si3474_get_channels(priv, id, &chan[0], &chan[1]);
+
+ for (i = 0; i < 2; i++) {
+ quad_ev = &ev[si3474_chan_quad(chan[i])];
+ over_current |= si3474_chan_over_current(quad_ev, chan[i]);
+ power_change |= quad_ev->power &
+ (CHAN_BIT(chan[i]) | CHAN_UPPER_BIT(chan[i]));
+ }
+
+ if (over_current) {
+ set_bit(id, &priv->fault_pis);
+ notifs[id] |= ETHTOOL_PSE_EVENT_OVER_CURRENT;
+ }
+
+ if (over_current || power_change)
+ set_bit(id, notifs_mask);
+}
+
+static int si3474_map_event(int irq, struct pse_controller_dev *pcdev,
+ unsigned long *notifs, unsigned long *notifs_mask)
+{
+ struct si3474_priv *priv = to_si3474_priv(pcdev);
+ struct si3474_events ev[2] = {};
+ int quad, id, ret = 0, err;
+
+ /* Events of a quad that was read are consumed even if the other fails */
+ for (quad = 0; quad < 2; quad++) {
+ err = si3474_events_read(priv->client[quad], &ev[quad]);
+ if (err)
+ ret = err;
+ }
+
+ for (id = 0; id < SI3474_MAX_CHANS; id++)
+ si3474_pi_map_event(priv, id, ev, notifs, notifs_mask);
+
+ return bitmap_empty(notifs_mask, pcdev->nr_lines) ? ret : 0;
+}
+
+static int si3474_setup_irq(struct si3474_priv *priv, int irq)
+{
+ struct pse_irq_desc irq_desc = {
+ .name = "si3474-irq",
+ .map_event = si3474_map_event,
+ };
+ struct si3474_events ev;
+ int quad, ret;
+
+ for (quad = 0; quad < 2; quad++) {
+ ret = si3474_events_read(priv->client[quad], &ev);
+ if (ret)
+ return ret;
+
+ ret = i2c_smbus_write_byte_data(priv->client[quad],
+ INTERRUPT_MASK_REG,
+ SI3474_INT_MASK);
+ if (ret)
+ return ret;
+ }
+
+ return devm_pse_irq_helper(&priv->pcdev, irq, 0, &irq_desc);
+}
+
static const struct pse_controller_ops si3474_ops = {
.setup_pi_matrix = si3474_setup_pi_matrix,
.pi_enable = si3474_pi_enable,
@@ -546,7 +714,10 @@ static int si3474_i2c_probe(struct i2c_c
return ret;
}
- return 0;
+ if (client->irq <= 0)
+ return 0;
+
+ return si3474_setup_irq(priv, client->irq);
}
static const struct i2c_device_id si3474_id[] = {