From fae16dabf56b4fedbb1116945156f9f29436286f Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 3 Jul 2026 20:57:45 -0500 Subject: [PATCH 01/25] rtl837x_igmp: cast &entry to __xdata pointer in memset() SDCC rejects memset() on the generic &entry pointer (error 78, incompatible types). The ipmc/l2mc table entries are __xdata; pass an explicit (__xdata uint8_t *) so the call resolves. --- rtl837x_igmp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtl837x_igmp.c b/rtl837x_igmp.c index e36dda62..ec9d59af 100644 --- a/rtl837x_igmp.c +++ b/rtl837x_igmp.c @@ -223,7 +223,7 @@ void igmp_packet_handler(void) __banked #endif #ifdef IPMC_USES_L3MC - memset(&entry, 0, sizeof(struct ipmc_table_entry)); + memset((__xdata uint8_t *)&entry, 0, sizeof(struct ipmc_table_entry)); // For IPv4 MC, the Source-IP is 0.0.0.0 entry.sip[0] = 0x00; entry.sip[1] = 0x00; entry.sip[2] = 0x00; entry.sip[3] = 0x00; // For IPv4 MC, the Destination-IP is the IPv4 MC address @@ -235,7 +235,7 @@ void igmp_packet_handler(void) __banked * yy = MC_IP[2] * zz = MC_IP[3] */ - memset(&entry, 0, sizeof(struct l2mc_table_entry)); + memset((__xdata uint8_t *)&entry, 0, sizeof(struct l2mc_table_entry)); entry.mac[0] = 0x01; entry.mac[1] = 0x00; entry.mac[2] = 0x5e; entry.mac[3] = IGMP_I->mc_ip[1] & 0x7f; entry.mac[4] = IGMP_I->mc_ip[2]; entry.mac[5] = IGMP_I->mc_ip[3]; entry.vlan = 1; //TODO: Get this out of the packet and compare with VLAN table! From 6b190eb9f308da59a63bca12fa73e8bdd4f11fd1 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Mon, 10 Aug 2026 15:55:08 -0500 Subject: [PATCH 02/25] makefile: derive BUILD_DATE deterministically Honor SOURCE_DATE_EPOCH, falling back to the HEAD commit date and then to wall-clock when git is absent. Same-commit builds become byte-identical (BUILD_DATE is baked into the image and covered by the trailing CRC), which makes it possible to verify that a binary matches a source tree. --- Makefile | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0cbc04f8..9ab0da85 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,17 @@ endif VERSION_EXTENSION = v$(VERSION)-$(GIT_VERSION) FILENAME_EXTENSION = $(VERSION_EXTENSION)-$(MACHINE) +# Deterministic build date: honor SOURCE_DATE_EPOCH, else the HEAD commit date, +# else wall-clock (no-git fallback). Keeps same-commit builds byte-identical +# (BUILD_DATE is baked into the image and covered by the trailing CRC). +SOURCE_DATE_EPOCH ?= $(shell git show -s --format=%ct HEAD 2>/dev/null) +ifeq ($(SOURCE_DATE_EPOCH),) +BUILD_DATE := $(shell date +"%Y-%m-%d %H:%M:%S") +else +BUILD_DATE := $(shell date -u -d @$(SOURCE_DATE_EPOCH) +"%Y-%m-%d %H:%M:%S" 2>/dev/null \ + || date -u -r $(SOURCE_DATE_EPOCH) +"%Y-%m-%d %H:%M:%S") +endif + all: create_build_dir $(VERSION_HEADER) $(SUBDIRS) $(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin create_build_dir: @@ -84,7 +95,7 @@ $(VERSION_HEADER): @echo "#ifndef VERSION_H" > $(VERSION_HEADER) @echo "#define VERSION_H" >> $(VERSION_HEADER) @echo "#define VERSION_SW \"$(VERSION_EXTENSION)\"" >> $(VERSION_HEADER) - @echo "#define BUILD_DATE \"$(shell date +"%Y-%m-%d %H:%M:%S")\"" >> $(VERSION_HEADER) + @echo "#define BUILD_DATE \"$(BUILD_DATE)\"" >> $(VERSION_HEADER) @echo "#endif" >> $(VERSION_HEADER) httpd: html_data.h From 00a5f8187159d09a95467a4f651c11f6bf56a306 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Mon, 10 Aug 2026 15:59:48 -0500 Subject: [PATCH 03/25] dhcp: announce the hostname via option 12 Send the configured hostname on discover and request so the DHCP server can register the device (e.g. in local DNS). Skipped while the hostname is still empty, i.e. before set_hostname_default() has run. --- dhcp.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dhcp.c b/dhcp.c index 18845015..ad5c5364 100644 --- a/dhcp.c +++ b/dhcp.c @@ -41,6 +41,7 @@ __xdata uip_ipaddr_t server; #define DHCP_REBIND_LEN 4 #define DHCP_CLIENT_ID 61 #define DHCP_CLIENT_ID_LEN 7 +#define DHCP_HOSTNAME 12 #define DHCP_REQUEST_IP 50 #define DHCP_REQUEST_IP_LEN 4 #define DHCP_PARAMS 55 @@ -116,6 +117,20 @@ void dhcp_addopt_client_id(void) } +void dhcp_addopt_hostname(void) +{ + uint8_t len = 0; + while (hostname[len]) + len++; + if (!len) + return; + DHCP_OPT[dhcp_state.opt_ptr++] = DHCP_HOSTNAME; + DHCP_OPT[dhcp_state.opt_ptr++] = len; + memcpy(&DHCP_OPT[dhcp_state.opt_ptr], hostname, len); + dhcp_state.opt_ptr += len; +} + + void dhcp_addopt_request_ip(void) { DHCP_OPT[dhcp_state.opt_ptr++] = DHCP_REQUEST_IP; @@ -152,6 +167,7 @@ void dhcp_send_discover(void) dhcp_addopt_client_id(); dhcp_addopt_request_ip(); + dhcp_addopt_hostname(); DHCP_OPT[dhcp_state.opt_ptr++] = DHCP_PARAMS; DHCP_OPT[dhcp_state.opt_ptr++] = 3; @@ -188,6 +204,7 @@ void dhcp_send_request(void) dhcp_addopt_client_id(); dhcp_addopt_request_ip(); dhcp_addopt_server_id(); + dhcp_addopt_hostname(); DHCP_OPT[dhcp_state.opt_ptr++] = DHCP_PARAMS; DHCP_OPT[dhcp_state.opt_ptr++] = 3; From 3853a2701d169b3596630045c9ad1e498e3b6766 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Fri, 3 Jul 2026 20:57:45 -0500 Subject: [PATCH 04/25] machine: read a factory MAC from flash via mac_flash_offset Adds an optional per-board mac_flash_offset. When non-zero, RTLPlayground reads a 6-byte MAC from that flash address and uses it if it is a valid globally-administered unicast address; otherwise it falls back to the generated locally-administered MAC. Offset 0 (default for every board) preserves current behaviour. Enable it for the SWTGW218AS, whose stock firmware keeps the factory MAC in nvcfg at 0x1FC000. RTLPlayground images and web upgrades only touch flash below that region, so the factory MAC survives flashing. --- machine.c | 1 + machine.h | 1 + rtlplayground.c | 36 ++++++++++++++++++++++-------------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/machine.c b/machine.c index 091097aa..c27b92a2 100644 --- a/machine.c +++ b/machine.c @@ -415,6 +415,7 @@ void machine_custom_init(void) { } __code const struct machine machine = { .machine_name = "SWTGW218AS 8+1 Managed Switch", .isRTL8373 = 1, + .mac_flash_offset = 0x1FC000, .min_port = 0, .max_port = 8, .n_sfp = 1, diff --git a/machine.h b/machine.h index c3368f6a..523beea3 100644 --- a/machine.h +++ b/machine.h @@ -85,6 +85,7 @@ typedef struct machine { uint32_t led_sets[4][4]; uint8_t led_mux_custom; uint8_t led_mux[28]; + uint32_t mac_flash_offset; }; typedef struct machine_runtime diff --git a/rtlplayground.c b/rtlplayground.c index ce739632..bdb8ab3e 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -2112,20 +2112,28 @@ void main(void) uip_ipaddr(&uip_hostaddr, ownIP[0], ownIP[1], ownIP[2], ownIP[3]); uip_ipaddr(&uip_draddr, gatewayIP[0], gatewayIP[1], gatewayIP[2], gatewayIP[3]); uip_ipaddr(&uip_netmask, netmask[0], netmask[1], netmask[2], netmask[3]); - reg_read_m(RTL837X_REG_CHIP_UUID); -#ifdef DEBUG - print_string("SoC UUID: "); print_sfr_data(); -#endif - uip_ethaddr.addr[0] = 0x06; // LAA prefix - uip_ethaddr.addr[3] = sfr_data[0] ^ sfr_data[3]; - uip_ethaddr.addr[4] = sfr_data[1] ^ sfr_data[3]; - uip_ethaddr.addr[5] = sfr_data[2] ^ sfr_data[3]; - reg_read_m(RTL837X_REG_CHIP_LOT_NO); -#ifdef DEBUG - print_string(", LOT: "); print_sfr_data(); write_char(' '); -#endif - uip_ethaddr.addr[1] = sfr_data[0] ^ sfr_data[2]; - uip_ethaddr.addr[2] = sfr_data[1] ^ sfr_data[3]; + uip_ethaddr.addr[0] = 0xff; + if (machine.mac_flash_offset) { + flash_region.addr = machine.mac_flash_offset; + flash_region.len = FLASH_BUF_SIZE; + flash_read_bulk(flash_buf); + // accept only a real unicast, globally-administered address (reject blank/LAA/multicast/all-zero OUI) + if (flash_buf[0] != 0xff && !(flash_buf[0] & 0x03) && (flash_buf[0] | flash_buf[1] | flash_buf[2])) { + uip_ethaddr.addr[0] = flash_buf[0]; uip_ethaddr.addr[1] = flash_buf[1]; + uip_ethaddr.addr[2] = flash_buf[2]; uip_ethaddr.addr[3] = flash_buf[3]; + uip_ethaddr.addr[4] = flash_buf[4]; uip_ethaddr.addr[5] = flash_buf[5]; + } + } + if (uip_ethaddr.addr[0] == 0xff) { // no valid flash MAC -> generate locally-administered + reg_read_m(RTL837X_REG_CHIP_UUID); + uip_ethaddr.addr[0] = 0x06; // LAA prefix + uip_ethaddr.addr[3] = sfr_data[0] ^ sfr_data[3]; + uip_ethaddr.addr[4] = sfr_data[1] ^ sfr_data[3]; + uip_ethaddr.addr[5] = sfr_data[2] ^ sfr_data[3]; + reg_read_m(RTL837X_REG_CHIP_LOT_NO); + uip_ethaddr.addr[1] = sfr_data[0] ^ sfr_data[2]; + uip_ethaddr.addr[2] = sfr_data[1] ^ sfr_data[3]; + } print_string("Setting MAC to: "); print_byte(uip_ethaddr.addr[0]); write_char(':'); print_byte(uip_ethaddr.addr[1]); write_char(':'); print_byte(uip_ethaddr.addr[2]); write_char(':'); print_byte(uip_ethaddr.addr[3]); write_char(':'); From 0ed45e07107d73b331632f5225bd66f253622412 Mon Sep 17 00:00:00 2001 From: bloqaudio Date: Tue, 11 Aug 2026 10:26:06 -0500 Subject: [PATCH 05/25] cmd_editor: fix cmd_buffer overflow and serial-ring skip on long input Typing or pasting a line of 128 characters or more into the CLI hangs the editor and overruns cmd_buffer[128]: the length check did not reserve room for the terminating NUL written on Enter, and on a full buffer the character was retried via continue without advancing the serial-ring read pointer at the bottom of the loop, so input processing never caught up again. Cap the line at CMD_BUF_SIZE-1 and, when full, drop the character but fall through to consume the ring byte instead of spinning on it. The functional change is three lines; the rest of the diff is re-indentation of the insert-and-echo block (git diff -w shows the minimal form). --- cmd_editor.c | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/cmd_editor.c b/cmd_editor.c index 9a837e90..866cf403 100644 --- a/cmd_editor.c +++ b/cmd_editor.c @@ -40,21 +40,24 @@ void cmd_edit(void) __banked { while (l != sbuf_ptr) { if (sbuf[l] >= ' ' && sbuf[l] < 127) { // A printable character, copy to command line - if (cmd_line_len >= CMD_BUF_SIZE) - continue; - write_char(sbuf[l]); - // Shift buffer to right - for (uint8_t i = cmd_line_len; i > cursor; i--) - cmd_buffer[i] = cmd_buffer[i-1]; - // Insert char in comand buffer - cmd_buffer[cursor++] = sbuf[l]; - cmd_line_len++; - // Print rest of line - for (uint8_t i = cursor; i < cmd_line_len; i++) - write_char(cmd_buffer[i]); - // Move backwards - for (uint8_t i = cursor; i < cmd_line_len; i++) - write_char('\010'); // BS works like cursor-left + // Reserve one byte for the terminating NUL written on Enter. When the + // line is full, drop the character but still fall through to advance the + // serial-ring read pointer below; a 'continue' here would spin forever. + if (cmd_line_len < CMD_BUF_SIZE - 1) { + write_char(sbuf[l]); + // Shift buffer to right + for (uint8_t i = cmd_line_len; i > cursor; i--) + cmd_buffer[i] = cmd_buffer[i-1]; + // Insert char in comand buffer + cmd_buffer[cursor++] = sbuf[l]; + cmd_line_len++; + // Print rest of line + for (uint8_t i = cursor; i < cmd_line_len; i++) + write_char(cmd_buffer[i]); + // Move backwards + for (uint8_t i = cursor; i < cmd_line_len; i++) + write_char('\010'); // BS works like cursor-left + } } else if (sbuf[l] == '\033') { // ESC-Sequence // Wait until we have at least 3 characters including the ESC character in the serial buffer if (((sbuf_ptr + SBUF_SIZE - l) & SBUF_MASK) < 3) From 43845b911b4c1ec113f5701f9f881f33d7774df7 Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 12 Aug 2026 03:22:01 +0200 Subject: [PATCH 06/25] httpd: stop the GET request line walk at the end of the buffer The POST path tests for a NUL before it looks at a byte. The GET path did not, and is_separator() counts only space, tab, question mark and equals, so a request line carrying none of those walks past the end of uip_buf and writes its terminator into whatever xdata it happens to stop on. Everything that is not a POST reaches that walk. The pointer advances past the method before anything checks that the method was GET, so a TLS record sent to port 80 by a browser trying https first is enough on its own, as is a port scanner or a malformed line. The stop is wherever the first space, tab, question mark or equals turns up in memory, which is why the symptoms are erratic. Two bytes of BANK1. BANK2 and xdata do not move. --- httpd/httpd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index 561bd936..03b2d74d 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -640,7 +640,7 @@ void httpd_appcall(void) p += 4; scan_header(p); __xdata uint8_t *q = p; - while (!is_separator(*p)) + while (*p && !is_separator(*p)) p++; *p = '\0'; dbg_string_x(q); From 4ff009dbfc7293d4af914b54e9095165796581a1 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:42:36 +0200 Subject: [PATCH 07/25] uip: cap TCP MSS to 1460 to survive jumbo-MTU clients (#298) * uip: parenthesise UIP_LLH_LEN The macro expands to a bare sum, so wherever it is subtracted the second term gets added instead. UIP_TCP_MSS - and with it UIP_RECEIVE_WINDOW - therefore comes out 24 bytes above the buffer's real capacity. UIP_APPDATA_SIZE and UIP_REASS_BUFSIZE are wrong the same way, though neither is reachable today. The additions, uip_buf[UIP_LLH_LEN] and friends, were right by luck. * uip: keep the advertised MSS below the buffer edge Deriving the MSS straight from the buffer size makes the switch advertise exactly the segment that fills uip_buf to its last byte, and a peer that takes it literally corrupts every large upload: the firmware image arrives fully acknowledged, with no retransmissions on the wire, yet the CRC over the streamed body never matches and the flash write is abandoned. Isolated by changing nothing but the segment size, same buffer and same file: 1490-byte segments fail four times out of four, 1460-byte segments succeed, 745-byte segments succeed. Linux halves its segments against a window this small, so only macOS on a jumbo link ever produces a full-size segment - which is why the failure hides so well. Where exactly the full segment breaks the stream is not pinned down yet; until it is, the advertised MSS stays a step below the edge. * uip: size the buffer to the largest frame the CPU port accepts UIP_TCP_MSS derives from UIP_CONF_BUFFER_SIZE, and the buffer was large enough for frames the hardware will never deliver, so the switch advertised a segment size no peer could usefully reach. A client on a jumbo-MTU link took it at its word and the oversized replies went nowhere. Size the buffer to the ingress limit instead. ICMP bypasses MSS and so probes the hardware directly: on a SWTGW218AS a 1502-byte payload is answered and 1503 never arrives, which puts the largest frame the NIC hands us at 1556 bytes of uip_buf. UIP_TCP_MSS then derives to 1490, the same edge measured over TCP. Frames above the limit are dropped by the NIC rather than written to the buffer - an 8 kB ping leaves the switch untouched - so nothing overruns it. Frees 644 bytes of XDATA. * uip: trim these comments, one of which had stopped being true The note above UIP_CONF_BUFFER_SIZE claimed the MSS derives from it as 1490. It does not: the commit that follows pins the MSS at 1460 on purpose, a step below that ceiling, because a segment filling the buffer to its last byte corrupts large uploads. Left as it was, the file argued with itself. Both blocks are shorter now. What justifies the numbers stays, which is the ICMP measurement behind 1556 and the four-out-of-four failure behind 1460. What went is the storytelling around them, which belongs in this thread rather than in a config header. * uip: derive the MSS from the buffer again, minus explicit headroom The review asked why the buffer size and the MSS are both set by hand when one used to follow from the other. They answer different questions, but the gap between them is a number in its own right, so it gets a name now: UIP_CONF_BUFFER_EXTRA, and UIP_TCP_MSS goes back to being derived. The headroom is where the measurement lives. A segment that fills uip_buf to its last byte corrupts large uploads: with nothing but the segment size changing, 1490 fails four times out of four and 1460 succeeds. With the buffer sized to the frame the NIC accepts, an extra of 30 lands on 1460. Deriving it the other way round does not work. Sizing the buffer from a 1460 byte MSS gives 1526, which is 30 bytes under the frame the NIC actually delivers. A 1502 byte ICMP payload occupies 1556 bytes of uip_buf and is answered today, and it would stop fitting. The generated image is byte for byte the same as the one with 1460 written out, so the expression lands on the value that was measured. --------- Co-authored-by: d00f --- uip/uip-conf.h | 16 +++++++++++++++- uip/uipopt.h | 18 +++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/uip/uip-conf.h b/uip/uip-conf.h index 384f7fa5..2da25a3e 100644 --- a/uip/uip-conf.h +++ b/uip/uip-conf.h @@ -106,9 +106,23 @@ typedef unsigned short uip_stats_t; /** * uIP buffer size. * + * Sized to the largest frame the CPU port accepts on ingress: anything above + * that the NIC drops in hardware, so a larger buffer only costs XDATA. Measured + * with ICMP, which bypasses MSS and so probes the hardware directly: a 1502-byte + * payload is answered and 1503 is not, which puts the frame at 1556 bytes of + * uip_buf. The limit is the NIC's rather than a port's, so how the frame was + * tagged on the wire does not change it. + * * \hideinitializer */ -#define UIP_CONF_BUFFER_SIZE 2200 +#define UIP_CONF_BUFFER_SIZE 1556 + +/** + * Bytes of the buffer kept out of the advertised MSS. + * + * \hideinitializer + */ +#define UIP_CONF_BUFFER_EXTRA 30 /** * CPU byte order. diff --git a/uip/uipopt.h b/uip/uipopt.h index 16046cfe..731484c0 100644 --- a/uip/uipopt.h +++ b/uip/uipopt.h @@ -298,11 +298,8 @@ /** * The TCP maximum segment size. - * - * This is should not be to set to more than - * UIP_BUFSIZE - UIP_LLH_LEN - UIP_TCPIP_HLEN. */ -#define UIP_TCP_MSS (UIP_BUFSIZE - UIP_LLH_LEN - UIP_TCPIP_HLEN) +#define UIP_TCP_MSS (UIP_BUFSIZE - UIP_LLH_LEN - UIP_TCPIP_HLEN - UIP_BUFFER_EXTRA) /** * The size of the advertised receiver's window. @@ -381,6 +378,17 @@ #define UIP_BUFSIZE UIP_CONF_BUFFER_SIZE #endif /* UIP_CONF_BUFFER_SIZE */ +/** + * Bytes of uip_buf kept out of the advertised MSS. + * + * \hideinitializer + */ +#ifndef UIP_CONF_BUFFER_EXTRA +#define UIP_BUFFER_EXTRA 0 +#else /* UIP_CONF_BUFFER_EXTRA */ +#define UIP_BUFFER_EXTRA UIP_CONF_BUFFER_EXTRA +#endif /* UIP_CONF_BUFFER_EXTRA */ + extern __xdata uint8_t uip_buf[UIP_CONF_BUFFER_SIZE+2]; @@ -447,7 +455,7 @@ void uip_log(char *msg); #ifdef UIP_CONF_LLH_LEN #define UIP_LLH_LEN UIP_CONF_LLH_LEN #else /* UIP_CONF_LLH_LEN */ -#define UIP_LLH_LEN ETHER_HEADER_SIZE + RTL_FRAME_DESC_SIZE +#define UIP_LLH_LEN (ETHER_HEADER_SIZE + RTL_FRAME_DESC_SIZE) #endif /* UIP_CONF_LLH_LEN */ /** @} */ From be6d47a3ab6a8b3b96177efaea8a2442b486ecfd Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 01:43:49 +0200 Subject: [PATCH 08/25] regs: wrap REG_SET and REG_WRITE in do { } while (0) Both macros expand to a run of statements joined by backslashes with nothing around them, so as the unbraced body of an if or a for only the first assignment belongs to that body. The other three and the reg_write() call sit after it and run once, unconditionally, with whatever the loop counter ended on. Wrapping each macro into a single statement is what every call site already assumes it to be. This hands the compiler no new room around the SFR writes, which is worth showing rather than asserting. Building the whole image before and after and comparing the generated assembly module by module, with label numbering, block scope suffixes and the version string normalised away, three modules differ: rtl837x_igmp, rtl837x_port and rtl837x_leds. Ten modules call these macros, so the other seven come out identical, and so does everything else in the image. Those three differ because they hold the five unbraced uses. Two of them want a fix rather than only the brace, and that is left to the commits that follow. Six bytes of BANK1 on SWTGW218AS, nothing anywhere else. --- rtl837x_regs.h | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/rtl837x_regs.h b/rtl837x_regs.h index f5bd9512..c9185a64 100644 --- a/rtl837x_regs.h +++ b/rtl837x_regs.h @@ -305,32 +305,40 @@ #ifdef REGDBG -#define REG_SET(r, v) SFR_DATA_24 = (((uint32_t)v) >> 24) & 0xff; \ +#define REG_SET(r, v) do { \ + SFR_DATA_24 = (((uint32_t)v) >> 24) & 0xff; \ SFR_DATA_16 = (((uint32_t)v) >> 16) & 0xff; \ SFR_DATA_8 = (((uint16_t)v) >> 8 & 0xff); \ SFR_DATA_0 = (v) & 0xff; \ reg_write(r); \ write_char('R'); print_byte(r >> 8); print_byte(r); write_char('-'); \ - print_byte(((v) >> 24) & 0xff); print_byte((v) >> 16 & 0xff); print_byte((v) >> 8 & 0xff); print_byte( (v) & 0xff); write_char(' '); + print_byte(((v) >> 24) & 0xff); print_byte((v) >> 16 & 0xff); print_byte((v) >> 8 & 0xff); print_byte( (v) & 0xff); write_char(' '); \ +} while (0) -#define REG_WRITE(r, v24, v16, v8, v0) SFR_DATA_24 = (v24); \ +#define REG_WRITE(r, v24, v16, v8, v0) do { \ + SFR_DATA_24 = (v24); \ SFR_DATA_16 = (v16); \ SFR_DATA_8 = (v8); \ SFR_DATA_0 = (v0); \ reg_write(r); \ - write_char('R'); print_byte(r>>8); print_byte(r); write_char('-'); print_byte(v24); print_byte(v16); print_byte(v8); print_byte(v0); write_char(' '); + write_char('R'); print_byte(r>>8); print_byte(r); write_char('-'); print_byte(v24); print_byte(v16); print_byte(v8); print_byte(v0); write_char(' '); \ +} while (0) #else -#define REG_SET(r, v) SFR_DATA_24 = (((uint32_t)v) >> 24) & 0xff; \ +#define REG_SET(r, v) do { \ + SFR_DATA_24 = (((uint32_t)v) >> 24) & 0xff; \ SFR_DATA_16 = (((uint32_t)v) >> 16) & 0xff; \ SFR_DATA_8 = (((uint16_t)v) >> 8 & 0xff); \ SFR_DATA_0 = (v) & 0xff; \ - reg_write(r); + reg_write(r); \ +} while (0) -#define REG_WRITE(r, v24, v16, v8, v0) SFR_DATA_24 = (v24); \ +#define REG_WRITE(r, v24, v16, v8, v0) do { \ + SFR_DATA_24 = (v24); \ SFR_DATA_16 = (v16); \ SFR_DATA_8 = (v8); \ SFR_DATA_0 = (v0); \ - reg_write(r); + reg_write(r); \ +} while (0) #endif #endif From 5c5dcb52094cb630da12b1659f0989d0700fdc78 Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 01:04:20 +0200 Subject: [PATCH 09/25] igmp: drop the duplicate port configuration loop igmp_setup() writes the per-port IGMP configuration twice, once with the value spelled out and once with the same number assembled from the constants: IGMP_MAX_GROUP | IGMP_PROTOCOL_ENABLE | IGMP_FLOOD is exactly 0x00ff7c15. The second loop carries the comment block explaining the bit layout, so the literal one is the one to drop. Today the cost is one redundant register write, because REG_SET is not a single statement and the unbraced loop body only ever reaches index machine.max_port + 1. Once the macro is wrapped it becomes one redundant write per port on every boot and on every "igmp off", which is what makes this worth removing rather than leaving. Fifty nine bytes of BANK1 on SWTGW218AS, nothing anywhere else. --- rtl837x_igmp.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rtl837x_igmp.c b/rtl837x_igmp.c index ec9d59af..3b267afc 100644 --- a/rtl837x_igmp.c +++ b/rtl837x_igmp.c @@ -96,11 +96,6 @@ void igmp_setup(void) __banked // Enable lookup of IPv4 MC addresses in table reg_bit_set(RTL837X_L2_CTRL, L2_CTRL_LUT_IPMC_HASH); - // Configure per-port IGMP configuration, bits 0-10 enable MC protocol snooping, - // bits 16-24 configure max MC group used by that port. For now all protocols are flooded (01) - for (i = machine.min_port; i <= machine.max_port; i++) - REG_SET(RTL837X_IGMP_PORT_CFG + (i << 2), 0x00ff7c15); - /* Configure per-port IGMP operations when protocol messages are received * bits 0-9 enable MC protocol snooping * bit 10: Enable dynamic router port learning From 9f2d6b72b7995d3559552661534068c6dcbc33f2 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Fri, 14 Aug 2026 00:11:50 +0300 Subject: [PATCH 10/25] build: Fix version.h race condition in parallel builds --- Makefile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 9ab0da85..c59c0b77 100644 --- a/Makefile +++ b/Makefile @@ -92,11 +92,10 @@ html_data.c html_data.h: $(HTML) tools/output/fileadder tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -d html -p html_data $(VERSION_HEADER): - @echo "#ifndef VERSION_H" > $(VERSION_HEADER) - @echo "#define VERSION_H" >> $(VERSION_HEADER) - @echo "#define VERSION_SW \"$(VERSION_EXTENSION)\"" >> $(VERSION_HEADER) - @echo "#define BUILD_DATE \"$(BUILD_DATE)\"" >> $(VERSION_HEADER) - @echo "#endif" >> $(VERSION_HEADER) + @printf '%s\n' "#ifndef VERSION_H" "#define VERSION_H" \ + "#define VERSION_SW \"$(VERSION_EXTENSION)\"" \ + "#define BUILD_DATE \"$(BUILD_DATE)\"" \ + "#endif" > $(VERSION_HEADER) httpd: html_data.h From 31024388ba247b26e056533ef6742f5e0cc0ba11 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Fri, 14 Aug 2026 00:18:23 +0300 Subject: [PATCH 11/25] build: Fix tools dependency for parallel builds Replace file-path prerequisite tools/output/fileadder with order-only dependency on the tools PHONY target. Also add tools as order-only dependency to the final .bin target which invokes all of the tools. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c59c0b77..996a19d9 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ OBJS = ${SRCS:%.c=$(BUILDDIR)/%.rel} DEPS := ${SRCS:%.c=$(BUILDDIR)/%.d} HTML := $(shell find $(html) -name '*.js' -or -name '*.html' -or -name '*.svg') -html_data.c html_data.h: $(HTML) tools/output/fileadder +html_data.c html_data.h: $(HTML) | tools tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -d html -p html_data $(VERSION_HEADER): @@ -123,7 +123,7 @@ $(BUILDDIR)/rtlplayground.ihx: $(OBJS) $(BUILDDIR)/crtstart.rel $(BUILDDIR)/crc1 $(BUILDDIR)/rtlplayground.img: $(BUILDDIR)/rtlplayground.ihx objcopy --input-target=ihex -O binary $< $@ -$(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.img +$(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.img | tools if [ -e $@ ]; then rm $@; fi tools/output/imagebuilder -i $^ $@ tools/output/fileadder -a $(DEFAULT_CONFIG_LOCATION) -s $(IMAGESIZE) -d config.txt $@ From 0126b159bfa33dc080bba9bd74f59a7a05c0f35e Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Fri, 14 Aug 2026 00:24:25 +0300 Subject: [PATCH 12/25] build: Make create_build_dir PHONY and add order-only deps Mark create_build_dir as PHONY so directory creation is never skipped. Also add html_data.h as order-only prerequisite to the .c pattern rule, fixing another the race where httpd/httpd.c and httpd/page_impl.c are compiled before the generated header exists. --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 996a19d9..71f3d6e6 100644 --- a/Makefile +++ b/Makefile @@ -110,10 +110,10 @@ distclean: -rm -f html_data.c html_data.h $(VERSION_HEADER) -rm -rf $(BUILDDIR) -$(BUILDDIR)/%.rel: %.c +$(BUILDDIR)/%.rel: %.c | create_build_dir html_data.h $(CC) -MMD $(CC_FLAGS) -o $@ -c $< -$(BUILDDIR)/%.rel: %.asm +$(BUILDDIR)/%.rel: %.asm | create_build_dir ${ASM} ${AFLAGS} -o $@ $< # mv -f $(addprefix $(basename $^), .lst .rel .sym) . @@ -132,7 +132,7 @@ $(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.i tools/output/crc_calculator -u $@ ln -sf $(MACHINE)/rtlplayground-$(FILENAME_EXTENSION).bin output/rtlplayground.bin -.PHONY: clean all $(SUBDIRS) $(VERSION_HEADER) +.PHONY: clean all $(SUBDIRS) $(VERSION_HEADER) create_build_dir .PHONY: machine_check: From 65f8afb908cdbe5deb56378f13d904335841fb03 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Fri, 14 Aug 2026 00:28:58 +0300 Subject: [PATCH 13/25] build: Remove undefined html variable HTML target references $(html) which was never defined, which causes find to run through the entire source tree. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 71f3d6e6..e64dacf6 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ SRCS += \ OBJS = ${SRCS:%.c=$(BUILDDIR)/%.rel} DEPS := ${SRCS:%.c=$(BUILDDIR)/%.d} -HTML := $(shell find $(html) -name '*.js' -or -name '*.html' -or -name '*.svg') +HTML := $(shell find html -name '*.js' -or -name '*.html' -or -name '*.svg') html_data.c html_data.h: $(HTML) | tools tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -d html -p html_data From 11be13fe5338a94b84af33d05b7fb4c0a7e398fc Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Fri, 14 Aug 2026 10:41:58 +0300 Subject: [PATCH 14/25] build: Fix double generation of html_data Make supports grouped target which runs once for all listed targets. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e64dacf6..05cd7d37 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ OBJS = ${SRCS:%.c=$(BUILDDIR)/%.rel} DEPS := ${SRCS:%.c=$(BUILDDIR)/%.d} HTML := $(shell find html -name '*.js' -or -name '*.html' -or -name '*.svg') -html_data.c html_data.h: $(HTML) | tools +html_data.c html_data.h &: $(HTML) | tools tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -d html -p html_data $(VERSION_HEADER): From 0503e7952dbbce5ef9d61e0d80a1c5ba17e5b592 Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 03:50:03 +0200 Subject: [PATCH 15/25] httpd: check the port index /counters.json is given The handler took one raw character of the request line and passed it to send_counters(), which uses it to index machine.phys_to_log_port. That array has nine entries and the character is whatever the client sent, so the read ran up to 246 entries past the end and the result went on to STAT_GET as a port number. is_word() accepts any request whose name is followed by a question mark, so nothing constrained the byte to a digit. Bounding it where it is read keeps the check beside the assumption it protects and needs nothing from the machine description. sdcc leaves plain char unsigned and the subtraction wraps in eight bits, so a byte below '0' comes out above 200 and one upper test covers both ends: exactly '0' to '8' now reach send_counters. The compiled test is add a,#0xf7 followed by jnc, which I read back out of the assembly rather than assuming. Out of range answers 400 by the path the other malformed requests already take, rather than an empty array. An empty array would have been worse than useless here, since the statistics page calls BigInt on the first element before it looks at the length. The page asks only for index zero to the port count minus one, so nothing that answered before stops answering, and a non-200 reply makes its handler do nothing at all. 11 bytes of BANK1, nothing in the common segment, BANK2, xdata or internal RAM. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0. --- httpd/httpd.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/httpd/httpd.c b/httpd/httpd.c index 03b2d74d..d856ccdb 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -664,7 +664,16 @@ void httpd_appcall(void) parse_short(q + 15); send_vlan(short_parsed); } else if (is_word(q, "/counters.json")) { - send_counters(q[20]-'0'); + /* The port is one raw character of the request line and + * indexes a nine entry table, so bound it here instead + * of trusting the client to have sent a digit. Anything + * below '0' wraps well past eight, so the one test + * covers both ends. */ + uint8_t cport = q[20] - '0'; + if (cport > 8) + send_bad_request(); + else + send_counters(cport); } else if (is_word(q, "/eee.json")) { send_eee(); } else if (is_word(q, "/bandwidth.json")) { From f2c6ac01d931360a4a5c2fe17193fa6b74baa436 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sat, 15 Aug 2026 18:20:01 +0200 Subject: [PATCH 16/25] port: read a trunk's members through one function The member mask of an aggregation group is decoded by hand in two places, the lag command and the JSON behind the aggregation page, and every branch that touches trunks adds another copy. port_lag_members_get() sits next to port_lag_members_set() and both readers call it. It answers from the hardware, so it covers a group configured with lag and one a protocol brought up, without either having to say so. It reads through reg_read() rather than reg_read_m(), so sfr_data is left alone. Neither caller looked at it afterwards; both read the hash register next. --- cmd_parser.c | 3 +-- httpd/page_impl.c | 3 +-- rtl837x_port.c | 13 +++++++++++++ rtl837x_port.h | 1 + 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index 22a4c78a..9e2be41e 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -250,8 +250,7 @@ void parse_lag(void) print_string("LAG status:\n"); for (uint8_t i = 0; i < 4; i++) { write_char(' '); write_char('1' + i); - reg_read_m(RTL837X_TRK_MBR_CTRL_BASE + (i << 2)); - members = ((uint16_t)sfr_data[2]) << 8 | sfr_data[3]; + members = port_lag_members_get(i); if (!members) { print_string(" disabled\n"); continue; diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 44332e5f..19796edc 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -518,8 +518,7 @@ void send_lag(void) slen += strtox(outbuf + slen, "{\"lagNum\":"); itoa_html(l); slen += strtox(outbuf + slen, ",\"members\":\""); - reg_read_m(RTL837X_TRK_MBR_CTRL_BASE + (l << 2)); - uint16_t ports = ((uint16_t)sfr_data[2] << 8) | sfr_data[3]; + uint16_t ports = port_lag_members_get(l); for (uint8_t i = 0; i < 16; i++) { bool_to_html(!!(ports & 0x8000)); ports <<= 1; diff --git a/rtl837x_port.c b/rtl837x_port.c index d203e12d..4dfbbbc0 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -733,6 +733,19 @@ void port_rldp_on(__xdata uint16_t p_ms) } +/* + * Reads the member port bitmask of a Link Aggregation Group. + * The groups have numbers 0-3; bit n is set when logical port n is a member. + * The bitmask reflects what the hardware holds, so it covers groups set up + * statically and groups a protocol brought up, without either having to say so. + */ +uint16_t port_lag_members_get(uint8_t lag) __banked +{ + reg_read(RTL837X_TRK_MBR_CTRL_BASE + (lag << 2)); + return ((uint16_t)SFR_DATA_8 << 8) | SFR_DATA_0; +} + + /* * Configure LAGs * Sets the members via port bitmask of a given Link Aggregation Group diff --git a/rtl837x_port.h b/rtl837x_port.h index a5ce278c..b4598321 100644 --- a/rtl837x_port.h +++ b/rtl837x_port.h @@ -61,6 +61,7 @@ void port_mirror_set(register uint8_t port, __xdata uint16_t rx_pmask, __xdata u void port_mirror_del(void) __banked; bool port_ingress_filter(__xdata uint8_t port, __xdata vlan_ingress_mode_t type) __banked; void port_l2_setup(void) __banked; +uint16_t port_lag_members_get(uint8_t lag) __banked; void port_lag_members_set(__xdata uint8_t lag, __xdata uint16_t members) __banked; void port_lag_hash_set(__xdata uint8_t lag, __xdata uint8_t hash) __banked; void port_eee_enable_all(__xdata uint8_t speed) __banked; From a3c586ef3861666d1a3ba012b1c789c12a6e4a75 Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 01:47:36 +0200 Subject: [PATCH 17/25] port: make the trunk hash default reachable again, and per group Wrapping REG_SET restored the guard in front of the hash default, and that exposed three things about the line it guards. The test was against zero. The register does not read zero: it comes out of reset holding source port number plus both MAC fields, both IP fields and the L4 source port, which the header now names LAG_HASH_RESET. Measured on an SWTGW218AS, where all four groups read 0x3f after a cold boot and a value written before a power cycle is gone afterwards. With the guard working and the test unreachable, the default would never be installed, where before it was installed on every call. Testing against the reset value restores the intent, and zero is still accepted in case another device does reset that way. The write went to the base address while the read that decides it used the group offset, so a group other than zero was tested and group zero was written. Both ends use the offset now. The range check printed a complaint and carried on. It returns, which matters more now that the hash write also uses the group number to build an address. Thirty bytes of BANK1. --- rtl837x_port.c | 9 ++++++--- rtl837x_regs.h | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/rtl837x_port.c b/rtl837x_port.c index 4dfbbbc0..9388cc60 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -757,11 +757,14 @@ void port_lag_members_set(__xdata uint8_t lag, __xdata uint16_t members) __banke { print_string("port_lag_members_set, lag: "); print_byte(lag); print_string(", members: "); print_short(members); write_char('\n'); - if (lag > 3) + if (lag > 3) { print_string("Link aggregation group must be 0-3!\n"); + return; + } reg_read_m(RTL837X_TRK_HASH_CTRL_BASE + (lag << 2)); - if (!(sfr_data[0] | sfr_data [1] | sfr_data [2] | sfr_data [3])) - REG_SET(RTL837X_TRK_HASH_CTRL_BASE, LAG_HASH_DEFAULT); + if (!(sfr_data[0] | sfr_data[1] | sfr_data[2]) + && (sfr_data[3] == LAG_HASH_RESET || sfr_data[3] == 0)) + REG_SET(RTL837X_TRK_HASH_CTRL_BASE + (lag << 2), LAG_HASH_DEFAULT); REG_WRITE(RTL837X_TRK_MBR_CTRL_BASE + (lag << 2), 0, 0, members >> 8, members & 0xff); } diff --git a/rtl837x_regs.h b/rtl837x_regs.h index c9185a64..37a7e99d 100644 --- a/rtl837x_regs.h +++ b/rtl837x_regs.h @@ -232,6 +232,8 @@ #define LAG_HASH_L4_SPORT 0x20 #define LAG_HASH_L4_DPORT 0x40 #define LAG_HASH_DEFAULT (LAG_HASH_L2_SMAC | LAG_HASH_L2_DMAC | LAG_HASH_L3_SIP | LAG_HASH_L3_DIP | LAG_HASH_L4_SPORT | LAG_HASH_L4_DPORT) +#define LAG_HASH_RESET (LAG_HASH_SOURCE_PORT_NUMBER | LAG_HASH_L2_SMAC | LAG_HASH_L2_DMAC \ + | LAG_HASH_L3_SIP | LAG_HASH_L3_DIP | LAG_HASH_L4_SPORT) /* * Port isolation From 59c60504e7a873ecd9bea41e879cd72bc96156ba Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:41:34 +0200 Subject: [PATCH 18/25] crtbank: put the bank switching helpers in HOME __sdcc_banked_call and __sdcc_banked_ret were assembled into GSFINAL, which sits in the startup path: GSINIT ends exactly where GSFINAL begins, so the processor falls into it rather than being sent there. It works today only because this object comes after every C object on the link line, so the LJMP to __sdcc_program_startup is laid down first and the helpers land behind it. Reordering that line, or moving main() into another module, would put the helper body at the fallthrough address instead, and the board would not come up out of a build that reports nothing wrong. SDCC's own crtbank.asm declares the area order and then puts both symbols in HOME, so the file takes that name and that preamble as well. GSFINAL now holds the three byte jump and nothing else. Of 401 symbols 17 change address, every one in the startup region, and between the reset vector and 0x0094 not a byte differs, so no interrupt vector is disturbed. Run on a SWTGW218AS: it came back after about 42 seconds reporting the new build, with its stored configuration byte identical and every link at the speed it had before. --- Makefile | 2 +- crtbank.asm | 29 +++++++++++++++++++++++++++++ crtstart.asm | 17 ----------------- 3 files changed, 30 insertions(+), 18 deletions(-) create mode 100644 crtbank.asm delete mode 100644 crtstart.asm diff --git a/Makefile b/Makefile index 9ab0da85..40445516 100644 --- a/Makefile +++ b/Makefile @@ -118,7 +118,7 @@ $(BUILDDIR)/%.rel: %.asm ${ASM} ${AFLAGS} -o $@ $< # mv -f $(addprefix $(basename $^), .lst .rel .sym) . -$(BUILDDIR)/rtlplayground.ihx: $(OBJS) $(BUILDDIR)/crtstart.rel $(BUILDDIR)/crc16.rel +$(BUILDDIR)/rtlplayground.ihx: $(OBJS) $(BUILDDIR)/crtbank.rel $(BUILDDIR)/crc16.rel $(CC) $(CC_FLAGS) -Wl-bHOME=0x00000 -Wl-bBANK1=0x14000 -Wl-bBANK2=0x24000 -Wl-r -o $@ $^ $(BUILDDIR)/rtlplayground.img: $(BUILDDIR)/rtlplayground.ihx diff --git a/crtbank.asm b/crtbank.asm new file mode 100644 index 00000000..7a008687 --- /dev/null +++ b/crtbank.asm @@ -0,0 +1,29 @@ + .area HOME (CODE) + .area GSINIT0 (CODE) + .area GSINIT1 (CODE) + .area GSINIT2 (CODE) + .area GSINIT3 (CODE) + .area GSINIT4 (CODE) + .area GSINIT5 (CODE) + .area GSINIT (CODE) + .area GSFINAL (CODE) + .area CSEG (CODE) + + + .area HOME (CODE) + +__sdcc_banked_call:: + push _PSBANK + xch a,r0 + push a + mov a,r1 + push a + mov a,r2 + anl a,#0x1f + mov _PSBANK, a + xch a, r0 + ret + +__sdcc_banked_ret:: + pop _PSBANK + ret diff --git a/crtstart.asm b/crtstart.asm deleted file mode 100644 index 23146a75..00000000 --- a/crtstart.asm +++ /dev/null @@ -1,17 +0,0 @@ - .area GSFINAL (CODE) - -__sdcc_banked_call:: - push _PSBANK - xch a,r0 - push a - mov a,r1 - push a - mov a,r2 - anl a,#0x1f - mov _PSBANK, a - xch a, r0 - ret - -__sdcc_banked_ret:: - pop _PSBANK - ret From 3be66677895e254903fd8aa1229d33d14de38e11 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:21:05 +0200 Subject: [PATCH 19/25] httpd: emit valid JSON from send_l2 The MAC table listing wrote its separator once per iteration rather than once per object. An entry the table engine reports as invalid produces no object, so it contributed a bare comma, and two in a row give ",," which JSON.parse rejects. The whole table then fails to load, not just the row that was missing. The separator now goes before each object and the closing bracket after the loop, which is the shape send_vlanlist already uses further down the file. The next index for an invalid entry was computed as h | low + 1, and the addition binds tighter than the or. That agrees with (h | low) + 1 except when the low byte reads 0xff and bit 8 of the index is already set, eight of the 4096 combinations. There the result is the start of the current block of 256 rather than the start of the next one, so the walk repeats a block it has already covered. Reading the index once after the branch rather than once in each arm removes the second copy of that expression along with the bug. The VLAN now comes first in each object. It is taken from the same L2_DATA_OUT_B read that decides whether the entry is valid, which saves reading that register a second time. The page addresses the fields by name, so the order they arrive in does not matter to it. A bound check on the output buffer goes in for consistency with send_vlanlist. Thirty entries of at most 74 bytes plus the brackets fit in the 2500 byte buffer with 179 to spare, so nothing changes today, but the margin was nowhere stated and L2_MAX_TRANSFER is a tunable. 5 bytes of BANK1, nothing in BANK2, xdata or internal RAM. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0. --- httpd/page_impl.c | 52 ++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 19796edc..7b12927b 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -356,6 +356,7 @@ void send_l2(uint16_t idx) */ __xdata uint16_t entry = idx & 0xfff; __xdata uint16_t first_entry = 0xffff; // An illegal entry index + bool first = true; char_to_html('['); while (1) { entries_left--; @@ -369,9 +370,22 @@ void send_l2(uint16_t idx) } while (sfr_data[3] & TBL_EXECUTE); reg_read_m(RTL837x_L2_DATA_OUT_B); - if ((sfr_data[0] & 0x20)) { // Check entry is valid + bool valid = (sfr_data[0] & 0x20) != 0; + if (valid) { + /* separator + 74-byte worst-case entry + closing "]" */ + if (slen + 76 > TCP_OUTBUF_SIZE) + break; + if (!first) + char_to_html(','); + first = false; + + // VLAN, taken from the read above instead of reading the register twice + slen += strtox(outbuf + slen, "{\"vlan\":\""); + charhex_to_html(sfr_data[0] & 0x0f); + byte_to_html(sfr_data[1]); + // MAC - slen += strtox(outbuf + slen, "{\"mac\":\""); + slen += strtox(outbuf + slen, "\",\"mac\":\""); byte_to_html(sfr_data[2]); char_to_html(':'); byte_to_html(sfr_data[3]); char_to_html(':'); port = (sfr_data[0] >> 6) & 0x3; @@ -381,12 +395,6 @@ void send_l2(uint16_t idx) byte_to_html(sfr_data[2]); char_to_html(':'); byte_to_html(sfr_data[3]); - // VLAN - slen += strtox(outbuf + slen, "\",\"vlan\":\""); - reg_read_m(RTL837x_L2_DATA_OUT_B); - charhex_to_html(sfr_data[0] & 0x0f); - byte_to_html(sfr_data[1]); - // type reg_read_m(RTL837x_L2_DATA_OUT_C); if (sfr_data[2] & 0x1) @@ -396,32 +404,26 @@ void send_l2(uint16_t idx) port |= (sfr_data[3] & 0x3) << 2; itoa_html(port); + } - // Index - reg_read_m(RTL837x_TBL_DATA_0); - entry = (((uint16_t)sfr_data[2] & 0x0f) << 8) | sfr_data[3]; + // Index + reg_read_m(RTL837x_TBL_DATA_0); + entry = (((uint16_t)sfr_data[2] & 0x0f) << 8) | sfr_data[3]; + if (valid) { slen += strtox(outbuf + slen, ",\"idx\":\""); byte_to_html(entry >> 8); byte_to_html(entry); char_to_html('"'); char_to_html('}'); - entry += 1; // We want the next entry following after the current entry - } else { - reg_read_m(RTL837x_TBL_DATA_0); - entry = (((uint16_t)sfr_data[2] & 0x0f) << 8) | sfr_data[3] + 1; } - if (first_entry == 0xffff) { - char_to_html(','); + entry += 1; // We want the next entry following after the current entry + + if (first_entry == 0xffff) first_entry = entry; - } else { - if (first_entry == entry || !entries_left) { - char_to_html(']'); - break; - } else { - char_to_html(','); - } - } + else if (first_entry == entry || !entries_left) + break; } + char_to_html(']'); } From 3fda9ccd86424970a94f1149d6f0877a69967360 Mon Sep 17 00:00:00 2001 From: d00f Date: Fri, 14 Aug 2026 03:03:48 +0200 Subject: [PATCH 20/25] html: handle an empty MAC table reply and fix the entry cap l2.js reads the last index of a reply to know where the next page starts. With the firmware side of this branch an empty table answers [], and s[s.length-1] then throws on undefined. It used to answer commas with nothing between them and throw in JSON.parse instead, so this is the same case reaching a different line rather than a new one. An empty reply now renders what has been collected and starts the next pass from zero. The 4096 entry cap compared the array against the number instead of its length, so it never fired: an empty array and a 5000 element one both compare false. Comparing the length restores what the check was for. --- html/l2.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/html/l2.js b/html/l2.js index d3a058bc..aa5742fa 100644 --- a/html/l2.js +++ b/html/l2.js @@ -159,12 +159,17 @@ function getL2() { return e; }); l2Entries.push(...s); - if (l2Entries >= 4096) { + if (l2Entries.length >= 4096) { l2Entries = []; l2CurrentEntry = 0; clearInterval(l2GetInterval); return; } + if (!s.length) { + l2CurrentEntry = 0; + fillL2(l2Entries); + return; + } var w = 0; for (var i = l2Entries.length-1; i > 0; i--) { if (l2Entries[0].idx == l2Entries[i].idx) { From a96fdfe10c471cd04db86b76a53a57edd4c29684 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:59:01 +0200 Subject: [PATCH 21/25] html: move the L2 table walk into a shared walkL2() helper Both the L2 page and the ports page in #335 need to page through /l2.json and decode the same fields, and the second copy arrived carrying the two bugs the first one had only just been fixed for. Rather than keep two copies in step by hand, the transport and the decoding move to main.js, which every page already loads, and each page keeps only what is its own. walkL2(onDone) pages through the table once, parses idx and vlan out of hex, maps the port to a physical number or to 'CPU', and calls onDone(entries, ok). It stops on a wrapped index, an empty page or 4096 entries, all of which set ok. A page that comes back as anything other than 200, or with a body JSON.parse rejects, is asked for again at the same index up to three times; only once those run out does the walk end with ok clear, so a caller can tell a finished table from a partial one. l2.js keeps the s and l to label mapping, since that needs the page's own translations, redraws only when ok is set, and restarts the walk from its callback either way. Two things change while moving: The next request goes out from the previous reply rather than from a setInterval that fires whether or not the last one came back. The httpd serves one connection at a time, so a timer that outruns the responses only queues work it cannot use. A walk that reaches 4096 entries hands over what it collected. Before it threw the entries away and cleared its own interval, which left the page unable to refresh again until it was reloaded. The retry is not a new idea, it is the old behaviour written down. The previous code ignored anything that was not a 200 and let the interval ask for the same index again, so a blip never disturbed the table on screen. Dropping that on the way to a chained walk would have made every timeout redraw the page with a truncated table, which at one connection at a time is not a rare event. Driven with a scripted server in node, running the helper itself rather than a copy of it: an empty table gives 0 entries in 1 request; three pages ending in a repeated index give 61 entries in 3 requests, asking for 0, 30 and 60; an empty page ends the walk after 2; a 500 and a malformed body are each retried at the same index and then complete normally, asking 0, 30, 30 and 31; three failures in a row end the walk with ok clear and the 30 entries already collected; 4096 entries in one page end it with ok set; the CPU port decodes to 'CPU'; vlan and idx come back as numbers. main.js grows by 1331 bytes and l2.js loses 1039, so 292 bytes of flash. Worth stating where they land: main.js is loaded by every page, so pages that never walk the table now carry the helper too. That is the cost of having the decoding exist exactly once, which is the point of the move. --- html/l2.js | 52 ++++++------------------------------------- html/main.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 45 deletions(-) diff --git a/html/l2.js b/html/l2.js index aa5742fa..2011c655 100644 --- a/html/l2.js +++ b/html/l2.js @@ -1,7 +1,3 @@ -var l2GetInterval; -var l2Entries = []; -var l2CurrentEntry = 0; - function fillStats() { var tbl = document.getElementById('statstable'); if (!numPorts) @@ -118,7 +114,6 @@ function fillL2(s) s = uniq(s); l2All = s; renderL2(); - l2Entries = []; } function paintL2(tbl, s) @@ -147,53 +142,20 @@ function paintL2(tbl, s) } function getL2() { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - var s = JSON.parse(xhttp.responseText); - var s = s.map(function(e) { - e.vlan = parseInt(e.vlan, 16); - e.idx = parseInt(e.idx, 16); - e.type = e.type == "s" ? t('l2_static') : t('l2_learned'); - e.port = e.port == 9 ? 'CPU' : logToPhysPort[e.port]; - return e; - }); - l2Entries.push(...s); - if (l2Entries.length >= 4096) { - l2Entries = []; - l2CurrentEntry = 0; - clearInterval(l2GetInterval); - return; - } - if (!s.length) { - l2CurrentEntry = 0; - fillL2(l2Entries); - return; - } - var w = 0; - for (var i = l2Entries.length-1; i > 0; i--) { - if (l2Entries[0].idx == l2Entries[i].idx) { - w = 1; - break; - } - } - if (w) { - l2CurrentEntry = 0; - fillL2(l2Entries); - } else { - l2CurrentEntry = s[s.length-1].idx + 1; - } + walkL2(function(entries, ok) { + if (ok) { + for (var i = 0; i < entries.length; i++) + entries[i].type = entries[i].type == "s" ? t('l2_static') : t('l2_learned'); + fillL2(entries); } - }; - xhttp.open("GET", "/l2.json?idx=" + l2CurrentEntry, true); - xhttp.timeout = 1500; sendXHTTP(xhttp); + setTimeout(getL2, 1000); + }); } window.addEventListener("load", function() { update( () => { getL2(); const interval = setInterval(update, 2000); - l2GetInterval = setInterval(getL2, 1000); });; }); diff --git a/html/main.js b/html/main.js index f82b2328..bd9c19d7 100644 --- a/html/main.js +++ b/html/main.js @@ -285,3 +285,66 @@ function sendXHTTP(x) currentRequests.push(x); } + +function walkL2(onDone) +{ + var entries = []; + var idx = 0; + var tries = 0; + + function retry() { + if (++tries < 3) { + setTimeout(page, 1000); + return; + } + onDone(entries, false); + } + + function page() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState != 4) + return; + if (this.status != 200) { + retry(); + return; + } + var s; + try { + s = JSON.parse(xhttp.responseText); + } catch (err) { + retry(); + return; + } + tries = 0; + s = s.map(function(e) { + e.vlan = parseInt(e.vlan, 16); + e.idx = parseInt(e.idx, 16); + e.port = e.port == 9 ? 'CPU' : logToPhysPort[e.port]; + return e; + }); + if (!s.length) { + onDone(entries, true); + return; + } + entries.push(...s); + for (var i = entries.length - 1; i > 0; i--) { + if (entries[0].idx == entries[i].idx) { + onDone(entries, true); + return; + } + } + if (entries.length >= 4096) { + onDone(entries, true); + return; + } + idx = s[s.length - 1].idx + 1; + setTimeout(page, 1000); + }; + xhttp.open("GET", "/l2.json?idx=" + idx, true); + xhttp.timeout = 1500; + sendXHTTP(xhttp); + } + + page(); +} From 9ada6adad7a8242c4e5b4004eee620e74cafd8e6 Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:41:51 +0200 Subject: [PATCH 22/25] lag: number the groups from one and bound what the command is given lag show has always printed the groups as 1 to 4 while lag took the number literally, so typing what you saw configured the group beside it. Both lag and lag hash count from one now, matching how ports are numbered everywhere else, and reject anything outside 1 to 4. Subtracting '1' makes 0 wrap well past three, so one test covers both ends. The port argument indexed machine.phys_to_log_port, which holds nine entries, before it was checked, and a two digit argument reaches 109. It is bounded before the table is touched rather than after. port_lag_members_set() and port_lag_hash_set() complained about a group out of range and then wrote the registers anyway, past the four the groups occupy. They return instead. lag hash also read cmd_words_b[1] without checking a word was there, and now shares the error path parse_lag() already had. --- cmd_parser.c | 19 +++++++++++++++---- rtl837x_port.c | 8 +++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index 9e2be41e..a1e187ea 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -273,7 +273,9 @@ void parse_lag(void) if (cmd_words_len < 2 || !isnumber(cmd_buffer[cmd_words_b[1]])) goto err; - group = cmd_buffer[cmd_words_b[1]] - '0'; + group = cmd_buffer[cmd_words_b[1]] - '1'; + if (group > 3) /* '0' wraps well past three, so one test does both ends */ + goto err; uint8_t w = 2; while (w < cmd_words_len) { @@ -283,7 +285,9 @@ void parse_lag(void) port = cmd_buffer[cmd_words_b[w]] - '1'; if (isnumber(cmd_buffer[cmd_words_b[w] + 1])) port = (port + 1) * 10 + cmd_buffer[cmd_words_b[w] + 1] - '1'; - port = machine.phys_to_log_port[port]; + if (port > 8) /* phys_to_log_port holds nine entries */ + goto err; + port = machine.phys_to_log_port[port]; } else { goto err; } @@ -295,7 +299,7 @@ void parse_lag(void) port_lag_members_set(group, members); return; err: - print_string("Error: lag [port]...\n"); + print_string("Error: lag <1-4> [port]...\n"); } @@ -304,7 +308,11 @@ void parse_lag_hash(void) __xdata uint8_t group; __xdata uint8_t hash = 0; - group = cmd_buffer[cmd_words_b[1]] - '0'; + if (cmd_words_len < 2 || !isnumber(cmd_buffer[cmd_words_b[1]])) + goto err; + group = cmd_buffer[cmd_words_b[1]] - '1'; + if (group > 3) /* '0' wraps well past three, so one test does both ends */ + goto err; uint8_t w = 2; while (w < cmd_words_len) { @@ -330,6 +338,9 @@ void parse_lag_hash(void) w++; } port_lag_hash_set(group, hash); + return; +err: + print_string("Error: lag hash <1-4> [type]...\n"); } diff --git a/rtl837x_port.c b/rtl837x_port.c index 9388cc60..a85e90df 100644 --- a/rtl837x_port.c +++ b/rtl837x_port.c @@ -758,7 +758,7 @@ void port_lag_members_set(__xdata uint8_t lag, __xdata uint16_t members) __banke print_string("port_lag_members_set, lag: "); print_byte(lag); print_string(", members: "); print_short(members); write_char('\n'); if (lag > 3) { - print_string("Link aggregation group must be 0-3!\n"); + print_string("Link aggregation group out of range\n"); return; } reg_read_m(RTL837X_TRK_HASH_CTRL_BASE + (lag << 2)); @@ -777,8 +777,10 @@ void port_lag_hash_set(__xdata uint8_t lag, __xdata uint8_t hash_bits) __banked { print_string("port_lag_hash_set, lag: "); print_byte(lag); print_string(", hash: "); print_byte(hash_bits); write_char('\n'); - if (lag > 3) - print_string("Link aggregation group must be 0-3!\n"); + if (lag > 3) { + print_string("Link aggregation group out of range\n"); + return; + } REG_WRITE(RTL837X_TRK_HASH_CTRL_BASE + (lag << 2), 0, 0, 0, hash_bits); } From 1ed8b131bc4a6f1949f93d83fead9925cfe49e9f Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:23:56 +0200 Subject: [PATCH 23/25] httpd: keep the send_l2 flags in bit memory The two flags added with the JSON fix sit in data, where internal RAM is full enough that this branch stopped linking for some toolchains. __bit puts them in the bit area instead and hands three bytes back to the stack: SSEG goes from 131 to 134 on SWTGW218AS. Patch by vDorst on the pull request. --- httpd/page_impl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 7b12927b..bf3e2682 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -356,7 +356,7 @@ void send_l2(uint16_t idx) */ __xdata uint16_t entry = idx & 0xfff; __xdata uint16_t first_entry = 0xffff; // An illegal entry index - bool first = true; + __bit first = true; char_to_html('['); while (1) { entries_left--; @@ -370,7 +370,7 @@ void send_l2(uint16_t idx) } while (sfr_data[3] & TBL_EXECUTE); reg_read_m(RTL837x_L2_DATA_OUT_B); - bool valid = (sfr_data[0] & 0x20) != 0; + __bit valid = (sfr_data[0] & 0x20) != 0; if (valid) { /* separator + 74-byte worst-case entry + closing "]" */ if (slen + 76 > TCP_OUTBUF_SIZE) From fa7895ad62339b627cca4266ad029d73f856524a Mon Sep 17 00:00:00 2001 From: d00f <8052722+DrDoof@users.noreply.github.com> Date: Sun, 16 Aug 2026 04:10:03 +0200 Subject: [PATCH 24/25] doc: the aggregation example used group zero, which no longer parses The command now numbers groups the way 'lag show' prints them, so the walkthrough would have failed at its first step. --- doc/link_aggregation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/link_aggregation.md b/doc/link_aggregation.md index b901d374..6bde787e 100644 --- a/doc/link_aggregation.md +++ b/doc/link_aggregation.md @@ -92,7 +92,7 @@ The following shows the network configuration On _both_ switches create a LAG with ports 1 and 2 inside and the default hash algorithm which takes source and destination ports into account, e.g. just use the default: ``` -> lag 0 1 2 +> lag 1 1 2 ``` From 56fa96c49420e832afc542e7c16a3ef0c647770f Mon Sep 17 00:00:00 2001 From: d00f Date: Thu, 13 Aug 2026 23:22:09 +0200 Subject: [PATCH 25/25] igmp: only hand reports to the CPU while snooping is on handle_rx() dispatched to igmp_packet_handler() on the destination address alone, so an ordinary IGMPv3 report off the wire reached the handler and could write a table entry whether or not anyone had asked for snooping. The STP branch right above it is gated on stpEnabled; this brings the IGMP branch in line. Snooping state lived only in the per-port registers, and the receive path cannot afford to read one per packet, so the flag shadows it: igmp_enable() sets it, igmp_setup() clears it, and igmp_setup() runs from both the boot path and "igmp off". While here, igmp off becomes an explicit subcommand instead of the fall-through, and an unrecognised igmp subcommand prints the usage line rather than silently turning snooping off. Six bytes of BANK1 and one of xdata, no internal RAM. --- cmd_parser.c | 4 +++- rtl837x_igmp.c | 3 +++ rtlplayground.c | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index 9e2be41e..ee5a93d7 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -1529,10 +1529,12 @@ void cmd_parser(void) __banked } else if (cmd_compare(0, "igmp")) { if (cmd_compare(1, "on")) igmp_enable(); + else if (cmd_compare(1, "off")) + igmp_setup(); else if (cmd_compare(1, "show")) igmp_show(); else - igmp_setup(); // Reverts to default with IP-MC being flooded + print_string("Error: igmp on|off|show\n"); } else if (cmd_compare(0, "hostname")) { /* "hostname" alone reports the current name; "hostname " * sets it, sanitized to JSON-safe printable ASCII. A name with diff --git a/rtl837x_igmp.c b/rtl837x_igmp.c index 3b267afc..feddf9ad 100644 --- a/rtl837x_igmp.c +++ b/rtl837x_igmp.c @@ -16,6 +16,7 @@ #include "machine.h" extern __code struct machine machine; +extern __xdata uint8_t igmpEnabled; #include "uip.h" @@ -85,6 +86,7 @@ void igmp_setup(void) __banked { uint8_t i; print_string("igmp_setup called\n"); + igmpEnabled = 0; // For now, forward all unkown IP-MC pkts (2 bits per port. 00: flood via floodmask, 01: drop, 10: trap, 11: to rport) REG_SET(RTL837X_IPV4_PORT_MC_LM_ACT, LOOKUP_MISS_FLOOD); REG_SET(RTL837X_IPV6_PORT_MC_LM_ACT, LOOKUP_MISS_FLOOD); @@ -130,6 +132,7 @@ void igmp_setup(void) __banked void igmp_enable(void) __banked { print_string("igmp_enable called\n"); + igmpEnabled = 1; // Configure trapping of unhandled IGMP protocol packets to CPU REG_SET(RTL837X_IGMP_TRAP_CFG, IGMP_CPU_PORT | IGMP_TRAP_PRIORITY); diff --git a/rtlplayground.c b/rtlplayground.c index bdb8ab3e..b9cc05d9 100644 --- a/rtlplayground.c +++ b/rtlplayground.c @@ -121,6 +121,7 @@ __xdata uint16_t management_vlan; __xdata uint8_t tx_seq; __xdata uint8_t stpEnabled; +__xdata uint8_t igmpEnabled; __xdata char hostname[24]; /* device hostname, default set at boot, see rtl837x_common.h */ __code uint16_t bit_mask[16] = { @@ -1152,7 +1153,7 @@ void handle_rx(void) print_string("STP TX\n"); tcpip_output(); } - } else if (uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet? + } else if (igmpEnabled && uip_buf[0] == 0x01 && uip_buf[1] == 0x00 && uip_buf[2] == 0x5e // IPv4-MC packet? && uip_buf[3] == 0x00 && uip_buf[4] == 0x00 && uip_buf[5] == 0x16) { igmp_packet_handler(); if (uip_len) {