Files
vt1200/harness/mame/vt1200.cpp
T
arti b6da987aae harness: dump the LANCE DMA; the C - LANCE TEST's failing frame carries no FCS by design
The failing loopback case is a negative test: its TX buffer deliberately has
no FCS, and the test expects a CRC error.  The DTCR=1 case that does carry an
FCS is delivered intact and passes, so the DMA is not dropping four bytes.
The sole remaining mismatch is one bit: the test wants RMD1=0x6B00
(ERR|FRAM|CRC|STP|ENP) and MAME's am79c90 reports 0x4B00 -- it never sets
RMD1_FRAM.  No MAME-tree change made; reported for a second reader.
2026-10-16 17:12:02 +03:00

2421 lines
99 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) / pointing
device (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: the board system logic beyond the register model. The
LANCE register file, its address ROM, its interrupt and its bus-master DMA
ARE modelled (see below); what is not yet satisfying is the chip's own
internal loopback (a MAME FCS question, reported in LANCE_REPORT.md).
---------------------------------------------------------------------
LANCE Ethernet (milestone 1 of the LANCE plan):
* AM79C90 (MAME's am79c90) at the firmware's two windows: bit 0x5900000
is RAP (register-address port) and 0x5800000 is RDP (register-data
port). Every other offset in those two windows still falls through to
the board model -- the logic fronting the LANCE is otherwise undecoded.
* The Ethernet address ROM at bit 0x6800000 (physical byte 0xD00000):
EIGHT bytes -- 08:00:2b:16:4b:5a (DEC OUI + this unit's serial) followed
by a two-byte checksum. It is a physical device, not firmware data --
the MAC does not appear in the ROM image. The firmware reads eight
bytes and validates a folded ones-complement checksum over the first
six (0x3FDD7520 / 0x3FDD78B0); six bytes alone fail error 23.
* Interrupt INT1, which is where the firmware installs the LANCE ISR
0x3FA93400 (the DUARTs sit on INT2 in the OS phase).
* Bus-master DMA: bound, translating the LANCE's byte address to the
TMS34010's bit-addressed main RAM (byte B -> RAM word B>>1). The
firmware's init block at CSR1 = 0xFF60 lands at main-RAM word 0x7FB0,
which is where the firmware's own RAM dump has it.
* The chip's own internal loopback still reports a CRC error under MAME
(RCV descriptor 1 = 0x4B00, message count 0) because the firmware runs
loopback with MODE_DTCR; see README.md -- reported, not papered over,
and no MAME change was made.
The Ethernet-related tests this affects: the level-1 Ethernet address ROM
test (error 23) now PASSES; the LANCE test's register/INIT stage works (the
chip reads the init block, completes INIT, sees IDON, STRTs and moves
descriptors) but its loopback still reports a CRC failure under MAME. The
level-0 "LANCE not stopped" check (error 33 -- a reset chip reads
CSR0 = 0x0004, STOP set) is behind DUART1 IP bit 2, which this harness holds
low, so that path is not taken.
---------------------------------------------------------------------
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_LANCELOG=<path> every LANCE RAP/RDP access (and the address
ROM reads) with the PC, so the OS's
programming of the chip is visible.
VT1200_LANCE_DMA_LOG=<path> every LANCE bus-master DMA word access (byte
address, value, byte lanes, PC), so the
transmit buffer and the descriptor write-back
are observed directly rather than inferred.
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.
VT1200_MOUSE=1 force the VSXXX-AA mouse (the faithful part)
back onto DUART2 channel B. The DEFAULT is
the VSXXX-AB tablet, a DELIBERATE divergence
from the real terminal (which almost
certainly has a mouse attached): the tablet
reports the host pointer's ABSOLUTE position,
so the emulated cursor tracks the host
pointer instead of integrating relative
deltas. This is a VALUE-AWARE inverse
switch -- the default lives in code, not in
a wrapper script, so it can always be
flipped either way (VT1200_MOUSE=0, or
empty, leaves the tablet selected). Both
parts need MAME's `-lightgun -lightgun_device
mouse` for the host pointer to reach them,
and `-lightgun` does NOT grab the pointer.
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 "diserial.h"
#include "emupal.h"
#include "machine/am79c90.h"
#include "machine/mc68681.h"
#include "screen.h"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <string>
#include <utility>
#include <vector>
#include "vt1200_board.h"
//**************************************************************************
// DEC LK401 keyboard + VSXXX-AA mouse -- real device models on DUART2
//**************************************************************************
//
// The VT1200 keyboard is an LK401 speaking the LK201 host protocol (4800 8N1;
// see reference/keyboard-mouse/PROTOCOL.md). MAME's own lk201 device executes
// the real 68HC05 keyboard firmware, but that ROM (`23-001s9-00.bin`) is not in
// our set: the downloaded zip only carries the *8051* image (`23-004M2-00.bin`,
// SHA1 9744c561... = MAME's unused `23-004m2.8051h.e1`). So the keyboard is an
// HLE model of the documented protocol, not a firmware emulation.
//
// The mouse has no MAME device at all; the VSXXX-AA is modelled here too
// (4800 8O1). Both connect to the real SCN2681 at DUART2 through the genuine
// bit-level serial interfaces (`rx_a_w`/`rx_b_w` and `a_tx_cb`/`b_tx_cb`), so
// the framing/timing is the DUART's, not a shortcut.
//
// These are device models: they answer `FD`/`T` because that is the protocol,
// not because a test wants an answer. They are NOT tuned to satisfy the
// level-0 I/O data path test (which wants 10 bytes; the keyboard's own power-up
// and FD reply is 4). See reference/level0-10byte-puzzle.md.
// --- LK401 keycode / division table -----------------------------------------
// Every LK401 key has one keycode (netbsd-lk201-keycodes.html and elkay's
// keymap.md). Each key also belongs to one of 14 divisions, which the terminal
// programs to a mode (down / auto-repeat-down / down-up).
#define VT1200_LK401_KEY_LIST(X) \
X( 0, 0xbf, 1, PORT_NAME("` ~") PORT_CODE(KEYCODE_TILDE) PORT_CHAR('`') PORT_CHAR('~') ) \
X( 1, 0xc0, 1, PORT_NAME("1 !") PORT_CODE(KEYCODE_1) PORT_CHAR('1') PORT_CHAR('!') ) \
X( 2, 0xc5, 1, PORT_NAME("2 @") PORT_CODE(KEYCODE_2) PORT_CHAR('2') PORT_CHAR('@') ) \
X( 3, 0xcb, 1, PORT_NAME("3 #") PORT_CODE(KEYCODE_3) PORT_CHAR('3') PORT_CHAR('#') ) \
X( 4, 0xd0, 1, PORT_NAME("4 $") PORT_CODE(KEYCODE_4) PORT_CHAR('4') PORT_CHAR('$') ) \
X( 5, 0xd6, 1, PORT_NAME("5 %") PORT_CODE(KEYCODE_5) PORT_CHAR('5') PORT_CHAR('%') ) \
X( 6, 0xdb, 1, PORT_NAME("6 ^") PORT_CODE(KEYCODE_6) PORT_CHAR('6') PORT_CHAR('^') ) \
X( 7, 0xe0, 1, PORT_NAME("7 &") PORT_CODE(KEYCODE_7) PORT_CHAR('7') PORT_CHAR('&') ) \
X( 8, 0xe5, 1, PORT_NAME("8 *") PORT_CODE(KEYCODE_8) PORT_CHAR('8') PORT_CHAR('*') ) \
X( 9, 0xea, 1, PORT_NAME("9 (") PORT_CODE(KEYCODE_9) PORT_CHAR('9') PORT_CHAR('(') ) \
X(10, 0xef, 1, PORT_NAME("0 )") PORT_CODE(KEYCODE_0) PORT_CHAR('0') PORT_CHAR(')') ) \
X(11, 0xf9, 1, PORT_NAME("- _") PORT_CODE(KEYCODE_MINUS) PORT_CHAR('-') PORT_CHAR('_') ) \
X(12, 0xf5, 1, PORT_NAME("= +") PORT_CODE(KEYCODE_EQUALS) PORT_CHAR('=') PORT_CHAR('+') ) \
X(13, 0xc1, 1, PORT_NAME("Q") PORT_CODE(KEYCODE_Q) PORT_CHAR('q') PORT_CHAR('Q') ) \
X(14, 0xc6, 1, PORT_NAME("W") PORT_CODE(KEYCODE_W) PORT_CHAR('w') PORT_CHAR('W') ) \
X(15, 0xcc, 1, PORT_NAME("E") PORT_CODE(KEYCODE_E) PORT_CHAR('e') PORT_CHAR('E') ) \
X(16, 0xd1, 1, PORT_NAME("R") PORT_CODE(KEYCODE_R) PORT_CHAR('r') PORT_CHAR('R') ) \
X(17, 0xd7, 1, PORT_NAME("T") PORT_CODE(KEYCODE_T) PORT_CHAR('t') PORT_CHAR('T') ) \
X(18, 0xdc, 1, PORT_NAME("Y") PORT_CODE(KEYCODE_Y) PORT_CHAR('y') PORT_CHAR('Y') ) \
X(19, 0xe1, 1, PORT_NAME("U") PORT_CODE(KEYCODE_U) PORT_CHAR('u') PORT_CHAR('U') ) \
X(20, 0xe6, 1, PORT_NAME("I") PORT_CODE(KEYCODE_I) PORT_CHAR('i') PORT_CHAR('I') ) \
X(21, 0xeb, 1, PORT_NAME("O") PORT_CODE(KEYCODE_O) PORT_CHAR('o') PORT_CHAR('O') ) \
X(22, 0xf0, 1, PORT_NAME("P") PORT_CODE(KEYCODE_P) PORT_CHAR('p') PORT_CHAR('P') ) \
X(23, 0xfa, 1, PORT_NAME("[ {") PORT_CODE(KEYCODE_OPENBRACE) PORT_CHAR('[') PORT_CHAR('{') ) \
X(24, 0xf6, 1, PORT_NAME("] }") PORT_CODE(KEYCODE_CLOSEBRACE)PORT_CHAR(']') PORT_CHAR('}') ) \
X(25, 0xf7, 1, PORT_NAME("\\ |") PORT_CODE(KEYCODE_BACKSLASH) PORT_CHAR('\\') PORT_CHAR('|') ) \
X(26, 0xc2, 1, PORT_NAME("A") PORT_CODE(KEYCODE_A) PORT_CHAR('a') PORT_CHAR('A') ) \
X(27, 0xc7, 1, PORT_NAME("S") PORT_CODE(KEYCODE_S) PORT_CHAR('s') PORT_CHAR('S') ) \
X(28, 0xcd, 1, PORT_NAME("D") PORT_CODE(KEYCODE_D) PORT_CHAR('d') PORT_CHAR('D') ) \
X(29, 0xd2, 1, PORT_NAME("F") PORT_CODE(KEYCODE_F) PORT_CHAR('f') PORT_CHAR('F') ) \
X(30, 0xd8, 1, PORT_NAME("G") PORT_CODE(KEYCODE_G) PORT_CHAR('g') PORT_CHAR('G') ) \
X(31, 0xdd, 1, PORT_NAME("H") PORT_CODE(KEYCODE_H) PORT_CHAR('h') PORT_CHAR('H') ) \
X(32, 0xe2, 1, PORT_NAME("J") PORT_CODE(KEYCODE_J) PORT_CHAR('j') PORT_CHAR('J') ) \
X(33, 0xe7, 1, PORT_NAME("K") PORT_CODE(KEYCODE_K) PORT_CHAR('k') PORT_CHAR('K') ) \
X(34, 0xec, 1, PORT_NAME("L") PORT_CODE(KEYCODE_L) PORT_CHAR('l') PORT_CHAR('L') ) \
X(35, 0xf2, 1, PORT_NAME("; :") PORT_CODE(KEYCODE_COLON) PORT_CHAR(';') PORT_CHAR(':') ) \
X(36, 0xfb, 1, PORT_NAME("' \"") PORT_CODE(KEYCODE_QUOTE) PORT_CHAR('\'') PORT_CHAR('"') ) \
X(37, 0xc9, 1, PORT_NAME("< >") PORT_CODE(KEYCODE_BACKSLASH2)PORT_CHAR('<') PORT_CHAR('>') ) \
X(38, 0xc3, 1, PORT_NAME("Z") PORT_CODE(KEYCODE_Z) PORT_CHAR('z') PORT_CHAR('Z') ) \
X(39, 0xc8, 1, PORT_NAME("X") PORT_CODE(KEYCODE_X) PORT_CHAR('x') PORT_CHAR('X') ) \
X(40, 0xce, 1, PORT_NAME("C") PORT_CODE(KEYCODE_C) PORT_CHAR('c') PORT_CHAR('C') ) \
X(41, 0xd3, 1, PORT_NAME("V") PORT_CODE(KEYCODE_V) PORT_CHAR('v') PORT_CHAR('V') ) \
X(42, 0xd9, 1, PORT_NAME("B") PORT_CODE(KEYCODE_B) PORT_CHAR('b') PORT_CHAR('B') ) \
X(43, 0xde, 1, PORT_NAME("N") PORT_CODE(KEYCODE_N) PORT_CHAR('n') PORT_CHAR('N') ) \
X(44, 0xe3, 1, PORT_NAME("M") PORT_CODE(KEYCODE_M) PORT_CHAR('m') PORT_CHAR('M') ) \
X(45, 0xe8, 1, PORT_NAME(", <") PORT_CODE(KEYCODE_COMMA) PORT_CHAR(',') PORT_CHAR('<') ) \
X(46, 0xed, 1, PORT_NAME(". >") PORT_CODE(KEYCODE_STOP) PORT_CHAR('.') PORT_CHAR('>') ) \
X(47, 0xf3, 1, PORT_NAME("/ ?") PORT_CODE(KEYCODE_SLASH) PORT_CHAR('/') PORT_CHAR('?') ) \
X(48, 0xd4, 1, PORT_NAME("Space") PORT_CODE(KEYCODE_SPACE) PORT_CHAR(' ') ) \
X(49, 0x92, 2, PORT_NAME("KP 0") PORT_CODE(KEYCODE_0_PAD) ) \
X(50, 0x96, 2, PORT_NAME("KP 1") PORT_CODE(KEYCODE_1_PAD) ) \
X(51, 0x97, 2, PORT_NAME("KP 2") PORT_CODE(KEYCODE_2_PAD) ) \
X(52, 0x98, 2, PORT_NAME("KP 3") PORT_CODE(KEYCODE_3_PAD) ) \
X(53, 0x99, 2, PORT_NAME("KP 4") PORT_CODE(KEYCODE_4_PAD) ) \
X(54, 0x9a, 2, PORT_NAME("KP 5") PORT_CODE(KEYCODE_5_PAD) ) \
X(55, 0x9b, 2, PORT_NAME("KP 6") PORT_CODE(KEYCODE_6_PAD) ) \
X(56, 0x9d, 2, PORT_NAME("KP 7") PORT_CODE(KEYCODE_7_PAD) ) \
X(57, 0x9e, 2, PORT_NAME("KP 8") PORT_CODE(KEYCODE_8_PAD) ) \
X(58, 0x9f, 2, PORT_NAME("KP 9") PORT_CODE(KEYCODE_9_PAD) ) \
X(59, 0x94, 2, PORT_NAME("KP .") PORT_CODE(KEYCODE_DEL_PAD) ) \
X(60, 0x9c, 2, PORT_NAME("KP ,") PORT_CODE(KEYCODE_COMMA_PAD) ) \
X(61, 0xa0, 2, PORT_NAME("KP -") PORT_CODE(KEYCODE_MINUS_PAD) ) \
X(62, 0x95, 2, PORT_NAME("KP Enter") PORT_CODE(KEYCODE_ENTER_PAD) ) \
X(63, 0xa1, 2, PORT_NAME("PF1") PORT_CODE(KEYCODE_NUMLOCK) ) \
X(64, 0xa2, 2, PORT_NAME("PF2") PORT_CODE(KEYCODE_SLASH_PAD) ) \
X(65, 0xa3, 2, PORT_NAME("PF3") PORT_CODE(KEYCODE_ASTERISK) ) \
X(66, 0xa4, 2, PORT_NAME("PF4") PORT_CODE(KEYCODE_EQUALS_PAD) ) \
X(67, 0xbc, 3, PORT_NAME("Delete") PORT_CODE(KEYCODE_BACKSPACE) PORT_CHAR(8) ) \
X(68, 0xbd, 4, PORT_NAME("Return") PORT_CODE(KEYCODE_ENTER) PORT_CHAR(13) ) \
X(69, 0xbe, 4, PORT_NAME("Tab") PORT_CODE(KEYCODE_TAB) PORT_CHAR(9) ) \
X(70, 0xb0, 5, PORT_NAME("Lock") PORT_CODE(KEYCODE_CAPSLOCK) ) \
X(71, 0xb1, 5, PORT_NAME("Compose") PORT_CODE(KEYCODE_MENU) ) \
X(72, 0xae, 6, PORT_NAME("Shift") PORT_CODE(KEYCODE_LSHIFT) ) \
X(73, 0xaf, 6, PORT_NAME("Ctrl") PORT_CODE(KEYCODE_LCONTROL) ) \
X(74, 0xa7, 7, PORT_NAME("Left") PORT_CODE(KEYCODE_LEFT) PORT_CHAR(UCHAR_MAMEKEY(LEFT)) ) \
X(75, 0xa8, 7, PORT_NAME("Right") PORT_CODE(KEYCODE_RIGHT) PORT_CHAR(UCHAR_MAMEKEY(RIGHT)) ) \
X(76, 0xa9, 8, PORT_NAME("Down") PORT_CODE(KEYCODE_DOWN) PORT_CHAR(UCHAR_MAMEKEY(DOWN)) ) \
X(77, 0xaa, 8, PORT_NAME("Up") PORT_CODE(KEYCODE_UP) PORT_CHAR(UCHAR_MAMEKEY(UP)) ) \
X(78, 0x8a, 9, PORT_NAME("Find") PORT_CODE(KEYCODE_INSERT) PORT_CHAR(UCHAR_MAMEKEY(INSERT)) ) \
X(79, 0x8b, 9, PORT_NAME("Insert Here") PORT_CODE(KEYCODE_HOME) PORT_CHAR(UCHAR_MAMEKEY(HOME)) ) \
X(80, 0x8c, 9, PORT_NAME("Remove") PORT_CODE(KEYCODE_PGUP) PORT_CHAR(UCHAR_MAMEKEY(PGUP)) ) \
X(81, 0x8d, 9, PORT_NAME("Select") PORT_CODE(KEYCODE_DEL) PORT_CHAR(UCHAR_MAMEKEY(DEL)) ) \
X(82, 0x8e, 9, PORT_NAME("Prev Screen") PORT_CODE(KEYCODE_END) PORT_CHAR(UCHAR_MAMEKEY(END)) ) \
X(83, 0x8f, 9, PORT_NAME("Next Screen") PORT_CODE(KEYCODE_PGDN) PORT_CHAR(UCHAR_MAMEKEY(PGDN)) ) \
X(84, 0x56, 10, PORT_NAME("F1 Hold") PORT_CODE(KEYCODE_F1) PORT_CHAR(UCHAR_MAMEKEY(F1)) ) \
X(85, 0x57, 10, PORT_NAME("F2 Print") PORT_CODE(KEYCODE_F2) PORT_CHAR(UCHAR_MAMEKEY(F2)) ) \
X(86, 0x58, 10, PORT_NAME("F3 Setup") PORT_CODE(KEYCODE_F3) PORT_CHAR(UCHAR_MAMEKEY(F3)) ) \
X(87, 0x59, 10, PORT_NAME("F4 Data") PORT_CODE(KEYCODE_F4) PORT_CHAR(UCHAR_MAMEKEY(F4)) ) \
X(88, 0x5a, 10, PORT_NAME("F5 Break") PORT_CODE(KEYCODE_F5) PORT_CHAR(UCHAR_MAMEKEY(F5)) ) \
X(89, 0x64, 11, PORT_NAME("F6") PORT_CODE(KEYCODE_F6) PORT_CHAR(UCHAR_MAMEKEY(F6)) ) \
X(90, 0x65, 11, PORT_NAME("F7") PORT_CODE(KEYCODE_F7) PORT_CHAR(UCHAR_MAMEKEY(F7)) ) \
X(91, 0x66, 11, PORT_NAME("F8") PORT_CODE(KEYCODE_F8) PORT_CHAR(UCHAR_MAMEKEY(F8)) ) \
X(92, 0x67, 11, PORT_NAME("F9") PORT_CODE(KEYCODE_F9) PORT_CHAR(UCHAR_MAMEKEY(F9)) ) \
X(93, 0x68, 11, PORT_NAME("F10") PORT_CODE(KEYCODE_F10) PORT_CHAR(UCHAR_MAMEKEY(F10)) ) \
X(94, 0x71, 12, PORT_NAME("F11 Esc") PORT_CODE(KEYCODE_F11) PORT_CHAR(UCHAR_MAMEKEY(F11)) ) \
X(95, 0x72, 12, PORT_NAME("F12 BS") PORT_CODE(KEYCODE_F12) PORT_CHAR(UCHAR_MAMEKEY(F12)) ) \
X(96, 0x73, 12, PORT_NAME("F13 LF") PORT_CODE(KEYCODE_F13) PORT_CHAR(UCHAR_MAMEKEY(F13)) ) \
X(97, 0x74, 12, PORT_NAME("F14") PORT_CODE(KEYCODE_F14) PORT_CHAR(UCHAR_MAMEKEY(F14)) ) \
X(98, 0x7c, 13, PORT_NAME("Help") PORT_CODE(KEYCODE_F15) PORT_CHAR(UCHAR_MAMEKEY(F15)) ) \
X(99, 0x7d, 13, PORT_NAME("Do") PORT_CODE(KEYCODE_F16) PORT_CHAR(UCHAR_MAMEKEY(F16)) ) \
X(100,0x80, 14, PORT_NAME("F17") PORT_CODE(KEYCODE_F17) PORT_CHAR(UCHAR_MAMEKEY(F17)) ) \
X(101,0x81, 14, PORT_NAME("F18") PORT_CODE(KEYCODE_F18) PORT_CHAR(UCHAR_MAMEKEY(F18)) ) \
X(102,0x82, 14, PORT_NAME("F19") PORT_CODE(KEYCODE_F19) PORT_CHAR(UCHAR_MAMEKEY(F19)) ) \
X(103,0x83, 14, PORT_NAME("F20") PORT_CODE(KEYCODE_F20) PORT_CHAR(UCHAR_MAMEKEY(F20)) ) \
X(104,0xab, 8, PORT_NAME("RShift") PORT_CODE(KEYCODE_RSHIFT) ) \
X(105,0xac, 5, PORT_NAME("LAlt") PORT_CODE(KEYCODE_LALT) ) \
X(106,0xb2, 5, PORT_NAME("RAlt") PORT_CODE(KEYCODE_RALT) ) \
X(107,0xad, 5, PORT_NAME("RCompose") PORT_CODE(KEYCODE_RWIN) )
#define VT1200_LK401_COUNT(n, code, div, ...) + 1
#define VT1200_LK401_PORT(n, code, div, ...) \
PORT_START("K" #n) PORT_BIT(0x01, IP_ACTIVE_HIGH, IPT_KEYBOARD) __VA_ARGS__
#define VT1200_LK401_ENTRY(n, code, div, ...) { code, div },
enum { VT1200_LK401_NKEYS = 0 VT1200_LK401_KEY_LIST(VT1200_LK401_COUNT) };
// Keyboard command / response opcodes (PROTOCOL.md section 3.3/3.4).
enum : uint8_t
{
LK_CMD_POWER_UP = 0xfd,
LK_CMD_REQUEST_ID = 0xab,
LK_CMD_SET_DEFAULTS = 0xd3,
LK_CMD_ENABLE_LK401 = 0xe9,
LK_CMD_LED_ON = 0x13,
LK_CMD_LED_OFF = 0x11,
LK_CMD_KEYCLICK_ON = 0x1b,
LK_CMD_KEYCLICK_OFF = 0x99,
LK_CMD_CTRLCLICK_ON = 0xbb,
LK_CMD_CTRLCLICK_OFF = 0xb9,
LK_CMD_BELL_ON = 0x23,
LK_CMD_BELL_OFF = 0xa1,
LK_CMD_BELL = 0xa7,
LK_CMD_SOUND_CLICK = 0x9f,
LK_CMD_MMS_NO_RPT = 0xd1,
LK_CMD_ENB_RPT = 0xe3,
LK_CMD_DIS_RPT = 0xe1,
LK_CMD_RPT_TO_DOWN = 0xd9,
LK_CMD_RESUME = 0x8b,
LK_CMD_TEST_MODE = 0xcb,
LK_ALL_KEYS_UP = 0xb3,
LK_METRONOME = 0xb4,
LK_OUTPUT_ERROR = 0xb5,
LK_INPUT_ERROR = 0xb6,
LK_KBD_LOCKED = 0xb7,
LK_TEST_MODE_ACK = 0xb8,
LK_PREFIX_KEY_DOWN = 0xb9,
LK_MODE_CHANGE_ACK = 0xba
};
// Power-up / `FD` self-test reply. PROTOCOL.md section 3.5: firmware ID 01,
// hardware ID 00, error 00, no stuck key.
static const uint8_t LK_SELFTEST[4] = { 0x01, 0x00, 0x00, 0x00 };
class vt1200_lk401_device : public device_t, public device_buffered_serial_interface<64U>
{
public:
vt1200_lk401_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0);
auto tx_handler() { return m_tx_handler.bind(); }
protected:
virtual void device_start() override ATTR_COLD;
virtual void device_reset() override ATTR_COLD;
virtual ioport_constructor device_input_ports() const override ATTR_COLD;
virtual void tra_callback() override;
virtual void received_byte(u8 byte) override;
TIMER_CALLBACK_MEMBER(scan);
TIMER_CALLBACK_MEMBER(power_up_selftest);
private:
void process_command();
void run_self_test();
void queue(u8 byte);
void emit_key(unsigned index, bool pressed);
static bool lk401_only(uint8_t code);
devcb_write_line m_tx_handler;
required_ioport_array<VT1200_LK401_NKEYS> m_kbd;
emu_timer *m_scan_timer = nullptr;
emu_timer *m_power_up_timer = nullptr;
std::vector<u8> m_cmd; // command bytes accumulated so far
u8 m_div_mode[15] = { 0 }; // [1..14]: 0=down 1=autodown 3=updown
bool m_autorepeat = true;
bool m_lk401_mode = false;
bool m_inhibit = false;
u8 m_led_mask = 0;
bool m_lost_keys = false;
std::array<bool, VT1200_LK401_NKEYS> m_down = { };
std::array<attotime, VT1200_LK401_NKEYS> m_repeat = { };
int m_updown_down = 0;
FILE *m_log = nullptr; // VT1200_INPUT_LOG=<path>
};
static const struct { uint8_t code; uint8_t div; } VT1200_LK401_TABLE[] =
{
VT1200_LK401_KEY_LIST(VT1200_LK401_ENTRY)
};
INPUT_PORTS_START(vt1200_lk401)
VT1200_LK401_KEY_LIST(VT1200_LK401_PORT)
INPUT_PORTS_END
// Mouse host inputs. X/Y are relative (MAME accumulates movement into the port
// value, which the device reads as a signed 8-bit delta). The three button
// bits are laid out in the VSXXX report order: bit 2 = left, bit 1 = middle,
// bit 0 = right.
INPUT_PORTS_START(vt1200_vsxxxaa)
PORT_START("MOUSE_X")
PORT_BIT(0xff, 0x00, IPT_MOUSE_X) PORT_SENSITIVITY(50) PORT_KEYDELTA(0) PORT_PLAYER(1)
PORT_START("MOUSE_Y")
PORT_BIT(0xff, 0x00, IPT_MOUSE_Y) PORT_SENSITIVITY(50) PORT_KEYDELTA(0) PORT_PLAYER(1)
PORT_START("MOUSE_BTN")
PORT_BIT(0x04, IP_ACTIVE_HIGH, IPT_BUTTON1) PORT_CODE(MOUSECODE_BUTTON1) PORT_PLAYER(1) // left
PORT_BIT(0x02, IP_ACTIVE_HIGH, IPT_BUTTON2) PORT_CODE(MOUSECODE_BUTTON2) PORT_PLAYER(1) // middle
PORT_BIT(0x01, IP_ACTIVE_HIGH, IPT_BUTTON3) PORT_CODE(MOUSECODE_BUTTON3) PORT_PLAYER(1) // right
INPUT_PORTS_END
// Tablet host inputs. The VSXXX-AB is an ABSOLUTE pointing device, so its X/Y
// come from MAME's absolute pointing controls, IPT_LIGHTGUN_X / IPT_LIGHTGUN_Y:
// their value is the host pointer's position in the MAME window scaled into
// MIN..MAX (0..4095, the tablet's 12-bit coordinate space), NOT an accumulated
// delta. That is exactly the property the mouse cannot offer. Drive them from
// the host mouse with `-lightgun -lightgun_device mouse` (lightgun_device
// defaults to keyboard); see TABLET_REPORT.md and run.sh.
INPUT_PORTS_START(vt1200_vsxxxab)
PORT_START("TABLET_X")
PORT_BIT(0x0fff, 0x0000, IPT_LIGHTGUN_X) PORT_MINMAX(0x0000, 2199) PORT_SENSITIVITY(100) PORT_KEYDELTA(0) PORT_PLAYER(1) PORT_NAME("Tablet X")
PORT_START("TABLET_Y")
PORT_BIT(0x0fff, 0x0000, IPT_LIGHTGUN_Y) PORT_MINMAX(0x0000, 2199) PORT_SENSITIVITY(100) PORT_KEYDELTA(0) PORT_PLAYER(1) PORT_NAME("Tablet Y")
// VSXXX-AB ABS report byte 0: 110 B4 B3 B2 B1 Pr. By convention puck
// B1->mouse left, B2->middle, B3->right; B4 is the puck's fourth button.
// Pr ("in proximity") is held asserted: the emulated pointer is always
// present over the tablet, so the host always has a valid position.
PORT_START("TABLET_BTN")
PORT_BIT(0x02, IP_ACTIVE_HIGH, IPT_BUTTON1) PORT_CODE(MOUSECODE_BUTTON1) PORT_PLAYER(1) // B1 left
PORT_BIT(0x04, IP_ACTIVE_HIGH, IPT_BUTTON2) PORT_CODE(MOUSECODE_BUTTON2) PORT_PLAYER(1) // B2 middle
PORT_BIT(0x08, IP_ACTIVE_HIGH, IPT_BUTTON3) PORT_CODE(MOUSECODE_BUTTON3) PORT_PLAYER(1) // B3 right
PORT_BIT(0x10, IP_ACTIVE_HIGH, IPT_BUTTON4) PORT_CODE(MOUSECODE_BUTTON4) PORT_PLAYER(1) // B4
INPUT_PORTS_END
// --- VSXXX family: VSXXX-AA/-GA mouse + VSXXX-AB tablet ---------------------
// 4800 8O1 (VCB02 section C.6.1). Commands are printable ASCII; the self-test
// (`T`, and at power-up) returns a 4-byte report. Movement is reported as
// 3-byte incremental packets in stream mode (`R`), or on request (`P`).
//
// The mouse and the tablet share that whole serial interface and command set
// (VCB02 C.7.4: "the tablet uses the same connector and serial interface
// format as the mouse, and recognizes the same commands"). Exactly two things
// differ, and they are the subclass hooks below:
//
// * the self-test ID: byte 2's device code is 0010 for the mouse and 0100
// for the tablet (VCB02 C.6.5 figure C-5 + C.7.4). This is how the host
// tells the two apart -- there is no separate "is it a tablet?" command.
// * the position report: 3-byte relative (header 100) for the mouse vs
// 5-byte absolute (header 110) for the tablet (VCB02 C.6.6 + D.3).
enum : uint8_t
{
VS_CMD_STREAM = 'R',
VS_CMD_PROMPT = 'D',
VS_CMD_POSITION = 'P',
VS_CMD_SELFTEST = 'T',
VS_CMD_FORMAT = 'S'
};
class vt1200_vsxxx_base : public device_t, public device_buffered_serial_interface<16U>
{
public:
vt1200_vsxxx_base(const machine_config &mconfig, device_type type, const char *tag,
device_t *owner, uint32_t clock, const char *x_tag, const char *y_tag,
const char *btn_tag);
auto tx_handler() { return m_tx_handler.bind(); }
protected:
virtual void device_start() override ATTR_COLD;
virtual void device_reset() override ATTR_COLD;
virtual void tra_callback() override;
virtual void received_byte(u8 byte) override;
TIMER_CALLBACK_MEMBER(scan);
TIMER_CALLBACK_MEMBER(power_up_selftest);
// What the two parts do differently.
virtual void send_self_test() = 0; // device-code byte differs
virtual void sample_host() = 0; // read the host input for this tick
virtual bool stream_report_due() = 0; // a report is waiting to be sent
virtual void emit_stream_report() = 0; // ...send it (REL vs ABS)
virtual void emit_position_report() = 0; // answer `P`
virtual void on_stream_mode() { } // entered stream mode (`R`)
void queue(u8 byte);
devcb_write_line m_tx_handler;
required_ioport m_host_x;
required_ioport m_host_y;
required_ioport m_host_buttons;
emu_timer *m_scan_timer = nullptr;
emu_timer *m_power_up_timer = nullptr;
bool m_stream = false; // false = prompt mode (power-up default)
FILE *m_log = nullptr;
};
// VSXXX-AA/-GA mouse: relative.
class vt1200_vsxxxaa_device : public vt1200_vsxxx_base
{
public:
vt1200_vsxxxaa_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0);
protected:
virtual void device_reset() override ATTR_COLD;
virtual ioport_constructor device_input_ports() const override ATTR_COLD;
virtual void send_self_test() override;
virtual void sample_host() override;
virtual bool stream_report_due() override;
virtual void emit_stream_report() override;
virtual void emit_position_report() override;
private:
void send_report(int dx, int dy, int buttons);
u8 m_last_x = 0;
u8 m_last_y = 0;
int m_acc_dx = 0;
int m_acc_dy = 0;
int m_last_buttons = 0;
int m_buttons = 0;
};
// VSXXX-AB tablet: absolute.
class vt1200_vsxxxab_device : public vt1200_vsxxx_base
{
public:
vt1200_vsxxxab_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0);
protected:
virtual void device_reset() override ATTR_COLD;
virtual ioport_constructor device_input_ports() const override ATTR_COLD;
virtual void send_self_test() override;
virtual void sample_host() override;
virtual bool stream_report_due() override;
virtual void emit_stream_report() override;
virtual void emit_position_report() override;
virtual void on_stream_mode() override;
private:
void send_abs(int x, int y, int buttons);
int m_x = 0; // 12-bit absolute tablet coordinates
int m_y = 0; // (host pointer scaled into 0..0xfff)
int m_buttons = 0;
int m_last_x = -1; // -1 forces a first report on entering stream
int m_last_y = -1;
int m_last_buttons = 0;
};
DECLARE_DEVICE_TYPE(VT1200_LK401, vt1200_lk401_device)
DECLARE_DEVICE_TYPE(VT1200_VSXXXAA, vt1200_vsxxxaa_device)
DECLARE_DEVICE_TYPE(VT1200_VSXXXAB, vt1200_vsxxxab_device)
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_lk401(*this, "lk401")
, m_mouse(*this, "mouse")
, m_tablet(*this, "tablet")
, m_lance(*this, "lance")
, 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
// The real devices on DUART2. Keyboard on channel A, pointing device on
// channel B, as the board has them. Wired bit-by-bit to the SCN2681's
// serial lines. Channel B carries the VSXXX-AB tablet BY DEFAULT, or the
// VSXXX-AA mouse with VT1200_MOUSE=1; exactly one is instantiated, so the
// connector carries one real part. (See the divergence note above and
// README.md.)
required_device<vt1200_lk401_device> m_lk401;
optional_device<vt1200_vsxxxaa_device> m_mouse;
optional_device<vt1200_vsxxxab_device> m_tablet;
bool m_tablet_selected = false;
void duart2_a_tx_w(int state);
void duart2_b_tx_w(int state);
// --- LANCE (Am7990/Am79C90) Ethernet controller --------------------------
// The firmware's Ethernet interface is the LANCE register file: bit
// 0x5900000 = RAP (register-address port), bit 0x5800000 = RDP
// (register-data port). Both are byte-addressed devices, which is why the
// firmware does srl 0x3 on a bit address before loading CSR1/CSR2 (see
// docs/superpowers/plans/2026-10-16-lance-ethernet.md section 1.1).
//
// The chip is MAME's am79c90_device (AM79C90 C-LANCE). Its CSR0 STOP
// semantics make the level-0 "LANCE not stopped" test pass by construction
// (a reset chip reads 0x0004), and its CSR1 read/write path satisfies the
// level-1 register test.
//
// The bus-master DMA callbacks are bound in vt1200(): LANCE byte B maps to
// main-RAM word B>>1. See the note there.
required_device<am79c90_device> m_lance;
// Ethernet address ROM: bit 0x6800000 (physical byte 0xD00000). The
// firmware reads EIGHT bytes, one per 16-bit step:
// movi 0x6800000,a11 / ... / movb *a7(0x0),*a11(0x1b0) / addk 16,a7
// (0x3FDD7520-0x3FDD7620), then validates a checksum over the first six.
// The TMS34010's RBYTE() extracts the LOW byte of the word at the bit
// address (34010ops.h RFIELDMAC(0xff,9): RDMEM_WORD >> (bit&0xf)), so
// handler offset i (=(bitaddr-base)>>4) returns ROM byte i in the low byte.
u16 lance_rom_r(offs_t off);
u16 lance_rdp_r(offs_t off); // RDP = chip data port (regs_r offset 0)
void lance_rdp_w(offs_t off, u16 data);
u16 lance_rap_r(offs_t off); // RAP = chip register select (regs_r offset 1)
void lance_rap_w(offs_t off, u16 data);
// VT1200_LANCELOG=<path>: every LANCE register access with the PC, so the
// OS's programming of the chip (RAP writes, CSR0 INIT/STRT/STOP) is visible.
FILE *m_lancelog = nullptr;
void log_lance(char kind, const char *what, u16 value);
// VT1200_LANCE_DMA_LOG=<path>: every bus-master DMA word access (the LANCE's
// own byte address, the value and, for writes, the byte lanes). This is how
// the transmit buffer and the descriptor write-back are observed directly,
// rather than inferred: the chip's byte address B is main-RAM word B>>1, and
// the even/odd byte lane is dma_in/dma_out's own (am79c90.cpp dma_in/dma_out).
FILE *m_lance_dmalog = nullptr;
void log_lance_dma(char kind, u32 addr, u16 value, u16 mask);
// 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");
}
if (const char *p = getenv("VT1200_LANCELOG"))
{
m_lancelog = fopen(p, "w");
if (m_lancelog)
fprintf(m_lancelog, "# kind port val pc cyc t -- every LANCE RAP/RDP/ROM access\n");
}
if (const char *p = getenv("VT1200_LANCE_DMA_LOG"))
{
m_lance_dmalog = fopen(p, "w");
if (m_lance_dmalog)
fprintf(m_lance_dmalog,
"# kind byteaddr value mask pc cyc t -- every LANCE bus-master DMA word access\n"
"# 'r' = chip read (dma_in), 'w' = chip write (dma_out)\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)");
logerror("vt1200: DUART2 devices: LK401 keyboard (ch A) + %s (ch B)%s\n",
m_tablet_selected ? "VSXXX-AB tablet (default)" : "VSXXX-AA mouse",
m_tablet_selected ? " [divergence: VT1200_MOUSE=1 forces the mouse]" : " [VT1200_MOUSE]");
logerror("vt1200: lance=on (AM79C90 @RAP 0x5900000 / RDP 0x5800000, IRQ->INT1), rom=08:00:2b:16:4b:5a+c186, dma=bound (byte>>1 -> main RAM)\n");
}
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));
// LANCE Ethernet controller. Registered AFTER the board-window model so
// this wins the register slice (map precedence is last-wins); every OTHER
// offset in the two windows still falls through to the board model, because
// the board logic fronting the LANCE is otherwise undecoded (plan risk 1).
//
// bit 0x5900000 (WIN1) = RAP, the register-address port
// bit 0x5800000 (WIN0) = RDP, the register-data port
//
// Each window entry covers one 16-bit word (0x10 map units); a word access
// at the window base is handler offset 0. The firmware accesses only offset
// 0 of each window.
map(VT1200_WIN1, VT1200_WIN1 + 0x000f).rw(FUNC(vt1200_state::lance_rap_r), FUNC(vt1200_state::lance_rap_w));
map(VT1200_WIN0, VT1200_WIN0 + 0x000f).rw(FUNC(vt1200_state::lance_rdp_r), FUNC(vt1200_state::lance_rdp_w));
// The Ethernet address ROM at bit 0x6800000 (physical byte 0xD00000). It is
// a separate device, not a region of the firmware image: the known MAC does
// not appear anywhere in vt1200_v22.bin. EIGHT bytes, one per 16-bit step
// (addk 16,a7), read with movb (low byte of the word -- see lance_rom_r):
// six MAC bytes then a two-byte checksum, so eight 16-bit words.
map(0x6800000, 0x6800000 + 0x007f).r(FUNC(vt1200_state::lance_rom_r));
}
// 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 t=%.6f\n",
win, kind, reg, value, (unsigned)m_maincpu->state_int(TMS34010_PC),
(unsigned long long)m_maincpu->total_cycles(),
machine().time().as_double());
fflush(m_duartlog);
}
// --- LANCE (Am7990/Am79C90) -------------------------------------------------
//
// The register protocol is the chip's own: a write to RAP selects one of CSR0-3,
// and RDP then reads/writes that register. The firmware's sequence (plan
// section 1.1) is the textbook "program IADR/CSR1-3, then CSR0 INIT":
//
// RAP=1; RDP=addr_low ; RAP=2; RDP=addr_high ; RAP=3; RDP=0 ; RAP=0; RDP=0x7f41
//
// MAME's regs_r/regs_w take a single offset: 0 selects RDP, non-zero selects RAP.
// So the RDP handlers call regs_r/regs_w(space, 0) and the RAP handlers call
// regs_r/regs_w(space, 1).
u16 vt1200_state::lance_rdp_r(offs_t)
{
const u16 v = m_lance->regs_r(m_maincpu->space(AS_PROGRAM), 0);
log_lance('R', "RDP", v);
return v;
}
void vt1200_state::lance_rdp_w(offs_t, u16 data)
{
log_lance('W', "RDP", data);
m_lance->regs_w(0, data);
}
u16 vt1200_state::lance_rap_r(offs_t)
{
const u16 v = m_lance->regs_r(m_maincpu->space(AS_PROGRAM), 1);
log_lance('R', "RAP", v);
return v;
}
void vt1200_state::lance_rap_w(offs_t, u16 data)
{
log_lance('W', "RAP", data);
m_lance->regs_w(1, data);
}
u16 vt1200_state::lance_rom_r(offs_t off)
{
// The board's Ethernet address ROM is EIGHT bytes, not six: the firmware's
// reader at 0x3FDD7520 loops a9 = 0..7 (cmpi 0x7 / jrle), then computes a
// folded ones-complement checksum over bytes 0..5 and compares it to
// (rom[6] << 8) | rom[7] at 0x3FDD78B0-0x3FDD7930. That is the "format and
// checksum" the service guide describes, and it is why a six-byte ROM fails
// LEVEL1/2 ERROR -> ETHERNET ROM CHECKSUM - 23.
//
// Bytes 0..5 are DEC's OUI 08:00:2b plus this unit's serial 16:4b:5a
// (NEXT_SESSION.md section 6). Bytes 6..7 are the checksum the firmware
// itself computes for those six bytes (derivation in LANCE_REPORT.md); a
// real PROM would carry the same value because it must validate. The
// TMS34010 byte load takes the LOW byte of the 16-bit word (RBYTE ->
// RFIELDMAC(0xff,9): RDMEM_WORD >> (bit & 0xf)), so byte i goes in the low
// byte of word i.
static const u8 rom[8] = {
0x08, 0x00, 0x2b, 0x16, 0x4b, 0x5a, // MAC (DEC OUI + unit serial)
0xc1, 0x86 // checksum over the MAC
};
const u16 v = (off < 8) ? u16(rom[off]) : 0x0000;
log_lance('R', "ROM", v);
return v;
}
void vt1200_state::log_lance(char kind, const char *what, u16 value)
{
if (!m_lancelog)
return;
fprintf(m_lancelog, "%c %-3s val=%04X pc=%08X cyc=%llu t=%.6f\n",
kind, what, value, (unsigned)m_maincpu->state_int(TMS34010_PC),
(unsigned long long)m_maincpu->total_cycles(), machine().time().as_double());
fflush(m_lancelog);
}
void vt1200_state::log_lance_dma(char kind, u32 addr, u16 value, u16 mask)
{
if (!m_lance_dmalog)
return;
fprintf(m_lance_dmalog, "%c %06X %04X %04X pc=%08X cyc=%llu t=%.6f\n",
kind, addr, value, mask, (unsigned)m_maincpu->state_int(TMS34010_PC),
(unsigned long long)m_maincpu->total_cycles(), machine().time().as_double());
fflush(m_lance_dmalog);
}
// 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));
// LANCE Ethernet. AM79C90 (C-LANCE) with the 12.5 MHz reference clock the
// DEC kn01 uses; the clock only paces the chip's poll/interrupt timers, so
// any plausible value satisfies the register tests and INIT (plan risk 3).
//
// Interrupt -> INT1 (line 0). The LANCE ISR 0x3FA93400 is installed on the
// INT1 slot and 0x3FA8C340-470 then enables INT1 (INTENB bit 1 = 0x0002) --
// NOT INT2, where DUART1 sits in the OS phase (see the plan, section 1.2).
// /INTR is active-low, hence .invert(). During POST the chip is STOPped and
// INEA is clear, so it never asserts, and the same line's POST occupant
// (DUART1) is unaffected.
//
// Bus-master DMA, below: the LANCE addresses memory in BYTES, while the
// TMS34010 program space is bit-addressed (each 16-bit word spans 16
// address units), so LANCE byte B is main-RAM word B>>1. Cross-checked
// against the firmware's own init block address: it programs CSR1 = 0xFF60,
// which is bit 0x7fb00 = main-RAM word 0x7FB0, and the init block IS at
// byte 0xFF60 in the RAM dump (mode 0x004C, PADR 08:00:2b:16:4b:5a).
AM79C90(config, m_lance, XTAL(12'500'000));
m_lance->intr_out().set_inputline(m_maincpu, 0).invert();
m_lance->dma_in().set([this](offs_t o) -> u16 {
const offs_t w = o >> 1;
u16 v;
if (w < 0x00200000) // 4 MiB of main RAM = 0x200000 words
v = m_mainram[w];
else
{
log_lance('d', "DMA-RANGE", u16(w >> 16));
v = 0x0000;
}
log_lance_dma('r', u32(o), v, 0xffff);
return v;
});
m_lance->dma_out().set([this](offs_t o, u16 data, u16 mem_mask) {
log_lance_dma('w', u32(o), data, mem_mask);
const offs_t w = o >> 1;
if (w < 0x00200000)
{
COMBINE_DATA(&m_mainram[w]);
}
else
{
log_lance('d', "DMA-RANGE", u16(w >> 16));
}
});
// The real devices on DUART2: channel A = keyboard (4800 8N1), channel B =
// the pointing device (4800 8O1), exactly as the board wires them. Both
// talk to the SCN2681 through the genuine device_serial_interface bit lines:
// the device's transmit output drives the DUART's receiver input
// (rx_a_w / rx_b_w), and the DUART's transmitter output (a_tx_cb / b_tx_cb)
// drives the device's receiver input (rx_w). No byte-level shortcut, so the
// framing and the 4800-baud timing are the DUART's own.
//
// The VSXXX-AB tablet is the DEFAULT device on channel B. This is a
// DELIBERATE divergence from the target machine: the real terminal almost
// certainly has a VSXXX-AA mouse attached, but the tablet reports the host
// pointer's ABSOLUTE position, so the emulated cursor tracks the host
// pointer while interacting with the terminal (the mouse only integrates
// relative deltas). Exactly the same kind of divergence as
// VT1200_SKIP_LEVEL1 and VT1200_IO_PATH_STUB.
//
// VT1200_MOUSE=1 forces the VSXXX-AA mouse (the faithful device) back.
// This is a VALUE-AWARE inverse switch, not a presence check: the default
// is expressed in code, not by setting a variable in a wrapper script, so
// the choice stays the user's and can always be flipped either way.
// VT1200_MOUSE=0 (or empty) leaves the tablet selected. The two parts are
// the same connector; the terminal tells them apart from the self-test ID,
// so only one is instantiated.
const char *force_mouse = getenv("VT1200_MOUSE");
m_tablet_selected = !(force_mouse && *force_mouse && strcmp(force_mouse, "0") != 0);
VT1200_LK401(config, m_lk401, 0);
if (m_tablet_selected)
{
VT1200_VSXXXAB(config, m_tablet, 0);
m_tablet->tx_handler().set(m_duart2, FUNC(scn2681_device::rx_b_w));
}
else
{
VT1200_VSXXXAA(config, m_mouse, 0);
m_mouse->tx_handler().set(m_duart2, FUNC(scn2681_device::rx_b_w));
}
m_lk401->tx_handler().set(m_duart2, FUNC(scn2681_device::rx_a_w));
m_duart2->a_tx_cb().set(FUNC(vt1200_state::duart2_a_tx_w));
m_duart2->b_tx_cb().set(FUNC(vt1200_state::duart2_b_tx_w));
}
// These two just fan the DUART's serial transmitter output into the attached
// device's receiver. (A state-level forward keeps the devcb binding simple;
// rx_w() is public on device_serial_interface.)
void vt1200_state::duart2_a_tx_w(int state) { m_lk401->rx_w(state); }
void vt1200_state::duart2_b_tx_w(int state)
{
if (m_tablet_selected)
m_tablet->rx_w(state);
else
m_mouse->rx_w(state);
}
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
//**************************************************************************
// vt1200_lk401_device -- HLE model of the DEC LK401 keyboard
//**************************************************************************
//
// The protocol comes from reference/keyboard-mouse/PROTOCOL.md. This is not
// the keyboard's firmware; it is a model of what the firmware does on the wire.
// It answers `FD` with the documented 4-byte self-test because that is what the
// part does, and it reports key transitions with the documented keycodes.
DEFINE_DEVICE_TYPE(VT1200_LK401, vt1200_lk401_device, "vt1200lk401", "DEC LK401 Keyboard")
vt1200_lk401_device::vt1200_lk401_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: device_t(mconfig, VT1200_LK401, tag, owner, clock)
, device_buffered_serial_interface(mconfig, *this)
, m_tx_handler(*this)
, m_kbd(*this, "K%u", 0U)
{
}
ioport_constructor vt1200_lk401_device::device_input_ports() const
{
return INPUT_PORTS_NAME(vt1200_lk401);
}
void vt1200_lk401_device::device_start()
{
m_scan_timer = timer_alloc(FUNC(vt1200_lk401_device::scan), this);
m_power_up_timer = timer_alloc(FUNC(vt1200_lk401_device::power_up_selftest), this);
if (const char *p = getenv("VT1200_INPUT_LOG"))
m_log = fopen(p, "a");
if (m_log)
fprintf(m_log, "# LK401 device started\n");
}
void vt1200_lk401_device::device_reset()
{
// 4800 baud, 8 data bits, no parity, 1 stop bit (PROTOCOL.md section 3.1).
set_data_frame(1, 8, PARITY_NONE, STOP_BITS_1);
set_rate(4800);
clear_fifo();
receive_register_reset();
m_cmd.clear();
m_down.fill(false);
m_repeat.fill(attotime::never);
m_updown_down = 0;
m_autorepeat = true;
m_lk401_mode = false;
m_inhibit = false;
m_led_mask = 0;
m_lost_keys = false;
for (int i = 1; i <= 14; i++)
m_div_mode[i] = 0; // down-only at power-up
// The real keyboard runs its self-test and transmits the 4-byte result a
// few tens of ms after power-up, before the host has configured the port.
// Queued faithfully; if the DUART channel is not yet clocked the bytes are
// discarded, which is exactly what happens on real hardware.
if (m_power_up_timer)
m_power_up_timer->adjust(attotime::from_msec(70));
// Poll the host keyboard at 120 Hz. Each scan reads the ioport state and
// turns key transitions into keycodes.
if (m_scan_timer)
m_scan_timer->adjust(attotime::from_hz(120), 0, attotime::from_hz(120));
}
bool vt1200_lk401_device::lk401_only(uint8_t code)
{
// Codes only reported after the `E9` LK401-mode command.
return code == 0xab || code == 0xac || code == 0xad || code == 0xb2;
}
void vt1200_lk401_device::queue(u8 byte)
{
if (m_log)
fprintf(m_log, "LK401 tx %02X t=%.6f\n", byte, machine().time().as_double());
transmit_byte(byte);
}
void vt1200_lk401_device::tra_callback()
{
m_tx_handler(transmit_register_get_data_bit());
}
void vt1200_lk401_device::received_byte(u8 byte)
{
if (m_log)
fprintf(m_log, "LK401 rx %02X t=%.6f\n", byte, machine().time().as_double());
// A command runs until a byte with bit 7 set arrives (PARAM bit: 1 = no
// parameter follows). See PROTOCOL.md section 3.2 and the netbsd LK201
// interface description.
m_cmd.push_back(byte);
if (byte & 0x80)
{
process_command();
m_cmd.clear();
}
}
void vt1200_lk401_device::process_command()
{
if (m_cmd.empty())
return;
const u8 op = m_cmd[0];
switch (op)
{
case LK_CMD_POWER_UP:
run_self_test();
return;
case LK_CMD_REQUEST_ID:
// Returns the same 2 bytes as the power-up self test (PROTOCOL.md 3.3).
queue(LK_SELFTEST[0]);
queue(LK_SELFTEST[1]);
return;
case LK_CMD_SET_DEFAULTS:
m_autorepeat = true;
m_lk401_mode = false;
for (int i = 1; i <= 14; i++)
m_div_mode[i] = 0;
return;
case LK_CMD_ENABLE_LK401:
m_lk401_mode = true;
return;
case LK_CMD_LED_ON:
if (m_cmd.size() >= 2)
m_led_mask |= (m_cmd[1] & 0x0f);
return;
case LK_CMD_LED_OFF:
if (m_cmd.size() >= 2)
m_led_mask &= ~(m_cmd[1] & 0x0f);
return;
case LK_CMD_KEYCLICK_ON:
case LK_CMD_KEYCLICK_OFF:
case LK_CMD_CTRLCLICK_ON:
case LK_CMD_CTRLCLICK_OFF:
case LK_CMD_BELL_ON:
case LK_CMD_BELL_OFF:
case LK_CMD_BELL:
case LK_CMD_SOUND_CLICK:
case LK_CMD_RPT_TO_DOWN:
return;
case LK_CMD_ENB_RPT:
m_autorepeat = true;
return;
case LK_CMD_DIS_RPT:
m_autorepeat = false;
return;
case LK_CMD_MMS_NO_RPT:
// Temporary auto-repeat inhibit for the key currently down.
return;
case LK_CMD_RESUME:
m_inhibit = false;
if (m_lost_keys)
{
m_lost_keys = false;
queue(LK_OUTPUT_ERROR);
}
return;
case LK_CMD_TEST_MODE:
queue(LK_TEST_MODE_ACK);
return;
default:
break;
}
// Mode commands: bit 7 set, bit 0 clear, division in bits 3-6, mode in
// bits 1-2. Also reached with bit 7 clear (an autorepeat-register
// association byte follows) -- same division/mode fields. The keyboard
// acknowledges a successful change with BA.
if (!(op & 0x01) && (op & 0x78))
{
const int div = (op >> 3) & 0x0f;
const int mode = (op >> 1) & 0x03;
if (div >= 1 && div <= 14)
m_div_mode[div] = u8(mode);
queue(LK_MODE_CHANGE_ACK);
return;
}
queue(LK_INPUT_ERROR);
}
void vt1200_lk401_device::run_self_test()
{
// `FD` resets the keyboard and re-runs the self-test. Healthy result is
// the documented four bytes; a stuck key would change byte 3/4 but we have
// no stuck keys here.
for (unsigned i = 0; i < sizeof(LK_SELFTEST); i++)
queue(LK_SELFTEST[i]);
}
TIMER_CALLBACK_MEMBER(vt1200_lk401_device::power_up_selftest)
{
run_self_test();
}
void vt1200_lk401_device::emit_key(unsigned index, bool pressed)
{
const uint8_t code = VT1200_LK401_TABLE[index].code;
const uint8_t div = VT1200_LK401_TABLE[index].div;
if (lk401_only(code) && !m_lk401_mode)
return; // LK401-only key, host has not enabled LK401 mode
if (m_inhibit)
{
m_lost_keys = true;
return;
}
const int mode = m_div_mode[div];
if (pressed)
{
queue(code);
if (mode == 3)
m_updown_down++;
else if (mode == 1 && m_autorepeat)
m_repeat[index] = machine().time() + attotime::from_msec(500);
}
else
{
if (mode == 3)
{
if (m_updown_down > 0)
m_updown_down--;
if (m_updown_down == 0)
queue(LK_ALL_KEYS_UP);
else
queue(code);
}
m_repeat[index] = attotime::never;
}
}
TIMER_CALLBACK_MEMBER(vt1200_lk401_device::scan)
{
// A UART's TxD idles at mark (high). Driving the idle level matters for
// the SCN2681's receiver: after its receiver is reset it needs to see an
// explicit idle-high before the next start bit, or it synchronises late and
// mis-samples the byte. Keeping the line high whenever we are not mid-byte
// is both faithful and what makes the link work.
if (fifo_empty() && is_transmit_register_empty())
m_tx_handler(1);
const attotime now = machine().time();
for (unsigned i = 0; i < VT1200_LK401_NKEYS; i++)
{
const bool down = (m_kbd[i]->read() & 1) != 0;
if (down != m_down[i])
{
m_down[i] = down;
emit_key(i, down);
}
if (down && m_repeat[i] != attotime::never && now >= m_repeat[i] && m_autorepeat && !m_inhibit)
{
// Auto-repeat tick (B4) for an auto-repeat-down key.
queue(LK_METRONOME);
m_repeat[i] += attotime::from_hz(20);
}
}
}
//**************************************************************************
// vt1200_vsxxx_base -- shared DEC VSXXX serial pointer interface
//**************************************************************************
vt1200_vsxxx_base::vt1200_vsxxx_base(const machine_config &mconfig, device_type type, const char *tag,
device_t *owner, uint32_t clock, const char *x_tag, const char *y_tag, const char *btn_tag)
: device_t(mconfig, type, tag, owner, clock)
, device_buffered_serial_interface(mconfig, *this)
, m_tx_handler(*this)
, m_host_x(*this, x_tag)
, m_host_y(*this, y_tag)
, m_host_buttons(*this, btn_tag)
{
}
void vt1200_vsxxx_base::device_start()
{
m_scan_timer = timer_alloc(FUNC(vt1200_vsxxx_base::scan), this);
m_power_up_timer = timer_alloc(FUNC(vt1200_vsxxx_base::power_up_selftest), this);
if (const char *p = getenv("VT1200_INPUT_LOG"))
m_log = fopen(p, "a");
if (m_log)
fprintf(m_log, "# %s device started\n", shortname());
}
void vt1200_vsxxx_base::device_reset()
{
// 4800 baud, 8 data bits, ODD parity, 1 stop bit (PROTOCOL.md section 4.1).
set_data_frame(1, 8, PARITY_ODD, STOP_BITS_1);
set_rate(4800);
clear_fifo();
receive_register_reset();
m_stream = false; // prompt mode is the power-up default
if (m_power_up_timer)
m_power_up_timer->adjust(attotime::from_msec(250));
if (m_scan_timer)
m_scan_timer->adjust(attotime::from_hz(120), 0, attotime::from_hz(120));
}
void vt1200_vsxxx_base::queue(u8 byte)
{
if (m_log)
fprintf(m_log, "VSXXX tx %02X t=%.6f\n", byte, machine().time().as_double());
transmit_byte(byte);
}
void vt1200_vsxxx_base::tra_callback()
{
m_tx_handler(transmit_register_get_data_bit());
}
void vt1200_vsxxx_base::received_byte(u8 byte)
{
if (m_log)
fprintf(m_log, "VSXXX rx %02X (%c) t=%.6f\n", byte, (byte >= 0x20 && byte < 0x7f) ? byte : '.', machine().time().as_double());
switch (byte)
{
case VS_CMD_SELFTEST:
send_self_test();
break;
case VS_CMD_STREAM:
m_stream = true;
on_stream_mode();
break;
case VS_CMD_PROMPT:
m_stream = false;
break;
case VS_CMD_POSITION:
m_stream = false;
emit_position_report();
break;
case VS_CMD_FORMAT:
// Standard format: already the only one modelled.
break;
default:
break; // invalid commands are ignored
}
}
TIMER_CALLBACK_MEMBER(vt1200_vsxxx_base::power_up_selftest)
{
send_self_test();
}
TIMER_CALLBACK_MEMBER(vt1200_vsxxx_base::scan)
{
// Hold TxD at idle-high when not transmitting (see the LK401 scan note).
if (fifo_empty() && is_transmit_register_empty())
m_tx_handler(1);
sample_host();
if (m_stream && stream_report_due())
emit_stream_report();
}
//**************************************************************************
// vt1200_vsxxxaa_device -- DEC VSXXX-AA mouse (relative)
//**************************************************************************
DEFINE_DEVICE_TYPE(VT1200_VSXXXAA, vt1200_vsxxxaa_device, "vt1200vsxxxaa", "DEC VSXXX-AA Mouse")
vt1200_vsxxxaa_device::vt1200_vsxxxaa_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: vt1200_vsxxx_base(mconfig, VT1200_VSXXXAA, tag, owner, clock, "MOUSE_X", "MOUSE_Y", "MOUSE_BTN")
{
}
ioport_constructor vt1200_vsxxxaa_device::device_input_ports() const
{
return INPUT_PORTS_NAME(vt1200_vsxxxaa);
}
void vt1200_vsxxxaa_device::device_reset()
{
vt1200_vsxxx_base::device_reset();
m_last_x = u8(m_host_x->read());
m_last_y = u8(m_host_y->read());
m_acc_dx = 0;
m_acc_dy = 0;
m_last_buttons = 0;
m_buttons = 0;
}
void vt1200_vsxxxaa_device::sample_host()
{
const u8 now_x = u8(m_host_x->read());
const u8 now_y = u8(m_host_y->read());
// The MAME mouse inputs are relative; MAME accumulates them into the port
// value, so a signed 8-bit difference (with wrap) is the movement since the
// last scan. Screen Y grows downward; the VSXXX reports DEC Y (up).
int dx = int(now_x) - int(m_last_x);
int dy = int(now_y) - int(m_last_y);
if (dx > 0x80) dx -= 0x100; else if (dx < -0x80) dx += 0x100;
if (dy > 0x80) dy -= 0x100; else if (dy < -0x80) dy += 0x100;
m_last_x = now_x;
m_last_y = now_y;
if (dx || dy)
{
m_acc_dx += dx;
m_acc_dy -= dy; // DEC Y is inverted relative to screen Y
}
m_buttons = m_host_buttons->read() & 0x07;
}
bool vt1200_vsxxxaa_device::stream_report_due()
{
return m_acc_dx || m_acc_dy || m_buttons != m_last_buttons;
}
void vt1200_vsxxxaa_device::emit_stream_report()
{
send_report(m_acc_dx, m_acc_dy, m_buttons);
m_acc_dx = m_acc_dy = 0;
m_last_buttons = m_buttons;
}
void vt1200_vsxxxaa_device::emit_position_report()
{
send_report(m_acc_dx, m_acc_dy, m_buttons);
m_acc_dx = m_acc_dy = 0;
m_last_buttons = m_buttons;
}
void vt1200_vsxxxaa_device::send_self_test()
{
// Four-byte power-on/self-test report (PROTOCOL.md section 4.4, VCB02 figure
// C-5): header 101 + revision, manufacturer 0 + device code 0010 (mouse),
// error 00, no buttons.
queue(0xa0); // 1010 0000: header 101, revision 0
queue(0x02); // manufacturer 0, device code 0010 = mouse
queue(0x00); // error code 0 = OK
queue(0x00); // no buttons
}
void vt1200_vsxxxaa_device::send_report(int dx, int dy, int buttons)
{
int sx = (dx >= 0) ? 1 : 0;
int sy = (dy >= 0) ? 1 : 0;
int ax = (dx < 0) ? -dx : dx;
int ay = (dy < 0) ? -dy : dy;
if (ax > 0x7f) ax = 0x7f;
if (ay > 0x7f) ay = 0x7f;
const u8 b0 = u8(0x80 | (sx << 4) | (sy << 3) | (buttons & 0x07));
queue(b0);
queue(u8(ax));
queue(u8(ay));
}
//**************************************************************************
// vt1200_vsxxxab_device -- DEC VSXXX-AB tablet (absolute)
//**************************************************************************
//
// Same connector, same 4800 8O1 serial format and the same command set as the
// mouse (VCB02 C.7.4). The two differences are the self-test device code
// (0100) and the 5-byte absolute position report (header 110). Position comes
// from the host pointer via IPT_LIGHTGUN_X/Y, so the emulated cursor is
// positionally mapped rather than integrated from deltas.
DEFINE_DEVICE_TYPE(VT1200_VSXXXAB, vt1200_vsxxxab_device, "vt1200vsxxxab", "DEC VSXXX-AB Tablet")
// The tablet's coordinate space is its active area: 11 in square at 200
// counts/in = 2200 counts, so 0..2199 (VCB02 Appendix D.1). The 12-bit report
// field (figure D-3) can carry a larger value, but the part only reports inside
// its active area. The host absolute pointer is scaled into this range by the
// ioport layer; see INPUT_PORTS_NAME(vt1200_vsxxxab).
static const int VSXXX_ABS_MAX = 2199;
vt1200_vsxxxab_device::vt1200_vsxxxab_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: vt1200_vsxxx_base(mconfig, VT1200_VSXXXAB, tag, owner, clock, "TABLET_X", "TABLET_Y", "TABLET_BTN")
{
}
ioport_constructor vt1200_vsxxxab_device::device_input_ports() const
{
return INPUT_PORTS_NAME(vt1200_vsxxxab);
}
void vt1200_vsxxxab_device::device_reset()
{
vt1200_vsxxx_base::device_reset();
m_x = 0;
m_y = 0;
m_buttons = 0;
m_last_x = -1; // force a first report when stream mode starts
m_last_y = -1;
m_last_buttons = 0;
}
void vt1200_vsxxxab_device::on_stream_mode()
{
m_last_x = -1; // send the current absolute position at once
m_last_y = -1;
}
void vt1200_vsxxxab_device::sample_host()
{
// The port already scales the host pointer into 0..VSXXX_ABS_MAX; clamp
// defensively (do NOT mask -- VSXXX_ABS_MAX is a value, not a bit mask).
m_x = int(m_host_x->read());
m_y = int(m_host_y->read());
if (m_x < 0) m_x = 0;
if (m_x > VSXXX_ABS_MAX) m_x = VSXXX_ABS_MAX;
if (m_y < 0) m_y = 0;
if (m_y > VSXXX_ABS_MAX) m_y = VSXXX_ABS_MAX;
m_buttons = m_host_buttons->read() & 0x1e; // B4..B1 (Pr is added on send)
}
bool vt1200_vsxxxab_device::stream_report_due()
{
return m_x != m_last_x || m_y != m_last_y || m_buttons != m_last_buttons;
}
void vt1200_vsxxxab_device::emit_stream_report()
{
send_abs(m_x, m_y, m_buttons);
m_last_x = m_x;
m_last_y = m_y;
m_last_buttons = m_buttons;
}
void vt1200_vsxxxab_device::emit_position_report()
{
sample_host();
send_abs(m_x, m_y, m_buttons);
}
void vt1200_vsxxxab_device::send_self_test()
{
// Same 4-byte self-test format as the mouse, but device code 0100 = tablet
// (VCB02 C.6.5 figure C-5 and C.7.4). This is what a host reads to learn
// that the pointing device is a tablet rather than a mouse.
queue(0xa0); // 1010 0000: header 101, revision 0
queue(0x04); // manufacturer 0, device code 0100 = tablet
queue(0x00); // error code 0 = OK
queue(0x00); // no buttons
}
void vt1200_vsxxxab_device::send_abs(int x, int y, int buttons)
{
// VSXXX-AB absolute position report, 5 bytes, header 110 (VCB02 D.3 /
// figure D-3):
// [0] 1 1 0 B4 B3 B2 B1 Pr
// [1] 0 0 X5 X4 X3 X2 X1 X0
// [2] 0 0 X11 X10 X9 X8 X7 X6
// [3] 0 0 Y5 Y4 Y3 Y2 Y1 Y0
// [4] 0 0 Y11 Y10 Y9 Y8 Y7 Y6
// Y is reported DEC-style (up); the host pointer's Y is screen-style (down),
// so it is inverted. Pr is asserted: the emulated pointer is always in
// proximity, so the host always has a valid position.
if (x < 0) x = 0;
if (x > VSXXX_ABS_MAX) x = VSXXX_ABS_MAX;
if (y < 0) y = 0;
if (y > VSXXX_ABS_MAX) y = VSXXX_ABS_MAX;
const int dy = VSXXX_ABS_MAX - y;
const u8 b0 = u8(0xc0 | (buttons & 0x1e) | 0x01);
queue(b0);
queue(u8(x & 0x3f));
queue(u8((x >> 6) & 0x3f));
queue(u8(dy & 0x3f));
queue(u8((dy >> 6) & 0x3f));
}
// 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)