//! 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: ok` or `pci-cap-test: FAIL `; 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"); }