]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
kernel: add DSA driver for MaxLinear MxL862xx switches 22612/head
authorDaniel Golle <daniel@makrotopia.org>
Thu, 26 Mar 2026 04:00:28 +0000 (04:00 +0000)
committerDaniel Golle <daniel@makrotopia.org>
Fri, 27 Mar 2026 15:49:23 +0000 (15:49 +0000)
Backport upstream driver and apply pending downstream patches to
support using the MaxLinear MxL86252 and MxL86282 switches.

The driver supports a native proprietary 8-byte DSA special tag format
(mxl862xx) as well as using an 802.1Q-based DSA tag (mxl862xx-8021q).

All basic bridge, VLAN and LAG operations are supported. A single port
can be used as mirror port. Hardware counters are made available as
ethtool stats or directly serve as interface counters (bytes,
packets).

The switch runs a complex ZephyrOS-based firmware on an integrated
ARC microcontroller, the driver uses the firmware management API over
MDIO to interact with the switch hardware.

Note that the firmware needs to be rather recent (WSP 1.0.78 or later)
to work well with this driver. It can be updated at runtime using devlink.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
48 files changed:
package/kernel/linux/modules/netdevices.mk
target/linux/generic/backport-6.12/760-v6.18-net-phy-introduce-phy_id_compare_model-PHY-ID-helper.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/761-v6.19-net-phy-mxl-gpy-add-support-for-mxl86252-and-mxl86282.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/762-v7.0-net-dsa-add-tag-format-for-MxL862xx-switches.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/763-v7.0-net-mdio-add-unlocked-mdiodev-C45-bus-accessors.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/764-v7.0-net-dsa-add-basic-initial-driver-for-MxL862xx-switch.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/765-v7.0-net-dsa-mxl862xx-rename-MDIO-op-arguments.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/766-v7.0-net-dsa-mxl862xx-don-t-set-user_mii_bus.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/767-v7.0-net-dsa-MxL862xx-don-t-force-enable-MAXLINEAR_GPHY.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/768-v7.1-net-dsa-MxL862xx-don-t-force-enable-MAXLINEAR_GPHY.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/769-v7.1-net-dsa-mxl862xx-add-CRC-for-MDIO-communication.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/770-v7.1-net-dsa-mxl862xx-use-RST_DATA-to-skip-writing-zero-w.patch [new file with mode: 0644]
target/linux/generic/backport-6.12/782-04-v6.16-net-phy-introduce-genphy_match_phy_device.patch
target/linux/generic/backport-6.12/783-01-v6.18-net-dsa-Move-KS8995-to-the-DSA-subsystem.patch
target/linux/generic/backport-6.12/783-04-v6.18-net-dsa-ks8995-Add-basic-switch-set-up.patch
target/linux/generic/backport-6.12/786-01-v6.18-net-phy-introduce-phy_id_compare_vendor-PHY-ID-helpe.patch
target/linux/generic/pending-6.12/750-net-sfp-add-quirk-for-QINIYEK-BJ-SFP-10G-T-copper-SF.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-01-net-dsa-move-dsa_bridge_ports-helper-to-dsa.h.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-02-net-dsa-add-bridge-member-iteration-macro.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-03-dsa-tag_mxl862xx-set-dsa_default_offload_fwd_mark.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-04-net-dsa-mxl862xx-implement-bridge-offloading.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-05-net-dsa-mxl862xx-implement-VLAN-functionality.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-06-net-dsa-mxl862xx-add-ethtool-statistics-support.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-07-net-dsa-mxl862xx-implement-.get_stats64.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-08-net-dsa-mxl862xx-store-firmware-version-for-feature-.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-09-net-dsa-mxl862xx-move-phylink-stubs-to-mxl862xx-phyl.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-10-net-dsa-mxl862xx-move-API-macros-to-mxl862xx-host.h.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-11-net-dsa-mxl862xx-add-support-for-SerDes-ports.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-12-net-dsa-mxl862xx-add-SerDes-ethtool-statistics.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-13-net-dsa-mxl862xx-add-SerDes-self-test-via-PRBS-and-B.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-14-net-dsa-mxl862xx-trap-link-local-frames-to-the-CPU-p.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-15-net-dsa-mxl862xx-warn-about-old-firmware-default-PCE.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-16-net-dsa-add-802.1Q-VLAN-based-tag-driver-for-MxL862x.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-17-net-dsa-mxl862xx-add-link-aggregation-support.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-18-net-dsa-mxl862xx-add-support-for-mirror-port.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-19-net-dsa-wire-flash_update-devlink-callback-to-driver.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-20-net-dsa-mxl862xx-add-SMDIO-clause-22-register-access.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-21-net-dsa-mxl862xx-add-devlink-flash_update-and-info_g.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-22-net-dsa-mxl862xx-implement-port-MTU-configuration.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-23-net-dsa-mxl862xx-support-BR_HAIRPIN_MODE-bridge-flag.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-24-net-dsa-mxl862xx-support-BR_ISOLATED-bridge-flag.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-25-DO-NOT-SUBMIT-net-dsa-mxl862xx-re-introduce-PCE-work.patch [new file with mode: 0644]
target/linux/generic/pending-6.12/760-26-DO-NOT-SUBMIT-net-dsa-mxl862xx-legacy-SFP-API-fallba.patch [new file with mode: 0644]
target/linux/ipq40xx/patches-6.12/701-net-dsa-add-out-of-band-tagging-protocol.patch
target/linux/realtek/patches-6.12/700-dsa-mdio-increase-max-ports-for-rtl839x-rtl931x.patch
target/linux/realtek/patches-6.12/714-net-phy-sfp-add-support-for-SMBus.patch
target/linux/realtek/patches-6.12/718-net-dsa-add-support-for-rtl838x-switch.patch
target/linux/realtek/patches-6.12/721-net-dsa-add-support-for-tag-rtl-otto.patch

index 3dc28952d50ae48344d0cdabbe8f324ac31a69d0..cae14f3614fb935aee77555b164f82d018786bee 100644 (file)
@@ -804,6 +804,23 @@ endef
 
 $(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
diff --git a/target/linux/generic/backport-6.12/760-v6.18-net-phy-introduce-phy_id_compare_model-PHY-ID-helper.patch b/target/linux/generic/backport-6.12/760-v6.18-net-phy-introduce-phy_id_compare_model-PHY-ID-helper.patch
new file mode 100644 (file)
index 0000000..a367f66
--- /dev/null
@@ -0,0 +1,39 @@
+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
diff --git a/target/linux/generic/backport-6.12/761-v6.19-net-phy-mxl-gpy-add-support-for-mxl86252-and-mxl86282.patch b/target/linux/generic/backport-6.12/761-v6.19-net-phy-mxl-gpy-add-support-for-mxl86252-and-mxl86282.patch
new file mode 100644 (file)
index 0000000..87b3e36
--- /dev/null
@@ -0,0 +1,163 @@
+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);
diff --git a/target/linux/generic/backport-6.12/762-v7.0-net-dsa-add-tag-format-for-MxL862xx-switches.patch b/target/linux/generic/backport-6.12/762-v7.0-net-dsa-add-tag-format-for-MxL862xx-switches.patch
new file mode 100644 (file)
index 0000000..94fb211
--- /dev/null
@@ -0,0 +1,191 @@
+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 */
diff --git a/target/linux/generic/backport-6.12/763-v7.0-net-mdio-add-unlocked-mdiodev-C45-bus-accessors.patch b/target/linux/generic/backport-6.12/763-v7.0-net-mdio-add-unlocked-mdiodev-C45-bus-accessors.patch
new file mode 100644 (file)
index 0000000..e05dd1f
--- /dev/null
@@ -0,0 +1,41 @@
+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)
+ {
diff --git a/target/linux/generic/backport-6.12/764-v7.0-net-dsa-add-basic-initial-driver-for-MxL862xx-switch.patch b/target/linux/generic/backport-6.12/764-v7.0-net-dsa-add-basic-initial-driver-for-MxL862xx-switch.patch
new file mode 100644 (file)
index 0000000..247c698
--- /dev/null
@@ -0,0 +1,1583 @@
+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 */
diff --git a/target/linux/generic/backport-6.12/765-v7.0-net-dsa-mxl862xx-rename-MDIO-op-arguments.patch b/target/linux/generic/backport-6.12/765-v7.0-net-dsa-mxl862xx-rename-MDIO-op-arguments.patch
new file mode 100644 (file)
index 0000000..b478806
--- /dev/null
@@ -0,0 +1,94 @@
+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)
diff --git a/target/linux/generic/backport-6.12/766-v7.0-net-dsa-mxl862xx-don-t-set-user_mii_bus.patch b/target/linux/generic/backport-6.12/766-v7.0-net-dsa-mxl862xx-don-t-set-user_mii_bus.patch
new file mode 100644 (file)
index 0000000..69c8ecc
--- /dev/null
@@ -0,0 +1,31 @@
+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;
diff --git a/target/linux/generic/backport-6.12/767-v7.0-net-dsa-MxL862xx-don-t-force-enable-MAXLINEAR_GPHY.patch b/target/linux/generic/backport-6.12/767-v7.0-net-dsa-MxL862xx-don-t-force-enable-MAXLINEAR_GPHY.patch
new file mode 100644 (file)
index 0000000..724a9d3
--- /dev/null
@@ -0,0 +1,54 @@
+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;
+       }
diff --git a/target/linux/generic/backport-6.12/768-v7.1-net-dsa-MxL862xx-don-t-force-enable-MAXLINEAR_GPHY.patch b/target/linux/generic/backport-6.12/768-v7.1-net-dsa-MxL862xx-don-t-force-enable-MAXLINEAR_GPHY.patch
new file mode 100644 (file)
index 0000000..a2252d3
--- /dev/null
@@ -0,0 +1,39 @@
+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.
diff --git a/target/linux/generic/backport-6.12/769-v7.1-net-dsa-mxl862xx-add-CRC-for-MDIO-communication.patch b/target/linux/generic/backport-6.12/769-v7.1-net-dsa-mxl862xx-add-CRC-for-MDIO-communication.patch
new file mode 100644 (file)
index 0000000..8f65bea
--- /dev/null
@@ -0,0 +1,585 @@
+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 */
diff --git a/target/linux/generic/backport-6.12/770-v7.1-net-dsa-mxl862xx-use-RST_DATA-to-skip-writing-zero-w.patch b/target/linux/generic/backport-6.12/770-v7.1-net-dsa-mxl862xx-use-RST_DATA-to-skip-writing-zero-w.patch
new file mode 100644 (file)
index 0000000..dc21f10
--- /dev/null
@@ -0,0 +1,93 @@
+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);
index 1ac3b2333ff81f0da6f3dfd33c9e84a7c5a81a4f..fed9d3c11fa3469c72d55968096bb2950e634da3 100644 (file)
@@ -97,7 +97,7 @@ Signed-off-by: Jakub Kicinski <kuba@kernel.org>
  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);
  
index 204595693fe894905a625a8aea260d00d6848b50..b91c0cd8cfddd9a3cfdef446bf04877a8b3b00ee 100644 (file)
@@ -28,7 +28,7 @@ Signed-off-by: Jakub Kicinski <kuba@kernel.org>
 
 --- 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.
  
index 670fe61252a51cf16b8aae4653f22d6084f840a8..3454e113baabb310ae38a4876b2fc1d415e50825 100644 (file)
@@ -51,7 +51,7 @@ Signed-off-by: Jakub Kicinski <kuba@kernel.org>
 
 --- 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
index 82e9e0695acb52117ea271dd9b7179768c1fe2ed..a69a5641485fea6a9fc9b91218da3895056c4136 100644 (file)
@@ -36,7 +36,7 @@ Signed-off-by: Jakub Kicinski <kuba@kernel.org>
  
  /**
   * 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(
  }
  
  /**
diff --git a/target/linux/generic/pending-6.12/750-net-sfp-add-quirk-for-QINIYEK-BJ-SFP-10G-T-copper-SF.patch b/target/linux/generic/pending-6.12/750-net-sfp-add-quirk-for-QINIYEK-BJ-SFP-10G-T-copper-SF.patch
new file mode 100644 (file)
index 0000000..fc2d26c
--- /dev/null
@@ -0,0 +1,26 @@
+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),
diff --git a/target/linux/generic/pending-6.12/760-01-net-dsa-move-dsa_bridge_ports-helper-to-dsa.h.patch b/target/linux/generic/pending-6.12/760-01-net-dsa-move-dsa_bridge_ports-helper-to-dsa.h.patch
new file mode 100644 (file)
index 0000000..c0e77d1
--- /dev/null
@@ -0,0 +1,38 @@
+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)
+ {
diff --git a/target/linux/generic/pending-6.12/760-02-net-dsa-add-bridge-member-iteration-macro.patch b/target/linux/generic/pending-6.12/760-02-net-dsa-add-bridge-member-iteration-macro.patch
new file mode 100644 (file)
index 0000000..37891ff
--- /dev/null
@@ -0,0 +1,45 @@
+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;
+ }
diff --git a/target/linux/generic/pending-6.12/760-03-dsa-tag_mxl862xx-set-dsa_default_offload_fwd_mark.patch b/target/linux/generic/pending-6.12/760-03-dsa-tag_mxl862xx-set-dsa_default_offload_fwd_mark.patch
new file mode 100644 (file)
index 0000000..7c82499
--- /dev/null
@@ -0,0 +1,29 @@
+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.
+        */
diff --git a/target/linux/generic/pending-6.12/760-04-net-dsa-mxl862xx-implement-bridge-offloading.patch b/target/linux/generic/pending-6.12/760-04-net-dsa-mxl862xx-implement-bridge-offloading.patch
new file mode 100644 (file)
index 0000000..9d1c1c9
--- /dev/null
@@ -0,0 +1,1382 @@
+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(&param, 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 */
diff --git a/target/linux/generic/pending-6.12/760-05-net-dsa-mxl862xx-implement-VLAN-functionality.patch b/target/linux/generic/pending-6.12/760-05-net-dsa-mxl862xx-implement-VLAN-functionality.patch
new file mode 100644 (file)
index 0000000..1098087
--- /dev/null
@@ -0,0 +1,1652 @@
+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 */
diff --git a/target/linux/generic/pending-6.12/760-06-net-dsa-mxl862xx-add-ethtool-statistics-support.patch b/target/linux/generic/pending-6.12/760-06-net-dsa-mxl862xx-add-ethtool-statistics-support.patch
new file mode 100644 (file)
index 0000000..a1517cc
--- /dev/null
@@ -0,0 +1,383 @@
+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,
diff --git a/target/linux/generic/pending-6.12/760-07-net-dsa-mxl862xx-implement-.get_stats64.patch b/target/linux/generic/pending-6.12/760-07-net-dsa-mxl862xx-implement-.get_stats64.patch
new file mode 100644 (file)
index 0000000..9d4d93b
--- /dev/null
@@ -0,0 +1,325 @@
+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 */
diff --git a/target/linux/generic/pending-6.12/760-08-net-dsa-mxl862xx-store-firmware-version-for-feature-.patch b/target/linux/generic/pending-6.12/760-08-net-dsa-mxl862xx-store-firmware-version-for-feature-.patch
new file mode 100644 (file)
index 0000000..d62b466
--- /dev/null
@@ -0,0 +1,98 @@
+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;
diff --git a/target/linux/generic/pending-6.12/760-09-net-dsa-mxl862xx-move-phylink-stubs-to-mxl862xx-phyl.patch b/target/linux/generic/pending-6.12/760-09-net-dsa-mxl862xx-move-phylink-stubs-to-mxl862xx-phyl.patch
new file mode 100644 (file)
index 0000000..833ec06
--- /dev/null
@@ -0,0 +1,163 @@
+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;
diff --git a/target/linux/generic/pending-6.12/760-10-net-dsa-mxl862xx-move-API-macros-to-mxl862xx-host.h.patch b/target/linux/generic/pending-6.12/760-10-net-dsa-mxl862xx-move-API-macros-to-mxl862xx-host.h.patch
new file mode 100644 (file)
index 0000000..01013ee
--- /dev/null
@@ -0,0 +1,53 @@
+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.
diff --git a/target/linux/generic/pending-6.12/760-11-net-dsa-mxl862xx-add-support-for-SerDes-ports.patch b/target/linux/generic/pending-6.12/760-11-net-dsa-mxl862xx-add-support-for-SerDes-ports.patch
new file mode 100644 (file)
index 0000000..790f50c
--- /dev/null
@@ -0,0 +1,1053 @@
+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;
diff --git a/target/linux/generic/pending-6.12/760-12-net-dsa-mxl862xx-add-SerDes-ethtool-statistics.patch b/target/linux/generic/pending-6.12/760-12-net-dsa-mxl862xx-add-SerDes-ethtool-statistics.patch
new file mode 100644 (file)
index 0000000..5f1e1bb
--- /dev/null
@@ -0,0 +1,268 @@
+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,
diff --git a/target/linux/generic/pending-6.12/760-13-net-dsa-mxl862xx-add-SerDes-self-test-via-PRBS-and-B.patch b/target/linux/generic/pending-6.12/760-13-net-dsa-mxl862xx-add-SerDes-self-test-via-PRBS-and-B.patch
new file mode 100644 (file)
index 0000000..1da11ae
--- /dev/null
@@ -0,0 +1,208 @@
+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)
diff --git a/target/linux/generic/pending-6.12/760-14-net-dsa-mxl862xx-trap-link-local-frames-to-the-CPU-p.patch b/target/linux/generic/pending-6.12/760-14-net-dsa-mxl862xx-trap-link-local-frames-to-the-CPU-p.patch
new file mode 100644 (file)
index 0000000..385e44f
--- /dev/null
@@ -0,0 +1,831 @@
+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
diff --git a/target/linux/generic/pending-6.12/760-15-net-dsa-mxl862xx-warn-about-old-firmware-default-PCE.patch b/target/linux/generic/pending-6.12/760-15-net-dsa-mxl862xx-warn-about-old-firmware-default-PCE.patch
new file mode 100644 (file)
index 0000000..38dc45c
--- /dev/null
@@ -0,0 +1,32 @@
+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);
diff --git a/target/linux/generic/pending-6.12/760-16-net-dsa-add-802.1Q-VLAN-based-tag-driver-for-MxL862x.patch b/target/linux/generic/pending-6.12/760-16-net-dsa-add-802.1Q-VLAN-based-tag-driver-for-MxL862x.patch
new file mode 100644 (file)
index 0000000..8663883
--- /dev/null
@@ -0,0 +1,2466 @@
+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);
diff --git a/target/linux/generic/pending-6.12/760-17-net-dsa-mxl862xx-add-link-aggregation-support.patch b/target/linux/generic/pending-6.12/760-17-net-dsa-mxl862xx-add-link-aggregation-support.patch
new file mode 100644 (file)
index 0000000..d373f65
--- /dev/null
@@ -0,0 +1,862 @@
+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;
+ };
diff --git a/target/linux/generic/pending-6.12/760-18-net-dsa-mxl862xx-add-support-for-mirror-port.patch b/target/linux/generic/pending-6.12/760-18-net-dsa-mxl862xx-add-support-for-mirror-port.patch
new file mode 100644 (file)
index 0000000..faf69c0
--- /dev/null
@@ -0,0 +1,229 @@
+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;
+ };
diff --git a/target/linux/generic/pending-6.12/760-19-net-dsa-wire-flash_update-devlink-callback-to-driver.patch b/target/linux/generic/pending-6.12/760-19-net-dsa-wire-flash_update-devlink-callback-to-driver.patch
new file mode 100644 (file)
index 0000000..19da9a0
--- /dev/null
@@ -0,0 +1,57 @@
+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,
diff --git a/target/linux/generic/pending-6.12/760-20-net-dsa-mxl862xx-add-SMDIO-clause-22-register-access.patch b/target/linux/generic/pending-6.12/760-20-net-dsa-mxl862xx-add-SMDIO-clause-22-register-access.patch
new file mode 100644 (file)
index 0000000..17668b9
--- /dev/null
@@ -0,0 +1,72 @@
+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 */
diff --git a/target/linux/generic/pending-6.12/760-21-net-dsa-mxl862xx-add-devlink-flash_update-and-info_g.patch b/target/linux/generic/pending-6.12/760-21-net-dsa-mxl862xx-add-devlink-flash_update-and-info_g.patch
new file mode 100644 (file)
index 0000000..28b91f8
--- /dev/null
@@ -0,0 +1,569 @@
+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;
+ };
diff --git a/target/linux/generic/pending-6.12/760-22-net-dsa-mxl862xx-implement-port-MTU-configuration.patch b/target/linux/generic/pending-6.12/760-22-net-dsa-mxl862xx-implement-port-MTU-configuration.patch
new file mode 100644 (file)
index 0000000..1869a9f
--- /dev/null
@@ -0,0 +1,110 @@
+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;
diff --git a/target/linux/generic/pending-6.12/760-23-net-dsa-mxl862xx-support-BR_HAIRPIN_MODE-bridge-flag.patch b/target/linux/generic/pending-6.12/760-23-net-dsa-mxl862xx-support-BR_HAIRPIN_MODE-bridge-flag.patch
new file mode 100644 (file)
index 0000000..bf4d2f0
--- /dev/null
@@ -0,0 +1,106 @@
+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;
diff --git a/target/linux/generic/pending-6.12/760-24-net-dsa-mxl862xx-support-BR_ISOLATED-bridge-flag.patch b/target/linux/generic/pending-6.12/760-24-net-dsa-mxl862xx-support-BR_ISOLATED-bridge-flag.patch
new file mode 100644 (file)
index 0000000..a524d15
--- /dev/null
@@ -0,0 +1,95 @@
+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 */
diff --git a/target/linux/generic/pending-6.12/760-25-DO-NOT-SUBMIT-net-dsa-mxl862xx-re-introduce-PCE-work.patch b/target/linux/generic/pending-6.12/760-25-DO-NOT-SUBMIT-net-dsa-mxl862xx-re-introduce-PCE-work.patch
new file mode 100644 (file)
index 0000000..98cfb44
--- /dev/null
@@ -0,0 +1,81 @@
+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(),
diff --git a/target/linux/generic/pending-6.12/760-26-DO-NOT-SUBMIT-net-dsa-mxl862xx-legacy-SFP-API-fallba.patch b/target/linux/generic/pending-6.12/760-26-DO-NOT-SUBMIT-net-dsa-mxl862xx-legacy-SFP-API-fallba.patch
new file mode 100644 (file)
index 0000000..41cd927
--- /dev/null
@@ -0,0 +1,251 @@
+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 = {};
index 11101163baccdb296960773c96d6291c69617afc..3a4985e403bc25a3dda82bc919cdba4726269dc1 100644 (file)
@@ -105,18 +105,18 @@ Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
  
 --- 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,
  };
  
@@ -148,7 +148,7 @@ Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
  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.
  
@@ -166,7 +166,7 @@ Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
        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
index 4a71319aeac4b498f794f2da55e670cd2c157b2c..5f7e96358ac53cb10d2d9d1bbbb934413817409f 100644 (file)
@@ -70,7 +70,7 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
  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.
         */
@@ -79,7 +79,7 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
        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)))
  
index f31cddf0ba29c3cfbf7c958da1e55ee56b16c758..b6c56b300a4235681968a9214eebf9d98c0d2da8 100644 (file)
@@ -10,7 +10,7 @@ Signed-off-by: Antoine Tenart <antoine.tenart@bootlin.com>
 
 --- 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;
  }
  
@@ -77,7 +77,7 @@ Signed-off-by: Antoine Tenart <antoine.tenart@bootlin.com>
  
        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;
  }
  
@@ -107,7 +107,7 @@ Signed-off-by: Antoine Tenart <antoine.tenart@bootlin.com>
  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)
  {
index fdf0b30d43fdd6e9a65d87382f83691c7ae05eea..dc261efe36b36cefac7ebc8491b6234e471a571a 100644 (file)
@@ -22,7 +22,7 @@ Submitted-by: John Crispin <john@phrozen.org>
 
 --- 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"
  
@@ -33,7 +33,7 @@ Submitted-by: John Crispin <john@phrozen.org>
        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/
index 7ad71228fc7c814cb54c3d91fc3173a7d597d936..d82a05edc2a7f094872f488129d651fa655ff4ac 100644 (file)
@@ -8,7 +8,7 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
 
 --- 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
@@ -18,7 +18,7 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
  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.
  
@@ -33,18 +33,18 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
        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,
  };