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
+25
View File
@@ -3,6 +3,7 @@
//! ownership of its hardware; the claim is the capability the kernel checks before
//! mapping registers or routing an IRQ.
const abi = @import("abi");
const device_abi = @import("device-abi");
const sc = @import("system-call.zig");
@@ -65,3 +66,27 @@ pub fn irqBind(device_id: u64, resource_index: u64, endpoint: usize) bool {
pub fn irqAck(device_id: u64, resource_index: u64) bool {
return !failed(sc.systemCall2(.irq_ack, device_id, resource_index));
}
/// The Message-Signalled Interrupt address/data a driver programs into its device's
/// MSI capability. The device raises the interrupt by writing `data` to `address`.
pub const Msi = struct { address: u64, data: u32 };
/// Set up MSI for a claimed device: the kernel allocates a per-device edge-triggered
/// vector, binds it to `endpoint` (delivered like `irqBind`, but with no mask and no
/// `irqAck` cycle), and returns the (address, data) to write into the device's MSI
/// capability — found by mmio_mapping the device's ECAM config space (resource 0) and
/// walking its capability list. Returns null on failure. Two return values (address in
/// rax, data in rdx), so a hand-written stub.
pub fn msiBind(device_id: u64, endpoint: usize) ?Msi {
var rax: usize = undefined;
var rdx: usize = undefined;
asm volatile ("syscall"
: [rax] "={rax}" (rax),
[rdx] "={rdx}" (rdx),
: [n] "{rax}" (@intFromEnum(abi.SystemCall.msi_bind)),
[a0] "{rdi}" (device_id),
[a1] "{rsi}" (endpoint),
: .{ .rcx = true, .r11 = true, .memory = true });
if (failed(rax)) return null;
return .{ .address = rax, .data = @intCast(rdx) };
}