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:
@@ -0,0 +1,178 @@
|
||||
//! pci-cap-test — QEMU fixture for the driver-side PCI library (library/device/pci).
|
||||
//! The pci-caps test case boots with an extra `-device e1000e` NIC that no danos driver
|
||||
//! claims; this fixture claims it and exercises the whole claimed-function surface
|
||||
//! against real (emulated) hardware: header accessors, command bits, the capability
|
||||
//! walk, MSI programming (the first driver-side `msi_bind` use), the MSI-X table,
|
||||
//! extended capabilities, power state, and — where offered — function-level reset.
|
||||
//! Every check prints `pci-cap-test: <check> ok` or `pci-cap-test: FAIL <check>`; the
|
||||
//! harness asserts on the final `all checks passed` marker (test/qemu_test.py).
|
||||
|
||||
const std = @import("std");
|
||||
const device = @import("driver");
|
||||
const ipc = @import("ipc");
|
||||
const time = @import("time");
|
||||
const logging = @import("logging");
|
||||
const mmio = @import("mmio");
|
||||
const pci = @import("pci");
|
||||
const pci_class = @import("pci-class");
|
||||
|
||||
/// QEMU's e1000e: Intel 82574L.
|
||||
const intel_vendor: u16 = 0x8086;
|
||||
const e1000e_device: u16 = 0x10D3;
|
||||
|
||||
const ethernet_class: u64 = pci_class.ClassCode.pack(.{
|
||||
.base = @intFromEnum(pci_class.BaseClass.network),
|
||||
.subclass = @intFromEnum(pci_class.network.SubClass.ethernet),
|
||||
.prog_if = 0,
|
||||
});
|
||||
|
||||
/// `pci.Function` keeps a pointer to the descriptor, so it must outlive the stack frame
|
||||
/// that found it.
|
||||
var descriptor: device.DeviceDescriptor = undefined;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = logging.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Print the check's verdict; the caller returns on false to stop at the first failure.
|
||||
fn check(comptime name: []const u8, ok: bool) bool {
|
||||
if (ok) {
|
||||
_ = logging.write("pci-cap-test: " ++ name ++ " ok\n");
|
||||
} else {
|
||||
_ = logging.write("pci-cap-test: FAIL " ++ name ++ "\n");
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
// The bus scan runs in another process; poll until the NIC shows up.
|
||||
const nic_id: u64 = found: {
|
||||
var tries: u32 = 0;
|
||||
while (tries < 150) : (tries += 1) {
|
||||
var descriptors: [64]device.DeviceDescriptor = undefined;
|
||||
const total = device.enumerate(&descriptors);
|
||||
for (descriptors[0..@min(total, descriptors.len)]) |*entry| {
|
||||
if (entry.class == @intFromEnum(device.DeviceClass.pci_device) and entry.pci_class == ethernet_class) {
|
||||
descriptor = entry.*;
|
||||
break :found entry.id;
|
||||
}
|
||||
}
|
||||
time.sleepMillis(100);
|
||||
}
|
||||
_ = logging.write("pci-cap-test: FAIL no ethernet function found\n");
|
||||
return;
|
||||
};
|
||||
writeLine("pci-cap-test: claiming ethernet function (device {d})\n", .{nic_id});
|
||||
if (!check("claim", device.claim(nic_id))) return;
|
||||
var function = pci.Function.map(nic_id, &descriptor) orelse {
|
||||
_ = logging.write("pci-cap-test: FAIL config-space map\n");
|
||||
return;
|
||||
};
|
||||
|
||||
// Identity: the header accessors against known e1000e values.
|
||||
if (!check("vendor/device id", function.vendorId() == intel_vendor and function.deviceId() == e1000e_device)) return;
|
||||
if (!check("class code", function.classCode().pack() == ethernet_class)) return;
|
||||
if (!check("subsystem ids readable", function.subsystemVendorId() != 0xFFFF and function.subsystemId() != 0xFFFF)) return;
|
||||
|
||||
// Command bits: enable, read back, quiesce, read back, re-enable.
|
||||
function.enableMemoryAndBusMaster();
|
||||
if (!check("memory+bus-master enable", function.command() & pci_class.command_memory_and_bus_master == pci_class.command_memory_and_bus_master)) return;
|
||||
function.disableBusMaster();
|
||||
if (!check("bus-master disable", function.command() & pci_class.command_bus_master == 0)) return;
|
||||
function.enableMemoryAndBusMaster();
|
||||
|
||||
// The capability walk: e1000e advertises PM, MSI, PCIe, and MSI-X.
|
||||
var seen_power = false;
|
||||
var seen_msi = false;
|
||||
var seen_pci_express = false;
|
||||
var seen_msix = false;
|
||||
var walk = function.capabilities();
|
||||
while (walk.next()) |capability| {
|
||||
switch (capability.id) {
|
||||
@intFromEnum(pci_class.CapabilityId.power_management) => seen_power = true,
|
||||
@intFromEnum(pci_class.CapabilityId.msi) => seen_msi = true,
|
||||
@intFromEnum(pci_class.CapabilityId.pci_express) => seen_pci_express = true,
|
||||
@intFromEnum(pci_class.CapabilityId.msix) => seen_msix = true,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
if (!check("capability walk", seen_power and seen_msi and seen_pci_express and seen_msix)) return;
|
||||
if (!check("findCapability", function.findCapability(.msi) != null and function.findCapability(.pci_express) != null)) return;
|
||||
|
||||
// MSI: bind a vector (the syscall's first driver-side use), program the capability,
|
||||
// and read the registers straight back.
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = logging.write("pci-cap-test: FAIL endpoint creation\n");
|
||||
return;
|
||||
};
|
||||
const message = device.msiBind(nic_id, endpoint) orelse {
|
||||
_ = logging.write("pci-cap-test: FAIL msi_bind\n");
|
||||
return;
|
||||
};
|
||||
if (!check("msi_bind address", message.address == 0xFEE0_0000)) return;
|
||||
if (!check("programMsi", function.programMsi(message))) return;
|
||||
const msi_cap = function.findCapability(.msi).?;
|
||||
const msi_control = mmio.readRegister(u16, msi_cap.offset + pci_class.msi.control);
|
||||
const msi_data_offset: usize = if (msi_control & pci_class.msi.control_64bit_capable != 0) pci_class.msi.data_64 else pci_class.msi.data_32;
|
||||
if (!check("msi registers read back", msi_control & pci_class.msi.control_enable != 0 and
|
||||
msi_control & pci_class.msi.control_multiple_message_enable_mask == 0 and
|
||||
mmio.readRegister(u32, msi_cap.offset + pci_class.msi.address) == @as(u32, @truncate(message.address)) and
|
||||
mmio.readRegister(u16, msi_cap.offset + msi_data_offset) == @as(u16, @truncate(message.data)))) return;
|
||||
if (!check("intx disabled with msi", function.command() & pci_class.command_interrupt_disable != 0)) return;
|
||||
function.disableMsi();
|
||||
if (!check("disableMsi", mmio.readRegister(u16, msi_cap.offset + pci_class.msi.control) & pci_class.msi.control_enable == 0)) return;
|
||||
|
||||
// MSI-X: map the table, program entry 0, exercise the masks. Never enable — this
|
||||
// proves the programming surface, not delivery.
|
||||
const msix_table = function.msix() orelse {
|
||||
_ = logging.write("pci-cap-test: FAIL msix table map\n");
|
||||
return;
|
||||
};
|
||||
writeLine("pci-cap-test: msix table has {d} entries\n", .{msix_table.entry_count});
|
||||
if (!check("msix entry count", msix_table.entry_count >= 1)) return;
|
||||
if (!check("msix programEntry", msix_table.programEntry(0, message))) return;
|
||||
if (!check("msix entry reads back", mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_address) == @as(u32, @truncate(message.address)) and
|
||||
mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_data) == message.data and
|
||||
mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked != 0)) return;
|
||||
if (!check("msix unmask entry", msix_table.unmaskEntry(0) and
|
||||
mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked == 0)) return;
|
||||
if (!check("msix re-mask entry", msix_table.maskEntry(0) and
|
||||
mmio.readRegister(u32, msix_table.table + pci_class.msix.entry_vector_control) & pci_class.msix.entry_vector_control_masked != 0)) return;
|
||||
msix_table.setFunctionMask();
|
||||
if (!check("msix function mask", mmio.readRegister(u16, msix_table.capability + pci_class.msix.control) & pci_class.msix.control_function_mask != 0)) return;
|
||||
msix_table.clearFunctionMask();
|
||||
if (!check("msix function unmask", mmio.readRegister(u16, msix_table.capability + pci_class.msix.control) & pci_class.msix.control_function_mask == 0)) return;
|
||||
if (!check("msix out-of-range rejected", !msix_table.programEntry(msix_table.entry_count, message))) return;
|
||||
|
||||
// Extended capabilities: the walk must terminate cleanly; the count is informative
|
||||
// (don't hard-bind to QEMU's exact extended-capability set).
|
||||
var extended_count: u32 = 0;
|
||||
var extended = function.extendedCapabilities();
|
||||
while (extended.next()) |_| extended_count += 1;
|
||||
writeLine("pci-cap-test: {d} extended capabilities\n", .{extended_count});
|
||||
if (!check("extended walk terminates", extended_count < 480)) return;
|
||||
|
||||
// Power: QEMU leaves the function in D0; ensurePowerStateD0 must agree and not
|
||||
// disturb the PMCSR.
|
||||
const power_cap = function.findCapability(.power_management).?;
|
||||
const pmcsr_before = mmio.readRegister(u16, power_cap.offset + pci_class.power_management.control_status);
|
||||
if (!check("power state is D0", pmcsr_before & pci_class.power_management.control_status_power_state_mask == pci_class.power_management.power_state_d0)) return;
|
||||
function.ensurePowerStateD0();
|
||||
if (!check("ensurePowerStateD0 is a no-op at D0", mmio.readRegister(u16, power_cap.offset + pci_class.power_management.control_status) == pmcsr_before)) return;
|
||||
|
||||
// Function-level reset, where the device offers it: afterwards the function must be
|
||||
// readable with its identity intact, and bring-up must work again.
|
||||
const express_cap = function.findCapability(.pci_express).?;
|
||||
const device_capabilities = mmio.readRegister(u32, express_cap.offset + pci_class.pci_express.device_capabilities);
|
||||
if (device_capabilities & pci_class.pci_express.device_capabilities_flr != 0) {
|
||||
if (!check("functionLevelReset", function.functionLevelReset())) return;
|
||||
if (!check("identity after flr", function.vendorId() == intel_vendor and function.deviceId() == e1000e_device)) return;
|
||||
function.enableMemoryAndBusMaster();
|
||||
if (!check("re-enable after flr", function.command() & pci_class.command_memory_and_bus_master == pci_class.command_memory_and_bus_master)) return;
|
||||
} else {
|
||||
_ = logging.write("pci-cap-test: flr not offered, skipped\n");
|
||||
}
|
||||
|
||||
_ = logging.write("pci-cap-test: all checks passed\n");
|
||||
}
|
||||
Reference in New Issue
Block a user