Files
vt1200/harness/mame/vt1200.cpp
T
SDD f7caf9aed8 harness: opt-in VT1200_IO_PATH_STUB stand-in for the level-0 keyboard-port test fixture
The level-0 I/O data path test writes one 0xE1 byte to DUART2-A THRA and
demands exactly 10 bytes back; it checks only the count and never compares
the content, and no documented keyboard/mouse/tablet message is 10 bytes.
Add an opt-in stand-in that watches for that 0xE1 write and feeds 10
arbitrary (0x00) bytes on the channel's receive path via the SCN2681's
public rx_fifo_push(), refilling the 4-deep FIFO on each RHRA read.

This is explicitly NOT a keyboard model: it stands in for an unknown test
fixture on the keyboard port. It replies only while the channel is polled
(IMR=0x00), so it raises no interrupt; with the stub on the run clears
level 0 and reaches level 1's HOST PORT - 15, but still no OS (SKIP_LEVEL1
remains the only route to X11). Default is unchanged: no env still fails
level 0 honestly (progress.lua samples=198/3 PCs); stub-on is 53/75 PCs.
See harness/mame/README.md.
2026-10-16 14:31:20 +03:00

1089 lines
45 KiB
C++

// license:BSD-3-Clause
// copyright-holders: VT1200 reverse-engineering project
/***************************************************************************
DEC VT1200 X terminal -- MAME execution harness
This is NOT an accurate hardware emulation. It exists to give us a way
to *execute* the VT1200 firmware under the TMS34010 interpreter with a
debugger (breakpoints, single step, memory watch, trace).
What is modelled BY DEFAULT. A bare `vt1200 vt1200` boots the way the
old six-variable incantation
VT1200_VRAM=1 VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2
VT1200_BOARD_SWITCH="0640000D:70:70" VT1200_DUART=1 VT1200_DUART_SEL=1
did. Five of those knobs are now the default and no longer exist; the
sixth, VT1200_BOARD_SWITCH, was superseded once DUART1's input port
(IP4-IP6) became the monitor-switch source.
* TMS34010 CPU (bit-addressed: MAME's tms34010 memory space uses a
right shift of 3, so address-map units are BITS).
* RAM at logical bit address 0, 4 MiB (byte 0 .. 0x3fffff).
* Video RAM at bit 0x04000000 (required for the level-1 VIDEO MEMORY
test to pass).
* The four undecoded board windows, driving the handshake register model
with a two-tick busy bit (required to leave early POST).
* MAME's SCN2681 at DUART1 (0x0640000 = host/printer), with its input
port IP4-IP6 held high -- which is how the driver reads the VR150
monitor switch (value 0x70). The level-1 host-port transmit is
ISR-driven, so a real DUART is required.
* The 1.75 MiB firmware ROM (physical byte base 0x07e40000 = bit
0x3f200000), mirrored into the three upper 1 GiB quadrants so that
the reset-vector page at logical bit 0xffffffe0 resolves to the last
four bytes of the image (file offset 0x1bfffc).
* MAME's SCN2681 at DUART2 (0x0620000 = keyboard (ch A) / mouse (ch B)),
wired to INT2. This is modelled unconditionally because the real
board has it and the OS's DUART interrupt dispatcher needs a real
chip. NOTE: with a real DUART2 and nothing attached to the keyboard
port, the *faithful* (no-env) boot now fails the level-0 I/O data path
test and drops into the interactive diagnostics menu; before DUART2
was modelled the board write-back latch accidentally faked that test
(see OS_BRINGUP_REPORT.md). VT1200_IO_PATH_STUB=1 adds an OPT-IN
stand-in for the unknown test fixture on that port (it is NOT a
keyboard model) so the count-only level-0 test can pass; the default
stays faithful and still fails.
What is NOT modelled: LANCE Ethernet and the board system logic beyond the
register model. The firmware therefore still stops at the level-1 HOST
PORT test and wedges in the system-board LED loop; that is expected.
---------------------------------------------------------------------
Instrumentation knobs (all opt-in; none changes execution):
VT1200_BOARD_LOG=<path> append a line for every *distinct*
(address, PC) board access, flushed
immediately. Shows every register the
firmware polls and the PC polling it.
VT1200_BOARD_MAP=<list> per-address overrides, e.g.
"0x06200010=1;0x0640000d=0"
VT1200_DUARTLOG=<path> every DUART access (not de-duplicated),
so a write that happens once per iteration
cannot hide.
VT1200_IRQLOG=<path> DUART1 IRQ assert: ISR/SRA at the moment of
the interrupt.
VT1200_LOG_UNMAPPED=<path> catch-all over the whole address space;
anything the driver does not model is
logged once per distinct (address, PC,
direction).
VT1200_DPYLOG=<path> dump the TMS34010 display params, one line
per scanline, for the first
VT1200_DPYLOG_FRAMES frames (default 1).
VT1200_DUMP_FRAME=<n> write frame <n> as vt1200-frame-<n>.ppm
VT1200_DUMP_RAM=<n> write main RAM at frame <n> as
vt1200-ram-<n>.bin
VT1200_DUMP_VRAM=<n> write video memory at frame <n> as
vram-<n>.bin
VT1200_DUMP_FRAMES=<list> comma-separated frames, all in ONE run, so the
same machine can be sampled at several
increasing times and the samples compared.
Same for VT1200_DUMP_RAM_FRAMES and
VT1200_DUMP_VRAM_FRAMES.
VT1200_DUMP_DIR=<dir> directory for the dumps (default ".")
VT1200_NO_RENDER=1 blank the screen instead of painting it
Behaviour switch (deliberately diverges from the real terminal):
VT1200_SKIP_LEVEL1=1 drive DUART1's input-port bit 3 high. The
real terminal reads IP3 low, so with this
UNSET the boot runs the whole level-1
self-test batch (the faithful default).
With it set, the boot gate at bit
0x3F2042D0 (`@0x64000D0` bit 3 set and the
0x6200000 status handshake not 0x55)
branches to the OS cold-start 0x3F5788C0,
skipping ALL of the level-1 batch. Because
the OS phase is then in charge, the switch
ALSO routes DUART1's IRQ to INT2 (line 1):
the OS's DUART1 driver attaches there,
while POST's DUART dispatcher sits on INT1
(line 0), the default. Both lines are
enabled (INTENB = 0x0606) and the RAM stub
at 0x0F60 is rewritten between the two
phases, so the two wirings correspond to
the two phases' own routings. See
reference/level1-bypass-analysis.md.
VT1200_IO_PATH_STUB=1 feed 10 bytes into DUART2 channel A after
the level-0 I/O data path test writes 0xE1
to THRA. This is a STAND-IN for an unknown
test fixture on the keyboard port -- it is
NOT a keyboard model, and the bytes are
arbitrary (0x00) because the firmware checks
only the COUNT (it stores the received byte
and never compares it, and it does not
enable loopback). No documented
keyboard/mouse/tablet message is 10 bytes
long, so no documented device can satisfy
this test; see reference/level0-10byte-puzzle.md.
The stub replies only while the channel is
polled (IMR = 0x00), so it cannot inject
into the interrupt-enabled OS phase or
raise an interrupt there. Unset (the
default) the level-0 test fails honestly
and the boot stops in the diagnostics
menu.
Video: the driver paints the active scanlines itself, reading the
framebuffer out of video RAM (see render_active_line).
See FULL_BOOT_ESTIMATE.md section 4b for measured results and
OS_BRINGUP_REPORT.md for the DUART2 / level-0 analysis.
***************************************************************************/
#include "emu.h"
#include "cpu/tms34010/tms34010.h"
#include "emupal.h"
#include "machine/mc68681.h"
#include "screen.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "vt1200_board.h"
namespace {
// The undecoded windows, in bit-address units. 0x5900000 is paired with
// 0x5800000 as a command/data register interface (write a command to
// 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] = { VT1200_WIN0, VT1200_WIN1, VT1200_WIN2, VT1200_WIN3 };
// --- Framebuffer geometry (measured, not derived from MAME) -----------------
// Video RAM is mapped at bit address 0x04000000 (the base that unblocks the
// level-1 VIDEO MEMORY test). The firmware's own blitter writes it as a
// linear 1-bit-per-pixel buffer with a 0x40-word (128-byte) row pitch; the
// TMS34010 paints the visible pixels LSB-first within each 16-bit word.
// Verified by rendering the dumped VRAM: at this geometry the level-1 error
// screen is legible ("LEVEL1/2 ERROR -> HOST PORT - 15"); at the derived
// 160-byte / 80-word pitch MAME computed from HEBLNK/HSBLNK/HTOTAL the text is
// sheared and unreadable. See RENDER_REPORT.md.
static const u32 VRAM_BASE_BITS = 0x04000000; // where the framebuffer lives
static const int VMEM_PITCH_BITS = 1024; // 0x40 words = 128 bytes
static const int VMEM_WIDTH = 1024; // visible pixels per row
static const int VMEM_HEIGHT = 1024; // rows in the framebuffer
class vt1200_state : public driver_device
{
public:
vt1200_state(const machine_config &mconfig, device_type type, const char *tag)
: driver_device(mconfig, type, tag)
, m_maincpu(*this, "maincpu")
, m_screen(*this, "screen")
, m_palette(*this, "palette")
, m_mainram(*this, "mainram")
, m_duart1(*this, "duart1")
, m_duart2(*this, "duart2")
, m_vram(*this, "vram")
{
}
void vt1200(machine_config &config);
virtual void machine_start() override ATTR_COLD;
virtual void machine_reset() override ATTR_COLD;
// scanline callback: renders (or blanks) the line, and can dump a frame
void scanline(screen_device &screen, bitmap_ind16 &bitmap, int scanline,
const tms340x0_device::display_params *params);
void render_active_line(bitmap_ind16 &bitmap, int scanline,
const tms340x0_device::display_params *params);
void dump_frame(screen_device &screen, bitmap_ind16 &bitmap, long frame);
void dump_ram(long frame);
void dump_vram(long frame);
// We drive the TMS34010 renderer ourselves so the frame dump does not
// depend on the device's scanline-callback plumbing.
uint32_t screen_update(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect);
// Board window access, one pair per window so the absolute address is
// unambiguous no matter whether MAME hands us an entry-relative or an
// absolute offset.
u16 board0_r(offs_t off); void board0_w(offs_t off, u16 data);
u16 board1_r(offs_t off); void board1_w(offs_t off, u16 data);
u16 board2_r(offs_t off); void board2_w(offs_t off, u16 data);
u16 board3_r(offs_t off); void board3_w(offs_t off, u16 data);
// The two DUARTs. DUART1 = host (ch A) + printer (ch B) at window
// 0x0640000; DUART2 = keyboard (ch A) + mouse (ch B) at window 0x0620000.
// Both are mapped unconditionally.
u16 duart1_r(offs_t off); void duart1_w(offs_t off, u16 data);
u16 duart2_r(offs_t off); void duart2_w(offs_t off, u16 data);
// DUART interrupt outputs -> the TMS34010's two external interrupt lines.
// The firmware installs RAM trampolines in the CPU's fixed vector table
// (INT1 at vector 0xffffffc0 -> stub at bit 0x0f60 = JA 0x3FA92F40, the
// DUART1 ISR; INT2 at 0xffffffa0 -> stub at bit 0x0f30 = JA 0x3FA93070,
// also a DUART1 entry). Without this wiring the level-1 host-port test's
// interrupt-driven transmit never runs (see DUART_REPORT.md).
void duart1_irq_w(int state);
void duart2_irq_w(int state);
private:
void program_map(address_map &map) ATTR_COLD;
u16 board_read(int win, offs_t off);
void board_write(int win, offs_t off, u16 data);
void log_pc_access(char kind, u32 addr, u16 value);
required_device<tms34010_device> m_maincpu;
required_device<screen_device> m_screen;
required_device<palette_device> m_palette;
required_shared_ptr<u16> m_mainram;
// The two DUARTs (Signetics SCN2681 / Motorola MC68681 family). The
// firmware's level-1 I/O tests drive them byte-per-register (register N at
// byte 2N, i.e. harness offset N), which MAME's 68681 model expects.
required_device<scn2681_device> m_duart1; // 0x0640000: host(A)/printer(B)
required_device<scn2681_device> m_duart2; // 0x0620000: keyboard(A)/mouse(B)
bool m_duart1_int2 = false; // set by VT1200_SKIP_LEVEL1
// VT1200_IO_PATH_STUB=1: opt-in stand-in for the unknown test fixture on
// the keyboard port, so level-0's count-only I/O data path test can pass.
// NOT a keyboard model. See iopath_stub_arm() and the README.
bool m_iopath_stub = false;
int m_iopath_supply = 0; // bytes still to feed for the current 0xE1
u8 m_duart2_imr = 0; // last value written to DUART2 IMR (reg 5)
bool m_iopath_irq_reported = false;
void iopath_stub_arm(); // on the level-0 0xE1 THRA write
void iopath_stub_refill(); // after a channel-A RHRA read
// 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;
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
// behaviour. VT1200_DUMP_FRAME=<n> writes that frame as a PPM.
//
// The *_FRAMES variants take a comma-separated list and dump at EXACTLY
// each frame, so one run can sample the same machine at several increasing
// times -- the instrument for telling "progressing" from "stuck": if two
// samples minutes apart are byte-identical, nothing is being drawn.
bool m_render = true;
long m_dump_frame = -1;
std::vector<long> m_dump_frames; // VT1200_DUMP_FRAMES=3000,10000,...
std::string m_dump_dir = ".";
// VT1200_DPYLOG=<path>: dump the TMS34010 display params for the first
// VT1200_DPYLOG_FRAMES frames (default 1), one line per visible scanline.
// Used to pin the VRAM fetch address/geometry against the real machine's
// 1280x1024 (160 bytes/line) numbers.
FILE *m_dpylog = nullptr;
long m_dpylog_frames = 1;
// VT1200_DUMP_RAM=<frame> writes main RAM at that frame to
// vt1200-ram-<frame>.bin so the framebuffer can be located offline.
long m_dump_ram = -1;
bool m_dumped_ram = false;
std::vector<long> m_dump_ram_frames; // VT1200_DUMP_RAM_FRAMES=...
// VT1200_DUMP_VRAM=<frame> writes the video memory to vram-<frame>.bin so the
// framebuffer layout can be determined offline.
long m_dump_vram = -1;
bool m_dumped_vram = false;
std::vector<long> m_dump_vram_frames; // VT1200_DUMP_VRAM_FRAMES=...
optional_shared_ptr<u16> m_vram;
// VT1200_LOG_UNMAPPED=<path>
// A catch-all mapping over the whole address space, registered FIRST so every
// real region added afterwards takes precedence. Anything the firmware
// touches that we do not model lands here, and is logged once per distinct
// (address, PC, direction). This is how we find regions we have never
// mapped -- the video memory in particular.
FILE *m_unmapped_log = nullptr;
std::map<std::pair<u32, u32>, u32> m_unmapped_seen; // (addr, pc|dir) -> count
u16 unmapped_r(offs_t off);
void unmapped_w(offs_t off, u16 data);
void log_unmapped(const char *kind, u32 addr, const char *pc);
// VT1200_IRQLOG=<path>: log DUART1's IRQ assertions (ISR/SRA at the
// moment of the interrupt).
FILE *m_irqlog = nullptr;
// VT1200_DUARTLOG=<path>
// EVERY DUART access (not de-duplicated), so a write that happens once per
// iteration cannot hide. Used to prove whether the transmit register is
// ever written on the host-port test path.
FILE *m_duartlog = nullptr;
void log_duart(int win, char kind, u32 reg, u16 value);
};
// Parse a comma-separated frame list ("3000,10000,30000,60000") into a vector,
// ignoring empty items. Used by the multi-timestamp dump knobs.
static std::vector<long> parse_frame_list(const char *s)
{
std::vector<long> out;
if (!s)
return out;
std::string str(s);
size_t pos = 0;
while (pos <= str.size())
{
const size_t comma = str.find(',', pos);
std::string item = str.substr(pos, (comma == std::string::npos)
? std::string::npos : comma - pos);
// trim surrounding whitespace
const size_t b = item.find_first_not_of(" \t");
const size_t e = item.find_last_not_of(" \t");
if (b != std::string::npos)
out.push_back(strtol(item.substr(b, e - b + 1).c_str(), nullptr, 0));
pos = (comma == std::string::npos) ? str.size() + 1 : comma + 1;
}
return out;
}
void vt1200_state::machine_start()
{
if (const char *list = getenv("VT1200_BOARD_MAP"))
{
std::string s(list), item;
size_t pos = 0;
while (pos <= s.size())
{
size_t semi = s.find(';', pos);
item = s.substr(pos, (semi == std::string::npos) ? std::string::npos : semi - pos);
pos = (semi == std::string::npos) ? s.size() + 1 : semi + 1;
if (item.empty())
continue;
size_t eq = item.find('=');
if (eq == std::string::npos)
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_board.set_override(a, v);
}
}
if (const char *path = getenv("VT1200_BOARD_LOG"))
{
m_log = fopen(path, "w");
if (m_log)
fprintf(m_log, "# kind addr pc value (bit addresses; distinct (addr,pc) only)\n");
}
// Handshake model: write-back registers + a self-clearing busy bit. The
// board model owns the state machine; these are the settled defaults (a
// two-tick busy bit is what leaves early POST).
m_board.set_model_handshake(true);
m_board.set_busy_ticks(2);
// Video: render by default; optionally dump a frame as PPM.
if (getenv("VT1200_NO_RENDER"))
m_render = false;
if (const char *f = getenv("VT1200_DUMP_FRAME"))
m_dump_frame = strtol(f, nullptr, 0);
if (const char *f = getenv("VT1200_DUMP_RAM"))
m_dump_ram = strtol(f, nullptr, 0);
if (const char *f = getenv("VT1200_DUMP_VRAM"))
m_dump_vram = strtol(f, nullptr, 0);
if (const char *f = getenv("VT1200_DUMP_FRAMES"))
m_dump_frames = parse_frame_list(f);
if (const char *f = getenv("VT1200_DUMP_RAM_FRAMES"))
m_dump_ram_frames = parse_frame_list(f);
if (const char *f = getenv("VT1200_DUMP_VRAM_FRAMES"))
m_dump_vram_frames = parse_frame_list(f);
if (const char *d = getenv("VT1200_DUMP_DIR"))
m_dump_dir = d;
if (const char *f = getenv("VT1200_DPYLOG"))
{
m_dpylog = fopen(f, "w");
if (m_dpylog)
fprintf(m_dpylog, "# line rowaddr coladdr yoff heblnk hsblnk veblnk vsblnk vcount\n");
}
if (const char *f = getenv("VT1200_DPYLOG_FRAMES"))
m_dpylog_frames = strtol(f, nullptr, 0);
if (const char *p = getenv("VT1200_LOG_UNMAPPED"))
{
m_unmapped_log = fopen(p, "w");
if (m_unmapped_log)
fprintf(m_unmapped_log,
"# kind addr pc -- accesses to memory the driver does NOT model\n"
"# address units are the address-map's; firmware operand = addr << 4\n");
}
if (const char *p = getenv("VT1200_IRQLOG"))
{
m_irqlog = fopen(p, "w");
if (m_irqlog)
fprintf(m_irqlog, "# DUART1 IRQ assert: ISR/SRA at the moment of the interrupt\n");
}
if (const char *p = getenv("VT1200_DUARTLOG"))
{
m_duartlog = fopen(p, "w");
if (m_duartlog)
fprintf(m_duartlog, "# win kind reg val pc -- every DUART access (not de-duplicated)\n");
}
// With the real DUART in place, board-level signals that used to be faked
// by the window model come from the DUART's input port (read register 0x0D:
// IP0-IP6, bit 7 always set). The monitor-selection switch is IP bits 4-6;
// driving them high selects the VR150 CRT (0x70), matching the value the
// firmware looks for. IP2 stays low (the value for the blit-status test at
// bit 0x3F204AA0).
m_duart1->ip4_w(1);
m_duart1->ip5_w(1);
m_duart1->ip6_w(1);
// VT1200_SKIP_LEVEL1=1 drives DUART1 IP3 high, which makes the boot gate at
// bit 0x3F2042D0 take the OS cold-start branch (0x3F5788C0) and skip the
// whole level-1 self-test batch. Since the OS phase is then in charge, the
// same switch routes DUART1's IRQ to INT2 (line 1): the OS's DUART1 driver
// attaches there, while POST's dispatcher sits on INT1 (line 0). Both
// lines are enabled (INTENB = 0x0606), so this selects which phase's
// routing applies.
if (getenv("VT1200_SKIP_LEVEL1"))
{
m_duart1->ip3_w(1);
m_duart1_int2 = true;
}
// VT1200_IO_PATH_STUB=1: feed >=10 bytes on DUART2-A after the level-0
// I/O data path test writes 0xE1 to THRA. Off by default: with it off the
// run still fails level 0 honestly and parks in the diagnostics menu.
if (getenv("VT1200_IO_PATH_STUB"))
m_iopath_stub = true;
logerror("vt1200: board model=handshake busy_ticks=2 switch=duart-ip4/5/6 log=%s render=%d dump=%ld\n",
m_log ? "on" : "off", m_render ? 1 : 0, m_dump_frame);
logerror("vt1200: duart1=on (SCN2681 @0640000 host/printer), duart2=on (SCN2681 @0620000 kbd/mouse), skip_level1=%s\n",
getenv("VT1200_SKIP_LEVEL1") ? "on (IP3 high, IRQ->INT2)" : "off (IP3 low, IRQ->INT1)");
logerror("vt1200: io_path_stub=%s\n", m_iopath_stub ? "on (DUART2-A reply to 0xE1)" : "off (faithful)");
}
void vt1200_state::machine_reset()
{
if (m_log)
fflush(m_log);
if (m_unmapped_log)
fflush(m_unmapped_log);
// The device resets IMR to 0; mirror that in our tracker and drop any
// in-flight stub reply.
m_duart2_imr = 0;
m_iopath_supply = 0;
m_iopath_irq_reported = false;
}
void vt1200_state::log_unmapped(const char *kind, u32 addr, const char *pc)
{
if (!m_unmapped_log)
return;
const u32 p = (u32)strtoul(pc, nullptr, 16);
const char d = (kind[0] == 'W') ? 1u : 0u;
auto key = std::make_pair(addr, (p << 1) | d);
auto it = m_unmapped_seen.find(key);
if (it == m_unmapped_seen.end())
{
m_unmapped_seen.emplace(key, 1);
fprintf(m_unmapped_log, "%s %08X pc=%08X\n", kind, addr, p);
fflush(m_unmapped_log);
}
}
u16 vt1200_state::unmapped_r(offs_t off)
{
char pc[16];
snprintf(pc, sizeof(pc), "%08X", (unsigned)m_maincpu->state_int(TMS34010_PC));
log_unmapped("R", (u32)off, pc);
return 0x0000;
}
void vt1200_state::unmapped_w(offs_t off, u16 data)
{
char pc[16];
snprintf(pc, sizeof(pc), "%08X", (unsigned)m_maincpu->state_int(TMS34010_PC));
log_unmapped("W", (u32)off, pc);
}
void vt1200_state::program_map(address_map &map)
{
// Catch-all for anything we do not model -- registered FIRST so every region
// below takes precedence. Anything still landing here is memory the
// firmware touches that the driver has never mapped; it is logged.
// NOTE: gated on the environment variable, not on m_unmapped_log, because
// program_map() runs at machine-configuration time and machine_start()
// (which opens the file) has not run yet.
if (getenv("VT1200_LOG_UNMAPPED"))
map(0x00000000, 0xffffffff).rw(FUNC(vt1200_state::unmapped_r), FUNC(vt1200_state::unmapped_w));
// Video memory, at bit 0x04000000. Located empirically and then corrected
// by a closed cross-check of two instruments: the catch-all's handler
// receives (bitaddr - base) >> 4, so its logged 0x400000..0x40D7FF means BIT
// 0x04000000..0x040D7FF0 == bytes 0x800000..0x81AFFE (~110 KB, about one
// 1280x1024 1bpp framebuffer). Mapping it is what lets the level-1 VIDEO
// MEMORY test's walking-bit read-back pass, so it is unconditional now.
map(VRAM_BASE_BITS, 0x1fffffff).ram().share("vram");
// Main RAM: 4 MiB (1 MiB onboard + 3 x 1 MiB SIMMs on the real machine).
// The TMS34010 is bit-addressed, so 4 MiB of bytes == 0x2000000 address
// units. (The earlier 0x200000 range modelled only 256 KiB, which is
// almost certainly why POST mis-detected memory.)
map(0x00000000, 0x01ffffff).ram().share("mainram");
// ROM: physical byte base 0x07e40000 -> bit base 0x3f200000, 0x1c0000 bytes
// (0xe00000 bit addresses), plus the three upper logical aliases that the
// TMS34010 generates because its top two address bits are not emitted.
map(0x3f200000, 0x3fffffff).rom().region("maincpu", 0);
map(0x7f200000, 0x7fffffff).rom().region("maincpu", 0);
map(0xbf200000, 0xbfffffff).rom().region("maincpu", 0);
map(0xff200000, 0xffffffff).rom().region("maincpu", 0);
// Undecoded board windows, driving the handshake register model. Always
// mapped: the busy handshake is what leaves early POST.
map(BOARD_BASE[0], BOARD_BASE[0] + 0xffff).rw(FUNC(vt1200_state::board0_r), FUNC(vt1200_state::board0_w));
map(BOARD_BASE[1], BOARD_BASE[1] + 0xffff).rw(FUNC(vt1200_state::board1_r), FUNC(vt1200_state::board1_w));
map(BOARD_BASE[2], BOARD_BASE[2] + 0xffff).rw(FUNC(vt1200_state::board2_r), FUNC(vt1200_state::board2_w));
map(BOARD_BASE[3], BOARD_BASE[3] + 0xffff).rw(FUNC(vt1200_state::board3_r), FUNC(vt1200_state::board3_w));
// MAME's SCN2681 at DUART1 (0x0640000 = host/printer), registered AFTER the
// board-window model so it wins the register slice (map precedence is
// last-wins); the model still serves any other register in the same window.
// Base in map units is the bit address; 16 registers at 2 bytes each = 32
// bytes = 0x100 bit units. DUART2 (0x0620000 = keyboard/mouse) is mapped
// the same way, unconditionally.
map(VT1200_WIN3, VT1200_WIN3 + 0xff).rw(FUNC(vt1200_state::duart1_r), FUNC(vt1200_state::duart1_w));
// DUART2 (0x0620000 = keyboard/mouse) is mapped unconditionally: the real
// board has it, and the OS's DUART dispatcher requires a real read-only ISR
// distinct from the write-only IMR (the board write-back latch aliased them,
// producing a phantom interrupt that wedged the OS at the X11 weave).
map(VT1200_WIN2, VT1200_WIN2 + 0xff).rw(FUNC(vt1200_state::duart2_r), FUNC(vt1200_state::duart2_w));
}
// MAME may hand us either an entry-relative offset (0..0xffff) or the absolute
// address; BOARD_BASE is 0x10000-aligned, so ORing the low 16 bits is correct
// in both cases.
u16 vt1200_state::board_read(int win, offs_t off)
{
const u32 addr = BOARD_BASE[win] | (u32(off) & 0xffff);
const u16 value = m_board.read(addr);
log_pc_access('R', addr, value);
return value;
}
void vt1200_state::board_write(int win, offs_t off, u16 data)
{
const u32 addr = BOARD_BASE[win] | (u32(off) & 0xffff);
m_board.write(addr, data);
log_pc_access('W', addr, data);
}
void vt1200_state::log_pc_access(char kind, u32 addr, u16 value)
{
if (!m_log)
return;
const u32 pc = u32(m_maincpu->state_int(TMS34010_PC));
auto key = std::make_pair(addr, pc);
auto it = m_seen.find(key);
if (it == m_seen.end())
{
m_seen.emplace(key, 1);
fprintf(m_log, "%c %08X pc=%08X val=%04X\n", kind, addr, pc, value);
fflush(m_log);
}
else if (kind == 'R')
{
it->second++;
}
}
u16 vt1200_state::board0_r(offs_t off) { return board_read(0, off); }
u16 vt1200_state::board1_r(offs_t off) { return board_read(1, off); }
u16 vt1200_state::board2_r(offs_t off) { return board_read(2, off); }
u16 vt1200_state::board3_r(offs_t off) { return board_read(3, off); }
void vt1200_state::board0_w(offs_t off, u16 data) { board_write(0, off, data); }
void vt1200_state::board1_w(offs_t off, u16 data) { board_write(1, off, data); }
void vt1200_state::board2_w(offs_t off, u16 data) { board_write(2, off, data); }
void vt1200_state::board3_w(offs_t off, u16 data) { board_write(3, off, data); }
// --- DUARTs ---------------------------------------------------------------
//
// Remap. The map entry base is a BIT address and the handler receives
// (bitaddr - base) >> 4, so byte offset within the window = off * 2. The
// observed access pattern is a canonical 68681 setup (CRA writes of
// 0x3A/0x2A/0x4A/0x1A, CSRA=0x88/0x99, MR1A/MR2A at off 0) and those offsets
// line up with the datasheet register map only when register index = off:
// byte 2N (harness off N) with the register in the low byte of the 16-bit
// access. So:
//
// firmware operand 0x064000D0 -> bit addr -> harness off 0x0D
// -> byte offset 0x1A = register 0x0D (IP)
//
// i.e. duart_byte = handler_off * 2 and duart_register = handler_off.
//
// Each window is 16 registers (32 bytes = 0x100 map units); the entry only
// covers that, so anything else in the window still falls through to the
// board model below it (a window may mix a DUART with other board registers).
// The firmware presents the register value in the LOW byte of its 16-bit
// move (the board log shows value 0x0088 for CSRA), hence `data & 0xff`.
u16 vt1200_state::duart1_r(offs_t off)
{
const u32 a = VT1200_WIN3 | (u32(off) & 0xffff);
if ((off & 0xffff) < 0x10)
{
const u16 v = m_duart1->read(u32(off) & 0xf);
log_pc_access('R', a, v);
log_duart(1, 'R', u32(off) & 0xf, v);
return v;
}
return board_read(3, off);
}
void vt1200_state::duart1_w(offs_t off, u16 data)
{
const u32 a = VT1200_WIN3 | (u32(off) & 0xffff);
if ((off & 0xffff) < 0x10)
{
m_duart1->write(u32(off) & 0xf, u8(data & 0xff));
log_pc_access('W', a, data);
log_duart(1, 'W', u32(off) & 0xf, data);
return;
}
board_write(3, off, data);
}
u16 vt1200_state::duart2_r(offs_t off)
{
const u32 a = VT1200_WIN2 | (u32(off) & 0xffff);
if ((off & 0xffff) < 0x10)
{
const u32 reg = u32(off) & 0xf;
const u16 v = m_duart2->read(reg);
// The stub tops the receive FIFO back up as RHRA (reg 3) is drained,
// so RXRDY stays asserted for all ten bytes (the FIFO is only 4 deep).
if (m_iopath_stub && reg == 3)
iopath_stub_refill();
log_pc_access('R', a, v);
log_duart(2, 'R', reg, v);
return v;
}
return board_read(2, off);
}
void vt1200_state::duart2_w(offs_t off, u16 data)
{
const u32 a = VT1200_WIN2 | (u32(off) & 0xffff);
if ((off & 0xffff) < 0x10)
{
const u32 reg = u32(off) & 0xf;
m_duart2->write(reg, u8(data & 0xff));
if (reg == 5)
m_duart2_imr = u8(data & 0xff);
// The level-0 I/O data path test's one data byte: 0xE1 to THRA. That
// is the trigger for the opt-in test-fixture stand-in (not a keyboard).
if (m_iopath_stub && reg == 3 && u8(data & 0xff) == 0xe1)
iopath_stub_arm();
log_pc_access('W', a, data);
log_duart(2, 'W', reg, data);
return;
}
board_write(2, off, data);
}
// --- VT1200_IO_PATH_STUB: the level-0 test-fixture stand-in -----------------
//
// The level-0 I/O data path test (L3FDCE470) writes ONE byte, 0xE1, to DUART2-A
// THRA and then requires EXACTLY 10 bytes to come back on the same channel. It
// never checks their content, never sets loopback, and transmits nothing else.
// No documented LK401/mouse/tablet message is 10 bytes long, and the routine's
// receiver-reset flushes the FIFO, so no documented device can satisfy it; see
// reference/level0-10byte-puzzle.md. This is therefore a stand-in for an
// UNKNOWN TEST FIXTURE on the keyboard port -- NOT a keyboard model -- and the
// bytes are deliberately arbitrary (0x00): only the COUNT is compared.
//
// Mechanism: duart_channel::rx_fifo_push() (the public channel API) instead of
// synthesising a serial bit stream on rx_a_w. A bit-level UART model would add
// timing surface without adding fidelity, because there is no document to be
// faithful to and the firmware ignores the values. The SCN2681 receive FIFO is
// only MC68681_RX_FIFO_SIZE+1 = 4 deep, so arm() pre-fills it and refill() adds
// one byte after every RHRA read, keeping RXRDY asserted until the ten bytes
// have been drained. At this point IMR = 0x00 (written by the channel
// initialiser), so pushing receive data cannot raise the chip's IRQ:
// update_interrupts() drives the line only when ISR & IMR != 0.
void vt1200_state::iopath_stub_arm()
{
if (m_iopath_supply)
return; // a previous reply is still draining
// Only reply while the channel is polled (interrupts masked), which is
// exactly the level-0 condition. This keeps the stub from injecting into
// the interrupt-enabled OS phase (IMR = 0x33) and raising an interrupt
// there -- it is scoped to the one test it exists for.
if (m_duart2_imr != 0x00)
return;
m_iopath_supply = 10;
const int fifo_depth = MC68681_RX_FIFO_SIZE + 1;
const int prefill = (m_iopath_supply < fifo_depth) ? m_iopath_supply : fifo_depth;
for (int i = 0; i < prefill; i++)
m_duart2->m_chanA->rx_fifo_push(0x00, 0);
m_iopath_supply -= prefill;
osd_printf_info("vt1200: IO-PATH STUB: 0xE1 on DUART2-A -> feeding 10 arbitrary bytes\n");
}
void vt1200_state::iopath_stub_refill()
{
if (m_iopath_supply <= 0)
return;
if (m_duart2_imr != 0x00) // interrupts got enabled: stop, don't arm
{
m_iopath_supply = 0;
return;
}
m_duart2->m_chanA->rx_fifo_push(0x00, 0);
m_iopath_supply--;
}
// The DUART's IRQ output is a single open-drain line per chip. Wire it to the
// CPU's external interrupt input the firmware's vector trampoline expects:
// line 0 -> TMS34010_INT1 (INTPEND 0x0002), line 1 -> TMS34010_INT2 (0x0004).
// MAME's tms34010 latches the pending bit from the line state, so deasserting
// the line (which the DUART does once ISR & IMR == 0) clears it again.
void vt1200_state::duart1_irq_w(int state)
{
if (m_irqlog && state)
{
// ISR (reg 5) and SRA (reg 1) are side-effect-free reads.
const u32 isr = m_duart1->read(5) & 0xff;
const u32 sra = m_duart1->read(1) & 0xff;
fprintf(m_irqlog, "irq=1 isr=%02X sra=%02X cyc=%llu\n", isr, sra,
(unsigned long long)m_maincpu->total_cycles());
fflush(m_irqlog);
}
m_maincpu->set_input_line(m_duart1_int2 ? 1 : 0, state ? ASSERT_LINE : CLEAR_LINE);
}
void vt1200_state::duart2_irq_w(int state)
{
// Under the stub, report the first DUART2 IRQ assertion with the ISR/IMR
// and how many injected bytes were outstanding. In the faithful level-0
// run this line MUST NOT appear -- IMR = 0x00 there, so feeding receive
// data cannot interrupt. (It can appear under VT1200_SKIP_LEVEL1, where
// the OS enables DUART2 interrupts for its own traffic.)
if (state && m_iopath_stub && !m_iopath_irq_reported)
{
m_iopath_irq_reported = true;
const u32 isr = m_duart2->read(5) & 0xff; // side-effect-free read
osd_printf_info("vt1200: IO-PATH STUB: DUART2 IRQ asserted (isr=%02X imr=%02X supply=%d)\n",
isr, m_duart2_imr, m_iopath_supply);
}
m_maincpu->set_input_line(1, state ? ASSERT_LINE : CLEAR_LINE);
}
void vt1200_state::log_duart(int win, char kind, u32 reg, u16 value)
{
if (!m_duartlog)
return;
fprintf(m_duartlog, "%d %c reg=%X val=%04X pc=%08X cyc=%llu\n",
win, kind, reg, value, (unsigned)m_maincpu->state_int(TMS34010_PC),
(unsigned long long)m_maincpu->total_cycles());
fflush(m_duartlog);
}
// Copy one active scanline out of the video RAM framebuffer.
//
// The TMS34010 screen-refresh address is reconstructed by the device into
// `params`, but the firmware's timing registers (HEBLNK/HSBLNK/VTOTAL) are not
// initialised to the real machine's values, so the geometry MAME derives from
// them (a 1280x901 screen, active x 192..1215) does not describe the memory
// layout. The measured memory layout does:
//
// * base bit 0x04000000 (the mapped video RAM)
// * pitch 1024 bits (0x40 words / 128 bytes) per row — taken from the
// firmware's own blitter, which advances its destination by
// 0x40 words per glyph row (pc=3FDF9A00 in VRAM write logs)
// * bit order LSB-first within each 16-bit word — the TMS34010 pixel order
// * active x the device's heblnk..hsblnk span is 1024 pixels, exactly the
// framebuffer width, so screen x-heblnk maps to framebuffer x
//
// The screen line maps 1:1 to a framebuffer row, offset by the vertical
// blanking start (params->veblnk).
void vt1200_state::render_active_line(bitmap_ind16 &bitmap, int scanline,
const tms340x0_device::display_params *params)
{
// Blank first: the device only repaints the horizontal blanking regions,
// so anything we leave here would be stale bitmap contents.
std::fill_n(&bitmap.pix(scanline, 0), bitmap.width(), u16(0));
if (!m_vram) // no video RAM mapped -> nothing to draw
return;
const int fby = scanline - (int)params->veblnk;
if (fby < 0 || fby >= VMEM_HEIGHT)
return;
const u16 *const vram = m_vram.target();
const int x0 = (int)params->heblnk;
const int x1 = (int)params->hsblnk;
const u32 row_bit = (u32)fby * VMEM_PITCH_BITS;
for (int fx = 0; fx < VMEM_WIDTH; fx++)
{
const int x = x0 + fx;
if (x >= x1 || x >= bitmap.width())
break;
const u32 bit = row_bit + (u32)fx;
// Address-map units are bits, so a 16-bit word is 16 units.
bitmap.pix(scanline, x) = (vram[bit >> 4] >> (bit & 15)) & 1;
}
}
void vt1200_state::scanline(screen_device &screen, bitmap_ind16 &bitmap, int scanline,
const tms340x0_device::display_params *params)
{
{
static int calls = 0;
if (calls++ < 4)
osd_printf_info("vt1200: scanline#%d line=%d enabled=%d\n",
calls, scanline, params ? (params->enabled ? 1 : 0) : -1);
}
if (m_dpylog && params && (long)screen.frame_number() < m_dpylog_frames)
{
fprintf(m_dpylog, "%d %04X %04X %d %d %d %d %d %d\n",
scanline, params->rowaddr, params->coladdr, params->yoffset,
params->heblnk, params->hsblnk, params->veblnk, params->vsblnk, params->vcount);
fflush(m_dpylog);
}
// Paint the active pixels ourselves. MAME's TMS34010 renderer does NOT
// read VRAM; tms340x0_ind16 calls this callback for the active region and
// only paints the blanking regions itself. If we leave the active pixels
// untouched the frame stays whatever the bitmap was (all black on the
// first frame).
if (m_render && params && params->enabled)
render_active_line(bitmap, scanline, params);
else
std::fill_n(&bitmap.pix(scanline, 0), bitmap.width(), u16(0));
// Dump on the last visible line of the target frame: by then the whole
// frame is in the bitmap, and unlike screen_update the scanline callback
// 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();
const long f = (long)screen.frame_number();
// PPM frame dumps. The single-frame knob fires at GE n (its historical
// semantics); the list knob fires at EXACTLY each listed frame.
if (m_dump_frame >= 0 && f == m_dump_frame)
dump_frame(screen, bitmap, f);
for (long fr : m_dump_frames)
if (f == fr)
dump_frame(screen, bitmap, fr);
if (m_dump_ram >= 0 && !m_dumped_ram && f >= m_dump_ram)
{
m_dumped_ram = true;
dump_ram(f);
}
for (long fr : m_dump_ram_frames)
if (f == fr)
dump_ram(fr);
if (m_dump_vram >= 0 && !m_dumped_vram && f >= m_dump_vram)
{
m_dumped_vram = true;
dump_vram(f);
}
for (long fr : m_dump_vram_frames)
if (f == fr)
dump_vram(fr);
}
}
void vt1200_state::dump_frame(screen_device &screen, bitmap_ind16 &bitmap, long frame)
{
char path[1024];
snprintf(path, sizeof(path), "%s/vt1200-frame-%ld.ppm", m_dump_dir.c_str(), frame);
FILE *fp = fopen(path, "wb");
if (!fp)
{
osd_printf_info("vt1200: cannot write %s\n", path);
return;
}
const int w = bitmap.width(), h = bitmap.height();
fprintf(fp, "P6\n%d %d\n255\n", w, h);
unsigned long lit = 0;
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
const u16 v = bitmap.pix(y, x);
if (v)
lit++;
const unsigned char c = v ? 255 : 0;
fputc(c, fp); fputc(c, fp); fputc(c, fp);
}
}
fclose(fp);
osd_printf_info("vt1200: dumped frame %ld -> %s (%dx%d, %lu lit = %.4f%%)\n",
frame, path, w, h, lit, 100.0 * lit / ((double)w * h));
}
uint32_t vt1200_state::screen_update(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect)
{
{
static int calls = 0;
if (calls++ < 4)
osd_printf_info("vt1200: screen_update#%d frame=%u clip_y=%d..%d screen=%dx%d\n",
calls, (unsigned)screen.frame_number(), cliprect.min_y, cliprect.max_y,
screen.width(), screen.height());
}
// The TMS34010 display controller does not read VRAM: it calls our
// scanline callback for the active region (which paints it from the
// framebuffer) and blanks the blanking region itself.
const uint32_t changed = m_maincpu->tms340x0_ind16(screen, bitmap, cliprect);
// Dump once the whole frame has been rendered (the video manager calls us
// per region, so wait for the region that reaches the bottom line).
if (cliprect.max_y >= screen.height() - 1)
{
static int guards = 0;
if (guards++ < 5)
osd_printf_info("vt1200: guard#%d frame=%u dump_frame=%ld dump_ram=%ld\n",
guards, (unsigned)screen.frame_number(), m_dump_frame, m_dump_ram);
const long f = (long)screen.frame_number();
if (m_dump_frame >= 0 && f == m_dump_frame)
dump_frame(screen, bitmap, f);
if (m_dump_ram >= 0 && !m_dumped_ram && f >= m_dump_ram)
{
m_dumped_ram = true;
dump_ram(f);
}
}
return changed;
}
void vt1200_state::dump_ram(long frame)
{
char path[1024];
snprintf(path, sizeof(path), "%s/vt1200-ram-%ld.bin", m_dump_dir.c_str(), frame);
FILE *fp = fopen(path, "wb");
if (!fp)
{
osd_printf_info("vt1200: cannot write %s\n", path);
return;
}
address_space &sp = m_maincpu->space(AS_PROGRAM);
(void)sp;
const u8 *base = reinterpret_cast<const u8 *>(m_mainram.target());
const size_t n = m_mainram.bytes();
fwrite(base, 1, n, fp);
fclose(fp);
osd_printf_info("vt1200: dumped %zu bytes of main RAM (frame %ld) -> %s\n",
n, frame, path);
}
void vt1200_state::dump_vram(long frame)
{
char path[1024];
snprintf(path, sizeof(path), "%s/vram-%ld.bin", m_dump_dir.c_str(), frame);
FILE *fp = fopen(path, "wb");
if (!fp)
{
osd_printf_info("vt1200: cannot write %s\n", path);
return;
}
if (!m_vram)
{
osd_printf_info("vt1200: video RAM not mapped; nothing to dump\n");
fclose(fp);
return;
}
const u8 *base = reinterpret_cast<const u8 *>(m_vram.target());
const size_t n = m_vram.bytes();
fwrite(base, 1, n, fp);
fclose(fp);
osd_printf_info("vt1200: dumped %zu bytes of video RAM (frame %ld) -> %s\n",
n, frame, path);
}
void vt1200_state::vt1200(machine_config &config)
{
// Clocks, derived from the real machine's 1280x1024 @ 72 Hz mono display and
// the firmware's own raster registers (HTOTAL=79, VTOTAL=900 observed):
//
// HTOTAL = 79 -> 80 pixel-clocks per line, and 80 * 16 = 1280 pixels,
// so the TMS34010 fetches SIXTEEN pixels per clock.
// refresh = pixel_clock / ((HTOTAL+1) * (VTOTAL+1))
// = pixel_clock / 72000 -> 72 Hz => pixel_clock = 5.184 MHz
//
// The CPU is a TMS34010FNL-50, i.e. 50 MHz (per the TMS34010 users guide and
// the wiki's part number).
TMS34010(config, m_maincpu, XTAL(50'000'000));
m_maincpu->set_addrmap(AS_PROGRAM, &vt1200_state::program_map);
m_maincpu->set_pixel_clock(5'184'000);
m_maincpu->set_pixels_per_clock(16);
m_maincpu->set_scanline_ind16_callback(FUNC(vt1200_state::scanline));
m_maincpu->set_screen("screen");
SCREEN(config, m_screen, SCREEN_TYPE_RASTER);
// Must be consistent with pixels_per_clock=16, or the renderer (which
// computes heblnk = HEBLNK * 16) overruns the bitmap and corrupts the heap.
// The device reconfigures this from the firmware's registers anyway.
m_screen->set_raw(5'184'000, 1280, 0, 1280, 901, 0, 901);
m_screen->set_screen_update(FUNC(vt1200_state::screen_update));
m_screen->set_palette(m_palette);
// 256 entries so the TMS34010 renderer can encode multi-bit pixels; the
// terminal's real output stage is a later question.
// Explicit monochrome ramp: pen 0 = black, pen 1 = white. render_active_line()
// writes only pens 0/1, so with no init delegate the on-screen colour was
// whatever MAME's default palette held (reported: red). Two entries suffice.
PALETTE(config, m_palette, palette_device::MONOCHROME).set_entries(2);
// The two Signetics SCN2681 DUARTs (same family as the Motorola MC68681:
// 2681 = RD/WR strobes, 68681 = R/W + DTACK + interrupt vector). The
// VT1200 firmware drives them with the generic 68681 register map, and the
// level-1 host-port test uses the chip's own local loopback (MR2 bits 7:6 =
// 10), so the SCN2681 fits and needs no vector-register behaviour.
SCN2681(config, m_duart1, XTAL(3'686'400));
SCN2681(config, m_duart2, XTAL(3'686'400));
// Connect the DUART interrupt outputs to the CPU. The level-1 host-port
// test enables the DUART's TxRDYA/RxRDYA interrupts (IMR = 7) and the
// firmware's ISR is what feeds THRA -- but only if the IRQ reaches the CPU.
// No external TX->RX loopback is wired: the firmware sets MR2's local
// loopback itself and the datasheet holds TxD high in local loopback, so
// the external wire is unnecessary. duart1_irq_w picks INT1 (POST) or INT2
// (OS) from VT1200_SKIP_LEVEL1; DUART2's trampoline is on INT2.
m_duart1->irq_cb().set(FUNC(vt1200_state::duart1_irq_w));
m_duart2->irq_cb().set(FUNC(vt1200_state::duart2_irq_w));
}
INPUT_PORTS_START(vt1200)
INPUT_PORTS_END
ROM_START(vt1200)
ROM_REGION(0x1c0000, "maincpu", 0)
ROM_LOAD("vt1200_v22.bin", 0x000000, 0x1c0000,
CRC(29448f23) SHA1(c2d65bce2c34701b53006f16ccba33e0adbbfd67))
ROM_END
} // anonymous namespace
// year, name, parent, compat, machine, input, class, init, company, fullname
CONS(1990, vt1200, 0, 0, vt1200, vt1200, vt1200_state, empty_init, "DEC",
"VT1200 X terminal (v2.2 firmware)", MACHINE_NOT_WORKING | MACHINE_NO_SOUND)