harness: record headless-watchpoint void + the unresolved address-unit blocker, with a calibration plan

This commit is contained in:
SDD committed 2026-10-16 10:07:38 +03:00
1 parent ccd03b8879
commit 7030f2068d
4 files changed
+136 -281

No files matched your search

+6 -17
View File
@@ -1,19 +1,8 @@
MAME debugger version 0.289 (mame0289-dirty)
Currently targeting vt1200 (VT1200 X terminal (v2.2 firmware))
Breakpoint 1 set
PRINT pc=3FDE00C0 a9=3FDCA8E8
PRINT pc=3FDE00C0 a9=3FDC98F8
PRINT pc=3FDE00C0 a9=00C0000C
PRINT pc=3FDE00C0 a9=0100000C
PRINT pc=3FDE00C0 a9=0140000C
PRINT pc=3FDE00C0 a9=0180000C
PRINT pc=3FDE00C0 a9=01C0000C
PRINT pc=3FDE00C0 a9=00000000
Error in expression: printf "CAL-A wpaddr=%08X data=%04X acc=%02X pc=%08X\n", wpaddr, wpdata, wpaccess, pc ; g
^unknown symbol
Error in expression: printf "CAL-B wpaddr=%08X data=%04X acc=%02X pc=%08X\n", wpaddr, wpdata, wpaccess, pc ; g
^unknown symbol
Error in expression: printf "CAL-C wpaddr=%08X data=%04X acc=%02X pc=%08X\n", wpaddr, wpdata, wpaccess, pc ; g
^unknown symbol
+53 -264
View File
@@ -637,271 +637,60 @@ This is the single most useful diagnostic we have built: **the firmware narrates
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.
---
## Next steps
(Design work — see the design spec.)
1. **Getting a picture requires unblocking the firmware.** Video memory is located
(`0x00400000`) and mapped, and the blitter's region is known, but the firmware only
*clears/tests* it — it never draws, because it is still stuck early. The rendering
pipeline is the least of it; the blocker is the same one as before: `INTENB` never
leaves zero.
2. **Strongest known unmodelled region:** `0x5600000` (~110 static accesses, display/X-init
path, unreached while stuck).
3. **Write the scanline renderer** — it must read video memory rather than draw nothing.
Note the display's `DPYADR` gives `rowaddr = 0x1B0` under MAME's quirk, and the
framebuffer geometry needs confirming against `0x00400000`.
4. Then the two remaining follow-on plans: video RAM + renderer, and DUART / NVRAM / MAC ROM.
1. **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.
2. **Decide the scope** — level-0 POST only, or toward a fully working terminal
(LANCE, DUARTs, video, keyboard/mouse).
3. Then the remaining items: stateful busy/done status, the scanline renderer, and
confirming the firmware runs from a RAM copy of the image.
1. **The `05800000`/`05900000` command/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.
2. **Find what the firmware waits for before enabling interrupts.** `INTENB` is never
written at all (verified with watchpoints), so the stall is a board-level wait.
3. **Hunt the remaining bit-mixed ports** — note `VT1200_BOARD_SWITCH` now models only
the monitor switch port (`0x0640000D`, bits 4-6); other ports warn and are ignored.
4. **Build the stateful status model**: board status bits are busy/done handshakes, never
constants — every gate so far has needed behaviour.
5. **Write the scanline renderer** (needs the framebuffer layout; MAME has no generic
TMS34010 renderer).
6. **The RAM is full of ROM strings and ROM pointers** — evidence the firmware runs from
a RAM copy of the image; worth confirming.
1. **Find what the firmware waits for *before* enabling interrupts.** `INTENB` never
becomes non-zero, so level 0's display-interrupt test can never pass. This is the
current blocker. `INTENB` write sites: file `0x000060`, `0x00077C`, `0x000810`,
`0x000EE4` (reset-bank disables) plus `0x06F308`, `0x09DC54`, `0x09DC60`, `0x09DDAC`.
2. **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_SWITCH` now models only the monitor switch port
(`0x0640000D`, bits 4-6); other ports warn and are ignored.
3. **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.
4. **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
(`DPYADR` sites: file `0x016DF0`, `0x016F7A`, `0x0E29E6`, `0x1185A2`).
Note MAME's quirk: `if (!(DPYCTL & 0x400)) dpyadr ^= 0xfffc;` then
`rowaddr = dpyadr >> 4` — with our values that yields `rowaddr = 0x1B0`.
5. **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.
6. 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.
Raw runs, verbatim:
```
$ VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=0 \
$ VT1200_BOARD_SWITCH="0640000D:70:70" \
$ COV_FRAMES=600 COV_WINDOW=300 timeout 600 $M vt1200 -rompath harness/mame/roms -video none \
$ -sound none -nothrottle -debug -debugger none -autoboot_script harness/mame/coverage.lua -autoboot_delay 0 2>&1 \
$ | grep -E "^frame" | tail -1
BUSY_TICKS=0 -> frame 600 new= 2 total_distinct= 5 PC=3FDCE400
$ VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=0 VT1200_BOARD_BUSY_W2=1 \
$ VT1200_BOARD_SWITCH="0640000D:70:70" \
$ COV_FRAMES=600 COV_WINDOW=300 timeout 600 $M vt1200 -rompath harness/mame/roms -video none \
$ -sound none -nothrottle -debug -debugger none -autoboot_script harness/mame/coverage.lua -autoboot_delay 0 2>&1 \
$ | grep -E "^frame" | tail -1
BUSY_TICKS=0 BUSY_W2=1 -> frame 600 new= 2 total_distinct= 56 PC=3F208950
$ VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=0 VT1200_BOARD_BUSY_W3=1 \
$ VT1200_BOARD_SWITCH="0640000D:70:70" \
$ COV_FRAMES=600 COV_WINDOW=300 timeout 600 $M vt1200 -rompath harness/mame/roms -video none \
$ -sound none -nothrottle -debug -debugger none -autoboot_script harness/mame/coverage.lua -autoboot_delay 0 2>&1 \
$ | grep -E "^frame" | tail -1
BUSY_TICKS=0 BUSY_W3=1 -> frame 600 new= 2 total_distinct= 5 PC=3FDCE400
```
### 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` |
Raw run, verbatim (cwd `harness/mame`-repo root,
`M=/home/arti/code/k-space/mame0289/vt1200`):
```
$ for N in 1 2 4 8; do
$ printf "W2=%s -> " "$N"
$ VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_W2=$N VT1200_BOARD_BUSY_W3=2 \
$ VT1200_BOARD_DMA_TICKS=2 VT1200_BOARD_SWITCH="0640000D:70:70" \
$ COV_FRAMES=900 COV_WINDOW=300 timeout 600 $M vt1200 -rompath harness/mame/roms -video none \
$ -sound none -nothrottle -debug -debugger none -autoboot_script harness/mame/coverage.lua -autoboot_delay 0 2>&1 \
$ | grep -E "^frame" | tail -1
$ done
W2=1 -> frame 900 new= 3 total_distinct= 59 PC=3F208B20
W2=2 -> frame 900 new= 3 total_distinct= 59 PC=3F208B20
W2=4 -> frame 900 new= 3 total_distinct= 59 PC=3F208B20
W2=8 -> frame 900 new= 3 total_distinct= 59 PC=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, verbatim:
```
$ VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_W2=2 VT1200_BOARD_BUSY_W3=2 \
$ VT1200_BOARD_DMA_TICKS=2 VT1200_BOARD_SWITCH="0640000D:70:70" \
$ INT_FRAMES=900 INT_EVERY=300 timeout 600 $M vt1200 -rompath harness/mame/roms -video none \
$ -sound none -nothrottle -debug -debugger none -autoboot_script harness/mame/intr.lua -autoboot_delay 0 2>&1 \
$ | grep -E "CHANGED|distinct|INTENB="
f 1 INTENB CHANGED -> 0000 (DI=0)
f 1 INTPEND CHANGED -> 0000
f 2 INTPEND CHANGED -> 0400
f 300 DPYCTL=E008 INTENB=0000(DI=0) INTPEND=0400 DI=1 DPYINT=0000 VCOUNT=0383 VTOTAL=0384 HTOTAL=004F HSBLNK=004C
f 600 DPYCTL=E008 INTENB=0000(DI=0) INTPEND=0400 DI=1 DPYINT=0000 VCOUNT=0383 VTOTAL=0384 HTOTAL=004F HSBLNK=004C
f 900 DPYCTL=E008 INTENB=0000(DI=0) INTPEND=0400 DI=1 DPYINT=0000 VCOUNT=0383 VTOTAL=0384 HTOTAL=004F HSBLNK=004C
distinct INTENB values seen:
INTENB=0000 DI=0 (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
1. **`0x5600000` (byte `0xAC0000`) — 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
the `no screens found` strings). 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.
2. **`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.
3. **`0x0640000D` bit 2** (gate 4's "graphics/blit complete") is still not
modelled — only bits 4-6 (the monitor switch) are.
4. Rows 7 and 10 of the memory-space map — candidate NVRAM at `0x6000000` and
the Ethernet MAC ROM at `0x6800000` — 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.
FIX THE INSTRUMENTATION FIRST (see above), then model video memory.
+42
View File
@@ -0,0 +1,42 @@
-- CALIBRATION: settle the address-unit conventions once and for all.
--
-- Four different conventions have been in play and I keep tripping on them:
-- 1. the firmware's own operands (bit addresses, e.g. @0x64000D0)
-- 2. the driver's address-map units (BOARD_BASE 0x06400000 + offset)
-- 3. the catch-all log's addresses (it printed 0x400000..0x40D7FF)
-- 4. the debugger's wpset/bpset units
--
-- The access log PROVES the firmware hits the window at driver address
-- 0x0620000D. So: watch that, and also the firmware's operand for it, and see
-- which one fires and what wpaddr reports. Whatever fires calibrates the rest.
local machine = manager.machine
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
if not dbg then
print("ERROR: debugger not enabled; pass -debug")
machine:exit()
return
end
-- A: the driver-map address (proven hit by VT1200_BOARD_LOG)
dbg:command('wpset 0620000d,10,rw,{ printf "CAL-A wpaddr=%08X data=%04X acc=%02X pc=%08X\\n", wpaddr, wpdata, wpaccess, pc ; g }')
-- B: the firmware operand (bit address as printed in disassembly)
dbg:command('wpset 6200010,10,rw,{ printf "CAL-B wpaddr=%08X data=%04X acc=%02X pc=%08X\\n", wpaddr, wpdata, wpaccess, pc ; g }')
-- C: the same value shifted the other way
dbg:command('wpset 00620000,10,rw,{ printf "CAL-C wpaddr=%08X data=%04X acc=%02X pc=%08X\\n", wpaddr, wpdata, wpaccess, pc ; g }')
print("calibration watchpoints armed: A=0x0620000d B=0x6200010 C=0x00620000")
local maxframes = tonumber(os.getenv("CAL_FRAMES") or "200")
local n = 0
emu.register_frame_done(function()
n = n + 1
if n >= maxframes then
print("done (" .. n .. " frames)")
machine:exit()
end
end, "vt1200-cal")
dbg.visible_cpu = cpu
dbg.execution_state = 'run'
+35
View File
@@ -0,0 +1,35 @@
-- Watch the video-memory test's accesses with their VALUES, to see what it
-- expects versus what it gets. The test reports failing at bit address
-- 0x40000C and walks in 0x400000 steps.
--
-- WP2_FRAMES=400 <mame> vt1200 ... -autoboot_script harness/mame/wp2.lua
local machine = manager.machine
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
if not dbg then
print("ERROR: debugger not enabled; pass -debug")
machine:exit()
return
end
-- Watch a window around both candidate locations for the video memory
-- (the reported failing address is 0x40000C; the driver-side catch-all saw
-- traffic at 0x400000-0x40D7FF).
dbg:command('wpset 400000,1000,rw,{ printf "WP-A %08X data=%04X acc=%02X pc=%08X\\n", wpaddr, wpdata, wpaccess, pc ; g }')
dbg:command('wpset 4000000,1000,rw,{ printf "WP-B %08X data=%04X acc=%02X pc=%08X\\n", wpaddr, wpdata, wpaccess, pc ; g }')
print("watchpoints armed: A=0x400000..0x400FFF B=0x4000000..0x4000FFF")
local maxframes = tonumber(os.getenv("WP2_FRAMES") or "300")
local n = 0
emu.register_frame_done(function()
n = n + 1
if n >= maxframes then
print("done (" .. n .. " frames)")
machine:exit()
end
end, "vt1200-wp2")
dbg.visible_cpu = cpu
dbg.execution_state = 'run'