Files
vt1200/harness/mame
arti 8d8f8656df NETWORK_REPORT: correct 54.4's coverage claim in place
It said every addressed function has a BSD counterpart. The protocol layer
does; the deferred receive/submit path and the interrupt dispatcher are
firmware glue with no 4.4BSD equivalent (section 56).
2026-10-30 01:41:06 +02:00
..

VT1200 execution harness (MAME)

A minimal MAME machine that loads the VT1200 firmware into a TMS34010 CPU so we can run it under MAME's debugger: instruction trace, breakpoints, single-stepping, and direct memory watch. It is a visibility tool, not an accurate VT1200 emulation.

Everything in this directory is source-of-truth in this repo; the MAME source tree is disposable and lives outside the project (/home/arti/code/k-space/mame0289).

Working conventions

Five rules. Each of them has already cost this project real time, which is why they are written down rather than assumed.

  1. Never search from /. A filesystem-wide find / or grep -r is slow, visible to whoever is at the keyboard, and never necessary. Use rg with an explicit path — this directory, or /home/arti/code/k-space/mame0289/src (within which src/devices/machine, src/devices/bus, src/osd/modules/netdev cover most questions). Never /usr, /opt, /home at large, /proc or /sys. Worth knowing why this keeps happening: scn2681.cpp does not exist (the SCN2681 lives in mc68681.*), so a search for it walks the entire disk before giving up.

  2. Stage only your own paths — and do not edit a file someone else is holding. git add <path> [<path>…] — never git add -A, never git commit -a. Several people and workers share this repository at once, and a -A sweeps someone else's in-flight files into your commit. That happened twice in one day, and then a third time because the orchestrator edited this very README while a worker held it — so note that an uncommitted edit of yours can be swept into someone else's commit just as easily as an unstaged file. If you are about to touch a file another worker owns, don't: say what you want changed instead. If a collision does happen, unstage, git commit --amend, and say so.

  3. One sample is not evidence. "It never paints", "the driver only models 256 KiB of RAM", and "it is stuck" were all wrong, and all three came from a single observation. Before concluding, either compare across time (several frames, several minutes, several runs) or read the source. A claim about behaviour needs more than one look; a claim about the code needs the code.

  4. Changing MAME requires a quote and a second reader. Plausible-sounding sentences get assembled to justify a number that was really found by trial and error — one such edit was reverted after an independent reader found the datasheet said the opposite. So: quote the sentence or the source line, add a revert note, and have someone else read the citation before it is believed.

  5. Do not fake it. No responder that feeds the firmware the answer a test wants. Where a real device is unknown, model what the documentation actually supports and report the gap; where a stand-in is genuinely unavoidable, make it opt-in and name it for what it is — see the divergences table above: VT1200_IO_PATH_STUB, VT1200_SKIP_LEVEL1, and the tablet default are all documented divergences, not quiet assumptions. And the ROM stays byte-identical (md5 b46a3e442ccb86134d2356bd70d22fd2): patch at runtime if you must, never the file.

harness/mame/
├── vt1200.cpp        # the MAME driver (symlinked into the MAME tree)
├── build.sh          # clone-safe build wrapper
├── *.lua             # probe / capture / dump scripts (many; see the reports)
└── roms/vt1200/      # MAME ROM dir (symlink to ../../vt1200_v22.bin)

Reports in this directory:

file what it records
GATE_CLEANUP_REPORT.md promoting the working config to the default; retiring the settled env gates
TABLET_REPORT.md the VSXXX-AB tablet: the self-test device-code ID, the 5-byte absolute report, and absolute host-pointer input
LANCE_REPORT.md the LANCE Ethernet model: the register file, the 8-byte address ROM and its checksum, the byte↔bit DMA, and the remaining loopback/CRC blocker
LANCE_LOOPBACK_REPORT.md C — LANCE TEST: the TX buffer does carry a correct FCS in the passing DTCR=1 case and deliberately carries none in the failing one; the residual mismatch is MAME never setting RMD1_FRAM
NVRAM_REPORT.md the battery-backed NVR (bit 0x6000000): previously unmodelled (its address was swallowed by the video-memory mapping), now a persisted nvram_device; the font path lives there, the font data does not
FONT_STORAGE_REPORT.md the TFTP font "Disk full" abort: a capture proves the terminal sends TFTP error 3, and it is the firmware's tftp_close (0x3F5EC860) reporting an incomplete read, not a storage condition; the font buffers are in correctly-mapped RAM, so there is no second map overlap
RENDER_REPORT.md the active-pixel scanline renderer, and the geometry it derived
DUART_REPORT.md the DUARTs: wiring, the ISR-driven transmit, the datasheet-verified model edits
BRINGUP_LOG.md the whole bring-up narrative, including withdrawn claims — read it sceptically
ROMPATCH_REPORT.md runtime ROM patching (debug only — see the caveat)
HELLO_WORLD_REPORT.md the TMS34010 SDK assembles an 82-byte payload of our own code; it runs, writes RAM markers and paints a VRAM block. Debugger entry, not a delivery mechanism

Address model (the important part)

The TMS34010 is bit-addressed: a register/PC holds a bit address, and a pointer P refers to physical byte P >> 3. MAME models this with a program space of memory_access<32, 1, 3>, i.e. the address-map unit is 1 bit (a 16-bit word spans 16 address units). Source: tms34010.h — note the TMS34020 differs (memory_access<32, 2, 3>), so this is not interchangeable with the 34020. Therefore:

  • Address-map entries here are bit addresses, not byte addresses.
  • read_word(0x3f200000) returns ROM bytes 0..1 (physical byte 0x07e40000).
  • The reset vector is read from logical bit 0xffffffe0, which maps to the last 4 bytes of the image (file offset 0x1bfffc).

The driver maps:

region bit range bytes
RAM 0x00000000–0x001fffff 4 MiB at byte 0
ROM 0x3f200000–0x3fffffff 1.75 MiB at byte 0x07e40000
ROM aliases 0x7f…, 0xbf…, 0xff200000–0xffffffff same ROM (top 2 logical bits not decoded)

The 0xff… alias is what makes the reset-vector page resolve.

What is modelled by default

This changed — see GATE_CLEANUP_REPORT.md. The working configuration used to be a six-variable incantation; it is now the default, and a bare vt1200 vt1200 with no environment runs it.

  • Video RAM at bit 0x04000000 — required for the level-1 VIDEO MEMORY test.
  • The board-window handshake model (two-tick busy bit).
  • MAME's SCN2681 DUART at DUART1 (0x0640000, host/printer), with input port IP4–IP6 held high — that is how the driver reads the VR150 monitor switch (0x70). The level-1 host-port transmit is ISR-driven, so a real DUART is required here, not a convenience.
  • MAME's SCN2681 at DUART2 (0x0620000, keyboard/mouse). This one is a faithfulness fix, not a convenience — the real board has one. Without it the board write-back latch answers a read of DUART2's write-only IMR with its own written value, so the OS services a phantom interrupt that can never clear.
  • The real input devices on DUART2, driven by MAME's host input: a DEC LK401 keyboard (channel A, 4800 8N1) and a pointing device on channel B (4800 8O1) — a DEC VSXXX-AB tablet (the default, absolute) or, with VT1200_MOUSE=1, a DEC VSXXX-AA mouse (relative). All are modelled from reference/keyboard-mouse/PROTOCOL.md and wired to the SCN2681 at the bit level (rx_a_w/rx_b_w and a_tx_cb/b_tx_cb). They answer FD/T because that is the protocol, not to satisfy a test. See INPUT_REPORT.md and TABLET_REPORT.md.
  • A mono palette (pens 0/1 = black/white) and an active-pixel scanline renderer that reads VRAM. See RENDER_REPORT.md.
  • The LANCE Ethernet controller — MAME's am79c90 (AM79C90) at the firmware's two register windows (bit 0x5900000 = RAP, 0x5800000 = RDP), with its bus-master DMA bound (LANCE byte B → main-RAM word B >> 1) and its interrupt on INT1. The Ethernet address ROM at bit 0x6800000 is modelled as the eight bytes the firmware actually reads: 08 00 2b 16 4b 5a c1 86 (MAC + a checksum it validates). See LANCE_REPORT.md.
  • The NVR (battery-backed settings RAM) at bit 0x6000000 — a persisted nvram_device over the 0x800-word user-settings block, saved to MAME's nvram directory. Before this it was unmodelled and its address was swallowed by the video-memory mapping, so settings never persisted. See NVRAM_REPORT.md.

A bare run gets through POST and then fails level 0's I/O data path test, so it lands in the interactive diagnostics menu (VERSION 2.08 - SELECTED TEST TYPE:) instead of reaching level 1.

That test wants 10 bytes on DUART2 channel A and nothing is attached, so this is an honest failure. It used to "pass" by accident: the write-back latch returned 0x0099 from DUART2's status register, and bit 0 of 0x99 is RXRDY — stuck asserted — so the firmware's drain loop counted ten phantom 0x8C bytes.

It is not satisfiable by any documented device — see reference/level0-10byte-puzzle.md. The routine writes exactly one byte (0xE1, "disable auto-repeat", which has no reply), sets MR2A=0x07 so the channel is in normal mode, not loopback (confirmed statically and from a live access log), and checks only the count: the received byte is stored and never compared, and there is no expected-value table. The keyboard's FD/power-up reply is 4 bytes, the mouse's T reply is 4, the tablet's 5 — none is 10. A faithful LK401 would not help either: the init sequence's receiver-reset flushes the FIFO.

The real terminal does pass this test, so the bytes exist on real hardware — they are simply not inferable from the ROM, and the only honest way to close the question is to observe a real keyboard port at power-up. No faithful responder can be derived from any document.

So instead there is an opt-in stand-in, VT1200_IO_PATH_STUB=1. Read what it is carefully, because it is easy to misread:

  • It is not a keyboard model and it is not any documented device. It stands in for an unknown test fixture on the keyboard port.
  • Its bytes are arbitrary (0x00). Nothing compares them: the routine stores the received byte and never reads it back, and there is no expected-value table, so only the count matters. Feeding a pattern would imply a protocol that does not exist.
  • It triggers on the exact thing the test does: the one 0xE1 write to DUART2-A THRA (handler reg 3, window 0x0620000, store at bit 3FDCE8B0), then feeds 10 bytes back on the same channel's receive path via the SCN2681's own rx_fifo_push(). It replies only while the channel is polled (IMR = 0x00, the level-0 condition), so it cannot inject into the interrupt-enabled OS phase or cause an interrupt there.
  • It is off by default. A bare run still fails level 0 exactly as before and says so. This is deliberate: the faithful configuration stays the default.

With the stub on the boot clears level 0 and reaches level 1, where the DUART tests fail: HOST PORT - 15, then PRINTER PORT - 16. It does not park at HOST PORT - 15. Each failure is reported, the board LED blinks once (finite), the firmware prints TYPE 'GO' TO CONTINUE (which nothing answers and which does not wait), and the runner continues. It then enables DUART1's counter-ready interrupt and wedges in a DUART1 interrupt storm — that is the faithful ceiling, not HOST PORT - 15. It does not produce a booted OS. See LEVEL1_CONTINUE_REPORT.md.

There is no infinite board-LED loop on this path. 3F208060/3F208650 are never executed here, and the boot loop's 3F204440 jane L3F208060 is never reached (the level-1 runner never returns). The blink that is seen is the finite L3F208660 called by the error handler. A level-1 failure is fatal only through L3FDF1010 → L3FDF1270, the genuinely infinite blink, which is entered when the status word *A13(0xFFA0) is nonzero; the DUART failures leave it zero.

How far the OS gets

With VT1200_SKIP_LEVEL1=1 (below) the OS boots to a running X11 server and on to the Terminal Manager: the root-window weave renders, the cursor is handed over to the terminal's own mouse cursor, and the V2.2 box appears — title bar, Messages, Ethernet/IP/LAT, a Memory bar, and "All Diagnostics Passed".

Confirmed in the GUI and from frame dumps: skip_frame3000.png (the weave) and duart2_default_tm_frame3000.png (the Terminal Manager). The real terminal shows the same sequence — see reference/vt1200-boot-video/.

The OS's keyboard and mouse drivers initialise and speak the real protocol: it programs DUART2 (MR1A=0x13, MR2A=0x07, CSRA/CSRB=0x99, ACR=0x60, IMR=0x33) and sends FD plus LED/click/bell/auto-repeat to the keyboard and 'T' to the mouse. Input now works: the LK401 and VSXXX-AA device models answer those commands, the OS reads 01 00 00 00 / A0 02 00 00, switches the mouse to stream mode, and the X11 cursor moves with the mouse while keycodes reach the OS. See INPUT_REPORT.md (the cursor evidence is the input_cursor_*.png frames).

The channel-B device is by default the VSXXX-AB tablet: it identifies as device code 0100, so the OS switches to 5-byte absolute reports, and the emulated cursor follows the host pointer's absolute position (driven through IPT_LIGHTGUN_X/Y, i.e. MAME needs -lightgun -lightgun_device mouse). See TABLET_REPORT.md (evidence: tablet_cursor_*.png). VT1200_MOUSE=1 puts the VSXXX-AA mouse back (device code 0010, 3-byte relative reports). The mouse is the faithful part — the real terminal almost certainly has a mouse — so the tablet default is a deliberate divergence (below).

The OS now also programs the LANCE: it reads the Ethernet address ROM, programs CSR1/2/3 with its init block, issues INIT, sees IDON, issues STRT and polls CSR0 in its interrupt path. The Terminal Manager therefore shows the real hardware address — Ethernet Address 08-00-2B-16-4B-5A, LAT Host Name LAT_08002B164B5A (it used to show 00-00-00-00-00-00). The Could not determine IP address banner is the missing address the plan allows (no network peer), not a LANCE failure. See LANCE_REPORT.md.

Not fully working: the firmware's C - LANCE TEST (its internal loopback) still fails, but not because our DMA loses the FCS. The test's failing case is a negative test that deliberately sends a frame with no FCS and expects a CRC error; the frame that carries an FCS (the DTCR=1 passing case) is delivered intact and passes. The one bit left is RMD1_FRAM — the test expects RMD1 = 0x6B00, MAME reports 0x4B00, and MAME never sets FRAM. See LANCE_LOOPBACK_REPORT.md; the M - ETHERNET ROM TEST passes.

Getting past level 1

VT1200_SKIP_LEVEL1=1 drives DUART1's IP3 high, which the firmware takes as a signal to jump to the OS cold-start and skip all 15 level-1 tests. Note what this is: a modelled board input, and the real machine reads IP3 low. So a level-1-skipped run is evidence about the OS, graphics and crash behaviour, and is not evidence about the real terminal's POST.

The skip is deliberately not the default: the real machine runs level 1, so the faithful configuration stays the default and the switch is how we diverge.

Deliberate divergences from the target machine

Three harness behaviours are not what the real terminal does. They exist so the emulator can be driven and inspected, and they are collected here so a reader cannot mistake the harness for the machine:

divergence default what it does how to restore the real machine
VSXXX-AB tablet on puts the tablet, not the mouse, on DUART2-B; absolute reports let the host pointer drive the cursor positionally VT1200_MOUSE=1
VT1200_SKIP_LEVEL1 off drives DUART1 IP3 high, jumping past all 15 level-1 tests to the OS cold-start leave it unset
VT1200_IO_PATH_STUB off feeds 10 bytes on DUART2-A after the level-0 0xE1 write, so the count-only I/O data path test passes leave it unset

The tablet is the one that is on by default because the OS is only reachable through the skip gate, and there the tablet's absolute pointer is what makes the Terminal Manager usable. It is a value-aware inverse switch: the default lives in the code, not in a wrapper script setting VT1200_TABLET, so VT1200_MOUSE=1 (or VT1200_MOUSE=0 for the tablet) always flips it either way.

Environment knobs (21; down from 33)

Almost all are opt-in and off by default — there is no longer a "working incantation" to remember, because everything the firmware needs to run is now the default. The one exception is that the tablet is on by default; VT1200_MOUSE is the switch that restores the faithful mouse (see the divergences above).

Instrumentation: VT1200_DUARTLOG, VT1200_LANCELOG (every LANCE RAP/RDP and address-ROM access with the PC), VT1200_LANCE_DMA_LOG (every LANCE bus-master DMA word access: byte address, value, byte lanes, PC — how the transmit buffer and the descriptor write-back are read directly), VT1200_BOARD_LOG, VT1200_BOARD_MAP, VT1200_IRQLOG, VT1200_LOG_UNMAPPED, VT1200_DPYLOG (+_FRAMES), VT1200_DUMP_FRAME (+_FRAMES), VT1200_DUMP_RAM (+_FRAMES), VT1200_DUMP_VRAM (+_FRAMES), VT1200_DUMP_DIR, VT1200_NO_RENDER, VT1200_INPUT_LOG (per-byte keyboard/mouse/tablet TX/RX trace, with emulated time). Behaviour switches: VT1200_SKIP_LEVEL1, VT1200_MOUSE (put the VSXXX-AA mouse (relative) back on DUART2-B; the VSXXX-AB tablet is the default — either pointing device needs MAME's -lightgun -lightgun_device mouse to follow the host pointer), VT1200_IO_PATH_STUB (the level-0 test-fixture stand-in described above — not a keyboard model, off by default).

Regression baseline

progress.lua at 200 frames, no env:

PC=3FDCE400 samples=198 (3 distinct PCs)

This has moved twice, both deliberately:

  1. samples=199 (1 PC) → samples=53 (78 PCs): the working configuration became the default, so a bare run stopped wedging at the hardware-ready gate and began executing real POST work.
  2. samples=53 (78 PCs) → samples=198 (3 PCs): DUART2 became a real SCN2681. Level 0's I/O data path test now fails honestly instead of being satisfied by the write-back latch's phantom RXRDY, so the run parks in the diagnostics menu. That looks like a regression and is not one — it is the removal of a false pass.

The tablet-default change did NOT move this baseline (198 → 198), and that is expected, not an oversight. The tablet changes what DUART2-B carries on the wire — its T self-test reply is A0 04 00 00 versus the mouse's A0 02 00 00, and it then frames 5-byte absolute reports instead of 3-byte relative ones — but at 200 frames a no-env run is still parked in the level-0 diagnostics menu and has never addressed DUART2-B at all: the T/R exchange happens only in the OS phase, which the no-env run does not reach. The only DUART2-B traffic in the window is the device's unsolicited power-up self-test, which nothing clocks in. The OS-phase render is unchanged too: with VT1200_SKIP_LEVEL1=1 the frame-3000 dump is pixel-identical for the tablet and for VT1200_MOUSE=1 (magick compare -metric AE = 0), and both reproduce the committed tablet_cursor_*.png frames exactly.

The no-env value is unchanged by VT1200_IO_PATH_STUB — stub off is the default, so the samples=198 (3 PCs) line above still holds. The stub is a separate observed configuration:

# VT1200_IO_PATH_STUB=1, progress.lua at 200 frames
PC=3FDCE400 samples=53 (75 distinct PCs)

That is a genuine move, not a false pass: with the count-only level-0 test satisfied, the run leaves the level-0 wait and executes level-0/level-1 work. It is not parked at HOST PORT - 15: the console transcript continues (LEVEL1/2 TESTING **, LEVEL1/2 ERROR -> HOST PORT - 15, LEVEL1/2 ERROR -> PRINTER PORT - 16, TYPE 'GO' TO CONTINUE), and the run ends in a DUART1 counter-ready interrupt storm (from frame ~2839; PC in the DUART1 INT1 ISR, polling ISR reg 5 = 0x08). See LEVEL1_CONTINUE_REPORT.md. It is recorded as a different baseline because it is opt-in; the default must stay samples=198.

It is a change detector, not a specification. If it moves without you meaning it to, find out why.

Build

MAME source is required. A shallow 0.289 checkout is large and slow to fetch.

git clone --depth 1 --branch mame0289 \
    https://github.com/mamedev/mame.git /home/arti/code/k-space/mame0289

harness/mame/build.sh            # symlinks the driver in and runs make

build.sh runs make -j$JOBS SUBTARGET=vt1200 SOURCES=src/mame/dec/vt1200.cpp, which builds a single-driver binary /home/arti/code/k-space/mame0289/vt1200.

The MAME tree is large and the build is memory-hungry: build.sh defaults to -j8, not all cores. Override with JOBS=4 harness/mame/build.sh on a small/low-memory machine. Progress is written to harness/mame/build.log.

Run

Headless execution and inspection use MAME's Lua debugger API from -autoboot_script. (-debugscript only works when a debugger UI drives the console, so it is not usable headless.)

cd /home/arti/code/k-space/vt1200
M=/home/arti/code/k-space/mame0289/vt1200

# trace execution from the reset vector
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
   -debug -debugger none \
   -autoboot_script harness/mame/trace.lua -autoboot_delay 0

# demonstrate the pcfReadBitmaps n*4 overflow (OVF_N overrides n, decimal)
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
   -debug -debugger none -debuglog -seconds_to_run 5 \
   -autoboot_script harness/mame/overflow.lua -autoboot_delay 0

The Lua API exposes manager.machine.debugger, device.debug (step, go, bpset, wpset), CPU registers (cpu.state['A0'].value), memory (cpu.spaces['program']:read_u32/write_u32), and frame hooks.

Drive the keyboard and mouse (input_inject.lua)

The LK401/VSXXX-AA models read MAME's host input, so they can be driven from Lua for a headless acceptance run:

M=/home/arti/code/k-space/mame0289/vt1200
VT1200_SKIP_LEVEL1=1 \
VT1200_DUMP_FRAMES=4400,4600 VT1200_DUMP_DIR=/tmp \
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
   -debug -debugger none \
   -autoboot_script harness/mame/input_inject.lua -autoboot_delay 0

input_inject.lua walks the mouse from frame 4400 and taps A at frame 4450. input_probe.lua prints every device port/field tag. See INPUT_REPORT.md.

Trace from the reset vector (trace.lua)

Prints the reset PC (expect 3F200040), disassembles the power-on code to reset.dasm, and writes an instruction trace of the first two frames to vt1200-reset.trace.

Exercise the pcfReadBitmaps overflow (overflow.lua)

Instead of booting the parser we jump directly into its arithmetic block with a crafted in-memory PCF_CharInfo:

a9  = 0x00040000   ; fake pCS;    pCS->nChars at a9+0x270
a13 = 0x00060000   ; fake arg blk; n (from pcfReadInt32) at a13+0x20
write_u32(0x00040270, 0)        ; nChars == 0
write_u32(0x00060020, n)        ; n
pc = 3f984e70
break at 3f985060               ; just after 'sll 0x2,a11'

This is possible because the font parser's field moves use field 1, which SETF 0,0,1 leaves at 32 bits, so n is a full 32-bit pcfReadInt32 value. See FINDINGS.md for results and the rest of the analysis.

Run our own payload (hello.lua)

harness/hello-world/ is an 82-byte payload written in GSPA assembly, built by the 1987 TI SDK (GSPA/GSPLNK under DOSBox). hello.lua loads it into main RAM at bit 0x01800000 at frame 120, then arms a breakpoint on the level-0 menu idle loop (3FDCE400) whose action is { pc = 01800000 ; g }.

This is debugger entry, not a delivery mechanism: it proves the toolchain produces runnable code and the code runs on the emulated CPU, not how code would reach a real terminal.

M=/home/arti/code/k-space/mame0289/vt1200
HELLO_END=150 VT1200_DUMP_FRAME=145 VT1200_DUMP_RAM=145 \
VT1200_DUMP_DIR=harness/hello-world/evidence \
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
   -debug -debugger none \
   -autoboot_script harness/mame/hello.lua -autoboot_delay 0

The payload writes 0xDEAD, 0xBEEF and a 1024-iteration counter at bit 0x01810000, and fills a 256x64 block in video RAM at row 100, column 64. The script reads every value back and prints ok/WRONG; a frame dump shows the block at screen x 256..511, y 136..199. HELLO_AT=9999 is the control run (nothing loads, markers stay zero). See HELLO_WORLD_REPORT.md and harness/hello-world/README.md.