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 |
TABLET_REPORT.md |
the VSXXX-AB tablet: the self-test device-code ID, the 5-byte absolute report, and absolute host-pointer input |
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 offset0x1bfffc).
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
SCN2681DUART 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
SCN2681at 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) — either a DEC VSXXX-AA mouse (the default, relative)
or, with
VT1200_TABLET=1, a DEC VSXXX-AB tablet (absolute). All are modelled fromreference/keyboard-mouse/PROTOCOL.mdand wired to the SCN2681 at the bit level (rx_a_w/rx_b_wanda_tx_cb/b_tx_cb). They answerFD/Tbecause that is the protocol, not to satisfy a test. SeeINPUT_REPORT.mdandTABLET_REPORT.md. - 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
0xE1write to DUART2-ATHRA(handler reg 3, window0x0620000, store at bit3FDCE8B0), then feeds 10 bytes back on the same channel's receive path via the SCN2681's ownrx_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).
With VT1200_TABLET=1 the channel-B device is the VSXXX-AB tablet
instead: 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). The mouse stays the default.
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 (19; 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,
VT1200_INPUT_LOG (per-byte keyboard/mouse/tablet TX/RX trace, with emulated
time). Behaviour switches: VT1200_SKIP_LEVEL1, VT1200_TABLET (put the
VSXXX-AB tablet on DUART2-B instead of the VSXXX-AA mouse — the tablet 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:
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.samples=53(78 PCs) →samples=198(3 PCs): DUART2 became a realSCN2681. Level 0's I/O data path test now fails honestly instead of being satisfied by the write-back latch's phantomRXRDY, 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. 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.