From 874d1fbbf566cec0c7187be6ff5fbc1800c9346b Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Fri, 30 Oct 2026 01:25:21 +0200 Subject: [PATCH] networking RE report + LANCE payload variants harness/mame/NETWORK_REPORT.md: how the VT1200 firmware drives the LANCE, from ROM disassembly and MAME traces. 50 sections, each claim tied to a bit address, a trace count or a quoted source line. Covers the driver device struct (0x23CBF0), init (0x3F9D4BC0), descriptor/DMA encoding, the live handler (0x3F9D5B40) with per-status-bit dispatch, the RX poll (0x3F9D62B0), ring arming (0x3F9D36C0), TX completion (0x3F9D8AF0) and the single transmit path every protocol uses (0x3F6084E0 -> 0x3F9DC890 -> 0x3F9D4BC0), plus the IP protocol dispatch (0x3F6270F0), ICMP (343-count chain), TCP state tables and live TIME_WAIT, the ARP responder (0x3F6119A0) and the socket 4-tuple demux confirmed by a memory dump. It also records what was withdrawn: five successive explanations for the real-silicon CSR0=0x1BB / TMD1=0x8338 result, and the descriptor-length encoding bug found by comparing Linux 7990.c and NetBSD am7990.c (length must be 0xF000 | -len; MAME masks the difference in get_buf_length). harness/udpstream: the LANCE loopback/takeover payload and the variants used to test the init/kick orderings in MAME (bare TDMD, STOP before INIT, double INIT, long wait with CSR0 sampling, IDON clear, corrected descriptor length). build-lance.sh takes LANCE_SRC/LANCE_OUT so the variants build from one script. --- harness/mame/NETWORK_REPORT.md | 3513 ++++++++++++++++++ harness/udpstream/.gitignore | 6 + harness/udpstream/build-lance.sh | 93 + harness/udpstream/build-tcp.sh | 91 + harness/udpstream/build.sh | 91 + harness/udpstream/build/lance.coff | Bin 0 -> 3927 bytes harness/udpstream/build/lance_bare.coff | Bin 0 -> 3927 bytes harness/udpstream/build/lance_fixlen.coff | Bin 0 -> 3929 bytes harness/udpstream/build/lance_idonclear.coff | Bin 0 -> 3971 bytes harness/udpstream/build/lance_init2.coff | Bin 0 -> 3943 bytes harness/udpstream/build/lance_longwait.coff | Bin 0 -> 4065 bytes harness/udpstream/build/lance_stopinit.coff | Bin 0 -> 3943 bytes harness/udpstream/entry.asm | 136 + harness/udpstream/lance.c | 274 ++ harness/udpstream/lance_bare.c | 274 ++ harness/udpstream/lance_fixlen.c | 274 ++ harness/udpstream/lance_idonclear.c | 279 ++ harness/udpstream/lance_init2.c | 275 ++ harness/udpstream/lance_longwait.c | 288 ++ harness/udpstream/lance_stopinit.c | 275 ++ harness/udpstream/romapi.h | 93 + harness/udpstream/send.py | 121 + harness/udpstream/tcpstream.c | 121 + harness/udpstream/udpstream.c | 161 + 24 files changed, 6365 insertions(+) create mode 100644 harness/mame/NETWORK_REPORT.md create mode 100644 harness/udpstream/.gitignore create mode 100755 harness/udpstream/build-lance.sh create mode 100755 harness/udpstream/build-tcp.sh create mode 100755 harness/udpstream/build.sh create mode 100644 harness/udpstream/build/lance.coff create mode 100644 harness/udpstream/build/lance_bare.coff create mode 100644 harness/udpstream/build/lance_fixlen.coff create mode 100644 harness/udpstream/build/lance_idonclear.coff create mode 100644 harness/udpstream/build/lance_init2.coff create mode 100644 harness/udpstream/build/lance_longwait.coff create mode 100644 harness/udpstream/build/lance_stopinit.coff create mode 100644 harness/udpstream/entry.asm create mode 100644 harness/udpstream/lance.c create mode 100644 harness/udpstream/lance_bare.c create mode 100644 harness/udpstream/lance_fixlen.c create mode 100644 harness/udpstream/lance_idonclear.c create mode 100644 harness/udpstream/lance_init2.c create mode 100644 harness/udpstream/lance_longwait.c create mode 100644 harness/udpstream/lance_stopinit.c create mode 100644 harness/udpstream/romapi.h create mode 100644 harness/udpstream/send.py create mode 100644 harness/udpstream/tcpstream.c create mode 100644 harness/udpstream/udpstream.c diff --git a/harness/mame/NETWORK_REPORT.md b/harness/mame/NETWORK_REPORT.md new file mode 100644 index 0000000..f7aaf66 --- /dev/null +++ b/harness/mame/NETWORK_REPORT.md @@ -0,0 +1,3513 @@ +# VT1200 networking — how the firmware drives the LANCE + +Reverse engineering notes, **measurement and disassembly only**. The ROM is +untouched (`md5 b46a3e442ccb86134d2356bd70d22fd2`), no driver edits. Evidence +for each claim is the command/address quoted next to it. + +Address convention: **bit addresses** (TMS34010). Ghidra address = bit / 8, and +in the Ghidra project the ROM is loaded at `0x07E40000 + file offset`. + +--- + +## 1. Hardware surface + +| item | address | evidence | +|---|---|---| +| LANCE register address port (RAP) | bit `0x05900000` | `movi 0x5900000` in the only code that uses the immediate, the ISR at `0x3FDD8E80` | +| LANCE register data port (RDP) | bit `0x05800000` | same site | +| Ethernet address ROM | bit `0x06800000`, **8 bytes** = MAC + 2 checksum bytes | reader `0x3FDD74B0`; `LANCE_REPORT.md` | +| interrupt | LANCE `intr_out` → INT1 | `LANCE_REPORT.md`; `INTENB` bit 1 at `0x3FA8C340` | + +Registers are 16-bit (`move …,0`). `csr_w(reg, v)` = write `reg` to RAP, then +`v` to RDP; `csr_r(reg)` = write `reg` to RAP, read RDP. + +## 2. The driver's device struct (found in a live RAM dump) + +The driver does **not** hard-code the register addresses in its normal paths; it +keeps them in a struct. On a live boot (RAM dump at frame 5500) the struct at +**bit `0x23CBF0`** (byte `0x4797E`) reads: + +``` ++0x00 u32 0x05900000 RAP base ++0x04 u32 0x05900000 RAP base (copy) ++0x08 u32 0x05800000 RDP base ++0x0C u32 0x05800000 RDP base (copy) ++0x10 u32 0x06800060 Ethernet ROM pointer (0x6800000 + 8 bytes read) ++0x14 u32 0x0008FFC9 driver status ++0x16 u8[6] 08 00 2b 16 4b 5a PADR (MAC, copied from the ROM) +``` + +How it was found: a byte search of the RAM dump for the little-endian values +`0x05900000` / `0x05800000` returns byte offsets `0x4797E` / `0x47986` +(= bits `0x23CBF0` / `0x23CC30`), and the MAC appears at `0x47996` +(= struct + 0x16). + +This is also why a static search for `movi 0x5900000` finds only two sites: the +normal driver loads the pointer from this struct. + +## 3. Initialisation sequence (from the firmware's own code) + +`io5800_5900_init` at bit `0x3FDDCBE0` (Ghidra `0x07FBB97C`): loads the RAP/RDP +immediates, then: + +``` +RAP=1 RDP=initblock>>0 ; CSR1 +RAP=2 RDP=initblock>>16 ; CSR2 +RAP=3 RDP=0 ; CSR3 (bus config) +RAP=0 RDP=0x7F41 ; CSR0 = INIT + standard interrupt enables +... poll CSR0 bit 8 (IDON) ... +CSR0 = 0x0042 ; STRT | INEA (the OS's own start) +``` + +The OS's live path is the same sequence (LANCELOG in `LANCE_REPORT.md`: +`CSR1=0x0F80`, `CSR2=0`, `CSR3=0`, `CSR0=0x0001`, poll `CSR0=0x0181`, then +`CSR0=0x0042`). + +## 4. Initialisation block + +12 words / 24 bytes, read by the chip over DMA: + +| word | field | live value | +|---|---|---| +| 0 | mode | `0x0000` (normal; our test used `0x8000` PROM / `0x0044` LOOP\|INTL) | +| 1-3 | PADR (6 B) | `08 00 2b 16 4b 5a` | +| 4-7 | LADRF (8 B) | `0000 0000 0008 0000` | +| 8 | RX ring address low 16 | `0x0A80` | +| 9 | RX ring high 8 + length in bits 13-15 | `0xE000` → length `1<<7` = **128** | +| 10 | TX ring address low 16 | `0x0E80` | +| 11 | TX ring high 8 + length | `0xA000` → length `1<<5` = **32** | + +Captured live from the running unit with `peek` at bit `0x7C00` (byte `0xF80`, +the address stored in the pointer cell `0x23E7A0`): + +``` +00 00 08 00 2b 16 4b 5a 00 00 00 00 08 00 00 00 80 0a 00 e0 80 0e 00 a0 +``` + +After boot the descriptors at byte `0x0A80` read `00 00 00 00 | 00 f0 | 00 00` +per entry — address 0, flags 0, length `0xF000` (= 4096). So the ring is +**re-armed dynamically** by the driver; it is not left statically owned. + +## 5. Descriptors and DMA + +8 bytes per descriptor: + +``` ++0 u16 buffer address low 16 ++2 u16 flags | buffer address high 8 (OWN=0x8000, STP=0x0200, ENP=0x0100, + ERR=0x4000, ...) ++4 u16 buffer length ++6 u16 message count (RX completion) +``` + +Buffer length encoding (C-LANCE, `get_buf_length` in MAME's `am79c90.cpp:191`): + +``` +length = -s16(0xF000 | data) e.g. 64 B -> 0x0FC0, 1526 B -> 0x0A0A, 4096 -> 0xF000 +``` + +DMA addresses are **24-bit byte addresses**: buffer = `(RMD1 & 0xFF) << 16 | RMD0`; +rings = `(word9 & 0x1FFF) << 16 | word8`. + +## 6. Interrupt handling (corrected by trace) + +Two different handlers exist; only one is live. + +**Live path.** The trace shows the CPU being interrupted and the OS dispatching +by handler table: + +``` +(interrupted at 3F579870, IRQ 0) +00000F60: JA 3F7144C0 +3F7144C0: MMTM SP,A0-A3,A8 +3F7144E0: MOVI 3F7151F0,A0 ; handler table +3F715040: SETF 8 +3F715050: A3 = *0x1E9420; A3++ ; interrupt counter +3F7150C0: A1 = *A0 ; handler from the table +3F7150D0: CALL A1 ; -> 3F9D5B40 +3F9D5B40: MMTM SP,A7,A9,A11 ; LANCE handler +``` + +The LANCE handler `0x3F9D5B40` (803 calls in an idle-window trace): + +``` +a11 = *0x23CBF0 ; RAP pointer (0x05900000) +*RAP = 0 ; select CSR0 +a7 = *0x23CC50 ; RDP pointer #2 (0x05800000) +a9 = *RDP ; read CSR0 +btst 6, a9 ; INTR +jreq return ; not ours +a7 = *0x23CC30 ; RDP pointer #1 +*RDP = a9 ^ 0x40 ; *RDP = 0x40 ; clear INTR +btst 15 .. 8 on a9 ; ERR, MISS, RINT, TINT ... + bit 10 set -> calla 0x3F9D62B0 ; RX poll + bit 9 set -> calla 0x3F9D8A50 ; TX routine + bit 8 set -> set 0x23EC90 = 1 +``` + +**Not live.** `0x3FDD8E80` (the other CSR0 handler, reached only from the vector +stubs `0x3FA93470`/`0x3FA935F0`) has **0 hits** in a 168 MB boot trace with 200 +ICMP echoes. It belongs to another path (diagnostics), not the running stack. + +Note the three RAP/RDP pointer cells: `0x23CBF0` = RAP, `0x23CC30` and +`0x23CC50` = two RDP aliases (all `0x05900000` / `0x05800000` in the RAM dump). +`0x23CC50` is also the **CSR0 command shadow** the init/TX code reads and ORs +before writing. + +## 7. What the traffic actually exercises + +Differential trace: the same boot window traced with and without ICMP traffic +(`/tmp/opencode/no_ping.tr` vs `with_ping.tr`), PC sets diffed. The PCs that +appear **only** with traffic in the driver area are `0x3F9D90E0–0x3F9D9270` +and `0x3F9D9400` — the protocol layer (TCP/IP flag and counter handling, cells +`0x23CB70/0x23CB80/0x23ECxx/0x23EFxx`), not the chip ring handling. The ring +handling code also runs without traffic (it is the poll loop), which is why it +is not in the diff. + +## 8. Independent validation of the above against real silicon + +A payload (`harness/udpstream/lance.c`) built from this description was tested +in MAME loopback and reached: + +``` +ok=1 RMD1=0x0338 (OWN clear, STP|ENP) MCNT=0x001C (28 = 24 + 4 FCS) TMD1=0x0338 +``` + +i.e. init block, ring addressing, descriptor format, length encoding, TX and RX +DMA are all correct for the modelled chip. + +On the real unit the same payload completes `INIT → IDON → STRT` (CSR0 = +`0x1B3`, then a stable `0x33`), but: + +* `TDMD` stays set and the TX descriptor is never touched (`0x8338`), +* the RX descriptors are never touched, +* one earlier run showed `MISS|ERR`, i.e. the receiver did see frames but had + no usable buffer. + +This is the open question: the driver's dynamic ring arming (section 4) is not +yet reproduced, and the chip does not service a transmit demand from a +hand-built ring. + +## 9. Reproduction / instrumentation + +* Boot trace: `-autoboot_script` traces (`trace `), e.g. + `/tmp/opencode/ping_trace.lua`, `/tmp/opencode/win_trace.lua`. +* Headless debugger note: **breakpoint `printf` actions print nothing**; use the + `trace ` action instead. `bpset` addresses are bit addresses. +* Register/ROM reads on the real unit: `dwt/solve.py --peek --len N`. +* Ring/init-block dumps: `VT1200_DUMP_RAM=` and search the dump. + +## 10. Ring arming and the TX kick (the part that matters for a takeover) + +From the traffic trace and the ring-handler sites (code referencing the ring +cells `0x23E7E0` RX / `0x23E820` TX), both rings are driven the same way. + +### RX arming — `0x3F9D36C0` + +``` +a9 = *0x23EC50 ; list of receive buffers +a7 = a9 + 0xA0 ; buffer data = entry + 20 bytes +a11 = a7 >> 3 ; BIT address -> BYTE address <-- key +a7 = *0x23EBB0 << 6 ; ring position * 64 bits (8-byte descriptors) +a12 = *0x23E7E0 + a7 ; ring base (BIT address 0x5400 = byte 0xA80) +*a12 = a11 ; buffer BYTE address (32-bit store) +setf 12; *a12(0x20) = 0xFDDA ; RMD2 = 12-bit length, -(550) => 550 bytes +*a12(0x18) = 0x80 ; byte 3 = OWN, set LAST +*0x23EBB0 = (pos + 1) & 0x7F ; 128-entry ring wrap +``` + +Two details our first payloads got wrong or approximated: + +* the buffer address is converted with `srl 3` to a **byte** address before it + goes in the descriptor; +* the length is written with `setf 12` (a 12-bit field), value `0xFDDA` for a + 550-byte buffer; +* OWN is written as a single byte `0x80` at descriptor + 3, **after** the + address and length. + +### TX kick — `0x3F9D57F0` + +``` +a7 = a11 << 6 ; descriptor offset +a0 = *0x23E820 + a7 ; TX ring (BIT address 0x7400 = byte 0xE80) +a7 = *(a0 + 0x18) ; byte 3 (RMD1 high byte) +a7 |= 0x80 ; OWN +*(a0 + 0x18) = a7 ; own the descriptor +... +a7 = *0x23CBF0 ; RAP pointer +*a7 = 0 ; RAP = 0 +a5 = *0x23CC50 ; CSR0 command shadow kept by the driver +a5 |= 0x0008 ; TDMD +*0x23CC30 = a5 ; RDP = shadow | TDMD +``` + +> **Correction — see section 40.** The explanation originally written here +> ("the command bits are not set-only: writing `0x0008` alone clears `STRT`") is +> **wrong**. The AM79C90 model makes `INIT/STRT/STOP/TDMD` write-1-to-set and +> `INEA` read/write, so a bare `TDMD` disables interrupts but does *not* stop the +> transmitter. The firmware's shadow exists to preserve `INEA`; and the +> real-silicon `CSR0 = 0x1BB` decode (`STRT`=1, `TDMD`=1, `TXON`=1) shows a poll +> that never ran, which is a different problem entirely. + +**The transmit demand is sent as `shadow | TDMD`, never as a bare `TDMD`.** The +write must preserve the shadow's read/write bits (in particular `INEA`), and it +matches the driver; our payload arms the descriptor at kick time and writes +`STRT|TDMD` in one word. + +## 11. The live driver paths (RX poll, TX completion) + +The stack is **polled**: the driver's task calls a receive poll and a transmit +completion check. Ring positions and counters are global cells: + +| cell | meaning | +|---|---| +| `0x23E6A0` | free receive buffers | +| `0x23EBA0` | RX ring **consumer** position (poll reads here) | +| `0x23EBB0` | RX ring **producer** position (refill writes here, `& 0x7F`) | +| `0x23EC50` | list of receive buffers (data at entry + `0xA0` bits) | +| `0x23CB00`, `0x23CB30` | saved `a0`/`a2` across a poll with no frame | +| `0x23CB70`, `0x23CB90`, `0x23CBB0`, `0x23CBC0` | TX/RX statistics | + +### RX poll — `0x3F9D62B0`, body at `0x3F9D6300` + +``` +a7 = *0x23EBA0 << 6 ; consumer position * 64 bits +a10 = *0x23E7E0 + a7 ; RX ring base (BIT address 0x5400) +setf 8 +a6 = *(a10 + 0x18) ; descriptor byte 3 = RMD1 high byte +btst 7, a6 ; OWN still set -> no frame +jrne 0x3F9D8980 ; (saves a0/a2 to 0x23CB00/0x23CB30, returns 0) +a9 = *a10 ; buffer BYTE address (32-bit store) +jreq 0x3F9D8590 ; null buffer +a9 = (a9 << 8) >> 5 ; BYTE address -> BIT address +a9 -= 0xA0 ; back to the buffer-list entry +... link the buffer into the queue, *0x23E6A0 = *0x23E6A0 - 1 ... +btst 1, a6 ; STP +btst 0, a6 ; ENP +``` + +The poll returns 0 when the descriptor is still owned (the normal idle case) — +this is the loop that replaces interrupts in the live stack. + +### TX completion — `0x3F9D8AF0` + +``` +a5 = *0x23E820 ; TX ring base (BIT address 0x7400) +a11 = position * 64 + a5 +setf 8 +a9 = *(a11 + 0x18) ; TMD1 high byte +btst 7, a9 ; OWN still set -> not complete +jrne 0x3F9DAEF0 ; (saves a2/a4/a6 to 0x23CB90/0x23CBC0/0x23CBB0) +a12 = *a11 ; buffer BYTE address +jreq 0x3F9DAC80 +a12 = (a12 << 8) >> 5 ; BYTE -> BIT +btst 1, a9 ; STP +btst 0, a9 ; ENP +... error/status bits update the statistics cells ... +``` + +### How the poll is driven + +The RX poll `0x3F9D62B0` and the TX routine are called **from the interrupt +handler** (`0x3F9D5B40`) on status bits 10 and 9 — 402 poll calls were counted in +an idle-window trace, driven by the broadcast/ARP traffic the terminal receives +even with no pings. So the hardware model for a payload is: ring + INEA, and +service CSR0 in the handler (or poll CSR0 yourself with INEA clear). + +## 12. Handler dispatch and the observed protocol chain + +`0x3F9D5B40` is a pure status dispatcher. After reading CSR0 and clearing INTR +it tests the status word bit by bit (all evidence: the disassembly of +`0x3F9D5B40`..`0x3F9D6280`): + +| status bit | action | +|---|---| +| 15 ERR | error counter `0x23ED20`, call `0x3F9DAFF0(5)` | +| 14 | counter `0x23EEF0` | +| 13 | counter `0x23EF50` (skipped while `0x23ECA0` bit 2 is set) | +| 12 | counter `0x23EE80` | +| 11 MISS | counter `0x23ED20` | +| **10 RINT** | **`calla 0x3F9D62B0`** — the RX poll | +| **9 TINT** | **`calla 0x3F9D8A50`** — TX completion | +| 8 | `0x23EC90 = 1` | + +So the interrupt handler never builds or sends a frame itself: it only reaps +status and calls the RX poll / TX completion. **Frame submission is done by the +protocol layer calling into the driver**, which is why the init function +`0x3F9D4BC0` (which contains the only `TDMD` write, at `0x3F9D57F0`) is the +driver's command entry — it is called with `(cmd, arg)` from the protocol side +(and from the refill code at `0x3F9D3A30`). + +### Protocol entry points seen in a ping window + +Trace window frames 3000-4200 with an ICMP flood; the RX poll's callees, in +order (addresses are bit addresses): + +``` +3F9DF160 / 3F9DF4C0 driver/task helpers +3F604780 3F606210 3F600200 3F60BD40 packet processing +3F9E0B80 3F9DEDA0 receive-queue / socket delivery +3FB2C000 3FB2C290 byte-swap (htons/htonl) + per-fragment checksum loop +3F61E490 3F61AB30 upper-layer (socket) handling +``` + +The TX completion routine `0x3F9D8A50` is entered 343 times in that window +(status bit 9), while the init-path kick `0x3F9D57F0` is never reached — so the +live TX is submitted from the protocol side into the driver command entry and +completed via `0x3F9D8A50`. + +Open item for the next pass: pin the exact protocol->driver send call (the +argument layout of `0x3F9D4BC0` and which command value means "transmit"), and +map the per-fragment checksum loop `0x3FB2C290` (chain at struct+0x40). + +## 13. Protocol -> driver transmit submission (resolved) + +The driver command entry `0x3F9D4BC0` is what the protocol layer calls to hand +over a frame. Evidence: in a ping-flood window (frames 3000-4200) the trace +contains **343** entries to `0x3F9D4BC0`, all from the same call site: + +``` +3F9DCF00: MOVE A14,-*SP,1 +3F9DCF10: MOVE A2,*A14+,1 ; last pushed -> arg2 = request/command value +3F9DCF20: MOVK 1,A5 +3F9DCF30: MOVE A5,*A14+,1 ; arg1 = 1 (unit / path selector) +3F9DCF40: MOVE A11,*A14+,1 ; first pushed -> arg0 = device struct +3F9DCF50: CALLA 3F9D4BC0h +``` + +The wrapper just above (`0x3F9DCE40`) prepares the request at `a11 + 0x100`: + +``` +a7 = *0x23CCA0 -> *(a4+0x30) ; MAC / PADR copied into the request +a7 = *0x23CCC0 -> *(a4+0x50) +a10 -> *(a4+0x60) +``` + +so the request block is `device+0x100` with fields at `+0x30`, `+0x50`, `+0x60`. + +### The callee's transmit branch + +`0x3F9D4BC0` receives `(arg0 = device, arg1 = 1, arg2 = request)`: + +``` +a9 = arg0 ; device/ring structure +a10 = arg1 ; 1 +if (a10 > 1) { print 0x3F9E1F78; ... } ; only "1" is a valid path +... +loop over the buffer chain: + a2 = *(a9 + 0x40) ; next buffer + a7 = *0x23EBF0 ; TX producer position + a7 <<= 6 ; 8-byte descriptor stride + a0 = *0x23E820 + a7 ; TX ring (BIT address 0x7400 = byte 0xE80) + a9 + 0x100 / +0x40 ... ; per-buffer fields +``` + +The loop arms the descriptors in the TX ring; the **TDMD kick** for that batch is +the code at `0x3F9D57F0` already documented in section 10: + +``` +a7 = *(a0 + 0x18) | 0x80 ; OWN the descriptor +a5 = *0x23CC50 ; a5 |= 0x0008 ; command shadow | TDMD +*0x23CC30 = a5 ; RDP = shadow | TDMD +``` + +So the transmit submission chain is: + +``` +RX poll 0x3F9D62B0 + -> protocol (0x3F604780 / 0x3F606210 / 0x3F600200 / 0x3F60BD40 / 0x3F61E490 / 0x3F61AB30) + -> wrapper 0x3F9DCE40 (fills device+0x100 request: MAC at +0x30 ...) + -> driver command entry 0x3F9D4BC0(device, 1, request) [343 calls/window] + -> TX ring arming at 0x23E820 + producer 0x23EBF0 + -> TDMD kick 0x3F9D57F0 = CSR0 shadow | 0x0008 + -> TX completion later on TINT: handler -> 0x3F9D8A50 -> 0x3F9D8AF0 +``` + +Open detail (recorded, not resolved): the exact byte layout of the +`device+0x100` request block beyond `+0x30`, `+0x50`, `+0x60`, and the +packet-buffer objects behind `0x3FB2C000`/`0x3FB2C290`. + +## 14. The packet object and the `+0x100` control block + +Disassembly of the wrapper `0x3F9DC890` (the function that calls the driver +command entry at `0x3F9DCF50`) shows that **arg2 (`a11`) is a packet object**, +not a device: + +``` +a9 = arg0 +a10 = arg1 +a11 = arg2 ; packet object +... +a7 = *(a11 + 0x90) ; packet state word +a5 = *(a11 + 0xF0) + 0x0E ; total length + 14 (Ethernet header) +a12 = a11 + 0x40 +loop: + a7 = *(a12 + 0xB0) ; fragment length + a0 += a7 ; running total + a2++ ; fragment count + a12 = *a12 ; next fragment +``` + +Packet object fields (bit offsets from `a11`): + +| offset | meaning | +|---|---| +| `+0x40` | next fragment pointer | +| `+0x90` | packet state (the wrapper checks it against 0x16) | +| `+0xB0` | fragment length | +| `+0xF0` | total packet length | +| `+0x100` | control block built by the wrapper for the driver call | + +The wrapper copies into the control block: + +``` +*(cb + 0x20) = *(a9 + 0x20) +*(cb + 0x30) = MAC from 0x23CCA0 (PADR) +*(cb + 0x50) = *0x23CCC0 +*(cb + 0x60) = a10 +``` + +Given the LANCE init-block field order (mode, PADR, LADRF, rings), a control +block whose PADR sits at `+0x30` means the block starts `0x2E` bits (mode) before +it — i.e. the wrapper is handing the driver an **interface control block** +(mode/PADR/LADRF/ring words) together with the packet object; the driver entry +with `arg1 = 1` consumes it and, for the packet case, walks the fragment chain +and arms TX descriptors (section 13). + +This is the last structural piece needed to describe the transmit path from the +protocol layer down to the ring and the `TDMD` kick; the byte-level semantics of +`cb+0x50`/`cb+0x60` (the two ring words copied from `0x23CCC0` and `a10`) and +the `0x3FB2C000`/`0x3FB2C290` pool internals remain the documented open items. + +## 15. Correction: `0x3FB2C000` / `0x3FB2C290` are utilities, not a pool + +Disassembly: + +* **`0x3FB2C000`** — byte swap. The 16-bit form does + `sll 8 / andi 0xFF00 / srl 8 / or`; a second entry does the same with + `0xFF00FF00` / `0x00FF00FF` masks for 32-bit values. That is `htons`/`htonl` + (network byte order), which is why it appears repeatedly on packet headers in + the RX/TX traces. +* **`0x3FB2C290`** — a per-fragment loop over a structure: it takes a struct in + `a9`, a count in `a10`, rejects null/negative counts, reads `*(a9+0x40)` as + the next link and iterates with `a5 = 8` — a checksum/accumulate pass over the + fragment chain, not an allocator. + +So the earlier "buffer pool" reading was wrong; the packet path is +header byte-swapping plus checksumming, consistent with the protocol chain in +section 13. + +## 16. The control block is an init-block template (cb+0x50 / cb+0x60 resolved) + +The two cells behind the wrapper's control block are not independent values: + +``` +0x23CCA0 (bit) = byte 0x47994: 08 00 2b 16 4b 5a <- PADR, 6 bytes +word read at 0x23CCA0 = 0x162b0008 <- MAC bytes 0..3 +word read at 0x23CCC0 = 0x00005a4b <- MAC bytes 4..5 +``` + +`0x23CCC0` is simply `0x23CCA0 + 4 bytes` (the last two MAC bytes), so the +wrapper's two stores reassemble the contiguous PADR: + +``` +MOVE *A9(0x20),*A4(0x20),0 ; 16-bit : mode -> cb + 0x20 +MOVE A7=0x162b0008,*A4(0x30),1 ; 32-bit : MAC[0..3] -> cb + 0x30 +MOVE A7=0x00005a4b,*A4(0x50),0 ; 16-bit : MAC[4..5] -> cb + 0x50 +MOVE A10,*A4(0x60),0 ; 16-bit : -> cb + 0x60 +``` + +The field spacing proves the block is a **LANCE initialisation block**: + +| block bit offset | field | source | +|---|---|---| +| `cb+0x20` | mode (16) | `*(a9+0x20)` | +| `cb+0x30` | PADR bytes 0-3 (32) | MAC at `0x23CCA0` | +| `cb+0x50` | PADR bytes 4-5 (16) | MAC at `0x23CCC0` (= `0x23CCA0+4`) | +| `cb+0x60` | next word (LADRF[0]) | `a10` | + +`mode` at `cb+0x20` and PADR starting at `cb+0x30` (16 bits later) is exactly +the init-block layout; LADRF follows at `cb+0x60`. So the wrapper hands the +driver a freshly built init block whose base is `cb+0x20`, together with the +packet object — the driver command entry then programs the chip from it and +transmits the packet. + +### Protocol callees reached from the RX poll (located, partially characterised) + +| entry | head of function shows | +|---|---| +| `0x3F604780` | calls `0x3FB2E040`; state cells `0x1DA4D0`, `0x1DA4E0`; index `0x1D27D0` | +| `0x3F606210` | counter `0x1DA4F0`, calls `0x3F606010` and `0x3FB2E040` | +| `0x3F600200` | large frame (`a14 += 0x9A0`), two args (`a9`,`a10`), frame-length fields at `a13+0x20/+0xB0/+0xC0` | +| `0x3F60BD40` | lock `0x3FB33870`, cell `0x95FD0` | +| `0x3F61E490` | list walk: `a7 = *(arg)`, compare/loop, depth counter `a13+0` | +| `0x3F61AB30` | cell `0x96AC0`, `*(a9+0x100)`, tests bit 1 of `*(a11+0x40)` | + +These are the IP/ICMP and queue-handling entries; the table records what each +one touches at its entry so the next pass can map them field by field. + +## 17. Protocol-region map and call-graph evidence + +Method: from the ping-window trace (`icmpw.tr`, frames 3000-4200), every +`CALLA` line was parsed into a caller->callee edge +(`^([0-9A-F]{8}): CALLA\s+([0-9A-F]+)h`). Result: **131 unique edges, 47,440 +calls**. The top edges are infrastructure, which is itself informative about how +the stack is built: + +``` +3F57A690 -> 3F702440 x2666 free() +3FB3FD60 -> 3F57A710 x1072 +3FB41AD0 -> 3F701230 x1031 malloc() +3F616730 -> 3FB2C140 x1029 checksum/compare helper +3FB41F50 -> 3F701230 x948 malloc() +3F2BE2F0 -> 3F27A110 x858 +3F27A770 -> 3FB3FA00 x858 +3F2C5B10 -> 3F2798B0 x840 +3F2C5B80 -> 3FB3FE90 x840 +3F6249D0 -> 3FB33870 x688 lock +3F6254C0 -> 3FB338D0 x688 unlock +3F9DF550 -> 3F9DF160 x687 driver task helper +3F9DEE40 -> 3F703D90 x687 +3F61ACC0 -> 3FB2C140 x686 checksum/compare helper +``` + +So the protocol layer is dominated by per-packet `malloc`/`free` and lock +traffic, with the checksum helper `0x3FB2C140` called twice per packet path +(`0x3F616730`, `0x3F61ACC0`). + +### Region map `0x3F600000-0x3F630000` + +A 24 KB disassembly of that range (8,824 lines) was scanned for small-constant +protocol dispatch (`cmpi 0x1/0x6/0x8/0x11`). Only **one** site matched: + +``` +3F6156B0 loop: + a7 = *a9 >> 4 ; a7 <<= 3 ; a7 += 0x1E0600 + *a11++ = *(a7) ; copy a table entry + a7 = *a9++ & 0xF ; a7 <<= 3 ; a7 += 0x1E0600 + *a11++ = *(a7) + *a11++ = 0x3A ; ':' + inc a10 + cmpi 0x6, a10 + jrlt loop +``` + +> **Correction (see section 25.2).** This paragraph originally called that loop +> an "ARP/route table display builder". It is not: the two tables it indexes +> (`0x1E0570`, `0x1E0600`) are literally the ASCII string `0123456789abcdef`, +> and the loop converts six bytes into `xx:xx:xx:xx:xx:xx` — a **MAC-address +> formatter**. The `arpre…`/`mods …` strings that follow are unrelated version +> headers (`in.c`, `in_pcb.c`), not table data. + +The absence of literal protocol +compares in this range means the stack dispatches through tables/pointers rather +than a `switch` on the protocol byte, which is why a constant scan does not find +it. + +This is the state of the protocol-layer RE: entry points, per-packet call costs, +and one concrete routine are documented; the table-driven dispatch and the +IP/ICMP state machines remain the open area. + +## 18. Table-driven dispatch: the indirect calls resolved + +Method: in the trace, an instruction like `call a5` / `call a11` is followed on +the next line by the callee's PC, so indirect edges can be reconstructed. The +ping-window trace yields **12 indirect edges, 3,333 calls**: + +``` +3F7150D0 -call a1-> 3F9D5B40 x687 IRQ dispatcher -> LANCE handler (known) +3F606830 -call a11-> 3F602DB0 x344 +3F6271E0 -call a5-> 3F61FFA0 x343 +3F6603A0 -call a5-> 3F615E00 x343 +3F660BB0 -call a5-> 3F616090 x343 +3F630090 -call a5-> 3F607C90 x343 +3F6AACF0 -call a5-> 3F64DC00 x29 +3F6AA8F0 -> 3F6293A0 / 3F64E150 x20 +3F6271E0 -> 3F6540C0 x1 +``` + +### The dispatch table at `0x1E3D80` + +The site `0x3F660380` is the protocol dispatcher: + +``` +a5 = *(a13 + 0xC0) ; index +a5 <<= 6 ; 8-byte entries +a5 += 0x1E3D80 ; table +a5 = *a5 ; handler pointer +call a5 +``` + +The table was read from a live RAM dump (bit `0x1E3D80` = byte `0x3C7B0`), +8 bytes per entry, two function pointers per entry: + +| index | +0 | +4 | +|---|---|---| +| 0,1 | `0x3F658570` | `0x3F658650` | +| **2,3** | **`0x3F615E00`** | **`0x3F616090`** | +| 4..15 | `0x3F658570` | `0x3F658650` | + +Entries 2/3 are exactly the `x343` indirect targets from the trace, i.e. the two +handlers used by the ping traffic. Disassembly: + +* **`0x3F615E00`** — converts a 32-bit value from the packet to host order: it + takes `*(arg0+0x20)`, calls the helper `0x3F6166D0`, then shifts the result + down byte by byte until a non-zero low byte remains, stores it at + `*(arg1+0x20)`, and calls the byte-swap helper `0x3FB2C140`. +* **`0x3F616090`** — converts two values (from `arg0+0x20` and `arg1+0x20`) with + the same helper and compares them, returning 1 when equal, 0 otherwise. That + is a packet-field comparison (address/filter match). + +So the "protocol dispatch" is a per-field helper table (convert + compare), +indexed by a field-type byte at `a13+0xC0`, not a protocol switch — which is why +the constant scan of section 17 found nothing. The IP/ICMP state machines call +these helpers per header field. + +The second dispatch site, `0x3F630050`, uses a **device entry point**: +`a5 = *(a11 + 0x160); call a5` — the protocol calls the driver through a +function pointer stored in the device structure, which is consistent with the +submission path documented in section 13. + +## 19. Dispatch index semantics (bounded) and the two-pointer entries + +The dispatcher function `0x3F660280`..`0x3F6603A0` narrows the index before it +indexes the table: + +``` +L3F660280: + a5 = *(a13 + 0xC0) ; index + subk 17, a5 + jrnc 0x3F661190 ; index >= 17 -> epilogue (mmfm sp,..; rets) + ... + a5 = *(a13 + 0xC0) + a5 <<= 6 ; 8-byte entries + a5 += 0x1E3D80 + a5 = *a5 + call a5 ; entry.fn0 + ... + a5 = *(a13 + 0xC0) + a5 <<= 6 + a5 += 0x1E3D80 + a5 = *(a5 + 0x20) + *(a13 + 0x20) = a5 ; entry.fn1 copied out +``` + +Facts established from this and the trace: + +* only indexes **0..16** are dispatched; `>= 17` returns immediately + (`0x3F661190` is the function epilogue `mmfm sp,a5..a13; rets 2`); +* each entry is **two pointers**: `fn0` at `+0` and `fn1` at `+0x20`; +* the two call sites seen in the trace use the two pointers of the **same** + entry: `0x3F6603A0` calls `entry[2].fn0` = `0x3F615E00` (convert) and + `0x3F660BB0` calls `entry[2].fn1` = `0x3F616090` (compare); +* entries 0,1 and 4..15 all point at `0x3F658570` / `0x3F658650`, i.e. the + table is sparse — only index 2 is populated with real work for the ping path. + +That the ping path uses entry index 2, whose helpers are "convert a 32-bit value +to host order" and "compare two converted values", makes the index a **field +type code** (2 = a 32-bit address field) rather than the IP protocol number +(ICMP would be 1, TCP 6, UDP 17 — and 17 is out of range here). The table at +`0x1E3D80` is therefore a per-field codec/compare table used by the packet +handlers, not the protocol switch itself. + +### Socket layer + +No separate socket dispatcher was found in this range. The protocol layer hands +packets to the driver through the device entry pointer at `*(device + 0x160)` +(dispatcher site `0x3F630050`: `a5 = *(a11+0x160); call a5`), and the socket +API seen from user code (`socket`/`bind`/`sendto`/`recvfrom`/`listen`/`accept`/ +`read`/`close` at `3F5C9A00`/`3F5CAA80`/`3F5D3D70`/`3F5D7D50`/`3F5CBCD0`/ +`3F5CCB40`/`3F5D8E70`/`3F5E8930`) is the kernel-side entry into this same +machinery — documented from the user side in `dwt/notes/TCPIP.md` and measured +(this session: ~20 datagrams/s delivery ceiling). + +## 20. Packet builders: UDP header builder, and a corrected `CMPI` encoding + +> **Correction (see section 22).** This section originally claimed that +> `CMPI #8` "does not exist in the ROM" because a byte search for +> `0x0B40|reg, 0x0008` returned zero hits. That search was wrong: on the +> TMS34010 the `CMPI` immediate is stored **one's-complemented**. A known-good +> site proves it — `cmpi 0x8,a2 ; 0b42 fff7` at `0x3F9D2C80`, i.e. the immediate +> word is `~8`, not `8`. Searching for `0x0B40|reg, 0xFFF7` finds **42** +> `cmpi #8` sites. The ICMP type test is therefore a perfectly ordinary compare; +> the earlier negative result was a tooling error, and the paragraph below is +> kept only as a record of it. + +### UDP header builder — `0x3F656200` + +The ping-only code at `0x3F6562xx` is a transmit header builder, and it declares +its protocol explicitly: + +``` +*(a11+0x20) = 0 +*(a11) = 0 +*(a11+0x40) = 0 ; byte: version/IHL area, filled later +movb 17,*(a11+0x48) ; <<< protocol 17 = UDP +... +a5 = a12 + 8 +calla 0x3FB2C000 ; byte swap (htons) +*(a11+0x50) = a8 (16-bit) ; port/checksum word +*(a11+0x60) = *(a9+0x90) +*(a11+0x80) = *(a9+0x60) +*(a11+0xa0) = *(a9+0xb0) ; 16-bit +*(a11+0xb0) = *(a9+0x80) ; 16-bit +*(a11+0xc0) = *(a11+0x50) ; 16-bit +*(a11+0xd0) = 0 +``` + +So the stack has a per-protocol header builder that fills a packet template at +`a11` from a control block at `a9` and byte-swaps the port/checksum words through +`0x3FB2C000` (the `htons`/`htonl` helper of section 15). The ICMP reply builder +is the analogous routine for type 8 -> 0; it is the next target, and the zero +`CMPI #8` result above says its type test is value- or table-based rather than a +literal compare. + +## 21. The send path above the driver wrapper, and checksum validation + +### Transmit request — around `0x3F609D40`, wrapper call at `0x3F609F20` + +The driver submission wrapper `0x3F9DC890` has exactly **one** caller in the +ping window (343 calls): + +``` +3F609F20: CALLA 3F9DC890h +``` + +and the code in front of it is the send request: + +``` +a9 = *(a13+0xC0) ; packet object +a7 = *(a9+0xF0) ; total length +if (a7 < 0x32) *(a9+0xF0) = 0x32 ; enforce 50-byte minimum (Ethernet+IP+ICMP min) +... +calla 0x3F9DC890(packet, arg1, arg2) +a8 = result +if (a8 != 0) { + (*(a13+0xFFE0)+0x260)++ ; tx error counter + (*(a13+0xFFE0)+0x240)++ ; tx error counter + log 0x3F60E6E0 +} +``` + +So the layering above the driver is: packet object -> minimum-length clamp +(50 bytes) -> submission wrapper `0x3F9DC890` -> driver command entry +`0x3F9D4BC0` -> ring arming -> `TDMD` kick. + +### Checksum validation on receive + +`0x3FB2C290` (the per-fragment checksum loop of section 15) is called once per +packet from four sites in the receive path: + +``` +343 3F6206D0 344 3F6235F0 344 3F625B40 343 3F62FFC0 +``` + +The callers compare the result and bump per-protocol error counters +(e.g. `0x96800` incremented when the checksum check fails), i.e. these are the +**verification** calls, not the reply builder. The reply builder's own checksum +call is the remaining target; the send path above shows where its packet would +have to go. + +### Note on tooling + +The send function's prologue is not present in Ghidra's listing for that region +(`listing.getInstructionAt(0x07EC13A8)` returns none even though surrounding +addresses are disassembled), so this part was read from raw disassembly of the +ROM image, which is why the function is described by address range rather than +by a named entry. + + +## 22. The ICMP echo path, end to end (type 8 -> 0 -> transmit) + +This closes the protocol half: one ping is followed from the ICMP type dispatch +down to the `TDMD` kick. **Every** link below executes exactly **343 times** in +the ping-window trace (`icmpw.tr`, frames 3000-4200), i.e. once per echo request +— that count is the evidence that these addresses form one chain and not merely +a list of functions that happen to run. + +``` +3F6224E0 343 ICMP type dispatch +3F6212A0 343 type 8 handler: movb 0,*a10 +3F621A30 343 reply path: adjust IP total length, per-type stats +3F622A40 343 reflect: pick interface, prepare +3F623350 343 ICMP checksum, then call the IP transmit +3F62EA00 343 IP transmit finalisation: htons + IP checksum +3F62FE00 343 header fix-up block inside the IP transmit +3F630070 343 load device tx method *(device+0x160) +3F607C90 343 device transmit method (reached by indirect call) +3F6084E0 343 driver send (0x32 clamp, builds request) +3F609F20 343 call submission wrapper +3F9DC890 343 driver submission wrapper + -> 3F9D4BC0 -> ring arm -> TDMD (sections 10, 13) +``` + +### 22.1 ICMP type dispatch — `0x3F6224E0`, jump table `0x3F622580` + +The dispatch is a `jr` inside the IP-input function, not a call, and the field +size is already 8 bits when it is entered, so the key is the **type byte**: + +``` +3F6209B0 setf 8,0,0 +3F6209C0 a7 = *a10 ; ICMP type byte +3F6209D0 subk 0x12,a7 +3F6209E0 jrhi 0x3F6227E0 ; type > 18 -> drop +3F620A00 a7 = *(a10) << 5 +3F620A20 a7 += 0x96860 ; received-per-type counter (4 bytes/entry) +3F620A50 (*a7) += 1 +3F620A80 a7 = *(a10+0x08) ; ICMP code byte +3F620AA0 *(a13+0x80) = a7 +3F620AC0 jr 0x3F6224E0 ; dispatch + +3F6224E0 move *a10,a8,0 ; type byte again (same 8-bit field) +3F6224F0 cmpi 0x12,a8 ; type > 18 -> default +3F622510 jrhi 0x3F6227E0 +3F622520 sll 0x5,a8 ; 32-bit (4-byte) entries +3F622530 addi 0x3F622580,a8 +3F622560 move *a8,a8,1 +3F622570 jump a8 ; 343 executions in the ping window +``` + +(The earlier draft of this section said the key was a 16-bit type+code word; +the trace shows the `jr` at `0x3F620AC0` carries the 8-bit field size straight +into the dispatch, so it is the type byte alone. The code byte is saved +separately at `a13+0x80`.) + +The table read straight out of the ROM image (19 entries, 4 bytes each): + +| type | meaning | handler | +|---|---|---| +| 0 | echo reply | `0x3F6227E0` (default/drop) | +| 3 | destination unreachable | `0x3F620AE0` | +| 4 | source quench | `0x3F620C90` | +| 5 | redirect | `0x3F621C40` | +| **8** | **echo request** | **`0x3F6212A0`** | +| 11 | time exceeded | `0x3F620B80` | +| 12 | parameter problem | `0x3F620C20` | +| 13 | timestamp request | `0x3F6212D0` | +| 15 | information request | `0x3F621440` | +| 17 | address mask request | `0x3F621730` | +| 1,2,6,7,9,10,14,16,18 | — | `0x3F6227E0` (drop) | + +Reproduce the table (bit address -> ROM file offset `bit/8 - 0x7E40000`): + +```sh +python3 - <<'PY' +import struct +rom=open('vt1200_v22.bin','rb').read() +base=(0x3F622580//8)-0x7E40000 +for i in range(19): + print(i, hex(struct.unpack(" 0` rewrite that the whole investigation was looking for, and it does not +recompute anything itself — the shared reply path does. + +### 22.3 Shared reply path — `0x3F621A30` + +``` +a7 = *(a13+0x20) ; length delta +a5 = *(a11+0x10) ; IP total length (byte 2) +a5 += a7 ; *(a11+0x10) = a5 ; adjust total length for the reply +*0x96840 += 1 ; "replies sent" counter +a3 = *a10 << 5 ; a3 += 0x96560 ; per-ICMP-type counter (4 bytes/entry) +(*a3) += 1 +calla 0x3F622A40(reflect args) +``` + +`a11` is the IP header (byte 2 = total length), `a10` the ICMP header (type). +Two statistics tables are involved: per-type counters at `0x96560` (indexed by +type, 4 bytes per entry) and the reply counter `0x96840`. + +### 22.4 Reflect/send — `0x3F622A40` (the whole `0x3F622A40`..`0x3F623170` body) + +``` +a9 = arg (IP header / packet) +*a13(0x40) = (*a9 << 2) - 20 ; header/length arithmetic +*(a13+0x00) = *(a9+0x80) +*(a9+0x80) = *(a9+0x60) +a10 = *0x96AC0 ; interface list head +loop: match *(a10+0x20) against the packet's interface + test bit 1 of *(*(a10+0x100)+0x40) ; interface flags (up/broadcast) + ... +calla 0x3F623180 ; route/interface resolve +calla 0x3F62BF30 ; prepare output block +calla 0x3F62C4B0 ; prepare output block +3F622EA0 movi 0xFF,a11 +3F622EC0 movb a11,*(a9+0x40) ; <<< TTL (byte 8) := 255 +calla 0x3F623350 ; ICMP checksum, then ip transmit +``` + +So the reply is sent out of the interface the request arrived on: the interface +list at `0x96AC0` is walked, the interface flags word (`iface+0x100` -> `+0x40`, +bit 1) decides whether it is usable, the route is resolved, and the TTL is set +to 255 — the ordinary initial TTL of a freshly generated IP packet. All of this +is inside `0x3F622A40` (epilogue at `0x3F623150`/`0x3F623170`), not in +`ip`-output. + +### 22.5 Transport checksum, then IP transmit — `0x3F623350` + +``` +a12 = a9 & 0xFFFFFC00 ; packet block base (1024-bit aligned) +a10 = (4-bit field at a9) << 2 ; setf 4 -> read nibble, scale by 4 +a5 = *(a12+0x20) + a10 ; store ; block offset updated +a11 = a12 + (*(a12+0x20) << 3) ; transport header = block + offset +*a11(0x10) = 0 ; <<< zero the checksum field (byte 2 of ICMP) +calla 0x3FB2C290(*a9(0x10)-a10, a12) ; ones-complement sum helper (section 15) +*a11(0x10) = a8 ; <<< store the computed checksum +... +calla 0x3F62EA00 ; IP transmit finalisation +``` + +This is the ICMP checksum: field at **byte 2 of the transport header**, zeroed, +summed by `0x3FB2C290`, stored back. (Section 21's receive-side calls to the +same helper at `0x3F6206D0`/`0x3F6235F0`/`0x3F625B40`/`0x3F62FFC0` are the +matching *verification* on the way in; `0x3F623350` is the transmit side.) + +### 22.6 IP transmit finalisation — `0x3F62EA00` + +Args: `a9` = IP header, `a11` = device/interface, `a12` = packet block. +Function extent confirmed in the ROM: prologue `mmtm sp,a0,a2,a3,a5,a7,a9,a10,a11,a12,a13` +at `0x3F62EA00`, a single `rets 2` at `0x3F631B00`, so everything between — +including the fix-up block and the device call — is one function. The header +fix-up code sits at `0x3F62FEB0`: + +``` +3F62FEB0 a7 = *(a9+0x10) ; IP total length (byte 2) +3F62FEE0 calla 0x3FB2C000 ; htons + *(a9+0x10) = a8 +3F62FED0 a7 = *(a9+0x30) ; flags + fragment offset (byte 6) +3F62FF00 calla 0x3FB2C000 ; htons + *(a9+0x30) = a8 +3F62FF60 sub a7,a7 ; *(a9+0x50) = 0 ; <<< IP header checksum (byte 10) := 0 +3F62FFC0 calla 0x3FB2C290(a0, a12) ; compute +3F630000 *(a9+0x50) = a8 ; <<< store IP header checksum +3F630030 push args +3F630070 a5 = *(a11+0x160) ; <<< device transmit method +3F630090 call a5 ; -> 0x3F607C90 +``` + +Offsets are **bit** offsets, so `0x10/0x30/0x40/0x50` are bytes 2, 6, 8 and 10 — +total length, fragment word, TTL and header checksum: an ordinary IPv4 header +fix-up, byte-swapping the two network-order words and recomputing the checksum. + +### 22.7 The device transmit method is a pointer in the device struct + +The trace shows the dispatch literally (`icmpw.tr`, around line 3614): + +``` +3F630070: MOVE *A11(160h),A5,1 ; device + 0x160 = transmit method +3F630090: CALL A5 +3F607C90: MMTM SP,A5,A7,A9-A11,A13 ; the method +``` + +`0x3F607C90` then checks a device flag word (`*(a9+0x40) & 0x41 == 0x41`), +byte-swaps its own checksum field, sets the minimum length `0x32`, and calls the +driver send `0x3F6084E0` — which is the function whose `0x32` clamp and wrapper +call were documented in section 21. The full stack is therefore: + +``` +application / sockets + -> ip_output 0x3F62EA00 (IP header, checksum, route) + -> device->transmit *(dev+0x160) = 0x3F607C90 + -> driver send 0x3F6084E0 (request build, 0x32 clamp) + -> wrapper 0x3F9DC890 + -> driver command entry 0x3F9D4BC0 (ring arm) + -> 0x3F9D57F0 CSR0 = shadow | TDMD +``` + +### 22.8 What this settles, and what it does not + +Settled with trace + disassembly evidence: + +* the ICMP type test is a **19-entry jump table on the type byte** + (`0x3F622580`), reached by `jr` with the 8-bit field size already set, and the + earlier "no `CMPI #8`" finding was an encoding error in my search (`CMPI` + immediate is one's-complemented); +* the echo request is answered by `movb 0,*(icmp+0)` in `0x3F6212A0`, then the + shared reply path adjusts the IP total length and counts the reply; +* the reply's transport checksum is computed at transport-header byte 2 with the + same helper used for receive-side verification; +* the IP transmit finalisation (`0x3F62EA00`) byte-swaps total length and the + fragment word and recomputes the IP header checksum at byte 10 (TTL 255 is set + by the reflect function `0x3F622A40`); +* the protocol layer reaches the driver through a **function pointer at + device+0x160**, not a fixed call; +* the driver's transmit path is the `shadow | TDMD` kick of section 10. + +Not settled: the internal TCP state machine (the `estab`/`listen` transitions +behind `0x3F635910` and `0x3F645170`), the socket-API binding table itself (entry +points listed in section 19 and `dwt/notes/TCPIP.md`), and the routing/ARP tables +behind `0x96AC0` and `0x3F6156B0`. The hardware half — whether real silicon +services `TDMD` with the corrected command — is still unverified because the +unit is down. + +The dispatch itself, and the TCP and UDP *entry* paths with their checksum, +list-walk and delivery calls, are in section 23. + +### 22.9 Receive-side entry: checksum verification before the type dispatch + +The receive half of the same function is visible in the trace just before the +dispatch (lines 2736-2762 of `icmpw.tr`): + +``` +3F6206D0 calla 0x3FB2C290 ; ICMP checksum over the received message +3F620700 a5 = a8 +3F620710 jreq 0x3F6207B0 ; checksum OK -> continue +3F620720 *0x96800 += 1 ; <<< ICMP checksum-error counter +3F620790 jruc 0x3F6229A0 ; drop +3F6207B0 ... +3F6209B0 setf 8 +3F6209C0 a7 = *a10 ; subk 0x12 ; jrhi drop ; type range check +3F620A20 a7 += 0x96860 ; (*a7) += 1 ; received-per-type counter +3F620AC0 jr 0x3F6224E0 ; -> the jump-table dispatch +``` + +So the same helper `0x3FB2C290` verifies the ICMP checksum on the way in +(counter `0x96800`, and `0x96820` for malformed/unsupported types) and computes +it on the way out (`0x3F623350`), which is a consistent ones-complement sum used +for both directions. The whole receive path — checksum verify, type counter, +type dispatch, per-type handlers, reply build — is one function spanning +`0x3F620xxx`..`0x3F622A30` (epilogue `0x3F622A10`: `mmfm sp,a3,a5,a7,a9,a10,a11,a12,a13; rets 2`); +its entry and the IP-protocol test that routes into it are both pinned in +section 23: the function is `0x3F61FFA0` (ICMP input) and it is entered through +the protocol dispatch table at `0x1E0F70`/`0xD6BE0`, keyed on the IP protocol +byte. + +Counters seen so far, all in RAM: + +| cell | meaning | +|---|---| +| `0x96800` | ICMP checksum errors (receive) | +| `0x96820` | malformed / unsupported ICMP | +| `0x96840` | ICMP replies sent | +| `0x96860` | received per ICMP type (4 bytes/entry, `type << 5`) | +| `0x96560` | sent per ICMP type (4 bytes/entry, `type << 5`) | + +## 23. IP protocol dispatch, TCP and UDP input + +This is the layer above ICMP: how the stack decides which transport handler gets +a received IP packet. + +### 23.1 The dispatcher — `0x3F6270F0` + +``` +3F6270F0 setf 8,0,0 +3F6270F0 a5 = *(a9+0x48) ; <<< IP protocol byte (byte 9) +3F627110 a5 <<= 3 ; 1 byte per table entry +3F627120 a5 += 0x1E0F70 ; protocol -> index table (RAM) +3F627150 a5 = *a5,0 ; index +3F627160 a7 = 0x170 +3F627180 mpys a7,a5 ; index * 0x170 bits (46 bytes per block) +3F627190 a5 += 0xD6BE0 ; protocol block array (RAM) +3F6271C0 a5 = *(a5+0x50) ; <<< handler pointer (block byte 10) +3F6271E0 call a5 +``` + +Two RAM structures drive it: a **256-entry byte table** at bit `0x1E0F70` +(protocol -> block index) and an array of **46-byte protocol blocks** at bit +`0xD6BE0`. + +### 23.2 The table, read from a live RAM dump + +From `ramdump3/vt1200-ram-5500.bin` (bit address -> byte `bit/8`): + +``` +table 0x1E0F70: 03 04 03 03 03 03 02 03 ... 03 01 03 ... +proto: 0 1 2 3 4 5 6 7 ... 16 17 18 ... +``` + +| proto | index | block (bit) | handler | | +|---|---|---|---|---| +| 1 ICMP | 4 | `0xD71A0` | `0x3F61FFA0` | ICMP input (section 22) | +| 6 TCP | 2 | `0xD6EC0` | `0x3F63A340` | TCP input | +| 17 UDP | 1 | `0xD6D50` | `0x3F6540C0` | UDP input | +| everything else (253 values) | 3 | `0xD7030` | `0x3F634600` | default / unreachable | + +Block layout, verified in all four blocks (UDP block bytes 0..13 shown): + +``` +02 00 80 74 | 0d 00 | 11 00 | 03 00 | c0 40 65 3f + ^+4 ^+6 ^+8 ^+10 handler = 0x3F6540C0 + +6 = 17 = UDP (TCP 6, ICMP 1, default 255) +``` + +So a protocol block carries its own protocol number at byte 6 and its input +function at byte 10 — the dispatcher does not need the number once it has the +block. + +### 23.3 Trace evidence + +| trace | dispatches (`3F6270F0`) | ICMP `3F61FFA0` | TCP `3F63A340` | UDP `3F6540C0` | default `3F634600` | +|---|---|---|---|---|---| +| `icmpw.tr` (ping flood) | 344 | **343** | 0 | 1 | 0 | +| `rttcp2.tr` (80 TCP connects + UDP) | 56 | 0 | **44** | **12** | 0 | + +56 = 44 + 12 exactly, i.e. every dispatch in the second trace is accounted for +by one of the two handlers, and the ICMP trace sends essentially all traffic to +the ICMP entry. `rttcp2.tr` was recorded in **real time** (no `-nothrottle`), so +wall-clock traffic from the host lands inside the traced frame window. + +### 23.4 TCP input — `0x3F63A340` .. `0x3F643C40` + +One function (prologue `mmtm sp,a0,a1,a3,a5,a7,a9,a10,a11,a12,a13`, single +`rets 2` at `0x3F643C40`). Head of the function: + +``` +3F63A340 mmtm sp,a0,a1,a3,a5,a7,a9,a10,a11,a12,a13 +3F63A430 *0x97590 += 1 ; TCP packets received +a10 = packet ; a5 = a10 + (*(a10+0x20)<<3) ; a9 = a5 +3F63A510 setf 4 ; a5 = *a9 ; 4-bit IHL +3F63A530 subk 5,a5 ; jrls ... ; IHL >= 5 words +3F63A5B0 length checks (0x7c, 0x28) +3F63A660 calla 0x3F6B0160 ; pull the header up / trim +3F63A700 *0x97630 += 1 ; TCP error counter +``` + +Ordered call sequence for the first TCP segment (a SYN to port 6000) taken from +`rttcp2.tr` — 8,835 traced instructions, 233 calls: + +``` +3F63A980 -> 3FB2C000 htons (ports) +3F63AA20 -> 3FB2C290 <<< TCP checksum verification +3F63B160 -> 3F6AC3C0 lock +3F63B360 -> 3FB44400 queue/memory helper +3F63B510 -> 3FB44400 +3F63B670 -> 3FB2C140 compare helper +3F63B6F0 -> 3FB2C140 compare helper +3F63B790 -> 3FB2C000 htons +3F63B830 -> 3FB2C000 htons +3F63B9F0 -> 3F61E490 <<< TCB / connection list walk +3F63BC80 -> 3F6BE610 +3F6500E0 -> ... +3F653230 -> 3F61B000 list operation +3F653320 -> 3F64BC70 +3F652630 -> 3F635910 connection processing +``` + +Per-edge counts over all 44 segments: + +``` +44 3F63A980 -> 3FB2C000 htons +44 3F63AA20 -> 3FB2C290 TCP checksum +44 3F63B670 -> 3FB2C140 compare +44 3F63B6F0 -> 3FB2C140 compare +44 3F63B790 -> 3FB2C000 htons +44 3F63B830 -> 3FB2C000 htons +44 3F63B9F0 -> 3F61E490 TCB list walk +33 3F6431F0 -> 3F635910 per-connection processing +33 3F6432D0 -> 3F645170 +22 3F642BF0 -> 3F6ADF20 logging +22 3F640EA0 -> 3F6C30F0 +11 3F63B160 -> 3F6AC3C0 lock +11 3F63C450 -> 3F61AB30 +11 3F63C760 -> 3F61BFC0 +11 3F63C8C0 -> 3F64A710 +11 3F63CA90 -> 3F643C50 +11 3F63F780 -> 3F6448E0 +``` + +So every segment gets ports byte-swapped and a checksum verified, is matched +against the connection table by the list walk `0x3F61E490`, and 11 of the 44 +went on to the data-delivery path (`0x3F61AB30`, `0x3F61BFC0`, `0x3F6448E0`) +while 33 took the connection-state path. + +### 23.5 UDP input — `0x3F6540C0` .. `0x3F655070` + +Head of the function (`mmtm sp,a3,a5,a7,a9,a10,a11,a13`), and the calls it makes +for all 12 datagrams in the same trace: + +``` +3F654440 -> 3FB2C000 htons (ports) x12 +3F6547F0 -> 3FB2C290 <<< UDP checksum x12 +3F654AA0 -> 3F61E490 <<< socket/PCB list walk x12 +3F654B40 -> 3F61AB30 socket delivery prep x12 +3F654C60 -> 3F61EC00 deliver to socket x11 +3F655000 -> 3F6ADF20 log / drop x1 +``` + +12 datagrams, 11 delivered to a bound socket, 1 unmatched (the probe sent to a +closed port) and logged. The same checksum helper `0x3FB2C290` verifies UDP here +as it verifies ICMP and TCP — one ones-complement routine for all three. + +### 23.6 Where the transport layer hands back to the driver + +Both TCP and UDP delivery paths funnel into the reply/output machinery of +section 22: `0x3F61EC00`/`0x3F61B000` lead to `0x3F622A40` (reflect), +`0x3F623350` (transport checksum), `0x3F62EA00` (IP transmit finalisation) and +the device method `*(device+0x160)`. A UDP datagram in `rttcp2.tr` shows exactly +that continuation: + +``` +3F654440 -> 3FB2C000 htons +3F6547F0 -> 3FB2C290 UDP checksum verify +3F654AA0 -> 3F61E490 socket lookup +3F654C60 -> 3F61EC00 deliver +3F61F350 -> 3F6AC3C0 lock +3F61FA50 -> 3FB2C000 htons +3F61FE80 -> 3F622A40 <<< reply: reflect/send path +3F623080 -> 3F623350 transport checksum +3F6238C0 -> 3F62EA00 <<< IP transmit finalisation +``` + +So the complete receive-to-transmit loop is closed: LANCE interrupt -> RX poll -> +IP protocol dispatch -> transport input -> socket -> output -> IP transmit -> +device method -> ring -> `TDMD`. + +### 23.7 Repro and one tooling trap + +Read the dispatch tables from a RAM dump: + +```sh +python3 - <<'PY' +import struct +d=open('vt1200-ram-5500.bin','rb').read() +tab=d[0x1E0F70>>3:(0x1E0F70>>3)+256] +for p in (1,6,17,0): + idx=tab[p]; blk=0xD6BE0+idx*0x170 + h=struct.unpack(">3)+10:(blk>>3)+14])[0] + print("proto %d idx %d block %#x handler %#x" % (p,idx,blk,h)) +PY +``` + +Trap worth recording: a **stale MAME instance keeps the tap device** and happily +answers pings and TCP connects on `192.168.99.2`. Two runs produced traces with +zero network activity while the host was successfully connecting — the traffic +was going to an orphaned instance that was not the one being traced. Check +`pgrep -af mame0289/vt1200` before trusting a "no packets" trace. + +## 24. The TCP state machine + +The TCP input function of section 23.4 (`0x3F63A340`..`0x3F643C40`) contains the +state machine: one state word in the TCB, two jump tables over it, and the +transition code. All evidence below is ROM disassembly plus execution counts +from `rttcp2.tr` (44 TCP segments over 11 completed connections). + +### 24.1 The state field + +The state is a 16-bit word at **TCB bit offset `0x40`** (byte 8). Every write to +it in the TCP input function, with its immediate: + +| site | value written | state | +|---|---|---| +| `0x3F63BE90` | `movk 1` | LISTEN | +| `0x3F63D980` | `movk 3` | SYN_RECEIVED | +| `0x3F63D4F0` | `movk 4` | ESTABLISHED | +| `0x3F63F740` | `movk 4` | ESTABLISHED (from the SYN_RECEIVED handler) | +| `0x3F6413F0` | `movk 10` | TIME_WAIT | + +and the read that drives both dispatchers is the same field: + +``` +3F63F290 a8 = *(a11+0x40) ; state +3F63F2B0 subk 3,a8 +3F63F2C0 cmpi 7,a8 ; jrhi default ; states 3..10 index the table +``` + +### 24.2 Two dispatch tables over states 3..10 + +Table A at `0x3F63F350` (reached at `0x3F63F340`), table B at `0x3F6417C0` +(reached at `0x3F6417B0`). Both are 8 entries of 4 bytes, indexed by +`state - 3`, i.e. states 3..10. Read from the ROM image: + +| state | table A | table B | +|---|---|---| +| 3 SYN_RECEIVED | `0x3F63EFE0` | **`0x3F63F570`** | +| 4 ESTABLISHED | `0x3F63F060` | **`0x3F63F980`** | +| 5 CLOSE_WAIT | `0x3F63F060` | `0x3F63F980` | +| 6 FIN_WAIT_1 | `0x3F63F060` | `0x3F63F980` | +| 7 CLOSING | `0x3F63F200` | `0x3F63F980` | +| 8 LAST_ACK | `0x3F63F200` | `0x3F63F980` | +| 9 FIN_WAIT_2 | `0x3F63F060` | `0x3F63F980` | +| 10 TIME_WAIT | `0x3F63F200` | `0x3F63F980` | + +States **0, 1, 2** (CLOSED, LISTEN, SYN_SENT) fall out of both tables through +`jrhi` (`state-3` is unsigned-large): table A's default is `0x3F63F450`, table +B's default is `0x3F6418C0`. + +### 24.3 What actually executed + +``` +3F63F2C0 / 3F63F340 (table A dispatch) 0 <- never: every state seen was < 3 +3F63F450 (table A default) 33 +3F641730 / 3F6417B0 (table B dispatch) 33 +3F6417B0 targets: 3F63F570 x11 <- SYN_RECEIVED + 3F63F980 x22 <- established-family states +3F63F570 (SYN_RECEIVED handler) 11 +3F63F980 (established handler) 33 +``` + +The per-segment flow is a perfectly repeating 4-segment cycle (11 connections +× 4 = 44 segments): + +``` +seg 0 alloc/copy -> table-B default -> alloc/copy -> tcp-output -> alloc/copy +seg 1 A-default -> B:SYN_RECEIVED -> B:other -> B-default -> alloc -> output -> alloc +seg 2 A-default -> B:other -> ... (data, several outputs) +seg 3 A-default -> B:other +``` + +and the state writes inside those segments: + +| segment | state write observed | count | +|---|---|---| +| 0 (SYN) | `0x3F63BE90` → state = **LISTEN** | 11 | +| 1 (handshake ACK) | `0x3F63F740` → state = **ESTABLISHED** | 11 | +| 2 (data) | none; data counter `0x3F63FA90` | 11 | + +So a connection is seen going **CLOSED → LISTEN → (SYN_RECEIVED) → ESTABLISHED**, +11 times, which is exactly the number of completed TCP connections in that run. + +### 24.4 The handlers + +**SYN_RECEIVED — `0x3F63F570`** (state 3, the path that completes the handshake): + +``` +3F63F570 a7 = *(a11+0x120) ; TCB send-sequence field +3F63F590 a5 = *(a9+0x0E0) ; segment sequence field +3F63F5B0 cmp ; jrgt 0x3F643460 ; out of window -> drop +3F63F5E0 a7 = *(a9+0x0E0) ; a5 = *(a11+0x280) ; cmp ; jrgt drop +3F63F650 *0x972B0 += 1 ; SYN_RECEIVED segment counter (x11) +3F63F6F0 calla 0x3F6BD9F0 +3F63F720 movk 4,a7 +3F63F740 *(a11+0x40) = 4 ; <<< state := ESTABLISHED (x11) +3F63F780 calla 0x3F6448E0 ; send (ACK) +3F63F7C0 window arithmetic on *(a11+0xC0) +``` + +**Established family — `0x3F63F980`** (states 4,5,6,7,8,9,10): + +``` +3F63F980 a7 = *(a9+0x0E0) ; a5 = *(a11+0x120) ; cmp ; jrgt 0x3F6400C0 +3F63F9E0 a7 = *(a9+0x50) ; jrne 0x3F640070 ; segment length/flags +3F63FA20 a7 = *(a9+0x110) ; a5 = *(a11+0x1E0) ; cmp ; jrne 0x3F640070 +3F63FA90 *0x97790 += 1 ; data segment counter (x11) +3F63FB00 a5 = *(a11+0x50) ; jreq ... +``` + +**States below SYN_RECEIVED — `0x3F6418C0`** (table B default; the SYN path): + +``` +3F6418C0 btst 4,a12 ; segment flag bit 4 = ACK +3F6418D0 jreq 0x3F641DB0 +3F6418E0 a7 = *(a11+0x180) ; a5 = *(a9+0xC0) ; cmp ; jrlt 0x3F641AD0 +3F6419A0 a7 = *(a11+0x1A0) ; a5 = *(a9+0xE0) ; cmp ; jrlt 0x3F641AD0 +... +``` + +### 24.5 TCB fields used by the machine + +Only the fields the state code actually touches are listed; `a11` = TCB, +`a9` = parsed segment: + +| TCB offset (bit) | use | +|---|---| +| `0x40` (byte 8) | state (24.1) | +| `0x50` (byte 10) | flags/queue marker (`jreq` gates in `0x3F63F980`) | +| `0xC0` (byte 24) | sequence field used in window arithmetic | +| `0x120` | send-sequence, compared against segment `+0xE0` | +| `0x180` | receive-sequence, compared against segment `+0xC0` | +| `0x1A0` | receive-sequence, compared against segment `+0xE0` | +| `0x1E0` | compared against segment `+0x110` | +| `0x280` | send-sequence bound, compared against segment `+0xE0` | +| `0x2A0` | alternative bound (min/max selection) | +| `0x2D0` | store in the SYN path | + +Segment (not TCB) fields: `+0x50` length, `+0xC0` and `+0xE0` sequence words, +`+0x110` acknowledgement/sequence word. + +Two 32-bit sequence spaces are maintained in the TCB and validated against the +segment before any state change — the jump to `0x3F643460` on failure is the +drop path. + +### 24.6 The two helper calls seen in every segment + +``` +0x3F635910 "alloc/copy" 44 (from 0x3F652630, once per segment) + 33 (from 0x3F6431F0), 33 (from 0x3F6489A0) +0x3F645170 "tcp output" 33 (from 0x3F6432D0), 22 (from 0x3F653A10) +``` + +`0x3F635910` allocates from a pool at bit `0x97870`: index from `0x97D70`, +address = `0x97870 + index*(1024+256)` bits (160-byte records), then fills +`+0x20`, `+0x30`, `+0x40` and copies 26 words to `+0x1B0`. `0x3F645170` reads +`*(tcpcb+0x100)` → `+0xC0`, then `+0x280`, `+0x120`, `+0x140`, `+0x1E0`, +`+0x2A0` and a flag byte at `+0xD0` — the send/window arithmetic of an output +segment. + +### 24.7 Counters seen in this path + +| cell | meaning | observed | +|---|---|---| +| `0x97590` | TCP segments received | 44 | +| `0x97630` | TCP input errors | — | +| `0x972B0` | segments handled in SYN_RECEIVED | 11 | +| `0x97790` | data segments handled in established states | 11 | +| `0x97D70` | pool index for `0x3F635910` | — | + +### 24.8 Still open in TCP + +The option parsing (MSS/window-scale) and the close path (`0x3F63F200`, +`0x3F6413F0` writing TIME_WAIT, never exercised by this traffic), the exact +meaning of the TCB fields beyond the sequence comparisons, and the socket +binding table behind `0x3F61E490`/`0x3F61EC00`. + +## 25. Interface list, and a correction about the "ARP table" + +### 25.1 The interface list at `0x96AC0` + +`0x96AC0` is the head cell of the interface list. The lookup `0x3F623180` +walks it and compares the requested address against `*(iface+0x100)`: + +``` +3F623200 a9 = *0x96AC0 +3F623230 jreq not-found +3F623240 a11 = *(a9+0x100) ; the interface's address object +3F623260 a7 = requested address +3F623280 cmp a7,a11 ; jrne next +3F6232A0 return a9 ; found +3F6232C0 a9 = *(a9+0x200) ; jrne loop ; <<< next interface +``` + +Read from the RAM dump (this boot ran the MAME harness, so the address is the +harness's own `192.168.99.2`; the structure is what matters): + +``` +iface at bit 0x7AA060: + +0x00 (byte 0) u32 0x00000002 family = AF_INET + +0x04 (byte 4) u32 0x0263A8C0 address -> C0 A8 63 02 = 192.168.99.2 + +0x10 (byte 8) u32 0x00000002 family (second address) + +0x14 u32 0xFF63A8C0 broadcast -> 192.168.99.255 + +0x20 u32 0x001DA570 pointer to the address/flags object + +0x28 u32 0xC0A86300 network -> 192.168.99.0 + +0x2C u32 0xFFFFFF00 netmask -> 255.255.255.0 + +0x200 u32 0x00000000 next interface (single-interface boot) +``` + +The address/flags object at `0x1DA570` is what `0x3F622A40` inspects +(`*(*(iface+0x100)+0x40)`, bit 1) before sending a reply: that bit is the +interface's "usable" flag. + +### 25.2 Correction: `0x1E0570` / `0x1E0600` are a hex-digit table, not ARP data + +The routine `0x3F615610` was described in section 17 as an ARP/route table +serialiser. It is a **MAC-address formatter**. The evidence is the table +contents themselves: + +``` +bit 0x1E0600 (byte 0x3C0C0): + 30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 "0123456789abcdef" +``` + +and the loop: + +``` +a7 = *a9 >> 4 ; high nibble +a7 <<= 3 ; a7 += 0x1E0600 ; *a11++ = *a7 ; -> hex character +a7 = *a9++ & 0xF ; low nibble +a7 <<= 3 ; a7 += 0x1E0600 ; *a11++ = *a7 +*a11++ = 0x3A ; ':' separator +inc a10 ; cmpi 6,a10 ; jrlt loop +``` + +Six bytes, each rendered as two hex digits, separated by `:` — the terminal's +own MAC address in the form `08:00:2b:16:4b:5a`, which is exactly what the +Terminal Manager displays. The `arpre…`/`mods …`/`glev…` strings that follow in +the image are unrelated version headers (`in.c`, `in_pcb.c`). + +### 25.3 ARP: pointer to section 26 + +The ARP cache, the resolve path and the ARP input function **were** located +afterwards — see section 26. The earlier statement here ("no ARP entry +structure was found") was wrong because the entry is 16 bytes inside a hashed +bucket, not a standalone structure, and because the fragment searched for was +the gateway address in the wrong byte order. + +Consequence for the next pass: ARP is handled **below** IP, on the Ethernet +EtherType (0x0806) rather than the IP protocol byte, so it will not appear in the +dispatch table of section 23. The next thing to pin is the EtherType demux in +the receive path (`0x0800` vs `0x0806` vs others), which will also give the +`0x3F61FFA0`-equivalent entry for ARP. Note when searching for those compares +that `CMPI` stores its immediate one's-complemented (section 20), so `0x0806` +appears as `0xF7F9` and `0x0800` as `0xF7FF`. + +## 26. ARP: the cache and the input path + +Section 25.3 said the ARP cache was not found. That was wrong — it is found +below, and the reason the earlier searches missed it is recorded in 26.5. + +### 26.1 ARP is present (BSD strings) + +The ROM carries the usual BSD ARP diagnostics, which is what identifies the +implementation: + +``` +bit 0x3F6158D0 "arpresolve: no free entry" +bit 0x3F6159D0 "arp: ether address is broadcast for IP address ..." +bit 0x3F615C60 "...: %s." (log format used by the ARP code) +``` + +and the code that references them: + +``` +bit 0x3F610660 movi 0x3F6158D0 -> "arpresolve: no free entry" +bit 0x3F6120A0 movi 0x3F6159D0 -> broadcast-MAC warning +``` + +### 26.2 The ARP cache — 19 buckets × 9 entries + +`0x3F614200` is the lookup/allocate routine: + +``` +3F614240 a7 = 0xFFFF ; *(a13) = a7 ; "best slot" seed +3F614280 a7 = *0x1E0550 ; jreq continue ; ARP lock/flag cell +3F6142C0 *0x1E0550 = a11 +3F614300 calla 0x3F6FC200(0x95DB0, 0x3F60EE00) ; deferred work +L3F6143B0: +3F6143D0 a5 = *arg ; the IP address +3F6143E0 movk 19,a7 +3F6143F0 modu a7,a5 ; <<< hash = IP mod 19 +3F614410 a7 = a5 << 3 ; a7 += a5 ; *9 +3F614430 a7 <<= 7 ; *128 => *1152 bits = 144 bytes +3F614450 a10 = 0x1DAF60 + hash*1152 ; <<< ARP cache base +L3F614620 (9-iteration walk, a10 += 0x80 bits per step): + a7 = *(a10+0x58) ; byte 11: entry state + jreq -> empty slot, use it + btst 2,a7 ; jrne next ; bit 2: entry not usable + a7 = *(a10+0x50) ; byte 10: 16-bit aging/LRU field + cmp with the running best -> remember this entry +``` + +So the cache is **19 buckets keyed by `IP mod 19`, each holding 9 entries of +16 bytes** (9 × 16 = 144 bytes), based at bit `0x1DAF60`. + +### 26.3 The entry layout + +From the allocation path: + +``` +3F614820 a7 = arg (pointer to the IP address) +3F614840 *a10 = *a7 ; <<< entry +0 = IP address (32-bit) +3F614850 movk 1,a5 +3F614860 *(a10+0x58) = 1 ; <<< entry byte 11 = state (1 = incomplete) +3F614880 return a10 +``` + +and from the input path (`0x3F6152A0`, `0x3F6152D0`) the state byte at `+0x58` +and the 16-bit field at `+0x50` are updated when a reply arrives. MAC storage +sits in the bytes between (the broadcast check at `0x3F6120A0` compares the +sender's 6-byte address), but the exact offset was not pinned by this pass. + +### 26.4 ARP input — `0x3F6148E0` + +``` +3F6148E0 mmtm sp,a3,a5,a7,a9,a10,a11,a12,a13 +3F614920 a9 = arg +3F614950 a7 = *(a9) ; 16-bit field (ARP opcode area) +3F614960 subk 2,a7 ; jrne ... ; <<< test for opcode 2 (reply) +3F614980 a7 = *(a9+0x80) ; jreq ... +... +3F6152A0 *(a10+0x58) = a5 ; entry state +3F6152D0 *(a10+0x50) = a5 ; entry aging field +3F6154F0 subk 2,a8 ; second opcode test (request/reply split) +3F615800 cmpi 6,a10 ; 6-byte hardware-address loop +``` + +The `cmpi 6` at `0x3F615800` is the Ethernet-address length loop — the point +where the 6-byte MAC is copied into the cache entry. + +### 26.5 Why the earlier searches missed ARP — and what is still open + +Three searches came back empty, and each is a real fact worth recording: + +| search | result | +|---|---| +| `cmpi 0x806` anywhere in the ROM | **0 sites** (with the corrected one's-complement encoding) | +| word `0x00000806` in the frame-5500 RAM dump | **0 occurrences** | +| `cmpi 0x800` (IPv4 EtherType), 19 sites in ROM | **none execute** in either trace (`icmpw.tr`, `rttcp2.tr`) | + +So the Ethernet demux does **not** compare the EtherType with a literal +`cmpi`. ARP input `0x3F6148E0` is therefore reached by some other classification +— a table lookup or a masked compare — and that classification (the receive-side +EtherType demux) remains **the open item** in this area. Practical note for the +next attempt: the ARP input is only exercised when a peer has to be resolved, so +a trace that starts with an empty ARP cache (fresh nvram) and pings the terminal +from a new source address is the cheapest way to catch it. + +## 27. The one claim that needs real silicon, and how to test it + +Everything in this report is reachable from the ROM image and from MAME traces, +with two exceptions — both about the same claim: + +* **the transmit kick:** the firmware writes `CSR0 = shadow | TDMD`, never a bare + `TDMD` (section 10). MAME reproduces this: the loopback payload returns + `ok=1, rmd1=0x0300, mcnt=0x1C`. +* **the same payload on real silicon** completes `INIT → IDON → STRT` + (`CSR0 = 0x1B3`, then a stable `0x33`) but with the **bare-TDMD** kick the + transmitter never services the demand: `TDMD` stays set and the TX descriptor + stays owned (`TMD1 = 0x8338`). The corrected payload has **not** been run on + the machine yet. + +Status: **deferred** — the VT1200 is not available (taken off site), so this is +not waiting on a power cycle any more, it is waiting on the hardware being back. + +When it is, this is the whole procedure: + +```sh +# 1. the unit answers (this is the gate — without it, stop) +ping -c1 -W2 100.103.100.103 + +# 2. build the payload (COFF, loaded by the existing loader) +harness/udpstream/build-lance.sh + +# 3. run it through the loader, watching the marker output +# (same invocation used all session; LOADER_COFF is the payload) +# expected on success: +# ok=1 rmd1=0x0338 (OWN clear, STP|ENP) mcnt=0x001C txmd1=0x0338 +# failure signature to watch for: +# csr0 keeps bit 3 (TDMD) set, txmd1 keeps bit 15 (OWN) set -> 0x8338 +``` + +What to compare, in order: + +| observation | meaning | +|---|---| +| `CSR0` bit 3 (`TDMD`) clears | the chip serviced the demand — the whole question | +| `TMD1` loses bit 15 (`OWN`) | the descriptor was consumed | +| `RMD1 = 0x0338`, `MCNT = 0x001C` | the loopback frame came back: OWN clear, STP\|ENP, 28 bytes of data including the 4-byte FCS | + +If `TDMD` clears but no frame arrives, the remaining suspect is the loopback +setup rather than the kick, and `LANCE_LOOPBACK_REPORT.md` covers what MAME +does differently there (`RMD1_FRAM`). + +> **Correction — see section 41.2/41.3.** `CSR0 = 0x1BB` is +> `IDON|INIT|STRT|TDMD|TXON|RXON|INTR` with `ERR` **clear**; there is no memory +> error. The anomaly is `INIT` still being set while `IDON` is set, which points +> at the initialization sequence (the datasheet wants `STOP` set before `INIT`) +> rather than at the transmit kick. + +## 28. Where a received frame becomes an IP packet — and why ARP still will not run + +### 28.1 The ARP dispatcher has exactly one caller + +``` +CALLA 0x3F6148E0 (ARP input) <- 0x3F65A790 +``` + +`0x3F65A790` is inside `0x3F65A570`, a small dispatcher whose entry is +(see section 30.2: this dispatcher is **not** on the IPv4 receive path — it has +zero executions in every trace) + +``` +3F65A570 mmtm sp,a7,a9,a10,a11,a13 +3F65A590 move a14,a13 ; addk 32,a14 +3F65A5B0 jruc L3F65A7E0 +``` + +and whose body selects a handler by folding constants into one register: + +``` +L3F65A7E0 a8 = *(a13-0x40) ; the type word argument +3F65A800 addi 0x7FDB96E2,a8 ; jreq 0x3F65A660 +3F65A840 subk 2,a8 ; jreq 0x3F65A660 +3F65A860 subi 0x3FE3FFF4,a8 ; jreq 0x3F65A5C0 +3F65A8A0 subi 0x1C000B,a8 ; jreq 0x3F65A740 -> ARP input + default -> 0x3F65A8E0 (calls 0x3F65A0C0, returns 6) +``` + +This is a compare-chain on the EtherType done as a sequence of subtract-and-test +(the compiler's form for a `switch`), with **ARP as the fourth case**. The +literal 16-bit values could not be recovered reliably from the disassembler's +immediate printing (it prints `ADDI` immediates directly but `SUBI` ones +complemented in the same listing), so the mapping "case N = 0x0806" is +established structurally — by the ARP input call — rather than by reading the +constants. Recording that rather than guessing them. + +### 28.2 It never executes — and neither does any ARP traffic + +| trace | `0x3F65A570` (dispatcher) | `0x3F6148E0` (ARP input) | +|---|---|---| +| `icmpw.tr` (ping flood) | 0 | 0 | +| `rttcp2.tr` (TCP/UDP) | 0 | 0 | +| `arp2.tr` (fresh nvram, pings from a **new source address**) | 0 | 0 | + +The last run was built to force ARP: empty nvram directory, and a second address +`192.168.99.77/24` added to the tap interface so the terminal would have to +resolve a peer it had never spoken to. 60 of 60 pings from that address were +answered — and `tcpdump -i vt1200 arp` captured **zero** ARP packets (0-byte +pcap) for the whole exchange. + +Conclusion, and it is a limitation of the test bench rather than of the +firmware: **the terminal answers to the Ethernet source MAC of the request**, so +a peer that shares the tap's MAC never causes an ARP lookup. Exercising ARP +needs a second L2 peer with its own MAC — e.g. the MAME tap attached to a bridge +with a veth pair into a network namespace. That is a harness change, not a RE +question, and it is the prerequisite for finishing section 26. + +### 28.3 The queue that feeds the IP task + +The IP task `0x3F624980` does not look at Ethernet headers at all. It takes +already-classified packets from a queue: + +``` +3F624A20 a10 = *0x96AE0 ; queue head +3F624A70 a7 = *(a10+0x3E0) ; next +3F624A90 *0x96AE0 = a7 ; head advance +3F624BB0 a7 = *(a10+0x20) ; <<3 ; a7 += a10 ; buffer chain offset +3F624BF0 *(a13+0x20) = *(a7) ; first buffer +3F624C20 *(a10+0x20) += 4 ; consume 4 address units +3F624C80 *(a10+0x40) -= 4 ; remaining length +``` + +and the enqueue side: + +``` +3F6032A0 a9 = *0x96B20 ; queue count +3F6032D0 a11 = *0x96B40 ; cmp ; watermark check +3F603540 a11 = *0x96B00 ; tail +3F603570 *(a11+0x3E0) = a9 ; link +3F603590 *0x96B00 = a9 ; tail = new +3F6035F0 *0x96B20 = count + 1 +3F603630 *0x95FD0 |= 4 ; post the "IP input ready" event +``` + +So the receive path is: LANCE RX poll -> *classification* (the EtherType +dispatcher of 28.1 and its siblings) -> enqueue at `0x96AE0`/`0x96B00` -> +IP task `0x3F624980` -> protocol dispatch `0x3F6270F0` (section 23). + +The classification step itself is reached from `0x3F6A8670`, which is inside a +function that first tests a *byte* of the type word: + +``` +3F6A86D0 a11 = type >> 8 ; andi 0xFF +3F6A8710 cmpi 0x72,a11 ; 0x72 = 'r' +3F6A8730 jrne 0x3F6A87C0 +3F6A8780 calla 0x3F662410 +``` + +`cmpi 0x72` on a byte of the type field means the classification also handles +non-EtherType (character-coded) protocol ids, which is consistent with this +terminal running LAT/DECnet alongside IP. That is the next thing to pin: which +byte values map to which handler, and whether `0x0800` is even the discriminator +for IPv4 on this stack. + +## 29. Socket binding table, and the classifier constants decoded + +### 29.1 The binding lookup — `0x3F61E490` (best-match scoring) + +Called once per segment/datagram by both transports: + +``` +rttcp2.tr: 3F63B9F0 -> 3F61E490 x44 (TCP, every segment) + 3F654AA0 -> 3F61E490 x12 (UDP, every datagram) +``` + +The lookup walks a linked list and scores candidates — the BSD +`in_pcbconnect` pattern (score 3 = exact, less for each wildcard): + +``` +3F61E4F0 movk 3,a11 ; *(a13) = a11 ; score starts at 3 +3F61E510 a7 = arg1 ; list head +3F61E530 a9 = *a7 ; first entry +3F61E540 cmp a7,a9 ; jreq end ; empty list +L3F61E570: +3F61E570 a11 = *(a9+0xB0) ; <<< entry byte 22 = local port +3F61E590 a7 = arg(-0xA0) ; requested port +3F61E5B0 cmp a7,a11 ; jrne next ; port must match +3F61E600 a7 = *(a9+0x90) ; <<< entry byte 18 = local address +3F61E630 a7 = arg(-0x80) ; requested local address + (0 on either side = wildcard, score--, keep as fallback) +L3F61E750: +3F61E750 a11 = *(a9+0x60) ; <<< entry byte 12 = foreign address +3F61E780 a11 = arg(-0x40) ; requested foreign address + (again 0 = wildcard, score--) +L3F61EA20: +3F61EA20 a9 = *a9,1 ; <<< entry +0 = next pointer +``` + +So the binding-table entry (PCB) is: + +| offset (bit) | byte | field | +|---|---|---| +| `0x00` | 0 | next pointer | +| `0x60` | 12 | foreign address (0 = wildcard) | +| `0x90` | 18 | local address (0 = wildcard) | +| `0xB0` | 22 | local port | + +The specificity scoring (3 for a full match, decreasing as wildcards are +accepted) is what makes a bound socket win over a wildcard listener — visible in +the code as the `inc a11` / `*(a13+0x20)` score counter. + +### 29.2 Delivery to the socket — `0x3F61EC00` + +``` +rttcp2.tr: 3F654C60 -> 3F61EC00 x11 (11 UDP datagrams delivered) +``` + +``` +3F61EC00 mmtm sp,a7,a9,a10,a11,a13 +3F61EC50 a11 = arg +3F61EC70 setf 4 ; 4-bit field +3F61EC80 a7 = *a11 ; a9 = a7 << 2 ; packet field -> socket index +3F61ECB0 a7 = *0x1E0BF0 ; jreq ... ; debug/log gate +3F61ED50 log via 0x3F623B80 when set +3F61EDE0 a7 = arg(-0x40) ; subk 5 ; jreq 0x3F61EE70 ; special case 5 +3F61EE40 *0x96500 += 1 ; unmatched-datagram counter +L3F61EE70: +3F61EE80 a7 = *(a11+0x30) ; andi 0xFFFF9FFF ; option/flag bits 0x6000 +3F61EF00 a7 = *(a11+0x48) ; dsj ; byte 9 bit 7 (option flag) +3F61EF40 a7 = arg(-0x40) ; subk 5 ; jreq +3F61EF90 a7 = a9<<3 ; a7 += a11 ; a7 = *(a7) +3F61F010 subk 8,a7 ; jreq ... +3F61F070 subk 13,a7 ... +``` + +The socket struct fields this function reads are therefore: + +| offset (bit) | byte | field | +|---|---|---| +| `0x30` | 6 | option/flag word (mask `0x6000` tested) | +| `0x48` | 9 | option byte, bit 7 via `dsj` | +| index table at `a11 + (index<<3)` | | per-socket state words, compared with **8** and **13** | + +Two more counters belong to this layer: `0x96500` (unmatched datagrams) and the +state values 8 and 13 used by the delivery switch. The remaining offsets are resolved in section 31 — note that `+0x20` and +`+0x40` turn out to be stack slots, not structure fields. + +### 29.3 The classifier constants, decoded from the raw immediate words + +The chain at `0x3F65A7E0` is four subtract-and-test cases. The disassembler +prints `ADDI` operands directly but `SUBI` operands complemented, so both +readings were computed from the raw 32-bit immediate words: + +``` +ADDI imm = 0x7FDB96E2 (raw 0x96e2,0x7fdb — printed as-is) +SUBI imm = 0xC01C000B (raw 0x000b,0xc01c — printed 0x3FE3FFF4) +SUBI imm = 0xFFE3FFF4 (raw 0xfff4,0xffe3 — printed 0x001C000B) +``` + +| case | value compared (raw-immediate reading) | value (literal-printout reading) | +|---|---|---| +| 1 | `0x8024691E` | `0x8024691E` | +| 2 | `0x80246920` | `0x80246920` | +| 3 | `0x4040692B` | `0xC0086914` | +| 4 (the ARP branch) | `0x4024691F` | `0xC024691F` | + +**Neither reading produces `0x0800` or `0x0806`**, so the classifier's argument +is not a bare EtherType: it is an internal encoded value (the caller +`0x3F6A86D0` even tests a *byte* of it against `0x72`, section 28.3). That is a +real answer to the question rather than a guess — the constants are now known +exactly, and the remaining unknown is the *encoder*, i.e. the code that builds +this value from the frame header. That code is upstream of `0x3F6A8670` and is +the next thing to read. + +Attempted empirical shortcut, recorded so it is not repeated: sending crafted +ARP requests into the tap with `AF_PACKET` from a foreign MAC +(`aa:bb:cc:dd:ee:ff`) did **not** reach the guest — `tcpdump -i vt1200` showed +0 packets and the classifier never ran. Frame injection into a MAME tap needs +the bridge/veth arrangement of section 28.2, not a raw socket on the tap. + +## 30. The receive path has no EtherType demux — and the TCP close path + +### 30.1 The full receive chain, with one call per frame + +Tracing the receive side backwards from the IP task gives this chain, and every +link executes **exactly once per received frame**: + +| address | role | `icmpw.tr` | `rttcp2.tr` | +|---|---|---|---| +| `0x3F602E00` | RX entry: buffer state, re-arm | 344 | 56 | +| `0x3F9E0B80` | prepare the frame | 344 | 56 | +| `0x3F600200` | frame -> packet block (lengths 0x5EE/0x226/0x44C) | 344 | 56 | +| `0x3F603250` | enqueue + post "IP input ready" | 344 | 56 | +| `0x3F624980` | IP task | 344 | 56 | +| `0x3F6270F0` | IP **protocol** dispatch (section 23) | 344 | 56 | + +344 = 343 ICMP + 1 other, and 56 = 44 TCP + 12 UDP: the counts are the number of +**received frames**, not the number of IP packets, because every frame goes +through the same five functions regardless of its EtherType. And the chain +contains **no comparison against the EtherType**: + +* `0x3F600200` only works with frame lengths (`cmpi 0x5EE`, `0x226`, `0x44C`, + `0x200`, `0x400`) — it assembles the frame into a packet block, it does not + classify it; +* `0x3F602E00` checks a buffer state word (`*(buf+0x90)`) and otherwise just + re-arms and processes; +* `0x3F624980` (IP task) takes the packet from the queue and never looks at an + Ethernet header. + +**Conclusion: on the IPv4 receive path the firmware does not demux by +EtherType.** Every frame is handed to the IP parser, which computes its header +from a 4-bit field at the packet start (`0x3F6200A0`, `setf 4`) without even +checking that the version nibble is 4. Non-IP frames are therefore "parsed" as +IP and rejected later. That is consistent with everything else measured: no +`cmpi 0x0806` exists anywhere in the ROM (section 26.5), and all 19 `cmpi +0x0800` sites never execute (same section). + +### 30.2 What that means for the classifier at `0x3F65A570` + +Section 28 called `0x3F65A570` "the EtherType dispatcher" because it can call +the ARP input. It is **not** on the IPv4 receive path: + +``` +0x3F65A570 executions: icmpw.tr = 0 rttcp2.tr = 0 arp2.tr = 0 +``` + +So the four decoded case values (`0x8024691E`, `0x80246920`, `0x4040692B`, +`0x4024691F`, section 29.3) belong to a dispatcher that our traffic never +enters, and the IPv4 classification they might implement is not used for IPv4 +receive at all. What remains genuinely unknown is (a) the encoder of that +argument and (b) which traffic reaches this dispatcher — the `cmpi 0x72` +(`'r'`) test on a byte of it in the caller suggests a character-coded protocol +family (LAT/DECnet), not Ethernet types. + +### 30.3 The TCP close path — `0x3F63F200` and the TIME_WAIT entry + +The close-side handler that table A selects for states 7 (CLOSING), 8 +(LAST_ACK) and 10 (TIME_WAIT) is a teardown that hands off to a helper: + +``` +L3F63F200 push a11 +3F63F220 calla 0x3F64C730 ; close / abort +3F63F250 a11 = a8 +3F63F260 jruc 0x3F6438F0 ; common exit +``` + +and the TIME_WAIT entry itself: + +``` +L3F6413B0 a7 = *(a13+0x100) ; jreq 0x3F6418C0 ; only with a pending close +3F6413E0 movk 10,a5 +3F6413F0 *(a11+0x40) = 10 ; <<< state := TIME_WAIT +3F641410 calla 0x3F64E960 ; start the 2xMSL timer +3F641460 movi 0x78,a3 ; 120 +3F641490 *(a11+0x80) = a3 ; <<< TCB +0x80 = timeout value (120) +3F6414E0 calla 0x3F6BE2F0 ; schedule it +``` + +That assigns one of the fields section 29.2 left open: + +| field | meaning | +|---|---| +| TCB `+0x80` (byte 32) | timeout/timer value; 120 when entering TIME_WAIT | + +**None of this executed in any trace** — `0x3F63F200`, `0x3F63F060` (the +established-family handler of table A) and `0x3F6413F0` all have 0 hits, +because the workload never closed a connection. The close path is therefore +documented statically but unverified dynamically; forcing it needs a trace +where the peer sends FIN and then a second FIN (CLOSING) or simply idles out +(TIME_WAIT). + +## 31. PCB layout confirmed; the delivery state set; a correction + +### 31.1 The binding entry, field by field + +Re-reading the lookup with the argument slots resolved (the caller pushes five +arguments; `-0xA0` is the first, `-0x20` the last): + +| argument | meaning | compared against | +|---|---|---| +| `-0xA0` (arg1) | local port | entry `+0xB0` | +| `-0x80` (arg2) | local address | entry `+0x90` | +| `-0x60` (arg3) | foreign port | entry `+0x80` | +| `-0x40` (arg4) | foreign address | entry `+0x60` | +| `-0x20` (arg5) | flags | bit 0 tested at `0x3F61E990` | + +so the PCB is: + +| offset (bit) | byte | field | +|---|---|---| +| `0x00` | 0 | next pointer (`move *a9,a9` at `0x3F61EA20`) | +| `0x60` | 12 | foreign address | +| `0x80` | 16 | **foreign port** | +| `0x90` | 18 | local address | +| `0xB0` | 22 | local port | +| `0x40` | 8 | TCP state (section 24.1) | +| `0x80` | 32 | timer value, 120 in TIME_WAIT (section 30.3) — a different struct, the TCP control block | + +Note `+0x80` appears in both tables because they are different objects: `0x3F61E490` +matches a **binding** entry (port/address), while the TCP control block of +section 24 is the per-connection structure. Do not conflate them. + +Matching and scoring: + +``` +L3F61E570 if (entry.local_port != arg1) next + score = 3 + if (entry.local_addr != arg2) { if (arg2 != 0) next; } + if (entry.foreign_port!= arg3) next + if (entry.foreign_addr!= arg4) next +L3F61E940 if (score > best) { best = score; winner = entry } +L3F61EA20 next: a9 = *a9 +``` + +**Correction to section 29.1:** the offsets `+0x20` and `+0x40` listed there as +PCB fields were **stack slots** (`a13+0x20` is the caller's score word, +`a13-0x40` is an argument), not structure fields. The table above is the +verified layout. + +### 31.2 The delivery function suppresses a set of values (see section 39 — not proven to be socket states) + +`0x3F61EC00` reads a per-socket state word as `*(a11 + (index<<3))` and refuses +delivery when that word equals any of: + +``` +subk 5 ; subk 8 ; subk 13 ; subk 14 ; subk 15 ; subk 16 ; subk 17 ; subk 18 +``` + +so the "not deliverable" set is **{5, 8, 13, 14, 15, 16, 17, 18}** — a compiler +switch on small state values. The other fields this function touches: + +| offset | use | +|---|---| +| `+0x30` | option word, mask `0x6000` tested | +| `+0x48` | option byte, bit 7 via `dsj` | +| `+0x10` | 16-bit counter/sequence used later in the same function | +| `+0x20` | a second per-socket word read at `0x3F61FCF0` | +| `+0x40` | a per-socket value written at `0x3F61FCD0` | + +The exact *meaning* of 5 and 8 and of the 13..18 block is not established: the +enumeration lives in the socket layer, and the only states we can name from +evidence are the TCP ones (section 24.1). What is established is the suppression +logic itself, which is what determines whether a received segment reaches a +socket. + +### 31.3 The close-path trace attempt failed (recorded — superseded by section 32) + +A run was made to exercise the close path: 60 X11 connections to port 6000, half +closed with FIN and half with RST, under a real-time trace window +(frames 2400-9000). Result: + +``` +0x3F63A340 (TCP input) 0 +0x3F63F200 (close handler) 0 +0x3F6413F0 (TIME_WAIT) 0 +0x61FFA0 / 0x3F624980 0 <- not even the boot-time packets +``` + +The trace contains **no network activity at all**, i.e. the window closed before +the emulated stack processed anything: at the measured 53 fps this boot needs +more wall-clock time than the 124 s window allowed. The close path therefore +remained documented statically at that point; section 32 records the successful +rerun (bridge `vt1200` up, no stale `serve.py`) in which the TIME_WAIT block did +execute, and explains why `0x3F63F200` still did not. + +## 32. The TIME_WAIT path executed on live traffic + +Section 31.3 recorded a failed close-path run. This one worked, and the reason +the previous attempts failed was environmental, not architectural. + +### 32.1 Run setup (two traps worth recording) + +``` +bridge vt1200 must be UP before the run: + sudo ip link set vt1200 up +the bootp server must not already be bound: + sudo pkill -9 -f harness/serve.py +``` + +Both were hit: the first attempt of this kind showed `network up=0` because the +bridge `vt1200` was **down** (an earlier test of mine had taken it down), and a +stale `serve.py` from a crashed run held UDP/69, so the fresh server died with +`OSError: [Errno 98] Address already in use` and the guest never got an address. +The guest's side was fine all along — `serve.py` logged +`bootp: request from 08:00:2b:16:4b:5a -> 192.168.99.2`. + +With the bridge up and the port free: **150 of 150** X11 connections to port 6000 +succeeded (half closed with FIN, half with RST). + +### 32.2 The TIME_WAIT block ran + +Gating a trace on the TIME_WAIT write (`0x3F6413F0` → `trace`, `0x3F641520` → +`trace off`) produced `/tmp/opencode/closeB.tr`, 53,514 instructions. The block +is present verbatim in the trace: + +``` +3F641410 MOVE A14,-*SP,1 +3F641430 CALLA 3F64E960 ; 2xMSL timer / cleanup +3F641460 MOVI 78h,A3 ; 120 +3F641480 SETF 10h,1,0 +3F641490 MOVE A3,*A11(80h),0 ; <<< TCB +0x80 = 120 (confirmed executing) +3F6414C0 MOVE *A13(A0h),*A14+,1 +3F6414E0 CALLA 3F6BE2F0 ; schedule the timer +3F6BE2F0 MMTM SP,A7,A9,A11,A13 +``` + +So section 30.3's static reading is now confirmed dynamically: entering TIME_WAIT +sets the state word at `TCB+0x40`, writes the timeout value **0x78 (120)** to +`TCB+0x80`, and schedules the timer through `0x3F6BE2F0`. + +### 32.3 But `0x3F63F200` did not run — and that is informative + +No trace file was produced for the close handler gated on `0x3F63F200` → +`0x3F6438F0`, i.e. the close handler for states 7 (CLOSING), 8 (LAST_ACK) and 10 +(TIME_WAIT) never executed, even though 150 connections were closed. + +That refines the picture: a **passive close** (peer sends FIN, the terminal's +socket closes) enters TIME_WAIT through the state-4 handler at `0x3F6413B0`, +not through `0x3F63F200`. The `0x3F63F200` path is table A's handler for +states 7/8/10 — it needs a *simultaneous* close (FIN from both sides → +CLOSING), or a FIN arriving while already in LAST_ACK. Producing it requires the +client to send FIN, let the terminal send its own FIN, and then have the client +send a **second** FIN instead of ACKing — a TCP state the 150-connection test +never created. + +### 32.4 Status of the three "documented but unexecuted" items + +| item | state | +|---|---| +| TIME_WAIT entry `0x3F6413F0`, `TCB+0x80 = 0x78` | **confirmed on live traffic** (32.2) | +| CLOSING / LAST_ACK handler `0x3F63F200` | still static only — needs a simultaneous close | +| dispatcher `0x3F65A570` (four case values) | still static only — needs non-IP (LAT-family) traffic | + +## 33. Why `0x3F63F200` never fires — answered by measurement + +### 33.1 The forcing attempt + +A client that completes a real X11 handshake and then drives a passive close was +run against the live guest (bridge up, no stale `serve.py`): + +``` +mode=lastack ok=21 err=39 server_closed=21 # valid setup, then FIN, drain to EOF +mode=race ok=30 err=30 server_closed=0 # FIN immediately after connect +``` + +140 connection attempts in total, in both orders, with the trace gated on +`0x3F63F200` → `0x3F6438F0`. Result: **no trace file** — the handler did not +execute once. TIME_WAIT meanwhile fires every time (section 32). + +### 33.2 Which object each table is evaluated for + +A trace captured around a TIME_WAIT entry (`closeB.tr`) shows both dispatchers +running during one close: + +``` +3F63F280 (table A index setup) 1 <- evaluated once +3F63F2C0 (table A range check) 1 +3F63F450 (table A *default*) 3 <- taken, not the table +3F6416F0 (table B index setup) 3 +3F641730 (table B range check) 3 +3F63F980 (table B established) 3 +``` + +So table A *is* evaluated during a close, but its subject is always in a state +**below 3** (`subk 3` then `jrhi` → default `0x3F63F450`), while table B is the +one that dispatches on states 3..10 and drives the connection: + +``` +table B -> 0x3F63F980 (established family) -> ... -> 0x3F6413B0 (TIME_WAIT entry) +``` + +The object table A looks at is therefore the **listening** socket (state 1), +not the connection. That is why `0x3F63F350`'s states-7/8/10 handler +`0x3F63F200` is never reached: the only object that would need it is a +connection, and connections are dispatched through table B. + +### 33.3 Conclusion for this item + +`0x3F63F200` (CLOSING/LAST_ACK/TIME_WAIT handler of table A) is **not reachable +from TCP segment input in this configuration**: + +* 140 forced closes in both close orders — 0 executions; +* the table A dispatch was observed executing and taking its `state < 3` + default; +* `0x3F63F200` has 0 executions in every trace recorded in this whole + investigation (`icmpw.tr`, `rttcp2.tr`, `close.tr`, `closeB.tr`). + +It belongs to a path fed by a caller we have never entered. That is a bounded +negative result, not an unfinished experiment: the work it would do (teardown of +a connection in CLOSING/LAST_ACK) is done for real connections by the table B +path ending in `0x3F6413B0`, which *is* confirmed live. + +### 33.4 Two harness lessons from this run + +``` +* a SIGKILL of the shell wrapper leaves MAME running (the EXIT trap never runs); + always check `pgrep -af mame0289/vt1200` afterwards. +* MAME's Lua `print` is block-buffered when stdout is redirected to a file, so + "no f= lines in mame.log" does not mean the emulator did not run. +* the X11 server refuses roughly half the attempts (ok=21/60, ok=30/60), so a + close-path experiment needs a large attempt count to be meaningful. +``` + +## 34. Frame injection works — and it overturns the "ARP is separate" assumption + +### 34.1 Injecting on the bridge (not the tap) + +Sending raw frames with `AF_PACKET` **bound to the bridge `vt1200`** delivers +them to the guest, because the MAME tap is enslaved to that bridge and the +bridge floods to its port: + +``` +aa:bb:cc:dd:ee:ff > 08:00:2b:16:4b:5a ARP, Request who-has 192.168.99.2 tell 192.168.99.99 +08:00:2b:16:4b:5a > aa:bb:cc:dd:ee:ff ARP, Reply 192.168.99.2 is-at 08:00:2b:16:4b:5a +``` + +So the terminal **does** answer ARP for its own address from a sender it has +never met. Section 29.3's earlier failure was only the injection point: binding +`AF_PACKET` to the tap delivers nothing, binding it to the bridge works. + +### 34.2 But the ARP frame takes the IP path + +A trace of one complete frame-processing pass (`0x3F602E00` → `0x3F603780`, the +function's own epilogue) for an ARP-only injection contains exactly: + +``` +3F602E00 frame entry +3F9E0B80 prepare frame +3F600200 frame -> packet block +3F603250 enqueue to 0x96AE0 + post "IP input ready" (0x3F60BD40) +3F603780 rets +callees: 3F600200, 3F9E0B80, 3F60BD40, 3FB33870/D0 (lock/unlock), + 3F701230 (malloc), 3FB44400, 3FB41A30 +``` + +No ARP-specific call, and `0x3F6148E0` (the ARP input identified in section 26.4) +is **not** among them. An ARP frame is queued exactly like an IP frame, which +fits section 30.1's conclusion that the receive path does not demux by +EtherType. + +### 34.3 The default IP-protocol handler is a diagnostic dumper + +Since an ARP frame reaches the IP protocol dispatch with a garbage protocol byte, +the interesting handler is the default one, `0x3F634600` (section 23.2): + +``` +3F6346B0 a7 = *(packet+0x48) ; the byte at IP offset 9 +3F6346E0 *0x1E23C0 = a7 ; save it +3F634710 a5 = *(packet+0x80) ; *0x1E22D0 = a5 +3F634760 a5 = *(packet+0x60) ; *0x1E2350 = a5 +3F6347C0 push 0x1E22B0 / 0x1E2330 / 0x1E23B0 ; address variables +``` + +It copies header words into globals under `0x1E22B0..0x1E23C0` and logs — a +"last unknown packet" dump, not an ARP handler. + +### 34.4 What this leaves, precisely + +The ARP reply observed in 34.1 must be produced somewhere the traced pass does +not reach, i.e. **after** the frame is queued. The next step is therefore not +"find the EtherType demux" any more, but: trace the IP task `0x3F624980` for an +ARP frame (gate the trace on `0x3F624980`, stop at its epilogue) and see which +branch it takes instead of the protocol dispatch `0x3F6270F0`. Two candidates, +both consistent with the evidence so far: + +* the task validates the IP header and, on failure, inspects the frame's + EtherType itself (a path not yet read), or +* the reply is built from the queued block by a routine that never appears in the + frame pass because it runs in the task's context. + +Either way, section 26.4's claim that `0x3F6148E0` is *the* ARP input is now +doubtful: it is a function that parses ARP and writes cache entries, but it is +not reached by the observed reply. That claim is downgraded accordingly. + +## 35. ARP is answered — but not by `0x3F6148E0`; and ARP frames are parsed as IP + +### 35.1 The ARP reply is genuinely the firmware's + +With frame injection working (section 34.1), an ARP request from a foreign MAC is +answered. The reply cannot come from anywhere else: + +* the MAME driver has **no ARP code** (`grep -niE "\barp\b|0806" harness/mame/vt1200.cpp` + → nothing), so the reply is not a driver stand-in; +* the host owns only `192.168.99.1` (`ip addr | grep -c 192.168.99.2` → 0), so the + Linux kernel is not answering for `.2`. + +So the VT1200 firmware itself answers ARP — but not through the function this +report previously called "the ARP input". + +### 35.2 Correction: `0x3F6148E0` does not run + +A trace gated on `0x3F6148E0` → `0x3F615600` (its epilogue) produced **no trace +file** while four ARP requests were being answered: + +``` +sent 4 ARP requests only +ARP input did NOT fire +``` + +That is a direct contradiction of section 26.4, and it also explains the earlier +"0 executions in every trace": those traces contained no ARP traffic at all. +**Superseded by section 36**: the responder is `0x3F6119A0`, confirmed firing on +live ARP traffic. +`0x3F6148E0` is a function that manipulates ARP cache entries, but it is not the +code path that answers a request. + +### 35.3 ARP frames are fed to the IP parser and mis-parsed as ICMP + +The frame pass of section 34.2 already showed an ARP frame being enqueued to +`0x96AE0` like an IP frame. Tracing the IP task (`0x3F624980` → `0x3F6272C0`) +with an ARP frame shows what happens next: + +``` +3F6270F0 protocol dispatch +3F6271E0 CALL A5 -> 3F61FFA0 <<< the ICMP input handler +3F6206D0 ICMP checksum verification +3F6224E0 ICMP type dispatch +3F6212A0 echo-request handler (type 8: rewrites the type to 0) +``` + +So the ARP frame's "IP protocol byte" resolves to **1 (ICMP)** and its "type" +resolves to **8 (echo request)** — the stack then builds a reply from a frame +that is not IP at all. No such packet appears on the wire (the capture shows only +the ARP request/reply pair), so it is discarded later, most plausibly at the +checksum or route stage. + +This is a real, verifiable firmware behaviour: **the receive path has no +EtherType filter (section 30.1), so ARP frames are parsed as IP and only +discarded by the IP-layer sanity checks** — while the genuine ARP answer comes +from a path that has not yet been read. + +### 35.4 What to look for next + +The responder builds an ARP *reply*: a frame whose EtherType at offset 12 is +`0x0806` and whose opcode at offset 20 is 2. Two concrete leads: + +* the `arpresolve` family around `0x3F6105E0` and `0x3F6120A0` (the code that + references the ARP diagnostic strings) — the responder may live beside the + cache lookup rather than in the input function; +* a first-hit trace of the frame entry `0x3F602E00` with an ARP-only injection, + capturing the **first** indirect call, to see whether the frame is classified + before it reaches the queue at all (the fragment/block function `0x3F600200` + returns a value that `0x3F602E00` tests at `0x3F6031A0`; that branch has not + been read with an ARP frame in hand). + +## 36. The ARP responder found: `0x3F6119A0` — and it drives the same hardware path + +### 36.1 Identification, confirmed on live traffic + +Gating a trace on `0x3F6119A0` while four ARP requests were injected (and +answered) produced a **3.1 MB trace** (344,861 instructions). The function is the +ARP responder: its head byte-swaps the two ARP header words + +``` +3F6119A0 mmtm sp,a3,a5,a7,a9,a10,a11,a12,a13 +3F6119B0 a8 = *(packet+0x10) ; ARP ptype (byte 2) -> htons +3F611BD0 a8 = *(packet+0x30) ; ARP opcode (byte 6) -> htons +``` + +and the trace contains the request/response handling at `0x3F611E80` (the +broadcast-sender check that emits `arp: ether address is broadcast for IP …`). + +### 36.2 It sends through the same driver path as IP + +The ordered callees of one ARP pass: + +``` +3FB2C000 htons +3F942230 address compare/copy helper +3F6084E0 <<< the driver send of section 21 (called from 0x3F608450) +3F9DF6E0 task/queue helper +3F9DC890 <<< the driver submission wrapper +3F9D4BC0 <<< the driver command entry (ring arming) +``` + +with the call itself visible in the trace: + +``` +3F608410 MOVI 1DA570h,A7 +3F608440 MOVE A7,*A14+,1 +3F608450 CALLA 3F6084E0h ; clamp to 0x32, build request +3F6084E0 MMTM SP,A7,A9,A11,A13 +``` + +So an ARP reply is handed to the chip exactly like an IP packet: + +``` +ARP input 0x3F6119A0 -> 0x3F6084E0 -> 0x3F9DC890 -> 0x3F9D4BC0 + -> TX ring -> CSR0 = shadow | TDMD (section 10) +``` + +That matters beyond ARP: it means the `shadow | TDMD` kick is on the **only** +transmit path this stack has, for every protocol. There is no second way to get a +frame onto the wire. + +### 36.3 Corrections to sections 26.4 and 35.2 + +| function | status | +|---|---| +| `0x3F6148E0` (called "the ARP input" in 26.4) | **0 executions** with live ARP — not the responder | +| `0x3F614200` (ARP cache lookup, section 26.2) | **0 executions** in the ARP pass | +| `0x3F6120A0` (broadcast warning site) | **0 executions** in this pass | +| `0x3F6119A0` | **fires on every ARP request** — this is the responder | + +The cache lookup and the `0x3F6148E0` cache-entry writer are therefore *not* on +the reply path. The responder answers directly from the request it received, +which also explains why no ARP entry had to exist for the reply to work. + +### 36.4 Still open in ARP + +Where (if anywhere) the responder updates the cache: `0x3F614200` is not called, +so either it writes entries inline (the bucket arithmetic of section 26.2 +inlined into `0x3F6119A0`) or this firmware keeps no learned cache and only +answers. Deciding between those needs a trace of the responder's *writes* to +`0x1DAF60 + (ip mod 19)*1152`, not more traffic. + +## 37. The ARP responder does maintain the cache — 36.4 answered + +Section 36.4 asked whether `0x3F6119A0` stores cache entries, since it never +calls the cache lookup `0x3F614200`. It does store them, with the bucket +arithmetic **inlined**. Two sites inside the responder reference the cache base +`0x1DAF60`: + +``` +bit 0x3F612640 addi 0x1DAF60,a11 +bit 0x3F613300 addi 0x1DAF60,a11 +``` + +(The literal `0x001DAF60` appears at exactly six places in the ROM: +`0x3F60EF40`, `0x3F6101C0`, `0x3F612640`, `0x3F613300`, `0x3F614460`, +`0x3F614AE0` — the middle two are inside the responder, the last two in the +lookup/entry-writer pair of section 26.) + +### 37.1 The search inside the responder + +``` +3F612600 a7 = hash ; (IP mod 19) * 9 * 128 +3F612620 a11 = a7 +3F612630 a11 += 0x1DAF60 ; bucket base +3F612660 a12 = 0 +L3F612670: +3F612670 a7 = *a11 ; entry+0 = cached IP +3F612680 a5 = *(a13+0x140) ; the sender IP from the request +3F6126A0 if (a7 == a5) -> found +3F6126C0 a12++ +3F6126D0 a11 += 0x80 ; next entry (16 bytes) +3F6126F0 if (a12 < 9) loop +``` + +identical in structure to the lookup of section 26.2: 19 buckets, 9 entries of +16 bytes, keyed on the IP at entry+0. + +### 37.2 The write + +``` +3F612760 if (a11 == 0) -> no slot (0x3F612AA0) +3F612790 movk 6,a7 ; 6-byte copy +3F6127C0 a7 = a11 + 0x20 ; entry byte 4 <<< the MAC field +3F612820 calla 0x3FB44400 ; memcpy(6) +3F612860 a7 = *(a11+0x58) ; entry byte 11 = state +3F612880 btst 1,a7 ; jrne ... ; state bit 1 +3F6128A0 movk 1,a7 ; *(a13+0x200) = a7 ; flag: entry was created/updated +``` + +So on a request from an address already present (or the first free slot) the +responder copies the sender's **6-byte MAC into entry+4**, then updates the state +byte at entry+`0x58`. + +The second site (`0x3F613300`) walks the same buckets against a *different* +address (`*(a13+0x160)`), tests bit **3** of the state byte at `+0x58` +(`0x3F613480`), and then runs the same 6-byte copy — so the responder updates +entries for both addresses involved in the exchange (sender and target). + +### 37.3 Consequence + +The ARP cache is maintained by two code paths: + +| path | how | +|---|---| +| responder `0x3F6119A0` | inline bucket arithmetic, copies MAC into entry+4, sets entry state | +| lookup/allocator `0x3F614200` + writer `0x3F6148E0` | the pair documented in section 26, **not** called by the responder | + +That also explains section 26.2's "entry state 1 = incomplete on allocation": +state bit 1 / bit 3 distinguish a freshly created entry from a completed one, and +the responder is what completes them. + +Nothing here needed new traffic: the answer was in the ROM, and the earlier +`0x3F614200 == 0 hits` observation was correct but misleading — the responder +does not need that helper because it inlines the same arithmetic. + +## 38. `0x3F65A570` is not an Ethernet path — bounded negative + +### 38.1 The experiment + +Using the bridge injection that works (section 34.1), six EtherTypes were sent to +the guest, five frames each: + +``` +0x6003 DECnet/LAT 0x1234 0x8035 RARP 0x0800 (bogus IP body) 0x8888 0x0001 +``` + +The trace was gated on the dispatcher itself: + +``` +bpset 0x3F65A570 -> trace ; bpset 0x3F65B1B0 -> trace off +``` + +Result: + +``` +sent 0x6003 DECnet ... sent 0x0001 +dispatcher did NOT fire +``` + +### 38.2 Conclusion + +`0x3F65A570` is **not reachable from Ethernet input of any EtherType**. It is not +an EtherType demux, and its four case values +(`0x8024691E`, `0x80246920`, `0x4040692B`, `0x4024691F`, section 29.3) are not +EtherType codes. That is consistent with the one hard clue we had all along: its +caller `0x3F6A86D0` extracts a **byte** of the argument and compares it with +`0x72` (`'r'`), which is a character-coded protocol id, not a 16-bit EtherType. + +The dispatcher is an **internal protocol-id demux**, fed by something inside the +firmware (most plausibly the LAT/service layer originating its own messages), not +by frames arriving from the wire. Reaching it would require the firmware itself to +originate such a message — a UI-driven action on the terminal, not something that +can be injected at L2. + +### 38.3 What this changes in the report + +| earlier claim | now | +|---|---| +| 28: "the EtherType dispatcher" | superseded — not an Ethernet path | +| 29.3: "encoder unresolved, maps to ARP/IPv4?" | answered: no EtherType mapping exists | +| 30.2: "belongs to a dispatcher our traffic never enters" | confirmed, and now shown to be unreachable by any EtherType | + +Combined with sections 30.1 and 35.3, the picture of the receive side is +consistent and closed as far as the wire is concerned: **the firmware has no +EtherType demux at all** — frames go straight into the IP queue, IP-layer +sanitisation rejects non-IP, and the only non-IP protocol that answers (ARP) does +so from `0x3F6119A0` after the same queue, in the same way IP does. + +The remaining unknown is therefore *inside* the firmware's own protocol layer +(what feeds `0x3F65A570`), not at the network interface. + +## 39. The delivery suppression set never fires — and the "states" are not named + +### 39.1 Measurement + +In `rttcp2.tr` (44 TCP segments + 12 UDP datagrams, 11 delivered through +`0x3F61EC00`) the delivery function runs, but the state-suppression chain of +section 31.2 executes **zero** times: + +| site | meaning | executions | +|---|---|---| +| `0x3F61EC00` | delivery entry | 11 | +| `0x3F61EF60` | `subk 5` compare | **0** | +| `0x3F61F010` | `subk 8` compare | **0** | +| `0x3F61F070` … `0x3F61F250` | `subk 13 … subk 18` | **0** | +| `0x3F61F300` | common exit | 11 | + +So for ordinary, correctly delivered traffic none of the {5, 8, 13…18} values is +ever compared against — every delivery takes an earlier exit. Those values are +therefore *exceptional* states, not the normal states of a socket in service. + +### 39.2 What that does and does not establish + +* Established: the suppression set is not part of the normal data path, and a + workload with successfully delivered segments never reaches it. +* **Not** established, and previously over-claimed in section 31.2: that the word + being compared is a *socket state*. The function reads a 16-bit value from + `a11 + (index << 3)` where `index` comes from a 4-bit field at `*a11`, and the + object itself is passed as an argument by the UDP/TCP input paths. Whether + `a11` is a socket, a protocol entry or a descriptor table has not been proven, + and the values 5/8/13…18 have therefore no names — only a position in a compare + chain that our traffic never walks. + +Section 31.2 is thus downgraded from "the delivery function suppresses a set of +socket states" to "the delivery function contains an unreached compare chain over +values 5, 8 and 13…18 on a per-entry word". + +### 39.3 Why this is worth recording rather than chasing + +Three attempts in this investigation tried to name socket-layer values from +static structure alone (this one, the classifier constants in 29.3, and the ARP +input in 26.4) and two of them produced wrong answers that later had to be +withdrawn. The pattern is consistent: **this firmware's interior enumerations +cannot be named from the ROM alone** — they need a workload that takes the +branch, or a symbol source. For the socket layer specifically, the branch is only +taken for exceptional conditions (which is exactly what 39.1 shows), so the +honest state of that item is "known to exist, unreached, unnamed". + +What is *not* affected by this: every path that carries real traffic. Those are +documented with execution counts — the receive chain (344/56), the ICMP path +(343), the TCP state tables (11/22/33), ARP (`0x3F6119A0` firing on live +requests), and the single transmit sequence `0x3F6084E0 → 0x3F9DC890 → +0x3F9D4BC0 → shadow|TDMD`. + +## 40. Correction: the shadow is about INEA, not STRT — section 10 was wrong + +Section 10 claims that "CSR0's command bits are not set-only: writing `0x0008` +alone clears `STRT`, which stops the transmitter, and the demand is never +serviced". **The device model says otherwise**, and the claim has to be +withdrawn. + +### 40.1 The source + +`src/devices/machine/am79c90.cpp`, CSR0 write handler (register 0): + +```c +/* + * INIT, STRT, STOP, TDMD - read/write with 1 only + * TXON, RXON, INTR, ERR - read only + * INEA - read/write + * IDON, TINT, RINT, MERR, MISS, CERR, BABL - read/clear only + */ +... +// handle STRT +if ((data & CSR0_STRT) && !(m_csr[0] & CSR0_STRT)) +{ + m_csr[0] |= CSR0_STRT; + m_csr[0] &= ~CSR0_STOP; + ... + m_csr[0] |= CSR0_TXON; + m_transmit_poll->adjust(attotime::zero, 0, TX_POLL_PERIOD); +} + +// transmit demand +if ((data & CSR0_TDMD) && !(m_csr[0] & CSR0_TDMD)) +{ + m_csr[0] |= CSR0_TDMD; + m_transmit_poll->adjust(attotime::zero, 0, TX_POLL_PERIOD); +} + +// interrupt enable +if (!(m_csr[0] & CSR0_STOP) || type() == AM79C90) +{ + if (data & CSR0_INEA) m_csr[0] |= CSR0_INEA; + else m_csr[0] &= ~CSR0_INEA; +} +``` + +and `transmit_poll()`: + +```c +if (m_csr[0] & CSR0_TXON) +{ + m_csr[0] &= ~CSR0_TDMD; // demand is cleared when the poll runs + ... +``` + +Two facts follow directly: + +1. **`STRT` is set-only.** No path in the write handler clears it when a write + omits it. Writing a bare `TDMD = 0x0008` therefore leaves `STRT`/`TXON` set, + and the demand *is* serviced (`TDMD` is cleared by the poll). +2. **`INEA` is genuinely read/write.** A bare `TDMD` write has bit 14 clear, so + it **disables interrupts**. That is the real consequence of not using the + shadow. + +### 40.2 What the firmware's shadow is actually for + +The ROM fact is unchanged and still exact — `0x3F9D57F0` reads the shadow cell +`0x23CC50`, ORs in `TDMD`, and writes the result: + +``` +a5 = *0x23CC50 ; shadow of the CSR0 read/write bits (INEA among them) +a5 |= 0x0008 ; TDMD +*0x23CC30 = a5 ; write back: shadow | TDMD +``` + +That is a driver preserving the **read/write** bits (`INEA`) across a command +write — not a workaround for set-only command bits. The distinction matters, +because the two explanations predict different failures. + +### 40.3 Consequence for the real-silicon result + +The on-machine symptom was `CSR0 = 0x1BB` with `TMD1 = 0x8338` (descriptor still +owned, `TDMD` still set). Decoding `0x1BB` against the bit definitions: +`STRT`(1) = 1, `TDMD`(3) = 1, `TXON`(4) = 1 — i.e. **the transmitter was enabled +and the demand was raised, but the poll never ran**. "Bare TDMD cleared STRT" does +not describe that state at all; it would have shown `STRT`/`TXON` clear. + +So the open question on real hardware is *not* the shape of the CSR0 write (the +firmware's `shadow | TDMD` is correct and the corrected payload matches it), but: +**why did the chip not run a transmit poll with `TXON` and `TDMD` both set?** The +model cannot answer that — in the model a set `TDMD` always schedules the poll. +Candidate causes to test on the machine, in order: + +1. the transmit ring words in the init block were not what the chip latched + (ring base/length encoding, `CSR1/CSR2` written while `STOP` was clear); +2. the chip latched its init block from the wrong address (the init-block pointer + only loads when `INIT` is accepted, which requires `STOP`); +3. the descriptor as written is not accepted as owned by the chip (byte order of + the `OWN` bit / the 24-bit address split). + +### 40.4 Revert note + +If this correction is found to be wrong, the evidence to beat is the two source +blocks quoted above (`am79c90.cpp`, CSR0 `case 0:` and `transmit_poll()`), not the +earlier prose in section 10. Section 10 now carries a pointer here. + +## 41. Bare `TDMD` vs `shadow|TDMD`: identical in MAME — and what `0x1BB` really says + +### 41.1 The experiment + +The payload was built twice, differing only in the kick: + +``` +shadow: csr_w(0, CSR0_STRT | CSR0_TDMD); +bare: csr_w(0, CSR0_TDMD); +``` + +Both were run through the MAME loader probe on the same boot path: + +| payload | csr0a | csr0b | ok | rmd1 | mcnt | txmd1 | +|---|---|---|---|---|---|---| +| shadow `STRT\|TDMD` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` | +| bare `TDMD` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` | + +**Byte-for-byte identical.** Two conclusions follow, and both matter: + +1. The corrected CSR0 semantics of section 40 are confirmed empirically: in the + model a bare `TDMD` works exactly as well, because `STRT` is set-only and the + demand is serviced regardless. +2. **MAME never distinguished the two payloads.** The earlier statement "MAME + verifies the corrected `shadow|TDMD` kick" is void: what those runs verify is + the ring, descriptors, length encoding and DMA — not the shape of the CSR0 + write. The payload polls rather than using interrupts, so the one bit the + shadow actually protects (`INEA`) is irrelevant to this test. + +That also means the real-silicon failure cannot be attributed to the kick at all, +by either model or payload experiment. + +### 41.2 What `CSR0 = 0x1BB` actually decodes to + +Using the model's own bit definitions (`am79c90.h`): + +``` +CSR0_INIT 0x0001 CSR0_STRT 0x0002 CSR0_STOP 0x0004 CSR0_TDMD 0x0008 +CSR0_TXON 0x0010 CSR0_INEA 0x0040 CSR0_IDON 0x0100 CSR0_ERR 0x8000 +``` + +`0x1BB = 0x0100 | 0x00BB` = **IDON | INIT | STRT | TDMD | TXON | RXON | INTR**, +with **`ERR` clear** and **`INEA` clear**. + +The earlier note in section 27 guessed that `ERR` was set and that the poll had +failed on a DMA error. That guess was wrong — there is no error bit at all. + +### 41.3 The anomaly that is now the best lead (withdrawn — see section 47.2) + +`INIT` **and** `IDON` are both set, which the model treats as contradictory: +initialization sets `IDON` and clears `INIT`, and `INIT` is only re-asserted by a +new `INIT` write. With `TXON = 1`, `TDMD = 1` and no error, a chip that still +considers itself initializing is a plausible reason for the transmit poll never +running. + +The datasheet requirement quoted in the model's own comment is directly relevant: + +``` +* The STOP bit must be set prior to setting the INIT bit. +* Setting INIT clears the STOP bit. +``` + +~~Our payload writes `INIT` **without first setting `STOP`**.~~ **Withdrawn — see +section 42.1**: the payload does set `STOP` at the start of `main()`. The +`INIT`+`IDON` state remains unexplained, and a STOP-immediately-before-INIT +variant (`lance_stopinit.coff`) is byte-identical in MAME (section 42.2). + +### 41.4 Testable on the machine, in this order + +1. read `CSR0` immediately after the `INIT` write and after the `IDON` poll, to + see whether `INIT` clears (`0x1BB` shows it did not); +2. set `STOP` (`0x0004`) before writing `INIT`, per the datasheet, then `INIT`, + then poll `IDON`, then `STRT`, then `TDMD`; +3. if `TDMD` still sticks, read `CSR1`/`CSR2` back to confirm the chip latched the + init block address the payload thinks it wrote. + +Each of these is a register read the existing payload can already perform; no new +tooling is needed, only the VT1200 being available (section 27). + +## 42. STOP-before-INIT gives no observable difference — and 41.3 was wrong about our payload + +### 42.1 The payload already set STOP first + +Section 41.3 suggested the real-chip anomaly (`INIT` still set with `IDON`) might +be because "our payload writes `INIT` without first setting `STOP`". Reading the +payload shows that is **wrong**: `main()` does + +```c +csr_w(0, CSR0_STOP); /* line 131, first thing after the marker */ +... +csr_w(1, INITB >> 3); csr_w(2, (INITB >> 3) >> 16); csr_w(3, 0); +csr_w(0, CSR0_INIT); /* line 186 */ +``` + +so the datasheet order was already respected (with the whole descriptor/VRAM +setup in between). The hypothesis is withdrawn. + +### 42.2 A tighter variant, and its result + +The only meaningful variant left is `STOP` *immediately* before `INIT`: + +```c +csr_w(3, 0); +csr_w(0, CSR0_STOP); /* added: STOP immediately before INIT */ +csr_w(0, CSR0_INIT); +``` + +`lance_stopinit.coff` was built and run against the current payload: + +| payload | csr0a | csr0b | ok | rmd1 | mcnt | txmd1 | +|---|---|---|---|---|---|---| +| `lance.coff` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` | +| `lance_stopinit.coff` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` | + +Identical again. The model does not model any INIT/STOP ordering effect — which +matches its own source comment, where the ordering rule is explicitly described +as contradictory in the datasheet and not enforced: + +``` +* The STOP bit must be set prior to setting the INIT bit. +* ... +* This is clearly contradictory; driver code sets the STRT bit +* after INIT, so assume STRT does not require STOP to be set. +``` + +So neither ordering can be validated or invalidated in MAME. The +`INIT`+`IDON` state seen on real silicon is therefore still unexplained, but the +list of candidate causes has lost its "payload didn't set STOP" entry. + +### 42.3 What is left as a cause + +The remaining candidates all concern what the *chip* latched, not what the +payload wrote: + +1. `INIT` write accepted but the chip kept `INIT` set because the init-block DMA + read did not complete as expected (the model performs it synchronously; a + real chip reports errors through `MERR`, which is *clear* here, so this is + weaker); +2. the chip re-asserting `INIT` because the payload writes `INIT` again later + (there is a retry path at `lance.c:263`), and the real chip not clearing it; +3. a real-chip `CSR0` read-back quirk that leaves `INIT` visible while idle. + +None of these can be decided in MAME. They are register reads on the machine. + +### 42.4 Item closed for the offline phase + +`lance_stopinit.coff` exists, produces identical results, and the section 41.3 +hypothesis is withdrawn. The offline phase on the hardware question is now +exhausted: every variant that MAME can distinguish has been run, and the +remaining difference is a real-chip register state that the model does not +reproduce. + +## 43. The delivery word: addressing pinned, names still absent + +### 43.1 How the compared word is addressed + +The chain at `0x3F61EC00` reads a 16-bit word from a computed slot: + +``` +3F61EC70 setf 4,0,0 +3F61EC80 a7 = *a11 ; 4-bit field at the object's first 4 bits +3F61EC90 a9 = a7 << 2 ; index * 2 (kept shifted for the next step) +... +3F61EF90 a7 = a9 << 3 ; (index*2) << 3 = index * 16 bits +3F61EFA0 a7 += a11 ; -> a11 + index * 2 bytes ... see note +3F61EFB0 a7 = *a7,0 ; 16-bit read -> the compared word +3F61F010 subk 8,a7 +3F61F070 subk 13,a7 +... +``` + +so the value tested against {5, 8, 13…18} is a **16-bit entry in a table at the +object, selected by a 4-bit field stored at the object's own start**, with a +2-byte stride (`index * 2`). Whatever the object is, it carries a small tag and +an array of 16-bit words — that much is now exact. + +### 43.2 What the object is + +The caller builds the binding query from the object's own fields and then passes +the same object to the delivery function: + +``` +3F654980 a8 = *(a9+0xB0) ; port-like field +3F6549B0 push *(a9+0x80) +3F6549F0 push *(a9+0xA0) +3F654A20 push *(a9+0x60) +3F654A60 push 0x96E70 ; binding list +3F654AA0 calla 0x3F61E490 ; lookup -> result in a8, saved to a10 +3F654C50 push a9 ; <<< same object goes to 0x3F61EC00 +``` + +The offsets used (`+0x60`, `+0x80`, `+0xA0`, `+0xB0`) are the PCB-style +address/port fields of section 31.1, so the object is the **per-datagram +binding structure** — but whether it is a real PCB from the binding table or a +scratch copy built by the UDP input (which does copy five words of it into its +own frame at `0x3F654BD0`) is not settled by this. Recording the distinction +instead of picking one. + +### 43.3 No enumeration table exists in the ROM + +A table of the exact values would have named them. Searched the whole image: + +``` +16-bit 5, 8, 13, 14, 15, 16, 17, 18 -> 0 occurrences +byte 5, 8, 13, 14, 15, 16, 17, 18 -> 0 occurrences +16-bit 13, 14, 15, 16, 17, 18 -> 3 occurrences, all in data regions +``` + +So the values are produced by *storing* them, not by indexing a table. Finding +that store statically requires alias analysis of `a11`'s provenance, which is +exactly the technique that has produced two wrong answers in this area already +(26.4, 31.2). I am not going to guess a third time. + +### 43.4 The dynamic capture that should settle it + +The right tool is a memory dump from a breakpoint, using the register in the +address expression: + +``` +bpset 0x3F61EE70,1,"dump a11,64,/tmp/opencode/obj.bin" +``` + +That was attempted with six UDP probes to ports 7/9/6000/37/111. No file was +produced — either the breakpoint never fired (the probes may not have reached a +delivered socket in that run) or the `dump` action's address expression was +rejected. The recipe is recorded so the next attempt can iterate on it (check the +MAME debugger console for the action's own error output, and confirm the probe +actually lands on a bound socket first). + +Until that dump is captured, the values 5/8/13…18 stay **unnamed**, and section 39 +remains the accurate summary of this item. + +## 44. The delivery object is the PACKET, not a socket — captured on the wire + +### 44.1 The dump now works (two syntax bugs fixed) + +The earlier attempt failed for two reasons, both mine: + +``` +wrong: dump a11,64,/tmp/opencode/obj.bin ; filename must come FIRST +right: dump /tmp/opencode/obj.bin,a11,512 ; and length is in ADDRESS UNITS +``` + +`debugcmd.cpp` `execute_dump()` takes `params[0]` as the filename, `params[1]` as +the address and `params[2]` as the length in address units. With the TMS34010's +1-bit address unit, 64 bytes is **512** units. + +With the corrected command, a breakpoint at `0x3F61EE70` (inside the delivery +function, before the compare chain) dumped 64 bytes while 24 UDP datagrams were +being delivered. The file contains **our own datagram**: + +``` +45 00 11 00 3f 99 00 40 40 11 00 00 c0 a8 63 01 c0 a8 63 02 ... + 192.168.99.1 192.168.99.2 +... 70 72 6f 62 65 2d 31 31 0a "probe-11\n" +``` + +version/IHL `0x45`, TTL 64, protocol **17 (UDP)**, source `192.168.99.1`, +destination `192.168.99.2`, payload `probe-11`. + +### 44.2 So `a11` is the packet — and the lookup keys finally make sense + +`a11` in `0x3F61EC00` points at the received packet's IP header. Re-reading the +binding lookup (`0x3F61E490`) with that in hand, the query arguments are the +datagram's own header fields: + +| pushed at | reads | byte offset | meaning | +|---|---|---|---| +| `0x3F654980` | `*(a9+0xB0)` | 22 | UDP **destination** port | +| `0x3F6549B0` | `*(a9+0x80)` | 16 | IP **destination** address | +| `0x3F6549F0` | `*(a9+0xA0)` | 20 | UDP **source** port | +| `0x3F654A20` | `*(a9+0x60)` | 12 | IP **source** address | + +and the lookup compares them against the PCB fields of section 31.1: + +``` +arg1 (packet: dport) == entry +0xB0 ; the socket's LOCAL port +arg2 (packet: dst IP) == entry +0x90 ; the socket's LOCAL address +arg3 (packet: sport) == entry +0x80 ; the socket's FOREIGN port +arg4 (packet: src IP) == entry +0x60 ; the socket's FOREIGN address +``` + +That is exactly a UDP demux, and it validates the PCB layout of section 31.1 from +an independent direction. + +### 44.3 Corrections + +| claim | status | +|---|---| +| 43.2: "the object is a per-datagram binding structure / PCB-like" | **wrong** — it is the packet buffer | +| 31.2/39: "the compared word is a per-socket state" | **wrong anchor** — the word is read out of the packet | +| 31.1: PCB layout (`+0x60` foreign addr, `+0x80` foreign port, `+0x90` local addr, `+0xB0` local port) | **confirmed** by the query/compare pairing above | + +The compare chain's values (5, 8, 13…18) are therefore compared against **packet +header data indexed by a 4-bit field of the packet** (the dump shows that field +is the IP version nibble, `4`). They are not socket states, which is precisely +why the chain never matches for IPv4 traffic — the value it tests is a header +word, not a state code. Naming them as "states" was a mistake now withdrawn. + +### 44.4 What this leaves + +The delivery function's early part is now understood: identify the packet, find +the PCB by (dport, dst IP, sport, src IP) against the binding table, and hand the +payload to the socket. The compare chain is a **header-value** test whose +constants are still unnamed, and it does not run for ordinary traffic — which is +consistent with it being a diagnostic/exceptional branch rather than the normal +path. That is a coherent end state for this corner, and it is supported by a +memory dump rather than by inference. + +## 45. What the 5/8/13…18 compares actually test — a debug-gated header check + +### 45.1 The index arithmetic, exactly + +``` +3F61EC80 setf 4 ; 4-bit field size +3F61EC80 a7 = *a11 ; nibble of the packet's first byte +3F61EC90 a9 = a7 +3F61ECA0 a9 <<= 2 ; field * 2 +... +3F61EF80 a7 = a9 +3F61EF90 a7 <<= 3 ; field * 16 bits = field * 2 ... i.e. field*4 bytes after the <<2 +3F61EFA0 a7 += a11 ; slot = packet + field*4 bytes +3F61EFB0 a7 = *a7,0 ; 16-bit word there +3F61F010 subk 8,a7 +3F61F070 subk 13,a7 +3F61F0D0 subk 14,a7 ... through subk 18 +``` + +So the compared value is a **16-bit word at `packet + field*4 bytes`**, where the +field is a nibble of the packet's first byte. + +### 45.2 What that lands on, using the captured dump + +The section 44 dump of the same object (`a11` at `0x3F61EE70`) begins: + +``` +45 00 11 00 3f 99 00 40 40 11 00 00 c0 a8 63 01 c0 a8 63 02 ... +^ byte 0 = 0x45 (IPv4, IHL 5) +``` + +with `field` = `4` or `5` (the two nibbles of `0x45`) the slot lands at +`packet + 16` or `packet + 20` — i.e. on the **destination address** or the +**UDP source port** — not on any state word. Comparing those against 5, 8, 13…18 +can essentially never be true for real traffic. + +### 45.3 Why that is consistent with everything measured + +* the chain has **0 executions** in `rttcp2.tr` across 44 segments and 12 + datagrams (section 39.1, re-measured after this correction); +* the same function is guarded by a debug flag: at `0x3F61ECB0` it reads + `@0x1E0BF0` and, when set, logs through `0x3F623B80` before doing anything else + (the same gate appears in `0x3F622F10` and `0x3F623740`). + +~~Taken together, the 5/8/13…18 compares are best described as a debug-gated +validation branch over packet header data.~~ **Superseded by section 46**: the +debug flag only gates a log call; the chain is gated by `dsj` on the IP protocol +byte and the constants are ICMP query types. That is a different thing from the "socket state enumeration" +this report claimed in sections 31.2 and 39, and both are now superseded: + +| earlier claim | status | +|---|---| +| 31.2 "suppresses a set of socket states" | withdrawn | +| 39 "exceptional socket states, unnamed" | withdrawn | +| 43 "the per-entry word is a table slot on a PCB" | withdrawn (43.2: object is the packet) | +| 45 (this section) | debug-gated header-value test, never taken by normal traffic | + +### 45.4 Where the socket layer now stands + +Everything on the operational path is accounted for with evidence: + +``` +packet -> 0x3F61EC00: identify the datagram (packet buffer, section 44) + -> 0x3F61E490: match PCB by (dport, dst IP, sport, src IP) vs + (local port +0xB0, local addr +0x90, foreign port +0x80, + foreign addr +0x60) [section 31.1, 44.2] + -> deliver to the socket, or log an unmatched datagram (0x96500) + -> reply path 0x3F622A40 -> 0x3F623350 -> 0x3F62EA00 -> device +``` + +The unnamed constants were the last open item in this layer, and they are now +explained structurally rather than by name — which is the honest limit of a +ROM-only analysis of a debug branch that the running firmware never takes. + +## 46. The 5/8/13…18 constants are ICMP types — the chain is protocol-gated, not debug-gated + +Section 45 said the compare chain was "debug-gated". That was wrong: the debug +flag (`0x1E0BF0`) only gates a **log call**, and flipping it in RAM (verified this +turn: the flag was poked every 30 frames and the chain still did not run) changes +nothing. The real gate is one instruction earlier. + +### 46.1 The guard: `dsj` on the IP protocol byte + +``` +3F61EF00 a7 = *(a11+0x48) ; byte 9 of the IP header = PROTOCOL +3F61EF20 dsj a7, L3F61F300 ; decrement; jump if the result is nonzero +``` + +`DSJ` is decrement-and-jump-if-nonzero (`34010ops.hxx`): + +```c +#define DSJ(R) { if (--R##REG(DSTREG(op))) { m_pc += ...; } else { SKIP_WORD(); } } +``` + +so the fall-through — the path that computes the indexed word and runs the +compare chain — is taken **only when the protocol byte is 1**, i.e. **ICMP**. + +That single fact explains every count observed so far: + +| trace | protocol | `0x3F61EC00` | `0x3F61EF00` | chain (`0x3F61F010`…) | +|---|---|---|---|---| +| `rttcp2.tr` (UDP probes) | 17 | 11 | 11 | **0** — jumps away to `0x3F61F300` | +| `icmpw.tr` (echo requests) | 1 | 0 | 0 | **0** — echo requests are handled by `0x3F61FFA0` and never reach this function | + +### 46.2 The constants are the ICMP query types + +With the chain reached only for ICMP, the eight compared values are exactly the +ICMP types the stack answers itself: + +| value | ICMP type | +|---|---| +| 5 | redirect | +| 8 | echo request | +| 13 | timestamp request | +| 14 | timestamp reply | +| 15 | information request | +| 16 | information reply | +| 17 | address mask request | +| 18 | address mask reply | + +and the chain's action is to skip socket delivery (`jreq L3F61F300`) for those +types — because the stack answers them internally (section 22 shows the echo +request being rewritten and sent from the ICMP input path). + +The indexed word is therefore **the ICMP type byte of the packet**, reached via +the packet's own first nibble — which is why the values are single-digit types and +why nothing else ever matches. + +### 46.3 Corrections + +| claim | status | +|---|---| +| 31.2 / 39 "socket states 5/8/13…18" | withdrawn (already) | +| 45 "debug-gated header test" | **wrong gate** — the debug flag only gates logging; the gate is `dsj` on the protocol byte | +| 45.4 "constants remain unnamed" | **now named**: ICMP query types | + +### 46.4 Why this closes the item + +The chain is not a socket-state suppression set and not a debug path: it is the +**ICMP "answer it ourselves" test**, one `dsj` away from being skipped for every +non-ICMP packet. Every measurement now agrees with that reading — the UDP path +skips it 11 times out of 11, and the ICMP path never reaches this function at all +because echo requests are answered earlier. + +This is the first item in the report resolved by combining three independent +lines of evidence: the CPU core's `DSJ` semantics, the packet's protocol byte +(confirmed by the section 44 memory dump), and the exact constant set. + +## 47. Double INIT changes nothing — and INIT is set in MAME too + +### 47.1 The variant + +`lance_init2.coff` writes `INIT` **twice** (the second before the `IDON` poll), +on the theory that a second write while `m_idon` is still false would leave +`INIT` visible: + +| payload | csr0a | csr0b | ok | rmd1 | mcnt | txmd1 | +|---|---|---|---|---|---|---| +| `lance.coff` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` | +| `lance_init2.coff` | `0x7B3` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` | + +Identical again. That is the **fourth** payload variant with byte-identical +results, and it rules out the "second INIT leaves INIT set" theory in the model. + +### 47.2 The bit-by-bit difference between model and machine + +Comparing the model's `CSR0 = 0x7B3` with the real chip's `0x1BB`: + +| bit | name | MAME | real | | +|---|---|---|---|---| +| `0x001` | INIT | 1 | 1 | | +| `0x002` | STRT | 1 | 1 | | +| `0x004` | STOP | 0 | 0 | | +| `0x008` | TDMD | **0** | **1** | ← the demand was serviced only in the model | +| `0x010` | TXON | 1 | 1 | | +| `0x020` | RXON | 1 | 1 | | +| `0x040` | INEA | 0 | 0 | | +| `0x080` | INTR | 1 | 1 | | +| `0x100` | IDON | 1 | 1 | | +| `0x200` | TINT | **1** | **0** | ← no transmit completion on the machine | +| `0x400` | RINT | **1** | **0** | ← no receive on the machine | + +Three facts come out of one table: + +1. **`INIT` is set in the model too.** Sections 41.3 and 42.3 treated "INIT still + set" as a real-chip anomaly and built hypotheses on it. It is not an anomaly — + every payload run in MAME ends with `INIT` set. That hypothesis is withdrawn. +2. `STRT`, `TXON`, `RXON`, `IDON`, `INTR` agree exactly between model and machine: + the chip *was* started and *did* complete initialization. +3. The real differences are the **activity** bits: on the machine `TINT` and + `RINT` never fired and `TDMD` was never cleared, i.e. the chip accepted the + command set and then **did nothing at all** — no transmit poll, no receive. + +### 47.3 What that means + +The model clears `TDMD` at the top of `transmit_poll()` before it even looks at +the descriptor, so in MAME `TDMD = 0` is guaranteed once the poll is scheduled. +On the machine `TDMD` stayed set, so the poll was never scheduled. Since `TXON` +is set and `STRT` took effect, the remaining explanations are all about the +chip's internal scheduling rather than about the register write: + +* the chip was still inside its initialization sequence (both `INIT` and `IDON` + set — the model tolerates that state, the machine may not); +* the transmit poll period is derived from the chip clock and our modelled clock + choice is wrong (the driver builds the device with a clock that the real board + may not match), so the poll never comes due in the payload's timing window; +* the payload's 50 000-iteration busy-wait is far shorter on real silicon than + the poll period. + +The last one is testable **on the machine with no new tooling**: increase the +busy-wait after the `TDMD` write by an order of magnitude and see whether `TDMD` +clears. That is now the cheapest decisive experiment, ahead of any further +register archaeology. + +### 47.4 Status + +Four payload variants, all byte-identical in MAME; the model's `CSR0` compared +bit-for-bit with the machine's; the "INIT anomaly" withdrawn; and a concrete, +timing-based hypothesis added that the existing payload can test by waiting +longer. Real-silicon confirmation still needs the VT1200. + +## 48. Long wait: the model never leaves TDMD set (0 of 600 polls) + +### 48.1 The variant + +`lance_longwait.coff` keeps the normal init and kick, then waits **600,000 +iterations**, sampling `CSR0` every 1,000 iterations (600 polls) and recording: + +* `cell(0x1C0)` — `CSR0` after the wait +* `cell(0x1E0)` — how many of the 600 polls still saw `TDMD` set + +### 48.2 Result + +``` +lance_longwait: csr0a=0x7B3 csr0b=0x7B3 ok=1 rmd1=0x0300 mcnt=0x1C txmd1=0x8300 + csr0long=0x7B3 tdmdsamples=0x00000000 +lance (baseline): csr0a=0x7B3 csr0b=0x7B3 ok=1 rmd1=0x0300 mcnt=0x1C txmd1=0x8300 +``` + +**0 of 600 polls saw `TDMD` set**, and `CSR0` after the wait is identical to the +baseline. In the model the demand is serviced before the first sample — the +poll is scheduled at zero delay (`m_transmit_poll->adjust(attime::zero, ...)`) +and `transmit_poll()` clears `TDMD` at its top. + +### 48.3 What this closes + +Together with sections 41, 42 and 47, the offline phase is now complete on this +question, with five variants and one timing sweep: + +| payload | kick | init order | wait | TDMD left set? | +|---|---|---|---|---| +| `lance.coff` | `STRT\|TDMD` | STOP…INIT | 50 k | no | +| `lance_bare.coff` | `TDMD` | STOP…INIT | 50 k | no | +| `lance_stopinit.coff` | `STRT\|TDMD` | STOP,INIT adjacent | 50 k | no | +| `lance_init2.coff` | `STRT\|TDMD` | INIT twice | 50 k | no | +| `lance_longwait.coff` | `STRT\|TDMD` | STOP…INIT | 600 k, 600 polls | **no (0/600)** | + +All five return `0x7B3 / ok=1 / 0x0300 / 0x1C / 0x8300`. **No offline +configuration reproduces the machine's `TDMD`-stuck state**, so the difference is +a property of the real chip that the model does not implement — not something +further payload variants can reveal. + +### 48.4 The payload that answers it on the machine + +`lance_longwait.coff` is also the right instrument for the hardware run: it +already records exactly the two numbers needed, in the marker cells the loader +prints. + +``` +expected if the chip polls: csr0long = (TDMD clear), tdmdsamples small +expected if the chip never polls: csr0long = TDMD set, tdmdsamples = 600 +``` + +The intermediate case (`tdmdsamples` between 1 and 599, falling over time) would +mean the poll *is* running but slower than the payload's first samples — which +would immediately confirm the timing hypothesis of section 47.3. That is the +single most informative read available on the machine, and it needs no tooling +beyond running an existing build. + +## 49. Cross-check against Linux's LANCE driver (`drivers/net/ethernet/amd/7990.c`) + +An independent reference implementation was compared field by field with +`harness/udpstream/lance.c`. Linux's generic core is the right comparison: it is +the same chip family (Am7990/C-LANCE) driven on seven non-x86 buses. + +### 49.1 The sequences side by side + +Linux `lance_reset()` → `load_csrs()` → `lance_init_ring()` → +`init_restart_lance()`: + +```c +WRITERDP(lp, LE_C0_STOP); /* stop */ +WRITERAP(lp, LE_CSR1); WRITERDP(lp, leptr & 0xFFFF); +WRITERAP(lp, LE_CSR2); WRITERDP(lp, leptr >> 16); +WRITERAP(lp, LE_CSR3); WRITERDP(lp, lp->busmaster_regval); +WRITERDP(lp, LE_C0_INIT); /* initialize */ +for (i = 0; (i < 100) && !(READRDP(lp) & (LE_C0_ERR | LE_C0_IDON)); i++); +if ((i == 100) || (READRDP(lp) & LE_C0_ERR)) return -1; +WRITERDP(lp, LE_C0_IDON); /* <<< CLEAR IDON */ +WRITERDP(lp, LE_C0_INEA | LE_C0_STRT); /* start, interrupts on */ +``` + +| step | Linux `7990.c` | `lance.c` (ours) | note | +|---|---|---|---| +| STOP first | yes | yes (line 131) | same | +| CSR1 / CSR2 | init-block address | same | same | +| CSR3 | `busmaster_regval` (0 for little-endian, `LE_C3_BSWP` for big-endian; declance comments "should be zero") | `0` | matches the firmware's own value | +| INIT | yes | yes | same | +| poll | `ERR \| IDON`, 100 tries | `IDON` only, 20 000 tries | **ours ignores ERR** | +| **clear IDON** | **`WRITERDP(lp, LE_C0_IDON)`** | **not done** | **only real difference in ordering** | +| start | `INEA \| STRT` | `STRT` alone | interrupts irrelevant (we poll) | +| kick | `INEA \| TDMD` | `STRT \| TDMD` | equivalent command bits | +| init-block mode | `LE_MO_PROM` (0x8000) | `LE_MO_LOOP\|LE_MO_INTL` (0x0044) | loopback is the point of the test | +| TX descriptor length | `0xf000` then set at xmit | `0x0FE8` (24 B) | same negative-length convention | +| RX descriptor length | `-RX_BUFF_SIZE \| 0xf000` | `0x0A0A` (-1526) | same convention | +| OWN bit | set at transmit time | set at kick | same effect | + +### 49.2 The one substantive difference (weakened — see section 50.5) + +Linux **writes `IDON` back (clearing it) before writing `STRT`**. Our payload +leaves `IDON` set, and the machine's register snapshot was +`CSR0 = 0x1BB`, which contains **both `IDON` and `INIT` set** while `STRT`, +`TXON` and `TDMD` are also set. + +That combination is exactly what a chip looks like when it has latched the start +command but never left the "initialization done, not acknowledged" state: + +* `STRT`/`TXON` readable as set — the bit was accepted; +* `IDON` still pending because nobody acknowledged it; +* consequently the internal sequencer never enters normal operation, so the + transmit poll is never scheduled and `TDMD` is never cleared. + +The model cannot show this because `transmit_poll()` is scheduled on the `STRT` +write unconditionally and clears `TDMD` on its first tick. + +This is the first hypothesis derived from an independent implementation rather +than from the ROM alone, and it is cheap to test. + +### 49.3 Variant built to test it + +`lance_idonclear.c` / `lance_idonclear.coff` inserts, right after the `IDON` +poll and before the start: + +```c +csr_w(0, CSR0_IDON); /* clear IDON by writing 1, as Linux does */ +for (idle = 0; idle < 2000; idle++) { } +``` + +Everything else is unchanged. In MAME this should be a no-op (the model ignores +the acknowledgement), and on the machine it is the sequence Linux has used on +seven architectures. + +### 49.4 MAME result for the IDON-clearing variant + +| payload | csr0 | ok | rmd1 | mcnt | txmd1 | +|---|---|---|---|---|---| +| `lance.coff` | `0x7B3` | 1 | `0x0300` | `0x1C` | `0x8300` | +| `lance_idonclear.coff` | **`0x6B3`** | 1 | `0x0300` | `0x1C` | `0x8300` | + +`0x7B3 − 0x6B3 = 0x100` = `IDON`. So the acknowledgement takes effect exactly as +the datasheet describes, nothing else changes, and there is no regression — the +loopback still completes (`ok=1`, `rmd1=0x0300`, `mcnt=0x1C`). + +In MAME the two payloads differ only in that bit, because the model's +`transmit_poll()` is scheduled on `STRT` regardless. On the machine this variant +is the first that follows the sequence every Linux LANCE driver uses: + +``` +STOP -> CSR1/2/3 -> INIT -> wait IDON -> CLEAR IDON -> INEA|STRT -> TDMD +``` + +If `TDMD` clears with `lance_idonclear.coff` but not with `lance.coff`, the +missing `IDON` acknowledgement is the answer to the whole hardware question. If +it still does not clear, `TDMD`-stuck is independent of the init handshake and +the remaining candidates are the poll period / clock model (section 47.3). + +## 50. The descriptor length encoding was wrong — found by comparing drivers + +The NetBSD drivers, the Linux driver and the VT1200 firmware all agree on one +detail that our payload got wrong, and it explains the machine's symptoms better +than any earlier hypothesis. + +### 50.1 The rule: the length's top nibble must be ones + +NetBSD `sys/dev/ic/am7990reg.h`: + +```c +#define LE_XMD2_ONES 0xf000 +``` + +and `sys/dev/ic/am7990.c` writes every descriptor length as + +```c +rmd.rmd2 = -LEBLEN | LE_XMD2_ONES; /* receive */ +tmd.tmd2 = 0 | LE_XMD2_ONES; /* transmit, idle */ +tmd.tmd2 = -len | LE_XMD2_ONES; /* transmit, real length */ +... +len = -tmd.tmd2; /* read back */ +``` + +Linux `7990.c` says the same in comments: + +```c +ib->btx_ring[i].length = 0xf000; /* The ones required by tmd2 */ +ib->brx_ring[i].length = -RX_BUFF_SIZE | 0xf000; /* 0xf000 == bits that must be one */ +``` + +### 50.2 The VT1200 firmware obeys it too + +From this report's own disassembly: + +* RX arming `0x3F9D36C0` writes the length with `setf 12` and the value + **`0xFDDA`** = `0xF000 | 0x0DDA` -> 550 bytes, top nibble **ones** (section 10); +* the TX ring initialiser `0x3F9D2FF0` writes **`0xF000`** as the idle TX length + (section 10). + +So the firmware writes exactly the encoding NetBSD and Linux use. + +### 50.3 What our payload wrote, and why MAME hid it + +``` +lance.c (before): RX length 0x0A0A TX length 0x0FE8 +required: RX length 0xFA0A TX length 0xFFE8 (0xF000 | -len) +``` + +MAME's model **ORs the nibble in itself**: + +```cpp +virtual int get_buf_length(u16 data) const override +{ return (data == 0xf000) ? 4096 : -s16(0xf000 | data); } +``` + +so `0x0A0A` is silently turned into `0xFA0A` and our payload passes every MAME +test. Read literally, our values are **positive** (4072 and 2570) where the chip +expects a **negative** length — invalid for both directions. + +### 50.4 Why this explains the whole machine result + +A chip rejecting both descriptor sets produces exactly what was measured: + +| observation | explanation | +|---|---| +| `TDMD` still set, no `TINT` | the transmit descriptor is invalid, the transmit never completes | +| no `RINT`, no receive | the receive descriptors are invalid, nothing is received | +| `STRT`/`TXON`/`IDON` set, `ERR` clear | the chip is initialised and started, it just has nothing valid to do | + +That is a single cause for both missing interrupt bits, and it does not depend on +chip-internal behaviour the model does not implement. + +### 50.5 Correction to section 49.2 + +The `IDON`-clearing hypothesis is weakened: **NetBSD does not clear `IDON`** +either (`sys/dev/ic/lance.c` writes `LE_C0_INEA | LE_C0_STRT` immediately once +`IDON` is set). Two mainstream drivers disagree on that step and both work, so it +is not a requirement. The difference that matters is the length encoding, which +all three implementations agree on. + +### 50.6 Fixed payload + +`lance_fixlen.c` / `lance_fixlen.coff` writes `0xFA0A` and `0xFFE8`. In MAME it is +identical to the baseline (`0x7B3 / ok=1 / 0x0300 / 0x1C / 0x8300`), as it must +be, because the model masks the nibble. On the machine it is the first payload +whose descriptors match what the firmware, Linux and NetBSD all write. diff --git a/harness/udpstream/.gitignore b/harness/udpstream/.gitignore new file mode 100644 index 0000000..a35912c --- /dev/null +++ b/harness/udpstream/.gitignore @@ -0,0 +1,6 @@ +# DOSBox build scratch: extracted 1987 SDK binaries, not project source +work/ + +# generated listings/maps (the .coff payloads are committed) +build/*.lst +build/*.map diff --git a/harness/udpstream/build-lance.sh b/harness/udpstream/build-lance.sh new file mode 100755 index 0000000..72d769d --- /dev/null +++ b/harness/udpstream/build-lance.sh @@ -0,0 +1,93 @@ +#!/bin/sh +# Build the raw LANCE receive probe. +# +# harness/udpstream/build.sh +# +# .text at bit 0x018C0000, .bss at 0x01C00000 (both inside the firmware's large +# free block), entry _start, no .cinit (the runtime's autoinit pass does not +# run -- see harness/c-hello/README.md). +set -eu + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +DIR=$(cd "$(dirname "$0")" && pwd) +WORK="$DIR/work" +OUT="$DIR/build" +IMG="$ROOT/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img" +DOSBOX_CONF="$ROOT/harness/sdk-scratch/dosbox-bench.conf" + +TEXT=018C0000 +BSS=01C00000 + +mkdir -p "$WORK" "$OUT" + +if [ ! -x "$WORK/GSPCG.EXE" ]; then + mcopy -s -n -i "$IMG" "::/A2/*" "$WORK/" + mcopy -s -n -i "$IMG" "::/C2/*" "$WORK/" +fi + +SRC=${LANCE_SRC:-$DIR/lance.c} +cp "$SRC" "$WORK/LANCE.C" +cp "$DIR/romapi.h" "$WORK/ROMAPI.H" +cp "$DIR/entry.asm" "$WORK/ENTRY.ASM" + +{ + printf '%s\n' '-a' + printf '%s\n' '-o LANCE.out' + printf '%s\n' '-m LANCE.map' + printf '%s\n' '-e _start' + printf '%s\n' 'LANCE.obj' + printf '%s\n' 'ENTRY.obj' + printf '%s\n' '-l RTS.LIB' + printf '%s\n' 'MEMORY' + printf '%s\n' '{' + printf '%s\n' " prog: origin = ${TEXT}h, length = 20000h" + printf '%s\n' " ram: origin = ${BSS}h, length = 40000h" + printf '%s\n' '}' + printf '%s\n' 'SECTIONS' + printf '%s\n' '{' + printf '%s\n' ' .text: {} > prog' + printf '%s\n' ' .data: {} > prog' + printf '%s\n' ' .cinit: {} > prog' + printf '%s\n' ' .bss: {} > ram' + printf '%s\n' '}' +} > "$WORK/LANCE.CMD" +sed -i 's/$/\r/' "$WORK/LANCE.CMD" + +rm -f "$WORK"/LANCE.OBJ "$WORK"/LANCE.OUT "$WORK"/LANCE.LST \ + "$WORK"/LANCE.MAP "$WORK"/LANCE.ASM "$WORK"/LANCE.CPP \ + "$WORK"/LANCE.IF "$WORK"/ENTRY.OBJ "$WORK"/ENTRY.LST + +SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ +timeout 240 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \ + -c "mount c $WORK" -c "c:" \ + -c "gspcpp LANCE" \ + -c "gspcc LANCE" \ + -c "gspcg LANCE" \ + -c "gspa LANCE LANCE.obj LANCE.lst -l" \ + -c "gspa ENTRY ENTRY.obj ENTRY.lst -l" \ + -c "gsplnk LANCE.cmd" \ + -c "exit" -exit > "$WORK/dosbox.log" 2>&1 + +if [ ! -f "$WORK/LANCE.OUT" ]; then + echo "build.sh: GSPLNK produced no LANCE.OUT" >&2 + sed 's/\r$//' "$WORK/LANCE.MAP" 2>/dev/null | tail -20 >&2 || true + for f in LANCE.LST ENTRY.LST; do + sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' | grep -vi 'no errors' >&2 || true + done + exit 1 +fi +for f in LANCE.LST ENTRY.LST; do + if sed 's/\r$//' "$WORK/$f" | grep -i 'error' | grep -qvi 'no errors'; then + echo "build.sh: $f diagnostics:" >&2 + sed 's/\r$//' "$WORK/$f" | grep -in 'error' | grep -vi 'no errors' >&2 + exit 1 + fi +done + +OUTNAME=${LANCE_OUT:-lance.coff} +cp "$WORK/LANCE.OUT" "$OUT/$OUTNAME" +sed 's/\r$//' "$WORK/LANCE.MAP" > "$OUT/lance.map" +sed 's/\r$//' "$WORK/LANCE.LST" > "$OUT/lance.lst" +sed 's/\r$//' "$WORK/ENTRY.LST" > "$OUT/entry.lst" +echo "build.sh: $OUT/$OUTNAME ($(wc -c < "$OUT/lance.coff") bytes)" +grep -E "ENTRY POINT|^\.(text|cinit|bss)" "$OUT/lance.map" | head -8 diff --git a/harness/udpstream/build-tcp.sh b/harness/udpstream/build-tcp.sh new file mode 100755 index 0000000..ac8c822 --- /dev/null +++ b/harness/udpstream/build-tcp.sh @@ -0,0 +1,91 @@ +#!/bin/sh +# Build the TCP throughput probe (C + ROM-API thunks). +# +# harness/udpstream/build.sh +# +# .text at bit 0x018C0000, .bss at 0x01C00000 (both inside the firmware's large +# free block), entry _start, no .cinit (the runtime's autoinit pass does not +# run -- see harness/c-hello/README.md). +set -eu + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +DIR=$(cd "$(dirname "$0")" && pwd) +WORK="$DIR/work" +OUT="$DIR/build" +IMG="$ROOT/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img" +DOSBOX_CONF="$ROOT/harness/sdk-scratch/dosbox-bench.conf" + +TEXT=018C0000 +BSS=01C00000 + +mkdir -p "$WORK" "$OUT" + +if [ ! -x "$WORK/GSPCG.EXE" ]; then + mcopy -s -n -i "$IMG" "::/A2/*" "$WORK/" + mcopy -s -n -i "$IMG" "::/C2/*" "$WORK/" +fi + +cp "$DIR/tcpstream.c" "$WORK/TCPSTRM.C" +cp "$DIR/romapi.h" "$WORK/ROMAPI.H" +cp "$DIR/entry.asm" "$WORK/ENTRY.ASM" + +{ + printf '%s\n' '-a' + printf '%s\n' '-o TCPSTRM.out' + printf '%s\n' '-m TCPSTRM.map' + printf '%s\n' '-e _start' + printf '%s\n' 'TCPSTRM.obj' + printf '%s\n' 'ENTRY.obj' + printf '%s\n' '-l RTS.LIB' + printf '%s\n' 'MEMORY' + printf '%s\n' '{' + printf '%s\n' " prog: origin = ${TEXT}h, length = 20000h" + printf '%s\n' " ram: origin = ${BSS}h, length = 40000h" + printf '%s\n' '}' + printf '%s\n' 'SECTIONS' + printf '%s\n' '{' + printf '%s\n' ' .text: {} > prog' + printf '%s\n' ' .data: {} > prog' + printf '%s\n' ' .cinit: {} > prog' + printf '%s\n' ' .bss: {} > ram' + printf '%s\n' '}' +} > "$WORK/TCPSTRM.CMD" +sed -i 's/$/\r/' "$WORK/TCPSTRM.CMD" + +rm -f "$WORK"/TCPSTRM.OBJ "$WORK"/TCPSTRM.OUT "$WORK"/TCPSTRM.LST \ + "$WORK"/TCPSTRM.MAP "$WORK"/TCPSTRM.ASM "$WORK"/TCPSTRM.CPP \ + "$WORK"/TCPSTRM.IF "$WORK"/ENTRY.OBJ "$WORK"/ENTRY.LST + +SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ +timeout 240 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \ + -c "mount c $WORK" -c "c:" \ + -c "gspcpp TCPSTRM" \ + -c "gspcc TCPSTRM" \ + -c "gspcg TCPSTRM" \ + -c "gspa TCPSTRM TCPSTRM.obj TCPSTRM.lst -l" \ + -c "gspa ENTRY ENTRY.obj ENTRY.lst -l" \ + -c "gsplnk TCPSTRM.cmd" \ + -c "exit" -exit > "$WORK/dosbox.log" 2>&1 + +if [ ! -f "$WORK/TCPSTRM.OUT" ]; then + echo "build.sh: GSPLNK produced no TCPSTRM.OUT" >&2 + sed 's/\r$//' "$WORK/TCPSTRM.MAP" 2>/dev/null | tail -20 >&2 || true + for f in TCPSTRM.LST ENTRY.LST; do + sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' | grep -vi 'no errors' >&2 || true + done + exit 1 +fi +for f in TCPSTRM.LST ENTRY.LST; do + if sed 's/\r$//' "$WORK/$f" | grep -i 'error' | grep -qvi 'no errors'; then + echo "build.sh: $f diagnostics:" >&2 + sed 's/\r$//' "$WORK/$f" | grep -in 'error' | grep -vi 'no errors' >&2 + exit 1 + fi +done + +cp "$WORK/TCPSTRM.OUT" "$OUT/tcpstream.coff" +sed 's/\r$//' "$WORK/TCPSTRM.MAP" > "$OUT/tcpstream.map" +sed 's/\r$//' "$WORK/TCPSTRM.LST" > "$OUT/tcpstream.lst" +sed 's/\r$//' "$WORK/ENTRY.LST" > "$OUT/entry.lst" +echo "build.sh: $OUT/tcpstream.coff ($(wc -c < "$OUT/tcpstream.coff") bytes)" +grep -E "ENTRY POINT|^\.(text|cinit|bss)" "$OUT/tcpstream.map" | head -8 diff --git a/harness/udpstream/build.sh b/harness/udpstream/build.sh new file mode 100755 index 0000000..170f340 --- /dev/null +++ b/harness/udpstream/build.sh @@ -0,0 +1,91 @@ +#!/bin/sh +# Build the UDP frame-stream receiver (C + ROM-API thunks). +# +# harness/udpstream/build.sh +# +# .text at bit 0x018C0000, .bss at 0x01C00000 (both inside the firmware's large +# free block), entry _start, no .cinit (the runtime's autoinit pass does not +# run -- see harness/c-hello/README.md). +set -eu + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +DIR=$(cd "$(dirname "$0")" && pwd) +WORK="$DIR/work" +OUT="$DIR/build" +IMG="$ROOT/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img" +DOSBOX_CONF="$ROOT/harness/sdk-scratch/dosbox-bench.conf" + +TEXT=018C0000 +BSS=01C00000 + +mkdir -p "$WORK" "$OUT" + +if [ ! -x "$WORK/GSPCG.EXE" ]; then + mcopy -s -n -i "$IMG" "::/A2/*" "$WORK/" + mcopy -s -n -i "$IMG" "::/C2/*" "$WORK/" +fi + +cp "$DIR/udpstream.c" "$WORK/UDPSTRM.C" +cp "$DIR/romapi.h" "$WORK/ROMAPI.H" +cp "$DIR/entry.asm" "$WORK/ENTRY.ASM" + +{ + printf '%s\n' '-a' + printf '%s\n' '-o UDPSTRM.out' + printf '%s\n' '-m UDPSTRM.map' + printf '%s\n' '-e _start' + printf '%s\n' 'UDPSTRM.obj' + printf '%s\n' 'ENTRY.obj' + printf '%s\n' '-l RTS.LIB' + printf '%s\n' 'MEMORY' + printf '%s\n' '{' + printf '%s\n' " prog: origin = ${TEXT}h, length = 20000h" + printf '%s\n' " ram: origin = ${BSS}h, length = 40000h" + printf '%s\n' '}' + printf '%s\n' 'SECTIONS' + printf '%s\n' '{' + printf '%s\n' ' .text: {} > prog' + printf '%s\n' ' .data: {} > prog' + printf '%s\n' ' .cinit: {} > prog' + printf '%s\n' ' .bss: {} > ram' + printf '%s\n' '}' +} > "$WORK/UDPSTRM.CMD" +sed -i 's/$/\r/' "$WORK/UDPSTRM.CMD" + +rm -f "$WORK"/UDPSTRM.OBJ "$WORK"/UDPSTRM.OUT "$WORK"/UDPSTRM.LST \ + "$WORK"/UDPSTRM.MAP "$WORK"/UDPSTRM.ASM "$WORK"/UDPSTRM.CPP \ + "$WORK"/UDPSTRM.IF "$WORK"/ENTRY.OBJ "$WORK"/ENTRY.LST + +SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ +timeout 240 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \ + -c "mount c $WORK" -c "c:" \ + -c "gspcpp UDPSTRM" \ + -c "gspcc UDPSTRM" \ + -c "gspcg UDPSTRM" \ + -c "gspa UDPSTRM UDPSTRM.obj UDPSTRM.lst -l" \ + -c "gspa ENTRY ENTRY.obj ENTRY.lst -l" \ + -c "gsplnk UDPSTRM.cmd" \ + -c "exit" -exit > "$WORK/dosbox.log" 2>&1 + +if [ ! -f "$WORK/UDPSTRM.OUT" ]; then + echo "build.sh: GSPLNK produced no UDPSTRM.OUT" >&2 + sed 's/\r$//' "$WORK/UDPSTRM.MAP" 2>/dev/null | tail -20 >&2 || true + for f in UDPSTRM.LST ENTRY.LST; do + sed 's/\r$//' "$WORK/$f" 2>/dev/null | grep -i 'error' | grep -vi 'no errors' >&2 || true + done + exit 1 +fi +for f in UDPSTRM.LST ENTRY.LST; do + if sed 's/\r$//' "$WORK/$f" | grep -i 'error' | grep -qvi 'no errors'; then + echo "build.sh: $f diagnostics:" >&2 + sed 's/\r$//' "$WORK/$f" | grep -in 'error' | grep -vi 'no errors' >&2 + exit 1 + fi +done + +cp "$WORK/UDPSTRM.OUT" "$OUT/udpstream.coff" +sed 's/\r$//' "$WORK/UDPSTRM.MAP" > "$OUT/udpstream.map" +sed 's/\r$//' "$WORK/UDPSTRM.LST" > "$OUT/udpstream.lst" +sed 's/\r$//' "$WORK/ENTRY.LST" > "$OUT/entry.lst" +echo "build.sh: $OUT/udpstream.coff ($(wc -c < "$OUT/udpstream.coff") bytes)" +grep -E "ENTRY POINT|^\.(text|cinit|bss)" "$OUT/udpstream.map" | head -8 diff --git a/harness/udpstream/build/lance.coff b/harness/udpstream/build/lance.coff new file mode 100644 index 0000000000000000000000000000000000000000..24627e9640f92a91d33d4da5652a17f544ad39cf GIT binary patch literal 3927 zcmbW4Z)hAv6u@V5xn8&TC)ZL+Y^z?e(n7vVgpi#iAZ!$F5-AN0_{ka}#!+1$L>HvAE+mWD zb44m~86kIQyYWcCYqCFGL=)Ht|4@o!cz-f9l9|DwK``jm^XjC^3Oxe7lf=9v4S7x9EWU68~=Aqx` z%+j9t$_2ADRnee*cdlA7z-L23pXn%Z9OFrAVRBC=*zTkKdIGm@iVhL1>muTIzRE&Az}-YHrOdoM0S)X$`cqreM?CRg)g;L z$0J+H6U;`nqG24FPtYqyeF|6&+rK*h&c(K(j>^Tl6eMIUs6^#3cmmPs2_n&I#2@Qe z96ewR>Gpw5DplTq4&%(+s-P{c|`H?`dE^09hlOwJq&Pi->(z83PT-BTu&@>*Seum}(|`^f;w^ zIhulX3;E_4%lGq5nB0UNYQlOrhSl~!e3N6gc#(Re`smk`p7GJ2Db4%n6-v+f=y#NA zKDtWjMGsAFzdYIjIiFr6#4HV9#zBf1#~d`WwCt=vmSD9!gR>x`-pax@p_bzux5axM zsGZ;_i>t&jSaGlxF+xg|?KrqErQ&=$=V_<5@;emn2W~m~KMD0@8S#zAoJkGrebFA5e zAnolW)(q?`xON&N%OnWjG_Uo6@YNR`=TESYnry$tIiiQ_>#i5m6zpOR8w-;xCbeCg z?d5c?OEd!eAOcV;P2=Q^2faub_jX=u1E=MmmvVm1S%7(Y zV@x0-{D{$6jevEUAJ+r&hvTfwSjfXmG~OtQ&Kh_+m{rLk_=2fp0%YcWr(@m>o}urr zIU>C0>H97xFy9qNUCi4OajmW-i(~BBtu=%;fBD{v-Ypc-1;B4Y3U(he# zIT}x61MX5#If5--Z^8}GzuSbPFaI>*7VvHKi`cMl!g(xy%l*ExGlwm{;nAWe7B~U1 zyvFHDZJbYBU4wc|bwkGTQ)Fc(AuOj}2g|q$yep~%=P<{qj(2&g4JtthB|+k#MBk6= zq*13ik0xK2k5+}NKVlWy;>-0q!!b7Y#=_PbJ15DZs)%RW+ucg^T8A#c$tgrMODKmD z@Pw-U{KQLFDVe4OPK2CX`?9sX=Yx|i`%9mmYAxTAZz)f_Fxpan=Xz`ZZOI(U5v2*y!D`dl&n@4e-; zQ5{aqbUZhg_VsjXw<^48v9ZQry(T$h|8J~^?@7wp^xzO!#+8O+wGkzq$z=0IMLJn0 zBiVGn0=KeLb<&^BrOzuwdb>`}rgORBj3OPclR`F^&43-HPWlFisGZ(ADGX=c&4SJ` zm*6BZE)pCfYW|!a$!2kMI6%}$CO2FFB)wH9xxqp)JETY_>ZDLij}+N$mG$yxu#IGE uamL$bO~-Rj#SDVZL&SUcL{C9ys2~30?bs3+>426plFht#dSv*lLjD7~OD1pt literal 0 HcmV?d00001 diff --git a/harness/udpstream/build/lance_bare.coff b/harness/udpstream/build/lance_bare.coff new file mode 100644 index 0000000000000000000000000000000000000000..6d8ed94de9e225766166d29296baacba4a1a4b3f GIT binary patch literal 3927 zcmbW4Z)hAv6u@V5xn8&TC)ZL+Y^z?e(n7TOXn>RZ%M%u{__kVlmuLvQ#2q8O3K-ehUBvKk0@RKz{jH9|hh%QKJT}T$Q z=ZaL~GD7aqcH@zN*JOXXh$gTP{-G4d@cv|IBr}6UgJ96B=hboFlYNB(d$+)rC7@sa zAEM91cHwrnSy^)Sgo&mL=C_tZNVTGptD=_Z6C3iLp!v=EvU?vl>to*V_lN{iG1c<(MUVM@eC$R>zfSJ^+!Z**_Rwb$;TlL&O#)ZLm|Gi0mj&lqWEL`j(Os3SVlg zjz_kXCzy?DMZ-8UpP*Na`V_Dlwtsd0or`Tn9hHl9DM-jzP>IT6@C2gM6GWobh(Fe` zIC{Wz++L6`+GFJcvvM!x?G-_9!m_Y1+0|K|&<~);xMmk7@oh}RH$Co3)CiNdg9biw$M$6Af#*HFQuhA*#zXAeZ83DiR(;2hB2 z12N7qTimw|X~IVjA4e*2RPRM5H(@rp{KB`h(!$+C!H9rMi zTa?+{X=d42wy1IpZQ7z%XIM4QLDfJ8pM{lOy15%_@*{y@UDRS2CP!RJoRiq%rcJIS z!AUS~%$DTgjXL<-`F;+vvl(X%&QfoTHo6LF#5ZybG6ouuN1kp+*2y#RFx5=>=y6K- zax?|&7V^z8mhb1AFu4gk)P(hL46E&d_$J3}@gns`_0g{>J>#Q4Q=0eDE0mt|(eEhL zd~}u4iyoTVetEP5az4FCh*=uIjDr+2jyY&#Y1vtUEWv7d24_J=y_JP+LM_KRZj1Li zP&>g<7FUU5u;O4XVuX|^+i`GTO2zqh&eKk9<##CD57_RdAHXlmw{g&6^dOz&B(@1|pM#;a|SG53`$IOc1CW*yq@eh}xyFv*tLFd7OI{=2)`_ zLE76%tQpu>aP2fkmPru4XgzdH?^KOx|#(xBKxvUFKVE{=I0WN@EV4%twIljvx%d8LBD+G zXgrM#xJyCh2)1~=2{%ChZWE5a{L_S6z_-yaV#B@(=dt)L_xsAu9Jcs|M~j|V-~`0- z8mB9@aXxW%4eBw~4H?T%k(HT*u$+1wEaNKhuBZ~6!yKnN-sPz_s01OD1c`$ZeLt>~ zMxEw7ntWY8S{1JTh*fBdFW2h~$Jp2#3tMaKoFs>;BA#h)cPr6r9l8J~rx4LBp&UxU z6RP&}6E9t*WSSB<5pr_v%hvLq4^Fo1FMWEdwR}syr9AP%XiNE>>#hBh=UdD3<<|Z$ z?JTt%U*7HaKY4mEmxY6Q4F1;OG(I4t_8)oWKr#bu_vgLo;PoXS7)J@|bH(((_m@8#u|h5n&gcAzp);^Cn;ysgF|2$R~nAhMwE0Wlg$?u>13UZ zWYhf$+{#YXNq;t%KCckz?K(M|&gF(PigdhA3fWvX19p@;=^GrPc6#fiFr0Ze3p&SK zf|JC!NN|X#`Ez049nU=#GYC2l5%1j-Jq4Yie)x~KV@qJ916s;RHuK);k>Rrn`47mUCM*B| literal 0 HcmV?d00001 diff --git a/harness/udpstream/build/lance_fixlen.coff b/harness/udpstream/build/lance_fixlen.coff new file mode 100644 index 0000000000000000000000000000000000000000..c2b83af5155cf7879ca75a2483704dd7c5c8034d GIT binary patch literal 3929 zcmbW4Z)hAv6u{r+a=mWvPp+ku*jBw_rGRJOV;;hZtr%smxFqc z&Cc(=dGqGId9yQP&zT^m*pg#0RQX0ekQ?L?EJrC{Y^YacgmMPdnP~W&OH+H?GSzNY=`+%6MQ$WxO&z zy=2*DzPux^@=+_}YB=k|NL1|~4BoSU60Kvz7AMPSr!W!PQJ5%9Q2dN7c{LEc)KVD_ zZ7ED}8uL$$WXVq>0YX3VQ*-AQ^5bL}jV9c+El^}Y8(dqUR(W}H8>qwM5 z;5tq(%oppidVyQHlW_M6kvBm_TAb`?FH9H*$YWZwi<9&=Cexb{btGDdOUg1xg$aDN z(DM>|_F)wGY@L`GXz^imKJk(MgLXP_{LmxKM)GQC=uzF5m(jDkGUyWX5G0xd*}E%8 z1y+{#Z6g}<(8I@xb_&$!A*M87-3^%5fMo?%mSde{QTI^Lh>m%v#&nAw%5`VF6m`q8 z!spI1%f~9qn!w0rS=MWes^%H28qDC+xU%!tcVkU{Bmvb$HHJYr;z*(%$)@)mNlcJX z+_z%XN|fq4|^c4F-!lMge^n1>!`bgw`Y zxNb4u0^|99wgFQbutN=4x4@{{?#gcntSnz--e?~B4Wnl~^k+u19{Lrd=REX#Ms*Lp z$LK{DO>DnB+J-rwUIH-l{giQ(QpPC<^(?K}tC%HREl<%bVANe%)COib>fv{A|0eyI zzSq8)+nU&aCYwp^Z5U^C8)M#J?RF{Ch3 z`i`tTwrRmEDS%&*Sp|s#2ZO(S{Rhr(dMO52Ik?mDJ(0BYL3_=%Lkl$f3eKi#55u&# z9jsa0S!ngtM^?a(Up2S&{@|78ZToMuPnvwkr8$y^>+7x;)dcQibsLM5JSNqBoA2gq zuFEU}`!E7nEm(Qv<1+4qfi9fUhqfKS85uRtA4R=Tkal-bYlEQWotJuk&0fTLxnm3% z5pl%uoDOK6<;U>={54ye$A!D&MbHa;(3j- zm0CZcIJ!mVG1CncSDt{?*_gDFcoi+vDsZo;Jf6gCyE5M4sy3_yDG*1AO%h{2t&@74 zW?Y(LT|QhD-uscNuq?k=t1}v7V^1V#t?`qR5~#>@s=e8zhG*Ig3C~Ue&@HJDh~X2f z_VW`jTw!FI5j+zLa{bHZ^6n2#HtpZ}>8a-OE!n2>*mI*z<+rXj_fJ`9F3%R4`@g(1 z-*kLsx7Yvp>48idkLD5lTf_alUrOvh^3s8L3fs=leb=E2j8B?53ef9_*?;#vr-kVV zVy@%5yR@#SQ@vNwU5k%3hU+yh82f)?J$O@6&n5>3(K4+x8mk4=WGa=;j;PAX8W~O} z`&7J_ovM+(bS8OT1?8<8Ih)L6hEl3>yhd{AOge>j)Eem>7-V*OY9u$5dMAxK#~ebF zq_`+}jHvo^dN`e?(eVgTC8^9%4w3R^jbsLLBk4g^IZ-3Ik>v0QzpwIM-VC-tx*BJ? rZ&r0&H&x0Y>O4T)w@>mEbq4$JKYEDu$w?cw)ZujM-P6NEXI1zQ6W%Cm literal 0 HcmV?d00001 diff --git a/harness/udpstream/build/lance_idonclear.coff b/harness/udpstream/build/lance_idonclear.coff new file mode 100644 index 0000000000000000000000000000000000000000..0f6560b2de78903c890786d74c8d77c128ffa15c GIT binary patch literal 3971 zcmb7HUu;uV7(ch8luQ4t5JD!P7R5lqQl}5b7!xllfk;QD4^9!d?Yc6iWm~pI;lWs$ zu}Dg7wl^P4lV!Py2?Mv7#t7y<$-+uTvKK?5&X_o2Sm%q1FT4J}bK2W`?n)y~dhb2I z?{~iMJKuM{^QXWzFt+;JyH5le+szo;#afh2$_-ZNK?6T6gGYBxVJxQ^|e?;&Z?M;j(@TNZWCn?!*{xmeA$^Lz_qIWI5AWC2a`T32F~wYdvoh}6 zSsAa4PcNF5kuC4c>cVPeTn`SlAQDymhy1J7PqKDIY+;& zi=N7OU~7It=%|!36i49`dWE=8EoNQ!uhzeFp>4#8OR>&+kz=nO)co)S(dqIMlBpu@ zSck&of#B47Az!@5%6XyXZp_&$K;HN@Wnr?tEkBVvNFLMtT$rSARS{>o5|$)o zb@S$$BqB*DZbDaw1FzS?Kh_U($j%lTH7%8t$D)VNLLTu=5{8U{fjr9S4#L`GOg+Mx z5f^%lqx&Q@hIxy8OPI*_^9?Yq0d}|n)+J$7ZTHl-B&@7n;NHYt=r$$ocdlV@9@*G7c$aoN`dl(z3OJEMc}hL!)40&dQ=T z+)Xn7yH#5(G-S$ns;G=Z%uau!qcOKs0v9|I62ZV06=q5r(?KIww{{VrTuv% zY3vn@v8oS2+SkU+Ijk%29X$k2zLRAy_BGkU)A#>|1)67dFW2+>{8wJItiP&Eu{J+Q zXPx_?_{Xr~)paaPiuhGmc(LyDxGeE#?MJjwSEy0)aS3atuM?yH$g+GGea`m$QSbtO z+6QFTMu{blzt69qTeB80E@wU%q9@aq_ctJrHKd@To4ex3G{hFoXdx(GUwp!-f!^EW=FCoWqJI8$HE5f4K1*aD?V2JV6 z5R=J_qN^Q8kD{cLtbECL$+JA3#f`M0p~{r9daVIo1piJ0oP4?805^ee=euWJzk#z` z_$JNB`uU`+zTsf;(+y5bBClaStJiaj++)u56_=l6D>D&gIrcg<(=2e#s4UKGmQ@*V zcT^^-M)5@da6I2!XZ9{{~Av2eM;=W(N_*elW5m|&RY>( zby^hG&6txR--tYdQ|oTOowpp^%Sg|8xA|F4c*jOYQt4C@dT>an zYVPSD;CgVFs3IeS$@fy=R7Xl9!DR~n!rM0I)Nm?A))O|8Ob?C#Djc(sbpOa`YCtcX zu#u6`#PBE-*_!RBVhH9JgHZ&coap1ZSWZ61qv73i<9~t$6Cg NspR{oh6m5+>_0S%JMaJi literal 0 HcmV?d00001 diff --git a/harness/udpstream/build/lance_init2.coff b/harness/udpstream/build/lance_init2.coff new file mode 100644 index 0000000000000000000000000000000000000000..1de7b454b8e6e133e1b0a7d7868638c1d9c87527 GIT binary patch literal 3943 zcmb7HUuaup6hAj)Rg*oAH5Uj#SVX`XedXaQmZKZyTt~5Xdj@kD$-Q zHsMaUURkjYgdnd8`nQG!P_3wNUCc}UqAl+U=-+HEyZ>>sG5SsKh;ZOecQhn})^~M$ zpMl?b33eKK#js`yx;*$adI8WjgZ}99Ohp1+)^DolQDFPEIqV@aQz=>cvELT-!hzWO zCA~0H$z%WCLbXytpG#fjGqVyS8&4Wb(+4_GcRw4~>C7LE%=blS(A($4J!l9}PXQespWch%HTWXzC05B0yB60{l!RYh^2+7wFZ>*y+@_=*d zqcC4=#QG(!<$l6BDooylWMOH#v!ghr9U+ft%`Qz-YeJ-!7OP9tFqfDj2*oM<`sjOw zeFrcKe75zNXsGc~IG^~5|FE7;v>$$~l_f7jMUQM>UPaH=M92%wLl9^Vq;E}(xiC{a zw2M&5gC0LgXqOAs`UsPoU_DJRwF#DWVWuc`k;c3S1(nbV52`Y5$%Ark*^46A6lFel zmRWAg6jc{SI!!U(z^H1T!>YjyK8q{6aBDx-v3UJZEND2F3c1!GjCK6`ZYt(dC;F2n)RU97<$2je#g+f z2YtZM%MLWL`|5Z*=6rS;KrakZ#!*TcryST>TD8_OOSoE|p;^GVv$CiUOmobIo8tX; zVoNTR$F<9a;flkxND)#Ze8-`EDG_t`Gmd^@r+bH@{ebUfd_48}Zl)cfljiEY4BK&@ zWF1#?R1+eI(y6lg#I{AfA_JZ&b21__91Z>a)$cgl*<}f^j&R@OJ0!6SA#203!iyA( zD$cp44}*5F1B^M`XJ{?imSqUw8RyhK7`pzVW&MfziD~;x9&XOvEXE1k)odM0(>zAC zU7YXjY_2OT0*5dHSUXsUvIO-p%oTyl0@c9bX%BL;TWE1>V$xqdEaVZ)C1>O z`!!3%c9ymGS^mY&7;!0YE0{O6kvF!w1#Xd_bhFEOWbpab5d++elBciYh>}YuS^0vU zgcn#mOIv8y!b%cM@kSH8g#3F=aPsBvCb)t4E_OoN`gOdY#ctQnTYd*I#Wx%*c7wqi z5zlLktyKHIQsWpk`?Tdzt4SsKugH@64xOcjhNV#1T@Fo?6yde~W z5`M92KR@--b%x9`1aF8gdH&1R@}3V)w;bQ~>6zB@9od$0>4ou@^1C-$`zJ59mS>Bt z{a@KzXlY;G@AW@^b|{m^i+U9QHt=>nC?pP_c;!euh5h=^`SPLXj9-v?0h|o?tH;MU z_0jf!=S!w-GfwT5MK3RIYXaA6+*X1u=L_rq+j{ujq?}I<4Wm9N-~v7XLCyV zbORYpCkGVdoM|8f=}hvX0_C?G$oXU@Gm=uuCmYCEI+ISJ9;Jcw4-GRteGO!6B=v3@ zIklP6Oz^>ke`UN>)ciR+nog5-yinATRAyuhq4HY|Br`OYOAjmMQw?M+mmJLjd%f~e y-VFL6U5hilK5HDuyOn%F&O?YdgUA^kAh+?gYzXAL58CmojHXlXogE!HufTsan>1$t literal 0 HcmV?d00001 diff --git a/harness/udpstream/build/lance_longwait.coff b/harness/udpstream/build/lance_longwait.coff new file mode 100644 index 0000000000000000000000000000000000000000..1703021d1f4b1e1741d874a8afc4b6a4fd55dfc9 GIT binary patch literal 4065 zcmbW4U2GIp6vyv$yDY&)85{HJLCp_O5SlPoMgiOy#p(tq4eEBX&SRj((vMjReheR&<8-B+2Ra1WV6 zV<3C?)v&}0>Ygn`BQARA7|}L~W{(ilYOszPEM9{RNUWen+Q_2eqAX5y%thl|H|L^4 z_llcBx1efb>^!rguYwwv7}+eSMupL;c@e7y8GHdVJ9A?v*5oG&qjgb=p`RTtOTv<* zpl;YumP8~8#ZB01bMWdqcx3-LhwN;mQNvm8O|F5@LLTw;5<|wIfjr7|E3syosfV~` z#6^#Bx?7?#%v+i6ETwpp%FR1ZY;%I>F~<>IWyHey`V z8)k1Ps{}!57g>I0!@QZ-81|4E4MbxH{J&oP6Qi46j4-Sl)HzWRNju}WS8Y2mPh&4) zj3s*r((YzvJwjcfxl`>~V_xiOPU}7X%dgn>U$9S_qTbRN$-}jE*NSNjRjg`bVN%3| zsy2;#3FBtmatrqY8SrHKx4?UM9GF7mXV_sd#vR_9*hP3mmUu*}vFY*ez4N#I6TElY z2%&CRl{iu4mRNviKNa&#u}F@X^_?hTO)g+>2kfD3ugqA){Y!jABni&u=$tYeX9uyD#>W)Q zkq_;rdDDA}zrSItcpu>JEw*RAHB7pE1}UhrQe-s68JX!t19MD_Ps9qR5Gi)ki#b<2 zjxTbO%?Nt=itoW^csz6Ks9sJlYT(yu@Ek1Mt-;Bce`;_Gd<);3tM*Nt48u3eJH0sl z6x6pITKw#UvzW+hn6H`YNvW)x;~sNeUvBwXwlWh}mSb!)B z7m6cN9MR1XU3tCbZ`eXKu%bJMCgv1z`dN~h~v?RxM^ zQ&zzl&c_U_310+H)Q>NZ@4d{)G$%NlN^;}t`tpvCPSpLr?emlM<(mfT$|En2)Ro`9 zR^LBuzP@~5w7&mK+cS0h*LJ%7k9PN_>8cgN-zx3}JxXlf;n(&@lW3QJ&N~-fq&&*Q zql|Tz#r(hXzSY2WB(czO+@TuR(kb2V=*}nl8pC{zO2+a~+sEy^ zTOmXJ$@f#xIaVe#NQ#SbyiD}&!BmQR@2Zewx_=1BI8h<#-l5@CpKjo|QyRt4aAI&+ z+?xe)XRv{#N^z!pZ%M~-W2Ov3=P}~E1(T=H>FdIOx~E%;AWdlLgQ?^P-Glw7b@m^` C1!S%O literal 0 HcmV?d00001 diff --git a/harness/udpstream/build/lance_stopinit.coff b/harness/udpstream/build/lance_stopinit.coff new file mode 100644 index 0000000000000000000000000000000000000000..664d5bd33ed3fcdeb335a4d5789e20ff1fe98e5f GIT binary patch literal 3943 zcmb7HUuaup6hAj$6YM#n-3Bw<$|y-wnz{+xlgXF86EUR25LpHL)Xj~QD1sJ=lhb}KTCrP^!EFH z=l7j+zH`3w{h5FcIQ7S`?`(;Zt?;1@OvH=&%w4*p~fKsv4q0J>04>Owr9KA&fj z1_p44_3e*@&G=w4Pbz2({gISxIDayB;;G@0VN~ekIXR7Xd?1(OZ4-5w0{P|r5%ihZ zA>8gYDl68$5NNt!d}~?&wTcSYL`@nHZFyh7_-13-y^mXsF>ZKAgafyGqahje{ww2q zO#IGEu-!B&rZrPAwMq$nF7=Sl%u0-GJZUaX@9RR{18iKM|JF^>0>NB8UYiW=s!i4=XO~UO zC{%Y96mGRPsYJ4Vj6_}k;m`x?N4Iv2*wSfW022WvB8Siuj80#G5Uq}QV;zl=2b|Lw zh52G5)-G}__Y%%gVe%#<3ro}8UBxN=5P3{%c4?Yg6C$2%5k=1cBy2`tFM{7giMy z>>yO~phr&-+T%j?e!}DySYHcFZGmN7SXGpINTcRKK_zs;gQ|>M@}Qhs_M*tGiZY)& z%PhB56;&5TI;*1A#HebX!>YjyK8q{6aC0x#Qy0EHviFu=X(61SK)`R}U(5wf&%Fy#3^gD)X z9`pf2FFDY}&MV`cnDg0X0HZKO8AmB)oN{1iY1LZ8Ea7T-hGqfd&dQ=bFwHR+UKQ_k z5?gYiJgz-13|AbkMT(FT;X4lPONp4fpKUW&&?6L${N4W3t9g^6EkhN}E;YEr?4d-0f zhe6xl1?C*?Gqjd$%Q6J;jB{!q3SE2Avi?H-#I)x?9&XItD8>og)odM0(>zA?U7YXj zY_2OT0tYYxSUXsU5kyqqxx zjELJ}WL^W*&T`rC0RFVBwKX!~{2^9;yM=OnN`9MjgQt517DW7U0 zW9j6ef}GP$WH6mcUQnR?b`v?5%w$GWO8G<+$)z*t6zWl$$iVOj)6?HXa-*qt)5xjM zlxBhtCj2YorK0Z7nXz=5tmB2Ej-)c9IfTk@HIdA4E}tGz$|svhE}tCB1AD#lQQi#t vAYG3$y*}$4$Geq$LC!;nH-pF-86>yywQLIHyAL|?tBj>n@0}SNJ*U8bY&0{X literal 0 HcmV?d00001 diff --git a/harness/udpstream/entry.asm b/harness/udpstream/entry.asm new file mode 100644 index 0000000..809145f --- /dev/null +++ b/harness/udpstream/entry.asm @@ -0,0 +1,136 @@ +; entry.asm -- C process entry + call-throughs to the firmware socket API. +; +; _start replaces boot.obj (whose exit() spins and whose _c_int00 ends in +; `reti`): set up the C program stack, call main, return with `rets` to the +; create_process trampoline. +; +; The socket wrappers are NOT a bare `calla`/`rets`. The firmware's wrappers +; end in `RETS 2`: they pop the return address *and* two argument words, and +; they restore A14 from a caller-pushed slot. A plain thunk therefore returns +; with SP one word off, and its own `rets` pops the saved-A14 slot (a bit +; pointer into .bss) as the return address -- the CPU then executes data and +; the board lands in its reset/delay path. dwt/tcpip/tftpc.asm avoids this by +; saving SP and A14 in cells around every ROM call; these thunks do the same. +; +; socket = 3F5C9A00h socket(domain, type, protocol) -> fd +; bind = 3F5CAA80h bind(fd, sockaddr*, len) -> 0 / -1 +; recvfrom = 3F5D7D50h recvfrom(fd, buf, len, flags, &from, &fromlen) -> n +; close = 3F5E8930h close(fd) +; malloc = 3F701230h malloc(bytes) -> bit pointer +; free = 3F702440h free(bit pointer) +; +; See dwt/notes/TCPIP.md. + + .text + .globl _start + .globl _main +_start: + setf 32, 0, 1 + movi _cstack, a14 + move a14, a13 + calla _main + rets + + .globl _socket_ +_socket_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F5C9A00h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _bind_ +_bind_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F5CAA80h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _recvfrom_ +_recvfrom_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F5D7D50h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _close_ +_close_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F5E8930h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _malloc_ +_malloc_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F701230h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _delay_ +_delay_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F833A20h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _listen_ +_listen_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F5CBCD0h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _accept_ +_accept_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F5CCB40h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _read_ +_read_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F5D8E70h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _select_ +_select_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F871CE0h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .globl _free_ +_free_: + move sp, @_sys_sp, 1 + move a14, @_sys_a14, 1 + calla 3F702440h + move @_sys_sp, sp, 1 + move @_sys_a14, a14, 1 + rets 2 + + .bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes + .bss _sys_sp, 32, 16 + .bss _sys_a14, 32, 16 + .end diff --git a/harness/udpstream/lance.c b/harness/udpstream/lance.c new file mode 100644 index 0000000..71ff827 --- /dev/null +++ b/harness/udpstream/lance.c @@ -0,0 +1,274 @@ +/* + * lance.c -- LANCE loopback probe using LOW-MEMORY rings. + * + * The OS's own init block (pointer at bit 0x23E7A0 -> byte 0xF80 on a live + * boot) puts its rings at byte 0x0A80 / 0x0E80 and stores only the LENGTH in + * the ring words' bits 13-15 (0xE000 / 0xA000), with no address bits above 16. + * MAME's model packs address bits 16+ into that word; if the real C-LANCE does + * not, a high ring address like 0x380028 is seen as 0x0028. This payload + * therefore places the block, rings and buffers below 64 KB. + * + * Memory used (BYTE addresses, well below 64 KB): + * 0x3000 init block (24 B) 0x3020 RX ring (8 * 8 B) 0x3060 TX ring (8 B) + * 0x3100 TX frame (24 B) 0x3200 RX buffer (2048 B) + * + * Result cells at 0x018C8000 (32-bit): 0x160/0x180 CSR0 samples, + * 0x200 RX RMD1, 0x220 RX MCNT, 0x240 TX TMD1, 0x1A0 payload-found flag. + * + * Panel rows (real unit, 256-byte stride): y 670/700 = CSR0, 730 = TMD1, + * 760 = RMD1, 790 = MCNT; blocks y 850: left = payload returned, right = TX done. + */ +#define RAP 0x05900000 +#define RDP 0x05800000 +#define MARK 0x018C8000 +#define VRAM_BASE 0x04000000 + +#define MODE_LOOP 0x0004 +#define MODE_INTL 0x0040 +#define CSR0_STOP 0x0004 +#define CSR0_STRT 0x0002 +#define CSR0_INIT 0x0001 +#define CSR0_TDMD 0x0008 +#define CSR0_IDON 0x0100 +#define RMD1_OWN 0x8000 +#define TMD1_OWN 0x8000 +#define TMD1_ENP 0x0100 +#define TMD1_STP 0x0200 + +#define B(byte) ((unsigned int)(byte) * 8) /* bit address of a byte */ +#define INITB B(0x3000) +#define RXR B(0x3020) +#define TXR B(0x3060) +#define RXBUF B(0x3200) +#define TXBUF B(0x3180) + +#define RING_ENTRIES 8 + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static void w16(bit, v) +unsigned int bit, v; +{ + *(unsigned short *)bit = v; +} + +static unsigned int r16(bit) +unsigned int bit; +{ + return *(unsigned short *)bit; +} + +static void wb(bit, v) +unsigned int bit, v; +{ + *(unsigned char *)bit = v; +} + +static void rect(x, y, w, h, v) +unsigned int x, y, w, h, v; +{ + unsigned int row, col; + for (row = 0; row < h; row++) { + for (col = 0; col < w; col = col + 8) { + *(unsigned char *)(VRAM_BASE + (y + row) * 2048 + x + col) = v; + } + } +} + +static void bits16(x, y, v) +unsigned int x, y, v; +{ + unsigned int b; + for (b = 0; b < 16; b++) { + rect(x + b * 30, y, 24, 24, (v & (1 << b)) ? 0xFF : 0x00); + } +} + +static void dot(n) +unsigned int n; +{ + rect(60, 650 + n * 44, 24, 24, 0xFF); +} + +static void settle() +{ + unsigned int i; + for (i = 0; i < 30; i++) { + } +} + +static void csr_w(reg, v) +unsigned int reg, v; +{ + *(unsigned short *)RAP = reg; + settle(); + *(unsigned short *)RDP = v; + settle(); +} + +static unsigned int csr_r(reg) +unsigned int reg; +{ + unsigned int v; + *(unsigned short *)RAP = reg; + settle(); + v = *(unsigned short *)RDP; + settle(); + return v; +} + +int main() +{ + unsigned int i, r, idle, ok, rxmd1, txmd1, mcnt; + unsigned int s0[2]; + + cell(0x000, 1); + dot(0); + csr_w(0, CSR0_STOP); + + rect(100, 650, 512, 256, 0x00); + rect(100, 650, 512, 4, 0xFF); + rect(100, 902, 512, 4, 0xFF); + rect(100, 650, 8, 256, 0xFF); + rect(608, 650, 8, 256, 0xFF); + + /* init block: mode, PADR, LADRF, RX addr/len, TX addr/len */ + w16(INITB + 0 * 16, MODE_LOOP | MODE_INTL); + w16(INITB + 1 * 16, 0x0008); + w16(INITB + 2 * 16, 0x162b); + w16(INITB + 3 * 16, 0x5a4b); + w16(INITB + 4 * 16, 0); + w16(INITB + 5 * 16, 0); + w16(INITB + 6 * 16, 0); + w16(INITB + 7 * 16, 0); + w16(INITB + 8 * 16, RXR >> 3 & 0xFFFF); /* ring addr low 16 */ + w16(INITB + 9 * 16, 3 << 13); /* length only: 2^3 = 8 */ + w16(INITB + 10 * 16, TXR >> 3 & 0xFFFF); + w16(INITB + 11 * 16, 3 << 13); + cell(0x040, INITB >> 3); + cell(0x060, RXR >> 3); + cell(0x080, TXR >> 3); + + /* RX descriptors: all point at the same buffer */ + for (i = 0; i < RING_ENTRIES; i++) { + w16(RXR + i * 64 + 0, (RXBUF >> 3) & 0xFFFF); + w16(RXR + i * 64 + 16, RMD1_OWN | (((RXBUF >> 3) >> 16) & 0xFF)); + w16(RXR + i * 64 + 32, 0x0A0A); /* 1526 bytes */ + w16(RXR + i * 64 + 48, 0); + } + + /* one 24-byte frame (MAME's loopback accepts 8..32 bytes) */ + wb(TXBUF + 0 * 8, 0x08); wb(TXBUF + 1 * 8, 0x00); + wb(TXBUF + 2 * 8, 0x2b); wb(TXBUF + 3 * 8, 0x16); + wb(TXBUF + 4 * 8, 0x4b); wb(TXBUF + 5 * 8, 0x5a); + wb(TXBUF + 6 * 8, 0x02); wb(TXBUF + 7 * 8, 0x00); + wb(TXBUF + 8 * 8, 0x00); wb(TXBUF + 9 * 8, 0x00); + wb(TXBUF + 10 * 8, 0x00); wb(TXBUF + 11 * 8, 0x01); + wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB5); + wb(TXBUF + 14 * 8, 0xDE); wb(TXBUF + 15 * 8, 0xAD); + wb(TXBUF + 16 * 8, 0xBE); wb(TXBUF + 17 * 8, 0xEF); + for (i = 18; i < 24; i++) { + wb(TXBUF + i * 8, i); + } + + w16(TXR + 0, (TXBUF >> 3) & 0xFFFF); + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + w16(TXR + 32, 0x0FE8); /* 24 bytes */ + w16(TXR + 48, 0); + + csr_w(1, (INITB >> 3) & 0xFFFF); + csr_w(2, ((INITB >> 3) >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + dot(1); + + r = 0; + for (idle = 0; idle < 20000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + cell(0x140, r); + if (!(r & CSR0_IDON)) { + cell(0x000, 0xEE); + return 1; + } + dot(2); + for (idle = 0; idle < 50000; idle++) { + } + + csr_w(0, CSR0_STRT); + for (idle = 0; idle < 50000; idle++) { + } + /* own the TX descriptor now, then kick with STRT|TDMD in ONE write: + * CSR0 is a command shadow, a bare TDMD write clears STRT. */ + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + csr_w(0, CSR0_STRT | CSR0_TDMD); + dot(3); + s0[0] = csr_r(0); + + for (idle = 0; idle < 100000; idle++) { + if (!(r16(RXR + 16) & RMD1_OWN)) { + dot(4); + break; + } + if (!(r16(TXR + 16) & TMD1_OWN)) { + dot(4); + } + } + s0[1] = csr_r(0); + + rxmd1 = r16(RXR + 16); + mcnt = r16(RXR + 48) & 0x0FFF; + txmd1 = r16(TXR + 16); + ok = 0; + if (*(unsigned char *)(RXBUF + 12 * 8) == 0x88 && + *(unsigned char *)(RXBUF + 13 * 8) == 0xB5 && + *(unsigned char *)(RXBUF + 14 * 8) == 0xDE) { + ok = 1; + } + cell(0x160, s0[0]); + cell(0x180, s0[1]); + cell(0x200, rxmd1); + cell(0x220, mcnt); + cell(0x240, txmd1); + cell(0x1A0, ok); + + bits16(120, 670, s0[0]); + bits16(120, 700, s0[1]); + bits16(120, 730, txmd1); + bits16(120, 760, rxmd1); + bits16(120, 790, mcnt); + if (ok) { + rect(120, 850, 24, 24, 0xFF); + } + if (!(txmd1 & TMD1_OWN)) { + rect(160, 850, 24, 24, 0xFF); + } + + /* best effort: give the chip back to the OS */ + { + unsigned int osblk; + osblk = *(unsigned int *)0x23E7A0; + if (osblk != 0) { + csr_w(0, CSR0_STOP); + csr_w(1, osblk & 0xFFFF); + csr_w(2, (osblk >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + for (idle = 0; idle < 10000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + csr_w(0, 0x0042); + } + } + return 0; +} diff --git a/harness/udpstream/lance_bare.c b/harness/udpstream/lance_bare.c new file mode 100644 index 0000000..2ce5d40 --- /dev/null +++ b/harness/udpstream/lance_bare.c @@ -0,0 +1,274 @@ +/* + * lance.c -- LANCE loopback probe using LOW-MEMORY rings. + * + * The OS's own init block (pointer at bit 0x23E7A0 -> byte 0xF80 on a live + * boot) puts its rings at byte 0x0A80 / 0x0E80 and stores only the LENGTH in + * the ring words' bits 13-15 (0xE000 / 0xA000), with no address bits above 16. + * MAME's model packs address bits 16+ into that word; if the real C-LANCE does + * not, a high ring address like 0x380028 is seen as 0x0028. This payload + * therefore places the block, rings and buffers below 64 KB. + * + * Memory used (BYTE addresses, well below 64 KB): + * 0x3000 init block (24 B) 0x3020 RX ring (8 * 8 B) 0x3060 TX ring (8 B) + * 0x3100 TX frame (24 B) 0x3200 RX buffer (2048 B) + * + * Result cells at 0x018C8000 (32-bit): 0x160/0x180 CSR0 samples, + * 0x200 RX RMD1, 0x220 RX MCNT, 0x240 TX TMD1, 0x1A0 payload-found flag. + * + * Panel rows (real unit, 256-byte stride): y 670/700 = CSR0, 730 = TMD1, + * 760 = RMD1, 790 = MCNT; blocks y 850: left = payload returned, right = TX done. + */ +#define RAP 0x05900000 +#define RDP 0x05800000 +#define MARK 0x018C8000 +#define VRAM_BASE 0x04000000 + +#define MODE_LOOP 0x0004 +#define MODE_INTL 0x0040 +#define CSR0_STOP 0x0004 +#define CSR0_STRT 0x0002 +#define CSR0_INIT 0x0001 +#define CSR0_TDMD 0x0008 +#define CSR0_IDON 0x0100 +#define RMD1_OWN 0x8000 +#define TMD1_OWN 0x8000 +#define TMD1_ENP 0x0100 +#define TMD1_STP 0x0200 + +#define B(byte) ((unsigned int)(byte) * 8) /* bit address of a byte */ +#define INITB B(0x3000) +#define RXR B(0x3020) +#define TXR B(0x3060) +#define RXBUF B(0x3200) +#define TXBUF B(0x3180) + +#define RING_ENTRIES 8 + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static void w16(bit, v) +unsigned int bit, v; +{ + *(unsigned short *)bit = v; +} + +static unsigned int r16(bit) +unsigned int bit; +{ + return *(unsigned short *)bit; +} + +static void wb(bit, v) +unsigned int bit, v; +{ + *(unsigned char *)bit = v; +} + +static void rect(x, y, w, h, v) +unsigned int x, y, w, h, v; +{ + unsigned int row, col; + for (row = 0; row < h; row++) { + for (col = 0; col < w; col = col + 8) { + *(unsigned char *)(VRAM_BASE + (y + row) * 2048 + x + col) = v; + } + } +} + +static void bits16(x, y, v) +unsigned int x, y, v; +{ + unsigned int b; + for (b = 0; b < 16; b++) { + rect(x + b * 30, y, 24, 24, (v & (1 << b)) ? 0xFF : 0x00); + } +} + +static void dot(n) +unsigned int n; +{ + rect(60, 650 + n * 44, 24, 24, 0xFF); +} + +static void settle() +{ + unsigned int i; + for (i = 0; i < 30; i++) { + } +} + +static void csr_w(reg, v) +unsigned int reg, v; +{ + *(unsigned short *)RAP = reg; + settle(); + *(unsigned short *)RDP = v; + settle(); +} + +static unsigned int csr_r(reg) +unsigned int reg; +{ + unsigned int v; + *(unsigned short *)RAP = reg; + settle(); + v = *(unsigned short *)RDP; + settle(); + return v; +} + +int main() +{ + unsigned int i, r, idle, ok, rxmd1, txmd1, mcnt; + unsigned int s0[2]; + + cell(0x000, 1); + dot(0); + csr_w(0, CSR0_STOP); + + rect(100, 650, 512, 256, 0x00); + rect(100, 650, 512, 4, 0xFF); + rect(100, 902, 512, 4, 0xFF); + rect(100, 650, 8, 256, 0xFF); + rect(608, 650, 8, 256, 0xFF); + + /* init block: mode, PADR, LADRF, RX addr/len, TX addr/len */ + w16(INITB + 0 * 16, MODE_LOOP | MODE_INTL); + w16(INITB + 1 * 16, 0x0008); + w16(INITB + 2 * 16, 0x162b); + w16(INITB + 3 * 16, 0x5a4b); + w16(INITB + 4 * 16, 0); + w16(INITB + 5 * 16, 0); + w16(INITB + 6 * 16, 0); + w16(INITB + 7 * 16, 0); + w16(INITB + 8 * 16, RXR >> 3 & 0xFFFF); /* ring addr low 16 */ + w16(INITB + 9 * 16, 3 << 13); /* length only: 2^3 = 8 */ + w16(INITB + 10 * 16, TXR >> 3 & 0xFFFF); + w16(INITB + 11 * 16, 3 << 13); + cell(0x040, INITB >> 3); + cell(0x060, RXR >> 3); + cell(0x080, TXR >> 3); + + /* RX descriptors: all point at the same buffer */ + for (i = 0; i < RING_ENTRIES; i++) { + w16(RXR + i * 64 + 0, (RXBUF >> 3) & 0xFFFF); + w16(RXR + i * 64 + 16, RMD1_OWN | (((RXBUF >> 3) >> 16) & 0xFF)); + w16(RXR + i * 64 + 32, 0x0A0A); /* 1526 bytes */ + w16(RXR + i * 64 + 48, 0); + } + + /* one 24-byte frame (MAME's loopback accepts 8..32 bytes) */ + wb(TXBUF + 0 * 8, 0x08); wb(TXBUF + 1 * 8, 0x00); + wb(TXBUF + 2 * 8, 0x2b); wb(TXBUF + 3 * 8, 0x16); + wb(TXBUF + 4 * 8, 0x4b); wb(TXBUF + 5 * 8, 0x5a); + wb(TXBUF + 6 * 8, 0x02); wb(TXBUF + 7 * 8, 0x00); + wb(TXBUF + 8 * 8, 0x00); wb(TXBUF + 9 * 8, 0x00); + wb(TXBUF + 10 * 8, 0x00); wb(TXBUF + 11 * 8, 0x01); + wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB5); + wb(TXBUF + 14 * 8, 0xDE); wb(TXBUF + 15 * 8, 0xAD); + wb(TXBUF + 16 * 8, 0xBE); wb(TXBUF + 17 * 8, 0xEF); + for (i = 18; i < 24; i++) { + wb(TXBUF + i * 8, i); + } + + w16(TXR + 0, (TXBUF >> 3) & 0xFFFF); + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + w16(TXR + 32, 0x0FE8); /* 24 bytes */ + w16(TXR + 48, 0); + + csr_w(1, (INITB >> 3) & 0xFFFF); + csr_w(2, ((INITB >> 3) >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + dot(1); + + r = 0; + for (idle = 0; idle < 20000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + cell(0x140, r); + if (!(r & CSR0_IDON)) { + cell(0x000, 0xEE); + return 1; + } + dot(2); + for (idle = 0; idle < 50000; idle++) { + } + + csr_w(0, CSR0_STRT); + for (idle = 0; idle < 50000; idle++) { + } + /* own the TX descriptor now, then kick with STRT|TDMD in ONE write: + * CSR0 is a command shadow, a bare TDMD write clears STRT. */ + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + csr_w(0, CSR0_TDMD); /* BARE TDMD: no STRT, no INEA */ + dot(3); + s0[0] = csr_r(0); + + for (idle = 0; idle < 100000; idle++) { + if (!(r16(RXR + 16) & RMD1_OWN)) { + dot(4); + break; + } + if (!(r16(TXR + 16) & TMD1_OWN)) { + dot(4); + } + } + s0[1] = csr_r(0); + + rxmd1 = r16(RXR + 16); + mcnt = r16(RXR + 48) & 0x0FFF; + txmd1 = r16(TXR + 16); + ok = 0; + if (*(unsigned char *)(RXBUF + 12 * 8) == 0x88 && + *(unsigned char *)(RXBUF + 13 * 8) == 0xB5 && + *(unsigned char *)(RXBUF + 14 * 8) == 0xDE) { + ok = 1; + } + cell(0x160, s0[0]); + cell(0x180, s0[1]); + cell(0x200, rxmd1); + cell(0x220, mcnt); + cell(0x240, txmd1); + cell(0x1A0, ok); + + bits16(120, 670, s0[0]); + bits16(120, 700, s0[1]); + bits16(120, 730, txmd1); + bits16(120, 760, rxmd1); + bits16(120, 790, mcnt); + if (ok) { + rect(120, 850, 24, 24, 0xFF); + } + if (!(txmd1 & TMD1_OWN)) { + rect(160, 850, 24, 24, 0xFF); + } + + /* best effort: give the chip back to the OS */ + { + unsigned int osblk; + osblk = *(unsigned int *)0x23E7A0; + if (osblk != 0) { + csr_w(0, CSR0_STOP); + csr_w(1, osblk & 0xFFFF); + csr_w(2, (osblk >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + for (idle = 0; idle < 10000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + csr_w(0, 0x0042); + } + } + return 0; +} diff --git a/harness/udpstream/lance_fixlen.c b/harness/udpstream/lance_fixlen.c new file mode 100644 index 0000000..ea22923 --- /dev/null +++ b/harness/udpstream/lance_fixlen.c @@ -0,0 +1,274 @@ +/* + * lance.c -- LANCE loopback probe using LOW-MEMORY rings. + * + * The OS's own init block (pointer at bit 0x23E7A0 -> byte 0xF80 on a live + * boot) puts its rings at byte 0x0A80 / 0x0E80 and stores only the LENGTH in + * the ring words' bits 13-15 (0xE000 / 0xA000), with no address bits above 16. + * MAME's model packs address bits 16+ into that word; if the real C-LANCE does + * not, a high ring address like 0x380028 is seen as 0x0028. This payload + * therefore places the block, rings and buffers below 64 KB. + * + * Memory used (BYTE addresses, well below 64 KB): + * 0x3000 init block (24 B) 0x3020 RX ring (8 * 8 B) 0x3060 TX ring (8 B) + * 0x3100 TX frame (24 B) 0x3200 RX buffer (2048 B) + * + * Result cells at 0x018C8000 (32-bit): 0x160/0x180 CSR0 samples, + * 0x200 RX RMD1, 0x220 RX MCNT, 0x240 TX TMD1, 0x1A0 payload-found flag. + * + * Panel rows (real unit, 256-byte stride): y 670/700 = CSR0, 730 = TMD1, + * 760 = RMD1, 790 = MCNT; blocks y 850: left = payload returned, right = TX done. + */ +#define RAP 0x05900000 +#define RDP 0x05800000 +#define MARK 0x018C8000 +#define VRAM_BASE 0x04000000 + +#define MODE_LOOP 0x0004 +#define MODE_INTL 0x0040 +#define CSR0_STOP 0x0004 +#define CSR0_STRT 0x0002 +#define CSR0_INIT 0x0001 +#define CSR0_TDMD 0x0008 +#define CSR0_IDON 0x0100 +#define RMD1_OWN 0x8000 +#define TMD1_OWN 0x8000 +#define TMD1_ENP 0x0100 +#define TMD1_STP 0x0200 + +#define B(byte) ((unsigned int)(byte) * 8) /* bit address of a byte */ +#define INITB B(0x3000) +#define RXR B(0x3020) +#define TXR B(0x3060) +#define RXBUF B(0x3200) +#define TXBUF B(0x3180) + +#define RING_ENTRIES 8 + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static void w16(bit, v) +unsigned int bit, v; +{ + *(unsigned short *)bit = v; +} + +static unsigned int r16(bit) +unsigned int bit; +{ + return *(unsigned short *)bit; +} + +static void wb(bit, v) +unsigned int bit, v; +{ + *(unsigned char *)bit = v; +} + +static void rect(x, y, w, h, v) +unsigned int x, y, w, h, v; +{ + unsigned int row, col; + for (row = 0; row < h; row++) { + for (col = 0; col < w; col = col + 8) { + *(unsigned char *)(VRAM_BASE + (y + row) * 2048 + x + col) = v; + } + } +} + +static void bits16(x, y, v) +unsigned int x, y, v; +{ + unsigned int b; + for (b = 0; b < 16; b++) { + rect(x + b * 30, y, 24, 24, (v & (1 << b)) ? 0xFF : 0x00); + } +} + +static void dot(n) +unsigned int n; +{ + rect(60, 650 + n * 44, 24, 24, 0xFF); +} + +static void settle() +{ + unsigned int i; + for (i = 0; i < 30; i++) { + } +} + +static void csr_w(reg, v) +unsigned int reg, v; +{ + *(unsigned short *)RAP = reg; + settle(); + *(unsigned short *)RDP = v; + settle(); +} + +static unsigned int csr_r(reg) +unsigned int reg; +{ + unsigned int v; + *(unsigned short *)RAP = reg; + settle(); + v = *(unsigned short *)RDP; + settle(); + return v; +} + +int main() +{ + unsigned int i, r, idle, ok, rxmd1, txmd1, mcnt; + unsigned int s0[2]; + + cell(0x000, 1); + dot(0); + csr_w(0, CSR0_STOP); + + rect(100, 650, 512, 256, 0x00); + rect(100, 650, 512, 4, 0xFF); + rect(100, 902, 512, 4, 0xFF); + rect(100, 650, 8, 256, 0xFF); + rect(608, 650, 8, 256, 0xFF); + + /* init block: mode, PADR, LADRF, RX addr/len, TX addr/len */ + w16(INITB + 0 * 16, MODE_LOOP | MODE_INTL); + w16(INITB + 1 * 16, 0x0008); + w16(INITB + 2 * 16, 0x162b); + w16(INITB + 3 * 16, 0x5a4b); + w16(INITB + 4 * 16, 0); + w16(INITB + 5 * 16, 0); + w16(INITB + 6 * 16, 0); + w16(INITB + 7 * 16, 0); + w16(INITB + 8 * 16, RXR >> 3 & 0xFFFF); /* ring addr low 16 */ + w16(INITB + 9 * 16, 3 << 13); /* length only: 2^3 = 8 */ + w16(INITB + 10 * 16, TXR >> 3 & 0xFFFF); + w16(INITB + 11 * 16, 3 << 13); + cell(0x040, INITB >> 3); + cell(0x060, RXR >> 3); + cell(0x080, TXR >> 3); + + /* RX descriptors: all point at the same buffer */ + for (i = 0; i < RING_ENTRIES; i++) { + w16(RXR + i * 64 + 0, (RXBUF >> 3) & 0xFFFF); + w16(RXR + i * 64 + 16, RMD1_OWN | (((RXBUF >> 3) >> 16) & 0xFF)); + w16(RXR + i * 64 + 32, 0xFA0A); /* 0xF000 | -1526: top nibble must be ones */ + w16(RXR + i * 64 + 48, 0); + } + + /* one 24-byte frame (MAME's loopback accepts 8..32 bytes) */ + wb(TXBUF + 0 * 8, 0x08); wb(TXBUF + 1 * 8, 0x00); + wb(TXBUF + 2 * 8, 0x2b); wb(TXBUF + 3 * 8, 0x16); + wb(TXBUF + 4 * 8, 0x4b); wb(TXBUF + 5 * 8, 0x5a); + wb(TXBUF + 6 * 8, 0x02); wb(TXBUF + 7 * 8, 0x00); + wb(TXBUF + 8 * 8, 0x00); wb(TXBUF + 9 * 8, 0x00); + wb(TXBUF + 10 * 8, 0x00); wb(TXBUF + 11 * 8, 0x01); + wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB5); + wb(TXBUF + 14 * 8, 0xDE); wb(TXBUF + 15 * 8, 0xAD); + wb(TXBUF + 16 * 8, 0xBE); wb(TXBUF + 17 * 8, 0xEF); + for (i = 18; i < 24; i++) { + wb(TXBUF + i * 8, i); + } + + w16(TXR + 0, (TXBUF >> 3) & 0xFFFF); + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + w16(TXR + 32, 0xFFE8); /* 0xF000 | -24: top nibble must be ones */ + w16(TXR + 48, 0); + + csr_w(1, (INITB >> 3) & 0xFFFF); + csr_w(2, ((INITB >> 3) >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + dot(1); + + r = 0; + for (idle = 0; idle < 20000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + cell(0x140, r); + if (!(r & CSR0_IDON)) { + cell(0x000, 0xEE); + return 1; + } + dot(2); + for (idle = 0; idle < 50000; idle++) { + } + + csr_w(0, CSR0_STRT); + for (idle = 0; idle < 50000; idle++) { + } + /* own the TX descriptor now, then kick with STRT|TDMD in ONE write: + * CSR0 is a command shadow, a bare TDMD write clears STRT. */ + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + csr_w(0, CSR0_STRT | CSR0_TDMD); + dot(3); + s0[0] = csr_r(0); + + for (idle = 0; idle < 100000; idle++) { + if (!(r16(RXR + 16) & RMD1_OWN)) { + dot(4); + break; + } + if (!(r16(TXR + 16) & TMD1_OWN)) { + dot(4); + } + } + s0[1] = csr_r(0); + + rxmd1 = r16(RXR + 16); + mcnt = r16(RXR + 48) & 0x0FFF; + txmd1 = r16(TXR + 16); + ok = 0; + if (*(unsigned char *)(RXBUF + 12 * 8) == 0x88 && + *(unsigned char *)(RXBUF + 13 * 8) == 0xB5 && + *(unsigned char *)(RXBUF + 14 * 8) == 0xDE) { + ok = 1; + } + cell(0x160, s0[0]); + cell(0x180, s0[1]); + cell(0x200, rxmd1); + cell(0x220, mcnt); + cell(0x240, txmd1); + cell(0x1A0, ok); + + bits16(120, 670, s0[0]); + bits16(120, 700, s0[1]); + bits16(120, 730, txmd1); + bits16(120, 760, rxmd1); + bits16(120, 790, mcnt); + if (ok) { + rect(120, 850, 24, 24, 0xFF); + } + if (!(txmd1 & TMD1_OWN)) { + rect(160, 850, 24, 24, 0xFF); + } + + /* best effort: give the chip back to the OS */ + { + unsigned int osblk; + osblk = *(unsigned int *)0x23E7A0; + if (osblk != 0) { + csr_w(0, CSR0_STOP); + csr_w(1, osblk & 0xFFFF); + csr_w(2, (osblk >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + for (idle = 0; idle < 10000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + csr_w(0, 0x0042); + } + } + return 0; +} diff --git a/harness/udpstream/lance_idonclear.c b/harness/udpstream/lance_idonclear.c new file mode 100644 index 0000000..6c27970 --- /dev/null +++ b/harness/udpstream/lance_idonclear.c @@ -0,0 +1,279 @@ +/* + * lance.c -- LANCE loopback probe using LOW-MEMORY rings. + * + * The OS's own init block (pointer at bit 0x23E7A0 -> byte 0xF80 on a live + * boot) puts its rings at byte 0x0A80 / 0x0E80 and stores only the LENGTH in + * the ring words' bits 13-15 (0xE000 / 0xA000), with no address bits above 16. + * MAME's model packs address bits 16+ into that word; if the real C-LANCE does + * not, a high ring address like 0x380028 is seen as 0x0028. This payload + * therefore places the block, rings and buffers below 64 KB. + * + * Memory used (BYTE addresses, well below 64 KB): + * 0x3000 init block (24 B) 0x3020 RX ring (8 * 8 B) 0x3060 TX ring (8 B) + * 0x3100 TX frame (24 B) 0x3200 RX buffer (2048 B) + * + * Result cells at 0x018C8000 (32-bit): 0x160/0x180 CSR0 samples, + * 0x200 RX RMD1, 0x220 RX MCNT, 0x240 TX TMD1, 0x1A0 payload-found flag. + * + * Panel rows (real unit, 256-byte stride): y 670/700 = CSR0, 730 = TMD1, + * 760 = RMD1, 790 = MCNT; blocks y 850: left = payload returned, right = TX done. + */ +#define RAP 0x05900000 +#define RDP 0x05800000 +#define MARK 0x018C8000 +#define VRAM_BASE 0x04000000 + +#define MODE_LOOP 0x0004 +#define MODE_INTL 0x0040 +#define CSR0_STOP 0x0004 +#define CSR0_STRT 0x0002 +#define CSR0_INIT 0x0001 +#define CSR0_TDMD 0x0008 +#define CSR0_IDON 0x0100 +#define RMD1_OWN 0x8000 +#define TMD1_OWN 0x8000 +#define TMD1_ENP 0x0100 +#define TMD1_STP 0x0200 + +#define B(byte) ((unsigned int)(byte) * 8) /* bit address of a byte */ +#define INITB B(0x3000) +#define RXR B(0x3020) +#define TXR B(0x3060) +#define RXBUF B(0x3200) +#define TXBUF B(0x3180) + +#define RING_ENTRIES 8 + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static void w16(bit, v) +unsigned int bit, v; +{ + *(unsigned short *)bit = v; +} + +static unsigned int r16(bit) +unsigned int bit; +{ + return *(unsigned short *)bit; +} + +static void wb(bit, v) +unsigned int bit, v; +{ + *(unsigned char *)bit = v; +} + +static void rect(x, y, w, h, v) +unsigned int x, y, w, h, v; +{ + unsigned int row, col; + for (row = 0; row < h; row++) { + for (col = 0; col < w; col = col + 8) { + *(unsigned char *)(VRAM_BASE + (y + row) * 2048 + x + col) = v; + } + } +} + +static void bits16(x, y, v) +unsigned int x, y, v; +{ + unsigned int b; + for (b = 0; b < 16; b++) { + rect(x + b * 30, y, 24, 24, (v & (1 << b)) ? 0xFF : 0x00); + } +} + +static void dot(n) +unsigned int n; +{ + rect(60, 650 + n * 44, 24, 24, 0xFF); +} + +static void settle() +{ + unsigned int i; + for (i = 0; i < 30; i++) { + } +} + +static void csr_w(reg, v) +unsigned int reg, v; +{ + *(unsigned short *)RAP = reg; + settle(); + *(unsigned short *)RDP = v; + settle(); +} + +static unsigned int csr_r(reg) +unsigned int reg; +{ + unsigned int v; + *(unsigned short *)RAP = reg; + settle(); + v = *(unsigned short *)RDP; + settle(); + return v; +} + +int main() +{ + unsigned int i, r, idle, ok, rxmd1, txmd1, mcnt; + unsigned int s0[2]; + + cell(0x000, 1); + dot(0); + csr_w(0, CSR0_STOP); + + rect(100, 650, 512, 256, 0x00); + rect(100, 650, 512, 4, 0xFF); + rect(100, 902, 512, 4, 0xFF); + rect(100, 650, 8, 256, 0xFF); + rect(608, 650, 8, 256, 0xFF); + + /* init block: mode, PADR, LADRF, RX addr/len, TX addr/len */ + w16(INITB + 0 * 16, MODE_LOOP | MODE_INTL); + w16(INITB + 1 * 16, 0x0008); + w16(INITB + 2 * 16, 0x162b); + w16(INITB + 3 * 16, 0x5a4b); + w16(INITB + 4 * 16, 0); + w16(INITB + 5 * 16, 0); + w16(INITB + 6 * 16, 0); + w16(INITB + 7 * 16, 0); + w16(INITB + 8 * 16, RXR >> 3 & 0xFFFF); /* ring addr low 16 */ + w16(INITB + 9 * 16, 3 << 13); /* length only: 2^3 = 8 */ + w16(INITB + 10 * 16, TXR >> 3 & 0xFFFF); + w16(INITB + 11 * 16, 3 << 13); + cell(0x040, INITB >> 3); + cell(0x060, RXR >> 3); + cell(0x080, TXR >> 3); + + /* RX descriptors: all point at the same buffer */ + for (i = 0; i < RING_ENTRIES; i++) { + w16(RXR + i * 64 + 0, (RXBUF >> 3) & 0xFFFF); + w16(RXR + i * 64 + 16, RMD1_OWN | (((RXBUF >> 3) >> 16) & 0xFF)); + w16(RXR + i * 64 + 32, 0x0A0A); /* 1526 bytes */ + w16(RXR + i * 64 + 48, 0); + } + + /* one 24-byte frame (MAME's loopback accepts 8..32 bytes) */ + wb(TXBUF + 0 * 8, 0x08); wb(TXBUF + 1 * 8, 0x00); + wb(TXBUF + 2 * 8, 0x2b); wb(TXBUF + 3 * 8, 0x16); + wb(TXBUF + 4 * 8, 0x4b); wb(TXBUF + 5 * 8, 0x5a); + wb(TXBUF + 6 * 8, 0x02); wb(TXBUF + 7 * 8, 0x00); + wb(TXBUF + 8 * 8, 0x00); wb(TXBUF + 9 * 8, 0x00); + wb(TXBUF + 10 * 8, 0x00); wb(TXBUF + 11 * 8, 0x01); + wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB5); + wb(TXBUF + 14 * 8, 0xDE); wb(TXBUF + 15 * 8, 0xAD); + wb(TXBUF + 16 * 8, 0xBE); wb(TXBUF + 17 * 8, 0xEF); + for (i = 18; i < 24; i++) { + wb(TXBUF + i * 8, i); + } + + w16(TXR + 0, (TXBUF >> 3) & 0xFFFF); + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + w16(TXR + 32, 0x0FE8); /* 24 bytes */ + w16(TXR + 48, 0); + + csr_w(1, (INITB >> 3) & 0xFFFF); + csr_w(2, ((INITB >> 3) >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + dot(1); + + r = 0; + for (idle = 0; idle < 20000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + cell(0x140, r); + if (!(r & CSR0_IDON)) { + cell(0x000, 0xEE); + return 1; + } + /* acknowledge IDON (write 1 to clear it) before starting, as Linux's + * drivers/net/ethernet/amd/7990.c does */ + csr_w(0, CSR0_IDON); + for (idle = 0; idle < 2000; idle++) { + } + dot(2); + for (idle = 0; idle < 50000; idle++) { + } + + csr_w(0, CSR0_STRT); + for (idle = 0; idle < 50000; idle++) { + } + /* own the TX descriptor now, then kick with STRT|TDMD in ONE write: + * CSR0 is a command shadow, a bare TDMD write clears STRT. */ + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + csr_w(0, CSR0_STRT | CSR0_TDMD); + dot(3); + s0[0] = csr_r(0); + + for (idle = 0; idle < 100000; idle++) { + if (!(r16(RXR + 16) & RMD1_OWN)) { + dot(4); + break; + } + if (!(r16(TXR + 16) & TMD1_OWN)) { + dot(4); + } + } + s0[1] = csr_r(0); + + rxmd1 = r16(RXR + 16); + mcnt = r16(RXR + 48) & 0x0FFF; + txmd1 = r16(TXR + 16); + ok = 0; + if (*(unsigned char *)(RXBUF + 12 * 8) == 0x88 && + *(unsigned char *)(RXBUF + 13 * 8) == 0xB5 && + *(unsigned char *)(RXBUF + 14 * 8) == 0xDE) { + ok = 1; + } + cell(0x160, s0[0]); + cell(0x180, s0[1]); + cell(0x200, rxmd1); + cell(0x220, mcnt); + cell(0x240, txmd1); + cell(0x1A0, ok); + + bits16(120, 670, s0[0]); + bits16(120, 700, s0[1]); + bits16(120, 730, txmd1); + bits16(120, 760, rxmd1); + bits16(120, 790, mcnt); + if (ok) { + rect(120, 850, 24, 24, 0xFF); + } + if (!(txmd1 & TMD1_OWN)) { + rect(160, 850, 24, 24, 0xFF); + } + + /* best effort: give the chip back to the OS */ + { + unsigned int osblk; + osblk = *(unsigned int *)0x23E7A0; + if (osblk != 0) { + csr_w(0, CSR0_STOP); + csr_w(1, osblk & 0xFFFF); + csr_w(2, (osblk >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + for (idle = 0; idle < 10000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + csr_w(0, 0x0042); + } + } + return 0; +} diff --git a/harness/udpstream/lance_init2.c b/harness/udpstream/lance_init2.c new file mode 100644 index 0000000..5f5fac6 --- /dev/null +++ b/harness/udpstream/lance_init2.c @@ -0,0 +1,275 @@ +/* + * lance.c -- LANCE loopback probe using LOW-MEMORY rings. + * + * The OS's own init block (pointer at bit 0x23E7A0 -> byte 0xF80 on a live + * boot) puts its rings at byte 0x0A80 / 0x0E80 and stores only the LENGTH in + * the ring words' bits 13-15 (0xE000 / 0xA000), with no address bits above 16. + * MAME's model packs address bits 16+ into that word; if the real C-LANCE does + * not, a high ring address like 0x380028 is seen as 0x0028. This payload + * therefore places the block, rings and buffers below 64 KB. + * + * Memory used (BYTE addresses, well below 64 KB): + * 0x3000 init block (24 B) 0x3020 RX ring (8 * 8 B) 0x3060 TX ring (8 B) + * 0x3100 TX frame (24 B) 0x3200 RX buffer (2048 B) + * + * Result cells at 0x018C8000 (32-bit): 0x160/0x180 CSR0 samples, + * 0x200 RX RMD1, 0x220 RX MCNT, 0x240 TX TMD1, 0x1A0 payload-found flag. + * + * Panel rows (real unit, 256-byte stride): y 670/700 = CSR0, 730 = TMD1, + * 760 = RMD1, 790 = MCNT; blocks y 850: left = payload returned, right = TX done. + */ +#define RAP 0x05900000 +#define RDP 0x05800000 +#define MARK 0x018C8000 +#define VRAM_BASE 0x04000000 + +#define MODE_LOOP 0x0004 +#define MODE_INTL 0x0040 +#define CSR0_STOP 0x0004 +#define CSR0_STRT 0x0002 +#define CSR0_INIT 0x0001 +#define CSR0_TDMD 0x0008 +#define CSR0_IDON 0x0100 +#define RMD1_OWN 0x8000 +#define TMD1_OWN 0x8000 +#define TMD1_ENP 0x0100 +#define TMD1_STP 0x0200 + +#define B(byte) ((unsigned int)(byte) * 8) /* bit address of a byte */ +#define INITB B(0x3000) +#define RXR B(0x3020) +#define TXR B(0x3060) +#define RXBUF B(0x3200) +#define TXBUF B(0x3180) + +#define RING_ENTRIES 8 + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static void w16(bit, v) +unsigned int bit, v; +{ + *(unsigned short *)bit = v; +} + +static unsigned int r16(bit) +unsigned int bit; +{ + return *(unsigned short *)bit; +} + +static void wb(bit, v) +unsigned int bit, v; +{ + *(unsigned char *)bit = v; +} + +static void rect(x, y, w, h, v) +unsigned int x, y, w, h, v; +{ + unsigned int row, col; + for (row = 0; row < h; row++) { + for (col = 0; col < w; col = col + 8) { + *(unsigned char *)(VRAM_BASE + (y + row) * 2048 + x + col) = v; + } + } +} + +static void bits16(x, y, v) +unsigned int x, y, v; +{ + unsigned int b; + for (b = 0; b < 16; b++) { + rect(x + b * 30, y, 24, 24, (v & (1 << b)) ? 0xFF : 0x00); + } +} + +static void dot(n) +unsigned int n; +{ + rect(60, 650 + n * 44, 24, 24, 0xFF); +} + +static void settle() +{ + unsigned int i; + for (i = 0; i < 30; i++) { + } +} + +static void csr_w(reg, v) +unsigned int reg, v; +{ + *(unsigned short *)RAP = reg; + settle(); + *(unsigned short *)RDP = v; + settle(); +} + +static unsigned int csr_r(reg) +unsigned int reg; +{ + unsigned int v; + *(unsigned short *)RAP = reg; + settle(); + v = *(unsigned short *)RDP; + settle(); + return v; +} + +int main() +{ + unsigned int i, r, idle, ok, rxmd1, txmd1, mcnt; + unsigned int s0[2]; + + cell(0x000, 1); + dot(0); + csr_w(0, CSR0_STOP); + + rect(100, 650, 512, 256, 0x00); + rect(100, 650, 512, 4, 0xFF); + rect(100, 902, 512, 4, 0xFF); + rect(100, 650, 8, 256, 0xFF); + rect(608, 650, 8, 256, 0xFF); + + /* init block: mode, PADR, LADRF, RX addr/len, TX addr/len */ + w16(INITB + 0 * 16, MODE_LOOP | MODE_INTL); + w16(INITB + 1 * 16, 0x0008); + w16(INITB + 2 * 16, 0x162b); + w16(INITB + 3 * 16, 0x5a4b); + w16(INITB + 4 * 16, 0); + w16(INITB + 5 * 16, 0); + w16(INITB + 6 * 16, 0); + w16(INITB + 7 * 16, 0); + w16(INITB + 8 * 16, RXR >> 3 & 0xFFFF); /* ring addr low 16 */ + w16(INITB + 9 * 16, 3 << 13); /* length only: 2^3 = 8 */ + w16(INITB + 10 * 16, TXR >> 3 & 0xFFFF); + w16(INITB + 11 * 16, 3 << 13); + cell(0x040, INITB >> 3); + cell(0x060, RXR >> 3); + cell(0x080, TXR >> 3); + + /* RX descriptors: all point at the same buffer */ + for (i = 0; i < RING_ENTRIES; i++) { + w16(RXR + i * 64 + 0, (RXBUF >> 3) & 0xFFFF); + w16(RXR + i * 64 + 16, RMD1_OWN | (((RXBUF >> 3) >> 16) & 0xFF)); + w16(RXR + i * 64 + 32, 0x0A0A); /* 1526 bytes */ + w16(RXR + i * 64 + 48, 0); + } + + /* one 24-byte frame (MAME's loopback accepts 8..32 bytes) */ + wb(TXBUF + 0 * 8, 0x08); wb(TXBUF + 1 * 8, 0x00); + wb(TXBUF + 2 * 8, 0x2b); wb(TXBUF + 3 * 8, 0x16); + wb(TXBUF + 4 * 8, 0x4b); wb(TXBUF + 5 * 8, 0x5a); + wb(TXBUF + 6 * 8, 0x02); wb(TXBUF + 7 * 8, 0x00); + wb(TXBUF + 8 * 8, 0x00); wb(TXBUF + 9 * 8, 0x00); + wb(TXBUF + 10 * 8, 0x00); wb(TXBUF + 11 * 8, 0x01); + wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB5); + wb(TXBUF + 14 * 8, 0xDE); wb(TXBUF + 15 * 8, 0xAD); + wb(TXBUF + 16 * 8, 0xBE); wb(TXBUF + 17 * 8, 0xEF); + for (i = 18; i < 24; i++) { + wb(TXBUF + i * 8, i); + } + + w16(TXR + 0, (TXBUF >> 3) & 0xFFFF); + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + w16(TXR + 32, 0x0FE8); /* 24 bytes */ + w16(TXR + 48, 0); + + csr_w(1, (INITB >> 3) & 0xFFFF); + csr_w(2, ((INITB >> 3) >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + csr_w(0, CSR0_INIT); /* second INIT before IDON: does the chip keep INIT set? */ + dot(1); + + r = 0; + for (idle = 0; idle < 20000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + cell(0x140, r); + if (!(r & CSR0_IDON)) { + cell(0x000, 0xEE); + return 1; + } + dot(2); + for (idle = 0; idle < 50000; idle++) { + } + + csr_w(0, CSR0_STRT); + for (idle = 0; idle < 50000; idle++) { + } + /* own the TX descriptor now, then kick with STRT|TDMD in ONE write: + * CSR0 is a command shadow, a bare TDMD write clears STRT. */ + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + csr_w(0, CSR0_STRT | CSR0_TDMD); + dot(3); + s0[0] = csr_r(0); + + for (idle = 0; idle < 100000; idle++) { + if (!(r16(RXR + 16) & RMD1_OWN)) { + dot(4); + break; + } + if (!(r16(TXR + 16) & TMD1_OWN)) { + dot(4); + } + } + s0[1] = csr_r(0); + + rxmd1 = r16(RXR + 16); + mcnt = r16(RXR + 48) & 0x0FFF; + txmd1 = r16(TXR + 16); + ok = 0; + if (*(unsigned char *)(RXBUF + 12 * 8) == 0x88 && + *(unsigned char *)(RXBUF + 13 * 8) == 0xB5 && + *(unsigned char *)(RXBUF + 14 * 8) == 0xDE) { + ok = 1; + } + cell(0x160, s0[0]); + cell(0x180, s0[1]); + cell(0x200, rxmd1); + cell(0x220, mcnt); + cell(0x240, txmd1); + cell(0x1A0, ok); + + bits16(120, 670, s0[0]); + bits16(120, 700, s0[1]); + bits16(120, 730, txmd1); + bits16(120, 760, rxmd1); + bits16(120, 790, mcnt); + if (ok) { + rect(120, 850, 24, 24, 0xFF); + } + if (!(txmd1 & TMD1_OWN)) { + rect(160, 850, 24, 24, 0xFF); + } + + /* best effort: give the chip back to the OS */ + { + unsigned int osblk; + osblk = *(unsigned int *)0x23E7A0; + if (osblk != 0) { + csr_w(0, CSR0_STOP); + csr_w(1, osblk & 0xFFFF); + csr_w(2, (osblk >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + for (idle = 0; idle < 10000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + csr_w(0, 0x0042); + } + } + return 0; +} diff --git a/harness/udpstream/lance_longwait.c b/harness/udpstream/lance_longwait.c new file mode 100644 index 0000000..aa0019a --- /dev/null +++ b/harness/udpstream/lance_longwait.c @@ -0,0 +1,288 @@ +/* + * lance.c -- LANCE loopback probe using LOW-MEMORY rings. + * + * The OS's own init block (pointer at bit 0x23E7A0 -> byte 0xF80 on a live + * boot) puts its rings at byte 0x0A80 / 0x0E80 and stores only the LENGTH in + * the ring words' bits 13-15 (0xE000 / 0xA000), with no address bits above 16. + * MAME's model packs address bits 16+ into that word; if the real C-LANCE does + * not, a high ring address like 0x380028 is seen as 0x0028. This payload + * therefore places the block, rings and buffers below 64 KB. + * + * Memory used (BYTE addresses, well below 64 KB): + * 0x3000 init block (24 B) 0x3020 RX ring (8 * 8 B) 0x3060 TX ring (8 B) + * 0x3100 TX frame (24 B) 0x3200 RX buffer (2048 B) + * + * Result cells at 0x018C8000 (32-bit): 0x160/0x180 CSR0 samples, + * 0x200 RX RMD1, 0x220 RX MCNT, 0x240 TX TMD1, 0x1A0 payload-found flag. + * + * Panel rows (real unit, 256-byte stride): y 670/700 = CSR0, 730 = TMD1, + * 760 = RMD1, 790 = MCNT; blocks y 850: left = payload returned, right = TX done. + */ +#define RAP 0x05900000 +#define RDP 0x05800000 +#define MARK 0x018C8000 +#define VRAM_BASE 0x04000000 + +#define MODE_LOOP 0x0004 +#define MODE_INTL 0x0040 +#define CSR0_STOP 0x0004 +#define CSR0_STRT 0x0002 +#define CSR0_INIT 0x0001 +#define CSR0_TDMD 0x0008 +#define CSR0_IDON 0x0100 +#define RMD1_OWN 0x8000 +#define TMD1_OWN 0x8000 +#define TMD1_ENP 0x0100 +#define TMD1_STP 0x0200 + +#define B(byte) ((unsigned int)(byte) * 8) /* bit address of a byte */ +#define INITB B(0x3000) +#define RXR B(0x3020) +#define TXR B(0x3060) +#define RXBUF B(0x3200) +#define TXBUF B(0x3180) + +#define RING_ENTRIES 8 + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static void w16(bit, v) +unsigned int bit, v; +{ + *(unsigned short *)bit = v; +} + +static unsigned int r16(bit) +unsigned int bit; +{ + return *(unsigned short *)bit; +} + +static void wb(bit, v) +unsigned int bit, v; +{ + *(unsigned char *)bit = v; +} + +static void rect(x, y, w, h, v) +unsigned int x, y, w, h, v; +{ + unsigned int row, col; + for (row = 0; row < h; row++) { + for (col = 0; col < w; col = col + 8) { + *(unsigned char *)(VRAM_BASE + (y + row) * 2048 + x + col) = v; + } + } +} + +static void bits16(x, y, v) +unsigned int x, y, v; +{ + unsigned int b; + for (b = 0; b < 16; b++) { + rect(x + b * 30, y, 24, 24, (v & (1 << b)) ? 0xFF : 0x00); + } +} + +static void dot(n) +unsigned int n; +{ + rect(60, 650 + n * 44, 24, 24, 0xFF); +} + +static void settle() +{ + unsigned int i; + for (i = 0; i < 30; i++) { + } +} + +static void csr_w(reg, v) +unsigned int reg, v; +{ + *(unsigned short *)RAP = reg; + settle(); + *(unsigned short *)RDP = v; + settle(); +} + +static unsigned int csr_r(reg) +unsigned int reg; +{ + unsigned int v; + *(unsigned short *)RAP = reg; + settle(); + v = *(unsigned short *)RDP; + settle(); + return v; +} + +int main() +{ + unsigned int i, r, idle, ok, rxmd1, txmd1, mcnt, tdmd_seen, samples; + unsigned int s0[2]; + + cell(0x000, 1); + dot(0); + csr_w(0, CSR0_STOP); + + rect(100, 650, 512, 256, 0x00); + rect(100, 650, 512, 4, 0xFF); + rect(100, 902, 512, 4, 0xFF); + rect(100, 650, 8, 256, 0xFF); + rect(608, 650, 8, 256, 0xFF); + + /* init block: mode, PADR, LADRF, RX addr/len, TX addr/len */ + w16(INITB + 0 * 16, MODE_LOOP | MODE_INTL); + w16(INITB + 1 * 16, 0x0008); + w16(INITB + 2 * 16, 0x162b); + w16(INITB + 3 * 16, 0x5a4b); + w16(INITB + 4 * 16, 0); + w16(INITB + 5 * 16, 0); + w16(INITB + 6 * 16, 0); + w16(INITB + 7 * 16, 0); + w16(INITB + 8 * 16, RXR >> 3 & 0xFFFF); /* ring addr low 16 */ + w16(INITB + 9 * 16, 3 << 13); /* length only: 2^3 = 8 */ + w16(INITB + 10 * 16, TXR >> 3 & 0xFFFF); + w16(INITB + 11 * 16, 3 << 13); + cell(0x040, INITB >> 3); + cell(0x060, RXR >> 3); + cell(0x080, TXR >> 3); + + /* RX descriptors: all point at the same buffer */ + for (i = 0; i < RING_ENTRIES; i++) { + w16(RXR + i * 64 + 0, (RXBUF >> 3) & 0xFFFF); + w16(RXR + i * 64 + 16, RMD1_OWN | (((RXBUF >> 3) >> 16) & 0xFF)); + w16(RXR + i * 64 + 32, 0x0A0A); /* 1526 bytes */ + w16(RXR + i * 64 + 48, 0); + } + + /* one 24-byte frame (MAME's loopback accepts 8..32 bytes) */ + wb(TXBUF + 0 * 8, 0x08); wb(TXBUF + 1 * 8, 0x00); + wb(TXBUF + 2 * 8, 0x2b); wb(TXBUF + 3 * 8, 0x16); + wb(TXBUF + 4 * 8, 0x4b); wb(TXBUF + 5 * 8, 0x5a); + wb(TXBUF + 6 * 8, 0x02); wb(TXBUF + 7 * 8, 0x00); + wb(TXBUF + 8 * 8, 0x00); wb(TXBUF + 9 * 8, 0x00); + wb(TXBUF + 10 * 8, 0x00); wb(TXBUF + 11 * 8, 0x01); + wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB5); + wb(TXBUF + 14 * 8, 0xDE); wb(TXBUF + 15 * 8, 0xAD); + wb(TXBUF + 16 * 8, 0xBE); wb(TXBUF + 17 * 8, 0xEF); + for (i = 18; i < 24; i++) { + wb(TXBUF + i * 8, i); + } + + w16(TXR + 0, (TXBUF >> 3) & 0xFFFF); + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + w16(TXR + 32, 0x0FE8); /* 24 bytes */ + w16(TXR + 48, 0); + + csr_w(1, (INITB >> 3) & 0xFFFF); + csr_w(2, ((INITB >> 3) >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + dot(1); + + r = 0; + for (idle = 0; idle < 20000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + cell(0x140, r); + if (!(r & CSR0_IDON)) { + cell(0x000, 0xEE); + return 1; + } + dot(2); + for (idle = 0; idle < 50000; idle++) { + } + + csr_w(0, CSR0_STRT); + for (idle = 0; idle < 50000; idle++) { + } + /* own the TX descriptor now, then kick with STRT|TDMD in ONE write: + * CSR0 is a command shadow, a bare TDMD write clears STRT. */ + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + csr_w(0, CSR0_STRT | CSR0_TDMD); + dot(3); + /* long wait (600k iterations) with sparse CSR0 sampling: 600 polls */ + tdmd_seen = 0; + samples = 0; + for (idle = 0; idle < 600000; idle++) { + if ((idle % 1000) == 0) { + r = csr_r(0); + samples = samples + 1; + if (r & CSR0_TDMD) { + tdmd_seen = tdmd_seen + 1; + } + } + } + cell(0x1C0, csr_r(0)); /* CSR0 after the long wait */ + cell(0x1E0, tdmd_seen); /* polls that still saw TDMD set (of `samples`) */ + s0[0] = csr_r(0); + + for (idle = 0; idle < 100000; idle++) { + if (!(r16(RXR + 16) & RMD1_OWN)) { + dot(4); + break; + } + if (!(r16(TXR + 16) & TMD1_OWN)) { + dot(4); + } + } + s0[1] = csr_r(0); + + rxmd1 = r16(RXR + 16); + mcnt = r16(RXR + 48) & 0x0FFF; + txmd1 = r16(TXR + 16); + ok = 0; + if (*(unsigned char *)(RXBUF + 12 * 8) == 0x88 && + *(unsigned char *)(RXBUF + 13 * 8) == 0xB5 && + *(unsigned char *)(RXBUF + 14 * 8) == 0xDE) { + ok = 1; + } + cell(0x160, s0[0]); + cell(0x180, s0[1]); + cell(0x200, rxmd1); + cell(0x220, mcnt); + cell(0x240, txmd1); + cell(0x1A0, ok); + + bits16(120, 670, s0[0]); + bits16(120, 700, s0[1]); + bits16(120, 730, txmd1); + bits16(120, 760, rxmd1); + bits16(120, 790, mcnt); + if (ok) { + rect(120, 850, 24, 24, 0xFF); + } + if (!(txmd1 & TMD1_OWN)) { + rect(160, 850, 24, 24, 0xFF); + } + + /* best effort: give the chip back to the OS */ + { + unsigned int osblk; + osblk = *(unsigned int *)0x23E7A0; + if (osblk != 0) { + csr_w(0, CSR0_STOP); + csr_w(1, osblk & 0xFFFF); + csr_w(2, (osblk >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + for (idle = 0; idle < 10000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + csr_w(0, 0x0042); + } + } + return 0; +} diff --git a/harness/udpstream/lance_stopinit.c b/harness/udpstream/lance_stopinit.c new file mode 100644 index 0000000..c78b9eb --- /dev/null +++ b/harness/udpstream/lance_stopinit.c @@ -0,0 +1,275 @@ +/* + * lance.c -- LANCE loopback probe using LOW-MEMORY rings. + * + * The OS's own init block (pointer at bit 0x23E7A0 -> byte 0xF80 on a live + * boot) puts its rings at byte 0x0A80 / 0x0E80 and stores only the LENGTH in + * the ring words' bits 13-15 (0xE000 / 0xA000), with no address bits above 16. + * MAME's model packs address bits 16+ into that word; if the real C-LANCE does + * not, a high ring address like 0x380028 is seen as 0x0028. This payload + * therefore places the block, rings and buffers below 64 KB. + * + * Memory used (BYTE addresses, well below 64 KB): + * 0x3000 init block (24 B) 0x3020 RX ring (8 * 8 B) 0x3060 TX ring (8 B) + * 0x3100 TX frame (24 B) 0x3200 RX buffer (2048 B) + * + * Result cells at 0x018C8000 (32-bit): 0x160/0x180 CSR0 samples, + * 0x200 RX RMD1, 0x220 RX MCNT, 0x240 TX TMD1, 0x1A0 payload-found flag. + * + * Panel rows (real unit, 256-byte stride): y 670/700 = CSR0, 730 = TMD1, + * 760 = RMD1, 790 = MCNT; blocks y 850: left = payload returned, right = TX done. + */ +#define RAP 0x05900000 +#define RDP 0x05800000 +#define MARK 0x018C8000 +#define VRAM_BASE 0x04000000 + +#define MODE_LOOP 0x0004 +#define MODE_INTL 0x0040 +#define CSR0_STOP 0x0004 +#define CSR0_STRT 0x0002 +#define CSR0_INIT 0x0001 +#define CSR0_TDMD 0x0008 +#define CSR0_IDON 0x0100 +#define RMD1_OWN 0x8000 +#define TMD1_OWN 0x8000 +#define TMD1_ENP 0x0100 +#define TMD1_STP 0x0200 + +#define B(byte) ((unsigned int)(byte) * 8) /* bit address of a byte */ +#define INITB B(0x3000) +#define RXR B(0x3020) +#define TXR B(0x3060) +#define RXBUF B(0x3200) +#define TXBUF B(0x3180) + +#define RING_ENTRIES 8 + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static void w16(bit, v) +unsigned int bit, v; +{ + *(unsigned short *)bit = v; +} + +static unsigned int r16(bit) +unsigned int bit; +{ + return *(unsigned short *)bit; +} + +static void wb(bit, v) +unsigned int bit, v; +{ + *(unsigned char *)bit = v; +} + +static void rect(x, y, w, h, v) +unsigned int x, y, w, h, v; +{ + unsigned int row, col; + for (row = 0; row < h; row++) { + for (col = 0; col < w; col = col + 8) { + *(unsigned char *)(VRAM_BASE + (y + row) * 2048 + x + col) = v; + } + } +} + +static void bits16(x, y, v) +unsigned int x, y, v; +{ + unsigned int b; + for (b = 0; b < 16; b++) { + rect(x + b * 30, y, 24, 24, (v & (1 << b)) ? 0xFF : 0x00); + } +} + +static void dot(n) +unsigned int n; +{ + rect(60, 650 + n * 44, 24, 24, 0xFF); +} + +static void settle() +{ + unsigned int i; + for (i = 0; i < 30; i++) { + } +} + +static void csr_w(reg, v) +unsigned int reg, v; +{ + *(unsigned short *)RAP = reg; + settle(); + *(unsigned short *)RDP = v; + settle(); +} + +static unsigned int csr_r(reg) +unsigned int reg; +{ + unsigned int v; + *(unsigned short *)RAP = reg; + settle(); + v = *(unsigned short *)RDP; + settle(); + return v; +} + +int main() +{ + unsigned int i, r, idle, ok, rxmd1, txmd1, mcnt; + unsigned int s0[2]; + + cell(0x000, 1); + dot(0); + csr_w(0, CSR0_STOP); + + rect(100, 650, 512, 256, 0x00); + rect(100, 650, 512, 4, 0xFF); + rect(100, 902, 512, 4, 0xFF); + rect(100, 650, 8, 256, 0xFF); + rect(608, 650, 8, 256, 0xFF); + + /* init block: mode, PADR, LADRF, RX addr/len, TX addr/len */ + w16(INITB + 0 * 16, MODE_LOOP | MODE_INTL); + w16(INITB + 1 * 16, 0x0008); + w16(INITB + 2 * 16, 0x162b); + w16(INITB + 3 * 16, 0x5a4b); + w16(INITB + 4 * 16, 0); + w16(INITB + 5 * 16, 0); + w16(INITB + 6 * 16, 0); + w16(INITB + 7 * 16, 0); + w16(INITB + 8 * 16, RXR >> 3 & 0xFFFF); /* ring addr low 16 */ + w16(INITB + 9 * 16, 3 << 13); /* length only: 2^3 = 8 */ + w16(INITB + 10 * 16, TXR >> 3 & 0xFFFF); + w16(INITB + 11 * 16, 3 << 13); + cell(0x040, INITB >> 3); + cell(0x060, RXR >> 3); + cell(0x080, TXR >> 3); + + /* RX descriptors: all point at the same buffer */ + for (i = 0; i < RING_ENTRIES; i++) { + w16(RXR + i * 64 + 0, (RXBUF >> 3) & 0xFFFF); + w16(RXR + i * 64 + 16, RMD1_OWN | (((RXBUF >> 3) >> 16) & 0xFF)); + w16(RXR + i * 64 + 32, 0x0A0A); /* 1526 bytes */ + w16(RXR + i * 64 + 48, 0); + } + + /* one 24-byte frame (MAME's loopback accepts 8..32 bytes) */ + wb(TXBUF + 0 * 8, 0x08); wb(TXBUF + 1 * 8, 0x00); + wb(TXBUF + 2 * 8, 0x2b); wb(TXBUF + 3 * 8, 0x16); + wb(TXBUF + 4 * 8, 0x4b); wb(TXBUF + 5 * 8, 0x5a); + wb(TXBUF + 6 * 8, 0x02); wb(TXBUF + 7 * 8, 0x00); + wb(TXBUF + 8 * 8, 0x00); wb(TXBUF + 9 * 8, 0x00); + wb(TXBUF + 10 * 8, 0x00); wb(TXBUF + 11 * 8, 0x01); + wb(TXBUF + 12 * 8, 0x88); wb(TXBUF + 13 * 8, 0xB5); + wb(TXBUF + 14 * 8, 0xDE); wb(TXBUF + 15 * 8, 0xAD); + wb(TXBUF + 16 * 8, 0xBE); wb(TXBUF + 17 * 8, 0xEF); + for (i = 18; i < 24; i++) { + wb(TXBUF + i * 8, i); + } + + w16(TXR + 0, (TXBUF >> 3) & 0xFFFF); + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + w16(TXR + 32, 0x0FE8); /* 24 bytes */ + w16(TXR + 48, 0); + + csr_w(1, (INITB >> 3) & 0xFFFF); + csr_w(2, ((INITB >> 3) >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_STOP); /* STOP immediately before INIT (datasheet order) */ + csr_w(0, CSR0_INIT); + dot(1); + + r = 0; + for (idle = 0; idle < 20000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + cell(0x140, r); + if (!(r & CSR0_IDON)) { + cell(0x000, 0xEE); + return 1; + } + dot(2); + for (idle = 0; idle < 50000; idle++) { + } + + csr_w(0, CSR0_STRT); + for (idle = 0; idle < 50000; idle++) { + } + /* own the TX descriptor now, then kick with STRT|TDMD in ONE write: + * CSR0 is a command shadow, a bare TDMD write clears STRT. */ + w16(TXR + 16, TMD1_OWN | TMD1_STP | TMD1_ENP | (((TXBUF >> 3) >> 16) & 0xFF)); + csr_w(0, CSR0_STRT | CSR0_TDMD); + dot(3); + s0[0] = csr_r(0); + + for (idle = 0; idle < 100000; idle++) { + if (!(r16(RXR + 16) & RMD1_OWN)) { + dot(4); + break; + } + if (!(r16(TXR + 16) & TMD1_OWN)) { + dot(4); + } + } + s0[1] = csr_r(0); + + rxmd1 = r16(RXR + 16); + mcnt = r16(RXR + 48) & 0x0FFF; + txmd1 = r16(TXR + 16); + ok = 0; + if (*(unsigned char *)(RXBUF + 12 * 8) == 0x88 && + *(unsigned char *)(RXBUF + 13 * 8) == 0xB5 && + *(unsigned char *)(RXBUF + 14 * 8) == 0xDE) { + ok = 1; + } + cell(0x160, s0[0]); + cell(0x180, s0[1]); + cell(0x200, rxmd1); + cell(0x220, mcnt); + cell(0x240, txmd1); + cell(0x1A0, ok); + + bits16(120, 670, s0[0]); + bits16(120, 700, s0[1]); + bits16(120, 730, txmd1); + bits16(120, 760, rxmd1); + bits16(120, 790, mcnt); + if (ok) { + rect(120, 850, 24, 24, 0xFF); + } + if (!(txmd1 & TMD1_OWN)) { + rect(160, 850, 24, 24, 0xFF); + } + + /* best effort: give the chip back to the OS */ + { + unsigned int osblk; + osblk = *(unsigned int *)0x23E7A0; + if (osblk != 0) { + csr_w(0, CSR0_STOP); + csr_w(1, osblk & 0xFFFF); + csr_w(2, (osblk >> 16) & 0xFF); + csr_w(3, 0); + csr_w(0, CSR0_INIT); + for (idle = 0; idle < 10000; idle++) { + r = csr_r(0); + if (r & CSR0_IDON) { + break; + } + } + csr_w(0, 0x0042); + } + } + return 0; +} diff --git a/harness/udpstream/romapi.h b/harness/udpstream/romapi.h new file mode 100644 index 0000000..49f2d37 --- /dev/null +++ b/harness/udpstream/romapi.h @@ -0,0 +1,93 @@ +/* + * romapi.h -- the VT1200 firmware entry points a C payload may call. + * + * Every address here is a TMS34010 BIT address (= Ghidra byte address x 8). + * The evidence for each is dwt/notes/TCPIP.md (sockets) and dwt/notes/HEAP.md + * (allocator); the ones marked "verified live" have been called from a payload + * on real hardware, not just read out of the ROM. + * + * K&R C only. This compiler rejects ANSI prototypes (`int f(int a)` is a + * syntax error), so the declarations below are old-style: the parameter lists + * live in the comments, and the compiler checks nothing but the name. That is + * the price of using the 1987 toolchain; keep the comments honest. + * + * Calling convention: the C compiler and the firmware agree -- arguments are + * widened to 32 bits and pushed upward on the program stack (A14), first + * argument last, and the result comes back in A8. The thunks in entry.asm + * forward with `calla`, so the caller's already-pushed arguments pass through + * untouched and only A8 has to survive. + */ +#ifndef ROMAPI_H +#define ROMAPI_H + +/* ---- ROM entry points -------------------------------------------------- */ +#define ROM_SOCKET 0x3F5C9A00 /* Ghidra 07EB9340 verified live */ +#define ROM_BIND 0x3F5CAA80 /* Ghidra 07EB9550 BOOTP caller */ +#define ROM_LISTEN 0x3F5CBCD0 /* Ghidra 07EB979A inetd caller */ +#define ROM_ACCEPT 0x3F5CCB40 /* Ghidra 07EB9968 inetd caller */ +#define ROM_SENDTO 0x3F5D3D70 /* Ghidra 07EBA7AE verified live */ +#define ROM_RECVFROM 0x3F5D7D50 /* Ghidra 07EBAFAA verified live */ +#define ROM_READ 0x3F5D8E70 /* Ghidra 07EBB1CE rdebug / Xlib */ +#define ROM_CLOSE 0x3F5E8930 /* Ghidra 07EBD126 verified live */ +#define ROM_SETSOCKOPT 0x3F5DD260 /* Ghidra 07EBBA4C BOOTP / Xlib */ +#define ROM_DELAY 0x3F833A20 /* Ghidra 07F66744 sleep(ms), telnet caller */ +#define ROM_SELECT 0x3F871CE0 /* Ghidra 07F0E39C Xlib caller, candidate */ +#define ROM_MALLOC 0x3F701230 /* Ghidra 07EE0246 verified live */ +#define ROM_FREE 0x3F702440 /* Ghidra 07EE0488 allocator */ +#define ROM_CREATE_PROC 0x3F706A40 /* Ghidra 07EE0D48 GUI work */ + +/* ---- thunks implemented in entry.asm ----------------------------------- */ +/* + * int socket_(domain, type, protocol) -> fd, or -1 + * int bind_(fd, sockaddr *, len) -> 0, or -1 + * int recvfrom_(fd, buf, len, flags, from *, fromlen *) + * -> bytes, or -1 + * int close_(fd) -> 0, or -1 + * unsigned int malloc_(bytes) -> BIT pointer, or 0 + * int free_(bit pointer) -> 0 + * + * `from` is a 16-byte sockaddr the stack fills in; `fromlen` points to an + * int that must be 16 on entry (the call writes back the real length). + */ +int socket_(); +int bind_(); +int recvfrom_(); +int close_(); +int listen_(); +int accept_(); +int read_(); +int select_(); +int delay_(); +unsigned int malloc_(); +int free_(); + +/* ---- socket constants -------------------------------------------------- */ +#define AF_INET 2 +#define SOCK_DGRAM 2 +#define SOCK_STREAM 1 +#define IPPROTO_UDP 0 + +#define SOCKADDR_LEN 16 +/* + * struct sockaddr, 16 bytes, NETWORK byte order: + * +0 u16 family (AF_INET = 2 -> bytes 00 02) + * +2 u16 port (high byte first, e.g. 7000 = 0x1B58 -> 1B 58) + * +4 u32 addr (0 = INADDR_ANY) + * +8 u8 zero[8] + */ + +/* Fill sa (an unsigned char[16]) with AF_INET, `port`, and INADDR_ANY. + * `port` is in host order so callers can write 7000 rather than 0x1B58. */ +static void sa_any(sa, port) +unsigned char *sa; +unsigned int port; +{ + sa[0] = 0; sa[1] = AF_INET; + sa[2] = (port >> 8) & 0xFF; + sa[3] = port & 0xFF; + sa[4] = 0; sa[5] = 0; sa[6] = 0; sa[7] = 0; + sa[8] = 0; sa[9] = 0; sa[10] = 0; sa[11] = 0; + sa[12] = 0; sa[13] = 0; sa[14] = 0; sa[15] = 0; +} + +#endif /* ROMAPI_H */ diff --git a/harness/udpstream/send.py b/harness/udpstream/send.py new file mode 100644 index 0000000..7ff4f9c --- /dev/null +++ b/harness/udpstream/send.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Host sender for the VT1200 udpstream spike. + +Sends full 1280x1024 frames as 8-row datagrams: + + +0 u16 start row (LE) +2 u16 row count +4 rows*160 bytes + +Usage: + python3 harness/udpstream/send.py --host 100.103.100.103 --fps 5 + python3 harness/udpstream/send.py --host 100.103.100.103 --fps 0 --seconds 5 + +The frame is a black field with a white bar (encoding the frame number), a +moving box, and a border, so a screenshot proves the whole frame arrived and +which frame it was. +""" +from __future__ import annotations + +import argparse +import socket +import struct +import sys +import time + +W, H = 1280, 1024 +ROW_BYTES = W // 8 # 160 +ROWS_PER_PKT = 8 +PORT = 7000 + + +def frame(n: int) -> bytes: + """One 1-bpp frame, packed rows of ROW_BYTES, LSB-first.""" + rows = bytearray(H * ROW_BYTES) + # border: rows 0,1,H-2,H-1 and columns 0..3 / W-4..W-1 white + for y in (0, 1, H - 2, H - 1): + rows[y * ROW_BYTES:(y + 1) * ROW_BYTES] = b"\xff" * ROW_BYTES + for y in range(H): + r = rows[y * ROW_BYTES:(y + 1) * ROW_BYTES] + r[0] = 0xFF + r[ROW_BYTES - 1] = 0xFF + rows[y * ROW_BYTES:(y + 1) * ROW_BYTES] = r + + # moving 128x128 box + x0 = (n * 37) % (W - 128) + y0 = (n * 23) % (H - 128) + for y in range(y0, y0 + 128): + base = y * ROW_BYTES + for xb in range(x0 // 8, (x0 + 128) // 8): + rows[base + xb] = 0xFF + + # frame-number bar on row 64: 16 pixels of white per 16-frame step + y = 64 + base = y * ROW_BYTES + for i in range(n % 64): + x = 32 + i * 16 + rows[base + (x // 8)] |= 0xFF + rows[base + (x // 8) + 1] |= 0xFF + + return bytes(rows) + + +def main(argv: list[str]) -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--host", default="100.103.100.103") + ap.add_argument("--port", type=int, default=PORT) + ap.add_argument("--fps", type=float, default=5.0, + help="target frames/s; 0 = as fast as possible") + ap.add_argument("--pkt-gap-ms", type=float, default=0.0, + help="sleep between datagrams; gives the cooperative OS a " + "chance to run (a continuous burst starves it)") + ap.add_argument("--seconds", type=float, default=0, + help="stop after N seconds (0 = run until Ctrl-C)") + a = ap.parse_args(argv[1:]) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 20) + dst = (a.host, a.port) + + n = sent_bytes = sent_pkts = 0 + t0 = last = time.monotonic() + frames_at_last = 0 + + try: + while True: + f = frame(n) + for r in range(0, H, ROWS_PER_PKT): + payload = f[r * ROW_BYTES:(r + ROWS_PER_PKT) * ROW_BYTES] + pkt = struct.pack("= 1.0: + rate = (n - frames_at_last) / (now - last) + print(f"{n:6d} frames {rate:5.2f} fps " + f"{sent_bytes / (now - t0) / 1e6:5.3f} MB/s " + f"{sent_pkts / (now - t0):6.0f} pkt/s", flush=True) + last = now + frames_at_last = n + if a.seconds and now - t0 >= a.seconds: + break + if a.fps > 0: + sleep = t0 + n / a.fps - time.monotonic() + if sleep > 0: + time.sleep(sleep) + except KeyboardInterrupt: + pass + finally: + dt = time.monotonic() - t0 + print(f"sent {n} frames, {sent_bytes} bytes in {dt:.1f}s " + f"({sent_bytes / dt / 1e6:.2f} MB/s, {sent_pkts / dt:.0f} pkt/s)", + flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/harness/udpstream/tcpstream.c b/harness/udpstream/tcpstream.c new file mode 100644 index 0000000..887d6a9 --- /dev/null +++ b/harness/udpstream/tcpstream.c @@ -0,0 +1,121 @@ +/* + * tcpstream.c -- measure the firmware's TCP receive throughput. + * + * The terminal is the server: socket(SOCK_STREAM) -> bind -> listen -> accept, + * then read() into a buffer and copy the bytes into video RAM in order. One + * read() can return a whole socket-buffer's worth of bytes, so this can beat + * the per-datagram recvfrom cost -- by how much is the question. + * + * Host-written cells (before the load): + * 0x018C9000 byte limit (0 = unlimited) + * 0x018C9040 TCP port (0 = 7009) + * + * Markers at 0x018C8000: + * +0x000 stage: 1 entered, 2 socket, 3 bound, 4 listening, 5 accepted, + * 0xEE socket failed, 0xEF bind failed, 0xF0 listen failed, + * 0xF1 accept failed + * +0x020 fd, +0x040 bind ret, +0x060 listen ret, +0x080 accept ret + * +0x0A0 reads, +0x0C0 bytes read, +0x0E0 last read length, +0x100 port + */ +#include "romapi.h" + +#define VRAM_BASE 0x04000000 +#define VRAM_BYTES 0x40000 /* 256 KiB, the real unit's framebuffer */ +#define PORT 7009 +#define MARK 0x018C8000 +#define LIMIT_CELL 0x018C9000 +#define PORT_CELL 0x018C9040 + +static unsigned char sa[16]; +static unsigned char from[16]; +static unsigned char buf[1472]; + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static unsigned int getcell(off) +unsigned int off; +{ + return *(unsigned int *)(MARK + off); +} + +int main() +{ + int fd, cfd, n, r; + unsigned int limit, port, off, total, reads, i; + + cell(0x000, 1); + limit = *(unsigned int *)LIMIT_CELL; + port = *(unsigned int *)PORT_CELL; + if (port == 0) { + port = PORT; + } + cell(0x100, port); + + /* per-process user area is uninitialised (see udpstream.c) */ + { + unsigned int proc, user, k; + proc = *(unsigned int *)0x1B3900; + user = *(unsigned int *)(proc + 0x100); + for (k = 0; k < 0x100; k++) { + *(unsigned char *)(user + k * 8) = 0; + } + cell(0x140, user); + } + + fd = socket_(AF_INET, SOCK_STREAM, 0); + cell(0x020, fd); + if (fd < 0) { cell(0x000, 0xEE); return 1; } + cell(0x000, 2); + + sa_any(sa, port); + r = bind_(fd, sa, SOCKADDR_LEN); + cell(0x040, r); + if (r < 0) { cell(0x000, 0xEF); close_(fd); return 2; } + cell(0x000, 3); + + r = listen_(fd, 1); + cell(0x060, r); + if (r < 0) { cell(0x000, 0xF0); close_(fd); return 3; } + cell(0x000, 4); + + cfd = accept_(fd, 0, 0); + cell(0x080, cfd); + if (cfd < 0) { cell(0x000, 0xF1); close_(fd); return 4; } + cell(0x000, 5); + + cell(0x0A0, 0); + cell(0x0C0, 0); + off = 0; + total = 0; + reads = 0; + + for (;;) { + n = read_(cfd, buf, sizeof(buf)); + cell(0x0E0, (unsigned int)n); + if (n <= 0) { + close_(cfd); + close_(fd); + return 5; + } + for (i = 0; i < (unsigned int)n; i++) { + *(unsigned char *)(VRAM_BASE + off * 8) = buf[i]; + off = off + 1; + if (off >= VRAM_BYTES) { + off = 0; + } + } + total = total + (unsigned int)n; + reads = reads + 1; + cell(0x0A0, reads); + cell(0x0C0, total); + if (limit != 0 && total >= limit) { + close_(cfd); + close_(fd); + return 0; + } + } +} diff --git a/harness/udpstream/udpstream.c b/harness/udpstream/udpstream.c new file mode 100644 index 0000000..a40abf0 --- /dev/null +++ b/harness/udpstream/udpstream.c @@ -0,0 +1,161 @@ +/* + * udpstream.c -- spike: receive full frames over UDP and blit them into VRAM. + * + * The terminal is the client; the host sends self-describing datagrams: + * + * +0 u16 start row (little endian) + * +2 u16 row count + * +4 row_count * 160 bytes of 1-bpp pixels (1280 px per row) + * + * Each row lands at bit 0x04000000 + row * 2048 (the real unit's 256-byte row + * pitch). Only the 160 visible bytes of a row are sent; the remaining 96 mask + * bytes are left as they are. 8 rows per datagram = 1284 bytes, under the + * 1500-byte Ethernet MTU. + * + * Markers at 0x018C8000 (32-bit cells): + * +0x000 stage 1 entered main, 2 socket ok, 3 bound, 4 receiving + * 0xEE socket failed, 0xEF bind failed + * +0x020 fd + * +0x040 bind() result + * +0x060 datagrams received + * +0x080 last datagram length + * +0x0A0 rows written + * +0x0C0 bytes blitted + * + * K&R C only; the ROM entry points and the sockaddr layout are in romapi.h. + */ +#include "romapi.h" + +#define VRAM_BASE 0x04000000 +#define ROW_BITS 2048 /* 256-byte row pitch on the real unit */ +#define ROW_BYTES 160 /* 1280 visible pixels per row */ +#define MAX_ROWS 8 +#define PORT 7000 +#define MARK 0x018C8000 +#define LIMIT_CELL 0x018C9000 /* host-written datagram limit, 0 = unlimited */ +#define PORT_CELL 0x018C9040 /* host-written UDP port, 0 = 7000 */ + +static unsigned char sa[16]; +static unsigned char from[16]; +static int fromlen; +static unsigned char buf[4 + MAX_ROWS * ROW_BYTES]; +static unsigned int rset[8]; +static unsigned int tv[2]; + +static void cell(off, v) +unsigned int off, v; +{ + *(unsigned int *)(MARK + off) = v; +} + +static unsigned int getcell(off) +unsigned int off; +{ + return *(unsigned int *)(MARK + off); +} + +int main() +{ + int fd, r, n; + unsigned int limit, count, port; + unsigned int start, rows, row, i, dst, len; + + cell(0x000, 1); + limit = *(unsigned int *)LIMIT_CELL; + count = 0; + port = *(unsigned int *)PORT_CELL; + if (port == 0) { + port = PORT; + } + + /* The per-process user area (process+0x100 bits) is not initialised by + * create_process: on both the emulator and real hardware it reads as the + * heap fill 0x55. The syscall layer keeps its file table and syscall + * state there, so clear it before the first call. */ + { + unsigned int proc, user, k; + proc = *(unsigned int *)0x1B3900; + cell(0x140, proc); + user = *(unsigned int *)(proc + 0x100); + cell(0x160, user); + for (k = 0; k < 0x100; k++) { + *(unsigned char *)(user + k * 8) = 0; + } + } + + fd = socket_(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + cell(0x020, fd); + if (fd < 0) { + cell(0x000, 0xEE); + return 1; + } + cell(0x000, 2); + + sa_any(sa, port); + cell(0x120, port); + r = bind_(fd, sa, SOCKADDR_LEN); + cell(0x040, r); + if (r < 0) { + cell(0x000, 0xEF); + close_(fd); + return 2; + } + cell(0x000, 3); + + /* limit == 0xDEAD: bind and return without reading, so the host can test + * whether traffic to an unread socket is what kills the machine. */ + if (limit == 0xDEAD) { + cell(0x000, 0xDD); + return 0; + } + + cell(0x060, 0); + cell(0x0A0, 0); + cell(0x0C0, 0); + + for (;;) { + fromlen = SOCKADDR_LEN; + n = recvfrom_(fd, buf, sizeof(buf), 0, from, &fromlen); + cell(0x0E0, (unsigned int)n); + if (n <= 0) { + close_(fd); + return 3; + } + if (n < 4) { + close_(fd); + return 4; + } + len = (unsigned int)n; + start = (unsigned int)buf[0] | ((unsigned int)buf[1] << 8); + rows = (unsigned int)buf[2] | ((unsigned int)buf[3] << 8); + if (rows > MAX_ROWS || start + rows > 1024) { + close_(fd); + return 5; + } + if (len < 4 + rows * ROW_BYTES) { + close_(fd); + return 6; + } + + for (row = 0; row < rows; row++) { + dst = VRAM_BASE + (start + row) * ROW_BITS; + for (i = 0; i < ROW_BYTES; i++) { + *(unsigned char *)(dst + i * 8) = + buf[4 + row * ROW_BYTES + i]; + } + } + + cell(0x080, len); + cell(0x060, getcell(0x060) + 1); + cell(0x0A0, getcell(0x0A0) + rows); + cell(0x0C0, getcell(0x0C0) + rows * ROW_BYTES); + cell(0x000, 4); + + count = count + 1; + if (limit != 0 && count >= limit) { + cell(0x100, count); + close_(fd); + return 0; /* bounded run: leave the OS to it */ + } + } +}