From 5329193987a76dad21a998cefb0a6af135c0613b Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 02:29:17 +0200 Subject: [PATCH 01/68] port: add a helper to steer a link-local group via a static L2 entry port_l2mc_set() writes a static L2 multicast entry for a reserved group 01:80:C2:00:00: in a given VLAN with a given member portmask. Slow-protocol frames must reach the management CPU without being flooded to other ports, but the RMA "trap" action cannot deliver to the internal NIC on this hardware - its destination is an external CPU attached to a physical port. The working alternative is to keep the RMA action at "forward" and constrain the egress with a static entry: the forward lookup then hits the entry's member mask instead of the VLAN flood mask. Hardware-verified on a SWTGW218AS in both directions: a mask without the CPU bit stops delivery to the CPU, a CPU-only mask delivers with no port egress. Lookups are IVL, so callers add one entry per VID they care about; rewriting the same MAC+VID replaces the entry in place. Used by the BPDU containment in the next commit; the pending LACP branch adopts it for 01:80:C2:00:00:02 the same way. --- rtl837x_port.c | 42 ++++++++++++++++++++++++++++++++++++++++++ rtl837x_port.h | 1 + 2 files changed, 43 insertions(+) diff --git a/rtl837x_port.c b/rtl837x_port.c index a85e90d..9e7ff00 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -402,6 +402,48 @@ void port_l2_learned(void) __banked } +/* + * Static L2 multicast entry for the link-local group 01:80:C2:00:00: + * in VLAN `vid`, with member portmask `pmask` (bit 9 = CPU port). + * + * Slow-protocol frames (LACP, STP BPDUs) must reach the CPU without being + * flooded to other ports. The RMA "trap" action cannot deliver to the + * internal NIC on this hardware (its destination is an external CPU on a + * physical port), so the protocol modules keep the RMA action at "forward" + * and constrain the egress with this entry instead: the lookup hits the + * entry's portmask rather than the VLAN flood mask (hardware-verified with + * both the CPU bit cleared - delivery stops - and CPU-only - no egress). + * + * SMI layout (vendor SDK, L2-multicast entry variant): + * DATA_IN_A = MAC bytes 5..2 -> c2 00 00 + * DATA_IN_B = MAC[1..0] | vid<<16 | IVL<<29 | pmask[1:0]<<30 + * DATA_IN_C = pmask[9:2] + * Lookups are IVL (a VID-0 entry is not matched), so callers add one entry + * per PVID in use. The write command (table 4 = the whole L2 LUT) hashes + * MAC+VID and picks the bucket slot itself; TBL_EXECUTE self-clears. + * Overwriting the same MAC+VID replaces the entry, so a caller can retarget + * the mask at will (e.g. back to all ports to restore flooding). + */ +__xdata uint8_t l2mc_guard; /* xdata: the internal-RAM overlay (OSEG) is full */ + +void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked +{ + l2mc_guard = 0; + do { /* wait out any previous table op (bounded, cf. the IGMP guards) */ + reg_read_m(RTL837X_TBL_CTRL); + } while ((sfr_data[3] & TBL_EXECUTE) && ++l2mc_guard); + + REG_WRITE(RTL837x_TBL_DATA_IN_A, 0xc2, 0x00, 0x00, mac_last); + REG_WRITE(RTL837x_TBL_DATA_IN_B, 0x20 | (vid >> 8) | ((pmask & 0x3) << 6), vid, 0x01, 0x80); + REG_WRITE(RTL837x_TBL_DATA_IN_C, 0, 0, 0, pmask >> 2); + REG_WRITE(RTL837X_TBL_CTRL, 0, 0, TBL_L2_UNICAST, TBL_WRITE | TBL_EXECUTE); + l2mc_guard = 0; + do { + reg_read_m(RTL837X_TBL_CTRL); + } while ((sfr_data[3] & TBL_EXECUTE) && ++l2mc_guard); +} + + /* * Basic L2 configuration such as time to forget an entry */ diff --git a/rtl837x_port.h b/rtl837x_port.h index b459832..072173a 100644 --- a/rtl837x_port.h +++ b/rtl837x_port.h @@ -54,6 +54,7 @@ void vlan_name_remove(uint16_t vlan) __banked; void vlan_setup(void) __banked; void port_pvid_set(uint8_t port, __xdata uint16_t pvid) __banked; uint16_t port_pvid_get(uint8_t port) __banked; +void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked; void vlan_create(void) __banked; void vlan_delete(uint16_t vlan) __banked; void vlan_dump(void) __banked; From 6e1f199571b3975bd06918a141af33209f784f50 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 02:14:24 +0200 Subject: [PATCH 02/68] stp: contain BPDUs to the CPU while STP runs With STP enabled the switch is a participating bridge, so BPDUs must be consumed, not relayed - yet the reserved group 01:80:C2:00:00:00 was flooded across the VLAN just like any multicast, leaking every BPDU to all ports (the same defect class as the LACPDU flood addressed in the LACP branch, PR #299). On stp on, write a CPU-only static L2 multicast entry for the BPDU group per VLAN: BPDUs can arrive VLAN-tagged and classify into the tag's VID, so cover every VLAN present in the VLAN table plus every port's PVID for the untagged case. On stp off the same entries are retargeted to all ports + CPU, restoring the previous flood behaviour: an unmanaged switch is expected to be transparent to BPDUs so the surrounding spanning tree can span through it, and dropping them instead would partition that topology. Note: with STP enabled the ports start out blocking, which also stops egress of CPU-originated LACPDUs, so an active LACP aggregate drops until the ports reach forwarding - a pre-existing interaction, not changed here. --- rtl837x_stp.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 2eb4f92..e90227a 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -11,11 +11,18 @@ #include "rtl837x_sfr.h" #include "rtl837x_regs.h" #include "rtl837x_stp.h" +#include "rtl837x_port.h" /* port_pvid_get(), port_l2mc_set() */ #include "uip.h" #include "machine.h" extern __code struct machine machine; extern __xdata uint8_t sfr_data[4]; +extern __xdata struct machine_runtime machine_detected; /* owned by rtl837x_port.c */ + +/* Scratch for stp_fdb_update(), in xdata: plain locals would overflow the + * near-full internal-RAM overlay (OSEG), cf. rtl837x_lacp.c. */ +__xdata uint16_t stp_fdb_vid; +__xdata uint8_t stp_fdb_i; extern __xdata struct uip_eth_addr uip_ethaddr; @@ -202,6 +209,39 @@ void stp_timers(void) __banked } +/* + * Steer BPDUs (01:80:C2:00:00:00) while STP runs: one static CPU-only L2 + * multicast entry per PVID in use, so BPDUs reach the CPU without being + * flooded to other ports (a bridge running STP must consume BPDUs, not + * relay them - relaying poisons the neighbours' view of the topology). + * + * With STP off the same entries are retargeted to all ports + CPU, which + * restores the previous flood behaviour ("BPDU transparency"): the + * surrounding spanning tree can keep spanning *through* this switch, which + * unmanaged setups rely on. Same per-PVID/IVL rules as the LACP steering - + * see port_l2mc_set() and rtl837x_lacp.c. NOTE: changing a port's PVID + * while STP runs needs `stp off`/`on` to refresh the entries. + */ +static void stp_fdb_update(__xdata uint16_t pmask) +{ + /* Unlike LACPDUs (always untagged, so per-PVID entries suffice), BPDUs + * can arrive VLAN-tagged and then classify into the tag's VID - cover + * every VLAN that exists in the VLAN table, plus every port's PVID for + * the untagged case. A duplicate VID just overwrites the same slot. */ + for (stp_fdb_vid = 1; stp_fdb_vid < 4095; stp_fdb_vid++) { + if (vlan_get(stp_fdb_vid) < 0) + continue; + if (!(sfr_data[0] & 0x02)) /* bit 1: VLAN table entry valid */ + continue; + port_l2mc_set(0x00, stp_fdb_vid, pmask); + } + for (stp_fdb_i = machine.min_port; stp_fdb_i <= machine.max_port; stp_fdb_i++) { + stp_fdb_vid = port_pvid_get(stp_fdb_i); + port_l2mc_set(0x00, stp_fdb_vid, pmask); + } +} + + void stp_setup(void) __banked { print_string("Enabling STP: "); @@ -222,6 +262,9 @@ void stp_setup(void) __banked root_bridge.prio = 0x80; // This corresponds to 32768 root_bridge.ext = 0x00; memcpy(root_bridge.mac, uip_ethaddr.addr, 6); + + /* Take BPDUs to the CPU only - we are a participating bridge now. */ + stp_fdb_update(PMASK_CPU); } @@ -236,4 +279,7 @@ void stp_off(void) __banked } sfr_data[1] |= 0x0f; // Do not block CPU-Port reg_write_m(RTL837X_MSTP_STATES); + + /* Restore BPDU transparency: flood them again like an unmanaged switch. */ + stp_fdb_update(PMASK_CPU | (machine_detected.isRTL8373 ? PMASK_9 : PMASK_6)); } From b16c7d97233a9e5bd41a88bae59d39a3565d2e45 Mon Sep 17 00:00:00 2001 From: d00f Date: Sat, 25 Jul 2026 12:03:05 +0200 Subject: [PATCH 03/68] common: define the RTL frame-tag flag bits shared by STP and LACP The rtl_tag `flags` word (LEARN_DIS, KEEP) and the `pmask` ALLOW-bit semantics are properties of the RTL8_4 CPU tag, not of any one protocol: STP injects BPDUs with LEARN_DIS set and LACP emits slow-protocol frames the same way. Define them once in the shared header, with the HTONS byte-order caveat documented, so every feature that hand-builds a CPU tag frame uses the same constants. (cherry picked from commit 7f905b3e90f9bc5df586a5138723d97edf3d6aaf) --- rtl837x_common.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rtl837x_common.h b/rtl837x_common.h index 66df88a..8cd556e 100644 --- a/rtl837x_common.h +++ b/rtl837x_common.h @@ -71,6 +71,20 @@ struct vlan_tag { #define VLAN_TAG_SIZE (sizeof (struct vlan_tag)) #define RTL_FRAME_TAG_ID 0x8899 #define RTL_FRAME_TAG_VERSION 0x04 +/* Bits of the tag's `flags` word (word2), per Linux DSA tag_rtl8_4: + * bit15 EFID_EN | 14:12 EFID | 11 PRI_EN | 10:8 PRI | + * bit7 KEEP | 6 VSEL | 5 LEARN_DIS | 4:0 VIDX + * NOTE: this word must be written through HTONS like every other tag field - + * writing the constant raw puts the bits in the wrong byte (0x0020 raw lands on + * the wire as 0x2000 = EFID, not LEARN_DIS), the ASIC then fails to parse the + * tag and forwards the frame with the 0x8899 header still on it. */ +#define RTL_TAG_LEARN_DIS 0x0020 /* do not learn the CPU's SA on the egress port */ +#define RTL_TAG_KEEP 0x0080 /* keep the frame's 802.1Q tag format as injected */ +/* The `pmask` word (word3): bit15 ALLOW selects how 14:0 is interpreted. + * ALLOW=0 -> forwarding port mask (directed egress: frame goes exactly to the + * ports set). ALLOW=1 -> allowance mask (permission filter on a normal lookup), + * which for a one-hot mask yields an empty egress set - the frame disappears. + * Directed egress therefore requires ALLOW cleared, as mainline does. */ // For TX, an 8 byte (plus 4 byte padding when when VLAN is enabled) // header describing the frame to be moved to the Asic is used From 032305ad3a69d78177be983293e49fa73378eedf Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 03:18:28 +0200 Subject: [PATCH 04/68] stp: move the STP module to code bank 2 The always-mapped common area is nearly full (349 bytes free before this change), and the STP state machine that follows does not fit there. Move the module to BANK2 next to the other protocol code; its public entry points are already __banked, and cmpMAC/stp_cnf_send have no callers outside the file. --- rtl837x_stp.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index e90227a..6ba1847 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -6,6 +6,12 @@ // #define REGDBG // #define DEBUG +/* Place this module's code and constants in code bank 2 (cf. rtl837x_igmp.c): + * the always-mapped common area is nearly full, and the full state machine + * does not fit there. */ +#pragma codeseg BANK2 +#pragma constseg BANK2 + #include #include "rtl837x_common.h" #include "rtl837x_sfr.h" From 0df699659a4497901236a26ba77cbc3f3937b543 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 21 Jul 2026 07:52:34 +0200 Subject: [PATCH 05/68] stp: actually promote ports out of blocking; calibrate timers "stp on" put every port into blocking (stp_setup, port_timers = "10 s") but nothing ever counted those timers down: stp_timers() only sent hello BPDUs. On a network with no other (R)STP bridge - i.e. nobody sends us BPDUs - every port therefore stayed blocking FOREVER and enabling STP took the whole network down until "stp off". - stp_timers(): count port_timers down; when a port's listen period expires with no better root heard, promote it to forwarding in MSTP_STATES (we are the designated bridge on that port). - Calibrate the tick constants to the real stp_timers() rate (~64 Hz: main loop ~256 Hz / (STP_TICK_DIVIDER+1)): TIME_HELLO 0x200->0x80 is an actual 2 s hello, port_timers 0xa00->0x280 an actual 10 s listen period. Measured before the fix, ports converged only after ~40 s. - Move struct bridge into rtl837x_stp.h and export root_bridge/-_cost for the web UI status endpoint. Verified on hardware: "stp on" -> ports report Blocking, after the 10 s listen period all ports promote to Forwarding and LAN connectivity returns; "stp off" restores forwarding immediately. We elect ourselves root (weRoot) with no other bridge present. (cherry picked from commit 8537a15ca254b2122272b20bec7a66426e86df4b) --- rtl837x_stp.c | 25 ++++++++++++++++++------- rtl837x_stp.h | 16 +++++++++++++++- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 6ba1847..3266041 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -34,12 +34,7 @@ extern __xdata struct uip_eth_addr uip_ethaddr; extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; -struct bridge { - uint8_t prio; - uint8_t ext; - uint8_t mac[6]; -}; - +/* struct bridge now lives in rtl837x_stp.h (shared with the web UI). */ __xdata struct bridge root_bridge; __xdata uint32_t root_bridge_cost; @@ -211,6 +206,22 @@ void stp_timers(void) __banked print_byte(i); write_char('\n'); stp_cnf_send(i); } + /* Promote a port out of the initial blocking state once its listen + * period expires. stp_setup() puts every port into blocking with + * port_timers = 10 s, but nothing ever counted that down - so on a + * network with no other RSTP bridge (nobody sends us BPDUs) every + * port stayed blocking FOREVER and "stp on" killed the whole + * network. If no better root was heard during the listen period we + * are the designated bridge on that port: go to forwarding. */ + if (port_timers[i]) { + if (!--port_timers[i]) { + reg_read_m(RTL837X_MSTP_STATES); + sfr_data[3 - (i >> 2)] |= (uint8_t)(0b11 << ((i << 1) & 0x7)); + reg_write_m(RTL837X_MSTP_STATES); + print_string("STP: port forwarding "); + print_byte(i); write_char('\n'); + } + } } } @@ -258,7 +269,7 @@ void stp_setup(void) __banked uint8_t bit_mask = 0b01 << ( (i << 1) & 0x7); sfr_data[3 - (i >> 2)] |= bit_mask; port_hello[i] = TIME_HELLO; - port_timers[i] = 0xa00; // 10 sec in blocking state + port_timers[i] = 0x280; // 10 s in blocking state (at the ~64 Hz stp_timers rate) } sfr_data[1] |= 0x0f; // Do not block CPU-Port reg_write_m(RTL837X_MSTP_STATES); // R5310-000d555f diff --git a/rtl837x_stp.h b/rtl837x_stp.h index b1c2a71..4aaf959 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -7,6 +7,20 @@ void stp_setup(void) __banked; void stp_timers(void) __banked; void stp_off(void) __banked; -#define TIME_HELLO 0x200 // 2 sec +#define TIME_HELLO 0x80 // 2 sec (stp_timers runs at ~64 Hz: main loop ~256 Hz / STP_TICK_DIVIDER+1) + +/* Bridge identifier as carried in a BPDU (priority, extension, MAC). */ +struct bridge { + uint8_t prio; + uint8_t ext; + uint8_t mac[6]; +}; + +/* Protocol state, exposed read-only for the web UI (page_impl.c send_stp()) + * - the elected root bridge and our path cost to it. Owned by rtl837x_stp.c; + * stpEnabled is owned by rtlplayground.c. */ +extern __xdata uint8_t stpEnabled; +extern __xdata struct bridge root_bridge; +extern __xdata uint32_t root_bridge_cost; #endif From 5c881da4feeba88da963e57850ed8353b925ba56 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 03:20:07 +0200 Subject: [PATCH 06/68] stp: stop forcing the SFP port to forwarding in the CPU-port mask The "do not block the CPU port" mask 0x0f covers bits 3:0 of MSTP_STATES byte 1, which is ports 8 AND 9 - so stp_setup unconditionally forced port 8 (a real front port, the SFP uplink on SWTGW218AS) into forwarding and it could never be blocked. The CPU port alone is bits 3:2 = 0x0c. --- rtl837x_stp.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 3266041..8d2e3fd 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -271,8 +271,8 @@ void stp_setup(void) __banked port_hello[i] = TIME_HELLO; port_timers[i] = 0x280; // 10 s in blocking state (at the ~64 Hz stp_timers rate) } - sfr_data[1] |= 0x0f; // Do not block CPU-Port - reg_write_m(RTL837X_MSTP_STATES); // R5310-000d555f + sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9) + reg_write_m(RTL837X_MSTP_STATES); print_reg(RTL837X_MSTP_STATES); write_char('\n'); @@ -294,7 +294,7 @@ void stp_off(void) __banked uint8_t bit_mask = 0b11 << ( (i << 1) & 0x7); sfr_data[3 - (i >> 2)] |= bit_mask; } - sfr_data[1] |= 0x0f; // Do not block CPU-Port + sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9) reg_write_m(RTL837X_MSTP_STATES); /* Restore BPDU transparency: flood them again like an unmanaged switch. */ From b5387016fa7a68b517ce4abc83001d690cb04c3d Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 03:21:43 +0200 Subject: [PATCH 07/68] gui: Spanning Tree page (config + live status) Add a Spanning Tree page: an on/off toggle driving the existing "stp" command over /cmd, and a live status section fed by a new /stp.json endpoint - the elected root bridge (priority + MAC), our path cost, whether we are the root, and the per-port STP state read live from the ASIC's MSTP register (same 2-bit encoding stp_setup() writes). Ports are reported by their physical numbers. Recovered-from: 3132319, 9365c86 --- html/navigation.js | 1 + html/stp.html | 21 ++++++++++++++++ html/stp.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++ httpd/httpd.c | 2 ++ httpd/page_impl.c | 51 ++++++++++++++++++++++++++++++++++++++ httpd/page_impl.h | 1 + 6 files changed, 137 insertions(+) create mode 100644 html/stp.html create mode 100644 html/stp.js diff --git a/html/navigation.js b/html/navigation.js index b42f8fd..d9eeed0 100644 --- a/html/navigation.js +++ b/html/navigation.js @@ -4,6 +4,7 @@ document.getElementById('sidebar').innerHTML = + "
  • Port Statistics
  • " + "
  • VLAN
  • " + "
  • L2 Configuration
  • " + + "
  • Spanning Tree
  • " + "
  • Mirroring
  • " + "
  • Link Aggregation
  • " + "
  • EEE
  • " diff --git a/html/stp.html b/html/stp.html new file mode 100644 index 0000000..af968f5 --- /dev/null +++ b/html/stp.html @@ -0,0 +1,21 @@ + + + + + + FreeSwitchOS Spanning Tree + + + +
    +
    +

    Spanning Tree (RSTP)

    +

    Mode: +

    +
    +
    + +
    + + + diff --git a/html/stp.js b/html/stp.js new file mode 100644 index 0000000..3682207 --- /dev/null +++ b/html/stp.js @@ -0,0 +1,61 @@ +/* ---- Spanning Tree (RSTP) section ---- */ + +// STP port states as encoded in the ASIC's MSTP register (2 bits per port) +const STP_STATES = ["Disabled", "Blocking", "Learning", "Forwarding"]; + +// "user is editing" flag: while set, the periodic refresh must not overwrite +// the mode dropdown (same pattern as the LAG page - without it the 2 s refresh +// silently reverts the user's choice before Apply). +var stpDirty = false; + +function fetchStp() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + const s = JSON.parse(xhttp.responseText); + // textContent throughout: rootMac comes from received BPDUs + // (remote-controlled), never render it as HTML + if (!stpDirty) + document.getElementById("stpMode").value = s.on ? "on" : "off"; + document.getElementById("stpStat").textContent = s.on + ? (s.weRoot + ? "This switch is the root bridge (priority 0x" + s.rootPrio + ")" + : "Root bridge: 0x" + s.rootPrio + " / " + s.rootMac + + " \u2014 path cost: 0x" + s.cost) + : ""; + let t = ""; + if (s.on) { + t = "port state\n"; + for (const p of s.ports) + t += String(p.p).padEnd(6) + STP_STATES[p.st] + "\n"; + } + document.getElementById("stpPorts").textContent = t; + } + }; + xhttp.open("GET", `/stp.json`, true); + sendXHTTP(xhttp); +} + +async function stpSub() { + const on = document.getElementById("stpMode").value === "on"; + try { + await fetch('/cmd', { method: 'POST', body: on ? "stp on" : "stp off" }); + } catch(err) { + console.error(`Error: ${err}`); + } + stpDirty = false; // editing done - let the refresh show the truth + fetchStp(); +} + +window.addEventListener("load", function() { + document.getElementById("stpMode") + .addEventListener("change", () => { stpDirty = true; }); +}); + +window.addEventListener("load", function() { + update( () => { + fetchStp(); + const interval = setInterval(update, 2000); + const stpInt = setInterval(fetchStp, 2000); + }); +}); diff --git a/httpd/httpd.c b/httpd/httpd.c index d856ccd..46db120 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -690,6 +690,8 @@ void httpd_appcall(void) send_mtu(); } else if (is_word(q, "/lag.json")) { send_lag(); + } else if (is_word(q, "/stp.json")) { + send_stp(); } else if (is_word(q, "/vlanlist")) { send_vlanlist(); } else if (is_word(q, "/config")) { diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 4d3b90f..794b01a 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -12,6 +12,7 @@ #include "phy.h" #include "version.h" #include "machine.h" +#include "rtl837x_stp.h" #include "page_impl.h" #include "syslog.h" @@ -535,6 +536,56 @@ void send_lag(void) } +/* STP status for the L2 page ("/stp.json"): enable state, the elected root + * bridge (priority+MAC) and our path cost to it, whether we are the root, and + * the live per-port STP state read from the ASIC's MSTP register (2 bits per + * port: 0 disable, 1 blocking, 2 learning, 3 forwarding - same encoding + * stp_setup() writes). Ports are reported by their physical number. */ +/* Scratch for send_stp(): a plain local would land in the near-full 8051 + * internal-RAM overlay (OSEG). */ +__xdata uint8_t stp_we_root; + +void send_stp(void) +{ + dbg_string("send_stp called\n"); + slen = strtox(outbuf, HTTP_RESPONCE_JSON); + + slen += strtox(outbuf + slen, "{\"on\":"); + bool_to_html(stpEnabled); + slen += strtox(outbuf + slen, ",\"rootPrio\":\""); + byte_to_html(root_bridge.prio); + byte_to_html(root_bridge.ext); + slen += strtox(outbuf + slen, "\",\"rootMac\":\""); + for (uint8_t j = 0; j < 6; j++) + byte_to_html(root_bridge.mac[j]); + slen += strtox(outbuf + slen, "\",\"cost\":\""); + byte_to_html(root_bridge_cost >> 24); + byte_to_html(root_bridge_cost >> 16); + byte_to_html(root_bridge_cost >> 8); + byte_to_html(root_bridge_cost); + /* are we the elected root? (our MAC == root MAC) */ + stp_we_root = 1; + for (uint8_t j = 0; j < 6; j++) { + if (root_bridge.mac[j] != uip_ethaddr.addr[j]) + stp_we_root = 0; + } + slen += strtox(outbuf + slen, "\",\"weRoot\":"); + bool_to_html(stp_we_root); + slen += strtox(outbuf + slen, ",\"ports\":["); + reg_read_m(RTL837X_MSTP_STATES); + for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + slen += strtox(outbuf + slen, "{\"p\":"); + itoa_html(machine.log_to_phys_port[i]); + slen += strtox(outbuf + slen, ",\"st\":"); + /* 2-bit field per logical port, packed from byte 3 up (cf. stp_setup) */ + itoa_html((sfr_data[3 - (i >> 2)] >> ((i << 1) & 0x7)) & 0x3); + slen += strtox(outbuf + slen, "},"); + } + slen -= 1; // remove comma + slen += strtox(outbuf + slen, "]}"); +} + + void send_eee(void) { dbg_string("send_eee called\nsending EEE status\n"); diff --git a/httpd/page_impl.h b/httpd/page_impl.h index 7907285..1d44719 100644 --- a/httpd/page_impl.h +++ b/httpd/page_impl.h @@ -14,6 +14,7 @@ void send_mtu(void); void send_config(void); void send_cmd_log(void); void send_lag(void); +void send_stp(void); void send_vlanlist(void); /* Convert only the lower nibble to ascii HEX char. From 524c37d7c7ee927c238feb1e735346b94663eca9 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 21 Jul 2026 09:10:47 +0200 Subject: [PATCH 08/68] stp: full RSTP configuration (bridge + per-port), CLI + GUI + persistence Implements the standard 802.1D-2004/802.1w configuration surface: Bridge: priority (0-15 x4096), hello time, max age, forward delay, force-version (RSTP v2 / STP-compatible v0 Config BPDUs), tx hold count (per-port per-second BPDU budget). Per port: enable, admin edge (forwarding immediately - no listen gap), auto edge (forwarding after 3 s of BPDU silence; DEFAULT, so host-facing ports no longer take the full forward delay), path cost (0=auto/20000), port priority, BPDU guard (port disabled on BPDU receipt), root guard (never accept a better root on the port), BPDU filter (no BPDUs in or out). Engine additions: root max-age expiry (reclaim the tree when the root goes silent), root path cost accounting (rx cost + root-port cost, advertised in our BPDUs), loop detection (our own BPDU coming back blocks the port for a listen period), topology-change counter, approximated per-port roles (Root/Designated/Alternate) for diagnostics. CLI: "stp prio|hello|maxage|fwd|txhold|version ..." and "stp port on|off|edge|cost|prio|guard|filter ..." (stp_parse, delegated from cmd_parser); all forms accepted by the startup-config validator so the whole configuration persists. /stp.json now reports config + status; the Spanning Tree page exposes everything with immediate-apply controls and live state/role columns (edit-in-flight guard against the 2 s refresh). 8051 memory: the module moves to code BANK2; internal-RAM pressure from cross-bank calls resolved by xdata loop iterators/scratch, __reentrant on the small helpers, and moving httpd's header-pointer globals to xdata. Verified on hardware (SWTGW218AS): defaults land per standard; priority and hello change live; admin-edge ports (the LACP bond uplinks) keep the LAN at 0% loss THROUGH "stp on"; auto-edge ports forward after 3 s; a port that heard real BPDUs (a VM bridge behind physical port 6) correctly declined auto-edge, sat out the full listen period and became Designated; tc counts promotions; we win the root election at priority 16384 vs 32768. (cherry picked from commit 09a34dc6acdc81ab9cab0727d2f4a59c68131a3e) --- cmd_parser.c | 10 +- html/config.js | 11 +- html/stp.html | 22 +- html/stp.js | 143 ++++++++++--- httpd/httpd.c | 6 +- httpd/page_impl.c | 68 +++++-- rtl837x_stp.c | 502 +++++++++++++++++++++++++++++++++++++--------- rtl837x_stp.h | 36 +++- rtlplayground.c | 1 + 9 files changed, 641 insertions(+), 158 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index 93c51e6..b724d15 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -1570,15 +1570,7 @@ void cmd_parser(void) __banked print_string("Error: hostname [name] - the name must not contain spaces\n"); } } else if (cmd_compare(0, "stp")) { - if (cmd_compare(1, "on")) { - print_string("STP enabled\n"); - stpEnabled = 1; - stp_setup(); - } else { - print_string("STP disabled\n"); - stp_off(); - stpEnabled = 0; - } + stp_parse(); } else if (cmd_compare(0, "pvid") && cmd_words_len == 3) { __xdata uint16_t pvid; if (cmd_buffer[cmd_words_b[1]] >= '1' diff --git a/html/config.js b/html/config.js index 8af3fba..29ca1ef 100644 --- a/html/config.js +++ b/html/config.js @@ -22,6 +22,14 @@ const conf_cmds = [ /^laghash\s+\d(\s+\w+)+$/, /^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/, /^stp\s+(on|off)$/, + /^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/, + /^stp\s+version\s+(rstp|stp)$/, + /^stp\s+port\s+\d{1,2}\s+(on|off)$/, + /^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/, + /^stp\s+port\s+\d{1,2}\s+cost\s+\d{1,3}$/, + /^stp\s+port\s+\d{1,2}\s+prio\s+\d{1,3}$/, + /^stp\s+port\s+\d{1,2}\s+guard\s+(none|bpdu|root)$/, + /^stp\s+port\s+\d{1,2}\s+filter\s+(on|off)$/, /^igmp\s+(on|off)$/, /^mtu\s+\d{1,2}\s+\d+$/, /^bw\s+(in|out)\s+\d{1,2}\s+\S+$/, @@ -46,7 +54,8 @@ const conf_overwrite = [ /^lag\s+\d+\b/, /^laghash\b/, /^isolate\s+\d{1,2}\b/, - /^stp\b/, + /^stp\s+(prio|hello|maxage|fwd|txhold|version)\b/, + /^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter)\b/, /^igmp\b/, /^mtu\s+\d{1,2}\b/, /^bw\s+(in|out)\s+\d{1,2}\b/, diff --git a/html/stp.html b/html/stp.html index af968f5..1e5a5c2 100644 --- a/html/stp.html +++ b/html/stp.html @@ -13,7 +13,27 @@

    Mode:

    -
    +

    Bridge settings

    + + + + + + + + + + + + +
    PriorityVersionHello [s]Max age [s]Fwd delay [s]Tx hold
    +

    Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.

    +

    Ports

    + + + + +
    PortStateRoleSTPEdgeCost [k]PriorityGuardFilter
    diff --git a/html/stp.js b/html/stp.js index 3682207..c85712a 100644 --- a/html/stp.js +++ b/html/stp.js @@ -1,12 +1,78 @@ -/* ---- Spanning Tree (RSTP) section ---- */ +/* Spanning Tree page: full RSTP configuration + live status. + * + * Every control applies IMMEDIATELY on change (POST /cmd "stp ...") - there is + * no per-row Apply. The refresh (2 s) repopulates controls from /stp.json; + * a global dirty flag suppresses that between a change and its confirmation + * so the refresh never reverts an edit in flight (same lesson as the LAG page). + */ // STP port states as encoded in the ASIC's MSTP register (2 bits per port) const STP_STATES = ["Disabled", "Blocking", "Learning", "Forwarding"]; +const STP_ROLES = ["-", "Root", "Designated", "Alternate"]; + +// stp_pflags bits (keep in sync with rtl837x_stp.h) +const PF_ENABLED = 1, PF_ADMEDGE = 2, PF_AUTOEDGE = 4, PF_BPDUGUARD = 8, + PF_ROOTGUARD = 16, PF_FILTER = 32, PF_OPEREDGE = 64, PF_TRIPPED = 128; -// "user is editing" flag: while set, the periodic refresh must not overwrite -// the mode dropdown (same pattern as the LAG page - without it the 2 s refresh -// silently reverts the user's choice before Apply). var stpDirty = false; +var stpRows = 0; // ports table built? + +async function stpCmd(cmd) { + stpDirty = true; + try { + await fetch('/cmd', { method: 'POST', body: cmd }); + } catch(err) { + console.error(`Error: ${err}`); + } + stpDirty = false; + fetchStp(); +} + +function sel(id, opts, onch) { + const s = document.createElement("select"); + s.id = id; + for (const [v, label] of opts) { + const o = document.createElement("option"); + o.value = v; o.textContent = label; + s.appendChild(o); + } + s.addEventListener("change", onch); + return s; +} + +function num(id, min, max, onch) { + const n = document.createElement("input"); + n.type = "number"; n.id = id; n.min = min; n.max = max; n.style.width = "4em"; + n.addEventListener("change", onch); + return n; +} + +function buildPortsTable(ports) { + const tbl = document.getElementById("stpPortsTbl"); + for (const p of ports) { + const tr = tbl.insertRow(); + tr.insertCell().textContent = p.p; // Port + tr.insertCell().id = "st_" + p.p; // State + tr.insertCell().id = "role_" + p.p; // Role + tr.insertCell().appendChild(sel("en_" + p.p, + [["on","on"],["off","off"]], + e => stpCmd("stp port " + p.p + " " + e.target.value))); + tr.insertCell().appendChild(sel("edge_" + p.p, + [["auto","auto"],["on","edge"],["off","off"]], + e => stpCmd("stp port " + p.p + " edge " + e.target.value))); + tr.insertCell().appendChild(num("cost_" + p.p, 0, 255, + e => stpCmd("stp port " + p.p + " cost " + e.target.value))); + tr.insertCell().appendChild(num("prio_" + p.p, 0, 240, + e => stpCmd("stp port " + p.p + " prio " + e.target.value))); + tr.insertCell().appendChild(sel("guard_" + p.p, + [["none","none"],["bpdu","BPDU"],["root","Root"]], + e => stpCmd("stp port " + p.p + " guard " + e.target.value))); + tr.insertCell().appendChild(sel("filt_" + p.p, + [["off","off"],["on","on"]], + e => stpCmd("stp port " + p.p + " filter " + e.target.value))); + } + stpRows = ports.length; +} function fetchStp() { var xhttp = new XMLHttpRequest(); @@ -15,21 +81,42 @@ function fetchStp() { const s = JSON.parse(xhttp.responseText); // textContent throughout: rootMac comes from received BPDUs // (remote-controlled), never render it as HTML - if (!stpDirty) - document.getElementById("stpMode").value = s.on ? "on" : "off"; + if (!stpRows) + buildPortsTable(s.ports); document.getElementById("stpStat").textContent = s.on ? (s.weRoot - ? "This switch is the root bridge (priority 0x" + s.rootPrio + ")" + ? "This switch is the root bridge (priority 0x" + s.rootPrio + ") — topology changes: " + parseInt(s.tc, 16) : "Root bridge: 0x" + s.rootPrio + " / " + s.rootMac - + " \u2014 path cost: 0x" + s.cost) + + " via port " + s.rootPort + " — path cost: 0x" + s.cost + + " — topology changes: " + parseInt(s.tc, 16)) : ""; - let t = ""; - if (s.on) { - t = "port state\n"; - for (const p of s.ports) - t += String(p.p).padEnd(6) + STP_STATES[p.st] + "\n"; + // live status columns always refresh + for (const p of s.ports) { + const trip = (p.f & PF_TRIPPED) ? " (guard!)" : ""; + document.getElementById("st_" + p.p).textContent = + s.on ? STP_STATES[p.st] + trip : "-"; + document.getElementById("role_" + p.p).textContent = + s.on ? STP_ROLES[p.role] + ((p.f & PF_OPEREDGE) ? " edge" : "") : "-"; + } + if (stpDirty) // an edit is in flight - do not revert controls + return; + document.getElementById("stpMode").value = s.on ? "on" : "off"; + document.getElementById("bPrio").value = s.prio; + document.getElementById("bVer").value = s.rstp ? "rstp" : "stp"; + document.getElementById("bHello").value = s.hello; + document.getElementById("bMaxage").value = s.maxage; + document.getElementById("bFwd").value = s.fwd; + document.getElementById("bTxhold").value = s.txhold; + for (const p of s.ports) { + document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off"; + document.getElementById("edge_" + p.p).value = + (p.f & PF_ADMEDGE) ? "on" : ((p.f & PF_AUTOEDGE) ? "auto" : "off"); + document.getElementById("cost_" + p.p).value = p.cost; + document.getElementById("prio_" + p.p).value = p.prio; + document.getElementById("guard_" + p.p).value = + (p.f & PF_BPDUGUARD) ? "bpdu" : ((p.f & PF_ROOTGUARD) ? "root" : "none"); + document.getElementById("filt_" + p.p).value = (p.f & PF_FILTER) ? "on" : "off"; } - document.getElementById("stpPorts").textContent = t; } }; xhttp.open("GET", `/stp.json`, true); @@ -38,21 +125,31 @@ function fetchStp() { async function stpSub() { const on = document.getElementById("stpMode").value === "on"; - try { - await fetch('/cmd', { method: 'POST', body: on ? "stp on" : "stp off" }); - } catch(err) { - console.error(`Error: ${err}`); - } - stpDirty = false; // editing done - let the refresh show the truth - fetchStp(); + await stpCmd(on ? "stp on" : "stp off"); } window.addEventListener("load", function() { + // bridge priority: 0-15 (x4096) + const bp = document.getElementById("bPrio"); + for (let i = 0; i < 16; i++) { + const o = document.createElement("option"); + o.value = i; o.textContent = (i * 4096) + (i === 8 ? " (default)" : ""); + bp.appendChild(o); + } + bp.addEventListener("change", e => stpCmd("stp prio " + e.target.value)); + document.getElementById("bVer") + .addEventListener("change", e => stpCmd("stp version " + e.target.value)); + document.getElementById("bHello") + .addEventListener("change", e => stpCmd("stp hello " + e.target.value)); + document.getElementById("bMaxage") + .addEventListener("change", e => stpCmd("stp maxage " + e.target.value)); + document.getElementById("bFwd") + .addEventListener("change", e => stpCmd("stp fwd " + e.target.value)); + document.getElementById("bTxhold") + .addEventListener("change", e => stpCmd("stp txhold " + e.target.value)); document.getElementById("stpMode") .addEventListener("change", () => { stpDirty = true; }); -}); -window.addEventListener("load", function() { update( () => { fetchStp(); const interval = setInterval(update, 2000); diff --git a/httpd/httpd.c b/httpd/httpd.c index 46db120..837509d 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -41,8 +41,8 @@ __xdata uint32_t cont_addr; // HTTP header properties __xdata uint8_t boundary[72]; -__xdata uint8_t *content_type = 0; -__xdata uint8_t *session = 0; +__xdata uint8_t * __xdata content_type = 0; +__xdata uint8_t * __xdata session = 0; // Global variables holding POST state __xdata uint16_t bindex; // Current index into the boundary @@ -54,7 +54,7 @@ __xdata char passwd[21]; __xdata char session_id[SESSION_ID_LENGTH + 1]; __xdata uint8_t authenticated; __xdata uint32_t now; -__xdata uint8_t *timeptr; +__xdata uint8_t * __xdata timeptr; __xdata uint32_t last_session_use; #define TSTATE_NONE 0 diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 794b01a..623fdb8 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -536,14 +536,14 @@ void send_lag(void) } -/* STP status for the L2 page ("/stp.json"): enable state, the elected root - * bridge (priority+MAC) and our path cost to it, whether we are the root, and - * the live per-port STP state read from the ASIC's MSTP register (2 bits per - * port: 0 disable, 1 blocking, 2 learning, 3 forwarding - same encoding - * stp_setup() writes). Ports are reported by their physical number. */ -/* Scratch for send_stp(): a plain local would land in the near-full 8051 - * internal-RAM overlay (OSEG). */ +/* STP status + configuration for the Spanning Tree page ("/stp.json"). + * Bridge config (prio index 0-15, hello/maxage/fwd seconds, rstp flag, tx + * hold), elected root (priority byte + MAC), our path cost, root port, TC + * counter, and per port: physical number, live ASIC state (2-bit MSTP field: + * 0 Dis 1 Blk 2 Lrn 3 Fwd), an approximated role, and the per-port config + * (enabled, edge admin/auto/oper, cost/1000, prio, guard, filter, tripped). */ __xdata uint8_t stp_we_root; +__xdata uint8_t pi_i, pi_j; /* shared loop iterators (DSEG relief) */ void send_stp(void) { @@ -552,33 +552,61 @@ void send_stp(void) slen += strtox(outbuf + slen, "{\"on\":"); bool_to_html(stpEnabled); + slen += strtox(outbuf + slen, ",\"rstp\":"); + bool_to_html(stp_rstp); + slen += strtox(outbuf + slen, ",\"prio\":"); + itoa_html(stp_prio >> 4); + slen += strtox(outbuf + slen, ",\"hello\":"); + itoa_html(stp_hello_s); + slen += strtox(outbuf + slen, ",\"maxage\":"); + itoa_html(stp_maxage_s); + slen += strtox(outbuf + slen, ",\"fwd\":"); + itoa_html(stp_fwddelay_s); + slen += strtox(outbuf + slen, ",\"txhold\":"); + itoa_html(stp_txhold); slen += strtox(outbuf + slen, ",\"rootPrio\":\""); byte_to_html(root_bridge.prio); byte_to_html(root_bridge.ext); slen += strtox(outbuf + slen, "\",\"rootMac\":\""); - for (uint8_t j = 0; j < 6; j++) - byte_to_html(root_bridge.mac[j]); + for (pi_j = 0; pi_j < 6; pi_j++) + byte_to_html(root_bridge.mac[pi_j]); slen += strtox(outbuf + slen, "\",\"cost\":\""); byte_to_html(root_bridge_cost >> 24); byte_to_html(root_bridge_cost >> 16); byte_to_html(root_bridge_cost >> 8); byte_to_html(root_bridge_cost); - /* are we the elected root? (our MAC == root MAC) */ - stp_we_root = 1; - for (uint8_t j = 0; j < 6; j++) { - if (root_bridge.mac[j] != uip_ethaddr.addr[j]) - stp_we_root = 0; - } + stp_we_root = (stp_root_port == 0xff) ? 1 : 0; slen += strtox(outbuf + slen, "\",\"weRoot\":"); bool_to_html(stp_we_root); - slen += strtox(outbuf + slen, ",\"ports\":["); + slen += strtox(outbuf + slen, ",\"rootPort\":"); + itoa_html(stp_root_port == 0xff ? 0 : machine.log_to_phys_port[stp_root_port]); + slen += strtox(outbuf + slen, ",\"tc\":\""); + byte_to_html(stp_tc_count >> 8); + byte_to_html(stp_tc_count); + slen += strtox(outbuf + slen, "\",\"ports\":["); reg_read_m(RTL837X_MSTP_STATES); - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + for (pi_i = machine.min_port; pi_i <= machine.max_port; pi_i++) { slen += strtox(outbuf + slen, "{\"p\":"); - itoa_html(machine.log_to_phys_port[i]); + itoa_html(machine.log_to_phys_port[pi_i]); slen += strtox(outbuf + slen, ",\"st\":"); - /* 2-bit field per logical port, packed from byte 3 up (cf. stp_setup) */ - itoa_html((sfr_data[3 - (i >> 2)] >> ((i << 1) & 0x7)) & 0x3); + stp_we_root = (sfr_data[3 - (pi_i >> 2)] >> ((pi_i << 1) & 0x7)) & 0x3; + itoa_html(stp_we_root); + /* role (approximated): 0 none/disabled, 1 root, 2 designated, 3 alternate(blocked) */ + slen += strtox(outbuf + slen, ",\"role\":"); + if (!(stp_pflags[pi_i] & STP_PF_ENABLED) || (stp_pflags[pi_i] & STP_PF_TRIPPED)) + itoa_html(0); + else if (pi_i == stp_root_port) + itoa_html(1); + else if (stp_we_root == 3) + itoa_html(2); + else + itoa_html(3); + slen += strtox(outbuf + slen, ",\"f\":"); + itoa_html(stp_pflags[pi_i]); + slen += strtox(outbuf + slen, ",\"cost\":"); + itoa_html(stp_pcost[pi_i] / 1000); + slen += strtox(outbuf + slen, ",\"prio\":"); + itoa_html(stp_pprio[pi_i]); slen += strtox(outbuf + slen, "},"); } slen -= 1; // remove comma diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 8d2e3fd..176f672 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -1,6 +1,17 @@ /* * This is a driver implementation for the Spanning Tree Protocol features for the RTL837x platform * This code is in the Public Domain + * + * Configurable per 802.1D-2004/802.1w: bridge priority, hello time, max age, + * forward delay, force-version (RSTP/STP), tx hold count; per port: enable, + * admin/auto edge, path cost, port priority, BPDU guard, root guard, BPDU + * filter. CLI: "stp ..." (see stp_parse), status: /stp.json (send_stp). + * + * The engine itself stays deliberately simple (no proposal/agreement + * handshake, no full port-role machine): we elect a root from received BPDUs, + * promote ports to forwarding after the listen period (or immediately for + * edge ports), age the root out via max age, and block ports on which we see + * our own BPDU (loop!) or - with root guard - a better root. */ // #define REGDBG @@ -34,14 +45,48 @@ extern __xdata struct uip_eth_addr uip_ethaddr; extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; -/* struct bridge now lives in rtl837x_stp.h (shared with the web UI). */ +/* CLI tokenizer state + helpers (owned by cmd_parser.c, HOME bank) */ +extern __xdata uint8_t cmd_buffer[CMD_BUF_SIZE]; +extern __xdata uint8_t cmd_words_len; +extern __xdata uint8_t cmd_words_b[15]; +uint8_t cmd_compare(uint8_t start, __code uint8_t * cmd); +uint8_t atoi_byte(__xdata uint8_t *out, uint8_t idx); + +/* ---- Configuration ---- */ +__xdata uint8_t stp_prio; /* bridge priority high byte (0x80 = 32768) */ +__xdata uint8_t stp_hello_s; +__xdata uint8_t stp_maxage_s; +__xdata uint8_t stp_fwddelay_s; +__xdata uint8_t stp_rstp; +__xdata uint8_t stp_txhold; + +__xdata uint8_t stp_pflags[10]; +__xdata uint32_t stp_pcost[10]; +__xdata uint8_t stp_pprio[10]; + +/* ---- Status / runtime ---- */ __xdata struct bridge root_bridge; -__xdata uint32_t root_bridge_cost; +__xdata uint32_t root_bridge_cost; /* our cost to the root (rx cost + root port cost) */ +__xdata uint8_t stp_root_port; /* 0xff = we are the root */ +__xdata uint16_t stp_tc_count; -__xdata uint8_t port_types[10]; -__xdata uint16_t port_timers[10]; -__xdata uint16_t port_hello[10]; +__xdata uint16_t port_timers[10]; /* listen-period countdown (0 = not listening) */ +__xdata uint16_t port_hello[10]; /* hello TX countdown */ +__xdata uint16_t stp_bpdu_age[10]; /* ticks since last BPDU seen on port (saturating) */ +__xdata uint8_t stp_tx_budget[10]; /* tx hold: BPDUs left in the current second */ +__xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */ +/* Scratch (8051: locals would overflow the internal-RAM overlay area) */ +__xdata uint8_t stp_scratch; +__xdata uint8_t stp_i; /* shared loop iterator (DSEG relief) */ +__xdata uint32_t stp_cost_scratch; + +/* stp_timers() runs at ~64 Hz (main loop ~256 Hz / (STP_TICK_DIVIDER+1)) */ +#define STP_HZ 64 +#define STP_EDGE_DELAY (3 * STP_HZ) /* auto-edge: forward after 3 s without BPDU */ + +#define AUTO_COST 20000UL /* path cost used when stp_pcost == 0 (1G default) */ +#define PCOST(i) (stp_pcost[i] ? stp_pcost[i] : AUTO_COST) struct stp_pkt { uint8_t stp_addr[6]; @@ -93,11 +138,7 @@ struct stp_pkt_in { #define STP_O ((__xdata struct stp_pkt *)&uip_buf[RTL_FRAME_DESC_SIZE]) #define STP_I ((__xdata struct stp_pkt_in *)&uip_buf[0]) -#define FLAG_PROPOSAL 0x02 -#define P_DESIGNATED ((STP_I->flags & 0x0c) == 0x0c) -#define P_PROPOSAL (STP_I->flags & FLAG_PROPOSAL) - -signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2) +signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2) __reentrant { for (uint8_t i = 0; i < 6; i++) { if (m1[i] == m2[i]) @@ -110,50 +151,37 @@ signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2) } -void stp_in(void) __banked +/* Write one port's 2-bit state into the ASIC's MSTP register. + * 00 disable, 01 blocking, 10 learning, 11 forwarding. */ +static void stp_state_set(uint8_t port, uint8_t state) __reentrant { - // By default we do not send anything out - uip_len = 0; - // MSTPSTP_I_STATES 0x5310 - // reg_read_m(RTL837X_MSTP_STATES); - - print_string("Check BPDU... \n"); - for (uint8_t i = 0; i < 80; i++) { - print_byte(uip_buf[i]); - write_char(' '); - } - write_char('\n'); - print_byte(STP_I->dsap); - print_byte(STP_I->ssap); - print_byte(STP_I->ctrl); - - write_char('\n'); - // Make sure this is the type of RSTP packet we are interested in: - if (!(STP_I->dsap == 0x42 && STP_I->ssap == 0x42 && STP_I->ctrl == 0x03)) - return; - print_string("Checking RSTP\n"); - if (STP_I->proto) - return; -// write_char('A'); print_byte(STP_I->version); write_char('\n'); - if (STP_I->version != 2) - return; -// write_char('B'); print_byte(STP_I->bpdu_type); write_char('\n'); - if (STP_I->bpdu_type != 2) - return; -// write_char('\n'); -// print_string("Flags: "); print_byte(STP_I->flags); write_char('\n'); - print_string("Check new Root\n"); - if (STP_I->root.prio < root_bridge.prio - || ((STP_I->root.prio == root_bridge.prio) && cmpMAC(STP_I->root.mac, root_bridge.mac) < 0)) { - print_string("Updating Root bridge\n"); - root_bridge.prio = STP_I->root.prio; - memcpy(root_bridge.mac, STP_I->root.mac, 6); - } + reg_read_m(RTL837X_MSTP_STATES); + stp_scratch = 3 - (port >> 2); + sfr_data[stp_scratch] &= ~(uint8_t)(0b11 << ((port << 1) & 0x7)); + sfr_data[stp_scratch] |= (uint8_t)(state << ((port << 1) & 0x7)); + reg_write_m(RTL837X_MSTP_STATES); } -void stp_cnf_send(uint8_t port) +/* Take the bridge back as root of its own tree (initial state / root aged out) */ +static void stp_claim_root(void) { + root_bridge.prio = stp_prio; + root_bridge.ext = 0x00; + memcpy(root_bridge.mac, uip_ethaddr.addr, 6); + root_bridge_cost = 0; + stp_root_port = 0xff; +} + + +void stp_cnf_send(uint8_t port) __reentrant +{ + if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & (STP_PF_FILTER | STP_PF_TRIPPED))) + return; + if (!stp_tx_budget[port]) /* tx hold count exhausted for this second */ + return; + stp_tx_budget[port]--; + STP_O->stp_addr[0] = 0x01; STP_O->stp_addr[1] = 0x80; STP_O->stp_addr[2] = 0xc2; STP_O->stp_addr[3] = STP_O->stp_addr[4] = STP_O->stp_addr[5] = 0x00; @@ -168,61 +196,211 @@ void stp_cnf_send(uint8_t port) STP_O->ssap = 0x42; STP_O->ctrl = 0x03; STP_O->proto = 0x0000; - STP_O->version = 0x02; // RSTP - STP_O->bpdu_type = 0x00; // Config - STP_O->flags = 0x81; + if (stp_rstp) { + STP_O->version = 0x02; /* RSTP */ + STP_O->bpdu_type = 0x02; /* Rapid Spanning Tree BPDU */ + /* flags: role designated (0b11 << 2) + learning + forwarding */ + STP_O->flags = 0x3c; + } else { + STP_O->version = 0x00; /* legacy STP */ + STP_O->bpdu_type = 0x00; /* Config BPDU */ + STP_O->flags = 0x00; + } memcpy(STP_O->src_addr, uip_ethaddr.addr, 6); memcpy(STP_O->root.mac, root_bridge.mac, 6); memcpy(STP_O->bridge.mac, uip_ethaddr.addr, 6); STP_O->root.prio = root_bridge.prio; - STP_O->root.ext = 0x00; - STP_O->root_path_cost = 0x00000000; + STP_O->root.ext = root_bridge.ext; + /* Our root path cost, big-endian (0 while we are the root ourselves) */ + STP_O->root_path_cost = ((root_bridge_cost & 0xff) << 24) + | ((root_bridge_cost & 0xff00) << 8) + | ((root_bridge_cost >> 8) & 0xff00) + | (root_bridge_cost >> 24); - STP_O->bridge.prio = 0x80; + STP_O->bridge.prio = stp_prio; STP_O->bridge.ext = 0x00; - STP_O->port_prio = 0x80; - STP_O->port_id = port; + STP_O->port_prio = stp_pprio[port]; + STP_O->port_id = port + 1; STP_O->age = 0x00; // FIXME: This only works because we do not use HTONS and the values are in 1/256 seconds - STP_O->age_max = 20; - STP_O->hello = 2; - STP_O->fwd_delay = 0x0f; + STP_O->age_max = stp_maxage_s; + STP_O->hello = stp_hello_s; + STP_O->fwd_delay = stp_fwddelay_s; -// uip_len = 0x27 + sizeof(struct rtl_tag); uip_len = sizeof(struct stp_pkt); tcpip_output(); } +void stp_in(void) __banked +{ + // By default we do not send anything out + uip_len = 0; + + /* Ingress port: low nibble of the CPU tag's pmask on RX */ + stp_scratch = ((uint8_t)HTONS(STP_I->rtl_tag.pmask)) & 0x0f; + if (stp_scratch < machine.min_port || stp_scratch > machine.max_port) + return; + { + __xdata static uint8_t port_l; /* NOT stp_scratch: stp_state_set() clobbers it */ + uint8_t port = (port_l = stp_scratch); + (void)port_l; + + // Make sure this is the type of (R)STP packet we are interested in: + if (!(STP_I->dsap == 0x42 && STP_I->ssap == 0x42 && STP_I->ctrl == 0x03)) + return; + if (STP_I->proto) + return; + /* Accept RSTP BPDUs (v2 type 2) and legacy Config BPDUs (v0 type 0) */ + if (!((STP_I->version == 2 && STP_I->bpdu_type == 2) + || (STP_I->version == 0 && STP_I->bpdu_type == 0))) + return; + + if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & STP_PF_FILTER)) + return; + + /* BPDU guard: an edge-facing port must never see a BPDU - shut it down. */ + if (stp_pflags[port] & STP_PF_BPDUGUARD) { + print_string("STP: BPDU guard tripped, disabling port "); + print_byte(port); write_char('\n'); + stp_pflags[port] |= STP_PF_TRIPPED; + stp_state_set(port, 0b00); + stp_tc_count++; + return; + } + + stp_bpdu_age[port] = 0; + + /* Our own BPDU coming back at us = a loop in the network. Block the port + * for a listen period; if the loop persists the BPDUs keep arriving and + * the port stays blocked. */ + if (cmpMAC(STP_I->bridge.mac, uip_ethaddr.addr) == 0) { + if (port_timers[port] == 0 && !(stp_pflags[port] & STP_PF_TRIPPED)) { + print_string("STP: loop detected on port "); + print_byte(port); write_char('\n'); + stp_state_set(port, 0b01); + port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; + stp_pflags[port] &= ~STP_PF_OPEREDGE; + stp_tc_count++; + } + return; + } + + /* Better root than the one we know? */ + if (STP_I->root.prio < root_bridge.prio + || ((STP_I->root.prio == root_bridge.prio) && cmpMAC(STP_I->root.mac, root_bridge.mac) < 0)) { + /* Root guard: this port must never become our path to the root. */ + if (stp_pflags[port] & STP_PF_ROOTGUARD) { + print_string("STP: root guard blocking port "); + print_byte(port); write_char('\n'); + stp_state_set(port, 0b01); + port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; + stp_pflags[port] &= ~STP_PF_OPEREDGE; + return; + } + print_string("Updating Root bridge\n"); + root_bridge.prio = STP_I->root.prio; + root_bridge.ext = STP_I->root.ext; + memcpy(root_bridge.mac, STP_I->root.mac, 6); + stp_root_port = port; + stp_tc_count++; + } + + /* Refresh our cost to the root when the update comes in on the root port */ + if (port == stp_root_port) { + stp_cost_scratch = STP_I->root_path_cost; + /* big-endian on the wire */ + root_bridge_cost = ((stp_cost_scratch & 0xff) << 24) + | ((stp_cost_scratch & 0xff00) << 8) + | ((stp_cost_scratch >> 8) & 0xff00) + | (stp_cost_scratch >> 24); + root_bridge_cost += PCOST(port); + } + } +} + + void stp_timers(void) __banked { - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { - port_hello[i]--; - if (!port_hello[i]) { - port_hello[i] = TIME_HELLO; - print_string("STP_HELLO port "); - print_byte(i); write_char('\n'); - stp_cnf_send(i); + /* Refill the per-port tx budgets once per second (tx hold count) */ + if (++stp_sec_tick >= STP_HZ) { + stp_sec_tick = 0; + for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) + stp_tx_budget[stp_i] = stp_txhold; + } + + for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { + if (!(stp_pflags[stp_i] & STP_PF_ENABLED)) + continue; + + if (stp_bpdu_age[stp_i] < 0xffff) + stp_bpdu_age[stp_i]++; + + /* Periodic hello */ + if (port_hello[stp_i]) + port_hello[stp_i]--; + if (!port_hello[stp_i]) { + port_hello[stp_i] = (uint16_t)stp_hello_s * STP_HZ; + stp_cnf_send(stp_i); } - /* Promote a port out of the initial blocking state once its listen - * period expires. stp_setup() puts every port into blocking with - * port_timers = 10 s, but nothing ever counted that down - so on a - * network with no other RSTP bridge (nobody sends us BPDUs) every - * port stayed blocking FOREVER and "stp on" killed the whole - * network. If no better root was heard during the listen period we - * are the designated bridge on that port: go to forwarding. */ - if (port_timers[i]) { - if (!--port_timers[i]) { - reg_read_m(RTL837X_MSTP_STATES); - sfr_data[3 - (i >> 2)] |= (uint8_t)(0b11 << ((i << 1) & 0x7)); - reg_write_m(RTL837X_MSTP_STATES); + + /* Promote a port out of blocking once its listen period expires + * with no reason to stay blocked (no better root heard: we are + * the designated bridge on that port). */ + if (port_timers[stp_i]) { + if (!--port_timers[stp_i]) { + stp_state_set(stp_i, 0b11); print_string("STP: port forwarding "); - print_byte(i); write_char('\n'); + print_byte(stp_i); write_char('\n'); + stp_tc_count++; + } else if ((stp_pflags[stp_i] & STP_PF_AUTOEDGE) + && stp_bpdu_age[stp_i] > STP_EDGE_DELAY) { + /* Auto edge: nothing talks (R)STP on this port - it is + * host-facing, go to forwarding without the full wait. */ + port_timers[stp_i] = 0; + stp_pflags[stp_i] |= STP_PF_OPEREDGE; + stp_state_set(stp_i, 0b11); + print_string("STP: edge port forwarding "); + print_byte(stp_i); write_char('\n'); } } } + + /* Age out a root that went silent: reclaim the tree. */ + if (stp_root_port != 0xff + && stp_bpdu_age[stp_root_port] > (uint16_t)stp_maxage_s * STP_HZ) { + print_string("STP: root aged out, claiming root\n"); + stp_claim_root(); + stp_tc_count++; + } +} + + +/* Reset all configuration to the 802.1D/802.1w defaults. Called once at boot + * (before the startup config replays "stp ..." commands over it). */ +void stp_defaults(void) __banked +{ + stp_prio = 0x80; /* 32768 */ + stp_hello_s = 2; + stp_maxage_s = 20; + stp_fwddelay_s = 15; + stp_rstp = 1; + stp_txhold = 6; + for (stp_i = 0; stp_i < 10; stp_i++) { + /* enabled, auto-edge on: host-facing ports go forwarding after + * 3 s of BPDU silence instead of the full forward delay */ + stp_pflags[stp_i] = STP_PF_ENABLED | STP_PF_AUTOEDGE; + stp_pcost[stp_i] = 0; /* auto */ + stp_pprio[stp_i] = 0x80; + stp_bpdu_age[stp_i] = 0; + port_timers[stp_i] = 0; + port_hello[stp_i] = 0; + stp_tx_budget[stp_i] = 6; + } + stp_tc_count = 0; + stp_claim_root(); } @@ -263,22 +441,29 @@ void stp_setup(void) __banked { print_string("Enabling STP: "); sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0; - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { - // Set STP port state to blocking - // States are: 00 disable, 01 blocking, 10 learning, 11 forwarding - uint8_t bit_mask = 0b01 << ( (i << 1) & 0x7); - sfr_data[3 - (i >> 2)] |= bit_mask; - port_hello[i] = TIME_HELLO; - port_timers[i] = 0x280; // 10 s in blocking state (at the ~64 Hz stp_timers rate) + for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { + stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); + stp_bpdu_age[stp_i] = 0; + stp_tx_budget[stp_i] = stp_txhold; + if (!(stp_pflags[stp_i] & STP_PF_ENABLED) || (stp_pflags[stp_i] & STP_PF_ADMEDGE)) { + /* not participating, or admin edge: forwarding immediately */ + if (stp_pflags[stp_i] & STP_PF_ADMEDGE) + stp_pflags[stp_i] |= STP_PF_OPEREDGE; + sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b11 << ((stp_i << 1) & 0x7)); + port_timers[stp_i] = 0; + } else { + /* listen first: blocking until the forward-delay expires */ + sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b01 << ((stp_i << 1) & 0x7)); + port_timers[stp_i] = (uint16_t)stp_fwddelay_s * STP_HZ; + } + port_hello[stp_i] = (uint16_t)stp_hello_s * STP_HZ; } sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9) reg_write_m(RTL837X_MSTP_STATES); print_reg(RTL837X_MSTP_STATES); write_char('\n'); - root_bridge.prio = 0x80; // This corresponds to 32768 - root_bridge.ext = 0x00; - memcpy(root_bridge.mac, uip_ethaddr.addr, 6); + stp_claim_root(); /* Take BPDUs to the CPU only - we are a participating bridge now. */ stp_fdb_update(PMASK_CPU); @@ -288,11 +473,12 @@ void stp_setup(void) __banked void stp_off(void) __banked { sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0; - for (uint8_t i = machine.min_port; i <= machine.max_port; i++) { + for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { // Set STP port state to forwarding // States are: 00 disable, 01 blocking, 10 learning, 11 forwarding - uint8_t bit_mask = 0b11 << ( (i << 1) & 0x7); - sfr_data[3 - (i >> 2)] |= bit_mask; + sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b11 << ((stp_i << 1) & 0x7)); + stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); + port_timers[stp_i] = 0; } sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9) reg_write_m(RTL837X_MSTP_STATES); @@ -300,3 +486,131 @@ void stp_off(void) __banked /* Restore BPDU transparency: flood them again like an unmanaged switch. */ stp_fdb_update(PMASK_CPU | (machine_detected.isRTL8373 ? PMASK_9 : PMASK_6)); } + + +/* ---- "stp ..." CLI ---- + * stp on|off + * stp prio <0-15> (bridge priority = n * 4096) + * stp hello <1-10> | stp maxage <6-40> | stp fwd <4-30> | stp txhold <1-10> + * stp version rstp|stp + * stp port <1-9> on|off + * stp port <1-9> edge on|off|auto + * stp port <1-9> cost <0-255> (x1000; 0 = auto/20000) + * stp port <1-9> prio <0-240> + * stp port <1-9> guard none|bpdu|root + * stp port <1-9> filter on|off + */ +void stp_parse(void) __banked __reentrant +{ + if (cmd_compare(1, "on")) { + print_string("STP enabled\n"); + stpEnabled = 1; + stp_setup(); + return; + } + if (cmd_compare(1, "off")) { + print_string("STP disabled\n"); + stp_off(); + stpEnabled = 0; + return; + } + if (cmd_words_len < 3) + goto err; + + if (cmd_compare(1, "port")) { + if (cmd_words_len < 4) + goto err; + if (atoi_byte(&stp_scratch, cmd_words_b[2]) || stp_scratch < 1 || stp_scratch > 9) + goto err; + { + uint8_t port = machine.phys_to_log_port[stp_scratch - 1]; + if (cmd_compare(3, "on")) { + stp_pflags[port] |= STP_PF_ENABLED; + stp_pflags[port] &= ~STP_PF_TRIPPED; + if (stpEnabled) { /* (re)join: listen first */ + stp_state_set(port, 0b01); + port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; + } + } else if (cmd_compare(3, "off")) { + stp_pflags[port] &= ~STP_PF_ENABLED; + if (stpEnabled) + stp_state_set(port, 0b11); /* plain forwarding */ + } else if (cmd_compare(3, "edge")) { + stp_pflags[port] &= ~(STP_PF_ADMEDGE | STP_PF_AUTOEDGE); + if (cmd_compare(4, "on")) + stp_pflags[port] |= STP_PF_ADMEDGE; + else if (cmd_compare(4, "auto")) + stp_pflags[port] |= STP_PF_AUTOEDGE; + else if (!cmd_compare(4, "off")) + goto err; + } else if (cmd_compare(3, "cost")) { + if (atoi_byte(&stp_scratch, cmd_words_b[4])) + goto err; + stp_pcost[port] = (uint32_t)stp_scratch * 1000; + } else if (cmd_compare(3, "prio")) { + if (atoi_byte(&stp_scratch, cmd_words_b[4])) + goto err; + stp_pprio[port] = stp_scratch & 0xf0; + } else if (cmd_compare(3, "guard")) { + stp_pflags[port] &= ~(STP_PF_BPDUGUARD | STP_PF_ROOTGUARD); + if (cmd_compare(4, "bpdu")) + stp_pflags[port] |= STP_PF_BPDUGUARD; + else if (cmd_compare(4, "root")) + stp_pflags[port] |= STP_PF_ROOTGUARD; + else if (!cmd_compare(4, "none")) + goto err; + } else if (cmd_compare(3, "filter")) { + if (cmd_compare(4, "on")) + stp_pflags[port] |= STP_PF_FILTER; + else if (cmd_compare(4, "off")) + stp_pflags[port] &= ~STP_PF_FILTER; + else + goto err; + } else { + goto err; + } + } + return; + } + + if (atoi_byte(&stp_scratch, cmd_words_b[2])) { + if (cmd_compare(1, "version")) { + if (cmd_compare(2, "rstp")) + stp_rstp = 1; + else if (cmd_compare(2, "stp")) + stp_rstp = 0; + else + goto err; + return; + } + goto err; + } + if (cmd_compare(1, "prio")) { + if (stp_scratch > 15) + goto err; + stp_prio = stp_scratch << 4; /* n * 4096, as the BPDU's high byte */ + if (stp_root_port == 0xff) + stp_claim_root(); /* re-announce with the new priority */ + } else if (cmd_compare(1, "hello")) { + if (stp_scratch < 1 || stp_scratch > 10) + goto err; + stp_hello_s = stp_scratch; + } else if (cmd_compare(1, "maxage")) { + if (stp_scratch < 6 || stp_scratch > 40) + goto err; + stp_maxage_s = stp_scratch; + } else if (cmd_compare(1, "fwd")) { + if (stp_scratch < 4 || stp_scratch > 30) + goto err; + stp_fwddelay_s = stp_scratch; + } else if (cmd_compare(1, "txhold")) { + if (stp_scratch < 1 || stp_scratch > 10) + goto err; + stp_txhold = stp_scratch; + } else { + goto err; + } + return; +err: + print_string("Error: stp on|off | prio <0-15> | hello <1-10> | maxage <6-40> | fwd <4-30> | txhold <1-10> | version rstp|stp | port <1-9> on|off|edge|cost|prio|guard|filter ...\n"); +} diff --git a/rtl837x_stp.h b/rtl837x_stp.h index 4aaf959..cf9e9e9 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -6,8 +6,8 @@ void stp_in(void) __banked; void stp_setup(void) __banked; void stp_timers(void) __banked; void stp_off(void) __banked; - -#define TIME_HELLO 0x80 // 2 sec (stp_timers runs at ~64 Hz: main loop ~256 Hz / STP_TICK_DIVIDER+1) +void stp_parse(void) __banked __reentrant; /* "stp ..." CLI handler (cmd_parser delegates here) */ +void stp_defaults(void) __banked; /* boot init: 802.1D/w default configuration */ /* Bridge identifier as carried in a BPDU (priority, extension, MAC). */ struct bridge { @@ -16,11 +16,33 @@ struct bridge { uint8_t mac[6]; }; -/* Protocol state, exposed read-only for the web UI (page_impl.c send_stp()) - * - the elected root bridge and our path cost to it. Owned by rtl837x_stp.c; - * stpEnabled is owned by rtlplayground.c. */ -extern __xdata uint8_t stpEnabled; +/* ---- Configuration (defaults per 802.1D-2004/802.1w, set in stp_defaults) --- */ +extern __xdata uint8_t stpEnabled; +extern __xdata uint8_t stp_prio; /* bridge priority, high byte: 0x80 = 32768; CLI takes 0-15 (steps of 4096) */ +extern __xdata uint8_t stp_hello_s; /* hello time, 1-10 s (default 2) */ +extern __xdata uint8_t stp_maxage_s; /* max age, 6-40 s (default 20) */ +extern __xdata uint8_t stp_fwddelay_s; /* forward delay, 4-30 s (default 15); our listen period */ +extern __xdata uint8_t stp_rstp; /* 1 = RSTP BPDUs (v2), 0 = STP-compatible Config BPDUs (v0) */ +extern __xdata uint8_t stp_txhold; /* max BPDUs per port per second (default 6) */ + +/* Per-port config/status flags (stp_pflags[]) */ +#define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */ +#define STP_PF_ADMEDGE 0x02 /* admin edge: forwarding immediately */ +#define STP_PF_AUTOEDGE 0x04 /* auto edge: forward after 3 s without BPDU */ +#define STP_PF_BPDUGUARD 0x08 /* disable port if a BPDU arrives */ +#define STP_PF_ROOTGUARD 0x10 /* never accept a better root on this port */ +#define STP_PF_FILTER 0x20 /* neither send nor accept BPDUs */ +#define STP_PF_OPEREDGE 0x40 /* runtime: port went forwarding as an edge */ +#define STP_PF_TRIPPED 0x80 /* runtime: disabled by BPDU guard */ + +extern __xdata uint8_t stp_pflags[10]; +extern __xdata uint32_t stp_pcost[10]; /* path cost; 0 = auto (20000) */ +extern __xdata uint8_t stp_pprio[10]; /* port priority (default 0x80) */ + +/* ---- Status, exposed read-only for the web UI (send_stp) ---- */ extern __xdata struct bridge root_bridge; -extern __xdata uint32_t root_bridge_cost; +extern __xdata uint32_t root_bridge_cost; /* our path cost to the root (0 if we are root) */ +extern __xdata uint8_t stp_root_port; /* logical port towards the root; 0xff = we are root */ +extern __xdata uint16_t stp_tc_count; /* topology change counter (diagnostics) */ #endif diff --git a/rtlplayground.c b/rtlplayground.c index b9cc05d..8fa2a7a 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -2180,6 +2180,7 @@ void main(void) print_reg(RTL837X_REG_SEC_COUNTER); #endif stpEnabled = 0; + stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */ nic_setup(); vlan_setup(); port_l2_setup(); From 9702f8e4c4cd2213f1e0613c54e22cdff938fb48 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 21 Jul 2026 09:25:21 +0200 Subject: [PATCH 09/68] stp: reject port sub-commands with a missing argument "stp port 7 edge" (no value) passed the cmd_words_len < 4 check and then cmd_compare(4, ...) read a stale word left over from the PREVIOUS command line - cmd_words_b is not cleared between commands - so the sub-command could randomly match whatever was typed before. Require 5 words for every per-port sub-command that carries an argument (everything except on/off). (cherry picked from commit 1210f4f9257b14c31ad653fc7616ef403a494d28) --- rtl837x_stp.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 176f672..1627b9f 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -524,6 +524,11 @@ void stp_parse(void) __banked __reentrant goto err; { uint8_t port = machine.phys_to_log_port[stp_scratch - 1]; + /* every sub-command except on/off carries one more argument; without + * this check cmd_compare(4,..) would read a stale word from the + * PREVIOUS command line (cmd_words_b is not cleared between commands) */ + if (cmd_words_len < 5 && !cmd_compare(3, "on") && !cmd_compare(3, "off")) + goto err; if (cmd_compare(3, "on")) { stp_pflags[port] |= STP_PF_ENABLED; stp_pflags[port] &= ~STP_PF_TRIPPED; From 267e371d3c5ab76122050c1aadfbd3bbc57835c6 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 21 Jul 2026 18:46:04 +0200 Subject: [PATCH 10/68] stp: BPDUs finally reach the wire (CPU tag flags + management VLAN) Two TX bugs meant our BPDUs NEVER left the switch as valid STP frames - on the wire they appeared as ethertype 0x8899 (the raw Realtek CPU tag) and were flooded to all ports instead of directed. Every earlier root election was a solo act: no other bridge ever saw us. Both are the same bug classes fixed for LACP earlier: - rtl_tag.flags was written raw (0x0020); like every other tag field it must go through HTONS, otherwise the bits land in the wrong byte (0x2000 = EFID), the ASIC fails to parse the tag and floods the frame with the 0x8899 header still attached. - With a management VLAN set, tcpip_output() splices an 802.1Q tag after the SA, again shifting the CPU tag out of the parsed position. BPDUs are link-local and must egress untagged: suppress the VLAN insert per frame, exactly as lacp_send() does. Hardware note discovered while fixing this: RTL_TAG_KEEP on an LLC/802.3 (length-field) frame makes the ASIC drop it entirely - the same flag works fine on ethertype frames (LACP). So BPDUs use LEARN_DIS only. Verified on the wire (tcpdump on the peer): clean "802.3 ... LLC, dsap STP 0x42 ... Rapid STP, bridge-id 8000." at the hello interval, sent directed (no flood), management HTTP unaffected, LAN at 0% loss throughout. (cherry picked from commit 4a41a292a9ab88d4fb05a8481ad28f8ffcfd9bc4) --- rtl837x_stp.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 1627b9f..6604ffd 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -44,6 +44,7 @@ __xdata uint8_t stp_fdb_i; extern __xdata struct uip_eth_addr uip_ethaddr; extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; +extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed per-frame for BPDUs */ /* CLI tokenizer state + helpers (owned by cmd_parser.c, HOME bank) */ extern __xdata uint8_t cmd_buffer[CMD_BUF_SIZE]; @@ -188,7 +189,13 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID); STP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION; STP_O->rtl_tag.reason = 0x00; - STP_O->rtl_tag.flags = 0x0020; // Disable L2 learning + /* Through HTONS like every tag field: raw 0x0020 lands on the wire as + * 0x2000 (EFID), the ASIC fails to parse the tag and floods the frame + * with the 0x8899 header still on it (same bug class as LACP had). + * NOTE: no RTL_TAG_KEEP here - hardware-verified that KEEP on an + * LLC/802.3 (length-field) frame makes the ASIC drop it entirely, + * while the same flag works fine on ethertype frames (LACP). */ + STP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS); STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << port); STP_O->msg_len = HTONS(0x27); @@ -229,8 +236,18 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->hello = stp_hello_s; STP_O->fwd_delay = stp_fwddelay_s; + /* BPDUs are link-local and must egress untagged: with a management VLAN + * set, tcpip_output() splices an 802.1Q tag after the SA, shifting the + * in-frame rtl_tag out of the position the ASIC parses - the CPU tag then + * leaks onto the wire as 0x8899 and the BPDU is flooded, not sent. + * Hardware-verified fix, same as lacp_send(). */ + { + uint16_t saved_mgmt_vlan = management_vlan; + management_vlan = 0; uip_len = sizeof(struct stp_pkt); tcpip_output(); + management_vlan = saved_mgmt_vlan; + } } From 2110cf128a5381cb2ae95546c0f731dcc9dd2c81 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 21 Jul 2026 22:25:57 +0200 Subject: [PATCH 11/68] stp: management failsafe (commit-confirm) + bounded NIC waits Enabling STP on a bridge whose management rides an in-band VLAN can cut off that very management - and not only by our own blocking: on this network the upstream TP-Link Easy Smart switch's "loop prevention" reacted to our BPDU hellos by blocking ITS port towards us while our ASIC was all-forwarding, isolating the whole segment until a power cycle. Recoverable only by going quiet. Add a commit-confirm watchdog: while STP is enabled, any HTTP request re-arms a countdown ("stp failsafe ", default 180, 0 disables); if management stays silent for the whole window, STP disables itself, which also stops BPDU TX so a neighbour's loop protection can release its block. The web UI polls /stp.json every 2 s, so an open browser naturally keeps the watchdog re-armed. The trip is reported via /stp.json (fs, fsT) and as a warning on the Spanning Tree page. Deliberately not conditioned on our own MSTP port states - the incident above proves the uplink can be dead while every local port forwards. Also bound the NIC DMA busy-waits (nic_tx_packet, nic_rx_header, nic_rx_packet): an unbounded spin on SFR_NIC_CTRL freezes the entire main loop (timers, HTTP, ARP) if the ASIC ever fails to consume a transfer; give up after ~65k polls and drop the frame instead. Hardware-verified end to end: with priority 15 against a live RSTP bridge the uplink died 6 s after "stp on" and the network recovered BY ITSELF 66 s later (trip at 45 s + neighbour release), fsT=1, LACP and LAN intact. Telemetry via syslog-to-edge-port host confirmed the full chain: countdown 44->4, trip, hello TX stopping at the trip. (cherry picked from commit 1fa9775156fd6d7ebfdda2382f73430b86601230) --- html/config.js | 3 ++- html/stp.html | 3 ++- html/stp.js | 7 ++++++- httpd/httpd.c | 5 +++++ httpd/page_impl.c | 4 ++++ rtl837x_stp.c | 38 ++++++++++++++++++++++++++++++++++++++ rtl837x_stp.h | 4 +++- rtlplayground.c | 23 ++++++++++++++++++++--- 8 files changed, 80 insertions(+), 7 deletions(-) diff --git a/html/config.js b/html/config.js index 29ca1ef..1657134 100644 --- a/html/config.js +++ b/html/config.js @@ -23,6 +23,7 @@ const conf_cmds = [ /^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/, /^stp\s+(on|off)$/, /^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/, + /^stp\s+failsafe\s+\d{1,3}$/, /^stp\s+version\s+(rstp|stp)$/, /^stp\s+port\s+\d{1,2}\s+(on|off)$/, /^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/, @@ -54,7 +55,7 @@ const conf_overwrite = [ /^lag\s+\d+\b/, /^laghash\b/, /^isolate\s+\d{1,2}\b/, - /^stp\s+(prio|hello|maxage|fwd|txhold|version)\b/, + /^stp\s+(prio|hello|maxage|fwd|txhold|version|failsafe)\b/, /^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter)\b/, /^igmp\b/, /^mtu\s+\d{1,2}\b/, diff --git a/html/stp.html b/html/stp.html index 1e5a5c2..15815be 100644 --- a/html/stp.html +++ b/html/stp.html @@ -16,7 +16,7 @@

    Bridge settings

    - + @@ -25,6 +25,7 @@ +
    PriorityVersionHello [s]Max age [s]Fwd delay [s]Tx holdPriorityVersionHello [s]Max age [s]Fwd delay [s]Tx holdMgmt failsafe [s]

    Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.

    diff --git a/html/stp.js b/html/stp.js index c85712a..c6cd124 100644 --- a/html/stp.js +++ b/html/stp.js @@ -83,7 +83,9 @@ function fetchStp() { // (remote-controlled), never render it as HTML if (!stpRows) buildPortsTable(s.ports); - document.getElementById("stpStat").textContent = s.on + document.getElementById("stpStat").textContent = s.fsT + ? "\u26a0 STP was disabled by the management failsafe (ports were blocked while management was unreachable). Review the topology before re-enabling." + : s.on ? (s.weRoot ? "This switch is the root bridge (priority 0x" + s.rootPrio + ") — topology changes: " + parseInt(s.tc, 16) : "Root bridge: 0x" + s.rootPrio + " / " + s.rootMac @@ -107,6 +109,7 @@ function fetchStp() { document.getElementById("bMaxage").value = s.maxage; document.getElementById("bFwd").value = s.fwd; document.getElementById("bTxhold").value = s.txhold; + document.getElementById("bFailsafe").value = s.fs; for (const p of s.ports) { document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off"; document.getElementById("edge_" + p.p).value = @@ -147,6 +150,8 @@ window.addEventListener("load", function() { .addEventListener("change", e => stpCmd("stp fwd " + e.target.value)); document.getElementById("bTxhold") .addEventListener("change", e => stpCmd("stp txhold " + e.target.value)); + document.getElementById("bFailsafe") + .addEventListener("change", e => stpCmd("stp failsafe " + e.target.value)); document.getElementById("stpMode") .addEventListener("change", () => { stpDirty = true; }); diff --git a/httpd/httpd.c b/httpd/httpd.c index 837509d..bc98013 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -20,6 +20,9 @@ #pragma constseg BANK1 extern volatile __xdata uint8_t sfr_data[4]; +extern volatile __xdata uint32_t ticks; +/* 200 Hz free-running tick, owned by rtlplayground.c */ +volatile __xdata uint8_t mgmt_alive; /* consumed by the STP management failsafe */ extern __code uint8_t * __code hex; extern __code struct f_data f_data[]; extern __code char * __code mime_strings[]; @@ -548,6 +551,8 @@ void httpd_appcall(void) __xdata struct httpd_state * __xdata s = &(uip_conn->appstate); dbg_char('P'); + if (uip_newdata()) + mgmt_alive = 1; /* any HTTP activity proves management still works (STP failsafe) */ #ifdef DEBUG if (uip_newdata()) write_char('N'); diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 623fdb8..4aad451 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -564,6 +564,10 @@ void send_stp(void) itoa_html(stp_fwddelay_s); slen += strtox(outbuf + slen, ",\"txhold\":"); itoa_html(stp_txhold); + slen += strtox(outbuf + slen, ",\"fs\":"); + itoa_html(stp_failsafe_s); + slen += strtox(outbuf + slen, ",\"fsT\":"); + itoa_html(stp_failsafe_tripped); slen += strtox(outbuf + slen, ",\"rootPrio\":\""); byte_to_html(root_bridge.prio); byte_to_html(root_bridge.ext); diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 6604ffd..a07e435 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -61,6 +61,16 @@ __xdata uint8_t stp_fwddelay_s; __xdata uint8_t stp_rstp; __xdata uint8_t stp_txhold; +/* Management failsafe: if any port is held out of Forwarding while no HTTP + * request has been seen for stp_failsafe_s seconds, assume STP just cut off + * in-band management (mgmt VLAN rides a blockable front port!) and disable + * itself, restoring forwarding. Commit-confirm pattern; hardware lockout of + * 2026-07-20 is the motivating incident. 0 disables the watchdog. */ +__xdata uint8_t stp_failsafe_s; +__xdata uint8_t stp_failsafe_cnt; /* seconds left before the trip */ +__xdata uint8_t stp_failsafe_tripped; +extern volatile __xdata uint8_t mgmt_alive; /* set by httpd on any request */ + __xdata uint8_t stp_pflags[10]; __xdata uint32_t stp_pcost[10]; __xdata uint8_t stp_pprio[10]; @@ -346,6 +356,26 @@ void stp_timers(void) __banked stp_sec_tick = 0; for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) stp_tx_budget[stp_i] = stp_txhold; + + /* Management failsafe: plain commit-confirm. While STP is on, ANY + * HTTP request re-arms the countdown (the web UI polls /stp.json + * every 2 s, so an open browser keeps it alive); stp_failsafe_s + * seconds of management silence disable STP and restore the + * pre-STP state. Deliberately NOT conditioned on our own MSTP + * states: hardware incident 2026-07-21 showed a NEIGHBOR (TP-Link + * Easy Smart loop prevention) cutting our uplink in reaction to + * our BPDUs while our ASIC was all-forwarding - only going fully + * quiet (no BPDU TX) lets such a neighbor recover. */ + if (mgmt_alive) { + mgmt_alive = 0; + stp_failsafe_cnt = stp_failsafe_s; + } else if (stp_failsafe_s && stp_failsafe_cnt && --stp_failsafe_cnt == 0) { + print_string("STP failsafe: no management activity - disabling STP\n"); + stp_off(); + stpEnabled = 0; + stp_failsafe_tripped = 1; + return; + } } for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { @@ -405,6 +435,8 @@ void stp_defaults(void) __banked stp_fwddelay_s = 15; stp_rstp = 1; stp_txhold = 6; + stp_failsafe_s = 180; + stp_failsafe_tripped = 0; for (stp_i = 0; stp_i < 10; stp_i++) { /* enabled, auto-edge on: host-facing ports go forwarding after * 3 s of BPDU silence instead of the full forward delay */ @@ -521,6 +553,8 @@ void stp_parse(void) __banked __reentrant { if (cmd_compare(1, "on")) { print_string("STP enabled\n"); + stp_failsafe_tripped = 0; + stp_failsafe_cnt = stp_failsafe_s; stpEnabled = 1; stp_setup(); return; @@ -629,6 +663,10 @@ void stp_parse(void) __banked __reentrant if (stp_scratch < 1 || stp_scratch > 10) goto err; stp_txhold = stp_scratch; + } else if (cmd_compare(1, "failsafe")) { + /* 0 disables the management watchdog; otherwise seconds to trip */ + stp_failsafe_s = stp_scratch; + stp_failsafe_cnt = stp_scratch; } else { goto err; } diff --git a/rtl837x_stp.h b/rtl837x_stp.h index cf9e9e9..bad0d03 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -23,7 +23,9 @@ extern __xdata uint8_t stp_hello_s; /* hello time, 1-10 s (default 2) */ extern __xdata uint8_t stp_maxage_s; /* max age, 6-40 s (default 20) */ extern __xdata uint8_t stp_fwddelay_s; /* forward delay, 4-30 s (default 15); our listen period */ extern __xdata uint8_t stp_rstp; /* 1 = RSTP BPDUs (v2), 0 = STP-compatible Config BPDUs (v0) */ -extern __xdata uint8_t stp_txhold; /* max BPDUs per port per second (default 6) */ +extern __xdata uint8_t stp_txhold; +extern __xdata uint8_t stp_failsafe_s; /* mgmt watchdog, seconds (0 = off) */ +extern __xdata uint8_t stp_failsafe_tripped; /* max BPDUs per port per second (default 6) */ /* Per-port config/status flags (stp_pflags[]) */ #define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */ diff --git a/rtlplayground.c b/rtlplayground.c index 8fa2a7a..6e2d9df 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -624,7 +624,11 @@ void nic_rx_header(uint16_t ring_ptr) SFR_NIC_DATA_U16LE = buffer; SFR_NIC_RING_U16LE = ring_ptr; SFR_NIC_CTRL = 1; - do { } while (SFR_NIC_CTRL != 0); + /* Bounded, cf. nic_tx_packet: a stuck NIC DMA must not freeze the loop */ + { + uint16_t rx_guard = 0; + do { } while (SFR_NIC_CTRL != 0 && ++rx_guard != 0); + } } @@ -647,7 +651,11 @@ void nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr) print_short(len); #endif SFR_NIC_CTRL = len; - do { } while (SFR_NIC_CTRL != 0); + /* Bounded, cf. nic_tx_packet: a stuck NIC DMA must not freeze the loop */ + { + uint16_t rx_guard = 0; + do { } while (SFR_NIC_CTRL != 0 && ++rx_guard != 0); + } } @@ -691,7 +699,16 @@ void nic_tx_packet(uint16_t ring_ptr) len += 0xf; len >>= 3; SFR_NIC_CTRL = len; - do { } while (SFR_NIC_CTRL != 0); + /* Bounded wait: normally the NIC consumes the frame in microseconds, but + * when the egress port is held in an MSTP non-forwarding state the ASIC + * has been observed to never complete the TX - an unbounded spin here + * then freezes the entire main loop (no STP/LACP timers, no HTTP, no + * ARP) until a power cycle. Give up after ~65k polls and drop the frame: + * losing one packet is recoverable, a frozen switch is not. */ + { + uint16_t tx_guard = 0; + do { } while (SFR_NIC_CTRL != 0 && ++tx_guard != 0); + } } From eaff9537ef7d7eab13f7fb741ff0b75f2a903c66 Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 22 Jul 2026 09:44:47 +0200 Subject: [PATCH 12/68] stp: vendor-style per-port config + status (path cost, p2p, designated info) Bring the Spanning Tree page in line with a typical managed switch's per-port panel. Configuration gains the full-range path cost (raw 0..200000000, 0 = auto, replacing the old 1000x-scaled byte), a point-to-point admin control (auto/on/off), and the priority is now a 0..240 step-16 dropdown. A new status table shows, per port, the Port State, Role, Designated Bridge / Port ID / Cost (learned from received BPDUs, kept per port and aged via the BPDU age), Operational Edge and Operational Point-to-Point. The designated fields fall back to presenting this switch as the segment's designated bridge when no fresh BPDU has been heard (so a quiet port shows our own bridge-id, as the vendor UIs do). /stp.json carries the packed hex fields plus our own MAC for that fallback. Space: reclaim BANK2 for the above by moving rtl837x_pins to HOME and compacting leds_dump into a register-address table (~800B); bandwidth returns to BANK1. No BANK3 - hardware-verified that PSBANK > 2 crashes this SoC at boot (a bricked unit and an SPI-programmer recovery earlier today); a warning to that effect is now in rtl837x_lldp.c. Hardware-verified: cost 200000000 and p2p off round-trip through the CLI and JSON, the status table populates correctly with STP enabled (all ports Forwarding/Designated, oper-edge and oper-p2p True), LACP 3f/3f and the LAN unaffected. (cherry picked from commit 2ec62072f061dc9e78bc821ba1c297cb6819e206) --- html/config.js | 5 ++-- html/stp.html | 10 +++++-- html/stp.js | 72 +++++++++++++++++++++++++++++------------------ httpd/page_impl.c | 57 +++++++++++++++++++++++++++++++++---- rtl837x_pins.c | 2 -- rtl837x_stp.c | 29 +++++++++++++++++-- rtl837x_stp.h | 10 ++++++- 7 files changed, 144 insertions(+), 41 deletions(-) diff --git a/html/config.js b/html/config.js index 1657134..e44652c 100644 --- a/html/config.js +++ b/html/config.js @@ -27,10 +27,11 @@ const conf_cmds = [ /^stp\s+version\s+(rstp|stp)$/, /^stp\s+port\s+\d{1,2}\s+(on|off)$/, /^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/, - /^stp\s+port\s+\d{1,2}\s+cost\s+\d{1,3}$/, + /^stp\s+port\s+\d{1,2}\s+cost\s+\d{1,9}$/, /^stp\s+port\s+\d{1,2}\s+prio\s+\d{1,3}$/, /^stp\s+port\s+\d{1,2}\s+guard\s+(none|bpdu|root)$/, /^stp\s+port\s+\d{1,2}\s+filter\s+(on|off)$/, + /^stp\s+port\s+\d{1,2}\s+p2p\s+(auto|on|off)$/, /^igmp\s+(on|off)$/, /^mtu\s+\d{1,2}\s+\d+$/, /^bw\s+(in|out)\s+\d{1,2}\s+\S+$/, @@ -56,7 +57,7 @@ const conf_overwrite = [ /^laghash\b/, /^isolate\s+\d{1,2}\b/, /^stp\s+(prio|hello|maxage|fwd|txhold|version|failsafe)\b/, - /^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter)\b/, + /^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter|p2p)\b/, /^igmp\b/, /^mtu\s+\d{1,2}\b/, /^bw\s+(in|out)\s+\d{1,2}\b/, diff --git a/html/stp.html b/html/stp.html index 15815be..5bdbeb5 100644 --- a/html/stp.html +++ b/html/stp.html @@ -29,10 +29,16 @@

    Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.

    -

    Ports

    +

    Port configuration

    - + + +
    PortStateRoleSTPEdgeCost [k]PriorityGuardFilterPortStatePath Cost
    (0 = Auto)
    PriorityEdge PortBPDU FilterGuardPoint-to-Point
    +

    Port status

    + + +
    PortPort StateRoleDesignated BridgeDesignated Port IDDesignated CostOper. EdgeOper. P2P
    diff --git a/html/stp.js b/html/stp.js index c6cd124..1af2230 100644 --- a/html/stp.js +++ b/html/stp.js @@ -1,16 +1,7 @@ -/* Spanning Tree page: full RSTP configuration + live status. - * - * Every control applies IMMEDIATELY on change (POST /cmd "stp ...") - there is - * no per-row Apply. The refresh (2 s) repopulates controls from /stp.json; - * a global dirty flag suppresses that between a change and its confirmation - * so the refresh never reverts an edit in flight (same lesson as the LAG page). - */ -// STP port states as encoded in the ASIC's MSTP register (2 bits per port) const STP_STATES = ["Disabled", "Blocking", "Learning", "Forwarding"]; const STP_ROLES = ["-", "Root", "Designated", "Alternate"]; -// stp_pflags bits (keep in sync with rtl837x_stp.h) const PF_ENABLED = 1, PF_ADMEDGE = 2, PF_AUTOEDGE = 4, PF_BPDUGUARD = 8, PF_ROOTGUARD = 16, PF_FILTER = 32, PF_OPEREDGE = 64, PF_TRIPPED = 128; @@ -49,38 +40,59 @@ function num(id, min, max, onch) { function buildPortsTable(ports) { const tbl = document.getElementById("stpPortsTbl"); + const stat = document.getElementById("stpStatTbl"); for (const p of ports) { const tr = tbl.insertRow(); tr.insertCell().textContent = p.p; // Port - tr.insertCell().id = "st_" + p.p; // State - tr.insertCell().id = "role_" + p.p; // Role tr.insertCell().appendChild(sel("en_" + p.p, - [["on","on"],["off","off"]], + [["on","Enable"],["off","Disable"]], e => stpCmd("stp port " + p.p + " " + e.target.value))); + const pc = num("cost_" + p.p, 0, 200000000, + e => stpCmd("stp port " + p.p + " cost " + e.target.value)); + pc.style.width = "7em"; + pc.title = "0 - 200000000 (0 = Auto)"; + tr.insertCell().appendChild(pc); + const pr = sel("prio_" + p.p, [], + e => stpCmd("stp port " + p.p + " prio " + e.target.value)); + for (let v = 0; v <= 240; v += 16) { + const o = document.createElement("option"); + o.value = v; o.textContent = v + (v === 128 ? " (default)" : ""); + pr.appendChild(o); + } + tr.insertCell().appendChild(pr); tr.insertCell().appendChild(sel("edge_" + p.p, - [["auto","auto"],["on","edge"],["off","off"]], + [["auto","Auto"],["on","Enable"],["off","Disable"]], e => stpCmd("stp port " + p.p + " edge " + e.target.value))); - tr.insertCell().appendChild(num("cost_" + p.p, 0, 255, - e => stpCmd("stp port " + p.p + " cost " + e.target.value))); - tr.insertCell().appendChild(num("prio_" + p.p, 0, 240, - e => stpCmd("stp port " + p.p + " prio " + e.target.value))); - tr.insertCell().appendChild(sel("guard_" + p.p, - [["none","none"],["bpdu","BPDU"],["root","Root"]], - e => stpCmd("stp port " + p.p + " guard " + e.target.value))); tr.insertCell().appendChild(sel("filt_" + p.p, - [["off","off"],["on","on"]], + [["off","Disable"],["on","Enable"]], e => stpCmd("stp port " + p.p + " filter " + e.target.value))); + tr.insertCell().appendChild(sel("guard_" + p.p, + [["none","None"],["bpdu","BPDU"],["root","Root"]], + e => stpCmd("stp port " + p.p + " guard " + e.target.value))); + tr.insertCell().appendChild(sel("p2p_" + p.p, + [["auto","Auto"],["on","Enable"],["off","Disable"]], + e => stpCmd("stp port " + p.p + " p2p " + e.target.value))); + + const sr = stat.insertRow(); + sr.insertCell().textContent = p.p; + for (const id of ["st","role","db","dp","dc","oe","op"]) + sr.insertCell().id = id + "_" + p.p; } stpRows = ports.length; } +function fmtBridgeId(h) { + if (!h || h.length < 16) return ""; + const prio = parseInt(h.slice(0, 4), 16); + const mac = h.slice(4).replace(/(..)(?=.)/g, "$1:"); + return prio + "-" + mac.toUpperCase(); +} + function fetchStp() { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { const s = JSON.parse(xhttp.responseText); - // textContent throughout: rootMac comes from received BPDUs - // (remote-controlled), never render it as HTML if (!stpRows) buildPortsTable(s.ports); document.getElementById("stpStat").textContent = s.fsT @@ -92,13 +104,19 @@ function fetchStp() { + " via port " + s.rootPort + " — path cost: 0x" + s.cost + " — topology changes: " + parseInt(s.tc, 16)) : ""; - // live status columns always refresh for (const p of s.ports) { const trip = (p.f & PF_TRIPPED) ? " (guard!)" : ""; document.getElementById("st_" + p.p).textContent = s.on ? STP_STATES[p.st] + trip : "-"; document.getElementById("role_" + p.p).textContent = - s.on ? STP_ROLES[p.role] + ((p.f & PF_OPEREDGE) ? " edge" : "") : "-"; + s.on ? STP_ROLES[p.role] : "-"; + document.getElementById("db_" + p.p).textContent = s.on ? fmtBridgeId(p.db) : "-"; + document.getElementById("dp_" + p.p).textContent = + s.on ? (parseInt(p.dp.slice(0, 2), 16) + "-" + parseInt(p.dp.slice(2), 16)) : "-"; + document.getElementById("dc_" + p.p).textContent = s.on ? parseInt(p.dc, 16) : "-"; + document.getElementById("oe_" + p.p).textContent = + s.on ? ((p.f & PF_OPEREDGE) ? "True" : "False") : "-"; + document.getElementById("op_" + p.p).textContent = s.on ? (p.p2 == 2 ? "False" : "True") : "-"; } if (stpDirty) // an edit is in flight - do not revert controls return; @@ -114,8 +132,9 @@ function fetchStp() { document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off"; document.getElementById("edge_" + p.p).value = (p.f & PF_ADMEDGE) ? "on" : ((p.f & PF_AUTOEDGE) ? "auto" : "off"); - document.getElementById("cost_" + p.p).value = p.cost; + document.getElementById("cost_" + p.p).value = parseInt(p.pc, 16); document.getElementById("prio_" + p.p).value = p.prio; + document.getElementById("p2p_" + p.p).value = ["auto","on","off"][p.p2]; document.getElementById("guard_" + p.p).value = (p.f & PF_BPDUGUARD) ? "bpdu" : ((p.f & PF_ROOTGUARD) ? "root" : "none"); document.getElementById("filt_" + p.p).value = (p.f & PF_FILTER) ? "on" : "off"; @@ -132,7 +151,6 @@ async function stpSub() { } window.addEventListener("load", function() { - // bridge priority: 0-15 (x4096) const bp = document.getElementById("bPrio"); for (let i = 0; i < 16; i++) { const o = document.createElement("option"); diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 4aad451..c0d9e0b 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -543,7 +543,31 @@ void send_lag(void) * 0 Dis 1 Blk 2 Lrn 3 Fwd), an approximated role, and the per-port config * (enabled, edge admin/auto/oper, cost/1000, prio, guard, filter, tripped). */ __xdata uint8_t stp_we_root; -__xdata uint8_t pi_i, pi_j; /* shared loop iterators (DSEG relief) */ +__xdata uint8_t pi_i, pi_j, pi_j2; /* shared loop iterators (DSEG relief) */ + +/* Parameter relays in xdata: keeps these helpers off the IRAM overlay */ +static __xdata uint32_t pi_u32; +static __xdata uint8_t pi_prio, pi_ext; +static __xdata uint8_t * __xdata pi_mac; + +static void u32hex_html(void) +{ + /* byte access instead of uint32 shifts: sdcc/mcs51 expands each + * 32-bit shift into a large helper sequence. Little-endian layout. */ + __xdata uint8_t *b = (__xdata uint8_t *)&pi_u32; + byte_to_html(b[3]); + byte_to_html(b[2]); + byte_to_html(b[1]); + byte_to_html(b[0]); +} + +static void bridge_to_html(void) +{ + byte_to_html(pi_prio); + byte_to_html(pi_ext); + for (pi_j2 = 0; pi_j2 < 6; pi_j2++) + byte_to_html(pi_mac[pi_j2]); +} void send_stp(void) { @@ -574,6 +598,9 @@ void send_stp(void) slen += strtox(outbuf + slen, "\",\"rootMac\":\""); for (pi_j = 0; pi_j < 6; pi_j++) byte_to_html(root_bridge.mac[pi_j]); + slen += strtox(outbuf + slen, "\",\"myMac\":\""); + for (pi_j = 0; pi_j < 6; pi_j++) + byte_to_html(uip_ethaddr.addr[pi_j]); slen += strtox(outbuf + slen, "\",\"cost\":\""); byte_to_html(root_bridge_cost >> 24); byte_to_html(root_bridge_cost >> 16); @@ -607,11 +634,31 @@ void send_stp(void) itoa_html(3); slen += strtox(outbuf + slen, ",\"f\":"); itoa_html(stp_pflags[pi_i]); - slen += strtox(outbuf + slen, ",\"cost\":"); - itoa_html(stp_pcost[pi_i] / 1000); - slen += strtox(outbuf + slen, ",\"prio\":"); + /* path cost (raw hex, full 0..200M range), priority, p2p */ + slen += strtox(outbuf + slen, ",\"pc\":\""); + pi_u32 = stp_pcost[pi_i]; u32hex_html(); + slen += strtox(outbuf + slen, "\",\"prio\":"); itoa_html(stp_pprio[pi_i]); - slen += strtox(outbuf + slen, "},"); + slen += strtox(outbuf + slen, ",\"p2\":"); + itoa_html(stp_pp2p[pi_i]); + /* designated info: a freshly heard BPDU wins, else we are the + * segment's designated bridge and report our own values */ + stp_we_root = stp_dbridge[pi_i].mac[5] && stp_bpdu_age[pi_i] < (uint16_t)stp_maxage_s * 64; + slen += strtox(outbuf + slen, ",\"db\":\""); + if (stp_we_root) { + pi_prio = stp_dbridge[pi_i].prio; pi_ext = stp_dbridge[pi_i].ext; + pi_mac = stp_dbridge[pi_i].mac; + } else { + pi_prio = stp_prio; pi_ext = 0; + pi_mac = uip_ethaddr.addr; + } + bridge_to_html(); + slen += strtox(outbuf + slen, "\",\"dp\":\""); + byte_to_html(stp_we_root ? (stp_dpid[pi_i] >> 8) : stp_pprio[pi_i]); + byte_to_html(stp_we_root ? stp_dpid[pi_i] : (pi_i + 1)); + slen += strtox(outbuf + slen, "\",\"dc\":\""); + pi_u32 = stp_we_root ? stp_dcost[pi_i] : root_bridge_cost; u32hex_html(); + slen += strtox(outbuf + slen, "\"},"); } slen -= 1; // remove comma slen += strtox(outbuf + slen, "]}"); diff --git a/rtl837x_pins.c b/rtl837x_pins.c index 90015b7..e340c26 100644 --- a/rtl837x_pins.c +++ b/rtl837x_pins.c @@ -2,8 +2,6 @@ #include "rtl837x_common.h" #include "rtl837x_regs.h" -#pragma codeseg BANK2 -#pragma constseg BANK2 uint8_t i2c_bus_from_sda_pin(uint8_t sda_pin) __banked { switch (sda_pin) { diff --git a/rtl837x_stp.c b/rtl837x_stp.c index a07e435..13980b8 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -74,6 +74,11 @@ extern volatile __xdata uint8_t mgmt_alive; /* set by httpd on any request */ __xdata uint8_t stp_pflags[10]; __xdata uint32_t stp_pcost[10]; __xdata uint8_t stp_pprio[10]; +__xdata uint8_t stp_pp2p[10]; + +__xdata struct bridge stp_dbridge[10]; +__xdata uint16_t stp_dpid[10]; +__xdata uint32_t stp_dcost[10]; /* ---- Status / runtime ---- */ __xdata struct bridge root_bridge; @@ -600,9 +605,29 @@ void stp_parse(void) __banked __reentrant else if (!cmd_compare(4, "off")) goto err; } else if (cmd_compare(3, "cost")) { - if (atoi_byte(&stp_scratch, cmd_words_b[4])) + /* raw 802.1D value, 0..200000000; 0 = auto (speed-based) */ + stp_cost_scratch = 0; + { + __xdata uint8_t *cp = &cmd_buffer[cmd_words_b[4]]; + if (*cp < '0' || *cp > '9') + goto err; + while (*cp >= '0' && *cp <= '9') { + stp_cost_scratch = stp_cost_scratch * 10 + (*cp - '0'); + cp++; + } + } + if (stp_cost_scratch > 200000000UL) + goto err; + stp_pcost[port] = stp_cost_scratch; + } else if (cmd_compare(3, "p2p")) { + if (cmd_compare(4, "auto")) + stp_pp2p[port] = 0; + else if (cmd_compare(4, "on")) + stp_pp2p[port] = 1; + else if (cmd_compare(4, "off")) + stp_pp2p[port] = 2; + else goto err; - stp_pcost[port] = (uint32_t)stp_scratch * 1000; } else if (cmd_compare(3, "prio")) { if (atoi_byte(&stp_scratch, cmd_words_b[4])) goto err; diff --git a/rtl837x_stp.h b/rtl837x_stp.h index bad0d03..56aea9d 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -39,7 +39,15 @@ extern __xdata uint8_t stp_failsafe_tripped; /* max BPDUs per port per second ( extern __xdata uint8_t stp_pflags[10]; extern __xdata uint32_t stp_pcost[10]; /* path cost; 0 = auto (20000) */ -extern __xdata uint8_t stp_pprio[10]; /* port priority (default 0x80) */ +extern __xdata uint8_t stp_pprio[10]; +extern __xdata uint8_t stp_pp2p[10]; /* admin point-to-point: 0 auto, 1 on, 2 off */ + +/* Last-heard designated info per port (from received BPDUs); consult + * stp_bpdu_age to decide whether it is still current. */ +extern __xdata struct bridge stp_dbridge[10]; +extern __xdata uint16_t stp_dpid[10]; +extern __xdata uint32_t stp_dcost[10]; +extern __xdata uint16_t stp_bpdu_age[10]; /* ticks since a BPDU was heard */ /* port priority (default 0x80) */ /* ---- Status, exposed read-only for the web UI (send_stp) ---- */ extern __xdata struct bridge root_bridge; From f39eefa90ce2c6c43f5ef4a71b028e444ab14efc Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 03:30:23 +0200 Subject: [PATCH 13/68] stp: carry the version-1 length field in RST BPDUs An RST BPDU body is 36 bytes: the Config-BPDU fields plus a trailing version-1 length octet (zero - there is no version-1 information). Ours was 35 - strict 802.1w parsers treat such a BPDU as malformed and drop it. Add the field, keep legacy Config BPDUs at 35 bytes, and set the 802.3 length accordingly (0x27 with LLC for RST, 0x26 for Config). --- rtl837x_stp.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 13980b8..2764731 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -125,6 +125,7 @@ struct stp_pkt { uint16_t age_max; uint16_t hello; uint16_t fwd_delay; + uint8_t version1_length; /* RST BPDU only: length of the (empty) v1 part */ }; struct stp_pkt_in { @@ -149,6 +150,7 @@ struct stp_pkt_in { uint16_t age_max; uint16_t hello; uint16_t fwd_delay; + uint8_t version1_length; /* RST BPDU only: length of the (empty) v1 part */ }; #define STP_O ((__xdata struct stp_pkt *)&uip_buf[RTL_FRAME_DESC_SIZE]) @@ -213,17 +215,20 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS); STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << port); - STP_O->msg_len = HTONS(0x27); STP_O->dsap = 0x42; STP_O->ssap = 0x42; STP_O->ctrl = 0x03; STP_O->proto = 0x0000; if (stp_rstp) { + /* 802.3 length = LLC (3) + RST BPDU body (36, incl. version1_length) */ + STP_O->msg_len = HTONS(0x27); STP_O->version = 0x02; /* RSTP */ STP_O->bpdu_type = 0x02; /* Rapid Spanning Tree BPDU */ /* flags: role designated (0b11 << 2) + learning + forwarding */ STP_O->flags = 0x3c; } else { + /* 802.3 length = LLC (3) + Config BPDU body (35) */ + STP_O->msg_len = HTONS(0x26); STP_O->version = 0x00; /* legacy STP */ STP_O->bpdu_type = 0x00; /* Config BPDU */ STP_O->flags = 0x00; @@ -250,6 +255,7 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->age_max = stp_maxage_s; STP_O->hello = stp_hello_s; STP_O->fwd_delay = stp_fwddelay_s; + STP_O->version1_length = 0; /* RST BPDU: no version-1 information */ /* BPDUs are link-local and must egress untagged: with a management VLAN * set, tcpip_output() splices an 802.1Q tag after the SA, shifting the @@ -259,7 +265,9 @@ void stp_cnf_send(uint8_t port) __reentrant { uint16_t saved_mgmt_vlan = management_vlan; management_vlan = 0; - uip_len = sizeof(struct stp_pkt); + /* A legacy Config BPDU body is 35 bytes - without the trailing + * version-1 length byte that only the RST BPDU (36 bytes) carries. */ + uip_len = stp_rstp ? sizeof(struct stp_pkt) : sizeof(struct stp_pkt) - 1; tcpip_output(); management_vlan = saved_mgmt_vlan; } From a31be093e7f5200124bda731a37684f783a5abe5 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 03:32:04 +0200 Subject: [PATCH 14/68] stp: answer TCN BPDUs and validate the received BPDU length Accept legacy Topology Change Notification BPDUs (v0, type 0x80): reply on the ingress port with a Config BPDU carrying Topology Change Acknowledgment so the sender stops repeating, and count the change. Also stop reading fields past the end of short frames: require the header through bpdu_type (33 bytes with the CPU/VLAN prefix) before classifying, and the full 35-byte body before the election logic - truncated or fuzzed BPDUs are dropped instead of parsed as garbage. --- rtl837x_stp.c | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 2764731..3e6a677 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -94,6 +94,8 @@ __xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */ /* Scratch (8051: locals would overflow the internal-RAM overlay area) */ __xdata uint8_t stp_scratch; +__xdata uint8_t stp_tx_flags_extra; /* one-shot flags OR-ed into the next BPDU (TCA) */ +__xdata uint16_t stp_rxlen; /* received frame length, saved before uip_len is consumed */ __xdata uint8_t stp_i; /* shared loop iterator (DSEG relief) */ __xdata uint32_t stp_cost_scratch; @@ -233,6 +235,8 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->bpdu_type = 0x00; /* Config BPDU */ STP_O->flags = 0x00; } + STP_O->flags |= stp_tx_flags_extra; /* e.g. TCA in reply to a TCN */ + stp_tx_flags_extra = 0; memcpy(STP_O->src_addr, uip_ethaddr.addr, 6); memcpy(STP_O->root.mac, root_bridge.mac, 6); @@ -276,7 +280,16 @@ void stp_cnf_send(uint8_t port) __reentrant void stp_in(void) __banked { - // By default we do not send anything out + /* Robustness: never read fields past the received frame. 33 covers the + * header through bpdu_type; the full Config/RST body is re-checked below. + * (uip_len is consumed and zeroed at the end - keep a local view.) */ + if (uip_len < 33) { + uip_len = 0; + return; + } + stp_rxlen = uip_len; + + // By default we do not send anything out (handle_rx would TX otherwise) uip_len = 0; /* Ingress port: low nibble of the CPU tag's pmask on RX */ @@ -293,9 +306,11 @@ void stp_in(void) __banked return; if (STP_I->proto) return; - /* Accept RSTP BPDUs (v2 type 2) and legacy Config BPDUs (v0 type 0) */ + /* Accept RSTP BPDUs (v2 type 2), legacy Config BPDUs (v0 type 0) and + * legacy TCN BPDUs (v0 type 0x80, 4-byte body) */ if (!((STP_I->version == 2 && STP_I->bpdu_type == 2) - || (STP_I->version == 0 && STP_I->bpdu_type == 0))) + || (STP_I->version == 0 + && (STP_I->bpdu_type == 0 || STP_I->bpdu_type == 0x80)))) return; if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & STP_PF_FILTER)) @@ -313,6 +328,21 @@ void stp_in(void) __banked stp_bpdu_age[port] = 0; + if (STP_I->bpdu_type == 0x80) { + /* TCN: a downstream bridge reports a topology change. Acknowledge it + * on this port so the sender stops repeating; the change itself is + * counted (and, once implemented, propagated rootward). */ + stp_tx_flags_extra = 0x80; /* Topology Change Acknowledgment */ + stp_cnf_send(port); /* transmits internally */ + uip_len = 0; /* ...so handle_rx must not TX again */ + stp_tc_count++; + return; + } + + /* Everything below reads the full Config/RST body. */ + if (stp_rxlen < 64) + return; + /* Our own BPDU coming back at us = a loop in the network. Block the port * for a listen period; if the loop persists the BPDUs keep arriving and * the port stays blocked. */ From 5361c85d307d61518e37d73d32076d1904125b88 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 03:33:23 +0200 Subject: [PATCH 15/68] stp: keep the root port quiet and relay the message age Two protocol-correctness fixes on the information we advertise: Only designated ports announce periodically. The root port is where our root information arrives; sending it back there feeds the upstream bridge its own data and makes us look like a competing designated bridge on that segment. Relay the message age instead of always claiming zero. A bridge increments the received age by one second per hop, so downstream neighbours can age the information out; advertising 0 forever made our BPDUs look eternally fresh no matter how stale the root information was. Age stays 0 while we are the root ourselves. --- rtl837x_stp.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 3e6a677..c6d0b6e 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -96,6 +96,7 @@ __xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */ __xdata uint8_t stp_scratch; __xdata uint8_t stp_tx_flags_extra; /* one-shot flags OR-ed into the next BPDU (TCA) */ __xdata uint16_t stp_rxlen; /* received frame length, saved before uip_len is consumed */ +__xdata uint8_t stp_msg_age; /* message age of the root info we hold, seconds */ __xdata uint8_t stp_i; /* shared loop iterator (DSEG relief) */ __xdata uint32_t stp_cost_scratch; @@ -191,6 +192,7 @@ static void stp_claim_root(void) memcpy(root_bridge.mac, uip_ethaddr.addr, 6); root_bridge_cost = 0; stp_root_port = 0xff; + stp_msg_age = 0; } @@ -255,7 +257,11 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->port_prio = stp_pprio[port]; STP_O->port_id = port + 1; - STP_O->age = 0x00; // FIXME: This only works because we do not use HTONS and the values are in 1/256 seconds + /* Message age, incremented by one second per bridge we relay through. + * The timer fields are in 1/256 s on the wire, and sdcc stores uint16 + * little-endian, so assigning the plain second count lands the value in + * the high (seconds) octet - see age_max/hello/fwd_delay below. */ + STP_O->age = (stp_root_port == 0xff) ? 0 : (uint16_t)(stp_msg_age + 1); STP_O->age_max = stp_maxage_s; STP_O->hello = stp_hello_s; STP_O->fwd_delay = stp_fwddelay_s; @@ -380,6 +386,9 @@ void stp_in(void) __banked /* Refresh our cost to the root when the update comes in on the root port */ if (port == stp_root_port) { + /* Age of the information we now hold (see the TX note on the wire + * format); saturate rather than wrap on absurd input. */ + stp_msg_age = (STP_I->age > 254) ? 254 : (uint8_t)STP_I->age; stp_cost_scratch = STP_I->root_path_cost; /* big-endian on the wire */ root_bridge_cost = ((stp_cost_scratch & 0xff) << 24) @@ -433,7 +442,12 @@ void stp_timers(void) __banked port_hello[stp_i]--; if (!port_hello[stp_i]) { port_hello[stp_i] = (uint16_t)stp_hello_s * STP_HZ; - stp_cnf_send(stp_i); + /* Only designated ports announce periodically: the root port is + * where our own root information comes FROM, and echoing it back + * there just feeds the upstream bridge its own data (and looks + * like a competing designated bridge on that segment). */ + if (stp_i != stp_root_port) + stp_cnf_send(stp_i); } /* Promote a port out of blocking once its listen period expires From 65a41eaddd555527893b990de2c26c3175cf36dc Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 03:34:22 +0200 Subject: [PATCH 16/68] port: add a bounded single-port L2 flush port_l2_forget() flushes the whole table and polls the flush engine without a bound. A topology change only needs to age out the port that changed, and the STP tick cannot afford an unbounded poll: add port_l2_forget_port() with a single-port mask and the same bounded wait the static-entry helper uses. --- rtl837x_port.c | 25 ++++++++++++++++++++++++- rtl837x_port.h | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/rtl837x_port.c b/rtl837x_port.c index 9e7ff00..1de3a4f 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -28,6 +28,10 @@ extern __xdata struct machine_runtime machine_detected; __xdata uint32_t l2_head; +/* Bounded-wait counter for the L2 table helpers; xdata because the 8051 + * internal-RAM overlay (OSEG) is full. */ +__xdata uint8_t l2mc_guard; + __xdata struct vlan_settings vlan_settings; void port_mirror_set(register uint8_t port, __xdata uint16_t rx_pmask, __xdata uint16_t tx_pmask) __banked @@ -314,6 +318,26 @@ void vlan_setup(void) __banked } +/* + * Forget the dynamic L2 entries learned on one port. + * + * Same flush engine as port_l2_forget(), but with a single-port mask so a + * topology change only ages out the affected port instead of the whole + * table. Bounded wait (cf. port_l2mc_set): this runs from the STP tick, and + * an unbounded poll on a stuck engine would freeze the main loop. + */ +void port_l2_forget_port(uint8_t port) __banked +{ + REG_SET(RTL837x_L2_TBL_FLUSH_CNF, 0x0); /* port-based, dynamic entries */ + REG_SET(RTL837x_L2_TBL_FLUSH_CTRL, L2_TBL_FLUSH_EXEC | (((uint16_t)1) << port)); + + l2mc_guard = 0; + do { + reg_read_m(RTL837x_L2_TBL_FLUSH_CTRL); + } while (sfr_data[1] && ++l2mc_guard); +} + + /* * Forget all dynamic L2 learned entries */ @@ -424,7 +448,6 @@ void port_l2_learned(void) __banked * Overwriting the same MAC+VID replaces the entry, so a caller can retarget * the mask at will (e.g. back to all ports to restore flooding). */ -__xdata uint8_t l2mc_guard; /* xdata: the internal-RAM overlay (OSEG) is full */ void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked { diff --git a/rtl837x_port.h b/rtl837x_port.h index 072173a..73cc0bf 100644 --- a/rtl837x_port.h +++ b/rtl837x_port.h @@ -55,6 +55,7 @@ void vlan_setup(void) __banked; void port_pvid_set(uint8_t port, __xdata uint16_t pvid) __banked; uint16_t port_pvid_get(uint8_t port) __banked; void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked; +void port_l2_forget_port(uint8_t port) __banked; void vlan_create(void) __banked; void vlan_delete(uint16_t vlan) __banked; void vlan_dump(void) __banked; From b51ed71bb1b8f59325e038f1efd990cdff0ff5ca Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 03:35:06 +0200 Subject: [PATCH 17/68] stp: announce topology changes and flush the port that changed A port entering forwarding, or being blocked because its own BPDU came back, changes where MAC addresses live - but the counter was bumped and nothing else happened: our forwarding table kept the stale entries and the neighbours were never told. Flush the affected port's dynamic entries (bounded single-port flush) and set the Topology Change flag in our BPDUs for max age + forward delay, so neighbours age their tables out as well. Edge ports are exempt: a host coming or going is not a topology change. --- rtl837x_stp.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index c6d0b6e..bd16597 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -97,6 +97,7 @@ __xdata uint8_t stp_scratch; __xdata uint8_t stp_tx_flags_extra; /* one-shot flags OR-ed into the next BPDU (TCA) */ __xdata uint16_t stp_rxlen; /* received frame length, saved before uip_len is consumed */ __xdata uint8_t stp_msg_age; /* message age of the root info we hold, seconds */ +__xdata uint16_t stp_tc_while; /* ticks left to set the TC flag in our BPDUs */ __xdata uint8_t stp_i; /* shared loop iterator (DSEG relief) */ __xdata uint32_t stp_cost_scratch; @@ -184,6 +185,21 @@ static void stp_state_set(uint8_t port, uint8_t state) __reentrant } +/* Signal a topology change: flush the stale forwarding entries of the port + * that changed, count it, and set the TC flag in our BPDUs for one + * max-age+forward-delay period (802.1D 8.6.14) so the neighbours age their + * own tables out too. Edge ports are exempt: a host appearing or leaving is + * not a topology change. */ +static void stp_topology_change(uint8_t port) __reentrant +{ + if (stp_pflags[port] & STP_PF_OPEREDGE) + return; + stp_tc_count++; + stp_tc_while = ((uint16_t)stp_maxage_s + stp_fwddelay_s) * STP_HZ; + port_l2_forget_port(port); +} + + /* Take the bridge back as root of its own tree (initial state / root aged out) */ static void stp_claim_root(void) { @@ -237,6 +253,8 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->bpdu_type = 0x00; /* Config BPDU */ STP_O->flags = 0x00; } + if (stp_tc_while) + STP_O->flags |= 0x01; /* Topology Change */ STP_O->flags |= stp_tx_flags_extra; /* e.g. TCA in reply to a TCN */ stp_tx_flags_extra = 0; @@ -359,7 +377,7 @@ void stp_in(void) __banked stp_state_set(port, 0b01); port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; stp_pflags[port] &= ~STP_PF_OPEREDGE; - stp_tc_count++; + stp_topology_change(port); } return; } @@ -458,7 +476,7 @@ void stp_timers(void) __banked stp_state_set(stp_i, 0b11); print_string("STP: port forwarding "); print_byte(stp_i); write_char('\n'); - stp_tc_count++; + stp_topology_change(stp_i); } else if ((stp_pflags[stp_i] & STP_PF_AUTOEDGE) && stp_bpdu_age[stp_i] > STP_EDGE_DELAY) { /* Auto edge: nothing talks (R)STP on this port - it is @@ -472,6 +490,9 @@ void stp_timers(void) __banked } } + if (stp_tc_while) + stp_tc_while--; + /* Age out a root that went silent: reclaim the tree. */ if (stp_root_port != 0xff && stp_bpdu_age[stp_root_port] > (uint16_t)stp_maxage_s * STP_HZ) { @@ -506,6 +527,7 @@ void stp_defaults(void) __banked stp_tx_budget[stp_i] = 6; } stp_tc_count = 0; + stp_tc_while = 0; stp_claim_root(); } From f1f69e26fdd14885283cff9e02820269defde4f5 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 05:30:57 +0200 Subject: [PATCH 18/68] stp: do not carry a dropped BPDU's flags over to the next one The Topology Change Acknowledgment is staged in a one-shot variable and consumed when the BPDU is built - but stp_cnf_send() can return before that, when the port is filtered/tripped or its tx-hold budget for this second is spent. The flag then survived and was OR-ed into the next BPDU this switch sent, on whatever port that happened to be. Clear it with the frame it belonged to. --- rtl837x_stp.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index bd16597..610dcc7 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -214,10 +214,16 @@ static void stp_claim_root(void) void stp_cnf_send(uint8_t port) __reentrant { - if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & (STP_PF_FILTER | STP_PF_TRIPPED))) + /* A one-shot flag (TCA) belongs to the BPDU we were asked to send: drop + * it with the frame, or it would surface on an unrelated port later. */ + if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & (STP_PF_FILTER | STP_PF_TRIPPED))) { + stp_tx_flags_extra = 0; return; - if (!stp_tx_budget[port]) /* tx hold count exhausted for this second */ + } + if (!stp_tx_budget[port]) { /* tx hold count exhausted for this second */ + stp_tx_flags_extra = 0; return; + } stp_tx_budget[port]--; STP_O->stp_addr[0] = 0x01; STP_O->stp_addr[1] = 0x80; STP_O->stp_addr[2] = 0xc2; From 3c4fb679bda37f6f6557b4b5d2e09810f3332771 Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 05:32:00 +0200 Subject: [PATCH 19/68] stp: correct the timer tick rate (50 Hz, measured) The timers assumed stp_timers() runs at 64 Hz. It does not: the main loop idles on the 200 Hz system tick and calls us every fourth pass, i.e. 50 Hz. Measured on hardware - with hello configured to 2 s the BPDUs left the port 2.560 s apart, exactly the 28 % overshoot the wrong constant implies, and every other timer (forward delay, max age, tx-hold refill) was stretched the same way. Move the constant to the header with the arithmetic spelled out, and use it in the status page too, which had the 64 hardcoded and therefore aged the same counters differently than the engine. --- httpd/page_impl.c | 2 +- rtl837x_stp.c | 2 -- rtl837x_stp.h | 7 +++++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index c0d9e0b..42c3503 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -643,7 +643,7 @@ void send_stp(void) itoa_html(stp_pp2p[pi_i]); /* designated info: a freshly heard BPDU wins, else we are the * segment's designated bridge and report our own values */ - stp_we_root = stp_dbridge[pi_i].mac[5] && stp_bpdu_age[pi_i] < (uint16_t)stp_maxage_s * 64; + stp_we_root = stp_dbridge[pi_i].mac[5] && stp_bpdu_age[pi_i] < (uint16_t)stp_maxage_s * STP_HZ; slen += strtox(outbuf + slen, ",\"db\":\""); if (stp_we_root) { pi_prio = stp_dbridge[pi_i].prio; pi_ext = stp_dbridge[pi_i].ext; diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 610dcc7..be3f79b 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -101,8 +101,6 @@ __xdata uint16_t stp_tc_while; /* ticks left to set the TC flag in our BPDUs */ __xdata uint8_t stp_i; /* shared loop iterator (DSEG relief) */ __xdata uint32_t stp_cost_scratch; -/* stp_timers() runs at ~64 Hz (main loop ~256 Hz / (STP_TICK_DIVIDER+1)) */ -#define STP_HZ 64 #define STP_EDGE_DELAY (3 * STP_HZ) /* auto-edge: forward after 3 s without BPDU */ #define AUTO_COST 20000UL /* path cost used when stp_pcost == 0 (1G default) */ diff --git a/rtl837x_stp.h b/rtl837x_stp.h index 56aea9d..ce09b69 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -9,6 +9,13 @@ void stp_off(void) __banked; void stp_parse(void) __banked __reentrant; /* "stp ..." CLI handler (cmd_parser delegates here) */ void stp_defaults(void) __banked; /* boot init: 802.1D/w default configuration */ +/* Tick rate of stp_timers(): the main loop idles on the 200 Hz system tick + * and rtlplayground.c calls us every (STP_TICK_DIVIDER + 1) = 4th pass. + * Measured on hardware: hello 2 s produced BPDUs exactly 2.560 s apart with + * the previous value of 64, i.e. 20 ms per tick - every configured timer ran + * 28 % long. Shared with the web UI, which ages the same counters. */ +#define STP_HZ 50 + /* Bridge identifier as carried in a BPDU (priority, extension, MAC). */ struct bridge { uint8_t prio; From 6b4d2b9cfa29e3c3f3d816e47bf177ef3e21a82d Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 12:48:31 +0200 Subject: [PATCH 20/68] stp: apply an edge-port change immediately "stp port N edge off" cleared only the admin and auto flags, not the operational one - and that is the flag the engine actually consults: it exempts the port from topology changes and lets it skip the listen period. A port therefore stayed an edge port until the next "stp off" / "stp on", silently ignoring the new setting. Clear it with the others, and mark an admin edge operational right away, as stp_setup() does. --- rtl837x_stp.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index be3f79b..3386688 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -675,9 +675,13 @@ void stp_parse(void) __banked __reentrant if (stpEnabled) stp_state_set(port, 0b11); /* plain forwarding */ } else if (cmd_compare(3, "edge")) { - stp_pflags[port] &= ~(STP_PF_ADMEDGE | STP_PF_AUTOEDGE); + /* Also drop the *operational* edge flag: it is what exempts the + * port from topology changes and lets it skip the listen period, + * so leaving it set would keep the old behaviour until the next + * "stp off"/"stp on". An admin edge is operational immediately. */ + stp_pflags[port] &= ~(STP_PF_ADMEDGE | STP_PF_AUTOEDGE | STP_PF_OPEREDGE); if (cmd_compare(4, "on")) - stp_pflags[port] |= STP_PF_ADMEDGE; + stp_pflags[port] |= STP_PF_ADMEDGE | STP_PF_OPEREDGE; else if (cmd_compare(4, "auto")) stp_pflags[port] |= STP_PF_AUTOEDGE; else if (!cmd_compare(4, "off")) From 293a196c918f7a8b21bd602100be4503002373bc Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 13:38:22 +0200 Subject: [PATCH 21/68] doc: describe the Spanning Tree support Covers enabling it, the bridge and per-port settings, how BPDUs are delivered on this hardware (RMA forward constrained by a CPU-only static L2 entry, since trap-to-CPU targets an external CPU these boards do not have), the timer base, and the management failsafe - enabling STP over the network can block the very port the management VLAN rides on, so that part is spelled out rather than left to be discovered. --- doc/stp.md | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 doc/stp.md diff --git a/doc/stp.md b/doc/stp.md new file mode 100644 index 0000000..a88f9df --- /dev/null +++ b/doc/stp.md @@ -0,0 +1,154 @@ +# Spanning Tree (STP / RSTP) + +The switch can take part in a spanning tree (IEEE 802.1D / 802.1w) so that +redundant links between bridges are blocked instead of forming a loop. The +implementation elects a root bridge from the BPDUs it receives, promotes ports +to forwarding once their listen period expires, ages the root out when it goes +silent, and blocks a port on which it sees its own BPDU. + +It is deliberately simple: there is no proposal/agreement handshake and no full +port-role machine. What it does do reliably is stop a cabling loop from melting +the network, and interoperate with neighbouring bridges as a well-behaved +(if unexciting) participant. + +> **Before you enable it on a switch you reach over the network**: read the +> [management failsafe](#management-failsafe) section. The management VLAN +> rides a port that STP can block. + +## Quick start + +``` +stp on # start participating +stp off # stop, all ports back to forwarding +``` + +Live status is on the Spanning Tree page of the web UI (or `/stp.json`). + +With no other bridge around, the switch elects itself root and every port ends +up forwarding — you can leave it on safely. Put the settings in the startup +config to make them survive a reboot: + +``` +stp prio 15 +stp port 1 edge on +stp on +``` + +## Hardware background + +BPDUs are addressed to `01:80:C2:00:00:00`, a reserved link-local group. The +ASIC's Reserved-Multicast action for that address decides what happens to the +frame. The "trap to CPU" action does **not** reach the internal 8051 on this +hardware — its trap destination is an external CPU attached to a physical port +(`cpuTag_externalCpuPort_set` in the vendor SDK), which these boards do not +have. Delivery therefore uses the *forward* action, constrained to the CPU port +by a static L2 multicast entry (`port_l2mc_set()`), one per VLAN in use: + +* while STP runs, the entry's member mask is the CPU port only — BPDUs reach + the CPU and are not flooded to other ports, as a participating bridge + requires; +* with STP off, the same entries are retargeted to all ports, restoring the + transparency an unmanaged switch is expected to have, so a surrounding + spanning tree can span *through* this device. + +Port states live in `RTL837X_MSTP_STATES (0x5310)`, two bits per port: +`00` disabled, `01` blocking, `10` learning, `11` forwarding. Note that a port +held in blocking also drops frames the CPU injects into it, so a blocked port +cannot transmit BPDUs of its own. + +## Timers + +`stp_timers()` runs at 50 Hz (the main loop idles on the 200 Hz system tick and +STP is called every fourth pass), which is what `STP_HZ` in `rtl837x_stp.h` +encodes. All configured values are in seconds: + +| setting | default | range | +|---|---|---| +| `stp hello ` | 2 | 1–10 | +| `stp maxage ` | 20 | 6–40 | +| `stp fwd ` | 15 | 4–30 | +| `stp txhold ` | 6 | 1–10 | + +A port entering the tree spends `fwd` seconds in blocking before it forwards +(an edge port skips the wait). Root information is discarded after `maxage` +seconds without a BPDU, and the switch then reclaims the root role. + +## Bridge settings + +``` +stp prio <0-15> # bridge priority = n * 4096, default 8 (32768) +stp version rstp|stp # RST BPDUs (default) or legacy Config BPDUs +stp hello|maxage|fwd|txhold +``` + +The bridge with the lowest priority wins the root election; ties are broken by +the MAC address. If you do not want this switch to become the root of an +existing network, give it a worse priority than the current root — `stp prio 15` +(61440) is the usual "never me" value. + +## Per-port settings + +``` +stp port <1-9> on|off # take part in STP, or stay plain forwarding +stp port <1-9> edge on|off|auto # host-facing port handling (default: auto) +stp port <1-9> cost <0-200000000> # path cost, 0 = automatic (20000) +stp port <1-9> prio <0-240> # port priority, steps of 16 +stp port <1-9> guard none|bpdu|root +stp port <1-9> filter on|off # neither send nor accept BPDUs +stp port <1-9> p2p auto|on|off +``` + +**edge** — an edge port goes forwarding immediately and does not trigger a +topology change when it comes and goes; `auto` promotes a port to edge after +three seconds without a BPDU, and demotes it as soon as one arrives. Use +`edge on` for ports where only hosts are attached. + +**guard** — `bpdu` disables a port as soon as a BPDU arrives on it (a host port +should never see one); `root` keeps a port from ever becoming the path to the +root, which protects an existing topology from a newly attached bridge that +claims a better priority. + +**filter** — the port neither sends nor accepts BPDUs. Useful when the device +on the far side reacts badly to them (some unmanaged switches with loop +prevention cut the link) but you still want STP on the rest of the ports. + +## Management failsafe + +Enabling STP on a switch you administer over the network is a genuine risk: the +management VLAN rides a port that STP may put into blocking, and once that +happens the way back is a power cycle. + +The firmware therefore runs a commit-confirm watchdog. While STP is enabled, +any HTTP request re-arms a countdown; if management stays silent for +`stp failsafe ` (default 180, 0 disables it), STP disables itself and +restores forwarding. Keeping the web UI open on the Spanning Tree page is +enough to hold it off, since the page polls for status. + +``` +stp failsafe 180 # seconds of silence before STP gives up (0 = never) +``` + +The status page shows whether the failsafe has tripped since STP was last +enabled. + +## Status + +The Spanning Tree page shows the elected root (priority and MAC), the path cost +to it, the root port, the topology-change counter and, per port, the live state +read from the ASIC together with the configured options. The same data is +available as JSON: + +``` +GET /stp.json +``` + +## Limitations + +* One spanning-tree instance; no MSTP, no per-VLAN trees. +* No proposal/agreement handshake — an RST-capable neighbour will still + converge, but through the timers rather than the fast transition. +* Port roles are approximated: the root port and designated ports are + distinguished, alternate/backup are not. +* A port in blocking cannot transmit, so a blocked port stops announcing + itself; recovery relies on the listen timer rather than on a neighbour's + agreement. From 7a790b32b1215dc0f228102bb59f4a130be4c8cf Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 4 Aug 2026 13:38:53 +0200 Subject: [PATCH 22/68] README: STP is no longer missing Point at doc/stp.md instead, with the caveat that the implementation is a simplified one and that enabling it remotely deserves a read first. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e149058..acf787e 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ only the following features are provided: GUI While the firmware provides already considerable improvements over the original managed firmware, -the firmware still lacks support for STP and the proprietary loop prevention -protocols as well as DHCP. If you need these features, do not install the playground on your managed +the firmware still lacks support for the proprietary loop prevention +protocols as well as DHCP. Spanning Tree is available (see doc/stp.md), but is +a simplified implementation - read that document before enabling it on a +switch you administer over the network. If you need these features, do not install the playground on your managed devices. In any case, installation is strongly discouraged unless you can at least make a backup of the original flash content via a SOIC clamp such as also used for BIOS backups and can re-install that firmware in case something is wrong. For this no soldering From f3c1b7f3ac93c1f9c3bd723b8d7841398863b21b Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 5 Aug 2026 02:17:39 +0200 Subject: [PATCH 23/68] stp: react to a port losing carrier The state machine never looked at link state, so a port whose cable was pulled stayed in forwarding: it kept being announced, kept its learned entries, and the most ordinary topology change there is went unnoticed. Observed on hardware - a non-edge port with the link administratively down still reported forwarding and left the topology-change counter at zero for the whole observation window. Sample the carrier bitmap once per second, alongside the tx-budget refill (the 50 Hz tick has no business doing register reads). On carrier loss put the port back to blocking and run the normal topology-change path, which flushes just that port's entries. On carrier return re-run the listen period rather than forwarding immediately - the segment may have been rewired while we were down - and clear the operational edge flag so a port that was auto-edged has to earn it again. stp_setup() seeds the bitmap from the hardware so enabling STP does not report every already-down port as a fresh topology change. --- rtl837x_stp.c | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 3386688..7a02f15 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -91,6 +91,8 @@ __xdata uint16_t port_hello[10]; /* hello TX countdown */ __xdata uint16_t stp_bpdu_age[10]; /* ticks since last BPDU seen on port (saturating) */ __xdata uint8_t stp_tx_budget[10]; /* tx hold: BPDUs left in the current second */ __xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */ +__xdata uint16_t stp_link_prev; /* carrier bitmap as of the last check */ +__xdata uint16_t stp_link_now; /* Scratch (8051: locals would overflow the internal-RAM overlay area) */ __xdata uint8_t stp_scratch; @@ -450,6 +452,39 @@ void stp_timers(void) __banked stp_failsafe_tripped = 1; return; } + + /* Link supervision. Without this the state machine never learns + * that a port lost carrier: it keeps the port in forwarding, keeps + * announcing on it, and never flushes what was learned behind it - + * yet losing a link is the most ordinary topology change there is. + * Once per second is soon enough, and it keeps register reads out + * of the 50 Hz tick. */ + reg_read_m(RTL837X_REG_LINKS_STS); + stp_link_now = (uint16_t)sfr_data[1] | ((uint16_t)sfr_data[2] << 8); + if (stp_link_now != stp_link_prev) { + for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { + if (!(stp_pflags[stp_i] & STP_PF_ENABLED)) + continue; + if (!((stp_link_now ^ stp_link_prev) >> stp_i & 1)) + continue; + /* Either way the port must stop forwarding first. */ + stp_state_set(stp_i, 0b01); + if ((stp_link_now >> stp_i) & 1) { + /* Carrier back: re-run the listen period rather than + * forwarding straight away - the segment may have been + * rewired while we were down. Auto edge still applies. */ + port_timers[stp_i] = (uint16_t)stp_fwddelay_s * STP_HZ; + stp_pflags[stp_i] &= ~STP_PF_OPEREDGE; + stp_bpdu_age[stp_i] = 0; + } else { + port_timers[stp_i] = 0; + print_string("STP: link down, port blocking "); + print_byte(stp_i); write_char('\n'); + stp_topology_change(stp_i); + } + } + stp_link_prev = stp_link_now; + } } for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { @@ -595,6 +630,11 @@ void stp_setup(void) __banked print_reg(RTL837X_MSTP_STATES); write_char('\n'); + /* Seed the carrier bitmap, so turning STP on does not report every + * port that was already down as a fresh topology change. */ + reg_read_m(RTL837X_REG_LINKS_STS); + stp_link_prev = (uint16_t)sfr_data[1] | ((uint16_t)sfr_data[2] << 8); + stp_claim_root(); /* Take BPDUs to the CPU only - we are a participating bridge now. */ From be520d67160e8ef7522e7c9b95762bfdfbd941aa Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 5 Aug 2026 03:43:54 +0200 Subject: [PATCH 24/68] stp: accept BPDUs with a protocol version above 2 We only recognised RST BPDUs when the Protocol Version Identifier was exactly 2, which silently drops every MST BPDU: 802.1s uses version 3 with type 2 and a prefix deliberately laid out to be identical to an RST BPDU, precisely so that an RSTP bridge can parse it. 802.1D-2004 14.4 spells the rule out - a bridge shall accept a version identifier of 2 or greater and treat the BPDU as RST, ignoring anything beyond what it understands. Compare with >= instead of ==. The receive path already length-checks before touching the body and only reads the fields common to both formats, so a longer MST body needs no other care. The two fields are deliberately asymmetric: the Protocol Identifier must be exactly zero (it is a sanity check), while the version is an extension point that has to tolerate the future. --- rtl837x_stp.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 7a02f15..7cdbfc3 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -337,8 +337,13 @@ void stp_in(void) __banked if (STP_I->proto) return; /* Accept RSTP BPDUs (v2 type 2), legacy Config BPDUs (v0 type 0) and - * legacy TCN BPDUs (v0 type 0x80, 4-byte body) */ - if (!((STP_I->version == 2 && STP_I->bpdu_type == 2) + * legacy TCN BPDUs (v0 type 0x80, 4-byte body). + * Version 2 *or greater*: 802.1D-2004 14.4 requires an RSTP bridge to + * accept a higher Protocol Version and treat it as RST, ignoring what + * it does not understand. MSTP (802.1s) sends version 3 type 2 with a + * prefix deliberately identical to an RST BPDU for exactly this reason; + * insisting on == 2 makes us blind to every MST bridge on the segment. */ + if (!((STP_I->version >= 2 && STP_I->bpdu_type == 2) || (STP_I->version == 0 && (STP_I->bpdu_type == 0 || STP_I->bpdu_type == 0x80)))) return; From 4b5bf09c831fd01fb810b04d8fe614155e562e3b Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 5 Aug 2026 18:44:12 +0200 Subject: [PATCH 25/68] doc: separate the trap action from CPU-port delivery The wording read as a claim about the CPU interface in general, which is wrong and misleading: the 8051 sits behind an ordinary port of the internal switch and is an ordinary member of a forwarding mask - which is exactly what this implementation relies on. Say what is actually broken instead: the trap action, a separate mechanism whose destination is an external CPU port these boards do not populate. Record the measurements behind it, including the widened CPU_PMSK and both external-CPU destinations, and add the ACL trap result - a rule matching the group intercepts frames but does not deliver them either, which is a second, independent path to the same conclusion. Reported-by: vDorst --- doc/stp.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/doc/stp.md b/doc/stp.md index a88f9df..d3d2028 100644 --- a/doc/stp.md +++ b/doc/stp.md @@ -38,10 +38,23 @@ stp on BPDUs are addressed to `01:80:C2:00:00:00`, a reserved link-local group. The ASIC's Reserved-Multicast action for that address decides what happens to the -frame. The "trap to CPU" action does **not** reach the internal 8051 on this -hardware — its trap destination is an external CPU attached to a physical port -(`cpuTag_externalCpuPort_set` in the vendor SDK), which these boards do not -have. Delivery therefore uses the *forward* action, constrained to the CPU port +frame. + +Forwarding to the CPU port works normally — the 8051 sits behind an ordinary +port of the internal switch and is an ordinary member of a forwarding mask. +What does not work is the *trap* action, which is a separate mechanism: its +destination is an external CPU attached to a physical port +(`cpuTag_externalCpuPort_set`, `EXT_CPU_CTRL` in the vendor SDK), which these +boards do not populate. Measured on a SWTGW218AS: with the RMA action set to +trap, zero frames arrive at the 8051, including with `CPU_PMSK` widened and the +external-CPU destination pointed at both `0xF` and `9`; with the *forward* +action plus the L2 entry below, they arrive. The ACL trap behaves the same way, +measured on the neighbouring reserved group `01:80:C2:00:00:02`: a rule matching +it intercepts the frames — the LACP receive counters stop advancing while the +rule is enabled and resume the moment it is disabled — but they never reach the +8051, with `FWD_INT_TRAP` and with `REDIRECT` aimed at the CPU port alike. + +Delivery therefore uses the *forward* action, constrained to the CPU port by a static L2 multicast entry (`port_l2mc_set()`), one per VLAN in use: * while STP runs, the entry's member mask is the CPU port only — BPDUs reach From 0ce5fb4af38bdaed95ac6a1f7e25a28179370953 Mon Sep 17 00:00:00 2001 From: d00f Date: Thu, 6 Aug 2026 15:06:19 +0200 Subject: [PATCH 26/68] stp: latch the block on a looped port Two of our own ports on one segment blocked each other in turn instead of settling. The guard on the loop path only acted when port_timers[] had already run out, so a BPDU arriving while the port was blocked did nothing: the timer expired, the port went forwarding, the loop reopened and the pair started over. The comment above the code claimed the opposite - "if the loop persists the BPDUs keep arriving and the port stays blocked" - but nothing implemented it. Measured on a SWTGW218AS with a patch cord between two free ports: both ports blocked, both returned to forwarding one forward delay later, and the topology-change counter reached 0x51 in 5.5 minutes - 15.6 changes per minute for as long as the cable was in. Let the better Port ID decide for both. That port is forwarding by construction, so it goes on hearing the loop and re-arms the other port's timer on every BPDU, which is what turns the block into a latch; the held port only has to keep transmitting, which the send path already allows in any MSTP state. Nothing here depends on a blocked port still receiving - that was never established. Having one writer also removes a race: while both ends decided for themselves, the winner's re-arm could land in the loser's port_timers[] first, the loser read it as "already blocked" and skipped its own state change, and the loop stayed open. 802.1D compares the priority before the number and stp_cnf_send() puts stp_pprio[] on the wire next to it, so compare that first - otherwise "stp port N prio" would quietly not influence which end of a looped pair keeps forwarding. The port number arrives in a frame and our bridge MAC is public in every BPDU we send, so bound it to the ports this module manages before indexing anything. Outside that range nothing would release the block either: stp_timers() walks min_port..max_port and skips ports that are not STP-enabled, so their port_timers[] never counts down. Equal Port IDs mean the frame came back on the port it left - a loop further out, behind an unmanaged switch. There is no pair to choose from, so that port holds itself down; since it can only re-arm while it is receiving, that case stays the forward-delay pulse it was before rather than becoming a real latch. The work sits in a __reentrant helper on purpose, like the two functions above it: parameters and locals then live on the stack. Inlined into stp_in(), which is __banked and whose temporaries cannot be overlaid, the same code costs two more bytes of DSEG - enough to stop an image that also carries LACP from linking at all. Verified on hardware: with the loop in place for 1 h 36 min exactly one port blocked, the other kept forwarding, and the topology-change counter moved four times in total - three of them the link event and the promotion in the first minute. --- rtl837x_stp.c | 97 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 12 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 7cdbfc3..1ce1a2a 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -10,8 +10,9 @@ * The engine itself stays deliberately simple (no proposal/agreement * handshake, no full port-role machine): we elect a root from received BPDUs, * promote ports to forwarding after the listen period (or immediately for - * edge ports), age the root out via max age, and block ports on which we see - * our own BPDU (loop!) or - with root guard - a better root. + * edge ports), age the root out via max age, and block on a loop (our own + * BPDU comes back: the worse Port ID of the pair stops forwarding and stays + * blocked while it keeps coming back) or - with root guard - a better root. */ // #define REGDBG @@ -102,6 +103,7 @@ __xdata uint8_t stp_msg_age; /* message age of the root info we hold, seconds __xdata uint16_t stp_tc_while; /* ticks left to set the TC flag in our BPDUs */ __xdata uint8_t stp_i; /* shared loop iterator (DSEG relief) */ __xdata uint32_t stp_cost_scratch; +__xdata uint8_t stp_loop_peer; /* the other own port seen on a looped segment */ #define STP_EDGE_DELAY (3 * STP_HZ) /* auto-edge: forward after 3 s without BPDU */ @@ -200,6 +202,43 @@ static void stp_topology_change(uint8_t port) __reentrant } +/* Hold one port out of forwarding because a loop was seen on it, and keep + * holding it for as long as the caller keeps saying so. The caller is the + * port that won the Port ID compare (see stp_in) - a different port than + * the one held, except when the frame came back on the port it left. + * + * Reentrant on purpose, like the two above: parameters and locals then live + * on the stack instead of taking internal RAM of their own. Inlined into + * stp_in - which is __banked, so its temporaries cannot be overlaid - the + * same code costs two more bytes of DSEG, and that is enough to stop the + * image with LACP from linking at all. */ +static void stp_loop_hold_peer(uint8_t port) __reentrant +{ + /* The port number arrives in a BPDU, so it is somebody else's data, + * and our own bridge MAC is public in every BPDU we send - a forged + * frame can name any port it likes. Bound it to the ports this module + * actually manages, like every other loop here does. Out of that + * range nothing would ever release the block either: stp_timers() + * walks min_port..max_port and skips ports that are not STP-enabled, + * so their port_timers[] never counts down. Naming the CPU port would + * otherwise cost us our own management path. */ + if (port < machine.min_port || port > machine.max_port) + return; + if (!(stp_pflags[port] & STP_PF_ENABLED)) + return; + if (stp_pflags[port] & STP_PF_TRIPPED) + return; + if (!port_timers[port]) { /* not held down yet */ + print_string("STP: loop detected, blocking port "); + print_byte(port); write_char('\n'); + stp_state_set(port, 0b01); + stp_pflags[port] &= ~STP_PF_OPEREDGE; + stp_topology_change(port); + } + port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; +} + + /* Take the bridge back as root of its own tree (initial state / root aged out) */ static void stp_claim_root(void) { @@ -378,18 +417,52 @@ void stp_in(void) __banked if (stp_rxlen < 64) return; - /* Our own BPDU coming back at us = a loop in the network. Block the port - * for a listen period; if the loop persists the BPDUs keep arriving and - * the port stays blocked. */ + /* Our own BPDU coming back at us: two of our ports sit on one segment. + * Only the one with the worse Port ID has to stop forwarding - 802.1D + * calls it a backup port. Blocking both, as we used to, kills a segment + * that can still carry traffic, and worse, leaves nobody forwarding to + * hear the loop: both then time out of blocking together and the pair + * oscillates for as long as the cable is in (measured: a topology change + * every ~4 s). + * + * The port with the better Port ID decides for both and is the only one + * that touches state - the other just drops the frame. One writer is + * what makes this safe: while both were still deciding for themselves, + * the winner's re-arm landed in the loser's port_timers[] first, the + * loser then read it as "already blocked" and skipped its own + * stp_state_set(), and the loop stayed open. Whether that happened came + * down to which frame the switch handed us first. + * + * The winner is forwarding by construction (nothing here ever blocks + * it), so it goes on hearing the loop and re-arms the loser's timer on + * every BPDU - that is what makes the block a latch rather than a + * forward-delay pulse, and it needs no assumption about a blocked port + * still receiving. Pull the cable and the re-arming stops, so the loser + * comes back on its own after a forward delay - and the link + * supervision above gets there first anyway. */ if (cmpMAC(STP_I->bridge.mac, uip_ethaddr.addr) == 0) { - if (port_timers[port] == 0 && !(stp_pflags[port] & STP_PF_TRIPPED)) { - print_string("STP: loop detected on port "); - print_byte(port); write_char('\n'); - stp_state_set(port, 0b01); - port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; - stp_pflags[port] &= ~STP_PF_OPEREDGE; - stp_topology_change(port); + /* Equal means the frame came back on the port it left: a loop + * further out, behind an unmanaged switch. There is no pair to + * pick from, so that port holds itself down - and since it can + * only re-arm while it is receiving, that case degrades to the + * forward-delay pulse we had before rather than a real latch. + * The peer's number is validated by the callee, not here. */ + stp_loop_peer = STP_I->port_id; /* 1-based, as we send it */ + if (!stp_loop_peer) + return; + stp_loop_peer--; + /* A Port ID is (priority, number) and priority is compared + * first - stp_cnf_send() puts stp_pprio[] on the wire next to + * the number, so "stp port N prio" has to be able to decide + * which end of a looped pair keeps forwarding. Comparing the + * number alone would quietly ignore it. */ + if (STP_I->port_prio != stp_pprio[port]) { + if (STP_I->port_prio < stp_pprio[port]) + return; /* peer is better: it decides */ + } else if (stp_loop_peer < port) { + return; } + stp_loop_hold_peer(stp_loop_peer); return; } From 2cf60e177b4cfc13a2b9164e23c419445e459bd1 Mon Sep 17 00:00:00 2001 From: d00f Date: Sat, 8 Aug 2026 05:04:42 +0200 Subject: [PATCH 27/68] stp: derive the BPDU flags from the port state Every RST BPDU we sent carried flags 0x3c - designated, learning, forwarding - whatever the port was actually doing. A blocked port kept announcing itself as forwarding, and the root port would have called itself designated. Nothing on this bench acted on it, but it is a lie in the protocol frame and the kind that surfaces in somebody else's mixed network. Derive the flags instead: the root port reports the root role, every other transmitting port is designated (alternates do not transmit at all), and the learning and forwarding bits mirror the ASIC state, so a listening port now sends 0x0c. TC and TCA stay dynamic as before. Legacy Config BPDUs are unchanged - their flags only ever carried TC and TCA. Costs nothing in internal RAM; the state comes from the register scratch that is already there. --- rtl837x_stp.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 1ce1a2a..8999e3b 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -289,8 +289,20 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->msg_len = HTONS(0x27); STP_O->version = 0x02; /* RSTP */ STP_O->bpdu_type = 0x02; /* Rapid Spanning Tree BPDU */ - /* flags: role designated (0b11 << 2) + learning + forwarding */ - STP_O->flags = 0x3c; + /* Flags describe this port, so derive them instead of announcing + * designated+learning+forwarding unconditionally: a blocked port + * claiming to forward, or the root port claiming designated, is a + * lie on the wire even when nothing downstream acts on it (yet). + * Role is root on the root port and designated everywhere else - + * there is no alternate/backup role computation, so a port blocked + * by loop detection still transmits as designated, just with the + * learning and forwarding bits clear. Those two mirror the ASIC + * state (0b11 = forwarding); a listening or blocked port sends + * neither. */ + reg_read_m(RTL837X_MSTP_STATES); + STP_O->flags = (uint8_t)((port == stp_root_port ? 0b10 : 0b11) << 2); + if (((sfr_data[3 - (port >> 2)] >> ((port << 1) & 0x7)) & 0b11) == 0b11) + STP_O->flags |= 0x30; /* learning + forwarding */ } else { /* 802.3 length = LLC (3) + Config BPDU body (35) */ STP_O->msg_len = HTONS(0x26); From 7ebb420e7d7fdc225c0183d22c4ff82801c156de Mon Sep 17 00:00:00 2001 From: d00f Date: Sat, 8 Aug 2026 18:49:51 +0200 Subject: [PATCH 28/68] stp: warn when an enabled port cannot receive BPDUs A port set to admit tagged frames only will never see a BPDU, because delivery rides the forward action and the ingress pipeline drops untagged frames before the L2 lookup. The failure is silent and looks like a dead receive path: the port turns edge after three seconds, the bridge elects itself root, and nothing hints at the ingress setting. Diagnosing exactly that cost most of a day on a live switch, with the neighbour provably transmitting the whole time. stp_setup() now prints one line per affected port, so the hint lands at "stp on" and at every config replay on boot. The check runs in its own loop after the MSTP write: port_ingress_filter_get() reads a register into sfr_data, which the state-building loop above is still using. The port number in the message is physical, matching what the ingress command takes. doc/stp.md explains why this can happen here and not on a normal bridge, where BPDUs are consumed before any VLAN classification. stp.rel stays at DSEG 5 with no OSEG and the image at 10498 bytes of XDATA. --- doc/stp.md | 11 +++++++++++ rtl837x_port.h | 1 + rtl837x_stp.c | 10 ++++++++++ 3 files changed, 22 insertions(+) diff --git a/doc/stp.md b/doc/stp.md index d3d2028..baf1496 100644 --- a/doc/stp.md +++ b/doc/stp.md @@ -64,6 +64,17 @@ by a static L2 multicast entry (`port_l2mc_set()`), one per VLAN in use: transparency an unmanaged switch is expected to have, so a surrounding spanning tree can span *through* this device. +Because delivery rides the forward action, a BPDU is an ordinary frame to the +ingress pipeline and is subject to the port's acceptable-frame-type setting. +BPDUs are untagged by definition, so a port configured to admit tagged frames +only (`ingress t`) will never deliver one: a port left on the default +auto edge turns edge after three seconds of silence, one with edge switched off +sits out the full forward delay instead, and either way the bridge elects +itself root no matter what the neighbour sends. `stp_setup()` prints a warning for every +STP-enabled port in that state. On a normal bridge this cannot happen, since +BPDUs are consumed before any VLAN classification; here it is a direct +consequence of the delivery path above. + Port states live in `RTL837X_MSTP_STATES (0x5310)`, two bits per port: `00` disabled, `01` blocking, `10` learning, `11` forwarding. Note that a port held in blocking also drops frames the CPU injects into it, so a blocked port diff --git a/rtl837x_port.h b/rtl837x_port.h index 73cc0bf..56397ee 100644 --- a/rtl837x_port.h +++ b/rtl837x_port.h @@ -75,6 +75,7 @@ void port_eee_status(uint8_t port) __banked; void print_port_ingress_filter_mode(vlan_ingress_mode_t mode) __banked; bool port_ingress_vlan_filter_set(__xdata uint8_t port, __xdata bool enabled) __banked; bool port_ingress_vlan_filter_get(__xdata uint8_t port) __banked; +vlan_ingress_mode_t port_ingress_filter_get(__xdata uint8_t port) __banked; void port_isolate(register uint8_t port, __xdata uint16_t pmask) __banked; uint16_t port_isolation_get(register uint8_t port) __banked; diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 8999e3b..c333291 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -720,6 +720,16 @@ void stp_setup(void) __banked print_reg(RTL837X_MSTP_STATES); write_char('\n'); + for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { + if (!(stp_pflags[stp_i] & STP_PF_ENABLED)) + continue; + if (port_ingress_filter_get(stp_i) != VLAN_TAGGED) + continue; + print_string("STP: port "); + write_char('0' + machine.log_to_phys_port[stp_i]); + print_string(" admits tagged frames only - BPDUs are untagged and will not arrive\n"); + } + /* Seed the carrier bitmap, so turning STP on does not report every * port that was already down as a fresh topology change. */ reg_read_m(RTL837X_REG_LINKS_STS); From c72d36af36fbfce8639d33d7bfe45a3e4446eb1f Mon Sep 17 00:00:00 2001 From: d00f Date: Sat, 8 Aug 2026 19:17:56 +0200 Subject: [PATCH 29/68] stp: turn the management failsafe into a one-shot window The failsafe used to watch management traffic for as long as STP ran, so three minutes of nobody looking at the web UI took the tree down on any quiet network. That made a standing STP config impractical, which is the problem raised in the review of the original PR. Enabling STP arms a window of stp_failsafe_s seconds. One HTTP request inside it confirms that management survived the new tree and disarms the watchdog; a silent window disables STP and restores forwarding. Both outcomes print to the console and the syslog. The window re-arms on any later event that newly takes a port out of forwarding: a port rejoining via "stp port N on", root guard firing, the loop latch. Those were covered by the old always-on surveillance and a disarmed window would have left them able to cut management off for good. If management traffic keeps flowing past the new block, the next request confirms straight away, which is the correct verdict, the block did not cut it. The arming deliberately does not refresh an already armed window: root guard can re-fire on every hello, and refreshing the countdown on each one would keep a cut-off window from ever expiring. A stable network with nothing newly blocked never re-arms, which is the reviewed-for behaviour. The request or console command that causes the arming never counts as its own confirmation: mgmt_alive is cleared when a command arms, and the console hook only disarms when the window predates the command. Without that, enabling from the web UI or the console would confirm the window before the new tree had any chance to cut management off. A command on the serial console confirms like HTTP does. An operator at the console has out-of-band access that no tree can cut, so the automatic restore only takes STP away from someone equipped to deal with the situation. The hook sits on the interactive console path only, identified by cmd_available, so neither the config replay at boot nor HTTP commands pass through it. After a confirmation STP runs unsupervised until something new blocks. Headless installs where nobody will confirm should set stp failsafe 0; doc/stp.md says so. Costs two bytes of XDATA, the armed flag and the console-path snapshot; stp.rel and rtlplayground.rel keep their segment sizes. --- doc/stp.md | 28 ++++++++++++++----- rtl837x_stp.c | 71 ++++++++++++++++++++++++++++++++----------------- rtlplayground.c | 7 +++++ 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/doc/stp.md b/doc/stp.md index baf1496..1c31d2d 100644 --- a/doc/stp.md +++ b/doc/stp.md @@ -142,16 +142,32 @@ Enabling STP on a switch you administer over the network is a genuine risk: the management VLAN rides a port that STP may put into blocking, and once that happens the way back is a power cycle. -The firmware therefore runs a commit-confirm watchdog. While STP is enabled, -any HTTP request re-arms a countdown; if management stays silent for -`stp failsafe ` (default 180, 0 disables it), STP disables itself and -restores forwarding. Keeping the web UI open on the Spanning Tree page is -enough to hold it off, since the page polls for status. +The firmware therefore runs a commit-confirm watchdog. Enabling STP, by hand or +from the startup config, arms a one-shot window of `stp failsafe ` +(default 180). One HTTP request inside the window confirms that management +survived the new tree and disarms the watchdog until the next enable; a window +with no management activity disables STP and restores forwarding. After the +confirmation STP runs unsupervised, so a quiet network no longer loses its +tree to three minutes of nobody looking at the web UI. ``` -stp failsafe 180 # seconds of silence before STP gives up (0 = never) +stp failsafe 180 # length of the armed window after enabling (0 = never armed) ``` +Any later event that newly takes a port out of forwarding arms the window +again: a port rejoining via `stp port on`, root guard firing, the loop +latch. If management traffic keeps flowing past the new block, the very next +request confirms and disarms; if the block cut it, the silent window restores +forwarding as above. A stable network with nothing newly blocked never re-arms. + +A command executed on the serial console also confirms, on the grounds that an +operator with out-of-band access does not need the automatic restore; the +command that enabled STP does not count, only activity after it. + +A headless switch that nobody confirms over HTTP should set `stp failsafe 0`, +otherwise a reboot with STP in the startup config disables it again three +minutes later. Setting a new value while STP runs arms a fresh window. + The status page shows whether the failsafe has tripped since STP was last enabled. diff --git a/rtl837x_stp.c b/rtl837x_stp.c index c333291..b67f230 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -62,13 +62,15 @@ __xdata uint8_t stp_fwddelay_s; __xdata uint8_t stp_rstp; __xdata uint8_t stp_txhold; -/* Management failsafe: if any port is held out of Forwarding while no HTTP - * request has been seen for stp_failsafe_s seconds, assume STP just cut off - * in-band management (mgmt VLAN rides a blockable front port!) and disable - * itself, restoring forwarding. Commit-confirm pattern; hardware lockout of - * 2026-07-20 is the motivating incident. 0 disables the watchdog. */ +/* Management failsafe, commit-confirm: enabling STP arms a one-shot window of + * stp_failsafe_s seconds. One HTTP request inside the window confirms that + * management survived the new tree and disarms it until the next enable; a + * window with no management activity disables STP and restores forwarding + * (mgmt VLAN rides a blockable front port; lockout of 2026-07-20 is the + * motivating incident). 0 never arms. Headless installs should set 0. */ __xdata uint8_t stp_failsafe_s; -__xdata uint8_t stp_failsafe_cnt; /* seconds left before the trip */ +__xdata uint8_t stp_failsafe_cnt; /* seconds left of the armed window */ +__xdata uint8_t stp_failsafe_armed; __xdata uint8_t stp_failsafe_tripped; extern volatile __xdata uint8_t mgmt_alive; /* set by httpd on any request */ @@ -232,6 +234,10 @@ static void stp_loop_hold_peer(uint8_t port) __reentrant print_string("STP: loop detected, blocking port "); print_byte(port); write_char('\n'); stp_state_set(port, 0b01); + if (stp_failsafe_s && !stp_failsafe_armed) { + stp_failsafe_armed = 1; + stp_failsafe_cnt = stp_failsafe_s; + } stp_pflags[port] &= ~STP_PF_OPEREDGE; stp_topology_change(port); } @@ -486,6 +492,10 @@ void stp_in(void) __banked print_string("STP: root guard blocking port "); print_byte(port); write_char('\n'); stp_state_set(port, 0b01); + if (stp_failsafe_s && !stp_failsafe_armed) { + stp_failsafe_armed = 1; + stp_failsafe_cnt = stp_failsafe_s; + } port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; stp_pflags[port] &= ~STP_PF_OPEREDGE; return; @@ -523,25 +533,28 @@ void stp_timers(void) __banked for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) stp_tx_budget[stp_i] = stp_txhold; - /* Management failsafe: plain commit-confirm. While STP is on, ANY - * HTTP request re-arms the countdown (the web UI polls /stp.json - * every 2 s, so an open browser keeps it alive); stp_failsafe_s - * seconds of management silence disable STP and restore the - * pre-STP state. Deliberately NOT conditioned on our own MSTP - * states: hardware incident 2026-07-21 showed a NEIGHBOR (TP-Link - * Easy Smart loop prevention) cutting our uplink in reaction to - * our BPDUs while our ASIC was all-forwarding - only going fully + /* Management failsafe: armed as a one-shot window by "stp on". + * The first HTTP request inside the window proves management + * survived the new tree and disarms it; a silent window disables + * STP. Deliberately NOT conditioned on our own MSTP states: + * hardware incident 2026-07-21 showed a NEIGHBOR (TP-Link Easy + * Smart loop prevention) cutting our uplink in reaction to our + * BPDUs while our ASIC was all-forwarding - only going fully * quiet (no BPDU TX) lets such a neighbor recover. */ - if (mgmt_alive) { - mgmt_alive = 0; - stp_failsafe_cnt = stp_failsafe_s; - } else if (stp_failsafe_s && stp_failsafe_cnt && --stp_failsafe_cnt == 0) { - print_string("STP failsafe: no management activity - disabling STP\n"); - stp_off(); - stpEnabled = 0; - stp_failsafe_tripped = 1; - return; + if (stp_failsafe_armed) { + if (mgmt_alive) { + stp_failsafe_armed = 0; + print_string("STP failsafe: management confirmed - disarmed\n"); + } else if (--stp_failsafe_cnt == 0) { + print_string("STP failsafe: no management activity - disabling STP\n"); + stp_failsafe_armed = 0; + stp_off(); + stpEnabled = 0; + stp_failsafe_tripped = 1; + return; + } } + mgmt_alive = 0; /* Link supervision. Without this the state machine never learns * that a port lost carrier: it keeps the port in forwarding, keeps @@ -778,6 +791,8 @@ void stp_parse(void) __banked __reentrant print_string("STP enabled\n"); stp_failsafe_tripped = 0; stp_failsafe_cnt = stp_failsafe_s; + stp_failsafe_armed = stp_failsafe_s ? 1 : 0; + mgmt_alive = 0; stpEnabled = 1; stp_setup(); return; @@ -786,6 +801,7 @@ void stp_parse(void) __banked __reentrant print_string("STP disabled\n"); stp_off(); stpEnabled = 0; + stp_failsafe_armed = 0; return; } if (cmd_words_len < 3) @@ -809,6 +825,11 @@ void stp_parse(void) __banked __reentrant if (stpEnabled) { /* (re)join: listen first */ stp_state_set(port, 0b01); port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; + if (stp_failsafe_s) { + stp_failsafe_armed = 1; + stp_failsafe_cnt = stp_failsafe_s; + mgmt_alive = 0; + } } } else if (cmd_compare(3, "off")) { stp_pflags[port] &= ~STP_PF_ENABLED; @@ -911,9 +932,11 @@ void stp_parse(void) __banked __reentrant goto err; stp_txhold = stp_scratch; } else if (cmd_compare(1, "failsafe")) { - /* 0 disables the management watchdog; otherwise seconds to trip */ + /* 0 never arms; otherwise the length of the armed window */ stp_failsafe_s = stp_scratch; stp_failsafe_cnt = stp_scratch; + stp_failsafe_armed = (stp_scratch && stpEnabled) ? 1 : 0; + mgmt_alive = 0; } else { goto err; } diff --git a/rtlplayground.c b/rtlplayground.c index 6e2d9df..3b2d6b5 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -122,6 +122,8 @@ __xdata uint8_t tx_seq; __xdata uint8_t stpEnabled; __xdata uint8_t igmpEnabled; +extern __xdata uint8_t stp_failsafe_armed; +__xdata uint8_t fs_was_armed; __xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ __code uint16_t bit_mask[16] = { @@ -1518,9 +1520,14 @@ void idle(void) // Check whether a command is waiting in the cmd_buffer and execute if (cmd_available) { cmd_available = 0; + fs_was_armed = stp_failsafe_armed; cmd_tokenize(); if (err_status == ERR_OK) cmd_parser(); + if (fs_was_armed && stp_failsafe_armed) { + stp_failsafe_armed = 0; + print_string("STP failsafe: console activity - disarmed\n"); + } print_cmd_prompt(); } } From 0160b430f489d5f949b6afa13d7fe0ad3a0a28aa Mon Sep 17 00:00:00 2001 From: d00f Date: Mon, 10 Aug 2026 23:50:26 +0200 Subject: [PATCH 30/68] doc: correct what a blocked port does with frames The note said a blocked port drops frames the CPU injects into it and so cannot send BPDUs of its own. Hardware says otherwise, and it matters, because that sentence is the reason one would go looking for a way to let control frames out of a blocked port when there is nothing to fix there. Held a two port group in blocking and watched from the neighbour. Our BPDUs kept leaving it, 27 of them with a largest gap of 2.00 s, which is the hello interval with nothing missed. Pings across the same port stopped dead for 11.63 s in one unbroken gap, so data really is held. In the same window the neighbour sent 74 frames with a largest gap of 1.04 s while our receive counter for them moved by 2, and the port stayed in its trunk throughout, so nothing in the aggregation code was discarding them. --- doc/stp.md | 58 +++++++++++++++++++++--------------------------------- 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/doc/stp.md b/doc/stp.md index 1c31d2d..f96fb2b 100644 --- a/doc/stp.md +++ b/doc/stp.md @@ -6,10 +6,8 @@ implementation elects a root bridge from the BPDUs it receives, promotes ports to forwarding once their listen period expires, ages the root out when it goes silent, and blocks a port on which it sees its own BPDU. -It is deliberately simple: there is no proposal/agreement handshake and no full -port-role machine. What it does do reliably is stop a cabling loop from melting -the network, and interoperate with neighbouring bridges as a well-behaved -(if unexciting) participant. +STP can be enabled and controlled via the web interface or the command line, +as follows: > **Before you enable it on a switch you reach over the network**: read the > [management failsafe](#management-failsafe) section. The management VLAN @@ -22,7 +20,8 @@ stp on # start participating stp off # stop, all ports back to forwarding ``` -Live status is on the Spanning Tree page of the web UI (or `/stp.json`). +Live status is on the Spanning Tree page of the web UI (or `/stp.json`), +and on the serial console via `stp status`. With no other bridge around, the switch elects itself root and every port ends up forwarding — you can leave it on safely. Put the settings in the startup @@ -40,19 +39,12 @@ BPDUs are addressed to `01:80:C2:00:00:00`, a reserved link-local group. The ASIC's Reserved-Multicast action for that address decides what happens to the frame. -Forwarding to the CPU port works normally — the 8051 sits behind an ordinary +Forwarding to the CPU port works normally: the 8051 sits behind an ordinary port of the internal switch and is an ordinary member of a forwarding mask. -What does not work is the *trap* action, which is a separate mechanism: its -destination is an external CPU attached to a physical port -(`cpuTag_externalCpuPort_set`, `EXT_CPU_CTRL` in the vendor SDK), which these -boards do not populate. Measured on a SWTGW218AS: with the RMA action set to -trap, zero frames arrive at the 8051, including with `CPU_PMSK` widened and the -external-CPU destination pointed at both `0xF` and `9`; with the *forward* -action plus the L2 entry below, they arrive. The ACL trap behaves the same way, -measured on the neighbouring reserved group `01:80:C2:00:00:02`: a rule matching -it intercepts the frames — the LACP receive counters stop advancing while the -rule is enabled and resume the moment it is disabled — but they never reach the -8051, with `FWD_INT_TRAP` and with `REDIRECT` aimed at the CPU port alike. +The *trap* action does not deliver to it. Its destination is an external CPU +attached to a physical port (`cpuTag_externalCpuPort_set`, `EXT_CPU_CTRL` in +the vendor SDK), which these boards do not populate. The ACL trap and +redirect actions do not deliver to the 8051 either. Delivery therefore uses the *forward* action, constrained to the CPU port by a static L2 multicast entry (`port_l2mc_set()`), one per VLAN in use: @@ -64,21 +56,16 @@ by a static L2 multicast entry (`port_l2mc_set()`), one per VLAN in use: transparency an unmanaged switch is expected to have, so a surrounding spanning tree can span *through* this device. -Because delivery rides the forward action, a BPDU is an ordinary frame to the -ingress pipeline and is subject to the port's acceptable-frame-type setting. -BPDUs are untagged by definition, so a port configured to admit tagged frames -only (`ingress t`) will never deliver one: a port left on the default -auto edge turns edge after three seconds of silence, one with edge switched off -sits out the full forward delay instead, and either way the bridge elects -itself root no matter what the neighbour sends. `stp_setup()` prints a warning for every -STP-enabled port in that state. On a normal bridge this cannot happen, since -BPDUs are consumed before any VLAN classification; here it is a direct -consequence of the delivery path above. +A BPDU delivered this way is an ordinary frame to the port's ingress logic +and passes through its acceptable-frame-type filter. BPDUs are untagged, so a +port set to admit tagged frames only (`ingress t`) never delivers one +to the CPU. `stp_setup()` prints a warning for every STP-enabled port in that +state. Port states live in `RTL837X_MSTP_STATES (0x5310)`, two bits per port: -`00` disabled, `01` blocking, `10` learning, `11` forwarding. Note that a port -held in blocking also drops frames the CPU injects into it, so a blocked port -cannot transmit BPDUs of its own. +`00` disabled, `01` blocking, `10` learning, `11` forwarding. In the blocking +state a port forwards nothing except frames sent by the CPU, and nothing it +receives reaches the CPU. ## Timers @@ -122,9 +109,9 @@ stp port <1-9> filter on|off # neither send nor accept BPDUs stp port <1-9> p2p auto|on|off ``` -**edge** — an edge port goes forwarding immediately and does not trigger a -topology change when it comes and goes; `auto` promotes a port to edge after -three seconds without a BPDU, and demotes it as soon as one arrives. Use +**edge** — an edge port forwards immediately and does not trigger a +topology change when its link comes and goes; `auto` promotes a port to edge +after three seconds without a BPDU, and demotes it as soon as one arrives. Use `edge on` for ports where only hosts are attached. **guard** — `bpdu` disables a port as soon as a BPDU arrives on it (a host port @@ -182,6 +169,8 @@ available as JSON: GET /stp.json ``` +The `stp status` command prints the same view on the serial console. + ## Limitations * One spanning-tree instance; no MSTP, no per-VLAN trees. @@ -189,6 +178,3 @@ GET /stp.json converge, but through the timers rather than the fast transition. * Port roles are approximated: the root port and designated ports are distinguished, alternate/backup are not. -* A port in blocking cannot transmit, so a blocked port stops announcing - itself; recovery relies on the listen timer rather than on a neighbour's - agreement. From 4a78b2dc8c91428ca2bc65cde81a885a844559ed Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 11 Aug 2026 17:32:25 +0200 Subject: [PATCH 31/68] doc: move the L2 multicast and tag word details out of the code Review asked for this directly: the hardware layout above port_l2mc_set() would be better as documentation than as a comment, keeping only the two lines that say what the function does. doc/l2.md gains a section on static multicast entries, why delivery uses the forward action rather than the trap, and the SMI layout of the entry. doc/CpuPort.md gains the layout of the tag's flags and pmask words, with the byte order trap that cost an afternoon: writing the flags constant raw instead of through HTONS puts 0x0020 on the wire as 0x2000, which is EFID rather than LEARN_DIS, and the ASIC then leaves the 0x8899 header on the frame. The comments those paragraphs came from are replaced by a pointer to the file that now holds them. --- doc/CpuPort.md | 30 ++++++++++++++++++++++++++++++ doc/l2.md | 33 +++++++++++++++++++++++++++++++++ rtl837x_common.h | 14 ++------------ rtl837x_port.c | 27 +-------------------------- 4 files changed, 66 insertions(+), 38 deletions(-) diff --git a/doc/CpuPort.md b/doc/CpuPort.md index 9e0b028..c10d0a1 100644 --- a/doc/CpuPort.md +++ b/doc/CpuPort.md @@ -64,3 +64,33 @@ Writing 0x1 to register 0x7850 will transmit the frame. The Ethernet frame checksum and the TCP checksum are automatically calculated (offloaded) by the ASIC before transmitting on the wire. + +## The RTL tag words + +The frame header uses the Realtek Remote Control Protocol (RRCP) format or +the like. + +The `flags` word: + +``` +bit15 EFID_EN | 14:12 EFID | 11 PRI_EN | 10:8 PRI | +bit7 KEEP | 6 VSEL | 5 LEARN_DIS | 4:0 VIDX +``` + +All fields are in network byte order. + +* `EFID_EN`, `EFID`: look the destination up under this filtering ID + instead of the port's own +* `PRI_EN`, `PRI`: force the given priority on the frame +* `KEEP`: keep the 802.1Q tagging of the frame exactly as injected, + bypassing the egress tagging rules of the port +* `VSEL`, `VIDX`: classify the frame into the VLAN at this index of the + VLAN table +* `LEARN_DIS`: do not learn the source address from this frame + +The `pmask` word: bit 15 is `ALLOW`, bits 14 to 0 are a port mask. + +* `ALLOW` clear: the mask is the egress set, the frame goes to exactly + the ports given +* `ALLOW` set: the ASIC looks the destination up as usual and the mask + only limits which ports the result may use diff --git a/doc/l2.md b/doc/l2.md index c0aaf61..0a785a8 100644 --- a/doc/l2.md +++ b/doc/l2.md @@ -65,3 +65,36 @@ ASIC and flushing the table in order to quickly forget the learned entries. 3c:18:a0:7e:11:00 0x0001 learned 5 1c:2a:a3:23:00:02 0x0001 learned 7 ``` + +## Static multicast entries + +Slow-protocol frames such as LACPDUs and STP BPDUs have to reach the CPU +without being flooded to the other ports. No bridge relays these frames: +their addresses are in the set that 802.1D-2004 clause 7.12.6 forbids a +bridge to forward, and what travels the network is the information, with +every bridge regenerating BPDUs of its own on its designated ports. The reserved-multicast *trap* action +cannot do that on this hardware, because its destination is an external CPU +attached to a physical port, which these boards do not populate. The protocol +modules therefore leave the reserved-multicast action at *forward* and constrain +the egress with a static L2 multicast entry instead: the lookup hits the entry's +own port mask rather than the VLAN flood mask. Verified on a SWTGW218AS both +ways, with the CPU bit cleared, where delivery stops, and with the CPU bit alone, +where nothing egresses. + +`port_l2mc_set()` writes one such entry. The SMI layout is the L2 multicast +variant of the table entry: + +``` +DATA_IN_A = MAC bytes 5..2 -> c2 00 00 +DATA_IN_B = MAC[1..0] | vid<<16 | IVL<<29 | pmask[1:0]<<30 +DATA_IN_C = pmask[9:2] +``` + +Lookups are IVL, so an entry made for VID 0 is never matched and a caller adds +one entry per PVID in use. The write goes through the table access register +with the table selector set to the L2 lookup table, `TBL_L2_UNICAST` in the +code, a name that despite appearances covers the multicast entries as well. +The hardware hashes MAC and VID to pick the bucket slot by itself. +Writing the same MAC and VID again replaces the entry rather than adding a +second one, so a caller can retarget the mask at will, for instance back to all +ports to restore flooding. diff --git a/rtl837x_common.h b/rtl837x_common.h index 8cd556e..50bffb1 100644 --- a/rtl837x_common.h +++ b/rtl837x_common.h @@ -71,20 +71,10 @@ struct vlan_tag { #define VLAN_TAG_SIZE (sizeof (struct vlan_tag)) #define RTL_FRAME_TAG_ID 0x8899 #define RTL_FRAME_TAG_VERSION 0x04 -/* Bits of the tag's `flags` word (word2), per Linux DSA tag_rtl8_4: - * bit15 EFID_EN | 14:12 EFID | 11 PRI_EN | 10:8 PRI | - * bit7 KEEP | 6 VSEL | 5 LEARN_DIS | 4:0 VIDX - * NOTE: this word must be written through HTONS like every other tag field - - * writing the constant raw puts the bits in the wrong byte (0x0020 raw lands on - * the wire as 0x2000 = EFID, not LEARN_DIS), the ASIC then fails to parse the - * tag and forwards the frame with the 0x8899 header still on it. */ +/* Bits of the tag's `flags` word, see doc/CpuPort.md. */ #define RTL_TAG_LEARN_DIS 0x0020 /* do not learn the CPU's SA on the egress port */ #define RTL_TAG_KEEP 0x0080 /* keep the frame's 802.1Q tag format as injected */ -/* The `pmask` word (word3): bit15 ALLOW selects how 14:0 is interpreted. - * ALLOW=0 -> forwarding port mask (directed egress: frame goes exactly to the - * ports set). ALLOW=1 -> allowance mask (permission filter on a normal lookup), - * which for a one-hot mask yields an empty egress set - the frame disappears. - * Directed egress therefore requires ALLOW cleared, as mainline does. */ +/* The `pmask` word, see doc/CpuPort.md. */ // For TX, an 8 byte (plus 4 byte padding when when VLAN is enabled) // header describing the frame to be moved to the Asic is used diff --git a/rtl837x_port.c b/rtl837x_port.c index 1de3a4f..f9168bf 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -28,8 +28,6 @@ extern __xdata struct machine_runtime machine_detected; __xdata uint32_t l2_head; -/* Bounded-wait counter for the L2 table helpers; xdata because the 8051 - * internal-RAM overlay (OSEG) is full. */ __xdata uint8_t l2mc_guard; __xdata struct vlan_settings vlan_settings; @@ -320,11 +318,6 @@ void vlan_setup(void) __banked /* * Forget the dynamic L2 entries learned on one port. - * - * Same flush engine as port_l2_forget(), but with a single-port mask so a - * topology change only ages out the affected port instead of the whole - * table. Bounded wait (cf. port_l2mc_set): this runs from the STP tick, and - * an unbounded poll on a stuck engine would freeze the main loop. */ void port_l2_forget_port(uint8_t port) __banked { @@ -429,30 +422,12 @@ void port_l2_learned(void) __banked /* * Static L2 multicast entry for the link-local group 01:80:C2:00:00: * in VLAN `vid`, with member portmask `pmask` (bit 9 = CPU port). - * - * Slow-protocol frames (LACP, STP BPDUs) must reach the CPU without being - * flooded to other ports. The RMA "trap" action cannot deliver to the - * internal NIC on this hardware (its destination is an external CPU on a - * physical port), so the protocol modules keep the RMA action at "forward" - * and constrain the egress with this entry instead: the lookup hits the - * entry's portmask rather than the VLAN flood mask (hardware-verified with - * both the CPU bit cleared - delivery stops - and CPU-only - no egress). - * - * SMI layout (vendor SDK, L2-multicast entry variant): - * DATA_IN_A = MAC bytes 5..2 -> c2 00 00 - * DATA_IN_B = MAC[1..0] | vid<<16 | IVL<<29 | pmask[1:0]<<30 - * DATA_IN_C = pmask[9:2] - * Lookups are IVL (a VID-0 entry is not matched), so callers add one entry - * per PVID in use. The write command (table 4 = the whole L2 LUT) hashes - * MAC+VID and picks the bucket slot itself; TBL_EXECUTE self-clears. - * Overwriting the same MAC+VID replaces the entry, so a caller can retarget - * the mask at will (e.g. back to all ports to restore flooding). */ void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked { l2mc_guard = 0; - do { /* wait out any previous table op (bounded, cf. the IGMP guards) */ + do { reg_read_m(RTL837X_TBL_CTRL); } while ((sfr_data[3] & TBL_EXECUTE) && ++l2mc_guard); From 1ba122464431b568346670b5897b44c4128eb5cd Mon Sep 17 00:00:00 2001 From: d00f Date: Tue, 11 Aug 2026 17:32:25 +0200 Subject: [PATCH 32/68] stp: trim the comments, and put one back on the variable it describes Review asked for this across the other commits too. Gone are the blocks that restate what doc/stp.md already says, the ones that explain what an embedded programmer already knows, and one that had gone stale inside this very branch: the CLI summary above stp_parse still described "cost <0-255> (x1000)" while the parser has taken the raw 0 to 200000000 for some time, and it never learned about p2p or trk at all. A usage list next to the parser is the kind of thing that rots first, so it is out rather than updated. The review flagged one comment saying a variable is in xdata because the internal RAM overlay is full, on the grounds that it may stop being true. Four more of the same kind were in these files and are out as well, one of them pointing at a file that does not exist in this branch at all. The declarations still say __xdata, which is the part a reader needs. Also out: the note on why three helpers are __reentrant, which was really a paragraph about two bytes of DSEG, and the measurement story behind the tick divider, which belongs with the other timer numbers in doc/stp.md. One comment was not stale but simply wrong. "max BPDUs per port per second" sat on stp_failsafe_tripped, having slid down two lines when the two failsafe variables were inserted above it. It describes stp_txhold and is back there now. Short factual labels stay: they sit next to the magic number they explain and the codebase uses them throughout. The generated code is byte for byte what it was before this commit, both banks and xdata unchanged. --- httpd/page_impl.c | 10 ++------ rtl837x_stp.c | 64 ++++------------------------------------------- rtl837x_stp.h | 10 +++----- 3 files changed, 10 insertions(+), 74 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 42c3503..bef34d9 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -536,16 +536,10 @@ void send_lag(void) } -/* STP status + configuration for the Spanning Tree page ("/stp.json"). - * Bridge config (prio index 0-15, hello/maxage/fwd seconds, rstp flag, tx - * hold), elected root (priority byte + MAC), our path cost, root port, TC - * counter, and per port: physical number, live ASIC state (2-bit MSTP field: - * 0 Dis 1 Blk 2 Lrn 3 Fwd), an approximated role, and the per-port config - * (enabled, edge admin/auto/oper, cost/1000, prio, guard, filter, tripped). */ +/* STP status and configuration for the Spanning Tree page ("/stp.json"). */ __xdata uint8_t stp_we_root; -__xdata uint8_t pi_i, pi_j, pi_j2; /* shared loop iterators (DSEG relief) */ +__xdata uint8_t pi_i, pi_j, pi_j2; -/* Parameter relays in xdata: keeps these helpers off the IRAM overlay */ static __xdata uint32_t pi_u32; static __xdata uint8_t pi_prio, pi_ext; static __xdata uint8_t * __xdata pi_mac; diff --git a/rtl837x_stp.c b/rtl837x_stp.c index b67f230..6ad56e5 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -1,26 +1,11 @@ /* * This is a driver implementation for the Spanning Tree Protocol features for the RTL837x platform * This code is in the Public Domain - * - * Configurable per 802.1D-2004/802.1w: bridge priority, hello time, max age, - * forward delay, force-version (RSTP/STP), tx hold count; per port: enable, - * admin/auto edge, path cost, port priority, BPDU guard, root guard, BPDU - * filter. CLI: "stp ..." (see stp_parse), status: /stp.json (send_stp). - * - * The engine itself stays deliberately simple (no proposal/agreement - * handshake, no full port-role machine): we elect a root from received BPDUs, - * promote ports to forwarding after the listen period (or immediately for - * edge ports), age the root out via max age, and block on a loop (our own - * BPDU comes back: the worse Port ID of the pair stops forwarding and stays - * blocked while it keeps coming back) or - with root guard - a better root. */ // #define REGDBG // #define DEBUG -/* Place this module's code and constants in code bank 2 (cf. rtl837x_igmp.c): - * the always-mapped common area is nearly full, and the full state machine - * does not fit there. */ #pragma codeseg BANK2 #pragma constseg BANK2 @@ -37,8 +22,6 @@ extern __code struct machine machine; extern __xdata uint8_t sfr_data[4]; extern __xdata struct machine_runtime machine_detected; /* owned by rtl837x_port.c */ -/* Scratch for stp_fdb_update(), in xdata: plain locals would overflow the - * near-full internal-RAM overlay (OSEG), cf. rtl837x_lacp.c. */ __xdata uint16_t stp_fdb_vid; __xdata uint8_t stp_fdb_i; @@ -62,12 +45,6 @@ __xdata uint8_t stp_fwddelay_s; __xdata uint8_t stp_rstp; __xdata uint8_t stp_txhold; -/* Management failsafe, commit-confirm: enabling STP arms a one-shot window of - * stp_failsafe_s seconds. One HTTP request inside the window confirms that - * management survived the new tree and disarms it until the next enable; a - * window with no management activity disables STP and restores forwarding - * (mgmt VLAN rides a blockable front port; lockout of 2026-07-20 is the - * motivating incident). 0 never arms. Headless installs should set 0. */ __xdata uint8_t stp_failsafe_s; __xdata uint8_t stp_failsafe_cnt; /* seconds left of the armed window */ __xdata uint8_t stp_failsafe_armed; @@ -97,13 +74,12 @@ __xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */ __xdata uint16_t stp_link_prev; /* carrier bitmap as of the last check */ __xdata uint16_t stp_link_now; -/* Scratch (8051: locals would overflow the internal-RAM overlay area) */ __xdata uint8_t stp_scratch; __xdata uint8_t stp_tx_flags_extra; /* one-shot flags OR-ed into the next BPDU (TCA) */ __xdata uint16_t stp_rxlen; /* received frame length, saved before uip_len is consumed */ __xdata uint8_t stp_msg_age; /* message age of the root info we hold, seconds */ __xdata uint16_t stp_tc_while; /* ticks left to set the TC flag in our BPDUs */ -__xdata uint8_t stp_i; /* shared loop iterator (DSEG relief) */ +__xdata uint8_t stp_i; __xdata uint32_t stp_cost_scratch; __xdata uint8_t stp_loop_peer; /* the other own port seen on a looped segment */ @@ -189,11 +165,7 @@ static void stp_state_set(uint8_t port, uint8_t state) __reentrant } -/* Signal a topology change: flush the stale forwarding entries of the port - * that changed, count it, and set the TC flag in our BPDUs for one - * max-age+forward-delay period (802.1D 8.6.14) so the neighbours age their - * own tables out too. Edge ports are exempt: a host appearing or leaving is - * not a topology change. */ +/* Signal a topology change. Edge ports are exempt. */ static void stp_topology_change(uint8_t port) __reentrant { if (stp_pflags[port] & STP_PF_OPEREDGE) @@ -208,12 +180,7 @@ static void stp_topology_change(uint8_t port) __reentrant * holding it for as long as the caller keeps saying so. The caller is the * port that won the Port ID compare (see stp_in) - a different port than * the one held, except when the frame came back on the port it left. - * - * Reentrant on purpose, like the two above: parameters and locals then live - * on the stack instead of taking internal RAM of their own. Inlined into - * stp_in - which is __banked, so its temporaries cannot be overlaid - the - * same code costs two more bytes of DSEG, and that is enough to stop the - * image with LACP from linking at all. */ + */ static void stp_loop_hold_peer(uint8_t port) __reentrant { /* The port number arrives in a BPDU, so it is somebody else's data, @@ -675,17 +642,8 @@ void stp_defaults(void) __banked /* - * Steer BPDUs (01:80:C2:00:00:00) while STP runs: one static CPU-only L2 - * multicast entry per PVID in use, so BPDUs reach the CPU without being - * flooded to other ports (a bridge running STP must consume BPDUs, not - * relay them - relaying poisons the neighbours' view of the topology). - * - * With STP off the same entries are retargeted to all ports + CPU, which - * restores the previous flood behaviour ("BPDU transparency"): the - * surrounding spanning tree can keep spanning *through* this switch, which - * unmanaged setups rely on. Same per-PVID/IVL rules as the LACP steering - - * see port_l2mc_set() and rtl837x_lacp.c. NOTE: changing a port's PVID - * while STP runs needs `stp off`/`on` to refresh the entries. + * Steer BPDUs while STP runs, and restore flooding when it stops. + * Changing a port's PVID while STP runs needs "stp off" then "stp on". */ static void stp_fdb_update(__xdata uint16_t pmask) { @@ -773,18 +731,6 @@ void stp_off(void) __banked } -/* ---- "stp ..." CLI ---- - * stp on|off - * stp prio <0-15> (bridge priority = n * 4096) - * stp hello <1-10> | stp maxage <6-40> | stp fwd <4-30> | stp txhold <1-10> - * stp version rstp|stp - * stp port <1-9> on|off - * stp port <1-9> edge on|off|auto - * stp port <1-9> cost <0-255> (x1000; 0 = auto/20000) - * stp port <1-9> prio <0-240> - * stp port <1-9> guard none|bpdu|root - * stp port <1-9> filter on|off - */ void stp_parse(void) __banked __reentrant { if (cmd_compare(1, "on")) { diff --git a/rtl837x_stp.h b/rtl837x_stp.h index ce09b69..fb534ae 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -9,11 +9,7 @@ void stp_off(void) __banked; void stp_parse(void) __banked __reentrant; /* "stp ..." CLI handler (cmd_parser delegates here) */ void stp_defaults(void) __banked; /* boot init: 802.1D/w default configuration */ -/* Tick rate of stp_timers(): the main loop idles on the 200 Hz system tick - * and rtlplayground.c calls us every (STP_TICK_DIVIDER + 1) = 4th pass. - * Measured on hardware: hello 2 s produced BPDUs exactly 2.560 s apart with - * the previous value of 64, i.e. 20 ms per tick - every configured timer ran - * 28 % long. Shared with the web UI, which ages the same counters. */ +/* Tick rate of stp_timers(), also used by the web UI. */ #define STP_HZ 50 /* Bridge identifier as carried in a BPDU (priority, extension, MAC). */ @@ -30,9 +26,9 @@ extern __xdata uint8_t stp_hello_s; /* hello time, 1-10 s (default 2) */ extern __xdata uint8_t stp_maxage_s; /* max age, 6-40 s (default 20) */ extern __xdata uint8_t stp_fwddelay_s; /* forward delay, 4-30 s (default 15); our listen period */ extern __xdata uint8_t stp_rstp; /* 1 = RSTP BPDUs (v2), 0 = STP-compatible Config BPDUs (v0) */ -extern __xdata uint8_t stp_txhold; +extern __xdata uint8_t stp_txhold; /* max BPDUs per port per second (default 6) */ extern __xdata uint8_t stp_failsafe_s; /* mgmt watchdog, seconds (0 = off) */ -extern __xdata uint8_t stp_failsafe_tripped; /* max BPDUs per port per second (default 6) */ +extern __xdata uint8_t stp_failsafe_tripped; /* Per-port config/status flags (stp_pflags[]) */ #define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */ From eb4a6ac27553d8304bf729dbae2d59aff8d6b4d9 Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 12 Aug 2026 15:13:15 +0200 Subject: [PATCH 33/68] stp: drop the bounds on the L2 flush and L2MC table waits The review asked for these to come out and the reasoning holds. A guard that gives up mid-transaction lets the code carry on with whatever the engine left behind, and that is what cost me an SPI clip twice. An unbounded wait on a wedged engine still hangs, but it hangs in a known place instead of writing garbage into the L2 table. BANK1 loses 45 bytes and xdata one. BANK2 and the common bank do not move. --- rtl837x_port.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/rtl837x_port.c b/rtl837x_port.c index f9168bf..7cff91e 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -28,8 +28,6 @@ extern __xdata struct machine_runtime machine_detected; __xdata uint32_t l2_head; -__xdata uint8_t l2mc_guard; - __xdata struct vlan_settings vlan_settings; void port_mirror_set(register uint8_t port, __xdata uint16_t rx_pmask, __xdata uint16_t tx_pmask) __banked @@ -324,10 +322,9 @@ void port_l2_forget_port(uint8_t port) __banked REG_SET(RTL837x_L2_TBL_FLUSH_CNF, 0x0); /* port-based, dynamic entries */ REG_SET(RTL837x_L2_TBL_FLUSH_CTRL, L2_TBL_FLUSH_EXEC | (((uint16_t)1) << port)); - l2mc_guard = 0; do { reg_read_m(RTL837x_L2_TBL_FLUSH_CTRL); - } while (sfr_data[1] && ++l2mc_guard); + } while (sfr_data[1]); } @@ -426,19 +423,17 @@ void port_l2_learned(void) __banked void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked { - l2mc_guard = 0; do { reg_read_m(RTL837X_TBL_CTRL); - } while ((sfr_data[3] & TBL_EXECUTE) && ++l2mc_guard); + } while (sfr_data[3] & TBL_EXECUTE); REG_WRITE(RTL837x_TBL_DATA_IN_A, 0xc2, 0x00, 0x00, mac_last); REG_WRITE(RTL837x_TBL_DATA_IN_B, 0x20 | (vid >> 8) | ((pmask & 0x3) << 6), vid, 0x01, 0x80); REG_WRITE(RTL837x_TBL_DATA_IN_C, 0, 0, 0, pmask >> 2); REG_WRITE(RTL837X_TBL_CTRL, 0, 0, TBL_L2_UNICAST, TBL_WRITE | TBL_EXECUTE); - l2mc_guard = 0; do { reg_read_m(RTL837X_TBL_CTRL); - } while ((sfr_data[3] & TBL_EXECUTE) && ++l2mc_guard); + } while (sfr_data[3] & TBL_EXECUTE); } From d31980eb9e136474cca48c75672ca296f38965ed Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 12 Aug 2026 15:37:50 +0200 Subject: [PATCH 34/68] stp: sort the port rows, show this switch's bridge ID, react to enabling Three things from the page feedback. The rows came out in logical order while carrying the physical port number, so on the six-port boards the first row is labelled 5. Sorting the rows by that number in JS puts every board back into front-panel order. I walked all 25 machine definitions and each one now yields a clean 1..N. The Designated Bridge column is hard to read without knowing this switch's own bridge ID, so the status line shows it in the same priority and MAC shape as the cells use. On the test switch that reads 61440-06:05:16:1E:F9:24 and matches the Designated Bridge of every locally designated port, which is the comparison that was missing. The root bridge and the path cost now use the same formatting as the columns instead of raw hex. Enabling STP printed nothing until the next poll, and because the ports start blocked, management can stay quiet for the whole listening and learning period, so the page had no chance to say anything later. It now writes what is about to happen before the command goes out, and how long the ports need. Page data only. Both banks, xdata and the common bank are unchanged. --- html/stp.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/html/stp.js b/html/stp.js index 1af2230..ec73ee7 100644 --- a/html/stp.js +++ b/html/stp.js @@ -41,7 +41,7 @@ function num(id, min, max, onch) { function buildPortsTable(ports) { const tbl = document.getElementById("stpPortsTbl"); const stat = document.getElementById("stpStatTbl"); - for (const p of ports) { + for (const p of [...ports].sort((a, b) => a.p - b.p)) { const tr = tbl.insertRow(); tr.insertCell().textContent = p.p; // Port tr.insertCell().appendChild(sel("en_" + p.p, @@ -81,6 +81,10 @@ function buildPortsTable(ports) { stpRows = ports.length; } +function bridgeSelf(s) { + return fmtBridgeId((s.prio * 4096).toString(16).padStart(4, "0") + s.myMac); +} + function fmtBridgeId(h) { if (!h || h.length < 16) return ""; const prio = parseInt(h.slice(0, 4), 16); @@ -99,9 +103,11 @@ function fetchStp() { ? "\u26a0 STP was disabled by the management failsafe (ports were blocked while management was unreachable). Review the topology before re-enabling." : s.on ? (s.weRoot - ? "This switch is the root bridge (priority 0x" + s.rootPrio + ") — topology changes: " + parseInt(s.tc, 16) - : "Root bridge: 0x" + s.rootPrio + " / " + s.rootMac - + " via port " + s.rootPort + " — path cost: 0x" + s.cost + ? "This switch (" + bridgeSelf(s) + ") is the root bridge — topology changes: " + + parseInt(s.tc, 16) + : "This switch: " + bridgeSelf(s) + + " — root bridge: " + fmtBridgeId(s.rootPrio + s.rootMac) + + " via port " + s.rootPort + " — path cost: " + parseInt(s.cost, 16) + " — topology changes: " + parseInt(s.tc, 16)) : ""; for (const p of s.ports) { @@ -147,6 +153,11 @@ function fetchStp() { async function stpSub() { const on = document.getElementById("stpMode").value === "on"; + document.getElementById("stpStat").textContent = on + ? "Enabling STP. The ports start blocked and take up to " + + (2 * document.getElementById("bFwd").value) + + " s to reach forwarding, and this page can stay silent until they do." + : "Disabling STP."; await stpCmd(on ? "stp on" : "stp off"); } From 5a6b854c5d249921334748d83a741e69c701009c Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 12 Aug 2026 16:46:29 +0200 Subject: [PATCH 35/68] stp: record the designated bridge, port and cost from received BPDUs stp_dbridge, stp_dpid and stp_dcost were declared and read by the status page, but nothing ever wrote them, so they stayed zero for the life of the firmware. The page's validity test then always failed, and the Designated Bridge, Designated Port ID and Designated Cost columns reported our own values on every port, including the root port where the answer is the upstream neighbour. The three arrays reserved 140 bytes of xdata and never used any of it. They are filled now, right after the loop check, so a frame that came back from one of our own ports is not mistaken for a neighbour. The validity test moves from the last byte of the stored MAC to the stored Port ID. A Port ID is 1-based on the wire and cannot be zero, while a neighbour whose MAC happens to end in 0x00 would have failed the old test. The root path cost byte swap happens once now, and the root port branch reuses the value instead of repeating the shifts. BANK2 grows 148 bytes, BANK1 loses 9, and xdata does not move. --- httpd/page_impl.c | 2 +- rtl837x_stp.c | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index bef34d9..ac389e8 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -637,7 +637,7 @@ void send_stp(void) itoa_html(stp_pp2p[pi_i]); /* designated info: a freshly heard BPDU wins, else we are the * segment's designated bridge and report our own values */ - stp_we_root = stp_dbridge[pi_i].mac[5] && stp_bpdu_age[pi_i] < (uint16_t)stp_maxage_s * STP_HZ; + stp_we_root = stp_dpid[pi_i] && stp_bpdu_age[pi_i] < (uint16_t)stp_maxage_s * STP_HZ; slen += strtox(outbuf + slen, ",\"db\":\""); if (stp_we_root) { pi_prio = stp_dbridge[pi_i].prio; pi_ext = stp_dbridge[pi_i].ext; diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 6ad56e5..a0387e4 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -451,6 +451,16 @@ void stp_in(void) __banked return; } + stp_dbridge[port].prio = STP_I->bridge.prio; + stp_dbridge[port].ext = STP_I->bridge.ext; + memcpy(stp_dbridge[port].mac, STP_I->bridge.mac, 6); + stp_dpid[port] = ((uint16_t)STP_I->port_prio << 8) | STP_I->port_id; + stp_cost_scratch = STP_I->root_path_cost; + stp_dcost[port] = ((stp_cost_scratch & 0xff) << 24) + | ((stp_cost_scratch & 0xff00) << 8) + | ((stp_cost_scratch >> 8) & 0xff00) + | (stp_cost_scratch >> 24); + /* Better root than the one we know? */ if (STP_I->root.prio < root_bridge.prio || ((STP_I->root.prio == root_bridge.prio) && cmpMAC(STP_I->root.mac, root_bridge.mac) < 0)) { @@ -480,13 +490,7 @@ void stp_in(void) __banked /* Age of the information we now hold (see the TX note on the wire * format); saturate rather than wrap on absurd input. */ stp_msg_age = (STP_I->age > 254) ? 254 : (uint8_t)STP_I->age; - stp_cost_scratch = STP_I->root_path_cost; - /* big-endian on the wire */ - root_bridge_cost = ((stp_cost_scratch & 0xff) << 24) - | ((stp_cost_scratch & 0xff00) << 8) - | ((stp_cost_scratch >> 8) & 0xff00) - | (stp_cost_scratch >> 24); - root_bridge_cost += PCOST(port); + root_bridge_cost = stp_dcost[port] + PCOST(port); } } } From a6c5f558bf02e83420b709923fa044d7d57947a7 Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 12 Aug 2026 19:34:25 +0200 Subject: [PATCH 36/68] stp: do not arm the management failsafe while the config replays The failsafe is a commit confirm window for an interactive change: turn STP on, and if management goes quiet for stp_failsafe_s seconds the switch undoes it. The three places that arm it sit in the command parser, and execute_config() drives that same parser at boot, so a saved "stp on" arms the window too. A switch that reboots with nobody watching then turns its own STP back off. Measured on a SWTGW218AS with "stp failsafe 180" in the saved config: cold boot, no HTTP and no console for four minutes, and "STP failsafe: disabling" arrives on time, with stp.json reporting on:0 and fsT:1. execute_config() already clears save_cmd while it replays and sets it again at the end, so the three parser sites can just test it. The two on the protocol side, the loop latch and the root guard, stay unconditional. They react to what arrived on the wire, which is the case the failsafe exists for, and they only run once the replay is long finished. BANK2 grows 24 bytes. Nothing else moves. --- rtl837x_stp.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index a0387e4..e4af019 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -34,6 +34,7 @@ extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed extern __xdata uint8_t cmd_buffer[CMD_BUF_SIZE]; extern __xdata uint8_t cmd_words_len; extern __xdata uint8_t cmd_words_b[15]; +extern __xdata char save_cmd; /* 0 while execute_config() replays the saved config */ uint8_t cmd_compare(uint8_t start, __code uint8_t * cmd); uint8_t atoi_byte(__xdata uint8_t *out, uint8_t idx); @@ -741,7 +742,7 @@ void stp_parse(void) __banked __reentrant print_string("STP enabled\n"); stp_failsafe_tripped = 0; stp_failsafe_cnt = stp_failsafe_s; - stp_failsafe_armed = stp_failsafe_s ? 1 : 0; + stp_failsafe_armed = (stp_failsafe_s && save_cmd) ? 1 : 0; mgmt_alive = 0; stpEnabled = 1; stp_setup(); @@ -775,7 +776,7 @@ void stp_parse(void) __banked __reentrant if (stpEnabled) { /* (re)join: listen first */ stp_state_set(port, 0b01); port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; - if (stp_failsafe_s) { + if (stp_failsafe_s && save_cmd) { stp_failsafe_armed = 1; stp_failsafe_cnt = stp_failsafe_s; mgmt_alive = 0; @@ -885,7 +886,7 @@ void stp_parse(void) __banked __reentrant /* 0 never arms; otherwise the length of the armed window */ stp_failsafe_s = stp_scratch; stp_failsafe_cnt = stp_scratch; - stp_failsafe_armed = (stp_scratch && stpEnabled) ? 1 : 0; + stp_failsafe_armed = (stp_scratch && stpEnabled && save_cmd) ? 1 : 0; mgmt_alive = 0; } else { goto err; From 4d7a2e28c788a3a031e500f9637e9b31043c5649 Mon Sep 17 00:00:00 2001 From: d00f Date: Thu, 13 Aug 2026 12:16:02 +0200 Subject: [PATCH 37/68] stp: keep the designated bridge recording out of internal RAM The report on the PR is that it will not link for KP_9000_6XHML_X2, with "?ASlink-Error-Could not get N consecutive bytes in internal RAM for area OSEG" five times over. It builds here on sdcc 4.2.0 and 4.5.0 for that same machine and the same commit, so something in the toolchain differs, but the pressure it is complaining about is mine and it costs little to give back. stp_in() is __banked, so its temporaries get exclusive DSEG instead of overlaying with anything else. Recording the designated bridge put four more live values across a memcpy in the middle of it and the register allocator answered with five spill locations. The module went from 5 bytes of DSEG to 12, and from 17 sloc references to 49. Moving that block into a __reentrant helper puts its temporaries on the stack instead. The module now claims no DSEG at all, 5 bytes better than before the recording was added, and the image sits at 95 bytes of DSEG against 101 on main. It costs 170 bytes of BANK2, where there is room. --- rtl837x_stp.c | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index e4af019..3386b2e 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -141,6 +141,22 @@ struct stp_pkt_in { #define STP_O ((__xdata struct stp_pkt *)&uip_buf[RTL_FRAME_DESC_SIZE]) #define STP_I ((__xdata struct stp_pkt_in *)&uip_buf[0]) +/* __reentrant so the temporaries land on the stack: stp_in() is __banked and + * its locals get exclusive internal RAM, which is what runs out first here. */ +static void stp_record_designated(uint8_t port) __reentrant +{ + stp_dbridge[port].prio = STP_I->bridge.prio; + stp_dbridge[port].ext = STP_I->bridge.ext; + memcpy(stp_dbridge[port].mac, STP_I->bridge.mac, 6); + stp_dpid[port] = ((uint16_t)STP_I->port_prio << 8) | STP_I->port_id; + stp_cost_scratch = STP_I->root_path_cost; + stp_dcost[port] = ((stp_cost_scratch & 0xff) << 24) + | ((stp_cost_scratch & 0xff00) << 8) + | ((stp_cost_scratch >> 8) & 0xff00) + | (stp_cost_scratch >> 24); +} + + signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2) __reentrant { for (uint8_t i = 0; i < 6; i++) { @@ -452,15 +468,7 @@ void stp_in(void) __banked return; } - stp_dbridge[port].prio = STP_I->bridge.prio; - stp_dbridge[port].ext = STP_I->bridge.ext; - memcpy(stp_dbridge[port].mac, STP_I->bridge.mac, 6); - stp_dpid[port] = ((uint16_t)STP_I->port_prio << 8) | STP_I->port_id; - stp_cost_scratch = STP_I->root_path_cost; - stp_dcost[port] = ((stp_cost_scratch & 0xff) << 24) - | ((stp_cost_scratch & 0xff00) << 8) - | ((stp_cost_scratch >> 8) & 0xff00) - | (stp_cost_scratch >> 24); + stp_record_designated(port); /* Better root than the one we know? */ if (STP_I->root.prio < root_bridge.prio From 64906ad13506ebd991ed34dcb799f6ea7da2a6c5 Mon Sep 17 00:00:00 2001 From: d00f Date: Thu, 13 Aug 2026 12:25:18 +0200 Subject: [PATCH 38/68] stp: name front panel ports on the console, and add "stp status" Two things from the review, both about the console being where you end up when the tree is not what you expected. The six messages that name a port were printing the internal index. On a board whose map is not the identity that is a different number from the one written next to the socket, which is worse than no number at all. They go through machine.log_to_phys_port now, in a small helper that also swallows the newline each of them repeated. "stp status" prints the bridge and root IDs, the root port and path cost, the topology change count, the failsafe setting, and a line per port with state, role and operational edge. Everything it shows is state the module already keeps, apart from the port states, which come from one read of MSTP_STATES. 660 bytes of BANK2, which leaves 3366 free. No internal RAM, no xdata. --- rtl837x_stp.c | 74 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 3386b2e..d870af5 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -141,6 +141,64 @@ struct stp_pkt_in { #define STP_O ((__xdata struct stp_pkt *)&uip_buf[RTL_FRAME_DESC_SIZE]) #define STP_I ((__xdata struct stp_pkt_in *)&uip_buf[0]) +/* Console messages name the port on the front panel, not the internal index. */ +static void print_port_nl(uint8_t port) __reentrant +{ + print_byte(machine.log_to_phys_port[port]); + write_char('\n'); +} + + +static void print_bridge_id(uint8_t prio, uint8_t ext, __xdata uint8_t *mac) __reentrant +{ + print_byte(prio); print_byte(ext); write_char('/'); + for (stp_i = 0; stp_i < 6; stp_i++) + print_byte(mac[stp_i]); +} + + +/* Where you look when the tree is not what you expected. */ +static void stp_status(void) +{ + if (!stpEnabled) { + print_string("STP off\n"); + return; + } + print_string(stp_rstp ? "STP on, RSTP\n" : "STP on, STP\n"); + print_string("bridge "); + print_bridge_id(stp_prio, 0, uip_ethaddr.addr); + print_string("\nroot "); + print_bridge_id(root_bridge.prio, root_bridge.ext, root_bridge.mac); + if (stp_root_port == 0xff) { + print_string(" (this switch)\n"); + } else { + print_string(" port "); + print_byte(machine.log_to_phys_port[stp_root_port]); + print_string(" cost "); + print_long(root_bridge_cost); + write_char('\n'); + } + print_string("changes "); + print_short(stp_tc_count); + print_string(" failsafe "); + itoa(stp_failsafe_s); + print_string(stp_failsafe_tripped ? "s TRIPPED\n" : "s\n"); + print_string("port state role edge\n"); + reg_read_m(RTL837X_MSTP_STATES); + for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { + write_char(' '); + print_byte(machine.log_to_phys_port[stp_i]); + print_string(" "); + print_byte((sfr_data[3 - (stp_i >> 2)] >> ((stp_i << 1) & 0x7)) & 0x3); + print_string(" "); + print_byte(stp_i == stp_root_port ? 1 : 2); + print_string(" "); + print_byte(stp_pflags[stp_i] & STP_PF_OPEREDGE ? 1 : 0); + write_char('\n'); + } +} + + /* __reentrant so the temporaries land on the stack: stp_in() is __banked and * its locals get exclusive internal RAM, which is what runs out first here. */ static void stp_record_designated(uint8_t port) __reentrant @@ -216,7 +274,7 @@ static void stp_loop_hold_peer(uint8_t port) __reentrant return; if (!port_timers[port]) { /* not held down yet */ print_string("STP: loop detected, blocking port "); - print_byte(port); write_char('\n'); + print_port_nl(port); stp_state_set(port, 0b01); if (stp_failsafe_s && !stp_failsafe_armed) { stp_failsafe_armed = 1; @@ -395,7 +453,7 @@ void stp_in(void) __banked /* BPDU guard: an edge-facing port must never see a BPDU - shut it down. */ if (stp_pflags[port] & STP_PF_BPDUGUARD) { print_string("STP: BPDU guard tripped, disabling port "); - print_byte(port); write_char('\n'); + print_port_nl(port); stp_pflags[port] |= STP_PF_TRIPPED; stp_state_set(port, 0b00); stp_tc_count++; @@ -476,7 +534,7 @@ void stp_in(void) __banked /* Root guard: this port must never become our path to the root. */ if (stp_pflags[port] & STP_PF_ROOTGUARD) { print_string("STP: root guard blocking port "); - print_byte(port); write_char('\n'); + print_port_nl(port); stp_state_set(port, 0b01); if (stp_failsafe_s && !stp_failsafe_armed) { stp_failsafe_armed = 1; @@ -562,7 +620,7 @@ void stp_timers(void) __banked } else { port_timers[stp_i] = 0; print_string("STP: link down, port blocking "); - print_byte(stp_i); write_char('\n'); + print_port_nl(stp_i); stp_topology_change(stp_i); } } @@ -597,7 +655,7 @@ void stp_timers(void) __banked if (!--port_timers[stp_i]) { stp_state_set(stp_i, 0b11); print_string("STP: port forwarding "); - print_byte(stp_i); write_char('\n'); + print_port_nl(stp_i); stp_topology_change(stp_i); } else if ((stp_pflags[stp_i] & STP_PF_AUTOEDGE) && stp_bpdu_age[stp_i] > STP_EDGE_DELAY) { @@ -607,7 +665,7 @@ void stp_timers(void) __banked stp_pflags[stp_i] |= STP_PF_OPEREDGE; stp_state_set(stp_i, 0b11); print_string("STP: edge port forwarding "); - print_byte(stp_i); write_char('\n'); + print_port_nl(stp_i); } } } @@ -763,6 +821,10 @@ void stp_parse(void) __banked __reentrant stp_failsafe_armed = 0; return; } + if (cmd_compare(1, "status")) { + stp_status(); + return; + } if (cmd_words_len < 3) goto err; From 024c8cef492487567ec9731544144a361d678ef5 Mon Sep 17 00:00:00 2001 From: d00f Date: Thu, 13 Aug 2026 12:31:56 +0200 Subject: [PATCH 39/68] stp: arm the management failsafe only where an operator asked for it Your console session shows the shape of this better than I could have. STP found a loop, blocked the port, unblocked the other side of the pair, and then the failsafe turned STP off. It had been disarmed by your first console command and re-armed by the loop detection itself, so a mechanism that exists to protect against a lockout ended up removing loop protection while a loop was physically present. That is the part that did not make sense, and it wasn't the console. Two changes, both narrowing. Loop detection and root guard no longer arm the window. Those are the protocol doing its job on evidence off the wire. Nothing an operator did needs undoing there, and nobody is waiting to confirm anything. Typing on the serial console no longer disarms it. The failsafe asks one question, whether the operator can still reach management over the network, and serial activity doesn't answer it. It proves somebody is standing at the box, which is the one case where a lockout doesn't matter, and it took the safety net away from a remote operator on behalf of someone not using it. HTTP activity still confirms, because that is the path being measured, and the console in the web interface counts for the same reason. What is left arms on stp on, stp port N on and stp failsafe, each of them an operator choosing something whose outcome the protocol then decides. Gives back 61 bytes of BANK2, 31 of the common area and a byte of xdata. --- rtl837x_stp.c | 8 -------- rtlplayground.c | 6 ------ 2 files changed, 14 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index d870af5..40e5bd4 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -276,10 +276,6 @@ static void stp_loop_hold_peer(uint8_t port) __reentrant print_string("STP: loop detected, blocking port "); print_port_nl(port); stp_state_set(port, 0b01); - if (stp_failsafe_s && !stp_failsafe_armed) { - stp_failsafe_armed = 1; - stp_failsafe_cnt = stp_failsafe_s; - } stp_pflags[port] &= ~STP_PF_OPEREDGE; stp_topology_change(port); } @@ -536,10 +532,6 @@ void stp_in(void) __banked print_string("STP: root guard blocking port "); print_port_nl(port); stp_state_set(port, 0b01); - if (stp_failsafe_s && !stp_failsafe_armed) { - stp_failsafe_armed = 1; - stp_failsafe_cnt = stp_failsafe_s; - } port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; stp_pflags[port] &= ~STP_PF_OPEREDGE; return; diff --git a/rtlplayground.c b/rtlplayground.c index 3b2d6b5..b24f8f5 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -123,7 +123,6 @@ __xdata uint8_t tx_seq; __xdata uint8_t stpEnabled; __xdata uint8_t igmpEnabled; extern __xdata uint8_t stp_failsafe_armed; -__xdata uint8_t fs_was_armed; __xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ __code uint16_t bit_mask[16] = { @@ -1520,14 +1519,9 @@ void idle(void) // Check whether a command is waiting in the cmd_buffer and execute if (cmd_available) { cmd_available = 0; - fs_was_armed = stp_failsafe_armed; cmd_tokenize(); if (err_status == ERR_OK) cmd_parser(); - if (fs_was_armed && stp_failsafe_armed) { - stp_failsafe_armed = 0; - print_string("STP failsafe: console activity - disarmed\n"); - } print_cmd_prompt(); } } From a9af466702fa12ad48c3f44180a891b7761d348e Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 04:49:33 +0200 Subject: [PATCH 40/68] stp: drop the management failsafe The window could be armed from the serial console but only ever disarmed by an HTTP request. save_cmd, which gates arming, is cleared only while execute_config() replays the startup config, so every interactive command armed it wherever it was typed, while mgmt_alive, which disarms it, was written in exactly one place, on HTTP traffic. An operator working entirely on the serial console therefore lost STP 180 seconds after enabling it however much they typed, which is what makes the mechanism impossible to test from a console. The documentation described the behaviour that was intended rather than the one that was built, and in both directions: it said a command on the serial console also confirms, and it said a reboot with STP in the startup config disables it again three minutes later. Neither held. The replay path never armed the window at all. Repairing the asymmetry would have kept a mechanism whose premise is contested anyway. A watchdog that switches the protection off in response to silence adds a second failure mode on top of the first: where the network is misconfigured and STP is the thing holding a storm back, restoring forwarding removes the last reason management still answers. Gone with it: the stp failsafe command, the fs and fsT fields of /stp.json, the input and the tripped banner on the Spanning Tree page, the two persistence patterns in config.js, the documentation section, and mgmt_alive itself, which had no other reader. 550 bytes back, 145 of BANK1 and 405 of BANK2, and five of xdata, which is the four counters and mgmt_alive and nothing else. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0. --- doc/stp.md | 39 ------------------------------------ html/config.js | 3 +-- html/stp.html | 3 +-- html/stp.js | 7 +------ httpd/httpd.c | 3 --- httpd/page_impl.c | 4 ---- rtl837x_stp.c | 50 +---------------------------------------------- rtl837x_stp.h | 2 -- rtlplayground.c | 1 - 9 files changed, 4 insertions(+), 108 deletions(-) diff --git a/doc/stp.md b/doc/stp.md index f96fb2b..61bb5e5 100644 --- a/doc/stp.md +++ b/doc/stp.md @@ -9,10 +9,6 @@ silent, and blocks a port on which it sees its own BPDU. STP can be enabled and controlled via the web interface or the command line, as follows: -> **Before you enable it on a switch you reach over the network**: read the -> [management failsafe](#management-failsafe) section. The management VLAN -> rides a port that STP can block. - ## Quick start ``` @@ -123,41 +119,6 @@ claims a better priority. on the far side reacts badly to them (some unmanaged switches with loop prevention cut the link) but you still want STP on the rest of the ports. -## Management failsafe - -Enabling STP on a switch you administer over the network is a genuine risk: the -management VLAN rides a port that STP may put into blocking, and once that -happens the way back is a power cycle. - -The firmware therefore runs a commit-confirm watchdog. Enabling STP, by hand or -from the startup config, arms a one-shot window of `stp failsafe ` -(default 180). One HTTP request inside the window confirms that management -survived the new tree and disarms the watchdog until the next enable; a window -with no management activity disables STP and restores forwarding. After the -confirmation STP runs unsupervised, so a quiet network no longer loses its -tree to three minutes of nobody looking at the web UI. - -``` -stp failsafe 180 # length of the armed window after enabling (0 = never armed) -``` - -Any later event that newly takes a port out of forwarding arms the window -again: a port rejoining via `stp port on`, root guard firing, the loop -latch. If management traffic keeps flowing past the new block, the very next -request confirms and disarms; if the block cut it, the silent window restores -forwarding as above. A stable network with nothing newly blocked never re-arms. - -A command executed on the serial console also confirms, on the grounds that an -operator with out-of-band access does not need the automatic restore; the -command that enabled STP does not count, only activity after it. - -A headless switch that nobody confirms over HTTP should set `stp failsafe 0`, -otherwise a reboot with STP in the startup config disables it again three -minutes later. Setting a new value while STP runs arms a fresh window. - -The status page shows whether the failsafe has tripped since STP was last -enabled. - ## Status The Spanning Tree page shows the elected root (priority and MAC), the path cost diff --git a/html/config.js b/html/config.js index e44652c..505f0f3 100644 --- a/html/config.js +++ b/html/config.js @@ -23,7 +23,6 @@ const conf_cmds = [ /^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/, /^stp\s+(on|off)$/, /^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/, - /^stp\s+failsafe\s+\d{1,3}$/, /^stp\s+version\s+(rstp|stp)$/, /^stp\s+port\s+\d{1,2}\s+(on|off)$/, /^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/, @@ -56,7 +55,7 @@ const conf_overwrite = [ /^lag\s+\d+\b/, /^laghash\b/, /^isolate\s+\d{1,2}\b/, - /^stp\s+(prio|hello|maxage|fwd|txhold|version|failsafe)\b/, + /^stp\s+(prio|hello|maxage|fwd|txhold|version)\b/, /^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter|p2p)\b/, /^igmp\b/, /^mtu\s+\d{1,2}\b/, diff --git a/html/stp.html b/html/stp.html index 5bdbeb5..59a98e4 100644 --- a/html/stp.html +++ b/html/stp.html @@ -16,7 +16,7 @@

    Bridge settings

    - + @@ -25,7 +25,6 @@ -
    PriorityVersionHello [s]Max age [s]Fwd delay [s]Tx holdMgmt failsafe [s]PriorityVersionHello [s]Max age [s]Fwd delay [s]Tx hold

    Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.

    diff --git a/html/stp.js b/html/stp.js index ec73ee7..acb59e9 100644 --- a/html/stp.js +++ b/html/stp.js @@ -99,9 +99,7 @@ function fetchStp() { const s = JSON.parse(xhttp.responseText); if (!stpRows) buildPortsTable(s.ports); - document.getElementById("stpStat").textContent = s.fsT - ? "\u26a0 STP was disabled by the management failsafe (ports were blocked while management was unreachable). Review the topology before re-enabling." - : s.on + document.getElementById("stpStat").textContent = s.on ? (s.weRoot ? "This switch (" + bridgeSelf(s) + ") is the root bridge — topology changes: " + parseInt(s.tc, 16) @@ -133,7 +131,6 @@ function fetchStp() { document.getElementById("bMaxage").value = s.maxage; document.getElementById("bFwd").value = s.fwd; document.getElementById("bTxhold").value = s.txhold; - document.getElementById("bFailsafe").value = s.fs; for (const p of s.ports) { document.getElementById("en_" + p.p).value = (p.f & PF_ENABLED) ? "on" : "off"; document.getElementById("edge_" + p.p).value = @@ -179,8 +176,6 @@ window.addEventListener("load", function() { .addEventListener("change", e => stpCmd("stp fwd " + e.target.value)); document.getElementById("bTxhold") .addEventListener("change", e => stpCmd("stp txhold " + e.target.value)); - document.getElementById("bFailsafe") - .addEventListener("change", e => stpCmd("stp failsafe " + e.target.value)); document.getElementById("stpMode") .addEventListener("change", () => { stpDirty = true; }); diff --git a/httpd/httpd.c b/httpd/httpd.c index bc98013..9681caf 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -22,7 +22,6 @@ extern volatile __xdata uint8_t sfr_data[4]; extern volatile __xdata uint32_t ticks; /* 200 Hz free-running tick, owned by rtlplayground.c */ -volatile __xdata uint8_t mgmt_alive; /* consumed by the STP management failsafe */ extern __code uint8_t * __code hex; extern __code struct f_data f_data[]; extern __code char * __code mime_strings[]; @@ -551,8 +550,6 @@ void httpd_appcall(void) __xdata struct httpd_state * __xdata s = &(uip_conn->appstate); dbg_char('P'); - if (uip_newdata()) - mgmt_alive = 1; /* any HTTP activity proves management still works (STP failsafe) */ #ifdef DEBUG if (uip_newdata()) write_char('N'); diff --git a/httpd/page_impl.c b/httpd/page_impl.c index ac389e8..a53362e 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -582,10 +582,6 @@ void send_stp(void) itoa_html(stp_fwddelay_s); slen += strtox(outbuf + slen, ",\"txhold\":"); itoa_html(stp_txhold); - slen += strtox(outbuf + slen, ",\"fs\":"); - itoa_html(stp_failsafe_s); - slen += strtox(outbuf + slen, ",\"fsT\":"); - itoa_html(stp_failsafe_tripped); slen += strtox(outbuf + slen, ",\"rootPrio\":\""); byte_to_html(root_bridge.prio); byte_to_html(root_bridge.ext); diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 40e5bd4..1416553 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -46,11 +46,6 @@ __xdata uint8_t stp_fwddelay_s; __xdata uint8_t stp_rstp; __xdata uint8_t stp_txhold; -__xdata uint8_t stp_failsafe_s; -__xdata uint8_t stp_failsafe_cnt; /* seconds left of the armed window */ -__xdata uint8_t stp_failsafe_armed; -__xdata uint8_t stp_failsafe_tripped; -extern volatile __xdata uint8_t mgmt_alive; /* set by httpd on any request */ __xdata uint8_t stp_pflags[10]; __xdata uint32_t stp_pcost[10]; @@ -180,9 +175,7 @@ static void stp_status(void) } print_string("changes "); print_short(stp_tc_count); - print_string(" failsafe "); - itoa(stp_failsafe_s); - print_string(stp_failsafe_tripped ? "s TRIPPED\n" : "s\n"); + write_char('\n'); print_string("port state role edge\n"); reg_read_m(RTL837X_MSTP_STATES); for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { @@ -563,29 +556,6 @@ void stp_timers(void) __banked for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) stp_tx_budget[stp_i] = stp_txhold; - /* Management failsafe: armed as a one-shot window by "stp on". - * The first HTTP request inside the window proves management - * survived the new tree and disarms it; a silent window disables - * STP. Deliberately NOT conditioned on our own MSTP states: - * hardware incident 2026-07-21 showed a NEIGHBOR (TP-Link Easy - * Smart loop prevention) cutting our uplink in reaction to our - * BPDUs while our ASIC was all-forwarding - only going fully - * quiet (no BPDU TX) lets such a neighbor recover. */ - if (stp_failsafe_armed) { - if (mgmt_alive) { - stp_failsafe_armed = 0; - print_string("STP failsafe: management confirmed - disarmed\n"); - } else if (--stp_failsafe_cnt == 0) { - print_string("STP failsafe: no management activity - disabling STP\n"); - stp_failsafe_armed = 0; - stp_off(); - stpEnabled = 0; - stp_failsafe_tripped = 1; - return; - } - } - mgmt_alive = 0; - /* Link supervision. Without this the state machine never learns * that a port lost carrier: it keeps the port in forwarding, keeps * announcing on it, and never flushes what was learned behind it - @@ -685,8 +655,6 @@ void stp_defaults(void) __banked stp_fwddelay_s = 15; stp_rstp = 1; stp_txhold = 6; - stp_failsafe_s = 180; - stp_failsafe_tripped = 0; for (stp_i = 0; stp_i < 10; stp_i++) { /* enabled, auto-edge on: host-facing ports go forwarding after * 3 s of BPDU silence instead of the full forward delay */ @@ -798,10 +766,6 @@ void stp_parse(void) __banked __reentrant { if (cmd_compare(1, "on")) { print_string("STP enabled\n"); - stp_failsafe_tripped = 0; - stp_failsafe_cnt = stp_failsafe_s; - stp_failsafe_armed = (stp_failsafe_s && save_cmd) ? 1 : 0; - mgmt_alive = 0; stpEnabled = 1; stp_setup(); return; @@ -810,7 +774,6 @@ void stp_parse(void) __banked __reentrant print_string("STP disabled\n"); stp_off(); stpEnabled = 0; - stp_failsafe_armed = 0; return; } if (cmd_compare(1, "status")) { @@ -838,11 +801,6 @@ void stp_parse(void) __banked __reentrant if (stpEnabled) { /* (re)join: listen first */ stp_state_set(port, 0b01); port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; - if (stp_failsafe_s && save_cmd) { - stp_failsafe_armed = 1; - stp_failsafe_cnt = stp_failsafe_s; - mgmt_alive = 0; - } } } else if (cmd_compare(3, "off")) { stp_pflags[port] &= ~STP_PF_ENABLED; @@ -944,12 +902,6 @@ void stp_parse(void) __banked __reentrant if (stp_scratch < 1 || stp_scratch > 10) goto err; stp_txhold = stp_scratch; - } else if (cmd_compare(1, "failsafe")) { - /* 0 never arms; otherwise the length of the armed window */ - stp_failsafe_s = stp_scratch; - stp_failsafe_cnt = stp_scratch; - stp_failsafe_armed = (stp_scratch && stpEnabled && save_cmd) ? 1 : 0; - mgmt_alive = 0; } else { goto err; } diff --git a/rtl837x_stp.h b/rtl837x_stp.h index fb534ae..a8e68a8 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -27,8 +27,6 @@ extern __xdata uint8_t stp_maxage_s; /* max age, 6-40 s (default 20) */ extern __xdata uint8_t stp_fwddelay_s; /* forward delay, 4-30 s (default 15); our listen period */ extern __xdata uint8_t stp_rstp; /* 1 = RSTP BPDUs (v2), 0 = STP-compatible Config BPDUs (v0) */ extern __xdata uint8_t stp_txhold; /* max BPDUs per port per second (default 6) */ -extern __xdata uint8_t stp_failsafe_s; /* mgmt watchdog, seconds (0 = off) */ -extern __xdata uint8_t stp_failsafe_tripped; /* Per-port config/status flags (stp_pflags[]) */ #define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */ diff --git a/rtlplayground.c b/rtlplayground.c index b24f8f5..6e2d9df 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -122,7 +122,6 @@ __xdata uint8_t tx_seq; __xdata uint8_t stpEnabled; __xdata uint8_t igmpEnabled; -extern __xdata uint8_t stp_failsafe_armed; __xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ __code uint16_t bit_mask[16] = { From 35d26cb08f866cac2b01e96760e40440911a1497 Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 15:27:01 +0200 Subject: [PATCH 41/68] stp: name the status subcommand in the usage line "stp status" has always worked, but the line printed on a bad command listed only on and off, so the one subcommand that shows what the bridge thinks was the one you had to already know about. --- rtl837x_stp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 1416553..04afaa6 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -907,5 +907,5 @@ void stp_parse(void) __banked __reentrant } return; err: - print_string("Error: stp on|off | prio <0-15> | hello <1-10> | maxage <6-40> | fwd <4-30> | txhold <1-10> | version rstp|stp | port <1-9> on|off|edge|cost|prio|guard|filter ...\n"); + print_string("Error: stp on|off|status | prio <0-15> | hello <1-10> | maxage <6-40> | fwd <4-30> | txhold <1-10> | version rstp|stp | port <1-9> on|off|edge|cost|prio|guard|filter ...\n"); } From 426485610985884822246ef3cfdefb18dfd219e2 Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 15:27:41 +0200 Subject: [PATCH 42/68] stp: stop treating a port as an edge once it hears a BPDU 802.1D has a port leave the edge state when a BPDU arrives on it. Here the flag was only ever cleared by the loop latch, root guard, a link coming back, "stp on", "stp off" and the edge command itself, so a port that auto-edged during the three seconds of silence after link-up kept the flag for as long as it stayed up, whatever the neighbour sent. Two things read that flag. The status page prints it, so a port talking to a bridge reported edge 1 and there was no way to tell from the output whether a BPDU had ever arrived. More quietly, stp_topology_change() returns early for an edge port, which is right for a real one and wrong for this: a topology change on such a port was neither counted nor propagated, and port_l2_forget_port() never ran, so what was learned behind it stayed in the table. Only the flag is cleared. The port is not pushed back through the listen period, which would take a working link out of forwarding for a forward delay the first time a neighbour speaks. --- rtl837x_stp.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 04afaa6..7a9c894 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -451,6 +451,16 @@ void stp_in(void) __banked stp_bpdu_age[port] = 0; + /* A port that hears a BPDU is not an edge port, whatever it decided + * during the silence after the link came up. Only the flag is dropped: + * the port keeps whatever forwarding state the rules below give it, + * rather than being pushed back through the listen period, which would + * black-hole a working link for a forward delay on the first BPDU. The + * flag matters beyond the status page, since stp_topology_change() + * exempts edge ports and so would go on skipping the counter and the + * L2 flush for a port that has a bridge behind it. */ + stp_pflags[port] &= ~STP_PF_OPEREDGE; + if (STP_I->bpdu_type == 0x80) { /* TCN: a downstream bridge reports a topology change. Acknowledge it * on this port so the sender stops repeating; the change itself is From 988bddacf773e0a7408082aec17fa2b0c4a27bb8 Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 15:30:09 +0200 Subject: [PATCH 43/68] stp: compare the whole Bridge Identifier, not the priority byte and the MAC A Bridge Identifier is two priority octets followed by the MAC, compared as one unsigned number. The test here read the first priority octet and then went straight to the MAC, so the system ID extension in between was never looked at and two bridges differing only in it were ranked by MAC instead. The field is stored, sent and printed, just not compared. Ordinary single instance RSTP leaves the extension zero on both sides, which is why this has not shown up. Where it is not zero the ranking is simply wrong: same priority octet, extension 0x0a against 0x00, and the worse bridge wins if its MAC happens to be lower. cmpMAC becomes cmpBytes with a length, since the identifier is eight contiguous bytes in both the packet overlay and root_bridge, and the loop was already doing the right thing for six of them. sdcc lays the struct out with no padding, checked, so the eight byte compare is the standard's rule written directly. --- rtl837x_stp.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 7a9c894..f8ee6b9 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -208,9 +208,12 @@ static void stp_record_designated(uint8_t port) __reentrant } -signed char cmpMAC(__xdata uint8_t *m1, __xdata uint8_t *m2) __reentrant +/* Lexicographic compare of n bytes. A MAC is 6 of them; a Bridge Identifier + * is 8, the two priority octets ahead of the MAC, compared as one unsigned + * number per 802.1D. */ +signed char cmpBytes(__xdata uint8_t *m1, __xdata uint8_t *m2, uint8_t n) __reentrant { - for (uint8_t i = 0; i < 6; i++) { + for (uint8_t i = 0; i < n; i++) { if (m1[i] == m2[i]) continue; if (m1[i] < m2[i]) @@ -499,7 +502,7 @@ void stp_in(void) __banked * still receiving. Pull the cable and the re-arming stops, so the loser * comes back on its own after a forward delay - and the link * supervision above gets there first anyway. */ - if (cmpMAC(STP_I->bridge.mac, uip_ethaddr.addr) == 0) { + if (cmpBytes(STP_I->bridge.mac, uip_ethaddr.addr, 6) == 0) { /* Equal means the frame came back on the port it left: a loop * further out, behind an unmanaged switch. There is no pair to * pick from, so that port holds itself down - and since it can @@ -527,9 +530,11 @@ void stp_in(void) __banked stp_record_designated(port); - /* Better root than the one we know? */ - if (STP_I->root.prio < root_bridge.prio - || ((STP_I->root.prio == root_bridge.prio) && cmpMAC(STP_I->root.mac, root_bridge.mac) < 0)) { + /* Better root than the one we know? The identifier is priority, system + * ID extension and MAC in that order: comparing the priority byte and + * then jumping to the MAC skipped the twelve bits in between, so two + * bridges differing only in the extension were ranked by MAC. */ + if (cmpBytes((__xdata uint8_t *)&STP_I->root, (__xdata uint8_t *)&root_bridge, 8) < 0) { /* Root guard: this port must never become our path to the root. */ if (stp_pflags[port] & STP_PF_ROOTGUARD) { print_string("STP: root guard blocking port "); From 38b19820e464f4555566401afa2df50b7cf83817 Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 15:32:01 +0200 Subject: [PATCH 44/68] stp: show how long ago each port last heard a BPDU The status output named state, role and edge, none of which separates a port nobody is speaking (R)STP to from a port whose BPDUs we are dropping. Both look the same: forwarding, designated, edge, and the tree rooted at ourselves. stp_in() leaves on eight different conditions, from a short frame through an unexpected LLC header to a disabled port, and none of them says anything. stp_bpdu_age was already maintained for the ageing rules, so this only prints it, in seconds and capped at 255. A column that counts up means nothing is arriving; a column that stays near zero means frames are arriving and any disagreement about the tree is ours. Eighty two bytes of BANK2 and two of xdata, most of it the sixteen bit divide by the tick rate. It comes out of a branch that gives back three hundred and twenty eight, so it is affordable, and printing raw ticks to save it would put the reader back to converting in their head. --- rtl837x_stp.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index f8ee6b9..ca56f8d 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -61,6 +61,7 @@ __xdata struct bridge root_bridge; __xdata uint32_t root_bridge_cost; /* our cost to the root (rx cost + root port cost) */ __xdata uint8_t stp_root_port; /* 0xff = we are the root */ __xdata uint16_t stp_tc_count; +__xdata uint16_t stp_scratch16; /* scratch for status printing only */ __xdata uint16_t port_timers[10]; /* listen-period countdown (0 = not listening) */ __xdata uint16_t port_hello[10]; /* hello TX countdown */ @@ -176,7 +177,7 @@ static void stp_status(void) print_string("changes "); print_short(stp_tc_count); write_char('\n'); - print_string("port state role edge\n"); + print_string("port state role edge bpdu\n"); reg_read_m(RTL837X_MSTP_STATES); for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { write_char(' '); @@ -187,6 +188,13 @@ static void stp_status(void) print_byte(stp_i == stp_root_port ? 1 : 2); print_string(" "); print_byte(stp_pflags[stp_i] & STP_PF_OPEREDGE ? 1 : 0); + /* Seconds since the last BPDU on this port, capped at 255. Without + * it nothing in the output separates "nobody is speaking (R)STP + * out there" from "we are dropping what arrives", and stp_in() + * leaves on eight different conditions without saying so. */ + print_string(" "); + stp_scratch16 = stp_bpdu_age[stp_i] / STP_HZ; + itoa(stp_scratch16 > 255 ? 255 : (uint8_t)stp_scratch16); write_char('\n'); } } From 266c95e4d7e65513eaec20e6d38ee7becc3c50d0 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:38:32 +0200 Subject: [PATCH 45/68] stp: name the port state, role and edge in stp status The table printed the ASIC's raw two bit state, a 1 or a 2 for the role and a 1 or a 0 for the edge flag, so reading it meant having the source open next to the console. The columns carry the words now: port state role edge bpdu 05 fwd desg yes 255 01 block desg no 21 02 learn desg no 5 03 fwd root no 0 They come from fixed width tables indexed by the same values as before, so nothing about how any of the three is derived changes, and the columns line up under the header without a formatter. The role column still only tells the root port from everything else, because that is all the state machine tracks. A port sitting in blocking because a better BPDU arrived on it reads as designated here. Naming the column makes that visible rather than introducing it. 154 bytes of BANK2, nothing in BANK1, xdata or internal RAM. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0. --- rtl837x_stp.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index ca56f8d..4e5e6c8 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -153,6 +153,21 @@ static void print_bridge_id(uint8_t prio, uint8_t ext, __xdata uint8_t *mac) __r } +/* Fixed width columns so the rows line up under the header without a + * formatter. The state indices are the ASIC's own two bits, in the order + * stp_state_set() writes them. */ +static __code const char stp_state_txt[] = "off blocklearnfwd "; +static __code const char stp_role_txt[] = "desgroot"; +static __code const char stp_edge_txt[] = "no yes "; + +static void print_field(__code const char *txt, uint8_t idx, uint8_t width) __reentrant +{ + txt += idx * width; + while (width--) + write_char(*txt++); +} + + /* Where you look when the tree is not what you expected. */ static void stp_status(void) { @@ -182,12 +197,15 @@ static void stp_status(void) for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { write_char(' '); print_byte(machine.log_to_phys_port[stp_i]); - print_string(" "); - print_byte((sfr_data[3 - (stp_i >> 2)] >> ((stp_i << 1) & 0x7)) & 0x3); - print_string(" "); - print_byte(stp_i == stp_root_port ? 1 : 2); - print_string(" "); - print_byte(stp_pflags[stp_i] & STP_PF_OPEREDGE ? 1 : 0); + print_string(" "); + print_field(stp_state_txt, (sfr_data[3 - (stp_i >> 2)] >> ((stp_i << 1) & 0x7)) & 0x3, 5); + write_char(' '); + /* Only the root port is named. Everything else reads as designated + * because that is all the state machine tracks today; an alternate + * port is a designated one that happens to sit in blocking. */ + print_field(stp_role_txt, stp_i == stp_root_port ? 1 : 0, 4); + write_char(' '); + print_field(stp_edge_txt, stp_pflags[stp_i] & STP_PF_OPEREDGE ? 1 : 0, 4); /* Seconds since the last BPDU on this port, capped at 255. Without * it nothing in the output separates "nobody is speaking (R)STP * out there" from "we are dropping what arrives", and stp_in() From 2c26a4f3898f3be2f86881f9619d0d801139bb1a Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:42:39 +0200 Subject: [PATCH 46/68] stp: count the BPDUs each port has sent The table could say a port was designated and had heard nothing, which is two different situations wearing the same face: either we are not announcing on that segment, or we are and nobody is answering. Telling them apart needed a capture on the far side. port state role edge tx bpdu 05 fwd desg yes 2a 255 01 block desg no 2a 21 03 fwd root no 00 0 The tx column counts BPDUs actually handed to the hardware, so it moves only past the enable, filter and tx hold checks in stp_cnf_send(). A designated port has to show it climbing once per hello time. The root port never does, because we do not announce back towards the root, so a neighbour that has taken us as root falls silent in both directions on that link and the two columns together say exactly that rather than looking like a fault. The counter is a byte and wraps at 256. It is meant to be watched moving, not summed, and it starts again when STP is enabled. 67 bytes of BANK2 and 10 of xdata, nothing in BANK1 or internal RAM. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0. --- rtl837x_stp.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 4e5e6c8..87bfb22 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -67,6 +67,7 @@ __xdata uint16_t port_timers[10]; /* listen-period countdown (0 = not listening) __xdata uint16_t port_hello[10]; /* hello TX countdown */ __xdata uint16_t stp_bpdu_age[10]; /* ticks since last BPDU seen on port (saturating) */ __xdata uint8_t stp_tx_budget[10]; /* tx hold: BPDUs left in the current second */ +__xdata uint8_t stp_tx_count[10]; /* BPDUs actually put on the wire, wraps at 256 */ __xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */ __xdata uint16_t stp_link_prev; /* carrier bitmap as of the last check */ __xdata uint16_t stp_link_now; @@ -192,7 +193,7 @@ static void stp_status(void) print_string("changes "); print_short(stp_tc_count); write_char('\n'); - print_string("port state role edge bpdu\n"); + print_string("port state role edge tx bpdu\n"); reg_read_m(RTL837X_MSTP_STATES); for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { write_char(' '); @@ -206,11 +207,18 @@ static void stp_status(void) print_field(stp_role_txt, stp_i == stp_root_port ? 1 : 0, 4); write_char(' '); print_field(stp_edge_txt, stp_pflags[stp_i] & STP_PF_OPEREDGE ? 1 : 0, 4); + /* BPDUs we put on the wire here. A designated port must show this + * climbing once per hello; the root port never does, because we do + * not announce back towards the root. Without it the only way to + * tell "we are silent" from "the neighbour is not listening" is a + * capture on the far side. */ + write_char(' '); + print_byte(stp_tx_count[stp_i]); /* Seconds since the last BPDU on this port, capped at 255. Without * it nothing in the output separates "nobody is speaking (R)STP * out there" from "we are dropping what arrives", and stp_in() * leaves on eight different conditions without saying so. */ - print_string(" "); + write_char(' '); stp_scratch16 = stp_bpdu_age[stp_i] / STP_HZ; itoa(stp_scratch16 > 255 ? 255 : (uint8_t)stp_scratch16); write_char('\n'); @@ -330,6 +338,7 @@ void stp_cnf_send(uint8_t port) __reentrant return; } stp_tx_budget[port]--; + stp_tx_count[port]++; STP_O->stp_addr[0] = 0x01; STP_O->stp_addr[1] = 0x80; STP_O->stp_addr[2] = 0xc2; STP_O->stp_addr[3] = STP_O->stp_addr[4] = STP_O->stp_addr[5] = 0x00; @@ -745,6 +754,7 @@ void stp_setup(void) __banked stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); stp_bpdu_age[stp_i] = 0; stp_tx_budget[stp_i] = stp_txhold; + stp_tx_count[stp_i] = 0; if (!(stp_pflags[stp_i] & STP_PF_ENABLED) || (stp_pflags[stp_i] & STP_PF_ADMEDGE)) { /* not participating, or admin edge: forwarding immediately */ if (stp_pflags[stp_i] & STP_PF_ADMEDGE) From 201c7e333d8baceb3420057ce864250260a3c247 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:00:06 +0200 Subject: [PATCH 47/68] httpd: drop the byte-access justification from u32hex_html Byte access instead of 32-bit shifts is how this codebase works everywhere, so the comment explained a house rule at one call site. --- httpd/page_impl.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index a53362e..31aad43 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -546,8 +546,6 @@ static __xdata uint8_t * __xdata pi_mac; static void u32hex_html(void) { - /* byte access instead of uint32 shifts: sdcc/mcs51 expands each - * 32-bit shift into a large helper sequence. Little-endian layout. */ __xdata uint8_t *b = (__xdata uint8_t *)&pi_u32; byte_to_html(b[3]); byte_to_html(b[2]); From 78b3971782dd726e45cf619f15c75602c1bca59c Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:46:02 +0200 Subject: [PATCH 48/68] stp: pass a received topology change through the switch The tree structure already crossed the switch by regeneration, but the topology-change information did not: a received TC flag was ignored and a TCN only acknowledged, so bridges behind this one kept stale entries until normal aging. A TC flag in a received BPDU now flushes the other non-edge ports once and arms the transmit window our BPDUs already copy the flag from, refreshed to hello+1 seconds by every further flagged frame so it ends one hello after the neighbour stops, without shortening the longer window a local change arms. A TCN is acknowledged as before and then treated like a local change on that port. --- doc/stp.md | 13 +++++++++++++ rtl837x_stp.c | 28 +++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/doc/stp.md b/doc/stp.md index 61bb5e5..0ccca7c 100644 --- a/doc/stp.md +++ b/doc/stp.md @@ -80,6 +80,16 @@ A port entering the tree spends `fwd` seconds in blocking before it forwards (an edge port skips the wait). Root information is discarded after `maxage` seconds without a BPDU, and the switch then reclaims the root role. +## Topology changes + +A change on a local non-edge port (the link coming or going, a port promoted +to forwarding) flushes the addresses learned on it and sets the TC flag in +our BPDUs for `maxage + fwd` seconds. A TC flag received in a BPDU is passed +on: the switch flushes the other non-edge ports once and keeps the flag in +its own BPDUs until one hello after the last flagged frame, so the +notification crosses the switch instead of dying at it. A legacy TCN is +acknowledged with TCA and then treated like a local change. + ## Bridge settings ``` @@ -139,3 +149,6 @@ The `stp status` command prints the same view on the serial console. converge, but through the timers rather than the fast transition. * Port roles are approximated: the root port and designated ports are distinguished, alternate/backup are not. +* Topology changes propagate away from the root only: nothing is announced + on the root port (no TCN and no BPDUs at all), so bridges upstream rely on + their own detection. diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 87bfb22..5bafe17 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -501,12 +501,14 @@ void stp_in(void) __banked if (STP_I->bpdu_type == 0x80) { /* TCN: a downstream bridge reports a topology change. Acknowledge it - * on this port so the sender stops repeating; the change itself is - * counted (and, once implemented, propagated rootward). */ + * on this port so the sender stops repeating, then treat it like a + * change of our own: flush the port and carry the TC flag on the + * designated ports for the full window. No TCN goes towards the + * root, so a legacy root further up keeps its normal aging. */ stp_tx_flags_extra = 0x80; /* Topology Change Acknowledgment */ stp_cnf_send(port); /* transmits internally */ uip_len = 0; /* ...so handle_rx must not TX again */ - stp_tc_count++; + stp_topology_change(port); return; } @@ -563,6 +565,26 @@ void stp_in(void) __banked return; } + /* Topology Change in transit. The flag arms a short window that our + * own BPDUs copy downstream (the TX side already sends TC while + * stp_tc_while runs) and that each further flagged BPDU refreshes, so + * it expires one hello after the neighbour stops - without shortening + * the long window a local change may have armed. The flush runs once, + * on the arming edge: everything learned on the other non-edge ports + * may sit behind the moved link and must be relearned. */ + if (STP_I->flags & 0x01) { + if (!stp_tc_while) { + uint8_t i; + stp_tc_count++; + for (i = machine.min_port; i <= machine.max_port; i++) + if (i != port && (stp_pflags[i] & STP_PF_ENABLED) + && !(stp_pflags[i] & STP_PF_OPEREDGE)) + port_l2_forget_port(i); + } + if (stp_tc_while < ((uint16_t)stp_hello_s + 1) * STP_HZ) + stp_tc_while = ((uint16_t)stp_hello_s + 1) * STP_HZ; + } + stp_record_designated(port); /* Better root than the one we know? The identifier is priority, system From cbd2a8c080366509bf6d9085caa4dd3fd2176cac Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:56:31 +0200 Subject: [PATCH 49/68] httpd: drop the tick rate comment on the ticks extern --- httpd/httpd.c | 1 - 1 file changed, 1 deletion(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index 9681caf..8d98b8f 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -21,7 +21,6 @@ extern volatile __xdata uint8_t sfr_data[4]; extern volatile __xdata uint32_t ticks; -/* 200 Hz free-running tick, owned by rtlplayground.c */ extern __code uint8_t * __code hex; extern __code struct f_data f_data[]; extern __code char * __code mime_strings[]; From addca1cb265337a1cfb200889775baea5069baf5 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:24:24 +0200 Subject: [PATCH 50/68] httpd: use local variables for the loop indexes in send_stp The three xdata bytes and the reused stp_we_root scratch are gone; the compiler needs a register for an index either way. Frees four bytes of xdata and 29 bytes of code. --- httpd/page_impl.c | 64 ++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 31aad43..75fcf08 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -536,14 +536,11 @@ void send_lag(void) } -/* STP status and configuration for the Spanning Tree page ("/stp.json"). */ -__xdata uint8_t stp_we_root; -__xdata uint8_t pi_i, pi_j, pi_j2; - static __xdata uint32_t pi_u32; static __xdata uint8_t pi_prio, pi_ext; static __xdata uint8_t * __xdata pi_mac; + static void u32hex_html(void) { __xdata uint8_t *b = (__xdata uint8_t *)&pi_u32; @@ -553,16 +550,20 @@ static void u32hex_html(void) byte_to_html(b[0]); } + static void bridge_to_html(void) { byte_to_html(pi_prio); byte_to_html(pi_ext); - for (pi_j2 = 0; pi_j2 < 6; pi_j2++) - byte_to_html(pi_mac[pi_j2]); + for (uint8_t i = 0; i < 6; i++) + byte_to_html(pi_mac[i]); } + void send_stp(void) { + uint8_t i, j, st, dsg; + dbg_string("send_stp called\n"); slen = strtox(outbuf, HTTP_RESPONCE_JSON); @@ -584,19 +585,18 @@ void send_stp(void) byte_to_html(root_bridge.prio); byte_to_html(root_bridge.ext); slen += strtox(outbuf + slen, "\",\"rootMac\":\""); - for (pi_j = 0; pi_j < 6; pi_j++) - byte_to_html(root_bridge.mac[pi_j]); + for (j = 0; j < 6; j++) + byte_to_html(root_bridge.mac[j]); slen += strtox(outbuf + slen, "\",\"myMac\":\""); - for (pi_j = 0; pi_j < 6; pi_j++) - byte_to_html(uip_ethaddr.addr[pi_j]); + for (j = 0; j < 6; j++) + byte_to_html(uip_ethaddr.addr[j]); slen += strtox(outbuf + slen, "\",\"cost\":\""); byte_to_html(root_bridge_cost >> 24); byte_to_html(root_bridge_cost >> 16); byte_to_html(root_bridge_cost >> 8); byte_to_html(root_bridge_cost); - stp_we_root = (stp_root_port == 0xff) ? 1 : 0; slen += strtox(outbuf + slen, "\",\"weRoot\":"); - bool_to_html(stp_we_root); + bool_to_html(stp_root_port == 0xff ? 1 : 0); slen += strtox(outbuf + slen, ",\"rootPort\":"); itoa_html(stp_root_port == 0xff ? 0 : machine.log_to_phys_port[stp_root_port]); slen += strtox(outbuf + slen, ",\"tc\":\""); @@ -604,48 +604,44 @@ void send_stp(void) byte_to_html(stp_tc_count); slen += strtox(outbuf + slen, "\",\"ports\":["); reg_read_m(RTL837X_MSTP_STATES); - for (pi_i = machine.min_port; pi_i <= machine.max_port; pi_i++) { + for (i = machine.min_port; i <= machine.max_port; i++) { slen += strtox(outbuf + slen, "{\"p\":"); - itoa_html(machine.log_to_phys_port[pi_i]); + itoa_html(machine.log_to_phys_port[i]); slen += strtox(outbuf + slen, ",\"st\":"); - stp_we_root = (sfr_data[3 - (pi_i >> 2)] >> ((pi_i << 1) & 0x7)) & 0x3; - itoa_html(stp_we_root); - /* role (approximated): 0 none/disabled, 1 root, 2 designated, 3 alternate(blocked) */ + st = (sfr_data[3 - (i >> 2)] >> ((i << 1) & 0x7)) & 0x3; + itoa_html(st); slen += strtox(outbuf + slen, ",\"role\":"); - if (!(stp_pflags[pi_i] & STP_PF_ENABLED) || (stp_pflags[pi_i] & STP_PF_TRIPPED)) + if (!(stp_pflags[i] & STP_PF_ENABLED) || (stp_pflags[i] & STP_PF_TRIPPED)) itoa_html(0); - else if (pi_i == stp_root_port) + else if (i == stp_root_port) itoa_html(1); - else if (stp_we_root == 3) + else if (st == 3) itoa_html(2); else itoa_html(3); slen += strtox(outbuf + slen, ",\"f\":"); - itoa_html(stp_pflags[pi_i]); - /* path cost (raw hex, full 0..200M range), priority, p2p */ + itoa_html(stp_pflags[i]); slen += strtox(outbuf + slen, ",\"pc\":\""); - pi_u32 = stp_pcost[pi_i]; u32hex_html(); + pi_u32 = stp_pcost[i]; u32hex_html(); slen += strtox(outbuf + slen, "\",\"prio\":"); - itoa_html(stp_pprio[pi_i]); + itoa_html(stp_pprio[i]); slen += strtox(outbuf + slen, ",\"p2\":"); - itoa_html(stp_pp2p[pi_i]); - /* designated info: a freshly heard BPDU wins, else we are the - * segment's designated bridge and report our own values */ - stp_we_root = stp_dpid[pi_i] && stp_bpdu_age[pi_i] < (uint16_t)stp_maxage_s * STP_HZ; + itoa_html(stp_pp2p[i]); + dsg = stp_dpid[i] && stp_bpdu_age[i] < (uint16_t)stp_maxage_s * STP_HZ; slen += strtox(outbuf + slen, ",\"db\":\""); - if (stp_we_root) { - pi_prio = stp_dbridge[pi_i].prio; pi_ext = stp_dbridge[pi_i].ext; - pi_mac = stp_dbridge[pi_i].mac; + if (dsg) { + pi_prio = stp_dbridge[i].prio; pi_ext = stp_dbridge[i].ext; + pi_mac = stp_dbridge[i].mac; } else { pi_prio = stp_prio; pi_ext = 0; pi_mac = uip_ethaddr.addr; } bridge_to_html(); slen += strtox(outbuf + slen, "\",\"dp\":\""); - byte_to_html(stp_we_root ? (stp_dpid[pi_i] >> 8) : stp_pprio[pi_i]); - byte_to_html(stp_we_root ? stp_dpid[pi_i] : (pi_i + 1)); + byte_to_html(dsg ? (stp_dpid[i] >> 8) : stp_pprio[i]); + byte_to_html(dsg ? stp_dpid[i] : (i + 1)); slen += strtox(outbuf + slen, "\",\"dc\":\""); - pi_u32 = stp_we_root ? stp_dcost[pi_i] : root_bridge_cost; u32hex_html(); + pi_u32 = dsg ? stp_dcost[i] : root_bridge_cost; u32hex_html(); slen += strtox(outbuf + slen, "\"},"); } slen -= 1; // remove comma From f1f3d521b5d3bee0c064e63d0587c40959dd5e0e Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:25:21 +0200 Subject: [PATCH 51/68] stp: drop the comment on the rtl837x_port.h include --- rtl837x_stp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 5bafe17..03fbeef 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -14,7 +14,7 @@ #include "rtl837x_sfr.h" #include "rtl837x_regs.h" #include "rtl837x_stp.h" -#include "rtl837x_port.h" /* port_pvid_get(), port_l2mc_set() */ +#include "rtl837x_port.h" #include "uip.h" #include "machine.h" From 50b68f908e9f63135ba064aee8fd8efc4d7606d7 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:25:41 +0200 Subject: [PATCH 52/68] stp: drop the owner comments on the externs --- rtl837x_stp.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 03fbeef..3cc8162 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -20,7 +20,7 @@ extern __code struct machine machine; extern __xdata uint8_t sfr_data[4]; -extern __xdata struct machine_runtime machine_detected; /* owned by rtl837x_port.c */ +extern __xdata struct machine_runtime machine_detected; __xdata uint16_t stp_fdb_vid; __xdata uint8_t stp_fdb_i; @@ -30,7 +30,6 @@ extern __xdata struct uip_eth_addr uip_ethaddr; extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed per-frame for BPDUs */ -/* CLI tokenizer state + helpers (owned by cmd_parser.c, HOME bank) */ extern __xdata uint8_t cmd_buffer[CMD_BUF_SIZE]; extern __xdata uint8_t cmd_words_len; extern __xdata uint8_t cmd_words_b[15]; From 3cd9131795c5854ad127764ed07f6be74fbaddb7 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:26:21 +0200 Subject: [PATCH 53/68] stp: drop the inline comments in stp_loop_hold_peer The port number comes out of a received BPDU, so the range check is there to keep a forged frame from naming a port this module does not manage - including the CPU port, which would cost us the management path. Nothing outside min_port..max_port would ever release the block either, because stp_timers() only counts down the ports it walks. --- rtl837x_stp.c | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 3cc8162..e929392 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -287,21 +287,13 @@ static void stp_topology_change(uint8_t port) __reentrant */ static void stp_loop_hold_peer(uint8_t port) __reentrant { - /* The port number arrives in a BPDU, so it is somebody else's data, - * and our own bridge MAC is public in every BPDU we send - a forged - * frame can name any port it likes. Bound it to the ports this module - * actually manages, like every other loop here does. Out of that - * range nothing would ever release the block either: stp_timers() - * walks min_port..max_port and skips ports that are not STP-enabled, - * so their port_timers[] never counts down. Naming the CPU port would - * otherwise cost us our own management path. */ if (port < machine.min_port || port > machine.max_port) return; if (!(stp_pflags[port] & STP_PF_ENABLED)) return; if (stp_pflags[port] & STP_PF_TRIPPED) return; - if (!port_timers[port]) { /* not held down yet */ + if (!port_timers[port]) { print_string("STP: loop detected, blocking port "); print_port_nl(port); stp_state_set(port, 0b01); From 9df9eece3fa2b941f61d562704cf3d7594cf9956 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:26:51 +0200 Subject: [PATCH 54/68] stp: drop the comment on the CPU tag flags Worth keeping out of the code but on record: RTL_TAG_KEEP is deliberately not set here. On an LLC/802.3 frame the ASIC drops the frame outright with that flag, while on ethertype frames such as LACP it works fine. --- rtl837x_stp.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index e929392..cb2d5e8 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -337,12 +337,6 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->rtl_tag.tag = HTONS(RTL_FRAME_TAG_ID); STP_O->rtl_tag.version = RTL_FRAME_TAG_VERSION; STP_O->rtl_tag.reason = 0x00; - /* Through HTONS like every tag field: raw 0x0020 lands on the wire as - * 0x2000 (EFID), the ASIC fails to parse the tag and floods the frame - * with the 0x8899 header still on it (same bug class as LACP had). - * NOTE: no RTL_TAG_KEEP here - hardware-verified that KEEP on an - * LLC/802.3 (length-field) frame makes the ASIC drop it entirely, - * while the same flag works fine on ethertype frames (LACP). */ STP_O->rtl_tag.flags = HTONS(RTL_TAG_LEARN_DIS); STP_O->rtl_tag.pmask = HTONS(((uint16_t)1) << port); From 236eac610db24787f88e4da96eed10513d03601d Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:29:51 +0200 Subject: [PATCH 55/68] stp: name the BPDU version, type and flag constants The comments they replace are gone with them. Two things the comments carried that the names do not: Accepting version >= 2 rather than == 2 is deliberate. 802.1D-2004 14.4 has an RSTP bridge accept a higher Protocol Version and treat it as RST, and MSTP sends version 3 type 2 with a prefix identical to an RST BPDU for exactly that reason, so insisting on == 2 would make us blind to every MST bridge on the segment. In the TCN branch stp_cnf_send() transmits by itself, so uip_len is cleared afterwards to keep handle_rx() from sending the frame twice. --- rtl837x_stp.c | 79 ++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index cb2d5e8..290db65 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -137,6 +137,24 @@ struct stp_pkt_in { #define STP_O ((__xdata struct stp_pkt *)&uip_buf[RTL_FRAME_DESC_SIZE]) #define STP_I ((__xdata struct stp_pkt_in *)&uip_buf[0]) +#define BPDU_VER_STP 0x00 +#define BPDU_VER_RSTP 0x02 + +#define BPDU_TYPE_CONFIG 0x00 +#define BPDU_TYPE_RST 0x02 +#define BPDU_TYPE_TCN 0x80 + +#define BPDU_LEN_CONFIG 0x26 // LLC and a 35 byte body +#define BPDU_LEN_RST 0x27 // LLC and a 36 byte body + +#define BPDU_FLAG_TC 0x01 +#define BPDU_FLAG_LEARNING 0x10 +#define BPDU_FLAG_FORWARDING 0x20 +#define BPDU_FLAG_TCACK 0x80 + +#define BPDU_ROLE_ROOT (0b10 << 2) +#define BPDU_ROLE_DESIGNATED (0b11 << 2) + /* Console messages name the port on the front panel, not the internal index. */ static void print_port_nl(uint8_t port) __reentrant { @@ -345,34 +363,22 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->ctrl = 0x03; STP_O->proto = 0x0000; if (stp_rstp) { - /* 802.3 length = LLC (3) + RST BPDU body (36, incl. version1_length) */ - STP_O->msg_len = HTONS(0x27); - STP_O->version = 0x02; /* RSTP */ - STP_O->bpdu_type = 0x02; /* Rapid Spanning Tree BPDU */ - /* Flags describe this port, so derive them instead of announcing - * designated+learning+forwarding unconditionally: a blocked port - * claiming to forward, or the root port claiming designated, is a - * lie on the wire even when nothing downstream acts on it (yet). - * Role is root on the root port and designated everywhere else - - * there is no alternate/backup role computation, so a port blocked - * by loop detection still transmits as designated, just with the - * learning and forwarding bits clear. Those two mirror the ASIC - * state (0b11 = forwarding); a listening or blocked port sends - * neither. */ + STP_O->msg_len = HTONS(BPDU_LEN_RST); + STP_O->version = BPDU_VER_RSTP; + STP_O->bpdu_type = BPDU_TYPE_RST; reg_read_m(RTL837X_MSTP_STATES); - STP_O->flags = (uint8_t)((port == stp_root_port ? 0b10 : 0b11) << 2); + STP_O->flags = port == stp_root_port ? BPDU_ROLE_ROOT : BPDU_ROLE_DESIGNATED; if (((sfr_data[3 - (port >> 2)] >> ((port << 1) & 0x7)) & 0b11) == 0b11) - STP_O->flags |= 0x30; /* learning + forwarding */ + STP_O->flags |= BPDU_FLAG_LEARNING | BPDU_FLAG_FORWARDING; } else { - /* 802.3 length = LLC (3) + Config BPDU body (35) */ - STP_O->msg_len = HTONS(0x26); - STP_O->version = 0x00; /* legacy STP */ - STP_O->bpdu_type = 0x00; /* Config BPDU */ + STP_O->msg_len = HTONS(BPDU_LEN_CONFIG); + STP_O->version = BPDU_VER_STP; + STP_O->bpdu_type = BPDU_TYPE_CONFIG; STP_O->flags = 0x00; } if (stp_tc_while) - STP_O->flags |= 0x01; /* Topology Change */ - STP_O->flags |= stp_tx_flags_extra; /* e.g. TCA in reply to a TCN */ + STP_O->flags |= BPDU_FLAG_TC; + STP_O->flags |= stp_tx_flags_extra; stp_tx_flags_extra = 0; memcpy(STP_O->src_addr, uip_ethaddr.addr, 6); @@ -447,16 +453,10 @@ void stp_in(void) __banked return; if (STP_I->proto) return; - /* Accept RSTP BPDUs (v2 type 2), legacy Config BPDUs (v0 type 0) and - * legacy TCN BPDUs (v0 type 0x80, 4-byte body). - * Version 2 *or greater*: 802.1D-2004 14.4 requires an RSTP bridge to - * accept a higher Protocol Version and treat it as RST, ignoring what - * it does not understand. MSTP (802.1s) sends version 3 type 2 with a - * prefix deliberately identical to an RST BPDU for exactly this reason; - * insisting on == 2 makes us blind to every MST bridge on the segment. */ - if (!((STP_I->version >= 2 && STP_I->bpdu_type == 2) - || (STP_I->version == 0 - && (STP_I->bpdu_type == 0 || STP_I->bpdu_type == 0x80)))) + if (!((STP_I->version >= BPDU_VER_RSTP && STP_I->bpdu_type == BPDU_TYPE_RST) + || (STP_I->version == BPDU_VER_STP + && (STP_I->bpdu_type == BPDU_TYPE_CONFIG + || STP_I->bpdu_type == BPDU_TYPE_TCN)))) return; if (!(stp_pflags[port] & STP_PF_ENABLED) || (stp_pflags[port] & STP_PF_FILTER)) @@ -484,15 +484,10 @@ void stp_in(void) __banked * L2 flush for a port that has a bridge behind it. */ stp_pflags[port] &= ~STP_PF_OPEREDGE; - if (STP_I->bpdu_type == 0x80) { - /* TCN: a downstream bridge reports a topology change. Acknowledge it - * on this port so the sender stops repeating, then treat it like a - * change of our own: flush the port and carry the TC flag on the - * designated ports for the full window. No TCN goes towards the - * root, so a legacy root further up keeps its normal aging. */ - stp_tx_flags_extra = 0x80; /* Topology Change Acknowledgment */ - stp_cnf_send(port); /* transmits internally */ - uip_len = 0; /* ...so handle_rx must not TX again */ + if (STP_I->bpdu_type == BPDU_TYPE_TCN) { + stp_tx_flags_extra = BPDU_FLAG_TCACK; + stp_cnf_send(port); + uip_len = 0; stp_topology_change(port); return; } @@ -557,7 +552,7 @@ void stp_in(void) __banked * the long window a local change may have armed. The flush runs once, * on the arming edge: everything learned on the other non-edge ports * may sit behind the moved link and must be relearned. */ - if (STP_I->flags & 0x01) { + if (STP_I->flags & BPDU_FLAG_TC) { if (!stp_tc_while) { uint8_t i; stp_tc_count++; From f9d3dec50fed6f90d46b7b2821ec25b18fbc2a3f Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:32:18 +0200 Subject: [PATCH 56/68] Do not insert the management VLAN tag into a CPU-tagged frame tcpip_output() splices the 802.1Q tag in right behind the source address, which is exactly where the ASIC expects the RTL tag of a frame the CPU addressed to a port itself. The tag then ends up behind the VLAN tag, the ASIC does not find it, and the frame goes out flooded with the 0x8899 header still on it instead of being sent to the port that was asked for. Whether a frame is CPU-tagged is a property of the frame, so decide it here from the ether-type rather than having every sender of such a frame clear management_vlan around its tcpip_output() call. stp_cnf_send() did that, and no longer has to. --- rtl837x_stp.c | 13 ------------- rtlplayground.c | 8 ++++++-- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 290db65..583a3d6 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -28,7 +28,6 @@ __xdata uint8_t stp_fdb_i; extern __xdata struct uip_eth_addr uip_ethaddr; extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE + 2]; -extern __xdata uint16_t management_vlan; /* owned by rtlplayground.c; suppressed per-frame for BPDUs */ extern __xdata uint8_t cmd_buffer[CMD_BUF_SIZE]; extern __xdata uint8_t cmd_words_len; @@ -408,20 +407,8 @@ void stp_cnf_send(uint8_t port) __reentrant STP_O->fwd_delay = stp_fwddelay_s; STP_O->version1_length = 0; /* RST BPDU: no version-1 information */ - /* BPDUs are link-local and must egress untagged: with a management VLAN - * set, tcpip_output() splices an 802.1Q tag after the SA, shifting the - * in-frame rtl_tag out of the position the ASIC parses - the CPU tag then - * leaks onto the wire as 0x8899 and the BPDU is flooded, not sent. - * Hardware-verified fix, same as lacp_send(). */ - { - uint16_t saved_mgmt_vlan = management_vlan; - management_vlan = 0; - /* A legacy Config BPDU body is 35 bytes - without the trailing - * version-1 length byte that only the RST BPDU (36 bytes) carries. */ uip_len = stp_rstp ? sizeof(struct stp_pkt) : sizeof(struct stp_pkt) - 1; tcpip_output(); - management_vlan = saved_mgmt_vlan; - } } diff --git a/rtlplayground.c b/rtlplayground.c index 6e2d9df..02fe55c 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -203,6 +203,9 @@ struct nonq_frame { // The output frame structure with 802.1Q field and the padding moved before the buffer-start #define FRAME_Q ((__xdata struct q_frame *)&uip_buf[0]) +// Ether-type of the output frame, which is the RTL tag on a CPU-tagged frame +#define FRAME_ETHERTYPE (*(__xdata uint16_t *)&uip_buf[RTL_FRAME_DESC_SIZE + 2 * sizeof(struct uip_eth_addr)]) + void isr_timer0(void) __interrupt(1) { } @@ -1097,8 +1100,9 @@ void tcpip_output(void) FRAME->len = uip_len; FRAME->reserved_2[0] = 0x00; FRAME->reserved_2[1] = 0x00; - // For the management VLAN we insert an 802.1Q VLAN tag - if (management_vlan) { + // For the management VLAN we insert an 802.1Q VLAN tag, but never into a + // CPU-tagged frame, where the ASIC expects its tag right behind the addresses + if (management_vlan && FRAME_ETHERTYPE != HTONS(RTL_FRAME_TAG_ID)) { // Shift the ethernet header before the HW type including the rtl_frame_desc to the beginning of uip_buf // to allow space to insert the dot 1Q tag for (uint8_t i = 0; i < sizeof(struct q_frame) - DOT_1Q_TAG_SIZE; i++) From 810db48a00f552d61c74d3d5d14256a68f6cb10e Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:34:42 +0200 Subject: [PATCH 57/68] stp: drop the bare scope blocks around the port variable Declaring port at the top of stp_in() and stp_parse() does the same job without a block that is not indented like one. The static xdata copy in stp_in() went with it, it was only ever written. The argument count check in stp_parse() that lost its comment guards against cmd_compare(4, ..) reading a stale word from the previous command line, because cmd_words_b is not cleared between commands. --- rtl837x_stp.c | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 583a3d6..46e8770 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -414,26 +414,24 @@ void stp_cnf_send(uint8_t port) __reentrant void stp_in(void) __banked { - /* Robustness: never read fields past the received frame. 33 covers the - * header through bpdu_type; the full Config/RST body is re-checked below. - * (uip_len is consumed and zeroed at the end - keep a local view.) */ + uint8_t port; + + /* The header through bpdu_type is 33 bytes, the full Config/RST body is + * checked further down before anything past it is read. */ if (uip_len < 33) { uip_len = 0; return; } stp_rxlen = uip_len; - // By default we do not send anything out (handle_rx would TX otherwise) + // By default we do not send anything out uip_len = 0; /* Ingress port: low nibble of the CPU tag's pmask on RX */ stp_scratch = ((uint8_t)HTONS(STP_I->rtl_tag.pmask)) & 0x0f; if (stp_scratch < machine.min_port || stp_scratch > machine.max_port) return; - { - __xdata static uint8_t port_l; /* NOT stp_scratch: stp_state_set() clobbers it */ - uint8_t port = (port_l = stp_scratch); - (void)port_l; + port = stp_scratch; // Make sure this is the type of (R)STP packet we are interested in: if (!(STP_I->dsap == 0x42 && STP_I->ssap == 0x42 && STP_I->ctrl == 0x03)) @@ -583,7 +581,6 @@ void stp_in(void) __banked stp_msg_age = (STP_I->age > 254) ? 254 : (uint8_t)STP_I->age; root_bridge_cost = stp_dcost[port] + PCOST(port); } - } } @@ -804,6 +801,8 @@ void stp_off(void) __banked void stp_parse(void) __banked __reentrant { + uint8_t port; + if (cmd_compare(1, "on")) { print_string("STP enabled\n"); stpEnabled = 1; @@ -828,11 +827,7 @@ void stp_parse(void) __banked __reentrant goto err; if (atoi_byte(&stp_scratch, cmd_words_b[2]) || stp_scratch < 1 || stp_scratch > 9) goto err; - { - uint8_t port = machine.phys_to_log_port[stp_scratch - 1]; - /* every sub-command except on/off carries one more argument; without - * this check cmd_compare(4,..) would read a stale word from the - * PREVIOUS command line (cmd_words_b is not cleared between commands) */ + port = machine.phys_to_log_port[stp_scratch - 1]; if (cmd_words_len < 5 && !cmd_compare(3, "on") && !cmd_compare(3, "off")) goto err; if (cmd_compare(3, "on")) { @@ -904,7 +899,6 @@ void stp_parse(void) __banked __reentrant } else { goto err; } - } return; } From 85d1d7520a417d95abdf7fd081f1d302e27690f6 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:58:44 +0200 Subject: [PATCH 58/68] stp: send BPDUs with a per port source address 802.1D puts the port's own address in the source field and the bridge address only in the Bridge Identifier. We used the bridge address for both, and on this hardware that costs the management path. Measured on a SWTGW218AS: the ASIC learns the source address of a frame addressed to 01:80:c2:00:00:00, and the bridge's own address is not exempt. A BPDU that leaves a blocked port and comes back on a forwarding one therefore moves the management address off the CPU port, and frames for it are then sent down that port instead of to the CPU. Traffic between other stations is unaffected, which is what makes it look like the CPU port has been blocked. The derived address keeps the bridge address and sets the locally administered bit, so it differs from the bridge address in the first octet for any globally assigned OUI, with the port number in the low nibble of the last octet. Nothing here reads the source address of a received BPDU; the loop check compares the Bridge Identifier. --- rtl837x_stp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 46e8770..f82afa3 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -381,6 +381,8 @@ void stp_cnf_send(uint8_t port) __reentrant stp_tx_flags_extra = 0; memcpy(STP_O->src_addr, uip_ethaddr.addr, 6); + STP_O->src_addr[0] |= 0x02; + STP_O->src_addr[5] = (uip_ethaddr.addr[5] & 0xf0) | port; memcpy(STP_O->root.mac, root_bridge.mac, 6); memcpy(STP_O->bridge.mac, uip_ethaddr.addr, 6); From 81d11c246c43f9e6dc8925404d5fb5ca5b6741d9 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 17 Aug 2026 19:02:36 +0200 Subject: [PATCH 59/68] stp: correct what the blocking state does A blocked port does pass a received BPDU up to the CPU. The evidence is in logicog's capture of a looped pair: the port the loop check had already blocked kept reporting a BPDU age of zero seconds across dumps taken more than a forward delay apart, and that counter is only cleared in stp_in(). That is also what makes the loop latch work, since the port that stays forwarding has to go on hearing the blocked one. The tag flag comment said the source address is not learned on the egress port. doc/CpuPort.md defines it as not learning the source address from the frame at all, which is the narrower claim to make. The dangling heading above it described a field that is documented there too. --- doc/stp.md | 7 ++++--- rtl837x_common.h | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/stp.md b/doc/stp.md index 0ccca7c..49bedd3 100644 --- a/doc/stp.md +++ b/doc/stp.md @@ -59,9 +59,10 @@ to the CPU. `stp_setup()` prints a warning for every STP-enabled port in that state. Port states live in `RTL837X_MSTP_STATES (0x5310)`, two bits per port: -`00` disabled, `01` blocking, `10` learning, `11` forwarding. In the blocking -state a port forwards nothing except frames sent by the CPU, and nothing it -receives reaches the CPU. +`00` disabled, `01` blocking, `10` learning, `11` forwarding. A port in +blocking forwards nothing between ports, but it still sends what the CPU +hands it and still passes a received BPDU up to the CPU, which is what lets +loop detection go on working on a port it has already blocked. ## Timers diff --git a/rtl837x_common.h b/rtl837x_common.h index 50bffb1..4dcadce 100644 --- a/rtl837x_common.h +++ b/rtl837x_common.h @@ -72,9 +72,8 @@ struct vlan_tag { #define RTL_FRAME_TAG_ID 0x8899 #define RTL_FRAME_TAG_VERSION 0x04 /* Bits of the tag's `flags` word, see doc/CpuPort.md. */ -#define RTL_TAG_LEARN_DIS 0x0020 /* do not learn the CPU's SA on the egress port */ +#define RTL_TAG_LEARN_DIS 0x0020 /* do not learn the source address from this frame */ #define RTL_TAG_KEEP 0x0080 /* keep the frame's 802.1Q tag format as injected */ -/* The `pmask` word, see doc/CpuPort.md. */ // For TX, an 8 byte (plus 4 byte padding when when VLAN is enabled) // header describing the frame to be moved to the Asic is used From ec069a5bcc817abad79c467abc089a33d666d77b Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:58:42 +0200 Subject: [PATCH 60/68] stp: rename stpEnabled and take the comments out of the header The variable lives in rtlplayground.c, so it is declared in rtl837x_common.h with the others there, and it follows the naming of the rest. The header carried comments on the externs that the definitions in rtl837x_stp.c repeat, sometimes differently, which is one place too many to keep in step. What only the header had, the value ranges and what the designated arrays hold, moved to the definitions; the rest is gone. Function declarations lost their comments too. The status printer only prints, so its running commentary went. A define replaces the bare 33 in stp_in(), and the note on the loop check is down to what applies at that line. --- cmd_parser.c | 2 +- httpd/page_impl.c | 2 +- rtl837x_common.h | 2 ++ rtl837x_stp.c | 74 ++++++++++++++--------------------------------- rtl837x_stp.h | 33 +++++++++------------ rtlplayground.c | 8 ++--- 6 files changed, 43 insertions(+), 78 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index b724d15..5271dd4 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -26,7 +26,7 @@ #pragma constseg BANK2 extern __code struct machine machine; -extern __xdata uint8_t stpEnabled; +extern __xdata uint8_t stp_enabled; extern __code uint8_t log_to_phys_port[9]; extern volatile __xdata uint32_t ticks; diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 75fcf08..36b3444 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -568,7 +568,7 @@ void send_stp(void) slen = strtox(outbuf, HTTP_RESPONCE_JSON); slen += strtox(outbuf + slen, "{\"on\":"); - bool_to_html(stpEnabled); + bool_to_html(stp_enabled); slen += strtox(outbuf + slen, ",\"rstp\":"); bool_to_html(stp_rstp); slen += strtox(outbuf + slen, ",\"prio\":"); diff --git a/rtl837x_common.h b/rtl837x_common.h index 4dcadce..0fd6f35 100644 --- a/rtl837x_common.h +++ b/rtl837x_common.h @@ -118,6 +118,8 @@ struct flash_region_t { extern __xdata char port_names[9][PORT_NAME_SIZE]; +extern __xdata uint8_t stp_enabled; + /* System hostname (device identity). Set via `hostname ` and the System * Settings page, reported in /information.json. Other modules (e.g. LLDP, which * advertises it as the System Name TLV) read it from here. */ diff --git a/rtl837x_stp.c b/rtl837x_stp.c index f82afa3..2697cfc 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -38,18 +38,21 @@ uint8_t atoi_byte(__xdata uint8_t *out, uint8_t idx); /* ---- Configuration ---- */ __xdata uint8_t stp_prio; /* bridge priority high byte (0x80 = 32768) */ -__xdata uint8_t stp_hello_s; -__xdata uint8_t stp_maxage_s; -__xdata uint8_t stp_fwddelay_s; -__xdata uint8_t stp_rstp; -__xdata uint8_t stp_txhold; +__xdata uint8_t stp_hello_s; /* 1-10 s */ +__xdata uint8_t stp_maxage_s; /* 6-40 s */ +__xdata uint8_t stp_fwddelay_s; /* 4-30 s, also our listen period */ +__xdata uint8_t stp_rstp; /* 1 = RST BPDUs, 0 = legacy Config BPDUs */ +__xdata uint8_t stp_txhold; /* BPDUs per port per second */ __xdata uint8_t stp_pflags[10]; -__xdata uint32_t stp_pcost[10]; +__xdata uint32_t stp_pcost[10]; /* 0 = auto */ __xdata uint8_t stp_pprio[10]; -__xdata uint8_t stp_pp2p[10]; +__xdata uint8_t stp_pp2p[10]; /* admin point-to-point: 0 auto, 1 on, 2 off */ +/* Designated bridge, port and cost last heard on the port; stp_bpdu_age tells + * whether they are still current. + */ __xdata struct bridge stp_dbridge[10]; __xdata uint16_t stp_dpid[10]; __xdata uint32_t stp_dcost[10]; @@ -145,6 +148,7 @@ struct stp_pkt_in { #define BPDU_LEN_CONFIG 0x26 // LLC and a 35 byte body #define BPDU_LEN_RST 0x27 // LLC and a 36 byte body +#define BPDU_LEN_MIN_HEADER 33 // addresses through bpdu_type #define BPDU_FLAG_TC 0x01 #define BPDU_FLAG_LEARNING 0x10 @@ -185,10 +189,9 @@ static void print_field(__code const char *txt, uint8_t idx, uint8_t width) __re } -/* Where you look when the tree is not what you expected. */ static void stp_status(void) { - if (!stpEnabled) { + if (!stp_enabled) { print_string("STP off\n"); return; } @@ -217,23 +220,11 @@ static void stp_status(void) print_string(" "); print_field(stp_state_txt, (sfr_data[3 - (stp_i >> 2)] >> ((stp_i << 1) & 0x7)) & 0x3, 5); write_char(' '); - /* Only the root port is named. Everything else reads as designated - * because that is all the state machine tracks today; an alternate - * port is a designated one that happens to sit in blocking. */ print_field(stp_role_txt, stp_i == stp_root_port ? 1 : 0, 4); write_char(' '); print_field(stp_edge_txt, stp_pflags[stp_i] & STP_PF_OPEREDGE ? 1 : 0, 4); - /* BPDUs we put on the wire here. A designated port must show this - * climbing once per hello; the root port never does, because we do - * not announce back towards the root. Without it the only way to - * tell "we are silent" from "the neighbour is not listening" is a - * capture on the far side. */ write_char(' '); print_byte(stp_tx_count[stp_i]); - /* Seconds since the last BPDU on this port, capped at 255. Without - * it nothing in the output separates "nobody is speaking (R)STP - * out there" from "we are dropping what arrives", and stp_in() - * leaves on eight different conditions without saying so. */ write_char(' '); stp_scratch16 = stp_bpdu_age[stp_i] / STP_HZ; itoa(stp_scratch16 > 255 ? 255 : (uint8_t)stp_scratch16); @@ -242,8 +233,6 @@ static void stp_status(void) } -/* __reentrant so the temporaries land on the stack: stp_in() is __banked and - * its locals get exclusive internal RAM, which is what runs out first here. */ static void stp_record_designated(uint8_t port) __reentrant { stp_dbridge[port].prio = STP_I->bridge.prio; @@ -418,9 +407,7 @@ void stp_in(void) __banked { uint8_t port; - /* The header through bpdu_type is 33 bytes, the full Config/RST body is - * checked further down before anything past it is read. */ - if (uip_len < 33) { + if (uip_len < BPDU_LEN_MIN_HEADER) { uip_len = 0; return; } @@ -483,29 +470,10 @@ void stp_in(void) __banked if (stp_rxlen < 64) return; - /* Our own BPDU coming back at us: two of our ports sit on one segment. - * Only the one with the worse Port ID has to stop forwarding - 802.1D - * calls it a backup port. Blocking both, as we used to, kills a segment - * that can still carry traffic, and worse, leaves nobody forwarding to - * hear the loop: both then time out of blocking together and the pair - * oscillates for as long as the cable is in (measured: a topology change - * every ~4 s). - * - * The port with the better Port ID decides for both and is the only one - * that touches state - the other just drops the frame. One writer is - * what makes this safe: while both were still deciding for themselves, - * the winner's re-arm landed in the loser's port_timers[] first, the - * loser then read it as "already blocked" and skipped its own - * stp_state_set(), and the loop stayed open. Whether that happened came - * down to which frame the switch handed us first. - * - * The winner is forwarding by construction (nothing here ever blocks - * it), so it goes on hearing the loop and re-arms the loser's timer on - * every BPDU - that is what makes the block a latch rather than a - * forward-delay pulse, and it needs no assumption about a blocked port - * still receiving. Pull the cable and the re-arming stops, so the loser - * comes back on its own after a forward delay - and the link - * supervision above gets there first anyway. */ + /* Our own BPDU coming back: two of our ports sit on one segment. Only + * the one with the worse Port ID stops forwarding, and only the other + * one writes that state, so the two never race each other. + */ if (cmpBytes(STP_I->bridge.mac, uip_ethaddr.addr, 6) == 0) { /* Equal means the frame came back on the port it left: a loop * further out, behind an unmanaged switch. There is no pair to @@ -807,14 +775,14 @@ void stp_parse(void) __banked __reentrant if (cmd_compare(1, "on")) { print_string("STP enabled\n"); - stpEnabled = 1; + stp_enabled = 1; stp_setup(); return; } if (cmd_compare(1, "off")) { print_string("STP disabled\n"); stp_off(); - stpEnabled = 0; + stp_enabled = 0; return; } if (cmd_compare(1, "status")) { @@ -835,13 +803,13 @@ void stp_parse(void) __banked __reentrant if (cmd_compare(3, "on")) { stp_pflags[port] |= STP_PF_ENABLED; stp_pflags[port] &= ~STP_PF_TRIPPED; - if (stpEnabled) { /* (re)join: listen first */ + if (stp_enabled) { /* (re)join: listen first */ stp_state_set(port, 0b01); port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; } } else if (cmd_compare(3, "off")) { stp_pflags[port] &= ~STP_PF_ENABLED; - if (stpEnabled) + if (stp_enabled) stp_state_set(port, 0b11); /* plain forwarding */ } else if (cmd_compare(3, "edge")) { /* Also drop the *operational* edge flag: it is what exempts the diff --git a/rtl837x_stp.h b/rtl837x_stp.h index a8e68a8..97080ce 100644 --- a/rtl837x_stp.h +++ b/rtl837x_stp.h @@ -6,8 +6,8 @@ void stp_in(void) __banked; void stp_setup(void) __banked; void stp_timers(void) __banked; void stp_off(void) __banked; -void stp_parse(void) __banked __reentrant; /* "stp ..." CLI handler (cmd_parser delegates here) */ -void stp_defaults(void) __banked; /* boot init: 802.1D/w default configuration */ +void stp_parse(void) __banked __reentrant; +void stp_defaults(void) __banked; /* Tick rate of stp_timers(), also used by the web UI. */ #define STP_HZ 50 @@ -19,14 +19,12 @@ struct bridge { uint8_t mac[6]; }; -/* ---- Configuration (defaults per 802.1D-2004/802.1w, set in stp_defaults) --- */ -extern __xdata uint8_t stpEnabled; -extern __xdata uint8_t stp_prio; /* bridge priority, high byte: 0x80 = 32768; CLI takes 0-15 (steps of 4096) */ -extern __xdata uint8_t stp_hello_s; /* hello time, 1-10 s (default 2) */ -extern __xdata uint8_t stp_maxage_s; /* max age, 6-40 s (default 20) */ -extern __xdata uint8_t stp_fwddelay_s; /* forward delay, 4-30 s (default 15); our listen period */ -extern __xdata uint8_t stp_rstp; /* 1 = RSTP BPDUs (v2), 0 = STP-compatible Config BPDUs (v0) */ -extern __xdata uint8_t stp_txhold; /* max BPDUs per port per second (default 6) */ +extern __xdata uint8_t stp_prio; +extern __xdata uint8_t stp_hello_s; +extern __xdata uint8_t stp_maxage_s; +extern __xdata uint8_t stp_fwddelay_s; +extern __xdata uint8_t stp_rstp; +extern __xdata uint8_t stp_txhold; /* Per-port config/status flags (stp_pflags[]) */ #define STP_PF_ENABLED 0x01 /* port participates in STP (default on) */ @@ -39,21 +37,18 @@ extern __xdata uint8_t stp_txhold; /* max BPDUs per port per second (default 6) #define STP_PF_TRIPPED 0x80 /* runtime: disabled by BPDU guard */ extern __xdata uint8_t stp_pflags[10]; -extern __xdata uint32_t stp_pcost[10]; /* path cost; 0 = auto (20000) */ +extern __xdata uint32_t stp_pcost[10]; extern __xdata uint8_t stp_pprio[10]; -extern __xdata uint8_t stp_pp2p[10]; /* admin point-to-point: 0 auto, 1 on, 2 off */ +extern __xdata uint8_t stp_pp2p[10]; -/* Last-heard designated info per port (from received BPDUs); consult - * stp_bpdu_age to decide whether it is still current. */ extern __xdata struct bridge stp_dbridge[10]; extern __xdata uint16_t stp_dpid[10]; extern __xdata uint32_t stp_dcost[10]; -extern __xdata uint16_t stp_bpdu_age[10]; /* ticks since a BPDU was heard */ /* port priority (default 0x80) */ +extern __xdata uint16_t stp_bpdu_age[10]; -/* ---- Status, exposed read-only for the web UI (send_stp) ---- */ extern __xdata struct bridge root_bridge; -extern __xdata uint32_t root_bridge_cost; /* our path cost to the root (0 if we are root) */ -extern __xdata uint8_t stp_root_port; /* logical port towards the root; 0xff = we are root */ -extern __xdata uint16_t stp_tc_count; /* topology change counter (diagnostics) */ +extern __xdata uint32_t root_bridge_cost; +extern __xdata uint8_t stp_root_port; +extern __xdata uint16_t stp_tc_count; #endif diff --git a/rtlplayground.c b/rtlplayground.c index 02fe55c..cf67699 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -120,7 +120,7 @@ __xdata uint16_t rx_packet_vlan; __xdata uint16_t management_vlan; __xdata uint8_t tx_seq; -__xdata uint8_t stpEnabled; +__xdata uint8_t stp_enabled; __xdata uint8_t igmpEnabled; __xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ @@ -1167,7 +1167,7 @@ void handle_rx(void) print_byte(uip_buf[3]); print_byte(uip_buf[4]); print_byte(uip_buf[5]); write_char('\n'); print_string(" MGMT-VLAN: "); print_short(management_vlan); write_char('\n'); #endif - if (stpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 && uip_buf[2] == 0xc2 // STP packet? + if (stp_enabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x80 && uip_buf[2] == 0xc2 // STP packet? && uip_buf[3] == 0x00 && uip_buf[4] == 0x00 && uip_buf[5] == 0x00) { stp_in(); if (uip_len) { @@ -1511,7 +1511,7 @@ void idle(void) // Check UIP for packets to transmit handle_tx(); // If STP protocol enabled, decrease STP timers to trigger actions - if (stpEnabled) { + if (stp_enabled) { if (!stp_clock) { stp_clock = STP_TICK_DIVIDER; stp_timers(); @@ -2200,7 +2200,7 @@ void main(void) REG_SET(RTL837X_REG_SEC_COUNTER, 0x3); write_char(' '); print_reg(RTL837X_REG_SEC_COUNTER); #endif - stpEnabled = 0; + stp_enabled = 0; stp_defaults(); /* 802.1D/w default config before any "stp ..." replay */ nic_setup(); vlan_setup(); From 803d9c12421f82a2c7b8138457ce832e3a0af922 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:42:37 +0200 Subject: [PATCH 61/68] nic: do not claim a cause for the bounded TX wait The comment said the ASIC never completes a TX when the egress port is in a non-forwarding MSTP state. The guard is worth keeping either way, since an unbounded spin in the DMA wait takes the whole main loop down, but the mechanism is more than the evidence supports and the DMA into the TX ring has no business knowing the egress port at all. --- rtlplayground.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rtlplayground.c b/rtlplayground.c index cf67699..c6c4006 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -702,12 +702,9 @@ void nic_tx_packet(uint16_t ring_ptr) len += 0xf; len >>= 3; SFR_NIC_CTRL = len; - /* Bounded wait: normally the NIC consumes the frame in microseconds, but - * when the egress port is held in an MSTP non-forwarding state the ASIC - * has been observed to never complete the TX - an unbounded spin here - * then freezes the entire main loop (no STP/LACP timers, no HTTP, no - * ARP) until a power cycle. Give up after ~65k polls and drop the frame: - * losing one packet is recoverable, a frozen switch is not. */ + /* Bounded wait: the NIC normally consumes the frame in microseconds, and + * an unbounded spin here would freeze the main loop for good if it ever + * did not. Dropping one frame is recoverable, a frozen switch is not. */ { uint16_t tx_guard = 0; do { } while (SFR_NIC_CTRL != 0 && ++tx_guard != 0); From 62f80f0ad226c90fcfb27f23b68fd4b94cbcc73f Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:20:34 +0200 Subject: [PATCH 62/68] nic: report a transfer that does not complete instead of carrying on The bounded waits were silent: on timeout the code went straight back to its caller, and handle_rx() then read a frame the DMA may never have delivered, which is worse than waiting longer. Each of the three transfers now says so on the console, the two RX ones report failure to handle_rx(), and handle_rx() acknowledges the packet and gives up on it rather than parsing whatever is in the buffer. The guard variable moved to the top of its function, so the block that held it and its indentation are gone. --- rtlplayground.c | 50 +++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/rtlplayground.c b/rtlplayground.c index c6c4006..ac3f2b5 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -621,17 +621,21 @@ void get_random_32(void) * data will be stored in the rx_header structure * len is the length of data to be transferred */ -void nic_rx_header(uint16_t ring_ptr) +bool nic_rx_header(uint16_t ring_ptr) { uint16_t buffer = (uint16_t) &rx_headers[0]; + uint16_t guard = 0; + SFR_NIC_DATA_U16LE = buffer; SFR_NIC_RING_U16LE = ring_ptr; SFR_NIC_CTRL = 1; - /* Bounded, cf. nic_tx_packet: a stuck NIC DMA must not freeze the loop */ - { - uint16_t rx_guard = 0; - do { } while (SFR_NIC_CTRL != 0 && ++rx_guard != 0); + while (SFR_NIC_CTRL != 0) { + if (++guard == 0) { + print_string("NIC: RX header transfer did not complete\n"); + return false; + } } + return true; } @@ -641,8 +645,10 @@ void nic_rx_header(uint16_t ring_ptr) * data will be returned in the xmem buffer points to * ring_ptr is the current position of the RX Ring on the ASIC side */ -void nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr) +bool nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr) { + uint16_t guard = 0; + SFR_NIC_DATA_U16LE = buffer; SFR_NIC_RING_U16LE = ring_ptr; @@ -654,11 +660,13 @@ void nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr) print_short(len); #endif SFR_NIC_CTRL = len; - /* Bounded, cf. nic_tx_packet: a stuck NIC DMA must not freeze the loop */ - { - uint16_t rx_guard = 0; - do { } while (SFR_NIC_CTRL != 0 && ++rx_guard != 0); + while (SFR_NIC_CTRL != 0) { + if (++guard == 0) { + print_string("NIC: RX transfer did not complete\n"); + return false; + } } + return true; } @@ -668,6 +676,7 @@ void nic_rx_packet(register uint16_t buffer, register uint16_t ring_ptr) void nic_tx_packet(uint16_t ring_ptr) { uint16_t len; + uint16_t guard = 0; /* If we have a management VLAN, we have inserted a dot1Q-tag into the frame and * the frame starts at the beginning of uip_buf with the RTL TX descriptor, @@ -702,12 +711,11 @@ void nic_tx_packet(uint16_t ring_ptr) len += 0xf; len >>= 3; SFR_NIC_CTRL = len; - /* Bounded wait: the NIC normally consumes the frame in microseconds, and - * an unbounded spin here would freeze the main loop for good if it ever - * did not. Dropping one frame is recoverable, a frozen switch is not. */ - { - uint16_t tx_guard = 0; - do { } while (SFR_NIC_CTRL != 0 && ++tx_guard != 0); + while (SFR_NIC_CTRL != 0) { + if (++guard == 0) { + print_string("NIC: TX transfer did not complete\n"); + return; + } } } @@ -1133,7 +1141,10 @@ void handle_rx(void) uint16_t ring_ptr = ((uint16_t)sfr_data[2]) << 8; ring_ptr |= sfr_data[3]; ring_ptr <<= 3; - nic_rx_header(ring_ptr); + if (!nic_rx_header(ring_ptr)) { + REG_SET(RTL837X_REG_NIC_RXCMD, 1); + return; + } #ifdef RXTXDBG __xdata uint8_t *ptr = rx_headers; print_string("RX on port "); print_byte(rx_headers[3] & 0xf); @@ -1143,7 +1154,10 @@ void handle_rx(void) write_char(' '); } #endif - nic_rx_packet((uint16_t) &uip_buf[0], ring_ptr + 8); + if (!nic_rx_packet((uint16_t) &uip_buf[0], ring_ptr + 8)) { + REG_SET(RTL837X_REG_NIC_RXCMD, 1); + return; + } #ifdef RXTXDBG print_string("\n<< "); From 9a6e2b3e010e916c8b9a174fe4f0a931af71f697 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Wed, 26 Aug 2026 01:05:40 +0200 Subject: [PATCH 63/68] pins: put the module back in BANK2 This branch moved rtl837x_pins to the common window in eaff953 to reclaim BANK2 for the per-port status work, which was free at the time because the module was only the I2C and GPIO pin helpers. main has since put the SFP EEPROM transfer there, so the module now weighs 934 bytes and the common window is 156 bytes short. BANK2 has room for it again, and that is where main keeps it. Common segment +934 bytes, BANK2 -934. --- rtl837x_pins.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rtl837x_pins.c b/rtl837x_pins.c index 81ec887..bee9818 100644 --- a/rtl837x_pins.c +++ b/rtl837x_pins.c @@ -7,6 +7,8 @@ extern __code const struct machine machine; extern __xdata uint8_t sfr_data[4]; +#pragma codeseg BANK2 +#pragma constseg BANK2 uint8_t i2c_bus_from_sda_pin(uint8_t sda_pin) __banked { switch (sda_pin) { From 8e9bb56a29f4d1920c57fc0a87a9a5844b054ecc Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:52:00 +0200 Subject: [PATCH 64/68] nic: send a frame with the layout it actually has tcpip_output() decides whether to splice in the 802.1Q tag, and that decision also moves the frame: the header is shifted forward over its padding, so a tagged frame starts at uip_buf with the q_frame layout, while an untagged one keeps the padding and the nonq_frame layout. nic_tx_packet() took that decision a second time, from management_vlan alone. That agreed while the sender suppressed the management VLAN around the transmission, but not since the tag is skipped per frame for a CPU-tagged one: the descriptor then still sits behind the padding while the transfer is set up for the shifted layout. The frame goes out four bytes early and its length is read from the offset where tx_seq and chksum_flags live, so a sixty byte BPDU is sent as around 1800 bytes of whatever follows it. Looped back it is no longer a BPDU, the port hears nothing, auto edge promotes it and the loop stays open. Record the decision where it is taken and let the transfer follow it. A variable rather than an argument because internal RAM is full once the aggregation module shares the image: the overlay area ends at 0x7f, and an argument or any temporary for the condition no longer fits. --- rtlplayground.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/rtlplayground.c b/rtlplayground.c index b63eed1..23e93fa 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -118,6 +118,7 @@ __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE+2]; __xdata uint16_t rx_packet_vlan; __xdata uint16_t management_vlan; +__xdata bool frame_tagged; __xdata uint8_t tx_seq; __xdata uint8_t stp_enabled; @@ -711,13 +712,11 @@ void nic_tx_packet(uint16_t ring_ptr) uint16_t len; uint16_t guard = 0; - /* If we have a management VLAN, we have inserted a dot1Q-tag into the frame and - * the frame starts at the beginning of uip_buf with the RTL TX descriptor, - * otherwise the frame is a normal Ethernet frame which starts with - * an RTL TX descriptor being padded at the beginning, in the second case - * we need to skip the padding for the sending of the frame. + /* A frame that got a dot1Q tag was shifted forward over its padding, so it + * starts at uip_buf and carries the q_frame layout. One that did not keeps + * the padding in front and the nonq_frame layout, so the padding is skipped. */ - if (management_vlan) { + if (frame_tagged) { SFR_NIC_DATA_U16LE = (uint16_t) uip_buf; len = FRAME_Q->len; /* @@ -1122,7 +1121,9 @@ void tcpip_output(void) // For the management VLAN we insert an 802.1Q VLAN tag, but never into a // CPU-tagged frame, where the ASIC expects its tag right behind the addresses + frame_tagged = false; if (management_vlan && FRAME_ETHERTYPE != HTONS(RTL_FRAME_TAG_ID)) { + frame_tagged = true; // Shift the ethernet header before the HW type including the rtl_frame_desc to the beginning of uip_buf // to allow space to insert the dot 1Q tag for (uint8_t i = 0; i < sizeof(struct q_frame) - DOT_1Q_TAG_SIZE; i++) From bd06f7f5e58a5ba606cd2284f383f5dc0f13dbfe Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:26:05 +0200 Subject: [PATCH 65/68] stp: do not let auto edge undo a loop block A port held out of forwarding because a loop was seen on it stops hearing the frames that justified the hold, so its BPDU age climbs. The auto edge branch lives inside the same countdown as the hold, reads that age, and promotes the port back to forwarding after three seconds. The loop opens, a BPDU comes back, the block is applied again, and the two take turns. Measured on a looped pair present when the tree came up: twenty cycles in one window and fifteen in the next, each block followed by a promotion, still going when the capture ended. The countdown alone cannot tell a loop hold from an ordinary listen, so record the hold and leave the port alone while it stands. --- rtl837x_stp.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 2dc8337..cc4e916 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -70,6 +70,7 @@ __xdata uint16_t stp_scratch16; /* scratch for status printing only */ __xdata uint16_t port_timers[10]; /* listen-period countdown (0 = not listening) */ __xdata uint16_t port_hello[10]; /* hello TX countdown */ __xdata uint16_t stp_bpdu_age[10]; /* ticks since last BPDU seen on port (saturating) */ +__xdata uint8_t stp_loop_held[10]; /* port is out of forwarding because a loop was seen on it */ __xdata uint8_t stp_tx_budget[10]; /* tx hold: BPDUs left in the current second */ __xdata uint8_t stp_tx_count[10]; /* BPDUs actually put on the wire, wraps at 256 */ __xdata uint16_t stp_sec_tick; /* 1 s window for the tx budget */ @@ -309,6 +310,7 @@ static void stp_loop_hold_peer(uint8_t port) __reentrant stp_pflags[port] &= ~STP_PF_OPEREDGE; stp_topology_change(port); } + stp_loop_held[port] = 1; port_timers[port] = (uint16_t)stp_fwddelay_s * STP_HZ; } @@ -624,11 +626,13 @@ void stp_timers(void) __banked * the designated bridge on that port). */ if (port_timers[stp_i]) { if (!--port_timers[stp_i]) { + stp_loop_held[stp_i] = 0; stp_state_set(stp_i, 0b11); print_string("STP: port forwarding "); print_port_nl(stp_i); stp_topology_change(stp_i); } else if ((stp_pflags[stp_i] & STP_PF_AUTOEDGE) + && !stp_loop_held[stp_i] && stp_bpdu_age[stp_i] > STP_EDGE_DELAY) { /* Auto edge: nothing talks (R)STP on this port - it is * host-facing, go to forwarding without the full wait. */ @@ -711,6 +715,7 @@ void stp_setup(void) __banked sfr_data[0] = sfr_data[1] = sfr_data[2] = sfr_data[3] = 0; for (stp_i = machine.min_port; stp_i <= machine.max_port; stp_i++) { stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); + stp_loop_held[stp_i] = 0; stp_bpdu_age[stp_i] = 0; stp_tx_budget[stp_i] = stp_txhold; stp_tx_count[stp_i] = 0; @@ -762,6 +767,7 @@ void stp_off(void) __banked // States are: 00 disable, 01 blocking, 10 learning, 11 forwarding sfr_data[3 - (stp_i >> 2)] |= (uint8_t)(0b11 << ((stp_i << 1) & 0x7)); stp_pflags[stp_i] &= ~(STP_PF_OPEREDGE | STP_PF_TRIPPED); + stp_loop_held[stp_i] = 0; port_timers[stp_i] = 0; } sfr_data[1] |= 0x0c; // Do not block the CPU port (bits 3:2 of byte 1 = port 9) From c0d9bf7f0d137d9bc1d5e4a0cd68722642fe51e5 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:50:19 +0200 Subject: [PATCH 66/68] stp: take the review notes on types and register reads stp_enabled is a flag, so say bool. cmpBytes() returns a comparison result, so say int8_t. The three busy waits this branch adds read the status straight out of the SFR instead of copying four bytes to xdata first. --- cmd_parser.c | 2 +- rtl837x_common.h | 2 +- rtl837x_port.c | 12 ++++++------ rtl837x_stp.c | 2 +- rtlplayground.c | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index bf266b2..0f94b58 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -26,7 +26,7 @@ #pragma constseg BANK2 extern __code struct machine machine; -extern __xdata uint8_t stp_enabled; +extern __xdata bool stp_enabled; extern __code uint8_t log_to_phys_port[9]; extern volatile __xdata uint32_t ticks; diff --git a/rtl837x_common.h b/rtl837x_common.h index 923d02a..77a31b1 100644 --- a/rtl837x_common.h +++ b/rtl837x_common.h @@ -119,7 +119,7 @@ struct flash_region_t { extern __xdata char port_names[9][PORT_NAME_SIZE]; -extern __xdata uint8_t stp_enabled; +extern __xdata bool stp_enabled; /* System hostname (device identity). Set via `hostname ` and the System * Settings page, reported in /information.json. Other modules (e.g. LLDP, which diff --git a/rtl837x_port.c b/rtl837x_port.c index b357e58..8eea5a3 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -323,8 +323,8 @@ void port_l2_forget_port(uint8_t port) __banked REG_SET(RTL837x_L2_TBL_FLUSH_CTRL, L2_TBL_FLUSH_EXEC | (((uint16_t)1) << port)); do { - reg_read_m(RTL837x_L2_TBL_FLUSH_CTRL); - } while (sfr_data[1]); + reg_read(RTL837x_L2_TBL_FLUSH_CTRL); + } while (SFR_DATA_16); } @@ -421,16 +421,16 @@ void port_l2_learned(void) __banked void port_l2mc_set(uint8_t mac_last, __xdata uint16_t vid, __xdata uint16_t pmask) __banked { do { - reg_read_m(RTL837X_TBL_CTRL); - } while (sfr_data[3] & TBL_EXECUTE); + reg_read(RTL837X_TBL_CTRL); + } while (SFR_DATA_0 & TBL_EXECUTE); REG_WRITE(RTL837x_TBL_DATA_IN_A, 0xc2, 0x00, 0x00, mac_last); REG_WRITE(RTL837x_TBL_DATA_IN_B, 0x20 | (vid >> 8) | ((pmask & 0x3) << 6), vid, 0x01, 0x80); REG_WRITE(RTL837x_TBL_DATA_IN_C, 0, 0, 0, pmask >> 2); REG_WRITE(RTL837X_TBL_CTRL, 0, 0, TBL_L2_UNICAST, TBL_WRITE | TBL_EXECUTE); do { - reg_read_m(RTL837X_TBL_CTRL); - } while (sfr_data[3] & TBL_EXECUTE); + reg_read(RTL837X_TBL_CTRL); + } while (SFR_DATA_0 & TBL_EXECUTE); } diff --git a/rtl837x_stp.c b/rtl837x_stp.c index cc4e916..83d351c 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -254,7 +254,7 @@ static void stp_record_designated(uint8_t port) __reentrant /* Lexicographic compare of n bytes. A MAC is 6 of them; a Bridge Identifier * is 8, the two priority octets ahead of the MAC, compared as one unsigned * number per 802.1D. */ -signed char cmpBytes(__xdata uint8_t *m1, __xdata uint8_t *m2, uint8_t n) __reentrant +int8_t cmpBytes(__xdata uint8_t *m1, __xdata uint8_t *m2, uint8_t n) __reentrant { for (uint8_t i = 0; i < n; i++) { if (m1[i] == m2[i]) diff --git a/rtlplayground.c b/rtlplayground.c index 23e93fa..a853e42 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -121,7 +121,7 @@ __xdata uint16_t management_vlan; __xdata bool frame_tagged; __xdata uint8_t tx_seq; -__xdata uint8_t stp_enabled; +__xdata bool stp_enabled; __xdata uint8_t igmpEnabled; __xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ From 05ded88f85ba12e1755d324a346d62328b6cdbed Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:12:35 +0200 Subject: [PATCH 67/68] stp: keep the FDB update's counters to the function that uses them Both only ever served stp_fdb_update(), so they belong there. Internal RAM has room for them on this branch and on the one that carries the aggregation module too. --- rtl837x_stp.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 83d351c..32bac99 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -24,8 +24,6 @@ extern __code struct machine machine; extern __xdata uint8_t sfr_data[4]; extern __xdata struct machine_runtime machine_detected; -__xdata uint16_t stp_fdb_vid; -__xdata uint8_t stp_fdb_i; extern __xdata struct uip_eth_addr uip_ethaddr; @@ -691,6 +689,9 @@ void stp_defaults(void) __banked */ static void stp_fdb_update(__xdata uint16_t pmask) { + uint16_t stp_fdb_vid; + uint8_t stp_fdb_i; + /* Unlike LACPDUs (always untagged, so per-PVID entries suffice), BPDUs * can arrive VLAN-tagged and then classify into the tag's VID - cover * every VLAN that exists in the VLAN table, plus every port's PVID for From 3a3967960f42afb461c53735a5e5fdf42142e530 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:55:41 +0200 Subject: [PATCH 68/68] stp: take the review notes on the port and priority arguments The port argument went through its own digit test and machine table lookup; cmd_parse_port_separator() does both and also checks the port exists on this board, which the open-coded test did not. A port priority was masked to its top nibble, so "prio 17" quietly became 16. The bridge priority next to it refuses what it cannot represent, and now the port priority does too. --- rtl837x_stp.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rtl837x_stp.c b/rtl837x_stp.c index 32bac99..e33dbb4 100644 --- a/rtl837x_stp.c +++ b/rtl837x_stp.c @@ -35,6 +35,7 @@ extern __xdata uint8_t cmd_words_b[15]; extern __xdata char save_cmd; /* 0 while execute_config() replays the saved config */ uint8_t cmd_compare(uint8_t start, __code uint8_t * cmd); uint8_t atoi_byte(uint8_t idx); +uint8_t cmd_parse_port_separator(uint8_t idx); extern __xdata uint8_t atoi_results_u8; /* ---- Configuration ---- */ @@ -660,7 +661,7 @@ void stp_timers(void) __banked * (before the startup config replays "stp ..." commands over it). */ void stp_defaults(void) __banked { - stp_prio = 0x80; /* 32768 */ + stp_prio = 0x80; /* high byte of the priority: 0x8000 is 32768 */ stp_hello_s = 2; stp_maxage_s = 20; stp_fwddelay_s = 15; @@ -805,12 +806,9 @@ void stp_parse(void) __banked __reentrant if (cmd_compare(1, "port")) { if (cmd_words_len < 4) goto err; - if (!atoi_byte(cmd_words_b[2])) + if (!cmd_parse_port_separator(cmd_words_b[2])) goto err; - stp_scratch = atoi_results_u8; - if (stp_scratch < 1 || stp_scratch > 9) - goto err; - port = machine.phys_to_log_port[stp_scratch - 1]; + port = atoi_results_u8; if (cmd_words_len < 5 && !cmd_compare(3, "on") && !cmd_compare(3, "off")) goto err; if (cmd_compare(3, "on")) { @@ -863,7 +861,9 @@ void stp_parse(void) __banked __reentrant } else if (cmd_compare(3, "prio")) { if (!atoi_byte(cmd_words_b[4])) goto err; - stp_pprio[port] = atoi_results_u8 & 0xf0; + if (atoi_results_u8 > 240 || (atoi_results_u8 & 0x0f)) + goto err; + stp_pprio[port] = atoi_results_u8; } else if (cmd_compare(3, "guard")) { stp_pflags[port] &= ~(STP_PF_BPDUGUARD | STP_PF_ROOTGUARD); if (cmd_compare(4, "bpdu"))