Artemis Milestone 2d: xHCI Event Ring servicing, polled not interrupt-driven

Implements Event Ring TRB parsing and ERDP dequeue-pointer update
(xhci_poll_events(), src/starkernel/usb/xhci.c), called from
sk_repl_idle()'s existing ~1s idle cadence rather than a per-arch
interrupt handler.

A first attempt wired real interrupt delivery (PCI->IOAPIC GSI routing,
a dedicated isr_stub34/vector 0x22, GIC/PLIC routing mirroring
virtio_input.c). Checked live via QMP query-pci before trusting it: the
amd64 PIRQ swizzle formula predicted GSI 16 for the xHCI controller at
PCI slot 4; the real QEMU-assigned IRQ was 10, and embedded ICH9
functions contradicted the same formula too. Reverted all of it back to
the exact committed baseline rather than chasing chipset PIRQ routing
further, and reframed around Section U item 6's own design intent
("interrupt-driven, coarse cadence, cheap early-exit... quick check
blocks... done") via sk_repl_idle() instead -- USB insertion is a
human-timescale event, not a hot path.

Added -device qemu-xhci to all three QEMU launch targets (required for
any of this to be testable). Verified end to end via genuine post-boot
hotplug (QMP device_add/device_del usb-storage): all three architectures
detect a live attach within seconds. A false-alarm heartbeat "freeze"
found mid-verification traced to querying the wrong counter
(vm->heartbeat.tick_count, which only advances during word execution,
not the kernel's real ISR-driven heartbeat_ticks()) -- confirmed via a
temporary diagnostic word, captured and reverted.

Full writeup, including the discarded interrupt-routing attempt and the
false-alarm investigation, in FABRIC-2.md's Milestone 2c/2d entries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZ8kNoTuP63pbQtro4qvrm
This commit is contained in:
Robert Allan James
2026-08-22 09:25:25 -04:00
co-authored by Claude Sonnet 5
parent c2f1d94c97
commit 2b16daba16
30 changed files with 99620 additions and 26 deletions
+16
View File
@@ -64,6 +64,7 @@ EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
#include "starkernel/pci.h"
#include "starkernel/virtio_blk.h"
#include "starkernel/virtio_input.h"
#include "starkernel/xhci_driver.h"
#include "block_subsystem.h"
#include "vm.h" /* DictEntry, vm_find_word, ACL_MODE_STRICT */
#include "log.h" /* must follow vm.h: vm.h's LOG_LINE_MAX has no
@@ -615,6 +616,21 @@ static void kernel_main_deep(BootInfo *boot_info) {
* sequence parity across all three architectures is unaffected. */
(void)virtio_input_find_keyboard();
/* Artemis Milestone 2b-2c: xHCI controller discovery + bring-up.
* Diagnostic-only wiring for now -- nothing yet consumes a connected
* device (Milestone 2e/2f/2g); this call site exists so the driver's
* two stages actually run and log their own outcome during boot, the
* same graceful-noop precedent virtio_input_find_keyboard() above
* already establishes. Event Ring servicing is polled from
* sk_repl_idle() (Milestone 2d), not driven from here -- see
* xhci_poll_events()'s own doc comment for why this driver is polled
* rather than interrupt-driven. */
{
static xhci_dev_t xhci_dev;
(void)(xhci_find_and_map(&xhci_dev) == 0 &&
xhci_bringup(&xhci_dev) == 0);
}
/* FABRIC.md item 4.4g (decided 2026-08-11): console_fb_init() moved here,
* before capsule_birth_mama(), so the fleet-birth/self-test transcript is
* framebuffer-visible too, not just the small post-birth tail. Costs
+11 -2
View File
@@ -34,6 +34,7 @@
#include "version.h"
#include "starkernel/timer.h"
#include "starkernel/arch.h"
#include "starkernel/xhci_driver.h"
#include "word_source/include/keyboard_words.h"
#include <stdint.h>
#include <string.h>
@@ -76,8 +77,16 @@ static uint64_t g_last_beat_tick; /* zero-initialized (BSS) */
static void sk_repl_idle(void)
{
/* Placeholder — extended by higher-level subsystems as they come online */
(void)0;
/* Artemis Milestone 2d: xHCI Event Ring servicing. This is exactly the
* "interrupt-driven, coarse cadence, cheap early-exit" trigger Section
* U item 6 asked for -- xhci_poll_events() is a no-op read (loop
* condition false immediately) whenever nothing is pending, and this
* hook already runs at a deliberately coarser cadence than the raw
* per-tick ISR (SK_IDLE_BEAT_INTERVAL, ~1s at 100Hz), matching "quick
* check... done... ignore what we can... done." A no-op call if no
* controller was found/brought up (xhci_bringup() never latched a
* device). */
xhci_poll_events();
}
/*===========================================================================
+78 -4
View File
@@ -1,7 +1,8 @@
/*
* xhci.c — xHCI USB host controller driver for StarKernel: discovery and
* register-region mapping (Milestone 2b). Controller bring-up (2c) and
* beyond follow in later increments.
* xhci.c — xHCI USB host controller driver for StarKernel: PCI discovery
* (Milestone 2b), controller bring-up (2c), and polled Event Ring
* servicing (2d). Hotplug/enumeration/BOT read-write (2e-2g) follow in
* later increments.
*
* Memory model: BAR0 is mapped identity (virtual address == physical
* address), matching virtio_blk.c's precedent and pci_map_bar()'s own
@@ -92,6 +93,11 @@ static int xhci_wait_bit(volatile uint32_t *reg, uint32_t mask, int want_set,
}
}
/* Only one controller is supported (matches xhci_dev_t's own doc comment);
* latched at the end of a successful xhci_bringup() for
* xhci_poll_events()'s use. */
static xhci_dev_t *g_xhci_dev = NULL;
int xhci_bringup(xhci_dev_t *dev)
{
if (!dev || !dev->op) return -2;
@@ -195,6 +201,7 @@ int xhci_bringup(xhci_dev_t *dev)
dev->evt_ring[i].control = 0;
}
dev->evt_ring_cycle = 1;
dev->evt_ring_deq = 0;
/* Event Ring Segment Table entry layout: u64 base + u32 size + u32
* reserved = 16 bytes. One segment is enough (ERST Max >= 1 always). */
@@ -207,7 +214,8 @@ int xhci_bringup(xhci_dev_t *dev)
erst[0] = (uint64_t)(uintptr_t)dev->evt_ring; /* base address */
erst[1] = (uint64_t)XHCI_RING_TRB_COUNT; /* size, low 32 bits used */
xhci_intr_regs_t *intr0 = (xhci_intr_regs_t *)((uint8_t *)dev->runtime + 0x20);
dev->intr0 = (xhci_intr_regs_t *)((uint8_t *)dev->runtime + 0x20);
xhci_intr_regs_t *intr0 = dev->intr0;
intr0->erstsz = 1;
intr0->erstba = (uint64_t)(uintptr_t)dev->evt_ring_seg_table;
intr0->erdp = ((uint64_t)(uintptr_t)dev->evt_ring & XHCI_ERDP_PTR_MASK);
@@ -223,5 +231,71 @@ int xhci_bringup(xhci_dev_t *dev)
}
console_println("xhci: controller running");
g_xhci_dev = dev;
return 0;
}
/* -------------------------------------------------------------------------
* Milestone 2d: Event Ring servicing, polled from sk_repl_idle().
*
* Only one controller is supported (matches xhci_dev_t's own doc comment),
* so xhci_poll_events() is a self-contained singleton call, no argument
* needed -- it recovers the device pointer latched by xhci_bringup() above
* rather than taking one. See xhci_driver.h's own doc comment for why this
* is polled rather than interrupt-driven (a real, checked-live finding,
* not a shortcut: the amd64 PCI INTx routing formula tried first turned
* out to be simply wrong).
* ------------------------------------------------------------------------- */
void xhci_poll_events(void)
{
xhci_dev_t *dev = g_xhci_dev;
if (!dev) return;
while (((dev->evt_ring[dev->evt_ring_deq].control & XHCI_TRB_CONTROL_CYCLE) != 0)
== (dev->evt_ring_cycle != 0)) {
xhci_trb_t *trb = &dev->evt_ring[dev->evt_ring_deq];
uint32_t type = XHCI_TRB_TYPE(trb->control);
switch (type) {
case XHCI_TRB_TYPE_PORT_STATUS_CHANGE_EVT:
/* Hotplug trigger -- full port-read/slot-enable handling is
* Milestone 2e's scope, not this one's. Logged so the
* event-ring path is visibly exercised before 2e consumes
* it for real. */
console_println("xhci: port status change event");
break;
case XHCI_TRB_TYPE_COMMAND_COMPLETION_EVT:
/* No commands are issued yet (Milestone 2e is the first
* command-ring user) -- logged for the same reason. */
console_println("xhci: command completion event");
break;
case XHCI_TRB_TYPE_TRANSFER_EVENT:
/* No transfer rings exist yet (Milestone 2g) -- logged. */
console_println("xhci: transfer event");
break;
default:
break;
}
dev->evt_ring_deq++;
if (dev->evt_ring_deq == XHCI_RING_TRB_COUNT) {
dev->evt_ring_deq = 0;
dev->evt_ring_cycle ^= 1u;
}
}
/* Event Ring dequeue-pointer update (xHCI 1.2 spec §4.9.4): write the
* new dequeue pointer back to ERDP with bit3 (EHB, Event Handler Busy,
* RW1C) set -- writing 1 to EHB is what clears it, per spec, not a
* read-modify-write of the current value. Skipping this leaves the
* controller believing the event handler is still busy and it will
* not post further events on this interrupter. IMAN.IP/USBSTS.EINT
* are deliberately not touched here: this driver never sets
* USBCMD.INTE/IMAN.IE (polled, not interrupt-driven -- see this
* function's own doc comment), so those RW1C bits never latch and
* have nothing to clear. */
dev->intr0->erdp = ((uint64_t)(uintptr_t)&dev->evt_ring[dev->evt_ring_deq]
& XHCI_ERDP_PTR_MASK) | XHCI_ERDP_EHB;
}