* [PATCH net-next 0/2] net: pse-pd: add Realtek/Broadcom PSE MCU support
@ 2026-06-08 20:57 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 ` [PATCH net-next 2/2] net: pse-pd: add Realtek/Broadcom PSE MCU driver Jonas Jelonek
0 siblings, 2 replies; 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
This series adds a PSE-PD driver for the microcontroller (MCU) that fronts
the PSE silicon on a range of managed switches, together with its DT
binding.
Hardware model
==============
These boards do not expose the PSE chips to the host directly. A small
microcontroller sits on an I2C/SMBus or UART bus and manages one or more PSE
chips behind it; the host CPU only ever talks to that MCU, using a fixed
12-byte request/response protocol with a trailing checksum. The PSE silicon
never appears on the bus.
The same protocol family is used by MCUs fronting Realtek PSE chips
(RTL8238B, RTL8239, RTL8239C) and Broadcom PSE chips (BCM59111, BCM59121),
diverging in opcode numbering and a few response layouts. The driver
abstracts that behind a per-dialect opcode table and parser hooks, selected
by the compatible. The specific PSE chip behind the MCU is detected at
runtime and only influences per-chip constants (power scaling and the
per-port cap).
Why the compatible names the protocol, not the chip
===================================================
The compatibles are "realtek,pse-mcu-rtk" and "realtek,pse-mcu-bcm". This is
a deliberate choice and the part most likely to raise questions, so the
reasoning up front.
The node names the protocol dialect, not a part:
- The DT node describes the MCU, not a PSE chip: the PSE chips are behind
the MCU and never appear on the bus, so naming the node after one (e.g.
"realtek,rtl8239") would describe hardware that isn't at that address.
- The PSE chips are, in principle, usable without this MCU (host-driven
directly) - different hardware with a different programming model that
would warrant its own binding. Claiming the PSE-chip compatibles here
would collide with that.
- Naming the MCU silicon is equally wrong: these are ordinary
general-purpose microcontrollers (GigaDevice, Nuvoton, ...) that vary
across boards and are not dedicated to this application.
- What is fixed, and all the driver needs at DT-parse time, is the
protocol dialect, so the compatible encodes exactly that. The two
dialects share one protocol family and one binding, kept in a single
"realtek" vendor namespace because this MCU front-end is found almost
exclusively on Realtek-based switches; a "-rtk"/"-bcm" suffix selects
the dialect. This follows the "google,cros-ec-*" pattern: a compatible
for a firmware/protocol interface implemented by varying
microcontrollers.
One compatible per dialect spans both transports:
- The 12-byte wire protocol is identical over I2C/SMBus and UART; only the
plumbing differs (SMBus vs native framing on I2C, baud rate on UART),
and the transport is already expressed structurally by the node's parent
bus (i2c@... vs serial@...). A "-i2c"/"-uart" suffix would only
duplicate that, for a protocol that does not change across transports.
- This is the multi-transport model used by e.g. "bosch,bmi160" (one
compatible, separate i2c and spi drivers binding it), rather than the
cros-ec model of per-transport compatibles - cros-ec splits because its
on-wire framing genuinely differs per bus, which is not the case here.
The binding documents both points as well.
Testing
=======
- Linksys LGS328MPCv2 (RTL8238B, I2C)
- Zyxel GS1900-10HP A1 (BCM59121, UART)
- Zyxel GS1900-10HP B1 (RTL8238B, UART)
- Zyxel XMG1915-10EP (RTL8239C, UART)
- Zyxel XS1930-12HP (RTL8239, SMBus)
---
Jonas Jelonek (2):
dt-bindings: net: pse-pd: add bindings for Realtek/Broadcom PSE MCU
net: pse-pd: add Realtek/Broadcom PSE MCU driver
.../bindings/net/pse-pd/realtek,pse-mcu.yaml | 154 +++
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 ++
8 files changed, 1575 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu.yaml
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
base-commit: 903db046d5579bef0ea699eae4b279dd6455fc9f
--
2.51.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH net-next 1/2] dt-bindings: net: pse-pd: add bindings for Realtek/Broadcom PSE MCU
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 ` Jonas Jelonek
2026-06-08 20:57 ` [PATCH net-next 2/2] net: pse-pd: add Realtek/Broadcom PSE MCU driver Jonas Jelonek
1 sibling, 0 replies; 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
Add a binding for the microcontroller (MCU) that fronts the PSE silicon
on a range of managed switches. The host talks only to the MCU, over
I2C/SMBus or UART, using a fixed message-based protocol; the PSE chips
behind it never appear on the bus.
The compatible identifies the PSE-MCU protocol dialect
(realtek,pse-mcu-rtk or realtek,pse-mcu-bcm), not a specific part: the
node describes the MCU - whose silicon is a general-purpose
microcontroller that varies across boards - and the 'realtek' vendor
prefix reflects the platform these MCUs are found on (Realtek-based PoE
switches), following the google,cros-ec-* pattern rather than naming the
MCU silicon. The '-rtk'/'-bcm' suffix selects the Realtek or Broadcom
dialect within that one family. The specific PSE chip is detected at
runtime and is not described here.
A single compatible per dialect covers both the I2C/SMBus and UART
attachments: the wire protocol is identical across them and the transport
is expressed by the node's parent bus, so it is not encoded in the
compatible.
Both dialects share one protocol family and one device tree contract, so
they are documented in a single binding under one vendor prefix. The
'realtek' prefix is used because this MCU front-end is found almost
exclusively on Realtek-based switches; the Broadcom dialect is expressed
as the realtek,pse-mcu-bcm compatible within the same family.
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
---
.../bindings/net/pse-pd/realtek,pse-mcu.yaml | 154 ++++++++++++++++++
1 file changed, 154 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu.yaml
diff --git a/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu.yaml b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu.yaml
new file mode 100644
index 000000000000..2fb729dcb41f
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu.yaml
@@ -0,0 +1,154 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/pse-pd/realtek,pse-mcu.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Realtek/Broadcom PSE MCU
+
+maintainers:
+ - Jonas Jelonek <jelonek.jonas@gmail.com>
+
+description: |
+ Microcontroller (MCU) that fronts the PSE hardware on switches using
+ Realtek (RTL8238B, RTL8239, RTL8239C) or Broadcom (BCM59111, BCM59121)
+ PSE chips. The MCU exposes a small message-based protocol over either
+ I2C/SMBus or UART; the actual PSE silicon is not accessed directly. The
+ Realtek and Broadcom variants share this device tree contract but use
+ different protocol opcodes, selected by the compatible.
+
+ The compatible identifies the PSE-MCU protocol dialect, not a specific
+ part. The device described here is the MCU, whose own silicon varies
+ across boards and is incidental to the protocol. The MCU is not
+ made by Realtek or Broadcom; the 'realtek' vendor prefix reflects the
+ platform these MCUs are found on (Realtek-based PoE switches) and the
+ '-rtk'/'-bcm' suffix selects the Realtek or Broadcom protocol dialect.
+ The specific PSE chip behind the MCU is not described in the device
+ tree either; it is detected at runtime by querying the MCU.
+
+ A single compatible per dialect covers both the I2C/SMBus and UART
+ attachments: the wire protocol is identical across them and the
+ transport is already expressed by the node's parent bus, so it is not
+ encoded in the compatible. Transport-specific properties differ
+ accordingly - the I2C attachment carries 'reg' (and, for Realtek,
+ 'realtek,i2c-protocol'), while the UART attachment carries the serial
+ peripheral properties such as 'current-speed'.
+
+properties:
+ compatible:
+ enum:
+ - realtek,pse-mcu-rtk
+ - realtek,pse-mcu-bcm
+
+ reg:
+ maxItems: 1
+
+ power-supply:
+ description: Regulator supplying the PoE power rail.
+
+ enable-gpios:
+ maxItems: 1
+
+ realtek,i2c-protocol:
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [ i2c, smbus ]
+ description: |
+ Wire framing the MCU firmware expects on the I2C bus. "smbus" means
+ reads carry a leading command byte (0x00) and a repeated start; "i2c"
+ means bare 12-byte writes and reads with no command prefix. Only
+ applies to the Realtek I2C attachment.
+
+required:
+ - compatible
+
+allOf:
+ - $ref: pse-controller.yaml#
+ - $ref: /schemas/serial/serial-peripheral-props.yaml#
+ # The I2C attachment (identified by 'reg') cannot carry serial bus props.
+ - if:
+ required: [reg]
+ then:
+ properties:
+ current-speed: false
+ max-speed: false
+ # 'realtek,i2c-protocol' is meaningful only for the Realtek I2C attachment;
+ # the Broadcom variant and any UART attachment must not carry it.
+ - if:
+ properties:
+ compatible:
+ contains:
+ const: realtek,pse-mcu-rtk
+ required: [reg]
+ then:
+ required:
+ - realtek,i2c-protocol
+ else:
+ properties:
+ "realtek,i2c-protocol": false
+
+unevaluatedProperties: false
+
+examples:
+ # Realtek PSE chip, I2C attachment (SMBus framing).
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet-pse@20 {
+ compatible = "realtek,pse-mcu-rtk";
+ reg = <0x20>;
+ realtek,i2c-protocol = "smbus";
+
+ pse-pis {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pse-pi@0 {
+ reg = <0>;
+ #pse-cells = <0>;
+ };
+ };
+ };
+ };
+
+ # Broadcom PSE chip, I2C attachment.
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet-pse@20 {
+ compatible = "realtek,pse-mcu-bcm";
+ reg = <0x20>;
+
+ pse-pis {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pse-pi@0 {
+ reg = <0>;
+ #pse-cells = <0>;
+ };
+ };
+ };
+ };
+
+ # Realtek PSE chip, UART attachment.
+ - |
+ serial {
+ ethernet-pse {
+ compatible = "realtek,pse-mcu-rtk";
+ current-speed = <115200>;
+
+ pse-pis {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pse-pi@0 {
+ reg = <0>;
+ #pse-cells = <0>;
+ };
+ };
+ };
+ };
--
2.51.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [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
* Re: [PATCH net-next 2/2] net: pse-pd: add Realtek/Broadcom PSE MCU driver
2026-06-08 20:57 ` [PATCH net-next 2/2] net: pse-pd: add Realtek/Broadcom PSE MCU driver Jonas Jelonek
@ 2026-06-09 20:58 ` sashiko-bot
0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-06-09 20:58 UTC (permalink / raw)
To: Jonas Jelonek; +Cc: robh, devicetree, conor+dt
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Unprotected data race on UART receive buffer state between asynchronous RX callback and process-context TX function.
- [High] I2C transport handlers swallow explicit MCU error opcodes, causing 1-second stalls and masking failures.
- [High] UART receive handler returns partially consumed bytes, leaving overflow bytes in the serdev tty buffer which corrupts subsequent frames.
- [High] The advertised 60W maximum power limit for the BCM59121 chip is physically unreachable and silently clamped to 51W.
- [Low] Use of non-existent macro `kzalloc_obj()` causes a build failure.
--
commit 99e0f1290d70d0585553ebd399054bea3d8ef281
Author: Jonas Jelonek <jelonek.jonas@gmail.com>
net: pse-pd: add Realtek/Broadcom PSE MCU driver
This commit introduces a new driver for the microcontroller fronting Realtek
and Broadcom PSE chips on PoE switches. It includes both I2C and UART
transport modules alongside a shared core to handle protocol dialects.
> diff --git a/drivers/net/pse-pd/realtek-pse-core.c b/drivers/net/pse-pd/realtek-pse-core.c
> index 0000000000000..711f85aebf5e8
> --- /dev/null
> +++ b/drivers/net/pse-pd/realtek-pse-core.c
[ ... ]
> +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,
> +};
[ ... ]
> +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;
[ ... ]
> + 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);
> +}
[Severity: High]
Is the 60W maximum power limit for the BCM59121 chip physically unreachable
here?
With max_mW_per_port set to 60000 and pw_set_lsb_mW set to 200, programming
60W results in a target of 300. Since this is packed into an 8-bit command
payload, the min_t calculation clamps the value to 0xff (255).
This seems to silently restrict the maximum output to 51W, which could break
functionality for devices expecting the advertised power budget.
[ ... ]
> +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;
[Severity: Low]
Will this code compile successfully? There does not appear to be a
kzalloc_obj() macro in the standard kernel API, which might lead to an
immediate build failure. Should this just be kzalloc() instead?
> diff --git a/drivers/net/pse-pd/realtek-pse-i2c.c b/drivers/net/pse-pd/realtek-pse-i2c.c
> index 0000000000000..8b9c31cbdfe99
> --- /dev/null
> +++ b/drivers/net/pse-pd/realtek-pse-i2c.c
[ ... ]
> +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;
> +}
[Severity: High]
Do these transport loops inadvertently swallow explicit MCU error opcodes?
If the MCU returns a valid error opcode like 0xfd (request incomplete) or
0xfe (checksum error), resp->opcode won't match req->opcode.
Instead of returning the error so the core can translate and handle it,
the loop appears to ignore the response and continues to poll until hitting
the timeout.
Could this needlessly stall the bus for a full second on every invalid
command in both the SMBus and native I2C receive paths?
> diff --git a/drivers/net/pse-pd/realtek-pse-uart.c b/drivers/net/pse-pd/realtek-pse-uart.c
> index 0000000000000..27fa7e0c3f95e
> --- /dev/null
> +++ b/drivers/net/pse-pd/realtek-pse-uart.c
[ ... ]
> +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;
> +}
[Severity: High]
Can returning take instead of count here cause framing desynchronization?
If the handler returns a value less than count, the serdev core keeps the
unconsumed bytes buffered.
When the next transaction begins, these leftover overflow bytes might
immediately push into the new buffer, permanently misaligning future
responses. Does it make sense to unconditionally return count here to drop
any overflow bytes and allow resynchronization?
[ ... ]
> +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;
[Severity: High]
Is there a data race on ctx->rx_len between the receive callback and this
send function?
The serdev callback rtpse_uart_receive() modifies ctx->rx_len asynchronously
from a softirq or workqueue context. If a delayed byte arrives just as
rtpse_uart_send() locklessly resets ctx->rx_len to 0, it could corrupt the
buffer state.
The pse->mutex appears to only serialize host-initiated requests, but not
the asynchronous UART receive callback. Could this lead to dropped frames or
out-of-bounds writes?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260608205758.1830521-1-jelonek.jonas@gmail.com?part=2
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-06-09 20:58 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH net-next 2/2] net: pse-pd: add Realtek/Broadcom PSE MCU driver Jonas Jelonek
2026-06-09 20:58 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox