//! 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 }; } };