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:
Daniel Samson
2026-07-26 18:31:27 +01:00
parent e376c9e908
commit 9e649178bf
8 changed files with 781 additions and 22 deletions
+155 -3
View File
@@ -52,16 +52,134 @@ pub const config_vendor_id: usize = 0x00;
pub const config_device_id: usize = 0x02;
pub const config_command: usize = 0x04;
pub const config_status: usize = 0x06;
pub const config_capabilities_pointer: usize = 0x34;
pub const config_revision_id: usize = 0x08;
pub const config_class_code: usize = 0x09; // 3 bytes: prog-IF 0x09, subclass 0x0A, base class 0x0B
pub const config_bar0: usize = 0x10; // BAR0; BAR n is at config_bar0 + n*4
pub const config_subsystem_vendor_id: usize = 0x2C;
pub const config_subsystem_id: usize = 0x2E;
pub const config_expansion_rom: usize = 0x30;
pub const config_capabilities_pointer: usize = 0x34;
pub const config_interrupt_line: usize = 0x3C;
pub const config_interrupt_pin: usize = 0x3D; // 0 = none, 1..4 = INTA..INTD
/// Command register: Memory-Space enable (bit 1) | Bus-Master enable (bit 2).
pub const command_memory_and_bus_master: u16 = 0x06;
/// Command register bits.
pub const command_io_space: u16 = 0x0001; // bit 0: I/O-space decode enable
pub const command_memory_space: u16 = 0x0002; // bit 1: memory-space decode enable
pub const command_bus_master: u16 = 0x0004; // bit 2: bus-master (DMA) enable
pub const command_interrupt_disable: u16 = 0x0400; // bit 10: suppress legacy INTx (MSI/MSI-X unaffected)
/// The pair a bus-mastering driver enables together: decode my BARs, let me DMA.
pub const command_memory_and_bus_master: u16 = command_memory_space | command_bus_master;
/// Status register bit 3: legacy INTx is asserted (upstream of the command bit-10 gate).
pub const status_interrupt: u16 = 0x0008;
/// Status register bit 4: a capability list is present at config_capabilities_pointer.
pub const status_capabilities_list: u16 = 0x10;
/// Capability pointers are dword-aligned; the low two bits are reserved.
pub const capability_pointer_mask: u8 = 0xFC;
/// Capability IDs — the first byte of each entry in the legacy capability list.
/// Non-exhaustive: hardware may report IDs not named here.
pub const CapabilityId = enum(u8) {
power_management = 0x01,
msi = 0x05,
vendor_specific = 0x09,
pci_express = 0x10,
msix = 0x11,
_,
};
/// MSI capability (id 0x05) register layout. Offsets are relative to the capability
/// header; whether the address is one or two dwords (and therefore where the data word
/// sits) depends on `control_64bit_capable`.
pub const msi = struct {
pub const control: usize = 0x02; // u16 Message Control
pub const control_enable: u16 = 0x0001;
pub const control_multiple_message_capable_mask: u16 = 0x000E; // bits 3:1, log2(vectors requested)
pub const control_multiple_message_enable_mask: u16 = 0x0070; // bits 6:4, log2(vectors granted)
pub const control_64bit_capable: u16 = 0x0080; // bit 7: address is 64-bit (layout shifts)
pub const control_per_vector_masking: u16 = 0x0100; // bit 8
pub const address: usize = 0x04; // u32 low address dword (both layouts)
pub const address_high: usize = 0x08; // u32, present only when 64-bit capable
pub const data_32: usize = 0x08; // u16 message data, 32-bit layout
pub const data_64: usize = 0x0C; // u16 message data, 64-bit layout
pub const mask_bits_32: usize = 0x0C; // u32, only with per-vector masking
pub const mask_bits_64: usize = 0x10;
};
/// MSI-X capability (id 0x11) register layout, plus the 16-byte vector table entry that
/// lives in BAR space (not configuration space) at the decoded (BIR, offset).
pub const msix = struct {
pub const control: usize = 0x02; // u16 Message Control
pub const control_table_size_mask: u16 = 0x07FF; // bits 10:0, encoded as N-1
pub const control_function_mask: u16 = 0x4000; // bit 14: mask every vector
pub const control_enable: u16 = 0x8000; // bit 15
pub const table_offset_word: usize = 0x04; // u32: BIR in bits 2:0, table offset in bits 31:3
pub const pba_offset_word: usize = 0x08; // u32: same encoding, pending-bit array
pub const bir_mask: u32 = 0x0000_0007;
pub const offset_mask: u32 = 0xFFFF_FFF8;
pub const entry_size: usize = 16; // table entry stride; offsets within an entry:
pub const entry_address: usize = 0x0; // u32 low
pub const entry_address_high: usize = 0x4; // u32 high
pub const entry_data: usize = 0x8; // u32
pub const entry_vector_control: usize = 0xC; // u32
pub const entry_vector_control_masked: u32 = 0x1; // bit 0; entries reset to masked
/// Where the table (or pending-bit array) lives, decoded from its offset/BIR dword.
pub const TableLocation = struct { bar: u8, offset: u32 };
pub fn tableLocation(word: u32) TableLocation {
return .{ .bar = @intCast(word & bir_mask), .offset = word & offset_mask };
}
/// Number of table entries (the control field encodes N-1).
pub fn tableSize(control_value: u16) u16 {
return (control_value & control_table_size_mask) + 1;
}
};
/// Power-management capability (id 0x01) register layout.
pub const power_management = struct {
pub const capabilities: usize = 0x02; // u16 PMC (read-only: version, D-state support)
pub const control_status: usize = 0x04; // u16 PMCSR
pub const control_status_power_state_mask: u16 = 0x0003; // bits 1:0
pub const power_state_d0: u16 = 0x0;
pub const power_state_d3_hot: u16 = 0x3;
pub const control_status_pme_enable: u16 = 0x0100; // bit 8: plain RW — preserve on writes
pub const control_status_pme_status: u16 = 0x8000; // bit 15: RW1C — write 0 or you clear it
};
/// PCI Express capability (id 0x10) register layout — the slice function-level reset
/// needs; the full capability is much larger.
pub const pci_express = struct {
pub const capabilities: usize = 0x02; // u16 PCIe Capabilities register
pub const device_capabilities: usize = 0x04; // u32
pub const device_capabilities_flr: u32 = 1 << 28; // Function Level Reset supported
pub const device_control: usize = 0x08; // u16
pub const device_control_initiate_flr: u16 = 1 << 15;
pub const device_status: usize = 0x0A; // u16
pub const device_status_transactions_pending: u16 = 1 << 5;
};
/// Extended (PCI Express) capabilities start here in the 4 KiB configuration space; a
/// conventional-PCI function has nothing there (the space reads as all-ones).
pub const extended_capability_start: usize = 0x100;
/// Extended-capability next pointers are dword-aligned within the 4 KiB space.
pub const extended_capability_pointer_mask: u16 = 0xFFC;
/// The 32-bit header at the start of each extended capability: ID in bits 15:0,
/// version in 19:16, next offset in 31:20 (0 = end of list).
pub const ExtendedCapabilityHeader = struct {
id: u16,
version: u4,
next: u16,
pub fn decode(word: u32) ExtendedCapabilityHeader {
return .{
.id = @truncate(word),
.version = @truncate(word >> 16),
.next = @intCast((word >> 20) & extended_capability_pointer_mask),
};
}
};
/// BAR bit layout: bit 0 selects I/O (1) vs memory (0) space; for a memory BAR, bits 2:1
/// give the type (00 = 32-bit, 10 = 64-bit spanning the next BAR), and the base address is
/// the dword with the low 4 flag bits masked off.
@@ -595,3 +713,37 @@ test "named parts pack to the raw triple" {
};
try std.testing.expectEqual(@as(u24, 0x0C_03_30), xhci.pack());
}
test "MSI-X table word decodes to BIR and offset" {
const eq = std.testing.expectEqual;
// BIR 3, table at 0x2000 within that BAR.
try eq(msix.TableLocation{ .bar = 3, .offset = 0x2000 }, msix.tableLocation(0x0000_2003));
// BIR 0, offset 0 — the degenerate-but-common "table at BAR start" case.
try eq(msix.TableLocation{ .bar = 0, .offset = 0 }, msix.tableLocation(0));
// Table size encodes N-1 in bits 10:0; enable/function-mask bits must not leak in.
try eq(@as(u16, 11), msix.tableSize(msix.control_enable | 0x000A));
try eq(@as(u16, 1), msix.tableSize(0));
try eq(@as(u16, 2048), msix.tableSize(msix.control_table_size_mask));
}
test "extended capability header unpacks id, version, next" {
const eq = std.testing.expectEqual;
// AER (id 0x0001), version 1, next capability at 0x140.
const aer = ExtendedCapabilityHeader.decode(0x1401_0001);
try eq(@as(u16, 0x0001), aer.id);
try eq(@as(u4, 1), aer.version);
try eq(@as(u16, 0x140), aer.next);
// A zero header is the "nothing here" terminator.
const none = ExtendedCapabilityHeader.decode(0);
try eq(@as(u16, 0), none.id);
try eq(@as(u16, 0), none.next);
}
test "command bits and capability ids compose" {
const eq = std.testing.expectEqual;
try eq(command_memory_space | command_bus_master, command_memory_and_bus_master);
try eq(@as(u8, 0x05), @intFromEnum(CapabilityId.msi));
try eq(@as(u8, 0x11), @intFromEnum(CapabilityId.msix));
try eq(@as(u8, 0x01), @intFromEnum(CapabilityId.power_management));
try eq(@as(u8, 0x10), @intFromEnum(CapabilityId.pci_express));
}