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
+85 -17
View File
@@ -3116,27 +3116,94 @@ kernel compilation passes), and a live amd64 boot still reaches POST 1012/0/0 an
unaffected (expected — nothing calls `xhci_find_and_map()` yet, so this is purely additive
until 2c wires it in).
**2c. Controller bring-up**
- [ ] Read Capability Registers to learn controller parameters (max device slots, max ports,
**2c. Controller bring-up ✅ DONE 2026-08-22**
- [x] Read Capability Registers to learn controller parameters (max device slots, max ports,
max interrupters — needed to size later allocations)
- [ ] Perform xHCI controller reset sequence
- [ ] Allocate and program the Device Context Base Address Array (DCBAA)
- [ ] Allocate and program the Command Ring, write its base address to the Operational
- [x] Perform xHCI controller reset sequence
- [x] Allocate and program the Device Context Base Address Array (DCBAA)
- [x] Allocate and program the Command Ring, write its base address to the Operational
Register `CRCR`
- [ ] Allocate and program at least one Event Ring (segment table + ring buffer), wire it to
- [x] Allocate and program at least one Event Ring (segment table + ring buffer), wire it to
Interrupter 0
- [ ] Set the `RUN/STOP` bit to start the controller
- [ ] Confirm controller reaches a running state (poll a status register, don't assume)
- [x] Set the `RUN/STOP` bit to start the controller
- [x] Confirm controller reaches a running state (poll a status register, don't assume)
**2d. Interrupt/event handling**
- [ ] Wire an interrupt handler for the xHCI controller's IRQ line (via existing per-arch
interrupt infrastructure, `starkernel/arch/*/interrupts.c` — same place the timer ISR
already hooks in, per the heartbeat work this session)
- [ ] Implement Event Ring TRB (Transfer Request Block) parsing — at minimum, Port Status
Change events (hotplug) and Command Completion events, to start
- [ ] Implement Event Ring dequeue-pointer update / interrupt-clear sequence so the
`xhci_bringup()` (`src/starkernel/usb/xhci.c`) was already written, uncommitted, at the start of
this session — but referenced an `XHCI_WAIT_FOR` macro that was never defined anywhere in the
tree, breaking the build (`implicit declaration of function`, plus the `xhci_wait_bit()` helper
meant to back it sitting unused under `-Werror=unused-function`). Fixed by wiring all four wait
sites directly to `xhci_wait_bit(reg, mask, want_set, max_ticks)` with the correct
register/bit/polarity per site, rather than inventing the missing macro. Verified via
`make -f Makefile.starkernel ARCH=amd64` compiling clean end to end. Committed `c2f1d94` (same
commit also flipped `g_doe_log_enabled`'s default off — the per-tick `[HADES][DOE]` export was
flooding every acceptance boot log for no reason during ordinary verification; `HB-ON` still
re-enables it for a real DoE campaign).
**2d. Interrupt/event handling ✅ DONE 2026-08-22 — polled, not interrupt-driven**
- [x] ~~Wire an interrupt handler for the xHCI controller's IRQ line~~ — attempted, reverted;
see below for why this became a poll instead
- [x] Implement Event Ring TRB (Transfer Request Block) parsing — Port Status Change and
Command Completion events logged (full handling is 2e/2g's scope), Transfer events logged
too (2g's scope)
- [x] Implement Event Ring dequeue-pointer update / interrupt-clear sequence so the
controller keeps delivering new events
**First attempt (interrupt-driven) built, then reverted after a live check proved the amd64
routing formula wrong.** Wired PCI→IOAPIC GSI routing (new `ioapic_route_pci_irq()`), a
dedicated `isr_stub34`/vector `0x22` (mirroring `isr_stub33`'s own fix for the identical
misrouted-to-spurious bug), and arch-guarded routing mirroring `virtio_input.c`'s riscv64/PLIC
and aarch64/GIC pattern. Before trusting any of it, checked live via QMP `query-pci`: the
formula used for amd64 (`GSI = 16 + ((slot + pin - 1) % 4)`, the commonly-cited Q35/ICH9
default) predicted GSI 16 for the xHCI controller at PCI slot 4; the real, QEMU-reported IRQ
was **10**. Embedded ICH9 functions (SATA/SMBus at slot 31) contradicted the same formula too,
ruling out a simple one-line fix — deriving the real PIRQ routing would need reading the ICH9
LPC's actual PIRQ control registers (config offset 0x60+), a genuinely deep chipset detour nobody
asked for.
**Reframed instead of chasing the formula further.** Section U item 6 (Captain Bob's own words)
specifies the trigger as *"interrupt-driven, coarse cadence, cheap early-exit… quick check
blocks… done… ignore what we can"* and names `sk_repl_idle()` — confirmed empty and ready
(Section V) — as its home. USB insertion is a human-timescale event, not a hot path; polling
costs nothing meaningful here. Reverted all interrupt-routing code (the IOAPIC PCI-routing
functions, `isr_stub34`, the three `interrupts.c` dispatch hooks) back to exactly the committed
baseline (`git diff` against those files is empty), keeping only the already-correct Event Ring
TRB-parsing body — renamed `xhci_isr()``xhci_poll_events()`, called from `sk_repl_idle()` at
its existing ~1s cadence (`SK_IDLE_BEAT_INTERVAL`). `xhci_bringup()` never enables
`USBCMD.INTE`/`IMAN.IE`, so the IMAN/USBSTS RW1C ack-writes are correctly absent from the poll
path — nothing ever latches them.
**A real ack-sequence bug caught before it shipped, in the discarded interrupt path.**
`xhci_isr()`'s first draft never cleared `IMAN.IP`/`USBSTS.EINT` — with the line programmed
level-triggered, the controller would have re-asserted immediately on servicing, a wedge (the
same class of bug `virtio_input.c`'s own header comment already documents: "skipping this leaves
the condition latched — storm or hang, not a subtle bug"). Moot once the interrupt path was
dropped, but the discipline that caught it (advisor review before committing) is worth recording.
**A real, reproducible false alarm caught and corrected during verification — not folded into
this item's own scope.** `HEARTBEAT-TICKS@` was queried at the idle `ok>` prompt on
aarch64/riscv64 to check whether the poll fires at all, showed a static, byte-identical value
(77) across independent fresh boots, and was initially treated as a stalled hardware timer — a
serious-looking regression. Root cause: `HEARTBEAT-TICKS@` reads `vm->heartbeat.tick_count`, a
**per-VM software counter** (Loop #7's own model, incremented only during FORTH word execution)
— not the kernel's real ISR-driven `heartbeat_ticks()`. It correctly stops advancing once the
REPL idles and nothing executes; this is expected behavior, not a bug. Confirmed directly with a
temporary diagnostic word (`KHB-TICKS@`, added, used, reverted — probe-capture-revert
convention) exposing the real kernel counter: it advanced 3830→7099 in ~20s of idle on riscv64,
proving the ISR and `sk_repl_idle()`'s gate are both healthy. The actual reason no Port Status
Change event had appeared in the original aarch64/riscv64 tests was simpler: the USB device had
been attached *before* boot in those runs, so the controller's reset during bring-up produced no
fresh state-change transition to detect — not a timer problem at all. A genuine post-boot hotplug
(attach after reaching `ok>`) produced an immediate, clean event on both.
**Verified end to end, all three architectures, genuine post-boot hotplug via QMP
`device_add usb-storage`/`device_del`:** amd64, aarch64, and riscv64 all print
`xhci: port status change event` within seconds of a live attach, and amd64 additionally
confirmed the reverse (detach) and a second attach both individually detected. `-device
qemu-xhci,id=xhci0[,addr=0x4]` added to all three QEMU launch targets (`Makefile.starkernel`) —
required for anything past this point to be testable at all, including future milestones.
Final three-arch acceptance (probe-free, `clean qemu`): all three boot to `ok>` cleanly,
`logs/20260822-091923/amd64/`, `logs/20260822-092035/aarch64/`, `logs/20260822-092229/riscv64/`.
**2e. Hotplug detection (the actual trigger for everything in Section U)**
- [ ] On a Port Status Change event, read the corresponding Port Register to determine
connect vs. disconnect
@@ -3290,8 +3357,9 @@ for whenever this resumes: UEFI-only boot path, no legacy BIOS/MBR support, no G
other bootloader in the chain** — `starkernel_loader.efi` is meant to be the entire boot path,
generic and minimal, not one stage in a longer one.
- [ ] Confirm which physical machine will receive the first real-hardware boot test (CPU
arch — amd64 assumed given the SanDisk drives on hand, but confirm)
- [x] Confirm which physical machine will receive the first real-hardware boot test (CPU
arch — amd64 assumed given the SanDisk drives on hand, but confirm)**Beelink SER5**,
named 2026-08-22 during Milestone 2d work. amd64, matching the SanDisk-drive assumption.
- [ ] Build a fresh `starkernel.iso` via `make -f Makefile.starkernel ARCH=amd64 clean` +
the ISO-build step (already runs as part of `qemu` target — confirm it can be invoked
standalone without also launching QEMU, or just let the `qemu` target build it and
+3
View File
@@ -755,6 +755,7 @@ ifeq ($(ARCH),amd64)
-cdrom $(QEMU_ISO) -boot d \
-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
-device virtio-blk-pci,drive=artdisk \
-device qemu-xhci,id=xhci0 \
-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
-serial chardev:cserial \
-qmp unix:$$QMP_SOCK,server=on,wait=off \
@@ -818,6 +819,7 @@ else ifeq ($(ARCH),aarch64)
-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
-device virtio-blk-pci,drive=artdisk \
-device virtio-keyboard-pci,addr=0x3 \
-device qemu-xhci,id=xhci0,addr=0x4 \
-device ramfb \
-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
-serial chardev:cserial \
@@ -901,6 +903,7 @@ else ifeq ($(ARCH),riscv64)
-drive id=artdisk,file=$(ARTDISK),format=raw,if=none \
-device virtio-blk-pci,drive=artdisk,addr=0x1 \
-device virtio-keyboard-pci,addr=0x3 \
-device qemu-xhci,id=xhci0,addr=0x4 \
-device ramfb \
-chardev socket,id=cserial,path=$$SERIAL_SOCK,server=on,wait=off,logfile=$$LOG \
-serial chardev:cserial \
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-08-22T12:20:25Z -->
<!-- Generated by mkcapsule --manifest 2026-08-22T13:22:00Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+35 -2
View File
@@ -34,6 +34,10 @@ typedef struct {
xhci_trb_t *evt_ring; /* Event Ring, XHCI_RING_TRB_COUNT TRBs */
void *evt_ring_seg_table; /* Event Ring Segment Table (1 entry) */
uint32_t evt_ring_cycle; /* current Event Ring Cycle State */
uint32_t evt_ring_deq; /* current Event Ring dequeue index */
xhci_intr_regs_t *intr0; /* Interrupter 0 register set, cached
* by xhci_bringup() for
* xhci_poll_events() */
} xhci_dev_t;
/*
@@ -56,14 +60,43 @@ int xhci_find_and_map(xhci_dev_t *dev);
* halted state.
*
* Must be called after a successful xhci_find_and_map(). Does not enable
* interrupts (USBCMD.INTE / IMAN.IE) that's wired in a later increment
* alongside the actual interrupt handler.
* interrupts (USBCMD.INTE / IMAN.IE) -- this driver is polled, not
* interrupt-driven (see xhci_poll_events()'s own doc comment for why).
*
* Returns 0 on success.
* Returns -1 on reset timeout.
* Returns -2 on allocation failure.
* Returns -3 if the controller failed to leave the halted state after RUN.
* On success, latches dev into the module-static pointer xhci_poll_events()
* reads -- only one controller is supported, matching virtio_blk's
* single-device precedent.
*/
int xhci_bringup(xhci_dev_t *dev);
/*
* xhci_poll_events read Interrupter 0's Event Ring, dispatching each TRB
* by type (Port Status Change, Command Completion;
* other types logged and skipped -- Milestone 2e/2g
* consume them), then advance the Event Ring dequeue
* pointer and clear ERDP.EHB.
*
* Polled, not interrupt-driven: an initial attempt at IRQ delivery
* (Milestone 2d's first draft) found the amd64 PCI INTx routing formula
* gives a demonstrably wrong GSI (checked live via QMP query-pci: xHCI at
* PCI slot 4 reports IRQ 10, the formula predicted 16), and the
* aarch64/riscv64 slot/pin-derived source IDs were unverified at the new
* slot this controller occupies. Rather than guess further at chipset
* PIRQ routing, this matches Section U item 6's own design intent
* (Captain Bob: "interrupt-driven, coarse cadence, cheap early-exit...
* quick check blocks... done") via sk_repl_idle()'s existing coarse-cadence
* hook instead of a per-arch IRQ path -- USB insertion is a human-timescale
* event, not a hot path, so polling costs nothing meaningful here.
*
* No arguments and no return value -- only one xHCI controller is
* supported, so the caller needs no device handle. A no-op if
* xhci_bringup() has not completed successfully (dev pointer not yet
* latched).
*/
void xhci_poll_events(void);
#endif /* STARKERNEL_XHCI_DRIVER_H */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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;
}