feat(usb): a USB keyboard -- a full one -- and no hang on a PC without PS/2

boards/BRINGUP.md A3 and S3, FABRIC-3 SXLI.  A HID boot keyboard is
configured (interrupt IN endpoint, SET_PROTOCOL boot) and its reports
become the same Linux-keycode events as PS/2 and virtio (usb_kbd.c): the
full layout -- F-keys, navigation block, arrows, Num Lock, keypad.  The
console treats Caps and Num Lock as toggles shown on the keyboard's
lights, and the keypad as digits or navigation.  While a USB keyboard
runs, its event ring is serviced every tick, not once a second.

Found on the way: with no i8042 (port 0x64 reads 0xFF) the kernel hung at
boot in an endless drain -- a legacy-free PC would stop there.  Detected
and skipped now; drains bounded.

QEMU, amd64 with the USB keyboard the only one: 1 2 + . on main keys and
keypad, Caps Lock, Num Lock all right; aarch64/riscv64 configure it too.
Acceptance on three ISAs unchanged; identities hot-plug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 14:04:33 -04:00
co-authored by Claude Opus 5.5
parent 080b7012ba
commit 739cb9c163
30 changed files with 82072 additions and 14 deletions
+24 -6
View File
@@ -62,13 +62,33 @@ static inline uint8_t inb(uint16_t port) {
return val;
}
/* Is there a controller at all? A PC without one -- "legacy-free", common
* now, the keyboard on USB alone -- answers port 0x64 with 0xFF, every bit
* set, "output full" included, for ever: until 2026-10-14 the drains below
* then never ended, and the kernel hung right here (boards/BRINGUP.md S3,
* found rehearsing A3 with QEMU's i8042=off). */
static int g_i8042_present;
/* Read and drop what the controller holds -- a bounded number of bytes:
* a real controller holds at most a few. */
static void i8042_drain(void) {
int i;
for (i = 0; i < 64 && (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL); i++) {
(void)inb(I8042_DATA_PORT);
}
}
void i8042_init(void) {
if (inb(I8042_STATUS_PORT) == 0xFF) {
console_println("i8042: no PS/2 controller -- keyboard by USB only");
return;
}
g_i8042_present = 1;
/* Drain any stale output byte left by firmware before enabling IRQ1 —
* otherwise the first "interrupt" would really be this leftover byte
* read via polling, not a real IRQ1 delivery. */
while (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) {
(void)inb(I8042_DATA_PORT);
}
i8042_drain();
outb(I8042_CMD_PORT, I8042_CMD_READ_CONFIG);
uint8_t config = inb(I8042_DATA_PORT);
@@ -110,9 +130,7 @@ void i8042_drain_stale(void) {
* edge ever again (the textbook "fires once, then never" pattern).
* i8042_init()'s drain happens at M4; VM bootstrap runs for real wall
* time between then and unmask, so re-drain immediately before. */
while (inb(I8042_STATUS_PORT) & I8042_STATUS_OUTPUT_FULL) {
(void)inb(I8042_DATA_PORT);
}
if (g_i8042_present) i8042_drain();
}
/* Ring buffer: single producer (the ISR), single reader (i8042_pop_scancode(),