forked from arti/vt1200
697 lines
31 KiB
Markdown
697 lines
31 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** — and that size is itself a finding,
|
||
because the map range `0..0x1fffff` with 8 address units per byte means **the driver
|
||
only models 256 KiB of main RAM**. The real VT1200 has 1 MiB onboard plus up to
|
||
3 × 1 MiB SIMMs (service guide), and POST runs a memory test. **Enlarging the RAM
|
||
mapping is now a prime suspect and is a cheap experiment.**
|
||
|
||
Contents of what we do model: full of 32-bit **ROM pointers** (`0x3FD24A90`, `0x3FD2C800`,
|
||
…) and resident strings (`$Header: init.c,v 1.3 …`, `DWT TCP/IP Rev 1.0`,
|
||
`LATSERVER`/`LANCESERVER`/`MOPSERVER`). 99.6% of the *earlier* low-memory dump was
|
||
zeros, but that dump's granularity was wrong; the real picture is tables + pointers.
|
||
|
||
**No framebuffer appears in main RAM.** Combined with the self-test having a separate
|
||
`N - VIDEO MEMORY TEST`, the framebuffer is most likely **separate VRAM** mapped
|
||
somewhere not yet identified. That fits the black screen: the display address resolves
|
||
into unmapped space, so the renderer reads zeros.
|
||
|
||
### Hardware facts supplied from the real machine (2026-10-16)
|
||
|
||
* Output is **always mono black and white** → 1 bit/pixel.
|
||
* **1280×1024** → **160 bytes per scanline**, framebuffer **163,840 bytes (0xA0000)**.
|
||
* Boot sequence on real hardware: an **ASCII/font table** is printed first, then the
|
||
screen wipes, then the **self-test progress** runs — shown as a large **circle** in
|
||
the middle of the screen.
|
||
|
||
These give the renderer its geometry **and** a validation target: a correct renderer
|
||
should show the character table first.
|
||
|
||
### RAM-as-bitmap probe
|
||
|
||
`vt1200-ram-200.bin` rendered at the 160-byte stride (`ram-as-bitmap.png`,
|
||
`crop-glyphs.png`) shows a dense band around rows 738–747. Zooming shows it is **not**
|
||
glyphs — the hexdump is little-endian 32-bit ROM pointers, i.e. a pointer table.
|
||
|
||
## What the service guide says (already in `docs/` — read this first next time)
|
||
|
||
`docs/EK-V1000-SV-002_service-guide.txt` §2.1.1 documents the power-up self-test almost
|
||
exactly as observed on the real machine. This is the authoritative boot sequence.
|
||
|
||
**Level 0 — 8 tests, must complete within 5 seconds:**
|
||
|
||
```
|
||
Simple CPU test Video control register test
|
||
ROM checksum test I/O data path test
|
||
RAM test Illegal monitor test
|
||
Interrupt test Ethernet port (LANCE) not reset on power-up test
|
||
```
|
||
|
||
**Level 1 — 15 tests, complete within 25 seconds of power-up.** It "begins by
|
||
displaying various patterns on the screen. Next, a **pie graph** display appears to
|
||
indicate the progress of level 1 testing. As each test finishes, a segment of the pie
|
||
fills in."
|
||
|
||
```
|
||
Video RAM test Video test NVR checksum test Ethernet address ROM test
|
||
Option RAM test Host port test Printer port test Image test
|
||
DUART1 timer test Keyboard port test Mouse port test DUART2 timer test
|
||
Ethernet port (LANCE) test Mouse test Keyboard test
|
||
```
|
||
|
||
### Why this matters for our stall
|
||
|
||
* The user's **"large circle in the middle of the screen" is the level-1 pie graph** —
|
||
and the "ASCII font table" seen first is the level-1 "various patterns". So the real
|
||
machine is well into **level 1** by ~12 s, and finished by ~25 s.
|
||
* We have run **22–44 s of emulated time** and are still stuck. Confirmed: blocked, not slow.
|
||
* **Level 0 needs hardware we do not model**: an *Interrupt test*, an *I/O data path
|
||
test*, and an *Ethernet port (LANCE) not reset on power-up* test. Our gates
|
||
(status bits on the `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.
|
||
|
||
**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.
|
||
|
||
---
|
||
|
||
## Next steps
|
||
|
||
FIX THE INSTRUMENTATION FIRST (see above), then model video memory.
|