bank_occupancy.sh takes the main and branch build directories as
optional arguments and prints a markdown table, so CI calls it instead
of a second script. It gains the HOME, BANK4 and stack rows, and reads
the web assets from the build directory, where html_min is now.
A port named in the CLI or the web UI (port <n> name ...) now carries
that name in a Port Description TLV, so a neighbour sees "Uplink" next
to "Port 9". The tail of the frame is built per port, since the TLV
length differs from port to port; ports without a name send no TLV.
The bank growth step only exported the sizes to the environment since the
comment that used them was removed, so nothing showed them any more. It now
writes a table of the code banks, XSEG, the stack left after the internal
RAM is allocated and the web assets, main against the branch, to the job
summary, which needs no write permission on the pull request.
The mcs51 library target writes into ../build/$(PORT) without depending on
it, so with -j the archiver can start before the directory exists and the
toolchain build fails with
sdar: ../build/small/mcs51.lib: No such file or directory
It is intermittent: the same tree builds fine on a retry.
The port ID in the BPDUs was the chip port plus one, so on most boards
it did not match the port label, neither on the Spanning Tree page nor
on the neighbour. Send the front panel number instead, and 101 and up
for the LAGs, the same numbers the page already uses for the Port
column. A returned BPDU of our own is mapped back to the port by that
number.
Scenario 66 gives one port a front panel number that differs from its
chip port.
send_lldp(), send_eee(), send_bandwidth(), send_storm(), send_mtu() and
send_status() wrote the closing ']' inside the loop, on the last port.
Write it once after the loop, as send_isolation() already does, so the
array is closed however the loop ends.
Build the harness with -fpack-struct=1 and assert the frame struct sizes,
so the host reads wire bytes the way the firmware lays them out. Add
scenarios for a port that regains edge after a hold, one that heard a BPDU
and stays non-edge, auto-edge switched off, and a captured RouterOS BPDU
fed in on every port, tagged and untagged.
stp_loop_hold_peer() takes operEdge away from the port that sent the
BPDU we got back, though that port heard nothing itself. When the hold
ran out the port stayed non-edge until its next link change, and every
later state change on it counted as a topology change. Restore edge when
the hold ends with the port still silent and auto-edge on, before the
transition is reported, so the recovery itself is not a change. Hellos
go out on edge ports too, so a loop that persists is caught again.
The console column that shows seconds since the last BPDU is now called
age[s]; it never was a receive count.
stp_reselect() took every port with fresh information as a candidate, so a
BPDU credited to a port whose carrier the once-a-second link poll had not
seen yet made that port the root port before the port was set up for it.
A candidate now needs its link bit as well; the poll that sees the carrier
a moment later runs the selection again. Scenario 54.
stp_in() propagated the TC flag of any BPDU that did not arrive on an
alternate port, and acknowledged and propagated a TCN from any port, so a
port still in its listen period or held discarding in a dispute could flush
the addresses of every other port and move the topology change counter.
802.1D-2004 17.31 keeps the Topology Change machine in LEARNING until the
port forwards and drops rcvdTc and rcvdTcn there. Both paths now ask for
the forwarding state first; an alternate port is discarding, so the old
check is covered by the new one.
Scenario 53 plays a TC flag and a TCN into a port that is still discarding,
and then into the same port once it forwards. Scenario 51 used a TCN on a
port one tick short of its edge transition as the trigger for the clock it
tests; it now waits for the transition.
The frame the simulator builds carries a vlan_tag between the RTL tag and the BPDU, but left its TPID zero. stp_in() now takes the body offset from TRAP_RX_BODY(), which reads those two bytes, so it took the untagged branch and parsed the body four bytes early. Writing 0x8100 makes the frame what its layout says it is.
Reception moved to the RMA trap in main, so rtl837x_stp.c calls trap_rma_set() from stp_setup() and stp_off(). The simulator defines neither, so it does not link. Linking rtl837x_trap.c instead would pull trap_init() and the register primitives it needs, which the simulator does not mock.
stp_sim.c declared variables in the middle of the scenarios and put
whole loops and conditions on one line. Each block now starts with its
declarations; an initializer that depends on what happens before it
stays where it was as an assignment. Bodies of if and for are on lines
of their own, functions are separated by an empty line, and lines stay
within 120 columns. The output of stp_sim -v is unchanged.
Scenarios 4 and 7 start from the state scenario 3 leaves behind, and 38
from the one 37 leaves, and they called those scenarios for it, so the
headings of 3 and 37 showed up again in the middle of the run. The setup
of 3 and 37 now lives in functions of its own that the scenarios call.
test/stp/shim held hand-written copies of the headers the STP code
includes, and they had drifted: the uIP buffer was 600 bytes instead of
1556, struct machine had three fields, port_ingress_filter_get()
returned a plain uint8_t. The simulator now compiles rtl837x_stp.c and
rtl837x_stp_cli.c the way test/ compiles firmware sources, with the real
headers and sdcc_shim.h forced in, and with the same warnings, including
-Wno-pointer-sign for the string literals passed to cmd_compare(). It
stays on -std=c11: in gnu11 mode glibc defines LITTLE_ENDIAN, which
uip-conf.h names as the byte order, and HTONS() stops swapping. Its own
print_string() and cmd_compare() take the firmware's argument types.
* stp: select the root port by cost and block redundant links
* stp: move the protocol to BANK4 and keep the command line in BANK2
* stp: implement the RSTP state machines of 802.1D-2004 clause 17
* stp: counters, more settings and a fuller Spanning Tree page
* stp: make a link aggregation group one port of the RSTP tree
* web: serve the Spanning Tree JSON from BANK4 as well
Kept in BANK1, the larger Spanning Tree page would leave under 1 kB
there for the rest of the web server and every page still to come, so
send_stp() and send_stp_counters() move to httpd/page_stp.c in BANK4
with the protocol, as banked calls, with their own copies of the few output
helpers and of the JSON header, so nothing in BANK4 calls into BANK1
directly or hands it a pointer into its code space. BANK4 takes the
free flash between BANK3 and the web files.
KP_9000_6XHML_X2: BANK1 44336, BANK2 36969, BANK3 11942, BANK4 20965
bytes of 49152.
Horaco HC-SWTGW218AS units on this PCB keep their factory MAC at
0x1FC000, the offset the other boards in machine/ read it from, so use
it instead of a generated locally administered one. If the flash holds
no valid MAC there, the generated one is still used.
200 seconds was too short in practice: the UI stops polling while its
tab is hidden, so looking at another tab for a few minutes ended the
session. Make it 3600 seconds; "session <seconds>" still overrides it.
Hardware reset starts the firmware with SFR 0x97 = 1, which maps xdata
0x4000-0xbfff to the flash image: reads return flash bytes, writes are
lost. main() cleared it only after __mcs51_genXINIT and the XSEG clear
had run, so XISEG (initialised globals) and the part of XSEG above
0x4000 kept stale RAM contents, e.g. session_timeout = 4484 and LLDP
blocked on logical ports 0, 3 and 5. Clear it in
__sdcc_external_startup(), which SDCC calls before both.
Rename SFR_97 to MAP_FLASH_PAGE_TO_XDATA.
SW1 on PCB-SWTG024AS-A-V2.0.1_19650 is wired to GPIO48. Measured on
HC-SWTG124AS: GPIO48 reads 1 idle and 0 while the button is held.
HC_SWTG124AS.c is a symlink to PCB_SWTG024AS_A_2_0_1.c.
A commit hash can be looked up directly, a checksum cannot. Print the last commit that touched sdcc/, with -dirty for local changes, and fall back to the checksum when git is not available. The toolchain directory keeps the checksum.
The installer Makefile called whatever sdcc was on the PATH, so an
installer built by hand came out of the system compiler, without the
fixes in sdcc/. Use the same toolchain/sdcc-<cksum> as the main Makefile,
built on demand by sdcc/build.sh if a bare installer build gets there
first, and keep CC= on the command line as the way to pick another
compiler. The README example shows the new command lines.
The boot banner and the version command print the SDCC release and
revision from the compiler's own macros, and the name of the in-tree
toolchain the Makefile built, which is the checksum of sdcc/ and so
changes with every patch in it. A build with CC given on the command
line says "system" instead, since that compiler need not carry the
patches. A plain release and the same release with the patches report
the same revision, so the toolchain line is what tells them apart.
Costs 66 bytes of BANK2.
Review feedback on #560 (DrDoof):
- A login that came with a cookie that is still valid took a second
slot. The first one looked fresh, so the next login could push out
someone who is using the switch. The slot of the cookie now gets the
new id.
- Changing the password leaves no old login valid: session_init() from
parse_passwd(). With one session the next login pushed a leftover one
out, with several it would stay valid until the next restart.
- The 32-bit comparison of the last use times moves into a leaf function,
so the compiler does not give it a 4-byte scratch in internal RAM.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The web server's bank had no room left for the four sessions once the
original firmware's more robust header handling came in. Picking a slot,
checking a cookie and starting a session move to sessions.c in bank 3;
httpd.c only calls session_init(), session_check() and session_new().
The random id is written straight into the reply, which drops
gen_random_hex_chars() from bank 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Logging in from a second browser ended the first one's session at its next
request: two browsers open on the switch logged each other out every few
seconds. Up to four sessions now, each with its own cookie and expiry; a
fifth login replaces the least recently used. The expiry check is a leaf
function so its 32-bit scratch stays out of internal RAM (stack still
159 bytes with HEALTH).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tx2W7xVecMoKbBCkD2i8PG
(cherry picked from commit 03330f4077ef8872d861d951c8e44ece3c573935)
it'll accept just the definition.
Also throughout the regex we have over-specified the number of digits
sometimes ports are 4 digits long, sometimes they are 2. This seems
unnecessary, 1 or more is sufficient
the user clicks apply on the specific vlan. In order for this to
work when the user is simultaneously changing the vlan that their
machine is connected through and the mgmt vlan id, we need to be
able to send a batch of commands through to the switch atomically
so the switch can process all of them and the momentary disconnection
of the machine doesn't matter
When adding a new VLAN, we should load it after addition
Prevent deleting the mgmt vlan
Adding a button to promote a vlan to be the management VLAN
We can't create a vlan called d or mgmt as that clashes with the
commands to delete and promote a vlan
Making the Management VLAN more obvious and adding a "confused"
disabled button to where delete and promote are
Switching to the i image and making it so you can click the entire
row apart from the cell where the buttons are (to prevent accidents)
Cleaning up the last bits of ingress filtering I can see
Also making the first VLAN in the table load when we load the UI
Removing the tickbox for the PVID, I'll swap this out to ticks
or something later on, but the PVID will be automatically set
based on the untagged membership, otherwise it will be 1
Send the vlan with name command when attempting to add a new VLAN
Because of the structure of the vlan command, we need to send the name as well if its set
Set the PVID based on the untagged membership and reload the
table when applied. We need to block setting the PVID to the tagged by proxy
Blue button
Need to provide array of pvid commands not concat
Storing state of each vlan so we can refer later
Blue button
Device page following doc/adding-a-device.md: label data, PCB versus label
naming, CPU identified with 'regget 4' (RTL8373, non-N), separate PHY and flash
identification, build target, PCB silkscreen, port mapping, board signals, front
panel and LED configuration, reset button, MAC location, what works and what is
unverified, plus photos. Row added to supported_devices.md.
RTL8373 (non-N) with an RTL8224 quad 2.5G PHY, 2 MB GD25Q16ESIG flash, no PoE.
The wiring is close to the KP-9000-9XHML-X V3.1 (same 8+1 topology, same I2C on
GPIO39/40), but three things differ and were determined on hardware:
- ModAbs is GPIO30 (low with a module inserted, high with an empty cage) and LOS
is GPIO37 (high without light), not GPIO38. GPIO38, the ModAbs pin of the
KP-9000 reference, is tied low on this board, which made the firmware believe a
module was permanently inserted and retry the I2C read forever.
- The reset button is GPIO54, not GPIO48. GPIO48 never changes here, so the
button appeared to do nothing.
- The LED wiring follows the stock port order (pin 3n+m belongs to port n+1,
LED function m), so the mux table is the chip default except that pin 25 --
the SFP LED -- is driven by function 0, the only populated function in SET1.
Per copper port the left LED shows link/activity and the right LED shows
2.5G, matching the front panel labels.
Verified on hardware: all eight 2.5G ports at 10/100/1000/2500 Mbps, 1G and 10G
SFP modules including DDM data, both LED columns, module insert/remove and the
reset button.
html_min was phony, so every make deleted output/html_min and minified
all of html/ again, even with nothing changed. A stamp file now stands for
the minified copy and depends on the sources and on minify.py. A list of
the source names, rewritten only when it changes, is a dependency too, so
adding or removing a file in html/ still redoes the copy and a removed
file does not stay behind in the image. If output/html_min is missing,
the stamp is remade in the same run.
The note on version.h in webui-compression.md still said it was phony.
Three more bench scenarios around the closing boundary. Every value
of the last image byte, uploaded whole and split right after the
payload, must end the part. A run of CR before the boundary of even
length passed before the fix and an odd one did not, so both are
checked. An image made of CR only, longer than a flash page, and one
made of repeated "\r\n-" exercise the broken matches back to back:
each one appends to the page buffer without a byte written, and the
page flush after both paths has to keep up.
stream_upload() matches the closing multipart boundary byte by byte.
When a byte breaks a partial match, the matched prefix goes to flash as
data, and so does the breaking byte, without being compared against the
start of the boundary.
The boundary starts with "\r\n--", so an image whose last byte is '\r'
never completes: that '\r' starts a match, the '\r' of the boundary
breaks it and is written as data, and the rest of the boundary no
longer matches. The browser waits for a reply that never comes.
Flush the matched prefix and look at the breaking byte again from the
start of the boundary. A boundary may not contain CR, so no longer
suffix of the matched prefix can start a new match.
The 8051 assembler sign-extends every value to 24 bits, so the top
byte of a __sfr32 address list like 0xa4a5a6a7 comes out as 0xff.
The patch keeps 32-bit arithmetic while leaving the 24-bit T line
addresses alone, so the relocations are unchanged. Proposed upstream
in the bug thread. With it SFR_DATA_U32 can be used again, which
saves about 240 bytes of code in REG_SET.
The workflow and the Dockerfile install what building SDCC needs instead
of the distribution's sdcc, and the workflow caches toolchain/ keyed on
the contents of sdcc/.
Released SDCC 4.5.0 and 4.6.0 miscompile some of our code in the mcs51
register allocator (SDCC bugs 4153 and 4154, fixed in trunk r17025 and
r17039). The first make now downloads the 4.6.0 sources, checks their
sha256, applies the two fixes from sdcc/ and installs the compiler into
toolchain/, named after a checksum of sdcc/, so a changed patch builds a
new one. make CC=sdcc still uses an installed compiler.
The masks were 0, so 0x3e2 was ORed into the reset value 0xbff and both
SerDes stayed off on boards without 10G ports (n_10g == 0), e.g. the
HC-SWTG124AS. With SDS 0 off RTL837X_REG_SEC_COUNTER does not hold a
written value: it reads 0, so NTP never retried after a failed first
attempt at boot and froze the clock after a sync.
Tested with regset on an HC-SWTG124AS: the register keeps a write with
SDS 0 in SGMII or 10GR, not with SDS 0 off (also not with only SDS 1
on).
Firefox filled a saved login into the System tab's password fields and
its user name into the NTP poll interval, the input before them. That
input event marked the Time card as edited, so ntpLoad() left NTP off,
the server empty and the time zone at UTC-12:00.
In HEALTH=1 builds health_phase(HEALTH_LLDP) used id 7 while ph_max[] and
ph_slow[] had HEALTH_PHASES = 6 entries. The LLDP checkpoint therefore
wrote one byte past ph_max (into the high byte of ph_slow[0]) and, when
the phase took two ticks or more, incremented two bytes past ph_slow
(dhcp_state.state and transaction_id on KP_9000_6XHML_X2).
Rename it HEALTH_PH_LLDP = 6, derive HEALTH_PHASES from it so the arrays
always cover the last phase, print an lldp line in "health" and document
it.
Fixes#658
Chrome on macOS labels .bin files application/macbinary. Searching for application/octet-stream mistakes the image bytes for more part headers and aborts when the preamble buffer fills.
Find the multipart header terminator instead, after the HTTP separator retained by handle_post. Keep authentication, CRC checks and staging limits unchanged. Cover MacBinary, an omitted part Content-Type, and a split header terminator with exact staged-byte checks.
the bits we need
But we need both main and branch checked out and the action can
do it for us
Pulling the bank occupancy from the map produced by sdcc rather
than the output of the build as it appears the output number is
only the maximum size of the banks
BANK0 isn't output, but what I'm guessing are the consituents for it are
With UIP_CONF_MAX_CONNECTIONS set to one, a verified upload can leave
reset intent set when its response is never acknowledged. A subsequent
TCP connection reuses that slot, and ACKing its plain GET response can
otherwise trigger reset_chip(). This is sequential slot reuse, not
concurrent HTTP connections.
Store reset intent in the connection appstate and initialize both tstate
and fw_reset_pending on every uip_connected() event, regardless of stale
appstate contents. Reset only after the verified upload response is fully
acknowledged.
Exercise the real HTTP callback for all stale tstate bytes, sequential
slot reuse and a later GET, partial/final ACKs, abort, timeout and CRC
failure. Keep the regression tests in the existing tab-indented style.
Address review by documenting regget 4 and RTL8373 N/non-N ID values under Step 4 of adding-a-device.md, without referring to a particular board or set of units. Keep the verified Horaco CPU and flash capacity in its device table and retain the unverified-chip caveat.
Read register 0x0004 on all three tested HC-SWTGW218AS units. Each returned 0x83730000, identifying RTL8373 rather than RTL8373N. Firmware reports 2 MB flash on all three.
Fill in the verified device-table fields and document the non-N/N register distinction. Leave PHY and flash-chip models explicitly unverified.
The HC-SWTGW218AS is listed only with the SWTG018AS-A V2.0 board, but it
also ships with SWTG118AS-V2.1, the board of the XikeStor SKS3200-8E1X.
That version runs MACHINE=SWTGW218AS: three such switches were flashed
through the OEM web upgrade and work.
Note in the device page that the model name alone does not tell which
firmware to build.
Add the assembled top, front and bottom of the case next to the PCB photos, and
record what the nameplate says (manufacturer, the model description translated
from the Chinese label, 12 V DC 2 A and the serial number format) so a device
can be identified by its label. The top of the case carries no model name.
Also point out where the three console through-holes sit in the top side photo
and note the PWR and SFP1/SFP2 LEDs on the front panel; the case legend for the
copper LEDs reads 1000M/2500M.
The hongyavision HYWS-SGT0204S is sold as an unmanaged 4x2.5G + 2x10G SFP+
switch and carries the 2G5F4_10G2_V1.01 marking on the PCB. It is a sibling of
the KP-9000-6XH-X2 (2M-PCB43-V1.1), so machine/2G5F4_10G2_V1_01.c is that board
definition with these changes:
- The two SFP+ cages (physical ports 5 and 6) are wired the other way round:
physical 5 is on SDS1 with GPIO30/GPIO37, physical 6 on SDS0 with
GPIO50/GPIO10. Copper ports 1-4 match the KP-9000 layout.
- No reset button is fitted and the case has no opening for one, so reset_pin
is GPIO_NA. GPIO54, the reset pin on the KP-9000 layout, reads as permanently
pressed here.
- The board definition sets flash_sio, so the flash is read with single-IO: the
fitted 512 KB Bright Moon T25S40 cannot be read in DIO mode on this board, an
image that leaves DIO enabled does not come up at all, while the same image
with single-IO boots and runs normally (two units).
- The board is added to the machine_custom_init() group that sets
LED_GLB_IO_EN bit 6, otherwise the RTL837x "solid" conditions (LINK/speed)
never drive the LEDs and the 2.5G LED glows faintly at 100M/1000M.
flash_sio is a new field of struct machine that rtlplayground.c hands to
flash_init(); only this board sets it, every other machine keeps DIO. It costs
management-plane speed only, the ASIC keeps forwarding in hardware.
The stock firmware runs a factory test console on the serial header at 9600
baud, whose silkscreen labels are shifted by one pin.
Verified: warning-free build, `make machine_check` passes for every target, the
host tests pass, and a control build of this target without the LED hook differs
from the KP-9000-6XH_X2_V1_1 image in 44 bytes: all inside the machine
description (including flash_sio) and the image CRC16, nothing else in the
firmware moved. On the board the single-IO image boots and runs on two units;
port order, SFP cages and LED behaviour come from the machine definition
verified on this board earlier.
UART is broken out to the unpopulated switch header at the front of the
board.
This isn't quite the same as the V2.0 definition as the system led is
always on, not routed through the CPU.
Additionally, document that the newer revision of the same "YS25-0402"
is supported via the existing PCB-K0402WS-V3.0 target.
The strip no longer wraps. Ports shrink to fit the row and scroll
when they cannot, so the header on a phone stays at two rows. The
strip moves under the title only below 720 px, where it no longer
fits beside it. Drop the unused d_ports strings and the strip title.
The row of port icons lived in a card on the Dashboard, so link
state and speed were out of sight on every other tab. Move it into the
header between the title and the save button, a little smaller, and
poll /status.json whenever the page is open instead of only on the
Dashboard, Ports and Statistics tabs; the existing idle and hidden-tab
gating still pauses it. On narrow screens the strip wraps to its own
row under the title.
The column counts the addresses the switch learns from incoming frames,
so "Connected" suggested a connection it does not show. The translations
follow the words the L2 tab already uses for learned entries.
Save to flash merges the running log into the startup configuration.
Neither its toggle nor its overwrite rules knew "lldp on|off", so every
switch of LLDP added another line: four switches left four lines.
"port <n> lldp permit|block" fell under the rule for "port <n> <speed>",
so setting a port's speed dropped its LLDP setting from the saved
configuration, and the other way round.
"lldp on|off" is now a toggle, and a per-port LLDP setting replaces only
an earlier per-port LLDP setting of that port.
hexvalue[] holds four bytes, but atoi_hex() stored every digit it was
given, so a ninth digit was written past the array into whatever follows
it in xdata. More than eight digits now return 0, as any bad input does.
c - '0' < 10 is computed as an int, so characters below '0' such as '-',
'.' or '/' passed as digits and "-1" came out as 0xdf. The difference is
now taken as uint8_t, so they are rejected.
The button in the header was always drawn as the primary action, on
every page, so it competed with each page's own buttons, for example
the upload button on the Firmware page. It is now a plain button and
turns primary together with the "unsaved changes" badge.
The port tooltip and the port details showed "TX good / bad: 8794 / 0
pkts", which reads as 8794 bad packets. Each counter now has its own
line, labelled as on the Statistics page, and the two strings only that
line used are removed from every language.
"table.t th" sets text-align:left and is more specific than ".num", so
the headers of the counter columns on the dashboard and the statistics
page stayed left-aligned above right-aligned values.
The switch keeps flooding 224.0.0.0/24 with IGMP on, but a report for a
group in that block still made an entry for it, and the entry limited
the group to the ports that reported it. A host announcing mDNS on
224.0.0.251 thereby cut mDNS off from every other port. Such reports
are now ignored, as RFC 4541 asks.
igmp_setup(), which `igmp off` also calls, enabled the lookup of IPv4
multicast in the L2 table. Groups learned while IGMP was on therefore
kept going only to the ports that had reported them, and with reports
no longer trapped nothing updated those entries until a reboot.
The lookup is now enabled in igmp_enable() and disabled in igmp_setup(),
so with IGMP off all IP multicast is flooded again.
Move port_bit() to rtl837x_common.h as PORT_BIT(), a uint16_t shift,
and MAC_ADDR_LEN from rtl837x_lldp.h next to it, and use MAC_ADDR_LEN
for every address in the LACP code instead of a literal 6. Define
lacp_mux_update() before its first caller and drop the forward
declaration, declare variables at the top of their blocks, put the
operator at the end of a continued line, drop the field value comments
of the LACPDU struct, and leave lacp_port_lag to lacp_init() instead of
initialising it in the test environment.
The trap is described with STP, so the LACP page only says that it is
used. The LACP sections leave link_aggregation.md, which now points to
lacp.md, and the timing section gives the values in seconds and the
802.3ad requirements without the test history.
The comments repeated the documentation, described tests or explained
the code to its author. What stays describes the LACPDU structures. The
timer constants are decimal, in units of the LACP timer.
The LACP test now compiles with the shared harness flags and
sdcc_shim.h against the firmware headers, so the copies under
test/lacp/shim are gone. test_port_tables already covers the trunk hash
seed that test/port_hash checked against its own register copies.
lacp_in() declares its locals at the top, which the stricter host build
needs.
The port and group of lacp_in(), the member mask passed to
lacp_lag_set() and the compiler temporaries both functions spilled took
six bytes of DSEG, and with them six bytes of stack. The locals and the
mask live in xdata now, the partner fields go through the scratch word,
and lacp_lag_set() records the new group of a port before it releases
the old one. One byte is left: the group number passed in.
lacp_in() always leaves uip_len at 0, so the tcpip_output() after it
never ran. The LAG page names the active members by front-panel port
instead of a logical mask. The partner key is not part of selection;
say so under Known limits.
Listing by next-address also returned the reserved-multicast entries STP
writes, shown as port 1 with a delete button, while l2_delete() still
looks up by next-unicast and so removed the unicast entry after that
index instead. The change was there to show the LACP steering entries;
delivery by trap writes none, so it goes.
Both paths wrote the trunk registers directly and left the protocol
running on the group, so "lag show" said disabled while the partner kept
both links in one aggregate, and re-sending the LACP line did not repair
it because the same-ports shortcut returns early. Each path now releases
the group from LACP first; for a group that never ran LACP that is a
no-op.
The receive machine armed current_while from the partner's TIMEOUT bit,
so against a partner on the slow rate a port whose LACPDUs stopped with
the link still up stayed in the trunk for the long timeout, two minutes.
802.3ad 43.4.12 starts the timer from the actor's own LACP_Timeout, and
we always advertise the short one: the partner sends every second and is
let go after the short timeout.
Sandbox T23 plays a slow-rate partner that goes quiet and fails on the
code before this.
Dynamic link aggregation on top of the static LAGG, for RTL8372/8373:
receive, selection and mux state machines (coupled mux, one aggregator
per group), with "lacp show" to look inside. Each of the four trunk
groups runs its own aggregator on the candidate ports assigned with
"lag <n> lacp <ports>"; the engine starts with the first such group and
stops with the last.
LACPDUs reach the CPU through the reserved-multicast trap, so delivery
needs no L2 entry and nothing written per VLAN or per PVID. The trap
acts on the address before the VLAN lookup and before the ingress
filters, so a LACPDU also arrives on a port that admits tagged frames
only and on a port STP holds in blocking, neither of which a forwarded
one reaches. A trapped frame carries no VLAN tag,
so lacp_in() takes the offset of the PDU body from TRAP_RX_BODY(). The
action is set with trap_rma_set(), which leaves the flags sharing that
register alone, and "lacp off" puts the slow-protocols drop default
back. doc/lacp.md and doc/link_aggregation.md record what was measured,
including why the trap looked like a dead end at first.
The web UI gets a Static/LACP mode per group on the LAG page with live
status, and the startup-config validator knows the per-group syntax. A
host-side sandbox for the state machines runs in the test harness
together with the LAG hash check. It drives both forms of an incoming
LACPDU, so the body offset of a trapped one is covered as well.
The L2 table page iterated with the "next unicast" read method, so
static multicast entries such as an IGMP group were invisible. It now
iterates with "next address", skips L3 entries (their layout carries IP
addresses where a MAC is expected), and reports the lowest member of a
multicast entry's port mask, so such an entry shows up as a static
entry on that port.
A port taken out of LACP forgets its partner, so "lacp show" after
"lacp off" no longer reports one. Applying the same candidate set to a
group again, as the LAG page does on every Apply, leaves the trunk
alone instead of tearing it down for a second or two. lacp_in() checks
the frame length and ethertype before reading a LACPDU, so a runt or a
foreign frame on the slow-protocols address cannot leave stale buffer
bytes as a partner. The sandbox covers these three in T20-T22. On the
LAG page, LACP mode with no ports checked sends "lag N lacp off", and
the startup-config validator wants at least one port after "lacp".
The document was renamed from doc/trunking.md in "Update LAG configuration
registers" and the entry in the README kept the old name, so it leads
nowhere.
"port <port> cabletest" runs the cable diagnostic of the copper PHYs
and reports one line per pair: ok, or open, short, short to another
pair or impedance mismatch with the distance to the fault.
The PHY part follows the cable_test_start and cable_test_get_status
ops of the Linux phy_driver and takes the port number:
rtl_phy_cable_test_start() triggers the test and
rtl_phy_cable_test_get_status() returns true once all four pairs are
done, with a result code and a length per pair. The decode order is
the one of the Linux driver. The same two functions drive the RTL8224
and the internal PHYs of the RTL8372 and RTL8373.
The command only powers the port down and returns. A state machine
called from handle_tick() calls the start op after one second, polls
the status op, stores the result, clears the trigger, also after a
timeout, and restores the control register, so the main loop never
waits. The result is printed when it is ready and "port <port>
cabletest show" prints it again. One test runs at a time.
The result flags are the low byte of the per-pair status word. The
vendor code reads the high byte, which is always zero on the RTL8373.
A link partner that keeps transmitting makes the PHY report an
inter-pair short on every pair, and forcing the speed with
autonegotiation off, as the Linux driver does, does not take the link
down. The port is therefore powered down for a second before the
test. The control register is restored afterwards with an
autonegotiation restart, so speed settings survive and the link
returns.
A test started on a port that is switched off never finishes and
leaves the PHY unusable until the switch is reset, so such a port is
refused, and a test is dropped if the port is switched off during the
wait. SFP ports are refused by the existing check. The external copper
PHYs of the RTL8372 boards are refused because the test has not been
run on one.
The distance is the raw length word minus 150, at 43 counts per 50 cm.
Fitted on open cables of 0.9 m, 2 m and 7.6 m; the pairs of one cable
differ by about 0.2 m. The Linux formula, raw minus 620 at 78 counts
per metre, gives 0, 0 and 2.4 m for these cables.
An unplugged port has no defined result: some pairs report open at
about 0.5 m and others a short or a mismatch at 2 to 3 m.
lldp_tick counted main-loop passes, so the 6000 meant for 30 s at
SYS_TICK_HZ ran at whatever rate the loop happened to have, and the
interval moved with httpd and SFP load. Step once per second from the
tick counter like the STP timers do, count seconds to the interval and
derive the TTL from it with the 802.1AB hold multiplier of 4.
Move port_bit() to rtl837x_common.h as PORT_BIT(), a uint16_t shift,
and MAC_ADDR_LEN from rtl837x_lldp.h next to it, and use MAC_ADDR_LEN
for every address in the LACP code instead of a literal 6. Define
lacp_mux_update() before its first caller and drop the forward
declaration, declare variables at the top of their blocks, put the
operator at the end of a continued line, drop the field value comments
of the LACPDU struct, and leave lacp_port_lag to lacp_init() instead of
initialising it in the test environment.
The System Description TLV carried the model name alone, so an
inventory built from LLDP could not tell which firmware a switch runs.
Append the version string, as in "SWTGW218AS 8+1 Managed Switch
v0.1.0-1856aeba".
lldp off and port <n> lldp block went silent and left the neighbour
holding our entry until its TTL ran out, 120 s later. Send one LLDPDU
with a TTL of 0 on the ports that stop, the shutdown form 802.1AB
defines for this, before the transmit mask changes.
Neighbours could not tell a bridge from a host and had no address to
reach us on. Add the System Capabilities TLV (bridge, enabled) and a
Management Address TLV with the IPv4 address, left out while the
address is still 0.0.0.0. Size the payload buffer for the frame it can
now hold: 64 bytes did not cover a 23 character hostname next to the
longer model names.
The Port ID TLV carried a single digit with the locally assigned
subtype, so a neighbour listed us as port "3". Send "Port 3" with the
interface name subtype instead, the form the port keeps in the CLI and
the web UI.
The simulator answered /status.json without the "macs" field the Ports page now reads. It counts the dynamic entries of its own L2 table for the port, so the column and /l2.json agree and the two static entries show the difference.
The column listed the MAC addresses of the L2 table entries of a port, which cost a chain of paged /l2.json requests every tenth status poll. The status JSON carries the count now, so the column reads it from there and the page does not touch the L2 table at all.
The register counts the dynamic entries, so the static ones the list used to include are not in the number.
The trap is described with STP, so the LACP page only says that it is
used. The LACP sections leave link_aggregation.md, which now points to
lacp.md, and the timing section gives the values in seconds and the
802.3ad requirements without the test history.
The comments repeated the documentation, described tests or explained
the code to its author. What stays describes the LACPDU structures. The
timer constants are decimal, in units of the LACP timer.
The LACP test now compiles with the shared harness flags and
sdcc_shim.h against the firmware headers, so the copies under
test/lacp/shim are gone. test_port_tables already covers the trunk hash
seed that test/port_hash checked against its own register copies.
lacp_in() declares its locals at the top, which the stricter host build
needs.
The port and group of lacp_in(), the member mask passed to
lacp_lag_set() and the compiler temporaries both functions spilled took
six bytes of DSEG, and with them six bytes of stack. The locals and the
mask live in xdata now, the partner fields go through the scratch word,
and lacp_lag_set() records the new group of a port before it releases
the old one. One byte is left: the group number passed in.
"mirror status" printed the local mirroring_port, which is still 0 at
that point, so it named the same port whatever was configured. Print
the port from RTL837x_MIRROR_CTRL instead, which the function has
already read to tell whether mirroring is enabled. send_mirror() decodes
the register the same way for the web interface.
lacp_in() always leaves uip_len at 0, so the tcpip_output() after it
never ran. The LAG page names the active members by front-panel port
instead of a logical mask. The partner key is not part of selection;
say so under Known limits.
Listing by next-address also returned the reserved-multicast entries STP
writes, shown as port 1 with a delete button, while l2_delete() still
looks up by next-unicast and so removed the unicast entry after that
index instead. The change was there to show the LACP steering entries;
delivery by trap writes none, so it goes.
Both paths wrote the trunk registers directly and left the protocol
running on the group, so "lag show" said disabled while the partner kept
both links in one aggregate, and re-sending the LACP line did not repair
it because the same-ports shortcut returns early. Each path now releases
the group from LACP first; for a group that never ran LACP that is a
no-op.
The receive machine armed current_while from the partner's TIMEOUT bit,
so against a partner on the slow rate a port whose LACPDUs stopped with
the link still up stayed in the trunk for the long timeout, two minutes.
802.3ad 43.4.12 starts the timer from the actor's own LACP_Timeout, and
we always advertise the short one: the partner sends every second and is
let go after the short timeout.
Sandbox T23 plays a slow-rate partner that goes quiet and fails on the
code before this.
Dynamic link aggregation on top of the static LAGG, for RTL8372/8373:
receive, selection and mux state machines (coupled mux, one aggregator
per group), with "lacp show" to look inside. Each of the four trunk
groups runs its own aggregator on the candidate ports assigned with
"lag <n> lacp <ports>"; the engine starts with the first such group and
stops with the last.
LACPDUs reach the CPU through the reserved-multicast trap, so delivery
needs no L2 entry and nothing written per VLAN or per PVID. The trap
acts on the address before the VLAN lookup and before the ingress
filters, so a LACPDU also arrives on a port that admits tagged frames
only and on a port STP holds in blocking, neither of which a forwarded
one reaches. A trapped frame carries no VLAN tag,
so lacp_in() takes the offset of the PDU body from TRAP_RX_BODY(). The
action is set with trap_rma_set(), which leaves the flags sharing that
register alone, and "lacp off" puts the slow-protocols drop default
back. doc/lacp.md and doc/link_aggregation.md record what was measured,
including why the trap looked like a dead end at first.
The web UI gets a Static/LACP mode per group on the LAG page with live
status, and the startup-config validator knows the per-group syntax. A
host-side sandbox for the state machines runs in the test harness
together with the LAG hash check. It drives both forms of an incoming
LACPDU, so the body offset of a trapped one is covered as well.
The L2 table page iterated with the "next unicast" read method, so
static multicast entries such as an IGMP group were invisible. It now
iterates with "next address", skips L3 entries (their layout carries IP
addresses where a MAC is expected), and reports the lowest member of a
multicast entry's port mask, so such an entry shows up as a static
entry on that port.
A port taken out of LACP forgets its partner, so "lacp show" after
"lacp off" no longer reports one. Applying the same candidate set to a
group again, as the LAG page does on every Apply, leaves the trunk
alone instead of tearing it down for a second or two. lacp_in() checks
the frame length and ethertype before reading a LACPDU, so a runt or a
foreign frame on the slow-protocols address cannot leave stale buffer
bytes as a partner. The sandbox covers these three in T20-T22. On the
LAG page, LACP mode with no ports checked sends "lag N lacp off", and
the startup-config validator wants at least one port after "lacp".
A reserved-multicast trap goes to the port named in EXT_CPU_CTRL, which
names no port out of reset, so trapped frames were dropped at ingress.
trap_init() points it at the CPU port once at startup, and
trap_rma_set() changes only the action field of an RMA register.
A trapped frame arrives without the VLAN tag a forwarded one gets, so
TRAP_RX_BODY() gives the offset of the payload for both forms.
STP now traps its address while it runs instead of steering it with a
static L2 entry per VLAN, so turning it on no longer walks the VLAN
table and a pvid change no longer needs stp off and on. With STP off
the flooding entries are restored as before.
A trapped BPDU passes the tagged-only ingress filter that drops a
forwarded one, so the stp_setup() warning about such ports goes.
This adds a howto for figuring out LED configuration via tracing
of functions in the originam firmware image. The method also provides
information on PHY and SDS configuration, including PHY patches.
Opcode 0x48 outputs 8 dummy clocks between the address and the first data
byte. flash_configure_sio() sets DUMMYCYCLES to 0 and flash_read_security()
never raises it, so the controller captures the dummy slot as the first of
its four data bytes while the address still advances by four. The printed
output therefore shows 0xff in place of every fourth byte and skips one
content byte per transfer. flash_read_uid() already sets DUMMYCYCLES for
opcode 0x4b, which needs the same allowance.
The answer to a lookup arrives after the command has returned, so the
console of the web interface never shows it. Plain "dns" now prints the
name and the address, or whether it is still pending or failed.
Needed to increase buffer size for this to send app.js
The logical port status is what is returned in the array, for
non-sequential systems we need to map through physToLog rather
than just subtracting 1
physToLog is 1 based! not 0; also fixing up typecasts
Reimplementing using an array of objects to represent ports.
The previous approach of a bitmask of logical ports, was just too
simple on devices such as the SWGT024_V2_0_MANAGED which has
gaps in the port assignments.
timing with ticks
Fixing up the log_to_phys_port assignment
2000 is about every 10 seconds, so scaling up to once every 30ish
Removing the attempt to block lldp frames,
This does not work however the hard work has been done for us in
the #514 so we can borrow that work later on
boot_to_html outputs 1 and 0 not true and false, making the sim match
the real implementation.
lldp.json has a hanging , in the array; so overwriting it
Moving the global on/off onto the LLDP page
Building a table of LLDP status from the S ports. Later on, if
we wish to implement an lldp receiver we can display the remote
details here
Adding support to the cmd parser for the lldp commands
to each port once every 30 seconds.
* Additionally fixes the on-ward passing of LLDP frames through
the switch
The ports on the case are marked 1-8 so this should reflect that
rather than being 0-based
We can send the hostname as well, this can be dynamically allocated
Let's send the model as the system description as well
igmp_setup(), which `igmp off` also calls, enabled the lookup of IPv4
multicast in the L2 table. Groups learned while IGMP was on therefore
kept going only to the ports that had reported them, and with reports
no longer trapped nothing updated those entries until a reboot.
The lookup is now enabled in igmp_enable() and disabled in igmp_setup(),
so with IGMP off all IP multicast is flooded again.
The switch keeps a count of the dynamic L2 entries learned on every
port in L2_LRN_PORT_CONSTRT_CNT (0x53ac + 4 * port, 13 bits). `stat`
prints it in a new MACs column and /status.json carries it as "macs".
On a SWTGW218AS the count matched the L2 table port by port, static
entries are not included, and taking a port down drops it to 0.
The switch keeps flooding 224.0.0.0/24 with IGMP on, but a report for a
group in that block still made an entry for it, and the entry limited
the group to the ports that reported it. A host announcing mDNS on
224.0.0.251 thereby cut mDNS off from every other port. Such reports
are now ignored, as RFC 4541 asks.
leds_dump() printed the pin mux, the LED enables and every LED set on each
boot, from BANK2. It now lives in its own file in BANK3, where there is
room, and runs only on the new "leds" command, next to gpio and sds, so it
stays at hand for bringing up a new board without costing BANK2 or boot
output.
A matrix of the ports, one row per ingress port, showing which ports a
frame entering there may leave through. The port number of a row turns
red when the port is not fully open, and the cell for the port itself is
marked since it is always allowed. Apply sends one "isolate" command per
changed row, and Allow all ticks the whole matrix.
The switch side is a new /isolation.json, which reports each port's
allowance mask read back from the hardware. The page is an Isolation
section of the single-page UI.
494320b removed strcpy() from rtl837x_flash.c but left its declaration
in rtl837x_common.h. Since then the linker has taken strcpy from the
SDCC library instead, which expects the destination as a generic pointer
in DPL, DPH and B. The callers pass an __xdata pointer and leave B as it
is, so the copy lands wherever B happens to point. boot.c only gets away
with it because B is zero there; a call with B set, as from the ChaCha20
test in #207, writes into internal RAM and stops the 8051.
With several switches open, every tab was called "Switch". The
dashboard tab now shows the hostname, and the login page shows the
hostname seen at the last login on that address (kept in localStorage,
which is per switch), or the address itself before the first login.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The firmware page accepted any valid image, also one built for a
different board whose SFP, LED and button pins differ. Every image
carries its machine name as a NUL-terminated string, the same one the
switch reports as hw_ver; when the selected image lacks it, the page
now warns below the check result and in the confirmation dialog. The
upload stays possible on purpose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The V3.2 board has the same component placement as V3.1 and works with
its machine definition: RJ45 links at 2.5G, LEDs, SFP detection with DOM
readings at 10G, factory MAC at 0x1FC000 and the reset button. The new
target only reports "V3.2" as the machine name.
Installed from the original firmware V100.9.25 through its web
interface with the OEM installer image. Reading the flash in-circuit with
a SOIC-8 clip gave the chip ID EF FF FF, as on a V3.1 board.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #494. Storm control (#455) was merged after it and brings
three objects in code space without const: the extern declaration of
machine in rtl837x_storm.c and the name tables storm_types and
storm_names. SDCC 4.6.0 reports them as warnings 356 and 357, which
fail the build with CI=1 (--Werror). SDCC 4.5, which the CI uses, does
not report them.
The declarations now take the form used in the rest of the tree, as in
mime_strings in httpd.c. The images for KP_9000_6XHML_X2 and
KP_9000_9XHML_X_V3_1 are byte-identical before and after.
Adds a German dictionary to LANG in app.js (all 317 keys, placeholders
unchanged), the language selector entry in index.html and the login
strings in login.html. Uses Swiss spelling (ss instead of ß), which
reads correctly for all German speakers.
Checked against a mock of the httpd endpoints: every section renders
with the German strings; no layout changes compared to English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only frames to 01:00:5e:00:00:16 reached the IGMP handler, so v1/v2
reports, which go to the group address, were never seen. The handler
also took the group from the first v3 group record for every type.
Pass all IPv4 multicast to the handler, which takes the IGMP header
from the IP header length, the group from the IGMP header for v1/v2
reports and v2 leaves and from the first group record for v3, and
returns 0 for anything that is not IGMP so it goes on to the IP stack.
The message and record types have names now.
Storm control used meters 0 to 39, the same meters the ACL hands out,
so a storm limit and an acl meter could overwrite each other. It now
uses 24 + port * 4 + type, meters 24 to 63.
parse_storm() is no longer reentrant and uses plain locals, looks the
type up in a while loop and returns straight from the one-line cases.
storm_show() uses plain locals too.
The documentation says how IGMP decides between mcast and umcast, how
to get from a meter index to its limit, where the IFG bit of a meter
is, and what an ingress bandwidth limit on the same port does to the
storm limit.
The switch has no battery backed clock, only a seconds counter that starts
at zero on every boot. An SNTP client keeps UTC as the time of the last
answer plus the seconds counted since: "ntp server <name|ip>", "ntp interval
<minutes>", "ntp timezone <+hh:mm>", "ntp dst off|eu|us", "ntp on|off",
"ntp" and "time". A server given by name goes through the DNS resolver, an
answer from an unsynchronised server is ignored, and a failed attempt is
repeated after 30 seconds.
The local time adds the time zone offset and, while the European or United
States rule says so, one hour. The date is split with a shift and subtract
division and a walk over the years, so no 32-bit division from the library
ends up in the common area.
The System page gets the NTP settings and shows the local time.
A small resolver for IPv4 address records, so that clients such as NTP can
be given a host name: "dns server <ip> [<ip>]", "dns lookup <name>" and
"dns". Without a configured server it uses the one DHCP handed out. A query
goes to the servers in turn every two seconds and gives up after six tries;
answers with compressed names and CNAME records ahead of the address are
understood. A name that already is a dotted address is taken without a
query.
The resolver sits in BANK3, the command line in BANK2 next to the helpers it
uses. The System page shows and sets the servers.
The image rule no longer passes -p, so fileadder fell through to printing
html_data.h and html_data.c on stdout on every image build. The listing is
only useful when fileadder is run without an image, to see what it would
generate.
The .bin rule passes -p html_data to fileadder, which rewrites
html_data.c and html_data.h after the objects that include them have
already been compiled and linked. The next make therefore rebuilds
html_data.c, httpd.c and page_impl.c even when nothing changed, and
so does the one after that. The rule only needs to embed the files
into the image; the header comes from the html_data.c rule above.
Spotted by plaes in #358, which cannot be merged as it stands.
With this and the previous commit, a second make after a full build
compiles nothing, and the image is byte for byte the same as before.
Two things kept the same source from producing the same image.
fileadder walked the input directory with readdir(), which returns entries
in whatever order the filesystem hands them over. The same tree therefore
embedded the files in a different order on a different filesystem, and the
image differed byte for byte. scandir() with alphasort() fixes the order.
version.h was listed in .PHONY, which makes it permanently out of date and
propagates that to everything including it, so a plain build recompiled the
four largest translation units even when nothing had changed. Dropping it
from .PHONY alone would go too far the other way: with no prerequisites the
recipe would never run again and the version string would freeze at its
first value. A FORCE prerequisite keeps the recipe running every time, and
the generated header is only moved into place when its content actually
differs, so the timestamp survives an unchanged rebuild.
Measured on this tree: with FORCE the version follows GIT_VERSION and an
unchanged rebuild leaves version.h untouched; without it the header keeps
the value from the first build.
Restore the sy_igmp and sy_services_note lines to their original
indentation so they do not show up as changed in the diff; the new
session timeout strings move to their own lines. Also add the es
strings, which were missing after the merge with main.
test_port_tables and test_page_json link httpd/page_impl.c unmodified,
which now references session_timeout. Define it in support.c next to
the other firmware globals so the tests link again on current main.
The server timeout is an inactivity timeout: scan_header() refreshes
last_session_use on every authenticated request. Max-Age counts from
login instead, and the cookie is only set there, so the browser deletes
it session_timeout seconds after logging in however busy the session
is. Leave expiry to the server, which already handles it.
The five-digit itoa16_html() stays: information.json must print
timeouts from 10000 to 65535 correctly.
uip has no reassembly, so every fragment it receives is discarded and
logged. A neighbour that broadcasts fragmented traffic therefore produces a
continuous stream of "ip: fragment dropped." rather than an occasional one,
at roughly thirteen lines a second on a quiet segment, which buries every
other console message.
The message carries real information: a sudden burst of it says something
about the network. What carries none is repeating it per fragment. Report
the count once per five second window instead, which keeps the signal and
ends the flood.
The count is printed through the no_syslog primitives, as the original
message was, so the flood cannot move from the console into the log ring.
itoa() writes through write_char(), which also feeds the ring, so it gets
a no_syslog twin beside it rather than a private copy of its digits.
rtl837x_port.c prints "called" and "done" around mirroring, PVID, VLAN
and L2 setup, and dhcp.c around discover, request, start and stop. They
land on the console on every boot and config replay, and in the log
once syslog is running. The VLAN line keeps the VLAN, members and
tagged values, only without the function name in front.
On the KP_9000_6XHML_X2 this takes 359 bytes out of BANK1, 88 out of
BANK2 and 24 out of BANK3. BANK1 goes from 431 to 790 bytes free.
dhcp_addopt_request_ip() and dhcp_addopt_server_id() both end with a
copy of four bytes of our MAC to the current position, without moving
the position on. The next option always starts there, so those bytes
are written over before the packet goes out and never reach the wire.
The packet stays the same without the copy, and BANK3 is 102 bytes
smaller on the KP_9000_6XHML_X2.
ip_opt() copies four bytes whatever the option length says, so a
subnet, router, DNS, server id or broadcast option shorter than four
bytes stores the start of the next option as the address. long_opt()
does not look at the length at all and always moves on by six bytes, so
a lease, renewal or rebind option of another length puts the parser out
of step for the rest of the packet.
Both now read the length. An address or time shorter than four bytes is
skipped, a longer one is taken from its first four bytes and stepped
over whole, and a skipped time leaves the previous value alone.
Found in the code, not on the wire; no server here sends such options.
BANK3 is 47 bytes smaller on the KP_9000_6XHML_X2.
The main loop drains the NIC only after an RX interrupt, at most
RX_BUDGET frames, then clears the status and waits for the next one.
When frames are left and the buffer reaches the RX stop threshold, the
NIC takes no more frames and raises no further interrupt, so reception
stops for good.
Check the buffer after clearing the status and keep draining while it
holds data; the interrupt is re-enabled only once it is empty.
rtl8224_enable() waits 2.5 s after releasing the RTL8224 reset before
anything talks to the PHY. On an SWTGW218AS the chip id already reads
back 0x8224 at the first poll, about 265 ms after the release.
Remember when the reset was released and, once SMI is up, poll the chip
id until it answers, giving up after the same 2.5 s as before. The PHY
writes that follow read back the same as before.
The reset pulse before it goes from 0.5 s to 20 ms, twice the 10 ms the
related RTL8372 and RTL8221B need. The pulse does reset the chip: the
MDI reverse register (MMD 30, 0x0a90) still reads its power-on value
0x00f3 just before the init writes 0x00fc, as it does with 0.5 s.
Together the console banner comes 3 s sooner.
The SerDes write loop in phy_config_8224() waited on the busy bit with
no limit, so a PHY that never answers stopped the boot before the
network came up. Bound it and report a timeout.
The name branch read the fourth word without checking that the line had
one, and the tokenizer does not reset unused word slots, so "port 5 name"
took the name from wherever the previous command's fourth word had been:
"port 5 name hello" followed by "port 5 name" gave "hello" again, and
after a longer line it gave the tail of that line. The web UI never sends
the form, the console can. With no argument the name is now empty, which
also gives the console a way to remove a name.
The receive interrupt was wired up but never used: EX1 was enabled, the
handler re-enabled itself and did nothing, and the mask bit for it in the
NIC stayed clear, so idle() read the receive buffer register on every
pass to find out whether a packet had arrived, and slept until the next
tick when none had. A packet that arrived during that sleep waited for
the tick: 2.2 ms on average, 5 ms at worst, for every frame the CPU
handles.
Now the handler latches rx_irq and masks itself, and idle() sleeps
unless a receive, a link change, a console command or the tick is
pending. On rx_irq the loop drains the buffer, clears the status bit and
re-enables EX1, in that order: the NIC holds the line as long as the
status bit is set, and the bit does not clear while the buffer still
holds a frame, so a frame that lands between the last read and EX1 = 1
raises the interrupt again rather than getting lost. The other order,
clearing before draining, left the line asserted and produced an
interrupt on every pass. The mask is written after nic_setup(), which
resets it.
Measured on an RTL8373 board with the same counters in both images,
clocked by the tick counter, traffic from a host behind an LACP trunk:
```
polling interrupt
idle, register ops/s 788 79
25 pps, RTT avg/max 2.19/4.96 0.44/0.75 ms
500 pps, RTT avg/max 2.78/5.06 0.39/5.40 ms
500 pps, loss 0.08% 0.08%
```
The handler compiles to clr EX1 / setb rx_irq / reti.
The timer interrupt now raises a tick flag too, so idle() no longer works
out TICKS_DUE twice per pass, and cmd_available moves from xdata to a
bit. The four wake-up flags sit in the low bits of one byte at 0x20, so
the sleep test is a single read of that byte. tx_buf_empty deliberately
stays out: it is 1 when there is nothing to do, so including it would
keep the sleep test from ever firing.
idle() ran everything on every pass: the seconds block, the SFP pin
test, the TCP periodic timer behind a gate of its own, the STP timers
behind theirs. The timed work moves into handle_tick(), which idle()
calls once per system tick from a single gate; handle_tx() loses its
private one.
handle_sfp() ran on every pass as well, two GPIO reads per pass to learn
that nothing changed; it now runs every ten ticks. A module inserted is
still seen within 50 ms, and the read itself has been off the loop since
the wake-up wait moved to the tick.
No change in behaviour otherwise: the link, receive and console paths
stay where they were. SWTGW218AS: code +45 B, xdata +1 B, stack
unchanged.
The health checkpoints follow the work they measure: the seconds block,
SFP, TX and STP phases are taken inside handle_tick(), the link, RX and
command phases in idle().
The link-change interrupt was set up at boot (mode 0x5f84, mask 0x5f34)
but never enabled: the handler did nothing and did not clear the event in
the switch, so EX0 stayed commented out and idle() read the two link
registers on every pass instead, 400 reads a second on a quiet switch.
The handler now only latches a flag and masks itself; the register access
macros build a write in the shared SFR data bytes, so an interrupt that
touched the switch in the middle of a write from the main loop would
corrupt it. idle() clears the event register (write-one-to-clear, 0x3ff
for the ten ports), re-enables EX0 and then reads the link state, in that
order, so a change that lands during the read raises a new edge. The flag
is also set once a second from the seconds block, so a board whose INT0
line turns out not to be wired keeps detecting link changes, a second
late at worst. Boot drops the events pending from bringing the links up
before the mask is set.
Measured on an RTL8373 switch before this change: 0x5f88 held 0x3fd, nine
ports had reported a change nobody read. With the change a counter in the
loop went 2 -> 3 -> 4 on "port 5 off" / "port 5 on" and stayed at 4 over
three reads without events, so the event is delivered, cleared and
re-armed, and there is no flood.
SWTGW218AS: code +57 B, xdata 0, stack 151 -> 150 B; the flag is a bit
variable. The handler compiles to clr EX0 / setb link_irq / reti.
idle() also keeps in a bit what it saw in NIC_RX_BUFF_DATA when deciding
whether to sleep, and the first iteration of the receive loop uses it
instead of reading the register again a moment later. Between the two
only the link check and handle_sfp() run, and neither can make a queued
packet disappear. Every later iteration reads the register as before, so
the budget loop behaves as it did. This part was not measured on
hardware: one register read per pass needs a throughput run to show up.
SWTGW218AS: BANK1 and BANK2 +6 bytes each, BANK3 unchanged.
stp_timers() ran every fourth pass of the main loop and stp_clock counted
passes, which only measures time while a pass is one tick long. It never
quite was: a pass is as long as the packets it handles, and #482 now takes
up to four of them, so under an IPv4 stream a pass is about 16 ms and STP
time runs at a third of its rate, with hellos 6 s apart and a root aged out
after a minute. Since #483 the loop also stops sleeping while packets wait,
so under a flood of cheap frames passes come faster than ticks and STP time
runs fast instead, shortening the forward delay in exactly the situation a
loop produces. uip_periodic() had the same problem from the other side: it
ran once per pass, so the retransmit timer sped up with the loop.
Both now go by the tick counter the timer interrupt keeps. The STP block
catches up on ticks a long pass missed, at most eight steps at a time, and
handle_tx() runs its periodics once per tick at most. One byte of xdata
more, 48 bytes of code.
A host test drives the gate with one pass every 5 ms, every 16 ms and
every 0.6 ms for ten seconds each; the STP clock lands on 500 steps in
all three, where counting passes gave 156 and 4166. A pass of 1.3 s loses
time rather than firing 65 steps at once, and the arithmetic survives the
tick counter wrapping.
The advertisement written for a forced speed of 1G and above is 0x1001,
while auto and the forced 10M and 100M modes all carry bit 10, the PAUSE
capability. Forcing a port to 1g or faster therefore takes pause off the
link: the advertisement narrows and the far end has nothing left to
agree to.
Measured on a SWTGW218AS through phyget and physet, on a port with no
link so nothing was disturbed. Auto reads 0x15e1, port 1 1g reads
0x1001, port 1 auto reads 0x15e1 again, and writing 0x1401 by hand
sticks. That bit 10 is the pause capability follows from a second
reading: two ports advertise the same 0x15e1 from this side, and pause
is active only against the partner whose own advertisement carries the
bit.
Reported by feelfree69 in #489.
The .d files track headers, so adding or removing a flag on a tree that
was already built leaves every object that did not change for some other
reason compiled with the old flags, and the image ends up a mix of the
two.
How that shows depends on the flag. DIAG=1 fails the link on undefined
symbols, which at least stops the build. An opt-in whose hooks expand to
empty macros loses the calls instead, so the link succeeds and the
feature quietly measures nothing.
Objects now depend on a stamp holding CC_FLAGS, rewritten only when the
flags differ, so an unchanged flag set still does not force a rebuild.
Two per-port MAC status registers that nothing in the firmware reads:
0x6408 says which end is master on a BASE-T link, 0x63FC and 0x6400 say
whether pause was negotiated in each direction. port <n> show prints
both at the end.
The read sits in rtl837x_port.c rather than phy_show(), because these
are MAC registers and phy_show() is skipped for SFP ports. Master/slave
keys off machine.is_sfp[] so it stays off the fibre ports, where the
notion does not apply.
Bit N of each register is chip port N, which is not how
RTL837X_REG_LINKS_STS indexes its bytes a few lines above in the same
header.
uip-neighbor.c is the IPv6 neighbour cache. Its only caller is the neighbour
solicitation handler in uip_process(), which sits inside #if UIP_CONF_IPV6,
so on an IPv4 build the module is linked without being reachable.
Guard it on the same macro as its caller.
Worth recording separately: the module does not currently compile with
UIP_CONF_IPV6 set. uip_ipaddr_cmpx() is defined only in the non-IPv6 half of
uip.h, and uip_neighbor_add() calls it, so an IPv6 build stops with an
implicit declaration. That is true before this change as well, and is left
alone here.
On SWTGW218AS, on top of the forwarding change, the stack gains a further 12
bytes to 172 available and BANK1 drops another 714 bytes to 46080.
uip-fw.c forwards packets between several network interfaces. Nothing in
this firmware calls it: the only references to uip_fw_output() outside the
module are commented out in uip-split.c. It is compiled and linked all the
same, and its register pressure lands in direct RAM, which on SWTGW218AS is
already reported as having no spare space.
Add UIP_CONF_FORWARDING alongside the existing UIP_CONF_BROADCAST and build
the module only when it is set. A device that forwards turns it on and gets
the module unchanged; a single-interface device stops paying for it.
On SWTGW218AS the stack gains 9 bytes, from 151 available to 160, and BANK1
drops 1200 bytes, from 47994 to 46794.
The counters, the phase hooks in idle() and the "health" command are now
behind HEALTH, set from the command line with HEALTH=1. Without it the
hooks are empty macros and the command does not exist, so a default build
carries none of it: on SWTGW218AS that is 1065 bytes of bank 2, 101 of the
common segment and 28 of xdata, plus seven banked calls per main-loop pass.
It is a developer's tool. Reading the dump takes knowing the main loop, and
a user who needs it for a report can build with the flag on, which is how
the kernel treats its lock and latency instrumentation as well.
The tcp slot dump printed the slot index behind `if (UIP_CONNS > 1)`, and
with one connection configured that is a constant-false condition around
live code: sdcc reported two "unreachable code" warnings for the body and
one "conditional flow changed by optimizer" for the loop that could only
run once.
Print the single slot directly and state the assumption as a build-time
check, so a build that raises UIP_CONNS fails loudly here instead of
quietly labelling every line as slot zero. Builds warning-free again.
What each line of the dump means, how two dumps give rates, and the
two things worth knowing before trusting the numbers: the command
inflates its own cmd phase counters by the cost of printing, and the
untouched figure is a high-water mark, not a live level.
Every probe of a misbehaving switch so far measured it from the
outside. This puts the witnesses inside.
Each idle() pass increments a loop counter and stamps a checkpoint
after every phase: link polling, SFP, RX, TX, STP and the command
dispatch. A phase spanning two or more system ticks counts as slow and
its worst time is kept, in 5 ms units. handle_rx counts frames, so the
dump separates a flooded CPU from a stalled loop.
At boot the free stack area is painted, preserving the interrupt state
around the painting, and the dump reports how much of the pattern
survives next to the live stack pointer. The TCP connection table is
printed whole: state, timers, retransmissions, ports and peer, next to
the httpd transfer state, so a permanently occupied slot names its
occupant. With more than one slot configured every slot is listed.
Everything is raw hex; two dumps a known time apart give the rates.
The implementation sits in cmd_parser next to the other command
handlers and needs no storage beyond its counters.
The row sits between Hardware and Flash. The firmware sends the formatted string
rather than the raw register, since the System table prints its values as they
arrive.
Costs 186 bytes in BANK1.
The chip has a pair of thermal meters and nothing was reading them. This adds a
`temp` command that prints the current reading next to the one taken at power-on.
The result register at 0x340 splits a 19 bit value across two fields: bits 18:3
live in the low 16 bits, bits 2:0 in bits 18:16, with the sign at bit 18, degrees
in 17:10 and a fraction below. The nice part of that layout is that the degrees
fall out of a shift of the low half, so the conversion stays 16 bit.
The meter comes out of reset latched: the register keeps whatever was measured
when the board powered up and then never moves. Four samples 40 s apart came back
identical down to the raw ADC, and 0x340 matched the power-on register at 0x350.
Clearing bit 2 of the control register at 0x338 during init is what makes it live.
Measured on an SWTGW218AS: 58.7 to 59.5 C while running, with the power-on
register still showing the 49.2 C it started from. For what it is worth, the
scale lines up three ways: the threshold register reads -40 and +125, the slope
from warm-up works out at 4450 LSB/C, and the slope between the two meters at
4388 LSB/C.
We only read the meter that is up after boot. The other sits in reset with no
coefficients, so it would just repeat its power-on value.
Costs 231 bytes in BANK2.
The session cookie carried neither Expires nor Max-Age, so browsers that
clean up session cookies aggressively (LibreWolf, for one) may drop it
while the session is still valid. Send Max-Age=session_timeout so the
declared lifetime matches the timeout already enforced by the firmware.
itoa16_html() only converted four digits, so extend it to five to print
timeouts up to 65535. That also fixes information.json, which used the
same helper and reported wrong values from 10000 s up.
Adds a numeric input next to the syslog settings to read and set the
session timeout without knowing the command. The current value is
reported in information.json, so the field also shows the default the
firmware was built with.
Costs 86 bytes of code (BANK1) and nothing in RAM.
The session timeout was a compile-time constant (200 s), so changing it
meant rebuilding the firmware. Make it a variable that keeps the same
default and add a "session <seconds>" command, so it can be set from
the console or from the startup configuration like the other settings.
Measured on KP_9000_9XHML_X_V3_1: ROM +180 bytes (BANK1 +17, BANK2
+161) and xdata +2 bytes; the internal RAM map is unchanged.
2026-09-15 00:24:45 +02:00
448 changed files with 21959 additions and 5053 deletions
cd main && make CI=1 MACHINE="KEEPLINK_KP_9000_6XHML_X2" && cp -r output/KEEPLINK_KP_9000_6XHML_X2/rtlplayground.map output/KEEPLINK_KP_9000_6XHML_X2/rtlplayground.mem output/KEEPLINK_KP_9000_6XHML_X2/html_min /tmp/main/
A device is supported by adding one board header `.h` and board definition `.c` file
under `machine/`.
the build selects the definition by name, and CI compiles every file it finds.
To compile for your added device, do:
```
make MACHINE=<NAME>
```
## What a new device consists of
| File | Purpose |
|------|---------|
| `machine/<NAME>.c` | the `struct machine machine` board definition |
| `machine/<NAME>.h` | `MACHINE_MODEL` (the model string shown in the UI and the CLI) and `MACHINE_CHIPSET` (`RTL8372` or `RTL8373`) |
| `machine/<NAME>_init.c` | optional `machine_custom_init()` boot hook, for registers the machine definition cannot express |
| `doc/devices/<name>.md` + `doc/supported_devices.md` | (only if the board is verified and you want to support it publicly) user-facing documentation |
`<NAME>` is the machine symbol without its `MACHINE_` prefix, so `MACHINE` is
passed exactly as spelled on the command line: `make MACHINE=MY_BOARD_1_0`
needs `machine/MY_BOARD_1_0.c` and `machine/MY_BOARD_1_0.h`.
## Step 1: the board header
`machine/<NAME>.h` carries the product string and the SoC family. Use the
existing files as the template:
```c
#ifndef _MACHINE_MY_BOARD_1_0_H_
#define _MACHINE_MY_BOARD_1_0_H_
#define MACHINE_MODEL "Brand Model V1.0"
#define MACHINE_CHIPSET RTL8373
#endif
```
`MACHINE_CHIPSET` is `RTL8373` or `RTL8372`; a build without it will fail. It selects the default
logical port range: `MACHINE_MIN_PORT` is `0` for RTL8373 and `3` for RTL8372, while
`MACHINE_MAX_PORT` is `8`, the highest logical port index. A board with a different range defines
`MACHINE_MIN_PORT` and/or `MACHINE_MAX_PORT` in its header to override the
defaults.
The device product string is what the CLI or the Web-Interface will print to identify the device; `machine.h` includes it by name.
> [!NOTE]
> `machine_name` is a `char[30]` and `MACHINE_MODEL` is copied into it at
> compile time. A longer model string still builds, but SDCC emits
> `warning 147: excess elements in array of chars initializer` and the name is
> truncated in the UI. Keep it to 29 characters or less.
## Step 2: the board definition
`machine/<NAME>.c` starts with the shared prelude and then defines the single
`machine` object. Copy the closest existing board and adjust it — do not start
from a blank file, the field meanings are easier to see in a real example.
The best choice is usually a device that has the same number and type of Ethernet
`MACHINE_COMMON_SETTING` (from `machine_defs.h`) initializes `machine_name` and
`isRTL8373` from the header's `MACHINE_MODEL` and `MACHINE_CHIPSET`. The logical
port range comes from `MACHINE_MIN_PORT` and `MACHINE_MAX_PORT` in `machine.h`.
Keep it as the first initializer and list the board-specific fields after it.
### Fields
| Field | Meaning |
|-------|---------|
| `MACHINE_COMMON_SETTING` | expands to `machine_name` (`MACHINE_MODEL`) and `isRTL8373` (`1` for `RTL8373`, `0` for `RTL8372`); both are derived from the header's `MACHINE_CHIPSET` (see Step 1) |
| `n_sfp` | number of SFP+ cages |
| `n_10g` | SerDes 10G capability used by the RTL837xN init: `0` none, `1` one, `2` two 10G-capable SerDes |
| `log_to_phys_port[9]` | logical port (0-based index) → physical front-panel port number |
| `phys_to_log_port[9]` | the inverse, physical port 1 is index 0, unused slots are 0 |
| `is_sfp[9]` | `0` for copper, `1` for left slot and `2` for right slot |
| `sfp_port[2]` | per-cage wiring, `sfp_port[0]` is the leftmost cage |
| `reset_pin` | GPIO of the reset button, `GPIO_NA` if unpopulated |
| `high_leds` | mux/enable for the LED pins 27–29 (`LED_27`, `LED_28_SYS`, `LED_29`) |
| `port_led_set[9]` | which LED set (0–3) each logical port uses |
| `led_sets[4][4]` | Each port can have up to 4 GPIO output pins assigned. 4 sets of LEDs with 4 LED-conditions per set; the conditions define which GPIO pins become active for a port given a port link-state. Which LEDs are then enabled depends on the wiring of the pins to up to 3 LEDs for that port, which can make a logical combination of the 4 LED output-pins. |
| `led_mux_custom` / `led_mux[28]` | `1` plus the 28 mux values when the stock LED muxing does not fit the board |
| `mac_flash_offset` | flash offset of the factory MAC address, omit or `0` if the board does not store one |
Each `struct sfp_port` entry holds `pin_detect`, `pin_los`, `pin_tx_disable`,
`sds` (which SerDes drives the cage) and `i2c` (built with `I2CBUS(sda, scl)`,
or `GPIO_NA`-based values where a cage has no such pin).
`I2CBUS(...)` only accepts the pin pairs listed in `machine_defs.h`. A wrong
pair is caught at compile time: SDCC reports
`error 176: sizeof applied to an incomplete type` pointing at the offending
`.i2c` line, because the macro deliberately turns an invalid bus number into an
array of size 0.
### Reusing an existing definition
If the new board is electrically identical to one that already exists — same
port mapping, same SFP wiring, same LEDs — do not copy the file. Make the new
`.c` a symlink and keep only a distinct `.h`, so the two boards cannot drift
apart:
```
ln -s KP_9000_6XHML_X2.c machine/MY_BOARD_1_0.c
```
The new board still needs `machine/MY_BOARD_1_0.h` (pick the version number on
the PCB) with its own `MACHINE_MODEL` and `MACHINE_CHIPSET`: `MACHINE_MODEL` is
what distinguishes the products, and `MACHINE_CHIPSET` must match the SoC of
- All 4 2.5GBASE-T RJ45 ports work at 10/100/1000/2500 Mbps
- All 4 2.5GBASE-T RJ45 ports work at 10/100/1000/2500 Mbps
- The SFP+ port supports 1G, 2.5G and 10G modules
- The SFP+ port supports 1G, 2.5G and 10G modules
- LEDs work with the same indiciations as the OEM firmware (use KP_9000_6XHML_X2_V1_1 in machine.h if building yourself or the corresponding pre-compiled binary)
- LEDs work with the same indiciations as the OEM firmware (build with `make MACHINE=MOKERLINK_2G040210GSM` if building yourself, or use the corresponding pre-compiled binary)
- untested due to missing Hardware: SFP+ ports equipped with 1G or 2.5G SFPs.
- untested due to missing Hardware: SFP+ ports equipped with 1G or 2.5G SFPs.
The `POE-2G080110GS` is an unmanaged 8x2.5G RJ45 and 1xSFP+ PoE switch sold by Mokerlink. There is also a `POE-2G080110GSM` managed version of the switch which may be similar but this has not been verified on actual hardware.
The `POE-2G080110GS` is an unmanaged 8x2.5G RJ45 and 1xSFP+ PoE switch sold by Mokerlink. There is also a `POE-2G080110GSM` managed version of the switch which may be similar but this has not been verified on actual hardware.
The`KP-9000-9XHP-X` is the KeepLink rebrand for same product.
> This device operates on mains voltage, which can be a dangerous or potentially lethal hazard. Even after the device is unplugged, high-voltage capacitors can remain charged afterward and may still shock you. Do not attempt to open/disassemble a mains powered device unless you understand how to do so safely. Also, opening the device may void your warranty.
> This device operates on mains voltage, which can be a dangerous or potentially lethal hazard. Even after the device is unplugged, high-voltage capacitors can remain charged afterward and may still shock you. Do not attempt to open/disassemble a mains powered device unless you understand how to do so safely. Also, opening the device may void your warranty.
## Notes
## Notes
The stock firmware blinks both the 2.5G and 1G LEDs on activity, despite the front of the device indicating that each LED should be independent. The `POE_2G080110GS` machine config behaves as the front of the device indicates (and what seems more logical) -- each port LED turns on and blinks independently.
The stock firmware blinks both the 2.5G and 1G LEDs on activity, despite the front of the device indicating that each LED should be independent. The `MOKERLINK_POE_2G080110GS` machine config behaves as the front of the device indicates (and what seems more logical) -- each port LED turns on and blinks independently.
**Note:** These devices are **RTL8372N-based** with **LAN ports mounted upside-down on the PCB**, requiring a custom configuration. The `MACHINE_ONT_S207CW_62TS_SE` definition uses a **custom configuration** with:
*Photos courtesy of [andreas5232](https://github.com/andreas5232) from [emesix/ONT-S207CW-62TS-SE#1](https://github.com/emesix/ONT-S207CW-62TS-SE/issues/1)*
## Hardware Specification
| Feature | Value |
|---|---|
| **CPU** | RTL8372N |
| **Flash** | GD25Q128E (16MB) |
| **RAM** | Integrated in RTL8372N |
| **RJ45 Ports** | 4x 2.5GBase-T (Ports 1-4) |
| **SFP+ Ports** | 2x 10G (Ports 5-6) |
| **Console** | UART (115200 baud for RTLPlayground, 9600 baud for stock) |
**Note:** Ports 1-2 (RJ45) have reversed LED wiring and use LED-Set 2. SFP Ports 5-6 use LED-Set 3 to account for wiring differences.
## Flashing Instructions
See the [README.md](../../README.md) for general flashing instructions.
For compilation, use command `make MACHINE=ONT_S207CW_62TS_SE` to specify the machine.
If you are using managed version of the switch, you can then also compile the installer and flash using GUI - run command `cd installer` and finally `make`.
- **LED Wiring:** RJ45 Ports 1-2 and SFP Port 6 have reversed LED pins (orange/green swapped)
- **LED Sets:** Set 0 for RJ45 Ports 3-4, Set 1 unused, Set 2 for RJ45 Ports 1-2, Set 3 for SFP Ports 5-6
## Notes
- ONT-S207CW-62TS-SE, Binardat 2G06-04210GSM (managed), and Binardat 2G06-04210GS (unmanaged) are **RTL8372N-based** with **LAN ports mounted upside-down on the PCB**
- Web UI shows correct physical port order (1-2-3-4 for RJ45, 5-6 for SFP)
- Loader Mode always remains accessible at `192.168.10.247` for recovery
- Serial console: 9600 baud (stock) → 115200 baud (RTLPlayground)
## References
- [GitHub Issue: emesix/ONT-S207CW-62TS-SE#1](https://github.com/emesix/ONT-S207CW-62TS-SE/issues/1) - Original photos and discussion
- [PR: logicog/RTLPlayground#465](https://github.com/logicog/RTLPlayground/pull/465) - Feedback and improvements
This is the first revision of the PCB-K0402WS series of boards; it's very similar to 2.0 in terms of layout/wiring.
The board does have footprints for a VLAN modeswitch at the front and a second flash chip on the bottom of the PCB, but these have been seen to ship unpopulated.
### Brands
* YuanLey YS25-0402 <sup><sub>Note: there are [multiple revisions](PCB-K0402W-V3.0.md) of the YS25-0402. This first revision lacks the "Vlan" switch cutout on the front of the enclosure.</sub></sup>
### Programming
Use a SOIC-8 clamp to the flash chip (U1 on silkscreen) at the top of the board, leaving it otherwise unpowered.
The board has been seen to ship with a single [BoyaMicro BY25Q16BSSIG](https://www.lcsc.com/datasheet/C382744.pdf) 16Mbit (2MiB) chip, though exact BOM may vary.
* YuanLey YS25-0402 <sup><sub>Note: there are [multiple revisions](PCB-K0402W-V1.0.md) of the YS25-0402. This is the newer revision that has a "Vlan" switch on the front.</sub></sup>
Using SPI clamp in-board is the only method for initial installation.
Using SPI clamp in-board is the only method for initial installation.
@@ -32,7 +37,7 @@ The switch actually controls the HOLD line of each flash chip, and toggling the
If the programming clip keeps HOLD not connected, the flashing will commence on whatever the switch selected, regardless on which chip was clipped.
If the programming clip keeps HOLD not connected, the flashing will commence on whatever the switch selected, regardless on which chip was clipped.
For the initial flash (at least with flashrom), the bin file produced by the build is much smaller than the flash chip, it is suggested to pad the file to keep flashrom happy: `truncate -s 2097152 rtlplayground-*-PCB_K0402WS_V3.bin`. Note: do not then proceed to use this resulting padded file for the web flashing (as it bricks the device), use the original unpadded .bin.
For the initial flash (at least with flashrom), the bin file produced by the build is much smaller than the flash chip, it is suggested to pad the file to keep flashrom happy: `truncate -s 2097152 rtlplayground-*-HISOURCE_HI_K0402WS_PCB_K0402WS_V3_0.bin`. Note: do not then proceed to use this resulting padded file for the web flashing (as it bricks the device), use the original unpadded .bin.
### What works (expected from label + similar devices)
### What works (expected from label + similar devices)
@@ -43,15 +48,15 @@ For the initial flash (at least with flashrom), the bin file produced by the bui
### PCB overview
### PCB overview
**Board markings**
**Board markings**
- Top silkscreen: PCB-KO4022W-V3.0 / DIP-KO4022WS-V3.0
- Top silkscreen: PCB-K0402W-V3.0 / DIP-K0402WS-V3.0
@@ -77,3 +82,15 @@ Voltage is made by a `Techcode TD1720` .
Voltage is created by chip marked as `Techcode TD1720`.
Voltage is created by chip marked as `Techcode TD1720`.
**There seems to have been a miscalculation when choosing the inductor and the device is ~25% more efficient with an 5V power supply.**
**There seems to have been a miscalculation when choosing the inductor and the device is ~25% more efficient with an 5V power supply.**
## Reset Mod
A reset button can be easily added by soldering a push button from TP131 (near the Spi ROM) to ground via a 1KOhm resistor.
**PS:** `The reset button GPIO must be defined in the machine.c file before building the image, it must be selected the GPIO30_ACL_BIT3_EN instead of GPIO_NA under the PCB-K0402W-V3.0 configuration.`
The four-pin through-hole serial header can be seen to the right of the CPU heatsink in the photo above. It is labelled J1, with pin 1 (Indicated by a triangle on the silkscren) at the bottom. the pinout is also on the silkscreen:
| Pin | Label |
|-----|--------|
| 1 | TX |
| 2 | RX |
| 3 | GND |
| 4 | 3V3 |
Adding a header is a few moments work with a soldering iron. Only pins 1-3 are required for serial port use, and the stock firmware outputs at 57600bps at boot.
@@ -68,7 +69,7 @@ Port 5 RJ45 is interfaced through a RTL8221B IC.
This connector goes to U4 `I2C EEPROM` and U10 `SPI FLASH` (mappings identical to SWTG024AS).
This connector goes to U4 `I2C EEPROM` and U10 `SPI FLASH` (mappings identical to SWTG024AS).
For detailed Slave Interface functionality and protocol information, see [T3 documentation in SWTG024AS.md](SWTG024AS.md#t3-slave-interface).
For detailed Slave Interface functionality and protocol information, see [T3 documentation in SWTG024AS.md](PCB-SWTG024AS-V2.0-17452.md#t3-slave-interface).
|`T3` pin|what|Signal|
|`T3` pin|what|Signal|
|---|---|---|
|---|---|---|
@@ -108,7 +109,7 @@ For detailed Slave Interface functionality and protocol information, see [T3 doc
#### Notes
#### Notes
* 1 pin is square shaped.
* 1 pin is square shaped.
* Mapping unverified but assumed the same as [LIANGUO SWTG024AS](SWTG024AS.md#t8).
* Mapping unverified but assumed the same as [LIANGUO SWTG024AS](PCB-SWTG024AS-V2.0-17452.md#t8).
## Reset Circuit
## Reset Circuit
| Function | GPIO |
| Function | GPIO |
@@ -120,7 +121,7 @@ For detailed Slave Interface functionality and protocol information, see [T3 doc
## GPIO
## GPIO
Note: T3/U4/U10-related signal annotations below are copied from [LIANGUO SWTG024AS T3 section](SWTG024AS.md#t3-slave-interface) as well as T8 port from [LIANGUO SWTG024AS T8 section](SWTG024AS.md#t8). They should be treated as assumed identical for ZX-SWTGW215AS as it has not been 100% confirmed true at the moment.
Note: T3/U4/U10-related signal annotations below are copied from [LIANGUO SWTG024AS T3 section](PCB-SWTG024AS-V2.0-17452.md#t3-slave-interface) as well as T8 port from [LIANGUO SWTG024AS T8 section](PCB-SWTG024AS-V2.0-17452.md#t8). They should be treated as assumed identical for ZX-SWTGW215AS as it has not been 100% confirmed true at the moment.
| 00100000 | GPIO20 | PORT4 LED GREEN | | | GPIO52 | | |
| 00100000 | GPIO20 | PORT4 LED GREEN | | | GPIO52 | | |
@@ -176,7 +177,7 @@ Note: T3/U4/U10-related signal annotations below are copied from [LIANGUO SWTG02
## Notes
## Notes
While [SWTG024AS.md](SWTG024AS.md) can be used as a general reference for hardware concepts and interface specifications, this device should not be assumed to be identical beside the difference implicitely highlighted below. Not all information has been validated for compatibility with the SWTG215AS. Consult the SWTG024AS documentation with caution and verify any critical details against this device's.
While [SWTG024AS.md](PCB-SWTG024AS-V2.0-17452.md) can be used as a general reference for hardware concepts and interface specifications, this device should not be assumed to be identical beside the difference implicitely highlighted below. Not all information has been validated for compatibility with the SWTG215AS. Consult the SWTG024AS documentation with caution and verify any critical details against this device's.
The 8 copper ports support PoE powered by a DC 52V 2.5A input. Power management is handled by two HS104PTI chipsets over I2C (GPIO47_I2C_SDA0, GPIO46_I2C_SCL0):
* Chip 1 (I2C address 0x1A): 2.5G Ports 1–4
* Chip 2 (I2C address 0x2A): 2.5G Ports 5–8
Note: The current implementation does not yet support PoE management. Consequently, PoE currently operates in standalone mode without software control.
Loaded 100 of 448 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.