* [PATCH net-next 2/2] net: pse-pd: add Realtek/Broadcom PSE MCU driver
2026-06-08 20:57 [PATCH net-next 0/2] net: pse-pd: add Realtek/Broadcom PSE MCU support Jonas Jelonek
2026-06-08 20:57 ` [PATCH net-next 1/2] dt-bindings: net: pse-pd: add bindings for Realtek/Broadcom PSE MCU Jonas Jelonek
@ 2026-06-08 20:57 ` Jonas Jelonek
2026-06-09 20:58 ` sashiko-bot
1 sibling, 1 reply; 4+ messages in thread
From: Jonas Jelonek @ 2026-06-08 20:57 UTC (permalink / raw)
To: Oleksij Rempel, Kory Maincent, Andrew Lunn, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Rob Herring,
Krzysztof Kozlowski, Conor Dooley
Cc: netdev, devicetree, linux-kernel, Daniel Golle, Bjørn Mork,
Jonas Jelonek
A range of 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. The same protocol family is spoken by Realtek and Broadcom PSE
MCUs, diverging in opcode numbering and a few response layouts, which the
driver abstracts 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 driver is split into a shared core and two transport modules:
- PSE_REALTEK: protocol, message framing, dialect machinery, and the
pse_controller_ops glue.
- PSE_REALTEK_I2C / PSE_REALTEK_UART: transport modules registering the
MCU on an I2C bus or a serdev port respectively.
The realtek-* file names and PSE_REALTEK* symbols reflect the platform
this setup appears on rather than vendor scope: this MCU front-end is
found almost exclusively on Realtek-based switches. Broadcom PSE MCUs
speak the same protocol family and are supported by the same shared core
through the dialect abstraction, so the realtek-* naming is kept rather
than splitting the code or renaming to a vendor-neutral scheme. For the
same reason the device tree compatibles live under one vendor prefix,
realtek,pse-mcu-rtk and realtek,pse-mcu-bcm, with the suffix selecting the
dialect.
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 | 28 +
drivers/net/pse-pd/Makefile | 3 +
drivers/net/pse-pd/realtek-pse-core.c | 1002 +++++++++++++++++++++++++
drivers/net/pse-pd/realtek-pse-i2c.c | 164 ++++
drivers/net/pse-pd/realtek-pse-uart.c | 147 ++++
drivers/net/pse-pd/realtek-pse.h | 70 ++
7 files changed, 1421 insertions(+)
create mode 100644 drivers/net/pse-pd/realtek-pse-core.c
create mode 100644 drivers/net/pse-pd/realtek-pse-i2c.c
create mode 100644 drivers/net/pse-pd/realtek-pse-uart.c
create mode 100644 drivers/net/pse-pd/realtek-pse.h
diff --git a/MAINTAINERS b/MAINTAINERS
index eb8cdcc76324..4adc929b7d01 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22484,6 +22484,13 @@ S: Maintained
F: include/sound/rt*.h
F: sound/soc/codecs/rt*
+REALTEK/BROADCOM 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.yaml
+F: drivers/net/pse-pd/realtek-pse*
+
REALTEK OTTO WATCHDOG
M: Sander Vanheule <sander@svanheule.net>
L: linux-watchdog@vger.kernel.org
diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index 7ef29657ee5d..b065b19db126 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
+ tristate
+ help
+ Shared core for the Realtek/Broadcom PSE MCU driver. This is
+ selected automatically by the transport options below.
+
+config PSE_REALTEK_I2C
+ tristate "Realtek/Broadcom PSE MCU driver (I2C transport)"
+ depends on I2C
+ select PSE_REALTEK
+ help
+ Driver for the microcontroller (MCU) that fronts the PSE
+ hardware on switches with Realtek or Broadcom PSE chips, 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-i2c.
+
+config PSE_REALTEK_UART
+ tristate "Realtek/Broadcom PSE MCU driver (UART transport)"
+ depends on SERIAL_DEV_BUS
+ select PSE_REALTEK
+ help
+ Driver for the microcontroller (MCU) that fronts the PSE
+ hardware on switches with Realtek or Broadcom PSE chips, 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-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..ad6ebf50fd56 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) += realtek-pse-core.o
+obj-$(CONFIG_PSE_REALTEK_I2C) += realtek-pse-i2c.o
+obj-$(CONFIG_PSE_REALTEK_UART) += realtek-pse-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-core.c b/drivers/net/pse-pd/realtek-pse-core.c
new file mode 100644
index 000000000000..711f85aebf5e
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-core.c
@@ -0,0 +1,1002 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for the microcontroller (MCU) fronting Realtek or Broadcom PSE
+ * chips. Both vendors' MCUs speak a closely related 12-byte fixed-frame
+ * management protocol; this driver covers both 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 (12-byte fixed-size frames; opcode
+ * + arg + 9 payload bytes + checksum). The PSE chips themselves are not
+ * accessed directly; everything goes through MCU commands.
+ *
+ * This driver targets that architecture for the Realtek-family protocol.
+ * Two dialects are supported: Realtek MCUs managing RTL823x/RTL8239* PSE
+ * chips, and Broadcom MCUs managing BCM590xx PSE chips. 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.
+ * Those, if and when they show up in the kernel, belong in separate
+ * drivers under drivers/net/pse-pd/.
+ *
+ * This core module implements the protocol, decoding/encoding of MCU
+ * responses, and the pse_controller_ops integration. Transport modules
+ * (realtek-pse-i2c, realtek-pse-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/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/unaligned.h>
+
+#include "realtek-pse.h"
+
+#define RTPSE_DEVICE_ID_RTL8238B 0x0138
+#define RTPSE_DEVICE_ID_RTL8239 0x0039
+#define RTPSE_DEVICE_ID_RTL8239C 0x0139
+#define RTPSE_DEVICE_ID_BCM59111 0xe111
+#define RTPSE_DEVICE_ID_BCM59121 0xe121
+
+#define RTPSE_PORT_STS_DISABLED 0x00
+#define RTPSE_PORT_STS_SEARCHING 0x01
+#define RTPSE_PORT_STS_DELIVERING 0x02
+#define RTPSE_PORT_STS_FAULT 0x04
+#define RTPSE_PORT_STS_REQUESTING 0x06
+
+/* RTPSE_PORT_SET_POWER_LIMIT_TYPE values */
+#define RTPSE_PORT_PW_LIMIT_TYPE_USER 0x02
+
+#define RTPSE_MAX_PORTS 48
+#define RTPSE_PORT_MAX_PRIORITY 3
+
+enum rtpse_cmd {
+ RTPSE_CMD_MCU_SET_GLOBAL_STATE,
+ RTPSE_CMD_MCU_GET_SYSTEM_INFO,
+ RTPSE_CMD_MCU_GET_EXT_CONFIG,
+
+ RTPSE_CMD_PORT_ENABLE,
+ RTPSE_CMD_PORT_SET_POWER_LIMIT_TYPE,
+ RTPSE_CMD_PORT_SET_POWER_LIMIT,
+ RTPSE_CMD_PORT_SET_POWER_LIMIT_EXT,
+ RTPSE_CMD_PORT_SET_PRIORITY,
+ RTPSE_CMD_PORT_GET_STATUS,
+ RTPSE_CMD_PORT_GET_POWER_STATS,
+ RTPSE_CMD_PORT_GET_CONFIG,
+ RTPSE_CMD_PORT_GET_EXT_CONFIG,
+
+ RTPSE_NUM_CMDS,
+};
+
+struct rtpse_opcode {
+ u8 op;
+ bool valid;
+};
+
+/* Shorthand for the designated-initializer entries in dialect opcode tables. */
+#define RTPSE_OP(opc) { .op = (opc), .valid = true }
+
+/* Forward-declared so dialects can supply response parsers (defined below). */
+struct rtpse_mcu_info;
+struct rtpse_port_status;
+
+struct rtpse_mcu_dialect {
+ struct rtpse_opcode opcode[RTPSE_NUM_CMDS];
+
+ /*
+ * Response parsers. Each dialect must supply its own; the core calls
+ * these unconditionally rather than carrying a default that would
+ * silently mis-decode bytes from a dialect that forgot to set them.
+ */
+ int (*parse_system_info)(const u8 *payload, struct rtpse_mcu_info *info);
+ int (*parse_port_class)(const struct rtpse_port_status *status);
+ const char *(*mcu_type_str)(unsigned int mcu_type);
+};
+
+/*
+ * Per-compatible match data: selected by the DT/I2C compatible, it bundles
+ * the protocol dialect with attachment quirks that the exact MCU silicon
+ * does not determine (only its firmware protocol and the host bus do).
+ */
+struct rtpse_match_data {
+ const struct rtpse_mcu_dialect *dialect;
+ /* I2C framing must come from DT (realtek,i2c-protocol); else SMBus. */
+ bool i2c_proto_dt_required;
+};
+
+struct rtpse_chip_info {
+ const char *name;
+ u16 device_id;
+ u32 max_mW_per_port;
+ enum rtpse_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 */
+};
+
+/* 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_port_status {
+ u8 sts1;
+ u8 sts2;
+ u8 sts3;
+};
+
+struct rtpse_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_port_config {
+ bool enable;
+ u8 function_mode;
+ u8 detection_type;
+ u8 cls_type;
+ u8 disconnect_type;
+ u8 pair_type;
+};
+
+struct rtpse_port_ext_config {
+ u8 inrush_mode;
+ u8 limit_type;
+ u8 max_power;
+ u8 priority;
+ u8 chip_addr;
+ u8 channel;
+};
+
+static const struct rtpse_chip_info rtl8238b_info = {
+ .device_id = RTPSE_DEVICE_ID_RTL8238B,
+ .max_mW_per_port = 30000,
+ .name = "RTL8238B",
+ .pw_read_lsb_mW = 200,
+ .pw_set_cmd = RTPSE_CMD_PORT_SET_POWER_LIMIT,
+ .pw_set_lsb_mW = 200,
+};
+
+static const struct rtpse_chip_info rtl8239_info = {
+ .device_id = RTPSE_DEVICE_ID_RTL8239,
+ .max_mW_per_port = 90000,
+ .name = "RTL8239",
+ .pw_read_lsb_mW = 400,
+ .pw_set_cmd = RTPSE_CMD_PORT_SET_POWER_LIMIT_EXT,
+ .pw_set_lsb_mW = 400,
+};
+
+static const struct rtpse_chip_info rtl8239c_info = {
+ .device_id = RTPSE_DEVICE_ID_RTL8239C,
+ .max_mW_per_port = 90000,
+ .name = "RTL8239C",
+ .pw_read_lsb_mW = 400,
+ .pw_set_cmd = RTPSE_CMD_PORT_SET_POWER_LIMIT_EXT,
+ .pw_set_lsb_mW = 400,
+};
+
+static const struct rtpse_chip_info bcm59111_info = {
+ .device_id = RTPSE_DEVICE_ID_BCM59111,
+ .max_mW_per_port = 30000,
+ .name = "BCM59111",
+ .pw_read_lsb_mW = 200,
+ .pw_set_cmd = RTPSE_CMD_PORT_SET_POWER_LIMIT,
+ .pw_set_lsb_mW = 200,
+};
+
+static const struct rtpse_chip_info bcm59121_info = {
+ .device_id = RTPSE_DEVICE_ID_BCM59121,
+ .max_mW_per_port = 60000, /* 802.3bt Type 3 */
+ .name = "BCM59121",
+ .pw_read_lsb_mW = 200,
+ .pw_set_cmd = RTPSE_CMD_PORT_SET_POWER_LIMIT,
+ .pw_set_lsb_mW = 200,
+};
+
+/* Helpers and basic functions */
+
+static inline struct rtpse_ctrl *to_rtpse_ctrl(struct pse_controller_dev *pcdev)
+{
+ return container_of(pcdev, struct rtpse_ctrl, pcdev);
+}
+
+bool rtpse_needs_i2c_proto(const struct rtpse_match_data *match)
+{
+ return match->i2c_proto_dt_required;
+}
+EXPORT_SYMBOL_GPL(rtpse_needs_i2c_proto);
+
+static inline void rtpse_mcu_msg_init(struct rtpse_mcu_msg *msg, u8 opcode)
+{
+ memset(msg, 0xff, sizeof(*msg));
+ msg->opcode = opcode;
+}
+
+static u8 rtpse_checksum(const u8 *buf, size_t len)
+{
+ u8 sum = 0;
+
+ while (len--)
+ sum += *buf++;
+ return sum;
+}
+
+static int rtpse_do_xfer(struct rtpse_ctrl *pse, struct rtpse_mcu_msg *req,
+ struct rtpse_mcu_msg *resp)
+{
+ int ret;
+
+ req->checksum = rtpse_checksum((u8 *)req, RTPSE_MCU_MSG_SIZE - 1);
+
+ scoped_guard(mutex, &pse->mutex) {
+ 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 BCM dialect; harmless
+ * to check for RTL too). Catch these before the generic opcode/CRC
+ * mismatch path so callers see a meaningful errno.
+ */
+ switch (resp->opcode) {
+ case 0xfd: return -EBADE; /* request incomplete */
+ case 0xfe: return -EBADMSG; /* MCU-reported checksum error */
+ case 0xff: return -EAGAIN; /* MCU not ready */
+ }
+
+ if (resp->opcode != req->opcode ||
+ resp->checksum != rtpse_checksum((u8 *)resp, RTPSE_MCU_MSG_SIZE - 1))
+ return -EBADMSG;
+
+ return 0;
+}
+
+static int rtpse_port_query(struct rtpse_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_do_xfer(pse, &req, resp);
+ if (ret)
+ return ret;
+
+ if (resp->payload[0] != port)
+ return -EIO;
+
+ return 0;
+}
+
+static int rtpse_port_cmd(struct rtpse_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_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_ctrl *pse, struct rtpse_mcu_info *info)
+{
+ struct rtpse_mcu_msg req, resp;
+ const struct rtpse_opcode *opc;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_MCU_GET_SYSTEM_INFO];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ rtpse_mcu_msg_init(&req, opc->op);
+ ret = rtpse_do_xfer(pse, &req, &resp);
+ if (ret)
+ return ret;
+
+ return pse->dialect->parse_system_info(resp.payload, info);
+}
+
+static int rtpse_mcu_get_ext_config(struct rtpse_ctrl *pse, struct rtpse_mcu_ext_config *config)
+{
+ struct rtpse_mcu_msg req, resp;
+ const struct rtpse_opcode *opc;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_MCU_GET_EXT_CONFIG];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ rtpse_mcu_msg_init(&req, opc->op);
+ ret = rtpse_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_set_global_state(struct rtpse_ctrl *pse, bool enable)
+{
+ struct rtpse_mcu_msg req, resp;
+ const struct rtpse_opcode *opc;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_MCU_SET_GLOBAL_STATE];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ rtpse_mcu_msg_init(&req, opc->op);
+ req.payload[0] = enable ? 0x1 : 0x0;
+
+ ret = rtpse_do_xfer(pse, &req, &resp);
+ if (ret)
+ return ret;
+
+ return (resp.payload[0] == 0x0) ? 0 : -EIO;
+}
+
+/* Port operations */
+
+static int rtpse_port_get_status(struct rtpse_ctrl *pse, unsigned int port,
+ struct rtpse_port_status *status)
+{
+ const struct rtpse_opcode *opc;
+ struct rtpse_mcu_msg resp;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_PORT_GET_STATUS];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ ret = rtpse_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_port_get_measurement(struct rtpse_ctrl *pse, unsigned int port,
+ struct rtpse_port_measurement *measurement)
+{
+ const struct rtpse_opcode *opc;
+ struct rtpse_mcu_msg resp;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_PORT_GET_POWER_STATS];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ ret = rtpse_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_port_get_config(struct rtpse_ctrl *pse, unsigned int port,
+ struct rtpse_port_config *config)
+{
+ const struct rtpse_opcode *opc;
+ struct rtpse_mcu_msg resp;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_PORT_GET_CONFIG];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ ret = rtpse_port_query(pse, port, opc->op, &resp);
+ if (ret)
+ return ret;
+
+ config->enable = (resp.payload[1] == 1);
+ config->function_mode = resp.payload[2];
+ config->detection_type = resp.payload[3];
+ config->cls_type = resp.payload[4];
+ config->disconnect_type = resp.payload[5];
+ config->pair_type = resp.payload[6];
+
+ return 0;
+}
+
+static int rtpse_port_get_ext_config(struct rtpse_ctrl *pse, unsigned int port,
+ struct rtpse_port_ext_config *config)
+{
+ const struct rtpse_opcode *opc;
+ struct rtpse_mcu_msg resp;
+ int ret;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_PORT_GET_EXT_CONFIG];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ ret = rtpse_port_query(pse, port, opc->op, &resp);
+ if (ret)
+ return ret;
+
+ config->inrush_mode = resp.payload[1];
+ config->limit_type = resp.payload[2];
+ config->max_power = resp.payload[3];
+ config->priority = resp.payload[4];
+ config->chip_addr = resp.payload[5];
+ config->channel = resp.payload[6];
+
+ return 0;
+}
+
+static int rtpse_port_set_state(struct rtpse_ctrl *pse, unsigned int port, bool enable)
+{
+ const struct rtpse_opcode *opc;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_PORT_ENABLE];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ return rtpse_port_cmd(pse, port, opc->op, enable ? 0x1 : 0x0);
+}
+
+/*
+ * PSE controller ops
+ *
+ * @id is the PSE PI index from DT, used directly as the MCU port number.
+ * This assumes 0-based, contiguous MCU port addressing. Boards whose PSE
+ * outputs are wired to the chip at an offset would need a PI-index ->
+ * MCU-port mapping here.
+ */
+
+static int rtpse_port_get_admin_state(struct pse_controller_dev *pcdev, int id,
+ struct pse_admin_state *admin_state)
+{
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ struct rtpse_port_config config;
+ int ret;
+
+ ret = rtpse_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_port_get_pw_status(struct pse_controller_dev *pcdev, int id,
+ struct pse_pw_status *pw_status)
+{
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ struct rtpse_port_status status;
+ int ret;
+
+ ret = rtpse_port_get_status(pse, id, &status);
+ if (ret)
+ return ret;
+
+ switch (status.sts1) {
+ case RTPSE_PORT_STS_DISABLED:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
+ break;
+ case RTPSE_PORT_STS_SEARCHING:
+ case RTPSE_PORT_STS_REQUESTING:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
+ break;
+ case RTPSE_PORT_STS_DELIVERING:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
+ break;
+ case RTPSE_PORT_STS_FAULT:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;
+ break;
+ default:
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_UNKNOWN;
+ break;
+ }
+
+ return 0;
+}
+
+static int rtpse_port_get_pw_class(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ struct rtpse_port_status status;
+ int ret;
+
+ ret = rtpse_port_get_status(pse, id, &status);
+ if (ret)
+ return ret;
+
+ /*
+ * sts2 carries detection+classification only when sts1 is not a
+ * fault state; in fault states it encodes the fault type instead.
+ * Treat the two reserved sts1 codes (0x3, 0x5) as faults too, since
+ * the datasheet hints at "other fault" beyond the explicit 0x4.
+ */
+ switch (status.sts1) {
+ case RTPSE_PORT_STS_DISABLED:
+ case RTPSE_PORT_STS_SEARCHING:
+ case RTPSE_PORT_STS_DELIVERING:
+ case RTPSE_PORT_STS_REQUESTING:
+ return pse->dialect->parse_port_class(&status);
+ default:
+ return 0;
+ }
+}
+
+static int rtpse_port_get_actual_pw(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ struct rtpse_port_measurement measurement;
+ int ret;
+
+ ret = rtpse_port_get_measurement(pse, id, &measurement);
+ if (ret)
+ return ret;
+
+ /* 100mW per LSB */
+ return measurement.power_raw * 100U;
+}
+
+static int rtpse_port_get_voltage(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ struct rtpse_port_measurement measurement;
+ int ret;
+ u32 uV;
+
+ ret = rtpse_port_get_measurement(pse, id, &measurement);
+ if (ret)
+ return ret;
+
+ /* 64.45mV per LSB */
+ uV = (u32)measurement.voltage_raw * 64450U;
+ return min_t(u32, uV, INT_MAX);
+}
+
+static int rtpse_port_enable(struct pse_controller_dev *pcdev, int id)
+{
+ return rtpse_port_set_state(to_rtpse_ctrl(pcdev), id, true);
+}
+
+static int rtpse_port_disable(struct pse_controller_dev *pcdev, int id)
+{
+ return rtpse_port_set_state(to_rtpse_ctrl(pcdev), id, false);
+}
+
+static int rtpse_port_get_pw_limit(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ struct rtpse_port_ext_config config;
+ int ret;
+
+ ret = rtpse_port_get_ext_config(pse, id, &config);
+ if (ret)
+ return ret;
+
+ return config.max_power * pse->chip->pw_read_lsb_mW;
+}
+
+static int rtpse_port_set_pw_limit(struct pse_controller_dev *pcdev, int id, int max_mW)
+{
+ const struct rtpse_opcode *type_opc, *val_opc;
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ const struct rtpse_chip_info *chip = pse->chip;
+ unsigned int prg_val;
+ int ret;
+
+ if (max_mW < 0 || max_mW > chip->max_mW_per_port)
+ return -ERANGE;
+
+ type_opc = &pse->dialect->opcode[RTPSE_CMD_PORT_SET_POWER_LIMIT_TYPE];
+ val_opc = &pse->dialect->opcode[chip->pw_set_cmd];
+ if (!type_opc->valid || !val_opc->valid)
+ 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_port_cmd(pse, id, type_opc->op, RTPSE_PORT_PW_LIMIT_TYPE_USER);
+ if (ret)
+ return ret;
+
+ prg_val = min_t(unsigned int, max_mW / chip->pw_set_lsb_mW, 0xff);
+
+ return rtpse_port_cmd(pse, id, val_opc->op, prg_val);
+}
+
+static int rtpse_port_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
+ struct pse_pw_limit_ranges *out)
+{
+ struct ethtool_c33_pse_pw_limit_range *range;
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+
+ 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_port_get_prio(struct pse_controller_dev *pcdev, int id)
+{
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ struct rtpse_port_ext_config config;
+ int ret;
+
+ ret = rtpse_port_get_ext_config(pse, id, &config);
+ if (ret)
+ return ret;
+
+ return config.priority;
+}
+
+static int rtpse_port_set_prio(struct pse_controller_dev *pcdev, int id, unsigned int prio)
+{
+ struct rtpse_ctrl *pse = to_rtpse_ctrl(pcdev);
+ const struct rtpse_opcode *opc;
+
+ if (prio > RTPSE_PORT_MAX_PRIORITY)
+ return -ERANGE;
+
+ opc = &pse->dialect->opcode[RTPSE_CMD_PORT_SET_PRIORITY];
+ if (!opc->valid)
+ return -EOPNOTSUPP;
+
+ return rtpse_port_cmd(pse, id, opc->op, prio);
+}
+
+static const struct pse_controller_ops rtpse_ops = {
+ .pi_get_admin_state = rtpse_port_get_admin_state,
+ .pi_get_pw_status = rtpse_port_get_pw_status,
+ .pi_get_pw_class = rtpse_port_get_pw_class,
+ .pi_get_actual_pw = rtpse_port_get_actual_pw,
+ .pi_enable = rtpse_port_enable,
+ .pi_disable = rtpse_port_disable,
+ .pi_get_voltage = rtpse_port_get_voltage,
+ .pi_get_pw_limit = rtpse_port_get_pw_limit,
+ .pi_set_pw_limit = rtpse_port_set_pw_limit,
+ .pi_get_pw_limit_ranges = rtpse_port_get_pw_limit_ranges,
+ .pi_get_prio = rtpse_port_get_prio,
+ .pi_set_prio = rtpse_port_set_prio,
+};
+
+static int rtpse_discover(struct rtpse_ctrl *pse, struct rtpse_mcu_info *info)
+{
+ struct rtpse_mcu_ext_config ext_config;
+ unsigned long deadline;
+ int ret;
+
+ /*
+ * The MCU may not answer on the bus yet right after power-up or
+ * enable-gpios assertion: depending on the transport it either stays
+ * silent (-ETIMEDOUT) or does not ACK its address at all (-ENXIO /
+ * -EREMOTEIO). Retry within a bounded wall-time window so a slow boot
+ * still probes, while a genuinely unresponsive MCU fails with its real
+ * error instead of deferring forever and masking it.
+ */
+ 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)
+ 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_DEVICE_ID_RTL8238B:
+ pse->chip = &rtl8238b_info;
+ break;
+ case RTPSE_DEVICE_ID_RTL8239:
+ pse->chip = &rtl8239_info;
+ break;
+ case RTPSE_DEVICE_ID_RTL8239C:
+ pse->chip = &rtl8239c_info;
+ break;
+ case RTPSE_DEVICE_ID_BCM59111:
+ pse->chip = &bcm59111_info;
+ break;
+ case RTPSE_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_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_regulator_disable(void *data)
+{
+ regulator_disable(data);
+}
+
+int rtpse_register(struct rtpse_ctrl *pse)
+{
+ const struct rtpse_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_discover(pse, &info);
+ if (ret)
+ return ret;
+
+ if (!info.system_enable) {
+ ret = rtpse_set_global_state(pse, true);
+ /* Dialects without a global-state concept (e.g. BCM) 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");
+ }
+
+ 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_regulator_disable, pse->poe_supply);
+ 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.
+ */
+
+ pse->pcdev.owner = THIS_MODULE;
+ pse->pcdev.ops = &rtpse_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_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_register);
+
+static int rtpse_rtl_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];
+ return 0;
+}
+
+static int rtpse_rtl_parse_port_class(const struct rtpse_port_status *status)
+{
+ /* Class lives in the upper nibble of sts2. */
+ return FIELD_GET(GENMASK(7, 4), status->sts2);
+}
+
+static const char *rtpse_rtl_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 int rtpse_bcm_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
+{
+ info->max_ports = payload[1];
+ /* BCM 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];
+ return 0;
+}
+
+static int rtpse_bcm_parse_port_class(const struct rtpse_port_status *status)
+{
+ /* BCM 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_bcm_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_dialect_rtk = {
+ .parse_system_info = rtpse_rtl_parse_system_info,
+ .parse_port_class = rtpse_rtl_parse_port_class,
+ .mcu_type_str = rtpse_rtl_mcu_type_str,
+ .opcode = {
+ [RTPSE_CMD_MCU_SET_GLOBAL_STATE] = RTPSE_OP(0x00),
+ [RTPSE_CMD_MCU_GET_SYSTEM_INFO] = RTPSE_OP(0x40),
+ [RTPSE_CMD_MCU_GET_EXT_CONFIG] = RTPSE_OP(0x4a),
+
+ [RTPSE_CMD_PORT_ENABLE] = RTPSE_OP(0x01),
+ [RTPSE_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_OP(0x12),
+ [RTPSE_CMD_PORT_SET_POWER_LIMIT] = RTPSE_OP(0x13),
+ [RTPSE_CMD_PORT_SET_POWER_LIMIT_EXT] = RTPSE_OP(0x14),
+ [RTPSE_CMD_PORT_SET_PRIORITY] = RTPSE_OP(0x15),
+ [RTPSE_CMD_PORT_GET_STATUS] = RTPSE_OP(0x42),
+ [RTPSE_CMD_PORT_GET_POWER_STATS] = RTPSE_OP(0x44),
+ [RTPSE_CMD_PORT_GET_CONFIG] = RTPSE_OP(0x48),
+ [RTPSE_CMD_PORT_GET_EXT_CONFIG] = RTPSE_OP(0x49),
+ },
+};
+
+static const struct rtpse_mcu_dialect rtpse_dialect_bcm = {
+ .parse_system_info = rtpse_bcm_parse_system_info,
+ .parse_port_class = rtpse_bcm_parse_port_class,
+ .mcu_type_str = rtpse_bcm_mcu_type_str,
+ .opcode = {
+ [RTPSE_CMD_MCU_GET_SYSTEM_INFO] = RTPSE_OP(0x20),
+ [RTPSE_CMD_MCU_GET_EXT_CONFIG] = RTPSE_OP(0x2b),
+
+ [RTPSE_CMD_PORT_ENABLE] = RTPSE_OP(0x00),
+ [RTPSE_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_OP(0x15),
+ [RTPSE_CMD_PORT_SET_POWER_LIMIT] = RTPSE_OP(0x16),
+ [RTPSE_CMD_PORT_SET_PRIORITY] = RTPSE_OP(0x1a),
+ [RTPSE_CMD_PORT_GET_STATUS] = RTPSE_OP(0x21),
+ [RTPSE_CMD_PORT_GET_POWER_STATS] = RTPSE_OP(0x30),
+ [RTPSE_CMD_PORT_GET_CONFIG] = RTPSE_OP(0x25),
+ [RTPSE_CMD_PORT_GET_EXT_CONFIG] = RTPSE_OP(0x26),
+ },
+};
+
+const struct rtpse_match_data rtpse_rtk_data = {
+ .dialect = &rtpse_dialect_rtk,
+ .i2c_proto_dt_required = true,
+};
+EXPORT_SYMBOL_GPL(rtpse_rtk_data);
+
+const struct rtpse_match_data rtpse_bcm_data = {
+ .dialect = &rtpse_dialect_bcm,
+};
+EXPORT_SYMBOL_GPL(rtpse_bcm_data);
+
+MODULE_DESCRIPTION("Realtek/Broadcom PSE MCU driver (core)");
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/pse-pd/realtek-pse-i2c.c b/drivers/net/pse-pd/realtek-pse-i2c.c
new file mode 100644
index 000000000000..8b9c31cbdfe9
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-i2c.c
@@ -0,0 +1,164 @@
+// 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 "realtek-pse.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_I2C_RETRY_MS RTPSE_MCU_RESPONSE_MS
+#define RTPSE_I2C_MAX_TRIES (RTPSE_MCU_RESPONSE_MAX_MS / RTPSE_I2C_RETRY_MS)
+
+static int rtpse_i2c_smbus_send(struct rtpse_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_i2c_smbus_recv(struct rtpse_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_I2C_MAX_TRIES; tries++) {
+ if (tries > 0)
+ msleep(RTPSE_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 && resp->opcode == req->opcode)
+ return 0;
+ }
+
+ return -ETIMEDOUT;
+}
+
+static const struct rtpse_transport_ops rtpse_i2c_smbus_ops = {
+ .send = rtpse_i2c_smbus_send,
+ .recv = rtpse_i2c_smbus_recv,
+};
+
+static int rtpse_i2c_native_send(struct rtpse_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+ struct i2c_client *client = to_i2c_client(pse->dev);
+ int ret;
+
+ ret = i2c_master_send(client, (const u8 *)req, RTPSE_MCU_MSG_SIZE);
+ if (ret < 0)
+ return ret;
+ return ret == RTPSE_MCU_MSG_SIZE ? 0 : -EIO;
+}
+
+static int rtpse_i2c_native_recv(struct rtpse_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_I2C_MAX_TRIES; tries++) {
+ if (tries > 0)
+ msleep(RTPSE_I2C_RETRY_MS);
+
+ ret = i2c_master_recv(client, (u8 *)resp, RTPSE_MCU_MSG_SIZE);
+ if (ret < 0)
+ return ret;
+ if (ret == RTPSE_MCU_MSG_SIZE && resp->opcode == req->opcode)
+ return 0;
+ }
+
+ return -ETIMEDOUT;
+}
+
+static const struct rtpse_transport_ops rtpse_i2c_native_ops = {
+ .send = rtpse_i2c_native_send,
+ .recv = rtpse_i2c_native_recv,
+};
+
+static int rtpse_i2c_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ const struct rtpse_match_data *match;
+ struct rtpse_ctrl *pse;
+ bool use_native = false;
+ int ret;
+
+ match = device_get_match_data(dev);
+ if (!match)
+ return dev_err_probe(dev, -ENODEV, "missing match data\n");
+
+ if (rtpse_needs_i2c_proto(match)) {
+ const char *proto;
+
+ ret = device_property_read_string(dev, "realtek,i2c-protocol", &proto);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "missing required \"realtek,i2c-protocol\" property\n");
+
+ if (!strcmp(proto, "i2c"))
+ use_native = true;
+ else if (!strcmp(proto, "smbus"))
+ use_native = false;
+ else
+ return dev_err_probe(dev, -EINVAL,
+ "unknown realtek,i2c-protocol \"%s\"\n", proto);
+ }
+
+ 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->transport = use_native ? &rtpse_i2c_native_ops : &rtpse_i2c_smbus_ops;
+
+ return rtpse_register(pse);
+}
+
+static const struct of_device_id rtpse_i2c_of_match[] = {
+ { .compatible = "realtek,pse-mcu-rtk", .data = &rtpse_rtk_data },
+ { .compatible = "realtek,pse-mcu-bcm", .data = &rtpse_bcm_data },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rtpse_i2c_of_match);
+
+static struct i2c_driver rtpse_i2c_driver = {
+ .driver = {
+ .name = "realtek-pse-i2c",
+ .of_match_table = rtpse_i2c_of_match,
+ },
+ .probe = rtpse_i2c_probe,
+};
+module_i2c_driver(rtpse_i2c_driver);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek/Broadcom PSE MCU driver (I2C transport)");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/pse-pd/realtek-pse-uart.c b/drivers/net/pse-pd/realtek-pse-uart.c
new file mode 100644
index 000000000000..27fa7e0c3f95
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-uart.c
@@ -0,0 +1,147 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/serdev.h>
+#include <linux/string.h>
+
+#include "realtek-pse.h"
+
+#define RTPSE_UART_BAUD_DEFAULT 19200
+#define RTPSE_UART_TX_TIMEOUT msecs_to_jiffies(100)
+#define RTPSE_UART_RX_TIMEOUT msecs_to_jiffies(RTPSE_MCU_RESPONSE_MAX_MS)
+
+struct rtpse_uart {
+ struct rtpse_ctrl pse;
+ struct serdev_device *serdev;
+ struct completion rx_done;
+ size_t rx_len;
+ u8 rx_buf[RTPSE_MCU_MSG_SIZE];
+};
+
+#define to_rtpse_uart(p) container_of(p, struct rtpse_uart, pse)
+
+/*
+ * No frame alignment is performed here: a stray byte arriving during
+ * transmission, or a truncated/extra-byte frame, will misalign the next
+ * response. The misaligned frame then fails opcode/checksum validation
+ * in the core (-EBADMSG); the following _send resets rx_len and
+ * resyncs. Net cost is one lost transaction per glitch.
+ */
+static size_t rtpse_uart_receive(struct serdev_device *serdev,
+ const u8 *buf, size_t count)
+{
+ struct rtpse_uart *ctx = serdev_device_get_drvdata(serdev);
+ size_t take;
+
+ take = min(count, sizeof(ctx->rx_buf) - ctx->rx_len);
+ if (take == 0)
+ return count; /* drop overflow bytes */
+
+ 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);
+
+ return take;
+}
+
+static const struct serdev_device_ops rtpse_uart_serdev_ops = {
+ .receive_buf = rtpse_uart_receive,
+ .write_wakeup = serdev_device_write_wakeup,
+};
+
+static int rtpse_uart_send(struct rtpse_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+ struct rtpse_uart *ctx = to_rtpse_uart(pse);
+ int written;
+
+ /* clear any leftover rx state before transmitting */
+ reinit_completion(&ctx->rx_done);
+ ctx->rx_len = 0;
+
+ written = serdev_device_write(ctx->serdev, (const u8 *)req, sizeof(*req),
+ RTPSE_UART_TX_TIMEOUT);
+ if (written < 0)
+ return written;
+ if (written != sizeof(*req))
+ return -EIO;
+
+ return 0;
+}
+
+static int rtpse_uart_recv(struct rtpse_ctrl *pse,
+ const struct rtpse_mcu_msg *req,
+ struct rtpse_mcu_msg *resp)
+{
+ struct rtpse_uart *ctx = to_rtpse_uart(pse);
+
+ if (!wait_for_completion_timeout(&ctx->rx_done, RTPSE_UART_RX_TIMEOUT))
+ return -ETIMEDOUT;
+
+ if (ctx->rx_len != sizeof(*resp))
+ return -EIO;
+
+ memcpy(resp, ctx->rx_buf, sizeof(*resp));
+ return 0;
+}
+
+static const struct rtpse_transport_ops rtpse_uart_transport_ops = {
+ .send = rtpse_uart_send,
+ .recv = rtpse_uart_recv,
+};
+
+static int rtpse_uart_probe(struct serdev_device *serdev)
+{
+ u32 speed = RTPSE_UART_BAUD_DEFAULT;
+ struct device *dev = &serdev->dev;
+ struct rtpse_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.transport = &rtpse_uart_transport_ops;
+ init_completion(&ctx->rx_done);
+
+ serdev_device_set_drvdata(serdev, ctx);
+ serdev_device_set_client_ops(serdev, &rtpse_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_register(&ctx->pse);
+}
+
+static const struct of_device_id rtpse_uart_of_match[] = {
+ { .compatible = "realtek,pse-mcu-rtk", .data = &rtpse_rtk_data },
+ { .compatible = "realtek,pse-mcu-bcm", .data = &rtpse_bcm_data },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rtpse_uart_of_match);
+
+static struct serdev_device_driver rtpse_uart_driver = {
+ .driver = {
+ .name = "realtek-pse-uart",
+ .of_match_table = rtpse_uart_of_match,
+ },
+ .probe = rtpse_uart_probe,
+};
+module_serdev_device_driver(rtpse_uart_driver);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek/Broadcom PSE MCU driver (UART transport)");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/pse-pd/realtek-pse.h b/drivers/net/pse-pd/realtek-pse.h
new file mode 100644
index 000000000000..812e62b1752b
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse.h
@@ -0,0 +1,70 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef _REALTEK_PSE_H
+#define _REALTEK_PSE_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;
+
+/* Opaque to transports; defined in realtek-pse-core.c. */
+struct rtpse_mcu_dialect;
+struct rtpse_match_data;
+struct rtpse_chip_info;
+struct rtpse_ctrl;
+
+struct rtpse_transport_ops {
+ int (*send)(struct rtpse_ctrl *pse, const struct rtpse_mcu_msg *req);
+ int (*recv)(struct rtpse_ctrl *pse, const struct rtpse_mcu_msg *req,
+ struct rtpse_mcu_msg *resp);
+};
+
+struct rtpse_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_chip_info *chip;
+ const struct rtpse_transport_ops *transport;
+
+ struct regulator *poe_supply;
+};
+
+int rtpse_register(struct rtpse_ctrl *pse);
+
+/* Whether the I2C transport must read "realtek,i2c-protocol" from DT. */
+bool rtpse_needs_i2c_proto(const struct rtpse_match_data *match);
+
+extern const struct rtpse_match_data rtpse_rtk_data;
+extern const struct rtpse_match_data rtpse_bcm_data;
+
+#endif
--
2.51.0
^ permalink raw reply related [flat|nested] 4+ messages in thread