Netdev List
 help / color / mirror / Atom feed
* Re: [PATCH 02/13] m68k/coldfire: replace linux/gpio.h inclusions
From: Greg Ungerer @ 2026-07-06 11:22 UTC (permalink / raw)
  To: Arnd Bergmann, linux-gpio
  Cc: Arnd Bergmann, Bartosz Golaszewski, Andrew Lunn,
	Sebastian Hesselbarth, Gregory Clement, Frank Li, Robert Jarzmik,
	Krzysztof Kozlowski, Thomas Bogendoerfer, Hauke Mehrtens,
	Rafał Miłecki, Yoshinori Sato,
	John Paul Adrian Glaubitz, Linus Walleij, Dmitry Torokhov,
	Jakub Kicinski, Paolo Abeni, Dominik Brodowski, linux-kernel,
	linux-arm-kernel, linux-samsung-soc, patches, linux-m68k,
	linux-mips, linux-sh, linux-input, linux-media, netdev,
	linux-sunxi, linux-phy, linux-rockchip, linux-sound
In-Reply-To: <20260629132633.1300009-3-arnd@kernel.org>

Hi Arnd,

On 29/6/26 23:26, Arnd Bergmann wrote:
> From: Arnd Bergmann <arnd@arndb.de>
> 
> linux/gpio.h should no longer be used, convert these instead to
> linux/gpio/legacy.h for coldfire.
> 
> Signed-off-by: Arnd Bergmann <arnd@arndb.de>

For the ColdFire changes, LGTM:

Reviewed-by: Greg Ungerer <gerg@linux-m68k.com>

Regards
Greg



> ---
>   arch/m68k/coldfire/device.c     | 2 +-
>   arch/m68k/include/asm/mcfgpio.h | 2 +-
>   2 files changed, 2 insertions(+), 2 deletions(-)
> 
> diff --git a/arch/m68k/coldfire/device.c b/arch/m68k/coldfire/device.c
> index 1420bae0964f..9a0258acd998 100644
> --- a/arch/m68k/coldfire/device.c
> +++ b/arch/m68k/coldfire/device.c
> @@ -12,7 +12,7 @@
>   #include <linux/init.h>
>   #include <linux/io.h>
>   #include <linux/spi/spi.h>
> -#include <linux/gpio.h>
> +#include <linux/gpio/legacy.h>
>   #include <linux/fec.h>
>   #include <linux/dmaengine.h>
>   #include <asm/traps.h>
> diff --git a/arch/m68k/include/asm/mcfgpio.h b/arch/m68k/include/asm/mcfgpio.h
> index 7103cfa4edb6..29726aa40eb6 100644
> --- a/arch/m68k/include/asm/mcfgpio.h
> +++ b/arch/m68k/include/asm/mcfgpio.h
> @@ -16,7 +16,7 @@ int __mcfgpio_request(unsigned gpio);
>   void __mcfgpio_free(unsigned gpio);
>   
>   #ifdef CONFIG_GPIOLIB
> -#include <linux/gpio.h>
> +#include <linux/gpio/legacy.h>
>   #else
>   
>   /* our alternate 'gpiolib' functions */


^ permalink raw reply

* Re: [PATCH net-next v4 2/2] net: pse-pd: add Realtek/Broadcom PSE MCU driver
From: Jonas Jelonek @ 2026-07-06 11:22 UTC (permalink / raw)
  To: Paolo Abeni
  Cc: netdev, devicetree, linux-kernel, Daniel Golle, Bjørn Mork,
	Conor Dooley, Krzysztof Kozlowski, David S . Miller, Andrew Lunn,
	Eric Dumazet, Jakub Kicinski, Rob Herring, Kory Maincent,
	Oleksij Rempel
In-Reply-To: <2ee45ab5-a329-4891-8326-ac8f14b6374a@redhat.com>

Hi Paolo,

On 03.07.26 09:55, Paolo Abeni wrote:
> On 6/30/26 12:56 PM, Jonas Jelonek wrote:
>> 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_MCU: protocol, message framing, dialect machinery, and the
>>   pse_controller_ops glue.
>> - PSE_REALTEK_MCU_I2C / PSE_REALTEK_MCU_UART: transport modules
>>   registering the MCU on an I2C bus or a serdev port respectively.
>>
>> The realtek-pse-mcu-* files and PSE_REALTEK_MCU* symbols match the
>> realtek,pse-mcu-rtk / realtek,pse-mcu-brcm compatibles: all name the
>> Realtek PSE-MCU front-end, not the MCU silicon or the PSE chip behind
>> it (see the binding for the prefix rationale). Broadcom PSE MCUs speak
>> the same protocol family and are handled by the same shared core
>> through the dialect abstraction selected by the '-brcm' compatible.
>>
>> Power budgeting is left to the MCU firmware; the driver advertises
>> PSE_BUDGET_EVAL_STRAT_DYNAMIC (controller-managed budget) accordingly.
>>
>> Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
>> ---
>>  MAINTAINERS                               |    7 +
>>  drivers/net/pse-pd/Kconfig                |   28 +
>>  drivers/net/pse-pd/Makefile               |    3 +
>>  drivers/net/pse-pd/realtek-pse-mcu-core.c | 1019 +++++++++++++++++++++
>>  drivers/net/pse-pd/realtek-pse-mcu-i2c.c  |  163 ++++
>>  drivers/net/pse-pd/realtek-pse-mcu-uart.c |  156 ++++
>>  drivers/net/pse-pd/realtek-pse-mcu.h      |   87 ++
>>  7 files changed, 1463 insertions(+)
>>  create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-core.c
>>  create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-i2c.c
>>  create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-uart.c
>>  create mode 100644 drivers/net/pse-pd/realtek-pse-mcu.h
> This is quite large, and shouls be split in smaller patches to help
> reviewers.
>
> [...]
>> +struct rtpse_mcu_dialect {
>> +	struct rtpse_mcu_opcode opcode[RTPSE_MCU_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);
> The 2 existing implementation always return 0; you may consider change
> it to a void function.
>
>> +static int rtpse_mcu_port_get_voltage(struct pse_controller_dev *pcdev, int id)
>> +{
>> +	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
>> +	struct rtpse_mcu_port_measurement measurement;
>> +	int ret;
>> +	u32 uV;
>> +
>> +	ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
>> +	if (ret)
>> +		return ret;
>> +
>> +	/* 64.45mV per LSB */
>> +	uV = (u32)measurement.voltage_raw * 64450U;
> This cast    ^^^^^ should be unneeded.
>
>> +	return min_t(u32, uV, INT_MAX);
>> +}
>> +
>> +static int rtpse_mcu_port_enable(struct pse_controller_dev *pcdev, int id)
>> +{
>> +	return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, true);
>> +}
>> +
>> +static int rtpse_mcu_port_disable(struct pse_controller_dev *pcdev, int id)
>> +{
>> +	return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, false);
>> +}
>> +
>> +static int rtpse_mcu_port_get_pw_limit(struct pse_controller_dev *pcdev, int id)
>> +{
>> +	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
>> +	struct rtpse_mcu_port_ext_config config;
>> +	int ret;
>> +
>> +	ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
>> +	if (ret)
>> +		return ret;
>> +
>> +	return config.max_power * pse->chip->pw_read_lsb_mW;
>> +}
>> +
>> +static int rtpse_mcu_port_set_pw_limit(struct pse_controller_dev *pcdev, int id, int max_mW)
>> +{
>> +	const struct rtpse_mcu_opcode *type_opc, *val_opc;
>> +	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
>> +	const struct rtpse_mcu_chip_info *chip = pse->chip;
>> +	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_MCU_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_mcu_port_cmd(pse, id, type_opc->op, RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER);
>> +	if (ret)
>> +		return ret;
>> +
>> +	prg_val = min_t(unsigned int, max_mW / chip->pw_set_lsb_mW, 0xff);
>> +
>> +	return rtpse_mcu_port_cmd(pse, id, val_opc->op, prg_val);
>> +}
>> +
>> +static int rtpse_mcu_port_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
>> +					      struct pse_pw_limit_ranges *out)
>> +{
>> +	struct ethtool_c33_pse_pw_limit_range *range;
>> +	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
>> +
>> +	range = kzalloc_obj(*range, GFP_KERNEL);
> or just:
>
> 	range = kzalloc_obj(*range);
>
>
>> +static int rtpse_mcu_discover(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
>> +{
>> +	struct rtpse_mcu_ext_config ext_config;
>> +	unsigned long deadline;
>> +	int ret;
>> +
>> +	/*
>> +	 * 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_MCU_DEVICE_ID_RTL8238B:
>> +		pse->chip = &rtl8238b_info;
>> +		break;
>> +	case RTPSE_MCU_DEVICE_ID_RTL8239:
>> +		pse->chip = &rtl8239_info;
>> +		break;
>> +	case RTPSE_MCU_DEVICE_ID_RTL8239C:
>> +		pse->chip = &rtl8239c_info;
>> +		break;
>> +	case RTPSE_MCU_DEVICE_ID_BCM59111:
>> +		pse->chip = &bcm59111_info;
>> +		break;
>> +	case RTPSE_MCU_DEVICE_ID_BCM59121:
>> +		pse->chip = &bcm59121_info;
>> +		break;
>> +	default:
>> +		return dev_err_probe(pse->dev, -EINVAL, "unknown PSE id 0x%x\n",
>> +				     info->device_id);
>> +	}
>> +
>> +	if (!info->max_ports || info->max_ports > RTPSE_MCU_MAX_PORTS)
>> +		return dev_err_probe(pse->dev, -EINVAL,
>> +				     "MCU reports invalid port count %u\n", info->max_ports);
>> +
>> +	ret = rtpse_mcu_get_ext_config(pse, &ext_config);
>> +	if (ret)
>> +		return dev_err_probe(pse->dev, ret, "failed to read MCU ext config\n");
>> +
>> +	dev_info(pse->dev, "%s MCU, %s (id 0x%04x), %u ports across %u PSE chip(s)\n",
>> +		 pse->dialect->mcu_type_str(info->mcu_type), pse->chip->name,
>> +		 info->device_id, info->max_ports, ext_config.num_of_pses);
> The general guidance is to try to avoid unneeded print on dmsg, as they
> tend to scary admins, but I personally agree on message on modprobe.
>
> No strong opionion either ways.
>
> /P
>
Thanks for your remarks, I'll address them in v5.

Best regards,
Jonas

^ permalink raw reply

* [PATCH net-next v5 0/4] net: pse-pd: add Realtek/Broadcom PSE MCU support
From: Jonas Jelonek @ 2026-07-06 11:24 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-brcm".
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
    "realtek" prefix names the owner of the protocol the MCU runs -
    Realtek documents it and supplies the firmware - following the
    "google,cros-ec-*" pattern (the prefix is the protocol/firmware owner,
    not the varying controller silicon). The "-rtk"/"-brcm" suffix selects
    the Realtek or Broadcom dialect; the specific PSE chip behind the MCU
    is detected at runtime.

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)
 - Unifi USW Pro XG 8 PoE  (RTL8239, SMBus)
 - Zyxel GS1900-10HP A1    (BCM59121, UART)
 - Zyxel GS1900-10HP B1    (RTL8238B, UART)
 - Zyxel GS1920-24HPv2     (BCM59121, SMBus)
 - Zyxel XMG1915-10EP      (RTL8239C, UART)
 - Zyxel XS1930-12HP       (RTL8239, SMBus)

---
v4 -> v5:
 - split the single driver patch into three — core / I2C transport / UART
   transport. Binding stays patch 1, unchanged in shape. (Paolo)
   Please give guidance on how to if I should split more.
 - core: set_pw_limit: guard divide-by-zero on pw_set_lsb_mW; cap the
   programmed value with U8_MAX instead of a bare 0xff; prg_val is now u8.
   (Oleksij, Sashiko)
 - core: discover: also retry transient boot-time frames (-EBADMSG / -EBADE)
   within the bounded window, not just silence/NAK/not-ready (Sashiko).
 - core: pw_status: report Broadcom 0x3 → TEST and 0x5 → OTHERFAULT
   (new STS_TEST/STS_OTHER_FAULT); pw_class comment corrected (0x3/0x5
   aren't "other fault" on RTL; class-0-vs-fault note). (Sashiko)
 - core: dropped unused decoded fields — function_mode, cls_type,
   disconnect_type, pair_type, inrush_mode, limit_type, chip_addr,
   channel. (Oleksij)
 - core: removed forward declarations by moving the response structs above
   the dialect struct. (Oleksij)
 - core: get_pw_limit_ranges: reverse-Christmas-tree local ordering.
   (Oleksij)
 - core: dialect comment clarified (only divergent responses are hooked);
   commit message "parser hooks" tightened to "…for the responses that
   differ." (Sashiko)
 - core: made parse_system_info hook void, both implementations return
   hardcoded 0. (Paolo)
 - core: dropped GFP_KERNEL from kzalloc_obj. (Paolo)
 - core: dropped unneeded u32 cast
 - kept probe dev_info() for now deliberately, due to different opinions
   on whether a probe might print or not
 - NOT included Acked-by from Oleksij, due to several changes
v4: https://lore.kernel.org/netdev/20260630105651.756058-1-jelonek.jonas@gmail.com/

v3 -> v4:
 - move owner setting from core to transport, mitigating possible
   use-after-free (Sashiko)
 - resend because net-next was still closed
v3: https://lore.kernel.org/netdev/20260628222705.4052815-1-jelonek.jonas@gmail.com/

v2 -> v3:
 - dt-bindings: using brcm instead of bcm for Broadcom
 - rename the driver files and Kconfig symbols to realtek-pse-mcu-* /
   PSE_REALTEK_MCU* for consistency with the realtek,pse-mcu-* compatibles
 - rename driver-internal prefix from 'rtpse_' to 'rtpse_mcu' to
   emphasize this targets the MCU-centric setup (and leaves room open
   for eventual directly addressable PSE chips)
 - rework the vendor-prefix rationale (binding + commit message): the
   prefix names the protocol/firmware owner (Realtek documents the protocol
   and supplies the firmware), and -rtk/-brcm select the Realtek or Broadcom
   protocol dialect
 - core: reject zeroed/echo-mismatched responses via the echoed seq_num
   (a BCM PORT_ENABLE on port 0 was otherwise accepted from an all-zero
   frame)
 - core: enable the PoE supply before global-enabling the MCU, and roll
   back the global enable on probe failure or driver removal
 - core: drop inline from helpers (flagged by automated check)
 - uart: update the completion under rx_lock too, so a late frame can no
   longer make the next transaction fail spuriously with -EIO
v2: https://lore.kernel.org/netdev/20260612132944.460646-1-jelonek.jonas@gmail.com/

v1 -> v2:
 - all points flagged by Sashiko addressed:
 - uart: drop frame overflow (return count, not the stored length) so
   serdev retains no leftover bytes that would misalign the next response
 - uart: guard rx_buf/rx_len with a spinlock to close a data race between
   the async receive_buf callback and send/recv
 - i2c: return terminal MCU error opcodes (0xfd/0xfe) to the core
   immediately instead of polling to the 1 s timeout
 - core: cap BCM59121 at 30 W (802.3at) — the basic 8-bit set command
   can't program the advertised 60 W (it silently clamped to 51 W)
v1: https://lore.kernel.org/netdev/20260608205758.1830521-1-jelonek.jonas@gmail.com/

---
Jonas Jelonek (4):
  dt-bindings: net: pse-pd: add bindings for Realtek/Broadcom PSE MCU
  net: pse-pd: add Realtek/Broadcom PSE MCU core
  net: pse-pd: realtek-pse-mcu: add I2C transport
  net: pse-pd: realtek-pse-mcu: add UART transport

 .../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-mcu-core.c     | 1006 +++++++++++++++++
 drivers/net/pse-pd/realtek-pse-mcu-i2c.c      |  163 +++
 drivers/net/pse-pd/realtek-pse-mcu-uart.c     |  156 +++
 drivers/net/pse-pd/realtek-pse-mcu.h          |   87 ++
 8 files changed, 1604 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/net/pse-pd/realtek,pse-mcu.yaml
 create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-core.c
 create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-i2c.c
 create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-uart.c
 create mode 100644 drivers/net/pse-pd/realtek-pse-mcu.h


base-commit: b73bc9ca3686b78b642fb35dcc1fdf874ecb74a1
-- 
2.51.0


^ permalink raw reply

* [PATCH net-next v5 1/4] dt-bindings: net: pse-pd: add bindings for Realtek/Broadcom PSE MCU
From: Jonas Jelonek @ 2026-07-06 11:24 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
In-Reply-To: <20260706112425.3149226-1-jelonek.jonas@gmail.com>

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 names the MCU front-end, not a specific part. These
boards front the PSE silicon with an MCU that presents a stable
message protocol Realtek documents. The PSE chip behind it varies
- Broadcom on older boards, Realtek on newer - and is detected at
runtime; the arrangement appears to be a Realtek MCU-based PoE design
carried across those PSE-chip generations. So the 'realtek' prefix
names that front-end (Realtek's protocol and firmware), not the
general-purpose MCU silicon or the PSE chip - the google,cros-ec-*
model. The '-rtk'/'-brcm' suffix selects the Realtek or Broadcom dialect.

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 the one 'realtek' prefix,
with the '-rtk'/'-brcm' suffix distinguishing the dialect.

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..d0dfae220dc1
--- /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 here is the MCU: it presents a stable message protocol
+  documented by Realtek, with the PSE silicon behind it - Broadcom on
+  older boards, Realtek on newer - detected at runtime and not described
+  here. The MCU's own silicon is general-purpose and varies across
+  boards, so the 'realtek' vendor prefix names the protocol front-end
+  (following the google,cros-ec pattern); the '-rtk'/'-brcm' suffix
+  selects the Realtek or Broadcom dialect.
+
+  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-brcm
+
+  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-brcm";
+            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

* [PATCH net-next v5 2/4] net: pse-pd: add Realtek/Broadcom PSE MCU core
From: Jonas Jelonek @ 2026-07-06 11:24 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
In-Reply-To: <20260706112425.3149226-1-jelonek.jonas@gmail.com>

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; the driver
handles this with a per-dialect opcode table and parser hooks for the
responses that differ, selected by the compatible. The specific PSE chip
behind the MCU is detected at runtime and only influences per-chip
constants (power scaling and the per-port cap).

This core module implements the protocol, message framing, the dialect
machinery and the pse_controller_ops glue, and exports a registration
helper for transport modules. The I2C and UART transports that drive it
follow in the next patches; the core (PSE_REALTEK_MCU) is selected
automatically by those transports and is not user-selectable on its own.

The realtek-pse-mcu-* files and PSE_REALTEK_MCU* symbols match the
realtek,pse-mcu-rtk / realtek,pse-mcu-brcm compatibles: all name the
Realtek PSE-MCU front-end, not the MCU silicon or the PSE chip behind
it (see the binding for the prefix rationale). Broadcom PSE MCUs speak
the same protocol family and are handled by the same shared core
through the dialect abstraction selected by the '-brcm' compatible.

Power budgeting is left to the MCU firmware; the driver advertises
PSE_BUDGET_EVAL_STRAT_DYNAMIC (controller-managed budget) accordingly.

Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
---
 MAINTAINERS                               |    7 +
 drivers/net/pse-pd/Kconfig                |    6 +
 drivers/net/pse-pd/Makefile               |    1 +
 drivers/net/pse-pd/realtek-pse-mcu-core.c | 1006 +++++++++++++++++++++
 drivers/net/pse-pd/realtek-pse-mcu.h      |   87 ++
 5 files changed, 1107 insertions(+)
 create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-core.c
 create mode 100644 drivers/net/pse-pd/realtek-pse-mcu.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 52f1a55eca99..e3f426a3b201 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22716,6 +22716,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-mcu*
+
 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..626f47b8acd7 100644
--- a/drivers/net/pse-pd/Kconfig
+++ b/drivers/net/pse-pd/Kconfig
@@ -13,6 +13,12 @@ menuconfig PSE_CONTROLLER
 
 if PSE_CONTROLLER
 
+config PSE_REALTEK_MCU
+	tristate
+	help
+	  Shared core for the Realtek/Broadcom PSE MCU driver. This is
+	  selected automatically by the transport options below.
+
 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..bf35e2a5b110 100644
--- a/drivers/net/pse-pd/Makefile
+++ b/drivers/net/pse-pd/Makefile
@@ -3,6 +3,7 @@
 
 obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
 
+obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
 obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
 obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
 obj-$(CONFIG_PSE_SI3474) += si3474.o
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-core.c b/drivers/net/pse-pd/realtek-pse-mcu-core.c
new file mode 100644
index 000000000000..961ac9e9ee9c
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-core.c
@@ -0,0 +1,1006 @@
+// 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-mcu.h"
+
+#define RTPSE_MCU_DEVICE_ID_RTL8238B		0x0138
+#define RTPSE_MCU_DEVICE_ID_RTL8239		0x0039
+#define RTPSE_MCU_DEVICE_ID_RTL8239C		0x0139
+#define RTPSE_MCU_DEVICE_ID_BCM59111		0xe111
+#define RTPSE_MCU_DEVICE_ID_BCM59121		0xe121
+
+#define RTPSE_MCU_PORT_STS_DISABLED		0x00
+#define RTPSE_MCU_PORT_STS_SEARCHING		0x01
+#define RTPSE_MCU_PORT_STS_DELIVERING		0x02
+#define RTPSE_MCU_PORT_STS_TEST			0x03	/* Broadcom; reserved on Realtek */
+#define RTPSE_MCU_PORT_STS_FAULT		0x04
+#define RTPSE_MCU_PORT_STS_OTHER_FAULT		0x05	/* Broadcom; reserved on Realtek */
+#define RTPSE_MCU_PORT_STS_REQUESTING		0x06
+
+/* RTPSE_MCU_PORT_SET_POWER_LIMIT_TYPE values */
+#define RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER	0x02
+
+#define RTPSE_MCU_MAX_PORTS			48
+#define RTPSE_MCU_PORT_MAX_PRIORITY		3
+
+enum rtpse_mcu_cmd {
+	RTPSE_MCU_CMD_SET_GLOBAL_STATE,
+	RTPSE_MCU_CMD_GET_SYSTEM_INFO,
+	RTPSE_MCU_CMD_GET_EXT_CONFIG,
+
+	RTPSE_MCU_CMD_PORT_ENABLE,
+	RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE,
+	RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+	RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+	RTPSE_MCU_CMD_PORT_SET_PRIORITY,
+	RTPSE_MCU_CMD_PORT_GET_STATUS,
+	RTPSE_MCU_CMD_PORT_GET_POWER_STATS,
+	RTPSE_MCU_CMD_PORT_GET_CONFIG,
+	RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG,
+
+	RTPSE_MCU_NUM_CMDS,
+};
+
+struct rtpse_mcu_opcode {
+	u8 op;
+	bool valid;
+};
+
+/* Shorthand for the designated-initializer entries in dialect opcode tables. */
+#define RTPSE_MCU_OP(opc)	{ .op = (opc), .valid = true }
+
+/* Parsed MCU response structures (decoded from rtpse_mcu_msg replies) */
+
+struct rtpse_mcu_info {
+	u8 max_ports;
+	bool system_enable;
+	u16 device_id;
+	u8 sw_ver;
+	u8 mcu_type;
+	u8 config_status;
+	u8 ext_ver;
+};
+
+struct rtpse_mcu_ext_config {
+	u8 uvlo;
+	u8 ovlo;
+	bool prealloc_enable;
+	u8 num_of_pses;
+};
+
+struct rtpse_mcu_port_status {
+	u8 sts1;
+	u8 sts2;
+	u8 sts3;
+};
+
+struct rtpse_mcu_port_measurement {
+	u16 voltage_raw;	/* 64.45mV/LSB */
+	u16 current_raw;	/* 1mA/LSB */
+	u16 temperature_raw;	/* T(mC) = 1250 * (220 - raw) */
+	u16 power_raw;		/* 100mW/LSB */
+};
+
+struct rtpse_mcu_port_config {
+	bool enable;
+};
+
+struct rtpse_mcu_port_ext_config {
+	u8 max_power;
+	u8 priority;
+};
+
+struct rtpse_mcu_dialect {
+	struct rtpse_mcu_opcode opcode[RTPSE_MCU_NUM_CMDS];
+
+	/*
+	 * Response parsers for the fields that differ between dialects; each
+	 * dialect supplies its own. Other responses share one layout and are
+	 * decoded directly - a dialect that diverges there must add a hook,
+	 * as a mismatched layout cannot be detected (the checksum still passes).
+	 */
+	void (*parse_system_info)(const u8 *payload, struct rtpse_mcu_info *info);
+	int (*parse_port_class)(const struct rtpse_mcu_port_status *status);
+	const char *(*mcu_type_str)(unsigned int mcu_type);
+};
+
+/*
+ * 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_mcu_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_mcu_chip_info {
+	const char *name;
+	u16 device_id;
+	u32 max_mW_per_port;
+	enum rtpse_mcu_cmd pw_set_cmd;	/* command used by set_pw_limit */
+	u32 pw_set_lsb_mW;		/* LSB of pw_set_cmd value, in mW */
+	u32 pw_read_lsb_mW;		/* LSB of ext_config.max_power read-back, in mW */
+};
+
+static const struct rtpse_mcu_chip_info rtl8238b_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_RTL8238B,
+	.max_mW_per_port = 30000,
+	.name = "RTL8238B",
+	.pw_read_lsb_mW = 200,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+	.pw_set_lsb_mW = 200,
+};
+
+static const struct rtpse_mcu_chip_info rtl8239_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_RTL8239,
+	.max_mW_per_port = 90000,
+	.name = "RTL8239",
+	.pw_read_lsb_mW = 400,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+	.pw_set_lsb_mW = 400,
+};
+
+static const struct rtpse_mcu_chip_info rtl8239c_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_RTL8239C,
+	.max_mW_per_port = 90000,
+	.name = "RTL8239C",
+	.pw_read_lsb_mW = 400,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
+	.pw_set_lsb_mW = 400,
+};
+
+static const struct rtpse_mcu_chip_info bcm59111_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_BCM59111,
+	.max_mW_per_port = 30000,
+	.name = "BCM59111",
+	.pw_read_lsb_mW = 200,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+	.pw_set_lsb_mW = 200,
+};
+
+static const struct rtpse_mcu_chip_info bcm59121_info = {
+	.device_id = RTPSE_MCU_DEVICE_ID_BCM59121,
+	/*
+	 * BCM59121 is a 60W Type-3 part, but known boards run it at 802.3at
+	 * and the BCM dialect has only the 8-bit/0.2W set command (<=51W);
+	 * cap at the 30W the hardware actually offers.
+	 */
+	.max_mW_per_port = 30000,
+	.name = "BCM59121",
+	.pw_read_lsb_mW = 200,
+	.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
+	.pw_set_lsb_mW = 200,
+};
+
+/* Helpers and basic functions */
+
+static struct rtpse_mcu_ctrl *to_rtpse_mcu_ctrl(struct pse_controller_dev *pcdev)
+{
+	return container_of(pcdev, struct rtpse_mcu_ctrl, pcdev);
+}
+
+bool rtpse_mcu_needs_i2c_proto(const struct rtpse_mcu_match_data *match)
+{
+	return match->i2c_proto_dt_required;
+}
+EXPORT_SYMBOL_GPL(rtpse_mcu_needs_i2c_proto);
+
+static void rtpse_mcu_msg_init(struct rtpse_mcu_msg *msg, u8 opcode)
+{
+	memset(msg, 0xff, sizeof(*msg));
+	msg->opcode = opcode;
+}
+
+static u8 rtpse_mcu_checksum(const u8 *buf, size_t len)
+{
+	u8 sum = 0;
+
+	while (len--)
+		sum += *buf++;
+	return sum;
+}
+
+static int rtpse_mcu_do_xfer(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_msg *req,
+			     struct rtpse_mcu_msg *resp)
+{
+	int ret;
+
+	req->checksum = rtpse_mcu_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 RTPSE_MCU_OPCODE_INCOMPLETE:
+		return -EBADE;
+	case RTPSE_MCU_OPCODE_BAD_CSUM:
+		return -EBADMSG;
+	case RTPSE_MCU_OPCODE_NOT_READY:
+		return -EAGAIN;
+	}
+
+	if (resp->opcode != req->opcode ||
+	    resp->seq_num != req->seq_num ||
+	    resp->checksum != rtpse_mcu_checksum((u8 *)resp, RTPSE_MCU_MSG_SIZE - 1))
+		return -EBADMSG;
+
+	return 0;
+}
+
+static int rtpse_mcu_port_query(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode,
+				struct rtpse_mcu_msg *resp)
+{
+	struct rtpse_mcu_msg req;
+	int ret;
+
+	rtpse_mcu_msg_init(&req, opcode);
+	req.payload[0] = port;
+
+	ret = rtpse_mcu_do_xfer(pse, &req, resp);
+	if (ret)
+		return ret;
+
+	if (resp->payload[0] != port)
+		return -EIO;
+
+	return 0;
+}
+
+static int rtpse_mcu_port_cmd(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode, u8 arg)
+{
+	struct rtpse_mcu_msg req, resp;
+	int ret;
+
+	rtpse_mcu_msg_init(&req, opcode);
+	req.payload[0] = port;
+	req.payload[1] = arg;
+
+	ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+	if (ret)
+		return ret;
+
+	if (resp.payload[0] != port || resp.payload[1] != 0)
+		return -EIO;
+
+	return 0;
+}
+
+/* Global operations */
+
+static int rtpse_mcu_get_info(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
+{
+	struct rtpse_mcu_msg req, resp;
+	const struct rtpse_mcu_opcode *opc;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_SYSTEM_INFO];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	rtpse_mcu_msg_init(&req, opc->op);
+	ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+	if (ret)
+		return ret;
+
+	pse->dialect->parse_system_info(resp.payload, info);
+	return 0;
+}
+
+static int rtpse_mcu_get_ext_config(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_ext_config *config)
+{
+	struct rtpse_mcu_msg req, resp;
+	const struct rtpse_mcu_opcode *opc;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_EXT_CONFIG];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	rtpse_mcu_msg_init(&req, opc->op);
+	ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+	if (ret)
+		return ret;
+
+	config->uvlo = resp.payload[0];
+	config->ovlo = resp.payload[5];
+	config->prealloc_enable = (resp.payload[1] == 0x1);
+	config->num_of_pses = resp.payload[6];
+
+	return 0;
+}
+
+static int rtpse_mcu_set_global_state(struct rtpse_mcu_ctrl *pse, bool enable)
+{
+	struct rtpse_mcu_msg req, resp;
+	const struct rtpse_mcu_opcode *opc;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_SET_GLOBAL_STATE];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	rtpse_mcu_msg_init(&req, opc->op);
+	req.payload[0] = enable ? 0x1 : 0x0;
+
+	ret = rtpse_mcu_do_xfer(pse, &req, &resp);
+	if (ret)
+		return ret;
+
+	return (resp.payload[0] == 0x0) ? 0 : -EIO;
+}
+
+/* Port operations */
+
+static int rtpse_mcu_port_get_status(struct rtpse_mcu_ctrl *pse, unsigned int port,
+				     struct rtpse_mcu_port_status *status)
+{
+	const struct rtpse_mcu_opcode *opc;
+	struct rtpse_mcu_msg resp;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_STATUS];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+	if (ret)
+		return ret;
+
+	status->sts1 = resp.payload[1];
+	status->sts2 = resp.payload[2];
+	status->sts3 = resp.payload[3];
+
+	return 0;
+}
+
+static int rtpse_mcu_port_get_measurement(struct rtpse_mcu_ctrl *pse, unsigned int port,
+					  struct rtpse_mcu_port_measurement *measurement)
+{
+	const struct rtpse_mcu_opcode *opc;
+	struct rtpse_mcu_msg resp;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_POWER_STATS];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+	if (ret)
+		return ret;
+
+	measurement->voltage_raw = get_unaligned_be16(&resp.payload[1]);
+	measurement->current_raw = get_unaligned_be16(&resp.payload[3]);
+	measurement->temperature_raw = get_unaligned_be16(&resp.payload[5]);
+	measurement->power_raw = get_unaligned_be16(&resp.payload[7]);
+
+	return 0;
+}
+
+static int rtpse_mcu_port_get_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
+				     struct rtpse_mcu_port_config *config)
+{
+	const struct rtpse_mcu_opcode *opc;
+	struct rtpse_mcu_msg resp;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_CONFIG];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+	if (ret)
+		return ret;
+
+	config->enable = (resp.payload[1] == 1);
+
+	return 0;
+}
+
+static int rtpse_mcu_port_get_ext_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
+					 struct rtpse_mcu_port_ext_config *config)
+{
+	const struct rtpse_mcu_opcode *opc;
+	struct rtpse_mcu_msg resp;
+	int ret;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
+	if (ret)
+		return ret;
+
+	config->max_power = resp.payload[3];
+	config->priority = resp.payload[4];
+
+	return 0;
+}
+
+static int rtpse_mcu_port_set_state(struct rtpse_mcu_ctrl *pse, unsigned int port, bool enable)
+{
+	const struct rtpse_mcu_opcode *opc;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_ENABLE];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	return rtpse_mcu_port_cmd(pse, port, opc->op, enable ? 0x1 : 0x0);
+}
+
+/* PSE controller ops */
+
+static int rtpse_mcu_port_get_admin_state(struct pse_controller_dev *pcdev, int id,
+					  struct pse_admin_state *admin_state)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_config config;
+	int ret;
+
+	ret = rtpse_mcu_port_get_config(pse, id, &config);
+	if (ret)
+		return ret;
+
+	admin_state->c33_admin_state = config.enable ? ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED :
+						       ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
+	return 0;
+}
+
+static int rtpse_mcu_port_get_pw_status(struct pse_controller_dev *pcdev, int id,
+					struct pse_pw_status *pw_status)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_status status;
+	int ret;
+
+	ret = rtpse_mcu_port_get_status(pse, id, &status);
+	if (ret)
+		return ret;
+
+	switch (status.sts1) {
+	case RTPSE_MCU_PORT_STS_DISABLED:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
+		break;
+	case RTPSE_MCU_PORT_STS_SEARCHING:
+	case RTPSE_MCU_PORT_STS_REQUESTING:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
+		break;
+	case RTPSE_MCU_PORT_STS_DELIVERING:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
+		break;
+	case RTPSE_MCU_PORT_STS_TEST:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_TEST;
+		break;
+	case RTPSE_MCU_PORT_STS_FAULT:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;
+		break;
+	case RTPSE_MCU_PORT_STS_OTHER_FAULT:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_OTHERFAULT;
+		break;
+	default:
+		pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_UNKNOWN;
+		break;
+	}
+
+	return 0;
+}
+
+static int rtpse_mcu_port_get_pw_class(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_status status;
+	int ret;
+
+	ret = rtpse_mcu_port_get_status(pse, id, &status);
+	if (ret)
+		return ret;
+
+	/*
+	 * sts2 holds the class only in the operational states below. In a
+	 * fault/test state (or a code reserved on the current dialect) it
+	 * does not, so there is no class to report and we return 0. That is
+	 * indistinguishable from a real class-0 PD; userspace disambiguates
+	 * via the power status.
+	 */
+	switch (status.sts1) {
+	case RTPSE_MCU_PORT_STS_DISABLED:
+	case RTPSE_MCU_PORT_STS_SEARCHING:
+	case RTPSE_MCU_PORT_STS_DELIVERING:
+	case RTPSE_MCU_PORT_STS_REQUESTING:
+		return pse->dialect->parse_port_class(&status);
+	default:
+		return 0;
+	}
+}
+
+static int rtpse_mcu_port_get_actual_pw(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_measurement measurement;
+	int ret;
+
+	ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
+	if (ret)
+		return ret;
+
+	/* 100mW per LSB */
+	return measurement.power_raw * 100U;
+}
+
+static int rtpse_mcu_port_get_voltage(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_measurement measurement;
+	int ret;
+	u32 uV;
+
+	ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
+	if (ret)
+		return ret;
+
+	/* 64.45mV per LSB */
+	uV = measurement.voltage_raw * 64450U;
+	return min_t(u32, uV, INT_MAX);
+}
+
+static int rtpse_mcu_port_enable(struct pse_controller_dev *pcdev, int id)
+{
+	return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, true);
+}
+
+static int rtpse_mcu_port_disable(struct pse_controller_dev *pcdev, int id)
+{
+	return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, false);
+}
+
+static int rtpse_mcu_port_get_pw_limit(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_ext_config config;
+	int ret;
+
+	ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
+	if (ret)
+		return ret;
+
+	return config.max_power * pse->chip->pw_read_lsb_mW;
+}
+
+static int rtpse_mcu_port_set_pw_limit(struct pse_controller_dev *pcdev, int id, int max_mW)
+{
+	const struct rtpse_mcu_opcode *type_opc, *val_opc;
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	const struct rtpse_mcu_chip_info *chip = pse->chip;
+	u8 prg_val;
+	int ret;
+
+	if (max_mW < 0 || max_mW > chip->max_mW_per_port)
+		return -ERANGE;
+
+	type_opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE];
+	val_opc = &pse->dialect->opcode[chip->pw_set_cmd];
+	/* pw_set_lsb_mW is the divisor below; reject a chip that lacks it. */
+	if (!type_opc->valid || !val_opc->valid || !chip->pw_set_lsb_mW)
+		return -EOPNOTSUPP;
+
+	/*
+	 * Switch the port to user-defined limit mode first, then program the
+	 * limit value. If the second cmd fails, the port is left in
+	 * user-defined mode but with the previous limit value; the next
+	 * successful set_pw_limit call recovers it.
+	 */
+	ret = rtpse_mcu_port_cmd(pse, id, type_opc->op, RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER);
+	if (ret)
+		return ret;
+
+	prg_val = min_t(unsigned int, max_mW / chip->pw_set_lsb_mW, U8_MAX);
+
+	return rtpse_mcu_port_cmd(pse, id, val_opc->op, prg_val);
+}
+
+static int rtpse_mcu_port_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
+					      struct pse_pw_limit_ranges *out)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct ethtool_c33_pse_pw_limit_range *range;
+
+	range = kzalloc_obj(*range);
+	if (!range)
+		return -ENOMEM;
+
+	range[0].min = 0;
+	range[0].max = pse->chip->max_mW_per_port;
+
+	out->c33_pw_limit_ranges = range;
+	return 1;
+}
+
+static int rtpse_mcu_port_get_prio(struct pse_controller_dev *pcdev, int id)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	struct rtpse_mcu_port_ext_config config;
+	int ret;
+
+	ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
+	if (ret)
+		return ret;
+
+	return config.priority;
+}
+
+static int rtpse_mcu_port_set_prio(struct pse_controller_dev *pcdev, int id, unsigned int prio)
+{
+	struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
+	const struct rtpse_mcu_opcode *opc;
+
+	if (prio > RTPSE_MCU_PORT_MAX_PRIORITY)
+		return -ERANGE;
+
+	opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_PRIORITY];
+	if (!opc->valid)
+		return -EOPNOTSUPP;
+
+	return rtpse_mcu_port_cmd(pse, id, opc->op, prio);
+}
+
+static const struct pse_controller_ops rtpse_mcu_ops = {
+	.pi_get_admin_state = rtpse_mcu_port_get_admin_state,
+	.pi_get_pw_status = rtpse_mcu_port_get_pw_status,
+	.pi_get_pw_class = rtpse_mcu_port_get_pw_class,
+	.pi_get_actual_pw = rtpse_mcu_port_get_actual_pw,
+	.pi_enable = rtpse_mcu_port_enable,
+	.pi_disable = rtpse_mcu_port_disable,
+	.pi_get_voltage = rtpse_mcu_port_get_voltage,
+	.pi_get_pw_limit = rtpse_mcu_port_get_pw_limit,
+	.pi_set_pw_limit = rtpse_mcu_port_set_pw_limit,
+	.pi_get_pw_limit_ranges = rtpse_mcu_port_get_pw_limit_ranges,
+	.pi_get_prio = rtpse_mcu_port_get_prio,
+	.pi_set_prio = rtpse_mcu_port_set_prio,
+};
+
+static int rtpse_mcu_discover(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
+{
+	struct rtpse_mcu_ext_config ext_config;
+	unsigned long deadline;
+	int ret;
+
+	/*
+	 * A booting MCU may stay silent (-ETIMEDOUT), not ACK its address
+	 * (-ENXIO / -EREMOTEIO), report not-ready (-EAGAIN), or emit a
+	 * corrupt/partial frame (-EBADMSG / -EBADE). Retry those within a
+	 * bounded window; other errors (e.g. -EOPNOTSUPP) are fatal and fail
+	 * immediately.
+	 */
+	deadline = jiffies + msecs_to_jiffies(RTPSE_MCU_BOOT_TIMEOUT_MS);
+	do {
+		ret = rtpse_mcu_get_info(pse, info);
+		if (ret != -ETIMEDOUT && ret != -ENXIO && ret != -EREMOTEIO &&
+		    ret != -EAGAIN && ret != -EBADMSG && ret != -EBADE)
+			break;
+		msleep(RTPSE_MCU_BOOT_RETRY_MS);
+	} while (time_before(jiffies, deadline));
+	if (ret)
+		return dev_err_probe(pse->dev, ret, "failed to read MCU info\n");
+
+	switch (info->device_id) {
+	case RTPSE_MCU_DEVICE_ID_RTL8238B:
+		pse->chip = &rtl8238b_info;
+		break;
+	case RTPSE_MCU_DEVICE_ID_RTL8239:
+		pse->chip = &rtl8239_info;
+		break;
+	case RTPSE_MCU_DEVICE_ID_RTL8239C:
+		pse->chip = &rtl8239c_info;
+		break;
+	case RTPSE_MCU_DEVICE_ID_BCM59111:
+		pse->chip = &bcm59111_info;
+		break;
+	case RTPSE_MCU_DEVICE_ID_BCM59121:
+		pse->chip = &bcm59121_info;
+		break;
+	default:
+		return dev_err_probe(pse->dev, -EINVAL, "unknown PSE id 0x%x\n",
+				     info->device_id);
+	}
+
+	if (!info->max_ports || info->max_ports > RTPSE_MCU_MAX_PORTS)
+		return dev_err_probe(pse->dev, -EINVAL,
+				     "MCU reports invalid port count %u\n", info->max_ports);
+
+	ret = rtpse_mcu_get_ext_config(pse, &ext_config);
+	if (ret)
+		return dev_err_probe(pse->dev, ret, "failed to read MCU ext config\n");
+
+	dev_info(pse->dev, "%s MCU, %s (id 0x%04x), %u ports across %u PSE chip(s)\n",
+		 pse->dialect->mcu_type_str(info->mcu_type), pse->chip->name,
+		 info->device_id, info->max_ports, ext_config.num_of_pses);
+	return 0;
+}
+
+static void rtpse_mcu_regulator_disable(void *data)
+{
+	regulator_disable(data);
+}
+
+static void rtpse_mcu_global_disable(void *data)
+{
+	struct rtpse_mcu_ctrl *pse = data;
+
+	rtpse_mcu_set_global_state(pse, false);
+}
+
+int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse)
+{
+	const struct rtpse_mcu_match_data *match;
+	struct gpio_desc *enable_gpio;
+	struct rtpse_mcu_info info;
+	int ret;
+
+	BUILD_BUG_ON(sizeof(struct rtpse_mcu_msg) != RTPSE_MCU_MSG_SIZE);
+
+	ret = devm_mutex_init(pse->dev, &pse->mutex);
+	if (ret)
+		return ret;
+
+	match = device_get_match_data(pse->dev);
+	if (!match)
+		return dev_err_probe(pse->dev, -ENODEV, "missing match data\n");
+	pse->dialect = match->dialect;
+
+	/*
+	 * Catch a dialect that forgot to set one of the required hooks at
+	 * probe time, rather than NULL-deref'ing later from a fast path.
+	 */
+	if (!pse->dialect ||
+	    !pse->dialect->parse_system_info ||
+	    !pse->dialect->parse_port_class ||
+	    !pse->dialect->mcu_type_str)
+		return dev_err_probe(pse->dev, -EINVAL,
+				     "dialect for chip is incomplete\n");
+
+	pse->poe_supply = devm_regulator_get(pse->dev, "power");
+	if (IS_ERR(pse->poe_supply))
+		return dev_err_probe(pse->dev, PTR_ERR(pse->poe_supply),
+				     "failed to get PoE supply\n");
+
+	enable_gpio = devm_gpiod_get_optional(pse->dev, "enable", GPIOD_OUT_HIGH);
+	if (IS_ERR(enable_gpio))
+		return dev_err_probe(pse->dev, PTR_ERR(enable_gpio),
+				     "failed to get enable gpio\n");
+
+	ret = rtpse_mcu_discover(pse, &info);
+	if (ret)
+		return ret;
+
+	ret = regulator_enable(pse->poe_supply);
+	if (ret)
+		return dev_err_probe(pse->dev, ret, "failed to enable PoE supply\n");
+
+	ret = devm_add_action_or_reset(pse->dev, rtpse_mcu_regulator_disable, pse->poe_supply);
+	if (ret)
+		return ret;
+
+	if (!info.system_enable) {
+		ret = rtpse_mcu_set_global_state(pse, true);
+		/* Dialects without a global-state concept (e.g. 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");
+		if (!ret) {
+			ret = devm_add_action_or_reset(pse->dev,
+						       rtpse_mcu_global_disable, pse);
+			if (ret)
+				return ret;
+		}
+	}
+
+	/*
+	 * Depending on the MCU firmware configuration (which might be different
+	 * for every board), it isn't known whether the PoE subsystem is active or
+	 * inactive by default. At this stage, the PSE chips might already deliver
+	 * power to PDs without any explicit enable.
+	 */
+
+	/* pcdev.owner is set by the transport, so the registered controller
+	 * pins the transport module that owns the live device, not the core.
+	 */
+	pse->pcdev.ops      = &rtpse_mcu_ops;
+	pse->pcdev.dev      = pse->dev;
+	pse->pcdev.types    = ETHTOOL_PSE_C33;
+	pse->pcdev.nr_lines = info.max_ports;
+	pse->pcdev.pis_prio_max = RTPSE_MCU_PORT_MAX_PRIORITY;
+	pse->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_DYNAMIC;
+
+	return devm_pse_controller_register(pse->dev, &pse->pcdev);
+}
+EXPORT_SYMBOL_GPL(rtpse_mcu_register);
+
+static void rtpse_mcu_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];
+}
+
+static int rtpse_mcu_rtl_parse_port_class(const struct rtpse_mcu_port_status *status)
+{
+	/* Class lives in the upper nibble of sts2. */
+	return FIELD_GET(GENMASK(7, 4), status->sts2);
+}
+
+static const char *rtpse_mcu_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 void rtpse_mcu_brcm_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];
+}
+
+static int rtpse_mcu_brcm_parse_port_class(const struct rtpse_mcu_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_mcu_brcm_mcu_type_str(unsigned int mcu_type)
+{
+	switch (mcu_type) {
+	case 0x00:	return "ST Micro ST32F100";
+	case 0x01:	return "Nuvoton M05xx LAN";
+	case 0x02:	return "ST Micro STF030C8";
+	case 0x03:	return "Nuvoton M058SAN";
+	case 0x04:	return "Nuvoton NUC122";
+	default:	return "unknown";
+	}
+}
+
+/* Map each logical command the core issues to its per-dialect opcode. */
+static const struct rtpse_mcu_dialect rtpse_mcu_dialect_rtk = {
+	.parse_system_info = rtpse_mcu_rtl_parse_system_info,
+	.parse_port_class  = rtpse_mcu_rtl_parse_port_class,
+	.mcu_type_str      = rtpse_mcu_rtl_mcu_type_str,
+	.opcode = {
+		[RTPSE_MCU_CMD_SET_GLOBAL_STATE]	= RTPSE_MCU_OP(0x00),
+		[RTPSE_MCU_CMD_GET_SYSTEM_INFO]		= RTPSE_MCU_OP(0x40),
+		[RTPSE_MCU_CMD_GET_EXT_CONFIG]		= RTPSE_MCU_OP(0x4a),
+
+		[RTPSE_MCU_CMD_PORT_ENABLE]		= RTPSE_MCU_OP(0x01),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x12),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT]	= RTPSE_MCU_OP(0x13),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT] = RTPSE_MCU_OP(0x14),
+		[RTPSE_MCU_CMD_PORT_SET_PRIORITY]	= RTPSE_MCU_OP(0x15),
+		[RTPSE_MCU_CMD_PORT_GET_STATUS]		= RTPSE_MCU_OP(0x42),
+		[RTPSE_MCU_CMD_PORT_GET_POWER_STATS]	= RTPSE_MCU_OP(0x44),
+		[RTPSE_MCU_CMD_PORT_GET_CONFIG]		= RTPSE_MCU_OP(0x48),
+		[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG]	= RTPSE_MCU_OP(0x49),
+	},
+};
+
+static const struct rtpse_mcu_dialect rtpse_mcu_dialect_brcm = {
+	.parse_system_info = rtpse_mcu_brcm_parse_system_info,
+	.parse_port_class  = rtpse_mcu_brcm_parse_port_class,
+	.mcu_type_str      = rtpse_mcu_brcm_mcu_type_str,
+	.opcode = {
+		[RTPSE_MCU_CMD_GET_SYSTEM_INFO]		= RTPSE_MCU_OP(0x20),
+		[RTPSE_MCU_CMD_GET_EXT_CONFIG]		= RTPSE_MCU_OP(0x2b),
+
+		[RTPSE_MCU_CMD_PORT_ENABLE]		= RTPSE_MCU_OP(0x00),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x15),
+		[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT]	= RTPSE_MCU_OP(0x16),
+		[RTPSE_MCU_CMD_PORT_SET_PRIORITY]	= RTPSE_MCU_OP(0x1a),
+		[RTPSE_MCU_CMD_PORT_GET_STATUS]		= RTPSE_MCU_OP(0x21),
+		[RTPSE_MCU_CMD_PORT_GET_POWER_STATS]	= RTPSE_MCU_OP(0x30),
+		[RTPSE_MCU_CMD_PORT_GET_CONFIG]		= RTPSE_MCU_OP(0x25),
+		[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG]	= RTPSE_MCU_OP(0x26),
+	},
+};
+
+const struct rtpse_mcu_match_data rtpse_mcu_rtk_data = {
+	.dialect = &rtpse_mcu_dialect_rtk,
+	.i2c_proto_dt_required = true,
+};
+EXPORT_SYMBOL_GPL(rtpse_mcu_rtk_data);
+
+const struct rtpse_mcu_match_data rtpse_mcu_brcm_data = {
+	.dialect = &rtpse_mcu_dialect_brcm,
+};
+EXPORT_SYMBOL_GPL(rtpse_mcu_brcm_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-mcu.h b/drivers/net/pse-pd/realtek-pse-mcu.h
new file mode 100644
index 000000000000..b9bf3b2dde08
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu.h
@@ -0,0 +1,87 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef _REALTEK_PSE_MCU_H
+#define _REALTEK_PSE_MCU_H
+
+#include <linux/mutex.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/types.h>
+
+/*
+ * Time the MCU itself needs between accepting a request and having a
+ * response ready. These are properties of the MCU firmware, not of the
+ * underlying transport: the core paces transactions by RTPSE_MCU_RESPONSE_MS
+ * and both transports size their per-transaction recv ceiling from
+ * RTPSE_MCU_RESPONSE_MAX_MS, since some commands are documented as
+ * needing up to ~1s to produce a reply.
+ */
+#define RTPSE_MCU_RESPONSE_MS			25
+#define RTPSE_MCU_RESPONSE_MAX_MS		1000
+
+/*
+ * Total time to keep retrying the first MCU read at probe, and the pause
+ * between attempts. Right after enable-gpios is asserted the MCU may not
+ * answer on the bus yet; give it a bounded window to come up before
+ * declaring the probe failed.
+ */
+#define RTPSE_MCU_BOOT_TIMEOUT_MS		3000
+#define RTPSE_MCU_BOOT_RETRY_MS			100
+
+#define RTPSE_MCU_MSG_SIZE			12
+
+struct rtpse_mcu_msg {
+	u8 opcode;
+	u8 seq_num;
+	u8 payload[9];
+	u8 checksum;
+} __packed;
+
+/*
+ * MCU status opcodes (seen on the BCM dialect; RTL never emits them).
+ * INCOMPLETE/BAD_CSUM are terminal; NOT_READY is transient.
+ */
+#define RTPSE_MCU_OPCODE_INCOMPLETE		0xfd	/* -EBADE   */
+#define RTPSE_MCU_OPCODE_BAD_CSUM		0xfe	/* -EBADMSG */
+#define RTPSE_MCU_OPCODE_NOT_READY		0xff	/* -EAGAIN  */
+
+/* A polling transport can stop here: the matching reply, or a terminal error. */
+static inline bool rtpse_mcu_resp_is_final(const struct rtpse_mcu_msg *req,
+					   const struct rtpse_mcu_msg *resp)
+{
+	return resp->opcode == req->opcode ||
+	       resp->opcode == RTPSE_MCU_OPCODE_INCOMPLETE ||
+	       resp->opcode == RTPSE_MCU_OPCODE_BAD_CSUM;
+}
+
+/* Opaque to transports; defined in realtek-pse-core.c. */
+struct rtpse_mcu_dialect;
+struct rtpse_mcu_match_data;
+struct rtpse_mcu_chip_info;
+struct rtpse_mcu_ctrl;
+
+struct rtpse_mcu_transport_ops {
+	int (*send)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req);
+	int (*recv)(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+		    struct rtpse_mcu_msg *resp);
+};
+
+struct rtpse_mcu_ctrl {
+	struct device *dev;
+	struct pse_controller_dev pcdev;
+	struct mutex mutex; /* serializes MCU request/response transactions */
+	const struct rtpse_mcu_dialect *dialect;
+	const struct rtpse_mcu_chip_info *chip;
+	const struct rtpse_mcu_transport_ops *transport;
+
+	struct regulator *poe_supply;
+};
+
+int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse);
+
+/* Whether the I2C transport must read "realtek,i2c-protocol" from DT. */
+bool rtpse_mcu_needs_i2c_proto(const struct rtpse_mcu_match_data *match);
+
+extern const struct rtpse_mcu_match_data rtpse_mcu_rtk_data;
+extern const struct rtpse_mcu_match_data rtpse_mcu_brcm_data;
+
+#endif
-- 
2.51.0


^ permalink raw reply related

* [PATCH net-next v5 3/4] net: pse-pd: realtek-pse-mcu: add I2C transport
From: Jonas Jelonek @ 2026-07-06 11:24 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
In-Reply-To: <20260706112425.3149226-1-jelonek.jonas@gmail.com>

Add the I2C/SMBus transport for the Realtek/Broadcom PSE MCU core. It
registers the MCU on an I2C bus and provides the send/recv callbacks the
core uses to exchange the 12-byte frames.

The MCU firmware expects one of two framings on the I2C bus, selected per
board via the "realtek,i2c-protocol" property: "smbus" (reads carry a
leading command byte and a repeated start) or "i2c" (bare block writes and
reads). The property is required for the Realtek dialect; the Broadcom
dialect and boards that omit it default to SMBus framing.

Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
---
 drivers/net/pse-pd/Kconfig               |  11 ++
 drivers/net/pse-pd/Makefile              |   1 +
 drivers/net/pse-pd/realtek-pse-mcu-i2c.c | 163 +++++++++++++++++++++++
 3 files changed, 175 insertions(+)
 create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-i2c.c

diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index 626f47b8acd7..7074d6733ff3 100644
--- a/drivers/net/pse-pd/Kconfig
+++ b/drivers/net/pse-pd/Kconfig
@@ -19,6 +19,17 @@ config PSE_REALTEK_MCU
 	  Shared core for the Realtek/Broadcom PSE MCU driver. This is
 	  selected automatically by the transport options below.
 
+config PSE_REALTEK_MCU_I2C
+	tristate "Realtek/Broadcom PSE MCU driver (I2C transport)"
+	depends on I2C
+	select PSE_REALTEK_MCU
+	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-mcu-i2c.
+
 config PSE_REGULATOR
 	tristate "Regulator based PSE controller"
 	help
diff --git a/drivers/net/pse-pd/Makefile b/drivers/net/pse-pd/Makefile
index bf35e2a5b110..ef869bba5ed9 100644
--- a/drivers/net/pse-pd/Makefile
+++ b/drivers/net/pse-pd/Makefile
@@ -4,6 +4,7 @@
 obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
 
 obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
+obj-$(CONFIG_PSE_REALTEK_MCU_I2C) += realtek-pse-mcu-i2c.o
 obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
 obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
 obj-$(CONFIG_PSE_SI3474) += si3474.o
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-i2c.c b/drivers/net/pse-pd/realtek-pse-mcu-i2c.c
new file mode 100644
index 000000000000..6e6e3645c509
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-i2c.c
@@ -0,0 +1,163 @@
+// 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-mcu.h"
+
+/*
+ * The core has already waited RTPSE_MCU_RESPONSE_MS before calling us, so
+ * the response is normally ready on the very first read. For commands the
+ * MCU produces more slowly, keep polling at the typical response cadence
+ * up to the worst-case ceiling.
+ */
+#define RTPSE_MCU_I2C_RETRY_MS	RTPSE_MCU_RESPONSE_MS
+#define RTPSE_MCU_I2C_MAX_TRIES	(RTPSE_MCU_RESPONSE_MAX_MS / RTPSE_MCU_I2C_RETRY_MS)
+
+static int rtpse_mcu_i2c_smbus_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+	struct i2c_client *client = to_i2c_client(pse->dev);
+
+	/* Send opcode as SMBus command byte; remaining 11 bytes as block data */
+	return i2c_smbus_write_i2c_block_data(client, req->opcode, RTPSE_MCU_MSG_SIZE - 1,
+					      (u8 *)req + 1);
+}
+
+static int rtpse_mcu_i2c_smbus_recv(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+				    struct rtpse_mcu_msg *resp)
+{
+	struct i2c_client *client = to_i2c_client(pse->dev);
+	int tries, ret;
+
+	for (tries = 0; tries < RTPSE_MCU_I2C_MAX_TRIES; tries++) {
+		if (tries > 0)
+			msleep(RTPSE_MCU_I2C_RETRY_MS);
+
+		/* MCU needs 0x00 as command byte for read */
+		ret = i2c_smbus_read_i2c_block_data(client, 0x00,
+						    RTPSE_MCU_MSG_SIZE,
+						    (u8 *)resp);
+		if (ret < 0)
+			return ret;
+		if (ret == RTPSE_MCU_MSG_SIZE && rtpse_mcu_resp_is_final(req, resp))
+			return 0;
+	}
+
+	return -ETIMEDOUT;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_i2c_smbus_ops = {
+	.send = rtpse_mcu_i2c_smbus_send,
+	.recv = rtpse_mcu_i2c_smbus_recv,
+};
+
+static int rtpse_mcu_i2c_native_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+	struct i2c_client *client = to_i2c_client(pse->dev);
+	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_mcu_i2c_native_recv(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req,
+				     struct rtpse_mcu_msg *resp)
+{
+	struct i2c_client *client = to_i2c_client(pse->dev);
+	int tries, ret;
+
+	for (tries = 0; tries < RTPSE_MCU_I2C_MAX_TRIES; tries++) {
+		if (tries > 0)
+			msleep(RTPSE_MCU_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 && rtpse_mcu_resp_is_final(req, resp))
+			return 0;
+	}
+
+	return -ETIMEDOUT;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_i2c_native_ops = {
+	.send = rtpse_mcu_i2c_native_send,
+	.recv = rtpse_mcu_i2c_native_recv,
+};
+
+static int rtpse_mcu_i2c_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	const struct rtpse_mcu_match_data *match;
+	struct rtpse_mcu_ctrl *pse;
+	bool use_native = false;
+	int ret;
+
+	match = device_get_match_data(dev);
+	if (!match)
+		return dev_err_probe(dev, -ENODEV, "missing match data\n");
+
+	if (rtpse_mcu_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->pcdev.owner = THIS_MODULE;
+	pse->transport = use_native ? &rtpse_mcu_i2c_native_ops : &rtpse_mcu_i2c_smbus_ops;
+
+	return rtpse_mcu_register(pse);
+}
+
+static const struct of_device_id rtpse_mcu_i2c_of_match[] = {
+	{ .compatible = "realtek,pse-mcu-rtk", .data = &rtpse_mcu_rtk_data },
+	{ .compatible = "realtek,pse-mcu-brcm", .data = &rtpse_mcu_brcm_data },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rtpse_mcu_i2c_of_match);
+
+static struct i2c_driver rtpse_mcu_i2c_driver = {
+	.driver = {
+		.name		= "realtek-pse-mcu-i2c",
+		.of_match_table	= rtpse_mcu_i2c_of_match,
+	},
+	.probe		= rtpse_mcu_i2c_probe,
+};
+module_i2c_driver(rtpse_mcu_i2c_driver);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek/Broadcom PSE MCU driver (I2C transport)");
+MODULE_LICENSE("GPL");
-- 
2.51.0


^ permalink raw reply related

* [PATCH net-next v5 4/4] net: pse-pd: realtek-pse-mcu: add UART transport
From: Jonas Jelonek @ 2026-07-06 11:24 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
In-Reply-To: <20260706112425.3149226-1-jelonek.jonas@gmail.com>

Add the serdev (UART) transport for the Realtek/Broadcom PSE MCU core. It
registers the MCU as a serdev device and provides the send/recv callbacks
the core uses to exchange the 12-byte frames, receiving asynchronously via
the serdev receive_buf callback.

The baud rate defaults to 19200 and can be overridden per board with the
"current-speed" property.

Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
---
 drivers/net/pse-pd/Kconfig                |  11 ++
 drivers/net/pse-pd/Makefile               |   1 +
 drivers/net/pse-pd/realtek-pse-mcu-uart.c | 156 ++++++++++++++++++++++
 3 files changed, 168 insertions(+)
 create mode 100644 drivers/net/pse-pd/realtek-pse-mcu-uart.c

diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index 7074d6733ff3..23e44dde3dbf 100644
--- a/drivers/net/pse-pd/Kconfig
+++ b/drivers/net/pse-pd/Kconfig
@@ -30,6 +30,17 @@ config PSE_REALTEK_MCU_I2C
 	  PSE silicon is not accessed directly. To compile this driver as a
 	  module, choose M here: the module will be called realtek-pse-mcu-i2c.
 
+config PSE_REALTEK_MCU_UART
+	tristate "Realtek/Broadcom PSE MCU driver (UART transport)"
+	depends on SERIAL_DEV_BUS
+	select PSE_REALTEK_MCU
+	help
+	  Driver for the microcontroller (MCU) that fronts the PSE
+	  hardware on 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-mcu-uart.
+
 config PSE_REGULATOR
 	tristate "Regulator based PSE controller"
 	help
diff --git a/drivers/net/pse-pd/Makefile b/drivers/net/pse-pd/Makefile
index ef869bba5ed9..9cca5900fe34 100644
--- a/drivers/net/pse-pd/Makefile
+++ b/drivers/net/pse-pd/Makefile
@@ -5,6 +5,7 @@ obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
 
 obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
 obj-$(CONFIG_PSE_REALTEK_MCU_I2C) += realtek-pse-mcu-i2c.o
+obj-$(CONFIG_PSE_REALTEK_MCU_UART) += realtek-pse-mcu-uart.o
 obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
 obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
 obj-$(CONFIG_PSE_SI3474) += si3474.o
diff --git a/drivers/net/pse-pd/realtek-pse-mcu-uart.c b/drivers/net/pse-pd/realtek-pse-mcu-uart.c
new file mode 100644
index 000000000000..ef04e0d92963
--- /dev/null
+++ b/drivers/net/pse-pd/realtek-pse-mcu-uart.c
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <linux/cleanup.h>
+#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/spinlock.h>
+#include <linux/string.h>
+
+#include "realtek-pse-mcu.h"
+
+#define RTPSE_MCU_UART_BAUD_DEFAULT	19200
+#define RTPSE_MCU_UART_TX_TIMEOUT	msecs_to_jiffies(100)
+#define RTPSE_MCU_UART_RX_TIMEOUT	msecs_to_jiffies(RTPSE_MCU_RESPONSE_MAX_MS)
+
+struct rtpse_mcu_uart {
+	struct rtpse_mcu_ctrl pse;
+	struct serdev_device *serdev;
+	struct completion rx_done;
+	spinlock_t rx_lock;		/* protects rx_buf and rx_len */
+	size_t rx_len;
+	u8 rx_buf[RTPSE_MCU_MSG_SIZE];
+};
+
+#define to_rtpse_mcu_uart(p)  container_of(p, struct rtpse_mcu_uart, pse)
+
+/*
+ * No framing is done here: a glitched frame costs one transaction, then
+ * the next _send re-frames from rx_len 0. Resync works by returning count
+ * (not take), dropping any overflow so serdev keeps no leftover to bleed
+ * into the next frame.
+ */
+static size_t rtpse_mcu_uart_receive(struct serdev_device *serdev,
+				     const u8 *buf, size_t count)
+{
+	struct rtpse_mcu_uart *ctx = serdev_device_get_drvdata(serdev);
+	size_t take;
+
+	scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+		take = min(count, sizeof(ctx->rx_buf) - ctx->rx_len);
+		if (take) {
+			memcpy(ctx->rx_buf + ctx->rx_len, buf, take);
+			ctx->rx_len += take;
+			if (ctx->rx_len == sizeof(ctx->rx_buf))
+				complete(&ctx->rx_done);
+		}
+	}
+
+	/* consume all to avoid desync/misalignment */
+	return count;
+}
+
+static const struct serdev_device_ops rtpse_mcu_uart_serdev_ops = {
+	.receive_buf = rtpse_mcu_uart_receive,
+	.write_wakeup = serdev_device_write_wakeup,
+};
+
+static int rtpse_mcu_uart_send(struct rtpse_mcu_ctrl *pse, const struct rtpse_mcu_msg *req)
+{
+	struct rtpse_mcu_uart *ctx = to_rtpse_mcu_uart(pse);
+	int written;
+
+	/* clear any leftover rx state before transmitting */
+	scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+		reinit_completion(&ctx->rx_done);
+		ctx->rx_len = 0;
+	}
+
+	written = serdev_device_write(ctx->serdev, (const u8 *)req, sizeof(*req),
+				      RTPSE_MCU_UART_TX_TIMEOUT);
+	if (written < 0)
+		return written;
+	if (written != sizeof(*req))
+		return -EIO;
+
+	return 0;
+}
+
+static int rtpse_mcu_uart_recv(struct rtpse_mcu_ctrl *pse,
+			       const struct rtpse_mcu_msg *req,
+			       struct rtpse_mcu_msg *resp)
+{
+	struct rtpse_mcu_uart *ctx = to_rtpse_mcu_uart(pse);
+
+	if (!wait_for_completion_timeout(&ctx->rx_done, RTPSE_MCU_UART_RX_TIMEOUT))
+		return -ETIMEDOUT;
+
+	scoped_guard(spinlock_irqsave, &ctx->rx_lock) {
+		if (ctx->rx_len != sizeof(*resp))
+			return -EIO;
+
+		memcpy(resp, ctx->rx_buf, sizeof(*resp));
+	}
+	return 0;
+}
+
+static const struct rtpse_mcu_transport_ops rtpse_mcu_uart_transport_ops = {
+	.send = rtpse_mcu_uart_send,
+	.recv = rtpse_mcu_uart_recv,
+};
+
+static int rtpse_mcu_uart_probe(struct serdev_device *serdev)
+{
+	u32 speed = RTPSE_MCU_UART_BAUD_DEFAULT;
+	struct device *dev = &serdev->dev;
+	struct rtpse_mcu_uart *ctx;
+	int ret;
+
+	ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);
+	if (!ctx)
+		return -ENOMEM;
+
+	ctx->serdev = serdev;
+	ctx->pse.dev = dev;
+	ctx->pse.pcdev.owner = THIS_MODULE;
+	ctx->pse.transport = &rtpse_mcu_uart_transport_ops;
+	init_completion(&ctx->rx_done);
+	spin_lock_init(&ctx->rx_lock);
+
+	serdev_device_set_drvdata(serdev, ctx);
+	serdev_device_set_client_ops(serdev, &rtpse_mcu_uart_serdev_ops);
+
+	ret = devm_serdev_device_open(dev, serdev);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to open serdev\n");
+
+	fwnode_property_read_u32(dev_fwnode(dev), "current-speed", &speed);
+	serdev_device_set_baudrate(serdev, speed);
+	serdev_device_set_flow_control(serdev, false);
+	serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
+
+	return rtpse_mcu_register(&ctx->pse);
+}
+
+static const struct of_device_id rtpse_mcu_uart_of_match[] = {
+	{ .compatible = "realtek,pse-mcu-rtk", .data = &rtpse_mcu_rtk_data },
+	{ .compatible = "realtek,pse-mcu-brcm", .data = &rtpse_mcu_brcm_data },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rtpse_mcu_uart_of_match);
+
+static struct serdev_device_driver rtpse_mcu_uart_driver = {
+	.driver = {
+		.name = "realtek-pse-mcu-uart",
+		.of_match_table = rtpse_mcu_uart_of_match,
+	},
+	.probe  = rtpse_mcu_uart_probe,
+};
+module_serdev_device_driver(rtpse_mcu_uart_driver);
+
+MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
+MODULE_DESCRIPTION("Realtek/Broadcom PSE MCU driver (UART transport)");
+MODULE_LICENSE("GPL");
-- 
2.51.0


^ permalink raw reply related

* Re: [syzbot] Monthly virt report (Jun 2026)
From: mawupeng @ 2026-07-06 11:31 UTC (permalink / raw)
  To: sgarzare
  Cc: mawupeng1, syzbot+listbf7b8eeeb8dda31d6de1, linux-kernel,
	syzkaller-bugs, virtualization, mst, jasowang, xuanzhuo, eperezma,
	stefanha, davem, edumazet, kuba, pabeni, horms, kvm, netdev
In-Reply-To: <CAGxU2F582RpWGOUxTU783zZuxWj7e4eZtiDcNdUmWobLSFSq2g@mail.gmail.com>



On 周一 2026-7-6 17:24, Stefano Garzarella wrote:
> On Thu, 2 Jul 2026 at 04:55, mawupeng <mawupeng1@huawei.com> wrote:
>> On 周四 2026-6-25 04:32, syzbot wrote:
>>> Hello virt maintainers/developers,
>>>
>>> This is a 31-day syzbot report for the virt subsystem.
>>> All related reports/information can be found at:
>>> https://syzkaller.appspot.com/upstream/s/virt
>>>
>>> During the period, 0 new issues were detected and 0 were fixed.
>>> In total, 5 issues are still open and 61 have already been fixed.
>>> There are also 2 low-priority issues.
>>>
>>> Some of the still happening issues:
>>>
>>> Ref Crashes Repro Title
>>> <1> 24      No    WARNING: refcount bug in call_timer_fn (4)
>>>                   https://syzkaller.appspot.com/bug?extid=07dcf509f4c013e25dc5
>>> <2> 3       Yes   memory leak in __vsock_create (2)
>>>                   https://syzkaller.appspot.com/bug?extid=1b2c9c4a0f8708082678
>>
>> Hi,
>>
>> This is regarding the still-open "memory leak in __vsock_create (2)"
>> bug (#2 in the monthly virt report, extid 1b2c9c4a0f8708082678):
>>   https://syzkaller.appspot.com/bug?extid=1b2c9c4a0f8708082678
>>
>> I spent some time analyzing the root cause and the previous fix
>> attempt; below is a summary and a direction that tested out.
> 
> [...]
> 
>>
>> I'm not subscribed to follow the list at full volume; happy to send a
>> formal patch (with the af_vsock.h / pending_work changes folded in)
>> if the direction looks right to the maintainers.
> 
> Yes, please, a formal patch with a great commit message is much better
> than a long text to read IMO.

Thanks for replying.

As we conducted more thorough testing and further analysis of our own fix,
we found that it is not as comprehensive as the existing AI-generated fix
provided[1].

Could you please let us know if there are any issues with this AI-generated fix?

[1]: https://syzkaller.appspot.com/ai_job?id=cd978d21-2110-4f57-9525-d7d91c4a7e02

> 
> Thanks,
> Stefano
> 
> 


^ permalink raw reply

* Re: [PATCH net] ptp: netc: explicitly clear TMR_OFF during initialization
From: Vadim Fedorenko @ 2026-07-06 11:43 UTC (permalink / raw)
  To: wei.fang, richardcochran, xiaoning.wang, andrew+netdev, davem,
	edumazet, kuba, pabeni, Frank.Li
  Cc: netdev, imx, wei.fang
In-Reply-To: <20260706081232.3661826-1-wei.fang@oss.nxp.com>

On 06/07/2026 09:12, wei.fang@oss.nxp.com wrote:
> From: Clark Wang <xiaoning.wang@nxp.com>
> 
> The NETC timer does not support function level reset, so TMR_OFF_L/H
> registers are not cleared by pcie_flr(). If TMR_OFF was set to a
> non-zero value in a previous binding, it will persist across driver
> rebind and cause inaccurate PTP time.
> 
> There is also a hardware issue: after a warm reset or soft reset,
> TMR_OFF_L/H registers appear to be cleared to zero, but the timer clock
> domain internally retains the stale value. When the timer is re-enabled,
> TMR_CUR_TIME continues to track the old offset until TMR_OFF is written
> explicitly. This can cause incorrect PTP timestamps and even PTP clock
> synchronization failures.
> 
> Per the recommendation from the IP team, explicitly write 0 to TMR_OFF
> in netc_timer_init() to flush the internally cached value and ensure
> TMR_CUR_TIME follows the freshly initialized counter.
> 
> Fixes: 87a201d59963 ("ptp: netc: add NETC V4 Timer PTP driver support")
> Signed-off-by: Clark Wang <xiaoning.wang@nxp.com>
> Signed-off-by: Wei Fang <wei.fang@nxp.com>
> ---
>   drivers/ptp/ptp_netc.c | 1 +
>   1 file changed, 1 insertion(+)
> 
> diff --git a/drivers/ptp/ptp_netc.c b/drivers/ptp/ptp_netc.c
> index 94e952ee6990..5e381c354d74 100644
> --- a/drivers/ptp/ptp_netc.c
> +++ b/drivers/ptp/ptp_netc.c
> @@ -779,6 +779,7 @@ static void netc_timer_init(struct netc_timer *priv)
>   	netc_timer_wr(priv, NETC_TMR_FIPER_CTRL, fiper_ctrl);
>   	netc_timer_wr(priv, NETC_TMR_ECTRL, NETC_TMR_DEFAULT_ETTF_THR);
>   
> +	netc_timer_offset_write(priv, 0);
>   	ktime_get_real_ts64(&now);
>   	ns = timespec64_to_ns(&now);
>   	netc_timer_cnt_write(priv, ns);

Reviewed-by: Vadim Fedorenko <vadim.fedorenko@linux.dev>

^ permalink raw reply

* Re: [PATCH net-next v6 2/5] dpll: add DPLL_PIN_TYPE_INT_NCO pin type
From: Vadim Fedorenko @ 2026-07-06 11:47 UTC (permalink / raw)
  To: Paolo Abeni, Ivan Vecera, netdev
  Cc: Jiri Pirko, David S. Miller, Donald Hunter, Eric Dumazet,
	Jakub Kicinski, Jiri Pirko, Michal Schmidt, Pasi Vaananen,
	Arkadiusz Kubalewski, Petr Oros, Prathosh Satish, Simon Horman,
	linux-kernel
In-Reply-To: <455c46ac-25cb-45d4-9cb3-5330f7b553c0@redhat.com>

On 06/07/2026 10:31, Paolo Abeni wrote:
> On 6/30/26 2:55 PM, Ivan Vecera wrote:
>> Add DPLL_PIN_TYPE_INT_NCO pin type for virtual pins representing
>> the NCO mode of a DPLL. When connected as a DPLL input, the DPLL
>> enters NCO mode where the output frequency is adjusted by the host
>> via the PTP clock interface.
>>
>> Update the fractional-frequency-offset and fractional-frequency-
>> offset-ppt attribute documentation to note that for INT_NCO pins
>> these attributes represent the DPLL's current output frequency
>> offset from its nominal frequency.
>>
>> Reviewed-by: Jiri Pirko <jiri@nvidia.com>
>> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> 
> @Vadim: it's not clear to me if your doubts over the design choice
> raised in previous iterations are still there. My understanding is there
> is reasonable agreement vs the new pin type, if you have very strong
> objections please raise them soonish.

@Paolo: I still have my doubts here, but the patchset has Rb tags from
Jiri, who is DPLL maintainer as well, so I think it's good to go...



^ permalink raw reply

* [PATCH nf] netfilter: nf_conncount: fix zone comparison in tuple dedup
From: Yizhou Zhao @ 2026-07-06 11:48 UTC (permalink / raw)
  To: netfilter-devel
  Cc: Yizhou Zhao, Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, coreteam, netdev, linux-kernel, Yuxiang Yang,
	Ao Wang, Xuewei Feng, Qi Li, Ke Xu, stable

The "already exists" dedup logic in __nf_conncount_add() decides
whether a connection has already been counted and can be skipped instead
of incrementing the connlimit count.  It compares the conntrack zone of a
list entry with the zone of the connection being added using
nf_ct_zone_id() and nf_ct_zone_equal(), passing conn->zone.dir or
zone->dir as the direction argument.

Those helpers take enum ip_conntrack_dir values: IP_CT_DIR_ORIGINAL is 0
and IP_CT_DIR_REPLY is 1.  However, zone->dir is a u8 bitmask:
NF_CT_ZONE_DIR_ORIG is 1, NF_CT_ZONE_DIR_REPL is 2 and
NF_CT_DEFAULT_ZONE_DIR is 3.  Passing that bitmask as the enum direction
shifts the meaning of every non-zero value.  An ORIG-only zone passes 1
and is tested as REPLY, while REPL-only and default zones pass 2 or 3 and
test bits beyond the valid direction range.  In those cases
nf_ct_zone_id() can fall back to NF_CT_DEFAULT_ZONE_ID instead of using
the real zone id, so different zones can be treated as equal and dedup
collapses to tuple equality alone.

Do not special-case NF_CT_DEFAULT_ZONE_DIR and do not compare raw zone
ids: that would address only the common bidirectional case and would
bypass the direction-aware NF_CT_DEFAULT_ZONE_ID fallback.  Instead, add
a small conncount-local helper that converts the zone direction bitmask
to a valid enum direction before calling the existing zone helpers.  A
default bidirectional zone contains the ORIG bit, so it naturally maps to
IP_CT_DIR_ORIGINAL; single-direction zones continue to use the existing
nf_ct_zone_id() fallback semantics.

Fixes: 21ba8847f857 ("netfilter: nf_conncount: Fix garbage collection with zones")
Fixes: b36e4523d4d5 ("netfilter: nf_conncount: fix garbage collection confirm race")
Cc: stable@vger.kernel.org
Reported-by: Yizhou Zhao <zhaoyz24@mails.tsinghua.edu.cn>
Reported-by: Yuxiang Yang <yangyx22@mails.tsinghua.edu.cn>
Reported-by: Ao Wang <wangao@seu.edu.cn>
Reported-by: Xuewei Feng <fengxw06@126.com>
Reported-by: Qi Li <qli01@tsinghua.edu.cn>
Reported-by: Ke Xu <xuke@tsinghua.edu.cn>
Assisted-by: Claude-Code:GLM-5.2
Signed-off-by: Yizhou Zhao <zhaoyz24@mails.tsinghua.edu.cn>
---
diff --git a/net/netfilter/nf_conncount.c b/net/netfilter/nf_conncount.c
index 91582069f6d2..eb3156782405 100644
--- a/net/netfilter/nf_conncount.c
+++ b/net/netfilter/nf_conncount.c
@@ -127,6 +127,15 @@ find_or_evict(struct net *net, struct nf_conncount_list *list,
 	return ERR_PTR(-EAGAIN);
 }
 
+static enum ip_conntrack_dir
+nf_conncount_zone_dir(const struct nf_conntrack_zone *zone)
+{
+	if (zone->dir & NF_CT_ZONE_DIR_ORIG)
+		return IP_CT_DIR_ORIGINAL;
+
+	return IP_CT_DIR_REPLY;
+}
+
 static bool get_ct_or_tuple_from_skb(struct net *net,
 				     const struct sk_buff *skb,
 				     u16 l3num,
@@ -211,8 +220,10 @@ static int __nf_conncount_add(struct net *net,
 			/* Not found, but might be about to be confirmed */
 			if (PTR_ERR(found) == -EAGAIN) {
 				if (nf_ct_tuple_equal(&conn->tuple, &tuple) &&
-				    nf_ct_zone_id(&conn->zone, conn->zone.dir) ==
-				    nf_ct_zone_id(zone, zone->dir))
+				    nf_ct_zone_id(&conn->zone,
+						  nf_conncount_zone_dir(&conn->zone)) ==
+				    nf_ct_zone_id(zone,
+						  nf_conncount_zone_dir(zone)))
 					goto out_put; /* already exists */
 			} else {
 				collect++;
@@ -223,7 +234,7 @@ static int __nf_conncount_add(struct net *net,
 		found_ct = nf_ct_tuplehash_to_ctrack(found);
 
 		if (nf_ct_tuple_equal(&conn->tuple, &tuple) &&
-		    nf_ct_zone_equal(found_ct, zone, zone->dir)) {
+		    nf_ct_zone_equal(found_ct, zone, nf_conncount_zone_dir(zone))) {
 			/*
 			 * We should not see tuples twice unless someone hooks
 			 * this into a table without "-p tcp --syn".

--
2.47.3


^ permalink raw reply related

* Re: [PATCH net-next] net/mlx5e: bound TX CQ poll softirq residency with a time budget
From: Dragos Tatulea @ 2026-07-06 11:48 UTC (permalink / raw)
  To: Jose Fernandez (Anthropic), Saeed Mahameed, Tariq Toukan,
	Mark Bloch, Leon Romanovsky, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni
  Cc: netdev, linux-rdma, linux-kernel, Ben Cressey
In-Reply-To: <20260703-mlx5e-tx-cq-time-budget-v1-1-6da2cfe9c7b1@linux.dev>

Hi Jose,

Thanks for your patch.

On 03.07.26 03:36, Jose Fernandez (Anthropic) wrote:
> Under strict IOMMU invalidation (iommu.strict=1), each per-fragment DMA
> unmap in the TX completion path issues a synchronous TLB invalidate and
> waits for CMD_SYNC, spinning IRQ-off in the SMMU command queue. Under
> cross-CPU command-queue contention this per-unmap cost inflates from
> microseconds to hundreds of microseconds. mlx5e_poll_tx_cq()'s per-CQE
> budget (128) does not bound time in this regime: one CQE can cover a
> multi-WQE batch with many fragments, so a single poll invocation can
> accumulate seconds of softirq residency and trip the soft-lockup
> watchdog on arm64/SMMU-v3 systems.
> 
This is interesting. I can think of 2 cases here which can overlap:

1) Packets are sent with xmit_more which trigger this many WQEs per
  CQE scenario.

2) TSO packets can have many fragments so even a single WQE will have
   more than one DMA unmap. For the default GSO size of 64K each TSO
   skb will have 2 fragments on your platform. If the GSO is bumped to
   256K then you usually get ~5 fragments.

Let's first understand if this is a case of 1) (small packets sent
with xmit_more) or 2) (many TSO packets) or 1+2) (many TSO packets sent
with xmit_more).

If xmit_more is used then you can actually tune the BQL to avoid this
behavior which seems to wreak havoc on your configuration.

> Bound the invocation by time: check local_clock() every 8 CQEs against
> a budget (default 500us; module parameter tx_cq_time_budget_us,
> runtime-writable, 0 disables) and break out of the CQE loop when
> exceeded, reporting busy exactly like the existing CQE-budget
> exhaustion path so NAPI keeps the poll scheduled. Remaining
> completions are delayed by one reschedule, never stranded. The inner
> WQE walk is never interrupted mid-CQE (sqcc/dma_fifo_cc accounting).
> A new ethtool statistic (tx_time_budget_exit) counts early exits.
> 
> Also add cond_resched() in mlx5e_free_txqsq_descs(): the teardown path
> walks the same per-fragment unmaps in process context.
> 
> Tested on arm64 with SMMU-v3 under strict mode: throughput cost is
> within run-to-run variance at every measured load shape; under active
> invalidation-storm contention, the bounded poll measures 35-50%
> faster than unbounded (bounded polling yields cores back to the
> transmit path).
> 
The bounded poll is faster because it is interrupted earlier or is
it faster because it results in less contention on the IOMMU?

> Assisted-by: Claude:unspecified
> Signed-off-by: Jose Fernandez (Anthropic) <jose.fernandez@linux.dev>
> Reviewed-by: Ben Cressey <ben@cressey.dev>
> ---
>  drivers/net/ethernet/mellanox/mlx5/core/en_stats.c |  5 ++++
>  drivers/net/ethernet/mellanox/mlx5/core/en_stats.h |  2 ++
>  drivers/net/ethernet/mellanox/mlx5/core/en_tx.c    | 29 +++++++++++++++++++++-
>  3 files changed, 35 insertions(+), 1 deletion(-)
> 
> diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_stats.c b/drivers/net/ethernet/mellanox/mlx5/core/en_stats.c
> index 7f33261ba655..b940280af19d 100644
> --- a/drivers/net/ethernet/mellanox/mlx5/core/en_stats.c
> +++ b/drivers/net/ethernet/mellanox/mlx5/core/en_stats.c
> @@ -171,6 +171,7 @@ static const struct counter_desc sw_stats_desc[] = {
>  	{ MLX5E_DECLARE_STAT(struct mlx5e_sw_stats, tx_cqes) },
>  	{ MLX5E_DECLARE_STAT(struct mlx5e_sw_stats, tx_queue_wake) },
>  	{ MLX5E_DECLARE_STAT(struct mlx5e_sw_stats, tx_cqe_err) },
> +	{ MLX5E_DECLARE_STAT(struct mlx5e_sw_stats, tx_time_budget_exit) },
>  	{ MLX5E_DECLARE_STAT(struct mlx5e_sw_stats, tx_xdp_xmit) },
>  	{ MLX5E_DECLARE_STAT(struct mlx5e_sw_stats, tx_xdp_mpwqe) },
>  	{ MLX5E_DECLARE_STAT(struct mlx5e_sw_stats, tx_xdp_inlnw) },
> @@ -426,6 +427,7 @@ static void mlx5e_stats_grp_sw_update_stats_sq(struct mlx5e_sw_stats *s,
>  	s->tx_queue_wake            += sq_stats->wake;
>  	s->tx_queue_dropped         += sq_stats->dropped;
>  	s->tx_cqe_err               += sq_stats->cqe_err;
> +	s->tx_time_budget_exit      += sq_stats->time_budget_exit;
>  	s->tx_recover               += sq_stats->recover;
>  	s->tx_xmit_more             += sq_stats->xmit_more;
>  	s->tx_csum_partial_inner    += sq_stats->csum_partial_inner;
> @@ -2323,6 +2325,7 @@ static const struct counter_desc sq_stats_desc[] = {
>  	{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, cqes) },
>  	{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, wake) },
>  	{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, cqe_err) },
> +	{ MLX5E_DECLARE_TX_STAT(struct mlx5e_sq_stats, time_budget_exit) },
>  };
>  
>  static const struct counter_desc rq_xdpsq_stats_desc[] = {
> @@ -2399,6 +2402,7 @@ static const struct counter_desc ptp_sq_stats_desc[] = {
>  	{ MLX5E_DECLARE_PTP_TX_STAT(struct mlx5e_sq_stats, cqes) },
>  	{ MLX5E_DECLARE_PTP_TX_STAT(struct mlx5e_sq_stats, wake) },
>  	{ MLX5E_DECLARE_PTP_TX_STAT(struct mlx5e_sq_stats, cqe_err) },
> +	{ MLX5E_DECLARE_PTP_TX_STAT(struct mlx5e_sq_stats, time_budget_exit) },
>  };
>  
>  static const struct counter_desc ptp_ch_stats_desc[] = {
> @@ -2476,6 +2480,7 @@ static const struct counter_desc qos_sq_stats_desc[] = {
>  	{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, cqes) },
>  	{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, wake) },
>  	{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, cqe_err) },
> +	{ MLX5E_DECLARE_QOS_TX_STAT(struct mlx5e_sq_stats, time_budget_exit) },
>  };
>  
>  #define NUM_RQ_STATS			ARRAY_SIZE(rq_stats_desc)
> diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_stats.h b/drivers/net/ethernet/mellanox/mlx5/core/en_stats.h
> index 09f155acb461..5ba954f42ccd 100644
> --- a/drivers/net/ethernet/mellanox/mlx5/core/en_stats.h
> +++ b/drivers/net/ethernet/mellanox/mlx5/core/en_stats.h
> @@ -187,6 +187,7 @@ struct mlx5e_sw_stats {
>  	u64 tx_cqes;
>  	u64 tx_queue_wake;
>  	u64 tx_cqe_err;
> +	u64 tx_time_budget_exit;
>  	u64 tx_xdp_xmit;
>  	u64 tx_xdp_mpwqe;
>  	u64 tx_xdp_inlnw;
> @@ -445,6 +446,7 @@ struct mlx5e_sq_stats {
>  	u64 cqes ____cacheline_aligned_in_smp;
>  	u64 wake;
>  	u64 cqe_err;
> +	u64 time_budget_exit;
>  };
>  
>  struct mlx5e_xdpsq_stats {
> diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_tx.c b/drivers/net/ethernet/mellanox/mlx5/core/en_tx.c
> index 0b5e600e4a6a..994df912b765 100644
> --- a/drivers/net/ethernet/mellanox/mlx5/core/en_tx.c
> +++ b/drivers/net/ethernet/mellanox/mlx5/core/en_tx.c
> @@ -43,6 +43,13 @@
>  #include "en_accel/macsec.h"
>  #include "en/ptp.h"
>  #include <net/ipv6.h>
> +#include <linux/moduleparam.h>
> +#include <linux/sched/clock.h>
> +
> +static unsigned int mlx5e_tx_cq_time_budget_us = 500;
> +module_param_named(tx_cq_time_budget_us, mlx5e_tx_cq_time_budget_us, uint, 0644);
> +MODULE_PARM_DESC(tx_cq_time_budget_us,
> +		 "Max microseconds one TX CQ poll may spend before yielding (0 = unbounded)");
>  
>  static void mlx5e_dma_unmap_wqe_err(struct mlx5e_txqsq *sq, u8 num_dma)
>  {
> @@ -760,9 +767,12 @@ void mlx5e_txqsq_wake(struct mlx5e_txqsq *sq)
>  bool mlx5e_poll_tx_cq(struct mlx5e_cq *cq, int napi_budget)
>  {
>  	struct mlx5e_sq_stats *stats;
> +	bool time_exceeded = false;
> +	u64 time_budget_end = 0;
>  	struct mlx5e_txqsq *sq;
>  	struct mlx5_cqe64 *cqe;
>  	u32 dma_fifo_cc;
> +	u32 budget_us;
>  	u32 nbytes;
>  	u16 npkts;
>  	u16 sqcc;
> @@ -790,6 +800,10 @@ bool mlx5e_poll_tx_cq(struct mlx5e_cq *cq, int napi_budget)
>  	/* avoid dirtying sq cache line every cqe */
>  	dma_fifo_cc = sq->dma_fifo_cc;
>  
> +	budget_us = READ_ONCE(mlx5e_tx_cq_time_budget_us);
> +	if (budget_us)
> +		time_budget_end = local_clock() + (u64)budget_us * NSEC_PER_USEC;
> +
>  	i = 0;
>  	do {
>  		struct mlx5e_tx_wqe_info *wi;
> @@ -842,8 +856,19 @@ bool mlx5e_poll_tx_cq(struct mlx5e_cq *cq, int napi_budget)
>  			stats->cqe_err++;
>  		}
>  
> +		/* Check between CQEs only (sqcc/dma_fifo_cc must advance together). */
> +		if (unlikely(time_budget_end && (i & 7) == 7 &&
> +			     local_clock() >= time_budget_end)) {
> +			time_exceeded = true;
> +			i++;
> +			break;
> +		}
> +
>  	} while ((++i < MLX5E_TX_CQ_POLL_BUDGET) && (cqe = mlx5_cqwq_get_cqe(&cq->wq)));
>  
> +	if (unlikely(time_exceeded))
> +		stats->time_budget_exit++;
> +
>  	stats->cqes += i;
>  
>  	mlx5_cqwq_update_db_record(&cq->wq);
> @@ -858,7 +883,7 @@ bool mlx5e_poll_tx_cq(struct mlx5e_cq *cq, int napi_budget)
>  
>  	mlx5e_txqsq_wake(sq);
>  
> -	return (i == MLX5E_TX_CQ_POLL_BUDGET);
> +	return time_exceeded || (i == MLX5E_TX_CQ_POLL_BUDGET);
>  }
>  
This change is too invasive in the driver for all the other cases. We have to figure
another way to go about this issue.

Thanks,
Dragos

^ permalink raw reply

* Re: [PATCH net] net: openvswitch: reject oversized nested action attrs
From: Eelco Chaudron @ 2026-07-06 11:49 UTC (permalink / raw)
  To: Asim Viladi Oglu Manizada
  Cc: netdev, dev, aconole, i.maximets, davem, edumazet, kuba, pabeni,
	horms, stable
In-Reply-To: <20260706094336.38639-1-manizada@pm.me>



On 6 Jul 2026, at 11:44, Asim Viladi Oglu Manizada wrote:

> Open vSwitch stores generated flow actions as nlattrs, whose nla_len
> field is u16. Commit a1e64addf3ff ("net: openvswitch: remove
> misbehaving actions length check") allowed the total sw_flow_actions
> stream to grow beyond 64 KiB, which is valid, but also removed the last
> guard preventing a generated nested action attribute from exceeding
> U16_MAX.
>
> An oversized generated container can thus be closed with a truncated
> nla_len. A later dump or teardown then walks a structurally different
> stream than the one that was validated. In particular, an oversized
> nested CLONE/CT action may cause subsequent bytes in the generated
> stream to be interpreted as independent actions.
>
> Keep the larger total-action-stream behavior, but make nested action
> close reject generated containers that do not fit in nla_len, and return
> the error through all callers. For recursive SAMPLE, CLONE, DEC_TTL, and
> CHECK_PKT_LEN builders, trim resource-owning action-list tails in reverse
> construction order before discarding failed wrappers, so resources copied
> into the rejected tails are released before the wrappers are removed.
>
> Most failed outer wrappers are discarded by truncating actions_len after
> child resources have been released. CHECK_PKT_LEN also trims its parent
> after branch resources are gone. SET/TUNNEL close failures unwind their
> known tun_dst ownership directly, and SET_TO_MASKED has no external
> ownership and truncates on close failure.
>
> Fixes: a1e64addf3ff ("net: openvswitch: remove misbehaving actions length check")
> Cc: stable@vger.kernel.org
> Assisted-by: avom-custom-harness:gpt-5.5-qwen3.6-mod-mix
> Signed-off-by: Asim Viladi Oglu Manizada <manizada@pm.me>

Thanks Asim for the patch, and Ilya for the offline review.

The changes look good to me.

Reviewed-by: Eelco Chaudron <echaudro@redhat.com>


^ permalink raw reply

* Re: [PATCH] octeon_ep_vf: Fix RX page leak on napi_build_skb() failure
From: Maciej Fijalkowski @ 2026-07-06 11:50 UTC (permalink / raw)
  To: Guangshuo Li
  Cc: Veerasenareddy Burru, Sathesh Edara, Shinas Rasheed,
	Satananda Burla, Andrew Lunn, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, David Carlier, netdev, linux-kernel,
	maoyixie.tju
In-Reply-To: <20260705031620.274396-1-lgs201920130244@gmail.com>

On Sun, Jul 05, 2026 at 11:16:20AM +0800, Guangshuo Li wrote:
> __octep_vf_oq_process_rx() clears buff_info->page before building an skb
> from the RX page. On the success path the page is consumed by the skb,
> either as the skb head or as an RX fragment.
> 
> If napi_build_skb() fails, however, the page is not consumed by an skb.
> The error path advances the descriptor and leaves the ring slot cleared,
> so the page is no longer tracked and is leaked. In the multi-fragment
> case, the remaining fragment pages are also unmapped and removed from
> their ring slots without being released.
> 
> Release the head page when napi_build_skb() fails, and release each
> remaining fragment page before clearing its ring slot.
> 
> Fixes: dd66b4285470 ("octeon_ep_vf: add NULL check for napi_build_skb()")
> Signed-off-by: Guangshuo Li <lgs201920130244@gmail.com>

This will conflict with:
https://lore.kernel.org/netdev/20260704061511.2350737-3-maoyixie.tju@gmail.com/

Maoyi, maybe you could take this and apply your changes on top and send a
v4?

> ---
>  drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c | 3 +++
>  1 file changed, 3 insertions(+)
> 
> diff --git a/drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c b/drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c
> index d98247408242..302559b16be7 100644
> --- a/drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c
> +++ b/drivers/net/ethernet/marvell/octeon_ep_vf/octep_vf_rx.c
> @@ -418,6 +418,7 @@ static int __octep_vf_oq_process_rx(struct octep_vf_device *oct,
>  			skb = napi_build_skb((void *)resp_hw, PAGE_SIZE);
>  			if (!skb) {
>  				oq->stats->alloc_failures++;
> +				put_page(virt_to_page(resp_hw));
>  				desc_used++;
>  				read_idx = octep_vf_oq_next_idx(oq, read_idx);
>  				continue;
> @@ -434,6 +435,7 @@ static int __octep_vf_oq_process_rx(struct octep_vf_device *oct,
>  			skb = napi_build_skb((void *)resp_hw, PAGE_SIZE);
>  			if (!skb) {
>  				oq->stats->alloc_failures++;
> +				put_page(virt_to_page(resp_hw));
>  				desc_used++;
>  				read_idx = octep_vf_oq_next_idx(oq, read_idx);
>  				data_len = buff_info->len - oq->max_single_buffer_size;
> @@ -442,6 +444,7 @@ static int __octep_vf_oq_process_rx(struct octep_vf_device *oct,
>  						       PAGE_SIZE, DMA_FROM_DEVICE);
>  					buff_info = (struct octep_vf_rx_buffer *)
>  						    &oq->buff_info[read_idx];
> +					put_page(buff_info->page);
>  					buff_info->page = NULL;
>  					if (data_len < oq->buffer_size)
>  						data_len = 0;
> -- 
> 2.43.0
> 
> 

^ permalink raw reply

* Re: [syzbot] Monthly virt report (Jun 2026)
From: Aleksandr Nogikh @ 2026-07-06 11:54 UTC (permalink / raw)
  To: mawupeng
  Cc: sgarzare, syzbot+listbf7b8eeeb8dda31d6de1, linux-kernel,
	syzkaller-bugs, virtualization, mst, jasowang, xuanzhuo, eperezma,
	stefanha, davem, edumazet, kuba, pabeni, horms, kvm, netdev
In-Reply-To: <0e51efaa-24b7-4a19-97f4-0669fd92d513@huawei.com>

On Mon, Jul 6, 2026 at 1:32 PM 'mawupeng' via syzkaller-bugs
<syzkaller-bugs@googlegroups.com> wrote:
>
>
>
> On 周一 2026-7-6 17:24, Stefano Garzarella wrote:
> > On Thu, 2 Jul 2026 at 04:55, mawupeng <mawupeng1@huawei.com> wrote:
> >> On 周四 2026-6-25 04:32, syzbot wrote:
> >>> Hello virt maintainers/developers,
> >>>
> >>> This is a 31-day syzbot report for the virt subsystem.
> >>> All related reports/information can be found at:
> >>> https://syzkaller.appspot.com/upstream/s/virt
> >>>
> >>> During the period, 0 new issues were detected and 0 were fixed.
> >>> In total, 5 issues are still open and 61 have already been fixed.
> >>> There are also 2 low-priority issues.
> >>>
> >>> Some of the still happening issues:
> >>>
> >>> Ref Crashes Repro Title
> >>> <1> 24      No    WARNING: refcount bug in call_timer_fn (4)
> >>>                   https://syzkaller.appspot.com/bug?extid=07dcf509f4c013e25dc5
> >>> <2> 3       Yes   memory leak in __vsock_create (2)
> >>>                   https://syzkaller.appspot.com/bug?extid=1b2c9c4a0f8708082678
> >>
> >> Hi,
> >>
> >> This is regarding the still-open "memory leak in __vsock_create (2)"
> >> bug (#2 in the monthly virt report, extid 1b2c9c4a0f8708082678):
> >>   https://syzkaller.appspot.com/bug?extid=1b2c9c4a0f8708082678
> >>
> >> I spent some time analyzing the root cause and the previous fix
> >> attempt; below is a summary and a direction that tested out.
> >
> > [...]
> >
> >>
> >> I'm not subscribed to follow the list at full volume; happy to send a
> >> formal patch (with the af_vsock.h / pending_work changes folded in)
> >> if the direction looks right to the maintainers.
> >
> > Yes, please, a formal patch with a great commit message is much better
> > than a long text to read IMO.
>
> Thanks for replying.
>
> As we conducted more thorough testing and further analysis of our own fix,
> we found that it is not as comprehensive as the existing AI-generated fix
> provided[1].
>
> Could you please let us know if there are any issues with this AI-generated fix?
>
> [1]: https://syzkaller.appspot.com/ai_job?id=cd978d21-2110-4f57-9525-d7d91c4a7e02

I'll chime in from the syzbot side.
If anything is missing, please let us know :)

Otherwise, please note that the bot can incorporate code review notes
and, once the patch is ready, submit it to the LKML:
https://github.com/google/syzkaller/blob/master/docs/syzbot_ai_patches.md

Best Regards,
Aleksandr

>
> >
> > Thanks,
> > Stefano
> >
> >
>

^ permalink raw reply

* Re: [PATCH net-next v2 0/8] netconsole: stop charging netpoll users for netconsole-only data
From: Breno Leitao @ 2026-07-06 11:55 UTC (permalink / raw)
  To: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, Andrew Lunn
  Cc: netdev, asantostc, gustavold, linux-kernel, kernel-team
In-Reply-To: <20260702-netconsole_move_more-v2-0-1ebedd921dcb@debian.org>

On Thu, Jul 02, 2026 at 05:19:44AM -0700, Breno Leitao wrote:
> This work continue to untangle netconsole and netpoll, improving
> memory usage for netpoll users that are not netconsole.

Quick follow up on the sashiko findings. most of them are assuming that
netconsole_write is called locklelly, which is not the case, except
on panic (CON_NBCON_ATOMIC_UNSAFE). 


There are in order as they appear in
https://sashiko.dev/#/patchset/20260702-netconsole_move_more-v2-0-1ebedd921dcb%40debian.org

Tl;DR: There are two pre-existing issue that I will get fixed, but
I don't think they are blockers for this series (IMO).

Details of the findings and analyzes:

1) This isn't a bug introduced by this patch, but is it safe to modify
   target_list using standard list primitives while netconsole_write()
   iterates over it locklessly?

netconsole_write() is never called without the lock, unless on
emergency, which is known to be unsafe (CON_NBCON_ATOMIC_UNSAFE).

Sashiko doesn't undersatnd that nbcon .device_lock
(netconsole_device_lock), is called before netconsole_write

2) This is a pre-existing issue, but I noticed a potential race
   condition between netdevice events and configfs rmdir that skips
   cleanup. If netconsole_netdev_event() processes NETDEV_RELEASE or
   NETDEV_JOIN, it sets nt->state = STATE_DISABLED and moves the target
   to target_cleanup_list:

This is a pre-existing issue, and I am happy to fix in here or as
a follow-up


3) This isn't a bug introduced by this patch, but I found a potential
   lockless iteration race in netconsole_write() in this file.

Again, netconsole_write() is not called locklelssly. 


4) This is a pre-existing issue, but does lockless iteration of
   target_list in netconsole_write() race with non-RCU list
   modifications?

Same as above.

5) This is a pre-existing issue, but is there a race condition between
   interface disablement and configfs removal that skips cleanup?

This is a pre-existing issue, and I am happy to fix in here or as
a follow-up.

6) This is a pre-existing issue, but does skb_dequeue() acquire
   a spinlock_t in NMI contexts?

netconsole is not used on NMI contexts, unless on emergency
(CON_NBCON_ATOMIC_UNSAFE).

7) When netconsole_write() is called as an atomic console callback, it
   iterates target_list locklessly. Concurrently, if a privileged user
   removes the target via configfs:

netconsole_write() is only called on atomic context on emergencies.
There is no conflict with configfs.

8) This isn't a bug introduced by this patch, but could the lockless
   iteration of target_list in netconsole_write() race with list
   removal? In drivers/net/netconsole.c:netconsole_write():

Same as above, netconsole_write() are not called locklessly except on
ops.


^ permalink raw reply

* Re: [PATCH net] net: openvswitch: reject oversized nested action attrs
From: Aaron Conole @ 2026-07-06 12:00 UTC (permalink / raw)
  To: Asim Viladi Oglu Manizada
  Cc: netdev, dev, echaudro, i.maximets, davem, edumazet, kuba, pabeni,
	horms, stable
In-Reply-To: <20260706094336.38639-1-manizada@pm.me>

Asim Viladi Oglu Manizada <manizada@pm.me> writes:

> Open vSwitch stores generated flow actions as nlattrs, whose nla_len
> field is u16. Commit a1e64addf3ff ("net: openvswitch: remove
> misbehaving actions length check") allowed the total sw_flow_actions
> stream to grow beyond 64 KiB, which is valid, but also removed the last
> guard preventing a generated nested action attribute from exceeding
> U16_MAX.
>
> An oversized generated container can thus be closed with a truncated
> nla_len. A later dump or teardown then walks a structurally different
> stream than the one that was validated. In particular, an oversized
> nested CLONE/CT action may cause subsequent bytes in the generated
> stream to be interpreted as independent actions.
>
> Keep the larger total-action-stream behavior, but make nested action
> close reject generated containers that do not fit in nla_len, and return
> the error through all callers. For recursive SAMPLE, CLONE, DEC_TTL, and
> CHECK_PKT_LEN builders, trim resource-owning action-list tails in reverse
> construction order before discarding failed wrappers, so resources copied
> into the rejected tails are released before the wrappers are removed.
>
> Most failed outer wrappers are discarded by truncating actions_len after
> child resources have been released. CHECK_PKT_LEN also trims its parent
> after branch resources are gone. SET/TUNNEL close failures unwind their
> known tun_dst ownership directly, and SET_TO_MASKED has no external
> ownership and truncates on close failure.
>
> Fixes: a1e64addf3ff ("net: openvswitch: remove misbehaving actions
> length check")
> Cc: stable@vger.kernel.org
> Assisted-by: avom-custom-harness:gpt-5.5-qwen3.6-mod-mix
> Signed-off-by: Asim Viladi Oglu Manizada <manizada@pm.me>
> ---

Thanks for the fix!

Reviewed-by: Aaron Conole <aconole@redhat.com>


^ permalink raw reply

* Re: [PATCH net-next 2/4] net: usb: centralize usbnet_cdc_zte_rx_fixup in usbnet
From: Oliver Neukum @ 2026-07-06 12:00 UTC (permalink / raw)
  To: Manuel Ebner, andrew+netdev, davem, edumazet, kuba, pabeni,
	shaoxul, netdev, linux-usb, linux-kernel
In-Reply-To: <ebfe837b7d4ef61442822105c3d71ca306106c14.camel@mailbox.org>



On 02.07.26 20:19, Manuel Ebner wrote:
> On Thu, 2026-07-02 at 16:25 +0200, Oliver Neukum wrote:

>>
>> diff --git a/drivers/net/usb/usbnet.c b/drivers/net/usb/usbnet.c
>> index 5544af1f4aa5..7beea6d0e731 100644
>> --- a/drivers/net/usb/usbnet.c
>> +++ b/drivers/net/usb/usbnet.c
>> @@ -2347,6 +2347,25 @@ void usbnet_cdc_status(struct usbnet *dev, struct urb *urb)
>>   	}
>>   }
>>   EXPORT_SYMBOL_GPL(usbnet_cdc_status);
>> +
>> +/* Make sure packets have correct destination MAC address
>     /*
>      * Make sure packets have the correct destination MAC address

This sort of puts me into a conundrum. Does networking code use
the short style of comments or not? The documentation says that it does.
I am fine either way, but I need a clear rule.

>> + *
>> + * A firmware bug observed on some devices (ZTE MF823/831/910) is that the
>> + * device sends packets with a static, bogus, random MAC address (event if
> 
>   (even if the device ...

Understood

> 
>> + * device MAC address has been updated). Always set MAC address to that of the
>> + * device.
> 
> Always set the MAC address to the one of your device.

Understood

	Regards
		Oliver


^ permalink raw reply

* [PATCH net] net: phy: marvell: fix return code
From: Michael Walle @ 2026-07-06 12:06 UTC (permalink / raw)
  To: Andrew Lunn, Heiner Kallweit, Russell King, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni
  Cc: Russell King, netdev, linux-kernel, Michael Walle

Return the correct error code, not the value written to the register.

Fixes: a219912e0fec ("net: phy: marvell: implement config_inband() method")
Signed-off-by: Michael Walle <mwalle@kernel.org>
---
 drivers/net/phy/marvell.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/net/phy/marvell.c b/drivers/net/phy/marvell.c
index 7a578b5aa2ed..f71cffa88406 100644
--- a/drivers/net/phy/marvell.c
+++ b/drivers/net/phy/marvell.c
@@ -753,7 +753,7 @@ static int m88e1111_config_inband(struct phy_device *phydev, unsigned int modes)
 	err = phy_modify(phydev, MII_M1111_PHY_EXT_SR,
 			 MII_M1111_HWCFG_SERIAL_AN_BYPASS, extsr);
 	if (err < 0)
-		return extsr;
+		return err;
 
 	return phy_modify_paged(phydev, MII_MARVELL_FIBER_PAGE, MII_BMCR,
 				BMCR_ANENABLE, bmcr);
-- 
2.47.3


^ permalink raw reply related

* Re: [PATCH net] netfilter: nf_nat_masquerade: recalculate TCP TS offset when port is randomized
From: xietangxin @ 2026-07-06 12:08 UTC (permalink / raw)
  To: Florian Westphal
  Cc: Pablo Neira Ayuso, Phil Sutter, David S . Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, gaoxingwang, huyizhen,
	netfilter-devel, coreteam, netdev, linux-kernel, stable
In-Reply-To: <akUhid7_3iHovivd@strlen.de>



On 7/1/2026 10:17 PM, Florian Westphal wrote:
> xietangxin <xietangxin@h-partners.com> wrote:
>> Shifting the helper down to nf_nat_l4proto_unique_tuple() as you suggested
>> encounters a structural roadblock. we don't have access to the skb there.
>> Adding skb to all intermediate callers (like nf_nat_setup_info, get_unique_tuple)
>> would severely pollute the core NAT APIs.
> 
> Right, propagating the skb is too much code churn.
> 
>> would it be acceptable to place this logic in nf_nat_inet_fn() before do_nat?
>>
>>  963 do_nat:
>>              ..here
> 
> This is hit for every packet, not just the first one after
> nf_nat_setup_info().  I suggest a slightly earlier spot in the
> same function.
> 
>  936                                 ret = e->hooks[i].hook(e->hooks[i].priv, skb,
>  937                                                        state);
>  938                                 if (ret != NF_ACCEPT)
>  939                                         return ret;
>  940                                 if (nf_nat_initialized(ct, maniptype))
>  941                                         goto do_nat;
>  942                         }
>  943 null_bind:
>  944                         ret = nf_nat_alloc_null_binding(ct, state->hook);
>  945                         if (ret != NF_ACCEPT)
>  946                                 return ret;
> 
>  .... Here.
> 
>  947                 } else {
> 
> This spot runs only for new connections, right after a nf_nat_setup_info() call.

Hi Florian,

Thanks for your guidance. I’ve successfully fix the helper location
as you suggested, and it works fine for local traffic.

However, I realized that I had completely overlooked the forwarding scenario
(where SNAT acts as a middlebox gateway, e.g. Host A -> Gateway B -> Server C).
In this gateway scenario, when random-fully is enabled, the test results show
a massive performance degradation: the QPS drops from ~19000 down to ~10000.

Since skb->sk is NULL on the forwarding gateway, my current approach of
updating tp->tsoffset in struct tcp_sock cannot be applied here.
To be honest, I am currently stuck on how to handle this forwarding scenario
within the netfilter architecture without adding redundant overhead to the fast path.

Could you please give some advice on how the community would prefer to resolve this?
For instance, should we look into extending the Conntrack NAT extension to
track and adjust the TCP timestamps?

Any suggestions would be highly appreciated!

-- 
Best regards,
Tangxin Xie


^ permalink raw reply

* [PATCH net] net: dpaa: always set a valid mode I/F mode
From: Michael Walle @ 2026-07-06 12:08 UTC (permalink / raw)
  To: Madalin Bucur, Sean Anderson, Andrew Lunn, David S . Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni
  Cc: netdev, linux-kernel, Michael Walle

Before converting to the phylink interface, the init function would have
set the correct mode in the maccfg2. After converting, init will just
set 0 as the mode. According to the "QorIQ Data Path Acceleration
Architecture (DPAA) Reference Manual", this is a reserved value. In
fact, this will prevent the PCS to establish a link to a connected SGMII
PHY. In turn, mac_link_up() is never called. Fix it by setting a
non-reserved mode; mac_link_up() will then set the correct mode later.

Fixes: 5d93cfcf7360 ("net: dpaa: Convert to phylink")
Signed-off-by: Michael Walle <mwalle@kernel.org>
---
FWIW, I've tested this with a Marvell 88E1112 PHY.

 drivers/net/ethernet/freescale/fman/fman_dtsec.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/drivers/net/ethernet/freescale/fman/fman_dtsec.c b/drivers/net/ethernet/freescale/fman/fman_dtsec.c
index fe35703c509e..566921d3a884 100644
--- a/drivers/net/ethernet/freescale/fman/fman_dtsec.c
+++ b/drivers/net/ethernet/freescale/fman/fman_dtsec.c
@@ -402,7 +402,10 @@ static int init(struct dtsec_regs __iomem *regs, struct dtsec_cfg *cfg,
 	tmp |= MACCFG1_TX_FLOW;
 	iowrite32be(tmp, &regs->maccfg1);
 
-	tmp = 0;
+	/* write a non-reserved mode, otherwise the PCS won't establish a link
+	 * and .mac_link_up() is never called.
+	 */
+	tmp = MACCFG2_NIBBLE_MODE;
 
 	tmp |= (cfg->preamble_len << MACCFG2_PREAMBLE_LENGTH_SHIFT) &
 		MACCFG2_PREAMBLE_LENGTH_MASK;
-- 
2.47.3


^ permalink raw reply related

* Re: [PATCH net] net: phy: marvell: fix return code
From: Maxime Chevallier @ 2026-07-06 12:26 UTC (permalink / raw)
  To: Michael Walle, Andrew Lunn, Heiner Kallweit, Russell King,
	David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni
  Cc: Russell King, netdev, linux-kernel
In-Reply-To: <20260706120637.1947685-1-mwalle@kernel.org>

Hi,

On 7/6/26 14:06, Michael Walle wrote:
> Return the correct error code, not the value written to the register.
> 
> Fixes: a219912e0fec ("net: phy: marvell: implement config_inband() method")
> Signed-off-by: Michael Walle <mwalle@kernel.org>

Reviewed-by: Maxime Chevallier <maxime.chevallier@bootlin.com>

Maxime



^ permalink raw reply

* Re: [PATCH net v2] net/mlx5: free mlx5_st_idx_data on final dealloc
From: Tariq Toukan @ 2026-07-06 12:33 UTC (permalink / raw)
  To: Zhiping Zhang, saeedm, leon, mbloch
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, netdev, linux-rdma,
	linux-kernel, michaelgur, stable
In-Reply-To: <20260630165324.2859353-1-zhipingz@meta.com>



On 30/06/2026 19:53, Zhiping Zhang wrote:
> Workloads that repeatedly allocate and release mkeys carrying TPH
> steering-tag hints (e.g. churning RDMA MRs) leak one
> struct mlx5_st_idx_data per cycle; kmemleak flags it as unreferenced
> and the kmalloc slab grows over time.
> 
> When the last reference to an ST table entry is dropped,
> mlx5_st_dealloc_index() removed the entry from idx_xa but the backing
> mlx5_st_idx_data allocation was never freed.
> 
> Free idx_data after the xa_erase() so the lifetime of the bookkeeping
> struct matches the lifetime of the ST entry it tracks.
> 
> Cc: stable@vger.kernel.org
> Fixes: 888a7776f4fb ("net/mlx5: Add support for device steering tag")
> Reviewed-by: Michael Gur <michaelgur@nvidia.com>
> Signed-off-by: Zhiping Zhang <zhipingz@meta.com>
> ---
> v2: respin per maintainer-netdev.rst; no code change.
> v1: https://lore.kernel.org/linux-rdma/20260612170406.3339093-1-zhipingz@meta.com/
> 
>   drivers/net/ethernet/mellanox/mlx5/core/lib/st.c | 1 +
>   1 file changed, 1 insertion(+)
> 
> diff --git a/drivers/net/ethernet/mellanox/mlx5/core/lib/st.c b/drivers/net/ethernet/mellanox/mlx5/core/lib/st.c
> index 997be91f0a13..7cedc348790d 100644
> --- a/drivers/net/ethernet/mellanox/mlx5/core/lib/st.c
> +++ b/drivers/net/ethernet/mellanox/mlx5/core/lib/st.c
> @@ -175,6 +175,7 @@ int mlx5_st_dealloc_index(struct mlx5_core_dev *dev, u16 st_index)
>   
>   	if (refcount_dec_and_test(&idx_data->usecount)) {
>   		xa_erase(&st->idx_xa, st_index);
> +		kfree(idx_data);
>   		/* We leave PCI config space as was before, no mkey will refer to it */
>   	}
>   

Reviewed-by: Tariq Toukan <tariqt@nvidia.com>

Thanks.

^ permalink raw reply

* Re: [PATCH net-next v3 1/2] net/sched: sch_fq_pie: add per-flow statistics via class ops
From: Paolo Abeni @ 2026-07-06 12:38 UTC (permalink / raw)
  To: Hemendra M. Naik, netdev
  Cc: davem, edumazet, kuba, horms, jiri, jhs, shuah, linux-kernel,
	linux-kselftest, vishy0777, tahiliani
In-Reply-To: <20260630183702.170798-2-hemendranaik@gmail.com>

On 6/30/26 8:37 PM, Hemendra M. Naik wrote:
> diff --git a/net/sched/sch_fq_pie.c b/net/sched/sch_fq_pie.c
> index 72f48fa4010b..60e85c002ae7 100644
> --- a/net/sched/sch_fq_pie.c
> +++ b/net/sched/sch_fq_pie.c
> @@ -330,7 +330,7 @@ static int fq_pie_change(struct Qdisc *sch, struct nlattr *opt,
>  	/* tupdate is in jiffies */
>  	if (tb[TCA_FQ_PIE_TUPDATE])
>  		WRITE_ONCE(q->p_params.tupdate,
> -			usecs_to_jiffies(nla_get_u32(tb[TCA_FQ_PIE_TUPDATE])));
> +			   usecs_to_jiffies(nla_get_u32(tb[TCA_FQ_PIE_TUPDATE])));

Minor nit, only if a repost is needed for other reasons: unrelated
formatting changes should go in a separate patch.

> @@ -517,10 +519,10 @@ static int fq_pie_dump_stats(struct Qdisc *sch, struct gnet_dump *d)
>  	st.packets_in	= q->stats.packets_in;
>  	st.overlimit	= q->stats.overlimit;
>  	st.overmemory	= q->overmemory;
> -	st.dropped	= q->stats.dropped;
> -	st.ecn_mark	= q->stats.ecn_mark;
> -	st.new_flow_count = q->new_flow_count;
> -	st.memory_usage   = q->memory_usage;
> +	st.dropped		= q->stats.dropped;
> +	st.ecn_mark		= q->stats.ecn_mark;
> +	st.new_flow_count	= q->new_flow_count;
> +	st.memory_usage	= q->memory_usage;

Same here.

@Jamal: the patch LGTM, and I think that the alignment concern raised by
sashiko is not relevant:

https://sashiko.dev/#/patchset/20260630183702.170798-1-hemendranaik%40gmail.com

as user-space already has to deal with 64-bit aligned fields in tc stats.

WDYT?

Thanks,

Paolo


^ permalink raw reply

* [PATCH v3 00/20] driver core: count references of the platform device's fwnode, not OF node
From: Bartosz Golaszewski @ 2026-07-06 12:44 UTC (permalink / raw)
  To: Lee Jones, Mark Brown, Thierry Reding, Sebastian Hesselbarth,
	Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Srinivas Kandagatla, Greg Kroah-Hartman, Vinod Koul,
	Rafael J. Wysocki, Danilo Krummrich, Rob Herring, Saravana Kannan,
	Madhavan Srinivasan, Michael Ellerman, Nicholas Piggin,
	Christophe Leroy (CS GROUP), Andi Shyti, Andy Shevchenko,
	Joerg Roedel, Will Deacon, Robin Murphy, Doug Berger,
	Florian Fainelli, Broadcom internal kernel review list,
	Ulf Hansson, Frank Li, Sascha Hauer, Pengutronix Kernel Team,
	Fabio Estevam, Matthew Brost, Thomas Hellström, Rodrigo Vivi,
	David Airlie, Simona Vetter, Peter Chen, Paul Cercueil, Bin Liu,
	Philipp Zabel, Maximilian Luz, Hans de Goede, Ilpo Järvinen,
	Krzysztof Kozlowski, Benjamin Herrenschmidt
  Cc: brgl, linux-kernel, netdev, linux-arm-msm, linux-sound,
	driver-core, devicetree, linuxppc-dev, linux-i2c, iommu, linux-pm,
	imx, linux-arm-kernel, intel-xe, dri-devel, linux-usb, linux-mips,
	platform-driver-x86, mfd, Bartosz Golaszewski, stable,
	Manuel Ebner, Wolfram Sang, Konrad Dybcio

Platform device core provides helper interfaces for dealing with
dynamically created platform devices. Most users should use
platform_device_register_full() which encapsulates most of the
operations but some modules will want to use the split approach of
calling platform_device_alloc() + platform_device_add() separately for
various reasons.

With many platform devices now using dynamic software nodes as their
primary firmware nodes and with the platform device interface being
extended to also better cover the use-cases of secondary software nodes,
I believe it makes sense to switch to counting the references of all
kinds of firmware nodes.

To that end, I identified all users of platform_device_alloc() that also
assign dev.of_node or dev.fwnode manually. I noticed five cases where
the references are not increased as they should (patches 1-5 fix these
users) and provided three new functions in platform_device.h that now
become the preferred interfaces for assigning firmware nodes to dynamic
platform devices (in line with platform_device_add_data(),
platform_device_add_resources(), etc.). The bulk of the patches in this
series are small driver conversions to port all users to going through
the new functions that now encapsulate the refcount logic. With that
done, the final patch seamlessly switches to counting the references of
all firmware node types.

This effort is prerequisite of removing platform_device_release_full()
and unifying the release path for dynamic platform devices using
unmanaged software nodes.

Merging strategy: The entire series should go through the driver core
tree, possibly with an immutable branch provided to solve any potential
conflicts though these are rather unlikely.

Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
---
Changes in v3:
- Use device_set_node() where applicable
- Use __free(device_node) in fsl iommu driver
- Don't use __free() where not really required
- Add a patch updating the kerneldoc for platform_device_alloc()
- Collect more tags
- Link to v2: https://patch.msgid.link/20260629-pdev-fwnode-ref-v2-0-8abe2513f96e@oss.qualcomm.com

Changes in v2:
- Rebased on top of v7.2-rc1, dropped applied patches, collected tags
- Link to v1: https://patch.msgid.link/20260521-pdev-fwnode-ref-v1-0-88c324a1b8d2@oss.qualcomm.com

---
Bartosz Golaszewski (20):
      powerpc/powermac: fix OF node refcount
      driver core: platform: provide platform_device_set_of_node()
      driver core: platform: provide platform_device_set_fwnode()
      driver core: platform: provide platform_device_set_of_node_from_dev()
      driver core: update kerneldoc for platform_device_alloc()
      of: platform: use platform_device_set_of_node()
      powerpc/powermac: use platform_device_set_of_node()
      i2c: pxa-pci: use platform_device_set_of_node()
      iommu/fsl: use platform_device_set_of_node()
      net: bcmgenet: use platform_device_set_of_node()
      pmdomain: imx: use platform_device_set_of_node()
      mfd: tps6586: use platform_device_set_of_node()
      slimbus: qcom-ngd-ctrl: use platform_device_set_of_node()
      net: mv643xx: use platform_device_set_of_node()
      drm/xe/i2c: use platform_device_set_fwnode()
      platform/surface: gpe: use platform_device_set_fwnode()
      usb: chipidea: use platform_device_set_of_node_from_dev()
      usb: musb: use platform_device_set_of_node_from_dev()
      reset: rzg2l: use platform_device_set_of_node_from_dev()
      driver core: platform: count references to all kinds of firmware nodes

 arch/powerpc/platforms/powermac/low_i2c.c    |  2 +-
 drivers/base/platform.c                      | 61 ++++++++++++++++++++++++++--
 drivers/gpu/drm/xe/xe_i2c.c                  |  2 +-
 drivers/i2c/busses/i2c-pxa-pci.c             |  3 +-
 drivers/iommu/fsl_pamu.c                     | 16 +++-----
 drivers/mfd/tps6586x.c                       |  2 +-
 drivers/net/ethernet/broadcom/genet/bcmmii.c |  3 +-
 drivers/net/ethernet/marvell/mv643xx_eth.c   |  2 +-
 drivers/of/platform.c                        |  2 +-
 drivers/platform/surface/surface_gpe.c       |  2 +-
 drivers/pmdomain/imx/gpc.c                   |  3 +-
 drivers/reset/reset-rzg2l-usbphy-ctrl.c      |  2 +-
 drivers/slimbus/qcom-ngd-ctrl.c              |  2 +-
 drivers/usb/chipidea/core.c                  |  2 +-
 drivers/usb/musb/jz4740.c                    |  2 +-
 include/linux/platform_device.h              |  9 ++++
 16 files changed, 88 insertions(+), 27 deletions(-)
---
base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda
change-id: 20260520-pdev-fwnode-ref-d867836971eb

Best regards,
-- 
Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>


^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox