aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaolo Abeni <pabeni@redhat.com>2026-08-18 15:04:35 +0200
committerPaolo Abeni <pabeni@redhat.com>2026-08-18 15:04:36 +0200
commit194e4ffd2ede5b5c635f2db3bd313ef4ba9d26b7 (patch)
treeb935ee428bd2a02fc5e701966e42792f4bbef50f
parent66da914db44e51e612b07305243dc9775f53d825 (diff)
parent5cc93d9897496bcfde6821579907d309b6883ab7 (diff)
Merge branch 'net-pse-pd-add-realtek-pse-mcu-support'
Jonas Jelonek says: ==================== net: pse-pd: add Realtek PSE MCU support This series adds a PSE-PD driver for the microcontroller (MCU) that fronts the PSE silicon on a range of managed switches, together with its DT binding. Hardware model ============== These boards do not expose the PSE chips to the host directly. A small microcontroller sits on an I2C/SMBus or UART bus and manages one or more PSE chips behind it; the host CPU only ever talks to that MCU, using a fixed 12-byte request/response protocol with a trailing checksum. The PSE silicon never appears on the bus. Two generations of the protocol exist, both Realtek's: an older one on boards with Broadcom PSE silicon (BCM59111, BCM59121) and a newer one used with Realtek's own PSE silicon (RTL8238B, RTL8239, RTL8239C). They diverge in opcode numbering and a few response layouts; the driver abstracts that behind a per-dialect opcode table and parser hooks, selected by the compatible. The specific PSE chip behind the MCU is detected at runtime and only influences per-chip constants (power scaling and the per-port cap). The compatibles =============== The protocol compatibles name two generations of the Realtek protocol, with the I2C framing folded in: realtek,pse-mcu-gen1 gen1, UART realtek,pse-mcu-gen1-smbus gen1, I2C/SMBus realtek,pse-mcu-gen2 gen2, UART realtek,pse-mcu-gen2-smbus gen2, I2C/SMBus realtek,pse-mcu-gen2-i2c gen2, raw I2C and each board carries a device-specific compatible that falls back to one of these, e.g. compatible = "zyxel,xs1930-12hp-pse", "realtek,pse-mcu-gen2-smbus"; The naming is the part most likely to raise questions, so the reasoning up front (the binding documents it too): - The node describes the MCU together with its Realtek firmware, not a PSE chip and not the microcontroller silicon. The PSE chips sit behind the MCU, never appear on the bus, and are reported by the MCU and detected at runtime; the microcontroller itself is a general-purpose part (GigaDevice, Nuvoton, ...) that varies across boards. What is fixed and Realtek's is the firmware and its host protocol - hence the 'realtek' prefix. - gen1 and gen2 are two generations of that protocol, both Realtek's: gen1 on older boards fronting Broadcom PSE silicon, gen2 the altered protocol used once Realtek shipped their own PSE silicon. The generation is fixed per board and is all the driver needs at DT-parse time, so the compatible encodes it. - On I2C the MCU firmware expects one of two framings - SMBus or raw I2C - which is a genuine programming-model difference, so it is part of the compatible ('-smbus' / '-i2c'). A UART attachment carries no framing suffix; the transport is given structurally by the parent 'serial' node. - Each board additionally carries a device-specific compatible that falls back to the protocol one. The driver only ever binds on the protocol compatible; the device-specific string keeps the binding specific and reserves a place for a future per-board quirk without having to retrofit device trees already deployed in the field. Testing ======= - Linksys LGS328MPCv2 (RTL8238B, I2C) - Zyxel GS1900-10HP A1 (BCM59121, UART) - Zyxel GS1900-10HP B1 (RTL8238B, UART) - Zyxel GS1920-24HPv2 (BCM59121, SMBus) - Zyxel XMG1915-10EP (RTL8239C, UART) - Zyxel XS1930-12HP (RTL8239, SMBus) ==================== Link: https://patch.msgid.link/20260813222036.873930-1-jelonek.jonas@gmail.com Signed-off-by: Paolo Abeni <pabeni@redhat.com>
-rw-r--r--Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml182
-rw-r--r--MAINTAINERS7
-rw-r--r--drivers/net/pse-pd/Kconfig28
-rw-r--r--drivers/net/pse-pd/Makefile3
-rw-r--r--drivers/net/pse-pd/realtek-pse-mcu-core.c998
-rw-r--r--drivers/net/pse-pd/realtek-pse-mcu-i2c.c148
-rw-r--r--drivers/net/pse-pd/realtek-pse-mcu-uart.c164
-rw-r--r--drivers/net/pse-pd/realtek-pse-mcu.h94
8 files changed, 1624 insertions, 0 deletions
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 000000000000..3bb32349c28c
--- /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 94a468d66abf..f68dd1b99339 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22768,6 +22768,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 7ef29657ee5d..a0f2ae668c67 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 cc78f7ea7f5f..9cca5900fe34 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 000000000000..e64f13ea6e4e
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-core.c
@@ -0,0 +1,998 @@
+// 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
+
+/* Bounded resends when the MCU replies NOT_READY (busy). */
+#define RTPSE_MCU_NOT_READY_RETRIES 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)
+{
+ unsigned int tries;
+ int ret;
+
+ for (tries = 0; ; tries++) {
+ scoped_guard(mutex, &pse->mutex) {
+ /* Rolling seq_num (skip 0) so a stale/all-zero reply can't match. */
+ if (++pse->seq == 0)
+ pse->seq = 1;
+ 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;
+
+ /* Pace the base reply delay; the transport waits its own way. */
+ msleep(RTPSE_MCU_RESPONSE_MS);
+
+ memset(resp, 0, sizeof(*resp));
+ ret = pse->transport->recv(pse, req, resp);
+ if (ret)
+ return ret;
+ }
+
+ /* NOT_READY: MCU busy, wants the command resent; bounded retry. */
+ if (resp->opcode != RTPSE_MCU_OPCODE_NOT_READY ||
+ tries >= RTPSE_MCU_NOT_READY_RETRIES)
+ break;
+ msleep(RTPSE_MCU_RESPONSE_MS);
+ }
+
+ /* Explicit MCU error opcodes (Gen1); map to 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;
+
+ /*
+ * The MCU's raw max_power byte can scale above the chip's rated cap;
+ * clamp to the same bound set_pw_limit() and the advertised range use.
+ */
+ return min_t(u32, config.max_power * pse->chip->pw_read_lsb_mW,
+ pse->chip->max_mW_per_port);
+}
+
+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;
+
+ /*
+ * 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);
+
+ /*
+ * Program the value before switching to user-defined mode. The two
+ * commands aren't atomic, but this order never leaves a stale cap: a
+ * failure keeps the previous cap, or (already user mode) the requested.
+ */
+ ret = rtpse_mcu_port_cmd(pse, id, val_opc->op, prg_val);
+ if (ret)
+ return ret;
+
+ return rtpse_mcu_port_cmd(pse, id, type_opc->op, RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER);
+}
+
+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;
+
+ /* Clamp to the advertised max; set_prio() and pis_prio_max use the same bound. */
+ return min_t(u8, config.priority, RTPSE_MCU_PORT_MAX_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 = rtp