Files
TermiNexus 859197e5e9 feat(kbd): USB 键盘按住自动重复
USB HID 键盘只在状态变化时上报;a3c1d22 为避免 Enter 洪流加入整份报告去重后,
按住不放不再产生任何字符。

- 记录当前按住的非 CapsLock 键与下一次补发时刻
- 同一份报告重复到达(按键仍按住)时,按 500ms 首次延迟、33ms 间隔
  重新走 _decode_press,注入与按下沿相同的 ASCII/ANSI 字节
- 松开或换键时更新/清除重复目标;CapsLock 本身不重复
2026-10-27 12:43:19 +08:00

241 lines
9.9 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import drivers.usb.usb as usb
import drivers.usb.hid.hid as hid
import drivers.usb.uhci as uhci
import drivers.input.keyboard.keyboard as keyboard
import drivers.serial.uart.serial as serial
import platform.pch.timer as timer
define KBD_BUF_SIZE = 1024
define KBD_EVT_PRESS = 0
define KBD_EVT_RELEASE = 1
# 按住自动重复:首次延迟 500ms,之后每 33ms 一次(约 30 字符/秒)。
# USB 键盘只在状态变化时上报,同一份"按键仍按住"报告重复到达时由 IRQ 回调补发。
define KBD_REPEAT_DELAY_MS = 500
define KBD_REPEAT_INTERVAL_MS = 33
struct usb_kbd_event:
modifier: uint8
keycode: uint8
event_type: uint8
kbd_buf: [KBD_BUF_SIZE]usb_kbd_event
kbd_buf_head: int = 0
kbd_buf_tail: int = 0
kbd_buf_count: int = 0
kbd_dev: usb.usb_device* = None
kbd_report: hid.hid_kbd_report
kbd_prev_keycodes: [6]uint8
# 上一份报告的逻辑状态签名(modifier + 6 个 keycode,各 8 bit):设备对每次中断 IN
# 都会回同一份"当前按键"报告,同一份报告重复到达绝不能当成新的按下沿。
kbd_prev_sig: uint64 = 0
kbd_have_sig: int = 0
# 自动重复状态:当前按住的可重复键 + 下一次补发时刻(timer ticks,1ms/拍)
kbd_rep_code: uint8 = 0
kbd_rep_mod: uint8 = 0
kbd_rep_valid: int = 0
kbd_rep_next: uint64 = 0
kbd_initialized: int = 0
kbd_slot_id: int = -1
kbd_caps: int = 0
kbd_first_key: int = 0
# HID boot 报告 usage ID (0x04..0x38) -> ASCII;CapsLock / 方向键等在 _decode_press 单独处理。
hid_ascii: [64]char = [
'\0', '\0', '\0', '\0',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
'\n', '\x1b', '\b', '\t', ' ',
'-', '=', '[', ']', '\\', '\0', ';', '\'', '`', ',', '.', '/',
'\0', '\0', '\0', '\0', '\0', '\0', '\0'
]
hid_ascii_shift: [64]char = [
'\0', '\0', '\0', '\0',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
'\n', '\x1b', '\b', '\t', ' ',
'_', '+', '{', '}', '|', '\0', ':', '"', '~', '<', '>', '?',
'\0', '\0', '\0', '\0', '\0', '\0', '\0'
]
# 按下沿调用一次:boot 报告 keycode -> ASCII / ANSI,直接进 PS/2 同一输出队列。
def _decode_press(code: uint8, modifier: uint8):
global kbd_caps
if code == uint8(0x39):
kbd_caps = kbd_caps ^ 1
return
if code == uint8(0x52): keyboard.inject_csi(uint8(0x41)) # Up
elif code == uint8(0x51): keyboard.inject_csi(uint8(0x42)) # Down
elif code == uint8(0x4F): keyboard.inject_csi(uint8(0x43)) # Right
elif code == uint8(0x50): keyboard.inject_csi(uint8(0x44)) # Left
elif code == uint8(0x4A): keyboard.inject_csi(uint8(0x48)) # Home
elif code == uint8(0x4D): keyboard.inject_csi(uint8(0x46)) # End
elif code == uint8(0x49): keyboard.inject_csi_tilde(uint8(0x32)) # Insert
elif code == uint8(0x4C): keyboard.inject_csi_tilde(uint8(0x33)) # Delete
elif code == uint8(0x4B): keyboard.inject_csi_tilde(uint8(0x35)) # PageUp
elif code == uint8(0x4E): keyboard.inject_csi_tilde(uint8(0x36)) # PageDown
# usage ID 最大 0xE7;ASCII 表只有 64 项,越界前必须先拒绝(表是只读的,但读越界同样非法)。
if int(code) >= 64: return
shifted: bool = (modifier & uint8(0x22)) != 0
if kbd_caps != 0 and code >= uint8(0x04) and code <= uint8(0x1D):
shifted = not shifted
ch: char = hid_ascii[int(code)]
if shifted:
ch = hid_ascii_shift[int(code)]
if ch != '\0':
keyboard.inject_byte(uint8(ch))
def init() -> int:
global kbd_dev, kbd_initialized, kbd_slot_id
dev: usb.usb_device* = usb.find_hid_device(0x01)
if dev is None:
serial.puts("[usb_kbd] no USB keyboard found\n")
return -1
kbd_dev = dev
if hid.init_keyboard(dev) != 0:
serial.puts("[usb_kbd] init failed\n")
return -2
kbd_initialized = 1
serial.puts("[usb_kbd] initialized\n")
kbd_slot_id = uhci.register_int_callback(_on_int_complete)
if kbd_slot_id < 0:
serial.puts("[usb_kbd] register callback failed\n")
kbd_initialized = 0
return -3
# 中断传输长度不得超过 kbd_report(8 字节),否则 DMA 越界写全局
kbd_pkt_len: uint8 = 8
if dev.ep_in_max < 8: kbd_pkt_len = uint8(dev.ep_in_max)
uhci.schedule_int_transfer(kbd_slot_id, dev.addr, dev.ep_in, dev.ls, kbd_pkt_len, dev.ep_in_toggle, &kbd_report)
return 0
# 只取语义字段(modifier + 6 个 keycode);reserved 变化不代表按键状态变化。
def _report_sig() -> uint64:
sig: uint64 = uint64(kbd_report.modifier)
mul: uint64 = uint64(256)
for i in range(6):
sig = sig | (uint64(kbd_report.keycode[i]) * mul)
mul = mul * uint64(256)
return sig
# 重排下一次中断 IN。**必须**在此之前完成对 kbd_report 的读取:重排后控制器随时会覆盖它。
def _rearm():
dev: usb.usb_device* = kbd_dev
if dev is None or kbd_slot_id < 0: return
kbd_pkt_len2: uint8 = 8
if dev.ep_in_max < 8: kbd_pkt_len2 = uint8(dev.ep_in_max)
# 推进 DATA0/DATA1 翻转位:本回调表示上一次中断 IN 事务已结束(IRQ 中 TD 不再
# ACTIVE),翻转后本次重排使用另一 PID。全仓仅此处推进 ep_in_toggle。
dev.ep_in_toggle = uint8(dev.ep_in_toggle ^ 1)
uhci.schedule_int_transfer(kbd_slot_id, dev.addr, dev.ep_in, dev.ls, kbd_pkt_len2, dev.ep_in_toggle, &kbd_report)
def _on_int_complete() -> int:
global kbd_buf_head, kbd_buf_tail, kbd_buf_count, kbd_prev_keycodes, kbd_prev_sig, kbd_have_sig, kbd_first_key
global kbd_rep_code, kbd_rep_mod, kbd_rep_valid, kbd_rep_next
if kbd_initialized == 0: return 0
sig: uint64 = _report_sig()
# 同一份报告再次到达(按键仍按住时设备每帧回同一份 / TD 重完成):没有新沿,
# 但正是自动重复的触发点 —— 按 delay/interval 补发当前按住键的字节。
if kbd_have_sig != 0 and sig == kbd_prev_sig:
if kbd_rep_valid != 0:
now: uint64 = timer.timer_get_ticks()
if now >= kbd_rep_next:
kbd_rep_next = now + uint64(KBD_REPEAT_INTERVAL_MS)
_decode_press(kbd_rep_code, kbd_rep_mod)
_rearm()
return 0
# 先把本份报告提交为"上一次状态",再做判定:无论后面发生什么,下一帧都不会重复按下沿。
old: [6]uint8
for i in range(6):
old[i] = kbd_prev_keycodes[i]
kbd_prev_keycodes[i] = kbd_report.keycode[i]
kbd_prev_sig = sig
kbd_have_sig = 1
# 全 0 报告(无按键)只更新状态,绝不能注入任何字节。
if sig == 0:
kbd_rep_valid = 0
_rearm()
return 0
j: int
new_press: uint8 = uint8(0)
for i in range(6):
code: uint8 = kbd_report.keycode[i]
if code == 0: continue
found: int = 0
for j in range(6):
if code == old[j]:
found = 1
break
if found == 0:
if kbd_first_key == 0:
kbd_first_key = 1
serial.puts("[usb_kbd] first key\n")
# 只在按下沿解码,避免按住时重复入队。
_decode_press(code, kbd_report.modifier)
if new_press == 0 and code != uint8(0x39):
new_press = code
if kbd_buf_count < KBD_BUF_SIZE:
kbd_buf[kbd_buf_tail].modifier = kbd_report.modifier
kbd_buf[kbd_buf_tail].keycode = code
kbd_buf[kbd_buf_tail].event_type = KBD_EVT_PRESS
kbd_buf_tail = kbd_buf_tail + 1
if kbd_buf_tail >= KBD_BUF_SIZE:
kbd_buf_tail = 0
kbd_buf_count += 1
for i in range(6):
prev: uint8 = old[i]
if prev == 0: continue
found2: int = 0
for j in range(6):
if prev == kbd_report.keycode[j]:
found2 = 1
break
if found2 == 0:
if kbd_buf_count < KBD_BUF_SIZE:
kbd_buf[kbd_buf_tail].modifier = kbd_report.modifier
kbd_buf[kbd_buf_tail].keycode = prev
kbd_buf[kbd_buf_tail].event_type = KBD_EVT_RELEASE
kbd_buf_tail = kbd_buf_tail + 1
if kbd_buf_tail >= KBD_BUF_SIZE:
kbd_buf_tail = 0
kbd_buf_count += 1
# 更新自动重复目标:优先刚按下的键;否则保持任一仍按住的键(CapsLock 不重复)。
rep: uint8 = new_press
if rep == 0:
for i in range(6):
c: uint8 = kbd_report.keycode[i]
if c != 0 and c != uint8(0x39):
rep = c
break
if rep != 0:
kbd_rep_code = rep
kbd_rep_mod = kbd_report.modifier
kbd_rep_valid = 1
kbd_rep_next = timer.timer_get_ticks() + uint64(KBD_REPEAT_DELAY_MS)
else:
kbd_rep_valid = 0
_rearm()
return 0
def has_data() -> int:
if kbd_buf_count > 0: return 1
return 0
def poll() -> int:
if kbd_initialized == 0: return 0
if kbd_buf_count > 0: return 1
return 0
def read_event(out: usb_kbd_event*) -> int:
global kbd_buf_head, kbd_buf_count
if kbd_buf_count == 0: return -1
out.modifier = kbd_buf[kbd_buf_head].modifier
out.keycode = kbd_buf[kbd_buf_head].keycode
out.event_type = kbd_buf[kbd_buf_head].event_type
kbd_buf_head = kbd_buf_head + 1
if kbd_buf_head >= KBD_BUF_SIZE:
kbd_buf_head = 0
kbd_buf_count -= 1
return 0