forked from arti/vt1200
checkpoint: VT1200 reverse-engineering workspace before board-model work
This commit is contained in:
commit
acb92d3e44
325 files changed
+951894
No files matched your search
@@ -0,0 +1,481 @@
|
||||
# VT1200 board register model — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the driver's ad-hoc board-window stubs with a modelled, unit-testable register file for the four undecoded VT1200 windows, so the firmware's status/command handshakes behave as real state machines instead of constants.
|
||||
|
||||
**Architecture:** A standalone C++ class `vt1200_board` with **no MAME dependencies** (fixed-width integer types only), so it can be unit-tested with plain `g++` outside the emulator. It models two shapes discovered in the firmware: `big` (windows `0x6200000` and `0x6400000` — STATUS at `+0x10`, COMMAND/DATA at `+0x20`, plus `+0x30`/`+0xD0`/`+0xE0`/`+0xF0`) and `dma` (the `0x5800000` data port + `0x5900000` command port pair). The MAME driver owns one instance and forwards its address-map callbacks to it.
|
||||
|
||||
**Tech Stack:** C++17, MAME 0.289 driver harness, plain `g++` for the model's unit tests, the project's existing `harness/mame/build.sh`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-16-vt1200-board-model-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **No regression:** with no `VT1200_*` environment variables set, the driver must still reproduce the original wedge — `PC=3FDCE400` with ~199/200 frame samples — verified by the command in Task 1 Step 6.
|
||||
- All harness behaviour stays **opt-in** via environment variables; new behaviour must default off.
|
||||
- **Do not modify MAME's own source** (`/home/arti/code/k-space/mame0289/src/...`). Only the driver source in this repo changes.
|
||||
- Builds use `harness/mame/build.sh` (it now exits non-zero on compile failure — a bare `built:` means success).
|
||||
- The board model must use **only** `<cstdint>`, `<map>`, `<vector>` — no `emu.h` — so unit tests compile without MAME.
|
||||
- Addresses inside the model are in the **harness's units**: window base `| (offset & 0xffff)`, where the base is the disassembly operand shifted right by 4 (e.g. code `@0x64000D0` is harness `0x0640000D`).
|
||||
- Every finding discovered while implementing goes into `harness/mame/BRINGUP_LOG.md`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `harness/mame/vt1200_board.h` | **Create.** Declares `vt1200_board`: the four windows, both shapes, configuration, `read`/`write`/`tick`. No MAME types. |
|
||||
| `harness/mame/vt1200_board.cpp` | **Create.** Implements the register file and the status state machines. |
|
||||
| `harness/mame/test_vt1200_board.cpp` | **Create.** Standalone `g++` unit tests for the model. No MAME, no emulator. |
|
||||
| `harness/mame/vt1200.cpp` | **Modify.** Own a `vt1200_board`, forward the four window callbacks to it, keep the existing env knobs working as configuration. |
|
||||
| `harness/mame/build.sh` | **Modify.** Compile the extra translation unit and symlink it into the MAME tree. |
|
||||
| `harness/mame/BRINGUP_LOG.md` | **Modify.** Record findings. |
|
||||
|
||||
Later plans (separate documents, not this one): *video RAM + scanline renderer*, then *DUARTs / NVRAM / MAC ROM*.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Extract the existing behaviour into a standalone, unit-tested class
|
||||
|
||||
Behaviour-preserving refactor. This is the riskiest task for regressions, so it lands first and alone.
|
||||
|
||||
**Files:**
|
||||
- Create: `harness/mame/vt1200_board.h`
|
||||
- Create: `harness/mame/vt1200_board.cpp`
|
||||
- Create: `harness/mame/test_vt1200_board.cpp`
|
||||
- Modify: `harness/mame/vt1200.cpp`
|
||||
- Modify: `harness/mame/build.sh`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing (first task).
|
||||
- Produces:
|
||||
- `class vt1200_board` with `uint16_t read(uint32_t addr)`, `void write(uint32_t addr, uint16_t data)`, `void tick()`
|
||||
- `void set_default(uint16_t v)`, `void set_override(uint32_t addr, uint16_t v)`
|
||||
- `void set_monitor_switch(uint8_t position)`, `void set_busy_ticks(int n)`
|
||||
- `void set_model_handshake(bool on)`
|
||||
- constants `VT1200_WIN0` = `0x05800000`, `VT1200_WIN1` = `0x05900000`, `VT1200_WIN2` = `0x06200000`, `VT1200_WIN3` = `0x06400000`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `harness/mame/test_vt1200_board.cpp`:
|
||||
|
||||
```cpp
|
||||
// Standalone unit tests for the VT1200 board register model.
|
||||
// g++ -std=c++17 -o /tmp/t vt1200_board.cpp test_vt1200_board.cpp && /tmp/t
|
||||
#include "vt1200_board.h"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond) do { if (!(cond)) { \
|
||||
std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } } while (0)
|
||||
|
||||
static void test_default_read(void)
|
||||
{
|
||||
vt1200_board b;
|
||||
b.set_default(0x0000);
|
||||
CHECK(b.read(VT1200_WIN2 + VT1200_OFF_STATUS) == 0x0000);
|
||||
}
|
||||
|
||||
static void test_override_read(void)
|
||||
{
|
||||
vt1200_board b;
|
||||
b.set_override(VT1200_WIN2 + VT1200_OFF_STATUS, 0x00b7);
|
||||
CHECK(b.read(VT1200_WIN2 + VT1200_OFF_STATUS) == 0x00b7);
|
||||
}
|
||||
|
||||
static void test_monitor_switch_is_mixed_with_latches(void)
|
||||
{
|
||||
vt1200_board b;
|
||||
b.set_monitor_switch(7); // position 7 -> bits 4-6 = 0x70
|
||||
b.write(VT1200_WIN3 + VT1200_OFF_SWITCH, 0x0001); // output latch writes bit 0
|
||||
const uint16_t v = b.read(VT1200_WIN3 + VT1200_OFF_SWITCH);
|
||||
CHECK((v & 0x0070) == 0x0070); // switch bits read back as input
|
||||
CHECK((v & 0x0001) == 0x0001); // output bits read back as latched
|
||||
}
|
||||
|
||||
static void test_busy_bit_arms_on_write_and_clears(void)
|
||||
{
|
||||
vt1200_board b;
|
||||
b.set_model_handshake(true);
|
||||
b.set_busy_ticks(2);
|
||||
const uint32_t status = VT1200_WIN2 + VT1200_OFF_STATUS;
|
||||
const uint32_t cmd = VT1200_WIN2 + VT1200_OFF_CMD;
|
||||
|
||||
CHECK((b.read(status) & 0x0001) == 0x0000); // idle
|
||||
b.write(cmd, 0x00ca); // command arms it
|
||||
CHECK((b.read(status) & 0x0001) == 0x0001); // busy
|
||||
b.tick();
|
||||
CHECK((b.read(status) & 0x0001) == 0x0001); // still busy
|
||||
b.tick();
|
||||
CHECK((b.read(status) & 0x0001) == 0x0000); // done
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_default_read();
|
||||
test_override_read();
|
||||
test_monitor_switch_is_mixed_with_latches();
|
||||
test_busy_bit_arms_on_write_and_clears();
|
||||
std::printf(failures ? "%d FAILURE(S)\n" : "all board tests passed\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd harness/mame && g++ -std=c++17 -o /tmp/t vt1200_board.cpp test_vt1200_board.cpp`
|
||||
Expected: FAIL — `vt1200_board.cpp: No such file or directory`.
|
||||
|
||||
- [ ] **Step 3: Write the header**
|
||||
|
||||
Create `harness/mame/vt1200_board.h`:
|
||||
|
||||
```cpp
|
||||
// VT1200 board register model. Deliberately free of MAME dependencies so it can
|
||||
// be unit-tested with plain g++.
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
|
||||
// Window bases in the harness's address units (disassembly operand >> 4).
|
||||
static const uint32_t VT1200_WIN0 = 0x05800000; // data port of the DMA pair
|
||||
static const uint32_t VT1200_WIN1 = 0x05900000; // command port of the DMA pair
|
||||
static const uint32_t VT1200_WIN2 = 0x06200000; // big: STATUS +0x10, CMD +0x20
|
||||
static const uint32_t VT1200_WIN3 = 0x06400000; // big, plus the monitor switch
|
||||
|
||||
// Window-relative register offsets IN HARNESS UNITS.
|
||||
//
|
||||
// The harness address is the firmware's operand shifted right by 4 (MAME's
|
||||
// TMS34010 space is word-unit here), so an operand offset of +0x10 is harness
|
||||
// +0x01, and an operand offset of +0xD0 is harness +0x0d. Getting this wrong
|
||||
// makes registers silently unreachable, so always use these names.
|
||||
//
|
||||
// firmware operand harness offset role
|
||||
// +0x10 0x01 STATUS (bit 0 = ready/done)
|
||||
// +0x20 0x02 COMMAND / DATA
|
||||
// +0x30 0x03 (read/written in the same block)
|
||||
// +0xD0 0x0d mixed I/O -- monitor switch in bits 4-6
|
||||
// +0xE0 0x0e
|
||||
// +0xF0 0x0f
|
||||
static const uint32_t VT1200_OFF_STATUS = 0x0001;
|
||||
static const uint32_t VT1200_OFF_CMD = 0x0002;
|
||||
static const uint32_t VT1200_OFF_IO30 = 0x0003;
|
||||
static const uint32_t VT1200_OFF_SWITCH = 0x000d;
|
||||
static const uint32_t VT1200_OFF_E0 = 0x000e;
|
||||
static const uint32_t VT1200_OFF_F0 = 0x000f;
|
||||
|
||||
class vt1200_board
|
||||
{
|
||||
public:
|
||||
vt1200_board() = default;
|
||||
|
||||
// Configuration (replaces the test-time env knobs).
|
||||
void set_default(uint16_t v) { m_default = v; }
|
||||
void set_override(uint32_t addr, uint16_t v) { m_override[addr] = v; }
|
||||
void set_model_handshake(bool on) { m_handshake = on; }
|
||||
void set_busy_ticks(int n) { m_busy_ticks = n; }
|
||||
void set_monitor_switch(uint8_t position) { m_switch_bits = (uint16_t)((position & 7) << 4); }
|
||||
|
||||
// Bus interface.
|
||||
uint16_t read(uint32_t addr);
|
||||
void write(uint32_t addr, uint16_t data);
|
||||
void tick();
|
||||
|
||||
private:
|
||||
bool is_switch_port(uint32_t addr) const;
|
||||
|
||||
std::map<uint32_t, uint16_t> m_store; // write-back latches
|
||||
std::map<uint32_t, uint16_t> m_override; // fixed values (experiments)
|
||||
uint16_t m_default = 0;
|
||||
bool m_handshake = false;
|
||||
int m_busy_ticks = 0;
|
||||
int m_busy_left = 0;
|
||||
uint16_t m_switch_bits = 0;
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write the implementation**
|
||||
|
||||
Create `harness/mame/vt1200_board.cpp`:
|
||||
|
||||
```cpp
|
||||
#include "vt1200_board.h"
|
||||
|
||||
bool vt1200_board::is_switch_port(uint32_t addr) const
|
||||
{
|
||||
// The monitor-selection switch lives in bits 4-6 of winner 3's +0xD0
|
||||
// register (harness offset 0x0d).
|
||||
return addr == (VT1200_WIN3 + VT1200_OFF_SWITCH);
|
||||
}
|
||||
|
||||
uint16_t vt1200_board::read(uint32_t addr)
|
||||
{
|
||||
auto ov = m_override.find(addr);
|
||||
if (ov != m_override.end())
|
||||
return ov->second;
|
||||
|
||||
uint16_t value = m_default;
|
||||
auto st = m_store.find(addr);
|
||||
if (st != m_store.end())
|
||||
value = st->second;
|
||||
|
||||
if (is_switch_port(addr))
|
||||
return (uint16_t)((value & 0xff8f) | (m_switch_bits & 0x0070));
|
||||
|
||||
if (m_handshake && (addr & 0xffff) == VT1200_OFF_STATUS)
|
||||
{
|
||||
if (m_busy_left > 0)
|
||||
value |= 0x0001;
|
||||
else
|
||||
value &= ~0x0001;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void vt1200_board::write(uint32_t addr, uint16_t data)
|
||||
{
|
||||
m_store[addr] = data;
|
||||
if (m_handshake && (addr & 0xffff) == VT1200_OFF_CMD)
|
||||
m_busy_left = m_busy_ticks;
|
||||
}
|
||||
|
||||
void vt1200_board::tick()
|
||||
{
|
||||
if (m_busy_left > 0)
|
||||
m_busy_left--;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the unit tests to verify they pass**
|
||||
|
||||
Run: `cd harness/mame && g++ -std=c++17 -o /tmp/t vt1200_board.cpp test_vt1200_board.cpp && /tmp/t`
|
||||
Expected: `all board tests passed`
|
||||
|
||||
- [ ] **Step 6: Wire the driver to the model and verify no regression**
|
||||
|
||||
In `harness/mame/vt1200.cpp`: add `#include "vt1200_board.h"`, give `vt1200_state` a `vt1200_board m_board;` member, and in `board_read`/`board_write` delegate to `m_board.read(addr)` / `m_board.write(addr, data)` instead of the current inline logic. In `machine_start`, translate the existing env vars into the model's setters (`VT1200_BOARD_DEFAULT`, `VT1200_BOARD_MAP`, `VT1200_BOARD_MODEL`, `VT1200_BOARD_BUSY_TICKS`, `VT1200_BOARD_SWITCH`). Call `m_board.tick()` once per emulated frame — the simplest correct place is the existing `scanline` callback guarded so it only fires once per frame, or `screen_update` when `cliprect.min_y == 0`.
|
||||
|
||||
In `harness/mame/build.sh`: add the new source to the symlink list and to `SOURCES=` (comma-separated), and add `g++ -c vt1200_board.cpp` to the unit-test recipe.
|
||||
|
||||
Then verify:
|
||||
|
||||
Run:
|
||||
```sh
|
||||
cd /home/arti/code/k-space/vt1200
|
||||
JOBS=8 harness/mame/build.sh 2>&1 | tail -2
|
||||
M=/home/arti/code/k-space/mame0289/vt1200
|
||||
PROG_FRAMES=200 timeout 300 $M vt1200 -rompath harness/mame/roms -video none -sound none \
|
||||
-nothrottle -debug -debugger none -autoboot_script harness/mame/progress.lua -autoboot_delay 0 2>&1 \
|
||||
| grep -oE "PC=[0-9A-F]{8} samples=[0-9]+" | head -1
|
||||
```
|
||||
Expected: `PC=3FDCE400 samples=199` (the original wedge — no regression).
|
||||
|
||||
Then re-run the handshake configuration and confirm it still advances:
|
||||
|
||||
Run:
|
||||
```sh
|
||||
VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2 VT1200_BOARD_SWITCH="0640000D:70:70" \
|
||||
COV_FRAMES=600 COV_WINDOW=300 timeout 400 $M vt1200 -rompath harness/mame/roms -video none \
|
||||
-sound none -nothrottle -debug -debugger none -autoboot_script harness/mame/coverage.lua -autoboot_delay 0 2>&1 \
|
||||
| grep -E "^frame" | tail -1
|
||||
```
|
||||
Expected: a `total_distinct` in the high 50s (not the single-instruction wedge).
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add harness/mame/vt1200_board.h harness/mame/vt1200_board.cpp \
|
||||
harness/mame/test_vt1200_board.cpp harness/mame/vt1200.cpp harness/mame/build.sh
|
||||
git commit -m "harness: extract VT1200 board register model, unit-tested"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Per-window busy machines (`win_big` shape)
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/mame/vt1200_board.h`, `harness/mame/vt1200_board.cpp`
|
||||
- Modify: `harness/mame/test_vt1200_board.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `vt1200_board` from Task 1.
|
||||
- Produces: `void set_window_busy_ticks(uint32_t window_base, int n)` — per-window tick counts, so `0x6200000` and `0x6400000` can be tuned independently. `void tick()` now advances every window.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Add to `test_vt1200_board.cpp`:
|
||||
|
||||
```cpp
|
||||
static void test_windows_are_independent(void)
|
||||
{
|
||||
vt1200_board b;
|
||||
b.set_model_handshake(true);
|
||||
b.set_window_busy_ticks(VT1200_WIN2, 1);
|
||||
b.set_window_busy_ticks(VT1200_WIN3, 3);
|
||||
|
||||
b.write(VT1200_WIN2 + VT1200_OFF_CMD, 0x0001);
|
||||
b.write(VT1200_WIN3 + VT1200_OFF_CMD, 0x0001);
|
||||
CHECK((b.read(VT1200_WIN2 + VT1200_OFF_STATUS) & 1) == 1);
|
||||
CHECK((b.read(VT1200_WIN3 + VT1200_OFF_STATUS) & 1) == 1);
|
||||
|
||||
b.tick();
|
||||
CHECK((b.read(VT1200_WIN2 + VT1200_OFF_STATUS) & 1) == 0); // WIN2 finished
|
||||
CHECK((b.read(VT1200_WIN3 + VT1200_OFF_STATUS) & 1) == 1); // WIN3 still busy
|
||||
}
|
||||
```
|
||||
|
||||
Register it in `main()` with `test_windows_are_independent();`
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd harness/mame && g++ -std=c++17 -o /tmp/t vt1200_board.cpp test_vt1200_board.cpp && /tmp/t`
|
||||
Expected: FAIL — `'set_window_busy_ticks' was not declared`.
|
||||
|
||||
- [ ] **Step 3: Implement per-window busy state**
|
||||
|
||||
In `vt1200_board.h`, replace the single `m_busy_left` with `std::map<uint32_t, int> m_busy_left; std::map<uint32_t, int> m_busy_ticks_by_win;`, declare `void set_window_busy_ticks(uint32_t window_base, int n);`, and keep `set_busy_ticks` as a shorthand that applies to all windows. In `.cpp`, key both maps by `addr & 0xffff0000` so all four windows behave uniformly, and have `tick()` decrement every entry.
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `cd harness/mame && g++ -std=c++17 -o /tmp/t vt1200_board.cpp test_vt1200_board.cpp && /tmp/t`
|
||||
Expected: `all board tests passed`
|
||||
|
||||
- [ ] **Step 5: Verify in the emulator**
|
||||
|
||||
Run the Task 1 Step 6 emulator commands unchanged.
|
||||
Expected: identical results (this task is a pure generalisation; observable behaviour must not move).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add harness/mame/vt1200_board.h harness/mame/vt1200_board.cpp harness/mame/test_vt1200_board.cpp
|
||||
git commit -m "harness: per-window busy state machines for the big windows"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: The DMA pair (`win_pair` shape)
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/mame/vt1200_board.h`, `harness/mame/vt1200_board.cpp`
|
||||
- Modify: `harness/mame/test_vt1200_board.cpp`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `vt1200_board` from Tasks 1-2.
|
||||
- Produces: `void set_dma_ticks(int n)` and a real model of the pair — a write to the command port (`VT1200_WIN1`) followed by a write to the data port (`VT1200_WIN0`) completes in `n` ticks, after which the command port reads back `0`.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Add to `test_vt1200_board.cpp`:
|
||||
|
||||
```cpp
|
||||
static void test_dma_pair_command_completes(void)
|
||||
{
|
||||
vt1200_board b;
|
||||
b.set_model_handshake(true);
|
||||
b.set_dma_ticks(2);
|
||||
|
||||
b.write(VT1200_WIN0, 0x0000); // data port first (as the firmware does)
|
||||
b.write(VT1200_WIN1, 0x0001); // then a command
|
||||
CHECK(b.read(VT1200_WIN1) == 0x0001); // accepted
|
||||
|
||||
b.tick(); b.tick();
|
||||
CHECK(b.read(VT1200_WIN1) == 0x0000); // completed
|
||||
}
|
||||
```
|
||||
|
||||
Register it in `main()`.
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd harness/mame && g++ -std=c++17 -o /tmp/t vt1200_board.cpp test_vt1200_board.cpp && /tmp/t`
|
||||
Expected: FAIL — `'set_dma_ticks' was not declared`.
|
||||
|
||||
- [ ] **Step 3: Implement the DMA pair**
|
||||
|
||||
Add `int m_dma_ticks = 0; int m_dma_left = 0;` and `void set_dma_ticks(int n)`. In `write()`, a write to `VT1200_WIN1` (any offset whose high 16 bits are `VT1200_WIN1`) arms `m_dma_left = m_dma_ticks`. In `read()`, while `m_dma_left > 0` return the latched command for `VT1200_WIN1`; once it reaches 0 return `0`. Decrement in `tick()`.
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `cd harness/mame && g++ -std=c++17 -o /tmp/t vt1200_board.cpp test_vt1200_board.cpp && /tmp/t`
|
||||
Expected: `all board tests passed`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add harness/mame/vt1200_board.h harness/mame/vt1200_board.cpp harness/mame/test_vt1200_board.cpp
|
||||
git commit -m "harness: model the 0x5800000/0x5900000 DMA command/data pair"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Make it configurable, and confirm on real firmware behaviour
|
||||
|
||||
**Files:**
|
||||
- Modify: `harness/mame/vt1200.cpp`
|
||||
- Modify: `harness/mame/BRINGUP_LOG.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: everything from Tasks 1-3.
|
||||
- Produces: env knobs `VT1200_BOARD_BUSY_W2=<n>`, `VT1200_BOARD_BUSY_W3=<n>`, `VT1200_BOARD_DMA_TICKS=<n>`.
|
||||
|
||||
- [ ] **Step 1: Add the env plumbing**
|
||||
|
||||
In `machine_start()`, read the three new variables and call `set_window_busy_ticks(VT1200_WIN2, ...)`, `set_window_busy_ticks(VT1200_WIN3, ...)`, `set_dma_ticks(...)`. Keep every existing variable working unchanged.
|
||||
|
||||
- [ ] **Step 2: Rebuild and run a parameter sweep**
|
||||
|
||||
Run each of `VT1200_BOARD_BUSY_W2` in `{1,2,4,8}` (with `W3` fixed at 2, DMA at 2, handshake on, switch `0x70`), recording the coverage result:
|
||||
|
||||
```sh
|
||||
for N in 1 2 4 8; do
|
||||
printf "W2=%s -> " "$N"
|
||||
VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_W2=$N VT1200_BOARD_BUSY_W3=2 \
|
||||
VT1200_BOARD_DMA_TICKS=2 VT1200_BOARD_SWITCH="0640000D:70:70" \
|
||||
COV_FRAMES=900 COV_WINDOW=300 timeout 600 $M vt1200 -rompath harness/mame/roms -video none \
|
||||
-sound none -nothrottle -debug -debugger none -autoboot_script harness/mame/coverage.lua -autoboot_delay 0 2>&1 \
|
||||
| grep -E "^frame" | tail -1
|
||||
done
|
||||
```
|
||||
|
||||
Expected: this is an experiment, not a pass/fail — but record which value moves the firmware furthest (most `total_distinct`, or a park point outside the blit loop).
|
||||
|
||||
- [ ] **Step 3: Check whether `INTENB` ever becomes non-zero**
|
||||
|
||||
Run `harness/mame/intr.lua` with the best sweep setting from Step 2.
|
||||
Expected: if `INTENB CHANGED -> <non-zero>` appears, the firmware has passed into interrupt setup — record it, because that is the level-0 boundary. If it stays `0000`, record that too; it means the model is still missing an input.
|
||||
|
||||
- [ ] **Step 4: Record findings**
|
||||
|
||||
Append a section to `harness/mame/BRINGUP_LOG.md` titled
|
||||
"Register model (2026-10-16)" with: the sweep table, the `INTENB` result, and which
|
||||
window/offset still looks unmodelled.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add harness/mame/vt1200.cpp harness/mame/BRINGUP_LOG.md
|
||||
git commit -m "harness: configurable board model busy timings + findings"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-review
|
||||
|
||||
**Spec coverage:** Task 1 establishes the two-shape structure and the unit-test harness; Task 2 covers the `big` shape (`0x6200000`/`0x6400000` with STATUS `+0x10`/CMD `+0x20`); Task 3 covers the `dma` pair (`0x5800000`/`0x5900000`); Task 4 turns env knobs into configuration, which the spec explicitly requires, and its sweep answers the "which values are right" question with data rather than a guess. The spec's *video RAM + renderer* and *DUART/NVRAM* items are deliberately **not** in this plan — they are the two follow-on plans named in the File Structure table above.
|
||||
|
||||
**Placeholder scan:** no TBD/TODO; every code step contains real code; the sweep in Task 4 Step 2 is an experiment by design and its expected-result line says so.
|
||||
|
||||
**Type consistency:** `set_window_busy_ticks(uint32_t, int)` (Task 2) is used unchanged in Task 4; `set_dma_ticks(int)` (Task 3) likewise; the constants `VT1200_WIN0..3` are defined in Task 1 and used in Tasks 2-4 with the same names.
|
||||
|
||||
---
|
||||
|
||||
## Execution note
|
||||
|
||||
The repository is **not** under git (`git status` reports no repo), so the `git add`/`git
|
||||
commit` steps will fail until it is initialised. Execute them only if the user initialises
|
||||
a repository; otherwise treat "Commit" as "stop here and report", and rely on the
|
||||
BRINGUP_LOG for the audit trail.
|
||||
@@ -0,0 +1,136 @@
|
||||
# VT1200 board model — design
|
||||
|
||||
Date: 2026-10-16
|
||||
Status: design approved in conversation; spec written for review
|
||||
|
||||
## Goal
|
||||
|
||||
Get the MAME VT1200 driver through **level 0 and level 1 power-up self-tests to the
|
||||
Terminal Manager window**, with the screen rendering — so the emulator can be compared
|
||||
against the real machine (font table, then the pie graph, then Terminal Manager).
|
||||
|
||||
Level 1 is the smallest scope that produces something *verifiable*. Level 0 alone risks
|
||||
declaring victory on a subtle fake; a fully usable terminal (LANCE + LAT + X sessions) is
|
||||
a larger project that this one is a prerequisite for anyway.
|
||||
|
||||
## Evidence this design rests on
|
||||
|
||||
* The firmware polls a **status bit** and writes a **command**, and the routine that does
|
||||
it takes the **window base as an argument** (bit `0x3FA911C0`), i.e. the windows are
|
||||
driven generically.
|
||||
* Observed layout, from `VT1200_BOARD_LOG` plus disassembly:
|
||||
|
||||
| offset | role | evidence |
|
||||
|---|---|---|
|
||||
| `+0x10` | STATUS, bit 0 = ready/done; bit 2 tested after `FILL XY` | gate 1 poll `while(!(read8(base+0x10) & 1)) delay(100)` |
|
||||
| `+0x20` | COMMAND / DATA, written in a delayed sequence | `move a9,*a11(0x20),0` then `delay(10)`, values 0xCA/0xEA/0x3A/... |
|
||||
| `+0xD0` | mixed I/O — bits 4-6 = **monitor switch** (input), rest latched output | the monitor-switch check at `0x3F2C8320` |
|
||||
|
||||
* Four windows exist: `0x5800000`, `0x5900000`, `0x6200000`, `0x6400000`. All are in the
|
||||
harness's address units (disassembly operand `>> 4`).
|
||||
* The RAM dump contains ROM strings and ROM pointers — the firmware appears to run from a
|
||||
**RAM copy** of the image.
|
||||
* Hardware, confirmed from the service guide, the wiki part number (`TMS34010FNL-50`) and
|
||||
a readable motherboard photo:
|
||||
* CPU **TMS34010FNL-50 = 50 MHz**; display **1280x1024 @ 72 Hz**, mono, 1 bpp.
|
||||
* Main RAM is **CY7C109 SRAM** (~1 MB on the system logic board; 4 MB total).
|
||||
* Video memory is a **separate bank of Toshiba TC514256 DRAMs** — the framebuffer is
|
||||
*not* in main RAM, which is why a RAM dump never located it.
|
||||
* Level 0 = 8 tests within 5 s (incl. interrupt, video control register, I/O data path,
|
||||
illegal monitor, LANCE-not-reset). Level 1 = 15 tests within 25 s (incl. video RAM,
|
||||
video, NVR, Ethernet address ROM, DUART1/2 + timers, host/printer/keyboard/mouse ports,
|
||||
keyboard, mouse). Level 1 begins with on-screen patterns, then a **pie graph**.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
vt1200_state (driver)
|
||||
|- TMS34010 50 MHz, 16 pixels/clock (exists)
|
||||
|- screen / palette needs a real renderer (exists, incomplete)
|
||||
|- main RAM 4 MiB (exists)
|
||||
|- vt1200_board NEW: owns all four windows
|
||||
| window[0] 0x5800000 <- register file, common layout
|
||||
| window[1] 0x5900000 <- +0x10 STATUS
|
||||
| window[2] 0x6200000 <- +0x20 COMMAND/DATA
|
||||
| window[3] 0x6400000 <- +0xD0 mixed I/O
|
||||
|- video RAM NEW: separate memory, display scans from here
|
||||
\- (later) mc68681 x2, am79c90, NVRAM, MAC ROM as SIBLING devices
|
||||
```
|
||||
|
||||
Decisions:
|
||||
|
||||
1. **The board device owns no other device.** DUARTs, LANCE, NVRAM and the MAC ROM are
|
||||
siblings wired alongside, so each is independently testable and addable one at a time.
|
||||
2. **Status bits are state, not constants.** A write to `+0x20` arms a small state
|
||||
machine; `+0x10` reports its progress. Forcing constants merely relocates the wedge
|
||||
(demonstrated twice: gates 3 and 4).
|
||||
3. **Test-time env knobs become configuration.** `VT1200_BOARD_SWITCH` etc. become
|
||||
constructor/config parameters, so the model is real rather than a debugging patch.
|
||||
The env-var behaviour stays available for experiments.
|
||||
|
||||
## Risk, and the step this design takes first
|
||||
|
||||
The weakest link was the claim that **all four windows share one layout**. It was
|
||||
validated before construction (see result below), and it is **half true** — there are
|
||||
**two shapes, not one**.
|
||||
|
||||
### Validation result (2026-10-16)
|
||||
|
||||
Every absolute reference to each window, by offset:
|
||||
|
||||
```
|
||||
W2 0x6200000: +00(36) +10(8) +20(1) +30(9) +D0(15) +E0(5) +F0(8)
|
||||
W3 0x6400000: +00(18) +D0(29) +E0(9) +F0(17)
|
||||
W0 0x5800000: +00(24) +20(1) +D0(1)
|
||||
W1 0x5900000: +00(12) +10(1)
|
||||
```
|
||||
|
||||
* **W2 and W3 share a common register set** — identical offsets `+0xD0`, `+0xE0`,
|
||||
`+0xF0`, and both have a `+0x10` STATUS / `+0x20` COMMAND pair. This is the shape the
|
||||
design was built on, and it holds.
|
||||
* **W0 and W1 are a different, simpler shape** — dominated by `+0x00` (24 and 12 sites),
|
||||
with almost nothing at the higher offsets. That matches the disassembly, where
|
||||
`0x5800000`/`0x5900000` act as a **command/data pair at the window base** feeding a
|
||||
byte-addressed (DMA-style) device.
|
||||
* `+0xD0` is referenced at file offset `0x12` — inside the **reset vector region**, which
|
||||
corroborates the service guide's statement that the monitor switch is read only during
|
||||
power-up.
|
||||
|
||||
**Consequence for the design:** two register-file classes, not one.
|
||||
|
||||
```
|
||||
vt1200_win_big (W2 0x6200000, W3 0x6400000) -> STATUS +0x10, CMD +0x20,
|
||||
+0x30, +0xD0 (mixed I/O), +0xE0, +0xF0
|
||||
vt1200_win_pair (W0 0x5800000, W1 0x5900000) -> command/data pair at the base,
|
||||
bit->byte (DMA-style) addressing
|
||||
```
|
||||
|
||||
The "status bits are state machines" and "env knobs become configuration" decisions are
|
||||
unchanged. This is a much cheaper discovery than it would have been after building a
|
||||
single generic class.
|
||||
|
||||
## Scope
|
||||
|
||||
**In:** the four windows as a modelled register file; status state machines; video RAM so
|
||||
the display can render; the two DUARTs and NVRAM/MAC ROM as far as level 1 requires; the
|
||||
monitor switch as configuration; a scanline renderer producing a real image.
|
||||
|
||||
**Out (for now):** a working LANCE/TCP-IP/LAT stack; keyboard and mouse *input* beyond
|
||||
what the level-1 tests probe; sound; bit-accurate peripheral timings.
|
||||
|
||||
## Validation
|
||||
|
||||
* Per-register: write a command, assert the status machine reaches "done".
|
||||
* Per-level: the firmware prints its own progress. Success at level 0 is the firmware
|
||||
moving past the 8 tests; at level 1, reaching the patterns/pie graph/Terminal Manager.
|
||||
* **Visual oracle:** the rendered screen should match the real boot sequence — a
|
||||
character/font table, then a large circle (the pie graph) — which we have on video.
|
||||
* Regression: with the model disabled, the driver must still reproduce the original
|
||||
wedge at `PC=3FDCE400`.
|
||||
|
||||
## Success criteria
|
||||
|
||||
1. ~~The shared-layout hypothesis is confirmed or refuted, with evidence.~~
|
||||
**DONE 2026-10-16 — half confirmed: two shapes (W2/W3 common; W0/W1 a command/data pair).**
|
||||
2. `INTENB` becomes non-zero and level 0 completes.
|
||||
3. A rendered frame shows the boot patterns, comparable against the real machine.
|
||||
Reference in new issue
Block a user