docs: plan the LANCE Ethernet model + network font delivery

Evidence for the RAP/RDP window pair (0x5900000/0x5800000), INT1 as the LANCE
interrupt, and the separate MAC address ROM at 0x6800000; MAME am79c90 drop-in
assessment with the kn01/ncd68k templates; the TFTP/LAT font delivery path
(no X font server on 7100); acceptance, risks and sequencing.
This commit is contained in:
SDD committed 2026-10-16 14:16:05 +03:00
1 parent 47dac6b283
commit d017953d52
1 file changed
+546
@@ -0,0 +1,546 @@
# Plan: model the LANCE Ethernet controller, and deliver a font over the network
Date: 2026-10-16
Status: **planned, not started.** This runs **after** the level-0 puzzle and the
keyboard/mouse input models land. Read-only investigation only; nothing in
`harness/mame/`, the MAME tree, `ghidra/` or the ROMs was changed to produce it.
Address conventions (four, reconciled — same as `reference/level1-bypass-analysis.md`):
| name | formula | example |
|---|---|---|
| firmware **bit** address (what `hostport_dis34.txt` prints) | `0x3F200000 + file_offset*8` | `0x5800000` |
| physical **byte** address | `bit >> 3` | `0x00B00000` |
| driver / board-model address ("harness unit") | `bit >> 4` | `0x05800000` |
| a handler at `base` receives | `(bitaddr - base) >> 4` | offset `0x0` at the base |
Throughout I quote the **bit** address and give byte equivalents. "PC" values are
TMS34010 `PC` (bit addresses). ROM byte-identical (`md5
b46a3e442ccb86134d2356bd70d22fd2`).
---
## 0. Verdict (short)
* **INT1 is the LANCE interrupt; `0x5900000`/`0x5800000` is its register
interface.** They are **RAP / RDP** (register-address port / register-data
port) of an Am7990/Am79C90-class LANCE — *not* a generic board DMA latch.
Evidence in §1.1–1.2. This **corrects** `reference/memory-space-map.md` rows 5–6
("DMA/board data/command port") and `vt1200_board.h`'s "DMA pair" naming.
* The **address ROM is a separate device** at physical byte `0xD00000` (bit
`0x6800000`), not a region of the firmware image: the known MAC
`08:00:2b:16:4b:5a` does **not** appear anywhere in `vt1200_v22.bin`.
* MAME has **`am79c90` / `am7990`** (`src/devices/machine/am79c90.cpp`). It is a
**near drop-in for the register protocol** (its `csr0` STOP semantics satisfy
the firmware's level-0 check by construction, and its buffer-length encoding
matches the OS driver byte-for-byte). It is **bus-master**, so the work is in
the **DMA callbacks, the IRQ line and the address ROM**, not in the chip.
* The terminal gets fonts over **TFTP (its own font path format) or DEC's LAT
font daemon** — **not** an X Font Server on TCP 7100 (refuted in §3.1).
The smallest host-side server is the project's existing TFTP server; the
emulator additionally needs a host-side network peer (`tap` backend + bridge;
this build has **no pcap and no slirp**, §2.3).
* **Difficulty: 1–2 weeks part-time**, one engineer. The chip is easy; the two
real costs are the **bus-master DMA address translation** and the
**host-peer plumbing** (TAP bridge + BOOTP). Biggest single risk: the board
window that *fronts* the LANCE is otherwise still undecoded, and the OS's
LANCE driver may touch board registers beyond RAP/RDP (§5).
---
## 1. Evidence — the firmware's side
### 1.1 The interface is RAP/RDP (Am7990 register file)
The firmware writes a **register number** to `0x5900000` and then reads/writes
the selected **register** at `0x5800000`. Every access is 16-bit (`move …,0`).
The command values and data are, in order, exactly the LANCE's CSR0–CSR3 access
protocol. Decisive site, `io5800_5900_init` `L3FDDCBE0` (bit `0x3FDDCBE0`):
```
3fddcc20: movi 0x5900000, a2 ; a2 = RAP
3fddcc50: movi 0x5800000, a3 ; a3 = RDP
3fddcc80: movk 1, a0
3fddcc90: move a0, *a2, 0 ; RAP = 1 (CSR1)
3fddcca0: move *a14(0xffe0), a1, 1 ; caller's argument = a BIT address
3fddccc0: srl 0x3, a1 ; >>3 -> BYTE address (the device is byte-addressed)
3fddccd0: move a1, *a3, 0 ; CSR1 = address low
3fddcce0: movk 2, a0
3fddccf0: move a0, *a2, 0 ; RAP = 2 (CSR2)
3fddcd00: srl 0x10, a1
3fddcd10: move a1, *a3, 0 ; CSR2 = address high
3fddcd20: movk 3, a0
3fddcd30: move a0, *a2, 0 ; RAP = 3 (CSR3 / bus-master config)
3fddcd40: movi 0x0, a1
3fddcd60: move a1, *a3, 0 ; CSR3 = 0
3fddcd70: clr a0
3fddcd80: move a0, *a2, 0 ; RAP = 0 (CSR0)
3fddcd90: movi 0x7f41, a1
3fddcdb0: move a1, *a3, 0 ; CSR0 = 0x7f41 = INIT|INEA|IDON|TINT|RINT|MERR|MISS|CERR|BABL
```
That is the textbook LANCE "program IADR then issue INIT" sequence:
CSR1/CSR2/CSR3 hold the address of the 24-byte initialization block; CSR0 bit 0
(`INIT`) starts initialization. `0x7f41` sets `INIT` (bit 0) plus the standard
interrupt-enable/clear mask.
The other driver routines confirm the same decode:
| routine | bit addr | sequence | meaning |
|---|---|---|---|
| `lance_stop` | `L3FDDCA90` | `RAP=0; RDP=0x0004` | CSR0 = STOP |
| `lance_start` | `L3FDDCB30` | `RAP=0; RDP=0x0042` | CSR0 = STRT\|INEA |
| `lance_read_csr0` | `0x3FDDCE10` | `RAP=0; read RDP` | read CSR0 |
| `lance_int_ack` | `L3FDDCEA0` | `RAP=0; read RDP`; OR into `@0x8fc00`; `RDP = status & 0x7ff0` | read-and-clear CSR0 interrupt/error bits |
The **level-0** and **level-1** self-tests use the same two addresses:
* Level-0 "Ethernet port (LANCE) not reset on power-up test" —
`0x3F201320`: `RAP=0` (`3f201340`), read CSR0 (`3f201370`),
`btst 2` (`STOP`, `3f2013a0`); if clear → error path `L3F207A60`, which sets
`a10 = 0x33` (the LED code for **error 33**) **and** writes `4` (CSR0 STOP)
back to `0x5800000` (`3f207a80`). Service-guide string
`LEVEL0 ERROR -> LANCE NOT STOPPED - 33` (`0x3FDF4CB0`).
**A reset LANCE has CSR0 = 0x0004, so bit 2 is set → the test passes.**
* Level-1 "Ethernet port (LANCE) test" — `0x3F202830`: `RAP=1`
(`3f202830`), then 15 iterations write `0`/`a7` to RDP (CSR1) and read back,
accumulating mismatches (`3f2028d0`…`3f202a00`). A register read/write
path test — no packet loopback.
OS cold-start runs `lance_stop` immediately (`0x3F578900`: `RAP=0`;
`0x3F578960`: `CSR0=4`), then the OS LANCE driver init lives at
`0x3F9D2880`–`0x3F9D46D0` (board log: `W 05900000 … val=0000/0001/…`,
`R 05800000 … val=0001`, per `OS_BRINGUP_REPORT.md` §4.2). It builds the init
block and **two descriptor rings**, and reads the MAC ROM (below).
**Verdict:** `0x5900000` = RAP, `0x5800000` = RDP. The `>>3` on a bit address
before programming CSR1/2 proves the device sees **byte** addresses.
### 1.2 The interrupt is INT1, and the LANCE's ISR reads `0x5800000`
* TMS34010 external interrupts are **active-low** on `LINT1`/`LINT2`.
`INT1` vector = `0xFFFFFFC0`, `INT2` vector = `0xFFFFFFA0`
(`docs/TMS34010-users-guide.txt` Table 8-2). `INTENB` bit 1 = `X1E`
(enable INT1), bit 2 = `X2E` (enable INT2); `INTPEND` bit 1 = `X1P`,
bit 2 = `X2P` (guide §6, register descriptions).
* The ROM vector page holds `RLONG(0xFFFFFFC0) = 0x00000F60` and
`RLONG(0xFFFFFFA0) = 0x00000F30` — **RAM stubs** (bit `0x0F60` / `0x0F30`)
into which the firmware writes `JA` instructions at runtime (`MEMORY_MAP.md`
§"Reset / interrupt vector page"; `DUART_REPORT.md` §1).
* The **LANCE ISR** is `0x3FA93400`:
```
3fa93400: move a8, -*sp, 1
3fa93410: calla L3FDDCEA0 ; <-- the LANCE read-CSR0 + interrupt-ack
3fa93440: move *sp+, a8, 1
3fa93450: reti
```
`L3FDDCEA0` **reads `@0x5800000`** (`3fddcf10`). This is the lead confirmed:
the handler on one of the two external-interrupt vectors reads the RDP.
* The LANCE driver **installs** `0x3FA93400` and then enables **INT1 only**:
`0x3FA8C340` – `0x3FA8C470` calls `L3FA90230(handler=0x3FA93400, …)` and then
`L3FDF9420(2)`. `L3FDF9420` is `INTENB |= mask` (`3fdf94b0` loads
`0xC0000110`), so `2` = **bit 1 = `X1E` = enable INT1**.
* Contrast: the DUART1/board-window setup (`0x3FA84C70`) installs its handlers
with `L3FDF9420(6)` = `INT1|INT2`, and later OS-phase DUART1 handlers
`0x3FA934C0` / `0x3FA935D0` (window `0x6400000`) are installed on the two
slots in a mode-dependent pair. The exact slot bookkeeping is the subject of
the **preceding** "interrupt-line" work; what this plan needs is the fact in
the bullet above — the LANCE's own handler is installed and INT1 is enabled.
* The level-0/OS dispatcher likewise branches on `INTPEND` bit 1 as the
external-interrupt-1 source (`0x3F2026C0: btst 1,a2 / jane L3F207D80`).
One reconciliation note for the implementer: `DUART_REPORT.md` §1 concluded
"DUART1 is expected on INT1" because the *POST* handler stub
`0x3FA92F40` (INT1) also dispatches to window `0x6400000`. That report itself
records that `L3FA78D90` ignores its mask argument, and `0x3FA93070` (the INT2
stub) dispatches to the **same** window. So the DUART is happy on either line,
while the LANCE is **not**: its `lance_int_ack` emits its IRQ and its sole
handler is installed on the INT1 slot. Route **DUART1 → INT2, LANCE → INT1**
(the "interrupt-line question" work that precedes this plan).
### 1.3 The address ROM is a separate device, holding the MAC
The firmware reads exactly **six bytes** from bit `0x6800000` (physical byte
`0xD00000`), one MAC byte per 16-bit step (`movb *a7(0),dst; addk 16,a7`):
* Level-1 **"Ethernet address ROM test"** (error 23): `0x3F230890`–
`0x3F2309D0` — `movi 0x6800000,a11`, read 6 bytes into a local struct.
Strings: `Ethernet ROM Error - 23` (`0x3F4AB2F8` / `0x3FAD60F8`).
* POST LANCE init: `0x3FDD74B0`, `0x3FDD7C30`/`0x3FDD7CE0`.
* OS: `0x3F57C6B0`, `0x3F57DE00`, and the OS LANCE init `0x3F9D2920`
(reads 6 bytes into `@0x23cca0`, then at `0x3F9D2B60` copies them into the
**init block's PADR** at `@0x23e780 + 16 bits`).
**The MAC is `08:00:2b:16:4b:5a`** (DEC OUI `08:00:2b`), from the live unit
(`NEXT_SESSION.md` §6; service guide shows the same OUI on other units).
A byte search of `vt1200_v22.bin` (and v2.1) finds **no** occurrence of those
six bytes → the ROM is a **physical device in the memory map**, not firmware
data. So MAME must supply it.
Open sub-question (not needed to start): the guide calls the test "format and
checksum", but the firmware reads only the 6 MAC bytes from this window; no
second read or sum over `0x6800000` exists in the image. Resolve empirically —
run T12 and, if it fails, reverse the check. **Model the ROM as
`mac[i]` at handler offset `i` (6 read-only bytes).**
---
## 2. MAME's side
### 2.1 The device exists and is well-suited
`src/devices/machine/am79c90.{cpp,h}` define `AM7990` (`am7990`) and `AM79C90`
(`am79c90`), class `am7990_device_base`. Driver-facing surface:
```c++
u16 regs_r(address_space &space, offs_t offset); // offset 0 = RDP; !=0 = RAP
void regs_w(offs_t offset, u16 data);
auto intr_out(); // devcb_write_line — /INTR, ACTIVE-LOW (0 = asserted)
auto dma_in(); // devcb_read16 — bus-master read: offset is a BYTE address
auto dma_out(); // devcb_write16 — bus-master write
void reset_w(int state); // reset input
```
* **Bus master:** yes. It reads the 24-byte init block with single-word DMA and
then reads/writes packet descriptors and buffers through `dma_in`/`dma_out`.
`initialize()` computes `init_addr = ((csr2<<16)|csr1) & 0xfffffe` — a **byte**
address — and reads 12 words. `get_buf_length()` implements the standard
LANCE buffer-length encoding.
* It already includes `dinetwork.h` (`device_network_interface`), so it plugs
into MAME's `-netdev` provider chain and gets a real peer (§2.3).
* **The address PROM is not an input to the device.** `initialize()` takes the
MAC from the init block's `PADR` (bytes 2–7) and calls `set_mac()`. So the
address ROM is just a small read-only region the **CPU** reads and copies into
PADR — exactly what the firmware does (§1.3).
Two ready templates:
* `src/mame/dec/kn01.cpp` — **a DEC machine with `am79c90`**:
```c++
AM79C90(config, m_lance, XTAL(12'500'000));
m_lance->intr_out().set_inputline(m_cpu, INPUT_LINE_IRQ1).invert();
m_lance->dma_in().set([this](offs_t o){ return m_nram[(o & 0xffff) >> 1]; });
m_lance->dma_out().set([this](offs_t o, u16 d, u16 m){ COMBINE_DATA(&m_nram[(o & 0xffff) >> 1]); });
map(0x18000000, 0x18000007).rw(m_lance, FUNC(am79c90_device::regs_r),
FUNC(am79c90_device::regs_w)).umask32(0x0000ffff);
```
* `src/mame/ncd/ncd68k.cpp` — an **NCD X terminal** with LANCE + SCN2681 + VRAM
(the closest whole-machine analogue), showing explicit `lance_dma_r/w`.
`FULL_BOOT_ESTIMATE.md` §3 already names both.
### 2.2 Concrete wiring sketch (for the implementer — not done here)
Add `#include "machine/am79c90.h"`, a `required_device<am79c90_device> m_lance`,
and — keeping the board model for every other offset — map **offset 0 only** of
the two windows to the chip:
```c++
// RDP (data) at bit 0x5800000; RAP (register select) at bit 0x5900000.
map(VT1200_WIN0, VT1200_WIN0 + 0x000f).rw(FUNC(vt1200_state::lance_rdp_r),
FUNC(vt1200_state::lance_rdp_w));
map(VT1200_WIN1, VT1200_WIN1 + 0x000f).rw(FUNC(vt1200_state::lance_rap_r),
FUNC(vt1200_state::lance_rap_w));
// RDP handlers call m_lance->regs_r(space, 0) / regs_w(0, data)
// RAP handlers call m_lance->regs_r(space, 1) / regs_w(1, data)
```
* **DMA:** the LANCE address is a **byte** address; main RAM is bit-addressed.
Map byte `B` → Main-RAM word `B >> 1`:
```c++
m_lance->dma_in().set([this](offs_t o){ return m_mainram[o >> 1]; });
m_lance->dma_out().set([this](offs_t o, u16 d, u16 m){ COMBINE_DATA(&m_mainram[o >> 1]); });
```
(or go through `m_maincpu->space(AS_PROGRAM)` if DMA must reach outside RAM;
the init block and both descriptor rings are in RAM). **Verify the unit**:
the whole driver already uses the convention `handler offset = (bitaddr-base)>>4`
(each 16-bit word occupies 16 address units), so a physical byte `B` is at
CPU bit address `B*8`.
* **IRQ:** `m_lance->intr_out().set_inputline(m_maincpu, 0).invert();`
(line 0 = INT1; `.invert()` because `/INTR` is active-low and the TMS34010
input is asserted by `ASSERT_LINE`, cf. the DUART wiring at
`vt1200.cpp:645`). Gate it to the OS phase if a POST-phase INT1 conflict with
the DUART appears — the LANCE is never `INEA`-enabled during POST, so it
should be idle anyway.
* **Address ROM:** map bit `0x6800000` (byte `0xD00000`) as a 6-byte read-only
handler returning `{08,00,2b,16,4b,5a}` at handler offsets 0–5 (the firmware
reads bytes at 2-byte stride, so handler offset `i` = MAC byte `i`).
Where the board model currently serves this window, override it.
Long-term a `rom` region + `set_rom` is cleaner than an inline lambda.
### 2.3 Network backends in *this* build
* **Compiled into `/home/arti/code/k-space/mame0289/vt1200`: `none` and `tap`
(taptun) only.** Verified by symbol inspection: `netdev_none` and
`netdev_tap` are present; **`netdev_pcap` is absent**. The source tree has
`pcap.cpp` and `taptun.cpp` (`src/osd/modules/netdev/`), and `NETDEV_PCAP`
exists as a name string, but pcap was not built.
* **No slirp / NAT / user-mode network** exists in this build (or in the
snapshot's source tree).
* To attach a **host-side peer** (the font server):
1. build/run with the **`tap`** provider (`-netdev tap -netdev_interface …`),
create a persistent TAP interface, and **bridge** it to the host; or
2. **rebuild MAME with `OSD_NET_USE_PCAP`** and attach to a real NIC/bridge.
Then point the host TFTP/LAT server at the bridge address.
* For the level-0/level-1 tests and for "the OS brings the interface up",
**`-netdev none` is sufficient** — no host peer is needed.
**Drop-in verdict: *yes, with caveats.*** The register protocol matches exactly
and MAME's reset state (`CSR0 = STOP = 0x0004`) makes the level-0 test pass by
construction. The caveats are: (a) the two registers live in two windows
`0x20000` bytes apart, so the map is two entries, not one 4-word block;
(b) the **DMA callbacks and their byte→bit translation** are new code;
(c) the **IRQ polarity/routing** must be got right; (d) the **address ROM** is a
new small region; (e) the parts of the fronting board window beyond offset 0
are still the board model.
---
## 3. The delivery path — how a font actually reaches the terminal
### 3.1 Mechanism (refutes "X font server on TCP 7100")
The VT1200 supports **two font paths**, selectable by a radio in the Terminal
Manager (`docs/EK-V1000-SV-002_service-guide.txt` lines 2555–2700;
`docs/DTJ-1991-vol3num4_X-Window-Terminals.txt` lines 545–600):
| path | syntax | transport | who serves |
|---|---|---|---|
| **LAT Font Path** | `\|ROM,BETA::` | DEC **LAT** (ethertype `0x6004`) | DEC's **font daemon** on the host |
| **TFTP Font Path** | `\|ROM,123.45.67.89:/vt1200/fonts.dir` | **TFTP** (UDP 69) | any TFTP server |
* The LAT path is DEC's own **font daemon**; the DTJ article calls it "a
precursor to a general font server, and it is very similar to the X Font
Server" — i.e. *similar in spirit*, **not** the X Font Server protocol.
There is **no TCP 7100 / `fs` protocol** in the firmware (ROM strings show
`Font server is %s, font path is %s`, `LAT Font Path`, `TFTP Font Path`,
`/fonts.dir`; no 7100 usage). **The XFS hypothesis is refuted.**
* The TFTP path file is the index (`fonts.dir` / `font.paths`). The terminal
**TFTP-fetches `<dir>/fonts.dir`**, then fetches each font file named inside.
This is the mechanism already confirmed live in `EXPLOIT_NOTES.md`
("Working delivery recipe (confirmed)"): RRQ `"69"`, RRQ `"69/fonts.dir"`,
then RRQ `"baseline.pcf"` etc.
### 3.2 Smallest host-side server
Nothing new is needed for the **TFTP** route — the project already has it:
* `harness/serve.py` — the working TFTP font server (serves `fonts.dir` + the
PCF/SNF set from `reference/fonts/`), and
* `tools/fontgen.py` (`TFTPServer`, `mutate`, `pcf_tables`, `set_num_chars`) —
builds the crafted PCF (`num_chars = 0x40000001` → `n*4` wrap → heap
overflow, the confirmed primitive).
For a **live** terminal this is already end-to-end (see `EXPLOIT_NOTES.md`);
the LANCE work does not change it. It matters here because it is the thing the
LANCE must *let through* in the emulator.
### 3.3 For the emulated terminal
`-netdev none` gives the emulated LANCE no peer, so the OS's LANCE init runs but
no font is ever requested. The honest minimal route to "a font arrives in the
emulator" is:
1. LANCE modelled (§2.2) and the OS interface up (§4).
2. A host peer via **`tap` + bridge** (or pcap rebuild).
3. The terminal needs an **IP**. The emulator loads default NVRAM settings, so
the IP is `0.0.0.0`; the OS then prints
`Could not determine IP address after 5 retries / TCP/IP is now down.` The
firmware has BOOTP (`bootp_proto`, `do_bootp`), so boot the OS either with a
**minimal BOOTP responder** on the bridge (not yet written; ~a few dozen
lines) **or** set the IP through the Terminal Manager UI — which is exactly
what the preceding **keyboard/mouse model** work enables.
4. Serve `fonts.dir` + the crafted PCF with `harness/serve.py` / `fontgen`,
pointed at the Terminal Manager's TFTP Font Path.
5. Watch the parse: breakpoint the confirmed sites `pcfReadBitmaps`
`0x0F09CE`, `pcfReadFont`, `fosAlloc` `0x3F703D90` (already done by
`overflow.lua`). This is the payoff the LANCE unlocks.
---
## 4. Acceptance criteria
Each criterion is observable and testable.
**A. Register protocol (unit / early boot).**
1. With the LANCE modelled, the **level-0 "LANCE not stopped" test passes**:
no `LEVEL0 ERROR -> LANCE NOT STOPPED - 33`, and the error handler at
`0x3F207A60` is never entered.
2. The **level-1 "Ethernet port (LANCE) test"** (`0x3F202830`) and the
**"Ethernet address ROM test"** (`0x3F230890`) both pass — no
`Ethernet Port Error - 24` (`0x3F4AB090`) or `Ethernet ROM Error - 23`
(`0x3F4AB2F8`). Because level 1 wedges on the earlier host-port test today
(`reference/level1-bypass-analysis.md` §4), invoke these tests through the
diagnostics **test map** (the `l1patch.lua` runtime-patch mechanism, or the
NCP `T 9` / `T 12` menu) rather than a full level-1 run.
*Acceptable evidence:* a run that reaches `L3F202B60` (LANCE pass branch) and
the ROM-test pass branch, with the error strings absent.
**B. OS interface up.**
3. The OS LANCE driver completes INIT: `CSR0` reads back with `IDON` set,
`lance_start` is issued, and the machine **does not** print
`Could not determine IP address after 5 retries / TCP/IP is now down.` for
the LANCE reason (a missing *address* is still allowed). Observable: a
breakpoint on `L3FDDCEA0` / the OS init at `0x3F9D2880`, plus the absence of
the LANCE error banner.
4. The Terminal Manager reports the real MAC (`08-00-2B-16-4B-5A`) instead of
`Ethernet Address 00-00-00-00-00-00`. Observable: frame dump of the
Terminal Manager (`OS_BRINGUP_REPORT.md` §5 shows the field).
**C. Font end to end.**
5. With a host peer on the bridge and a TFTP Font Path set, the emulator's
terminal issues an RRQ for `<dir>/fonts.dir` and then the named PCF, and the
parser is entered. Observable on **both** sides: the `harness/serve.py` log
shows the RRQs, and the MAME debugger breaks at `pcfReadBitmaps`
(`0x0F09CE`) with `num_chars` from our file. This is the acceptance that the
whole exercise exists to reach.
**D. No regressions.** `-netdev none`, no-LANCE runs remain byte-identical to
today's baseline (the promoted-default discipline of
`2026-10-16-env-gate-cleanup.md`), and the DUART/level-0 paths are unchanged.
---
## 5. Scope, risks and unknowns (honest)
**Realistic size: 1–2 weeks part-time** for one engineer who already knows this
driver. Breakdown: LANCE register window + IRQ + address ROM ≈ 1–2 days (the
`kn01.cpp` pattern); DMA callback + byte→bit translation + first INIT observed
≈ 1–3 days (this is where surprises live); host-peer TAP bridge + BOOTP + TFTP
end-to-end ≈ 2–4 days. This is far smaller than the 6–12-week "full boot"
estimate in `FULL_BOOT_ESTIMATE.md`, because that estimate included decoding the
undecoded `0x6200000`/`0x6400000` ASIC and the whole video path, which are now
mostly done.
**What could make it fail to be a drop-in:**
1. **The fronting board window.** `0x5800000`/`0x5900000` are the LANCE's RAP/RDP,
but the same windows (and `0x5600000`, `0x6000000`) are still partly
unexplained board logic. If the OS LANCE driver touches board registers
*within those windows* beyond offset 0 (chip select, reset, interrupt
routing, a DMA controller), the board model will not satisfy them. **This is
the single biggest risk.** Mitigation: keep the board model for all other
offsets (the sketch in §2.2 does) and log every access the OS makes.
2. **DMA address translation.** The LANCE's byte addresses must be mapped to the
TMS34010's bit-addressed space exactly (`<<3`), and descriptor rings use the
masked-address semantics (`RING_ADDR_MASK = 0xfffff8`). Getting the unit
wrong produces silent garbage rather than a clean failure. Mitigation: cross-
check against the POST init's known ring addresses (`0x7fb00`, `0x7fbc0`,
`0x87bc0` bit → `0xFF60`, `0xFF78`, `0x10F78` byte) and the blitter's RAM use.
3. **LANCE clock unknown.** The reference clock feeds interrupt/poll timing
only; `kn01` uses 12.5 MHz, `ncd` 10 MHz. Any value should work for the
register tests and INIT; note it if transmit pacing matters.
4. **Address-ROM checksum.** The guide says "format and checksum"; the firmware
visibly reads only 6 bytes. If T12 still fails, the format (or a checksum
check we have not located) must be reversed before criterion A.2 is met.
Low cost, but unknown until tried.
5. **Host-peer plumbing is a host-configuration problem, not a code problem.**
This build has **no pcap and no slirp**, so the emulated LAN needs a TAP +
bridge (or a MAME rebuild). `tap` needs privileges; the bridge must be up
before MAME starts.
6. **No slirp means no user-mode NAT**, so "just make TFTP work out of the box"
is not available; the bridge is mandatory for criterion C.
**Known but out of scope here:** the custom interrupt controller inside the
`0x6200000`/`0x6400000` ASIC (the LANCE IRQ must reach INT1 through whatever the
board does, but at the TMS34010 level the harness wires the device straight to
`INT1`, which is what the OS expects); full OS/IP configuration; the LAT font
daemon (the existing `lat/` + `latd` cover the LAT side).
**Open, stated plainly:**
* Whether the OS's LANCE driver needs board registers in the `0x5800000` window
beyond RDP — resolve by logging the OS run.
* The exact address-ROM format/checksum.
* The LANCE's real clock.
* Whether `INT2` fully substitutes for `INT1` for DUART1 during POST (the
preceding interrupt-line work settles this; the LANCE requires it).
---
## 6. Sequencing
1. **Prerequisites (land first):** the level-0 puzzle; the keyboard/mouse input
models; the interrupt-line work that settles **DUART1 → INT2** and frees
**INT1** for the LANCE.
2. **This plan:** LANCE register window + IRQ + address ROM → DMA → OS
interface up → host peer (TAP/BOOTP/TFTP) → font parse in-emulator.
3. Only then revisit the live-terminal LAT/announcement crash work
(`EXPLOIT_NOTES.md`, `NEXT_SESSION.md` §5) with the offline reproducer.
---
## 7. Executable task list
Each step has a check; do not proceed on a failed check.
1. **Map the two registers.** Add the LANCE device and the WIN0/WIN1 offset-0
maps (§2.2), keeping the board model elsewhere.
*Check:* run with `VT1200_BOARD_LOG`; the level-0 test's `R 05800000 pc=3F201370`
now returns `0x0004` (bit 2 set) and `3F207A60` is not reached.
2. **Address ROM.** Map `0x6800000` with `08 00 2b 16 4b 5a` at handler
offsets 0–5.
*Check:* the ROM read at `0x3F2308F0` yields those six bytes; run the ROM test
via the test map and confirm no `Ethernet ROM Error - 23` (reverse the check
first if it still fails — risk 4).
3. **Level-1 LANCE test.** Verify the register read/write path.
*Check:* `0x3F202830`–`0x3F202A00` accumulates zero and branches to `L3F202B60`.
4. **DMA callbacks.** Wire `dma_in`/`dma_out` to main RAM with the byte→bit
translation; verify against the POST init's ring addresses.
*Check:* breakpoint `initialize()`; the 12 init-block words read back are the
ones the firmware wrote; the PADR equals the MAC.
5. **IRQ.** Wire `intr_out` to INT1 (`.invert()`), with DUART1 on INT2.
*Check:* an asserted LANCE IRQ is serviced by `0x3FA93400`, which reads
`0x5800000`; `INTENB` bit 1 is set (`L3FDF9420(2)`).
6. **OS interface up.** Boot the skip-level-1 path; confirm INIT/IDON/start.
*Check:* criterion B.3 and B.4.
7. **Host peer.** Add the `tap` provider path (bridge) or rebuild with pcap;
add a minimal BOOTP responder (or set the IP via the keyboard/mouse UI).
*Check:* `ping` from the host to the emulated terminal, or a host capture
showing the terminal's ARP/BOOTP.
8. **Font end to end.** Serve `fonts.dir` + a crafted PCF; breakpoint
`pcfReadBitmaps`.
*Check:* criterion C.5.
9. **Baseline discipline.** Re-baseline the no-LANCE run and document the diff.
Expected driver observability knobs to add (keep in the instrumentation style of
`vt1200.cpp`): a LANCE access log, and a dump hook around `initialize()`.
---
## 8. Reproduce this investigation
```sh
cd /home/arti/code/k-space/vt1200
# interface / IRQ / address-ROM evidence (bit addresses):
grep -n "0x5800000\|0x5900000\|0x6800000" harness/mame/hostport_dis34.txt
sed -n '195,240p;400,445p;1180,1215p;556630,556720p' harness/mame/hostport_dis34.txt
sed -n '391160,391200p' harness/mame/hostport_dis34.txt # LANCE ISR 0x3FA93400
# MAME device + templates:
sed -n '535,720p' /home/arti/code/k-space/mame0289/src/devices/machine/am79c90.cpp
grep -n "AM79C90\|regs_r\|dma_in\|intr_out" \
/home/arti/code/k-space/mame0289/src/mame/dec/kn01.cpp \
/home/arti/code/k-space/mame0289/src/mame/ncd/ncd68k.cpp
# network backends actually compiled into this build:
strings -n 5 /home/arti/code/k-space/mame0289/vt1200 | grep -E "netdev_(none|tap|pcap)"
# the MAC is not in the ROM:
python3 - <<'EOF'
d=open("vt1200_v22.bin","rb").read()
print(d.find(bytes.fromhex("08002b164b5a"))) # -> -1
EOF
# split instructions (256 KiB ROM banks):
tools/smart34010/dis34 vt1200_v22.bin 0x<off> ...
```
References: `reference/memory-space-map.md` (rows 5–6, 10 — **corrected here**),
`reference/level1-bypass-analysis.md` (address conventions, level-1 gating),
`reference/level1-test-sequence.md` (test list, error codes),
`harness/mame/{OS_BRINGUP_REPORT,DUART_REPORT,FULL_BOOT_ESTIMATE}.md`,
`EXPLOIT_NOTES.md` (font delivery recipe, the heap primitive),
`NEXT_SESSION.md` §5–6 (font path gotchas, MAC), `lat/notes/lat-receive-audit.md`
(LAT path), `docs/EK-V1000-SV-002_service-guide.txt` (levels, P3, font paths),
`docs/DTJ-1991-vol3num4_X-Window-Terminals.txt` (font daemon vs TFTP).