Files
vt1200/harness/mame/vt1200.cpp
T
arti 02acabb7ef harness: real LK401 keyboard + VSXXX-AA mouse devices on DUART2
Add the terminal's actual input parts to the harness as device models on the
already-modelled SCN2681 at DUART2: a DEC LK401 keyboard (channel A, 4800 8N1)
and a DEC VSXXX-AA mouse (channel B, 4800 8O1), both driven by MAME's host
input and wired bit-by-bit to the DUART (rx_a_w/rx_b_w and a_tx_cb/b_tx_cb).

Route: the docs, not MAME's lk201. MAME's lk201 device executes the 68HC05
firmware 23-001s9-00.bin, which is not in our ROM set (the downloaded zip only
carries the 8051 23-004M2-00.bin, MAME's unused second region), so route 1 is
unusable without sourcing that ROM. The models follow PROTOCOL.md: the keyboard
answers FD with the 4-byte self-test and reports documented keycodes honouring
the division modes; the mouse answers T with its 4-byte self-test and streams
3-byte incremental reports.

One physical-layer detail was required: the SCN2681 receiver mis-samples the
first byte after a receiver reset unless the line is explicitly idle-high
before the start bit. Both models now hold TxD at mark when idle (which is
faithful anyway) -- isolated evidence: bit-banging 0x55 into rx_a_w gives
RHRA=D5 with no idle-high, RHRA=55 with four idle-high bits.

Verified (VT1200_SKIP_LEVEL1=1, input_inject.lua): the OS reads the keyboard
keycode 0xC2 (A) and ~900 bytes of VSXXX reports (dx=+16/+32); after the
self-test it switches the mouse to stream mode; the X11 cursor arrow is visible
at frame 4400 and has moved by frame 4600.

Nothing was tuned for the level-0 I/O data-path test: with no env the boot
still fails it honestly and the regression baseline is unchanged
(PC=3FDCE400 samples=198, 3 PCs). VT1200_IO_PATH_STUB=1 (53/75) and the skip
run (Terminal Manager at frame 3000) are also unchanged. ROM md5 unchanged.

See harness/mame/INPUT_REPORT.md.
2026-10-16 15:33:30 +03:00

1875 lines
76 KiB
C++

// license:BSD-3-Clause
// copyright-holders: VT1200 reverse-engineering project
/***************************************************************************
DEC VT1200 X terminal -- MAME execution harness
This is NOT an accurate hardware emulation. It exists to give us a way
to *execute* the VT1200 firmware under the TMS34010 interpreter with a
debugger (breakpoints, single step, memory watch, trace).
What is modelled BY DEFAULT. A bare `vt1200 vt1200` boots the way the
old six-variable incantation
VT1200_VRAM=1 VT1200_BOARD_MODEL=handshake VT1200_BOARD_BUSY_TICKS=2
VT1200_BOARD_SWITCH="0640000D:70:70" VT1200_DUART=1 VT1200_DUART_SEL=1
did. Five of those knobs are now the default and no longer exist; the
sixth, VT1200_BOARD_SWITCH, was superseded once DUART1's input port
(IP4-IP6) became the monitor-switch source.
* TMS34010 CPU (bit-addressed: MAME's tms34010 memory space uses a
right shift of 3, so address-map units are BITS).
* RAM at logical bit address 0, 4 MiB (byte 0 .. 0x3fffff).
* Video RAM at bit 0x04000000 (required for the level-1 VIDEO MEMORY
test to pass).
* The four undecoded board windows, driving the handshake register model
with a two-tick busy bit (required to leave early POST).
* MAME's SCN2681 at DUART1 (0x0640000 = host/printer), with its input
port IP4-IP6 held high -- which is how the driver reads the VR150
monitor switch (value 0x70). The level-1 host-port transmit is
ISR-driven, so a real DUART is required.
* The 1.75 MiB firmware ROM (physical byte base 0x07e40000 = bit
0x3f200000), mirrored into the three upper 1 GiB quadrants so that
the reset-vector page at logical bit 0xffffffe0 resolves to the last
four bytes of the image (file offset 0x1bfffc).
* MAME's SCN2681 at DUART2 (0x0620000 = keyboard (ch A) / mouse (ch B)),
wired to INT2. This is modelled unconditionally because the real
board has it and the OS's DUART interrupt dispatcher needs a real
chip. NOTE: with a real DUART2 and nothing attached to the keyboard
port, the *faithful* (no-env) boot now fails the level-0 I/O data path
test and drops into the interactive diagnostics menu; before DUART2
was modelled the board write-back latch accidentally faked that test
(see OS_BRINGUP_REPORT.md). VT1200_IO_PATH_STUB=1 adds an OPT-IN
stand-in for the unknown test fixture on that port (it is NOT a
keyboard model) so the count-only level-0 test can pass; the default
stays faithful and still fails.
What is NOT modelled: LANCE Ethernet and the board system logic beyond the
register model. The firmware therefore still stops at the level-1 HOST
PORT test and wedges in the system-board LED loop; that is expected.
---------------------------------------------------------------------
Instrumentation knobs (all opt-in; none changes execution):
VT1200_BOARD_LOG=<path> append a line for every *distinct*
(address, PC) board access, flushed
immediately. Shows every register the
firmware polls and the PC polling it.
VT1200_BOARD_MAP=<list> per-address overrides, e.g.
"0x06200010=1;0x0640000d=0"
VT1200_DUARTLOG=<path> every DUART access (not de-duplicated),
so a write that happens once per iteration
cannot hide.
VT1200_IRQLOG=<path> DUART1 IRQ assert: ISR/SRA at the moment of
the interrupt.
VT1200_LOG_UNMAPPED=<path> catch-all over the whole address space;
anything the driver does not model is
logged once per distinct (address, PC,
direction).
VT1200_DPYLOG=<path> dump the TMS34010 display params, one line
per scanline, for the first
VT1200_DPYLOG_FRAMES frames (default 1).
VT1200_DUMP_FRAME=<n> write frame <n> as vt1200-frame-<n>.ppm
VT1200_DUMP_RAM=<n> write main RAM at frame <n> as
vt1200-ram-<n>.bin
VT1200_DUMP_VRAM=<n> write video memory at frame <n> as
vram-<n>.bin
VT1200_DUMP_FRAMES=<list> comma-separated frames, all in ONE run, so the
same machine can be sampled at several
increasing times and the samples compared.
Same for VT1200_DUMP_RAM_FRAMES and
VT1200_DUMP_VRAM_FRAMES.
VT1200_DUMP_DIR=<dir> directory for the dumps (default ".")
VT1200_NO_RENDER=1 blank the screen instead of painting it
Behaviour switch (deliberately diverges from the real terminal):
VT1200_SKIP_LEVEL1=1 drive DUART1's input-port bit 3 high. The
real terminal reads IP3 low, so with this
UNSET the boot runs the whole level-1
self-test batch (the faithful default).
With it set, the boot gate at bit
0x3F2042D0 (`@0x64000D0` bit 3 set and the
0x6200000 status handshake not 0x55)
branches to the OS cold-start 0x3F5788C0,
skipping ALL of the level-1 batch. Because
the OS phase is then in charge, the switch
ALSO routes DUART1's IRQ to INT2 (line 1):
the OS's DUART1 driver attaches there,
while POST's DUART dispatcher sits on INT1
(line 0), the default. Both lines are
enabled (INTENB = 0x0606) and the RAM stub
at 0x0F60 is rewritten between the two
phases, so the two wirings correspond to
the two phases' own routings. See
reference/level1-bypass-analysis.md.
VT1200_IO_PATH_STUB=1 feed 10 bytes into DUART2 channel A after
the level-0 I/O data path test writes 0xE1
to THRA. This is a STAND-IN for an unknown
test fixture on the keyboard port -- it is
NOT a keyboard model, and the bytes are
arbitrary (0x00) because the firmware checks
only the COUNT (it stores the received byte
and never compares it, and it does not
enable loopback). No documented
keyboard/mouse/tablet message is 10 bytes
long, so no documented device can satisfy
this test; see reference/level0-10byte-puzzle.md.
The stub replies only while the channel is
polled (IMR = 0x00), so it cannot inject
into the interrupt-enabled OS phase or
raise an interrupt there. Unset (the
default) the level-0 test fails honestly
and the boot stops in the diagnostics
menu.
Video: the driver paints the active scanlines itself, reading the
framebuffer out of video RAM (see render_active_line).
See FULL_BOOT_ESTIMATE.md section 4b for measured results and
OS_BRINGUP_REPORT.md for the DUART2 / level-0 analysis.
***************************************************************************/
#include "emu.h"
#include "cpu/tms34010/tms34010.h"
#include "diserial.h"
#include "emupal.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
// --- VSXXX-AA mouse ---------------------------------------------------------
// 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`).
enum : uint8_t
{
MS_CMD_STREAM = 'R',
MS_CMD_PROMPT = 'D',
MS_CMD_POSITION = 'P',
MS_CMD_SELFTEST = 'T',
MS_CMD_FORMAT = 'S'
};
class vt1200_vsxxxaa_device : public device_t, public device_buffered_serial_interface<16U>
{
public:
vt1200_vsxxxaa_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 send_self_test();
void send_report(int dx, int dy, int buttons);
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)
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;
bool m_buttons_dirty = false;
FILE *m_log = nullptr;
};
DECLARE_DEVICE_TYPE(VT1200_LK401, vt1200_lk401_device)
DECLARE_DEVICE_TYPE(VT1200_VSXXXAA, vt1200_vsxxxaa_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_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, mouse on channel B,
// as the board has them. Wired bit-by-bit to the SCN2681's serial lines.
required_device<vt1200_lk401_device> m_lk401;
required_device<vt1200_vsxxxaa_device> m_mouse;
void duart2_a_tx_w(int state);
void duart2_b_tx_w(int state);
// VT1200_IO_PATH_STUB=1: opt-in stand-in for the unknown test fixture on
// the keyboard port, so level-0's count-only I/O data path test can pass.
// NOT a keyboard model. See iopath_stub_arm() and the README.
bool m_iopath_stub = false;
int m_iopath_supply = 0; // bytes still to feed for the current 0xE1
u8 m_duart2_imr = 0; // last value written to DUART2 IMR (reg 5)
bool m_iopath_irq_reported = false;
void iopath_stub_arm(); // on the level-0 0xE1 THRA write
void iopath_stub_refill(); // after a channel-A RHRA read
// The four undecoded board windows, owned by the standalone model in
// vt1200_board.h so it can be unit-tested without MAME. The driver only
// forwards its window callbacks and configures the model from the env knobs.
vt1200_board m_board;
std::map<std::pair<u32, u32>, u32> m_seen; // (addr, pc) -> count
FILE *m_log = nullptr;
// Video. Rendering is on by default now that the firmware reaches the
// display code; VT1200_NO_RENDER=1 restores the old blank-everything
// behaviour. VT1200_DUMP_FRAME=<n> writes that frame as a PPM.
//
// The *_FRAMES variants take a comma-separated list and dump at EXACTLY
// each frame, so one run can sample the same machine at several increasing
// times -- the instrument for telling "progressing" from "stuck": if two
// samples minutes apart are byte-identical, nothing is being drawn.
bool m_render = true;
long m_dump_frame = -1;
std::vector<long> m_dump_frames; // VT1200_DUMP_FRAMES=3000,10000,...
std::string m_dump_dir = ".";
// VT1200_DPYLOG=<path>: dump the TMS34010 display params for the first
// VT1200_DPYLOG_FRAMES frames (default 1), one line per visible scanline.
// Used to pin the VRAM fetch address/geometry against the real machine's
// 1280x1024 (160 bytes/line) numbers.
FILE *m_dpylog = nullptr;
long m_dpylog_frames = 1;
// VT1200_DUMP_RAM=<frame> writes main RAM at that frame to
// vt1200-ram-<frame>.bin so the framebuffer can be located offline.
long m_dump_ram = -1;
bool m_dumped_ram = false;
std::vector<long> m_dump_ram_frames; // VT1200_DUMP_RAM_FRAMES=...
// VT1200_DUMP_VRAM=<frame> writes the video memory to vram-<frame>.bin so the
// framebuffer layout can be determined offline.
long m_dump_vram = -1;
bool m_dumped_vram = false;
std::vector<long> m_dump_vram_frames; // VT1200_DUMP_VRAM_FRAMES=...
optional_shared_ptr<u16> m_vram;
// VT1200_LOG_UNMAPPED=<path>
// A catch-all mapping over the whole address space, registered FIRST so every
// real region added afterwards takes precedence. Anything the firmware
// touches that we do not model lands here, and is logged once per distinct
// (address, PC, direction). This is how we find regions we have never
// mapped -- the video memory in particular.
FILE *m_unmapped_log = nullptr;
std::map<std::pair<u32, u32>, u32> m_unmapped_seen; // (addr, pc|dir) -> count
u16 unmapped_r(offs_t off);
void unmapped_w(offs_t off, u16 data);
void log_unmapped(const char *kind, u32 addr, const char *pc);
// VT1200_IRQLOG=<path>: log DUART1's IRQ assertions (ISR/SRA at the
// moment of the interrupt).
FILE *m_irqlog = nullptr;
// VT1200_DUARTLOG=<path>
// EVERY DUART access (not de-duplicated), so a write that happens once per
// iteration cannot hide. Used to prove whether the transmit register is
// ever written on the host-port test path.
FILE *m_duartlog = nullptr;
void log_duart(int win, char kind, u32 reg, u16 value);
};
// Parse a comma-separated frame list ("3000,10000,30000,60000") into a vector,
// ignoring empty items. Used by the multi-timestamp dump knobs.
static std::vector<long> parse_frame_list(const char *s)
{
std::vector<long> out;
if (!s)
return out;
std::string str(s);
size_t pos = 0;
while (pos <= str.size())
{
const size_t comma = str.find(',', pos);
std::string item = str.substr(pos, (comma == std::string::npos)
? std::string::npos : comma - pos);
// trim surrounding whitespace
const size_t b = item.find_first_not_of(" \t");
const size_t e = item.find_last_not_of(" \t");
if (b != std::string::npos)
out.push_back(strtol(item.substr(b, e - b + 1).c_str(), nullptr, 0));
pos = (comma == std::string::npos) ? str.size() + 1 : comma + 1;
}
return out;
}
void vt1200_state::machine_start()
{
if (const char *list = getenv("VT1200_BOARD_MAP"))
{
std::string s(list), item;
size_t pos = 0;
while (pos <= s.size())
{
size_t semi = s.find(';', pos);
item = s.substr(pos, (semi == std::string::npos) ? std::string::npos : semi - pos);
pos = (semi == std::string::npos) ? s.size() + 1 : semi + 1;
if (item.empty())
continue;
size_t eq = item.find('=');
if (eq == std::string::npos)
continue;
u32 a = u32(strtoul(item.substr(0, eq).c_str(), nullptr, 0));
u16 v = u16(strtoul(item.substr(eq + 1).c_str(), nullptr, 0));
m_board.set_override(a, v);
}
}
if (const char *path = getenv("VT1200_BOARD_LOG"))
{
m_log = fopen(path, "w");
if (m_log)
fprintf(m_log, "# kind addr pc value (bit addresses; distinct (addr,pc) only)\n");
}
// Handshake model: write-back registers + a self-clearing busy bit. The
// board model owns the state machine; these are the settled defaults (a
// two-tick busy bit is what leaves early POST).
m_board.set_model_handshake(true);
m_board.set_busy_ticks(2);
// Video: render by default; optionally dump a frame as PPM.
if (getenv("VT1200_NO_RENDER"))
m_render = false;
if (const char *f = getenv("VT1200_DUMP_FRAME"))
m_dump_frame = strtol(f, nullptr, 0);
if (const char *f = getenv("VT1200_DUMP_RAM"))
m_dump_ram = strtol(f, nullptr, 0);
if (const char *f = getenv("VT1200_DUMP_VRAM"))
m_dump_vram = strtol(f, nullptr, 0);
if (const char *f = getenv("VT1200_DUMP_FRAMES"))
m_dump_frames = parse_frame_list(f);
if (const char *f = getenv("VT1200_DUMP_RAM_FRAMES"))
m_dump_ram_frames = parse_frame_list(f);
if (const char *f = getenv("VT1200_DUMP_VRAM_FRAMES"))
m_dump_vram_frames = parse_frame_list(f);
if (const char *d = getenv("VT1200_DUMP_DIR"))
m_dump_dir = d;
if (const char *f = getenv("VT1200_DPYLOG"))
{
m_dpylog = fopen(f, "w");
if (m_dpylog)
fprintf(m_dpylog, "# line rowaddr coladdr yoff heblnk hsblnk veblnk vsblnk vcount\n");
}
if (const char *f = getenv("VT1200_DPYLOG_FRAMES"))
m_dpylog_frames = strtol(f, nullptr, 0);
if (const char *p = getenv("VT1200_LOG_UNMAPPED"))
{
m_unmapped_log = fopen(p, "w");
if (m_unmapped_log)
fprintf(m_unmapped_log,
"# kind addr pc -- accesses to memory the driver does NOT model\n"
"# address units are the address-map's; firmware operand = addr << 4\n");
}
if (const char *p = getenv("VT1200_IRQLOG"))
{
m_irqlog = fopen(p, "w");
if (m_irqlog)
fprintf(m_irqlog, "# DUART1 IRQ assert: ISR/SRA at the moment of the interrupt\n");
}
if (const char *p = getenv("VT1200_DUARTLOG"))
{
m_duartlog = fopen(p, "w");
if (m_duartlog)
fprintf(m_duartlog, "# win kind reg val pc -- every DUART access (not de-duplicated)\n");
}
// With the real DUART in place, board-level signals that used to be faked
// by the window model come from the DUART's input port (read register 0x0D:
// IP0-IP6, bit 7 always set). The monitor-selection switch is IP bits 4-6;
// driving them high selects the VR150 CRT (0x70), matching the value the
// firmware looks for. IP2 stays low (the value for the blit-status test at
// bit 0x3F204AA0).
m_duart1->ip4_w(1);
m_duart1->ip5_w(1);
m_duart1->ip6_w(1);
// VT1200_SKIP_LEVEL1=1 drives DUART1 IP3 high, which makes the boot gate at
// bit 0x3F2042D0 take the OS cold-start branch (0x3F5788C0) and skip the
// whole level-1 self-test batch. Since the OS phase is then in charge, the
// same switch routes DUART1's IRQ to INT2 (line 1): the OS's DUART1 driver
// attaches there, while POST's dispatcher sits on INT1 (line 0). Both
// lines are enabled (INTENB = 0x0606), so this selects which phase's
// routing applies.
if (getenv("VT1200_SKIP_LEVEL1"))
{
m_duart1->ip3_w(1);
m_duart1_int2 = true;
}
// VT1200_IO_PATH_STUB=1: feed >=10 bytes on DUART2-A after the level-0
// I/O data path test writes 0xE1 to THRA. Off by default: with it off the
// run still fails level 0 honestly and parks in the diagnostics menu.
if (getenv("VT1200_IO_PATH_STUB"))
m_iopath_stub = true;
logerror("vt1200: board model=handshake busy_ticks=2 switch=duart-ip4/5/6 log=%s render=%d dump=%ld\n",
m_log ? "on" : "off", m_render ? 1 : 0, m_dump_frame);
logerror("vt1200: duart1=on (SCN2681 @0640000 host/printer), duart2=on (SCN2681 @0620000 kbd/mouse), skip_level1=%s\n",
getenv("VT1200_SKIP_LEVEL1") ? "on (IP3 high, IRQ->INT2)" : "off (IP3 low, IRQ->INT1)");
logerror("vt1200: io_path_stub=%s\n", m_iopath_stub ? "on (DUART2-A reply to 0xE1)" : "off (faithful)");
logerror("vt1200: DUART2 devices: LK401 keyboard (ch A) + VSXXX-AA mouse (ch B)\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));
}
// 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);
}
// 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));
// The real devices on DUART2: channel A = keyboard (4800 8N1), channel B =
// mouse (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.
VT1200_LK401(config, m_lk401, 0);
VT1200_VSXXXAA(config, m_mouse, 0);
m_lk401->tx_handler().set(m_duart2, FUNC(scn2681_device::rx_a_w));
m_mouse->tx_handler().set(m_duart2, FUNC(scn2681_device::rx_b_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) { 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_vsxxxaa_device -- DEC VSXXX-AA mouse
//**************************************************************************
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)
: device_t(mconfig, VT1200_VSXXXAA, tag, owner, clock)
, device_buffered_serial_interface(mconfig, *this)
, m_tx_handler(*this)
, m_host_x(*this, "MOUSE_X")
, m_host_y(*this, "MOUSE_Y")
, m_host_buttons(*this, "MOUSE_BTN")
{
}
ioport_constructor vt1200_vsxxxaa_device::device_input_ports() const
{
return INPUT_PORTS_NAME(vt1200_vsxxxaa);
}
void vt1200_vsxxxaa_device::device_start()
{
m_scan_timer = timer_alloc(FUNC(vt1200_vsxxxaa_device::scan), this);
m_power_up_timer = timer_alloc(FUNC(vt1200_vsxxxaa_device::power_up_selftest), this);
if (const char *p = getenv("VT1200_INPUT_LOG"))
m_log = fopen(p, "a");
if (m_log)
fprintf(m_log, "# VSXXX-AA device started\n");
}
void vt1200_vsxxxaa_device::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
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_dirty = false;
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_vsxxxaa_device::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_vsxxxaa_device::tra_callback()
{
m_tx_handler(transmit_register_get_data_bit());
}
void vt1200_vsxxxaa_device::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 MS_CMD_SELFTEST:
send_self_test();
break;
case MS_CMD_STREAM:
m_stream = true;
break;
case MS_CMD_PROMPT:
m_stream = false;
break;
case MS_CMD_POSITION:
m_stream = false;
send_report(m_acc_dx, m_acc_dy, 0);
m_acc_dx = m_acc_dy = 0;
break;
case MS_CMD_FORMAT:
// Standard format: already the only one modelled.
break;
default:
break; // invalid commands are ignored
}
}
void vt1200_vsxxxaa_device::send_self_test()
{
// Four-byte power-on/self-test report (PROTOCOL.md section 4.4, 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));
}
TIMER_CALLBACK_MEMBER(vt1200_vsxxxaa_device::power_up_selftest)
{
send_self_test();
}
TIMER_CALLBACK_MEMBER(vt1200_vsxxxaa_device::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);
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;
const int buttons = m_host_buttons->read() & 0x07;
if (dx || dy)
{
m_acc_dx += dx;
m_acc_dy -= dy; // DEC Y is inverted relative to screen Y
}
if (m_stream)
{
if (m_acc_dx || m_acc_dy || buttons != m_last_buttons)
{
send_report(m_acc_dx, m_acc_dy, buttons);
m_acc_dx = m_acc_dy = 0;
}
}
m_last_buttons = buttons;
}
// 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)