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
+274 -3
View File
@@ -22,6 +22,8 @@
#include "starkernel/pci.h"
#include "starkernel/xhci.h"
#include "starkernel/xhci_driver.h"
#include "starkernel/usb_kbd.h" /* A3: a boot keyboard's reports and lights */
#include "starkernel/console.h" /* A3: "USB keyboard ready" */
#include "starkernel/kmalloc.h"
#include "starkernel/timer.h"
#include "starkernel/arch.h"
@@ -562,6 +564,9 @@ int xhci_cmd_address_device(xhci_dev_t *dev, uint32_t slot_id,
if (!ms->ep0_ring) return -1;
}
ms->in_use = 1;
ms->hid_kbd = 0; /* a new device in this slot: not (yet) a keyboard */
ms->hid_running = 0;
ms->hid_ep_addr = 0;
/* EP0 Transfer Ring: same fixed-ring-plus-Link-TRB pattern as the
* Command Ring (xhci_bringup()'s own comment on why). Freshly
@@ -751,6 +756,86 @@ int xhci_cmd_configure_endpoint(xhci_dev_t *dev, uint32_t slot_id)
return 0;
}
/* A boot keyboard's Configure Endpoint (2026-10-14, boards/BRINGUP.md A3):
* its one interrupt IN endpoint, in the same shape as the bulk pair above
* -- Device Context grown and EP0 kept, Input Context spanning every DCI
* up to the new one, Slot Context copied with Context Entries raised. The
* interval is the endpoint's bInterval in xHCI's terms (section 6.2.3.6):
* frames (ms) for full and low speed, given as the exponent of a period
* of 2^n x 125 us; already an exponent minus one at high speed and up. */
int xhci_cmd_configure_hid_endpoint(xhci_dev_t *dev, uint32_t slot_id)
{
xhci_msc_slot_t *ms = xhci_msc_slot_for(dev, slot_id);
if (!dev || !dev->cmd_ring || !ms || !ms->input_ctx || !ms->device_ctx) return -1;
if (ms->hid_ep_addr == 0) return -1;
const size_t cs = xhci_ctx_bytes(dev);
if (!ms->hid_ring) {
ms->hid_ring = (xhci_trb_t *)kmalloc_aligned(XHCI_RING_BYTES, 64);
if (!ms->hid_ring) return -1;
}
if (!ms->hid_buf) {
ms->hid_buf = (uint8_t *)kmalloc_aligned(128, 64); /* report [0,64), lights at 64 */
if (!ms->hid_buf) return -1;
}
xhci_init_bulk_ring(ms->hid_ring);
ms->hid_ring_cycle = 1;
ms->hid_ring_enq = 0;
uint32_t dci = XHCI_EP_ADDR_TO_DCI(ms->hid_ep_addr);
size_t old_bytes = 2 * cs;
size_t new_bytes = (1 + (size_t)dci) * cs;
uint8_t *ndctx = (uint8_t *)kmalloc_aligned(new_bytes, 64);
if (!ndctx) return -1;
for (size_t i = 0; i < new_bytes; i++) ndctx[i] = 0;
for (size_t i = 0; i < old_bytes; i++) ndctx[i] = ((const uint8_t *)ms->device_ctx)[i];
ms->device_ctx = ndctx;
((uint64_t *)dev->dcbaa)[slot_id] = (uint64_t)(uintptr_t)ms->device_ctx;
size_t total = cs + (1 + (size_t)dci) * cs;
uint8_t *ictx = (uint8_t *)kmalloc_aligned(total, 64);
if (!ictx) return -1;
for (size_t i = 0; i < total; i++) ictx[i] = 0;
ms->input_ctx = ictx;
xhci_input_ctrl_ctx32_t *ctrl = (xhci_input_ctrl_ctx32_t *)ictx;
ctrl->add_flags = XHCI_INPUT_CTRL_ADD_SLOT | (1u << dci);
xhci_slot_ctx32_t *dev_slot = (xhci_slot_ctx32_t *)ms->device_ctx;
xhci_slot_ctx32_t *in_slot = (xhci_slot_ctx32_t *)(ictx + cs);
uint32_t entries_mask = ~((uint32_t)0x1Fu << XHCI_SLOT_CTX_CONTEXT_ENTRIES_SHIFT);
in_slot->dword0 = (dev_slot->dword0 & entries_mask) | (dci << XHCI_SLOT_CTX_CONTEXT_ENTRIES_SHIFT);
in_slot->dword1 = dev_slot->dword1;
in_slot->dword2 = dev_slot->dword2;
uint32_t speed = (dev_slot->dword0 >> XHCI_SLOT_CTX_SPEED_SHIFT) & 0xFu;
uint32_t interval;
if (speed >= 3) { /* high, super */
interval = ms->hid_interval ? (uint32_t)ms->hid_interval - 1u : 0u;
if (interval > 15) interval = 15;
} else { /* full, low: frames */
uint32_t v = (ms->hid_interval ? ms->hid_interval : 1u) * 8u;
interval = 0;
while ((1u << (interval + 1)) <= v) interval++;
if (interval < 3) interval = 3;
if (interval > 10) interval = 10;
}
xhci_ep_ctx32_t *ep = (xhci_ep_ctx32_t *)(ictx + cs + (size_t)dci * cs);
ep->dword0 = interval << XHCI_EP_CTX_INTERVAL_SHIFT;
ep->dword1 = (3u << XHCI_EP_CTX_CERR_SHIFT) |
(XHCI_EP_CTX_TYPE_INTERRUPT_IN << XHCI_EP_CTX_TYPE_SHIFT) |
((uint32_t)ms->hid_max_packet << XHCI_EP_CTX_MAX_PACKET_SHIFT);
ep->tr_dequeue_ptr = ((uint64_t)(uintptr_t)ms->hid_ring) | 1u; /* DCS = 1 */
ep->dword4 = 8u | ((uint32_t)ms->hid_max_packet << 16); /* avg TRB length, max ESIT payload */
xhci_submit_command(dev, (uint64_t)(uintptr_t)ms->input_ctx, 0,
XHCI_TRB_TYPE_CONFIGURE_ENDPOINT_CMD, slot_id << 24);
log_message(LOG_DEBUG, "xhci: keyboard configure endpoint command submitted");
return 0;
}
/* G.1 / §F.14 stall recovery: Reset Endpoint command. ep_id is a full
* bEndpointAddress (bit 7 = direction) of the stalled bulk endpoint -- the
* xHC is told which endpoint to un-halt via its Endpoint ID (the DCI) in
@@ -1362,6 +1447,96 @@ int xhci_ep0_bot_mass_storage_reset(xhci_dev_t *dev, uint32_t slot_id)
return 0;
}
/* A boot keyboard's class requests (HID 1.11 section 7.2), on EP0, to its
* interface: SET_PROTOCOL(boot) -- a keyboard starts in report protocol,
* whose reports need not be the 8-byte boot ones -- and SET_REPORT for the
* lock lights (one output byte). (2026-10-14, boards/BRINGUP.md A3.) */
int xhci_ep0_hid_set_protocol(xhci_dev_t *dev, uint32_t slot_id)
{
xhci_msc_slot_t *ms = xhci_msc_slot_for(dev, slot_id);
if (!dev || !ms || !ms->ep0_ring) return -1;
usb_setup_packet_t setup = {
.bmRequestType = (USB_REQ_TYPE_CLASS << 5) | USB_RECIP_INTERFACE,
.bRequest = USB_HID_REQ_SET_PROTOCOL,
.wValue = USB_HID_PROTOCOL_BOOT,
.wIndex = ms->hid_iface,
.wLength = 0
};
uint64_t setup_bits;
memcpy(&setup_bits, &setup, sizeof(setup_bits));
xhci_ep0_enqueue_trb(dev, slot_id, setup_bits, 8u,
(XHCI_TRB_TYPE_SETUP_STAGE << XHCI_TRB_CONTROL_TYPE_SHIFT) |
XHCI_TRB_CONTROL_IDT |
(XHCI_SETUP_TRT_NO_DATA << XHCI_TRB_CONTROL_TRT_SHIFT));
xhci_ep0_enqueue_trb(dev, slot_id, 0, 0,
(XHCI_TRB_TYPE_STATUS_STAGE << XHCI_TRB_CONTROL_TYPE_SHIFT) |
XHCI_TRB_CONTROL_IOC |
XHCI_TRB_CONTROL_DIR_IN);
ms->transfer_purpose = XHCI_XFER_HID_SET_PROTOCOL;
dev->doorbell[slot_id] = XHCI_DB_TARGET(1);
log_message(LOG_DEBUG, "xhci: keyboard set protocol (boot) submitted");
return 0;
}
#define USB_HID_REQ_SET_REPORT 0x09u
#define USB_HID_REPORT_OUTPUT 0x0200u /* wValue: report type Output (2), report ID 0 */
static int xhci_ep0_hid_set_leds(xhci_dev_t *dev, uint32_t slot_id, uint8_t leds)
{
xhci_msc_slot_t *ms = xhci_msc_slot_for(dev, slot_id);
if (!dev || !ms || !ms->ep0_ring || !ms->hid_buf) return -1;
ms->hid_buf[64] = leds;
usb_setup_packet_t setup = {
.bmRequestType = (USB_REQ_TYPE_CLASS << 5) | USB_RECIP_INTERFACE,
.bRequest = USB_HID_REQ_SET_REPORT,
.wValue = USB_HID_REPORT_OUTPUT,
.wIndex = ms->hid_iface,
.wLength = 1
};
uint64_t setup_bits;
memcpy(&setup_bits, &setup, sizeof(setup_bits));
xhci_ep0_enqueue_trb(dev, slot_id, setup_bits, 8u,
(XHCI_TRB_TYPE_SETUP_STAGE << XHCI_TRB_CONTROL_TYPE_SHIFT) |
XHCI_TRB_CONTROL_IDT |
(XHCI_SETUP_TRT_OUT_DATA << XHCI_TRB_CONTROL_TRT_SHIFT));
xhci_ep0_enqueue_trb(dev, slot_id, (uint64_t)(uintptr_t)&ms->hid_buf[64], 1u,
(XHCI_TRB_TYPE_DATA_STAGE << XHCI_TRB_CONTROL_TYPE_SHIFT));
xhci_ep0_enqueue_trb(dev, slot_id, 0, 0,
(XHCI_TRB_TYPE_STATUS_STAGE << XHCI_TRB_CONTROL_TYPE_SHIFT) |
XHCI_TRB_CONTROL_IOC |
XHCI_TRB_CONTROL_DIR_IN);
ms->transfer_purpose = XHCI_XFER_HID_SET_LEDS;
usb_kbd_leds_sent(leds); /* once asked: a keyboard that refuses is not asked again and again */
dev->doorbell[slot_id] = XHCI_DB_TARGET(1);
return 0;
}
/* One Normal TRB on the keyboard's interrupt ring: the next report lands in
* hid_buf. One at a time -- queued again only once the last has been read. */
static void xhci_hid_queue_report(xhci_dev_t *dev, uint32_t slot_id, xhci_msc_slot_t *ms)
{
uint32_t len = ms->hid_max_packet ? ms->hid_max_packet : 8u;
if (len > 64u) len = 64u;
xhci_trb_t *trb = &ms->hid_ring[ms->hid_ring_enq];
trb->parameter = (uint64_t)(uintptr_t)ms->hid_buf;
trb->status = len;
trb->control = (XHCI_TRB_TYPE_NORMAL << XHCI_TRB_CONTROL_TYPE_SHIFT) | XHCI_TRB_CONTROL_IOC |
(ms->hid_ring_cycle ? XHCI_TRB_CONTROL_CYCLE : 0);
ms->hid_ring_enq++;
if (ms->hid_ring_enq == XHCI_RING_TRB_COUNT - 1) {
ms->hid_ring[XHCI_RING_TRB_COUNT - 1].control =
(XHCI_TRB_TYPE_LINK << XHCI_TRB_CONTROL_TYPE_SHIFT) | XHCI_TRB_CONTROL_TC |
(ms->hid_ring_cycle ? XHCI_TRB_CONTROL_CYCLE : 0);
ms->hid_ring_enq = 0;
ms->hid_ring_cycle ^= 1u;
}
dev->doorbell[slot_id] = XHCI_DB_TARGET(XHCI_EP_ADDR_TO_DCI(ms->hid_ep_addr));
}
/* -------------------------------------------------------------------------
* Milestone 2d: Event Ring servicing, polled from sk_repl_idle().
*
@@ -1676,6 +1851,14 @@ static void xhci_handle_bulk_stall(xhci_dev_t *dev, uint32_t slot_id,
}
}
/* How many boot keyboards are running (A3). While any is, the console
* services this event ring on every timer tick as it waits for a key
* (repl.c), not only on its once-a-second idle beat: one report at a time
* is queued, so at one a second fast typing overflowed the keyboard's own
* short queue and keys were lost (2026-10-14). */
static uint32_t g_xhci_keyboards;
int xhci_keyboard_running(void) { return g_xhci_keyboards != 0; }
void xhci_poll_events(void)
{
xhci_dev_t *dev = g_xhci_dev;
@@ -1820,7 +2003,13 @@ void xhci_poll_events(void)
* single-device fields), only in_use clears. */
{
xhci_msc_slot_t *dms2 = xhci_msc_slot_for(dev, dev->pending_disable_slot_id);
if (dms2) dms2->in_use = 0;
if (dms2) {
if (dms2->hid_kbd) usb_kbd_reset(); /* A3: no key stays held */
if (dms2->hid_running && g_xhci_keyboards) g_xhci_keyboards--;
dms2->hid_kbd = 0;
dms2->hid_running = 0;
dms2->in_use = 0;
}
}
log_message(LOG_DEBUG, "xhci: disable slot succeeded");
} else {
@@ -1936,6 +2125,24 @@ void xhci_poll_events(void)
* transfer is independent. */
uint32_t xfer_slot_id = XHCI_EVT_SLOT_ID(trb->control);
xhci_msc_slot_t *xms = xhci_msc_slot_for(dev, xfer_slot_id);
/* A boot keyboard's report, on its interrupt endpoint (A3):
* read it, and queue for the next -- deferred, like every
* doorbell here, until ERDP is updated below. */
if (xms && xms->hid_running && xms->hid_ep_addr &&
XHCI_EVT_EP_ID(trb->control) == XHCI_EP_ADDR_TO_DCI(xms->hid_ep_addr)) {
if (code == XHCI_COMPLETION_CODE_SUCCESS || code == XHCI_COMPLETION_CODE_SHORT_PACKET) {
uint32_t asked = xms->hid_max_packet ? xms->hid_max_packet : 8u;
uint32_t left = trb->status & 0xFFFFFFu;
if (asked > 64u) asked = 64u;
usb_kbd_report(xms->hid_buf, left < asked ? asked - left : 0u);
xms->next_action = XHCI_NEXT_ACTION_HID_POLL;
} else {
console_println("USB keyboard: report failed -- keyboard stopped");
xms->hid_running = 0;
if (g_xhci_keyboards) g_xhci_keyboards--;
}
break;
}
if (xms && xms->transfer_purpose != XHCI_XFER_NONE) {
uint32_t purpose = xms->transfer_purpose;
xms->transfer_purpose = XHCI_XFER_NONE;
@@ -2085,8 +2292,44 @@ void xhci_poll_events(void)
} else {
xms->next_action = XHCI_NEXT_ACTION_CONFIGURE_ENDPOINT;
}
} else if (iface_class == USB_CLASS_HID &&
iface_subclass == USB_SUBCLASS_HID_BOOT &&
iface_protocol == USB_PROTOCOL_HID_KEYBOARD) {
/* A boot keyboard (2026-10-14, A3): its
* interface number, and the first
* interrupt IN endpoint after it. */
uint16_t ep_off = (uint16_t)(off + desc_len);
xms->hid_iface = xms->config_descriptor[off + USB_IFACE_OFF_NUMBER];
while (ep_off + 2 <= len) {
uint8_t ep_desc_len = xms->config_descriptor[ep_off + USB_DESC_OFF_LENGTH];
uint8_t ep_desc_type = xms->config_descriptor[ep_off + USB_DESC_OFF_TYPE];
if (ep_desc_len == 0) break;
if (ep_desc_type == USB_DESC_TYPE_INTERFACE) break;
if (ep_desc_type == USB_DESC_TYPE_ENDPOINT &&
ep_off + USB_EP_OFF_INTERVAL < len) {
uint8_t ep_addr = xms->config_descriptor[ep_off + USB_EP_OFF_ADDRESS];
uint8_t ep_attr = xms->config_descriptor[ep_off + USB_EP_OFF_ATTRIBUTES];
if ((ep_attr & USB_EP_ATTR_TYPE_MASK) == USB_EP_TYPE_INTERRUPT &&
(ep_addr & USB_EP_ADDR_DIR_MASK)) {
xms->hid_ep_addr = ep_addr;
xms->hid_max_packet = (uint16_t)(xms->config_descriptor[ep_off + USB_EP_OFF_MAX_PACKET_SIZE] |
((uint16_t)xms->config_descriptor[ep_off + USB_EP_OFF_MAX_PACKET_SIZE + 1] << 8)) & 0x7FFu;
xms->hid_interval = xms->config_descriptor[ep_off + USB_EP_OFF_INTERVAL];
break;
}
}
ep_off = (uint16_t)(ep_off + ep_desc_len);
}
xms->next_action_config_value =
xms->config_descriptor[USB_CONFIG_OFF_CONFIG_VALUE];
if (xms->hid_ep_addr) {
xms->hid_kbd = 1;
xms->next_action = XHCI_NEXT_ACTION_CONFIGURE_ENDPOINT;
} else {
log_message(LOG_WARN, "xhci: keyboard without an interrupt IN endpoint -- not used");
}
} else {
log_message(LOG_WARN, "xhci: not a Mass Storage/SCSI/BOT device -- not usable as a drive");
log_message(LOG_WARN, "xhci: neither a Mass Storage/SCSI/BOT drive nor a boot keyboard -- not used");
}
found = 1;
break;
@@ -2100,6 +2343,10 @@ void xhci_poll_events(void)
}
case XHCI_XFER_SET_CONFIG: {
log_message(LOG_DEBUG, "xhci: device configured");
if (xms->hid_kbd) { /* A3: a keyboard, not a drive */
xms->next_action = XHCI_NEXT_ACTION_HID_SET_PROTOCOL;
break;
}
/* Milestone 2h: hand off to sk_repl_idle(),
* the only safe place to run the synchronous
* capacity query + block-subsystem attach --
@@ -2111,6 +2358,16 @@ void xhci_poll_events(void)
xms->bot_msc_attach_pending = 1;
break;
}
case XHCI_XFER_HID_SET_PROTOCOL: {
xms->hid_running = 1;
g_xhci_keyboards++;
usb_kbd_reset();
console_println("USB keyboard ready");
xms->next_action = XHCI_NEXT_ACTION_HID_POLL;
break;
}
case XHCI_XFER_HID_SET_LEDS:
break;
case XHCI_XFER_CBW_SENT: {
log_message(LOG_DEBUG, "xhci: CBW send completed");
/* Deferred (see xhci_dev_t's next_action doc
@@ -2397,11 +2654,25 @@ void xhci_poll_events(void)
break;
}
dev->connect_state = XHCI_CONN_AWAIT_CONFIGURE_ENDPOINT;
if (xhci_cmd_configure_endpoint(dev, next_slot_id) != 0) {
if ((nms->hid_kbd ? xhci_cmd_configure_hid_endpoint(dev, next_slot_id)
: xhci_cmd_configure_endpoint(dev, next_slot_id)) != 0) {
log_message(LOG_ERROR, "xhci: deferred configure endpoint request setup failed");
dev->connect_state = XHCI_CONN_IDLE;
}
break;
case XHCI_NEXT_ACTION_HID_SET_PROTOCOL:
if (xhci_ep0_hid_set_protocol(dev, next_slot_id) != 0) {
log_message(LOG_ERROR, "xhci: deferred keyboard set protocol setup failed");
}
break;
case XHCI_NEXT_ACTION_HID_POLL: {
uint8_t leds;
if (!nms->hid_running) break;
xhci_hid_queue_report(dev, next_slot_id, nms);
if (nms->transfer_purpose == XHCI_XFER_NONE && usb_kbd_leds_pending(&leds))
(void)xhci_ep0_hid_set_leds(dev, next_slot_id, leds);
break;
}
case XHCI_NEXT_ACTION_SET_CONFIG:
if (xhci_ep0_set_configuration(dev, next_slot_id, nms->next_action_config_value) != 0) {
log_message(LOG_ERROR, "xhci: deferred set configuration request setup failed");