mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
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.
2421 lines
99 KiB
C++
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)
|