30 KiB
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/0x5800000is 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 correctsreference/memory-space-map.mdrows 5–6 ("DMA/board data/command port") andvt1200_board.h's "DMA pair" naming. - The address ROM is a separate device at physical byte
0xD00000(bit0x6800000), not a region of the firmware image: the known MAC08:00:2b:16:4b:5adoes not appear anywhere invt1200_v22.bin. - MAME has
am79c90/am7990(src/devices/machine/am79c90.cpp). It is a near drop-in for the register protocol (itscsr0STOP 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 (
tapbackend + 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 pathL3F207A60, which setsa10 = 0x33(the LED code for error 33) and writes4(CSR0 STOP) back to0x5800000(3f207a80). Service-guide stringLEVEL0 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 write0/a7to 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.INT1vector =0xFFFFFFC0,INT2vector =0xFFFFFFA0(docs/TMS34010-users-guide.txtTable 8-2).INTENBbit 1 =X1E(enable INT1), bit 2 =X2E(enable INT2);INTPENDbit 1 =X1P, bit 2 =X2P(guide §6, register descriptions). - The ROM vector page holds
RLONG(0xFFFFFFC0) = 0x00000F60andRLONG(0xFFFFFFA0) = 0x00000F30— RAM stubs (bit0x0F60/0x0F30) into which the firmware writesJAinstructions 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: retiL3FDDCEA0reads@0x5800000(3fddcf10). This is the lead confirmed: the handler on one of the two external-interrupt vectors reads the RDP. - The LANCE driver installs
0x3FA93400and then enables INT1 only:0x3FA8C340–0x3FA8C470callsL3FA90230(handler=0x3FA93400, …)and thenL3FDF9420(2).L3FDF9420isINTENB |= mask(3fdf94b0loads0xC0000110), so2= bit 1 =X1E= enable INT1. - Contrast: the DUART1/board-window setup (
0x3FA84C70) installs its handlers withL3FDF9420(6)=INT1|INT2, and later OS-phase DUART1 handlers0x3FA934C0/0x3FA935D0(window0x6400000) 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
INTPENDbit 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 init0x3F9D2920(reads 6 bytes into@0x23cca0, then at0x3F9D2B60copies 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()computesinit_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-netdevprovider 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'sPADR(bytes 2–7) and callsset_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 witham79c90: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 explicitlance_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 wordB >> 1:(or go throughm_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]); });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 conventionhandler offset = (bitaddr-base)>>4(each 16-bit word occupies 16 address units), so a physical byteBis at CPU bit addressB*8. - IRQ:
m_lance->intr_out().set_inputline(m_maincpu, 0).invert();(line 0 = INT1;.invert()because/INTRis active-low and the TMS34010 input is asserted byASSERT_LINE, cf. the DUART wiring atvt1200.cpp:645). Gate it to the OS phase if a POST-phase INT1 conflict with the DUART appears — the LANCE is neverINEA-enabled during POST, so it should be idle anyway. - Address ROM: map bit
0x6800000(byte0xD00000) 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 offseti= MAC bytei). Where the board model currently serves this window, override it. Long-term aromregion +set_romis cleaner than an inline lambda.
2.3 Network backends in this build
- Compiled into
/home/arti/code/k-space/mame0289/vt1200:noneandtap(taptun) only. Verified by symbol inspection:netdev_noneandnetdev_tapare present;netdev_pcapis absent. The source tree haspcap.cppandtaptun.cpp(src/osd/modules/netdev/), andNETDEV_PCAPexists 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):
- build/run with the
tapprovider (-netdev tap -netdev_interface …), create a persistent TAP interface, and bridge it to the host; or - rebuild MAME with
OSD_NET_USE_PCAPand attach to a real NIC/bridge. Then point the host TFTP/LAT server at the bridge address.
- build/run with the
- For the level-0/level-1 tests and for "the OS brings the interface up",
-netdev noneis 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 /
fsprotocol in the firmware (ROM strings showFont 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 inEXPLOIT_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 (servesfonts.dir+ the PCF/SNF set fromreference/fonts/), andtools/fontgen.py(TFTPServer,mutate,pcf_tables,set_num_chars) — builds the crafted PCF (num_chars = 0x40000001→n*4wrap → 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:
- LANCE modelled (§2.2) and the OS interface up (§4).
- A host peer via
tap+ bridge (or pcap rebuild). - The terminal needs an IP. The emulator loads default NVRAM settings, so
the IP is
0.0.0.0; the OS then printsCould 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. - Serve
fonts.dir+ the crafted PCF withharness/serve.py/fontgen, pointed at the Terminal Manager's TFTP Font Path. - Watch the parse: breakpoint the confirmed sites
pcfReadBitmaps0x0F09CE,pcfReadFont,fosAlloc0x3F703D90(already done byoverflow.lua). This is the payoff the LANCE unlocks.
4. Acceptance criteria
Each criterion is observable and testable.
A. Register protocol (unit / early boot).
- With the LANCE modelled, the level-0 "LANCE not stopped" test passes:
no
LEVEL0 ERROR -> LANCE NOT STOPPED - 33, and the error handler at0x3F207A60is never entered. - The level-1 "Ethernet port (LANCE) test" (
0x3F202830) and the "Ethernet address ROM test" (0x3F230890) both pass — noEthernet Port Error - 24(0x3F4AB090) orEthernet 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 (thel1patch.luaruntime-patch mechanism, or the NCPT 9/T 12menu) rather than a full level-1 run. Acceptable evidence: a run that reachesL3F202B60(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:
- The fronting board window.
0x5800000/0x5900000are the LANCE's RAP/RDP, but the same windows (and0x5600000,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. - 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,0x87bc0bit →0xFF60,0xFF78,0x10F78byte) and the blitter's RAM use. - LANCE clock unknown. The reference clock feeds interrupt/poll timing
only;
kn01uses 12.5 MHz,ncd10 MHz. Any value should work for the register tests and INIT; note it if transmit pacing matters. - 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.
- 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).
tapneeds privileges; the bridge must be up before MAME starts. - 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
0x5800000window beyond RDP — resolve by logging the OS run. - The exact address-ROM format/checksum.
- The LANCE's real clock.
- Whether
INT2fully substitutes forINT1for DUART1 during POST (the preceding interrupt-line work settles this; the LANCE requires it).
6. Sequencing
- 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.
- This plan: LANCE register window + IRQ + address ROM → DMA → OS interface up → host peer (TAP/BOOTP/TFTP) → font parse in-emulator.
- 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.
- 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'sR 05800000 pc=3F201370now returns0x0004(bit 2 set) and3F207A60is not reached. - Address ROM. Map
0x6800000with08 00 2b 16 4b 5aat handler offsets 0–5. Check: the ROM read at0x3F2308F0yields those six bytes; run the ROM test via the test map and confirm noEthernet ROM Error - 23(reverse the check first if it still fails — risk 4). - Level-1 LANCE test. Verify the register read/write path.
Check:
0x3F202830–0x3F202A00accumulates zero and branches toL3F202B60. - DMA callbacks. Wire
dma_in/dma_outto main RAM with the byte→bit translation; verify against the POST init's ring addresses. Check: breakpointinitialize(); the 12 init-block words read back are the ones the firmware wrote; the PADR equals the MAC. - IRQ. Wire
intr_outto INT1 (.invert()), with DUART1 on INT2. Check: an asserted LANCE IRQ is serviced by0x3FA93400, which reads0x5800000;INTENBbit 1 is set (L3FDF9420(2)). - OS interface up. Boot the skip-level-1 path; confirm INIT/IDON/start. Check: criterion B.3 and B.4.
- Host peer. Add the
tapprovider path (bridge) or rebuild with pcap; add a minimal BOOTP responder (or set the IP via the keyboard/mouse UI). Check:pingfrom the host to the emulated terminal, or a host capture showing the terminal's ARP/BOOTP. - Font end to end. Serve
fonts.dir+ a crafted PCF; breakpointpcfReadBitmaps. Check: criterion C.5. - 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.