diff --git a/harness/mame/BRINGUP_LOG.md b/harness/mame/BRINGUP_LOG.md index 046f326..bca8067 100644 --- a/harness/mame/BRINGUP_LOG.md +++ b/harness/mame/BRINGUP_LOG.md @@ -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 `::` 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. diff --git a/harness/mame/build.sh b/harness/mame/build.sh index 07737b8..d84f238 100755 --- a/harness/mame/build.sh +++ b/harness/mame/build.sh @@ -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 diff --git a/harness/mame/test_vt1200_board.cpp b/harness/mame/test_vt1200_board.cpp new file mode 100644 index 0000000..2c8403d --- /dev/null +++ b/harness/mame/test_vt1200_board.cpp @@ -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 +#include + +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; +} diff --git a/harness/mame/vt1200.cpp b/harness/mame/vt1200.cpp index 83c4de0..7c313a8 100644 --- a/harness/mame/vt1200.cpp +++ b/harness/mame/vt1200.cpp @@ -59,6 +59,8 @@ #include #include +#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 m_palette; required_shared_ptr m_mainram; - std::map m_override; // absolute addr -> fixed value - std::map m_store; // absolute addr -> last written value - std::map, 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, 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-.bin so the framebuffer can be located offline. long m_dump_ram = -1; bool m_dumped_ram = false; - - // VT1200_BOARD_SWITCH=:: - // Bits in read as (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) diff --git a/harness/mame/vt1200_board.cpp b/harness/mame/vt1200_board.cpp new file mode 100644 index 0000000..ae71648 --- /dev/null +++ b/harness/mame/vt1200_board.cpp @@ -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--; +} diff --git a/harness/mame/vt1200_board.h b/harness/mame/vt1200_board.h new file mode 100644 index 0000000..7a69e0f --- /dev/null +++ b/harness/mame/vt1200_board.h @@ -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 +#include + +// 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 m_store; // write-back latches + std::map 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; +};