From 02acabb7ef9522afd2b0fce0c76f76451b9527ee Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Fri, 16 Oct 2026 15:33:30 +0300 Subject: [PATCH] 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. --- harness/mame/INPUT_REPORT.md | 195 +++++ harness/mame/README.md | 34 +- harness/mame/input_cursor_after_f4600.png | Bin 0 -> 2789 bytes .../mame/input_cursor_band_after_f4600.png | Bin 0 -> 544 bytes .../mame/input_cursor_band_before_f4400.png | Bin 0 -> 635 bytes harness/mame/input_cursor_before_f4400.png | Bin 0 -> 2816 bytes harness/mame/input_inject.lua | 46 + harness/mame/input_probe.lua | 20 + harness/mame/vt1200.cpp | 790 +++++++++++++++++- 9 files changed, 1079 insertions(+), 6 deletions(-) create mode 100644 harness/mame/INPUT_REPORT.md create mode 100644 harness/mame/input_cursor_after_f4600.png create mode 100644 harness/mame/input_cursor_band_after_f4600.png create mode 100644 harness/mame/input_cursor_band_before_f4400.png create mode 100644 harness/mame/input_cursor_before_f4400.png create mode 100644 harness/mame/input_inject.lua create mode 100644 harness/mame/input_probe.lua diff --git a/harness/mame/INPUT_REPORT.md b/harness/mame/INPUT_REPORT.md new file mode 100644 index 0000000..fac8532 --- /dev/null +++ b/harness/mame/INPUT_REPORT.md @@ -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=` — per-byte device TX/RX trace. diff --git a/harness/mame/README.md b/harness/mame/README.md index 5835793..bfd72dd 100644 --- a/harness/mame/README.md +++ b/harness/mame/README.md @@ -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 diff --git a/harness/mame/input_cursor_after_f4600.png b/harness/mame/input_cursor_after_f4600.png new file mode 100644 index 0000000000000000000000000000000000000000..e3586ac75e4b51d91b9c95a4d4360191e70d4a69 GIT binary patch literal 2789 zcmcImc|25WA3tY~8RFO~jOJQeEN`~4gw!w>S#D)Xix49sWyw;+;55klGPYD?iQGz- zkz|c)MiI((QzYATRhDXyZL+=xxA(s5z3)Hwoab}Sd7kq;zwP_|{hsG_!ZF8!yA)%^u@-!fQZ(phq)!a$NWAWH^zH1?lP+Izt2}t4)DMVd`QZa~tAf&X$7l z4Zg3U$7#vnXByT@2|xFVH!>+v*jhHM?8G)igv`gT#^9A23q@GT(Cg$|06+UY_{rD; zz)l!K`X+f;D_~HH)wCP|IEKO8?+V4q;AZmT8DLeI4jiU{Ea~uqHS);Q5uOfPB9910 zaaCqEUsX24UcPc#-ufk2+@a3oc5YQ*CAK5lmH)Fl{R8DVKtZ@aG(SANwBw(r22H=QwI1 zqX#eRZ_iLY8zUa%U1vmb{e?d-uVGHiP0~rd^zvDO{uRNVXyk_^f=O> ziQOxs>eNN|qf=#Ed$kP|BKr{-dt!3)tsB}?)3}_$+wscM*1AedhpV~j7mw3i_^s#S z>htYlx?Oz&?YMs-fj^=0_cPeah-mAu^l_jw+Lm;q#l}N^<-4bat+?Us?XdfV%b?R3Z+#$rfUx1x=TR~=wnv|NHVsU9K`)5smz+2SNpE@{ z6c~$u%%@1fPEWo4Za3*E5*yQdJK!KENIbMW)1m~QKN;JW{Hn=lb3(55&*ckMn#9Kl z^8&0pJKB|5|4Mg4OXy10UXeUzW%Br9)cdnBx2kK7-2lTokP|CnxdFzSl}Fm}9T$vCQW{79)l5wks@+mV!EDT;b9 zTt<@`_R^sk6SKp}xR=8B#99!z36b$(*^c;h^|v4?@sXtZ8nPr2OUzfEx}tE$UCUd6B5^H5}3Z|ND znFMf(Gu#%J@W!{Yr|k^r$_i~ndK%2EvBdsKMMlKdUaaY?hA2ABpj0`p0(4JC1a$%C zKE!3*7?Znx%y0aL?SktpsT~RQ#OeedW*l=ZjQX**>f>XoX~Qq$!8rzyxAca*;OU$} zDAOw{(}r7+a&*{IC%N_ZMERyLBHJ=kwEy!VRVY=i$Z@+(7UgGq-FGelqg9KlG7XH^ zoeiLluGT5mLH{SE{({#)dQ?Gjj_y9^A3S~-D>YK(lu3`~etA1m?cl#kA=El60EPP> z&7fR{c-Oo+%idGDEmZ9x95iiarYSdV)zX|UA158tOwC-WAZlnaoN*50yoC#+CE%kk zMX-^73J%O5_)#+|pCbe~^sl5T*x-ml5ZJ|xb;7QiJwv&KA)?0(^EI^1+nuzu@eB5*` zq;FR{d{3-UtWai^Vr%sJ?YZ2H%&2Uc4GVP^$xpi4+Pf19y8<#bk;&@-I#b(77WKND zw^%oj2A0tVdo*70ccHf%8C-Y$8(r7%sap#X-+EMtYpT{VRdS79O$b^A&{Z|F+gq1a zWwJspgOcautRGCc)|CGBbdDsdNX|YYJA&qRb!k)oK ztfks|MZn_f14B<%*z2(NR4B}NVgJi6{C{lJf6kQuaVq$8l>LF@LX>|$oUEGBAs*D; z4iwMyU?e9}j;$oPd<>Zq|Hcv&sR}Lj%EycIejGWa{b+9VrPsOxLk-h2juPn4@JXlJ zJIZIMZm=W$l44jS%7pP~`#X8d5~ixn(FxMw(r03nQ07(aqPXHd=@A#sHbu<8XiG;o z&IHCr24&>(T5U%UPAU^~`Vgxt*I^Up58U_`QnuML)Ym9TKTxHizj^>Q-)CW5WJsa? E9b%DYzyJUM literal 0 HcmV?d00001 diff --git a/harness/mame/input_cursor_band_after_f4600.png b/harness/mame/input_cursor_band_after_f4600.png new file mode 100644 index 0000000000000000000000000000000000000000..fc28cd3339e8e2aadbaf4639732681003bc75a74 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0y~yVA%j~)y}`)Iz`()m z_e#1tYJnphPgz-%A^Vgm&@#E6tcRp=18VZpS&;yMC4 zEZi*K8yq$W(3E-532MbbCX$$soHz6^2MCR{#J2 literal 0 HcmV?d00001 diff --git a/harness/mame/input_cursor_band_before_f4400.png b/harness/mame/input_cursor_band_before_f4400.png new file mode 100644 index 0000000000000000000000000000000000000000..5a85fac057957ef01d59416b44ab46da9a4816cc GIT binary patch literal 635 zcmeAS@N?(olHy`uVBq!ia0y~yVA%j>$8$ z@b~}aOG_Mo9f%8OWwVZ%VRosKVVZ>>1BX~c0%H@aLIgt-E11n824*Wn0L6)uL{oUA zi8b<@euP2DK5+mA;ktya7qSwvdkchv}xuU;oMiox=pwD4?@0 z|Jt7`I&f~&!Al6vth2xVu8QOkYhrC`1^V7Zcv%>nGi5eN5ELkQ16zkz3>cG<^ST)I z?=rG*3+ON$WD?ME*ucQT4J1HpAOU6*kc<%50UET)MwB^j7c*&5`+M)K&J)YCg3itX Or7BNXKbLh*2~7aVI=PSl literal 0 HcmV?d00001 diff --git a/harness/mame/input_cursor_before_f4400.png b/harness/mame/input_cursor_before_f4400.png new file mode 100644 index 0000000000000000000000000000000000000000..92aa47f068c56bc7e4374ade786cc9b871ce2118 GIT binary patch literal 2816 zcmcImdpK0<8eeOcS;VqUg*~!2iqhUR20P?7b5Q8Gbu-GPaVaJtnG?I1)|OOAY#qrZ z;*{E@NNx#B9=FD&kV^(rl3OnA>7trA->CCE?em;V|Ll3*XVzNZv)23m-rxIs-|su+ z;$$l?yFwO%AbEQ`o2?K;!XXH*lK~4*a`dYl2$F5IxB1KVh{sa{@u|5cikqH@=Ji^y zbB=q!mmsC;sLMxw>&jgx?n{!g!laj%um2sEeEP*t=>IO-@Iq&E4?=Ss%~NJ(eoxGW zUu=x1Jpoxk8E;u$N+T(iu26ngR1l;-^FpO+E&}p(Yv=S%Dz%h`l#PM33!j#DYu)*TVcrs=0CP<1h*J6}2?ExV_qEb{BeGLMS zgJE<#CPUBhVdAh7Lc*?4DQspv{^^wm$4mV^Tlshx2esr7`zv~_54Dy~Q4iGP!*Rdk$_w{Ef^X#0NdN?+>RmVh*Z6m6%RVH4obah@M+MeFbIH4a{ z4C{Kj*BH2xs8vw+LYF;^p@Lw-y8*8vD>fcXSzYY*ylqmtu|S2pMD$F#4ji!0gnuP$B0#Eym9pTaSin551G)_b6J2|9sN`8GL|yE-2-5 zZ4AqIoDOR_{n$wVM$`rd{T{xnmd*T#V*y%HY$nRZWE$}@17QbQ2W!H$$@B)posD{( z*5E-}y711)nX>no%50B(TDH%SN9;Wui->^oL(^`5XjSuJqT&|bPMtKk7Jm<`B!LU~ ziosznF~2_5hU;a?YwFrcC&4VNJTlayrJ%3qT<`6oxbv{syN?#Xw!y>CY+Dz=BFlzx0Z#@mZabBno{$^G>~e#E%!0G zXUP9f;7zr*e3=+uVt7|=qG-^VZ9~uDG0`rCD@rXAFuOwtx3Q4!ffhVJG3GX$P@!Fm zA4!P8tSv2eyls#t?T9X-GMnWPQi*V6#yG3DFzRdoC|+UM%#a&(L$W_A=uk*|Ps%ejEX+*Z(-fkY>71VG+)qLy`|QvUjne%QR{7Nm_an^paarpKEmf!W`dDRt@_abfUpWZy2 z%Rs6vmTi4Kwy{Rqv|rXNf0m_J(3Ty%Cd2+okhYjjxMwXLnA%E1(pKN_(%hEKe&}xU z%$GY}A*QL<7xZ@4!{3_xw1+5r9?KbB@gqW&V`*;;jk|ZY<7^k_gohx|qQiNl?jft< zc??_<4bbI4i=2F72H^!U*bXVbGRk`0GCz3jRXH$|oV0iI*u}IIW5*R1iYf0*7ec)K z0(RA5<%0@tZ#(xO+!0Te*yl8Pl8N!NIg`+H@DQDC?0qw#IY;tE$Z7fE53iQGhUjxwBqQ z6j#Dzouak(J=oMl4p3$-GU`Fg9RbpdE#7o_Mvq`%81|uHm8GL4-u8n zSweSI5aewgN}hLdvZGcUd|UbaJRq_p`Zds(9~1q95e${W>Wy{XH=Qx7^}qi9ru05O z_{yM_Ew4vVjz46>$YXFnFT`UQU{46NmV{4zu1bH%qORGiFA?ZMN&Q|4>}*j z`vVhY-kBPVVf9)q0KM@J00Q3@!Xe#sef;O#>cVpqN10(xMgBp#6Z%W zQV1^tw$?3EyI&flCTW#JE&ZN7a-gp{Ojs~@Hl&P$`Vq_BRj2OzG8=J!)1=Qd{UL)- zMf~z#4o%1)@ZDz0x)O?j)y%2tYal$=Kya@u zHdaSADPK-|d~#b;gKau=S+E4)ugCk2^@aVw-r1WwSO`K%O0Glwl&++vggRY=H@Uo0 za0j-mu(-Ng=RfSwU$Ke*Ij{Ve6T_bq?EfTw0Q&cX%OZ?YB9MpT1?|dBc(~U`_-w);x^)40JB9?S$w?f(G zZ68%_Y0W3js2x??7iS3O{<-yRKlFtu{X<~WvG*Nq8DPYpDOgs(9*gys?91R_wQiP| o4YMb=-B<#D&5;WrFVtRu%j1@w%qg^Tm7GoOH#*teuwqC569zkcWdHyG literal 0 HcmV?d00001 diff --git a/harness/mame/input_inject.lua b/harness/mame/input_inject.lua new file mode 100644 index 0000000..64195ec --- /dev/null +++ b/harness/mame/input_inject.lua @@ -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 \ +-- 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") diff --git a/harness/mame/input_probe.lua b/harness/mame/input_probe.lua new file mode 100644 index 0000000..ca71580 --- /dev/null +++ b/harness/mame/input_probe.lua @@ -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. +-- +-- 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") diff --git a/harness/mame/vt1200.cpp b/harness/mame/vt1200.cpp index 5ba70f7..d1d6e10 100644 --- a/harness/mame/vt1200.cpp +++ b/harness/mame/vt1200.cpp @@ -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 #include #include #include @@ -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 m_kbd; + emu_timer *m_scan_timer = nullptr; + emu_timer *m_power_up_timer = nullptr; + + std::vector 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 m_down = { }; + std::array m_repeat = { }; + int m_updown_down = 0; + + FILE *m_log = nullptr; // VT1200_INPUT_LOG= +}; + +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 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 m_lk401; + required_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)