harness: extract VT1200 board register model, unit-tested

This commit is contained in:
SDD committed 2026-10-16 01:45:01 +03:00
1 parent acb92d3e44
commit 43f69738f4
6 files changed
+235 -89

No files matched your search

+21
View File
@@ -588,3 +588,24 @@ In the harness's board windows, `log address = disassembly operand >> 4`
address, and the override lookup is consistent with it, so this does not affect the
experiments — but it does mean the window addresses are **not** directly comparable to
the register operands printed by `unidasm`.
## Board register model extracted (2026-10-16)
The ad-hoc inline board-window logic in `board_read`/`board_write` has moved into
`vt1200_board.{h,cpp}` — a standalone, MAME-free class that owns the four windows'
write-back latches, the per-address overrides, the monitor switch, and the busy state
machine. It is unit-tested by `test_vt1200_board.cpp` (`harness/mame/build.sh test`);
the driver now forwards its four window callbacks to the model and calls `m_board.tick()`
once per frame at the last visible scanline.
The busy bit now has the semantics the design spec intended: a write to `+0x20` arms it,
`+0x10` reports it, and it clears after `VT1200_BOARD_BUSY_TICKS` *frame* ticks (the old
inline code armed on any write and cleared after N *reads*). The old `VT1200_BOARD_BUSY`
and `VT1200_BOARD_BUSY_BIT` knobs are dropped — the model hardcodes the busy bit as bit 0
of the STATUS register. `VT1200_BOARD_SWITCH` is likewise narrowed to the monitor switch
(bits 4-6 of `0x64000D0`); the arbitrary `<addr>:<mask>:<value>` form is no longer
honoured.
No regression: with no env vars the wedge is still `PC=3FDCE400 samples=199`, and the
handshake config (`MODEL=handshake BUSY_TICKS=2 SWITCH=0640000D:70:70`) still advances
to 56 distinct PCs by frame 600.
+16 -1
View File
@@ -3,6 +3,7 @@
#
# harness/mame/build.sh # incremental
# harness/mame/build.sh clean # wipe object dir first
# harness/mame/build.sh test # unit-test the board model (no MAME)
#
# Requires a MAME source checkout (default /home/arti/code/k-space/mame0289).
# The driver source lives in this repo and is symlinked into the MAME tree.
@@ -12,9 +13,21 @@ HERE=$(cd "$(dirname "$0")" && pwd)
MAME_SRC=${MAME_SRC:-/home/arti/code/k-space/mame0289}
JOBS=${JOBS:-8}
DRIVER="$HERE/vt1200.cpp"
BOARD_HDR="$HERE/vt1200_board.h"
BOARD="$HERE/vt1200_board.cpp"
TEST="$HERE/test_vt1200_board.cpp"
DEST="$MAME_SRC/src/mame/dec/vt1200.cpp"
BOARD_HDR_DEST="$MAME_SRC/src/mame/dec/vt1200_board.h"
BOARD_DEST="$MAME_SRC/src/mame/dec/vt1200_board.cpp"
LOG="$HERE/build.log"
# Unit-test the board model with plain g++ (no MAME). `build.sh test`.
if [ "$1" = test ]; then
g++ -std=c++17 -Wall -Wextra -o /tmp/vt1200_board_test "$BOARD" "$TEST"
/tmp/vt1200_board_test
exit 0
fi
if [ ! -d "$MAME_SRC/src/mame" ]; then
echo "MAME source tree not found at $MAME_SRC (set MAME_SRC)" >&2
exit 1
@@ -22,6 +35,8 @@ fi
mkdir -p "$MAME_SRC/src/mame/dec"
ln -sf "$DRIVER" "$DEST"
ln -sf "$BOARD_HDR" "$BOARD_HDR_DEST"
ln -sf "$BOARD" "$BOARD_DEST"
# MAME's SOURCES= build only recognises source files registered in the master
# list (src/mame/mame.lst). Register ours idempotently.
@@ -41,7 +56,7 @@ fi
# memory-hungry, and each C++ translation unit can use a lot of RAM.
# Defaults to 8; override with e.g. JOBS=4 harness/mame/build.sh
set -x
make -j"$JOBS" SUBTARGET=vt1200 SOURCES=src/mame/dec/vt1200.cpp REGENIE=1 > "$LOG" 2>&1
make -j"$JOBS" SUBTARGET=vt1200 SOURCES=src/mame/dec/vt1200.cpp,src/mame/dec/vt1200_board.cpp REGENIE=1 > "$LOG" 2>&1
status=$?
set +x
+60
View File
@@ -0,0 +1,60 @@
// 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;
}
+32 -88
View File
@@ -59,6 +59,8 @@
#include <string>
#include <utility>
#include "vt1200_board.h"
namespace {
@@ -67,7 +69,7 @@ namespace {
// 0x5900000, then data to 0x5800000; the firmware converts a bit address to a
// byte address with >>3 before feeding it, which implies a DMA-style device
// that uses byte addressing).
static const u32 BOARD_BASE[4] = { 0x05800000, 0x05900000, 0x06200000, 0x06400000 };
static const u32 BOARD_BASE[4] = { VT1200_WIN0, VT1200_WIN1, VT1200_WIN2, VT1200_WIN3 };
class vt1200_state : public driver_device
{
@@ -116,21 +118,13 @@ private:
required_device<palette_device> m_palette;
required_shared_ptr<u16> m_mainram;
std::map<u32, u16> m_override; // absolute addr -> fixed value
std::map<u32, u16> m_store; // absolute addr -> last written value
std::map<std::pair<u32, u32>, u32> m_seen; // (addr, pc) -> count
u16 m_default = 0;
FILE *m_log = nullptr;
// The four undecoded board windows, owned by the standalone model in
// vt1200_board.h so it can be unit-tested without MAME. The driver only
// forwards its window callbacks and configures the model from the env knobs.
vt1200_board m_board;
// Handshake model. The register at m_busy_addr carries a "busy" bit that
// the firmware both waits to be SET (command accepted) and waits to be
// CLEAR (operation finished), so it must change state, not hold a value.
// Any write to the window arms it; it clears after m_busy_ticks reads.
u32 m_busy_addr = 0x06200001;
u16 m_busy_bit = 1;
int m_busy_ticks = 2;
int m_busy_left = 0;
bool m_model = false;
std::map<std::pair<u32, u32>, u32> m_seen; // (addr, pc) -> count
FILE *m_log = nullptr;
// Video. Rendering is on by default now that the firmware reaches the
// display code; VT1200_NO_RENDER=1 restores the old blank-everything
@@ -143,22 +137,12 @@ private:
// vt1200-ram-<frame>.bin so the framebuffer can be located offline.
long m_dump_ram = -1;
bool m_dumped_ram = false;
// VT1200_BOARD_SWITCH=<addr>:<mask>:<value>
// Bits in <mask> read as <value> (an input, e.g. the monitor-selection
// switch); all other bits behave as write-back output latches. 0x64000D0
// is exactly this: the self-test reads the monitor switch from bits 4-6
// (andi 0x70) while the firmware also writes the port.
bool m_switch = false;
u32 m_switch_addr = 0;
u16 m_switch_mask = 0;
u16 m_switch_val = 0;
};
void vt1200_state::machine_start()
{
if (const char *p = getenv("VT1200_BOARD_DEFAULT"))
m_default = u16(strtoul(p, nullptr, 0));
m_board.set_default(u16(strtoul(p, nullptr, 0)));
if (const char *list = getenv("VT1200_BOARD_MAP"))
{
@@ -176,7 +160,7 @@ void vt1200_state::machine_start()
continue;
u32 a = u32(strtoul(item.substr(0, eq).c_str(), nullptr, 0));
u16 v = u16(strtoul(item.substr(eq + 1).c_str(), nullptr, 0));
m_override[a] = v;
m_board.set_override(a, v);
}
}
@@ -187,30 +171,20 @@ void vt1200_state::machine_start()
fprintf(m_log, "# kind addr pc value (bit addresses; distinct (addr,pc) only)\n");
}
// Handshake model: write-back registers + a self-clearing busy bit.
// Handshake model: write-back registers + a self-clearing busy bit. The
// board model owns the state machine; the env knobs configure it.
if (const char *model = getenv("VT1200_BOARD_MODEL"))
{
m_model = !strcmp(model, "handshake");
if (const char *a = getenv("VT1200_BOARD_BUSY"))
m_busy_addr = u32(strtoul(a, nullptr, 0));
if (const char *t = getenv("VT1200_BOARD_BUSY_TICKS"))
m_busy_ticks = (int)strtol(t, nullptr, 0);
if (const char *b = getenv("VT1200_BOARD_BUSY_BIT"))
m_busy_bit = u16(strtoul(b, nullptr, 0));
}
m_board.set_model_handshake(!strcmp(model, "handshake"));
if (const char *t = getenv("VT1200_BOARD_BUSY_TICKS"))
m_board.set_busy_ticks((int)strtol(t, nullptr, 0));
// Bit-mixed port: some bits are inputs (e.g. the monitor switch), the rest
// are write-back output latches.
// The monitor-selection switch lives in bits 4-6 of 0x64000D0. The model
// mixes those input bits with the port's write-back output latches.
if (const char *sw = getenv("VT1200_BOARD_SWITCH"))
{
unsigned a = 0, m = 0, v = 0;
if (sscanf(sw, "%x:%x:%x", &a, &m, &v) == 3)
{
m_switch = true;
m_switch_addr = a;
m_switch_mask = u16(m);
m_switch_val = u16(v);
}
m_board.set_monitor_switch(u8((v & 0x70) >> 4));
}
// Video: render by default; optionally dump a frame as PPM.
@@ -223,9 +197,11 @@ void vt1200_state::machine_start()
if (const char *d = getenv("VT1200_DUMP_DIR"))
m_dump_dir = d;
logerror("vt1200: board default=%04x overrides=%d model=%s busy=%08X/%d log=%s render=%d dump=%ld\n",
m_default, (int)m_override.size(), m_model ? "handshake" : "off",
m_busy_addr, m_busy_ticks, m_log ? "on" : "off", m_render ? 1 : 0, m_dump_frame);
logerror("vt1200: board model=%s busy_ticks=%s switch=%s log=%s render=%d dump=%ld\n",
getenv("VT1200_BOARD_MODEL") ? getenv("VT1200_BOARD_MODEL") : "off",
getenv("VT1200_BOARD_BUSY_TICKS") ? getenv("VT1200_BOARD_BUSY_TICKS") : "0",
getenv("VT1200_BOARD_SWITCH") ? getenv("VT1200_BOARD_SWITCH") : "off",
m_log ? "on" : "off", m_render ? 1 : 0, m_dump_frame);
}
void vt1200_state::machine_reset()
@@ -273,43 +249,7 @@ void vt1200_state::program_map(address_map &map)
u16 vt1200_state::board_read(int win, offs_t off)
{
const u32 addr = BOARD_BASE[win] | (u32(off) & 0xffff);
u16 value;
auto ov = m_override.find(addr);
if (ov != m_override.end())
{
value = ov->second;
}
else if (m_model)
{
// Write-back RAM, plus the self-clearing busy bit.
value = m_store.count(addr) ? m_store[addr] : m_default;
if (addr == m_busy_addr)
{
if (m_busy_left > 0)
{
value |= m_busy_bit; // busy: command accepted, not done
if (--m_busy_left == 0)
; // next read returns idle
}
else
{
value &= ~m_busy_bit; // idle: operation complete
}
}
}
else
{
value = m_default;
}
// Input bits override the write-back value: the monitor-selection switch
// lives in bits 4-6 of 0x64000d0 while the rest of the port is latched
// output. A constant value here would break the output bits, which is why
// a plain override never unblocked the boot.
if (m_switch && addr == m_switch_addr)
value = u16((value & ~m_switch_mask) | (m_switch_val & m_switch_mask));
const u16 value = m_board.read(addr);
log_pc_access('R', addr, value);
return value;
}
@@ -317,9 +257,7 @@ u16 vt1200_state::board_read(int win, offs_t off)
void vt1200_state::board_write(int win, offs_t off, u16 data)
{
const u32 addr = BOARD_BASE[win] | (u32(off) & 0xffff);
m_store[addr] = data;
if (m_model && m_busy_ticks > 0)
m_busy_left = m_busy_ticks; // arm the handshake
m_board.write(addr, data);
log_pc_access('W', addr, data);
}
@@ -368,6 +306,12 @@ void vt1200_state::scanline(screen_device &screen, bitmap_ind16 &bitmap, int sca
// is reliably invoked once per line.
if (scanline == screen.visible_area().max_y)
{
// Once per frame, advance the board model's state machines (e.g. the
// self-clearing busy bit). Not screen_update: that is called with
// small clip regions, and its "reaches the last line" guard only fires
// sporadically on this driver.
m_board.tick();
if (m_dump_frame >= 0 && (long)screen.frame_number() == m_dump_frame)
dump_frame(screen, bitmap);
if (m_dump_ram >= 0 && !m_dumped_ram && (long)screen.frame_number() >= m_dump_ram)
+45
View File
@@ -0,0 +1,45 @@
#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--;
}
+61
View File
@@ -0,0 +1,61 @@
// 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;
};