]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
realtek: add Realtek PSE MCU driver
authorJonas Jelonek <jelonek.jonas@gmail.com>
Sun, 3 May 2026 20:44:35 +0000 (20:44 +0000)
committerJonas Jelonek <jelonek.jonas@gmail.com>
Tue, 14 Jul 2026 21:45:29 +0000 (23:45 +0200)
Add pending patches which add a PSE driver for the PSE setup found on
most of Realtek-based switches. An MCU with a Realtek-defined firmware
and protocol fronts one or more PSE chips (from Realtek or Broadcom) as
a management controller.

The driver provides both I2C/SMBus and UART communication, which varies
among our supported switches. There is no need for complicated userspace
handling anymore, the PSE setup is defined in the device tree and
interfaced in userspace via ethtool, netlink and netifd support.

Those patches have progressed far enough upstream, the bindings are
reviewed. Thus, we can keep this intermediate version downstream to open
it up earlier for usage. Only little changes are expected til the final
version lands.

Link: https://github.com/openwrt/openwrt/pull/23222
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
package/kernel/linux/modules/pse-pd.mk
target/linux/realtek/patches-6.18/816-01-net-pse-pd-add-Realtek-Broadcom-PSE-MCU-core.patch [new file with mode: 0644]
target/linux/realtek/patches-6.18/816-02-net-pse-pd-realtek-pse-mcu-add-I2C-transport.patch [new file with mode: 0644]
target/linux/realtek/patches-6.18/816-03-net-pse-pd-realtek-pse-mcu-add-UART-transport.patch [new file with mode: 0644]
target/linux/realtek/rtl838x/config-6.18
target/linux/realtek/rtl839x/config-6.18
target/linux/realtek/rtl930x/config-6.18
target/linux/realtek/rtl930x_nand/config-6.18
target/linux/realtek/rtl931x/config-6.18
target/linux/realtek/rtl931x_nand/config-6.18

index fb26e5fca39b7507e8a8280a4b979623e4433b85..5190d6ebf970bd2ddc8b1eac9814bb301939e4ff 100644 (file)
@@ -89,3 +89,57 @@ define KernelPackage/pse-tps23881/description
 endef
 
 $(eval $(call KernelPackage,pse-tps23881))
+
+define KernelPackage/pse-realtek-mcu
+  SUBMENU:=$(PSE_MENU)
+  TITLE:=Realtek/Broadcom PSE MCU driver (core)
+  KCONFIG:=CONFIG_PSE_REALTEK_MCU
+  DEPENDS:=@TARGET_realtek
+  FILES:=$(LINUX_DIR)/drivers/net/pse-pd/realtek-pse-mcu-core.ko
+  $(call AddDepends/pse-pd)
+endef
+
+define KernelPackage/pse-realtek-mcu/description
+ Library module shared by the drivers for the Realtek/Broadcom PSE MCU
+ (kmod-pse-realtek-mcu-i2c, kmod-pse-realtek-mcu-uart).
+ Pulled in automatically when one of the transport modules is selected.
+endef
+
+$(eval $(call KernelPackage,pse-realtek-mcu))
+
+define KernelPackage/pse-realtek-mcu-i2c
+  SUBMENU:=$(PSE_MENU)
+  TITLE:=Realtek/Broadcom PSE MCU driver (I2C transport)
+  KCONFIG:=CONFIG_PSE_REALTEK_MCU_I2C
+  DEPENDS:=@TARGET_realtek +kmod-i2c-core +kmod-pse-realtek-mcu
+  FILES:=$(LINUX_DIR)/drivers/net/pse-pd/realtek-pse-mcu-i2c.ko
+  AUTOLOAD:=$(call AutoProbe,realtek-pse-mcu-i2c)
+  $(call AddDepends/pse-pd)
+endef
+
+define KernelPackage/pse-realtek-mcu-i2c/description
+ Realtek/Broadcom PSE MCU driver, widely used on Realtek-based switches.
+ Attached via I2C/SMBus.
+endef
+
+$(eval $(call KernelPackage,pse-realtek-mcu-i2c))
+
+define KernelPackage/pse-realtek-mcu-uart
+  SUBMENU:=$(PSE_MENU)
+  TITLE:=Realtek/Broadcom PSE MCU driver (UART transport)
+  KCONFIG:= \
+       CONFIG_PSE_REALTEK_MCU_UART \
+       CONFIG_SERIAL_DEV_BUS=y \
+       CONFIG_SERIAL_DEV_CTRL_TTYPORT=y
+  DEPENDS:=@TARGET_realtek +kmod-pse-realtek-mcu
+  FILES:=$(LINUX_DIR)/drivers/net/pse-pd/realtek-pse-mcu-uart.ko
+  AUTOLOAD:=$(call AutoProbe,realtek-pse-mcu-uart)
+  $(call AddDepends/pse-pd)
+endef
+
+define KernelPackage/pse-realtek-mcu-uart/description
+ Realtek/Broadcom PSE MCU driver, widely used on Realtek-based switches.
+ Attached via UART.
+endef
+
+$(eval $(call KernelPackage,pse-realtek-mcu-uart))
diff --git a/target/linux/realtek/patches-6.18/816-01-net-pse-pd-add-Realtek-Broadcom-PSE-MCU-core.patch b/target/linux/realtek/patches-6.18/816-01-net-pse-pd-add-Realtek-Broadcom-PSE-MCU-core.patch
new file mode 100644 (file)
index 0000000..077fa59
--- /dev/null
@@ -0,0 +1,1175 @@
+From cf51089eb907e5cd8f9e50a4054d1d4345563799 Mon Sep 17 00:00:00 2001
+From: Jonas Jelonek <jelonek.jonas@gmail.com>
+Date: Sun, 5 Jul 2026 22:15:32 +0000
+Subject: [PATCH net-next v7 2/4] net: pse-pd: add Realtek PSE MCU core
+
+A range of managed Realtek-based PoE switches use a small microcontroller
+on the PCB to front the actual PSE silicon. The host CPU talks to that
+MCU over I2C/SMBus or UART using a fixed 12-byte request/response
+protocol with a trailing checksum; the PSE chips are managed by the MCU
+and are not accessed directly. Two generations of the protocol exist -
+both Realtek's - diverging in opcode numbering and a few response
+layouts; the driver handles this with a per-dialect opcode table and
+parser hooks for the responses that differ, 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).
+
+This core module implements the protocol, message framing, the dialect
+machinery and the pse_controller_ops glue, and exports a registration
+helper for transport modules. The I2C and UART transports that drive it
+follow in the next patches; the core (PSE_REALTEK_MCU) is selected
+automatically by those transports and is not user-selectable on its own.
+
+The realtek-pse-mcu-* files and PSE_REALTEK_MCU* symbols match the
+realtek,pse-mcu-* compatibles (see the binding for the naming rationale).
+The two protocol generations - gen1 on older Broadcom-PSE boards, gen2 on
+Realtek's own PSE silicon - are both Realtek's, handled by the same shared
+core, each selecting its dialect via the compatible.
+
+Power budgeting is left to the MCU firmware; the driver advertises
+PSE_BUDGET_EVAL_STRAT_DYNAMIC (controller-managed budget) accordingly.
+
+Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
+---
+ MAINTAINERS                               |   7 +
+ drivers/net/pse-pd/Kconfig                |   6 +
+ drivers/net/pse-pd/Makefile               |   1 +
+ drivers/net/pse-pd/realtek-pse-mcu-core.c | 995 ++++++++++++++++++++++
+ drivers/net/pse-pd/realtek-pse-mcu.h      |  91 ++
+ 5 files changed, 1100 insertions(+)
+ create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-core.c
+ create mode 100644 drivers/net/pse-pd/realtek-pse-mcu.h
+
+--- a/MAINTAINERS
++++ b/MAINTAINERS
+@@ -21668,6 +21668,13 @@ S:    Maintained
+ F:    include/sound/rt*.h
+ F:    sound/soc/codecs/rt*
++REALTEK PSE MCU DRIVER
++M:    Jonas Jelonek <jelonek.jonas@gmail.com>
++L:    netdev@vger.kernel.org
++S:    Maintained
++F:    Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu-gen1.yaml
++F:    drivers/net/pse-pd/realtek-pse-mcu*
++
+ REALTEK OTTO WATCHDOG
+ M:    Sander Vanheule <sander@svanheule.net>
+ L:    linux-watchdog@vger.kernel.org
+--- a/drivers/net/pse-pd/Kconfig
++++ b/drivers/net/pse-pd/Kconfig
+@@ -13,6 +13,12 @@ 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_REGULATOR
+       tristate "Regulator based PSE controller"
+       help
+--- a/drivers/net/pse-pd/Makefile
++++ b/drivers/net/pse-pd/Makefile
+@@ -3,6 +3,7 @@
+ obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
++obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
+ obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
+ obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
+ obj-$(CONFIG_PSE_SI3474) += si3474.o
+--- /dev/null
++++ b/drivers/net/pse-pd/realtek-pse-mcu-core.c
+@@ -0,0 +1,995 @@
++// 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/regulator/consumer.h>
++#include <linux/unaligned.h>
++
++#include "realtek-pse-mcu.h"
++
++#define RTPSE_MCU_DEVICE_ID_RTL8238B          0x0138
++#define RTPSE_MCU_DEVICE_ID_RTL8239           0x0039
++#define RTPSE_MCU_DEVICE_ID_RTL8239C          0x0139
++#define RTPSE_MCU_DEVICE_ID_BCM59111          0xe111
++#define RTPSE_MCU_DEVICE_ID_BCM59121          0xe121
++
++#define RTPSE_MCU_PORT_STS_DISABLED           0x00
++#define RTPSE_MCU_PORT_STS_SEARCHING          0x01
++#define RTPSE_MCU_PORT_STS_DELIVERING         0x02
++#define RTPSE_MCU_PORT_STS_TEST                       0x03    /* Gen1-only; reserved on Gen2 */
++#define RTPSE_MCU_PORT_STS_FAULT              0x04
++#define RTPSE_MCU_PORT_STS_OTHER_FAULT                0x05    /* Gen1-only; reserved on Gen2 */
++#define RTPSE_MCU_PORT_STS_REQUESTING         0x06
++
++/* RTPSE_MCU_PORT_SET_POWER_LIMIT_TYPE values */
++#define RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER     0x02
++
++#define RTPSE_MCU_MAX_PORTS                   48
++#define RTPSE_MCU_PORT_MAX_PRIORITY           3
++
++enum rtpse_mcu_cmd {
++      RTPSE_MCU_CMD_SET_GLOBAL_STATE,
++      RTPSE_MCU_CMD_GET_SYSTEM_INFO,
++      RTPSE_MCU_CMD_GET_EXT_CONFIG,
++
++      RTPSE_MCU_CMD_PORT_ENABLE,
++      RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE,
++      RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
++      RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
++      RTPSE_MCU_CMD_PORT_SET_PRIORITY,
++      RTPSE_MCU_CMD_PORT_GET_STATUS,
++      RTPSE_MCU_CMD_PORT_GET_POWER_STATS,
++      RTPSE_MCU_CMD_PORT_GET_CONFIG,
++      RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG,
++
++      RTPSE_MCU_NUM_CMDS,
++};
++
++struct rtpse_mcu_opcode {
++      u8 op;
++      bool valid;
++};
++
++/* Shorthand for the designated-initializer entries in dialect opcode tables. */
++#define RTPSE_MCU_OP(opc)     { .op = (opc), .valid = true }
++
++/* Parsed MCU response structures (decoded from rtpse_mcu_msg replies) */
++
++struct rtpse_mcu_info {
++      u8 max_ports;
++      bool system_enable;
++      u16 device_id;
++      u8 sw_ver;
++      u8 mcu_type;
++      u8 config_status;
++      u8 ext_ver;
++};
++
++struct rtpse_mcu_ext_config {
++      u8 uvlo;
++      u8 ovlo;
++      bool prealloc_enable;
++      u8 num_of_pses;
++};
++
++struct rtpse_mcu_port_status {
++      u8 sts1;
++      u8 sts2;
++      u8 sts3;
++};
++
++struct rtpse_mcu_port_measurement {
++      u16 voltage_raw;        /* 64.45mV/LSB */
++      u16 current_raw;        /* 1mA/LSB */
++      u16 temperature_raw;    /* T(mC) = 1250 * (220 - raw) */
++      u16 power_raw;          /* 100mW/LSB */
++};
++
++struct rtpse_mcu_port_config {
++      bool enable;
++};
++
++struct rtpse_mcu_port_ext_config {
++      u8 max_power;
++      u8 priority;
++};
++
++struct rtpse_mcu_dialect {
++      struct rtpse_mcu_opcode opcode[RTPSE_MCU_NUM_CMDS];
++
++      /*
++       * Response parsers for the fields that differ between dialects; each
++       * dialect supplies its own. Other responses share one layout and are
++       * decoded directly - a dialect that diverges there must add a hook,
++       * as a mismatched layout cannot be detected (the checksum still passes).
++       */
++      void (*parse_system_info)(const u8 *payload, struct rtpse_mcu_info *info);
++      int (*parse_port_class)(const struct rtpse_mcu_port_status *status);
++      const char *(*mcu_type_str)(unsigned int mcu_type);
++};
++
++struct rtpse_mcu_chip_info {
++      const char *name;
++      u16 device_id;
++      u32 max_mW_per_port;
++      enum rtpse_mcu_cmd pw_set_cmd;  /* command used by set_pw_limit */
++      u32 pw_set_lsb_mW;              /* LSB of pw_set_cmd value, in mW */
++      u32 pw_read_lsb_mW;             /* LSB of ext_config.max_power read-back, in mW */
++};
++
++static const struct rtpse_mcu_chip_info rtl8238b_info = {
++      .device_id = RTPSE_MCU_DEVICE_ID_RTL8238B,
++      .max_mW_per_port = 30000,
++      .name = "RTL8238B",
++      .pw_read_lsb_mW = 200,
++      .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
++      .pw_set_lsb_mW = 200,
++};
++
++static const struct rtpse_mcu_chip_info rtl8239_info = {
++      .device_id = RTPSE_MCU_DEVICE_ID_RTL8239,
++      .max_mW_per_port = 90000,
++      .name = "RTL8239",
++      .pw_read_lsb_mW = 400,
++      .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
++      .pw_set_lsb_mW = 400,
++};
++
++static const struct rtpse_mcu_chip_info rtl8239c_info = {
++      .device_id = RTPSE_MCU_DEVICE_ID_RTL8239C,
++      .max_mW_per_port = 90000,
++      .name = "RTL8239C",
++      .pw_read_lsb_mW = 400,
++      .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
++      .pw_set_lsb_mW = 400,
++};
++
++static const struct rtpse_mcu_chip_info bcm59111_info = {
++      .device_id = RTPSE_MCU_DEVICE_ID_BCM59111,
++      .max_mW_per_port = 30000,
++      .name = "BCM59111",
++      .pw_read_lsb_mW = 200,
++      .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
++      .pw_set_lsb_mW = 200,
++};
++
++static const struct rtpse_mcu_chip_info bcm59121_info = {
++      .device_id = RTPSE_MCU_DEVICE_ID_BCM59121,
++      /*
++       * BCM59121 is a 60W Type-3 part, but known boards run it at 802.3at
++       * and the Gen1 dialect has only the 8-bit/0.2W set command (<=51W);
++       * cap at the 30W the hardware actually offers.
++       */
++      .max_mW_per_port = 30000,
++      .name = "BCM59121",
++      .pw_read_lsb_mW = 200,
++      .pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
++      .pw_set_lsb_mW = 200,
++};
++
++/* Helpers and basic functions */
++
++static struct rtpse_mcu_ctrl *to_rtpse_mcu_ctrl(struct pse_controller_dev *pcdev)
++{
++      return container_of(pcdev, struct rtpse_mcu_ctrl, pcdev);
++}
++
++static void rtpse_mcu_msg_init(struct rtpse_mcu_msg *msg, u8 opcode)
++{
++      memset(msg, 0xff, sizeof(*msg));
++      msg->opcode = opcode;
++}
++
++static u8 rtpse_mcu_checksum(const u8 *buf, size_t len)
++{
++      u8 sum = 0;
++
++      while (len--)
++              sum += *buf++;
++      return sum;
++}
++
++static int rtpse_mcu_do_xfer(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_msg *req,
++                           struct rtpse_mcu_msg *resp)
++{
++      int ret;
++
++      scoped_guard(mutex, &pse->mutex) {
++              /* Rolling seq_num so a late reply can't pass as a later one. */
++              req->seq_num = pse->seq++;
++              req->checksum = rtpse_mcu_checksum((u8 *)req, RTPSE_MCU_MSG_SIZE - 1);
++
++              ret = pse->transport->send(pse, req);
++              if (ret)
++                      return ret;
++
++              /*
++               * The MCU needs a fixed amount of time between receiving a request
++               * and having the response ready, regardless of how the bytes get to
++               * us. Pace the transaction here so each transport can keep its recv
++               * path simple: a single bounded wait rather than a generic retry.
++               */
++              msleep(RTPSE_MCU_RESPONSE_MS);
++
++              memset(resp, 0, sizeof(*resp));
++              ret = pse->transport->recv(pse, req, resp);
++              if (ret)
++                      return ret;
++      }
++
++      /*
++       * Explicit MCU error opcodes (observed on the Gen1 dialect; harmless
++       * to check for Gen2 too). Catch these before the generic opcode/CRC
++       * mismatch path so callers see a meaningful errno.
++       */
++      switch (resp->opcode) {
++      case RTPSE_MCU_OPCODE_INCOMPLETE:
++              return -EBADE;
++      case RTPSE_MCU_OPCODE_BAD_CSUM:
++              return -EBADMSG;
++      case RTPSE_MCU_OPCODE_NOT_READY:
++              return -EAGAIN;
++      }
++
++      if (resp->opcode != req->opcode ||
++          resp->seq_num != req->seq_num ||
++          resp->checksum != rtpse_mcu_checksum((u8 *)resp, RTPSE_MCU_MSG_SIZE - 1))
++              return -EBADMSG;
++
++      return 0;
++}
++
++static int rtpse_mcu_port_query(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode,
++                              struct rtpse_mcu_msg *resp)
++{
++      struct rtpse_mcu_msg req;
++      int ret;
++
++      rtpse_mcu_msg_init(&req, opcode);
++      req.payload[0] = port;
++
++      ret = rtpse_mcu_do_xfer(pse, &req, resp);
++      if (ret)
++              return ret;
++
++      if (resp->payload[0] != port)
++              return -EIO;
++
++      return 0;
++}
++
++static int rtpse_mcu_port_cmd(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode, u8 arg)
++{
++      struct rtpse_mcu_msg req, resp;
++      int ret;
++
++      rtpse_mcu_msg_init(&req, opcode);
++      req.payload[0] = port;
++      req.payload[1] = arg;
++
++      ret = rtpse_mcu_do_xfer(pse, &req, &resp);
++      if (ret)
++              return ret;
++
++      if (resp.payload[0] != port || resp.payload[1] != 0)
++              return -EIO;
++
++      return 0;
++}
++
++/* Global operations */
++
++static int rtpse_mcu_get_info(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
++{
++      struct rtpse_mcu_msg req, resp;
++      const struct rtpse_mcu_opcode *opc;
++      int ret;
++
++      opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_SYSTEM_INFO];
++      if (!opc->valid)
++              return -EOPNOTSUPP;
++
++      rtpse_mcu_msg_init(&req, opc->op);
++      ret = rtpse_mcu_do_xfer(pse, &req, &resp);
++      if (ret)
++              return ret;
++
++      pse->dialect->parse_system_info(resp.payload, info);
++      return 0;
++}
++
++static int rtpse_mcu_get_ext_config(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_ext_config *config)
++{
++      struct rtpse_mcu_msg req, resp;
++      const struct rtpse_mcu_opcode *opc;
++      int ret;
++
++      opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_EXT_CONFIG];
++      if (!opc->valid)
++              return -EOPNOTSUPP;
++
++      rtpse_mcu_msg_init(&req, opc->op);
++      ret = rtpse_mcu_do_xfer(pse, &req, &resp);
++      if (ret)
++              return ret;
++
++      config->uvlo = resp.payload[0];
++      config->ovlo = resp.payload[5];
++      config->prealloc_enable = (resp.payload[1] == 0x1);
++      config->num_of_pses = resp.payload[6];
++
++      return 0;
++}
++
++static int rtpse_mcu_set_global_state(struct rtpse_mcu_ctrl *pse, bool enable)
++{
++      struct rtpse_mcu_msg req, resp;
++      const struct rtpse_mcu_opcode *opc;
++      int ret;
++
++      opc = &pse->dialect->opcode[RTPSE_MCU_CMD_SET_GLOBAL_STATE];
++      if (!opc->valid)
++              return -EOPNOTSUPP;
++
++      rtpse_mcu_msg_init(&req, opc->op);
++      req.payload[0] = enable ? 0x1 : 0x0;
++
++      ret = rtpse_mcu_do_xfer(pse, &req, &resp);
++      if (ret)
++              return ret;
++
++      return (resp.payload[0] == 0x0) ? 0 : -EIO;
++}
++
++/* Port operations */
++
++static int rtpse_mcu_port_get_status(struct rtpse_mcu_ctrl *pse, unsigned int port,
++                                   struct rtpse_mcu_port_status *status)
++{
++      const struct rtpse_mcu_opcode *opc;
++      struct rtpse_mcu_msg resp;
++      int ret;
++
++      opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_STATUS];
++      if (!opc->valid)
++              return -EOPNOTSUPP;
++
++      ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
++      if (ret)
++              return ret;
++
++      status->sts1 = resp.payload[1];
++      status->sts2 = resp.payload[2];
++      status->sts3 = resp.payload[3];
++
++      return 0;
++}
++
++static int rtpse_mcu_port_get_measurement(struct rtpse_mcu_ctrl *pse, unsigned int port,
++                                        struct rtpse_mcu_port_measurement *measurement)
++{
++      const struct rtpse_mcu_opcode *opc;
++      struct rtpse_mcu_msg resp;
++      int ret;
++
++      opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_POWER_STATS];
++      if (!opc->valid)
++              return -EOPNOTSUPP;
++
++      ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
++      if (ret)
++              return ret;
++
++      measurement->voltage_raw = get_unaligned_be16(&resp.payload[1]);
++      measurement->current_raw = get_unaligned_be16(&resp.payload[3]);
++      measurement->temperature_raw = get_unaligned_be16(&resp.payload[5]);
++      measurement->power_raw = get_unaligned_be16(&resp.payload[7]);
++
++      return 0;
++}
++
++static int rtpse_mcu_port_get_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
++                                   struct rtpse_mcu_port_config *config)
++{
++      const struct rtpse_mcu_opcode *opc;
++      struct rtpse_mcu_msg resp;
++      int ret;
++
++      opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_CONFIG];
++      if (!opc->valid)
++              return -EOPNOTSUPP;
++
++      ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
++      if (ret)
++              return ret;
++
++      config->enable = (resp.payload[1] == 1);
++
++      return 0;
++}
++
++static int rtpse_mcu_port_get_ext_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
++                                       struct rtpse_mcu_port_ext_config *config)
++{
++      const struct rtpse_mcu_opcode *opc;
++      struct rtpse_mcu_msg resp;
++      int ret;
++
++      opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG];
++      if (!opc->valid)
++              return -EOPNOTSUPP;
++
++      ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
++      if (ret)
++              return ret;
++
++      config->max_power = resp.payload[3];
++      config->priority = resp.payload[4];
++
++      return 0;
++}
++
++static int rtpse_mcu_port_set_state(struct rtpse_mcu_ctrl *pse, unsigned int port, bool enable)
++{
++      const struct rtpse_mcu_opcode *opc;
++
++      opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_ENABLE];
++      if (!opc->valid)
++              return -EOPNOTSUPP;
++
++      return rtpse_mcu_port_cmd(pse, port, opc->op, enable ? 0x1 : 0x0);
++}
++
++/* PSE controller ops */
++
++static int rtpse_mcu_port_get_admin_state(struct pse_controller_dev *pcdev, int id,
++                                        struct pse_admin_state *admin_state)
++{
++      struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
++      struct rtpse_mcu_port_config config;
++      int ret;
++
++      ret = rtpse_mcu_port_get_config(pse, id, &config);
++      if (ret)
++              return ret;
++
++      admin_state->c33_admin_state = config.enable ? ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED :
++                                                     ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
++      return 0;
++}
++
++static int rtpse_mcu_port_get_pw_status(struct pse_controller_dev *pcdev, int id,
++                                      struct pse_pw_status *pw_status)
++{
++      struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
++      struct rtpse_mcu_port_status status;
++      int ret;
++
++      ret = rtpse_mcu_port_get_status(pse, id, &status);
++      if (ret)
++              return ret;
++
++      switch (status.sts1) {
++      case RTPSE_MCU_PORT_STS_DISABLED:
++              pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
++              break;
++      case RTPSE_MCU_PORT_STS_SEARCHING:
++      case RTPSE_MCU_PORT_STS_REQUESTING:
++              pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
++              break;
++      case RTPSE_MCU_PORT_STS_DELIVERING:
++              pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
++              break;
++      case RTPSE_MCU_PORT_STS_TEST:
++              pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_TEST;
++              break;
++      case RTPSE_MCU_PORT_STS_FAULT:
++              pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;
++              break;
++      case RTPSE_MCU_PORT_STS_OTHER_FAULT:
++              pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_OTHERFAULT;
++              break;
++      default:
++              pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_UNKNOWN;
++              break;
++      }
++
++      return 0;
++}
++
++static int rtpse_mcu_port_get_pw_class(struct pse_controller_dev *pcdev, int id)
++{
++      struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
++      struct rtpse_mcu_port_status status;
++      int ret;
++
++      ret = rtpse_mcu_port_get_status(pse, id, &status);
++      if (ret)
++              return ret;
++
++      /*
++       * sts2 holds the class only in the operational states below. In a
++       * fault/test state (or a code reserved on the current dialect) it
++       * does not, so there is no class to report and we return 0. That is
++       * indistinguishable from a real class-0 PD; userspace disambiguates
++       * via the power status.
++       */
++      switch (status.sts1) {
++      case RTPSE_MCU_PORT_STS_DISABLED:
++      case RTPSE_MCU_PORT_STS_SEARCHING:
++      case RTPSE_MCU_PORT_STS_DELIVERING:
++      case RTPSE_MCU_PORT_STS_REQUESTING:
++              return pse->dialect->parse_port_class(&status);
++      default:
++              return 0;
++      }
++}
++
++static int rtpse_mcu_port_get_actual_pw(struct pse_controller_dev *pcdev, int id)
++{
++      struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
++      struct rtpse_mcu_port_measurement measurement;
++      int ret;
++
++      ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
++      if (ret)
++              return ret;
++
++      /* 100mW per LSB */
++      return measurement.power_raw * 100U;
++}
++
++static int rtpse_mcu_port_get_voltage(struct pse_controller_dev *pcdev, int id)
++{
++      struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
++      struct rtpse_mcu_port_measurement measurement;
++      int ret;
++      u32 uV;
++
++      ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
++      if (ret)
++              return ret;
++
++      /* 64.45mV per LSB */
++      uV = measurement.voltage_raw * 64450U;
++      return min_t(u32, uV, INT_MAX);
++}
++
++static int rtpse_mcu_port_enable(struct pse_controller_dev *pcdev, int id)
++{
++      return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, true);
++}
++
++static int rtpse_mcu_port_disable(struct pse_controller_dev *pcdev, int id)
++{
++      return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, false);
++}
++
++static int rtpse_mcu_port_get_pw_limit(struct pse_controller_dev *pcdev, int id)
++{
++      struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
++      struct rtpse_mcu_port_ext_config config;
++      int ret;
++
++      ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
++      if (ret)
++              return ret;
++
++      return config.max_power * pse->chip->pw_read_lsb_mW;
++}
++
++static int rtpse_mcu_port_set_pw_limit(struct pse_controller_dev *pcdev, int id, int max_mW)
++{
++      const struct rtpse_mcu_opcode *type_opc, *val_opc;
++      struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
++      const struct rtpse_mcu_chip_info *chip = pse->chip;
++      u8 prg_val;
++      int ret;
++
++      if (max_mW < 0 || max_mW > chip->max_mW_per_port)
++              return -ERANGE;
++
++      type_opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE];
++      val_opc = &pse->dialect->opcode[chip->pw_set_cmd];
++      /* pw_set_lsb_mW is the divisor below; reject a chip that lacks it. */
++      if (!type_opc->valid || !val_opc->valid || !chip->pw_set_lsb_mW)
++              return -EOPNOTSUPP;
++
++      /*
++       * Switch the port to user-defined limit mode first, then program the
++       * limit value. If the second cmd fails, the port is left in
++       * user-defined mode but with the previous limit value; the next
++       * successful set_pw_limit call recovers it.
++       */
++      ret = rtpse_mcu_port_cmd(pse, id, type_opc->op, RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER);
++      if (ret)
++              return ret;
++
++      prg_val = min_t(unsigned int, max_mW / chip->pw_set_lsb_mW, U8_MAX);
++
++      return rtpse_mcu_port_cmd(pse, id, val_opc->op, prg_val);
++}
++
++static int rtpse_mcu_port_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
++                                            struct pse_pw_limit_ranges *out)
++{
++      struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
++      struct ethtool_c33_pse_pw_limit_range *range;
++
++      range = kzalloc_obj(*range, GFP_KERNEL);
++      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_regulator_disable(void *data)
++{
++      regulator_disable(data);
++}
++
++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 gpio_desc *enable_gpio;
++      struct rtpse_mcu_info info;
++      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");
++
++      pse->poe_supply = devm_regulator_get(pse->dev, "power");
++      if (IS_ERR(pse->poe_supply))
++              return dev_err_probe(pse->dev, PTR_ERR(pse->poe_supply),
++                                   "failed to get PoE supply\n");
++
++      enable_gpio = devm_gpiod_get_optional(pse->dev, "enable", GPIOD_OUT_HIGH);
++      if (IS_ERR(enable_gpio))
++              return dev_err_probe(pse->dev, PTR_ERR(enable_gpio),
++                                   "failed to get enable gpio\n");
++
++      ret = rtpse_mcu_discover(pse, &info);
++      if (ret)
++              return ret;
++
++      ret = regulator_enable(pse->poe_supply);
++      if (ret)
++              return dev_err_probe(pse->dev, ret, "failed to enable PoE supply\n");
++
++      ret = devm_add_action_or_reset(pse->dev, rtpse_mcu_regulator_disable, pse->poe_supply);
++      if (ret)
++              return ret;
++
++      if (!info.system_enable) {
++              ret = rtpse_mcu_set_global_state(pse, true);
++              /* Dialects without a global-state concept (e.g. Gen1) return
++               * -EOPNOTSUPP; treat that as "no separate enable required".
++               */
++              if (ret && ret != -EOPNOTSUPP)
++                      return dev_err_probe(pse->dev, ret,
++                                           "failed to enable PSE system\n");
++              if (!ret) {
++                      ret = devm_add_action_or_reset(pse->dev,
++                                                     rtpse_mcu_global_disable, pse);
++                      if (ret)
++                              return ret;
++              }
++      }
++
++      /*
++       * Depending on the MCU firmware configuration (which might be different
++       * for every board), it isn't known whether the PoE subsystem is active or
++       * inactive by default. At this stage, the PSE chips might already deliver
++       * power to PDs without any explicit enable.
++       */
++
++      /* pcdev.owner is set by the transport, so the registered controller
++       * pins the transport module that owns the live device, not the core.
++       */
++      pse->pcdev.ops      = &rtpse_mcu_ops;
++      pse->pcdev.dev      = pse->dev;
++      pse->pcdev.types    = ETHTOOL_PSE_C33;
++      pse->pcdev.nr_lines = info.max_ports;
++      pse->pcdev.pis_prio_max = RTPSE_MCU_PORT_MAX_PRIORITY;
++      pse->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_DYNAMIC;
++
++      return devm_pse_controller_register(pse->dev, &pse->pcdev);
++}
++EXPORT_SYMBOL_GPL(rtpse_mcu_register);
++
++static void rtpse_mcu_gen2_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
++{
++      info->max_ports = payload[1];
++      info->system_enable = (payload[2] == 0x1);
++      info->device_id = get_unaligned_be16(&payload[3]);
++      info->sw_ver = payload[5];
++      info->mcu_type = payload[6];
++      info->config_status = payload[7];
++      info->ext_ver = payload[8];
++}
++
++static int rtpse_mcu_gen2_parse_port_class(const struct rtpse_mcu_port_status *status)
++{
++      /* Class lives in the upper nibble of sts2. */
++      return FIELD_GET(GENMASK(7, 4), status->sts2);
++}
++
++static const char *rtpse_mcu_gen2_mcu_type_str(unsigned int mcu_type)
++{
++      switch (mcu_type) {
++      case 0x00:      return "GigaDevice GD32F310";
++      case 0x01:      return "GigaDevice GD32F230";
++      case 0x02:      return "GigaDevice GD32F303";
++      case 0x03:      return "GigaDevice GD32F103";
++      case 0x04:      return "GigaDevice GD32E103";
++      case 0x10:      return "Nuvoton M0516";
++      case 0x11:      return "Nuvoton M0564";
++      case 0x12:      return "Nuvoton NUC029";
++      default:        return "unknown";
++      }
++}
++
++static void rtpse_mcu_gen1_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
++{
++      info->max_ports = payload[1];
++      /* Gen1 has no explicit system_enable byte; the closest analog is the
++       * "remote enable" bit in the system-status flags at payload[7].
++       */
++      info->system_enable = !!(payload[7] & BIT(2));
++      info->device_id = get_unaligned_be16(&payload[3]);
++      info->sw_ver = payload[5];
++      info->mcu_type = payload[6];
++      info->config_status = payload[7];
++      info->ext_ver = payload[8];
++}
++
++static int rtpse_mcu_gen1_parse_port_class(const struct rtpse_mcu_port_status *status)
++{
++      /* Gen1 puts the detected class in payload[3] (== sts3) directly.
++       * Mask to the low nibble; class is 0..8 and any high bits would be
++       * noise.
++       */
++      return status->sts3 & 0x0f;
++}
++
++static const char *rtpse_mcu_gen1_mcu_type_str(unsigned int mcu_type)
++{
++      switch (mcu_type) {
++      case 0x00:      return "ST Micro ST32F100";
++      case 0x01:      return "Nuvoton M05xx LAN";
++      case 0x02:      return "ST Micro STF030C8";
++      case 0x03:      return "Nuvoton M058SAN";
++      case 0x04:      return "Nuvoton NUC122";
++      default:        return "unknown";
++      }
++}
++
++/* Map each logical command the core issues to its per-dialect opcode. */
++static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen2 = {
++      .parse_system_info = rtpse_mcu_gen2_parse_system_info,
++      .parse_port_class  = rtpse_mcu_gen2_parse_port_class,
++      .mcu_type_str      = rtpse_mcu_gen2_mcu_type_str,
++      .opcode = {
++              [RTPSE_MCU_CMD_SET_GLOBAL_STATE]        = RTPSE_MCU_OP(0x00),
++              [RTPSE_MCU_CMD_GET_SYSTEM_INFO]         = RTPSE_MCU_OP(0x40),
++              [RTPSE_MCU_CMD_GET_EXT_CONFIG]          = RTPSE_MCU_OP(0x4a),
++
++              [RTPSE_MCU_CMD_PORT_ENABLE]             = RTPSE_MCU_OP(0x01),
++              [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x12),
++              [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT]    = RTPSE_MCU_OP(0x13),
++              [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT] = RTPSE_MCU_OP(0x14),
++              [RTPSE_MCU_CMD_PORT_SET_PRIORITY]       = RTPSE_MCU_OP(0x15),
++              [RTPSE_MCU_CMD_PORT_GET_STATUS]         = RTPSE_MCU_OP(0x42),
++              [RTPSE_MCU_CMD_PORT_GET_POWER_STATS]    = RTPSE_MCU_OP(0x44),
++              [RTPSE_MCU_CMD_PORT_GET_CONFIG]         = RTPSE_MCU_OP(0x48),
++              [RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG]     = RTPSE_MCU_OP(0x49),
++      },
++};
++
++static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen1 = {
++      .parse_system_info = rtpse_mcu_gen1_parse_system_info,
++      .parse_port_class  = rtpse_mcu_gen1_parse_port_class,
++      .mcu_type_str      = rtpse_mcu_gen1_mcu_type_str,
++      .opcode = {
++              [RTPSE_MCU_CMD_GET_SYSTEM_INFO]         = RTPSE_MCU_OP(0x20),
++              [RTPSE_MCU_CMD_GET_EXT_CONFIG]          = RTPSE_MCU_OP(0x2b),
++
++              [RTPSE_MCU_CMD_PORT_ENABLE]             = RTPSE_MCU_OP(0x00),
++              [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x15),
++              [RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT]    = RTPSE_MCU_OP(0x16),
++              [RTPSE_MCU_CMD_PORT_SET_PRIORITY]       = RTPSE_MCU_OP(0x1a),
++              [RTPSE_MCU_CMD_PORT_GET_STATUS]         = RTPSE_MCU_OP(0x21),
++              [RTPSE_MCU_CMD_PORT_GET_POWER_STATS]    = RTPSE_MCU_OP(0x30),
++              [RTPSE_MCU_CMD_PORT_GET_CONFIG]         = RTPSE_MCU_OP(0x25),
++              [RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG]     = RTPSE_MCU_OP(0x26),
++      },
++};
++
++const struct rtpse_mcu_match_data rtpse_mcu_gen1_data = {
++      .dialect = &rtpse_mcu_dialect_gen1,
++};
++EXPORT_SYMBOL_GPL(rtpse_mcu_gen1_data);
++
++const struct rtpse_mcu_match_data rtpse_mcu_gen2_data = {
++      .dialect = &rtpse_mcu_dialect_gen2,
++};
++EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_data);
++
++/* Same dialect as gen2, but the MCU expects raw-I2C framing. */
++const struct rtpse_mcu_match_data rtpse_mcu_gen2_i2c_data = {
++      .dialect = &rtpse_mcu_dialect_gen2,
++      .native_i2c = true,
++};
++EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_i2c_data);
++
++MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
++MODULE_DESCRIPTION("Realtek PSE MCU driver (core)");
++MODULE_LICENSE("GPL");
+--- /dev/null
++++ b/drivers/net/pse-pd/realtek-pse-mcu.h
+@@ -0,0 +1,91 @@
++/* SPDX-License-Identifier: GPL-2.0-or-later */
++
++#ifndef _REALTEK_PSE_MCU_H
++#define _REALTEK_PSE_MCU_H
++
++#include <linux/mutex.h>
++#include <linux/pse-pd/pse.h>
++#include <linux/types.h>
++
++/*
++ * Time the MCU itself needs between accepting a request and having a
++ * response ready. These are properties of the MCU firmware, not of the
++ * underlying transport: the core paces transactions by RTPSE_MCU_RESPONSE_MS
++ * and both transports size their per-transaction recv ceiling from
++ * RTPSE_MCU_RESPONSE_MAX_MS, since some commands are documented as
++ * needing up to ~1s to produce a reply.
++ */
++#define RTPSE_MCU_RESPONSE_MS                 25
++#define RTPSE_MCU_RESPONSE_MAX_MS             1000
++
++/*
++ * Total time to keep retrying the first MCU read at probe, and the pause
++ * between attempts. Right after enable-gpios is asserted the MCU may not
++ * answer on the bus yet; give it a bounded window to come up before
++ * declaring the probe failed.
++ */
++#define RTPSE_MCU_BOOT_TIMEOUT_MS             3000
++#define RTPSE_MCU_BOOT_RETRY_MS                       100
++
++#define RTPSE_MCU_MSG_SIZE                    12
++
++struct rtpse_mcu_msg {
++      u8 opcode;
++      u8 seq_num;
++      u8 payload[9];
++      u8 checksum;
++} __packed;
++
++/*
++ * MCU status opcodes (seen on the Gen1 dialect; Gen2 never emits them).
++ * INCOMPLETE/BAD_CSUM are terminal; NOT_READY is transient.
++ */
++#define RTPSE_MCU_OPCODE_INCOMPLETE           0xfd    /* -EBADE   */
++#define RTPSE_MCU_OPCODE_BAD_CSUM             0xfe    /* -EBADMSG */
++#define RTPSE_MCU_OPCODE_NOT_READY            0xff    /* -EAGAIN  */
++
++/* A polling transport can stop here: the matching reply, or a terminal error. */
++static inline bool rtpse_mcu_resp_is_final(const struct rtpse_mcu_msg *req,
++                                         const struct rtpse_mcu_msg *resp)
++{
++      return resp->opcode == req->opcode ||
++             resp->opcode == RTPSE_MCU_OPCODE_INCOMPLETE ||
++             resp->opcode == RTPSE_MCU_OPCODE_BAD_CSUM;
++}
++
++/* Opaque to transports; defined in realtek-pse-core.c. */
++struct rtpse_mcu_dialect;
++struct rtpse_mcu_chip_info;
++struct rtpse_mcu_ctrl;
++
++/* Per-compatible match data (the of_match .data). */
++struct rtpse_mcu_match_data {
++      const struct rtpse_mcu_dialect *dialect;
++      bool native_i2c;        /* raw-I2C framing (vs SMBus); I2C transport only */
++};
++
++struct rtpse_mcu_transport_ops {
++      int (*send)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req);
++      int (*recv)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
++                  struct rtpse_mcu_msg *resp);
++};
++
++struct rtpse_mcu_ctrl {
++      struct device *dev;
++      struct pse_controller_dev pcdev;
++      struct mutex mutex; /* serializes MCU request/response transactions */
++      const struct rtpse_mcu_dialect *dialect;
++      const struct rtpse_mcu_chip_info *chip;
++      const struct rtpse_mcu_transport_ops *transport;
++      u8 seq;                 /* rolling request seq_num, echoed by the MCU */
++
++      struct regulator *poe_supply;
++};
++
++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
diff --git a/target/linux/realtek/patches-6.18/816-02-net-pse-pd-realtek-pse-mcu-add-I2C-transport.patch b/target/linux/realtek/patches-6.18/816-02-net-pse-pd-realtek-pse-mcu-add-I2C-transport.patch
new file mode 100644 (file)
index 0000000..3745a75
--- /dev/null
@@ -0,0 +1,230 @@
+From bed19e2792428be06115a95ee7b23ead7132efc5 Mon Sep 17 00:00:00 2001
+From: Jonas Jelonek <jelonek.jonas@gmail.com>
+Date: Sun, 5 Jul 2026 22:16:27 +0000
+Subject: [PATCH net-next v7 3/4] net: pse-pd: realtek-pse-mcu: add I2C
+ transport
+
+Add the I2C/SMBus transport for the Realtek PSE MCU core. It registers
+the MCU on an I2C bus and provides the send/recv callbacks the core
+uses to exchange the 12-byte frames.
+
+The MCU firmware expects one of two framings on the I2C bus, and which one
+is part of the compatible: '-smbus' (reads carry a leading command byte
+and a repeated start) or raw '-i2c' (bare block writes and reads). The
+match data flags the raw-I2C case; SMBus is the default.
+
+Because i2c_master_send()/i2c_master_recv() may DMA, the raw-I2C path
+bounces each frame through a heap buffer rather than the core's stack
+buffers; the SMBus path is unaffected.
+
+Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
+---
+ drivers/net/pse-pd/Kconfig               |  11 ++
+ drivers/net/pse-pd/Makefile              |   1 +
+ drivers/net/pse-pd/realtek-pse-mcu-i2c.c | 170 +++++++++++++++++++++++
+ 3 files changed, 182 insertions(+)
+ create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-i2c.c
+
+--- a/drivers/net/pse-pd/Kconfig
++++ b/drivers/net/pse-pd/Kconfig
+@@ -19,6 +19,17 @@ config PSE_REALTEK_MCU
+         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_REGULATOR
+       tristate "Regulator based PSE controller"
+       help
+--- a/drivers/net/pse-pd/Makefile
++++ b/drivers/net/pse-pd/Makefile
+@@ -4,6 +4,7 @@
+ 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_REGULATOR) += pse_regulator.o
+ obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
+ obj-$(CONFIG_PSE_SI3474) += si3474.o
+--- /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/target/linux/realtek/patches-6.18/816-03-net-pse-pd-realtek-pse-mcu-add-UART-transport.patch b/target/linux/realtek/patches-6.18/816-03-net-pse-pd-realtek-pse-mcu-add-UART-transport.patch
new file mode 100644 (file)
index 0000000..f9fe3c3
--- /dev/null
@@ -0,0 +1,210 @@
+From 65c2e92b6172680ed08735891de038c129705f77 Mon Sep 17 00:00:00 2001
+From: Jonas Jelonek <jelonek.jonas@gmail.com>
+Date: Sun, 5 Jul 2026 22:19:49 +0000
+Subject: [PATCH net-next v7 4/4] net: pse-pd: realtek-pse-mcu: add UART
+ transport
+
+Add the serdev (UART) transport for the Realtek PSE MCU core. It registers
+the MCU as a serdev device and provides the send/recv callbacks the core
+uses to exchange the 12-byte frames, receiving asynchronously via the
+serdev receive_buf callback.
+
+The baud rate defaults to 19200 and can be overridden per board with the
+"current-speed" property.
+
+Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
+---
+ drivers/net/pse-pd/Kconfig                |  11 ++
+ drivers/net/pse-pd/Makefile               |   1 +
+ drivers/net/pse-pd/realtek-pse-mcu-uart.c | 155 ++++++++++++++++++++++
+ 3 files changed, 167 insertions(+)
+ create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-uart.c
+
+--- a/drivers/net/pse-pd/Kconfig
++++ b/drivers/net/pse-pd/Kconfig
+@@ -30,6 +30,17 @@ config PSE_REALTEK_MCU_I2C
+         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
+--- a/drivers/net/pse-pd/Makefile
++++ b/drivers/net/pse-pd/Makefile
+@@ -5,6 +5,7 @@ obj-$(CONFIG_PSE_CONTROLLER) += pse_core
+ 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
+--- /dev/null
++++ b/drivers/net/pse-pd/realtek-pse-mcu-uart.c
+@@ -0,0 +1,155 @@
++// 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;
++      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);
++      serdev_device_set_baudrate(serdev, speed);
++      serdev_device_set_flow_control(serdev, false);
++      serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
++
++      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");
index 4feac081b503ac53d12176b6d4e1556f569fa017..5e99390574b3c0d268a00f7b301529334ae60c69 100644 (file)
@@ -211,6 +211,7 @@ CONFIG_POWER_RESET_SYSCON=y
 CONFIG_PTP_1588_CLOCK_OPTIONAL=y
 CONFIG_RANDSTRUCT_NONE=y
 CONFIG_RATIONAL=y
+# CONFIG_RAVE_SP_CORE is not set
 CONFIG_REALTEK_OTTO_TIMER=y
 CONFIG_REALTEK_OTTO_WDT=y
 CONFIG_REALTEK_PHY=y
@@ -221,6 +222,8 @@ CONFIG_REGMAP=y
 CONFIG_REGMAP_I2C=y
 CONFIG_REGMAP_MDIO=y
 CONFIG_REGMAP_MMIO=y
+CONFIG_REGULATOR=y
+CONFIG_REGULATOR_FIXED_VOLTAGE=y
 CONFIG_RESET_CONTROLLER=y
 CONFIG_RTL838X=y
 # CONFIG_RTL839X is not set
index 5494c2b86e313b8aef42b2f2796142fcc4ee1d98..35b407d133bd829a0aa602a85af16e3db8cb82df 100644 (file)
@@ -221,6 +221,7 @@ CONFIG_QUEUED_RWLOCKS=y
 CONFIG_QUEUED_SPINLOCKS=y
 CONFIG_RANDSTRUCT_NONE=y
 CONFIG_RATIONAL=y
+# CONFIG_RAVE_SP_CORE is not set
 CONFIG_REALTEK_OTTO_TIMER=y
 CONFIG_REALTEK_OTTO_WDT=y
 CONFIG_REALTEK_PHY=y
@@ -231,6 +232,8 @@ CONFIG_REGMAP=y
 CONFIG_REGMAP_I2C=y
 CONFIG_REGMAP_MDIO=y
 CONFIG_REGMAP_MMIO=y
+CONFIG_REGULATOR=y
+CONFIG_REGULATOR_FIXED_VOLTAGE=y
 CONFIG_RESET_CONTROLLER=y
 CONFIG_RFS_ACCEL=y
 CONFIG_RPS=y
index f361ccfc245f406003fe3720f9ca8c4030c9346d..2cf7bd4e8c3f25535bdcadc866a20f7cd284a1a0 100644 (file)
@@ -211,6 +211,7 @@ CONFIG_QUEUED_RWLOCKS=y
 CONFIG_QUEUED_SPINLOCKS=y
 CONFIG_RANDSTRUCT_NONE=y
 CONFIG_RATIONAL=y
+# CONFIG_RAVE_SP_CORE is not set
 CONFIG_REALTEK_OTTO_TIMER=y
 CONFIG_REALTEK_OTTO_WDT=y
 CONFIG_REALTEK_PHY=y
@@ -221,6 +222,8 @@ CONFIG_REGMAP=y
 CONFIG_REGMAP_I2C=y
 CONFIG_REGMAP_MDIO=y
 CONFIG_REGMAP_MMIO=y
+CONFIG_REGULATOR=y
+CONFIG_REGULATOR_FIXED_VOLTAGE=y
 CONFIG_RESET_CONTROLLER=y
 CONFIG_RFS_ACCEL=y
 CONFIG_RPS=y
index 3708d692dc987d6c92553b040f677e862c409d89..64010a6bd2b577f04909ddf115acb3e5132e2c51 100644 (file)
@@ -221,6 +221,7 @@ CONFIG_QUEUED_RWLOCKS=y
 CONFIG_QUEUED_SPINLOCKS=y
 CONFIG_RANDSTRUCT_NONE=y
 CONFIG_RATIONAL=y
+# CONFIG_RAVE_SP_CORE is not set
 CONFIG_REALTEK_OTTO_TIMER=y
 CONFIG_REALTEK_OTTO_WDT=y
 CONFIG_REALTEK_PHY=y
@@ -231,6 +232,8 @@ CONFIG_REGMAP=y
 CONFIG_REGMAP_I2C=y
 CONFIG_REGMAP_MDIO=y
 CONFIG_REGMAP_MMIO=y
+CONFIG_REGULATOR=y
+CONFIG_REGULATOR_FIXED_VOLTAGE=y
 CONFIG_RESET_CONTROLLER=y
 CONFIG_RFS_ACCEL=y
 CONFIG_RPS=y
index 5f97d95869a85140c4fa9e60524b327a9a4f28b3..b34404d42fe073a2f8ac9ed1bb8758acc9a2b9f1 100644 (file)
@@ -217,6 +217,7 @@ CONFIG_QUEUED_RWLOCKS=y
 CONFIG_QUEUED_SPINLOCKS=y
 CONFIG_RANDSTRUCT_NONE=y
 CONFIG_RATIONAL=y
+# CONFIG_RAVE_SP_CORE is not set
 CONFIG_REALTEK_OTTO_TIMER=y
 CONFIG_REALTEK_OTTO_WDT=y
 CONFIG_REALTEK_PHY=y
@@ -226,6 +227,8 @@ CONFIG_REGMAP=y
 CONFIG_REGMAP_I2C=y
 CONFIG_REGMAP_MDIO=y
 CONFIG_REGMAP_MMIO=y
+CONFIG_REGULATOR=y
+CONFIG_REGULATOR_FIXED_VOLTAGE=y
 CONFIG_RESET_CONTROLLER=y
 CONFIG_RFS_ACCEL=y
 CONFIG_RPS=y
index 9483a0aeb13cb8e778b2e52e19a68f37a2baef93..01a40a04f56d8483cbbc790269e29e163ea44ef4 100644 (file)
@@ -228,6 +228,7 @@ CONFIG_QUEUED_RWLOCKS=y
 CONFIG_QUEUED_SPINLOCKS=y
 CONFIG_RANDSTRUCT_NONE=y
 CONFIG_RATIONAL=y
+# CONFIG_RAVE_SP_CORE is not set
 CONFIG_REALTEK_OTTO_TIMER=y
 CONFIG_REALTEK_OTTO_WDT=y
 CONFIG_REALTEK_PHY_MULTIPORT=y
@@ -235,6 +236,8 @@ CONFIG_REGMAP=y
 CONFIG_REGMAP_I2C=y
 CONFIG_REGMAP_MDIO=y
 CONFIG_REGMAP_MMIO=y
+CONFIG_REGULATOR=y
+CONFIG_REGULATOR_FIXED_VOLTAGE=y
 CONFIG_RESET_CONTROLLER=y
 CONFIG_RFS_ACCEL=y
 CONFIG_RPS=y