--- /dev/null
+From 6e56a6d47a7fad705a1a1d088237b0858c01a770 Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:22 +0100
+Subject: [PATCH] net: pse-pd: Add power limit check
+
+Checking only the current limit is not sufficient. According to the
+standard, voltage can reach up to 57V and current up to 1.92A, which
+exceeds the power limit described in the standard (99.9W). Add a power
+limit check to prevent this.
+
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pse_core.c | 3 +++
+ include/linux/pse-pd/pse.h | 2 ++
+ 2 files changed, 5 insertions(+)
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -868,6 +868,9 @@ int pse_ethtool_set_pw_limit(struct pse_
+ int uV, uA, ret;
+ s64 tmp_64;
+
++ if (pw_limit > MAX_PI_PW)
++ return -ERANGE;
++
+ ret = regulator_get_voltage(psec->ps);
+ if (!ret) {
+ NL_SET_ERR_MSG(extack,
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -11,6 +11,8 @@
+
+ /* Maximum current in uA according to IEEE 802.3-2022 Table 145-1 */
+ #define MAX_PI_CURRENT 1920000
++/* Maximum power in mW according to IEEE 802.3-2022 Table 145-16 */
++#define MAX_PI_PW 99900
+
+ struct phy_device;
+ struct pse_controller_dev;
--- /dev/null
+From 0b567519d1152de52b29b2da2c47aa0f39a46266 Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:23 +0100
+Subject: [PATCH] net: pse-pd: tps23881: Simplify function returns by removing
+ redundant checks
+
+Cleaned up several functions in tps23881 by removing redundant checks on
+return values at the end of functions. These check has been removed, and
+the return statement now directly returns the function result, reducing
+the code's complexity and making it more concise.
+
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Reviewed-by: Kyle Swenson <kyle.swenson@est.tech>
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/tps23881.c | 34 ++++++----------------------------
+ 1 file changed, 6 insertions(+), 28 deletions(-)
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -59,7 +59,6 @@ static int tps23881_pi_enable(struct pse
+ struct i2c_client *client = priv->client;
+ u8 chan;
+ u16 val;
+- int ret;
+
+ if (id >= TPS23881_MAX_CHANS)
+ return -ERANGE;
+@@ -78,11 +77,7 @@ static int tps23881_pi_enable(struct pse
+ val |= BIT(chan + 4);
+ }
+
+- ret = i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
+- if (ret)
+- return ret;
+-
+- return 0;
++ return i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
+ }
+
+ static int tps23881_pi_disable(struct pse_controller_dev *pcdev, int id)
+@@ -91,7 +86,6 @@ static int tps23881_pi_disable(struct ps
+ struct i2c_client *client = priv->client;
+ u8 chan;
+ u16 val;
+- int ret;
+
+ if (id >= TPS23881_MAX_CHANS)
+ return -ERANGE;
+@@ -110,11 +104,7 @@ static int tps23881_pi_disable(struct ps
+ val |= BIT(chan + 8);
+ }
+
+- ret = i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
+- if (ret)
+- return ret;
+-
+- return 0;
++ return i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
+ }
+
+ static int tps23881_pi_is_enabled(struct pse_controller_dev *pcdev, int id)
+@@ -480,7 +470,7 @@ tps23881_write_port_matrix(struct tps238
+ struct i2c_client *client = priv->client;
+ u8 pi_id, lgcl_chan, hw_chan;
+ u16 val = 0;
+- int i, ret;
++ int i;
+
+ for (i = 0; i < port_cnt; i++) {
+ pi_id = port_matrix[i].pi_id;
+@@ -511,11 +501,7 @@ tps23881_write_port_matrix(struct tps238
+ }
+
+ /* Write hardware ports matrix */
+- ret = i2c_smbus_write_word_data(client, TPS23881_REG_PORT_MAP, val);
+- if (ret)
+- return ret;
+-
+- return 0;
++ return i2c_smbus_write_word_data(client, TPS23881_REG_PORT_MAP, val);
+ }
+
+ static int
+@@ -564,11 +550,7 @@ tps23881_set_ports_conf(struct tps23881_
+ val |= BIT(port_matrix[i].lgcl_chan[1]) |
+ BIT(port_matrix[i].lgcl_chan[1] + 4);
+ }
+- ret = i2c_smbus_write_word_data(client, TPS23881_REG_DET_CLA_EN, val);
+- if (ret)
+- return ret;
+-
+- return 0;
++ return i2c_smbus_write_word_data(client, TPS23881_REG_DET_CLA_EN, val);
+ }
+
+ static int
+@@ -594,11 +576,7 @@ tps23881_set_ports_matrix(struct tps2388
+ if (ret)
+ return ret;
+
+- ret = tps23881_set_ports_conf(priv, port_matrix);
+- if (ret)
+- return ret;
+-
+- return 0;
++ return tps23881_set_ports_conf(priv, port_matrix);
+ }
+
+ static int tps23881_setup_pi_matrix(struct pse_controller_dev *pcdev)
--- /dev/null
+From 4c2bab507eb7edc8e497e91b9b7f05d76d7e32bb Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:24 +0100
+Subject: [PATCH] net: pse-pd: tps23881: Use helpers to calculate bit offset
+ for a channel
+
+This driver frequently follows a pattern where two registers are read or
+written in a single operation, followed by calculating the bit offset for
+a specific channel.
+
+Introduce helpers to streamline this process and reduce code redundancy,
+making the codebase cleaner and more maintainable.
+
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/tps23881.c | 107 ++++++++++++++++++++++------------
+ 1 file changed, 69 insertions(+), 38 deletions(-)
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -53,6 +53,55 @@ static struct tps23881_priv *to_tps23881
+ return container_of(pcdev, struct tps23881_priv, pcdev);
+ }
+
++/*
++ * Helper to extract a value from a u16 register value, which is made of two
++ * u8 registers. The function calculates the bit offset based on the channel
++ * and extracts the relevant bits using a provided field mask.
++ *
++ * @param reg_val: The u16 register value (composed of two u8 registers).
++ * @param chan: The channel number (0-7).
++ * @param field_offset: The base bit offset to apply (e.g., 0 or 4).
++ * @param field_mask: The mask to apply to extract the required bits.
++ * @return: The extracted value for the specific channel.
++ */
++static u16 tps23881_calc_val(u16 reg_val, u8 chan, u8 field_offset,
++ u16 field_mask)
++{
++ if (chan >= 4)
++ reg_val >>= 8;
++
++ return (reg_val >> field_offset) & field_mask;
++}
++
++/*
++ * Helper to combine individual channel values into a u16 register value.
++ * The function sets the value for a specific channel in the appropriate
++ * position.
++ *
++ * @param reg_val: The current u16 register value.
++ * @param chan: The channel number (0-7).
++ * @param field_offset: The base bit offset to apply (e.g., 0 or 4).
++ * @param field_mask: The mask to apply for the field (e.g., 0x0F).
++ * @param field_val: The value to set for the specific channel (masked by
++ * field_mask).
++ * @return: The updated u16 register value with the channel value set.
++ */
++static u16 tps23881_set_val(u16 reg_val, u8 chan, u8 field_offset,
++ u16 field_mask, u16 field_val)
++{
++ field_val &= field_mask;
++
++ if (chan < 4) {
++ reg_val &= ~(field_mask << field_offset);
++ reg_val |= (field_val << field_offset);
++ } else {
++ reg_val &= ~(field_mask << (field_offset + 8));
++ reg_val |= (field_val << (field_offset + 8));
++ }
++
++ return reg_val;
++}
++
+ static int tps23881_pi_enable(struct pse_controller_dev *pcdev, int id)
+ {
+ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
+@@ -64,17 +113,12 @@ static int tps23881_pi_enable(struct pse
+ return -ERANGE;
+
+ chan = priv->port[id].chan[0];
+- if (chan < 4)
+- val = BIT(chan);
+- else
+- val = BIT(chan + 4);
++ val = tps23881_set_val(0, chan, 0, BIT(chan % 4), BIT(chan % 4));
+
+ if (priv->port[id].is_4p) {
+ chan = priv->port[id].chan[1];
+- if (chan < 4)
+- val |= BIT(chan);
+- else
+- val |= BIT(chan + 4);
++ val = tps23881_set_val(val, chan, 0, BIT(chan % 4),
++ BIT(chan % 4));
+ }
+
+ return i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
+@@ -91,17 +135,12 @@ static int tps23881_pi_disable(struct ps
+ return -ERANGE;
+
+ chan = priv->port[id].chan[0];
+- if (chan < 4)
+- val = BIT(chan + 4);
+- else
+- val = BIT(chan + 8);
++ val = tps23881_set_val(0, chan, 4, BIT(chan % 4), BIT(chan % 4));
+
+ if (priv->port[id].is_4p) {
+ chan = priv->port[id].chan[1];
+- if (chan < 4)
+- val |= BIT(chan + 4);
+- else
+- val |= BIT(chan + 8);
++ val = tps23881_set_val(val, chan, 4, BIT(chan % 4),
++ BIT(chan % 4));
+ }
+
+ return i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
+@@ -113,6 +152,7 @@ static int tps23881_pi_is_enabled(struct
+ struct i2c_client *client = priv->client;
+ bool enabled;
+ u8 chan;
++ u16 val;
+ int ret;
+
+ ret = i2c_smbus_read_word_data(client, TPS23881_REG_PW_STATUS);
+@@ -120,17 +160,13 @@ static int tps23881_pi_is_enabled(struct
+ return ret;
+
+ chan = priv->port[id].chan[0];
+- if (chan < 4)
+- enabled = ret & BIT(chan);
+- else
+- enabled = ret & BIT(chan + 4);
++ val = tps23881_calc_val(ret, chan, 0, BIT(chan % 4));
++ enabled = !!(val);
+
+ if (priv->port[id].is_4p) {
+ chan = priv->port[id].chan[1];
+- if (chan < 4)
+- enabled &= !!(ret & BIT(chan));
+- else
+- enabled &= !!(ret & BIT(chan + 4));
++ val = tps23881_calc_val(ret, chan, 0, BIT(chan % 4));
++ enabled &= !!(val);
+ }
+
+ /* Return enabled status only if both channel are on this state */
+@@ -146,6 +182,7 @@ static int tps23881_ethtool_get_status(s
+ struct i2c_client *client = priv->client;
+ bool enabled, delivering;
+ u8 chan;
++ u16 val;
+ int ret;
+
+ ret = i2c_smbus_read_word_data(client, TPS23881_REG_PW_STATUS);
+@@ -153,23 +190,17 @@ static int tps23881_ethtool_get_status(s
+ return ret;
+
+ chan = priv->port[id].chan[0];
+- if (chan < 4) {
+- enabled = ret & BIT(chan);
+- delivering = ret & BIT(chan + 4);
+- } else {
+- enabled = ret & BIT(chan + 4);
+- delivering = ret & BIT(chan + 8);
+- }
++ val = tps23881_calc_val(ret, chan, 0, BIT(chan % 4));
++ enabled = !!(val);
++ val = tps23881_calc_val(ret, chan, 4, BIT(chan % 4));
++ delivering = !!(val);
+
+ if (priv->port[id].is_4p) {
+ chan = priv->port[id].chan[1];
+- if (chan < 4) {
+- enabled &= !!(ret & BIT(chan));
+- delivering &= !!(ret & BIT(chan + 4));
+- } else {
+- enabled &= !!(ret & BIT(chan + 4));
+- delivering &= !!(ret & BIT(chan + 8));
+- }
++ val = tps23881_calc_val(ret, chan, 0, BIT(chan % 4));
++ enabled &= !!(val);
++ val = tps23881_calc_val(ret, chan, 4, BIT(chan % 4));
++ delivering &= !!(val);
+ }
+
+ /* Return delivering status only if both channel are on this state */
--- /dev/null
+From f3cb3c7bea0c08e821d8e9dfd2f96acd1db7c24e Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:25 +0100
+Subject: [PATCH] net: pse-pd: tps23881: Add missing configuration register
+ after disable
+
+When setting the PWOFF register, the controller resets multiple
+configuration registers. This patch ensures these registers are
+reconfigured as needed following a disable operation.
+
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/tps23881.c | 30 +++++++++++++++++++++++++++++-
+ 1 file changed, 29 insertions(+), 1 deletion(-)
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -130,6 +130,7 @@ static int tps23881_pi_disable(struct ps
+ struct i2c_client *client = priv->client;
+ u8 chan;
+ u16 val;
++ int ret;
+
+ if (id >= TPS23881_MAX_CHANS)
+ return -ERANGE;
+@@ -143,7 +144,34 @@ static int tps23881_pi_disable(struct ps
+ BIT(chan % 4));
+ }
+
+- return i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
++ ret = i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
++ if (ret)
++ return ret;
++
++ /* PWOFF command resets lots of register which need to be
++ * configured again. According to the datasheet "It may take upwards
++ * of 5ms after PWOFFn command for all register values to be updated"
++ */
++ mdelay(5);
++
++ /* Enable detection and classification */
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_DET_CLA_EN);
++ if (ret < 0)
++ return ret;
++
++ chan = priv->port[id].chan[0];
++ val = tps23881_set_val(ret, chan, 0, BIT(chan % 4), BIT(chan % 4));
++ val = tps23881_set_val(val, chan, 4, BIT(chan % 4), BIT(chan % 4));
++
++ if (priv->port[id].is_4p) {
++ chan = priv->port[id].chan[1];
++ val = tps23881_set_val(ret, chan, 0, BIT(chan % 4),
++ BIT(chan % 4));
++ val = tps23881_set_val(val, chan, 4, BIT(chan % 4),
++ BIT(chan % 4));
++ }
++
++ return i2c_smbus_write_word_data(client, TPS23881_REG_DET_CLA_EN, val);
+ }
+
+ static int tps23881_pi_is_enabled(struct pse_controller_dev *pcdev, int id)
--- /dev/null
+From 3e9dbfec499807767d03592ebdf19d9c15fd495b Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:27 +0100
+Subject: [PATCH] net: pse-pd: Split ethtool_get_status into multiple callbacks
+
+The ethtool_get_status callback currently handles all status and PSE
+information within a single function. This approach has two key
+drawbacks:
+
+1. If the core requires some information for purposes other than
+ ethtool_get_status, redundant code will be needed to fetch the same
+ data from the driver (like is_enabled).
+
+2. Drivers currently have access to all information passed to ethtool.
+ New variables will soon be added to ethtool status, such as PSE ID,
+ power domain IDs, and budget evaluation strategies, which are meant
+ to be managed solely by the core. Drivers should not have the ability
+ to modify these variables.
+
+To resolve these issues, ethtool_get_status has been split into multiple
+callbacks, with each handling a specific piece of information required
+by ethtool or the core.
+
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 153 ++++++++++++++++++++---------
+ drivers/net/pse-pd/pse_core.c | 83 ++++++++++++++--
+ drivers/net/pse-pd/pse_regulator.c | 26 +++--
+ drivers/net/pse-pd/tps23881.c | 60 ++++++++---
+ include/linux/pse-pd/pse.h | 87 +++++++++++++---
+ net/ethtool/pse-pd.c | 8 +-
+ 6 files changed, 323 insertions(+), 94 deletions(-)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -517,21 +517,38 @@ pd692x0_pse_ext_state_map[] = {
+ { /* sentinel */ }
+ };
+
+-static void
+-pd692x0_get_ext_state(struct ethtool_c33_pse_ext_state_info *c33_ext_state_info,
+- u32 status_code)
++static int
++pd692x0_pi_get_ext_state(struct pse_controller_dev *pcdev, int id,
++ struct pse_ext_state_info *ext_state_info)
+ {
++ struct ethtool_c33_pse_ext_state_info *c33_ext_state_info;
+ const struct pd692x0_pse_ext_state_mapping *ext_state_map;
++ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
++ struct pd692x0_msg msg, buf = {0};
++ int ret;
++
++ ret = pd692x0_fw_unavailable(priv);
++ if (ret)
++ return ret;
+
++ msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_STATUS];
++ msg.sub[2] = id;
++ ret = pd692x0_sendrecv_msg(priv, &msg, &buf);
++ if (ret < 0)
++ return ret;
++
++ c33_ext_state_info = &ext_state_info->c33_ext_state_info;
+ ext_state_map = pd692x0_pse_ext_state_map;
+ while (ext_state_map->status_code) {
+- if (ext_state_map->status_code == status_code) {
++ if (ext_state_map->status_code == buf.sub[0]) {
+ c33_ext_state_info->c33_pse_ext_state = ext_state_map->pse_ext_state;
+ c33_ext_state_info->__c33_pse_ext_substate = ext_state_map->pse_ext_substate;
+- return;
++ return 0;
+ }
+ ext_state_map++;
+ }
++
++ return 0;
+ }
+
+ struct pd692x0_class_pw {
+@@ -613,35 +630,36 @@ static int pd692x0_pi_set_pw_from_table(
+ }
+
+ static int
+-pd692x0_pi_get_pw_ranges(struct pse_control_status *st)
++pd692x0_pi_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
++ struct pse_pw_limit_ranges *pw_limit_ranges)
+ {
++ struct ethtool_c33_pse_pw_limit_range *c33_pw_limit_ranges;
+ const struct pd692x0_class_pw *pw_table;
+ int i;
+
+ pw_table = pd692x0_class_pw_table;
+- st->c33_pw_limit_ranges = kcalloc(PD692X0_CLASS_PW_TABLE_SIZE,
+- sizeof(struct ethtool_c33_pse_pw_limit_range),
+- GFP_KERNEL);
+- if (!st->c33_pw_limit_ranges)
++ c33_pw_limit_ranges = kcalloc(PD692X0_CLASS_PW_TABLE_SIZE,
++ sizeof(*c33_pw_limit_ranges),
++ GFP_KERNEL);
++ if (!c33_pw_limit_ranges)
+ return -ENOMEM;
+
+ for (i = 0; i < PD692X0_CLASS_PW_TABLE_SIZE; i++, pw_table++) {
+- st->c33_pw_limit_ranges[i].min = pw_table->class_pw;
+- st->c33_pw_limit_ranges[i].max = pw_table->class_pw + pw_table->max_added_class_pw;
++ c33_pw_limit_ranges[i].min = pw_table->class_pw;
++ c33_pw_limit_ranges[i].max = pw_table->class_pw +
++ pw_table->max_added_class_pw;
+ }
+
+- st->c33_pw_limit_nb_ranges = i;
+- return 0;
++ pw_limit_ranges->c33_pw_limit_ranges = c33_pw_limit_ranges;
++ return i;
+ }
+
+-static int pd692x0_ethtool_get_status(struct pse_controller_dev *pcdev,
+- unsigned long id,
+- struct netlink_ext_ack *extack,
+- struct pse_control_status *status)
++static int
++pd692x0_pi_get_admin_state(struct pse_controller_dev *pcdev, int id,
++ struct pse_admin_state *admin_state)
+ {
+ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
+ struct pd692x0_msg msg, buf = {0};
+- u32 class;
+ int ret;
+
+ ret = pd692x0_fw_unavailable(priv);
+@@ -654,39 +672,65 @@ static int pd692x0_ethtool_get_status(st
+ if (ret < 0)
+ return ret;
+
+- /* Compare Port Status (Communication Protocol Document par. 7.1) */
+- if ((buf.sub[0] & 0xf0) == 0x80 || (buf.sub[0] & 0xf0) == 0x90)
+- status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
+- else if (buf.sub[0] == 0x1b || buf.sub[0] == 0x22)
+- status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
+- else if (buf.sub[0] == 0x12)
+- status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;
+- else
+- status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
+-
+ if (buf.sub[1])
+- status->c33_admin_state = ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED;
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED;
+ else
+- status->c33_admin_state = ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
+
+- priv->admin_state[id] = status->c33_admin_state;
++ priv->admin_state[id] = admin_state->c33_admin_state;
+
+- pd692x0_get_ext_state(&status->c33_ext_state_info, buf.sub[0]);
+- status->c33_actual_pw = (buf.data[0] << 4 | buf.data[1]) * 100;
++ return 0;
++}
+
+- msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_PARAM];
++static int
++pd692x0_pi_get_pw_status(struct pse_controller_dev *pcdev, int id,
++ struct pse_pw_status *pw_status)
++{
++ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
++ struct pd692x0_msg msg, buf = {0};
++ int ret;
++
++ ret = pd692x0_fw_unavailable(priv);
++ if (ret)
++ return ret;
++
++ msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_STATUS];
+ msg.sub[2] = id;
+- memset(&buf, 0, sizeof(buf));
+ ret = pd692x0_sendrecv_msg(priv, &msg, &buf);
+ if (ret < 0)
+ return ret;
+
+- ret = pd692x0_pi_get_pw_from_table(buf.data[0], buf.data[1]);
+- if (ret < 0)
++ /* Compare Port Status (Communication Protocol Document par. 7.1) */
++ if ((buf.sub[0] & 0xf0) == 0x80 || (buf.sub[0] & 0xf0) == 0x90)
++ pw_status->c33_pw_status =
++ ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
++ else if (buf.sub[0] == 0x1b || buf.sub[0] == 0x22)
++ pw_status->c33_pw_status =
++ ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
++ else if (buf.sub[0] == 0x12)
++ 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;
++}
++
++static int
++pd692x0_pi_get_pw_class(struct pse_controller_dev *pcdev, int id)
++{
++ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
++ struct pd692x0_msg msg, buf = {0};
++ u32 class;
++ int ret;
++
++ ret = pd692x0_fw_unavailable(priv);
++ if (ret)
+ return ret;
+- status->c33_avail_pw_limit = ret;
+
+- memset(&buf, 0, sizeof(buf));
+ msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_CLASS];
+ msg.sub[2] = id;
+ ret = pd692x0_sendrecv_msg(priv, &msg, &buf);
+@@ -695,13 +739,29 @@ static int pd692x0_ethtool_get_status(st
+
+ class = buf.data[3] >> 4;
+ if (class <= 8)
+- status->c33_pw_class = class;
++ return class;
++
++ return 0;
++}
++
++static int
++pd692x0_pi_get_actual_pw(struct pse_controller_dev *pcdev, int id)
++{
++ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
++ struct pd692x0_msg msg, buf = {0};
++ int ret;
++
++ ret = pd692x0_fw_unavailable(priv);
++ if (ret)
++ return ret;
+
+- ret = pd692x0_pi_get_pw_ranges(status);
++ msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_STATUS];
++ msg.sub[2] = id;
++ ret = pd692x0_sendrecv_msg(priv, &msg, &buf);
+ if (ret < 0)
+ return ret;
+
+- return 0;
++ return (buf.data[0] << 4 | buf.data[1]) * 100;
+ }
+
+ static struct pd692x0_msg_ver pd692x0_get_sw_version(struct pd692x0_priv *priv)
+@@ -1038,13 +1098,18 @@ static int pd692x0_pi_set_pw_limit(struc
+
+ static const struct pse_controller_ops pd692x0_ops = {
+ .setup_pi_matrix = pd692x0_setup_pi_matrix,
+- .ethtool_get_status = pd692x0_ethtool_get_status,
++ .pi_get_admin_state = pd692x0_pi_get_admin_state,
++ .pi_get_pw_status = pd692x0_pi_get_pw_status,
++ .pi_get_ext_state = pd692x0_pi_get_ext_state,
++ .pi_get_pw_class = pd692x0_pi_get_pw_class,
++ .pi_get_actual_pw = pd692x0_pi_get_actual_pw,
+ .pi_enable = pd692x0_pi_enable,
+ .pi_disable = pd692x0_pi_disable,
+ .pi_is_enabled = pd692x0_pi_is_enabled,
+ .pi_get_voltage = pd692x0_pi_get_voltage,
+ .pi_get_pw_limit = pd692x0_pi_get_pw_limit,
+ .pi_set_pw_limit = pd692x0_pi_set_pw_limit,
++ .pi_get_pw_limit_ranges = pd692x0_pi_get_pw_limit_ranges,
+ };
+
+ #define PD692X0_FW_LINE_MAX_SZ 0xff
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -443,6 +443,13 @@ int pse_controller_register(struct pse_c
+ if (!pcdev->nr_lines)
+ pcdev->nr_lines = 1;
+
++ if (!pcdev->ops->pi_get_admin_state ||
++ !pcdev->ops->pi_get_pw_status) {
++ dev_err(pcdev->dev,
++ "Mandatory status report callbacks are missing");
++ return -EINVAL;
++ }
++
+ ret = of_load_pse_pis(pcdev);
+ if (ret)
+ return ret;
+@@ -745,25 +752,81 @@ EXPORT_SYMBOL_GPL(of_pse_control_get);
+ */
+ int pse_ethtool_get_status(struct pse_control *psec,
+ struct netlink_ext_ack *extack,
+- struct pse_control_status *status)
++ struct ethtool_pse_control_status *status)
+ {
++ struct pse_admin_state admin_state = {0};
++ struct pse_pw_status pw_status = {0};
+ const struct pse_controller_ops *ops;
+ struct pse_controller_dev *pcdev;
+- int err;
++ int ret;
+
+ pcdev = psec->pcdev;
+ ops = pcdev->ops;
+- if (!ops->ethtool_get_status) {
+- NL_SET_ERR_MSG(extack,
+- "PSE driver does not support status report");
+- return -EOPNOTSUPP;
++ mutex_lock(&pcdev->lock);
++ ret = ops->pi_get_admin_state(pcdev, psec->id, &admin_state);
++ if (ret)
++ goto out;
++ status->podl_admin_state = admin_state.podl_admin_state;
++ status->c33_admin_state = admin_state.c33_admin_state;
++
++ ret = ops->pi_get_pw_status(pcdev, psec->id, &pw_status);
++ if (ret)
++ goto out;
++ status->podl_pw_status = pw_status.podl_pw_status;
++ status->c33_pw_status = pw_status.c33_pw_status;
++
++ if (ops->pi_get_ext_state) {
++ struct pse_ext_state_info ext_state_info = {0};
++
++ ret = ops->pi_get_ext_state(pcdev, psec->id,
++ &ext_state_info);
++ if (ret)
++ goto out;
++
++ memcpy(&status->c33_ext_state_info,
++ &ext_state_info.c33_ext_state_info,
++ sizeof(status->c33_ext_state_info));
+ }
+
+- mutex_lock(&pcdev->lock);
+- err = ops->ethtool_get_status(pcdev, psec->id, extack, status);
+- mutex_unlock(&pcdev->lock);
++ if (ops->pi_get_pw_class) {
++ ret = ops->pi_get_pw_class(pcdev, psec->id);
++ if (ret < 0)
++ goto out;
++
++ status->c33_pw_class = ret;
++ }
++
++ if (ops->pi_get_actual_pw) {
++ ret = ops->pi_get_actual_pw(pcdev, psec->id);
++ if (ret < 0)
++ goto out;
++
++ status->c33_actual_pw = ret;
++ }
++
++ if (ops->pi_get_pw_limit) {
++ ret = ops->pi_get_pw_limit(pcdev, psec->id);
++ if (ret < 0)
++ goto out;
++
++ status->c33_avail_pw_limit = ret;
++ }
++
++ if (ops->pi_get_pw_limit_ranges) {
++ struct pse_pw_limit_ranges pw_limit_ranges = {0};
+
+- return err;
++ ret = ops->pi_get_pw_limit_ranges(pcdev, psec->id,
++ &pw_limit_ranges);
++ if (ret < 0)
++ goto out;
++
++ status->c33_pw_limit_ranges =
++ pw_limit_ranges.c33_pw_limit_ranges;
++ status->c33_pw_limit_nb_ranges = ret;
++ }
++out:
++ mutex_unlock(&psec->pcdev->lock);
++ return ret;
+ }
+ EXPORT_SYMBOL_GPL(pse_ethtool_get_status);
+
+--- a/drivers/net/pse-pd/pse_regulator.c
++++ b/drivers/net/pse-pd/pse_regulator.c
+@@ -60,9 +60,19 @@ pse_reg_pi_is_enabled(struct pse_control
+ }
+
+ static int
+-pse_reg_ethtool_get_status(struct pse_controller_dev *pcdev, unsigned long id,
+- struct netlink_ext_ack *extack,
+- struct pse_control_status *status)
++pse_reg_pi_get_admin_state(struct pse_controller_dev *pcdev, int id,
++ struct pse_admin_state *admin_state)
++{
++ struct pse_reg_priv *priv = to_pse_reg(pcdev);
++
++ admin_state->podl_admin_state = priv->admin_state;
++
++ return 0;
++}
++
++static int
++pse_reg_pi_get_pw_status(struct pse_controller_dev *pcdev, int id,
++ struct pse_pw_status *pw_status)
+ {
+ struct pse_reg_priv *priv = to_pse_reg(pcdev);
+ int ret;
+@@ -72,18 +82,18 @@ pse_reg_ethtool_get_status(struct pse_co
+ return ret;
+
+ if (!ret)
+- status->podl_pw_status = ETHTOOL_PODL_PSE_PW_D_STATUS_DISABLED;
++ pw_status->podl_pw_status =
++ ETHTOOL_PODL_PSE_PW_D_STATUS_DISABLED;
+ else
+- status->podl_pw_status =
++ pw_status->podl_pw_status =
+ ETHTOOL_PODL_PSE_PW_D_STATUS_DELIVERING;
+
+- status->podl_admin_state = priv->admin_state;
+-
+ return 0;
+ }
+
+ static const struct pse_controller_ops pse_reg_ops = {
+- .ethtool_get_status = pse_reg_ethtool_get_status,
++ .pi_get_admin_state = pse_reg_pi_get_admin_state,
++ .pi_get_pw_status = pse_reg_pi_get_pw_status,
+ .pi_enable = pse_reg_pi_enable,
+ .pi_is_enabled = pse_reg_pi_is_enabled,
+ .pi_disable = pse_reg_pi_disable,
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -201,14 +201,13 @@ static int tps23881_pi_is_enabled(struct
+ return enabled;
+ }
+
+-static int tps23881_ethtool_get_status(struct pse_controller_dev *pcdev,
+- unsigned long id,
+- struct netlink_ext_ack *extack,
+- struct pse_control_status *status)
++static int
++tps23881_pi_get_admin_state(struct pse_controller_dev *pcdev, int id,
++ struct pse_admin_state *admin_state)
+ {
+ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
+ struct i2c_client *client = priv->client;
+- bool enabled, delivering;
++ bool enabled;
+ u8 chan;
+ u16 val;
+ int ret;
+@@ -220,28 +219,56 @@ static int tps23881_ethtool_get_status(s
+ chan = priv->port[id].chan[0];
+ val = tps23881_calc_val(ret, chan, 0, BIT(chan % 4));
+ enabled = !!(val);
+- val = tps23881_calc_val(ret, chan, 4, BIT(chan % 4));
+- delivering = !!(val);
+
+ if (priv->port[id].is_4p) {
+ chan = priv->port[id].chan[1];
+ val = tps23881_calc_val(ret, chan, 0, BIT(chan % 4));
+ enabled &= !!(val);
++ }
++
++ /* Return enabled status only if both channel are on this state */
++ if (enabled)
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED;
++ else
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
++
++ return 0;
++}
++
++static int
++tps23881_pi_get_pw_status(struct pse_controller_dev *pcdev, int id,
++ struct pse_pw_status *pw_status)
++{
++ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
++ struct i2c_client *client = priv->client;
++ bool delivering;
++ u8 chan;
++ u16 val;
++ int ret;
++
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_PW_STATUS);
++ if (ret < 0)
++ return ret;
++
++ chan = priv->port[id].chan[0];
++ val = tps23881_calc_val(ret, chan, 4, BIT(chan % 4));
++ delivering = !!(val);
++
++ if (priv->port[id].is_4p) {
++ chan = priv->port[id].chan[1];
+ val = tps23881_calc_val(ret, chan, 4, BIT(chan % 4));
+ delivering &= !!(val);
+ }
+
+ /* Return delivering status only if both channel are on this state */
+ if (delivering)
+- status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
+- else
+- status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
+-
+- /* Return enabled status only if both channel are on this state */
+- if (enabled)
+- status->c33_admin_state = ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED;
++ pw_status->c33_pw_status =
++ ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
+ else
+- status->c33_admin_state = ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
++ pw_status->c33_pw_status =
++ ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
+
+ return 0;
+ }
+@@ -664,7 +691,8 @@ static const struct pse_controller_ops t
+ .pi_enable = tps23881_pi_enable,
+ .pi_disable = tps23881_pi_disable,
+ .pi_is_enabled = tps23881_pi_is_enabled,
+- .ethtool_get_status = tps23881_ethtool_get_status,
++ .pi_get_admin_state = tps23881_pi_get_admin_state,
++ .pi_get_pw_status = tps23881_pi_get_pw_status,
+ };
+
+ static const char fw_parity_name[] = "ti/tps23881/tps23881-parity-14.bin";
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -31,7 +31,52 @@ struct pse_control_config {
+ };
+
+ /**
+- * struct pse_control_status - PSE control/channel status.
++ * struct pse_admin_state - PSE operational state
++ *
++ * @podl_admin_state: operational state of the PoDL PSE
++ * functions. IEEE 802.3-2018 30.15.1.1.2 aPoDLPSEAdminState
++ * @c33_admin_state: operational state of the PSE
++ * functions. IEEE 802.3-2022 30.9.1.1.2 aPSEAdminState
++ */
++struct pse_admin_state {
++ enum ethtool_podl_pse_admin_state podl_admin_state;
++ enum ethtool_c33_pse_admin_state c33_admin_state;
++};
++
++/**
++ * struct pse_pw_status - PSE power detection status
++ *
++ * @podl_pw_status: power detection status of the PoDL PSE.
++ * IEEE 802.3-2018 30.15.1.1.3 aPoDLPSEPowerDetectionStatus:
++ * @c33_pw_status: power detection status of the PSE.
++ * IEEE 802.3-2022 30.9.1.1.5 aPSEPowerDetectionStatus:
++ */
++struct pse_pw_status {
++ enum ethtool_podl_pse_pw_d_status podl_pw_status;
++ enum ethtool_c33_pse_pw_d_status c33_pw_status;
++};
++
++/**
++ * struct pse_ext_state_info - PSE extended state information
++ *
++ * @c33_ext_state_info: extended state information of the PSE
++ */
++struct pse_ext_state_info {
++ struct ethtool_c33_pse_ext_state_info c33_ext_state_info;
++};
++
++/**
++ * struct pse_pw_limit_ranges - PSE power limit configuration range
++ *
++ * @c33_pw_limit_ranges: supported power limit configuration range. The driver
++ * is in charge of the memory allocation.
++ */
++struct pse_pw_limit_ranges {
++ struct ethtool_c33_pse_pw_limit_range *c33_pw_limit_ranges;
++};
++
++/**
++ * struct ethtool_pse_control_status - PSE control/channel status.
+ *
+ * @podl_admin_state: operational state of the PoDL PSE
+ * functions. IEEE 802.3-2018 30.15.1.1.2 aPoDLPSEAdminState
+@@ -49,11 +94,11 @@ struct pse_control_config {
+ * @c33_avail_pw_limit: available power limit of the PSE in mW
+ * IEEE 802.3-2022 145.2.5.4 pse_avail_pwr
+ * @c33_pw_limit_ranges: supported power limit configuration range. The driver
+- * is in charge of the memory allocation.
++ * is in charge of the memory allocation
+ * @c33_pw_limit_nb_ranges: number of supported power limit configuration
+ * ranges
+ */
+-struct pse_control_status {
++struct ethtool_pse_control_status {
+ enum ethtool_podl_pse_admin_state podl_admin_state;
+ enum ethtool_podl_pse_pw_d_status podl_pw_status;
+ enum ethtool_c33_pse_admin_state c33_admin_state;
+@@ -69,22 +114,37 @@ struct pse_control_status {
+ /**
+ * struct pse_controller_ops - PSE controller driver callbacks
+ *
+- * @ethtool_get_status: get PSE control status for ethtool interface
+ * @setup_pi_matrix: setup PI matrix of the PSE controller
++ * @pi_get_admin_state: Get the operational state of the PSE PI. This ops
++ * is mandatory.
++ * @pi_get_pw_status: Get the power detection status of the PSE PI. This
++ * ops is mandatory.
++ * @pi_get_ext_state: Get the extended state of the PSE PI.
++ * @pi_get_pw_class: Get the power class of the PSE PI.
++ * @pi_get_actual_pw: Get actual power of the PSE PI in mW.
+ * @pi_is_enabled: Return 1 if the PSE PI is enabled, 0 if not.
+ * May also return negative errno.
+ * @pi_enable: Configure the PSE PI as enabled.
+ * @pi_disable: Configure the PSE PI as disabled.
+ * @pi_get_voltage: Return voltage similarly to get_voltage regulator
+- * callback.
+- * @pi_get_pw_limit: Get the configured power limit of the PSE PI.
+- * @pi_set_pw_limit: Configure the power limit of the PSE PI.
++ * callback in uV.
++ * @pi_get_pw_limit: Get the configured power limit of the PSE PI in mW.
++ * @pi_set_pw_limit: Configure the power limit of the PSE PI in mW.
++ * @pi_get_pw_limit_ranges: Get the supported power limit configuration
++ * range. The driver is in charge of the memory
++ * allocation and should return the number of
++ * ranges.
+ */
+ struct pse_controller_ops {
+- int (*ethtool_get_status)(struct pse_controller_dev *pcdev,
+- unsigned long id, struct netlink_ext_ack *extack,
+- struct pse_control_status *status);
+ int (*setup_pi_matrix)(struct pse_controller_dev *pcdev);
++ int (*pi_get_admin_state)(struct pse_controller_dev *pcdev, int id,
++ struct pse_admin_state *admin_state);
++ int (*pi_get_pw_status)(struct pse_controller_dev *pcdev, int id,
++ struct pse_pw_status *pw_status);
++ int (*pi_get_ext_state)(struct pse_controller_dev *pcdev, int id,
++ struct pse_ext_state_info *ext_state_info);
++ int (*pi_get_pw_class)(struct pse_controller_dev *pcdev, int id);
++ int (*pi_get_actual_pw)(struct pse_controller_dev *pcdev, int id);
+ int (*pi_is_enabled)(struct pse_controller_dev *pcdev, int id);
+ int (*pi_enable)(struct pse_controller_dev *pcdev, int id);
+ int (*pi_disable)(struct pse_controller_dev *pcdev, int id);
+@@ -93,12 +153,15 @@ struct pse_controller_ops {
+ int id);
+ int (*pi_set_pw_limit)(struct pse_controller_dev *pcdev,
+ int id, int max_mW);
++ int (*pi_get_pw_limit_ranges)(struct pse_controller_dev *pcdev, int id,
++ struct pse_pw_limit_ranges *pw_limit_ranges);
+ };
+
+ struct module;
+ struct device_node;
+ struct of_phandle_args;
+ struct pse_control;
++struct ethtool_pse_control_status;
+
+ /* PSE PI pairset pinout can either be Alternative A or Alternative B */
+ enum pse_pi_pairset_pinout {
+@@ -175,7 +238,7 @@ void pse_control_put(struct pse_control
+
+ int pse_ethtool_get_status(struct pse_control *psec,
+ struct netlink_ext_ack *extack,
+- struct pse_control_status *status);
++ struct ethtool_pse_control_status *status);
+ int pse_ethtool_set_config(struct pse_control *psec,
+ struct netlink_ext_ack *extack,
+ const struct pse_control_config *config);
+@@ -201,7 +264,7 @@ static inline void pse_control_put(struc
+
+ static inline int pse_ethtool_get_status(struct pse_control *psec,
+ struct netlink_ext_ack *extack,
+- struct pse_control_status *status)
++ struct ethtool_pse_control_status *status)
+ {
+ return -EOPNOTSUPP;
+ }
+--- a/net/ethtool/pse-pd.c
++++ b/net/ethtool/pse-pd.c
+@@ -19,7 +19,7 @@ struct pse_req_info {
+
+ struct pse_reply_data {
+ struct ethnl_reply_data base;
+- struct pse_control_status status;
++ struct ethtool_pse_control_status status;
+ };
+
+ #define PSE_REPDATA(__reply_base) \
+@@ -80,7 +80,7 @@ static int pse_reply_size(const struct e
+ const struct ethnl_reply_data *reply_base)
+ {
+ const struct pse_reply_data *data = PSE_REPDATA(reply_base);
+- const struct pse_control_status *st = &data->status;
++ const struct ethtool_pse_control_status *st = &data->status;
+ int len = 0;
+
+ if (st->podl_admin_state > 0)
+@@ -114,7 +114,7 @@ static int pse_reply_size(const struct e
+ }
+
+ static int pse_put_pw_limit_ranges(struct sk_buff *skb,
+- const struct pse_control_status *st)
++ const struct ethtool_pse_control_status *st)
+ {
+ const struct ethtool_c33_pse_pw_limit_range *pw_limit_ranges;
+ int i;
+@@ -146,7 +146,7 @@ static int pse_fill_reply(struct sk_buff
+ const struct ethnl_reply_data *reply_base)
+ {
+ const struct pse_reply_data *data = PSE_REPDATA(reply_base);
+- const struct pse_control_status *st = &data->status;
++ const struct ethtool_pse_control_status *st = &data->status;
+
+ if (st->podl_admin_state > 0 &&
+ nla_put_u32(skb, ETHTOOL_A_PODL_PSE_ADMIN_STATE,
--- /dev/null
+From 4640a1f0d8f2246f34d6e74330d7e7d2cf75605b Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:28 +0100
+Subject: [PATCH] net: pse-pd: Remove is_enabled callback from drivers
+
+The is_enabled callback is now redundant as the admin_state can be obtained
+directly from the driver and provides the same information.
+
+To simplify functionality, the core will handle this internally, making
+the is_enabled callback unnecessary at the driver level. Remove the
+callback from all drivers.
+
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 26 --------------------------
+ drivers/net/pse-pd/pse_core.c | 13 +++++++++++--
+ drivers/net/pse-pd/pse_regulator.c | 9 ---------
+ drivers/net/pse-pd/tps23881.c | 28 ----------------------------
+ include/linux/pse-pd/pse.h | 3 ---
+ 5 files changed, 11 insertions(+), 68 deletions(-)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -431,31 +431,6 @@ static int pd692x0_pi_disable(struct pse
+ return 0;
+ }
+
+-static int pd692x0_pi_is_enabled(struct pse_controller_dev *pcdev, int id)
+-{
+- struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
+- struct pd692x0_msg msg, buf = {0};
+- int ret;
+-
+- ret = pd692x0_fw_unavailable(priv);
+- if (ret)
+- return ret;
+-
+- msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_STATUS];
+- msg.sub[2] = id;
+- ret = pd692x0_sendrecv_msg(priv, &msg, &buf);
+- if (ret < 0)
+- return ret;
+-
+- if (buf.sub[1]) {
+- priv->admin_state[id] = ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED;
+- return 1;
+- } else {
+- priv->admin_state[id] = ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
+- return 0;
+- }
+-}
+-
+ struct pd692x0_pse_ext_state_mapping {
+ u32 status_code;
+ enum ethtool_c33_pse_ext_state pse_ext_state;
+@@ -1105,7 +1080,6 @@ static const struct pse_controller_ops p
+ .pi_get_actual_pw = pd692x0_pi_get_actual_pw,
+ .pi_enable = pd692x0_pi_enable,
+ .pi_disable = pd692x0_pi_disable,
+- .pi_is_enabled = pd692x0_pi_is_enabled,
+ .pi_get_voltage = pd692x0_pi_get_voltage,
+ .pi_get_pw_limit = pd692x0_pi_get_pw_limit,
+ .pi_set_pw_limit = pd692x0_pi_set_pw_limit,
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -210,16 +210,25 @@ out:
+ static int pse_pi_is_enabled(struct regulator_dev *rdev)
+ {
+ struct pse_controller_dev *pcdev = rdev_get_drvdata(rdev);
++ struct pse_admin_state admin_state = {0};
+ const struct pse_controller_ops *ops;
+ int id, ret;
+
+ ops = pcdev->ops;
+- if (!ops->pi_is_enabled)
++ if (!ops->pi_get_admin_state)
+ return -EOPNOTSUPP;
+
+ id = rdev_get_id(rdev);
+ mutex_lock(&pcdev->lock);
+- ret = ops->pi_is_enabled(pcdev, id);
++ ret = ops->pi_get_admin_state(pcdev, id, &admin_state);
++ if (ret)
++ goto out;
++
++ if (admin_state.podl_admin_state == ETHTOOL_PODL_PSE_ADMIN_STATE_ENABLED ||
++ admin_state.c33_admin_state == ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED)
++ ret = 1;
++
++out:
+ mutex_unlock(&pcdev->lock);
+
+ return ret;
+--- a/drivers/net/pse-pd/pse_regulator.c
++++ b/drivers/net/pse-pd/pse_regulator.c
+@@ -52,14 +52,6 @@ pse_reg_pi_disable(struct pse_controller
+ }
+
+ static int
+-pse_reg_pi_is_enabled(struct pse_controller_dev *pcdev, int id)
+-{
+- struct pse_reg_priv *priv = to_pse_reg(pcdev);
+-
+- return regulator_is_enabled(priv->ps);
+-}
+-
+-static int
+ pse_reg_pi_get_admin_state(struct pse_controller_dev *pcdev, int id,
+ struct pse_admin_state *admin_state)
+ {
+@@ -95,7 +87,6 @@ static const struct pse_controller_ops p
+ .pi_get_admin_state = pse_reg_pi_get_admin_state,
+ .pi_get_pw_status = pse_reg_pi_get_pw_status,
+ .pi_enable = pse_reg_pi_enable,
+- .pi_is_enabled = pse_reg_pi_is_enabled,
+ .pi_disable = pse_reg_pi_disable,
+ };
+
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -174,33 +174,6 @@ static int tps23881_pi_disable(struct ps
+ return i2c_smbus_write_word_data(client, TPS23881_REG_DET_CLA_EN, val);
+ }
+
+-static int tps23881_pi_is_enabled(struct pse_controller_dev *pcdev, int id)
+-{
+- struct tps23881_priv *priv = to_tps23881_priv(pcdev);
+- struct i2c_client *client = priv->client;
+- bool enabled;
+- u8 chan;
+- u16 val;
+- int ret;
+-
+- ret = i2c_smbus_read_word_data(client, TPS23881_REG_PW_STATUS);
+- if (ret < 0)
+- return ret;
+-
+- chan = priv->port[id].chan[0];
+- val = tps23881_calc_val(ret, chan, 0, BIT(chan % 4));
+- enabled = !!(val);
+-
+- if (priv->port[id].is_4p) {
+- chan = priv->port[id].chan[1];
+- val = tps23881_calc_val(ret, chan, 0, BIT(chan % 4));
+- enabled &= !!(val);
+- }
+-
+- /* Return enabled status only if both channel are on this state */
+- return enabled;
+-}
+-
+ static int
+ tps23881_pi_get_admin_state(struct pse_controller_dev *pcdev, int id,
+ struct pse_admin_state *admin_state)
+@@ -690,7 +663,6 @@ static const struct pse_controller_ops t
+ .setup_pi_matrix = tps23881_setup_pi_matrix,
+ .pi_enable = tps23881_pi_enable,
+ .pi_disable = tps23881_pi_disable,
+- .pi_is_enabled = tps23881_pi_is_enabled,
+ .pi_get_admin_state = tps23881_pi_get_admin_state,
+ .pi_get_pw_status = tps23881_pi_get_pw_status,
+ };
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -122,8 +122,6 @@ struct ethtool_pse_control_status {
+ * @pi_get_ext_state: Get the extended state of the PSE PI.
+ * @pi_get_pw_class: Get the power class of the PSE PI.
+ * @pi_get_actual_pw: Get actual power of the PSE PI in mW.
+- * @pi_is_enabled: Return 1 if the PSE PI is enabled, 0 if not.
+- * May also return negative errno.
+ * @pi_enable: Configure the PSE PI as enabled.
+ * @pi_disable: Configure the PSE PI as disabled.
+ * @pi_get_voltage: Return voltage similarly to get_voltage regulator
+@@ -145,7 +143,6 @@ struct pse_controller_ops {
+ struct pse_ext_state_info *ext_state_info);
+ int (*pi_get_pw_class)(struct pse_controller_dev *pcdev, int id);
+ int (*pi_get_actual_pw)(struct pse_controller_dev *pcdev, int id);
+- int (*pi_is_enabled)(struct pse_controller_dev *pcdev, int id);
+ int (*pi_enable)(struct pse_controller_dev *pcdev, int id);
+ int (*pi_disable)(struct pse_controller_dev *pcdev, int id);
+ int (*pi_get_voltage)(struct pse_controller_dev *pcdev, int id);
--- /dev/null
+From 7f076ce3f17334964590c2cce49a02c0851c099a Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:29 +0100
+Subject: [PATCH] net: pse-pd: tps23881: Add support for power limit and
+ measurement features
+
+Expand PSE callbacks to support the newly introduced
+pi_get/set_pw_limit() and pi_get_voltage() functions. These callbacks
+allow for power limit configuration in the TPS23881 controller.
+
+Additionally, the patch includes the pi_get_pw_class() the
+pi_get_actual_pw(), and the pi_get_pw_limit_ranges') callbacks providing
+more comprehensive PoE status reporting.
+
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/tps23881.c | 258 +++++++++++++++++++++++++++++++++-
+ 1 file changed, 256 insertions(+), 2 deletions(-)
+
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -25,20 +25,32 @@
+ #define TPS23881_REG_GEN_MASK 0x17
+ #define TPS23881_REG_NBITACC BIT(5)
+ #define TPS23881_REG_PW_EN 0x19
++#define TPS23881_REG_2PAIR_POL1 0x1e
+ #define TPS23881_REG_PORT_MAP 0x26
+ #define TPS23881_REG_PORT_POWER 0x29
+-#define TPS23881_REG_POEPLUS 0x40
++#define TPS23881_REG_4PAIR_POL1 0x2a
++#define TPS23881_REG_INPUT_V 0x2e
++#define TPS23881_REG_CHAN1_A 0x30
++#define TPS23881_REG_CHAN1_V 0x32
++#define TPS23881_REG_FOLDBACK 0x40
+ #define TPS23881_REG_TPON BIT(0)
+ #define TPS23881_REG_FWREV 0x41
+ #define TPS23881_REG_DEVID 0x43
+ #define TPS23881_REG_DEVID_MASK 0xF0
+ #define TPS23881_DEVICE_ID 0x02
++#define TPS23881_REG_CHAN1_CLASS 0x4c
+ #define TPS23881_REG_SRAM_CTRL 0x60
+ #define TPS23881_REG_SRAM_DATA 0x61
+
++#define TPS23881_UV_STEP 3662
++#define TPS23881_NA_STEP 70190
++#define TPS23881_MW_STEP 500
++#define TPS23881_MIN_PI_PW_LIMIT_MW 2000
++
+ struct tps23881_port_desc {
+ u8 chan[2];
+ bool is_4p;
++ int pw_pol;
+ };
+
+ struct tps23881_priv {
+@@ -102,6 +114,54 @@ static u16 tps23881_set_val(u16 reg_val,
+ return reg_val;
+ }
+
++static int
++tps23881_pi_set_pw_pol_limit(struct tps23881_priv *priv, int id, u8 pw_pol,
++ bool is_4p)
++{
++ struct i2c_client *client = priv->client;
++ int ret, reg;
++ u16 val;
++ u8 chan;
++
++ chan = priv->port[id].chan[0];
++ if (!is_4p) {
++ reg = TPS23881_REG_2PAIR_POL1 + (chan % 4);
++ } else {
++ /* One chan is enough to configure the 4p PI power limit */
++ if ((chan % 4) < 2)
++ reg = TPS23881_REG_4PAIR_POL1;
++ else
++ reg = TPS23881_REG_4PAIR_POL1 + 1;
++ }
++
++ ret = i2c_smbus_read_word_data(client, reg);
++ if (ret < 0)
++ return ret;
++
++ val = tps23881_set_val(ret, chan, 0, 0xff, pw_pol);
++ return i2c_smbus_write_word_data(client, reg, val);
++}
++
++static int tps23881_pi_enable_manual_pol(struct tps23881_priv *priv, int id)
++{
++ struct i2c_client *client = priv->client;
++ int ret;
++ u8 chan;
++ u16 val;
++
++ ret = i2c_smbus_read_byte_data(client, TPS23881_REG_FOLDBACK);
++ if (ret < 0)
++ return ret;
++
++ /* No need to test if the chan is PoE4 as setting either bit for a
++ * 4P configured port disables the automatic configuration on both
++ * channels.
++ */
++ chan = priv->port[id].chan[0];
++ val = tps23881_set_val(ret, chan, 0, BIT(chan % 4), BIT(chan % 4));
++ return i2c_smbus_write_byte_data(client, TPS23881_REG_FOLDBACK, val);
++}
++
+ static int tps23881_pi_enable(struct pse_controller_dev *pcdev, int id)
+ {
+ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
+@@ -171,7 +231,21 @@ static int tps23881_pi_disable(struct ps
+ BIT(chan % 4));
+ }
+
+- return i2c_smbus_write_word_data(client, TPS23881_REG_DET_CLA_EN, val);
++ ret = i2c_smbus_write_word_data(client, TPS23881_REG_DET_CLA_EN, val);
++ if (ret)
++ return ret;
++
++ /* No power policy */
++ if (priv->port[id].pw_pol < 0)
++ return 0;
++
++ ret = tps23881_pi_enable_manual_pol(priv, id);
++ if (ret < 0)
++ return ret;
++
++ /* Set power policy */
++ return tps23881_pi_set_pw_pol_limit(priv, id, priv->port[id].pw_pol,
++ priv->port[id].is_4p);
+ }
+
+ static int
+@@ -246,6 +320,177 @@ tps23881_pi_get_pw_status(struct pse_con
+ return 0;
+ }
+
++static int tps23881_pi_get_voltage(struct pse_controller_dev *pcdev, int id)
++{
++ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
++ struct i2c_client *client = priv->client;
++ int ret;
++ u64 uV;
++
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_INPUT_V);
++ if (ret < 0)
++ return ret;
++
++ uV = ret & 0x3fff;
++ uV *= TPS23881_UV_STEP;
++
++ return (int)uV;
++}
++
++static int
++tps23881_pi_get_chan_current(struct tps23881_priv *priv, u8 chan)
++{
++ struct i2c_client *client = priv->client;
++ int reg, ret;
++ u64 tmp_64;
++
++ /* Registers 0x30 to 0x3d */
++ reg = TPS23881_REG_CHAN1_A + (chan % 4) * 4 + (chan >= 4);
++ ret = i2c_smbus_read_word_data(client, reg);
++ if (ret < 0)
++ return ret;
++
++ tmp_64 = ret & 0x3fff;
++ tmp_64 *= TPS23881_NA_STEP;
++ /* uA = nA / 1000 */
++ tmp_64 = DIV_ROUND_CLOSEST_ULL(tmp_64, 1000);
++ return (int)tmp_64;
++}
++
++static int tps23881_pi_get_pw_class(struct pse_controller_dev *pcdev,
++ int id)
++{
++ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
++ struct i2c_client *client = priv->client;
++ int ret, reg;
++ u8 chan;
++
++ chan = priv->port[id].chan[0];
++ reg = TPS23881_REG_CHAN1_CLASS + (chan % 4);
++ ret = i2c_smbus_read_word_data(client, reg);
++ if (ret < 0)
++ return ret;
++
++ return tps23881_calc_val(ret, chan, 4, 0x0f);
++}
++
++static int
++tps23881_pi_get_actual_pw(struct pse_controller_dev *pcdev, int id)
++{
++ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
++ int ret, uV, uA;
++ u64 tmp_64;
++ u8 chan;
++
++ ret = tps23881_pi_get_voltage(&priv->pcdev, id);
++ if (ret < 0)
++ return ret;
++ uV = ret;
++
++ chan = priv->port[id].chan[0];
++ ret = tps23881_pi_get_chan_current(priv, chan);
++ if (ret < 0)
++ return ret;
++ uA = ret;
++
++ if (priv->port[id].is_4p) {
++ chan = priv->port[id].chan[1];
++ ret = tps23881_pi_get_chan_current(priv, chan);
++ if (ret < 0)
++ return ret;
++ uA += ret;
++ }
++
++ tmp_64 = uV;
++ tmp_64 *= uA;
++ /* mW = uV * uA / 1000000000 */
++ return DIV_ROUND_CLOSEST_ULL(tmp_64, 1000000000);
++}
++
++static int
++tps23881_pi_get_pw_limit_chan(struct tps23881_priv *priv, u8 chan)
++{
++ struct i2c_client *client = priv->client;
++ int ret, reg;
++ u16 val;
++
++ reg = TPS23881_REG_2PAIR_POL1 + (chan % 4);
++ ret = i2c_smbus_read_word_data(client, reg);
++ if (ret < 0)
++ return ret;
++
++ val = tps23881_calc_val(ret, chan, 0, 0xff);
++ return val * TPS23881_MW_STEP;
++}
++
++static int tps23881_pi_get_pw_limit(struct pse_controller_dev *pcdev, int id)
++{
++ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
++ int ret, mW;
++ u8 chan;
++
++ chan = priv->port[id].chan[0];
++ ret = tps23881_pi_get_pw_limit_chan(priv, chan);
++ if (ret < 0)
++ return ret;
++
++ mW = ret;
++ if (priv->port[id].is_4p) {
++ chan = priv->port[id].chan[1];
++ ret = tps23881_pi_get_pw_limit_chan(priv, chan);
++ if (ret < 0)
++ return ret;
++ mW += ret;
++ }
++
++ return mW;
++}
++
++static int tps23881_pi_set_pw_limit(struct pse_controller_dev *pcdev,
++ int id, int max_mW)
++{
++ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
++ u8 pw_pol;
++ int ret;
++
++ if (max_mW < TPS23881_MIN_PI_PW_LIMIT_MW || MAX_PI_PW < max_mW) {
++ dev_err(&priv->client->dev,
++ "power limit %d out of ranges [%d,%d]",
++ max_mW, TPS23881_MIN_PI_PW_LIMIT_MW, MAX_PI_PW);
++ return -ERANGE;
++ }
++
++ ret = tps23881_pi_enable_manual_pol(priv, id);
++ if (ret < 0)
++ return ret;
++
++ pw_pol = DIV_ROUND_CLOSEST_ULL(max_mW, TPS23881_MW_STEP);
++
++ /* Save power policy to reconfigure it after a disabled call */
++ priv->port[id].pw_pol = pw_pol;
++ return tps23881_pi_set_pw_pol_limit(priv, id, pw_pol,
++ priv->port[id].is_4p);
++}
++
++static int
++tps23881_pi_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
++ struct pse_pw_limit_ranges *pw_limit_ranges)
++{
++ struct ethtool_c33_pse_pw_limit_range *c33_pw_limit_ranges;
++
++ c33_pw_limit_ranges = kzalloc(sizeof(*c33_pw_limit_ranges),
++ GFP_KERNEL);
++ if (!c33_pw_limit_ranges)
++ return -ENOMEM;
++
++ c33_pw_limit_ranges->min = TPS23881_MIN_PI_PW_LIMIT_MW;
++ c33_pw_limit_ranges->max = MAX_PI_PW;
++ pw_limit_ranges->c33_pw_limit_ranges = c33_pw_limit_ranges;
++
++ /* Return the number of ranges */
++ return 1;
++}
++
+ /* Parse managers subnode into a array of device node */
+ static int
+ tps23881_get_of_channels(struct tps23881_priv *priv,
+@@ -540,6 +785,9 @@ tps23881_write_port_matrix(struct tps238
+ if (port_matrix[i].exist)
+ priv->port[pi_id].chan[0] = lgcl_chan;
+
++ /* Initialize power policy internal value */
++ priv->port[pi_id].pw_pol = -1;
++
+ /* Set hardware port matrix for all ports */
+ val |= hw_chan << (lgcl_chan * 2);
+
+@@ -665,6 +913,12 @@ static const struct pse_controller_ops t
+ .pi_disable = tps23881_pi_disable,
+ .pi_get_admin_state = tps23881_pi_get_admin_state,
+ .pi_get_pw_status = tps23881_pi_get_pw_status,
++ .pi_get_pw_class = tps23881_pi_get_pw_class,
++ .pi_get_actual_pw = tps23881_pi_get_actual_pw,
++ .pi_get_voltage = tps23881_pi_get_voltage,
++ .pi_get_pw_limit = tps23881_pi_get_pw_limit,
++ .pi_set_pw_limit = tps23881_pi_set_pw_limit,
++ .pi_get_pw_limit_ranges = tps23881_pi_get_pw_limit_ranges,
+ };
+
+ static const char fw_parity_name[] = "ti/tps23881/tps23881-parity-14.bin";
--- /dev/null
+From 10276f3e1c7e7f5de9f0bba58f8a849cb195253d Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:30 +0100
+Subject: [PATCH] net: pse-pd: Fix missing PI of_node description
+
+The PI of_node was not assigned in the regulator_config structure, leading
+to failures in resolving the correct supply when different power supplies
+are assigned to multiple PIs of a PSE controller. This fix ensures that the
+of_node is properly set in the regulator_config, allowing accurate supply
+resolution for each PI.
+
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pse_core.c | 1 +
+ 1 file changed, 1 insertion(+)
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -422,6 +422,7 @@ devm_pse_pi_regulator_register(struct ps
+ rconfig.dev = pcdev->dev;
+ rconfig.driver_data = pcdev;
+ rconfig.init_data = rinit_data;
++ rconfig.of_node = pcdev->pi[id].np;
+
+ rdev = devm_regulator_register(pcdev->dev, rdesc, &rconfig);
+ if (IS_ERR(rdev)) {
--- /dev/null
+From 5385f1e1923ca8131eb143567d509b101a344e06 Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Fri, 10 Jan 2025 10:40:31 +0100
+Subject: [PATCH] net: pse-pd: Clean ethtool header of PSE structures
+
+Remove PSE-specific structures from the ethtool header to improve code
+modularity, maintain independent headers, and reduce incremental build
+time.
+
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pse_core.c | 1 +
+ include/linux/ethtool.h | 20 --------------------
+ include/linux/pse-pd/pse.h | 22 +++++++++++++++++++++-
+ 3 files changed, 22 insertions(+), 21 deletions(-)
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -6,6 +6,7 @@
+ //
+
+ #include <linux/device.h>
++#include <linux/ethtool.h>
+ #include <linux/of.h>
+ #include <linux/pse-pd/pse.h>
+ #include <linux/regulator/driver.h>
+--- a/include/linux/ethtool.h
++++ b/include/linux/ethtool.h
+@@ -1322,24 +1322,4 @@ struct ethtool_forced_speed_map {
+
+ void
+ ethtool_forced_speed_maps_init(struct ethtool_forced_speed_map *maps, u32 size);
+-
+-/* C33 PSE extended state and substate. */
+-struct ethtool_c33_pse_ext_state_info {
+- enum ethtool_c33_pse_ext_state c33_pse_ext_state;
+- union {
+- enum ethtool_c33_pse_ext_substate_error_condition error_condition;
+- enum ethtool_c33_pse_ext_substate_mr_pse_enable mr_pse_enable;
+- enum ethtool_c33_pse_ext_substate_option_detect_ted option_detect_ted;
+- enum ethtool_c33_pse_ext_substate_option_vport_lim option_vport_lim;
+- enum ethtool_c33_pse_ext_substate_ovld_detected ovld_detected;
+- enum ethtool_c33_pse_ext_substate_power_not_available power_not_available;
+- enum ethtool_c33_pse_ext_substate_short_detected short_detected;
+- u32 __c33_pse_ext_substate;
+- };
+-};
+-
+-struct ethtool_c33_pse_pw_limit_range {
+- u32 min;
+- u32 max;
+-};
+ #endif /* _LINUX_ETHTOOL_H */
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -5,7 +5,6 @@
+ #ifndef _LINUX_PSE_CONTROLLER_H
+ #define _LINUX_PSE_CONTROLLER_H
+
+-#include <linux/ethtool.h>
+ #include <linux/list.h>
+ #include <uapi/linux/ethtool.h>
+
+@@ -16,6 +15,27 @@
+
+ struct phy_device;
+ struct pse_controller_dev;
++struct netlink_ext_ack;
++
++/* C33 PSE extended state and substate. */
++struct ethtool_c33_pse_ext_state_info {
++ enum ethtool_c33_pse_ext_state c33_pse_ext_state;
++ union {
++ enum ethtool_c33_pse_ext_substate_error_condition error_condition;
++ enum ethtool_c33_pse_ext_substate_mr_pse_enable mr_pse_enable;
++ enum ethtool_c33_pse_ext_substate_option_detect_ted option_detect_ted;
++ enum ethtool_c33_pse_ext_substate_option_vport_lim option_vport_lim;
++ enum ethtool_c33_pse_ext_substate_ovld_detected ovld_detected;
++ enum ethtool_c33_pse_ext_substate_power_not_available power_not_available;
++ enum ethtool_c33_pse_ext_substate_short_detected short_detected;
++ u32 __c33_pse_ext_substate;
++ };
++};
++
++struct ethtool_c33_pse_pw_limit_range {
++ u32 min;
++ u32 max;
++};
+
+ /**
+ * struct pse_control_config - PSE control/channel configuration.
--- /dev/null
+From fa2f0454174c2f33005f5a6e6f70c7160a15b2a1 Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:00 +0200
+Subject: [PATCH] net: pse-pd: Introduce attached_phydev to pse control
+
+In preparation for reporting PSE events via ethtool notifications,
+introduce an attached_phydev field in the pse_control structure.
+This field stores the phy_device associated with the PSE PI,
+ensuring that notifications are sent to the correct network
+interface.
+
+The attached_phydev pointer is directly tied to the PHY lifecycle. It
+is set when the PHY is registered and cleared when the PHY is removed.
+There is no need to use a refcount, as doing so could interfere with
+the PHY removal process.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Reviewed-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-1-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/mdio/fwnode_mdio.c | 26 ++++++++++++++------------
+ drivers/net/pse-pd/pse_core.c | 11 ++++++++---
+ include/linux/pse-pd/pse.h | 6 ++++--
+ 3 files changed, 26 insertions(+), 17 deletions(-)
+--- a/drivers/net/mdio/fwnode_mdio.c
++++ b/drivers/net/mdio/fwnode_mdio.c
+@@ -18,7 +18,8 @@ MODULE_LICENSE("GPL");
+ MODULE_DESCRIPTION("FWNODE MDIO bus (Ethernet PHY) accessors");
+
+ static struct pse_control *
+-fwnode_find_pse_control(struct fwnode_handle *fwnode)
++fwnode_find_pse_control(struct fwnode_handle *fwnode,
++ struct phy_device *phydev)
+ {
+ struct pse_control *psec;
+ struct device_node *np;
+@@ -30,7 +31,7 @@ fwnode_find_pse_control(struct fwnode_ha
+ if (!np)
+ return NULL;
+
+- psec = of_pse_control_get(np);
++ psec = of_pse_control_get(np, phydev);
+ if (PTR_ERR(psec) == -ENOENT)
+ return NULL;
+
+@@ -128,15 +129,9 @@ int fwnode_mdiobus_register_phy(struct m
+ u32 phy_id;
+ int rc;
+
+- psec = fwnode_find_pse_control(child);
+- if (IS_ERR(psec))
+- return PTR_ERR(psec);
+-
+ mii_ts = fwnode_find_mii_timestamper(child);
+- if (IS_ERR(mii_ts)) {
+- rc = PTR_ERR(mii_ts);
+- goto clean_pse;
+- }
++ if (IS_ERR(mii_ts))
++ return PTR_ERR(mii_ts);
+
+ is_c45 = fwnode_device_is_compatible(child, "ethernet-phy-ieee802.3-c45");
+ if (is_c45 || fwnode_get_phy_id(child, &phy_id))
+@@ -169,6 +164,12 @@ int fwnode_mdiobus_register_phy(struct m
+ goto clean_phy;
+ }
+
++ psec = fwnode_find_pse_control(child, phy);
++ if (IS_ERR(psec)) {
++ rc = PTR_ERR(psec);
++ goto unregister_phy;
++ }
++
+ phy->psec = psec;
+
+ /* phy->mii_ts may already be defined by the PHY driver. A
+@@ -180,12 +181,13 @@ int fwnode_mdiobus_register_phy(struct m
+
+ return 0;
+
++unregister_phy:
++ if (is_acpi_node(child) || is_of_node(child))
++ phy_device_remove(phy);
+ clean_phy:
+ phy_device_free(phy);
+ clean_mii_ts:
+ unregister_mii_timestamper(mii_ts);
+-clean_pse:
+- pse_control_put(psec);
+
+ return rc;
+ }
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -23,6 +23,7 @@ static LIST_HEAD(pse_controller_list);
+ * @list: list entry for the pcdev's PSE controller list
+ * @id: ID of the PSE line in the PSE controller device
+ * @refcnt: Number of gets of this pse_control
++ * @attached_phydev: PHY device pointer attached by the PSE control
+ */
+ struct pse_control {
+ struct pse_controller_dev *pcdev;
+@@ -30,6 +31,7 @@ struct pse_control {
+ struct list_head list;
+ unsigned int id;
+ struct kref refcnt;
++ struct phy_device *attached_phydev;
+ };
+
+ static int of_load_single_pse_pi_pairset(struct device_node *node,
+@@ -599,7 +601,8 @@ void pse_control_put(struct pse_control
+ EXPORT_SYMBOL_GPL(pse_control_put);
+
+ static struct pse_control *
+-pse_control_get_internal(struct pse_controller_dev *pcdev, unsigned int index)
++pse_control_get_internal(struct pse_controller_dev *pcdev, unsigned int index,
++ struct phy_device *phydev)
+ {
+ struct pse_control *psec;
+ int ret;
+@@ -638,6 +641,7 @@ pse_control_get_internal(struct pse_cont
+ psec->pcdev = pcdev;
+ list_add(&psec->list, &pcdev->pse_control_head);
+ psec->id = index;
++ psec->attached_phydev = phydev;
+ kref_init(&psec->refcnt);
+
+ return psec;
+@@ -693,7 +697,8 @@ static int psec_id_xlate(struct pse_cont
+ return pse_spec->args[0];
+ }
+
+-struct pse_control *of_pse_control_get(struct device_node *node)
++struct pse_control *of_pse_control_get(struct device_node *node,
++ struct phy_device *phydev)
+ {
+ struct pse_controller_dev *r, *pcdev;
+ struct of_phandle_args args;
+@@ -743,7 +748,7 @@ struct pse_control *of_pse_control_get(s
+ }
+
+ /* pse_list_mutex also protects the pcdev's pse_control list */
+- psec = pse_control_get_internal(pcdev, psec_id);
++ psec = pse_control_get_internal(pcdev, psec_id, phydev);
+
+ out:
+ mutex_unlock(&pse_list_mutex);
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -250,7 +250,8 @@ struct device;
+ int devm_pse_controller_register(struct device *dev,
+ struct pse_controller_dev *pcdev);
+
+-struct pse_control *of_pse_control_get(struct device_node *node);
++struct pse_control *of_pse_control_get(struct device_node *node,
++ struct phy_device *phydev);
+ void pse_control_put(struct pse_control *psec);
+
+ int pse_ethtool_get_status(struct pse_control *psec,
+@@ -270,7 +271,8 @@ bool pse_has_c33(struct pse_control *pse
+
+ #else
+
+-static inline struct pse_control *of_pse_control_get(struct device_node *node)
++static inline struct pse_control *of_pse_control_get(struct device_node *node,
++ struct phy_device *phydev)
+ {
+ return ERR_PTR(-ENOENT);
+ }
--- /dev/null
+# ADAPTED FOR OPENWRT 6.12.67 - Documentation changes removed
+# Original commit: fc0e6db30941
+From fc0e6db30941a66e284b8516b82356f97f31061d Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:01 +0200
+Subject: [PATCH] net: pse-pd: Add support for reporting events
+
+Add support for devm_pse_irq_helper() to register PSE interrupts and report
+events such as over-current or over-temperature conditions. This follows a
+similar approach to the regulator API but also sends notifications using a
+dedicated PSE ethtool netlink socket.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-2-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ Documentation/netlink/specs/ethtool.yaml | 34 ++++
+ Documentation/networking/ethtool-netlink.rst | 19 ++
+ drivers/net/pse-pd/pse_core.c | 179 ++++++++++++++++++
+ include/linux/ethtool_netlink.h | 7 +
+ include/linux/pse-pd/pse.h | 20 ++
+ .../uapi/linux/ethtool_netlink_generated.h | 19 ++
+ net/ethtool/pse-pd.c | 39 ++++
+ 7 files changed, 317 insertions(+)
+
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -7,10 +7,14 @@
+
+ #include <linux/device.h>
+ #include <linux/ethtool.h>
++#include <linux/ethtool_netlink.h>
+ #include <linux/of.h>
++#include <linux/phy.h>
+ #include <linux/pse-pd/pse.h>
+ #include <linux/regulator/driver.h>
+ #include <linux/regulator/machine.h>
++#include <linux/rtnetlink.h>
++#include <net/net_trackers.h>
+
+ static DEFINE_MUTEX(pse_list_mutex);
+ static LIST_HEAD(pse_controller_list);
+@@ -210,6 +214,48 @@ out:
+ return ret;
+ }
+
++/**
++ * pse_control_find_net_by_id - Find net attached to the pse control id
++ * @pcdev: a pointer to the PSE
++ * @id: index of the PSE control
++ *
++ * Return: pse_control pointer or NULL. The device returned has had a
++ * reference added and the pointer is safe until the user calls
++ * pse_control_put() to indicate they have finished with it.
++ */
++static struct pse_control *
++pse_control_find_by_id(struct pse_controller_dev *pcdev, int id)
++{
++ struct pse_control *psec;
++
++ mutex_lock(&pse_list_mutex);
++ list_for_each_entry(psec, &pcdev->pse_control_head, list) {
++ if (psec->id == id) {
++ kref_get(&psec->refcnt);
++ mutex_unlock(&pse_list_mutex);
++ return psec;
++ }
++ }
++ mutex_unlock(&pse_list_mutex);
++ return NULL;
++}
++
++/**
++ * pse_control_get_netdev - Return netdev associated to a PSE control
++ * @psec: PSE control pointer
++ *
++ * Return: netdev pointer or NULL
++ */
++static struct net_device *pse_control_get_netdev(struct pse_control *psec)
++{
++ ASSERT_RTNL();
++
++ if (!psec || !psec->attached_phydev)
++ return NULL;
++
++ return psec->attached_phydev->attached_dev;
++}
++
+ static int pse_pi_is_enabled(struct regulator_dev *rdev)
+ {
+ struct pse_controller_dev *pcdev = rdev_get_drvdata(rdev);
+@@ -559,6 +605,139 @@ int devm_pse_controller_register(struct
+ }
+ EXPORT_SYMBOL_GPL(devm_pse_controller_register);
+
++struct pse_irq {
++ struct pse_controller_dev *pcdev;
++ struct pse_irq_desc desc;
++ unsigned long *notifs;
++};
++
++/**
++ * pse_to_regulator_notifs - Convert PSE notifications to Regulator
++ * notifications
++ * @notifs: PSE notifications
++ *
++ * Return: Regulator notifications
++ */
++static unsigned long pse_to_regulator_notifs(unsigned long notifs)
++{
++ unsigned long rnotifs = 0;
++
++ if (notifs & ETHTOOL_PSE_EVENT_OVER_CURRENT)
++ rnotifs |= REGULATOR_EVENT_OVER_CURRENT;
++ if (notifs & ETHTOOL_PSE_EVENT_OVER_TEMP)
++ rnotifs |= REGULATOR_EVENT_OVER_TEMP;
++
++ return rnotifs;
++}
++
++/**
++ * 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;
++ unsigned long notifs_mask = 0;
++ 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));
++ mutex_lock(&pcdev->lock);
++ ret = desc->map_event(irq, pcdev, h->notifs, ¬ifs_mask);
++ mutex_unlock(&pcdev->lock);
++ if (ret || !notifs_mask)
++ return IRQ_NONE;
++
++ for_each_set_bit(i, ¬ifs_mask, pcdev->nr_lines) {
++ unsigned long notifs, rnotifs;
++ struct net_device *netdev;
++ struct pse_control *psec;
++
++ /* Do nothing PI not described */
++ if (!pcdev->pi[i].rdev)
++ continue;
++
++ notifs = h->notifs[i];
++ dev_dbg(h->pcdev->dev,
++ "Sending PSE notification EVT 0x%lx\n", notifs);
++
++ psec = pse_control_find_by_id(pcdev, i);
++ rtnl_lock();
++ netdev = pse_control_get_netdev(psec);
++ if (netdev)
++ ethnl_pse_send_ntf(netdev, notifs);
++ rtnl_unlock();
++ pse_control_put(psec);
++
++ rnotifs = pse_to_regulator_notifs(notifs);
++ regulator_notifier_call_chain(pcdev->pi[i].rdev, rnotifs,
++ NULL);
++ }
++
++ return IRQ_HANDLED;
++}
++
++/**
++ * devm_pse_irq_helper - Register IRQ based PSE event notifier
++ * @pcdev: a pointer to the PSE
++ * @irq: the irq value to be passed to request_irq
++ * @irq_flags: the flags to be passed to request_irq
++ * @d: PSE interrupt description
++ *
++ * Return: 0 on success and errno on failure
++ */
++int devm_pse_irq_helper(struct pse_controller_dev *pcdev, int irq,
++ int irq_flags, const struct pse_irq_desc *d)
++{
++ struct device *dev = pcdev->dev;
++ size_t irq_name_len;
++ struct pse_irq *h;
++ char *irq_name;
++ int ret;
++
++ if (!d || !d->map_event || !d->name)
++ return -EINVAL;
++
++ h = devm_kzalloc(dev, sizeof(*h), GFP_KERNEL);
++ if (!h)
++ return -ENOMEM;
++
++ h->pcdev = pcdev;
++ h->desc = *d;
++
++ /* IRQ name len is pcdev dev name + 5 char + irq desc name + 1 */
++ irq_name_len = strlen(dev_name(pcdev->dev)) + 5 + strlen(d->name) + 1;
++ irq_name = devm_kzalloc(dev, irq_name_len, GFP_KERNEL);
++ if (!irq_name)
++ return -ENOMEM;
++
++ snprintf(irq_name, irq_name_len, "pse-%s:%s", dev_name(pcdev->dev),
++ d->name);
++
++ h->notifs = devm_kcalloc(dev, pcdev->nr_lines,
++ sizeof(*h->notifs), GFP_KERNEL);
++ if (!h->notifs)
++ return -ENOMEM;
++
++ ret = devm_request_threaded_irq(dev, irq, NULL, pse_isr,
++ IRQF_ONESHOT | irq_flags,
++ irq_name, h);
++ if (ret)
++ dev_err(pcdev->dev, "Failed to request IRQ %d\n", irq);
++
++ pcdev->irq = irq;
++ return ret;
++}
++EXPORT_SYMBOL_GPL(devm_pse_irq_helper);
++
+ /* PSE control section */
+
+ static void __pse_control_release(struct kref *kref)
+--- a/include/linux/ethtool_netlink.h
++++ b/include/linux/ethtool_netlink.h
+@@ -43,6 +43,8 @@ void ethtool_aggregate_rmon_stats(struct
+ struct ethtool_rmon_stats *rmon_stats);
+ bool ethtool_dev_mm_supported(struct net_device *dev);
+
++void ethnl_pse_send_ntf(struct net_device *netdev, unsigned long notif);
++
+ #else
+ static inline int ethnl_cable_test_alloc(struct phy_device *phydev, u8 cmd)
+ {
+@@ -120,6 +122,11 @@ static inline bool ethtool_dev_mm_suppor
+ return false;
+ }
+
++static inline void ethnl_pse_send_ntf(struct phy_device *phydev,
++ unsigned long notif)
++{
++}
++
+ #endif /* IS_ENABLED(CONFIG_ETHTOOL_NETLINK) */
+
+ static inline int ethnl_cable_test_result(struct phy_device *phydev, u8 pair,
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -7,12 +7,15 @@
+
+ #include <linux/list.h>
+ #include <uapi/linux/ethtool.h>
++#include <uapi/linux/ethtool_netlink_generated.h>
++#include <linux/regulator/driver.h>
+
+ /* Maximum current in uA according to IEEE 802.3-2022 Table 145-1 */
+ #define MAX_PI_CURRENT 1920000
+ /* Maximum power in mW according to IEEE 802.3-2022 Table 145-16 */
+ #define MAX_PI_PW 99900
+
++struct net_device;
+ struct phy_device;
+ struct pse_controller_dev;
+ struct netlink_ext_ack;
+@@ -38,6 +41,19 @@ struct ethtool_c33_pse_pw_limit_range {
+ };
+
+ /**
++ * struct pse_irq_desc - notification sender description for IRQ based events.
++ *
++ * @name: the visible name for the IRQ
++ * @map_event: driver callback to map IRQ status into PSE devices with events.
++ */
++struct pse_irq_desc {
++ const char *name;
++ int (*map_event)(int irq, struct pse_controller_dev *pcdev,
++ unsigned long *notifs,
++ unsigned long *notifs_mask);
++};
++
++/**
+ * struct pse_control_config - PSE control/channel configuration.
+ *
+ * @podl_admin_control: set PoDL PSE admin control as described in
+@@ -228,6 +244,7 @@ struct pse_pi {
+ * @types: types of the PSE controller
+ * @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
+ */
+ struct pse_controller_dev {
+ const struct pse_controller_ops *ops;
+@@ -241,6 +258,7 @@ struct pse_controller_dev {
+ enum ethtool_pse_types types;
+ struct pse_pi *pi;
+ bool no_of_pse_pi;
++ int irq;
+ };
+
+ #if IS_ENABLED(CONFIG_PSE_CONTROLLER)
+@@ -249,6 +267,8 @@ void pse_controller_unregister(struct ps
+ struct device;
+ int devm_pse_controller_register(struct device *dev,
+ 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);
+
+ struct pse_control *of_pse_control_get(struct device_node *node,
+ struct phy_device *phydev);
+--- a/net/ethtool/pse-pd.c
++++ b/net/ethtool/pse-pd.c
+@@ -315,3 +315,42 @@ const struct ethnl_request_ops ethnl_pse
+ .set = ethnl_set_pse,
+ /* PSE has no notification */
+ };
++
++void ethnl_pse_send_ntf(struct net_device *netdev, unsigned long notifs)
++{
++ void *reply_payload;
++ struct sk_buff *skb;
++ int reply_len;
++ int ret;
++
++ ASSERT_RTNL();
++
++ if (!netdev || !notifs)
++ return;
++
++ reply_len = ethnl_reply_header_size() +
++ nla_total_size(sizeof(u32)); /* _PSE_NTF_EVENTS */
++
++ skb = genlmsg_new(reply_len, GFP_KERNEL);
++ if (!skb)
++ return;
++
++ reply_payload = ethnl_bcastmsg_put(skb, ETHTOOL_MSG_PSE_NTF);
++ if (!reply_payload)
++ goto err_skb;
++
++ ret = ethnl_fill_reply_header(skb, netdev, ETHTOOL_A_PSE_NTF_HEADER);
++ if (ret < 0)
++ goto err_skb;
++
++ if (nla_put_uint(skb, ETHTOOL_A_PSE_NTF_EVENTS, notifs))
++ goto err_skb;
++
++ genlmsg_end(skb, reply_payload);
++ ethnl_multicast(skb, netdev);
++ return;
++
++err_skb:
++ nlmsg_free(skb);
++}
++EXPORT_SYMBOL_GPL(ethnl_pse_send_ntf);
--- /dev/null
+From f5e7aecaa4efcd4c85477b6a62f94fea668031db Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:02 +0200
+Subject: [PATCH] net: pse-pd: tps23881: Add support for PSE events and
+ interrupts
+
+Add support for PSE event reporting through interrupts. Set up the newly
+introduced devm_pse_irq_helper helper to register the interrupt. Events are
+reported for over-current and over-temperature conditions.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Reviewed-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-3-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/tps23881.c | 189 +++++++++++++++++++++++++++++++++-
+ 1 file changed, 187 insertions(+), 2 deletions(-)
+
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -16,7 +16,15 @@
+ #include <linux/pse-pd/pse.h>
+
+ #define TPS23881_MAX_CHANS 8
++#define TPS23881_MAX_IRQ_RETRIES 10
+
++#define TPS23881_REG_IT 0x0
++#define TPS23881_REG_IT_MASK 0x1
++#define TPS23881_REG_IT_IFAULT BIT(5)
++#define TPS23881_REG_IT_SUPF BIT(7)
++#define TPS23881_REG_FAULT 0x7
++#define TPS23881_REG_SUPF_EVENT 0xb
++#define TPS23881_REG_TSD BIT(7)
+ #define TPS23881_REG_PW_STATUS 0x10
+ #define TPS23881_REG_OP_MODE 0x12
+ #define TPS23881_OP_MODE_SEMIAUTO 0xaaaa
+@@ -24,6 +32,7 @@
+ #define TPS23881_REG_DET_CLA_EN 0x14
+ #define TPS23881_REG_GEN_MASK 0x17
+ #define TPS23881_REG_NBITACC BIT(5)
++#define TPS23881_REG_INTEN BIT(7)
+ #define TPS23881_REG_PW_EN 0x19
+ #define TPS23881_REG_2PAIR_POL1 0x1e
+ #define TPS23881_REG_PORT_MAP 0x26
+@@ -51,6 +60,7 @@ struct tps23881_port_desc {
+ u8 chan[2];
+ bool is_4p;
+ int pw_pol;
++ bool exist;
+ };
+
+ struct tps23881_priv {
+@@ -782,8 +792,10 @@ tps23881_write_port_matrix(struct tps238
+ hw_chan = port_matrix[i].hw_chan[0] % 4;
+
+ /* Set software port matrix for existing ports */
+- if (port_matrix[i].exist)
++ if (port_matrix[i].exist) {
+ priv->port[pi_id].chan[0] = lgcl_chan;
++ priv->port[pi_id].exist = true;
++ }
+
+ /* Initialize power policy internal value */
+ priv->port[pi_id].pw_pol = -1;
+@@ -1017,6 +1029,173 @@ static int tps23881_flash_sram_fw(struct
+ return 0;
+ }
+
++/* Convert interrupt events to 0xff to be aligned with the chan
++ * number.
++ */
++static u8 tps23881_irq_export_chans_helper(u16 reg_val, u8 field_offset)
++{
++ u8 val;
++
++ val = (reg_val >> (4 + field_offset) & 0xf0) |
++ (reg_val >> field_offset & 0x0f);
++
++ return val;
++}
++
++/* Convert chan number to port number */
++static void tps23881_set_notifs_helper(struct tps23881_priv *priv,
++ u8 chans,
++ unsigned long *notifs,
++ unsigned long *notifs_mask,
++ enum ethtool_pse_event event)
++{
++ u8 chan;
++ int i;
++
++ if (!chans)
++ return;
++
++ for (i = 0; i < TPS23881_MAX_CHANS; i++) {
++ if (!priv->port[i].exist)
++ continue;
++ /* No need to look at the 2nd channel in case of PoE4 as
++ * both registers are set.
++ */
++ chan = priv->port[i].chan[0];
++
++ if (BIT(chan) & chans) {
++ *notifs_mask |= BIT(i);
++ notifs[i] |= event;
++ }
++ }
++}
++
++static void tps23881_irq_event_over_temp(struct tps23881_priv *priv,
++ u16 reg_val,
++ unsigned long *notifs,
++ unsigned long *notifs_mask)
++{
++ int i;
++
++ if (reg_val & TPS23881_REG_TSD) {
++ for (i = 0; i < TPS23881_MAX_CHANS; i++) {
++ if (!priv->port[i].exist)
++ continue;
++
++ *notifs_mask |= BIT(i);
++ notifs[i] |= ETHTOOL_PSE_EVENT_OVER_TEMP;
++ }
++ }
++}
++
++static void tps23881_irq_event_over_current(struct tps23881_priv *priv,
++ u16 reg_val,
++ unsigned long *notifs,
++ unsigned long *notifs_mask)
++{
++ u8 chans;
++
++ chans = tps23881_irq_export_chans_helper(reg_val, 0);
++ if (chans)
++ tps23881_set_notifs_helper(priv, chans, notifs, notifs_mask,
++ ETHTOOL_PSE_EVENT_OVER_CURRENT);
++}
++
++static int tps23881_irq_event_handler(struct tps23881_priv *priv, u16 reg,
++ unsigned long *notifs,
++ unsigned long *notifs_mask)
++{
++ struct i2c_client *client = priv->client;
++ int ret;
++
++ /* The Supply event bit is repeated twice so we only need to read
++ * the one from the first byte.
++ */
++ if (reg & TPS23881_REG_IT_SUPF) {
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_SUPF_EVENT);
++ if (ret < 0)
++ return ret;
++ tps23881_irq_event_over_temp(priv, ret, notifs, notifs_mask);
++ }
++
++ if (reg & (TPS23881_REG_IT_IFAULT | TPS23881_REG_IT_IFAULT << 8)) {
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_FAULT);
++ if (ret < 0)
++ return ret;
++ tps23881_irq_event_over_current(priv, ret, notifs, notifs_mask);
++ }
++
++ return 0;
++}
++
++static int tps23881_irq_handler(int irq, struct pse_controller_dev *pcdev,
++ unsigned long *notifs,
++ unsigned long *notifs_mask)
++{
++ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
++ struct i2c_client *client = priv->client;
++ int ret, it_mask, retry;
++
++ /* Get interruption mask */
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_IT_MASK);
++ if (ret < 0)
++ return ret;
++ it_mask = ret;
++
++ /* Read interrupt register until it frees the interruption pin. */
++ retry = 0;
++ while (true) {
++ if (retry > TPS23881_MAX_IRQ_RETRIES) {
++ dev_err(&client->dev, "interrupt never freed");
++ return -ETIMEDOUT;
++ }
++
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_IT);
++ if (ret < 0)
++ return ret;
++
++ /* No more relevant interruption */
++ if (!(ret & it_mask))
++ return 0;
++
++ ret = tps23881_irq_event_handler(priv, (u16)ret, notifs,
++ notifs_mask);
++ if (ret)
++ return ret;
++
++ retry++;
++ }
++ return 0;
++}
++
++static int tps23881_setup_irq(struct tps23881_priv *priv, int irq)
++{
++ struct i2c_client *client = priv->client;
++ struct pse_irq_desc irq_desc = {
++ .name = "tps23881-irq",
++ .map_event = tps23881_irq_handler,
++ };
++ int ret;
++ u16 val;
++
++ val = TPS23881_REG_IT_IFAULT | TPS23881_REG_IT_SUPF;
++ val |= val << 8;
++ ret = i2c_smbus_write_word_data(client, TPS23881_REG_IT_MASK, val);
++ if (ret)
++ return ret;
++
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_GEN_MASK);
++ if (ret < 0)
++ return ret;
++
++ val = (u16)(ret | TPS23881_REG_INTEN | TPS23881_REG_INTEN << 8);
++ ret = i2c_smbus_write_word_data(client, TPS23881_REG_GEN_MASK, val);
++ if (ret < 0)
++ return ret;
++
++ return devm_pse_irq_helper(&priv->pcdev, irq, 0, &irq_desc);
++}
++
+ static int tps23881_i2c_probe(struct i2c_client *client)
+ {
+ struct device *dev = &client->dev;
+@@ -1097,6 +1276,12 @@ static int tps23881_i2c_probe(struct i2c
+ "failed to register PSE controller\n");
+ }
+
++ if (client->irq) {
++ ret = tps23881_setup_irq(priv, client->irq);
++ if (ret)
++ return ret;
++ }
++
+ return ret;
+ }
+
--- /dev/null
+From 50f8b341d26826aa5fdccb8f497fbff2500934b3 Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:03 +0200
+Subject: [PATCH] net: pse-pd: Add support for PSE power domains
+
+Introduce PSE power domain support as groundwork for upcoming port
+priority features. Multiple PSE PIs can now be grouped under a single
+PSE power domain, enabling future enhancements like defining available
+power budgets, port priority modes, and disconnection policies. This
+setup will allow the system to assess whether activating a port would
+exceed the available power budget, preventing over-budget states
+proactively.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Reviewed-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-4-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pse_core.c | 140 ++++++++++++++++++++++++++++++++++
+ include/linux/pse-pd/pse.h | 2 +
+ 2 files changed, 142 insertions(+)
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -16,8 +16,12 @@
+ #include <linux/rtnetlink.h>
+ #include <net/net_trackers.h>
+
++#define PSE_PW_D_LIMIT INT_MAX
++
+ static DEFINE_MUTEX(pse_list_mutex);
+ static LIST_HEAD(pse_controller_list);
++static DEFINE_XARRAY_ALLOC(pse_pw_d_map);
++static DEFINE_MUTEX(pse_pw_d_mutex);
+
+ /**
+ * struct pse_control - a PSE control
+@@ -38,6 +42,18 @@ struct pse_control {
+ struct phy_device *attached_phydev;
+ };
+
++/**
++ * struct pse_power_domain - a PSE power domain
++ * @id: ID of the power domain
++ * @supply: Power supply the Power Domain
++ * @refcnt: Number of gets of this pse_power_domain
++ */
++struct pse_power_domain {
++ int id;
++ struct regulator *supply;
++ struct kref refcnt;
++};
++
+ static int of_load_single_pse_pi_pairset(struct device_node *node,
+ struct pse_pi *pi,
+ int pairset_num)
+@@ -485,6 +501,125 @@ devm_pse_pi_regulator_register(struct ps
+ return 0;
+ }
+
++static void __pse_pw_d_release(struct kref *kref)
++{
++ struct pse_power_domain *pw_d = container_of(kref,
++ struct pse_power_domain,
++ refcnt);
++
++ regulator_put(pw_d->supply);
++ xa_erase(&pse_pw_d_map, pw_d->id);
++ mutex_unlock(&pse_pw_d_mutex);
++}
++
++/**
++ * pse_flush_pw_ds - flush all PSE power domains of a PSE
++ * @pcdev: a pointer to the initialized PSE controller device
++ */
++static void pse_flush_pw_ds(struct pse_controller_dev *pcdev)
++{
++ struct pse_power_domain *pw_d;
++ int i;
++
++ for (i = 0; i < pcdev->nr_lines; i++) {
++ if (!pcdev->pi[i].pw_d)
++ continue;
++
++ pw_d = xa_load(&pse_pw_d_map, pcdev->pi[i].pw_d->id);
++ if (!pw_d)
++ continue;
++
++ kref_put_mutex(&pw_d->refcnt, __pse_pw_d_release,
++ &pse_pw_d_mutex);
++ }
++}
++
++/**
++ * devm_pse_alloc_pw_d - allocate a new PSE power domain for a device
++ * @dev: device that is registering this PSE power domain
++ *
++ * Return: Pointer to the newly allocated PSE power domain or error pointers
++ */
++static struct pse_power_domain *devm_pse_alloc_pw_d(struct device *dev)
++{
++ struct pse_power_domain *pw_d;
++ int index, ret;
++
++ pw_d = devm_kzalloc(dev, sizeof(*pw_d), GFP_KERNEL);
++ if (!pw_d)
++ return ERR_PTR(-ENOMEM);
++
++ ret = xa_alloc(&pse_pw_d_map, &index, pw_d, XA_LIMIT(1, PSE_PW_D_LIMIT),
++ GFP_KERNEL);
++ if (ret)
++ return ERR_PTR(ret);
++
++ kref_init(&pw_d->refcnt);
++ pw_d->id = index;
++ return pw_d;
++}
++
++/**
++ * pse_register_pw_ds - register the PSE power domains for a PSE
++ * @pcdev: a pointer to the PSE controller device
++ *
++ * Return: 0 on success and failure value on error
++ */
++static int pse_register_pw_ds(struct pse_controller_dev *pcdev)
++{
++ int i, ret = 0;
++
++ mutex_lock(&pse_pw_d_mutex);
++ for (i = 0; i < pcdev->nr_lines; i++) {
++ struct regulator_dev *rdev = pcdev->pi[i].rdev;
++ struct pse_power_domain *pw_d;
++ struct regulator *supply;
++ bool present = false;
++ unsigned long index;
++
++ /* No regulator or regulator parent supply registered.
++ * We need a regulator parent to register a PSE power domain
++ */
++ if (!rdev || !rdev->supply)
++ continue;
++
++ xa_for_each(&pse_pw_d_map, index, pw_d) {
++ /* Power supply already registered as a PSE power
++ * domain.
++ */
++ if (regulator_is_equal(pw_d->supply, rdev->supply)) {
++ present = true;
++ pcdev->pi[i].pw_d = pw_d;
++ break;
++ }
++ }
++ if (present) {
++ kref_get(&pw_d->refcnt);
++ continue;
++ }
++
++ pw_d = devm_pse_alloc_pw_d(pcdev->dev);
++ if (IS_ERR(pw_d)) {
++ ret = PTR_ERR(pw_d);
++ goto out;
++ }
++
++ supply = regulator_get(&rdev->dev, rdev->supply_name);
++ if (IS_ERR(supply)) {
++ xa_erase(&pse_pw_d_map, pw_d->id);
++ ret = PTR_ERR(supply);
++ goto out;
++ }
++
++ pw_d->supply = supply;
++ pcdev->pi[i].pw_d = pw_d;
++ }
++
++out:
++ mutex_unlock(&pse_pw_d_mutex);
++ return ret;
++}
++
+ /**
+ * pse_controller_register - register a PSE controller device
+ * @pcdev: a pointer to the initialized PSE controller device
+@@ -544,6 +679,10 @@ int pse_controller_register(struct pse_c
+ return ret;
+ }
+
++ ret = pse_register_pw_ds(pcdev);
++ if (ret)
++ return ret;
++
+ mutex_lock(&pse_list_mutex);
+ list_add(&pcdev->list, &pse_controller_list);
+ mutex_unlock(&pse_list_mutex);
+@@ -558,6 +697,7 @@ EXPORT_SYMBOL_GPL(pse_controller_registe
+ */
+ void pse_controller_unregister(struct pse_controller_dev *pcdev)
+ {
++ pse_flush_pw_ds(pcdev);
+ pse_release_pis(pcdev);
+ mutex_lock(&pse_list_mutex);
+ list_del(&pcdev->list);
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -222,12 +222,14 @@ struct pse_pi_pairset {
+ * @np: device node pointer of the PSE PI node
+ * @rdev: regulator represented by the PSE PI
+ * @admin_state_enabled: PI enabled state
++ * @pw_d: Power domain of the PSE PI
+ */
+ struct pse_pi {
+ struct pse_pi_pairset pairset[2];
+ struct device_node *np;
+ struct regulator_dev *rdev;
+ bool admin_state_enabled;
++ struct pse_power_domain *pw_d;
+ };
+
+ /**
--- /dev/null
+# ADAPTED FOR OPENWRT 6.12.67 - Documentation changes removed
+# Original commit: 1176978ed851
+From 1176978ed851952652ddea3685e2f71a0e5d61ff Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:04 +0200
+Subject: [PATCH] net: ethtool: Add support for new power domains index
+ description
+
+Report the index of the newly introduced PSE power domain to the user,
+enabling improved management of the power budget for PSE devices.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Reviewed-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-5-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ Documentation/netlink/specs/ethtool.yaml | 5 +++++
+ Documentation/networking/ethtool-netlink.rst | 4 ++++
+ drivers/net/pse-pd/pse_core.c | 3 +++
+ include/linux/pse-pd/pse.h | 2 ++
+ include/uapi/linux/ethtool_netlink_generated.h | 1 +
+ net/ethtool/pse-pd.c | 7 +++++++
+ 6 files changed, 22 insertions(+)
+
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -1098,6 +1098,9 @@ int pse_ethtool_get_status(struct pse_co
+ pcdev = psec->pcdev;
+ ops = pcdev->ops;
+ mutex_lock(&pcdev->lock);
++ if (pcdev->pi[psec->id].pw_d)
++ status->pw_d_id = pcdev->pi[psec->id].pw_d->id;
++
+ ret = ops->pi_get_admin_state(pcdev, psec->id, &admin_state);
+ if (ret)
+ goto out;
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -114,6 +114,7 @@ struct pse_pw_limit_ranges {
+ /**
+ * struct ethtool_pse_control_status - PSE control/channel status.
+ *
++ * @pw_d_id: PSE power domain index.
+ * @podl_admin_state: operational state of the PoDL PSE
+ * functions. IEEE 802.3-2018 30.15.1.1.2 aPoDLPSEAdminState
+ * @podl_pw_status: power detection status of the PoDL PSE.
+@@ -135,6 +136,7 @@ struct pse_pw_limit_ranges {
+ * ranges
+ */
+ struct ethtool_pse_control_status {
++ u32 pw_d_id;
+ enum ethtool_podl_pse_admin_state podl_admin_state;
+ enum ethtool_podl_pse_pw_d_status podl_pw_status;
+ enum ethtool_c33_pse_admin_state c33_admin_state;
+--- a/net/ethtool/pse-pd.c
++++ b/net/ethtool/pse-pd.c
+@@ -83,6 +83,8 @@ static int pse_reply_size(const struct e
+ const struct ethtool_pse_control_status *st = &data->status;
+ int len = 0;
+
++ if (st->pw_d_id)
++ len += nla_total_size(sizeof(u32)); /* _PSE_PW_D_ID */
+ if (st->podl_admin_state > 0)
+ len += nla_total_size(sizeof(u32)); /* _PODL_PSE_ADMIN_STATE */
+ if (st->podl_pw_status > 0)
+@@ -148,6 +150,11 @@ static int pse_fill_reply(struct sk_buff
+ const struct pse_reply_data *data = PSE_REPDATA(reply_base);
+ const struct ethtool_pse_control_status *st = &data->status;
+
++ if (st->pw_d_id &&
++ nla_put_u32(skb, ETHTOOL_A_PSE_PW_D_ID,
++ st->pw_d_id))
++ return -EMSGSIZE;
++
+ if (st->podl_admin_state > 0 &&
+ nla_put_u32(skb, ETHTOOL_A_PODL_PSE_ADMIN_STATE,
+ st->podl_admin_state))
--- /dev/null
+From c394e757dedd9cf947f9ac470d615d28fd2b07d1 Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:05 +0200
+Subject: [PATCH] net: pse-pd: Add helper to report hardware enable status of
+ the PI
+
+Refactor code by introducing a helper function to retrieve the hardware
+enabled state of the PI, avoiding redundant implementations in the
+future.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Reviewed-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-6-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pse_core.c | 36 +++++++++++++++++++++++++----------
+ 1 file changed, 26 insertions(+), 10 deletions(-)
+
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -272,10 +272,34 @@ static struct net_device *pse_control_ge
+ return psec->attached_phydev->attached_dev;
+ }
+
++/**
++ * pse_pi_is_hw_enabled - Is PI enabled at the hardware level
++ * @pcdev: a pointer to the PSE controller device
++ * @id: Index of the PI
++ *
++ * Return: 1 if the PI is enabled at the hardware level, 0 if not, and
++ * a failure value on error
++ */
++static int pse_pi_is_hw_enabled(struct pse_controller_dev *pcdev, int id)
++{
++ struct pse_admin_state admin_state = {0};
++ int ret;
++
++ ret = pcdev->ops->pi_get_admin_state(pcdev, id, &admin_state);
++ if (ret < 0)
++ return ret;
++
++ /* PI is well enabled at the hardware level */
++ if (admin_state.podl_admin_state == ETHTOOL_PODL_PSE_ADMIN_STATE_ENABLED ||
++ admin_state.c33_admin_state == ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED)
++ return 1;
++
++ return 0;
++}
++
+ static int pse_pi_is_enabled(struct regulator_dev *rdev)
+ {
+ struct pse_controller_dev *pcdev = rdev_get_drvdata(rdev);
+- struct pse_admin_state admin_state = {0};
+ const struct pse_controller_ops *ops;
+ int id, ret;
+
+@@ -285,15 +309,7 @@ static int pse_pi_is_enabled(struct regu
+
+ id = rdev_get_id(rdev);
+ mutex_lock(&pcdev->lock);
+- ret = ops->pi_get_admin_state(pcdev, id, &admin_state);
+- if (ret)
+- goto out;
+-
+- if (admin_state.podl_admin_state == ETHTOOL_PODL_PSE_ADMIN_STATE_ENABLED ||
+- admin_state.c33_admin_state == ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED)
+- ret = 1;
+-
+-out:
++ ret = pse_pi_is_hw_enabled(pcdev, id);
+ mutex_unlock(&pcdev->lock);
+
+ return ret;
--- /dev/null
+From ffef61d6d27374542f1bce4452200d9bdd2e1edd Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:06 +0200
+Subject: [PATCH] net: pse-pd: Add support for budget evaluation strategies
+
+ADAPTED FOR OPENWRT 6.12.67 - Documentation and ethtool_netlink_generated.h changes removed
+
+This patch introduces the ability to configure the PSE PI budget evaluation
+strategies. Budget evaluation strategies is utilized by PSE controllers to
+determine which ports to turn off first in scenarios such as power budget
+exceedance.
+
+The pis_prio_max value is used to define the maximum priority level
+supported by the controller. Both the current priority and the maximum
+priority are exposed to the user through the pse_ethtool_get_status call.
+
+This patch add support for two mode of budget evaluation strategies.
+1. Static Method:
+ This method involves distributing power based on PD classification.
+
+2. Dynamic Method:
+ Budget evaluation strategy based on the current consumption per ports
+ compared to the total power budget.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-7-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pse_core.c | 729 +++++++++++++++++++++++++++++++++-
+ include/linux/pse-pd/pse.h | 76 ++++
+ 2 files changed, 791 insertions(+), 14 deletions(-)
+
+--- a/drivers/net/pse-pd/pse_core.c
++++ b/drivers/net/pse-pd/pse_core.c
+@@ -47,11 +47,14 @@ struct pse_control {
+ * @id: ID of the power domain
+ * @supply: Power supply the Power Domain
+ * @refcnt: Number of gets of this pse_power_domain
++ * @budget_eval_strategy: Current power budget evaluation strategy of the
++ * power domain
+ */
+ struct pse_power_domain {
+ int id;
+ struct regulator *supply;
+ struct kref refcnt;
++ u32 budget_eval_strategy;
+ };
+
+ static int of_load_single_pse_pi_pairset(struct device_node *node,
+@@ -297,6 +300,115 @@ static int pse_pi_is_hw_enabled(struct p
+ return 0;
+ }
+
++/**
++ * pse_pi_is_admin_enable_pending - Check if PI is in admin enable pending state
++ * which mean the power is not yet being
++ * delivered
++ * @pcdev: a pointer to the PSE controller device
++ * @id: Index of the PI
++ *
++ * Detects if a PI is enabled in software with a PD detected, but the hardware
++ * admin state hasn't been applied yet.
++ *
++ * This function is used in the power delivery and retry mechanisms to determine
++ * which PIs need to have power delivery attempted again.
++ *
++ * Return: true if the PI has admin enable flag set in software but not yet
++ * reflected in the hardware admin state, false otherwise.
++ */
++static bool
++pse_pi_is_admin_enable_pending(struct pse_controller_dev *pcdev, int id)
++{
++ int ret;
++
++ /* PI not enabled or nothing is plugged */
++ if (!pcdev->pi[id].admin_state_enabled ||
++ !pcdev->pi[id].isr_pd_detected)
++ return false;
++
++ ret = pse_pi_is_hw_enabled(pcdev, id);
++ /* PSE PI is already enabled at hardware level */
++ if (ret == 1)
++ return false;
++
++ return true;
++}
++
++static int _pse_pi_delivery_power_sw_pw_ctrl(struct pse_controller_dev *pcdev,
++ int id,
++ struct netlink_ext_ack *extack);
++
++/**
++ * pse_pw_d_retry_power_delivery - Retry power delivery for pending ports in a
++ * PSE power domain
++ * @pcdev: a pointer to the PSE controller device
++ * @pw_d: a pointer to the PSE power domain
++ *
++ * Scans all ports in the specified power domain and attempts to enable power
++ * delivery to any ports that have admin enable state set but don't yet have
++ * hardware power enabled. Used when there are changes in connection status,
++ * admin state, or priority that might allow previously unpowered ports to
++ * receive power, especially in over-budget conditions.
++ */
++static void pse_pw_d_retry_power_delivery(struct pse_controller_dev *pcdev,
++ struct pse_power_domain *pw_d)
++{
++ int i, ret = 0;
++
++ for (i = 0; i < pcdev->nr_lines; i++) {
++ int prio_max = pcdev->nr_lines;
++ struct netlink_ext_ack extack;
++
++ if (pcdev->pi[i].pw_d != pw_d)
++ continue;
++
++ if (!pse_pi_is_admin_enable_pending(pcdev, i))
++ continue;
++
++ /* Do not try to enable PI with a lower prio (higher value)
++ * than one which already can't be enabled.
++ */
++ if (pcdev->pi[i].prio > prio_max)
++ continue;
++
++ ret = _pse_pi_delivery_power_sw_pw_ctrl(pcdev, i, &extack);
++ if (ret == -ERANGE)
++ prio_max = pcdev->pi[i].prio;
++ }
++}
++
++/**
++ * pse_pw_d_is_sw_pw_control - Determine if power control is software managed
++ * @pcdev: a pointer to the PSE controller device
++ * @pw_d: a pointer to the PSE power domain
++ *
++ * This function determines whether the power control for a specific power
++ * domain is managed by software in the interrupt handler rather than directly
++ * by hardware.
++ *
++ * Software power control is active in the following cases:
++ * - When the budget evaluation strategy is set to static
++ * - When the budget evaluation strategy is disabled but the PSE controller
++ * has an interrupt handler that can report if a Powered Device is connected
++ *
++ * Return: true if the power control of the power domain is managed by software,
++ * false otherwise
++ */
++static bool pse_pw_d_is_sw_pw_control(struct pse_controller_dev *pcdev,
++ struct pse_power_domain *pw_d)
++{
++ if (!pw_d)
++ return false;
++
++ if (pw_d->budget_eval_strategy == PSE_BUDGET_EVAL_STRAT_STATIC)
++ return true;
++ if (pw_d->budget_eval_strategy == PSE_BUDGET_EVAL_STRAT_DISABLED &&
++ pcdev->ops->pi_enable && pcdev->irq)
++ return true;
++
++ return false;
++}
++
+ static int pse_pi_is_enabled(struct regulator_dev *rdev)
+ {
+ struct pse_controller_dev *pcdev = rdev_get_drvdata(rdev);
+@@ -309,17 +421,252 @@ static int pse_pi_is_enabled(struct regu
+
+ id = rdev_get_id(rdev);
+ mutex_lock(&pcdev->lock);
++ if (pse_pw_d_is_sw_pw_control(pcdev, pcdev->pi[id].pw_d)) {
++ ret = pcdev->pi[id].admin_state_enabled;
++ goto out;
++ }
++
+ ret = pse_pi_is_hw_enabled(pcdev, id);
++
++out:
+ mutex_unlock(&pcdev->lock);
+
+ return ret;
+ }
+
++/**
++ * pse_pi_deallocate_pw_budget - Deallocate power budget of the PI
++ * @pi: a pointer to the PSE PI
++ */
++static void pse_pi_deallocate_pw_budget(struct pse_pi *pi)
++{
++ if (!pi->pw_d || !pi->pw_allocated_mW)
++ return;
++
++ regulator_free_power_budget(pi->pw_d->supply, pi->pw_allocated_mW);
++ pi->pw_allocated_mW = 0;
++}
++
++/**
++ * _pse_pi_disable - Call disable operation. Assumes the PSE lock has been
++ * acquired.
++ * @pcdev: a pointer to the PSE
++ * @id: index of the PSE control
++ *
++ * Return: 0 on success and failure value on error
++ */
++static int _pse_pi_disable(struct pse_controller_dev *pcdev, int id)
++{
++ const struct pse_controller_ops *ops = pcdev->ops;
++ int ret;
++
++ if (!ops->pi_disable)
++ return -EOPNOTSUPP;
++
++ ret = ops->pi_disable(pcdev, id);
++ if (ret)
++ return ret;
++
++ pse_pi_deallocate_pw_budget(&pcdev->pi[id]);
++
++ if (pse_pw_d_is_sw_pw_control(pcdev, pcdev->pi[id].pw_d))
++ pse_pw_d_retry_power_delivery(pcdev, pcdev->pi[id].pw_d);
++
++ return 0;
++}
++
++/**
++ * pse_disable_pi_pol - Disable a PI on a power budget policy
++ * @pcdev: a pointer to the PSE
++ * @id: index of the PSE PI
++ *
++ * Return: 0 on success and failure value on error
++ */
++static int pse_disable_pi_pol(struct pse_controller_dev *pcdev, int id)
++{
++ unsigned long notifs = ETHTOOL_PSE_EVENT_OVER_BUDGET;
++ struct pse_ntf ntf = {};
++ int ret;
++
++ dev_dbg(pcdev->dev, "Disabling PI %d to free power budget\n", id);
++
++ ret = _pse_pi_disable(pcdev, id);
++ if (ret)
++ notifs |= ETHTOOL_PSE_EVENT_SW_PW_CONTROL_ERROR;
++
++ ntf.notifs = notifs;
++ ntf.id = id;
++ kfifo_in_spinlocked(&pcdev->ntf_fifo, &ntf, 1, &pcdev->ntf_fifo_lock);
++ schedule_work(&pcdev->ntf_work);
++
++ return ret;
++}
++
++/**
++ * pse_disable_pi_prio - Disable all PIs of a given priority inside a PSE
++ * power domain
++ * @pcdev: a pointer to the PSE
++ * @pw_d: a pointer to the PSE power domain
++ * @prio: priority
++ *
++ * Return: 0 on success and failure value on error
++ */
++static int pse_disable_pi_prio(struct pse_controller_dev *pcdev,
++ struct pse_power_domain *pw_d,
++ int prio)
++{
++ int i;
++
++ for (i = 0; i < pcdev->nr_lines; i++) {
++ int ret;
++
++ if (pcdev->pi[i].prio != prio ||
++ pcdev->pi[i].pw_d != pw_d ||
++ pse_pi_is_hw_enabled(pcdev, i) <= 0)
++ continue;
++
++ ret = pse_disable_pi_pol(pcdev, i);
++ if (ret)
++ return ret;
++ }
++
++ return 0;
++}
++
++/**
++ * pse_pi_allocate_pw_budget_static_prio - Allocate power budget for the PI
++ * when the budget eval strategy is
++ * static
++ * @pcdev: a pointer to the PSE
++ * @id: index of the PSE control
++ * @pw_req: power requested in mW
++ * @extack: extack for error reporting
++ *
++ * Allocates power using static budget evaluation strategy, where allocation
++ * is based on PD classification. When insufficient budget is available,
++ * lower-priority ports (higher priority numbers) are turned off first.
++ *
++ * Return: 0 on success and failure value on error
++ */
++static int
++pse_pi_allocate_pw_budget_static_prio(struct pse_controller_dev *pcdev, int id,
++ int pw_req, struct netlink_ext_ack *extack)
++{
++ struct pse_pi *pi = &pcdev->pi[id];
++ int ret, _prio;
++
++ _prio = pcdev->nr_lines;
++ while (regulator_request_power_budget(pi->pw_d->supply, pw_req) == -ERANGE) {
++ if (_prio <= pi->prio) {
++ NL_SET_ERR_MSG_FMT(extack,
++ "PI %d: not enough power budget available",
++ id);
++ return -ERANGE;
++ }
++
++ ret = pse_disable_pi_prio(pcdev, pi->pw_d, _prio);
++ if (ret < 0)
++ return ret;
++
++ _prio--;
++ }
++
++ pi->pw_allocated_mW = pw_req;
++ return 0;
++}
++
++/**
++ * pse_pi_allocate_pw_budget - Allocate power budget for the PI
++ * @pcdev: a pointer to the PSE
++ * @id: index of the PSE control
++ * @pw_req: power requested in mW
++ * @extack: extack for error reporting
++ *
++ * Return: 0 on success and failure value on error
++ */
++static int pse_pi_allocate_pw_budget(struct pse_controller_dev *pcdev, int id,
++ int pw_req, struct netlink_ext_ack *extack)
++{
++ struct pse_pi *pi = &pcdev->pi[id];
++
++ if (!pi->pw_d)
++ return 0;
++
++ /* PSE_BUDGET_EVAL_STRAT_STATIC */
++ if (pi->pw_d->budget_eval_strategy == PSE_BUDGET_EVAL_STRAT_STATIC)
++ return pse_pi_allocate_pw_budget_static_prio(pcdev, id, pw_req,
++ extack);
++
++ return 0;
++}
++
++/**
++ * _pse_pi_delivery_power_sw_pw_ctrl - Enable PSE PI in case of software power
++ * control. Assumes the PSE lock has been
++ * acquired.
++ * @pcdev: a pointer to the PSE
++ * @id: index of the PSE control
++ * @extack: extack for error reporting
++ *
++ * Return: 0 on success and failure value on error
++ */
++static int _pse_pi_delivery_power_sw_pw_ctrl(struct pse_controller_dev *pcdev,
++ int id,
++ struct netlink_ext_ack *extack)
++{
++ const struct pse_controller_ops *ops = pcdev->ops;
++ struct pse_pi *pi = &pcdev->pi[id];
++ int ret, pw_req;
++
++ if (!ops->pi_get_pw_req) {
++ /* No power allocation management */
++ ret = ops->pi_enable(pcdev, id);
++ if (ret)
++ NL_SET_ERR_MSG_FMT(extack,
++ "PI %d: enable error %d",
++ id, ret);
++ return ret;
++ }
++
++ ret = ops->pi_get_pw_req(pcdev, id);
++ if (ret < 0)
++ return ret;
++
++ pw_req = ret;
++
++ /* Compare requested power with port power limit and use the lowest
++ * one.
++ */
++ if (ops->pi_get_pw_limit) {
++ ret = ops->pi_get_pw_limit(pcdev, id);
++ if (ret < 0)
++ return ret;
++
++ if (ret < pw_req)
++ pw_req = ret;
++ }
++
++ ret = pse_pi_allocate_pw_budget(pcdev, id, pw_req, extack);
++ if (ret)
++ return ret;
++
++ ret = ops->pi_enable(pcdev, id);
++ if (ret) {
++ pse_pi_deallocate_pw_budget(pi);
++ NL_SET_ERR_MSG_FMT(extack,
++ "PI %d: enable error %d",
++ id, ret);
++ return ret;
++ }
++
++ return 0;
++}
++
+ static int pse_pi_enable(struct regulator_dev *rdev)
+ {
+ struct pse_controller_dev *pcdev = rdev_get_drvdata(rdev);
+ const struct pse_controller_ops *ops;
+- int id, ret;
++ int id, ret = 0;
+
+ ops = pcdev->ops;
+ if (!ops->pi_enable)
+@@ -327,6 +674,23 @@ static int pse_pi_enable(struct regulato
+
+ id = rdev_get_id(rdev);
+ mutex_lock(&pcdev->lock);
++ if (pse_pw_d_is_sw_pw_control(pcdev, pcdev->pi[id].pw_d)) {
++ /* Manage enabled status by software.
++ * Real enable process will happen if a port is connected.
++ */
++ if (pcdev->pi[id].isr_pd_detected) {
++ struct netlink_ext_ack extack;
++
++ ret = _pse_pi_delivery_power_sw_pw_ctrl(pcdev, id, &extack);
++ }
++ if (!ret || ret == -ERANGE) {
++ pcdev->pi[id].admin_state_enabled = 1;
++ ret = 0;
++ }
++ mutex_unlock(&pcdev->lock);
++ return ret;
++ }
++
+ ret = ops->pi_enable(pcdev, id);
+ if (!ret)
+ pcdev->pi[id].admin_state_enabled = 1;
+@@ -338,21 +702,18 @@ static int pse_pi_enable(struct regulato
+ static int pse_pi_disable(struct regulator_dev *rdev)
+ {
+ struct pse_controller_dev *pcdev = rdev_get_drvdata(rdev);
+- const struct pse_controller_ops *ops;
++ struct pse_pi *pi;
+ int id, ret;
+
+- ops = pcdev->ops;
+- if (!ops->pi_disable)
+- return -EOPNOTSUPP;
+-
+ id = rdev_get_id(rdev);
++ pi = &pcdev->pi[id];
+ mutex_lock(&pcdev->lock);
+- ret = ops->pi_disable(pcdev, id);
++ ret = _pse_pi_disable(pcdev, id);
+ if (!ret)
+- pcdev->pi[id].admin_state_enabled = 0;
+- mutex_unlock(&pcdev->lock);
++ pi->admin_state_enabled = 0;
+
+- return ret;
++ mutex_unlock(&pcdev->lock);
++ return 0;
+ }
+
+ static int _pse_pi_get_voltage(struct regulator_dev *rdev)
+@@ -628,6 +989,11 @@ static int pse_register_pw_ds(struct pse
+ }
+
+ pw_d->supply = supply;
++ if (pcdev->supp_budget_eval_strategies)
++ pw_d->budget_eval_strategy = pcdev->supp_budget_eval_strategies;
++ else
++ pw_d->budget_eval_strategy = PSE_BUDGET_EVAL_STRAT_DISABLED;
++ kref_init(&pw_d->refcnt);
+ pcdev->pi[i].pw_d = pw_d;
+ }
+
+@@ -637,6 +1003,34 @@ out:
+ }
+
+ /**
++ * pse_send_ntf_worker - Worker to send PSE notifications
++ * @work: work object
++ *
++ * Manage and send PSE netlink notifications using a workqueue to avoid
++ * deadlock between pcdev_lock and pse_list_mutex.
++ */
++static void pse_send_ntf_worker(struct work_struct *work)
++{
++ struct pse_controller_dev *pcdev;
++ struct pse_ntf ntf;
++
++ pcdev = container_of(work, struct pse_controller_dev, ntf_work);
++
++ while (kfifo_out(&pcdev->ntf_fifo, &ntf, 1)) {
++ struct net_device *netdev;
++ struct pse_control *psec;
++
++ psec = pse_control_find_by_id(pcdev, ntf.id);
++ rtnl_lock();
++ netdev = pse_control_get_netdev(psec);
++ if (netdev)
++ ethnl_pse_send_ntf(netdev, ntf.notifs);
++ rtnl_unlock();
++ pse_control_put(psec);
++ }
++}
++
++/**
+ * pse_controller_register - register a PSE controller device
+ * @pcdev: a pointer to the initialized PSE controller device
+ *
+@@ -649,6 +1043,13 @@ int pse_controller_register(struct pse_c
+
+ mutex_init(&pcdev->lock);
+ INIT_LIST_HEAD(&pcdev->pse_control_head);
++ spin_lock_init(&pcdev->ntf_fifo_lock);
++ ret = kfifo_alloc(&pcdev->ntf_fifo, pcdev->nr_lines, GFP_KERNEL);
++ if (ret) {
++ dev_err(pcdev->dev, "failed to allocate kfifo notifications\n");
++ return ret;
++ }
++ INIT_WORK(&pcdev->ntf_work, pse_send_ntf_worker);
+
+ if (!pcdev->nr_lines)
+ pcdev->nr_lines = 1;
+@@ -715,6 +1116,10 @@ void pse_controller_unregister(struct ps
+ {
+ pse_flush_pw_ds(pcdev);
+ pse_release_pis(pcdev);
++ if (pcdev->irq)
++ disable_irq(pcdev->irq);
++ cancel_work_sync(&pcdev->ntf_work);
++ kfifo_free(&pcdev->ntf_fifo);
+ mutex_lock(&pse_list_mutex);
+ list_del(&pcdev->list);
+ mutex_unlock(&pse_list_mutex);
+@@ -787,6 +1192,52 @@ static unsigned long pse_to_regulator_no
+ }
+
+ /**
++ * pse_set_config_isr - Set PSE control config according to the PSE
++ * notifications
++ * @pcdev: a pointer to the PSE
++ * @id: index of the PSE control
++ * @notifs: PSE event notifications
++ *
++ * Return: 0 on success and failure value on error
++ */
++static int pse_set_config_isr(struct pse_controller_dev *pcdev, int id,
++ unsigned long notifs)
++{
++ int ret = 0;
++
++ if (notifs & PSE_BUDGET_EVAL_STRAT_DYNAMIC)
++ return 0;
++
++ if ((notifs & ETHTOOL_C33_PSE_EVENT_DISCONNECTION) &&
++ ((notifs & ETHTOOL_C33_PSE_EVENT_DETECTION) ||
++ (notifs & ETHTOOL_C33_PSE_EVENT_CLASSIFICATION))) {
++ dev_dbg(pcdev->dev,
++ "PI %d: error, connection and disconnection reported simultaneously",
++ id);
++ return -EINVAL;
++ }
++
++ if (notifs & ETHTOOL_C33_PSE_EVENT_CLASSIFICATION) {
++ struct netlink_ext_ack extack;
++
++ pcdev->pi[id].isr_pd_detected = true;
++ if (pcdev->pi[id].admin_state_enabled) {
++ ret = _pse_pi_delivery_power_sw_pw_ctrl(pcdev, id,
++ &extack);
++ if (ret == -ERANGE)
++ ret = 0;
++ }
++ } else if (notifs & ETHTOOL_C33_PSE_EVENT_DISCONNECTION) {
++ if (pcdev->pi[id].admin_state_enabled &&
++ pcdev->pi[id].isr_pd_detected)
++ ret = _pse_pi_disable(pcdev, id);
++ pcdev->pi[id].isr_pd_detected = false;
++ }
++
++ return ret;
++}
++
++/**
+ * pse_isr - IRQ handler for PSE
+ * @irq: irq number
+ * @data: pointer to user interrupt structure
+@@ -808,36 +1259,42 @@ static irqreturn_t pse_isr(int irq, void
+ memset(h->notifs, 0, pcdev->nr_lines * sizeof(*h->notifs));
+ mutex_lock(&pcdev->lock);
+ ret = desc->map_event(irq, pcdev, h->notifs, ¬ifs_mask);
+- mutex_unlock(&pcdev->lock);
+- if (ret || !notifs_mask)
++ if (ret || !notifs_mask) {
++ mutex_unlock(&pcdev->lock);
+ return IRQ_NONE;
++ }
+
+ for_each_set_bit(i, ¬ifs_mask, pcdev->nr_lines) {
+ unsigned long notifs, rnotifs;
+- struct net_device *netdev;
+- struct pse_control *psec;
++ struct pse_ntf ntf = {};
+
+ /* Do nothing PI not described */
+ if (!pcdev->pi[i].rdev)
+ continue;
+
+ notifs = h->notifs[i];
++ if (pse_pw_d_is_sw_pw_control(pcdev, pcdev->pi[i].pw_d)) {
++ ret = pse_set_config_isr(pcdev, i, notifs);
++ if (ret)
++ notifs |= ETHTOOL_PSE_EVENT_SW_PW_CONTROL_ERROR;
++ }
++
+ dev_dbg(h->pcdev->dev,
+ "Sending PSE notification EVT 0x%lx\n", notifs);
+
+- psec = pse_control_find_by_id(pcdev, i);
+- rtnl_lock();
+- netdev = pse_control_get_netdev(psec);
+- if (netdev)
+- ethnl_pse_send_ntf(netdev, notifs);
+- rtnl_unlock();
+- pse_control_put(psec);
++ ntf.notifs = 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);
+ regulator_notifier_call_chain(pcdev->pi[i].rdev, rnotifs,
+ NULL);
+ }
+
++ mutex_unlock(&pcdev->lock);
++
+ return IRQ_HANDLED;
+ }
+
+@@ -960,6 +1417,20 @@ pse_control_get_internal(struct pse_cont
+ goto free_psec;
+ }
+
++ if (!pcdev->ops->pi_get_admin_state) {
++ ret = -EOPNOTSUPP;
++ goto free_psec;
++ }
++
++ /* Initialize admin_state_enabled before the regulator_get. This
++ * aims to have the right value reported in the first is_enabled
++ * call in case of control managed by software.
++ */
++ ret = pse_pi_is_hw_enabled(pcdev, index);
++ if (ret < 0)
++ goto free_psec;
++
++ pcdev->pi[index].admin_state_enabled = ret;
+ psec->ps = devm_regulator_get_exclusive(pcdev->dev,
+ rdev_get_name(pcdev->pi[index].rdev));
+ if (IS_ERR(psec->ps)) {
+@@ -967,12 +1438,6 @@ pse_control_get_internal(struct pse_cont
+ goto put_module;
+ }
+
+- ret = regulator_is_enabled(psec->ps);
+- if (ret < 0)
+- goto regulator_put;
+-
+- pcdev->pi[index].admin_state_enabled = ret;
+-
+ psec->pcdev = pcdev;
+ list_add(&psec->list, &pcdev->pse_control_head);
+ psec->id = index;
+@@ -981,8 +1446,6 @@ pse_control_get_internal(struct pse_cont
+
+ return psec;
+
+-regulator_put:
+- devm_regulator_put(psec->ps);
+ put_module:
+ module_put(pcdev->owner);
+ free_psec:
+@@ -1094,6 +1557,35 @@ out:
+ EXPORT_SYMBOL_GPL(of_pse_control_get);
+
+ /**
++ * pse_get_sw_admin_state - Convert the software admin state to c33 or podl
++ * admin state value used in the standard
++ * @psec: PSE control pointer
++ * @admin_state: a pointer to the admin_state structure
++ */
++static void pse_get_sw_admin_state(struct pse_control *psec,
++ struct pse_admin_state *admin_state)
++{
++ struct pse_pi *pi = &psec->pcdev->pi[psec->id];
++
++ if (pse_has_podl(psec)) {
++ if (pi->admin_state_enabled)
++ admin_state->podl_admin_state =
++ ETHTOOL_PODL_PSE_ADMIN_STATE_ENABLED;
++ else
++ admin_state->podl_admin_state =
++ ETHTOOL_PODL_PSE_ADMIN_STATE_DISABLED;
++ }
++ if (pse_has_c33(psec)) {
++ if (pi->admin_state_enabled)
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED;
++ else
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
++ }
++}
++
++/**
+ * pse_ethtool_get_status - get status of PSE control
+ * @psec: PSE control pointer
+ * @extack: extack for reporting useful error messages
+@@ -1109,19 +1601,46 @@ int pse_ethtool_get_status(struct pse_co
+ struct pse_pw_status pw_status = {0};
+ const struct pse_controller_ops *ops;
+ struct pse_controller_dev *pcdev;
++ struct pse_pi *pi;
+ int ret;
+
+ pcdev = psec->pcdev;
+ ops = pcdev->ops;
++
++ pi = &pcdev->pi[psec->id];
+ mutex_lock(&pcdev->lock);
+- if (pcdev->pi[psec->id].pw_d)
+- status->pw_d_id = pcdev->pi[psec->id].pw_d->id;
++ if (pi->pw_d) {
++ status->pw_d_id = pi->pw_d->id;
++ if (pse_pw_d_is_sw_pw_control(pcdev, pi->pw_d)) {
++ pse_get_sw_admin_state(psec, &admin_state);
++ } else {
++ ret = ops->pi_get_admin_state(pcdev, psec->id,
++ &admin_state);
++ if (ret)
++ goto out;
++ }
++ status->podl_admin_state = admin_state.podl_admin_state;
++ status->c33_admin_state = admin_state.c33_admin_state;
+
+- ret = ops->pi_get_admin_state(pcdev, psec->id, &admin_state);
+- if (ret)
+- goto out;
+- status->podl_admin_state = admin_state.podl_admin_state;
+- status->c33_admin_state = admin_state.c33_admin_state;
++ switch (pi->pw_d->budget_eval_strategy) {
++ case PSE_BUDGET_EVAL_STRAT_STATIC:
++ status->prio_max = pcdev->nr_lines - 1;
++ status->prio = pi->prio;
++ break;
++ case PSE_BUDGET_EVAL_STRAT_DYNAMIC:
++ status->prio_max = pcdev->pis_prio_max;
++ if (ops->pi_get_prio) {
++ ret = ops->pi_get_prio(pcdev, psec->id);
++ if (ret < 0)
++ goto out;
++
++ status->prio = ret;
++ }
++ break;
++ default:
++ break;
++ }
++ }
+
+ ret = ops->pi_get_pw_status(pcdev, psec->id, &pw_status);
+ if (ret)
+@@ -1271,6 +1790,52 @@ int pse_ethtool_set_config(struct pse_co
+ EXPORT_SYMBOL_GPL(pse_ethtool_set_config);
+
+ /**
++ * pse_pi_update_pw_budget - Update PSE power budget allocated with new
++ * power in mW
++ * @pcdev: a pointer to the PSE controller device
++ * @id: index of the PSE PI
++ * @pw_req: power requested
++ * @extack: extack for reporting useful error messages
++ *
++ * Return: Previous power allocated on success and failure value on error
++ */
++static int pse_pi_update_pw_budget(struct pse_controller_dev *pcdev, int id,
++ const unsigned int pw_req,
++ struct netlink_ext_ack *extack)
++{
++ struct pse_pi *pi = &pcdev->pi[id];
++ int previous_pw_allocated;
++ int pw_diff, ret = 0;
++
++ /* We don't want pw_allocated_mW value change in the middle of an
++ * power budget update
++ */
++ mutex_lock(&pcdev->lock);
++ previous_pw_allocated = pi->pw_allocated_mW;
++ pw_diff = pw_req - previous_pw_allocated;
++ if (!pw_diff) {
++ goto out;
++ } else if (pw_diff > 0) {
++ ret = regulator_request_power_budget(pi->pw_d->supply, pw_diff);
++ if (ret) {
++ NL_SET_ERR_MSG_FMT(extack,
++ "PI %d: not enough power budget available",
++ id);
++ goto out;
++ }
++
++ } else {
++ regulator_free_power_budget(pi->pw_d->supply, -pw_diff);
++ }
++ pi->pw_allocated_mW = pw_req;
++ ret = previous_pw_allocated;
++
++out:
++ mutex_unlock(&pcdev->lock);
++ return ret;
++}
++
++/**
+ * pse_ethtool_set_pw_limit - set PSE control power limit
+ * @psec: PSE control pointer
+ * @extack: extack for reporting useful error messages
+@@ -1282,7 +1847,7 @@ int pse_ethtool_set_pw_limit(struct pse_
+ struct netlink_ext_ack *extack,
+ const unsigned int pw_limit)
+ {
+- int uV, uA, ret;
++ int uV, uA, ret, previous_pw_allocated = 0;
+ s64 tmp_64;
+
+ if (pw_limit > MAX_PI_PW)
+@@ -1306,10 +1871,100 @@ int pse_ethtool_set_pw_limit(struct pse_
+ /* uA = mW * 1000000000 / uV */
+ uA = DIV_ROUND_CLOSEST_ULL(tmp_64, uV);
+
+- return regulator_set_current_limit(psec->ps, 0, uA);
++ /* Update power budget only in software power control case and
++ * if a Power Device is powered.
++ */
++ if (pse_pw_d_is_sw_pw_control(psec->pcdev,
++ psec->pcdev->pi[psec->id].pw_d) &&
++ psec->pcdev->pi[psec->id].admin_state_enabled &&
++ psec->pcdev->pi[psec->id].isr_pd_detected) {
++ ret = pse_pi_update_pw_budget(psec->pcdev, psec->id,
++ pw_limit, extack);
++ if (ret < 0)
++ return ret;
++ previous_pw_allocated = ret;
++ }
++
++ ret = regulator_set_current_limit(psec->ps, 0, uA);
++ if (ret < 0 && previous_pw_allocated) {
++ pse_pi_update_pw_budget(psec->pcdev, psec->id,
++ previous_pw_allocated, extack);
++ }
++
++ return ret;
+ }
+ EXPORT_SYMBOL_GPL(pse_ethtool_set_pw_limit);
+
++/**
++ * pse_ethtool_set_prio - Set PSE PI priority according to the budget
++ * evaluation strategy
++ * @psec: PSE control pointer
++ * @extack: extack for reporting useful error messages
++ * @prio: priovity value
++ *
++ * Return: 0 on success and failure value on error
++ */
++int pse_ethtool_set_prio(struct pse_control *psec,
++ struct netlink_ext_ack *extack,
++ unsigned int prio)
++{
++ struct pse_controller_dev *pcdev = psec->pcdev;
++ const struct pse_controller_ops *ops;
++ int ret = 0;
++
++ if (!pcdev->pi[psec->id].pw_d) {
++ NL_SET_ERR_MSG(extack, "no power domain attached");
++ return -EOPNOTSUPP;
++ }
++
++ /* We don't want priority change in the middle of an
++ * enable/disable call or a priority mode change
++ */
++ mutex_lock(&pcdev->lock);
++ switch (pcdev->pi[psec->id].pw_d->budget_eval_strategy) {
++ case PSE_BUDGET_EVAL_STRAT_STATIC:
++ if (prio >= pcdev->nr_lines) {
++ NL_SET_ERR_MSG_FMT(extack,
++ "priority %d exceed priority max %d",
++ prio, pcdev->nr_lines);
++ ret = -ERANGE;
++ goto out;
++ }
++
++ pcdev->pi[psec->id].prio = prio;
++ pse_pw_d_retry_power_delivery(pcdev, pcdev->pi[psec->id].pw_d);
++ break;
++
++ case PSE_BUDGET_EVAL_STRAT_DYNAMIC:
++ ops = psec->pcdev->ops;
++ if (!ops->pi_set_prio) {
++ NL_SET_ERR_MSG(extack,
++ "pse driver does not support setting port priority");
++ ret = -EOPNOTSUPP;
++ goto out;
++ }
++
++ if (prio > pcdev->pis_prio_max) {
++ NL_SET_ERR_MSG_FMT(extack,
++ "priority %d exceed priority max %d",
++ prio, pcdev->pis_prio_max);
++ ret = -ERANGE;
++ goto out;
++ }
++
++ ret = ops->pi_set_prio(pcdev, psec->id, prio);
++ break;
++
++ default:
++ ret = -EOPNOTSUPP;
++ }
++
++out:
++ mutex_unlock(&pcdev->lock);
++ return ret;
++}
++EXPORT_SYMBOL_GPL(pse_ethtool_set_prio);
++
+ bool pse_has_podl(struct pse_control *psec)
+ {
+ return psec->pcdev->types & ETHTOOL_PSE_PODL;
+--- a/include/linux/pse-pd/pse.h
++++ b/include/linux/pse-pd/pse.h
+@@ -6,6 +6,8 @@
+ #define _LINUX_PSE_CONTROLLER_H
+
+ #include <linux/list.h>
++#include <linux/netlink.h>
++#include <linux/kfifo.h>
+ #include <uapi/linux/ethtool.h>
+ #include <uapi/linux/ethtool_netlink_generated.h>
+ #include <linux/regulator/driver.h>
+@@ -134,6 +136,9 @@ struct pse_pw_limit_ranges {
+ * is in charge of the memory allocation
+ * @c33_pw_limit_nb_ranges: number of supported power limit configuration
+ * ranges
++ * @prio_max: max priority allowed for the c33_prio variable value.
++ * @prio: priority of the PSE. Managed by PSE core in case of static budget
++ * evaluation strategy.
+ */
+ struct ethtool_pse_control_status {
+ u32 pw_d_id;
+@@ -147,6 +152,8 @@ struct ethtool_pse_control_status {
+ u32 c33_avail_pw_limit;
+ struct ethtool_c33_pse_pw_limit_range *c33_pw_limit_ranges;
+ u32 c33_pw_limit_nb_ranges;
++ u32 prio_max;
++ u32 prio;
+ };
+
+ /**
+@@ -170,6 +177,11 @@ struct ethtool_pse_control_status {
+ * range. The driver is in charge of the memory
+ * allocation and should return the number of
+ * ranges.
++ * @pi_get_prio: Get the PSE PI priority.
++ * @pi_set_prio: Configure the PSE PI priority.
++ * @pi_get_pw_req: Get the power requested by a PD before enabling the PSE PI.
++ * This is only relevant when an interrupt is registered using
++ * devm_pse_irq_helper helper.
+ */
+ struct pse_controller_ops {
+ int (*setup_pi_matrix)(struct pse_controller_dev *pcdev);
+@@ -190,6 +202,10 @@ struct pse_controller_ops {
+ int id, int max_mW);
+ int (*pi_get_pw_limit_ranges)(struct pse_controller_dev *pcdev, int id,
+ struct pse_pw_limit_ranges *pw_limit_ranges);
++ int (*pi_get_prio)(struct pse_controller_dev *pcdev, int id);
++ int (*pi_set_prio)(struct pse_controller_dev *pcdev, int id,
++ unsigned int prio);
++ int (*pi_get_pw_req)(struct pse_controller_dev *pcdev, int id);
+ };
+
+ struct module;
+@@ -225,6 +241,13 @@ struct pse_pi_pairset {
+ * @rdev: regulator represented by the PSE PI
+ * @admin_state_enabled: PI enabled state
+ * @pw_d: Power domain of the PSE PI
++ * @prio: Priority of the PSE PI. Used in static budget evaluation strategy
++ * @isr_pd_detected: PSE PI detection status managed by the interruption
++ * handler. This variable is relevant when the power enabled
++ * management is managed in software like the static
++ * budget evaluation strategy.
++ * @pw_allocated_mW: Power allocated to a PSE PI to manage power budget in
++ * static budget evaluation strategy.
+ */
+ struct pse_pi {
+ struct pse_pi_pairset pairset[2];
+@@ -232,6 +255,20 @@ struct pse_pi {
+ struct regulator_dev *rdev;
+ bool admin_state_enabled;
+ struct pse_power_domain *pw_d;
++ int prio;
++ bool isr_pd_detected;
++ int pw_allocated_mW;
++};
++
++/**
++ * struct pse_ntf - PSE notification element
++ *
++ * @id: ID of the PSE control
++ * @notifs: PSE notifications to be reported
++ */
++struct pse_ntf {
++ int id;
++ unsigned long notifs;
+ };
+
+ /**
+@@ -249,6 +286,12 @@ struct pse_pi {
+ * @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
++ * @pis_prio_max: Maximum value allowed for the PSE PIs priority
++ * @supp_budget_eval_strategies: budget evaluation strategies supported
++ * by the PSE
++ * @ntf_work: workqueue for PSE notification management
++ * @ntf_fifo: PSE notifications FIFO
++ * @ntf_fifo_lock: protect @ntf_fifo writer
+ */
+ struct pse_controller_dev {
+ const struct pse_controller_ops *ops;
+@@ -263,6 +306,29 @@ struct pse_controller_dev {
+ struct pse_pi *pi;
+ bool no_of_pse_pi;
+ int irq;
++ unsigned int pis_prio_max;
++ u32 supp_budget_eval_strategies;
++ struct work_struct ntf_work;
++ DECLARE_KFIFO_PTR(ntf_fifo, struct pse_ntf);
++ spinlock_t ntf_fifo_lock; /* Protect @ntf_fifo writer */
++};
++
++/**
++ * enum pse_budget_eval_strategies - PSE budget evaluation strategies.
++ * @PSE_BUDGET_EVAL_STRAT_DISABLED: Budget evaluation strategy disabled.
++ * @PSE_BUDGET_EVAL_STRAT_STATIC: PSE static budget evaluation strategy.
++ * Budget evaluation strategy based on the power requested during PD
++ * classification. This strategy is managed by the PSE core.
++ * @PSE_BUDGET_EVAL_STRAT_DYNAMIC: PSE dynamic budget evaluation
++ * strategy. Budget evaluation strategy based on the current consumption
++ * per ports compared to the total power budget. This mode is managed by
++ * the PSE controller.
++ */
++
++enum pse_budget_eval_strategies {
++ PSE_BUDGET_EVAL_STRAT_DISABLED = 1 << 0,
++ PSE_BUDGET_EVAL_STRAT_STATIC = 1 << 1,
++ PSE_BUDGET_EVAL_STRAT_DYNAMIC = 1 << 2,
+ };
+
+ #if IS_ENABLED(CONFIG_PSE_CONTROLLER)
+@@ -287,6 +353,9 @@ int pse_ethtool_set_config(struct pse_co
+ int pse_ethtool_set_pw_limit(struct pse_control *psec,
+ struct netlink_ext_ack *extack,
+ const unsigned int pw_limit);
++int pse_ethtool_set_prio(struct pse_control *psec,
++ struct netlink_ext_ack *extack,
++ unsigned int prio);
+ int pse_ethtool_get_pw_limit(struct pse_control *psec,
+ struct netlink_ext_ack *extack);
+
+@@ -331,6 +400,13 @@ static inline int pse_ethtool_get_pw_lim
+ {
+ return -EOPNOTSUPP;
+ }
++
++static inline int pse_ethtool_set_prio(struct pse_control *psec,
++ struct netlink_ext_ack *extack,
++ unsigned int prio)
++{
++ return -EOPNOTSUPP;
++}
+
+ static inline bool pse_has_podl(struct pse_control *psec)
+ {
--- /dev/null
+From 359754013e6a7fc81af6735ebbfedd4a01999f68 Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:08 +0200
+Subject: [PATCH] net: pse-pd: pd692x0: Add support for PSE PI priority feature
+
+This patch extends the PSE callbacks by adding support for the newly
+introduced pi_set_prio() callback, enabling the configuration of PSE PI
+priorities. The current port priority is now also included in the status
+information returned to users.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Reviewed-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-9-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 205 +++++++++++++++++++++++++++++++++++
+ 1 file changed, 205 insertions(+)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -12,6 +12,8 @@
+ #include <linux/of.h>
+ #include <linux/platform_device.h>
+ #include <linux/pse-pd/pse.h>
++#include <linux/regulator/driver.h>
++#include <linux/regulator/machine.h>
+
+ #define PD692X0_PSE_NAME "pd692x0_pse"
+
+@@ -76,6 +78,8 @@ enum {
+ PD692X0_MSG_GET_PORT_CLASS,
+ PD692X0_MSG_GET_PORT_MEAS,
+ PD692X0_MSG_GET_PORT_PARAM,
++ PD692X0_MSG_GET_POWER_BANK,
++ PD692X0_MSG_SET_POWER_BANK,
+
+ /* add new message above here */
+ PD692X0_MSG_CNT
+@@ -95,6 +99,8 @@ struct pd692x0_priv {
+ unsigned long last_cmd_key_time;
+
+ enum ethtool_c33_pse_admin_state admin_state[PD692X0_MAX_PIS];
++ struct regulator_dev *manager_reg[PD692X0_MAX_MANAGERS];
++ int manager_pw_budget[PD692X0_MAX_MANAGERS];
+ };
+
+ /* Template list of communication messages. The non-null bytes defined here
+@@ -170,6 +176,16 @@ static const struct pd692x0_msg pd692x0_
+ .data = {0x4e, 0x4e, 0x4e, 0x4e,
+ 0x4e, 0x4e, 0x4e, 0x4e},
+ },
++ [PD692X0_MSG_GET_POWER_BANK] = {
++ .key = PD692X0_KEY_REQ,
++ .sub = {0x07, 0x0b, 0x57},
++ .data = { 0, 0x4e, 0x4e, 0x4e,
++ 0x4e, 0x4e, 0x4e, 0x4e},
++ },
++ [PD692X0_MSG_SET_POWER_BANK] = {
++ .key = PD692X0_KEY_CMD,
++ .sub = {0x07, 0x0b, 0x57},
++ },
+ };
+
+ static u8 pd692x0_build_msg(struct pd692x0_msg *msg, u8 echo)
+@@ -739,6 +755,29 @@ pd692x0_pi_get_actual_pw(struct pse_cont
+ return (buf.data[0] << 4 | buf.data[1]) * 100;
+ }
+
++static int
++pd692x0_pi_get_prio(struct pse_controller_dev *pcdev, int id)
++{
++ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
++ struct pd692x0_msg msg, buf = {0};
++ int ret;
++
++ ret = pd692x0_fw_unavailable(priv);
++ if (ret)
++ return ret;
++
++ msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_PARAM];
++ msg.sub[2] = id;
++ ret = pd692x0_sendrecv_msg(priv, &msg, &buf);
++ if (ret < 0)
++ return ret;
++ if (!buf.data[2] || buf.data[2] > pcdev->pis_prio_max + 1)
++ return -ERANGE;
++
++ /* PSE core priority start at 0 */
++ return buf.data[2] - 1;
++}
++
+ static struct pd692x0_msg_ver pd692x0_get_sw_version(struct pd692x0_priv *priv)
+ {
+ struct device *dev = &priv->client->dev;
+@@ -766,6 +805,7 @@ static struct pd692x0_msg_ver pd692x0_ge
+
+ struct pd692x0_manager {
+ struct device_node *port_node[PD692X0_MAX_MANAGER_PORTS];
++ struct device_node *node;
+ int nports;
+ };
+
+@@ -857,6 +897,8 @@ pd692x0_of_get_managers(struct pd692x0_p
+ if (ret)
+ goto out;
+
++ of_node_get(node);
++ manager[manager_id].node = node;
+ nmanagers++;
+ }
+
+@@ -869,6 +911,8 @@ out:
+ of_node_put(manager[i].port_node[j]);
+ manager[i].port_node[j] = NULL;
+ }
++ of_node_put(manager[i].node);
++ manager[i].node = NULL;
+ }
+
+ of_node_put(node);
+@@ -876,6 +920,130 @@ out:
+ return ret;
+ }
+
++static const struct regulator_ops dummy_ops;
++
++static struct regulator_dev *
++pd692x0_register_manager_regulator(struct device *dev, char *reg_name,
++ struct device_node *node)
++{
++ struct regulator_init_data *rinit_data;
++ struct regulator_config rconfig = {0};
++ struct regulator_desc *rdesc;
++ struct regulator_dev *rdev;
++
++ rinit_data = devm_kzalloc(dev, sizeof(*rinit_data),
++ GFP_KERNEL);
++ if (!rinit_data)
++ return ERR_PTR(-ENOMEM);
++
++ rdesc = devm_kzalloc(dev, sizeof(*rdesc), GFP_KERNEL);
++ if (!rdesc)
++ return ERR_PTR(-ENOMEM);
++
++ rdesc->name = reg_name;
++ rdesc->type = REGULATOR_VOLTAGE;
++ rdesc->ops = &dummy_ops;
++ rdesc->owner = THIS_MODULE;
++
++ rinit_data->supply_regulator = "vmain";
++
++ rconfig.dev = dev;
++ rconfig.init_data = rinit_data;
++ rconfig.of_node = node;
++
++ rdev = devm_regulator_register(dev, rdesc, &rconfig);
++ if (IS_ERR(rdev)) {
++ dev_err_probe(dev, PTR_ERR(rdev),
++ "Failed to register regulator\n");
++ return rdev;
++ }
++
++ return rdev;
++}
++
++static int
++pd692x0_register_managers_regulator(struct pd692x0_priv *priv,
++ const struct pd692x0_manager *manager,
++ int nmanagers)
++{
++ struct device *dev = &priv->client->dev;
++ size_t reg_name_len;
++ int i;
++
++ /* Each regulator name len is dev name + 12 char +
++ * int max digit number (10) + 1
++ */
++ reg_name_len = strlen(dev_name(dev)) + 23;
++
++ for (i = 0; i < nmanagers; i++) {
++ struct regulator_dev *rdev;
++ char *reg_name;
++
++ reg_name = devm_kzalloc(dev, reg_name_len, GFP_KERNEL);
++ if (!reg_name)
++ return -ENOMEM;
++ snprintf(reg_name, 26, "pse-%s-manager%d", dev_name(dev), i);
++ rdev = pd692x0_register_manager_regulator(dev, reg_name,
++ manager[i].node);
++ if (IS_ERR(rdev))
++ return PTR_ERR(rdev);
++
++ priv->manager_reg[i] = rdev;
++ }
++
++ return 0;
++}
++
++static int
++pd692x0_conf_manager_power_budget(struct pd692x0_priv *priv, int id, int pw)
++{
++ struct pd692x0_msg msg, buf;
++ int ret, pw_mW = pw / 1000;
++
++ msg = pd692x0_msg_template_list[PD692X0_MSG_GET_POWER_BANK];
++ msg.data[0] = id;
++ ret = pd692x0_sendrecv_msg(priv, &msg, &buf);
++ if (ret < 0)
++ return ret;
++
++ msg = pd692x0_msg_template_list[PD692X0_MSG_SET_POWER_BANK];
++ msg.data[0] = id;
++ msg.data[1] = pw_mW >> 8;
++ msg.data[2] = pw_mW & 0xff;
++ msg.data[3] = buf.sub[2];
++ msg.data[4] = buf.data[0];
++ msg.data[5] = buf.data[1];
++ msg.data[6] = buf.data[2];
++ msg.data[7] = buf.data[3];
++ return pd692x0_sendrecv_msg(priv, &msg, &buf);
++}
++
++static int
++pd692x0_configure_managers(struct pd692x0_priv *priv, int nmanagers)
++{
++ int i, ret;
++
++ for (i = 0; i < nmanagers; i++) {
++ struct regulator *supply = priv->manager_reg[i]->supply;
++ int pw_budget;
++
++ pw_budget = regulator_get_unclaimed_power_budget(supply);
++ /* Max power budget per manager */
++ if (pw_budget > 6000000)
++ pw_budget = 6000000;
++ ret = regulator_request_power_budget(supply, pw_budget);
++ if (ret < 0)
++ return ret;
++
++ priv->manager_pw_budget[i] = pw_budget;
++ ret = pd692x0_conf_manager_power_budget(priv, i, pw_budget);
++ if (ret < 0)
++ return ret;
++ }
++
++ return 0;
++}
++
+ static int
+ pd692x0_set_port_matrix(const struct pse_pi_pairset *pairset,
+ const struct pd692x0_manager *manager,
+@@ -998,6 +1166,14 @@ static int pd692x0_setup_pi_matrix(struc
+ return ret;
+
+ nmanagers = ret;
++ ret = pd692x0_register_managers_regulator(priv, manager, nmanagers);
++ if (ret)
++ goto out;
++
++ ret = pd692x0_configure_managers(priv, nmanagers);
++ if (ret)
++ goto out;
++
+ ret = pd692x0_set_ports_matrix(priv, manager, nmanagers, port_matrix);
+ if (ret)
+ goto out;
+@@ -1008,8 +1184,14 @@ static int pd692x0_setup_pi_matrix(struc
+
+ out:
+ for (i = 0; i < nmanagers; i++) {
++ struct regulator *supply = priv->manager_reg[i]->supply;
++
++ regulator_free_power_budget(supply,
++ priv->manager_pw_budget[i]);
++
+ for (j = 0; j < manager[i].nports; j++)
+ of_node_put(manager[i].port_node[j]);
++ of_node_put(manager[i].node);
+ }
+ return ret;
+ }
+@@ -1071,6 +1253,25 @@ static int pd692x0_pi_set_pw_limit(struc
+ return pd692x0_sendrecv_msg(priv, &msg, &buf);
+ }
+
++static int pd692x0_pi_set_prio(struct pse_controller_dev *pcdev, int id,
++ unsigned int prio)
++{
++ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
++ struct pd692x0_msg msg, buf = {0};
++ int ret;
++
++ ret = pd692x0_fw_unavailable(priv);
++ if (ret)
++ return ret;
++
++ msg = pd692x0_msg_template_list[PD692X0_MSG_SET_PORT_PARAM];
++ msg.sub[2] = id;
++ /* Controller priority from 1 to 3 */
++ msg.data[4] = prio + 1;
++
++ return pd692x0_sendrecv_msg(priv, &msg, &buf);
++}
++
+ static const struct pse_controller_ops pd692x0_ops = {
+ .setup_pi_matrix = pd692x0_setup_pi_matrix,
+ .pi_get_admin_state = pd692x0_pi_get_admin_state,
+@@ -1084,6 +1285,8 @@ static const struct pse_controller_ops p
+ .pi_get_pw_limit = pd692x0_pi_get_pw_limit,
+ .pi_set_pw_limit = pd692x0_pi_set_pw_limit,
+ .pi_get_pw_limit_ranges = pd692x0_pi_get_pw_limit_ranges,
++ .pi_get_prio = pd692x0_pi_get_prio,
++ .pi_set_prio = pd692x0_pi_set_prio,
+ };
+
+ #define PD692X0_FW_LINE_MAX_SZ 0xff
+@@ -1500,6 +1703,8 @@ static int pd692x0_i2c_probe(struct i2c_
+ priv->pcdev.ops = &pd692x0_ops;
+ priv->pcdev.dev = dev;
+ priv->pcdev.types = ETHTOOL_PSE_C33;
++ priv->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_DYNAMIC;
++ priv->pcdev.pis_prio_max = 2;
+ ret = devm_pse_controller_register(dev, &priv->pcdev);
+ if (ret)
+ return dev_err_probe(dev, ret,
--- /dev/null
+From 24a4e3a05dd0eadd0c9585c411880e5dcb6be97f Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:09 +0200
+Subject: [PATCH] net: pse-pd: pd692x0: Add support for controller and manager
+ power supplies
+
+Add support for managing the VDD and VDDA power supplies for the PD692x0
+PSE controller, as well as the VAUX5 and VAUX3P3 power supplies for the
+PD6920x PSE managers.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Reviewed-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-10-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 20 ++++++++++++++++++++
+ 1 file changed, 20 insertions(+)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -976,8 +976,10 @@ pd692x0_register_managers_regulator(stru
+ reg_name_len = strlen(dev_name(dev)) + 23;
+
+ for (i = 0; i < nmanagers; i++) {
++ static const char * const regulators[] = { "vaux5", "vaux3p3" };
+ struct regulator_dev *rdev;
+ char *reg_name;
++ int ret;
+
+ reg_name = devm_kzalloc(dev, reg_name_len, GFP_KERNEL);
+ if (!reg_name)
+@@ -988,6 +990,17 @@ pd692x0_register_managers_regulator(stru
+ if (IS_ERR(rdev))
+ return PTR_ERR(rdev);
+
++ /* VMAIN is described as main supply for the manager.
++ * Add other VAUX power supplies and link them to the
++ * virtual device rdev->dev.
++ */
++ ret = devm_regulator_bulk_get_enable(&rdev->dev,
++ ARRAY_SIZE(regulators),
++ regulators);
++ if (ret)
++ return dev_err_probe(&rdev->dev, ret,
++ "Failed to enable regulators\n");
++
+ priv->manager_reg[i] = rdev;
+ }
+
+@@ -1640,6 +1653,7 @@ static const struct fw_upload_ops pd692x
+
+ static int pd692x0_i2c_probe(struct i2c_client *client)
+ {
++ static const char * const regulators[] = { "vdd", "vdda" };
+ struct pd692x0_msg msg, buf = {0}, zero = {0};
+ struct device *dev = &client->dev;
+ struct pd692x0_msg_ver ver;
+@@ -1647,6 +1661,12 @@ static int pd692x0_i2c_probe(struct i2c_
+ struct fw_upload *fwl;
+ int ret;
+
++ ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(regulators),
++ regulators);
++ if (ret)
++ return dev_err_probe(dev, ret,
++ "Failed to enable regulators\n");
++
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
+ dev_err(dev, "i2c check functionality failed\n");
+ return -ENXIO;
--- /dev/null
+From 56cfc97635e9164395c9242f72746454347155ab Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Tue, 17 Jun 2025 14:12:11 +0200
+Subject: [PATCH] net: pse-pd: tps23881: Add support for static port priority
+ feature
+
+This patch enhances PSE callbacks by introducing support for the static
+port priority feature. It extends interrupt management to handle and report
+detection, classification, and disconnection events. Additionally, it
+introduces the pi_get_pw_req() callback, which provides information about
+the power requested by the Powered Devices.
+
+Interrupt support is essential for the proper functioning of the TPS23881
+controller. Without it, after a power-on (PWON), the controller will
+no longer perform detection and classification. This could lead to
+potential hazards, such as connecting a non-PoE device after a PoE device,
+which might result in magic smoke.
+
+Signed-off-by: Kory Maincent (Dent Project) <kory.maincent@bootlin.com>
+Reviewed-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250617-feature_poe_port_prio-v14-12-78a1a645e2ee@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/tps23881.c | 244 +++++++++++++++++++++++++++++++---
+ 1 file changed, 228 insertions(+), 16 deletions(-)
+
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -20,20 +20,30 @@
+
+ #define TPS23881_REG_IT 0x0
+ #define TPS23881_REG_IT_MASK 0x1
++#define TPS23881_REG_IT_DISF BIT(2)
++#define TPS23881_REG_IT_DETC BIT(3)
++#define TPS23881_REG_IT_CLASC BIT(4)
+ #define TPS23881_REG_IT_IFAULT BIT(5)
+ #define TPS23881_REG_IT_SUPF BIT(7)
++#define TPS23881_REG_DET_EVENT 0x5
+ #define TPS23881_REG_FAULT 0x7
+ #define TPS23881_REG_SUPF_EVENT 0xb
+ #define TPS23881_REG_TSD BIT(7)
++#define TPS23881_REG_DISC 0xc
+ #define TPS23881_REG_PW_STATUS 0x10
+ #define TPS23881_REG_OP_MODE 0x12
++#define TPS23881_REG_DISC_EN 0x13
+ #define TPS23881_OP_MODE_SEMIAUTO 0xaaaa
+ #define TPS23881_REG_DIS_EN 0x13
+ #define TPS23881_REG_DET_CLA_EN 0x14
+ #define TPS23881_REG_GEN_MASK 0x17
++#define TPS23881_REG_CLCHE BIT(2)
++#define TPS23881_REG_DECHE BIT(3)
+ #define TPS23881_REG_NBITACC BIT(5)
+ #define TPS23881_REG_INTEN BIT(7)
+ #define TPS23881_REG_PW_EN 0x19
++#define TPS23881_REG_RESET 0x1a
++#define TPS23881_REG_CLRAIN BIT(7)
+ #define TPS23881_REG_2PAIR_POL1 0x1e
+ #define TPS23881_REG_PORT_MAP 0x26
+ #define TPS23881_REG_PORT_POWER 0x29
+@@ -178,6 +188,7 @@ static int tps23881_pi_enable(struct pse
+ struct i2c_client *client = priv->client;
+ u8 chan;
+ u16 val;
++ int ret;
+
+ if (id >= TPS23881_MAX_CHANS)
+ return -ERANGE;
+@@ -191,7 +202,22 @@ static int tps23881_pi_enable(struct pse
+ BIT(chan % 4));
+ }
+
+- return i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
++ ret = i2c_smbus_write_word_data(client, TPS23881_REG_PW_EN, val);
++ if (ret)
++ return ret;
++
++ /* Enable DC disconnect*/
++ chan = priv->port[id].chan[0];
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_DISC_EN);
++ if (ret < 0)
++ return ret;
++
++ val = tps23881_set_val(ret, chan, 0, BIT(chan % 4), BIT(chan % 4));
++ ret = i2c_smbus_write_word_data(client, TPS23881_REG_DISC_EN, val);
++ if (ret)
++ return ret;
++
++ return 0;
+ }
+
+ static int tps23881_pi_disable(struct pse_controller_dev *pcdev, int id)
+@@ -224,6 +250,17 @@ static int tps23881_pi_disable(struct ps
+ */
+ mdelay(5);
+
++ /* Disable DC disconnect*/
++ chan = priv->port[id].chan[0];
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_DISC_EN);
++ if (ret < 0)
++ return ret;
++
++ val = tps23881_set_val(ret, chan, 0, 0, BIT(chan % 4));
++ ret = i2c_smbus_write_word_data(client, TPS23881_REG_DISC_EN, val);
++ if (ret)
++ return ret;
++
+ /* Enable detection and classification */
+ ret = i2c_smbus_read_word_data(client, TPS23881_REG_DET_CLA_EN);
+ if (ret < 0)
+@@ -919,6 +956,47 @@ static int tps23881_setup_pi_matrix(stru
+ return ret;
+ }
+
++static int tps23881_power_class_table[] = {
++ -ERANGE,
++ 4000,
++ 7000,
++ 15500,
++ 30000,
++ 15500,
++ 15500,
++ -ERANGE,
++ 45000,
++ 60000,
++ 75000,
++ 90000,
++ 15500,
++ 45000,
++ -ERANGE,
++ -ERANGE,
++};
++
++static int tps23881_pi_get_pw_req(struct pse_controller_dev *pcdev, int id)
++{
++ struct tps23881_priv *priv = to_tps23881_priv(pcdev);
++ struct i2c_client *client = priv->client;
++ u8 reg, chan;
++ int ret;
++ u16 val;
++
++ /* For a 4-pair the classification need 5ms to be completed */
++ if (priv->port[id].is_4p)
++ mdelay(5);
++
++ chan = priv->port[id].chan[0];
++ reg = TPS23881_REG_DISC + (chan % 4);
++ ret = i2c_smbus_read_word_data(client, reg);
++ if (ret < 0)
++ return ret;
++
++ val = tps23881_calc_val(ret, chan, 4, 0xf);
++ return tps23881_power_class_table[val];
++}
++
+ static const struct pse_controller_ops tps23881_ops = {
+ .setup_pi_matrix = tps23881_setup_pi_matrix,
+ .pi_enable = tps23881_pi_enable,
+@@ -931,6 +1009,7 @@ static const struct pse_controller_ops t
+ .pi_get_pw_limit = tps23881_pi_get_pw_limit,
+ .pi_set_pw_limit = tps23881_pi_set_pw_limit,
+ .pi_get_pw_limit_ranges = tps23881_pi_get_pw_limit_ranges,
++ .pi_get_pw_req = tps23881_pi_get_pw_req,
+ };
+
+ static const char fw_parity_name[] = "ti/tps23881/tps23881-parity-14.bin";
+@@ -1088,17 +1167,113 @@ static void tps23881_irq_event_over_temp
+ }
+ }
+
+-static void tps23881_irq_event_over_current(struct tps23881_priv *priv,
+- u16 reg_val,
+- unsigned long *notifs,
+- unsigned long *notifs_mask)
++static int tps23881_irq_event_over_current(struct tps23881_priv *priv,
++ u16 reg_val,
++ unsigned long *notifs,
++ unsigned long *notifs_mask)
+ {
++ int i, ret;
+ u8 chans;
+
+ chans = tps23881_irq_export_chans_helper(reg_val, 0);
++ if (!chans)
++ return 0;
++
++ tps23881_set_notifs_helper(priv, chans, notifs, notifs_mask,
++ ETHTOOL_PSE_EVENT_OVER_CURRENT |
++ ETHTOOL_C33_PSE_EVENT_DISCONNECTION);
++
++ /* Over Current event resets the power limit registers so we need
++ * to configured it again.
++ */
++ for_each_set_bit(i, notifs_mask, priv->pcdev.nr_lines) {
++ if (priv->port[i].pw_pol < 0)
++ continue;
++
++ ret = tps23881_pi_enable_manual_pol(priv, i);
++ if (ret < 0)
++ return ret;
++
++ /* Set power policy */
++ ret = tps23881_pi_set_pw_pol_limit(priv, i,
++ priv->port[i].pw_pol,
++ priv->port[i].is_4p);
++ if (ret < 0)
++ return ret;
++ }
++
++ return 0;
++}
++
++static void tps23881_irq_event_disconnection(struct tps23881_priv *priv,
++ u16 reg_val,
++ unsigned long *notifs,
++ unsigned long *notifs_mask)
++{
++ u8 chans;
++
++ chans = tps23881_irq_export_chans_helper(reg_val, 4);
+ if (chans)
+ tps23881_set_notifs_helper(priv, chans, notifs, notifs_mask,
+- ETHTOOL_PSE_EVENT_OVER_CURRENT);
++ ETHTOOL_C33_PSE_EVENT_DISCONNECTION);
++}
++
++static int tps23881_irq_event_detection(struct tps23881_priv *priv,
++ u16 reg_val,
++ unsigned long *notifs,
++ unsigned long *notifs_mask)
++{
++ enum ethtool_pse_event event;
++ int reg, ret, i, val;
++ unsigned long chans;
++
++ chans = tps23881_irq_export_chans_helper(reg_val, 0);
++ for_each_set_bit(i, &chans, TPS23881_MAX_CHANS) {
++ reg = TPS23881_REG_DISC + (i % 4);
++ ret = i2c_smbus_read_word_data(priv->client, reg);
++ if (ret < 0)
++ return ret;
++
++ val = tps23881_calc_val(ret, i, 0, 0xf);
++ /* If detection valid */
++ if (val == 0x4)
++ event = ETHTOOL_C33_PSE_EVENT_DETECTION;
++ else
++ event = ETHTOOL_C33_PSE_EVENT_DISCONNECTION;
++
++ tps23881_set_notifs_helper(priv, BIT(i), notifs,
++ notifs_mask, event);
++ }
++
++ return 0;
++}
++
++static int tps23881_irq_event_classification(struct tps23881_priv *priv,
++ u16 reg_val,
++ unsigned long *notifs,
++ unsigned long *notifs_mask)
++{
++ int reg, ret, val, i;
++ unsigned long chans;
++
++ chans = tps23881_irq_export_chans_helper(reg_val, 4);
++ for_each_set_bit(i, &chans, TPS23881_MAX_CHANS) {
++ reg = TPS23881_REG_DISC + (i % 4);
++ ret = i2c_smbus_read_word_data(priv->client, reg);
++ if (ret < 0)
++ return ret;
++
++ val = tps23881_calc_val(ret, i, 4, 0xf);
++ /* Do not report classification event for unknown class */
++ if (!val || val == 0x8 || val == 0xf)
++ continue;
++
++ tps23881_set_notifs_helper(priv, BIT(i), notifs,
++ notifs_mask,
++ ETHTOOL_C33_PSE_EVENT_CLASSIFICATION);
++ }
++
++ return 0;
+ }
+
+ static int tps23881_irq_event_handler(struct tps23881_priv *priv, u16 reg,
+@@ -1106,7 +1281,7 @@ static int tps23881_irq_event_handler(st
+ unsigned long *notifs_mask)
+ {
+ struct i2c_client *client = priv->client;
+- int ret;
++ int ret, val;
+
+ /* The Supply event bit is repeated twice so we only need to read
+ * the one from the first byte.
+@@ -1118,13 +1293,36 @@ static int tps23881_irq_event_handler(st
+ tps23881_irq_event_over_temp(priv, ret, notifs, notifs_mask);
+ }
+
+- if (reg & (TPS23881_REG_IT_IFAULT | TPS23881_REG_IT_IFAULT << 8)) {
++ if (reg & (TPS23881_REG_IT_IFAULT | TPS23881_REG_IT_IFAULT << 8 |
++ TPS23881_REG_IT_DISF | TPS23881_REG_IT_DISF << 8)) {
+ ret = i2c_smbus_read_word_data(client, TPS23881_REG_FAULT);
+ if (ret < 0)
+ return ret;
+- tps23881_irq_event_over_current(priv, ret, notifs, notifs_mask);
++ ret = tps23881_irq_event_over_current(priv, ret, notifs,
++ notifs_mask);
++ if (ret)
++ return ret;
++
++ tps23881_irq_event_disconnection(priv, ret, notifs, notifs_mask);
+ }
+
++ if (reg & (TPS23881_REG_IT_DETC | TPS23881_REG_IT_DETC << 8 |
++ TPS23881_REG_IT_CLASC | TPS23881_REG_IT_CLASC << 8)) {
++ ret = i2c_smbus_read_word_data(client, TPS23881_REG_DET_EVENT);
++ if (ret < 0)
++ return ret;
++
++ val = ret;
++ ret = tps23881_irq_event_detection(priv, val, notifs,
++ notifs_mask);
++ if (ret)
++ return ret;
++
++ ret = tps23881_irq_event_classification(priv, val, notifs,
++ notifs_mask);
++ if (ret)
++ return ret;
++ }
+ return 0;
+ }
+
+@@ -1178,7 +1376,14 @@ static int tps23881_setup_irq(struct tps
+ int ret;
+ u16 val;
+
+- val = TPS23881_REG_IT_IFAULT | TPS23881_REG_IT_SUPF;
++ if (!irq) {
++ dev_err(&client->dev, "interrupt is missing");
++ return -EINVAL;
++ }
++
++ val = TPS23881_REG_IT_IFAULT | TPS23881_REG_IT_SUPF |
++ TPS23881_REG_IT_DETC | TPS23881_REG_IT_CLASC |
++ TPS23881_REG_IT_DISF;
+ val |= val << 8;
+ ret = i2c_smbus_write_word_data(client, TPS23881_REG_IT_MASK, val);
+ if (ret)
+@@ -1188,11 +1393,19 @@ static int tps23881_setup_irq(struct tps
+ if (ret < 0)
+ return ret;
+
+- val = (u16)(ret | TPS23881_REG_INTEN | TPS23881_REG_INTEN << 8);
++ val = TPS23881_REG_INTEN | TPS23881_REG_CLCHE | TPS23881_REG_DECHE;
++ val |= val << 8;
++ val |= (u16)ret;
+ ret = i2c_smbus_write_word_data(client, TPS23881_REG_GEN_MASK, val);
+ if (ret < 0)
+ return ret;
+
++ /* Reset interrupts registers */
++ ret = i2c_smbus_write_word_data(client, TPS23881_REG_RESET,
++ TPS23881_REG_CLRAIN);
++ if (ret < 0)
++ return ret;
++
+ return devm_pse_irq_helper(&priv->pcdev, irq, 0, &irq_desc);
+ }
+
+@@ -1270,17 +1483,16 @@ static int tps23881_i2c_probe(struct i2c
+ priv->pcdev.dev = dev;
+ priv->pcdev.types = ETHTOOL_PSE_C33;
+ priv->pcdev.nr_lines = TPS23881_MAX_CHANS;
++ priv->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_STATIC;
+ ret = devm_pse_controller_register(dev, &priv->pcdev);
+ if (ret) {
+ return dev_err_probe(dev, ret,
+ "failed to register PSE controller\n");
+ }
+
+- if (client->irq) {
+- ret = tps23881_setup_irq(priv, client->irq);
+- if (ret)
+- return ret;
+- }
++ ret = tps23881_setup_irq(priv, client->irq);
++ if (ret)
++ return ret;
+
+ return ret;
+ }
--- /dev/null
+From d12b3dc106090b358fb67b7c0c717a0884327ddf Mon Sep 17 00:00:00 2001
+From: Arnd Bergmann <arnd@arndb.de>
+Date: Wed, 9 Jul 2025 17:32:04 +0200
+Subject: [PATCH] net: pse-pd: pd692x0: reduce stack usage in
+ pd692x0_setup_pi_matrix
+
+The pd692x0_manager array in this function is really too big to fit on the
+stack, though this never triggered a warning until a recent patch made
+it slightly bigger:
+
+drivers/net/pse-pd/pd692x0.c: In function 'pd692x0_setup_pi_matrix':
+drivers/net/pse-pd/pd692x0.c:1210:1: error: the frame size of 1584 bytes is larger than 1536 bytes [-Werror=frame-larger-than=]
+
+Change the function to dynamically allocate the array here.
+
+Fixes: 359754013e6a ("net: pse-pd: pd692x0: Add support for PSE PI priority feature")
+Signed-off-by: Arnd Bergmann <arnd@arndb.de>
+Reviewed-by: Kory Maincent <kory.maincent@bootlin.com>
+Link: https://patch.msgid.link/20250709153210.1920125-1-arnd@kernel.org
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 8 ++++++--
+ 1 file changed, 6 insertions(+), 2 deletions(-)
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -860,7 +860,7 @@ out:
+
+ static int
+ pd692x0_of_get_managers(struct pd692x0_priv *priv,
+- struct pd692x0_manager manager[PD692X0_MAX_MANAGERS])
++ struct pd692x0_manager *manager)
+ {
+ struct device_node *managers_node, *node;
+ int ret, nmanagers, i, j;
+@@ -1164,7 +1164,7 @@ pd692x0_write_ports_matrix(struct pd692x
+
+ static int pd692x0_setup_pi_matrix(struct pse_controller_dev *pcdev)
+ {
+- struct pd692x0_manager manager[PD692X0_MAX_MANAGERS] = {0};
++ struct pd692x0_manager *manager __free(kfree) = NULL;
+ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
+ struct pd692x0_matrix port_matrix[PD692X0_MAX_PIS];
+ int ret, i, j, nmanagers;
+@@ -1174,6 +1174,10 @@ static int pd692x0_setup_pi_matrix(struc
+ priv->fw_state != PD692X0_FW_COMPLETE)
+ return 0;
+
++ manager = kcalloc(PD692X0_MAX_MANAGERS, sizeof(*manager), GFP_KERNEL);
++ if (!manager)
++ return -ENOMEM;
++
+ ret = pd692x0_of_get_managers(priv, manager);
+ if (ret < 0)
+ return ret;
--- /dev/null
+From 1c67f9c54cdc70627e3f6472b89cd3d895df974c Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Wed, 20 Aug 2025 15:27:07 +0200
+Subject: [PATCH] net: pse-pd: pd692x0: Fix power budget leak in manager setup
+ error path
+
+Fix a resource leak where manager power budgets were freed on both
+success and error paths during manager setup. Power budgets should
+only be freed on error paths after regulator registration or during
+driver removal.
+
+Refactor cleanup logic by extracting OF node cleanup and power budget
+freeing into separate helper functions for better maintainability.
+
+Fixes: 359754013e6a ("net: pse-pd: pd692x0: Add support for PSE PI priority feature")
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Link: https://patch.msgid.link/20250820132708.837255-1-kory.maincent@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 59 +++++++++++++++++++++++++++---------
+ 1 file changed, 44 insertions(+), 15 deletions(-)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -1162,12 +1162,44 @@ pd692x0_write_ports_matrix(struct pd692x
+ return 0;
+ }
+
++static void pd692x0_of_put_managers(struct pd692x0_priv *priv,
++ struct pd692x0_manager *manager,
++ int nmanagers)
++{
++ int i, j;
++
++ for (i = 0; i < nmanagers; i++) {
++ for (j = 0; j < manager[i].nports; j++)
++ of_node_put(manager[i].port_node[j]);
++ of_node_put(manager[i].node);
++ }
++}
++
++static void pd692x0_managers_free_pw_budget(struct pd692x0_priv *priv)
++{
++ int i;
++
++ for (i = 0; i < PD692X0_MAX_MANAGERS; i++) {
++ struct regulator *supply;
++
++ if (!priv->manager_reg[i] || !priv->manager_pw_budget[i])
++ continue;
++
++ supply = priv->manager_reg[i]->supply;
++ if (!supply)
++ continue;
++
++ regulator_free_power_budget(supply,
++ priv->manager_pw_budget[i]);
++ }
++}
++
+ static int pd692x0_setup_pi_matrix(struct pse_controller_dev *pcdev)
+ {
+ struct pd692x0_manager *manager __free(kfree) = NULL;
+ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
+ struct pd692x0_matrix port_matrix[PD692X0_MAX_PIS];
+- int ret, i, j, nmanagers;
++ int ret, nmanagers;
+
+ /* Should we flash the port matrix */
+ if (priv->fw_state != PD692X0_FW_OK &&
+@@ -1185,31 +1217,27 @@ static int pd692x0_setup_pi_matrix(struc
+ nmanagers = ret;
+ ret = pd692x0_register_managers_regulator(priv, manager, nmanagers);
+ if (ret)
+- goto out;
++ goto err_of_managers;
+
+ ret = pd692x0_configure_managers(priv, nmanagers);
+ if (ret)
+- goto out;
++ goto err_of_managers;
+
+ ret = pd692x0_set_ports_matrix(priv, manager, nmanagers, port_matrix);
+ if (ret)
+- goto out;
++ goto err_managers_req_pw;
+
+ ret = pd692x0_write_ports_matrix(priv, port_matrix);
+ if (ret)
+- goto out;
+-
+-out:
+- for (i = 0; i < nmanagers; i++) {
+- struct regulator *supply = priv->manager_reg[i]->supply;
++ goto err_managers_req_pw;
+
+- regulator_free_power_budget(supply,
+- priv->manager_pw_budget[i]);
++ pd692x0_of_put_managers(priv, manager, nmanagers);
++ return 0;
+
+- for (j = 0; j < manager[i].nports; j++)
+- of_node_put(manager[i].port_node[j]);
+- of_node_put(manager[i].node);
+- }
++err_managers_req_pw:
++ pd692x0_managers_free_pw_budget(priv);
++err_of_managers:
++ pd692x0_of_put_managers(priv, manager, nmanagers);
+ return ret;
+ }
+
+@@ -1748,6 +1776,7 @@ static void pd692x0_i2c_remove(struct i2
+ {
+ struct pd692x0_priv *priv = i2c_get_clientdata(client);
+
++ pd692x0_managers_free_pw_budget(priv);
+ firmware_upload_unregister(priv->fwl);
+ }
+
--- /dev/null
+From 7ef353879f714602b43f98662069f4fb86536761 Mon Sep 17 00:00:00 2001
+From: Kory Maincent <kory.maincent@bootlin.com>
+Date: Wed, 20 Aug 2025 15:33:21 +0200
+Subject: [PATCH] net: pse-pd: pd692x0: Skip power budget configuration when
+ undefined
+
+If the power supply's power budget is not defined in the device tree,
+the current code still requests power and configures the PSE manager
+with a 0W power limit, which is undesirable behavior.
+
+Skip power budget configuration entirely when the budget is zero,
+avoiding unnecessary power requests and preventing invalid 0W limits
+from being set on the PSE manager.
+
+Fixes: 359754013e6a ("net: pse-pd: pd692x0: Add support for PSE PI priority feature")
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20250820133321.841054-1-kory.maincent@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -1041,6 +1041,10 @@ pd692x0_configure_managers(struct pd692x
+ int pw_budget;
+
+ pw_budget = regulator_get_unclaimed_power_budget(supply);
++ if (!pw_budget)
++ /* Do nothing if no power budget */
++ continue;
++
+ /* Max power budget per manager */
+ if (pw_budget > 6000000)
+ pw_budget = 6000000;
--- /dev/null
+From a2317231df4b22e6634fe3d8645e7cef848acf49 Mon Sep 17 00:00:00 2001
+From: Piotr Kubik <piotr.kubik@adtran.com>
+Date: Tue, 26 Aug 2025 14:41:58 +0000
+Subject: [PATCH] net: pse-pd: Add Si3474 PSE controller driver
+
+Add a driver for the Skyworks Si3474 I2C Power Sourcing Equipment
+controller.
+
+Driver supports basic features of Si3474 IC:
+- get port status,
+- get port power,
+- get port voltage,
+- enable/disable port power.
+
+Only 4p configurations are supported at this moment.
+
+Signed-off-by: Piotr Kubik <piotr.kubik@adtran.com>
+Reviewed-by: Kory Maincent <kory.maincent@bootlin.com>
+Link: https://patch.msgid.link/9b72c8cd-c8d3-4053-9c80-671b9481d166@adtran.com
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/Kconfig | 11 +
+ drivers/net/pse-pd/Makefile | 1 +
+ drivers/net/pse-pd/si3474.c | 578 ++++++++++++++++++++++++++++++++++++
+ 3 files changed, 590 insertions(+)
+ create mode 100644 drivers/net/pse-pd/si3474.c
+--- a/drivers/net/pse-pd/Kconfig
++++ b/drivers/net/pse-pd/Kconfig
+@@ -32,6 +32,17 @@ config PSE_PD692X0
+ To compile this driver as a module, choose M here: the
+ module will be called pd692x0.
+
++config PSE_SI3474
++ tristate "Si3474 PSE controller"
++ depends on I2C
++ help
++ This module provides support for Si3474 regulator based Ethernet
++ Power Sourcing Equipment.
++ Only 4-pair PSE configurations are supported.
++
++ To compile this driver as a module, choose M here: the
++ module will be called si3474.
++
+ config PSE_TPS23881
+ tristate "TPS23881 PSE controller"
+ depends on I2C
+--- a/drivers/net/pse-pd/Makefile
++++ b/drivers/net/pse-pd/Makefile
+@@ -5,4 +5,5 @@ obj-$(CONFIG_PSE_CONTROLLER) += pse_core
+
+ obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
+ obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
++obj-$(CONFIG_PSE_SI3474) += si3474.o
+ obj-$(CONFIG_PSE_TPS23881) += tps23881.o
+--- /dev/null
++++ b/drivers/net/pse-pd/si3474.c
+@@ -0,0 +1,578 @@
++// SPDX-License-Identifier: GPL-2.0-only
++/*
++ * Driver for the Skyworks Si3474 PoE PSE Controller
++ *
++ * Chip Architecture & Terminology:
++ *
++ * The Si3474 is a single-chip PoE PSE controller managing 8 physical power
++ * delivery channels. Internally, it's structured into two logical "Quads".
++ *
++ * Quad 0: Manages physical channels ('ports' in datasheet) 0, 1, 2, 3
++ * Quad 1: Manages physical channels ('ports' in datasheet) 4, 5, 6, 7
++ *
++ * Each Quad is accessed via a separate I2C address. The base address range is
++ * set by hardware pins A1-A4, and the specific address selects Quad 0 (usually
++ * the lower/even address) or Quad 1 (usually the higher/odd address).
++ * See datasheet Table 2.2 for the address mapping.
++ *
++ * While the Quads manage channel-specific operations, the Si3474 package has
++ * several resources shared across the entire chip:
++ * - Single RESETb input pin.
++ * - Single INTb output pin (signals interrupts from *either* Quad).
++ * - Single OSS input pin (Emergency Shutdown).
++ * - Global I2C Address (0x7F) used for firmware updates.
++ * - Global status monitoring (Temperature, VDD/VPWR Undervoltage Lockout).
++ *
++ * Driver Architecture:
++ *
++ * To handle the mix of per-Quad access and shared resources correctly, this
++ * driver treats the entire Si3474 package as one logical device. The driver
++ * instance associated with the primary I2C address (Quad 0) takes ownership.
++ * It discovers and manages the I2C client for the secondary address (Quad 1).
++ * This primary instance handles shared resources like IRQ management and
++ * registers a single PSE controller device representing all logical PIs.
++ * Internal functions route I2C commands to the appropriate Quad's i2c_client
++ * based on the target channel or PI.
++ *
++ * Terminology Mapping:
++ *
++ * - "PI" (Power Interface): Refers to the logical PSE port as defined by
++ * IEEE 802.3 (typically corresponds to an RJ45 connector). This is the
++ * `id` (0-7) used in the pse_controller_ops.
++ * - "Channel": Refers to one of the 8 physical power control paths within
++ * the Si3474 chip itself (hardware channels 0-7). This terminology is
++ * used internally within the driver to avoid confusion with 'ports'.
++ * - "Quad": One of the two internal 4-channel management units within the
++ * Si3474, each accessed via its own I2C address.
++ *
++ * Relationship:
++ * - A 2-Pair PoE PI uses 1 Channel.
++ * - A 4-Pair PoE PI uses 2 Channels.
++ *
++ * ASCII Schematic:
++ *
++ * +-----------------------------------------------------+
++ * | Si3474 Chip |
++ * | |
++ * | +---------------------+ +---------------------+ |
++ * | | Quad 0 | | Quad 1 | |
++ * | | Channels 0, 1, 2, 3 | | Channels 4, 5, 6, 7 | |
++ * | +----------^----------+ +-------^-------------+ |
++ * | I2C Addr 0 | | I2C Addr 1 |
++ * | +------------------------+ |
++ * | (Primary Driver Instance) (Managed by Primary) |
++ * | |
++ * | Shared Resources (affect whole chip): |
++ * | - Single INTb Output -> Handled by Primary |
++ * | - Single RESETb Input |
++ * | - Single OSS Input -> Handled by Primary |
++ * | - Global I2C Addr (0x7F) for Firmware Update |
++ * | - Global Status (Temp, VDD/VPWR UVLO) |
++ * +-----------------------------------------------------+
++ * | | | | | | | |
++ * Ch0 Ch1 Ch2 Ch3 Ch4 Ch5 Ch6 Ch7 (Physical Channels)
++ *
++ * Example Mapping (Logical PI to Physical Channel(s)):
++ * * 2-Pair Mode (8 PIs):
++ * PI 0 -> Ch 0
++ * PI 1 -> Ch 1
++ * ...
++ * PI 7 -> Ch 7
++ * * 4-Pair Mode (4 PIs):
++ * PI 0 -> Ch 0 + Ch 1 (Managed via Quad 0 Addr)
++ * PI 1 -> Ch 2 + Ch 3 (Managed via Quad 0 Addr)
++ * PI 2 -> Ch 4 + Ch 5 (Managed via Quad 1 Addr)
++ * PI 3 -> Ch 6 + Ch 7 (Managed via Quad 1 Addr)
++ * (Note: Actual mapping depends on Device Tree and PORT_REMAP config)
++ */
++
++#include <linux/i2c.h>
++#include <linux/module.h>
++#include <linux/of.h>
++#include <linux/platform_device.h>
++#include <linux/pse-pd/pse.h>
++
++#define SI3474_MAX_CHANS 8
++
++#define MANUFACTURER_ID 0x08
++#define IC_ID 0x05
++#define SI3474_DEVICE_ID (MANUFACTURER_ID << 3 | IC_ID)
++
++/* Misc registers */
++#define VENDOR_IC_ID_REG 0x1B
++#define TEMPERATURE_REG 0x2C
++#define FIRMWARE_REVISION_REG 0x41
++#define CHIP_REVISION_REG 0x43
++
++/* Main status registers */
++#define POWER_STATUS_REG 0x10
++#define PORT_MODE_REG 0x12
++#define DETECT_CLASS_ENABLE_REG 0x14
++
++/* PORTn Current */
++#define PORT1_CURRENT_LSB_REG 0x30
++
++/* PORTn Current [mA], return in [nA] */
++/* 1000 * ((PORTn_CURRENT_MSB << 8) + PORTn_CURRENT_LSB) / 16384 */
++#define SI3474_NA_STEP (1000 * 1000 * 1000 / 16384)
++
++/* VPWR Voltage */
++#define VPWR_LSB_REG 0x2E
++#define VPWR_MSB_REG 0x2F
++
++/* PORTn Voltage */
++#define PORT1_VOLTAGE_LSB_REG 0x32
++
++/* VPWR Voltage [V], return in [uV] */
++/* 60 * (( VPWR_MSB << 8) + VPWR_LSB) / 16384 */
++#define SI3474_UV_STEP (1000 * 1000 * 60 / 16384)
++
++/* Helper macros */
++#define CHAN_IDX(chan) ((chan) % 4)
++#define CHAN_BIT(chan) BIT(CHAN_IDX(chan))
++#define CHAN_UPPER_BIT(chan) BIT(CHAN_IDX(chan) + 4)
++
++#define CHAN_MASK(chan) (0x03U << (2 * CHAN_IDX(chan)))
++#define CHAN_REG(base, chan) ((base) + (CHAN_IDX(chan) * 4))
++
++struct si3474_pi_desc {
++ u8 chan[2];
++ bool is_4p;
++};
++
++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];
++};
++
++static struct si3474_priv *to_si3474_priv(struct pse_controller_dev *pcdev)
++{
++ return container_of(pcdev, struct si3474_priv, pcdev);
++}
++
++static void si3474_get_channels(struct si3474_priv *priv, int id,
++ u8 *chan0, u8 *chan1)
++{
++ *chan0 = priv->pi[id].chan[0];
++ *chan1 = priv->pi[id].chan[1];
++}
++
++static struct i2c_client *si3474_get_chan_client(struct si3474_priv *priv,
++ u8 chan)
++{
++ return (chan < 4) ? priv->client[0] : priv->client[1];
++}
++
++static int si3474_pi_get_admin_state(struct pse_controller_dev *pcdev, int id,
++ struct pse_admin_state *admin_state)
++{
++ struct si3474_priv *priv = to_si3474_priv(pcdev);
++ struct i2c_client *client;
++ bool is_enabled;
++ u8 chan0, chan1;
++ s32 ret;
++
++ si3474_get_channels(priv, id, &chan0, &chan1);
++ client = si3474_get_chan_client(priv, chan0);
++
++ ret = i2c_smbus_read_byte_data(client, PORT_MODE_REG);
++ if (ret < 0) {
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_UNKNOWN;
++ return ret;
++ }
++
++ is_enabled = ret & (CHAN_MASK(chan0) | CHAN_MASK(chan1));
++
++ if (is_enabled)
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED;
++ else
++ admin_state->c33_admin_state =
++ ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
++
++ return 0;
++}
++
++static int si3474_pi_get_pw_status(struct pse_controller_dev *pcdev, int id,
++ struct pse_pw_status *pw_status)
++{
++ struct si3474_priv *priv = to_si3474_priv(pcdev);
++ struct i2c_client *client;
++ bool delivering;
++ u8 chan0, chan1;
++ s32 ret;
++
++ si3474_get_channels(priv, id, &chan0, &chan1);
++ client = si3474_get_chan_client(priv, chan0);
++
++ ret = i2c_smbus_read_byte_data(client, POWER_STATUS_REG);
++ if (ret < 0) {
++ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_UNKNOWN;
++ return ret;
++ }
++
++ delivering = ret & (CHAN_UPPER_BIT(chan0) | CHAN_UPPER_BIT(chan1));
++
++ if (delivering)
++ pw_status->c33_pw_status =
++ ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
++ else
++ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
++
++ return 0;
++}
++
++static int si3474_get_of_channels(struct si3474_priv *priv)
++{
++ struct pse_pi *pi;
++ u32 chan_id;
++ u8 pi_no;
++ s32 ret;
++
++ for (pi_no = 0; pi_no < SI3474_MAX_CHANS; pi_no++) {
++ pi = &priv->pcdev.pi[pi_no];
++ bool pairset_found = false;
++ u8 pairset_no;
++
++ for (pairset_no = 0; pairset_no < 2; pairset_no++) {
++ if (!pi->pairset[pairset_no].np)
++ continue;
++
++ pairset_found = true;
++
++ ret = of_property_read_u32(pi->pairset[pairset_no].np,
++ "reg", &chan_id);
++ if (ret) {
++ dev_err(&priv->client[0]->dev,
++ "Failed to read channel reg property\n");
++ return ret;
++ }
++ if (chan_id > SI3474_MAX_CHANS) {
++ dev_err(&priv->client[0]->dev,
++ "Incorrect channel number: %d\n", chan_id);
++ return -EINVAL;
++ }
++
++ priv->pi[pi_no].chan[pairset_no] = chan_id;
++ /* Mark as 4-pair if second pairset is present */
++ priv->pi[pi_no].is_4p = (pairset_no == 1);
++ }
++
++ if (pairset_found && !priv->pi[pi_no].is_4p) {
++ dev_err(&priv->client[0]->dev,
++ "Second pairset is missing for PI %pOF, only 4p configs are supported\n",
++ pi->np);
++ return -EINVAL;
++ }
++ }
++
++ return 0;
++}
++
++static int si3474_setup_pi_matrix(struct pse_controller_dev *pcdev)
++{
++ struct si3474_priv *priv = to_si3474_priv(pcdev);
++ s32 ret;
++
++ ret = si3474_get_of_channels(priv);
++ if (ret < 0)
++ dev_warn(&priv->client[0]->dev,
++ "Unable to parse DT PSE power interface matrix\n");
++
++ return ret;
++}
++
++static int si3474_pi_enable(struct pse_controller_dev *pcdev, int id)
++{
++ struct si3474_priv *priv = to_si3474_priv(pcdev);
++ struct i2c_client *client;
++ u8 chan0, chan1;
++ s32 ret;
++ u8 val;
++
++ si3474_get_channels(priv, id, &chan0, &chan1);
++ client = si3474_get_chan_client(priv, chan0);
++
++ /* Release PI from shutdown */
++ ret = i2c_smbus_read_byte_data(client, PORT_MODE_REG);
++ if (ret < 0)
++ return ret;
++
++ val = (u8)ret;
++ val |= CHAN_MASK(chan0);
++ val |= CHAN_MASK(chan1);
++
++ ret = i2c_smbus_write_byte_data(client, PORT_MODE_REG, val);
++ if (ret)
++ return ret;
++
++ /* DETECT_CLASS_ENABLE must be set when using AUTO mode,
++ * otherwise PI does not power up - datasheet section 2.10.2
++ */
++ val = CHAN_BIT(chan0) | CHAN_UPPER_BIT(chan0) |
++ CHAN_BIT(chan1) | CHAN_UPPER_BIT(chan1);
++
++ ret = i2c_smbus_write_byte_data(client, DETECT_CLASS_ENABLE_REG, val);
++ if (ret)
++ return ret;
++
++ return 0;
++}
++
++static int si3474_pi_disable(struct pse_controller_dev *pcdev, int id)
++{
++ struct si3474_priv *priv = to_si3474_priv(pcdev);
++ struct i2c_client *client;
++ u8 chan0, chan1;
++ s32 ret;
++ u8 val;
++
++ si3474_get_channels(priv, id, &chan0, &chan1);
++ client = si3474_get_chan_client(priv, chan0);
++
++ /* Set PI in shutdown mode */
++ ret = i2c_smbus_read_byte_data(client, PORT_MODE_REG);
++ if (ret < 0)
++ return ret;
++
++ val = (u8)ret;
++ val &= ~CHAN_MASK(chan0);
++ val &= ~CHAN_MASK(chan1);
++
++ ret = i2c_smbus_write_byte_data(client, PORT_MODE_REG, val);
++ if (ret)
++ return ret;
++
++ return 0;
++}
++
++static int si3474_pi_get_chan_current(struct si3474_priv *priv, u8 chan)
++{
++ struct i2c_client *client;
++ u64 tmp_64;
++ s32 ret;
++ u8 reg;
++
++ client = si3474_get_chan_client(priv, chan);
++
++ /* Registers 0x30 to 0x3d */
++ reg = CHAN_REG(PORT1_CURRENT_LSB_REG, chan);
++
++ ret = i2c_smbus_read_word_data(client, reg);
++ if (ret < 0)
++ return ret;
++
++ tmp_64 = ret * SI3474_NA_STEP;
++
++ /* uA = nA / 1000 */
++ tmp_64 = DIV_ROUND_CLOSEST_ULL(tmp_64, 1000);
++ return (int)tmp_64;
++}
++
++static int si3474_pi_get_chan_voltage(struct si3474_priv *priv, u8 chan)
++{
++ struct i2c_client *client;
++ s32 ret;
++ u32 val;
++ u8 reg;
++
++ client = si3474_get_chan_client(priv, chan);
++
++ /* Registers 0x32 to 0x3f */
++ reg = CHAN_REG(PORT1_VOLTAGE_LSB_REG, chan);
++
++ ret = i2c_smbus_read_word_data(client, reg);
++ if (ret < 0)
++ return ret;
++
++ val = ret * SI3474_UV_STEP;
++
++ return (int)val;
++}
++
++static int si3474_pi_get_voltage(struct pse_controller_dev *pcdev, int id)
++{
++ struct si3474_priv *priv = to_si3474_priv(pcdev);
++ struct i2c_client *client;
++ u8 chan0, chan1;
++ s32 ret;
++
++ si3474_get_channels(priv, id, &chan0, &chan1);
++ client = si3474_get_chan_client(priv, chan0);
++
++ /* Check which channels are enabled*/
++ ret = i2c_smbus_read_byte_data(client, POWER_STATUS_REG);
++ if (ret < 0)
++ return ret;
++
++ /* Take voltage from the first enabled channel */
++ if (ret & CHAN_BIT(chan0))
++ ret = si3474_pi_get_chan_voltage(priv, chan0);
++ else if (ret & CHAN_BIT(chan1))
++ ret = si3474_pi_get_chan_voltage(priv, chan1);
++ else
++ /* 'should' be no voltage in this case */
++ return 0;
++
++ return ret;
++}
++
++static int si3474_pi_get_actual_pw(struct pse_controller_dev *pcdev, int id)
++{
++ struct si3474_priv *priv = to_si3474_priv(pcdev);
++ u8 chan0, chan1;
++ u32 uV, uA;
++ u64 tmp_64;
++ s32 ret;
++
++ ret = si3474_pi_get_voltage(&priv->pcdev, id);
++
++ /* Do not read currents if voltage is 0 */
++ if (ret <= 0)
++ return ret;
++ uV = ret;
++
++ si3474_get_channels(priv, id, &chan0, &chan1);
++
++ ret = si3474_pi_get_chan_current(priv, chan0);
++ if (ret < 0)
++ return ret;
++ uA = ret;
++
++ ret = si3474_pi_get_chan_current(priv, chan1);
++ if (ret < 0)
++ return ret;
++ uA += ret;
++
++ tmp_64 = uV;
++ tmp_64 *= uA;
++ /* mW = uV * uA / 1000000000 */
++ return DIV_ROUND_CLOSEST_ULL(tmp_64, 1000000000);
++}
++
++static const struct pse_controller_ops si3474_ops = {
++ .setup_pi_matrix = si3474_setup_pi_matrix,
++ .pi_enable = si3474_pi_enable,
++ .pi_disable = si3474_pi_disable,
++ .pi_get_actual_pw = si3474_pi_get_actual_pw,
++ .pi_get_voltage = si3474_pi_get_voltage,
++ .pi_get_admin_state = si3474_pi_get_admin_state,
++ .pi_get_pw_status = si3474_pi_get_pw_status,
++};
++
++static void si3474_ancillary_i2c_remove(void *data)
++{
++ struct i2c_client *client = data;
++
++ i2c_unregister_device(client);
++}
++
++static int si3474_i2c_probe(struct i2c_client *client)
++{
++ struct device *dev = &client->dev;
++ struct si3474_priv *priv;
++ u8 fw_version;
++ s32 ret;
++
++ if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
++ dev_err(dev, "i2c check functionality failed\n");
++ return -ENXIO;
++ }
++
++ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
++ if (!priv)
++ return -ENOMEM;
++
++ ret = i2c_smbus_read_byte_data(client, VENDOR_IC_ID_REG);
++ if (ret < 0)
++ return ret;
++
++ if (ret != SI3474_DEVICE_ID) {
++ dev_err(dev, "Wrong device ID: 0x%x\n", ret);
++ return -ENXIO;
++ }
++
++ ret = i2c_smbus_read_byte_data(client, FIRMWARE_REVISION_REG);
++ if (ret < 0)
++ return ret;
++ fw_version = ret;
++
++ ret = i2c_smbus_read_byte_data(client, CHIP_REVISION_REG);
++ if (ret < 0)
++ return ret;
++
++ dev_dbg(dev, "Chip revision: 0x%x, firmware version: 0x%x\n",
++ ret, fw_version);
++
++ priv->client[0] = client;
++ i2c_set_clientdata(client, priv);
++
++ priv->client[1] = i2c_new_ancillary_device(priv->client[0], "secondary",
++ priv->client[0]->addr + 1);
++ if (IS_ERR(priv->client[1]))
++ return PTR_ERR(priv->client[1]);
++
++ ret = devm_add_action_or_reset(dev, si3474_ancillary_i2c_remove, priv->client[1]);
++ if (ret < 0) {
++ dev_err(&priv->client[1]->dev, "Cannot register remove callback\n");
++ return ret;
++ }
++
++ ret = i2c_smbus_read_byte_data(priv->client[1], VENDOR_IC_ID_REG);
++ if (ret < 0) {
++ dev_err(&priv->client[1]->dev, "Cannot access secondary PSE controller\n");
++ return ret;
++ }
++
++ if (ret != SI3474_DEVICE_ID) {
++ dev_err(&priv->client[1]->dev,
++ "Wrong device ID for secondary PSE controller: 0x%x\n", ret);
++ return -ENXIO;
++ }
++
++ priv->np = dev->of_node;
++ priv->pcdev.owner = THIS_MODULE;
++ priv->pcdev.ops = &si3474_ops;
++ priv->pcdev.dev = dev;
++ priv->pcdev.types = ETHTOOL_PSE_C33;
++ priv->pcdev.nr_lines = SI3474_MAX_CHANS;
++
++ ret = devm_pse_controller_register(dev, &priv->pcdev);
++ if (ret) {
++ dev_err(dev, "Failed to register PSE controller: 0x%x\n", ret);
++ return ret;
++ }
++
++ return 0;
++}
++
++static const struct i2c_device_id si3474_id[] = {
++ { "si3474" },
++ {}
++};
++MODULE_DEVICE_TABLE(i2c, si3474_id);
++
++static const struct of_device_id si3474_of_match[] = {
++ {
++ .compatible = "skyworks,si3474",
++ },
++ {},
++};
++MODULE_DEVICE_TABLE(of, si3474_of_match);
++
++static struct i2c_driver si3474_driver = {
++ .probe = si3474_i2c_probe,
++ .id_table = si3474_id,
++ .driver = {
++ .name = "si3474",
++ .of_match_table = si3474_of_match,
++ },
++};
++module_i2c_driver(si3474_driver);
++
++MODULE_AUTHOR("Piotr Kubik <piotr.kubik@adtran.com>");
++MODULE_DESCRIPTION("Skyworks Si3474 PoE PSE Controller driver");
++MODULE_LICENSE("GPL");
--- /dev/null
+From 2c95a756e0cfc19af6d0b32b0c6cf3bada334998 Mon Sep 17 00:00:00 2001
+From: Thomas Wismer <thomas.wismer@scs.ch>
+Date: Mon, 6 Oct 2025 22:40:29 +0200
+Subject: [PATCH] net: pse-pd: tps23881: Fix current measurement scaling
+
+The TPS23881 improves on the TPS23880 with current sense resistors reduced
+from 255 mOhm to 200 mOhm. This has a direct impact on the scaling of the
+current measurement. However, the latest TPS23881 data sheet from May 2023
+still shows the scaling of the TPS23880 model.
+
+Fixes: 7f076ce3f1733 ("net: pse-pd: tps23881: Add support for power limit and measurement features")
+Signed-off-by: Thomas Wismer <thomas.wismer@scs.ch>
+Acked-by: Kory Maincent <kory.maincent@bootlin.com>
+Link: https://patch.msgid.link/20251006204029.7169-2-thomas@wismer.xyz
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/tps23881.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -62,7 +62,7 @@
+ #define TPS23881_REG_SRAM_DATA 0x61
+
+ #define TPS23881_UV_STEP 3662
+-#define TPS23881_NA_STEP 70190
++#define TPS23881_NA_STEP 89500
+ #define TPS23881_MW_STEP 500
+ #define TPS23881_MIN_PI_PW_LIMIT_MW 2000
+
--- /dev/null
+From f197902cd21ae833850679b216bb62c0d056bbb3 Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Mon, 13 Oct 2025 16:05:31 +0200
+Subject: [PATCH] net: pse-pd: pd692x0: Replace __free macro with explicit
+ kfree calls
+
+Replace __free(kfree) with explicit kfree() calls to follow the net
+subsystem policy of avoiding automatic cleanup macros as described in
+the documentation.
+
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Reviewed-by: Simon Horman <horms@kernel.org>
+Link: https://patch.msgid.link/20251013-feature_pd692x0_reboot_keep_conf-v2-1-68ab082a93dd@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 7 +++++--
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -1200,9 +1200,9 @@ static void pd692x0_managers_free_pw_bud
+
+ static int pd692x0_setup_pi_matrix(struct pse_controller_dev *pcdev)
+ {
+- struct pd692x0_manager *manager __free(kfree) = NULL;
+ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
+ struct pd692x0_matrix port_matrix[PD692X0_MAX_PIS];
++ struct pd692x0_manager *manager;
+ int ret, nmanagers;
+
+ /* Should we flash the port matrix */
+@@ -1216,7 +1216,7 @@ static int pd692x0_setup_pi_matrix(struc
+
+ ret = pd692x0_of_get_managers(priv, manager);
+ if (ret < 0)
+- return ret;
++ goto err_free_manager;
+
+ nmanagers = ret;
+ ret = pd692x0_register_managers_regulator(priv, manager, nmanagers);
+@@ -1236,12 +1236,15 @@ static int pd692x0_setup_pi_matrix(struc
+ goto err_managers_req_pw;
+
+ pd692x0_of_put_managers(priv, manager, nmanagers);
++ kfree(manager);
+ return 0;
+
+ err_managers_req_pw:
+ pd692x0_managers_free_pw_budget(priv);
+ err_of_managers:
+ pd692x0_of_put_managers(priv, manager, nmanagers);
++err_free_manager:
++ kfree(manager);
+ return ret;
+ }
+
--- /dev/null
+From 6fa1f8b64a47edd7d8420d8fd1008507aee2853e Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Mon, 13 Oct 2025 16:05:32 +0200
+Subject: [PATCH] net: pse-pd: pd692x0: Separate configuration parsing from
+ hardware setup
+
+Cache the port matrix configuration in driver private data to enable
+PSE controller reconfiguration. This refactoring separates device tree
+parsing from hardware configuration application, allowing settings to be
+reapplied without reparsing the device tree.
+
+This refactoring is a prerequisite for preserving PSE configuration
+across reboots to prevent power disruption to connected devices.
+
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Reviewed-by: Simon Horman <horms@kernel.org>
+Link: https://patch.msgid.link/20251013-feature_pd692x0_reboot_keep_conf-v2-2-68ab082a93dd@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 115 +++++++++++++++++++++++------------
+ 1 file changed, 76 insertions(+), 39 deletions(-)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -85,6 +85,11 @@ enum {
+ PD692X0_MSG_CNT
+ };
+
++struct pd692x0_matrix {
++ u8 hw_port_a;
++ u8 hw_port_b;
++};
++
+ struct pd692x0_priv {
+ struct i2c_client *client;
+ struct pse_controller_dev pcdev;
+@@ -101,6 +106,8 @@ struct pd692x0_priv {
+ enum ethtool_c33_pse_admin_state admin_state[PD692X0_MAX_PIS];
+ struct regulator_dev *manager_reg[PD692X0_MAX_MANAGERS];
+ int manager_pw_budget[PD692X0_MAX_MANAGERS];
++ int nmanagers;
++ struct pd692x0_matrix *port_matrix;
+ };
+
+ /* Template list of communication messages. The non-null bytes defined here
+@@ -809,11 +816,6 @@ struct pd692x0_manager {
+ int nports;
+ };
+
+-struct pd692x0_matrix {
+- u8 hw_port_a;
+- u8 hw_port_b;
+-};
+-
+ static int
+ pd692x0_of_get_ports_manager(struct pd692x0_priv *priv,
+ struct pd692x0_manager *manager,
+@@ -903,7 +905,8 @@ pd692x0_of_get_managers(struct pd692x0_p
+ }
+
+ of_node_put(managers_node);
+- return nmanagers;
++ priv->nmanagers = nmanagers;
++ return 0;
+
+ out:
+ for (i = 0; i < nmanagers; i++) {
+@@ -963,8 +966,7 @@ pd692x0_register_manager_regulator(struc
+
+ static int
+ pd692x0_register_managers_regulator(struct pd692x0_priv *priv,
+- const struct pd692x0_manager *manager,
+- int nmanagers)
++ const struct pd692x0_manager *manager)
+ {
+ struct device *dev = &priv->client->dev;
+ size_t reg_name_len;
+@@ -975,7 +977,7 @@ pd692x0_register_managers_regulator(stru
+ */
+ reg_name_len = strlen(dev_name(dev)) + 23;
+
+- for (i = 0; i < nmanagers; i++) {
++ for (i = 0; i < priv->nmanagers; i++) {
+ static const char * const regulators[] = { "vaux5", "vaux3p3" };
+ struct regulator_dev *rdev;
+ char *reg_name;
+@@ -1008,10 +1010,14 @@ pd692x0_register_managers_regulator(stru
+ }
+
+ static int
+-pd692x0_conf_manager_power_budget(struct pd692x0_priv *priv, int id, int pw)
++pd692x0_conf_manager_power_budget(struct pd692x0_priv *priv, int id)
+ {
+ struct pd692x0_msg msg, buf;
+- int ret, pw_mW = pw / 1000;
++ int ret, pw_mW;
++
++ pw_mW = priv->manager_pw_budget[id] / 1000;
++ if (!pw_mW)
++ return 0;
+
+ msg = pd692x0_msg_template_list[PD692X0_MSG_GET_POWER_BANK];
+ msg.data[0] = id;
+@@ -1032,11 +1038,11 @@ pd692x0_conf_manager_power_budget(struct
+ }
+
+ static int
+-pd692x0_configure_managers(struct pd692x0_priv *priv, int nmanagers)
++pd692x0_req_managers_pw_budget(struct pd692x0_priv *priv)
+ {
+ int i, ret;
+
+- for (i = 0; i < nmanagers; i++) {
++ for (i = 0; i < priv->nmanagers; i++) {
+ struct regulator *supply = priv->manager_reg[i]->supply;
+ int pw_budget;
+
+@@ -1053,7 +1059,18 @@ pd692x0_configure_managers(struct pd692x
+ return ret;
+
+ priv->manager_pw_budget[i] = pw_budget;
+- ret = pd692x0_conf_manager_power_budget(priv, i, pw_budget);
++ }
++
++ return 0;
++}
++
++static int
++pd692x0_configure_managers(struct pd692x0_priv *priv)
++{
++ int i, ret;
++
++ for (i = 0; i < priv->nmanagers; i++) {
++ ret = pd692x0_conf_manager_power_budget(priv, i);
+ if (ret < 0)
+ return ret;
+ }
+@@ -1101,10 +1118,9 @@ pd692x0_set_port_matrix(const struct pse
+
+ static int
+ pd692x0_set_ports_matrix(struct pd692x0_priv *priv,
+- const struct pd692x0_manager *manager,
+- int nmanagers,
+- struct pd692x0_matrix port_matrix[PD692X0_MAX_PIS])
++ const struct pd692x0_manager *manager)
+ {
++ struct pd692x0_matrix *port_matrix = priv->port_matrix;
+ struct pse_controller_dev *pcdev = &priv->pcdev;
+ int i, ret;
+
+@@ -1117,7 +1133,7 @@ pd692x0_set_ports_matrix(struct pd692x0_
+ /* Update with values for every PSE PIs */
+ for (i = 0; i < pcdev->nr_lines; i++) {
+ ret = pd692x0_set_port_matrix(&pcdev->pi[i].pairset[0],
+- manager, nmanagers,
++ manager, priv->nmanagers,
+ &port_matrix[i]);
+ if (ret) {
+ dev_err(&priv->client->dev,
+@@ -1126,7 +1142,7 @@ pd692x0_set_ports_matrix(struct pd692x0_
+ }
+
+ ret = pd692x0_set_port_matrix(&pcdev->pi[i].pairset[1],
+- manager, nmanagers,
++ manager, priv->nmanagers,
+ &port_matrix[i]);
+ if (ret) {
+ dev_err(&priv->client->dev,
+@@ -1139,9 +1155,9 @@ pd692x0_set_ports_matrix(struct pd692x0_
+ }
+
+ static int
+-pd692x0_write_ports_matrix(struct pd692x0_priv *priv,
+- const struct pd692x0_matrix port_matrix[PD692X0_MAX_PIS])
++pd692x0_write_ports_matrix(struct pd692x0_priv *priv)
+ {
++ struct pd692x0_matrix *port_matrix = priv->port_matrix;
+ struct pd692x0_msg msg, buf;
+ int ret, i;
+
+@@ -1166,13 +1182,32 @@ pd692x0_write_ports_matrix(struct pd692x
+ return 0;
+ }
+
++static int pd692x0_hw_conf_init(struct pd692x0_priv *priv)
++{
++ int ret;
++
++ /* Is PD692x0 ready to be configured? */
++ if (priv->fw_state != PD692X0_FW_OK &&
++ priv->fw_state != PD692X0_FW_COMPLETE)
++ return 0;
++
++ ret = pd692x0_configure_managers(priv);
++ if (ret)
++ return ret;
++
++ ret = pd692x0_write_ports_matrix(priv);
++ if (ret)
++ return ret;
++
++ return 0;
++}
++
+ static void pd692x0_of_put_managers(struct pd692x0_priv *priv,
+- struct pd692x0_manager *manager,
+- int nmanagers)
++ struct pd692x0_manager *manager)
+ {
+ int i, j;
+
+- for (i = 0; i < nmanagers; i++) {
++ for (i = 0; i < priv->nmanagers; i++) {
+ for (j = 0; j < manager[i].nports; j++)
+ of_node_put(manager[i].port_node[j]);
+ of_node_put(manager[i].node);
+@@ -1201,48 +1236,50 @@ static void pd692x0_managers_free_pw_bud
+ static int pd692x0_setup_pi_matrix(struct pse_controller_dev *pcdev)
+ {
+ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
+- struct pd692x0_matrix port_matrix[PD692X0_MAX_PIS];
++ struct pd692x0_matrix *port_matrix;
+ struct pd692x0_manager *manager;
+- int ret, nmanagers;
+-
+- /* Should we flash the port matrix */
+- if (priv->fw_state != PD692X0_FW_OK &&
+- priv->fw_state != PD692X0_FW_COMPLETE)
+- return 0;
++ int ret;
+
+ manager = kcalloc(PD692X0_MAX_MANAGERS, sizeof(*manager), GFP_KERNEL);
+ if (!manager)
+ return -ENOMEM;
+
++ port_matrix = devm_kcalloc(&priv->client->dev, PD692X0_MAX_PIS,
++ sizeof(*port_matrix), GFP_KERNEL);
++ if (!port_matrix) {
++ ret = -ENOMEM;
++ goto err_free_manager;
++ }
++ priv->port_matrix = port_matrix;
++
+ ret = pd692x0_of_get_managers(priv, manager);
+ if (ret < 0)
+ goto err_free_manager;
+
+- nmanagers = ret;
+- ret = pd692x0_register_managers_regulator(priv, manager, nmanagers);
++ ret = pd692x0_register_managers_regulator(priv, manager);
+ if (ret)
+ goto err_of_managers;
+
+- ret = pd692x0_configure_managers(priv, nmanagers);
++ ret = pd692x0_req_managers_pw_budget(priv);
+ if (ret)
+ goto err_of_managers;
+
+- ret = pd692x0_set_ports_matrix(priv, manager, nmanagers, port_matrix);
++ ret = pd692x0_set_ports_matrix(priv, manager);
+ if (ret)
+ goto err_managers_req_pw;
+
+- ret = pd692x0_write_ports_matrix(priv, port_matrix);
++ ret = pd692x0_hw_conf_init(priv);
+ if (ret)
+ goto err_managers_req_pw;
+
+- pd692x0_of_put_managers(priv, manager, nmanagers);
++ pd692x0_of_put_managers(priv, manager);
+ kfree(manager);
+ return 0;
+
+ err_managers_req_pw:
+ pd692x0_managers_free_pw_budget(priv);
+ err_of_managers:
+- pd692x0_of_put_managers(priv, manager, nmanagers);
++ pd692x0_of_put_managers(priv, manager);
+ err_free_manager:
+ kfree(manager);
+ return ret;
+@@ -1647,7 +1684,7 @@ static enum fw_upload_err pd692x0_fw_pol
+ return FW_UPLOAD_ERR_FW_INVALID;
+ }
+
+- ret = pd692x0_setup_pi_matrix(&priv->pcdev);
++ ret = pd692x0_hw_conf_init(priv);
+ if (ret < 0) {
+ dev_err(&client->dev, "Error configuring ports matrix (%pe)\n",
+ ERR_PTR(ret));
--- /dev/null
+From 8f3d044b34fe99b894046edb84605456195cabc0 Mon Sep 17 00:00:00 2001
+From: "Kory Maincent (Dent Project)" <kory.maincent@bootlin.com>
+Date: Mon, 13 Oct 2025 16:05:33 +0200
+Subject: [PATCH] net: pse-pd: pd692x0: Preserve PSE configuration across
+ reboots
+
+Detect when PSE hardware is already configured (user byte == 42) and
+skip hardware initialization to prevent power interruption to connected
+devices during system reboots.
+
+Previously, the driver would always reconfigure the PSE hardware on
+probe, causing a port matrix reflash that resulted in temporary power
+loss to all connected devices. This change maintains power continuity
+by preserving existing configuration when the PSE has been previously
+initialized.
+
+Signed-off-by: Kory Maincent <kory.maincent@bootlin.com>
+Reviewed-by: Simon Horman <horms@kernel.org>
+Link: https://patch.msgid.link/20251013-feature_pd692x0_reboot_keep_conf-v2-3-68ab082a93dd@bootlin.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/pd692x0.c | 35 ++++++++++++++++++++++++++++++++---
+ 1 file changed, 32 insertions(+), 3 deletions(-)
+
+--- a/drivers/net/pse-pd/pd692x0.c
++++ b/drivers/net/pse-pd/pd692x0.c
+@@ -30,6 +30,8 @@
+ #define PD692X0_FW_MIN_VER 5
+ #define PD692X0_FW_PATCH_VER 5
+
++#define PD692X0_USER_BYTE 42
++
+ enum pd692x0_fw_state {
+ PD692X0_FW_UNKNOWN,
+ PD692X0_FW_OK,
+@@ -80,6 +82,7 @@ enum {
+ PD692X0_MSG_GET_PORT_PARAM,
+ PD692X0_MSG_GET_POWER_BANK,
+ PD692X0_MSG_SET_POWER_BANK,
++ PD692X0_MSG_SET_USER_BYTE,
+
+ /* add new message above here */
+ PD692X0_MSG_CNT
+@@ -103,6 +106,7 @@ struct pd692x0_priv {
+ bool last_cmd_key;
+ unsigned long last_cmd_key_time;
+
++ bool cfg_saved;
+ enum ethtool_c33_pse_admin_state admin_state[PD692X0_MAX_PIS];
+ struct regulator_dev *manager_reg[PD692X0_MAX_MANAGERS];
+ int manager_pw_budget[PD692X0_MAX_MANAGERS];
+@@ -193,6 +197,12 @@ static const struct pd692x0_msg pd692x0_
+ .key = PD692X0_KEY_CMD,
+ .sub = {0x07, 0x0b, 0x57},
+ },
++ [PD692X0_MSG_SET_USER_BYTE] = {
++ .key = PD692X0_KEY_PRG,
++ .sub = {0x41, PD692X0_USER_BYTE},
++ .data = {0x4e, 0x4e, 0x4e, 0x4e,
++ 0x4e, 0x4e, 0x4e, 0x4e},
++ },
+ };
+
+ static u8 pd692x0_build_msg(struct pd692x0_msg *msg, u8 echo)
+@@ -1233,6 +1243,15 @@ static void pd692x0_managers_free_pw_bud
+ }
+ }
+
++static int
++pd692x0_save_user_byte(struct pd692x0_priv *priv)
++{
++ struct pd692x0_msg msg, buf;
++
++ msg = pd692x0_msg_template_list[PD692X0_MSG_SET_USER_BYTE];
++ return pd692x0_sendrecv_msg(priv, &msg, &buf);
++}
++
+ static int pd692x0_setup_pi_matrix(struct pse_controller_dev *pcdev)
+ {
+ struct pd692x0_priv *priv = to_pd692x0_priv(pcdev);
+@@ -1268,9 +1287,16 @@ static int pd692x0_setup_pi_matrix(struc
+ if (ret)
+ goto err_managers_req_pw;
+
+- ret = pd692x0_hw_conf_init(priv);
+- if (ret)
+- goto err_managers_req_pw;
++ /* Do not init the conf if it is already saved */
++ if (!priv->cfg_saved) {
++ ret = pd692x0_hw_conf_init(priv);
++ if (ret)
++ goto err_managers_req_pw;
++
++ ret = pd692x0_save_user_byte(priv);
++ if (ret)
++ goto err_managers_req_pw;
++ }
+
+ pd692x0_of_put_managers(priv, manager);
+ kfree(manager);
+@@ -1793,6 +1819,9 @@ static int pd692x0_i2c_probe(struct i2c_
+ }
+ }
+
++ if (buf.data[2] == PD692X0_USER_BYTE)
++ priv->cfg_saved = true;
++
+ priv->np = dev->of_node;
+ priv->pcdev.nr_lines = PD692X0_MAX_PIS;
+ priv->pcdev.owner = THIS_MODULE;
--- /dev/null
+From 4d07797faaa19aa8e80e10a04ca1a72c643ef5cf Mon Sep 17 00:00:00 2001
+From: Thomas Wismer <thomas.wismer@scs.ch>
+Date: Wed, 29 Oct 2025 22:23:09 +0100
+Subject: [PATCH] net: pse-pd: tps23881: Add support for TPS23881B
+
+The TPS23881B uses different firmware than the TPS23881. Trying to load the
+TPS23881 firmware on a TPS23881B device fails and must be omitted.
+
+The TPS23881B ships with a more recent ROM firmware. Moreover, no updated
+firmware has been released yet and so the firmware loading step must be
+skipped. As of today, the TPS23881B is intended to use its ROM firmware.
+
+Signed-off-by: Thomas Wismer <thomas.wismer@scs.ch>
+Reviewed-by: Kory Maincent <kory.maincent@bootlin.com>
+Acked-by: Oleksij Rempel <o.rempel@pengutronix.de>
+Link: https://patch.msgid.link/20251029212312.108749-2-thomas@wismer.xyz
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ drivers/net/pse-pd/tps23881.c | 69 +++++++++++++++++++++++++++--------
+ 1 file changed, 54 insertions(+), 15 deletions(-)
+
+--- a/drivers/net/pse-pd/tps23881.c
++++ b/drivers/net/pse-pd/tps23881.c
+@@ -55,8 +55,6 @@
+ #define TPS23881_REG_TPON BIT(0)
+ #define TPS23881_REG_FWREV 0x41
+ #define TPS23881_REG_DEVID 0x43
+-#define TPS23881_REG_DEVID_MASK 0xF0
+-#define TPS23881_DEVICE_ID 0x02
+ #define TPS23881_REG_CHAN1_CLASS 0x4c
+ #define TPS23881_REG_SRAM_CTRL 0x60
+ #define TPS23881_REG_SRAM_DATA 0x61
+@@ -1012,8 +1010,28 @@ static const struct pse_controller_ops t
+ .pi_get_pw_req = tps23881_pi_get_pw_req,
+ };
+
+-static const char fw_parity_name[] = "ti/tps23881/tps23881-parity-14.bin";
+-static const char fw_sram_name[] = "ti/tps23881/tps23881-sram-14.bin";
++struct tps23881_info {
++ u8 dev_id; /* device ID and silicon revision */
++ const char *fw_parity_name; /* parity code firmware file name */
++ const char *fw_sram_name; /* SRAM code firmware file name */
++};
++
++enum tps23881_model {
++ TPS23881,
++ TPS23881B,
++};
++
++static const struct tps23881_info tps23881_info[] = {
++ [TPS23881] = {
++ .dev_id = 0x22,
++ .fw_parity_name = "ti/tps23881/tps23881-parity-14.bin",
++ .fw_sram_name = "ti/tps23881/tps23881-sram-14.bin",
++ },
++ [TPS23881B] = {
++ .dev_id = 0x24,
++ /* skip SRAM load, ROM provides Clause 145 hardware-level support */
++ },
++};
+
+ struct tps23881_fw_conf {
+ u8 reg;
+@@ -1085,16 +1103,17 @@ out:
+ return ret;
+ }
+
+-static int tps23881_flash_sram_fw(struct i2c_client *client)
++static int tps23881_flash_sram_fw(struct i2c_client *client,
++ const struct tps23881_info *info)
+ {
+ int ret;
+
+- ret = tps23881_flash_sram_fw_part(client, fw_parity_name,
++ ret = tps23881_flash_sram_fw_part(client, info->fw_parity_name,
+ tps23881_fw_parity_conf);
+ if (ret)
+ return ret;
+
+- ret = tps23881_flash_sram_fw_part(client, fw_sram_name,
++ ret = tps23881_flash_sram_fw_part(client, info->fw_sram_name,
+ tps23881_fw_sram_conf);
+ if (ret)
+ return ret;
+@@ -1412,6 +1431,7 @@ static int tps23881_setup_irq(struct tps
+ static int tps23881_i2c_probe(struct i2c_client *client)
+ {
+ struct device *dev = &client->dev;
++ const struct tps23881_info *info;
+ struct tps23881_priv *priv;
+ struct gpio_desc *reset;
+ int ret;
+@@ -1422,6 +1442,10 @@ static int tps23881_i2c_probe(struct i2c
+ return -ENXIO;
+ }
+
++ info = i2c_get_match_data(client);
++ if (!info)
++ return -EINVAL;
++
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+@@ -1440,7 +1464,7 @@ static int tps23881_i2c_probe(struct i2c
+ * to Load TPS2388x SRAM and Parity Code over I2C" (Rev E))
+ * indicates we should delay that programming by at least 50ms. So
+ * we'll wait the entire 50ms here to ensure we're safe to go to the
+- * SRAM loading proceedure.
++ * SRAM loading procedure.
+ */
+ msleep(50);
+ }
+@@ -1449,20 +1473,27 @@ static int tps23881_i2c_probe(struct i2c
+ if (ret < 0)
+ return ret;
+
+- if (FIELD_GET(TPS23881_REG_DEVID_MASK, ret) != TPS23881_DEVICE_ID) {
++ if (ret != info->dev_id) {
+ dev_err(dev, "Wrong device ID\n");
+ return -ENXIO;
+ }
+
+- ret = tps23881_flash_sram_fw(client);
+- if (ret < 0)
+- return ret;
++ if (info->fw_sram_name) {
++ ret = tps23881_flash_sram_fw(client, info);
++ if (ret < 0)
++ return ret;
++ }
+
+ ret = i2c_smbus_read_byte_data(client, TPS23881_REG_FWREV);
+ if (ret < 0)
+ return ret;
+
+- dev_info(&client->dev, "Firmware revision 0x%x\n", ret);
++ if (ret == 0xFF) {
++ dev_err(&client->dev, "Device entered safe mode\n");
++ return -ENXIO;
++ }
++ dev_info(&client->dev, "Firmware revision 0x%x%s\n", ret,
++ ret == 0x00 ? " (ROM firmware)" : "");
+
+ /* Set configuration B, 16 bit access on a single device address */
+ ret = i2c_smbus_read_byte_data(client, TPS23881_REG_GEN_MASK);
+@@ -1498,13 +1529,21 @@ static int tps23881_i2c_probe(struct i2c
+ }
+
+ static const struct i2c_device_id tps23881_id[] = {
+- { "tps23881" },
++ { "tps23881", .driver_data = (kernel_ulong_t)&tps23881_info[TPS23881] },
++ { "tps23881b", .driver_data = (kernel_ulong_t)&tps23881_info[TPS23881B] },
+ { }
+ };
+ MODULE_DEVICE_TABLE(i2c, tps23881_id);
+
+ static const struct of_device_id tps23881_of_match[] = {
+- { .compatible = "ti,tps23881", },
++ {
++ .compatible = "ti,tps23881",
++ .data = &tps23881_info[TPS23881]
++ },
++ {
++ .compatible = "ti,tps23881b",
++ .data = &tps23881_info[TPS23881B]
++ },
+ { },
+ };
+ MODULE_DEVICE_TABLE(of, tps23881_of_match);
--- /dev/null
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: OpenWrt PSE-PD Backport <openwrt@openwrt.org>
+Date: Fri, 31 Jan 2025 00:00:00 +0000
+Subject: [PATCH] net: pse-pd: Add ethtool netlink definitions for PSE events
+
+GENERATED PATCH - OpenWrt PSE-PD Backport
+
+This patch:
+1. Creates include/uapi/linux/ethtool_netlink_generated.h with PSE event
+ definitions (in mainline 6.17+, this file is auto-generated)
+2. Adds ETHTOOL_MSG_PSE_NTF to the kernel message types enum
+
+These definitions are required by PSE event reporting patches.
+
+Signed-off-by: OpenWrt PSE-PD Backport <openwrt@openwrt.org>
+Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
+---
+ include/uapi/linux/ethtool_netlink.h | 1 +
+ include/uapi/linux/ethtool_netlink_generated.h | 45 ++++++++++++++++++
+ 2 files changed, 46 insertions(+)
+ create mode 100644 include/uapi/linux/ethtool_netlink_generated.h
+
+--- a/include/uapi/linux/ethtool_netlink.h
++++ b/include/uapi/linux/ethtool_netlink.h
+@@ -115,6 +115,7 @@ enum {
+ ETHTOOL_MSG_PHY_GET_REPLY,
+ ETHTOOL_MSG_PHY_NTF,
+
++ ETHTOOL_MSG_PSE_NTF,
+ /* add new constants above here */
+ __ETHTOOL_MSG_KERNEL_CNT,
+ ETHTOOL_MSG_KERNEL_MAX = __ETHTOOL_MSG_KERNEL_CNT - 1
+--- /dev/null
++++ b/include/uapi/linux/ethtool_netlink_generated.h
+@@ -0,0 +1,45 @@
++/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
++/*
++ * Minimal PSE ethtool netlink definitions - OpenWrt backport for 6.12.67
++ * In mainline 6.17+, this file is auto-generated from:
++ * Documentation/netlink/specs/ethtool.yaml
++ *
++ * GENERATED PATCH - OpenWrt PSE-PD Backport
++ */
++
++#ifndef _UAPI_LINUX_ETHTOOL_NETLINK_GENERATED_H
++#define _UAPI_LINUX_ETHTOOL_NETLINK_GENERATED_H
++
++/**
++ * enum ethtool_pse_event - PSE event list for the PSE controller
++ * @ETHTOOL_PSE_EVENT_OVER_CURRENT: PSE output current is too high
++ * @ETHTOOL_PSE_EVENT_OVER_TEMP: PSE in over temperature state
++ * @ETHTOOL_C33_PSE_EVENT_DETECTION: detection process occur on the PSE
++ * @ETHTOOL_C33_PSE_EVENT_CLASSIFICATION: classification process occur
++ * @ETHTOOL_C33_PSE_EVENT_DISCONNECTION: PD has been disconnected
++ * @ETHTOOL_PSE_EVENT_OVER_BUDGET: PSE turned off due to over budget
++ * @ETHTOOL_PSE_EVENT_SW_PW_CONTROL_ERROR: PSE power control error
++ */
++enum ethtool_pse_event {
++ ETHTOOL_PSE_EVENT_OVER_CURRENT = 1,
++ ETHTOOL_PSE_EVENT_OVER_TEMP = 2,
++ ETHTOOL_C33_PSE_EVENT_DETECTION = 4,
++ ETHTOOL_C33_PSE_EVENT_CLASSIFICATION = 8,
++ ETHTOOL_C33_PSE_EVENT_DISCONNECTION = 16,
++ ETHTOOL_PSE_EVENT_OVER_BUDGET = 32,
++ ETHTOOL_PSE_EVENT_SW_PW_CONTROL_ERROR = 64,
++};
++
++/* PSE notification attributes */
++enum {
++ ETHTOOL_A_PSE_NTF_HEADER = 1,
++ ETHTOOL_A_PSE_NTF_EVENTS,
++
++ __ETHTOOL_A_PSE_NTF_CNT,
++ ETHTOOL_A_PSE_NTF_MAX = (__ETHTOOL_A_PSE_NTF_CNT - 1)
++};
++
++/* PSE power domain ID attribute - value 14 in the ETHTOOL_A_PSE_* enum */
++#define ETHTOOL_A_PSE_PW_D_ID 14
++
++#endif /* _UAPI_LINUX_ETHTOOL_NETLINK_GENERATED_H */
--- a/drivers/net/mdio/fwnode_mdio.c
+++ b/drivers/net/mdio/fwnode_mdio.c
-@@ -89,7 +89,7 @@ int fwnode_mdiobus_phy_device_register(s
+@@ -90,7 +90,7 @@ int fwnode_mdiobus_phy_device_register(s
}
if (fwnode_property_read_bool(child, "broken-turn-around"))