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(),
+68 -1
View File
@@ -50,6 +50,7 @@
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
#include "block_subsystem.h"
#include "word_source/include/keyboard_words.h"
#include "starkernel/usb_kbd.h" /* usb_kbd_set_leds() -- the lock lights, A3 */
#include "word_source/include/block_words.h"
#include "word_registry.h"
#include "freestanding/stdio.h"
@@ -906,10 +907,53 @@ static const char sk_kbd_shifted[SK_KBD_TABLE_SIZE] = {
#define SK_KEY_RIGHTSHIFT 54u
#define SK_KEY_LEFTALT 56u
#define SK_KEY_RIGHTALT 100u
#define SK_KEY_CAPSLOCK 58u
#define SK_KEY_NUMLOCK 69u
#define SK_KEY_KPENTER 96u
#define SK_KEY_KPSLASH 98u
#define SK_KEY_102ND 86u
static int g_kbd_shift_down; /* zero-initialized (BSS) */
static int g_kbd_alt_down;
/* A full-size keyboard (Captain Bob, 2026-10-14: "include caps lock,
* numlock, keypad and arrow"). Caps Lock and Num Lock are toggles, held
* here for every keyboard alike and shown on a USB keyboard's lights
* (usb_kbd_set_leds()). Num Lock starts on, as a PC's firmware usually
* leaves it. Caps Lock acts on letters only; Shift reverses it. */
static int g_kbd_caps_lock;
static int g_kbd_num_lock = 1;
static void sk_kbd_show_locks(void)
{
usb_kbd_set_leds((uint8_t)((g_kbd_num_lock ? USB_KBD_LED_NUM : 0) |
(g_kbd_caps_lock ? USB_KBD_LED_CAPS : 0)));
}
/* The keypad: what each key types. With Num Lock on, 0-9 and '.'; with it
* off those are Home/arrows/PgUp/... -- navigation, which this REPL's
* append/backspace editing has no use for, so they type nothing (as the
* arrow keys do). / * - + always type; keypad Enter is Enter. */
static int sk_kbd_keypad(uint16_t keycode)
{
switch (keycode) {
case 55u: return '*';
case 74u: return '-';
case 78u: return '+';
case SK_KEY_KPSLASH: return '/';
case SK_KEY_KPENTER: return '\n';
default: break;
}
if (!g_kbd_num_lock) return 0;
switch (keycode) {
case 71u: return '7'; case 72u: return '8'; case 73u: return '9';
case 75u: return '4'; case 76u: return '5'; case 77u: return '6';
case 79u: return '1'; case 80u: return '2'; case 81u: return '3';
case 82u: return '0'; case 83u: return '.';
default: return 0;
}
}
/* Drains and translates one physically-typed key. Modifier state persists
* across calls (a real keyboard's shift/alt state is global, not
* per-line). Alt+TAB is intercepted here and drives the graphics/text
@@ -932,6 +976,14 @@ static int sk_kbd_getc(void)
}
if (!pressed) continue; /* only act on press/repeat */
if (keycode == SK_KEY_CAPSLOCK) { g_kbd_caps_lock = !g_kbd_caps_lock; sk_kbd_show_locks(); continue; }
if (keycode == SK_KEY_NUMLOCK) { g_kbd_num_lock = !g_kbd_num_lock; sk_kbd_show_locks(); continue; }
{
int k = sk_kbd_keypad(keycode);
if (k) return k;
}
if (keycode == SK_KEY_102ND) return g_kbd_shift_down ? '|' : '\\';
if (keycode == SK_KEY_TAB) {
if (g_kbd_alt_down) console_fb_toggle_graphics();
continue; /* bare TAB: not mapped, same as arrows/F-keys */
@@ -940,7 +992,10 @@ static int sk_kbd_getc(void)
if (keycode == SK_KEY_BACKSPACE) return 0x7F;
if (keycode < SK_KBD_TABLE_SIZE) {
char c = g_kbd_shift_down ? sk_kbd_shifted[keycode] : sk_kbd_unshifted[keycode];
char lower = sk_kbd_unshifted[keycode];
int shift = g_kbd_shift_down;
if (lower >= 'a' && lower <= 'z' && g_kbd_caps_lock) shift = !shift;
char c = shift ? sk_kbd_shifted[keycode] : lower;
if (c) return (unsigned char)c;
}
/* unmapped keycode -- drop and keep draining */
@@ -954,8 +1009,20 @@ static int sk_kbd_getc(void)
* sk_console_key_available() (the standard dictionary's KEY/?TERMINAL, wired
* through shim.c's getchar()) and sk_console_readline() (QUERY/EXPECT, wired
* through shim.c's fgets()) alike. */
static uint64_t g_kbd_poll_tick;
static int sk_console_getc_raw(void)
{
/* A USB keyboard's reports come in on the xHCI event ring: while one is
* running, service the ring once per timer tick here, not only on the
* once-a-second idle beat -- keys arrive at typing speed (A3). */
if (xhci_keyboard_running()) {
uint64_t t = heartbeat_ticks();
if (t != g_kbd_poll_tick) {
g_kbd_poll_tick = t;
xhci_poll_events();
}
}
int c = console_getc();
if (c < 0) c = sk_kbd_getc(); /* FABRIC-0.md 4.4v: second source, same buffer */
return c;
+141
View File
@@ -0,0 +1,141 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2026 Robert A. James. All rights reserved.
Licensed under the StarForth License, Version 1.0.
*/
/**
* usb_kbd.c - a USB boot-protocol keyboard's reports, as key events
* (2026-10-14, boards/BRINGUP.md A3). See usb_kbd.h.
*
* A boot report says which keys are held now, not what changed: what was
* held in the report before and is not now has been released, and the
* other way round has been pressed. The eight modifier bits are keys too
* (left/right Ctrl, Shift, Alt, GUI). A report whose keys are all 0x01 is
* the keyboard's "too many keys" (ErrorRollOver) and changes nothing.
*
* Usage IDs (HID Usage Tables 1.12, page 7) become Linux keycodes
* (input-event-codes.h) -- the same numbers the PS/2 and virtio keyboards
* already give sk_key_event_poll(). Only the keys a console uses are
* mapped; others are dropped. The keyboard does not repeat a held key:
* held is held, and nothing here repeats it either.
*/
#include "starkernel/usb_kbd.h"
/* HID usage ID -> Linux keycode; 0 = not mapped. A full-size keyboard:
* the main block, F1-F12, the navigation block, arrows and the keypad
* (Captain Bob, 2026-10-14: "include caps lock, numlock, keypad and
* arrow"). What a key means -- Caps and Num Lock as toggles, the keypad
* as digits or navigation -- is the console's (repl.c), for every
* keyboard alike. */
static const uint8_t usage_to_key[0x66] = {
[0x04] = 30, [0x05] = 48, [0x06] = 46, [0x07] = 32, [0x08] = 18, /* a b c d e */
[0x09] = 33, [0x0A] = 34, [0x0B] = 35, [0x0C] = 23, [0x0D] = 36, /* f g h i j */
[0x0E] = 37, [0x0F] = 38, [0x10] = 50, [0x11] = 49, [0x12] = 24, /* k l m n o */
[0x13] = 25, [0x14] = 16, [0x15] = 19, [0x16] = 31, [0x17] = 20, /* p q r s t */
[0x18] = 22, [0x19] = 47, [0x1A] = 17, [0x1B] = 45, [0x1C] = 21, /* u v w x y */
[0x1D] = 44, /* z */
[0x1E] = 2, [0x1F] = 3, [0x20] = 4, [0x21] = 5, [0x22] = 6, /* 1 2 3 4 5 */
[0x23] = 7, [0x24] = 8, [0x25] = 9, [0x26] = 10, [0x27] = 11, /* 6 7 8 9 0 */
[0x28] = 28, [0x29] = 1, [0x2A] = 14, [0x2B] = 15, [0x2C] = 57, /* enter esc bksp tab space */
[0x2D] = 12, [0x2E] = 13, [0x2F] = 26, [0x30] = 27, [0x31] = 43, /* - = [ ] \ */
[0x32] = 43, [0x33] = 39, [0x34] = 40, [0x35] = 41, [0x36] = 51, /* non-US # ; ' ` , */
[0x37] = 52, [0x38] = 53, [0x39] = 58, /* . / capslock */
[0x3A] = 59, [0x3B] = 60, [0x3C] = 61, [0x3D] = 62, [0x3E] = 63, /* F1-F5 */
[0x3F] = 64, [0x40] = 65, [0x41] = 66, [0x42] = 67, [0x43] = 68, /* F6-F10 */
[0x44] = 87, [0x45] = 88, /* F11 F12 */
[0x46] = 99, [0x47] = 70, [0x48] = 119, /* printscreen scrolllock pause */
[0x49] = 110, [0x4A] = 102, [0x4B] = 104, /* insert home pageup */
[0x4C] = 111, [0x4D] = 107, [0x4E] = 109, /* delete end pagedown */
[0x4F] = 106, [0x50] = 105, [0x51] = 108, [0x52] = 103, /* right left down up */
[0x53] = 69, /* numlock */
[0x54] = 98, [0x55] = 55, [0x56] = 74, [0x57] = 78, [0x58] = 96, /* kp/ kp* kp- kp+ kpenter */
[0x59] = 79, [0x5A] = 80, [0x5B] = 81, [0x5C] = 75, [0x5D] = 76, /* kp1 kp2 kp3 kp4 kp5 */
[0x5E] = 77, [0x5F] = 71, [0x60] = 72, [0x61] = 73, [0x62] = 82, /* kp6 kp7 kp8 kp9 kp0 */
[0x63] = 83, [0x64] = 86, [0x65] = 127, /* kp. non-US\ compose(menu) */
};
/* Modifier bit n -> Linux keycode: LCtrl LShift LAlt LGUI RCtrl RShift RAlt RGUI. */
static const uint8_t modifier_key[8] = { 29, 42, 56, 125, 97, 54, 100, 126 };
#define KBD_EVENTS 64u
static struct { uint16_t keycode; uint8_t pressed; } g_events[KBD_EVENTS];
static uint32_t g_head, g_count;
static uint8_t g_prev[8];
/* Num Lock on to start, matching the console (repl.c); 0xFF = nothing
* sent yet, so a new keyboard is always told. */
static uint8_t g_leds_want = USB_KBD_LED_NUM;
static uint8_t g_leds_sent = 0xFFu;
static void push(uint16_t keycode, int pressed)
{
if (keycode == 0 || g_count == KBD_EVENTS) return; /* a full queue drops: nothing reads it */
g_events[(g_head + g_count) % KBD_EVENTS].keycode = keycode;
g_events[(g_head + g_count) % KBD_EVENTS].pressed = (uint8_t)(pressed != 0);
g_count++;
}
static uint16_t key_for(uint8_t usage)
{
return usage < sizeof usage_to_key ? usage_to_key[usage] : 0;
}
static int held(const uint8_t *keys, uint32_t n, uint8_t usage)
{
uint32_t i;
for (i = 0; i < n; i++)
if (keys[i] == usage) return 1;
return 0;
}
void usb_kbd_report(const uint8_t *report, uint32_t len)
{
uint8_t now[8] = {0};
uint32_t i, n;
if (!report || len < 3) return;
if (len > 8) len = 8;
for (i = 0; i < len; i++) now[i] = report[i];
if (now[2] == 0x01) return; /* ErrorRollOver: nothing known */
for (i = 0; i < 8; i++) {
uint8_t bit = (uint8_t)(1u << i);
if ((now[0] & bit) != (g_prev[0] & bit)) push(modifier_key[i], (now[0] & bit) != 0);
}
n = 6;
for (i = 2; i < 8; i++) /* released first, then pressed */
if (g_prev[i] > 0x01 && !held(now + 2, n, g_prev[i])) push(key_for(g_prev[i]), 0);
for (i = 2; i < 8; i++)
if (now[i] > 0x01 && !held(g_prev + 2, n, now[i])) push(key_for(now[i]), 1);
for (i = 0; i < 8; i++) g_prev[i] = now[i];
}
void usb_kbd_reset(void)
{
uint32_t i;
for (i = 0; i < 8; i++) g_prev[i] = 0;
g_leds_sent = 0xFFu;
}
void usb_kbd_set_leds(uint8_t leds) { g_leds_want = leds; }
int usb_kbd_leds_pending(uint8_t *leds)
{
if (g_leds_want == g_leds_sent) return 0;
*leds = g_leds_want;
return 1;
}
void usb_kbd_leds_sent(uint8_t leds) { g_leds_sent = leds; }
int usb_kbd_pop_event(uint16_t *keycode, int *pressed)
{
if (g_count == 0) return 0;
*keycode = g_events[g_head].keycode;
*pressed = g_events[g_head].pressed;
g_head = (g_head + 1u) % KBD_EVENTS;
g_count--;
return 1;
}
+274 -3
View File
File diff suppressed because it is too large Load Diff