--- /dev/null
+From 0b577e2fe06c023ab996c3d7684538dbbf6e99bc Mon Sep 17 00:00:00 2001
+From: Johan Alvarado <contact@c127.dev>
+Date: Sat, 11 Jul 2026 23:31:58 -0500
+Subject: net: dsa: realtek: rtl8365mb: add SGMII support for RTL8367S
+
+The RTL8367S can mux its embedded SerDes to external interface 1,
+which is typically used to connect the switch to a CPU port. The chip
+info table already declares SGMII as a supported interface mode for
+this chip, but the driver only implements RGMII so far.
+
+Implement SGMII support as a phylink PCS, with the configuration
+sequence derived from the GPL-licensed Realtek rtl8367c vendor driver
+as distributed in the Mercusys MR80X GPL code drop:
+
+ - Add accessors for the SerDes indirect access registers (SDS_INDACS),
+ through which the SerDes internal registers are reached.
+
+ - Register a phylink_pcs for the SerDes, selected from mac_select_pcs
+ for the SGMII interface, so the SerDes handling lives in the PCS
+ operations rather than in the MAC operations.
+
+ - Probe the SerDes tuning variant from the chip option register once
+ at setup. The vendor driver keeps two sets of SerDes tuning
+ parameters and selects between them based on this option; only the
+ variant for a non-zero option (which all RTL8367S parts seen so far
+ report) has been validated on hardware, so the SerDes interface
+ modes are only advertised in that case. An unsupported variant thus
+ fails at phylink validation time instead of at link configuration
+ time.
+
+ - Keep the embedded DW8051 microcontroller in reset and disabled. The
+ vendor driver loads firmware into it to manage the SerDes link, but
+ analysis of that firmware shows it only duplicates the link
+ management phylink already performs: it polls the port status and
+ writes the external interface force registers behind the driver's
+ back.
+
+ - Clear the line rate bypass bit for the external interface, tune the
+ SerDes with the vendor-prescribed parameters, mux the SerDes to MAC8
+ in SGMII mode and only then take the SerDes out of reset, as the
+ vendor driver does.
+
+ - After deasserting the SerDes reset, reset the SerDes data path via
+ the SerDes BMCR register to flush the FIFOs and resync the PLL.
+ This mirrors what the vendor firmware does right after deasserting
+ the SerDes reset, and ensures a clean link state from cold boot.
+
+ - Force the SGMII link parameters (link, speed, duplex) in the SDS_MISC
+ register from pcs_link_up(). SGMII in-band autonegotiation is not
+ implemented, so only fixed-link and conventional PHY setups are
+ supported, just like RGMII. This is reported to phylink through
+ pcs_inband_caps() returning LINK_INBAND_DISABLE, so phylink never
+ selects an in-band-enabled negotiation mode for this PCS.
+
+ - Program the SerDes pause enables in SDS_MISC from the resolved
+ pause modes when forcing the MAC external interface in mac_link_up,
+ as the vendor driver does, rather than leaving whatever state the
+ boot firmware left there. Flow control testing shows these bits,
+ not the MAC force pause bits, gate pause on the SerDes external
+ interface. This is done in the MAC layer because pcs_link_up()
+ carries no pause information.
+
+ - Implement pcs_get_state() by reading the link status from the
+ SerDes, with the forced speed and duplex read back from SDS_MISC.
+ Although the supported fixed-link and conventional PHY setups do not
+ use it, the PCS owns the SerDes link state, and phylink consults
+ pcs_get_state() to track the physical link when operating in in-band
+ mode with autonegotiation disabled. The SerDes has no link interrupt
+ wired up, so the PCS sets its poll flag.
+
+Tested on a Mercusys MR80X v2.20, where the RTL8367S is connected to
+the SoC over SGMII.
+
+Suggested-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
+Suggested-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
+Suggested-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
+Signed-off-by: Johan Alvarado <contact@c127.dev>
+Reviewed-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
+Reviewed-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
+Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
+Tested-by: Stanislaw Pal <kuncy7@gmail.com>
+Link: https://patch.msgid.link/20260711-rtl8367s-sgmii-v6-1-88f7944ddca7@c127.dev
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/rtl8365mb_main.c | 515 ++++++++++++++++++++++++++++++-
+ 1 file changed, 511 insertions(+), 4 deletions(-)
+
+--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -40,7 +40,8 @@
+ * driver has only been tested with a fixed-link, but in principle it should not
+ * matter.
+ *
+- * NOTE: Currently, only the RGMII interface is implemented in this driver.
++ * NOTE: Currently, only the RGMII and SGMII interfaces are implemented in this
++ * driver.
+ *
+ * The interrupt line is asserted on link UP/DOWN events. The driver creates a
+ * custom irqchip to handle this interrupt and demultiplex the events by reading
+@@ -94,11 +95,13 @@
+ #include <linux/bitops.h>
+ #include <linux/interrupt.h>
+ #include <linux/irqdomain.h>
++#include <linux/mii.h>
+ #include <linux/mutex.h>
+ #include <linux/of_irq.h>
+ #include <linux/regmap.h>
+ #include <linux/if_bridge.h>
+ #include <linux/if_vlan.h>
++#include <linux/phylink.h>
+
+ #include "realtek.h"
+ #include "realtek-smi.h"
+@@ -129,6 +132,7 @@
+
+ /* Chip reset register */
+ #define RTL8365MB_CHIP_RESET_REG 0x1322
++#define RTL8365MB_CHIP_RESET_DW8051_MASK 0x0010
+ #define RTL8365MB_CHIP_RESET_SW_MASK 0x0002
+ #define RTL8365MB_CHIP_RESET_HW_MASK 0x0001
+
+@@ -238,6 +242,76 @@
+ #define RTL8365MB_EXT_RGMXF_RXDELAY_MASK 0x0007
+ #define RTL8365MB_EXT_RGMXF_TXDELAY_MASK 0x0008
+
++/* External interface line rate bypass register - one bit per external
++ * interface, indexed by the external port number with port 5 (the first
++ * external port) as the base. Other RTL8367 families index this register
++ * differently (e.g. the RTL8367R uses (id + 1) % 2), so this mapping only
++ * holds for the RTL8367C-style parts this driver supports.
++ */
++#define RTL8365MB_BYPASS_LINE_RATE_REG 0x03F7
++#define RTL8365MB_BYPASS_LINE_RATE_MASK(_port) BIT((_port) - 5)
++
++/* SerDes indirect access registers */
++#define RTL8365MB_SDS_INDACS_CMD_REG 0x6600
++#define RTL8365MB_SDS_INDACS_CMD_BUSY_MASK 0x0100
++#define RTL8365MB_SDS_INDACS_CMD_RUN_MASK 0x0080
++#define RTL8365MB_SDS_INDACS_CMD_WR_MASK 0x0040
++#define RTL8365MB_SDS_INDACS_ADR_REG 0x6601
++#define RTL8365MB_SDS_INDACS_DATA_REG 0x6602
++
++/* SerDes miscellaneous configuration register */
++#define RTL8365MB_SDS_MISC_REG 0x1D11
++#define RTL8365MB_SDS_MISC_SGMII_RXFC_MASK 0x4000
++#define RTL8365MB_SDS_MISC_SGMII_TXFC_MASK 0x2000
++#define RTL8365MB_SDS_MISC_MAC8_SEL_HSGMII_MASK 0x0800
++#define RTL8365MB_SDS_MISC_SGMII_FDUP_MASK 0x0400
++#define RTL8365MB_SDS_MISC_SGMII_LINK_MASK 0x0200
++#define RTL8365MB_SDS_MISC_SGMII_SPD_MASK 0x0180
++#define RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK 0x0040
++
++/* SerDes internal registers, accessed via the SDS_INDACS registers. The BMCR
++ * data path reset holds BMCR_ANENABLE | BMCR_ISOLATE while toggling the
++ * vendor-specific low bits from phase 1 to phase 2, which triggers a data path
++ * reset and PLL resync.
++ */
++#define RTL8365MB_SDS_REG_BMCR 0x0000
++#define RTL8365MB_SDS_BMCR_DPRST_PHASE1 (BMCR_ANENABLE | BMCR_ISOLATE | 0x1)
++#define RTL8365MB_SDS_BMCR_DPRST_PHASE2 (BMCR_ANENABLE | BMCR_ISOLATE | 0x3)
++#define RTL8365MB_SDS_REG_NWAY 0x0002
++#define RTL8365MB_SDS_NWAY_EN_MASK 0x0200
++#define RTL8365MB_SDS_NWAY_RESTART_MASK 0x0100
++#define RTL8365MB_SDS_REG_RESET 0x0003
++#define RTL8365MB_SDS_RESET_DEASSERT 0x7106
++#define RTL8365MB_SDS_REG_LINK_STATUS 0x003d
++#define RTL8365MB_SDS_LINK_STATUS_LINK_MASK 0x0010
++
++/* The embedded SerDes can only be muxed to external interface 1 (MAC8),
++ * which is port 6.
++ */
++#define RTL8365MB_SDS_EXT_INTERFACE_ID 1
++#define RTL8365MB_SDS_EXT_INTERFACE_PORT 6
++
++/* Line rate bypass bit for the SerDes external interface */
++#define RTL8365MB_SDS_BYPASS_LINE_RATE_MASK \
++ RTL8365MB_BYPASS_LINE_RATE_MASK(RTL8365MB_SDS_EXT_INTERFACE_PORT)
++
++/* SerDes tuning parameter variant selector. The vendor driver picks between
++ * two sets of SerDes tuning parameters based on this chip option. Reading it
++ * requires first arming the read by writing a magic key to the arm register,
++ * then disarming it afterwards.
++ */
++#define RTL8365MB_SDS_OPTION_ARM_REG 0x13C0
++#define RTL8365MB_SDS_OPTION_ARM_KEY 0x0249
++#define RTL8365MB_SDS_OPTION_REG 0x13C1
++
++/* Embedded DW8051 microcontroller control registers. The microcontroller
++ * can run firmware to manage the SerDes link, but this driver keeps it in
++ * reset and disabled: phylink already performs the link management that
++ * the firmware would otherwise do.
++ */
++#define RTL8365MB_MISC_CFG0_REG 0x130C
++#define RTL8365MB_MISC_CFG0_DW8051_EN_MASK 0x0020
++
+ /* External interface port speed values - used in DIGITAL_INTERFACE_FORCE */
+ #define RTL8365MB_PORT_SPEED_10M 0
+ #define RTL8365MB_PORT_SPEED_100M 1
+@@ -551,6 +625,18 @@ static const struct rtl8365mb_jam_tbl_en
+ { 0x1D32, 0x0002 },
+ };
+
++/* SGMII SerDes tuning parameters, lifted from the vendor driver sources. The
++ * vendor driver keeps two variants of this table and selects between them
++ * based on the chip option register; these are the values for a non-zero
++ * option, which is what RTL8367S parts seen so far report. See
++ * rtl8365mb_sds_probe_option().
++ */
++static const struct rtl8365mb_jam_tbl_entry rtl8365mb_sds_jam_sgmii[] = {
++ { 0x0480, 0x04D7 }, { 0x0481, 0xF994 }, { 0x0482, 0x2420 },
++ { 0x0483, 0x6960 }, { 0x0484, 0x9728 }, { 0x0423, 0x9D85 },
++ { 0x0424, 0xD810 }, { 0x002E, 0x83F2 },
++};
++
+ enum rtl8365mb_phy_interface_mode {
+ RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
+ RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
+@@ -730,6 +816,9 @@ struct rtl8365mb_port {
+ * @cpu: CPU tagging and CPU port configuration for this chip
+ * @mib_lock: prevent concurrent reads of MIB counters
+ * @ports: per-port data
++ * @pcs: PCS for the SerDes external interface
++ * @sds_supported: SerDes tuning parameters match the chip option, so the
++ * SerDes interface modes can be advertised
+ *
+ * Private data for this driver.
+ */
+@@ -740,8 +829,12 @@ struct rtl8365mb {
+ struct rtl8365mb_cpu cpu;
+ struct mutex mib_lock;
+ struct rtl8365mb_port ports[RTL8365MB_MAX_NUM_PORTS];
++ struct phylink_pcs pcs;
++ bool sds_supported;
+ };
+
++#define pcs_to_rtl8365mb(_pcs) container_of((_pcs), struct rtl8365mb, pcs)
++
+ static int rtl8365mb_phy_poll_busy(struct realtek_priv *priv)
+ {
+ u32 val;
+@@ -1042,6 +1135,333 @@ static int rtl8365mb_ext_config_rgmii(st
+ return 0;
+ }
+
++static int rtl8365mb_sds_write(struct realtek_priv *priv, u16 addr, u16 data)
++{
++ int ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_INDACS_DATA_REG, data);
++ if (ret)
++ return ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_INDACS_ADR_REG, addr);
++ if (ret)
++ return ret;
++
++ /* The SerDes indirect access engine completes the command within the
++ * register write transaction, so there is no need to wait or poll for
++ * completion before the next access, matching the vendor driver.
++ */
++ return regmap_write(priv->map, RTL8365MB_SDS_INDACS_CMD_REG,
++ RTL8365MB_SDS_INDACS_CMD_RUN_MASK |
++ RTL8365MB_SDS_INDACS_CMD_WR_MASK);
++}
++
++static int rtl8365mb_sds_read(struct realtek_priv *priv, u16 addr, u16 *data)
++{
++ u32 val;
++ int ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_INDACS_ADR_REG, addr);
++ if (ret)
++ return ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_INDACS_CMD_REG,
++ RTL8365MB_SDS_INDACS_CMD_RUN_MASK);
++ if (ret)
++ return ret;
++
++ /* Wait for the indirect read to complete: the engine clears the BUSY
++ * bit once the data register holds the result.
++ */
++ ret = regmap_read_poll_timeout(priv->map, RTL8365MB_SDS_INDACS_CMD_REG,
++ val,
++ !(val & RTL8365MB_SDS_INDACS_CMD_BUSY_MASK),
++ 10, 1000);
++ if (ret)
++ return ret;
++
++ ret = regmap_read(priv->map, RTL8365MB_SDS_INDACS_DATA_REG, &val);
++ if (ret)
++ return ret;
++
++ *data = val;
++
++ return 0;
++}
++
++/* The vendor driver selects between two sets of SerDes tuning parameters based
++ * on the chip option register. Only the variant for a non-zero option has been
++ * tested on real hardware - the RTL8367S parts seen so far all report 1. The
++ * variant for option 0 uses different tuning values that cannot be verified,
++ * so probe the option once at setup and only advertise the SerDes interface
++ * modes when the tuning parameters are known to match, so that an unsupported
++ * variant fails at phylink validation time rather than when configuring the
++ * link.
++ */
++static int rtl8365mb_sds_probe_option(struct realtek_priv *priv)
++{
++ struct rtl8365mb *mb = priv->chip_data;
++ const struct rtl8365mb_extint *extint;
++ u32 option;
++ int ret;
++ int i;
++
++ /* Nothing to probe if no external interface is wired to the SerDes */
++ for (i = 0; i < RTL8365MB_MAX_NUM_EXTINTS; i++) {
++ extint = &mb->chip_info->extints[i];
++
++ if (extint->supported_interfaces &
++ (RTL8365MB_PHY_INTERFACE_MODE_SGMII |
++ RTL8365MB_PHY_INTERFACE_MODE_HSGMII))
++ break;
++ }
++ if (i == RTL8365MB_MAX_NUM_EXTINTS)
++ return 0;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_OPTION_ARM_REG,
++ RTL8365MB_SDS_OPTION_ARM_KEY);
++ if (ret)
++ return ret;
++
++ ret = regmap_read(priv->map, RTL8365MB_SDS_OPTION_REG, &option);
++ if (ret)
++ return ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_OPTION_ARM_REG, 0);
++ if (ret)
++ return ret;
++
++ if (option == 0) {
++ dev_warn(priv->dev,
++ "unsupported SerDes tuning variant (chip option 0), disabling SerDes interface modes\n");
++ return 0;
++ }
++
++ mb->sds_supported = true;
++
++ return 0;
++}
++
++static int rtl8365mb_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
++ phy_interface_t interface,
++ const unsigned long *advertising,
++ bool permit_pause_to_mac)
++{
++ const int id = RTL8365MB_SDS_EXT_INTERFACE_ID;
++ struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
++ struct realtek_priv *priv;
++ u16 val;
++ int ret;
++ int i;
++
++ priv = mb->priv;
++
++ /* Hold the embedded DW8051 microcontroller in reset and keep it
++ * disabled. The vendor driver loads firmware into it to manage the
++ * SerDes link, but the firmware only duplicates work that phylink
++ * already does: it polls the port status and forces the external
++ * interface configuration in the very registers this driver manages.
++ * Letting it run would race with phylink.
++ */
++ ret = regmap_update_bits(priv->map, RTL8365MB_CHIP_RESET_REG,
++ RTL8365MB_CHIP_RESET_DW8051_MASK,
++ RTL8365MB_CHIP_RESET_DW8051_MASK);
++ if (ret)
++ return ret;
++
++ ret = regmap_update_bits(priv->map, RTL8365MB_MISC_CFG0_REG,
++ RTL8365MB_MISC_CFG0_DW8051_EN_MASK, 0);
++ if (ret)
++ return ret;
++
++ /* The vendor driver clears the line rate bypass for all interface
++ * modes except TMII.
++ */
++ ret = regmap_update_bits(priv->map, RTL8365MB_BYPASS_LINE_RATE_REG,
++ RTL8365MB_SDS_BYPASS_LINE_RATE_MASK, 0);
++ if (ret)
++ return ret;
++
++ /* Tune the SerDes with vendor-prescribed parameters */
++ for (i = 0; i < ARRAY_SIZE(rtl8365mb_sds_jam_sgmii); i++) {
++ ret = rtl8365mb_sds_write(priv,
++ rtl8365mb_sds_jam_sgmii[i].reg,
++ rtl8365mb_sds_jam_sgmii[i].val);
++ if (ret)
++ return ret;
++ }
++
++ /* Mux the SerDes to MAC8 in SGMII mode */
++ ret = regmap_update_bits(priv->map, RTL8365MB_SDS_MISC_REG,
++ RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK |
++ RTL8365MB_SDS_MISC_MAC8_SEL_HSGMII_MASK,
++ RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK);
++ if (ret)
++ return ret;
++
++ val = RTL8365MB_EXT_PORT_MODE_SGMII
++ << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(id);
++ ret = regmap_update_bits(priv->map,
++ RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(id),
++ RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(id),
++ val);
++ if (ret)
++ return ret;
++
++ /* Take the SerDes out of reset. The vendor driver does this only
++ * after the SerDes mux and the interface mode are configured.
++ */
++ ret = rtl8365mb_sds_write(priv, RTL8365MB_SDS_REG_RESET,
++ RTL8365MB_SDS_RESET_DEASSERT);
++ if (ret)
++ return ret;
++
++ /* Reset the SerDes data path and resync its PLL, mirroring what the
++ * vendor firmware does right after deasserting the SerDes reset.
++ * This flushes the FIFOs and ensures a clean state for the link,
++ * preventing silent drops and CRC errors.
++ */
++ ret = rtl8365mb_sds_write(priv, RTL8365MB_SDS_REG_BMCR,
++ RTL8365MB_SDS_BMCR_DPRST_PHASE1);
++ if (ret)
++ return ret;
++
++ ret = rtl8365mb_sds_write(priv, RTL8365MB_SDS_REG_BMCR,
++ RTL8365MB_SDS_BMCR_DPRST_PHASE2);
++ if (ret)
++ return ret;
++
++ /* Keep SGMII in-band autonegotiation disabled: the link parameters are
++ * forced from rtl8365mb_pcs_link_up() instead.
++ */
++ ret = rtl8365mb_sds_read(priv, RTL8365MB_SDS_REG_NWAY, &val);
++ if (ret)
++ return ret;
++
++ val &= ~RTL8365MB_SDS_NWAY_EN_MASK;
++ val |= RTL8365MB_SDS_NWAY_RESTART_MASK;
++
++ return rtl8365mb_sds_write(priv, RTL8365MB_SDS_REG_NWAY, val);
++}
++
++static bool rtl8365mb_interface_is_serdes(phy_interface_t interface)
++{
++ return interface == PHY_INTERFACE_MODE_SGMII;
++}
++
++static unsigned int rtl8365mb_pcs_inband_caps(struct phylink_pcs *pcs,
++ phy_interface_t interface)
++{
++ /* In-band autonegotiation is not implemented; the link is always
++ * forced. Report that to phylink so that it never selects an
++ * in-band-enabled negotiation mode for this PCS.
++ */
++ return LINK_INBAND_DISABLE;
++}
++
++static void rtl8365mb_pcs_get_state(struct phylink_pcs *pcs,
++ struct phylink_link_state *state)
++{
++ struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
++ struct realtek_priv *priv = mb->priv;
++ u16 status;
++ u32 val;
++ int ret;
++
++ /* In-band autonegotiation is not implemented, so the link parameters are
++ * forced from rtl8365mb_pcs_link_up(). The real link state must still be
++ * read from the SerDes itself: the embedded DW8051 microcontroller that
++ * the vendor firmware uses to poll the SerDes is kept disabled (see
++ * rtl8365mb_pcs_config()), so the link status register can be read
++ * directly through the SDS_INDACS window without racing the auto-poll.
++ */
++ ret = rtl8365mb_sds_read(priv, RTL8365MB_SDS_REG_LINK_STATUS, &status);
++ if (ret) {
++ state->link = false;
++ return;
++ }
++
++ state->link = !!(status & RTL8365MB_SDS_LINK_STATUS_LINK_MASK);
++ state->an_complete = state->link;
++ if (!state->link)
++ return;
++
++ /* The speed and duplex are forced; read them back from the values
++ * programmed into the SerDes MISC register.
++ */
++ ret = regmap_read(priv->map, RTL8365MB_SDS_MISC_REG, &val);
++ if (ret) {
++ state->link = false;
++ return;
++ }
++
++ state->duplex = (val & RTL8365MB_SDS_MISC_SGMII_FDUP_MASK) ?
++ DUPLEX_FULL : DUPLEX_HALF;
++
++ switch (FIELD_GET(RTL8365MB_SDS_MISC_SGMII_SPD_MASK, val)) {
++ case RTL8365MB_PORT_SPEED_1000M:
++ state->speed = SPEED_1000;
++ break;
++ case RTL8365MB_PORT_SPEED_100M:
++ state->speed = SPEED_100;
++ break;
++ case RTL8365MB_PORT_SPEED_10M:
++ state->speed = SPEED_10;
++ break;
++ }
++}
++
++static void rtl8365mb_pcs_link_up(struct phylink_pcs *pcs,
++ unsigned int neg_mode,
++ phy_interface_t interface, int speed,
++ int duplex)
++{
++ struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
++ struct realtek_priv *priv = mb->priv;
++ u32 mask = RTL8365MB_SDS_MISC_SGMII_FDUP_MASK |
++ RTL8365MB_SDS_MISC_SGMII_LINK_MASK |
++ RTL8365MB_SDS_MISC_SGMII_SPD_MASK;
++ u32 val = RTL8365MB_SDS_MISC_SGMII_LINK_MASK;
++ u32 r_speed;
++ int ret;
++
++ if (speed == SPEED_1000) {
++ r_speed = RTL8365MB_PORT_SPEED_1000M;
++ } else if (speed == SPEED_100) {
++ r_speed = RTL8365MB_PORT_SPEED_100M;
++ } else if (speed == SPEED_10) {
++ r_speed = RTL8365MB_PORT_SPEED_10M;
++ } else {
++ dev_err(priv->dev, "unsupported SerDes speed %s\n",
++ phy_speed_to_str(speed));
++ return;
++ }
++
++ val |= FIELD_PREP(RTL8365MB_SDS_MISC_SGMII_SPD_MASK, r_speed);
++
++ if (duplex == DUPLEX_FULL)
++ val |= RTL8365MB_SDS_MISC_SGMII_FDUP_MASK;
++
++ /* pcs_link_up() carries no pause information, so the SerDes flow
++ * control bits are programmed together with the MAC external interface
++ * force from rtl8365mb_phylink_mac_link_up(), where the resolved pause
++ * modes are known.
++ */
++ ret = regmap_update_bits(priv->map, RTL8365MB_SDS_MISC_REG, mask, val);
++ if (ret) {
++ dev_err(priv->dev, "failed to force SerDes link: %pe\n",
++ ERR_PTR(ret));
++ return;
++ }
++}
++
++static const struct phylink_pcs_ops rtl8365mb_pcs_ops = {
++ .pcs_inband_caps = rtl8365mb_pcs_inband_caps,
++ .pcs_config = rtl8365mb_pcs_config,
++ .pcs_get_state = rtl8365mb_pcs_get_state,
++ .pcs_link_up = rtl8365mb_pcs_link_up,
++};
++
+ static int rtl8365mb_ext_config_forcemode(struct realtek_priv *priv, int port,
+ bool link, int speed, int duplex,
+ bool tx_pause, bool rx_pause)
+@@ -1118,6 +1538,8 @@ static void rtl8365mb_phylink_get_caps(s
+ {
+ const struct rtl8365mb_extint *extint =
+ rtl8365mb_get_port_extint(ds->priv, port);
++ struct realtek_priv *priv = ds->priv;
++ struct rtl8365mb *mb = priv->chip_data;
+
+ config->mac_capabilities = MAC_SYM_PAUSE | MAC_ASYM_PAUSE |
+ MAC_10 | MAC_100 | MAC_1000FD;
+@@ -1141,6 +1563,25 @@ static void rtl8365mb_phylink_get_caps(s
+
+ if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_RGMII)
+ phy_interface_set_rgmii(config->supported_interfaces);
++
++ if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_SGMII &&
++ mb->sds_supported)
++ __set_bit(PHY_INTERFACE_MODE_SGMII,
++ config->supported_interfaces);
++}
++
++static struct phylink_pcs *
++rtl8365mb_phylink_mac_select_pcs(struct phylink_config *config,
++ phy_interface_t interface)
++{
++ struct dsa_port *dp = dsa_phylink_to_port(config);
++ struct realtek_priv *priv = dp->ds->priv;
++ struct rtl8365mb *mb = priv->chip_data;
++
++ if (rtl8365mb_interface_is_serdes(interface))
++ return &mb->pcs;
++
++ return NULL;
+ }
+
+ static void rtl8365mb_phylink_mac_config(struct phylink_config *config,
+@@ -1168,6 +1609,12 @@ static void rtl8365mb_phylink_mac_config
+ return;
+ }
+
++ /* SGMII is handled by the SerDes PCS, configured through the
++ * phylink_pcs ops, so there is nothing to do here for it.
++ */
++ if (rtl8365mb_interface_is_serdes(state->interface))
++ return;
++
+ /* TODO: Implement MII and RMII modes, which the RTL8365MB-VC also
+ * supports
+ */
+@@ -1188,7 +1635,13 @@ static void rtl8365mb_phylink_mac_link_d
+ p = &mb->ports[port];
+ cancel_delayed_work_sync(&p->mib_work);
+
+- if (phy_interface_mode_is_rgmii(interface)) {
++ /* phylink has no pcs_link_down callback, so on the SerDes path only the
++ * MAC external interface force is reset here. Clearing the MAC force is
++ * enough to bring the link down; the SerDes keeps presenting its last
++ * forced state until the next pcs_link_up() reprograms it.
++ */
++ if (phy_interface_mode_is_rgmii(interface) ||
++ rtl8365mb_interface_is_serdes(interface)) {
+ ret = rtl8365mb_ext_config_forcemode(priv, port, false, 0, 0,
+ false, false);
+ if (ret)
+@@ -1218,14 +1671,51 @@ static void rtl8365mb_phylink_mac_link_u
+ p = &mb->ports[port];
+ schedule_delayed_work(&p->mib_work, 0);
+
+- if (phy_interface_mode_is_rgmii(interface)) {
++ /* The SerDes forced link state is programmed by the PCS in
++ * rtl8365mb_pcs_link_up(); here only the MAC external interface force
++ * is configured, for both RGMII and SerDes.
++ */
++ if (phy_interface_mode_is_rgmii(interface) ||
++ rtl8365mb_interface_is_serdes(interface)) {
+ ret = rtl8365mb_ext_config_forcemode(priv, port, true, speed,
+ duplex, tx_pause,
+ rx_pause);
+- if (ret)
++ if (ret) {
+ dev_err(priv->dev,
+ "failed to force mode on port %d: %pe\n", port,
+ ERR_PTR(ret));
++ return;
++ }
++
++ /* The SerDes has its own pause enables; program them from
++ * the resolved pause modes, as the vendor driver does when
++ * forcing the link on a SerDes external interface. These
++ * bits, not the MAC force pause bits, gate pause on the
++ * SerDes external interface: flow control testing shows
++ * that pause frames are only emitted with the SerDes TXFC
++ * bit set, while the MAC force pause bits alone have no
++ * effect on this port. This is done here rather than in
++ * rtl8365mb_pcs_link_up() because pcs_link_up() carries no
++ * pause information.
++ */
++ if (rtl8365mb_interface_is_serdes(interface)) {
++ u32 val = 0;
++
++ if (tx_pause)
++ val |= RTL8365MB_SDS_MISC_SGMII_TXFC_MASK;
++ if (rx_pause)
++ val |= RTL8365MB_SDS_MISC_SGMII_RXFC_MASK;
++
++ ret = regmap_update_bits(priv->map,
++ RTL8365MB_SDS_MISC_REG,
++ RTL8365MB_SDS_MISC_SGMII_TXFC_MASK |
++ RTL8365MB_SDS_MISC_SGMII_RXFC_MASK,
++ val);
++ if (ret)
++ dev_err(priv->dev,
++ "failed to force SerDes pause modes on port %d: %pe\n",
++ port, ERR_PTR(ret));
++ }
+
+ return;
+ }
+@@ -2419,6 +2909,14 @@ static int rtl8365mb_setup(struct dsa_sw
+ mb = priv->chip_data;
+ cpu = &mb->cpu;
+
++ mb->pcs.ops = &rtl8365mb_pcs_ops;
++
++ /* The SerDes has no link interrupt wired up, so phylink must poll the
++ * PCS for link changes when it tracks the link through pcs_get_state()
++ * (in-band mode with autonegotiation disabled).
++ */
++ mb->pcs.poll = true;
++
+ ret = rtl8365mb_reset_chip(priv);
+ if (ret) {
+ dev_err(priv->dev, "failed to reset chip: %pe\n",
+@@ -2426,6 +2924,13 @@ static int rtl8365mb_setup(struct dsa_sw
+ goto out_error;
+ }
+
++ ret = rtl8365mb_sds_probe_option(priv);
++ if (ret) {
++ dev_err(priv->dev, "failed to probe SerDes chip option: %pe\n",
++ ERR_PTR(ret));
++ goto out_error;
++ }
++
+ /* Configure switch to vendor-defined initial state */
+ ret = rtl8365mb_switch_init(priv);
+ if (ret) {
+@@ -2658,6 +3163,7 @@ static int rtl8365mb_detect(struct realt
+ }
+
+ static const struct phylink_mac_ops rtl8365mb_phylink_mac_ops = {
++ .mac_select_pcs = rtl8365mb_phylink_mac_select_pcs,
+ .mac_config = rtl8365mb_phylink_mac_config,
+ .mac_link_down = rtl8365mb_phylink_mac_link_down,
+ .mac_link_up = rtl8365mb_phylink_mac_link_up,
--- /dev/null
+From 987137345f3312fd68cc9c11bd46b754bfb0046f Mon Sep 17 00:00:00 2001
+From: Johan Alvarado <contact@c127.dev>
+Date: Sat, 11 Jul 2026 23:31:59 -0500
+Subject: net: dsa: realtek: rtl8365mb: add HSGMII support for RTL8367S
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+In addition to SGMII, the RTL8367S SerDes also supports HSGMII, which
+carries 2.5 Gbps with the same signaling as SGMII at 2.5x clock rate.
+The chip info table already declares HSGMII as a supported interface
+mode for external interface 1.
+
+Extend the SerDes PCS to handle HSGMII, which phylink represents as
+2500base-x:
+
+ - Select the HSGMII SerDes tuning parameters and external interface
+ mode, and mux the SerDes to MAC8 in HSGMII mode, from pcs_config()
+ according to the interface. The parameters are again lifted from the
+ GPL-licensed Realtek rtl8367c vendor driver, and again only cover
+ the tuning variant for a non-zero chip option, so the mode is gated
+ on the option probed at setup.
+
+ - Advertise 2500base-x and MAC_2500FD on ports whose external
+ interface supports HSGMII.
+
+ - Accept SPEED_2500 in the forced link configuration. The MAC speed
+ field has no 2.5 Gbps value: the rate is determined by the HSGMII
+ SerDes configuration, and the vendor driver programs the 1 Gbps
+ value here, so do the same.
+
+ - Raise the port 6 ingress and egress rate limiters to their maximum
+ at setup time, as the vendor switch init does unconditionally for
+ the whole chip family. The chip resets them to 0x1FFFF (~1.048 Gbps
+ in units of 8 Kbps), which caps the aggregate HSGMII throughput at
+ roughly 1 Gbps. The vendor documentation describes the reset
+ default as disabling the limiter, but the cap is real: on an
+ RTL8367S-based Mercusys MR85X running an OpenWrt backport of this
+ series, several clients on 1 Gbps user ports were limited to about
+ 1.02 Gbps combined across the HSGMII CPU port until these limiters
+ were raised, after which throughput reached about 2 Gbps [1]. The
+ related HSGMII scheduler line rate (LINE_RATE_HSG_H) is already set
+ to its maximum by the common init jam table.
+
+Tested on a Mercusys MR80X v2.20, where the RTL8367S is connected to
+the SoC over HSGMII.
+
+Link: https://github.com/openwrt/openwrt/pull/19445#issuecomment-4505613294 [1]
+Suggested-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
+Suggested-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
+Signed-off-by: Johan Alvarado <contact@c127.dev>
+Tested-by: Stanisław Pal <kuncy7@gmail.com>
+Reviewed-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
+Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
+Tested-by: Stanislaw Pal <kuncy7@gmail.com>
+Link: https://patch.msgid.link/20260711-rtl8367s-sgmii-v6-2-88f7944ddca7@c127.dev
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/rtl8365mb_main.c | 134 +++++++++++++++++++++++++++----
+ 1 file changed, 118 insertions(+), 16 deletions(-)
+
+--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -40,8 +40,8 @@
+ * driver has only been tested with a fixed-link, but in principle it should not
+ * matter.
+ *
+- * NOTE: Currently, only the RGMII and SGMII interfaces are implemented in this
+- * driver.
++ * NOTE: Currently, only the RGMII, SGMII and HSGMII interfaces are implemented
++ * in this driver.
+ *
+ * The interrupt line is asserted on link UP/DOWN events. The driver creates a
+ * custom irqchip to handle this interrupt and demultiplex the events by reading
+@@ -251,6 +251,18 @@
+ #define RTL8365MB_BYPASS_LINE_RATE_REG 0x03F7
+ #define RTL8365MB_BYPASS_LINE_RATE_MASK(_port) BIT((_port) - 5)
+
++/* Port 6 ingress and egress rate limiter registers. Each limit is a 19-bit
++ * value in units of 8 Kbps, split across a 16-bit LSB register (CTRL0) and a
++ * 3-bit MSB field (CTRL1). The chip resets them to 0x1FFFF; see
++ * rtl8365mb_sds_raise_rate_limits().
++ */
++#define RTL8365MB_INGRESSBW_PORT6_RATE_CTRL0_REG 0x00CF
++#define RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_REG 0x00D0
++#define RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_MASK 0x0007
++#define RTL8365MB_PORT6_EGRESSBW_CTRL0_REG 0x0398
++#define RTL8365MB_PORT6_EGRESSBW_CTRL1_REG 0x0399
++#define RTL8365MB_PORT6_EGRESSBW_CTRL1_MASK 0x0007
++
+ /* SerDes indirect access registers */
+ #define RTL8365MB_SDS_INDACS_CMD_REG 0x6600
+ #define RTL8365MB_SDS_INDACS_CMD_BUSY_MASK 0x0100
+@@ -637,6 +649,18 @@ static const struct rtl8365mb_jam_tbl_en
+ { 0x0424, 0xD810 }, { 0x002E, 0x83F2 },
+ };
+
++/* HSGMII SerDes tuning parameters, lifted from the vendor driver sources. As
++ * with the SGMII table, the vendor driver keeps several variants and selects
++ * one based on the chip option register; these are the values for a non-zero
++ * option, which is what RTL8367S parts seen so far report. See
++ * rtl8365mb_sds_probe_option().
++ */
++static const struct rtl8365mb_jam_tbl_entry rtl8365mb_sds_jam_hsgmii[] = {
++ { 0x0500, 0x82F0 }, { 0x0501, 0xF195 }, { 0x0502, 0x31A2 },
++ { 0x0503, 0x7960 }, { 0x0504, 0x9728 }, { 0x0423, 0x9D85 },
++ { 0x0424, 0xD810 }, { 0x0001, 0x0F80 }, { 0x002E, 0x83F2 },
++};
++
+ enum rtl8365mb_phy_interface_mode {
+ RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
+ RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
+@@ -1242,20 +1266,70 @@ static int rtl8365mb_sds_probe_option(st
+ return 0;
+ }
+
++/* The vendor driver raises the port 6 ingress and egress rate limiters to
++ * their maximum in its switch init, unconditionally for the whole chip
++ * family. The chip reset in rtl8365mb_setup() puts them back to their reset
++ * default of 0x1FFFF, a ~1.048 Gbps limit which caps the aggregate
++ * throughput of an HSGMII CPU port at roughly 1 Gbps. The vendor
++ * documentation describes the reset default as disabling the limiter, but
++ * the cap has been observed on hardware. Raise them likewise, to 0x7FFFF
++ * (~4.19 Gbps, above the HSGMII line rate). The related HSGMII scheduler
++ * line rate register (LINE_RATE_HSG_H, 0x03FA) is already set to its
++ * maximum by the common init jam table.
++ */
++static int rtl8365mb_sds_raise_rate_limits(struct realtek_priv *priv)
++{
++ int ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_INGRESSBW_PORT6_RATE_CTRL0_REG,
++ 0xFFFF);
++ if (ret)
++ return ret;
++
++ ret = regmap_update_bits(priv->map,
++ RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_REG,
++ RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_MASK,
++ RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_MASK);
++ if (ret)
++ return ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_PORT6_EGRESSBW_CTRL0_REG,
++ 0xFFFF);
++ if (ret)
++ return ret;
++
++ return regmap_update_bits(priv->map, RTL8365MB_PORT6_EGRESSBW_CTRL1_REG,
++ RTL8365MB_PORT6_EGRESSBW_CTRL1_MASK,
++ RTL8365MB_PORT6_EGRESSBW_CTRL1_MASK);
++}
++
+ static int rtl8365mb_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
+ phy_interface_t interface,
+ const unsigned long *advertising,
+ bool permit_pause_to_mac)
+ {
++ const struct rtl8365mb_jam_tbl_entry *sds_jam;
+ const int id = RTL8365MB_SDS_EXT_INTERFACE_ID;
+ struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
+ struct realtek_priv *priv;
++ size_t sds_jam_size;
++ u32 mode;
+ u16 val;
+ int ret;
+ int i;
+
+ priv = mb->priv;
+
++ if (interface == PHY_INTERFACE_MODE_2500BASEX) {
++ sds_jam = rtl8365mb_sds_jam_hsgmii;
++ sds_jam_size = ARRAY_SIZE(rtl8365mb_sds_jam_hsgmii);
++ mode = RTL8365MB_EXT_PORT_MODE_HSGMII;
++ } else {
++ sds_jam = rtl8365mb_sds_jam_sgmii;
++ sds_jam_size = ARRAY_SIZE(rtl8365mb_sds_jam_sgmii);
++ mode = RTL8365MB_EXT_PORT_MODE_SGMII;
++ }
++
+ /* Hold the embedded DW8051 microcontroller in reset and keep it
+ * disabled. The vendor driver loads firmware into it to manage the
+ * SerDes link, but the firmware only duplicates work that phylink
+@@ -1283,24 +1357,24 @@ static int rtl8365mb_pcs_config(struct p
+ return ret;
+
+ /* Tune the SerDes with vendor-prescribed parameters */
+- for (i = 0; i < ARRAY_SIZE(rtl8365mb_sds_jam_sgmii); i++) {
+- ret = rtl8365mb_sds_write(priv,
+- rtl8365mb_sds_jam_sgmii[i].reg,
+- rtl8365mb_sds_jam_sgmii[i].val);
++ for (i = 0; i < sds_jam_size; i++) {
++ ret = rtl8365mb_sds_write(priv, sds_jam[i].reg,
++ sds_jam[i].val);
+ if (ret)
+ return ret;
+ }
+
+- /* Mux the SerDes to MAC8 in SGMII mode */
++ /* Mux the SerDes to MAC8 in the requested mode */
+ ret = regmap_update_bits(priv->map, RTL8365MB_SDS_MISC_REG,
+ RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK |
+ RTL8365MB_SDS_MISC_MAC8_SEL_HSGMII_MASK,
+- RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK);
++ mode == RTL8365MB_EXT_PORT_MODE_SGMII ?
++ RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK :
++ RTL8365MB_SDS_MISC_MAC8_SEL_HSGMII_MASK);
+ if (ret)
+ return ret;
+
+- val = RTL8365MB_EXT_PORT_MODE_SGMII
+- << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(id);
++ val = mode << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(id);
+ ret = regmap_update_bits(priv->map,
+ RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(id),
+ RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(id),
+@@ -1346,7 +1420,8 @@ static int rtl8365mb_pcs_config(struct p
+
+ static bool rtl8365mb_interface_is_serdes(phy_interface_t interface)
+ {
+- return interface == PHY_INTERFACE_MODE_SGMII;
++ return interface == PHY_INTERFACE_MODE_SGMII ||
++ interface == PHY_INTERFACE_MODE_2500BASEX;
+ }
+
+ static unsigned int rtl8365mb_pcs_inband_caps(struct phylink_pcs *pcs,
+@@ -1400,7 +1475,9 @@ static void rtl8365mb_pcs_get_state(stru
+
+ switch (FIELD_GET(RTL8365MB_SDS_MISC_SGMII_SPD_MASK, val)) {
+ case RTL8365MB_PORT_SPEED_1000M:
+- state->speed = SPEED_1000;
++ state->speed =
++ state->interface == PHY_INTERFACE_MODE_2500BASEX ?
++ SPEED_2500 : SPEED_1000;
+ break;
+ case RTL8365MB_PORT_SPEED_100M:
+ state->speed = SPEED_100;
+@@ -1425,7 +1502,11 @@ static void rtl8365mb_pcs_link_up(struct
+ u32 r_speed;
+ int ret;
+
+- if (speed == SPEED_1000) {
++ /* The speed field has no value for 2.5 Gbps: the rate is determined by
++ * the HSGMII SerDes configuration, and the vendor driver programs the
++ * 1 Gbps value here.
++ */
++ if (speed == SPEED_2500 || speed == SPEED_1000) {
+ r_speed = RTL8365MB_PORT_SPEED_1000M;
+ } else if (speed == SPEED_100) {
+ r_speed = RTL8365MB_PORT_SPEED_100M;
+@@ -1485,7 +1566,11 @@ static int rtl8365mb_ext_config_forcemod
+ r_rx_pause = rx_pause ? 1 : 0;
+ r_tx_pause = tx_pause ? 1 : 0;
+
+- if (speed == SPEED_1000) {
++ /* The speed field has no value for 2.5 Gbps: the rate is
++ * determined by the HSGMII SerDes configuration, and the
++ * vendor driver programs the 1 Gbps value here.
++ */
++ if (speed == SPEED_2500 || speed == SPEED_1000) {
+ r_speed = RTL8365MB_PORT_SPEED_1000M;
+ } else if (speed == SPEED_100) {
+ r_speed = RTL8365MB_PORT_SPEED_100M;
+@@ -1568,6 +1653,13 @@ static void rtl8365mb_phylink_get_caps(s
+ mb->sds_supported)
+ __set_bit(PHY_INTERFACE_MODE_SGMII,
+ config->supported_interfaces);
++
++ if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_HSGMII &&
++ mb->sds_supported) {
++ __set_bit(PHY_INTERFACE_MODE_2500BASEX,
++ config->supported_interfaces);
++ config->mac_capabilities |= MAC_2500FD;
++ }
+ }
+
+ static struct phylink_pcs *
+@@ -1609,8 +1701,8 @@ static void rtl8365mb_phylink_mac_config
+ return;
+ }
+
+- /* SGMII is handled by the SerDes PCS, configured through the
+- * phylink_pcs ops, so there is nothing to do here for it.
++ /* SGMII and 2500base-x are handled by the SerDes PCS, configured
++ * through the phylink_pcs ops, so nothing to do here for them.
+ */
+ if (rtl8365mb_interface_is_serdes(state->interface))
+ return;
+@@ -2939,6 +3031,16 @@ static int rtl8365mb_setup(struct dsa_sw
+ goto out_error;
+ }
+
++ if (mb->sds_supported) {
++ ret = rtl8365mb_sds_raise_rate_limits(priv);
++ if (ret) {
++ dev_err(priv->dev,
++ "failed to raise port rate limits: %pe\n",
++ ERR_PTR(ret));
++ goto out_error;
++ }
++ }
++
+ /* Set up cascading IRQs */
+ ret = rtl8365mb_irq_setup(priv);
+ if (ret == -EPROBE_DEFER)
--- /dev/null
+From 0b577e2fe06c023ab996c3d7684538dbbf6e99bc Mon Sep 17 00:00:00 2001
+From: Johan Alvarado <contact@c127.dev>
+Date: Sat, 11 Jul 2026 23:31:58 -0500
+Subject: net: dsa: realtek: rtl8365mb: add SGMII support for RTL8367S
+
+The RTL8367S can mux its embedded SerDes to external interface 1,
+which is typically used to connect the switch to a CPU port. The chip
+info table already declares SGMII as a supported interface mode for
+this chip, but the driver only implements RGMII so far.
+
+Implement SGMII support as a phylink PCS, with the configuration
+sequence derived from the GPL-licensed Realtek rtl8367c vendor driver
+as distributed in the Mercusys MR80X GPL code drop:
+
+ - Add accessors for the SerDes indirect access registers (SDS_INDACS),
+ through which the SerDes internal registers are reached.
+
+ - Register a phylink_pcs for the SerDes, selected from mac_select_pcs
+ for the SGMII interface, so the SerDes handling lives in the PCS
+ operations rather than in the MAC operations.
+
+ - Probe the SerDes tuning variant from the chip option register once
+ at setup. The vendor driver keeps two sets of SerDes tuning
+ parameters and selects between them based on this option; only the
+ variant for a non-zero option (which all RTL8367S parts seen so far
+ report) has been validated on hardware, so the SerDes interface
+ modes are only advertised in that case. An unsupported variant thus
+ fails at phylink validation time instead of at link configuration
+ time.
+
+ - Keep the embedded DW8051 microcontroller in reset and disabled. The
+ vendor driver loads firmware into it to manage the SerDes link, but
+ analysis of that firmware shows it only duplicates the link
+ management phylink already performs: it polls the port status and
+ writes the external interface force registers behind the driver's
+ back.
+
+ - Clear the line rate bypass bit for the external interface, tune the
+ SerDes with the vendor-prescribed parameters, mux the SerDes to MAC8
+ in SGMII mode and only then take the SerDes out of reset, as the
+ vendor driver does.
+
+ - After deasserting the SerDes reset, reset the SerDes data path via
+ the SerDes BMCR register to flush the FIFOs and resync the PLL.
+ This mirrors what the vendor firmware does right after deasserting
+ the SerDes reset, and ensures a clean link state from cold boot.
+
+ - Force the SGMII link parameters (link, speed, duplex) in the SDS_MISC
+ register from pcs_link_up(). SGMII in-band autonegotiation is not
+ implemented, so only fixed-link and conventional PHY setups are
+ supported, just like RGMII. This is reported to phylink through
+ pcs_inband_caps() returning LINK_INBAND_DISABLE, so phylink never
+ selects an in-band-enabled negotiation mode for this PCS.
+
+ - Program the SerDes pause enables in SDS_MISC from the resolved
+ pause modes when forcing the MAC external interface in mac_link_up,
+ as the vendor driver does, rather than leaving whatever state the
+ boot firmware left there. Flow control testing shows these bits,
+ not the MAC force pause bits, gate pause on the SerDes external
+ interface. This is done in the MAC layer because pcs_link_up()
+ carries no pause information.
+
+ - Implement pcs_get_state() by reading the link status from the
+ SerDes, with the forced speed and duplex read back from SDS_MISC.
+ Although the supported fixed-link and conventional PHY setups do not
+ use it, the PCS owns the SerDes link state, and phylink consults
+ pcs_get_state() to track the physical link when operating in in-band
+ mode with autonegotiation disabled. The SerDes has no link interrupt
+ wired up, so the PCS sets its poll flag.
+
+Tested on a Mercusys MR80X v2.20, where the RTL8367S is connected to
+the SoC over SGMII.
+
+Suggested-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
+Suggested-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
+Suggested-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
+Signed-off-by: Johan Alvarado <contact@c127.dev>
+Reviewed-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
+Reviewed-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
+Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
+Tested-by: Stanislaw Pal <kuncy7@gmail.com>
+Link: https://patch.msgid.link/20260711-rtl8367s-sgmii-v6-1-88f7944ddca7@c127.dev
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/rtl8365mb_main.c | 515 ++++++++++++++++++++++++++++++-
+ 1 file changed, 511 insertions(+), 4 deletions(-)
+
+--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -40,7 +40,8 @@
+ * driver has only been tested with a fixed-link, but in principle it should not
+ * matter.
+ *
+- * NOTE: Currently, only the RGMII interface is implemented in this driver.
++ * NOTE: Currently, only the RGMII and SGMII interfaces are implemented in this
++ * driver.
+ *
+ * The interrupt line is asserted on link UP/DOWN events. The driver creates a
+ * custom irqchip to handle this interrupt and demultiplex the events by reading
+@@ -94,11 +95,13 @@
+ #include <linux/bitops.h>
+ #include <linux/interrupt.h>
+ #include <linux/irqdomain.h>
++#include <linux/mii.h>
+ #include <linux/mutex.h>
+ #include <linux/of_irq.h>
+ #include <linux/regmap.h>
+ #include <linux/if_bridge.h>
+ #include <linux/if_vlan.h>
++#include <linux/phylink.h>
+
+ #include "realtek.h"
+ #include "realtek-smi.h"
+@@ -129,6 +132,7 @@
+
+ /* Chip reset register */
+ #define RTL8365MB_CHIP_RESET_REG 0x1322
++#define RTL8365MB_CHIP_RESET_DW8051_MASK 0x0010
+ #define RTL8365MB_CHIP_RESET_SW_MASK 0x0002
+ #define RTL8365MB_CHIP_RESET_HW_MASK 0x0001
+
+@@ -238,6 +242,76 @@
+ #define RTL8365MB_EXT_RGMXF_RXDELAY_MASK 0x0007
+ #define RTL8365MB_EXT_RGMXF_TXDELAY_MASK 0x0008
+
++/* External interface line rate bypass register - one bit per external
++ * interface, indexed by the external port number with port 5 (the first
++ * external port) as the base. Other RTL8367 families index this register
++ * differently (e.g. the RTL8367R uses (id + 1) % 2), so this mapping only
++ * holds for the RTL8367C-style parts this driver supports.
++ */
++#define RTL8365MB_BYPASS_LINE_RATE_REG 0x03F7
++#define RTL8365MB_BYPASS_LINE_RATE_MASK(_port) BIT((_port) - 5)
++
++/* SerDes indirect access registers */
++#define RTL8365MB_SDS_INDACS_CMD_REG 0x6600
++#define RTL8365MB_SDS_INDACS_CMD_BUSY_MASK 0x0100
++#define RTL8365MB_SDS_INDACS_CMD_RUN_MASK 0x0080
++#define RTL8365MB_SDS_INDACS_CMD_WR_MASK 0x0040
++#define RTL8365MB_SDS_INDACS_ADR_REG 0x6601
++#define RTL8365MB_SDS_INDACS_DATA_REG 0x6602
++
++/* SerDes miscellaneous configuration register */
++#define RTL8365MB_SDS_MISC_REG 0x1D11
++#define RTL8365MB_SDS_MISC_SGMII_RXFC_MASK 0x4000
++#define RTL8365MB_SDS_MISC_SGMII_TXFC_MASK 0x2000
++#define RTL8365MB_SDS_MISC_MAC8_SEL_HSGMII_MASK 0x0800
++#define RTL8365MB_SDS_MISC_SGMII_FDUP_MASK 0x0400
++#define RTL8365MB_SDS_MISC_SGMII_LINK_MASK 0x0200
++#define RTL8365MB_SDS_MISC_SGMII_SPD_MASK 0x0180
++#define RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK 0x0040
++
++/* SerDes internal registers, accessed via the SDS_INDACS registers. The BMCR
++ * data path reset holds BMCR_ANENABLE | BMCR_ISOLATE while toggling the
++ * vendor-specific low bits from phase 1 to phase 2, which triggers a data path
++ * reset and PLL resync.
++ */
++#define RTL8365MB_SDS_REG_BMCR 0x0000
++#define RTL8365MB_SDS_BMCR_DPRST_PHASE1 (BMCR_ANENABLE | BMCR_ISOLATE | 0x1)
++#define RTL8365MB_SDS_BMCR_DPRST_PHASE2 (BMCR_ANENABLE | BMCR_ISOLATE | 0x3)
++#define RTL8365MB_SDS_REG_NWAY 0x0002
++#define RTL8365MB_SDS_NWAY_EN_MASK 0x0200
++#define RTL8365MB_SDS_NWAY_RESTART_MASK 0x0100
++#define RTL8365MB_SDS_REG_RESET 0x0003
++#define RTL8365MB_SDS_RESET_DEASSERT 0x7106
++#define RTL8365MB_SDS_REG_LINK_STATUS 0x003d
++#define RTL8365MB_SDS_LINK_STATUS_LINK_MASK 0x0010
++
++/* The embedded SerDes can only be muxed to external interface 1 (MAC8),
++ * which is port 6.
++ */
++#define RTL8365MB_SDS_EXT_INTERFACE_ID 1
++#define RTL8365MB_SDS_EXT_INTERFACE_PORT 6
++
++/* Line rate bypass bit for the SerDes external interface */
++#define RTL8365MB_SDS_BYPASS_LINE_RATE_MASK \
++ RTL8365MB_BYPASS_LINE_RATE_MASK(RTL8365MB_SDS_EXT_INTERFACE_PORT)
++
++/* SerDes tuning parameter variant selector. The vendor driver picks between
++ * two sets of SerDes tuning parameters based on this chip option. Reading it
++ * requires first arming the read by writing a magic key to the arm register,
++ * then disarming it afterwards.
++ */
++#define RTL8365MB_SDS_OPTION_ARM_REG 0x13C0
++#define RTL8365MB_SDS_OPTION_ARM_KEY 0x0249
++#define RTL8365MB_SDS_OPTION_REG 0x13C1
++
++/* Embedded DW8051 microcontroller control registers. The microcontroller
++ * can run firmware to manage the SerDes link, but this driver keeps it in
++ * reset and disabled: phylink already performs the link management that
++ * the firmware would otherwise do.
++ */
++#define RTL8365MB_MISC_CFG0_REG 0x130C
++#define RTL8365MB_MISC_CFG0_DW8051_EN_MASK 0x0020
++
+ /* External interface port speed values - used in DIGITAL_INTERFACE_FORCE */
+ #define RTL8365MB_PORT_SPEED_10M 0
+ #define RTL8365MB_PORT_SPEED_100M 1
+@@ -551,6 +625,18 @@ static const struct rtl8365mb_jam_tbl_en
+ { 0x1D32, 0x0002 },
+ };
+
++/* SGMII SerDes tuning parameters, lifted from the vendor driver sources. The
++ * vendor driver keeps two variants of this table and selects between them
++ * based on the chip option register; these are the values for a non-zero
++ * option, which is what RTL8367S parts seen so far report. See
++ * rtl8365mb_sds_probe_option().
++ */
++static const struct rtl8365mb_jam_tbl_entry rtl8365mb_sds_jam_sgmii[] = {
++ { 0x0480, 0x04D7 }, { 0x0481, 0xF994 }, { 0x0482, 0x2420 },
++ { 0x0483, 0x6960 }, { 0x0484, 0x9728 }, { 0x0423, 0x9D85 },
++ { 0x0424, 0xD810 }, { 0x002E, 0x83F2 },
++};
++
+ enum rtl8365mb_phy_interface_mode {
+ RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
+ RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
+@@ -730,6 +816,9 @@ struct rtl8365mb_port {
+ * @cpu: CPU tagging and CPU port configuration for this chip
+ * @mib_lock: prevent concurrent reads of MIB counters
+ * @ports: per-port data
++ * @pcs: PCS for the SerDes external interface
++ * @sds_supported: SerDes tuning parameters match the chip option, so the
++ * SerDes interface modes can be advertised
+ *
+ * Private data for this driver.
+ */
+@@ -740,8 +829,12 @@ struct rtl8365mb {
+ struct rtl8365mb_cpu cpu;
+ struct mutex mib_lock;
+ struct rtl8365mb_port ports[RTL8365MB_MAX_NUM_PORTS];
++ struct phylink_pcs pcs;
++ bool sds_supported;
+ };
+
++#define pcs_to_rtl8365mb(_pcs) container_of((_pcs), struct rtl8365mb, pcs)
++
+ static int rtl8365mb_phy_poll_busy(struct realtek_priv *priv)
+ {
+ u32 val;
+@@ -1042,6 +1135,334 @@ static int rtl8365mb_ext_config_rgmii(st
+ return 0;
+ }
+
++static int rtl8365mb_sds_write(struct realtek_priv *priv, u16 addr, u16 data)
++{
++ int ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_INDACS_DATA_REG, data);
++ if (ret)
++ return ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_INDACS_ADR_REG, addr);
++ if (ret)
++ return ret;
++
++ /* The SerDes indirect access engine completes the command within the
++ * register write transaction, so there is no need to wait or poll for
++ * completion before the next access, matching the vendor driver.
++ */
++ return regmap_write(priv->map, RTL8365MB_SDS_INDACS_CMD_REG,
++ RTL8365MB_SDS_INDACS_CMD_RUN_MASK |
++ RTL8365MB_SDS_INDACS_CMD_WR_MASK);
++}
++
++static int rtl8365mb_sds_read(struct realtek_priv *priv, u16 addr, u16 *data)
++{
++ u32 val;
++ int ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_INDACS_ADR_REG, addr);
++ if (ret)
++ return ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_INDACS_CMD_REG,
++ RTL8365MB_SDS_INDACS_CMD_RUN_MASK);
++ if (ret)
++ return ret;
++
++ /* Wait for the indirect read to complete: the engine clears the BUSY
++ * bit once the data register holds the result.
++ */
++ ret = regmap_read_poll_timeout(priv->map, RTL8365MB_SDS_INDACS_CMD_REG,
++ val,
++ !(val & RTL8365MB_SDS_INDACS_CMD_BUSY_MASK),
++ 10, 1000);
++ if (ret)
++ return ret;
++
++ ret = regmap_read(priv->map, RTL8365MB_SDS_INDACS_DATA_REG, &val);
++ if (ret)
++ return ret;
++
++ *data = val;
++
++ return 0;
++}
++
++/* The vendor driver selects between two sets of SerDes tuning parameters based
++ * on the chip option register. Only the variant for a non-zero option has been
++ * tested on real hardware - the RTL8367S parts seen so far all report 1. The
++ * variant for option 0 uses different tuning values that cannot be verified,
++ * so probe the option once at setup and only advertise the SerDes interface
++ * modes when the tuning parameters are known to match, so that an unsupported
++ * variant fails at phylink validation time rather than when configuring the
++ * link.
++ */
++static int rtl8365mb_sds_probe_option(struct realtek_priv *priv)
++{
++ struct rtl8365mb *mb = priv->chip_data;
++ const struct rtl8365mb_extint *extint;
++ u32 option;
++ int ret;
++ int i;
++
++ /* Nothing to probe if no external interface is wired to the SerDes */
++ for (i = 0; i < RTL8365MB_MAX_NUM_EXTINTS; i++) {
++ extint = &mb->chip_info->extints[i];
++
++ if (extint->supported_interfaces &
++ (RTL8365MB_PHY_INTERFACE_MODE_SGMII |
++ RTL8365MB_PHY_INTERFACE_MODE_HSGMII))
++ break;
++ }
++ if (i == RTL8365MB_MAX_NUM_EXTINTS)
++ return 0;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_OPTION_ARM_REG,
++ RTL8365MB_SDS_OPTION_ARM_KEY);
++ if (ret)
++ return ret;
++
++ ret = regmap_read(priv->map, RTL8365MB_SDS_OPTION_REG, &option);
++ if (ret)
++ return ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_SDS_OPTION_ARM_REG, 0);
++ if (ret)
++ return ret;
++
++ if (option == 0) {
++ dev_warn(priv->dev,
++ "unsupported SerDes tuning variant (chip option 0), disabling SerDes interface modes\n");
++ return 0;
++ }
++
++ mb->sds_supported = true;
++
++ return 0;
++}
++
++static int rtl8365mb_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
++ phy_interface_t interface,
++ const unsigned long *advertising,
++ bool permit_pause_to_mac)
++{
++ const int id = RTL8365MB_SDS_EXT_INTERFACE_ID;
++ struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
++ struct realtek_priv *priv;
++ u16 val;
++ int ret;
++ int i;
++
++ priv = mb->priv;
++
++ /* Hold the embedded DW8051 microcontroller in reset and keep it
++ * disabled. The vendor driver loads firmware into it to manage the
++ * SerDes link, but the firmware only duplicates work that phylink
++ * already does: it polls the port status and forces the external
++ * interface configuration in the very registers this driver manages.
++ * Letting it run would race with phylink.
++ */
++ ret = regmap_update_bits(priv->map, RTL8365MB_CHIP_RESET_REG,
++ RTL8365MB_CHIP_RESET_DW8051_MASK,
++ RTL8365MB_CHIP_RESET_DW8051_MASK);
++ if (ret)
++ return ret;
++
++ ret = regmap_update_bits(priv->map, RTL8365MB_MISC_CFG0_REG,
++ RTL8365MB_MISC_CFG0_DW8051_EN_MASK, 0);
++ if (ret)
++ return ret;
++
++ /* The vendor driver clears the line rate bypass for all interface
++ * modes except TMII.
++ */
++ ret = regmap_update_bits(priv->map, RTL8365MB_BYPASS_LINE_RATE_REG,
++ RTL8365MB_SDS_BYPASS_LINE_RATE_MASK, 0);
++ if (ret)
++ return ret;
++
++ /* Tune the SerDes with vendor-prescribed parameters */
++ for (i = 0; i < ARRAY_SIZE(rtl8365mb_sds_jam_sgmii); i++) {
++ ret = rtl8365mb_sds_write(priv,
++ rtl8365mb_sds_jam_sgmii[i].reg,
++ rtl8365mb_sds_jam_sgmii[i].val);
++ if (ret)
++ return ret;
++ }
++
++ /* Mux the SerDes to MAC8 in SGMII mode */
++ ret = regmap_update_bits(priv->map, RTL8365MB_SDS_MISC_REG,
++ RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK |
++ RTL8365MB_SDS_MISC_MAC8_SEL_HSGMII_MASK,
++ RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK);
++ if (ret)
++ return ret;
++
++ val = RTL8365MB_EXT_PORT_MODE_SGMII
++ << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(id);
++ ret = regmap_update_bits(priv->map,
++ RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(id),
++ RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(id),
++ val);
++ if (ret)
++ return ret;
++
++ /* Take the SerDes out of reset. The vendor driver does this only
++ * after the SerDes mux and the interface mode are configured.
++ */
++ ret = rtl8365mb_sds_write(priv, RTL8365MB_SDS_REG_RESET,
++ RTL8365MB_SDS_RESET_DEASSERT);
++ if (ret)
++ return ret;
++
++ /* Reset the SerDes data path and resync its PLL, mirroring what the
++ * vendor firmware does right after deasserting the SerDes reset.
++ * This flushes the FIFOs and ensures a clean state for the link,
++ * preventing silent drops and CRC errors.
++ */
++ ret = rtl8365mb_sds_write(priv, RTL8365MB_SDS_REG_BMCR,
++ RTL8365MB_SDS_BMCR_DPRST_PHASE1);
++ if (ret)
++ return ret;
++
++ ret = rtl8365mb_sds_write(priv, RTL8365MB_SDS_REG_BMCR,
++ RTL8365MB_SDS_BMCR_DPRST_PHASE2);
++ if (ret)
++ return ret;
++
++ /* Keep SGMII in-band autonegotiation disabled: the link parameters are
++ * forced from rtl8365mb_pcs_link_up() instead.
++ */
++ ret = rtl8365mb_sds_read(priv, RTL8365MB_SDS_REG_NWAY, &val);
++ if (ret)
++ return ret;
++
++ val &= ~RTL8365MB_SDS_NWAY_EN_MASK;
++ val |= RTL8365MB_SDS_NWAY_RESTART_MASK;
++
++ return rtl8365mb_sds_write(priv, RTL8365MB_SDS_REG_NWAY, val);
++}
++
++static bool rtl8365mb_interface_is_serdes(phy_interface_t interface)
++{
++ return interface == PHY_INTERFACE_MODE_SGMII;
++}
++
++static unsigned int rtl8365mb_pcs_inband_caps(struct phylink_pcs *pcs,
++ phy_interface_t interface)
++{
++ /* In-band autonegotiation is not implemented; the link is always
++ * forced. Report that to phylink so that it never selects an
++ * in-band-enabled negotiation mode for this PCS.
++ */
++ return LINK_INBAND_DISABLE;
++}
++
++static void rtl8365mb_pcs_get_state(struct phylink_pcs *pcs,
++ unsigned int neg_mode,
++ struct phylink_link_state *state)
++{
++ struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
++ struct realtek_priv *priv = mb->priv;
++ u16 status;
++ u32 val;
++ int ret;
++
++ /* In-band autonegotiation is not implemented, so the link parameters are
++ * forced from rtl8365mb_pcs_link_up(). The real link state must still be
++ * read from the SerDes itself: the embedded DW8051 microcontroller that
++ * the vendor firmware uses to poll the SerDes is kept disabled (see
++ * rtl8365mb_pcs_config()), so the link status register can be read
++ * directly through the SDS_INDACS window without racing the auto-poll.
++ */
++ ret = rtl8365mb_sds_read(priv, RTL8365MB_SDS_REG_LINK_STATUS, &status);
++ if (ret) {
++ state->link = false;
++ return;
++ }
++
++ state->link = !!(status & RTL8365MB_SDS_LINK_STATUS_LINK_MASK);
++ state->an_complete = state->link;
++ if (!state->link)
++ return;
++
++ /* The speed and duplex are forced; read them back from the values
++ * programmed into the SerDes MISC register.
++ */
++ ret = regmap_read(priv->map, RTL8365MB_SDS_MISC_REG, &val);
++ if (ret) {
++ state->link = false;
++ return;
++ }
++
++ state->duplex = (val & RTL8365MB_SDS_MISC_SGMII_FDUP_MASK) ?
++ DUPLEX_FULL : DUPLEX_HALF;
++
++ switch (FIELD_GET(RTL8365MB_SDS_MISC_SGMII_SPD_MASK, val)) {
++ case RTL8365MB_PORT_SPEED_1000M:
++ state->speed = SPEED_1000;
++ break;
++ case RTL8365MB_PORT_SPEED_100M:
++ state->speed = SPEED_100;
++ break;
++ case RTL8365MB_PORT_SPEED_10M:
++ state->speed = SPEED_10;
++ break;
++ }
++}
++
++static void rtl8365mb_pcs_link_up(struct phylink_pcs *pcs,
++ unsigned int neg_mode,
++ phy_interface_t interface, int speed,
++ int duplex)
++{
++ struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
++ struct realtek_priv *priv = mb->priv;
++ u32 mask = RTL8365MB_SDS_MISC_SGMII_FDUP_MASK |
++ RTL8365MB_SDS_MISC_SGMII_LINK_MASK |
++ RTL8365MB_SDS_MISC_SGMII_SPD_MASK;
++ u32 val = RTL8365MB_SDS_MISC_SGMII_LINK_MASK;
++ u32 r_speed;
++ int ret;
++
++ if (speed == SPEED_1000) {
++ r_speed = RTL8365MB_PORT_SPEED_1000M;
++ } else if (speed == SPEED_100) {
++ r_speed = RTL8365MB_PORT_SPEED_100M;
++ } else if (speed == SPEED_10) {
++ r_speed = RTL8365MB_PORT_SPEED_10M;
++ } else {
++ dev_err(priv->dev, "unsupported SerDes speed %s\n",
++ phy_speed_to_str(speed));
++ return;
++ }
++
++ val |= FIELD_PREP(RTL8365MB_SDS_MISC_SGMII_SPD_MASK, r_speed);
++
++ if (duplex == DUPLEX_FULL)
++ val |= RTL8365MB_SDS_MISC_SGMII_FDUP_MASK;
++
++ /* pcs_link_up() carries no pause information, so the SerDes flow
++ * control bits are programmed together with the MAC external interface
++ * force from rtl8365mb_phylink_mac_link_up(), where the resolved pause
++ * modes are known.
++ */
++ ret = regmap_update_bits(priv->map, RTL8365MB_SDS_MISC_REG, mask, val);
++ if (ret) {
++ dev_err(priv->dev, "failed to force SerDes link: %pe\n",
++ ERR_PTR(ret));
++ return;
++ }
++}
++
++static const struct phylink_pcs_ops rtl8365mb_pcs_ops = {
++ .pcs_inband_caps = rtl8365mb_pcs_inband_caps,
++ .pcs_config = rtl8365mb_pcs_config,
++ .pcs_get_state = rtl8365mb_pcs_get_state,
++ .pcs_link_up = rtl8365mb_pcs_link_up,
++};
++
+ static int rtl8365mb_ext_config_forcemode(struct realtek_priv *priv, int port,
+ bool link, int speed, int duplex,
+ bool tx_pause, bool rx_pause)
+@@ -1118,6 +1539,8 @@ static void rtl8365mb_phylink_get_caps(s
+ {
+ const struct rtl8365mb_extint *extint =
+ rtl8365mb_get_port_extint(ds->priv, port);
++ struct realtek_priv *priv = ds->priv;
++ struct rtl8365mb *mb = priv->chip_data;
+
+ config->mac_capabilities = MAC_SYM_PAUSE | MAC_ASYM_PAUSE |
+ MAC_10 | MAC_100 | MAC_1000FD;
+@@ -1141,6 +1564,25 @@ static void rtl8365mb_phylink_get_caps(s
+
+ if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_RGMII)
+ phy_interface_set_rgmii(config->supported_interfaces);
++
++ if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_SGMII &&
++ mb->sds_supported)
++ __set_bit(PHY_INTERFACE_MODE_SGMII,
++ config->supported_interfaces);
++}
++
++static struct phylink_pcs *
++rtl8365mb_phylink_mac_select_pcs(struct phylink_config *config,
++ phy_interface_t interface)
++{
++ struct dsa_port *dp = dsa_phylink_to_port(config);
++ struct realtek_priv *priv = dp->ds->priv;
++ struct rtl8365mb *mb = priv->chip_data;
++
++ if (rtl8365mb_interface_is_serdes(interface))
++ return &mb->pcs;
++
++ return NULL;
+ }
+
+ static void rtl8365mb_phylink_mac_config(struct phylink_config *config,
+@@ -1168,6 +1610,12 @@ static void rtl8365mb_phylink_mac_config
+ return;
+ }
+
++ /* SGMII is handled by the SerDes PCS, configured through the
++ * phylink_pcs ops, so there is nothing to do here for it.
++ */
++ if (rtl8365mb_interface_is_serdes(state->interface))
++ return;
++
+ /* TODO: Implement MII and RMII modes, which the RTL8365MB-VC also
+ * supports
+ */
+@@ -1188,7 +1636,13 @@ static void rtl8365mb_phylink_mac_link_d
+ p = &mb->ports[port];
+ cancel_delayed_work_sync(&p->mib_work);
+
+- if (phy_interface_mode_is_rgmii(interface)) {
++ /* phylink has no pcs_link_down callback, so on the SerDes path only the
++ * MAC external interface force is reset here. Clearing the MAC force is
++ * enough to bring the link down; the SerDes keeps presenting its last
++ * forced state until the next pcs_link_up() reprograms it.
++ */
++ if (phy_interface_mode_is_rgmii(interface) ||
++ rtl8365mb_interface_is_serdes(interface)) {
+ ret = rtl8365mb_ext_config_forcemode(priv, port, false, 0, 0,
+ false, false);
+ if (ret)
+@@ -1218,14 +1672,51 @@ static void rtl8365mb_phylink_mac_link_u
+ p = &mb->ports[port];
+ schedule_delayed_work(&p->mib_work, 0);
+
+- if (phy_interface_mode_is_rgmii(interface)) {
++ /* The SerDes forced link state is programmed by the PCS in
++ * rtl8365mb_pcs_link_up(); here only the MAC external interface force
++ * is configured, for both RGMII and SerDes.
++ */
++ if (phy_interface_mode_is_rgmii(interface) ||
++ rtl8365mb_interface_is_serdes(interface)) {
+ ret = rtl8365mb_ext_config_forcemode(priv, port, true, speed,
+ duplex, tx_pause,
+ rx_pause);
+- if (ret)
++ if (ret) {
+ dev_err(priv->dev,
+ "failed to force mode on port %d: %pe\n", port,
+ ERR_PTR(ret));
++ return;
++ }
++
++ /* The SerDes has its own pause enables; program them from
++ * the resolved pause modes, as the vendor driver does when
++ * forcing the link on a SerDes external interface. These
++ * bits, not the MAC force pause bits, gate pause on the
++ * SerDes external interface: flow control testing shows
++ * that pause frames are only emitted with the SerDes TXFC
++ * bit set, while the MAC force pause bits alone have no
++ * effect on this port. This is done here rather than in
++ * rtl8365mb_pcs_link_up() because pcs_link_up() carries no
++ * pause information.
++ */
++ if (rtl8365mb_interface_is_serdes(interface)) {
++ u32 val = 0;
++
++ if (tx_pause)
++ val |= RTL8365MB_SDS_MISC_SGMII_TXFC_MASK;
++ if (rx_pause)
++ val |= RTL8365MB_SDS_MISC_SGMII_RXFC_MASK;
++
++ ret = regmap_update_bits(priv->map,
++ RTL8365MB_SDS_MISC_REG,
++ RTL8365MB_SDS_MISC_SGMII_TXFC_MASK |
++ RTL8365MB_SDS_MISC_SGMII_RXFC_MASK,
++ val);
++ if (ret)
++ dev_err(priv->dev,
++ "failed to force SerDes pause modes on port %d: %pe\n",
++ port, ERR_PTR(ret));
++ }
+
+ return;
+ }
+@@ -2419,6 +2910,14 @@ static int rtl8365mb_setup(struct dsa_sw
+ mb = priv->chip_data;
+ cpu = &mb->cpu;
+
++ mb->pcs.ops = &rtl8365mb_pcs_ops;
++
++ /* The SerDes has no link interrupt wired up, so phylink must poll the
++ * PCS for link changes when it tracks the link through pcs_get_state()
++ * (in-band mode with autonegotiation disabled).
++ */
++ mb->pcs.poll = true;
++
+ ret = rtl8365mb_reset_chip(priv);
+ if (ret) {
+ dev_err(priv->dev, "failed to reset chip: %pe\n",
+@@ -2426,6 +2925,13 @@ static int rtl8365mb_setup(struct dsa_sw
+ goto out_error;
+ }
+
++ ret = rtl8365mb_sds_probe_option(priv);
++ if (ret) {
++ dev_err(priv->dev, "failed to probe SerDes chip option: %pe\n",
++ ERR_PTR(ret));
++ goto out_error;
++ }
++
+ /* Configure switch to vendor-defined initial state */
+ ret = rtl8365mb_switch_init(priv);
+ if (ret) {
+@@ -2658,6 +3164,7 @@ static int rtl8365mb_detect(struct realt
+ }
+
+ static const struct phylink_mac_ops rtl8365mb_phylink_mac_ops = {
++ .mac_select_pcs = rtl8365mb_phylink_mac_select_pcs,
+ .mac_config = rtl8365mb_phylink_mac_config,
+ .mac_link_down = rtl8365mb_phylink_mac_link_down,
+ .mac_link_up = rtl8365mb_phylink_mac_link_up,
--- /dev/null
+From 987137345f3312fd68cc9c11bd46b754bfb0046f Mon Sep 17 00:00:00 2001
+From: Johan Alvarado <contact@c127.dev>
+Date: Sat, 11 Jul 2026 23:31:59 -0500
+Subject: net: dsa: realtek: rtl8365mb: add HSGMII support for RTL8367S
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+In addition to SGMII, the RTL8367S SerDes also supports HSGMII, which
+carries 2.5 Gbps with the same signaling as SGMII at 2.5x clock rate.
+The chip info table already declares HSGMII as a supported interface
+mode for external interface 1.
+
+Extend the SerDes PCS to handle HSGMII, which phylink represents as
+2500base-x:
+
+ - Select the HSGMII SerDes tuning parameters and external interface
+ mode, and mux the SerDes to MAC8 in HSGMII mode, from pcs_config()
+ according to the interface. The parameters are again lifted from the
+ GPL-licensed Realtek rtl8367c vendor driver, and again only cover
+ the tuning variant for a non-zero chip option, so the mode is gated
+ on the option probed at setup.
+
+ - Advertise 2500base-x and MAC_2500FD on ports whose external
+ interface supports HSGMII.
+
+ - Accept SPEED_2500 in the forced link configuration. The MAC speed
+ field has no 2.5 Gbps value: the rate is determined by the HSGMII
+ SerDes configuration, and the vendor driver programs the 1 Gbps
+ value here, so do the same.
+
+ - Raise the port 6 ingress and egress rate limiters to their maximum
+ at setup time, as the vendor switch init does unconditionally for
+ the whole chip family. The chip resets them to 0x1FFFF (~1.048 Gbps
+ in units of 8 Kbps), which caps the aggregate HSGMII throughput at
+ roughly 1 Gbps. The vendor documentation describes the reset
+ default as disabling the limiter, but the cap is real: on an
+ RTL8367S-based Mercusys MR85X running an OpenWrt backport of this
+ series, several clients on 1 Gbps user ports were limited to about
+ 1.02 Gbps combined across the HSGMII CPU port until these limiters
+ were raised, after which throughput reached about 2 Gbps [1]. The
+ related HSGMII scheduler line rate (LINE_RATE_HSG_H) is already set
+ to its maximum by the common init jam table.
+
+Tested on a Mercusys MR80X v2.20, where the RTL8367S is connected to
+the SoC over HSGMII.
+
+Link: https://github.com/openwrt/openwrt/pull/19445#issuecomment-4505613294 [1]
+Suggested-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
+Suggested-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
+Signed-off-by: Johan Alvarado <contact@c127.dev>
+Tested-by: Stanisław Pal <kuncy7@gmail.com>
+Reviewed-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
+Reviewed-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
+Tested-by: Stanislaw Pal <kuncy7@gmail.com>
+Link: https://patch.msgid.link/20260711-rtl8367s-sgmii-v6-2-88f7944ddca7@c127.dev
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/realtek/rtl8365mb_main.c | 134 +++++++++++++++++++++++++++----
+ 1 file changed, 118 insertions(+), 16 deletions(-)
+
+--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
++++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
+@@ -40,8 +40,8 @@
+ * driver has only been tested with a fixed-link, but in principle it should not
+ * matter.
+ *
+- * NOTE: Currently, only the RGMII and SGMII interfaces are implemented in this
+- * driver.
++ * NOTE: Currently, only the RGMII, SGMII and HSGMII interfaces are implemented
++ * in this driver.
+ *
+ * The interrupt line is asserted on link UP/DOWN events. The driver creates a
+ * custom irqchip to handle this interrupt and demultiplex the events by reading
+@@ -251,6 +251,18 @@
+ #define RTL8365MB_BYPASS_LINE_RATE_REG 0x03F7
+ #define RTL8365MB_BYPASS_LINE_RATE_MASK(_port) BIT((_port) - 5)
+
++/* Port 6 ingress and egress rate limiter registers. Each limit is a 19-bit
++ * value in units of 8 Kbps, split across a 16-bit LSB register (CTRL0) and a
++ * 3-bit MSB field (CTRL1). The chip resets them to 0x1FFFF; see
++ * rtl8365mb_sds_raise_rate_limits().
++ */
++#define RTL8365MB_INGRESSBW_PORT6_RATE_CTRL0_REG 0x00CF
++#define RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_REG 0x00D0
++#define RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_MASK 0x0007
++#define RTL8365MB_PORT6_EGRESSBW_CTRL0_REG 0x0398
++#define RTL8365MB_PORT6_EGRESSBW_CTRL1_REG 0x0399
++#define RTL8365MB_PORT6_EGRESSBW_CTRL1_MASK 0x0007
++
+ /* SerDes indirect access registers */
+ #define RTL8365MB_SDS_INDACS_CMD_REG 0x6600
+ #define RTL8365MB_SDS_INDACS_CMD_BUSY_MASK 0x0100
+@@ -637,6 +649,18 @@ static const struct rtl8365mb_jam_tbl_en
+ { 0x0424, 0xD810 }, { 0x002E, 0x83F2 },
+ };
+
++/* HSGMII SerDes tuning parameters, lifted from the vendor driver sources. As
++ * with the SGMII table, the vendor driver keeps several variants and selects
++ * one based on the chip option register; these are the values for a non-zero
++ * option, which is what RTL8367S parts seen so far report. See
++ * rtl8365mb_sds_probe_option().
++ */
++static const struct rtl8365mb_jam_tbl_entry rtl8365mb_sds_jam_hsgmii[] = {
++ { 0x0500, 0x82F0 }, { 0x0501, 0xF195 }, { 0x0502, 0x31A2 },
++ { 0x0503, 0x7960 }, { 0x0504, 0x9728 }, { 0x0423, 0x9D85 },
++ { 0x0424, 0xD810 }, { 0x0001, 0x0F80 }, { 0x002E, 0x83F2 },
++};
++
+ enum rtl8365mb_phy_interface_mode {
+ RTL8365MB_PHY_INTERFACE_MODE_INVAL = 0,
+ RTL8365MB_PHY_INTERFACE_MODE_INTERNAL = BIT(0),
+@@ -1242,20 +1266,70 @@ static int rtl8365mb_sds_probe_option(st
+ return 0;
+ }
+
++/* The vendor driver raises the port 6 ingress and egress rate limiters to
++ * their maximum in its switch init, unconditionally for the whole chip
++ * family. The chip reset in rtl8365mb_setup() puts them back to their reset
++ * default of 0x1FFFF, a ~1.048 Gbps limit which caps the aggregate
++ * throughput of an HSGMII CPU port at roughly 1 Gbps. The vendor
++ * documentation describes the reset default as disabling the limiter, but
++ * the cap has been observed on hardware. Raise them likewise, to 0x7FFFF
++ * (~4.19 Gbps, above the HSGMII line rate). The related HSGMII scheduler
++ * line rate register (LINE_RATE_HSG_H, 0x03FA) is already set to its
++ * maximum by the common init jam table.
++ */
++static int rtl8365mb_sds_raise_rate_limits(struct realtek_priv *priv)
++{
++ int ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_INGRESSBW_PORT6_RATE_CTRL0_REG,
++ 0xFFFF);
++ if (ret)
++ return ret;
++
++ ret = regmap_update_bits(priv->map,
++ RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_REG,
++ RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_MASK,
++ RTL8365MB_INGRESSBW_PORT6_RATE_CTRL1_MASK);
++ if (ret)
++ return ret;
++
++ ret = regmap_write(priv->map, RTL8365MB_PORT6_EGRESSBW_CTRL0_REG,
++ 0xFFFF);
++ if (ret)
++ return ret;
++
++ return regmap_update_bits(priv->map, RTL8365MB_PORT6_EGRESSBW_CTRL1_REG,
++ RTL8365MB_PORT6_EGRESSBW_CTRL1_MASK,
++ RTL8365MB_PORT6_EGRESSBW_CTRL1_MASK);
++}
++
+ static int rtl8365mb_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
+ phy_interface_t interface,
+ const unsigned long *advertising,
+ bool permit_pause_to_mac)
+ {
++ const struct rtl8365mb_jam_tbl_entry *sds_jam;
+ const int id = RTL8365MB_SDS_EXT_INTERFACE_ID;
+ struct rtl8365mb *mb = pcs_to_rtl8365mb(pcs);
+ struct realtek_priv *priv;
++ size_t sds_jam_size;
++ u32 mode;
+ u16 val;
+ int ret;
+ int i;
+
+ priv = mb->priv;
+
++ if (interface == PHY_INTERFACE_MODE_2500BASEX) {
++ sds_jam = rtl8365mb_sds_jam_hsgmii;
++ sds_jam_size = ARRAY_SIZE(rtl8365mb_sds_jam_hsgmii);
++ mode = RTL8365MB_EXT_PORT_MODE_HSGMII;
++ } else {
++ sds_jam = rtl8365mb_sds_jam_sgmii;
++ sds_jam_size = ARRAY_SIZE(rtl8365mb_sds_jam_sgmii);
++ mode = RTL8365MB_EXT_PORT_MODE_SGMII;
++ }
++
+ /* Hold the embedded DW8051 microcontroller in reset and keep it
+ * disabled. The vendor driver loads firmware into it to manage the
+ * SerDes link, but the firmware only duplicates work that phylink
+@@ -1283,24 +1357,24 @@ static int rtl8365mb_pcs_config(struct p
+ return ret;
+
+ /* Tune the SerDes with vendor-prescribed parameters */
+- for (i = 0; i < ARRAY_SIZE(rtl8365mb_sds_jam_sgmii); i++) {
+- ret = rtl8365mb_sds_write(priv,
+- rtl8365mb_sds_jam_sgmii[i].reg,
+- rtl8365mb_sds_jam_sgmii[i].val);
++ for (i = 0; i < sds_jam_size; i++) {
++ ret = rtl8365mb_sds_write(priv, sds_jam[i].reg,
++ sds_jam[i].val);
+ if (ret)
+ return ret;
+ }
+
+- /* Mux the SerDes to MAC8 in SGMII mode */
++ /* Mux the SerDes to MAC8 in the requested mode */
+ ret = regmap_update_bits(priv->map, RTL8365MB_SDS_MISC_REG,
+ RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK |
+ RTL8365MB_SDS_MISC_MAC8_SEL_HSGMII_MASK,
+- RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK);
++ mode == RTL8365MB_EXT_PORT_MODE_SGMII ?
++ RTL8365MB_SDS_MISC_MAC8_SEL_SGMII_MASK :
++ RTL8365MB_SDS_MISC_MAC8_SEL_HSGMII_MASK);
+ if (ret)
+ return ret;
+
+- val = RTL8365MB_EXT_PORT_MODE_SGMII
+- << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(id);
++ val = mode << RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_OFFSET(id);
+ ret = regmap_update_bits(priv->map,
+ RTL8365MB_DIGITAL_INTERFACE_SELECT_REG(id),
+ RTL8365MB_DIGITAL_INTERFACE_SELECT_MODE_MASK(id),
+@@ -1346,7 +1420,8 @@ static int rtl8365mb_pcs_config(struct p
+
+ static bool rtl8365mb_interface_is_serdes(phy_interface_t interface)
+ {
+- return interface == PHY_INTERFACE_MODE_SGMII;
++ return interface == PHY_INTERFACE_MODE_SGMII ||
++ interface == PHY_INTERFACE_MODE_2500BASEX;
+ }
+
+ static unsigned int rtl8365mb_pcs_inband_caps(struct phylink_pcs *pcs,
+@@ -1401,7 +1476,9 @@ static void rtl8365mb_pcs_get_state(stru
+
+ switch (FIELD_GET(RTL8365MB_SDS_MISC_SGMII_SPD_MASK, val)) {
+ case RTL8365MB_PORT_SPEED_1000M:
+- state->speed = SPEED_1000;
++ state->speed =
++ state->interface == PHY_INTERFACE_MODE_2500BASEX ?
++ SPEED_2500 : SPEED_1000;
+ break;
+ case RTL8365MB_PORT_SPEED_100M:
+ state->speed = SPEED_100;
+@@ -1426,7 +1503,11 @@ static void rtl8365mb_pcs_link_up(struct
+ u32 r_speed;
+ int ret;
+
+- if (speed == SPEED_1000) {
++ /* The speed field has no value for 2.5 Gbps: the rate is determined by
++ * the HSGMII SerDes configuration, and the vendor driver programs the
++ * 1 Gbps value here.
++ */
++ if (speed == SPEED_2500 || speed == SPEED_1000) {
+ r_speed = RTL8365MB_PORT_SPEED_1000M;
+ } else if (speed == SPEED_100) {
+ r_speed = RTL8365MB_PORT_SPEED_100M;
+@@ -1486,7 +1567,11 @@ static int rtl8365mb_ext_config_forcemod
+ r_rx_pause = rx_pause ? 1 : 0;
+ r_tx_pause = tx_pause ? 1 : 0;
+
+- if (speed == SPEED_1000) {
++ /* The speed field has no value for 2.5 Gbps: the rate is
++ * determined by the HSGMII SerDes configuration, and the
++ * vendor driver programs the 1 Gbps value here.
++ */
++ if (speed == SPEED_2500 || speed == SPEED_1000) {
+ r_speed = RTL8365MB_PORT_SPEED_1000M;
+ } else if (speed == SPEED_100) {
+ r_speed = RTL8365MB_PORT_SPEED_100M;
+@@ -1569,6 +1654,13 @@ static void rtl8365mb_phylink_get_caps(s
+ mb->sds_supported)
+ __set_bit(PHY_INTERFACE_MODE_SGMII,
+ config->supported_interfaces);
++
++ if (extint->supported_interfaces & RTL8365MB_PHY_INTERFACE_MODE_HSGMII &&
++ mb->sds_supported) {
++ __set_bit(PHY_INTERFACE_MODE_2500BASEX,
++ config->supported_interfaces);
++ config->mac_capabilities |= MAC_2500FD;
++ }
+ }
+
+ static struct phylink_pcs *
+@@ -1610,8 +1702,8 @@ static void rtl8365mb_phylink_mac_config
+ return;
+ }
+
+- /* SGMII is handled by the SerDes PCS, configured through the
+- * phylink_pcs ops, so there is nothing to do here for it.
++ /* SGMII and 2500base-x are handled by the SerDes PCS, configured
++ * through the phylink_pcs ops, so nothing to do here for them.
+ */
+ if (rtl8365mb_interface_is_serdes(state->interface))
+ return;
+@@ -2940,6 +3032,16 @@ static int rtl8365mb_setup(struct dsa_sw
+ goto out_error;
+ }
+
++ if (mb->sds_supported) {
++ ret = rtl8365mb_sds_raise_rate_limits(priv);
++ if (ret) {
++ dev_err(priv->dev,
++ "failed to raise port rate limits: %pe\n",
++ ERR_PTR(ret));
++ goto out_error;
++ }
++ }
++
+ /* Set up cascading IRQs */
+ ret = rtl8365mb_irq_setup(priv);
+ if (ret == -EPROBE_DEFER)