Symptom: in boot protocol (BIOS / UEFI) one keypress emitted ~10 characters. _kbd_write_ep1 was rebuilding and re-arming EP1 on every USB SOF poll; report-mode HID drivers diff consecutive reports and collapse duplicates, but boot-mode hosts treat each IN as a fresh press. Changes: - Add 9-byte lastEP1_* shadow in bank-0 RAM, plus hidProtocol / hidIdleRate / hidIdleCounter state. - _kbd_write_ep1: compare freshly-built bytes against the shadow with CMPRS A, M. On match, only resend when the SET_IDLE counter has expired; otherwise NAK silently. On mismatch, copy to shadow and send. - _usb_htd_hid_set_idle: actually capture wValueHi (HID 1.11 §7.2.4) instead of just ACKing — store as hidIdleRate, reload counter. - New _usb_htd_hid_set_protocol: capture wValueLo (HID 1.11 §7.2.5) into hidProtocol; invalidate shadow so the new wire format ships immediately. Wire dispatch table (0x210b) to it instead of the default STALL handler. - _usb_sof: tick hidIdleCounter when rate is non-zero so the resend fires at idle-rate cadence. - _kbd_write_ep1 sets UE1R_C=8 in boot protocol, 9 in report. The 9th byte (consumer/extraState0) is kept in the EP1 buffer slack but truncated on the wire under boot protocol. Net effect: a held key in BIOS mode now produces one keydown plus one keyup, matching report-mode behaviour.
ku1255cfw
Custom open firmware for the Lenovo KU-1255 compact USB keyboard
Overview
The Lenovo ThinkPad Compact USB Keyboard with TrackPoint (KU-1255) uses a Sonix SN8F2288FG 8-bit MCU with 12K words (24KB) flash and 512 bytes RAM. The TrackPoint is connected via bit-banged I2C (P2.4 SCL, P2.5 SDA, address 0x2A, Synaptics proprietary protocol).
This firmware is a from-scratch rewrite based on the original by ranma, adding standalone middle-button scroll and other missing features so the keyboard works fully without an external converter.
Features added over base firmware
Middle-button scroll
3-state state machine (IDLE / UNDECIDED / SCROLLING) with 150ms timeout:
- Short press (<150ms): sends a normal middle click (deferred on release)
- Hold + TrackPoint movement: converts XY deltas to scroll wheel events
- FN + middle button: passes middle button through directly (no scroll logic)
CapsLock LED feedback
Host LED output reports (SET_REPORT) are parsed and CapsLock state (bit 1) drives the power LED on P5.3/PWM0 (active-low). The flasher magic byte sequence detection is preserved.
FN+F7 through FN+F12
| Key | Normal | With FN held |
|---|---|---|
| F7 | F7 | LGUI+P (display settings) |
| F8 | F8 | F8 (passthrough) |
| F9 | F9 | LGUI+I (settings) |
| F10 | F10 | LGUI (search) |
| F11 | F11 | LCTRL+LALT+TAB (task switch) |
| F12 | F12 | F12 (passthrough) |
USB compliance fixes
- HID GET_REPORT: returns proper per-interface empty reports (8B keyboard / 5B mouse) instead of stale buffer contents. Some OSes query this on resume from suspend.
- SET/CLEAR FEATURE: tracks DEVICE_REMOTE_WAKEUP state instead of just ACKing.
Flash space
| Firmware | Words used | Free | Utilization |
|---|---|---|---|
| OEM | 10,226 / 10,239 | 13 | 99.9% |
| This | 10,238 / 10,239 | 1 | 100.0% |
Key debouncing is not implemented (and not needed) — the 8ms scan cycle naturally debounces scissor switches (<1ms bounce time). The OEM firmware also does not debounce.
Flashing
Requires vpelletier/dissn8 tools.
- Build:
asn8 main.s -o ku1255cfw.bin - Enter bootloader: hold Return while plugging in the keyboard
- Flash:
flashsn8 ku1255cfw.bin
Simulator testing
test_scroll.py runs 23 automated tests against the
dissn8 simulator via the ku1255_sim.py
harness. Tests cover all scroll state transitions, deferred click timing, FN modifier
interaction, and edge cases (rapid clicks, timeout behavior).
Note: The simulator requires a fix for PnUR register read handlers (see below).
Simulator fixes (for vpelletier/dissn8)
Running this firmware in the dissn8 simulator exposed three bugs:
-
PnUR read handlers crash —
_volatile_dictmaps P0UR-P5UR read handlers toNone. B0BSET/B0BCLR (read-modify-write) on these registers causesTypeError: 'NoneType' object is not callable. Fixed by addingreadPullUp()returning the latch value. Branch:pnur_readpullup(PR pending upstream). -
Hardcoded HID descriptor sizes —
ku1255_sim.pyassumed 0x51/0xD3 byte HID report descriptors (OEM sizes). This firmware uses 91/61 bytes. Fixed by parsing sizes dynamically from the config descriptor. -
Wrong HID descriptor recipient — HID report descriptor requests must use interface recipient (0x81), not device (0x80), per USB spec. The OEM firmware happened to accept both.
Dev setup
- OpenViszla USB protocol analyzer
- 5V-tolerant PL2303 UART interface (e.g. https://www.adafruit.com/product/954)
- S15 pad (SN8F2288 UTX) connected to header for UART debug interface
