Files
vt1200/docs/superpowers/plans/2026-10-16-lance-ethernet.md
T

30 KiB
Raw Blame History

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:

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:
    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:

// 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:
    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

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).


Host-side network plumbing (for the network milestone)

Recorded 2026-10-16 from the project owner, with the interface name verified from MAME's own source.

MAME's tap interface is not tap0. src/osd/modules/netdev/taptun.cpp:115 names it from the uid:

sprintf(ifr.ifr_name, "tap-mess-%d-0", getuid());

so for uid 1000 it is tap-mess-1000-0 (15 characters — right at IFNAMSIZ). The provider is taptun; networkprovider defaults to auto. This build has taptun and pcap modules present but the plan's earlier finding stands: tap is the usable path.

The owner's sequence, corrected for the real name and completed:

sudo ip link add dev vt1200 type bridge            # already exists (DOWN)
sudo ip link set tap-mess-1000-0 master vt1200     # NOTE: not tap0
sudo ip link set vt1200 up
sudo ip addr add 192.168.99.1/24 dev vt1200        # an address, so the host can serve

Better: pre-create the tap, so the bridge setup is done once rather than after MAME has started:

sudo ip tuntap add dev tap-mess-1000-0 mode tap
sudo ip link set tap-mess-1000-0 master vt1200
sudo ip link set tap-mess-1000-0 up
sudo ip link set vt1200 up
sudo ip addr add 192.168.99.1/24 dev vt1200

TUNSETIFF with an existing name attaches to it, so MAME will use the one we made. Host state as of recording: bridge vt1200 exists and is DOWN; no tap device is present yet (nothing has needed one).

Why this matters: the emulated terminal fetches fonts over TFTP (fonts.dir then the PCF) — see NEXT_SESSION.md and harness/serve.py / tools/fontgen.py. Bridging the tap where the host's TFTP server is reachable is what lets a font be delivered, which is the prerequisite for the crash work. The emulated side needs BOOTP or a manual IP in the same subnet.

Attaching the physical NIC to the bridge as well would give the emulated terminal real LAN access, which the LAT/announcement work might want — not required for font delivery, which is host-local.