34 KiB
VT1200 MAME bring-up log
Gate-by-gate boot progress. Companion to FULL_BOOT_ESTIMATE.md (the cost estimate)
and FINDINGS.md (the original harness + font work).
Goal: get the driver to actually boot the terminal, by discovering each hardware handshake the firmware waits on and modelling it correctly.
Harness features added (all opt-in via env vars)
| env var | effect |
|---|---|
VT1200_BOARD_LOG=<path> |
log every distinct (addr, PC) access to the three undecoded board windows |
VT1200_BOARD_MAP=<a=v;...> |
per-address fixed return values, e.g. 0x06200001=1 |
VT1200_BOARD_DEFAULT=<v> |
value for addresses with no override |
VT1200_BOARD_MODEL=handshake |
write-back registers plus a self-clearing busy bit |
VT1200_BOARD_BUSY_TICKS=<n> |
busy-bit tick count for VT1200_BOARD_MODEL=handshake (the busy bit is fixed to bit 0 of the +0x10 STATUS register) |
VT1200_BOARD_BUSY_W2=<n> |
busy ticks for window 0x06200000 only; applied after BUSY_TICKS, so it overrides it for that window |
VT1200_BOARD_BUSY_W3=<n> |
busy ticks for window 0x06400000 only; likewise |
VT1200_BOARD_DMA_TICKS=<n> |
completion time (ticks) of a command written to the 0x05900000 DMA command port; 0 leaves the pair as a plain latch |
VT1200_BOARD_SWITCH=<addr>:<mask>:<value> |
monitor switch: bits 4-6 of 0x0640000D read as <value> (raw bits, or a position 0-7) |
VT1200_BOARD_RAM=1 |
windows behave as plain read/write RAM |
VT1200_NO_RENDER=1 |
restore the old blank-everything video behaviour |
VT1200_DUMP_FRAME=<n> / VT1200_DUMP_DIR=<d> |
write that frame as a PPM |
With no env vars set the driver behaves exactly as before. Tools: progress.lua
(where is it), stackwalk.lua (who called it), coverage.lua (progress vs loop),
dpyctl.lua (display registers).
Gates found, in order
Gate 1 — hardware-ready poll (SOLVED, real value gate)
bcf240: CALLA delay(100)
bcf2a0: MOVE *A9(10h),A11,0 ; read the 0x6200000 window
bcf2c0: BTST 0h,A11
bcf2d0: JREQ BCF200h ; retry until bit0 set
while (!(read8(0x06200010) & 1)) delay(100);
Fixed with a single override on the harness's address 0x06200001. Verified: the
firmware leaves the poll and runs on into bank 6.
Gate 2 — NOT a gate (a pure delay)
008880: MOVI 112A880h,A0 / 0088b0: DSJS A0,88B0h — 18,000,000 iterations inside a
routine that writes 0640000D/E/F with long settling delays. Passes with time; no
value needed. Do not confuse a DSJS spin for a missing register.
Gate 3 — the register is a HANDSHAKE, so constants cannot work
The same register bit is polled with opposite polarity:
bcf420: BTST 0h,A9 JRNE -> waits for bit0 SET (command accepted)
bce670: BTST 0h,A11 JREQ -> waits for bit0 CLEAR (operation finished)
Therefore "the correct value" is not a constant. Implemented a stateful model:
registers are write-back RAM, and the busy bit arms on any write and clears after
VT1200_BOARD_BUSY_TICKS reads.
Result: with VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 the boot leaves
early POST and reaches the display/blit code.
Gate 4 — graphics/blit-complete poll (needs a stateful model, like gate 3)
In the boot bank, immediately after a TMS34010 graphics operation:
004880: JREQ 4960h
004a10: FILL XY ; graphics fill
004a20: CMPI 32h,A10 / JRNE 4BA0h
004a60: MOVE @64000D0h,A0,0 ; read the 0x6400000 window
004a90: BTST 2h,A0 ; test BIT 2
004aa0: JAEQ 3F204BA0h ; bit clear -> branch away
The access log corroborates it exactly: R 0640000D pc=3F204A90. So the firmware waits on
bit 2 of the 0x6400000 window right after a graphics fill — i.e. a
"graphics/blit complete" status bit.
Measured:
0640000D bit 2 |
result |
|---|---|
0 (stub default) |
parked in the display/blit loop, 59 distinct PCs |
4 (forced set) |
falls back into the POST video/refresh delay loop, 10 distinct PCs |
Neither boots. Forcing the bit constant simply relocates the wedge — the same lesson as gate 3. These board status bits are dynamic handshakes (busy → done), so a static value can never satisfy every site that reads them.
The shape of the problem (worth recording)
Every gate so far has been one of just two kinds:
- A pure
DSJSdelay (gate 2) — passes with time, needs nothing. - A status/handshake bit (gates 1, 3, 4) — needs behaviour, not a value.
That is a much better-scoped problem than "decode an unknown ASIC": the work is to understand the semantics of a handful of status bits, not hundreds of registers.
Where it stands
| stage | stuck at |
|---|---|
| no stubs | PC=3FDCE400 — one instruction, forever |
| gate 1 fixed | runs into bank 6, then the POST delay loop |
| handshake model | display/blit code, ~59–72 distinct PCs, still discovering new ones |
| + 4 MiB RAM | unchanged — size alone is not the blocker |
| + gate 4 bit forced | moves to the POST delay loop — also not it |
coverage.lua shows new PCs still appearing at frame 3000 (30 → 72 distinct), so it is
executing, not frozen — but over a very tight footprint, which may be a legitimate
display-refresh loop.
Video status (asked: "are we getting any video?")
The display controller IS enabled. Probed live:
frame 200 DPYCTL=E008 <ENABLED> CONTROL=000C DPYSTRT=FFFC DPYINT=0000 DPYTAP=0000 DPYADR=E4FC
frame 600 DPYCTL=6008 (bit 15 momentarily clear)
Register map used (MAME's tms34010 enum — MEMORY_MAP.md is correct here, but note these are not the classic datasheet offsets):
0x80 DPYCTL 0x90 DPYSTRT 0xA0 DPYINT 0xB0 CONTROL
0x1B0 DPYTAP 0x1E0 DPYADR 0x160 PSIZE 0x1F0 REFCNT
Firmware reference counts (ROM is little-endian for instruction operands — verified against the reset instruction at file 0x54):
DPYCTL 45 PSIZE 48 CONTROL 88 DPYADR 4 DPYTAP 2 DPYSTRT 2 DPYINT 8
So the display is fully configured by the firmware.
But the rendered frame is black — and that is our harness's fault, not the
firmware's. MAME's tms340x0_ind16 only paints the blanking regions:
if (params.enabled)
m_scanline_ind16_cb(screen, bitmap, cliprect.min_y, ¶ms); // active pixels
...
// blank out the blank regions
The active pixels are the driver's job — every TMS34010 driver in MAME supplies a
scanline renderer (see atari/harddriv_v.cpp:411 scanline_driver for the pattern).
Our callback draws nothing, so the active area stays zero.
Two traps hit while chasing this (recorded so they are not repeated)
-video nonenever callsscreen_update, so the scanline callback never runs andVT1200_DUMP_FRAMEsilently does nothing. Force rendering with-aviwrite.logerrorneeds-log(writeserror.log);-debuglogonly captures debugger console output (e.g. theprintfinside abpsetaction).- Put frame/RAM dumps in the scanline callback, not
screen_update. MAME callsscreen_updatewith small clip regions, and the "region that reaches the last line" (clip_y = 1..999) only happened once in an 80-frame run — so a dump guarded on it silently never fires. The scanline callback is invoked once per line (168, 169, 170, …), so guard onscanline == screen.visible_area().max_y.
Both callbacks do fire when rendering is forced, and the scanline callback reports
enabled=1 — i.e. the display really is on.
Observed frame dump (frame 60)
vt1200-frame-60.ppm 80x901, "79.56% lit"
Treat both numbers as unreliable: 80x901 is what MAME reconfigured the screen to from the firmware's HEBLNK/HSBLNK/VTOTAL registers (implausible for a real display — suggesting the timing registers are not yet properly initialised), and the "lit" percentage counts uninitialised bitmap data because the active pixels are never drawn.
RAM dump (frame 200, via the shared RAM pointer)
vt1200-ram-200.bin is 262,144 bytes = 256 KiB — and that size is itself a finding,
because the map range 0..0x1fffff with 8 address units per byte means the driver
only models 256 KiB of main RAM. The real VT1200 has 1 MiB onboard plus up to
3 × 1 MiB SIMMs (service guide), and POST runs a memory test. Enlarging the RAM
mapping is now a prime suspect and is a cheap experiment.
Contents of what we do model: full of 32-bit ROM pointers (0x3FD24A90, 0x3FD2C800,
…) and resident strings ($Header: init.c,v 1.3 …, DWT TCP/IP Rev 1.0,
LATSERVER/LANCESERVER/MOPSERVER). 99.6% of the earlier low-memory dump was
zeros, but that dump's granularity was wrong; the real picture is tables + pointers.
No framebuffer appears in main RAM. Combined with the self-test having a separate
N - VIDEO MEMORY TEST, the framebuffer is most likely separate VRAM mapped
somewhere not yet identified. That fits the black screen: the display address resolves
into unmapped space, so the renderer reads zeros.
Hardware facts supplied from the real machine (2026-10-16)
- Output is always mono black and white → 1 bit/pixel.
- 1280×1024 → 160 bytes per scanline, framebuffer 163,840 bytes (0xA0000).
- Boot sequence on real hardware: an ASCII/font table is printed first, then the screen wipes, then the self-test progress runs — shown as a large circle in the middle of the screen.
These give the renderer its geometry and a validation target: a correct renderer should show the character table first.
RAM-as-bitmap probe
vt1200-ram-200.bin rendered at the 160-byte stride (ram-as-bitmap.png,
crop-glyphs.png) shows a dense band around rows 738–747. Zooming shows it is not
glyphs — the hexdump is little-endian 32-bit ROM pointers, i.e. a pointer table.
What the service guide says (already in docs/ — read this first next time)
docs/EK-V1000-SV-002_service-guide.txt §2.1.1 documents the power-up self-test almost
exactly as observed on the real machine. This is the authoritative boot sequence.
Level 0 — 8 tests, must complete within 5 seconds:
Simple CPU test Video control register test
ROM checksum test I/O data path test
RAM test Illegal monitor test
Interrupt test Ethernet port (LANCE) not reset on power-up test
Level 1 — 15 tests, complete within 25 seconds of power-up. It "begins by displaying various patterns on the screen. Next, a pie graph display appears to indicate the progress of level 1 testing. As each test finishes, a segment of the pie fills in."
Video RAM test Video test NVR checksum test Ethernet address ROM test
Option RAM test Host port test Printer port test Image test
DUART1 timer test Keyboard port test Mouse port test DUART2 timer test
Ethernet port (LANCE) test Mouse test Keyboard test
Why this matters for our stall
- The user's "large circle in the middle of the screen" is the level-1 pie graph — and the "ASCII font table" seen first is the level-1 "various patterns". So the real machine is well into level 1 by ~12 s, and finished by ~25 s.
- We have run 22–44 s of emulated time and are still stuck. Confirmed: blocked, not slow.
- Level 0 needs hardware we do not model: an Interrupt test, an I/O data path
test, and an Ethernet port (LANCE) not reset on power-up test. Our gates
(status bits on the
0x6400000window) are almost certainly part of the video control register test or the I/O data path test.
The monitor selection switch is read at power-up
From §1.1.2: "The terminal searches for the monitor selection switch position only during the power-up sequence." Positions select the monitor model, which sets the screen size / dots-per-inch:
1 = VR150 CRT (15") 2 = VR319 CRT (19") 3 = VR262 CRT (19")
3 = VRE01 EL flat panel 5 = VR315 CRT (15")
And level 0 includes an Illegal monitor test. So the firmware reads that switch during power-up, and our model does not provide it — a stub returning all-ones or zero could plausibly fail "illegal monitor". Finding where that switch is read, and returning a valid position, is a concrete next target.
Video alignment patterns (§2.1.3) — useful validation targets
- Screen of H's — fills the active area with uppercase H's (focus)
- Circle Cross-hatch — crosshatch plus a large circle in the middle (linearity/aspect)
- All White — the whole active area white (active-area adjustment)
Confirmed hardware facts
- CPU is a TMS34010FNL-50 → 50 MHz (the driver currently guesses 40 MHz; the users guide lists the -50 part and VCC = max, 50 MHz).
- Display 1280 x 1024 at 72 Hz, monochrome.
- Memory: 4 MB total — 1 MB on the system logic board plus up to three 1 MB SIMMs.
Monitor-selection switch — FOUND (2026-10-16)
Located via the ROM string "Undefined monitor switch setting" (file 0x019AF4,
bit 0x3F2CD7A0) and its reference at file 0x190F8. The checking code is around bit
0x3F2C8320 (file ~0x19060):
3f2c80b0: move @0x62000d0, a11, 0 ; another config bit -> "Present"/"Not Present"
3f2c80e0: btst 5, a11
3f2c8320: move @0x64000d0, a11, 0 ; <<< THE MONITOR SWITCH PORT
3f2c8350: andi 0x70, a11 ; switch position is bits 4-6
3f2c8380: cmpi 0x70, a11 -> "VR150"
3f2c84a0: cmpi 0x50, a11 -> "VR262"
3f2c85c0: cmpi 0x30, a11 -> "VR315"
(also) -> "VR319"
else -> "Undefined monitor switch setting"
| bits 4–6 | monitor |
|---|---|
0x70 |
VR150 |
0x50 |
VR262 |
0x30 |
VR315 |
| other | VR319 / undefined |
The firmware prints a configuration block on screen during power-up:
Display Type Monochrome
Monitor Type VR150
All 6,574 ROM strings are now extracted to reference/rom-strings.txt (file offset
and bit address per line), so these messages are browsable.
The reason a constant override did NOT unblock the boot
0x64000D0 is a bit-mixed port: bits 4–6 are the switch input, while the rest is
a latched output that the firmware writes (the reset vector writes 0 to it). Returning
a constant for the whole byte breaks the output bits, so the boot stays stuck.
New harness knob for exactly this: VT1200_BOARD_SWITCH=<addr>:<mask>:<value> —
bits in <mask> read as <value>, all other bits stay write-back.
(Updated 2026-10-16: the model now applies this mix only to the monitor switch port
0x0640000D with mask 0x70, and <value> is the raw bits or a position 0-7. A
different <addr>/<mask> is warned about and ignored — see the extraction section at
the end.)
VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 \
VT1200_BOARD_SWITCH="0640000D:70:70"
Note the harness address is the disassembly operand >> 4 (see the address-unit note at
the end of this file), so the code's @0x64000D0 is the harness's 0x0640000D.
Verified with VT1200_BOARD_LOG: the port reads 0x70 while the firmware's own
write is 0x0000, i.e. the switch bits are inputs and the rest are latched outputs, as
intended.
It does not by itself unblock the boot — 0x00 and 0x70 take different paths
(56 vs 57 distinct PCs, different park points) but both still wedge in the display/blit
code. The remaining read sites are unchanged:
06200001 (6 PCs) 06200003 (3 PCs)
0640000D (6 PCs) 0640000E (2 PCs) 0640000F (2 PCs)
So the monitor switch was a correctness fix (no more "Undefined monitor switch setting"), not the blocker.
Harness bug fixed: build.sh was hiding compile failures
make ... | tee "$LOG" made the script's exit status that of tee, so set -e never
fired and the script printed built: even when the compile had failed — which sent
one whole round of experiments against a stale binary. build.sh now captures make's
status, tails the log and exits non-zero on failure. Treat a bare built: as
insufficient; it now genuinely means success.
Raster geometry — SOLVED (16 pixels per clock)
The firmware's own raster registers were the key:
DPYCTL=E008 (enabled) HTOTAL=004F(79) VTOTAL=0384(900) HSBLNK=004C(76)
DPYINT=0000 VCOUNT=0383
MAME computes width = (HTOTAL+1) * pixels_per_clock. With the driver's guess of
1 pixel per clock that gave an absurd 80-pixel line. But:
80 pixel-clocks per line x 16 pixels/clock = 1280 pixels <- exactly the display width
72 Hz x 80 x 900 = 5.184 MHz <- a sane pixel clock
So the TMS34010 fetches 16 pixels per clock, and the driver now sets:
TMS34010(config, m_maincpu, XTAL(50'000'000)); // TMS34010FNL-50
m_maincpu->set_pixel_clock(5'184'000);
m_maincpu->set_pixels_per_clock(16);
m_screen->set_raw(5'184'000, 1280, 0, 1280, 901, 0, 901);
Verified: screen: 1280x901, effective frame period 0.0142 s (~70 Hz, target 72).
Crash trap: pixels_per_clock must match the SCREEN's raw geometry. With
pixels_per_clock=16 and an 80-wide bitmap, the renderer computed
heblnk = 76 * 16 = 1216 and wrote 1216 pixels into an 80-wide bitmap —
double free or corruption. Always keep the screen width consistent with
(HTOTAL+1) * pixels_per_clock.
The clock comment in the driver was wrong
The original driver said "the exact value is irrelevant to instruction semantics". That is false: the firmware calibrates its delays from the video, and the raster geometry drives MAME's whole emulated timebase.
Interrupts — where the real blocker is
The self-test error strings sit immediately adjacent to the wedge we spent hours on
(file 0x178F00-0x179500; our stuck loop was 0x179C80):
LEVEL0 ERROR -> no display interrupt error - 28
LEVEL0 ERROR -> display interrupt too slow error - 28
LEVEL0 ERROR -> display interrupt too fast error - 28
LEVEL0 ERROR -> Perpetual (pending) Display Interrupt - 28
LEVEL0 ERROR -> Extraneous Interrupt 1 / 2 Occurrence - 28
LEVEL0 ERROR -> Extraneous Host Interface Interrupt Occurrence - 28
LEVEL0 ERROR -> Extraneous Non-Maskable Interrupt Occurrence - 28
LEVEL0 ERROR -> Trap Interrupt Occurrence - 28
That is level 0's interrupt test, and it is exactly where we were stuck. Probed live:
DPYCTL=E008 INTENB=0000 (interrupts NOT enabled) INTPEND=0400 (display IRQ pending)
So MAME is generating the display interrupt, but INTENB never gets enabled by the firmware, so the CPU never takes it and the test can never pass. Either the firmware is stuck before it enables interrupts, or the enable write is being lost.
INTENB (0xC0000110) write sites in the ROM: file 0x000060, 0x00077C, 0x000810,
0x000EE4 (the reset/POST bank — these are the disable at reset), plus 0x06F308,
0x09DC54, 0x09DC60, 0x09DDAC. Our wedge is at file 0x17F2xx, so the firmware has
not reached the later enable sites.
This is the item to attack next: find what the firmware is waiting for before enabling interrupts, and model it.
Watchpoint results (decisive)
harness/mame/wp.lua arms debugger watchpoints on the CPU's internal registers (the
driver cannot see those writes). Over 300 frames:
watchpoints armed on INTENB / INTPEND / DPYCTL
done (300 frames) <- NOT ONE write reported
And sampling INTENB every frame (intr.lua) shows it is never written — it is
cleared once in the reset bank and stays 0000 for the whole run:
f 1 INTENB CHANGED -> 0000 (DI=0)
f 1 INTPEND CHANGED -> 0000
f 2 INTPEND CHANGED -> 0400 <- display IRQ fires here and stays pending
distinct INTENB values: 0000 (all 600 frames)
Conclusion: the firmware is stuck before any interrupt setup. It is not failing the interrupt test — it has not reached it. Whatever it is spinning on is a board-level wait (a status bit in the undecoded windows), not the CPU's interrupt registers.
What this means for the remaining work
The bring-up has reached the boundary the original estimate predicted: to go further we
need a modelled interrupt controller / status block in the 0x5800000–0x6400000
windows, and eventually the DUARTs and the LANCE (level 0 explicitly tests the LANCE, and
level 1 tests both DUARTs, keyboard and mouse).
The difference from the original estimate is that the problem is now scoped: we know
the access surface (VT1200_BOARD_LOG gives ~18 read sites), we know the gate shapes
(handshake bits, not values), we have the right raster and clocks, and every finding is
recorded here.
Board register map — current best understanding
From the access log (VT1200_BOARD_LOG) and the disassembly. All addresses in the
harness's units (disassembly operand >> 4). Four windows, not three — 0x5900000
was only discovered when a disassembly read showed the firmware pairing it with
0x5800000.
| window | what we know |
|---|---|
05800000 |
command/data pair, low half. Written with a value; the code at 3fddcbf0 writes a bit address shifted right by 3 (bit -> byte) into it, implying a DMA-style device that uses byte addressing |
05900000 |
command/data pair, high half. Written with command numbers (0, 1, 2, 4, 0x42). Only one access seen in 400 frames, so barely used during our stall |
06200001 |
polled 6 sites; bit 0 = a ready/handshake flag (gate 1). Bit 2 tested elsewhere |
06200003 |
polled 3 sites, written with 0x11/0x8C/0xE1 in sequence |
06200000/02/08/09/0A |
written in a block by the code at file 3FA91xxx (looks like a register initialisation table) |
0640000D |
bit-mixed port: bits 4-6 = the monitor-selection switch; the rest is latched output. Bit 2 is polled after a FILL XY (gate 4) |
0640000E/F |
written 1 / 0xFF around the POST video delay routine; read at 2 sites each |
Tools that did NOT work (so they are not retried)
screen_updatefor frame dumps — only called in small clip regions; use the scanline callback.logerrorfor diagnostics — needs-log;-debuglogonly captures debugger console output.- The
historydebugger command — produces nothing in free-run (-nothrottle) mode; it needs the debugger to be actively stepping. dis34 -a(analyze) — mis-detects several code regions here as data (reports0/N words marked as code). Disassemble those linearly withdis34 -l -b <base>.
The windows share a common register layout (key structural finding)
The routine at bit 0x3FA911C0 (file 0x112238) takes the window base as an
argument and drives it generically:
3fa911f0: move *a13(0xffe0), a11, 1 ; a11 = window base, passed in
3fa91210: movi 0xca, a9
3fa91240: move a9, *a11(0x20), 0 ; write the COMMAND/DATA register
3fa91260: push 10 / calla 0x3fdce1c0 ; delay(10)
3fa912c0: addk 32, a9 ; next pattern value
3fa912e0: move a9, *a11(0x20), 0 ; and again
...
and the gate-1 poll reads *[base+0x10] (0x06200010) testing bit 0.
So each window is a same-shaped controller:
| offset | role |
|---|---|
+0x10 |
STATUS (bit 0 = ready/complete; bit 2 tested after FILL XY) |
+0x20 |
COMMAND / DATA (written in a delayed sequence: 0xCA, 0xEA, 0x3A, 0x4A, 0x5A, 0x1A, ...) |
+0xD0 |
mixed I/O: bits 4-6 = monitor switch (input), rest latched output |
That means the model is one generic register file instantiated per window, not four unrelated blocks — and it explains why the same shapes keep appearing.
Next steps
(Design work now — see the design spec once approved.)
-
Design the generic register-file model for the four windows: STATUS at
+0x10, COMMAND/DATA at+0x20, mixed I/O at+0xD0, latched outputs elsewhere. -
Decide the scope — level-0 POST only, or toward a fully working terminal (LANCE, DUARTs, video, keyboard/mouse).
-
Then the remaining items: stateful busy/done status, the scanline renderer, and confirming the firmware runs from a RAM copy of the image.
-
The
05800000/05900000command/data pair is the most promising unexplored block. A DMA-style interface (bit->byte address conversion) that the firmware barely touches during our stall — probably not the immediate blocker, but it is where LANCE / host interface traffic will live, and level 0 requires the LANCE. -
Find what the firmware waits for before enabling interrupts.
INTENBis never written at all (verified with watchpoints), so the stall is a board-level wait. -
Hunt the remaining bit-mixed ports — note
VT1200_BOARD_SWITCHnow models only the monitor switch port (0x0640000D, bits 4-6); other ports warn and are ignored. -
Build the stateful status model: board status bits are busy/done handshakes, never constants — every gate so far has needed behaviour.
-
Write the scanline renderer (needs the framebuffer layout; MAME has no generic TMS34010 renderer).
-
The RAM is full of ROM strings and ROM pointers — evidence the firmware runs from a RAM copy of the image; worth confirming.
-
Find what the firmware waits for before enabling interrupts.
INTENBnever becomes non-zero, so level 0's display-interrupt test can never pass. This is the current blocker.INTENBwrite sites: file0x000060,0x00077C,0x000810,0x000EE4(reset-bank disables) plus0x06F308,0x09DC54,0x09DC60,0x09DDAC. -
Hunt the remaining bit-mixed ports — level 0 includes an I/O data path test, and every other configuration input will have the shape "input bits mixed with latched outputs" — note
VT1200_BOARD_SWITCHnow models only the monitor switch port (0x0640000D, bits 4-6); other ports warn and are ignored. -
Build the stateful status model (gate 4, and the video / I-O-path tests): a graphics or I/O op starts -> busy bit set -> clears after N reads. Every gate so far has needed behaviour, never a constant.
-
Write the scanline renderer — the whole of the remaining video work. It needs the VT1200's framebuffer layout, which is board-specific (MAME gives no generic TMS34010 renderer). Derive it from the display-init code that writes DPYADR/PSIZE (
DPYADRsites: file0x016DF0,0x016F7A,0x0E29E6,0x1185A2). Note MAME's quirk:if (!(DPYCTL & 0x400)) dpyadr ^= 0xfffc;thenrowaddr = dpyadr >> 4— with our values that yieldsrowaddr = 0x1B0. -
Keep gating: the remaining read sites after the handshake model are
0640000D(6 PCs),0640000E,0640000F,06200001,06200003. Each needs its semantics (what the bit means), not just a value. -
Once video shows something, the terminal state is verifiable at a glance — which also makes further bring-up much faster.
Done so far: monitor switch (bits 4-6 of 0x64000D0), raster geometry
(16 px/clock, 1280x901 @ 72 Hz), CPU 50 MHz.
Address-unit note
In the harness's board windows, log address = disassembly operand >> 4
(e.g. code @64000F0h appears as 0640000F). The override/log key is the harness
address, and the override lookup is consistent with it, so this does not affect the
experiments — but it does mean the window addresses are not directly comparable to
the register operands printed by unidasm.
Board register model extracted (2026-10-16)
The ad-hoc inline board-window logic in board_read/board_write has moved into
vt1200_board.{h,cpp} — a standalone, MAME-free class that owns the four windows'
write-back latches, the per-address overrides, the monitor switch, and the busy state
machine. It is unit-tested by test_vt1200_board.cpp (harness/mame/build.sh test);
the driver now forwards its four window callbacks to the model and calls m_board.tick()
once per frame at the last visible scanline.
The busy bit now has the semantics the design spec intended: a write to +0x20 arms it,
+0x10 reports it, and it clears after VT1200_BOARD_BUSY_TICKS frame ticks (the old
inline code armed on any write and cleared after N reads). The old VT1200_BOARD_BUSY
and VT1200_BOARD_BUSY_BIT knobs are dropped — the model hardcodes the busy bit as bit 0
of the STATUS register. VT1200_BOARD_SWITCH is likewise narrowed to the monitor switch
(bits 4-6 of 0x64000D0): <value> is accepted as the raw register bits (e.g. 70) or a
position 0-7, and the input mix is applied only when the knob is supplied. A <addr> or
<mask> that does not match the monitor switch port warns (via osd_printf_info) and is
ignored instead of silently no-oping.
No regression: with no env vars the wedge is still PC=3FDCE400 samples=199, and the
handshake config (MODEL=handshake BUSY_TICKS=2 SWITCH=0640000D:70:70) still advances
to 56 distinct PCs by frame 600.
Register model (2026-10-16)
The model is now configurable, per window
VT1200_BOARD_BUSY_W2/_W3 set the busy count for 0x06200000/0x06400000
individually, and VT1200_BOARD_DMA_TICKS sets the completion time of the
0x05800000/0x05900000 DMA pair. All three are modifiers of
VT1200_BOARD_MODEL=handshake, applied after VT1200_BOARD_BUSY_TICKS so a
per-window value wins for its window; like BUSY_TICKS/SWITCH they need one
of the on-switches (MODEL/LOG/MAP/DEFAULT) to map the windows at all.
The three knobs provably reach the model — the -log line reports the parsed
values (busy_w2=3 busy_w3=4 dma_ticks=5), and a differential run shows they
change behaviour: BUSY_TICKS=0 alone → stuck at PC=3FDCE400, 5 distinct;
BUSY_TICKS=0 BUSY_W2=1 → 56 distinct at PC=3F208950; BUSY_TICKS=0 BUSY_W3=1 → still PC=3FDCE400, 5 distinct. So the knobs are wired and the
critical handshake is isolated to the 0x06200000 window: arming only WIN2
leaves gate 1, arming only WIN3 does not.
Sweep: BUSY_W2 in {1,2,4,8}
W3=2, DMA=2, MODEL=handshake, SWITCH=0640000D:70:70,
coverage.lua at 900 frames / 300-frame window:
BUSY_W2 |
total_distinct @ f900 |
frame-900 PC |
|---|---|---|
| 1 | 59 | 3F208B20 |
| 2 | 59 | 3F208B20 |
| 4 | 59 | 3F208B20 |
| 8 | 59 | 3F208B20 |
The sweep is flat. No BUSY_W2 value moves the firmware further than any
other, and none leaves the park loop. The reason is structural, not a bug:
tick() runs once per frame (last visible scanline), while the gate polls
are tight in-frame DSJS/branch spins. Any n >= 1 arms the bit for the whole
frame and clears it at the next frame boundary, so 1, 2, 4 and 8 are
indistinguishable; only 0 differs (it never arms, and the firmware stays at
the gate-1 poll). For this firmware the busy presence matters; the busy
duration is not a tunable.
The other two knobs are also inert at this stall, which rules them out as the missing input:
| config | total_distinct @ f900 |
frame-900 PC |
|---|---|---|
boundary (W2=2 W3=2 DMA=2) |
59 | 3F208B20 |
DMA=0 |
59 | 3F208B20 |
DMA=64 |
59 | 3F208B20 |
W3=0 |
59 | 3F208B20 |
Where the firmware actually sits
progress.lua at 900 frames on the boundary config (59 distinct PCs) shows the
loop: PC=3F2088B0 (414 samples, from frame 91), then 3F208A80 (138, from
f671), 3F208950 (138, f505), 3F208B20 (78, f809), 3F2089F0 (28, f574),
3F208BC0 (14, f878). That is bit 0x3F2088B0–0x3F208BC0 = file
0x1116–0x1178, in the reset/POST bank, and it is not the display/blit
loop. The access log from the same config shows exactly what that loop does:
W 0640000D pc=3F208830 val=0000
W 0640000F pc=3F208880 val=00FF
R 0640000E pc=3F208920 val=0001
R 0640000F pc=3F2089C0 val=00FF
i.e. it pokes 0x0640000D, pokes 0x0640000F, then reads 0x0640000E/F, and
spins — the POST video/refresh delay routine already identified in gate 4. New
PCs are still appearing as late as frame 878, so it is cycling, not frozen,
but over a tiny footprint and long past the point the real machine is in
level 1.
INTENB — still never non-zero
intr.lua, boundary config, 900 frames:
f 1 INTENB CHANGED -> 0000 (DI=0)
f 2 INTPEND CHANGED -> 0400
f 300 DPYCTL=E008 INTENB=0000(DI=0) INTPEND=0400 DI=1
f 900 DPYCTL=E008 INTENB=0000(DI=0) INTPEND=0400 DI=1
distinct INTENB values seen: 0000 (900 frames)
INTENB never becomes non-zero, so the level-0 boundary is not crossed:
the firmware has still not reached interrupt setup. INTPEND=0400 (display IRQ
pending) throughout confirms MAME is generating the interrupt and the firmware
simply has not enabled it. No busy window and no DMA timing changes this —
the model is still missing an input.
Which window/offset still looks unmodelled
0x5600000(byte0xAC0000) — not mapped at all. An independent sweep of the address space (reference/memory-space-map.md, row 4) found this block touched ~110 times with offsets+0x00 +0x10 +0x20 +0x30 +0x80 +0x100 +0x200 +0x380, on the display / X-server init path (writer near theno screens foundstrings). It is the most-touched region the driver does not model, and it is a prime suspect. Caveat: its writer is on the X-server init path, which this firmware has not reached (it is still in level 0 POST), so it is a plausible future blocker as much as a current one. Modelling it is explicitly out of this task's scope.0x0640000E/0x0640000F— read in the park loop, modelled only as write-back latches. The loop reads them expecting some hardware state; the model returns the last value written. That is the narrowest "missing input" at the current park point.0x0640000Dbit 2 (gate 4's "graphics/blit complete") is still not modelled — only bits 4-6 (the monitor switch) are.- Rows 7 and 10 of the memory-space map — candidate NVRAM at
0x6000000and the Ethernet MAC ROM at0x6800000— are unmapped; level 0's LANCE test and level 1's NVRAM test sit behind them.
Verdict
The configurable model is in place and the knobs are wired and effective, but
the sweep is honestly flat: the busy duration, the W3 busy count and the
DMA completion time do not move the firmware. The model is still missing an
input, and the strongest candidates are the entirely-unmapped 0x5600000 block
and the 0x0640000E/F read semantics. No claimed improvement is made this
round.