Files
vt1200/harness/mame/BRINGUP_LOG.md
T
arti 41454d5d4c harness: the stub level-1 run continues past both DUART failures; correct the "fatal/LED loop" claim
The stub run does not park at HOST PORT - 15. Both DUART failures are reported,
the board LED blinks once (finite L3F208660), TYPE 'GO' TO CONTINUE is printed
(no input is read), and the level-1 runner continues ('CONTINUING THE TESTS').
The runner never returns to 3F204440, so 3F208060/3F208650 are never executed.

The run then wedges at frame ~2839 in a DUART1 counter-ready interrupt storm:
the ISR polls DUART1 reg 5 (bit 0x06400050) and always reads 0x08; the counter
runs in timer mode (ACR=0x60) with CTUR/CTLR never programmed, so MAME re-arms
at 1 tick and the interrupt re-asserts every ~229 CPU cycles. Frames at 3500 /
6000 / 12000 are byte-identical; RAM differs by one byte.

Corrects README.md and annotates GATE_CLEANUP_REPORT.md, BRINGUP_LOG.md and
DUART_REPORT.md. Adds a console+PC/milestone instrument and the finding report.
2026-10-16 14:56:47 +03:00

1040 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 four 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 (guessed; unverified); 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:
1. **A pure `DSJS` delay** (gate 2) — passes with time, needs nothing.
2. **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:
```cpp
if (params.enabled)
m_scanline_ind16_cb(screen, bitmap, cliprect.min_y, &params); // 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)
1. **A disabled display silences the scanline callback.** `tms340x0_ind16` skips
the callback when the display is off (DPYCTL bit 15 clear); without the callback
`tick()` would not run and the busy machines would not clear (and
`VT1200_DUMP_FRAME` would write nothing). With the display enabled the callback
*does* fire under `-video none`, and `VT1200_DUMP_FRAME` does write a PPM — so
`-video none` alone is not the trap. Force rendering with `-aviwrite` only if you
also need `screen_update`.
2. **`logerror` needs `-log`** (writes `error.log`); `-debuglog` only captures
*debugger console* output (e.g. the `printf` inside a `bpset` action).
3. **Put frame/RAM dumps in the *scanline* callback, not `screen_update`.** MAME
calls `screen_update` with 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 on `scanline == 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**.
> **CORRECTION (2026-10-16).** The 256 KiB figure above is **stale** — it is from an
> early dump, and the inference built on it ("the driver only models 256 KiB") is
> **wrong**. The driver today maps `map(0x00000000, 0x01ffffff)`; on this space at
> 8 address units per byte that is `0x2000000` units = **0x400000 bytes = 4 MiB**,
> confirmed empirically (`machine.memory.shares[':mainram'].size == 4194304`). The
> old range was `0x1fffff` (256 KiB) — one extra digit. So the mapping is already
> 4 MiB and does **not** need enlarging. This stale line misled a reader once; keep
> the correction with it. (The separate question of what the firmware *concludes*
> about memory — the SIMM presence bits at `@0x5000000` — is unaffected.)
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 `0x6400000` window) 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:
```cpp
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_update` for frame dumps — only called in small clip regions; use the **scanline** callback.
* `logerror` for diagnostics — needs `-log`; `-debuglog` only captures debugger console output.
* The `history` debugger 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 (reports
`0/N words marked as code`). Disassemble those **linearly** with `dis34 -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.
## Video memory FOUND — `0x00400000` (2026-10-16)
Found empirically, not by static sweep. A new catch-all mapping
(`VT1200_LOG_UNMAPPED=<path>`) logs every access to memory the driver does **not**
model. With it, the firmware's unmapped traffic collapses to exactly one region:
```
range: 0x00400000 .. 0x0040D7FF (map units; 1 unit = 1 bit, 8 per byte)
writers: 117,002 W from pc=3F204A20 (file 0x944 -- the POST blit, right after FILL XY)
readers: 6,409 R from pc=3FDF9A00 (file 0x17F340 -- the display/blit code,
reading at an 8-byte stride)
also: pc=3FDF97A0 (file 0x17F2F4)
```
That is the region the spike independently ranked as its VRAM candidate
(`MOVI 0x4000000,B4` in video init). Two independent methods agree.
**Why the screen is black:** we never mapped it, so the blitter's writes vanished and its
reads returned zero.
**Now mapped** under `VT1200_VRAM=1` (`map(0x00400000, 0x0047ffff).ram().share("vram")`,
i.e. 512 K bits = 64 KiB of video memory, gated so the no-env baseline is untouched), with
`VT1200_DUMP_VRAM=<frame>` to dump it.
**Dumped at frames 5, 30, 120 and 400: all zero.** That is *consistent*, not a bug — the
writes are clearing/testing video RAM (a memory test writes and reads back), and the
firmware has not yet drawn anything because it is stuck early. So the blitter region is
identified, but **no picture can appear until the firmware gets far enough to draw one.**
### Method notes worth keeping
- `map()` entries are built at machine-**configuration** time, so a mapping gated on a
pointer initialised in `machine_start()` is never registered. Gate on `getenv()`.
- The register-first catch-all works: real regions added after it take precedence, which
is why the firmware still boots unchanged while the catch-all is installed.
- Map units are **bits** (8 per byte) — confirmed by a 0x80000-unit `.ram()` reporting
`bytes() == 65536`.
## WHAT THE FIRMWARE IS ACTUALLY DOING (2026-10-16) — the key diagnostic
We can now read the firmware's own console output without seeing the screen. The printer
routine entry is at bit `0x3FDE00C0`, and every call site loads the message pointer into
`a9` immediately before `calla` — so a breakpoint at that entry logging `a9` yields the
transcript. `harness/mame/cap.lua` does this.
Captured:
```
'LEVEL1/2 TESTING ** '
'LEVEL1/2 ERROR -> VIDEO MEMORY - 22 '
```
### What this means (this reframes the whole bring-up)
1. **The firmware has already PASSED level 0.** It is in level 1/2 testing, so the level-0
interrupt test passed. `INTENB` staying `0000` is therefore **not** a level-0 failure.
2. **It fails the level-1 VIDEO MEMORY test** (error 22 — the service guide's level-1
"Video RAM test") and then **restarts**.
3. The restart is explained: the stall loop entry `0x3F208060` begins with **`DINT`** —
interrupts disabled — and resets `SP` to `0x53E0`. It is called from **8 sites**, all in
the reset/POST bank (file `0x0568`, `0x05C0`, `0x0970`, `0x0C86`, `0x0E76`, `0x0ED2`,
`0x0F48`, `0x1008`). So the firmware loops: run a test -> fail -> restart with
interrupts off -> run again.
> **CORRECTION (2026-10-16, later).** Point 3 is not where the stub level-1 run
> ends. Breakpoints on `3F208060`/`3F208650` fire **0** times in 20 000 frames,
> and the level-1 runner never returns to the boot loop's `3F204440 jane
> L3F208060`. The DUART failures are reported, the finite `L3F208660` blink runs,
> `TYPE 'GO' TO CONTINUE` is printed (no input is read), and the runner
> **continues** (`CONTINUING THE TESTS`), then wedges in a **DUART1 counter-ready
> interrupt storm** from frame ~2839. See `LEVEL1_CONTINUE_REPORT.md`. The
> "restart" reading above came from a 3000-frame window that ended during the
> LED blink.
**The causal chain is now complete and consistent:**
> video memory not modelled -> the level-1 video RAM test fails -> error 22 is reported ->
> the firmware restarts with interrupts disabled -> it never reaches the point where it
> draws -> the screen is black, and no renderer can help.
So the fix is **not** the renderer: it is to model video memory well enough that the
level-1 video RAM test passes. That is the next task.
### Resolving the message pointers
Pointers are bit addresses; the image is byte-based at `0x07E40000`, so file offset is
`(P >> 3) - 0x07E40000`.
```
a9=0x3FDCA8E8 -> file 0x17951D : 'LEVEL1/2 TESTING ** '
a9=0x3FDC98F8 -> file 0x17931F : 'LEVEL1/2 ERROR -> VIDEO MEMORY - 22 '
```
The other captured `a9` values (`0x00C0000C`, `0x0100000C`, `0x0140000C`, `0x0180000C`,
`0x01C0000C`) are not ROM pointers — they step by `0x400000` and look like addresses the
video RAM test walks. Worth following up when modelling VRAM: the test appears to walk
memory in `0x400000`-sized steps.
### Method note
This is the single most useful diagnostic we have built: **the firmware narrates its own
self-test through `0x3FDE00C0`.** Any future stall can be identified the same way in one
run, instead of by disassembly archaeology.
## BLOCKER: address-unit confusion + headless debugger limits (2026-10-16)
Two things are now blocking progress, and both are instrumentation problems rather than
missing knowledge. Fix these before further register work.
### 1. Watchpoints DO NOT FIRE in this headless configuration
`harness/mame/cal.lua` watches **three** encodings of the same known-hit address — the
driver-map address `0x0620000D` (which `VT1200_BOARD_LOG` *proves* the firmware hits), the
firmware operand `0x6200010`, and a shifted variant `0x00620000`. Over 200 frames:
```
not a single hit
```
Since the first of those is demonstrably accessed, the watchpoint itself is not firing:
**`wpset` is a silent no-op when the debugger has no UI**, exactly like the `history`
command. `bpset` *does* work (the console capture at `0x3FDE00C0` fires reliably).
**Consequence, and it is a correction:** `wp.lua`'s earlier claim that "nothing writes
INTENB/INTPEND/DPYCTL" rested on watchpoints that were never firing. The *frame-sampled*
part of that conclusion stands (`intr.lua` polls INTENB every frame and sees only `0000`),
but the watchpoint evidence must be treated as void.
**For memory observation, instrument the DRIVER, not the debugger.** `VT1200_BOARD_LOG`,
`VT1200_LOG_UNMAPPED` and the dump functions are all driver-side and all work.
### 2. Four address conventions, still not reconciled
| # | convention | example |
|---|---|---|
| 1 | firmware operands (bit addresses, as disassembly prints) | `@0x64000D0` |
| 2 | the driver's address-map units (`BOARD_BASE` + offset) | `0x0640000D` |
| 3 | the catch-all log's addresses | `0x400000`..`0x40D7FF` |
| 4 | the debugger's units | unusable headless |
The unresolved contradiction: the catch-all logged `0x400000..0x40D7FF` as **unmapped**,
yet the main-RAM map (`0x00000000..0x01FFFFFF`, 4 MiB) contains those addresses. Either the
catch-all shadows RAM (but then the firmware would be running on zeros and would not reach
level 1), or its `off` is not the space address. **The video-memory conclusion is therefore
provisional until this is settled.**
Concrete calibration plan (uses only driver-side logging, which works):
1. Add a logging mapping over a slice of RAM (`0x00000000..0x0000FFFF`) registered AFTER
the RAM entry. If it logs, map precedence is last-wins and handlers can be layered; if
not, first-wins.
2. Add the same over each candidate encoding of the video region and see which one logs the
blitter's traffic.
3. That single run fixes the units and the precedence, and every address in this document
can then be normalised.
### 2. Four address conventions, still not reconciled — and two instruments that DISAGREE
**Proven this session:** map precedence is **last-wins**. A logging proxy over
`0x00000000-0x0000FFFF` registered *after* the RAM entry fires (4458 hits, then 22734 over a
longer run), while proxies registered over other ranges do not. So drivers can layer
handlers over real regions — that part is settled and useful.
**The contradiction that blocks everything else:**
| instrument | range | result |
|---|---|---|
| catch-all (registered FIRST, shadowed by RAM under last-wins) | logged `0x400000`-`0x40D7FF` | claims those are **UNMAPPED** |
| layered proxy, registered LAST | `0x00400000`-`0x00400FFF` | **zero hits** |
| layered proxy, registered LAST | `0x00000000`-`0x0000FFFF` | **fires** |
| layered proxy, registered LAST | `0x00200000`-`0x01FFFFFF` | **zero hits** |
The catch-all says `0x400000` is unmapped; but the RAM map covers `0x00000000-0x01FFFFFF`,
and under last-wins RAM must win there — so the catch-all should never have seen it. And a
proxy over exactly `0x00400000` sees nothing.
**Therefore the catch-all's logged addresses are not address-map addresses**, and the
"video memory at 0x00400000" conclusion is **unreliable**. It may still be right, but it has
not been established by a trustworthy instrument.
The blitter's target read straight from the POST code is `0x3600400` (a bit address), which
matches none of the candidate encodings either.
**Until this is closed, no video-memory work can be trusted.** The next step is one *closed*
cross-check: run the catch-all and a layered proxy over the **same** range in the **same**
run, and see which one reports. Whichever is trustworthy calibrates the rest; the other gets
fixed or discarded.
## ADDRESS UNITS RESOLVED — the map is in BITS (2026-10-16)
Authoritative, from MAME's own source rather than inference:
```
src/devices/cpu/tms34010/tms34010.h:991
memory_access<32, 1, 3, ENDIANNESS_LITTLE>::specific m_program;
^ data_width = 1 BIT
src/emu/emumem.h:2078
inline offs_t addr2byte(offs_t address) const {
return (m_addr_shift < 0) ? (address << -m_addr_shift) : (address >> m_addr_shift);
}
```
* The TMS34010 program space declares **`data_width = 1`**, so **address-map units are
BITS**, not bytes and not words.
* `addr2byte(addr) = addr >> 3` → **bytes = bit address / 8**.
* This reconciles the measurement that a `.ram()` over `0x00000000-0x01FFFFFF` reports
`bytes() == 0x400000`: `0x2000000 >> 3 == 0x400000` (4 MiB). The units were consistent
all along; only my interpretation of them was not.
**Read every address in this file, in the driver's map, and in the logs as a BIT address,
and divide by 8 for bytes.** So the video-region traffic the catch-all reported at
`0x400000`-`0x40D7FF` means bytes `0x80000`-`0x81AFF` — a **64 KiB** region, which is a
plausible VRAM size and the first time that number has rested on something solid.
### Two corrections this forced
1. **`DETECT_OVERLAPPING_MEMORY` is `(0)`** (`src/emu/addrmap.cpp:20`). MAME silently
permits overlapping map entries and never warns. An earlier step in this project used
"no overlap warning appeared" as evidence that two entries did not overlap — **that
evidence was void.**
2. **Handlers receive the offset within their own entry.** `populate_map_entry` installs
entries in list order (`emumem_mview.cpp:478-481`) and MAME subtracts the entry's start,
so a start-`0` entry (RAM, the catch-all, the calibration proxies) sees the absolute
address, while the board windows (start `0x06200000`) see `address - 0x06200000`.
### Still open — and now sharply posed
With bit units, RAM genuinely covers bit `0x400000`, so the catch-all should never have seen
traffic there; yet it logged 123,411 accesses. The precedence rule must be subtler than
"later wins". MAME has a distinct path, `populate_mismatched(...)`, for handler-tree
conflicts (`emumem_mview.cpp:257` and `:293`) — the open question is whether resolution
differs when the overlapping handlers have the **same** width versus **different** widths.
A dedicated cross-check is measuring exactly that.
## Video memory: located, mapped, and still not enough (2026-10-16, latest)
**Corrected location.** The cross-check settled the instrument rule — a handler on an entry
at `base` receives `(bitaddr - base) >> 4`, so the catch-all (base 0) logs `bitaddr >> 4`
(2 bytes per logged unit). Therefore the blitter's traffic logged at
`0x400000`-`0x40D7FF` is **bit `0x04000000`-`0x040D7FF0` = bytes `0x800000`-`0x81AFFE`**
(~110 KiB). My earlier `0x00400000` mapping was **16x too low** and is fixed.
The proof was a closed cross-check: adding a probe at `0x04000000` moved *exactly*
183,536 accesses out of the catch-all, while a probe at the old wrong address stayed at
zero. Precedence is plain last-wins for both same- and mismatched-width overlaps.
**The firmware's failing address agrees.** Reading `a11` at the printer breakpoint gives the
address the failing test reports: `0x0040000C` in the same `>>4` units, i.e.
**bit `0x040000C0`** — inside the region above. `harness/mame/cap.lua` now logs `a9`, `a11`
and `a5`, so every reported failure comes with its address.
**But mapping it is not sufficient.** With video memory mapped at the corrected address —
and also with a deliberately huge range (`0x04000000`-`0x1fffffff`) — the level-1 video RAM
test **still reports `LEVEL1/2 ERROR -> VIDEO MEMORY - 22` at the same address**. So the
test is not a plain write/read-back over memory that simply needs to exist.
**Where the answer is.** The test walks banks and calls a helper at bit **`0x3FDDFCB0`**
(file `0x17BF96`) for each — that helper implements the actual check. Reading it is the next
step. The bank walk observed so far steps in `0x400000` units (`>>4`), reaching at least
`0x0200000C` (bit `0x200000C0`), so video memory may extend far beyond the first 110 KiB.
**Honest status:** level 1 is NOT passing. Video memory is located and mapped correctly; the
test's *requirement* is not yet understood.
## The video test DRAWS, then verifies (2026-10-16)
Reading the helper the test calls per bank — bit `0x3FDDFCB0`, file `0x17BF96` — shows it is
**not** the check. It is the display:
```
3fddfd60: ... loop building 8 hex digits from a9 ... ; format a9 as hex
3fddff40: movk 7,a11 ; 8 characters
3fddff50: movb *a5,a8 ; push char ; push a10 ; push 0xffff
3fde0000: calla 0x3fdf97f0 ; plot the character at a10
3fde0030: addk 12,a10 ; next cell, +12
```
It formats the address in hex and **draws it on the screen**, 12 units per character, via the
character plotter at `0x3fdf97f0`. That also explains the read PCs the catch-all saw inside
the video region (`3FDF97A0`, `3FDF9A00` — the drawing code reading VRAM).
**Consequence for the diagnosis:** the level-1 "video RAM test" **draws characters and then
verifies them**; it is not a raw memory walk. So the fact that mapping the memory does not
make it pass is *expected* — the check is on the drawing path, which depends on the character
generator, not merely on memory existing.
**Next:** find the verification — where the test compares what it drew against what it reads
back. That comparison is what fails and reports error 22 at bit `0x040000C0`.
## The video RAM test characterised — and it still fails (2026-10-16, latest)
A transparent, word-oriented logging proxy over the start of video memory
(`VT1200_VRAMLOG=<path>`) reveals exactly what the test does:
```
W 0000 0001 pc=3F201BB0 <- walking-1 pattern: 0x0001 0x0002 0x0004 0x0008 0x0010 ...
R 0000 0001 pc=3F201C20 <- read back, same values
```
So the level-1 video RAM test is a **classic data-bus walking-bit test**: 8,208 pattern
writes, 8,208 read-backs, across 4,096 offsets — write the pattern, read it back, compare.
That also explains the other traffic: `pc=3F204A20` writes 16,384 zeros (the POST clear of
video memory) and `pc=3FDF97A0` writes 8,192 — the character-drawing path.
**But the test still fails.** With video memory mapped as `.ram()`, and again with the
word-oriented transparent proxy serving it — both of which round-trip the pattern correctly —
the firmware still reports `LEVEL1/2 ERROR -> VIDEO MEMORY - 22` with `a11=0x0040000C`.
So the memory now **round-trips correctly and the test still fails**, which rules out the
simplest explanations (absent memory, wrong address, wrong size, bit-vs-word storage). The
remaining candidates:
* the test walks banks up to `a11=0x0200000C` (bit `0x200000C0`), which is **outside** the
mapped range `0x04000000`-`0x1fffffff` — the last bank would be unmapped;
* the reported `0x0040000C` may be a bank base in a unit I have not pinned, rather than the
offset that failed;
* the test may require the *drawn* content (the character path) to verify, not only raw
pattern round-tripping.
**Honest status: level 1 is NOT passing.** Video memory is located, correctly mapped and now
proven to round-trip a walking-bit pattern; the test's remaining requirement is not yet met.
## Error strings come from a STATUS-CODE DISPATCHER, not from addresses (2026-10-16)
Following the level-1 error path in Ghidra corrects a wrong turn I had taken.
The block immediately before the error handlers is a switch on a status code:
```
07fbe0c6: MOVE *A13(0xffe0),A8,0x1 ; A8 = status code set by the test
07fbe0ca: JREQ 0x07fbdfd0 ; code 0 -> (no error)
07fbe0cc: DEC A8 / JREQ 0x07fbce12 ; codes 1..8 -> ROM CHECKSUM handler
07fbe0fc: SUBI -0xff88,A8 / JREQ 0x07fbcf40
07fbe104: SUBI -0xff80,A8 / JREQ 0x07fbcfe0 ; <- VIDEO MEMORY handler
07fbe10c: SUBI -0xff00,A8 / JREQ 0x07fbd02a
...
```
So each error string is selected by a **code**, and the values printed alongside (e.g.
`0x40000C`, `0x4000C0`) are **constants in the message**, not addresses.
**Correction.** My earlier claim that "the video RAM test fails at bit `0x040000C0`" rested on
reading `a11` at the printer breakpoint. `a11` is a stale local, and `0x40000C` turned out to
be a printed constant that also appears in the ROM-checksum path. **That inference was
unsound and is withdrawn.** The video RAM test's failing address is not known.
**Useful landmarks now mapped (Ghidra addresses):**
| what | address |
|---|---|
| the level-1 test routine | `0x07FBCB02`-`0x07FBE21B` |
| status-code dispatcher | `0x07FBE0C6` |
| VIDEO MEMORY error handler | `0x07FBCFE0` (reached only for its one code) |
| ROM CHECKSUM error handler | `0x07FBCE12` |
| hex-format + draw helper | `0x07FBBF96`-`0x07FBC015` |
**Method note, worth keeping.** A register snapshot at a call is not evidence of an argument.
Read the **call site in the ROM** — which is what Ghidra just made easy, and what I should
have done before chasing an address for several rounds. Ghidra and `smart34010` agree
instruction-for-instruction on every range checked, so the two disassemblers cross-validate.
**Next:** find where the test **sets** `*A13(0xffe0)` to the video-memory code — that
assignment is the actual failing comparison, and it is the thing to fix.
## Ghidra vs smart34010: which tool to trust for what (2026-10-16)
Both tools disassemble this ROM, and they **agree instruction-for-instruction on every range
checked**. But they differ in one way that matters, found while following the level-1 test:
| tool | the same `CALLA` instruction |
|---|---|
| `tools/smart34010/dis34` | `calla 0x3fde00c0` — full bit address, matches the ROM layout |
| Ghidra (MCP) | `CALLA 0x000bc018` — does not resolve to any function |
Ghidra's **absolute operands are correct** (`MOVI 0x3fdca990` is a genuine bit address), so
it is reliable for mnemonic structure, `MOVI` constants, xrefs on data, and decompilation.
Its **`CALLA` targets do not resolve**, so:
* Ghidra's **call graph is empty** for this ROM — `function_callers`/`function_callees`
return nothing for functions that clearly call others (`FUN_rom_07fbd000` reports 0 and 0);
* use **`smart34010` for call targets**, and Ghidra for structure and decompilation.
Do not build conclusions on Ghidra's call graph here until that is fixed; the earlier
"0 callers" results are artefacts of this, not evidence of unreachable code.
### Structural result from this pass
The level-1 error dispatcher is owned by `FUN_rom_07fbd000` (body `0x07FBD000`-`0x07FBE201`),
and it takes the failing **status code as its argument** (`*A13(0xffe0)` — there is exactly
one `0xffe0` reference in the routine, the read in the dispatcher, so the code is not set
locally). The failing code is therefore chosen by **whatever calls this routine** — and that
caller is what still needs finding.
## MILESTONE: the level-1 VIDEO MEMORY test now PASSES (2026-10-16)
The code-22 investigation (`harness/mame/CODE22_REPORT.md`) found exactly what the test
requires, and two changes satisfy it. **The firmware has moved on to the next level-1 test.**
### What raises code 22 (in the firmware's own terms)
```
level-1 video-memory self-test at bit 0x3F2018B0 (base 0x04000000, count 0x20000)
returns non-zero -> L3FA7C050 -> code 0x100 stored at 0x3FA7DE30
-> dispatcher 0x3FDF0630 -> the VIDEO MEMORY - 22 handler
```
Its failing condition depended on configuration, which turned out to be two separate bugs:
1. **Default driver:** the 16-bit walking-bit read-back at `0x04000000` returns 0 because
**video RAM is unmapped** -> fixed by mapping it (`VT1200_VRAM=1`, base bit `0x04000000`).
2. **With video RAM mapped:** the pattern walk then *passes*, and the failure moved to the
**REFCNT check at `0x3F201ED0`** — `0xC00001F0` returned the **same value twice**, which
the firmware treats as an error (`a8=0x0E`).
### The REFCNT fix (a local MAME core change — revert note included)
MAME modelled the refresh counter too coarsely:
```cpp
case REG_REFCNT:
return (total_cycles() / 16) & 0xfffc; // changes only every 64 cycles
```
The test reads REFCNT twice a few instructions apart and requires the value to have moved.
Changed in `/home/arti/code/k-space/mame0289/src/devices/cpu/tms34010/tms34010.cpp` to
`return total_cycles() & 0xffff;`, with the original line preserved in a comment directly
above it for reverting. **This is a local change to the emulator, not to anything DEC made** —
the ROM is untouched, and the change makes the emulator's refresh counter behave the way the
firmware is probing for.
### Result
With video RAM mapped and REFCNT advancing:
```
error-22 prints: 0 <- the video-memory test PASSES
next failure: 'LEVEL1/2 ERROR -> HOST PORT - 15 '
```
### Where it stands now
The level-1 sequence has advanced to the **HOST PORT** test — DUART1 channel A, per the
service guide's "Host port test". The DUARTs are not modelled at all, so that is the next
gate. The shape of the work is now established and repeatable: identify the test, find the
condition it fails on, model it, re-run the console capture.
---
## Next steps
1. Model the host DUART (DUART1 channel A) enough for the level-1 HOST PORT test
(bit 0x3F...); the DUARTs are unmodelled and are the current gate.
2. Keep using cap.lua to read which test fails next -- the loop is now repeatable.
## Next gate: level-1 HOST PORT (DUART1 channel A) — evidence so far
With video memory mapped and REFCNT advancing, the level-1 sequence advanced to:
```
'LEVEL1/2 ERROR -> HOST PORT - 15 ' (error-string file 0x17938D, handler 0x07FBD3C2)
```
This is DUART1 channel A per the service guide's level-1 list — a local-loopback test on the
host serial port. The DUARTs are **not modelled**, so it fails.
**Evidence** (`VT1200_BOARD_LOG`, 700 frames, with the video-memory fixes in place) — the
most-touched unmodelled register during that window:
```
31 W 06400002 <- by far the hottest; a DUART register being written repeatedly
13 W 0620000A 13 W 06200002
12 W 0640000A 6 R 0640000D 6 R 06200001
```
So window **`0x6400000`** (the driver's address units; offset `0x02`) is where the host-port
test lives. A loopback test writes the transmit register and reads the receive register, so
the next step is to find which offsets it reads back and give the window DUART-like
behaviour.
**Preferred approach:** wire MAME's `SCN2681`/`MC68681` device at that window (it is the
right part, and MAME has it), rather than hand-rolling a loopback. Fallback: a stateful
transparent register file in the driver with write-to-TX appearing at RX.
**Context for scheduling:** level 1 is 15 tests. One now passes (video memory). The method is
settled and repeatable — identify the test from the console, find the condition, model it,
re-run — but each remaining gate (DUARTs, keyboard, mouse, NVRAM, LANCE, video RAM) needs a
modelled device. Level 0 already passes.