Reference notes from working on the mxl862xx DSA driver (frank-w 7.1-main, and
dangowrt/linux for comparison) against the BananaPi-shipped MxL86252C firmware
1.0.85 on a BPI-R4 Pro. Everything below was derived from the firmware image’s
command dispatch table and handler code, then checked against driver behaviour on
hardware. Addresses are given so the findings can be reproduced; no firmware
contents are reproduced here. Part is also AI inferred. I have no NDA so this is just based on work on the firmware blob released.
Hope it helps get people started.
1. BRIDGEPORT_CONFIGSET (0x0402) is a read-modify-write with a blind spot
This is the one that cost the most time.
How the handler works. It reads the bridge port’s two hardware table entries
(ingress 20, egress 21), applies only the field groups selected by the 32-bit
mask, and writes both entries back unconditionally. So unselected fields are
preserved — from whatever the read returned.
The blind spot. For bridge ports the firmware allocated itself
(BRIDGEPORT_ALLOC, i.e. anything not backed by a physical port: tag_8021q virtual
bridge ports, LAG bridge ports), the read-back returns an empty port map.
BRIDGEPORT_CONFIGGET shows this directly — nBridgePortMap[] reads as all-zero
for such ports even when forwarding through them works. CONFIGGET and CONFIGSET use
the same read path and copy the same words, so the SET’s RMW starts from that same
empty map.
Consequence for drivers. Any BRIDGEPORT_CONFIGSET on a firmware-allocated
bridge port that does not include MASK_BRIDGE_PORT_MAP (bit 8) writes the map
back empty. The port then has no egress members and CPU-originated traffic
reassigned onto it goes nowhere. Physical bridge ports are unaffected — their
read-back is accurate.
How it hides. In frank-w’s tag_8021q mode the VBP map is cleared at boot by an
EGRESS_VLAN-only write that runs after the map was set, and then restored by a
later full write triggered when the port joins a bridge (br_add_if sets
promiscuity → port_set_host_flood → VBP rewrite). A port that is bridged at boot
works; a port that is never bridged doesn’t. Symptom: “only works once it’s in a
bridge”.
Rule of thumb. Treat a firmware-allocated bridge port as write-only: have one function own the whole request and select every field the driver ever sets, every time.
Handler: thunk 0x400a065c → 0x40065728; table read 0x400690e8; write-back
0x40068ed0; GET 0x400651b8; ALLOC 0x4006499c.
2. There is a firmware-side port map manager
The image contains a Zephyr subsystem, zephyr/subsys/net/l2/bp/bp_update.c
(__bp_update, __bp_link_up, __bp_link_down, log strings like
"BP%u: link up - entering portmap (fid=%u)").
- It keeps an 11-byte record per bridge port (allocated, link-up, FID, logical port) and a per-FID active bitmap.
BRIDGEPORT_CONFIGSETruns it as a post-step wheneverMASK_BRIDGE_IDis selected and the main handler succeeded. It then rewrites this bridge port’s map to “all bridge ports in the new FID, if this port’s link is up, else empty”, and adds/removes this port’s bit in every peer’s map — using its own CONFIGSET calls.- It is gated on a configuration bit (bit 17 of a RAM word at
0x20f369e0).
On the BPI-R4 Pro with 1.0.85 this gate appears to be off: the manager ignores whatever map the driver sent, and driver-supplied map contents demonstrably matter on this board. That is an inference from behaviour, not a register read.
Entry: 0x4001f69c(bpid, bridge_id) → 0x4001ef6c; record base 0x20f11a45.
3. Command-set deltas vs. the driver headers
Dispatch table: 234 entries of {u32 cmd, u32 handler, u32 payload_size} at file
offset 0x11cab8 of the switch image. Comparing every MXL862XX_API_* call site’s
struct size against the declared payload size:
- Present, used by frank-w, payload mismatch (XPCS only):
0x1a01 PCS_CONFIGfw 10 / driver 6;0x1a02 PCS_GET_STATEfw 12 / driver 8. Everything else matched exactly (35/35). - Absent from 1.0.85, used by frank-w:
0x1a03 XPCS_PCS_ENABLE,0x1a06 XPCS_AN_DISABLE. These are the source of the-134(ZephyrENOTSUP) errors — the command is gone, not refused. dangowrt’s XPCS v2 structs (10/12/4/4/6) match the firmware byte-for-byte. - Present, unknown to frank-w:
0x1908 SYS_MISC_REG_RD(8 B),0x0106 PORTLINKCFGSET(30 B),0x0108 PORTCFGSET(37 B),0x020a PCERULELOGICWRITE(466 B, same struct as0x0202),0x1a0e(10 B, §5). - Present, used by frank-w, dropped by dangowrt:
0x1a0a PRBS_CFG,0x1a0b BERT_CFG,0x1a0c EQ_GET,0x1a0d SIGNAL_DETECT— the serdes self-test and ethtool serdes stats. A wholesale sync to dangowrt loses them. 0x1a07(6 B): frank-w calls itFORCE_SPEED, dangowrtPCS_LINK_UP. The handler takes a packed mode word plus a speed in Mbit/s, acts for SGMII/USXGMII/QSGMII, and short-circuits when in-band AN is on — it is a per-link-upPCS_LINK_UP.
4. SYS_MISC_REG_RD (0x1908)
{ __le32 addr; __le32 val; }. Reads one 32-bit register. The address must be in
[0xC0938000, 0xD0FBFFFF] (the switch register window) or the firmware returns
-EINVAL (-22). No side effects, so it is safe to probe for rather than version-gate.
dangowrt uses it to read the CHIP ID registers at 0xC0D28884/0xC0D28888
(part number from L[15:12] | M[11:0] << 4, revision from M[14:12]). Its sibling
0x190a (12 B) is a masked write against a five-entry address whitelist.
Handler 0x40060ce0.
5. 0x1a0e identified: XPCS per-channel AN/link status
10-byte payload, not in either driver. Inputs: byte 0 XPCS index (<2), byte 1
channel index (< channel count). Outputs: byte 2 AN complete, byte 3 link, u16 at 4
speed in Mbit/s, byte 6 duplex, bytes 7–8 two further AN-result bits. Channel count
is derived from the PCS mode (USXGMII 1/2/4-channel via 0xe001c[12:10] → table
{1,1,1,2,2,4}; QSGMII → 4). It’s the per-channel form of what 0x1a02 already
returns via its sub-port field; nothing in the driver needs it.
Handler 0x40053c9c.
6. The image is two firmwares
The BPI .bin is a container with two MCUboot images: the switch firmware
(1.0.85, ~1.27 MB, Zephyr on ARC) at file 0x00a7f4, and the internal GPHY
firmware (build 105 = 0x69) at 0x1ba7f4. devlink dev flash updates both at
once. If you see GPHY-side behaviour change across a “switch firmware” update,
that’s why.
7. Reproducing any of this
- Load mapping for the switch image:
file_offset = vaddr - 0x40000000 + 0xa014. - Toolchain:
binutils-arc-linux-gnu(Debian/Ubuntu package). Wrap the image with an assembler stub that.incbins it, thenarc-linux-gnu-objdump -d -M cpu=hs38 --adjust-vma=0x3fff5fec. - Dispatch table at
0x11cab8; walk it with a 12-bytestruct.unpack('<III'). - Log strings resolve through the same mapping and are the fastest way to find a
subsystem (
bp_update.c,mxl_bp.h, etc.).
Corrections welcome.