]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
generic: backport rtl8365mb SGMII/HSGMII support for RTL8367S 24373/head
authorMieczyslaw Nalewaj <namiltd@yahoo.com>
Wed, 22 Jul 2026 18:20:04 +0000 (20:20 +0200)
committerJonas Jelonek <jelonek.jonas@gmail.com>
Fri, 24 Jul 2026 06:49:40 +0000 (08:49 +0200)
Backport two upstream patches adding SGMII and HSGMII support to the
net/dsa/realtek rtl8365mb driver for the RTL8367S switch:

 - 943-01-v7.3-net-dsa-realtek-rtl8365mb-add-SGMII-support.patch
 - 943-02-v7.3-net-dsa-realtek-rtl8365mb-add-HSGMII-support.patch

The RTL8367S SerDes can be muxed to external interface 1 (typically
the CPU port). SGMII/HSGMII were already listed as supported modes
in the chip info table, but only RGMII was implemented. This adds a
phylink PCS for the SerDes, with the configuration derived from the
GPL-licensed Realtek rtl8367c vendor driver.

Tested on a Mercusys MR80X v2.20 (RTL8367S over HSGMII).

Signed-off-by: Mieczyslaw Nalewaj <namiltd@yahoo.com>
Link: https://github.com/openwrt/openwrt/pull/24373
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
target/linux/generic/backport-6.12/943-01-v7.3-net-dsa-realtek-rtl8365mb-add-SGMII-support.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/943-02-v7.3-net-dsa-realtek-rtl8365mb-add-HSGMII-support.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/943-01-v7.3-net-dsa-realtek-rtl8365mb-add-SGMII-support.patch [new file with mode: 0644]
target/linux/generic/backport-6.18/943-02-v7.3-net-dsa-realtek-rtl8365mb-add-HSGMII-support.patch [new file with mode: 0644]

diff --git a/target/linux/generic/backport-6.12/943-01-v7.3-net-dsa-realtek-rtl8365mb-add-SGMII-support.patch b/target/linux/generic/backport-6.12/943-01-v7.3-net-dsa-realtek-rtl8365mb-add-SGMII-support.patch
new file mode 100644 (file)
index 0000000..586f145
--- /dev/null
@@ -0,0 +1,728 @@
+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,
diff --git a/target/linux/generic/backport-6.12/943-02-v7.3-net-dsa-realtek-rtl8365mb-add-HSGMII-support.patch b/target/linux/generic/backport-6.12/943-02-v7.3-net-dsa-realtek-rtl8365mb-add-HSGMII-support.patch
new file mode 100644 (file)
index 0000000..5aeb8c2
--- /dev/null
@@ -0,0 +1,305 @@
+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)
diff --git a/target/linux/generic/backport-6.18/943-01-v7.3-net-dsa-realtek-rtl8365mb-add-SGMII-support.patch b/target/linux/generic/backport-6.18/943-01-v7.3-net-dsa-realtek-rtl8365mb-add-SGMII-support.patch
new file mode 100644 (file)
index 0000000..5b60932
--- /dev/null
@@ -0,0 +1,729 @@
+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,
diff --git a/target/linux/generic/backport-6.18/943-02-v7.3-net-dsa-realtek-rtl8365mb-add-HSGMII-support.patch b/target/linux/generic/backport-6.18/943-02-v7.3-net-dsa-realtek-rtl8365mb-add-HSGMII-support.patch
new file mode 100644 (file)
index 0000000..557feef
--- /dev/null
@@ -0,0 +1,305 @@
+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)