Files
vt1200/harness/mame
SDD f7caf9aed8 harness: opt-in VT1200_IO_PATH_STUB stand-in for the level-0 keyboard-port test fixture
The level-0 I/O data path test writes one 0xE1 byte to DUART2-A THRA and
demands exactly 10 bytes back; it checks only the count and never compares
the content, and no documented keyboard/mouse/tablet message is 10 bytes.
Add an opt-in stand-in that watches for that 0xE1 write and feeds 10
arbitrary (0x00) bytes on the channel's receive path via the SCN2681's
public rx_fifo_push(), refilling the 4-deep FIFO on each RHRA read.

This is explicitly NOT a keyboard model: it stands in for an unknown test
fixture on the keyboard port. It replies only while the channel is polled
(IMR=0x00), so it raises no interrupt; with the stub on the run clears
level 0 and reaches level 1's HOST PORT - 15, but still no OS (SKIP_LEVEL1
remains the only route to X11). Default is unchanged: no env still fails
level 0 honestly (progress.lua samples=198/3 PCs); stub-on is 53/75 PCs.
See harness/mame/README.md.
2026-10-16 14:31:20 +03: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).

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

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.
  • A mono palette (pens 0/1 = black/white) and an active-pixel scanline renderer that reads VRAM. See RENDER_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's HOST PORT - 15 (and PRINTER PORT - 16), which is the faithful ceiling. It does not produce a booted OS: level 1 still fails on the DUART fidelity ceiling, so VT1200_SKIP_LEVEL1=1 remains the only route to X11.

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 does not work — nothing is attached — and nothing is faked to pretend otherwise.

Not modelled: LANCE Ethernet. It does not block the X11 bring-up.

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.

Environment knobs (17; down from 33)

All opt-in, and all off by default — there is no longer a "working incantation" to remember, because everything the firmware needs to run is now the default.

Instrumentation: VT1200_DUARTLOG, 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. Behaviour switches: VT1200_SKIP_LEVEL1, 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 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, then parks at level 1's HOST PORT - 15 (verified from the console transcript: LEVEL1/2 TESTING **, then LEVEL1/2 ERROR -> HOST PORT - 15). 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.

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.