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));
}
+283 -7
View File
@@ -1,7 +1,8 @@
//! library/device/pci/pci.zig — a device driver's view of the ONE PCI function it has
//! claimed. Config space is mapped as resource 0; this gives header-field accessors, BAR
//! decode + map, and a capability-list iterator, so a driver never re-derives the
//! config-space layout by hand.
//! claimed. Config space is mapped as resource 0 (a full 4 KiB ECAM page); this gives
//! header-field accessors, BAR decode + map, capability walks (legacy and extended),
//! MSI/MSI-X programming, power-state handling, and function-level reset, so a driver
//! never re-derives the config-space layout by hand.
//!
//! This is the *device-owned* view: read my own function's live config, map my own BARs.
//! The bus enumerator's view — probing arbitrary, not-yet-claimed functions and sizing
@@ -13,6 +14,18 @@ const std = @import("std");
const mmio = @import("mmio");
const pci_class = @import("pci-class");
const device = @import("driver");
const time = @import("time");
/// Spec recovery time after a D3hot -> D0 transition.
const d0_recovery_millis: u64 = 10;
/// How long to wait for in-flight transactions to drain before a function-level reset
/// (then reset anyway — resetting a stuck function is the point of FLR).
const flr_pending_timeout_millis: u64 = 100;
/// The spec's maximum FLR completion time.
const flr_settle_millis: u64 = 100;
/// How long to wait for the function to become readable again after an FLR.
const flr_ready_timeout_millis: u64 = 1000;
const flr_poll_interval_millis: u64 = 10;
/// A claimed PCI function whose configuration space is mapped (resource 0). `descriptor`
/// must outlive the Function — the driver's `device.enumerate` buffer does, for the whole
@@ -42,12 +55,61 @@ pub const Function = struct {
pub fn status(self: *const Function) u16 {
return mmio.readRegister(u16, self.config + pci_class.config_status);
}
pub fn revisionId(self: *const Function) u8 {
return mmio.readRegister(u8, self.config + pci_class.config_revision_id);
}
/// Subsystem vendor ID (config 0x2C) — with `subsystemId`, the standard key for
/// board-level quirk matching.
pub fn subsystemVendorId(self: *const Function) u16 {
return mmio.readRegister(u16, self.config + pci_class.config_subsystem_vendor_id);
}
pub fn subsystemId(self: *const Function) u16 {
return mmio.readRegister(u16, self.config + pci_class.config_subsystem_id);
}
/// Interrupt pin (config 0x3D): 0 = none, 1..4 = INTA..INTD.
pub fn interruptPin(self: *const Function) u8 {
return mmio.readRegister(u8, self.config + pci_class.config_interrupt_pin);
}
/// The live class-code triple (config 0x09..0x0B), same shape discovery records.
pub fn classCode(self: *const Function) pci_class.ClassCode {
return .{
.prog_if = mmio.readRegister(u8, self.config + pci_class.config_class_code),
.subclass = mmio.readRegister(u8, self.config + pci_class.config_class_code + 1),
.base = mmio.readRegister(u8, self.config + pci_class.config_class_code + 2),
};
}
/// Set Memory-Space + Bus-Master enable in the command register. Firmware often leaves
/// a secondary display's decode off; a bus-mastering device must enable both.
pub fn enableMemoryAndBusMaster(self: *const Function) void {
fn commandSetBits(self: *const Function, bits: u16) void {
const at = self.config + pci_class.config_command;
mmio.writeRegister(u16, at, mmio.readRegister(u16, at) | pci_class.command_memory_and_bus_master);
mmio.writeRegister(u16, at, mmio.readRegister(u16, at) | bits);
}
fn commandClearBits(self: *const Function, bits: u16) void {
const at = self.config + pci_class.config_command;
mmio.writeRegister(u16, at, mmio.readRegister(u16, at) & ~bits);
}
/// Set Memory-Space + Bus-Master Enable in the command register. Firmware only enables
/// memory decode on devices it used at boot; any other device has dead BARs until its
/// driver sets it. Bus mastering is separately required for the device to do DMA.
pub fn enableMemoryAndBusMaster(self: *const Function) void {
self.commandSetBits(pci_class.command_memory_and_bus_master);
}
/// Clear Bus-Master Enable — stop the device initiating DMA. The quiesce half of a
/// driver's shutdown (or a supervisor restart): after this the device can no longer
/// write memory the process is about to stop owning.
pub fn disableBusMaster(self: *const Function) void {
self.commandClearBits(pci_class.command_bus_master);
}
/// Set command bit 10: suppress legacy INTx assertion. MSI/MSI-X are unaffected —
/// set this when enabling either, so the device cannot also raise the shared pin.
pub fn setInterruptDisable(self: *const Function) void {
self.commandSetBits(pci_class.command_interrupt_disable);
}
/// Clear command bit 10, re-allowing legacy INTx assertion.
pub fn clearInterruptDisable(self: *const Function) void {
self.commandClearBits(pci_class.command_interrupt_disable);
}
/// Decode BAR `bar` (0..5) and map it: read the BAR register, reject I/O-space BARs,
@@ -86,6 +148,129 @@ pub const Function = struct {
0;
return .{ .config = self.config, .cursor = first };
}
/// First capability with `id`, or null.
pub fn findCapability(self: *const Function, id: pci_class.CapabilityId) ?Capability {
var walk = self.capabilities();
while (walk.next()) |capability| {
if (capability.id == @intFromEnum(id)) return capability;
}
return null;
}
/// Program the MSI capability with the kernel's `msi_bind` result and enable it —
/// one vector (multiple-message-enable 0, matching the kernel's single-vector
/// grant), INTx suppressed. false if the function has no MSI capability.
pub fn programMsi(self: *const Function, message: device.Msi) bool {
const cap = self.findCapability(.msi) orelse return false;
const control_at = cap.offset + pci_class.msi.control;
const control = mmio.readRegister(u16, control_at);
// Program the registers while the capability is disabled.
mmio.writeRegister(u16, control_at, control & ~pci_class.msi.control_enable);
mmio.writeRegister(u32, cap.offset + pci_class.msi.address, @truncate(message.address));
const data_offset = if (control & pci_class.msi.control_64bit_capable != 0) offset: {
mmio.writeRegister(u32, cap.offset + pci_class.msi.address_high, @intCast(message.address >> 32));
break :offset pci_class.msi.data_64;
} else pci_class.msi.data_32;
// Message data is a 16-bit register in both layouts.
mmio.writeRegister(u16, cap.offset + data_offset, @truncate(message.data));
mmio.writeRegister(u16, control_at, (control & ~pci_class.msi.control_multiple_message_enable_mask) | pci_class.msi.control_enable);
self.setInterruptDisable();
return true;
}
/// Clear the MSI enable bit. No-op if the function has no MSI capability.
pub fn disableMsi(self: *const Function) void {
const cap = self.findCapability(.msi) orelse return;
const control_at = cap.offset + pci_class.msi.control;
mmio.writeRegister(u16, control_at, mmio.readRegister(u16, control_at) & ~pci_class.msi.control_enable);
}
/// The function's MSI-X capability with its vector table mapped: the table's BIR is
/// resolved through `mapBar` (a free cache hit when it is a BAR the driver already
/// mapped). null if the capability is absent or the table's BAR cannot be mapped.
pub fn msix(self: *Function) ?MsiX {
const cap = self.findCapability(.msix) orelse return null;
const control = mmio.readRegister(u16, cap.offset + pci_class.msix.control);
const word = mmio.readRegister(u32, cap.offset + pci_class.msix.table_offset_word);
const location = pci_class.msix.tableLocation(word);
const bar_base = self.mapBar(location.bar) orelse return null;
return .{
.capability = cap.offset,
.table = bar_base + location.offset,
.entry_count = pci_class.msix.tableSize(control),
};
}
/// Bring the function to D0. Firmware can leave a non-boot device in D3hot, where
/// its BARs and MSI registers do not decode; call this before touching either. No
/// power-management capability means the function is always at D0: nothing to do.
/// Preserves PME-Enable and never clears the write-1-to-clear PME-Status bit.
pub fn ensurePowerStateD0(self: *const Function) void {
const cap = self.findCapability(.power_management) orelse return;
const at = cap.offset + pci_class.power_management.control_status;
const pmcsr = mmio.readRegister(u16, at);
if (pmcsr & pci_class.power_management.control_status_power_state_mask == pci_class.power_management.power_state_d0) return;
// PME-Status is RW1C: echoing a read 1 back would clear it, so write it as 0.
mmio.writeRegister(u16, at, (pmcsr & ~pci_class.power_management.control_status_power_state_mask & ~pci_class.power_management.control_status_pme_status) | pci_class.power_management.power_state_d0);
time.sleepMillis(d0_recovery_millis);
}
/// Function Level Reset via the PCI Express capability: return the hardware to a
/// known state (a supervisor re-claiming a device after its driver died, or a driver
/// recovering a wedged function). The six BAR dwords are saved and restored — FLR
/// clears them, and the bus enumerator's assignment must survive for the descriptor
/// correlation and `mapBar` cache to stay valid. Everything else is reset: command
/// enables and MSI/MSI-X programming are gone, so the caller re-runs its whole
/// bring-up afterwards. false if the function has no PCI Express capability, does
/// not advertise FLR (conventional-PCI Advanced Features FLR is a possible
/// follow-up), or never became readable again. Blocks for at least 100 ms.
pub fn functionLevelReset(self: *const Function) bool {
const cap = self.findCapability(.pci_express) orelse return false;
const device_capabilities = mmio.readRegister(u32, cap.offset + pci_class.pci_express.device_capabilities);
if (device_capabilities & pci_class.pci_express.device_capabilities_flr == 0) return false;
// Stop new DMA, then give in-flight transactions a bounded chance to drain —
// and reset anyway on timeout, since resetting a stuck function is the point.
self.disableBusMaster();
var waited: u64 = 0;
while (mmio.readRegister(u16, cap.offset + pci_class.pci_express.device_status) & pci_class.pci_express.device_status_transactions_pending != 0) {
if (waited >= flr_pending_timeout_millis) break;
time.sleepMillis(flr_poll_interval_millis);
waited += flr_poll_interval_millis;
}
var bars: [6]u32 = undefined;
for (&bars, 0..) |*bar, index| bar.* = mmio.readRegister(u32, self.config + pci_class.config_bar0 + index * 4);
const control_at = cap.offset + pci_class.pci_express.device_control;
mmio.writeRegister(u16, control_at, mmio.readRegister(u16, control_at) | pci_class.pci_express.device_control_initiate_flr);
time.sleepMillis(flr_settle_millis);
waited = 0;
while (self.vendorId() == 0xFFFF) {
if (waited >= flr_ready_timeout_millis) return false;
time.sleepMillis(flr_poll_interval_millis);
waited += flr_poll_interval_millis;
}
for (bars, 0..) |bar, index| mmio.writeRegister(u32, self.config + pci_class.config_bar0 + index * 4, bar);
return true;
}
/// Iterate the extended (PCI Express) capability list at 0x100.. in the 4 KiB ECAM
/// page. Empty on a conventional-PCI function (the space reads as all-ones).
pub fn extendedCapabilities(self: *const Function) ExtendedCapabilityIterator {
return .{ .config = self.config };
}
/// First extended capability with `id`, or null.
pub fn findExtendedCapability(self: *const Function, id: u16) ?ExtendedCapability {
var walk = self.extendedCapabilities();
while (walk.next()) |capability| {
if (capability.id == id) return capability;
}
return null;
}
};
/// One capability header. `offset` is the ABSOLUTE virtual address of the header, so the
@@ -106,3 +291,94 @@ pub const CapabilityIterator = struct {
return .{ .id = id, .offset = at };
}
};
/// A resolved MSI-X capability from `Function.msix`: `capability` is the absolute
/// virtual address of the config-space header, `table` of vector-table entry 0 (in BAR
/// space — table writes are MMIO, not config space). Entries reset masked; bring-up
/// order is programEntry per vector, unmaskEntry per used vector, `enable`, then
/// `Function.setInterruptDisable`.
pub const MsiX = struct {
capability: usize,
table: usize,
entry_count: u16,
/// Write `message` into table entry `entry`, leaving the entry masked (its reset
/// state) — the spec requires masking while address/data change. false if `entry`
/// is out of range.
pub fn programEntry(self: *const MsiX, entry: u16, message: device.Msi) bool {
if (entry >= self.entry_count) return false;
const at = self.table + @as(usize, entry) * pci_class.msix.entry_size;
mmio.writeRegister(u32, at + pci_class.msix.entry_vector_control, pci_class.msix.entry_vector_control_masked);
mmio.writeRegister(u32, at + pci_class.msix.entry_address, @truncate(message.address));
mmio.writeRegister(u32, at + pci_class.msix.entry_address_high, @intCast(message.address >> 32));
mmio.writeRegister(u32, at + pci_class.msix.entry_data, message.data);
return true;
}
/// Set the entry's vector-control mask bit — its interrupt is held off (pended in
/// the PBA, not lost). false if `entry` is out of range.
pub fn maskEntry(self: *const MsiX, entry: u16) bool {
return self.writeEntryMask(entry, true);
}
/// Clear the entry's vector-control mask bit. false if `entry` is out of range.
pub fn unmaskEntry(self: *const MsiX, entry: u16) bool {
return self.writeEntryMask(entry, false);
}
fn writeEntryMask(self: *const MsiX, entry: u16, masked: bool) bool {
if (entry >= self.entry_count) return false;
const at = self.table + @as(usize, entry) * pci_class.msix.entry_size + pci_class.msix.entry_vector_control;
const control = mmio.readRegister(u32, at);
mmio.writeRegister(u32, at, if (masked)
control | pci_class.msix.entry_vector_control_masked
else
control & ~pci_class.msix.entry_vector_control_masked);
return true;
}
/// Set the function-mask control bit: every vector masked regardless of entry bits.
pub fn setFunctionMask(self: *const MsiX) void {
self.writeControl(pci_class.msix.control_function_mask, true);
}
/// Clear the function-mask control bit.
pub fn clearFunctionMask(self: *const MsiX) void {
self.writeControl(pci_class.msix.control_function_mask, false);
}
/// Set MSI-X Enable. The caller also calls `Function.setInterruptDisable` (INTx off).
pub fn enable(self: *const MsiX) void {
self.writeControl(pci_class.msix.control_enable, true);
}
/// Clear MSI-X Enable.
pub fn disable(self: *const MsiX) void {
self.writeControl(pci_class.msix.control_enable, false);
}
fn writeControl(self: *const MsiX, bit: u16, set: bool) void {
const at = self.capability + pci_class.msix.control;
const control = mmio.readRegister(u16, at);
mmio.writeRegister(u16, at, if (set) control | bit else control & ~bit);
}
};
/// One extended capability. `offset` is the ABSOLUTE virtual address of its header,
/// like `Capability.offset`.
pub const ExtendedCapability = struct { id: u16, version: u4, offset: usize };
pub const ExtendedCapabilityIterator = struct {
config: usize,
cursor: u16 = @intCast(pci_class.extended_capability_start),
guard: u32 = 0, // bounds a malformed chain (480 = the 0xF00-byte space / 8-byte minimum spacing)
pub fn next(self: *ExtendedCapabilityIterator) ?ExtendedCapability {
if (self.cursor == 0 or self.guard >= 480) return null;
self.guard += 1;
const at = self.config + self.cursor;
const header = pci_class.ExtendedCapabilityHeader.decode(mmio.readRegister(u32, at));
// Id 0 marks an empty list; all-ones is a conventional-PCI function (no
// extended space — reads come back as FFs).
if (header.id == 0 or header.id == 0xFFFF) return null;
// A next pointer below 0x100 would walk into the legacy header; treat it as the
// terminator it must be (0 is the normal one). The 0xFFC decode mask already
// keeps `config + cursor + 4` inside the 4 KiB page.
self.cursor = if (header.next >= pci_class.extended_capability_start) header.next else 0;
return .{ .id = header.id, .version = header.version, .offset = at };
}
};