AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/07/27 01:15 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "PSE_CONTROLLER",
    "PSE_REALTEK_MCU_I2C",
    "PSE_REALTEK_MCU_UART",
    "SERIAL_DEV_BUS",
    "I2C"
  ],
  "FocusSymbols": [
    "rtpse_mcu_register",
    "rtpse_mcu_uart_probe",
    "rtpse_mcu_i2c_probe",
    "rtpse_mcu_port_get_admin_state",
    "rtpse_mcu_port_get_pw_status",
    "rtpse_mcu_port_get_pw_class",
    "rtpse_mcu_port_get_actual_pw",
    "rtpse_mcu_port_get_voltage",
    "rtpse_mcu_port_enable",
    "rtpse_mcu_port_disable",
    "rtpse_mcu_port_get_pw_limit",
    "rtpse_mcu_port_set_pw_limit",
    "rtpse_mcu_port_get_pw_limit_ranges",
    "rtpse_mcu_port_get_prio",
    "rtpse_mcu_port_set_prio",
    "rtpse_mcu_uart_receive"
  ],
  "Reasoning": "This patch introduces a new driver for the Realtek PSE MCU, supporting both I2C and UART transports. The driver exposes PSE controller operations to userspace via ethtool netlink and parses data received from the device over I2C/UART. It can be probed in virtual environments via device tree overlays or I2C sysfs, making it reachable and suitable for fuzzing.",
  "WorthFuzzing": true
}

1/1 2026/07/27 01:15 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit b86c21662059da481e719ab9adaeae877a0681e2\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Mon Jul 27 01:15:15 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml\nnew file mode 100644\nindex 00000000000000..0f1a819db49bbc\n--- /dev/null\n+++ b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml\n@@ -0,0 +1,180 @@\n+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)\n+%YAML 1.2\n+---\n+$id: http://devicetree.org/schemas/net/pse-pd/realtek,pse-mcu-gen1.yaml#\n+$schema: http://devicetree.org/meta-schemas/core.yaml#\n+\n+title: Realtek PSE MCU\n+\n+maintainers:\n+  - Jonas Jelonek \u003cjelonek.jonas@gmail.com\u003e\n+\n+description: |\n+  A microcontroller (MCU) that manages the PSE (Power Sourcing Equipment)\n+  hardware on a range of managed PoE switches. The host CPU talks only to\n+  this MCU - over I2C/SMBus or UART - using a small message-based protocol;\n+  the PSE silicon it drives sits behind the MCU and is never accessed\n+  directly. For example, on the Zyxel GS1900-10HP the SoC reaches the MCU\n+  over UART, and the MCU manages the on-board PSE chip.\n+\n+  This binding describes the MCU together with its Realtek firmware: the\n+  firmware and its host protocol, which are stable across boards. The\n+  microcontroller silicon is a general-purpose part that varies, and the\n+  PSE silicon behind the MCU (Realtek RTL823x/RTL8239* or Broadcom\n+  BCM59xxx) is reported by the MCU and detected at runtime - neither is\n+  named here.\n+\n+  Two protocol generations exist, both Realtek's:\n+    gen1  older boards, where the MCU fronts Broadcom PSE silicon\n+    gen2  the altered protocol used with Realtek's own PSE silicon\n+\n+  On an I2C attachment the framing the MCU firmware expects is part of the\n+  compatible: '-smbus' (reads carry a leading command byte and a repeated\n+  start) or '-i2c' (bare block writes and reads). A UART attachment carries\n+  no framing suffix; the transport is given by the parent 'serial' node.\n+\n+  Each board additionally carries a device-specific compatible that falls\n+  back to one of the protocol compatibles above. Drivers bind on the\n+  protocol compatible; the device-specific string identifies the board and\n+  reserves a place for a future per-board quirk without having to retrofit\n+  device trees already in the field.\n+\n+properties:\n+  compatible:\n+    oneOf:\n+      # UART\n+      - items:\n+          - enum:\n+              - zyxel,gs1900-10hp-a1-pse\n+          - const: realtek,pse-mcu-gen1\n+\n+      # I2C, SMBus framing\n+      - items:\n+          - enum:\n+              - zyxel,gs1920-24hp-v2-pse\n+          - const: realtek,pse-mcu-gen1-smbus\n+\n+      # UART\n+      - items:\n+          - enum:\n+              - zyxel,gs1900-10hp-b1-pse\n+              - zyxel,xmg1915-10ep-pse\n+          - const: realtek,pse-mcu-gen2\n+\n+      # I2C, SMBus framing\n+      - items:\n+          - enum:\n+              - zyxel,xs1930-12hp-pse\n+          - const: realtek,pse-mcu-gen2-smbus\n+\n+      # I2C, raw framing\n+      - items:\n+          - enum:\n+              - linksys,lgs328mpc-v2-pse\n+          - const: realtek,pse-mcu-gen2-i2c\n+\n+  reg:\n+    maxItems: 1\n+\n+  reset-gpios:\n+    description: Reset line of the MCU.\n+    maxItems: 1\n+\n+  disable-ports-gpios:\n+    description:\n+      Hardware gate that forces all ports into admin-disabled state while\n+      asserted.\n+    maxItems: 1\n+\n+required:\n+  - compatible\n+\n+allOf:\n+  - $ref: pse-controller.yaml#\n+  - $ref: /schemas/serial/serial-peripheral-props.yaml#\n+  # A '-smbus'/'-i2c' compatible is an I2C attachment: it has 'reg' and\n+  # cannot carry serial bus properties. A bare gen compatible is a UART\n+  # attachment: no 'reg', the transport comes from the parent serial node.\n+  - if:\n+      properties:\n+        compatible:\n+          contains:\n+            enum:\n+              - realtek,pse-mcu-gen1-smbus\n+              - realtek,pse-mcu-gen2-smbus\n+              - realtek,pse-mcu-gen2-i2c\n+    then:\n+      required:\n+        - reg\n+      properties:\n+        current-speed: false\n+        max-speed: false\n+    else:\n+      properties:\n+        reg: false\n+\n+unevaluatedProperties: false\n+\n+examples:\n+  # SMBus-framed I2C attachment\n+  - |\n+    i2c {\n+        #address-cells = \u003c1\u003e;\n+        #size-cells = \u003c0\u003e;\n+\n+        ethernet-pse@20 {\n+            compatible = \"zyxel,xs1930-12hp-pse\", \"realtek,pse-mcu-gen2-smbus\";\n+            reg = \u003c0x20\u003e;\n+\n+            pse-pis {\n+                #address-cells = \u003c1\u003e;\n+                #size-cells = \u003c0\u003e;\n+\n+                pse-pi@0 {\n+                    reg = \u003c0\u003e;\n+                    #pse-cells = \u003c0\u003e;\n+                };\n+            };\n+        };\n+    };\n+\n+  # Raw-I2C-framed attachment\n+  - |\n+    i2c {\n+        #address-cells = \u003c1\u003e;\n+        #size-cells = \u003c0\u003e;\n+\n+        ethernet-pse@20 {\n+            compatible = \"linksys,lgs328mpc-v2-pse\", \"realtek,pse-mcu-gen2-i2c\";\n+            reg = \u003c0x20\u003e;\n+\n+            pse-pis {\n+                #address-cells = \u003c1\u003e;\n+                #size-cells = \u003c0\u003e;\n+\n+                pse-pi@0 {\n+                    reg = \u003c0\u003e;\n+                    #pse-cells = \u003c0\u003e;\n+                };\n+            };\n+        };\n+    };\n+\n+  # UART attachment\n+  - |\n+    serial {\n+        ethernet-pse {\n+            compatible = \"zyxel,gs1900-10hp-a1-pse\", \"realtek,pse-mcu-gen1\";\n+            current-speed = \u003c19200\u003e;\n+\n+            pse-pis {\n+                #address-cells = \u003c1\u003e;\n+                #size-cells = \u003c0\u003e;\n+\n+                pse-pi@0 {\n+                    reg = \u003c0\u003e;\n+                    #pse-cells = \u003c0\u003e;\n+                };\n+            };\n+        };\n+    };\ndiff --git a/MAINTAINERS b/MAINTAINERS\nindex 60cff00953dcbe..16136ac11e0637 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -22736,6 +22736,13 @@ S:\tMaintained\n F:\tinclude/sound/rt*.h\n F:\tsound/soc/codecs/rt*\n \n+REALTEK PSE MCU DRIVER\n+M:\tJonas Jelonek \u003cjelonek.jonas@gmail.com\u003e\n+L:\tnetdev@vger.kernel.org\n+S:\tMaintained\n+F:\tDocumentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml\n+F:\tdrivers/net/pse-pd/realtek-pse-mcu*\n+\n REALTEK OTTO WATCHDOG\n M:\tSander Vanheule \u003csander@svanheule.net\u003e\n L:\tlinux-watchdog@vger.kernel.org\ndiff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig\nindex 7ef29657ee5dd1..a0f2ae668c67c8 100644\n--- a/drivers/net/pse-pd/Kconfig\n+++ b/drivers/net/pse-pd/Kconfig\n@@ -13,6 +13,34 @@ menuconfig PSE_CONTROLLER\n \n if PSE_CONTROLLER\n \n+config PSE_REALTEK_MCU\n+\ttristate\n+\thelp\n+\t  Shared core for the Realtek PSE MCU driver. This is selected\n+\t  automatically by the transport options below.\n+\n+config PSE_REALTEK_MCU_I2C\n+\ttristate \"Realtek PSE MCU driver (I2C transport)\"\n+\tdepends on I2C\n+\tselect PSE_REALTEK_MCU\n+\thelp\n+\t  Driver for the microcontroller (MCU) that fronts the PSE\n+\t  hardware on various Realtek-based managed switches, attached\n+\t  via I2C/SMBus. The MCU exposes a message-based protocol; the actual\n+\t  PSE silicon is not accessed directly. To compile this driver as a\n+\t  module, choose M here: the module will be called realtek-pse-mcu-i2c.\n+\n+config PSE_REALTEK_MCU_UART\n+\ttristate \"Realtek PSE MCU driver (UART transport)\"\n+\tdepends on SERIAL_DEV_BUS\n+\tselect PSE_REALTEK_MCU\n+\thelp\n+\t  Driver for the microcontroller (MCU) that fronts the PSE\n+\t  hardware on various Realtek-based managed switches, attached\n+\t  via UART. The MCU exposes a message-based protocol; the actual PSE\n+\t  silicon is not accessed directly. To compile this driver as a\n+\t  module, choose M here: the module will be called realtek-pse-mcu-uart.\n+\n config PSE_REGULATOR\n \ttristate \"Regulator based PSE controller\"\n \thelp\ndiff --git a/drivers/net/pse-pd/Makefile b/drivers/net/pse-pd/Makefile\nindex cc78f7ea7f5f71..9cca5900fe3421 100644\n--- a/drivers/net/pse-pd/Makefile\n+++ b/drivers/net/pse-pd/Makefile\n@@ -3,6 +3,9 @@\n \n obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o\n \n+obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o\n+obj-$(CONFIG_PSE_REALTEK_MCU_I2C) += realtek-pse-mcu-i2c.o\n+obj-$(CONFIG_PSE_REALTEK_MCU_UART) += realtek-pse-mcu-uart.o\n obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o\n obj-$(CONFIG_PSE_PD692X0) += pd692x0.o\n obj-$(CONFIG_PSE_SI3474) += si3474.o\ndiff --git a/drivers/net/pse-pd/realtek-pse-mcu-core.c b/drivers/net/pse-pd/realtek-pse-mcu-core.c\nnew file mode 100644\nindex 00000000000000..8204cb430ffea8\n--- /dev/null\n+++ b/drivers/net/pse-pd/realtek-pse-mcu-core.c\n@@ -0,0 +1,989 @@\n+// SPDX-License-Identifier: GPL-2.0-or-later\n+/*\n+ * Driver for the microcontroller (MCU) fronting PSE silicon on various\n+ * Realtek-based managed switches. The MCU speaks a 12-byte fixed-frame\n+ * management protocol; this driver covers two generations of the\n+ * protocol via a per-dialect opcode table and response parsers.\n+ *\n+ * Many PoE switch designs put a dedicated microcontroller in front of the\n+ * actual PSE silicon: the host CPU talks to the MCU over I2C/SMBus or\n+ * UART, and the MCU in turn manages the PSE chips on the board. The MCU\n+ * speaks a small message-based protocol. The PSE chips themselves are not\n+ * accessed directly; everything goes through MCU commands.\n+ *\n+ * This driver targets that architecture for the Realtek-family protocol.\n+ * Two generations are supported: Gen1 being used on older switches where\n+ * the MCU fronts and manages Broadcom PSE silicon; Gen2 being used with\n+ * Realtek PSE silicon. The two share frame format and a sum-mod-256\n+ * checksum but diverge on opcode numbers and on a few response layouts;\n+ * this is handled by the per-dialect opcode table and parser hooks.\n+ *\n+ * Out of scope: PSE chips that are interfaced directly from the host\n+ * without a management MCU, MCU designs that speak an unrelated protocol\n+ * family, and \"dumb PSE\" modes where no host control is wired up at all.\n+ *\n+ * This core module implements the protocol, decoding/encoding of MCU\n+ * responses, and the pse_controller_ops integration. Transport modules\n+ * (realtek-pse-mcu-i2c, realtek-pse-mcu-uart) provide the send/recv\n+ * callbacks.\n+ */\n+\n+#include \u003clinux/bitfield.h\u003e\n+#include \u003clinux/cleanup.h\u003e\n+#include \u003clinux/container_of.h\u003e\n+#include \u003clinux/delay.h\u003e\n+#include \u003clinux/gpio/consumer.h\u003e\n+#include \u003clinux/jiffies.h\u003e\n+#include \u003clinux/minmax.h\u003e\n+#include \u003clinux/module.h\u003e\n+#include \u003clinux/property.h\u003e\n+#include \u003clinux/pse-pd/pse.h\u003e\n+#include \u003clinux/unaligned.h\u003e\n+\n+#include \"realtek-pse-mcu.h\"\n+\n+#define RTPSE_MCU_DEVICE_ID_RTL8238B\t\t0x0138\n+#define RTPSE_MCU_DEVICE_ID_RTL8239\t\t0x0039\n+#define RTPSE_MCU_DEVICE_ID_RTL8239C\t\t0x0139\n+#define RTPSE_MCU_DEVICE_ID_BCM59111\t\t0xe111\n+#define RTPSE_MCU_DEVICE_ID_BCM59121\t\t0xe121\n+\n+#define RTPSE_MCU_PORT_STS_DISABLED\t\t0x00\n+#define RTPSE_MCU_PORT_STS_SEARCHING\t\t0x01\n+#define RTPSE_MCU_PORT_STS_DELIVERING\t\t0x02\n+#define RTPSE_MCU_PORT_STS_TEST\t\t\t0x03\t/* Gen1-only; reserved on Gen2 */\n+#define RTPSE_MCU_PORT_STS_FAULT\t\t0x04\n+#define RTPSE_MCU_PORT_STS_OTHER_FAULT\t\t0x05\t/* Gen1-only; reserved on Gen2 */\n+#define RTPSE_MCU_PORT_STS_REQUESTING\t\t0x06\n+\n+/* RTPSE_MCU_PORT_SET_POWER_LIMIT_TYPE values */\n+#define RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER\t0x02\n+\n+#define RTPSE_MCU_MAX_PORTS\t\t\t48\n+#define RTPSE_MCU_PORT_MAX_PRIORITY\t\t3\n+\n+enum rtpse_mcu_cmd {\n+\tRTPSE_MCU_CMD_SET_GLOBAL_STATE,\n+\tRTPSE_MCU_CMD_GET_SYSTEM_INFO,\n+\tRTPSE_MCU_CMD_GET_EXT_CONFIG,\n+\n+\tRTPSE_MCU_CMD_PORT_ENABLE,\n+\tRTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE,\n+\tRTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,\n+\tRTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,\n+\tRTPSE_MCU_CMD_PORT_SET_PRIORITY,\n+\tRTPSE_MCU_CMD_PORT_GET_STATUS,\n+\tRTPSE_MCU_CMD_PORT_GET_POWER_STATS,\n+\tRTPSE_MCU_CMD_PORT_GET_CONFIG,\n+\tRTPSE_MCU_CMD_PORT_GET_EXT_CONFIG,\n+\n+\tRTPSE_MCU_NUM_CMDS,\n+};\n+\n+struct rtpse_mcu_opcode {\n+\tu8 op;\n+\tbool valid;\n+};\n+\n+/* Shorthand for the designated-initializer entries in dialect opcode tables. */\n+#define RTPSE_MCU_OP(opc)\t{ .op = (opc), .valid = true }\n+\n+/* Parsed MCU response structures (decoded from rtpse_mcu_msg replies) */\n+\n+struct rtpse_mcu_info {\n+\tu8 max_ports;\n+\tbool system_enable;\n+\tu16 device_id;\n+\tu8 sw_ver;\n+\tu8 mcu_type;\n+\tu8 config_status;\n+\tu8 ext_ver;\n+};\n+\n+struct rtpse_mcu_ext_config {\n+\tu8 uvlo;\n+\tu8 ovlo;\n+\tbool prealloc_enable;\n+\tu8 num_of_pses;\n+};\n+\n+struct rtpse_mcu_port_status {\n+\tu8 sts1;\n+\tu8 sts2;\n+\tu8 sts3;\n+};\n+\n+struct rtpse_mcu_port_measurement {\n+\tu16 voltage_raw;\t/* 64.45mV/LSB */\n+\tu16 current_raw;\t/* 1mA/LSB */\n+\tu16 temperature_raw;\t/* T(mC) = 1250 * (220 - raw) */\n+\tu16 power_raw;\t\t/* 100mW/LSB */\n+};\n+\n+struct rtpse_mcu_port_config {\n+\tbool enable;\n+};\n+\n+struct rtpse_mcu_port_ext_config {\n+\tu8 max_power;\n+\tu8 priority;\n+};\n+\n+struct rtpse_mcu_dialect {\n+\tstruct rtpse_mcu_opcode opcode[RTPSE_MCU_NUM_CMDS];\n+\n+\t/*\n+\t * Response parsers for the fields that differ between dialects; each\n+\t * dialect supplies its own. Other responses share one layout and are\n+\t * decoded directly - a dialect that diverges there must add a hook,\n+\t * as a mismatched layout cannot be detected (the checksum still passes).\n+\t */\n+\tvoid (*parse_system_info)(const u8 *payload, struct rtpse_mcu_info *info);\n+\tint (*parse_port_class)(const struct rtpse_mcu_port_status *status);\n+\tconst char *(*mcu_type_str)(unsigned int mcu_type);\n+};\n+\n+struct rtpse_mcu_chip_info {\n+\tconst char *name;\n+\tu16 device_id;\n+\tu32 max_mW_per_port;\n+\tenum rtpse_mcu_cmd pw_set_cmd;\t/* command used by set_pw_limit */\n+\tu32 pw_set_lsb_mW;\t\t/* LSB of pw_set_cmd value, in mW */\n+\tu32 pw_read_lsb_mW;\t\t/* LSB of ext_config.max_power read-back, in mW */\n+};\n+\n+static const struct rtpse_mcu_chip_info rtl8238b_info = {\n+\t.device_id = RTPSE_MCU_DEVICE_ID_RTL8238B,\n+\t.max_mW_per_port = 30000,\n+\t.name = \"RTL8238B\",\n+\t.pw_read_lsb_mW = 200,\n+\t.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,\n+\t.pw_set_lsb_mW = 200,\n+};\n+\n+static const struct rtpse_mcu_chip_info rtl8239_info = {\n+\t.device_id = RTPSE_MCU_DEVICE_ID_RTL8239,\n+\t.max_mW_per_port = 90000,\n+\t.name = \"RTL8239\",\n+\t.pw_read_lsb_mW = 400,\n+\t.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,\n+\t.pw_set_lsb_mW = 400,\n+};\n+\n+static const struct rtpse_mcu_chip_info rtl8239c_info = {\n+\t.device_id = RTPSE_MCU_DEVICE_ID_RTL8239C,\n+\t.max_mW_per_port = 90000,\n+\t.name = \"RTL8239C\",\n+\t.pw_read_lsb_mW = 400,\n+\t.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,\n+\t.pw_set_lsb_mW = 400,\n+};\n+\n+static const struct rtpse_mcu_chip_info bcm59111_info = {\n+\t.device_id = RTPSE_MCU_DEVICE_ID_BCM59111,\n+\t.max_mW_per_port = 30000,\n+\t.name = \"BCM59111\",\n+\t.pw_read_lsb_mW = 200,\n+\t.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,\n+\t.pw_set_lsb_mW = 200,\n+};\n+\n+static const struct rtpse_mcu_chip_info bcm59121_info = {\n+\t.device_id = RTPSE_MCU_DEVICE_ID_BCM59121,\n+\t/*\n+\t * BCM59121 is a 60W Type-3 part, but known boards run it at 802.3at\n+\t * and the Gen1 dialect has only the 8-bit/0.2W set command (\u003c=51W);\n+\t * cap at the 30W the hardware actually offers.\n+\t */\n+\t.max_mW_per_port = 30000,\n+\t.name = \"BCM59121\",\n+\t.pw_read_lsb_mW = 200,\n+\t.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,\n+\t.pw_set_lsb_mW = 200,\n+};\n+\n+/* Helpers and basic functions */\n+\n+static struct rtpse_mcu_ctrl *to_rtpse_mcu_ctrl(struct pse_controller_dev *pcdev)\n+{\n+\treturn container_of(pcdev, struct rtpse_mcu_ctrl, pcdev);\n+}\n+\n+static void rtpse_mcu_msg_init(struct rtpse_mcu_msg *msg, u8 opcode)\n+{\n+\tmemset(msg, 0xff, sizeof(*msg));\n+\tmsg-\u003eopcode = opcode;\n+}\n+\n+static u8 rtpse_mcu_checksum(const u8 *buf, size_t len)\n+{\n+\tu8 sum = 0;\n+\n+\twhile (len--)\n+\t\tsum += *buf++;\n+\treturn sum;\n+}\n+\n+static int rtpse_mcu_do_xfer(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_msg *req,\n+\t\t\t     struct rtpse_mcu_msg *resp)\n+{\n+\tint ret;\n+\n+\tscoped_guard(mutex, \u0026pse-\u003emutex) {\n+\t\t/* Rolling seq_num so a late reply can't pass as a later one. */\n+\t\treq-\u003eseq_num = pse-\u003eseq++;\n+\t\treq-\u003echecksum = rtpse_mcu_checksum((u8 *)req, RTPSE_MCU_MSG_SIZE - 1);\n+\n+\t\tret = pse-\u003etransport-\u003esend(pse, req);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\n+\t\t/*\n+\t\t * The MCU needs a fixed amount of time between receiving a request\n+\t\t * and having the response ready, regardless of how the bytes get to\n+\t\t * us. Pace the transaction here so each transport can keep its recv\n+\t\t * path simple: a single bounded wait rather than a generic retry.\n+\t\t */\n+\t\tmsleep(RTPSE_MCU_RESPONSE_MS);\n+\n+\t\tmemset(resp, 0, sizeof(*resp));\n+\t\tret = pse-\u003etransport-\u003erecv(pse, req, resp);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\t}\n+\n+\t/*\n+\t * Explicit MCU error opcodes (observed on the Gen1 dialect; harmless\n+\t * to check for Gen2 too). Catch these before the generic opcode/CRC\n+\t * mismatch path so callers see a meaningful errno.\n+\t */\n+\tswitch (resp-\u003eopcode) {\n+\tcase RTPSE_MCU_OPCODE_INCOMPLETE:\n+\t\treturn -EBADE;\n+\tcase RTPSE_MCU_OPCODE_BAD_CSUM:\n+\t\treturn -EBADMSG;\n+\tcase RTPSE_MCU_OPCODE_NOT_READY:\n+\t\treturn -EAGAIN;\n+\t}\n+\n+\tif (resp-\u003eopcode != req-\u003eopcode ||\n+\t    resp-\u003eseq_num != req-\u003eseq_num ||\n+\t    resp-\u003echecksum != rtpse_mcu_checksum((u8 *)resp, RTPSE_MCU_MSG_SIZE - 1))\n+\t\treturn -EBADMSG;\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_port_query(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode,\n+\t\t\t\tstruct rtpse_mcu_msg *resp)\n+{\n+\tstruct rtpse_mcu_msg req;\n+\tint ret;\n+\n+\trtpse_mcu_msg_init(\u0026req, opcode);\n+\treq.payload[0] = port;\n+\n+\tret = rtpse_mcu_do_xfer(pse, \u0026req, resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tif (resp-\u003epayload[0] != port)\n+\t\treturn -EIO;\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_port_cmd(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode, u8 arg)\n+{\n+\tstruct rtpse_mcu_msg req, resp;\n+\tint ret;\n+\n+\trtpse_mcu_msg_init(\u0026req, opcode);\n+\treq.payload[0] = port;\n+\treq.payload[1] = arg;\n+\n+\tret = rtpse_mcu_do_xfer(pse, \u0026req, \u0026resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tif (resp.payload[0] != port || resp.payload[1] != 0)\n+\t\treturn -EIO;\n+\n+\treturn 0;\n+}\n+\n+/* Global operations */\n+\n+static int rtpse_mcu_get_info(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)\n+{\n+\tstruct rtpse_mcu_msg req, resp;\n+\tconst struct rtpse_mcu_opcode *opc;\n+\tint ret;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_GET_SYSTEM_INFO];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\trtpse_mcu_msg_init(\u0026req, opc-\u003eop);\n+\tret = rtpse_mcu_do_xfer(pse, \u0026req, \u0026resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tpse-\u003edialect-\u003eparse_system_info(resp.payload, info);\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_get_ext_config(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_ext_config *config)\n+{\n+\tstruct rtpse_mcu_msg req, resp;\n+\tconst struct rtpse_mcu_opcode *opc;\n+\tint ret;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_GET_EXT_CONFIG];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\trtpse_mcu_msg_init(\u0026req, opc-\u003eop);\n+\tret = rtpse_mcu_do_xfer(pse, \u0026req, \u0026resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tconfig-\u003euvlo = resp.payload[0];\n+\tconfig-\u003eovlo = resp.payload[5];\n+\tconfig-\u003eprealloc_enable = (resp.payload[1] == 0x1);\n+\tconfig-\u003enum_of_pses = resp.payload[6];\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_set_global_state(struct rtpse_mcu_ctrl *pse, bool enable)\n+{\n+\tstruct rtpse_mcu_msg req, resp;\n+\tconst struct rtpse_mcu_opcode *opc;\n+\tint ret;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_SET_GLOBAL_STATE];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\trtpse_mcu_msg_init(\u0026req, opc-\u003eop);\n+\treq.payload[0] = enable ? 0x1 : 0x0;\n+\n+\tret = rtpse_mcu_do_xfer(pse, \u0026req, \u0026resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\treturn (resp.payload[0] == 0x0) ? 0 : -EIO;\n+}\n+\n+/* Port operations */\n+\n+static int rtpse_mcu_port_get_status(struct rtpse_mcu_ctrl *pse, unsigned int port,\n+\t\t\t\t     struct rtpse_mcu_port_status *status)\n+{\n+\tconst struct rtpse_mcu_opcode *opc;\n+\tstruct rtpse_mcu_msg resp;\n+\tint ret;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_PORT_GET_STATUS];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\tret = rtpse_mcu_port_query(pse, port, opc-\u003eop, \u0026resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tstatus-\u003ests1 = resp.payload[1];\n+\tstatus-\u003ests2 = resp.payload[2];\n+\tstatus-\u003ests3 = resp.payload[3];\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_port_get_measurement(struct rtpse_mcu_ctrl *pse, unsigned int port,\n+\t\t\t\t\t  struct rtpse_mcu_port_measurement *measurement)\n+{\n+\tconst struct rtpse_mcu_opcode *opc;\n+\tstruct rtpse_mcu_msg resp;\n+\tint ret;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_PORT_GET_POWER_STATS];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\tret = rtpse_mcu_port_query(pse, port, opc-\u003eop, \u0026resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tmeasurement-\u003evoltage_raw = get_unaligned_be16(\u0026resp.payload[1]);\n+\tmeasurement-\u003ecurrent_raw = get_unaligned_be16(\u0026resp.payload[3]);\n+\tmeasurement-\u003etemperature_raw = get_unaligned_be16(\u0026resp.payload[5]);\n+\tmeasurement-\u003epower_raw = get_unaligned_be16(\u0026resp.payload[7]);\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_port_get_config(struct rtpse_mcu_ctrl *pse, unsigned int port,\n+\t\t\t\t     struct rtpse_mcu_port_config *config)\n+{\n+\tconst struct rtpse_mcu_opcode *opc;\n+\tstruct rtpse_mcu_msg resp;\n+\tint ret;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_PORT_GET_CONFIG];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\tret = rtpse_mcu_port_query(pse, port, opc-\u003eop, \u0026resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tconfig-\u003eenable = (resp.payload[1] == 1);\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_port_get_ext_config(struct rtpse_mcu_ctrl *pse, unsigned int port,\n+\t\t\t\t\t struct rtpse_mcu_port_ext_config *config)\n+{\n+\tconst struct rtpse_mcu_opcode *opc;\n+\tstruct rtpse_mcu_msg resp;\n+\tint ret;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\tret = rtpse_mcu_port_query(pse, port, opc-\u003eop, \u0026resp);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tconfig-\u003emax_power = resp.payload[3];\n+\tconfig-\u003epriority = resp.payload[4];\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_port_set_state(struct rtpse_mcu_ctrl *pse, unsigned int port, bool enable)\n+{\n+\tconst struct rtpse_mcu_opcode *opc;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_PORT_ENABLE];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\treturn rtpse_mcu_port_cmd(pse, port, opc-\u003eop, enable ? 0x1 : 0x0);\n+}\n+\n+/* PSE controller ops */\n+\n+static int rtpse_mcu_port_get_admin_state(struct pse_controller_dev *pcdev, int id,\n+\t\t\t\t\t  struct pse_admin_state *admin_state)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tstruct rtpse_mcu_port_config config;\n+\tint ret;\n+\n+\tret = rtpse_mcu_port_get_config(pse, id, \u0026config);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tadmin_state-\u003ec33_admin_state = config.enable ? ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED :\n+\t\t\t\t\t\t       ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_port_get_pw_status(struct pse_controller_dev *pcdev, int id,\n+\t\t\t\t\tstruct pse_pw_status *pw_status)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tstruct rtpse_mcu_port_status status;\n+\tint ret;\n+\n+\tret = rtpse_mcu_port_get_status(pse, id, \u0026status);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tswitch (status.sts1) {\n+\tcase RTPSE_MCU_PORT_STS_DISABLED:\n+\t\tpw_status-\u003ec33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;\n+\t\tbreak;\n+\tcase RTPSE_MCU_PORT_STS_SEARCHING:\n+\tcase RTPSE_MCU_PORT_STS_REQUESTING:\n+\t\tpw_status-\u003ec33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;\n+\t\tbreak;\n+\tcase RTPSE_MCU_PORT_STS_DELIVERING:\n+\t\tpw_status-\u003ec33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;\n+\t\tbreak;\n+\tcase RTPSE_MCU_PORT_STS_TEST:\n+\t\tpw_status-\u003ec33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_TEST;\n+\t\tbreak;\n+\tcase RTPSE_MCU_PORT_STS_FAULT:\n+\t\tpw_status-\u003ec33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;\n+\t\tbreak;\n+\tcase RTPSE_MCU_PORT_STS_OTHER_FAULT:\n+\t\tpw_status-\u003ec33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_OTHERFAULT;\n+\t\tbreak;\n+\tdefault:\n+\t\tpw_status-\u003ec33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_UNKNOWN;\n+\t\tbreak;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_port_get_pw_class(struct pse_controller_dev *pcdev, int id)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tstruct rtpse_mcu_port_status status;\n+\tint ret;\n+\n+\tret = rtpse_mcu_port_get_status(pse, id, \u0026status);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/*\n+\t * sts2 holds the class only in the operational states below. In a\n+\t * fault/test state (or a code reserved on the current dialect) it\n+\t * does not, so there is no class to report and we return 0. That is\n+\t * indistinguishable from a real class-0 PD; userspace disambiguates\n+\t * via the power status.\n+\t */\n+\tswitch (status.sts1) {\n+\tcase RTPSE_MCU_PORT_STS_DISABLED:\n+\tcase RTPSE_MCU_PORT_STS_SEARCHING:\n+\tcase RTPSE_MCU_PORT_STS_DELIVERING:\n+\tcase RTPSE_MCU_PORT_STS_REQUESTING:\n+\t\treturn pse-\u003edialect-\u003eparse_port_class(\u0026status);\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+}\n+\n+static int rtpse_mcu_port_get_actual_pw(struct pse_controller_dev *pcdev, int id)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tstruct rtpse_mcu_port_measurement measurement;\n+\tint ret;\n+\n+\tret = rtpse_mcu_port_get_measurement(pse, id, \u0026measurement);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/* 100mW per LSB */\n+\treturn measurement.power_raw * 100U;\n+}\n+\n+static int rtpse_mcu_port_get_voltage(struct pse_controller_dev *pcdev, int id)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tstruct rtpse_mcu_port_measurement measurement;\n+\tint ret;\n+\tu32 uV;\n+\n+\tret = rtpse_mcu_port_get_measurement(pse, id, \u0026measurement);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/* 64.45mV per LSB */\n+\tuV = measurement.voltage_raw * 64450U;\n+\treturn min_t(u32, uV, INT_MAX);\n+}\n+\n+static int rtpse_mcu_port_enable(struct pse_controller_dev *pcdev, int id)\n+{\n+\treturn rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, true);\n+}\n+\n+static int rtpse_mcu_port_disable(struct pse_controller_dev *pcdev, int id)\n+{\n+\treturn rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, false);\n+}\n+\n+static int rtpse_mcu_port_get_pw_limit(struct pse_controller_dev *pcdev, int id)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tstruct rtpse_mcu_port_ext_config config;\n+\tint ret;\n+\n+\tret = rtpse_mcu_port_get_ext_config(pse, id, \u0026config);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\treturn config.max_power * pse-\u003echip-\u003epw_read_lsb_mW;\n+}\n+\n+static int rtpse_mcu_port_set_pw_limit(struct pse_controller_dev *pcdev, int id, int max_mW)\n+{\n+\tconst struct rtpse_mcu_opcode *type_opc, *val_opc;\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tconst struct rtpse_mcu_chip_info *chip = pse-\u003echip;\n+\tu8 prg_val;\n+\tint ret;\n+\n+\tif (max_mW \u003c 0 || max_mW \u003e chip-\u003emax_mW_per_port)\n+\t\treturn -ERANGE;\n+\n+\ttype_opc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE];\n+\tval_opc = \u0026pse-\u003edialect-\u003eopcode[chip-\u003epw_set_cmd];\n+\t/* pw_set_lsb_mW is the divisor below; reject a chip that lacks it. */\n+\tif (!type_opc-\u003evalid || !val_opc-\u003evalid || !chip-\u003epw_set_lsb_mW)\n+\t\treturn -EOPNOTSUPP;\n+\n+\t/*\n+\t * Switch the port to user-defined limit mode first, then program the\n+\t * limit value. If the second cmd fails, the port is left in\n+\t * user-defined mode but with the previous limit value; the next\n+\t * successful set_pw_limit call recovers it.\n+\t */\n+\tret = rtpse_mcu_port_cmd(pse, id, type_opc-\u003eop, RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tprg_val = min_t(unsigned int, max_mW / chip-\u003epw_set_lsb_mW, U8_MAX);\n+\n+\treturn rtpse_mcu_port_cmd(pse, id, val_opc-\u003eop, prg_val);\n+}\n+\n+static int rtpse_mcu_port_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,\n+\t\t\t\t\t      struct pse_pw_limit_ranges *out)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tstruct ethtool_c33_pse_pw_limit_range *range;\n+\n+\trange = kzalloc_obj(*range);\n+\tif (!range)\n+\t\treturn -ENOMEM;\n+\n+\trange[0].min = 0;\n+\trange[0].max = pse-\u003echip-\u003emax_mW_per_port;\n+\n+\tout-\u003ec33_pw_limit_ranges = range;\n+\treturn 1;\n+}\n+\n+static int rtpse_mcu_port_get_prio(struct pse_controller_dev *pcdev, int id)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tstruct rtpse_mcu_port_ext_config config;\n+\tint ret;\n+\n+\tret = rtpse_mcu_port_get_ext_config(pse, id, \u0026config);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\treturn config.priority;\n+}\n+\n+static int rtpse_mcu_port_set_prio(struct pse_controller_dev *pcdev, int id, unsigned int prio)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);\n+\tconst struct rtpse_mcu_opcode *opc;\n+\n+\tif (prio \u003e RTPSE_MCU_PORT_MAX_PRIORITY)\n+\t\treturn -ERANGE;\n+\n+\topc = \u0026pse-\u003edialect-\u003eopcode[RTPSE_MCU_CMD_PORT_SET_PRIORITY];\n+\tif (!opc-\u003evalid)\n+\t\treturn -EOPNOTSUPP;\n+\n+\treturn rtpse_mcu_port_cmd(pse, id, opc-\u003eop, prio);\n+}\n+\n+static const struct pse_controller_ops rtpse_mcu_ops = {\n+\t.pi_get_admin_state = rtpse_mcu_port_get_admin_state,\n+\t.pi_get_pw_status = rtpse_mcu_port_get_pw_status,\n+\t.pi_get_pw_class = rtpse_mcu_port_get_pw_class,\n+\t.pi_get_actual_pw = rtpse_mcu_port_get_actual_pw,\n+\t.pi_enable = rtpse_mcu_port_enable,\n+\t.pi_disable = rtpse_mcu_port_disable,\n+\t.pi_get_voltage = rtpse_mcu_port_get_voltage,\n+\t.pi_get_pw_limit = rtpse_mcu_port_get_pw_limit,\n+\t.pi_set_pw_limit = rtpse_mcu_port_set_pw_limit,\n+\t.pi_get_pw_limit_ranges = rtpse_mcu_port_get_pw_limit_ranges,\n+\t.pi_get_prio = rtpse_mcu_port_get_prio,\n+\t.pi_set_prio = rtpse_mcu_port_set_prio,\n+};\n+\n+static int rtpse_mcu_discover(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)\n+{\n+\tstruct rtpse_mcu_ext_config ext_config;\n+\tunsigned long deadline;\n+\tint ret;\n+\n+\t/*\n+\t * A booting MCU may stay silent (-ETIMEDOUT), not ACK its address\n+\t * (-ENXIO / -EREMOTEIO), report not-ready (-EAGAIN), or emit a\n+\t * corrupt/partial frame (-EBADMSG / -EBADE). Retry those within a\n+\t * bounded window; other errors (e.g. -EOPNOTSUPP) are fatal and fail\n+\t * immediately.\n+\t */\n+\tdeadline = jiffies + msecs_to_jiffies(RTPSE_MCU_BOOT_TIMEOUT_MS);\n+\tdo {\n+\t\tret = rtpse_mcu_get_info(pse, info);\n+\t\tif (ret != -ETIMEDOUT \u0026\u0026 ret != -ENXIO \u0026\u0026 ret != -EREMOTEIO \u0026\u0026\n+\t\t    ret != -EAGAIN \u0026\u0026 ret != -EBADMSG \u0026\u0026 ret != -EBADE)\n+\t\t\tbreak;\n+\t\tmsleep(RTPSE_MCU_BOOT_RETRY_MS);\n+\t} while (time_before(jiffies, deadline));\n+\tif (ret)\n+\t\treturn dev_err_probe(pse-\u003edev, ret, \"failed to read MCU info\\n\");\n+\n+\tswitch (info-\u003edevice_id) {\n+\tcase RTPSE_MCU_DEVICE_ID_RTL8238B:\n+\t\tpse-\u003echip = \u0026rtl8238b_info;\n+\t\tbreak;\n+\tcase RTPSE_MCU_DEVICE_ID_RTL8239:\n+\t\tpse-\u003echip = \u0026rtl8239_info;\n+\t\tbreak;\n+\tcase RTPSE_MCU_DEVICE_ID_RTL8239C:\n+\t\tpse-\u003echip = \u0026rtl8239c_info;\n+\t\tbreak;\n+\tcase RTPSE_MCU_DEVICE_ID_BCM59111:\n+\t\tpse-\u003echip = \u0026bcm59111_info;\n+\t\tbreak;\n+\tcase RTPSE_MCU_DEVICE_ID_BCM59121:\n+\t\tpse-\u003echip = \u0026bcm59121_info;\n+\t\tbreak;\n+\tdefault:\n+\t\treturn dev_err_probe(pse-\u003edev, -EINVAL, \"unknown PSE id 0x%x\\n\",\n+\t\t\t\t     info-\u003edevice_id);\n+\t}\n+\n+\tif (!info-\u003emax_ports || info-\u003emax_ports \u003e RTPSE_MCU_MAX_PORTS)\n+\t\treturn dev_err_probe(pse-\u003edev, -EINVAL,\n+\t\t\t\t     \"MCU reports invalid port count %u\\n\", info-\u003emax_ports);\n+\n+\tret = rtpse_mcu_get_ext_config(pse, \u0026ext_config);\n+\tif (ret)\n+\t\treturn dev_err_probe(pse-\u003edev, ret, \"failed to read MCU ext config\\n\");\n+\n+\tdev_info(pse-\u003edev, \"%s MCU, %s (id 0x%04x), %u ports across %u PSE chip(s)\\n\",\n+\t\t pse-\u003edialect-\u003emcu_type_str(info-\u003emcu_type), pse-\u003echip-\u003ename,\n+\t\t info-\u003edevice_id, info-\u003emax_ports, ext_config.num_of_pses);\n+\treturn 0;\n+}\n+\n+static void rtpse_mcu_global_disable(void *data)\n+{\n+\tstruct rtpse_mcu_ctrl *pse = data;\n+\n+\trtpse_mcu_set_global_state(pse, false);\n+}\n+\n+int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse)\n+{\n+\tconst struct rtpse_mcu_match_data *match;\n+\tstruct rtpse_mcu_info info;\n+\tstruct gpio_desc *gpiod;\n+\tint ret;\n+\n+\tBUILD_BUG_ON(sizeof(struct rtpse_mcu_msg) != RTPSE_MCU_MSG_SIZE);\n+\n+\tret = devm_mutex_init(pse-\u003edev, \u0026pse-\u003emutex);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tmatch = device_get_match_data(pse-\u003edev);\n+\tif (!match)\n+\t\treturn dev_err_probe(pse-\u003edev, -ENODEV, \"missing match data\\n\");\n+\tpse-\u003edialect = match-\u003edialect;\n+\n+\t/*\n+\t * Catch a dialect that forgot to set one of the required hooks at\n+\t * probe time, rather than NULL-deref'ing later from a fast path.\n+\t */\n+\tif (!pse-\u003edialect ||\n+\t    !pse-\u003edialect-\u003eparse_system_info ||\n+\t    !pse-\u003edialect-\u003eparse_port_class ||\n+\t    !pse-\u003edialect-\u003emcu_type_str)\n+\t\treturn dev_err_probe(pse-\u003edev, -EINVAL,\n+\t\t\t\t     \"dialect for chip is incomplete\\n\");\n+\n+\t/*\n+\t * Release the MCU from reset before the first transaction; the\n+\t * boot-retry loop in discover() waits for it to answer.\n+\t */\n+\tgpiod = devm_gpiod_get_optional(pse-\u003edev, \"reset\", GPIOD_OUT_LOW);\n+\tif (IS_ERR(gpiod))\n+\t\treturn dev_err_probe(pse-\u003edev, PTR_ERR(gpiod),\n+\t\t\t\t     \"failed to get reset gpio\\n\");\n+\n+\t/*\n+\t * Some boards gate all ports through a hardware line; deassert it so\n+\t * the MCU protocol governs per-port admin state from here on.\n+\t */\n+\tgpiod = devm_gpiod_get_optional(pse-\u003edev, \"disable-ports\", GPIOD_OUT_LOW);\n+\tif (IS_ERR(gpiod))\n+\t\treturn dev_err_probe(pse-\u003edev, PTR_ERR(gpiod),\n+\t\t\t\t     \"failed to get disable-ports gpio\\n\");\n+\n+\tret = rtpse_mcu_discover(pse, \u0026info);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tif (!info.system_enable) {\n+\t\tret = rtpse_mcu_set_global_state(pse, true);\n+\t\t/* Dialects without a global-state concept (e.g. Gen1) return\n+\t\t * -EOPNOTSUPP; treat that as \"no separate enable required\".\n+\t\t */\n+\t\tif (ret \u0026\u0026 ret != -EOPNOTSUPP)\n+\t\t\treturn dev_err_probe(pse-\u003edev, ret,\n+\t\t\t\t\t     \"failed to enable PSE system\\n\");\n+\t\tif (!ret) {\n+\t\t\tret = devm_add_action_or_reset(pse-\u003edev,\n+\t\t\t\t\t\t       rtpse_mcu_global_disable, pse);\n+\t\t\tif (ret)\n+\t\t\t\treturn ret;\n+\t\t}\n+\t}\n+\n+\t/*\n+\t * Depending on the MCU firmware configuration (which might be different\n+\t * for every board), it isn't known whether the PoE subsystem is active or\n+\t * inactive by default. At this stage, the PSE chips might already deliver\n+\t * power to PDs without any explicit enable.\n+\t */\n+\n+\t/* pcdev.owner is set by the transport, so the registered controller\n+\t * pins the transport module that owns the live device, not the core.\n+\t */\n+\tpse-\u003epcdev.ops      = \u0026rtpse_mcu_ops;\n+\tpse-\u003epcdev.dev      = pse-\u003edev;\n+\tpse-\u003epcdev.types    = ETHTOOL_PSE_C33;\n+\tpse-\u003epcdev.nr_lines = info.max_ports;\n+\tpse-\u003epcdev.pis_prio_max = RTPSE_MCU_PORT_MAX_PRIORITY;\n+\tpse-\u003epcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_DYNAMIC;\n+\n+\treturn devm_pse_controller_register(pse-\u003edev, \u0026pse-\u003epcdev);\n+}\n+EXPORT_SYMBOL_GPL(rtpse_mcu_register);\n+\n+static void rtpse_mcu_gen2_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)\n+{\n+\tinfo-\u003emax_ports = payload[1];\n+\tinfo-\u003esystem_enable = (payload[2] == 0x1);\n+\tinfo-\u003edevice_id = get_unaligned_be16(\u0026payload[3]);\n+\tinfo-\u003esw_ver = payload[5];\n+\tinfo-\u003emcu_type = payload[6];\n+\tinfo-\u003econfig_status = payload[7];\n+\tinfo-\u003eext_ver = payload[8];\n+}\n+\n+static int rtpse_mcu_gen2_parse_port_class(const struct rtpse_mcu_port_status *status)\n+{\n+\t/* Class lives in the upper nibble of sts2. */\n+\treturn FIELD_GET(GENMASK(7, 4), status-\u003ests2);\n+}\n+\n+static const char *rtpse_mcu_gen2_mcu_type_str(unsigned int mcu_type)\n+{\n+\tswitch (mcu_type) {\n+\tcase 0x00:\treturn \"GigaDevice GD32F310\";\n+\tcase 0x01:\treturn \"GigaDevice GD32F230\";\n+\tcase 0x02:\treturn \"GigaDevice GD32F303\";\n+\tcase 0x03:\treturn \"GigaDevice GD32F103\";\n+\tcase 0x04:\treturn \"GigaDevice GD32E103\";\n+\tcase 0x10:\treturn \"Nuvoton M0516\";\n+\tcase 0x11:\treturn \"Nuvoton M0564\";\n+\tcase 0x12:\treturn \"Nuvoton NUC029\";\n+\tdefault:\treturn \"unknown\";\n+\t}\n+}\n+\n+static void rtpse_mcu_gen1_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)\n+{\n+\tinfo-\u003emax_ports = payload[1];\n+\t/* Gen1 has no explicit system_enable byte; the closest analog is the\n+\t * \"remote enable\" bit in the system-status flags at payload[7].\n+\t */\n+\tinfo-\u003esystem_enable = !!(payload[7] \u0026 BIT(2));\n+\tinfo-\u003edevice_id = get_unaligned_be16(\u0026payload[3]);\n+\tinfo-\u003esw_ver = payload[5];\n+\tinfo-\u003emcu_type = payload[6];\n+\tinfo-\u003econfig_status = payload[7];\n+\tinfo-\u003eext_ver = payload[8];\n+}\n+\n+static int rtpse_mcu_gen1_parse_port_class(const struct rtpse_mcu_port_status *status)\n+{\n+\t/* Gen1 puts the detected class in payload[3] (== sts3) directly.\n+\t * Mask to the low nibble; class is 0..8 and any high bits would be\n+\t * noise.\n+\t */\n+\treturn status-\u003ests3 \u0026 0x0f;\n+}\n+\n+static const char *rtpse_mcu_gen1_mcu_type_str(unsigned int mcu_type)\n+{\n+\tswitch (mcu_type) {\n+\tcase 0x00:\treturn \"ST Micro ST32F100\";\n+\tcase 0x01:\treturn \"Nuvoton M05xx LAN\";\n+\tcase 0x02:\treturn \"ST Micro STF030C8\";\n+\tcase 0x03:\treturn \"Nuvoton M058SAN\";\n+\tcase 0x04:\treturn \"Nuvoton NUC122\";\n+\tdefault:\treturn \"unknown\";\n+\t}\n+}\n+\n+/* Map each logical command the core issues to its per-dialect opcode. */\n+static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen2 = {\n+\t.parse_system_info = rtpse_mcu_gen2_parse_system_info,\n+\t.parse_port_class  = rtpse_mcu_gen2_parse_port_class,\n+\t.mcu_type_str      = rtpse_mcu_gen2_mcu_type_str,\n+\t.opcode = {\n+\t\t[RTPSE_MCU_CMD_SET_GLOBAL_STATE]\t= RTPSE_MCU_OP(0x00),\n+\t\t[RTPSE_MCU_CMD_GET_SYSTEM_INFO]\t\t= RTPSE_MCU_OP(0x40),\n+\t\t[RTPSE_MCU_CMD_GET_EXT_CONFIG]\t\t= RTPSE_MCU_OP(0x4a),\n+\n+\t\t[RTPSE_MCU_CMD_PORT_ENABLE]\t\t= RTPSE_MCU_OP(0x01),\n+\t\t[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x12),\n+\t\t[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT]\t= RTPSE_MCU_OP(0x13),\n+\t\t[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT] = RTPSE_MCU_OP(0x14),\n+\t\t[RTPSE_MCU_CMD_PORT_SET_PRIORITY]\t= RTPSE_MCU_OP(0x15),\n+\t\t[RTPSE_MCU_CMD_PORT_GET_STATUS]\t\t= RTPSE_MCU_OP(0x42),\n+\t\t[RTPSE_MCU_CMD_PORT_GET_POWER_STATS]\t= RTPSE_MCU_OP(0x44),\n+\t\t[RTPSE_MCU_CMD_PORT_GET_CONFIG]\t\t= RTPSE_MCU_OP(0x48),\n+\t\t[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG]\t= RTPSE_MCU_OP(0x49),\n+\t},\n+};\n+\n+static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen1 = {\n+\t.parse_system_info = rtpse_mcu_gen1_parse_system_info,\n+\t.parse_port_class  = rtpse_mcu_gen1_parse_port_class,\n+\t.mcu_type_str      = rtpse_mcu_gen1_mcu_type_str,\n+\t.opcode = {\n+\t\t[RTPSE_MCU_CMD_GET_SYSTEM_INFO]\t\t= RTPSE_MCU_OP(0x20),\n+\t\t[RTPSE_MCU_CMD_GET_EXT_CONFIG]\t\t= RTPSE_MCU_OP(0x2b),\n+\n+\t\t[RTPSE_MCU_CMD_PORT_ENABLE]\t\t= RTPSE_MCU_OP(0x00),\n+\t\t[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x15),\n+\t\t[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT]\t= RTPSE_MCU_OP(0x16),\n+\t\t[RTPSE_MCU_CMD_PORT_SET_PRIORITY]\t= RTPSE_MCU_OP(0x1a),\n+\t\t[RTPSE_MCU_CMD_PORT_GET_STATUS]\t\t= RTPSE_MCU_OP(0x21),\n+\t\t[RTPSE_MCU_CMD_PORT_GET_POWER_STATS]\t= RTPSE_MCU_OP(0x30),\n+\t\t[RTPSE_MCU_CMD_PORT_GET_CONFIG]\t\t= RTPSE_MCU_OP(0x25),\n+\t\t[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG]\t= RTPSE_MCU_OP(0x26),\n+\t},\n+};\n+\n+const struct rtpse_mcu_match_data rtpse_mcu_gen1_data = {\n+\t.dialect = \u0026rtpse_mcu_dialect_gen1,\n+};\n+EXPORT_SYMBOL_GPL(rtpse_mcu_gen1_data);\n+\n+const struct rtpse_mcu_match_data rtpse_mcu_gen2_data = {\n+\t.dialect = \u0026rtpse_mcu_dialect_gen2,\n+};\n+EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_data);\n+\n+/* Same dialect as gen2, but the MCU expects raw-I2C framing. */\n+const struct rtpse_mcu_match_data rtpse_mcu_gen2_i2c_data = {\n+\t.dialect = \u0026rtpse_mcu_dialect_gen2,\n+\t.native_i2c = true,\n+};\n+EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_i2c_data);\n+\n+MODULE_AUTHOR(\"Jonas Jelonek \u003cjelonek.jonas@gmail.com\u003e\");\n+MODULE_DESCRIPTION(\"Realtek PSE MCU driver (core)\");\n+MODULE_LICENSE(\"GPL\");\ndiff --git a/drivers/net/pse-pd/realtek-pse-mcu-i2c.c b/drivers/net/pse-pd/realtek-pse-mcu-i2c.c\nnew file mode 100644\nindex 00000000000000..e10f8c633b68ec\n--- /dev/null\n+++ b/drivers/net/pse-pd/realtek-pse-mcu-i2c.c\n@@ -0,0 +1,170 @@\n+// SPDX-License-Identifier: GPL-2.0-or-later\n+\n+#include \u003clinux/delay.h\u003e\n+#include \u003clinux/i2c.h\u003e\n+#include \u003clinux/module.h\u003e\n+#include \u003clinux/of.h\u003e\n+#include \u003clinux/pse-pd/pse.h\u003e\n+#include \u003clinux/slab.h\u003e\n+#include \u003clinux/string.h\u003e\n+\n+#include \"realtek-pse-mcu.h\"\n+\n+/*\n+ * The core has already waited RTPSE_MCU_RESPONSE_MS before calling us, so\n+ * the response is normally ready on the very first read. For commands the\n+ * MCU produces more slowly, keep polling at the typical response cadence\n+ * up to the worst-case ceiling.\n+ */\n+#define RTPSE_MCU_I2C_RETRY_MS\tRTPSE_MCU_RESPONSE_MS\n+#define RTPSE_MCU_I2C_MAX_TRIES\t(RTPSE_MCU_RESPONSE_MAX_MS / RTPSE_MCU_I2C_RETRY_MS)\n+\n+static int rtpse_mcu_i2c_smbus_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)\n+{\n+\tstruct i2c_client *client = to_i2c_client(pse-\u003edev);\n+\n+\t/* Send opcode as SMBus command byte; remaining 11 bytes as block data */\n+\treturn i2c_smbus_write_i2c_block_data(client, req-\u003eopcode, RTPSE_MCU_MSG_SIZE - 1,\n+\t\t\t\t\t      (u8 *)req + 1);\n+}\n+\n+static int rtpse_mcu_i2c_smbus_recv(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,\n+\t\t\t\t    struct rtpse_mcu_msg *resp)\n+{\n+\tstruct i2c_client *client = to_i2c_client(pse-\u003edev);\n+\tint tries, ret;\n+\n+\tfor (tries = 0; tries \u003c RTPSE_MCU_I2C_MAX_TRIES; tries++) {\n+\t\tif (tries \u003e 0)\n+\t\t\tmsleep(RTPSE_MCU_I2C_RETRY_MS);\n+\n+\t\t/* MCU needs 0x00 as command byte for read */\n+\t\tret = i2c_smbus_read_i2c_block_data(client, 0x00,\n+\t\t\t\t\t\t    RTPSE_MCU_MSG_SIZE,\n+\t\t\t\t\t\t    (u8 *)resp);\n+\t\tif (ret \u003c 0)\n+\t\t\treturn ret;\n+\t\tif (ret == RTPSE_MCU_MSG_SIZE \u0026\u0026 rtpse_mcu_resp_is_final(req, resp))\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn -ETIMEDOUT;\n+}\n+\n+static const struct rtpse_mcu_transport_ops rtpse_mcu_i2c_smbus_ops = {\n+\t.send = rtpse_mcu_i2c_smbus_send,\n+\t.recv = rtpse_mcu_i2c_smbus_recv,\n+};\n+\n+static int rtpse_mcu_i2c_native_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)\n+{\n+\tstruct i2c_client *client = to_i2c_client(pse-\u003edev);\n+\tvoid *buf;\n+\tint ret;\n+\n+\t/* i2c_master_send() may DMA, so the buffer must not be on the stack. */\n+\tbuf = kmemdup(req, RTPSE_MCU_MSG_SIZE, GFP_KERNEL);\n+\tif (!buf)\n+\t\treturn -ENOMEM;\n+\n+\tret = i2c_master_send(client, buf, RTPSE_MCU_MSG_SIZE);\n+\tkfree(buf);\n+\tif (ret \u003c 0)\n+\t\treturn ret;\n+\treturn ret == RTPSE_MCU_MSG_SIZE ? 0 : -EIO;\n+}\n+\n+static int rtpse_mcu_i2c_native_recv(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,\n+\t\t\t\t     struct rtpse_mcu_msg *resp)\n+{\n+\tstruct i2c_client *client = to_i2c_client(pse-\u003edev);\n+\tint tries, ret;\n+\tu8 *buf;\n+\n+\t/* i2c_master_recv() may DMA, so read into an off-stack buffer. */\n+\tbuf = kmalloc(RTPSE_MCU_MSG_SIZE, GFP_KERNEL);\n+\tif (!buf)\n+\t\treturn -ENOMEM;\n+\n+\tfor (tries = 0; tries \u003c RTPSE_MCU_I2C_MAX_TRIES; tries++) {\n+\t\tif (tries \u003e 0)\n+\t\t\tmsleep(RTPSE_MCU_I2C_RETRY_MS);\n+\n+\t\tret = i2c_master_recv(client, buf, RTPSE_MCU_MSG_SIZE);\n+\t\tif (ret \u003c 0)\n+\t\t\tgoto out;\n+\t\tif (ret == RTPSE_MCU_MSG_SIZE) {\n+\t\t\tmemcpy(resp, buf, RTPSE_MCU_MSG_SIZE);\n+\t\t\tif (rtpse_mcu_resp_is_final(req, resp)) {\n+\t\t\t\tret = 0;\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t}\n+\t}\n+\tret = -ETIMEDOUT;\n+out:\n+\tkfree(buf);\n+\treturn ret;\n+}\n+\n+static const struct rtpse_mcu_transport_ops rtpse_mcu_i2c_native_ops = {\n+\t.send = rtpse_mcu_i2c_native_send,\n+\t.recv = rtpse_mcu_i2c_native_recv,\n+};\n+\n+static int rtpse_mcu_i2c_probe(struct i2c_client *client)\n+{\n+\tstruct device *dev = \u0026client-\u003edev;\n+\tconst struct rtpse_mcu_match_data *match;\n+\tstruct rtpse_mcu_ctrl *pse;\n+\tbool use_native;\n+\n+\tmatch = device_get_match_data(dev);\n+\tif (!match)\n+\t\treturn dev_err_probe(dev, -ENODEV, \"missing match data\\n\");\n+\n+\t/* The framing (raw I2C vs SMBus) is carried by the match data. */\n+\tuse_native = match-\u003enative_i2c;\n+\tif (use_native) {\n+\t\tif (!i2c_check_functionality(client-\u003eadapter, I2C_FUNC_I2C))\n+\t\t\treturn dev_err_probe(dev, -EOPNOTSUPP,\n+\t\t\t\t\"plain-I2C MCU protocol requires I2C-capable adapter\\n\");\n+\t} else {\n+\t\tif (!i2c_check_functionality(client-\u003eadapter,\n+\t\t\t\t\t     I2C_FUNC_SMBUS_WRITE_I2C_BLOCK |\n+\t\t\t\t\t     I2C_FUNC_SMBUS_READ_I2C_BLOCK))\n+\t\t\treturn dev_err_probe(dev, -EOPNOTSUPP,\n+\t\t\t\t\"SMBus MCU protocol requires SMBus I2C-block support\\n\");\n+\t}\n+\n+\tpse = devm_kzalloc(dev, sizeof(*pse), GFP_KERNEL);\n+\tif (!pse)\n+\t\treturn -ENOMEM;\n+\n+\tpse-\u003edev = dev;\n+\tpse-\u003epcdev.owner = THIS_MODULE;\n+\tpse-\u003etransport = use_native ? \u0026rtpse_mcu_i2c_native_ops : \u0026rtpse_mcu_i2c_smbus_ops;\n+\n+\treturn rtpse_mcu_register(pse);\n+}\n+\n+static const struct of_device_id rtpse_mcu_i2c_of_match[] = {\n+\t{ .compatible = \"realtek,pse-mcu-gen1-smbus\", .data = \u0026rtpse_mcu_gen1_data },\n+\t{ .compatible = \"realtek,pse-mcu-gen2-smbus\", .data = \u0026rtpse_mcu_gen2_data },\n+\t{ .compatible = \"realtek,pse-mcu-gen2-i2c\", .data = \u0026rtpse_mcu_gen2_i2c_data },\n+\t{ /* sentinel */ }\n+};\n+MODULE_DEVICE_TABLE(of, rtpse_mcu_i2c_of_match);\n+\n+static struct i2c_driver rtpse_mcu_i2c_driver = {\n+\t.driver = {\n+\t\t.name\t\t= \"realtek-pse-mcu-i2c\",\n+\t\t.of_match_table\t= rtpse_mcu_i2c_of_match,\n+\t},\n+\t.probe\t\t= rtpse_mcu_i2c_probe,\n+};\n+module_i2c_driver(rtpse_mcu_i2c_driver);\n+\n+MODULE_AUTHOR(\"Jonas Jelonek \u003cjelonek.jonas@gmail.com\u003e\");\n+MODULE_DESCRIPTION(\"Realtek PSE MCU driver (I2C transport)\");\n+MODULE_LICENSE(\"GPL\");\ndiff --git a/drivers/net/pse-pd/realtek-pse-mcu-uart.c b/drivers/net/pse-pd/realtek-pse-mcu-uart.c\nnew file mode 100644\nindex 00000000000000..9baa17d8d31fa2\n--- /dev/null\n+++ b/drivers/net/pse-pd/realtek-pse-mcu-uart.c\n@@ -0,0 +1,164 @@\n+// SPDX-License-Identifier: GPL-2.0-or-later\n+\n+#include \u003clinux/cleanup.h\u003e\n+#include \u003clinux/completion.h\u003e\n+#include \u003clinux/module.h\u003e\n+#include \u003clinux/of.h\u003e\n+#include \u003clinux/pse-pd/pse.h\u003e\n+#include \u003clinux/serdev.h\u003e\n+#include \u003clinux/spinlock.h\u003e\n+#include \u003clinux/string.h\u003e\n+\n+#include \"realtek-pse-mcu.h\"\n+\n+#define RTPSE_MCU_UART_BAUD_DEFAULT\t19200\n+#define RTPSE_MCU_UART_TX_TIMEOUT\tmsecs_to_jiffies(100)\n+#define RTPSE_MCU_UART_RX_TIMEOUT\tmsecs_to_jiffies(RTPSE_MCU_RESPONSE_MAX_MS)\n+\n+struct rtpse_mcu_uart {\n+\tstruct rtpse_mcu_ctrl pse;\n+\tstruct serdev_device *serdev;\n+\tstruct completion rx_done;\n+\tspinlock_t rx_lock;\t\t/* protects rx_buf and rx_len */\n+\tsize_t rx_len;\n+\tu8 rx_buf[RTPSE_MCU_MSG_SIZE];\n+};\n+\n+#define to_rtpse_mcu_uart(p)  container_of(p, struct rtpse_mcu_uart, pse)\n+\n+/*\n+ * No framing is done here: a glitched frame costs one transaction, then\n+ * the next _send re-frames from rx_len 0. Resync works by returning count\n+ * (not take), dropping any overflow so serdev keeps no leftover to bleed\n+ * into the next frame.\n+ */\n+static size_t rtpse_mcu_uart_receive(struct serdev_device *serdev,\n+\t\t\t\t     const u8 *buf, size_t count)\n+{\n+\tstruct rtpse_mcu_uart *ctx = serdev_device_get_drvdata(serdev);\n+\tsize_t take;\n+\n+\tscoped_guard(spinlock_irqsave, \u0026ctx-\u003erx_lock) {\n+\t\ttake = min(count, sizeof(ctx-\u003erx_buf) - ctx-\u003erx_len);\n+\t\tif (take) {\n+\t\t\tmemcpy(ctx-\u003erx_buf + ctx-\u003erx_len, buf, take);\n+\t\t\tctx-\u003erx_len += take;\n+\t\t\tif (ctx-\u003erx_len == sizeof(ctx-\u003erx_buf))\n+\t\t\t\tcomplete(\u0026ctx-\u003erx_done);\n+\t\t}\n+\t}\n+\n+\t/* consume all to avoid desync/misalignment */\n+\treturn count;\n+}\n+\n+static const struct serdev_device_ops rtpse_mcu_uart_serdev_ops = {\n+\t.receive_buf = rtpse_mcu_uart_receive,\n+\t.write_wakeup = serdev_device_write_wakeup,\n+};\n+\n+static int rtpse_mcu_uart_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)\n+{\n+\tstruct rtpse_mcu_uart *ctx = to_rtpse_mcu_uart(pse);\n+\tint written;\n+\n+\t/* clear any leftover rx state before transmitting */\n+\tscoped_guard(spinlock_irqsave, \u0026ctx-\u003erx_lock) {\n+\t\treinit_completion(\u0026ctx-\u003erx_done);\n+\t\tctx-\u003erx_len = 0;\n+\t}\n+\n+\twritten = serdev_device_write(ctx-\u003eserdev, (const u8 *)req, sizeof(*req),\n+\t\t\t\t      RTPSE_MCU_UART_TX_TIMEOUT);\n+\tif (written \u003c 0)\n+\t\treturn written;\n+\tif (written != sizeof(*req))\n+\t\treturn -EIO;\n+\n+\treturn 0;\n+}\n+\n+static int rtpse_mcu_uart_recv(struct rtpse_mcu_ctrl *pse,\n+\t\t\t       const struct rtpse_mcu_msg *req,\n+\t\t\t       struct rtpse_mcu_msg *resp)\n+{\n+\tstruct rtpse_mcu_uart *ctx = to_rtpse_mcu_uart(pse);\n+\n+\tif (!wait_for_completion_timeout(\u0026ctx-\u003erx_done, RTPSE_MCU_UART_RX_TIMEOUT))\n+\t\treturn -ETIMEDOUT;\n+\n+\tscoped_guard(spinlock_irqsave, \u0026ctx-\u003erx_lock) {\n+\t\tif (ctx-\u003erx_len != sizeof(*resp))\n+\t\t\treturn -EIO;\n+\n+\t\tmemcpy(resp, ctx-\u003erx_buf, sizeof(*resp));\n+\t}\n+\treturn 0;\n+}\n+\n+static const struct rtpse_mcu_transport_ops rtpse_mcu_uart_transport_ops = {\n+\t.send = rtpse_mcu_uart_send,\n+\t.recv = rtpse_mcu_uart_recv,\n+};\n+\n+static int rtpse_mcu_uart_probe(struct serdev_device *serdev)\n+{\n+\tu32 speed = RTPSE_MCU_UART_BAUD_DEFAULT;\n+\tstruct device *dev = \u0026serdev-\u003edev;\n+\tstruct rtpse_mcu_uart *ctx;\n+\tunsigned int baud;\n+\tint ret;\n+\n+\tctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);\n+\tif (!ctx)\n+\t\treturn -ENOMEM;\n+\n+\tctx-\u003eserdev = serdev;\n+\tctx-\u003epse.dev = dev;\n+\tctx-\u003epse.pcdev.owner = THIS_MODULE;\n+\tctx-\u003epse.transport = \u0026rtpse_mcu_uart_transport_ops;\n+\tinit_completion(\u0026ctx-\u003erx_done);\n+\tspin_lock_init(\u0026ctx-\u003erx_lock);\n+\n+\tserdev_device_set_drvdata(serdev, ctx);\n+\tserdev_device_set_client_ops(serdev, \u0026rtpse_mcu_uart_serdev_ops);\n+\n+\tret = devm_serdev_device_open(dev, serdev);\n+\tif (ret)\n+\t\treturn dev_err_probe(dev, ret, \"failed to open serdev\\n\");\n+\n+\tfwnode_property_read_u32(dev_fwnode(dev), \"current-speed\", \u0026speed);\n+\n+\tbaud = serdev_device_set_baudrate(serdev, speed);\n+\tif (baud != speed)\n+\t\tdev_warn(dev, \"could not set baudrate %u, controller uses %u\\n\",\n+\t\t\t speed, baud);\n+\n+\tserdev_device_set_flow_control(serdev, false);\n+\n+\tret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);\n+\tif (ret)\n+\t\tdev_warn(dev, \"could not set parity to none: %d\\n\", ret);\n+\n+\treturn rtpse_mcu_register(\u0026ctx-\u003epse);\n+}\n+\n+static const struct of_device_id rtpse_mcu_uart_of_match[] = {\n+\t{ .compatible = \"realtek,pse-mcu-gen1\", .data = \u0026rtpse_mcu_gen1_data },\n+\t{ .compatible = \"realtek,pse-mcu-gen2\", .data = \u0026rtpse_mcu_gen2_data },\n+\t{ /* sentinel */ }\n+};\n+MODULE_DEVICE_TABLE(of, rtpse_mcu_uart_of_match);\n+\n+static struct serdev_device_driver rtpse_mcu_uart_driver = {\n+\t.driver = {\n+\t\t.name = \"realtek-pse-mcu-uart\",\n+\t\t.of_match_table = rtpse_mcu_uart_of_match,\n+\t},\n+\t.probe  = rtpse_mcu_uart_probe,\n+};\n+module_serdev_device_driver(rtpse_mcu_uart_driver);\n+\n+MODULE_AUTHOR(\"Jonas Jelonek \u003cjelonek.jonas@gmail.com\u003e\");\n+MODULE_DESCRIPTION(\"Realtek PSE MCU driver (UART transport)\");\n+MODULE_LICENSE(\"GPL\");\ndiff --git a/drivers/net/pse-pd/realtek-pse-mcu.h b/drivers/net/pse-pd/realtek-pse-mcu.h\nnew file mode 100644\nindex 00000000000000..0a3197c9c9cd1d\n--- /dev/null\n+++ b/drivers/net/pse-pd/realtek-pse-mcu.h\n@@ -0,0 +1,89 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+\n+#ifndef _REALTEK_PSE_MCU_H\n+#define _REALTEK_PSE_MCU_H\n+\n+#include \u003clinux/mutex.h\u003e\n+#include \u003clinux/pse-pd/pse.h\u003e\n+#include \u003clinux/types.h\u003e\n+\n+/*\n+ * Time the MCU itself needs between accepting a request and having a\n+ * response ready. These are properties of the MCU firmware, not of the\n+ * underlying transport: the core paces transactions by RTPSE_MCU_RESPONSE_MS\n+ * and both transports size their per-transaction recv ceiling from\n+ * RTPSE_MCU_RESPONSE_MAX_MS, since some commands are documented as\n+ * needing up to ~1s to produce a reply.\n+ */\n+#define RTPSE_MCU_RESPONSE_MS\t\t\t25\n+#define RTPSE_MCU_RESPONSE_MAX_MS\t\t1000\n+\n+/*\n+ * Total time to keep retrying the first MCU read at probe, and the pause\n+ * between attempts. Right after enable-gpios is asserted the MCU may not\n+ * answer on the bus yet; give it a bounded window to come up before\n+ * declaring the probe failed.\n+ */\n+#define RTPSE_MCU_BOOT_TIMEOUT_MS\t\t3000\n+#define RTPSE_MCU_BOOT_RETRY_MS\t\t\t100\n+\n+#define RTPSE_MCU_MSG_SIZE\t\t\t12\n+\n+struct rtpse_mcu_msg {\n+\tu8 opcode;\n+\tu8 seq_num;\n+\tu8 payload[9];\n+\tu8 checksum;\n+} __packed;\n+\n+/*\n+ * MCU status opcodes (seen on the Gen1 dialect; Gen2 never emits them).\n+ * INCOMPLETE/BAD_CSUM are terminal; NOT_READY is transient.\n+ */\n+#define RTPSE_MCU_OPCODE_INCOMPLETE\t\t0xfd\t/* -EBADE   */\n+#define RTPSE_MCU_OPCODE_BAD_CSUM\t\t0xfe\t/* -EBADMSG */\n+#define RTPSE_MCU_OPCODE_NOT_READY\t\t0xff\t/* -EAGAIN  */\n+\n+/* A polling transport can stop here: the matching reply, or a terminal error. */\n+static inline bool rtpse_mcu_resp_is_final(const struct rtpse_mcu_msg *req,\n+\t\t\t\t\t   const struct rtpse_mcu_msg *resp)\n+{\n+\treturn resp-\u003eopcode == req-\u003eopcode ||\n+\t       resp-\u003eopcode == RTPSE_MCU_OPCODE_INCOMPLETE ||\n+\t       resp-\u003eopcode == RTPSE_MCU_OPCODE_BAD_CSUM;\n+}\n+\n+/* Opaque to transports; defined in realtek-pse-core.c. */\n+struct rtpse_mcu_dialect;\n+struct rtpse_mcu_chip_info;\n+struct rtpse_mcu_ctrl;\n+\n+/* Per-compatible match data (the of_match .data). */\n+struct rtpse_mcu_match_data {\n+\tconst struct rtpse_mcu_dialect *dialect;\n+\tbool native_i2c;\t/* raw-I2C framing (vs SMBus); I2C transport only */\n+};\n+\n+struct rtpse_mcu_transport_ops {\n+\tint (*send)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req);\n+\tint (*recv)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,\n+\t\t    struct rtpse_mcu_msg *resp);\n+};\n+\n+struct rtpse_mcu_ctrl {\n+\tstruct device *dev;\n+\tstruct pse_controller_dev pcdev;\n+\tstruct mutex mutex; /* serializes MCU request/response transactions */\n+\tconst struct rtpse_mcu_dialect *dialect;\n+\tconst struct rtpse_mcu_chip_info *chip;\n+\tconst struct rtpse_mcu_transport_ops *transport;\n+\tu8 seq;\t\t\t/* rolling request seq_num, echoed by the MCU */\n+};\n+\n+int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse);\n+\n+extern const struct rtpse_mcu_match_data rtpse_mcu_gen1_data;\n+extern const struct rtpse_mcu_match_data rtpse_mcu_gen2_data;\n+extern const struct rtpse_mcu_match_data rtpse_mcu_gen2_i2c_data;\n+\n+#endif\n"
}

2/1 2026/07/27 01:15 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "PSE_CONTROLLER",
    "PSE_REALTEK_MCU_I2C",
    "PSE_REALTEK_MCU_UART",
    "SERIAL_DEV_BUS",
    "I2C"
  ],
  "FocusSymbols": [
    "rtpse_mcu_register",
    "rtpse_mcu_uart_probe",
    "rtpse_mcu_i2c_probe",
    "rtpse_mcu_port_get_admin_state",
    "rtpse_mcu_port_get_pw_status",
    "rtpse_mcu_port_get_pw_class",
    "rtpse_mcu_port_get_actual_pw",
    "rtpse_mcu_port_get_voltage",
    "rtpse_mcu_port_enable",
    "rtpse_mcu_port_disable",
    "rtpse_mcu_port_get_pw_limit",
    "rtpse_mcu_port_set_pw_limit",
    "rtpse_mcu_port_get_pw_limit_ranges",
    "rtpse_mcu_port_get_prio",
    "rtpse_mcu_port_set_prio",
    "rtpse_mcu_uart_receive"
  ],
  "Reasoning": "This patch introduces a new driver for the Realtek PSE MCU, supporting both I2C and UART transports. The driver exposes PSE controller operations to userspace via ethtool netlink and parses data received from the device over I2C/UART. It can be probed in virtual environments via device tree overlays or I2C sysfs, making it reachable and suitable for fuzzing.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit b86c21662059da481e719ab9adaeae877a0681e2
Author: syz-cluster <triage@syzkaller.com>
Date:   Mon Jul 27 01:15:15 2026 +0000

    syz-cluster: applied patch under review

diff --git a/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
new file mode 100644
index 00000000000000..0f1a819db49bbc
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
@@ -0,0 +1,180 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/pse-pd/realtek,pse-mcu-gen1.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Realtek PSE MCU
+
+maintainers:
+  - Jonas Jelonek <jelonek.jonas@gmail.com>
+
+description: |
+  A microcontroller (MCU) that manages the PSE (Power Sourcing Equipment)
+  hardware on a range of managed PoE switches. The host CPU talks only to
+  this MCU - over I2C/SMBus or UART - using a small message-based protocol;
+  the PSE silicon it drives sits behind the MCU and is never accessed
+  directly. For example, on the Zyxel GS1900-10HP the SoC reaches the MCU
+  over UART, and the MCU manages the on-board PSE chip.
+
+  This binding describes the MCU together with its Realtek firmware: the
+  firmware and its host protocol, which are stable across boards. The
+  microcontroller silicon is a general-purpose part that varies, and the
+  PSE silicon behind the MCU (Realtek RTL823x/RTL8239* or Broadcom
+  BCM59xxx) is reported by the MCU and detected at runtime - neither is
+  named here.
+
+  Two protocol generations exist, both Realtek's:
+    gen1  older boards, where the MCU fronts Broadcom PSE silicon
+    gen2  the altered protocol used with Realtek's own PSE silicon
+
+  On an I2C attachment the framing the MCU firmware expects is part of the
+  compatible: '-smbus' (reads carry a leading command byte and a repeated
+  start) or '-i2c' (bare block writes and reads). A UART attachment carries
+  no framing suffix; the transport is given by the parent 'serial' node.
+
+  Each board additionally carries a device-specific compatible that falls
+  back to one of the protocol compatibles above. Drivers bind on the
+  protocol compatible; the device-specific string identifies the board and
+  reserves a place for a future per-board quirk without having to retrofit
+  device trees already in the field.
+
+properties:
+  compatible:
+    oneOf:
+      # UART
+      - items:
+          - enum:
+              - zyxel,gs1900-10hp-a1-pse
+          - const: realtek,pse-mcu-gen1
+
+      # I2C, SMBus framing
+      - items:
+          - enum:
+              - zyxel,gs1920-24hp-v2-pse
+          - const: realtek,pse-mcu-gen1-smbus
+
+      # UART
+      - items:
+          - enum:
+              - zyxel,gs1900-10hp-b1-pse
+              - zyxel,xmg1915-10ep-pse
+          - const: realtek,pse-mcu-gen2
+
+      # I2C, SMBus framing
+      - items:
+          - enum:
+              - zyxel,xs1930-12hp-pse
+          - const: realtek,pse-mcu-gen2-smbus
+
+      # I2C, raw framing
+      - items:
+          - enum:
+              - linksys,lgs328mpc-v2-pse
+          - const: realtek,pse-mcu-gen2-i2c
+
+  reg:
+    maxItems: 1
+
+  reset-gpios:
+    description: Reset line of the MCU.
+    maxItems: 1
+
+  disable-ports-gpios:
+    description:
+      Hardware gate that forces all ports into admin-disabled state while
+      asserted.
+    maxItems: 1
+
+required:
+  - compatible
+
+allOf:
+  - $ref: pse-controller.yaml#
+  - $ref: /schemas/serial/serial-peripheral-props.yaml#
+  # A '-smbus'/'-i2c' compatible is an I2C attachment: it has 'reg' and
+  # cannot carry serial bus properties. A bare gen compatible is a UART
+  # attachment: no 'reg', the transport comes from the parent serial node.
+  - if:
+      properties:
+        compatible:
+          contains:
+            enum:
+              - realtek,pse-mcu-gen1-smbus
+              - realtek,pse-mcu-gen2-smbus
+              - realtek,pse-mcu-gen2-i2c
+    then:
+      required:
+        - reg
+      properties:
+        current-speed: false
+        max-speed: false
+    else:
+      properties:
+        reg: false
+
+unevaluatedProperties: false
+
+examples:
+  # SMBus-framed I2C attachment
+  - |
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        ethernet-pse@20 {
+            compatible = "zyxel,xs1930-12hp-pse", "realtek,pse-mcu-gen2-smbus";
+            reg = <0x20>;
+
+            pse-pis {
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                pse-pi@0 {
+                    reg = <0>;
+                    #pse-cells = <0>;
+                };
+            };
+        };
+    };
+
+  # Raw-I2C-framed attachment
+  - |
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        ethernet-pse@20 {
+            compatible = "linksys,lgs328mpc-v2-pse", "realtek,pse-mcu-gen2-i2c";
+            reg = <0x20>;
+
+            pse-pis {
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                pse-pi@0 {
+                    reg = <0>;
+                    #pse-cells = <0>;
+                };
+            };
+        };
+    };
+
+  # UART attachment
+  - |
+    serial {
+        ethernet-pse {
+            compatible = "zyxel,gs1900-10hp-a1-pse", "realtek,pse-mcu-gen1";
+            current-speed = <19200>;
+
+            pse-pis {
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                pse-pi@0 {
+                    reg = <0>;
+                    #pse-cells = <0>;
+                };
+            };
+        };
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 60cff00953dcbe..16136ac11e0637 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22736,6 +22736,13 @@ S:	Maintained
 F:	include/sound/rt*.h
 F:	sound/soc/codecs/rt*
 
+REALTEK PSE MCU DRIVER
+M:	Jonas Jelonek <jelonek.jonas@gmail.com>
+L:	netdev@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
+F:	drivers/net/pse-pd/realtek-pse-mcu*
+
 REALTEK OTTO WATCHDOG
 M:	Sander Vanheule <sander@svanheule.net>
 L:	linux-watchdog@vger.kernel.org
diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index 7ef29657ee5dd1..a0f2ae668c67c8 100644
--- a/drivers/net/pse-pd/Kconfig
+++ b/drivers/net/pse-pd/Kconfig
@@ -13,6 +13,34 @@ menuconfig PSE_CONTROLLER
 
 if PSE_CONTROLLER
 
+config PSE_REALTEK_MCU
+	tristate
+	help
+	  Shared core for the Realtek PSE MCU driver. This is selected
+	  automatically by the transport options below.
+
+config PSE_REALTEK_MCU_I2C
+	tristate "Realtek PSE MCU driver (I2C transport)"
+	depends on I2C
+	select PSE_REALTEK_MCU
+	help
+	  Driver for the microcontroller (MCU) that fronts the PSE
+	  hardware on various Realtek-based managed switches, attached
+	  via I2C/SMBus. The MCU exposes a message-based protocol; the actual
+	  PSE silicon is not accessed directly. To compile this driver as a
+	  module, choose M here: the module will be called realtek-pse-mcu-i2c.
+
+config PSE_REALTEK_MCU_UART
+	tristate "Realtek PSE MCU driver (UART transport)"
+	depends on SERIAL_DEV_BUS
+	select PSE_REALTEK_MCU
+	help
+	  Driver for the microcontroller (MCU) that fronts the PSE
+	  hardware on various Realtek-based managed switches, attached
+	  via UART. The MCU exposes a message-based protocol; the actual PSE
+	  silicon is not accessed directly. To compile this driver as a
+	  module, choose M here: the module will be called realtek-pse-mcu-uart.
+
 config PSE_REGULATOR
 	tristate "Regulator based PSE controller"
 	help
diff --git a/drivers/net/pse-pd/Makefile b/drivers/net/pse-pd/Makefile
index cc78f7ea7f5f71..9cca5900fe3421 100644
--- a/drivers/net/pse-pd/Makefile
+++ b/drivers/net/pse-pd/Makefile
@@ -3,6 +3,9 @@
 
 obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
 
+obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
+obj-$(CONFIG_PSE_REALTEK_MCU_I2C) += realtek-pse-mcu-i2c.o
+obj-$(CONFIG_PSE_REALTEK_MCU_UART) += realtek-pse-mcu-uart.o
 obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
 obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
 obj-$(CONFIG_PSE_SI3474) += si3474.o
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-core.c b/drivers/net/pse-pd/realtek-pse-mcu-core.c
new file mode 100644
index 00000000000000..8204cb430ffea8
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-core.c
@@ -0,0 +1,989 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for the microcontroller (MCU) fronting PSE silicon on various
+ * Realtek-based managed switches. The MCU speaks a 12-byte fixed-frame
+ * management protocol; this driver covers two generations of the
+ * protocol via a per-dialect opcode table and response parsers.
+ *
+ * Many PoE switch designs put a dedicated microcontroller in front of the
+ * actual PSE silicon: the host CPU talks to the MCU over I2C/SMBus or
+ * UART, and the MCU in turn manages the PSE chips on the board. The MCU
+ * speaks a small message-based protocol. The PSE chips themselves are not
+ * accessed directly; everything goes through MCU commands.
+ *
+ * This driver targets that architecture for the Realtek-family protocol.
+ * Two generations are supported: Gen1 being used on older switches where
+ * the MCU fronts and manages Broadcom PSE silicon; Gen2 being used with
+ * Realtek PSE silicon. The two share frame format and a sum-mod-256
+ * checksum but diverge on opcode numbers and on a few response layouts;
+ * this is handled by the per-dialect opcode table and parser hooks.
+ *
+ * Out of scope: PSE chips that are interfaced directly from the host
+ * without a management MCU, MCU designs that speak an unrelated protocol
+ * family, and "dumb PSE" modes where no host control is wired up at all.
+ *
+ * This core module implements the protocol, decoding/encoding of MCU
+ * responses, and the pse_controller_ops integration. Transport modules
+ * (realtek-pse-mcu-i2c, realtek-pse-mcu-uart) provide the send/recv
+ * callbacks.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/jiffies.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/unaligned.h>
+
+#include "realtek-pse-mcu.h"
+
+#define RTPSE_MCU_DEVICE_ID_RTL8238B		0x0138
+#define RTPSE_MCU_DEVICE_ID_RTL8239		0x0039
+#define RTPSE_MCU_DEVICE_ID_RTL8239C		0x0139
+#define RTPSE_MCU_DEVICE_ID_BCM59111		0xe111
+#define RTPSE_MCU_DEVICE_ID_BCM59121		0xe121
+
+#define RTPSE_MCU_PORT_STS_DISABLED		0x00
+#define RTPSE_MCU_PORT_STS_SEARCHING		0x01
+#define RTPSE_MCU_PORT_STS_DELIVERING		0x02
+#define RTPSE_MCU_PORT_STS_TEST			0x03	/* Gen1-only; reserved on Gen2 */
+#define RTPSE_MCU_PORT_STS_FAULT		0x04
+#define RTPSE_MCU_PORT_STS_OTHER_FAULT		0x05	/* Gen1-only; reserved on Gen2 */
+#define RTPSE_MCU_PORT_STS_REQUESTING		0x06
+
+/* RTPSE_MCU_PORT_SET_POWER_LIMIT_TYPE values */
+#define RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER	0x02
+
+#define RTPSE_MCU_MAX_PORTS			48
+#define RTPSE_MCU_PORT_MAX_PRIORITY		3
+
+enum rtpse_mcu_cmd {
+	RTPSE_MCU_CMD_SET_GLOBAL_STATE,
+	RTPSE_MCU_CMD_GET_SYSTEM_INFO,
+	RTPSE_MCU_CMD_GET_EXT_CONFIG,
+
+	RTPSE_MCU_CMD_PORT_ENABLE,
+	RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE,
+	RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+	RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+	RTPSE_MCU_CMD_PORT_SET_PRIORITY,
+	RTPSE_MCU_CMD_PORT_GET_STATUS,
+	RTPSE_MCU_CMD_PORT_GET_POWER_STATS,
+	RTPSE_MCU_CMD_PORT_GET_CONFIG,
+	RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG,
+
+	RTPSE_MCU_NUM_CMDS,
+};
+
+struct rtpse_mcu_opcode {
+	u8 op;
+	bool valid;
+};
+
+/* Shorthand for the designated-initializer entries in dialect opcode tables. */
+#define RTPSE_MCU_OP(opc)	{ .op = (opc), .valid = true }
+
+/* Parsed MCU response structures (decoded from rtpse_mcu_msg replies) */
+
+struct rtpse_mcu_info {
+	u8 max_ports;
+	bool system_enable;
+	u16 device_id;
+	u8 sw_ver;
+	u8 mcu_type;
+	u8 config_status;
+	u8 ext_ver;
+};
+
+struct rtpse_mcu_ext_config {
+	u8 uvlo;
+	u8 ovlo;
+	bool prealloc_enable;
+	u8 num_of_pses;
+};
+
+struct rtpse_mcu_port_status {
+	u8 sts1;
+	u8 sts2;
+	u8 sts3;
+};
+
+struct rtpse_mcu_port_measurement {
+	u16 voltage_raw;	/* 64.45mV/LSB */
+	u16 current_raw;	/* 1mA/LSB */
+	u16 temperature_raw;	/* T(mC) = 1250 * (220 - raw) */
+	u16 power_raw;		/* 100mW/LSB */
+};
+
+struct rtpse_mcu_port_config {
+	bool enable;
+};
+
+struct rtpse_mcu_port_ext_config {
+	u8 max_power;
+	u8 priority;
+};
+
+struct rtpse_mcu_dialect {
+	struct rtpse_mcu_opcode opcode[RTPSE_MCU_NUM_CMDS];
+
+	/*
+	 * Response parsers for the fields that differ between dialects; each
+	 * dialect supplies its own. Other responses share one layout and are
+	 * decoded directly - a dialect that diverges there must add a hook,
+	 * as a mismatched layout cannot be detected (the checksum still passes).
+	 */
+	void (*parse_system_info)(const u8 *payload, struct rtpse_mcu_info *info);
+	int (*parse_port_class)(const struct rtpse_mcu_port_status *status);
+	const char *(*mcu_type_str)(unsigned int mcu_type);
+};
+
+struct rtpse_mcu_chip_info {
+	const char *name;
+	u16 device_id;
+	u32 max_mW_per_port;
+	enum rtpse_mcu_cmd pw_set_cmd;	/* command used by set_pw_limit */
+	u32 pw_set_lsb_mW;		/* LSB of pw_set_cmd value, in mW */
+	u32 pw_read_lsb_mW;		/* LSB of ext_config.max_power read-back, in mW */
+};
+
+static const struct rtpse_mcu_chip_info rtl8238b_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_RTL8238B,
+	.max_mW_per_port = 30000,
+	.name = "RTL8238B",
+	.pw_read_lsb_mW = 200,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+	.pw_set_lsb_mW = 200,
+};
+
+static const struct rtpse_mcu_chip_info rtl8239_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_RTL8239,
+	.max_mW_per_port = 90000,
+	.name = "RTL8239",
+	.pw_read_lsb_mW = 400,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+	.pw_set_lsb_mW = 400,
+};
+
+static const struct rtpse_mcu_chip_info rtl8239c_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_RTL8239C,
+	.max_mW_per_port = 90000,
+	.name = "RTL8239C",
+	.pw_read_lsb_mW = 400,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+	.pw_set_lsb_mW = 400,
+};
+
+static const struct rtpse_mcu_chip_info bcm59111_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_BCM59111,
+	.max_mW_per_port = 30000,
+	.name = "BCM59111",
+	.pw_read_lsb_mW = 200,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+	.pw_set_lsb_mW = 200,
+};
+
+static const struct rtpse_mcu_chip_info bcm59121_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_BCM59121,
+	/*
+	 * BCM59121 is a 60W Type-3 part, but known boards run it at 802.3at
+	 * and the Gen1 dialect has only the 8-bit/0.2W set command (<=51W);
+	 * cap at the 30W the hardware actually offers.
+	 */
+	.max_mW_per_port = 30000,
+	.name = "BCM59121",
+	.pw_read_lsb_mW = 200,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+	.pw_set_lsb_mW = 200,
+};
+
+/* Helpers and basic functions */
+
+static struct rtpse_mcu_ctrl *to_rtpse_mcu_ctrl(struct pse_controller_dev *pcdev)
+{
+	return container_of(pcdev, struct rtpse_mcu_ctrl, pcdev);
+}
+
+static void rtpse_mcu_msg_init(struct rtpse_mcu_msg *msg, u8 opcode)
+{
+	memset(msg, 0xff, sizeof(*msg));
+	msg->opcode = opcode;
+}
+
+static u8 rtpse_mcu_checksum(const u8 *buf, size_t len)
+{
+	u8 sum = 0;
+
+	while (len--)
+		sum += *buf++;
+	return sum;
+}
+
+static int rtpse_mcu_do_xfer(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_msg *req,
+			     struct rtpse_mcu_msg *resp)
+{
+	int ret;
+
+	scoped_guard(mutex, &pse->mutex) {
+		/* Rolling seq_num so a late reply can't pass as a later one. */
+		req->seq_num = pse->seq++;
+		req->checksum = rtpse_mcu_checksum((u8 *)req, RTPSE_MCU_MSG_SIZE - 1);
+
+		ret = pse->transport->send(pse, req);
+		if (ret)
+			return ret;
+
+		/*
+		 * The MCU needs a fixed amount of time between receiving a request
+		 * and having the response ready, regardless of how the bytes get to
+		 * us. Pace the transaction here so each transport can keep its recv
+		 * path simple: a single bounded wait rather than a generic retry.
+		 */
+		msleep(RTPSE_MCU_RESPONSE_MS);
+
+		memset(resp, 0, sizeof(*resp));
+		ret = pse->transport->recv(pse, req, resp);
+		if (ret)
+			return ret;
+	}
+
+	/*
+	 * Explicit MCU error opcodes (observed on the Gen1 dialect; harmless
+	 * to check for Gen2 too). Catch these before the generic opcode/CRC
+	 * mismatch path so callers see a meaningful errno.
+	 */
+	switch (resp->opcode) {
+	case RTPSE_MCU_OPCODE_INCOMPLETE:
+		return -EBADE;
+	case RTPSE_MCU_OPCODE_BAD_CSUM:
+		return -EBADMSG;
+	case RTPSE_MCU_OPCODE_NOT_READY:
+		return -EAGAIN;
+	}
+
+	if (resp->opcode != req->opcode ||
+	    resp->seq_num != req->seq_num ||
+	    resp->checksum != rtpse_mcu_checksum((u8 *)resp, RTPSE_MCU_MSG_SIZE - 1))
+		return -EBADMSG;
+
+	return 0;
+}
+
+static int rtpse_mcu_port_query(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode,
+				struct rtpse_mcu_msg *resp)
+{
+	struct rtpse_mcu_msg req;
+	int ret;
+
+	rtpse_mcu_msg_init(&req, opcode);
+	req.payload[0] = port;
+
+	ret = rtpse_mcu_do_xfer(pse, &req, resp);
+	if (ret)
+		return ret;
+
+	if (resp->payload[0] != port)
+		return -EIO;
+
+	return 0;
+}
+
+static int rtpse_mcu_port_cmd(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode, u8 arg)
+{
+	struct rtpse_mcu_msg req, resp;
+	int ret;
+
+	rtpse_mcu_msg_init(&req, opcode);
+	req.payload[0] = port;
+	req.payload[1] = arg;
+
+	ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+	if (ret)
+		return ret;
+
+	if (resp.payload[0] != port || resp.payload[1] != 0)
+		return -EIO;
+
+	return 0;
+}
+
+/* Global operations */
+
+static int rtpse_mcu_get_info(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
+{
+	struct rtpse_mcu_msg req, resp;
+	const struct rtpse_mcu_opcode *opc;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_SYSTEM_INFO];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	rtpse_mcu_msg_init(&req, opc->op);
+	ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+	if (ret)
+		return ret;
+
+	pse->dialect->parse_system_info(resp.payload, info);
+	return 0;
+}
+
+static int rtpse_mcu_get_ext_config(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_ext_config *config)
+{
+	struct rtpse_mcu_msg req, resp;
+	const struct rtpse_mcu_opcode *opc;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_EXT_CONFIG];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	rtpse_mcu_msg_init(&req, opc->op);
+	ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+	if (ret)
+		return ret;
+
+	config->uvlo = resp.payload[0];
+	config->ovlo = resp.payload[5];
+	config->prealloc_enable = (resp.payload[1] == 0x1);
+	config->num_of_pses = resp.payload[6];
+
+	return 0;
+}
+
+static int rtpse_mcu_set_global_state(struct rtpse_mcu_ctrl *pse, bool enable)
+{
+	struct rtpse_mcu_msg req, resp;
+	const struct rtpse_mcu_opcode *opc;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_SET_GLOBAL_STATE];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	rtpse_mcu_msg_init(&req, opc->op);
+	req.payload[0] = enable ? 0x1 : 0x0;
+
+	ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+	if (ret)
+		return ret;
+
+	return (resp.payload[0] == 0x0) ? 0 : -EIO;
+}
+
+/* Port operations */
+
+static int rtpse_mcu_port_get_status(struct rtpse_mcu_ctrl *pse, unsigned int port,
+				     struct rtpse_mcu_port_status *status)
+{
+	const struct rtpse_mcu_opcode *opc;
+	struct rtpse_mcu_msg resp;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_STATUS];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+	if (ret)
+		return ret;
+
+	status->sts1 = resp.payload[1];
+	status->sts2 = resp.payload[2];
+	status->sts3 = resp.payload[3];
+
+	return 0;
+}
+
+static int rtpse_mcu_port_get_measurement(struct rtpse_mcu_ctrl *pse, unsigned int port,
+					  struct rtpse_mcu_port_measurement *measurement)
+{
+	const struct rtpse_mcu_opcode *opc;
+	struct rtpse_mcu_msg resp;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_POWER_STATS];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+	if (ret)
+		return ret;
+
+	measurement->voltage_raw = get_unaligned_be16(&resp.payload[1]);
+	measurement->current_raw = get_unaligned_be16(&resp.payload[3]);
+	measurement->temperature_raw = get_unaligned_be16(&resp.payload[5]);
+	measurement->power_raw = get_unaligned_be16(&resp.payload[7]);
+
+	return 0;
+}
+
+static int rtpse_mcu_port_get_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
+				     struct rtpse_mcu_port_config *config)
+{
+	const struct rtpse_mcu_opcode *opc;
+	struct rtpse_mcu_msg resp;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_CONFIG];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+	if (ret)
+		return ret;
+
+	config->enable = (resp.payload[1] == 1);
+
+	return 0;
+}
+
+static int rtpse_mcu_port_get_ext_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
+					 struct rtpse_mcu_port_ext_config *config)
+{
+	const struct rtpse_mcu_opcode *opc;
+	struct rtpse_mcu_msg resp;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+	if (ret)
+		return ret;
+
+	config->max_power = resp.payload[3];
+	config->priority = resp.payload[4];
+
+	return 0;
+}
+
+static int rtpse_mcu_port_set_state(struct rtpse_mcu_ctrl *pse, unsigned int port, bool enable)
+{
+	const struct rtpse_mcu_opcode *opc;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_ENABLE];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	return rtpse_mcu_port_cmd(pse, port, opc->op, enable ? 0x1 : 0x0);
+}
+
+/* PSE controller ops */
+
+static int rtpse_mcu_port_get_admin_state(struct pse_controller_dev *pcdev, int id,
+					  struct pse_admin_state *admin_state)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_config config;
+	int ret;
+
+	ret = rtpse_mcu_port_get_config(pse, id, &config);
+	if (ret)
+		return ret;
+
+	admin_state->c33_admin_state = config.enable ? ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED :
+						       ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
+	return 0;
+}
+
+static int rtpse_mcu_port_get_pw_status(struct pse_controller_dev *pcdev, int id,
+					struct pse_pw_status *pw_status)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_status status;
+	int ret;
+
+	ret = rtpse_mcu_port_get_status(pse, id, &status);
+	if (ret)
+		return ret;
+
+	switch (status.sts1) {
+	case RTPSE_MCU_PORT_STS_DISABLED:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
+		break;
+	case RTPSE_MCU_PORT_STS_SEARCHING:
+	case RTPSE_MCU_PORT_STS_REQUESTING:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
+		break;
+	case RTPSE_MCU_PORT_STS_DELIVERING:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
+		break;
+	case RTPSE_MCU_PORT_STS_TEST:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_TEST;
+		break;
+	case RTPSE_MCU_PORT_STS_FAULT:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;
+		break;
+	case RTPSE_MCU_PORT_STS_OTHER_FAULT:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_OTHERFAULT;
+		break;
+	default:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_UNKNOWN;
+		break;
+	}
+
+	return 0;
+}
+
+static int rtpse_mcu_port_get_pw_class(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_status status;
+	int ret;
+
+	ret = rtpse_mcu_port_get_status(pse, id, &status);
+	if (ret)
+		return ret;
+
+	/*
+	 * sts2 holds the class only in the operational states below. In a
+	 * fault/test state (or a code reserved on the current dialect) it
+	 * does not, so there is no class to report and we return 0. That is
+	 * indistinguishable from a real class-0 PD; userspace disambiguates
+	 * via the power status.
+	 */
+	switch (status.sts1) {
+	case RTPSE_MCU_PORT_STS_DISABLED:
+	case RTPSE_MCU_PORT_STS_SEARCHING:
+	case RTPSE_MCU_PORT_STS_DELIVERING:
+	case RTPSE_MCU_PORT_STS_REQUESTING:
+		return pse->dialect->parse_port_class(&status);
+	default:
+		return 0;
+	}
+}
+
+static int rtpse_mcu_port_get_actual_pw(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_measurement measurement;
+	int ret;
+
+	ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
+	if (ret)
+		return ret;
+
+	/* 100mW per LSB */
+	return measurement.power_raw * 100U;
+}
+
+static int rtpse_mcu_port_get_voltage(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_measurement measurement;
+	int ret;
+	u32 uV;
+
+	ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
+	if (ret)
+		return ret;
+
+	/* 64.45mV per LSB */
+	uV = measurement.voltage_raw * 64450U;
+	return min_t(u32, uV, INT_MAX);
+}
+
+static int rtpse_mcu_port_enable(struct pse_controller_dev *pcdev, int id)
+{
+	return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, true);
+}
+
+static int rtpse_mcu_port_disable(struct pse_controller_dev *pcdev, int id)
+{
+	return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, false);
+}
+
+static int rtpse_mcu_port_get_pw_limit(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_ext_config config;
+	int ret;
+
+	ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
+	if (ret)
+		return ret;
+
+	return config.max_power * pse->chip->pw_read_lsb_mW;
+}
+
+static int rtpse_mcu_port_set_pw_limit(struct pse_controller_dev *pcdev, int id, int max_mW)
+{
+	const struct rtpse_mcu_opcode *type_opc, *val_opc;
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	const struct rtpse_mcu_chip_info *chip = pse->chip;
+	u8 prg_val;
+	int ret;
+
+	if (max_mW < 0 || max_mW > chip->max_mW_per_port)
+		return -ERANGE;
+
+	type_opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE];
+	val_opc = &pse->dialect->opcode[chip->pw_set_cmd];
+	/* pw_set_lsb_mW is the divisor below; reject a chip that lacks it. */
+	if (!type_opc->valid || !val_opc->valid || !chip->pw_set_lsb_mW)
+		return -EOPNOTSUPP;
+
+	/*
+	 * Switch the port to user-defined limit mode first, then program the
+	 * limit value. If the second cmd fails, the port is left in
+	 * user-defined mode but with the previous limit value; the next
+	 * successful set_pw_limit call recovers it.
+	 */
+	ret = rtpse_mcu_port_cmd(pse, id, type_opc->op, RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER);
+	if (ret)
+		return ret;
+
+	prg_val = min_t(unsigned int, max_mW / chip->pw_set_lsb_mW, U8_MAX);
+
+	return rtpse_mcu_port_cmd(pse, id, val_opc->op, prg_val);
+}
+
+static int rtpse_mcu_port_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
+					      struct pse_pw_limit_ranges *out)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct ethtool_c33_pse_pw_limit_range *range;
+
+	range = kzalloc_obj(*range);
+	if (!range)
+		return -ENOMEM;
+
+	range[0].min = 0;
+	range[0].max = pse->chip->max_mW_per_port;
+
+	out->c33_pw_limit_ranges = range;
+	return 1;
+}
+
+static int rtpse_mcu_port_get_prio(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_ext_config config;
+	int ret;
+
+	ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
+	if (ret)
+		return ret;
+
+	return config.priority;
+}
+
+static int rtpse_mcu_port_set_prio(struct pse_controller_dev *pcdev, int id, unsigned int prio)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	const struct rtpse_mcu_opcode *opc;
+
+	if (prio > RTPSE_MCU_PORT_MAX_PRIORITY)
+		return -ERANGE;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_PRIORITY];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	return rtpse_mcu_port_cmd(pse, id, opc->op, prio);
+}
+
+static const struct pse_controller_ops rtpse_mcu_ops = {
+	.pi_get_admin_state = rtpse_mcu_port_get_admin_state,
+	.pi_get_pw_status = rtpse_mcu_port_get_pw_status,
+	.pi_get_pw_class = rtpse_mcu_port_get_pw_class,
+	.pi_get_actual_pw = rtpse_mcu_port_get_actual_pw,
+	.pi_enable = rtpse_mcu_port_enable,
+	.pi_disable = rtpse_mcu_port_disable,
+	.pi_get_voltage = rtpse_mcu_port_get_voltage,
+	.pi_get_pw_limit = rtpse_mcu_port_get_pw_limit,
+	.pi_set_pw_limit = rtpse_mcu_port_set_pw_limit,
+	.pi_get_pw_limit_ranges = rtpse_mcu_port_get_pw_limit_ranges,
+	.pi_get_prio = rtpse_mcu_port_get_prio,
+	.pi_set_prio = rtpse_mcu_port_set_prio,
+};
+
+static int rtpse_mcu_discover(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
+{
+	struct rtpse_mcu_ext_config ext_config;
+	unsigned long deadline;
+	int ret;
+
+	/*
+	 * A booting MCU may stay silent (-ETIMEDOUT), not ACK its address
+	 * (-ENXIO / -EREMOTEIO), report not-ready (-EAGAIN), or emit a
+	 * corrupt/partial frame (-EBADMSG / -EBADE). Retry those within a
+	 * bounded window; other errors (e.g. -EOPNOTSUPP) are fatal and fail
+	 * immediately.
+	 */
+	deadline = jiffies + msecs_to_jiffies(RTPSE_MCU_BOOT_TIMEOUT_MS);
+	do {
+		ret = rtpse_mcu_get_info(pse, info);
+		if (ret != -ETIMEDOUT && ret != -ENXIO && ret != -EREMOTEIO &&
+		    ret != -EAGAIN && ret != -EBADMSG && ret != -EBADE)
+			break;
+		msleep(RTPSE_MCU_BOOT_RETRY_MS);
+	} while (time_before(jiffies, deadline));
+	if (ret)
+		return dev_err_probe(pse->dev, ret, "failed to read MCU info\n");
+
+	switch (info->device_id) {
+	case RTPSE_MCU_DEVICE_ID_RTL8238B:
+		pse->chip = &rtl8238b_info;
+		break;
+	case RTPSE_MCU_DEVICE_ID_RTL8239:
+		pse->chip = &rtl8239_info;
+		break;
+	case RTPSE_MCU_DEVICE_ID_RTL8239C:
+		pse->chip = &rtl8239c_info;
+		break;
+	case RTPSE_MCU_DEVICE_ID_BCM59111:
+		pse->chip = &bcm59111_info;
+		break;
+	case RTPSE_MCU_DEVICE_ID_BCM59121:
+		pse->chip = &bcm59121_info;
+		break;
+	default:
+		return dev_err_probe(pse->dev, -EINVAL, "unknown PSE id 0x%x\n",
+				     info->device_id);
+	}
+
+	if (!info->max_ports || info->max_ports > RTPSE_MCU_MAX_PORTS)
+		return dev_err_probe(pse->dev, -EINVAL,
+				     "MCU reports invalid port count %u\n", info->max_ports);
+
+	ret = rtpse_mcu_get_ext_config(pse, &ext_config);
+	if (ret)
+		return dev_err_probe(pse->dev, ret, "failed to read MCU ext config\n");
+
+	dev_info(pse->dev, "%s MCU, %s (id 0x%04x), %u ports across %u PSE chip(s)\n",
+		 pse->dialect->mcu_type_str(info->mcu_type), pse->chip->name,
+		 info->device_id, info->max_ports, ext_config.num_of_pses);
+	return 0;
+}
+
+static void rtpse_mcu_global_disable(void *data)
+{
+	struct rtpse_mcu_ctrl *pse = data;
+
+	rtpse_mcu_set_global_state(pse, false);
+}
+
+int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse)
+{
+	const struct rtpse_mcu_match_data *match;
+	struct rtpse_mcu_info info;
+	struct gpio_desc *gpiod;
+	int ret;
+
+	BUILD_BUG_ON(sizeof(struct rtpse_mcu_msg) != RTPSE_MCU_MSG_SIZE);
+
+	ret = devm_mutex_init(pse->dev, &pse->mutex);
+	if (ret)
+		return ret;
+
+	match = device_get_match_data(pse->dev);
+	if (!match)
+		return dev_err_probe(pse->dev, -ENODEV, "missing match data\n");
+	pse->dialect = match->dialect;
+
+	/*
+	 * Catch a dialect that forgot to set one of the required hooks at
+	 * probe time, rather than NULL-deref'ing later from a fast path.
+	 */
+	if (!pse->dialect ||
+	    !pse->dialect->parse_system_info ||
+	    !pse->dialect->parse_port_class ||
+	    !pse->dialect->mcu_type_str)
+		return dev_err_probe(pse->dev, -EINVAL,
+				     "dialect for chip is incomplete\n");
+
+	/*
+	 * Release the MCU from reset before the first transaction; the
+	 * boot-retry loop in discover() waits for it to answer.
+	 */
+	gpiod = devm_gpiod_get_optional(pse->dev, "reset", GPIOD_OUT_LOW);
+	if (IS_ERR(gpiod))
+		return dev_err_probe(pse->dev, PTR_ERR(gpiod),
+				     "failed to get reset gpio\n");
+
+	/*
+	 * Some boards gate all ports through a hardware line; deassert it so
+	 * the MCU protocol governs per-port admin state from here on.
+	 */
+	gpiod = devm_gpiod_get_optional(pse->dev, "disable-ports", GPIOD_OUT_LOW);
+	if (IS_ERR(gpiod))
+		return dev_err_probe(pse->dev, PTR_ERR(gpiod),
+				     "failed to get disable-ports gpio\n");
+
+	ret = rtpse_mcu_discover(pse, &info);
+	if (ret)
+		return ret;
+
+	if (!info.system_enable) {
+		ret = rtpse_mcu_set_global_state(pse, true);
+		/* Dialects without a global-state concept (e.g. Gen1) return
+		 * -EOPNOTSUPP; treat that as "no separate enable required".
+		 */
+		if (ret && ret != -EOPNOTSUPP)
+			return dev_err_probe(pse->dev, ret,
+					     "failed to enable PSE system\n");
+		if (!ret) {
+			ret = devm_add_action_or_reset(pse->dev,
+						       rtpse_mcu_global_disable, pse);
+			if (ret)
+				return ret;
+		}
+	}
+
+	/*
+	 * Depending on the MCU firmware configuration (which might be different
+	 * for every board), it isn't known whether the PoE subsystem is active or
+	 * inactive by default. At this stage, the PSE chips might already deliver
+	 * power to PDs without any explicit enable.
+	 */
+
+	/* pcdev.owner is set by the transport, so the registered controller
+	 * pins the transport module that owns the live device, not the core.
+	 */
+	pse->pcdev.ops      = &rtpse_mcu_ops;
+	pse->pcdev.dev      = pse->dev;
+	pse->pcdev.types    = ETHTOOL_PSE_C33;
+	pse->pcdev.nr_lines = info.max_ports;
+	pse->pcdev.pis_prio_max = RTPSE_MCU_PORT_MAX_PRIORITY;
+	pse->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_DYNAMIC;
+
+	return devm_pse_controller_register(pse->dev, &pse->pcdev);
+}
+EXPORT_SYMBOL_GPL(rtpse_mcu_register);
+
+static void rtpse_mcu_gen2_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
+{
+	info->max_ports = payload[1];
+	info->system_enable = (payload[2] == 0x1);
+	info->device_id = get_unaligned_be16(&payload[3]);
+	info->sw_ver = payload[5];
+	info->mcu_type = payload[6];
+	info->config_status = payload[7];
+	info->ext_ver = payload[8];
+}
+
+static int rtpse_mcu_gen2_parse_port_class(const struct rtpse_mcu_port_status *status)
+{
+	/* Class lives in the upper nibble of sts2. */
+	return FIELD_GET(GENMASK(7, 4), status->sts2);
+}
+
+static const char *rtpse_mcu_gen2_mcu_type_str(unsigned int mcu_type)
+{
+	switch (mcu_type) {
+	case 0x00:	return "GigaDevice GD32F310";
+	case 0x01:	return "GigaDevice GD32F230";
+	case 0x02:	return "GigaDevice GD32F303";
+	case 0x03:	return "GigaDevice GD32F103";
+	case 0x04:	return "GigaDevice GD32E103";
+	case 0x10:	return "Nuvoton M0516";
+	case 0x11:	return "Nuvoton M0564";
+	case 0x12:	return "Nuvoton NUC029";
+	default:	return "unknown";
+	}
+}
+
+static void rtpse_mcu_gen1_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
+{
+	info->max_ports = payload[1];
+	/* Gen1 has no explicit system_enable byte; the closest analog is the
+	 * "remote enable" bit in the system-status flags at payload[7].
+	 */
+	info->system_enable = !!(payload[7] & BIT(2));
+	info->device_id = get_unaligned_be16(&payload[3]);
+	info->sw_ver = payload[5];
+	info->mcu_type = payload[6];
+	info->config_status = payload[7];
+	info->ext_ver = payload[8];
+}
+
+static int rtpse_mcu_gen1_parse_port_class(const struct rtpse_mcu_port_status *status)
+{
+	/* Gen1 puts the detected class in payload[3] (== sts3) directly.
+	 * Mask to the low nibble; class is 0..8 and any high bits would be
+	 * noise.
+	 */
+	return status->sts3 & 0x0f;
+}
+
+static const char *rtpse_mcu_gen1_mcu_type_str(unsigned int mcu_type)
+{
+	switch (mcu_type) {
+	case 0x00:	return "ST Micro ST32F100";
+	case 0x01:	return "Nuvoton M05xx LAN";
+	case 0x02:	return "ST Micro STF030C8";
+	case 0x03:	return "Nuvoton M058SAN";
+	case 0x04:	return "Nuvoton NUC122";
+	default:	return "unknown";
+	}
+}
+
+/* Map each logical command the core issues to its per-dialect opcode. */
+static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen2 = {
+	.parse_system_info = rtpse_mcu_gen2_parse_system_info,
+	.parse_port_class  = rtpse_mcu_gen2_parse_port_class,
+	.mcu_type_str      = rtpse_mcu_gen2_mcu_type_str,
+	.opcode = {
+		[RTPSE_MCU_CMD_SET_GLOBAL_STATE]	= RTPSE_MCU_OP(0x00),
+		[RTPSE_MCU_CMD_GET_SYSTEM_INFO]		= RTPSE_MCU_OP(0x40),
+		[RTPSE_MCU_CMD_GET_EXT_CONFIG]		= RTPSE_MCU_OP(0x4a),
+
+		[RTPSE_MCU_CMD_PORT_ENABLE]		= RTPSE_MCU_OP(0x01),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x12),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT]	= RTPSE_MCU_OP(0x13),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT] = RTPSE_MCU_OP(0x14),
+		[RTPSE_MCU_CMD_PORT_SET_PRIORITY]	= RTPSE_MCU_OP(0x15),
+		[RTPSE_MCU_CMD_PORT_GET_STATUS]		= RTPSE_MCU_OP(0x42),
+		[RTPSE_MCU_CMD_PORT_GET_POWER_STATS]	= RTPSE_MCU_OP(0x44),
+		[RTPSE_MCU_CMD_PORT_GET_CONFIG]		= RTPSE_MCU_OP(0x48),
+		[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG]	= RTPSE_MCU_OP(0x49),
+	},
+};
+
+static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen1 = {
+	.parse_system_info = rtpse_mcu_gen1_parse_system_info,
+	.parse_port_class  = rtpse_mcu_gen1_parse_port_class,
+	.mcu_type_str      = rtpse_mcu_gen1_mcu_type_str,
+	.opcode = {
+		[RTPSE_MCU_CMD_GET_SYSTEM_INFO]		= RTPSE_MCU_OP(0x20),
+		[RTPSE_MCU_CMD_GET_EXT_CONFIG]		= RTPSE_MCU_OP(0x2b),
+
+		[RTPSE_MCU_CMD_PORT_ENABLE]		= RTPSE_MCU_OP(0x00),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x15),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT]	= RTPSE_MCU_OP(0x16),
+		[RTPSE_MCU_CMD_PORT_SET_PRIORITY]	= RTPSE_MCU_OP(0x1a),
+		[RTPSE_MCU_CMD_PORT_GET_STATUS]		= RTPSE_MCU_OP(0x21),
+		[RTPSE_MCU_CMD_PORT_GET_POWER_STATS]	= RTPSE_MCU_OP(0x30),
+		[RTPSE_MCU_CMD_PORT_GET_CONFIG]		= RTPSE_MCU_OP(0x25),
+		[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG]	= RTPSE_MCU_OP(0x26),
+	},
+};
+
+const struct rtpse_mcu_match_data rtpse_mcu_gen1_data = {
+	.dialect = &rtpse_mcu_dialect_gen1,
+};
+EXPORT_SYMBOL_GPL(rtpse_mcu_gen1_data);
+
+const struct rtpse_mcu_match_data rtpse_mcu_gen2_data = {
+	.dialect = &rtpse_mcu_dialect_gen2,
+};
+EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_data);
+
+/* Same dialect as gen2, but the MCU expects raw-I2C framing. */
+const struct rtpse_mcu_match_data rtpse_mcu_gen2_i2c_data = {
+	.dialect = &rtpse_mcu_dialect_gen2,
+	.native_i2c = true,
+};
+EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_i2c_data);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek PSE MCU driver (core)");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-i2c.c b/drivers/net/pse-pd/realtek-pse-mcu-i2c.c
new file mode 100644
index 00000000000000..e10f8c633b68ec
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-i2c.c
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "realtek-pse-mcu.h"
+
+/*
+ * The core has already waited RTPSE_MCU_RESPONSE_MS before calling us, so
+ * the response is normally ready on the very first read. For commands the
+ * MCU produces more slowly, keep polling at the typical response cadence
+ * up to the worst-case ceiling.
+ */
+#define RTPSE_MCU_I2C_RETRY_MS	RTPSE_MCU_RESPONSE_MS
+#define RTPSE_MCU_I2C_MAX_TRIES	(RTPSE_MCU_RESPONSE_MAX_MS / RTPSE_MCU_I2C_RETRY_MS)
+
+static int rtpse_mcu_i2c_smbus_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+	struct i2c_client *client = to_i2c_client(pse->dev);
+
+	/* Send opcode as SMBus command byte; remaining 11 bytes as block data */
+	return i2c_smbus_write_i2c_block_data(client, req->opcode, RTPSE_MCU_MSG_SIZE - 1,
+					      (u8 *)req + 1);
+}
+
+static int rtpse_mcu_i2c_smbus_recv(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+				    struct rtpse_mcu_msg *resp)
+{
+	struct i2c_client *client = to_i2c_client(pse->dev);
+	int tries, ret;
+
+	for (tries = 0; tries < RTPSE_MCU_I2C_MAX_TRIES; tries++) {
+		if (tries > 0)
+			msleep(RTPSE_MCU_I2C_RETRY_MS);
+
+		/* MCU needs 0x00 as command byte for read */
+		ret = i2c_smbus_read_i2c_block_data(client, 0x00,
+						    RTPSE_MCU_MSG_SIZE,
+						    (u8 *)resp);
+		if (ret < 0)
+			return ret;
+		if (ret == RTPSE_MCU_MSG_SIZE && rtpse_mcu_resp_is_final(req, resp))
+			return 0;
+	}
+
+	return -ETIMEDOUT;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_i2c_smbus_ops = {
+	.send = rtpse_mcu_i2c_smbus_send,
+	.recv = rtpse_mcu_i2c_smbus_recv,
+};
+
+static int rtpse_mcu_i2c_native_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+	struct i2c_client *client = to_i2c_client(pse->dev);
+	void *buf;
+	int ret;
+
+	/* i2c_master_send() may DMA, so the buffer must not be on the stack. */
+	buf = kmemdup(req, RTPSE_MCU_MSG_SIZE, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	ret = i2c_master_send(client, buf, RTPSE_MCU_MSG_SIZE);
+	kfree(buf);
+	if (ret < 0)
+		return ret;
+	return ret == RTPSE_MCU_MSG_SIZE ? 0 : -EIO;
+}
+
+static int rtpse_mcu_i2c_native_recv(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+				     struct rtpse_mcu_msg *resp)
+{
+	struct i2c_client *client = to_i2c_client(pse->dev);
+	int tries, ret;
+	u8 *buf;
+
+	/* i2c_master_recv() may DMA, so read into an off-stack buffer. */
+	buf = kmalloc(RTPSE_MCU_MSG_SIZE, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	for (tries = 0; tries < RTPSE_MCU_I2C_MAX_TRIES; tries++) {
+		if (tries > 0)
+			msleep(RTPSE_MCU_I2C_RETRY_MS);
+
+		ret = i2c_master_recv(client, buf, RTPSE_MCU_MSG_SIZE);
+		if (ret < 0)
+			goto out;
+		if (ret == RTPSE_MCU_MSG_SIZE) {
+			memcpy(resp, buf, RTPSE_MCU_MSG_SIZE);
+			if (rtpse_mcu_resp_is_final(req, resp)) {
+				ret = 0;
+				goto out;
+			}
+		}
+	}
+	ret = -ETIMEDOUT;
+out:
+	kfree(buf);
+	return ret;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_i2c_native_ops = {
+	.send = rtpse_mcu_i2c_native_send,
+	.recv = rtpse_mcu_i2c_native_recv,
+};
+
+static int rtpse_mcu_i2c_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	const struct rtpse_mcu_match_data *match;
+	struct rtpse_mcu_ctrl *pse;
+	bool use_native;
+
+	match = device_get_match_data(dev);
+	if (!match)
+		return dev_err_probe(dev, -ENODEV, "missing match data\n");
+
+	/* The framing (raw I2C vs SMBus) is carried by the match data. */
+	use_native = match->native_i2c;
+	if (use_native) {
+		if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
+			return dev_err_probe(dev, -EOPNOTSUPP,
+				"plain-I2C MCU protocol requires I2C-capable adapter\n");
+	} else {
+		if (!i2c_check_functionality(client->adapter,
+					     I2C_FUNC_SMBUS_WRITE_I2C_BLOCK |
+					     I2C_FUNC_SMBUS_READ_I2C_BLOCK))
+			return dev_err_probe(dev, -EOPNOTSUPP,
+				"SMBus MCU protocol requires SMBus I2C-block support\n");
+	}
+
+	pse = devm_kzalloc(dev, sizeof(*pse), GFP_KERNEL);
+	if (!pse)
+		return -ENOMEM;
+
+	pse->dev = dev;
+	pse->pcdev.owner = THIS_MODULE;
+	pse->transport = use_native ? &rtpse_mcu_i2c_native_ops : &rtpse_mcu_i2c_smbus_ops;
+
+	return rtpse_mcu_register(pse);
+}
+
+static const struct of_device_id rtpse_mcu_i2c_of_match[] = {
+	{ .compatible = "realtek,pse-mcu-gen1-smbus", .data = &rtpse_mcu_gen1_data },
+	{ .compatible = "realtek,pse-mcu-gen2-smbus", .data = &rtpse_mcu_gen2_data },
+	{ .compatible = "realtek,pse-mcu-gen2-i2c", .data = &rtpse_mcu_gen2_i2c_data },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rtpse_mcu_i2c_of_match);
+
+static struct i2c_driver rtpse_mcu_i2c_driver = {
+	.driver = {
+		.name		= "realtek-pse-mcu-i2c",
+		.of_match_table	= rtpse_mcu_i2c_of_match,
+	},
+	.probe		= rtpse_mcu_i2c_probe,
+};
+module_i2c_driver(rtpse_mcu_i2c_driver);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek PSE MCU driver (I2C transport)");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-uart.c b/drivers/net/pse-pd/realtek-pse-mcu-uart.c
new file mode 100644
index 00000000000000..9baa17d8d31fa2
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-uart.c
@@ -0,0 +1,164 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/serdev.h>
+#include <linux/spinlock.h>
+#include <linux/string.h>
+
+#include "realtek-pse-mcu.h"
+
+#define RTPSE_MCU_UART_BAUD_DEFAULT	19200
+#define RTPSE_MCU_UART_TX_TIMEOUT	msecs_to_jiffies(100)
+#define RTPSE_MCU_UART_RX_TIMEOUT	msecs_to_jiffies(RTPSE_MCU_RESPONSE_MAX_MS)
+
+struct rtpse_mcu_uart {
+	struct rtpse_mcu_ctrl pse;
+	struct serdev_device *serdev;
+	struct completion rx_done;
+	spinlock_t rx_lock;		/* protects rx_buf and rx_len */
+	size_t rx_len;
+	u8 rx_buf[RTPSE_MCU_MSG_SIZE];
+};
+
+#define to_rtpse_mcu_uart(p)  container_of(p, struct rtpse_mcu_uart, pse)
+
+/*
+ * No framing is done here: a glitched frame costs one transaction, then
+ * the next _send re-frames from rx_len 0. Resync works by returning count
+ * (not take), dropping any overflow so serdev keeps no leftover to bleed
+ * into the next frame.
+ */
+static size_t rtpse_mcu_uart_receive(struct serdev_device *serdev,
+				     const u8 *buf, size_t count)
+{
+	struct rtpse_mcu_uart *ctx = serdev_device_get_drvdata(serdev);
+	size_t take;
+
+	scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+		take = min(count, sizeof(ctx->rx_buf) - ctx->rx_len);
+		if (take) {
+			memcpy(ctx->rx_buf + ctx->rx_len, buf, take);
+			ctx->rx_len += take;
+			if (ctx->rx_len == sizeof(ctx->rx_buf))
+				complete(&ctx->rx_done);
+		}
+	}
+
+	/* consume all to avoid desync/misalignment */
+	return count;
+}
+
+static const struct serdev_device_ops rtpse_mcu_uart_serdev_ops = {
+	.receive_buf = rtpse_mcu_uart_receive,
+	.write_wakeup = serdev_device_write_wakeup,
+};
+
+static int rtpse_mcu_uart_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+	struct rtpse_mcu_uart *ctx = to_rtpse_mcu_uart(pse);
+	int written;
+
+	/* clear any leftover rx state before transmitting */
+	scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+		reinit_completion(&ctx->rx_done);
+		ctx->rx_len = 0;
+	}
+
+	written = serdev_device_write(ctx->serdev, (const u8 *)req, sizeof(*req),
+				      RTPSE_MCU_UART_TX_TIMEOUT);
+	if (written < 0)
+		return written;
+	if (written != sizeof(*req))
+		return -EIO;
+
+	return 0;
+}
+
+static int rtpse_mcu_uart_recv(struct rtpse_mcu_ctrl *pse,
+			       const struct rtpse_mcu_msg *req,
+			       struct rtpse_mcu_msg *resp)
+{
+	struct rtpse_mcu_uart *ctx = to_rtpse_mcu_uart(pse);
+
+	if (!wait_for_completion_timeout(&ctx->rx_done, RTPSE_MCU_UART_RX_TIMEOUT))
+		return -ETIMEDOUT;
+
+	scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+		if (ctx->rx_len != sizeof(*resp))
+			return -EIO;
+
+		memcpy(resp, ctx->rx_buf, sizeof(*resp));
+	}
+	return 0;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_uart_transport_ops = {
+	.send = rtpse_mcu_uart_send,
+	.recv = rtpse_mcu_uart_recv,
+};
+
+static int rtpse_mcu_uart_probe(struct serdev_device *serdev)
+{
+	u32 speed = RTPSE_MCU_UART_BAUD_DEFAULT;
+	struct device *dev = &serdev->dev;
+	struct rtpse_mcu_uart *ctx;
+	unsigned int baud;
+	int ret;
+
+	ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);
+	if (!ctx)
+		return -ENOMEM;
+
+	ctx->serdev = serdev;
+	ctx->pse.dev = dev;
+	ctx->pse.pcdev.owner = THIS_MODULE;
+	ctx->pse.transport = &rtpse_mcu_uart_transport_ops;
+	init_completion(&ctx->rx_done);
+	spin_lock_init(&ctx->rx_lock);
+
+	serdev_device_set_drvdata(serdev, ctx);
+	serdev_device_set_client_ops(serdev, &rtpse_mcu_uart_serdev_ops);
+
+	ret = devm_serdev_device_open(dev, serdev);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to open serdev\n");
+
+	fwnode_property_read_u32(dev_fwnode(dev), "current-speed", &speed);
+
+	baud = serdev_device_set_baudrate(serdev, speed);
+	if (baud != speed)
+		dev_warn(dev, "could not set baudrate %u, controller uses %u\n",
+			 speed, baud);
+
+	serdev_device_set_flow_control(serdev, false);
+
+	ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
+	if (ret)
+		dev_warn(dev, "could not set parity to none: %d\n", ret);
+
+	return rtpse_mcu_register(&ctx->pse);
+}
+
+static const struct of_device_id rtpse_mcu_uart_of_match[] = {
+	{ .compatible = "realtek,pse-mcu-gen1", .data = &rtpse_mcu_gen1_data },
+	{ .compatible = "realtek,pse-mcu-gen2", .data = &rtpse_mcu_gen2_data },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rtpse_mcu_uart_of_match);
+
+static struct serdev_device_driver rtpse_mcu_uart_driver = {
+	.driver = {
+		.name = "realtek-pse-mcu-uart",
+		.of_match_table = rtpse_mcu_uart_of_match,
+	},
+	.probe  = rtpse_mcu_uart_probe,
+};
+module_serdev_device_driver(rtpse_mcu_uart_driver);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek PSE MCU driver (UART transport)");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/pse-pd/realtek-pse-mcu.h b/drivers/net/pse-pd/realtek-pse-mcu.h
new file mode 100644
index 00000000000000..0a3197c9c9cd1d
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu.h
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef _REALTEK_PSE_MCU_H
+#define _REALTEK_PSE_MCU_H
+
+#include <linux/mutex.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/types.h>
+
+/*
+ * Time the MCU itself needs between accepting a request and having a
+ * response ready. These are properties of the MCU firmware, not of the
+ * underlying transport: the core paces transactions by RTPSE_MCU_RESPONSE_MS
+ * and both transports size their per-transaction recv ceiling from
+ * RTPSE_MCU_RESPONSE_MAX_MS, since some commands are documented as
+ * needing up to ~1s to produce a reply.
+ */
+#define RTPSE_MCU_RESPONSE_MS			25
+#define RTPSE_MCU_RESPONSE_MAX_MS		1000
+
+/*
+ * Total time to keep retrying the first MCU read at probe, and the pause
+ * between attempts. Right after enable-gpios is asserted the MCU may not
+ * answer on the bus yet; give it a bounded window to come up before
+ * declaring the probe failed.
+ */
+#define RTPSE_MCU_BOOT_TIMEOUT_MS		3000
+#define RTPSE_MCU_BOOT_RETRY_MS			100
+
+#define RTPSE_MCU_MSG_SIZE			12
+
+struct rtpse_mcu_msg {
+	u8 opcode;
+	u8 seq_num;
+	u8 payload[9];
+	u8 checksum;
+} __packed;
+
+/*
+ * MCU status opcodes (seen on the Gen1 dialect; Gen2 never emits them).
+ * INCOMPLETE/BAD_CSUM are terminal; NOT_READY is transient.
+ */
+#define RTPSE_MCU_OPCODE_INCOMPLETE		0xfd	/* -EBADE   */
+#define RTPSE_MCU_OPCODE_BAD_CSUM		0xfe	/* -EBADMSG */
+#define RTPSE_MCU_OPCODE_NOT_READY		0xff	/* -EAGAIN  */
+
+/* A polling transport can stop here: the matching reply, or a terminal error. */
+static inline bool rtpse_mcu_resp_is_final(const struct rtpse_mcu_msg *req,
+					   const struct rtpse_mcu_msg *resp)
+{
+	return resp->opcode == req->opcode ||
+	       resp->opcode == RTPSE_MCU_OPCODE_INCOMPLETE ||
+	       resp->opcode == RTPSE_MCU_OPCODE_BAD_CSUM;
+}
+
+/* Opaque to transports; defined in realtek-pse-core.c. */
+struct rtpse_mcu_dialect;
+struct rtpse_mcu_chip_info;
+struct rtpse_mcu_ctrl;
+
+/* Per-compatible match data (the of_match .data). */
+struct rtpse_mcu_match_data {
+	const struct rtpse_mcu_dialect *dialect;
+	bool native_i2c;	/* raw-I2C framing (vs SMBus); I2C transport only */
+};
+
+struct rtpse_mcu_transport_ops {
+	int (*send)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req);
+	int (*recv)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+		    struct rtpse_mcu_msg *resp);
+};
+
+struct rtpse_mcu_ctrl {
+	struct device *dev;
+	struct pse_controller_dev pcdev;
+	struct mutex mutex; /* serializes MCU request/response transactions */
+	const struct rtpse_mcu_dialect *dialect;
+	const struct rtpse_mcu_chip_info *chip;
+	const struct rtpse_mcu_transport_ops *transport;
+	u8 seq;			/* rolling request seq_num, echoed by the MCU */
+};
+
+int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse);
+
+extern const struct rtpse_mcu_match_data rtpse_mcu_gen1_data;
+extern const struct rtpse_mcu_match_data rtpse_mcu_gen2_data;
+extern const struct rtpse_mcu_match_data rtpse_mcu_gen2_i2c_data;
+
+#endif

3/2 2026/07/27 01:15 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24014
							output: 384
							thoughts: 3114
						

Thoughts:
**Fuzzing the Realtek PSE MCU Driver**

Okay, so we've got this new patch introducing a driver for the Realtek PSE MCU, handling both I2C and UART transports. My immediate thought is, can we reach it in QEMU? We're dealing with I2C and UART, which can be emulated or instantiated in a few ways. I2C, for instance, has `i2c-stub`, `i2c-dev`, or device tree nodes for userspace interaction, or syzkaller itself can inject traffic or create virtual devices. Syzkaller, right... it might use `/dev/i2c-X`, but this is a *specific* kernel driver. So, can we probe this driver?

The answer is yes. Device tree nodes or I2C sysfs `new_device` should do the trick. UART, being serdev, also relies on device tree, though `ldattach` is tempting, it is not the usual case. Syzkaller can inject device tree nodes or overlays, and it has built-in ways to test serdev and I2C drivers if instantiated. So, can we fuzz this thing? Absolutely.

The driver implements `pse_controller_ops`, so if it's probed, we're talking ethtool netlink commands to reach those functions. Crucially, I2C devices can be instantiated by writing to `/sys/bus/i2c/devices/i2c-X/new_device`. *That's* the key. I see an `of_match_table`, not an `id_table`. Does `new_device` work with an `of_match_table`? Normally it wants an `id_table` or `driver.name`. Wait... I2C core falls back to `of_match_table` if the string matches the compatible string *without* the vendor prefix, or something like that. Syzkaller can load device tree overlays anyway.

Now, which functions are critical for our fuzzing efforts? We need to pinpoint the entry points. The driver exposes these functions: `rtpse_mcu_port_get_admin_state`, `rtpse_mcu_port_get_pw_status`, `rtpse_mcu_port_get_pw_class`, and so on. All these are reachable via ethtool netlink if the device is probed. And of course, the I2C and UART receive paths: `rtpse_mcu_uart_receive`, `rtpse_mcu_i2c_smbus_recv`, `rtpse_mcu_i2c_native_recv`. Importantly, `rtpse_mcu_uart_receive` is called when data comes in over UART. Instantiate the serdev device and we can feed it data, e.g., via a pty. Excellent. Time to fuzz.

This is a new driver, it parses data from the device, and exposes operations to userspace – prime fuzzing material.

To enable this, we need to enable `CONFIG_PSE_CONTROLLER`, `CONFIG_PSE_REALTEK_MCU`, `CONFIG_PSE_REALTEK_MCU_I2C`, and `CONFIG_PSE_REALTEK_MCU_UART`. Let's double-check the Kconfig. Yep, that looks right; `PSE_CONTROLLER` and `SERIAL_DEV_BUS` need to be enabled and `I2C`.

Okay, let's list the functions we need to focus on for fuzzing:

*   `rtpse_mcu_port_get_admin_state`
*   `rtpse_mcu_port_get_pw_status`
*   `rtpse_mcu_port_get_pw_class`
*   `rtpse_mcu_port_get_actual_pw`
*   `rtpse_mcu_port_enable`
*   `rtpse_mcu_port_disable`
*   `rtpse_mcu_port_get_voltage`
*   `rtpse_mcu_port_get_pw_limit`
*   `rtpse_mcu_port_set_pw_limit`
*   `rtpse_mcu_port_get_pw_limit_ranges`
*   `rtpse_mcu_port_get_prio`
*   `rtpse_mcu_port_set_prio`
*   `rtpse_mcu_uart_receive`
*   `rtpse_mcu_i2c_smbus_recv`
*   `rtpse_mcu_i2c_native_recv`
*   `rtpse_mcu_discover


4/2 2026/07/27 01:15 tool set-results
0m Args:
{
  "EnableConfigs": [
    "PSE_CONTROLLER",
    "PSE_REALTEK_MCU_I2C",
    "PSE_REALTEK_MCU_UART",
    "SERIAL_DEV_BUS",
    "I2C"
  ],
  "FocusSymbols": [
    "rtpse_mcu_register",
    "rtpse_mcu_uart_probe",
    "rtpse_mcu_i2c_probe",
    "rtpse_mcu_port_get_admin_state",
    "rtpse_mcu_port_get_pw_status",
    "rtpse_mcu_port_get_pw_class",
    "rtpse_mcu_port_get_actual_pw",
    "rtpse_mcu_port_get_voltage",
    "rtpse_mcu_port_enable",
    "rtpse_mcu_port_disable",
    "rtpse_mcu_port_get_pw_limit",
    "rtpse_mcu_port_set_pw_limit",
    "rtpse_mcu_port_get_pw_limit_ranges",
    "rtpse_mcu_port_get_prio",
    "rtpse_mcu_port_set_prio",
    "rtpse_mcu_uart_receive"
  ],
  "Reasoning": "This patch introduces a new driver for the Realtek PSE MCU, supporting both I2C and UART transports. The driver exposes PSE controller operations to userspace via ethtool netlink and parses data received from the device over I2C/UART. It can be probed in virtual environments via device tree overlays or I2C sysfs, making it reachable and suitable for fuzzing.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "PSE_CONTROLLER",
    "PSE_REALTEK_MCU_I2C",
    "PSE_REALTEK_MCU_UART",
    "SERIAL_DEV_BUS",
    "I2C"
  ],
  "FocusSymbols": [
    "rtpse_mcu_register",
    "rtpse_mcu_uart_probe",
    "rtpse_mcu_i2c_probe",
    "rtpse_mcu_port_get_admin_state",
    "rtpse_mcu_port_get_pw_status",
    "rtpse_mcu_port_get_pw_class",
    "rtpse_mcu_port_get_actual_pw",
    "rtpse_mcu_port_get_voltage",
    "rtpse_mcu_port_enable",
    "rtpse_mcu_port_disable",
    "rtpse_mcu_port_get_pw_limit",
    "rtpse_mcu_port_set_pw_limit",
    "rtpse_mcu_port_get_pw_limit_ranges",
    "rtpse_mcu_port_get_prio",
    "rtpse_mcu_port_set_prio",
    "rtpse_mcu_uart_receive"
  ],
  "Reasoning": "This patch introduces a new driver for the Realtek PSE MCU, supporting both I2C and UART transports. The driver exposes PSE controller operations to userspace via ethtool netlink and parses data received from the device over I2C/UART. It can be probed in virtual environments via device tree overlays or I2C sysfs, making it reachable and suitable for fuzzing.",
  "WorthFuzzing": true
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)