IPQ5018: NSS offload on kernel 6.18 with the upstream ethernet stack (GL-B3000)

Short version: the NSS core on the IPQ5018 accelerates routed NAT and both Wi-Fi radios on kernel 6.18 with the upstream stmmac driver - no qca-nss-dp, no qca-ssdk - and the CPU stays above 90 % idle while it does. It has been running here since late August on a GL.iNet GL-B3000.

This replaces the original opening post. Its Wi-Fi section said the offload could not work and blamed the firmware; that was wrong, and the reason is in post #166. Everything else stands.

Both branches are public:

Documentation: https://github.com/kuncy7/openwrt-nss-edma/blob/ipq50xx-nss/README.ipq50xx.md

  • build steps, the .config, how the plane comes up, the porting checklist for other IPQ5018 boards.

They are layered on @JuliusBairaktaris's edma-nss tree, which is where the NSS-on-mainline work actually lives; my part is the ipq50xx half.

This is a second data point rather than a first. @ADCDS got NSS running on an IPQ5018 (Xiaomi AX3000T) back in July and wrote it up in Xiaomi AX3000T (RD03v2, IPQ5018): working OpenWrt, installs to NAND. That port uses qca-nss-dp and an Airoha switch on 6.12; this one goes the other way - upstream stmmac on 6.18, QCA8337 - so between the two, most IPQ5018 boards are now covered by some known-working combination.

@georgem83 asked in that thread whether NSS would work with the new ethernet stack. It does, and this is what it takes.

The glue

The NSS firmware drives the Synopsys GMAC DMA itself, with its own descriptor rings. Rather than replace the MAC driver, a small module (qca-dwmac-nss) claims the data path through an API added to stmmac: TX queues drained, NAPI off, DMA stopped, ndo_start_xmit redirected, and only then the controller handed over. phylink, MDIO and the netdev stay with the host, so the PHY and the link keep being managed by the kernel exactly as before. Releasing the claim deliberately leaves TX disabled - after the firmware has owned the controller the host ring state is stale, and the only correct way back is a full reinit, which the owner triggers by bouncing the interface.

None of it happens at probe. Which GMACs to take over is a runtime decision through a debugfs mask, and if the takeover fails the port stays on the host data path with working networking. That property is worth more than it sounds: every failed experiment during this work ended with a reachable router.

Three orderings that fail silently

All measured, each cost me days:

  • arming on a down interface - the firmware starts, TX works, and not one ingress frame is ever delivered;
  • arming while netifd is still applying config - netifd bounces the interface mid-takeover, and the promisc flag the bridge sets afterwards never reaches the hardware;
  • loading qca-nss-vlan before the arm - the VLAN manager resolves the trunk's NSS interface number at NETDEV_REGISTER time; before the arm that number does not exist, registration fails silently (the driver's debug macros are compiled out at the default log level), and every tagged frame disappears.

Working order: wait until netifd is done, arm, wait for the takeover, then load the VLAN manager - its notifier replays NETDEV_REGISTER for existing netdevs, so ordering against netifd stops mattering. That is what the nss-tools-dwmac service in the branch does.

One prerequisite that is easy to miss: the CMN PLL clock fix. The driver lets pm_clk gate the CMN block's bus clocks a few milliseconds after probe, which races with early boot on IPQ5018; a build that loses the race dies right after the CMN PLL probe - watchdog reset, nothing on the console. Whether a given board and build lose it varies (@georgem83's IPQ5018 boards boot without it; my B3000 boot-loops 8/8, and on the AX6000 the stock snapshot boots while an NSS build does not), so treat it as required for this branch rather than universally. Merged into openwrt main as 86b584bd0994; kernel v4 still on the clk list: https://lore.kernel.org/linux-clk/20260813093351.178419-1-kuncy7@gmail.com/

The switch, and why one CPU port is enough

The NSS firmware parses 802.1Q natively but cannot parse the two-byte Atheros header that DSA's tag_qca puts where the ethertype should be. So on this SoC DSA user ports and ECM acceleration are mutually exclusive. The port model is also reversed compared to ipq807x: the NSS phys_if is the GMAC, and the netdev is the switch trunk.

If you are seeing eth_rx_unknown_l3_protocol counting most of your frames, with iface_count=0 and accelerated_count=0 on a QCA8337 board

  • @LS3434 posted exactly that in the Qualcommax NSS Build thread - this is why, and no ECM patch will fix it.

What works instead: let qca8k do the hard bring-up (SerDes, clocks, uniphy), then unbind it and drive the fabric directly. qca8337-nss wakes the front PHYs, re-enables the MACs, turns the Atheros header off and programs the VTU from a module parameter. The CPU port then carries a plain 802.1Q trunk - eth0.1 for LAN, eth0.2 for WAN - and those two become NSS dynamic interfaces of type 17, which the firmware understands.

The rule then carries two distinct NSS interface numbers over a single physical port, which is why this works on a board that has only one CPU port. That is the practical difference from the AX3000T approach, which needs LAN and WAN on different conduits. The cost is that the LAN ports share one VLAN, so per-port separation inside the LAN is gone; WAN/LAN isolation, VLAN-aware bridging on top and the clean fallback are intact.

Second board: Archer AX55

The reason this is written as a platform port rather than a device port: the same arrangement should carry to the TP-Link Archer AX55 v1, also IPQ5018 + QCN6122, whose OpenWrt support I maintain separately. What differs is the switch - RTL8367S instead of QCA8337 - so it needs an rtl8367-nss counterpart doing the same job: unbind rtl8365mb, program the VLAN table and PVIDs over MDIO. The register knowledge is already in the kernel driver, so that is transcription rather than reverse engineering.

Two things make it interesting beyond "one more board". Its CPU port runs at 2.5 Gbit/s, which removes the ceiling described below entirely. And it is the only board here with a foreign tagger, so getting it to work would show the approach is not shaped around one switch.

It is not done yet. I am saying what it needs, not claiming it runs.

Warm reboot, and a note for @ADCDS

Your docs/no-uart-reflash.md describes a state where, after a chain of warm reboots, the switch and the NSS core do not come up cleanly and only a cold start recovers. I had exactly that, and it has a one-line cause.

A soft reboot does not reset the NSS block, so the previous kernel's firmware is still executing out of the load region while nss_hal_firmware_load() memcpy's the new image over it. The new firmware then comes up dead or mute; cold boots are fine, which is what makes it look intermittent. TFTP-booted test images hide it completely, because u-boot's ethernet init crashes the old firmware first. The fix is to assert the core-local reset clamp before the copy - the same thing nss_hal_remove() already does on rmmod. Four out of four warm reboots clean afterwards, and the full matrix (cold, warm, warm-after-cold, sysupgrade) passes. It is patch 0136 in the feed branch.

One warning attached: do not pulse the GCC block resets on IPQ5018 from the driver. I tried; asserting them kills ethernet until a power cycle, because - as your own patch 0029 explains - asserting the GCC reset clears the CSM registers including the clamp you just set, so the core runs from an unprogrammed boot address.

In the other direction: your patch 0031 (meminfo block table ioremap_wc) applies to the 12.2 line as well. The vendor feed's patch 0004 removed the cache clean there too, so the same latent hazard is in every tree built on that feed, mine included. It is carried as 0137 with your name on it. Thanks for measuring it - 33 cold boots is not a number anyone enjoys collecting.

Wi-Fi on the NSS path

Both radios - the internal 2.4 GHz IPQ5018 and the 5 GHz QCN6122 - run the firmware's Wi-Fi data path (wifili), with ath11k on the host doing management only.

What used to kill it was a single clock branch, gcc_ubi0_core_clk. The bootloader leaves it running; Linux has no consumer for it until qca-nss-drv probes, some 25 s into boot, so clk_disable_unused() gates it at ~2 s and the driver switches it back on at probe - after __nss_hal_core_reset() has already released the UBI32 core from reset. The core comes out of reset unclocked, gets its clock about a millisecond later, boots, answers, and then the first WPA2 client takes the offload down. CLK_IGNORE_UNUSED on that one branch is the whole fix: on cold boots, with a client as the test, 11 lives / 0 deaths with the branch kept and 0 / 3 with it gated.

Same family as the CMN PLL fix above - the kernel gating a clock it does not know is used by an agent outside Linux - which is what makes it easy to miss twice.

What I wrote here before, about the core trapping inside the blob on the first RX exception, was wrong, and so were the host-side hypotheses built on top of it. The firmware was never at fault. Two side notes for anyone who followed that hunt: results from warm reboots are worthless here (one arm lived 5/0 on reboot and died on its first cold boot), and the empty WIFILI section in nss_stats was a bug in my own N2H bounds check, which dropped every SOC statistics message because those are larger than the data frame size the host advertises.

The NSS core also runs at 1 GHz now instead of 850 MHz, and the rcg didn't update its configuration warnings are gone: the driver was setting the rate before enabling the clock, and configuring the AXI buses before the core.

Numbers

Test setup: a gigabit host on a LAN port, an upstream router on WAN, NAT and routing on the B3000, iperf3 between the two.

  • Sustained routed flow, 28 s steady: ~66k packets/s through the firmware, CPU 93-96 % idle across both cores, zero ECM rule churn during the transfer.
  • The opposite direction reaches ~100k packets/s at the same CPU cost.
  • The lowest idle figure seen under any load was 68 %. The offload is never the bottleneck.

Two honest caveats, because the headline number would mislead without them.

My test host tops out at 569 Mbit/s transmitting - measured against the upstream router directly, with the B3000 out of the path (569 up / 925 down, zero retransmits both ways) - so I never loaded the box to its limit in that direction.

And this board has a single 1 G CPU port, so a routed flow crosses it twice and data shares a wire direction with the opposing ACK stream. That caps routed TCP at

2 x 1448 B payload / (2 x 1542 + 94) B on the wire = 91.1 %  ->  ~911 Mbit/s


while the upstream router can send 925. Push a line-rate stream through it and TCP collapses into an 8-seconds-on / 8-seconds-off sawtooth with ~9200 retransmits, averaging 724 Mbit/s - CPU still 95 % idle, zero drops on any counter I can read, because the loss happens inside the switch. UDP one-way at 900 Mbit/s gets through with 0.45 % loss.

So ~910 Mbit/s is a ceiling of this board's design, not of the offload, and it is exactly why the AX3000T port moves WAN to a second CPU port. I could not measure the real ceiling cleanly with the hardware on hand, and would rather say that than quote a nicer number.

With Wi-Fi on the NSS path - board as a router, one laptop on 5 GHz, iperf3 to a server on the WAN side: 734/447 Mbit/s single stream, 791/534 with three streams, CPU ~90 % idle. Single runs, so read them as magnitudes rather than a benchmark.

Disclosure

This was done with heavy use of Claude Code - the bring-up debugging, the blob disassembly, the patches and this post. The measurements are real and were taken on hardware; the reasoning about what they mean had a machine in the loop, and I would rather say so than have someone work it out from the writing style.

Credits

  • @JuliusBairaktaris - the edma-nss tree everything here is layered on, and the glue-over-stmmac idea.
  • qosmio - the NSS packages and firmware repository this whole SoC family still stands on.
  • @ADCDS - patch 0029 (core-boot reset ordering) and 0031 (meminfo cacheability); the second is a real bug in every tree using that feed.
  • @georgem83 - the qualcommax/ipq50xx target itself.

Questions welcome, patches more so - particularly from anyone with another IPQ5018 board who wants to try the switch half.

Oh wow ! Great job with this on new networking stack !

I wonder If rmnet_nss would work for wwan modems... I'm fighting with it currently on qosmio's tree.

Hey, i have a question regarding qca8337-nss. Is it specific to your boards, because i tried it on my router and it doesn't work. I'm still on Qosmio's tree so any help would be apprieciated.

quite a few Linksys MR5500's around, I have one of these with IPQ5018 - and I still have the serial wired up. @georgem83 has been working on this chipset and has done a fantastic job so far, I would be interested in seeing developments.

I am not a dev, but i do have a different board if this takes off with development.

from DDWRT's page: switch QCA8337-AL3C, radios IPQ5018 / QCN9024

IMO try to make PPE work

Wow, good stuff! :heart_eyes:

I wonder if my Linksys MX2000 could also get accelerated NSS with this.

Will this work eventually be landing on mainline OpenWrt?

@LS3434 Two answers.

rmnet_nss: honestly, no idea - I never touched the rmnet path. The firmware line is the same, so nothing rules it out, but I have no modem on PCIe to try. Your earlier counters (eth_rx_rx_pkts matching rmnet_rx) at least showed the core sees that traffic.

qca8337-nss on your board: expected, and fixable. It is not a probe-based driver - there is no compatible string and nothing binds automatically. It reaches the switch over raw paged MDIO through named MDIO devices, and the defaults are hardcoded for the GL-B3000:

bus_via=90000.mdio-1:01
wake_phys=90000.mdio-1:00,90000.mdio-1:01,90000.mdio-1:02
vlans=1:0t,2u,3u;2:0t,1u

That last one is the B3000 wiring: port 0 = CPU trunk (tagged), ports 2+3 = lan on VLAN 1, port 1 = wan on VLAN 2. Your Exigo D50 almost certainly wires the ports differently, so with the defaults the module programs a VTU that matches nothing.

The recipe on your tree (the module itself is tree-agnostic - it only needs an mii_bus, so qosmio vs stmmac makes no difference here):

  1. Let qca8k finish the boot-time bring-up (SerDes, clocks), then unbind it: echo <dev> > /sys/bus/mdio_bus/drivers/qca8k/unbind. The module does not undo tag_qca by itself if the DSA driver still owns the switch.
  2. ls /sys/bus/mdio_bus/devices/ - find your device names. Any device on the switch's bus works for bus_via; wake_phys is the list of front-panel PHYs (qca8k's teardown leaves them in BMCR power-down).
  3. Work out which switch port is your CPU port and which front port is which, then write your own vlans= map.
  4. Load with insmod, not modprobe - kmodloader's modprobe drops module parameters: insmod qca8337-nss.ko bus_via=... wake_phys=... vlans=...

After that the CPU port carries plain 802.1q, the Atheros header is off on every port, and the eth_rx_unknown_l3_protocol counter should stop moving. The ECM side still needs the VLAN NSS interfaces (type 17) on top of the trunk - on qosmio's tree qca-nss-vlan gives you that.

If it still fails, post: dmesg | grep 8337, the mdio device listing, and which physical port is the CPU port on the D50.

@JuliusBairaktaris Can you unpack that one a little? As far as I can tell the IPQ5018 has no PPE block - that is ipq807x/ipq60xx silicon. It is also how George's tree splits the conversion: ipq60xx/ipq807x move to the qca_ppe stack, ipq50xx moves to plain DWMAC, because on Maple there is nothing else there. The UBI32 core is the only offload engine this SoC has, which is why this thread exists at all.

And if you ever have a spare minute, the two wifili questions from the OP still stand - especially whether the timing of PEER_UPDATE_AUTH_FLAG matters. You know that firmware better than anyone here.

@pepermint The MR5500 is the same recipe as the B3000: IPQ5018 + QCA8337, so the switch half of the branch applies as-is - only the DTS, the MDIO device names and the port-to-VLAN map differ. The QCN9024 radio is a PCIe ath11k device, which the NSS side does not touch (Wi-Fi runs on the host path). A board with serial already wired is exactly what a port needs; once you have a DTS that boots (remember the CMN PLL fix - without it 6.18 dies at the clock probe, it is in the branch), the rest is configuration. Happy to help in this thread when you get there.

@elipp If the MX2000 is the usual IPQ5018 + QCA8337 combination the same applies - check which switch your revision has.

On mainline OpenWrt: not in this form. The acceleration depends on a proprietary firmware blob plus a stack of out-of-tree kernel patches, and neither is upstreamable - this lives in forks like Julius's tree, same as every NSS build before it. Individual pieces do go up when they qualify: the CMN PLL fix is on the clk list right now, and the stmmac claim API is written so it could be proposed independently. The blob itself never will be.

This is my board DTS:

&switch {
	status = "okay";
	switch_mac_mode = <MAC_MODE_SGMII_CHANNEL0>;

	qcom,port_phyinfo {
		// MAC0 -> GE Phy
		port@0 {
			port_id = <1>;
			mdiobus = <&mdio0>;
			phy_address = <7>;
		};

		// MAC1 ---SGMII---> QCA8337 SerDes
		port@1 {
			port_id = <2>;
			forced-speed = <1000>;
			forced-duplex = <1>;
		};
	};
};

&dp1 {
	status = "okay";
	nvmem-cells = <&macaddr_dp1 0>;
	nvmem-cell-names = "mac-address";
	label = "wan";
};

// MAC1 ---SGMII---> QCA8337 SerDes
&dp2 {
	status = "okay";
	nvmem-cells = <&macaddr_dp2 0>;
	nvmem-cell-names = "mac-address";

	fixed-link {
		speed = <1000>;
		full-duplex;
	};
};

&mdio0 {
	status = "okay";
};

&mdio1 {
	status = "okay";
	pinctrl-0 = <&mdio1_pins>;
	pinctrl-names = "default";
	reset-gpios = <&tlmm 39 GPIO_ACTIVE_LOW>;

	// QCA8337 Phy0 -> LAN3
	qca8337_0: ethernet-phy@0 {
		reg = <0>;
	};

	// QCA8337 Phy1 -> LAN2
	qca8337_1: ethernet-phy@1 {
		reg = <1>;
	};

	// QCA8337 Phy3 -> LAN1
	qca8337_2: ethernet-phy@2 {
		reg = <2>;
	};

	// QCA8337 switch
	switch0: ethernet-switch@17 {
		compatible = "qca,qca8337";
		reg = <17>;
		#address-cells = <1>;
		#size-cells = <0>;

		ports {
			#address-cells = <1>;
			#size-cells = <0>;

			port@0 {
				reg = <0>;
				phy-mode = "sgmii";
				ethernet = <&dp2>;
				qca,sgmii-enable-pll;

				fixed-link {
					speed = <1000>;
					full-duplex;
				};
			};

			// QCA8337 Phy0 -> LAN3
			port@1 {
				reg = <1>;
				label = "lan3";
				phy-handle = <&qca8337_0>;

                nvmem-cells = <&macaddr_dp2 0>;
				nvmem-cell-names = "mac-address";
			};

			// QCA8337 Phy1 -> LAN2
			port@2 {
				reg = <2>;
				label = "lan2";
				phy-handle = <&qca8337_1>;

                nvmem-cells = <&macaddr_dp2 0>;
				nvmem-cell-names = "mac-address";
			};

			// QCA8337 Phy3 -> LAN1
			port@3 {
				reg = <3>;
				label = "lan1";
				phy-handle = <&qca8337_2>;

                nvmem-cells = <&macaddr_dp2 0>;
				nvmem-cell-names = "mac-address";
			};
		};
	};
};

Thanks @kuncy7.

From what I can tell based on a few search results, the MX2000 has the "usual" combination of IPQ5018+QCA8337. :grin:

So trying this will require a bunch of patching and manual compiling / cutting and stuff - has someone compiled instructions for this? I'm quite familiar with the software aspect of this; hw and OpenWrt, not so much.

What's the worst that could happen when trying this out (apart from bricking)? Could the hardware get damaged from this or something? :smiley:

@LS3434 Good news: your board is the easy case - easier than mine.

The key line in your DTS is dp1 → GE PHY (mdio0, addr 7) as a dedicated WAN. Your WAN never touches the QCA8337; the switch only carries the three LANs on dp2. That removes the entire reason I program the VTU: on the B3000 all jacks - WAN included - hang off the switch, so I need VLANs to keep them apart on one trunk. You don't. The switch can stay a dumb fabric: ports on, Atheros header off, flooding on - which is exactly what the module's default switch_fixup does with vlans left empty.

Even better, your addresses match mine: PHYs at 0/1/2 on mdio1, switch CPU port = port 0. So the module defaults should just fit:

  1. unbind qca8k (after boot has finished),
  2. insmod qca8337-nss.ko - no parameters at all,
  3. rebuild your network config: the DSA netdevs (lan1-3) are gone, so put bare dp2 into br-lan; dp1 stays your wan.

The one assumption to verify first: my defaults name the bus 90000.mdio-1 (that is what 6.18 calls it on the B3000). Check ls /sys/bus/mdio_bus/devices/ on your 6.12 tree - if the names differ, pass bus_via= and wake_phys= accordingly.

And this should also dissolve your original ECM problem: with DSA gone, dp1 and dp2 are two plain nss-dp interfaces with valid NSS interface numbers, so ECM can build the dp1↔br-lan hierarchy without any of the tag_8021q patches. The eth_rx_unknown_l3_protocol counter should go quiet the moment the header is off.

@elipp Better news than you think. The MX2000 is a supported OpenWrt board, and its DTS is already in the branch - ipq5018-mx2000.dts, image target linksys_mx2000. So a standard (non-NSS) image builds from the branch today, the ordinary OpenWrt way; no porting, no cutting.

What the MX2000 does not have yet is the NSS enablement - and that delta is genuinely small. On the B3000 the whole board-side diff against a plain device is one line in the DTS (#include "ipq5018-nss.dtsi"); everything else happens at runtime. The switch half then needs the MX2000's own port map (its wiring differs from mine: wan on PHY 1, lans on PHYs 2-4), which is module parameters, not code. There is no step-by-step written up yet - a README with the build steps is on my list, and your question just moved it up.

On risk: software cannot damage this hardware - nothing in the stack touches anything that could. The realistic worst case is a soft-brick: an image that does not boot, recovered over the serial console and TFTP from the bootloader, which on these boards survives pretty much anything - and Linksys devices additionally keep a second firmware partition to fall back on. The practical entry bar for an experimental NSS image is a wired, working UART before you flash; for the standard branch build it is no riskier than any snapshot.

root@OpenWrt:~# ls /sys/bus/mdio_bus/devices/
88000.mdio-1:07  90000.mdio-1:01  90000.mdio-1:11
90000.mdio-1:00  90000.mdio-1:02  fixed-0:00

Jesus Christ, it works !!!! OMG, thank you so much :slight_smile: eth_rx_unknown_l3_protocol doesn't rise anymore !!!

Luckily @georgem83 has a 6.18 build with working USB on the Linksys MR5500 which I currently have flashed - so I assume most of the heavy lifting is already done for my specific router (correct?). I know he is pushing a PR for his hard work (correct me if I am wrong here)

With that said, Next Spring 2027 - I will be in better shape with a workbench and place to really dig into adding this skill/knowledge. Currently - without a work bench I can test builds, I can compile on my devuan linux desktop (slow old 7th gen i5 but hey it works!) - so if there is a guide and how to I will take some limited stabs. Alternatively, I can test builds made - for MR5500 and MR7350.

the following devices all contain the qca8337 switch:

  • Linksys MR5500, MX2000, MX5500, SPNMX56
  • Xiaomi AX600 and Redmi AX5400
  • GL.iNet GL-B3000
  • Zyxel SCR50AXE
  • ELECOM WRC-X3000GST2 and WRC-X3000GS2
  • CMCC PZ-L8

I don't have the time now, will look at it later, unless someone is able to come up with a recipe for this switch.

this isn't accurate, my IPQ5018 devices boot just fine on 6.18 (Linksys MR5500, MX2000, MX5500, MX6200, and SPNMX56).
It may be board specific or maybe specific to a certain HW revision of the SoC.
I know the mp02.1 is quite 'special', is your board based on that?

@pepermint @elipp The README is up, and the one thing that would have stopped both of your boards is gone with it.

What it covers: the ten commits and what each is for, the feed and the .config the validated image was built from, why the 12.2 firmware, how the nss service brings the plane up and the three orderings that fail silently, why DSA and ECM acceleration exclude each other on this SoC, the qca8337-nss parameters, and a porting checklist.

The stopper: the switch re-arm was hard-wired to the B3000 wiring - CPU port 0, front ports 1-3. Both the MX2000 and the MR5500 have the CPU port on port 6. As of today the module takes cpu_port and ports parameters instead, and the service passes whatever you put in nss.general.switch_args straight through, so neither board needs a code change any more. Worked out from the DTS files in the tree:

MX2000 (wan on port 2, lan1-3 on ports 5/4/3):

uci set nss.general.switch_args='cpu_port=6 ports=0x7c wake_phys=90000.mdio-1:01,90000.mdio-1:02,90000.mdio-1:03,90000.mdio-1:04'
uci set nss.general.vtu='1:6t,3u,4u,5u;2:6t,2u'

MR5500 (lan1-4 on ports 1-4, wan on port 5):

uci set nss.general.switch_args='cpu_port=6 ports=0x7e wake_phys=90000.mdio-1:00,90000.mdio-1:01,90000.mdio-1:02,90000.mdio-1:03,90000.mdio-1:04'
uci set nss.general.vtu='1:6t,1u,2u,3u,4u;2:6t,5u'

Honest label: these are derived from the DTS, not tested - I have neither board. Which is exactly where a tester with serial comes in.

@pepermint since you can build: the branch, the nss feed, the .config from the README, plus one line in ipq5018-mr5500.dts (#include "ipq5018-nss.dtsi" next to the other includes), then the two uci set lines above before the first reboot with NSS enabled. Serial attached, please - if the port map is wrong the symptom is a switch with dead jacks, and the console is how you see why. First things to paste back: dmesg | grep 8337 and cat /sys/kernel/debug/qca-dwmac-nss/status. And George's 6.18 MR5500 build is the right starting point - the base of this branch is the same 6.18 qualcommax, so his DTS work carries over.

@elipp same recipe with the MX2000 lines; no rush, the thread will be here when you have the bench.

@georgem83 A recipe for that switch exists - it is what this branch does, and as of a couple of hours ago it carries no board assumption any more.

The problem it solves, briefly: the NSS firmware parses 802.1Q natively but cannot parse the two-byte Atheros header that DSA's tag_qca puts where the ethertype should be, and DSA user ports never get an NSS interface number. So on IPQ5018 DSA user ports and ECM acceleration are mutually exclusive - a board with a QCA8337 either has DSA or has acceleration, not both.

The way out keeps qca8k for the hard part: let it do the boot-time bring-up (SerDes, clocks, uniphy), then unbind it and reprogram the fabric directly over paged MDIO - MACs on, Atheros header off, VLANs in the VTU with the front ports untagged in their VLAN and the CPU port carrying all of them tagged. The trunk then carries plain 802.1Q, which both the firmware (dynamic interface type 17) and the host stack parse. That is kmod-qca8337-nss, ~380 lines, the MDIO protocol lifted from qca8k-8xxx.c:

Until today it assumed my wiring (CPU port 0, front ports 1-3). It now takes cpu_port and a ports bitmask, so the Linksys boards with the CPU on port 6 need parameters rather than a patch - the README has the MX2000 and MR5500 parameter sets worked out from their DTS files (untested, I have neither board).

Given your list is eleven boards, the obvious next step is to stop asking for parameters at all: everything the module needs is already in the ethernet-switch node - the CPU port is the one with the ethernet phandle, the front ports carry label = "lan*" / "wan", and the PHYs hang off phy-handle. Reading that node after the unbind and deriving the port mask, the CPU port and a default VLAN map (CPU tagged in both, lan* untagged in 1, wan untagged in 2) would make it parameterless on every board in the tree, with the parameters left as an override.

I am happy to write that - it is a contained change and the DTS files are all there. What I cannot do is validate it beyond the B3000; that needs the people in this thread with the other boards. If you would rather it lived somewhere other than a standalone module - or took a different shape entirely before anyone builds on it - say so now and I will follow your lead.