docs: keyboard/mouse protocol spec — LK401 (LK201 protocol) + VSXXX-AA mouse, sources, MAME lk201 device

Distils public docs into reference/keyboard-mouse/PROTOCOL.md for the level-1
Keyboard (27) / Mouse (26) device tests:
- keyboard: 4800 8N1 RS-423, self-test via FD -> 01 00 00 00 (errors 3D/3E)
- mouse: 4800 8O1 RS-232, self-test via 'T' -> 4-byte report (errors 3D/3E, >=0x20 fatal)
- separate ports: DUART2 A=keyboard, B=mouse (NOT daisy-chained)
MAME already has an LK201 device (src/mame/dec/lk201.cpp/.h) but no DEC mouse.
This commit is contained in:
SDD committed 2026-10-16 11:38:52 +03:00
1 parent 23e508f075
commit 3fd1e13b2e
129 files changed
+401708

No files matched your search

@@ -0,0 +1,115 @@
# Keymap: LK401 → USB HID → veetee
elkay sends the PC key **in the same position** as each LK401 key, so veetee's stock positional
keymap (`crates/vt-keyboard/src/default.toml`) turns it back into the right DEC key. Other
software sees an ordinary PC keyboard.
LK401 keycodes come from `lkkbd.c`, with the LK401 labels added. Every row is **provisional**
until Phase 3 checks it on the real keyboard.
## Top row
| LK | LK401 key | HID usage | veetee |
|----|-----------|-----------|--------|
| `56` | F1 Hold Screen | F1 `3A` | hold-screen |
| `57` | F2 Print Screen | F2 `3B` | print-screen |
| `58` | F3 Set-Up | F3 `3C` | set-up |
| `59` | F4 Data/Talk | F4 `3D` | session |
| `5A` | F5 Break | F5 `3E` | break |
| `64`–`68` | F6–F10 | F6–F10 `3F`–`43` | f6–f10 |
| `71` | F11 (Esc) | F11 `44` | f11 |
| `72` | F12 (BS) | F12 `45` | f12 |
| `73` | F13 (LF) | F13 `68` | f13 ⚠ |
| `74` | F14 | F14 `69` | f14 ⚠ |
| `7C` | Help | F15 `6A` | help ⚠ |
| `7D` | Do | F16 `6B` | do ⚠ |
| `80`–`83` | F17–F20 | F17–F20 `6C`–`6F` | f17–f20 ⚠ |
⚠ On Linux these need the xkb option `elkay:fkeys`; see
[host/linux/README.md](../host/linux/README.md). Windows needs nothing.
## Editing and cursor keys
| LK | LK401 key | HID usage | veetee |
|----|-----------|-----------|--------|
| `8A` | Find | Insert `49` | find |
| `8B` | Insert Here | Home `4A` | insert-here |
| `8C` | Remove | Page Up `4B` | remove |
| `8D` | Select | Delete `4C` | select |
| `8E` | Prev Screen | End `4D` | prev-screen |
| `8F` | Next Screen | Page Down `4E` | next-screen |
| `A7` | ← | Left `50` | left |
| `A8` | → | Right `4F` | right |
| `A9` | ↓ | Down `51` | down |
| `AA` | ↑ | Up `52` | up |
## Numeric keypad
| LK | LK401 key | HID usage | veetee |
|----|-----------|-----------|--------|
| `A1` | PF1 | Num Lock `53` | pf1 |
| `A2` | PF2 | KP / `54` | pf2 |
| `A3` | PF3 | KP * `55` | pf3 |
| `A4` | PF4 | KP − `56` | pf4 |
| `A0` | KP − | KP = `67` | kp-minus (veetee after 1.7.0) |
| `9C` | KP , | KP + `57` | kp-comma |
| `95` | Enter | KP Enter `58` | kp-enter |
| `94` | KP . | KP . `63` | kp-period |
| `92` | KP 0 | KP 0 `62` | kp0 |
| `96`–`98` | KP 1–3 | KP 1–3 `59`–`5B` | kp1–kp3 |
| `99`–`9B` | KP 4–6 | KP 4–6 `5C`–`5E` | kp4–kp6 |
| `9D`–`9F` | KP 7–9 | KP 7–9 `5F`–`61` | kp7–kp9 |
The HID Keypad Comma usage (`85`) would be the obvious choice for KP `,`, but xkb gives it
`KP_Decimal`, which collides with KP `.`. KP + reaches veetee's existing kp-comma binding.
## Modifiers and special keys
| LK | LK401 key | HID usage | Notes |
|----|-----------|-----------|-------|
| `AE` | Shift (left) | Left Shift `E1` | |
| `AB` | Shift (right) | Right Shift `E5` | LK401 mode only |
| `AF` | Ctrl | Left Ctrl `E0` | |
| `B0` | Lock | Caps Lock `39` | Lock LED follows the host's Caps Lock |
| `AC` | Alt Function (left) | Left Alt `E2` | LK401 mode only |
| `B2` | Alt Function (right) | Right Alt `E6` | LK401 mode only |
| `B1` | Compose Character (left) | Menu `65` | proposed; GNOME Compose Key = Menu |
| `AD` | Compose Character (right) | Menu `65` | LK401 mode only |
| `BC` | `<X]` | Backspace `2A` | veetee: delete |
| `BD` | Return | Enter `28` | |
| `BE` | Tab | Tab `2B` | |
| `D4` | Space | Space `2C` | |
## Main typing keys (US layout)
| LK | Key | HID | LK | Key | HID | LK | Key | HID |
|----|-----|-----|----|-----|-----|----|-----|-----|
| `BF` | `` ` ~ `` ? | `35` | `C0` | 1 | `1E` | `C5` | 2 | `1F` |
| `CB` | 3 | `20` | `D0` | 4 | `21` | `D6` | 5 | `22` |
| `DB` | 6 | `23` | `E0` | 7 | `24` | `E5` | 8 | `25` |
| `EA` | 9 | `26` | `EF` | 0 | `27` | `F9` | - _ | `2D` |
| `F5` | = + | `2E` | `C1` | Q | `14` | `C6` | W | `1A` |
| `CC` | E | `08` | `D1` | R | `15` | `D7` | T | `17` |
| `DC` | Y | `1C` | `E1` | U | `18` | `E6` | I | `0C` |
| `EB` | O | `12` | `F0` | P | `13` | `FA` | [ { | `2F` |
| `F6` | ] } | `30` | `F7` | \ \| | `31` | `C2` | A | `04` |
| `C7` | S | `16` | `CD` | D | `07` | `D2` | F | `09` |
| `D8` | G | `0A` | `DD` | H | `0B` | `E2` | J | `0D` |
| `E7` | K | `0E` | `EC` | L | `0F` | `F2` | ; : | `33` |
| `FB` | ' " | `34` | `C9` | < > | `64` | `C3` | Z | `1D` |
| `C8` | X | `1B` | `CE` | C | `06` | `D3` | V | `19` |
| `D9` | B | `05` | `DE` | N | `11` | `E3` | M | `10` |
| `E8` | , < | `36` | `ED` | . > | `37` | `F3` | / ? | `38` |
`lkkbd.c` calls `BF` KEY_ESC, but the LK401 has no Esc key; it is probably the `` ` ~ `` key.
**Verify**.
## Open questions
1. **Compose Character.** Proposed: Menu (`65`). veetee's default Compose setting is Local
Compose, which on a Linux desktop means the input method's compose. GNOME can use Menu as
its Compose key (Settings → Keyboard → Compose Key), so no veetee change is needed.
2. **No Esc key.** DEC-faithful behaviour is F11. Optionally, a converter layer could make
F11 send Esc for desktop use.
3. **PF1 is Num Lock.** Pressing it toggles the host's Num Lock state. veetee binds both keypad
states, so it is harmless there, but other applications will see the keypad change.
@@ -0,0 +1,149 @@
# Phase 1: characterising the keyboard
Goal: before building anything, find which connector pin is which, how much current the
keyboard draws, and what voltages its data line uses. Only a multimeter and a 12 V supply are
needed; no microcontroller yet.
Record every result in the table at the end and commit it.
## What you need
- LK401. Its cable is hardwired at the keyboard and ends in a 4P4C (RJ10) plug, about 7.7 mm
wide. Don't cut it.
- Multimeter
- USB adjustable power module with a display (3 W, short-circuit protected) as the bench
supply. The MT3608 is kept for the final box.
- An RJ10 (4P4C) female-to-female coupler, 1:1 wiring
- An RJ10 plug-to-plug flat cable with one end cut off and the four wires stripped
- A breadboard or terminal block for the flat cable's wires; jumper wires
- A 4P4C jack breakout board with screw terminals. They are often sold as "RJ9"; RJ9, RJ10 and
RJ22 all mean the same 7.7 mm, 4-contact handset jack. Not RJ11 or 6P6C, which is wider.
The first setup is: LK401 plug → coupler → flat cable → four bare wires. The flat cable's wires
are fine and break easily, so once the breakout board has been mapped (section 2) the LK401
plugs straight into it instead, and everything connects to its screw terminals.
## 1. Set the supply
1. Plug the power module into USB, ideally on an extension cable, with **nothing** connected
to its screw terminal output except the multimeter.
2. Find the + terminal: a positive reading means the red probe is on +.
3. Turn the trimmer (clockwise raises it) until the **multimeter** reads **12.0 V**. The
module's own display can be a few tenths out. Too much voltage can damage the keyboard.
4. Unplug it again.
5. **Before the first power-up, do the load test.** Put a 1 kΩ resistor across the output for
5 minutes. If it holds steady, re-set it to 12.0 V with the resistor in place. If it keeps
climbing, don't use it on the keyboard until that is understood.
A multimeter with a weak battery can read high and drift. An apparent upward creep of about
0.8 V turned out to be exactly that. If readings wander, change the meter battery first.
## 2. Map the flat cable (no keyboard needed)
This finds which bare wire goes to which pin, so the colours found in section 3 can be checked
against the pin order in `lkkbd.c`. The keyboard is not involved.
Pins are numbered as in [protocol.md](protocol.md): hold the plug with the cable downwards, the
latch away from you and the contacts facing you. Pins 1–4 then run from left to right.
1. Meter on **continuity** (beeper), or the lowest Ω range.
2. For each plug contact 1–4 in turn, hold a probe tip on the contact's gold edge and touch the
other probe to each bare wire until it beeps. Record the colour. Each contact should connect
to exactly one wire, reading under about 1 Ω.
3. Check for shorts: probe each of the six pairs of bare wires. All should read OL.
Once the coupler has arrived, map through it too, before the keyboard is plugged in:
4. Plug the flat cable into one side of the coupler. Look into the empty side with the latch
slot at the top. The contacts are along the bottom, and pin 1 is on the **right**, because a
socket is the mirror image of a plug. The keyboard's pin 1 will land on that contact.
5. Meter probes are too thick for the socket, so hold a jumper wire's male pin against one
probe and use it as a fine tip. Touch each socket contact and find its bare wire, as in
step 2. Record the colours. This maps each keyboard pin to a colour, and shows whether the
coupler is straight or crossed.
Once the breakout board has arrived, map it before the keyboard goes near it. Boards label
their terminals differently, and some number from the other end, so don't trust the labels:
6. Plug the flat cable's plug into the board's jack.
7. On continuity, find which screw terminal beeps with each colour. Using the keyboard pins from
step 5, label each terminal with its pin number and signal.
8. Plug the LK401 into the board and repeat the pin 1 check from section 3, step 4, on the
20 MΩ range. If pin 1 still reads open, the coupler's contact is ruled out as the cause.
## 3. Find ground and +12 V (keyboard unpowered)
The keyboard is not opened and its cable is not touched: its plug sits in the coupler exactly
as it would in a terminal. Everything is measured on the cut end of our own flat cable.
1. Plug the LK401's plug into the coupler and the flat cable into its other side. Push the four
bare wires into a breadboard or terminal block so none can touch. Label each wire with its
colour and the keyboard pin section 2 mapped it to.
2. Per `lkkbd.c` the order is `1 RX-in, 2 GND, 3 +12 V, 4 TX-out`. A reversed cable only flips
it end to end, so the **two middle wires are the supply** and the two outer wires are data.
Check this in the next steps rather than trusting it.
3. Meter in **diode-test** mode. It drives about 1 mA at 2–3 V, harmless to the keyboard.
Measure between the two middle wires, then swap the probes:
- **red on +12 V, black on GND:** the reading climbs and ends at OL, as the meter charges
the keyboard's supply capacitor;
- **red on GND, black on +12 V:** a steady 0.4–0.7 V through the protection diodes of the
keyboard's chips.
4. Red probe on GND, black probe on each outer wire in turn. The keyboard's output (pin 4)
should show a diode drop. Its input (pin 1) may read OL: on our keyboard it read OL to every
other pin in both directions, in diode test and on the 20 MΩ range. Whether that is a
high-impedance input or an open wire is settled when the keyboard is first sent a command
(**verify**). A reading of 0 on any data wire is a short: stop.
5. If the middle wires don't behave as in step 3, stop and record every pair's readings in both
directions before powering anything.
## 4. Power up and measure current
1. Put the multimeter in **DC current (mA)** mode, in series between the supply's +12 V
output and the +12 V pin. Connect the supply's ground to the GND pin.
2. Power up. The keyboard should beep or click and flash its four LEDs, which is its self-test.
3. Record the current at idle, the **peak** as the LEDs flash, and the current with keys held.
4. If the supply's display blanks or the keyboard keeps restarting, the supply is hitting its
3 W limit (about 250 mA at 12 V). Note it; the final MT3608 has more headroom.
## 5. Identify the data pins and their levels
Keyboard powered, multimeter in DC volts, black probe on GND:
1. Measure each of the two data pins at idle. The keyboard's **output** (TX) is driven, so it
will show a steady voltage. Its **input** (RX) will read near 0 V or float.
2. On the TX pin, hold a key with auto-repeat, such as a letter, and watch the reading. A
multimeter can't show the serial data, but the average voltage will shift while bytes are
being sent.
3. Record the idle voltage of TX. It tells us:
- idle near **0 V**, active high: RS-232 polarity, and the receive path needs an inverter
(the expected case);
- idle near **+12 V** or +5 V: TTL polarity, and a resistor divider is enough.
## Results
| Measurement | Value |
|-------------|-------|
| Supply display offset | display 12.4 V = meter 12.0 V (1 kΩ load) |
| Supply drift, 1 kΩ load | holds 12.0 V (step 5 passed) |
| Flat cable plug, pins 1–4 (colours) | 1 yellow, 2 green, 3 red, 4 black; no shorts between wires |
| Through coupler, keyboard pins 1–4 (colours) | 1 yellow, 2 green, 3 red, 4 black |
| Coupler wiring (straight / crossed) | straight |
| Pin 1 (yellow) resistance | OL to every other pin, both directions, on 20 MΩ |
| Breakout board, terminal for pins 1–4 | |
| Pin 1 resistance via breakout board | |
| GND wire (colour) | green (keyboard pin 2) |
| +12 V wire | red (keyboard pin 3) |
| Keyboard TX wire | |
| Keyboard RX wire | |
| Current, idle | |
| Current, LED flash peak | |
| Current, keys held | |
| TX idle voltage | |
| TX voltage with a key held | |
| Diode test, +12 V → GND (red → black) | OL |
| Diode test, GND → +12 V | 771 mV |
| Diode test, GND → each data wire | yellow (pin 1) OL; black (pin 4) 1027 mV |
With these, the interface circuit in [hardware/README.md](../hardware/README.md) can be
confirmed and the power budget checked: USB 2.0 gives 500 mA at 5 V, and the boost module turns
roughly 1 mA at 12 V into 3 mA at 5 V.
@@ -0,0 +1,119 @@
# LK201 / LK401 protocol
What the firmware relies on. Sources: the VCB02 technical manual (EK-104AA-TM-001, LK201
chapter) and Linux `drivers/input/keyboard/lkkbd.c`. Anything marked **verify** comes from
secondary sources or memory and must be confirmed on this keyboard in Phase 3.
## Physical layer
- Asynchronous serial, **4800 baud, 8 data bits, no parity, 1 stop bit**, full duplex.
- RS-423-style single-ended levels. The keyboard has only a +12 V supply, so its output cannot
swing negative; the real levels are measured in [Phase 1](phase1-bench.md).
- Polarity is RS-232 style: idle (mark) is the *low* voltage, which is the opposite of a
microcontroller UART, so both directions go through an inverter.
- Connector: 4P4C modular. Per `lkkbd.c`, looking at the plug with the cable downwards and the
latch away from you, pins 1–4 left to right:
| Pin | Signal |
|-----|--------|
| 1 | Data **to** keyboard (host TX) |
| 2 | Ground |
| 3 | +12 V |
| 4 | Data **from** keyboard (host RX) |
Modular cables can be straight or reversed, so **verify** at the end of the cable we use.
## Power-up
On power-up, or after the host sends `FD` (power-up reset), the keyboard runs its self-test,
flashes the LEDs, and sends four bytes:
| Byte | Meaning |
|------|---------|
| 1 | Keyboard ID, firmware (`01`) |
| 2 | Keyboard ID, hardware (**verify**: `00`) |
| 3 | Error: `00` none, `3D` key down at power-up, `3E` self-test failed |
| 4 | Keycode of the key held down, or `00` |
The host then asks for the ID (`AB`), which returns two bytes. `lkkbd.c` tells an LK201 (`01`)
from an LK401 (`02`) by the second of them (**verify**).
Because the converter and the keyboard power up together, the power-up bytes are usually sent
before anyone is listening. The console's `reset` command sends `FD` to repeat them.
## Keycodes and special responses
Every key sends a single byte from `56` to `FB`. The bytes `B3`–`BB` are responses, not keys:
| Byte | Name | Meaning |
|------|------|---------|
| `B3` | ALL UPS | The last down/up key was released: every key is now up |
| `B4` | METRONOME | Auto-repeat tick (not used: elkay turns auto-repeat off) |
| `B5` | OUTPUT ERROR | Keyboard's transmit buffer overflowed |
| `B6` | INPUT ERROR | Keyboard received a bad command |
| `B7` | KBD LOCKED | Acknowledges "inhibit transmission" |
| `B8` | TEST MODE ACK | Acknowledges test mode |
| `B9` | PREFIX KEYS DOWN | Precedes a list of held keys after a mode change |
| `BA` | MODE CHANGE ACK | Acknowledges a mode-set command |
The full keycode table is in [keymap.md](keymap.md).
## Divisions and modes
Keys are grouped into 14 **divisions**, and each division is in one of three modes:
| Mode | Bits | Behaviour |
|------|------|-----------|
| Down only | `00` | Keycode on press, nothing on release |
| Auto-repeat down | `01` | Keycode on press, then `B4` repeats while held |
| Down/up | `11` | Keycode on press, and again on release |
USB HID needs releases, so elkay sets **every division to down/up** and lets the host do the
auto-repeat.
A mode-set command is one byte: `1 DDDD MM 0` (division in bits 6–3, mode in bits 2–1), so
down/up for division *d* is `0x86 | d << 3`.
### Releases in down/up mode
A release is reported by sending the same keycode again, **except** when the released key was
the only down/up key still held: then the keyboard sends `B3` (ALL UPS) instead. So the firmware
keeps a set of held keys, toggles a key's state each time its code arrives, and clears the whole
set on `B3`. This is also what `lkkbd.c` does.
## Commands (host → keyboard)
Parameter bytes have bit 7 set; the last byte of every command has bit 7 set, which is how the
keyboard knows the command is complete.
| Bytes | Command |
|-------|---------|
| `FD` | Power-up reset (self-test, then the four power-up bytes) |
| `AB` | Request keyboard ID (two bytes back) |
| `D3` | Reinstate defaults |
| `E9` | Enable LK401 mode: separate codes for the Alt Function keys, right Compose and right Shift |
| `0x86 \| d<<3` | Division *d* to down/up mode (d = 1–14) |
| `13 p` | LEDs on, `p` = `0x80` \| mask |
| `11 p` | LEDs off, `p` = `0x80` \| mask |
| `99` | Disable keyclick |
| `1B v` | Enable keyclick, volume `v` = `0x80` \| 0–7 (0 loudest) |
| `B9` / `BB v` | Disable / enable Ctrl-key click |
| `A1` / `23 v` | Disable / enable bell, with volume |
| `A7` | Sound the bell |
LED mask bits: `01` Wait, `02` Compose, `04` Lock, `08` Hold Screen.
## Initialisation sequence used by elkay
Taken from `lkkbd_reinit()`, with keyclick off:
```
AB request ID
D3 defaults
E9 LK401 mode
8E 96 9E … F6 divisions 1–14 to down/up
99 B9 keyclick and Ctrl click off
11 8F all LEDs off
```
The console's `init` command sends exactly this.