mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
harness: extract VT1200 board register model, unit-tested
This commit is contained in:
1 parent
acb92d3e44
commit
43f69738f4
6 files changed
+235
-89
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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--;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user