pci: full driver-side library (MSI/MSI-X, power, FLR, extended caps); xhci goes interrupt-driven
library/device/pci is now the complete generic floor a leaf PCI driver needs, instead of just what virtio-gpu used: - pci-class: capability IDs, MSI/MSI-X/power-management/PCI-Express register layouts, extended-capability header decode (host-tested), per-bit command constants, remaining header offsets. - pci.Function: header accessors, disableBusMaster + interrupt-disable helpers, findCapability, programMsi/disableMsi, MsiX vector-table struct, ensurePowerStateD0, functionLevelReset (BAR save/restore), extended-capability iterator. Proven by the new pci-caps QEMU case: a pci-cap-test fixture claims an extra e1000e (PM+MSI+PCIe+MSI-X, no danos driver) and readback-verifies every surface, including the first driver-side use of msi_bind. usb-xhci-bus converts from 8 ms event-ring polling to message-signalled interrupts: plain MSI where offered (real Intel xHC), MSI-X entry 0 otherwise (qemu-xhci has no MSI capability), byte-identical polling as fallback. The timer survives as a 250 ms port-reconcile/lost-edge tick — real-hardware USB2 hub debounce still needs it. MSI setup runs BEFORE controller bring-up: QEMU's xhci only registers the MSI-X vector as used when IMAN.IE is written while MSI-X is already enabled; interrupts are silently dropped otherwise (real hardware does not care about the order). 101/101 QEMU cases green; real-hardware smoke passed (mouse works, boot 2026-07-23T174805Z, plain-MSI branch, vector 33).
This commit is contained in:
@@ -28,18 +28,37 @@ const usb_ids = @import("usb-ids");
|
||||
const usb_abi = @import("usb-abi");
|
||||
const usb_transfer_protocol = @import("usb-transfer-protocol");
|
||||
const library = @import("usb-xhci-library.zig");
|
||||
const pci = @import("pci");
|
||||
|
||||
/// The controller engine (reset, rings, transfers), stood up in `initialise`.
|
||||
var controller: ?library.Controller = null;
|
||||
|
||||
/// This driver's service endpoint (registered as `.usb_bus`), where class-driver
|
||||
/// requests, signals, and the interrupt-poll timer all arrive.
|
||||
/// requests, signals, MSI notifications, and the poll/reconcile timer all arrive.
|
||||
var service_endpoint: ipc.Handle = 0;
|
||||
|
||||
/// How often the driver drains the event ring for interrupt reports (~125 Hz),
|
||||
/// re-armed each tick. Frequent enough for responsive input.
|
||||
/// How often the driver drains the event ring for interrupt reports (~125 Hz) when
|
||||
/// polling, re-armed each tick. Frequent enough for responsive input.
|
||||
const poll_interval_ms: u64 = 8;
|
||||
|
||||
/// The timer interval in MSI mode: the ring is drained at interrupt time, and the tick
|
||||
/// only reconciles root ports (real hardware delivers late USB2 companion-hub debounce
|
||||
/// with no reliable port-change event — see onNotification) and un-wedges a lost MSI
|
||||
/// edge (edge-triggered, no kernel mask/ack: a missed IP clear stalls, never storms).
|
||||
const reconcile_interval_ms: u64 = 250;
|
||||
|
||||
/// The controller's own descriptor, kept at file scope because `pci.Function` holds a
|
||||
/// pointer to it for the whole bring-up.
|
||||
var controller_descriptor: device.DeviceDescriptor = undefined;
|
||||
|
||||
/// Non-null iff MSI mode is active: the vector whose notification badge means "the
|
||||
/// controller interrupted". Null means the 8 ms polling fallback is running.
|
||||
var msi_vector: ?u32 = null;
|
||||
|
||||
fn timerInterval() u64 {
|
||||
return if (msi_vector != null) reconcile_interval_ms else poll_interval_ms;
|
||||
}
|
||||
|
||||
/// The class driver endpoints that opened each device, so interrupt reports can
|
||||
/// be pushed back to them. Keyed by the device token (the interface's device id).
|
||||
const Open = struct {
|
||||
@@ -95,6 +114,7 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
std.log.info("device {d} not in the device tree", .{controller_id});
|
||||
return false;
|
||||
};
|
||||
controller_descriptor = descriptor;
|
||||
|
||||
// The xHC's registers live behind the first memory BAR. Resource 0 is the
|
||||
// function's ECAM configuration space (M15), so the walk starts at 1.
|
||||
@@ -118,6 +138,14 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Message-signalled interrupt setup comes BEFORE the controller bring-up, not
|
||||
// after: Controller.init writes IMAN.IE, and QEMU's xhci only registers the MSI-X
|
||||
// vector as in-use when that write happens with MSI-X already enabled (its
|
||||
// intr_update callback early-outs on !msix_enabled, and msix_notify silently
|
||||
// drops interrupts for an unused vector). Real hardware does not care about the
|
||||
// order; QEMU requires it.
|
||||
setupMsi();
|
||||
|
||||
// Bring the controller up: reset it, stand up the command and event rings,
|
||||
// and start it running (the hardware half lives in usb-xhci-library.zig).
|
||||
controller = library.Controller.init(register_base) orelse {
|
||||
@@ -146,12 +174,52 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
|
||||
scanPorts(handle);
|
||||
|
||||
// Arm the poll timer that drains interrupt reports from the event ring. It is
|
||||
// re-armed on each tick in onNotification; class drivers subscribe later.
|
||||
_ = time.timerOnce(service_endpoint, poll_interval_ms);
|
||||
// Arm the timer: in polling mode it drains the event ring; in MSI mode it is the
|
||||
// slower port-reconcile/safety-net tick. Re-armed on each tick in onNotification.
|
||||
_ = time.timerOnce(service_endpoint, timerInterval());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Switch the event ring from timer polling to message-signalled interrupts, if the
|
||||
/// whole path is available: map the function's config space, bind a vector, program
|
||||
/// the MSI capability — or, on an MSI-X-only function (QEMU's qemu-xhci is one: it
|
||||
/// advertises MSI-X and PCIe but no plain MSI), entry 0 of the MSI-X table, which
|
||||
/// takes the same kernel (address, data) pair (xHCI interrupter 0 raises vector 0).
|
||||
/// Any step failing leaves `msi_vector` null and the 8 ms polling path exactly as it
|
||||
/// was. The controller side needs nothing extra — IMAN.IE and USBCMD.INTE are already
|
||||
/// set (see Controller.init: QEMU only writes runtime events with the interrupter
|
||||
/// enabled).
|
||||
///
|
||||
/// After a supervised kill, the kernel drops the vector binding but the device still
|
||||
/// has the interrupt enabled and fires the stale vector; the kernel EOIs it
|
||||
/// harmlessly, and the respawned driver re-runs this with its fresh vector.
|
||||
fn setupMsi() void {
|
||||
var function = pci.Function.map(controller_id, &controller_descriptor) orelse {
|
||||
std.log.info("config-space map failed; polling at {d} ms", .{poll_interval_ms});
|
||||
return;
|
||||
};
|
||||
function.enableMemoryAndBusMaster();
|
||||
const message = device.msiBind(controller_id, service_endpoint) orelse {
|
||||
std.log.info("msi_bind unavailable; polling at {d} ms", .{poll_interval_ms});
|
||||
return;
|
||||
};
|
||||
if (function.programMsi(message)) {
|
||||
msi_vector = message.data;
|
||||
std.log.info("msi active (vector {d}); reconcile tick at {d} ms", .{ message.data, reconcile_interval_ms });
|
||||
return;
|
||||
}
|
||||
if (function.msix()) |table| {
|
||||
if (table.programEntry(0, message) and table.unmaskEntry(0)) {
|
||||
table.enable();
|
||||
function.setInterruptDisable();
|
||||
msi_vector = message.data;
|
||||
std.log.info("msix active (vector {d}); reconcile tick at {d} ms", .{ message.data, reconcile_interval_ms });
|
||||
return;
|
||||
}
|
||||
}
|
||||
std.log.info("no msi/msi-x capability; polling at {d} ms", .{poll_interval_ms});
|
||||
}
|
||||
|
||||
var register_base: usize = 0;
|
||||
|
||||
/// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10)
|
||||
@@ -501,10 +569,27 @@ fn handleBulk(message: []const u8, reply: []u8) usize {
|
||||
return writeReply(reply, usb_transfer_protocol.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 });
|
||||
}
|
||||
|
||||
/// The poll timer landed: drain any interrupt reports off the event ring and push
|
||||
/// each to the class driver that subscribed, then re-arm the timer.
|
||||
/// A timer tick or an MSI landed: drain the event ring, reconcile ports, and fan out.
|
||||
/// The timer arm re-arms itself (8 ms drain when polling, 250 ms reconcile under MSI);
|
||||
/// the MSI arm clears the interrupter's pending bit FIRST, then drains — so an event
|
||||
/// arriving after the drain takes IP 0→1 and fires a fresh edge instead of being
|
||||
/// swallowed until the reconcile tick.
|
||||
fn onNotification(badge: u64) void {
|
||||
if (badge & ipc.notify_timer_bit == 0) return;
|
||||
if (badge & ipc.notify_timer_bit != 0) {
|
||||
serviceController();
|
||||
_ = time.timerOnce(service_endpoint, timerInterval());
|
||||
return;
|
||||
}
|
||||
const vector = msi_vector orelse return;
|
||||
if (badge & ~ipc.notify_badge_bit != vector) return;
|
||||
if (controller) |*engine| engine.acknowledgeInterrupt();
|
||||
serviceController();
|
||||
}
|
||||
|
||||
/// Everything one servicing pass does, shared verbatim by the poll/reconcile tick and
|
||||
/// the MSI notification: drain the event ring, reconcile root ports, service hub
|
||||
/// changes, and push interrupt reports to their class drivers.
|
||||
fn serviceController() void {
|
||||
if (controller) |*engine| {
|
||||
engine.pump();
|
||||
// Poll every root port and reconcile — a device present but not yet
|
||||
@@ -564,7 +649,6 @@ fn onNotification(badge: u64) void {
|
||||
_ = ipc.send(report.report_endpoint, std.mem.asBytes(&message));
|
||||
}
|
||||
}
|
||||
_ = time.timerOnce(service_endpoint, poll_interval_ms);
|
||||
}
|
||||
|
||||
pub fn main(init: process.Init) void {
|
||||
|
||||
Reference in New Issue
Block a user