$(eval $(call KernelPackage,dsa-mv88e6xxx))
+define KernelPackage/dsa-mxl862xx
+ SUBMENU:=Network Devices
+ TITLE:=MaxLinear MXL862 switch support
+ KCONFIG:= \
+ CONFIG_NET_DSA_TAG_MXL_862XX \
+ CONFIG_NET_DSA_TAG_MXL_862XX_8021Q \
+ CONFIG_NET_DSA_MXL862
+ DEPENDS:=+kmod-dsa +kmod-lib-crc16 +kmod-phy-maxlinear
+ FILES:= \
+ $(LINUX_DIR)/drivers/net/dsa/mxl862xx/mxl862xx_dsa.ko \
+ $(LINUX_DIR)/net/dsa/tag_mxl862xx.ko \
+ $(LINUX_DIR)/net/dsa/tag_mxl862xx_8021q.ko
+ AUTOLOAD:=$(call AutoProbe,mxl862xx_dsa)
+endef
+
+$(eval $(call KernelPackage,dsa-mxl862xx))
+
define KernelPackage/dsa-qca8k
SUBMENU:=$(NETWORK_DEVICES_MENU)
TITLE:=Qualcomm Atheros QCA8xxx switch family DSA support
--- /dev/null
+From ae1c658b33d4bec20c037aebba583a68375d4773 Mon Sep 17 00:00:00 2001
+From: Christian Marangi <ansuelsmth@gmail.com>
+Date: Thu, 11 Sep 2025 15:08:31 +0200
+Subject: [PATCH] net: phy: introduce phy_id_compare_model() PHY ID helper
+
+Similar to phy_id_compare_vendor(), introduce the equivalent
+phy_id_compare_model() helper for the generic PHY ID Model mask.
+
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Reviewed-by: Florian Fainelli <florian.fainelli@broadcom.com>
+Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
+Link: https://patch.msgid.link/20250911130840.23569-1-ansuelsmth@gmail.com
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ include/linux/phy.h | 13 +++++++++++++
+ 1 file changed, 13 insertions(+)
+
+--- a/include/linux/phy.h
++++ b/include/linux/phy.h
+@@ -1284,6 +1284,19 @@ static inline bool phy_id_compare(u32 id
+ }
+
+ /**
++ * phy_id_compare_model - compare @id with @model mask
++ * @id: PHY ID
++ * @model_mask: PHY Model mask
++ *
++ * Return: true if the bits from @id match @model using the
++ * generic PHY Model mask.
++ */
++static inline bool phy_id_compare_model(u32 id, u32 model_mask)
++{
++ return phy_id_compare(id, model_mask, PHY_ID_MATCH_MODEL_MASK);
++}
++
++/**
+ * phydev_id_compare - compare @id with the PHY's Clause 22 ID
+ * @phydev: the PHY device
+ * @id: the PHY ID to be matched
--- /dev/null
+From de1e5c9333f426348571f7a3b034f99490d3f926 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sat, 22 Nov 2025 13:33:47 +0000
+Subject: [PATCH] net: phy: mxl-gpy: add support for MxL86252 and MxL86282
+
+Add PHY driver support for Maxlinear MxL86252 and MxL86282 switches.
+The PHYs built-into those switches are just like any other GPY 2.5G PHYs
+with the exception of the temperature sensor data being encoded in a
+different way.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Link: https://patch.msgid.link/a6cd7fe461b011cec2b59dffaf34e9c8b0819059.1763818120.git.daniel@makrotopia.org
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/phy/mxl-gpy.c | 91 ++++++++++++++++++++++++++++++++++++++-
+ 1 file changed, 89 insertions(+), 2 deletions(-)
+
+--- a/drivers/net/phy/mxl-gpy.c
++++ b/drivers/net/phy/mxl-gpy.c
+@@ -31,6 +31,8 @@
+ #define PHY_ID_GPY241BM 0x67C9DE80
+ #define PHY_ID_GPY245B 0x67C9DEC0
+ #define PHY_ID_MXL86211C 0xC1335400
++#define PHY_ID_MXL86252 0xC1335520
++#define PHY_ID_MXL86282 0xC1335500
+
+ #define PHY_CTL1 0x13
+ #define PHY_CTL1_MDICD BIT(3)
+@@ -200,6 +202,29 @@ static int gpy_hwmon_read(struct device
+ return 0;
+ }
+
++static int mxl862x2_hwmon_read(struct device *dev,
++ enum hwmon_sensor_types type,
++ u32 attr, int channel, long *value)
++{
++ struct phy_device *phydev = dev_get_drvdata(dev);
++ long tmp;
++ int ret;
++
++ ret = phy_read_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_TEMP_STA);
++ if (ret < 0)
++ return ret;
++ if (!ret)
++ return -ENODATA;
++
++ tmp = (s16)ret;
++ tmp *= 78125;
++ tmp /= 10000;
++
++ *value = tmp;
++
++ return 0;
++}
++
+ static umode_t gpy_hwmon_is_visible(const void *data,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel)
+@@ -217,14 +242,25 @@ static const struct hwmon_ops gpy_hwmon_
+ .read = gpy_hwmon_read,
+ };
+
++static const struct hwmon_ops mxl862x2_hwmon_hwmon_ops = {
++ .is_visible = gpy_hwmon_is_visible,
++ .read = mxl862x2_hwmon_read,
++};
++
+ static const struct hwmon_chip_info gpy_hwmon_chip_info = {
+ .ops = &gpy_hwmon_hwmon_ops,
+ .info = gpy_hwmon_info,
+ };
+
++static const struct hwmon_chip_info mxl862x2_hwmon_chip_info = {
++ .ops = &mxl862x2_hwmon_hwmon_ops,
++ .info = gpy_hwmon_info,
++};
++
+ static int gpy_hwmon_register(struct phy_device *phydev)
+ {
+ struct device *dev = &phydev->mdio.dev;
++ const struct hwmon_chip_info *info;
+ struct device *hwmon_dev;
+ char *hwmon_name;
+
+@@ -232,10 +268,15 @@ static int gpy_hwmon_register(struct phy
+ if (IS_ERR(hwmon_name))
+ return PTR_ERR(hwmon_name);
+
++ if (phy_id_compare_model(phydev->phy_id, PHY_ID_MXL86252) ||
++ phy_id_compare_model(phydev->phy_id, PHY_ID_MXL86282))
++ info = &mxl862x2_hwmon_chip_info;
++ else
++ info = &gpy_hwmon_chip_info;
++
+ hwmon_dev = devm_hwmon_device_register_with_info(dev, hwmon_name,
+ phydev,
+- &gpy_hwmon_chip_info,
+- NULL);
++ info, NULL);
+
+ return PTR_ERR_OR_ZERO(hwmon_dev);
+ }
+@@ -1331,6 +1372,50 @@ static struct phy_driver gpy_drivers[] =
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
+ },
++ {
++ PHY_ID_MATCH_MODEL(PHY_ID_MXL86252),
++ .name = "MaxLinear Ethernet MxL86252",
++ .get_features = genphy_c45_pma_read_abilities,
++ .config_init = gpy_config_init,
++ .probe = gpy_probe,
++ .suspend = genphy_suspend,
++ .resume = genphy_resume,
++ .config_aneg = gpy_config_aneg,
++ .aneg_done = genphy_c45_aneg_done,
++ .read_status = gpy_read_status,
++ .config_intr = gpy_config_intr,
++ .handle_interrupt = gpy_handle_interrupt,
++ .set_wol = gpy_set_wol,
++ .get_wol = gpy_get_wol,
++ .set_loopback = gpy_loopback,
++ .led_brightness_set = gpy_led_brightness_set,
++ .led_hw_is_supported = gpy_led_hw_is_supported,
++ .led_hw_control_get = gpy_led_hw_control_get,
++ .led_hw_control_set = gpy_led_hw_control_set,
++ .led_polarity_set = gpy_led_polarity_set,
++ },
++ {
++ PHY_ID_MATCH_MODEL(PHY_ID_MXL86282),
++ .name = "MaxLinear Ethernet MxL86282",
++ .get_features = genphy_c45_pma_read_abilities,
++ .config_init = gpy_config_init,
++ .probe = gpy_probe,
++ .suspend = genphy_suspend,
++ .resume = genphy_resume,
++ .config_aneg = gpy_config_aneg,
++ .aneg_done = genphy_c45_aneg_done,
++ .read_status = gpy_read_status,
++ .config_intr = gpy_config_intr,
++ .handle_interrupt = gpy_handle_interrupt,
++ .set_wol = gpy_set_wol,
++ .get_wol = gpy_get_wol,
++ .set_loopback = gpy_loopback,
++ .led_brightness_set = gpy_led_brightness_set,
++ .led_hw_is_supported = gpy_led_hw_is_supported,
++ .led_hw_control_get = gpy_led_hw_control_get,
++ .led_hw_control_set = gpy_led_hw_control_set,
++ .led_polarity_set = gpy_led_polarity_set,
++ },
+ };
+ module_phy_driver(gpy_drivers);
+
+@@ -1348,6 +1433,8 @@ static const struct mdio_device_id __may
+ {PHY_ID_MATCH_MODEL(PHY_ID_GPY241BM)},
+ {PHY_ID_MATCH_MODEL(PHY_ID_GPY245B)},
+ {PHY_ID_MATCH_MODEL(PHY_ID_MXL86211C)},
++ {PHY_ID_MATCH_MODEL(PHY_ID_MXL86252)},
++ {PHY_ID_MATCH_MODEL(PHY_ID_MXL86282)},
+ { }
+ };
+ MODULE_DEVICE_TABLE(mdio, gpy_tbl);
--- /dev/null
+From 1ecc2ebd1298d5c0eaa238e71b7d2109d7d77538 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sat, 7 Feb 2026 03:07:11 +0000
+Subject: [PATCH 01/35] net: dsa: add tag format for MxL862xx switches
+
+Add proprietary special tag format for the MaxLinear MXL862xx family of
+switches. While using the same Ethertype as MaxLinear's GSW1xx switches,
+the actual tag format differs significantly, hence we need a dedicated
+tag driver for that.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Link: https://patch.msgid.link/c64e6ddb6c93a4fac39f9ab9b2d8bf551a2b118d.1770433307.git.daniel@makrotopia.org
+Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+---
+ include/net/dsa.h | 2 +
+ net/dsa/Kconfig | 7 +++
+ net/dsa/Makefile | 1 +
+ net/dsa/tag_mxl862xx.c | 110 +++++++++++++++++++++++++++++++++++++++++
+ 4 files changed, 120 insertions(+)
+ create mode 100644 net/dsa/tag_mxl862xx.c
+
+--- a/include/net/dsa.h
++++ b/include/net/dsa.h
+@@ -55,6 +55,7 @@ struct tc_action;
+ #define DSA_TAG_PROTO_LAN937X_VALUE 27
+ #define DSA_TAG_PROTO_VSC73XX_8021Q_VALUE 28
+ #define DSA_TAG_PROTO_BRCM_LEGACY_FCS_VALUE 29
++#define DSA_TAG_PROTO_MXL862_VALUE 30
+
+ enum dsa_tag_protocol {
+ DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
+@@ -87,6 +88,7 @@ enum dsa_tag_protocol {
+ DSA_TAG_PROTO_RZN1_A5PSW = DSA_TAG_PROTO_RZN1_A5PSW_VALUE,
+ DSA_TAG_PROTO_LAN937X = DSA_TAG_PROTO_LAN937X_VALUE,
+ DSA_TAG_PROTO_VSC73XX_8021Q = DSA_TAG_PROTO_VSC73XX_8021Q_VALUE,
++ DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
+ };
+
+ struct dsa_switch;
+--- a/net/dsa/Kconfig
++++ b/net/dsa/Kconfig
+@@ -104,6 +104,13 @@ config NET_DSA_TAG_MTK
+ Say Y or M if you want to enable support for tagging frames for
+ Mediatek switches.
+
++config NET_DSA_TAG_MXL_862XX
++ tristate "Tag driver for MaxLinear MxL862xx switches"
++ help
++ Say Y or M if you want to enable support for tagging frames for the
++ MaxLinear MxL86252 and MxL86282 switches using their native 8-byte
++ tagging protocol.
++
+ config NET_DSA_TAG_KSZ
+ tristate "Tag driver for Microchip 8795/937x/9477/9893 families of switches"
+ help
+--- a/net/dsa/Makefile
++++ b/net/dsa/Makefile
+@@ -28,6 +28,7 @@ obj-$(CONFIG_NET_DSA_TAG_HELLCREEK) += t
+ obj-$(CONFIG_NET_DSA_TAG_KSZ) += tag_ksz.o
+ obj-$(CONFIG_NET_DSA_TAG_LAN9303) += tag_lan9303.o
+ obj-$(CONFIG_NET_DSA_TAG_MTK) += tag_mtk.o
++obj-$(CONFIG_NET_DSA_TAG_MXL_862XX) += tag_mxl862xx.o
+ obj-$(CONFIG_NET_DSA_TAG_NONE) += tag_none.o
+ obj-$(CONFIG_NET_DSA_TAG_OCELOT) += tag_ocelot.o
+ obj-$(CONFIG_NET_DSA_TAG_OCELOT_8021Q) += tag_ocelot_8021q.o
+--- /dev/null
++++ b/net/dsa/tag_mxl862xx.c
+@@ -0,0 +1,110 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * DSA Special Tag for MaxLinear 862xx switch chips
++ *
++ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
++ * Copyright (C) 2024 MaxLinear Inc.
++ */
++
++#include <linux/bitops.h>
++#include <linux/etherdevice.h>
++#include <linux/skbuff.h>
++#include <net/dsa.h>
++#include "tag.h"
++
++#define MXL862_NAME "mxl862xx"
++
++#define MXL862_HEADER_LEN 8
++
++/* Word 0 -> EtherType */
++
++/* Word 2 */
++#define MXL862_SUBIF_ID GENMASK(4, 0)
++
++/* Word 3 */
++#define MXL862_IGP_EGP GENMASK(3, 0)
++
++static struct sk_buff *mxl862_tag_xmit(struct sk_buff *skb,
++ struct net_device *dev)
++{
++ struct dsa_port *dp = dsa_user_to_port(dev);
++ struct dsa_port *cpu_dp = dp->cpu_dp;
++ unsigned int cpu_port, sub_interface;
++ __be16 *mxl862_tag;
++
++ cpu_port = cpu_dp->index;
++
++ /* target port sub-interface ID relative to the CPU port */
++ sub_interface = dp->index + 16 - cpu_port;
++
++ /* provide additional space 'MXL862_HEADER_LEN' bytes */
++ skb_push(skb, MXL862_HEADER_LEN);
++
++ /* shift MAC address to the beginning of the enlarged buffer,
++ * releasing the space required for DSA tag (between MAC address and
++ * Ethertype)
++ */
++ dsa_alloc_etype_header(skb, MXL862_HEADER_LEN);
++
++ /* special tag ingress (from the perspective of the switch) */
++ mxl862_tag = dsa_etype_header_pos_tx(skb);
++ mxl862_tag[0] = htons(ETH_P_MXLGSW);
++ mxl862_tag[1] = 0;
++ mxl862_tag[2] = htons(FIELD_PREP(MXL862_SUBIF_ID, sub_interface));
++ mxl862_tag[3] = htons(FIELD_PREP(MXL862_IGP_EGP, cpu_port));
++
++ return skb;
++}
++
++static struct sk_buff *mxl862_tag_rcv(struct sk_buff *skb,
++ struct net_device *dev)
++{
++ __be16 *mxl862_tag;
++ int port;
++
++ if (unlikely(!pskb_may_pull(skb, MXL862_HEADER_LEN))) {
++ dev_warn_ratelimited(&dev->dev, "Cannot pull SKB, packet dropped\n");
++ return NULL;
++ }
++
++ mxl862_tag = dsa_etype_header_pos_rx(skb);
++
++ if (unlikely(mxl862_tag[0] != htons(ETH_P_MXLGSW))) {
++ dev_warn_ratelimited(&dev->dev,
++ "Invalid special tag marker, packet dropped, tag: %8ph\n",
++ mxl862_tag);
++ return NULL;
++ }
++
++ /* Get source port information */
++ port = FIELD_GET(MXL862_IGP_EGP, ntohs(mxl862_tag[3]));
++ skb->dev = dsa_conduit_find_user(dev, 0, port);
++ if (unlikely(!skb->dev)) {
++ dev_warn_ratelimited(&dev->dev,
++ "Invalid source port, packet dropped, tag: %8ph\n",
++ mxl862_tag);
++ return NULL;
++ }
++
++ /* remove the MxL862xx special tag between the MAC addresses and the
++ * current ethertype field.
++ */
++ skb_pull_rcsum(skb, MXL862_HEADER_LEN);
++ dsa_strip_etype_header(skb, MXL862_HEADER_LEN);
++
++ return skb;
++}
++
++static const struct dsa_device_ops mxl862_netdev_ops = {
++ .name = MXL862_NAME,
++ .proto = DSA_TAG_PROTO_MXL862,
++ .xmit = mxl862_tag_xmit,
++ .rcv = mxl862_tag_rcv,
++ .needed_headroom = MXL862_HEADER_LEN,
++};
++
++MODULE_ALIAS_DSA_TAG_DRIVER(DSA_TAG_PROTO_MXL862, MXL862_NAME);
++MODULE_DESCRIPTION("DSA tag driver for MaxLinear MxL862xx switches");
++MODULE_LICENSE("GPL");
++
++module_dsa_tag_driver(mxl862_netdev_ops);
+--- a/include/uapi/linux/if_ether.h
++++ b/include/uapi/linux/if_ether.h
+@@ -92,6 +92,9 @@
+ #define ETH_P_ETHERCAT 0x88A4 /* EtherCAT */
+ #define ETH_P_8021AD 0x88A8 /* 802.1ad Service VLAN */
+ #define ETH_P_802_EX1 0x88B5 /* 802.1 Local Experimental 1. */
++#define ETH_P_MXLGSW 0x88C3 /* Infineon Technologies Corporate Research ST
++ * Used by MaxLinear GSW DSA
++ */
+ #define ETH_P_PREAUTH 0x88C7 /* 802.11 Preauthentication */
+ #define ETH_P_TIPC 0x88CA /* TIPC */
+ #define ETH_P_LLDP 0x88CC /* Link Layer Discovery Protocol */
--- /dev/null
+From 1111454d5a637e039a46b867088b524c73159da4 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sat, 7 Feb 2026 03:07:18 +0000
+Subject: [PATCH 02/35] net: mdio: add unlocked mdiodev C45 bus accessors
+
+Add helper inline functions __mdiodev_c45_read() and
+__mdiodev_c45_write(), which are the C45 equivalents of the existing
+__mdiodev_read() and __mdiodev_write() added by commit e6a45700e7e1
+("net: mdio: add unlocked mdiobus and mdiodev bus accessors")
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Reviewed-by: Russell King (Oracle) <rmk+kernel@armlinux.org.uk>
+Link: https://patch.msgid.link/8d1d55949a75a871d2a3b90e421de4bd58d77685.1770433307.git.daniel@makrotopia.org
+Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+---
+ include/linux/mdio.h | 13 +++++++++++++
+ 1 file changed, 13 insertions(+)
+
+--- a/include/linux/mdio.h
++++ b/include/linux/mdio.h
+@@ -668,6 +668,19 @@ static inline int mdiodev_modify_changed
+ mask, set);
+ }
+
++static inline int __mdiodev_c45_read(struct mdio_device *mdiodev, int devad,
++ u16 regnum)
++{
++ return __mdiobus_c45_read(mdiodev->bus, mdiodev->addr, devad, regnum);
++}
++
++static inline int __mdiodev_c45_write(struct mdio_device *mdiodev, u32 devad,
++ u16 regnum, u16 val)
++{
++ return __mdiobus_c45_write(mdiodev->bus, mdiodev->addr, devad, regnum,
++ val);
++}
++
+ static inline int mdiodev_c45_modify(struct mdio_device *mdiodev, int devad,
+ u32 regnum, u16 mask, u16 set)
+ {
--- /dev/null
+From c764b51397f5f5919b07fdfbb9b70082059e1c16 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sat, 7 Feb 2026 03:07:27 +0000
+Subject: [PATCH 03/35] net: dsa: add basic initial driver for MxL862xx
+ switches
+
+Add very basic DSA driver for MaxLinear's MxL862xx switches.
+
+In contrast to previous MaxLinear switches the MxL862xx has a built-in
+processor that runs a sophisticated firmware based on Zephyr RTOS.
+Interaction between the host and the switch hence is organized using a
+software API of that firmware rather than accessing hardware registers
+directly.
+
+Add descriptions of the most basic firmware API calls to access the
+built-in MDIO bus hosting the 2.5GE PHYs, basic port control as well as
+setting up the CPU port.
+
+Implement a very basic DSA driver using that API which is sufficient to
+get packets flowing between the user ports and the CPU port.
+
+The firmware offers all features one would expect from a modern switch
+hardware, they are going to be added one by one in follow-up patch
+series.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Link: https://patch.msgid.link/ccde07e8cf33d8ae243000013b57cfaa2695e0a9.1770433307.git.daniel@makrotopia.org
+Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
+Signed-off-by: Paolo Abeni <pabeni@redhat.com>
+---
+ drivers/net/dsa/Kconfig | 2 +
+ drivers/net/dsa/Makefile | 1 +
+ drivers/net/dsa/mxl862xx/Kconfig | 12 +
+ drivers/net/dsa/mxl862xx/Makefile | 3 +
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 675 +++++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 49 ++
+ drivers/net/dsa/mxl862xx/mxl862xx-host.c | 245 ++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-host.h | 12 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 476 ++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 16 +
+ 10 files changed, 1491 insertions(+)
+ create mode 100644 drivers/net/dsa/mxl862xx/Kconfig
+ create mode 100644 drivers/net/dsa/mxl862xx/Makefile
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-api.h
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-host.c
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-host.h
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx.c
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx.h
+
+--- a/drivers/net/dsa/Kconfig
++++ b/drivers/net/dsa/Kconfig
+@@ -79,6 +79,8 @@ source "drivers/net/dsa/microchip/Kconfi
+
+ source "drivers/net/dsa/mv88e6xxx/Kconfig"
+
++source "drivers/net/dsa/mxl862xx/Kconfig"
++
+ source "drivers/net/dsa/ocelot/Kconfig"
+
+ source "drivers/net/dsa/qca/Kconfig"
+--- a/drivers/net/dsa/Makefile
++++ b/drivers/net/dsa/Makefile
+@@ -21,6 +21,7 @@ obj-y += b53/
+ obj-y += hirschmann/
+ obj-y += microchip/
+ obj-y += mv88e6xxx/
++obj-y += mxl862xx/
+ obj-y += ocelot/
+ obj-y += qca/
+ obj-y += realtek/
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/Kconfig
+@@ -0,0 +1,12 @@
++# SPDX-License-Identifier: GPL-2.0-only
++config NET_DSA_MXL862
++ tristate "MaxLinear MxL862xx"
++ depends on NET_DSA
++ select MAXLINEAR_GPHY
++ select NET_DSA_TAG_MXL_862XX
++ help
++ This enables support for the MaxLinear MxL862xx switch family.
++ These switches have two 10GE SerDes interfaces, one typically
++ used as CPU port.
++ - MxL86282 has eight 2.5 Gigabit PHYs
++ - MxL86252 has five 2.5 Gigabit PHYs
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/Makefile
+@@ -0,0 +1,3 @@
++# SPDX-License-Identifier: GPL-2.0
++obj-$(CONFIG_NET_DSA_MXL862) += mxl862xx_dsa.o
++mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -0,0 +1,675 @@
++/* SPDX-License-Identifier: GPL-2.0-or-later */
++
++#ifndef __MXL862XX_API_H
++#define __MXL862XX_API_H
++
++#include <linux/if_ether.h>
++
++/**
++ * struct mdio_relay_data - relayed access to the switch internal MDIO bus
++ * @data: data to be read or written
++ * @phy: PHY index
++ * @mmd: MMD device
++ * @reg: register index
++ */
++struct mdio_relay_data {
++ __le16 data;
++ u8 phy;
++ u8 mmd;
++ __le16 reg;
++} __packed;
++
++/**
++ * struct mxl862xx_register_mod - Register access parameter to directly
++ * modify internal registers
++ * @addr: Register address offset for modification
++ * @data: Value to write to the register address
++ * @mask: Mask of bits to be modified (1 to modify, 0 to ignore)
++ *
++ * Used for direct register modification operations.
++ */
++struct mxl862xx_register_mod {
++ __le16 addr;
++ __le16 data;
++ __le16 mask;
++} __packed;
++
++/**
++ * enum mxl862xx_mac_clear_type - MAC table clear type
++ * @MXL862XX_MAC_CLEAR_PHY_PORT: clear dynamic entries based on port_id
++ * @MXL862XX_MAC_CLEAR_DYNAMIC: clear all dynamic entries
++ */
++enum mxl862xx_mac_clear_type {
++ MXL862XX_MAC_CLEAR_PHY_PORT = 0,
++ MXL862XX_MAC_CLEAR_DYNAMIC,
++};
++
++/**
++ * struct mxl862xx_mac_table_clear - MAC table clear
++ * @type: see &enum mxl862xx_mac_clear_type
++ * @port_id: physical port id
++ */
++struct mxl862xx_mac_table_clear {
++ u8 type;
++ u8 port_id;
++} __packed;
++
++/**
++ * enum mxl862xx_age_timer - Aging Timer Value.
++ * @MXL862XX_AGETIMER_1_SEC: 1 second aging time
++ * @MXL862XX_AGETIMER_10_SEC: 10 seconds aging time
++ * @MXL862XX_AGETIMER_300_SEC: 300 seconds aging time
++ * @MXL862XX_AGETIMER_1_HOUR: 1 hour aging time
++ * @MXL862XX_AGETIMER_1_DAY: 24 hours aging time
++ * @MXL862XX_AGETIMER_CUSTOM: Custom aging time in seconds
++ */
++enum mxl862xx_age_timer {
++ MXL862XX_AGETIMER_1_SEC = 1,
++ MXL862XX_AGETIMER_10_SEC,
++ MXL862XX_AGETIMER_300_SEC,
++ MXL862XX_AGETIMER_1_HOUR,
++ MXL862XX_AGETIMER_1_DAY,
++ MXL862XX_AGETIMER_CUSTOM,
++};
++
++/**
++ * struct mxl862xx_bridge_alloc - Bridge Allocation
++ * @bridge_id: If the bridge allocation is successful, a valid ID will be
++ * returned in this field. Otherwise, INVALID_HANDLE is
++ * returned. For bridge free, this field should contain a
++ * valid ID returned by the bridge allocation. ID 0 is not
++ * used for historic reasons.
++ *
++ * Used by MXL862XX_BRIDGE_ALLOC and MXL862XX_BRIDGE_FREE.
++ */
++struct mxl862xx_bridge_alloc {
++ __le16 bridge_id;
++};
++
++/**
++ * enum mxl862xx_bridge_config_mask - Bridge configuration mask
++ * @MXL862XX_BRIDGE_CONFIG_MASK_MAC_LEARNING_LIMIT:
++ * Mask for mac_learning_limit_enable and mac_learning_limit.
++ * @MXL862XX_BRIDGE_CONFIG_MASK_MAC_LEARNED_COUNT:
++ * Mask for mac_learning_count
++ * @MXL862XX_BRIDGE_CONFIG_MASK_MAC_DISCARD_COUNT:
++ * Mask for learning_discard_event
++ * @MXL862XX_BRIDGE_CONFIG_MASK_SUB_METER:
++ * Mask for sub_metering_enable and traffic_sub_meter_id
++ * @MXL862XX_BRIDGE_CONFIG_MASK_FORWARDING_MODE:
++ * Mask for forward_broadcast, forward_unknown_multicast_ip,
++ * forward_unknown_multicast_non_ip and forward_unknown_unicast.
++ * @MXL862XX_BRIDGE_CONFIG_MASK_ALL: Enable all
++ * @MXL862XX_BRIDGE_CONFIG_MASK_FORCE: Bypass any check for debug purpose
++ */
++enum mxl862xx_bridge_config_mask {
++ MXL862XX_BRIDGE_CONFIG_MASK_MAC_LEARNING_LIMIT = BIT(0),
++ MXL862XX_BRIDGE_CONFIG_MASK_MAC_LEARNED_COUNT = BIT(1),
++ MXL862XX_BRIDGE_CONFIG_MASK_MAC_DISCARD_COUNT = BIT(2),
++ MXL862XX_BRIDGE_CONFIG_MASK_SUB_METER = BIT(3),
++ MXL862XX_BRIDGE_CONFIG_MASK_FORWARDING_MODE = BIT(4),
++ MXL862XX_BRIDGE_CONFIG_MASK_ALL = 0x7FFFFFFF,
++ MXL862XX_BRIDGE_CONFIG_MASK_FORCE = BIT(31)
++};
++
++/**
++ * enum mxl862xx_bridge_port_egress_meter - Meters for egress traffic type
++ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST:
++ * Index of broadcast traffic meter
++ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_MULTICAST:
++ * Index of known multicast traffic meter
++ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP:
++ * Index of unknown multicast IP traffic meter
++ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP:
++ * Index of unknown multicast non-IP traffic meter
++ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC:
++ * Index of unknown unicast traffic meter
++ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_OTHERS:
++ * Index of traffic meter for other types
++ * @MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX: Number of index
++ */
++enum mxl862xx_bridge_port_egress_meter {
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST = 0,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_MULTICAST,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_OTHERS,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX,
++};
++
++/**
++ * enum mxl862xx_bridge_forward_mode - Bridge forwarding type of packet
++ * @MXL862XX_BRIDGE_FORWARD_FLOOD: Packet is flooded to port members of
++ * ingress bridge port
++ * @MXL862XX_BRIDGE_FORWARD_DISCARD: Packet is discarded
++ */
++enum mxl862xx_bridge_forward_mode {
++ MXL862XX_BRIDGE_FORWARD_FLOOD = 0,
++ MXL862XX_BRIDGE_FORWARD_DISCARD,
++};
++
++/**
++ * struct mxl862xx_bridge_config - Bridge Configuration
++ * @bridge_id: Bridge ID (FID)
++ * @mask: See &enum mxl862xx_bridge_config_mask
++ * @mac_learning_limit_enable: Enable MAC learning limitation
++ * @mac_learning_limit: Max number of MAC addresses that can be learned in
++ * this bridge (all bridge ports)
++ * @mac_learning_count: Number of MAC addresses learned from this bridge
++ * @learning_discard_event: Number of learning discard events due to
++ * hardware resource not available
++ * @sub_metering_enable: Traffic metering on type of traffic (such as
++ * broadcast, multicast, unknown unicast, etc) applies
++ * @traffic_sub_meter_id: Meter for bridge process with specific type (such
++ * as broadcast, multicast, unknown unicast, etc)
++ * @forward_broadcast: Forwarding mode of broadcast traffic. See
++ * &enum mxl862xx_bridge_forward_mode
++ * @forward_unknown_multicast_ip: Forwarding mode of unknown multicast IP
++ * traffic.
++ * See &enum mxl862xx_bridge_forward_mode
++ * @forward_unknown_multicast_non_ip: Forwarding mode of unknown multicast
++ * non-IP traffic.
++ * See &enum mxl862xx_bridge_forward_mode
++ * @forward_unknown_unicast: Forwarding mode of unknown unicast traffic. See
++ * &enum mxl862xx_bridge_forward_mode
++ */
++struct mxl862xx_bridge_config {
++ __le16 bridge_id;
++ __le32 mask; /* enum mxl862xx_bridge_config_mask */
++ u8 mac_learning_limit_enable;
++ __le16 mac_learning_limit;
++ __le16 mac_learning_count;
++ __le32 learning_discard_event;
++ u8 sub_metering_enable[MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX];
++ __le16 traffic_sub_meter_id[MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX];
++ __le32 forward_broadcast; /* enum mxl862xx_bridge_forward_mode */
++ __le32 forward_unknown_multicast_ip; /* enum mxl862xx_bridge_forward_mode */
++ __le32 forward_unknown_multicast_non_ip; /* enum mxl862xx_bridge_forward_mode */
++ __le32 forward_unknown_unicast; /* enum mxl862xx_bridge_forward_mode */
++} __packed;
++
++/**
++ * struct mxl862xx_bridge_port_alloc - Bridge Port Allocation
++ * @bridge_port_id: If the bridge port allocation is successful, a valid ID
++ * will be returned in this field. Otherwise, INVALID_HANDLE
++ * is returned. For bridge port free, this field should
++ * contain a valid ID returned by the bridge port allocation.
++ *
++ * Used by MXL862XX_BRIDGE_PORT_ALLOC and MXL862XX_BRIDGE_PORT_FREE.
++ */
++struct mxl862xx_bridge_port_alloc {
++ __le16 bridge_port_id;
++};
++
++/**
++ * enum mxl862xx_bridge_port_config_mask - Bridge Port configuration mask
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID:
++ * Mask for bridge_id
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN:
++ * Mask for ingress_extended_vlan_enable,
++ * ingress_extended_vlan_block_id and ingress_extended_vlan_block_size
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN:
++ * Mask for egress_extended_vlan_enable, egress_extended_vlan_block_id
++ * and egress_extended_vlan_block_size
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_MARKING:
++ * Mask for ingress_marking_mode
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_REMARKING:
++ * Mask for egress_remarking_mode
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_METER:
++ * Mask for ingress_metering_enable and ingress_traffic_meter_id
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER:
++ * Mask for egress_sub_metering_enable and egress_traffic_sub_meter_id
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING:
++ * Mask for dest_logical_port_id, pmapper_enable, dest_sub_if_id_group,
++ * pmapper_mapping_mode, pmapper_id_valid and pmapper
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP:
++ * Mask for bridge_port_map
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_DEST_IP_LOOKUP:
++ * Mask for mc_dest_ip_lookup_disable
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_IP_LOOKUP:
++ * Mask for mc_src_ip_lookup_enable
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_DEST_MAC_LOOKUP:
++ * Mask for dest_mac_lookup_disable
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING:
++ * Mask for src_mac_learning_disable
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_SPOOFING:
++ * Mask for mac_spoofing_detect_enable
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_PORT_LOCK:
++ * Mask for port_lock_enable
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_LEARNING_LIMIT:
++ * Mask for mac_learning_limit_enable and mac_learning_limit
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_LEARNED_COUNT:
++ * Mask for mac_learning_count
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN_FILTER:
++ * Mask for ingress_vlan_filter_enable, ingress_vlan_filter_block_id
++ * and ingress_vlan_filter_block_size
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER1:
++ * Mask for bypass_egress_vlan_filter1, egress_vlan_filter1enable,
++ * egress_vlan_filter1block_id and egress_vlan_filter1block_size
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER2:
++ * Mask for egress_vlan_filter2enable, egress_vlan_filter2block_id and
++ * egress_vlan_filter2block_size
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING:
++ * Mask for vlan_tag_selection, vlan_src_mac_priority_enable,
++ * vlan_src_mac_dei_enable, vlan_src_mac_vid_enable,
++ * vlan_dst_mac_priority_enable, vlan_dst_mac_dei_enable and
++ * vlan_dst_mac_vid_enable
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MULTICAST_LOOKUP:
++ * Mask for vlan_multicast_priority_enable,
++ * vlan_multicast_dei_enable and vlan_multicast_vid_enable
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_LOOP_VIOLATION_COUNTER:
++ * Mask for loop_violation_count
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_ALL: Enable all
++ * @MXL862XX_BRIDGE_PORT_CONFIG_MASK_FORCE: Bypass any check for debug purpose
++ */
++enum mxl862xx_bridge_port_config_mask {
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID = BIT(0),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN = BIT(1),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN = BIT(2),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_MARKING = BIT(3),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_REMARKING = BIT(4),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_METER = BIT(5),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER = BIT(6),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING = BIT(7),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP = BIT(8),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_DEST_IP_LOOKUP = BIT(9),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_IP_LOOKUP = BIT(10),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_DEST_MAC_LOOKUP = BIT(11),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING = BIT(12),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_SPOOFING = BIT(13),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_PORT_LOCK = BIT(14),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_LEARNING_LIMIT = BIT(15),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MAC_LEARNED_COUNT = BIT(16),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN_FILTER = BIT(17),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER1 = BIT(18),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER2 = BIT(19),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING = BIT(20),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MULTICAST_LOOKUP = BIT(21),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_LOOP_VIOLATION_COUNTER = BIT(22),
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_ALL = 0x7FFFFFFF,
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_FORCE = BIT(31)
++};
++
++/**
++ * enum mxl862xx_color_marking_mode - Color Marking Mode
++ * @MXL862XX_MARKING_ALL_GREEN: mark packets (except critical) to green
++ * @MXL862XX_MARKING_INTERNAL_MARKING: do not change color and priority
++ * @MXL862XX_MARKING_DEI: DEI mark mode
++ * @MXL862XX_MARKING_PCP_8P0D: PCP 8P0D mark mode
++ * @MXL862XX_MARKING_PCP_7P1D: PCP 7P1D mark mode
++ * @MXL862XX_MARKING_PCP_6P2D: PCP 6P2D mark mode
++ * @MXL862XX_MARKING_PCP_5P3D: PCP 5P3D mark mode
++ * @MXL862XX_MARKING_DSCP_AF: DSCP AF class
++ */
++enum mxl862xx_color_marking_mode {
++ MXL862XX_MARKING_ALL_GREEN = 0,
++ MXL862XX_MARKING_INTERNAL_MARKING,
++ MXL862XX_MARKING_DEI,
++ MXL862XX_MARKING_PCP_8P0D,
++ MXL862XX_MARKING_PCP_7P1D,
++ MXL862XX_MARKING_PCP_6P2D,
++ MXL862XX_MARKING_PCP_5P3D,
++ MXL862XX_MARKING_DSCP_AF,
++};
++
++/**
++ * enum mxl862xx_color_remarking_mode - Color Remarking Mode
++ * @MXL862XX_REMARKING_NONE: values from last process stage
++ * @MXL862XX_REMARKING_DEI: DEI mark mode
++ * @MXL862XX_REMARKING_PCP_8P0D: PCP 8P0D mark mode
++ * @MXL862XX_REMARKING_PCP_7P1D: PCP 7P1D mark mode
++ * @MXL862XX_REMARKING_PCP_6P2D: PCP 6P2D mark mode
++ * @MXL862XX_REMARKING_PCP_5P3D: PCP 5P3D mark mode
++ * @MXL862XX_REMARKING_DSCP_AF: DSCP AF class
++ */
++enum mxl862xx_color_remarking_mode {
++ MXL862XX_REMARKING_NONE = 0,
++ MXL862XX_REMARKING_DEI = 2,
++ MXL862XX_REMARKING_PCP_8P0D,
++ MXL862XX_REMARKING_PCP_7P1D,
++ MXL862XX_REMARKING_PCP_6P2D,
++ MXL862XX_REMARKING_PCP_5P3D,
++ MXL862XX_REMARKING_DSCP_AF,
++};
++
++/**
++ * enum mxl862xx_pmapper_mapping_mode - P-mapper Mapping Mode
++ * @MXL862XX_PMAPPER_MAPPING_PCP: Use PCP for VLAN tagged packets to derive
++ * sub interface ID group
++ * @MXL862XX_PMAPPER_MAPPING_LAG: Use LAG Index for Pmapper access
++ * regardless of IP and VLAN packet
++ * @MXL862XX_PMAPPER_MAPPING_DSCP: Use DSCP for VLAN tagged IP packets to
++ * derive sub interface ID group
++ */
++enum mxl862xx_pmapper_mapping_mode {
++ MXL862XX_PMAPPER_MAPPING_PCP = 0,
++ MXL862XX_PMAPPER_MAPPING_LAG,
++ MXL862XX_PMAPPER_MAPPING_DSCP,
++};
++
++/**
++ * struct mxl862xx_pmapper - P-mapper Configuration
++ * @pmapper_id: Index of P-mapper (0-31)
++ * @dest_sub_if_id_group: Sub interface ID group. Entry 0 is for non-IP and
++ * non-VLAN tagged packets.
++ * Entries 1-8 are PCP mapping entries for VLAN tagged
++ * packets.
++ * Entries 9-72 are DSCP or LAG mapping entries.
++ *
++ * Used by CTP port config and bridge port config. In case of LAG, it is
++ * user's responsibility to provide the mapped entries in given P-mapper
++ * table. In other modes the entries are auto mapped from input packet.
++ */
++struct mxl862xx_pmapper {
++ __le16 pmapper_id;
++ u8 dest_sub_if_id_group[73];
++} __packed;
++
++/**
++ * struct mxl862xx_bridge_port_config - Bridge Port Configuration
++ * @bridge_port_id: Bridge Port ID allocated by bridge port allocation
++ * @mask: See &enum mxl862xx_bridge_port_config_mask
++ * @bridge_id: Bridge ID (FID) to which this bridge port is associated
++ * @ingress_extended_vlan_enable: Enable extended VLAN processing for
++ * ingress traffic
++ * @ingress_extended_vlan_block_id: Extended VLAN block allocated for
++ * ingress traffic
++ * @ingress_extended_vlan_block_size: Extended VLAN block size for ingress
++ * traffic
++ * @egress_extended_vlan_enable: Enable extended VLAN processing for egress
++ * traffic
++ * @egress_extended_vlan_block_id: Extended VLAN block allocated for egress
++ * traffic
++ * @egress_extended_vlan_block_size: Extended VLAN block size for egress
++ * traffic
++ * @ingress_marking_mode: Ingress color marking mode. See
++ * &enum mxl862xx_color_marking_mode
++ * @egress_remarking_mode: Color remarking for egress traffic. See
++ * &enum mxl862xx_color_remarking_mode
++ * @ingress_metering_enable: Traffic metering on ingress traffic applies
++ * @ingress_traffic_meter_id: Meter for ingress Bridge Port process
++ * @egress_sub_metering_enable: Traffic metering on various types of egress
++ * traffic
++ * @egress_traffic_sub_meter_id: Meter for egress Bridge Port process with
++ * specific type
++ * @dest_logical_port_id: Destination logical port
++ * @pmapper_enable: Enable P-mapper
++ * @dest_sub_if_id_group: Destination sub interface ID group when
++ * pmapper_enable is false
++ * @pmapper_mapping_mode: P-mapper mapping mode. See
++ * &enum mxl862xx_pmapper_mapping_mode
++ * @pmapper_id_valid: When true, P-mapper is re-used; when false,
++ * allocation is handled by API
++ * @pmapper: P-mapper configuration used when pmapper_enable is true
++ * @bridge_port_map: Port map defining broadcast domain. Each bit
++ * represents one bridge port. Bridge port ID is
++ * index * 16 + bit offset.
++ * @mc_dest_ip_lookup_disable: Disable multicast IP destination table
++ * lookup
++ * @mc_src_ip_lookup_enable: Enable multicast IP source table lookup
++ * @dest_mac_lookup_disable: Disable destination MAC lookup; packet treated
++ * as unknown
++ * @src_mac_learning_disable: Disable source MAC address learning
++ * @mac_spoofing_detect_enable: Enable MAC spoofing detection
++ * @port_lock_enable: Enable port locking
++ * @mac_learning_limit_enable: Enable MAC learning limitation
++ * @mac_learning_limit: Maximum number of MAC addresses that can be learned
++ * from this bridge port
++ * @loop_violation_count: Number of loop violation events from this bridge
++ * port
++ * @mac_learning_count: Number of MAC addresses learned from this bridge
++ * port
++ * @ingress_vlan_filter_enable: Enable ingress VLAN filter
++ * @ingress_vlan_filter_block_id: VLAN filter block of ingress traffic
++ * @ingress_vlan_filter_block_size: VLAN filter block size for ingress
++ * traffic
++ * @bypass_egress_vlan_filter1: For ingress traffic, bypass VLAN filter 1
++ * at egress bridge port processing
++ * @egress_vlan_filter1enable: Enable egress VLAN filter 1
++ * @egress_vlan_filter1block_id: VLAN filter block 1 of egress traffic
++ * @egress_vlan_filter1block_size: VLAN filter block 1 size
++ * @egress_vlan_filter2enable: Enable egress VLAN filter 2
++ * @egress_vlan_filter2block_id: VLAN filter block 2 of egress traffic
++ * @egress_vlan_filter2block_size: VLAN filter block 2 size
++ * @vlan_tag_selection: VLAN tag selection for MAC address/multicast
++ * learning, lookup and filtering.
++ * 0 - Intermediate outer VLAN tag is used.
++ * 1 - Original outer VLAN tag is used.
++ * @vlan_src_mac_priority_enable: Enable VLAN Priority field for source MAC
++ * learning and filtering
++ * @vlan_src_mac_dei_enable: Enable VLAN DEI/CFI field for source MAC
++ * learning and filtering
++ * @vlan_src_mac_vid_enable: Enable VLAN ID field for source MAC learning
++ * and filtering
++ * @vlan_dst_mac_priority_enable: Enable VLAN Priority field for destination
++ * MAC lookup and filtering
++ * @vlan_dst_mac_dei_enable: Enable VLAN CFI/DEI field for destination MAC
++ * lookup and filtering
++ * @vlan_dst_mac_vid_enable: Enable VLAN ID field for destination MAC lookup
++ * and filtering
++ * @vlan_multicast_priority_enable: Enable VLAN Priority field for IP
++ * multicast lookup
++ * @vlan_multicast_dei_enable: Enable VLAN CFI/DEI field for IP multicast
++ * lookup
++ * @vlan_multicast_vid_enable: Enable VLAN ID field for IP multicast lookup
++ */
++struct mxl862xx_bridge_port_config {
++ __le16 bridge_port_id;
++ __le32 mask; /* enum mxl862xx_bridge_port_config_mask */
++ __le16 bridge_id;
++ u8 ingress_extended_vlan_enable;
++ __le16 ingress_extended_vlan_block_id;
++ __le16 ingress_extended_vlan_block_size;
++ u8 egress_extended_vlan_enable;
++ __le16 egress_extended_vlan_block_id;
++ __le16 egress_extended_vlan_block_size;
++ __le32 ingress_marking_mode; /* enum mxl862xx_color_marking_mode */
++ __le32 egress_remarking_mode; /* enum mxl862xx_color_remarking_mode */
++ u8 ingress_metering_enable;
++ __le16 ingress_traffic_meter_id;
++ u8 egress_sub_metering_enable[MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX];
++ __le16 egress_traffic_sub_meter_id[MXL862XX_BRIDGE_PORT_EGRESS_METER_MAX];
++ u8 dest_logical_port_id;
++ u8 pmapper_enable;
++ __le16 dest_sub_if_id_group;
++ __le32 pmapper_mapping_mode; /* enum mxl862xx_pmapper_mapping_mode */
++ u8 pmapper_id_valid;
++ struct mxl862xx_pmapper pmapper;
++ __le16 bridge_port_map[8];
++ u8 mc_dest_ip_lookup_disable;
++ u8 mc_src_ip_lookup_enable;
++ u8 dest_mac_lookup_disable;
++ u8 src_mac_learning_disable;
++ u8 mac_spoofing_detect_enable;
++ u8 port_lock_enable;
++ u8 mac_learning_limit_enable;
++ __le16 mac_learning_limit;
++ __le16 loop_violation_count;
++ __le16 mac_learning_count;
++ u8 ingress_vlan_filter_enable;
++ __le16 ingress_vlan_filter_block_id;
++ __le16 ingress_vlan_filter_block_size;
++ u8 bypass_egress_vlan_filter1;
++ u8 egress_vlan_filter1enable;
++ __le16 egress_vlan_filter1block_id;
++ __le16 egress_vlan_filter1block_size;
++ u8 egress_vlan_filter2enable;
++ __le16 egress_vlan_filter2block_id;
++ __le16 egress_vlan_filter2block_size;
++ u8 vlan_tag_selection;
++ u8 vlan_src_mac_priority_enable;
++ u8 vlan_src_mac_dei_enable;
++ u8 vlan_src_mac_vid_enable;
++ u8 vlan_dst_mac_priority_enable;
++ u8 vlan_dst_mac_dei_enable;
++ u8 vlan_dst_mac_vid_enable;
++ u8 vlan_multicast_priority_enable;
++ u8 vlan_multicast_dei_enable;
++ u8 vlan_multicast_vid_enable;
++} __packed;
++
++/**
++ * struct mxl862xx_cfg - Global Switch configuration Attributes
++ * @mac_table_age_timer: See &enum mxl862xx_age_timer
++ * @age_timer: Custom MAC table aging timer in seconds
++ * @max_packet_len: Maximum Ethernet packet length
++ * @learning_limit_action: Automatic MAC address table learning limitation
++ * consecutive action
++ * @mac_locking_action: Accept or discard MAC port locking violation
++ * packets
++ * @mac_spoofing_action: Accept or discard MAC spoofing and port MAC locking
++ * violation packets
++ * @pause_mac_mode_src: Pause frame MAC source address mode
++ * @pause_mac_src: Pause frame MAC source address
++ */
++struct mxl862xx_cfg {
++ __le32 mac_table_age_timer; /* enum mxl862xx_age_timer */
++ __le32 age_timer;
++ __le16 max_packet_len;
++ u8 learning_limit_action;
++ u8 mac_locking_action;
++ u8 mac_spoofing_action;
++ u8 pause_mac_mode_src;
++ u8 pause_mac_src[ETH_ALEN];
++} __packed;
++
++/**
++ * enum mxl862xx_ss_sp_tag_mask - Special tag valid field indicator bits
++ * @MXL862XX_SS_SP_TAG_MASK_RX: valid RX special tag mode
++ * @MXL862XX_SS_SP_TAG_MASK_TX: valid TX special tag mode
++ * @MXL862XX_SS_SP_TAG_MASK_RX_PEN: valid RX special tag info over preamble
++ * @MXL862XX_SS_SP_TAG_MASK_TX_PEN: valid TX special tag info over preamble
++ */
++enum mxl862xx_ss_sp_tag_mask {
++ MXL862XX_SS_SP_TAG_MASK_RX = BIT(0),
++ MXL862XX_SS_SP_TAG_MASK_TX = BIT(1),
++ MXL862XX_SS_SP_TAG_MASK_RX_PEN = BIT(2),
++ MXL862XX_SS_SP_TAG_MASK_TX_PEN = BIT(3),
++};
++
++/**
++ * enum mxl862xx_ss_sp_tag_rx - RX special tag mode
++ * @MXL862XX_SS_SP_TAG_RX_NO_TAG_NO_INSERT: packet does NOT have special
++ * tag and special tag is NOT inserted
++ * @MXL862XX_SS_SP_TAG_RX_NO_TAG_INSERT: packet does NOT have special tag
++ * and special tag is inserted
++ * @MXL862XX_SS_SP_TAG_RX_TAG_NO_INSERT: packet has special tag and special
++ * tag is NOT inserted
++ */
++enum mxl862xx_ss_sp_tag_rx {
++ MXL862XX_SS_SP_TAG_RX_NO_TAG_NO_INSERT = 0,
++ MXL862XX_SS_SP_TAG_RX_NO_TAG_INSERT = 1,
++ MXL862XX_SS_SP_TAG_RX_TAG_NO_INSERT = 2,
++};
++
++/**
++ * enum mxl862xx_ss_sp_tag_tx - TX special tag mode
++ * @MXL862XX_SS_SP_TAG_TX_NO_TAG_NO_REMOVE: packet does NOT have special
++ * tag and special tag is NOT removed
++ * @MXL862XX_SS_SP_TAG_TX_TAG_REPLACE: packet has special tag and special
++ * tag is replaced
++ * @MXL862XX_SS_SP_TAG_TX_TAG_NO_REMOVE: packet has special tag and special
++ * tag is NOT removed
++ * @MXL862XX_SS_SP_TAG_TX_TAG_REMOVE: packet has special tag and special
++ * tag is removed
++ */
++enum mxl862xx_ss_sp_tag_tx {
++ MXL862XX_SS_SP_TAG_TX_NO_TAG_NO_REMOVE = 0,
++ MXL862XX_SS_SP_TAG_TX_TAG_REPLACE = 1,
++ MXL862XX_SS_SP_TAG_TX_TAG_NO_REMOVE = 2,
++ MXL862XX_SS_SP_TAG_TX_TAG_REMOVE = 3,
++};
++
++/**
++ * enum mxl862xx_ss_sp_tag_rx_pen - RX special tag info over preamble
++ * @MXL862XX_SS_SP_TAG_RX_PEN_ALL_0: special tag info inserted from byte 2
++ * to 7 are all 0
++ * @MXL862XX_SS_SP_TAG_RX_PEN_BYTE_5_IS_16: special tag byte 5 is 16, other
++ * bytes from 2 to 7 are 0
++ * @MXL862XX_SS_SP_TAG_RX_PEN_BYTE_5_FROM_PREAMBLE: special tag byte 5 is
++ * from preamble field, others
++ * are 0
++ * @MXL862XX_SS_SP_TAG_RX_PEN_BYTE_2_TO_7_FROM_PREAMBLE: special tag byte 2
++ * to 7 are from preamble
++ * field
++ */
++enum mxl862xx_ss_sp_tag_rx_pen {
++ MXL862XX_SS_SP_TAG_RX_PEN_ALL_0 = 0,
++ MXL862XX_SS_SP_TAG_RX_PEN_BYTE_5_IS_16 = 1,
++ MXL862XX_SS_SP_TAG_RX_PEN_BYTE_5_FROM_PREAMBLE = 2,
++ MXL862XX_SS_SP_TAG_RX_PEN_BYTE_2_TO_7_FROM_PREAMBLE = 3,
++};
++
++/**
++ * struct mxl862xx_ss_sp_tag - Special tag port settings
++ * @pid: port ID (1~16)
++ * @mask: See &enum mxl862xx_ss_sp_tag_mask
++ * @rx: See &enum mxl862xx_ss_sp_tag_rx
++ * @tx: See &enum mxl862xx_ss_sp_tag_tx
++ * @rx_pen: See &enum mxl862xx_ss_sp_tag_rx_pen
++ * @tx_pen: TX special tag info over preamble
++ * 0 - disabled
++ * 1 - enabled
++ */
++struct mxl862xx_ss_sp_tag {
++ u8 pid;
++ u8 mask; /* enum mxl862xx_ss_sp_tag_mask */
++ u8 rx; /* enum mxl862xx_ss_sp_tag_rx */
++ u8 tx; /* enum mxl862xx_ss_sp_tag_tx */
++ u8 rx_pen; /* enum mxl862xx_ss_sp_tag_rx_pen */
++ u8 tx_pen; /* boolean */
++} __packed;
++
++/**
++ * enum mxl862xx_logical_port_mode - Logical port mode
++ * @MXL862XX_LOGICAL_PORT_8BIT_WLAN: WLAN with 8-bit station ID
++ * @MXL862XX_LOGICAL_PORT_9BIT_WLAN: WLAN with 9-bit station ID
++ * @MXL862XX_LOGICAL_PORT_ETHERNET: Ethernet port
++ * @MXL862XX_LOGICAL_PORT_OTHER: Others
++ */
++enum mxl862xx_logical_port_mode {
++ MXL862XX_LOGICAL_PORT_8BIT_WLAN = 0,
++ MXL862XX_LOGICAL_PORT_9BIT_WLAN,
++ MXL862XX_LOGICAL_PORT_ETHERNET,
++ MXL862XX_LOGICAL_PORT_OTHER = 0xFF,
++};
++
++/**
++ * struct mxl862xx_ctp_port_assignment - CTP Port Assignment/association
++ * with logical port
++ * @logical_port_id: Logical Port Id. The valid range is hardware dependent
++ * @first_ctp_port_id: First CTP (Connectivity Termination Port) ID mapped
++ * to above logical port ID
++ * @number_of_ctp_port: Total number of CTP Ports mapped above logical port
++ * ID
++ * @mode: Logical port mode to define sub interface ID format. See
++ * &enum mxl862xx_logical_port_mode
++ * @bridge_port_id: Bridge Port ID (not FID). For allocation, each CTP
++ * allocated is mapped to the Bridge Port given by this field.
++ * The Bridge Port will be configured to use first CTP as
++ * egress CTP.
++ */
++struct mxl862xx_ctp_port_assignment {
++ u8 logical_port_id;
++ __le16 first_ctp_port_id;
++ __le16 number_of_ctp_port;
++ __le32 mode; /* enum mxl862xx_logical_port_mode */
++ __le16 bridge_port_id;
++} __packed;
++
++/**
++ * struct mxl862xx_sys_fw_image_version - Firmware version information
++ * @iv_major: firmware major version
++ * @iv_minor: firmware minor version
++ * @iv_revision: firmware revision
++ * @iv_build_num: firmware build number
++ */
++struct mxl862xx_sys_fw_image_version {
++ u8 iv_major;
++ u8 iv_minor;
++ __le16 iv_revision;
++ __le32 iv_build_num;
++} __packed;
++
++#endif /* __MXL862XX_API_H */
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -0,0 +1,49 @@
++/* SPDX-License-Identifier: GPL-2.0-or-later */
++
++#ifndef __MXL862XX_CMD_H
++#define __MXL862XX_CMD_H
++
++#define MXL862XX_MMD_DEV 30
++#define MXL862XX_MMD_REG_CTRL 0
++#define MXL862XX_MMD_REG_LEN_RET 1
++#define MXL862XX_MMD_REG_DATA_FIRST 2
++#define MXL862XX_MMD_REG_DATA_LAST 95
++#define MXL862XX_MMD_REG_DATA_MAX_SIZE \
++ (MXL862XX_MMD_REG_DATA_LAST - MXL862XX_MMD_REG_DATA_FIRST + 1)
++
++#define MXL862XX_COMMON_MAGIC 0x100
++#define MXL862XX_BRDG_MAGIC 0x300
++#define MXL862XX_BRDGPORT_MAGIC 0x400
++#define MXL862XX_CTP_MAGIC 0x500
++#define MXL862XX_SWMAC_MAGIC 0xa00
++#define MXL862XX_SS_MAGIC 0x1600
++#define GPY_GPY2XX_MAGIC 0x1800
++#define SYS_MISC_MAGIC 0x1900
++
++#define MXL862XX_COMMON_CFGGET (MXL862XX_COMMON_MAGIC + 0x9)
++#define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11)
++
++#define MXL862XX_BRIDGE_ALLOC (MXL862XX_BRDG_MAGIC + 0x1)
++#define MXL862XX_BRIDGE_CONFIGSET (MXL862XX_BRDG_MAGIC + 0x2)
++#define MXL862XX_BRIDGE_CONFIGGET (MXL862XX_BRDG_MAGIC + 0x3)
++#define MXL862XX_BRIDGE_FREE (MXL862XX_BRDG_MAGIC + 0x4)
++
++#define MXL862XX_BRIDGEPORT_ALLOC (MXL862XX_BRDGPORT_MAGIC + 0x1)
++#define MXL862XX_BRIDGEPORT_CONFIGSET (MXL862XX_BRDGPORT_MAGIC + 0x2)
++#define MXL862XX_BRIDGEPORT_CONFIGGET (MXL862XX_BRDGPORT_MAGIC + 0x3)
++#define MXL862XX_BRIDGEPORT_FREE (MXL862XX_BRDGPORT_MAGIC + 0x4)
++
++#define MXL862XX_CTP_PORTASSIGNMENTSET (MXL862XX_CTP_MAGIC + 0x3)
++
++#define MXL862XX_MAC_TABLECLEARCOND (MXL862XX_SWMAC_MAGIC + 0x8)
++
++#define MXL862XX_SS_SPTAG_SET (MXL862XX_SS_MAGIC + 0x02)
++
++#define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x01)
++#define INT_GPHY_WRITE (GPY_GPY2XX_MAGIC + 0x02)
++
++#define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x02)
++
++#define MMD_API_MAXIMUM_ID 0x7fff
++
++#endif /* __MXL862XX_CMD_H */
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+@@ -0,0 +1,245 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * Based upon the MaxLinear SDK driver
++ *
++ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
++ * Copyright (C) 2025 John Crispin <john@phrozen.org>
++ * Copyright (C) 2024 MaxLinear Inc.
++ */
++
++#include <linux/bits.h>
++#include <linux/iopoll.h>
++#include <linux/limits.h>
++#include <net/dsa.h>
++#include "mxl862xx.h"
++#include "mxl862xx-host.h"
++
++#define CTRL_BUSY_MASK BIT(15)
++
++#define MXL862XX_MMD_REG_CTRL 0
++#define MXL862XX_MMD_REG_LEN_RET 1
++#define MXL862XX_MMD_REG_DATA_FIRST 2
++#define MXL862XX_MMD_REG_DATA_LAST 95
++#define MXL862XX_MMD_REG_DATA_MAX_SIZE \
++ (MXL862XX_MMD_REG_DATA_LAST - MXL862XX_MMD_REG_DATA_FIRST + 1)
++
++#define MMD_API_SET_DATA_0 2
++#define MMD_API_GET_DATA_0 5
++#define MMD_API_RST_DATA 8
++
++#define MXL862XX_SWITCH_RESET 0x9907
++
++static int mxl862xx_reg_read(struct mxl862xx_priv *priv, u32 addr)
++{
++ return __mdiodev_c45_read(priv->mdiodev, MDIO_MMD_VEND1, addr);
++}
++
++static int mxl862xx_reg_write(struct mxl862xx_priv *priv, u32 addr, u16 data)
++{
++ return __mdiodev_c45_write(priv->mdiodev, MDIO_MMD_VEND1, addr, data);
++}
++
++static int mxl862xx_ctrl_read(struct mxl862xx_priv *priv)
++{
++ return mxl862xx_reg_read(priv, MXL862XX_MMD_REG_CTRL);
++}
++
++static int mxl862xx_busy_wait(struct mxl862xx_priv *priv)
++{
++ int val;
++
++ return readx_poll_timeout(mxl862xx_ctrl_read, priv, val,
++ !(val & CTRL_BUSY_MASK), 15, 500000);
++}
++
++static int mxl862xx_set_data(struct mxl862xx_priv *priv, u16 words)
++{
++ int ret;
++ u16 cmd;
++
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET,
++ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
++ if (ret < 0)
++ return ret;
++
++ cmd = words / MXL862XX_MMD_REG_DATA_MAX_SIZE - 1;
++ if (!(cmd < 2))
++ return -EINVAL;
++
++ cmd += MMD_API_SET_DATA_0;
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
++ cmd | CTRL_BUSY_MASK);
++ if (ret < 0)
++ return ret;
++
++ return mxl862xx_busy_wait(priv);
++}
++
++static int mxl862xx_get_data(struct mxl862xx_priv *priv, u16 words)
++{
++ int ret;
++ u16 cmd;
++
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET,
++ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
++ if (ret < 0)
++ return ret;
++
++ cmd = words / MXL862XX_MMD_REG_DATA_MAX_SIZE;
++ if (!(cmd > 0 && cmd < 3))
++ return -EINVAL;
++
++ cmd += MMD_API_GET_DATA_0;
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
++ cmd | CTRL_BUSY_MASK);
++ if (ret < 0)
++ return ret;
++
++ return mxl862xx_busy_wait(priv);
++}
++
++static int mxl862xx_firmware_return(int ret)
++{
++ /* Only 16-bit values are valid. */
++ if (WARN_ON(ret & GENMASK(31, 16)))
++ return -EINVAL;
++
++ /* Interpret value as signed 16-bit integer. */
++ return (s16)ret;
++}
++
++static int mxl862xx_send_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 size,
++ bool quiet)
++{
++ int ret;
++
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET, size);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
++ cmd | CTRL_BUSY_MASK);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_busy_wait(priv);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_LEN_RET);
++ if (ret < 0)
++ return ret;
++
++ /* handle errors returned by the firmware as -EIO
++ * The firmware is based on Zephyr OS and uses the errors as
++ * defined in errno.h of Zephyr OS. See
++ * https://github.com/zephyrproject-rtos/zephyr/blob/v3.7.0/lib/libc/minimal/include/errno.h
++ */
++ ret = mxl862xx_firmware_return(ret);
++ if (ret < 0) {
++ if (!quiet)
++ dev_err(&priv->mdiodev->dev,
++ "CMD %04x returned error %d\n", cmd, ret);
++ return -EIO;
++ }
++
++ return ret;
++}
++
++int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *_data,
++ u16 size, bool read, bool quiet)
++{
++ __le16 *data = _data;
++ int ret, cmd_ret;
++ u16 max, i;
++
++ dev_dbg(&priv->mdiodev->dev, "CMD %04x DATA %*ph\n", cmd, size, data);
++
++ mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
++
++ max = (size + 1) / 2;
++
++ ret = mxl862xx_busy_wait(priv);
++ if (ret < 0)
++ goto out;
++
++ for (i = 0; i < max; i++) {
++ u16 off = i % MXL862XX_MMD_REG_DATA_MAX_SIZE;
++
++ if (i && off == 0) {
++ /* Send command to set data when every
++ * MXL862XX_MMD_REG_DATA_MAX_SIZE of WORDs are written.
++ */
++ ret = mxl862xx_set_data(priv, i);
++ if (ret < 0)
++ goto out;
++ }
++
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_DATA_FIRST + off,
++ le16_to_cpu(data[i]));
++ if (ret < 0)
++ goto out;
++ }
++
++ ret = mxl862xx_send_cmd(priv, cmd, size, quiet);
++ if (ret < 0 || !read)
++ goto out;
++
++ /* store result of mxl862xx_send_cmd() */
++ cmd_ret = ret;
++
++ for (i = 0; i < max; i++) {
++ u16 off = i % MXL862XX_MMD_REG_DATA_MAX_SIZE;
++
++ if (i && off == 0) {
++ /* Send command to fetch next batch of data when every
++ * MXL862XX_MMD_REG_DATA_MAX_SIZE of WORDs are read.
++ */
++ ret = mxl862xx_get_data(priv, i);
++ if (ret < 0)
++ goto out;
++ }
++
++ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_DATA_FIRST + off);
++ if (ret < 0)
++ goto out;
++
++ if ((i * 2 + 1) == size) {
++ /* Special handling for last BYTE if it's not WORD
++ * aligned to avoid writing beyond the allocated data
++ * structure.
++ */
++ *(uint8_t *)&data[i] = ret & 0xff;
++ } else {
++ data[i] = cpu_to_le16((u16)ret);
++ }
++ }
++
++ /* on success return the result of the mxl862xx_send_cmd() */
++ ret = cmd_ret;
++
++ dev_dbg(&priv->mdiodev->dev, "RET %d DATA %*ph\n", ret, size, data);
++
++out:
++ mutex_unlock(&priv->mdiodev->bus->mdio_lock);
++
++ return ret;
++}
++
++int mxl862xx_reset(struct mxl862xx_priv *priv)
++{
++ int ret;
++
++ mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
++
++ /* Software reset */
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET, 0);
++ if (ret)
++ goto out;
++
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL, MXL862XX_SWITCH_RESET);
++out:
++ mutex_unlock(&priv->mdiodev->bus->mdio_lock);
++
++ return ret;
++}
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+@@ -0,0 +1,12 @@
++/* SPDX-License-Identifier: GPL-2.0-or-later */
++
++#ifndef __MXL862XX_HOST_H
++#define __MXL862XX_HOST_H
++
++#include "mxl862xx.h"
++
++int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *data, u16 size,
++ bool read, bool quiet);
++int mxl862xx_reset(struct mxl862xx_priv *priv);
++
++#endif /* __MXL862XX_HOST_H */
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -0,0 +1,476 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * Driver for MaxLinear MxL862xx switch family
++ *
++ * Copyright (C) 2024 MaxLinear Inc.
++ * Copyright (C) 2025 John Crispin <john@phrozen.org>
++ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
++ */
++
++#include <linux/module.h>
++#include <linux/delay.h>
++#include <linux/of_device.h>
++#include <linux/of_mdio.h>
++#include <linux/phy.h>
++#include <linux/phylink.h>
++#include <net/dsa.h>
++
++#include "mxl862xx.h"
++#include "mxl862xx-api.h"
++#include "mxl862xx-cmd.h"
++#include "mxl862xx-host.h"
++
++#define MXL862XX_API_WRITE(dev, cmd, data) \
++ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), false, false)
++#define MXL862XX_API_READ(dev, cmd, data) \
++ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, false)
++#define MXL862XX_API_READ_QUIET(dev, cmd, data) \
++ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
++
++#define MXL862XX_SDMA_PCTRLP(p) (0xbc0 + ((p) * 0x6))
++#define MXL862XX_SDMA_PCTRL_EN BIT(0)
++
++#define MXL862XX_FDMA_PCTRLP(p) (0xa80 + ((p) * 0x6))
++#define MXL862XX_FDMA_PCTRL_EN BIT(0)
++
++#define MXL862XX_READY_TIMEOUT_MS 10000
++#define MXL862XX_READY_POLL_MS 100
++
++static enum dsa_tag_protocol mxl862xx_get_tag_protocol(struct dsa_switch *ds,
++ int port,
++ enum dsa_tag_protocol m)
++{
++ return DSA_TAG_PROTO_MXL862;
++}
++
++/* PHY access via firmware relay */
++static int mxl862xx_phy_read_mmd(struct mxl862xx_priv *priv, int port,
++ int devadd, int reg)
++{
++ struct mdio_relay_data param = {
++ .phy = port,
++ .mmd = devadd,
++ .reg = cpu_to_le16(reg),
++ };
++ int ret;
++
++ ret = MXL862XX_API_READ(priv, INT_GPHY_READ, param);
++ if (ret)
++ return ret;
++
++ return le16_to_cpu(param.data);
++}
++
++static int mxl862xx_phy_write_mmd(struct mxl862xx_priv *priv, int port,
++ int devadd, int reg, u16 data)
++{
++ struct mdio_relay_data param = {
++ .phy = port,
++ .mmd = devadd,
++ .reg = cpu_to_le16(reg),
++ .data = cpu_to_le16(data),
++ };
++
++ return MXL862XX_API_WRITE(priv, INT_GPHY_WRITE, param);
++}
++
++static int mxl862xx_phy_read_mii_bus(struct mii_bus *bus, int port, int regnum)
++{
++ return mxl862xx_phy_read_mmd(bus->priv, port, 0, regnum);
++}
++
++static int mxl862xx_phy_write_mii_bus(struct mii_bus *bus, int port,
++ int regnum, u16 val)
++{
++ return mxl862xx_phy_write_mmd(bus->priv, port, 0, regnum, val);
++}
++
++static int mxl862xx_phy_read_c45_mii_bus(struct mii_bus *bus, int port,
++ int devadd, int regnum)
++{
++ return mxl862xx_phy_read_mmd(bus->priv, port, devadd, regnum);
++}
++
++static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int port,
++ int devadd, int regnum, u16 val)
++{
++ return mxl862xx_phy_write_mmd(bus->priv, port, devadd, regnum, val);
++}
++
++static int mxl862xx_wait_ready(struct dsa_switch *ds)
++{
++ struct mxl862xx_sys_fw_image_version ver = {};
++ unsigned long start = jiffies, timeout;
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_cfg cfg = {};
++ int ret;
++
++ timeout = start + msecs_to_jiffies(MXL862XX_READY_TIMEOUT_MS);
++ msleep(2000); /* it always takes at least 2 seconds */
++ do {
++ ret = MXL862XX_API_READ_QUIET(priv, SYS_MISC_FW_VERSION, ver);
++ if (ret || !ver.iv_major)
++ goto not_ready_yet;
++
++ /* being able to perform CFGGET indicates that
++ * the firmware is ready
++ */
++ ret = MXL862XX_API_READ_QUIET(priv,
++ MXL862XX_COMMON_CFGGET,
++ cfg);
++ if (ret)
++ goto not_ready_yet;
++
++ dev_info(ds->dev, "switch ready after %ums, firmware %u.%u.%u (build %u)\n",
++ jiffies_to_msecs(jiffies - start),
++ ver.iv_major, ver.iv_minor,
++ le16_to_cpu(ver.iv_revision),
++ le32_to_cpu(ver.iv_build_num));
++ return 0;
++
++not_ready_yet:
++ msleep(MXL862XX_READY_POLL_MS);
++ } while (time_before(jiffies, timeout));
++
++ dev_err(ds->dev, "switch not responding after reset\n");
++ return -ETIMEDOUT;
++}
++
++static int mxl862xx_setup_mdio(struct dsa_switch *ds)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct device *dev = ds->dev;
++ struct device_node *mdio_np;
++ struct mii_bus *bus;
++ int ret;
++
++ bus = devm_mdiobus_alloc(dev);
++ if (!bus)
++ return -ENOMEM;
++
++ bus->priv = priv;
++ ds->user_mii_bus = bus;
++ bus->name = KBUILD_MODNAME "-mii";
++ snprintf(bus->id, MII_BUS_ID_SIZE, "%s-mii", dev_name(dev));
++ bus->read_c45 = mxl862xx_phy_read_c45_mii_bus;
++ bus->write_c45 = mxl862xx_phy_write_c45_mii_bus;
++ bus->read = mxl862xx_phy_read_mii_bus;
++ bus->write = mxl862xx_phy_write_mii_bus;
++ bus->parent = dev;
++ bus->phy_mask = ~ds->phys_mii_mask;
++
++ mdio_np = of_get_child_by_name(dev->of_node, "mdio");
++ if (!mdio_np)
++ return -ENODEV;
++
++ ret = devm_of_mdiobus_register(dev, bus, mdio_np);
++ of_node_put(mdio_np);
++
++ return ret;
++}
++
++static int mxl862xx_setup(struct dsa_switch *ds)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ int ret;
++
++ ret = mxl862xx_reset(priv);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_wait_ready(ds);
++ if (ret)
++ return ret;
++
++ return mxl862xx_setup_mdio(ds);
++}
++
++static int mxl862xx_port_state(struct dsa_switch *ds, int port, bool enable)
++{
++ struct mxl862xx_register_mod sdma = {
++ .addr = cpu_to_le16(MXL862XX_SDMA_PCTRLP(port)),
++ .data = cpu_to_le16(enable ? MXL862XX_SDMA_PCTRL_EN : 0),
++ .mask = cpu_to_le16(MXL862XX_SDMA_PCTRL_EN),
++ };
++ struct mxl862xx_register_mod fdma = {
++ .addr = cpu_to_le16(MXL862XX_FDMA_PCTRLP(port)),
++ .data = cpu_to_le16(enable ? MXL862XX_FDMA_PCTRL_EN : 0),
++ .mask = cpu_to_le16(MXL862XX_FDMA_PCTRL_EN),
++ };
++ int ret;
++
++ ret = MXL862XX_API_WRITE(ds->priv, MXL862XX_COMMON_REGISTERMOD, sdma);
++ if (ret)
++ return ret;
++
++ return MXL862XX_API_WRITE(ds->priv, MXL862XX_COMMON_REGISTERMOD, fdma);
++}
++
++static int mxl862xx_port_enable(struct dsa_switch *ds, int port,
++ struct phy_device *phydev)
++{
++ return mxl862xx_port_state(ds, port, true);
++}
++
++static void mxl862xx_port_disable(struct dsa_switch *ds, int port)
++{
++ if (mxl862xx_port_state(ds, port, false))
++ dev_err(ds->dev, "failed to disable port %d\n", port);
++}
++
++static void mxl862xx_port_fast_age(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_mac_table_clear param = {
++ .type = MXL862XX_MAC_CLEAR_PHY_PORT,
++ .port_id = port,
++ };
++
++ if (MXL862XX_API_WRITE(ds->priv, MXL862XX_MAC_TABLECLEARCOND, param))
++ dev_err(ds->dev, "failed to clear fdb on port %d\n", port);
++}
++
++static int mxl862xx_configure_ctp_port(struct dsa_switch *ds, int port,
++ u16 first_ctp_port_id,
++ u16 number_of_ctp_ports)
++{
++ struct mxl862xx_ctp_port_assignment ctp_assign = {
++ .logical_port_id = port,
++ .first_ctp_port_id = cpu_to_le16(first_ctp_port_id),
++ .number_of_ctp_port = cpu_to_le16(number_of_ctp_ports),
++ .mode = cpu_to_le32(MXL862XX_LOGICAL_PORT_ETHERNET),
++ };
++
++ return MXL862XX_API_WRITE(ds->priv, MXL862XX_CTP_PORTASSIGNMENTSET,
++ ctp_assign);
++}
++
++static int mxl862xx_configure_sp_tag_proto(struct dsa_switch *ds, int port,
++ bool enable)
++{
++ struct mxl862xx_ss_sp_tag tag = {
++ .pid = port,
++ .mask = MXL862XX_SS_SP_TAG_MASK_RX | MXL862XX_SS_SP_TAG_MASK_TX,
++ .rx = enable ? MXL862XX_SS_SP_TAG_RX_TAG_NO_INSERT :
++ MXL862XX_SS_SP_TAG_RX_NO_TAG_INSERT,
++ .tx = enable ? MXL862XX_SS_SP_TAG_TX_TAG_NO_REMOVE :
++ MXL862XX_SS_SP_TAG_TX_TAG_REMOVE,
++ };
++
++ return MXL862XX_API_WRITE(ds->priv, MXL862XX_SS_SPTAG_SET, tag);
++}
++
++static int mxl862xx_setup_cpu_bridge(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_bridge_port_config br_port_cfg = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ u16 bridge_port_map = 0;
++ struct dsa_port *dp;
++
++ /* CPU port bridge setup */
++ br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING);
++
++ br_port_cfg.bridge_port_id = cpu_to_le16(port);
++ br_port_cfg.src_mac_learning_disable = false;
++ br_port_cfg.vlan_src_mac_vid_enable = true;
++ br_port_cfg.vlan_dst_mac_vid_enable = true;
++
++ /* include all assigned user ports in the CPU portmap */
++ dsa_switch_for_each_user_port(dp, ds) {
++ /* it's safe to rely on cpu_dp being valid for user ports */
++ if (dp->cpu_dp->index != port)
++ continue;
++
++ bridge_port_map |= BIT(dp->index);
++ }
++ br_port_cfg.bridge_port_map[0] |= cpu_to_le16(bridge_port_map);
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, br_port_cfg);
++}
++
++static int mxl862xx_add_single_port_bridge(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_bridge_port_config br_port_cfg = {};
++ struct dsa_port *dp = dsa_to_port(ds, port);
++ struct mxl862xx_bridge_alloc br_alloc = {};
++ int ret;
++
++ ret = MXL862XX_API_READ(ds->priv, MXL862XX_BRIDGE_ALLOC, br_alloc);
++ if (ret) {
++ dev_err(ds->dev, "failed to allocate a bridge for port %d\n", port);
++ return ret;
++ }
++
++ br_port_cfg.bridge_id = br_alloc.bridge_id;
++ br_port_cfg.bridge_port_id = cpu_to_le16(port);
++ br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING);
++ br_port_cfg.src_mac_learning_disable = true;
++ br_port_cfg.vlan_src_mac_vid_enable = false;
++ br_port_cfg.vlan_dst_mac_vid_enable = false;
++ /* As this function is only called for user ports it is safe to rely on
++ * cpu_dp being valid
++ */
++ br_port_cfg.bridge_port_map[0] = cpu_to_le16(BIT(dp->cpu_dp->index));
++
++ return MXL862XX_API_WRITE(ds->priv, MXL862XX_BRIDGEPORT_CONFIGSET, br_port_cfg);
++}
++
++static int mxl862xx_port_setup(struct dsa_switch *ds, int port)
++{
++ struct dsa_port *dp = dsa_to_port(ds, port);
++ bool is_cpu_port = dsa_port_is_cpu(dp);
++ int ret;
++
++ /* disable port and flush MAC entries */
++ ret = mxl862xx_port_state(ds, port, false);
++ if (ret)
++ return ret;
++
++ mxl862xx_port_fast_age(ds, port);
++
++ /* skip setup for unused and DSA ports */
++ if (dsa_port_is_unused(dp) ||
++ dsa_port_is_dsa(dp))
++ return 0;
++
++ /* configure tag protocol */
++ ret = mxl862xx_configure_sp_tag_proto(ds, port, is_cpu_port);
++ if (ret)
++ return ret;
++
++ /* assign CTP port IDs */
++ ret = mxl862xx_configure_ctp_port(ds, port, port,
++ is_cpu_port ? 32 - port : 1);
++ if (ret)
++ return ret;
++
++ if (is_cpu_port)
++ /* assign user ports to CPU port bridge */
++ return mxl862xx_setup_cpu_bridge(ds, port);
++
++ /* setup single-port bridge for user ports */
++ return mxl862xx_add_single_port_bridge(ds, port);
++}
++
++static void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
++ struct phylink_config *config)
++{
++ config->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
++ MAC_100 | MAC_1000 | MAC_2500FD;
++
++ __set_bit(PHY_INTERFACE_MODE_INTERNAL,
++ config->supported_interfaces);
++}
++
++static const struct dsa_switch_ops mxl862xx_switch_ops = {
++ .get_tag_protocol = mxl862xx_get_tag_protocol,
++ .setup = mxl862xx_setup,
++ .port_setup = mxl862xx_port_setup,
++ .phylink_get_caps = mxl862xx_phylink_get_caps,
++ .port_enable = mxl862xx_port_enable,
++ .port_disable = mxl862xx_port_disable,
++ .port_fast_age = mxl862xx_port_fast_age,
++};
++
++static void mxl862xx_phylink_mac_config(struct phylink_config *config,
++ unsigned int mode,
++ const struct phylink_link_state *state)
++{
++}
++
++static void mxl862xx_phylink_mac_link_down(struct phylink_config *config,
++ unsigned int mode,
++ phy_interface_t interface)
++{
++}
++
++static void mxl862xx_phylink_mac_link_up(struct phylink_config *config,
++ struct phy_device *phydev,
++ unsigned int mode,
++ phy_interface_t interface,
++ int speed, int duplex,
++ bool tx_pause, bool rx_pause)
++{
++}
++
++static const struct phylink_mac_ops mxl862xx_phylink_mac_ops = {
++ .mac_config = mxl862xx_phylink_mac_config,
++ .mac_link_down = mxl862xx_phylink_mac_link_down,
++ .mac_link_up = mxl862xx_phylink_mac_link_up,
++};
++
++static int mxl862xx_probe(struct mdio_device *mdiodev)
++{
++ struct device *dev = &mdiodev->dev;
++ struct mxl862xx_priv *priv;
++ struct dsa_switch *ds;
++
++ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
++ if (!priv)
++ return -ENOMEM;
++
++ priv->mdiodev = mdiodev;
++
++ ds = devm_kzalloc(dev, sizeof(*ds), GFP_KERNEL);
++ if (!ds)
++ return -ENOMEM;
++
++ priv->ds = ds;
++ ds->dev = dev;
++ ds->priv = priv;
++ ds->ops = &mxl862xx_switch_ops;
++ ds->phylink_mac_ops = &mxl862xx_phylink_mac_ops;
++ ds->num_ports = MXL862XX_MAX_PORTS;
++
++ dev_set_drvdata(dev, ds);
++
++ return dsa_register_switch(ds);
++}
++
++static void mxl862xx_remove(struct mdio_device *mdiodev)
++{
++ struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
++
++ if (!ds)
++ return;
++
++ dsa_unregister_switch(ds);
++}
++
++static void mxl862xx_shutdown(struct mdio_device *mdiodev)
++{
++ struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
++
++ if (!ds)
++ return;
++
++ dsa_switch_shutdown(ds);
++
++ dev_set_drvdata(&mdiodev->dev, NULL);
++}
++
++static const struct of_device_id mxl862xx_of_match[] = {
++ { .compatible = "maxlinear,mxl86282" },
++ { .compatible = "maxlinear,mxl86252" },
++ { /* sentinel */ }
++};
++MODULE_DEVICE_TABLE(of, mxl862xx_of_match);
++
++static struct mdio_driver mxl862xx_driver = {
++ .probe = mxl862xx_probe,
++ .remove = mxl862xx_remove,
++ .shutdown = mxl862xx_shutdown,
++ .mdiodrv.driver = {
++ .name = "mxl862xx",
++ .of_match_table = mxl862xx_of_match,
++ },
++};
++
++mdio_module_driver(mxl862xx_driver);
++
++MODULE_DESCRIPTION("Driver for MaxLinear MxL862xx switch family");
++MODULE_LICENSE("GPL");
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -0,0 +1,16 @@
++/* SPDX-License-Identifier: GPL-2.0-or-later */
++
++#ifndef __MXL862XX_H
++#define __MXL862XX_H
++
++#include <linux/mdio.h>
++#include <net/dsa.h>
++
++#define MXL862XX_MAX_PORTS 17
++
++struct mxl862xx_priv {
++ struct dsa_switch *ds;
++ struct mdio_device *mdiodev;
++};
++
++#endif /* __MXL862XX_H */
--- /dev/null
+From b5f8b39d22ab93cada5c88dc2cb6495b95f44c70 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 3 Mar 2026 03:17:43 +0000
+Subject: [PATCH 04/35] net: dsa: mxl862xx: rename MDIO op arguments
+
+The use of the 'port' argument name for functions implementing the MDIO
+bus operations is misleading as the port address isn't equal to the
+PHY address.
+
+Rename the MDIO operation argument name to match the prototypes of
+mdiobus_write, mdiobus_read, mdiobus_c45_read and mdiobus_c45_write.
+
+Suggested-by: Vladimir Oltean <olteanv@gmail.com>
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Link: https://patch.msgid.link/e1f4cb3bcffc7df9af0f2c9b673b14c7e1201c9a.1772507674.git.daniel@makrotopia.org
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 32 ++++++++++++++---------------
+ 1 file changed, 16 insertions(+), 16 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -44,13 +44,13 @@ static enum dsa_tag_protocol mxl862xx_ge
+ }
+
+ /* PHY access via firmware relay */
+-static int mxl862xx_phy_read_mmd(struct mxl862xx_priv *priv, int port,
+- int devadd, int reg)
++static int mxl862xx_phy_read_mmd(struct mxl862xx_priv *priv, int addr,
++ int devadd, int regnum)
+ {
+ struct mdio_relay_data param = {
+- .phy = port,
++ .phy = addr,
+ .mmd = devadd,
+- .reg = cpu_to_le16(reg),
++ .reg = cpu_to_le16(regnum),
+ };
+ int ret;
+
+@@ -61,40 +61,40 @@ static int mxl862xx_phy_read_mmd(struct
+ return le16_to_cpu(param.data);
+ }
+
+-static int mxl862xx_phy_write_mmd(struct mxl862xx_priv *priv, int port,
+- int devadd, int reg, u16 data)
++static int mxl862xx_phy_write_mmd(struct mxl862xx_priv *priv, int addr,
++ int devadd, int regnum, u16 data)
+ {
+ struct mdio_relay_data param = {
+- .phy = port,
++ .phy = addr,
+ .mmd = devadd,
+- .reg = cpu_to_le16(reg),
++ .reg = cpu_to_le16(regnum),
+ .data = cpu_to_le16(data),
+ };
+
+ return MXL862XX_API_WRITE(priv, INT_GPHY_WRITE, param);
+ }
+
+-static int mxl862xx_phy_read_mii_bus(struct mii_bus *bus, int port, int regnum)
++static int mxl862xx_phy_read_mii_bus(struct mii_bus *bus, int addr, int regnum)
+ {
+- return mxl862xx_phy_read_mmd(bus->priv, port, 0, regnum);
++ return mxl862xx_phy_read_mmd(bus->priv, addr, 0, regnum);
+ }
+
+-static int mxl862xx_phy_write_mii_bus(struct mii_bus *bus, int port,
++static int mxl862xx_phy_write_mii_bus(struct mii_bus *bus, int addr,
+ int regnum, u16 val)
+ {
+- return mxl862xx_phy_write_mmd(bus->priv, port, 0, regnum, val);
++ return mxl862xx_phy_write_mmd(bus->priv, addr, 0, regnum, val);
+ }
+
+-static int mxl862xx_phy_read_c45_mii_bus(struct mii_bus *bus, int port,
++static int mxl862xx_phy_read_c45_mii_bus(struct mii_bus *bus, int addr,
+ int devadd, int regnum)
+ {
+- return mxl862xx_phy_read_mmd(bus->priv, port, devadd, regnum);
++ return mxl862xx_phy_read_mmd(bus->priv, addr, devadd, regnum);
+ }
+
+-static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int port,
++static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int addr,
+ int devadd, int regnum, u16 val)
+ {
+- return mxl862xx_phy_write_mmd(bus->priv, port, devadd, regnum, val);
++ return mxl862xx_phy_write_mmd(bus->priv, addr, devadd, regnum, val);
+ }
+
+ static int mxl862xx_wait_ready(struct dsa_switch *ds)
--- /dev/null
+From d0341efa8f5182cafe16506b9bef98184f4951fe Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 10 Mar 2026 00:41:56 +0000
+Subject: [PATCH 05/35] net: dsa: mxl862xx: don't set user_mii_bus
+
+The PHY addresses in the MII bus are not equal to the port addresses,
+so the bus cannot be assigned as user_mii_bus. Falling back on the
+user_mii_bus in case a PHY isn't declared in device tree will result in
+using the wrong (in this case: off-by-+1) PHY.
+Remove the wrong assignment.
+
+Fixes: 23794bec1cb60 ("net: dsa: add basic initial driver for MxL862xx switches")
+Suggested-by: Vladimir Oltean <olteanv@gmail.com>
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
+Link: https://patch.msgid.link/0f0df310fd8cab57e0e5e3d0831dd057fd05bcd5.1773103271.git.daniel@makrotopia.org
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 1 -
+ 1 file changed, 1 deletion(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -149,7 +149,6 @@ static int mxl862xx_setup_mdio(struct ds
+ return -ENOMEM;
+
+ bus->priv = priv;
+- ds->user_mii_bus = bus;
+ bus->name = KBUILD_MODNAME "-mii";
+ snprintf(bus->id, MII_BUS_ID_SIZE, "%s-mii", dev_name(dev));
+ bus->read_c45 = mxl862xx_phy_read_c45_mii_bus;
--- /dev/null
+From c0402837642625ef13ade862e20e229f4a5810f5 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Wed, 18 Mar 2026 03:07:52 +0000
+Subject: [PATCH 06/35] net: dsa: mxl862xx: don't read out-of-bounds
+
+The write loop in mxl862xx_api_wrap() computes the word count as
+(size + 1) / 2, rounding up for odd-sized structs.
+
+On the last iteration of an odd-sized buffer it reads a full __le16
+from data[i], accessing one byte past the end of the caller's struct.
+KASAN catches this as a stack-out-of-bounds read during probe (e.g.
+from mxl862xx_bridge_config_fwd() because of the odd length of
+sizeof(struct mxl862xx_bridge_config) == 49).
+
+The read-back loop already handles this case, it writes only a single
+byte when (i * 2 + 1) == size. The write loop lacked the same guard.
+
+In practice the over-read is harmless: the extra stack byte is sent to
+the firmware which ignores trailing data beyond the command's declared
+payload size.
+
+Apply the same odd-size last-byte handling to the write path: when the
+final word contains only one valid byte, send *(u8 *)&data[i] instead
+of le16_to_cpu(data[i]). This is endian-safe because data is
+__le16-encoded and the low byte is always at the lowest address
+regardless of host byte order.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Reviewed-by: Simon Horman <horms@kernel.org>
+Link: https://patch.msgid.link/83356ad9c9a4470dd49b6b3d661c2a8dd85cc6a1.1773803190.git.daniel@makrotopia.org
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-host.c | 10 ++++++++--
+ 1 file changed, 8 insertions(+), 2 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+@@ -175,8 +175,14 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ goto out;
+ }
+
+- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_DATA_FIRST + off,
+- le16_to_cpu(data[i]));
++ if ((i * 2 + 1) == size)
++ ret = mxl862xx_reg_write(priv,
++ MXL862XX_MMD_REG_DATA_FIRST + off,
++ *(u8 *)&data[i]);
++ else
++ ret = mxl862xx_reg_write(priv,
++ MXL862XX_MMD_REG_DATA_FIRST + off,
++ le16_to_cpu(data[i]));
+ if (ret < 0)
+ goto out;
+ }
--- /dev/null
+From 06cdf1bf5ba80e90bc54e7fe0c096b47d5ab3d8d Mon Sep 17 00:00:00 2001
+From: Arnd Bergmann <arnd@arndb.de>
+Date: Mon, 16 Feb 2026 11:55:17 +0100
+Subject: [PATCH 07/35] net: dsa: MxL862xx: don't force-enable MAXLINEAR_GPHY
+
+The newly added dsa driver attempts to enable the corresponding PHY driver,
+but that one has additional dependencies that may not be available:
+
+WARNING: unmet direct dependencies detected for MAXLINEAR_GPHY
+ Depends on [m]: NETDEVICES [=y] && PHYLIB [=y] && (HWMON [=m] || HWMON [=m]=n [=n])
+ Selected by [y]:
+ - NET_DSA_MXL862 [=y] && NETDEVICES [=y] && NET_DSA [=y]
+aarch64-linux-ld: drivers/net/phy/mxl-gpy.o: in function `gpy_probe':
+mxl-gpy.c:(.text.gpy_probe+0x13c): undefined reference to `devm_hwmon_device_register_with_info'
+aarch64-linux-ld: drivers/net/phy/mxl-gpy.o: in function `gpy_hwmon_read':
+mxl-gpy.c:(.text.gpy_hwmon_read+0x48): undefined reference to `polynomial_calc'
+
+There is actually no compile-time dependency, as DSA correctly uses the
+PHY abstractions. Remove the 'select' statement to reduce the complexity.
+
+Fixes: 23794bec1cb6 ("net: dsa: add basic initial driver for MxL862xx switches")
+Signed-off-by: Arnd Bergmann <arnd@arndb.de>
+Reviewed-by: Daniel Golle <daniel@makrotopia.org>
+Link: https://patch.msgid.link/20260216105522.2382373-1-arnd@kernel.org
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/mxl862xx/Kconfig | 1 -
+ 1 file changed, 1 deletion(-)
+
+--- a/drivers/net/dsa/mxl862xx/Kconfig
++++ b/drivers/net/dsa/mxl862xx/Kconfig
+@@ -2,7 +2,6 @@
+ config NET_DSA_MXL862
+ tristate "MaxLinear MxL862xx"
+ depends on NET_DSA
+- select MAXLINEAR_GPHY
+ select NET_DSA_TAG_MXL_862XX
+ help
+ This enables support for the MaxLinear MxL862xx switch family.
--- /dev/null
+From d48001906168be3088f9cd7aa8d1ad8dbc53e4f4 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sun, 22 Mar 2026 13:27:20 +0000
+Subject: [PATCH 08/35] net: dsa: mxl862xx: add CRC for MDIO communication
+
+Enable the firmware's opt-in CRC validation on the MDIO/MMD command
+interface to detect bit errors on the bus. The firmware bundles CRC-6
+and CRC-16 under a single enable flag, so both are implemented
+together.
+
+CRC-6 protects the ctrl and len_ret command registers using a table-
+driven 3GPP algorithm. It is applied to every command exchange
+including SET_DATA/GET_DATA batch transfers. With CRC enabled, the
+firmware encodes its return value as a signed 11-bit integer within
+the CRC- protected register fields, replacing the previous 16-bit
+interpretation.
+
+CRC-16 protects the data payload using the kernel's crc16() library.
+The driver appends a CRC-16 checksum to outgoing data and verifies the
+firmware-appended checksum on responses. The checksum is placed at the
+exact byte offset where the struct data ends, correctly handling
+packed structs with odd sizes by splitting the checksum across word
+boundaries. SET_DATA/GET_DATA sub-commands carry only CRC-6.
+
+Upon detection of a CRC error on either side all conduit interfaces
+are taken down, triggering all user ports to go down as well. This is
+the most feasible option: CRC errors are likely caused either by
+broken hardware, or are symptom of overheating. In either case, trying
+to resume normal operation isn't reasonable.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Link: https://patch.msgid.link/620453b9a150bbe5b7ea4224331cb5dc5e57263b.1774185953.git.daniel@makrotopia.org
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/mxl862xx/Kconfig | 1 +
+ drivers/net/dsa/mxl862xx/mxl862xx-host.c | 345 ++++++++++++++++++-----
+ drivers/net/dsa/mxl862xx/mxl862xx-host.h | 2 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 11 +
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 2 +
+ 5 files changed, 296 insertions(+), 65 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/Kconfig
++++ b/drivers/net/dsa/mxl862xx/Kconfig
+@@ -2,6 +2,7 @@
+ config NET_DSA_MXL862
+ tristate "MaxLinear MxL862xx"
+ depends on NET_DSA
++ select CRC16
+ select NET_DSA_TAG_MXL_862XX
+ help
+ This enables support for the MaxLinear MxL862xx switch family.
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+@@ -7,7 +7,9 @@
+ * Copyright (C) 2024 MaxLinear Inc.
+ */
+
++#include <linux/bitops.h>
+ #include <linux/bits.h>
++#include <linux/crc16.h>
+ #include <linux/iopoll.h>
+ #include <linux/limits.h>
+ #include <net/dsa.h>
+@@ -15,6 +17,9 @@
+ #include "mxl862xx-host.h"
+
+ #define CTRL_BUSY_MASK BIT(15)
++#define CTRL_CRC_FLAG BIT(14)
++
++#define LEN_RET_LEN_MASK GENMASK(9, 0)
+
+ #define MXL862XX_MMD_REG_CTRL 0
+ #define MXL862XX_MMD_REG_LEN_RET 1
+@@ -27,7 +32,159 @@
+ #define MMD_API_GET_DATA_0 5
+ #define MMD_API_RST_DATA 8
+
+-#define MXL862XX_SWITCH_RESET 0x9907
++#define MXL862XX_SWITCH_RESET 0x9907
++
++static void mxl862xx_crc_err_work_fn(struct work_struct *work)
++{
++ struct mxl862xx_priv *priv = container_of(work, struct mxl862xx_priv,
++ crc_err_work);
++ struct dsa_port *dp;
++
++ dev_warn(&priv->mdiodev->dev,
++ "MDIO CRC error detected, shutting down all ports\n");
++
++ rtnl_lock();
++ dsa_switch_for_each_cpu_port(dp, priv->ds)
++ dev_close(dp->conduit);
++ rtnl_unlock();
++
++ clear_bit(0, &priv->crc_err);
++}
++
++/* Firmware CRC error codes (outside normal Zephyr errno range). */
++#define MXL862XX_FW_CRC6_ERR (-1024)
++#define MXL862XX_FW_CRC16_ERR (-1023)
++
++/* 3GPP CRC-6 lookup table (polynomial 0x6F).
++ * Matches the firmware's default CRC-6 implementation.
++ */
++static const u8 mxl862xx_crc6_table[256] = {
++ 0x00, 0x2f, 0x31, 0x1e, 0x0d, 0x22, 0x3c, 0x13,
++ 0x1a, 0x35, 0x2b, 0x04, 0x17, 0x38, 0x26, 0x09,
++ 0x34, 0x1b, 0x05, 0x2a, 0x39, 0x16, 0x08, 0x27,
++ 0x2e, 0x01, 0x1f, 0x30, 0x23, 0x0c, 0x12, 0x3d,
++ 0x07, 0x28, 0x36, 0x19, 0x0a, 0x25, 0x3b, 0x14,
++ 0x1d, 0x32, 0x2c, 0x03, 0x10, 0x3f, 0x21, 0x0e,
++ 0x33, 0x1c, 0x02, 0x2d, 0x3e, 0x11, 0x0f, 0x20,
++ 0x29, 0x06, 0x18, 0x37, 0x24, 0x0b, 0x15, 0x3a,
++ 0x0e, 0x21, 0x3f, 0x10, 0x03, 0x2c, 0x32, 0x1d,
++ 0x14, 0x3b, 0x25, 0x0a, 0x19, 0x36, 0x28, 0x07,
++ 0x3a, 0x15, 0x0b, 0x24, 0x37, 0x18, 0x06, 0x29,
++ 0x20, 0x0f, 0x11, 0x3e, 0x2d, 0x02, 0x1c, 0x33,
++ 0x09, 0x26, 0x38, 0x17, 0x04, 0x2b, 0x35, 0x1a,
++ 0x13, 0x3c, 0x22, 0x0d, 0x1e, 0x31, 0x2f, 0x00,
++ 0x3d, 0x12, 0x0c, 0x23, 0x30, 0x1f, 0x01, 0x2e,
++ 0x27, 0x08, 0x16, 0x39, 0x2a, 0x05, 0x1b, 0x34,
++ 0x1c, 0x33, 0x2d, 0x02, 0x11, 0x3e, 0x20, 0x0f,
++ 0x06, 0x29, 0x37, 0x18, 0x0b, 0x24, 0x3a, 0x15,
++ 0x28, 0x07, 0x19, 0x36, 0x25, 0x0a, 0x14, 0x3b,
++ 0x32, 0x1d, 0x03, 0x2c, 0x3f, 0x10, 0x0e, 0x21,
++ 0x1b, 0x34, 0x2a, 0x05, 0x16, 0x39, 0x27, 0x08,
++ 0x01, 0x2e, 0x30, 0x1f, 0x0c, 0x23, 0x3d, 0x12,
++ 0x2f, 0x00, 0x1e, 0x31, 0x22, 0x0d, 0x13, 0x3c,
++ 0x35, 0x1a, 0x04, 0x2b, 0x38, 0x17, 0x09, 0x26,
++ 0x12, 0x3d, 0x23, 0x0c, 0x1f, 0x30, 0x2e, 0x01,
++ 0x08, 0x27, 0x39, 0x16, 0x05, 0x2a, 0x34, 0x1b,
++ 0x26, 0x09, 0x17, 0x38, 0x2b, 0x04, 0x1a, 0x35,
++ 0x3c, 0x13, 0x0d, 0x22, 0x31, 0x1e, 0x00, 0x2f,
++ 0x15, 0x3a, 0x24, 0x0b, 0x18, 0x37, 0x29, 0x06,
++ 0x0f, 0x20, 0x3e, 0x11, 0x02, 0x2d, 0x33, 0x1c,
++ 0x21, 0x0e, 0x10, 0x3f, 0x2c, 0x03, 0x1d, 0x32,
++ 0x3b, 0x14, 0x0a, 0x25, 0x36, 0x19, 0x07, 0x28,
++};
++
++/* Compute 3GPP CRC-6 over the ctrl register (16 bits) and the lower
++ * 10 bits of the len_ret register. The 26-bit input is packed as
++ * { len_ret[9:0], ctrl[15:0] } and processed LSB-first through the
++ * lookup table.
++ */
++static u8 mxl862xx_crc6(u16 ctrl, u16 len_ret)
++{
++ u32 data = ((u32)(len_ret & LEN_RET_LEN_MASK) << 16) | ctrl;
++ u8 crc = 0;
++ int i;
++
++ for (i = 0; i < sizeof(data); i++, data >>= 8)
++ crc = mxl862xx_crc6_table[(crc << 2) ^ (data & 0xff)] & 0x3f;
++
++ return crc;
++}
++
++/* Encode CRC-6 into the ctrl and len_ret registers before writing them
++ * to MDIO. The caller must set ctrl = API_ID | CTRL_BUSY_MASK |
++ * CTRL_CRC_FLAG, and len_ret = parameter length (bits 0-9 only).
++ *
++ * After encoding:
++ * ctrl[12:0] = API ID (unchanged)
++ * ctrl[14:13] = CRC-6 bits 5-4
++ * ctrl[15] = busy flag (unchanged)
++ * len_ret[9:0] = parameter length (unchanged)
++ * len_ret[13:10] = CRC-6 bits 3-0
++ * len_ret[14] = original ctrl[14] (CRC check flag, forwarded to FW)
++ * len_ret[15] = original ctrl[13] (magic bit, always 1)
++ */
++static void mxl862xx_crc6_encode(u16 *pctrl, u16 *plen_ret)
++{
++ u16 crc, ctrl, len_ret;
++
++ /* Set magic bit before CRC computation */
++ *pctrl |= BIT(13);
++
++ crc = mxl862xx_crc6(*pctrl, *plen_ret);
++
++ /* Place CRC MSB (bits 5-4) into ctrl bits 13-14 */
++ ctrl = (*pctrl & ~GENMASK(14, 13));
++ ctrl |= (crc & 0x30) << 9;
++
++ /* Place CRC LSB (bits 3-0) into len_ret bits 10-13 */
++ len_ret = *plen_ret | ((crc & 0x0f) << 10);
++
++ /* Forward ctrl[14] (CRC check flag) to len_ret[14],
++ * and ctrl[13] (magic, always 1) to len_ret[15].
++ */
++ len_ret |= (*pctrl & BIT(14)) | ((*pctrl & BIT(13)) << 2);
++
++ *pctrl = ctrl;
++ *plen_ret = len_ret;
++}
++
++/* Verify CRC-6 on a firmware response and extract the return value.
++ *
++ * The firmware encodes the return value as a signed 11-bit integer:
++ * - Sign bit (bit 10) in ctrl[14]
++ * - Magnitude (bits 9-0) in len_ret[9:0]
++ * These are recoverable after CRC-6 verification by restoring the
++ * original ctrl from the auxiliary copies in len_ret[15:14].
++ *
++ * Return: 0 on CRC match (with *result set), or -EIO on mismatch.
++ */
++static int mxl862xx_crc6_verify(u16 ctrl, u16 len_ret, int *result)
++{
++ u16 crc_recv, crc_calc;
++
++ /* Extract the received CRC-6 */
++ crc_recv = ((ctrl >> 9) & 0x30) | ((len_ret >> 10) & 0x0f);
++
++ /* Reconstruct the original ctrl for re-computation:
++ * ctrl[14] = len_ret[14] (sign bit / CRC check flag)
++ * ctrl[13] = len_ret[15] >> 2 (magic bit)
++ */
++ ctrl &= ~GENMASK(14, 13);
++ ctrl |= len_ret & BIT(14);
++ ctrl |= (len_ret & BIT(15)) >> 2;
++
++ crc_calc = mxl862xx_crc6(ctrl, len_ret);
++ if (crc_recv != crc_calc)
++ return -EIO;
++
++ /* Extract signed 11-bit return value:
++ * bit 10 (sign) from ctrl[14], bits 9-0 from len_ret[9:0]
++ */
++ *result = sign_extend32((len_ret & LEN_RET_LEN_MASK) |
++ ((ctrl & CTRL_CRC_FLAG) >> 4), 10);
++
++ return 0;
++}
+
+ static int mxl862xx_reg_read(struct mxl862xx_priv *priv, u32 addr)
+ {
+@@ -52,60 +209,78 @@ static int mxl862xx_busy_wait(struct mxl
+ !(val & CTRL_BUSY_MASK), 15, 500000);
+ }
+
+-static int mxl862xx_set_data(struct mxl862xx_priv *priv, u16 words)
++/* Issue a firmware command with CRC-6 protection on the ctrl and len_ret
++ * registers, wait for completion, and verify the response CRC-6.
++ *
++ * Return: firmware result value (>= 0) on success, or negative errno.
++ */
++static int mxl862xx_issue_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 len)
+ {
+- int ret;
+- u16 cmd;
++ u16 ctrl_enc, len_enc;
++ int ret, fw_result;
++
++ ctrl_enc = cmd | CTRL_BUSY_MASK | CTRL_CRC_FLAG;
++ len_enc = len;
++ mxl862xx_crc6_encode(&ctrl_enc, &len_enc);
+
+- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET,
+- MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET, len_enc);
++ if (ret < 0)
++ return ret;
++
++ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL, ctrl_enc);
++ if (ret < 0)
++ return ret;
++
++ ret = mxl862xx_busy_wait(priv);
++ if (ret < 0)
++ return ret;
++
++ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_CTRL);
++ if (ret < 0)
++ return ret;
++ ctrl_enc = ret;
++
++ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_LEN_RET);
+ if (ret < 0)
+ return ret;
++ len_enc = ret;
++
++ ret = mxl862xx_crc6_verify(ctrl_enc, len_enc, &fw_result);
++ if (ret) {
++ if (!test_and_set_bit(0, &priv->crc_err))
++ schedule_work(&priv->crc_err_work);
++ return -EIO;
++ }
++
++ return fw_result;
++}
++
++static int mxl862xx_set_data(struct mxl862xx_priv *priv, u16 words)
++{
++ u16 cmd;
+
+ cmd = words / MXL862XX_MMD_REG_DATA_MAX_SIZE - 1;
+ if (!(cmd < 2))
+ return -EINVAL;
+
+ cmd += MMD_API_SET_DATA_0;
+- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
+- cmd | CTRL_BUSY_MASK);
+- if (ret < 0)
+- return ret;
+
+- return mxl862xx_busy_wait(priv);
++ return mxl862xx_issue_cmd(priv, cmd,
++ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
+ }
+
+ static int mxl862xx_get_data(struct mxl862xx_priv *priv, u16 words)
+ {
+- int ret;
+ u16 cmd;
+
+- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET,
+- MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
+- if (ret < 0)
+- return ret;
+-
+ cmd = words / MXL862XX_MMD_REG_DATA_MAX_SIZE;
+ if (!(cmd > 0 && cmd < 3))
+ return -EINVAL;
+
+ cmd += MMD_API_GET_DATA_0;
+- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
+- cmd | CTRL_BUSY_MASK);
+- if (ret < 0)
+- return ret;
+-
+- return mxl862xx_busy_wait(priv);
+-}
+-
+-static int mxl862xx_firmware_return(int ret)
+-{
+- /* Only 16-bit values are valid. */
+- if (WARN_ON(ret & GENMASK(31, 16)))
+- return -EINVAL;
+
+- /* Interpret value as signed 16-bit integer. */
+- return (s16)ret;
++ return mxl862xx_issue_cmd(priv, cmd,
++ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
+ }
+
+ static int mxl862xx_send_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 size,
+@@ -113,30 +288,23 @@ static int mxl862xx_send_cmd(struct mxl8
+ {
+ int ret;
+
+- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET, size);
+- if (ret)
+- return ret;
++ ret = mxl862xx_issue_cmd(priv, cmd, size);
+
+- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
+- cmd | CTRL_BUSY_MASK);
+- if (ret)
+- return ret;
+-
+- ret = mxl862xx_busy_wait(priv);
+- if (ret)
+- return ret;
+-
+- ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_LEN_RET);
+- if (ret < 0)
+- return ret;
+-
+- /* handle errors returned by the firmware as -EIO
++ /* Handle errors returned by the firmware as -EIO.
+ * The firmware is based on Zephyr OS and uses the errors as
+ * defined in errno.h of Zephyr OS. See
+ * https://github.com/zephyrproject-rtos/zephyr/blob/v3.7.0/lib/libc/minimal/include/errno.h
++ *
++ * The firmware signals CRC validation failures with dedicated
++ * error codes outside the normal Zephyr errno range:
++ * -1024: CRC-6 mismatch on ctrl/len_ret registers
++ * -1023: CRC-16 mismatch on data payload
+ */
+- ret = mxl862xx_firmware_return(ret);
+ if (ret < 0) {
++ if ((ret == MXL862XX_FW_CRC6_ERR ||
++ ret == MXL862XX_FW_CRC16_ERR) &&
++ !test_and_set_bit(0, &priv->crc_err))
++ schedule_work(&priv->crc_err_work);
+ if (!quiet)
+ dev_err(&priv->mdiodev->dev,
+ "CMD %04x returned error %d\n", cmd, ret);
+@@ -151,7 +319,7 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ {
+ __le16 *data = _data;
+ int ret, cmd_ret;
+- u16 max, i;
++ u16 max, crc, i;
+
+ dev_dbg(&priv->mdiodev->dev, "CMD %04x DATA %*ph\n", cmd, size, data);
+
+@@ -163,26 +331,45 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ if (ret < 0)
+ goto out;
+
+- for (i = 0; i < max; i++) {
++ /* Compute CRC-16 over the data payload; written as an extra word
++ * after the data so the firmware can verify the transfer.
++ */
++ crc = crc16(0xffff, (const u8 *)data, size);
++
++ for (i = 0; i < max + 1; i++) {
+ u16 off = i % MXL862XX_MMD_REG_DATA_MAX_SIZE;
++ u16 val;
+
+ if (i && off == 0) {
+ /* Send command to set data when every
+ * MXL862XX_MMD_REG_DATA_MAX_SIZE of WORDs are written.
+- */
++ */
+ ret = mxl862xx_set_data(priv, i);
+ if (ret < 0)
+ goto out;
+ }
+
+- if ((i * 2 + 1) == size)
+- ret = mxl862xx_reg_write(priv,
+- MXL862XX_MMD_REG_DATA_FIRST + off,
+- *(u8 *)&data[i]);
+- else
+- ret = mxl862xx_reg_write(priv,
+- MXL862XX_MMD_REG_DATA_FIRST + off,
+- le16_to_cpu(data[i]));
++ if (i == max) {
++ /* Even size: full CRC word.
++ * Odd size: only CRC high byte remains (low byte
++ * was packed into the previous word).
++ */
++ val = (size & 1) ? crc >> 8 : crc;
++ } else if ((i * 2 + 1) == size) {
++ /* Special handling for last BYTE if it's not WORD
++ * aligned to avoid reading beyond the allocated data
++ * structure. Pack the CRC low byte into the high
++ * byte of this word so it sits at byte offset 'size'
++ * in the firmware's contiguous buffer.
++ */
++ val = *(u8 *)&data[i] | ((crc & 0xff) << 8);
++ } else {
++ val = le16_to_cpu(data[i]);
++ }
++
++ ret = mxl862xx_reg_write(priv,
++ MXL862XX_MMD_REG_DATA_FIRST + off,
++ val);
+ if (ret < 0)
+ goto out;
+ }
+@@ -194,13 +381,13 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ /* store result of mxl862xx_send_cmd() */
+ cmd_ret = ret;
+
+- for (i = 0; i < max; i++) {
++ for (i = 0; i < max + 1; i++) {
+ u16 off = i % MXL862XX_MMD_REG_DATA_MAX_SIZE;
+
+ if (i && off == 0) {
+ /* Send command to fetch next batch of data when every
+ * MXL862XX_MMD_REG_DATA_MAX_SIZE of WORDs are read.
+- */
++ */
+ ret = mxl862xx_get_data(priv, i);
+ if (ret < 0)
+ goto out;
+@@ -210,17 +397,35 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ if (ret < 0)
+ goto out;
+
+- if ((i * 2 + 1) == size) {
++ if (i == max) {
++ /* Even size: full CRC word.
++ * Odd size: only CRC high byte remains (low byte
++ * was in the previous word).
++ */
++ if (size & 1)
++ crc = (crc & 0x00ff) |
++ (((u16)ret & 0xff) << 8);
++ else
++ crc = (u16)ret;
++ } else if ((i * 2 + 1) == size) {
+ /* Special handling for last BYTE if it's not WORD
+ * aligned to avoid writing beyond the allocated data
+- * structure.
++ * structure. The high byte carries the CRC low byte.
+ */
+ *(uint8_t *)&data[i] = ret & 0xff;
++ crc = (ret >> 8) & 0xff;
+ } else {
+ data[i] = cpu_to_le16((u16)ret);
+ }
+ }
+
++ if (crc16(0xffff, (const u8 *)data, size) != crc) {
++ if (!test_and_set_bit(0, &priv->crc_err))
++ schedule_work(&priv->crc_err_work);
++ ret = -EIO;
++ goto out;
++ }
++
+ /* on success return the result of the mxl862xx_send_cmd() */
+ ret = cmd_ret;
+
+@@ -249,3 +454,13 @@ out:
+
+ return ret;
+ }
++
++void mxl862xx_host_init(struct mxl862xx_priv *priv)
++{
++ INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn);
++}
++
++void mxl862xx_host_shutdown(struct mxl862xx_priv *priv)
++{
++ cancel_work_sync(&priv->crc_err_work);
++}
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+@@ -5,6 +5,8 @@
+
+ #include "mxl862xx.h"
+
++void mxl862xx_host_init(struct mxl862xx_priv *priv);
++void mxl862xx_host_shutdown(struct mxl862xx_priv *priv);
+ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *data, u16 size,
+ bool read, bool quiet);
+ int mxl862xx_reset(struct mxl862xx_priv *priv);
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -424,6 +424,7 @@ static int mxl862xx_probe(struct mdio_de
+ ds->ops = &mxl862xx_switch_ops;
+ ds->phylink_mac_ops = &mxl862xx_phylink_mac_ops;
+ ds->num_ports = MXL862XX_MAX_PORTS;
++ mxl862xx_host_init(priv);
+
+ dev_set_drvdata(dev, ds);
+
+@@ -433,22 +434,32 @@ static int mxl862xx_probe(struct mdio_de
+ static void mxl862xx_remove(struct mdio_device *mdiodev)
+ {
+ struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
++ struct mxl862xx_priv *priv;
+
+ if (!ds)
+ return;
+
++ priv = ds->priv;
++
+ dsa_unregister_switch(ds);
++
++ mxl862xx_host_shutdown(priv);
+ }
+
+ static void mxl862xx_shutdown(struct mdio_device *mdiodev)
+ {
+ struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
++ struct mxl862xx_priv *priv;
+
+ if (!ds)
+ return;
+
++ priv = ds->priv;
++
+ dsa_switch_shutdown(ds);
+
++ mxl862xx_host_shutdown(priv);
++
+ dev_set_drvdata(&mdiodev->dev, NULL);
+ }
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -11,6 +11,8 @@
+ struct mxl862xx_priv {
+ struct dsa_switch *ds;
+ struct mdio_device *mdiodev;
++ struct work_struct crc_err_work;
++ unsigned long crc_err;
+ };
+
+ #endif /* __MXL862XX_H */
--- /dev/null
+From 4a296a038c0ea3ad20afe8df00eb083232317646 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sun, 22 Mar 2026 13:27:26 +0000
+Subject: [PATCH 09/35] net: dsa: mxl862xx: use RST_DATA to skip writing zero
+ words
+
+Issue the firmware's RST_DATA command before writing data payloads that
+contain many zero words. RST_DATA zeroes both the firmware's internal
+buffer and the MMD data registers in a single command, allowing the
+driver to skip individual MDIO writes for zero-valued words. This
+reduces bus traffic for the common case where API structs have many
+unused or default-zero fields.
+
+The optimization is applied when at least 5 zero words are found in the
+payload, roughly the break-even point against the cost of the extra
+RST_DATA command round-trip.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Link: https://patch.msgid.link/d10bd6ad5df062d0da342c3e0d330550b3d2432b.1774185953.git.daniel@makrotopia.org
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-host.c | 38 ++++++++++++++++++++++++
+ 1 file changed, 38 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+@@ -283,6 +283,17 @@ static int mxl862xx_get_data(struct mxl8
+ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
+ }
+
++static int mxl862xx_rst_data(struct mxl862xx_priv *priv)
++{
++ return mxl862xx_issue_cmd(priv, MMD_API_RST_DATA, 0);
++}
++
++/* Minimum number of zero words in the data payload before issuing a
++ * RST_DATA command is worthwhile. RST_DATA costs one full command
++ * round-trip (~5 MDIO transactions), so the threshold must offset that.
++ */
++#define RST_DATA_THRESHOLD 5
++
+ static int mxl862xx_send_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 size,
+ bool quiet)
+ {
+@@ -318,6 +329,8 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ u16 size, bool read, bool quiet)
+ {
+ __le16 *data = _data;
++ bool use_rst = false;
++ unsigned int zeros;
+ int ret, cmd_ret;
+ u16 max, crc, i;
+
+@@ -331,6 +344,24 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ if (ret < 0)
+ goto out;
+
++ /* If the data contains enough zero words, issue RST_DATA to zero
++ * both the firmware buffer and MMD registers, then skip writing
++ * zero words individually.
++ */
++ for (i = 0, zeros = 0; i < size / 2 && zeros < RST_DATA_THRESHOLD; i++)
++ if (!data[i])
++ zeros++;
++
++ if (zeros < RST_DATA_THRESHOLD && (size & 1) && !*(u8 *)&data[i])
++ zeros++;
++
++ if (zeros >= RST_DATA_THRESHOLD) {
++ ret = mxl862xx_rst_data(priv);
++ if (ret < 0)
++ goto out;
++ use_rst = true;
++ }
++
+ /* Compute CRC-16 over the data payload; written as an extra word
+ * after the data so the firmware can verify the transfer.
+ */
+@@ -367,6 +398,13 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ val = le16_to_cpu(data[i]);
+ }
+
++ /* After RST_DATA, skip zero data words as the registers
++ * already contain zeros, but never skip the CRC word at the
++ * final word.
++ */
++ if (use_rst && i < max && val == 0)
++ continue;
++
+ ret = mxl862xx_reg_write(priv,
+ MXL862XX_MMD_REG_DATA_FIRST + off,
+ val);
static ssize_t
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
-@@ -1950,6 +1950,9 @@ char *phy_attached_info_irq(struct phy_d
+@@ -1963,6 +1963,9 @@ char *phy_attached_info_irq(struct phy_d
__malloc;
void phy_attached_info(struct phy_device *phydev);
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
-@@ -98,6 +98,13 @@ config NET_DSA_RZN1_A5PSW
+@@ -100,6 +100,13 @@ config NET_DSA_RZN1_A5PSW
This driver supports the A5PSW switch, which is embedded in Renesas
RZ/N1 SoC.
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
-@@ -101,6 +101,7 @@ config NET_DSA_RZN1_A5PSW
+@@ -103,6 +103,7 @@ config NET_DSA_RZN1_A5PSW
config NET_DSA_KS8995
tristate "Micrel KS8995 family 5-ports 10/100 Ethernet switches"
depends on SPI
/**
* phy_id_compare - compare @id1 with @id2 taking account of @mask
-@@ -1285,6 +1289,19 @@ static inline bool phy_id_compare(u32 id
+@@ -1298,6 +1302,19 @@ static inline bool phy_id_compare_model(
}
/**
--- /dev/null
+From 51a16863b04b1c2b28d45d80934f2267547734b7 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Fri, 9 May 2025 16:33:27 +0100
+Subject: [PATCH] net: sfp: add quirk for QINIYEK BJ-SFP-10G-T copper SFP+
+ module
+
+Add quirk for a copper SFP+ module that identifies itself as "QINIYEK"
+"BJ-SFP-10G-T".
+
+It uses RollBall protocol to talk to the built-in RealTek RTL8261N PHY.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/phy/sfp.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+--- a/drivers/net/phy/sfp.c
++++ b/drivers/net/phy/sfp.c
+@@ -557,6 +557,7 @@ static const struct sfp_quirk sfp_quirks
+ SFP_QUIRK_S("OEM", "SFP-2.5G-BX10-U", sfp_quirk_2500basex),
+ SFP_QUIRK_F("OEM", "RTSFP-10", sfp_fixup_rollball_cc),
+ SFP_QUIRK_F("OEM", "RTSFP-10G", sfp_fixup_rollball_cc),
++ SFP_QUIRK_F("QINIYEK", "BJ-SFP-10G-T", sfp_fixup_fs_10gt),
+ SFP_QUIRK_F("Turris", "RTSFP-2.5G", sfp_fixup_rollball),
+ SFP_QUIRK_F("Turris", "RTSFP-10", sfp_fixup_rollball),
+ SFP_QUIRK_F("Turris", "RTSFP-10G", sfp_fixup_rollball),
--- /dev/null
+From de6dd19a3edd1dc6400fecf77610e438441a02ac Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Wed, 25 Mar 2026 17:54:11 +0000
+Subject: [PATCH 10/35] net: dsa: move dsa_bridge_ports() helper to dsa.h
+
+The yt921x driver contains a helper to create a bitmap of ports
+which are members of a bridge.
+
+Move the helper as static inline function into dsa.h, so other driver
+can make use of it as well.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ include/net/dsa.h | 13 +++++++++++++
+ 1 file changed, 13 insertions(+)
+
+--- a/include/net/dsa.h
++++ b/include/net/dsa.h
+@@ -832,6 +832,19 @@ dsa_tree_offloads_bridge_dev(struct dsa_
+ return false;
+ }
+
++static inline u32
++dsa_bridge_ports(struct dsa_switch *ds, const struct net_device *bdev)
++{
++ struct dsa_port *dp;
++ u32 mask = 0;
++
++ dsa_switch_for_each_user_port(dp, ds)
++ if (dsa_port_offloads_bridge_dev(dp, bdev))
++ mask |= BIT(dp->index);
++
++ return mask;
++}
++
+ static inline bool dsa_port_tree_same(const struct dsa_port *a,
+ const struct dsa_port *b)
+ {
--- /dev/null
+From 880cde7abf58cb1316382ae7f59aac93c313e8fe Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Wed, 25 Mar 2026 17:54:41 +0000
+Subject: [PATCH 11/35] net: dsa: add bridge member iteration macro
+
+Drivers that offload bridges need to iterate over the ports that are
+members of a given bridge, for example to rebuild per-port forwarding
+bitmaps when membership changes. Currently drivers typically open-code
+this by combining dsa_switch_for_each_user_port() with a
+dsa_port_offloads_bridge_dev() check, or cache bridge membership
+within the driver.
+
+Add dsa_switch_for_each_bridge_member() macro to express this pattern
+directly, and use it for the existing dsa_bridge_ports() inline
+helper.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ include/net/dsa.h | 9 ++++++---
+ 1 file changed, 6 insertions(+), 3 deletions(-)
+
+--- a/include/net/dsa.h
++++ b/include/net/dsa.h
+@@ -832,15 +832,18 @@ dsa_tree_offloads_bridge_dev(struct dsa_
+ return false;
+ }
+
++#define dsa_switch_for_each_bridge_member(_dp, _ds, _bdev) \
++ dsa_switch_for_each_user_port(_dp, _ds) \
++ if (dsa_port_offloads_bridge_dev(_dp, _bdev))
++
+ static inline u32
+ dsa_bridge_ports(struct dsa_switch *ds, const struct net_device *bdev)
+ {
+ struct dsa_port *dp;
+ u32 mask = 0;
+
+- dsa_switch_for_each_user_port(dp, ds)
+- if (dsa_port_offloads_bridge_dev(dp, bdev))
+- mask |= BIT(dp->index);
++ dsa_switch_for_each_bridge_member(dp, ds, bdev)
++ mask |= BIT(dp->index);
+
+ return mask;
+ }
--- /dev/null
+From 149bb02d5bf031a1eb85f91377f54913de3a08ff Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Wed, 25 Mar 2026 17:54:52 +0000
+Subject: [PATCH 12/35] dsa: tag_mxl862xx: set dsa_default_offload_fwd_mark()
+
+The MxL862xx offloads bridge forwarding in hardware, so set
+dsa_default_offload_fwd_mark() to avoid duplicate forwarding of
+packets of (eg. flooded) frames arriving at the CPU port.
+
+Link-local frames are directly trapped to the CPU port only, so don't
+set dsa_default_offload_fwd_mark() on those.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ net/dsa/tag_mxl862xx.c | 3 +++
+ 1 file changed, 3 insertions(+)
+
+--- a/net/dsa/tag_mxl862xx.c
++++ b/net/dsa/tag_mxl862xx.c
+@@ -86,6 +86,9 @@ static struct sk_buff *mxl862_tag_rcv(st
+ return NULL;
+ }
+
++ if (likely(!is_link_local_ether_addr(eth_hdr(skb)->h_dest)))
++ dsa_default_offload_fwd_mark(skb);
++
+ /* remove the MxL862xx special tag between the MAC addresses and the
+ * current ethertype field.
+ */
--- /dev/null
+From 5acdee6df2fbd4a9b02045694227f25cb1d4e5e0 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Wed, 25 Mar 2026 17:55:08 +0000
+Subject: [PATCH 13/35] net: dsa: mxl862xx: implement bridge offloading
+
+Implement joining and leaving bridges as well as add, delete and dump
+operations on isolated FDBs, port MDB membership management, and
+setting a port's STP state.
+
+The switch supports a maximum of 63 bridges, however, up to 12 may
+be used as "single-port bridges" to isolate standalone ports.
+Allowing up to 48 bridges to be offloaded seems more than enough on
+that hardware, hence that is set as max_num_bridges.
+
+A total of 128 bridge ports are supported in the bridge portmap, and
+virtual bridge ports have to be used eg. for link-aggregation, hence
+potentially exceeding the number of hardware ports.
+
+The firmware-assigned bridge identifier (FID) for each offloaded bridge
+is stored in an array used to map DSA bridge num to firmware bridge ID,
+avoiding the need for a driver-private bridge tracking structure.
+Bridge member portmaps are rebuilt on join/leave using
+dsa_switch_for_each_bridge_member().
+
+As there are now more users of the BRIDGEPORT_CONFIG_SET API and the
+state of each port is cached locally, introduce a helper function
+mxl862xx_set_bridge_port(struct dsa_switch *ds, int port) which is
+then used to replace the direct calls to the API in
+mxl862xx_setup_cpu_bridge() and mxl862xx_add_single_port_bridge().
+
+Note that there is no convenient way to control flooding on per-port
+level, so the driver is using a 0-rate QoS meter setup as a stopper in
+lack of any better option. In order to be perfect the firmware-enforced
+minimum bucket size is bypassed by directly writing 0s to the relevant
+registers -- without that at least one 64-byte packet could still
+pass before the meter would change from 'yellow' into 'red' state.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 225 ++++++-
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 20 +-
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 752 ++++++++++++++++++++++--
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 133 +++++
+ 4 files changed, 1087 insertions(+), 43 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -3,6 +3,7 @@
+ #ifndef __MXL862XX_API_H
+ #define __MXL862XX_API_H
+
++#include <linux/bits.h>
+ #include <linux/if_ether.h>
+
+ /**
+@@ -35,6 +36,168 @@ struct mxl862xx_register_mod {
+ } __packed;
+
+ /**
++ * enum mxl862xx_mac_table_filter - Source/Destination MAC address filtering
++ *
++ * @MXL862XX_MAC_FILTER_NONE: no filter
++ * @MXL862XX_MAC_FILTER_SRC: source address filter
++ * @MXL862XX_MAC_FILTER_DEST: destination address filter
++ * @MXL862XX_MAC_FILTER_BOTH: both source and destination filter
++ */
++enum mxl862xx_mac_table_filter {
++ MXL862XX_MAC_FILTER_NONE = 0,
++ MXL862XX_MAC_FILTER_SRC = BIT(0),
++ MXL862XX_MAC_FILTER_DEST = BIT(1),
++ MXL862XX_MAC_FILTER_BOTH = BIT(0) | BIT(1),
++};
++
++#define MXL862XX_TCI_VLAN_ID GENMASK(11, 0)
++#define MXL862XX_TCI_VLAN_CFI_DEI BIT(12)
++#define MXL862XX_TCI_VLAN_PRI GENMASK(15, 13)
++
++/* Set in port_id to use port_map[] as a portmap bitmap instead of a single
++ * port ID. When clear, port_id selects one port; when set, the firmware
++ * ignores the lower bits of port_id and writes port_map[] directly into
++ * the PCE bridge port map.
++ */
++#define MXL862XX_PORTMAP_FLAG BIT(31)
++
++/**
++ * struct mxl862xx_mac_table_add - MAC Table Entry to be added
++ * @fid: Filtering Identifier (FID) (not supported by all switches)
++ * @port_id: Ethernet Port number
++ * @port_map: Bridge Port Map
++ * @sub_if_id: Sub-Interface Identifier Destination
++ * @age_timer: Aging Time in seconds
++ * @vlan_id: STAG VLAN Id
++ * @static_entry: Static Entry (value will be aged out if not set to static)
++ * @traffic_class: Egress queue traffic class
++ * @mac: MAC Address to add to the table
++ * @filter_flag: See &enum mxl862xx_mac_table_filter
++ * @igmp_controlled: Packet is marked as IGMP controlled if destination MAC
++ * address matches MAC in this entry
++ * @associated_mac: Associated Mac address
++ * @tci: TCI for B-Step
++ * Bit [0:11] - VLAN ID
++ * Bit [12] - VLAN CFI/DEI
++ * Bit [13:15] - VLAN PRI
++ */
++struct mxl862xx_mac_table_add {
++ __le16 fid;
++ __le32 port_id;
++ __le16 port_map[8];
++ __le16 sub_if_id;
++ __le32 age_timer;
++ __le16 vlan_id;
++ u8 static_entry;
++ u8 traffic_class;
++ u8 mac[ETH_ALEN];
++ u8 filter_flag;
++ u8 igmp_controlled;
++ u8 associated_mac[ETH_ALEN];
++ __le16 tci;
++} __packed;
++
++/**
++ * struct mxl862xx_mac_table_remove - MAC Table Entry to be removed
++ * @fid: Filtering Identifier (FID)
++ * @mac: MAC Address to be removed from the table.
++ * @filter_flag: See &enum mxl862xx_mac_table_filter
++ * @tci: TCI for B-Step
++ * Bit [0:11] - VLAN ID
++ * Bit [12] - VLAN CFI/DEI
++ * Bit [13:15] - VLAN PRI
++ */
++struct mxl862xx_mac_table_remove {
++ __le16 fid;
++ u8 mac[ETH_ALEN];
++ u8 filter_flag;
++ __le16 tci;
++} __packed;
++
++/**
++ * struct mxl862xx_mac_table_read - MAC Table Entry to be read
++ * @initial: Restart the get operation from the beginning of the table
++ * @last: Indicates that the read operation returned last entry
++ * @fid: Get the MAC table entry belonging to the given Filtering Identifier
++ * @port_id: The Bridge Port ID
++ * @port_map: Bridge Port Map
++ * @age_timer: Aging Time
++ * @vlan_id: STAG VLAN Id
++ * @static_entry: Indicates if this is a Static Entry
++ * @sub_if_id: Sub-Interface Identifier Destination
++ * @mac: MAC Address. Filled out by the switch API implementation.
++ * @filter_flag: See &enum mxl862xx_mac_table_filter
++ * @igmp_controlled: Packet is marked as IGMP controlled if destination MAC
++ * address matches the MAC in this entry
++ * @entry_changed: Indicate if the Entry has Changed
++ * @associated_mac: Associated MAC address
++ * @hit_status: MAC Table Hit Status Update
++ * @tci: TCI for B-Step
++ * Bit [0:11] - VLAN ID
++ * Bit [12] - VLAN CFI/DEI
++ * Bit [13:15] - VLAN PRI
++ * @first_bridge_port_id: The port this MAC address has first been learned.
++ * This is used for loop detection.
++ */
++struct mxl862xx_mac_table_read {
++ u8 initial;
++ u8 last;
++ __le16 fid;
++ __le32 port_id;
++ __le16 port_map[8];
++ __le32 age_timer;
++ __le16 vlan_id;
++ u8 static_entry;
++ __le16 sub_if_id;
++ u8 mac[ETH_ALEN];
++ u8 filter_flag;
++ u8 igmp_controlled;
++ u8 entry_changed;
++ u8 associated_mac[ETH_ALEN];
++ u8 hit_status;
++ __le16 tci;
++ __le16 first_bridge_port_id;
++} __packed;
++
++/**
++ * struct mxl862xx_mac_table_query - MAC Table Entry key-based lookup
++ * @mac: MAC Address to search for (input)
++ * @fid: Filtering Identifier (input)
++ * @found: Set by firmware: 1 if entry was found, 0 if not
++ * @port_id: Bridge Port ID (output; MSB set if portmap mode)
++ * @port_map: Bridge Port Map (output; valid for static entries)
++ * @sub_if_id: Sub-Interface Identifier Destination
++ * @age_timer: Aging Time
++ * @vlan_id: STAG VLAN Id
++ * @static_entry: Indicates if this is a Static Entry
++ * @filter_flag: See &enum mxl862xx_mac_table_filter (input+output)
++ * @igmp_controlled: IGMP controlled flag
++ * @entry_changed: Entry changed flag
++ * @associated_mac: Associated MAC address
++ * @hit_status: MAC Table Hit Status Update
++ * @tci: TCI (VLAN ID + CFI/DEI + PRI) (input)
++ * @first_bridge_port_id: First learned bridge port
++ */
++struct mxl862xx_mac_table_query {
++ u8 mac[ETH_ALEN];
++ __le16 fid;
++ u8 found;
++ __le32 port_id;
++ __le16 port_map[8];
++ __le16 sub_if_id;
++ __le32 age_timer;
++ __le16 vlan_id;
++ u8 static_entry;
++ u8 filter_flag;
++ u8 igmp_controlled;
++ u8 entry_changed;
++ u8 associated_mac[ETH_ALEN];
++ u8 hit_status;
++ __le16 tci;
++ __le16 first_bridge_port_id;
++} __packed;
++
++/**
+ * enum mxl862xx_mac_clear_type - MAC table clear type
+ * @MXL862XX_MAC_CLEAR_PHY_PORT: clear dynamic entries based on port_id
+ * @MXL862XX_MAC_CLEAR_DYNAMIC: clear all dynamic entries
+@@ -139,6 +302,40 @@ enum mxl862xx_bridge_port_egress_meter {
+ };
+
+ /**
++ * struct mxl862xx_qos_meter_cfg - Rate meter configuration
++ * @enable: Enable/disable meter
++ * @meter_id: Meter ID (assigned by firmware on alloc)
++ * @meter_name: Meter name string
++ * @meter_type: Meter algorithm type (srTCM = 0, trTCM = 1)
++ * @cbs: Committed Burst Size (in bytes)
++ * @res1: Reserved
++ * @ebs: Excess Burst Size (in bytes)
++ * @res2: Reserved
++ * @rate: Committed Information Rate (in kbit/s)
++ * @pi_rate: Peak Information Rate (in kbit/s)
++ * @colour_blind_mode: Colour-blind mode enable
++ * @pkt_mode: Packet mode enable
++ * @local_overhd: Local overhead accounting enable
++ * @local_overhd_val: Local overhead accounting value
++ */
++struct mxl862xx_qos_meter_cfg {
++ u8 enable;
++ __le16 meter_id;
++ char meter_name[32];
++ __le32 meter_type;
++ __le32 cbs;
++ __le32 res1;
++ __le32 ebs;
++ __le32 res2;
++ __le32 rate;
++ __le32 pi_rate;
++ u8 colour_blind_mode;
++ u8 pkt_mode;
++ u8 local_overhd;
++ __le16 local_overhd_val;
++} __packed;
++
++/**
+ * enum mxl862xx_bridge_forward_mode - Bridge forwarding type of packet
+ * @MXL862XX_BRIDGE_FORWARD_FLOOD: Packet is flooded to port members of
+ * ingress bridge port
+@@ -456,7 +653,7 @@ struct mxl862xx_pmapper {
+ */
+ struct mxl862xx_bridge_port_config {
+ __le16 bridge_port_id;
+- __le32 mask; /* enum mxl862xx_bridge_port_config_mask */
++ __le32 mask; /* enum mxl862xx_bridge_port_config_mask */
+ __le16 bridge_id;
+ u8 ingress_extended_vlan_enable;
+ __le16 ingress_extended_vlan_block_id;
+@@ -659,6 +856,32 @@ struct mxl862xx_ctp_port_assignment {
+ } __packed;
+
+ /**
++ * enum mxl862xx_stp_port_state - Spanning Tree Protocol port states
++ * @MXL862XX_STP_PORT_STATE_FORWARD: Forwarding state
++ * @MXL862XX_STP_PORT_STATE_DISABLE: Disabled/Discarding state
++ * @MXL862XX_STP_PORT_STATE_LEARNING: Learning state
++ * @MXL862XX_STP_PORT_STATE_BLOCKING: Blocking/Listening
++ */
++enum mxl862xx_stp_port_state {
++ MXL862XX_STP_PORT_STATE_FORWARD = 0,
++ MXL862XX_STP_PORT_STATE_DISABLE,
++ MXL862XX_STP_PORT_STATE_LEARNING,
++ MXL862XX_STP_PORT_STATE_BLOCKING,
++};
++
++/**
++ * struct mxl862xx_stp_port_cfg - Configures the Spanning Tree Protocol state
++ * @port_id: Port number
++ * @fid: Filtering Identifier (FID)
++ * @port_state: See &enum mxl862xx_stp_port_state
++ */
++struct mxl862xx_stp_port_cfg {
++ __le16 port_id;
++ __le16 fid;
++ __le32 port_state; /* enum mxl862xx_stp_port_state */
++} __packed;
++
++/**
+ * struct mxl862xx_sys_fw_image_version - Firmware version information
+ * @iv_major: firmware major version
+ * @iv_minor: firmware minor version
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -15,12 +15,15 @@
+ #define MXL862XX_BRDG_MAGIC 0x300
+ #define MXL862XX_BRDGPORT_MAGIC 0x400
+ #define MXL862XX_CTP_MAGIC 0x500
++#define MXL862XX_QOS_MAGIC 0x600
+ #define MXL862XX_SWMAC_MAGIC 0xa00
++#define MXL862XX_STP_MAGIC 0xf00
+ #define MXL862XX_SS_MAGIC 0x1600
+ #define GPY_GPY2XX_MAGIC 0x1800
+ #define SYS_MISC_MAGIC 0x1900
+
+ #define MXL862XX_COMMON_CFGGET (MXL862XX_COMMON_MAGIC + 0x9)
++#define MXL862XX_COMMON_CFGSET (MXL862XX_COMMON_MAGIC + 0xa)
+ #define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11)
+
+ #define MXL862XX_BRIDGE_ALLOC (MXL862XX_BRDG_MAGIC + 0x1)
+@@ -35,14 +38,23 @@
+
+ #define MXL862XX_CTP_PORTASSIGNMENTSET (MXL862XX_CTP_MAGIC + 0x3)
+
++#define MXL862XX_QOS_METERCFGSET (MXL862XX_QOS_MAGIC + 0x2)
++#define MXL862XX_QOS_METERALLOC (MXL862XX_QOS_MAGIC + 0x2a)
++
++#define MXL862XX_MAC_TABLEENTRYADD (MXL862XX_SWMAC_MAGIC + 0x2)
++#define MXL862XX_MAC_TABLEENTRYREAD (MXL862XX_SWMAC_MAGIC + 0x3)
++#define MXL862XX_MAC_TABLEENTRYQUERY (MXL862XX_SWMAC_MAGIC + 0x4)
++#define MXL862XX_MAC_TABLEENTRYREMOVE (MXL862XX_SWMAC_MAGIC + 0x5)
+ #define MXL862XX_MAC_TABLECLEARCOND (MXL862XX_SWMAC_MAGIC + 0x8)
+
+-#define MXL862XX_SS_SPTAG_SET (MXL862XX_SS_MAGIC + 0x02)
++#define MXL862XX_SS_SPTAG_SET (MXL862XX_SS_MAGIC + 0x2)
++
++#define MXL862XX_STP_PORTCFGSET (MXL862XX_STP_MAGIC + 0x2)
+
+-#define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x01)
+-#define INT_GPHY_WRITE (GPY_GPY2XX_MAGIC + 0x02)
++#define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x1)
++#define INT_GPHY_WRITE (GPY_GPY2XX_MAGIC + 0x2)
+
+-#define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x02)
++#define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2)
+
+ #define MMD_API_MAXIMUM_ID 0x7fff
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -7,8 +7,11 @@
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ */
+
+-#include <linux/module.h>
++#include <linux/bitfield.h>
+ #include <linux/delay.h>
++#include <linux/etherdevice.h>
++#include <linux/if_bridge.h>
++#include <linux/module.h>
+ #include <linux/of_device.h>
+ #include <linux/of_mdio.h>
+ #include <linux/phy.h>
+@@ -36,6 +39,17 @@
+ #define MXL862XX_READY_TIMEOUT_MS 10000
+ #define MXL862XX_READY_POLL_MS 100
+
++#define MXL862XX_TCM_INST_SEL 0xe00
++#define MXL862XX_TCM_CBS 0xe12
++#define MXL862XX_TCM_EBS 0xe13
++
++static const int mxl862xx_flood_meters[] = {
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP,
++ MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST,
++};
++
+ static enum dsa_tag_protocol mxl862xx_get_tag_protocol(struct dsa_switch *ds,
+ int port,
+ enum dsa_tag_protocol m)
+@@ -168,6 +182,225 @@ static int mxl862xx_setup_mdio(struct ds
+ return ret;
+ }
+
++static int mxl862xx_bridge_config_fwd(struct dsa_switch *ds, u16 bridge_id,
++ bool ucast_flood, bool mcast_flood,
++ bool bcast_flood)
++{
++ struct mxl862xx_bridge_config bridge_config = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ int ret;
++
++ bridge_config.mask = cpu_to_le32(MXL862XX_BRIDGE_CONFIG_MASK_FORWARDING_MODE);
++ bridge_config.bridge_id = cpu_to_le16(bridge_id);
++
++ bridge_config.forward_unknown_unicast = cpu_to_le32(ucast_flood ?
++ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD);
++
++ bridge_config.forward_unknown_multicast_ip = cpu_to_le32(mcast_flood ?
++ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD);
++ bridge_config.forward_unknown_multicast_non_ip =
++ bridge_config.forward_unknown_multicast_ip;
++
++ bridge_config.forward_broadcast = cpu_to_le32(bcast_flood ?
++ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD);
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGE_CONFIGSET, bridge_config);
++ if (ret)
++ dev_err(ds->dev, "failed to configure bridge %u forwarding: %d\n",
++ bridge_id, ret);
++
++ return ret;
++}
++
++/* Allocate a single zero-rate meter shared by all ports and flood types.
++ * All flood-blocking egress sub-meters point to this one meter so that any
++ * packet hitting this meter is unconditionally dropped.
++ *
++ * The firmware API requires CBS >= 64 (its bs2ls encoder clamps smaller
++ * values), so the meter is initially configured with CBS=EBS=64.
++ * A zero-rate bucket starts full at CBS bytes, which would let one packet
++ * through before the bucket empties. To eliminate this one-packet leak we
++ * override CBS and EBS to zero via direct register writes after the API call;
++ * the hardware accepts CBS=0 and immediately flags the bucket as exceeded,
++ * so no traffic can ever pass.
++ */
++static int mxl862xx_setup_drop_meter(struct dsa_switch *ds)
++{
++ struct mxl862xx_qos_meter_cfg meter = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_register_mod reg;
++ int ret;
++
++ /* meter_id=0 means auto-alloc */
++ ret = MXL862XX_API_READ(priv, MXL862XX_QOS_METERALLOC, meter);
++ if (ret)
++ return ret;
++
++ meter.enable = true;
++ meter.cbs = cpu_to_le32(64);
++ meter.ebs = cpu_to_le32(64);
++ snprintf(meter.meter_name, sizeof(meter.meter_name), "drop");
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_QOS_METERCFGSET, meter);
++ if (ret)
++ return ret;
++
++ priv->drop_meter = le16_to_cpu(meter.meter_id);
++
++ /* Select the meter instance for subsequent TCM register access. */
++ reg.addr = cpu_to_le16(MXL862XX_TCM_INST_SEL);
++ reg.data = cpu_to_le16(priv->drop_meter);
++ reg.mask = cpu_to_le16(0xffff);
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
++ if (ret)
++ return ret;
++
++ /* Zero CBS so the committed bucket starts empty (exceeded). */
++ reg.addr = cpu_to_le16(MXL862XX_TCM_CBS);
++ reg.data = 0;
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
++ if (ret)
++ return ret;
++
++ /* Zero EBS so the excess bucket starts empty (exceeded). */
++ reg.addr = cpu_to_le16(MXL862XX_TCM_EBS);
++ return MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
++}
++
++static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_bridge_port_config br_port_cfg = {};
++ struct dsa_port *dp = dsa_to_port(ds, port);
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ u16 bridge_id = dp->bridge ?
++ priv->bridges[dp->bridge->num] : p->fid;
++ bool enable;
++ int i, idx;
++
++ br_port_cfg.bridge_port_id = cpu_to_le16(port);
++ br_port_cfg.bridge_id = cpu_to_le16(bridge_id);
++ br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER);
++ br_port_cfg.src_mac_learning_disable = !p->learning;
++
++ mxl862xx_fw_portmap_from_bitmap(br_port_cfg.bridge_port_map, p->portmap);
++
++ for (i = 0; i < ARRAY_SIZE(mxl862xx_flood_meters); i++) {
++ idx = mxl862xx_flood_meters[i];
++ enable = !!(p->flood_block & BIT(idx));
++
++ br_port_cfg.egress_traffic_sub_meter_id[idx] =
++ enable ? cpu_to_le16(priv->drop_meter) : 0;
++ br_port_cfg.egress_sub_metering_enable[idx] = enable;
++ }
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET,
++ br_port_cfg);
++}
++
++static int mxl862xx_sync_bridge_members(struct dsa_switch *ds,
++ const struct dsa_bridge *bridge)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *dp, *member_dp;
++ int port, err, ret = 0;
++
++ dsa_switch_for_each_bridge_member(dp, ds, bridge->dev) {
++ port = dp->index;
++
++ bitmap_zero(priv->ports[port].portmap,
++ MXL862XX_MAX_BRIDGE_PORTS);
++
++ dsa_switch_for_each_bridge_member(member_dp, ds, bridge->dev) {
++ if (member_dp->index != port)
++ __set_bit(member_dp->index,
++ priv->ports[port].portmap);
++ }
++ __set_bit(dp->cpu_dp->index, priv->ports[port].portmap);
++
++ err = mxl862xx_set_bridge_port(ds, port);
++ if (err)
++ ret = err;
++ }
++
++ return ret;
++}
++
++/**
++ * mxl862xx_allocate_bridge - Allocate a firmware bridge instance
++ * @priv: driver private data
++ * @bridge_id: output -- firmware bridge ID assigned by the firmware
++ *
++ * Newly allocated bridges default to flooding all traffic classes
++ * (unknown unicast, multicast, broadcast). Callers that need
++ * different forwarding behavior must call mxl862xx_bridge_config_fwd()
++ * after allocation.
++ *
++ * Return: 0 on success, negative errno on failure.
++ */
++static int mxl862xx_allocate_bridge(struct mxl862xx_priv *priv, u16 *bridge_id)
++{
++ struct mxl862xx_bridge_alloc br_alloc = {};
++ int ret;
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_BRIDGE_ALLOC, br_alloc);
++ if (ret)
++ return ret;
++
++ *bridge_id = le16_to_cpu(br_alloc.bridge_id);
++ return 0;
++}
++
++static void mxl862xx_free_bridge(struct dsa_switch *ds,
++ const struct dsa_bridge *bridge)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ u16 fw_id = priv->bridges[bridge->num];
++ struct mxl862xx_bridge_alloc br_alloc = {
++ .bridge_id = cpu_to_le16(fw_id),
++ };
++ int ret;
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGE_FREE, br_alloc);
++ if (ret) {
++ dev_err(ds->dev, "failed to free fw bridge %u: %pe\n",
++ fw_id, ERR_PTR(ret));
++ return;
++ }
++
++ priv->bridges[bridge->num] = 0;
++}
++
++static int mxl862xx_add_single_port_bridge(struct dsa_switch *ds, int port)
++{
++ struct dsa_port *dp = dsa_to_port(ds, port);
++ struct mxl862xx_priv *priv = ds->priv;
++ int ret;
++
++ ret = mxl862xx_allocate_bridge(priv, &priv->ports[port].fid);
++ if (ret) {
++ dev_err(ds->dev, "failed to allocate a bridge for port %d\n", port);
++ return ret;
++ }
++
++ priv->ports[port].learning = false;
++ bitmap_zero(priv->ports[port].portmap, MXL862XX_MAX_BRIDGE_PORTS);
++ __set_bit(dp->cpu_dp->index, priv->ports[port].portmap);
++
++ ret = mxl862xx_set_bridge_port(ds, port);
++ if (ret)
++ return ret;
++
++ /* Standalone ports should not flood unknown unicast or multicast
++ * towards the CPU by default; only broadcast is needed initially.
++ */
++ return mxl862xx_bridge_config_fwd(ds, priv->ports[port].fid,
++ false, false, true);
++}
++
+ static int mxl862xx_setup(struct dsa_switch *ds)
+ {
+ struct mxl862xx_priv *priv = ds->priv;
+@@ -181,6 +414,10 @@ static int mxl862xx_setup(struct dsa_swi
+ if (ret)
+ return ret;
+
++ ret = mxl862xx_setup_drop_meter(ds);
++ if (ret)
++ return ret;
++
+ return mxl862xx_setup_mdio(ds);
+ }
+
+@@ -260,66 +497,87 @@ static int mxl862xx_configure_sp_tag_pro
+
+ static int mxl862xx_setup_cpu_bridge(struct dsa_switch *ds, int port)
+ {
+- struct mxl862xx_bridge_port_config br_port_cfg = {};
+ struct mxl862xx_priv *priv = ds->priv;
+- u16 bridge_port_map = 0;
+ struct dsa_port *dp;
+
+- /* CPU port bridge setup */
+- br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
+- MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING |
+- MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING);
+-
+- br_port_cfg.bridge_port_id = cpu_to_le16(port);
+- br_port_cfg.src_mac_learning_disable = false;
+- br_port_cfg.vlan_src_mac_vid_enable = true;
+- br_port_cfg.vlan_dst_mac_vid_enable = true;
++ priv->ports[port].fid = MXL862XX_DEFAULT_BRIDGE;
++ priv->ports[port].learning = true;
+
+ /* include all assigned user ports in the CPU portmap */
++ bitmap_zero(priv->ports[port].portmap, MXL862XX_MAX_BRIDGE_PORTS);
+ dsa_switch_for_each_user_port(dp, ds) {
+ /* it's safe to rely on cpu_dp being valid for user ports */
+ if (dp->cpu_dp->index != port)
+ continue;
+
+- bridge_port_map |= BIT(dp->index);
++ __set_bit(dp->index, priv->ports[port].portmap);
+ }
+- br_port_cfg.bridge_port_map[0] |= cpu_to_le16(bridge_port_map);
+
+- return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, br_port_cfg);
++ return mxl862xx_set_bridge_port(ds, port);
+ }
+
+-static int mxl862xx_add_single_port_bridge(struct dsa_switch *ds, int port)
++static int mxl862xx_port_bridge_join(struct dsa_switch *ds, int port,
++ const struct dsa_bridge bridge,
++ bool *tx_fwd_offload,
++ struct netlink_ext_ack *extack)
+ {
+- struct mxl862xx_bridge_port_config br_port_cfg = {};
+- struct dsa_port *dp = dsa_to_port(ds, port);
+- struct mxl862xx_bridge_alloc br_alloc = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ u16 fw_id;
+ int ret;
+
+- ret = MXL862XX_API_READ(ds->priv, MXL862XX_BRIDGE_ALLOC, br_alloc);
+- if (ret) {
+- dev_err(ds->dev, "failed to allocate a bridge for port %d\n", port);
+- return ret;
++ if (!priv->bridges[bridge.num]) {
++ ret = mxl862xx_allocate_bridge(priv, &fw_id);
++ if (ret)
++ return ret;
++
++ priv->bridges[bridge.num] = fw_id;
++
++ /* Free bridge here on error, DSA rollback won't. */
++ ret = mxl862xx_sync_bridge_members(ds, &bridge);
++ if (ret) {
++ mxl862xx_free_bridge(ds, &bridge);
++ return ret;
++ }
++
++ return 0;
+ }
+
+- br_port_cfg.bridge_id = br_alloc.bridge_id;
+- br_port_cfg.bridge_port_id = cpu_to_le16(port);
+- br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
+- MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
+- MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING |
+- MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING);
+- br_port_cfg.src_mac_learning_disable = true;
+- br_port_cfg.vlan_src_mac_vid_enable = false;
+- br_port_cfg.vlan_dst_mac_vid_enable = false;
+- /* As this function is only called for user ports it is safe to rely on
+- * cpu_dp being valid
++ return mxl862xx_sync_bridge_members(ds, &bridge);
++}
++
++static void mxl862xx_port_bridge_leave(struct dsa_switch *ds, int port,
++ const struct dsa_bridge bridge)
++{
++ struct dsa_port *dp = dsa_to_port(ds, port);
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ int err;
++
++ err = mxl862xx_sync_bridge_members(ds, &bridge);
++ if (err)
++ dev_err(ds->dev,
++ "failed to sync bridge members after port %d left: %pe\n",
++ port, ERR_PTR(err));
++
++ /* Revert leaving port, omitted by the sync above, to its
++ * single-port bridge
+ */
+- br_port_cfg.bridge_port_map[0] = cpu_to_le16(BIT(dp->cpu_dp->index));
++ bitmap_zero(p->portmap, MXL862XX_MAX_BRIDGE_PORTS);
++ __set_bit(dp->cpu_dp->index, p->portmap);
++ p->flood_block = 0;
++ err = mxl862xx_set_bridge_port(ds, port);
++ if (err)
++ dev_err(ds->dev,
++ "failed to update bridge port %d state: %pe\n", port,
++ ERR_PTR(err));
+
+- return MXL862XX_API_WRITE(ds->priv, MXL862XX_BRIDGEPORT_CONFIGSET, br_port_cfg);
++ if (!dsa_bridge_ports(ds, bridge.dev))
++ mxl862xx_free_bridge(ds, &bridge);
+ }
+
+ static int mxl862xx_port_setup(struct dsa_switch *ds, int port)
+ {
++ struct mxl862xx_priv *priv = ds->priv;
+ struct dsa_port *dp = dsa_to_port(ds, port);
+ bool is_cpu_port = dsa_port_is_cpu(dp);
+ int ret;
+@@ -352,7 +610,31 @@ static int mxl862xx_port_setup(struct ds
+ return mxl862xx_setup_cpu_bridge(ds, port);
+
+ /* setup single-port bridge for user ports */
+- return mxl862xx_add_single_port_bridge(ds, port);
++ ret = mxl862xx_add_single_port_bridge(ds, port);
++ if (ret)
++ return ret;
++
++ priv->ports[port].setup_done = true;
++
++ return 0;
++}
++
++static void mxl862xx_port_teardown(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *dp = dsa_to_port(ds, port);
++
++ if (dsa_port_is_unused(dp) || dsa_port_is_dsa(dp))
++ return;
++
++ /* Prevent deferred host_flood_work from acting on stale state.
++ * The flag is checked under rtnl_lock() by the worker; since
++ * teardown also runs under RTNL, this is race-free.
++ *
++ * HW EVLAN/VF blocks are not freed here -- the firmware receives
++ * a full reset on the next probe, which reclaims all resources.
++ */
++ priv->ports[port].setup_done = false;
+ }
+
+ static void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
+@@ -365,14 +647,385 @@ static void mxl862xx_phylink_get_caps(st
+ config->supported_interfaces);
+ }
+
++static int mxl862xx_get_fid(struct dsa_switch *ds, struct dsa_db db)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++
++ switch (db.type) {
++ case DSA_DB_PORT:
++ return priv->ports[db.dp->index].fid;
++
++ case DSA_DB_BRIDGE:
++ if (!priv->bridges[db.bridge.num])
++ return -ENOENT;
++ return priv->bridges[db.bridge.num];
++
++ default:
++ return -EOPNOTSUPP;
++ }
++}
++
++static int mxl862xx_port_fdb_add(struct dsa_switch *ds, int port,
++ const unsigned char *addr, u16 vid, struct dsa_db db)
++{
++ struct mxl862xx_mac_table_add param = {};
++ int fid = mxl862xx_get_fid(ds, db), ret;
++ struct mxl862xx_priv *priv = ds->priv;
++
++ if (fid < 0)
++ return fid;
++
++ param.port_id = cpu_to_le32(port);
++ param.static_entry = true;
++ param.fid = cpu_to_le16(fid);
++ param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
++ ether_addr_copy(param.mac, addr);
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, param);
++ if (ret)
++ dev_err(ds->dev, "failed to add FDB entry on port %d\n", port);
++
++ return ret;
++}
++
++static int mxl862xx_port_fdb_del(struct dsa_switch *ds, int port,
++ const unsigned char *addr, u16 vid, const struct dsa_db db)
++{
++ struct mxl862xx_mac_table_remove param = {};
++ int fid = mxl862xx_get_fid(ds, db), ret;
++ struct mxl862xx_priv *priv = ds->priv;
++
++ if (fid < 0)
++ return fid;
++
++ param.fid = cpu_to_le16(fid);
++ param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
++ ether_addr_copy(param.mac, addr);
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, param);
++ if (ret)
++ dev_err(ds->dev, "failed to remove FDB entry on port %d\n", port);
++
++ return ret;
++}
++
++static int mxl862xx_port_fdb_dump(struct dsa_switch *ds, int port,
++ dsa_fdb_dump_cb_t *cb, void *data)
++{
++ struct mxl862xx_mac_table_read param = { .initial = 1 };
++ struct mxl862xx_priv *priv = ds->priv;
++ u32 entry_port_id;
++ int ret;
++
++ while (true) {
++ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYREAD, param);
++ if (ret)
++ return ret;
++
++ if (param.last)
++ break;
++
++ entry_port_id = le32_to_cpu(param.port_id);
++
++ if (entry_port_id == port) {
++ ret = cb(param.mac, FIELD_GET(MXL862XX_TCI_VLAN_ID,
++ le16_to_cpu(param.tci)),
++ param.static_entry, data);
++ if (ret)
++ return ret;
++ }
++
++ memset(¶m, 0, sizeof(param));
++ }
++
++ return 0;
++}
++
++static int mxl862xx_port_mdb_add(struct dsa_switch *ds, int port,
++ const struct switchdev_obj_port_mdb *mdb,
++ const struct dsa_db db)
++{
++ struct mxl862xx_mac_table_query qparam = {};
++ struct mxl862xx_mac_table_add aparam = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ int fid, ret;
++
++ fid = mxl862xx_get_fid(ds, db);
++ if (fid < 0)
++ return fid;
++
++ /* Look up existing entry by {MAC, FID, TCI} */
++ ether_addr_copy(qparam.mac, mdb->addr);
++ qparam.fid = cpu_to_le16(fid);
++ qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
++ if (ret)
++ return ret;
++
++ /* Build the ADD command using portmap mode */
++ ether_addr_copy(aparam.mac, mdb->addr);
++ aparam.fid = cpu_to_le16(fid);
++ aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
++ aparam.static_entry = true;
++ aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG);
++
++ /* Merge with existing portmap if entry already exists */
++ if (qparam.found)
++ memcpy(aparam.port_map, qparam.port_map,
++ sizeof(aparam.port_map));
++
++ mxl862xx_fw_portmap_set_bit(aparam.port_map, port);
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam);
++}
++
++static int mxl862xx_port_mdb_del(struct dsa_switch *ds, int port,
++ const struct switchdev_obj_port_mdb *mdb,
++ const struct dsa_db db)
++{
++ struct mxl862xx_mac_table_remove rparam = {};
++ struct mxl862xx_mac_table_query qparam = {};
++ struct mxl862xx_mac_table_add aparam = {};
++ int fid = mxl862xx_get_fid(ds, db), ret;
++ struct mxl862xx_priv *priv = ds->priv;
++
++ if (fid < 0)
++ return fid;
++
++ /* Look up existing entry */
++ qparam.fid = cpu_to_le16(fid);
++ qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
++ ether_addr_copy(qparam.mac, mdb->addr);
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
++ if (ret)
++ return ret;
++
++ if (!qparam.found)
++ return 0;
++
++ mxl862xx_fw_portmap_clear_bit(qparam.port_map, port);
++
++ if (mxl862xx_fw_portmap_is_empty(qparam.port_map)) {
++ /* No ports left — remove the entry entirely */
++ rparam.fid = cpu_to_le16(fid);
++ rparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
++ ether_addr_copy(rparam.mac, mdb->addr);
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, rparam);
++ } else {
++ /* Write back with reduced portmap */
++ aparam.fid = cpu_to_le16(fid);
++ aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
++ ether_addr_copy(aparam.mac, mdb->addr);
++ aparam.static_entry = true;
++ aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG);
++ memcpy(aparam.port_map, qparam.port_map, sizeof(aparam.port_map));
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam);
++ }
++
++ return ret;
++}
++
++static int mxl862xx_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
++{
++ struct mxl862xx_cfg param = {};
++ int ret;
++
++ ret = MXL862XX_API_READ(ds->priv, MXL862XX_COMMON_CFGGET, param);
++ if (ret) {
++ dev_err(ds->dev, "failed to read switch config\n");
++ return ret;
++ }
++
++ param.mac_table_age_timer = cpu_to_le32(MXL862XX_AGETIMER_CUSTOM);
++ param.age_timer = cpu_to_le32(msecs / 1000);
++ ret = MXL862XX_API_WRITE(ds->priv, MXL862XX_COMMON_CFGSET, param);
++ if (ret)
++ dev_err(ds->dev, "failed to set ageing\n");
++
++ return ret;
++}
++
++static void mxl862xx_port_stp_state_set(struct dsa_switch *ds, int port,
++ u8 state)
++{
++ struct mxl862xx_stp_port_cfg param = {
++ .port_id = cpu_to_le16(port),
++ };
++ struct mxl862xx_priv *priv = ds->priv;
++ int ret;
++
++ switch (state) {
++ case BR_STATE_DISABLED:
++ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_DISABLE);
++ break;
++ case BR_STATE_BLOCKING:
++ case BR_STATE_LISTENING:
++ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_BLOCKING);
++ break;
++ case BR_STATE_LEARNING:
++ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_LEARNING);
++ break;
++ case BR_STATE_FORWARDING:
++ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_FORWARD);
++ break;
++ default:
++ dev_err(ds->dev, "invalid STP state: %d\n", state);
++ return;
++ }
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_STP_PORTCFGSET, param);
++ if (ret) {
++ dev_err(ds->dev, "failed to set STP state on port %d\n", port);
++ return;
++ }
++
++ /* The firmware may re-enable MAC learning as a side-effect of entering
++ * LEARNING or FORWARDING state (per 802.1D defaults).
++ * Re-apply the driver's intended learning and metering config so that
++ * standalone ports keep learning disabled.
++ * This is likely to get fixed in future firmware releases, however,
++ * the additional API call even then doesn't hurt much.
++ */
++ ret = mxl862xx_set_bridge_port(ds, port);
++ if (ret)
++ dev_err(ds->dev, "failed to reapply brport flags on port %d\n",
++ port);
++
++ mxl862xx_port_fast_age(ds, port);
++}
++
++/* Deferred work handler for host flood configuration.
++ *
++ * port_set_host_flood is called from atomic context (under
++ * netif_addr_lock), so firmware calls must be deferred. The worker
++ * acquires rtnl_lock() to serialize with DSA callbacks that access the
++ * same driver state.
++ */
++static void mxl862xx_host_flood_work_fn(struct work_struct *work)
++{
++ struct mxl862xx_port *p = container_of(work, struct mxl862xx_port,
++ host_flood_work);
++ struct mxl862xx_priv *priv = p->priv;
++ struct dsa_switch *ds = priv->ds;
++ int port = p - priv->ports;
++ bool uc, mc;
++
++ rtnl_lock();
++
++ /* Port may have been torn down between scheduling and now. */
++ if (!p->setup_done) {
++ rtnl_unlock();
++ return;
++ }
++
++ uc = p->host_flood_uc;
++ mc = p->host_flood_mc;
++
++ /* The hardware controls unknown-unicast/multicast forwarding per FID
++ * (bridge), not per source port. For bridged ports all members share
++ * one FID, so we cannot selectively suppress flooding to the CPU for
++ * one source port while allowing it for another. Silently ignore the
++ * request -- the excess flooding towards the CPU is harmless.
++ */
++ if (!dsa_port_bridge_dev_get(dsa_to_port(ds, port)))
++ mxl862xx_bridge_config_fwd(ds, p->fid, uc, mc, true);
++
++ rtnl_unlock();
++}
++
++static void mxl862xx_port_set_host_flood(struct dsa_switch *ds, int port,
++ bool uc, bool mc)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++
++ p->host_flood_uc = uc;
++ p->host_flood_mc = mc;
++ schedule_work(&p->host_flood_work);
++}
++
++static int mxl862xx_port_pre_bridge_flags(struct dsa_switch *ds, int port,
++ const struct switchdev_brport_flags flags,
++ struct netlink_ext_ack *extack)
++{
++ if (flags.mask & ~(BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD |
++ BR_LEARNING))
++ return -EINVAL;
++
++ return 0;
++}
++
++static int mxl862xx_port_bridge_flags(struct dsa_switch *ds, int port,
++ const struct switchdev_brport_flags flags,
++ struct netlink_ext_ack *extack)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ unsigned long old_block = priv->ports[port].flood_block;
++ unsigned long block = old_block;
++ bool need_update = false;
++ int ret;
++
++ if (flags.mask & BR_FLOOD) {
++ if (flags.val & BR_FLOOD)
++ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC);
++ else
++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC);
++ }
++
++ if (flags.mask & BR_MCAST_FLOOD) {
++ if (flags.val & BR_MCAST_FLOOD) {
++ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP);
++ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP);
++ } else {
++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP);
++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP);
++ }
++ }
++
++ if (flags.mask & BR_BCAST_FLOOD) {
++ if (flags.val & BR_BCAST_FLOOD)
++ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST);
++ else
++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST);
++ }
++
++ if (flags.mask & BR_LEARNING)
++ priv->ports[port].learning = !!(flags.val & BR_LEARNING);
++
++ need_update = (block != old_block) || (flags.mask & BR_LEARNING);
++ if (need_update) {
++ priv->ports[port].flood_block = block;
++ ret = mxl862xx_set_bridge_port(ds, port);
++ if (ret)
++ return ret;
++ }
++
++ return 0;
++}
++
+ static const struct dsa_switch_ops mxl862xx_switch_ops = {
+ .get_tag_protocol = mxl862xx_get_tag_protocol,
+ .setup = mxl862xx_setup,
+ .port_setup = mxl862xx_port_setup,
++ .port_teardown = mxl862xx_port_teardown,
+ .phylink_get_caps = mxl862xx_phylink_get_caps,
+ .port_enable = mxl862xx_port_enable,
+ .port_disable = mxl862xx_port_disable,
+ .port_fast_age = mxl862xx_port_fast_age,
++ .set_ageing_time = mxl862xx_set_ageing_time,
++ .port_bridge_join = mxl862xx_port_bridge_join,
++ .port_bridge_leave = mxl862xx_port_bridge_leave,
++ .port_pre_bridge_flags = mxl862xx_port_pre_bridge_flags,
++ .port_bridge_flags = mxl862xx_port_bridge_flags,
++ .port_stp_state_set = mxl862xx_port_stp_state_set,
++ .port_set_host_flood = mxl862xx_port_set_host_flood,
++ .port_fdb_add = mxl862xx_port_fdb_add,
++ .port_fdb_del = mxl862xx_port_fdb_del,
++ .port_fdb_dump = mxl862xx_port_fdb_dump,
++ .port_mdb_add = mxl862xx_port_mdb_add,
++ .port_mdb_del = mxl862xx_port_mdb_del,
+ };
+
+ static void mxl862xx_phylink_mac_config(struct phylink_config *config,
+@@ -407,6 +1060,7 @@ static int mxl862xx_probe(struct mdio_de
+ struct device *dev = &mdiodev->dev;
+ struct mxl862xx_priv *priv;
+ struct dsa_switch *ds;
++ int i;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+@@ -424,8 +1078,17 @@ static int mxl862xx_probe(struct mdio_de
+ ds->ops = &mxl862xx_switch_ops;
+ ds->phylink_mac_ops = &mxl862xx_phylink_mac_ops;
+ ds->num_ports = MXL862XX_MAX_PORTS;
++ ds->fdb_isolation = true;
++ ds->max_num_bridges = MXL862XX_MAX_BRIDGES;
++
+ mxl862xx_host_init(priv);
+
++ for (i = 0; i < MXL862XX_MAX_PORTS; i++) {
++ priv->ports[i].priv = priv;
++ INIT_WORK(&priv->ports[i].host_flood_work,
++ mxl862xx_host_flood_work_fn);
++ }
++
+ dev_set_drvdata(dev, ds);
+
+ return dsa_register_switch(ds);
+@@ -435,6 +1098,7 @@ static void mxl862xx_remove(struct mdio_
+ {
+ struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
+ struct mxl862xx_priv *priv;
++ int i;
+
+ if (!ds)
+ return;
+@@ -444,12 +1108,21 @@ static void mxl862xx_remove(struct mdio_
+ dsa_unregister_switch(ds);
+
+ mxl862xx_host_shutdown(priv);
++
++ /* Cancel any pending host flood work. dsa_unregister_switch()
++ * has already called port_teardown (which sets setup_done=false),
++ * but a worker could still be blocked on rtnl_lock(). Since we
++ * are now outside RTNL, cancel_work_sync() will not deadlock.
++ */
++ for (i = 0; i < MXL862XX_MAX_PORTS; i++)
++ cancel_work_sync(&priv->ports[i].host_flood_work);
+ }
+
+ static void mxl862xx_shutdown(struct mdio_device *mdiodev)
+ {
+ struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
+ struct mxl862xx_priv *priv;
++ int i;
+
+ if (!ds)
+ return;
+@@ -460,6 +1133,9 @@ static void mxl862xx_shutdown(struct mdi
+
+ mxl862xx_host_shutdown(priv);
+
++ for (i = 0; i < MXL862XX_MAX_PORTS; i++)
++ cancel_work_sync(&priv->ports[i].host_flood_work);
++
+ dev_set_drvdata(&mdiodev->dev, NULL);
+ }
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -4,15 +4,148 @@
+ #define __MXL862XX_H
+
+ #include <linux/mdio.h>
++#include <linux/workqueue.h>
+ #include <net/dsa.h>
+
++struct mxl862xx_priv;
++
+ #define MXL862XX_MAX_PORTS 17
++#define MXL862XX_DEFAULT_BRIDGE 0
++#define MXL862XX_MAX_BRIDGES 48
++#define MXL862XX_MAX_BRIDGE_PORTS 128
++
++/* Number of __le16 words in a firmware portmap (128-bit bitmap). */
++#define MXL862XX_FW_PORTMAP_WORDS (MXL862XX_MAX_BRIDGE_PORTS / 16)
++
++/**
++ * mxl862xx_fw_portmap_from_bitmap - convert a kernel bitmap to a firmware
++ * portmap (__le16[8])
++ * @dst: firmware portmap array (MXL862XX_FW_PORTMAP_WORDS entries)
++ * @src: kernel bitmap of at least MXL862XX_MAX_BRIDGE_PORTS bits
++ */
++static inline void
++mxl862xx_fw_portmap_from_bitmap(__le16 *dst, const unsigned long *src)
++{
++ int i;
++
++ for (i = 0; i < MXL862XX_FW_PORTMAP_WORDS; i++)
++ dst[i] = cpu_to_le16(bitmap_read(src, i * 16, 16));
++}
++
++/**
++ * mxl862xx_fw_portmap_to_bitmap - convert a firmware portmap (__le16[8]) to
++ * a kernel bitmap
++ * @dst: kernel bitmap of at least MXL862XX_MAX_BRIDGE_PORTS bits
++ * @src: firmware portmap array (MXL862XX_FW_PORTMAP_WORDS entries)
++ */
++static inline void
++mxl862xx_fw_portmap_to_bitmap(unsigned long *dst, const __le16 *src)
++{
++ int i;
++
++ bitmap_zero(dst, MXL862XX_MAX_BRIDGE_PORTS);
++ for (i = 0; i < MXL862XX_FW_PORTMAP_WORDS; i++)
++ bitmap_write(dst, le16_to_cpu(src[i]), i * 16, 16);
++}
++
++/**
++ * mxl862xx_fw_portmap_set_bit - set a single port bit in a firmware portmap
++ * @map: firmware portmap array (MXL862XX_FW_PORTMAP_WORDS entries)
++ * @port: port index (0..MXL862XX_MAX_BRIDGE_PORTS-1)
++ */
++static inline void mxl862xx_fw_portmap_set_bit(__le16 *map, int port)
++{
++ map[port / 16] |= cpu_to_le16(BIT(port % 16));
++}
++
++/**
++ * mxl862xx_fw_portmap_clear_bit - clear a single port bit in a firmware portmap
++ * @map: firmware portmap array (MXL862XX_FW_PORTMAP_WORDS entries)
++ * @port: port index (0..MXL862XX_MAX_BRIDGE_PORTS-1)
++ */
++static inline void mxl862xx_fw_portmap_clear_bit(__le16 *map, int port)
++{
++ map[port / 16] &= ~cpu_to_le16(BIT(port % 16));
++}
++
++/**
++ * mxl862xx_fw_portmap_is_empty - check whether a firmware portmap has no
++ * bits set
++ * @map: firmware portmap array (MXL862XX_FW_PORTMAP_WORDS entries)
++ *
++ * Return: true if every word in @map is zero.
++ */
++static inline bool mxl862xx_fw_portmap_is_empty(const __le16 *map)
++{
++ int i;
++
++ for (i = 0; i < MXL862XX_FW_PORTMAP_WORDS; i++)
++ if (map[i])
++ return false;
++ return true;
++}
++
++/**
++ * struct mxl862xx_port - per-port state tracked by the driver
++ * @priv: back-pointer to switch private data; needed by
++ * deferred work handlers to access ds and priv
++ * @fid: firmware FID for the permanent single-port bridge;
++ * kept alive for the lifetime of the port so traffic is
++ * never forwarded while the port is unbridged
++ * @portmap: bitmap of switch port indices that share the current
++ * bridge with this port
++ * @flood_block: bitmask of firmware meter indices that are currently
++ * rate-limiting flood traffic on this port (zero-rate
++ * meters used to block flooding)
++ * @learning: true when address learning is enabled on this port
++ * @setup_done: set at end of port_setup, cleared at start of
++ * port_teardown; guards deferred work against
++ * acting on torn-down state
++ * @host_flood_uc: desired host unicast flood state (true = flood);
++ * updated atomically by port_set_host_flood, consumed
++ * by the deferred host_flood_work
++ * @host_flood_mc: desired host multicast flood state (true = flood)
++ * @host_flood_work: deferred work for applying host flood changes;
++ * port_set_host_flood runs in atomic context (under
++ * netif_addr_lock) so firmware calls must be deferred.
++ * The worker acquires rtnl_lock() to serialize with
++ * DSA callbacks and checks @setup_done to avoid
++ * acting on torn-down ports.
++ */
++struct mxl862xx_port {
++ struct mxl862xx_priv *priv;
++ u16 fid;
++ DECLARE_BITMAP(portmap, MXL862XX_MAX_BRIDGE_PORTS);
++ unsigned long flood_block;
++ bool learning;
++ bool setup_done;
++ bool host_flood_uc;
++ bool host_flood_mc;
++ struct work_struct host_flood_work;
++};
+
++/**
++ * struct mxl862xx_priv - driver private data for an MxL862xx switch
++ * @ds: pointer to the DSA switch instance
++ * @mdiodev: MDIO device used to communicate with the switch firmware
++ * @crc_err_work: deferred work for shutting down all ports on MDIO CRC errors
++ * @crc_err: set atomically before CRC-triggered shutdown, cleared after
++ * @drop_meter: index of the single shared zero-rate firmware meter used
++ * to unconditionally drop traffic (used to block flooding)
++ * @ports: per-port state, indexed by switch port number
++ * @bridges: maps DSA bridge number to firmware bridge ID;
++ * zero means no firmware bridge allocated for that
++ * DSA bridge number. Indexed by dsa_bridge.num
++ * (0 .. ds->max_num_bridges).
++ */
+ struct mxl862xx_priv {
+ struct dsa_switch *ds;
+ struct mdio_device *mdiodev;
+ struct work_struct crc_err_work;
+ unsigned long crc_err;
++ u16 drop_meter;
++ struct mxl862xx_port ports[MXL862XX_MAX_PORTS];
++ u16 bridges[MXL862XX_MAX_BRIDGES + 1];
+ };
+
+ #endif /* __MXL862XX_H */
--- /dev/null
+From 7286ac4f850339aac37dd52633f4a70816b621a8 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 10 Mar 2026 02:36:00 +0000
+Subject: [PATCH 14/35] net: dsa: mxl862xx: implement VLAN functionality
+
+Add VLAN support using both the Extended VLAN (EVLAN) engine and the
+VLAN Filter (VF) engine in a hybrid architecture that allows a higher
+number of VIDs than either engine could achieve alone.
+
+The VLAN Filter engine handles per-port VID membership checks with
+discard-unmatched semantics. The Extended VLAN engine handles PVID
+insertion on ingress (via fixed catchall rules) and tag stripping on
+egress (2 rules per untagged VID). Tagged-only VIDs need no EVLAN
+egress rules at all, so they consume only a VF entry.
+
+Both engines draw from shared 1024-entry hardware pools. The VF pool
+is divided equally among user ports for VID membership, while the
+EVLAN pool is partitioned into small fixed-size ingress blocks (7
+entries of catchall rules per port) and variable-size egress blocks
+for tag stripping.
+
+With 5 user ports this yields up to 204 VIDs per port (limited by VF),
+of which up to 98 can be untagged (limited by EVLAN egress budget).
+With 9 user ports the numbers are 113 total and 53 untagged.
+
+Wire up .port_vlan_add, .port_vlan_del, and .port_vlan_filtering.
+Reprogram all EVLAN rules when the PVID or filtering mode changes.
+Detach blocks from the bridge port before freeing them on bridge leave
+to satisfy the firmware's internal refcount.
+
+Future optimizations could increase VID capacity by dynamically sizing
+the egress EVLAN blocks based on actual per-port untagged VID counts
+rather than worst-case pre-allocation, or by sharing EVLAN egress and
+VLAN Filter blocks across ports with identical VID sets.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 329 ++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 12 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 960 +++++++++++++++++++++++-
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 110 ++-
+ 4 files changed, 1386 insertions(+), 25 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -732,6 +732,335 @@ struct mxl862xx_cfg {
+ } __packed;
+
+ /**
++ * enum mxl862xx_extended_vlan_filter_type - Extended VLAN filter tag type
++ * @MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NORMAL: Normal tagged
++ * @MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_FILTER: No filter (wildcard)
++ * @MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT: Default entry
++ * @MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_TAG: Untagged
++ */
++enum mxl862xx_extended_vlan_filter_type {
++ MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NORMAL = 0,
++ MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_FILTER = 1,
++ MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT = 2,
++ MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_TAG = 3,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_filter_tpid - Extended VLAN filter TPID
++ * @MXL862XX_EXTENDEDVLAN_FILTER_TPID_NO_FILTER: No TPID filter
++ * @MXL862XX_EXTENDEDVLAN_FILTER_TPID_8021Q: 802.1Q TPID
++ * @MXL862XX_EXTENDEDVLAN_FILTER_TPID_VTETYPE: VLAN type extension
++ */
++enum mxl862xx_extended_vlan_filter_tpid {
++ MXL862XX_EXTENDEDVLAN_FILTER_TPID_NO_FILTER = 0,
++ MXL862XX_EXTENDEDVLAN_FILTER_TPID_8021Q = 1,
++ MXL862XX_EXTENDEDVLAN_FILTER_TPID_VTETYPE = 2,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_filter_dei - Extended VLAN filter DEI
++ * @MXL862XX_EXTENDEDVLAN_FILTER_DEI_NO_FILTER: No DEI filter
++ * @MXL862XX_EXTENDEDVLAN_FILTER_DEI_0: DEI = 0
++ * @MXL862XX_EXTENDEDVLAN_FILTER_DEI_1: DEI = 1
++ */
++enum mxl862xx_extended_vlan_filter_dei {
++ MXL862XX_EXTENDEDVLAN_FILTER_DEI_NO_FILTER = 0,
++ MXL862XX_EXTENDEDVLAN_FILTER_DEI_0 = 1,
++ MXL862XX_EXTENDEDVLAN_FILTER_DEI_1 = 2,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_treatment_remove_tag - Tag removal action
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG: Do not remove tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG: Remove one tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_2_TAG: Remove two tags
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM: Discard frame
++ */
++enum mxl862xx_extended_vlan_treatment_remove_tag {
++ MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG = 0,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG = 1,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_2_TAG = 2,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM = 3,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_treatment_priority - Treatment priority mode
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_PRIORITY_VAL: Use explicit value
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_PRORITY: Copy from inner tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_PRORITY: Copy from outer tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_DSCP: Derive from DSCP
++ */
++enum mxl862xx_extended_vlan_treatment_priority {
++ MXL862XX_EXTENDEDVLAN_TREATMENT_PRIORITY_VAL = 0,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_PRORITY = 1,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_PRORITY = 2,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_DSCP = 3,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_treatment_vid - Treatment VID mode
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL: Use explicit VID value
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_VID: Copy from inner tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_VID: Copy from outer tag
++ */
++enum mxl862xx_extended_vlan_treatment_vid {
++ MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL = 0,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_VID = 1,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_VID = 2,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_treatment_tpid - Treatment TPID mode
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_TPID: Copy from inner tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_TPID: Copy from outer tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_VTETYPE: Use VLAN type extension
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q: Use 802.1Q TPID
++ */
++enum mxl862xx_extended_vlan_treatment_tpid {
++ MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_TPID = 0,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_TPID = 1,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_VTETYPE = 2,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q = 3,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_treatment_dei - Treatment DEI mode
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_DEI: Copy from inner tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_DEI: Copy from outer tag
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_DEI_0: Set DEI to 0
++ * @MXL862XX_EXTENDEDVLAN_TREATMENT_DEI_1: Set DEI to 1
++ */
++enum mxl862xx_extended_vlan_treatment_dei {
++ MXL862XX_EXTENDEDVLAN_TREATMENT_INNER_DEI = 0,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_OUTER_DEI = 1,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_DEI_0 = 2,
++ MXL862XX_EXTENDEDVLAN_TREATMENT_DEI_1 = 3,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_4_tpid_mode - 4-TPID mode selector
++ * @MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_1: VLAN TPID type 1
++ * @MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_2: VLAN TPID type 2
++ * @MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_3: VLAN TPID type 3
++ * @MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_4: VLAN TPID type 4
++ */
++enum mxl862xx_extended_vlan_4_tpid_mode {
++ MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_1 = 0,
++ MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_2 = 1,
++ MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_3 = 2,
++ MXL862XX_EXTENDEDVLAN_TPID_VTETYPE_4 = 3,
++};
++
++/**
++ * enum mxl862xx_extended_vlan_filter_ethertype - Filter EtherType match
++ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_NO_FILTER: No filter
++ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_IPOE: IPoE
++ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_PPPOE: PPPoE
++ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_ARP: ARP
++ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_IPV6IPOE: IPv6 IPoE
++ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_EAPOL: EAPOL
++ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_DHCPV4: DHCPv4
++ * @MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_DHCPV6: DHCPv6
++ */
++enum mxl862xx_extended_vlan_filter_ethertype {
++ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_NO_FILTER = 0,
++ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_IPOE = 1,
++ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_PPPOE = 2,
++ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_ARP = 3,
++ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_IPV6IPOE = 4,
++ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_EAPOL = 5,
++ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_DHCPV4 = 6,
++ MXL862XX_EXTENDEDVLAN_FILTER_ETHERTYPE_DHCPV6 = 7,
++};
++
++/**
++ * struct mxl862xx_extendedvlan_filter_vlan - Per-tag filter in Extended VLAN
++ * @type: Tag presence/type match (see &enum mxl862xx_extended_vlan_filter_type)
++ * @priority_enable: Enable PCP value matching
++ * @priority_val: PCP value to match
++ * @vid_enable: Enable VID matching
++ * @vid_val: VID value to match
++ * @tpid: TPID match mode (see &enum mxl862xx_extended_vlan_filter_tpid)
++ * @dei: DEI match mode (see &enum mxl862xx_extended_vlan_filter_dei)
++ */
++struct mxl862xx_extendedvlan_filter_vlan {
++ __le32 type;
++ u8 priority_enable;
++ __le32 priority_val;
++ u8 vid_enable;
++ __le32 vid_val;
++ __le32 tpid;
++ __le32 dei;
++} __packed;
++
++/**
++ * struct mxl862xx_extendedvlan_filter - Extended VLAN filter configuration
++ * @original_packet_filter_mode: If true, filter on original (pre-treatment)
++ * packet
++ * @filter_4_tpid_mode: 4-TPID mode (see &enum mxl862xx_extended_vlan_4_tpid_mode)
++ * @outer_vlan: Outer VLAN tag filter
++ * @inner_vlan: Inner VLAN tag filter
++ * @ether_type: EtherType filter (see
++ * &enum mxl862xx_extended_vlan_filter_ethertype)
++ */
++struct mxl862xx_extendedvlan_filter {
++ u8 original_packet_filter_mode;
++ __le32 filter_4_tpid_mode;
++ struct mxl862xx_extendedvlan_filter_vlan outer_vlan;
++ struct mxl862xx_extendedvlan_filter_vlan inner_vlan;
++ __le32 ether_type;
++} __packed;
++
++/**
++ * struct mxl862xx_extendedvlan_treatment_vlan - Per-tag treatment in
++ * Extended VLAN
++ * @priority_mode: Priority assignment mode
++ * (see &enum mxl862xx_extended_vlan_treatment_priority)
++ * @priority_val: Priority value (when mode is VAL)
++ * @vid_mode: VID assignment mode
++ * (see &enum mxl862xx_extended_vlan_treatment_vid)
++ * @vid_val: VID value (when mode is VAL)
++ * @tpid: TPID assignment mode
++ * (see &enum mxl862xx_extended_vlan_treatment_tpid)
++ * @dei: DEI assignment mode
++ * (see &enum mxl862xx_extended_vlan_treatment_dei)
++ */
++struct mxl862xx_extendedvlan_treatment_vlan {
++ __le32 priority_mode;
++ __le32 priority_val;
++ __le32 vid_mode;
++ __le32 vid_val;
++ __le32 tpid;
++ __le32 dei;
++} __packed;
++
++/**
++ * struct mxl862xx_extendedvlan_treatment - Extended VLAN treatment
++ * @remove_tag: Tag removal action
++ * (see &enum mxl862xx_extended_vlan_treatment_remove_tag)
++ * @treatment_4_tpid_mode: 4-TPID treatment mode
++ * @add_outer_vlan: Add outer VLAN tag
++ * @outer_vlan: Outer VLAN tag treatment parameters
++ * @add_inner_vlan: Add inner VLAN tag
++ * @inner_vlan: Inner VLAN tag treatment parameters
++ * @reassign_bridge_port: Reassign to different bridge port
++ * @new_bridge_port_id: New bridge port ID
++ * @new_dscp_enable: Enable new DSCP assignment
++ * @new_dscp: New DSCP value
++ * @new_traffic_class_enable: Enable new traffic class assignment
++ * @new_traffic_class: New traffic class value
++ * @new_meter_enable: Enable new metering
++ * @s_new_traffic_meter_id: New traffic meter ID
++ * @dscp2pcp_map: DSCP to PCP mapping table (64 entries)
++ * @loopback_enable: Enable loopback
++ * @da_sa_swap_enable: Enable DA/SA swap
++ * @mirror_enable: Enable mirroring
++ */
++struct mxl862xx_extendedvlan_treatment {
++ __le32 remove_tag;
++ __le32 treatment_4_tpid_mode;
++ u8 add_outer_vlan;
++ struct mxl862xx_extendedvlan_treatment_vlan outer_vlan;
++ u8 add_inner_vlan;
++ struct mxl862xx_extendedvlan_treatment_vlan inner_vlan;
++ u8 reassign_bridge_port;
++ __le16 new_bridge_port_id;
++ u8 new_dscp_enable;
++ __le16 new_dscp;
++ u8 new_traffic_class_enable;
++ u8 new_traffic_class;
++ u8 new_meter_enable;
++ __le16 s_new_traffic_meter_id;
++ u8 dscp2pcp_map[64];
++ u8 loopback_enable;
++ u8 da_sa_swap_enable;
++ u8 mirror_enable;
++} __packed;
++
++/**
++ * struct mxl862xx_extendedvlan_alloc - Extended VLAN block allocation
++ * @number_of_entries: Number of entries to allocate (input) / allocated
++ * (output)
++ * @extended_vlan_block_id: Block ID assigned by firmware (output on alloc,
++ * input on free)
++ *
++ * Used with %MXL862XX_EXTENDEDVLAN_ALLOC and %MXL862XX_EXTENDEDVLAN_FREE.
++ */
++struct mxl862xx_extendedvlan_alloc {
++ __le16 number_of_entries;
++ __le16 extended_vlan_block_id;
++} __packed;
++
++/**
++ * struct mxl862xx_extendedvlan_config - Extended VLAN entry configuration
++ * @extended_vlan_block_id: Block ID from allocation
++ * @entry_index: Entry index within the block
++ * @filter: Filter (match) configuration
++ * @treatment: Treatment (action) configuration
++ *
++ * Used with %MXL862XX_EXTENDEDVLAN_SET and %MXL862XX_EXTENDEDVLAN_GET.
++ */
++struct mxl862xx_extendedvlan_config {
++ __le16 extended_vlan_block_id;
++ __le16 entry_index;
++ struct mxl862xx_extendedvlan_filter filter;
++ struct mxl862xx_extendedvlan_treatment treatment;
++} __packed;
++
++/**
++ * enum mxl862xx_vlan_filter_tci_mask - VLAN Filter TCI mask
++ * @MXL862XX_VLAN_FILTER_TCI_MASK_VID: TCI mask for VLAN ID
++ * @MXL862XX_VLAN_FILTER_TCI_MASK_PCP: TCI mask for VLAN PCP
++ * @MXL862XX_VLAN_FILTER_TCI_MASK_TCI: TCI mask for VLAN TCI
++ */
++enum mxl862xx_vlan_filter_tci_mask {
++ MXL862XX_VLAN_FILTER_TCI_MASK_VID = 0,
++ MXL862XX_VLAN_FILTER_TCI_MASK_PCP = 1,
++ MXL862XX_VLAN_FILTER_TCI_MASK_TCI = 2,
++};
++
++/**
++ * struct mxl862xx_vlanfilter_alloc - VLAN Filter block allocation
++ * @number_of_entries: Number of entries to allocate (input) / allocated
++ * (output)
++ * @vlan_filter_block_id: Block ID assigned by firmware (output on alloc,
++ * input on free)
++ * @discard_untagged: Discard untagged packets
++ * @discard_unmatched_tagged: Discard tagged packets that do not match any
++ * entry in the block
++ * @use_default_port_vid: Use default port VLAN ID for filtering
++ *
++ * Used with %MXL862XX_VLANFILTER_ALLOC and %MXL862XX_VLANFILTER_FREE.
++ */
++struct mxl862xx_vlanfilter_alloc {
++ __le16 number_of_entries;
++ __le16 vlan_filter_block_id;
++ u8 discard_untagged;
++ u8 discard_unmatched_tagged;
++ u8 use_default_port_vid;
++} __packed;
++
++/**
++ * struct mxl862xx_vlanfilter_config - VLAN Filter entry configuration
++ * @vlan_filter_block_id: Block ID from allocation
++ * @entry_index: Entry index within the block
++ * @vlan_filter_mask: TCI field(s) to match (see
++ * &enum mxl862xx_vlan_filter_tci_mask)
++ * @val: TCI value(s) to match (VID, PCP, or full TCI depending on mask)
++ * @discard_matched: When true, discard frames matching this entry;
++ * when false, allow them
++ *
++ * Used with %MXL862XX_VLANFILTER_SET and %MXL862XX_VLANFILTER_GET.
++ */
++struct mxl862xx_vlanfilter_config {
++ __le16 vlan_filter_block_id;
++ __le16 entry_index;
++ __le32 vlan_filter_mask; /* enum mxl862xx_vlan_filter_tci_mask */
++ __le32 val;
++ u8 discard_matched;
++} __packed;
++
++/**
+ * enum mxl862xx_ss_sp_tag_mask - Special tag valid field indicator bits
+ * @MXL862XX_SS_SP_TAG_MASK_RX: valid RX special tag mode
+ * @MXL862XX_SS_SP_TAG_MASK_TX: valid TX special tag mode
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -17,6 +17,8 @@
+ #define MXL862XX_CTP_MAGIC 0x500
+ #define MXL862XX_QOS_MAGIC 0x600
+ #define MXL862XX_SWMAC_MAGIC 0xa00
++#define MXL862XX_EXTVLAN_MAGIC 0xb00
++#define MXL862XX_VLANFILTER_MAGIC 0xc00
+ #define MXL862XX_STP_MAGIC 0xf00
+ #define MXL862XX_SS_MAGIC 0x1600
+ #define GPY_GPY2XX_MAGIC 0x1800
+@@ -47,6 +49,16 @@
+ #define MXL862XX_MAC_TABLEENTRYREMOVE (MXL862XX_SWMAC_MAGIC + 0x5)
+ #define MXL862XX_MAC_TABLECLEARCOND (MXL862XX_SWMAC_MAGIC + 0x8)
+
++#define MXL862XX_EXTENDEDVLAN_ALLOC (MXL862XX_EXTVLAN_MAGIC + 0x1)
++#define MXL862XX_EXTENDEDVLAN_SET (MXL862XX_EXTVLAN_MAGIC + 0x2)
++#define MXL862XX_EXTENDEDVLAN_GET (MXL862XX_EXTVLAN_MAGIC + 0x3)
++#define MXL862XX_EXTENDEDVLAN_FREE (MXL862XX_EXTVLAN_MAGIC + 0x4)
++
++#define MXL862XX_VLANFILTER_ALLOC (MXL862XX_VLANFILTER_MAGIC + 0x1)
++#define MXL862XX_VLANFILTER_SET (MXL862XX_VLANFILTER_MAGIC + 0x2)
++#define MXL862XX_VLANFILTER_GET (MXL862XX_VLANFILTER_MAGIC + 0x3)
++#define MXL862XX_VLANFILTER_FREE (MXL862XX_VLANFILTER_MAGIC + 0x4)
++
+ #define MXL862XX_SS_SPTAG_SET (MXL862XX_SS_MAGIC + 0x2)
+
+ #define MXL862XX_STP_PORTCFGSET (MXL862XX_STP_MAGIC + 0x2)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -50,6 +50,88 @@ static const int mxl862xx_flood_meters[]
+ MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST,
+ };
+
++enum mxl862xx_evlan_action {
++ EVLAN_ACCEPT, /* pass-through, no tag removal */
++ EVLAN_STRIP_IF_UNTAGGED, /* remove 1 tag if entry's untagged flag set */
++ EVLAN_DISCARD, /* discard upstream */
++ EVLAN_PVID_OR_DISCARD, /* insert PVID tag or discard if no PVID */
++ EVLAN_PVID_OR_PASS, /* insert PVID tag or pass-through */
++ EVLAN_STRIP1_AND_PVID_OR_DISCARD,/* strip 1 tag + insert PVID, or discard */
++};
++
++struct mxl862xx_evlan_rule_desc {
++ u8 outer_type; /* enum mxl862xx_extended_vlan_filter_type */
++ u8 inner_type; /* enum mxl862xx_extended_vlan_filter_type */
++ u8 outer_tpid; /* enum mxl862xx_extended_vlan_filter_tpid */
++ u8 inner_tpid; /* enum mxl862xx_extended_vlan_filter_tpid */
++ bool match_vid; /* true: match on VID from the vid parameter */
++ u8 action; /* enum mxl862xx_evlan_action */
++};
++
++/* Shorthand constants for readability */
++#define FT_NORMAL MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NORMAL
++#define FT_NO_FILTER MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_FILTER
++#define FT_DEFAULT MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT
++#define FT_NO_TAG MXL862XX_EXTENDEDVLAN_FILTER_TYPE_NO_TAG
++#define TP_NONE MXL862XX_EXTENDEDVLAN_FILTER_TPID_NO_FILTER
++#define TP_8021Q MXL862XX_EXTENDEDVLAN_FILTER_TPID_8021Q
++
++/*
++ * VLAN-aware ingress: 7 final catchall rules.
++ *
++ * VLAN Filter handles VID membership for tagged frames, so the
++ * Extended VLAN ingress block only needs to handle:
++ * - Priority-tagged (VID=0): strip + insert PVID
++ * - Untagged: insert PVID or discard
++ * - Standard 802.1Q VID>0: pass through (VF handles membership)
++ * - Non-8021Q TPID (0x88A8 etc.): treat as untagged
++ *
++ * Rule ordering is critical: the EVLAN engine scans entries in
++ * ascending index order and stops at the first match.
++ *
++ * The 802.1Q ACCEPT rules (indices 3--4) must appear BEFORE the
++ * NO_FILTER catchalls (indices 5--6). NO_FILTER matches any tag
++ * regardless of TPID, so without the ACCEPT guard, it would also
++ * catch standard 802.1Q VID>0 frames and corrupt them. With the
++ * guard, 802.1Q VID>0 frames match the ACCEPT rules first and
++ * pass through untouched; only non-8021Q TPID frames fall through
++ * to the NO_FILTER catchalls.
++ */
++static const struct mxl862xx_evlan_rule_desc ingress_aware_final[] = {
++ /* 802.1p / priority-tagged (VID 0): strip + PVID */
++ { FT_NORMAL, FT_NORMAL, TP_8021Q, TP_8021Q, true, EVLAN_STRIP1_AND_PVID_OR_DISCARD },
++ { FT_NORMAL, FT_NO_TAG, TP_8021Q, TP_NONE, true, EVLAN_STRIP1_AND_PVID_OR_DISCARD },
++ /* Untagged: PVID insertion or discard */
++ { FT_NO_TAG, FT_NO_TAG, TP_NONE, TP_NONE, false, EVLAN_PVID_OR_DISCARD },
++ /* 802.1Q VID>0: accept - VF handles membership.
++ * match_vid=false means any VID; VID=0 is already caught above.
++ */
++ { FT_NORMAL, FT_NORMAL, TP_8021Q, TP_8021Q, false, EVLAN_ACCEPT },
++ { FT_NORMAL, FT_NO_TAG, TP_8021Q, TP_NONE, false, EVLAN_ACCEPT },
++ /* Non-8021Q TPID (0x88A8 etc.): treat as untagged - strip + PVID */
++ { FT_NO_FILTER, FT_NO_FILTER, TP_NONE, TP_NONE, false, EVLAN_STRIP1_AND_PVID_OR_DISCARD },
++ { FT_NO_FILTER, FT_NO_TAG, TP_NONE, TP_NONE, false, EVLAN_STRIP1_AND_PVID_OR_DISCARD },
++};
++
++/*
++ * VID-specific accept rules (VLAN-aware, standard tag, 2 per VID).
++ * Outer tag carries the VLAN; inner may or may not be present.
++ */
++static const struct mxl862xx_evlan_rule_desc vid_accept_standard[] = {
++ { FT_NORMAL, FT_NORMAL, TP_8021Q, TP_8021Q, true, EVLAN_STRIP_IF_UNTAGGED },
++ { FT_NORMAL, FT_NO_TAG, TP_8021Q, TP_NONE, true, EVLAN_STRIP_IF_UNTAGGED },
++};
++
++/*
++ * VID-specific accept rules for VLAN-unaware egress.
++ * The HW sees the MxL tag as outer, real VLAN tag as inner.
++ * match on inner VID with outer=NO_FILTER.
++ */
++static const struct mxl862xx_evlan_rule_desc vid_accept_egress_unaware[] = {
++ { FT_NO_FILTER, FT_NORMAL, TP_NONE, TP_8021Q, true, EVLAN_STRIP_IF_UNTAGGED },
++ { FT_NO_FILTER, FT_NO_TAG, TP_NONE, TP_NONE, true, EVLAN_STRIP_IF_UNTAGGED },
++};
++
+ static enum dsa_tag_protocol mxl862xx_get_tag_protocol(struct dsa_switch *ds,
+ int port,
+ enum dsa_tag_protocol m)
+@@ -275,6 +357,7 @@ static int mxl862xx_set_bridge_port(stru
+ struct mxl862xx_port *p = &priv->ports[port];
+ u16 bridge_id = dp->bridge ?
+ priv->bridges[dp->bridge->num] : p->fid;
++ u16 vf_scan;
+ bool enable;
+ int i, idx;
+
+@@ -283,9 +366,69 @@ static int mxl862xx_set_bridge_port(stru
+ br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING |
+- MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER);
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_INGRESS_VLAN_FILTER |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN_FILTER1 |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING);
+ br_port_cfg.src_mac_learning_disable = !p->learning;
+
++ /* Extended VLAN block assignments.
++ * Ingress: block_size is sent as-is (all entries are finals).
++ * Egress: n_active narrows the scan window to only the
++ * entries actually written by evlan_program_egress.
++ */
++ br_port_cfg.ingress_extended_vlan_enable = p->ingress_evlan.in_use;
++ br_port_cfg.ingress_extended_vlan_block_id =
++ cpu_to_le16(p->ingress_evlan.block_id);
++ br_port_cfg.ingress_extended_vlan_block_size =
++ cpu_to_le16(p->ingress_evlan.block_size);
++ br_port_cfg.egress_extended_vlan_enable = p->egress_evlan.in_use;
++ br_port_cfg.egress_extended_vlan_block_id =
++ cpu_to_le16(p->egress_evlan.block_id);
++ br_port_cfg.egress_extended_vlan_block_size =
++ cpu_to_le16(p->egress_evlan.n_active);
++
++ /* VLAN Filter block assignments (per-port).
++ * The block_size sent to the firmware narrows the HW scan
++ * window to [block_id, block_id + active_count), relying on
++ * discard_unmatched_tagged for frames outside that range.
++ * When active_count=0, send 1 to scan only the DISCARD
++ * sentinel at index 0 (block_size=0 would disable narrowing
++ * and scan the entire allocated block).
++ *
++ * The bridge check ensures VF is disabled when the port
++ * leaves the bridge, without needing to prematurely clear
++ * vlan_filtering (which the DSA framework handles later via
++ * port_vlan_filtering).
++ */
++ if (p->vf.allocated && p->vlan_filtering &&
++ dsa_port_bridge_dev_get(dp)) {
++ vf_scan = max_t(u16, p->vf.active_count, 1);
++ br_port_cfg.ingress_vlan_filter_enable = 1;
++ br_port_cfg.ingress_vlan_filter_block_id =
++ cpu_to_le16(p->vf.block_id);
++ br_port_cfg.ingress_vlan_filter_block_size =
++ cpu_to_le16(vf_scan);
++
++ br_port_cfg.egress_vlan_filter1enable = 1;
++ br_port_cfg.egress_vlan_filter1block_id =
++ cpu_to_le16(p->vf.block_id);
++ br_port_cfg.egress_vlan_filter1block_size =
++ cpu_to_le16(vf_scan);
++ } else {
++ br_port_cfg.ingress_vlan_filter_enable = 0;
++ br_port_cfg.egress_vlan_filter1enable = 0;
++ }
++
++ /* IVL when VLAN-aware: include VID in FDB lookup keys so that
++ * learned entries are per-VID. In VLAN-unaware mode, SVL is
++ * used (VID excluded from key).
++ */
++ br_port_cfg.vlan_src_mac_vid_enable = p->vlan_filtering;
++ br_port_cfg.vlan_dst_mac_vid_enable = p->vlan_filtering;
++
+ mxl862xx_fw_portmap_from_bitmap(br_port_cfg.bridge_port_map, p->portmap);
+
+ for (i = 0; i < ARRAY_SIZE(mxl862xx_flood_meters); i++) {
+@@ -329,13 +472,131 @@ static int mxl862xx_sync_bridge_members(
+ return ret;
+ }
+
++static void mxl862xx_evlan_block_init(struct mxl862xx_evlan_block *blk,
++ u16 size)
++{
++ blk->allocated = false;
++ blk->in_use = false;
++ blk->block_id = 0;
++ blk->block_size = size;
++ blk->n_active = 0;
++}
++
++static int mxl862xx_evlan_block_alloc(struct mxl862xx_priv *priv,
++ struct mxl862xx_evlan_block *blk)
++{
++ struct mxl862xx_extendedvlan_alloc param = {};
++ int ret;
++
++ param.number_of_entries = cpu_to_le16(blk->block_size);
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_EXTENDEDVLAN_ALLOC, param);
++ if (ret)
++ return ret;
++
++ blk->block_id = le16_to_cpu(param.extended_vlan_block_id);
++ blk->allocated = true;
++
++ return 0;
++}
++
++/**
++ * mxl862xx_vf_init - Initialize per-port VF block software state
++ * @vf: VLAN Filter block to initialize
++ * @size: block size (entries per port)
++ */
++static void mxl862xx_vf_init(struct mxl862xx_vf_block *vf, u16 size)
++{
++ vf->allocated = false;
++ vf->block_id = 0;
++ vf->block_size = size;
++ vf->active_count = 0;
++ INIT_LIST_HEAD(&vf->vids);
++}
++
++/**
++ * mxl862xx_vf_block_alloc - Allocate a VLAN Filter block from firmware
++ * @priv: driver private data
++ * @size: number of entries to allocate
++ * @block_id: output -- block ID assigned by firmware
++ *
++ * Allocates a contiguous VLAN Filter block and configures it to discard
++ * unmatched tagged frames (VID membership enforcement).
++ */
++static int mxl862xx_vf_block_alloc(struct mxl862xx_priv *priv,
++ u16 size, u16 *block_id)
++{
++ struct mxl862xx_vlanfilter_alloc param = {};
++ int ret;
++
++ param.number_of_entries = cpu_to_le16(size);
++ param.discard_untagged = 0;
++ param.discard_unmatched_tagged = 1;
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_VLANFILTER_ALLOC, param);
++ if (ret)
++ return ret;
++
++ *block_id = le16_to_cpu(param.vlan_filter_block_id);
++ return 0;
++}
++
++/**
++ * mxl862xx_vf_entry_discard - Write a DISCARD entry to plug an unused slot
++ * @priv: driver private data
++ * @block_id: HW VLAN Filter block ID
++ * @index: entry index within the block
++ *
++ * Unwritten VLAN Filter entries default to VID=0 / ALLOW which would
++ * leak VID 0 traffic. This writes a DISCARD entry to plug the slot.
++ */
++static int mxl862xx_vf_entry_discard(struct mxl862xx_priv *priv,
++ u16 block_id, u16 index)
++{
++ struct mxl862xx_vlanfilter_config cfg = {};
++
++ cfg.vlan_filter_block_id = cpu_to_le16(block_id);
++ cfg.entry_index = cpu_to_le16(index);
++ cfg.vlan_filter_mask = cpu_to_le32(MXL862XX_VLAN_FILTER_TCI_MASK_VID);
++ cfg.val = cpu_to_le32(0);
++ cfg.discard_matched = 1;
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_VLANFILTER_SET, cfg);
++}
++
++/**
++ * mxl862xx_vf_alloc - Allocate the port's VF HW block
++ * @priv: driver private data
++ * @vf: VLAN Filter block (must have been initialized via mxl862xx_vf_init)
++ *
++ * Allocates the block and writes a DISCARD sentinel at index 0 so that
++ * when active_count is 0, the single-entry scan window blocks VID-0
++ * (which would otherwise match the zeroed-out default and be allowed).
++ * Called once per port from port_setup.
++ */
++static int mxl862xx_vf_alloc(struct mxl862xx_priv *priv,
++ struct mxl862xx_vf_block *vf)
++{
++ int ret;
++
++ ret = mxl862xx_vf_block_alloc(priv, vf->block_size, &vf->block_id);
++ if (ret)
++ return ret;
++
++ vf->allocated = true;
++ vf->active_count = 0;
++
++ /* Sentinel: block VID-0 when scan window covers only index 0 */
++ return mxl862xx_vf_entry_discard(priv, vf->block_id, 0);
++}
++
+ /**
+ * mxl862xx_allocate_bridge - Allocate a firmware bridge instance
+ * @priv: driver private data
+ * @bridge_id: output -- firmware bridge ID assigned by the firmware
+ *
+ * Newly allocated bridges default to flooding all traffic classes
+- * (unknown unicast, multicast, broadcast). Callers that need
++ * (unknown unicast, multicast, broadcast). Callers that need
+ * different forwarding behavior must call mxl862xx_bridge_config_fwd()
+ * after allocation.
+ *
+@@ -404,6 +665,9 @@ static int mxl862xx_add_single_port_brid
+ static int mxl862xx_setup(struct dsa_switch *ds)
+ {
+ struct mxl862xx_priv *priv = ds->priv;
++ int n_user_ports = 0, max_vlans;
++ int ingress_finals, vid_rules;
++ struct dsa_port *dp;
+ int ret;
+
+ ret = mxl862xx_reset(priv);
+@@ -414,6 +678,50 @@ static int mxl862xx_setup(struct dsa_swi
+ if (ret)
+ return ret;
+
++ /* Calculate Extended VLAN block sizes.
++ * With VLAN Filter handling VID membership checks:
++ * Ingress: only final catchall rules (PVID insertion, 802.1Q
++ * accept, non-8021Q TPID handling, discard).
++ * Block sized to exactly fit the finals -- no per-VID
++ * ingress EVLAN rules are needed. (7 entries.)
++ * Egress: 2 rules per VID that needs tag stripping (untagged VIDs).
++ * No egress final catchalls -- VLAN Filter does the discard.
++ * CPU: EVLAN is left disabled on CPU ports -- frames pass
++ * through without EVLAN processing.
++ *
++ * Total EVLAN budget:
++ * n_user_ports * (ingress + egress) ≤ 1024.
++ * Ingress blocks are small (7 entries), so almost all capacity
++ * goes to egress VID rules.
++ */
++ dsa_switch_for_each_user_port(dp, ds)
++ n_user_ports++;
++
++ if (n_user_ports) {
++ ingress_finals = ARRAY_SIZE(ingress_aware_final);
++ vid_rules = ARRAY_SIZE(vid_accept_standard);
++
++ /* Ingress block: fixed at finals count (7 entries) */
++ priv->evlan_ingress_size = ingress_finals;
++
++ /* Egress block: remaining budget divided equally among
++ * user ports. Each untagged VID needs vid_rules (2)
++ * EVLAN entries for tag stripping. Tagged-only VIDs
++ * need no EVLAN rules at all.
++ */
++ max_vlans = (MXL862XX_TOTAL_EVLAN_ENTRIES -
++ n_user_ports * ingress_finals) /
++ (n_user_ports * vid_rules);
++ priv->evlan_egress_size = vid_rules * max_vlans;
++
++ /* VLAN Filter block: one per user port. The 1024-entry
++ * table is divided equally among user ports. Each port
++ * gets its own VF block for per-port VID membership --
++ * discard_unmatched_tagged handles the rest.
++ */
++ priv->vf_block_size = MXL862XX_TOTAL_VF_ENTRIES / n_user_ports;
++ }
++
+ ret = mxl862xx_setup_drop_meter(ds);
+ if (ret)
+ return ret;
+@@ -495,27 +803,616 @@ static int mxl862xx_configure_sp_tag_pro
+ return MXL862XX_API_WRITE(ds->priv, MXL862XX_SS_SPTAG_SET, tag);
+ }
+
++/**
++ * mxl862xx_evlan_write_rule - Write a single Extended VLAN rule to hardware
++ * @priv: driver private data
++ * @block_id: HW Extended VLAN block ID
++ * @entry_index: entry index within the block
++ * @desc: rule descriptor (filter type + action)
++ * @vid: VLAN ID for VID-specific rules (ignored when !desc->match_vid)
++ * @untagged: strip tag on egress for EVLAN_STRIP_IF_UNTAGGED action
++ * @pvid: port VLAN ID for PVID insertion rules (0 = no PVID)
++ *
++ * Translates a compact rule descriptor into a full firmware
++ * mxl862xx_extendedvlan_config struct and writes it via the API.
++ */
++static int mxl862xx_evlan_write_rule(struct mxl862xx_priv *priv,
++ u16 block_id, u16 entry_index,
++ const struct mxl862xx_evlan_rule_desc *desc,
++ u16 vid, bool untagged, u16 pvid)
++{
++ struct mxl862xx_extendedvlan_config cfg = {};
++ struct mxl862xx_extendedvlan_filter_vlan *fv;
++
++ cfg.extended_vlan_block_id = cpu_to_le16(block_id);
++ cfg.entry_index = cpu_to_le16(entry_index);
++
++ /* Populate filter */
++ cfg.filter.outer_vlan.type = cpu_to_le32(desc->outer_type);
++ cfg.filter.inner_vlan.type = cpu_to_le32(desc->inner_type);
++ cfg.filter.outer_vlan.tpid = cpu_to_le32(desc->outer_tpid);
++ cfg.filter.inner_vlan.tpid = cpu_to_le32(desc->inner_tpid);
++
++ if (desc->match_vid) {
++ /* For egress unaware: outer=NO_FILTER, match on inner tag */
++ if (desc->outer_type == FT_NO_FILTER)
++ fv = &cfg.filter.inner_vlan;
++ else
++ fv = &cfg.filter.outer_vlan;
++
++ fv->vid_enable = 1;
++ fv->vid_val = cpu_to_le32(vid);
++ }
++
++ /* Populate treatment based on action */
++ switch (desc->action) {
++ case EVLAN_ACCEPT:
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
++ break;
++
++ case EVLAN_STRIP_IF_UNTAGGED:
++ cfg.treatment.remove_tag = cpu_to_le32(untagged ?
++ MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG :
++ MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
++ break;
++
++ case EVLAN_DISCARD:
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM);
++ break;
++
++ case EVLAN_PVID_OR_DISCARD:
++ if (pvid) {
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
++ cfg.treatment.add_outer_vlan = 1;
++ cfg.treatment.outer_vlan.vid_mode =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL);
++ cfg.treatment.outer_vlan.vid_val = cpu_to_le32(pvid);
++ cfg.treatment.outer_vlan.tpid =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q);
++ } else {
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM);
++ }
++ break;
++
++ case EVLAN_PVID_OR_PASS:
++ if (pvid) {
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
++ cfg.treatment.add_outer_vlan = 1;
++ cfg.treatment.outer_vlan.vid_mode =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL);
++ cfg.treatment.outer_vlan.vid_val = cpu_to_le32(pvid);
++ cfg.treatment.outer_vlan.tpid =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q);
++ } else {
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
++ }
++ break;
++
++ case EVLAN_STRIP1_AND_PVID_OR_DISCARD:
++ if (pvid) {
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG);
++ cfg.treatment.add_outer_vlan = 1;
++ cfg.treatment.outer_vlan.vid_mode =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL);
++ cfg.treatment.outer_vlan.vid_val = cpu_to_le32(pvid);
++ cfg.treatment.outer_vlan.tpid =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q);
++ } else {
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM);
++ }
++ break;
++ }
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_EXTENDEDVLAN_SET, cfg);
++}
++
++/**
++ * mxl862xx_evlan_deactivate_entry - Reset an Extended VLAN entry to no-op
++ * @priv: driver private data
++ * @block_id: HW Extended VLAN block ID
++ * @entry_index: entry index within the block
++ *
++ * Writes a zeroed-out config to the firmware, which deactivates the
++ * rule (making it transparent / no-op).
++ */
++static int mxl862xx_evlan_deactivate_entry(struct mxl862xx_priv *priv,
++ u16 block_id, u16 entry_index)
++{
++ struct mxl862xx_extendedvlan_config cfg = {};
++
++ cfg.extended_vlan_block_id = cpu_to_le16(block_id);
++ cfg.entry_index = cpu_to_le16(entry_index);
++
++ /* Use an unreachable filter (DEFAULT+DEFAULT) with DISCARD treatment.
++ * A zeroed entry would have NORMAL+NORMAL filter which matches
++ * real double-tagged traffic and passes it through.
++ */
++ cfg.filter.outer_vlan.type =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT);
++ cfg.filter.inner_vlan.type =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_FILTER_TYPE_DEFAULT);
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM);
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_EXTENDEDVLAN_SET, cfg);
++}
++
++/**
++ * mxl862xx_evlan_write_final_rules - Write catchall rules to the ingress block
++ * @priv: driver private data
++ * @blk: Extended VLAN block (already allocated)
++ * @rules: array of rule descriptors for the final rules
++ * @n_rules: number of final rules
++ * @pvid: port VLAN ID (for PVID insertion rules)
++ *
++ * Writes final catchall rules starting at block_size - n_rules. With
++ * VLAN Filter handling VID membership, only the ingress block uses
++ * finals, and the block is sized to exactly fit them (no VID entries),
++ * so the rules fill the entire block.
++ */
++static int mxl862xx_evlan_write_final_rules(struct mxl862xx_priv *priv,
++ struct mxl862xx_evlan_block *blk,
++ const struct mxl862xx_evlan_rule_desc *rules,
++ int n_rules, u16 pvid)
++{
++ u16 start_idx = blk->block_size - n_rules;
++ int i, ret;
++
++ for (i = 0; i < n_rules; i++) {
++ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
++ start_idx + i, &rules[i],
++ 0, false, pvid);
++ if (ret)
++ return ret;
++ }
++
++ return 0;
++}
++
++/**
++ * mxl862xx_vf_entry_set - Write a single VLAN Filter entry
++ * @priv: driver private data
++ * @block_id: HW VLAN Filter block ID
++ * @index: entry index within the block
++ * @vid: VLAN ID to allow
++ *
++ * Writes an ALLOW entry (discard_matched=false) for the given VID.
++ */
++static int mxl862xx_vf_entry_set(struct mxl862xx_priv *priv,
++ u16 block_id, u16 index, u16 vid)
++{
++ struct mxl862xx_vlanfilter_config cfg = {};
++
++ cfg.vlan_filter_block_id = cpu_to_le16(block_id);
++ cfg.entry_index = cpu_to_le16(index);
++ cfg.vlan_filter_mask = cpu_to_le32(MXL862XX_VLAN_FILTER_TCI_MASK_VID);
++ cfg.val = cpu_to_le32(vid);
++ cfg.discard_matched = 0;
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_VLANFILTER_SET, cfg);
++}
++
++/**
++ * mxl862xx_vf_find_vid - Find a VID entry in a VF block
++ * @vf: VLAN Filter block to search
++ * @vid: VLAN ID to find
++ */
++static struct mxl862xx_vf_vid *
++mxl862xx_vf_find_vid(struct mxl862xx_vf_block *vf, u16 vid)
++{
++ struct mxl862xx_vf_vid *ve;
++
++ list_for_each_entry(ve, &vf->vids, list)
++ if (ve->vid == vid)
++ return ve;
++
++ return NULL;
++}
++
++/**
++ * mxl862xx_vf_add_vid - Add a VID to a port's VLAN Filter block
++ * @priv: driver private data
++ * @vf: VLAN Filter block
++ * @vid: VLAN ID to add
++ * @untagged: whether this VID should strip tags on egress
++ *
++ * Idempotent. Writes an ALLOW entry at active_count and increments
++ * active_count. If the VID already exists, only the untagged flag
++ * is updated. The HW block must be allocated before calling this.
++ */
++static int mxl862xx_vf_add_vid(struct mxl862xx_priv *priv,
++ struct mxl862xx_vf_block *vf,
++ u16 vid, bool untagged)
++{
++ struct mxl862xx_vf_vid *ve;
++ int ret;
++
++ ve = mxl862xx_vf_find_vid(vf, vid);
++ if (ve) {
++ ve->untagged = untagged;
++ return 0;
++ }
++
++ if (vf->active_count >= vf->block_size)
++ return -ENOSPC;
++
++ ve = kzalloc(sizeof(*ve), GFP_KERNEL);
++ if (!ve)
++ return -ENOMEM;
++
++ ve->vid = vid;
++ ve->index = vf->active_count;
++ ve->untagged = untagged;
++
++ ret = mxl862xx_vf_entry_set(priv, vf->block_id, ve->index, vid);
++ if (ret) {
++ kfree(ve);
++ return ret;
++ }
++
++ list_add_tail(&ve->list, &vf->vids);
++ vf->active_count++;
++
++ return 0;
++}
++
++/**
++ * mxl862xx_vf_del_vid - Remove a VID from a port's VLAN Filter block
++ * @priv: driver private data
++ * @vf: VLAN Filter block
++ * @vid: VLAN ID to remove
++ *
++ * Swap-compacts: the last active entry is moved into the gap,
++ * active_count is decremented, and the old last slot is plugged
++ * with DISCARD. When active_count drops to 0, a DISCARD sentinel
++ * is restored at index 0.
++ */
++static int mxl862xx_vf_del_vid(struct mxl862xx_priv *priv,
++ struct mxl862xx_vf_block *vf, u16 vid)
++{
++ struct mxl862xx_vf_vid *ve, *last_ve;
++ u16 gap, last;
++ int ret;
++
++ ve = mxl862xx_vf_find_vid(vf, vid);
++ if (!ve)
++ return 0;
++
++ if (!vf->allocated) {
++ /* Software-only state -- just remove the tracking entry */
++ list_del(&ve->list);
++ kfree(ve);
++ vf->active_count--;
++ return 0;
++ }
++
++ gap = ve->index;
++ last = vf->active_count - 1;
++
++ if (vf->active_count == 1) {
++ /* Last VID -- restore DISCARD sentinel at index 0 */
++ ret = mxl862xx_vf_entry_discard(priv, vf->block_id, 0);
++ if (ret)
++ return ret;
++ } else if (gap < last) {
++ /* Swap: move the last ALLOW entry into the gap */
++ last_ve = NULL;
++ list_for_each_entry(last_ve, &vf->vids, list)
++ if (last_ve->index == last)
++ break;
++
++ if (WARN_ON(!last_ve || last_ve->index != last))
++ return -EINVAL;
++
++ ret = mxl862xx_vf_entry_set(priv, vf->block_id,
++ gap, last_ve->vid);
++ if (ret)
++ return ret;
++
++ last_ve->index = gap;
++
++ /* Plug the old last slot with DISCARD */
++ ret = mxl862xx_vf_entry_discard(priv, vf->block_id, last);
++ if (ret)
++ return ret;
++ } else {
++ /* Deleting the last entry -- just plug it */
++ ret = mxl862xx_vf_entry_discard(priv, vf->block_id, last);
++ if (ret)
++ return ret;
++ }
++
++ list_del(&ve->list);
++ kfree(ve);
++ vf->active_count--;
++
++ return 0;
++}
++
++/**
++ * mxl862xx_evlan_program_ingress - Write the fixed ingress catchall rules
++ * @priv: driver private data
++ * @port: port number
++ *
++ * In VLAN-aware mode the ingress EVLAN block handles PVID insertion for
++ * untagged/priority-tagged frames, passes through standard 802.1Q
++ * tagged frames for VF membership checking, and treats non-8021Q TPID
++ * frames as untagged. The block is sized to exactly fit the 7 catchall
++ * rules and is rewritten whenever PVID changes.
++ *
++ * In VLAN-unaware mode the firmware passes frames through unchanged when
++ * no ingress block is assigned, so nothing is programmed.
++ */
++static int mxl862xx_evlan_program_ingress(struct mxl862xx_priv *priv, int port)
++{
++ struct mxl862xx_port *p = &priv->ports[port];
++ struct mxl862xx_evlan_block *blk = &p->ingress_evlan;
++
++ if (!p->vlan_filtering)
++ return 0;
++
++ blk->in_use = true;
++ blk->n_active = blk->block_size;
++
++ return mxl862xx_evlan_write_final_rules(priv, blk,
++ ingress_aware_final,
++ ARRAY_SIZE(ingress_aware_final),
++ p->pvid);
++}
++
++/**
++ * mxl862xx_evlan_program_egress - Reprogram all egress tag-stripping rules
++ * @priv: driver private data
++ * @port: port number
++ *
++ * Walks the port's VF VID list and writes 2 EVLAN rules per VID that
++ * needs egress tag stripping. In VLAN-aware mode only untagged VIDs
++ * need rules (tagged VIDs pass through EVLAN untouched). In unaware
++ * mode every VID gets rules.
++ *
++ * Entries are packed starting at index 0, and the scan window
++ * (n_active) is narrowed so stale entries beyond it are never matched.
++ */
++static int mxl862xx_evlan_program_egress(struct mxl862xx_priv *priv, int port)
++{
++ struct mxl862xx_port *p = &priv->ports[port];
++ struct mxl862xx_evlan_block *blk = &p->egress_evlan;
++ const struct mxl862xx_evlan_rule_desc *vid_rules;
++ struct mxl862xx_vf_vid *vfv;
++ u16 old_active = blk->n_active;
++ u16 idx = 0, i;
++ int n_vid, ret;
++
++ if (p->vlan_filtering) {
++ vid_rules = vid_accept_standard;
++ n_vid = ARRAY_SIZE(vid_accept_standard);
++ } else {
++ vid_rules = vid_accept_egress_unaware;
++ n_vid = ARRAY_SIZE(vid_accept_egress_unaware);
++ }
++
++ list_for_each_entry(vfv, &p->vf.vids, list) {
++ /* In VLAN-aware mode tagged-only VIDs need no EVLAN
++ * rules -- VLAN Filter handles membership.
++ */
++ if (p->vlan_filtering && !vfv->untagged)
++ continue;
++
++ if (idx + n_vid > blk->block_size)
++ return -ENOSPC;
++
++ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
++ idx++, &vid_rules[0],
++ vfv->vid, vfv->untagged,
++ p->pvid);
++ if (ret)
++ return ret;
++
++ if (n_vid > 1) {
++ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
++ idx++, &vid_rules[1],
++ vfv->vid,
++ vfv->untagged,
++ p->pvid);
++ if (ret)
++ return ret;
++ }
++ }
++
++ /* Deactivate stale entries that are no longer needed.
++ * This closes the brief window between writing the new rules
++ * and set_bridge_port narrowing the scan window.
++ */
++ for (i = idx; i < old_active; i++) {
++ ret = mxl862xx_evlan_deactivate_entry(priv,
++ blk->block_id,
++ i);
++ if (ret)
++ return ret;
++ }
++
++ blk->n_active = idx;
++ blk->in_use = idx > 0;
++
++ return 0;
++}
++
++static int mxl862xx_port_vlan_filtering(struct dsa_switch *ds, int port,
++ bool vlan_filtering,
++ struct netlink_ext_ack *extack)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ bool changed = (p->vlan_filtering != vlan_filtering);
++ int ret;
++
++ p->vlan_filtering = vlan_filtering;
++
++ /* Reprogram Extended VLAN rules if filtering mode changed */
++ if (changed) {
++ /* When leaving VLAN-aware mode, release the ingress HW
++ * block. The firmware passes frames through unchanged
++ * when no ingress EVLAN block is assigned, so the block
++ * is unnecessary in unaware mode.
++ */
++ if (!vlan_filtering)
++ p->ingress_evlan.in_use = false;
++
++ ret = mxl862xx_evlan_program_ingress(priv, port);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_evlan_program_egress(priv, port);
++ if (ret)
++ return ret;
++ }
++
++ /* Push VLAN-based MAC learning flags and (possibly newly
++ * allocated) ingress block to hardware.
++ */
++ return mxl862xx_set_bridge_port(ds, port);
++}
++
++static int mxl862xx_port_vlan_add(struct dsa_switch *ds, int port,
++ const struct switchdev_obj_port_vlan *vlan,
++ struct netlink_ext_ack *extack)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
++ u16 vid = vlan->vid;
++ u16 old_pvid = p->pvid;
++ bool pvid_changed = false;
++ int ret;
++
++ /* CPU port is VLAN-transparent: the SP tag handles port
++ * identification and the host-side DSA tagger manages VLAN
++ * delivery. Egress EVLAN catchalls are set up once in
++ * setup_cpu_bridge; no per-VID VF/EVLAN programming needed.
++ */
++ if (dsa_is_cpu_port(ds, port))
++ return 0;
++
++ /* Update PVID tracking */
++ if (vlan->flags & BRIDGE_VLAN_INFO_PVID) {
++ if (p->pvid != vid) {
++ p->pvid = vid;
++ pvid_changed = true;
++ }
++ } else if (p->pvid == vid) {
++ p->pvid = 0;
++ pvid_changed = true;
++ }
++
++ /* Add/update VID in this port's VLAN Filter block.
++ * VF must be updated before programming egress EVLAN because
++ * evlan_program_egress walks the VF VID list.
++ */
++ ret = mxl862xx_vf_add_vid(priv, &p->vf, vid, untagged);
++ if (ret)
++ goto err_pvid;
++
++ /* Reprogram ingress finals if PVID changed */
++ if (pvid_changed) {
++ ret = mxl862xx_evlan_program_ingress(priv, port);
++ if (ret)
++ goto err_pvid;
++ }
++
++ /* Reprogram egress tag-stripping rules (walks VF VID list) */
++ ret = mxl862xx_evlan_program_egress(priv, port);
++ if (ret)
++ goto err_pvid;
++
++ /* Apply VLAN block IDs and MAC learning flags to bridge port */
++ ret = mxl862xx_set_bridge_port(ds, port);
++ if (ret)
++ goto err_pvid;
++
++ return 0;
++
++err_pvid:
++ p->pvid = old_pvid;
++ return ret;
++}
++
++static int mxl862xx_port_vlan_del(struct dsa_switch *ds, int port,
++ const struct switchdev_obj_port_vlan *vlan)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ u16 vid = vlan->vid;
++ bool pvid_changed = false;
++ int ret;
++
++ if (dsa_is_cpu_port(ds, port))
++ return 0;
++
++ /* Clear PVID if we're deleting it */
++ if (p->pvid == vid) {
++ p->pvid = 0;
++ pvid_changed = true;
++ }
++
++ /* Remove VID from this port's VLAN Filter block.
++ * Must happen before egress reprogram so the VID is no
++ * longer in the list that evlan_program_egress walks.
++ */
++ ret = mxl862xx_vf_del_vid(priv, &p->vf, vid);
++ if (ret)
++ return ret;
++
++ /* Reprogram egress tag-stripping rules (VID is now gone) */
++ ret = mxl862xx_evlan_program_egress(priv, port);
++ if (ret)
++ return ret;
++
++ /* If PVID changed, reprogram ingress finals */
++ if (pvid_changed) {
++ ret = mxl862xx_evlan_program_ingress(priv, port);
++ if (ret)
++ return ret;
++ }
++
++ return mxl862xx_set_bridge_port(ds, port);
++}
++
+ static int mxl862xx_setup_cpu_bridge(struct dsa_switch *ds, int port)
+ {
+ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
+ struct dsa_port *dp;
+
+- priv->ports[port].fid = MXL862XX_DEFAULT_BRIDGE;
+- priv->ports[port].learning = true;
++ p->fid = MXL862XX_DEFAULT_BRIDGE;
++ p->learning = true;
++
++ /* EVLAN is left disabled on CPU ports -- frames pass through
++ * without EVLAN processing. Only the portmap and bridge
++ * assignment need to be configured.
++ */
+
+ /* include all assigned user ports in the CPU portmap */
+- bitmap_zero(priv->ports[port].portmap, MXL862XX_MAX_BRIDGE_PORTS);
++ bitmap_zero(p->portmap, MXL862XX_MAX_BRIDGE_PORTS);
+ dsa_switch_for_each_user_port(dp, ds) {
+ /* it's safe to rely on cpu_dp being valid for user ports */
+ if (dp->cpu_dp->index != port)
+ continue;
+
+- __set_bit(dp->index, priv->ports[port].portmap);
++ __set_bit(dp->index, p->portmap);
+ }
+
+ return mxl862xx_set_bridge_port(ds, port);
+ }
+
++
+ static int mxl862xx_port_bridge_join(struct dsa_switch *ds, int port,
+ const struct dsa_bridge bridge,
+ bool *tx_fwd_offload,
+@@ -565,6 +1462,22 @@ static void mxl862xx_port_bridge_leave(s
+ bitmap_zero(p->portmap, MXL862XX_MAX_BRIDGE_PORTS);
+ __set_bit(dp->cpu_dp->index, p->portmap);
+ p->flood_block = 0;
++
++ /* Detach EVLAN and VF blocks from the bridge port BEFORE freeing
++ * them. The firmware tracks a usage count per block and rejects
++ * FREE while the count is non-zero.
++ *
++ * For EVLAN: setting in_use=false makes set_bridge_port send
++ * enable=false, which decrements the firmware refcount.
++ *
++ * For VF: set_bridge_port sees dp->bridge == NULL (DSA already
++ * cleared it) and sends vlan_filter_enable=0, which decrements
++ * the firmware VF refcount.
++ */
++ p->pvid = 0;
++ p->ingress_evlan.in_use = false;
++ p->egress_evlan.in_use = false;
++
+ err = mxl862xx_set_bridge_port(ds, port);
+ if (err)
+ dev_err(ds->dev,
+@@ -614,6 +1527,28 @@ static int mxl862xx_port_setup(struct ds
+ if (ret)
+ return ret;
+
++ /* Initialize and pre-allocate per-port EVLAN and VF blocks for
++ * user ports. CPU ports do not use EVLAN or VF -- frames pass
++ * through without processing. Pre-allocation avoids firmware
++ * EVLAN table fragmentation and simplifies control flow.
++ */
++ mxl862xx_evlan_block_init(&priv->ports[port].ingress_evlan,
++ priv->evlan_ingress_size);
++ ret = mxl862xx_evlan_block_alloc(priv, &priv->ports[port].ingress_evlan);
++ if (ret)
++ return ret;
++
++ mxl862xx_evlan_block_init(&priv->ports[port].egress_evlan,
++ priv->evlan_egress_size);
++ ret = mxl862xx_evlan_block_alloc(priv, &priv->ports[port].egress_evlan);
++ if (ret)
++ return ret;
++
++ mxl862xx_vf_init(&priv->ports[port].vf, priv->vf_block_size);
++ ret = mxl862xx_vf_alloc(priv, &priv->ports[port].vf);
++ if (ret)
++ return ret;
++
+ priv->ports[port].setup_done = true;
+
+ return 0;
+@@ -808,7 +1743,7 @@ static int mxl862xx_port_mdb_del(struct
+ mxl862xx_fw_portmap_clear_bit(qparam.port_map, port);
+
+ if (mxl862xx_fw_portmap_is_empty(qparam.port_map)) {
+- /* No ports left — remove the entry entirely */
++ /* No ports left -- remove the entry entirely */
+ rparam.fid = cpu_to_le16(fid);
+ rparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+ ether_addr_copy(rparam.mac, mdb->addr);
+@@ -899,7 +1834,7 @@ static void mxl862xx_port_stp_state_set(
+ /* Deferred work handler for host flood configuration.
+ *
+ * port_set_host_flood is called from atomic context (under
+- * netif_addr_lock), so firmware calls must be deferred. The worker
++ * netif_addr_lock), so firmware calls must be deferred. The worker
+ * acquires rtnl_lock() to serialize with DSA callbacks that access the
+ * same driver state.
+ */
+@@ -924,9 +1859,9 @@ static void mxl862xx_host_flood_work_fn(
+ mc = p->host_flood_mc;
+
+ /* The hardware controls unknown-unicast/multicast forwarding per FID
+- * (bridge), not per source port. For bridged ports all members share
++ * (bridge), not per source port. For bridged ports all members share
+ * one FID, so we cannot selectively suppress flooding to the CPU for
+- * one source port while allowing it for another. Silently ignore the
++ * one source port while allowing it for another. Silently ignore the
+ * request -- the excess flooding towards the CPU is harmless.
+ */
+ if (!dsa_port_bridge_dev_get(dsa_to_port(ds, port)))
+@@ -1026,6 +1961,9 @@ static const struct dsa_switch_ops mxl86
+ .port_fdb_dump = mxl862xx_port_fdb_dump,
+ .port_mdb_add = mxl862xx_port_mdb_add,
+ .port_mdb_del = mxl862xx_port_mdb_del,
++ .port_vlan_filtering = mxl862xx_port_vlan_filtering,
++ .port_vlan_add = mxl862xx_port_vlan_add,
++ .port_vlan_del = mxl862xx_port_vlan_del,
+ };
+
+ static void mxl862xx_phylink_mac_config(struct phylink_config *config,
+@@ -1111,7 +2049,7 @@ static void mxl862xx_remove(struct mdio_
+
+ /* Cancel any pending host flood work. dsa_unregister_switch()
+ * has already called port_teardown (which sets setup_done=false),
+- * but a worker could still be blocked on rtnl_lock(). Since we
++ * but a worker could still be blocked on rtnl_lock(). Since we
+ * are now outside RTNL, cancel_work_sync() will not deadlock.
+ */
+ for (i = 0; i < MXL862XX_MAX_PORTS; i++)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -13,6 +13,8 @@ struct mxl862xx_priv;
+ #define MXL862XX_DEFAULT_BRIDGE 0
+ #define MXL862XX_MAX_BRIDGES 48
+ #define MXL862XX_MAX_BRIDGE_PORTS 128
++#define MXL862XX_TOTAL_EVLAN_ENTRIES 1024
++#define MXL862XX_TOTAL_VF_ENTRIES 1024
+
+ /* Number of __le16 words in a firmware portmap (128-bit bitmap). */
+ #define MXL862XX_FW_PORTMAP_WORDS (MXL862XX_MAX_BRIDGE_PORTS / 16)
+@@ -86,12 +88,72 @@ static inline bool mxl862xx_fw_portmap_i
+ }
+
+ /**
++ * struct mxl862xx_vf_vid - Per-VID entry within a VLAN Filter block
++ * @list: Linked into &mxl862xx_vf_block.vids
++ * @vid: VLAN ID
++ * @index: Entry index within the VLAN Filter HW block
++ * @untagged: Strip tag on egress for this VID (drives EVLAN tag-stripping)
++ */
++struct mxl862xx_vf_vid {
++ struct list_head list;
++ u16 vid;
++ u16 index;
++ bool untagged;
++};
++
++/**
++ * struct mxl862xx_vf_block - Per-port VLAN Filter block
++ * @allocated: Whether the HW block has been allocated via VLANFILTER_ALLOC
++ * @block_id: HW VLAN Filter block ID from VLANFILTER_ALLOC
++ * @block_size: Total entries allocated in this block
++ * @active_count: Number of ALLOW entries at indices [0, active_count).
++ * The bridge port config sends max(active_count, 1) as
++ * block_size to narrow the HW scan window.
++ * discard_unmatched_tagged handles frames outside this range.
++ * @vids: List of &mxl862xx_vf_vid entries programmed in this block
++ */
++struct mxl862xx_vf_block {
++ bool allocated;
++ u16 block_id;
++ u16 block_size;
++ u16 active_count;
++ struct list_head vids;
++};
++
++/**
++ * struct mxl862xx_evlan_block - Per-port per-direction extended VLAN block
++ * @allocated: Whether the HW block has been allocated via EXTENDEDVLAN_ALLOC.
++ * Guards alloc/free idempotency--the block_id is only valid
++ * while allocated is true.
++ * @in_use: Whether the EVLAN engine should be enabled for this block
++ * on the bridge port (sent as the enable flag in
++ * set_bridge_port). Can be false while allocated is still
++ * true -- e.g. when all egress VIDs are removed (idx == 0 in
++ * evlan_program_egress) the block stays allocated for
++ * potential reuse, but the engine is disabled so an empty
++ * rule set does not discard all traffic.
++ * @block_id: HW block ID from EXTENDEDVLAN_ALLOC
++ * @block_size: Total entries allocated
++ * @n_active: Number of HW entries currently written. The bridge port
++ * config sends this as the egress scan window, so entries
++ * beyond n_active are never scanned. Always equals
++ * block_size for ingress blocks (fixed catchall rules).
++ */
++struct mxl862xx_evlan_block {
++ bool allocated;
++ bool in_use;
++ u16 block_id;
++ u16 block_size;
++ u16 n_active;
++};
++
++/**
+ * struct mxl862xx_port - per-port state tracked by the driver
+ * @priv: back-pointer to switch private data; needed by
+ * deferred work handlers to access ds and priv
+- * @fid: firmware FID for the permanent single-port bridge;
+- * kept alive for the lifetime of the port so traffic is
+- * never forwarded while the port is unbridged
++ * @fid: firmware FID for the permanent single-port bridge; kept
++ * alive for the lifetime of the port so traffic is never
++ * forwarded while the port is unbridged
+ * @portmap: bitmap of switch port indices that share the current
+ * bridge with this port
+ * @flood_block: bitmask of firmware meter indices that are currently
+@@ -101,6 +163,11 @@ static inline bool mxl862xx_fw_portmap_i
+ * @setup_done: set at end of port_setup, cleared at start of
+ * port_teardown; guards deferred work against
+ * acting on torn-down state
++ * @pvid: port VLAN ID (native VLAN) assigned to untagged traffic
++ * @vlan_filtering: true when VLAN filtering is enabled on this port
++ * @vf: per-port VLAN Filter block state
++ * @ingress_evlan: ingress extended VLAN block state
++ * @egress_evlan: egress extended VLAN block state
+ * @host_flood_uc: desired host unicast flood state (true = flood);
+ * updated atomically by port_set_host_flood, consumed
+ * by the deferred host_flood_work
+@@ -119,6 +186,12 @@ struct mxl862xx_port {
+ unsigned long flood_block;
+ bool learning;
+ bool setup_done;
++ /* VLAN state */
++ u16 pvid;
++ bool vlan_filtering;
++ struct mxl862xx_vf_block vf;
++ struct mxl862xx_evlan_block ingress_evlan;
++ struct mxl862xx_evlan_block egress_evlan;
+ bool host_flood_uc;
+ bool host_flood_mc;
+ struct work_struct host_flood_work;
+@@ -126,17 +199,23 @@ struct mxl862xx_port {
+
+ /**
+ * struct mxl862xx_priv - driver private data for an MxL862xx switch
+- * @ds: pointer to the DSA switch instance
+- * @mdiodev: MDIO device used to communicate with the switch firmware
+- * @crc_err_work: deferred work for shutting down all ports on MDIO CRC errors
+- * @crc_err: set atomically before CRC-triggered shutdown, cleared after
+- * @drop_meter: index of the single shared zero-rate firmware meter used
+- * to unconditionally drop traffic (used to block flooding)
+- * @ports: per-port state, indexed by switch port number
+- * @bridges: maps DSA bridge number to firmware bridge ID;
+- * zero means no firmware bridge allocated for that
+- * DSA bridge number. Indexed by dsa_bridge.num
+- * (0 .. ds->max_num_bridges).
++ * @ds: pointer to the DSA switch instance
++ * @mdiodev: MDIO device used to communicate with the switch firmware
++ * @crc_err_work: deferred work for shutting down all ports on MDIO CRC
++ * errors
++ * @crc_err: set atomically before CRC-triggered shutdown, cleared
++ * after
++ * @drop_meter: index of the single shared zero-rate firmware meter
++ * used to unconditionally drop traffic (used to block
++ * flooding)
++ * @ports: per-port state, indexed by switch port number
++ * @bridges: maps DSA bridge number to firmware bridge ID;
++ * zero means no firmware bridge allocated for that
++ * DSA bridge number. Indexed by dsa_bridge.num
++ * (0 .. ds->max_num_bridges).
++ * @evlan_ingress_size: per-port ingress Extended VLAN block size
++ * @evlan_egress_size: per-port egress Extended VLAN block size
++ * @vf_block_size: per-port VLAN Filter block size
+ */
+ struct mxl862xx_priv {
+ struct dsa_switch *ds;
+@@ -146,6 +225,9 @@ struct mxl862xx_priv {
+ u16 drop_meter;
+ struct mxl862xx_port ports[MXL862XX_MAX_PORTS];
+ u16 bridges[MXL862XX_MAX_BRIDGES + 1];
++ u16 evlan_ingress_size;
++ u16 evlan_egress_size;
++ u16 vf_block_size;
+ };
+
+ #endif /* __MXL862XX_H */
--- /dev/null
+From 03b583e774835f771dd7c3c265be5903f008e8e5 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sun, 22 Mar 2026 00:57:33 +0000
+Subject: [PATCH 15/35] net: dsa: mxl862xx: add ethtool statistics support
+
+The MxL862xx firmware exposes per-port RMON counters through the
+RMON_PORT_GET command, covering standard IEEE 802.3 MAC statistics
+(unicast/multicast/broadcast packet and byte counts, collision
+counters, pause frames) as well as hardware-specific counters such
+as extended VLAN discard and MTU exceed events.
+
+Add the RMON counter firmware API structures and command definitions.
+Implement .get_strings, .get_sset_count, and .get_ethtool_stats for
+legacy ethtool -S support. Implement .get_eth_mac_stats,
+.get_eth_ctrl_stats, and .get_pause_stats for the standardized
+IEEE 802.3 statistics interface.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 142 ++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 3 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 168 ++++++++++++++++++++++++
+ 3 files changed, 313 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -1224,4 +1224,146 @@ struct mxl862xx_sys_fw_image_version {
+ __le32 iv_build_num;
+ } __packed;
+
++/**
++ * enum mxl862xx_port_type - Port Type
++ * @MXL862XX_LOGICAL_PORT: Logical Port
++ * @MXL862XX_PHYSICAL_PORT: Physical Port
++ * @MXL862XX_CTP_PORT: Connectivity Termination Port (CTP)
++ * @MXL862XX_BRIDGE_PORT: Bridge Port
++ */
++enum mxl862xx_port_type {
++ MXL862XX_LOGICAL_PORT = 0,
++ MXL862XX_PHYSICAL_PORT,
++ MXL862XX_CTP_PORT,
++ MXL862XX_BRIDGE_PORT,
++};
++
++/**
++ * enum mxl862xx_rmon_port_type - RMON counter table type
++ * @MXL862XX_RMON_CTP_PORT_RX: CTP RX counters
++ * @MXL862XX_RMON_CTP_PORT_TX: CTP TX counters
++ * @MXL862XX_RMON_BRIDGE_PORT_RX: Bridge port RX counters
++ * @MXL862XX_RMON_BRIDGE_PORT_TX: Bridge port TX counters
++ * @MXL862XX_RMON_CTP_PORT_PCE_BYPASS: CTP PCE bypass counters
++ * @MXL862XX_RMON_TFLOW_RX: TFLOW RX counters
++ * @MXL862XX_RMON_TFLOW_TX: TFLOW TX counters
++ * @MXL862XX_RMON_QMAP: QMAP counters
++ * @MXL862XX_RMON_METER: Meter counters
++ * @MXL862XX_RMON_PMAC: PMAC counters
++ */
++enum mxl862xx_rmon_port_type {
++ MXL862XX_RMON_CTP_PORT_RX = 0,
++ MXL862XX_RMON_CTP_PORT_TX,
++ MXL862XX_RMON_BRIDGE_PORT_RX,
++ MXL862XX_RMON_BRIDGE_PORT_TX,
++ MXL862XX_RMON_CTP_PORT_PCE_BYPASS,
++ MXL862XX_RMON_TFLOW_RX,
++ MXL862XX_RMON_TFLOW_TX,
++ MXL862XX_RMON_QMAP = 0x0e,
++ MXL862XX_RMON_METER = 0x19,
++ MXL862XX_RMON_PMAC = 0x1c,
++};
++
++/**
++ * struct mxl862xx_rmon_port_cnt - RMON counters for a port
++ * @port_type: Port type for counter retrieval (see &enum mxl862xx_port_type)
++ * @port_id: Ethernet port number (zero-based)
++ * @sub_if_id_group: Sub-interface ID group
++ * @pce_bypass: Separate CTP Tx counters when PCE is bypassed
++ * @rx_extended_vlan_discard_pkts: Discarded at extended VLAN operation
++ * @mtu_exceed_discard_pkts: Discarded due to MTU exceeded
++ * @tx_under_size_good_pkts: Tx undersize (<64) packet count
++ * @tx_oversize_good_pkts: Tx oversize (>1518) packet count
++ * @rx_good_pkts: Received good packet count
++ * @rx_unicast_pkts: Received unicast packet count
++ * @rx_broadcast_pkts: Received broadcast packet count
++ * @rx_multicast_pkts: Received multicast packet count
++ * @rx_fcserror_pkts: Received FCS error packet count
++ * @rx_under_size_good_pkts: Received undersize good packet count
++ * @rx_oversize_good_pkts: Received oversize good packet count
++ * @rx_under_size_error_pkts: Received undersize error packet count
++ * @rx_good_pause_pkts: Received good pause packet count
++ * @rx_oversize_error_pkts: Received oversize error packet count
++ * @rx_align_error_pkts: Received alignment error packet count
++ * @rx_filtered_pkts: Filtered packet count
++ * @rx64byte_pkts: Received 64-byte packet count
++ * @rx127byte_pkts: Received 65-127 byte packet count
++ * @rx255byte_pkts: Received 128-255 byte packet count
++ * @rx511byte_pkts: Received 256-511 byte packet count
++ * @rx1023byte_pkts: Received 512-1023 byte packet count
++ * @rx_max_byte_pkts: Received 1024-max byte packet count
++ * @tx_good_pkts: Transmitted good packet count
++ * @tx_unicast_pkts: Transmitted unicast packet count
++ * @tx_broadcast_pkts: Transmitted broadcast packet count
++ * @tx_multicast_pkts: Transmitted multicast packet count
++ * @tx_single_coll_count: Transmit single collision count
++ * @tx_mult_coll_count: Transmit multiple collision count
++ * @tx_late_coll_count: Transmit late collision count
++ * @tx_excess_coll_count: Transmit excessive collision count
++ * @tx_coll_count: Transmit collision count
++ * @tx_pause_count: Transmit pause packet count
++ * @tx64byte_pkts: Transmitted 64-byte packet count
++ * @tx127byte_pkts: Transmitted 65-127 byte packet count
++ * @tx255byte_pkts: Transmitted 128-255 byte packet count
++ * @tx511byte_pkts: Transmitted 256-511 byte packet count
++ * @tx1023byte_pkts: Transmitted 512-1023 byte packet count
++ * @tx_max_byte_pkts: Transmitted 1024-max byte packet count
++ * @tx_dropped_pkts: Transmit dropped packet count
++ * @tx_acm_dropped_pkts: Transmit ACM dropped packet count
++ * @rx_dropped_pkts: Received dropped packet count
++ * @rx_good_bytes: Received good byte count (64-bit)
++ * @rx_bad_bytes: Received bad byte count (64-bit)
++ * @tx_good_bytes: Transmitted good byte count (64-bit)
++ */
++struct mxl862xx_rmon_port_cnt {
++ enum mxl862xx_port_type port_type;
++ __le16 port_id;
++ __le16 sub_if_id_group;
++ u8 pce_bypass;
++ __le32 rx_extended_vlan_discard_pkts;
++ __le32 mtu_exceed_discard_pkts;
++ __le32 tx_under_size_good_pkts;
++ __le32 tx_oversize_good_pkts;
++ __le32 rx_good_pkts;
++ __le32 rx_unicast_pkts;
++ __le32 rx_broadcast_pkts;
++ __le32 rx_multicast_pkts;
++ __le32 rx_fcserror_pkts;
++ __le32 rx_under_size_good_pkts;
++ __le32 rx_oversize_good_pkts;
++ __le32 rx_under_size_error_pkts;
++ __le32 rx_good_pause_pkts;
++ __le32 rx_oversize_error_pkts;
++ __le32 rx_align_error_pkts;
++ __le32 rx_filtered_pkts;
++ __le32 rx64byte_pkts;
++ __le32 rx127byte_pkts;
++ __le32 rx255byte_pkts;
++ __le32 rx511byte_pkts;
++ __le32 rx1023byte_pkts;
++ __le32 rx_max_byte_pkts;
++ __le32 tx_good_pkts;
++ __le32 tx_unicast_pkts;
++ __le32 tx_broadcast_pkts;
++ __le32 tx_multicast_pkts;
++ __le32 tx_single_coll_count;
++ __le32 tx_mult_coll_count;
++ __le32 tx_late_coll_count;
++ __le32 tx_excess_coll_count;
++ __le32 tx_coll_count;
++ __le32 tx_pause_count;
++ __le32 tx64byte_pkts;
++ __le32 tx127byte_pkts;
++ __le32 tx255byte_pkts;
++ __le32 tx511byte_pkts;
++ __le32 tx1023byte_pkts;
++ __le32 tx_max_byte_pkts;
++ __le32 tx_dropped_pkts;
++ __le32 tx_acm_dropped_pkts;
++ __le32 rx_dropped_pkts;
++ __le64 rx_good_bytes;
++ __le64 rx_bad_bytes;
++ __le64 tx_good_bytes;
++} __packed;
++
+ #endif /* __MXL862XX_API_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -16,6 +16,7 @@
+ #define MXL862XX_BRDGPORT_MAGIC 0x400
+ #define MXL862XX_CTP_MAGIC 0x500
+ #define MXL862XX_QOS_MAGIC 0x600
++#define MXL862XX_RMON_MAGIC 0x700
+ #define MXL862XX_SWMAC_MAGIC 0xa00
+ #define MXL862XX_EXTVLAN_MAGIC 0xb00
+ #define MXL862XX_VLANFILTER_MAGIC 0xc00
+@@ -43,6 +44,8 @@
+ #define MXL862XX_QOS_METERCFGSET (MXL862XX_QOS_MAGIC + 0x2)
+ #define MXL862XX_QOS_METERALLOC (MXL862XX_QOS_MAGIC + 0x2a)
+
++#define MXL862XX_RMON_PORT_GET (MXL862XX_RMON_MAGIC + 0x1)
++
+ #define MXL862XX_MAC_TABLEENTRYADD (MXL862XX_SWMAC_MAGIC + 0x2)
+ #define MXL862XX_MAC_TABLEENTRYREAD (MXL862XX_SWMAC_MAGIC + 0x3)
+ #define MXL862XX_MAC_TABLEENTRYQUERY (MXL862XX_SWMAC_MAGIC + 0x4)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -30,6 +30,64 @@
+ #define MXL862XX_API_READ_QUIET(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
+
++struct mxl862xx_mib_desc {
++ unsigned int size;
++ unsigned int offset;
++ const char *name;
++};
++
++#define MIB_DESC(_size, _name, _element) \
++{ \
++ .size = _size, \
++ .name = _name, \
++ .offset = offsetof(struct mxl862xx_rmon_port_cnt, _element) \
++}
++
++static const struct mxl862xx_mib_desc mxl862xx_mib[] = {
++ MIB_DESC(1, "TxGoodPkts", tx_good_pkts),
++ MIB_DESC(1, "TxUnicastPkts", tx_unicast_pkts),
++ MIB_DESC(1, "TxBroadcastPkts", tx_broadcast_pkts),
++ MIB_DESC(1, "TxMulticastPkts", tx_multicast_pkts),
++ MIB_DESC(1, "Tx64BytePkts", tx64byte_pkts),
++ MIB_DESC(1, "Tx127BytePkts", tx127byte_pkts),
++ MIB_DESC(1, "Tx255BytePkts", tx255byte_pkts),
++ MIB_DESC(1, "Tx511BytePkts", tx511byte_pkts),
++ MIB_DESC(1, "Tx1023BytePkts", tx1023byte_pkts),
++ MIB_DESC(1, "TxMaxBytePkts", tx_max_byte_pkts),
++ MIB_DESC(1, "TxDroppedPkts", tx_dropped_pkts),
++ MIB_DESC(1, "TxAcmDroppedPkts", tx_acm_dropped_pkts),
++ MIB_DESC(2, "TxGoodBytes", tx_good_bytes),
++ MIB_DESC(1, "TxSingleCollCount", tx_single_coll_count),
++ MIB_DESC(1, "TxMultCollCount", tx_mult_coll_count),
++ MIB_DESC(1, "TxLateCollCount", tx_late_coll_count),
++ MIB_DESC(1, "TxExcessCollCount", tx_excess_coll_count),
++ MIB_DESC(1, "TxCollCount", tx_coll_count),
++ MIB_DESC(1, "TxPauseCount", tx_pause_count),
++ MIB_DESC(1, "RxGoodPkts", rx_good_pkts),
++ MIB_DESC(1, "RxUnicastPkts", rx_unicast_pkts),
++ MIB_DESC(1, "RxBroadcastPkts", rx_broadcast_pkts),
++ MIB_DESC(1, "RxMulticastPkts", rx_multicast_pkts),
++ MIB_DESC(1, "RxFCSErrorPkts", rx_fcserror_pkts),
++ MIB_DESC(1, "RxUnderSizeGoodPkts", rx_under_size_good_pkts),
++ MIB_DESC(1, "RxOversizeGoodPkts", rx_oversize_good_pkts),
++ MIB_DESC(1, "RxUnderSizeErrorPkts", rx_under_size_error_pkts),
++ MIB_DESC(1, "RxOversizeErrorPkts", rx_oversize_error_pkts),
++ MIB_DESC(1, "RxFilteredPkts", rx_filtered_pkts),
++ MIB_DESC(1, "Rx64BytePkts", rx64byte_pkts),
++ MIB_DESC(1, "Rx127BytePkts", rx127byte_pkts),
++ MIB_DESC(1, "Rx255BytePkts", rx255byte_pkts),
++ MIB_DESC(1, "Rx511BytePkts", rx511byte_pkts),
++ MIB_DESC(1, "Rx1023BytePkts", rx1023byte_pkts),
++ MIB_DESC(1, "RxMaxBytePkts", rx_max_byte_pkts),
++ MIB_DESC(1, "RxDroppedPkts", rx_dropped_pkts),
++ MIB_DESC(1, "RxExtendedVlanDiscardPkts", rx_extended_vlan_discard_pkts),
++ MIB_DESC(1, "MtuExceedDiscardPkts", mtu_exceed_discard_pkts),
++ MIB_DESC(2, "RxGoodBytes", rx_good_bytes),
++ MIB_DESC(2, "RxBadBytes", rx_bad_bytes),
++ MIB_DESC(1, "RxGoodPausePkts", rx_good_pause_pkts),
++ MIB_DESC(1, "RxAlignErrorPkts", rx_align_error_pkts),
++};
++
+ #define MXL862XX_SDMA_PCTRLP(p) (0xbc0 + ((p) * 0x6))
+ #define MXL862XX_SDMA_PCTRL_EN BIT(0)
+
+@@ -1940,6 +1998,110 @@ static int mxl862xx_port_bridge_flags(st
+ return 0;
+ }
+
++static void mxl862xx_get_strings(struct dsa_switch *ds, int port,
++ u32 stringset, u8 *data)
++{
++ int i;
++
++ if (stringset != ETH_SS_STATS)
++ return;
++
++ for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++)
++ ethtool_puts(&data, mxl862xx_mib[i].name);
++}
++
++static int mxl862xx_get_sset_count(struct dsa_switch *ds, int port, int sset)
++{
++ if (sset != ETH_SS_STATS)
++ return 0;
++
++ return ARRAY_SIZE(mxl862xx_mib);
++}
++
++static int mxl862xx_read_rmon(struct dsa_switch *ds, int port,
++ struct mxl862xx_rmon_port_cnt *cnt)
++{
++ memset(cnt, 0, sizeof(*cnt));
++ cnt->port_type = MXL862XX_CTP_PORT;
++ cnt->port_id = cpu_to_le16(port);
++
++ return MXL862XX_API_READ(ds->priv, MXL862XX_RMON_PORT_GET, *cnt);
++}
++
++static void mxl862xx_get_ethtool_stats(struct dsa_switch *ds, int port,
++ u64 *data)
++{
++ const struct mxl862xx_mib_desc *mib;
++ struct mxl862xx_rmon_port_cnt cnt;
++ int ret, i;
++ void *field;
++
++ ret = mxl862xx_read_rmon(ds, port, &cnt);
++ if (ret) {
++ dev_err(ds->dev, "failed to read RMON stats on port %d\n", port);
++ return;
++ }
++
++ for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++) {
++ mib = &mxl862xx_mib[i];
++ field = (u8 *)&cnt + mib->offset;
++
++ if (mib->size == 1)
++ *data++ = le32_to_cpu(*(__le32 *)field);
++ else
++ *data++ = le64_to_cpu(*(__le64 *)field);
++ }
++}
++
++static void mxl862xx_get_eth_mac_stats(struct dsa_switch *ds, int port,
++ struct ethtool_eth_mac_stats *mac_stats)
++{
++ struct mxl862xx_rmon_port_cnt cnt;
++
++ if (mxl862xx_read_rmon(ds, port, &cnt))
++ return;
++
++ mac_stats->FramesTransmittedOK = le32_to_cpu(cnt.tx_good_pkts);
++ mac_stats->SingleCollisionFrames = le32_to_cpu(cnt.tx_single_coll_count);
++ mac_stats->MultipleCollisionFrames = le32_to_cpu(cnt.tx_mult_coll_count);
++ mac_stats->FramesReceivedOK = le32_to_cpu(cnt.rx_good_pkts);
++ mac_stats->FrameCheckSequenceErrors = le32_to_cpu(cnt.rx_fcserror_pkts);
++ mac_stats->AlignmentErrors = le32_to_cpu(cnt.rx_align_error_pkts);
++ mac_stats->OctetsTransmittedOK = le64_to_cpu(cnt.tx_good_bytes);
++ mac_stats->LateCollisions = le32_to_cpu(cnt.tx_late_coll_count);
++ mac_stats->FramesAbortedDueToXSColls = le32_to_cpu(cnt.tx_excess_coll_count);
++ mac_stats->OctetsReceivedOK = le64_to_cpu(cnt.rx_good_bytes);
++ mac_stats->MulticastFramesXmittedOK = le32_to_cpu(cnt.tx_multicast_pkts);
++ mac_stats->BroadcastFramesXmittedOK = le32_to_cpu(cnt.tx_broadcast_pkts);
++ mac_stats->MulticastFramesReceivedOK = le32_to_cpu(cnt.rx_multicast_pkts);
++ mac_stats->BroadcastFramesReceivedOK = le32_to_cpu(cnt.rx_broadcast_pkts);
++ mac_stats->FrameTooLongErrors = le32_to_cpu(cnt.rx_oversize_error_pkts);
++}
++
++static void mxl862xx_get_eth_ctrl_stats(struct dsa_switch *ds, int port,
++ struct ethtool_eth_ctrl_stats *ctrl_stats)
++{
++ struct mxl862xx_rmon_port_cnt cnt;
++
++ if (mxl862xx_read_rmon(ds, port, &cnt))
++ return;
++
++ ctrl_stats->MACControlFramesTransmitted = le32_to_cpu(cnt.tx_pause_count);
++ ctrl_stats->MACControlFramesReceived = le32_to_cpu(cnt.rx_good_pause_pkts);
++}
++
++static void mxl862xx_get_pause_stats(struct dsa_switch *ds, int port,
++ struct ethtool_pause_stats *pause_stats)
++{
++ struct mxl862xx_rmon_port_cnt cnt;
++
++ if (mxl862xx_read_rmon(ds, port, &cnt))
++ return;
++
++ pause_stats->tx_pause_frames = le32_to_cpu(cnt.tx_pause_count);
++ pause_stats->rx_pause_frames = le32_to_cpu(cnt.rx_good_pause_pkts);
++}
++
+ static const struct dsa_switch_ops mxl862xx_switch_ops = {
+ .get_tag_protocol = mxl862xx_get_tag_protocol,
+ .setup = mxl862xx_setup,
+@@ -1964,6 +2126,12 @@ static const struct dsa_switch_ops mxl86
+ .port_vlan_filtering = mxl862xx_port_vlan_filtering,
+ .port_vlan_add = mxl862xx_port_vlan_add,
+ .port_vlan_del = mxl862xx_port_vlan_del,
++ .get_strings = mxl862xx_get_strings,
++ .get_sset_count = mxl862xx_get_sset_count,
++ .get_ethtool_stats = mxl862xx_get_ethtool_stats,
++ .get_eth_mac_stats = mxl862xx_get_eth_mac_stats,
++ .get_eth_ctrl_stats = mxl862xx_get_eth_ctrl_stats,
++ .get_pause_stats = mxl862xx_get_pause_stats,
+ };
+
+ static void mxl862xx_phylink_mac_config(struct phylink_config *config,
--- /dev/null
+From 8b66d20f7e5226f4854a39cfb9f25a0591a5bb83 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 04:14:38 +0000
+Subject: [PATCH 16/35] net: dsa: mxl862xx: implement .get_stats64
+
+Poll free-running firmware RMON counters every 2 seconds and accumulate
+deltas into 64-bit per-port statistics. 32-bit packet counters wrap
+in ~880s at 2.5 Gbps line rate; the 2s polling interval provides a
+comfortable margin. The .get_stats64 callback forces a fresh poll so
+that counters are always up to date when queried.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 167 ++++++++++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 51 +++++++++
+ 2 files changed, 218 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -30,6 +30,12 @@
+ #define MXL862XX_API_READ_QUIET(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
+
++/* Polling interval for RMON counter accumulation. At 2.5 Gbps with
++ * minimum-size (64-byte) frames, a 32-bit packet counter wraps in ~880s.
++ * 2s gives a comfortable margin.
++ */
++#define MXL862XX_STATS_POLL_INTERVAL (2 * HZ)
++
+ struct mxl862xx_mib_desc {
+ unsigned int size;
+ unsigned int offset;
+@@ -784,6 +790,9 @@ static int mxl862xx_setup(struct dsa_swi
+ if (ret)
+ return ret;
+
++ schedule_delayed_work(&priv->stats_work,
++ MXL862XX_STATS_POLL_INTERVAL);
++
+ return mxl862xx_setup_mdio(ds);
+ }
+
+@@ -2102,6 +2111,156 @@ static void mxl862xx_get_pause_stats(str
+ pause_stats->rx_pause_frames = le32_to_cpu(cnt.rx_good_pause_pkts);
+ }
+
++/* Compute the delta between two 32-bit free-running counter snapshots,
++ * handling a single wrap-around correctly via unsigned subtraction.
++ */
++static u64 mxl862xx_delta32(u32 cur, u32 prev)
++{
++ return (u32)(cur - prev);
++}
++
++/**
++ * mxl862xx_stats_poll - Read RMON counters and accumulate into 64-bit stats
++ * @ds: DSA switch
++ * @port: port index
++ *
++ * The firmware RMON counters are free-running 32-bit values (64-bit for
++ * byte counters). This function reads the hardware via MDIO (may sleep),
++ * computes deltas from the previous snapshot, and accumulates them into
++ * 64-bit per-port stats under a spinlock.
++ *
++ * Called only from the stats polling workqueue -- serialized by the
++ * single-threaded delayed_work, so no MDIO locking is needed here.
++ */
++static void mxl862xx_stats_poll(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port_stats *s = &priv->ports[port].stats;
++ u32 rx_fcserr, rx_under, rx_over, rx_align, tx_drop;
++ u32 rx_drop, rx_evlan, mtu_exc, tx_acm;
++ struct mxl862xx_rmon_port_cnt cnt;
++ u64 rx_bytes, tx_bytes;
++ u32 rx_mcast, tx_coll;
++ u32 rx_pkts, tx_pkts;
++
++ /* MDIO read -- may sleep, done outside the spinlock. */
++ if (mxl862xx_read_rmon(ds, port, &cnt))
++ return;
++
++ rx_pkts = le32_to_cpu(cnt.rx_good_pkts);
++ tx_pkts = le32_to_cpu(cnt.tx_good_pkts);
++ rx_bytes = le64_to_cpu(cnt.rx_good_bytes);
++ tx_bytes = le64_to_cpu(cnt.tx_good_bytes);
++ rx_fcserr = le32_to_cpu(cnt.rx_fcserror_pkts);
++ rx_under = le32_to_cpu(cnt.rx_under_size_error_pkts);
++ rx_over = le32_to_cpu(cnt.rx_oversize_error_pkts);
++ rx_align = le32_to_cpu(cnt.rx_align_error_pkts);
++ tx_drop = le32_to_cpu(cnt.tx_dropped_pkts);
++ rx_drop = le32_to_cpu(cnt.rx_dropped_pkts);
++ rx_evlan = le32_to_cpu(cnt.rx_extended_vlan_discard_pkts);
++ mtu_exc = le32_to_cpu(cnt.mtu_exceed_discard_pkts);
++ tx_acm = le32_to_cpu(cnt.tx_acm_dropped_pkts);
++ rx_mcast = le32_to_cpu(cnt.rx_multicast_pkts);
++ tx_coll = le32_to_cpu(cnt.tx_coll_count);
++
++ /* Accumulate deltas under spinlock -- .get_stats64 reads these. */
++ spin_lock_bh(&priv->ports[port].stats_lock);
++
++ s->rx_packets += mxl862xx_delta32(rx_pkts, s->prev_rx_good_pkts);
++ s->tx_packets += mxl862xx_delta32(tx_pkts, s->prev_tx_good_pkts);
++ s->rx_bytes += rx_bytes - s->prev_rx_good_bytes;
++ s->tx_bytes += tx_bytes - s->prev_tx_good_bytes;
++
++ s->rx_errors +=
++ mxl862xx_delta32(rx_fcserr, s->prev_rx_fcserror_pkts) +
++ mxl862xx_delta32(rx_under, s->prev_rx_under_size_error_pkts) +
++ mxl862xx_delta32(rx_over, s->prev_rx_oversize_error_pkts) +
++ mxl862xx_delta32(rx_align, s->prev_rx_align_error_pkts);
++ s->tx_errors +=
++ mxl862xx_delta32(tx_drop, s->prev_tx_dropped_pkts);
++
++ s->rx_dropped +=
++ mxl862xx_delta32(rx_drop, s->prev_rx_dropped_pkts) +
++ mxl862xx_delta32(rx_evlan, s->prev_rx_evlan_discard_pkts) +
++ mxl862xx_delta32(mtu_exc, s->prev_mtu_exceed_discard_pkts);
++ s->tx_dropped +=
++ mxl862xx_delta32(tx_drop, s->prev_tx_dropped_pkts) +
++ mxl862xx_delta32(tx_acm, s->prev_tx_acm_dropped_pkts);
++
++ s->multicast += mxl862xx_delta32(rx_mcast, s->prev_rx_multicast_pkts);
++ s->collisions += mxl862xx_delta32(tx_coll, s->prev_tx_coll_count);
++
++ s->rx_length_errors +=
++ mxl862xx_delta32(rx_under, s->prev_rx_under_size_error_pkts) +
++ mxl862xx_delta32(rx_over, s->prev_rx_oversize_error_pkts);
++ s->rx_crc_errors +=
++ mxl862xx_delta32(rx_fcserr, s->prev_rx_fcserror_pkts);
++ s->rx_frame_errors +=
++ mxl862xx_delta32(rx_align, s->prev_rx_align_error_pkts);
++
++ s->prev_rx_good_pkts = rx_pkts;
++ s->prev_tx_good_pkts = tx_pkts;
++ s->prev_rx_good_bytes = rx_bytes;
++ s->prev_tx_good_bytes = tx_bytes;
++ s->prev_rx_fcserror_pkts = rx_fcserr;
++ s->prev_rx_under_size_error_pkts = rx_under;
++ s->prev_rx_oversize_error_pkts = rx_over;
++ s->prev_rx_align_error_pkts = rx_align;
++ s->prev_tx_dropped_pkts = tx_drop;
++ s->prev_rx_dropped_pkts = rx_drop;
++ s->prev_rx_evlan_discard_pkts = rx_evlan;
++ s->prev_mtu_exceed_discard_pkts = mtu_exc;
++ s->prev_tx_acm_dropped_pkts = tx_acm;
++ s->prev_rx_multicast_pkts = rx_mcast;
++ s->prev_tx_coll_count = tx_coll;
++
++ spin_unlock_bh(&priv->ports[port].stats_lock);
++}
++
++static void mxl862xx_stats_work_fn(struct work_struct *work)
++{
++ struct mxl862xx_priv *priv =
++ container_of(work, struct mxl862xx_priv, stats_work.work);
++ struct dsa_switch *ds = priv->ds;
++ struct dsa_port *dp;
++
++ dsa_switch_for_each_available_port(dp, ds)
++ mxl862xx_stats_poll(ds, dp->index);
++
++ schedule_delayed_work(&priv->stats_work,
++ MXL862XX_STATS_POLL_INTERVAL);
++}
++
++static void mxl862xx_get_stats64(struct dsa_switch *ds, int port,
++ struct rtnl_link_stats64 *s)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port_stats *ps = &priv->ports[port].stats;
++
++ spin_lock_bh(&priv->ports[port].stats_lock);
++
++ s->rx_packets = ps->rx_packets;
++ s->tx_packets = ps->tx_packets;
++ s->rx_bytes = ps->rx_bytes;
++ s->tx_bytes = ps->tx_bytes;
++ s->rx_errors = ps->rx_errors;
++ s->tx_errors = ps->tx_errors;
++ s->rx_dropped = ps->rx_dropped;
++ s->tx_dropped = ps->tx_dropped;
++ s->multicast = ps->multicast;
++ s->collisions = ps->collisions;
++ s->rx_length_errors = ps->rx_length_errors;
++ s->rx_crc_errors = ps->rx_crc_errors;
++ s->rx_frame_errors = ps->rx_frame_errors;
++
++ spin_unlock_bh(&priv->ports[port].stats_lock);
++
++ /* Trigger a fresh poll so the next read sees up-to-date counters.
++ * No-op if the work is already pending or running.
++ */
++ schedule_delayed_work(&priv->stats_work, 0);
++}
++
+ static const struct dsa_switch_ops mxl862xx_switch_ops = {
+ .get_tag_protocol = mxl862xx_get_tag_protocol,
+ .setup = mxl862xx_setup,
+@@ -2132,6 +2291,7 @@ static const struct dsa_switch_ops mxl86
+ .get_eth_mac_stats = mxl862xx_get_eth_mac_stats,
+ .get_eth_ctrl_stats = mxl862xx_get_eth_ctrl_stats,
+ .get_pause_stats = mxl862xx_get_pause_stats,
++ .get_stats64 = mxl862xx_get_stats64,
+ };
+
+ static void mxl862xx_phylink_mac_config(struct phylink_config *config,
+@@ -2193,8 +2353,11 @@ static int mxl862xx_probe(struct mdio_de
+ priv->ports[i].priv = priv;
+ INIT_WORK(&priv->ports[i].host_flood_work,
+ mxl862xx_host_flood_work_fn);
++ spin_lock_init(&priv->ports[i].stats_lock);
+ }
+
++ INIT_DELAYED_WORK(&priv->stats_work, mxl862xx_stats_work_fn);
++
+ dev_set_drvdata(dev, ds);
+
+ return dsa_register_switch(ds);
+@@ -2213,6 +2376,8 @@ static void mxl862xx_remove(struct mdio_
+
+ dsa_unregister_switch(ds);
+
++ cancel_delayed_work_sync(&priv->stats_work);
++
+ mxl862xx_host_shutdown(priv);
+
+ /* Cancel any pending host flood work. dsa_unregister_switch()
+@@ -2237,6 +2402,8 @@ static void mxl862xx_shutdown(struct mdi
+
+ dsa_switch_shutdown(ds);
+
++ cancel_delayed_work_sync(&priv->stats_work);
++
+ mxl862xx_host_shutdown(priv);
+
+ for (i = 0; i < MXL862XX_MAX_PORTS; i++)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -148,6 +148,47 @@ struct mxl862xx_evlan_block {
+ };
+
+ /**
++ * struct mxl862xx_port_stats - 64-bit accumulated hardware port statistics
++ *
++ * The firmware RMON counters are 32-bit free-running (64-bit for byte
++ * counters). This structure holds 64-bit accumulators alongside the
++ * previous raw snapshot so that deltas can be computed across polls,
++ * handling 32-bit wrap correctly via unsigned subtraction.
++ */
++struct mxl862xx_port_stats {
++ /* 64-bit accumulators */
++ u64 rx_packets;
++ u64 tx_packets;
++ u64 rx_bytes;
++ u64 tx_bytes;
++ u64 rx_errors;
++ u64 tx_errors;
++ u64 rx_dropped;
++ u64 tx_dropped;
++ u64 multicast;
++ u64 collisions;
++ u64 rx_length_errors;
++ u64 rx_crc_errors;
++ u64 rx_frame_errors;
++ /* Previous raw RMON values for delta computation */
++ u32 prev_rx_good_pkts;
++ u32 prev_tx_good_pkts;
++ u64 prev_rx_good_bytes;
++ u64 prev_tx_good_bytes;
++ u32 prev_rx_fcserror_pkts;
++ u32 prev_rx_under_size_error_pkts;
++ u32 prev_rx_oversize_error_pkts;
++ u32 prev_rx_align_error_pkts;
++ u32 prev_tx_dropped_pkts;
++ u32 prev_rx_dropped_pkts;
++ u32 prev_rx_evlan_discard_pkts;
++ u32 prev_mtu_exceed_discard_pkts;
++ u32 prev_tx_acm_dropped_pkts;
++ u32 prev_rx_multicast_pkts;
++ u32 prev_tx_coll_count;
++};
++
++/**
+ * struct mxl862xx_port - per-port state tracked by the driver
+ * @priv: back-pointer to switch private data; needed by
+ * deferred work handlers to access ds and priv
+@@ -178,6 +219,10 @@ struct mxl862xx_evlan_block {
+ * The worker acquires rtnl_lock() to serialize with
+ * DSA callbacks and checks @setup_done to avoid
+ * acting on torn-down ports.
++ * @stats: 64-bit accumulated hardware statistics; updated
++ * periodically by the stats polling work
++ * @stats_lock: protects accumulator reads in .get_stats64 against
++ * concurrent updates from the polling work
+ */
+ struct mxl862xx_port {
+ struct mxl862xx_priv *priv;
+@@ -195,6 +240,9 @@ struct mxl862xx_port {
+ bool host_flood_uc;
+ bool host_flood_mc;
+ struct work_struct host_flood_work;
++ /* Hardware stats accumulation */
++ struct mxl862xx_port_stats stats;
++ spinlock_t stats_lock;
+ };
+
+ /**
+@@ -216,6 +264,8 @@ struct mxl862xx_port {
+ * @evlan_ingress_size: per-port ingress Extended VLAN block size
+ * @evlan_egress_size: per-port egress Extended VLAN block size
+ * @vf_block_size: per-port VLAN Filter block size
++ * @stats_work: periodic work item that polls RMON hardware counters
++ * and accumulates them into 64-bit per-port stats
+ */
+ struct mxl862xx_priv {
+ struct dsa_switch *ds;
+@@ -228,6 +278,7 @@ struct mxl862xx_priv {
+ u16 evlan_ingress_size;
+ u16 evlan_egress_size;
+ u16 vf_block_size;
++ struct delayed_work stats_work;
+ };
+
+ #endif /* __MXL862XX_H */
--- /dev/null
+From fecfbea928cd762b19ff17aa16fb1ab143d73db1 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 17:56:35 +0000
+Subject: [PATCH 17/35] net: dsa: mxl862xx: store firmware version for feature
+ gating
+
+Query the firmware version at init (already done in wait_ready),
+cache it in priv->fw_version, and provide MXL862XX_FW_VER_MIN()
+for version-gated code paths throughout the driver.
+
+The union mxl862xx_fw_version lays out major/minor/revision so
+that the u32 raw field compares with natural version ordering on
+both big- and little-endian machines.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 3 +++
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 36 +++++++++++++++++++++++++++++
+ 2 files changed, 39 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -286,6 +286,9 @@ static int mxl862xx_wait_ready(struct ds
+ ver.iv_major, ver.iv_minor,
+ le16_to_cpu(ver.iv_revision),
+ le32_to_cpu(ver.iv_build_num));
++ priv->fw_version.major = ver.iv_major;
++ priv->fw_version.minor = ver.iv_minor;
++ priv->fw_version.revision = le16_to_cpu(ver.iv_revision);
+ return 0;
+
+ not_ready_yet:
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -3,6 +3,7 @@
+ #ifndef __MXL862XX_H
+ #define __MXL862XX_H
+
++#include <asm/byteorder.h>
+ #include <linux/mdio.h>
+ #include <linux/workqueue.h>
+ #include <net/dsa.h>
+@@ -246,6 +247,38 @@ struct mxl862xx_port {
+ };
+
+ /**
++ * union mxl862xx_fw_version - firmware version for comparison and display
++ * @major: firmware major version
++ * @minor: firmware minor version
++ * @revision: firmware revision number
++ * @raw: combined u32 for direct >= comparison (major most significant)
++ *
++ * The struct layout places major in the most-significant byte of the
++ * u32 on both big- and little-endian machines, so raw values compare
++ * with the natural major > minor > revision ordering.
++ */
++union mxl862xx_fw_version {
++ struct {
++#if defined(__BIG_ENDIAN)
++ u8 major;
++ u8 minor;
++ u16 revision;
++#elif defined(__LITTLE_ENDIAN)
++ u16 revision;
++ u8 minor;
++ u8 major;
++#endif
++ };
++ u32 raw;
++};
++
++#define MXL862XX_FW_VER(maj, min, rev) \
++ ((union mxl862xx_fw_version){ .major = (maj), .minor = (min), \
++ .revision = (rev) }).raw
++#define MXL862XX_FW_VER_MIN(priv, maj, min, rev) \
++ ((priv)->fw_version.raw >= MXL862XX_FW_VER(maj, min, rev))
++
++/**
+ * struct mxl862xx_priv - driver private data for an MxL862xx switch
+ * @ds: pointer to the DSA switch instance
+ * @mdiodev: MDIO device used to communicate with the switch firmware
+@@ -256,6 +289,8 @@ struct mxl862xx_port {
+ * @drop_meter: index of the single shared zero-rate firmware meter
+ * used to unconditionally drop traffic (used to block
+ * flooding)
++ * @fw_version: cached firmware version, populated at probe and
++ * compared with MXL862XX_FW_VER_MIN()
+ * @ports: per-port state, indexed by switch port number
+ * @bridges: maps DSA bridge number to firmware bridge ID;
+ * zero means no firmware bridge allocated for that
+@@ -273,6 +308,7 @@ struct mxl862xx_priv {
+ struct work_struct crc_err_work;
+ unsigned long crc_err;
+ u16 drop_meter;
++ union mxl862xx_fw_version fw_version;
+ struct mxl862xx_port ports[MXL862XX_MAX_PORTS];
+ u16 bridges[MXL862XX_MAX_BRIDGES + 1];
+ u16 evlan_ingress_size;
--- /dev/null
+From 3cb224514226928df80e43ca2280c7dca654bdfe Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Wed, 25 Mar 2026 21:39:30 +0000
+Subject: [PATCH 18/35] net: dsa: mxl862xx: move phylink stubs to
+ mxl862xx-phylink.c
+
+Move the phylink MAC operations and get_caps callback from mxl862xx.c
+into a dedicated mxl862xx-phylink.c file. This prepares for the SerDes
+PCS implementation which adds substantial phylink/PCS code -- keeping
+it in a separate file avoids function-position churn in the main
+driver file.
+
+No functional change.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/Makefile | 2 +-
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 51 +++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.h | 14 ++++++
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 38 +--------------
+ 4 files changed, 67 insertions(+), 38 deletions(-)
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
+
+--- a/drivers/net/dsa/mxl862xx/Makefile
++++ b/drivers/net/dsa/mxl862xx/Makefile
+@@ -1,3 +1,3 @@
+ # SPDX-License-Identifier: GPL-2.0
+ obj-$(CONFIG_NET_DSA_MXL862) += mxl862xx_dsa.o
+-mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o
++mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+@@ -0,0 +1,51 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * Phylink and PCS support for MaxLinear MxL862xx switch family
++ *
++ * Copyright (C) 2024 MaxLinear Inc.
++ * Copyright (C) 2025 John Crispin <john@phrozen.org>
++ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
++ */
++
++#include <linux/phylink.h>
++#include <net/dsa.h>
++
++#include "mxl862xx.h"
++#include "mxl862xx-phylink.h"
++
++void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
++ struct phylink_config *config)
++{
++ config->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
++ MAC_100 | MAC_1000 | MAC_2500FD;
++
++ __set_bit(PHY_INTERFACE_MODE_INTERNAL,
++ config->supported_interfaces);
++}
++
++static void mxl862xx_phylink_mac_config(struct phylink_config *config,
++ unsigned int mode,
++ const struct phylink_link_state *state)
++{
++}
++
++static void mxl862xx_phylink_mac_link_down(struct phylink_config *config,
++ unsigned int mode,
++ phy_interface_t interface)
++{
++}
++
++static void mxl862xx_phylink_mac_link_up(struct phylink_config *config,
++ struct phy_device *phydev,
++ unsigned int mode,
++ phy_interface_t interface,
++ int speed, int duplex,
++ bool tx_pause, bool rx_pause)
++{
++}
++
++const struct phylink_mac_ops mxl862xx_phylink_mac_ops = {
++ .mac_config = mxl862xx_phylink_mac_config,
++ .mac_link_down = mxl862xx_phylink_mac_link_down,
++ .mac_link_up = mxl862xx_phylink_mac_link_up,
++};
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
+@@ -0,0 +1,14 @@
++/* SPDX-License-Identifier: GPL-2.0-or-later */
++
++#ifndef __MXL862XX_PHYLINK_H
++#define __MXL862XX_PHYLINK_H
++
++#include <linux/phylink.h>
++
++#include "mxl862xx.h"
++
++extern const struct phylink_mac_ops mxl862xx_phylink_mac_ops;
++void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
++ struct phylink_config *config);
++
++#endif /* __MXL862XX_PHYLINK_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -22,6 +22,7 @@
+ #include "mxl862xx-api.h"
+ #include "mxl862xx-cmd.h"
+ #include "mxl862xx-host.h"
++#include "mxl862xx-phylink.h"
+
+ #define MXL862XX_API_WRITE(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), false, false)
+@@ -1642,16 +1643,6 @@ static void mxl862xx_port_teardown(struc
+ priv->ports[port].setup_done = false;
+ }
+
+-static void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
+- struct phylink_config *config)
+-{
+- config->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
+- MAC_100 | MAC_1000 | MAC_2500FD;
+-
+- __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+- config->supported_interfaces);
+-}
+-
+ static int mxl862xx_get_fid(struct dsa_switch *ds, struct dsa_db db)
+ {
+ struct mxl862xx_priv *priv = ds->priv;
+@@ -2297,33 +2288,6 @@ static const struct dsa_switch_ops mxl86
+ .get_stats64 = mxl862xx_get_stats64,
+ };
+
+-static void mxl862xx_phylink_mac_config(struct phylink_config *config,
+- unsigned int mode,
+- const struct phylink_link_state *state)
+-{
+-}
+-
+-static void mxl862xx_phylink_mac_link_down(struct phylink_config *config,
+- unsigned int mode,
+- phy_interface_t interface)
+-{
+-}
+-
+-static void mxl862xx_phylink_mac_link_up(struct phylink_config *config,
+- struct phy_device *phydev,
+- unsigned int mode,
+- phy_interface_t interface,
+- int speed, int duplex,
+- bool tx_pause, bool rx_pause)
+-{
+-}
+-
+-static const struct phylink_mac_ops mxl862xx_phylink_mac_ops = {
+- .mac_config = mxl862xx_phylink_mac_config,
+- .mac_link_down = mxl862xx_phylink_mac_link_down,
+- .mac_link_up = mxl862xx_phylink_mac_link_up,
+-};
+-
+ static int mxl862xx_probe(struct mdio_device *mdiodev)
+ {
+ struct device *dev = &mdiodev->dev;
--- /dev/null
+From de41d438c4e90876449715a307dd03fa37338742 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Thu, 26 Mar 2026 01:50:00 +0000
+Subject: [PATCH 19/35] net: dsa: mxl862xx: move API macros to mxl862xx-host.h
+
+Move the MXL862XX_API_WRITE, MXL862XX_API_READ and
+MXL862XX_API_READ_QUIET convenience macros from mxl862xx.c to
+mxl862xx-host.h next to the mxl862xx_api_wrap() prototype they wrap.
+This makes them available to other compilation units that include
+mxl862xx-host.h, which is needed once the SerDes PCS code in
+mxl862xx-phylink.c also calls firmware commands.
+
+No functional change.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-host.h | 8 ++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 7 -------
+ 2 files changed, 8 insertions(+), 7 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+@@ -9,6 +9,14 @@ void mxl862xx_host_init(struct mxl862xx_
+ void mxl862xx_host_shutdown(struct mxl862xx_priv *priv);
+ int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *data, u16 size,
+ bool read, bool quiet);
++
++#define MXL862XX_API_WRITE(dev, cmd, data) \
++ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), false, false)
++#define MXL862XX_API_READ(dev, cmd, data) \
++ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, false)
++#define MXL862XX_API_READ_QUIET(dev, cmd, data) \
++ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
++
+ int mxl862xx_reset(struct mxl862xx_priv *priv);
+
+ #endif /* __MXL862XX_HOST_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -24,13 +24,6 @@
+ #include "mxl862xx-host.h"
+ #include "mxl862xx-phylink.h"
+
+-#define MXL862XX_API_WRITE(dev, cmd, data) \
+- mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), false, false)
+-#define MXL862XX_API_READ(dev, cmd, data) \
+- mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, false)
+-#define MXL862XX_API_READ_QUIET(dev, cmd, data) \
+- mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
+-
+ /* Polling interval for RMON counter accumulation. At 2.5 Gbps with
+ * minimum-size (64-byte) frames, a 32-bit packet counter wraps in ~880s.
+ * 2s gives a comfortable margin.
--- /dev/null
+From 88f46eb32d1aed296af2005c3ed8f23a6eea64c3 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sun, 22 Mar 2026 00:57:44 +0000
+Subject: [PATCH 20/35] net: dsa: mxl862xx: add support for SerDes ports
+
+The MxL862xx has two XPCS/SerDes interfaces (XPCS0 for ports 9-12,
+XPCS1 for ports 13-16). Each can operate in various single-lane
+modes (SGMII, 1000BASE-X, 2500BASE-X, 10GBASE-R, 10GBASE-KR,
+USXGMII) or as QSGMII providing four sub-ports.
+
+Implement phylink PCS operations using the firmware's XPCS API:
+
+ - pcs_pre_config: power-sequence the SerDes (hard reset if already
+ running, then PCS_ENABLE with the target interface mode), polling
+ SIGNAL_DETECT until the XPCS exits reset.
+ - pcs_config: configure negotiation mode and CL37/SGMII advertising.
+ - pcs_get_state: read link/speed/duplex/LPA from firmware and decode
+ using phylink's standard CL37, SGMII, and USXGMII decoders, with
+ firmware-resolved speed/duplex override for downshift detection.
+ - pcs_an_restart: restart CL37 or CL73 auto-negotiation.
+ - pcs_link_up: force speed/duplex for SGMII.
+ - pcs_inband_caps: report per-mode in-band status capabilities.
+
+Register a PCS instance for each SerDes port and QSGMII sub-port
+during setup. Advertise the supported interface modes in
+phylink_get_caps based on port number.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 474 +++++++++++++++++++-
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 13 +
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 411 ++++++++++++++++-
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.h | 2 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 5 +-
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 19 +
+ 6 files changed, 907 insertions(+), 17 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -1185,6 +1185,242 @@ struct mxl862xx_ctp_port_assignment {
+ } __packed;
+
+ /**
++ * enum mxl862xx_port_duplex - Ethernet port duplex status
++ * @MXL862XX_DUPLEX_FULL: Port operates in full-duplex mode
++ * @MXL862XX_DUPLEX_HALF: Port operates in half-duplex mode
++ * @MXL862XX_DUPLEX_AUTO: Port operates in Auto mode
++ */
++enum mxl862xx_port_duplex {
++ MXL862XX_DUPLEX_FULL = 0,
++ MXL862XX_DUPLEX_HALF,
++ MXL862XX_DUPLEX_AUTO,
++};
++
++/**
++ * enum mxl862xx_port_type - Port Type
++ * @MXL862XX_LOGICAL_PORT: Logical Port
++ * @MXL862XX_PHYSICAL_PORT: Physical Port
++ * @MXL862XX_CTP_PORT: Connectivity Termination Port (CTP)
++ * @MXL862XX_BRIDGE_PORT: Bridge Port
++ */
++enum mxl862xx_port_type {
++ MXL862XX_LOGICAL_PORT = 0,
++ MXL862XX_PHYSICAL_PORT,
++ MXL862XX_CTP_PORT,
++ MXL862XX_BRIDGE_PORT,
++};
++
++/**
++ * enum mxl862xx_port_enable - port enable type selection.
++ * @MXL862XX_PORT_DISABLE: the port is disabled in both directions
++ * @MXL862XX_PORT_ENABLE_RXTX: the port is enabled in both directions
++ * @MXL862XX_PORT_ENABLE_RX: the port is enabled in the receive direction
++ * @MXL862XX_PORT_ENABLE_TX: the port is enabled in the transmit direction
++ */
++enum mxl862xx_port_enable{
++ MXL862XX_PORT_DISABLE = 0,
++ MXL862XX_PORT_ENABLE_RXTX,
++ MXL862XX_PORT_ENABLE_RX,
++ MXL862XX_PORT_ENABLE_TX,
++};
++
++/**
++ * enum mxl862xx_port_flow - ethernet flow control status
++ * @MXL862XX_FLOW_AUTO: automatic flow control
++ * @MXL862XX_FLOW_RX: receive flow control only
++ * @MXL862XX_FLOW_TX: transmit flow control only
++ * @MXL862XX_FLOW_RXTX: receive and transmit flow control
++ * @MXL862XX_FLOW_OFF: no flow control
++ */
++enum mxl862xx_port_flow {
++ MXL862XX_FLOW_AUTO = 0,
++ MXL862XX_FLOW_RX,
++ MXL862XX_FLOW_TX,
++ MXL862XX_FLOW_RXTX,
++ MXL862XX_FLOW_OFF,
++};
++
++/**
++ * enum mxl862xx_port_monitor - port mirror options
++ * @MXL862XX_PORT_MONITOR_NONE: mirroring is disabled
++ * @MXL862XX_PORT_MONITOR_RX: ingress packets are mirrored
++ * @MXL862XX_PORT_MONITOR_TX: egress packets are mirrored
++ * @MXL862XX_PORT_MONITOR_RXTX: ingress and egress packets are mirrored
++ * @MXL862XX_PORT_MONITOR_VLAN_UNKNOWN: mirroring of 'unknown VLAN violation' frames
++ * @MXL862XX_PORT_MONITOR_VLAN_MEMBERSHIP: mirroring of 'VLAN ingress or egress membership
++ violation' frames
++ * @MXL862XX_PORT_MONITOR_PORT_STATE: mirroring of 'port state violation' frames
++ * @MXL862XX_PORT_MONITOR_LEARNING_LIMIT: mirroring of 'MAC learning limit violation' frames
++ * @MXL862XX_PORT_MONITOR_PORT_LOCK: mirroring of 'port lock violation' frames
++ */
++enum mxl862xx_port_monitor {
++ MXL862XX_PORT_MONITOR_NONE = 0,
++ MXL862XX_PORT_MONITOR_RX,
++ MXL862XX_PORT_MONITOR_TX,
++ MXL862XX_PORT_MONITOR_RXTX,
++ MXL862XX_PORT_MONITOR_VLAN_UNKNOWN,
++ MXL862XX_PORT_MONITOR_VLAN_MEMBERSHIP = 16,
++ MXL862XX_PORT_MONITOR_PORT_STATE = 32,
++ MXL862XX_PORT_MONITOR_LEARNING_LIMIT = 64,
++ MXL862XX_PORT_MONITOR_PORT_LOCK = 128,
++};
++
++/**
++ * enum mxl862xx_if_rmon_mode - interface RMON counter mode
++ * @MXL862XX_IF_RMON_FID: FID based RMON counters
++ * @MXL862XX_IF_RMON_SUBID: sub-interface ID based
++ * @MXL862XX_IF_RMON_FLOWID_LSB: flow ID based (bits 3:0)
++ * @MXL862XX_IF_RMON_FLOWID_MSB: flow ID based (bits 7:4)
++ */
++enum mxl862xx_if_rmon_mode {
++ MXL862XX_IF_RMON_FID = 0,
++ MXL862XX_IF_RMON_SUBID,
++ MXL862XX_IF_RMON_FLOWID_LSB,
++ MXL862XX_IF_RMON_FLOWID_MSB,
++};
++
++/**
++ * struct mxl862xx_port_cfg - Port Configuration Parameters
++ * @port_type: See &enum mxl862xx_port_type
++ * @port_id: Ethernet Port number (zero-based counting)
++ * @enable: See &enum mxl862xx_port_enable
++ * @unicast_unknown_drop: Drop unknown unicast packets
++ * @multicast_unknown_drop: Drop unknown multicast packets
++ * @reserved_packet_drop: Drop reserved packet types
++ * @broadcast_drop: Drop broadcast packets
++ * @aging: Enables MAC address table aging.
++ * @learning: MAC address table learning
++ * @learning_mac_port_lock: Automatic MAC address table learning locking on the port
++ * @learning_limit: Automatic MAC address table learning limitation on this port
++ * @mac_spoofing_detection: MAC spoofing detection. Identifies ingress packets that carry
++ * a MAC source address which was previously learned on a different ingress port
++ * @flow_ctrl: See &enum mxl862xx_port_flow
++ * @port_monitor: See &enum mxl862xx_port_monitor
++ * @if_counters: Assign Interface RMON Counters for this Port
++ * @if_count_start_idx: Interface RMON Counters Start Index
++ * @if_rmonmode: See &enum mxl862xx_if_rmon_mode
++ */
++struct mxl862xx_port_cfg {
++ __le32 port_type; /* enum mxl862xx_port_type */
++ __le16 port_id;
++ __le32 enable; /* enum mxl862xx_port_enable */
++ u8 unicast_unknown_drop;
++ u8 multicast_unknown_drop;
++ u8 reserved_packet_drop;
++ u8 broadcast_drop;
++ u8 aging;
++ u8 learning;
++ u8 learning_mac_port_lock;
++ __le16 learning_limit;
++ u8 mac_spoofing_detection;
++ __le32 flow_ctrl; /* enum mxl862xx_port_flow */
++ __le32 port_monitor; /* enum mxl862xx_port_monitor */
++ u8 if_counters;
++ __le32 if_count_start_idx;
++ __le32 if_rmonmode; /* enum mxl862xx_if_rmon_mode */
++} __packed;
++
++/**
++ * enum mxl862xx_port_speed - Ethernet port speed mode
++ * @MXL862XX_PORT_SPEED_10: 10 Mbit/s
++ * @MXL862XX_PORT_SPEED_100: 100 Mbit/s
++ * @MXL862XX_PORT_SPEED_200: 200 Mbit/s
++ * @MXL862XX_PORT_SPEED_1000: 1000 Mbit/s
++ * @MXL862XX_PORT_SPEED_2500: 2.5 Gbit/s
++ * @MXL862XX_PORT_SPEED_5000: 5 Gbit/s
++ * @MXL862XX_PORT_SPEED_10000: 10 Gbit/s
++ * @MXL862XX_PORT_SPEED_AUTO: Auto speed for XGMAC
++ */
++enum mxl862xx_port_speed {
++ MXL862XX_PORT_SPEED_10 = 0,
++ MXL862XX_PORT_SPEED_100,
++ MXL862XX_PORT_SPEED_200,
++ MXL862XX_PORT_SPEED_1000,
++ MXL862XX_PORT_SPEED_2500,
++ MXL862XX_PORT_SPEED_5000,
++ MXL862XX_PORT_SPEED_10000,
++ MXL862XX_PORT_SPEED_AUTO,
++};
++
++/**
++ * enum mxl862xx_port_link - Force the MAC and PHY link modus
++ * @MXL862XX_PORT_LINK_UP: Link up
++ * @MXL862XX_PORT_LINK_DOWN: Link down
++ * @MXL862XX_PORT_LINK_AUTO: Link Auto
++ */
++enum mxl862xx_port_link {
++ MXL862XX_PORT_LINK_UP = 0,
++ MXL862XX_PORT_LINK_DOWN,
++ MXL862XX_PORT_LINK_AUTO,
++};
++
++/**
++ * enum mxl862xx_mii_mode - Ethernet port interface mode
++ * @MXL862XX_PORT_HW_MII: Normal PHY interface
++ * @MXL862XX_PORT_HW_RMII: Reduced MII interface in normal mode
++ * @MXL862XX_PORT_HW_GMII: GMII or MII, depending upon the speed
++ * @MXL862XX_PORT_HW_RGMII: RGMII mode
++ * @MXL862XX_PORT_HW_XGMII: XGMII mode
++ */
++enum mxl862xx_mii_mode {
++ MXL862XX_PORT_HW_MII = 0,
++ MXL862XX_PORT_HW_RMII,
++ MXL862XX_PORT_HW_GMII,
++ MXL862XX_PORT_HW_RGMII,
++ MXL862XX_PORT_HW_XGMII,
++};
++
++/**
++ * enum mxl862xx_mii_type - Ethernet port configuration for PHY or MAC mode
++ * @MXL862XX_PORT_MAC: The Ethernet port is configured to work in MAC mode
++ * @MXL862XX_PORT_PHY: The Ethernet port is configured to work in PHY mode
++ */
++enum mxl862xx_mii_type {
++ MXL862XX_PORT_MAC = 0,
++ MXL862XX_PORT_PHY,
++};
++
++/**
++ * enum mxl862xx_clk_mode - Ethernet port clock source configuration
++ * @MXL862XX_PORT_CLK_NA: Clock Mode not applicable
++ * @MXL862XX_PORT_CLK_MASTER: Clock Master Mode. The port is configured to provide the clock as output signal
++ * @MXL862XX_PORT_CLK_SLAVE: Clock Slave Mode. The port is configured to use the input clock signal
++ */
++enum mxl862xx_clk_mode {
++ MXL862XX_PORT_CLK_NA = 0,
++ MXL862XX_PORT_CLK_MASTER,
++ MXL862XX_PORT_CLK_SLAVE,
++};
++
++/**
++ * struct mxl862xx_port_link_cfg - Ethernet port link, speed status and flow control status
++ * @port_id: Ethernet Port number
++ * @duplex_force: Force Port Duplex Mode
++ * @duplex: See &enum mxl862xx_port_duplex
++ * @speed_force: Force Link Speed
++ * @speed: See &enum mxl862xx_port_speed
++ * @link_force: Force Link
++ * @link: See &enum mxl862xx_port_link
++ * @mii_mode: See &enum mxl862xx_mii_mode
++ * @mii_type: See &enum mxl862xx_mii_type
++ * @clk_mode: See &enum mxl862xx_clk_mode
++ * @lpi: 'Low Power Idle' Support for 'Energy Efficient Ethernet'
++ */
++struct mxl862xx_port_link_cfg {
++ __le16 port_id;
++ u8 duplex_force;
++ __le32 duplex; /* enum mxl862xx_port_duplex */
++ u8 speed_force;
++ __le32 speed; /* enum mxl862xx_port_speed */
++ u8 link_force;
++ __le32 link; /* enum mxl862xx_port_link */
++ __le32 mii_mode; /* enum mxl862xx_mii_mode */
++ __le32 mii_type; /* enum mxl862xx_mii_type */
++ __le32 clk_mode; /* enum mxl862xx_clk_mode */
++ u8 lpi;
++} __packed;
++
++/**
+ * enum mxl862xx_stp_port_state - Spanning Tree Protocol port states
+ * @MXL862XX_STP_PORT_STATE_FORWARD: Forwarding state
+ * @MXL862XX_STP_PORT_STATE_DISABLE: Disabled/Discarding state
+@@ -1225,20 +1461,6 @@ struct mxl862xx_sys_fw_image_version {
+ } __packed;
+
+ /**
+- * enum mxl862xx_port_type - Port Type
+- * @MXL862XX_LOGICAL_PORT: Logical Port
+- * @MXL862XX_PHYSICAL_PORT: Physical Port
+- * @MXL862XX_CTP_PORT: Connectivity Termination Port (CTP)
+- * @MXL862XX_BRIDGE_PORT: Bridge Port
+- */
+-enum mxl862xx_port_type {
+- MXL862XX_LOGICAL_PORT = 0,
+- MXL862XX_PHYSICAL_PORT,
+- MXL862XX_CTP_PORT,
+- MXL862XX_BRIDGE_PORT,
+-};
+-
+-/**
+ * enum mxl862xx_rmon_port_type - RMON counter table type
+ * @MXL862XX_RMON_CTP_PORT_RX: CTP RX counters
+ * @MXL862XX_RMON_CTP_PORT_TX: CTP TX counters
+@@ -1366,4 +1588,228 @@ struct mxl862xx_rmon_port_cnt {
+ __le64 tx_good_bytes;
+ } __packed;
+
++/**
++ * enum mxl862xx_xpcs_if_mode - XPCS interface mode
++ * @MXL862XX_XPCS_IF_SGMII: SGMII
++ * @MXL862XX_XPCS_IF_1000BASEX: 1000BASE-X
++ * @MXL862XX_XPCS_IF_2500BASEX: 2500BASE-X
++ * @MXL862XX_XPCS_IF_USXGMII: USXGMII
++ * @MXL862XX_XPCS_IF_10GBASER: 10GBASE-R
++ * @MXL862XX_XPCS_IF_10GKR: 10GBASE-KR
++ * @MXL862XX_XPCS_IF_5GBASER: 5GBASE-R
++ * @MXL862XX_XPCS_IF_QSGMII: QSGMII
++ */
++enum mxl862xx_xpcs_if_mode {
++ MXL862XX_XPCS_IF_SGMII = 0,
++ MXL862XX_XPCS_IF_1000BASEX = 1,
++ MXL862XX_XPCS_IF_2500BASEX = 2,
++ MXL862XX_XPCS_IF_USXGMII = 3,
++ MXL862XX_XPCS_IF_10GBASER = 4,
++ MXL862XX_XPCS_IF_10GKR = 5,
++ MXL862XX_XPCS_IF_5GBASER = 6,
++ MXL862XX_XPCS_IF_QSGMII = 7,
++};
++
++/**
++ * enum mxl862xx_xpcs_neg_mode - PCS negotiation mode
++ * @MXL862XX_XPCS_NEG_NONE: no inband negotiation
++ * @MXL862XX_XPCS_NEG_INBAND_AN_OFF: inband selected but AN disabled
++ * @MXL862XX_XPCS_NEG_INBAND_AN_ON: inband with AN enabled
++ */
++enum mxl862xx_xpcs_neg_mode {
++ MXL862XX_XPCS_NEG_NONE = 0,
++ MXL862XX_XPCS_NEG_INBAND_AN_OFF = 1,
++ MXL862XX_XPCS_NEG_INBAND_AN_ON = 2,
++};
++
++/**
++ * enum mxl862xx_xpcs_speed - PCS speed values
++ * @MXL862XX_XPCS_SPEED_UNKNOWN: unknown speed
++ * @MXL862XX_XPCS_SPEED_10: 10 Mbps
++ * @MXL862XX_XPCS_SPEED_100: 100 Mbps
++ * @MXL862XX_XPCS_SPEED_1000: 1000 Mbps
++ * @MXL862XX_XPCS_SPEED_2500: 2500 Mbps
++ * @MXL862XX_XPCS_SPEED_5000: 5000 Mbps
++ * @MXL862XX_XPCS_SPEED_10000: 10000 Mbps
++ */
++enum mxl862xx_xpcs_speed {
++ MXL862XX_XPCS_SPEED_UNKNOWN = 0,
++ MXL862XX_XPCS_SPEED_10 = 10,
++ MXL862XX_XPCS_SPEED_100 = 100,
++ MXL862XX_XPCS_SPEED_1000 = 1000,
++ MXL862XX_XPCS_SPEED_2500 = 2500,
++ MXL862XX_XPCS_SPEED_5000 = 5000,
++ MXL862XX_XPCS_SPEED_10000 = 10000,
++};
++
++/**
++ * enum mxl862xx_xpcs_duplex - PCS duplex mode
++ * @MXL862XX_XPCS_DUPLEX_HALF: half duplex
++ * @MXL862XX_XPCS_DUPLEX_FULL: full duplex
++ */
++enum mxl862xx_xpcs_duplex {
++ MXL862XX_XPCS_DUPLEX_HALF = 0,
++ MXL862XX_XPCS_DUPLEX_FULL = 1,
++};
++
++/**
++ * enum mxl862xx_xpcs_loopback_mode - XPCS loopback mode
++ * @MXL862XX_XPCS_LB_DISABLE: disable all loopback
++ * @MXL862XX_XPCS_LB_PCS_SERIAL: PCS TX-to-RX serial loopback
++ * @MXL862XX_XPCS_LB_PCS_PARALLEL: PCS RX-to-TX parallel loopback
++ * @MXL862XX_XPCS_LB_PMA_SERIAL: PMA TX-to-RX serial loopback
++ * @MXL862XX_XPCS_LB_PMA_PARALLEL: PMA RX-to-TX parallel loopback
++ */
++enum mxl862xx_xpcs_loopback_mode {
++ MXL862XX_XPCS_LB_DISABLE = 0,
++ MXL862XX_XPCS_LB_PCS_SERIAL = 1,
++ MXL862XX_XPCS_LB_PCS_PARALLEL = 2,
++ MXL862XX_XPCS_LB_PMA_SERIAL = 3,
++ MXL862XX_XPCS_LB_PMA_PARALLEL = 4,
++};
++
++/**
++ * enum mxl862xx_xpcs_reset_type - XPCS reset type
++ * @MXL862XX_XPCS_RESET_VR: vendor-specific reset (fast)
++ * @MXL862XX_XPCS_RESET_SOFT: PCS soft reset
++ * @MXL862XX_XPCS_RESET_HARD: full hardware reset
++ */
++enum mxl862xx_xpcs_reset_type {
++ MXL862XX_XPCS_RESET_VR = 0,
++ MXL862XX_XPCS_RESET_SOFT = 1,
++ MXL862XX_XPCS_RESET_HARD = 2,
++};
++
++/**
++ * struct mxl862xx_xpcs_pcs_cfg - PCS configuration
++ * @port_id: XPCS port index (0 or 1)
++ * @interface: interface mode (enum mxl862xx_xpcs_if_mode)
++ * @neg_mode: negotiation mode (enum mxl862xx_xpcs_neg_mode)
++ * @permit_pause: allow pause to MAC
++ * @usx_lane_mode: USXGMII lane mode (0=single, 1=quad)
++ * @phy_side: PHY side (1) or MAC side (0)
++ * @advertising: CL37 advertisement word
++ * @result: firmware result (>0 AN restart needed, 0 no change, <0 error)
++ */
++struct mxl862xx_xpcs_pcs_cfg {
++ u8 port_id:2;
++ u8 interface:6;
++ u8 neg_mode:2;
++ u8 permit_pause:1;
++ u8 usx_lane_mode:2;
++ u8 phy_side:1;
++ u8 __rsv:2;
++ __le16 advertising;
++ __le16 result;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_pcs_state - PCS link state
++ * @port_id: XPCS port index (0 or 1)
++ * @interface: interface mode (enum mxl862xx_xpcs_if_mode)
++ * @usx_lane_mode: USXGMII lane mode
++ * @usx_subport: USXGMII sub-port index (0-3)
++ * @link: link up
++ * @an_complete: auto-negotiation complete
++ * @duplex: duplex mode (enum mxl862xx_xpcs_duplex)
++ * @pcs_fault: PCS fault detected
++ * @pause: pause flags (bit 0 = symmetric, bit 1 = asymmetric)
++ * @speed: link speed in Mbps (enum mxl862xx_xpcs_speed)
++ * @lpa: raw link partner advertisement word
++ */
++struct mxl862xx_xpcs_pcs_state {
++ u8 port_id:2;
++ u8 interface:6;
++ u8 usx_lane_mode:2;
++ u8 usx_subport:2;
++ u8 link:1;
++ u8 an_complete:1;
++ u8 duplex:1;
++ u8 pcs_fault:1;
++ u8 pause:2;
++ u8 __rsv:6;
++ u8 __pad;
++ __le16 speed;
++ __le16 lpa;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_pcs_power - PCS enable/disable
++ * @port_id: XPCS port index (0 or 1)
++ * @interface: interface mode (enum mxl862xx_xpcs_if_mode)
++ * @phy_side: PHY side (1) or MAC side (0)
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_pcs_power {
++ u8 port_id:2;
++ u8 interface:6;
++ u8 phy_side:1;
++ u8 __rsv:7;
++ __le16 result;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_an_restart - AN restart parameters
++ * @port_id: XPCS port index (0 or 1)
++ * @interface: interface mode (enum mxl862xx_xpcs_if_mode)
++ * @usx_lane_mode: USXGMII lane mode
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_an_restart {
++ u8 port_id:2;
++ u8 interface:6;
++ u8 usx_lane_mode:2;
++ u8 __rsv:6;
++ __le16 result;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_an_disable - AN disable parameters
++ * @port_id: XPCS port index
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_an_disable {
++ u8 port_id;
++ u8 __pad;
++ __le16 result;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_force_speed - force PCS speed and duplex
++ * @port_id: XPCS port index
++ * @duplex: duplex mode (enum mxl862xx_xpcs_duplex)
++ * @speed: speed in Mbps (enum mxl862xx_xpcs_speed)
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_force_speed {
++ u8 port_id;
++ u8 duplex;
++ __le16 speed;
++ __le16 result;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_loopback_cfg - loopback control
++ * @port_id: XPCS port index
++ * @mode: loopback mode (enum mxl862xx_xpcs_loopback_mode)
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_loopback_cfg {
++ u8 port_id;
++ u8 mode;
++ __le16 result;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_reset_cfg - XPCS reset
++ * @port_id: XPCS port index
++ * @reset_type: reset type (enum mxl862xx_xpcs_reset_type)
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_reset_cfg {
++ u8 port_id;
++ u8 reset_type;
++ __le16 result;
++} __packed;
++
+ #endif /* __MXL862XX_API_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -25,6 +25,8 @@
+ #define GPY_GPY2XX_MAGIC 0x1800
+ #define SYS_MISC_MAGIC 0x1900
+
++#define MXL862XX_COMMON_PORTLINKCFGGET (MXL862XX_COMMON_MAGIC + 0x5)
++#define MXL862XX_COMMON_PORTCFGGET (MXL862XX_COMMON_MAGIC + 0x7)
+ #define MXL862XX_COMMON_CFGGET (MXL862XX_COMMON_MAGIC + 0x9)
+ #define MXL862XX_COMMON_CFGSET (MXL862XX_COMMON_MAGIC + 0xa)
+ #define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11)
+@@ -71,6 +73,17 @@
+
+ #define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2)
+
++#define MXL862XX_XPCS_MAGIC 0x1a00
++#define MXL862XX_XPCS_PCS_CONFIG (MXL862XX_XPCS_MAGIC + 0x1)
++#define MXL862XX_XPCS_PCS_GET_STATE (MXL862XX_XPCS_MAGIC + 0x2)
++#define MXL862XX_XPCS_PCS_ENABLE (MXL862XX_XPCS_MAGIC + 0x3)
++#define MXL862XX_XPCS_PCS_DISABLE (MXL862XX_XPCS_MAGIC + 0x4)
++#define MXL862XX_XPCS_AN_RESTART (MXL862XX_XPCS_MAGIC + 0x5)
++#define MXL862XX_XPCS_AN_DISABLE (MXL862XX_XPCS_MAGIC + 0x6)
++#define MXL862XX_XPCS_FORCE_SPEED (MXL862XX_XPCS_MAGIC + 0x7)
++#define MXL862XX_XPCS_LOOPBACK (MXL862XX_XPCS_MAGIC + 0x8)
++#define MXL862XX_XPCS_RESET (MXL862XX_XPCS_MAGIC + 0x9)
++
+ #define MMD_API_MAXIMUM_ID 0x7fff
+
+ #endif /* __MXL862XX_CMD_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+@@ -7,10 +7,14 @@
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ */
+
++#include <linux/iopoll.h>
+ #include <linux/phylink.h>
+ #include <net/dsa.h>
+
+ #include "mxl862xx.h"
++#include "mxl862xx-api.h"
++#include "mxl862xx-cmd.h"
++#include "mxl862xx-host.h"
+ #include "mxl862xx-phylink.h"
+
+ void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
+@@ -19,8 +23,393 @@ void mxl862xx_phylink_get_caps(struct ds
+ config->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
+ MAC_100 | MAC_1000 | MAC_2500FD;
+
+- __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+- config->supported_interfaces);
++ switch (port) {
++ case 1 ... 8:
++ __set_bit(PHY_INTERFACE_MODE_INTERNAL,
++ config->supported_interfaces);
++ break;
++ case 9:
++ case 13:
++ __set_bit(PHY_INTERFACE_MODE_SGMII, config->supported_interfaces);
++ __set_bit(PHY_INTERFACE_MODE_1000BASEX, config->supported_interfaces);
++ __set_bit(PHY_INTERFACE_MODE_2500BASEX, config->supported_interfaces);
++ __set_bit(PHY_INTERFACE_MODE_10GBASER, config->supported_interfaces);
++ __set_bit(PHY_INTERFACE_MODE_10GKR, config->supported_interfaces);
++ __set_bit(PHY_INTERFACE_MODE_USXGMII, config->supported_interfaces);
++ config->mac_capabilities |= MAC_10000FD | MAC_5000FD;
++ fallthrough;
++ case 10 ... 12:
++ case 14 ... 16:
++ __set_bit(PHY_INTERFACE_MODE_QSGMII, config->supported_interfaces);
++ break;
++ default:
++ break;
++ }
++}
++
++static struct mxl862xx_pcs *pcs_to_mxl862xx_pcs(struct phylink_pcs *pcs)
++{
++ return container_of(pcs, struct mxl862xx_pcs, pcs);
++}
++
++static int mxl862xx_xpcs_port_id(int port)
++{
++ return port >= 13;
++}
++
++static int mxl862xx_xpcs_if_mode(phy_interface_t interface)
++{
++ switch (interface) {
++ case PHY_INTERFACE_MODE_SGMII:
++ return MXL862XX_XPCS_IF_SGMII;
++ case PHY_INTERFACE_MODE_QSGMII:
++ return MXL862XX_XPCS_IF_QSGMII;
++ case PHY_INTERFACE_MODE_1000BASEX:
++ return MXL862XX_XPCS_IF_1000BASEX;
++ case PHY_INTERFACE_MODE_2500BASEX:
++ return MXL862XX_XPCS_IF_2500BASEX;
++ case PHY_INTERFACE_MODE_USXGMII:
++ return MXL862XX_XPCS_IF_USXGMII;
++ case PHY_INTERFACE_MODE_10GBASER:
++ return MXL862XX_XPCS_IF_10GBASER;
++ case PHY_INTERFACE_MODE_10GKR:
++ return MXL862XX_XPCS_IF_10GKR;
++ default:
++ return -EINVAL;
++ }
++}
++
++static int mxl862xx_xpcs_neg_mode(unsigned int neg_mode)
++{
++ if (!(neg_mode & PHYLINK_PCS_NEG_INBAND))
++ return MXL862XX_XPCS_NEG_NONE;
++ if (neg_mode & PHYLINK_PCS_NEG_ENABLED)
++ return MXL862XX_XPCS_NEG_INBAND_AN_ON;
++ return MXL862XX_XPCS_NEG_INBAND_AN_OFF;
++}
++
++static struct mxl862xx_xpcs_signal_detect
++mxl862xx_xpcs_signal_detect(struct mxl862xx_priv *priv, int port_id)
++{
++ struct mxl862xx_xpcs_signal_detect sd = { .port_id = port_id };
++
++ MXL862XX_API_READ(priv, MXL862XX_XPCS_SIGNAL_DETECT, sd);
++
++ return sd;
++}
++
++static int mxl862xx_xpcs_poll_ready(struct mxl862xx_priv *priv, int port_id)
++{
++ struct mxl862xx_xpcs_signal_detect sd;
++ int ret;
++
++ ret = read_poll_timeout(mxl862xx_xpcs_signal_detect, sd,
++ !sd.in_reset, 50000, 1000000,
++ false, priv, port_id);
++ if (ret)
++ dev_warn(priv->ds->dev, "XPCS%d ready timeout\n", port_id);
++
++ return ret;
++}
++
++static void mxl862xx_pcs_disable(struct phylink_pcs *pcs)
++{
++ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
++ struct mxl862xx_priv *priv = mpcs->priv;
++ int port = mpcs->port;
++ struct mxl862xx_xpcs_pcs_power pwr = {};
++
++ if (port != 9 && port != 13)
++ return;
++
++ pwr.port_id = mxl862xx_xpcs_port_id(port);
++
++ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PCS_DISABLE, pwr);
++ mpcs->enabled = false;
++}
++
++static void mxl862xx_pcs_pre_config(struct phylink_pcs *pcs,
++ phy_interface_t interface)
++{
++ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
++ struct mxl862xx_priv *priv = mpcs->priv;
++ int port = mpcs->port;
++ struct mxl862xx_xpcs_pcs_power pwr = {};
++ struct mxl862xx_xpcs_reset_cfg rst = {};
++ int port_id, if_mode;
++
++ if (port != 9 && port != 13)
++ return;
++
++ if_mode = mxl862xx_xpcs_if_mode(interface);
++ if (if_mode < 0)
++ return;
++
++ port_id = mxl862xx_xpcs_port_id(port);
++
++ /* Full reset only if PCS is already running (not after a clean disable,
++ * which already asserts hardware reset via XPCS_PCS_DISABLE).
++ */
++ if (mpcs->enabled) {
++ rst.port_id = port_id;
++ rst.reset_type = MXL862XX_XPCS_RESET_HARD;
++ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_RESET, rst);
++ mxl862xx_xpcs_poll_ready(priv, port_id);
++ }
++
++ pwr.port_id = port_id;
++ pwr.interface = if_mode;
++ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PCS_ENABLE, pwr);
++ mxl862xx_xpcs_poll_ready(priv, port_id);
++ mpcs->enabled = true;
++}
++
++static int mxl862xx_pcs_config(struct phylink_pcs *pcs, unsigned int neg_mode,
++ phy_interface_t interface,
++ const unsigned long *advertising,
++ bool permit_pause_to_mac)
++{
++ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
++ struct mxl862xx_priv *priv = mpcs->priv;
++ int port = mpcs->port;
++ struct mxl862xx_xpcs_pcs_cfg cfg = {};
++ int if_mode, ret;
++
++ /* Sub-interfaces are set up implicitly by the main interface */
++ if (port != 9 && port != 13)
++ return 0;
++
++ if_mode = mxl862xx_xpcs_if_mode(interface);
++ if (if_mode < 0) {
++ dev_err(priv->ds->dev, "unsupported interface: %s\n",
++ phy_modes(interface));
++ return if_mode;
++ }
++
++ mpcs->if_mode = if_mode;
++
++ cfg.port_id = mxl862xx_xpcs_port_id(port);
++ cfg.interface = if_mode;
++ cfg.neg_mode = mxl862xx_xpcs_neg_mode(neg_mode);
++ cfg.permit_pause = permit_pause_to_mac ? 1 : 0;
++
++ if (neg_mode & PHYLINK_PCS_NEG_INBAND) {
++ switch (interface) {
++ case PHY_INTERFACE_MODE_1000BASEX:
++ case PHY_INTERFACE_MODE_2500BASEX:
++ cfg.advertising = cpu_to_le16(
++ linkmode_adv_to_mii_adv_x(advertising,
++ ETHTOOL_LINK_MODE_1000baseX_Full_BIT));
++ break;
++ case PHY_INTERFACE_MODE_SGMII:
++ case PHY_INTERFACE_MODE_QSGMII:
++ cfg.advertising = cpu_to_le16(ADVERTISE_SGMII);
++ break;
++ default:
++ break;
++ }
++ }
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PCS_CONFIG, cfg);
++ if (ret)
++ return ret;
++
++ /* result > 0 means AN restart is needed */
++ return le16_to_cpu(cfg.result) > 0 ? 1 : 0;
++}
++
++static void mxl862xx_xpcs_decode_speed(u16 fw_speed,
++ struct phylink_link_state *state)
++{
++ switch (fw_speed) {
++ case MXL862XX_XPCS_SPEED_10:
++ state->speed = SPEED_10;
++ break;
++ case MXL862XX_XPCS_SPEED_100:
++ state->speed = SPEED_100;
++ break;
++ case MXL862XX_XPCS_SPEED_1000:
++ state->speed = SPEED_1000;
++ break;
++ case MXL862XX_XPCS_SPEED_2500:
++ state->speed = SPEED_2500;
++ break;
++ case MXL862XX_XPCS_SPEED_5000:
++ state->speed = SPEED_5000;
++ break;
++ case MXL862XX_XPCS_SPEED_10000:
++ state->speed = SPEED_10000;
++ break;
++ default:
++ state->speed = SPEED_UNKNOWN;
++ break;
++ }
++
++ state->duplex = DUPLEX_FULL;
++}
++
++static void mxl862xx_pcs_get_state(struct phylink_pcs *pcs,
++ struct phylink_link_state *state)
++{
++ struct mxl862xx_priv *priv = pcs_to_mxl862xx_pcs(pcs)->priv;
++ int port = pcs_to_mxl862xx_pcs(pcs)->port;
++ struct mxl862xx_xpcs_pcs_state st = {};
++ int if_mode, ret;
++ u16 fw_speed, lpa, bmsr;
++
++ if_mode = mxl862xx_xpcs_if_mode(state->interface);
++ if (if_mode < 0)
++ return;
++
++ st.port_id = mxl862xx_xpcs_port_id(port);
++ st.interface = if_mode;
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_PCS_GET_STATE, st);
++ if (ret)
++ return;
++
++ fw_speed = le16_to_cpu(st.speed);
++ lpa = le16_to_cpu(st.lpa);
++
++ state->link = st.link && !st.pcs_fault;
++ if (!state->link)
++ return;
++
++ switch (state->interface) {
++ case PHY_INTERFACE_MODE_1000BASEX:
++ case PHY_INTERFACE_MODE_2500BASEX:
++ case PHY_INTERFACE_MODE_SGMII:
++ case PHY_INTERFACE_MODE_QSGMII:
++ /* Synthesize BMSR from firmware state and use phylink's
++ * standard CL37/SGMII decoders for LPA, pause, and speed.
++ */
++ bmsr = BMSR_LSTATUS;
++ if (st.an_complete)
++ bmsr |= BMSR_ANEGCOMPLETE;
++ phylink_mii_c22_pcs_decode_state(state, bmsr, lpa);
++
++ /* Override speed/duplex with firmware's resolved values
++ * for downshift detection.
++ */
++ mxl862xx_xpcs_decode_speed(fw_speed, state);
++ state->duplex = st.duplex ? DUPLEX_FULL : DUPLEX_HALF;
++ break;
++
++ case PHY_INTERFACE_MODE_USXGMII:
++ state->an_complete = st.an_complete;
++ phylink_decode_usxgmii_word(state, lpa);
++
++ /* Override with firmware's resolved values */
++ mxl862xx_xpcs_decode_speed(fw_speed, state);
++ state->duplex = st.duplex ? DUPLEX_FULL : DUPLEX_HALF;
++ break;
++
++ case PHY_INTERFACE_MODE_10GBASER:
++ case PHY_INTERFACE_MODE_10GKR:
++ mxl862xx_xpcs_decode_speed(fw_speed, state);
++ break;
++
++ default:
++ state->link = false;
++ break;
++ }
++}
++
++static void mxl862xx_pcs_an_restart(struct phylink_pcs *pcs)
++{
++ struct mxl862xx_pcs *mpcs = pcs_to_mxl862xx_pcs(pcs);
++ struct mxl862xx_priv *priv = mpcs->priv;
++ int port = mpcs->port;
++ struct mxl862xx_xpcs_an_restart an = {};
++
++ if (port != 9 && port != 13)
++ return;
++
++ an.port_id = mxl862xx_xpcs_port_id(port);
++ an.interface = mpcs->if_mode;
++
++ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_AN_RESTART, an);
++}
++
++static void mxl862xx_pcs_link_up(struct phylink_pcs *pcs, unsigned int neg_mode,
++ phy_interface_t interface, int speed,
++ int duplex)
++{
++ struct mxl862xx_priv *priv = pcs_to_mxl862xx_pcs(pcs)->priv;
++ int port = pcs_to_mxl862xx_pcs(pcs)->port;
++ struct mxl862xx_xpcs_force_speed fs = {};
++
++ /* Only SGMII needs explicit speed forcing */
++ if (interface != PHY_INTERFACE_MODE_SGMII)
++ return;
++
++ if (port != 9 && port != 13)
++ return;
++
++ fs.port_id = mxl862xx_xpcs_port_id(port);
++ fs.duplex = (duplex == DUPLEX_FULL) ? MXL862XX_XPCS_DUPLEX_FULL :
++ MXL862XX_XPCS_DUPLEX_HALF;
++ fs.speed = cpu_to_le16(speed);
++
++ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_FORCE_SPEED, fs);
++}
++
++static unsigned int mxl862xx_pcs_inband_caps(struct phylink_pcs *pcs,
++ phy_interface_t interface)
++{
++ switch (interface) {
++ case PHY_INTERFACE_MODE_SGMII:
++ case PHY_INTERFACE_MODE_QSGMII:
++ case PHY_INTERFACE_MODE_USXGMII:
++ case PHY_INTERFACE_MODE_1000BASEX:
++ case PHY_INTERFACE_MODE_2500BASEX:
++ case PHY_INTERFACE_MODE_10GBASER:
++ return LINK_INBAND_DISABLE | LINK_INBAND_ENABLE |
++ LINK_INBAND_BYPASS;
++ case PHY_INTERFACE_MODE_10GKR:
++ return LINK_INBAND_ENABLE | LINK_INBAND_BYPASS;
++ default:
++ return 0;
++ }
++}
++
++static const struct phylink_pcs_ops mxl862xx_pcs_ops = {
++ .pcs_disable = mxl862xx_pcs_disable,
++ .pcs_pre_config = mxl862xx_pcs_pre_config,
++ .pcs_config = mxl862xx_pcs_config,
++ .pcs_get_state = mxl862xx_pcs_get_state,
++ .pcs_an_restart = mxl862xx_pcs_an_restart,
++ .pcs_link_up = mxl862xx_pcs_link_up,
++ .pcs_inband_caps = mxl862xx_pcs_inband_caps,
++};
++
++void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs,
++ int port)
++{
++ pcs->priv = priv;
++ pcs->port = port;
++
++ pcs->pcs.ops = &mxl862xx_pcs_ops;
++ pcs->pcs.poll = true;
++}
++
++static struct phylink_pcs *
++mxl862xx_phylink_mac_select_pcs(struct phylink_config *config,
++ phy_interface_t interface)
++{
++ struct dsa_port *dp = dsa_phylink_to_port(config);
++ struct mxl862xx_priv *priv = dp->ds->priv;
++ int port = dp->index;
++
++ if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
++ return NULL;
++
++ switch (port) {
++ case 9 ... 16:
++ return &priv->serdes_ports[port - 9].pcs;
++ default:
++ return NULL;
++ }
+ }
+
+ static void mxl862xx_phylink_mac_config(struct phylink_config *config,
+@@ -48,4 +437,5 @@ const struct phylink_mac_ops mxl862xx_ph
+ .mac_config = mxl862xx_phylink_mac_config,
+ .mac_link_down = mxl862xx_phylink_mac_link_down,
+ .mac_link_up = mxl862xx_phylink_mac_link_up,
++ .mac_select_pcs = mxl862xx_phylink_mac_select_pcs,
+ };
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
+@@ -10,5 +10,7 @@
+ extern const struct phylink_mac_ops mxl862xx_phylink_mac_ops;
+ void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
+ struct phylink_config *config);
++void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs,
++ int port);
+
+ #endif /* __MXL862XX_PHYLINK_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -729,7 +729,7 @@ static int mxl862xx_setup(struct dsa_swi
+ int n_user_ports = 0, max_vlans;
+ int ingress_finals, vid_rules;
+ struct dsa_port *dp;
+- int ret;
++ int ret, i;
+
+ ret = mxl862xx_reset(priv);
+ if (ret)
+@@ -739,6 +739,9 @@ static int mxl862xx_setup(struct dsa_swi
+ if (ret)
+ return ret;
+
++ for (i = 0; i < 8; i++)
++ mxl862xx_setup_pcs(priv, &priv->serdes_ports[i], i + 9);
++
+ /* Calculate Extended VLAN block sizes.
+ * With VLAN Filter handling VID membership checks:
+ * Ingress: only final catchall rules (PVID insertion, 802.1Q
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -247,6 +247,22 @@ struct mxl862xx_port {
+ };
+
+ /**
++ * struct mxl862xx_pcs - link SerDes interfaces to bridge ports
++ * @pcs: &struct phylink_pcs instance
++ * @priv: pointer to &struct mxl862xx_priv
++ * @port: bridge port index
++ * @if_mode: cached firmware interface mode (enum mxl862xx_xpcs_if_mode)
++ * @enabled: true if the PCS/SerDes is currently powered up
++ */
++struct mxl862xx_pcs {
++ struct phylink_pcs pcs;
++ struct mxl862xx_priv *priv;
++ int port;
++ int if_mode;
++ bool enabled;
++};
++
++/**
+ * union mxl862xx_fw_version - firmware version for comparison and display
+ * @major: firmware major version
+ * @minor: firmware minor version
+@@ -291,6 +307,8 @@ union mxl862xx_fw_version {
+ * flooding)
+ * @fw_version: cached firmware version, populated at probe and
+ * compared with MXL862XX_FW_VER_MIN()
++ * @serdes_ports: SerDes interfaces incl. sub-interfaces in case of
++ * 10G_QXGMII
+ * @ports: per-port state, indexed by switch port number
+ * @bridges: maps DSA bridge number to firmware bridge ID;
+ * zero means no firmware bridge allocated for that
+@@ -309,6 +327,7 @@ struct mxl862xx_priv {
+ unsigned long crc_err;
+ u16 drop_meter;
+ union mxl862xx_fw_version fw_version;
++ struct mxl862xx_pcs serdes_ports[8];
+ struct mxl862xx_port ports[MXL862XX_MAX_PORTS];
+ u16 bridges[MXL862XX_MAX_BRIDGES + 1];
+ u16 evlan_ingress_size;
--- /dev/null
+From d40565e2e00fc2c8f04b9c571fcbea2f146db844 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 18:14:33 +0000
+Subject: [PATCH 21/35] net: dsa: mxl862xx: add SerDes ethtool statistics
+
+Expose SerDes equalization and signal detect parameters as ethtool
+statistics on ports 9-16 (XPCS-backed ports). Uses the XPCS EQ_GET
+and SIGNAL_DETECT firmware commands to read TX/RX equalization
+coefficients, DFE taps, and link-level signal status.
+
+The 19 additional stats (serdes_tx_*, serdes_rx_*, serdes_pma_link,
+serdes_link_fault, serdes_in_reset) are appended after the standard
+RMON counters and gated on firmware >= 1.0.80.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 88 +++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 2 +
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 93 +++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.h | 3 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 6 +-
+ 5 files changed, 191 insertions(+), 1 deletion(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -1812,4 +1812,92 @@ struct mxl862xx_xpcs_reset_cfg {
+ __le16 result;
+ } __packed;
+
++/**
++ * struct mxl862xx_xpcs_eq_item - single equalization parameter
++ * @value: current initial value
++ * @ovrd: override value
++ * @ovrd_en: override enable flag
++ */
++struct mxl862xx_xpcs_eq_item {
++ u8 value;
++ u8 ovrd;
++ u8 ovrd_en;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_tx_eq_info - TX equalization status
++ * @main: TX main cursor (0-63)
++ * @pre: TX pre-cursor (0-63)
++ * @post: TX post-cursor (0-63)
++ * @iboost_lvl: TX iboost level (0-15)
++ * @vboost_lvl: TX vboost level (0-7)
++ * @vboost_en: TX vboost enable (0-1)
++ */
++struct mxl862xx_xpcs_tx_eq_info {
++ struct mxl862xx_xpcs_eq_item main;
++ struct mxl862xx_xpcs_eq_item pre;
++ struct mxl862xx_xpcs_eq_item post;
++ struct mxl862xx_xpcs_eq_item iboost_lvl;
++ struct mxl862xx_xpcs_eq_item vboost_lvl;
++ struct mxl862xx_xpcs_eq_item vboost_en;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_rx_eq_info - RX equalization status
++ * @att_lvl: RX attenuation level (0-7)
++ * @vga1_gain: RX VGA1 gain (0-7)
++ * @vga2_gain: RX VGA2 gain (0-7)
++ * @ctle_boost: RX CTLE boost (0-31)
++ * @ctle_pole: RX CTLE pole (0-3)
++ * @dfe_tap1: RX DFE tap1 (0-255)
++ * @dfe_bypass: RX DFE bypass (0-1)
++ * @adapt_mode: RX adapt mode (0-3)
++ * @adapt_sel: RX adapt select (0-1)
++ */
++struct mxl862xx_xpcs_rx_eq_info {
++ struct mxl862xx_xpcs_eq_item att_lvl;
++ struct mxl862xx_xpcs_eq_item vga1_gain;
++ struct mxl862xx_xpcs_eq_item vga2_gain;
++ struct mxl862xx_xpcs_eq_item ctle_boost;
++ struct mxl862xx_xpcs_eq_item ctle_pole;
++ struct mxl862xx_xpcs_eq_item dfe_tap1;
++ struct mxl862xx_xpcs_eq_item dfe_bypass;
++ struct mxl862xx_xpcs_eq_item adapt_mode;
++ struct mxl862xx_xpcs_eq_item adapt_sel;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_eq_get - EQ get request/response
++ * @port_id: XPCS port index (0 or 1)
++ * @result: firmware result
++ * @tx: TX equalization info
++ * @rx: RX equalization info
++ */
++struct mxl862xx_xpcs_eq_get {
++ u8 port_id;
++ __le16 result;
++ struct mxl862xx_xpcs_tx_eq_info tx;
++ struct mxl862xx_xpcs_rx_eq_info rx;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_signal_detect - signal detect status
++ * @port_id: XPCS port index (0 or 1)
++ * @rx_signal: RX signal detected
++ * @pma_link: PMA link up
++ * @link_fault: PCS link fault
++ * @in_reset: XPCS in reset
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_signal_detect {
++ u8 port_id:2;
++ u8 rx_signal:1;
++ u8 pma_link:1;
++ u8 link_fault:1;
++ u8 in_reset:1;
++ u8 __rsv:2;
++ u8 __pad;
++ __le16 result;
++} __packed;
++
+ #endif /* __MXL862XX_API_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -83,6 +83,8 @@
+ #define MXL862XX_XPCS_FORCE_SPEED (MXL862XX_XPCS_MAGIC + 0x7)
+ #define MXL862XX_XPCS_LOOPBACK (MXL862XX_XPCS_MAGIC + 0x8)
+ #define MXL862XX_XPCS_RESET (MXL862XX_XPCS_MAGIC + 0x9)
++#define MXL862XX_XPCS_EQ_GET (MXL862XX_XPCS_MAGIC + 0xc)
++#define MXL862XX_XPCS_SIGNAL_DETECT (MXL862XX_XPCS_MAGIC + 0xd)
+
+ #define MMD_API_MAXIMUM_ID 0x7fff
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+@@ -439,3 +439,96 @@ const struct phylink_mac_ops mxl862xx_ph
+ .mac_link_up = mxl862xx_phylink_mac_link_up,
+ .mac_select_pcs = mxl862xx_phylink_mac_select_pcs,
+ };
++
++/* --- SerDes ethtool statistics --- */
++
++static const char mxl862xx_serdes_stats[][ETH_GSTRING_LEN] = {
++ "serdes_tx_main",
++ "serdes_tx_pre",
++ "serdes_tx_post",
++ "serdes_tx_iboost",
++ "serdes_tx_vboost",
++ "serdes_tx_vboost_en",
++ "serdes_rx_att",
++ "serdes_rx_vga1",
++ "serdes_rx_vga2",
++ "serdes_rx_ctle_boost",
++ "serdes_rx_ctle_pole",
++ "serdes_rx_dfe_tap1",
++ "serdes_rx_dfe_bypass",
++ "serdes_rx_adapt_mode",
++ "serdes_rx_adapt_sel",
++ "serdes_rx_signal",
++ "serdes_pma_link",
++ "serdes_link_fault",
++ "serdes_in_reset",
++};
++
++static bool mxl862xx_port_has_serdes_stats(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++
++ return port >= 9 && port <= 16 &&
++ MXL862XX_FW_VER_MIN(priv, 1, 0, 80);
++}
++
++int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port)
++{
++ if (mxl862xx_port_has_serdes_stats(ds, port))
++ return ARRAY_SIZE(mxl862xx_serdes_stats);
++
++ return 0;
++}
++
++void mxl862xx_serdes_get_strings(struct dsa_switch *ds, int port, u8 *data)
++{
++ int i;
++
++ if (!mxl862xx_port_has_serdes_stats(ds, port))
++ return;
++
++ for (i = 0; i < ARRAY_SIZE(mxl862xx_serdes_stats); i++)
++ ethtool_puts(&data, mxl862xx_serdes_stats[i]);
++}
++
++void mxl862xx_serdes_get_stats(struct dsa_switch *ds, int port, u64 *data)
++{
++ struct mxl862xx_xpcs_eq_get eq = {
++ .port_id = mxl862xx_xpcs_port_id(port),
++ };
++ struct mxl862xx_xpcs_signal_detect sig = {};
++
++ if (!mxl862xx_port_has_serdes_stats(ds, port))
++ return;
++
++ sig.port_id = mxl862xx_xpcs_port_id(port);
++
++ if (!MXL862XX_API_READ(ds->priv, MXL862XX_XPCS_EQ_GET, eq)) {
++ *data++ = eq.tx.main.value;
++ *data++ = eq.tx.pre.value;
++ *data++ = eq.tx.post.value;
++ *data++ = eq.tx.iboost_lvl.value;
++ *data++ = eq.tx.vboost_lvl.value;
++ *data++ = eq.tx.vboost_en.value;
++ *data++ = eq.rx.att_lvl.value;
++ *data++ = eq.rx.vga1_gain.value;
++ *data++ = eq.rx.vga2_gain.value;
++ *data++ = eq.rx.ctle_boost.value;
++ *data++ = eq.rx.ctle_pole.value;
++ *data++ = eq.rx.dfe_tap1.value;
++ *data++ = eq.rx.dfe_bypass.value;
++ *data++ = eq.rx.adapt_mode.value;
++ *data++ = eq.rx.adapt_sel.value;
++ } else {
++ data += 15;
++ }
++
++ if (!MXL862XX_API_READ(ds->priv, MXL862XX_XPCS_SIGNAL_DETECT, sig)) {
++ *data++ = sig.rx_signal;
++ *data++ = sig.pma_link;
++ *data++ = sig.link_fault;
++ *data++ = sig.in_reset;
++ } else {
++ data += 4;
++ }
++}
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
+@@ -12,5 +12,8 @@ void mxl862xx_phylink_get_caps(struct ds
+ struct phylink_config *config);
+ void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs,
+ int port);
++int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port);
++void mxl862xx_serdes_get_strings(struct dsa_switch *ds, int port, u8 *data);
++void mxl862xx_serdes_get_stats(struct dsa_switch *ds, int port, u64 *data);
+
+ #endif /* __MXL862XX_PHYLINK_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -2007,6 +2007,8 @@ static void mxl862xx_get_strings(struct
+
+ for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++)
+ ethtool_puts(&data, mxl862xx_mib[i].name);
++
++ mxl862xx_serdes_get_strings(ds, port, data);
+ }
+
+ static int mxl862xx_get_sset_count(struct dsa_switch *ds, int port, int sset)
+@@ -2014,7 +2016,7 @@ static int mxl862xx_get_sset_count(struc
+ if (sset != ETH_SS_STATS)
+ return 0;
+
+- return ARRAY_SIZE(mxl862xx_mib);
++ return ARRAY_SIZE(mxl862xx_mib) + mxl862xx_serdes_stats_count(ds, port);
+ }
+
+ static int mxl862xx_read_rmon(struct dsa_switch *ds, int port,
+@@ -2050,6 +2052,8 @@ static void mxl862xx_get_ethtool_stats(s
+ else
+ *data++ = le64_to_cpu(*(__le64 *)field);
+ }
++
++ mxl862xx_serdes_get_stats(ds, port, data);
+ }
+
+ static void mxl862xx_get_eth_mac_stats(struct dsa_switch *ds, int port,
--- /dev/null
+From 54dd5fabc543f8538202367a863eb0e9161bacab Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 18:15:32 +0000
+Subject: [PATCH 22/35] net: dsa: mxl862xx: add SerDes self-test via PRBS and
+ BERT
+
+Implement the dsa_switch_ops.self_test callback for SerDes ports
+(9-16). Two loopback tests are run:
+
+ 1. PCS-level PRBS31: enables TX/RX PRBS31 pattern at the PCS layer,
+ waits 100ms, then reads the error counter.
+ 2. SerDes-level BERT PRBS31: enables TX/RX BERT with PRBS31 pattern
+ at the SerDes layer, waits 100ms, then reads the error counter.
+
+Both tests clean up (disable pattern generators) regardless of outcome.
+Gated on firmware >= 1.0.80 via mxl862xx_port_has_serdes_stats().
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 45 +++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 2 +
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 85 +++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.h | 3 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 1 +
+ 5 files changed, 136 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -1900,4 +1900,49 @@ struct mxl862xx_xpcs_signal_detect {
+ __le16 result;
+ } __packed;
+
++/**
++ * struct mxl862xx_xpcs_prbs_cfg - PCS-level PRBS31 test pattern
++ * @port_id: XPCS port index (0 or 1)
++ * @tx_en: TX PRBS31 enable
++ * @rx_en: RX PRBS31 enable
++ * @read_err: read error count
++ * @rx_err_cnt: RX PRBS31 error count (valid when read_err=1)
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_prbs_cfg {
++ u8 port_id:2;
++ u8 tx_en:1;
++ u8 rx_en:1;
++ u8 read_err:1;
++ u8 __rsv:3;
++ u8 __pad;
++ __le16 rx_err_cnt;
++ __le16 result;
++} __packed;
++
++/**
++ * struct mxl862xx_xpcs_bert_cfg - SerDes-level BERT test pattern
++ * @port_id: XPCS port index (0 or 1)
++ * @tx_en: TX BERT enable
++ * @rx_en: RX BERT enable
++ * @read_err: read RX error count
++ * @clear_err: clear RX error counter
++ * @insert_err: insert one TX error
++ * @pattern: PRBS pattern type (1-7; 0 = disable)
++ * @rx_err_cnt: RX BERT error count (valid when read_err=1)
++ * @result: firmware result
++ */
++struct mxl862xx_xpcs_bert_cfg {
++ u8 port_id:2;
++ u8 tx_en:1;
++ u8 rx_en:1;
++ u8 read_err:1;
++ u8 clear_err:1;
++ u8 insert_err:1;
++ u8 __rsv:1;
++ u8 pattern;
++ __le16 rx_err_cnt;
++ __le16 result;
++} __packed;
++
+ #endif /* __MXL862XX_API_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -83,6 +83,8 @@
+ #define MXL862XX_XPCS_FORCE_SPEED (MXL862XX_XPCS_MAGIC + 0x7)
+ #define MXL862XX_XPCS_LOOPBACK (MXL862XX_XPCS_MAGIC + 0x8)
+ #define MXL862XX_XPCS_RESET (MXL862XX_XPCS_MAGIC + 0x9)
++#define MXL862XX_XPCS_PRBS_CFG (MXL862XX_XPCS_MAGIC + 0xa)
++#define MXL862XX_XPCS_BERT_CFG (MXL862XX_XPCS_MAGIC + 0xb)
+ #define MXL862XX_XPCS_EQ_GET (MXL862XX_XPCS_MAGIC + 0xc)
+ #define MXL862XX_XPCS_SIGNAL_DETECT (MXL862XX_XPCS_MAGIC + 0xd)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+@@ -532,3 +532,88 @@ void mxl862xx_serdes_get_stats(struct ds
+ data += 4;
+ }
+ }
++
++void mxl862xx_serdes_self_test(struct dsa_switch *ds, int port,
++ struct ethtool_test *etest, u64 *data)
++{
++ struct mxl862xx_xpcs_prbs_cfg prbs = {};
++ struct mxl862xx_xpcs_bert_cfg bert = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ int xpcs_id = mxl862xx_xpcs_port_id(port);
++ int i = 0;
++ int ret;
++
++ if (!mxl862xx_port_has_serdes_stats(ds, port))
++ return;
++
++ /* Test 1: PCS PRBS31 */
++ prbs.port_id = xpcs_id;
++ prbs.tx_en = 1;
++ prbs.rx_en = 1;
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PRBS_CFG, prbs);
++ if (ret) {
++ data[i++] = 1;
++ goto skip_prbs;
++ }
++
++ msleep(100);
++
++ memset(&prbs, 0, sizeof(prbs));
++ prbs.port_id = xpcs_id;
++ prbs.read_err = 1;
++ ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_PRBS_CFG, prbs);
++
++ /* Disable PRBS */
++ memset(&prbs, 0, sizeof(prbs));
++ prbs.port_id = xpcs_id;
++ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PRBS_CFG, prbs);
++
++ if (ret) {
++ data[i++] = 1;
++ } else {
++ data[i] = le16_to_cpu(prbs.rx_err_cnt) ? 1 : 0;
++ if (data[i])
++ etest->flags |= ETH_TEST_FL_FAILED;
++ i++;
++ }
++
++skip_prbs:
++ /* Test 2: SerDes BERT PRBS31 -- clear error counter first */
++ bert.port_id = xpcs_id;
++ bert.clear_err = 1;
++ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_BERT_CFG, bert);
++
++ /* Enable BERT with PRBS31 pattern */
++ memset(&bert, 0, sizeof(bert));
++ bert.port_id = xpcs_id;
++ bert.tx_en = 1;
++ bert.rx_en = 1;
++ bert.pattern = 6; /* PRBS31 */
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_XPCS_BERT_CFG, bert);
++ if (ret) {
++ data[i++] = 1;
++ return;
++ }
++
++ msleep(100);
++
++ /* Read error count */
++ memset(&bert, 0, sizeof(bert));
++ bert.port_id = xpcs_id;
++ bert.read_err = 1;
++ ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_BERT_CFG, bert);
++
++ /* Disable BERT */
++ memset(&bert, 0, sizeof(bert));
++ bert.port_id = xpcs_id;
++ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_BERT_CFG, bert);
++
++ if (ret) {
++ data[i++] = 1;
++ } else {
++ data[i] = le16_to_cpu(bert.rx_err_cnt) ? 1 : 0;
++ if (data[i])
++ etest->flags |= ETH_TEST_FL_FAILED;
++ i++;
++ }
++}
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
+@@ -3,6 +3,7 @@
+ #ifndef __MXL862XX_PHYLINK_H
+ #define __MXL862XX_PHYLINK_H
+
++#include <linux/ethtool.h>
+ #include <linux/phylink.h>
+
+ #include "mxl862xx.h"
+@@ -15,5 +16,7 @@ void mxl862xx_setup_pcs(struct mxl862xx_
+ int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port);
+ void mxl862xx_serdes_get_strings(struct dsa_switch *ds, int port, u8 *data);
+ void mxl862xx_serdes_get_stats(struct dsa_switch *ds, int port, u64 *data);
++void mxl862xx_serdes_self_test(struct dsa_switch *ds, int port,
++ struct ethtool_test *etest, u64 *data);
+
+ #endif /* __MXL862XX_PHYLINK_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -2286,6 +2286,7 @@ static const struct dsa_switch_ops mxl86
+ .get_eth_ctrl_stats = mxl862xx_get_eth_ctrl_stats,
+ .get_pause_stats = mxl862xx_get_pause_stats,
+ .get_stats64 = mxl862xx_get_stats64,
++ .self_test = mxl862xx_serdes_self_test,
+ };
+
+ static int mxl862xx_probe(struct mdio_device *mdiodev)
--- /dev/null
+From dd62e68cd0bd29934c3efbce687d5e103cc4b331 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 18:51:13 +0000
+Subject: [PATCH 23/35] net: dsa: mxl862xx: trap link-local frames to the CPU
+ port
+
+Install per-CTP PCE rules on each user port that trap IEEE 802.1D
+link-local frames (01:80:c2:00:00:0x) to the CPU port via an
+explicit forwarding portmap with cross-state enabled, ensuring the
+frames reach the host even when the bridge port is in BLOCKING or
+LEARNING state.
+
+Add the PCE rule firmware API structures, command definitions, and
+the rule block allocation interface.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 684 ++++++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 5 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 69 +++
+ 3 files changed, 758 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -1420,6 +1420,690 @@ struct mxl862xx_port_link_cfg {
+ u8 lpi;
+ } __packed;
+
++/* PCE (Packet Classification Engine) rule structures.
++ *
++ * Binary layout must exactly match the firmware's GSW_PCE_rule_t
++ * (gsw_flow.h, packed little-endian). The firmware deserializes
++ * the structure directly from the MDIO data buffer produced by
++ * mxl862xx_api_wrap().
++ */
++
++/**
++ * union mxl862xx_ip - IPv4 or IPv6 address
++ * @ipv4: IPv4 address in little-endian
++ * @ipv6: IPv6 address as array of little-endian 16-bit words
++ */
++union mxl862xx_ip {
++ __le32 ipv4;
++ __le16 ipv6[8];
++} __packed;
++
++/**
++ * struct mxl862xx_pce_pattern - PCE rule match pattern
++ *
++ * Every field must remain in the order shown; the firmware
++ * interprets the buffer positionally.
++ *
++ * @index: PCE rule index (0..511)
++ * @dst_ip_mask: Destination IP nibble mask (outer)
++ * @inner_dst_ip_mask: Inner destination IP nibble mask
++ * @src_ip_mask: Source IP nibble mask (outer)
++ * @inner_src_ip_mask: Inner source IP nibble mask
++ * @sub_if_id: Incoming sub-interface ID value
++ * @pkt_lng: Packet length in bytes
++ * @pkt_lng_range: Packet length range upper bound
++ * @mac_dst_mask: Destination MAC nibble mask
++ * @mac_src_mask: Source MAC nibble mask
++ * @app_data_msb: MSB application field (first 2 bytes after IP header,
++ * typically TCP/UDP source port)
++ * @app_mask_range_msb: MSB application mask or range value
++ * @ether_type: EtherType value to match
++ * @ether_type_mask: EtherType nibble mask
++ * @session_id: PPPoE session ID
++ * @ppp_protocol: PPP protocol value
++ * @ppp_protocol_mask: PPP protocol bit mask
++ * @vid: CTAG VLAN ID (inner VLAN)
++ * @slan_vid: STAG VLAN ID (outer VLAN)
++ * @flex_field4_value: Flexible field 4 match value
++ * @flex_field4_mask_range: Flexible field 4 mask or range
++ * @flex_field3_value: Flexible field 3 match value
++ * @flex_field3_mask_range: Flexible field 3 mask or range
++ * @flex_field1_value: Flexible field 1 match value
++ * @flex_field1_mask_range: Flexible field 1 mask or range
++ * @outer_vid_range: Outer VLAN ID range
++ * @payload1: Payload-1 value (16-bit)
++ * @payload1_mask: Payload-1 bit mask
++ * @payload2: Payload-2 value (16-bit)
++ * @payload2_mask: Payload-2 bit mask
++ * @parser_flag_lsb: Parser flag LSW value (bits 15:0)
++ * @parser_flag_lsb_mask: Parser flag LSW mask (1 = masked out)
++ * @parser_flag_msb: Parser flag MSW value (bits 31:16)
++ * @parser_flag_msb_mask: Parser flag MSW mask (1 = masked out)
++ * @parser_flag1_lsb: Parser flag1 LSW value (bits 47:32)
++ * @parser_flag1_lsb_mask: Parser flag1 LSW mask
++ * @parser_flag1_msb: Parser flag1 MSW value (bits 63:48)
++ * @parser_flag1_msb_mask: Parser flag1 MSW mask
++ * @app_data_lsb: LSB application field (next 2 bytes after @app_data_msb,
++ * typically TCP/UDP destination port)
++ * @app_mask_range_lsb: LSB application mask or range value
++ * @insertion_flag: CPU-inserted packet flag
++ * @vid_range: CTAG VLAN ID range (used as mask when @vid_range_select is 0)
++ * @flex_field2_mask_range: Flexible field 2 mask or range
++ * @flex_field2_value: Flexible field 2 match value
++ * @port_id: Ingress port ID for classification
++ * @dscp: Outer DSCP value
++ * @inner_dscp: Inner DSCP value
++ * @pcp: CTAG VLAN PCP (bits 2:0) and DEI (bit 3)
++ * @stag_pcp_dei: STAG VLAN PCP (bits 2:0) and DEI (bit 3)
++ * @mac_dst: Destination MAC address
++ * @mac_src: Source MAC address
++ * @protocol: Outer IP protocol value
++ * @protocol_mask: Outer IP protocol nibble mask
++ * @inner_protocol: Inner IP protocol value
++ * @inner_protocol_mask: Inner IP protocol bit mask
++ * @flex_field4_parser_index: Flexible field 4 parser output index (0..127)
++ * @flex_field3_parser_index: Flexible field 3 parser output index (0..127)
++ * @flex_field1_parser_index: Flexible field 1 parser output index (0..127)
++ * @flex_field2_parser_index: Flexible field 2 parser output index (0..127)
++ * @enable: Rule is enabled (used) or disabled (unused)
++ * @port_id_enable: Enable ingress port ID matching
++ * @port_id_exclude: Exclude (negate) port ID match
++ * @sub_if_id_type: Sub-interface ID field mode selector
++ * @sub_if_id_enable: Enable sub-interface ID matching
++ * @sub_if_id_exclude: Exclude sub-interface ID match
++ * @dscp_enable: Enable outer DSCP matching
++ * @dscp_exclude: Exclude outer DSCP match
++ * @inner_dscp_enable: Enable inner DSCP matching
++ * @inner_dscp_exclude: Exclude inner DSCP match
++ * @pcp_enable: Enable CTAG PCP/DEI matching
++ * @ctag_pcp_dei_exclude: Exclude CTAG PCP/DEI match
++ * @stag_pcp_dei_enable: Enable STAG PCP/DEI matching
++ * @stag_pcp_dei_exclude: Exclude STAG PCP/DEI match
++ * @pkt_lng_enable: Enable packet length matching
++ * @pkt_lng_exclude: Exclude packet length match
++ * @mac_dst_enable: Enable destination MAC matching
++ * @dst_mac_exclude: Exclude destination MAC match
++ * @mac_src_enable: Enable source MAC matching
++ * @src_mac_exclude: Exclude source MAC match
++ * @app_data_msb_enable: Enable MSB application field matching
++ * @app_mask_range_msb_select: MSB application mask/range selection
++ * (0 = nibble mask, 1 = range)
++ * @app_msb_exclude: Exclude MSB application match
++ * @app_data_lsb_enable: Enable LSB application field matching
++ * @app_mask_range_lsb_select: LSB application mask/range selection
++ * (0 = nibble mask, 1 = range)
++ * @app_lsb_exclude: Exclude LSB application match
++ * @dst_ip_select: Outer destination IP selection
++ * (0 = disabled, 1 = IPv4, 2 = IPv6)
++ * @dst_ip: Outer destination IP address
++ * @dst_ip_exclude: Exclude outer destination IP match
++ * @inner_dst_ip_select: Inner destination IP selection
++ * @inner_dst_ip: Inner destination IP address
++ * @inner_dst_ip_exclude: Exclude inner destination IP match
++ * @src_ip_select: Outer source IP selection
++ * (0 = disabled, 1 = IPv4, 2 = IPv6)
++ * @src_ip: Outer source IP address
++ * @src_ip_exclude: Exclude outer source IP match
++ * @inner_src_ip_select: Inner source IP selection
++ * @inner_src_ip: Inner source IP address
++ * @inner_src_ip_exclude: Exclude inner source IP match
++ * @ether_type_enable: Enable EtherType matching
++ * @ether_type_exclude: Exclude EtherType match
++ * @protocol_enable: Enable outer IP protocol matching
++ * @protocol_exclude: Exclude outer IP protocol match
++ * @inner_protocol_enable: Enable inner IP protocol matching
++ * @inner_protocol_exclude: Exclude inner IP protocol match
++ * @session_id_enable: Enable PPPoE session ID matching
++ * @session_id_exclude: Exclude PPPoE session ID match
++ * @ppp_protocol_enable: Enable PPP protocol matching
++ * @ppp_protocol_exclude: Exclude PPP protocol match
++ * @vid_used: Enable CTAG VLAN ID matching
++ * @vid_range_select: CVLAN mask/range selection (0 = mask, 1 = range)
++ * @vid_exclude: Exclude CTAG VLAN ID match
++ * @vid_original: Use original VLAN ID as key even if modified earlier
++ * @slan_vid_used: Enable STAG VLAN ID matching
++ * @slan_vid_exclude: Exclude STAG VLAN ID match
++ * @svid_range_select: SVLAN mask/range selection (0 = mask, 1 = range)
++ * @outer_vid_original: Use original outer VLAN ID as key even if modified
++ * @payload1_src_enable: Enable payload-1 matching
++ * @payload1_mask_range_select: Payload-1 mask/range selection
++ * (0 = bit mask, 1 = range)
++ * @payload1_exclude: Exclude payload-1 match
++ * @payload2_src_enable: Enable payload-2 matching
++ * @payload2_mask_range_select: Payload-2 mask/range selection
++ * (0 = bit mask, 1 = range)
++ * @payload2_exclude: Exclude payload-2 match
++ * @parser_flag_lsb_enable: Enable parser flag LSW matching
++ * @parser_flag_lsb_exclude: Exclude parser flag LSW match
++ * @parser_flag_msb_enable: Enable parser flag MSW matching
++ * @parser_flag_msb_exclude: Exclude parser flag MSW match
++ * @parser_flag1_lsb_enable: Enable parser flag1 LSW matching
++ * @parser_flag1_lsb_exclude: Exclude parser flag1 LSW match
++ * @parser_flag1_msb_enable: Enable parser flag1 MSW matching
++ * @parser_flag1_msb_exclude: Exclude parser flag1 MSW match
++ * @insertion_flag_enable: Enable insertion flag matching
++ * @flex_field4_enable: Enable flexible field 4 matching
++ * @flex_field4_exclude_enable: Exclude flexible field 4 match
++ * @flex_field4_range_enable: Flexible field 4 range mode
++ * (0 = mask, 1 = range)
++ * @flex_field3_enable: Enable flexible field 3 matching
++ * @flex_field3_exclude_enable: Exclude flexible field 3 match
++ * @flex_field3_range_enable: Flexible field 3 range mode
++ * @flex_field2_enable: Enable flexible field 2 matching
++ * @flex_field2_exclude_enable: Exclude flexible field 2 match
++ * @flex_field2_range_enable: Flexible field 2 range mode
++ * @flex_field1_enable: Enable flexible field 1 matching
++ * @flex_field1_exclude_enable: Exclude flexible field 1 match
++ * @flex_field1_range_enable: Flexible field 1 range mode
++ */
++struct mxl862xx_pce_pattern {
++ __le16 index;
++ __le32 dst_ip_mask;
++ __le32 inner_dst_ip_mask;
++ __le32 src_ip_mask;
++ __le32 inner_src_ip_mask;
++ __le16 sub_if_id;
++ __le16 pkt_lng;
++ __le16 pkt_lng_range;
++ __le16 mac_dst_mask;
++ __le16 mac_src_mask;
++ __le16 app_data_msb;
++ __le16 app_mask_range_msb;
++ __le16 ether_type;
++ __le16 ether_type_mask;
++ __le16 session_id;
++ __le16 ppp_protocol;
++ __le16 ppp_protocol_mask;
++ __le16 vid;
++ __le16 slan_vid;
++ __le16 flex_field4_value;
++ __le16 flex_field4_mask_range;
++ __le16 flex_field3_value;
++ __le16 flex_field3_mask_range;
++ __le16 flex_field1_value;
++ __le16 flex_field1_mask_range;
++ __le16 outer_vid_range;
++ __le16 payload1;
++ __le16 payload1_mask;
++ __le16 payload2;
++ __le16 payload2_mask;
++ __le16 parser_flag_lsb;
++ __le16 parser_flag_lsb_mask;
++ __le16 parser_flag_msb;
++ __le16 parser_flag_msb_mask;
++ __le16 parser_flag1_lsb;
++ __le16 parser_flag1_lsb_mask;
++ __le16 parser_flag1_msb;
++ __le16 parser_flag1_msb_mask;
++ __le16 app_data_lsb;
++ __le16 app_mask_range_lsb;
++ __le16 insertion_flag;
++ __le16 vid_range;
++ __le16 flex_field2_mask_range;
++ __le16 flex_field2_value;
++ u8 port_id;
++ u8 dscp;
++ u8 inner_dscp;
++ u8 pcp;
++ u8 stag_pcp_dei;
++ u8 mac_dst[ETH_ALEN];
++ u8 mac_src[ETH_ALEN];
++ u8 protocol;
++ u8 protocol_mask;
++ u8 inner_protocol;
++ u8 inner_protocol_mask;
++ u8 flex_field4_parser_index;
++ u8 flex_field3_parser_index;
++ u8 flex_field1_parser_index;
++ u8 flex_field2_parser_index;
++ u8 enable;
++ u8 port_id_enable;
++ u8 port_id_exclude;
++ __le32 sub_if_id_type;
++ u8 sub_if_id_enable;
++ u8 sub_if_id_exclude;
++ u8 dscp_enable;
++ u8 dscp_exclude;
++ u8 inner_dscp_enable;
++ u8 inner_dscp_exclude;
++ u8 pcp_enable;
++ u8 ctag_pcp_dei_exclude;
++ u8 stag_pcp_dei_enable;
++ u8 stag_pcp_dei_exclude;
++ u8 pkt_lng_enable;
++ u8 pkt_lng_exclude;
++ u8 mac_dst_enable;
++ u8 dst_mac_exclude;
++ u8 mac_src_enable;
++ u8 src_mac_exclude;
++ u8 app_data_msb_enable;
++ u8 app_mask_range_msb_select;
++ u8 app_msb_exclude;
++ u8 app_data_lsb_enable;
++ u8 app_mask_range_lsb_select;
++ u8 app_lsb_exclude;
++ __le32 dst_ip_select;
++ union mxl862xx_ip dst_ip;
++ u8 dst_ip_exclude;
++ __le32 inner_dst_ip_select;
++ union mxl862xx_ip inner_dst_ip;
++ u8 inner_dst_ip_exclude;
++ __le32 src_ip_select;
++ union mxl862xx_ip src_ip;
++ u8 src_ip_exclude;
++ __le32 inner_src_ip_select;
++ union mxl862xx_ip inner_src_ip;
++ u8 inner_src_ip_exclude;
++ u8 ether_type_enable;
++ u8 ether_type_exclude;
++ u8 protocol_enable;
++ u8 protocol_exclude;
++ u8 inner_protocol_enable;
++ u8 inner_protocol_exclude;
++ u8 session_id_enable;
++ u8 session_id_exclude;
++ u8 ppp_protocol_enable;
++ u8 ppp_protocol_exclude;
++ u8 vid_used;
++ u8 vid_range_select;
++ u8 vid_exclude;
++ u8 vid_original;
++ u8 slan_vid_used;
++ u8 slan_vid_exclude;
++ u8 svid_range_select;
++ u8 outer_vid_original;
++ u8 payload1_src_enable;
++ u8 payload1_mask_range_select;
++ u8 payload1_exclude;
++ u8 payload2_src_enable;
++ u8 payload2_mask_range_select;
++ u8 payload2_exclude;
++ u8 parser_flag_lsb_enable;
++ u8 parser_flag_lsb_exclude;
++ u8 parser_flag_msb_enable;
++ u8 parser_flag_msb_exclude;
++ u8 parser_flag1_lsb_enable;
++ u8 parser_flag1_lsb_exclude;
++ u8 parser_flag1_msb_enable;
++ u8 parser_flag1_msb_exclude;
++ u8 insertion_flag_enable;
++ u8 flex_field4_enable;
++ u8 flex_field4_exclude_enable;
++ u8 flex_field4_range_enable;
++ u8 flex_field3_enable;
++ u8 flex_field3_exclude_enable;
++ u8 flex_field3_range_enable;
++ u8 flex_field2_enable;
++ u8 flex_field2_exclude_enable;
++ u8 flex_field2_range_enable;
++ u8 flex_field1_enable;
++ u8 flex_field1_exclude_enable;
++ u8 flex_field1_range_enable;
++} __packed;
++
++static_assert(sizeof(struct mxl862xx_pce_pattern) == 279);
++
++/**
++ * struct mxl862xx_pce_action_pbb - Provider Backbone Bridging (Mac-in-Mac)
++ * action configuration
++ *
++ * @tunnel_id_known_traffic: Tunnel template index for I-Header known traffic
++ * @tunnel_id_unknown_traffic: Tunnel template index for I-Header unknown
++ * traffic
++ * @process_id_known_traffic: Tunnel template index for B-TAG known traffic
++ * @process_id_unknown_traffic: Tunnel template index for B-TAG unknown
++ * traffic
++ * @iheader_op_mode: I-Header operation mode (0 = no change, 1 = insert,
++ * 2 = remove, 3 = replace)
++ * @btag_op_mode: B-TAG operation mode (0 = no change, 1 = insert,
++ * 2 = remove, 3 = replace)
++ * @mac_table_macinmac_select: MAC table Mac-in-Mac selection
++ * (0 = outer MAC, 1 = inner MAC)
++ * @iheader_action_enable: Enable Mac-in-Mac I-Header action
++ * @tunnel_id_known_traffic_enable: Enable tunnel ID for known traffic
++ * @tunnel_id_unknown_traffic_enable: Enable tunnel ID for unknown traffic
++ * @b_dst_mac_from_mac_table_enable: Use B-DA from MAC table instead of
++ * tunnel template (I-Header insertion
++ * mode only)
++ * @replace_b_src_mac_enable: Replace B-SA from tunnel template
++ * @replace_b_dst_mac_enable: Replace B-DA from tunnel template
++ * @replace_i_tag_res_enable: Replace I-Tag Res from tunnel template
++ * @replace_i_tag_uac_enable: Replace I-Tag UAC from tunnel template
++ * @replace_i_tag_dei_enable: Replace I-Tag DEI from tunnel template
++ * @replace_i_tag_pcp_enable: Replace I-Tag PCP from tunnel template
++ * @replace_i_tag_sid_enable: Replace I-Tag SID from tunnel template
++ * @replace_i_tag_tpid_enable: Replace I-Tag TPID from tunnel template
++ * @btag_action_enable: Enable B-TAG action
++ * @process_id_known_traffic_enable: Enable process ID for B-TAG known
++ * traffic
++ * @process_id_unknown_traffic_enable: Enable process ID for B-TAG unknown
++ * traffic
++ * @replace_b_tag_dei_enable: Replace B-Tag DEI from tunnel template
++ * @replace_b_tag_pcp_enable: Replace B-Tag PCP from tunnel template
++ * @replace_b_tag_vid_enable: Replace B-Tag VID from tunnel template
++ * @replace_b_tag_tpid_enable: Replace B-Tag TPID from tunnel template
++ * @mac_table_macinmac_action_enable: Enable MAC table Mac-in-Mac action
++ */
++struct mxl862xx_pce_action_pbb {
++ u8 tunnel_id_known_traffic;
++ u8 tunnel_id_unknown_traffic;
++ u8 process_id_known_traffic;
++ u8 process_id_unknown_traffic;
++ __le32 iheader_op_mode;
++ __le32 btag_op_mode;
++ __le32 mac_table_macinmac_select;
++ u8 iheader_action_enable;
++ u8 tunnel_id_known_traffic_enable;
++ u8 tunnel_id_unknown_traffic_enable;
++ u8 b_dst_mac_from_mac_table_enable;
++ u8 replace_b_src_mac_enable;
++ u8 replace_b_dst_mac_enable;
++ u8 replace_i_tag_res_enable;
++ u8 replace_i_tag_uac_enable;
++ u8 replace_i_tag_dei_enable;
++ u8 replace_i_tag_pcp_enable;
++ u8 replace_i_tag_sid_enable;
++ u8 replace_i_tag_tpid_enable;
++ u8 btag_action_enable;
++ u8 process_id_known_traffic_enable;
++ u8 process_id_unknown_traffic_enable;
++ u8 replace_b_tag_dei_enable;
++ u8 replace_b_tag_pcp_enable;
++ u8 replace_b_tag_vid_enable;
++ u8 replace_b_tag_tpid_enable;
++ u8 mac_table_macinmac_action_enable;
++} __packed;
++
++static_assert(sizeof(struct mxl862xx_pce_action_pbb) == 36);
++
++/**
++ * struct mxl862xx_pce_action_dest_subif - Destination sub-interface ID
++ * action configuration
++ *
++ * @dest_subifid_action_enable: Destination sub-interface ID group field
++ * action enable
++ * @dest_subifid_assignment_enable: Destination sub-interface ID group field
++ * assignment enable
++ * @dest_subifgrp_field: Destination sub-interface ID group field value,
++ * or LAG index when trunking action is enabled
++ */
++struct mxl862xx_pce_action_dest_subif {
++ u8 dest_subifid_action_enable;
++ u8 dest_subifid_assignment_enable;
++ __le16 dest_subifgrp_field;
++} __packed;
++
++static_assert(sizeof(struct mxl862xx_pce_action_dest_subif) == 4);
++
++/**
++ * enum mxl862xx_pce_action_portmap - Forwarding group action selector
++ *
++ * Selects how the packet is forwarded. Mutually exclusive with
++ * the flow_id_action (bFlowID_Action in vendor API).
++ *
++ * @MXL862XX_PCE_ACTION_PORTMAP_DISABLE: Forwarding action is disabled
++ * @MXL862XX_PCE_ACTION_PORTMAP_REGULAR: Use default port-map from
++ * forwarding classification
++ * @MXL862XX_PCE_ACTION_PORTMAP_DISCARD: Discard the packet
++ * @MXL862XX_PCE_ACTION_PORTMAP_CPU: Forward to the CPU port
++ * (as configured by GSW_CPU_PortCfgSet, typically the on-die
++ * microcontroller -- not the DSA CPU port)
++ * @MXL862XX_PCE_ACTION_PORTMAP_ALTERNATIVE: Forward to an explicit
++ * portmap given by forward_port_map[]
++ */
++enum mxl862xx_pce_action_portmap {
++ MXL862XX_PCE_ACTION_PORTMAP_DISABLE = 0,
++ MXL862XX_PCE_ACTION_PORTMAP_REGULAR = 1,
++ MXL862XX_PCE_ACTION_PORTMAP_DISCARD = 2,
++ MXL862XX_PCE_ACTION_PORTMAP_CPU = 3,
++ MXL862XX_PCE_ACTION_PORTMAP_ALTERNATIVE = 4,
++};
++
++/**
++ * enum mxl862xx_pce_action_cross_state - Cross state action selector
++ *
++ * Controls whether the packet ignores STP port-state filtering.
++ *
++ * @MXL862XX_PCE_ACTION_CROSS_STATE_DISABLE: Cross state action is disabled
++ * @MXL862XX_PCE_ACTION_CROSS_STATE_REGULAR: Enabled; packet is treated
++ * as non-cross-state (does not ignore port-state filtering)
++ * @MXL862XX_PCE_ACTION_CROSS_STATE_CROSS: Enabled; packet ignores
++ * port-state filtering rules (e.g. passes through BLOCKING state)
++ */
++enum mxl862xx_pce_action_cross_state {
++ MXL862XX_PCE_ACTION_CROSS_STATE_DISABLE = 0,
++ MXL862XX_PCE_ACTION_CROSS_STATE_REGULAR = 1,
++ MXL862XX_PCE_ACTION_CROSS_STATE_CROSS = 2,
++};
++
++/**
++ * struct mxl862xx_pce_action - PCE rule action configuration
++ *
++ * Defines the actions applied to packets matching a PCE rule pattern.
++ *
++ * @time_comp: Signed time compensation value for OAM delay measurement
++ * @extended_vlan_block_id: Extended VLAN block allocated for this flow
++ * entry (valid when @extended_vlan_enable is set)
++ * @ins_ext_point: Insertion/extraction point
++ * @ptp_seq_id: PTP sequence ID for PTP application
++ * @pkt_update_offset: Byte offset (2..255) for counter/timestamp
++ * insertion (used when @no_pkt_update and
++ * @append_to_pkt are both false)
++ * @oam_flow_id: Traffic flow counter ID for OAM loss measurement
++ * @record_id: Record ID used by extraction and/or OAM process
++ * @forward_port_map: Target portmap for forwarded packets. Each bit
++ * represents one bridge port. Used when
++ * @port_map_action is %MXL862XX_PCE_ACTION_PORTMAP_ALTERNATIVE.
++ * @rmon_id: RMON counter ID (index starts from zero)
++ * @svlan_id: Alternative STAG VLAN ID
++ * @flow_id: Flow ID
++ * @rout_ext_id: Routing extension ID value (8-bit range)
++ * @traffic_class_alternate: Alternative traffic class (used when
++ * @traffic_class_action selects alternate)
++ * @meter_id: Meter ID
++ * @vlan_id: Alternative CTAG VLAN ID
++ * @fid: Alternative Filtering Identifier (FID)
++ * @traffic_class_action: Traffic class action selector
++ * (0 = disable, 1 = regular CoS, 2 = alternative)
++ * @snooping_type_action: IGMP snooping control selector
++ * @learning_action: MAC learning action selector
++ * (0 = disable, 1 = regular, 2 = force no learn,
++ * 3 = force learn)
++ * @irq_action: Interrupt action selector
++ * (0 = disable, 1 = regular, 2 = generate interrupt)
++ * @cross_state_action: Cross state action selector.
++ * See &enum mxl862xx_pce_action_cross_state
++ * @crit_frame_action: Critical frame action selector
++ * (0 = disable, 1 = regular, 2 = critical)
++ * @color_frame_action: Color frame action selector (replaces
++ * @crit_frame_action in GSWIP-3.1)
++ * @timestamp_action: Timestamp action selector
++ * (0 = disable, 1 = regular, 2 = store timestamps)
++ * @port_map_action: Forwarding portmap action selector.
++ * See &enum mxl862xx_pce_action_portmap
++ * @meter_action: Meter action selector
++ * (0 = disable, 1 = regular, 2 = meter 1,
++ * 3 = meter 1 and 2)
++ * @vlan_action: CTAG VLAN action selector
++ * (0 = disable, 1 = regular, 2 = alternative)
++ * @svlan_action: STAG VLAN action selector
++ * (0 = disable, 1 = regular, 2 = alternative)
++ * @vlan_cross_action: Cross-VLAN action selector
++ * (0 = disable, 1 = regular, 2 = cross-VLAN)
++ * @process_path_action: MPE processing path assignment
++ * (0 = unused, 1 = path-1, 2 = path-2, 3 = both)
++ * @port_filter_type_action: Port filter action type (0..6)
++ * @pbb_action: Provider Backbone Bridging (Mac-in-Mac) action.
++ * See &struct mxl862xx_pce_action_pbb
++ * @dest_subif_action: Destination sub-interface ID action.
++ * See &struct mxl862xx_pce_action_dest_subif
++ * @remark_action: Enable remarking action
++ * @remark_pcp: Enable CTAG VLAN PCP remarking
++ * @remark_stag_pcp: Enable STAG VLAN PCP remarking
++ * @remark_stag_dei: Enable STAG VLAN DEI remarking
++ * @remark_dscp: Enable DSCP remarking
++ * @remark_class: Enable class remarking
++ * @rmon_action: Enable RMON counter action
++ * @fid_enable: Enable alternative FID
++ * @extended_vlan_enable: Enable extended VLAN operation for matching
++ * traffic
++ * @cvlan_ignore_control: CVLAN ignore control
++ * @port_bitmap_mux_control: Port bitmap mux control
++ * @port_trunk_action: Enable trunking action
++ * @port_link_selection: Port link selection control
++ * @flow_id_action: Enable flow ID action (mutually exclusive with
++ * @port_map_action)
++ * @rout_ext_id_action: Enable routing extension ID action
++ * @rt_dst_port_mask_cmp_action: Routing destination port mask comparison
++ * @rt_src_port_mask_cmp_action: Routing source port mask comparison
++ * @rt_dst_ip_mask_cmp_action: Routing destination IP mask comparison
++ * @rt_src_ip_mask_cmp_action: Routing source IP mask comparison
++ * @rt_inner_ip_as_key_action: Use inner IP in tunneled header as
++ * routing key
++ * @rt_accel_ena_action: Routing acceleration enable
++ * @rt_ctrl_ena_action: Routing control enable (selects routing
++ * accelerate action)
++ * @extract_enable: Enable packet extraction at point defined by
++ * @record_id
++ * @oam_enable: Enable OAM processing for matching packets
++ * @pce_bypass_path: Update packet in PCE bypass path (after QoS queue)
++ * @tx_flow_cnt: Use TX flow counter (otherwise RX flow counter)
++ * @time_format: Timestamp format (0 = digital 10B, 1 = binary 10B,
++ * 2 = digital 8B, 3 = binary 8B)
++ * @no_pkt_update: Do not update packet
++ * @append_to_pkt: Append counter/timestamp to end of packet (when
++ * @no_pkt_update is false)
++ * @pbb_action_enable: Enable PBB action. See &struct mxl862xx_pce_action_pbb
++ * @dest_subif_action_enable: Enable destination sub-interface ID action.
++ * See &struct mxl862xx_pce_action_dest_subif
++ */
++struct mxl862xx_pce_action {
++ __le64 time_comp;
++ __le16 extended_vlan_block_id;
++ u8 ins_ext_point;
++ u8 ptp_seq_id;
++ __le16 pkt_update_offset;
++ __le16 oam_flow_id;
++ __le16 record_id;
++ __le16 forward_port_map[8];
++ __le16 rmon_id;
++ __le16 svlan_id;
++ __le16 flow_id;
++ __le16 rout_ext_id;
++ u8 traffic_class_alternate;
++ u8 meter_id;
++ u8 vlan_id;
++ u8 fid;
++ __le32 traffic_class_action;
++ __le32 snooping_type_action;
++ __le32 learning_action;
++ __le32 irq_action;
++ __le32 cross_state_action;
++ __le32 crit_frame_action;
++ __le32 color_frame_action;
++ __le32 timestamp_action;
++ __le32 port_map_action;
++ __le32 meter_action;
++ __le32 vlan_action;
++ __le32 svlan_action;
++ __le32 vlan_cross_action;
++ __le32 process_path_action;
++ __le32 port_filter_type_action;
++ struct mxl862xx_pce_action_pbb pbb_action;
++ struct mxl862xx_pce_action_dest_subif dest_subif_action;
++ u8 remark_action;
++ u8 remark_pcp;
++ u8 remark_stag_pcp;
++ u8 remark_stag_dei;
++ u8 remark_dscp;
++ u8 remark_class;
++ u8 rmon_action;
++ u8 fid_enable;
++ u8 extended_vlan_enable;
++ u8 cvlan_ignore_control;
++ u8 port_bitmap_mux_control;
++ u8 port_trunk_action;
++ u8 port_link_selection;
++ u8 flow_id_action;
++ u8 rout_ext_id_action;
++ u8 rt_dst_port_mask_cmp_action;
++ u8 rt_src_port_mask_cmp_action;
++ u8 rt_dst_ip_mask_cmp_action;
++ u8 rt_src_ip_mask_cmp_action;
++ u8 rt_inner_ip_as_key_action;
++ u8 rt_accel_ena_action;
++ u8 rt_ctrl_ena_action;
++ u8 extract_enable;
++ u8 oam_enable;
++ u8 pce_bypass_path;
++ u8 tx_flow_cnt;
++ __le32 time_format;
++ u8 no_pkt_update;
++ u8 append_to_pkt;
++ u8 pbb_action_enable;
++ u8 dest_subif_action_enable;
++} __packed;
++
++static_assert(sizeof(struct mxl862xx_pce_action) == 180);
++
++/**
++ * enum mxl862xx_pce_rule_region - PCE rule table region selector
++ *
++ * Selects which region of the traffic flow table the rule belongs to.
++ *
++ * @MXL862XX_PCE_RULE_COMMON: Common region shared by all CTPs
++ * (global rules, indices 0..63)
++ * @MXL862XX_PCE_RULE_CTP: Per-CTP region. The rule index is relative
++ * to the CTP block identified by logicalportid; the firmware
++ * translates it to an absolute hardware index.
++ * @MXL862XX_PCE_RULE_DEBUG: Debug region with direct HW index mapping
++ */
++enum mxl862xx_pce_rule_region {
++ MXL862XX_PCE_RULE_COMMON = 0,
++ MXL862XX_PCE_RULE_CTP = 1,
++ MXL862XX_PCE_RULE_DEBUG = 2,
++};
++
++/**
++ * struct mxl862xx_pce_rule - PCE rule configuration
++ * @logicalportid: Logical Port Id
++ * @subifidgroup: Sub-interface ID group
++ * @region: PCE rule region (common or per-CTP)
++ * @pattern: Match pattern (destination MAC, EtherType, etc.)
++ * @action: Forwarding action (portmap, cross-state, etc.)
++ *
++ * This structure is passed to the firmware via the MDIO data
++ * buffer using the %MXL862XX_TFLOW_PCERULEWRITE command.
++ * The binary layout must exactly match the firmware's
++ * GSW_PCE_rule_t (466 bytes, packed, little-endian scalars).
++ */
++struct mxl862xx_pce_rule {
++ u8 logicalportid;
++ __le16 subifidgroup;
++ __le32 region;
++ struct mxl862xx_pce_pattern pattern;
++ struct mxl862xx_pce_action action;
++} __packed;
++
++static_assert(sizeof(struct mxl862xx_pce_rule) == 466);
++
++/**
++ * struct mxl862xx_pce_rule_alloc - PCE rule block allocation
++ * @num_of_rules: Number of rules to allocate (input) / allocated (output).
++ * The firmware rounds up to a multiple of four consecutive entries.
++ * @blockid: Starting rule index of the allocated block (output on alloc,
++ * input on free).
++ *
++ * Used with %MXL862XX_TFLOW_PCERULEALLOC and %MXL862XX_TFLOW_PCERULEFREE.
++ * Maps to the firmware's ``GSW_PCE_rule_alloc_t``.
++ */
++struct mxl862xx_pce_rule_alloc {
++ __le16 num_of_rules;
++ __le16 blockid;
++} __packed;
++
++static_assert(sizeof(struct mxl862xx_pce_rule_alloc) == 4);
++
+ /**
+ * enum mxl862xx_stp_port_state - Spanning Tree Protocol port states
+ * @MXL862XX_STP_PORT_STATE_FORWARD: Forwarding state
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -12,6 +12,7 @@
+ (MXL862XX_MMD_REG_DATA_LAST - MXL862XX_MMD_REG_DATA_FIRST + 1)
+
+ #define MXL862XX_COMMON_MAGIC 0x100
++#define MXL862XX_TFLOW_MAGIC 0x200
+ #define MXL862XX_BRDG_MAGIC 0x300
+ #define MXL862XX_BRDGPORT_MAGIC 0x400
+ #define MXL862XX_CTP_MAGIC 0x500
+@@ -31,6 +32,10 @@
+ #define MXL862XX_COMMON_CFGSET (MXL862XX_COMMON_MAGIC + 0xa)
+ #define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11)
+
++#define MXL862XX_TFLOW_PCERULEWRITE (MXL862XX_TFLOW_MAGIC + 0x2)
++#define MXL862XX_TFLOW_PCERULEALLOC (MXL862XX_TFLOW_MAGIC + 0x4)
++#define MXL862XX_TFLOW_PCERULEFREE (MXL862XX_TFLOW_MAGIC + 0x5)
++
+ #define MXL862XX_BRIDGE_ALLOC (MXL862XX_BRDG_MAGIC + 0x1)
+ #define MXL862XX_BRIDGE_CONFIGSET (MXL862XX_BRDG_MAGIC + 0x2)
+ #define MXL862XX_BRIDGE_CONFIGGET (MXL862XX_BRDG_MAGIC + 0x3)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -280,9 +280,11 @@ static int mxl862xx_wait_ready(struct ds
+ ver.iv_major, ver.iv_minor,
+ le16_to_cpu(ver.iv_revision),
+ le32_to_cpu(ver.iv_build_num));
++
+ priv->fw_version.major = ver.iv_major;
+ priv->fw_version.minor = ver.iv_minor;
+ priv->fw_version.revision = le16_to_cpu(ver.iv_revision);
++
+ return 0;
+
+ not_ready_yet:
+@@ -410,6 +412,68 @@ static int mxl862xx_setup_drop_meter(str
+ return MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
+ }
+
++
++/* Per-CTP offset used for the link-local trap rule. Each port's CTP
++ * flow-table block is pre-allocated by the firmware during init (44
++ * entries per port on a 10-port SKU, of which offset 0 is reserved
++ * for flow-control marking). Offset 1 is the first unused slot.
++ */
++#define MXL862XX_LINK_LOCAL_CTP_OFFSET 1
++
++/* Install a PCE rule that traps IEEE 802.1D link-local frames
++ * (01:80:c2:00:00:0x) to the CPU port for a single user port,
++ * preventing the hardware bridge from flooding them to other ports.
++ * The firmware does not install this rule by default because its own
++ * STP module is not used when DSA manages STP.
++ *
++ * The rule is written into the port's per-CTP flow table at offset 1.
++ * The firmware already allocates a 44-entry block for every CTP during
++ * init (8 entries exposed initially, expandable), so no dynamic
++ * allocation via PCERULEALLOC is needed. Using region=CTP causes the
++ * firmware to translate the CTP-relative offset into an absolute
++ * hardware index.
++ *
++ * Cross-state is enabled so that link-local frames reach the CPU even
++ * when the bridge port is in BLOCKING or LEARNING state.
++ */
++static int mxl862xx_setup_link_local_trap(struct dsa_switch *ds, int port)
++{
++ DECLARE_BITMAP(portmap, MXL862XX_MAX_BRIDGE_PORTS);
++ struct dsa_port *dp = dsa_to_port(ds, port);
++ struct mxl862xx_pce_rule rule = {};
++ int cpu_port = dp->cpu_dp->index;
++ int i;
++
++ /* Address this port's CTP flow-table block */
++ rule.logicalportid = port;
++ rule.subifidgroup = 0;
++ rule.region = cpu_to_le32(MXL862XX_PCE_RULE_CTP);
++
++ /* Pattern: link-local MAC on this specific ingress port */
++ rule.pattern.index = cpu_to_le16(MXL862XX_LINK_LOCAL_CTP_OFFSET);
++ rule.pattern.enable = 1;
++ rule.pattern.mac_dst_enable = 1;
++ memcpy(rule.pattern.mac_dst, eth_reserved_addr_base, ETH_ALEN);
++ rule.pattern.mac_dst_mask = cpu_to_le16(0x0001);
++
++ /* Action: forward to the CPU port via explicit portmap */
++ rule.action.port_map_action =
++ cpu_to_le32(MXL862XX_PCE_ACTION_PORTMAP_ALTERNATIVE);
++
++ bitmap_zero(portmap, MXL862XX_MAX_BRIDGE_PORTS);
++ __set_bit(cpu_port, portmap);
++ for (i = 0; i < ARRAY_SIZE(rule.action.forward_port_map); i++)
++ rule.action.forward_port_map[i] =
++ cpu_to_le16(bitmap_read(portmap, i * 16, 16));
++
++ /* Bypass STP port state */
++ rule.action.cross_state_action =
++ cpu_to_le32(MXL862XX_PCE_ACTION_CROSS_STATE_CROSS);
++
++ return MXL862XX_API_WRITE(ds->priv, MXL862XX_TFLOW_PCERULEWRITE,
++ rule);
++}
++
+ static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port)
+ {
+ struct mxl862xx_bridge_port_config br_port_cfg = {};
+@@ -1594,6 +1658,11 @@ static int mxl862xx_port_setup(struct ds
+ if (ret)
+ return ret;
+
++ /* install link-local trap for this user port */
++ ret = mxl862xx_setup_link_local_trap(ds, port);
++ if (ret)
++ return ret;
++
+ /* Initialize and pre-allocate per-port EVLAN and VF blocks for
+ * user ports. CPU ports do not use EVLAN or VF -- frames pass
+ * through without processing. Pre-allocation avoids firmware
--- /dev/null
+From 3bba25f7ba35e3bca8230bd37ffb612944dbf301 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 18:51:21 +0000
+Subject: [PATCH 24/35] net: dsa: mxl862xx: warn about old firmware default PCE
+ rules
+
+Firmware versions older than 1.0.80 install global PCE rules at
+boot that redirect link-local frames (BPDUs, LLDP, LACP) to port 0
+(the on-chip microcontroller) instead of the DSA CPU port. With
+port 0 disabled under DSA, these rules silently drop matching
+traffic.
+
+Log a warning when old firmware is detected so users know to update.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -854,6 +854,10 @@ static int mxl862xx_setup(struct dsa_swi
+ if (ret)
+ return ret;
+
++ if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
++ dev_warn(ds->dev, "firmware < 1.0.80 installs global PCE rules "
++ "that interfere with DSA operation, please update\n");
++
+ schedule_delayed_work(&priv->stats_work,
+ MXL862XX_STATS_POLL_INTERVAL);
+
--- /dev/null
+From 1687c5632dfd80461b12425b943e30555faa3dd4 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Sun, 22 Mar 2026 00:58:04 +0000
+Subject: [PATCH 25/35] net: dsa: add 802.1Q VLAN-based tag driver for MxL862xx
+
+The MxL862xx native 8-byte special tag (SpTag) requires firmware
+support on the switch CPU and is not compatible with all SoC Ethernet
+controllers. An 802.1Q VLAN-based tagging alternative allows the
+switch to operate with any standard Ethernet MAC that supports VLAN
+tag insertion and stripping.
+
+Add a DSA tag driver that uses the tag_8021q framework to encode
+source and destination port information in standard 802.1Q VLAN
+tags. Register the DSA_TAG_PROTO_MXL862_8021Q protocol in the DSA
+header. Map TX queue priority to PCP in the xmit path and extract
+switch_id and source port from the management VID in the receive
+path. Set promisc_on_conduit so the conduit interface accepts
+frames with management VIDs that are not in its own VLAN filter.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/Kconfig | 1 +
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 221 +++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 2 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 1626 ++++++++++++++++++++---
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 21 +-
+ include/net/dsa.h | 2 +
+ net/dsa/Kconfig | 7 +
+ net/dsa/Makefile | 1 +
+ net/dsa/tag_mxl862xx_8021q.c | 59 +
+ 9 files changed, 1738 insertions(+), 202 deletions(-)
+ create mode 100644 net/dsa/tag_mxl862xx_8021q.c
+
+--- a/drivers/net/dsa/mxl862xx/Kconfig
++++ b/drivers/net/dsa/mxl862xx/Kconfig
+@@ -4,6 +4,7 @@ config NET_DSA_MXL862
+ depends on NET_DSA
+ select CRC16
+ select NET_DSA_TAG_MXL_862XX
++ select NET_DSA_TAG_MXL_862XX_8021Q
+ help
+ This enables support for the MaxLinear MxL862xx switch family.
+ These switches have two 10GE SerDes interfaces, one typically
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -1185,6 +1185,227 @@ struct mxl862xx_ctp_port_assignment {
+ } __packed;
+
+ /**
++ * enum mxl862xx_ctp_port_config_mask - CTP Port Configuration Mask
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_BRIDGE_PORT_ID:
++ * Mask for bridge_port_id.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_FORCE_TRAFFIC_CLASS:
++ * Mask for forced_traffic_class and default_traffic_class.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_VLAN:
++ * Mask for ingress_extended_vlan_enable and
++ * ingress_extended_vlan_block_id.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_VLAN_IGMP:
++ * Mask for ingress_extended_vlan_igmp_enable and
++ * ingress_extended_vlan_block_id_igmp.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_VLAN:
++ * Mask for egress_extended_vlan_enable and
++ * egress_extended_vlan_block_id.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_VLAN_IGMP:
++ * Mask for egress_extended_vlan_igmp_enable and
++ * egress_extended_vlan_block_id_igmp.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_NTO1_VLAN:
++ * Mask for ingress_nto1vlan_enable.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_NTO1_VLAN:
++ * Mask for egress_nto1vlan_enable.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_MARKING:
++ * Mask for ingress_marking_mode.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_MARKING:
++ * Mask for egress_marking_mode.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_MARKING_OVERRIDE:
++ * Mask for egress_marking_override_enable and
++ * egress_marking_mode_override.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_REMARKING:
++ * Mask for egress_remarking_mode.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_METER:
++ * Mask for ingress_metering_enable and ingress_traffic_meter_id.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_METER:
++ * Mask for egress_metering_enable and egress_traffic_meter_id.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_BRIDGING_BYPASS:
++ * Mask for bridging_bypass, dest_logical_port_id, pmapper_enable,
++ * dest_sub_if_id_group, pmapper_mapping_mode, pmapper_id_valid and
++ * pmapper_dest_sub_if_id_group.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_FLOW_ENTRY:
++ * Mask for first_flow_entry_index and number_of_flow_entries.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_LOOPBACK_AND_MIRROR:
++ * Mask for ingress_loopback_enable, ingress_da_sa_swap_enable,
++ * egress_loopback_enable, egress_da_sa_swap_enable,
++ * ingress_mirror_enable and egress_mirror_enable.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_ALL: Enable all fields.
++ * @MXL862XX_CTP_PORT_CONFIG_MASK_FORCE: Bypass any check for debug purpose.
++ */
++enum mxl862xx_ctp_port_config_mask {
++ MXL862XX_CTP_PORT_CONFIG_MASK_BRIDGE_PORT_ID = BIT(0),
++ MXL862XX_CTP_PORT_CONFIG_MASK_FORCE_TRAFFIC_CLASS = BIT(1),
++ MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_VLAN = BIT(2),
++ MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_VLAN_IGMP = BIT(3),
++ MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_VLAN = BIT(4),
++ MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_VLAN_IGMP = BIT(5),
++ MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_NTO1_VLAN = BIT(6),
++ MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_NTO1_VLAN = BIT(7),
++ MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_MARKING = BIT(8),
++ MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_MARKING = BIT(9),
++ MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_MARKING_OVERRIDE = BIT(10),
++ MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_REMARKING = BIT(11),
++ MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_METER = BIT(12),
++ MXL862XX_CTP_PORT_CONFIG_MASK_EGRESS_METER = BIT(13),
++ MXL862XX_CTP_PORT_CONFIG_MASK_BRIDGING_BYPASS = BIT(14),
++ MXL862XX_CTP_PORT_CONFIG_MASK_FLOW_ENTRY = BIT(15),
++ MXL862XX_CTP_PORT_CONFIG_MASK_LOOPBACK_AND_MIRROR = BIT(16),
++ MXL862XX_CTP_PORT_CONFIG_MASK_ALL = 0x7FFFFFFF,
++ MXL862XX_CTP_PORT_CONFIG_MASK_FORCE = BIT(31),
++};
++
++/**
++ * struct mxl862xx_ctp_port_config - CTP Port Configuration
++ * @logical_port_id: Logical Port Id. The valid range is hardware dependent.
++ * Ignored when mask has
++ * %MXL862XX_CTP_PORT_CONFIG_MASK_FORCE.
++ * @n_sub_if_id_group: Sub interface ID group. The valid range is
++ * hardware/protocol dependent. When mask has
++ * %MXL862XX_CTP_PORT_CONFIG_MASK_FORCE, this is the
++ * absolute CTP index in hardware (debug only).
++ * @mask: See &enum mxl862xx_ctp_port_config_mask
++ * @bridge_port_id: Ingress Bridge Port ID to which this CTP port is
++ * associated for ingress traffic
++ * @forced_traffic_class: Default traffic class cannot be overridden by other
++ * rules (except traffic flow table and special tag)
++ * @default_traffic_class: Default traffic class for all ingress traffic from
++ * this CTP port
++ * @ingress_extended_vlan_enable: Enable extended VLAN processing for ingress
++ * non-IGMP traffic
++ * @ingress_extended_vlan_block_id: Extended VLAN block allocated for ingress
++ * non-IGMP traffic. Valid when
++ * ingress_extended_vlan_enable is set.
++ * @ingress_extended_vlan_block_size: Extended VLAN block size for ingress
++ * non-IGMP traffic. If 0, the block size of
++ * ingress_extended_vlan_block_id is used.
++ * @ingress_extended_vlan_igmp_enable: Enable extended VLAN processing for
++ * ingress IGMP traffic
++ * @ingress_extended_vlan_block_id_igmp: Extended VLAN block allocated for
++ * ingress IGMP traffic. Valid when
++ * ingress_extended_vlan_igmp_enable is
++ * set.
++ * @ingress_extended_vlan_block_size_igmp: Extended VLAN block size for ingress
++ * IGMP traffic. If 0, the block size of
++ * ingress_extended_vlan_block_id_igmp
++ * is used.
++ * @egress_extended_vlan_enable: Enable extended VLAN processing for egress
++ * non-IGMP traffic
++ * @egress_extended_vlan_block_id: Extended VLAN block allocated for egress
++ * non-IGMP traffic. Valid when
++ * egress_extended_vlan_enable is set.
++ * @egress_extended_vlan_block_size: Extended VLAN block size for egress
++ * non-IGMP traffic. If 0, the block size of
++ * egress_extended_vlan_block_id is used.
++ * @egress_extended_vlan_igmp_enable: Enable extended VLAN processing for
++ * egress IGMP traffic
++ * @egress_extended_vlan_block_id_igmp: Extended VLAN block allocated for
++ * egress IGMP traffic. Valid when
++ * egress_extended_vlan_igmp_enable is set.
++ * @egress_extended_vlan_block_size_igmp: Extended VLAN block size for egress
++ * IGMP traffic. If 0, the block size of
++ * egress_extended_vlan_block_id_igmp is
++ * used.
++ * @ingress_nto1vlan_enable: If enabled and ingress packet is VLAN tagged,
++ * outer VLAN ID is used for nSubIfId field in MAC
++ * table; otherwise 0 is used
++ * @egress_nto1vlan_enable: If enabled and egress packet is known unicast,
++ * outer VLAN ID is from nSubIfId field in MAC table
++ * @ingress_marking_mode: Ingress color marking mode for ingress traffic
++ * @egress_marking_mode: Egress color marking mode for ingress traffic at
++ * egress priority queue color marking stage
++ * @egress_marking_override_enable: Override color marking mode from last stage
++ * @egress_marking_mode_override: Egress color marking mode for egress traffic.
++ * Valid only when
++ * egress_marking_override_enable is set.
++ * @egress_remarking_mode: Color remarking for egress traffic
++ * @ingress_metering_enable: Traffic metering on ingress traffic applies
++ * @ingress_traffic_meter_id: Meter for ingress CTP process
++ * @egress_metering_enable: Traffic metering on egress traffic applies
++ * @egress_traffic_meter_id: Meter for egress CTP process
++ * @bridging_bypass: Ingress traffic bypasses bridging/multicast processing.
++ * Traffic flow table is not bypassed.
++ * @dest_logical_port_id: Destination logical port when bridging_bypass is set
++ * @pmapper_enable: When bridging_bypass is set, selects whether to use
++ * dest_sub_if_id_group or P-mapper for sub interface
++ * @dest_sub_if_id_group: Destination sub interface ID group when
++ * bridging_bypass is set and pmapper_enable is false
++ * @pmapper_mapping_mode: When bridging_bypass and pmapper_enable are set,
++ * selects DSCP or PCP to derive sub interface ID
++ * @pmapper_id_valid: When set, P-mapper is re-used and no new allocation or
++ * value change occurs. When false, allocation is handled
++ * by the API.
++ * @pmapper_id: P-mapper ID. Valid when pmapper_id_valid is set.
++ * @pmapper_dest_sub_if_id_group: P-mapper destination sub interface ID group
++ * entries (73 bytes, firmware layout)
++ * @first_flow_entry_index: First traffic flow table entry associated to this
++ * CTP port. Should be a multiple of 4.
++ * @number_of_flow_entries: Number of traffic flow table entries associated to
++ * this CTP port. Should be a multiple of 4.
++ * @ingress_loopback_enable: Ingress traffic is redirected to ingress logical
++ * port of this CTP with source sub interface ID as
++ * destination. Bypasses processing except flow table.
++ * @ingress_da_sa_swap_enable: Destination/Source MAC address of ingress traffic
++ * is swapped before transmitted
++ * @egress_loopback_enable: Egress traffic to this CTP port is redirected to
++ * ingress logical port with same sub interface ID
++ * @egress_da_sa_swap_enable: Destination/Source MAC address of egress traffic
++ * is swapped before transmitted
++ * @ingress_mirror_enable: If enabled, ingress traffic is mirrored to the
++ * monitoring port. Mutually exclusive with
++ * ingress_loopback_enable.
++ * @egress_mirror_enable: If enabled, egress traffic is mirrored to the
++ * monitoring port. Mutually exclusive with
++ * egress_loopback_enable.
++ */
++struct mxl862xx_ctp_port_config {
++ u8 logical_port_id;
++ __le16 n_sub_if_id_group;
++ __le32 mask;
++ __le16 bridge_port_id;
++ u8 forced_traffic_class;
++ u8 default_traffic_class;
++ u8 ingress_extended_vlan_enable;
++ __le16 ingress_extended_vlan_block_id;
++ __le16 ingress_extended_vlan_block_size;
++ u8 ingress_extended_vlan_igmp_enable;
++ __le16 ingress_extended_vlan_block_id_igmp;
++ __le16 ingress_extended_vlan_block_size_igmp;
++ u8 egress_extended_vlan_enable;
++ __le16 egress_extended_vlan_block_id;
++ __le16 egress_extended_vlan_block_size;
++ u8 egress_extended_vlan_igmp_enable;
++ __le16 egress_extended_vlan_block_id_igmp;
++ __le16 egress_extended_vlan_block_size_igmp;
++ u8 ingress_nto1vlan_enable;
++ u8 egress_nto1vlan_enable;
++ __le32 ingress_marking_mode;
++ __le32 egress_marking_mode;
++ u8 egress_marking_override_enable;
++ __le32 egress_marking_mode_override;
++ __le32 egress_remarking_mode;
++ u8 ingress_metering_enable;
++ __le16 ingress_traffic_meter_id;
++ u8 egress_metering_enable;
++ __le16 egress_traffic_meter_id;
++ u8 bridging_bypass;
++ u8 dest_logical_port_id;
++ u8 pmapper_enable;
++ __le16 dest_sub_if_id_group;
++ __le32 pmapper_mapping_mode;
++ u8 pmapper_id_valid;
++ __le16 pmapper_id;
++ u8 pmapper_dest_sub_if_id_group[73];
++ __le16 first_flow_entry_index;
++ __le16 number_of_flow_entries;
++ u8 ingress_loopback_enable;
++ u8 ingress_da_sa_swap_enable;
++ u8 egress_loopback_enable;
++ u8 egress_da_sa_swap_enable;
++ u8 ingress_mirror_enable;
++ u8 egress_mirror_enable;
++} __packed;
++
++/**
+ * enum mxl862xx_port_duplex - Ethernet port duplex status
+ * @MXL862XX_DUPLEX_FULL: Port operates in full-duplex mode
+ * @MXL862XX_DUPLEX_HALF: Port operates in half-duplex mode
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -47,12 +47,14 @@
+ #define MXL862XX_BRIDGEPORT_FREE (MXL862XX_BRDGPORT_MAGIC + 0x4)
+
+ #define MXL862XX_CTP_PORTASSIGNMENTSET (MXL862XX_CTP_MAGIC + 0x3)
++#define MXL862XX_CTP_PORTCONFIGSET (MXL862XX_CTP_MAGIC + 0x5)
+
+ #define MXL862XX_QOS_METERCFGSET (MXL862XX_QOS_MAGIC + 0x2)
+ #define MXL862XX_QOS_METERALLOC (MXL862XX_QOS_MAGIC + 0x2a)
+
+ #define MXL862XX_RMON_PORT_GET (MXL862XX_RMON_MAGIC + 0x1)
+
++#define MXL862XX_MAC_TABLECLEAR (MXL862XX_SWMAC_MAGIC + 0x1)
+ #define MXL862XX_MAC_TABLEENTRYADD (MXL862XX_SWMAC_MAGIC + 0x2)
+ #define MXL862XX_MAC_TABLEENTRYREAD (MXL862XX_SWMAC_MAGIC + 0x3)
+ #define MXL862XX_MAC_TABLEENTRYQUERY (MXL862XX_SWMAC_MAGIC + 0x4)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -16,6 +16,7 @@
+ #include <linux/of_mdio.h>
+ #include <linux/phy.h>
+ #include <linux/phylink.h>
++#include <linux/dsa/8021q.h>
+ #include <net/dsa.h>
+
+ #include "mxl862xx.h"
+@@ -115,6 +116,9 @@ enum mxl862xx_evlan_action {
+ EVLAN_PVID_OR_DISCARD, /* insert PVID tag or discard if no PVID */
+ EVLAN_PVID_OR_PASS, /* insert PVID tag or pass-through */
+ EVLAN_STRIP1_AND_PVID_OR_DISCARD,/* strip 1 tag + insert PVID, or discard */
++ EVLAN_INSERT_OUTER, /* insert outer tag with mgmt_vid */
++ EVLAN_STRIP1, /* strip 1 tag unconditionally */
++ EVLAN_REASSIGN, /* reassign bridge port (keep tags) */
+ };
+
+ struct mxl862xx_evlan_rule_desc {
+@@ -124,6 +128,7 @@ struct mxl862xx_evlan_rule_desc {
+ u8 inner_tpid; /* enum mxl862xx_extended_vlan_filter_tpid */
+ bool match_vid; /* true: match on VID from the vid parameter */
+ u8 action; /* enum mxl862xx_evlan_action */
++ u16 bridge_port_id; /* for EVLAN_REASSIGN */
+ };
+
+ /* Shorthand constants for readability */
+@@ -190,11 +195,69 @@ static const struct mxl862xx_evlan_rule_
+ { FT_NO_FILTER, FT_NO_TAG, TP_NONE, TP_NONE, true, EVLAN_STRIP_IF_UNTAGGED },
+ };
+
++/*
++ * tag_8021q: virtual bridge port egress rules.
++ *
++ * Inserts the management VID as an outer 802.1Q tag on all frames
++ * exiting toward the CPU via a virtual bridge port. Covers every
++ * possible frame type (untagged, single-tagged, double-tagged).
++ *
++ * 802.1Q ACCEPT rules must precede NO_FILTER catchalls to prevent
++ * NO_FILTER from matching standard 802.1Q frames first.
++ */
++static const struct mxl862xx_evlan_rule_desc cpu_egress_tag_8021q[] = {
++ /* 802.1Q outer + inner present */
++ { FT_NORMAL, FT_NORMAL, TP_8021Q, TP_8021Q, false, EVLAN_INSERT_OUTER },
++ /* 802.1Q outer, no inner */
++ { FT_NORMAL, FT_NO_TAG, TP_8021Q, TP_NONE, false, EVLAN_INSERT_OUTER },
++ /* Non-8021Q outer + inner present */
++ { FT_NO_FILTER, FT_NO_FILTER, TP_NONE, TP_NONE, false, EVLAN_INSERT_OUTER },
++ /* Non-8021Q outer only */
++ { FT_NO_FILTER, FT_NO_TAG, TP_NONE, TP_NONE, false, EVLAN_INSERT_OUTER },
++ /* Untagged */
++ { FT_NO_TAG, FT_NO_TAG, TP_NONE, TP_NONE, false, EVLAN_INSERT_OUTER },
++};
++
++/*
++ * tag_8021q: CPU port ingress reassignment rules.
++ *
++ * Each user port with a management VID gets these rules on the CPU port's
++ * ingress EVLAN block. They match the management VID as outer 802.1Q tag
++ * and reassign the frame to the user port's virtual bridge port.
++ *
++ * NO_FILTER is used for the inner position so that frames with any inner
++ * TPID (including non-802.1Q TPIDs like 802.1ad 0x88A8) are routed
++ * correctly. The management VID tag is kept and stripped later by the
++ * user port's egress EVLAN catchall rules.
++ *
++ * The bridge_port_id is overridden per-port at programming time.
++ */
++static const struct mxl862xx_evlan_rule_desc cpu_ingress_reassign[] = {
++ /* Mgmt VID outer + any inner tag present */
++ { FT_NORMAL, FT_NO_FILTER, TP_8021Q, TP_NONE, true, EVLAN_REASSIGN },
++ /* Mgmt VID outer, no inner */
++ { FT_NORMAL, FT_NO_TAG, TP_8021Q, TP_NONE, true, EVLAN_REASSIGN },
++};
++
++/* User port egress catchall rules for tag_8021q mode.
++ * Strip the outer management VID tag from CPU->user frames that were
++ * not matched by any per-VID egress rule. Appended to the user port
++ * egress EVLAN block when tag_8021q is active.
++ */
++static const struct mxl862xx_evlan_rule_desc tag_8021q_egress_strip[] = {
++ /* Any outer tag + inner present: strip outer (mgmt VID) */
++ { FT_NO_FILTER, FT_NO_FILTER, TP_NONE, TP_NONE, false, EVLAN_STRIP1 },
++ /* Any outer tag, no inner: strip it */
++ { FT_NO_FILTER, FT_NO_TAG, TP_NONE, TP_NONE, false, EVLAN_STRIP1 },
++};
++
+ static enum dsa_tag_protocol mxl862xx_get_tag_protocol(struct dsa_switch *ds,
+ int port,
+ enum dsa_tag_protocol m)
+ {
+- return DSA_TAG_PROTO_MXL862;
++ struct mxl862xx_priv *priv = ds->priv;
++
++ return priv->tag_proto;
+ }
+
+ /* PHY access via firmware relay */
+@@ -420,6 +483,78 @@ static int mxl862xx_setup_drop_meter(str
+ */
+ #define MXL862XX_LINK_LOCAL_CTP_OFFSET 1
+
++/**
++ * mxl862xx_cpu_bridge_port_id - Get the bridge port ID for CPU-side forwarding
++ * @ds: DSA switch
++ * @port: user port number
++ *
++ * In tag_8021q mode, returns the virtual bridge port ID so that frames
++ * destined for the CPU pass through the virtual bridge port's egress
++ * EVLAN (which inserts the management VID). In native SpTag mode,
++ * returns the physical CPU port index.
++ */
++static int mxl862xx_cpu_bridge_port_id(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q && p->bridge_port_cpu)
++ return p->bridge_port_cpu;
++
++ return dsa_to_port(ds, port)->cpu_dp->index;
++}
++
++/**
++ * mxl862xx_tag_8021q_disable_cpu_egress - Disable virtual bridge port egress EVLAN
++ * @ds: DSA switch
++ * @port: user port whose virtual bridge port egress EVLAN to disable
++ */
++static void mxl862xx_tag_8021q_disable_cpu_egress(struct dsa_switch *ds,
++ int port)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ struct mxl862xx_bridge_port_config bp_cfg = {};
++
++ if (!p->bridge_port_cpu || !p->cpu_egress_evlan.allocated)
++ return;
++
++ /* Disable egress EVLAN on the virtual bridge port */
++ bp_cfg.bridge_port_id = cpu_to_le16(p->bridge_port_cpu);
++ bp_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN);
++ bp_cfg.egress_extended_vlan_enable = 0;
++ MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, bp_cfg);
++
++ p->cpu_egress_evlan.in_use = false;
++}
++
++/**
++ * mxl862xx_set_cpu_ctp_ingress_evlan - Assign ingress EVLAN to the CPU
++ * port's CTP
++ * @ds: DSA switch
++ * @cpu: CPU port index
++ *
++ * Both the reference and legacy drivers assign the CPU port's ingress
++ * EVLAN at the CTP level (via CTP_PORTCONFIGSET) rather than the
++ * bridge port level (BRIDGEPORT_CONFIGSET). The GSWIP ingress
++ * pipeline evaluates Bridge Port EVLAN first, then CTP EVLAN; the
++ * bridge port reassignment treatment used by tag_8021q only works
++ * reliably from the CTP level.
++ */
++static int mxl862xx_set_cpu_ctp_ingress_evlan(struct dsa_switch *ds, int cpu)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_evlan_block *blk = &priv->ports[cpu].ingress_evlan;
++ struct mxl862xx_ctp_port_config ctp = {};
++
++ ctp.logical_port_id = cpu;
++ ctp.mask = cpu_to_le32(MXL862XX_CTP_PORT_CONFIG_MASK_INGRESS_VLAN);
++ ctp.ingress_extended_vlan_enable = blk->in_use;
++ ctp.ingress_extended_vlan_block_id = cpu_to_le16(blk->block_id);
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
++}
++
+ /* Install a PCE rule that traps IEEE 802.1D link-local frames
+ * (01:80:c2:00:00:0x) to the CPU port for a single user port,
+ * preventing the hardware bridge from flooding them to other ports.
+@@ -440,10 +575,14 @@ static int mxl862xx_setup_link_local_tra
+ {
+ DECLARE_BITMAP(portmap, MXL862XX_MAX_BRIDGE_PORTS);
+ struct dsa_port *dp = dsa_to_port(ds, port);
++ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_pce_rule rule = {};
+ int cpu_port = dp->cpu_dp->index;
++ struct mxl862xx_port *p;
+ int i;
+
++ p = &priv->ports[port];
++
+ /* Address this port's CTP flow-table block */
+ rule.logicalportid = port;
+ rule.subifidgroup = 0;
+@@ -466,11 +605,18 @@ static int mxl862xx_setup_link_local_tra
+ rule.action.forward_port_map[i] =
+ cpu_to_le16(bitmap_read(portmap, i * 16, 16));
+
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q &&
++ p->cpu_egress_evlan.in_use) {
++ rule.action.extended_vlan_enable = 1;
++ rule.action.extended_vlan_block_id =
++ cpu_to_le16(p->cpu_egress_evlan.block_id);
++ }
++
+ /* Bypass STP port state */
+ rule.action.cross_state_action =
+ cpu_to_le32(MXL862XX_PCE_ACTION_CROSS_STATE_CROSS);
+
+- return MXL862XX_API_WRITE(ds->priv, MXL862XX_TFLOW_PCERULEWRITE,
++ return MXL862XX_API_WRITE(priv, MXL862XX_TFLOW_PCERULEWRITE,
+ rule);
+ }
+
+@@ -587,7 +733,8 @@ static int mxl862xx_sync_bridge_members(
+ __set_bit(member_dp->index,
+ priv->ports[port].portmap);
+ }
+- __set_bit(dp->cpu_dp->index, priv->ports[port].portmap);
++ __set_bit(mxl862xx_cpu_bridge_port_id(ds, port),
++ priv->ports[port].portmap);
+
+ err = mxl862xx_set_bridge_port(ds, port);
+ if (err)
+@@ -762,7 +909,6 @@ static void mxl862xx_free_bridge(struct
+
+ static int mxl862xx_add_single_port_bridge(struct dsa_switch *ds, int port)
+ {
+- struct dsa_port *dp = dsa_to_port(ds, port);
+ struct mxl862xx_priv *priv = ds->priv;
+ int ret;
+
+@@ -774,15 +920,27 @@ static int mxl862xx_add_single_port_brid
+
+ priv->ports[port].learning = false;
+ bitmap_zero(priv->ports[port].portmap, MXL862XX_MAX_BRIDGE_PORTS);
+- __set_bit(dp->cpu_dp->index, priv->ports[port].portmap);
++ __set_bit(mxl862xx_cpu_bridge_port_id(ds, port),
++ priv->ports[port].portmap);
+
+ ret = mxl862xx_set_bridge_port(ds, port);
+ if (ret)
+ return ret;
+
+- /* Standalone ports should not flood unknown unicast or multicast
+- * towards the CPU by default; only broadcast is needed initially.
+- */
++ /* In tag_8021q mode the TX path goes through the bridge engine
++ * (CTP ingress EVLAN reassigns to a virtual bridge port which
++ * then forwards via the bridge). With learning disabled on
++ * standalone ports, unknown unicast must be flooded so that
++ * frames from the host can reach the user port.
++ *
++ * In native SpTag mode, TX bypasses the bridge engine entirely
++ * (the special tag selects the egress port directly), so flood
++ * control only affects CPU-bound traffic and can be restrictive.
++ */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q)
++ return mxl862xx_bridge_config_fwd(ds, priv->ports[port].fid,
++ true, true, true);
++
+ return mxl862xx_bridge_config_fwd(ds, priv->ports[port].fid,
+ false, false, true);
+ }
+@@ -790,10 +948,12 @@ static int mxl862xx_add_single_port_brid
+ static int mxl862xx_setup(struct dsa_switch *ds)
+ {
+ struct mxl862xx_priv *priv = ds->priv;
+- int n_user_ports = 0, max_vlans;
++ int n_user_ports = 0, n_cpu_ports = 0, max_vlans;
++ int cpu_egress_rules, cpu_ingress_per_port;
+ int ingress_finals, vid_rules;
++ int egress_catchalls, evlan_reserved;
+ struct dsa_port *dp;
+- int ret, i;
++ int ret, i, port;
+
+ ret = mxl862xx_reset(priv);
+ if (ret)
+@@ -806,7 +966,7 @@ static int mxl862xx_setup(struct dsa_swi
+ for (i = 0; i < 8; i++)
+ mxl862xx_setup_pcs(priv, &priv->serdes_ports[i], i + 9);
+
+- /* Calculate Extended VLAN block sizes.
++ /* Calculate Extended VLAN and VLAN Filter block sizes.
+ * With VLAN Filter handling VID membership checks:
+ * Ingress: only final catchall rules (PVID insertion, 802.1Q
+ * accept, non-8021Q TPID handling, discard).
+@@ -814,40 +974,67 @@ static int mxl862xx_setup(struct dsa_swi
+ * ingress EVLAN rules are needed. (7 entries.)
+ * Egress: 2 rules per VID that needs tag stripping (untagged VIDs).
+ * No egress final catchalls -- VLAN Filter does the discard.
+- * CPU: EVLAN is left disabled on CPU ports -- frames pass
+- * through without EVLAN processing.
++ *
++ * tag_8021q mode reserves additional resources from the global
++ * pools for management VID handling:
++ * EVLAN: 5 egress rules per user port (on virtual bridge ports)
++ * + dynamically-sized CPU ingress EVLAN (2 per user port,
++ * budgeted here to guarantee space).
++ * VF: CPU port needs its own VF block for management VIDs.
+ *
+ * Total EVLAN budget:
+- * n_user_ports * (ingress + egress) ≤ 1024.
+- * Ingress blocks are small (7 entries), so almost all capacity
+- * goes to egress VID rules.
++ * n_user_ports * (ingress + egress + cpu_egress + cpu_ingress_share)
++ * <= 1024.
++ * Total VF budget:
++ * (n_user_ports + n_cpu_ports) * vf_block_size <= 1024.
+ */
+ dsa_switch_for_each_user_port(dp, ds)
+ n_user_ports++;
++ dsa_switch_for_each_cpu_port(dp, ds)
++ n_cpu_ports++;
+
+ if (n_user_ports) {
+ ingress_finals = ARRAY_SIZE(ingress_aware_final);
+ vid_rules = ARRAY_SIZE(vid_accept_standard);
++ cpu_egress_rules = ARRAY_SIZE(cpu_egress_tag_8021q);
++ cpu_ingress_per_port = ARRAY_SIZE(cpu_ingress_reassign);
++ egress_catchalls = ARRAY_SIZE(tag_8021q_egress_strip);
+
+ /* Ingress block: fixed at finals count (7 entries) */
+ priv->evlan_ingress_size = ingress_finals;
+
++ /* CPU port ingress EVLAN: reassign rules per user port */
++ priv->cpu_evlan_ingress_size =
++ cpu_ingress_per_port * n_user_ports;
++
++ /* Reserve EVLAN entries for tag_8021q:
++ * - virtual bridge port egress blocks (cpu_egress_rules each)
++ * - CPU port ingress EVLAN (cpu_ingress_per_port each)
++ * - user port egress catchalls for mgmt VID stripping
++ */
++ evlan_reserved = n_user_ports * (ingress_finals +
++ cpu_egress_rules +
++ cpu_ingress_per_port +
++ egress_catchalls);
++
+ /* Egress block: remaining budget divided equally among
+ * user ports. Each untagged VID needs vid_rules (2)
+ * EVLAN entries for tag stripping. Tagged-only VIDs
+- * need no EVLAN rules at all.
++ * need no EVLAN rules at all. The block also includes
++ * space for the tag_8021q egress catchall rules.
+ */
+- max_vlans = (MXL862XX_TOTAL_EVLAN_ENTRIES -
+- n_user_ports * ingress_finals) /
++ max_vlans = (MXL862XX_TOTAL_EVLAN_ENTRIES - evlan_reserved) /
+ (n_user_ports * vid_rules);
+- priv->evlan_egress_size = vid_rules * max_vlans;
++ priv->evlan_egress_size = vid_rules * max_vlans +
++ egress_catchalls;
+
+- /* VLAN Filter block: one per user port. The 1024-entry
+- * table is divided equally among user ports. Each port
+- * gets its own VF block for per-port VID membership --
+- * discard_unmatched_tagged handles the rest.
++ /* VLAN Filter block: one per user port plus one per CPU
++ * port (used in tag_8021q mode for management VIDs).
++ * The 1024-entry table is divided equally among all
++ * consumers.
+ */
+- priv->vf_block_size = MXL862XX_TOTAL_VF_ENTRIES / n_user_ports;
++ priv->vf_block_size = MXL862XX_TOTAL_VF_ENTRIES /
++ (n_user_ports + n_cpu_ports);
+ }
+
+ ret = mxl862xx_setup_drop_meter(ds);
+@@ -858,6 +1045,68 @@ static int mxl862xx_setup(struct dsa_swi
+ dev_warn(ds->dev, "firmware < 1.0.80 installs global PCE rules "
+ "that interfere with DSA operation, please update\n");
+
++ /* Pre-allocate firmware resources for all ports. The DSA core
++ * calls change_tag_protocol() between setup() and port_setup(),
++ * and in tag_8021q mode that triggers dsa_tag_8021q_register()
++ * which fires tag_8021q_vlan_add callbacks that need EVLAN and
++ * VF blocks. complete_tag_8021q_setup() also needs per-port
++ * FIDs from add_single_port_bridge().
++ *
++ * Per-port configuration (SpTag, CTP, portmaps, link-local
++ * traps) is deferred to port_setup().
++ */
++ dsa_switch_for_each_cpu_port(dp, ds) {
++ port = dp->index;
++
++ mxl862xx_vf_init(&priv->ports[port].vf,
++ priv->vf_block_size);
++ mxl862xx_evlan_block_init(&priv->ports[port].ingress_evlan,
++ priv->cpu_evlan_ingress_size);
++ ret = mxl862xx_evlan_block_alloc(priv,
++ &priv->ports[port].ingress_evlan);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_vf_alloc(priv, &priv->ports[port].vf);
++ if (ret)
++ return ret;
++ }
++
++ dsa_switch_for_each_user_port(dp, ds) {
++ port = dp->index;
++
++ mxl862xx_evlan_block_init(&priv->ports[port].ingress_evlan,
++ priv->evlan_ingress_size);
++ ret = mxl862xx_evlan_block_alloc(priv,
++ &priv->ports[port].ingress_evlan);
++ if (ret)
++ return ret;
++
++ mxl862xx_evlan_block_init(&priv->ports[port].egress_evlan,
++ priv->evlan_egress_size);
++ ret = mxl862xx_evlan_block_alloc(priv,
++ &priv->ports[port].egress_evlan);
++ if (ret)
++ return ret;
++
++ mxl862xx_vf_init(&priv->ports[port].vf,
++ priv->vf_block_size);
++ ret = mxl862xx_vf_alloc(priv, &priv->ports[port].vf);
++ if (ret)
++ return ret;
++
++ mxl862xx_evlan_block_init(&priv->ports[port].cpu_egress_evlan,
++ ARRAY_SIZE(cpu_egress_tag_8021q));
++ ret = mxl862xx_evlan_block_alloc(priv,
++ &priv->ports[port].cpu_egress_evlan);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_add_single_port_bridge(ds, port);
++ if (ret)
++ return ret;
++ }
++
+ schedule_delayed_work(&priv->stats_work,
+ MXL862XX_STATS_POLL_INTERVAL);
+
+@@ -939,6 +1188,52 @@ static int mxl862xx_configure_sp_tag_pro
+ }
+
+ /**
++ * mxl862xx_set_cpu_vbp - Push CPU-side virtual bridge port config to firmware
++ * @ds: DSA switch
++ * @port: user port index whose VBP to configure
++ *
++ * Each user port in tag_8021q mode has a virtual bridge port (VBP) that
++ * sits on the CPU RX path. The VBP lives in the user port's permanent
++ * per-port FID so host FDB/MDB entries in that FID can target it directly.
++ * Per-port host flood control is implemented via egress sub-meters on
++ * the VBP.
++ *
++ * This is intentionally separate from mxl862xx_set_bridge_port() because
++ * the VBP and the physical bridge port are independent firmware entities:
++ * host flood changes (deferred from atomic context) only need the VBP
++ * update, and VLAN/STP changes only need the physical bridge port update.
++ */
++static int mxl862xx_set_cpu_vbp(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_bridge_port_config vbp_cfg = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ bool enable;
++ int i, idx;
++
++ if (!p->bridge_port_cpu)
++ return 0;
++
++ vbp_cfg.bridge_port_id = cpu_to_le16(p->bridge_port_cpu);
++ vbp_cfg.mask = cpu_to_le32(
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER);
++ vbp_cfg.bridge_id = cpu_to_le16(p->fid);
++
++ for (i = 0; i < ARRAY_SIZE(mxl862xx_flood_meters); i++) {
++ idx = mxl862xx_flood_meters[i];
++ enable = !!(p->host_flood_block & BIT(idx));
++
++ vbp_cfg.egress_traffic_sub_meter_id[idx] =
++ enable ? cpu_to_le16(priv->drop_meter) : 0;
++ vbp_cfg.egress_sub_metering_enable[idx] = enable;
++ }
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET,
++ vbp_cfg);
++}
++
++/**
+ * mxl862xx_evlan_write_rule - Write a single Extended VLAN rule to hardware
+ * @priv: driver private data
+ * @block_id: HW Extended VLAN block ID
+@@ -947,6 +1242,7 @@ static int mxl862xx_configure_sp_tag_pro
+ * @vid: VLAN ID for VID-specific rules (ignored when !desc->match_vid)
+ * @untagged: strip tag on egress for EVLAN_STRIP_IF_UNTAGGED action
+ * @pvid: port VLAN ID for PVID insertion rules (0 = no PVID)
++ * @mgmt_vid: tag_8021q management VID for outer tag insertion (0 = unused)
+ *
+ * Translates a compact rule descriptor into a full firmware
+ * mxl862xx_extendedvlan_config struct and writes it via the API.
+@@ -954,7 +1250,8 @@ static int mxl862xx_configure_sp_tag_pro
+ static int mxl862xx_evlan_write_rule(struct mxl862xx_priv *priv,
+ u16 block_id, u16 entry_index,
+ const struct mxl862xx_evlan_rule_desc *desc,
+- u16 vid, bool untagged, u16 pvid)
++ u16 vid, bool untagged, u16 pvid,
++ u16 mgmt_vid)
+ {
+ struct mxl862xx_extendedvlan_config cfg = {};
+ struct mxl862xx_extendedvlan_filter_vlan *fv;
+@@ -1044,6 +1341,31 @@ static int mxl862xx_evlan_write_rule(str
+ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_DISCARD_UPSTREAM);
+ }
+ break;
++
++ case EVLAN_INSERT_OUTER:
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
++ cfg.treatment.add_outer_vlan = 1;
++ cfg.treatment.outer_vlan.vid_mode =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_VID_VAL);
++ cfg.treatment.outer_vlan.vid_val = cpu_to_le32(mgmt_vid);
++ cfg.treatment.outer_vlan.tpid =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_8021Q);
++ break;
++
++ case EVLAN_STRIP1:
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_REMOVE_1_TAG);
++ break;
++
++ case EVLAN_REASSIGN:
++ cfg.treatment.remove_tag =
++ cpu_to_le32(MXL862XX_EXTENDEDVLAN_TREATMENT_NOT_REMOVE_TAG);
++ cfg.treatment.reassign_bridge_port = 1;
++ cfg.treatment.new_bridge_port_id =
++ cpu_to_le16(desc->bridge_port_id);
++ break;
++
+ }
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_EXTENDEDVLAN_SET, cfg);
+@@ -1104,7 +1426,7 @@ static int mxl862xx_evlan_write_final_ru
+ for (i = 0; i < n_rules; i++) {
+ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
+ start_idx + i, &rules[i],
+- 0, false, pvid);
++ 0, false, pvid, 0);
+ if (ret)
+ return ret;
+ }
+@@ -1273,6 +1595,27 @@ static int mxl862xx_vf_del_vid(struct mx
+ }
+
+ /**
++ * mxl862xx_vf_clear_vids - Remove all VID entries without freeing the HW block
++ * @priv: driver private data
++ * @vf: VLAN Filter block
++ *
++ * Frees the software VID list and resets active_count, but keeps the
++ * HW block allocated to avoid firmware table fragmentation.
++ */
++static void mxl862xx_vf_clear_vids(struct mxl862xx_priv *priv,
++ struct mxl862xx_vf_block *vf)
++{
++ struct mxl862xx_vf_vid *ve, *tmp;
++
++ list_for_each_entry_safe(ve, tmp, &vf->vids, list) {
++ list_del(&ve->list);
++ kfree(ve);
++ }
++
++ vf->active_count = 0;
++}
++
++/**
+ * mxl862xx_evlan_program_ingress - Write the fixed ingress catchall rules
+ * @priv: driver private data
+ * @port: port number
+@@ -1323,8 +1666,8 @@ static int mxl862xx_evlan_program_egress
+ const struct mxl862xx_evlan_rule_desc *vid_rules;
+ struct mxl862xx_vf_vid *vfv;
+ u16 old_active = blk->n_active;
++ int n_vid, n_catch, ret;
+ u16 idx = 0, i;
+- int n_vid, ret;
+
+ if (p->vlan_filtering) {
+ vid_rules = vid_accept_standard;
+@@ -1341,13 +1684,23 @@ static int mxl862xx_evlan_program_egress
+ if (p->vlan_filtering && !vfv->untagged)
+ continue;
+
++ /* Skip the tag_8021q management VID -- it must NOT get
++ * per-VID egress rules. The management VID arrives as
++ * the outer tag on CPU->user frames and is stripped by
++ * the catchall rules appended below. A per-VID rule
++ * here would match first (NO_FILTER outer) and prevent
++ * the catchall from stripping the tag.
++ */
++ if (p->tag_8021q_vid && vfv->vid == p->tag_8021q_vid)
++ continue;
++
+ if (idx + n_vid > blk->block_size)
+ return -ENOSPC;
+
+ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
+ idx++, &vid_rules[0],
+ vfv->vid, vfv->untagged,
+- p->pvid);
++ p->pvid, 0);
+ if (ret)
+ return ret;
+
+@@ -1356,7 +1709,29 @@ static int mxl862xx_evlan_program_egress
+ idx++, &vid_rules[1],
+ vfv->vid,
+ vfv->untagged,
+- p->pvid);
++ p->pvid, 0);
++ if (ret)
++ return ret;
++ }
++ }
++
++ /* In tag_8021q mode, append catchall rules that strip the outer
++ * management VID tag from CPU->user frames. The management VID
++ * is kept through the forwarding pipeline (CPU ingress EVLAN
++ * only reassigns the bridge port, without stripping) and must
++ * be removed here before the frame exits the user port.
++ */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q) {
++ n_catch = ARRAY_SIZE(tag_8021q_egress_strip);
++
++ if (idx + n_catch > blk->block_size)
++ return -ENOSPC;
++
++ for (i = 0; i < n_catch; i++) {
++ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
++ idx++,
++ &tag_8021q_egress_strip[i],
++ 0, false, 0, 0);
+ if (ret)
+ return ret;
+ }
+@@ -1368,8 +1743,7 @@ static int mxl862xx_evlan_program_egress
+ */
+ for (i = idx; i < old_active; i++) {
+ ret = mxl862xx_evlan_deactivate_entry(priv,
+- blk->block_id,
+- i);
++ blk->block_id, i);
+ if (ret)
+ return ret;
+ }
+@@ -1393,13 +1767,16 @@ static int mxl862xx_port_vlan_filtering(
+
+ /* Reprogram Extended VLAN rules if filtering mode changed */
+ if (changed) {
+- /* When leaving VLAN-aware mode, release the ingress HW
+- * block. The firmware passes frames through unchanged
+- * when no ingress EVLAN block is assigned, so the block
+- * is unnecessary in unaware mode.
++ /* When leaving VLAN-aware mode, disable the ingress
++ * EVLAN engine. The block stays allocated to avoid
++ * firmware EVLAN table fragmentation.
+ */
+- if (!vlan_filtering)
++ if (!vlan_filtering) {
+ p->ingress_evlan.in_use = false;
++ ret = mxl862xx_set_bridge_port(ds, port);
++ if (ret)
++ return ret;
++ }
+
+ ret = mxl862xx_evlan_program_ingress(priv, port);
+ if (ret)
+@@ -1536,18 +1913,19 @@ static int mxl862xx_setup_cpu_bridge(str
+
+ /* include all assigned user ports in the CPU portmap */
+ bitmap_zero(p->portmap, MXL862XX_MAX_BRIDGE_PORTS);
+- dsa_switch_for_each_user_port(dp, ds) {
+- /* it's safe to rely on cpu_dp being valid for user ports */
+- if (dp->cpu_dp->index != port)
+- continue;
++ if (priv->tag_proto != DSA_TAG_PROTO_MXL862_8021Q) {
++ dsa_switch_for_each_user_port(dp, ds) {
++ /* it's safe to rely on cpu_dp being valid for user ports */
++ if (dp->cpu_dp->index != port)
++ continue;
+
+- __set_bit(dp->index, p->portmap);
++ __set_bit(dp->index, p->portmap);
++ }
+ }
+
+ return mxl862xx_set_bridge_port(ds, port);
+ }
+
+-
+ static int mxl862xx_port_bridge_join(struct dsa_switch *ds, int port,
+ const struct dsa_bridge bridge,
+ bool *tx_fwd_offload,
+@@ -1580,7 +1958,6 @@ static int mxl862xx_port_bridge_join(str
+ static void mxl862xx_port_bridge_leave(struct dsa_switch *ds, int port,
+ const struct dsa_bridge bridge)
+ {
+- struct dsa_port *dp = dsa_to_port(ds, port);
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ int err;
+@@ -1595,34 +1972,587 @@ static void mxl862xx_port_bridge_leave(s
+ * single-port bridge
+ */
+ bitmap_zero(p->portmap, MXL862XX_MAX_BRIDGE_PORTS);
+- __set_bit(dp->cpu_dp->index, p->portmap);
++ __set_bit(mxl862xx_cpu_bridge_port_id(ds, port), p->portmap);
+ p->flood_block = 0;
++ p->host_flood_block = 0;
+
+- /* Detach EVLAN and VF blocks from the bridge port BEFORE freeing
+- * them. The firmware tracks a usage count per block and rejects
+- * FREE while the count is non-zero.
++ /* Reset VLAN state for standalone mode. Ingress EVLAN is not
++ * needed outside a VLAN-aware bridge. Egress EVLAN is
++ * reprogrammed below -- in tag_8021q mode it gets the
++ * management VID strip catchalls, in SpTag mode it is cleared.
+ *
+- * For EVLAN: setting in_use=false makes set_bridge_port send
+- * enable=false, which decrements the firmware refcount.
+- *
+- * For VF: set_bridge_port sees dp->bridge == NULL (DSA already
+- * cleared it) and sends vlan_filter_enable=0, which decrements
+- * the firmware VF refcount.
++ * Do NOT clear the VF VID list here. Bridge VLANs are already
++ * removed by port_vlan_del during the switchdev replay in
++ * dsa_port_pre_bridge_leave. The remaining VIDs (e.g. the
++ * tag_8021q management VID) must survive bridge leave.
+ */
+ p->pvid = 0;
+ p->ingress_evlan.in_use = false;
+- p->egress_evlan.in_use = false;
+
++ err = mxl862xx_evlan_program_egress(priv, port);
++ if (err)
++ dev_err(ds->dev,
++ "failed to restore egress EVLAN on port %d: %pe\n",
++ port, ERR_PTR(err));
++
++ /* Push the complete standalone port state to firmware. The
++ * firmware compares old vs new EVLAN/VF enable flags and adjusts
++ * block refcounts accordingly, so a single call suffices.
++ */
+ err = mxl862xx_set_bridge_port(ds, port);
+ if (err)
+ dev_err(ds->dev,
+ "failed to update bridge port %d state: %pe\n", port,
+ ERR_PTR(err));
+
++ err = mxl862xx_set_cpu_vbp(ds, port);
++ if (err)
++ dev_err(ds->dev,
++ "failed to update CPU VBP for port %d: %pe\n", port,
++ ERR_PTR(err));
++
+ if (!dsa_bridge_ports(ds, bridge.dev))
+ mxl862xx_free_bridge(ds, &bridge);
+ }
+
++static int mxl862xx_setup_virtual_bridge_port(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_bridge_port_alloc bp_alloc = {};
++ struct mxl862xx_bridge_port_config bp_cfg = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *cpu_dp;
++ int ret;
++
++ cpu_dp = dsa_to_port(ds, port)->cpu_dp;
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_BRIDGEPORT_ALLOC, bp_alloc);
++ if (ret) {
++ dev_err(ds->dev,
++ "failed to allocate virtual bridge port for port %d: %pe\n",
++ port, ERR_PTR(ret));
++ return ret;
++ }
++
++ priv->ports[port].bridge_port_cpu = le16_to_cpu(bp_alloc.bridge_port_id);
++
++ bp_cfg.bridge_port_id = bp_alloc.bridge_port_id;
++ bp_cfg.mask = cpu_to_le32(
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING |
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING);
++ bp_cfg.bridge_id = cpu_to_le16(priv->ports[port].fid);
++ bp_cfg.src_mac_learning_disable = 1;
++ bp_cfg.dest_logical_port_id = cpu_dp->index;
++ mxl862xx_fw_portmap_set_bit(bp_cfg.bridge_port_map, port);
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, bp_cfg);
++ if (ret)
++ dev_err(ds->dev,
++ "failed to configure virtual bridge port %u for port %d: %pe\n",
++ priv->ports[port].bridge_port_cpu, port, ERR_PTR(ret));
++
++ return ret;
++}
++
++static void mxl862xx_free_virtual_bridge_port(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_bridge_port_alloc bp_alloc = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ int ret;
++
++ if (!priv->ports[port].bridge_port_cpu)
++ return;
++
++ mxl862xx_tag_8021q_disable_cpu_egress(ds, port);
++
++ bp_alloc.bridge_port_id = cpu_to_le16(priv->ports[port].bridge_port_cpu);
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_FREE, bp_alloc);
++ if (ret)
++ dev_err(ds->dev,
++ "failed to free virtual bridge port %u for port %d: %pe\n",
++ priv->ports[port].bridge_port_cpu, port, ERR_PTR(ret));
++ else
++ priv->ports[port].bridge_port_cpu = 0;
++}
++
++static int mxl862xx_setup_tag_8021q(struct dsa_switch *ds)
++{
++ struct dsa_port *dp;
++ int ret;
++
++ dsa_switch_for_each_user_port(dp, ds) {
++ ret = mxl862xx_setup_virtual_bridge_port(ds, dp->index);
++ if (ret)
++ return ret;
++ }
++
++ return 0;
++}
++
++static void mxl862xx_teardown_tag_8021q(struct dsa_switch *ds)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *dp;
++ int cpu;
++
++ dsa_switch_for_each_user_port(dp, ds) {
++ mxl862xx_free_virtual_bridge_port(ds, dp->index);
++ priv->ports[dp->index].tag_8021q_vid = 0;
++ }
++
++ /* Disable CPU port EVLAN engine and clear VF VID entries.
++ * The HW blocks stay allocated (freed in port_teardown).
++ */
++ dsa_switch_for_each_cpu_port(dp, ds) {
++ cpu = dp->index;
++
++ priv->ports[cpu].ingress_evlan.in_use = false;
++ mxl862xx_set_cpu_ctp_ingress_evlan(ds, cpu);
++ mxl862xx_vf_clear_vids(priv, &priv->ports[cpu].vf);
++ }
++
++}
++
++/**
++ * mxl862xx_tag_8021q_program_cpu_egress - Program virtual bridge port egress EVLAN
++ * @ds: DSA switch
++ * @port: user port whose virtual bridge port needs programming
++ *
++ * Programs the egress EVLAN block on the virtual bridge port associated
++ * with @port. The block is pre-allocated in port_setup. The rules insert the
++ * port's tag_8021q management VID as an outer 802.1Q tag on all
++ * frames exiting toward the CPU through this virtual bridge port.
++ */
++static int mxl862xx_tag_8021q_program_cpu_egress(struct dsa_switch *ds,
++ int port)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ struct mxl862xx_evlan_block *blk = &p->cpu_egress_evlan;
++ struct mxl862xx_bridge_port_config bp_cfg = {};
++ int n_rules = ARRAY_SIZE(cpu_egress_tag_8021q);
++ int i, ret;
++
++ if (!p->bridge_port_cpu || !p->tag_8021q_vid)
++ return 0;
++
++ for (i = 0; i < n_rules; i++) {
++ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
++ i, &cpu_egress_tag_8021q[i],
++ 0, false, 0,
++ p->tag_8021q_vid);
++ if (ret)
++ return ret;
++ }
++
++ blk->n_active = n_rules;
++ blk->in_use = true;
++
++ /* Enable egress EVLAN on the virtual bridge port */
++ bp_cfg.bridge_port_id = cpu_to_le16(p->bridge_port_cpu);
++ bp_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_VLAN);
++ bp_cfg.egress_extended_vlan_enable = 1;
++ bp_cfg.egress_extended_vlan_block_id = cpu_to_le16(blk->block_id);
++ bp_cfg.egress_extended_vlan_block_size = cpu_to_le16(n_rules);
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, bp_cfg);
++}
++
++/**
++ * mxl862xx_tag_8021q_cpu_vlan_program - Reprogram CPU port ingress EVLAN
++ * @ds: DSA switch
++ *
++ * Rebuilds the CPU port ingress EVLAN block with reassign rules for
++ * every tag_8021q VID currently in use. Called whenever a tag_8021q
++ * VID is added or removed.
++ *
++ * Each user port with a non-zero tag_8021q_vid gets 2 rules:
++ * - outer VID match + inner present: reassign to virtual bridge port
++ * - outer VID match + no inner: reassign to virtual bridge port
++ *
++ * The EVLAN block is assigned to the CPU port's CTP (not its bridge
++ * port) via CTP_PORTCONFIGSET, matching the reference and legacy
++ * driver architecture.
++ */
++static int mxl862xx_tag_8021q_cpu_vlan_program(struct dsa_switch *ds)
++{
++ struct mxl862xx_evlan_rule_desc rule;
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_evlan_block *blk;
++ struct dsa_port *cpu_dp, *dp;
++ struct mxl862xx_port *p;
++ u16 idx, old_active, vid;
++ int cpu, ret, i;
++
++ dsa_switch_for_each_cpu_port(cpu_dp, ds)
++ break;
++
++ cpu = cpu_dp->index;
++ blk = &priv->ports[cpu].ingress_evlan;
++
++ old_active = blk->n_active;
++ idx = 0;
++
++ dsa_switch_for_each_user_port(dp, ds) {
++ p = &priv->ports[dp->index];
++ vid = p->tag_8021q_vid;
++
++ if (!vid)
++ continue;
++
++ for (i = 0; i < ARRAY_SIZE(cpu_ingress_reassign); i++) {
++ rule = cpu_ingress_reassign[i];
++
++ rule.bridge_port_id = p->bridge_port_cpu;
++ ret = mxl862xx_evlan_write_rule(priv, blk->block_id,
++ idx++, &rule, vid,
++ false, 0, 0);
++ if (ret)
++ return ret;
++ }
++ }
++
++ blk->n_active = idx;
++
++ /* Deactivate stale entries beyond the new active range */
++ for (; idx < old_active; idx++) {
++ ret = mxl862xx_evlan_deactivate_entry(priv, blk->block_id,
++ idx);
++ if (ret)
++ return ret;
++ }
++ blk->in_use = blk->n_active > 0;
++
++ return mxl862xx_set_cpu_ctp_ingress_evlan(ds, cpu);
++}
++
++static int mxl862xx_tag_8021q_cpu_vlan_add(struct dsa_switch *ds, int port,
++ u16 vid)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ int ret;
++
++ /* Add VID to CPU port's VF block so firmware accepts frames
++ * tagged with this VID on CPU port ingress.
++ */
++ ret = mxl862xx_vf_add_vid(priv, &priv->ports[port].vf, vid, false);
++ if (ret)
++ return ret;
++
++ return mxl862xx_tag_8021q_cpu_vlan_program(ds);
++}
++
++static int mxl862xx_tag_8021q_cpu_vlan_del(struct dsa_switch *ds, int port,
++ u16 vid)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ int ret;
++
++ ret = mxl862xx_vf_del_vid(priv, &priv->ports[port].vf, vid);
++ if (ret)
++ return ret;
++
++ return mxl862xx_tag_8021q_cpu_vlan_program(ds);
++}
++
++static int mxl862xx_tag_8021q_vlan_add(struct dsa_switch *ds, int port,
++ u16 vid, u16 flags)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ int ret;
++
++ if (dsa_is_cpu_port(ds, port))
++ return mxl862xx_tag_8021q_cpu_vlan_add(ds, port, vid);
++
++ /* User port: store the tag_8021q VID and add to VF block */
++ priv->ports[port].tag_8021q_vid = vid;
++
++ ret = mxl862xx_vf_add_vid(priv, &priv->ports[port].vf, vid, false);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_tag_8021q_program_cpu_egress(ds, port);
++ if (ret)
++ return ret;
++
++ /* Rebuild CPU ingress EVLAN to include this port's management VID.
++ * The DSA framework may call the CPU port's tag_8021q_vlan_add
++ * before this user port's callback (ports iterate in index order),
++ * so the CPU ingress EVLAN rebuild triggered by the CPU callback
++ * might have run before tag_8021q_vid was set. Rebuild now to
++ * ensure this port's reassignment rule is present.
++ */
++ return mxl862xx_tag_8021q_cpu_vlan_program(ds);
++}
++
++static int mxl862xx_tag_8021q_vlan_del(struct dsa_switch *ds, int port,
++ u16 vid)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++
++ if (dsa_is_cpu_port(ds, port))
++ return mxl862xx_tag_8021q_cpu_vlan_del(ds, port, vid);
++
++ if (priv->ports[port].tag_8021q_vid == vid) {
++ priv->ports[port].tag_8021q_vid = 0;
++ mxl862xx_tag_8021q_disable_cpu_egress(ds, port);
++ }
++
++ return mxl862xx_vf_del_vid(priv, &priv->ports[port].vf, vid);
++}
++
++/**
++ * mxl862xx_refresh_cpu_targets - Update portmaps and traps for new CPU target
++ * @ds: DSA switch
++ *
++ * After switching between SpTag and tag_8021q, the CPU-side target in
++ * each user port's portmap changes (physical CPU port vs. virtual
++ * bridge port). This rebuilds every user port's portmap with the
++ * correct CPU target and reinstalls the link-local PCE trap.
++ */
++static int mxl862xx_refresh_cpu_targets(struct dsa_switch *ds)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *dp, *member_dp;
++ struct mxl862xx_port *p;
++ int ret, port;
++
++ dsa_switch_for_each_user_port(dp, ds) {
++ port = dp->index;
++ p = &priv->ports[port];
++
++ bitmap_zero(p->portmap, MXL862XX_MAX_BRIDGE_PORTS);
++ if (dp->bridge) {
++
++ dsa_switch_for_each_bridge_member(member_dp, ds, dp->bridge->dev) {
++ if (member_dp->index != port)
++ __set_bit(member_dp->index, p->portmap);
++ }
++ }
++ __set_bit(mxl862xx_cpu_bridge_port_id(ds, port), p->portmap);
++
++ /* Reprogram user port egress EVLAN to add or remove the
++ * tag_8021q management VID strip catchalls.
++ */
++ ret = mxl862xx_evlan_program_egress(priv, port);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_set_bridge_port(ds, port);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_setup_link_local_trap(ds, port);
++ if (ret)
++ return ret;
++ }
++
++ return 0;
++}
++
++/**
++ * mxl862xx_complete_tag_8021q_setup - Finish deferred tag_8021q initialization
++ * @ds: DSA switch
++ *
++ * Called from change_tag_protocol() to configure the firmware for
++ * tag_8021q mode. Requires each user port to already have an FID
++ * (from add_single_port_bridge in setup()). Reconfigures CPU ports,
++ * allocates virtual bridge ports and enables flooding on standalone
++ * bridges. Link-local traps are refreshed separately after
++ * dsa_tag_8021q_register() has set cpu_egress_evlan.in_use.
++ */
++static int mxl862xx_complete_tag_8021q_setup(struct dsa_switch *ds)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *dp;
++ int ret, port;
++
++ /* Disable SpTag and reduce to a single CTP on CPU ports for
++ * 8021q mode. Without a special tag the PMAC cannot select a
++ * sub-CTP, so only CTP 0 must exist.
++ */
++ dsa_switch_for_each_cpu_port(dp, ds) {
++ ret = mxl862xx_configure_sp_tag_proto(ds, dp->index, false);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_configure_ctp_port(ds, dp->index,
++ dp->index, 1);
++ if (ret)
++ return ret;
++
++ ret = mxl862xx_setup_cpu_bridge(ds, dp->index);
++ if (ret)
++ return ret;
++ }
++
++ ret = mxl862xx_setup_tag_8021q(ds);
++ if (ret)
++ return ret;
++
++ /* In tag_8021q mode TX goes through the bridge engine (CTP
++ * ingress EVLAN reassigns to a virtual bridge port), so
++ * unknown unicast and multicast must be flooded at the bridge
++ * level for frames from the CPU to reach user ports. The
++ * per-port bridges may have been created with flooding
++ * disabled (SpTag mode default), so update them now.
++ *
++ * Block unknown UC and MC on the VBP egress meters so frames
++ * to unknown destinations are not flooded to the host. DSA
++ * core will selectively enable host flooding via
++ * port_set_host_flood when needed (e.g. promisc mode).
++ */
++ dsa_switch_for_each_user_port(dp, ds) {
++ port = dp->index;
++
++ if (dp->bridge)
++ continue;
++
++ ret = mxl862xx_bridge_config_fwd(ds,
++ priv->ports[port].fid,
++ true, true, true);
++ if (ret)
++ return ret;
++
++ priv->ports[port].host_flood_block =
++ BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC) |
++ BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP) |
++ BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP);
++
++ ret = mxl862xx_set_cpu_vbp(ds, port);
++ if (ret)
++ return ret;
++ }
++
++ return 0;
++}
++
++static int mxl862xx_change_tag_protocol(struct dsa_switch *ds,
++ enum dsa_tag_protocol proto)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ enum dsa_tag_protocol old_proto = priv->tag_proto;
++ struct dsa_port *dp;
++ int ret, port;
++
++ /* Flush all MAC entries on tag protocol change. Host entries
++ * installed via portmap (tag_8021q VBP-based) vs single port_id
++ * (SpTag) are not compatible across modes.
++ */
++ if (ds->setup)
++ mxl862xx_api_wrap(priv, MXL862XX_MAC_TABLECLEAR,
++ NULL, 0, false, false);
++
++ /* Set tag_proto early so that helpers called below (e.g.
++ * mxl862xx_setup_cpu_bridge) see the target protocol.
++ * Restored on failure.
++ */
++ priv->tag_proto = proto;
++
++ switch (proto) {
++ case DSA_TAG_PROTO_MXL862:
++ if (ds->tag_8021q_ctx) {
++ dsa_tag_8021q_unregister(ds);
++ mxl862xx_teardown_tag_8021q(ds);
++
++ /* Virtual bridge ports are gone; revert portmaps
++ * and traps to target the physical CPU port.
++ */
++ ret = mxl862xx_refresh_cpu_targets(ds);
++ if (ret)
++ goto err_restore;
++
++ /* Revert standalone bridges to SpTag mode
++ * defaults: discard unknown UC/MC (SpTag TX
++ * bypasses bridge engine) while keeping
++ * broadcast flooding.
++ */
++ dsa_switch_for_each_user_port(dp, ds) {
++ port = dp->index;
++
++ if (dp->bridge)
++ continue;
++
++ mxl862xx_bridge_config_fwd(ds,
++ priv->ports[port].fid,
++ false, false, true);
++ }
++ }
++ dsa_switch_for_each_cpu_port(dp, ds) {
++ ret = mxl862xx_configure_sp_tag_proto(ds, dp->index,
++ true);
++ if (ret)
++ goto err_restore;
++
++ /* Restore multiple CTPs so the special tag's
++ * sub_if_id can select per-port sub-CTPs.
++ */
++ ret = mxl862xx_configure_ctp_port(ds, dp->index,
++ dp->index,
++ 32 - dp->index);
++ if (ret)
++ goto err_restore;
++
++ /* Restore CPU portmap: SpTag mode needs all user
++ * ports in the CPU's bridge_port_map. tag_8021q
++ * mode clears it to prevent FID 0 flooding.
++ */
++ ret = mxl862xx_setup_cpu_bridge(ds, dp->index);
++ if (ret)
++ goto err_restore;
++ }
++ break;
++
++ case DSA_TAG_PROTO_MXL862_8021Q:
++ ret = mxl862xx_complete_tag_8021q_setup(ds);
++ if (ret)
++ goto err_restore;
++
++ /* RTNL is held by the DSA core when calling
++ * change_tag_protocol(), both during initial setup
++ * and at runtime.
++ */
++ ret = dsa_tag_8021q_register(ds, htons(ETH_P_8021Q));
++ if (ret) {
++ mxl862xx_teardown_tag_8021q(ds);
++ goto err_restore;
++ }
++
++ /* Refresh link-local traps now that tag_8021q_vlan_add
++ * callbacks have set cpu_egress_evlan.in_use, so the
++ * PCE rules get the correct EVLAN treatment.
++ */
++ ret = mxl862xx_refresh_cpu_targets(ds);
++ if (ret) {
++ dsa_tag_8021q_unregister(ds);
++ mxl862xx_teardown_tag_8021q(ds);
++ goto err_restore;
++ }
++ break;
++
++ default:
++ ret = -EPROTONOSUPPORT;
++ goto err_restore;
++ }
++
++ return 0;
++
++err_restore:
++ priv->tag_proto = old_proto;
++ return ret;
++}
++
++static void mxl862xx_teardown(struct dsa_switch *ds)
++{
++ /* tag_8021q teardown is handled in mxl862xx_remove() under
++ * RTNL, before dsa_unregister_switch() takes dsa2_mutex.
++ * dsa_tag_8021q_unregister() needs RTNL for vlan_vid_del(),
++ * and acquiring RTNL inside teardown() (which runs under
++ * dsa2_mutex) would invert the RTNL -> dsa2_mutex lock order.
++ */
++}
++
+ static int mxl862xx_port_setup(struct dsa_switch *ds, int port)
+ {
+ struct mxl862xx_priv *priv = ds->priv;
+@@ -1642,55 +2572,30 @@ static int mxl862xx_port_setup(struct ds
+ dsa_port_is_dsa(dp))
+ return 0;
+
+- /* configure tag protocol */
+- ret = mxl862xx_configure_sp_tag_proto(ds, port, is_cpu_port);
++ /* configure tag protocol: SpTag for native, disable for 8021q */
++ ret = mxl862xx_configure_sp_tag_proto(ds, port,
++ is_cpu_port &&
++ priv->tag_proto == DSA_TAG_PROTO_MXL862);
+ if (ret)
+ return ret;
+
+ /* assign CTP port IDs */
+ ret = mxl862xx_configure_ctp_port(ds, port, port,
+- is_cpu_port ? 32 - port : 1);
++ (is_cpu_port &&
++ priv->tag_proto == DSA_TAG_PROTO_MXL862) ?
++ 32 - port : 1);
+ if (ret)
+ return ret;
+
+ if (is_cpu_port)
+- /* assign user ports to CPU port bridge */
+ return mxl862xx_setup_cpu_bridge(ds, port);
+
+- /* setup single-port bridge for user ports */
+- ret = mxl862xx_add_single_port_bridge(ds, port);
+- if (ret)
+- return ret;
+-
+ /* install link-local trap for this user port */
+ ret = mxl862xx_setup_link_local_trap(ds, port);
+ if (ret)
+ return ret;
+
+- /* Initialize and pre-allocate per-port EVLAN and VF blocks for
+- * user ports. CPU ports do not use EVLAN or VF -- frames pass
+- * through without processing. Pre-allocation avoids firmware
+- * EVLAN table fragmentation and simplifies control flow.
+- */
+- mxl862xx_evlan_block_init(&priv->ports[port].ingress_evlan,
+- priv->evlan_ingress_size);
+- ret = mxl862xx_evlan_block_alloc(priv, &priv->ports[port].ingress_evlan);
+- if (ret)
+- return ret;
+-
+- mxl862xx_evlan_block_init(&priv->ports[port].egress_evlan,
+- priv->evlan_egress_size);
+- ret = mxl862xx_evlan_block_alloc(priv, &priv->ports[port].egress_evlan);
+- if (ret)
+- return ret;
+-
+- mxl862xx_vf_init(&priv->ports[port].vf, priv->vf_block_size);
+- ret = mxl862xx_vf_alloc(priv, &priv->ports[port].vf);
+- if (ret)
+- return ret;
+-
+ priv->ports[port].setup_done = true;
+-
+ return 0;
+ }
+
+@@ -1712,7 +2617,7 @@ static void mxl862xx_port_teardown(struc
+ priv->ports[port].setup_done = false;
+ }
+
+-static int mxl862xx_get_fid(struct dsa_switch *ds, struct dsa_db db)
++static int mxl862xx_get_fid(struct dsa_switch *ds, const struct dsa_db db)
+ {
+ struct mxl862xx_priv *priv = ds->priv;
+
+@@ -1730,23 +2635,244 @@ static int mxl862xx_get_fid(struct dsa_s
+ }
+ }
+
+-static int mxl862xx_port_fdb_add(struct dsa_switch *ds, int port,
+- const unsigned char *addr, u16 vid, struct dsa_db db)
++/**
++ * mxl862xx_fdb_bridge_port - Translate port for MAC table in tag_8021q mode
++ * @ds: DSA switch
++ * @port: port number passed by DSA (usually the CPU port for host entries)
++ * @db: database context identifying the user port or bridge
++ *
++ * In tag_8021q mode, host FDB/MDB entries for standalone ports must use
++ * the virtual bridge port (bridge_port_cpu) as the MAC table destination
++ * so that known-unicast and known-multicast frames exit through the
++ * virtual bridge port's egress EVLAN, which inserts the management VID.
++ * Without this, the firmware forwards known traffic directly to the
++ * physical CPU bridge port, bypassing management VID insertion, and DSA
++ * drops the untagged frame.
++ */
++static int mxl862xx_fdb_bridge_port(struct dsa_switch *ds, int port,
++ const struct dsa_db db)
+ {
+- struct mxl862xx_mac_table_add param = {};
+- int fid = mxl862xx_get_fid(ds, db), ret;
+ struct mxl862xx_priv *priv = ds->priv;
++ u16 bp_cpu;
+
+- if (fid < 0)
+- return fid;
++ if (dsa_is_cpu_port(ds, port) && priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q &&
++ db.type == DSA_DB_PORT) {
++ bp_cpu = priv->ports[db.dp->index].bridge_port_cpu;
++
++ if (bp_cpu)
++ return bp_cpu;
++ }
++
++ return port;
++}
++
++/**
++ * mxl862xx_fdb_add_per_fid - Install a unicast FDB entry in one FID
++ */
++static int mxl862xx_fdb_add_per_fid(struct dsa_switch *ds,
++ const unsigned char *addr, u16 vid,
++ u16 fid, int port_id)
++{
++ struct mxl862xx_mac_table_add param = {};
++ struct mxl862xx_priv *priv = ds->priv;
+
+- param.port_id = cpu_to_le32(port);
++ param.port_id = cpu_to_le32(port_id);
+ param.static_entry = true;
+ param.fid = cpu_to_le16(fid);
+ param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
+ ether_addr_copy(param.mac, addr);
+
+- ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, param);
++ return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, param);
++}
++
++/**
++ * mxl862xx_fdb_del_per_fid - Remove a unicast FDB entry from one FID
++ */
++static int mxl862xx_fdb_del_per_fid(struct dsa_switch *ds,
++ const unsigned char *addr, u16 vid,
++ u16 fid)
++{
++ struct mxl862xx_mac_table_remove param = {};
++ struct mxl862xx_priv *priv = ds->priv;
++
++ param.fid = cpu_to_le16(fid);
++ param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
++ ether_addr_copy(param.mac, addr);
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, param);
++}
++
++/**
++ * mxl862xx_mac_portmap_add - Set port bits in a MAC table entry's portmap
++ * @priv: driver private data
++ * @addr: MAC address
++ * @fid: firmware FID
++ * @vid: VLAN ID
++ * @add_map: firmware-format portmap of bits to set
++ *
++ * Queries the existing MAC table entry by {addr, fid, vid}. If found,
++ * the existing portmap is preserved and @add_map bits are OR'd in.
++ * The entry is then written back as a static portmap-mode entry.
++ */
++static int mxl862xx_mac_portmap_add(struct mxl862xx_priv *priv,
++ const unsigned char *addr,
++ u16 fid, u16 vid,
++ const __le16 *add_map)
++{
++ struct mxl862xx_mac_table_query qparam = {};
++ struct mxl862xx_mac_table_add aparam = {};
++ int i, ret;
++
++ ether_addr_copy(qparam.mac, addr);
++ qparam.fid = cpu_to_le16(fid);
++ qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
++ if (ret)
++ return ret;
++
++ ether_addr_copy(aparam.mac, addr);
++ aparam.fid = cpu_to_le16(fid);
++ aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
++ aparam.static_entry = true;
++ aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG);
++
++ if (qparam.found)
++ memcpy(aparam.port_map, qparam.port_map,
++ sizeof(aparam.port_map));
++
++ for (i = 0; i < ARRAY_SIZE(aparam.port_map); i++)
++ aparam.port_map[i] |= add_map[i];
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam);
++}
++
++/**
++ * mxl862xx_mac_portmap_del - Clear port bits from a MAC table entry's portmap
++ * @priv: driver private data
++ * @addr: MAC address
++ * @fid: firmware FID
++ * @vid: VLAN ID
++ * @del_map: firmware-format portmap of bits to clear
++ *
++ * Queries the existing MAC table entry. If not found, returns 0.
++ * Clears all @del_map bits from the portmap. If the portmap becomes
++ * empty, the entry is removed entirely; otherwise it is updated.
++ */
++static int mxl862xx_mac_portmap_del(struct mxl862xx_priv *priv,
++ const unsigned char *addr,
++ u16 fid, u16 vid,
++ const __le16 *del_map)
++{
++ struct mxl862xx_mac_table_remove rparam = {};
++ struct mxl862xx_mac_table_query qparam = {};
++ struct mxl862xx_mac_table_add aparam = {};
++ int i, ret;
++
++ ether_addr_copy(qparam.mac, addr);
++ qparam.fid = cpu_to_le16(fid);
++ qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
++ if (ret)
++ return ret;
++
++ if (!qparam.found)
++ return 0;
++
++ for (i = 0; i < ARRAY_SIZE(qparam.port_map); i++)
++ qparam.port_map[i] &= ~del_map[i];
++
++ if (mxl862xx_fw_portmap_is_empty(qparam.port_map)) {
++ ether_addr_copy(rparam.mac, addr);
++ rparam.fid = cpu_to_le16(fid);
++ rparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID,
++ vid));
++ return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE,
++ rparam);
++ }
++
++ ether_addr_copy(aparam.mac, addr);
++ aparam.fid = cpu_to_le16(fid);
++ aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
++ aparam.static_entry = true;
++ aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG);
++ memcpy(aparam.port_map, qparam.port_map, sizeof(aparam.port_map));
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam);
++}
++
++/**
++ * mxl862xx_mac_add_host_bridge - Install a host FDB/MDB entry with VBP portmap
++ * @ds: DSA switch
++ * @addr: MAC address
++ * @vid: VLAN ID
++ * @bridge: bridge whose members' VBPs to include
++ *
++ * In tag_8021q mode, host FDB/MDB entries in a shared bridge FID must use
++ * portmap mode targeting ALL bridge members' virtual bridge ports (VBPs).
++ * The firmware ANDs the entry's portmap with each ingress port's
++ * bridge_port_map, which contains only that port's own VBP. This
++ * selects the correct VBP per ingress port, ensuring frames exit
++ * through the right egress EVLAN (which inserts the per-port management
++ * VID that identifies the source port to DSA on the CPU side).
++ */
++static int mxl862xx_mac_add_host_bridge(struct dsa_switch *ds,
++ const unsigned char *addr, u16 vid,
++ const struct dsa_bridge *bridge)
++{
++ __le16 add_map[MXL862XX_FW_PORTMAP_WORDS] = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ u16 fid = priv->bridges[bridge->num];
++ struct dsa_port *member_dp;
++
++ dsa_switch_for_each_bridge_member(member_dp, ds, bridge->dev)
++ mxl862xx_fw_portmap_set_bit(add_map,
++ priv->ports[member_dp->index].bridge_port_cpu);
++
++ return mxl862xx_mac_portmap_add(priv, addr, fid, vid, add_map);
++}
++
++static int mxl862xx_port_fdb_add(struct dsa_switch *ds, int port,
++ const unsigned char *addr, u16 vid, const struct dsa_db db)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *target_dp;
++ int fid, ret;
++
++ /* tag_8021q host FDB for bridged ports: portmap with all VBPs */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q && dsa_is_cpu_port(ds, port) &&
++ db.type == DSA_DB_BRIDGE) {
++ if (!priv->bridges[db.bridge.num])
++ return -ENOENT;
++
++ return mxl862xx_mac_add_host_bridge(ds, addr, vid, &db.bridge);
++ }
++
++ /* tag_8021q standalone host FDB for bridged ports: also mirror
++ * into the bridge FID. DSA installs VID-specific host entries
++ * via the standalone path (DSA_DB_PORT), but with IVL enabled
++ * the firmware needs matching entries in the bridge FID for
++ * VID-keyed lookups to succeed.
++ */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q && dsa_is_cpu_port(ds, port) &&
++ db.type == DSA_DB_PORT && vid > 0) {
++ target_dp = dsa_to_port(ds, db.dp->index);
++
++ if (target_dp->bridge) {
++ ret = mxl862xx_mac_add_host_bridge(ds, addr, vid,
++ target_dp->bridge);
++ if (ret)
++ return ret;
++ }
++ }
++
++ fid = mxl862xx_get_fid(ds, db);
++ if (fid < 0)
++ return fid;
++
++ ret = mxl862xx_fdb_add_per_fid(ds, addr, vid, fid,
++ mxl862xx_fdb_bridge_port(ds, port, db));
+ if (ret)
+ dev_err(ds->dev, "failed to add FDB entry on port %d\n", port);
+
+@@ -1756,18 +2882,25 @@ static int mxl862xx_port_fdb_add(struct
+ static int mxl862xx_port_fdb_del(struct dsa_switch *ds, int port,
+ const unsigned char *addr, u16 vid, const struct dsa_db db)
+ {
+- struct mxl862xx_mac_table_remove param = {};
+- int fid = mxl862xx_get_fid(ds, db), ret;
+ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *target_dp;
++ int fid, ret;
+
++ /* Mirror of the standalone->bridge FID path in fdb_add */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q && dsa_is_cpu_port(ds, port) &&
++ db.type == DSA_DB_PORT && vid > 0) {
++ target_dp = dsa_to_port(ds, db.dp->index);
++
++ if (target_dp->bridge && priv->bridges[target_dp->bridge->num])
++ mxl862xx_fdb_del_per_fid(ds, addr, vid,
++ priv->bridges[target_dp->bridge->num]);
++ }
++
++ fid = mxl862xx_get_fid(ds, db);
+ if (fid < 0)
+ return fid;
+
+- param.fid = cpu_to_le16(fid);
+- param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid));
+- ether_addr_copy(param.mac, addr);
+-
+- ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, param);
++ ret = mxl862xx_fdb_del_per_fid(ds, addr, vid, fid);
+ if (ret)
+ dev_err(ds->dev, "failed to remove FDB entry on port %d\n", port);
+
+@@ -1806,88 +2939,147 @@ static int mxl862xx_port_fdb_dump(struct
+ return 0;
+ }
+
++/**
++ * mxl862xx_mdb_add_to_fid - Add a port bit to an MDB entry in one FID
++ * @ds: DSA switch
++ * @mdb: multicast group address and VID
++ * @fid: firmware FID to operate on
++ * @port_bit: port index to set in the portmap
++ * @vid: VLAN ID for the MAC table entry
++ */
++static int mxl862xx_mdb_add_to_fid(struct dsa_switch *ds,
++ const struct switchdev_obj_port_mdb *mdb,
++ u16 fid, int port_bit, u16 vid)
++{
++ __le16 add_map[MXL862XX_FW_PORTMAP_WORDS] = {};
++
++ mxl862xx_fw_portmap_set_bit(add_map, port_bit);
++
++ return mxl862xx_mac_portmap_add(ds->priv, mdb->addr, fid, vid,
++ add_map);
++}
++
++/**
++ * mxl862xx_mdb_del_from_fid - Remove a port bit from an MDB entry in one FID
++ * @ds: DSA switch
++ * @mdb: multicast group address
++ * @fid: firmware FID to operate on
++ * @port_bit: port index to clear from the portmap
++ * @vid: VLAN ID for the MAC table entry (0 for SVL/tag_8021q mode)
++ */
++static int mxl862xx_mdb_del_from_fid(struct dsa_switch *ds,
++ const struct switchdev_obj_port_mdb *mdb,
++ u16 fid, int port_bit, u16 vid)
++{
++ __le16 del_map[MXL862XX_FW_PORTMAP_WORDS] = {};
++
++ mxl862xx_fw_portmap_set_bit(del_map, port_bit);
++
++ return mxl862xx_mac_portmap_del(ds->priv, mdb->addr, fid, vid,
++ del_map);
++}
++
+ static int mxl862xx_port_mdb_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ const struct dsa_db db)
+ {
+- struct mxl862xx_mac_table_query qparam = {};
+- struct mxl862xx_mac_table_add aparam = {};
+ struct mxl862xx_priv *priv = ds->priv;
+ int fid, ret;
+
++ /* tag_8021q host MDB for bridged ports: portmap with all VBPs */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q && dsa_is_cpu_port(ds, port) &&
++ db.type == DSA_DB_BRIDGE) {
++ if (!priv->bridges[db.bridge.num])
++ return -ENOENT;
++
++ return mxl862xx_mac_add_host_bridge(ds, mdb->addr,
++ mdb->vid, &db.bridge);
++ }
++
+ fid = mxl862xx_get_fid(ds, db);
+ if (fid < 0)
+ return fid;
+
+- /* Look up existing entry by {MAC, FID, TCI} */
+- ether_addr_copy(qparam.mac, mdb->addr);
+- qparam.fid = cpu_to_le16(fid);
+- qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+-
+- ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
++ ret = mxl862xx_mdb_add_to_fid(ds, mdb, fid,
++ mxl862xx_fdb_bridge_port(ds, port, db),
++ mdb->vid);
+ if (ret)
+ return ret;
+
+- /* Build the ADD command using portmap mode */
+- ether_addr_copy(aparam.mac, mdb->addr);
+- aparam.fid = cpu_to_le16(fid);
+- aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+- aparam.static_entry = true;
+- aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG);
++ /* In tag_8021q mode, standalone host MDB entries need both the VBP
++ * and the physical port in the portmap. The TX path goes through
++ * the bridge engine (CPU -> VBP -> MAC lookup), so source-port
++ * filtering would remove the sole VBP entry, dropping the frame.
++ * With both bits set:
++ * TX: VBP source-filtered -> physical port remains -> frame exits
++ * RX: physical port source-filtered -> VBP remains -> CPU receives
++ */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q && db.type == DSA_DB_PORT)
++ ret = mxl862xx_mdb_add_to_fid(ds, mdb, fid, db.dp->index,
++ mdb->vid);
+
+- /* Merge with existing portmap if entry already exists */
+- if (qparam.found)
+- memcpy(aparam.port_map, qparam.port_map,
+- sizeof(aparam.port_map));
++ return ret;
++}
+
+- mxl862xx_fw_portmap_set_bit(aparam.port_map, port);
++/**
++ * mxl862xx_mac_del_host_bridge - Remove VBP bits from a host FDB/MDB entry
++ * @ds: DSA switch
++ * @addr: MAC address
++ * @vid: VLAN ID
++ * @bridge: bridge whose members' VBPs to clear
++ *
++ * Clears all bridge member VBP bits from the portmap. If the portmap
++ * becomes empty (no user-port bits remain), removes the entry entirely.
++ */
++static int mxl862xx_mac_del_host_bridge(struct dsa_switch *ds,
++ const unsigned char *addr, u16 vid,
++ const struct dsa_bridge *bridge)
++{
++ __le16 del_map[MXL862XX_FW_PORTMAP_WORDS] = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ u16 fid = priv->bridges[bridge->num];
++ struct dsa_port *member_dp;
+
+- return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam);
++ dsa_switch_for_each_bridge_member(member_dp, ds, bridge->dev)
++ mxl862xx_fw_portmap_set_bit(del_map,
++ priv->ports[member_dp->index].bridge_port_cpu);
++
++ return mxl862xx_mac_portmap_del(priv, addr, fid, vid, del_map);
+ }
+
+ static int mxl862xx_port_mdb_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_mdb *mdb,
+ const struct dsa_db db)
+ {
+- struct mxl862xx_mac_table_remove rparam = {};
+- struct mxl862xx_mac_table_query qparam = {};
+- struct mxl862xx_mac_table_add aparam = {};
+- int fid = mxl862xx_get_fid(ds, db), ret;
+ struct mxl862xx_priv *priv = ds->priv;
++ int fid, ret;
++
++ /* tag_8021q host MDB for bridged ports: clear all VBP bits */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q && dsa_is_cpu_port(ds, port) &&
++ db.type == DSA_DB_BRIDGE) {
++ if (!priv->bridges[db.bridge.num])
++ return -ENOENT;
++
++ return mxl862xx_mac_del_host_bridge(ds, mdb->addr,
++ mdb->vid, &db.bridge);
++ }
+
++ fid = mxl862xx_get_fid(ds, db);
+ if (fid < 0)
+ return fid;
+
+- /* Look up existing entry */
+- qparam.fid = cpu_to_le16(fid);
+- qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+- ether_addr_copy(qparam.mac, mdb->addr);
+-
+- ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam);
++ ret = mxl862xx_mdb_del_from_fid(ds, mdb, fid,
++ mxl862xx_fdb_bridge_port(ds, port, db),
++ mdb->vid);
+ if (ret)
+ return ret;
+
+- if (!qparam.found)
+- return 0;
+-
+- mxl862xx_fw_portmap_clear_bit(qparam.port_map, port);
+-
+- if (mxl862xx_fw_portmap_is_empty(qparam.port_map)) {
+- /* No ports left -- remove the entry entirely */
+- rparam.fid = cpu_to_le16(fid);
+- rparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+- ether_addr_copy(rparam.mac, mdb->addr);
+- ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, rparam);
+- } else {
+- /* Write back with reduced portmap */
+- aparam.fid = cpu_to_le16(fid);
+- aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid));
+- ether_addr_copy(aparam.mac, mdb->addr);
+- aparam.static_entry = true;
+- aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG);
+- memcpy(aparam.port_map, qparam.port_map, sizeof(aparam.port_map));
+- ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam);
+- }
++ /* In tag_8021q mode, standalone host MDB entries have both the VBP
++ * and the physical port in the portmap -- remove both bits.
++ */
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q && db.type == DSA_DB_PORT)
++ ret = mxl862xx_mdb_del_from_fid(ds, mdb, fid, db.dp->index,
++ mdb->vid);
+
+ return ret;
+ }
+@@ -1975,7 +3167,9 @@ static void mxl862xx_host_flood_work_fn(
+ struct mxl862xx_priv *priv = p->priv;
+ struct dsa_switch *ds = priv->ds;
+ int port = p - priv->ports;
++ unsigned long block;
+ bool uc, mc;
++ int ret;
+
+ rtnl_lock();
+
+@@ -1988,14 +3182,31 @@ static void mxl862xx_host_flood_work_fn(
+ uc = p->host_flood_uc;
+ mc = p->host_flood_mc;
+
+- /* The hardware controls unknown-unicast/multicast forwarding per FID
+- * (bridge), not per source port. For bridged ports all members share
+- * one FID, so we cannot selectively suppress flooding to the CPU for
+- * one source port while allowing it for another. Silently ignore the
+- * request -- the excess flooding towards the CPU is harmless.
+- */
+- if (!dsa_port_bridge_dev_get(dsa_to_port(ds, port)))
+- mxl862xx_bridge_config_fwd(ds, p->fid, uc, mc, true);
++ if (priv->tag_proto == DSA_TAG_PROTO_MXL862_8021Q) {
++ block = 0;
++
++ if (!uc)
++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC);
++ if (!mc) {
++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP);
++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP);
++ }
++
++ if (block != p->host_flood_block) {
++ p->host_flood_block = block;
++ ret = mxl862xx_set_cpu_vbp(ds, port);
++ if (ret)
++ dev_err(ds->dev,
++ "failed to set host flood on port %d: %pe\n",
++ port, ERR_PTR(ret));
++ }
++ } else {
++ /* SpTag mode: per-FID forwarding, only works for
++ * standalone ports (each has its own FID).
++ */
++ if (!dsa_port_bridge_dev_get(dsa_to_port(ds, port)))
++ mxl862xx_bridge_config_fwd(ds, p->fid, uc, mc, true);
++ }
+
+ rtnl_unlock();
+ }
+@@ -2330,7 +3541,9 @@ static void mxl862xx_get_stats64(struct
+
+ static const struct dsa_switch_ops mxl862xx_switch_ops = {
+ .get_tag_protocol = mxl862xx_get_tag_protocol,
++ .change_tag_protocol = mxl862xx_change_tag_protocol,
+ .setup = mxl862xx_setup,
++ .teardown = mxl862xx_teardown,
+ .port_setup = mxl862xx_port_setup,
+ .port_teardown = mxl862xx_port_teardown,
+ .phylink_get_caps = mxl862xx_phylink_get_caps,
+@@ -2352,6 +3565,8 @@ static const struct dsa_switch_ops mxl86
+ .port_vlan_filtering = mxl862xx_port_vlan_filtering,
+ .port_vlan_add = mxl862xx_port_vlan_add,
+ .port_vlan_del = mxl862xx_port_vlan_del,
++ .tag_8021q_vlan_add = mxl862xx_tag_8021q_vlan_add,
++ .tag_8021q_vlan_del = mxl862xx_tag_8021q_vlan_del,
+ .get_strings = mxl862xx_get_strings,
+ .get_sset_count = mxl862xx_get_sset_count,
+ .get_ethtool_stats = mxl862xx_get_ethtool_stats,
+@@ -2399,6 +3614,8 @@ static int mxl862xx_probe(struct mdio_de
+
+ INIT_DELAYED_WORK(&priv->stats_work, mxl862xx_stats_work_fn);
+
++ priv->tag_proto = DSA_TAG_PROTO_MXL862;
++
+ dev_set_drvdata(dev, ds);
+
+ return dsa_register_switch(ds);
+@@ -2415,16 +3632,29 @@ static void mxl862xx_remove(struct mdio_
+
+ priv = ds->priv;
+
++ /* Tear down tag_8021q under RTNL before dsa_unregister_switch().
++ * dsa_tag_8021q_unregister() calls vlan_vid_del() which needs
++ * RTNL. dsa_unregister_switch() takes dsa2_mutex, and other
++ * paths take RTNL -> dsa2_mutex, so RTNL must be acquired
++ * before dsa2_mutex to avoid lock inversion.
++ */
++ if (ds->tag_8021q_ctx) {
++ rtnl_lock();
++ dsa_tag_8021q_unregister(ds);
++ mxl862xx_teardown_tag_8021q(ds);
++ rtnl_unlock();
++ }
++
+ dsa_unregister_switch(ds);
+
+ cancel_delayed_work_sync(&priv->stats_work);
+
+ mxl862xx_host_shutdown(priv);
+
+- /* Cancel any pending host flood work. dsa_unregister_switch()
++ /* Cancel any pending host flood work. dsa_unregister_switch()
+ * has already called port_teardown (which sets setup_done=false),
+ * but a worker could still be blocked on rtnl_lock(). Since we
+- * are now outside RTNL, cancel_work_sync() will not deadlock.
++ * are now outside RTNL, cancel_work_sync() won't deadlock.
+ */
+ for (i = 0; i < MXL862XX_MAX_PORTS; i++)
+ cancel_work_sync(&priv->ports[i].host_flood_work);
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -8,8 +8,6 @@
+ #include <linux/workqueue.h>
+ #include <net/dsa.h>
+
+-struct mxl862xx_priv;
+-
+ #define MXL862XX_MAX_PORTS 17
+ #define MXL862XX_DEFAULT_BRIDGE 0
+ #define MXL862XX_MAX_BRIDGES 48
+@@ -20,6 +18,8 @@ struct mxl862xx_priv;
+ /* Number of __le16 words in a firmware portmap (128-bit bitmap). */
+ #define MXL862XX_FW_PORTMAP_WORDS (MXL862XX_MAX_BRIDGE_PORTS / 16)
+
++struct mxl862xx_priv;
++
+ /**
+ * mxl862xx_fw_portmap_from_bitmap - convert a kernel bitmap to a firmware
+ * portmap (__le16[8])
+@@ -210,6 +210,9 @@ struct mxl862xx_port_stats {
+ * @vf: per-port VLAN Filter block state
+ * @ingress_evlan: ingress extended VLAN block state
+ * @egress_evlan: egress extended VLAN block state
++ * @bridge_port_cpu: virtual bridge port ID for tag_8021q CPU-side CTP
++ * @host_flood_block: bitmask of firmware meter indices used to block
++ * host flooding on the virtual bridge port (tag_8021q)
+ * @host_flood_uc: desired host unicast flood state (true = flood);
+ * updated atomically by port_set_host_flood, consumed
+ * by the deferred host_flood_work
+@@ -224,6 +227,7 @@ struct mxl862xx_port_stats {
+ * periodically by the stats polling work
+ * @stats_lock: protects accumulator reads in .get_stats64 against
+ * concurrent updates from the polling work
++ * @tag_8021q_vid: currently assigned tag_8021q management VID
+ */
+ struct mxl862xx_port {
+ struct mxl862xx_priv *priv;
+@@ -238,9 +242,14 @@ struct mxl862xx_port {
+ struct mxl862xx_vf_block vf;
+ struct mxl862xx_evlan_block ingress_evlan;
+ struct mxl862xx_evlan_block egress_evlan;
++ /* tag_8021q state */
++ u16 bridge_port_cpu;
++ unsigned long host_flood_block;
+ bool host_flood_uc;
+ bool host_flood_mc;
+ struct work_struct host_flood_work;
++ u16 tag_8021q_vid;
++ struct mxl862xx_evlan_block cpu_egress_evlan;
+ /* Hardware stats accumulation */
+ struct mxl862xx_port_stats stats;
+ spinlock_t stats_lock;
+@@ -302,6 +311,7 @@ union mxl862xx_fw_version {
+ * errors
+ * @crc_err: set atomically before CRC-triggered shutdown, cleared
+ * after
++ * @tag_proto: active DSA tag protocol (native or 8021q)
+ * @drop_meter: index of the single shared zero-rate firmware meter
+ * used to unconditionally drop traffic (used to block
+ * flooding)
+@@ -310,12 +320,13 @@ union mxl862xx_fw_version {
+ * @serdes_ports: SerDes interfaces incl. sub-interfaces in case of
+ * 10G_QXGMII
+ * @ports: per-port state, indexed by switch port number
++ * @evlan_ingress_size: per-port ingress Extended VLAN block size
++ * @evlan_egress_size: per-port egress Extended VLAN block size
++ * @cpu_evlan_ingress_size: CPU port ingress EVLAN block size (tag_8021q)
+ * @bridges: maps DSA bridge number to firmware bridge ID;
+ * zero means no firmware bridge allocated for that
+ * DSA bridge number. Indexed by dsa_bridge.num
+ * (0 .. ds->max_num_bridges).
+- * @evlan_ingress_size: per-port ingress Extended VLAN block size
+- * @evlan_egress_size: per-port egress Extended VLAN block size
+ * @vf_block_size: per-port VLAN Filter block size
+ * @stats_work: periodic work item that polls RMON hardware counters
+ * and accumulates them into 64-bit per-port stats
+@@ -325,6 +336,7 @@ struct mxl862xx_priv {
+ struct mdio_device *mdiodev;
+ struct work_struct crc_err_work;
+ unsigned long crc_err;
++ enum dsa_tag_protocol tag_proto;
+ u16 drop_meter;
+ union mxl862xx_fw_version fw_version;
+ struct mxl862xx_pcs serdes_ports[8];
+@@ -332,6 +344,7 @@ struct mxl862xx_priv {
+ u16 bridges[MXL862XX_MAX_BRIDGES + 1];
+ u16 evlan_ingress_size;
+ u16 evlan_egress_size;
++ u16 cpu_evlan_ingress_size;
+ u16 vf_block_size;
+ struct delayed_work stats_work;
+ };
+--- a/include/net/dsa.h
++++ b/include/net/dsa.h
+@@ -56,6 +56,8 @@ struct tc_action;
+ #define DSA_TAG_PROTO_VSC73XX_8021Q_VALUE 28
+ #define DSA_TAG_PROTO_BRCM_LEGACY_FCS_VALUE 29
+ #define DSA_TAG_PROTO_MXL862_VALUE 30
++#define DSA_TAG_PROTO_MXL862_8021Q_VALUE 31
++
+
+ enum dsa_tag_protocol {
+ DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
+@@ -89,6 +91,7 @@ enum dsa_tag_protocol {
+ DSA_TAG_PROTO_LAN937X = DSA_TAG_PROTO_LAN937X_VALUE,
+ DSA_TAG_PROTO_VSC73XX_8021Q = DSA_TAG_PROTO_VSC73XX_8021Q_VALUE,
+ DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
++ DSA_TAG_PROTO_MXL862_8021Q = DSA_TAG_PROTO_MXL862_8021Q_VALUE,
+ };
+
+ struct dsa_switch;
+--- a/net/dsa/Kconfig
++++ b/net/dsa/Kconfig
+@@ -111,6 +111,13 @@ config NET_DSA_TAG_MXL_862XX
+ MaxLinear MxL86252 and MxL86282 switches using their native 8-byte
+ tagging protocol.
+
++config NET_DSA_TAG_MXL_862XX_8021Q
++ tristate "Tag driver for MaxLinear MxL862xx switches, using VLAN"
++ help
++ Say Y or M if you want to enable support for tagging frames for the
++ MaxLinear MxL86252 and MxL86282 switches using 802.1Q VLAN-based
++ tagging instead of their native 8-byte tagging protocol.
++
+ config NET_DSA_TAG_KSZ
+ tristate "Tag driver for Microchip 8795/937x/9477/9893 families of switches"
+ help
+--- a/net/dsa/Makefile
++++ b/net/dsa/Makefile
+@@ -29,6 +29,7 @@ obj-$(CONFIG_NET_DSA_TAG_KSZ) += tag_ksz
+ obj-$(CONFIG_NET_DSA_TAG_LAN9303) += tag_lan9303.o
+ obj-$(CONFIG_NET_DSA_TAG_MTK) += tag_mtk.o
+ obj-$(CONFIG_NET_DSA_TAG_MXL_862XX) += tag_mxl862xx.o
++obj-$(CONFIG_NET_DSA_TAG_MXL_862XX_8021Q) += tag_mxl862xx_8021q.o
+ obj-$(CONFIG_NET_DSA_TAG_NONE) += tag_none.o
+ obj-$(CONFIG_NET_DSA_TAG_OCELOT) += tag_ocelot.o
+ obj-$(CONFIG_NET_DSA_TAG_OCELOT_8021Q) += tag_ocelot_8021q.o
+--- /dev/null
++++ b/net/dsa/tag_mxl862xx_8021q.c
+@@ -0,0 +1,59 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * DSA 802.1Q-based tag driver for MaxLinear MxL862xx switches
++ *
++ * Uses the DSA tag_8021q framework to encode port information in
++ * 802.1Q VLAN tags instead of the native 8-byte MxL862xx special tag.
++ *
++ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
++ */
++
++#include <linux/dsa/8021q.h>
++
++#include "tag.h"
++#include "tag_8021q.h"
++
++#define MXL862_8021Q_NAME "mxl862xx-8021q"
++
++static struct sk_buff *mxl862_8021q_xmit(struct sk_buff *skb,
++ struct net_device *netdev)
++{
++ struct dsa_port *dp = dsa_user_to_port(netdev);
++ u16 tx_vid = dsa_tag_8021q_standalone_vid(dp);
++ u16 queue_mapping = skb_get_queue_mapping(skb);
++ u8 pcp = netdev_txq_to_tc(netdev, queue_mapping);
++
++ return dsa_8021q_xmit(skb, netdev, ETH_P_8021Q,
++ (pcp << VLAN_PRIO_SHIFT) | tx_vid);
++}
++
++static struct sk_buff *mxl862_8021q_rcv(struct sk_buff *skb,
++ struct net_device *netdev)
++{
++ int src_port = -1, switch_id = -1;
++
++ dsa_8021q_rcv(skb, &src_port, &switch_id, NULL, NULL);
++
++ skb->dev = dsa_conduit_find_user(netdev, switch_id, src_port);
++ if (!skb->dev)
++ return NULL;
++
++ dsa_default_offload_fwd_mark(skb);
++
++ return skb;
++}
++
++static const struct dsa_device_ops mxl862_8021q_netdev_ops = {
++ .name = MXL862_8021Q_NAME,
++ .proto = DSA_TAG_PROTO_MXL862_8021Q,
++ .xmit = mxl862_8021q_xmit,
++ .rcv = mxl862_8021q_rcv,
++ .needed_headroom = VLAN_HLEN,
++ .promisc_on_conduit = true,
++};
++
++MODULE_DESCRIPTION("DSA tag driver for MaxLinear MxL862xx switches, using VLAN");
++MODULE_LICENSE("GPL");
++MODULE_ALIAS_DSA_TAG_DRIVER(DSA_TAG_PROTO_MXL862_8021Q, MXL862_8021Q_NAME);
++
++module_dsa_tag_driver(mxl862_8021q_netdev_ops);
--- /dev/null
+From 31359e8b7673e656d0591a9eb5014b45911383ae Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 03:44:41 +0000
+Subject: [PATCH 26/35] net: dsa: mxl862xx: add link aggregation support
+
+Implement LAG offloading via the firmware's trunking engine. A
+dedicated firmware bridge port is allocated per LAG and remains
+stable for the LAG's lifetime. All member CTPs redirect to it,
+and FDB/MDB entries target it, so no entry migration is needed
+when the LAG master (lowest-numbered member port) changes.
+
+The firmware provides three cooperating mechanisms:
+
+ - PCE_TRUNK_CONF register: global 6-bit hash field selection
+ (SA, DA, SIP, DIP, sport, dport)
+ - CTP redirection: all member CTPs point bridge_port_id to the
+ LAG's dedicated bridge port
+ - P-mapper on the LAG bridge port: 64 hash-indexed entries
+ (indices 9..72) filled round-robin with active member ports
+
+Hash and active-backup bond modes are supported. The global hash
+register is widened monotonically on LAG join and recomputed from
+stored per-port requirements on LAG leave.
+
+The LAG master's full bridge port configuration (bridge_id, EVLAN,
+VLAN filter, learning, portmap, flood metering) is pushed to the
+LAG bridge port via __mxl862xx_set_bridge_port() whenever it
+changes. Bridge portmaps use the LAG bridge port ID instead of
+individual member port indices, ensuring correct cross-LAG
+forwarding.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 22 +
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 4 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 587 +++++++++++++++++++++++-
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 34 ++
+ 4 files changed, 630 insertions(+), 17 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -564,6 +564,28 @@ struct mxl862xx_pmapper {
+ } __packed;
+
+ /**
++ * struct mxl862xx_trunking_cfg - LAG hash algorithm configuration
++ * @ip_src: Include source IP address in trunk hash (1 = include)
++ * @ip_dst: Include destination IP address in trunk hash
++ * @mac_src: Include source MAC address in trunk hash
++ * @mac_dst: Include destination MAC address in trunk hash
++ * @src_port: Include TCP/UDP source port in trunk hash
++ * @dst_port: Include TCP/UDP destination port in trunk hash
++ *
++ * The firmware inverts the boolean sense when writing the hardware
++ * register (PCE_TRUNK_CONF): bit=0 means include, bit=1 means exclude.
++ * This struct uses the logical sense (1 = include).
++ */
++struct mxl862xx_trunking_cfg {
++ u8 ip_src;
++ u8 ip_dst;
++ u8 mac_src;
++ u8 mac_dst;
++ u8 src_port;
++ u8 dst_port;
++} __packed;
++
++/**
+ * struct mxl862xx_bridge_port_config - Bridge Port Configuration
+ * @bridge_port_id: Bridge Port ID allocated by bridge port allocation
+ * @mask: See &enum mxl862xx_bridge_port_config_mask
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -51,6 +51,7 @@
+
+ #define MXL862XX_QOS_METERCFGSET (MXL862XX_QOS_MAGIC + 0x2)
+ #define MXL862XX_QOS_METERALLOC (MXL862XX_QOS_MAGIC + 0x2a)
++#define MXL862XX_QOS_PMAPPERTABLESET (MXL862XX_QOS_MAGIC + 0x2e)
+
+ #define MXL862XX_RMON_PORT_GET (MXL862XX_RMON_MAGIC + 0x1)
+
+@@ -73,6 +74,9 @@
+
+ #define MXL862XX_SS_SPTAG_SET (MXL862XX_SS_MAGIC + 0x2)
+
++#define MXL862XX_TRUNKING_MAGIC 0xe00
++#define MXL862XX_TRUNKING_CFGSET (MXL862XX_TRUNKING_MAGIC + 0x2)
++
+ #define MXL862XX_STP_PORTCFGSET (MXL862XX_STP_MAGIC + 0x2)
+
+ #define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x1)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -620,7 +620,42 @@ static int mxl862xx_setup_link_local_tra
+ rule);
+ }
+
+-static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port)
++static bool mxl862xx_is_lag_master(const struct mxl862xx_priv *priv, int port)
++{
++ struct dsa_lag *lag = priv->ports[port].lag;
++ int i;
++
++ if (!lag)
++ return true;
++
++ for (i = 0; i < port; i++) {
++ if (priv->ports[i].lag == lag)
++ return false;
++ }
++
++ return true;
++}
++
++/**
++ * mxl862xx_lag_bridge_port - Get the effective bridge port ID for a port
++ * @priv: driver private data
++ * @port: port index
++ *
++ * If @port is a member of a LAG, returns the LAG's dedicated firmware
++ * bridge port ID. Otherwise returns @port itself.
++ */
++static u16 mxl862xx_lag_bridge_port(const struct mxl862xx_priv *priv, int port)
++{
++ struct dsa_lag *lag = priv->ports[port].lag;
++
++ if (lag && priv->lag_bridge_ports[lag->id])
++ return priv->lag_bridge_ports[lag->id];
++
++ return port;
++}
++
++static int __mxl862xx_set_bridge_port(struct dsa_switch *ds, int port,
++ u16 bp_id)
+ {
+ struct mxl862xx_bridge_port_config br_port_cfg = {};
+ struct dsa_port *dp = dsa_to_port(ds, port);
+@@ -632,7 +667,7 @@ static int mxl862xx_set_bridge_port(stru
+ bool enable;
+ int i, idx;
+
+- br_port_cfg.bridge_port_id = cpu_to_le16(port);
++ br_port_cfg.bridge_port_id = cpu_to_le16(bp_id);
+ br_port_cfg.bridge_id = cpu_to_le16(bridge_id);
+ br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
+@@ -715,12 +750,38 @@ static int mxl862xx_set_bridge_port(stru
+ br_port_cfg);
+ }
+
++static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ u16 lag_bp;
++ int ret;
++
++ ret = __mxl862xx_set_bridge_port(ds, port, port);
++ if (ret)
++ return ret;
++
++ /* If this port is a LAG master, also push its config to the
++ * LAG's dedicated bridge port (which is the actual target of
++ * all member CTP redirections).
++ */
++ if (p->lag && mxl862xx_is_lag_master(priv, port)) {
++ lag_bp = priv->lag_bridge_ports[p->lag->id];
++ if (lag_bp)
++ ret = __mxl862xx_set_bridge_port(ds, port, lag_bp);
++ }
++
++ return ret;
++}
++
+ static int mxl862xx_sync_bridge_members(struct dsa_switch *ds,
+ const struct dsa_bridge *bridge)
+ {
+ struct mxl862xx_priv *priv = ds->priv;
+ struct dsa_port *dp, *member_dp;
+- int port, err, ret = 0;
++ struct mxl862xx_port *p;
++ int port, member, err, ret = 0;
++ u16 lag_bp, bp;
+
+ dsa_switch_for_each_bridge_member(dp, ds, bridge->dev) {
+ port = dp->index;
+@@ -729,9 +790,21 @@ static int mxl862xx_sync_bridge_members(
+ MXL862XX_MAX_BRIDGE_PORTS);
+
+ dsa_switch_for_each_bridge_member(member_dp, ds, bridge->dev) {
+- if (member_dp->index != port)
+- __set_bit(member_dp->index,
+- priv->ports[port].portmap);
++ member = member_dp->index;
++
++ /* For LAG members, only include the LAG's
++ * dedicated bridge port in the portmap.
++ * Non-master members are skipped to avoid
++ * duplicates (they share the same LAG bridge
++ * port).
++ */
++ if (!mxl862xx_is_lag_master(priv, member))
++ continue;
++ if (member != port) {
++ bp = mxl862xx_lag_bridge_port(priv,
++ member);
++ __set_bit(bp, priv->ports[port].portmap);
++ }
+ }
+ __set_bit(mxl862xx_cpu_bridge_port_id(ds, port),
+ priv->ports[port].portmap);
+@@ -741,6 +814,25 @@ static int mxl862xx_sync_bridge_members(
+ ret = err;
+ }
+
++ /* Push updated portmaps to LAG bridge ports. Each LAG master's
++ * portmap (which excludes itself) is used for the LAG bridge
++ * port -- this naturally avoids self-forwarding.
++ */
++ dsa_switch_for_each_bridge_member(dp, ds, bridge->dev) {
++ p = &priv->ports[dp->index];
++
++ if (!p->lag || !mxl862xx_is_lag_master(priv, dp->index))
++ continue;
++
++ lag_bp = priv->lag_bridge_ports[p->lag->id];
++ if (!lag_bp)
++ continue;
++
++ err = __mxl862xx_set_bridge_port(ds, dp->index, lag_bp);
++ if (err)
++ ret = err;
++ }
++
+ return ret;
+ }
+
+@@ -1926,6 +2018,408 @@ static int mxl862xx_setup_cpu_bridge(str
+ return mxl862xx_set_bridge_port(ds, port);
+ }
+
++/**
++ * mxl862xx_lag_master_port - Find the LAG master (lowest-numbered member)
++ * @ds: DSA switch
++ * @lag: LAG to search
++ *
++ * The master's bridge port hosts the P-mapper and receives all ingress
++ * traffic via CTP redirection from other members.
++ *
++ * Return: port index of the master, or -ENOENT if no members.
++ */
++static int mxl862xx_lag_master_port(struct dsa_switch *ds,
++ const struct dsa_lag *lag)
++{
++ struct dsa_port *dp;
++ int master = -ENOENT;
++
++ dsa_lag_foreach_port(dp, ds->dst, lag) {
++ if (dp->ds != ds)
++ continue;
++ if (master < 0 || dp->index < master)
++ master = dp->index;
++ }
++
++ return master;
++}
++
++/**
++ * mxl862xx_lag_hash_bits - Translate Linux hash mode to firmware hash bitmask
++ * @info: bonding upper info (tx_type + hash_type)
++ *
++ * Return: 6-bit hash field bitmask (MXL862XX_TRUNK_HASH_*), or negative
++ * errno if the mode is unsupported.
++ */
++static int mxl862xx_lag_hash_bits(const struct netdev_lag_upper_info *info)
++{
++ if (info->tx_type != NETDEV_LAG_TX_TYPE_HASH)
++ return 0;
++
++ switch (info->hash_type) {
++ case NETDEV_LAG_HASH_L2:
++ return MXL862XX_TRUNK_HASH_SA | MXL862XX_TRUNK_HASH_DA;
++ case NETDEV_LAG_HASH_L34:
++ return MXL862XX_TRUNK_HASH_SIP | MXL862XX_TRUNK_HASH_DIP |
++ MXL862XX_TRUNK_HASH_SPORT | MXL862XX_TRUNK_HASH_DPORT;
++ case NETDEV_LAG_HASH_L23:
++ case NETDEV_LAG_HASH_E23:
++ return MXL862XX_TRUNK_HASH_SA | MXL862XX_TRUNK_HASH_DA |
++ MXL862XX_TRUNK_HASH_SIP | MXL862XX_TRUNK_HASH_DIP;
++ case NETDEV_LAG_HASH_E34:
++ return MXL862XX_TRUNK_HASH_SA | MXL862XX_TRUNK_HASH_DA |
++ MXL862XX_TRUNK_HASH_SIP | MXL862XX_TRUNK_HASH_DIP |
++ MXL862XX_TRUNK_HASH_SPORT | MXL862XX_TRUNK_HASH_DPORT;
++ default:
++ return -EOPNOTSUPP;
++ }
++}
++
++/**
++ * mxl862xx_lag_set_hash - Push trunk hash configuration to firmware
++ * @priv: driver private data
++ * @hash_bits: 6-bit hash field bitmask (MXL862XX_TRUNK_HASH_*)
++ *
++ * Only issues a firmware command when @hash_bits differs from the
++ * currently active configuration.
++ */
++static int mxl862xx_lag_set_hash(struct mxl862xx_priv *priv, u8 hash_bits)
++{
++ struct mxl862xx_trunking_cfg cfg = {};
++
++ if (priv->trunk_hash == hash_bits)
++ return 0;
++
++ cfg.mac_src = !!(hash_bits & MXL862XX_TRUNK_HASH_SA);
++ cfg.mac_dst = !!(hash_bits & MXL862XX_TRUNK_HASH_DA);
++ cfg.ip_src = !!(hash_bits & MXL862XX_TRUNK_HASH_SIP);
++ cfg.ip_dst = !!(hash_bits & MXL862XX_TRUNK_HASH_DIP);
++ cfg.src_port = !!(hash_bits & MXL862XX_TRUNK_HASH_SPORT);
++ cfg.dst_port = !!(hash_bits & MXL862XX_TRUNK_HASH_DPORT);
++
++ priv->trunk_hash = hash_bits;
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_TRUNKING_CFGSET, cfg);
++}
++
++/**
++ * mxl862xx_lag_recompute_hash - Recompute global hash from all active LAGs
++ * @ds: DSA switch
++ *
++ * Scans all ports and ORs together the stored hash requirements of every
++ * active LAG member. Used after a LAG is destroyed to potentially narrow
++ * the global hash configuration.
++ *
++ * Return: union of all active LAGs' hash field bitmasks.
++ */
++static u8 mxl862xx_lag_recompute_hash(struct dsa_switch *ds)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ u8 hash = 0;
++ int port;
++
++ for (port = 0; port < ds->num_ports; port++) {
++ if (priv->ports[port].lag)
++ hash |= priv->ports[port].lag_hash_bits;
++ }
++
++ return hash;
++}
++
++/**
++ * mxl862xx_lag_build_pmapper - Fill P-mapper with round-robin LAG distribution
++ * @ds: DSA switch
++ * @lag: LAG group
++ * @pm: P-mapper struct to fill (entries 9..72)
++ *
++ * Only ports with lag_tx_enabled are included. Falls back to the
++ * master port if no members are active.
++ */
++static void mxl862xx_lag_build_pmapper(struct dsa_switch *ds,
++ const struct dsa_lag *lag,
++ struct mxl862xx_pmapper *pm)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ int active_ports[MXL862XX_MAX_PORTS];
++ int n_active = 0, master, port;
++ struct dsa_port *dp;
++ int i;
++
++ dsa_lag_foreach_port(dp, ds->dst, lag) {
++ if (dp->ds != ds)
++ continue;
++ if (priv->ports[dp->index].lag_tx_enabled)
++ active_ports[n_active++] = dp->index;
++ }
++
++ /* Fallback: if no members are active, use the master port */
++ if (!n_active) {
++ master = mxl862xx_lag_master_port(ds, lag);
++
++ if (master >= 0) {
++ active_ports[0] = master;
++ n_active = 1;
++ }
++ }
++
++ if (!n_active)
++ return;
++
++ for (i = 0; i < MXL862XX_PMAPPER_LAG_COUNT; i++) {
++ port = active_ports[i % n_active];
++
++ pm->dest_sub_if_id_group[MXL862XX_PMAPPER_LAG_FIRST + i] =
++ (port << 4) & 0xff;
++ }
++}
++
++/**
++ * mxl862xx_lag_redirect_ctp - Redirect a port's CTP to the LAG master
++ * @priv: driver private data
++ * @port: port whose CTP to redirect
++ * @master_port: LAG master port index
++ */
++static int mxl862xx_lag_redirect_ctp(struct mxl862xx_priv *priv,
++ int port, int master_port)
++{
++ struct mxl862xx_ctp_port_config ctp = {};
++
++ ctp.logical_port_id = port;
++ ctp.mask = cpu_to_le32(MXL862XX_CTP_PORT_CONFIG_MASK_BRIDGE_PORT_ID);
++ ctp.bridge_port_id = cpu_to_le16(master_port);
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
++}
++
++/**
++ * mxl862xx_lag_restore_ctp - Restore a port's CTP to point to itself
++ * @priv: driver private data
++ * @port: port whose CTP to restore
++ */
++static int mxl862xx_lag_restore_ctp(struct mxl862xx_priv *priv, int port)
++{
++ return mxl862xx_lag_redirect_ctp(priv, port, port);
++}
++
++/**
++ * mxl862xx_lag_disable_pmapper - Disable P-mapper on a bridge port
++ * @ds: DSA switch
++ * @bp_id: firmware bridge port ID to reconfigure
++ */
++static int mxl862xx_lag_disable_pmapper(struct dsa_switch *ds, u16 bp_id)
++{
++ struct mxl862xx_bridge_port_config bp_cfg = {};
++ struct mxl862xx_priv *priv = ds->priv;
++
++ bp_cfg.bridge_port_id = cpu_to_le16(bp_id);
++ bp_cfg.mask = cpu_to_le32(
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING);
++ bp_cfg.dest_logical_port_id = bp_id;
++ bp_cfg.pmapper_enable = 0;
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, bp_cfg);
++}
++
++/**
++ * mxl862xx_lag_sync - Synchronize LAG hardware state for a LAG group
++ * @ds: DSA switch
++ * @lag: LAG group to synchronize
++ *
++ * Finds the master (lowest-numbered member), redirects all member CTPs
++ * to the LAG's dedicated firmware bridge port, configures the P-mapper
++ * for hash distribution, and pushes the master's full bridge port
++ * configuration (EVLAN, VF, portmap, learning) to the LAG bridge port.
++ */
++static int mxl862xx_lag_sync(struct dsa_switch *ds, const struct dsa_lag *lag)
++{
++ struct mxl862xx_bridge_port_config bp_cfg = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_pmapper pm = {};
++ struct dsa_port *dp;
++ int master, ret;
++ u16 lag_bp;
++
++ lag_bp = priv->lag_bridge_ports[lag->id];
++ if (!lag_bp)
++ return -ENOENT;
++
++ master = mxl862xx_lag_master_port(ds, lag);
++ if (master < 0)
++ return master;
++
++ /* Redirect all member CTPs to the LAG bridge port */
++ dsa_lag_foreach_port(dp, ds->dst, lag) {
++ if (dp->ds != ds)
++ continue;
++ ret = mxl862xx_lag_redirect_ctp(priv, dp->index, lag_bp);
++ if (ret)
++ return ret;
++ }
++
++ /* Push the master's full config to the LAG bridge port so it
++ * inherits the current bridge_id, EVLAN/VF blocks, portmap,
++ * learning and flood settings.
++ */
++ ret = __mxl862xx_set_bridge_port(ds, master, lag_bp);
++ if (ret)
++ return ret;
++
++ /* Build P-mapper with active members */
++ mxl862xx_lag_build_pmapper(ds, lag, &pm);
++
++ /* Enable P-mapper in LAG mode on the LAG bridge port */
++ bp_cfg.bridge_port_id = cpu_to_le16(lag_bp);
++ bp_cfg.mask = cpu_to_le32(
++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING);
++ bp_cfg.dest_logical_port_id = master;
++ bp_cfg.pmapper_enable = 1;
++ bp_cfg.pmapper_mapping_mode =
++ cpu_to_le32(MXL862XX_PMAPPER_MAPPING_LAG);
++ bp_cfg.pmapper = pm;
++
++ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, bp_cfg);
++}
++
++static int mxl862xx_port_lag_join(struct dsa_switch *ds, int port,
++ const struct dsa_lag lag,
++ struct netdev_lag_upper_info *info,
++ struct netlink_ext_ack *extack)
++{
++ struct mxl862xx_bridge_port_alloc bp_alloc = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *dp = dsa_to_port(ds, port);
++ int hash_bits;
++ u8 new_hash;
++ int ret;
++
++ if (dsa_is_cpu_port(ds, port)) {
++ NL_SET_ERR_MSG_MOD(extack, "CPU port LAG not supported");
++ return -EOPNOTSUPP;
++ }
++
++ if (info->tx_type != NETDEV_LAG_TX_TYPE_HASH &&
++ info->tx_type != NETDEV_LAG_TX_TYPE_ACTIVEBACKUP) {
++ NL_SET_ERR_MSG_MOD(extack, "Only hash and active-backup LAG modes supported");
++ return -EOPNOTSUPP;
++ }
++
++ hash_bits = mxl862xx_lag_hash_bits(info);
++ if (hash_bits < 0) {
++ NL_SET_ERR_MSG_MOD(extack, "Unsupported LAG hash mode");
++ return hash_bits;
++ }
++
++ /* Allocate a dedicated firmware bridge port for this LAG on
++ * first member join. This bridge port is stable for the
++ * LAG's lifetime -- all CTP redirections, FDB and MDB entries
++ * target it, so no migration is needed on membership changes.
++ */
++ if (!priv->lag_bridge_ports[lag.id]) {
++ ret = MXL862XX_API_READ(priv, MXL862XX_BRIDGEPORT_ALLOC,
++ bp_alloc);
++ if (ret) {
++ NL_SET_ERR_MSG_MOD(extack,
++ "Failed to allocate LAG bridge port");
++ return ret;
++ }
++ priv->lag_bridge_ports[lag.id] =
++ le16_to_cpu(bp_alloc.bridge_port_id);
++ }
++
++ priv->ports[port].lag = dp->lag;
++ priv->ports[port].lag_tx_enabled = dp->lag_tx_enabled;
++ priv->ports[port].lag_hash_bits = hash_bits;
++
++ /* Widen global hash to include this LAG's requirements */
++ new_hash = priv->trunk_hash | hash_bits;
++ ret = mxl862xx_lag_set_hash(priv, new_hash);
++ if (ret)
++ goto err_undo;
++
++ ret = mxl862xx_lag_sync(ds, dp->lag);
++ if (ret)
++ goto err_undo;
++
++ return 0;
++
++err_undo:
++ priv->ports[port].lag = NULL;
++ priv->ports[port].lag_tx_enabled = false;
++ priv->ports[port].lag_hash_bits = 0;
++ return ret;
++}
++
++static int mxl862xx_port_lag_leave(struct dsa_switch *ds, int port,
++ const struct dsa_lag lag)
++{
++ struct mxl862xx_bridge_port_alloc bp_alloc = {};
++ struct mxl862xx_priv *priv = ds->priv;
++ u8 new_hash;
++ int ret;
++
++ /* Restore this port's CTP to point to itself */
++ ret = mxl862xx_lag_restore_ctp(priv, port);
++ if (ret)
++ dev_err(ds->dev, "failed to restore CTP for port %d: %pe\n",
++ port, ERR_PTR(ret));
++
++ priv->ports[port].lag = NULL;
++ priv->ports[port].lag_tx_enabled = false;
++ priv->ports[port].lag_hash_bits = 0;
++
++ /* If other members remain, re-sync the LAG */
++ if (mxl862xx_lag_master_port(ds, &lag) >= 0) {
++ ret = mxl862xx_lag_sync(ds, &lag);
++ if (ret)
++ dev_err(ds->dev,
++ "failed to re-sync LAG after port %d left: %pe\n",
++ port, ERR_PTR(ret));
++ } else if (priv->lag_bridge_ports[lag.id]) {
++ /* Last member left -- disable P-mapper and free the
++ * LAG's dedicated bridge port.
++ */
++ ret = mxl862xx_lag_disable_pmapper(ds,
++ priv->lag_bridge_ports[lag.id]);
++ if (ret)
++ dev_err(ds->dev,
++ "failed to disable P-mapper on LAG bridge port %u: %pe\n",
++ priv->lag_bridge_ports[lag.id], ERR_PTR(ret));
++
++ bp_alloc.bridge_port_id =
++ cpu_to_le16(priv->lag_bridge_ports[lag.id]);
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_FREE,
++ bp_alloc);
++ if (ret)
++ dev_err(ds->dev,
++ "failed to free LAG bridge port %u: %pe\n",
++ priv->lag_bridge_ports[lag.id], ERR_PTR(ret));
++
++ priv->lag_bridge_ports[lag.id] = 0;
++ }
++
++ /* Recompute global hash from remaining LAGs */
++ new_hash = mxl862xx_lag_recompute_hash(ds);
++ ret = mxl862xx_lag_set_hash(priv, new_hash);
++ if (ret)
++ dev_err(ds->dev, "failed to update trunk hash: %pe\n",
++ ERR_PTR(ret));
++
++ return 0;
++}
++
++static int mxl862xx_port_lag_change(struct dsa_switch *ds, int port)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct dsa_port *dp = dsa_to_port(ds, port);
++
++ if (!priv->ports[port].lag)
++ return 0;
++
++ priv->ports[port].lag_tx_enabled = dp->lag_tx_enabled;
++
++ return mxl862xx_lag_sync(ds, priv->ports[port].lag);
++}
++
+ static int mxl862xx_port_bridge_join(struct dsa_switch *ds, int port,
+ const struct dsa_bridge bridge,
+ bool *tx_fwd_offload,
+@@ -1952,7 +2446,18 @@ static int mxl862xx_port_bridge_join(str
+ return 0;
+ }
+
+- return mxl862xx_sync_bridge_members(ds, &bridge);
++ ret = mxl862xx_sync_bridge_members(ds, &bridge);
++ if (ret)
++ return ret;
++
++ /* If this port is in a LAG, re-sync the LAG bridge port so it
++ * picks up the new bridge_id (switching from standalone FID to
++ * the shared bridge FID).
++ */
++ if (priv->ports[port].lag)
++ ret = mxl862xx_lag_sync(ds, priv->ports[port].lag);
++
++ return ret;
+ }
+
+ static void mxl862xx_port_bridge_leave(struct dsa_switch *ds, int port,
+@@ -2011,6 +2516,17 @@ static void mxl862xx_port_bridge_leave(s
+ "failed to update CPU VBP for port %d: %pe\n", port,
+ ERR_PTR(err));
+
++ /* If this port is in a LAG, re-sync the LAG bridge port so it
++ * reverts to the standalone FID.
++ */
++ if (p->lag) {
++ err = mxl862xx_lag_sync(ds, p->lag);
++ if (err)
++ dev_err(ds->dev,
++ "failed to re-sync LAG after port %d left bridge: %pe\n",
++ port, ERR_PTR(err));
++ }
++
+ if (!dsa_bridge_ports(ds, bridge.dev))
+ mxl862xx_free_bridge(ds, &bridge);
+ }
+@@ -2636,18 +3152,17 @@ static int mxl862xx_get_fid(struct dsa_s
+ }
+
+ /**
+- * mxl862xx_fdb_bridge_port - Translate port for MAC table in tag_8021q mode
++ * mxl862xx_fdb_bridge_port - Translate port to effective bridge port ID
+ * @ds: DSA switch
+ * @port: port number passed by DSA (usually the CPU port for host entries)
+ * @db: database context identifying the user port or bridge
+ *
+- * In tag_8021q mode, host FDB/MDB entries for standalone ports must use
+- * the virtual bridge port (bridge_port_cpu) as the MAC table destination
+- * so that known-unicast and known-multicast frames exit through the
+- * virtual bridge port's egress EVLAN, which inserts the management VID.
+- * Without this, the firmware forwards known traffic directly to the
+- * physical CPU bridge port, bypassing management VID insertion, and DSA
+- * drops the untagged frame.
++ * Returns the firmware bridge port ID that should be used for MAC table
++ * entries targeting @port:
++ * - CPU port in tag_8021q standalone mode: the virtual bridge port
++ * (bridge_port_cpu) so known traffic exits through egress EVLAN
++ * - User port in a LAG: the LAG's dedicated firmware bridge port
++ * - Otherwise: the port index itself
+ */
+ static int mxl862xx_fdb_bridge_port(struct dsa_switch *ds, int port,
+ const struct dsa_db db)
+@@ -2663,7 +3178,7 @@ static int mxl862xx_fdb_bridge_port(stru
+ return bp_cpu;
+ }
+
+- return port;
++ return mxl862xx_lag_bridge_port(priv, port);
+ }
+
+ /**
+@@ -2907,11 +3422,43 @@ static int mxl862xx_port_fdb_del(struct
+ return ret;
+ }
+
++static int mxl862xx_lag_fdb_add(struct dsa_switch *ds, const struct dsa_lag lag,
++ const unsigned char *addr, u16 vid,
++ const struct dsa_db db)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ u16 lag_bp = priv->lag_bridge_ports[lag.id];
++ int fid;
++
++ if (!lag_bp)
++ return -ENOENT;
++
++ fid = mxl862xx_get_fid(ds, db);
++ if (fid < 0)
++ return fid;
++
++ return mxl862xx_fdb_add_per_fid(ds, addr, vid, fid, lag_bp);
++}
++
++static int mxl862xx_lag_fdb_del(struct dsa_switch *ds, const struct dsa_lag lag,
++ const unsigned char *addr, u16 vid,
++ const struct dsa_db db)
++{
++ int fid;
++
++ fid = mxl862xx_get_fid(ds, db);
++ if (fid < 0)
++ return fid;
++
++ return mxl862xx_fdb_del_per_fid(ds, addr, vid, fid);
++}
++
+ static int mxl862xx_port_fdb_dump(struct dsa_switch *ds, int port,
+ dsa_fdb_dump_cb_t *cb, void *data)
+ {
+ struct mxl862xx_mac_table_read param = { .initial = 1 };
+ struct mxl862xx_priv *priv = ds->priv;
++ u16 lag_bp = mxl862xx_lag_bridge_port(priv, port);
+ u32 entry_port_id;
+ int ret;
+
+@@ -2925,7 +3472,7 @@ static int mxl862xx_port_fdb_dump(struct
+
+ entry_port_id = le32_to_cpu(param.port_id);
+
+- if (entry_port_id == port) {
++ if (entry_port_id == port || entry_port_id == lag_bp) {
+ ret = cb(param.mac, FIELD_GET(MXL862XX_TCI_VLAN_ID,
+ le16_to_cpu(param.tci)),
+ param.static_entry, data);
+@@ -3562,6 +4109,11 @@ static const struct dsa_switch_ops mxl86
+ .port_fdb_dump = mxl862xx_port_fdb_dump,
+ .port_mdb_add = mxl862xx_port_mdb_add,
+ .port_mdb_del = mxl862xx_port_mdb_del,
++ .port_lag_join = mxl862xx_port_lag_join,
++ .port_lag_leave = mxl862xx_port_lag_leave,
++ .port_lag_change = mxl862xx_port_lag_change,
++ .lag_fdb_add = mxl862xx_lag_fdb_add,
++ .lag_fdb_del = mxl862xx_lag_fdb_del,
+ .port_vlan_filtering = mxl862xx_port_vlan_filtering,
+ .port_vlan_add = mxl862xx_port_vlan_add,
+ .port_vlan_del = mxl862xx_port_vlan_del,
+@@ -3602,6 +4154,7 @@ static int mxl862xx_probe(struct mdio_de
+ ds->num_ports = MXL862XX_MAX_PORTS;
+ ds->fdb_isolation = true;
+ ds->max_num_bridges = MXL862XX_MAX_BRIDGES;
++ ds->num_lag_ids = MXL862XX_MAX_LAG_IDS;
+
+ mxl862xx_host_init(priv);
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -14,6 +14,19 @@
+ #define MXL862XX_MAX_BRIDGE_PORTS 128
+ #define MXL862XX_TOTAL_EVLAN_ENTRIES 1024
+ #define MXL862XX_TOTAL_VF_ENTRIES 1024
++#define MXL862XX_MAX_LAG_IDS 16
++
++/* Trunk hash field bitmask (matches PCE_TRUNK_CONF layout) */
++#define MXL862XX_TRUNK_HASH_SA BIT(0)
++#define MXL862XX_TRUNK_HASH_DA BIT(1)
++#define MXL862XX_TRUNK_HASH_SIP BIT(2)
++#define MXL862XX_TRUNK_HASH_DIP BIT(3)
++#define MXL862XX_TRUNK_HASH_SPORT BIT(4)
++#define MXL862XX_TRUNK_HASH_DPORT BIT(5)
++
++/* P-mapper LAG entries occupy indices 9..72 (64 entries) */
++#define MXL862XX_PMAPPER_LAG_FIRST 9
++#define MXL862XX_PMAPPER_LAG_COUNT 64
+
+ /* Number of __le16 words in a firmware portmap (128-bit bitmap). */
+ #define MXL862XX_FW_PORTMAP_WORDS (MXL862XX_MAX_BRIDGE_PORTS / 16)
+@@ -228,6 +241,12 @@ struct mxl862xx_port_stats {
+ * @stats_lock: protects accumulator reads in .get_stats64 against
+ * concurrent updates from the polling work
+ * @tag_8021q_vid: currently assigned tag_8021q management VID
++ * @lag: non-NULL when port is member of a LAG group;
++ * points to the DSA LAG structure
++ * @lag_tx_enabled: true when this port is active for TX in its LAG
++ * @lag_hash_bits: hash field bitmask (MXL862XX_TRUNK_HASH_*) requested
++ * when this port joined its LAG; used to recompute the
++ * global trunk_hash when a LAG is destroyed
+ */
+ struct mxl862xx_port {
+ struct mxl862xx_priv *priv;
+@@ -250,6 +269,10 @@ struct mxl862xx_port {
+ struct work_struct host_flood_work;
+ u16 tag_8021q_vid;
+ struct mxl862xx_evlan_block cpu_egress_evlan;
++ /* LAG state */
++ struct dsa_lag *lag;
++ bool lag_tx_enabled;
++ u8 lag_hash_bits;
+ /* Hardware stats accumulation */
+ struct mxl862xx_port_stats stats;
+ spinlock_t stats_lock;
+@@ -328,6 +351,15 @@ union mxl862xx_fw_version {
+ * DSA bridge number. Indexed by dsa_bridge.num
+ * (0 .. ds->max_num_bridges).
+ * @vf_block_size: per-port VLAN Filter block size
++ * @lag_bridge_ports: maps DSA LAG ID to firmware bridge port ID;
++ * zero means no bridge port allocated for that LAG.
++ * Indexed by lag->id (entry 0 is unused).
++ * The bridge port is stable for the LAG's lifetime
++ * so FDB/MDB entries never need migration on
++ * membership changes.
++ * @trunk_hash: current global hash field bitmask (6 bits,
++ * MXL862XX_TRUNK_HASH_*); union of all active LAGs'
++ * hash requirements
+ * @stats_work: periodic work item that polls RMON hardware counters
+ * and accumulates them into 64-bit per-port stats
+ */
+@@ -346,6 +378,8 @@ struct mxl862xx_priv {
+ u16 evlan_egress_size;
+ u16 cpu_evlan_ingress_size;
+ u16 vf_block_size;
++ u16 lag_bridge_ports[MXL862XX_MAX_LAG_IDS + 1];
++ u8 trunk_hash;
+ struct delayed_work stats_work;
+ };
+
--- /dev/null
+From fbfa1b0649c578e0d43e3a61617b53a9a722efad Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 12:05:29 +0000
+Subject: [PATCH 27/35] net: dsa: mxl862xx: add support for mirror port
+
+The MxL862xx hardware supports a single monitor port which can be
+configured to mirror any other port's ingress and/or egress traffic.
+
+Implement support for .port_mirror_add/.port_mirror_del using this
+feature.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 12 +++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 1 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 118 ++++++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 8 ++
+ 4 files changed, 139 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -729,6 +729,18 @@ struct mxl862xx_bridge_port_config {
+ } __packed;
+
+ /**
++ * struct mxl862xx_monitor_port_cfg - Monitor port configuration
++ * @port_id: Destination port for mirrored traffic (zero-based)
++ * @sub_if_id: Monitoring sub-interface ID
++ * @monitor_port: Reserved
++ */
++struct mxl862xx_monitor_port_cfg {
++ u8 port_id;
++ __le16 sub_if_id;
++ u8 monitor_port;
++} __packed;
++
++/**
+ * struct mxl862xx_cfg - Global Switch configuration Attributes
+ * @mac_table_age_timer: See &enum mxl862xx_age_timer
+ * @age_timer: Custom MAC table aging timer in seconds
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -30,6 +30,7 @@
+ #define MXL862XX_COMMON_PORTCFGGET (MXL862XX_COMMON_MAGIC + 0x7)
+ #define MXL862XX_COMMON_CFGGET (MXL862XX_COMMON_MAGIC + 0x9)
+ #define MXL862XX_COMMON_CFGSET (MXL862XX_COMMON_MAGIC + 0xa)
++#define MXL862XX_COMMON_MONITORPORTCFGSET (MXL862XX_COMMON_MAGIC + 0xe)
+ #define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11)
+
+ #define MXL862XX_TFLOW_PCERULEWRITE (MXL862XX_TFLOW_MAGIC + 0x2)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -1129,6 +1129,8 @@ static int mxl862xx_setup(struct dsa_swi
+ (n_user_ports + n_cpu_ports);
+ }
+
++ priv->mirror_dest = -1;
++
+ ret = mxl862xx_setup_drop_meter(ds);
+ if (ret)
+ return ret;
+@@ -2018,6 +2020,120 @@ static int mxl862xx_setup_cpu_bridge(str
+ return mxl862xx_set_bridge_port(ds, port);
+ }
+
++static int mxl862xx_port_mirror_add(struct dsa_switch *ds, int port,
++ struct dsa_mall_mirror_tc_entry *mirror,
++ bool ingress,
++ struct netlink_ext_ack *extack)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ struct mxl862xx_monitor_port_cfg mon = {
++ .port_id = mirror->to_local_port,
++ };
++ struct mxl862xx_ctp_port_config ctp = {
++ .logical_port_id = port,
++ .mask = cpu_to_le32(
++ MXL862XX_CTP_PORT_CONFIG_MASK_LOOPBACK_AND_MIRROR),
++ .ingress_mirror_enable = p->ingress_mirror,
++ .egress_mirror_enable = p->egress_mirror,
++ };
++ int ret;
++
++ /* The hardware has a single global monitor port. Reject if an
++ * existing mirror session targets a different destination.
++ */
++ if (priv->mirror_dest >= 0 &&
++ priv->mirror_dest != mirror->to_local_port) {
++ NL_SET_ERR_MSG_MOD(extack,
++ "Only one mirror destination port is supported");
++ return -EBUSY;
++ }
++
++ if (ingress)
++ ctp.ingress_mirror_enable = 1;
++ else
++ ctp.egress_mirror_enable = 1;
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
++ if (ret) {
++ dev_err(ds->dev, "mirror: CTP write failed for port %d: %pe\n",
++ port, ERR_PTR(ret));
++ return ret;
++ }
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_MONITORPORTCFGSET, mon);
++ if (ret) {
++ dev_err(ds->dev,
++ "mirror: failed to set monitor port %d: %pe\n",
++ mirror->to_local_port, ERR_PTR(ret));
++ /* Roll back CTP change */
++ ctp.ingress_mirror_enable = p->ingress_mirror;
++ ctp.egress_mirror_enable = p->egress_mirror;
++ MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
++ return ret;
++ }
++
++ if (ingress)
++ p->ingress_mirror = true;
++ else
++ p->egress_mirror = true;
++
++ priv->mirror_dest = mirror->to_local_port;
++
++ return 0;
++}
++
++static void mxl862xx_port_mirror_del(struct dsa_switch *ds, int port,
++ struct dsa_mall_mirror_tc_entry *mirror)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_port *p = &priv->ports[port];
++ struct mxl862xx_ctp_port_config ctp = {
++ .logical_port_id = port,
++ .mask = cpu_to_le32(
++ MXL862XX_CTP_PORT_CONFIG_MASK_LOOPBACK_AND_MIRROR),
++ .ingress_mirror_enable = p->ingress_mirror,
++ .egress_mirror_enable = p->egress_mirror,
++ };
++ struct mxl862xx_monitor_port_cfg mon = {};
++ bool active = false;
++ int i, ret;
++
++ if (mirror->ingress)
++ ctp.ingress_mirror_enable = 0;
++ else
++ ctp.egress_mirror_enable = 0;
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
++ if (ret)
++ dev_err(ds->dev, "mirror: CTP write failed for port %d: %pe\n",
++ port, ERR_PTR(ret));
++
++ if (mirror->ingress)
++ p->ingress_mirror = false;
++ else
++ p->egress_mirror = false;
++
++ /* If no ports have any mirrors active, clear the monitor port */
++ for (i = 0; i < ds->num_ports; i++) {
++ if (priv->ports[i].ingress_mirror ||
++ priv->ports[i].egress_mirror) {
++ active = true;
++ break;
++ }
++ }
++
++ if (active)
++ return;
++
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_MONITORPORTCFGSET, mon);
++ if (ret)
++ dev_err(ds->dev, "mirror: failed to clear monitor port: %pe\n",
++ ERR_PTR(ret));
++
++ priv->mirror_dest = -1;
++}
++
+ /**
+ * mxl862xx_lag_master_port - Find the LAG master (lowest-numbered member)
+ * @ds: DSA switch
+@@ -4109,6 +4225,8 @@ static const struct dsa_switch_ops mxl86
+ .port_fdb_dump = mxl862xx_port_fdb_dump,
+ .port_mdb_add = mxl862xx_port_mdb_add,
+ .port_mdb_del = mxl862xx_port_mdb_del,
++ .port_mirror_add = mxl862xx_port_mirror_add,
++ .port_mirror_del = mxl862xx_port_mirror_del,
+ .port_lag_join = mxl862xx_port_lag_join,
+ .port_lag_leave = mxl862xx_port_lag_leave,
+ .port_lag_change = mxl862xx_port_lag_change,
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -241,6 +241,8 @@ struct mxl862xx_port_stats {
+ * @stats_lock: protects accumulator reads in .get_stats64 against
+ * concurrent updates from the polling work
+ * @tag_8021q_vid: currently assigned tag_8021q management VID
++ * @ingress_mirror: true when ingress mirroring is active on this port
++ * @egress_mirror: true when egress mirroring is active on this port
+ * @lag: non-NULL when port is member of a LAG group;
+ * points to the DSA LAG structure
+ * @lag_tx_enabled: true when this port is active for TX in its LAG
+@@ -269,6 +271,9 @@ struct mxl862xx_port {
+ struct work_struct host_flood_work;
+ u16 tag_8021q_vid;
+ struct mxl862xx_evlan_block cpu_egress_evlan;
++ /* Mirror state */
++ bool ingress_mirror;
++ bool egress_mirror;
+ /* LAG state */
+ struct dsa_lag *lag;
+ bool lag_tx_enabled;
+@@ -360,6 +365,8 @@ union mxl862xx_fw_version {
+ * @trunk_hash: current global hash field bitmask (6 bits,
+ * MXL862XX_TRUNK_HASH_*); union of all active LAGs'
+ * hash requirements
++ * @mirror_dest: current mirror destination port, or -1 if no mirror
++ * session is active; used to detect monitor port conflicts
+ * @stats_work: periodic work item that polls RMON hardware counters
+ * and accumulates them into 64-bit per-port stats
+ */
+@@ -380,6 +387,7 @@ struct mxl862xx_priv {
+ u16 vf_block_size;
+ u16 lag_bridge_ports[MXL862XX_MAX_LAG_IDS + 1];
+ u8 trunk_hash;
++ int mirror_dest;
+ struct delayed_work stats_work;
+ };
+
--- /dev/null
+From 67f82834819b71417b58dc1293c20f71b990264f Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 16:30:08 +0000
+Subject: [PATCH 28/35] net: dsa: wire flash_update devlink callback to drivers
+
+Add a devlink_flash_update callback to dsa_switch_ops so that DSA
+drivers can support devlink dev flash without open-coding the devlink
+plumbing. The new trampoline in net/dsa/devlink.c follows the existing
+dsa_devlink_info_get pattern exactly.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ include/net/dsa.h | 3 +++
+ net/dsa/devlink.c | 13 +++++++++++++
+ 2 files changed, 16 insertions(+)
+
+--- a/include/net/dsa.h
++++ b/include/net/dsa.h
+@@ -1185,6 +1185,9 @@ struct dsa_switch_ops {
+ int (*devlink_info_get)(struct dsa_switch *ds,
+ struct devlink_info_req *req,
+ struct netlink_ext_ack *extack);
++ int (*devlink_flash_update)(struct dsa_switch *ds,
++ struct devlink_flash_update_params *params,
++ struct netlink_ext_ack *extack);
+ int (*devlink_sb_pool_get)(struct dsa_switch *ds,
+ unsigned int sb_index, u16 pool_index,
+ struct devlink_sb_pool_info *pool_info);
+--- a/net/dsa/devlink.c
++++ b/net/dsa/devlink.c
+@@ -20,6 +20,18 @@ static int dsa_devlink_info_get(struct d
+ return -EOPNOTSUPP;
+ }
+
++static int dsa_devlink_flash_update(struct devlink *dl,
++ struct devlink_flash_update_params *params,
++ struct netlink_ext_ack *extack)
++{
++ struct dsa_switch *ds = dsa_devlink_to_ds(dl);
++
++ if (!ds->ops->devlink_flash_update)
++ return -EOPNOTSUPP;
++
++ return ds->ops->devlink_flash_update(ds, params, extack);
++}
++
+ static int dsa_devlink_sb_pool_get(struct devlink *dl,
+ unsigned int sb_index, u16 pool_index,
+ struct devlink_sb_pool_info *pool_info)
+@@ -169,6 +181,7 @@ dsa_devlink_sb_occ_tc_port_bind_get(stru
+
+ static const struct devlink_ops dsa_devlink_ops = {
+ .info_get = dsa_devlink_info_get,
++ .flash_update = dsa_devlink_flash_update,
+ .sb_pool_get = dsa_devlink_sb_pool_get,
+ .sb_pool_set = dsa_devlink_sb_pool_set,
+ .sb_port_pool_get = dsa_devlink_sb_port_pool_get,
--- /dev/null
+From 1a87b829ef3280d646dc480f7b261d9e32896899 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 16:30:17 +0000
+Subject: [PATCH 29/35] net: dsa: mxl862xx: add SMDIO clause-22 register access
+
+Add mxl862xx_smdio_read() and mxl862xx_smdio_write() for clause-22
+SMDIO register access. MCUboot rescue mode only exposes clause-22
+registers; the existing clause-45 MMD interface is unavailable during
+firmware transfer. The MDIO bus lock is held per-transaction (not
+across polls) so that SB PDI polling during flash erase does not
+starve other MDIO users.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-host.c | 35 ++++++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-host.h | 2 ++
+ 2 files changed, 37 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+@@ -493,6 +493,41 @@ out:
+ return ret;
+ }
+
++#define MXL862XX_SMDIO_ADDR_REG 0x1f
++#define MXL862XX_SMDIO_PAGE_MASK 0xfff0
++#define MXL862XX_SMDIO_OFF_MASK 0x000f
++
++int mxl862xx_smdio_read(struct mxl862xx_priv *priv, u32 addr)
++{
++ struct mii_bus *bus = priv->mdiodev->bus;
++ int phy = priv->mdiodev->addr;
++ int ret;
++
++ mutex_lock(&bus->mdio_lock);
++ ret = __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG,
++ addr & MXL862XX_SMDIO_PAGE_MASK);
++ if (ret >= 0)
++ ret = __mdiobus_read(bus, phy, addr & MXL862XX_SMDIO_OFF_MASK);
++ mutex_unlock(&bus->mdio_lock);
++ return ret;
++}
++
++int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val)
++{
++ struct mii_bus *bus = priv->mdiodev->bus;
++ int phy = priv->mdiodev->addr;
++ int ret;
++
++ mutex_lock(&bus->mdio_lock);
++ ret = __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG,
++ addr & MXL862XX_SMDIO_PAGE_MASK);
++ if (ret >= 0)
++ ret = __mdiobus_write(bus, phy, addr & MXL862XX_SMDIO_OFF_MASK,
++ val);
++ mutex_unlock(&bus->mdio_lock);
++ return ret;
++}
++
+ void mxl862xx_host_init(struct mxl862xx_priv *priv)
+ {
+ INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn);
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+@@ -18,5 +18,7 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
+
+ int mxl862xx_reset(struct mxl862xx_priv *priv);
++int mxl862xx_smdio_read(struct mxl862xx_priv *priv, u32 addr);
++int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val);
+
+ #endif /* __MXL862XX_HOST_H */
--- /dev/null
+From b7e8f8fd4493b255f0f01fe790a73ad61b5e8ce8 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 16:30:31 +0000
+Subject: [PATCH 30/35] net: dsa: mxl862xx: add devlink flash_update and
+ info_get
+
+Implement runtime firmware upgrade via "devlink dev flash" and version
+reporting via "devlink dev info":
+
+ devlink dev info mdio_bus/<bus>/<addr>
+ devlink dev flash mdio_bus/<bus>/<addr> file <firmware.bin>
+
+The driver sends SYS_MISC_FW_UPDATE to enter MCUboot rescue mode,
+transfers the signed image over the SB PDI bulk-transfer protocol
+(clause-22 SMDIO), waits for the switch to reboot, then schedules
+device_reprobe() for a clean remove()+probe() cycle.
+
+Before the transfer begins the driver closes all conduit interfaces
+and marks every netdev (user and conduit) not-present via
+netif_device_detach() so that userspace cannot bring ports back up
+during the ~15 minute flash process. Progress is reported through
+devlink status notifications. Once the FW_UPDATE command has been
+sent the switch is in MCUboot mode and normal operation can only be
+restored by a reprobe, so the driver always schedules one regardless
+of transfer outcome.
+
+The reprobe work item is dynamically allocated (following the iwlwifi
+pattern) because device_reprobe() triggers remove() which frees the
+devm-managed priv while the work is still executing.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/Makefile | 2 +-
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 1 +
+ drivers/net/dsa/mxl862xx/mxl862xx-fw.c | 434 +++++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx-fw.h | 15 +
+ drivers/net/dsa/mxl862xx/mxl862xx-host.c | 7 +
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 4 +
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 2 +
+ 7 files changed, 464 insertions(+), 1 deletion(-)
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.c
+ create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.h
+
+--- a/drivers/net/dsa/mxl862xx/Makefile
++++ b/drivers/net/dsa/mxl862xx/Makefile
+@@ -1,3 +1,3 @@
+ # SPDX-License-Identifier: GPL-2.0
+ obj-$(CONFIG_NET_DSA_MXL862) += mxl862xx_dsa.o
+-mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o
++mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o mxl862xx-fw.o
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -83,6 +83,7 @@
+ #define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x1)
+ #define INT_GPHY_WRITE (GPY_GPY2XX_MAGIC + 0x2)
+
++#define SYS_MISC_FW_UPDATE (SYS_MISC_MAGIC + 0x1)
+ #define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2)
+
+ #define MXL862XX_XPCS_MAGIC 0x1a00
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
+@@ -0,0 +1,434 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++/*
++ * Firmware flash and devlink support for MaxLinear MxL862xx
++ *
++ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
++ *
++ * Usage:
++ * # Query running firmware version:
++ * devlink dev info mdio_bus/<bus>/<addr>
++ *
++ * # Flash new firmware (all ports are taken down automatically):
++ * devlink dev flash mdio_bus/<bus>/<addr> file <firmware.bin>
++ *
++ * The flash process takes approximately 15 minutes. Progress is
++ * reported via devlink status notifications. After a successful (or
++ * failed) flash the driver reprobes the device automatically.
++ */
++
++#include <linux/crc32.h>
++#include <linux/delay.h>
++#include <linux/device.h>
++#include <linux/module.h>
++#include <linux/netdevice.h>
++#include <linux/rtnetlink.h>
++#include <net/dsa.h>
++
++#include "mxl862xx.h"
++#include "mxl862xx-api.h"
++#include "mxl862xx-cmd.h"
++#include "mxl862xx-fw.h"
++#include "mxl862xx-host.h"
++
++/* SB PDI registers (clause-22 SMDIO address space) */
++#define MXL862XX_SB_PDI_CTRL 0xe100
++#define MXL862XX_SB_PDI_ADDR 0xe101
++#define MXL862XX_SB_PDI_DATA 0xe102
++#define MXL862XX_SB_PDI_STAT 0xe103
++
++/* SB PDI CTRL modes */
++#define MXL862XX_SB_PDI_CTRL_RST 0x00
++#define MXL862XX_SB_PDI_CTRL_WR 0x02
++
++/* SB PDI handshake magic */
++#define MXL862XX_SB_PDI_READY 0xc55c
++#define MXL862XX_SB_PDI_START 0xf48f
++#define MXL862XX_SB_PDI_END 0x3cc3
++
++/* Firmware transfer geometry */
++#define MXL862XX_FW_HDR_SIZE 20
++#define MXL862XX_FW_BANK_HALF 16384 /* words per half-bank */
++#define MXL862XX_FW_BANK_SLICE 32760 /* words per full slice */
++#define MXL862XX_FW_SB1_ADDR 0x7800 /* SB1 word address */
++
++/* Timeouts */
++#define MXL862XX_FW_READY_TIMEOUT_MS 30000
++#define MXL862XX_FW_ACK_TIMEOUT_MS 5000
++#define MXL862XX_FW_ERASE_TIMEOUT_MS 300000 /* flash erase is very slow */
++#define MXL862XX_FW_WRITE_TIMEOUT_MS 120000 /* per-slice program timeout */
++#define MXL862XX_FW_REBOOT_DELAY_MS 5000
++
++static void mxl862xx_sb_pdi_reset(struct mxl862xx_priv *priv)
++{
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
++ MXL862XX_SB_PDI_CTRL_RST);
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR,
++ MXL862XX_SB_PDI_CTRL_RST);
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA,
++ MXL862XX_SB_PDI_CTRL_RST);
++}
++
++static int mxl862xx_sb_pdi_poll_stat(struct mxl862xx_priv *priv, u16 expected,
++ unsigned long timeout_ms)
++{
++ unsigned long timeout = jiffies + msecs_to_jiffies(timeout_ms);
++ int ret;
++
++ do {
++ ret = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_STAT);
++ if (ret < 0)
++ return ret;
++ if ((u16)ret == expected)
++ return 0;
++ usleep_range(10000, 11000);
++ } while (time_before(jiffies, timeout));
++
++ return -ETIMEDOUT;
++}
++
++/* Reprobe work -- dynamically allocated so it survives remove().
++ * device_reprobe() -> remove() frees priv (devm) while work is executing,
++ * so the work struct must not live in mxl862xx_priv.
++ */
++struct mxl862xx_reprobe {
++ struct device *dev;
++ struct delayed_work dwork;
++};
++
++static void mxl862xx_reprobe_work_fn(struct work_struct *work)
++{
++ struct mxl862xx_reprobe *reprobe =
++ container_of(work, struct mxl862xx_reprobe, dwork.work);
++
++ if (device_reprobe(reprobe->dev))
++ dev_err(reprobe->dev, "reprobe failed\n");
++ put_device(reprobe->dev);
++ kfree(reprobe);
++ module_put(THIS_MODULE);
++}
++
++/* MCUboot firmware image header (20 bytes) */
++struct mxl862xx_fw_hdr {
++ __le32 image_type;
++ __le32 image_size_1;
++ __le32 image_checksum_1;
++ __le32 image_size_2;
++ __le32 image_checksum_2;
++} __packed;
++
++static int mxl862xx_flash_firmware(struct mxl862xx_priv *priv,
++ const struct firmware *fw,
++ struct devlink *dl)
++{
++ const struct mxl862xx_fw_hdr *hdr;
++ u32 word_idx = 0, data_written = 0, idx = 0;
++ unsigned long next_notify = 0;
++ const u8 *payload;
++ u32 payload_size;
++ u16 word, fdata;
++ int ret, i;
++ u32 crc;
++
++ if (fw->size < MXL862XX_FW_HDR_SIZE)
++ return -EINVAL;
++
++ hdr = (const struct mxl862xx_fw_hdr *)fw->data;
++ payload = fw->data + MXL862XX_FW_HDR_SIZE;
++ payload_size = le32_to_cpu(hdr->image_size_1) +
++ le32_to_cpu(hdr->image_size_2);
++
++ if (payload_size > fw->size - MXL862XX_FW_HDR_SIZE) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: firmware file too small for declared size\n");
++ return -EINVAL;
++ }
++
++ /* Validate CRC-32 of both image slots before touching hardware */
++ if (le32_to_cpu(hdr->image_size_1)) {
++ crc = ~crc32_le(~0U, payload,
++ le32_to_cpu(hdr->image_size_1));
++ if (crc != le32_to_cpu(hdr->image_checksum_1)) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: image 1 CRC mismatch (got %08x, expected %08x)\n",
++ crc, le32_to_cpu(hdr->image_checksum_1));
++ return -EINVAL;
++ }
++ }
++
++ if (le32_to_cpu(hdr->image_size_2)) {
++ crc = ~crc32_le(~0U,
++ payload + le32_to_cpu(hdr->image_size_1),
++ le32_to_cpu(hdr->image_size_2));
++ if (crc != le32_to_cpu(hdr->image_checksum_2)) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: image 2 CRC mismatch (got %08x, expected %08x)\n",
++ crc, le32_to_cpu(hdr->image_checksum_2));
++ return -EINVAL;
++ }
++ }
++
++ /* Step 1: Tell firmware to enter MCUboot rescue mode.
++ * The FW_UPDATE command takes no payload (size 0).
++ */
++ ret = mxl862xx_api_wrap(priv, SYS_MISC_FW_UPDATE, NULL, 0,
++ false, false);
++ if (ret) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: FW_UPDATE command failed: %pe\n",
++ ERR_PTR(ret));
++ return ret;
++ }
++
++ /* From this point on, the switch is in MCUboot rescue mode.
++ * Any failure must go through the end_magic label to tell
++ * MCUboot to reboot rather than leaving it stuck waiting.
++ */
++
++ /* Step 2: Reset PDI and wait for bootloader ready */
++ devlink_flash_update_status_notify(dl, "Waiting for bootloader",
++ NULL, 0, 0);
++ mxl862xx_sb_pdi_reset(priv);
++ ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_READY,
++ MXL862XX_FW_READY_TIMEOUT_MS);
++ if (ret) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: bootloader not ready: %pe\n", ERR_PTR(ret));
++ goto end_magic;
++ }
++
++ /* Step 3: Start handshake */
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
++ MXL862XX_SB_PDI_START);
++ ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_START + 1,
++ MXL862XX_FW_ACK_TIMEOUT_MS);
++ if (ret) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: start handshake failed: %pe\n", ERR_PTR(ret));
++ goto end_magic;
++ }
++
++ /* Step 4: Transfer 20-byte header using auto-increment write mode */
++ devlink_flash_update_status_notify(dl, "Erasing flash", NULL, 0, 0);
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
++ MXL862XX_SB_PDI_CTRL_WR);
++ for (i = 0; i < MXL862XX_FW_HDR_SIZE / 2; i++) {
++ word = fw->data[i * 2] |
++ ((u16)fw->data[i * 2 + 1] << 8);
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, word);
++ }
++ mxl862xx_sb_pdi_reset(priv);
++
++ /* Write header byte count to STAT to trigger erase */
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
++ MXL862XX_FW_HDR_SIZE);
++
++ /* Wait for header ACK (header_size + 1) */
++ ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_FW_HDR_SIZE + 1,
++ MXL862XX_FW_ACK_TIMEOUT_MS);
++ if (ret) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: header ACK failed: %pe\n", ERR_PTR(ret));
++ goto end_magic;
++ }
++
++ /* Step 5: Wait for erase to complete (STAT goes to 0) */
++ ret = mxl862xx_sb_pdi_poll_stat(priv, 0,
++ MXL862XX_FW_ERASE_TIMEOUT_MS);
++ if (ret) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: erase timeout: %pe\n", ERR_PTR(ret));
++ goto end_magic;
++ }
++
++ /* Step 6: Transfer payload using dual-bank auto-increment writes */
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
++ MXL862XX_SB_PDI_CTRL_WR);
++
++ while (idx < payload_size) {
++ if (idx + 1 < payload_size) {
++ fdata = payload[idx] |
++ ((u16)payload[idx + 1] << 8);
++ idx += 2;
++ data_written += 2;
++ } else {
++ fdata = payload[idx];
++ idx++;
++ data_written++;
++ }
++
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, fdata);
++ word_idx++;
++
++ /* Last byte(s): flush final partial slice */
++ if (idx >= payload_size) {
++ mxl862xx_sb_pdi_reset(priv);
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
++ data_written);
++ break;
++ }
++
++ /* Half-bank boundary: switch to SB1 address */
++ if (word_idx == MXL862XX_FW_BANK_HALF) {
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
++ MXL862XX_SB_PDI_CTRL_RST);
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR,
++ MXL862XX_FW_SB1_ADDR);
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
++ MXL862XX_SB_PDI_CTRL_WR);
++ } else if (word_idx >= MXL862XX_FW_BANK_SLICE) {
++ /* Full slice: flush and wait for program */
++ mxl862xx_sb_pdi_reset(priv);
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
++ data_written);
++ word_idx = 0;
++ data_written = 0;
++
++ ret = mxl862xx_sb_pdi_poll_stat(
++ priv, 0, MXL862XX_FW_WRITE_TIMEOUT_MS);
++ if (ret) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: write timeout at %u/%u: %pe\n",
++ idx, payload_size, ERR_PTR(ret));
++ goto end_magic;
++ }
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
++ MXL862XX_SB_PDI_CTRL_WR);
++
++ if (time_after(jiffies, next_notify)) {
++ devlink_flash_update_status_notify(
++ dl, "Flashing", NULL,
++ idx, payload_size);
++ next_notify = jiffies +
++ msecs_to_jiffies(500);
++ }
++ }
++ }
++
++ /* Wait for final slice to be programmed */
++ ret = mxl862xx_sb_pdi_poll_stat(priv, 0,
++ MXL862XX_FW_WRITE_TIMEOUT_MS);
++ if (ret) {
++ dev_err(&priv->mdiodev->dev,
++ "flash: final write timeout: %pe\n", ERR_PTR(ret));
++ goto end_magic;
++ }
++
++ devlink_flash_update_status_notify(dl, "Flashing", NULL,
++ payload_size, payload_size);
++
++end_magic:
++ /* Always send end magic so MCUboot reboots instead of sitting
++ * idle. The hardware reset during reprobe recovers the switch
++ * regardless of whether the transfer succeeded or failed.
++ */
++ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
++ MXL862XX_SB_PDI_END);
++ msleep(MXL862XX_FW_REBOOT_DELAY_MS);
++
++ return ret;
++}
++
++int mxl862xx_devlink_info_get(struct dsa_switch *ds,
++ struct devlink_info_req *req,
++ struct netlink_ext_ack *extack)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ char ver_str[32];
++
++ snprintf(ver_str, sizeof(ver_str), "%u.%u.%u",
++ priv->fw_version.major, priv->fw_version.minor,
++ priv->fw_version.revision);
++
++ return devlink_info_version_running_put(req, "fw", ver_str);
++}
++
++int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
++ struct devlink_flash_update_params *params,
++ struct netlink_ext_ack *extack)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_sys_fw_image_version ver = {};
++ struct mxl862xx_reprobe *reprobe;
++ struct dsa_port *dp;
++ int ret, i;
++
++ if (params->component) {
++ NL_SET_ERR_MSG_MOD(extack, "component is not supported");
++ return -EOPNOTSUPP;
++ }
++
++ dev_info(ds->dev, "flash: running firmware %u.%u.%u\n",
++ priv->fw_version.major, priv->fw_version.minor,
++ priv->fw_version.revision);
++
++ /* Close all user and CPU ports while the firmware is still
++ * alive. dev_close() on user ports triggers multicast group
++ * leave and host MDB/FDB removal on the CPU port through the
++ * normal DSA callbacks so the core's tracking lists are
++ * drained before we enter MCUboot. Then mark user ports
++ * not-present so userspace cannot bring them back up during
++ * the (slow) flash process. The conduit is only closed, not
++ * detached -- it is owned by the Ethernet MAC driver and
++ * dev_open() during reprobe must be able to bring it back.
++ */
++ rtnl_lock();
++ dsa_switch_for_each_user_port(dp, ds) {
++ if (dp->user) {
++ dev_close(dp->user);
++ netif_device_detach(dp->user);
++ }
++ }
++ dsa_switch_for_each_cpu_port(dp, ds)
++ dev_close(dp->conduit);
++ rtnl_unlock();
++
++ /* Block all firmware API commands while the switch is being
++ * reflashed. The conduit is intentionally kept open -- it is
++ * owned by the Ethernet MAC driver and would not recover on
++ * reprobe if we closed it here.
++ */
++ priv->block_host = true;
++
++ /* Stop stats polling and pending host-flood work */
++ cancel_delayed_work_sync(&priv->stats_work);
++ for (i = 0; i < ds->num_ports; i++)
++ cancel_work_sync(&priv->ports[i].host_flood_work);
++
++ ret = mxl862xx_flash_firmware(priv, params->fw, ds->devlink);
++ if (ret)
++ NL_SET_ERR_MSG_MOD(extack, "firmware transfer failed");
++
++ if (!ret) {
++ /* Read new firmware version (switch just rebooted).
++ * Temporarily lift the block for this single query.
++ */
++ priv->block_host = false;
++ memset(&ver, 0, sizeof(ver));
++ if (!MXL862XX_API_READ_QUIET(priv, SYS_MISC_FW_VERSION, ver)
++ && ver.iv_major)
++ dev_info(ds->dev, "flash: new firmware %u.%u.%u\n",
++ ver.iv_major, ver.iv_minor,
++ le16_to_cpu(ver.iv_revision));
++ }
++
++ /* Silently discard all API commands during the teardown that
++ * reprobe triggers -- the switch firmware has been reset and
++ * has no knowledge of the old configuration.
++ */
++ priv->skip_teardown = true;
++
++ reprobe = kzalloc(sizeof(*reprobe), GFP_KERNEL);
++ if (!reprobe)
++ return ret;
++
++ if (!try_module_get(THIS_MODULE)) {
++ kfree(reprobe);
++ return ret;
++ }
++
++ reprobe->dev = get_device(ds->dev);
++ INIT_DELAYED_WORK(&reprobe->dwork, mxl862xx_reprobe_work_fn);
++ schedule_delayed_work(&reprobe->dwork, msecs_to_jiffies(500));
++
++ return ret;
++}
+--- /dev/null
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
+@@ -0,0 +1,15 @@
++/* SPDX-License-Identifier: GPL-2.0-or-later */
++
++#ifndef __MXL862XX_FW_H
++#define __MXL862XX_FW_H
++
++#include <net/dsa.h>
++
++int mxl862xx_devlink_info_get(struct dsa_switch *ds,
++ struct devlink_info_req *req,
++ struct netlink_ext_ack *extack);
++int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
++ struct devlink_flash_update_params *params,
++ struct netlink_ext_ack *extack);
++
++#endif /* __MXL862XX_FW_H */
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+@@ -14,6 +14,7 @@
+ #include <linux/limits.h>
+ #include <net/dsa.h>
+ #include "mxl862xx.h"
++#include "mxl862xx-cmd.h"
+ #include "mxl862xx-host.h"
+
+ #define CTRL_BUSY_MASK BIT(15)
+@@ -334,6 +335,12 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
+ int ret, cmd_ret;
+ u16 max, crc, i;
+
++ if (priv->skip_teardown)
++ return 0;
++
++ if (priv->block_host && cmd != SYS_MISC_FW_UPDATE)
++ return -EBUSY;
++
+ dev_dbg(&priv->mdiodev->dev, "CMD %04x DATA %*ph\n", cmd, size, data);
+
+ mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -22,6 +22,7 @@
+ #include "mxl862xx.h"
+ #include "mxl862xx-api.h"
+ #include "mxl862xx-cmd.h"
++#include "mxl862xx-fw.h"
+ #include "mxl862xx-host.h"
+ #include "mxl862xx-phylink.h"
+
+@@ -4245,6 +4246,9 @@ static const struct dsa_switch_ops mxl86
+ .get_pause_stats = mxl862xx_get_pause_stats,
+ .get_stats64 = mxl862xx_get_stats64,
+ .self_test = mxl862xx_serdes_self_test,
++ .devlink_info_get = mxl862xx_devlink_info_get,
++ .devlink_flash_update = mxl862xx_devlink_flash_update,
++
+ };
+
+ static int mxl862xx_probe(struct mdio_device *mdiodev)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -388,6 +388,8 @@ struct mxl862xx_priv {
+ u16 lag_bridge_ports[MXL862XX_MAX_LAG_IDS + 1];
+ u8 trunk_hash;
+ int mirror_dest;
++ bool block_host;
++ bool skip_teardown;
+ struct delayed_work stats_work;
+ };
+
--- /dev/null
+From 2cb9aeb3a8d7ebac20331e0a533dcfbd73fa4237 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 23:42:18 +0000
+Subject: [PATCH 31/35] net: dsa: mxl862xx: implement port MTU configuration
+
+The firmware exposes a global max_packet_len register via
+MXL862XX_COMMON_CFGSET. Since this is switch-wide rather than
+per-port, cache each port's requested MTU and program the register
+with the maximum across all ports. The firmware call is skipped when
+the effective maximum does not change.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 50 +++++++++++++++++++++++++++++
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 4 +++
+ 2 files changed, 54 insertions(+)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -11,6 +11,7 @@
+ #include <linux/delay.h>
+ #include <linux/etherdevice.h>
+ #include <linux/if_bridge.h>
++#include <linux/if_vlan.h>
+ #include <linux/module.h>
+ #include <linux/of_device.h>
+ #include <linux/of_mdio.h>
+@@ -3768,6 +3769,53 @@ static int mxl862xx_set_ageing_time(stru
+ return ret;
+ }
+
++static int mxl862xx_port_change_mtu(struct dsa_switch *ds, int port,
++ int new_mtu)
++{
++ struct mxl862xx_priv *priv = ds->priv;
++ struct mxl862xx_cfg param = {};
++ int i, old_max = 0, new_max = 0;
++ int ret;
++
++ for (i = 0; i < ds->num_ports; i++) {
++ if (priv->ports[i].mtu > old_max)
++ old_max = priv->ports[i].mtu;
++ }
++
++ priv->ports[port].mtu = new_mtu;
++
++ for (i = 0; i < ds->num_ports; i++) {
++ if (priv->ports[i].mtu > new_max)
++ new_max = priv->ports[i].mtu;
++ }
++
++ if (new_max != old_max) {
++ ret = MXL862XX_API_READ(priv, MXL862XX_COMMON_CFGGET,
++ param);
++ if (ret)
++ return ret;
++
++ param.max_packet_len = cpu_to_le16(new_max +
++ VLAN_ETH_HLEN +
++ ETH_FCS_LEN);
++ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_CFGSET,
++ param);
++ if (ret) {
++ dev_err(ds->dev,
++ "failed to set MTU to %d: %pe\n",
++ new_mtu, ERR_PTR(ret));
++ return ret;
++ }
++ }
++
++ return 0;
++}
++
++static int mxl862xx_port_max_mtu(struct dsa_switch *ds, int port)
++{
++ return U16_MAX - VLAN_ETH_HLEN - ETH_FCS_LEN;
++}
++
+ static void mxl862xx_port_stp_state_set(struct dsa_switch *ds, int port,
+ u8 state)
+ {
+@@ -4215,6 +4263,8 @@ static const struct dsa_switch_ops mxl86
+ .port_disable = mxl862xx_port_disable,
+ .port_fast_age = mxl862xx_port_fast_age,
+ .set_ageing_time = mxl862xx_set_ageing_time,
++ .port_change_mtu = mxl862xx_port_change_mtu,
++ .port_max_mtu = mxl862xx_port_max_mtu,
+ .port_bridge_join = mxl862xx_port_bridge_join,
+ .port_bridge_leave = mxl862xx_port_bridge_leave,
+ .port_pre_bridge_flags = mxl862xx_port_pre_bridge_flags,
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -249,6 +249,8 @@ struct mxl862xx_port_stats {
+ * @lag_hash_bits: hash field bitmask (MXL862XX_TRUNK_HASH_*) requested
+ * when this port joined its LAG; used to recompute the
+ * global trunk_hash when a LAG is destroyed
++ * @mtu: per-port requested MTU; the global switch register
++ * is set to the maximum across all ports
+ */
+ struct mxl862xx_port {
+ struct mxl862xx_priv *priv;
+@@ -278,6 +280,8 @@ struct mxl862xx_port {
+ struct dsa_lag *lag;
+ bool lag_tx_enabled;
+ u8 lag_hash_bits;
++ /* MTU */
++ int mtu;
+ /* Hardware stats accumulation */
+ struct mxl862xx_port_stats stats;
+ spinlock_t stats_lock;
--- /dev/null
+From d55ca68eb0d20a66c32d531b0a454871b486c1b1 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Wed, 25 Mar 2026 01:47:19 +0000
+Subject: [PATCH 32/35] net: dsa: mxl862xx: support BR_HAIRPIN_MODE bridge flag
+
+Implement hairpin mode by including the port's own bridge port ID in
+its forwarding portmap. When hairpin is enabled, bridged frames whose
+destination resolves to the ingress port are allowed to egress there
+instead of being dropped.
+
+For LAG ports, the LAG's dedicated bridge port is added to the
+master's portmap, which naturally propagates to the LAG bridge port
+via the second loop in sync_bridge_members.
+
+The port_bridge_flags handler toggles the bit directly on the cached
+portmap and pushes the update via set_bridge_port, avoiding a full
+bridge member rebuild since only the calling port is affected.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 30 ++++++++++++++++++++++++++++-
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 6 ++++++
+ 2 files changed, 35 insertions(+), 1 deletion(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -811,6 +811,15 @@ static int mxl862xx_sync_bridge_members(
+ __set_bit(mxl862xx_cpu_bridge_port_id(ds, port),
+ priv->ports[port].portmap);
+
++ /* Hairpin: include the port's own bridge port so bridged
++ * frames can egress the ingress port.
++ * For LAG ports this adds the LAG bridge port, which
++ * propagates to the LAG BP in the second loop below.
++ */
++ if (priv->ports[port].hairpin)
++ __set_bit(mxl862xx_lag_bridge_port(priv, port),
++ priv->ports[port].portmap);
++
+ err = mxl862xx_set_bridge_port(ds, port);
+ if (err)
+ ret = err;
+@@ -3939,7 +3948,7 @@ static int mxl862xx_port_pre_bridge_flag
+ struct netlink_ext_ack *extack)
+ {
+ if (flags.mask & ~(BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD |
+- BR_LEARNING))
++ BR_LEARNING | BR_HAIRPIN_MODE))
+ return -EINVAL;
+
+ return 0;
+@@ -3954,6 +3963,7 @@ static int mxl862xx_port_bridge_flags(st
+ unsigned long block = old_block;
+ bool need_update = false;
+ int ret;
++ u16 bp;
+
+ if (flags.mask & BR_FLOOD) {
+ if (flags.val & BR_FLOOD)
+@@ -3988,6 +3998,24 @@ static int mxl862xx_port_bridge_flags(st
+ ret = mxl862xx_set_bridge_port(ds, port);
+ if (ret)
+ return ret;
++ }
++
++ if (flags.mask & BR_HAIRPIN_MODE) {
++ bp = mxl862xx_lag_bridge_port(priv, port);
++ priv->ports[port].hairpin = !!(flags.val & BR_HAIRPIN_MODE);
++
++ /* Hairpin adds/removes the port's own bridge port from its
++ * cached portmap. Only this port is affected -- push the
++ * updated portmap directly.
++ */
++ if (flags.val & BR_HAIRPIN_MODE)
++ __set_bit(bp, priv->ports[port].portmap);
++ else
++ __clear_bit(bp, priv->ports[port].portmap);
++
++ ret = mxl862xx_set_bridge_port(ds, port);
++ if (ret)
++ return ret;
+ }
+
+ return 0;
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -241,6 +241,10 @@ struct mxl862xx_port_stats {
+ * @stats_lock: protects accumulator reads in .get_stats64 against
+ * concurrent updates from the polling work
+ * @tag_8021q_vid: currently assigned tag_8021q management VID
++ * @hairpin: true when hairpin mode is active (BR_HAIRPIN_MODE);
++ * the port's own bridge port is included in its
++ * portmap so bridged frames can egress the ingress
++ * port
+ * @ingress_mirror: true when ingress mirroring is active on this port
+ * @egress_mirror: true when egress mirroring is active on this port
+ * @lag: non-NULL when port is member of a LAG group;
+@@ -273,6 +277,8 @@ struct mxl862xx_port {
+ struct work_struct host_flood_work;
+ u16 tag_8021q_vid;
+ struct mxl862xx_evlan_block cpu_egress_evlan;
++ /* Hairpin state */
++ bool hairpin;
+ /* Mirror state */
+ bool ingress_mirror;
+ bool egress_mirror;
--- /dev/null
+From 74b6654ba74eb142340de4c51b97c0221cfcae37 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Wed, 25 Mar 2026 01:51:33 +0000
+Subject: [PATCH 33/35] net: dsa: mxl862xx: support BR_ISOLATED bridge flag
+
+Implement port isolation by excluding isolated ports from each other's
+forwarding portmaps in sync_bridge_members. Non-isolated ports can
+still reach isolated ports and vice versa -- only isolated-to-isolated
+forwarding is blocked.
+
+When the isolation state changes, all bridge members' portmaps are
+rebuilt via sync_bridge_members since multiple ports are affected.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 26 +++++++++++++++++++++++++-
+ drivers/net/dsa/mxl862xx/mxl862xx.h | 4 ++++
+ 2 files changed, 29 insertions(+), 1 deletion(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -802,6 +802,14 @@ static int mxl862xx_sync_bridge_members(
+ */
+ if (!mxl862xx_is_lag_master(priv, member))
+ continue;
++
++ /* Isolated ports cannot forward to each other.
++ * Non-isolated ports can reach everyone.
++ */
++ if (priv->ports[port].isolated &&
++ priv->ports[member].isolated)
++ continue;
++
+ if (member != port) {
+ bp = mxl862xx_lag_bridge_port(priv,
+ member);
+@@ -3948,7 +3956,7 @@ static int mxl862xx_port_pre_bridge_flag
+ struct netlink_ext_ack *extack)
+ {
+ if (flags.mask & ~(BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD |
+- BR_LEARNING | BR_HAIRPIN_MODE))
++ BR_LEARNING | BR_HAIRPIN_MODE | BR_ISOLATED))
+ return -EINVAL;
+
+ return 0;
+@@ -3962,6 +3970,7 @@ static int mxl862xx_port_bridge_flags(st
+ unsigned long old_block = priv->ports[port].flood_block;
+ unsigned long block = old_block;
+ bool need_update = false;
++ struct dsa_port *dp;
+ int ret;
+ u16 bp;
+
+@@ -4018,6 +4027,21 @@ static int mxl862xx_port_bridge_flags(st
+ return ret;
+ }
+
++ if (flags.mask & BR_ISOLATED) {
++ dp = dsa_to_port(ds, port);
++ priv->ports[port].isolated = !!(flags.val & BR_ISOLATED);
++
++ /* Isolation affects all bridge members' portmaps:
++ * isolated ports must be removed from each other's
++ * portmaps. Rebuild all portmaps for this bridge.
++ */
++ if (dp->bridge) {
++ ret = mxl862xx_sync_bridge_members(ds, dp->bridge);
++ if (ret)
++ return ret;
++ }
++ }
++
+ return 0;
+ }
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
+@@ -214,6 +214,9 @@ struct mxl862xx_port_stats {
+ * @flood_block: bitmask of firmware meter indices that are currently
+ * rate-limiting flood traffic on this port (zero-rate
+ * meters used to block flooding)
++ * @isolated: true when port isolation is active (BR_ISOLATED);
++ * isolated ports are excluded from each other's
++ * forwarding portmaps
+ * @learning: true when address learning is enabled on this port
+ * @setup_done: set at end of port_setup, cleared at start of
+ * port_teardown; guards deferred work against
+@@ -261,6 +264,7 @@ struct mxl862xx_port {
+ u16 fid;
+ DECLARE_BITMAP(portmap, MXL862XX_MAX_BRIDGE_PORTS);
+ unsigned long flood_block;
++ bool isolated;
+ bool learning;
+ bool setup_done;
+ /* VLAN state */
--- /dev/null
+From 0902a6790750714445c75a66d60f1bc4897126ce Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 18:17:49 +0000
+Subject: [PATCH 34/35] DO NOT SUBMIT: net: dsa: mxl862xx: re-introduce PCE
+ workaround for old firmware
+
+Re-introduce the mxl862xx_disable_fw_global_rules() function that
+disables firmware default global PCE rules for firmware versions
+older than 1.0.80. The upstream submission replaced this with a
+dev_warn() since firmware >= 1.0.80 no longer installs these rules,
+but downstream deployments may still run older firmware.
+
+This commit is for downstream use only and must not be submitted
+upstream.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx.c | 45 +++++++++++++++++++++++++++--
+ 1 file changed, 42 insertions(+), 3 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
+@@ -477,6 +477,43 @@ static int mxl862xx_setup_drop_meter(str
+ return MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
+ }
+
++/* Disable firmware global PCE rules that trap various protocols to the
++ * on-die microcontroller (port 0) via PORTMAP_CPU. Under DSA, these
++ * frames must either reach the host CPU via per-port rules (link-local)
++ * or through the normal bridge forwarding path (ARP broadcast), so the
++ * global firmware rules are not needed. With the microcontroller port
++ * disabled they would silently drop matching traffic.
++ *
++ * Global rules have lower indices than CTP rules, hence higher priority
++ * in the PCE pipeline -- they must be explicitly disabled or they will
++ * shadow the per-CTP traps.
++ *
++ * Indices from gsw_flow_index.h:
++ * 1 -- BPDU (STP/RSTP, dst 01:80:c2:00:00:00)
++ * 3 -- LLDP (EtherType 0x88cc)
++ * 4 -- OAM/LACP (EtherType 0x8809)
++ * 6 -- System MAC (dst 02:e0:92:00:00:01, vendor management MAC)
++ * 7 -- ARP Request (broadcast + EtherType 0x0806 + TPA 192.0.2.1)
++ */
++static int mxl862xx_disable_fw_global_rules(struct dsa_switch *ds)
++{
++ static const u16 indices[] = { 1, 3, 4, 6, 7 };
++ struct mxl862xx_pce_rule rule;
++ int i, ret;
++
++ for (i = 0; i < ARRAY_SIZE(indices); i++) {
++ memset(&rule, 0, sizeof(rule));
++ rule.pattern.index = cpu_to_le16(indices[i]);
++ /* pattern.enable == 0 -> rule is disabled */
++
++ ret = MXL862XX_API_WRITE(ds->priv,
++ MXL862XX_TFLOW_PCERULEWRITE, rule);
++ if (ret)
++ return ret;
++ }
++
++ return 0;
++}
+
+ /* Per-CTP offset used for the link-local trap rule. Each port's CTP
+ * flow-table block is pre-allocated by the firmware during init (44
+@@ -1154,9 +1191,11 @@ static int mxl862xx_setup(struct dsa_swi
+ if (ret)
+ return ret;
+
+- if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
+- dev_warn(ds->dev, "firmware < 1.0.80 installs global PCE rules "
+- "that interfere with DSA operation, please update\n");
++ if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80)) {
++ ret = mxl862xx_disable_fw_global_rules(ds);
++ if (ret)
++ return ret;
++ }
+
+ /* Pre-allocate firmware resources for all ports. The DSA core
+ * calls change_tag_protocol() between setup() and port_setup(),
--- /dev/null
+From 0ac876d5b952218ab79ea0a0815cf6fd1290b1d0 Mon Sep 17 00:00:00 2001
+From: Daniel Golle <daniel@makrotopia.org>
+Date: Tue, 24 Mar 2026 18:19:56 +0000
+Subject: [PATCH 35/35] DO NOT SUBMIT: net: dsa: mxl862xx: legacy SFP API
+ fallback for old firmware
+
+Re-introduce the SYS_MISC_SFP_SET-based PCS implementation as a
+fallback for firmware versions older than 1.0.80 which lack the
+XPCS API. mxl862xx_setup_pcs() selects between the XPCS ops and
+legacy SFP ops based on firmware version.
+
+This commit is for downstream use only and must not be submitted
+upstream.
+
+Signed-off-by: Daniel Golle <daniel@makrotopia.org>
+---
+ drivers/net/dsa/mxl862xx/mxl862xx-api.h | 22 +++
+ drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 1 +
+ drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 160 +++++++++++++++++++-
+ 3 files changed, 178 insertions(+), 5 deletions(-)
+
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+@@ -2400,6 +2400,28 @@ struct mxl862xx_sys_fw_image_version {
+ } __packed;
+
+ /**
++ * struct mxl862xx_sys_sfp_cfg - legacy SFP/SerDes port configuration
++ * @port_id: port id (0 or 1)
++ * @option: config options (0 - SFP mode/speed/link-status, 1 - flow control)
++ * @mode: SFP mode (0 - auto, 1 - fix, 2 - disable)
++ * @speed: select speed when mode is 1
++ * @link: get link state
++ * @fc_en: flow control (0 - disable, 1 - enable)
++ */
++struct mxl862xx_sys_sfp_cfg {
++ u8 port_id:4;
++ u8 option:4;
++ union {
++ struct {
++ u8 mode;
++ u8 speed;
++ u8 link;
++ };
++ u8 fc_en;
++ };
++} __packed;
++
++/**
+ * enum mxl862xx_rmon_port_type - RMON counter table type
+ * @MXL862XX_RMON_CTP_PORT_RX: CTP RX counters
+ * @MXL862XX_RMON_CTP_PORT_TX: CTP TX counters
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+@@ -85,6 +85,7 @@
+
+ #define SYS_MISC_FW_UPDATE (SYS_MISC_MAGIC + 0x1)
+ #define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2)
++#define SYS_MISC_SFP_SET (SYS_MISC_MAGIC + 0xe)
+
+ #define MXL862XX_XPCS_MAGIC 0x1a00
+ #define MXL862XX_XPCS_PCS_CONFIG (MXL862XX_XPCS_MAGIC + 0x1)
+--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
++++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+@@ -52,6 +52,155 @@ static struct mxl862xx_pcs *pcs_to_mxl86
+ return container_of(pcs, struct mxl862xx_pcs, pcs);
+ }
+
++/* Legacy SFP-based PCS implementation for firmware < 1.0.80 */
++static int mxl862xx_legacy_pcs_config(struct phylink_pcs *pcs,
++ unsigned int neg_mode,
++ phy_interface_t interface,
++ const unsigned long *advertising,
++ bool permit_pause_to_mac)
++{
++ struct mxl862xx_priv *priv = pcs_to_mxl862xx_pcs(pcs)->priv;
++ int port = pcs_to_mxl862xx_pcs(pcs)->port;
++ struct mxl862xx_sys_sfp_cfg ser_intf = {
++ .option = 0,
++ .mode = 1,
++ };
++
++ if (port != 9 && port != 13)
++ return 0;
++
++ if (port == 9)
++ ser_intf.port_id = 0;
++ else
++ ser_intf.port_id = 1;
++
++ switch (interface) {
++ case PHY_INTERFACE_MODE_SGMII:
++ ser_intf.speed = 8;
++ break;
++ case PHY_INTERFACE_MODE_1000BASEX:
++ ser_intf.speed = (neg_mode & PHYLINK_PCS_NEG_INBAND) ? 1 : 7;
++ break;
++ case PHY_INTERFACE_MODE_2500BASEX:
++ ser_intf.speed = 4;
++ break;
++ case PHY_INTERFACE_MODE_10GBASER:
++ ser_intf.speed = 2;
++ break;
++ case PHY_INTERFACE_MODE_USXGMII:
++ ser_intf.speed = 3;
++ break;
++ default:
++ dev_err(priv->ds->dev, "unsupported interface: %s\n",
++ phy_modes(interface));
++ return -EINVAL;
++ }
++
++ return MXL862XX_API_WRITE(priv, SYS_MISC_SFP_SET, ser_intf);
++}
++
++static void mxl862xx_legacy_pcs_get_state(struct phylink_pcs *pcs,
++ struct phylink_link_state *state)
++{
++ struct mxl862xx_priv *priv = pcs_to_mxl862xx_pcs(pcs)->priv;
++ int port = pcs_to_mxl862xx_pcs(pcs)->port;
++ struct mxl862xx_port_link_cfg port_link_cfg = {
++ .port_id = port,
++ };
++ struct mxl862xx_port_cfg port_cfg = {
++ .port_id = port,
++ };
++ int ret;
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_COMMON_PORTLINKCFGGET,
++ port_link_cfg);
++ if (ret)
++ return;
++
++ ret = MXL862XX_API_READ(priv, MXL862XX_COMMON_PORTCFGGET, port_cfg);
++ if (ret)
++ return;
++
++ state->link = (port_link_cfg.link == MXL862XX_PORT_LINK_UP);
++ state->an_complete = state->link;
++
++ switch (port_link_cfg.speed) {
++ case MXL862XX_PORT_SPEED_10:
++ state->speed = SPEED_10;
++ break;
++ case MXL862XX_PORT_SPEED_100:
++ state->speed = SPEED_100;
++ break;
++ case MXL862XX_PORT_SPEED_1000:
++ state->speed = SPEED_1000;
++ break;
++ case MXL862XX_PORT_SPEED_2500:
++ state->speed = SPEED_2500;
++ break;
++ case MXL862XX_PORT_SPEED_5000:
++ state->speed = SPEED_5000;
++ break;
++ case MXL862XX_PORT_SPEED_10000:
++ state->speed = SPEED_10000;
++ break;
++ default:
++ state->speed = SPEED_UNKNOWN;
++ break;
++ }
++
++ switch (port_link_cfg.duplex) {
++ case MXL862XX_DUPLEX_HALF:
++ state->duplex = DUPLEX_HALF;
++ break;
++ case MXL862XX_DUPLEX_FULL:
++ state->duplex = DUPLEX_FULL;
++ break;
++ default:
++ state->duplex = DUPLEX_UNKNOWN;
++ break;
++ }
++
++ state->pause &= ~(MLO_PAUSE_RX | MLO_PAUSE_TX);
++ switch (port_cfg.flow_ctrl) {
++ case MXL862XX_FLOW_RXTX:
++ state->pause |= MLO_PAUSE_TXRX_MASK;
++ break;
++ case MXL862XX_FLOW_TX:
++ state->pause |= MLO_PAUSE_TX;
++ break;
++ case MXL862XX_FLOW_RX:
++ state->pause |= MLO_PAUSE_RX;
++ break;
++ case MXL862XX_FLOW_OFF:
++ default:
++ break;
++ }
++}
++
++static unsigned int
++mxl862xx_legacy_pcs_inband_caps(struct phylink_pcs *pcs,
++ phy_interface_t interface)
++{
++ switch (interface) {
++ case PHY_INTERFACE_MODE_SGMII:
++ case PHY_INTERFACE_MODE_USXGMII:
++ return LINK_INBAND_ENABLE;
++ case PHY_INTERFACE_MODE_1000BASEX:
++ return LINK_INBAND_DISABLE | LINK_INBAND_ENABLE;
++ case PHY_INTERFACE_MODE_10GBASER:
++ case PHY_INTERFACE_MODE_2500BASEX:
++ return LINK_INBAND_DISABLE;
++ default:
++ return 0;
++ }
++}
++
++static const struct phylink_pcs_ops mxl862xx_legacy_pcs_ops = {
++ .pcs_get_state = mxl862xx_legacy_pcs_get_state,
++ .pcs_config = mxl862xx_legacy_pcs_config,
++ .pcs_inband_caps = mxl862xx_legacy_pcs_inband_caps,
++};
++
+ static int mxl862xx_xpcs_port_id(int port)
+ {
+ return port >= 13;
+@@ -389,7 +538,10 @@ void mxl862xx_setup_pcs(struct mxl862xx_
+ pcs->priv = priv;
+ pcs->port = port;
+
+- pcs->pcs.ops = &mxl862xx_pcs_ops;
++ if (MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
++ pcs->pcs.ops = &mxl862xx_pcs_ops;
++ else
++ pcs->pcs.ops = &mxl862xx_legacy_pcs_ops;
+ pcs->pcs.poll = true;
+ }
+
+@@ -401,9 +553,6 @@ mxl862xx_phylink_mac_select_pcs(struct p
+ struct mxl862xx_priv *priv = dp->ds->priv;
+ int port = dp->index;
+
+- if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
+- return NULL;
+-
+ switch (port) {
+ case 9 ... 16:
+ return &priv->serdes_ports[port - 9].pcs;
+@@ -534,7 +683,7 @@ void mxl862xx_serdes_get_stats(struct ds
+ }
+
+ void mxl862xx_serdes_self_test(struct dsa_switch *ds, int port,
+- struct ethtool_test *etest, u64 *data)
++ struct ethtool_test *etest, u64 *data)
+ {
+ struct mxl862xx_xpcs_prbs_cfg prbs = {};
+ struct mxl862xx_xpcs_bert_cfg bert = {};
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
-@@ -55,6 +55,7 @@ struct tc_action;
- #define DSA_TAG_PROTO_LAN937X_VALUE 27
- #define DSA_TAG_PROTO_VSC73XX_8021Q_VALUE 28
+@@ -57,6 +57,7 @@ struct tc_action;
#define DSA_TAG_PROTO_BRCM_LEGACY_FCS_VALUE 29
-+#define DSA_TAG_PROTO_OOB_VALUE 30
+ #define DSA_TAG_PROTO_MXL862_VALUE 30
+ #define DSA_TAG_PROTO_MXL862_8021Q_VALUE 31
++#define DSA_TAG_PROTO_OOB_VALUE 32
+
enum dsa_tag_protocol {
- DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
-@@ -87,6 +88,7 @@ enum dsa_tag_protocol {
- DSA_TAG_PROTO_RZN1_A5PSW = DSA_TAG_PROTO_RZN1_A5PSW_VALUE,
- DSA_TAG_PROTO_LAN937X = DSA_TAG_PROTO_LAN937X_VALUE,
+@@ -92,6 +93,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_VSC73XX_8021Q = DSA_TAG_PROTO_VSC73XX_8021Q_VALUE,
+ DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
+ DSA_TAG_PROTO_MXL862_8021Q = DSA_TAG_PROTO_MXL862_8021Q_VALUE,
+ DSA_TAG_PROTO_OOB = DSA_TAG_PROTO_OOB_VALUE,
};
static __always_inline unsigned int skb_ext_total_length(void)
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
-@@ -131,6 +131,15 @@ config NET_DSA_TAG_OCELOT_8021Q
+@@ -145,6 +145,15 @@ config NET_DSA_TAG_OCELOT_8021Q
this mode, less TCAM resources (VCAP IS1, IS2, ES0) are available for
use with tc-flower.
help
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
-@@ -31,6 +31,7 @@ obj-$(CONFIG_NET_DSA_TAG_MTK) += tag_mtk
+@@ -33,6 +33,7 @@ obj-$(CONFIG_NET_DSA_TAG_MXL_862XX_8021Q
obj-$(CONFIG_NET_DSA_TAG_NONE) += tag_none.o
obj-$(CONFIG_NET_DSA_TAG_OCELOT) += tag_ocelot.o
obj-$(CONFIG_NET_DSA_TAG_OCELOT_8021Q) += tag_ocelot_8021q.o
struct dsa_chip_data {
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
-@@ -475,7 +475,7 @@ struct dsa_switch {
+@@ -480,7 +480,7 @@ struct dsa_switch {
/*
* User mii_bus and devices for the individual ports.
*/
struct mii_bus *user_mii_bus;
/* Ageing Time limits in msecs */
-@@ -611,24 +611,24 @@ static inline bool dsa_is_user_port(stru
+@@ -616,24 +616,24 @@ static inline bool dsa_is_user_port(stru
dsa_switch_for_each_port_continue_reverse((_dp), (_ds)) \
if (dsa_port_is_cpu((_dp)))
--- a/drivers/net/phy/sfp.c
+++ b/drivers/net/phy/sfp.c
-@@ -728,10 +728,64 @@ static int sfp_i2c_write(struct sfp *sfp
+@@ -729,10 +729,64 @@ static int sfp_i2c_write(struct sfp *sfp
return ret == ARRAY_SIZE(msgs) ? len : 0;
}
sfp->i2c = i2c;
sfp->read = sfp_i2c_read;
-@@ -763,6 +817,29 @@ static int sfp_i2c_mdiobus_create(struct
+@@ -764,6 +818,29 @@ static int sfp_i2c_mdiobus_create(struct
return 0;
}
static void sfp_i2c_mdiobus_destroy(struct sfp *sfp)
{
mdiobus_unregister(sfp->i2c_mii);
-@@ -1937,9 +2014,15 @@ static void sfp_sm_fault(struct sfp *sfp
+@@ -1938,9 +2015,15 @@ static void sfp_sm_fault(struct sfp *sfp
static int sfp_sm_add_mdio_bus(struct sfp *sfp)
{
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
-@@ -89,6 +89,8 @@ source "drivers/net/dsa/xrs700x/Kconfig"
+@@ -91,6 +91,8 @@ source "drivers/net/dsa/xrs700x/Kconfig"
source "drivers/net/dsa/realtek/Kconfig"
depends on OF && ARCH_RZN1
--- a/drivers/net/dsa/Makefile
+++ b/drivers/net/dsa/Makefile
-@@ -25,5 +25,6 @@ obj-y += mv88e6xxx/
+@@ -26,5 +26,6 @@ obj-y += mxl862xx/
obj-y += ocelot/
obj-y += qca/
obj-y += realtek/
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
-@@ -35,6 +35,7 @@ obj-$(CONFIG_NET_DSA_TAG_QCA) += tag_qca
+@@ -37,6 +37,7 @@ obj-$(CONFIG_NET_DSA_TAG_QCA) += tag_qca
obj-$(CONFIG_NET_DSA_TAG_RTL4_A) += tag_rtl4_a.o
obj-$(CONFIG_NET_DSA_TAG_RTL8_4) += tag_rtl8_4.o
obj-$(CONFIG_NET_DSA_TAG_RZN1_A5PSW) += tag_rzn1_a5psw.o
obj-$(CONFIG_NET_DSA_TAG_VSC73XX_8021Q) += tag_vsc73xx_8021q.o
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
-@@ -163,6 +163,12 @@ config NET_DSA_TAG_LAN9303
+@@ -177,6 +177,12 @@ config NET_DSA_TAG_LAN9303
Say Y or M if you want to enable support for tagging frames for the
SMSC/Microchip LAN9303 family of switches.
select PACKING
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
-@@ -55,6 +55,7 @@ struct tc_action;
- #define DSA_TAG_PROTO_LAN937X_VALUE 27
- #define DSA_TAG_PROTO_VSC73XX_8021Q_VALUE 28
+@@ -57,6 +57,7 @@ struct tc_action;
#define DSA_TAG_PROTO_BRCM_LEGACY_FCS_VALUE 29
-+#define DSA_TAG_PROTO_RTL_OTTO_VALUE 30
+ #define DSA_TAG_PROTO_MXL862_VALUE 30
+ #define DSA_TAG_PROTO_MXL862_8021Q_VALUE 31
++#define DSA_TAG_PROTO_RTL_OTTO_VALUE 32
+
enum dsa_tag_protocol {
- DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
-@@ -87,6 +88,7 @@ enum dsa_tag_protocol {
- DSA_TAG_PROTO_RZN1_A5PSW = DSA_TAG_PROTO_RZN1_A5PSW_VALUE,
- DSA_TAG_PROTO_LAN937X = DSA_TAG_PROTO_LAN937X_VALUE,
+@@ -92,6 +93,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_VSC73XX_8021Q = DSA_TAG_PROTO_VSC73XX_8021Q_VALUE,
+ DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
+ DSA_TAG_PROTO_MXL862_8021Q = DSA_TAG_PROTO_MXL862_8021Q_VALUE,
+ DSA_TAG_PROTO_RTL_OTTO = DSA_TAG_PROTO_RTL_OTTO_VALUE,
};