forked from arti/vt1200
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.
1040 lines
48 KiB
Markdown
1040 lines
48 KiB
Markdown
# 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, ¶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)
|
||
|
||
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.
|