kernel: add DSA driver for MaxLinear MxL862xx switches

Backport upstream driver and apply pending downstream patches to
support using the MaxLinear MxL86252 and MxL86282 switches.

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

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

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

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

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
This commit is contained in:
Daniel Golle
2026-03-26 04:00:28 +00:00
parent bf81c51b51
commit 27568d21d3
48 changed files with 14445 additions and 31 deletions
@@ -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
@@ -0,0 +1,39 @@
From ae1c658b33d4bec20c037aebba583a68375d4773 Mon Sep 17 00:00:00 2001
From: Christian Marangi <ansuelsmth@gmail.com>
Date: Thu, 11 Sep 2025 15:08:31 +0200
Subject: [PATCH] net: phy: introduce phy_id_compare_model() PHY ID helper
Similar to phy_id_compare_vendor(), introduce the equivalent
phy_id_compare_model() helper for the generic PHY ID Model mask.
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Reviewed-by: Florian Fainelli <florian.fainelli@broadcom.com>
Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
Link: https://patch.msgid.link/20250911130840.23569-1-ansuelsmth@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
include/linux/phy.h | 13 +++++++++++++
1 file changed, 13 insertions(+)
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
@@ -1284,6 +1284,19 @@ static inline bool phy_id_compare(u32 id
}
/**
+ * phy_id_compare_model - compare @id with @model mask
+ * @id: PHY ID
+ * @model_mask: PHY Model mask
+ *
+ * Return: true if the bits from @id match @model using the
+ * generic PHY Model mask.
+ */
+static inline bool phy_id_compare_model(u32 id, u32 model_mask)
+{
+ return phy_id_compare(id, model_mask, PHY_ID_MATCH_MODEL_MASK);
+}
+
+/**
* phydev_id_compare - compare @id with the PHY's Clause 22 ID
* @phydev: the PHY device
* @id: the PHY ID to be matched
@@ -0,0 +1,163 @@
From de1e5c9333f426348571f7a3b034f99490d3f926 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sat, 22 Nov 2025 13:33:47 +0000
Subject: [PATCH] net: phy: mxl-gpy: add support for MxL86252 and MxL86282
Add PHY driver support for Maxlinear MxL86252 and MxL86282 switches.
The PHYs built-into those switches are just like any other GPY 2.5G PHYs
with the exception of the temperature sensor data being encoded in a
different way.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Link: https://patch.msgid.link/a6cd7fe461b011cec2b59dffaf34e9c8b0819059.1763818120.git.daniel@makrotopia.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/phy/mxl-gpy.c | 91 ++++++++++++++++++++++++++++++++++++++-
1 file changed, 89 insertions(+), 2 deletions(-)
--- a/drivers/net/phy/mxl-gpy.c
+++ b/drivers/net/phy/mxl-gpy.c
@@ -31,6 +31,8 @@
#define PHY_ID_GPY241BM 0x67C9DE80
#define PHY_ID_GPY245B 0x67C9DEC0
#define PHY_ID_MXL86211C 0xC1335400
+#define PHY_ID_MXL86252 0xC1335520
+#define PHY_ID_MXL86282 0xC1335500
#define PHY_CTL1 0x13
#define PHY_CTL1_MDICD BIT(3)
@@ -200,6 +202,29 @@ static int gpy_hwmon_read(struct device
return 0;
}
+static int mxl862x2_hwmon_read(struct device *dev,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel, long *value)
+{
+ struct phy_device *phydev = dev_get_drvdata(dev);
+ long tmp;
+ int ret;
+
+ ret = phy_read_mmd(phydev, MDIO_MMD_VEND1, VSPEC1_TEMP_STA);
+ if (ret < 0)
+ return ret;
+ if (!ret)
+ return -ENODATA;
+
+ tmp = (s16)ret;
+ tmp *= 78125;
+ tmp /= 10000;
+
+ *value = tmp;
+
+ return 0;
+}
+
static umode_t gpy_hwmon_is_visible(const void *data,
enum hwmon_sensor_types type,
u32 attr, int channel)
@@ -217,14 +242,25 @@ static const struct hwmon_ops gpy_hwmon_
.read = gpy_hwmon_read,
};
+static const struct hwmon_ops mxl862x2_hwmon_hwmon_ops = {
+ .is_visible = gpy_hwmon_is_visible,
+ .read = mxl862x2_hwmon_read,
+};
+
static const struct hwmon_chip_info gpy_hwmon_chip_info = {
.ops = &gpy_hwmon_hwmon_ops,
.info = gpy_hwmon_info,
};
+static const struct hwmon_chip_info mxl862x2_hwmon_chip_info = {
+ .ops = &mxl862x2_hwmon_hwmon_ops,
+ .info = gpy_hwmon_info,
+};
+
static int gpy_hwmon_register(struct phy_device *phydev)
{
struct device *dev = &phydev->mdio.dev;
+ const struct hwmon_chip_info *info;
struct device *hwmon_dev;
char *hwmon_name;
@@ -232,10 +268,15 @@ static int gpy_hwmon_register(struct phy
if (IS_ERR(hwmon_name))
return PTR_ERR(hwmon_name);
+ if (phy_id_compare_model(phydev->phy_id, PHY_ID_MXL86252) ||
+ phy_id_compare_model(phydev->phy_id, PHY_ID_MXL86282))
+ info = &mxl862x2_hwmon_chip_info;
+ else
+ info = &gpy_hwmon_chip_info;
+
hwmon_dev = devm_hwmon_device_register_with_info(dev, hwmon_name,
phydev,
- &gpy_hwmon_chip_info,
- NULL);
+ info, NULL);
return PTR_ERR_OR_ZERO(hwmon_dev);
}
@@ -1331,6 +1372,50 @@ static struct phy_driver gpy_drivers[] =
.led_hw_control_set = gpy_led_hw_control_set,
.led_polarity_set = gpy_led_polarity_set,
},
+ {
+ PHY_ID_MATCH_MODEL(PHY_ID_MXL86252),
+ .name = "MaxLinear Ethernet MxL86252",
+ .get_features = genphy_c45_pma_read_abilities,
+ .config_init = gpy_config_init,
+ .probe = gpy_probe,
+ .suspend = genphy_suspend,
+ .resume = genphy_resume,
+ .config_aneg = gpy_config_aneg,
+ .aneg_done = genphy_c45_aneg_done,
+ .read_status = gpy_read_status,
+ .config_intr = gpy_config_intr,
+ .handle_interrupt = gpy_handle_interrupt,
+ .set_wol = gpy_set_wol,
+ .get_wol = gpy_get_wol,
+ .set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
+ },
+ {
+ PHY_ID_MATCH_MODEL(PHY_ID_MXL86282),
+ .name = "MaxLinear Ethernet MxL86282",
+ .get_features = genphy_c45_pma_read_abilities,
+ .config_init = gpy_config_init,
+ .probe = gpy_probe,
+ .suspend = genphy_suspend,
+ .resume = genphy_resume,
+ .config_aneg = gpy_config_aneg,
+ .aneg_done = genphy_c45_aneg_done,
+ .read_status = gpy_read_status,
+ .config_intr = gpy_config_intr,
+ .handle_interrupt = gpy_handle_interrupt,
+ .set_wol = gpy_set_wol,
+ .get_wol = gpy_get_wol,
+ .set_loopback = gpy_loopback,
+ .led_brightness_set = gpy_led_brightness_set,
+ .led_hw_is_supported = gpy_led_hw_is_supported,
+ .led_hw_control_get = gpy_led_hw_control_get,
+ .led_hw_control_set = gpy_led_hw_control_set,
+ .led_polarity_set = gpy_led_polarity_set,
+ },
};
module_phy_driver(gpy_drivers);
@@ -1348,6 +1433,8 @@ static const struct mdio_device_id __may
{PHY_ID_MATCH_MODEL(PHY_ID_GPY241BM)},
{PHY_ID_MATCH_MODEL(PHY_ID_GPY245B)},
{PHY_ID_MATCH_MODEL(PHY_ID_MXL86211C)},
+ {PHY_ID_MATCH_MODEL(PHY_ID_MXL86252)},
+ {PHY_ID_MATCH_MODEL(PHY_ID_MXL86282)},
{ }
};
MODULE_DEVICE_TABLE(mdio, gpy_tbl);
@@ -0,0 +1,191 @@
From 1ecc2ebd1298d5c0eaa238e71b7d2109d7d77538 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sat, 7 Feb 2026 03:07:11 +0000
Subject: [PATCH 01/35] net: dsa: add tag format for MxL862xx switches
Add proprietary special tag format for the MaxLinear MXL862xx family of
switches. While using the same Ethertype as MaxLinear's GSW1xx switches,
the actual tag format differs significantly, hence we need a dedicated
tag driver for that.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Link: https://patch.msgid.link/c64e6ddb6c93a4fac39f9ab9b2d8bf551a2b118d.1770433307.git.daniel@makrotopia.org
Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
---
include/net/dsa.h | 2 +
net/dsa/Kconfig | 7 +++
net/dsa/Makefile | 1 +
net/dsa/tag_mxl862xx.c | 110 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 120 insertions(+)
create mode 100644 net/dsa/tag_mxl862xx.c
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -55,6 +55,7 @@ struct tc_action;
#define DSA_TAG_PROTO_LAN937X_VALUE 27
#define DSA_TAG_PROTO_VSC73XX_8021Q_VALUE 28
#define DSA_TAG_PROTO_BRCM_LEGACY_FCS_VALUE 29
+#define DSA_TAG_PROTO_MXL862_VALUE 30
enum dsa_tag_protocol {
DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
@@ -87,6 +88,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_RZN1_A5PSW = DSA_TAG_PROTO_RZN1_A5PSW_VALUE,
DSA_TAG_PROTO_LAN937X = DSA_TAG_PROTO_LAN937X_VALUE,
DSA_TAG_PROTO_VSC73XX_8021Q = DSA_TAG_PROTO_VSC73XX_8021Q_VALUE,
+ DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
};
struct dsa_switch;
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
@@ -104,6 +104,13 @@ config NET_DSA_TAG_MTK
Say Y or M if you want to enable support for tagging frames for
Mediatek switches.
+config NET_DSA_TAG_MXL_862XX
+ tristate "Tag driver for MaxLinear MxL862xx switches"
+ help
+ Say Y or M if you want to enable support for tagging frames for the
+ MaxLinear MxL86252 and MxL86282 switches using their native 8-byte
+ tagging protocol.
+
config NET_DSA_TAG_KSZ
tristate "Tag driver for Microchip 8795/937x/9477/9893 families of switches"
help
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
@@ -28,6 +28,7 @@ obj-$(CONFIG_NET_DSA_TAG_HELLCREEK) += t
obj-$(CONFIG_NET_DSA_TAG_KSZ) += tag_ksz.o
obj-$(CONFIG_NET_DSA_TAG_LAN9303) += tag_lan9303.o
obj-$(CONFIG_NET_DSA_TAG_MTK) += tag_mtk.o
+obj-$(CONFIG_NET_DSA_TAG_MXL_862XX) += tag_mxl862xx.o
obj-$(CONFIG_NET_DSA_TAG_NONE) += tag_none.o
obj-$(CONFIG_NET_DSA_TAG_OCELOT) += tag_ocelot.o
obj-$(CONFIG_NET_DSA_TAG_OCELOT_8021Q) += tag_ocelot_8021q.o
--- /dev/null
+++ b/net/dsa/tag_mxl862xx.c
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * DSA Special Tag for MaxLinear 862xx switch chips
+ *
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ * Copyright (C) 2024 MaxLinear Inc.
+ */
+
+#include <linux/bitops.h>
+#include <linux/etherdevice.h>
+#include <linux/skbuff.h>
+#include <net/dsa.h>
+#include "tag.h"
+
+#define MXL862_NAME "mxl862xx"
+
+#define MXL862_HEADER_LEN 8
+
+/* Word 0 -> EtherType */
+
+/* Word 2 */
+#define MXL862_SUBIF_ID GENMASK(4, 0)
+
+/* Word 3 */
+#define MXL862_IGP_EGP GENMASK(3, 0)
+
+static struct sk_buff *mxl862_tag_xmit(struct sk_buff *skb,
+ struct net_device *dev)
+{
+ struct dsa_port *dp = dsa_user_to_port(dev);
+ struct dsa_port *cpu_dp = dp->cpu_dp;
+ unsigned int cpu_port, sub_interface;
+ __be16 *mxl862_tag;
+
+ cpu_port = cpu_dp->index;
+
+ /* target port sub-interface ID relative to the CPU port */
+ sub_interface = dp->index + 16 - cpu_port;
+
+ /* provide additional space 'MXL862_HEADER_LEN' bytes */
+ skb_push(skb, MXL862_HEADER_LEN);
+
+ /* shift MAC address to the beginning of the enlarged buffer,
+ * releasing the space required for DSA tag (between MAC address and
+ * Ethertype)
+ */
+ dsa_alloc_etype_header(skb, MXL862_HEADER_LEN);
+
+ /* special tag ingress (from the perspective of the switch) */
+ mxl862_tag = dsa_etype_header_pos_tx(skb);
+ mxl862_tag[0] = htons(ETH_P_MXLGSW);
+ mxl862_tag[1] = 0;
+ mxl862_tag[2] = htons(FIELD_PREP(MXL862_SUBIF_ID, sub_interface));
+ mxl862_tag[3] = htons(FIELD_PREP(MXL862_IGP_EGP, cpu_port));
+
+ return skb;
+}
+
+static struct sk_buff *mxl862_tag_rcv(struct sk_buff *skb,
+ struct net_device *dev)
+{
+ __be16 *mxl862_tag;
+ int port;
+
+ if (unlikely(!pskb_may_pull(skb, MXL862_HEADER_LEN))) {
+ dev_warn_ratelimited(&dev->dev, "Cannot pull SKB, packet dropped\n");
+ return NULL;
+ }
+
+ mxl862_tag = dsa_etype_header_pos_rx(skb);
+
+ if (unlikely(mxl862_tag[0] != htons(ETH_P_MXLGSW))) {
+ dev_warn_ratelimited(&dev->dev,
+ "Invalid special tag marker, packet dropped, tag: %8ph\n",
+ mxl862_tag);
+ return NULL;
+ }
+
+ /* Get source port information */
+ port = FIELD_GET(MXL862_IGP_EGP, ntohs(mxl862_tag[3]));
+ skb->dev = dsa_conduit_find_user(dev, 0, port);
+ if (unlikely(!skb->dev)) {
+ dev_warn_ratelimited(&dev->dev,
+ "Invalid source port, packet dropped, tag: %8ph\n",
+ mxl862_tag);
+ return NULL;
+ }
+
+ /* remove the MxL862xx special tag between the MAC addresses and the
+ * current ethertype field.
+ */
+ skb_pull_rcsum(skb, MXL862_HEADER_LEN);
+ dsa_strip_etype_header(skb, MXL862_HEADER_LEN);
+
+ return skb;
+}
+
+static const struct dsa_device_ops mxl862_netdev_ops = {
+ .name = MXL862_NAME,
+ .proto = DSA_TAG_PROTO_MXL862,
+ .xmit = mxl862_tag_xmit,
+ .rcv = mxl862_tag_rcv,
+ .needed_headroom = MXL862_HEADER_LEN,
+};
+
+MODULE_ALIAS_DSA_TAG_DRIVER(DSA_TAG_PROTO_MXL862, MXL862_NAME);
+MODULE_DESCRIPTION("DSA tag driver for MaxLinear MxL862xx switches");
+MODULE_LICENSE("GPL");
+
+module_dsa_tag_driver(mxl862_netdev_ops);
--- a/include/uapi/linux/if_ether.h
+++ b/include/uapi/linux/if_ether.h
@@ -92,6 +92,9 @@
#define ETH_P_ETHERCAT 0x88A4 /* EtherCAT */
#define ETH_P_8021AD 0x88A8 /* 802.1ad Service VLAN */
#define ETH_P_802_EX1 0x88B5 /* 802.1 Local Experimental 1. */
+#define ETH_P_MXLGSW 0x88C3 /* Infineon Technologies Corporate Research ST
+ * Used by MaxLinear GSW DSA
+ */
#define ETH_P_PREAUTH 0x88C7 /* 802.11 Preauthentication */
#define ETH_P_TIPC 0x88CA /* TIPC */
#define ETH_P_LLDP 0x88CC /* Link Layer Discovery Protocol */
@@ -0,0 +1,41 @@
From 1111454d5a637e039a46b867088b524c73159da4 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sat, 7 Feb 2026 03:07:18 +0000
Subject: [PATCH 02/35] net: mdio: add unlocked mdiodev C45 bus accessors
Add helper inline functions __mdiodev_c45_read() and
__mdiodev_c45_write(), which are the C45 equivalents of the existing
__mdiodev_read() and __mdiodev_write() added by commit e6a45700e7e1
("net: mdio: add unlocked mdiobus and mdiodev bus accessors")
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Russell King (Oracle) <rmk+kernel@armlinux.org.uk>
Link: https://patch.msgid.link/8d1d55949a75a871d2a3b90e421de4bd58d77685.1770433307.git.daniel@makrotopia.org
Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
---
include/linux/mdio.h | 13 +++++++++++++
1 file changed, 13 insertions(+)
--- a/include/linux/mdio.h
+++ b/include/linux/mdio.h
@@ -668,6 +668,19 @@ static inline int mdiodev_modify_changed
mask, set);
}
+static inline int __mdiodev_c45_read(struct mdio_device *mdiodev, int devad,
+ u16 regnum)
+{
+ return __mdiobus_c45_read(mdiodev->bus, mdiodev->addr, devad, regnum);
+}
+
+static inline int __mdiodev_c45_write(struct mdio_device *mdiodev, u32 devad,
+ u16 regnum, u16 val)
+{
+ return __mdiobus_c45_write(mdiodev->bus, mdiodev->addr, devad, regnum,
+ val);
+}
+
static inline int mdiodev_c45_modify(struct mdio_device *mdiodev, int devad,
u32 regnum, u16 mask, u16 set)
{
@@ -0,0 +1,94 @@
From b5f8b39d22ab93cada5c88dc2cb6495b95f44c70 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 3 Mar 2026 03:17:43 +0000
Subject: [PATCH 04/35] net: dsa: mxl862xx: rename MDIO op arguments
The use of the 'port' argument name for functions implementing the MDIO
bus operations is misleading as the port address isn't equal to the
PHY address.
Rename the MDIO operation argument name to match the prototypes of
mdiobus_write, mdiobus_read, mdiobus_c45_read and mdiobus_c45_write.
Suggested-by: Vladimir Oltean <olteanv@gmail.com>
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Link: https://patch.msgid.link/e1f4cb3bcffc7df9af0f2c9b673b14c7e1201c9a.1772507674.git.daniel@makrotopia.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 32 ++++++++++++++---------------
1 file changed, 16 insertions(+), 16 deletions(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -44,13 +44,13 @@ static enum dsa_tag_protocol mxl862xx_ge
}
/* PHY access via firmware relay */
-static int mxl862xx_phy_read_mmd(struct mxl862xx_priv *priv, int port,
- int devadd, int reg)
+static int mxl862xx_phy_read_mmd(struct mxl862xx_priv *priv, int addr,
+ int devadd, int regnum)
{
struct mdio_relay_data param = {
- .phy = port,
+ .phy = addr,
.mmd = devadd,
- .reg = cpu_to_le16(reg),
+ .reg = cpu_to_le16(regnum),
};
int ret;
@@ -61,40 +61,40 @@ static int mxl862xx_phy_read_mmd(struct
return le16_to_cpu(param.data);
}
-static int mxl862xx_phy_write_mmd(struct mxl862xx_priv *priv, int port,
- int devadd, int reg, u16 data)
+static int mxl862xx_phy_write_mmd(struct mxl862xx_priv *priv, int addr,
+ int devadd, int regnum, u16 data)
{
struct mdio_relay_data param = {
- .phy = port,
+ .phy = addr,
.mmd = devadd,
- .reg = cpu_to_le16(reg),
+ .reg = cpu_to_le16(regnum),
.data = cpu_to_le16(data),
};
return MXL862XX_API_WRITE(priv, INT_GPHY_WRITE, param);
}
-static int mxl862xx_phy_read_mii_bus(struct mii_bus *bus, int port, int regnum)
+static int mxl862xx_phy_read_mii_bus(struct mii_bus *bus, int addr, int regnum)
{
- return mxl862xx_phy_read_mmd(bus->priv, port, 0, regnum);
+ return mxl862xx_phy_read_mmd(bus->priv, addr, 0, regnum);
}
-static int mxl862xx_phy_write_mii_bus(struct mii_bus *bus, int port,
+static int mxl862xx_phy_write_mii_bus(struct mii_bus *bus, int addr,
int regnum, u16 val)
{
- return mxl862xx_phy_write_mmd(bus->priv, port, 0, regnum, val);
+ return mxl862xx_phy_write_mmd(bus->priv, addr, 0, regnum, val);
}
-static int mxl862xx_phy_read_c45_mii_bus(struct mii_bus *bus, int port,
+static int mxl862xx_phy_read_c45_mii_bus(struct mii_bus *bus, int addr,
int devadd, int regnum)
{
- return mxl862xx_phy_read_mmd(bus->priv, port, devadd, regnum);
+ return mxl862xx_phy_read_mmd(bus->priv, addr, devadd, regnum);
}
-static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int port,
+static int mxl862xx_phy_write_c45_mii_bus(struct mii_bus *bus, int addr,
int devadd, int regnum, u16 val)
{
- return mxl862xx_phy_write_mmd(bus->priv, port, devadd, regnum, val);
+ return mxl862xx_phy_write_mmd(bus->priv, addr, devadd, regnum, val);
}
static int mxl862xx_wait_ready(struct dsa_switch *ds)
@@ -0,0 +1,31 @@
From d0341efa8f5182cafe16506b9bef98184f4951fe Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 10 Mar 2026 00:41:56 +0000
Subject: [PATCH 05/35] net: dsa: mxl862xx: don't set user_mii_bus
The PHY addresses in the MII bus are not equal to the port addresses,
so the bus cannot be assigned as user_mii_bus. Falling back on the
user_mii_bus in case a PHY isn't declared in device tree will result in
using the wrong (in this case: off-by-+1) PHY.
Remove the wrong assignment.
Fixes: 23794bec1cb60 ("net: dsa: add basic initial driver for MxL862xx switches")
Suggested-by: Vladimir Oltean <olteanv@gmail.com>
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
Link: https://patch.msgid.link/0f0df310fd8cab57e0e5e3d0831dd057fd05bcd5.1773103271.git.daniel@makrotopia.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 1 -
1 file changed, 1 deletion(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -149,7 +149,6 @@ static int mxl862xx_setup_mdio(struct ds
return -ENOMEM;
bus->priv = priv;
- ds->user_mii_bus = bus;
bus->name = KBUILD_MODNAME "-mii";
snprintf(bus->id, MII_BUS_ID_SIZE, "%s-mii", dev_name(dev));
bus->read_c45 = mxl862xx_phy_read_c45_mii_bus;
@@ -0,0 +1,54 @@
From c0402837642625ef13ade862e20e229f4a5810f5 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Wed, 18 Mar 2026 03:07:52 +0000
Subject: [PATCH 06/35] net: dsa: mxl862xx: don't read out-of-bounds
The write loop in mxl862xx_api_wrap() computes the word count as
(size + 1) / 2, rounding up for odd-sized structs.
On the last iteration of an odd-sized buffer it reads a full __le16
from data[i], accessing one byte past the end of the caller's struct.
KASAN catches this as a stack-out-of-bounds read during probe (e.g.
from mxl862xx_bridge_config_fwd() because of the odd length of
sizeof(struct mxl862xx_bridge_config) == 49).
The read-back loop already handles this case, it writes only a single
byte when (i * 2 + 1) == size. The write loop lacked the same guard.
In practice the over-read is harmless: the extra stack byte is sent to
the firmware which ignores trailing data beyond the command's declared
payload size.
Apply the same odd-size last-byte handling to the write path: when the
final word contains only one valid byte, send *(u8 *)&data[i] instead
of le16_to_cpu(data[i]). This is endian-safe because data is
__le16-encoded and the low byte is always at the lowest address
regardless of host byte order.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Simon Horman <horms@kernel.org>
Link: https://patch.msgid.link/83356ad9c9a4470dd49b6b3d661c2a8dd85cc6a1.1773803190.git.daniel@makrotopia.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -175,8 +175,14 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
goto out;
}
- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_DATA_FIRST + off,
- le16_to_cpu(data[i]));
+ if ((i * 2 + 1) == size)
+ ret = mxl862xx_reg_write(priv,
+ MXL862XX_MMD_REG_DATA_FIRST + off,
+ *(u8 *)&data[i]);
+ else
+ ret = mxl862xx_reg_write(priv,
+ MXL862XX_MMD_REG_DATA_FIRST + off,
+ le16_to_cpu(data[i]));
if (ret < 0)
goto out;
}
@@ -0,0 +1,39 @@
From 06cdf1bf5ba80e90bc54e7fe0c096b47d5ab3d8d Mon Sep 17 00:00:00 2001
From: Arnd Bergmann <arnd@arndb.de>
Date: Mon, 16 Feb 2026 11:55:17 +0100
Subject: [PATCH 07/35] net: dsa: MxL862xx: don't force-enable MAXLINEAR_GPHY
The newly added dsa driver attempts to enable the corresponding PHY driver,
but that one has additional dependencies that may not be available:
WARNING: unmet direct dependencies detected for MAXLINEAR_GPHY
Depends on [m]: NETDEVICES [=y] && PHYLIB [=y] && (HWMON [=m] || HWMON [=m]=n [=n])
Selected by [y]:
- NET_DSA_MXL862 [=y] && NETDEVICES [=y] && NET_DSA [=y]
aarch64-linux-ld: drivers/net/phy/mxl-gpy.o: in function `gpy_probe':
mxl-gpy.c:(.text.gpy_probe+0x13c): undefined reference to `devm_hwmon_device_register_with_info'
aarch64-linux-ld: drivers/net/phy/mxl-gpy.o: in function `gpy_hwmon_read':
mxl-gpy.c:(.text.gpy_hwmon_read+0x48): undefined reference to `polynomial_calc'
There is actually no compile-time dependency, as DSA correctly uses the
PHY abstractions. Remove the 'select' statement to reduce the complexity.
Fixes: 23794bec1cb6 ("net: dsa: add basic initial driver for MxL862xx switches")
Signed-off-by: Arnd Bergmann <arnd@arndb.de>
Reviewed-by: Daniel Golle <daniel@makrotopia.org>
Link: https://patch.msgid.link/20260216105522.2382373-1-arnd@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/mxl862xx/Kconfig | 1 -
1 file changed, 1 deletion(-)
--- a/drivers/net/dsa/mxl862xx/Kconfig
+++ b/drivers/net/dsa/mxl862xx/Kconfig
@@ -2,7 +2,6 @@
config NET_DSA_MXL862
tristate "MaxLinear MxL862xx"
depends on NET_DSA
- select MAXLINEAR_GPHY
select NET_DSA_TAG_MXL_862XX
help
This enables support for the MaxLinear MxL862xx switch family.
@@ -0,0 +1,585 @@
From d48001906168be3088f9cd7aa8d1ad8dbc53e4f4 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sun, 22 Mar 2026 13:27:20 +0000
Subject: [PATCH 08/35] net: dsa: mxl862xx: add CRC for MDIO communication
Enable the firmware's opt-in CRC validation on the MDIO/MMD command
interface to detect bit errors on the bus. The firmware bundles CRC-6
and CRC-16 under a single enable flag, so both are implemented
together.
CRC-6 protects the ctrl and len_ret command registers using a table-
driven 3GPP algorithm. It is applied to every command exchange
including SET_DATA/GET_DATA batch transfers. With CRC enabled, the
firmware encodes its return value as a signed 11-bit integer within
the CRC- protected register fields, replacing the previous 16-bit
interpretation.
CRC-16 protects the data payload using the kernel's crc16() library.
The driver appends a CRC-16 checksum to outgoing data and verifies the
firmware-appended checksum on responses. The checksum is placed at the
exact byte offset where the struct data ends, correctly handling
packed structs with odd sizes by splitting the checksum across word
boundaries. SET_DATA/GET_DATA sub-commands carry only CRC-6.
Upon detection of a CRC error on either side all conduit interfaces
are taken down, triggering all user ports to go down as well. This is
the most feasible option: CRC errors are likely caused either by
broken hardware, or are symptom of overheating. In either case, trying
to resume normal operation isn't reasonable.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Link: https://patch.msgid.link/620453b9a150bbe5b7ea4224331cb5dc5e57263b.1774185953.git.daniel@makrotopia.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/mxl862xx/Kconfig | 1 +
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 345 ++++++++++++++++++-----
drivers/net/dsa/mxl862xx/mxl862xx-host.h | 2 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 11 +
drivers/net/dsa/mxl862xx/mxl862xx.h | 2 +
5 files changed, 296 insertions(+), 65 deletions(-)
--- a/drivers/net/dsa/mxl862xx/Kconfig
+++ b/drivers/net/dsa/mxl862xx/Kconfig
@@ -2,6 +2,7 @@
config NET_DSA_MXL862
tristate "MaxLinear MxL862xx"
depends on NET_DSA
+ select CRC16
select NET_DSA_TAG_MXL_862XX
help
This enables support for the MaxLinear MxL862xx switch family.
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -7,7 +7,9 @@
* Copyright (C) 2024 MaxLinear Inc.
*/
+#include <linux/bitops.h>
#include <linux/bits.h>
+#include <linux/crc16.h>
#include <linux/iopoll.h>
#include <linux/limits.h>
#include <net/dsa.h>
@@ -15,6 +17,9 @@
#include "mxl862xx-host.h"
#define CTRL_BUSY_MASK BIT(15)
+#define CTRL_CRC_FLAG BIT(14)
+
+#define LEN_RET_LEN_MASK GENMASK(9, 0)
#define MXL862XX_MMD_REG_CTRL 0
#define MXL862XX_MMD_REG_LEN_RET 1
@@ -27,7 +32,159 @@
#define MMD_API_GET_DATA_0 5
#define MMD_API_RST_DATA 8
-#define MXL862XX_SWITCH_RESET 0x9907
+#define MXL862XX_SWITCH_RESET 0x9907
+
+static void mxl862xx_crc_err_work_fn(struct work_struct *work)
+{
+ struct mxl862xx_priv *priv = container_of(work, struct mxl862xx_priv,
+ crc_err_work);
+ struct dsa_port *dp;
+
+ dev_warn(&priv->mdiodev->dev,
+ "MDIO CRC error detected, shutting down all ports\n");
+
+ rtnl_lock();
+ dsa_switch_for_each_cpu_port(dp, priv->ds)
+ dev_close(dp->conduit);
+ rtnl_unlock();
+
+ clear_bit(0, &priv->crc_err);
+}
+
+/* Firmware CRC error codes (outside normal Zephyr errno range). */
+#define MXL862XX_FW_CRC6_ERR (-1024)
+#define MXL862XX_FW_CRC16_ERR (-1023)
+
+/* 3GPP CRC-6 lookup table (polynomial 0x6F).
+ * Matches the firmware's default CRC-6 implementation.
+ */
+static const u8 mxl862xx_crc6_table[256] = {
+ 0x00, 0x2f, 0x31, 0x1e, 0x0d, 0x22, 0x3c, 0x13,
+ 0x1a, 0x35, 0x2b, 0x04, 0x17, 0x38, 0x26, 0x09,
+ 0x34, 0x1b, 0x05, 0x2a, 0x39, 0x16, 0x08, 0x27,
+ 0x2e, 0x01, 0x1f, 0x30, 0x23, 0x0c, 0x12, 0x3d,
+ 0x07, 0x28, 0x36, 0x19, 0x0a, 0x25, 0x3b, 0x14,
+ 0x1d, 0x32, 0x2c, 0x03, 0x10, 0x3f, 0x21, 0x0e,
+ 0x33, 0x1c, 0x02, 0x2d, 0x3e, 0x11, 0x0f, 0x20,
+ 0x29, 0x06, 0x18, 0x37, 0x24, 0x0b, 0x15, 0x3a,
+ 0x0e, 0x21, 0x3f, 0x10, 0x03, 0x2c, 0x32, 0x1d,
+ 0x14, 0x3b, 0x25, 0x0a, 0x19, 0x36, 0x28, 0x07,
+ 0x3a, 0x15, 0x0b, 0x24, 0x37, 0x18, 0x06, 0x29,
+ 0x20, 0x0f, 0x11, 0x3e, 0x2d, 0x02, 0x1c, 0x33,
+ 0x09, 0x26, 0x38, 0x17, 0x04, 0x2b, 0x35, 0x1a,
+ 0x13, 0x3c, 0x22, 0x0d, 0x1e, 0x31, 0x2f, 0x00,
+ 0x3d, 0x12, 0x0c, 0x23, 0x30, 0x1f, 0x01, 0x2e,
+ 0x27, 0x08, 0x16, 0x39, 0x2a, 0x05, 0x1b, 0x34,
+ 0x1c, 0x33, 0x2d, 0x02, 0x11, 0x3e, 0x20, 0x0f,
+ 0x06, 0x29, 0x37, 0x18, 0x0b, 0x24, 0x3a, 0x15,
+ 0x28, 0x07, 0x19, 0x36, 0x25, 0x0a, 0x14, 0x3b,
+ 0x32, 0x1d, 0x03, 0x2c, 0x3f, 0x10, 0x0e, 0x21,
+ 0x1b, 0x34, 0x2a, 0x05, 0x16, 0x39, 0x27, 0x08,
+ 0x01, 0x2e, 0x30, 0x1f, 0x0c, 0x23, 0x3d, 0x12,
+ 0x2f, 0x00, 0x1e, 0x31, 0x22, 0x0d, 0x13, 0x3c,
+ 0x35, 0x1a, 0x04, 0x2b, 0x38, 0x17, 0x09, 0x26,
+ 0x12, 0x3d, 0x23, 0x0c, 0x1f, 0x30, 0x2e, 0x01,
+ 0x08, 0x27, 0x39, 0x16, 0x05, 0x2a, 0x34, 0x1b,
+ 0x26, 0x09, 0x17, 0x38, 0x2b, 0x04, 0x1a, 0x35,
+ 0x3c, 0x13, 0x0d, 0x22, 0x31, 0x1e, 0x00, 0x2f,
+ 0x15, 0x3a, 0x24, 0x0b, 0x18, 0x37, 0x29, 0x06,
+ 0x0f, 0x20, 0x3e, 0x11, 0x02, 0x2d, 0x33, 0x1c,
+ 0x21, 0x0e, 0x10, 0x3f, 0x2c, 0x03, 0x1d, 0x32,
+ 0x3b, 0x14, 0x0a, 0x25, 0x36, 0x19, 0x07, 0x28,
+};
+
+/* Compute 3GPP CRC-6 over the ctrl register (16 bits) and the lower
+ * 10 bits of the len_ret register. The 26-bit input is packed as
+ * { len_ret[9:0], ctrl[15:0] } and processed LSB-first through the
+ * lookup table.
+ */
+static u8 mxl862xx_crc6(u16 ctrl, u16 len_ret)
+{
+ u32 data = ((u32)(len_ret & LEN_RET_LEN_MASK) << 16) | ctrl;
+ u8 crc = 0;
+ int i;
+
+ for (i = 0; i < sizeof(data); i++, data >>= 8)
+ crc = mxl862xx_crc6_table[(crc << 2) ^ (data & 0xff)] & 0x3f;
+
+ return crc;
+}
+
+/* Encode CRC-6 into the ctrl and len_ret registers before writing them
+ * to MDIO. The caller must set ctrl = API_ID | CTRL_BUSY_MASK |
+ * CTRL_CRC_FLAG, and len_ret = parameter length (bits 0-9 only).
+ *
+ * After encoding:
+ * ctrl[12:0] = API ID (unchanged)
+ * ctrl[14:13] = CRC-6 bits 5-4
+ * ctrl[15] = busy flag (unchanged)
+ * len_ret[9:0] = parameter length (unchanged)
+ * len_ret[13:10] = CRC-6 bits 3-0
+ * len_ret[14] = original ctrl[14] (CRC check flag, forwarded to FW)
+ * len_ret[15] = original ctrl[13] (magic bit, always 1)
+ */
+static void mxl862xx_crc6_encode(u16 *pctrl, u16 *plen_ret)
+{
+ u16 crc, ctrl, len_ret;
+
+ /* Set magic bit before CRC computation */
+ *pctrl |= BIT(13);
+
+ crc = mxl862xx_crc6(*pctrl, *plen_ret);
+
+ /* Place CRC MSB (bits 5-4) into ctrl bits 13-14 */
+ ctrl = (*pctrl & ~GENMASK(14, 13));
+ ctrl |= (crc & 0x30) << 9;
+
+ /* Place CRC LSB (bits 3-0) into len_ret bits 10-13 */
+ len_ret = *plen_ret | ((crc & 0x0f) << 10);
+
+ /* Forward ctrl[14] (CRC check flag) to len_ret[14],
+ * and ctrl[13] (magic, always 1) to len_ret[15].
+ */
+ len_ret |= (*pctrl & BIT(14)) | ((*pctrl & BIT(13)) << 2);
+
+ *pctrl = ctrl;
+ *plen_ret = len_ret;
+}
+
+/* Verify CRC-6 on a firmware response and extract the return value.
+ *
+ * The firmware encodes the return value as a signed 11-bit integer:
+ * - Sign bit (bit 10) in ctrl[14]
+ * - Magnitude (bits 9-0) in len_ret[9:0]
+ * These are recoverable after CRC-6 verification by restoring the
+ * original ctrl from the auxiliary copies in len_ret[15:14].
+ *
+ * Return: 0 on CRC match (with *result set), or -EIO on mismatch.
+ */
+static int mxl862xx_crc6_verify(u16 ctrl, u16 len_ret, int *result)
+{
+ u16 crc_recv, crc_calc;
+
+ /* Extract the received CRC-6 */
+ crc_recv = ((ctrl >> 9) & 0x30) | ((len_ret >> 10) & 0x0f);
+
+ /* Reconstruct the original ctrl for re-computation:
+ * ctrl[14] = len_ret[14] (sign bit / CRC check flag)
+ * ctrl[13] = len_ret[15] >> 2 (magic bit)
+ */
+ ctrl &= ~GENMASK(14, 13);
+ ctrl |= len_ret & BIT(14);
+ ctrl |= (len_ret & BIT(15)) >> 2;
+
+ crc_calc = mxl862xx_crc6(ctrl, len_ret);
+ if (crc_recv != crc_calc)
+ return -EIO;
+
+ /* Extract signed 11-bit return value:
+ * bit 10 (sign) from ctrl[14], bits 9-0 from len_ret[9:0]
+ */
+ *result = sign_extend32((len_ret & LEN_RET_LEN_MASK) |
+ ((ctrl & CTRL_CRC_FLAG) >> 4), 10);
+
+ return 0;
+}
static int mxl862xx_reg_read(struct mxl862xx_priv *priv, u32 addr)
{
@@ -52,60 +209,78 @@ static int mxl862xx_busy_wait(struct mxl
!(val & CTRL_BUSY_MASK), 15, 500000);
}
-static int mxl862xx_set_data(struct mxl862xx_priv *priv, u16 words)
+/* Issue a firmware command with CRC-6 protection on the ctrl and len_ret
+ * registers, wait for completion, and verify the response CRC-6.
+ *
+ * Return: firmware result value (>= 0) on success, or negative errno.
+ */
+static int mxl862xx_issue_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 len)
{
- int ret;
- u16 cmd;
+ u16 ctrl_enc, len_enc;
+ int ret, fw_result;
+
+ ctrl_enc = cmd | CTRL_BUSY_MASK | CTRL_CRC_FLAG;
+ len_enc = len;
+ mxl862xx_crc6_encode(&ctrl_enc, &len_enc);
- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET,
- MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
+ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET, len_enc);
+ if (ret < 0)
+ return ret;
+
+ ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL, ctrl_enc);
+ if (ret < 0)
+ return ret;
+
+ ret = mxl862xx_busy_wait(priv);
+ if (ret < 0)
+ return ret;
+
+ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_CTRL);
+ if (ret < 0)
+ return ret;
+ ctrl_enc = ret;
+
+ ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_LEN_RET);
if (ret < 0)
return ret;
+ len_enc = ret;
+
+ ret = mxl862xx_crc6_verify(ctrl_enc, len_enc, &fw_result);
+ if (ret) {
+ if (!test_and_set_bit(0, &priv->crc_err))
+ schedule_work(&priv->crc_err_work);
+ return -EIO;
+ }
+
+ return fw_result;
+}
+
+static int mxl862xx_set_data(struct mxl862xx_priv *priv, u16 words)
+{
+ u16 cmd;
cmd = words / MXL862XX_MMD_REG_DATA_MAX_SIZE - 1;
if (!(cmd < 2))
return -EINVAL;
cmd += MMD_API_SET_DATA_0;
- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
- cmd | CTRL_BUSY_MASK);
- if (ret < 0)
- return ret;
- return mxl862xx_busy_wait(priv);
+ return mxl862xx_issue_cmd(priv, cmd,
+ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
}
static int mxl862xx_get_data(struct mxl862xx_priv *priv, u16 words)
{
- int ret;
u16 cmd;
- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET,
- MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
- if (ret < 0)
- return ret;
-
cmd = words / MXL862XX_MMD_REG_DATA_MAX_SIZE;
if (!(cmd > 0 && cmd < 3))
return -EINVAL;
cmd += MMD_API_GET_DATA_0;
- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
- cmd | CTRL_BUSY_MASK);
- if (ret < 0)
- return ret;
-
- return mxl862xx_busy_wait(priv);
-}
-
-static int mxl862xx_firmware_return(int ret)
-{
- /* Only 16-bit values are valid. */
- if (WARN_ON(ret & GENMASK(31, 16)))
- return -EINVAL;
- /* Interpret value as signed 16-bit integer. */
- return (s16)ret;
+ return mxl862xx_issue_cmd(priv, cmd,
+ MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
}
static int mxl862xx_send_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 size,
@@ -113,30 +288,23 @@ static int mxl862xx_send_cmd(struct mxl8
{
int ret;
- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_LEN_RET, size);
- if (ret)
- return ret;
+ ret = mxl862xx_issue_cmd(priv, cmd, size);
- ret = mxl862xx_reg_write(priv, MXL862XX_MMD_REG_CTRL,
- cmd | CTRL_BUSY_MASK);
- if (ret)
- return ret;
-
- ret = mxl862xx_busy_wait(priv);
- if (ret)
- return ret;
-
- ret = mxl862xx_reg_read(priv, MXL862XX_MMD_REG_LEN_RET);
- if (ret < 0)
- return ret;
-
- /* handle errors returned by the firmware as -EIO
+ /* Handle errors returned by the firmware as -EIO.
* The firmware is based on Zephyr OS and uses the errors as
* defined in errno.h of Zephyr OS. See
* https://github.com/zephyrproject-rtos/zephyr/blob/v3.7.0/lib/libc/minimal/include/errno.h
+ *
+ * The firmware signals CRC validation failures with dedicated
+ * error codes outside the normal Zephyr errno range:
+ * -1024: CRC-6 mismatch on ctrl/len_ret registers
+ * -1023: CRC-16 mismatch on data payload
*/
- ret = mxl862xx_firmware_return(ret);
if (ret < 0) {
+ if ((ret == MXL862XX_FW_CRC6_ERR ||
+ ret == MXL862XX_FW_CRC16_ERR) &&
+ !test_and_set_bit(0, &priv->crc_err))
+ schedule_work(&priv->crc_err_work);
if (!quiet)
dev_err(&priv->mdiodev->dev,
"CMD %04x returned error %d\n", cmd, ret);
@@ -151,7 +319,7 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
{
__le16 *data = _data;
int ret, cmd_ret;
- u16 max, i;
+ u16 max, crc, i;
dev_dbg(&priv->mdiodev->dev, "CMD %04x DATA %*ph\n", cmd, size, data);
@@ -163,26 +331,45 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
if (ret < 0)
goto out;
- for (i = 0; i < max; i++) {
+ /* Compute CRC-16 over the data payload; written as an extra word
+ * after the data so the firmware can verify the transfer.
+ */
+ crc = crc16(0xffff, (const u8 *)data, size);
+
+ for (i = 0; i < max + 1; i++) {
u16 off = i % MXL862XX_MMD_REG_DATA_MAX_SIZE;
+ u16 val;
if (i && off == 0) {
/* Send command to set data when every
* MXL862XX_MMD_REG_DATA_MAX_SIZE of WORDs are written.
- */
+ */
ret = mxl862xx_set_data(priv, i);
if (ret < 0)
goto out;
}
- if ((i * 2 + 1) == size)
- ret = mxl862xx_reg_write(priv,
- MXL862XX_MMD_REG_DATA_FIRST + off,
- *(u8 *)&data[i]);
- else
- ret = mxl862xx_reg_write(priv,
- MXL862XX_MMD_REG_DATA_FIRST + off,
- le16_to_cpu(data[i]));
+ if (i == max) {
+ /* Even size: full CRC word.
+ * Odd size: only CRC high byte remains (low byte
+ * was packed into the previous word).
+ */
+ val = (size & 1) ? crc >> 8 : crc;
+ } else if ((i * 2 + 1) == size) {
+ /* Special handling for last BYTE if it's not WORD
+ * aligned to avoid reading beyond the allocated data
+ * structure. Pack the CRC low byte into the high
+ * byte of this word so it sits at byte offset 'size'
+ * in the firmware's contiguous buffer.
+ */
+ val = *(u8 *)&data[i] | ((crc & 0xff) << 8);
+ } else {
+ val = le16_to_cpu(data[i]);
+ }
+
+ ret = mxl862xx_reg_write(priv,
+ MXL862XX_MMD_REG_DATA_FIRST + off,
+ val);
if (ret < 0)
goto out;
}
@@ -194,13 +381,13 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
/* store result of mxl862xx_send_cmd() */
cmd_ret = ret;
- for (i = 0; i < max; i++) {
+ for (i = 0; i < max + 1; i++) {
u16 off = i % MXL862XX_MMD_REG_DATA_MAX_SIZE;
if (i && off == 0) {
/* Send command to fetch next batch of data when every
* MXL862XX_MMD_REG_DATA_MAX_SIZE of WORDs are read.
- */
+ */
ret = mxl862xx_get_data(priv, i);
if (ret < 0)
goto out;
@@ -210,17 +397,35 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
if (ret < 0)
goto out;
- if ((i * 2 + 1) == size) {
+ if (i == max) {
+ /* Even size: full CRC word.
+ * Odd size: only CRC high byte remains (low byte
+ * was in the previous word).
+ */
+ if (size & 1)
+ crc = (crc & 0x00ff) |
+ (((u16)ret & 0xff) << 8);
+ else
+ crc = (u16)ret;
+ } else if ((i * 2 + 1) == size) {
/* Special handling for last BYTE if it's not WORD
* aligned to avoid writing beyond the allocated data
- * structure.
+ * structure. The high byte carries the CRC low byte.
*/
*(uint8_t *)&data[i] = ret & 0xff;
+ crc = (ret >> 8) & 0xff;
} else {
data[i] = cpu_to_le16((u16)ret);
}
}
+ if (crc16(0xffff, (const u8 *)data, size) != crc) {
+ if (!test_and_set_bit(0, &priv->crc_err))
+ schedule_work(&priv->crc_err_work);
+ ret = -EIO;
+ goto out;
+ }
+
/* on success return the result of the mxl862xx_send_cmd() */
ret = cmd_ret;
@@ -249,3 +454,13 @@ out:
return ret;
}
+
+void mxl862xx_host_init(struct mxl862xx_priv *priv)
+{
+ INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn);
+}
+
+void mxl862xx_host_shutdown(struct mxl862xx_priv *priv)
+{
+ cancel_work_sync(&priv->crc_err_work);
+}
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
@@ -5,6 +5,8 @@
#include "mxl862xx.h"
+void mxl862xx_host_init(struct mxl862xx_priv *priv);
+void mxl862xx_host_shutdown(struct mxl862xx_priv *priv);
int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *data, u16 size,
bool read, bool quiet);
int mxl862xx_reset(struct mxl862xx_priv *priv);
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -424,6 +424,7 @@ static int mxl862xx_probe(struct mdio_de
ds->ops = &mxl862xx_switch_ops;
ds->phylink_mac_ops = &mxl862xx_phylink_mac_ops;
ds->num_ports = MXL862XX_MAX_PORTS;
+ mxl862xx_host_init(priv);
dev_set_drvdata(dev, ds);
@@ -433,22 +434,32 @@ static int mxl862xx_probe(struct mdio_de
static void mxl862xx_remove(struct mdio_device *mdiodev)
{
struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
+ struct mxl862xx_priv *priv;
if (!ds)
return;
+ priv = ds->priv;
+
dsa_unregister_switch(ds);
+
+ mxl862xx_host_shutdown(priv);
}
static void mxl862xx_shutdown(struct mdio_device *mdiodev)
{
struct dsa_switch *ds = dev_get_drvdata(&mdiodev->dev);
+ struct mxl862xx_priv *priv;
if (!ds)
return;
+ priv = ds->priv;
+
dsa_switch_shutdown(ds);
+ mxl862xx_host_shutdown(priv);
+
dev_set_drvdata(&mdiodev->dev, NULL);
}
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -11,6 +11,8 @@
struct mxl862xx_priv {
struct dsa_switch *ds;
struct mdio_device *mdiodev;
+ struct work_struct crc_err_work;
+ unsigned long crc_err;
};
#endif /* __MXL862XX_H */
@@ -0,0 +1,93 @@
From 4a296a038c0ea3ad20afe8df00eb083232317646 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sun, 22 Mar 2026 13:27:26 +0000
Subject: [PATCH 09/35] net: dsa: mxl862xx: use RST_DATA to skip writing zero
words
Issue the firmware's RST_DATA command before writing data payloads that
contain many zero words. RST_DATA zeroes both the firmware's internal
buffer and the MMD data registers in a single command, allowing the
driver to skip individual MDIO writes for zero-valued words. This
reduces bus traffic for the common case where API structs have many
unused or default-zero fields.
The optimization is applied when at least 5 zero words are found in the
payload, roughly the break-even point against the cost of the extra
RST_DATA command round-trip.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Link: https://patch.msgid.link/d10bd6ad5df062d0da342c3e0d330550b3d2432b.1774185953.git.daniel@makrotopia.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 38 ++++++++++++++++++++++++
1 file changed, 38 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -283,6 +283,17 @@ static int mxl862xx_get_data(struct mxl8
MXL862XX_MMD_REG_DATA_MAX_SIZE * sizeof(u16));
}
+static int mxl862xx_rst_data(struct mxl862xx_priv *priv)
+{
+ return mxl862xx_issue_cmd(priv, MMD_API_RST_DATA, 0);
+}
+
+/* Minimum number of zero words in the data payload before issuing a
+ * RST_DATA command is worthwhile. RST_DATA costs one full command
+ * round-trip (~5 MDIO transactions), so the threshold must offset that.
+ */
+#define RST_DATA_THRESHOLD 5
+
static int mxl862xx_send_cmd(struct mxl862xx_priv *priv, u16 cmd, u16 size,
bool quiet)
{
@@ -318,6 +329,8 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
u16 size, bool read, bool quiet)
{
__le16 *data = _data;
+ bool use_rst = false;
+ unsigned int zeros;
int ret, cmd_ret;
u16 max, crc, i;
@@ -331,6 +344,24 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
if (ret < 0)
goto out;
+ /* If the data contains enough zero words, issue RST_DATA to zero
+ * both the firmware buffer and MMD registers, then skip writing
+ * zero words individually.
+ */
+ for (i = 0, zeros = 0; i < size / 2 && zeros < RST_DATA_THRESHOLD; i++)
+ if (!data[i])
+ zeros++;
+
+ if (zeros < RST_DATA_THRESHOLD && (size & 1) && !*(u8 *)&data[i])
+ zeros++;
+
+ if (zeros >= RST_DATA_THRESHOLD) {
+ ret = mxl862xx_rst_data(priv);
+ if (ret < 0)
+ goto out;
+ use_rst = true;
+ }
+
/* Compute CRC-16 over the data payload; written as an extra word
* after the data so the firmware can verify the transfer.
*/
@@ -367,6 +398,13 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
val = le16_to_cpu(data[i]);
}
+ /* After RST_DATA, skip zero data words as the registers
+ * already contain zeros, but never skip the CRC word at the
+ * final word.
+ */
+ if (use_rst && i < max && val == 0)
+ continue;
+
ret = mxl862xx_reg_write(priv,
MXL862XX_MMD_REG_DATA_FIRST + off,
val);
@@ -97,7 +97,7 @@ Signed-off-by: Jakub Kicinski <kuba@kernel.org>
static ssize_t
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
@@ -1950,6 +1950,9 @@ char *phy_attached_info_irq(struct phy_d
@@ -1963,6 +1963,9 @@ char *phy_attached_info_irq(struct phy_d
__malloc;
void phy_attached_info(struct phy_device *phydev);
@@ -28,7 +28,7 @@ Signed-off-by: Jakub Kicinski <kuba@kernel.org>
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -98,6 +98,13 @@ config NET_DSA_RZN1_A5PSW
@@ -100,6 +100,13 @@ config NET_DSA_RZN1_A5PSW
This driver supports the A5PSW switch, which is embedded in Renesas
RZ/N1 SoC.
@@ -51,7 +51,7 @@ Signed-off-by: Jakub Kicinski <kuba@kernel.org>
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -101,6 +101,7 @@ config NET_DSA_RZN1_A5PSW
@@ -103,6 +103,7 @@ config NET_DSA_RZN1_A5PSW
config NET_DSA_KS8995
tristate "Micrel KS8995 family 5-ports 10/100 Ethernet switches"
depends on SPI
@@ -36,7 +36,7 @@ Signed-off-by: Jakub Kicinski <kuba@kernel.org>
/**
* phy_id_compare - compare @id1 with @id2 taking account of @mask
@@ -1285,6 +1289,19 @@ static inline bool phy_id_compare(u32 id
@@ -1298,6 +1302,19 @@ static inline bool phy_id_compare_model(
}
/**
@@ -0,0 +1,26 @@
From 51a16863b04b1c2b28d45d80934f2267547734b7 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Fri, 9 May 2025 16:33:27 +0100
Subject: [PATCH] net: sfp: add quirk for QINIYEK BJ-SFP-10G-T copper SFP+
module
Add quirk for a copper SFP+ module that identifies itself as "QINIYEK"
"BJ-SFP-10G-T".
It uses RollBall protocol to talk to the built-in RealTek RTL8261N PHY.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/phy/sfp.c | 1 +
1 file changed, 1 insertion(+)
--- a/drivers/net/phy/sfp.c
+++ b/drivers/net/phy/sfp.c
@@ -557,6 +557,7 @@ static const struct sfp_quirk sfp_quirks
SFP_QUIRK_S("OEM", "SFP-2.5G-BX10-U", sfp_quirk_2500basex),
SFP_QUIRK_F("OEM", "RTSFP-10", sfp_fixup_rollball_cc),
SFP_QUIRK_F("OEM", "RTSFP-10G", sfp_fixup_rollball_cc),
+ SFP_QUIRK_F("QINIYEK", "BJ-SFP-10G-T", sfp_fixup_fs_10gt),
SFP_QUIRK_F("Turris", "RTSFP-2.5G", sfp_fixup_rollball),
SFP_QUIRK_F("Turris", "RTSFP-10", sfp_fixup_rollball),
SFP_QUIRK_F("Turris", "RTSFP-10G", sfp_fixup_rollball),
@@ -0,0 +1,38 @@
From de6dd19a3edd1dc6400fecf77610e438441a02ac Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Wed, 25 Mar 2026 17:54:11 +0000
Subject: [PATCH 10/35] net: dsa: move dsa_bridge_ports() helper to dsa.h
The yt921x driver contains a helper to create a bitmap of ports
which are members of a bridge.
Move the helper as static inline function into dsa.h, so other driver
can make use of it as well.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
include/net/dsa.h | 13 +++++++++++++
1 file changed, 13 insertions(+)
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -832,6 +832,19 @@ dsa_tree_offloads_bridge_dev(struct dsa_
return false;
}
+static inline u32
+dsa_bridge_ports(struct dsa_switch *ds, const struct net_device *bdev)
+{
+ struct dsa_port *dp;
+ u32 mask = 0;
+
+ dsa_switch_for_each_user_port(dp, ds)
+ if (dsa_port_offloads_bridge_dev(dp, bdev))
+ mask |= BIT(dp->index);
+
+ return mask;
+}
+
static inline bool dsa_port_tree_same(const struct dsa_port *a,
const struct dsa_port *b)
{
@@ -0,0 +1,45 @@
From 880cde7abf58cb1316382ae7f59aac93c313e8fe Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Wed, 25 Mar 2026 17:54:41 +0000
Subject: [PATCH 11/35] net: dsa: add bridge member iteration macro
Drivers that offload bridges need to iterate over the ports that are
members of a given bridge, for example to rebuild per-port forwarding
bitmaps when membership changes. Currently drivers typically open-code
this by combining dsa_switch_for_each_user_port() with a
dsa_port_offloads_bridge_dev() check, or cache bridge membership
within the driver.
Add dsa_switch_for_each_bridge_member() macro to express this pattern
directly, and use it for the existing dsa_bridge_ports() inline
helper.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
include/net/dsa.h | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -832,15 +832,18 @@ dsa_tree_offloads_bridge_dev(struct dsa_
return false;
}
+#define dsa_switch_for_each_bridge_member(_dp, _ds, _bdev) \
+ dsa_switch_for_each_user_port(_dp, _ds) \
+ if (dsa_port_offloads_bridge_dev(_dp, _bdev))
+
static inline u32
dsa_bridge_ports(struct dsa_switch *ds, const struct net_device *bdev)
{
struct dsa_port *dp;
u32 mask = 0;
- dsa_switch_for_each_user_port(dp, ds)
- if (dsa_port_offloads_bridge_dev(dp, bdev))
- mask |= BIT(dp->index);
+ dsa_switch_for_each_bridge_member(dp, ds, bdev)
+ mask |= BIT(dp->index);
return mask;
}
@@ -0,0 +1,29 @@
From 149bb02d5bf031a1eb85f91377f54913de3a08ff Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Wed, 25 Mar 2026 17:54:52 +0000
Subject: [PATCH 12/35] dsa: tag_mxl862xx: set dsa_default_offload_fwd_mark()
The MxL862xx offloads bridge forwarding in hardware, so set
dsa_default_offload_fwd_mark() to avoid duplicate forwarding of
packets of (eg. flooded) frames arriving at the CPU port.
Link-local frames are directly trapped to the CPU port only, so don't
set dsa_default_offload_fwd_mark() on those.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
net/dsa/tag_mxl862xx.c | 3 +++
1 file changed, 3 insertions(+)
--- a/net/dsa/tag_mxl862xx.c
+++ b/net/dsa/tag_mxl862xx.c
@@ -86,6 +86,9 @@ static struct sk_buff *mxl862_tag_rcv(st
return NULL;
}
+ if (likely(!is_link_local_ether_addr(eth_hdr(skb)->h_dest)))
+ dsa_default_offload_fwd_mark(skb);
+
/* remove the MxL862xx special tag between the MAC addresses and the
* current ethertype field.
*/
@@ -0,0 +1,383 @@
From 03b583e774835f771dd7c3c265be5903f008e8e5 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Sun, 22 Mar 2026 00:57:33 +0000
Subject: [PATCH 15/35] net: dsa: mxl862xx: add ethtool statistics support
The MxL862xx firmware exposes per-port RMON counters through the
RMON_PORT_GET command, covering standard IEEE 802.3 MAC statistics
(unicast/multicast/broadcast packet and byte counts, collision
counters, pause frames) as well as hardware-specific counters such
as extended VLAN discard and MTU exceed events.
Add the RMON counter firmware API structures and command definitions.
Implement .get_strings, .get_sset_count, and .get_ethtool_stats for
legacy ethtool -S support. Implement .get_eth_mac_stats,
.get_eth_ctrl_stats, and .get_pause_stats for the standardized
IEEE 802.3 statistics interface.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-api.h | 142 ++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 3 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 168 ++++++++++++++++++++++++
3 files changed, 313 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -1224,4 +1224,146 @@ struct mxl862xx_sys_fw_image_version {
__le32 iv_build_num;
} __packed;
+/**
+ * enum mxl862xx_port_type - Port Type
+ * @MXL862XX_LOGICAL_PORT: Logical Port
+ * @MXL862XX_PHYSICAL_PORT: Physical Port
+ * @MXL862XX_CTP_PORT: Connectivity Termination Port (CTP)
+ * @MXL862XX_BRIDGE_PORT: Bridge Port
+ */
+enum mxl862xx_port_type {
+ MXL862XX_LOGICAL_PORT = 0,
+ MXL862XX_PHYSICAL_PORT,
+ MXL862XX_CTP_PORT,
+ MXL862XX_BRIDGE_PORT,
+};
+
+/**
+ * enum mxl862xx_rmon_port_type - RMON counter table type
+ * @MXL862XX_RMON_CTP_PORT_RX: CTP RX counters
+ * @MXL862XX_RMON_CTP_PORT_TX: CTP TX counters
+ * @MXL862XX_RMON_BRIDGE_PORT_RX: Bridge port RX counters
+ * @MXL862XX_RMON_BRIDGE_PORT_TX: Bridge port TX counters
+ * @MXL862XX_RMON_CTP_PORT_PCE_BYPASS: CTP PCE bypass counters
+ * @MXL862XX_RMON_TFLOW_RX: TFLOW RX counters
+ * @MXL862XX_RMON_TFLOW_TX: TFLOW TX counters
+ * @MXL862XX_RMON_QMAP: QMAP counters
+ * @MXL862XX_RMON_METER: Meter counters
+ * @MXL862XX_RMON_PMAC: PMAC counters
+ */
+enum mxl862xx_rmon_port_type {
+ MXL862XX_RMON_CTP_PORT_RX = 0,
+ MXL862XX_RMON_CTP_PORT_TX,
+ MXL862XX_RMON_BRIDGE_PORT_RX,
+ MXL862XX_RMON_BRIDGE_PORT_TX,
+ MXL862XX_RMON_CTP_PORT_PCE_BYPASS,
+ MXL862XX_RMON_TFLOW_RX,
+ MXL862XX_RMON_TFLOW_TX,
+ MXL862XX_RMON_QMAP = 0x0e,
+ MXL862XX_RMON_METER = 0x19,
+ MXL862XX_RMON_PMAC = 0x1c,
+};
+
+/**
+ * struct mxl862xx_rmon_port_cnt - RMON counters for a port
+ * @port_type: Port type for counter retrieval (see &enum mxl862xx_port_type)
+ * @port_id: Ethernet port number (zero-based)
+ * @sub_if_id_group: Sub-interface ID group
+ * @pce_bypass: Separate CTP Tx counters when PCE is bypassed
+ * @rx_extended_vlan_discard_pkts: Discarded at extended VLAN operation
+ * @mtu_exceed_discard_pkts: Discarded due to MTU exceeded
+ * @tx_under_size_good_pkts: Tx undersize (<64) packet count
+ * @tx_oversize_good_pkts: Tx oversize (>1518) packet count
+ * @rx_good_pkts: Received good packet count
+ * @rx_unicast_pkts: Received unicast packet count
+ * @rx_broadcast_pkts: Received broadcast packet count
+ * @rx_multicast_pkts: Received multicast packet count
+ * @rx_fcserror_pkts: Received FCS error packet count
+ * @rx_under_size_good_pkts: Received undersize good packet count
+ * @rx_oversize_good_pkts: Received oversize good packet count
+ * @rx_under_size_error_pkts: Received undersize error packet count
+ * @rx_good_pause_pkts: Received good pause packet count
+ * @rx_oversize_error_pkts: Received oversize error packet count
+ * @rx_align_error_pkts: Received alignment error packet count
+ * @rx_filtered_pkts: Filtered packet count
+ * @rx64byte_pkts: Received 64-byte packet count
+ * @rx127byte_pkts: Received 65-127 byte packet count
+ * @rx255byte_pkts: Received 128-255 byte packet count
+ * @rx511byte_pkts: Received 256-511 byte packet count
+ * @rx1023byte_pkts: Received 512-1023 byte packet count
+ * @rx_max_byte_pkts: Received 1024-max byte packet count
+ * @tx_good_pkts: Transmitted good packet count
+ * @tx_unicast_pkts: Transmitted unicast packet count
+ * @tx_broadcast_pkts: Transmitted broadcast packet count
+ * @tx_multicast_pkts: Transmitted multicast packet count
+ * @tx_single_coll_count: Transmit single collision count
+ * @tx_mult_coll_count: Transmit multiple collision count
+ * @tx_late_coll_count: Transmit late collision count
+ * @tx_excess_coll_count: Transmit excessive collision count
+ * @tx_coll_count: Transmit collision count
+ * @tx_pause_count: Transmit pause packet count
+ * @tx64byte_pkts: Transmitted 64-byte packet count
+ * @tx127byte_pkts: Transmitted 65-127 byte packet count
+ * @tx255byte_pkts: Transmitted 128-255 byte packet count
+ * @tx511byte_pkts: Transmitted 256-511 byte packet count
+ * @tx1023byte_pkts: Transmitted 512-1023 byte packet count
+ * @tx_max_byte_pkts: Transmitted 1024-max byte packet count
+ * @tx_dropped_pkts: Transmit dropped packet count
+ * @tx_acm_dropped_pkts: Transmit ACM dropped packet count
+ * @rx_dropped_pkts: Received dropped packet count
+ * @rx_good_bytes: Received good byte count (64-bit)
+ * @rx_bad_bytes: Received bad byte count (64-bit)
+ * @tx_good_bytes: Transmitted good byte count (64-bit)
+ */
+struct mxl862xx_rmon_port_cnt {
+ enum mxl862xx_port_type port_type;
+ __le16 port_id;
+ __le16 sub_if_id_group;
+ u8 pce_bypass;
+ __le32 rx_extended_vlan_discard_pkts;
+ __le32 mtu_exceed_discard_pkts;
+ __le32 tx_under_size_good_pkts;
+ __le32 tx_oversize_good_pkts;
+ __le32 rx_good_pkts;
+ __le32 rx_unicast_pkts;
+ __le32 rx_broadcast_pkts;
+ __le32 rx_multicast_pkts;
+ __le32 rx_fcserror_pkts;
+ __le32 rx_under_size_good_pkts;
+ __le32 rx_oversize_good_pkts;
+ __le32 rx_under_size_error_pkts;
+ __le32 rx_good_pause_pkts;
+ __le32 rx_oversize_error_pkts;
+ __le32 rx_align_error_pkts;
+ __le32 rx_filtered_pkts;
+ __le32 rx64byte_pkts;
+ __le32 rx127byte_pkts;
+ __le32 rx255byte_pkts;
+ __le32 rx511byte_pkts;
+ __le32 rx1023byte_pkts;
+ __le32 rx_max_byte_pkts;
+ __le32 tx_good_pkts;
+ __le32 tx_unicast_pkts;
+ __le32 tx_broadcast_pkts;
+ __le32 tx_multicast_pkts;
+ __le32 tx_single_coll_count;
+ __le32 tx_mult_coll_count;
+ __le32 tx_late_coll_count;
+ __le32 tx_excess_coll_count;
+ __le32 tx_coll_count;
+ __le32 tx_pause_count;
+ __le32 tx64byte_pkts;
+ __le32 tx127byte_pkts;
+ __le32 tx255byte_pkts;
+ __le32 tx511byte_pkts;
+ __le32 tx1023byte_pkts;
+ __le32 tx_max_byte_pkts;
+ __le32 tx_dropped_pkts;
+ __le32 tx_acm_dropped_pkts;
+ __le32 rx_dropped_pkts;
+ __le64 rx_good_bytes;
+ __le64 rx_bad_bytes;
+ __le64 tx_good_bytes;
+} __packed;
+
#endif /* __MXL862XX_API_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -16,6 +16,7 @@
#define MXL862XX_BRDGPORT_MAGIC 0x400
#define MXL862XX_CTP_MAGIC 0x500
#define MXL862XX_QOS_MAGIC 0x600
+#define MXL862XX_RMON_MAGIC 0x700
#define MXL862XX_SWMAC_MAGIC 0xa00
#define MXL862XX_EXTVLAN_MAGIC 0xb00
#define MXL862XX_VLANFILTER_MAGIC 0xc00
@@ -43,6 +44,8 @@
#define MXL862XX_QOS_METERCFGSET (MXL862XX_QOS_MAGIC + 0x2)
#define MXL862XX_QOS_METERALLOC (MXL862XX_QOS_MAGIC + 0x2a)
+#define MXL862XX_RMON_PORT_GET (MXL862XX_RMON_MAGIC + 0x1)
+
#define MXL862XX_MAC_TABLEENTRYADD (MXL862XX_SWMAC_MAGIC + 0x2)
#define MXL862XX_MAC_TABLEENTRYREAD (MXL862XX_SWMAC_MAGIC + 0x3)
#define MXL862XX_MAC_TABLEENTRYQUERY (MXL862XX_SWMAC_MAGIC + 0x4)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -30,6 +30,64 @@
#define MXL862XX_API_READ_QUIET(dev, cmd, data) \
mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
+struct mxl862xx_mib_desc {
+ unsigned int size;
+ unsigned int offset;
+ const char *name;
+};
+
+#define MIB_DESC(_size, _name, _element) \
+{ \
+ .size = _size, \
+ .name = _name, \
+ .offset = offsetof(struct mxl862xx_rmon_port_cnt, _element) \
+}
+
+static const struct mxl862xx_mib_desc mxl862xx_mib[] = {
+ MIB_DESC(1, "TxGoodPkts", tx_good_pkts),
+ MIB_DESC(1, "TxUnicastPkts", tx_unicast_pkts),
+ MIB_DESC(1, "TxBroadcastPkts", tx_broadcast_pkts),
+ MIB_DESC(1, "TxMulticastPkts", tx_multicast_pkts),
+ MIB_DESC(1, "Tx64BytePkts", tx64byte_pkts),
+ MIB_DESC(1, "Tx127BytePkts", tx127byte_pkts),
+ MIB_DESC(1, "Tx255BytePkts", tx255byte_pkts),
+ MIB_DESC(1, "Tx511BytePkts", tx511byte_pkts),
+ MIB_DESC(1, "Tx1023BytePkts", tx1023byte_pkts),
+ MIB_DESC(1, "TxMaxBytePkts", tx_max_byte_pkts),
+ MIB_DESC(1, "TxDroppedPkts", tx_dropped_pkts),
+ MIB_DESC(1, "TxAcmDroppedPkts", tx_acm_dropped_pkts),
+ MIB_DESC(2, "TxGoodBytes", tx_good_bytes),
+ MIB_DESC(1, "TxSingleCollCount", tx_single_coll_count),
+ MIB_DESC(1, "TxMultCollCount", tx_mult_coll_count),
+ MIB_DESC(1, "TxLateCollCount", tx_late_coll_count),
+ MIB_DESC(1, "TxExcessCollCount", tx_excess_coll_count),
+ MIB_DESC(1, "TxCollCount", tx_coll_count),
+ MIB_DESC(1, "TxPauseCount", tx_pause_count),
+ MIB_DESC(1, "RxGoodPkts", rx_good_pkts),
+ MIB_DESC(1, "RxUnicastPkts", rx_unicast_pkts),
+ MIB_DESC(1, "RxBroadcastPkts", rx_broadcast_pkts),
+ MIB_DESC(1, "RxMulticastPkts", rx_multicast_pkts),
+ MIB_DESC(1, "RxFCSErrorPkts", rx_fcserror_pkts),
+ MIB_DESC(1, "RxUnderSizeGoodPkts", rx_under_size_good_pkts),
+ MIB_DESC(1, "RxOversizeGoodPkts", rx_oversize_good_pkts),
+ MIB_DESC(1, "RxUnderSizeErrorPkts", rx_under_size_error_pkts),
+ MIB_DESC(1, "RxOversizeErrorPkts", rx_oversize_error_pkts),
+ MIB_DESC(1, "RxFilteredPkts", rx_filtered_pkts),
+ MIB_DESC(1, "Rx64BytePkts", rx64byte_pkts),
+ MIB_DESC(1, "Rx127BytePkts", rx127byte_pkts),
+ MIB_DESC(1, "Rx255BytePkts", rx255byte_pkts),
+ MIB_DESC(1, "Rx511BytePkts", rx511byte_pkts),
+ MIB_DESC(1, "Rx1023BytePkts", rx1023byte_pkts),
+ MIB_DESC(1, "RxMaxBytePkts", rx_max_byte_pkts),
+ MIB_DESC(1, "RxDroppedPkts", rx_dropped_pkts),
+ MIB_DESC(1, "RxExtendedVlanDiscardPkts", rx_extended_vlan_discard_pkts),
+ MIB_DESC(1, "MtuExceedDiscardPkts", mtu_exceed_discard_pkts),
+ MIB_DESC(2, "RxGoodBytes", rx_good_bytes),
+ MIB_DESC(2, "RxBadBytes", rx_bad_bytes),
+ MIB_DESC(1, "RxGoodPausePkts", rx_good_pause_pkts),
+ MIB_DESC(1, "RxAlignErrorPkts", rx_align_error_pkts),
+};
+
#define MXL862XX_SDMA_PCTRLP(p) (0xbc0 + ((p) * 0x6))
#define MXL862XX_SDMA_PCTRL_EN BIT(0)
@@ -1940,6 +1998,110 @@ static int mxl862xx_port_bridge_flags(st
return 0;
}
+static void mxl862xx_get_strings(struct dsa_switch *ds, int port,
+ u32 stringset, u8 *data)
+{
+ int i;
+
+ if (stringset != ETH_SS_STATS)
+ return;
+
+ for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++)
+ ethtool_puts(&data, mxl862xx_mib[i].name);
+}
+
+static int mxl862xx_get_sset_count(struct dsa_switch *ds, int port, int sset)
+{
+ if (sset != ETH_SS_STATS)
+ return 0;
+
+ return ARRAY_SIZE(mxl862xx_mib);
+}
+
+static int mxl862xx_read_rmon(struct dsa_switch *ds, int port,
+ struct mxl862xx_rmon_port_cnt *cnt)
+{
+ memset(cnt, 0, sizeof(*cnt));
+ cnt->port_type = MXL862XX_CTP_PORT;
+ cnt->port_id = cpu_to_le16(port);
+
+ return MXL862XX_API_READ(ds->priv, MXL862XX_RMON_PORT_GET, *cnt);
+}
+
+static void mxl862xx_get_ethtool_stats(struct dsa_switch *ds, int port,
+ u64 *data)
+{
+ const struct mxl862xx_mib_desc *mib;
+ struct mxl862xx_rmon_port_cnt cnt;
+ int ret, i;
+ void *field;
+
+ ret = mxl862xx_read_rmon(ds, port, &cnt);
+ if (ret) {
+ dev_err(ds->dev, "failed to read RMON stats on port %d\n", port);
+ return;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++) {
+ mib = &mxl862xx_mib[i];
+ field = (u8 *)&cnt + mib->offset;
+
+ if (mib->size == 1)
+ *data++ = le32_to_cpu(*(__le32 *)field);
+ else
+ *data++ = le64_to_cpu(*(__le64 *)field);
+ }
+}
+
+static void mxl862xx_get_eth_mac_stats(struct dsa_switch *ds, int port,
+ struct ethtool_eth_mac_stats *mac_stats)
+{
+ struct mxl862xx_rmon_port_cnt cnt;
+
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ mac_stats->FramesTransmittedOK = le32_to_cpu(cnt.tx_good_pkts);
+ mac_stats->SingleCollisionFrames = le32_to_cpu(cnt.tx_single_coll_count);
+ mac_stats->MultipleCollisionFrames = le32_to_cpu(cnt.tx_mult_coll_count);
+ mac_stats->FramesReceivedOK = le32_to_cpu(cnt.rx_good_pkts);
+ mac_stats->FrameCheckSequenceErrors = le32_to_cpu(cnt.rx_fcserror_pkts);
+ mac_stats->AlignmentErrors = le32_to_cpu(cnt.rx_align_error_pkts);
+ mac_stats->OctetsTransmittedOK = le64_to_cpu(cnt.tx_good_bytes);
+ mac_stats->LateCollisions = le32_to_cpu(cnt.tx_late_coll_count);
+ mac_stats->FramesAbortedDueToXSColls = le32_to_cpu(cnt.tx_excess_coll_count);
+ mac_stats->OctetsReceivedOK = le64_to_cpu(cnt.rx_good_bytes);
+ mac_stats->MulticastFramesXmittedOK = le32_to_cpu(cnt.tx_multicast_pkts);
+ mac_stats->BroadcastFramesXmittedOK = le32_to_cpu(cnt.tx_broadcast_pkts);
+ mac_stats->MulticastFramesReceivedOK = le32_to_cpu(cnt.rx_multicast_pkts);
+ mac_stats->BroadcastFramesReceivedOK = le32_to_cpu(cnt.rx_broadcast_pkts);
+ mac_stats->FrameTooLongErrors = le32_to_cpu(cnt.rx_oversize_error_pkts);
+}
+
+static void mxl862xx_get_eth_ctrl_stats(struct dsa_switch *ds, int port,
+ struct ethtool_eth_ctrl_stats *ctrl_stats)
+{
+ struct mxl862xx_rmon_port_cnt cnt;
+
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ ctrl_stats->MACControlFramesTransmitted = le32_to_cpu(cnt.tx_pause_count);
+ ctrl_stats->MACControlFramesReceived = le32_to_cpu(cnt.rx_good_pause_pkts);
+}
+
+static void mxl862xx_get_pause_stats(struct dsa_switch *ds, int port,
+ struct ethtool_pause_stats *pause_stats)
+{
+ struct mxl862xx_rmon_port_cnt cnt;
+
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ pause_stats->tx_pause_frames = le32_to_cpu(cnt.tx_pause_count);
+ pause_stats->rx_pause_frames = le32_to_cpu(cnt.rx_good_pause_pkts);
+}
+
static const struct dsa_switch_ops mxl862xx_switch_ops = {
.get_tag_protocol = mxl862xx_get_tag_protocol,
.setup = mxl862xx_setup,
@@ -1964,6 +2126,12 @@ static const struct dsa_switch_ops mxl86
.port_vlan_filtering = mxl862xx_port_vlan_filtering,
.port_vlan_add = mxl862xx_port_vlan_add,
.port_vlan_del = mxl862xx_port_vlan_del,
+ .get_strings = mxl862xx_get_strings,
+ .get_sset_count = mxl862xx_get_sset_count,
+ .get_ethtool_stats = mxl862xx_get_ethtool_stats,
+ .get_eth_mac_stats = mxl862xx_get_eth_mac_stats,
+ .get_eth_ctrl_stats = mxl862xx_get_eth_ctrl_stats,
+ .get_pause_stats = mxl862xx_get_pause_stats,
};
static void mxl862xx_phylink_mac_config(struct phylink_config *config,
@@ -0,0 +1,325 @@
From 8b66d20f7e5226f4854a39cfb9f25a0591a5bb83 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 04:14:38 +0000
Subject: [PATCH 16/35] net: dsa: mxl862xx: implement .get_stats64
Poll free-running firmware RMON counters every 2 seconds and accumulate
deltas into 64-bit per-port statistics. 32-bit packet counters wrap
in ~880s at 2.5 Gbps line rate; the 2s polling interval provides a
comfortable margin. The .get_stats64 callback forces a fresh poll so
that counters are always up to date when queried.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 167 ++++++++++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx.h | 51 +++++++++
2 files changed, 218 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -30,6 +30,12 @@
#define MXL862XX_API_READ_QUIET(dev, cmd, data) \
mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
+/* Polling interval for RMON counter accumulation. At 2.5 Gbps with
+ * minimum-size (64-byte) frames, a 32-bit packet counter wraps in ~880s.
+ * 2s gives a comfortable margin.
+ */
+#define MXL862XX_STATS_POLL_INTERVAL (2 * HZ)
+
struct mxl862xx_mib_desc {
unsigned int size;
unsigned int offset;
@@ -784,6 +790,9 @@ static int mxl862xx_setup(struct dsa_swi
if (ret)
return ret;
+ schedule_delayed_work(&priv->stats_work,
+ MXL862XX_STATS_POLL_INTERVAL);
+
return mxl862xx_setup_mdio(ds);
}
@@ -2102,6 +2111,156 @@ static void mxl862xx_get_pause_stats(str
pause_stats->rx_pause_frames = le32_to_cpu(cnt.rx_good_pause_pkts);
}
+/* Compute the delta between two 32-bit free-running counter snapshots,
+ * handling a single wrap-around correctly via unsigned subtraction.
+ */
+static u64 mxl862xx_delta32(u32 cur, u32 prev)
+{
+ return (u32)(cur - prev);
+}
+
+/**
+ * mxl862xx_stats_poll - Read RMON counters and accumulate into 64-bit stats
+ * @ds: DSA switch
+ * @port: port index
+ *
+ * The firmware RMON counters are free-running 32-bit values (64-bit for
+ * byte counters). This function reads the hardware via MDIO (may sleep),
+ * computes deltas from the previous snapshot, and accumulates them into
+ * 64-bit per-port stats under a spinlock.
+ *
+ * Called only from the stats polling workqueue -- serialized by the
+ * single-threaded delayed_work, so no MDIO locking is needed here.
+ */
+static void mxl862xx_stats_poll(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port_stats *s = &priv->ports[port].stats;
+ u32 rx_fcserr, rx_under, rx_over, rx_align, tx_drop;
+ u32 rx_drop, rx_evlan, mtu_exc, tx_acm;
+ struct mxl862xx_rmon_port_cnt cnt;
+ u64 rx_bytes, tx_bytes;
+ u32 rx_mcast, tx_coll;
+ u32 rx_pkts, tx_pkts;
+
+ /* MDIO read -- may sleep, done outside the spinlock. */
+ if (mxl862xx_read_rmon(ds, port, &cnt))
+ return;
+
+ rx_pkts = le32_to_cpu(cnt.rx_good_pkts);
+ tx_pkts = le32_to_cpu(cnt.tx_good_pkts);
+ rx_bytes = le64_to_cpu(cnt.rx_good_bytes);
+ tx_bytes = le64_to_cpu(cnt.tx_good_bytes);
+ rx_fcserr = le32_to_cpu(cnt.rx_fcserror_pkts);
+ rx_under = le32_to_cpu(cnt.rx_under_size_error_pkts);
+ rx_over = le32_to_cpu(cnt.rx_oversize_error_pkts);
+ rx_align = le32_to_cpu(cnt.rx_align_error_pkts);
+ tx_drop = le32_to_cpu(cnt.tx_dropped_pkts);
+ rx_drop = le32_to_cpu(cnt.rx_dropped_pkts);
+ rx_evlan = le32_to_cpu(cnt.rx_extended_vlan_discard_pkts);
+ mtu_exc = le32_to_cpu(cnt.mtu_exceed_discard_pkts);
+ tx_acm = le32_to_cpu(cnt.tx_acm_dropped_pkts);
+ rx_mcast = le32_to_cpu(cnt.rx_multicast_pkts);
+ tx_coll = le32_to_cpu(cnt.tx_coll_count);
+
+ /* Accumulate deltas under spinlock -- .get_stats64 reads these. */
+ spin_lock_bh(&priv->ports[port].stats_lock);
+
+ s->rx_packets += mxl862xx_delta32(rx_pkts, s->prev_rx_good_pkts);
+ s->tx_packets += mxl862xx_delta32(tx_pkts, s->prev_tx_good_pkts);
+ s->rx_bytes += rx_bytes - s->prev_rx_good_bytes;
+ s->tx_bytes += tx_bytes - s->prev_tx_good_bytes;
+
+ s->rx_errors +=
+ mxl862xx_delta32(rx_fcserr, s->prev_rx_fcserror_pkts) +
+ mxl862xx_delta32(rx_under, s->prev_rx_under_size_error_pkts) +
+ mxl862xx_delta32(rx_over, s->prev_rx_oversize_error_pkts) +
+ mxl862xx_delta32(rx_align, s->prev_rx_align_error_pkts);
+ s->tx_errors +=
+ mxl862xx_delta32(tx_drop, s->prev_tx_dropped_pkts);
+
+ s->rx_dropped +=
+ mxl862xx_delta32(rx_drop, s->prev_rx_dropped_pkts) +
+ mxl862xx_delta32(rx_evlan, s->prev_rx_evlan_discard_pkts) +
+ mxl862xx_delta32(mtu_exc, s->prev_mtu_exceed_discard_pkts);
+ s->tx_dropped +=
+ mxl862xx_delta32(tx_drop, s->prev_tx_dropped_pkts) +
+ mxl862xx_delta32(tx_acm, s->prev_tx_acm_dropped_pkts);
+
+ s->multicast += mxl862xx_delta32(rx_mcast, s->prev_rx_multicast_pkts);
+ s->collisions += mxl862xx_delta32(tx_coll, s->prev_tx_coll_count);
+
+ s->rx_length_errors +=
+ mxl862xx_delta32(rx_under, s->prev_rx_under_size_error_pkts) +
+ mxl862xx_delta32(rx_over, s->prev_rx_oversize_error_pkts);
+ s->rx_crc_errors +=
+ mxl862xx_delta32(rx_fcserr, s->prev_rx_fcserror_pkts);
+ s->rx_frame_errors +=
+ mxl862xx_delta32(rx_align, s->prev_rx_align_error_pkts);
+
+ s->prev_rx_good_pkts = rx_pkts;
+ s->prev_tx_good_pkts = tx_pkts;
+ s->prev_rx_good_bytes = rx_bytes;
+ s->prev_tx_good_bytes = tx_bytes;
+ s->prev_rx_fcserror_pkts = rx_fcserr;
+ s->prev_rx_under_size_error_pkts = rx_under;
+ s->prev_rx_oversize_error_pkts = rx_over;
+ s->prev_rx_align_error_pkts = rx_align;
+ s->prev_tx_dropped_pkts = tx_drop;
+ s->prev_rx_dropped_pkts = rx_drop;
+ s->prev_rx_evlan_discard_pkts = rx_evlan;
+ s->prev_mtu_exceed_discard_pkts = mtu_exc;
+ s->prev_tx_acm_dropped_pkts = tx_acm;
+ s->prev_rx_multicast_pkts = rx_mcast;
+ s->prev_tx_coll_count = tx_coll;
+
+ spin_unlock_bh(&priv->ports[port].stats_lock);
+}
+
+static void mxl862xx_stats_work_fn(struct work_struct *work)
+{
+ struct mxl862xx_priv *priv =
+ container_of(work, struct mxl862xx_priv, stats_work.work);
+ struct dsa_switch *ds = priv->ds;
+ struct dsa_port *dp;
+
+ dsa_switch_for_each_available_port(dp, ds)
+ mxl862xx_stats_poll(ds, dp->index);
+
+ schedule_delayed_work(&priv->stats_work,
+ MXL862XX_STATS_POLL_INTERVAL);
+}
+
+static void mxl862xx_get_stats64(struct dsa_switch *ds, int port,
+ struct rtnl_link_stats64 *s)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port_stats *ps = &priv->ports[port].stats;
+
+ spin_lock_bh(&priv->ports[port].stats_lock);
+
+ s->rx_packets = ps->rx_packets;
+ s->tx_packets = ps->tx_packets;
+ s->rx_bytes = ps->rx_bytes;
+ s->tx_bytes = ps->tx_bytes;
+ s->rx_errors = ps->rx_errors;
+ s->tx_errors = ps->tx_errors;
+ s->rx_dropped = ps->rx_dropped;
+ s->tx_dropped = ps->tx_dropped;
+ s->multicast = ps->multicast;
+ s->collisions = ps->collisions;
+ s->rx_length_errors = ps->rx_length_errors;
+ s->rx_crc_errors = ps->rx_crc_errors;
+ s->rx_frame_errors = ps->rx_frame_errors;
+
+ spin_unlock_bh(&priv->ports[port].stats_lock);
+
+ /* Trigger a fresh poll so the next read sees up-to-date counters.
+ * No-op if the work is already pending or running.
+ */
+ schedule_delayed_work(&priv->stats_work, 0);
+}
+
static const struct dsa_switch_ops mxl862xx_switch_ops = {
.get_tag_protocol = mxl862xx_get_tag_protocol,
.setup = mxl862xx_setup,
@@ -2132,6 +2291,7 @@ static const struct dsa_switch_ops mxl86
.get_eth_mac_stats = mxl862xx_get_eth_mac_stats,
.get_eth_ctrl_stats = mxl862xx_get_eth_ctrl_stats,
.get_pause_stats = mxl862xx_get_pause_stats,
+ .get_stats64 = mxl862xx_get_stats64,
};
static void mxl862xx_phylink_mac_config(struct phylink_config *config,
@@ -2193,8 +2353,11 @@ static int mxl862xx_probe(struct mdio_de
priv->ports[i].priv = priv;
INIT_WORK(&priv->ports[i].host_flood_work,
mxl862xx_host_flood_work_fn);
+ spin_lock_init(&priv->ports[i].stats_lock);
}
+ INIT_DELAYED_WORK(&priv->stats_work, mxl862xx_stats_work_fn);
+
dev_set_drvdata(dev, ds);
return dsa_register_switch(ds);
@@ -2213,6 +2376,8 @@ static void mxl862xx_remove(struct mdio_
dsa_unregister_switch(ds);
+ cancel_delayed_work_sync(&priv->stats_work);
+
mxl862xx_host_shutdown(priv);
/* Cancel any pending host flood work. dsa_unregister_switch()
@@ -2237,6 +2402,8 @@ static void mxl862xx_shutdown(struct mdi
dsa_switch_shutdown(ds);
+ cancel_delayed_work_sync(&priv->stats_work);
+
mxl862xx_host_shutdown(priv);
for (i = 0; i < MXL862XX_MAX_PORTS; i++)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -148,6 +148,47 @@ struct mxl862xx_evlan_block {
};
/**
+ * struct mxl862xx_port_stats - 64-bit accumulated hardware port statistics
+ *
+ * The firmware RMON counters are 32-bit free-running (64-bit for byte
+ * counters). This structure holds 64-bit accumulators alongside the
+ * previous raw snapshot so that deltas can be computed across polls,
+ * handling 32-bit wrap correctly via unsigned subtraction.
+ */
+struct mxl862xx_port_stats {
+ /* 64-bit accumulators */
+ u64 rx_packets;
+ u64 tx_packets;
+ u64 rx_bytes;
+ u64 tx_bytes;
+ u64 rx_errors;
+ u64 tx_errors;
+ u64 rx_dropped;
+ u64 tx_dropped;
+ u64 multicast;
+ u64 collisions;
+ u64 rx_length_errors;
+ u64 rx_crc_errors;
+ u64 rx_frame_errors;
+ /* Previous raw RMON values for delta computation */
+ u32 prev_rx_good_pkts;
+ u32 prev_tx_good_pkts;
+ u64 prev_rx_good_bytes;
+ u64 prev_tx_good_bytes;
+ u32 prev_rx_fcserror_pkts;
+ u32 prev_rx_under_size_error_pkts;
+ u32 prev_rx_oversize_error_pkts;
+ u32 prev_rx_align_error_pkts;
+ u32 prev_tx_dropped_pkts;
+ u32 prev_rx_dropped_pkts;
+ u32 prev_rx_evlan_discard_pkts;
+ u32 prev_mtu_exceed_discard_pkts;
+ u32 prev_tx_acm_dropped_pkts;
+ u32 prev_rx_multicast_pkts;
+ u32 prev_tx_coll_count;
+};
+
+/**
* struct mxl862xx_port - per-port state tracked by the driver
* @priv: back-pointer to switch private data; needed by
* deferred work handlers to access ds and priv
@@ -178,6 +219,10 @@ struct mxl862xx_evlan_block {
* The worker acquires rtnl_lock() to serialize with
* DSA callbacks and checks @setup_done to avoid
* acting on torn-down ports.
+ * @stats: 64-bit accumulated hardware statistics; updated
+ * periodically by the stats polling work
+ * @stats_lock: protects accumulator reads in .get_stats64 against
+ * concurrent updates from the polling work
*/
struct mxl862xx_port {
struct mxl862xx_priv *priv;
@@ -195,6 +240,9 @@ struct mxl862xx_port {
bool host_flood_uc;
bool host_flood_mc;
struct work_struct host_flood_work;
+ /* Hardware stats accumulation */
+ struct mxl862xx_port_stats stats;
+ spinlock_t stats_lock;
};
/**
@@ -216,6 +264,8 @@ struct mxl862xx_port {
* @evlan_ingress_size: per-port ingress Extended VLAN block size
* @evlan_egress_size: per-port egress Extended VLAN block size
* @vf_block_size: per-port VLAN Filter block size
+ * @stats_work: periodic work item that polls RMON hardware counters
+ * and accumulates them into 64-bit per-port stats
*/
struct mxl862xx_priv {
struct dsa_switch *ds;
@@ -228,6 +278,7 @@ struct mxl862xx_priv {
u16 evlan_ingress_size;
u16 evlan_egress_size;
u16 vf_block_size;
+ struct delayed_work stats_work;
};
#endif /* __MXL862XX_H */
@@ -0,0 +1,98 @@
From fecfbea928cd762b19ff17aa16fb1ab143d73db1 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 17:56:35 +0000
Subject: [PATCH 17/35] net: dsa: mxl862xx: store firmware version for feature
gating
Query the firmware version at init (already done in wait_ready),
cache it in priv->fw_version, and provide MXL862XX_FW_VER_MIN()
for version-gated code paths throughout the driver.
The union mxl862xx_fw_version lays out major/minor/revision so
that the u32 raw field compares with natural version ordering on
both big- and little-endian machines.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 3 +++
drivers/net/dsa/mxl862xx/mxl862xx.h | 36 +++++++++++++++++++++++++++++
2 files changed, 39 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -286,6 +286,9 @@ static int mxl862xx_wait_ready(struct ds
ver.iv_major, ver.iv_minor,
le16_to_cpu(ver.iv_revision),
le32_to_cpu(ver.iv_build_num));
+ priv->fw_version.major = ver.iv_major;
+ priv->fw_version.minor = ver.iv_minor;
+ priv->fw_version.revision = le16_to_cpu(ver.iv_revision);
return 0;
not_ready_yet:
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -3,6 +3,7 @@
#ifndef __MXL862XX_H
#define __MXL862XX_H
+#include <asm/byteorder.h>
#include <linux/mdio.h>
#include <linux/workqueue.h>
#include <net/dsa.h>
@@ -246,6 +247,38 @@ struct mxl862xx_port {
};
/**
+ * union mxl862xx_fw_version - firmware version for comparison and display
+ * @major: firmware major version
+ * @minor: firmware minor version
+ * @revision: firmware revision number
+ * @raw: combined u32 for direct >= comparison (major most significant)
+ *
+ * The struct layout places major in the most-significant byte of the
+ * u32 on both big- and little-endian machines, so raw values compare
+ * with the natural major > minor > revision ordering.
+ */
+union mxl862xx_fw_version {
+ struct {
+#if defined(__BIG_ENDIAN)
+ u8 major;
+ u8 minor;
+ u16 revision;
+#elif defined(__LITTLE_ENDIAN)
+ u16 revision;
+ u8 minor;
+ u8 major;
+#endif
+ };
+ u32 raw;
+};
+
+#define MXL862XX_FW_VER(maj, min, rev) \
+ ((union mxl862xx_fw_version){ .major = (maj), .minor = (min), \
+ .revision = (rev) }).raw
+#define MXL862XX_FW_VER_MIN(priv, maj, min, rev) \
+ ((priv)->fw_version.raw >= MXL862XX_FW_VER(maj, min, rev))
+
+/**
* struct mxl862xx_priv - driver private data for an MxL862xx switch
* @ds: pointer to the DSA switch instance
* @mdiodev: MDIO device used to communicate with the switch firmware
@@ -256,6 +289,8 @@ struct mxl862xx_port {
* @drop_meter: index of the single shared zero-rate firmware meter
* used to unconditionally drop traffic (used to block
* flooding)
+ * @fw_version: cached firmware version, populated at probe and
+ * compared with MXL862XX_FW_VER_MIN()
* @ports: per-port state, indexed by switch port number
* @bridges: maps DSA bridge number to firmware bridge ID;
* zero means no firmware bridge allocated for that
@@ -273,6 +308,7 @@ struct mxl862xx_priv {
struct work_struct crc_err_work;
unsigned long crc_err;
u16 drop_meter;
+ union mxl862xx_fw_version fw_version;
struct mxl862xx_port ports[MXL862XX_MAX_PORTS];
u16 bridges[MXL862XX_MAX_BRIDGES + 1];
u16 evlan_ingress_size;
@@ -0,0 +1,163 @@
From 3cb224514226928df80e43ca2280c7dca654bdfe Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Wed, 25 Mar 2026 21:39:30 +0000
Subject: [PATCH 18/35] net: dsa: mxl862xx: move phylink stubs to
mxl862xx-phylink.c
Move the phylink MAC operations and get_caps callback from mxl862xx.c
into a dedicated mxl862xx-phylink.c file. This prepares for the SerDes
PCS implementation which adds substantial phylink/PCS code -- keeping
it in a separate file avoids function-position churn in the main
driver file.
No functional change.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/Makefile | 2 +-
drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 51 +++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-phylink.h | 14 ++++++
drivers/net/dsa/mxl862xx/mxl862xx.c | 38 +--------------
4 files changed, 67 insertions(+), 38 deletions(-)
create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
--- a/drivers/net/dsa/mxl862xx/Makefile
+++ b/drivers/net/dsa/mxl862xx/Makefile
@@ -1,3 +1,3 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_NET_DSA_MXL862) += mxl862xx_dsa.o
-mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o
+mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Phylink and PCS support for MaxLinear MxL862xx switch family
+ *
+ * Copyright (C) 2024 MaxLinear Inc.
+ * Copyright (C) 2025 John Crispin <john@phrozen.org>
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ */
+
+#include <linux/phylink.h>
+#include <net/dsa.h>
+
+#include "mxl862xx.h"
+#include "mxl862xx-phylink.h"
+
+void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
+ struct phylink_config *config)
+{
+ config->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
+ MAC_100 | MAC_1000 | MAC_2500FD;
+
+ __set_bit(PHY_INTERFACE_MODE_INTERNAL,
+ config->supported_interfaces);
+}
+
+static void mxl862xx_phylink_mac_config(struct phylink_config *config,
+ unsigned int mode,
+ const struct phylink_link_state *state)
+{
+}
+
+static void mxl862xx_phylink_mac_link_down(struct phylink_config *config,
+ unsigned int mode,
+ phy_interface_t interface)
+{
+}
+
+static void mxl862xx_phylink_mac_link_up(struct phylink_config *config,
+ struct phy_device *phydev,
+ unsigned int mode,
+ phy_interface_t interface,
+ int speed, int duplex,
+ bool tx_pause, bool rx_pause)
+{
+}
+
+const struct phylink_mac_ops mxl862xx_phylink_mac_ops = {
+ .mac_config = mxl862xx_phylink_mac_config,
+ .mac_link_down = mxl862xx_phylink_mac_link_down,
+ .mac_link_up = mxl862xx_phylink_mac_link_up,
+};
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_PHYLINK_H
+#define __MXL862XX_PHYLINK_H
+
+#include <linux/phylink.h>
+
+#include "mxl862xx.h"
+
+extern const struct phylink_mac_ops mxl862xx_phylink_mac_ops;
+void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
+ struct phylink_config *config);
+
+#endif /* __MXL862XX_PHYLINK_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -22,6 +22,7 @@
#include "mxl862xx-api.h"
#include "mxl862xx-cmd.h"
#include "mxl862xx-host.h"
+#include "mxl862xx-phylink.h"
#define MXL862XX_API_WRITE(dev, cmd, data) \
mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), false, false)
@@ -1642,16 +1643,6 @@ static void mxl862xx_port_teardown(struc
priv->ports[port].setup_done = false;
}
-static void mxl862xx_phylink_get_caps(struct dsa_switch *ds, int port,
- struct phylink_config *config)
-{
- config->mac_capabilities = MAC_ASYM_PAUSE | MAC_SYM_PAUSE | MAC_10 |
- MAC_100 | MAC_1000 | MAC_2500FD;
-
- __set_bit(PHY_INTERFACE_MODE_INTERNAL,
- config->supported_interfaces);
-}
-
static int mxl862xx_get_fid(struct dsa_switch *ds, struct dsa_db db)
{
struct mxl862xx_priv *priv = ds->priv;
@@ -2297,33 +2288,6 @@ static const struct dsa_switch_ops mxl86
.get_stats64 = mxl862xx_get_stats64,
};
-static void mxl862xx_phylink_mac_config(struct phylink_config *config,
- unsigned int mode,
- const struct phylink_link_state *state)
-{
-}
-
-static void mxl862xx_phylink_mac_link_down(struct phylink_config *config,
- unsigned int mode,
- phy_interface_t interface)
-{
-}
-
-static void mxl862xx_phylink_mac_link_up(struct phylink_config *config,
- struct phy_device *phydev,
- unsigned int mode,
- phy_interface_t interface,
- int speed, int duplex,
- bool tx_pause, bool rx_pause)
-{
-}
-
-static const struct phylink_mac_ops mxl862xx_phylink_mac_ops = {
- .mac_config = mxl862xx_phylink_mac_config,
- .mac_link_down = mxl862xx_phylink_mac_link_down,
- .mac_link_up = mxl862xx_phylink_mac_link_up,
-};
-
static int mxl862xx_probe(struct mdio_device *mdiodev)
{
struct device *dev = &mdiodev->dev;
@@ -0,0 +1,53 @@
From de41d438c4e90876449715a307dd03fa37338742 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Thu, 26 Mar 2026 01:50:00 +0000
Subject: [PATCH 19/35] net: dsa: mxl862xx: move API macros to mxl862xx-host.h
Move the MXL862XX_API_WRITE, MXL862XX_API_READ and
MXL862XX_API_READ_QUIET convenience macros from mxl862xx.c to
mxl862xx-host.h next to the mxl862xx_api_wrap() prototype they wrap.
This makes them available to other compilation units that include
mxl862xx-host.h, which is needed once the SerDes PCS code in
mxl862xx-phylink.c also calls firmware commands.
No functional change.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-host.h | 8 ++++++++
drivers/net/dsa/mxl862xx/mxl862xx.c | 7 -------
2 files changed, 8 insertions(+), 7 deletions(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
@@ -9,6 +9,14 @@ void mxl862xx_host_init(struct mxl862xx_
void mxl862xx_host_shutdown(struct mxl862xx_priv *priv);
int mxl862xx_api_wrap(struct mxl862xx_priv *priv, u16 cmd, void *data, u16 size,
bool read, bool quiet);
+
+#define MXL862XX_API_WRITE(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), false, false)
+#define MXL862XX_API_READ(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, false)
+#define MXL862XX_API_READ_QUIET(dev, cmd, data) \
+ mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
+
int mxl862xx_reset(struct mxl862xx_priv *priv);
#endif /* __MXL862XX_HOST_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -24,13 +24,6 @@
#include "mxl862xx-host.h"
#include "mxl862xx-phylink.h"
-#define MXL862XX_API_WRITE(dev, cmd, data) \
- mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), false, false)
-#define MXL862XX_API_READ(dev, cmd, data) \
- mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, false)
-#define MXL862XX_API_READ_QUIET(dev, cmd, data) \
- mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
-
/* Polling interval for RMON counter accumulation. At 2.5 Gbps with
* minimum-size (64-byte) frames, a 32-bit packet counter wraps in ~880s.
* 2s gives a comfortable margin.
@@ -0,0 +1,268 @@
From d40565e2e00fc2c8f04b9c571fcbea2f146db844 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 18:14:33 +0000
Subject: [PATCH 21/35] net: dsa: mxl862xx: add SerDes ethtool statistics
Expose SerDes equalization and signal detect parameters as ethtool
statistics on ports 9-16 (XPCS-backed ports). Uses the XPCS EQ_GET
and SIGNAL_DETECT firmware commands to read TX/RX equalization
coefficients, DFE taps, and link-level signal status.
The 19 additional stats (serdes_tx_*, serdes_rx_*, serdes_pma_link,
serdes_link_fault, serdes_in_reset) are appended after the standard
RMON counters and gated on firmware >= 1.0.80.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-api.h | 88 +++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 2 +
drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 93 +++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-phylink.h | 3 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 6 +-
5 files changed, 191 insertions(+), 1 deletion(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -1812,4 +1812,92 @@ struct mxl862xx_xpcs_reset_cfg {
__le16 result;
} __packed;
+/**
+ * struct mxl862xx_xpcs_eq_item - single equalization parameter
+ * @value: current initial value
+ * @ovrd: override value
+ * @ovrd_en: override enable flag
+ */
+struct mxl862xx_xpcs_eq_item {
+ u8 value;
+ u8 ovrd;
+ u8 ovrd_en;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_tx_eq_info - TX equalization status
+ * @main: TX main cursor (0-63)
+ * @pre: TX pre-cursor (0-63)
+ * @post: TX post-cursor (0-63)
+ * @iboost_lvl: TX iboost level (0-15)
+ * @vboost_lvl: TX vboost level (0-7)
+ * @vboost_en: TX vboost enable (0-1)
+ */
+struct mxl862xx_xpcs_tx_eq_info {
+ struct mxl862xx_xpcs_eq_item main;
+ struct mxl862xx_xpcs_eq_item pre;
+ struct mxl862xx_xpcs_eq_item post;
+ struct mxl862xx_xpcs_eq_item iboost_lvl;
+ struct mxl862xx_xpcs_eq_item vboost_lvl;
+ struct mxl862xx_xpcs_eq_item vboost_en;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_rx_eq_info - RX equalization status
+ * @att_lvl: RX attenuation level (0-7)
+ * @vga1_gain: RX VGA1 gain (0-7)
+ * @vga2_gain: RX VGA2 gain (0-7)
+ * @ctle_boost: RX CTLE boost (0-31)
+ * @ctle_pole: RX CTLE pole (0-3)
+ * @dfe_tap1: RX DFE tap1 (0-255)
+ * @dfe_bypass: RX DFE bypass (0-1)
+ * @adapt_mode: RX adapt mode (0-3)
+ * @adapt_sel: RX adapt select (0-1)
+ */
+struct mxl862xx_xpcs_rx_eq_info {
+ struct mxl862xx_xpcs_eq_item att_lvl;
+ struct mxl862xx_xpcs_eq_item vga1_gain;
+ struct mxl862xx_xpcs_eq_item vga2_gain;
+ struct mxl862xx_xpcs_eq_item ctle_boost;
+ struct mxl862xx_xpcs_eq_item ctle_pole;
+ struct mxl862xx_xpcs_eq_item dfe_tap1;
+ struct mxl862xx_xpcs_eq_item dfe_bypass;
+ struct mxl862xx_xpcs_eq_item adapt_mode;
+ struct mxl862xx_xpcs_eq_item adapt_sel;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_eq_get - EQ get request/response
+ * @port_id: XPCS port index (0 or 1)
+ * @result: firmware result
+ * @tx: TX equalization info
+ * @rx: RX equalization info
+ */
+struct mxl862xx_xpcs_eq_get {
+ u8 port_id;
+ __le16 result;
+ struct mxl862xx_xpcs_tx_eq_info tx;
+ struct mxl862xx_xpcs_rx_eq_info rx;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_signal_detect - signal detect status
+ * @port_id: XPCS port index (0 or 1)
+ * @rx_signal: RX signal detected
+ * @pma_link: PMA link up
+ * @link_fault: PCS link fault
+ * @in_reset: XPCS in reset
+ * @result: firmware result
+ */
+struct mxl862xx_xpcs_signal_detect {
+ u8 port_id:2;
+ u8 rx_signal:1;
+ u8 pma_link:1;
+ u8 link_fault:1;
+ u8 in_reset:1;
+ u8 __rsv:2;
+ u8 __pad;
+ __le16 result;
+} __packed;
+
#endif /* __MXL862XX_API_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -83,6 +83,8 @@
#define MXL862XX_XPCS_FORCE_SPEED (MXL862XX_XPCS_MAGIC + 0x7)
#define MXL862XX_XPCS_LOOPBACK (MXL862XX_XPCS_MAGIC + 0x8)
#define MXL862XX_XPCS_RESET (MXL862XX_XPCS_MAGIC + 0x9)
+#define MXL862XX_XPCS_EQ_GET (MXL862XX_XPCS_MAGIC + 0xc)
+#define MXL862XX_XPCS_SIGNAL_DETECT (MXL862XX_XPCS_MAGIC + 0xd)
#define MMD_API_MAXIMUM_ID 0x7fff
--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
@@ -439,3 +439,96 @@ const struct phylink_mac_ops mxl862xx_ph
.mac_link_up = mxl862xx_phylink_mac_link_up,
.mac_select_pcs = mxl862xx_phylink_mac_select_pcs,
};
+
+/* --- SerDes ethtool statistics --- */
+
+static const char mxl862xx_serdes_stats[][ETH_GSTRING_LEN] = {
+ "serdes_tx_main",
+ "serdes_tx_pre",
+ "serdes_tx_post",
+ "serdes_tx_iboost",
+ "serdes_tx_vboost",
+ "serdes_tx_vboost_en",
+ "serdes_rx_att",
+ "serdes_rx_vga1",
+ "serdes_rx_vga2",
+ "serdes_rx_ctle_boost",
+ "serdes_rx_ctle_pole",
+ "serdes_rx_dfe_tap1",
+ "serdes_rx_dfe_bypass",
+ "serdes_rx_adapt_mode",
+ "serdes_rx_adapt_sel",
+ "serdes_rx_signal",
+ "serdes_pma_link",
+ "serdes_link_fault",
+ "serdes_in_reset",
+};
+
+static bool mxl862xx_port_has_serdes_stats(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+
+ return port >= 9 && port <= 16 &&
+ MXL862XX_FW_VER_MIN(priv, 1, 0, 80);
+}
+
+int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port)
+{
+ if (mxl862xx_port_has_serdes_stats(ds, port))
+ return ARRAY_SIZE(mxl862xx_serdes_stats);
+
+ return 0;
+}
+
+void mxl862xx_serdes_get_strings(struct dsa_switch *ds, int port, u8 *data)
+{
+ int i;
+
+ if (!mxl862xx_port_has_serdes_stats(ds, port))
+ return;
+
+ for (i = 0; i < ARRAY_SIZE(mxl862xx_serdes_stats); i++)
+ ethtool_puts(&data, mxl862xx_serdes_stats[i]);
+}
+
+void mxl862xx_serdes_get_stats(struct dsa_switch *ds, int port, u64 *data)
+{
+ struct mxl862xx_xpcs_eq_get eq = {
+ .port_id = mxl862xx_xpcs_port_id(port),
+ };
+ struct mxl862xx_xpcs_signal_detect sig = {};
+
+ if (!mxl862xx_port_has_serdes_stats(ds, port))
+ return;
+
+ sig.port_id = mxl862xx_xpcs_port_id(port);
+
+ if (!MXL862XX_API_READ(ds->priv, MXL862XX_XPCS_EQ_GET, eq)) {
+ *data++ = eq.tx.main.value;
+ *data++ = eq.tx.pre.value;
+ *data++ = eq.tx.post.value;
+ *data++ = eq.tx.iboost_lvl.value;
+ *data++ = eq.tx.vboost_lvl.value;
+ *data++ = eq.tx.vboost_en.value;
+ *data++ = eq.rx.att_lvl.value;
+ *data++ = eq.rx.vga1_gain.value;
+ *data++ = eq.rx.vga2_gain.value;
+ *data++ = eq.rx.ctle_boost.value;
+ *data++ = eq.rx.ctle_pole.value;
+ *data++ = eq.rx.dfe_tap1.value;
+ *data++ = eq.rx.dfe_bypass.value;
+ *data++ = eq.rx.adapt_mode.value;
+ *data++ = eq.rx.adapt_sel.value;
+ } else {
+ data += 15;
+ }
+
+ if (!MXL862XX_API_READ(ds->priv, MXL862XX_XPCS_SIGNAL_DETECT, sig)) {
+ *data++ = sig.rx_signal;
+ *data++ = sig.pma_link;
+ *data++ = sig.link_fault;
+ *data++ = sig.in_reset;
+ } else {
+ data += 4;
+ }
+}
--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
@@ -12,5 +12,8 @@ void mxl862xx_phylink_get_caps(struct ds
struct phylink_config *config);
void mxl862xx_setup_pcs(struct mxl862xx_priv *priv, struct mxl862xx_pcs *pcs,
int port);
+int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port);
+void mxl862xx_serdes_get_strings(struct dsa_switch *ds, int port, u8 *data);
+void mxl862xx_serdes_get_stats(struct dsa_switch *ds, int port, u64 *data);
#endif /* __MXL862XX_PHYLINK_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -2007,6 +2007,8 @@ static void mxl862xx_get_strings(struct
for (i = 0; i < ARRAY_SIZE(mxl862xx_mib); i++)
ethtool_puts(&data, mxl862xx_mib[i].name);
+
+ mxl862xx_serdes_get_strings(ds, port, data);
}
static int mxl862xx_get_sset_count(struct dsa_switch *ds, int port, int sset)
@@ -2014,7 +2016,7 @@ static int mxl862xx_get_sset_count(struc
if (sset != ETH_SS_STATS)
return 0;
- return ARRAY_SIZE(mxl862xx_mib);
+ return ARRAY_SIZE(mxl862xx_mib) + mxl862xx_serdes_stats_count(ds, port);
}
static int mxl862xx_read_rmon(struct dsa_switch *ds, int port,
@@ -2050,6 +2052,8 @@ static void mxl862xx_get_ethtool_stats(s
else
*data++ = le64_to_cpu(*(__le64 *)field);
}
+
+ mxl862xx_serdes_get_stats(ds, port, data);
}
static void mxl862xx_get_eth_mac_stats(struct dsa_switch *ds, int port,
@@ -0,0 +1,208 @@
From 54dd5fabc543f8538202367a863eb0e9161bacab Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 18:15:32 +0000
Subject: [PATCH 22/35] net: dsa: mxl862xx: add SerDes self-test via PRBS and
BERT
Implement the dsa_switch_ops.self_test callback for SerDes ports
(9-16). Two loopback tests are run:
1. PCS-level PRBS31: enables TX/RX PRBS31 pattern at the PCS layer,
waits 100ms, then reads the error counter.
2. SerDes-level BERT PRBS31: enables TX/RX BERT with PRBS31 pattern
at the SerDes layer, waits 100ms, then reads the error counter.
Both tests clean up (disable pattern generators) regardless of outcome.
Gated on firmware >= 1.0.80 via mxl862xx_port_has_serdes_stats().
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-api.h | 45 +++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 2 +
drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 85 +++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-phylink.h | 3 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 1 +
5 files changed, 136 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -1900,4 +1900,49 @@ struct mxl862xx_xpcs_signal_detect {
__le16 result;
} __packed;
+/**
+ * struct mxl862xx_xpcs_prbs_cfg - PCS-level PRBS31 test pattern
+ * @port_id: XPCS port index (0 or 1)
+ * @tx_en: TX PRBS31 enable
+ * @rx_en: RX PRBS31 enable
+ * @read_err: read error count
+ * @rx_err_cnt: RX PRBS31 error count (valid when read_err=1)
+ * @result: firmware result
+ */
+struct mxl862xx_xpcs_prbs_cfg {
+ u8 port_id:2;
+ u8 tx_en:1;
+ u8 rx_en:1;
+ u8 read_err:1;
+ u8 __rsv:3;
+ u8 __pad;
+ __le16 rx_err_cnt;
+ __le16 result;
+} __packed;
+
+/**
+ * struct mxl862xx_xpcs_bert_cfg - SerDes-level BERT test pattern
+ * @port_id: XPCS port index (0 or 1)
+ * @tx_en: TX BERT enable
+ * @rx_en: RX BERT enable
+ * @read_err: read RX error count
+ * @clear_err: clear RX error counter
+ * @insert_err: insert one TX error
+ * @pattern: PRBS pattern type (1-7; 0 = disable)
+ * @rx_err_cnt: RX BERT error count (valid when read_err=1)
+ * @result: firmware result
+ */
+struct mxl862xx_xpcs_bert_cfg {
+ u8 port_id:2;
+ u8 tx_en:1;
+ u8 rx_en:1;
+ u8 read_err:1;
+ u8 clear_err:1;
+ u8 insert_err:1;
+ u8 __rsv:1;
+ u8 pattern;
+ __le16 rx_err_cnt;
+ __le16 result;
+} __packed;
+
#endif /* __MXL862XX_API_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -83,6 +83,8 @@
#define MXL862XX_XPCS_FORCE_SPEED (MXL862XX_XPCS_MAGIC + 0x7)
#define MXL862XX_XPCS_LOOPBACK (MXL862XX_XPCS_MAGIC + 0x8)
#define MXL862XX_XPCS_RESET (MXL862XX_XPCS_MAGIC + 0x9)
+#define MXL862XX_XPCS_PRBS_CFG (MXL862XX_XPCS_MAGIC + 0xa)
+#define MXL862XX_XPCS_BERT_CFG (MXL862XX_XPCS_MAGIC + 0xb)
#define MXL862XX_XPCS_EQ_GET (MXL862XX_XPCS_MAGIC + 0xc)
#define MXL862XX_XPCS_SIGNAL_DETECT (MXL862XX_XPCS_MAGIC + 0xd)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
@@ -532,3 +532,88 @@ void mxl862xx_serdes_get_stats(struct ds
data += 4;
}
}
+
+void mxl862xx_serdes_self_test(struct dsa_switch *ds, int port,
+ struct ethtool_test *etest, u64 *data)
+{
+ struct mxl862xx_xpcs_prbs_cfg prbs = {};
+ struct mxl862xx_xpcs_bert_cfg bert = {};
+ struct mxl862xx_priv *priv = ds->priv;
+ int xpcs_id = mxl862xx_xpcs_port_id(port);
+ int i = 0;
+ int ret;
+
+ if (!mxl862xx_port_has_serdes_stats(ds, port))
+ return;
+
+ /* Test 1: PCS PRBS31 */
+ prbs.port_id = xpcs_id;
+ prbs.tx_en = 1;
+ prbs.rx_en = 1;
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PRBS_CFG, prbs);
+ if (ret) {
+ data[i++] = 1;
+ goto skip_prbs;
+ }
+
+ msleep(100);
+
+ memset(&prbs, 0, sizeof(prbs));
+ prbs.port_id = xpcs_id;
+ prbs.read_err = 1;
+ ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_PRBS_CFG, prbs);
+
+ /* Disable PRBS */
+ memset(&prbs, 0, sizeof(prbs));
+ prbs.port_id = xpcs_id;
+ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_PRBS_CFG, prbs);
+
+ if (ret) {
+ data[i++] = 1;
+ } else {
+ data[i] = le16_to_cpu(prbs.rx_err_cnt) ? 1 : 0;
+ if (data[i])
+ etest->flags |= ETH_TEST_FL_FAILED;
+ i++;
+ }
+
+skip_prbs:
+ /* Test 2: SerDes BERT PRBS31 -- clear error counter first */
+ bert.port_id = xpcs_id;
+ bert.clear_err = 1;
+ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_BERT_CFG, bert);
+
+ /* Enable BERT with PRBS31 pattern */
+ memset(&bert, 0, sizeof(bert));
+ bert.port_id = xpcs_id;
+ bert.tx_en = 1;
+ bert.rx_en = 1;
+ bert.pattern = 6; /* PRBS31 */
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_XPCS_BERT_CFG, bert);
+ if (ret) {
+ data[i++] = 1;
+ return;
+ }
+
+ msleep(100);
+
+ /* Read error count */
+ memset(&bert, 0, sizeof(bert));
+ bert.port_id = xpcs_id;
+ bert.read_err = 1;
+ ret = MXL862XX_API_READ(priv, MXL862XX_XPCS_BERT_CFG, bert);
+
+ /* Disable BERT */
+ memset(&bert, 0, sizeof(bert));
+ bert.port_id = xpcs_id;
+ MXL862XX_API_WRITE(priv, MXL862XX_XPCS_BERT_CFG, bert);
+
+ if (ret) {
+ data[i++] = 1;
+ } else {
+ data[i] = le16_to_cpu(bert.rx_err_cnt) ? 1 : 0;
+ if (data[i])
+ etest->flags |= ETH_TEST_FL_FAILED;
+ i++;
+ }
+}
--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.h
@@ -3,6 +3,7 @@
#ifndef __MXL862XX_PHYLINK_H
#define __MXL862XX_PHYLINK_H
+#include <linux/ethtool.h>
#include <linux/phylink.h>
#include "mxl862xx.h"
@@ -15,5 +16,7 @@ void mxl862xx_setup_pcs(struct mxl862xx_
int mxl862xx_serdes_stats_count(struct dsa_switch *ds, int port);
void mxl862xx_serdes_get_strings(struct dsa_switch *ds, int port, u8 *data);
void mxl862xx_serdes_get_stats(struct dsa_switch *ds, int port, u64 *data);
+void mxl862xx_serdes_self_test(struct dsa_switch *ds, int port,
+ struct ethtool_test *etest, u64 *data);
#endif /* __MXL862XX_PHYLINK_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -2286,6 +2286,7 @@ static const struct dsa_switch_ops mxl86
.get_eth_ctrl_stats = mxl862xx_get_eth_ctrl_stats,
.get_pause_stats = mxl862xx_get_pause_stats,
.get_stats64 = mxl862xx_get_stats64,
+ .self_test = mxl862xx_serdes_self_test,
};
static int mxl862xx_probe(struct mdio_device *mdiodev)
@@ -0,0 +1,831 @@
From dd62e68cd0bd29934c3efbce687d5e103cc4b331 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 18:51:13 +0000
Subject: [PATCH 23/35] net: dsa: mxl862xx: trap link-local frames to the CPU
port
Install per-CTP PCE rules on each user port that trap IEEE 802.1D
link-local frames (01:80:c2:00:00:0x) to the CPU port via an
explicit forwarding portmap with cross-state enabled, ensuring the
frames reach the host even when the bridge port is in BLOCKING or
LEARNING state.
Add the PCE rule firmware API structures, command definitions, and
the rule block allocation interface.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-api.h | 684 ++++++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 5 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 69 +++
3 files changed, 758 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -1420,6 +1420,690 @@ struct mxl862xx_port_link_cfg {
u8 lpi;
} __packed;
+/* PCE (Packet Classification Engine) rule structures.
+ *
+ * Binary layout must exactly match the firmware's GSW_PCE_rule_t
+ * (gsw_flow.h, packed little-endian). The firmware deserializes
+ * the structure directly from the MDIO data buffer produced by
+ * mxl862xx_api_wrap().
+ */
+
+/**
+ * union mxl862xx_ip - IPv4 or IPv6 address
+ * @ipv4: IPv4 address in little-endian
+ * @ipv6: IPv6 address as array of little-endian 16-bit words
+ */
+union mxl862xx_ip {
+ __le32 ipv4;
+ __le16 ipv6[8];
+} __packed;
+
+/**
+ * struct mxl862xx_pce_pattern - PCE rule match pattern
+ *
+ * Every field must remain in the order shown; the firmware
+ * interprets the buffer positionally.
+ *
+ * @index: PCE rule index (0..511)
+ * @dst_ip_mask: Destination IP nibble mask (outer)
+ * @inner_dst_ip_mask: Inner destination IP nibble mask
+ * @src_ip_mask: Source IP nibble mask (outer)
+ * @inner_src_ip_mask: Inner source IP nibble mask
+ * @sub_if_id: Incoming sub-interface ID value
+ * @pkt_lng: Packet length in bytes
+ * @pkt_lng_range: Packet length range upper bound
+ * @mac_dst_mask: Destination MAC nibble mask
+ * @mac_src_mask: Source MAC nibble mask
+ * @app_data_msb: MSB application field (first 2 bytes after IP header,
+ * typically TCP/UDP source port)
+ * @app_mask_range_msb: MSB application mask or range value
+ * @ether_type: EtherType value to match
+ * @ether_type_mask: EtherType nibble mask
+ * @session_id: PPPoE session ID
+ * @ppp_protocol: PPP protocol value
+ * @ppp_protocol_mask: PPP protocol bit mask
+ * @vid: CTAG VLAN ID (inner VLAN)
+ * @slan_vid: STAG VLAN ID (outer VLAN)
+ * @flex_field4_value: Flexible field 4 match value
+ * @flex_field4_mask_range: Flexible field 4 mask or range
+ * @flex_field3_value: Flexible field 3 match value
+ * @flex_field3_mask_range: Flexible field 3 mask or range
+ * @flex_field1_value: Flexible field 1 match value
+ * @flex_field1_mask_range: Flexible field 1 mask or range
+ * @outer_vid_range: Outer VLAN ID range
+ * @payload1: Payload-1 value (16-bit)
+ * @payload1_mask: Payload-1 bit mask
+ * @payload2: Payload-2 value (16-bit)
+ * @payload2_mask: Payload-2 bit mask
+ * @parser_flag_lsb: Parser flag LSW value (bits 15:0)
+ * @parser_flag_lsb_mask: Parser flag LSW mask (1 = masked out)
+ * @parser_flag_msb: Parser flag MSW value (bits 31:16)
+ * @parser_flag_msb_mask: Parser flag MSW mask (1 = masked out)
+ * @parser_flag1_lsb: Parser flag1 LSW value (bits 47:32)
+ * @parser_flag1_lsb_mask: Parser flag1 LSW mask
+ * @parser_flag1_msb: Parser flag1 MSW value (bits 63:48)
+ * @parser_flag1_msb_mask: Parser flag1 MSW mask
+ * @app_data_lsb: LSB application field (next 2 bytes after @app_data_msb,
+ * typically TCP/UDP destination port)
+ * @app_mask_range_lsb: LSB application mask or range value
+ * @insertion_flag: CPU-inserted packet flag
+ * @vid_range: CTAG VLAN ID range (used as mask when @vid_range_select is 0)
+ * @flex_field2_mask_range: Flexible field 2 mask or range
+ * @flex_field2_value: Flexible field 2 match value
+ * @port_id: Ingress port ID for classification
+ * @dscp: Outer DSCP value
+ * @inner_dscp: Inner DSCP value
+ * @pcp: CTAG VLAN PCP (bits 2:0) and DEI (bit 3)
+ * @stag_pcp_dei: STAG VLAN PCP (bits 2:0) and DEI (bit 3)
+ * @mac_dst: Destination MAC address
+ * @mac_src: Source MAC address
+ * @protocol: Outer IP protocol value
+ * @protocol_mask: Outer IP protocol nibble mask
+ * @inner_protocol: Inner IP protocol value
+ * @inner_protocol_mask: Inner IP protocol bit mask
+ * @flex_field4_parser_index: Flexible field 4 parser output index (0..127)
+ * @flex_field3_parser_index: Flexible field 3 parser output index (0..127)
+ * @flex_field1_parser_index: Flexible field 1 parser output index (0..127)
+ * @flex_field2_parser_index: Flexible field 2 parser output index (0..127)
+ * @enable: Rule is enabled (used) or disabled (unused)
+ * @port_id_enable: Enable ingress port ID matching
+ * @port_id_exclude: Exclude (negate) port ID match
+ * @sub_if_id_type: Sub-interface ID field mode selector
+ * @sub_if_id_enable: Enable sub-interface ID matching
+ * @sub_if_id_exclude: Exclude sub-interface ID match
+ * @dscp_enable: Enable outer DSCP matching
+ * @dscp_exclude: Exclude outer DSCP match
+ * @inner_dscp_enable: Enable inner DSCP matching
+ * @inner_dscp_exclude: Exclude inner DSCP match
+ * @pcp_enable: Enable CTAG PCP/DEI matching
+ * @ctag_pcp_dei_exclude: Exclude CTAG PCP/DEI match
+ * @stag_pcp_dei_enable: Enable STAG PCP/DEI matching
+ * @stag_pcp_dei_exclude: Exclude STAG PCP/DEI match
+ * @pkt_lng_enable: Enable packet length matching
+ * @pkt_lng_exclude: Exclude packet length match
+ * @mac_dst_enable: Enable destination MAC matching
+ * @dst_mac_exclude: Exclude destination MAC match
+ * @mac_src_enable: Enable source MAC matching
+ * @src_mac_exclude: Exclude source MAC match
+ * @app_data_msb_enable: Enable MSB application field matching
+ * @app_mask_range_msb_select: MSB application mask/range selection
+ * (0 = nibble mask, 1 = range)
+ * @app_msb_exclude: Exclude MSB application match
+ * @app_data_lsb_enable: Enable LSB application field matching
+ * @app_mask_range_lsb_select: LSB application mask/range selection
+ * (0 = nibble mask, 1 = range)
+ * @app_lsb_exclude: Exclude LSB application match
+ * @dst_ip_select: Outer destination IP selection
+ * (0 = disabled, 1 = IPv4, 2 = IPv6)
+ * @dst_ip: Outer destination IP address
+ * @dst_ip_exclude: Exclude outer destination IP match
+ * @inner_dst_ip_select: Inner destination IP selection
+ * @inner_dst_ip: Inner destination IP address
+ * @inner_dst_ip_exclude: Exclude inner destination IP match
+ * @src_ip_select: Outer source IP selection
+ * (0 = disabled, 1 = IPv4, 2 = IPv6)
+ * @src_ip: Outer source IP address
+ * @src_ip_exclude: Exclude outer source IP match
+ * @inner_src_ip_select: Inner source IP selection
+ * @inner_src_ip: Inner source IP address
+ * @inner_src_ip_exclude: Exclude inner source IP match
+ * @ether_type_enable: Enable EtherType matching
+ * @ether_type_exclude: Exclude EtherType match
+ * @protocol_enable: Enable outer IP protocol matching
+ * @protocol_exclude: Exclude outer IP protocol match
+ * @inner_protocol_enable: Enable inner IP protocol matching
+ * @inner_protocol_exclude: Exclude inner IP protocol match
+ * @session_id_enable: Enable PPPoE session ID matching
+ * @session_id_exclude: Exclude PPPoE session ID match
+ * @ppp_protocol_enable: Enable PPP protocol matching
+ * @ppp_protocol_exclude: Exclude PPP protocol match
+ * @vid_used: Enable CTAG VLAN ID matching
+ * @vid_range_select: CVLAN mask/range selection (0 = mask, 1 = range)
+ * @vid_exclude: Exclude CTAG VLAN ID match
+ * @vid_original: Use original VLAN ID as key even if modified earlier
+ * @slan_vid_used: Enable STAG VLAN ID matching
+ * @slan_vid_exclude: Exclude STAG VLAN ID match
+ * @svid_range_select: SVLAN mask/range selection (0 = mask, 1 = range)
+ * @outer_vid_original: Use original outer VLAN ID as key even if modified
+ * @payload1_src_enable: Enable payload-1 matching
+ * @payload1_mask_range_select: Payload-1 mask/range selection
+ * (0 = bit mask, 1 = range)
+ * @payload1_exclude: Exclude payload-1 match
+ * @payload2_src_enable: Enable payload-2 matching
+ * @payload2_mask_range_select: Payload-2 mask/range selection
+ * (0 = bit mask, 1 = range)
+ * @payload2_exclude: Exclude payload-2 match
+ * @parser_flag_lsb_enable: Enable parser flag LSW matching
+ * @parser_flag_lsb_exclude: Exclude parser flag LSW match
+ * @parser_flag_msb_enable: Enable parser flag MSW matching
+ * @parser_flag_msb_exclude: Exclude parser flag MSW match
+ * @parser_flag1_lsb_enable: Enable parser flag1 LSW matching
+ * @parser_flag1_lsb_exclude: Exclude parser flag1 LSW match
+ * @parser_flag1_msb_enable: Enable parser flag1 MSW matching
+ * @parser_flag1_msb_exclude: Exclude parser flag1 MSW match
+ * @insertion_flag_enable: Enable insertion flag matching
+ * @flex_field4_enable: Enable flexible field 4 matching
+ * @flex_field4_exclude_enable: Exclude flexible field 4 match
+ * @flex_field4_range_enable: Flexible field 4 range mode
+ * (0 = mask, 1 = range)
+ * @flex_field3_enable: Enable flexible field 3 matching
+ * @flex_field3_exclude_enable: Exclude flexible field 3 match
+ * @flex_field3_range_enable: Flexible field 3 range mode
+ * @flex_field2_enable: Enable flexible field 2 matching
+ * @flex_field2_exclude_enable: Exclude flexible field 2 match
+ * @flex_field2_range_enable: Flexible field 2 range mode
+ * @flex_field1_enable: Enable flexible field 1 matching
+ * @flex_field1_exclude_enable: Exclude flexible field 1 match
+ * @flex_field1_range_enable: Flexible field 1 range mode
+ */
+struct mxl862xx_pce_pattern {
+ __le16 index;
+ __le32 dst_ip_mask;
+ __le32 inner_dst_ip_mask;
+ __le32 src_ip_mask;
+ __le32 inner_src_ip_mask;
+ __le16 sub_if_id;
+ __le16 pkt_lng;
+ __le16 pkt_lng_range;
+ __le16 mac_dst_mask;
+ __le16 mac_src_mask;
+ __le16 app_data_msb;
+ __le16 app_mask_range_msb;
+ __le16 ether_type;
+ __le16 ether_type_mask;
+ __le16 session_id;
+ __le16 ppp_protocol;
+ __le16 ppp_protocol_mask;
+ __le16 vid;
+ __le16 slan_vid;
+ __le16 flex_field4_value;
+ __le16 flex_field4_mask_range;
+ __le16 flex_field3_value;
+ __le16 flex_field3_mask_range;
+ __le16 flex_field1_value;
+ __le16 flex_field1_mask_range;
+ __le16 outer_vid_range;
+ __le16 payload1;
+ __le16 payload1_mask;
+ __le16 payload2;
+ __le16 payload2_mask;
+ __le16 parser_flag_lsb;
+ __le16 parser_flag_lsb_mask;
+ __le16 parser_flag_msb;
+ __le16 parser_flag_msb_mask;
+ __le16 parser_flag1_lsb;
+ __le16 parser_flag1_lsb_mask;
+ __le16 parser_flag1_msb;
+ __le16 parser_flag1_msb_mask;
+ __le16 app_data_lsb;
+ __le16 app_mask_range_lsb;
+ __le16 insertion_flag;
+ __le16 vid_range;
+ __le16 flex_field2_mask_range;
+ __le16 flex_field2_value;
+ u8 port_id;
+ u8 dscp;
+ u8 inner_dscp;
+ u8 pcp;
+ u8 stag_pcp_dei;
+ u8 mac_dst[ETH_ALEN];
+ u8 mac_src[ETH_ALEN];
+ u8 protocol;
+ u8 protocol_mask;
+ u8 inner_protocol;
+ u8 inner_protocol_mask;
+ u8 flex_field4_parser_index;
+ u8 flex_field3_parser_index;
+ u8 flex_field1_parser_index;
+ u8 flex_field2_parser_index;
+ u8 enable;
+ u8 port_id_enable;
+ u8 port_id_exclude;
+ __le32 sub_if_id_type;
+ u8 sub_if_id_enable;
+ u8 sub_if_id_exclude;
+ u8 dscp_enable;
+ u8 dscp_exclude;
+ u8 inner_dscp_enable;
+ u8 inner_dscp_exclude;
+ u8 pcp_enable;
+ u8 ctag_pcp_dei_exclude;
+ u8 stag_pcp_dei_enable;
+ u8 stag_pcp_dei_exclude;
+ u8 pkt_lng_enable;
+ u8 pkt_lng_exclude;
+ u8 mac_dst_enable;
+ u8 dst_mac_exclude;
+ u8 mac_src_enable;
+ u8 src_mac_exclude;
+ u8 app_data_msb_enable;
+ u8 app_mask_range_msb_select;
+ u8 app_msb_exclude;
+ u8 app_data_lsb_enable;
+ u8 app_mask_range_lsb_select;
+ u8 app_lsb_exclude;
+ __le32 dst_ip_select;
+ union mxl862xx_ip dst_ip;
+ u8 dst_ip_exclude;
+ __le32 inner_dst_ip_select;
+ union mxl862xx_ip inner_dst_ip;
+ u8 inner_dst_ip_exclude;
+ __le32 src_ip_select;
+ union mxl862xx_ip src_ip;
+ u8 src_ip_exclude;
+ __le32 inner_src_ip_select;
+ union mxl862xx_ip inner_src_ip;
+ u8 inner_src_ip_exclude;
+ u8 ether_type_enable;
+ u8 ether_type_exclude;
+ u8 protocol_enable;
+ u8 protocol_exclude;
+ u8 inner_protocol_enable;
+ u8 inner_protocol_exclude;
+ u8 session_id_enable;
+ u8 session_id_exclude;
+ u8 ppp_protocol_enable;
+ u8 ppp_protocol_exclude;
+ u8 vid_used;
+ u8 vid_range_select;
+ u8 vid_exclude;
+ u8 vid_original;
+ u8 slan_vid_used;
+ u8 slan_vid_exclude;
+ u8 svid_range_select;
+ u8 outer_vid_original;
+ u8 payload1_src_enable;
+ u8 payload1_mask_range_select;
+ u8 payload1_exclude;
+ u8 payload2_src_enable;
+ u8 payload2_mask_range_select;
+ u8 payload2_exclude;
+ u8 parser_flag_lsb_enable;
+ u8 parser_flag_lsb_exclude;
+ u8 parser_flag_msb_enable;
+ u8 parser_flag_msb_exclude;
+ u8 parser_flag1_lsb_enable;
+ u8 parser_flag1_lsb_exclude;
+ u8 parser_flag1_msb_enable;
+ u8 parser_flag1_msb_exclude;
+ u8 insertion_flag_enable;
+ u8 flex_field4_enable;
+ u8 flex_field4_exclude_enable;
+ u8 flex_field4_range_enable;
+ u8 flex_field3_enable;
+ u8 flex_field3_exclude_enable;
+ u8 flex_field3_range_enable;
+ u8 flex_field2_enable;
+ u8 flex_field2_exclude_enable;
+ u8 flex_field2_range_enable;
+ u8 flex_field1_enable;
+ u8 flex_field1_exclude_enable;
+ u8 flex_field1_range_enable;
+} __packed;
+
+static_assert(sizeof(struct mxl862xx_pce_pattern) == 279);
+
+/**
+ * struct mxl862xx_pce_action_pbb - Provider Backbone Bridging (Mac-in-Mac)
+ * action configuration
+ *
+ * @tunnel_id_known_traffic: Tunnel template index for I-Header known traffic
+ * @tunnel_id_unknown_traffic: Tunnel template index for I-Header unknown
+ * traffic
+ * @process_id_known_traffic: Tunnel template index for B-TAG known traffic
+ * @process_id_unknown_traffic: Tunnel template index for B-TAG unknown
+ * traffic
+ * @iheader_op_mode: I-Header operation mode (0 = no change, 1 = insert,
+ * 2 = remove, 3 = replace)
+ * @btag_op_mode: B-TAG operation mode (0 = no change, 1 = insert,
+ * 2 = remove, 3 = replace)
+ * @mac_table_macinmac_select: MAC table Mac-in-Mac selection
+ * (0 = outer MAC, 1 = inner MAC)
+ * @iheader_action_enable: Enable Mac-in-Mac I-Header action
+ * @tunnel_id_known_traffic_enable: Enable tunnel ID for known traffic
+ * @tunnel_id_unknown_traffic_enable: Enable tunnel ID for unknown traffic
+ * @b_dst_mac_from_mac_table_enable: Use B-DA from MAC table instead of
+ * tunnel template (I-Header insertion
+ * mode only)
+ * @replace_b_src_mac_enable: Replace B-SA from tunnel template
+ * @replace_b_dst_mac_enable: Replace B-DA from tunnel template
+ * @replace_i_tag_res_enable: Replace I-Tag Res from tunnel template
+ * @replace_i_tag_uac_enable: Replace I-Tag UAC from tunnel template
+ * @replace_i_tag_dei_enable: Replace I-Tag DEI from tunnel template
+ * @replace_i_tag_pcp_enable: Replace I-Tag PCP from tunnel template
+ * @replace_i_tag_sid_enable: Replace I-Tag SID from tunnel template
+ * @replace_i_tag_tpid_enable: Replace I-Tag TPID from tunnel template
+ * @btag_action_enable: Enable B-TAG action
+ * @process_id_known_traffic_enable: Enable process ID for B-TAG known
+ * traffic
+ * @process_id_unknown_traffic_enable: Enable process ID for B-TAG unknown
+ * traffic
+ * @replace_b_tag_dei_enable: Replace B-Tag DEI from tunnel template
+ * @replace_b_tag_pcp_enable: Replace B-Tag PCP from tunnel template
+ * @replace_b_tag_vid_enable: Replace B-Tag VID from tunnel template
+ * @replace_b_tag_tpid_enable: Replace B-Tag TPID from tunnel template
+ * @mac_table_macinmac_action_enable: Enable MAC table Mac-in-Mac action
+ */
+struct mxl862xx_pce_action_pbb {
+ u8 tunnel_id_known_traffic;
+ u8 tunnel_id_unknown_traffic;
+ u8 process_id_known_traffic;
+ u8 process_id_unknown_traffic;
+ __le32 iheader_op_mode;
+ __le32 btag_op_mode;
+ __le32 mac_table_macinmac_select;
+ u8 iheader_action_enable;
+ u8 tunnel_id_known_traffic_enable;
+ u8 tunnel_id_unknown_traffic_enable;
+ u8 b_dst_mac_from_mac_table_enable;
+ u8 replace_b_src_mac_enable;
+ u8 replace_b_dst_mac_enable;
+ u8 replace_i_tag_res_enable;
+ u8 replace_i_tag_uac_enable;
+ u8 replace_i_tag_dei_enable;
+ u8 replace_i_tag_pcp_enable;
+ u8 replace_i_tag_sid_enable;
+ u8 replace_i_tag_tpid_enable;
+ u8 btag_action_enable;
+ u8 process_id_known_traffic_enable;
+ u8 process_id_unknown_traffic_enable;
+ u8 replace_b_tag_dei_enable;
+ u8 replace_b_tag_pcp_enable;
+ u8 replace_b_tag_vid_enable;
+ u8 replace_b_tag_tpid_enable;
+ u8 mac_table_macinmac_action_enable;
+} __packed;
+
+static_assert(sizeof(struct mxl862xx_pce_action_pbb) == 36);
+
+/**
+ * struct mxl862xx_pce_action_dest_subif - Destination sub-interface ID
+ * action configuration
+ *
+ * @dest_subifid_action_enable: Destination sub-interface ID group field
+ * action enable
+ * @dest_subifid_assignment_enable: Destination sub-interface ID group field
+ * assignment enable
+ * @dest_subifgrp_field: Destination sub-interface ID group field value,
+ * or LAG index when trunking action is enabled
+ */
+struct mxl862xx_pce_action_dest_subif {
+ u8 dest_subifid_action_enable;
+ u8 dest_subifid_assignment_enable;
+ __le16 dest_subifgrp_field;
+} __packed;
+
+static_assert(sizeof(struct mxl862xx_pce_action_dest_subif) == 4);
+
+/**
+ * enum mxl862xx_pce_action_portmap - Forwarding group action selector
+ *
+ * Selects how the packet is forwarded. Mutually exclusive with
+ * the flow_id_action (bFlowID_Action in vendor API).
+ *
+ * @MXL862XX_PCE_ACTION_PORTMAP_DISABLE: Forwarding action is disabled
+ * @MXL862XX_PCE_ACTION_PORTMAP_REGULAR: Use default port-map from
+ * forwarding classification
+ * @MXL862XX_PCE_ACTION_PORTMAP_DISCARD: Discard the packet
+ * @MXL862XX_PCE_ACTION_PORTMAP_CPU: Forward to the CPU port
+ * (as configured by GSW_CPU_PortCfgSet, typically the on-die
+ * microcontroller -- not the DSA CPU port)
+ * @MXL862XX_PCE_ACTION_PORTMAP_ALTERNATIVE: Forward to an explicit
+ * portmap given by forward_port_map[]
+ */
+enum mxl862xx_pce_action_portmap {
+ MXL862XX_PCE_ACTION_PORTMAP_DISABLE = 0,
+ MXL862XX_PCE_ACTION_PORTMAP_REGULAR = 1,
+ MXL862XX_PCE_ACTION_PORTMAP_DISCARD = 2,
+ MXL862XX_PCE_ACTION_PORTMAP_CPU = 3,
+ MXL862XX_PCE_ACTION_PORTMAP_ALTERNATIVE = 4,
+};
+
+/**
+ * enum mxl862xx_pce_action_cross_state - Cross state action selector
+ *
+ * Controls whether the packet ignores STP port-state filtering.
+ *
+ * @MXL862XX_PCE_ACTION_CROSS_STATE_DISABLE: Cross state action is disabled
+ * @MXL862XX_PCE_ACTION_CROSS_STATE_REGULAR: Enabled; packet is treated
+ * as non-cross-state (does not ignore port-state filtering)
+ * @MXL862XX_PCE_ACTION_CROSS_STATE_CROSS: Enabled; packet ignores
+ * port-state filtering rules (e.g. passes through BLOCKING state)
+ */
+enum mxl862xx_pce_action_cross_state {
+ MXL862XX_PCE_ACTION_CROSS_STATE_DISABLE = 0,
+ MXL862XX_PCE_ACTION_CROSS_STATE_REGULAR = 1,
+ MXL862XX_PCE_ACTION_CROSS_STATE_CROSS = 2,
+};
+
+/**
+ * struct mxl862xx_pce_action - PCE rule action configuration
+ *
+ * Defines the actions applied to packets matching a PCE rule pattern.
+ *
+ * @time_comp: Signed time compensation value for OAM delay measurement
+ * @extended_vlan_block_id: Extended VLAN block allocated for this flow
+ * entry (valid when @extended_vlan_enable is set)
+ * @ins_ext_point: Insertion/extraction point
+ * @ptp_seq_id: PTP sequence ID for PTP application
+ * @pkt_update_offset: Byte offset (2..255) for counter/timestamp
+ * insertion (used when @no_pkt_update and
+ * @append_to_pkt are both false)
+ * @oam_flow_id: Traffic flow counter ID for OAM loss measurement
+ * @record_id: Record ID used by extraction and/or OAM process
+ * @forward_port_map: Target portmap for forwarded packets. Each bit
+ * represents one bridge port. Used when
+ * @port_map_action is %MXL862XX_PCE_ACTION_PORTMAP_ALTERNATIVE.
+ * @rmon_id: RMON counter ID (index starts from zero)
+ * @svlan_id: Alternative STAG VLAN ID
+ * @flow_id: Flow ID
+ * @rout_ext_id: Routing extension ID value (8-bit range)
+ * @traffic_class_alternate: Alternative traffic class (used when
+ * @traffic_class_action selects alternate)
+ * @meter_id: Meter ID
+ * @vlan_id: Alternative CTAG VLAN ID
+ * @fid: Alternative Filtering Identifier (FID)
+ * @traffic_class_action: Traffic class action selector
+ * (0 = disable, 1 = regular CoS, 2 = alternative)
+ * @snooping_type_action: IGMP snooping control selector
+ * @learning_action: MAC learning action selector
+ * (0 = disable, 1 = regular, 2 = force no learn,
+ * 3 = force learn)
+ * @irq_action: Interrupt action selector
+ * (0 = disable, 1 = regular, 2 = generate interrupt)
+ * @cross_state_action: Cross state action selector.
+ * See &enum mxl862xx_pce_action_cross_state
+ * @crit_frame_action: Critical frame action selector
+ * (0 = disable, 1 = regular, 2 = critical)
+ * @color_frame_action: Color frame action selector (replaces
+ * @crit_frame_action in GSWIP-3.1)
+ * @timestamp_action: Timestamp action selector
+ * (0 = disable, 1 = regular, 2 = store timestamps)
+ * @port_map_action: Forwarding portmap action selector.
+ * See &enum mxl862xx_pce_action_portmap
+ * @meter_action: Meter action selector
+ * (0 = disable, 1 = regular, 2 = meter 1,
+ * 3 = meter 1 and 2)
+ * @vlan_action: CTAG VLAN action selector
+ * (0 = disable, 1 = regular, 2 = alternative)
+ * @svlan_action: STAG VLAN action selector
+ * (0 = disable, 1 = regular, 2 = alternative)
+ * @vlan_cross_action: Cross-VLAN action selector
+ * (0 = disable, 1 = regular, 2 = cross-VLAN)
+ * @process_path_action: MPE processing path assignment
+ * (0 = unused, 1 = path-1, 2 = path-2, 3 = both)
+ * @port_filter_type_action: Port filter action type (0..6)
+ * @pbb_action: Provider Backbone Bridging (Mac-in-Mac) action.
+ * See &struct mxl862xx_pce_action_pbb
+ * @dest_subif_action: Destination sub-interface ID action.
+ * See &struct mxl862xx_pce_action_dest_subif
+ * @remark_action: Enable remarking action
+ * @remark_pcp: Enable CTAG VLAN PCP remarking
+ * @remark_stag_pcp: Enable STAG VLAN PCP remarking
+ * @remark_stag_dei: Enable STAG VLAN DEI remarking
+ * @remark_dscp: Enable DSCP remarking
+ * @remark_class: Enable class remarking
+ * @rmon_action: Enable RMON counter action
+ * @fid_enable: Enable alternative FID
+ * @extended_vlan_enable: Enable extended VLAN operation for matching
+ * traffic
+ * @cvlan_ignore_control: CVLAN ignore control
+ * @port_bitmap_mux_control: Port bitmap mux control
+ * @port_trunk_action: Enable trunking action
+ * @port_link_selection: Port link selection control
+ * @flow_id_action: Enable flow ID action (mutually exclusive with
+ * @port_map_action)
+ * @rout_ext_id_action: Enable routing extension ID action
+ * @rt_dst_port_mask_cmp_action: Routing destination port mask comparison
+ * @rt_src_port_mask_cmp_action: Routing source port mask comparison
+ * @rt_dst_ip_mask_cmp_action: Routing destination IP mask comparison
+ * @rt_src_ip_mask_cmp_action: Routing source IP mask comparison
+ * @rt_inner_ip_as_key_action: Use inner IP in tunneled header as
+ * routing key
+ * @rt_accel_ena_action: Routing acceleration enable
+ * @rt_ctrl_ena_action: Routing control enable (selects routing
+ * accelerate action)
+ * @extract_enable: Enable packet extraction at point defined by
+ * @record_id
+ * @oam_enable: Enable OAM processing for matching packets
+ * @pce_bypass_path: Update packet in PCE bypass path (after QoS queue)
+ * @tx_flow_cnt: Use TX flow counter (otherwise RX flow counter)
+ * @time_format: Timestamp format (0 = digital 10B, 1 = binary 10B,
+ * 2 = digital 8B, 3 = binary 8B)
+ * @no_pkt_update: Do not update packet
+ * @append_to_pkt: Append counter/timestamp to end of packet (when
+ * @no_pkt_update is false)
+ * @pbb_action_enable: Enable PBB action. See &struct mxl862xx_pce_action_pbb
+ * @dest_subif_action_enable: Enable destination sub-interface ID action.
+ * See &struct mxl862xx_pce_action_dest_subif
+ */
+struct mxl862xx_pce_action {
+ __le64 time_comp;
+ __le16 extended_vlan_block_id;
+ u8 ins_ext_point;
+ u8 ptp_seq_id;
+ __le16 pkt_update_offset;
+ __le16 oam_flow_id;
+ __le16 record_id;
+ __le16 forward_port_map[8];
+ __le16 rmon_id;
+ __le16 svlan_id;
+ __le16 flow_id;
+ __le16 rout_ext_id;
+ u8 traffic_class_alternate;
+ u8 meter_id;
+ u8 vlan_id;
+ u8 fid;
+ __le32 traffic_class_action;
+ __le32 snooping_type_action;
+ __le32 learning_action;
+ __le32 irq_action;
+ __le32 cross_state_action;
+ __le32 crit_frame_action;
+ __le32 color_frame_action;
+ __le32 timestamp_action;
+ __le32 port_map_action;
+ __le32 meter_action;
+ __le32 vlan_action;
+ __le32 svlan_action;
+ __le32 vlan_cross_action;
+ __le32 process_path_action;
+ __le32 port_filter_type_action;
+ struct mxl862xx_pce_action_pbb pbb_action;
+ struct mxl862xx_pce_action_dest_subif dest_subif_action;
+ u8 remark_action;
+ u8 remark_pcp;
+ u8 remark_stag_pcp;
+ u8 remark_stag_dei;
+ u8 remark_dscp;
+ u8 remark_class;
+ u8 rmon_action;
+ u8 fid_enable;
+ u8 extended_vlan_enable;
+ u8 cvlan_ignore_control;
+ u8 port_bitmap_mux_control;
+ u8 port_trunk_action;
+ u8 port_link_selection;
+ u8 flow_id_action;
+ u8 rout_ext_id_action;
+ u8 rt_dst_port_mask_cmp_action;
+ u8 rt_src_port_mask_cmp_action;
+ u8 rt_dst_ip_mask_cmp_action;
+ u8 rt_src_ip_mask_cmp_action;
+ u8 rt_inner_ip_as_key_action;
+ u8 rt_accel_ena_action;
+ u8 rt_ctrl_ena_action;
+ u8 extract_enable;
+ u8 oam_enable;
+ u8 pce_bypass_path;
+ u8 tx_flow_cnt;
+ __le32 time_format;
+ u8 no_pkt_update;
+ u8 append_to_pkt;
+ u8 pbb_action_enable;
+ u8 dest_subif_action_enable;
+} __packed;
+
+static_assert(sizeof(struct mxl862xx_pce_action) == 180);
+
+/**
+ * enum mxl862xx_pce_rule_region - PCE rule table region selector
+ *
+ * Selects which region of the traffic flow table the rule belongs to.
+ *
+ * @MXL862XX_PCE_RULE_COMMON: Common region shared by all CTPs
+ * (global rules, indices 0..63)
+ * @MXL862XX_PCE_RULE_CTP: Per-CTP region. The rule index is relative
+ * to the CTP block identified by logicalportid; the firmware
+ * translates it to an absolute hardware index.
+ * @MXL862XX_PCE_RULE_DEBUG: Debug region with direct HW index mapping
+ */
+enum mxl862xx_pce_rule_region {
+ MXL862XX_PCE_RULE_COMMON = 0,
+ MXL862XX_PCE_RULE_CTP = 1,
+ MXL862XX_PCE_RULE_DEBUG = 2,
+};
+
+/**
+ * struct mxl862xx_pce_rule - PCE rule configuration
+ * @logicalportid: Logical Port Id
+ * @subifidgroup: Sub-interface ID group
+ * @region: PCE rule region (common or per-CTP)
+ * @pattern: Match pattern (destination MAC, EtherType, etc.)
+ * @action: Forwarding action (portmap, cross-state, etc.)
+ *
+ * This structure is passed to the firmware via the MDIO data
+ * buffer using the %MXL862XX_TFLOW_PCERULEWRITE command.
+ * The binary layout must exactly match the firmware's
+ * GSW_PCE_rule_t (466 bytes, packed, little-endian scalars).
+ */
+struct mxl862xx_pce_rule {
+ u8 logicalportid;
+ __le16 subifidgroup;
+ __le32 region;
+ struct mxl862xx_pce_pattern pattern;
+ struct mxl862xx_pce_action action;
+} __packed;
+
+static_assert(sizeof(struct mxl862xx_pce_rule) == 466);
+
+/**
+ * struct mxl862xx_pce_rule_alloc - PCE rule block allocation
+ * @num_of_rules: Number of rules to allocate (input) / allocated (output).
+ * The firmware rounds up to a multiple of four consecutive entries.
+ * @blockid: Starting rule index of the allocated block (output on alloc,
+ * input on free).
+ *
+ * Used with %MXL862XX_TFLOW_PCERULEALLOC and %MXL862XX_TFLOW_PCERULEFREE.
+ * Maps to the firmware's ``GSW_PCE_rule_alloc_t``.
+ */
+struct mxl862xx_pce_rule_alloc {
+ __le16 num_of_rules;
+ __le16 blockid;
+} __packed;
+
+static_assert(sizeof(struct mxl862xx_pce_rule_alloc) == 4);
+
/**
* enum mxl862xx_stp_port_state - Spanning Tree Protocol port states
* @MXL862XX_STP_PORT_STATE_FORWARD: Forwarding state
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -12,6 +12,7 @@
(MXL862XX_MMD_REG_DATA_LAST - MXL862XX_MMD_REG_DATA_FIRST + 1)
#define MXL862XX_COMMON_MAGIC 0x100
+#define MXL862XX_TFLOW_MAGIC 0x200
#define MXL862XX_BRDG_MAGIC 0x300
#define MXL862XX_BRDGPORT_MAGIC 0x400
#define MXL862XX_CTP_MAGIC 0x500
@@ -31,6 +32,10 @@
#define MXL862XX_COMMON_CFGSET (MXL862XX_COMMON_MAGIC + 0xa)
#define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11)
+#define MXL862XX_TFLOW_PCERULEWRITE (MXL862XX_TFLOW_MAGIC + 0x2)
+#define MXL862XX_TFLOW_PCERULEALLOC (MXL862XX_TFLOW_MAGIC + 0x4)
+#define MXL862XX_TFLOW_PCERULEFREE (MXL862XX_TFLOW_MAGIC + 0x5)
+
#define MXL862XX_BRIDGE_ALLOC (MXL862XX_BRDG_MAGIC + 0x1)
#define MXL862XX_BRIDGE_CONFIGSET (MXL862XX_BRDG_MAGIC + 0x2)
#define MXL862XX_BRIDGE_CONFIGGET (MXL862XX_BRDG_MAGIC + 0x3)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -280,9 +280,11 @@ static int mxl862xx_wait_ready(struct ds
ver.iv_major, ver.iv_minor,
le16_to_cpu(ver.iv_revision),
le32_to_cpu(ver.iv_build_num));
+
priv->fw_version.major = ver.iv_major;
priv->fw_version.minor = ver.iv_minor;
priv->fw_version.revision = le16_to_cpu(ver.iv_revision);
+
return 0;
not_ready_yet:
@@ -410,6 +412,68 @@ static int mxl862xx_setup_drop_meter(str
return MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
}
+
+/* Per-CTP offset used for the link-local trap rule. Each port's CTP
+ * flow-table block is pre-allocated by the firmware during init (44
+ * entries per port on a 10-port SKU, of which offset 0 is reserved
+ * for flow-control marking). Offset 1 is the first unused slot.
+ */
+#define MXL862XX_LINK_LOCAL_CTP_OFFSET 1
+
+/* Install a PCE rule that traps IEEE 802.1D link-local frames
+ * (01:80:c2:00:00:0x) to the CPU port for a single user port,
+ * preventing the hardware bridge from flooding them to other ports.
+ * The firmware does not install this rule by default because its own
+ * STP module is not used when DSA manages STP.
+ *
+ * The rule is written into the port's per-CTP flow table at offset 1.
+ * The firmware already allocates a 44-entry block for every CTP during
+ * init (8 entries exposed initially, expandable), so no dynamic
+ * allocation via PCERULEALLOC is needed. Using region=CTP causes the
+ * firmware to translate the CTP-relative offset into an absolute
+ * hardware index.
+ *
+ * Cross-state is enabled so that link-local frames reach the CPU even
+ * when the bridge port is in BLOCKING or LEARNING state.
+ */
+static int mxl862xx_setup_link_local_trap(struct dsa_switch *ds, int port)
+{
+ DECLARE_BITMAP(portmap, MXL862XX_MAX_BRIDGE_PORTS);
+ struct dsa_port *dp = dsa_to_port(ds, port);
+ struct mxl862xx_pce_rule rule = {};
+ int cpu_port = dp->cpu_dp->index;
+ int i;
+
+ /* Address this port's CTP flow-table block */
+ rule.logicalportid = port;
+ rule.subifidgroup = 0;
+ rule.region = cpu_to_le32(MXL862XX_PCE_RULE_CTP);
+
+ /* Pattern: link-local MAC on this specific ingress port */
+ rule.pattern.index = cpu_to_le16(MXL862XX_LINK_LOCAL_CTP_OFFSET);
+ rule.pattern.enable = 1;
+ rule.pattern.mac_dst_enable = 1;
+ memcpy(rule.pattern.mac_dst, eth_reserved_addr_base, ETH_ALEN);
+ rule.pattern.mac_dst_mask = cpu_to_le16(0x0001);
+
+ /* Action: forward to the CPU port via explicit portmap */
+ rule.action.port_map_action =
+ cpu_to_le32(MXL862XX_PCE_ACTION_PORTMAP_ALTERNATIVE);
+
+ bitmap_zero(portmap, MXL862XX_MAX_BRIDGE_PORTS);
+ __set_bit(cpu_port, portmap);
+ for (i = 0; i < ARRAY_SIZE(rule.action.forward_port_map); i++)
+ rule.action.forward_port_map[i] =
+ cpu_to_le16(bitmap_read(portmap, i * 16, 16));
+
+ /* Bypass STP port state */
+ rule.action.cross_state_action =
+ cpu_to_le32(MXL862XX_PCE_ACTION_CROSS_STATE_CROSS);
+
+ return MXL862XX_API_WRITE(ds->priv, MXL862XX_TFLOW_PCERULEWRITE,
+ rule);
+}
+
static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port)
{
struct mxl862xx_bridge_port_config br_port_cfg = {};
@@ -1594,6 +1658,11 @@ static int mxl862xx_port_setup(struct ds
if (ret)
return ret;
+ /* install link-local trap for this user port */
+ ret = mxl862xx_setup_link_local_trap(ds, port);
+ if (ret)
+ return ret;
+
/* Initialize and pre-allocate per-port EVLAN and VF blocks for
* user ports. CPU ports do not use EVLAN or VF -- frames pass
* through without processing. Pre-allocation avoids firmware
@@ -0,0 +1,32 @@
From 3bba25f7ba35e3bca8230bd37ffb612944dbf301 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 18:51:21 +0000
Subject: [PATCH 24/35] net: dsa: mxl862xx: warn about old firmware default PCE
rules
Firmware versions older than 1.0.80 install global PCE rules at
boot that redirect link-local frames (BPDUs, LLDP, LACP) to port 0
(the on-chip microcontroller) instead of the DSA CPU port. With
port 0 disabled under DSA, these rules silently drop matching
traffic.
Log a warning when old firmware is detected so users know to update.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 4 ++++
1 file changed, 4 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -854,6 +854,10 @@ static int mxl862xx_setup(struct dsa_swi
if (ret)
return ret;
+ if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
+ dev_warn(ds->dev, "firmware < 1.0.80 installs global PCE rules "
+ "that interfere with DSA operation, please update\n");
+
schedule_delayed_work(&priv->stats_work,
MXL862XX_STATS_POLL_INTERVAL);
@@ -0,0 +1,862 @@
From 31359e8b7673e656d0591a9eb5014b45911383ae Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 03:44:41 +0000
Subject: [PATCH 26/35] net: dsa: mxl862xx: add link aggregation support
Implement LAG offloading via the firmware's trunking engine. A
dedicated firmware bridge port is allocated per LAG and remains
stable for the LAG's lifetime. All member CTPs redirect to it,
and FDB/MDB entries target it, so no entry migration is needed
when the LAG master (lowest-numbered member port) changes.
The firmware provides three cooperating mechanisms:
- PCE_TRUNK_CONF register: global 6-bit hash field selection
(SA, DA, SIP, DIP, sport, dport)
- CTP redirection: all member CTPs point bridge_port_id to the
LAG's dedicated bridge port
- P-mapper on the LAG bridge port: 64 hash-indexed entries
(indices 9..72) filled round-robin with active member ports
Hash and active-backup bond modes are supported. The global hash
register is widened monotonically on LAG join and recomputed from
stored per-port requirements on LAG leave.
The LAG master's full bridge port configuration (bridge_id, EVLAN,
VLAN filter, learning, portmap, flood metering) is pushed to the
LAG bridge port via __mxl862xx_set_bridge_port() whenever it
changes. Bridge portmaps use the LAG bridge port ID instead of
individual member port indices, ensuring correct cross-LAG
forwarding.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-api.h | 22 +
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 4 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 587 +++++++++++++++++++++++-
drivers/net/dsa/mxl862xx/mxl862xx.h | 34 ++
4 files changed, 630 insertions(+), 17 deletions(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -564,6 +564,28 @@ struct mxl862xx_pmapper {
} __packed;
/**
+ * struct mxl862xx_trunking_cfg - LAG hash algorithm configuration
+ * @ip_src: Include source IP address in trunk hash (1 = include)
+ * @ip_dst: Include destination IP address in trunk hash
+ * @mac_src: Include source MAC address in trunk hash
+ * @mac_dst: Include destination MAC address in trunk hash
+ * @src_port: Include TCP/UDP source port in trunk hash
+ * @dst_port: Include TCP/UDP destination port in trunk hash
+ *
+ * The firmware inverts the boolean sense when writing the hardware
+ * register (PCE_TRUNK_CONF): bit=0 means include, bit=1 means exclude.
+ * This struct uses the logical sense (1 = include).
+ */
+struct mxl862xx_trunking_cfg {
+ u8 ip_src;
+ u8 ip_dst;
+ u8 mac_src;
+ u8 mac_dst;
+ u8 src_port;
+ u8 dst_port;
+} __packed;
+
+/**
* struct mxl862xx_bridge_port_config - Bridge Port Configuration
* @bridge_port_id: Bridge Port ID allocated by bridge port allocation
* @mask: See &enum mxl862xx_bridge_port_config_mask
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -51,6 +51,7 @@
#define MXL862XX_QOS_METERCFGSET (MXL862XX_QOS_MAGIC + 0x2)
#define MXL862XX_QOS_METERALLOC (MXL862XX_QOS_MAGIC + 0x2a)
+#define MXL862XX_QOS_PMAPPERTABLESET (MXL862XX_QOS_MAGIC + 0x2e)
#define MXL862XX_RMON_PORT_GET (MXL862XX_RMON_MAGIC + 0x1)
@@ -73,6 +74,9 @@
#define MXL862XX_SS_SPTAG_SET (MXL862XX_SS_MAGIC + 0x2)
+#define MXL862XX_TRUNKING_MAGIC 0xe00
+#define MXL862XX_TRUNKING_CFGSET (MXL862XX_TRUNKING_MAGIC + 0x2)
+
#define MXL862XX_STP_PORTCFGSET (MXL862XX_STP_MAGIC + 0x2)
#define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x1)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -620,7 +620,42 @@ static int mxl862xx_setup_link_local_tra
rule);
}
-static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port)
+static bool mxl862xx_is_lag_master(const struct mxl862xx_priv *priv, int port)
+{
+ struct dsa_lag *lag = priv->ports[port].lag;
+ int i;
+
+ if (!lag)
+ return true;
+
+ for (i = 0; i < port; i++) {
+ if (priv->ports[i].lag == lag)
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * mxl862xx_lag_bridge_port - Get the effective bridge port ID for a port
+ * @priv: driver private data
+ * @port: port index
+ *
+ * If @port is a member of a LAG, returns the LAG's dedicated firmware
+ * bridge port ID. Otherwise returns @port itself.
+ */
+static u16 mxl862xx_lag_bridge_port(const struct mxl862xx_priv *priv, int port)
+{
+ struct dsa_lag *lag = priv->ports[port].lag;
+
+ if (lag && priv->lag_bridge_ports[lag->id])
+ return priv->lag_bridge_ports[lag->id];
+
+ return port;
+}
+
+static int __mxl862xx_set_bridge_port(struct dsa_switch *ds, int port,
+ u16 bp_id)
{
struct mxl862xx_bridge_port_config br_port_cfg = {};
struct dsa_port *dp = dsa_to_port(ds, port);
@@ -632,7 +667,7 @@ static int mxl862xx_set_bridge_port(stru
bool enable;
int i, idx;
- br_port_cfg.bridge_port_id = cpu_to_le16(port);
+ br_port_cfg.bridge_port_id = cpu_to_le16(bp_id);
br_port_cfg.bridge_id = cpu_to_le16(bridge_id);
br_port_cfg.mask = cpu_to_le32(MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_ID |
MXL862XX_BRIDGE_PORT_CONFIG_MASK_BRIDGE_PORT_MAP |
@@ -715,12 +750,38 @@ static int mxl862xx_set_bridge_port(stru
br_port_cfg);
}
+static int mxl862xx_set_bridge_port(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ u16 lag_bp;
+ int ret;
+
+ ret = __mxl862xx_set_bridge_port(ds, port, port);
+ if (ret)
+ return ret;
+
+ /* If this port is a LAG master, also push its config to the
+ * LAG's dedicated bridge port (which is the actual target of
+ * all member CTP redirections).
+ */
+ if (p->lag && mxl862xx_is_lag_master(priv, port)) {
+ lag_bp = priv->lag_bridge_ports[p->lag->id];
+ if (lag_bp)
+ ret = __mxl862xx_set_bridge_port(ds, port, lag_bp);
+ }
+
+ return ret;
+}
+
static int mxl862xx_sync_bridge_members(struct dsa_switch *ds,
const struct dsa_bridge *bridge)
{
struct mxl862xx_priv *priv = ds->priv;
struct dsa_port *dp, *member_dp;
- int port, err, ret = 0;
+ struct mxl862xx_port *p;
+ int port, member, err, ret = 0;
+ u16 lag_bp, bp;
dsa_switch_for_each_bridge_member(dp, ds, bridge->dev) {
port = dp->index;
@@ -729,9 +790,21 @@ static int mxl862xx_sync_bridge_members(
MXL862XX_MAX_BRIDGE_PORTS);
dsa_switch_for_each_bridge_member(member_dp, ds, bridge->dev) {
- if (member_dp->index != port)
- __set_bit(member_dp->index,
- priv->ports[port].portmap);
+ member = member_dp->index;
+
+ /* For LAG members, only include the LAG's
+ * dedicated bridge port in the portmap.
+ * Non-master members are skipped to avoid
+ * duplicates (they share the same LAG bridge
+ * port).
+ */
+ if (!mxl862xx_is_lag_master(priv, member))
+ continue;
+ if (member != port) {
+ bp = mxl862xx_lag_bridge_port(priv,
+ member);
+ __set_bit(bp, priv->ports[port].portmap);
+ }
}
__set_bit(mxl862xx_cpu_bridge_port_id(ds, port),
priv->ports[port].portmap);
@@ -741,6 +814,25 @@ static int mxl862xx_sync_bridge_members(
ret = err;
}
+ /* Push updated portmaps to LAG bridge ports. Each LAG master's
+ * portmap (which excludes itself) is used for the LAG bridge
+ * port -- this naturally avoids self-forwarding.
+ */
+ dsa_switch_for_each_bridge_member(dp, ds, bridge->dev) {
+ p = &priv->ports[dp->index];
+
+ if (!p->lag || !mxl862xx_is_lag_master(priv, dp->index))
+ continue;
+
+ lag_bp = priv->lag_bridge_ports[p->lag->id];
+ if (!lag_bp)
+ continue;
+
+ err = __mxl862xx_set_bridge_port(ds, dp->index, lag_bp);
+ if (err)
+ ret = err;
+ }
+
return ret;
}
@@ -1926,6 +2018,408 @@ static int mxl862xx_setup_cpu_bridge(str
return mxl862xx_set_bridge_port(ds, port);
}
+/**
+ * mxl862xx_lag_master_port - Find the LAG master (lowest-numbered member)
+ * @ds: DSA switch
+ * @lag: LAG to search
+ *
+ * The master's bridge port hosts the P-mapper and receives all ingress
+ * traffic via CTP redirection from other members.
+ *
+ * Return: port index of the master, or -ENOENT if no members.
+ */
+static int mxl862xx_lag_master_port(struct dsa_switch *ds,
+ const struct dsa_lag *lag)
+{
+ struct dsa_port *dp;
+ int master = -ENOENT;
+
+ dsa_lag_foreach_port(dp, ds->dst, lag) {
+ if (dp->ds != ds)
+ continue;
+ if (master < 0 || dp->index < master)
+ master = dp->index;
+ }
+
+ return master;
+}
+
+/**
+ * mxl862xx_lag_hash_bits - Translate Linux hash mode to firmware hash bitmask
+ * @info: bonding upper info (tx_type + hash_type)
+ *
+ * Return: 6-bit hash field bitmask (MXL862XX_TRUNK_HASH_*), or negative
+ * errno if the mode is unsupported.
+ */
+static int mxl862xx_lag_hash_bits(const struct netdev_lag_upper_info *info)
+{
+ if (info->tx_type != NETDEV_LAG_TX_TYPE_HASH)
+ return 0;
+
+ switch (info->hash_type) {
+ case NETDEV_LAG_HASH_L2:
+ return MXL862XX_TRUNK_HASH_SA | MXL862XX_TRUNK_HASH_DA;
+ case NETDEV_LAG_HASH_L34:
+ return MXL862XX_TRUNK_HASH_SIP | MXL862XX_TRUNK_HASH_DIP |
+ MXL862XX_TRUNK_HASH_SPORT | MXL862XX_TRUNK_HASH_DPORT;
+ case NETDEV_LAG_HASH_L23:
+ case NETDEV_LAG_HASH_E23:
+ return MXL862XX_TRUNK_HASH_SA | MXL862XX_TRUNK_HASH_DA |
+ MXL862XX_TRUNK_HASH_SIP | MXL862XX_TRUNK_HASH_DIP;
+ case NETDEV_LAG_HASH_E34:
+ return MXL862XX_TRUNK_HASH_SA | MXL862XX_TRUNK_HASH_DA |
+ MXL862XX_TRUNK_HASH_SIP | MXL862XX_TRUNK_HASH_DIP |
+ MXL862XX_TRUNK_HASH_SPORT | MXL862XX_TRUNK_HASH_DPORT;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+/**
+ * mxl862xx_lag_set_hash - Push trunk hash configuration to firmware
+ * @priv: driver private data
+ * @hash_bits: 6-bit hash field bitmask (MXL862XX_TRUNK_HASH_*)
+ *
+ * Only issues a firmware command when @hash_bits differs from the
+ * currently active configuration.
+ */
+static int mxl862xx_lag_set_hash(struct mxl862xx_priv *priv, u8 hash_bits)
+{
+ struct mxl862xx_trunking_cfg cfg = {};
+
+ if (priv->trunk_hash == hash_bits)
+ return 0;
+
+ cfg.mac_src = !!(hash_bits & MXL862XX_TRUNK_HASH_SA);
+ cfg.mac_dst = !!(hash_bits & MXL862XX_TRUNK_HASH_DA);
+ cfg.ip_src = !!(hash_bits & MXL862XX_TRUNK_HASH_SIP);
+ cfg.ip_dst = !!(hash_bits & MXL862XX_TRUNK_HASH_DIP);
+ cfg.src_port = !!(hash_bits & MXL862XX_TRUNK_HASH_SPORT);
+ cfg.dst_port = !!(hash_bits & MXL862XX_TRUNK_HASH_DPORT);
+
+ priv->trunk_hash = hash_bits;
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_TRUNKING_CFGSET, cfg);
+}
+
+/**
+ * mxl862xx_lag_recompute_hash - Recompute global hash from all active LAGs
+ * @ds: DSA switch
+ *
+ * Scans all ports and ORs together the stored hash requirements of every
+ * active LAG member. Used after a LAG is destroyed to potentially narrow
+ * the global hash configuration.
+ *
+ * Return: union of all active LAGs' hash field bitmasks.
+ */
+static u8 mxl862xx_lag_recompute_hash(struct dsa_switch *ds)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ u8 hash = 0;
+ int port;
+
+ for (port = 0; port < ds->num_ports; port++) {
+ if (priv->ports[port].lag)
+ hash |= priv->ports[port].lag_hash_bits;
+ }
+
+ return hash;
+}
+
+/**
+ * mxl862xx_lag_build_pmapper - Fill P-mapper with round-robin LAG distribution
+ * @ds: DSA switch
+ * @lag: LAG group
+ * @pm: P-mapper struct to fill (entries 9..72)
+ *
+ * Only ports with lag_tx_enabled are included. Falls back to the
+ * master port if no members are active.
+ */
+static void mxl862xx_lag_build_pmapper(struct dsa_switch *ds,
+ const struct dsa_lag *lag,
+ struct mxl862xx_pmapper *pm)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ int active_ports[MXL862XX_MAX_PORTS];
+ int n_active = 0, master, port;
+ struct dsa_port *dp;
+ int i;
+
+ dsa_lag_foreach_port(dp, ds->dst, lag) {
+ if (dp->ds != ds)
+ continue;
+ if (priv->ports[dp->index].lag_tx_enabled)
+ active_ports[n_active++] = dp->index;
+ }
+
+ /* Fallback: if no members are active, use the master port */
+ if (!n_active) {
+ master = mxl862xx_lag_master_port(ds, lag);
+
+ if (master >= 0) {
+ active_ports[0] = master;
+ n_active = 1;
+ }
+ }
+
+ if (!n_active)
+ return;
+
+ for (i = 0; i < MXL862XX_PMAPPER_LAG_COUNT; i++) {
+ port = active_ports[i % n_active];
+
+ pm->dest_sub_if_id_group[MXL862XX_PMAPPER_LAG_FIRST + i] =
+ (port << 4) & 0xff;
+ }
+}
+
+/**
+ * mxl862xx_lag_redirect_ctp - Redirect a port's CTP to the LAG master
+ * @priv: driver private data
+ * @port: port whose CTP to redirect
+ * @master_port: LAG master port index
+ */
+static int mxl862xx_lag_redirect_ctp(struct mxl862xx_priv *priv,
+ int port, int master_port)
+{
+ struct mxl862xx_ctp_port_config ctp = {};
+
+ ctp.logical_port_id = port;
+ ctp.mask = cpu_to_le32(MXL862XX_CTP_PORT_CONFIG_MASK_BRIDGE_PORT_ID);
+ ctp.bridge_port_id = cpu_to_le16(master_port);
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
+}
+
+/**
+ * mxl862xx_lag_restore_ctp - Restore a port's CTP to point to itself
+ * @priv: driver private data
+ * @port: port whose CTP to restore
+ */
+static int mxl862xx_lag_restore_ctp(struct mxl862xx_priv *priv, int port)
+{
+ return mxl862xx_lag_redirect_ctp(priv, port, port);
+}
+
+/**
+ * mxl862xx_lag_disable_pmapper - Disable P-mapper on a bridge port
+ * @ds: DSA switch
+ * @bp_id: firmware bridge port ID to reconfigure
+ */
+static int mxl862xx_lag_disable_pmapper(struct dsa_switch *ds, u16 bp_id)
+{
+ struct mxl862xx_bridge_port_config bp_cfg = {};
+ struct mxl862xx_priv *priv = ds->priv;
+
+ bp_cfg.bridge_port_id = cpu_to_le16(bp_id);
+ bp_cfg.mask = cpu_to_le32(
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING);
+ bp_cfg.dest_logical_port_id = bp_id;
+ bp_cfg.pmapper_enable = 0;
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, bp_cfg);
+}
+
+/**
+ * mxl862xx_lag_sync - Synchronize LAG hardware state for a LAG group
+ * @ds: DSA switch
+ * @lag: LAG group to synchronize
+ *
+ * Finds the master (lowest-numbered member), redirects all member CTPs
+ * to the LAG's dedicated firmware bridge port, configures the P-mapper
+ * for hash distribution, and pushes the master's full bridge port
+ * configuration (EVLAN, VF, portmap, learning) to the LAG bridge port.
+ */
+static int mxl862xx_lag_sync(struct dsa_switch *ds, const struct dsa_lag *lag)
+{
+ struct mxl862xx_bridge_port_config bp_cfg = {};
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_pmapper pm = {};
+ struct dsa_port *dp;
+ int master, ret;
+ u16 lag_bp;
+
+ lag_bp = priv->lag_bridge_ports[lag->id];
+ if (!lag_bp)
+ return -ENOENT;
+
+ master = mxl862xx_lag_master_port(ds, lag);
+ if (master < 0)
+ return master;
+
+ /* Redirect all member CTPs to the LAG bridge port */
+ dsa_lag_foreach_port(dp, ds->dst, lag) {
+ if (dp->ds != ds)
+ continue;
+ ret = mxl862xx_lag_redirect_ctp(priv, dp->index, lag_bp);
+ if (ret)
+ return ret;
+ }
+
+ /* Push the master's full config to the LAG bridge port so it
+ * inherits the current bridge_id, EVLAN/VF blocks, portmap,
+ * learning and flood settings.
+ */
+ ret = __mxl862xx_set_bridge_port(ds, master, lag_bp);
+ if (ret)
+ return ret;
+
+ /* Build P-mapper with active members */
+ mxl862xx_lag_build_pmapper(ds, lag, &pm);
+
+ /* Enable P-mapper in LAG mode on the LAG bridge port */
+ bp_cfg.bridge_port_id = cpu_to_le16(lag_bp);
+ bp_cfg.mask = cpu_to_le32(
+ MXL862XX_BRIDGE_PORT_CONFIG_MASK_EGRESS_CTP_MAPPING);
+ bp_cfg.dest_logical_port_id = master;
+ bp_cfg.pmapper_enable = 1;
+ bp_cfg.pmapper_mapping_mode =
+ cpu_to_le32(MXL862XX_PMAPPER_MAPPING_LAG);
+ bp_cfg.pmapper = pm;
+
+ return MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_CONFIGSET, bp_cfg);
+}
+
+static int mxl862xx_port_lag_join(struct dsa_switch *ds, int port,
+ const struct dsa_lag lag,
+ struct netdev_lag_upper_info *info,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_bridge_port_alloc bp_alloc = {};
+ struct mxl862xx_priv *priv = ds->priv;
+ struct dsa_port *dp = dsa_to_port(ds, port);
+ int hash_bits;
+ u8 new_hash;
+ int ret;
+
+ if (dsa_is_cpu_port(ds, port)) {
+ NL_SET_ERR_MSG_MOD(extack, "CPU port LAG not supported");
+ return -EOPNOTSUPP;
+ }
+
+ if (info->tx_type != NETDEV_LAG_TX_TYPE_HASH &&
+ info->tx_type != NETDEV_LAG_TX_TYPE_ACTIVEBACKUP) {
+ NL_SET_ERR_MSG_MOD(extack, "Only hash and active-backup LAG modes supported");
+ return -EOPNOTSUPP;
+ }
+
+ hash_bits = mxl862xx_lag_hash_bits(info);
+ if (hash_bits < 0) {
+ NL_SET_ERR_MSG_MOD(extack, "Unsupported LAG hash mode");
+ return hash_bits;
+ }
+
+ /* Allocate a dedicated firmware bridge port for this LAG on
+ * first member join. This bridge port is stable for the
+ * LAG's lifetime -- all CTP redirections, FDB and MDB entries
+ * target it, so no migration is needed on membership changes.
+ */
+ if (!priv->lag_bridge_ports[lag.id]) {
+ ret = MXL862XX_API_READ(priv, MXL862XX_BRIDGEPORT_ALLOC,
+ bp_alloc);
+ if (ret) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "Failed to allocate LAG bridge port");
+ return ret;
+ }
+ priv->lag_bridge_ports[lag.id] =
+ le16_to_cpu(bp_alloc.bridge_port_id);
+ }
+
+ priv->ports[port].lag = dp->lag;
+ priv->ports[port].lag_tx_enabled = dp->lag_tx_enabled;
+ priv->ports[port].lag_hash_bits = hash_bits;
+
+ /* Widen global hash to include this LAG's requirements */
+ new_hash = priv->trunk_hash | hash_bits;
+ ret = mxl862xx_lag_set_hash(priv, new_hash);
+ if (ret)
+ goto err_undo;
+
+ ret = mxl862xx_lag_sync(ds, dp->lag);
+ if (ret)
+ goto err_undo;
+
+ return 0;
+
+err_undo:
+ priv->ports[port].lag = NULL;
+ priv->ports[port].lag_tx_enabled = false;
+ priv->ports[port].lag_hash_bits = 0;
+ return ret;
+}
+
+static int mxl862xx_port_lag_leave(struct dsa_switch *ds, int port,
+ const struct dsa_lag lag)
+{
+ struct mxl862xx_bridge_port_alloc bp_alloc = {};
+ struct mxl862xx_priv *priv = ds->priv;
+ u8 new_hash;
+ int ret;
+
+ /* Restore this port's CTP to point to itself */
+ ret = mxl862xx_lag_restore_ctp(priv, port);
+ if (ret)
+ dev_err(ds->dev, "failed to restore CTP for port %d: %pe\n",
+ port, ERR_PTR(ret));
+
+ priv->ports[port].lag = NULL;
+ priv->ports[port].lag_tx_enabled = false;
+ priv->ports[port].lag_hash_bits = 0;
+
+ /* If other members remain, re-sync the LAG */
+ if (mxl862xx_lag_master_port(ds, &lag) >= 0) {
+ ret = mxl862xx_lag_sync(ds, &lag);
+ if (ret)
+ dev_err(ds->dev,
+ "failed to re-sync LAG after port %d left: %pe\n",
+ port, ERR_PTR(ret));
+ } else if (priv->lag_bridge_ports[lag.id]) {
+ /* Last member left -- disable P-mapper and free the
+ * LAG's dedicated bridge port.
+ */
+ ret = mxl862xx_lag_disable_pmapper(ds,
+ priv->lag_bridge_ports[lag.id]);
+ if (ret)
+ dev_err(ds->dev,
+ "failed to disable P-mapper on LAG bridge port %u: %pe\n",
+ priv->lag_bridge_ports[lag.id], ERR_PTR(ret));
+
+ bp_alloc.bridge_port_id =
+ cpu_to_le16(priv->lag_bridge_ports[lag.id]);
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_BRIDGEPORT_FREE,
+ bp_alloc);
+ if (ret)
+ dev_err(ds->dev,
+ "failed to free LAG bridge port %u: %pe\n",
+ priv->lag_bridge_ports[lag.id], ERR_PTR(ret));
+
+ priv->lag_bridge_ports[lag.id] = 0;
+ }
+
+ /* Recompute global hash from remaining LAGs */
+ new_hash = mxl862xx_lag_recompute_hash(ds);
+ ret = mxl862xx_lag_set_hash(priv, new_hash);
+ if (ret)
+ dev_err(ds->dev, "failed to update trunk hash: %pe\n",
+ ERR_PTR(ret));
+
+ return 0;
+}
+
+static int mxl862xx_port_lag_change(struct dsa_switch *ds, int port)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct dsa_port *dp = dsa_to_port(ds, port);
+
+ if (!priv->ports[port].lag)
+ return 0;
+
+ priv->ports[port].lag_tx_enabled = dp->lag_tx_enabled;
+
+ return mxl862xx_lag_sync(ds, priv->ports[port].lag);
+}
+
static int mxl862xx_port_bridge_join(struct dsa_switch *ds, int port,
const struct dsa_bridge bridge,
bool *tx_fwd_offload,
@@ -1952,7 +2446,18 @@ static int mxl862xx_port_bridge_join(str
return 0;
}
- return mxl862xx_sync_bridge_members(ds, &bridge);
+ ret = mxl862xx_sync_bridge_members(ds, &bridge);
+ if (ret)
+ return ret;
+
+ /* If this port is in a LAG, re-sync the LAG bridge port so it
+ * picks up the new bridge_id (switching from standalone FID to
+ * the shared bridge FID).
+ */
+ if (priv->ports[port].lag)
+ ret = mxl862xx_lag_sync(ds, priv->ports[port].lag);
+
+ return ret;
}
static void mxl862xx_port_bridge_leave(struct dsa_switch *ds, int port,
@@ -2011,6 +2516,17 @@ static void mxl862xx_port_bridge_leave(s
"failed to update CPU VBP for port %d: %pe\n", port,
ERR_PTR(err));
+ /* If this port is in a LAG, re-sync the LAG bridge port so it
+ * reverts to the standalone FID.
+ */
+ if (p->lag) {
+ err = mxl862xx_lag_sync(ds, p->lag);
+ if (err)
+ dev_err(ds->dev,
+ "failed to re-sync LAG after port %d left bridge: %pe\n",
+ port, ERR_PTR(err));
+ }
+
if (!dsa_bridge_ports(ds, bridge.dev))
mxl862xx_free_bridge(ds, &bridge);
}
@@ -2636,18 +3152,17 @@ static int mxl862xx_get_fid(struct dsa_s
}
/**
- * mxl862xx_fdb_bridge_port - Translate port for MAC table in tag_8021q mode
+ * mxl862xx_fdb_bridge_port - Translate port to effective bridge port ID
* @ds: DSA switch
* @port: port number passed by DSA (usually the CPU port for host entries)
* @db: database context identifying the user port or bridge
*
- * In tag_8021q mode, host FDB/MDB entries for standalone ports must use
- * the virtual bridge port (bridge_port_cpu) as the MAC table destination
- * so that known-unicast and known-multicast frames exit through the
- * virtual bridge port's egress EVLAN, which inserts the management VID.
- * Without this, the firmware forwards known traffic directly to the
- * physical CPU bridge port, bypassing management VID insertion, and DSA
- * drops the untagged frame.
+ * Returns the firmware bridge port ID that should be used for MAC table
+ * entries targeting @port:
+ * - CPU port in tag_8021q standalone mode: the virtual bridge port
+ * (bridge_port_cpu) so known traffic exits through egress EVLAN
+ * - User port in a LAG: the LAG's dedicated firmware bridge port
+ * - Otherwise: the port index itself
*/
static int mxl862xx_fdb_bridge_port(struct dsa_switch *ds, int port,
const struct dsa_db db)
@@ -2663,7 +3178,7 @@ static int mxl862xx_fdb_bridge_port(stru
return bp_cpu;
}
- return port;
+ return mxl862xx_lag_bridge_port(priv, port);
}
/**
@@ -2907,11 +3422,43 @@ static int mxl862xx_port_fdb_del(struct
return ret;
}
+static int mxl862xx_lag_fdb_add(struct dsa_switch *ds, const struct dsa_lag lag,
+ const unsigned char *addr, u16 vid,
+ const struct dsa_db db)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ u16 lag_bp = priv->lag_bridge_ports[lag.id];
+ int fid;
+
+ if (!lag_bp)
+ return -ENOENT;
+
+ fid = mxl862xx_get_fid(ds, db);
+ if (fid < 0)
+ return fid;
+
+ return mxl862xx_fdb_add_per_fid(ds, addr, vid, fid, lag_bp);
+}
+
+static int mxl862xx_lag_fdb_del(struct dsa_switch *ds, const struct dsa_lag lag,
+ const unsigned char *addr, u16 vid,
+ const struct dsa_db db)
+{
+ int fid;
+
+ fid = mxl862xx_get_fid(ds, db);
+ if (fid < 0)
+ return fid;
+
+ return mxl862xx_fdb_del_per_fid(ds, addr, vid, fid);
+}
+
static int mxl862xx_port_fdb_dump(struct dsa_switch *ds, int port,
dsa_fdb_dump_cb_t *cb, void *data)
{
struct mxl862xx_mac_table_read param = { .initial = 1 };
struct mxl862xx_priv *priv = ds->priv;
+ u16 lag_bp = mxl862xx_lag_bridge_port(priv, port);
u32 entry_port_id;
int ret;
@@ -2925,7 +3472,7 @@ static int mxl862xx_port_fdb_dump(struct
entry_port_id = le32_to_cpu(param.port_id);
- if (entry_port_id == port) {
+ if (entry_port_id == port || entry_port_id == lag_bp) {
ret = cb(param.mac, FIELD_GET(MXL862XX_TCI_VLAN_ID,
le16_to_cpu(param.tci)),
param.static_entry, data);
@@ -3562,6 +4109,11 @@ static const struct dsa_switch_ops mxl86
.port_fdb_dump = mxl862xx_port_fdb_dump,
.port_mdb_add = mxl862xx_port_mdb_add,
.port_mdb_del = mxl862xx_port_mdb_del,
+ .port_lag_join = mxl862xx_port_lag_join,
+ .port_lag_leave = mxl862xx_port_lag_leave,
+ .port_lag_change = mxl862xx_port_lag_change,
+ .lag_fdb_add = mxl862xx_lag_fdb_add,
+ .lag_fdb_del = mxl862xx_lag_fdb_del,
.port_vlan_filtering = mxl862xx_port_vlan_filtering,
.port_vlan_add = mxl862xx_port_vlan_add,
.port_vlan_del = mxl862xx_port_vlan_del,
@@ -3602,6 +4154,7 @@ static int mxl862xx_probe(struct mdio_de
ds->num_ports = MXL862XX_MAX_PORTS;
ds->fdb_isolation = true;
ds->max_num_bridges = MXL862XX_MAX_BRIDGES;
+ ds->num_lag_ids = MXL862XX_MAX_LAG_IDS;
mxl862xx_host_init(priv);
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -14,6 +14,19 @@
#define MXL862XX_MAX_BRIDGE_PORTS 128
#define MXL862XX_TOTAL_EVLAN_ENTRIES 1024
#define MXL862XX_TOTAL_VF_ENTRIES 1024
+#define MXL862XX_MAX_LAG_IDS 16
+
+/* Trunk hash field bitmask (matches PCE_TRUNK_CONF layout) */
+#define MXL862XX_TRUNK_HASH_SA BIT(0)
+#define MXL862XX_TRUNK_HASH_DA BIT(1)
+#define MXL862XX_TRUNK_HASH_SIP BIT(2)
+#define MXL862XX_TRUNK_HASH_DIP BIT(3)
+#define MXL862XX_TRUNK_HASH_SPORT BIT(4)
+#define MXL862XX_TRUNK_HASH_DPORT BIT(5)
+
+/* P-mapper LAG entries occupy indices 9..72 (64 entries) */
+#define MXL862XX_PMAPPER_LAG_FIRST 9
+#define MXL862XX_PMAPPER_LAG_COUNT 64
/* Number of __le16 words in a firmware portmap (128-bit bitmap). */
#define MXL862XX_FW_PORTMAP_WORDS (MXL862XX_MAX_BRIDGE_PORTS / 16)
@@ -228,6 +241,12 @@ struct mxl862xx_port_stats {
* @stats_lock: protects accumulator reads in .get_stats64 against
* concurrent updates from the polling work
* @tag_8021q_vid: currently assigned tag_8021q management VID
+ * @lag: non-NULL when port is member of a LAG group;
+ * points to the DSA LAG structure
+ * @lag_tx_enabled: true when this port is active for TX in its LAG
+ * @lag_hash_bits: hash field bitmask (MXL862XX_TRUNK_HASH_*) requested
+ * when this port joined its LAG; used to recompute the
+ * global trunk_hash when a LAG is destroyed
*/
struct mxl862xx_port {
struct mxl862xx_priv *priv;
@@ -250,6 +269,10 @@ struct mxl862xx_port {
struct work_struct host_flood_work;
u16 tag_8021q_vid;
struct mxl862xx_evlan_block cpu_egress_evlan;
+ /* LAG state */
+ struct dsa_lag *lag;
+ bool lag_tx_enabled;
+ u8 lag_hash_bits;
/* Hardware stats accumulation */
struct mxl862xx_port_stats stats;
spinlock_t stats_lock;
@@ -328,6 +351,15 @@ union mxl862xx_fw_version {
* DSA bridge number. Indexed by dsa_bridge.num
* (0 .. ds->max_num_bridges).
* @vf_block_size: per-port VLAN Filter block size
+ * @lag_bridge_ports: maps DSA LAG ID to firmware bridge port ID;
+ * zero means no bridge port allocated for that LAG.
+ * Indexed by lag->id (entry 0 is unused).
+ * The bridge port is stable for the LAG's lifetime
+ * so FDB/MDB entries never need migration on
+ * membership changes.
+ * @trunk_hash: current global hash field bitmask (6 bits,
+ * MXL862XX_TRUNK_HASH_*); union of all active LAGs'
+ * hash requirements
* @stats_work: periodic work item that polls RMON hardware counters
* and accumulates them into 64-bit per-port stats
*/
@@ -346,6 +378,8 @@ struct mxl862xx_priv {
u16 evlan_egress_size;
u16 cpu_evlan_ingress_size;
u16 vf_block_size;
+ u16 lag_bridge_ports[MXL862XX_MAX_LAG_IDS + 1];
+ u8 trunk_hash;
struct delayed_work stats_work;
};
@@ -0,0 +1,229 @@
From fbfa1b0649c578e0d43e3a61617b53a9a722efad Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 12:05:29 +0000
Subject: [PATCH 27/35] net: dsa: mxl862xx: add support for mirror port
The MxL862xx hardware supports a single monitor port which can be
configured to mirror any other port's ingress and/or egress traffic.
Implement support for .port_mirror_add/.port_mirror_del using this
feature.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-api.h | 12 +++
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 1 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 118 ++++++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx.h | 8 ++
4 files changed, 139 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -729,6 +729,18 @@ struct mxl862xx_bridge_port_config {
} __packed;
/**
+ * struct mxl862xx_monitor_port_cfg - Monitor port configuration
+ * @port_id: Destination port for mirrored traffic (zero-based)
+ * @sub_if_id: Monitoring sub-interface ID
+ * @monitor_port: Reserved
+ */
+struct mxl862xx_monitor_port_cfg {
+ u8 port_id;
+ __le16 sub_if_id;
+ u8 monitor_port;
+} __packed;
+
+/**
* struct mxl862xx_cfg - Global Switch configuration Attributes
* @mac_table_age_timer: See &enum mxl862xx_age_timer
* @age_timer: Custom MAC table aging timer in seconds
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -30,6 +30,7 @@
#define MXL862XX_COMMON_PORTCFGGET (MXL862XX_COMMON_MAGIC + 0x7)
#define MXL862XX_COMMON_CFGGET (MXL862XX_COMMON_MAGIC + 0x9)
#define MXL862XX_COMMON_CFGSET (MXL862XX_COMMON_MAGIC + 0xa)
+#define MXL862XX_COMMON_MONITORPORTCFGSET (MXL862XX_COMMON_MAGIC + 0xe)
#define MXL862XX_COMMON_REGISTERMOD (MXL862XX_COMMON_MAGIC + 0x11)
#define MXL862XX_TFLOW_PCERULEWRITE (MXL862XX_TFLOW_MAGIC + 0x2)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -1129,6 +1129,8 @@ static int mxl862xx_setup(struct dsa_swi
(n_user_ports + n_cpu_ports);
}
+ priv->mirror_dest = -1;
+
ret = mxl862xx_setup_drop_meter(ds);
if (ret)
return ret;
@@ -2018,6 +2020,120 @@ static int mxl862xx_setup_cpu_bridge(str
return mxl862xx_set_bridge_port(ds, port);
}
+static int mxl862xx_port_mirror_add(struct dsa_switch *ds, int port,
+ struct dsa_mall_mirror_tc_entry *mirror,
+ bool ingress,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ struct mxl862xx_monitor_port_cfg mon = {
+ .port_id = mirror->to_local_port,
+ };
+ struct mxl862xx_ctp_port_config ctp = {
+ .logical_port_id = port,
+ .mask = cpu_to_le32(
+ MXL862XX_CTP_PORT_CONFIG_MASK_LOOPBACK_AND_MIRROR),
+ .ingress_mirror_enable = p->ingress_mirror,
+ .egress_mirror_enable = p->egress_mirror,
+ };
+ int ret;
+
+ /* The hardware has a single global monitor port. Reject if an
+ * existing mirror session targets a different destination.
+ */
+ if (priv->mirror_dest >= 0 &&
+ priv->mirror_dest != mirror->to_local_port) {
+ NL_SET_ERR_MSG_MOD(extack,
+ "Only one mirror destination port is supported");
+ return -EBUSY;
+ }
+
+ if (ingress)
+ ctp.ingress_mirror_enable = 1;
+ else
+ ctp.egress_mirror_enable = 1;
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
+ if (ret) {
+ dev_err(ds->dev, "mirror: CTP write failed for port %d: %pe\n",
+ port, ERR_PTR(ret));
+ return ret;
+ }
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_MONITORPORTCFGSET, mon);
+ if (ret) {
+ dev_err(ds->dev,
+ "mirror: failed to set monitor port %d: %pe\n",
+ mirror->to_local_port, ERR_PTR(ret));
+ /* Roll back CTP change */
+ ctp.ingress_mirror_enable = p->ingress_mirror;
+ ctp.egress_mirror_enable = p->egress_mirror;
+ MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
+ return ret;
+ }
+
+ if (ingress)
+ p->ingress_mirror = true;
+ else
+ p->egress_mirror = true;
+
+ priv->mirror_dest = mirror->to_local_port;
+
+ return 0;
+}
+
+static void mxl862xx_port_mirror_del(struct dsa_switch *ds, int port,
+ struct dsa_mall_mirror_tc_entry *mirror)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_port *p = &priv->ports[port];
+ struct mxl862xx_ctp_port_config ctp = {
+ .logical_port_id = port,
+ .mask = cpu_to_le32(
+ MXL862XX_CTP_PORT_CONFIG_MASK_LOOPBACK_AND_MIRROR),
+ .ingress_mirror_enable = p->ingress_mirror,
+ .egress_mirror_enable = p->egress_mirror,
+ };
+ struct mxl862xx_monitor_port_cfg mon = {};
+ bool active = false;
+ int i, ret;
+
+ if (mirror->ingress)
+ ctp.ingress_mirror_enable = 0;
+ else
+ ctp.egress_mirror_enable = 0;
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_CTP_PORTCONFIGSET, ctp);
+ if (ret)
+ dev_err(ds->dev, "mirror: CTP write failed for port %d: %pe\n",
+ port, ERR_PTR(ret));
+
+ if (mirror->ingress)
+ p->ingress_mirror = false;
+ else
+ p->egress_mirror = false;
+
+ /* If no ports have any mirrors active, clear the monitor port */
+ for (i = 0; i < ds->num_ports; i++) {
+ if (priv->ports[i].ingress_mirror ||
+ priv->ports[i].egress_mirror) {
+ active = true;
+ break;
+ }
+ }
+
+ if (active)
+ return;
+
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_MONITORPORTCFGSET, mon);
+ if (ret)
+ dev_err(ds->dev, "mirror: failed to clear monitor port: %pe\n",
+ ERR_PTR(ret));
+
+ priv->mirror_dest = -1;
+}
+
/**
* mxl862xx_lag_master_port - Find the LAG master (lowest-numbered member)
* @ds: DSA switch
@@ -4109,6 +4225,8 @@ static const struct dsa_switch_ops mxl86
.port_fdb_dump = mxl862xx_port_fdb_dump,
.port_mdb_add = mxl862xx_port_mdb_add,
.port_mdb_del = mxl862xx_port_mdb_del,
+ .port_mirror_add = mxl862xx_port_mirror_add,
+ .port_mirror_del = mxl862xx_port_mirror_del,
.port_lag_join = mxl862xx_port_lag_join,
.port_lag_leave = mxl862xx_port_lag_leave,
.port_lag_change = mxl862xx_port_lag_change,
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -241,6 +241,8 @@ struct mxl862xx_port_stats {
* @stats_lock: protects accumulator reads in .get_stats64 against
* concurrent updates from the polling work
* @tag_8021q_vid: currently assigned tag_8021q management VID
+ * @ingress_mirror: true when ingress mirroring is active on this port
+ * @egress_mirror: true when egress mirroring is active on this port
* @lag: non-NULL when port is member of a LAG group;
* points to the DSA LAG structure
* @lag_tx_enabled: true when this port is active for TX in its LAG
@@ -269,6 +271,9 @@ struct mxl862xx_port {
struct work_struct host_flood_work;
u16 tag_8021q_vid;
struct mxl862xx_evlan_block cpu_egress_evlan;
+ /* Mirror state */
+ bool ingress_mirror;
+ bool egress_mirror;
/* LAG state */
struct dsa_lag *lag;
bool lag_tx_enabled;
@@ -360,6 +365,8 @@ union mxl862xx_fw_version {
* @trunk_hash: current global hash field bitmask (6 bits,
* MXL862XX_TRUNK_HASH_*); union of all active LAGs'
* hash requirements
+ * @mirror_dest: current mirror destination port, or -1 if no mirror
+ * session is active; used to detect monitor port conflicts
* @stats_work: periodic work item that polls RMON hardware counters
* and accumulates them into 64-bit per-port stats
*/
@@ -380,6 +387,7 @@ struct mxl862xx_priv {
u16 vf_block_size;
u16 lag_bridge_ports[MXL862XX_MAX_LAG_IDS + 1];
u8 trunk_hash;
+ int mirror_dest;
struct delayed_work stats_work;
};
@@ -0,0 +1,57 @@
From 67f82834819b71417b58dc1293c20f71b990264f Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 16:30:08 +0000
Subject: [PATCH 28/35] net: dsa: wire flash_update devlink callback to drivers
Add a devlink_flash_update callback to dsa_switch_ops so that DSA
drivers can support devlink dev flash without open-coding the devlink
plumbing. The new trampoline in net/dsa/devlink.c follows the existing
dsa_devlink_info_get pattern exactly.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
include/net/dsa.h | 3 +++
net/dsa/devlink.c | 13 +++++++++++++
2 files changed, 16 insertions(+)
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -1185,6 +1185,9 @@ struct dsa_switch_ops {
int (*devlink_info_get)(struct dsa_switch *ds,
struct devlink_info_req *req,
struct netlink_ext_ack *extack);
+ int (*devlink_flash_update)(struct dsa_switch *ds,
+ struct devlink_flash_update_params *params,
+ struct netlink_ext_ack *extack);
int (*devlink_sb_pool_get)(struct dsa_switch *ds,
unsigned int sb_index, u16 pool_index,
struct devlink_sb_pool_info *pool_info);
--- a/net/dsa/devlink.c
+++ b/net/dsa/devlink.c
@@ -20,6 +20,18 @@ static int dsa_devlink_info_get(struct d
return -EOPNOTSUPP;
}
+static int dsa_devlink_flash_update(struct devlink *dl,
+ struct devlink_flash_update_params *params,
+ struct netlink_ext_ack *extack)
+{
+ struct dsa_switch *ds = dsa_devlink_to_ds(dl);
+
+ if (!ds->ops->devlink_flash_update)
+ return -EOPNOTSUPP;
+
+ return ds->ops->devlink_flash_update(ds, params, extack);
+}
+
static int dsa_devlink_sb_pool_get(struct devlink *dl,
unsigned int sb_index, u16 pool_index,
struct devlink_sb_pool_info *pool_info)
@@ -169,6 +181,7 @@ dsa_devlink_sb_occ_tc_port_bind_get(stru
static const struct devlink_ops dsa_devlink_ops = {
.info_get = dsa_devlink_info_get,
+ .flash_update = dsa_devlink_flash_update,
.sb_pool_get = dsa_devlink_sb_pool_get,
.sb_pool_set = dsa_devlink_sb_pool_set,
.sb_port_pool_get = dsa_devlink_sb_port_pool_get,
@@ -0,0 +1,72 @@
From 1a87b829ef3280d646dc480f7b261d9e32896899 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 16:30:17 +0000
Subject: [PATCH 29/35] net: dsa: mxl862xx: add SMDIO clause-22 register access
Add mxl862xx_smdio_read() and mxl862xx_smdio_write() for clause-22
SMDIO register access. MCUboot rescue mode only exposes clause-22
registers; the existing clause-45 MMD interface is unavailable during
firmware transfer. The MDIO bus lock is held per-transaction (not
across polls) so that SB PDI polling during flash erase does not
starve other MDIO users.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 35 ++++++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-host.h | 2 ++
2 files changed, 37 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -493,6 +493,41 @@ out:
return ret;
}
+#define MXL862XX_SMDIO_ADDR_REG 0x1f
+#define MXL862XX_SMDIO_PAGE_MASK 0xfff0
+#define MXL862XX_SMDIO_OFF_MASK 0x000f
+
+int mxl862xx_smdio_read(struct mxl862xx_priv *priv, u32 addr)
+{
+ struct mii_bus *bus = priv->mdiodev->bus;
+ int phy = priv->mdiodev->addr;
+ int ret;
+
+ mutex_lock(&bus->mdio_lock);
+ ret = __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG,
+ addr & MXL862XX_SMDIO_PAGE_MASK);
+ if (ret >= 0)
+ ret = __mdiobus_read(bus, phy, addr & MXL862XX_SMDIO_OFF_MASK);
+ mutex_unlock(&bus->mdio_lock);
+ return ret;
+}
+
+int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val)
+{
+ struct mii_bus *bus = priv->mdiodev->bus;
+ int phy = priv->mdiodev->addr;
+ int ret;
+
+ mutex_lock(&bus->mdio_lock);
+ ret = __mdiobus_write(bus, phy, MXL862XX_SMDIO_ADDR_REG,
+ addr & MXL862XX_SMDIO_PAGE_MASK);
+ if (ret >= 0)
+ ret = __mdiobus_write(bus, phy, addr & MXL862XX_SMDIO_OFF_MASK,
+ val);
+ mutex_unlock(&bus->mdio_lock);
+ return ret;
+}
+
void mxl862xx_host_init(struct mxl862xx_priv *priv)
{
INIT_WORK(&priv->crc_err_work, mxl862xx_crc_err_work_fn);
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.h
@@ -18,5 +18,7 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
mxl862xx_api_wrap(dev, cmd, &(data), sizeof((data)), true, true)
int mxl862xx_reset(struct mxl862xx_priv *priv);
+int mxl862xx_smdio_read(struct mxl862xx_priv *priv, u32 addr);
+int mxl862xx_smdio_write(struct mxl862xx_priv *priv, u32 addr, u16 val);
#endif /* __MXL862XX_HOST_H */
@@ -0,0 +1,569 @@
From b7e8f8fd4493b255f0f01fe790a73ad61b5e8ce8 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 16:30:31 +0000
Subject: [PATCH 30/35] net: dsa: mxl862xx: add devlink flash_update and
info_get
Implement runtime firmware upgrade via "devlink dev flash" and version
reporting via "devlink dev info":
devlink dev info mdio_bus/<bus>/<addr>
devlink dev flash mdio_bus/<bus>/<addr> file <firmware.bin>
The driver sends SYS_MISC_FW_UPDATE to enter MCUboot rescue mode,
transfers the signed image over the SB PDI bulk-transfer protocol
(clause-22 SMDIO), waits for the switch to reboot, then schedules
device_reprobe() for a clean remove()+probe() cycle.
Before the transfer begins the driver closes all conduit interfaces
and marks every netdev (user and conduit) not-present via
netif_device_detach() so that userspace cannot bring ports back up
during the ~15 minute flash process. Progress is reported through
devlink status notifications. Once the FW_UPDATE command has been
sent the switch is in MCUboot mode and normal operation can only be
restored by a reprobe, so the driver always schedules one regardless
of transfer outcome.
The reprobe work item is dynamically allocated (following the iwlwifi
pattern) because device_reprobe() triggers remove() which frees the
devm-managed priv while the work is still executing.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/Makefile | 2 +-
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 1 +
drivers/net/dsa/mxl862xx/mxl862xx-fw.c | 434 +++++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx-fw.h | 15 +
drivers/net/dsa/mxl862xx/mxl862xx-host.c | 7 +
drivers/net/dsa/mxl862xx/mxl862xx.c | 4 +
drivers/net/dsa/mxl862xx/mxl862xx.h | 2 +
7 files changed, 464 insertions(+), 1 deletion(-)
create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.c
create mode 100644 drivers/net/dsa/mxl862xx/mxl862xx-fw.h
--- a/drivers/net/dsa/mxl862xx/Makefile
+++ b/drivers/net/dsa/mxl862xx/Makefile
@@ -1,3 +1,3 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_NET_DSA_MXL862) += mxl862xx_dsa.o
-mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o
+mxl862xx_dsa-y := mxl862xx.o mxl862xx-host.o mxl862xx-phylink.o mxl862xx-fw.o
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -83,6 +83,7 @@
#define INT_GPHY_READ (GPY_GPY2XX_MAGIC + 0x1)
#define INT_GPHY_WRITE (GPY_GPY2XX_MAGIC + 0x2)
+#define SYS_MISC_FW_UPDATE (SYS_MISC_MAGIC + 0x1)
#define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2)
#define MXL862XX_XPCS_MAGIC 0x1a00
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.c
@@ -0,0 +1,434 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Firmware flash and devlink support for MaxLinear MxL862xx
+ *
+ * Copyright (C) 2025 Daniel Golle <daniel@makrotopia.org>
+ *
+ * Usage:
+ * # Query running firmware version:
+ * devlink dev info mdio_bus/<bus>/<addr>
+ *
+ * # Flash new firmware (all ports are taken down automatically):
+ * devlink dev flash mdio_bus/<bus>/<addr> file <firmware.bin>
+ *
+ * The flash process takes approximately 15 minutes. Progress is
+ * reported via devlink status notifications. After a successful (or
+ * failed) flash the driver reprobes the device automatically.
+ */
+
+#include <linux/crc32.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/rtnetlink.h>
+#include <net/dsa.h>
+
+#include "mxl862xx.h"
+#include "mxl862xx-api.h"
+#include "mxl862xx-cmd.h"
+#include "mxl862xx-fw.h"
+#include "mxl862xx-host.h"
+
+/* SB PDI registers (clause-22 SMDIO address space) */
+#define MXL862XX_SB_PDI_CTRL 0xe100
+#define MXL862XX_SB_PDI_ADDR 0xe101
+#define MXL862XX_SB_PDI_DATA 0xe102
+#define MXL862XX_SB_PDI_STAT 0xe103
+
+/* SB PDI CTRL modes */
+#define MXL862XX_SB_PDI_CTRL_RST 0x00
+#define MXL862XX_SB_PDI_CTRL_WR 0x02
+
+/* SB PDI handshake magic */
+#define MXL862XX_SB_PDI_READY 0xc55c
+#define MXL862XX_SB_PDI_START 0xf48f
+#define MXL862XX_SB_PDI_END 0x3cc3
+
+/* Firmware transfer geometry */
+#define MXL862XX_FW_HDR_SIZE 20
+#define MXL862XX_FW_BANK_HALF 16384 /* words per half-bank */
+#define MXL862XX_FW_BANK_SLICE 32760 /* words per full slice */
+#define MXL862XX_FW_SB1_ADDR 0x7800 /* SB1 word address */
+
+/* Timeouts */
+#define MXL862XX_FW_READY_TIMEOUT_MS 30000
+#define MXL862XX_FW_ACK_TIMEOUT_MS 5000
+#define MXL862XX_FW_ERASE_TIMEOUT_MS 300000 /* flash erase is very slow */
+#define MXL862XX_FW_WRITE_TIMEOUT_MS 120000 /* per-slice program timeout */
+#define MXL862XX_FW_REBOOT_DELAY_MS 5000
+
+static void mxl862xx_sb_pdi_reset(struct mxl862xx_priv *priv)
+{
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_RST);
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR,
+ MXL862XX_SB_PDI_CTRL_RST);
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA,
+ MXL862XX_SB_PDI_CTRL_RST);
+}
+
+static int mxl862xx_sb_pdi_poll_stat(struct mxl862xx_priv *priv, u16 expected,
+ unsigned long timeout_ms)
+{
+ unsigned long timeout = jiffies + msecs_to_jiffies(timeout_ms);
+ int ret;
+
+ do {
+ ret = mxl862xx_smdio_read(priv, MXL862XX_SB_PDI_STAT);
+ if (ret < 0)
+ return ret;
+ if ((u16)ret == expected)
+ return 0;
+ usleep_range(10000, 11000);
+ } while (time_before(jiffies, timeout));
+
+ return -ETIMEDOUT;
+}
+
+/* Reprobe work -- dynamically allocated so it survives remove().
+ * device_reprobe() -> remove() frees priv (devm) while work is executing,
+ * so the work struct must not live in mxl862xx_priv.
+ */
+struct mxl862xx_reprobe {
+ struct device *dev;
+ struct delayed_work dwork;
+};
+
+static void mxl862xx_reprobe_work_fn(struct work_struct *work)
+{
+ struct mxl862xx_reprobe *reprobe =
+ container_of(work, struct mxl862xx_reprobe, dwork.work);
+
+ if (device_reprobe(reprobe->dev))
+ dev_err(reprobe->dev, "reprobe failed\n");
+ put_device(reprobe->dev);
+ kfree(reprobe);
+ module_put(THIS_MODULE);
+}
+
+/* MCUboot firmware image header (20 bytes) */
+struct mxl862xx_fw_hdr {
+ __le32 image_type;
+ __le32 image_size_1;
+ __le32 image_checksum_1;
+ __le32 image_size_2;
+ __le32 image_checksum_2;
+} __packed;
+
+static int mxl862xx_flash_firmware(struct mxl862xx_priv *priv,
+ const struct firmware *fw,
+ struct devlink *dl)
+{
+ const struct mxl862xx_fw_hdr *hdr;
+ u32 word_idx = 0, data_written = 0, idx = 0;
+ unsigned long next_notify = 0;
+ const u8 *payload;
+ u32 payload_size;
+ u16 word, fdata;
+ int ret, i;
+ u32 crc;
+
+ if (fw->size < MXL862XX_FW_HDR_SIZE)
+ return -EINVAL;
+
+ hdr = (const struct mxl862xx_fw_hdr *)fw->data;
+ payload = fw->data + MXL862XX_FW_HDR_SIZE;
+ payload_size = le32_to_cpu(hdr->image_size_1) +
+ le32_to_cpu(hdr->image_size_2);
+
+ if (payload_size > fw->size - MXL862XX_FW_HDR_SIZE) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: firmware file too small for declared size\n");
+ return -EINVAL;
+ }
+
+ /* Validate CRC-32 of both image slots before touching hardware */
+ if (le32_to_cpu(hdr->image_size_1)) {
+ crc = ~crc32_le(~0U, payload,
+ le32_to_cpu(hdr->image_size_1));
+ if (crc != le32_to_cpu(hdr->image_checksum_1)) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: image 1 CRC mismatch (got %08x, expected %08x)\n",
+ crc, le32_to_cpu(hdr->image_checksum_1));
+ return -EINVAL;
+ }
+ }
+
+ if (le32_to_cpu(hdr->image_size_2)) {
+ crc = ~crc32_le(~0U,
+ payload + le32_to_cpu(hdr->image_size_1),
+ le32_to_cpu(hdr->image_size_2));
+ if (crc != le32_to_cpu(hdr->image_checksum_2)) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: image 2 CRC mismatch (got %08x, expected %08x)\n",
+ crc, le32_to_cpu(hdr->image_checksum_2));
+ return -EINVAL;
+ }
+ }
+
+ /* Step 1: Tell firmware to enter MCUboot rescue mode.
+ * The FW_UPDATE command takes no payload (size 0).
+ */
+ ret = mxl862xx_api_wrap(priv, SYS_MISC_FW_UPDATE, NULL, 0,
+ false, false);
+ if (ret) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: FW_UPDATE command failed: %pe\n",
+ ERR_PTR(ret));
+ return ret;
+ }
+
+ /* From this point on, the switch is in MCUboot rescue mode.
+ * Any failure must go through the end_magic label to tell
+ * MCUboot to reboot rather than leaving it stuck waiting.
+ */
+
+ /* Step 2: Reset PDI and wait for bootloader ready */
+ devlink_flash_update_status_notify(dl, "Waiting for bootloader",
+ NULL, 0, 0);
+ mxl862xx_sb_pdi_reset(priv);
+ ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_READY,
+ MXL862XX_FW_READY_TIMEOUT_MS);
+ if (ret) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: bootloader not ready: %pe\n", ERR_PTR(ret));
+ goto end_magic;
+ }
+
+ /* Step 3: Start handshake */
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_SB_PDI_START);
+ ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_SB_PDI_START + 1,
+ MXL862XX_FW_ACK_TIMEOUT_MS);
+ if (ret) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: start handshake failed: %pe\n", ERR_PTR(ret));
+ goto end_magic;
+ }
+
+ /* Step 4: Transfer 20-byte header using auto-increment write mode */
+ devlink_flash_update_status_notify(dl, "Erasing flash", NULL, 0, 0);
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_WR);
+ for (i = 0; i < MXL862XX_FW_HDR_SIZE / 2; i++) {
+ word = fw->data[i * 2] |
+ ((u16)fw->data[i * 2 + 1] << 8);
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, word);
+ }
+ mxl862xx_sb_pdi_reset(priv);
+
+ /* Write header byte count to STAT to trigger erase */
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_FW_HDR_SIZE);
+
+ /* Wait for header ACK (header_size + 1) */
+ ret = mxl862xx_sb_pdi_poll_stat(priv, MXL862XX_FW_HDR_SIZE + 1,
+ MXL862XX_FW_ACK_TIMEOUT_MS);
+ if (ret) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: header ACK failed: %pe\n", ERR_PTR(ret));
+ goto end_magic;
+ }
+
+ /* Step 5: Wait for erase to complete (STAT goes to 0) */
+ ret = mxl862xx_sb_pdi_poll_stat(priv, 0,
+ MXL862XX_FW_ERASE_TIMEOUT_MS);
+ if (ret) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: erase timeout: %pe\n", ERR_PTR(ret));
+ goto end_magic;
+ }
+
+ /* Step 6: Transfer payload using dual-bank auto-increment writes */
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_WR);
+
+ while (idx < payload_size) {
+ if (idx + 1 < payload_size) {
+ fdata = payload[idx] |
+ ((u16)payload[idx + 1] << 8);
+ idx += 2;
+ data_written += 2;
+ } else {
+ fdata = payload[idx];
+ idx++;
+ data_written++;
+ }
+
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_DATA, fdata);
+ word_idx++;
+
+ /* Last byte(s): flush final partial slice */
+ if (idx >= payload_size) {
+ mxl862xx_sb_pdi_reset(priv);
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ data_written);
+ break;
+ }
+
+ /* Half-bank boundary: switch to SB1 address */
+ if (word_idx == MXL862XX_FW_BANK_HALF) {
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_RST);
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_ADDR,
+ MXL862XX_FW_SB1_ADDR);
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_WR);
+ } else if (word_idx >= MXL862XX_FW_BANK_SLICE) {
+ /* Full slice: flush and wait for program */
+ mxl862xx_sb_pdi_reset(priv);
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ data_written);
+ word_idx = 0;
+ data_written = 0;
+
+ ret = mxl862xx_sb_pdi_poll_stat(
+ priv, 0, MXL862XX_FW_WRITE_TIMEOUT_MS);
+ if (ret) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: write timeout at %u/%u: %pe\n",
+ idx, payload_size, ERR_PTR(ret));
+ goto end_magic;
+ }
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_CTRL,
+ MXL862XX_SB_PDI_CTRL_WR);
+
+ if (time_after(jiffies, next_notify)) {
+ devlink_flash_update_status_notify(
+ dl, "Flashing", NULL,
+ idx, payload_size);
+ next_notify = jiffies +
+ msecs_to_jiffies(500);
+ }
+ }
+ }
+
+ /* Wait for final slice to be programmed */
+ ret = mxl862xx_sb_pdi_poll_stat(priv, 0,
+ MXL862XX_FW_WRITE_TIMEOUT_MS);
+ if (ret) {
+ dev_err(&priv->mdiodev->dev,
+ "flash: final write timeout: %pe\n", ERR_PTR(ret));
+ goto end_magic;
+ }
+
+ devlink_flash_update_status_notify(dl, "Flashing", NULL,
+ payload_size, payload_size);
+
+end_magic:
+ /* Always send end magic so MCUboot reboots instead of sitting
+ * idle. The hardware reset during reprobe recovers the switch
+ * regardless of whether the transfer succeeded or failed.
+ */
+ mxl862xx_smdio_write(priv, MXL862XX_SB_PDI_STAT,
+ MXL862XX_SB_PDI_END);
+ msleep(MXL862XX_FW_REBOOT_DELAY_MS);
+
+ return ret;
+}
+
+int mxl862xx_devlink_info_get(struct dsa_switch *ds,
+ struct devlink_info_req *req,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ char ver_str[32];
+
+ snprintf(ver_str, sizeof(ver_str), "%u.%u.%u",
+ priv->fw_version.major, priv->fw_version.minor,
+ priv->fw_version.revision);
+
+ return devlink_info_version_running_put(req, "fw", ver_str);
+}
+
+int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
+ struct devlink_flash_update_params *params,
+ struct netlink_ext_ack *extack)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_sys_fw_image_version ver = {};
+ struct mxl862xx_reprobe *reprobe;
+ struct dsa_port *dp;
+ int ret, i;
+
+ if (params->component) {
+ NL_SET_ERR_MSG_MOD(extack, "component is not supported");
+ return -EOPNOTSUPP;
+ }
+
+ dev_info(ds->dev, "flash: running firmware %u.%u.%u\n",
+ priv->fw_version.major, priv->fw_version.minor,
+ priv->fw_version.revision);
+
+ /* Close all user and CPU ports while the firmware is still
+ * alive. dev_close() on user ports triggers multicast group
+ * leave and host MDB/FDB removal on the CPU port through the
+ * normal DSA callbacks so the core's tracking lists are
+ * drained before we enter MCUboot. Then mark user ports
+ * not-present so userspace cannot bring them back up during
+ * the (slow) flash process. The conduit is only closed, not
+ * detached -- it is owned by the Ethernet MAC driver and
+ * dev_open() during reprobe must be able to bring it back.
+ */
+ rtnl_lock();
+ dsa_switch_for_each_user_port(dp, ds) {
+ if (dp->user) {
+ dev_close(dp->user);
+ netif_device_detach(dp->user);
+ }
+ }
+ dsa_switch_for_each_cpu_port(dp, ds)
+ dev_close(dp->conduit);
+ rtnl_unlock();
+
+ /* Block all firmware API commands while the switch is being
+ * reflashed. The conduit is intentionally kept open -- it is
+ * owned by the Ethernet MAC driver and would not recover on
+ * reprobe if we closed it here.
+ */
+ priv->block_host = true;
+
+ /* Stop stats polling and pending host-flood work */
+ cancel_delayed_work_sync(&priv->stats_work);
+ for (i = 0; i < ds->num_ports; i++)
+ cancel_work_sync(&priv->ports[i].host_flood_work);
+
+ ret = mxl862xx_flash_firmware(priv, params->fw, ds->devlink);
+ if (ret)
+ NL_SET_ERR_MSG_MOD(extack, "firmware transfer failed");
+
+ if (!ret) {
+ /* Read new firmware version (switch just rebooted).
+ * Temporarily lift the block for this single query.
+ */
+ priv->block_host = false;
+ memset(&ver, 0, sizeof(ver));
+ if (!MXL862XX_API_READ_QUIET(priv, SYS_MISC_FW_VERSION, ver)
+ && ver.iv_major)
+ dev_info(ds->dev, "flash: new firmware %u.%u.%u\n",
+ ver.iv_major, ver.iv_minor,
+ le16_to_cpu(ver.iv_revision));
+ }
+
+ /* Silently discard all API commands during the teardown that
+ * reprobe triggers -- the switch firmware has been reset and
+ * has no knowledge of the old configuration.
+ */
+ priv->skip_teardown = true;
+
+ reprobe = kzalloc(sizeof(*reprobe), GFP_KERNEL);
+ if (!reprobe)
+ return ret;
+
+ if (!try_module_get(THIS_MODULE)) {
+ kfree(reprobe);
+ return ret;
+ }
+
+ reprobe->dev = get_device(ds->dev);
+ INIT_DELAYED_WORK(&reprobe->dwork, mxl862xx_reprobe_work_fn);
+ schedule_delayed_work(&reprobe->dwork, msecs_to_jiffies(500));
+
+ return ret;
+}
--- /dev/null
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-fw.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __MXL862XX_FW_H
+#define __MXL862XX_FW_H
+
+#include <net/dsa.h>
+
+int mxl862xx_devlink_info_get(struct dsa_switch *ds,
+ struct devlink_info_req *req,
+ struct netlink_ext_ack *extack);
+int mxl862xx_devlink_flash_update(struct dsa_switch *ds,
+ struct devlink_flash_update_params *params,
+ struct netlink_ext_ack *extack);
+
+#endif /* __MXL862XX_FW_H */
--- a/drivers/net/dsa/mxl862xx/mxl862xx-host.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-host.c
@@ -14,6 +14,7 @@
#include <linux/limits.h>
#include <net/dsa.h>
#include "mxl862xx.h"
+#include "mxl862xx-cmd.h"
#include "mxl862xx-host.h"
#define CTRL_BUSY_MASK BIT(15)
@@ -334,6 +335,12 @@ int mxl862xx_api_wrap(struct mxl862xx_pr
int ret, cmd_ret;
u16 max, crc, i;
+ if (priv->skip_teardown)
+ return 0;
+
+ if (priv->block_host && cmd != SYS_MISC_FW_UPDATE)
+ return -EBUSY;
+
dev_dbg(&priv->mdiodev->dev, "CMD %04x DATA %*ph\n", cmd, size, data);
mutex_lock_nested(&priv->mdiodev->bus->mdio_lock, MDIO_MUTEX_NESTED);
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -22,6 +22,7 @@
#include "mxl862xx.h"
#include "mxl862xx-api.h"
#include "mxl862xx-cmd.h"
+#include "mxl862xx-fw.h"
#include "mxl862xx-host.h"
#include "mxl862xx-phylink.h"
@@ -4245,6 +4246,9 @@ static const struct dsa_switch_ops mxl86
.get_pause_stats = mxl862xx_get_pause_stats,
.get_stats64 = mxl862xx_get_stats64,
.self_test = mxl862xx_serdes_self_test,
+ .devlink_info_get = mxl862xx_devlink_info_get,
+ .devlink_flash_update = mxl862xx_devlink_flash_update,
+
};
static int mxl862xx_probe(struct mdio_device *mdiodev)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -388,6 +388,8 @@ struct mxl862xx_priv {
u16 lag_bridge_ports[MXL862XX_MAX_LAG_IDS + 1];
u8 trunk_hash;
int mirror_dest;
+ bool block_host;
+ bool skip_teardown;
struct delayed_work stats_work;
};
@@ -0,0 +1,110 @@
From 2cb9aeb3a8d7ebac20331e0a533dcfbd73fa4237 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 23:42:18 +0000
Subject: [PATCH 31/35] net: dsa: mxl862xx: implement port MTU configuration
The firmware exposes a global max_packet_len register via
MXL862XX_COMMON_CFGSET. Since this is switch-wide rather than
per-port, cache each port's requested MTU and program the register
with the maximum across all ports. The firmware call is skipped when
the effective maximum does not change.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 50 +++++++++++++++++++++++++++++
drivers/net/dsa/mxl862xx/mxl862xx.h | 4 +++
2 files changed, 54 insertions(+)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -11,6 +11,7 @@
#include <linux/delay.h>
#include <linux/etherdevice.h>
#include <linux/if_bridge.h>
+#include <linux/if_vlan.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/of_mdio.h>
@@ -3768,6 +3769,53 @@ static int mxl862xx_set_ageing_time(stru
return ret;
}
+static int mxl862xx_port_change_mtu(struct dsa_switch *ds, int port,
+ int new_mtu)
+{
+ struct mxl862xx_priv *priv = ds->priv;
+ struct mxl862xx_cfg param = {};
+ int i, old_max = 0, new_max = 0;
+ int ret;
+
+ for (i = 0; i < ds->num_ports; i++) {
+ if (priv->ports[i].mtu > old_max)
+ old_max = priv->ports[i].mtu;
+ }
+
+ priv->ports[port].mtu = new_mtu;
+
+ for (i = 0; i < ds->num_ports; i++) {
+ if (priv->ports[i].mtu > new_max)
+ new_max = priv->ports[i].mtu;
+ }
+
+ if (new_max != old_max) {
+ ret = MXL862XX_API_READ(priv, MXL862XX_COMMON_CFGGET,
+ param);
+ if (ret)
+ return ret;
+
+ param.max_packet_len = cpu_to_le16(new_max +
+ VLAN_ETH_HLEN +
+ ETH_FCS_LEN);
+ ret = MXL862XX_API_WRITE(priv, MXL862XX_COMMON_CFGSET,
+ param);
+ if (ret) {
+ dev_err(ds->dev,
+ "failed to set MTU to %d: %pe\n",
+ new_mtu, ERR_PTR(ret));
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static int mxl862xx_port_max_mtu(struct dsa_switch *ds, int port)
+{
+ return U16_MAX - VLAN_ETH_HLEN - ETH_FCS_LEN;
+}
+
static void mxl862xx_port_stp_state_set(struct dsa_switch *ds, int port,
u8 state)
{
@@ -4215,6 +4263,8 @@ static const struct dsa_switch_ops mxl86
.port_disable = mxl862xx_port_disable,
.port_fast_age = mxl862xx_port_fast_age,
.set_ageing_time = mxl862xx_set_ageing_time,
+ .port_change_mtu = mxl862xx_port_change_mtu,
+ .port_max_mtu = mxl862xx_port_max_mtu,
.port_bridge_join = mxl862xx_port_bridge_join,
.port_bridge_leave = mxl862xx_port_bridge_leave,
.port_pre_bridge_flags = mxl862xx_port_pre_bridge_flags,
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -249,6 +249,8 @@ struct mxl862xx_port_stats {
* @lag_hash_bits: hash field bitmask (MXL862XX_TRUNK_HASH_*) requested
* when this port joined its LAG; used to recompute the
* global trunk_hash when a LAG is destroyed
+ * @mtu: per-port requested MTU; the global switch register
+ * is set to the maximum across all ports
*/
struct mxl862xx_port {
struct mxl862xx_priv *priv;
@@ -278,6 +280,8 @@ struct mxl862xx_port {
struct dsa_lag *lag;
bool lag_tx_enabled;
u8 lag_hash_bits;
+ /* MTU */
+ int mtu;
/* Hardware stats accumulation */
struct mxl862xx_port_stats stats;
spinlock_t stats_lock;
@@ -0,0 +1,106 @@
From d55ca68eb0d20a66c32d531b0a454871b486c1b1 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Wed, 25 Mar 2026 01:47:19 +0000
Subject: [PATCH 32/35] net: dsa: mxl862xx: support BR_HAIRPIN_MODE bridge flag
Implement hairpin mode by including the port's own bridge port ID in
its forwarding portmap. When hairpin is enabled, bridged frames whose
destination resolves to the ingress port are allowed to egress there
instead of being dropped.
For LAG ports, the LAG's dedicated bridge port is added to the
master's portmap, which naturally propagates to the LAG bridge port
via the second loop in sync_bridge_members.
The port_bridge_flags handler toggles the bit directly on the cached
portmap and pushes the update via set_bridge_port, avoiding a full
bridge member rebuild since only the calling port is affected.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 30 ++++++++++++++++++++++++++++-
drivers/net/dsa/mxl862xx/mxl862xx.h | 6 ++++++
2 files changed, 35 insertions(+), 1 deletion(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -811,6 +811,15 @@ static int mxl862xx_sync_bridge_members(
__set_bit(mxl862xx_cpu_bridge_port_id(ds, port),
priv->ports[port].portmap);
+ /* Hairpin: include the port's own bridge port so bridged
+ * frames can egress the ingress port.
+ * For LAG ports this adds the LAG bridge port, which
+ * propagates to the LAG BP in the second loop below.
+ */
+ if (priv->ports[port].hairpin)
+ __set_bit(mxl862xx_lag_bridge_port(priv, port),
+ priv->ports[port].portmap);
+
err = mxl862xx_set_bridge_port(ds, port);
if (err)
ret = err;
@@ -3939,7 +3948,7 @@ static int mxl862xx_port_pre_bridge_flag
struct netlink_ext_ack *extack)
{
if (flags.mask & ~(BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD |
- BR_LEARNING))
+ BR_LEARNING | BR_HAIRPIN_MODE))
return -EINVAL;
return 0;
@@ -3954,6 +3963,7 @@ static int mxl862xx_port_bridge_flags(st
unsigned long block = old_block;
bool need_update = false;
int ret;
+ u16 bp;
if (flags.mask & BR_FLOOD) {
if (flags.val & BR_FLOOD)
@@ -3988,6 +3998,24 @@ static int mxl862xx_port_bridge_flags(st
ret = mxl862xx_set_bridge_port(ds, port);
if (ret)
return ret;
+ }
+
+ if (flags.mask & BR_HAIRPIN_MODE) {
+ bp = mxl862xx_lag_bridge_port(priv, port);
+ priv->ports[port].hairpin = !!(flags.val & BR_HAIRPIN_MODE);
+
+ /* Hairpin adds/removes the port's own bridge port from its
+ * cached portmap. Only this port is affected -- push the
+ * updated portmap directly.
+ */
+ if (flags.val & BR_HAIRPIN_MODE)
+ __set_bit(bp, priv->ports[port].portmap);
+ else
+ __clear_bit(bp, priv->ports[port].portmap);
+
+ ret = mxl862xx_set_bridge_port(ds, port);
+ if (ret)
+ return ret;
}
return 0;
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -241,6 +241,10 @@ struct mxl862xx_port_stats {
* @stats_lock: protects accumulator reads in .get_stats64 against
* concurrent updates from the polling work
* @tag_8021q_vid: currently assigned tag_8021q management VID
+ * @hairpin: true when hairpin mode is active (BR_HAIRPIN_MODE);
+ * the port's own bridge port is included in its
+ * portmap so bridged frames can egress the ingress
+ * port
* @ingress_mirror: true when ingress mirroring is active on this port
* @egress_mirror: true when egress mirroring is active on this port
* @lag: non-NULL when port is member of a LAG group;
@@ -273,6 +277,8 @@ struct mxl862xx_port {
struct work_struct host_flood_work;
u16 tag_8021q_vid;
struct mxl862xx_evlan_block cpu_egress_evlan;
+ /* Hairpin state */
+ bool hairpin;
/* Mirror state */
bool ingress_mirror;
bool egress_mirror;
@@ -0,0 +1,95 @@
From 74b6654ba74eb142340de4c51b97c0221cfcae37 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Wed, 25 Mar 2026 01:51:33 +0000
Subject: [PATCH 33/35] net: dsa: mxl862xx: support BR_ISOLATED bridge flag
Implement port isolation by excluding isolated ports from each other's
forwarding portmaps in sync_bridge_members. Non-isolated ports can
still reach isolated ports and vice versa -- only isolated-to-isolated
forwarding is blocked.
When the isolation state changes, all bridge members' portmaps are
rebuilt via sync_bridge_members since multiple ports are affected.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 26 +++++++++++++++++++++++++-
drivers/net/dsa/mxl862xx/mxl862xx.h | 4 ++++
2 files changed, 29 insertions(+), 1 deletion(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -802,6 +802,14 @@ static int mxl862xx_sync_bridge_members(
*/
if (!mxl862xx_is_lag_master(priv, member))
continue;
+
+ /* Isolated ports cannot forward to each other.
+ * Non-isolated ports can reach everyone.
+ */
+ if (priv->ports[port].isolated &&
+ priv->ports[member].isolated)
+ continue;
+
if (member != port) {
bp = mxl862xx_lag_bridge_port(priv,
member);
@@ -3948,7 +3956,7 @@ static int mxl862xx_port_pre_bridge_flag
struct netlink_ext_ack *extack)
{
if (flags.mask & ~(BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD |
- BR_LEARNING | BR_HAIRPIN_MODE))
+ BR_LEARNING | BR_HAIRPIN_MODE | BR_ISOLATED))
return -EINVAL;
return 0;
@@ -3962,6 +3970,7 @@ static int mxl862xx_port_bridge_flags(st
unsigned long old_block = priv->ports[port].flood_block;
unsigned long block = old_block;
bool need_update = false;
+ struct dsa_port *dp;
int ret;
u16 bp;
@@ -4018,6 +4027,21 @@ static int mxl862xx_port_bridge_flags(st
return ret;
}
+ if (flags.mask & BR_ISOLATED) {
+ dp = dsa_to_port(ds, port);
+ priv->ports[port].isolated = !!(flags.val & BR_ISOLATED);
+
+ /* Isolation affects all bridge members' portmaps:
+ * isolated ports must be removed from each other's
+ * portmaps. Rebuild all portmaps for this bridge.
+ */
+ if (dp->bridge) {
+ ret = mxl862xx_sync_bridge_members(ds, dp->bridge);
+ if (ret)
+ return ret;
+ }
+ }
+
return 0;
}
--- a/drivers/net/dsa/mxl862xx/mxl862xx.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.h
@@ -214,6 +214,9 @@ struct mxl862xx_port_stats {
* @flood_block: bitmask of firmware meter indices that are currently
* rate-limiting flood traffic on this port (zero-rate
* meters used to block flooding)
+ * @isolated: true when port isolation is active (BR_ISOLATED);
+ * isolated ports are excluded from each other's
+ * forwarding portmaps
* @learning: true when address learning is enabled on this port
* @setup_done: set at end of port_setup, cleared at start of
* port_teardown; guards deferred work against
@@ -261,6 +264,7 @@ struct mxl862xx_port {
u16 fid;
DECLARE_BITMAP(portmap, MXL862XX_MAX_BRIDGE_PORTS);
unsigned long flood_block;
+ bool isolated;
bool learning;
bool setup_done;
/* VLAN state */
@@ -0,0 +1,81 @@
From 0902a6790750714445c75a66d60f1bc4897126ce Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 18:17:49 +0000
Subject: [PATCH 34/35] DO NOT SUBMIT: net: dsa: mxl862xx: re-introduce PCE
workaround for old firmware
Re-introduce the mxl862xx_disable_fw_global_rules() function that
disables firmware default global PCE rules for firmware versions
older than 1.0.80. The upstream submission replaced this with a
dev_warn() since firmware >= 1.0.80 no longer installs these rules,
but downstream deployments may still run older firmware.
This commit is for downstream use only and must not be submitted
upstream.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx.c | 45 +++++++++++++++++++++++++++--
1 file changed, 42 insertions(+), 3 deletions(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx.c
@@ -477,6 +477,43 @@ static int mxl862xx_setup_drop_meter(str
return MXL862XX_API_WRITE(priv, MXL862XX_COMMON_REGISTERMOD, reg);
}
+/* Disable firmware global PCE rules that trap various protocols to the
+ * on-die microcontroller (port 0) via PORTMAP_CPU. Under DSA, these
+ * frames must either reach the host CPU via per-port rules (link-local)
+ * or through the normal bridge forwarding path (ARP broadcast), so the
+ * global firmware rules are not needed. With the microcontroller port
+ * disabled they would silently drop matching traffic.
+ *
+ * Global rules have lower indices than CTP rules, hence higher priority
+ * in the PCE pipeline -- they must be explicitly disabled or they will
+ * shadow the per-CTP traps.
+ *
+ * Indices from gsw_flow_index.h:
+ * 1 -- BPDU (STP/RSTP, dst 01:80:c2:00:00:00)
+ * 3 -- LLDP (EtherType 0x88cc)
+ * 4 -- OAM/LACP (EtherType 0x8809)
+ * 6 -- System MAC (dst 02:e0:92:00:00:01, vendor management MAC)
+ * 7 -- ARP Request (broadcast + EtherType 0x0806 + TPA 192.0.2.1)
+ */
+static int mxl862xx_disable_fw_global_rules(struct dsa_switch *ds)
+{
+ static const u16 indices[] = { 1, 3, 4, 6, 7 };
+ struct mxl862xx_pce_rule rule;
+ int i, ret;
+
+ for (i = 0; i < ARRAY_SIZE(indices); i++) {
+ memset(&rule, 0, sizeof(rule));
+ rule.pattern.index = cpu_to_le16(indices[i]);
+ /* pattern.enable == 0 -> rule is disabled */
+
+ ret = MXL862XX_API_WRITE(ds->priv,
+ MXL862XX_TFLOW_PCERULEWRITE, rule);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
/* Per-CTP offset used for the link-local trap rule. Each port's CTP
* flow-table block is pre-allocated by the firmware during init (44
@@ -1154,9 +1191,11 @@ static int mxl862xx_setup(struct dsa_swi
if (ret)
return ret;
- if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
- dev_warn(ds->dev, "firmware < 1.0.80 installs global PCE rules "
- "that interfere with DSA operation, please update\n");
+ if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80)) {
+ ret = mxl862xx_disable_fw_global_rules(ds);
+ if (ret)
+ return ret;
+ }
/* Pre-allocate firmware resources for all ports. The DSA core
* calls change_tag_protocol() between setup() and port_setup(),
@@ -0,0 +1,251 @@
From 0ac876d5b952218ab79ea0a0815cf6fd1290b1d0 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Tue, 24 Mar 2026 18:19:56 +0000
Subject: [PATCH 35/35] DO NOT SUBMIT: net: dsa: mxl862xx: legacy SFP API
fallback for old firmware
Re-introduce the SYS_MISC_SFP_SET-based PCS implementation as a
fallback for firmware versions older than 1.0.80 which lack the
XPCS API. mxl862xx_setup_pcs() selects between the XPCS ops and
legacy SFP ops based on firmware version.
This commit is for downstream use only and must not be submitted
upstream.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
drivers/net/dsa/mxl862xx/mxl862xx-api.h | 22 +++
drivers/net/dsa/mxl862xx/mxl862xx-cmd.h | 1 +
drivers/net/dsa/mxl862xx/mxl862xx-phylink.c | 160 +++++++++++++++++++-
3 files changed, 178 insertions(+), 5 deletions(-)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-api.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-api.h
@@ -2400,6 +2400,28 @@ struct mxl862xx_sys_fw_image_version {
} __packed;
/**
+ * struct mxl862xx_sys_sfp_cfg - legacy SFP/SerDes port configuration
+ * @port_id: port id (0 or 1)
+ * @option: config options (0 - SFP mode/speed/link-status, 1 - flow control)
+ * @mode: SFP mode (0 - auto, 1 - fix, 2 - disable)
+ * @speed: select speed when mode is 1
+ * @link: get link state
+ * @fc_en: flow control (0 - disable, 1 - enable)
+ */
+struct mxl862xx_sys_sfp_cfg {
+ u8 port_id:4;
+ u8 option:4;
+ union {
+ struct {
+ u8 mode;
+ u8 speed;
+ u8 link;
+ };
+ u8 fc_en;
+ };
+} __packed;
+
+/**
* enum mxl862xx_rmon_port_type - RMON counter table type
* @MXL862XX_RMON_CTP_PORT_RX: CTP RX counters
* @MXL862XX_RMON_CTP_PORT_TX: CTP TX counters
--- a/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-cmd.h
@@ -85,6 +85,7 @@
#define SYS_MISC_FW_UPDATE (SYS_MISC_MAGIC + 0x1)
#define SYS_MISC_FW_VERSION (SYS_MISC_MAGIC + 0x2)
+#define SYS_MISC_SFP_SET (SYS_MISC_MAGIC + 0xe)
#define MXL862XX_XPCS_MAGIC 0x1a00
#define MXL862XX_XPCS_PCS_CONFIG (MXL862XX_XPCS_MAGIC + 0x1)
--- a/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
+++ b/drivers/net/dsa/mxl862xx/mxl862xx-phylink.c
@@ -52,6 +52,155 @@ static struct mxl862xx_pcs *pcs_to_mxl86
return container_of(pcs, struct mxl862xx_pcs, pcs);
}
+/* Legacy SFP-based PCS implementation for firmware < 1.0.80 */
+static int mxl862xx_legacy_pcs_config(struct phylink_pcs *pcs,
+ unsigned int neg_mode,
+ phy_interface_t interface,
+ const unsigned long *advertising,
+ bool permit_pause_to_mac)
+{
+ struct mxl862xx_priv *priv = pcs_to_mxl862xx_pcs(pcs)->priv;
+ int port = pcs_to_mxl862xx_pcs(pcs)->port;
+ struct mxl862xx_sys_sfp_cfg ser_intf = {
+ .option = 0,
+ .mode = 1,
+ };
+
+ if (port != 9 && port != 13)
+ return 0;
+
+ if (port == 9)
+ ser_intf.port_id = 0;
+ else
+ ser_intf.port_id = 1;
+
+ switch (interface) {
+ case PHY_INTERFACE_MODE_SGMII:
+ ser_intf.speed = 8;
+ break;
+ case PHY_INTERFACE_MODE_1000BASEX:
+ ser_intf.speed = (neg_mode & PHYLINK_PCS_NEG_INBAND) ? 1 : 7;
+ break;
+ case PHY_INTERFACE_MODE_2500BASEX:
+ ser_intf.speed = 4;
+ break;
+ case PHY_INTERFACE_MODE_10GBASER:
+ ser_intf.speed = 2;
+ break;
+ case PHY_INTERFACE_MODE_USXGMII:
+ ser_intf.speed = 3;
+ break;
+ default:
+ dev_err(priv->ds->dev, "unsupported interface: %s\n",
+ phy_modes(interface));
+ return -EINVAL;
+ }
+
+ return MXL862XX_API_WRITE(priv, SYS_MISC_SFP_SET, ser_intf);
+}
+
+static void mxl862xx_legacy_pcs_get_state(struct phylink_pcs *pcs,
+ struct phylink_link_state *state)
+{
+ struct mxl862xx_priv *priv = pcs_to_mxl862xx_pcs(pcs)->priv;
+ int port = pcs_to_mxl862xx_pcs(pcs)->port;
+ struct mxl862xx_port_link_cfg port_link_cfg = {
+ .port_id = port,
+ };
+ struct mxl862xx_port_cfg port_cfg = {
+ .port_id = port,
+ };
+ int ret;
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_COMMON_PORTLINKCFGGET,
+ port_link_cfg);
+ if (ret)
+ return;
+
+ ret = MXL862XX_API_READ(priv, MXL862XX_COMMON_PORTCFGGET, port_cfg);
+ if (ret)
+ return;
+
+ state->link = (port_link_cfg.link == MXL862XX_PORT_LINK_UP);
+ state->an_complete = state->link;
+
+ switch (port_link_cfg.speed) {
+ case MXL862XX_PORT_SPEED_10:
+ state->speed = SPEED_10;
+ break;
+ case MXL862XX_PORT_SPEED_100:
+ state->speed = SPEED_100;
+ break;
+ case MXL862XX_PORT_SPEED_1000:
+ state->speed = SPEED_1000;
+ break;
+ case MXL862XX_PORT_SPEED_2500:
+ state->speed = SPEED_2500;
+ break;
+ case MXL862XX_PORT_SPEED_5000:
+ state->speed = SPEED_5000;
+ break;
+ case MXL862XX_PORT_SPEED_10000:
+ state->speed = SPEED_10000;
+ break;
+ default:
+ state->speed = SPEED_UNKNOWN;
+ break;
+ }
+
+ switch (port_link_cfg.duplex) {
+ case MXL862XX_DUPLEX_HALF:
+ state->duplex = DUPLEX_HALF;
+ break;
+ case MXL862XX_DUPLEX_FULL:
+ state->duplex = DUPLEX_FULL;
+ break;
+ default:
+ state->duplex = DUPLEX_UNKNOWN;
+ break;
+ }
+
+ state->pause &= ~(MLO_PAUSE_RX | MLO_PAUSE_TX);
+ switch (port_cfg.flow_ctrl) {
+ case MXL862XX_FLOW_RXTX:
+ state->pause |= MLO_PAUSE_TXRX_MASK;
+ break;
+ case MXL862XX_FLOW_TX:
+ state->pause |= MLO_PAUSE_TX;
+ break;
+ case MXL862XX_FLOW_RX:
+ state->pause |= MLO_PAUSE_RX;
+ break;
+ case MXL862XX_FLOW_OFF:
+ default:
+ break;
+ }
+}
+
+static unsigned int
+mxl862xx_legacy_pcs_inband_caps(struct phylink_pcs *pcs,
+ phy_interface_t interface)
+{
+ switch (interface) {
+ case PHY_INTERFACE_MODE_SGMII:
+ case PHY_INTERFACE_MODE_USXGMII:
+ return LINK_INBAND_ENABLE;
+ case PHY_INTERFACE_MODE_1000BASEX:
+ return LINK_INBAND_DISABLE | LINK_INBAND_ENABLE;
+ case PHY_INTERFACE_MODE_10GBASER:
+ case PHY_INTERFACE_MODE_2500BASEX:
+ return LINK_INBAND_DISABLE;
+ default:
+ return 0;
+ }
+}
+
+static const struct phylink_pcs_ops mxl862xx_legacy_pcs_ops = {
+ .pcs_get_state = mxl862xx_legacy_pcs_get_state,
+ .pcs_config = mxl862xx_legacy_pcs_config,
+ .pcs_inband_caps = mxl862xx_legacy_pcs_inband_caps,
+};
+
static int mxl862xx_xpcs_port_id(int port)
{
return port >= 13;
@@ -389,7 +538,10 @@ void mxl862xx_setup_pcs(struct mxl862xx_
pcs->priv = priv;
pcs->port = port;
- pcs->pcs.ops = &mxl862xx_pcs_ops;
+ if (MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
+ pcs->pcs.ops = &mxl862xx_pcs_ops;
+ else
+ pcs->pcs.ops = &mxl862xx_legacy_pcs_ops;
pcs->pcs.poll = true;
}
@@ -401,9 +553,6 @@ mxl862xx_phylink_mac_select_pcs(struct p
struct mxl862xx_priv *priv = dp->ds->priv;
int port = dp->index;
- if (!MXL862XX_FW_VER_MIN(priv, 1, 0, 80))
- return NULL;
-
switch (port) {
case 9 ... 16:
return &priv->serdes_ports[port - 9].pcs;
@@ -534,7 +683,7 @@ void mxl862xx_serdes_get_stats(struct ds
}
void mxl862xx_serdes_self_test(struct dsa_switch *ds, int port,
- struct ethtool_test *etest, u64 *data)
+ struct ethtool_test *etest, u64 *data)
{
struct mxl862xx_xpcs_prbs_cfg prbs = {};
struct mxl862xx_xpcs_bert_cfg bert = {};
@@ -105,18 +105,18 @@ Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -55,6 +55,7 @@ struct tc_action;
#define DSA_TAG_PROTO_LAN937X_VALUE 27
#define DSA_TAG_PROTO_VSC73XX_8021Q_VALUE 28
@@ -57,6 +57,7 @@ struct tc_action;
#define DSA_TAG_PROTO_BRCM_LEGACY_FCS_VALUE 29
+#define DSA_TAG_PROTO_OOB_VALUE 30
#define DSA_TAG_PROTO_MXL862_VALUE 30
#define DSA_TAG_PROTO_MXL862_8021Q_VALUE 31
+#define DSA_TAG_PROTO_OOB_VALUE 32
enum dsa_tag_protocol {
DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
@@ -87,6 +88,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_RZN1_A5PSW = DSA_TAG_PROTO_RZN1_A5PSW_VALUE,
DSA_TAG_PROTO_LAN937X = DSA_TAG_PROTO_LAN937X_VALUE,
@@ -92,6 +93,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_VSC73XX_8021Q = DSA_TAG_PROTO_VSC73XX_8021Q_VALUE,
DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
DSA_TAG_PROTO_MXL862_8021Q = DSA_TAG_PROTO_MXL862_8021Q_VALUE,
+ DSA_TAG_PROTO_OOB = DSA_TAG_PROTO_OOB_VALUE,
};
@@ -148,7 +148,7 @@ Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
static __always_inline unsigned int skb_ext_total_length(void)
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
@@ -131,6 +131,15 @@ config NET_DSA_TAG_OCELOT_8021Q
@@ -145,6 +145,15 @@ config NET_DSA_TAG_OCELOT_8021Q
this mode, less TCAM resources (VCAP IS1, IS2, ES0) are available for
use with tc-flower.
@@ -166,7 +166,7 @@ Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
help
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_NET_DSA_TAG_MTK) += tag_mtk
@@ -33,6 +33,7 @@ obj-$(CONFIG_NET_DSA_TAG_MXL_862XX_8021Q
obj-$(CONFIG_NET_DSA_TAG_NONE) += tag_none.o
obj-$(CONFIG_NET_DSA_TAG_OCELOT) += tag_ocelot.o
obj-$(CONFIG_NET_DSA_TAG_OCELOT_8021Q) += tag_ocelot_8021q.o
@@ -70,7 +70,7 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
struct dsa_chip_data {
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -475,7 +475,7 @@ struct dsa_switch {
@@ -480,7 +480,7 @@ struct dsa_switch {
/*
* User mii_bus and devices for the individual ports.
*/
@@ -79,7 +79,7 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
struct mii_bus *user_mii_bus;
/* Ageing Time limits in msecs */
@@ -611,24 +611,24 @@ static inline bool dsa_is_user_port(stru
@@ -616,24 +616,24 @@ static inline bool dsa_is_user_port(stru
dsa_switch_for_each_port_continue_reverse((_dp), (_ds)) \
if (dsa_port_is_cpu((_dp)))
@@ -10,7 +10,7 @@ Signed-off-by: Antoine Tenart <antoine.tenart@bootlin.com>
--- a/drivers/net/phy/sfp.c
+++ b/drivers/net/phy/sfp.c
@@ -728,10 +728,64 @@ static int sfp_i2c_write(struct sfp *sfp
@@ -729,10 +729,64 @@ static int sfp_i2c_write(struct sfp *sfp
return ret == ARRAY_SIZE(msgs) ? len : 0;
}
@@ -77,7 +77,7 @@ Signed-off-by: Antoine Tenart <antoine.tenart@bootlin.com>
sfp->i2c = i2c;
sfp->read = sfp_i2c_read;
@@ -763,6 +817,29 @@ static int sfp_i2c_mdiobus_create(struct
@@ -764,6 +818,29 @@ static int sfp_i2c_mdiobus_create(struct
return 0;
}
@@ -107,7 +107,7 @@ Signed-off-by: Antoine Tenart <antoine.tenart@bootlin.com>
static void sfp_i2c_mdiobus_destroy(struct sfp *sfp)
{
mdiobus_unregister(sfp->i2c_mii);
@@ -1937,9 +2014,15 @@ static void sfp_sm_fault(struct sfp *sfp
@@ -1938,9 +2015,15 @@ static void sfp_sm_fault(struct sfp *sfp
static int sfp_sm_add_mdio_bus(struct sfp *sfp)
{
@@ -22,7 +22,7 @@ Submitted-by: John Crispin <john@phrozen.org>
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -89,6 +89,8 @@ source "drivers/net/dsa/xrs700x/Kconfig"
@@ -91,6 +91,8 @@ source "drivers/net/dsa/xrs700x/Kconfig"
source "drivers/net/dsa/realtek/Kconfig"
@@ -33,7 +33,7 @@ Submitted-by: John Crispin <john@phrozen.org>
depends on OF && ARCH_RZN1
--- a/drivers/net/dsa/Makefile
+++ b/drivers/net/dsa/Makefile
@@ -25,5 +25,6 @@ obj-y += mv88e6xxx/
@@ -26,5 +26,6 @@ obj-y += mxl862xx/
obj-y += ocelot/
obj-y += qca/
obj-y += realtek/
@@ -8,7 +8,7 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
--- a/net/dsa/Makefile
+++ b/net/dsa/Makefile
@@ -35,6 +35,7 @@ obj-$(CONFIG_NET_DSA_TAG_QCA) += tag_qca
@@ -37,6 +37,7 @@ obj-$(CONFIG_NET_DSA_TAG_QCA) += tag_qca
obj-$(CONFIG_NET_DSA_TAG_RTL4_A) += tag_rtl4_a.o
obj-$(CONFIG_NET_DSA_TAG_RTL8_4) += tag_rtl8_4.o
obj-$(CONFIG_NET_DSA_TAG_RZN1_A5PSW) += tag_rzn1_a5psw.o
@@ -18,7 +18,7 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
obj-$(CONFIG_NET_DSA_TAG_VSC73XX_8021Q) += tag_vsc73xx_8021q.o
--- a/net/dsa/Kconfig
+++ b/net/dsa/Kconfig
@@ -163,6 +163,12 @@ config NET_DSA_TAG_LAN9303
@@ -177,6 +177,12 @@ config NET_DSA_TAG_LAN9303
Say Y or M if you want to enable support for tagging frames for the
SMSC/Microchip LAN9303 family of switches.
@@ -33,18 +33,18 @@ Signed-off-by: Markus Stockhausen <markus.stockhausen@gmx.de>
select PACKING
--- a/include/net/dsa.h
+++ b/include/net/dsa.h
@@ -55,6 +55,7 @@ struct tc_action;
#define DSA_TAG_PROTO_LAN937X_VALUE 27
#define DSA_TAG_PROTO_VSC73XX_8021Q_VALUE 28
@@ -57,6 +57,7 @@ struct tc_action;
#define DSA_TAG_PROTO_BRCM_LEGACY_FCS_VALUE 29
+#define DSA_TAG_PROTO_RTL_OTTO_VALUE 30
#define DSA_TAG_PROTO_MXL862_VALUE 30
#define DSA_TAG_PROTO_MXL862_8021Q_VALUE 31
+#define DSA_TAG_PROTO_RTL_OTTO_VALUE 32
enum dsa_tag_protocol {
DSA_TAG_PROTO_NONE = DSA_TAG_PROTO_NONE_VALUE,
@@ -87,6 +88,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_RZN1_A5PSW = DSA_TAG_PROTO_RZN1_A5PSW_VALUE,
DSA_TAG_PROTO_LAN937X = DSA_TAG_PROTO_LAN937X_VALUE,
@@ -92,6 +93,7 @@ enum dsa_tag_protocol {
DSA_TAG_PROTO_VSC73XX_8021Q = DSA_TAG_PROTO_VSC73XX_8021Q_VALUE,
DSA_TAG_PROTO_MXL862 = DSA_TAG_PROTO_MXL862_VALUE,
DSA_TAG_PROTO_MXL862_8021Q = DSA_TAG_PROTO_MXL862_8021Q_VALUE,
+ DSA_TAG_PROTO_RTL_OTTO = DSA_TAG_PROTO_RTL_OTTO_VALUE,
};