forked from arti/vt1200
310 lines
14 KiB
Markdown
310 lines
14 KiB
Markdown
# How much work is a "full boot" of the VT1200 in MAME?
|
||
|
||
Investigated 2026-10-15/16. Companion to `FINDINGS.md` (which covers the harness that
|
||
exists today and the font questions). This file answers: **what would it take to make
|
||
the MAME driver boot this machine as opposed to merely executing its firmware?**
|
||
|
||
Short answer: **a multi-week project, dominated by one unknown** — decoding the
|
||
board's custom peripheral windows. Everything else has a device model in MAME
|
||
already. Below is the evidence and the breakdown.
|
||
|
||
---
|
||
|
||
## 1. What "full boot" means here
|
||
|
||
The terminal boots to an **X server + the VTE terminal emulator + a LAT client + a
|
||
4.3BSD TCP/IP stack**. "Full boot" = the firmware completes POST, initialises video,
|
||
the DUARTs, the LANCE and the NVRAM, and reaches the point where it is listening on
|
||
the network and driving the screen. That is exactly the state in which the crash we
|
||
have been chasing happens — i.e. it is *not* just a debugging convenience. It is the
|
||
condition needed to reproduce the LAT/announcement crash offline.
|
||
|
||
A weaker goal — "run *specific* firmware routines" (e.g. the font parser) — is far
|
||
cheaper and is covered in §6.
|
||
|
||
---
|
||
|
||
## 2. Hardware inventory (derived, not guessed)
|
||
|
||
The ROM ships a **self-test menu that enumerates the board**, which gives a clean
|
||
device list (each entry is the test's bit in the selection mask, not an address):
|
||
|
||
```
|
||
C - LANCE TEST (00008000h)
|
||
D - KEYBOARD TEST (00004000h)
|
||
E - DUART1, CHANNEL A (HOST) TEST (00000400h)
|
||
F - DUART1, CHANNEL B (PRINT) TEST (00000800h)
|
||
G - DUART1 TIMER TEST (00080000h)
|
||
H - DUART2, CHANNEL A (KEYBOARD) TEST (00001000h)
|
||
I - DUART2, CHANNEL B (MOUSE) TEST (00002000h)
|
||
J - DUART2 TIMER TEST (00100000h)
|
||
K - MOUSE/TABLET TEST (00010000h)
|
||
L - VIDEO TEST (00020000h)
|
||
N - VIDEO MEMORY TEST (00000100h)
|
||
M - ETHERNET ROM TEST (00040000h)
|
||
A[x]/Y - ROM TEST (x = bank select)/NVM
|
||
```
|
||
|
||
So the board is:
|
||
|
||
| block | notes |
|
||
|---|---|
|
||
| **LANCE Ethernet** | AM79C90-class; 4.3BSD-derived stack above it (`lance_inet_interface.c`, `lpd_lance_devdriver.c`) |
|
||
| **2× DUART** | DUART1 = host + printer; DUART2 = keyboard + mouse; each has 2 timers. MC68681 / SCN2681 class |
|
||
| **Ethernet ROM** | MAC address storage |
|
||
| **NVM** | non-volatile setup memory (battery-backed) |
|
||
| **Video** | TMS34010 on-chip display controller + VRAM + an output stage |
|
||
| **Keyboard, mouse/tablet** | serial, behind DUART2 |
|
||
| **Custom ASIC(s)** | the undecoded windows in §4 |
|
||
|
||
Interrupt sources documented in the service guide: Display, Host Interface,
|
||
Interrupt 1, Interrupt 2, NMI, Trap, Window Violation, plus LANCE, DUART (2 ch +
|
||
2 timers) and video clock.
|
||
|
||
---
|
||
|
||
## 3. What MAME already gives us (this is the good news)
|
||
|
||
Everything needed exists as a device model, and there are two near-perfect templates:
|
||
|
||
| need | MAME device | template driver |
|
||
|---|---|---|
|
||
| CPU | `tms34010` (incl. display controller) | already used here |
|
||
| LANCE Ethernet | `am79c90` | `dec/kn01.cpp` (DEC PMAX) — shows the DMA callback wiring |
|
||
| DUART ×2 | `scn2681` / `mc68681` | `ncd/ncd68k.cpp` |
|
||
| NVRAM | `nvram`, `eepromser`, `ds1215` (phantom clock) | many |
|
||
| Video RAMDAC | `bt478_device`, `ramdac` family | `ncd/ncd68k.cpp` |
|
||
| Keyboard | `pc_kbdc`, or a plain serial model | `ncd/ncd68k.cpp` |
|
||
|
||
**`ncd/ncd68k.cpp` is the closest analogue in MAME: it is an NCD X terminal** with
|
||
LANCE + SCN2681 + VRAM + RAMDAC + EEPROM + keyboard. Anyone doing this work should
|
||
start from that file and from `dec/kn01.cpp`.
|
||
|
||
So the chipset is *not* the hard part. Nothing here needs a new device model.
|
||
|
||
---
|
||
|
||
## 4. The one real unknown: the board's custom windows
|
||
|
||
`MEMORY_MAP.md` already flags this, and it is the dominant cost:
|
||
|
||
> **Board peripherals** appear around byte `0x00B00000`, `0x00C40000`, `0x00C80000`
|
||
> (bit `0x5800000`, `0x6200000`, `0x6400000`). POST writes `@0x62000d0/f0` and
|
||
> `@0x64000d0/e0/f0`; the two identical windows at `0xC40000`/`0xC80000` are presumed
|
||
> to be the two DUARTs. **Device decoding not confirmed.**
|
||
|
||
The reset vector (file offset 0x08) is literally:
|
||
|
||
```
|
||
3F200080: MOVE A0,@64000D0h,0 ; before anything else
|
||
3F2000D0: MOVB A0,@64000F0h
|
||
3F200120: MOVB A0,@64000E0h
|
||
3F200160: MOVE A0,@62000D0h,0
|
||
3F2001B0: MOVE A0,@62000F0h,0
|
||
3F200260: MOVE A0,@C00000B0h,0 ; CONTROL
|
||
3F200290: MOVE A0,@C0000080h,0 ; DPYCTL
|
||
```
|
||
|
||
So two opaque windows (`0x6200000`, `0x6400000`) are programmed **before the CPU's own
|
||
display registers**, i.e. they are essential. They are presumably the system-logic
|
||
ASIC (interrupt controller, video timing, NVM chip-select, LAN chip-select, maybe a
|
||
watchdog). Nobody has decoded them yet.
|
||
|
||
**Work required:** reverse-engineer those register sets from POST + the diagnostics
|
||
code. This is ordinary RE with good anchors (the diagnostic tests name each block),
|
||
but it is open-ended: the register count is unknown until it is done.
|
||
|
||
**Risk:** high, and it is the item that decides the schedule. Everything else can be
|
||
estimated; this cannot.
|
||
|
||
---
|
||
|
||
## 4b. Can we just stub the unknown windows? (tested)
|
||
|
||
Yes — and it is cheap — but **it does not produce a boot**. Tested 2026-10-16.
|
||
|
||
The driver now takes `VT1200_BOARD_STUB`:
|
||
|
||
| value | behaviour of the `0x5800000` / `0x6200000` / `0x6400000` windows |
|
||
|---|---|
|
||
| unset | windows stay **unmapped** — identical to the pre-experiment driver |
|
||
| `0` | windows mapped, every read returns 0 (same visible result as unset) |
|
||
| `0xffff` | every read returns all-ones, i.e. every polled "ready" bit asserted |
|
||
| `ram` | windows behave as read/write RAM — **writes persist and read back** |
|
||
|
||
Results, 1200 frames each — three different places to get stuck, none of them a boot:
|
||
|
||
| stub | parked at | what that code is |
|
||
|---|---|---|
|
||
| `0` | `PC=3FDCE400` (file `0x179C80`) | the `0x6200000` ready-poll (see below) |
|
||
| `0xffff` | `PC=3F2082C0` (file `0x1058`) | POST video/refresh delay loop, `JR 8060h` |
|
||
| `ram` | `PC=3F2088B0` (file `0x1116`) | a third, different gate |
|
||
|
||
### What the ready-poll actually is
|
||
|
||
`stackwalk.lua` dumps the stack while parked in the delay loop, which is how the
|
||
first gate was identified. The delay routine is *fine* (it gets count 5968 ×
|
||
multiplier 100 ≈ 600k iterations ≈ 45 ms); it is simply **called in a poll loop**:
|
||
|
||
```
|
||
bcf210: MOVI 64h,A11 ; 100
|
||
bcf240: CALLA 3FDCE1C0h ; delay(100)
|
||
bcf270: MOVE *A13(FF80h),A9,1 ; A9 = 0x06200000
|
||
bcf2a0: MOVE *A9(10h),A11,0 ; read byte @ 0x06200010
|
||
bcf2c0: BTST 0h,A11 ; test bit 0
|
||
bcf2d0: JREQ BCF200h ; not ready -> delay + retry
|
||
```
|
||
|
||
i.e. `while (!(read8(0x06200010) & 1)) delay(100);` — a **hardware-ready poll on the
|
||
undecoded window**. Note the CPU's own registers (`CONTROL`, `HSTCTLH`) *are* modelled
|
||
by MAME's tms34010, so those reads were already real; the gate is purely the board ASIC.
|
||
|
||
### The conclusion that matters
|
||
|
||
Stubbing is a good **diagnostic** and a poor **shortcut**:
|
||
|
||
- It is cheap (≈20 lines in the driver) and it does move the firmware — from the
|
||
ready-poll into the POST video phase, and onwards under a different policy.
|
||
- But each policy lands somewhere *different* and still looping. Blindly asserting
|
||
"ready" does not walk the boot forward; it selects a different wrong path.
|
||
- Passing a gate *correctly* still requires knowing what the register means.
|
||
|
||
**The useful part:** iterating targeted per-gate stubs **enumerates the gates**. In one
|
||
afternoon, one known gate became three distinct stuck points. If that enumeration is
|
||
done systematically, item 1 above stops being "decode an unknown ASIC" and becomes
|
||
"decode the specific registers these N gates touch" — a bounded, much smaller job.
|
||
That is the recommended way to attack item 1.
|
||
|
||
The experimental stub is committed in `vt1200.cpp` and defaults to the old behaviour,
|
||
so nothing regresses if `VT1200_BOARD_STUB` is unset.
|
||
|
||
---
|
||
|
||
## 5. Where the boot stops today (measured, not assumed)
|
||
|
||
`harness/mame/progress.lua` (new) samples the PC once per emulated frame and reports
|
||
the hottest PCs. Two runs:
|
||
|
||
- 400 frames, and
|
||
- 3000 frames (≈ 10 minutes of emulated time at this driver's ~4.9 Hz frame rate).
|
||
|
||
Result — **the CPU sits in one instruction**:
|
||
|
||
```
|
||
PC=3FDCE400 samples=2976 of 3000 first_frame=1
|
||
PC=3FDCE1C0 samples=1 first_frame=2848 <- routine entry
|
||
PC=3FDCE460 samples=1 first_frame=2736 <- its RETS
|
||
```
|
||
|
||
Disassembled (`PC 0x3FDCE400` -> byte `0x07FB9C80` -> file `0x179C80`), the routine is:
|
||
|
||
```
|
||
bce1c0: MMTM SP,A9,A11,A13 ; entry
|
||
bce210: MOVE *A13(FFE0h),A9,1 ; A9 = caller's argument
|
||
bce230: MOVI C00000B0h,A11 ; CONTROL
|
||
bce2d0: MOVE *A13(20h),A11,1
|
||
bce300: MOVE *A11,A11,0 ; read CONTROL
|
||
bce310: BTST Fh,A11 ; test bit 15
|
||
bce330: MOVE *A13(40h),A11,1 ; HSTCTLH (C0000100)
|
||
bce350: MOVE *A11,A11,0 ; read HSTCTLH
|
||
bce360: BTST Eh,A11 ; test bit 14
|
||
bce380: MOVK 1Eh,A11 ; multiplier = 30
|
||
bce3b0: MOVI 64h,A11 ; ... or 100
|
||
bce3e0: MPYS A11,A9 ; A9 = argument * multiplier
|
||
bce400: DSJS A9,BCE400h ; <-- THE SPIN: ~2^32 iterations
|
||
bce460: RETS 2h
|
||
```
|
||
|
||
**Reading:** this is a timed-wait / calibration helper. **One call to it takes ~2,700
|
||
frames ≈ 9 minutes of emulated CPU time**, because the multiplier/count inputs come
|
||
from peripheral status that the current driver stubs to `0xFFFF` — so the loop runs
|
||
on garbage. The real machine boots in seconds, so this is not "slow firmware", it is
|
||
"missing hardware".
|
||
|
||
This is a useful, concrete marker: **the first thing that must be modelled is whatever
|
||
feeds this routine**, and we cannot get past early POST without it. It also confirms
|
||
the driver's own note that unmapped reads returning `0xffff` are being acted upon.
|
||
|
||
---
|
||
|
||
## 6. Work breakdown and estimate
|
||
|
||
Ordered by dependency. Effort is for one engineer already familiar with this codebase
|
||
and MAME, working part-time, and excludes the §6b shortcut.
|
||
|
||
| # | item | effort | uncertainty |
|
||
|---|---|---|---|
|
||
| 1 | Decode the `0x6200000` / `0x6400000` windows (interrupt controller, chip selects, video timing, NVM) | **1–3 weeks** | **high — this is the schedule** |
|
||
| 2 | Wire LANCE: address, DMA, IRQ (`dec/kn01.cpp` pattern) | 2–4 days | low |
|
||
| 3 | Wire 2× DUART + their 4 timers + IRQs | 2–3 days | low |
|
||
| 4 | NVRAM + Ethernet ROM (MAC) | 1–2 days | low (needs correct contents) |
|
||
| 5 | Video: display params, VRAM mapping, palette/output stage; get a non-blank screen | 3–7 days | medium (TMS34010 display is modelled; the *board* video path is not) |
|
||
| 6 | Interrupt plumbing: TMS34010 INTENB/INTPEND + the external controller | 2–4 days | medium — depends on #1 |
|
||
| 7 | Keyboard + mouse so the terminal is usable | 2–4 days | low–medium |
|
||
| 8 | Boot debugging to "network up + screen up" | **1–3 weeks** | high (integration) |
|
||
| 9 | Bonus: make the LAN path work end-to-end against latd | 3–7 days | medium |
|
||
|
||
**Total: roughly 6–12 weeks part-time**, with the spread driven almost entirely by
|
||
items 1 and 8. Nothing in it is research-grade; it is a large, ordinary bring-up task
|
||
with one genuinely unknown block.
|
||
|
||
### What would make it collapse to something much smaller
|
||
|
||
- If the custom windows turn out to be a **few dozen registers** and the diagnostics
|
||
code documents them (quite likely — the self-test knows every register it pokes),
|
||
item 1 could be days rather than weeks.
|
||
- If the boot turns out not to need the LANCE before reaching video, items 2 and 9
|
||
can be deferred, and the terminal becomes usable on-screen much sooner.
|
||
|
||
### What would blow it up
|
||
|
||
- The custom ASIC turning out to contain a DMA engine or a video path with
|
||
undocumented timings (plausible for the `0x6200000` writes at reset).
|
||
- POST validating device responses in ways that require behavioural accuracy, not
|
||
just register presence.
|
||
|
||
---
|
||
|
||
## 6b. The much cheaper alternative (and what I'd actually do)
|
||
|
||
For this project's real questions — the font parser, the VTE, the LAT client — a full
|
||
boot is **not the cheapest route**, and probably not needed at all.
|
||
|
||
A **call harness** gets you inside a specific routine in **days**:
|
||
|
||
- put a writable overlay at `fosRead` (`0x3f96eee0`) / `fosAlloc` (`0x3f703d90`),
|
||
- have Lua write a small TMS34010 stub that serves bytes from a RAM copy of the file
|
||
under test (a `.pcf`, an SNF, a LAT session buffer),
|
||
- set `PC` to the routine of interest with a crafted argument block.
|
||
|
||
That is how the `pcfReadBitmaps` overflow was already demonstrated in-emulator
|
||
(`overflow.lua` does exactly this pattern, jumping straight into the arithmetic with
|
||
fabricated state). Extending it to `pcfReadFont` is the cheapest way to answer the
|
||
outstanding font question.
|
||
|
||
**Recommendation:** do the call harness now; treat the full boot as an optional
|
||
parallel project, and only start it if you want offline reproduction of the LAT crash
|
||
— in which case item 1 (the custom-window decode) is the thing to schedule.
|
||
|
||
---
|
||
|
||
## 7. Reproduce
|
||
|
||
```sh
|
||
cd /home/arti/code/k-space/vt1200
|
||
M=/home/arti/code/k-space/mame0289/vt1200
|
||
|
||
# where does the boot get, and where does it spin?
|
||
PROG_FRAMES=3000 $M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
|
||
-debug -debugger none -autoboot_script harness/mame/progress.lua -autoboot_delay 0
|
||
|
||
# hardware inventory (the self-test menu)
|
||
strings -n 6 vt1200_v22.bin | grep -E "TEST ?\(|Selftest"
|
||
|
||
# disassemble the spin site (byte offsets; printed address = byte*8)
|
||
python3 lat/notes/ua.py 0x179c30 0x179cd0
|
||
```
|
||
|
||
Note: Lua `print()` goes to **stdout**; debugger `printf`/`-debuglog` output goes to
|
||
`./debug.log`. Two different streams — easy to lose an afternoon to that.
|