[PATCH net-next 10/14] net: dsa: netc: introduce NXP NETC switch driver for i.MX94

Wei Fang
2026-03-16T09:41:59Z

For i.MX94 series, the NETC IP provides full 802.1Q Ethernet switch
functionality, advanced QoS with 8 traffic classes, and a full range of
TSN standards capabilities. The switch has 3 user ports and 1 CPU port,
the CPU port is connected to an internal ENETC. Since the switch and the
internal ENETC are fully integrated within the NETC IP, no back-to-back
MAC connection is required. Instead, a light-weight "pseudo MAC" is used
between the switch and the ENETC. This translates to lower power (less
logic and memory) and lower delay (as there is no serialization delay
across this link).

This patch introduces the initial NETC switch driver. At this stage,
only basic probe and remove functionality is supported. More features
will be supported in the subsequent patches.

Signed-off-by: Wei Fang <wei.fang@nxp.com>
---
MAINTAINERS | 11 +
drivers/net/dsa/Kconfig | 3 +
drivers/net/dsa/Makefile | 1 +
drivers/net/dsa/netc/Kconfig | 14 +
drivers/net/dsa/netc/Makefile | 3 +
drivers/net/dsa/netc/netc_main.c | 698 ++++++++++++++++++++++++++
drivers/net/dsa/netc/netc_platform.c | 49 ++
drivers/net/dsa/netc/netc_switch.h | 92 ++++
drivers/net/dsa/netc/netc_switch_hw.h | 155 ++++++
9 files changed, 1026 insertions(+)
create mode 100644 drivers/net/dsa/netc/Kconfig
create mode 100644 drivers/net/dsa/netc/Makefile
create mode 100644 drivers/net/dsa/netc/netc_main.c
create mode 100644 drivers/net/dsa/netc/netc_platform.c
create mode 100644 drivers/net/dsa/netc/netc_switch.h
create mode 100644 drivers/net/dsa/netc/netc_switch_hw.h

diff --git a/MAINTAINERS b/MAINTAINERS
index ff6f17458f19..7fc69dd3a6f7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19085,6 +19085,17 @@ F: Documentation/devicetree/bindings/clock/*imx*
F: drivers/clk/imx/
F: include/dt-bindings/clock/*imx*

+NXP NETC ETHERNET SWITCH DRIVER
+M: Wei Fang <wei.fang@nxp.com>
+R: Clark Wang <xiaoning.wang@nxp.com>
+L: imx@lists.linux.dev
+L: netdev@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/net/dsa/nxp,netc-switch.yaml
+F: drivers/net/dsa/netc/
+F: include/linux/dsa/tag_netc.h
+F: net/dsa/tag_netc.c
+
NXP NETC TIMER PTP CLOCK DRIVER
M: Wei Fang <wei.fang@nxp.com>
M: Clark Wang <xiaoning.wang@nxp.com>
diff --git a/drivers/net/dsa/Kconfig b/drivers/net/dsa/Kconfig
index 39fb8ead16b5..e9c7a6874791 100644
--- a/drivers/net/dsa/Kconfig
+++ b/drivers/net/dsa/Kconfig
@@ -74,8 +74,11 @@ source "drivers/net/dsa/microchip/Kconfig"

source "drivers/net/dsa/mv88e6xxx/Kconfig"

+
source "drivers/net/dsa/mxl862xx/Kconfig"

+source "drivers/net/dsa/netc/Kconfig"
+
source "drivers/net/dsa/ocelot/Kconfig"

source "drivers/net/dsa/qca/Kconfig"
diff --git a/drivers/net/dsa/Makefile b/drivers/net/dsa/Makefile
index f5a463b87ec2..d2975badffc0 100644
--- a/drivers/net/dsa/Makefile
+++ b/drivers/net/dsa/Makefile
@@ -21,6 +21,7 @@ obj-y += lantiq/
obj-y += microchip/
obj-y += mv88e6xxx/
obj-y += mxl862xx/
+obj-y += netc/
obj-y += ocelot/
obj-y += qca/
obj-y += realtek/
diff --git a/drivers/net/dsa/netc/Kconfig b/drivers/net/dsa/netc/Kconfig
new file mode 100644
index 000000000000..8824d30ed3ea
--- /dev/null
+++ b/drivers/net/dsa/netc/Kconfig
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config NET_DSA_NETC_SWITCH
+ tristate "NXP NETC Ethernet switch support"
+ depends on NET_DSA && PCI
+ select NET_DSA_TAG_NETC
+ select FSL_ENETC_MDIO
+ select NXP_NTMP
+ select NXP_NETC_LIB
+ help
+ This driver supports the NXP NETC Ethernet switch, which is embedded
+ as a PCIe function of the NXP NETC IP. But note that this driver does
+ only support switch versions greater than or equal to NETC v4.3.
+
+ If compiled as module (M), the module name is nxp-netc-switch.
diff --git a/drivers/net/dsa/netc/Makefile b/drivers/net/dsa/netc/Makefile
new file mode 100644
index 000000000000..4a5767562574
--- /dev/null
+++ b/drivers/net/dsa/netc/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_NET_DSA_NETC_SWITCH) += nxp-netc-switch.o
+nxp-netc-switch-objs := netc_main.o netc_platform.o
diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
new file mode 100644
index 000000000000..bc7d48b99610
--- /dev/null
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -0,0 +1,698 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
+/*
+ * NXP NETC switch driver
+ * Copyright 2025-2026 NXP
+ */
+
+#include <linux/clk.h>
+#include <linux/etherdevice.h>
+#include <linux/fsl/enetc_mdio.h>
+#include <linux/if_vlan.h>
+#include <linux/of_mdio.h>
+
+#include "netc_switch.h"
+
+static enum dsa_tag_protocol
+netc_get_tag_protocol(struct dsa_switch *ds, int port,
+ enum dsa_tag_protocol mprot)
+{
+ return DSA_TAG_PROTO_NETC;
+}
+
+static void netc_port_rmw(struct netc_port *np, u32 reg,
+ u32 mask, u32 val)
+{
+ u32 old, new;
+
+ WARN_ON((mask | val) != mask);
+
+ old = netc_port_rd(np, reg);
+ new = (old & ~mask) | val;
+ if (new == old)
+ return;
+
+ netc_port_wr(np, reg, new);
+}
+
+static void netc_mac_port_wr(struct netc_port *np, u32 reg, u32 val)
+{
+ if (is_netc_pseudo_port(np))
+ return;
+
+ netc_port_wr(np, reg, val);
+ if (np->caps.pmac)
+ netc_port_wr(np, reg + NETC_PMAC_OFFSET, val);
+}
+
+static void netc_mac_port_rmw(struct netc_port *np, u32 reg,
+ u32 mask, u32 val)
+{
+ u32 old, new;
+
+ if (is_netc_pseudo_port(np))
+ return;
+
+ WARN_ON((mask | val) != mask);
+
+ old = netc_port_rd(np, reg);
+ new = (old & ~mask) | val;
+ if (new == old)
+ return;
+
+ netc_port_wr(np, reg, new);
+ if (np->caps.pmac)
+ netc_port_wr(np, reg + NETC_PMAC_OFFSET, new);
+}
+
+static void netc_port_get_capability(struct netc_port *np)
+{
+ u32 val;
+
+ val = netc_port_rd(np, NETC_PMCAPR);
+ if (val & PMCAPR_HD)
+ np->caps.half_duplex = true;
+
+ if (FIELD_GET(PMCAPR_FP, val) == FP_SUPPORT)
+ np->caps.pmac = true;
+
+ val = netc_port_rd(np, NETC_PCAPR);
+ if (val & PCAPR_LINK_TYPE)
+ np->caps.pseudo_link = true;
+}
+
+static int netc_port_get_info_from_dt(struct netc_port *np,
+ struct device_node *node,
+ struct device *dev)
+{
+ if (of_find_property(node, "clock-names", NULL)) {
+ np->ref_clk = devm_get_clk_from_child(dev, node, "ref");
+ if (IS_ERR(np->ref_clk)) {
+ dev_err(dev, "Port %d cannot get reference clock\n",
+ np->dp->index);
+ return PTR_ERR(np->ref_clk);
+ }
+ }
+
+ return 0;
+}
+
+static int netc_port_create_emdio_bus(struct netc_port *np,
+ struct device_node *node)
+{
+ struct netc_switch *priv = np->switch_priv;
+ struct enetc_mdio_priv *mdio_priv;
+ struct device *dev = priv->dev;
+ struct enetc_hw *hw;
+ struct mii_bus *bus;
+ int err;
+
+ hw = enetc_hw_alloc(dev, np->iobase);
+ if (IS_ERR(hw))
+ return dev_err_probe(dev, PTR_ERR(hw),
+ "Failed to allocate enetc_hw\n");
+
+ bus = devm_mdiobus_alloc_size(dev, sizeof(*mdio_priv));
+ if (!bus)
+ return -ENOMEM;
+
+ bus->name = "NXP NETC switch external MDIO Bus";
+ bus->read = enetc_mdio_read_c22;
+ bus->write = enetc_mdio_write_c22;
+ bus->read_c45 = enetc_mdio_read_c45;
+ bus->write_c45 = enetc_mdio_write_c45;
+ bus->parent = dev;
+ mdio_priv = bus->priv;
+ mdio_priv->hw = hw;
+ mdio_priv->mdio_base = NETC_EMDIO_BASE;
+ snprintf(bus->id, MII_BUS_ID_SIZE, "%s-p%d-emdio",
+ dev_name(dev), np->dp->index);
+
+ err = devm_of_mdiobus_register(dev, bus, node);
+ if (err)
+ return dev_err_probe(dev, err,
+ "Cannot register EMDIO bus\n");
+
+ np->emdio = bus;
+
+ return 0;
+}
+
+static int netc_port_create_mdio_bus(struct netc_port *np,
+ struct device_node *node)
+{
+ struct device_node *mdio_node;
+ int err;
+
+ mdio_node = of_get_child_by_name(node, "mdio");
+ if (mdio_node) {
+ err = netc_port_create_emdio_bus(np, mdio_node);
+ of_node_put(mdio_node);
+ if (err)
+ return err;
+ }
+
+ return 0;
+}
+
+static void netc_port_free_mdio_bus(struct netc_port *np)
+{
+ if (np->emdio)
+ mdiobus_unregister(np->emdio);
+}
+
+static void netc_free_mdio_bus(struct netc_switch *priv)
+{
+ struct dsa_port *dp;
+
+ dsa_switch_for_each_available_port(dp, priv->ds) {
+ struct netc_port *np = priv->ports[dp->index];
+
+ if (dsa_port_is_user(dp))
+ netc_port_free_mdio_bus(np);
+ }
+}
+
+static int netc_init_switch_id(struct netc_switch *priv)
+{
+ struct netc_switch_regs *regs = &priv->regs;
+ struct dsa_switch *ds = priv->ds;
+
+ /* The value of 0 is reserved for the VEPA switch and cannot
+ * be used.
+ */
+ if (ds->index > SWCR_SWID || !ds->index) {
+ dev_err(priv->dev, "Switch index %d out of range\n",
+ ds->index);
+ return -ERANGE;
+ }
+
+ netc_base_wr(regs, NETC_SWCR, ds->index);
+
+ return 0;
+}
+
+static int netc_init_all_ports(struct netc_switch *priv)
+{
+ struct device *dev = priv->dev;
+ struct netc_port *np;
+ struct dsa_port *dp;
+ int err;
+
+ priv->ports = devm_kcalloc(dev, priv->info->num_ports,
+ sizeof(struct netc_port *),
+ GFP_KERNEL);
+ if (!priv->ports)
+ return -ENOMEM;
+
+ /* Some DSA interfaces may set the port even it is disabled, such
+ * as .port_disable(), .port_stp_state_set() and so on. To avoid
+ * crash caused by accessing NULL port pointer, each port is
+ * allocated its own memory. Otherwise, we need to check whether
+ * the port pointer is NULL in these interfaces. The latter is
+ * difficult for us to cover.
+ */
+ for (int i = 0; i < priv->info->num_ports; i++) {
+ np = devm_kzalloc(dev, sizeof(*np), GFP_KERNEL);
+ if (!np)
+ return -ENOMEM;
+
+ np->switch_priv = priv;
+ np->iobase = priv->regs.port + PORT_IOBASE(i);
+ netc_port_get_capability(np);
+ priv->ports[i] = np;
+ }
+
+ dsa_switch_for_each_available_port(dp, priv->ds) {
+ np = priv->ports[dp->index];
+ np->dp = dp;
+ err = netc_port_get_info_from_dt(np, dp->dn, dev);
+ if (err)
+ goto free_mdio_bus;
+
+ if (dsa_port_is_user(dp)) {
+ err = netc_port_create_mdio_bus(np, dp->dn);
+ if (err) {
+ dev_err(dev, "Failed to create MDIO bus\n");
+ goto free_mdio_bus;
+ }
+ }
+ }
+
+ return 0;
+
+free_mdio_bus:
+ netc_free_mdio_bus(priv);
+
+ return err;
+}
+
+static void netc_init_ntmp_tbl_versions(struct netc_switch *priv)
+{
+ struct ntmp_user *ntmp = &priv->ntmp;
+
+ /* All tables default to version 0 */
+ memset(&ntmp->tbl, 0, sizeof(ntmp->tbl));
+}
+
+static int netc_init_all_cbdrs(struct netc_switch *priv)
+{
+ struct netc_switch_regs *regs = &priv->regs;
+ struct ntmp_user *ntmp = &priv->ntmp;
+ int i, err;
+
+ ntmp->cbdr_num = NETC_CBDR_NUM;
+ ntmp->dev = priv->dev;
+ ntmp->ring = devm_kcalloc(ntmp->dev, ntmp->cbdr_num,
+ sizeof(struct netc_cbdr),
+ GFP_KERNEL);
+ if (!ntmp->ring)
+ return -ENOMEM;
+
+ for (i = 0; i < ntmp->cbdr_num; i++) {
+ struct netc_cbdr *cbdr = &ntmp->ring[i];
+ struct netc_cbdr_regs cbdr_regs;
+
+ cbdr_regs.pir = regs->base + NETC_CBDRPIR(i);
+ cbdr_regs.cir = regs->base + NETC_CBDRCIR(i);
+ cbdr_regs.mr = regs->base + NETC_CBDRMR(i);
+ cbdr_regs.bar0 = regs->base + NETC_CBDRBAR0(i);
+ cbdr_regs.bar1 = regs->base + NETC_CBDRBAR1(i);
+ cbdr_regs.lenr = regs->base + NETC_CBDRLENR(i);
+
+ err = ntmp_init_cbdr(cbdr, ntmp->dev, &cbdr_regs);
+ if (err)
+ goto free_cbdrs;
+ }
+
+ return 0;
+
+free_cbdrs:
+ for (i--; i >= 0; i--)
+ ntmp_free_cbdr(&ntmp->ring[i]);
+
+ return err;
+}
+
+static void netc_remove_all_cbdrs(struct netc_switch *priv)
+{
+ struct ntmp_user *ntmp = &priv->ntmp;
+
+ for (int i = 0; i < NETC_CBDR_NUM; i++)
+ ntmp_free_cbdr(&ntmp->ring[i]);
+}
+
+static int netc_init_ntmp_user(struct netc_switch *priv)
+{
+ netc_init_ntmp_tbl_versions(priv);
+
+ return netc_init_all_cbdrs(priv);
+}
+
+static void netc_free_ntmp_user(struct netc_switch *priv)
+{
+ netc_remove_all_cbdrs(priv);
+}
+
+static void netc_switch_dos_default_config(struct netc_switch *priv)
+{
+ struct netc_switch_regs *regs = &priv->regs;
+ u32 val;
+
+ val = DOSL2CR_SAMEADDR | DOSL2CR_MSAMCC;
+ netc_base_wr(regs, NETC_DOSL2CR, val);
+
+ val = DOSL3CR_SAMEADDR | DOSL3CR_IPSAMCC;
+ netc_base_wr(regs, NETC_DOSL3CR, val);
+}
+
+static void netc_switch_vfht_default_config(struct netc_switch *priv)
+{
+ struct netc_switch_regs *regs = &priv->regs;
+ u32 val;
+
+ val = netc_base_rd(regs, NETC_VFHTDECR2);
+
+ /* if no match is found in the VLAN Filter table, then VFHTDECR2[MLO]
+ * will take effect. VFHTDECR2[MLO] is set to "Software MAC learning
+ * secure" by default. Notice BPCR[MLO] will override VFHTDECR2[MLO]
+ * if its value is not zero.
+ */
+ val = u32_replace_bits(val, MLO_SW_SEC, VFHTDECR2_MLO);
+ val = u32_replace_bits(val, MFO_NO_MATCH_DISCARD, VFHTDECR2_MFO);
+ netc_base_wr(regs, NETC_VFHTDECR2, val);
+}
+
+static void netc_port_set_max_frame_size(struct netc_port *np,
+ u32 max_frame_size)
+{
+ netc_mac_port_wr(np, NETC_PM_MAXFRM(0),
+ PM_MAXFRAM & max_frame_size);
+}
+
+static void netc_switch_fixed_config(struct netc_switch *priv)
+{
+ netc_switch_dos_default_config(priv);
+ netc_switch_vfht_default_config(priv);
+}
+
+static void netc_port_set_tc_max_sdu(struct netc_port *np,
+ int tc, u32 max_sdu)
+{
+ u32 val = max_sdu & PTCTMSDUR_MAXSDU;
+
+ val |= FIELD_PREP(PTCTMSDUR_SDU_TYPE, SDU_TYPE_MPDU);
+ netc_port_wr(np, NETC_PTCTMSDUR(tc), val);
+}
+
+static void netc_port_set_all_tc_msdu(struct netc_port *np)
+{
+ for (int tc = 0; tc < NETC_TC_NUM; tc++)
+ netc_port_set_tc_max_sdu(np, tc, NETC_MAX_FRAME_LEN);
+}
+
+static void netc_port_set_mlo(struct netc_port *np, enum netc_mlo mlo)
+{
+ netc_port_rmw(np, NETC_BPCR, BPCR_MLO, FIELD_PREP(BPCR_MLO, mlo));
+}
+
+static void netc_port_fixed_config(struct netc_port *np)
+{
+ /* Default IPV and DR setting */
+ netc_port_rmw(np, NETC_PQOSMR, PQOSMR_VS | PQOSMR_VE,
+ PQOSMR_VS | PQOSMR_VE);
+
+ /* Enable L2 and L3 DOS */
+ netc_port_rmw(np, NETC_PCR, PCR_L2DOSE | PCR_L3DOSE,
+ PCR_L2DOSE | PCR_L3DOSE);
+}
+
+static void netc_port_default_config(struct netc_port *np)
+{
+ netc_port_fixed_config(np);
+
+ /* Default VLAN unaware */
+ netc_port_rmw(np, NETC_BPDVR, BPDVR_RXVAM, BPDVR_RXVAM);
+
+ if (dsa_port_is_cpu(np->dp))
+ /* For CPU port, source port pruning is disabled and
+ * hardware MAC learning is enabled by default.
+ */
+ netc_port_rmw(np, NETC_BPCR, BPCR_SRCPRND | BPCR_MLO,
+ BPCR_SRCPRND | FIELD_PREP(BPCR_MLO, MLO_HW));
+ else
+ netc_port_set_mlo(np, MLO_DISABLE);
+
+ netc_port_set_max_frame_size(np, NETC_MAX_FRAME_LEN);
+ netc_port_set_all_tc_msdu(np);
+ netc_mac_port_rmw(np, NETC_PM_CMD_CFG(0), PM_CMD_CFG_TX_EN,
+ PM_CMD_CFG_TX_EN);
+ netc_port_rmw(np, NETC_POR, PCR_TXDIS, 0);
+}
+
+static int netc_setup(struct dsa_switch *ds)
+{
+ struct netc_switch *priv = ds->priv;
+ struct dsa_port *dp;
+ int err;
+
+ err = netc_init_switch_id(priv);
+ if (err)
+ return err;
+
+ err = netc_init_all_ports(priv);
+ if (err)
+ return err;
+
+ err = netc_init_ntmp_user(priv);
+ if (err)
+ goto free_mdio_bus;
+
+ netc_switch_fixed_config(priv);
+
+ /* default setting for ports */
+ dsa_switch_for_each_available_port(dp, ds)
+ netc_port_default_config(priv->ports[dp->index]);
+
+ return 0;
+
+free_mdio_bus:
+ netc_free_mdio_bus(priv);
+
+ return err;
+}
+
+static void netc_teardown(struct dsa_switch *ds)
+{
+ struct netc_switch *priv = ds->priv;
+
+ netc_free_ntmp_user(priv);
+ netc_free_mdio_bus(priv);
+}
+
+static struct device_node *netc_get_switch_ports(struct device_node *node)
+{
+ struct device_node *ports;
+
+ ports = of_get_child_by_name(node, "ports");
+ if (!ports)
+ ports = of_get_child_by_name(node, "ethernet-ports");
+
+ return ports;
+}
+
+static bool netc_port_is_emdio_consumer(struct device_node *node)
+{
+ struct device_node *mdio_node;
+
+ /* If the port node has phy-handle property and it does
+ * not contain a mdio child node, then the port is the
+ * EMDIO consumer.
+ */
+ mdio_node = of_get_child_by_name(node, "mdio");
+ if (!mdio_node)
+ return true;
+
+ of_node_put(mdio_node);
+
+ return false;
+}
+
+/* Currently, phylink_of_phy_connect() is called by dsa_user_create(),
+ * so if the switch uses the external MDIO controller (like the EMDIO
+ * function) to manage the external PHYs. The MDIO bus may not be
+ * created when phylink_of_phy_connect() is called, so it will return
+ * an error and cause the switch driver to fail to probe.
+ * This workaround can be removed when DSA phylink_of_phy_connect()
+ * calls are moved from probe() to ndo_open().
+ */
+static int netc_switch_check_emdio_is_ready(struct device *dev)
+{
+ struct device_node *ports, *phy_node;
+ struct phy_device *phydev;
+ int err = 0;
+
+ ports = netc_get_switch_ports(dev->of_node);
+ if (!ports)
+ return 0;
+
+ for_each_available_child_of_node_scoped(ports, child) {
+ /* If the node does not have phy-handle property, then
+ * the port does not connect to a PHY, so the port is
+ * not the EMDIO consumer.
+ */
+ phy_node = of_parse_phandle(child, "phy-handle", 0);
+ if (!phy_node)
+ continue;
+
+ if (!netc_port_is_emdio_consumer(child)) {
+ of_node_put(phy_node);
+ continue;
+ }
+
+ phydev = of_phy_find_device(phy_node);
+ of_node_put(phy_node);
+ if (!phydev) {
+ err = -EPROBE_DEFER;
+ goto out;
+ }
+
+ put_device(&phydev->mdio.dev);
+ }
+
+out:
+ of_node_put(ports);
+
+ return err;
+}
+
+static int netc_switch_pci_init(struct pci_dev *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct netc_switch_regs *regs;
+ struct netc_switch *priv;
+ int err;
+
+ pcie_flr(pdev);
+ err = pci_enable_device_mem(pdev);
+ if (err)
+ return dev_err_probe(dev, err, "Failed to enable device\n");
+
+ /* The command BD rings and NTMP tables need DMA */
+ dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
+ err = pci_request_mem_regions(pdev, KBUILD_MODNAME);
+ if (err) {
+ dev_err(dev, "Failed to request memory regions, err: %pe\n",
+ ERR_PTR(err));
+ goto disable_pci_device;
+ }
+
+ pci_set_master(pdev);
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv) {
+ err = -ENOMEM;
+ goto release_mem_regions;
+ }
+
+ priv->pdev = pdev;
+ priv->dev = dev;
+
+ regs = &priv->regs;
+ regs->base = pci_ioremap_bar(pdev, NETC_REGS_BAR);
+ if (!regs->base) {
+ err = -ENXIO;
+ dev_err(dev, "pci_ioremap_bar() failed\n");
+ goto release_mem_regions;
+ }
+
+ regs->port = regs->base + NETC_REGS_PORT_BASE;
+ regs->global = regs->base + NETC_REGS_GLOBAL_BASE;
+ pci_set_drvdata(pdev, priv);
+
+ return 0;
+
+release_mem_regions:
+ pci_release_mem_regions(pdev);
+disable_pci_device:
+ pci_disable_device(pdev);
+
+ return err;
+}
+
+static void netc_switch_pci_destroy(struct pci_dev *pdev)
+{
+ struct netc_switch *priv = pci_get_drvdata(pdev);
+
+ iounmap(priv->regs.base);
+ pci_release_mem_regions(pdev);
+ pci_disable_device(pdev);
+}
+
+static void netc_switch_get_ip_revision(struct netc_switch *priv)
+{
+ struct netc_switch_regs *regs = &priv->regs;
+ u32 val = netc_glb_rd(regs, NETC_IPBRR0);
+
+ priv->revision = val & IPBRR0_IP_REV;
+}
+
+static const struct dsa_switch_ops netc_switch_ops = {
+ .get_tag_protocol = netc_get_tag_protocol,
+ .setup = netc_setup,
+ .teardown = netc_teardown,
+};
+
+static int netc_switch_probe(struct pci_dev *pdev,
+ const struct pci_device_id *id)
+{
+ struct device_node *node = dev_of_node(&pdev->dev);
+ struct device *dev = &pdev->dev;
+ struct netc_switch *priv;
+ struct dsa_switch *ds;
+ int err;
+
+ if (!node)
+ return dev_err_probe(dev, -ENODEV,
+ "No DT bindings, skipping\n");
+
+ err = netc_switch_check_emdio_is_ready(dev);
+ if (err)
+ return err;
+
+ err = netc_switch_pci_init(pdev);
+ if (err)
+ return err;
+
+ priv = pci_get_drvdata(pdev);
+ netc_switch_get_ip_revision(priv);
+
+ err = netc_switch_platform_probe(priv);
+ if (err)
+ goto destroy_netc_switch;
+
+ ds = devm_kzalloc(dev, sizeof(*ds), GFP_KERNEL);
+ if (!ds) {
+ err = -ENOMEM;
+ goto destroy_netc_switch;
+ }
+
+ ds->dev = dev;
+ ds->num_ports = priv->info->num_ports;
+ ds->num_tx_queues = NETC_TC_NUM;
+ ds->ops = &netc_switch_ops;
+ ds->priv = priv;
+
+ priv->ds = ds;
+
+ err = dsa_register_switch(ds);
+ if (err) {
+ dev_err_probe(dev, err, "Failed to register DSA switch\n");
+ goto destroy_netc_switch;
+ }
+
+ return 0;
+
+destroy_netc_switch:
+ netc_switch_pci_destroy(pdev);
+
+ return err;
+}
+
+static void netc_switch_remove(struct pci_dev *pdev)
+{
+ struct netc_switch *priv = pci_get_drvdata(pdev);
+
+ if (!priv)
+ return;
+
+ dsa_unregister_switch(priv->ds);
+ netc_switch_pci_destroy(pdev);
+}
+
+static void netc_switch_shutdown(struct pci_dev *pdev)
+{
+ struct netc_switch *priv = pci_get_drvdata(pdev);
+
+ if (!priv)
+ return;
+
+ dsa_switch_shutdown(priv->ds);
+ pci_set_drvdata(pdev, NULL);
+}
+
+static const struct pci_device_id netc_switch_ids[] = {
+ { PCI_DEVICE(NETC_SWITCH_VENDOR_ID, NETC_SWITCH_DEVICE_ID) },
+ { }
+};
+MODULE_DEVICE_TABLE(pci, netc_switch_ids);
+
+static struct pci_driver netc_switch_driver = {
+ .name = KBUILD_MODNAME,
+ .id_table = netc_switch_ids,
+ .probe = netc_switch_probe,
+ .remove = netc_switch_remove,
+ .shutdown = netc_switch_shutdown,
+};
+module_pci_driver(netc_switch_driver);
+
+MODULE_DESCRIPTION("NXP NETC Switch driver");
+MODULE_LICENSE("Dual BSD/GPL");
diff --git a/drivers/net/dsa/netc/netc_platform.c b/drivers/net/dsa/netc/netc_platform.c
new file mode 100644
index 000000000000..abd599ea9c8d
--- /dev/null
+++ b/drivers/net/dsa/netc/netc_platform.c
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
+/*
+ * NXP NETC switch driver
+ * Copyright 2025-2026 NXP
+ */
+
+#include "netc_switch.h"
+
+struct netc_switch_platform {
+ u16 revision;
+ const struct netc_switch_info *info;
+};
+
+static const struct netc_switch_info imx94_info = {
+ .num_ports = 4,
+};
+
+static const struct netc_switch_platform netc_platforms[] = {
+ { .revision = NETC_SWITCH_REV_4_3, .info = &imx94_info, },
+ { }
+};
+
+static const struct netc_switch_info *
+netc_switch_get_info(struct netc_switch *priv)
+{
+ int i;
+
+ /* Matching based on IP revision */
+ for (i = 0; i < ARRAY_SIZE(netc_platforms); i++) {
+ if (priv->revision == netc_platforms[i].revision)
+ return netc_platforms[i].info;
+ }
+
+ return NULL;
+}
+
+int netc_switch_platform_probe(struct netc_switch *priv)
+{
+ const struct netc_switch_info *info = netc_switch_get_info(priv);
+
+ if (!info) {
+ dev_err(priv->dev, "Cannot find switch platform info\n");
+ return -EINVAL;
+ }
+
+ priv->info = info;
+
+ return 0;
+}
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
new file mode 100644
index 000000000000..dac19bfba02b
--- /dev/null
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause) */
+/*
+ * Copyright 2025-2026 NXP
+ */
+
+#ifndef _NETC_SWITCH_H
+#define _NETC_SWITCH_H
+
+#include <linux/dsa/tag_netc.h>
+#include <linux/fsl/netc_global.h>
+#include <linux/fsl/ntmp.h>
+#include <linux/of_device.h>
+#include <linux/of_net.h>
+#include <linux/pci.h>
+
+#include "netc_switch_hw.h"
+
+#define NETC_REGS_BAR 0
+#define NETC_MSIX_TBL_BAR 2
+#define NETC_REGS_PORT_BASE 0x4000
+/* register block size per port */
+#define NETC_REGS_PORT_SIZE 0x4000
+#define PORT_IOBASE(p) (NETC_REGS_PORT_SIZE * (p))
+#define NETC_REGS_GLOBAL_BASE 0x70000
+
+#define NETC_SWITCH_REV_4_3 0x0403
+
+#define NETC_TC_NUM 8
+#define NETC_CBDR_NUM 2
+
+#define NETC_MAX_FRAME_LEN 9600
+
+struct netc_switch;
+
+struct netc_switch_info {
+ u32 num_ports;
+};
+
+struct netc_port_caps {
+ u32 half_duplex:1; /* indicates whether the port support half-duplex */
+ u32 pmac:1; /* indicates whether the port has preemption MAC */
+ u32 pseudo_link:1;
+};
+
+struct netc_port {
+ void __iomem *iobase;
+ struct netc_switch *switch_priv;
+ struct netc_port_caps caps;
+ struct dsa_port *dp;
+ struct clk *ref_clk; /* RGMII/RMII reference clock */
+ struct mii_bus *emdio;
+};
+
+struct netc_switch_regs {
+ void __iomem *base;
+ void __iomem *port;
+ void __iomem *global;
+};
+
+struct netc_switch {
+ struct pci_dev *pdev;
+ struct device *dev;
+ struct dsa_switch *ds;
+ u16 revision;
+
+ const struct netc_switch_info *info;
+ struct netc_switch_regs regs;
+ struct netc_port **ports;
+
+ struct ntmp_user ntmp;
+};
+
+/* Write/Read Switch base registers */
+#define netc_base_rd(r, o) netc_read((r)->base + (o))
+#define netc_base_wr(r, o, v) netc_write((r)->base + (o), v)
+
+/* Write/Read registers of Switch Port (including pseudo MAC port) */
+#define netc_port_rd(p, o) netc_read((p)->iobase + (o))
+#define netc_port_wr(p, o, v) netc_write((p)->iobase + (o), v)
+
+/* Write/Read Switch global registers */
+#define netc_glb_rd(r, o) netc_read((r)->global + (o))
+#define netc_glb_wr(r, o, v) netc_write((r)->global + (o), v)
+
+static inline bool is_netc_pseudo_port(struct netc_port *np)
+{
+ return np->caps.pseudo_link;
+}
+
+int netc_switch_platform_probe(struct netc_switch *priv);
+
+#endif
diff --git a/drivers/net/dsa/netc/netc_switch_hw.h b/drivers/net/dsa/netc/netc_switch_hw.h
new file mode 100644
index 000000000000..03b49857c854
--- /dev/null
+++ b/drivers/net/dsa/netc/netc_switch_hw.h
@@ -0,0 +1,155 @@
+/* SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause) */
+/*
+ * Copyright 2025-2026 NXP
+ */
+
+#ifndef _NETC_SWITCH_HW_H
+#define _NETC_SWITCH_HW_H
+
+#include <linux/bitops.h>
+
+#define NETC_SWITCH_VENDOR_ID 0x1131
+#define NETC_SWITCH_DEVICE_ID 0xeef2
+
+/* Definition of Switch base registers */
+#define NETC_CBDRMR(a) (0x0800 + (a) * 0x30)
+#define NETC_CBDRBAR0(a) (0x0810 + (a) * 0x30)
+#define NETC_CBDRBAR1(a) (0x0814 + (a) * 0x30)
+#define NETC_CBDRPIR(a) (0x0818 + (a) * 0x30)
+#define NETC_CBDRCIR(a) (0x081c + (a) * 0x30)
+#define NETC_CBDRLENR(a) (0x0820 + (a) * 0x30)
+
+#define NETC_SWCR 0x1018
+#define SWCR_SWID GENMASK(2, 0)
+
+#define NETC_DOSL2CR 0x1220
+#define DOSL2CR_SAMEADDR BIT(0)
+#define DOSL2CR_MSAMCC BIT(1)
+
+#define NETC_DOSL3CR 0x1224
+#define DOSL3CR_SAMEADDR BIT(0)
+#define DOSL3CR_IPSAMCC BIT(1)
+
+#define NETC_VFHTDECR1 0x2014
+#define NETC_VFHTDECR2 0x2018
+#define VFHTDECR2_ET_PORT(a) BIT((a))
+#define VFHTDECR2_MLO GENMASK(26, 24)
+#define VFHTDECR2_MFO GENMASK(28, 27)
+
+/* Definition of Switch port registers */
+#define NETC_PCAPR 0x0000
+#define PCAPR_LINK_TYPE BIT(4)
+#define PCAPR_NUM_TC GENMASK(15, 12)
+#define PCAPR_NUM_Q GENMASK(19, 16)
+#define PCAPR_NUM_CG GENMASK(27, 24)
+#define PCAPR_TGS BIT(28)
+#define PCAPR_CBS BIT(29)
+
+#define NETC_PMCAPR 0x0004
+#define PMCAPR_HD BIT(8)
+#define PMCAPR_FP GENMASK(10, 9)
+#define FP_SUPPORT 2
+
+#define NETC_PCR 0x0010
+#define PCR_HDR_FMT BIT(0)
+#define PCR_NS_TAG_PORT BIT(3)
+#define PCR_L2DOSE BIT(4)
+#define PCR_L3DOSE BIT(5)
+#define PCR_TIMER_CS BIT(8)
+#define PCR_PSPEED GENMASK(29, 16)
+#define PSPEED_SET_VAL(s) FIELD_PREP(PCR_PSPEED, ((s) / 10 - 1))
+
+#define NETC_PQOSMR 0x0054
+#define PQOSMR_VS BIT(0)
+#define PQOSMR_VE BIT(1)
+#define PQOSMR_DDR GENMASK(3, 2)
+#define PQOSMR_DIPV GENMASK(6, 4)
+#define PQOSMR_VQMP GENMASK(19, 16)
+#define PQOSMR_QVMP GENMASK(23, 20)
+
+#define NETC_POR 0x100
+#define PCR_TXDIS BIT(0)
+#define PCR_RXDIS BIT(1)
+
+#define NETC_PTCTMSDUR(a) (0x208 + (a) * 0x20)
+#define PTCTMSDUR_MAXSDU GENMASK(15, 0)
+#define PTCTMSDUR_SDU_TYPE GENMASK(17, 16)
+#define SDU_TYPE_PPDU 0
+#define SDU_TYPE_MPDU 1
+#define SDU_TYPE_MSDU 2
+
+#define NETC_BPCR 0x500
+#define BPCR_DYN_LIMIT GENMASK(15, 0)
+#define BPCR_MLO GENMASK(22, 20)
+#define BPCR_UUCASTE BIT(24)
+#define BPCR_UMCASTE BIT(25)
+#define BPCR_MCASTE BIT(26)
+#define BPCR_BCASTE BIT(27)
+#define BPCR_STAMVD BIT(28)
+#define BPCR_SRCPRND BIT(29)
+
+/* MAC learning options, see BPCR[MLO], VFHTDECR2[MLO] and
+ * VLAN Filter Table CFGE_DATA[MLO]
+ */
+enum netc_mlo {
+ MLO_NOT_OVERRIDE = 0,
+ MLO_DISABLE,
+ MLO_HW,
+ MLO_SW_SEC,
+ MLO_SW_UNSEC,
+ MLO_DISABLE_SMAC,
+};
+
+/* MAC forwarding options, see VFHTDECR2[MFO] and VLAN
+ * Filter Table CFGE_DATA[MFO]
+ */
+enum netc_mfo {
+ MFO_NO_FDB_LOOKUP = 1,
+ MFO_NO_MATCH_FLOOD,
+ MFO_NO_MATCH_DISCARD,
+};
+
+#define NETC_BPDVR 0x510
+#define BPDVR_VID GENMASK(11, 0)
+#define BPDVR_DEI BIT(12)
+#define BPDVR_PCP GENMASK(15, 13)
+#define BPDVR_TPID BIT(16)
+#define BPDVR_RXTAGA GENMASK(23, 20)
+#define BPDVR_RXVAM BIT(24)
+#define BPDVR_TXTAGA GENMASK(26, 25)
+
+/* Definition of Switch ethernet MAC port registers */
+#define NETC_PMAC_OFFSET 0x400
+#define NETC_PM_CMD_CFG(a) (0x1008 + (a) * 0x400)
+#define PM_CMD_CFG_TX_EN BIT(0)
+#define PM_CMD_CFG_RX_EN BIT(1)
+#define PM_CMD_CFG_PAUSE_FWD BIT(7)
+#define PM_CMD_CFG_PAUSE_IGN BIT(8)
+#define PM_CMD_CFG_LOOP_EN BIT(10)
+#define PM_CMD_CFG_LPBK_MODE GENMASK(12, 11)
+#define PM_CMD_CFG_CNT_FRM_EN BIT(13)
+#define PM_CMD_CFG_TS_PNT BIT(14)
+#define PM_CMD_CFG_TXP BIT(15)
+#define PM_CMD_CFG_SEND_IDLE BIT(16)
+#define PM_CMD_CFG_HD_FCEN BIT(18)
+#define PM_CMD_CFG_SFD BIT(21)
+#define PM_CMD_CFG_TX_FLUSH BIT(22)
+#define PM_CMD_CFG_LOWP_EN BIT(23)
+#define PM_CMD_CFG_RX_LOWP_ETY BIT(24)
+#define PM_CMD_CFG_SWR BIT(26)
+#define PM_CMD_CFG_RX_FLUSH BIT(28)
+#define PM_CMD_CFG_RXSTP BIT(29)
+#define PM_CMD_CFG_TS_MODE BIT(30)
+#define PM_CMD_CFG_MG BIT(31)
+
+#define NETC_PM_MAXFRM(a) (0x1014 + (a) * 0x400)
+#define PM_MAXFRAM GENMASK(15, 0)
+
+#define NETC_PEMDIOCR 0x1c00
+#define NETC_EMDIO_BASE NETC_PEMDIOCR
+
+/* Definition of global registers (read only) */
+#define NETC_IPBRR0 0x0bf8
+#define IPBRR0_IP_REV GENMASK(15, 0)
+
+#endif
--
2.34.1