| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/08/02 18:28 | flow | patch-triage |
2mArgs:null Results: {
"EnableConfigs": null,
"FocusSymbols": [],
"Reasoning": "The patch introduces a new driver for Realtek PSE MCU, which supports I2C and UART (serdev) transports. The driver relies on device tree (OF) match data to probe successfully. In a virtualized environment like QEMU used by syzkaller, there is no device tree node for this hardware. Furthermore, the I2C driver cannot be instantiated via the sysfs `new_device` interface because it strictly requires `device_get_match_data()`, which returns NULL for sysfs-instantiated devices (as they lack an `of_node`), causing the probe to fail immediately with `-ENODEV`. Serdev devices also cannot be instantiated from userspace. Therefore, the driver's code is completely unreachable and cannot be fuzzed.",
"WorthFuzzing": false
} |
| 1/1 | 2026/08/02 18:28 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 9fa75536b4b8896d3bc0d954f375a6c1023d8196\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Sun Aug 2 18:28:44 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 0000000000000..3bb32349c28c4\n--- /dev/null\n+++ b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml\n@@ -0,0 +1,182 @@\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+ # 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+ allOf:\n+ - $ref: /schemas/serial/serial-peripheral-props.yaml#\n+\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 932ea1db048e5..fc7325dd51aa6 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -22767,6 +22767,13 @@ S:\tMaintained\n F:\tDocumentation/devicetree/bindings/watchdog/realtek,otto-wdt.yaml\n F:\tdrivers/watchdog/realtek_otto_wdt.c\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 RTL83xx SMI DSA ROUTER CHIPS\n M:\tLinus Walleij \u003clinusw@kernel.org\u003e\n M:\tLuiz Angelo Daros de Luca \u003cluizluca@gmail.com\u003e\ndiff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig\nindex 7ef29657ee5dd..a0f2ae668c67c 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 cc78f7ea7f5f7..9cca5900fe342 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 0000000000000..ed3b743470dbf\n--- /dev/null\n+++ b/drivers/net/pse-pd/realtek-pse-mcu-core.c\n@@ -0,0 +1,988 @@\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+/* Nominal PSE rail; 802.3at/bt operating range. */\n+#define RTPSE_MCU_PSE_VOLTAGE_UV\t\t54000000\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 mcu_type;\n+};\n+\n+struct rtpse_mcu_ext_config {\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+\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.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.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.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.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/*\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 base time between accepting a request and\n+\t\t * having a reply ready, independent of transport. Pace that here; the\n+\t\t * transport then finishes recv in its own model - a bounded poll of\n+\t\t * the response register (I2C) or one wait on a pushed frame (UART) -\n+\t\t * so the retry belongs in the transport, not here.\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-\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 * As per datasheet, the classification result is only valid when in\n+\t * one of those operational modes, otherwise not.\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\t/*\n+\t\t * No class to report, return 0 instead. This is indistinguishable\n+\t\t * from a real class-0 PD but userspace disambiguates via the\n+\t\t * power status.\n+\t\t */\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+\n+\t/*\n+\t * Idle ports measure 0V, which the core rejects when turning a power\n+\t * limit into a current limit. Fall back to the nominal rail so a limit\n+\t * can be set before a PD is attached.\n+\t */\n+\tif (!uV)\n+\t\treturn RTPSE_MCU_PSE_VOLTAGE_UV;\n+\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+\t/*\n+\t * Round up so a sub-LSB request maps to one LSB, not silently to 0;\n+\t * an explicit 0 still yields 0, and LSB-aligned maxima can't overshoot.\n+\t */\n+\tprg_val = min_t(unsigned int, DIV_ROUND_UP(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+\tret = rtpse_mcu_discover(pse, \u0026info);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\t/*\n+\t * Some boards gate all ports through a hardware line; deassert it only\n+\t * once the MCU is confirmed present, so a failed probe leaves the ports\n+\t * gated. From here the MCU protocol governs per-port admin state.\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+\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-\u003emcu_type = payload[6];\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-\u003emcu_type = payload[6];\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 0000000000000..e10f8c633b68e\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 0000000000000..9baa17d8d31fa\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 0000000000000..52f9442d7117c\n--- /dev/null\n+++ b/drivers/net/pse-pd/realtek-pse-mcu.h\n@@ -0,0 +1,93 @@\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 reset-gpios is deasserted 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+/*\n+ * A polling transport can stop here: the reply to this request (opcode and\n+ * seq_num), or a terminal error. The seq_num rejects a stale reply left by an\n+ * abandoned transaction, instead of desyncing by one frame.\n+ */\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 \u0026\u0026 resp-\u003eseq_num == req-\u003eseq_num) ||\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-mcu-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;\t/* 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/08/02 18:28 | agent | patch-evaluator |
2mModel:best-expensive Args: null Results: {
"EnableConfigs": null,
"FocusSymbols": [],
"Reasoning": "The patch introduces a new driver for Realtek PSE MCU, which supports I2C and UART (serdev) transports. The driver relies on device tree (OF) match data to probe successfully. In a virtualized environment like QEMU used by syzkaller, there is no device tree node for this hardware. Furthermore, the I2C driver cannot be instantiated via the sysfs `new_device` interface because it strictly requires `device_get_match_data()`, which returns NULL for sysfs-instantiated devices (as they lack an `of_node`), causing the probe to fail immediately with `-ENODEV`. Serdev devices also cannot be instantiated from userspace. Therefore, the driver's code is completely unreachable and cannot be fuzzed.",
"WorthFuzzing": false
}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 9fa75536b4b8896d3bc0d954f375a6c1023d8196
Author: syz-cluster <triage@syzkaller.com>
Date: Sun Aug 2 18:28:44 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 0000000000000..3bb32349c28c4
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
@@ -0,0 +1,182 @@
+# 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#
+ # 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:
+ allOf:
+ - $ref: /schemas/serial/serial-peripheral-props.yaml#
+
+ 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 932ea1db048e5..fc7325dd51aa6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22767,6 +22767,13 @@ S: Maintained
F: Documentation/devicetree/bindings/watchdog/realtek,otto-wdt.yaml
F: drivers/watchdog/realtek_otto_wdt.c
+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 RTL83xx SMI DSA ROUTER CHIPS
M: Linus Walleij <linusw@kernel.org>
M: Luiz Angelo Daros de Luca <luizluca@gmail.com>
diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index 7ef29657ee5dd..a0f2ae668c67c 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 cc78f7ea7f5f7..9cca5900fe342 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 0000000000000..ed3b743470dbf
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-core.c
@@ -0,0 +1,988 @@
+// 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
+
+/* Nominal PSE rail; 802.3at/bt operating range. */
+#define RTPSE_MCU_PSE_VOLTAGE_UV 54000000
+
+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 mcu_type;
+};
+
+struct rtpse_mcu_ext_config {
+ 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;
+ 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 = {
+ .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 = {
+ .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 = {
+ .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 = {
+ .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 = {
+ /*
+ * 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 base time between accepting a request and
+ * having a reply ready, independent of transport. Pace that here; the
+ * transport then finishes recv in its own model - a bounded poll of
+ * the response register (I2C) or one wait on a pushed frame (UART) -
+ * so the retry belongs in the transport, not here.
+ */
+ 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->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;
+
+ /*
+ * As per datasheet, the classification result is only valid when in
+ * one of those operational modes, otherwise not.
+ */
+ 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:
+ /*
+ * No class to report, return 0 instead. This is indistinguishable
+ * from a real class-0 PD but userspace disambiguates via the
+ * power status.
+ */
+ 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;
+
+ /*
+ * Idle ports measure 0V, which the core rejects when turning a power
+ * limit into a current limit. Fall back to the nominal rail so a limit
+ * can be set before a PD is attached.
+ */
+ if (!uV)
+ return RTPSE_MCU_PSE_VOLTAGE_UV;
+
+ 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;
+
+ /*
+ * Round up so a sub-LSB request maps to one LSB, not silently to 0;
+ * an explicit 0 still yields 0, and LSB-aligned maxima can't overshoot.
+ */
+ prg_val = min_t(unsigned int, DIV_ROUND_UP(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");
+
+ ret = rtpse_mcu_discover(pse, &info);
+ if (ret)
+ return ret;
+
+ /*
+ * Some boards gate all ports through a hardware line; deassert it only
+ * once the MCU is confirmed present, so a failed probe leaves the ports
+ * gated. From here the MCU protocol governs per-port admin state.
+ */
+ 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");
+
+ 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->mcu_type = payload[6];
+}
+
+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->mcu_type = payload[6];
+}
+
+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 0000000000000..e10f8c633b68e
--- /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 0000000000000..9baa17d8d31fa
--- /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 0000000000000..52f9442d7117c
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu.h
@@ -0,0 +1,93 @@
+/* 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 reset-gpios is deasserted 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 reply to this request (opcode and
+ * seq_num), or a terminal error. The seq_num rejects a stale reply left by an
+ * abandoned transaction, instead of desyncing by one frame.
+ */
+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->seq_num == req->seq_num) ||
+ resp->opcode == RTPSE_MCU_OPCODE_INCOMPLETE ||
+ resp->opcode == RTPSE_MCU_OPCODE_BAD_CSUM;
+}
+
+/* Opaque to transports; defined in realtek-pse-mcu-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/08/02 18:28 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 24023 output: 33 thoughts: 1371 Thoughts: **Fuzzing the New Realtek PSE MCU Driver** Okay, so we have a new driver for a Realtek PSE MCU, supporting I2C and UART. It's located in the `drivers/net/pse-pd` directory. My immediate thought is, can we fuzz this with syzkaller? It's an I2C and UART driver, which is interesting. Syzkaller doesn't automatically fuzz *all* I2C devices; we need to either explicitly describe them or find a generic fuzzing path. I'm recalling from previous experience that syzkaller uses `/dev/i2c-X` nodes for I2C, but doesn't have pseudo-syscalls like `syz_i2c`. The question becomes, can we get these devices to instantiate in a way syzkaller can reach? Since I2C client drivers are bound to I2C devices, if we can create the I2C device via `/sys/bus/i2c/devices/...` or the device tree, then maybe we're good to go. Syzkaller does have some UART support via `serdev`. Is this driver worth fuzzing? Yes, definitely. It's new, and it's parsing data from I2C/UART – that's a prime target for fuzzing. Syzkaller's ability to emulate these devices, or inject data, is key. Does syzkaller *emulate* I2C devices? Like `vhci` for USB or `mac80211_hwsim` for wifi, is there an `i2c-stub` equivalent? No, syzkaller doesn't seem to have device injection for I2C using BPF, that I can remember. The prompt mentions a restriction about code paths unreachable in virtualized environments like QEMU. Can we reach this driver in QEMU? It *is* an I2C/UART driver. UART utilizes `serdev`, and are usually instantiated via device tree or ACPI. Can we instantiate an I2C device from userspace via `/sys/bus/i2c/devices/i2c-X/new_device`? That's the key. Let's see if this `new_device` path will work. The driver has OF compatible strings: `realtek,pse-mcu-gen1-smbus`, `realtek,pse-mcu-gen2-smbus`, `realtek,pse-mcu-gen2-i2c`. Now, `new_device` takes an I2C device name, which should match the `i2c_device_id` table, right? Does this driver *have* an `i2c_device_id` table? Let's check `realtek-pse-mcu-i2c.c`. Okay, it has an `of_match_table` but no `i2c_device_id` table. Can we instantiate it via `new_device` with only an `of_match_table`? I know that since around Linux 4.10, the I2C core *can* match `of_match_table` via `new_device` if you pass the compatible string without the vendor prefix, or maybe it needs a `id_table`? `new_device` should match against `id_table` or `driver.name`. If there is no `id_table`, `i2c_match_id` will fail. `i2c_device_match` *also* checks `of_driver_match_device`. However, the `new_device` creates an `i2c_client` with the name provided, the name is matched against `id_table`. Without `id_table` it might fall back to `driver.name`? No, `i2c_device_match` checks `id_table` using `i2c_match_id(driver->id_table, client)`. I need to check `drivers/i2c/i2c-core-base.c` to be sure how this matching works! Then I'll be able to tell if syzkaller can trigger this new driver. |
| 4/2 | 2026/08/02 18:29 | tool | grepper |
0mArgs:{
"Expression": "i2c_device_match",
"PathPrefix": "drivers/i2c/"
}Results: {
"Output": "drivers/i2c/i2c-core-base.c=138=EXPORT_SYMBOL(i2c_get_match_data);\ndrivers/i2c/i2c-core-base.c-139-\ndrivers/i2c/i2c-core-base.c:140:static int i2c_device_match(struct device *dev, const struct device_driver *drv)\ndrivers/i2c/i2c-core-base.c-141-{\n--\ndrivers/i2c/i2c-core-base.c=699=const struct bus_type i2c_bus_type = {\ndrivers/i2c/i2c-core-base.c-700-\t.name\t\t= \"i2c\",\ndrivers/i2c/i2c-core-base.c:701:\t.match\t\t= i2c_device_match,\ndrivers/i2c/i2c-core-base.c-702-\t.probe\t\t= i2c_device_probe,\n"
} |
| 5/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 26604 output: 43 thoughts: 31 Thoughts: Here's my take on the `i2c_device_match` function, seen through the lens of a seasoned embedded systems developer: **Delving into I2C Device Matching** Alright, let's pull up `drivers/i2c/i2c-core-base.c` and zero in on `i2c_device_match`. I've seen this routine countless times in debugging and driver development, and it's absolutely critical for proper I2C device enumeration and loading the correct driver. My immediate focus is understanding the logic for how the I2C core determines if a driver is suitable for a particular I2C device. I expect to see the standard pattern: iterating through a list of supported device IDs, potentially using more than one method. I'm looking for the interplay of the driver's ID table (probably something like `i2c_device_id` or equivalent structures), and the device's descriptors. I'm keen on how it handles wildcarding, if any, and if there are provisions for vendor/product matching (or revision codes). Also I'll check how it handles any edge cases regarding driver probing. I want to see if the matching process handles platform-specific devices correctly. Specifically, I'll be interested in how the function manages conflicts or ambiguities, if any. Does it prioritize specific matches over others? What happens if multiple drivers claim the same I2C device? Are there mechanisms for overriding or forcing a driver assignment? Ultimately, my goal is to refresh my mental model of this fundamental I2C component. I want to be certain that I can quickly diagnose problems related to device matching or driver loading in future projects. And I know it's probably more complex than a simple "ID equals ID" comparison, but that's what I want to see how. |
| 6/2 | 2026/08/02 18:29 | tool | read-file |
0mArgs:{
"File": "drivers/i2c/i2c-core-base.c",
"FirstLine": 135,
"LineCount": 30
}Results: {
"Contents": " 135:\t\n 136:\t\treturn data;\n 137:\t}\n 138:\tEXPORT_SYMBOL(i2c_get_match_data);\n 139:\t\n 140:\tstatic int i2c_device_match(struct device *dev, const struct device_driver *drv)\n 141:\t{\n 142:\t\tstruct i2c_client\t*client = i2c_verify_client(dev);\n 143:\t\tconst struct i2c_driver\t*driver;\n 144:\t\n 145:\t\n 146:\t\t/* Attempt an OF style match */\n 147:\t\tif (i2c_of_match_device(drv-\u003eof_match_table, client))\n 148:\t\t\treturn 1;\n 149:\t\n 150:\t\t/* Then ACPI style match */\n 151:\t\tif (acpi_driver_match_device(dev, drv))\n 152:\t\t\treturn 1;\n 153:\t\n 154:\t\tdriver = to_i2c_driver(drv);\n 155:\t\n 156:\t\t/* Finally an I2C match */\n 157:\t\tif (i2c_match_id(driver-\u003eid_table, client))\n 158:\t\t\treturn 1;\n 159:\t\n 160:\t\treturn 0;\n 161:\t}\n 162:\t\n 163:\tstatic int i2c_device_uevent(const struct device *dev, struct kobj_uevent_env *env)\n 164:\t{\n"
} |
| 7/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 27496 output: 35 thoughts: 96 Thoughts: Okay, let me think this through. **Investigating i2c_of_match_device and Device Initialization** Right, so `i2c_of_match_device` is the first function called in the device matching process, that makes sense. But I'm curious about how the `of_node` and the compatible string are actually being set for the new I2C device. `new_device`... ah, yes, I remember now, `new_device` just assigns the provided string to `client->name`. So the kernel isn't directly setting `of_node` or compatible, interesting. If `client->name` *is* set, I wonder if that impacts `i2c_of_match_device`? That would be useful information. I'd better double-check the implementation of `i2c_of_match_device` to confirm. Let's dig into that code and see how it handles things when a name is already assigned. That'll probably tell me everything I need to know about the device-matching process when a device is instantiated this way. |
| 8/2 | 2026/08/02 18:29 | tool | grepper |
0mArgs:{
"Expression": "i2c_of_match_device",
"PathPrefix": "drivers/i2c/"
}Results: {
"Output": "drivers/i2c/i2c-core-base.c=140=static int i2c_device_match(struct device *dev, const struct device_driver *drv)\n--\ndrivers/i2c/i2c-core-base.c-146-\t/* Attempt an OF style match */\ndrivers/i2c/i2c-core-base.c:147:\tif (i2c_of_match_device(drv-\u003eof_match_table, client))\ndrivers/i2c/i2c-core-base.c-148-\t\treturn 1;\n--\ndrivers/i2c/i2c-core-base.c=491=static int i2c_device_probe(struct device *dev)\n--\ndrivers/i2c/i2c-core-base.c-541-\t !acpi_driver_match_device(dev, dev-\u003edriver) \u0026\u0026\ndrivers/i2c/i2c-core-base.c:542:\t !i2c_of_match_device(dev-\u003edriver-\u003eof_match_table, client)) {\ndrivers/i2c/i2c-core-base.c-543-\t\tstatus = -ENODEV;\n--\ndrivers/i2c/i2c-core-of.c=115=static const struct of_device_id*\ndrivers/i2c/i2c-core-of.c:116:i2c_of_match_device_sysfs(const struct of_device_id *matches,\ndrivers/i2c/i2c-core-of.c-117-\t\t\t\t struct i2c_client *client)\n--\ndrivers/i2c/i2c-core-of.c=144=const struct of_device_id\ndrivers/i2c/i2c-core-of.c:145:*i2c_of_match_device(const struct of_device_id *matches,\ndrivers/i2c/i2c-core-of.c-146-\t\t struct i2c_client *client)\n--\ndrivers/i2c/i2c-core-of.c-156-\ndrivers/i2c/i2c-core-of.c:157:\treturn i2c_of_match_device_sysfs(matches, client);\ndrivers/i2c/i2c-core-of.c-158-}\n--\ndrivers/i2c/i2c-core.h=87=void of_i2c_register_devices(struct i2c_adapter *adap);\ndrivers/i2c/i2c-core.h:88:const struct of_device_id *i2c_of_match_device(const struct of_device_id *matches,\ndrivers/i2c/i2c-core.h-89-\t\t\t\t\t struct i2c_client *client);\n--\ndrivers/i2c/i2c-core.h=93=static inline\ndrivers/i2c/i2c-core.h:94:const struct of_device_id *i2c_of_match_device(const struct of_device_id *matches,\ndrivers/i2c/i2c-core.h-95-\t\t\t\t\t struct i2c_client *client)\n"
} |
| 9/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 28684 output: 43 thoughts: 36 Thoughts: Here's my attempt at summarizing the thought process, tailored for an expert audience: **Diving into i2c_of_match_device_sysfs** Okay, time to dig into the `i2c_of_match_device_sysfs` function. It looks like the relevant code will be in `drivers/i2c/i2c-core-of.c`. I'm specifically interested in how this function interacts with the device tree and the sysfs interface for I2C devices. I need to understand its role in matching devices based on the DT node and then potentially creating entries under `/sys/bus/i2c/devices/` for them. I'm assuming it handles any configuration options that are exposed via sysfs as well. Let me check the code and see exactly what's going on... |
| 10/2 | 2026/08/02 18:29 | tool | read-file |
0mArgs:{
"File": "drivers/i2c/i2c-core-of.c",
"FirstLine": 115,
"LineCount": 30
}Results: {
"Contents": " 115:\tstatic const struct of_device_id*\n 116:\ti2c_of_match_device_sysfs(const struct of_device_id *matches,\n 117:\t\t\t\t\t struct i2c_client *client)\n 118:\t{\n 119:\t\tconst char *name;\n 120:\t\n 121:\t\tfor (; matches-\u003ecompatible[0]; matches++) {\n 122:\t\t\t/*\n 123:\t\t\t * Adding devices through the i2c sysfs interface provides us\n 124:\t\t\t * a string to match which may be compatible with the device\n 125:\t\t\t * tree compatible strings, however with no actual of_node the\n 126:\t\t\t * of_match_device() will not match\n 127:\t\t\t */\n 128:\t\t\tif (sysfs_streq(client-\u003ename, matches-\u003ecompatible))\n 129:\t\t\t\treturn matches;\n 130:\t\n 131:\t\t\tname = strchr(matches-\u003ecompatible, ',');\n 132:\t\t\tif (!name)\n 133:\t\t\t\tname = matches-\u003ecompatible;\n 134:\t\t\telse\n 135:\t\t\t\tname++;\n 136:\t\n 137:\t\t\tif (sysfs_streq(client-\u003ename, name))\n 138:\t\t\t\treturn matches;\n 139:\t\t}\n 140:\t\n 141:\t\treturn NULL;\n 142:\t}\n 143:\t\n 144:\tconst struct of_device_id\n"
} |
| 11/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 29344 output: 31 thoughts: 211 Thoughts: **Investigating sysfs-based I2C device matching** Okay, so `i2c_of_match_device_sysfs` looks like the key here. It seems like it's designed to compare the `client->name` against either the full `matches->compatible` string, or just the part after the comma, which is fantastic! That means if I write something like `pse-mcu-gen1-smbus` or, even better, the full `realtek,pse-mcu-gen1-smbus` string, to `/sys/bus/i2c/devices/i2c-X/new_device`, it should trigger a match and, ultimately, probe the driver. That's a clever way to dynamically create devices. But wait, a potential snag. What about `device_get_match_data`? That's what `rtpse_mcu_i2c_probe` is using: ```c match = device_get_match_data(dev); if (!match) return dev_err_probe(dev, -ENODEV, "missing match data\n"); ``` Does `device_get_match_data` function correctly when the device matching is achieved through sysfs? I need to check the implementation of `device_get_match_data` to be certain. Is this a potential pitfall, or will the framework handle this transparently? I must verify this to ensure the driver's correct operation after the sysfs-based device creation. |
| 12/2 | 2026/08/02 18:29 | tool | grepper |
0mArgs:{
"Expression": "device_get_match_data",
"PathPrefix": "drivers/base/"
}Results: {
"Output": "drivers/base/property.c=1347=EXPORT_SYMBOL(fwnode_graph_parse_endpoint);\ndrivers/base/property.c-1348-\ndrivers/base/property.c:1349:const void *device_get_match_data(const struct device *dev)\ndrivers/base/property.c-1350-{\ndrivers/base/property.c:1351:\treturn fwnode_call_ptr_op(dev_fwnode(dev), device_get_match_data, dev);\ndrivers/base/property.c-1352-}\ndrivers/base/property.c:1353:EXPORT_SYMBOL_GPL(device_get_match_data);\ndrivers/base/property.c-1354-\n"
} |
| 13/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 30119 output: 43 thoughts: 111 Thoughts: **Analyzing Device Matching in I2C Driver** Okay, so I'm tracing this `device_get_match_data` function call, and it's calling `fwnode_call_ptr_op` with the device's `fwnode`. Hmm, interesting. But if this device was created through sysfs, it won't have an `fwnode` attached to it, right? Which means `device_get_match_data` is going to return `NULL` in that scenario? That feels like something I need to verify. Let me go check out `i2c_get_match_data` in `drivers/i2c/i2c-core-base.c` and confirm my hunch. Need to see how that handles the case where there's no firmware node. |
| 14/2 | 2026/08/02 18:29 | tool | read-file |
0mArgs:{
"File": "drivers/i2c/i2c-core-base.c",
"FirstLine": 115,
"LineCount": 25
}Results: {
"Contents": " 115:\t\t\tid++;\n 116:\t\t}\n 117:\t\treturn NULL;\n 118:\t}\n 119:\tEXPORT_SYMBOL_GPL(i2c_match_id);\n 120:\t\n 121:\tconst void *i2c_get_match_data(const struct i2c_client *client)\n 122:\t{\n 123:\t\tstruct i2c_driver *driver = to_i2c_driver(client-\u003edev.driver);\n 124:\t\tconst struct i2c_device_id *match;\n 125:\t\tconst void *data;\n 126:\t\n 127:\t\tdata = device_get_match_data(\u0026client-\u003edev);\n 128:\t\tif (!data) {\n 129:\t\t\tmatch = i2c_match_id(driver-\u003eid_table, client);\n 130:\t\t\tif (!match)\n 131:\t\t\t\treturn NULL;\n 132:\t\n 133:\t\t\tdata = (const void *)match-\u003edriver_data;\n 134:\t\t}\n 135:\t\n 136:\t\treturn data;\n 137:\t}\n 138:\tEXPORT_SYMBOL(i2c_get_match_data);\n 139:\t\n"
} |
| 15/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 30797 output: 36 thoughts: 291 Thoughts: **Digging into the I2C Match Data Mystery** Okay, so I'm thinking about how the I2C driver is matching devices. My initial understanding was that `i2c_get_match_data` was the key function, and that it smartly fell back to `i2c_match_id` if the more specific `device_get_match_data` returned NULL. However, looking deeper at the `rtpse_mcu_i2c_probe` function, I'm starting to see a potential discrepancy. The code in `rtpse_mcu_i2c_probe` directly calls `device_get_match_data(dev)`, passing the device itself (`dev`), not the I2C client (`client`), which is where `i2c_get_match_data` would come into play, assuming a direct call to it. So, the `match` variable is populated with the result of calling `device_get_match_data` on the device structure. This is a bit unexpected and throws a wrinkle into my initial assessment of the driver's flow. If `device_get_match_data` returns NULL, the driver bails out. This has me thinking that there's a recent change in how these functions work. Perhaps `device_get_match_data` was updated fairly recently, say in Linux 6.6 or around that time. Is it possible that it *now* implicitly handles falling back to bus-specific matching when there's no direct match data for the device itself? I need to verify if the function implementation changed to support bus-specific fallback. I should investigate the source code of `device_get_match_data` directly, specifically looking in `drivers/base/property.c` or `drivers/base/core.c` to see if my hunch holds up. |
| 16/2 | 2026/08/02 18:29 | tool | read-file |
0mArgs:{
"File": "drivers/base/property.c",
"FirstLine": 1345,
"LineCount": 25
}Results: {
"Contents": "1345:\t\treturn fwnode_call_int_op(fwnode, graph_parse_endpoint, endpoint);\n1346:\t}\n1347:\tEXPORT_SYMBOL(fwnode_graph_parse_endpoint);\n1348:\t\n1349:\tconst void *device_get_match_data(const struct device *dev)\n1350:\t{\n1351:\t\treturn fwnode_call_ptr_op(dev_fwnode(dev), device_get_match_data, dev);\n1352:\t}\n1353:\tEXPORT_SYMBOL_GPL(device_get_match_data);\n1354:\t\n1355:\tstatic unsigned int fwnode_graph_devcon_matches(const struct fwnode_handle *fwnode,\n1356:\t\t\t\t\t\t\tconst char *con_id, void *data,\n1357:\t\t\t\t\t\t\tdevcon_match_fn_t match,\n1358:\t\t\t\t\t\t\tvoid **matches,\n1359:\t\t\t\t\t\t\tunsigned int matches_len)\n1360:\t{\n1361:\t\tstruct fwnode_handle *node;\n1362:\t\tstruct fwnode_handle *ep;\n1363:\t\tunsigned int count = 0;\n1364:\t\tvoid *ret;\n1365:\t\n1366:\t\tfwnode_graph_for_each_endpoint(fwnode, ep) {\n1367:\t\t\tif (matches \u0026\u0026 count \u003e= matches_len) {\n1368:\t\t\t\tfwnode_handle_put(ep);\n1369:\t\t\t\tbreak;\n"
} |
| 17/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 31936 output: 31 thoughts: 164 Thoughts: **Navigating the `device_get_match_data` Rabbit Hole** Okay, so I'm trying to wrap my head around this `device_get_match_data` function. My initial thought was, "Ah, it's moved to `drivers/base/property.c`, and it's using `fwnode_call_ptr_op`." Standard stuff, I figured. But then, a thought struck: what if the device's bus itself has a `get_match_data` function? I need to consider that possibility. So, where would that be defined? My gut tells me I should check `drivers/base/core.c`, the relevant header file in `include/linux/property.h`, or maybe even back in `drivers/base/property.c`. I need to ensure I'm getting the complete picture. Now I'm remembering something about Linux versions 6.6 or 6.7. Didn't they introduce a change to `device_get_match_data`? Did it start checking for the device's driver's `of_match_table` or the bus's `get_match_data` as well? I'm pretty sure I read something about that. Okay, let's cut through the noise. Time to pull up the source code and do a quick grep for `device_get_match_data` within the `drivers/base/` directory. That should give me a clear view of how it's actually being used and how it interacts with the different components. I need to make sure I understand the complete lookup path. |
| 18/2 | 2026/08/02 18:29 | tool | grepper |
0mArgs:{
"Expression": "device_get_match_data",
"PathPrefix": "drivers/base/"
}Results: {
"Output": "drivers/base/property.c=1347=EXPORT_SYMBOL(fwnode_graph_parse_endpoint);\ndrivers/base/property.c-1348-\ndrivers/base/property.c:1349:const void *device_get_match_data(const struct device *dev)\ndrivers/base/property.c-1350-{\ndrivers/base/property.c:1351:\treturn fwnode_call_ptr_op(dev_fwnode(dev), device_get_match_data, dev);\ndrivers/base/property.c-1352-}\ndrivers/base/property.c:1353:EXPORT_SYMBOL_GPL(device_get_match_data);\ndrivers/base/property.c-1354-\n"
} |
| 19/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 32678 output: 31 thoughts: 60 Thoughts: Here's my thought process, summarized as if I were considering the code: **Digging into Match Data Retrieval** Okay, so I'm looking into how device match data is pulled. It seems like `device_get_match_data` in `drivers/base/property.c` *solely* calls `fwnode_call_ptr_op`. Hmm, that's not quite what I expected. I need to understand that call chain better. I'm going to double-check the declaration of `device_get_match_data` in `include/linux/property.h` to see exactly what's going on there and to refresh my memory on the arguments and return values. This should clarify the call stack and how the data is actually retrieved. Let's see what the header has to say. |
| 20/2 | 2026/08/02 18:29 | tool | grepper |
0mArgs:{
"Expression": "device_get_match_data",
"PathPrefix": "include/linux/"
}Results: {
"Output": "include/linux/acpi.h=747=const struct acpi_device_id *acpi_match_device(const struct acpi_device_id *ids,\n--\ninclude/linux/acpi.h-749-\ninclude/linux/acpi.h:750:const void *acpi_device_get_match_data(const struct device *dev);\ninclude/linux/acpi.h-751-extern bool acpi_driver_match_device(struct device *dev,\n--\ninclude/linux/acpi.h=989=static inline const struct acpi_device_id *acpi_match_device(\n--\ninclude/linux/acpi.h-994-\ninclude/linux/acpi.h:995:static inline const void *acpi_device_get_match_data(const struct device *dev)\ninclude/linux/acpi.h-996-{\n--\ninclude/linux/fwnode.h=108=struct fwnode_reference_args {\n--\ninclude/linux/fwnode.h-118- * @device_is_available: Return true if the device is available.\ninclude/linux/fwnode.h:119: * @device_get_match_data: Return the device driver match data.\ninclude/linux/fwnode.h-120- * @property_present: Return true if a property is present.\n--\ninclude/linux/fwnode.h=140=struct fwnode_operations {\n--\ninclude/linux/fwnode.h-143-\tbool (*device_is_available)(const struct fwnode_handle *fwnode);\ninclude/linux/fwnode.h:144:\tconst void *(*device_get_match_data)(const struct fwnode_handle *fwnode,\ninclude/linux/fwnode.h-145-\t\t\t\t\t const struct device *dev);\n--\ninclude/linux/of.h=378=extern const struct of_device_id *of_match_node(\ninclude/linux/of.h-379-\tconst struct of_device_id *matches, const struct device_node *node);\ninclude/linux/of.h:380:extern const void *of_device_get_match_data(const struct device *dev);\ninclude/linux/of.h-381-extern int of_alias_from_compatible(const struct device_node *node, char *alias,\n--\ninclude/linux/of.h=975=static inline phys_addr_t of_dma_get_max_cpu_address(struct device_node *np)\n--\ninclude/linux/of.h-979-\ninclude/linux/of.h:980:static inline const void *of_device_get_match_data(const struct device *dev)\ninclude/linux/of.h-981-{\n--\ninclude/linux/property.h=499=enum dev_dma_attr device_get_dma_attr(const struct device *dev);\ninclude/linux/property.h-500-\ninclude/linux/property.h:501:const void *device_get_match_data(const struct device *dev);\ninclude/linux/property.h-502-\n--\ninclude/linux/surface_aggregator/device.h=250=const struct ssam_device_id *ssam_device_get_match(const struct ssam_device *dev);\ninclude/linux/surface_aggregator/device.h-251-\ninclude/linux/surface_aggregator/device.h:252:const void *ssam_device_get_match_data(const struct ssam_device *dev);\ninclude/linux/surface_aggregator/device.h-253-\n"
} |
| 21/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 33874 output: 35 thoughts: 587 Thoughts: **Debugging I2C Device Matching in the Kernel** Okay, so I'm tracing through this I2C device instantiation code, and something's not quite right. It looks like `device_get_match_data` is the function of interest and I am trying to figure out how it works in this context. It's calling `fwnode_call_ptr_op`, which, if `dev_fwnode(dev)` is NULL, returns NULL. I need to be sure the device in question has the appropriate `fwnode`. Let's look specifically at `i2c_get_match_data`. Hang on, looking at the code in `drivers/i2c/i2c-core-base.c`, it's actually calling `device_get_match_data` directly! It's checking if the data is valid, and then also checking `i2c_match_id` based on the driver's ID table. Alright, back to the problem at hand. `rtpse_mcu_i2c_probe` is directly calling `device_get_match_data`, and if that returns NULL, it's returning `-ENODEV`. That's not what I'd expect. If I create the device through sysfs with `new_device`, `dev_fwnode(dev)` is definitely NULL, so `device_get_match_data` fails, and the probe errors out. This is expected. I have to wonder, is there another way to instantiate an I2C device with a fwnode? Yes, from device tree. But syzkaller doesn't change the device tree dynamically, so that's not helping me here. Now, wait a minute, can syzkaller use `i2c_get_match_data` in some way? No, the I2C driver *directly* calls `device_get_match_data`! So, if the driver uses `device_get_match_data` directly and not `i2c_get_match_data`, then that isn't helping. I thought `i2c_get_match_data` was created *specifically* for this scenario, but the driver author jumped to the low level. Wait... something just clicked! Maybe, just maybe, since Linux 6.6, `i2c_get_match_data` was removed, or updated? Let me quickly pull up `drivers/i2c/i2c-core-base.c` in the current kernel source. That should give me the final answer. |
| 22/2 | 2026/08/02 18:29 | tool | grepper |
0mArgs:{
"Expression": "i2c_get_match_data",
"PathPrefix": "drivers/i2c/"
}Results: {
"Output": "drivers/i2c/i2c-core-base.c=119=EXPORT_SYMBOL_GPL(i2c_match_id);\ndrivers/i2c/i2c-core-base.c-120-\ndrivers/i2c/i2c-core-base.c:121:const void *i2c_get_match_data(const struct i2c_client *client)\ndrivers/i2c/i2c-core-base.c-122-{\n--\ndrivers/i2c/i2c-core-base.c-137-}\ndrivers/i2c/i2c-core-base.c:138:EXPORT_SYMBOL(i2c_get_match_data);\ndrivers/i2c/i2c-core-base.c-139-\n"
} |
| 23/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 35266 output: 36 thoughts: 80 Thoughts: **Investigating Persistent I2C Match Data** Okay, hold on a second. I could have sworn I saw a discussion about this recently. Why is `i2c_get_match_data` still hanging around? And I'm pretty sure `device_get_match_data` in `drivers/base/property.c` only concerns itself with `fwnode`, which seemed like a significant change. Something's not quite lining up with my mental model here. Did I miss something? Right, the changes around device matching and property handling in Linux 6.6 – that's the key. I need to refresh my memory on the specifics of those modifications. I'll need to double-check `drivers/base/property.c` and see exactly what adjustments were made in that release. This feels like an important detail to revisit. |
| 24/2 | 2026/08/02 18:29 | tool | read-file |
0mArgs:{
"File": "drivers/base/property.c",
"FirstLine": 1349,
"LineCount": 20
}Results: {
"Contents": "1349:\tconst void *device_get_match_data(const struct device *dev)\n1350:\t{\n1351:\t\treturn fwnode_call_ptr_op(dev_fwnode(dev), device_get_match_data, dev);\n1352:\t}\n1353:\tEXPORT_SYMBOL_GPL(device_get_match_data);\n1354:\t\n1355:\tstatic unsigned int fwnode_graph_devcon_matches(const struct fwnode_handle *fwnode,\n1356:\t\t\t\t\t\t\tconst char *con_id, void *data,\n1357:\t\t\t\t\t\t\tdevcon_match_fn_t match,\n1358:\t\t\t\t\t\t\tvoid **matches,\n1359:\t\t\t\t\t\t\tunsigned int matches_len)\n1360:\t{\n1361:\t\tstruct fwnode_handle *node;\n1362:\t\tstruct fwnode_handle *ep;\n1363:\t\tunsigned int count = 0;\n1364:\t\tvoid *ret;\n1365:\t\n1366:\t\tfwnode_graph_for_each_endpoint(fwnode, ep) {\n1367:\t\t\tif (matches \u0026\u0026 count \u003e= matches_len) {\n1368:\t\t\t\tfwnode_handle_put(ep);\n"
} |
| 25/2 | 2026/08/02 18:29 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 35906 output: 34 thoughts: 269 Thoughts: **Analyzing the Device Matching Logic** Okay, so I'm looking at this `device_get_match_data` function, and it seems like all it does is call `fwnode_call_ptr_op`. Hmm, that's interesting. I need to understand what's happening under the hood. Let me quickly double-check where `device_get_match_data` is defined. Ah, it's in `property.c`, not `core.c` like I initially thought. Right, so if `device_get_match_data` solely relies on `fwnode`, and if `fwnode` is *null* for devices created through sysfs, then the function will return *null*, and that doesn't make sense. But wait, did I miss something? Is there another match check being done? Is `dev->driver->of_match_table` being consulted anywhere? Let's dive deeper into `fwnode_call_ptr_op`. Okay, it calls `fwnode->ops->device_get_match_data`. Got it. And for OF nodes, that's `of_fwnode_device_get_match_data`. And if `fwnode` is indeed NULL, `fwnode_call_ptr_op` returns NULL, which makes this problem worse. I need to understand how the driver is supposed to be matched. Hang on a second… wasn't there a recent change? Didn't they update `device_get_match_data` after Linux 6.7 to *also* check `dev->driver->of_match_table` directly? This might be the crucial piece I'm missing. Right, let me go back and re-examine `drivers/base/property.c` to see if that's the case. |
| 26/2 | 2026/08/02 18:30 | tool | grepper |
0mArgs:{
"Expression": "device_get_match_data",
"PathPrefix": "drivers/base/property.c"
}Results: {
"Output": "drivers/base/property.c=1347=EXPORT_SYMBOL(fwnode_graph_parse_endpoint);\ndrivers/base/property.c-1348-\ndrivers/base/property.c:1349:const void *device_get_match_data(const struct device *dev)\ndrivers/base/property.c-1350-{\ndrivers/base/property.c:1351:\treturn fwnode_call_ptr_op(dev_fwnode(dev), device_get_match_data, dev);\ndrivers/base/property.c-1352-}\ndrivers/base/property.c:1353:EXPORT_SYMBOL_GPL(device_get_match_data);\ndrivers/base/property.c-1354-\n"
} |
| 27/2 | 2026/08/02 18:30 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 36804 output: 29 thoughts: 970 Thoughts: **Analyzing the Probe Mechanism for `rtpse_mcu_i2c_driver` and Potential Fuzzing Limitations** Okay, let's break this down. It looks like the driver uses `fwnode_call_ptr_op` with `device_get_match_data`. If the fwnode is NULL, which it might very well be, we get NULL. Now, how does `i2c_get_match_data` handle this? It falls back to `i2c_match_id` if `device_get_match_data` returns NULL. But, wait...the `rtpse_mcu_i2c_driver` *doesn't* have an `id_table`! This means, even if `i2c_get_match_data` *is* called, it's going to return NULL. So, if there's no fwnode and no `id_table`, how in the world can this driver be probed via sysfs? It can't! Right, so no sysfs probing. Can it be probed via a device tree overlay? No. Syzkaller doesn't dynamically support device tree overlays. Any other instantiation method that uses a fwnode? `i2c_new_client_device`? Potentially, if called from another driver. What about `serdev`? Let me quickly check the `realtek-pse-mcu-uart.c` driver. Ah, yes, serdev devices are instantiated from device tree or ACPI. Can Syzkaller instantiate serdev devices? No, those devices are children of UART controllers. They are created during boot if they are in the device tree. Syzkaller uses `vhci`, `mac80211_hwsim`, `uinput`, and `hci` for various things. But does it have a mechanism to fuzz I2C or serdev devices that require device tree? Not unless the device tree in the syzkaller image (e.g., QEMU or crosvm) *happens* to include this device, which it won't for a new driver. Is there any way to attach this driver via another subsystem? Not as an I2C or serdev device. Is there a way to use `i2c-stub` to instantiate it? `i2c-stub` creates an I2C adapter, and you can use `/sys/bus/i2c/devices/i2c-X/new_device`. But as we already established, `new_device` creates a device *without* a fwnode. Therefore, because the driver uses `device_get_match_data` and has no `id_table`, the `probe` will fail with `-ENODEV`. Let's just double check that `new_device` can't, in any way, magically set a fwnode. |
| 28/2 | 2026/08/02 18:30 | tool | grepper |
0mArgs:{
"Expression": "new_device",
"PathPrefix": "drivers/i2c/"
}Results: {
"Output": "drivers/i2c/i2c-core-acpi.c=523=struct notifier_block i2c_acpi_notifier = {\n--\ndrivers/i2c/i2c-core-acpi.c-527-/**\ndrivers/i2c/i2c-core-acpi.c:528: * i2c_acpi_new_device_by_fwnode - Create i2c-client for the Nth I2cSerialBus resource\ndrivers/i2c/i2c-core-acpi.c-529- * @fwnode: fwnode with the ACPI resources to get the client from\n--\ndrivers/i2c/i2c-core-acpi.c-544- */\ndrivers/i2c/i2c-core-acpi.c:545:struct i2c_client *i2c_acpi_new_device_by_fwnode(struct fwnode_handle *fwnode,\ndrivers/i2c/i2c-core-acpi.c-546-\t\t\t\t\t\t int index,\n--\ndrivers/i2c/i2c-core-acpi.c-579-}\ndrivers/i2c/i2c-core-acpi.c:580:EXPORT_SYMBOL_GPL(i2c_acpi_new_device_by_fwnode);\ndrivers/i2c/i2c-core-acpi.c-581-\n--\ndrivers/i2c/i2c-core-base.c=1263=static ssize_t\ndrivers/i2c/i2c-core-base.c:1264:new_device_store(struct device *dev, struct device_attribute *attr,\ndrivers/i2c/i2c-core-base.c-1265-\t\t const char *buf, size_t count)\n--\ndrivers/i2c/i2c-core-base.c-1276-\tif (!blank) {\ndrivers/i2c/i2c-core-base.c:1277:\t\tdev_err(dev, \"%s: Missing parameters\\n\", \"new_device\");\ndrivers/i2c/i2c-core-base.c-1278-\t\treturn -EINVAL;\n--\ndrivers/i2c/i2c-core-base.c-1280-\tif (blank - buf \u003e I2C_NAME_SIZE - 1) {\ndrivers/i2c/i2c-core-base.c:1281:\t\tdev_err(dev, \"%s: Invalid device name\\n\", \"new_device\");\ndrivers/i2c/i2c-core-base.c-1282-\t\treturn -EINVAL;\n--\ndrivers/i2c/i2c-core-base.c-1288-\tif (res \u003c 1) {\ndrivers/i2c/i2c-core-base.c:1289:\t\tdev_err(dev, \"%s: Can't parse I2C address\\n\", \"new_device\");\ndrivers/i2c/i2c-core-base.c-1290-\t\treturn -EINVAL;\n--\ndrivers/i2c/i2c-core-base.c-1292-\tif (res \u003e 1 \u0026\u0026 end != '\\n') {\ndrivers/i2c/i2c-core-base.c:1293:\t\tdev_err(dev, \"%s: Extra parameters\\n\", \"new_device\");\ndrivers/i2c/i2c-core-base.c-1294-\t\treturn -EINVAL;\n--\ndrivers/i2c/i2c-core-base.c-1314-\tmutex_unlock(\u0026adap-\u003euserspace_clients_lock);\ndrivers/i2c/i2c-core-base.c:1315:\tdev_info(dev, \"%s: Instantiated device %s at 0x%02hx\\n\", \"new_device\",\ndrivers/i2c/i2c-core-base.c-1316-\t\t info.type, info.addr);\n--\ndrivers/i2c/i2c-core-base.c-1319-}\ndrivers/i2c/i2c-core-base.c:1320:static DEVICE_ATTR_WO(new_device);\ndrivers/i2c/i2c-core-base.c-1321-\n--\ndrivers/i2c/i2c-core-base.c-1324- * they got it wrong. This interface can only be used to delete devices\ndrivers/i2c/i2c-core-base.c:1325: * instantiated by i2c_sysfs_new_device above. This guarantees that we\ndrivers/i2c/i2c-core-base.c-1326- * don't delete devices to which some kernel code still has references.\n--\ndrivers/i2c/i2c-core-base.c=1378=static struct attribute *i2c_adapter_attrs[] = {\ndrivers/i2c/i2c-core-base.c-1379-\t\u0026dev_attr_name.attr,\ndrivers/i2c/i2c-core-base.c:1380:\t\u0026dev_attr_new_device.attr,\ndrivers/i2c/i2c-core-base.c-1381-\t\u0026dev_attr_delete_device.attr,\n"
} |
| 29/2 | 2026/08/02 18:30 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 39783 output: 44 thoughts: 29 Thoughts: Here's my take on the `new_device_store` function, as if I'm walking through it myself, keeping in mind my deep understanding of kernel I2C drivers: **Analyzing the `new_device_store` Function** Alright, let's dive into this `new_device_store` function. The filename suggests we're dealing with device registration or something related to the I2C core, and the path confirms it's within the I2C driver infrastructure – specifically, the `i2c-core-base.c` file. This tells me it's likely a critical piece for handling the creation or modification of I2C device entries. I'm expecting this to be the place where we validate the user's provided I2C device details and actually get the devices ready. First I'll be looking for where this function is called from, and what arguments are passed. Based on that information, I'll be forming expectations as to what the purpose of this function is. My initial assumption is that this is called from sysfs or some other user-space interface, allowing someone to dynamically add or adjust I2C devices at runtime. It's probably responsible for handling the user's input, parsing some data (maybe a bus number, slave address, device name, etc.), and then initiating the driver binding process if everything checks out. If there is an error, that will be where the error is reported. I'll be looking for things like mutexes or spinlocks to ensure concurrency safety, since multiple users could potentially be fiddling with I2C devices simultaneously. I'm also anticipating some interaction with the I2C adapter structure (`i2c_adapter`) and potentially the I2C client structure (`i2c_client`). I'll also keep a sharp eye out for potential security vulnerabilities. Are they validating inputs properly? Is there a risk of buffer overflows or format string bugs? It's a kernel function, so any security hole here is going to be bad news. I'm thinking about the bigger picture. How does this fit into the overall I2C driver model? Does it involve probing for devices, or is it purely manual configuration? Understanding the context will be critical to fully grasping the function's role. |
| 30/2 | 2026/08/02 18:30 | tool | read-file |
0mArgs:{
"File": "drivers/i2c/i2c-core-base.c",
"FirstLine": 1263,
"LineCount": 40
}Results: {
"Contents": "1263:\tstatic ssize_t\n1264:\tnew_device_store(struct device *dev, struct device_attribute *attr,\n1265:\t\t\t const char *buf, size_t count)\n1266:\t{\n1267:\t\tstruct i2c_adapter *adap = to_i2c_adapter(dev);\n1268:\t\tstruct i2c_board_info info;\n1269:\t\tstruct i2c_client *client;\n1270:\t\tchar *blank, end;\n1271:\t\tint res;\n1272:\t\n1273:\t\tmemset(\u0026info, 0, sizeof(struct i2c_board_info));\n1274:\t\n1275:\t\tblank = strchr(buf, ' ');\n1276:\t\tif (!blank) {\n1277:\t\t\tdev_err(dev, \"%s: Missing parameters\\n\", \"new_device\");\n1278:\t\t\treturn -EINVAL;\n1279:\t\t}\n1280:\t\tif (blank - buf \u003e I2C_NAME_SIZE - 1) {\n1281:\t\t\tdev_err(dev, \"%s: Invalid device name\\n\", \"new_device\");\n1282:\t\t\treturn -EINVAL;\n1283:\t\t}\n1284:\t\tmemcpy(info.type, buf, blank - buf);\n1285:\t\n1286:\t\t/* Parse remaining parameters, reject extra parameters */\n1287:\t\tres = sscanf(++blank, \"%hi%c\", \u0026info.addr, \u0026end);\n1288:\t\tif (res \u003c 1) {\n1289:\t\t\tdev_err(dev, \"%s: Can't parse I2C address\\n\", \"new_device\");\n1290:\t\t\treturn -EINVAL;\n1291:\t\t}\n1292:\t\tif (res \u003e 1 \u0026\u0026 end != '\\n') {\n1293:\t\t\tdev_err(dev, \"%s: Extra parameters\\n\", \"new_device\");\n1294:\t\t\treturn -EINVAL;\n1295:\t\t}\n1296:\t\n1297:\t\tif ((info.addr \u0026 I2C_ADDR_OFFSET_TEN_BIT) == I2C_ADDR_OFFSET_TEN_BIT) {\n1298:\t\t\tinfo.addr \u0026= ~I2C_ADDR_OFFSET_TEN_BIT;\n1299:\t\t\tinfo.flags |= I2C_CLIENT_TEN;\n1300:\t\t}\n1301:\t\n1302:\t\tif (info.addr \u0026 I2C_ADDR_OFFSET_SLAVE) {\n"
} |
| 31/2 | 2026/08/02 18:30 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 41011 output: 36 thoughts: 735 Thoughts: **Unreachable Driver in Syzkaller's Environment** Okay, let's break this down. It seems the code path for this I2C driver is fundamentally unreachable within the confines of syzkaller's current environment. The core issue revolves around how the device gets instantiated, and consequently, how the probe function is triggered. First, `new_device_store` is calling `i2c_new_client_device`, but crucially, it's *not* setting the `fwnode` or `of_node` fields in the device information. This means the `client->dev.fwnode` will be null. Consequently, any subsequent calls to `device_get_match_data` are doomed from the start. Even if the driver *did* use an `id_table`, the initial setup wouldn't provide the necessary match data. Thus, the probe fails with `-ENODEV`. So, no immediate sysfs trigger. Alright, what about device tree overlays? That's a potential workaround for instantiating devices with fwnode data. But, syzkaller doesn't leverage device tree overlays, right? No, that's not its typical operation. Okay, let's explore serdev. Serdev devices can be born from device tree or ACPI. Does syzkaller do that? Nope, it doesn't. There's no equivalent of a "new_device" for serdev within the syzkaller framework. This points to a significant problem: the driver is completely isolated from syzkaller's instantiation methods, at least in a standard QEMU x86_64 or arm64 setup. No device tree support, no sysfs "new_device" mechanism working. So, is there *any* other mechanism for instantiating an I2C device with fwnode? I was thinking, could syzkaller use `bpf` to attach a kprobe and dynamically call `i2c_new_client_device` with fwnode, injecting it that way? But that's not something syzkaller typically does either. What about `syz_i2c_setup`? Syzkaller doesn't have such a dedicated function. It does have `/dev/i2c-X`, of course, which provides userspace access to send I2C messages. But that doesn't actually instantiate a *kernel* driver. Hold on. I'm recalling the patterns used with other buses. For USB we have `syz_usb_connect`. Wifi has `syz_80211_join_ibss`. But there is nothing similar for I2C or Device Tree. That further reinforces the idea: the driver is out of reach. Okay, what if the driver is built as a module and then loaded? If we load the module, the `probe` function doesn't automatically execute unless there's a matching device. The device doesn't exist in the syzkaller environment, so `probe` is never called. It's unreachable. Let's do one last check. Could `device_get_match_data` match on something besides `fwnode`? Perhaps `driver.name`? Nope. `device_get_match_data` is strictly tied to `fwnode`, confirmed by tracing the code through `fwnode_call_ptr_op`. So, it's highly improbable that `rtpse_mcu_i2c_probe` or `rtpse_mcu_uart_probe` can be reached. They're registered via `module_i2c_driver` and `module_serdev_device_driver`, meaning the driver core's matching logic needs a visible device, and that matching mechanism isn't present in this setup. The code is demonstrably unreachable. |
| 32/2 | 2026/08/02 18:30 | tool | grepper |
0mError:no matches Args: {
"Expression": "device_get_match_data",
"PathPrefix": "drivers/base/swnode.c"
}Results: {
"error": "no matches"
} |
| 33/2 | 2026/08/02 18:30 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 42616 output: 34 thoughts: 29 Thoughts: Okay, here's my take on those thoughts, framed as an expert's inner monologue: **Diving into the Device Tree Match Data Retrieval** Right, let's get into the nitty-gritty of how this matching works. My immediate thought is to zero in on `of_fwnode_device_get_match_data`, specifically within `drivers/of/property.c`. This function is the key to understanding how we extract that crucial matching data from the device tree – the properties that tell us *which* driver is the intended owner of this node. I need to refresh my memory on the specifics of its implementation, what kind of data structures it's interacting with, and the logic it uses to resolve the various match types (compatible strings, bindings, etc.). Understanding the retrieval process is critical to troubleshooting driver binding issues or optimizing the Device Tree handling, so this is where I'll begin my investigation. Let's see what gems we can find in the code. |
| 34/2 | 2026/08/02 18:30 | tool | grepper |
0mArgs:{
"Expression": "device_get_match_data",
"PathPrefix": "drivers/of/property.c"
}Results: {
"Output": "drivers/of/property.c=1251=static const void *\ndrivers/of/property.c:1252:of_fwnode_device_get_match_data(const struct fwnode_handle *fwnode,\ndrivers/of/property.c-1253-\t\t\t\tconst struct device *dev)\ndrivers/of/property.c-1254-{\ndrivers/of/property.c:1255:\treturn of_device_get_match_data(dev);\ndrivers/of/property.c-1256-}\n--\ndrivers/of/property.c=1687=const struct fwnode_operations of_fwnode_ops = {\n--\ndrivers/of/property.c-1690-\t.device_is_available = of_fwnode_device_is_available,\ndrivers/of/property.c:1691:\t.device_get_match_data = of_fwnode_device_get_match_data,\ndrivers/of/property.c-1692-\t.device_dma_supported = of_fwnode_device_dma_supported,\n"
} |
| 35/2 | 2026/08/02 18:30 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 43176 output: 36 thoughts: 22 Thoughts: Here's my summary, as though I'm thinking through this myself, given my expertise: **Diving into Device Matching in the Device Tree Context** Okay, so I'm looking at device tree stuff, and I've got to understand how this driver code is actually linking hardware descriptions to driver instances. Specifically, I'm zeroing in on this `of_device_get_match_data` function within `drivers/of/device.c`. That name alone screams "match data," which is precisely what I'm after: the nitty-gritty details used in the process of matching a device node in the device tree with the appropriate driver. I'm assuming this function is critical. My guess is it's responsible for pulling out all of the juicy details – the platform data, the properties, anything the driver might need to configure itself – from the device tree node that has been identified as a match, so that the driver can proceed to bind to the hardware. Now, I have to figure out how *it* does it, and how I can best use it. |
| 36/2 | 2026/08/02 18:30 | tool | grepper |
0mArgs:{
"Expression": "of_device_get_match_data",
"PathPrefix": "drivers/of/device.c"
}Results: {
"Output": "drivers/of/device.c=170=EXPORT_SYMBOL_GPL(of_dma_configure_id);\ndrivers/of/device.c-171-\ndrivers/of/device.c:172:const void *of_device_get_match_data(const struct device *dev)\ndrivers/of/device.c-173-{\n--\ndrivers/of/device.c-181-}\ndrivers/of/device.c:182:EXPORT_SYMBOL(of_device_get_match_data);\ndrivers/of/device.c-183-\n"
} |
| 37/2 | 2026/08/02 18:30 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 43617 output: 35 thoughts: 15 Thoughts: Okay, here we go... **Analyzing `of_device_get_match_data`** Alright, let's dive into this function, `of_device_get_match_data`. Immediately, my brain starts to parse what this means in the broader context. "of_device" suggests we're dealing with device tree information, so this is likely a kernel function designed to retrieve matching data related to a device's entry in the device tree. That makes sense; finding the right device node and extracting relevant configuration details is a fundamental part of device driver initialization. "get_match_data" implies this isn't just about finding *a* match, but about retrieving *specific* data associated with that match. So, it's not a simple 'yes/no' lookup. We're looking for an *object* or *structure* of data. I'll need to figure out what data specifically, this might be based on the type of match (compatible string, etc). I'm guessing this function is central to how a driver figures out *which* hardware it's talking to and how that hardware is configured. This is essential for any modern embedded Linux system. I'll be looking for how the function is used. What are the input parameters? What's the return value? Is it error-checked? Are there different matching schemes considered? This function will likely be part of a larger chain of operations – a core building block that's invoked as part of the device probe flow. I'm already anticipating questions about resource allocation, error handling, and memory management related to the returned data structure. Good, let's dig into the code itself. |
| 38/2 | 2026/08/02 18:30 | tool | read-file |
0mArgs:{
"File": "drivers/of/device.c",
"FirstLine": 172,
"LineCount": 15
}Results: {
"Contents": " 172:\tconst void *of_device_get_match_data(const struct device *dev)\n 173:\t{\n 174:\t\tconst struct of_device_id *match;\n 175:\t\n 176:\t\tmatch = of_match_device(dev-\u003edriver-\u003eof_match_table, dev);\n 177:\t\tif (!match)\n 178:\t\t\treturn NULL;\n 179:\t\n 180:\t\treturn match-\u003edata;\n 181:\t}\n 182:\tEXPORT_SYMBOL(of_device_get_match_data);\n 183:\t\n 184:\t/**\n 185:\t * of_device_modalias - Fill buffer with newline terminated modalias string\n 186:\t * @dev:\tCalling device\n"
} |
| 39/2 | 2026/08/02 18:30 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 44239 output: 190 thoughts: 574 Thoughts: **Unreachable Driver in a Virtualized Environment** Okay, let me break this down. It looks like this new Realtek PSE MCU driver is completely reliant on Device Tree (DT) information to probe. Specifically, `of_match_device` ultimately calls `of_match_node` which returns NULL if `dev->of_node` is NULL. Consequently, `of_device_get_match_data` also returns NULL, which causes the probe to fail with `-ENODEV` if the device is created through sysfs, which it would be in a syzkaller context. I confirmed that if a device is created via sysfs, `dev->of_node` will be NULL. I considered a software node (`swnode`) as a workaround, but those are exclusively created by kernel code, so syzkaller can't inject one. Alright, what other entry points exist? The driver registers `rtpse_mcu_i2c_driver` and `rtpse_mcu_uart_driver`. Standard driver registration. However, without a matching device, none of the driver's code will ever execute. So, in a virtualized QEMU environment, the core driver, and therefore the entire patch, is unreachable. Just to be absolutely certain, I checked if `serdev` devices could be instantiated from userspace. No dice. `serdev` devices, are instantiated through the serdev controller (UART driver), and it's based on either device tree or ACPI. There's no sysfs method. So, both I2C and UART transports, which is the drivers' entry point, are unreachable. The core driver `realtek-pse-mcu-core.c` is only called by the transport drivers. Therefore, the whole patch series is dead code, and un-fuzzable. I will set WorthFuzzing to false. |
| 40/2 | 2026/08/02 18:31 | tool | set-results |
0mArgs:{
"EnableConfigs": [],
"FocusSymbols": [],
"Reasoning": "The patch introduces a new driver for Realtek PSE MCU, which supports I2C and UART (serdev) transports. The driver relies on device tree (OF) match data to probe successfully. In a virtualized environment like QEMU used by syzkaller, there is no device tree node for this hardware. Furthermore, the I2C driver cannot be instantiated via the sysfs `new_device` interface because it strictly requires `device_get_match_data()`, which returns NULL for sysfs-instantiated devices (as they lack an `of_node`), causing the probe to fail immediately with `-ENODEV`. Serdev devices also cannot be instantiated from userspace. Therefore, the driver's code is completely unreachable and cannot be fuzzed.",
"WorthFuzzing": false
}Results: {
"EnableConfigs": null,
"FocusSymbols": [],
"Reasoning": "The patch introduces a new driver for Realtek PSE MCU, which supports I2C and UART (serdev) transports. The driver relies on device tree (OF) match data to probe successfully. In a virtualized environment like QEMU used by syzkaller, there is no device tree node for this hardware. Furthermore, the I2C driver cannot be instantiated via the sysfs `new_device` interface because it strictly requires `device_get_match_data()`, which returns NULL for sysfs-instantiated devices (as they lack an `of_node`), causing the probe to fail immediately with `-ENODEV`. Serdev devices also cannot be instantiated from userspace. Therefore, the driver's code is completely unreachable and cannot be fuzzed.",
"WorthFuzzing": false
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|