danos/library/device/pci/pci.zig

385 lines
20 KiB
Zig

//! 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 (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
//! their BARs — is a different mechanism and lives in the pci-bus driver. The pure
//! config-space layout both need (offsets, BAR bit fields) is named once in the `pci-class`
//! data module; this logic module adds the parts that need `mmio` + the `driver` client.
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
/// bring-up. Header reads and the capability walk hit live config space; `mapBar` caches.
pub const Function = struct {
device_id: u64,
descriptor: *const device.DeviceDescriptor,
config: usize, // virtual base of mapped resource 0
bar_virtual: [6]usize = .{ 0, 0, 0, 0, 0, 0 }, // per-BAR mmio_map cache
/// Map config space (resource 0) of the already-claimed `device_id`. null if the map
/// fails (not claimed, or no config resource).
pub fn map(device_id: u64, descriptor: *const device.DeviceDescriptor) ?Function {
const base = device.mmioMap(device_id, 0) orelse return null;
return .{ .device_id = device_id, .descriptor = descriptor, .config = base };
}
pub fn vendorId(self: *const Function) u16 {
return mmio.readRegister(u16, self.config + pci_class.config_vendor_id);
}
pub fn deviceId(self: *const Function) u16 {
return mmio.readRegister(u16, self.config + pci_class.config_device_id);
}
pub fn command(self: *const Function) u16 {
return mmio.readRegister(u16, self.config + pci_class.config_command);
}
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),
};
}
fn commandSetBits(self: *const Function, bits: u16) void {
const at = self.config + pci_class.config_command;
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,
/// combine the high dword for a 64-bit BAR, mask the base, then correlate that physical
/// base with one of the descriptor's memory resources and `mmio_map` it — a BAR names a
/// *number*, while `mmio_map` takes a *resource index*, and gaps/config-space shift the
/// numbering. Cached per BAR. null if the BAR is I/O-space or is not a mapped resource.
pub fn mapBar(self: *Function, bar: u8) ?usize {
if (bar >= 6) return null;
if (self.bar_virtual[bar] != 0) return self.bar_virtual[bar];
const low = mmio.readRegister(u32, self.config + pci_class.config_bar0 + @as(usize, bar) * 4);
if (low & pci_class.bar_io_space != 0) return null; // an I/O-space BAR
var base: u64 = low & pci_class.bar_memory_base_mask;
if ((low & pci_class.bar_type_mask) == pci_class.bar_type_64bit) { // 64-bit: high half is the next dword
const high = mmio.readRegister(u32, self.config + pci_class.config_bar0 + (@as(usize, bar) + 1) * 4);
base |= @as(u64, high) << 32;
}
for (self.descriptor.resources[0..@intCast(self.descriptor.resource_count)], 0..) |resource, index| {
if (resource.kind == @intFromEnum(device.ResourceKind.memory) and resource.start == base) {
const v = device.mmioMap(self.device_id, index) orelse return null;
self.bar_virtual[bar] = v;
return v;
}
}
return null;
}
/// Iterate the capability list. Empty when the function advertises none.
pub fn capabilities(self: *const Function) CapabilityIterator {
const present = self.status() & pci_class.status_capabilities_list != 0;
const first = if (present)
mmio.readRegister(u8, self.config + pci_class.config_capabilities_pointer) & pci_class.capability_pointer_mask
else
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
/// caller reads its body with `mmio.readRegister(T, cap.offset + n)`.
pub const Capability = struct { id: u8, offset: usize };
pub const CapabilityIterator = struct {
config: usize,
cursor: u8,
guard: u32 = 0, // bounds a malformed/looping chain (48 = the 256-byte space in dwords)
pub fn next(self: *CapabilityIterator) ?Capability {
if (self.cursor == 0 or self.guard >= 48) return null;
self.guard += 1;
const at = self.config + self.cursor;
const id = mmio.readRegister(u8, at + 0);
self.cursor = mmio.readRegister(u8, at + 1) & pci_class.capability_pointer_mask;
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 };
}
};