Fix BIOS-mode 10x repeat: change-detection + real SET_IDLE / SET_PROTOCOL

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.
This commit is contained in:
Markus Fritsche
2026-04-13 15:08:30 +00:00
parent 5a1784cacd
commit bdb0e2d99f
+156 -6
View File
@@ -150,6 +150,22 @@ keyState17 DS 1 ; 136-
extraState0 DS 1
; HID change-detection shadow (mirrors what was last shipped on EP1)
lastEP1_0 DS 1 ; modifierState1
lastEP1_1 DS 1 ; reserved (always 0)
lastEP1_2 DS 1 ; bootKeys0
lastEP1_3 DS 1 ; bootKeys1
lastEP1_4 DS 1 ; bootKeys2
lastEP1_5 DS 1 ; bootKeys3
lastEP1_6 DS 1 ; bootKeys4
lastEP1_7 DS 1 ; bootKeys5
lastEP1_8 DS 1 ; extraState0 (consumer; only sent in report protocol)
; HID protocol + idle state (HID 1.11 §7.2.4 / §7.2.5)
hidProtocol DS 1 ; 0 = boot, 1 = report (default per spec)
hidIdleRate DS 1 ; units of 4ms; 0 = infinite (only on change)
hidIdleCounter DS 1 ; SOF-decremented; 0 = expired, force resend
i2cTxData DS 1
i2cRxData DS 1
i2cBitCnt DS 1
@@ -556,6 +572,23 @@ _usb_suspend:
_usb_sof:
B0BCLR FSOF
; Tick the HID idle countdown if a non-infinite rate was set.
; When it reaches zero, _kbd_write_ep1 will force a resend even
; if the report bytes haven't changed.
B0MOV A, hidIdleRate
CMPRS A, #0
JMP _usb_sof_tick_idle
JMP _usb_sof_kbd
_usb_sof_tick_idle:
B0MOV A, hidIdleCounter
CMPRS A, #0
JMP _usb_sof_dec_idle
JMP _usb_sof_kbd ; counter already zero; awaiting resend
_usb_sof_dec_idle:
DECMS hidIdleCounter ; skip next if result is 0
JMP _usb_sof_kbd
_usb_sof_kbd:
; Check for cross-talk from too many depressed keys
CALL _kbd_count_rows_low
; Read the columns
@@ -787,7 +820,85 @@ _mouse_write_ep2_exit:
_kbd_write_ep1:
B0BTS0 FUE1M0 ; Skip if zero
RET
; FUE1M0 is zero (NAK)
; FUE1M0 is zero (NAK): previous IN ACK'd by host, may load next.
; ---- Change detection (HID 1.11 §7.2.4 / boot-mode correctness) ----
; Compare each of the 9 report bytes against the shadow of what we
; last shipped. If anything differs, jump to the changed-path.
B0MOV A, modifierState1
CMPRS A, lastEP1_0
JMP _kbd_ep1_changed
B0MOV A, lastEP1_1
CMPRS A, #0 ; reserved byte should always be 0 in shadow
JMP _kbd_ep1_changed
B0MOV A, bootKeys0
CMPRS A, lastEP1_2
JMP _kbd_ep1_changed
B0MOV A, bootKeys1
CMPRS A, lastEP1_3
JMP _kbd_ep1_changed
B0MOV A, bootKeys2
CMPRS A, lastEP1_4
JMP _kbd_ep1_changed
B0MOV A, bootKeys3
CMPRS A, lastEP1_5
JMP _kbd_ep1_changed
B0MOV A, bootKeys4
CMPRS A, lastEP1_6
JMP _kbd_ep1_changed
B0MOV A, bootKeys5
CMPRS A, lastEP1_7
JMP _kbd_ep1_changed
B0MOV A, extraState0
CMPRS A, lastEP1_8
JMP _kbd_ep1_changed
; All bytes match shadow. Honor SET_IDLE: if the host set a non-zero
; idle rate AND the SOF-driven counter has expired, force a resend;
; otherwise NAK silently this poll.
B0MOV A, hidIdleRate
CMPRS A, #0
JMP _kbd_ep1_check_idle_counter
RET ; rate=0 (infinite) -> only on change
_kbd_ep1_check_idle_counter:
B0MOV A, hidIdleCounter
CMPRS A, #0
JMP _kbd_ep1_idle_alive
JMP _kbd_ep1_send ; counter expired -> resend now
_kbd_ep1_idle_alive:
RET ; counter still ticking -> NAK silently
_kbd_ep1_changed:
; Copy current report into the shadow so we'll diff next round.
B0MOV A, modifierState1
B0MOV lastEP1_0, A
MOV A, #0
B0MOV lastEP1_1, A
B0MOV A, bootKeys0
B0MOV lastEP1_2, A
B0MOV A, bootKeys1
B0MOV lastEP1_3, A
B0MOV A, bootKeys2
B0MOV lastEP1_4, A
B0MOV A, bootKeys3
B0MOV lastEP1_5, A
B0MOV A, bootKeys4
B0MOV lastEP1_6, A
B0MOV A, bootKeys5
B0MOV lastEP1_7, A
B0MOV A, extraState0
B0MOV lastEP1_8, A
; Fall through to send.
_kbd_ep1_send:
; (Re)load the idle countdown from rate (units of 4ms; we tick every
; 1ms, so we resend faster than spec — harmless for boot-mode hosts.)
B0MOV A, hidIdleRate
B0MOV hidIdleCounter, A
; Fill the EP1 buffer (offset 8) with the 9-byte composite report.
; In boot protocol the 9th byte (consumer/extraState) is silently
; dropped via UE1R_C=8 below; the byte still occupies buffer slack.
MOV A, #8 ; EP1 begins at offset 8
B0MOV UDP0, A
@@ -819,14 +930,19 @@ _kbd_write_ep1:
B0MOV UDR0_W, A
INCMS UDP0
MOV A, #9 ; EP1 count is 9
; Protocol-aware count: boot=8, report=9.
B0MOV A, hidProtocol
CMPRS A, #0
JMP _kbd_ep1_cnt_report ; protocol != 0 -> report
MOV A, #8 ; protocol == 0 -> boot
JMP _kbd_ep1_cnt_set
_kbd_ep1_cnt_report:
MOV A, #9
_kbd_ep1_cnt_set:
B0MOV UE1R_C, A
; Set EP1 to ACK
B0BSET FUE1M0
; MOV A, #','
; CALL _uart_tx
RET
_usb_reset:
@@ -884,6 +1000,20 @@ _usb_init:
B0MOV usbState, A
B0MOV usbState2, A
B0MOV USTATUS, A
; Reset HID change-shadow + idle counter; default to report protocol
B0MOV lastEP1_0, A
B0MOV lastEP1_1, A
B0MOV lastEP1_2, A
B0MOV lastEP1_3, A
B0MOV lastEP1_4, A
B0MOV lastEP1_5, A
B0MOV lastEP1_6, A
B0MOV lastEP1_7, A
B0MOV lastEP1_8, A
B0MOV hidIdleRate, A ; 0 = infinite (send only on change)
B0MOV hidIdleCounter, A
MOV A, #1
B0MOV hidProtocol, A ; report protocol per HID spec default
MOV A, #32
B0MOV EP2FIFO_ADDR, A
MOV A, #0x80
@@ -2051,7 +2181,7 @@ _setup_dispatch_table:
DW 0x210a ; HID SET_IDLE
JMP _usb_htd_hid_set_idle
DW 0x210b ; HID SET_PROTOCOL
JMP _usb_setup_default
JMP _usb_htd_hid_set_protocol
DW 0x8000 ; GET_STATUS
JMP _usb_dth_get_status
DW 0x8006 ; GET_DESCRIPTOR (device)
@@ -2665,6 +2795,26 @@ _usb_htd_set_address:
_usb_htd_hid_set_idle:
MOV A, #'I'
CALL _uart_tx
; HID 1.11 §7.2.4 — duration is wValueHi, in units of 4ms.
; 0 = infinite (only on change). Reset the SOF-driven counter so
; the next forced resend lands ~rate*4ms from now.
B0MOV A, wValueHi
B0MOV hidIdleRate, A
B0MOV hidIdleCounter, A
MOV A, #0x20 ; ACK with no TX
B0MOV UE0R, A
RET
_usb_htd_hid_set_protocol:
MOV A, #'P'
CALL _uart_tx
; HID 1.11 §7.2.5 — wValueLo: 0 = boot, 1 = report.
B0MOV A, wValueLo
B0MOV hidProtocol, A
; Protocol just changed: invalidate shadow so next _kbd_write_ep1
; ships a fresh report under the new wire format.
MOV A, #0xff
B0MOV lastEP1_0, A
MOV A, #0x20 ; ACK with no TX
B0MOV UE0R, A
RET