From b2a6ce658df63b21da2b8a759a7076e1da70a875 Mon Sep 17 00:00:00 2001 From: Daniel Golle Date: Sun, 4 Oct 2026 09:12:45 +0100 Subject: [PATCH] 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 --- ...-03-net-pse-pd-add-fault-LED-trigger.patch | 142 +++++++++ ...se-pd-skyworks-si3474-add-interrupts.patch | 30 ++ ...-report-over-current-faults-via-INTb.patch | 285 ++++++++++++++++++ 3 files changed, 457 insertions(+) create mode 100644 target/linux/generic/pending-6.18/897-03-net-pse-pd-add-fault-LED-trigger.patch create mode 100644 target/linux/generic/pending-6.18/897-04-dt-bindings-net-pse-pd-skyworks-si3474-add-interrupts.patch create mode 100644 target/linux/generic/pending-6.18/897-05-net-pse-pd-si3474-report-over-current-faults-via-INTb.patch diff --git a/target/linux/generic/pending-6.18/897-03-net-pse-pd-add-fault-LED-trigger.patch b/target/linux/generic/pending-6.18/897-03-net-pse-pd-add-fault-LED-trigger.patch new file mode 100644 index 00000000000..f06a7d1457a --- /dev/null +++ b/target/linux/generic/pending-6.18/897-03-net-pse-pd-add-fault-LED-trigger.patch @@ -0,0 +1,142 @@ +From 631a975306fb701e2fe656883f4df6e3c06d7d85 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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; + }; + + /** diff --git a/target/linux/generic/pending-6.18/897-04-dt-bindings-net-pse-pd-skyworks-si3474-add-interrupts.patch b/target/linux/generic/pending-6.18/897-04-dt-bindings-net-pse-pd-skyworks-si3474-add-interrupts.patch new file mode 100644 index 00000000000..fb8e5c604b3 --- /dev/null +++ b/target/linux/generic/pending-6.18/897-04-dt-bindings-net-pse-pd-skyworks-si3474-add-interrupts.patch @@ -0,0 +1,30 @@ +From 3b57fcfd2ba85f59786904da3ea8a8416dfa2d2e Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + .../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 diff --git a/target/linux/generic/pending-6.18/897-05-net-pse-pd-si3474-report-over-current-faults-via-INTb.patch b/target/linux/generic/pending-6.18/897-05-net-pse-pd-si3474-report-over-current-faults-via-INTb.patch new file mode 100644 index 00000000000..7bf0015fb86 --- /dev/null +++ b/target/linux/generic/pending-6.18/897-05-net-pse-pd-si3474-report-over-current-faults-via-INTb.patch @@ -0,0 +1,285 @@ +From 3a0bb2b94a94d304890077d72c48340ca8ffd05f Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 ++#include + #include + #include + #include +@@ -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[] = {