diff --git a/package/kernel/linux/modules/netdevices.mk b/package/kernel/linux/modules/netdevices.mk index 3dc28952d50..cae14f3614f 100644 --- a/package/kernel/linux/modules/netdevices.mk +++ b/package/kernel/linux/modules/netdevices.mk @@ -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 index 00000000000..a367f66504d --- /dev/null +++ b/target/linux/generic/backport-6.12/760-v6.18-net-phy-introduce-phy_id_compare_model-PHY-ID-helper.patch @@ -0,0 +1,39 @@ +From ae1c658b33d4bec20c037aebba583a68375d4773 Mon Sep 17 00:00:00 2001 +From: Christian Marangi +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 +Reviewed-by: Florian Fainelli +Signed-off-by: Christian Marangi +Link: https://patch.msgid.link/20250911130840.23569-1-ansuelsmth@gmail.com +Signed-off-by: Jakub Kicinski +--- + 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 index 00000000000..87b3e3623d2 --- /dev/null +++ b/target/linux/generic/backport-6.12/761-v6.19-net-phy-mxl-gpy-add-support-for-mxl86252-and-mxl86282.patch @@ -0,0 +1,163 @@ +From de1e5c9333f426348571f7a3b034f99490d3f926 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Andrew Lunn +Link: https://patch.msgid.link/a6cd7fe461b011cec2b59dffaf34e9c8b0819059.1763818120.git.daniel@makrotopia.org +Signed-off-by: Jakub Kicinski +--- + 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 index 00000000000..94fb211a070 --- /dev/null +++ b/target/linux/generic/backport-6.12/762-v7.0-net-dsa-add-tag-format-for-MxL862xx-switches.patch @@ -0,0 +1,191 @@ +From 1ecc2ebd1298d5c0eaa238e71b7d2109d7d77538 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Link: https://patch.msgid.link/c64e6ddb6c93a4fac39f9ab9b2d8bf551a2b118d.1770433307.git.daniel@makrotopia.org +Reviewed-by: Vladimir Oltean +Signed-off-by: Paolo Abeni +--- + 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 ++ * Copyright (C) 2024 MaxLinear Inc. ++ */ ++ ++#include ++#include ++#include ++#include ++#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 index 00000000000..e05dd1fe3ca --- /dev/null +++ b/target/linux/generic/backport-6.12/763-v7.0-net-mdio-add-unlocked-mdiodev-C45-bus-accessors.patch @@ -0,0 +1,41 @@ +From 1111454d5a637e039a46b867088b524c73159da4 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Russell King (Oracle) +Link: https://patch.msgid.link/8d1d55949a75a871d2a3b90e421de4bd58d77685.1770433307.git.daniel@makrotopia.org +Reviewed-by: Vladimir Oltean +Signed-off-by: Paolo Abeni +--- + 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 index 00000000000..247c6981dab --- /dev/null +++ b/target/linux/generic/backport-6.12/764-v7.0-net-dsa-add-basic-initial-driver-for-MxL862xx-switch.patch @@ -0,0 +1,1583 @@ +From c764b51397f5f5919b07fdfbb9b70082059e1c16 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Link: https://patch.msgid.link/ccde07e8cf33d8ae243000013b57cfaa2695e0a9.1770433307.git.daniel@makrotopia.org +Reviewed-by: Vladimir Oltean +Signed-off-by: Paolo Abeni +--- + 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 ++ ++/** ++ * 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 ++ * Copyright (C) 2025 John Crispin ++ * Copyright (C) 2024 MaxLinear Inc. ++ */ ++ ++#include ++#include ++#include ++#include ++#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 ++ * Copyright (C) 2025 Daniel Golle ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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 ++#include ++ ++#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 index 00000000000..b4788069c6b --- /dev/null +++ b/target/linux/generic/backport-6.12/765-v7.0-net-dsa-mxl862xx-rename-MDIO-op-arguments.patch @@ -0,0 +1,94 @@ +From b5f8b39d22ab93cada5c88dc2cb6495b95f44c70 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Signed-off-by: Daniel Golle +Reviewed-by: Andrew Lunn +Link: https://patch.msgid.link/e1f4cb3bcffc7df9af0f2c9b673b14c7e1201c9a.1772507674.git.daniel@makrotopia.org +Signed-off-by: Jakub Kicinski +--- + 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 index 00000000000..69c8ecc545c --- /dev/null +++ b/target/linux/generic/backport-6.12/766-v7.0-net-dsa-mxl862xx-don-t-set-user_mii_bus.patch @@ -0,0 +1,31 @@ +From d0341efa8f5182cafe16506b9bef98184f4951fe Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Signed-off-by: Daniel Golle +Reviewed-by: Vladimir Oltean +Link: https://patch.msgid.link/0f0df310fd8cab57e0e5e3d0831dd057fd05bcd5.1773103271.git.daniel@makrotopia.org +Signed-off-by: Jakub Kicinski +--- + 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 index 00000000000..724a9d310a9 --- /dev/null +++ b/target/linux/generic/backport-6.12/767-v7.0-net-dsa-MxL862xx-don-t-force-enable-MAXLINEAR_GPHY.patch @@ -0,0 +1,54 @@ +From c0402837642625ef13ade862e20e229f4a5810f5 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Simon Horman +Link: https://patch.msgid.link/83356ad9c9a4470dd49b6b3d661c2a8dd85cc6a1.1773803190.git.daniel@makrotopia.org +Signed-off-by: Jakub Kicinski +--- + 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 index 00000000000..a2252d34505 --- /dev/null +++ b/target/linux/generic/backport-6.12/768-v7.1-net-dsa-MxL862xx-don-t-force-enable-MAXLINEAR_GPHY.patch @@ -0,0 +1,39 @@ +From 06cdf1bf5ba80e90bc54e7fe0c096b47d5ab3d8d Mon Sep 17 00:00:00 2001 +From: Arnd Bergmann +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 +Reviewed-by: Daniel Golle +Link: https://patch.msgid.link/20260216105522.2382373-1-arnd@kernel.org +Signed-off-by: Jakub Kicinski +--- + 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 index 00000000000..8f65bea2ed5 --- /dev/null +++ b/target/linux/generic/backport-6.12/769-v7.1-net-dsa-mxl862xx-add-CRC-for-MDIO-communication.patch @@ -0,0 +1,585 @@ +From d48001906168be3088f9cd7aa8d1ad8dbc53e4f4 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Andrew Lunn +Link: https://patch.msgid.link/620453b9a150bbe5b7ea4224331cb5dc5e57263b.1774185953.git.daniel@makrotopia.org +Signed-off-by: Jakub Kicinski +--- + 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 + #include ++#include + #include + #include + #include +@@ -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 index 00000000000..dc21f10cadc --- /dev/null +++ b/target/linux/generic/backport-6.12/770-v7.1-net-dsa-mxl862xx-use-RST_DATA-to-skip-writing-zero-w.patch @@ -0,0 +1,93 @@ +From 4a296a038c0ea3ad20afe8df00eb083232317646 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +Reviewed-by: Andrew Lunn +Link: https://patch.msgid.link/d10bd6ad5df062d0da342c3e0d330550b3d2432b.1774185953.git.daniel@makrotopia.org +Signed-off-by: Jakub Kicinski +--- + 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); diff --git a/target/linux/generic/backport-6.12/782-04-v6.16-net-phy-introduce-genphy_match_phy_device.patch b/target/linux/generic/backport-6.12/782-04-v6.16-net-phy-introduce-genphy_match_phy_device.patch index 1ac3b2333ff..fed9d3c11fa 100644 --- a/target/linux/generic/backport-6.12/782-04-v6.16-net-phy-introduce-genphy_match_phy_device.patch +++ b/target/linux/generic/backport-6.12/782-04-v6.16-net-phy-introduce-genphy_match_phy_device.patch @@ -97,7 +97,7 @@ Signed-off-by: Jakub Kicinski 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); diff --git a/target/linux/generic/backport-6.12/783-01-v6.18-net-dsa-Move-KS8995-to-the-DSA-subsystem.patch b/target/linux/generic/backport-6.12/783-01-v6.18-net-dsa-Move-KS8995-to-the-DSA-subsystem.patch index 204595693fe..b91c0cd8cfd 100644 --- a/target/linux/generic/backport-6.12/783-01-v6.18-net-dsa-Move-KS8995-to-the-DSA-subsystem.patch +++ b/target/linux/generic/backport-6.12/783-01-v6.18-net-dsa-Move-KS8995-to-the-DSA-subsystem.patch @@ -28,7 +28,7 @@ Signed-off-by: Jakub Kicinski --- 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. diff --git a/target/linux/generic/backport-6.12/783-04-v6.18-net-dsa-ks8995-Add-basic-switch-set-up.patch b/target/linux/generic/backport-6.12/783-04-v6.18-net-dsa-ks8995-Add-basic-switch-set-up.patch index 670fe61252a..3454e113baa 100644 --- a/target/linux/generic/backport-6.12/783-04-v6.18-net-dsa-ks8995-Add-basic-switch-set-up.patch +++ b/target/linux/generic/backport-6.12/783-04-v6.18-net-dsa-ks8995-Add-basic-switch-set-up.patch @@ -51,7 +51,7 @@ Signed-off-by: Jakub Kicinski --- 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 diff --git a/target/linux/generic/backport-6.12/786-01-v6.18-net-phy-introduce-phy_id_compare_vendor-PHY-ID-helpe.patch b/target/linux/generic/backport-6.12/786-01-v6.18-net-phy-introduce-phy_id_compare_vendor-PHY-ID-helpe.patch index 82e9e0695ac..a69a5641485 100644 --- a/target/linux/generic/backport-6.12/786-01-v6.18-net-phy-introduce-phy_id_compare_vendor-PHY-ID-helpe.patch +++ b/target/linux/generic/backport-6.12/786-01-v6.18-net-phy-introduce-phy_id_compare_vendor-PHY-ID-helpe.patch @@ -36,7 +36,7 @@ Signed-off-by: Jakub Kicinski /** * 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 index 00000000000..fc2d26cb1c0 --- /dev/null +++ b/target/linux/generic/pending-6.12/750-net-sfp-add-quirk-for-QINIYEK-BJ-SFP-10G-T-copper-SF.patch @@ -0,0 +1,26 @@ +From 51a16863b04b1c2b28d45d80934f2267547734b7 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..c0e77d17152 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-01-net-dsa-move-dsa_bridge_ports-helper-to-dsa.h.patch @@ -0,0 +1,38 @@ +From de6dd19a3edd1dc6400fecf77610e438441a02ac Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..37891ffe39a --- /dev/null +++ b/target/linux/generic/pending-6.12/760-02-net-dsa-add-bridge-member-iteration-macro.patch @@ -0,0 +1,45 @@ +From 880cde7abf58cb1316382ae7f59aac93c313e8fe Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..7c824994aa0 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-03-dsa-tag_mxl862xx-set-dsa_default_offload_fwd_mark.patch @@ -0,0 +1,29 @@ +From 149bb02d5bf031a1eb85f91377f54913de3a08ff Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..9d1c1c958b8 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-04-net-dsa-mxl862xx-implement-bridge-offloading.patch @@ -0,0 +1,1382 @@ +From 5acdee6df2fbd4a9b02045694227f25cb1d4e5e0 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 + #include + + /** +@@ -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 + */ + +-#include ++#include + #include ++#include ++#include ++#include + #include + #include + #include +@@ -36,6 +39,17 @@ + #define MXL862XX_READY_TIMEOUT_MS 10000 + #define MXL862XX_READY_POLL_MS 100 + ++#define MXL862XX_TCM_INST_SEL 0xe00 ++#define MXL862XX_TCM_CBS 0xe12 ++#define MXL862XX_TCM_EBS 0xe13 ++ ++static const int mxl862xx_flood_meters[] = { ++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC, ++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP, ++ MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP, ++ MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST, ++}; ++ + static enum dsa_tag_protocol mxl862xx_get_tag_protocol(struct dsa_switch *ds, + int port, + enum dsa_tag_protocol m) +@@ -168,6 +182,225 @@ static int mxl862xx_setup_mdio(struct ds + return ret; + } + ++static int mxl862xx_bridge_config_fwd(struct dsa_switch *ds, u16 bridge_id, ++ bool ucast_flood, bool mcast_flood, ++ bool bcast_flood) ++{ ++ struct mxl862xx_bridge_config bridge_config = {}; ++ struct mxl862xx_priv *priv = ds->priv; ++ int ret; ++ ++ bridge_config.mask = cpu_to_le32(MXL862XX_BRIDGE_CONFIG_MASK_FORWARDING_MODE); ++ bridge_config.bridge_id = cpu_to_le16(bridge_id); ++ ++ bridge_config.forward_unknown_unicast = cpu_to_le32(ucast_flood ? ++ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD); ++ ++ bridge_config.forward_unknown_multicast_ip = cpu_to_le32(mcast_flood ? ++ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD); ++ bridge_config.forward_unknown_multicast_non_ip = ++ bridge_config.forward_unknown_multicast_ip; ++ ++ bridge_config.forward_broadcast = cpu_to_le32(bcast_flood ? ++ MXL862XX_BRIDGE_FORWARD_FLOOD : MXL862XX_BRIDGE_FORWARD_DISCARD); ++ ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGE_CONFIGSET, bridge_config); ++ if (ret) ++ dev_err(ds->dev, "failed to configure bridge %u forwarding: %d\n", ++ bridge_id, ret); ++ ++ return ret; ++} ++ ++/* Allocate a single zero-rate meter shared by all ports and flood types. ++ * All flood-blocking egress sub-meters point to this one meter so that any ++ * packet hitting this meter is unconditionally dropped. ++ * ++ * The firmware API requires CBS >= 64 (its bs2ls encoder clamps smaller ++ * values), so the meter is initially configured with CBS=EBS=64. ++ * A zero-rate bucket starts full at CBS bytes, which would let one packet ++ * through before the bucket empties. To eliminate this one-packet leak we ++ * override CBS and EBS to zero via direct register writes after the API call; ++ * the hardware accepts CBS=0 and immediately flags the bucket as exceeded, ++ * so no traffic can ever pass. ++ */ ++static int mxl862xx_setup_drop_meter(struct dsa_switch *ds) ++{ ++ struct mxl862xx_qos_meter_cfg meter = {}; ++ struct mxl862xx_priv *priv = ds->priv; ++ struct mxl862xx_register_mod reg; ++ int ret; ++ ++ /* meter_id=0 means auto-alloc */ ++ ret = MXL862XX_API_READ(priv, MXL862XX_QOS_METERALLOC, meter); ++ if (ret) ++ return ret; ++ ++ meter.enable = true; ++ meter.cbs = cpu_to_le32(64); ++ meter.ebs = cpu_to_le32(64); ++ snprintf(meter.meter_name, sizeof(meter.meter_name), "drop"); ++ ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_QOS_METERCFGSET, meter); ++ if (ret) ++ return ret; ++ ++ priv->drop_meter = le16_to_cpu(meter.meter_id); ++ ++ /* Select the meter instance for subsequent TCM register access. */ ++ reg.addr = cpu_to_le16(MXL862XX_TCM_INST_SEL); ++ reg.data = cpu_to_le16(priv->drop_meter); ++ reg.mask = cpu_to_le16(0xffff); ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg); ++ if (ret) ++ return ret; ++ ++ /* Zero CBS so the committed bucket starts empty (exceeded). */ ++ reg.addr = cpu_to_le16(MXL862XX_TCM_CBS); ++ reg.data = 0; ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg); ++ if (ret) ++ return ret; ++ ++ /* Zero EBS so the excess bucket starts empty (exceeded). */ ++ reg.addr = cpu_to_le16(MXL862XX_TCM_EBS); ++ return MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg); ++} ++ ++static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port) ++{ ++ struct mxl862xx_bridge_port_config br_port_cfg = {}; ++ struct dsa_port *dp = dsa_to_port(ds, port); ++ struct mxl862xx_priv *priv = ds->priv; ++ struct mxl862xx_port *p = &priv->ports[port]; ++ u16 bridge_id = dp->bridge ? ++ priv->bridges[dp->bridge->num] : p->fid; ++ bool enable; ++ int i, idx; ++ ++ br_port_cfg.bridge_port_id = cpu_to_le16(port); ++ br_port_cfg.bridge_id = cpu_to_le16(bridge_id); ++ br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID | ++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP | ++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING | ++ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_SUB_METER); ++ br_port_cfg.src_mac_learning_disable = !p->learning; ++ ++ mxl862xx_fw_portmap_from_bitmap(br_port_cfg.bridge_port_map, p->portmap); ++ ++ for (i = 0; i < ARRAY_SIZE(mxl862xx_flood_meters); i++) { ++ idx = mxl862xx_flood_meters[i]; ++ enable = !!(p->flood_block & BIT(idx)); ++ ++ br_port_cfg.egress_traffic_sub_meter_id[idx] = ++ enable ? cpu_to_le16(priv->drop_meter) : 0; ++ br_port_cfg.egress_sub_metering_enable[idx] = enable; ++ } ++ ++ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, ++ br_port_cfg); ++} ++ ++static int mxl862xx_sync_bridge_members(struct dsa_switch *ds, ++ const struct dsa_bridge *bridge) ++{ ++ struct mxl862xx_priv *priv = ds->priv; ++ struct dsa_port *dp, *member_dp; ++ int port, err, ret = 0; ++ ++ dsa_switch_for_each_bridge_member(dp, ds, bridge->dev) { ++ port = dp->index; ++ ++ bitmap_zero(priv->ports[port].portmap, ++ MXL862XX_MAX_BRIDGE_PORTS); ++ ++ dsa_switch_for_each_bridge_member(member_dp, ds, bridge->dev) { ++ if (member_dp->index != port) ++ __set_bit(member_dp->index, ++ priv->ports[port].portmap); ++ } ++ __set_bit(dp->cpu_dp->index, priv->ports[port].portmap); ++ ++ err = mxl862xx_set_bridge_port(ds, port); ++ if (err) ++ ret = err; ++ } ++ ++ return ret; ++} ++ ++/** ++ * mxl862xx_allocate_bridge - Allocate a firmware bridge instance ++ * @priv: driver private data ++ * @bridge_id: output -- firmware bridge ID assigned by the firmware ++ * ++ * Newly allocated bridges default to flooding all traffic classes ++ * (unknown unicast, multicast, broadcast). Callers that need ++ * different forwarding behavior must call mxl862xx_bridge_config_fwd() ++ * after allocation. ++ * ++ * Return: 0 on success, negative errno on failure. ++ */ ++static int mxl862xx_allocate_bridge(struct mxl862xx_priv *priv, u16 *bridge_id) ++{ ++ struct mxl862xx_bridge_alloc br_alloc = {}; ++ int ret; ++ ++ ret = MXL862XX_API_READ(priv, MXL862XX_BRIDGE_ALLOC, br_alloc); ++ if (ret) ++ return ret; ++ ++ *bridge_id = le16_to_cpu(br_alloc.bridge_id); ++ return 0; ++} ++ ++static void mxl862xx_free_bridge(struct dsa_switch *ds, ++ const struct dsa_bridge *bridge) ++{ ++ struct mxl862xx_priv *priv = ds->priv; ++ u16 fw_id = priv->bridges[bridge->num]; ++ struct mxl862xx_bridge_alloc br_alloc = { ++ .bridge_id = cpu_to_le16(fw_id), ++ }; ++ int ret; ++ ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGE_FREE, br_alloc); ++ if (ret) { ++ dev_err(ds->dev, "failed to free fw bridge %u: %pe\n", ++ fw_id, ERR_PTR(ret)); ++ return; ++ } ++ ++ priv->bridges[bridge->num] = 0; ++} ++ ++static int mxl862xx_add_single_port_bridge(struct dsa_switch *ds, int port) ++{ ++ struct dsa_port *dp = dsa_to_port(ds, port); ++ struct mxl862xx_priv *priv = ds->priv; ++ int ret; ++ ++ ret = mxl862xx_allocate_bridge(priv, &priv->ports[port].fid); ++ if (ret) { ++ dev_err(ds->dev, "failed to allocate a bridge for port %d\n", port); ++ return ret; ++ } ++ ++ priv->ports[port].learning = false; ++ bitmap_zero(priv->ports[port].portmap, MXL862XX_MAX_BRIDGE_PORTS); ++ __set_bit(dp->cpu_dp->index, priv->ports[port].portmap); ++ ++ ret = mxl862xx_set_bridge_port(ds, port); ++ if (ret) ++ return ret; ++ ++ /* Standalone ports should not flood unknown unicast or multicast ++ * towards the CPU by default; only broadcast is needed initially. ++ */ ++ return mxl862xx_bridge_config_fwd(ds, priv->ports[port].fid, ++ false, false, true); ++} ++ + static int mxl862xx_setup(struct dsa_switch *ds) + { + struct mxl862xx_priv *priv = ds->priv; +@@ -181,6 +414,10 @@ static int mxl862xx_setup(struct dsa_swi + if (ret) + return ret; + ++ ret = mxl862xx_setup_drop_meter(ds); ++ if (ret) ++ return ret; ++ + return mxl862xx_setup_mdio(ds); + } + +@@ -260,66 +497,87 @@ static int mxl862xx_configure_sp_tag_pro + + static int mxl862xx_setup_cpu_bridge(struct dsa_switch *ds, int port) + { +- struct mxl862xx_bridge_port_config br_port_cfg = {}; + struct mxl862xx_priv *priv = ds->priv; +- u16 bridge_port_map = 0; + struct dsa_port *dp; + +- /* CPU port bridge setup */ +- br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP | +- MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING | +- MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING); +- +- br_port_cfg.bridge_port_id = cpu_to_le16(port); +- br_port_cfg.src_mac_learning_disable = false; +- br_port_cfg.vlan_src_mac_vid_enable = true; +- br_port_cfg.vlan_dst_mac_vid_enable = true; ++ priv->ports[port].fid = MXL862XX_DEFAULT_BRIDGE; ++ priv->ports[port].learning = true; + + /* include all assigned user ports in the CPU portmap */ ++ bitmap_zero(priv->ports[port].portmap, MXL862XX_MAX_BRIDGE_PORTS); + dsa_switch_for_each_user_port(dp, ds) { + /* it's safe to rely on cpu_dp being valid for user ports */ + if (dp->cpu_dp->index != port) + continue; + +- bridge_port_map |= BIT(dp->index); ++ __set_bit(dp->index, priv->ports[port].portmap); + } +- br_port_cfg.bridge_port_map[0] |= cpu_to_le16(bridge_port_map); + +- return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, br_port_cfg); ++ return mxl862xx_set_bridge_port(ds, port); + } + +-static int mxl862xx_add_single_port_bridge(struct dsa_switch *ds, int port) ++static int mxl862xx_port_bridge_join(struct dsa_switch *ds, int port, ++ const struct dsa_bridge bridge, ++ bool *tx_fwd_offload, ++ struct netlink_ext_ack *extack) + { +- struct mxl862xx_bridge_port_config br_port_cfg = {}; +- struct dsa_port *dp = dsa_to_port(ds, port); +- struct mxl862xx_bridge_alloc br_alloc = {}; ++ struct mxl862xx_priv *priv = ds->priv; ++ u16 fw_id; + int ret; + +- ret = MXL862XX_API_READ(ds->priv, MXL862XX_BRIDGE_ALLOC, br_alloc); +- if (ret) { +- dev_err(ds->dev, "failed to allocate a bridge for port %d\n", port); +- return ret; ++ if (!priv->bridges[bridge.num]) { ++ ret = mxl862xx_allocate_bridge(priv, &fw_id); ++ if (ret) ++ return ret; ++ ++ priv->bridges[bridge.num] = fw_id; ++ ++ /* Free bridge here on error, DSA rollback won't. */ ++ ret = mxl862xx_sync_bridge_members(ds, &bridge); ++ if (ret) { ++ mxl862xx_free_bridge(ds, &bridge); ++ return ret; ++ } ++ ++ return 0; + } + +- br_port_cfg.bridge_id = br_alloc.bridge_id; +- br_port_cfg.bridge_port_id = cpu_to_le16(port); +- br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID | +- MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP | +- MXL862XX_BRIDGE_PORT_CONFIG_MASK_MC_SRC_MAC_LEARNING | +- MXL862XX_BRIDGE_PORT_CONFIG_MASK_VLAN_BASED_MAC_LEARNING); +- br_port_cfg.src_mac_learning_disable = true; +- br_port_cfg.vlan_src_mac_vid_enable = false; +- br_port_cfg.vlan_dst_mac_vid_enable = false; +- /* As this function is only called for user ports it is safe to rely on +- * cpu_dp being valid ++ return mxl862xx_sync_bridge_members(ds, &bridge); ++} ++ ++static void mxl862xx_port_bridge_leave(struct dsa_switch *ds, int port, ++ const struct dsa_bridge bridge) ++{ ++ struct dsa_port *dp = dsa_to_port(ds, port); ++ struct mxl862xx_priv *priv = ds->priv; ++ struct mxl862xx_port *p = &priv->ports[port]; ++ int err; ++ ++ err = mxl862xx_sync_bridge_members(ds, &bridge); ++ if (err) ++ dev_err(ds->dev, ++ "failed to sync bridge members after port %d left: %pe\n", ++ port, ERR_PTR(err)); ++ ++ /* Revert leaving port, omitted by the sync above, to its ++ * single-port bridge + */ +- br_port_cfg.bridge_port_map[0] = cpu_to_le16(BIT(dp->cpu_dp->index)); ++ bitmap_zero(p->portmap, MXL862XX_MAX_BRIDGE_PORTS); ++ __set_bit(dp->cpu_dp->index, p->portmap); ++ p->flood_block = 0; ++ err = mxl862xx_set_bridge_port(ds, port); ++ if (err) ++ dev_err(ds->dev, ++ "failed to update bridge port %d state: %pe\n", port, ++ ERR_PTR(err)); + +- return MXL862XX_API_WRITE(ds->priv, MXL862XX_BRIDGEPORT_CONFIGSET, br_port_cfg); ++ if (!dsa_bridge_ports(ds, bridge.dev)) ++ mxl862xx_free_bridge(ds, &bridge); + } + + static int mxl862xx_port_setup(struct dsa_switch *ds, int port) + { ++ struct mxl862xx_priv *priv = ds->priv; + struct dsa_port *dp = dsa_to_port(ds, port); + bool is_cpu_port = dsa_port_is_cpu(dp); + int ret; +@@ -352,7 +610,31 @@ static int mxl862xx_port_setup(struct ds + return mxl862xx_setup_cpu_bridge(ds, port); + + /* setup single-port bridge for user ports */ +- return mxl862xx_add_single_port_bridge(ds, port); ++ ret = mxl862xx_add_single_port_bridge(ds, port); ++ if (ret) ++ return ret; ++ ++ priv->ports[port].setup_done = true; ++ ++ return 0; ++} ++ ++static void mxl862xx_port_teardown(struct dsa_switch *ds, int port) ++{ ++ struct mxl862xx_priv *priv = ds->priv; ++ struct dsa_port *dp = dsa_to_port(ds, port); ++ ++ if (dsa_port_is_unused(dp) || dsa_port_is_dsa(dp)) ++ return; ++ ++ /* Prevent deferred host_flood_work from acting on stale state. ++ * The flag is checked under rtnl_lock() by the worker; since ++ * teardown also runs under RTNL, this is race-free. ++ * ++ * HW EVLAN/VF blocks are not freed here -- the firmware receives ++ * a full reset on the next probe, which reclaims all resources. ++ */ ++ priv->ports[port].setup_done = false; + } + + static void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port, +@@ -365,14 +647,385 @@ static void mxl862xx_phylink_get_caps(st + config->supported_interfaces); + } + ++static int mxl862xx_get_fid(struct dsa_switch *ds, struct dsa_db db) ++{ ++ struct mxl862xx_priv *priv = ds->priv; ++ ++ switch (db.type) { ++ case DSA_DB_PORT: ++ return priv->ports[db.dp->index].fid; ++ ++ case DSA_DB_BRIDGE: ++ if (!priv->bridges[db.bridge.num]) ++ return -ENOENT; ++ return priv->bridges[db.bridge.num]; ++ ++ default: ++ return -EOPNOTSUPP; ++ } ++} ++ ++static int mxl862xx_port_fdb_add(struct dsa_switch *ds, int port, ++ const unsigned char *addr, u16 vid, struct dsa_db db) ++{ ++ struct mxl862xx_mac_table_add param = {}; ++ int fid = mxl862xx_get_fid(ds, db), ret; ++ struct mxl862xx_priv *priv = ds->priv; ++ ++ if (fid < 0) ++ return fid; ++ ++ param.port_id = cpu_to_le32(port); ++ param.static_entry = true; ++ param.fid = cpu_to_le16(fid); ++ param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid)); ++ ether_addr_copy(param.mac, addr); ++ ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, param); ++ if (ret) ++ dev_err(ds->dev, "failed to add FDB entry on port %d\n", port); ++ ++ return ret; ++} ++ ++static int mxl862xx_port_fdb_del(struct dsa_switch *ds, int port, ++ const unsigned char *addr, u16 vid, const struct dsa_db db) ++{ ++ struct mxl862xx_mac_table_remove param = {}; ++ int fid = mxl862xx_get_fid(ds, db), ret; ++ struct mxl862xx_priv *priv = ds->priv; ++ ++ if (fid < 0) ++ return fid; ++ ++ param.fid = cpu_to_le16(fid); ++ param.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, vid)); ++ ether_addr_copy(param.mac, addr); ++ ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, param); ++ if (ret) ++ dev_err(ds->dev, "failed to remove FDB entry on port %d\n", port); ++ ++ return ret; ++} ++ ++static int mxl862xx_port_fdb_dump(struct dsa_switch *ds, int port, ++ dsa_fdb_dump_cb_t *cb, void *data) ++{ ++ struct mxl862xx_mac_table_read param = { .initial = 1 }; ++ struct mxl862xx_priv *priv = ds->priv; ++ u32 entry_port_id; ++ int ret; ++ ++ while (true) { ++ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYREAD, param); ++ if (ret) ++ return ret; ++ ++ if (param.last) ++ break; ++ ++ entry_port_id = le32_to_cpu(param.port_id); ++ ++ if (entry_port_id == port) { ++ ret = cb(param.mac, FIELD_GET(MXL862XX_TCI_VLAN_ID, ++ le16_to_cpu(param.tci)), ++ param.static_entry, data); ++ if (ret) ++ return ret; ++ } ++ ++ memset(¶m, 0, sizeof(param)); ++ } ++ ++ return 0; ++} ++ ++static int mxl862xx_port_mdb_add(struct dsa_switch *ds, int port, ++ const struct switchdev_obj_port_mdb *mdb, ++ const struct dsa_db db) ++{ ++ struct mxl862xx_mac_table_query qparam = {}; ++ struct mxl862xx_mac_table_add aparam = {}; ++ struct mxl862xx_priv *priv = ds->priv; ++ int fid, ret; ++ ++ fid = mxl862xx_get_fid(ds, db); ++ if (fid < 0) ++ return fid; ++ ++ /* Look up existing entry by {MAC, FID, TCI} */ ++ ether_addr_copy(qparam.mac, mdb->addr); ++ qparam.fid = cpu_to_le16(fid); ++ qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid)); ++ ++ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam); ++ if (ret) ++ return ret; ++ ++ /* Build the ADD command using portmap mode */ ++ ether_addr_copy(aparam.mac, mdb->addr); ++ aparam.fid = cpu_to_le16(fid); ++ aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid)); ++ aparam.static_entry = true; ++ aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG); ++ ++ /* Merge with existing portmap if entry already exists */ ++ if (qparam.found) ++ memcpy(aparam.port_map, qparam.port_map, ++ sizeof(aparam.port_map)); ++ ++ mxl862xx_fw_portmap_set_bit(aparam.port_map, port); ++ ++ return MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam); ++} ++ ++static int mxl862xx_port_mdb_del(struct dsa_switch *ds, int port, ++ const struct switchdev_obj_port_mdb *mdb, ++ const struct dsa_db db) ++{ ++ struct mxl862xx_mac_table_remove rparam = {}; ++ struct mxl862xx_mac_table_query qparam = {}; ++ struct mxl862xx_mac_table_add aparam = {}; ++ int fid = mxl862xx_get_fid(ds, db), ret; ++ struct mxl862xx_priv *priv = ds->priv; ++ ++ if (fid < 0) ++ return fid; ++ ++ /* Look up existing entry */ ++ qparam.fid = cpu_to_le16(fid); ++ qparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid)); ++ ether_addr_copy(qparam.mac, mdb->addr); ++ ++ ret = MXL862XX_API_READ(priv, MXL862XX_MAC_TABLEENTRYQUERY, qparam); ++ if (ret) ++ return ret; ++ ++ if (!qparam.found) ++ return 0; ++ ++ mxl862xx_fw_portmap_clear_bit(qparam.port_map, port); ++ ++ if (mxl862xx_fw_portmap_is_empty(qparam.port_map)) { ++ /* No ports left — remove the entry entirely */ ++ rparam.fid = cpu_to_le16(fid); ++ rparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid)); ++ ether_addr_copy(rparam.mac, mdb->addr); ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYREMOVE, rparam); ++ } else { ++ /* Write back with reduced portmap */ ++ aparam.fid = cpu_to_le16(fid); ++ aparam.tci = cpu_to_le16(FIELD_PREP(MXL862XX_TCI_VLAN_ID, mdb->vid)); ++ ether_addr_copy(aparam.mac, mdb->addr); ++ aparam.static_entry = true; ++ aparam.port_id = cpu_to_le32(MXL862XX_PORTMAP_FLAG); ++ memcpy(aparam.port_map, qparam.port_map, sizeof(aparam.port_map)); ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_MAC_TABLEENTRYADD, aparam); ++ } ++ ++ return ret; ++} ++ ++static int mxl862xx_set_ageing_time(struct dsa_switch *ds, unsigned int msecs) ++{ ++ struct mxl862xx_cfg param = {}; ++ int ret; ++ ++ ret = MXL862XX_API_READ(ds->priv, MXL862XX_COMMON_CFGGET, param); ++ if (ret) { ++ dev_err(ds->dev, "failed to read switch config\n"); ++ return ret; ++ } ++ ++ param.mac_table_age_timer = cpu_to_le32(MXL862XX_AGETIMER_CUSTOM); ++ param.age_timer = cpu_to_le32(msecs / 1000); ++ ret = MXL862XX_API_WRITE(ds->priv, MXL862XX_COMMON_CFGSET, param); ++ if (ret) ++ dev_err(ds->dev, "failed to set ageing\n"); ++ ++ return ret; ++} ++ ++static void mxl862xx_port_stp_state_set(struct dsa_switch *ds, int port, ++ u8 state) ++{ ++ struct mxl862xx_stp_port_cfg param = { ++ .port_id = cpu_to_le16(port), ++ }; ++ struct mxl862xx_priv *priv = ds->priv; ++ int ret; ++ ++ switch (state) { ++ case BR_STATE_DISABLED: ++ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_DISABLE); ++ break; ++ case BR_STATE_BLOCKING: ++ case BR_STATE_LISTENING: ++ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_BLOCKING); ++ break; ++ case BR_STATE_LEARNING: ++ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_LEARNING); ++ break; ++ case BR_STATE_FORWARDING: ++ param.port_state = cpu_to_le32(MXL862XX_STP_PORT_STATE_FORWARD); ++ break; ++ default: ++ dev_err(ds->dev, "invalid STP state: %d\n", state); ++ return; ++ } ++ ++ ret = MXL862XX_API_WRITE(priv, MXL862XX_STP_PORTCFGSET, param); ++ if (ret) { ++ dev_err(ds->dev, "failed to set STP state on port %d\n", port); ++ return; ++ } ++ ++ /* The firmware may re-enable MAC learning as a side-effect of entering ++ * LEARNING or FORWARDING state (per 802.1D defaults). ++ * Re-apply the driver's intended learning and metering config so that ++ * standalone ports keep learning disabled. ++ * This is likely to get fixed in future firmware releases, however, ++ * the additional API call even then doesn't hurt much. ++ */ ++ ret = mxl862xx_set_bridge_port(ds, port); ++ if (ret) ++ dev_err(ds->dev, "failed to reapply brport flags on port %d\n", ++ port); ++ ++ mxl862xx_port_fast_age(ds, port); ++} ++ ++/* Deferred work handler for host flood configuration. ++ * ++ * port_set_host_flood is called from atomic context (under ++ * netif_addr_lock), so firmware calls must be deferred. The worker ++ * acquires rtnl_lock() to serialize with DSA callbacks that access the ++ * same driver state. ++ */ ++static void mxl862xx_host_flood_work_fn(struct work_struct *work) ++{ ++ struct mxl862xx_port *p = container_of(work, struct mxl862xx_port, ++ host_flood_work); ++ struct mxl862xx_priv *priv = p->priv; ++ struct dsa_switch *ds = priv->ds; ++ int port = p - priv->ports; ++ bool uc, mc; ++ ++ rtnl_lock(); ++ ++ /* Port may have been torn down between scheduling and now. */ ++ if (!p->setup_done) { ++ rtnl_unlock(); ++ return; ++ } ++ ++ uc = p->host_flood_uc; ++ mc = p->host_flood_mc; ++ ++ /* The hardware controls unknown-unicast/multicast forwarding per FID ++ * (bridge), not per source port. For bridged ports all members share ++ * one FID, so we cannot selectively suppress flooding to the CPU for ++ * one source port while allowing it for another. Silently ignore the ++ * request -- the excess flooding towards the CPU is harmless. ++ */ ++ if (!dsa_port_bridge_dev_get(dsa_to_port(ds, port))) ++ mxl862xx_bridge_config_fwd(ds, p->fid, uc, mc, true); ++ ++ rtnl_unlock(); ++} ++ ++static void mxl862xx_port_set_host_flood(struct dsa_switch *ds, int port, ++ bool uc, bool mc) ++{ ++ struct mxl862xx_priv *priv = ds->priv; ++ struct mxl862xx_port *p = &priv->ports[port]; ++ ++ p->host_flood_uc = uc; ++ p->host_flood_mc = mc; ++ schedule_work(&p->host_flood_work); ++} ++ ++static int mxl862xx_port_pre_bridge_flags(struct dsa_switch *ds, int port, ++ const struct switchdev_brport_flags flags, ++ struct netlink_ext_ack *extack) ++{ ++ if (flags.mask & ~(BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD | ++ BR_LEARNING)) ++ return -EINVAL; ++ ++ return 0; ++} ++ ++static int mxl862xx_port_bridge_flags(struct dsa_switch *ds, int port, ++ const struct switchdev_brport_flags flags, ++ struct netlink_ext_ack *extack) ++{ ++ struct mxl862xx_priv *priv = ds->priv; ++ unsigned long old_block = priv->ports[port].flood_block; ++ unsigned long block = old_block; ++ bool need_update = false; ++ int ret; ++ ++ if (flags.mask & BR_FLOOD) { ++ if (flags.val & BR_FLOOD) ++ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC); ++ else ++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_UC); ++ } ++ ++ if (flags.mask & BR_MCAST_FLOOD) { ++ if (flags.val & BR_MCAST_FLOOD) { ++ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP); ++ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP); ++ } else { ++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_IP); ++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_UNKNOWN_MC_NON_IP); ++ } ++ } ++ ++ if (flags.mask & BR_BCAST_FLOOD) { ++ if (flags.val & BR_BCAST_FLOOD) ++ block &= ~BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST); ++ else ++ block |= BIT(MXL862XX_BRIDGE_PORT_EGRESS_METER_BROADCAST); ++ } ++ ++ if (flags.mask & BR_LEARNING) ++ priv->ports[port].learning = !!(flags.val & BR_LEARNING); ++ ++ need_update = (block != old_block) || (flags.mask & BR_LEARNING); ++ if (need_update) { ++ priv->ports[port].flood_block = block; ++ ret = mxl862xx_set_bridge_port(ds, port); ++ if (ret) ++ return ret; ++ } ++ ++ return 0; ++} ++ + static const struct dsa_switch_ops mxl862xx_switch_ops = { + .get_tag_protocol = mxl862xx_get_tag_protocol, + .setup = mxl862xx_setup, + .port_setup = mxl862xx_port_setup, ++ .port_teardown = mxl862xx_port_teardown, + .phylink_get_caps = mxl862xx_phylink_get_caps, + .port_enable = mxl862xx_port_enable, + .port_disable = mxl862xx_port_disable, + .port_fast_age = mxl862xx_port_fast_age, ++ .set_ageing_time = mxl862xx_set_ageing_time, ++ .port_bridge_join = mxl862xx_port_bridge_join, ++ .port_bridge_leave = mxl862xx_port_bridge_leave, ++ .port_pre_bridge_flags = mxl862xx_port_pre_bridge_flags, ++ .port_bridge_flags = mxl862xx_port_bridge_flags, ++ .port_stp_state_set = mxl862xx_port_stp_state_set, ++ .port_set_host_flood = mxl862xx_port_set_host_flood, ++ .port_fdb_add = mxl862xx_port_fdb_add, ++ .port_fdb_del = mxl862xx_port_fdb_del, ++ .port_fdb_dump = mxl862xx_port_fdb_dump, ++ .port_mdb_add = mxl862xx_port_mdb_add, ++ .port_mdb_del = mxl862xx_port_mdb_del, + }; + + static void mxl862xx_phylink_mac_config(struct phylink_config *config, +@@ -407,6 +1060,7 @@ static int mxl862xx_probe(struct mdio_de + struct device *dev = &mdiodev->dev; + struct mxl862xx_priv *priv; + struct dsa_switch *ds; ++ int i; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) +@@ -424,8 +1078,17 @@ static int mxl862xx_probe(struct mdio_de + ds->ops = &mxl862xx_switch_ops; + ds->phylink_mac_ops = &mxl862xx_phylink_mac_ops; + ds->num_ports = MXL862XX_MAX_PORTS; ++ ds->fdb_isolation = true; ++ ds->max_num_bridges = MXL862XX_MAX_BRIDGES; ++ + mxl862xx_host_init(priv); + ++ for (i = 0; i < MXL862XX_MAX_PORTS; i++) { ++ priv->ports[i].priv = priv; ++ INIT_WORK(&priv->ports[i].host_flood_work, ++ mxl862xx_host_flood_work_fn); ++ } ++ + dev_set_drvdata(dev, ds); + + return dsa_register_switch(ds); +@@ -435,6 +1098,7 @@ static void mxl862xx_remove(struct mdio_ + { + struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev); + struct mxl862xx_priv *priv; ++ int i; + + if (!ds) + return; +@@ -444,12 +1108,21 @@ static void mxl862xx_remove(struct mdio_ + dsa_unregister_switch(ds); + + mxl862xx_host_shutdown(priv); ++ ++ /* Cancel any pending host flood work. dsa_unregister_switch() ++ * has already called port_teardown (which sets setup_done=false), ++ * but a worker could still be blocked on rtnl_lock(). Since we ++ * are now outside RTNL, cancel_work_sync() will not deadlock. ++ */ ++ for (i = 0; i < MXL862XX_MAX_PORTS; i++) ++ cancel_work_sync(&priv->ports[i].host_flood_work); + } + + static void mxl862xx_shutdown(struct mdio_device *mdiodev) + { + struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev); + struct mxl862xx_priv *priv; ++ int i; + + if (!ds) + return; +@@ -460,6 +1133,9 @@ static void mxl862xx_shutdown(struct mdi + + mxl862xx_host_shutdown(priv); + ++ for (i = 0; i < MXL862XX_MAX_PORTS; i++) ++ cancel_work_sync(&priv->ports[i].host_flood_work); ++ + dev_set_drvdata(&mdiodev->dev, NULL); + } + +--- a/drivers/net/dsa/mxl862xx/mxl862xx.h ++++ b/drivers/net/dsa/mxl862xx/mxl862xx.h +@@ -4,15 +4,148 @@ + #define __MXL862XX_H + + #include ++#include + #include + ++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 index 00000000000..109808792cb --- /dev/null +++ b/target/linux/generic/pending-6.12/760-05-net-dsa-mxl862xx-implement-VLAN-functionality.patch @@ -0,0 +1,1652 @@ +From 7286ac4f850339aac37dd52633f4a70816b621a8 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..a1517ccde0a --- /dev/null +++ b/target/linux/generic/pending-6.12/760-06-net-dsa-mxl862xx-add-ethtool-statistics-support.patch @@ -0,0 +1,383 @@ +From 03b583e774835f771dd7c3c265be5903f008e8e5 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..9d4d93bc2d2 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-07-net-dsa-mxl862xx-implement-.get_stats64.patch @@ -0,0 +1,325 @@ +From 8b66d20f7e5226f4854a39cfb9f25a0591a5bb83 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..d62b466029b --- /dev/null +++ b/target/linux/generic/pending-6.12/760-08-net-dsa-mxl862xx-store-firmware-version-for-feature-.patch @@ -0,0 +1,98 @@ +From fecfbea928cd762b19ff17aa16fb1ab143d73db1 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 + #include + #include + #include +@@ -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 index 00000000000..833ec0642ed --- /dev/null +++ b/target/linux/generic/pending-6.12/760-09-net-dsa-mxl862xx-move-phylink-stubs-to-mxl862xx-phyl.patch @@ -0,0 +1,163 @@ +From 3cb224514226928df80e43ca2280c7dca654bdfe Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 ++ * Copyright (C) 2025 Daniel Golle ++ */ ++ ++#include ++#include ++ ++#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 ++ ++#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 index 00000000000..01013eec3e7 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-10-net-dsa-mxl862xx-move-API-macros-to-mxl862xx-host.h.patch @@ -0,0 +1,53 @@ +From de41d438c4e90876449715a307dd03fa37338742 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..790f50c87df --- /dev/null +++ b/target/linux/generic/pending-6.12/760-11-net-dsa-mxl862xx-add-support-for-SerDes-ports.patch @@ -0,0 +1,1053 @@ +From 88f46eb32d1aed296af2005c3ed8f23a6eea64c3 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 + */ + ++#include + #include + #include + + #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 index 00000000000..5f1e1bb3e59 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-12-net-dsa-mxl862xx-add-SerDes-ethtool-statistics.patch @@ -0,0 +1,268 @@ +From d40565e2e00fc2c8f04b9c571fcbea2f146db844 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..1da11ae9c07 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-13-net-dsa-mxl862xx-add-SerDes-self-test-via-PRBS-and-B.patch @@ -0,0 +1,208 @@ +From 54dd5fabc543f8538202367a863eb0e9161bacab Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 + #include + + #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 index 00000000000..385e44f480c --- /dev/null +++ b/target/linux/generic/pending-6.12/760-14-net-dsa-mxl862xx-trap-link-local-frames-to-the-CPU-p.patch @@ -0,0 +1,831 @@ +From dd62e68cd0bd29934c3efbce687d5e103cc4b331 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..38dc45c594f --- /dev/null +++ b/target/linux/generic/pending-6.12/760-15-net-dsa-mxl862xx-warn-about-old-firmware-default-PCE.patch @@ -0,0 +1,32 @@ +From 3bba25f7ba35e3bca8230bd37ffb612944dbf301 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..86638830775 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-16-net-dsa-add-802.1Q-VLAN-based-tag-driver-for-MxL862x.patch @@ -0,0 +1,2466 @@ +From 1687c5632dfd80461b12425b943e30555faa3dd4 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 + #include + #include ++#include + #include + + #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 + #include + +-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 ++ */ ++ ++#include ++ ++#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 index 00000000000..d373f6511fd --- /dev/null +++ b/target/linux/generic/pending-6.12/760-17-net-dsa-mxl862xx-add-link-aggregation-support.patch @@ -0,0 +1,862 @@ +From 31359e8b7673e656d0591a9eb5014b45911383ae Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..faf69c0c0ea --- /dev/null +++ b/target/linux/generic/pending-6.12/760-18-net-dsa-mxl862xx-add-support-for-mirror-port.patch @@ -0,0 +1,229 @@ +From fbfa1b0649c578e0d43e3a61617b53a9a722efad Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..19da9a0010a --- /dev/null +++ b/target/linux/generic/pending-6.12/760-19-net-dsa-wire-flash_update-devlink-callback-to-driver.patch @@ -0,0 +1,57 @@ +From 67f82834819b71417b58dc1293c20f71b990264f Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..17668b9734a --- /dev/null +++ b/target/linux/generic/pending-6.12/760-20-net-dsa-mxl862xx-add-SMDIO-clause-22-register-access.patch @@ -0,0 +1,72 @@ +From 1a87b829ef3280d646dc480f7b261d9e32896899 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..28b91f8021c --- /dev/null +++ b/target/linux/generic/pending-6.12/760-21-net-dsa-mxl862xx-add-devlink-flash_update-and-info_g.patch @@ -0,0 +1,569 @@ +From b7e8f8fd4493b255f0f01fe790a73ad61b5e8ce8 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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// + devlink dev flash mdio_bus// file + +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 +--- + 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 ++ * ++ * Usage: ++ * # Query running firmware version: ++ * devlink dev info mdio_bus// ++ * ++ * # Flash new firmware (all ports are taken down automatically): ++ * devlink dev flash mdio_bus// file ++ * ++ * 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 ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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 ++ ++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 + #include + #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 index 00000000000..1869a9f17c9 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-22-net-dsa-mxl862xx-implement-port-MTU-configuration.patch @@ -0,0 +1,110 @@ +From 2cb9aeb3a8d7ebac20331e0a533dcfbd73fa4237 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 + #include + #include ++#include + #include + #include + #include +@@ -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 index 00000000000..bf4d2f0ca7d --- /dev/null +++ b/target/linux/generic/pending-6.12/760-23-net-dsa-mxl862xx-support-BR_HAIRPIN_MODE-bridge-flag.patch @@ -0,0 +1,106 @@ +From d55ca68eb0d20a66c32d531b0a454871b486c1b1 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..a524d157e51 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-24-net-dsa-mxl862xx-support-BR_ISOLATED-bridge-flag.patch @@ -0,0 +1,95 @@ +From 74b6654ba74eb142340de4c51b97c0221cfcae37 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..98cfb44d489 --- /dev/null +++ b/target/linux/generic/pending-6.12/760-25-DO-NOT-SUBMIT-net-dsa-mxl862xx-re-introduce-PCE-work.patch @@ -0,0 +1,81 @@ +From 0902a6790750714445c75a66d60f1bc4897126ce Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 index 00000000000..41cd927ac4f --- /dev/null +++ b/target/linux/generic/pending-6.12/760-26-DO-NOT-SUBMIT-net-dsa-mxl862xx-legacy-SFP-API-fallba.patch @@ -0,0 +1,251 @@ +From 0ac876d5b952218ab79ea0a0815cf6fd1290b1d0 Mon Sep 17 00:00:00 2001 +From: Daniel Golle +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 +--- + 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 = {}; diff --git a/target/linux/ipq40xx/patches-6.12/701-net-dsa-add-out-of-band-tagging-protocol.patch b/target/linux/ipq40xx/patches-6.12/701-net-dsa-add-out-of-band-tagging-protocol.patch index 11101163bac..3a4985e403b 100644 --- a/target/linux/ipq40xx/patches-6.12/701-net-dsa-add-out-of-band-tagging-protocol.patch +++ b/target/linux/ipq40xx/patches-6.12/701-net-dsa-add-out-of-band-tagging-protocol.patch @@ -105,18 +105,18 @@ Signed-off-by: Maxime Chevallier --- 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 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 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 diff --git a/target/linux/realtek/patches-6.12/700-dsa-mdio-increase-max-ports-for-rtl839x-rtl931x.patch b/target/linux/realtek/patches-6.12/700-dsa-mdio-increase-max-ports-for-rtl839x-rtl931x.patch index 4a71319aeac..5f7e96358ac 100644 --- a/target/linux/realtek/patches-6.12/700-dsa-mdio-increase-max-ports-for-rtl839x-rtl931x.patch +++ b/target/linux/realtek/patches-6.12/700-dsa-mdio-increase-max-ports-for-rtl839x-rtl931x.patch @@ -70,7 +70,7 @@ Signed-off-by: Markus Stockhausen 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 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))) diff --git a/target/linux/realtek/patches-6.12/714-net-phy-sfp-add-support-for-SMBus.patch b/target/linux/realtek/patches-6.12/714-net-phy-sfp-add-support-for-SMBus.patch index f31cddf0ba2..b6c56b300a4 100644 --- a/target/linux/realtek/patches-6.12/714-net-phy-sfp-add-support-for-SMBus.patch +++ b/target/linux/realtek/patches-6.12/714-net-phy-sfp-add-support-for-SMBus.patch @@ -10,7 +10,7 @@ Signed-off-by: Antoine Tenart --- 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 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 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) { diff --git a/target/linux/realtek/patches-6.12/718-net-dsa-add-support-for-rtl838x-switch.patch b/target/linux/realtek/patches-6.12/718-net-dsa-add-support-for-rtl838x-switch.patch index fdf0b30d43f..dc261efe36b 100644 --- a/target/linux/realtek/patches-6.12/718-net-dsa-add-support-for-rtl838x-switch.patch +++ b/target/linux/realtek/patches-6.12/718-net-dsa-add-support-for-rtl838x-switch.patch @@ -22,7 +22,7 @@ Submitted-by: John Crispin --- 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 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/ diff --git a/target/linux/realtek/patches-6.12/721-net-dsa-add-support-for-tag-rtl-otto.patch b/target/linux/realtek/patches-6.12/721-net-dsa-add-support-for-tag-rtl-otto.patch index 7ad71228fc7..d82a05edc2a 100644 --- a/target/linux/realtek/patches-6.12/721-net-dsa-add-support-for-tag-rtl-otto.patch +++ b/target/linux/realtek/patches-6.12/721-net-dsa-add-support-for-tag-rtl-otto.patch @@ -8,7 +8,7 @@ Signed-off-by: Markus Stockhausen --- 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 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 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, };