ENNS0 RX-forward root cause: why a received Ethernet frame never becomes an XMSG message¶
Date: 2026-07-24 Subsystem: ND Ethernet II (PCB 3094 / ND-110063), COSMOS network server ENNS0 Scope: the RECEIVE-forward path - LANCE RX -> 68000 firmware RCVCOMPLETE -> ND-100 / XMSG
Convention: [V] = byte-verified from the firmware disassembly / emulator source / a captured
frame. [I] = inferred (strongly supported, not fully traced). Firmware addresses are hex,
MC68000 big-endian, base 0x0, binary
E:\Dev\Repos\Ronny\RetroCore\Emulated.Tests\NDBusDevices\TestData\encos-ser-all-banks-68k.bin.
CORRECTION (2026-07-25) - this doc's original verdict is SUPERSEDED. The RX discard WAS a real emulator bug, and it was on the TRANSMIT side, not an unmet firmware precondition. The sender's Am7990 LANCE appended a 4-byte FCS to the frame handed to the network backend, while the receiver's LANCE RX assumes backend frames are FCS-less and adds 4 to MCNT - so the FCS was DOUBLE-COUNTED and every COSMOS frame arrived 4 bytes too long. That failed the firmware's 802.3 length-consistency check gate 2 (RCVCOMPLETE 0x5D18: received-data-length must equal the 802.3 length field), NOT the empty-pool gate 3 this doc originally blamed. Sections 1-2 describe the discard MECHANISM correctly, but the CAUSE is the FCS double-count and the "not a bug in the emulator" conclusion is wrong. Fixed in
Emulated.HW\AMD\LANCE\Am7990\Am2990Lance.cs(hand the backend the FCS-less frame; keep FCS on the internal-loopback path). Reproduced and validated offline byEmulated.Tests\ND100\Nd100TwoNodeEthernetHarnessTests.cs:conn-to d102now returns CONNECTION ESTABLISHED (was "Remote system not available"). Consolidated write-up:SINTRAN\XMSG\DOC\COSMOS-RE\COSMOS-MULTI-NODE-NETWORK-2026-07-25.md.
1. Verdict (one line)¶
The received frame is DISCARDED inside the 68000 firmware RX-completion handler RCVCOMPLETE (0x5C42), not lost between the LANCE and the ND-100. On any discard the firmware clears its "message built" pointer, takes the branch at 0x601A that only re-appends the RX buffer to the LANCE ring and yields - it never calls the deliver routine (0x134E6) and therefore never rings SCIP/INT12. That is exactly the observed symptom: RX ring pointers updated, then straight on to transmit, no SCIP, no superkick.
For a correctly-addressed IEEE-802.3 COSMOS frame (the real connect-to case) the specific discard is gate 3: the card's free XMSG receive-buffer pool at DRAM 0x188C6 is empty, so RCVCOMPLETE has nowhere to put the inbound message and drops it.
This is NOT a bug in the emulator's LANCE RX, descriptor write-back, RINT delivery, or the 68K interrupt path - those are all correct and were verified. The frame is dropped by CORRECT firmware logic because a precondition is not met.
2. The RX-completion handler RCVCOMPLETE @ 0x5C42 - full decode [V]¶
Entry is reached from the LANCE level-2 RX interrupt. Register A6 is a coroutine frame
pointer; the important local is $18(A6) = the message buffer this handler builds (0 =
"no message built" = the frame is being dropped).
5C54 lea $18000,A0 ; RX software ring header (FREE@+0, PROD@+2, CONS@+4)
5C5A move.w $4(A0),D0 ; D0 = CONS index
5C62 lea $8(A0,D0*8),A1 ; A1 = &descriptor[CONS] (RMDs start at 0x18008)
5C6E move.w $2(A2),D1 ; RMD1 (flags)
5C72 btst #15,D1 ; OWN ?
5C76 bne $604C ; OWN set (chip still owns) -> yield to scheduler
5C7A move.w $4(A2),D2 / tst.l
5C82 beq $604C ; zero byte-count -> yield
... copy dst-MAC (6 bytes) to $30(A6); length d5 = (MCNT & 0x0FFF) - 4 (FCS stripped)
5CC8 btst #14,D6 / bne $5EFC ; RMD1.ERR -> error-counter path -> discard
Frame-format classification -> sets $36(A6) [V]¶
5CD0 cmp.w #60,D5 / bcs $5CDE ; runt (<60) -> $36 handling below
5CDA cmp.w D5,D7(=1514)/bcc 5D0A; in [60..1514] -> length/type discrimination
5D0A tst.w $1888A / beq $5D58 ; if card "802.3 mode" word == 0 -> treat as 802.3 ($36=1)
5D14 cmp.w D5,#60 / bcc $5D58 ; frame <= 60 bytes -> $36=1 (accept as 802.3)
5D18 sub.w #14,D5 ; else D5 = payload length
5D1C move.w $C(A4),D1 ; D1 = frame bytes 12-13 (the 802.3 length / DIX type field)
5D20 cmp.w D5,D1 / bne $5D2C ; length field == payload length ?
5D24 move.w #1,$36(A6) ; YES -> 802.3 length frame ($36 = 1)
5D2C clr.w $36(A6) ; NO -> Ethernet-II TYPE frame ($36 = 0)
$36(A6) = 1 only for IEEE-802.3 length-encapsulated frames. A DIX Ethernet-II frame
whose bytes 12-13 are an EtherType (e.g. 0x0800 IP, 0x0806 ARP) that is larger than the
payload length gets $36 = 0.
"Is it for us?" -> sets $38(A6) [V]¶
5D5E test dst-MAC bit0 (multicast/broadcast?) -> $5DC8 else unicast
unicast: compare 6 bytes at $30(A6) (frame dst) vs $1885E (card MAC)
match -> $38=1 ; mismatch (and not promiscuous $18888) -> $38=0
mcast/bcast: bsr $542C multicast filter -> $38 from result
The three discard gates -> all clear $18(A6) and jump to 0x5EF4 [V]¶
5DF2 tst.w $38(A6) / beq $5EF4 ; GATE 1: not addressed to us -> DISCARD
5DFA tst.w $36(A6) / beq $5EF4 ; GATE 2: not an 802.3 length frame -> DISCARD
5E02 lea $188C6,A0 / tst.w (A0) / beq $5EF4 ; GATE 3a: rx-buffer pool count == 0
5E0E move.l $4(A0),D0 / tst.l / beq $5ECA ; GATE 3b: rx-buffer free-list head == 0
; -> DISCARD (no free receive buffer)
5E1C..5EC8 ACCEPT: pop a free buffer, tag it (ori #$4C00 at msg+0x0A = "received message"),
copy the frame in, store the buffer pointer in $18(A6).
5EF4 clr.l $18(A6) ; DISCARD marker: "no message built"
5EF8 bra $5FF2 ; common tail
The decisive branch - deliver vs silent drop @ 0x601A [V]¶
The common tail advances the RX ring (this is the "updates RX ring pointers" the log shows) and then:
5FFC addq.w #1,(A0) ; FREE++ (ring header 0x18000)
6008 CONS = (CONS+1) mod 128 ; consumer advanced (0x18004)
601A tst.l $18(A6) / beq $6034 ; was a message actually built ?
6020 lea $188D6,A0 ; YES ->
6024 jsr $134E6 ; ENQUEUE to the ND-100 ready-ring ($188D6) + SIGNAL.
6032 bra $6048 -> $5C54 ; (this call is the ONLY route to a SCIP / superkick)
6034 bsr $5B60 (RCVRINGAPP) ; NO -> just re-append the RX buffer to the LANCE ring
6046 jmp (A5) ; and YIELD. NO $134E6, NO SCIP, NO INT12.
So: deliver-to-ND-100 (and the SCIP that wakes ENNS0/PDRIV/XRTEN) happens only when
RCVCOMPLETE successfully built a message buffer ($18(A6) != 0), i.e. only when the frame
passed all three gates. Every discard takes the 0x6034 branch: re-append the buffer, yield -
which is precisely the reported trace (ring pointers move, then the scheduler goes on to
transmit the next routing frame, with no notification to the ND-100).
CORRECTION (2026-07-25, Ghidra RE pass) - two structural details above are wrong. 1. Ring advance is NOT part of the discard branch. Accept and discard both reach the shared tail
0x5ff2->LanceRxDescriptorClear(0x553c) ->0x5ffcFREE++ / CONS=(CONS+1) mod 128. The two paths diverge only at0x601a. So "advance the ring" is common, not discard-specific; what the discard branch uniquely does is re-arm the buffer via0x5b60and yield. 2.0x5ffcis NOT dead code, despite Ghidra reporting no xrefs to it. Execution RESUMES at the instruction following thejmp (A5). The same applies at0x5dd0(after the group-filter call) where0x5dd2clearly consumes the returned D0.CORRECTION (2026-07-26) - the REASON given here was wrong. This is not a "coroutine yield". It is the PLANC-MC two-entry return convention, now verified byte-level across the image: - Every compiled routine ends
movea.l (SP)+,A6 ; movea.l (SP)+,A2 ; jmp (0x2,A2)- the second pop takes the RETURN ADDRESS into A2, and the jump goes to retaddr + 2, skipping the 2-byte4E D5 jmp (A5). Byte search finds 400+ of these epilogues and zero routines that return to +0. -jmp (A5)is therefore the error-unwind trampoline, reached only when the runtime returns to +0. A5 holds#XRET(0x135A8) normally, or#ERET(0x13596) at an outermost frame.#XRETis the only +0 returner in the image; it pops one frame, re-arms A5 to itself, and lands on the caller'sjmp (A5)- a one-frame-at-a-time stack unwind. - A5 is loaded in exactly 5 places image-wide: the three process roots (POMNPROCES0x7BBA,AUTO_START0x7E26,POCSPROCES0xE398) set it to#XRET;#XRETand#ERETre-arm it.Note this firmware DOES also have a genuine cooperative-coroutine scheduler (the PIOCOS task system with STOP /
movemsave-restore, seeENNS0-Startup-RE-2026-07-23\FIRST-SUPERKICK-BRIDGE-DECODE-2026-07-23.md). The error above was attributingbsr; jmp (A5)to that scheduler. The two are unrelated.Also note the gate-2 mechanism is better understood now:
0x1888ais a "length field present" switch, and clearing it disables the gate-2 length test entirely, which is what makes Ethernet II traffic possible. Full decode: ETHERNET-II-FEASIBILITY-AND-MODE-WORD-RE-2026-07-25.md.
3. Mapping the decode onto the observed symptom¶
| Observed in the controller log | Explanation (from the decode) |
|---|---|
LANCE RX accepted ... rmd1=0x0303 (OWN cleared, RINT raised) |
LANCE RX + descriptor write-back correct; firmware RX ISR entered at 0x5C42. |
| firmware writes RX_FREE/CONS/PROD then moves on | the common tail (0x5FFC-0x6016) FREE++/CONS++ - runs on BOTH accept and discard. |
| RX_PROD == RX_CONS (both 2) | ring fully returned to the LANCE after consuming - normal post-consume state, not the bug. |
| no SCIP, no INT12, no superkick after RX | the frame hit a discard gate -> $18(A6)=0 -> 0x6034 branch -> no 0x134E6 -> no SCIP. |
| "goes straight on to TRANSMIT its next routing frame" | 0x6034 jmp (A5) yields to the scheduler, whose next work item is the pending TX. |
4. Which gate fires for the real connect-to frame¶
The boot-harness RX probe injects a real captured COSMOS connect-to frame (node 102 ->
node 100), Nd100SintranEthernetIIBootHarnessTests.cs:476:
dst = 08:00:26:64:00:00 (= this card's MAC, node 100)
src = 08:00:26:66:00:00 (node 102)
bytes 12-13 = 00 0E (= 14 : an IEEE-802.3 LENGTH field, NOT a DIX EtherType)
payload = a8 a8 03 0b 02 0f 00 00 ... (XMSG/COSMOS LLC data), padded to 60 bytes
Walking the gates for this 60-byte frame:
- Gate 1 ($38, MAC): dst == card MAC 08:00:26:64:00:00 -> $38 = 1. PASS. [V]
- Gate 2 ($36, 802.3): frame length <= 60 -> the short-frame branch (0x5D14 bcc $5D58)
sets $36 = 1 unconditionally. PASS. [V] (Independently, bytes 12-13 = 0x000E is a
genuine 802.3 length, confirming COSMOS uses IEEE-802.3, not DIX Ethernet-II framing -
despite the product name "Ethernet II".)
- Gate 3 ($188C6, free rx buffer): this is the only remaining gate -> the discard is
here: the card has no free XMSG receive buffer. [V that it is the only reachable gate;
[I] that the pool is actually empty at runtime - confirm with the instrumentation in section 6.]
Corollary, already seen: the earlier [RX-INJECT] probe used a broadcast ARP frame
(EtherType 0x0806). That frame is > 60 bytes with bytes 12-13 = 0x0806 != payload length ->
Gate 2 discards it ($36 = 0). The memory note "wrong EtherType, not COSMOS -> no
superkick" is exactly gate 2. COSMOS traffic must be 802.3-framed to survive gate 2.
Who fills the 0x188C6 pool¶
0x188C6 is a buffer-pool control block: +0 = free count word, +4 = RX free-buffer
list head, +8 = TX free-buffer list head (the TX side is popped by XMTRINGAPP at 0x6216).
[V] The free receive buffers are XMSG receive credits; the card can only deliver an inbound
message when the ND-100 / XMSG side has posted receive buffers into this pool. If ENNS0's
receive-buffer posting never runs (historically ENNS0 hangs in a device INPUT wait on LU
2240B and force-reserves the device - see ROOT-CAUSE-ENNS0-HANGS-INPUT-WAIT-LU2240B-2026-07-23.md),
the pool stays empty and every inbound frame is discarded at gate 3 with no notification.
Whether the pool is empty at the moment a frame arrives is a runtime fact that the section-6
instrumentation resolves definitively on the next run.
5. Is there an emulator bug to fix here?¶
Statically, no bug was found in the emulator's RX-forward path:
- Am2990Lance.ReceivePacket / ReceiveComplete
(E:\Dev\Repos\Ronny\RetroCore\Emulated.HW\AMD\LANCE\Am7990\Am2990Lance.cs:1359,1492)
filter, DMA into the descriptor, clear OWN, write MCNT (+4 for the stripped FCS), and raise
RINT correctly.
- The 68K level-2 interrupt reaches RCVCOMPLETE (the trace shows PC 0x5C42), the descriptor
is well-formed (rmd1=0x0303), and the firmware runs its real logic.
- The firmware then DROPS the frame by design (gate 3 / gate 2). The "missing notify" is a
correct consequence of the drop, not a lost SCIP.
Therefore the fix is NOT a patch to the notify/SCIP path. It is one of: 1. Ensure inbound COSMOS frames are IEEE-802.3 framed (bytes 12-13 = LLC data length). Real card-to-card traffic already is (the firmware TX builds 802.3), so a two-emulator bridge is fine; a hand-built or foreign DIX frame is dropped at gate 2. 2. Ensure the ND-100 / ENNS0 side posts receive buffers into the card pool (0x188C6) before frames arrive. This is an XMSG/driver-sequence interaction, one layer above the controller emulation. Until buffers are posted, gate 3 drops everything.
Because both remaining causes are in guest/higher-layer behaviour (not the C# controller), no behavioural controller-code change is warranted; making one blind would risk masking the real gate. What IS added is precise instrumentation to pin the exact gate on the next run.
6. Emulator change made (instrumentation only)¶
File: E:\Dev\Repos\Ronny\RetroCore\Emulated.HW\ND\CPU\NDBUS\NDBusEthernetII.cs
(the 68K PC-watch, _pcWatch + Watch68KPc). Added, gated by the existing Device log level
(zero cost when Device logging is off):
- Two new watch PCs at the decisive branch targets:
0x6020-> logs[68K-RX] DELIVERED(RCVCOMPLETE built a message ->jsr 0x134E6-> ND-100 ready-ring + SCIP).0x6034-> logs[68K-RX] DISCARDED(frame dropped by gate1/2/3 -> re-append ring, NO SCIP).
- At the RCVCOMPLETE entry
0x5C42, dumps the free XMSG receive-buffer pool state:[68K-RX] RCVCOMPLETE 0x5C42: XMSG rx-buffer pool $188C6 count=<n> freeHead=0x<addr>with an explicitPOOL EMPTY: frame WILL be discarded (gate3), no SCIPwhen the pool is empty. This DIRECTLY confirms or refutes the gate-3 hypothesis of section 4.
Build: dotnet build Emulated.HW/Emulated.HW.csproj -> 0 errors. [V]
Tests: EthernetII / LANCE suite = 150 passed, 2 failed; the 2 failures
(Test_LANCE_TDMD_TransmitDemand, Test6_SoftwareInterruptController) are the documented
pre-existing failures (fail identically on the pre-edit baseline, per
ethii-emulator-fixes memory). No regression from this change. [V]
7. How to validate end-to-end¶
Run with Device logging on and watch the [68K-RX] lines:
- Single-node harness RX probe (
Boot_Login_StartXmsg_And_EthernetServer_CaptureControllerLog, [Explicit]; needsD:\BIGDISK0-L.IMG, do not block if locked). The injected frame atNd100SintranEthernetIIBootHarnessTests.cs:476is a real 802.3 COSMOS frame. Expected with this instrumentation:[68K-RX] RCVCOMPLETE ... POOL EMPTY ...followed by[68K-RX] DISCARDED. That confirms gate 3 (no receive buffer posted) as the live blocker. - Two-node bridge (the real connect-to case): two emulators joined via
device add ETH 0 --net=tcp:.../--net=listen:...(or pcap). Driveconnect-tofrom one node. On the receiver, a[68K-RX] DELIVEREDline means the frame reached XMSG (fix works); a[68K-RX] DISCARDEDwithPOOL EMPTYmeans the receiver never posted receive buffers - chase the ND-100/ENNS0 receive-buffer posting (the LU-2240B input path).
The gate the log names tells you exactly where to continue:
- DISCARDED + POOL EMPTY -> ND-100/ENNS0 must post receive buffers (XMSG credit path).
- DISCARDED + pool non-empty -> gate 1 (MAC mismatch: check card MAC vs frame dst) or gate 2
(frame not 802.3: bytes 12-13 are a DIX EtherType, not a length).
- DELIVERED -> RX-forward is fixed; any remaining failure is downstream in XMSG/XROUT.
8. Key addresses (quick reference)¶
| Addr | Role |
|---|---|
| 0x5C42 | RCVCOMPLETE entry (LANCE RX complete, 68K level 2) [V] |
| 0x5DF2 | gate 1: dst-MAC-for-us ($38) beq 0x5EF4 [V] |
| 0x5DFA | gate 2: 802.3-length-frame ($36) beq 0x5EF4 [V] |
| 0x5E02 / 0x5E0E | gate 3: free rx-buffer pool 0x188C6 (+0 count / +4 head) [V] |
| 0x5EF4 | discard marker: clr.l $18(A6) [V] |
| 0x601A | deliver-vs-drop branch tst.l $18(A6); beq 0x6034 [V] |
| 0x6020 | DELIVERED: jsr 0x134E6 -> ready-ring 0x188D6 + SCIP [V] |
| 0x6034 | DISCARDED: bsr 0x5B60 RCVRINGAPP + jmp (A5), no SCIP [V] |
| 0x134E6 | generic "enqueue buffer on list + signal" (deliver / return) [V] |
| 0x18000/2/4 | RX software ring header FREE/PROD/CONS [V] |
| 0x18008 | RX descriptors (RMD), 8 bytes each [V] |
| 0x1885E | card MAC (dst recheck target) [V] |
| 0x1888A | card 802.3-vs-typed mode word (gate-2 discrimination) [V] |
| 0x1888C | card statistics/counter control block base [V] |
| 0x188C6 | XMSG receive-buffer pool: +0 count, +4 RX free-list, +8 TX free-list [V] |
| 0x188D6 | ND-100 delivered/ready message ring (target of 0x134E6 on RX) [V] |
9. Files¶
- Firmware binary:
E:\Dev\Repos\Ronny\RetroCore\Emulated.Tests\NDBusDevices\TestData\encos-ser-all-banks-68k.bin - Emulator change:
E:\Dev\Repos\Ronny\RetroCore\Emulated.HW\ND\CPU\NDBUS\NDBusEthernetII.cs(_pcWatch/Watch68KPc) - LANCE RX (verified correct):
E:\Dev\Repos\Ronny\RetroCore\Emulated.HW\AMD\LANCE\Am7990\Am2990Lance.cs(ReceivePacket1359,ReceiveComplete1492) - Harness + RX probe frame:
E:\Dev\Repos\Ronny\RetroCore\Emulated.Tests\ND100\Nd100SintranEthernetIIBootHarnessTests.cs:476 - Related prior RE:
E:\Dev\Ronny\NDInsight\SINTRAN\XMSG\DOC\COSMOS-RE\ENNS0-Startup-RE-2026-07-23\(ROOT-CAUSE-ENNS0-HANGS-INPUT-WAIT-LU2240B, FIRST-SUPERKICK-BRIDGE-DECODE, MAKE-RUNNABLE-TRIGGER-AND-FIX) - Master reference:
E:\Dev\Repos\Ronny\RetroCore\DOCS\ND_EthernetII_MASTER_REFERENCE_2026-07-23.md