M15: interrupts for PCI devices (ECAM config space + MSI)

Two parts, both blockers for real PCI drivers.

ECAM config space per function: enumeratePci now gives every pci_device its own 4 KiB
configuration window as resource 0. A claimed PCI driver mmio_maps that to reach its
command register, BARs, and — the point — its capability list (MSI/MSI-X, PCIe extended
caps), with no new syscall. Verified in the discovery test (QEMU q35's functions each
carry it).

MSI: msi_bind(device_id, endpoint) -> address (rax), data (rdx) allocates a per-device
edge-triggered vector, binds it to the endpoint, and returns the (address, data) the
driver programs into its own MSI capability. Unlike irq_bind there's no GSI, no I/O APIC
entry, no sharing, and no ack cycle — dispatch recognises an MSI vector (vector_gsi ==
none, msi_bound set), EOIs, and notifies. Owner-keyed release drops the binding on exit.
Legacy INTx (_PRT parsing + shared lines) is deliberately skipped; MSI is the real
answer.

QEMU's HPET has no MSI, so the new `msi` test proves the vector-routing path with a
self-IPI (new apic.selfIpi) standing in for the device's MSI write: bind a vector,
fire it, the bound endpoint is notified. The msi_bind syscall wraps irq.msiBind with
the claim check and lands its first real use with the first PCI driver. Suite 39/39
plus host tests.
This commit is contained in:
Daniel Samson
2026-07-10 19:59:09 +01:00
parent 125a3b4993
commit 4ef21fa083
10 changed files with 189 additions and 5 deletions
+50
View File
@@ -86,6 +86,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
capabilityTest();
} else if (eql(case, "dma")) {
dmaTest();
} else if (eql(case, "msi")) {
msiTest();
} else if (eql(case, "smp")) {
smpTest();
} else if (eql(case, "affinity")) {
@@ -234,6 +236,23 @@ fn discoveryTest() void {
check("AML parsed completely (consumed == total)", am.total > 0 and am.consumed == am.total);
check("at least one CPU enumerated (MADT)", platform.cpus().len >= 1);
// M15: every PCI function now carries its own 4 KiB ECAM configuration space as
// resource 0 — the window a driver mmio_maps to walk its capability list (MSI etc).
var buffer: [64]device_abi.DeviceDescriptor = undefined;
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
var pci_functions: u32 = 0;
var pci_config_ok = true;
for (buffer[0..n]) |d| {
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_device)) continue;
pci_functions += 1;
const has_config = d.resource_count >= 1 and
d.resources[0].kind == @intFromEnum(device_abi.ResourceKind.memory) and
d.resources[0].len == abi.page_size;
if (!has_config) pci_config_ok = false;
}
check("PCI functions were enumerated (MCFG/ECAM)", pci_functions >= 1);
check("each PCI function exposes its ECAM config space as resource 0", pci_config_ok);
result();
}
@@ -984,6 +1003,37 @@ fn dmaTest() void {
result();
}
/// MSI (M15): a per-device, edge-triggered interrupt vector delivered as an IPC
/// notification. QEMU's HPET has no MSI, so this exercises the vector-routing path with
/// a self-IPI standing in for the device's MSI memory write — proving the kernel
/// allocates a vector, `dispatch` recognises it as MSI (EOI + notify, no mask cycle),
/// and the bound endpoint is notified. The `msi_bind` syscall wraps `irq.msiBind` with
/// the device-claim check and lands its first real use with the first PCI driver.
fn msiTest() void {
log("DANOS-TEST-BEGIN: msi\n", .{});
const endpoint = ipcsync.createEndpoint().?;
const flags = sync.enter();
const vector = irq.msiBind(endpoint, 0) catch {
sync.leave(flags);
check("msiBind allocated a vector", false);
result();
return;
};
sync.leave(flags);
check("msiBind allocated a vector in the device window", vector >= architecture.irq_vector_base and
vector < architecture.irq_vector_base + architecture.irq_vector_count);
// Fire the vector — the stand-in for the device writing its MSI (address, data).
const tail: *volatile u8 = &endpoint.notify_tail;
const before = tail.*;
architecture.selfIpi(vector);
var spins: u64 = 0;
while (tail.* == before and spins < 100_000_000) : (spins += 1) scheduler.yield();
check("self-IPI at the MSI vector notified the bound endpoint", tail.* != before);
result();
}
var proc_worker_run: bool = true;
var proc_worker_ran: bool = false;