harness: real LK401 keyboard + VSXXX-AA mouse devices on DUART2

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

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

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

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

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

See harness/mame/INPUT_REPORT.md.
This commit is contained in:
arti committed 2026-10-16 15:33:30 +03:00
1 parent 7e65692b54
commit 02acabb7ef
9 files changed
+1079 -6

No files matched your search

+195
View File
@@ -0,0 +1,195 @@
# Keyboard & mouse as real devices on DUART2 — report
Adds the VT1200's actual input parts to the harness as **device models** on the
already-modelled SCN2681 at DUART2: a **DEC LK401 keyboard** on channel A and a
**DEC VSXXX-AA mouse** on channel B. Both are driven by MAME's host input and
talk to the DUART through its genuine bit-level serial lines.
Status: **works**. The X11 cursor moves with the mouse and keystrokes reach the
OS. Existing configurations are unchanged.
---
## 1. Route chosen: LK401 from the docs (route 2)
The brief offered two routes. **Route 1 (MAME's own `lk201` device + firmware
ROM) is not usable**, and the ROM check is decisive:
* MAME's `lk201` device (`src/mame/dec/lk201.cpp`) runs an **M68HC05EG** and
loads `23-001s9-00.bin` (8 KiB, CRC `be293c51`) from its `lk201_cpu` region.
* The firmware zip downloaded during the protocol research
(`reference/keyboard-mouse/bitsavers-23-004M2-00_LK201AA_Firmware.zip`)
contains only `23-004M2-00.BIN` (4096 bytes, SHA1 `9744c561…`,
CRC `35dd04c6`). That is exactly MAME's **second** ROM — the **8051**
`23-004m2.8051h.e1` — which the `lk201` device does **not execute** (it is the
red-LED LK201 variant; the device models the 6805 green-LED one).
* So MAME's `lk201` would need a ROM we do not have. Running it is impossible
without sourcing `23-001s9-00.bin`.
**Route 2 it is**: an HLE model of the documented LK401/LK201 host protocol from
`reference/keyboard-mouse/PROTOCOL.md`, driving the real DUART wires. This also
matches the **actual fitted part** (LK401) rather than an LK201, and needs no
external ROM. Had route 1 been available it would have had the honest drawback
of speaking LK201 keycodes — for this OS, which reads the keyboard over the
protocol and does not care which family the keycodes came from, that would
probably not have mattered in practice; but it is moot because the ROM is
missing.
The mouse has no MAME device, so the VSXXX-AA is modelled either way (it is
small).
## 2. What was implemented
Both devices live in `harness/mame/vt1200.cpp` (inside the owned driver, so the
MAME tree gains **no** files), derive from
`device_buffered_serial_interface<>`, and are wired bit-by-bit:
```
keyboard tx_handler -> duart2.rx_a_w (device TX -> DUART RX)
mouse tx_handler -> duart2.rx_b_w
duart2.a_tx_cb / b_tx_cb -> device rx_w (DUART TX -> device RX)
```
So the framing (keyboard 4800 8N1, mouse 4800 8**O**1) and the baud timing are
the DUART's own; there is no byte-level shortcut.
**LK401 keyboard** (`vt1200_lk401_device`), protocol from `PROTOCOL.md`:
* Answers `FD` with the documented 4-byte self-test `01 00 00 00` (and runs it
70 ms after power-up, as the real part does). `AB` returns the same ID bytes.
* Parses the command set: `D3`, `E9` (LK401 mode), `13 p`/`11 p` (LEDs),
keyclick/bell/auto-repeat commands, `CB`, `8B`, and the division mode-set
commands (acknowledged with `BA`).
* Reports key transitions as single keycodes (> 0x40) from the documented
table (`netbsd-lk201-keycodes.html` + `elkay/docs/keymap.md`), honouring each
division's programmed mode (down-only / auto-repeat-down / down-up) and
sending `B3` (all-ups) when the last down-up key is released. `B4` metronome
ticks implement auto-repeat. LK401-only keys (RShift/LAlt/RAlt/RCompose) are
gated on the `E9` command.
* Host input: one MAME keyboard port per key (`:lk401:K0…K107`) with `PORT_CODE`
and `PORT_CHAR`, polled at 120 Hz.
**VSXXX-AA mouse** (`vt1200_vsxxxaa_device`), protocol from `PROTOCOL.md` §4:
* Answers `T` (and power-up) with the 4-byte self-test report `A0 02 00 00`
(header 101 + revision 0; manufacturer 0; device code 0b0010 = mouse; error
0; no buttons).
* Implements stream mode (`R`), prompt mode (`D`/`P`), and the `S` format
command. Reports `80 | SX SY L M R`, `|dx|`, `|dy|`.
* Host input: relative `IPT_MOUSE_X` / `IPT_MOUSE_Y` plus three buttons;
deltas are turned into incremental reports (Y inverted for DEC coordinates).
### The one non-obvious fix: idle-high TxD
The SCN2681's `device_serial_interface` receiver has a phase dependency: if the
**first** edge it sees after a receiver reset is the start bit, with no explicit
idle-high beforehand, it synchronises late and mis-samples the byte. Isolated
evidence — bit-banging `0x55` (8N1, 4800) straight into `duart2.rx_a_w` with the
CPU halted:
| preceding bits | SRA | RHRA |
|---|---|---|
| none | `0D` | `D5` |
| 4 idle-high | `0D` | `55` |
The fix is faithful anyway: a UART's TxD **idles at mark (high)**. Both device
models now hold the line high whenever they are not mid-byte, which is what
makes the link work. This is the only place a physical-layer detail had to be
made explicit.
## 3. Verification
### 3.1 Does the cursor move / do keystrokes register? — YES
Run: `VT1200_SKIP_LEVEL1=1` (the only configuration that reaches the OS),
headless, with `harness/mame/input_inject.lua` injecting input.
* **Keystroke registers.** The script presses and releases `A`
(`:lk401:K26`) at frame 4450. The OS's DUART2 poller reads the keycode
`0xC2` (the documented LK401 code for `A`) from channel A at emulated
`t=62.2856 s` (`2 R reg=3 val=00C2 pc=3FB42710`). The protocol delivers
keystrokes to the OS.
* **Mouse reports reach the OS.** The script walks the mouse 16–32 units/frame.
The OS reads ~900 bytes of well-formed VSXXX reports from channel B; the raw
stream is `98 10 00` / `98 20 00` = header `100`, SX=1, SY=1, no buttons,
dx=+16/+32, dy=0.
* **The cursor moves.** The X server's X11 pointer arrow is visible at
`(768,472)` at frame 4400 and has left that location by frame 4600 (frames
4700 and 5000 are byte-identical, i.e. it settled). Evidence images:
`input_cursor_before_f4400.png`, `input_cursor_after_f4600.png` (full frames,
downscaled) and `input_cursor_band_before_f4400.png` /
`input_cursor_band_after_f4600.png` (3× zoom of the memory-bar band the
pointer sat on).
* The mouse self-test now decodes cleanly and the OS **switches the mouse to
incremental stream mode** (`R`, THRB) — before the idle-high fix the
self-test was garbled (`0x27…`) and the OS never sent `R`.
* After `FD`, the OS reads the keyboard self-test as `01 00 00 00` and the
mouse self-test as `A0 02 00 00`.
The pointer's travel under continuous injection is small in absolute terms
because it ends up clamped at the Terminal Manager window border; the point is
that it is the X server's own cursor and it tracks the device.
### 3.2 Level-0 test outcome — unchanged (report only)
**The level-0 I/O data-path outcome did not change.** With no environment the
boot still fails level 0's count-only 10-byte test and parks in the diagnostics
menu; the regression baseline is byte-for-byte the same:
```
# no env, progress.lua at 200 frames
PC=3FDCE400 samples=198 (3 distinct PCs)
```
This is the expected, honest result and **does not contradict
`reference/level0-10byte-puzzle.md`**: the keyboard's power-up self-test is
4 bytes (and is sent before the DUART channel is configured, so it is dropped),
`0x11 0x8C` and `0xE1` provoke no reply, and the test flushes the receiver — so
the real device contributes **0** bytes to the count, never 10. Nothing was
tuned to make that test pass.
### 3.3 Existing configurations still hold
| configuration | expected | observed | changed? |
|---|---|---|---|
| no env | level 0 fails honestly, menu | `PC=3FDCE400 samples=198 (3 PCs)` | no |
| `VT1200_IO_PATH_STUB=1` | reaches level 1 | `PC=3FDCE400 samples=53 (75 PCs)` | no |
| `VT1200_SKIP_LEVEL1=1` | X11 weave + Terminal Manager | frame 3000 = Terminal Manager | no |
ROM `md5 b46a3e442ccb86134d2356bd70d22fd2` — unchanged.
No baseline moved, so no baseline change needs recording. In particular the
keyboard's power-up transmission does **not** perturb the faithful boot: it
happens at 70 ms, before the OS programs DUART2, and the level-0 test's flush
would consume it anyway.
The stub run's level-1 outcome is also unchanged (still the DUART1
counter-ready interrupt storm), so the new DUART2 devices do not alter the
level-1 path.
## 4. Caveats / what is not done yet
* **HLE, not firmware.** The keyboard models what the LK401 firmware *does*; it
does not execute it. If `23-001s9-00.bin` (or an LK401 ROM) is ever sourced,
route 1 could replace this.
* **Keycode source.** Keycodes/divisions come from the NetBSD LK201 table and
elkay's LK401 map; the LK401 firmware's exact ID bytes are unknown, so the
self-test uses the documented healthy `01 00 00 00`.
* **Auto-repeat** is a simple 500 ms / 20 Hz model — enough to be correct in
shape, not measured against hardware.
* **`0xB9` ambiguity.** Per `PROTOCOL.md` it is "disable Ctrl-key click"
(no reply); NetBSD also lists it as "inhibit transmission" (reply `B7`). The
no-reply reading was chosen because the VT1200 OS never sends it.
* The mouse reports are read by the OS's poller; the exact pointer
acceleration/clamping is the terminal software's, not ours.
## 5. Files
* `harness/mame/vt1200.cpp` — the two device models, input ports, and DUART2
wiring (the only source file changed).
* `harness/mame/input_probe.lua` — lists the device ports/fields (the tags Lua
needs).
* `harness/mame/input_inject.lua` — drives the mouse and one key from Lua for
the acceptance test.
* `harness/mame/input_cursor_*.png` — the frame evidence above.
* New opt-in knob `VT1200_INPUT_LOG=<path>` — per-byte device TX/RX trace.
+30 -4
View File
@@ -67,6 +67,12 @@ to be a six-variable incantation; it is now the default, and a bare
*faithfulness* fix, not a convenience — the real board has one. Without it the
board write-back latch answers a read of DUART2's **write-only** IMR with its
own written value, so the OS services a phantom interrupt that can never clear.
* The **real input devices** on DUART2, driven by MAME's host input: a
**DEC LK401 keyboard** (channel A, 4800 8N1) and a **DEC VSXXX-AA mouse**
(channel B, 4800 8O1), both modelled from
`reference/keyboard-mouse/PROTOCOL.md` and wired to the SCN2681 at the
bit level (`rx_a_w`/`rx_b_w` and `a_tx_cb`/`b_tx_cb`). They answer `FD`/`T`
because that is the protocol, not to satisfy a test. See `INPUT_REPORT.md`.
* A mono palette (pens 0/1 = black/white) and an active-pixel scanline renderer
that reads VRAM. See `RENDER_REPORT.md`.
@@ -141,8 +147,10 @@ the same sequence — see `reference/vt1200-boot-video/`.
The OS's keyboard and mouse drivers **initialise and speak the real protocol**: it
programs DUART2 (`MR1A=0x13`, `MR2A=0x07`, `CSRA/CSRB=0x99`, `ACR=0x60`,
`IMR=0x33`) and sends `FD` plus LED/click/bell/auto-repeat to the keyboard and `'T'`
to the mouse. **Input does not work** — nothing is attached — and nothing is faked
to pretend otherwise.
to the mouse. **Input now works**: the LK401 and VSXXX-AA device models answer
those commands, the OS reads `01 00 00 00` / `A0 02 00 00`, switches the mouse to
stream mode, and the X11 cursor moves with the mouse while keycodes reach the OS.
See `INPUT_REPORT.md` (the cursor evidence is the `input_cursor_*.png` frames).
Not modelled: **LANCE Ethernet**. It does not block the X11 bring-up.
@@ -157,7 +165,7 @@ behaviour**, and is **not** evidence about the real terminal's POST.
The skip is deliberately **not** the default: the real machine runs level 1, so
the faithful configuration stays the default and the switch is how we diverge.
## Environment knobs (17; down from 33)
## Environment knobs (18; down from 33)
All opt-in, and all off by default — there is no longer a "working incantation" to
remember, because everything the firmware needs to run is now the default.
@@ -165,7 +173,8 @@ remember, because everything the firmware needs to run is now the default.
Instrumentation: `VT1200_DUARTLOG`, `VT1200_BOARD_LOG`, `VT1200_BOARD_MAP`,
`VT1200_IRQLOG`, `VT1200_LOG_UNMAPPED`, `VT1200_DPYLOG` (+`_FRAMES`),
`VT1200_DUMP_FRAME` (+`_FRAMES`), `VT1200_DUMP_RAM` (+`_FRAMES`),
`VT1200_DUMP_VRAM` (+`_FRAMES`), `VT1200_DUMP_DIR`, `VT1200_NO_RENDER`.
`VT1200_DUMP_VRAM` (+`_FRAMES`), `VT1200_DUMP_DIR`, `VT1200_NO_RENDER`,
`VT1200_INPUT_LOG` (per-byte keyboard/mouse TX/RX trace, with emulated time).
Behaviour switches: `VT1200_SKIP_LEVEL1`, `VT1200_IO_PATH_STUB` (the level-0
test-fixture stand-in described above — not a keyboard model, off by default).
@@ -255,6 +264,23 @@ The Lua API exposes `manager.machine.debugger`, `device.debug` (step, go,
bpset, wpset), CPU registers (`cpu.state['A0'].value`), memory
(`cpu.spaces['program']:read_u32/write_u32`), and frame hooks.
### Drive the keyboard and mouse (`input_inject.lua`)
The LK401/VSXXX-AA models read MAME's host input, so they can be driven from
Lua for a headless acceptance run:
```sh
M=/home/arti/code/k-space/mame0289/vt1200
VT1200_SKIP_LEVEL1=1 \
VT1200_DUMP_FRAMES=4400,4600 VT1200_DUMP_DIR=/tmp \
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none \
-autoboot_script harness/mame/input_inject.lua -autoboot_delay 0
```
`input_inject.lua` walks the mouse from frame 4400 and taps `A` at frame 4450.
`input_probe.lua` prints every device port/field tag. See `INPUT_REPORT.md`.
### Trace from the reset vector (`trace.lua`)
Prints the reset PC (expect `3F200040`), disassembles the power-on code to
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+46
View File
@@ -0,0 +1,46 @@
-- Drive the modelled LK401 keyboard and VSXXX-AA mouse from MAME's host input
-- ports, so the acceptance test "does the cursor move / do keystrokes register"
-- can be observed deterministically.
--
-- VT1200_SKIP_LEVEL1=1 \
-- VT1200_DUMP_FRAMES=4400,4700,5000 VT1200_DUMP_DIR=/tmp/opencode \
-- <mame> vt1200 ... -autoboot_script harness/mame/input_inject.lua
--
-- Between INJ_MOUSE_FROM and INJ_MOUSE_TO the mouse is walked 16 units/frame;
-- the keyboard presses and releases 'A' at INJ_KEY_FRAME.
local machine = manager.machine
local ports = machine.ioport.ports
local mouse_x = ports[":mouse:MOUSE_X"].fields["Mouse X"]
local key_a = ports[":lk401:K26"].fields["A"]
local INJ_MOUSE_FROM = 4400
local INJ_MOUSE_TO = 4700
local INJ_KEY_FRAME = 4450
local INJ_KEY_FRAMES = 20
local frames = 0
local mx = 0
local key_down = false
emu.register_frame_done(function()
frames = frames + 1
if frames >= INJ_MOUSE_FROM and frames <= INJ_MOUSE_TO then
mx = (mx + 32) % 256
mouse_x:set_value(mx)
end
if frames == INJ_KEY_FRAME then
key_a:set_value(1)
key_down = true
print(string.format("inject: frame %d press A", frames))
end
if key_down and frames == INJ_KEY_FRAME + INJ_KEY_FRAMES then
key_a:set_value(0)
key_down = false
print(string.format("inject: frame %d release A", frames))
end
if frames >= INJ_MOUSE_TO + 500 then
machine:exit()
end
end, "vt1200-input-inject")
+20
View File
@@ -0,0 +1,20 @@
-- List every I/O port and field the driver registers, with the tag needed to
-- address it from Lua. Used to drive the LK401/VSXXX-AA device models.
--
-- <mame> vt1200 ... -autoboot_script harness/mame/input_probe.lua
local machine = manager.machine
local frames = 0
emu.register_frame_done(function()
frames = frames + 1
if frames < 3 then return end
print("=== ioport ports ===")
for tag, port in pairs(machine.ioport.ports) do
print(string.format("port %s", tag))
for fname, field in pairs(port.fields) do
local ok, ftype = pcall(function() return field.type end)
print(string.format(" field %-28s type=%s name=%s", tostring(fname), tostring(ftype), tostring(field.name)))
end
end
machine:exit()
end, "vt1200-input-probe")
+788 -2
View File
@@ -134,10 +134,12 @@
#include "emu.h"
#include "cpu/tms34010/tms34010.h"
#include "diserial.h"
#include "emupal.h"
#include "machine/mc68681.h"
#include "screen.h"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -149,6 +151,315 @@
#include "vt1200_board.h"
//**************************************************************************
// DEC LK401 keyboard + VSXXX-AA mouse -- real device models on DUART2
//**************************************************************************
//
// The VT1200 keyboard is an LK401 speaking the LK201 host protocol (4800 8N1;
// see reference/keyboard-mouse/PROTOCOL.md). MAME's own lk201 device executes
// the real 68HC05 keyboard firmware, but that ROM (`23-001s9-00.bin`) is not in
// our set: the downloaded zip only carries the *8051* image (`23-004M2-00.bin`,
// SHA1 9744c561... = MAME's unused `23-004m2.8051h.e1`). So the keyboard is an
// HLE model of the documented protocol, not a firmware emulation.
//
// The mouse has no MAME device at all; the VSXXX-AA is modelled here too
// (4800 8O1). Both connect to the real SCN2681 at DUART2 through the genuine
// bit-level serial interfaces (`rx_a_w`/`rx_b_w` and `a_tx_cb`/`b_tx_cb`), so
// the framing/timing is the DUART's, not a shortcut.
//
// These are device models: they answer `FD`/`T` because that is the protocol,
// not because a test wants an answer. They are NOT tuned to satisfy the
// level-0 I/O data path test (which wants 10 bytes; the keyboard's own power-up
// and FD reply is 4). See reference/level0-10byte-puzzle.md.
// --- LK401 keycode / division table -----------------------------------------
// Every LK401 key has one keycode (netbsd-lk201-keycodes.html and elkay's
// keymap.md). Each key also belongs to one of 14 divisions, which the terminal
// programs to a mode (down / auto-repeat-down / down-up).
#define VT1200_LK401_KEY_LIST(X) \
X( 0, 0xbf, 1, PORT_NAME("` ~") PORT_CODE(KEYCODE_TILDE) PORT_CHAR('`') PORT_CHAR('~') ) \
X( 1, 0xc0, 1, PORT_NAME("1 !") PORT_CODE(KEYCODE_1) PORT_CHAR('1') PORT_CHAR('!') ) \
X( 2, 0xc5, 1, PORT_NAME("2 @") PORT_CODE(KEYCODE_2) PORT_CHAR('2') PORT_CHAR('@') ) \
X( 3, 0xcb, 1, PORT_NAME("3 #") PORT_CODE(KEYCODE_3) PORT_CHAR('3') PORT_CHAR('#') ) \
X( 4, 0xd0, 1, PORT_NAME("4 $") PORT_CODE(KEYCODE_4) PORT_CHAR('4') PORT_CHAR('$') ) \
X( 5, 0xd6, 1, PORT_NAME("5 %") PORT_CODE(KEYCODE_5) PORT_CHAR('5') PORT_CHAR('%') ) \
X( 6, 0xdb, 1, PORT_NAME("6 ^") PORT_CODE(KEYCODE_6) PORT_CHAR('6') PORT_CHAR('^') ) \
X( 7, 0xe0, 1, PORT_NAME("7 &") PORT_CODE(KEYCODE_7) PORT_CHAR('7') PORT_CHAR('&') ) \
X( 8, 0xe5, 1, PORT_NAME("8 *") PORT_CODE(KEYCODE_8) PORT_CHAR('8') PORT_CHAR('*') ) \
X( 9, 0xea, 1, PORT_NAME("9 (") PORT_CODE(KEYCODE_9) PORT_CHAR('9') PORT_CHAR('(') ) \
X(10, 0xef, 1, PORT_NAME("0 )") PORT_CODE(KEYCODE_0) PORT_CHAR('0') PORT_CHAR(')') ) \
X(11, 0xf9, 1, PORT_NAME("- _") PORT_CODE(KEYCODE_MINUS) PORT_CHAR('-') PORT_CHAR('_') ) \
X(12, 0xf5, 1, PORT_NAME("= +") PORT_CODE(KEYCODE_EQUALS) PORT_CHAR('=') PORT_CHAR('+') ) \
X(13, 0xc1, 1, PORT_NAME("Q") PORT_CODE(KEYCODE_Q) PORT_CHAR('q') PORT_CHAR('Q') ) \
X(14, 0xc6, 1, PORT_NAME("W") PORT_CODE(KEYCODE_W) PORT_CHAR('w') PORT_CHAR('W') ) \
X(15, 0xcc, 1, PORT_NAME("E") PORT_CODE(KEYCODE_E) PORT_CHAR('e') PORT_CHAR('E') ) \
X(16, 0xd1, 1, PORT_NAME("R") PORT_CODE(KEYCODE_R) PORT_CHAR('r') PORT_CHAR('R') ) \
X(17, 0xd7, 1, PORT_NAME("T") PORT_CODE(KEYCODE_T) PORT_CHAR('t') PORT_CHAR('T') ) \
X(18, 0xdc, 1, PORT_NAME("Y") PORT_CODE(KEYCODE_Y) PORT_CHAR('y') PORT_CHAR('Y') ) \
X(19, 0xe1, 1, PORT_NAME("U") PORT_CODE(KEYCODE_U) PORT_CHAR('u') PORT_CHAR('U') ) \
X(20, 0xe6, 1, PORT_NAME("I") PORT_CODE(KEYCODE_I) PORT_CHAR('i') PORT_CHAR('I') ) \
X(21, 0xeb, 1, PORT_NAME("O") PORT_CODE(KEYCODE_O) PORT_CHAR('o') PORT_CHAR('O') ) \
X(22, 0xf0, 1, PORT_NAME("P") PORT_CODE(KEYCODE_P) PORT_CHAR('p') PORT_CHAR('P') ) \
X(23, 0xfa, 1, PORT_NAME("[ {") PORT_CODE(KEYCODE_OPENBRACE) PORT_CHAR('[') PORT_CHAR('{') ) \
X(24, 0xf6, 1, PORT_NAME("] }") PORT_CODE(KEYCODE_CLOSEBRACE)PORT_CHAR(']') PORT_CHAR('}') ) \
X(25, 0xf7, 1, PORT_NAME("\\ |") PORT_CODE(KEYCODE_BACKSLASH) PORT_CHAR('\\') PORT_CHAR('|') ) \
X(26, 0xc2, 1, PORT_NAME("A") PORT_CODE(KEYCODE_A) PORT_CHAR('a') PORT_CHAR('A') ) \
X(27, 0xc7, 1, PORT_NAME("S") PORT_CODE(KEYCODE_S) PORT_CHAR('s') PORT_CHAR('S') ) \
X(28, 0xcd, 1, PORT_NAME("D") PORT_CODE(KEYCODE_D) PORT_CHAR('d') PORT_CHAR('D') ) \
X(29, 0xd2, 1, PORT_NAME("F") PORT_CODE(KEYCODE_F) PORT_CHAR('f') PORT_CHAR('F') ) \
X(30, 0xd8, 1, PORT_NAME("G") PORT_CODE(KEYCODE_G) PORT_CHAR('g') PORT_CHAR('G') ) \
X(31, 0xdd, 1, PORT_NAME("H") PORT_CODE(KEYCODE_H) PORT_CHAR('h') PORT_CHAR('H') ) \
X(32, 0xe2, 1, PORT_NAME("J") PORT_CODE(KEYCODE_J) PORT_CHAR('j') PORT_CHAR('J') ) \
X(33, 0xe7, 1, PORT_NAME("K") PORT_CODE(KEYCODE_K) PORT_CHAR('k') PORT_CHAR('K') ) \
X(34, 0xec, 1, PORT_NAME("L") PORT_CODE(KEYCODE_L) PORT_CHAR('l') PORT_CHAR('L') ) \
X(35, 0xf2, 1, PORT_NAME("; :") PORT_CODE(KEYCODE_COLON) PORT_CHAR(';') PORT_CHAR(':') ) \
X(36, 0xfb, 1, PORT_NAME("' \"") PORT_CODE(KEYCODE_QUOTE) PORT_CHAR('\'') PORT_CHAR('"') ) \
X(37, 0xc9, 1, PORT_NAME("< >") PORT_CODE(KEYCODE_BACKSLASH2)PORT_CHAR('<') PORT_CHAR('>') ) \
X(38, 0xc3, 1, PORT_NAME("Z") PORT_CODE(KEYCODE_Z) PORT_CHAR('z') PORT_CHAR('Z') ) \
X(39, 0xc8, 1, PORT_NAME("X") PORT_CODE(KEYCODE_X) PORT_CHAR('x') PORT_CHAR('X') ) \
X(40, 0xce, 1, PORT_NAME("C") PORT_CODE(KEYCODE_C) PORT_CHAR('c') PORT_CHAR('C') ) \
X(41, 0xd3, 1, PORT_NAME("V") PORT_CODE(KEYCODE_V) PORT_CHAR('v') PORT_CHAR('V') ) \
X(42, 0xd9, 1, PORT_NAME("B") PORT_CODE(KEYCODE_B) PORT_CHAR('b') PORT_CHAR('B') ) \
X(43, 0xde, 1, PORT_NAME("N") PORT_CODE(KEYCODE_N) PORT_CHAR('n') PORT_CHAR('N') ) \
X(44, 0xe3, 1, PORT_NAME("M") PORT_CODE(KEYCODE_M) PORT_CHAR('m') PORT_CHAR('M') ) \
X(45, 0xe8, 1, PORT_NAME(", <") PORT_CODE(KEYCODE_COMMA) PORT_CHAR(',') PORT_CHAR('<') ) \
X(46, 0xed, 1, PORT_NAME(". >") PORT_CODE(KEYCODE_STOP) PORT_CHAR('.') PORT_CHAR('>') ) \
X(47, 0xf3, 1, PORT_NAME("/ ?") PORT_CODE(KEYCODE_SLASH) PORT_CHAR('/') PORT_CHAR('?') ) \
X(48, 0xd4, 1, PORT_NAME("Space") PORT_CODE(KEYCODE_SPACE) PORT_CHAR(' ') ) \
X(49, 0x92, 2, PORT_NAME("KP 0") PORT_CODE(KEYCODE_0_PAD) ) \
X(50, 0x96, 2, PORT_NAME("KP 1") PORT_CODE(KEYCODE_1_PAD) ) \
X(51, 0x97, 2, PORT_NAME("KP 2") PORT_CODE(KEYCODE_2_PAD) ) \
X(52, 0x98, 2, PORT_NAME("KP 3") PORT_CODE(KEYCODE_3_PAD) ) \
X(53, 0x99, 2, PORT_NAME("KP 4") PORT_CODE(KEYCODE_4_PAD) ) \
X(54, 0x9a, 2, PORT_NAME("KP 5") PORT_CODE(KEYCODE_5_PAD) ) \
X(55, 0x9b, 2, PORT_NAME("KP 6") PORT_CODE(KEYCODE_6_PAD) ) \
X(56, 0x9d, 2, PORT_NAME("KP 7") PORT_CODE(KEYCODE_7_PAD) ) \
X(57, 0x9e, 2, PORT_NAME("KP 8") PORT_CODE(KEYCODE_8_PAD) ) \
X(58, 0x9f, 2, PORT_NAME("KP 9") PORT_CODE(KEYCODE_9_PAD) ) \
X(59, 0x94, 2, PORT_NAME("KP .") PORT_CODE(KEYCODE_DEL_PAD) ) \
X(60, 0x9c, 2, PORT_NAME("KP ,") PORT_CODE(KEYCODE_COMMA_PAD) ) \
X(61, 0xa0, 2, PORT_NAME("KP -") PORT_CODE(KEYCODE_MINUS_PAD) ) \
X(62, 0x95, 2, PORT_NAME("KP Enter") PORT_CODE(KEYCODE_ENTER_PAD) ) \
X(63, 0xa1, 2, PORT_NAME("PF1") PORT_CODE(KEYCODE_NUMLOCK) ) \
X(64, 0xa2, 2, PORT_NAME("PF2") PORT_CODE(KEYCODE_SLASH_PAD) ) \
X(65, 0xa3, 2, PORT_NAME("PF3") PORT_CODE(KEYCODE_ASTERISK) ) \
X(66, 0xa4, 2, PORT_NAME("PF4") PORT_CODE(KEYCODE_EQUALS_PAD) ) \
X(67, 0xbc, 3, PORT_NAME("Delete") PORT_CODE(KEYCODE_BACKSPACE) PORT_CHAR(8) ) \
X(68, 0xbd, 4, PORT_NAME("Return") PORT_CODE(KEYCODE_ENTER) PORT_CHAR(13) ) \
X(69, 0xbe, 4, PORT_NAME("Tab") PORT_CODE(KEYCODE_TAB) PORT_CHAR(9) ) \
X(70, 0xb0, 5, PORT_NAME("Lock") PORT_CODE(KEYCODE_CAPSLOCK) ) \
X(71, 0xb1, 5, PORT_NAME("Compose") PORT_CODE(KEYCODE_MENU) ) \
X(72, 0xae, 6, PORT_NAME("Shift") PORT_CODE(KEYCODE_LSHIFT) ) \
X(73, 0xaf, 6, PORT_NAME("Ctrl") PORT_CODE(KEYCODE_LCONTROL) ) \
X(74, 0xa7, 7, PORT_NAME("Left") PORT_CODE(KEYCODE_LEFT) PORT_CHAR(UCHAR_MAMEKEY(LEFT)) ) \
X(75, 0xa8, 7, PORT_NAME("Right") PORT_CODE(KEYCODE_RIGHT) PORT_CHAR(UCHAR_MAMEKEY(RIGHT)) ) \
X(76, 0xa9, 8, PORT_NAME("Down") PORT_CODE(KEYCODE_DOWN) PORT_CHAR(UCHAR_MAMEKEY(DOWN)) ) \
X(77, 0xaa, 8, PORT_NAME("Up") PORT_CODE(KEYCODE_UP) PORT_CHAR(UCHAR_MAMEKEY(UP)) ) \
X(78, 0x8a, 9, PORT_NAME("Find") PORT_CODE(KEYCODE_INSERT) PORT_CHAR(UCHAR_MAMEKEY(INSERT)) ) \
X(79, 0x8b, 9, PORT_NAME("Insert Here") PORT_CODE(KEYCODE_HOME) PORT_CHAR(UCHAR_MAMEKEY(HOME)) ) \
X(80, 0x8c, 9, PORT_NAME("Remove") PORT_CODE(KEYCODE_PGUP) PORT_CHAR(UCHAR_MAMEKEY(PGUP)) ) \
X(81, 0x8d, 9, PORT_NAME("Select") PORT_CODE(KEYCODE_DEL) PORT_CHAR(UCHAR_MAMEKEY(DEL)) ) \
X(82, 0x8e, 9, PORT_NAME("Prev Screen") PORT_CODE(KEYCODE_END) PORT_CHAR(UCHAR_MAMEKEY(END)) ) \
X(83, 0x8f, 9, PORT_NAME("Next Screen") PORT_CODE(KEYCODE_PGDN) PORT_CHAR(UCHAR_MAMEKEY(PGDN)) ) \
X(84, 0x56, 10, PORT_NAME("F1 Hold") PORT_CODE(KEYCODE_F1) PORT_CHAR(UCHAR_MAMEKEY(F1)) ) \
X(85, 0x57, 10, PORT_NAME("F2 Print") PORT_CODE(KEYCODE_F2) PORT_CHAR(UCHAR_MAMEKEY(F2)) ) \
X(86, 0x58, 10, PORT_NAME("F3 Setup") PORT_CODE(KEYCODE_F3) PORT_CHAR(UCHAR_MAMEKEY(F3)) ) \
X(87, 0x59, 10, PORT_NAME("F4 Data") PORT_CODE(KEYCODE_F4) PORT_CHAR(UCHAR_MAMEKEY(F4)) ) \
X(88, 0x5a, 10, PORT_NAME("F5 Break") PORT_CODE(KEYCODE_F5) PORT_CHAR(UCHAR_MAMEKEY(F5)) ) \
X(89, 0x64, 11, PORT_NAME("F6") PORT_CODE(KEYCODE_F6) PORT_CHAR(UCHAR_MAMEKEY(F6)) ) \
X(90, 0x65, 11, PORT_NAME("F7") PORT_CODE(KEYCODE_F7) PORT_CHAR(UCHAR_MAMEKEY(F7)) ) \
X(91, 0x66, 11, PORT_NAME("F8") PORT_CODE(KEYCODE_F8) PORT_CHAR(UCHAR_MAMEKEY(F8)) ) \
X(92, 0x67, 11, PORT_NAME("F9") PORT_CODE(KEYCODE_F9) PORT_CHAR(UCHAR_MAMEKEY(F9)) ) \
X(93, 0x68, 11, PORT_NAME("F10") PORT_CODE(KEYCODE_F10) PORT_CHAR(UCHAR_MAMEKEY(F10)) ) \
X(94, 0x71, 12, PORT_NAME("F11 Esc") PORT_CODE(KEYCODE_F11) PORT_CHAR(UCHAR_MAMEKEY(F11)) ) \
X(95, 0x72, 12, PORT_NAME("F12 BS") PORT_CODE(KEYCODE_F12) PORT_CHAR(UCHAR_MAMEKEY(F12)) ) \
X(96, 0x73, 12, PORT_NAME("F13 LF") PORT_CODE(KEYCODE_F13) PORT_CHAR(UCHAR_MAMEKEY(F13)) ) \
X(97, 0x74, 12, PORT_NAME("F14") PORT_CODE(KEYCODE_F14) PORT_CHAR(UCHAR_MAMEKEY(F14)) ) \
X(98, 0x7c, 13, PORT_NAME("Help") PORT_CODE(KEYCODE_F15) PORT_CHAR(UCHAR_MAMEKEY(F15)) ) \
X(99, 0x7d, 13, PORT_NAME("Do") PORT_CODE(KEYCODE_F16) PORT_CHAR(UCHAR_MAMEKEY(F16)) ) \
X(100,0x80, 14, PORT_NAME("F17") PORT_CODE(KEYCODE_F17) PORT_CHAR(UCHAR_MAMEKEY(F17)) ) \
X(101,0x81, 14, PORT_NAME("F18") PORT_CODE(KEYCODE_F18) PORT_CHAR(UCHAR_MAMEKEY(F18)) ) \
X(102,0x82, 14, PORT_NAME("F19") PORT_CODE(KEYCODE_F19) PORT_CHAR(UCHAR_MAMEKEY(F19)) ) \
X(103,0x83, 14, PORT_NAME("F20") PORT_CODE(KEYCODE_F20) PORT_CHAR(UCHAR_MAMEKEY(F20)) ) \
X(104,0xab, 8, PORT_NAME("RShift") PORT_CODE(KEYCODE_RSHIFT) ) \
X(105,0xac, 5, PORT_NAME("LAlt") PORT_CODE(KEYCODE_LALT) ) \
X(106,0xb2, 5, PORT_NAME("RAlt") PORT_CODE(KEYCODE_RALT) ) \
X(107,0xad, 5, PORT_NAME("RCompose") PORT_CODE(KEYCODE_RWIN) )
#define VT1200_LK401_COUNT(n, code, div, ...) + 1
#define VT1200_LK401_PORT(n, code, div, ...) \
PORT_START("K" #n) PORT_BIT(0x01, IP_ACTIVE_HIGH, IPT_KEYBOARD) __VA_ARGS__
#define VT1200_LK401_ENTRY(n, code, div, ...) { code, div },
enum { VT1200_LK401_NKEYS = 0 VT1200_LK401_KEY_LIST(VT1200_LK401_COUNT) };
// Keyboard command / response opcodes (PROTOCOL.md section 3.3/3.4).
enum : uint8_t
{
LK_CMD_POWER_UP = 0xfd,
LK_CMD_REQUEST_ID = 0xab,
LK_CMD_SET_DEFAULTS = 0xd3,
LK_CMD_ENABLE_LK401 = 0xe9,
LK_CMD_LED_ON = 0x13,
LK_CMD_LED_OFF = 0x11,
LK_CMD_KEYCLICK_ON = 0x1b,
LK_CMD_KEYCLICK_OFF = 0x99,
LK_CMD_CTRLCLICK_ON = 0xbb,
LK_CMD_CTRLCLICK_OFF = 0xb9,
LK_CMD_BELL_ON = 0x23,
LK_CMD_BELL_OFF = 0xa1,
LK_CMD_BELL = 0xa7,
LK_CMD_SOUND_CLICK = 0x9f,
LK_CMD_MMS_NO_RPT = 0xd1,
LK_CMD_ENB_RPT = 0xe3,
LK_CMD_DIS_RPT = 0xe1,
LK_CMD_RPT_TO_DOWN = 0xd9,
LK_CMD_RESUME = 0x8b,
LK_CMD_TEST_MODE = 0xcb,
LK_ALL_KEYS_UP = 0xb3,
LK_METRONOME = 0xb4,
LK_OUTPUT_ERROR = 0xb5,
LK_INPUT_ERROR = 0xb6,
LK_KBD_LOCKED = 0xb7,
LK_TEST_MODE_ACK = 0xb8,
LK_PREFIX_KEY_DOWN = 0xb9,
LK_MODE_CHANGE_ACK = 0xba
};
// Power-up / `FD` self-test reply. PROTOCOL.md section 3.5: firmware ID 01,
// hardware ID 00, error 00, no stuck key.
static const uint8_t LK_SELFTEST[4] = { 0x01, 0x00, 0x00, 0x00 };
class vt1200_lk401_device : public device_t, public device_buffered_serial_interface<64U>
{
public:
vt1200_lk401_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0);
auto tx_handler() { return m_tx_handler.bind(); }
protected:
virtual void device_start() override ATTR_COLD;
virtual void device_reset() override ATTR_COLD;
virtual ioport_constructor device_input_ports() const override ATTR_COLD;
virtual void tra_callback() override;
virtual void received_byte(u8 byte) override;
TIMER_CALLBACK_MEMBER(scan);
TIMER_CALLBACK_MEMBER(power_up_selftest);
private:
void process_command();
void run_self_test();
void queue(u8 byte);
void emit_key(unsigned index, bool pressed);
static bool lk401_only(uint8_t code);
devcb_write_line m_tx_handler;
required_ioport_array<VT1200_LK401_NKEYS> m_kbd;
emu_timer *m_scan_timer = nullptr;
emu_timer *m_power_up_timer = nullptr;
std::vector<u8> m_cmd; // command bytes accumulated so far
u8 m_div_mode[15] = { 0 }; // [1..14]: 0=down 1=autodown 3=updown
bool m_autorepeat = true;
bool m_lk401_mode = false;
bool m_inhibit = false;
u8 m_led_mask = 0;
bool m_lost_keys = false;
std::array<bool, VT1200_LK401_NKEYS> m_down = { };
std::array<attotime, VT1200_LK401_NKEYS> m_repeat = { };
int m_updown_down = 0;
FILE *m_log = nullptr; // VT1200_INPUT_LOG=<path>
};
static const struct { uint8_t code; uint8_t div; } VT1200_LK401_TABLE[] =
{
VT1200_LK401_KEY_LIST(VT1200_LK401_ENTRY)
};
INPUT_PORTS_START(vt1200_lk401)
VT1200_LK401_KEY_LIST(VT1200_LK401_PORT)
INPUT_PORTS_END
// Mouse host inputs. X/Y are relative (MAME accumulates movement into the port
// value, which the device reads as a signed 8-bit delta). The three button
// bits are laid out in the VSXXX report order: bit 2 = left, bit 1 = middle,
// bit 0 = right.
INPUT_PORTS_START(vt1200_vsxxxaa)
PORT_START("MOUSE_X")
PORT_BIT(0xff, 0x00, IPT_MOUSE_X) PORT_SENSITIVITY(50) PORT_KEYDELTA(0) PORT_PLAYER(1)
PORT_START("MOUSE_Y")
PORT_BIT(0xff, 0x00, IPT_MOUSE_Y) PORT_SENSITIVITY(50) PORT_KEYDELTA(0) PORT_PLAYER(1)
PORT_START("MOUSE_BTN")
PORT_BIT(0x04, IP_ACTIVE_HIGH, IPT_BUTTON1) PORT_CODE(MOUSECODE_BUTTON1) PORT_PLAYER(1) // left
PORT_BIT(0x02, IP_ACTIVE_HIGH, IPT_BUTTON2) PORT_CODE(MOUSECODE_BUTTON2) PORT_PLAYER(1) // middle
PORT_BIT(0x01, IP_ACTIVE_HIGH, IPT_BUTTON3) PORT_CODE(MOUSECODE_BUTTON3) PORT_PLAYER(1) // right
INPUT_PORTS_END
// --- VSXXX-AA mouse ---------------------------------------------------------
// 4800 8O1 (VCB02 section C.6.1). Commands are printable ASCII; the self-test
// (`T`, and at power-up) returns a 4-byte report. Movement is reported as
// 3-byte incremental packets in stream mode (`R`), or on request (`P`).
enum : uint8_t
{
MS_CMD_STREAM = 'R',
MS_CMD_PROMPT = 'D',
MS_CMD_POSITION = 'P',
MS_CMD_SELFTEST = 'T',
MS_CMD_FORMAT = 'S'
};
class vt1200_vsxxxaa_device : public device_t, public device_buffered_serial_interface<16U>
{
public:
vt1200_vsxxxaa_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock = 0);
auto tx_handler() { return m_tx_handler.bind(); }
protected:
virtual void device_start() override ATTR_COLD;
virtual void device_reset() override ATTR_COLD;
virtual ioport_constructor device_input_ports() const override ATTR_COLD;
virtual void tra_callback() override;
virtual void received_byte(u8 byte) override;
TIMER_CALLBACK_MEMBER(scan);
TIMER_CALLBACK_MEMBER(power_up_selftest);
private:
void send_self_test();
void send_report(int dx, int dy, int buttons);
void queue(u8 byte);
devcb_write_line m_tx_handler;
required_ioport m_host_x;
required_ioport m_host_y;
required_ioport m_host_buttons;
emu_timer *m_scan_timer = nullptr;
emu_timer *m_power_up_timer = nullptr;
bool m_stream = false; // false = prompt mode (power-up default)
u8 m_last_x = 0;
u8 m_last_y = 0;
int m_acc_dx = 0;
int m_acc_dy = 0;
int m_last_buttons = 0;
bool m_buttons_dirty = false;
FILE *m_log = nullptr;
};
DECLARE_DEVICE_TYPE(VT1200_LK401, vt1200_lk401_device)
DECLARE_DEVICE_TYPE(VT1200_VSXXXAA, vt1200_vsxxxaa_device)
namespace {
// The undecoded windows, in bit-address units. 0x5900000 is paired with
@@ -183,6 +494,8 @@ public:
, m_mainram(*this, "mainram")
, m_duart1(*this, "duart1")
, m_duart2(*this, "duart2")
, m_lk401(*this, "lk401")
, m_mouse(*this, "mouse")
, m_vram(*this, "vram")
{
}
@@ -247,6 +560,13 @@ private:
required_device<scn2681_device> m_duart2; // 0x0620000: keyboard(A)/mouse(B)
bool m_duart1_int2 = false; // set by VT1200_SKIP_LEVEL1
// The real devices on DUART2. Keyboard on channel A, mouse on channel B,
// as the board has them. Wired bit-by-bit to the SCN2681's serial lines.
required_device<vt1200_lk401_device> m_lk401;
required_device<vt1200_vsxxxaa_device> m_mouse;
void duart2_a_tx_w(int state);
void duart2_b_tx_w(int state);
// VT1200_IO_PATH_STUB=1: opt-in stand-in for the unknown test fixture on
// the keyboard port, so level-0's count-only I/O data path test can pass.
// NOT a keyboard model. See iopath_stub_arm() and the README.
@@ -464,6 +784,7 @@ void vt1200_state::machine_start()
logerror("vt1200: duart1=on (SCN2681 @0640000 host/printer), duart2=on (SCN2681 @0620000 kbd/mouse), skip_level1=%s\n",
getenv("VT1200_SKIP_LEVEL1") ? "on (IP3 high, IRQ->INT2)" : "off (IP3 low, IRQ->INT1)");
logerror("vt1200: io_path_stub=%s\n", m_iopath_stub ? "on (DUART2-A reply to 0xE1)" : "off (faithful)");
logerror("vt1200: DUART2 devices: LK401 keyboard (ch A) + VSXXX-AA mouse (ch B)\n");
}
void vt1200_state::machine_reset()
@@ -788,9 +1109,10 @@ void vt1200_state::log_duart(int win, char kind, u32 reg, u16 value)
{
if (!m_duartlog)
return;
fprintf(m_duartlog, "%d %c reg=%X val=%04X pc=%08X cyc=%llu\n",
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());
(unsigned long long)m_maincpu->total_cycles(),
machine().time().as_double());
fflush(m_duartlog);
}
@@ -1069,8 +1391,29 @@ void vt1200_state::vt1200(machine_config &config)
// (OS) from VT1200_SKIP_LEVEL1; DUART2's trampoline is on INT2.
m_duart1->irq_cb().set(FUNC(vt1200_state::duart1_irq_w));
m_duart2->irq_cb().set(FUNC(vt1200_state::duart2_irq_w));
// The real devices on DUART2: channel A = keyboard (4800 8N1), channel B =
// mouse (4800 8O1), exactly as the board wires them. Both talk to the
// SCN2681 through the genuine device_serial_interface bit lines: the
// device's transmit output drives the DUART's receiver input
// (rx_a_w / rx_b_w), and the DUART's transmitter output
// (a_tx_cb / b_tx_cb) drives the device's receiver input (rx_w). No
// byte-level shortcut, so the framing and the 4800-baud timing are the
// DUART's own.
VT1200_LK401(config, m_lk401, 0);
VT1200_VSXXXAA(config, m_mouse, 0);
m_lk401->tx_handler().set(m_duart2, FUNC(scn2681_device::rx_a_w));
m_mouse->tx_handler().set(m_duart2, FUNC(scn2681_device::rx_b_w));
m_duart2->a_tx_cb().set(FUNC(vt1200_state::duart2_a_tx_w));
m_duart2->b_tx_cb().set(FUNC(vt1200_state::duart2_b_tx_w));
}
// These two just fan the DUART's serial transmitter output into the attached
// device's receiver. (A state-level forward keeps the devcb binding simple;
// rx_w() is public on device_serial_interface.)
void vt1200_state::duart2_a_tx_w(int state) { m_lk401->rx_w(state); }
void vt1200_state::duart2_b_tx_w(int state) { m_mouse->rx_w(state); }
INPUT_PORTS_START(vt1200)
INPUT_PORTS_END
@@ -1083,6 +1426,449 @@ ROM_END
} // anonymous namespace
//**************************************************************************
// vt1200_lk401_device -- HLE model of the DEC LK401 keyboard
//**************************************************************************
//
// The protocol comes from reference/keyboard-mouse/PROTOCOL.md. This is not
// the keyboard's firmware; it is a model of what the firmware does on the wire.
// It answers `FD` with the documented 4-byte self-test because that is what the
// part does, and it reports key transitions with the documented keycodes.
DEFINE_DEVICE_TYPE(VT1200_LK401, vt1200_lk401_device, "vt1200lk401", "DEC LK401 Keyboard")
vt1200_lk401_device::vt1200_lk401_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: device_t(mconfig, VT1200_LK401, tag, owner, clock)
, device_buffered_serial_interface(mconfig, *this)
, m_tx_handler(*this)
, m_kbd(*this, "K%u", 0U)
{
}
ioport_constructor vt1200_lk401_device::device_input_ports() const
{
return INPUT_PORTS_NAME(vt1200_lk401);
}
void vt1200_lk401_device::device_start()
{
m_scan_timer = timer_alloc(FUNC(vt1200_lk401_device::scan), this);
m_power_up_timer = timer_alloc(FUNC(vt1200_lk401_device::power_up_selftest), this);
if (const char *p = getenv("VT1200_INPUT_LOG"))
m_log = fopen(p, "a");
if (m_log)
fprintf(m_log, "# LK401 device started\n");
}
void vt1200_lk401_device::device_reset()
{
// 4800 baud, 8 data bits, no parity, 1 stop bit (PROTOCOL.md section 3.1).
set_data_frame(1, 8, PARITY_NONE, STOP_BITS_1);
set_rate(4800);
clear_fifo();
receive_register_reset();
m_cmd.clear();
m_down.fill(false);
m_repeat.fill(attotime::never);
m_updown_down = 0;
m_autorepeat = true;
m_lk401_mode = false;
m_inhibit = false;
m_led_mask = 0;
m_lost_keys = false;
for (int i = 1; i <= 14; i++)
m_div_mode[i] = 0; // down-only at power-up
// The real keyboard runs its self-test and transmits the 4-byte result a
// few tens of ms after power-up, before the host has configured the port.
// Queued faithfully; if the DUART channel is not yet clocked the bytes are
// discarded, which is exactly what happens on real hardware.
if (m_power_up_timer)
m_power_up_timer->adjust(attotime::from_msec(70));
// Poll the host keyboard at 120 Hz. Each scan reads the ioport state and
// turns key transitions into keycodes.
if (m_scan_timer)
m_scan_timer->adjust(attotime::from_hz(120), 0, attotime::from_hz(120));
}
bool vt1200_lk401_device::lk401_only(uint8_t code)
{
// Codes only reported after the `E9` LK401-mode command.
return code == 0xab || code == 0xac || code == 0xad || code == 0xb2;
}
void vt1200_lk401_device::queue(u8 byte)
{
if (m_log)
fprintf(m_log, "LK401 tx %02X t=%.6f\n", byte, machine().time().as_double());
transmit_byte(byte);
}
void vt1200_lk401_device::tra_callback()
{
m_tx_handler(transmit_register_get_data_bit());
}
void vt1200_lk401_device::received_byte(u8 byte)
{
if (m_log)
fprintf(m_log, "LK401 rx %02X t=%.6f\n", byte, machine().time().as_double());
// A command runs until a byte with bit 7 set arrives (PARAM bit: 1 = no
// parameter follows). See PROTOCOL.md section 3.2 and the netbsd LK201
// interface description.
m_cmd.push_back(byte);
if (byte & 0x80)
{
process_command();
m_cmd.clear();
}
}
void vt1200_lk401_device::process_command()
{
if (m_cmd.empty())
return;
const u8 op = m_cmd[0];
switch (op)
{
case LK_CMD_POWER_UP:
run_self_test();
return;
case LK_CMD_REQUEST_ID:
// Returns the same 2 bytes as the power-up self test (PROTOCOL.md 3.3).
queue(LK_SELFTEST[0]);
queue(LK_SELFTEST[1]);
return;
case LK_CMD_SET_DEFAULTS:
m_autorepeat = true;
m_lk401_mode = false;
for (int i = 1; i <= 14; i++)
m_div_mode[i] = 0;
return;
case LK_CMD_ENABLE_LK401:
m_lk401_mode = true;
return;
case LK_CMD_LED_ON:
if (m_cmd.size() >= 2)
m_led_mask |= (m_cmd[1] & 0x0f);
return;
case LK_CMD_LED_OFF:
if (m_cmd.size() >= 2)
m_led_mask &= ~(m_cmd[1] & 0x0f);
return;
case LK_CMD_KEYCLICK_ON:
case LK_CMD_KEYCLICK_OFF:
case LK_CMD_CTRLCLICK_ON:
case LK_CMD_CTRLCLICK_OFF:
case LK_CMD_BELL_ON:
case LK_CMD_BELL_OFF:
case LK_CMD_BELL:
case LK_CMD_SOUND_CLICK:
case LK_CMD_RPT_TO_DOWN:
return;
case LK_CMD_ENB_RPT:
m_autorepeat = true;
return;
case LK_CMD_DIS_RPT:
m_autorepeat = false;
return;
case LK_CMD_MMS_NO_RPT:
// Temporary auto-repeat inhibit for the key currently down.
return;
case LK_CMD_RESUME:
m_inhibit = false;
if (m_lost_keys)
{
m_lost_keys = false;
queue(LK_OUTPUT_ERROR);
}
return;
case LK_CMD_TEST_MODE:
queue(LK_TEST_MODE_ACK);
return;
default:
break;
}
// Mode commands: bit 7 set, bit 0 clear, division in bits 3-6, mode in
// bits 1-2. Also reached with bit 7 clear (an autorepeat-register
// association byte follows) -- same division/mode fields. The keyboard
// acknowledges a successful change with BA.
if (!(op & 0x01) && (op & 0x78))
{
const int div = (op >> 3) & 0x0f;
const int mode = (op >> 1) & 0x03;
if (div >= 1 && div <= 14)
m_div_mode[div] = u8(mode);
queue(LK_MODE_CHANGE_ACK);
return;
}
queue(LK_INPUT_ERROR);
}
void vt1200_lk401_device::run_self_test()
{
// `FD` resets the keyboard and re-runs the self-test. Healthy result is
// the documented four bytes; a stuck key would change byte 3/4 but we have
// no stuck keys here.
for (unsigned i = 0; i < sizeof(LK_SELFTEST); i++)
queue(LK_SELFTEST[i]);
}
TIMER_CALLBACK_MEMBER(vt1200_lk401_device::power_up_selftest)
{
run_self_test();
}
void vt1200_lk401_device::emit_key(unsigned index, bool pressed)
{
const uint8_t code = VT1200_LK401_TABLE[index].code;
const uint8_t div = VT1200_LK401_TABLE[index].div;
if (lk401_only(code) && !m_lk401_mode)
return; // LK401-only key, host has not enabled LK401 mode
if (m_inhibit)
{
m_lost_keys = true;
return;
}
const int mode = m_div_mode[div];
if (pressed)
{
queue(code);
if (mode == 3)
m_updown_down++;
else if (mode == 1 && m_autorepeat)
m_repeat[index] = machine().time() + attotime::from_msec(500);
}
else
{
if (mode == 3)
{
if (m_updown_down > 0)
m_updown_down--;
if (m_updown_down == 0)
queue(LK_ALL_KEYS_UP);
else
queue(code);
}
m_repeat[index] = attotime::never;
}
}
TIMER_CALLBACK_MEMBER(vt1200_lk401_device::scan)
{
// A UART's TxD idles at mark (high). Driving the idle level matters for
// the SCN2681's receiver: after its receiver is reset it needs to see an
// explicit idle-high before the next start bit, or it synchronises late and
// mis-samples the byte. Keeping the line high whenever we are not mid-byte
// is both faithful and what makes the link work.
if (fifo_empty() && is_transmit_register_empty())
m_tx_handler(1);
const attotime now = machine().time();
for (unsigned i = 0; i < VT1200_LK401_NKEYS; i++)
{
const bool down = (m_kbd[i]->read() & 1) != 0;
if (down != m_down[i])
{
m_down[i] = down;
emit_key(i, down);
}
if (down && m_repeat[i] != attotime::never && now >= m_repeat[i] && m_autorepeat && !m_inhibit)
{
// Auto-repeat tick (B4) for an auto-repeat-down key.
queue(LK_METRONOME);
m_repeat[i] += attotime::from_hz(20);
}
}
}
//**************************************************************************
// vt1200_vsxxxaa_device -- DEC VSXXX-AA mouse
//**************************************************************************
DEFINE_DEVICE_TYPE(VT1200_VSXXXAA, vt1200_vsxxxaa_device, "vt1200vsxxxaa", "DEC VSXXX-AA Mouse")
vt1200_vsxxxaa_device::vt1200_vsxxxaa_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: device_t(mconfig, VT1200_VSXXXAA, tag, owner, clock)
, device_buffered_serial_interface(mconfig, *this)
, m_tx_handler(*this)
, m_host_x(*this, "MOUSE_X")
, m_host_y(*this, "MOUSE_Y")
, m_host_buttons(*this, "MOUSE_BTN")
{
}
ioport_constructor vt1200_vsxxxaa_device::device_input_ports() const
{
return INPUT_PORTS_NAME(vt1200_vsxxxaa);
}
void vt1200_vsxxxaa_device::device_start()
{
m_scan_timer = timer_alloc(FUNC(vt1200_vsxxxaa_device::scan), this);
m_power_up_timer = timer_alloc(FUNC(vt1200_vsxxxaa_device::power_up_selftest), this);
if (const char *p = getenv("VT1200_INPUT_LOG"))
m_log = fopen(p, "a");
if (m_log)
fprintf(m_log, "# VSXXX-AA device started\n");
}
void vt1200_vsxxxaa_device::device_reset()
{
// 4800 baud, 8 data bits, ODD parity, 1 stop bit (PROTOCOL.md section 4.1).
set_data_frame(1, 8, PARITY_ODD, STOP_BITS_1);
set_rate(4800);
clear_fifo();
receive_register_reset();
m_stream = false; // prompt mode is the power-up default
m_last_x = u8(m_host_x->read());
m_last_y = u8(m_host_y->read());
m_acc_dx = 0;
m_acc_dy = 0;
m_last_buttons = 0;
m_buttons_dirty = false;
if (m_power_up_timer)
m_power_up_timer->adjust(attotime::from_msec(250));
if (m_scan_timer)
m_scan_timer->adjust(attotime::from_hz(120), 0, attotime::from_hz(120));
}
void vt1200_vsxxxaa_device::queue(u8 byte)
{
if (m_log)
fprintf(m_log, "VSXXX tx %02X t=%.6f\n", byte, machine().time().as_double());
transmit_byte(byte);
}
void vt1200_vsxxxaa_device::tra_callback()
{
m_tx_handler(transmit_register_get_data_bit());
}
void vt1200_vsxxxaa_device::received_byte(u8 byte)
{
if (m_log)
fprintf(m_log, "VSXXX rx %02X (%c) t=%.6f\n", byte, (byte >= 0x20 && byte < 0x7f) ? byte : '.', machine().time().as_double());
switch (byte)
{
case MS_CMD_SELFTEST:
send_self_test();
break;
case MS_CMD_STREAM:
m_stream = true;
break;
case MS_CMD_PROMPT:
m_stream = false;
break;
case MS_CMD_POSITION:
m_stream = false;
send_report(m_acc_dx, m_acc_dy, 0);
m_acc_dx = m_acc_dy = 0;
break;
case MS_CMD_FORMAT:
// Standard format: already the only one modelled.
break;
default:
break; // invalid commands are ignored
}
}
void vt1200_vsxxxaa_device::send_self_test()
{
// Four-byte power-on/self-test report (PROTOCOL.md section 4.4, figure C-5):
// header 101 + revision, manufacturer 0 + device code 0010 (mouse), error
// 00, no buttons.
queue(0xa0); // 1010 0000: header 101, revision 0
queue(0x02); // manufacturer 0, device code 0010 = mouse
queue(0x00); // error code 0 = OK
queue(0x00); // no buttons
}
void vt1200_vsxxxaa_device::send_report(int dx, int dy, int buttons)
{
int sx = (dx >= 0) ? 1 : 0;
int sy = (dy >= 0) ? 1 : 0;
int ax = (dx < 0) ? -dx : dx;
int ay = (dy < 0) ? -dy : dy;
if (ax > 0x7f) ax = 0x7f;
if (ay > 0x7f) ay = 0x7f;
const u8 b0 = u8(0x80 | (sx << 4) | (sy << 3) | (buttons & 0x07));
queue(b0);
queue(u8(ax));
queue(u8(ay));
}
TIMER_CALLBACK_MEMBER(vt1200_vsxxxaa_device::power_up_selftest)
{
send_self_test();
}
TIMER_CALLBACK_MEMBER(vt1200_vsxxxaa_device::scan)
{
// Hold TxD at idle-high when not transmitting (see the LK401 scan note).
if (fifo_empty() && is_transmit_register_empty())
m_tx_handler(1);
const u8 now_x = u8(m_host_x->read());
const u8 now_y = u8(m_host_y->read());
// The MAME mouse inputs are relative; MAME accumulates them into the port
// value, so a signed 8-bit difference (with wrap) is the movement since the
// last scan. Screen Y grows downward; the VSXXX reports DEC Y (up).
int dx = int(now_x) - int(m_last_x);
int dy = int(now_y) - int(m_last_y);
if (dx > 0x80) dx -= 0x100; else if (dx < -0x80) dx += 0x100;
if (dy > 0x80) dy -= 0x100; else if (dy < -0x80) dy += 0x100;
m_last_x = now_x;
m_last_y = now_y;
const int buttons = m_host_buttons->read() & 0x07;
if (dx || dy)
{
m_acc_dx += dx;
m_acc_dy -= dy; // DEC Y is inverted relative to screen Y
}
if (m_stream)
{
if (m_acc_dx || m_acc_dy || buttons != m_last_buttons)
{
send_report(m_acc_dx, m_acc_dy, buttons);
m_acc_dx = m_acc_dy = 0;
}
}
m_last_buttons = buttons;
}
// year, name, parent, compat, machine, input, class, init, company, fullname
CONS(1990, vt1200, 0, 0, vt1200, vt1200, vt1200_state, empty_init, "DEC",
"VT1200 X terminal (v2.2 firmware)", MACHINE_NOT_WORKING | MACHINE_NO_SOUND)