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 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. - 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 bytes. An LK401
model cannot fix it either (the keyboard's FD reply is 4 bytes, not 10). See
reference/level1-test-sequence.md, where the 0xE1 + 10-byte question was first
raised and where it remains open.
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 (16; 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 switch: VT1200_SKIP_LEVEL1.
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.
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.