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:
+41
-4
@@ -67,6 +67,13 @@ var vector_gsi: [256]u32 = .{no_gsi} ** 256;
|
||||
/// Vector currently assigned to each GSI (0 = none), so a rebind reuses it.
|
||||
var gsi_vector: [maximum_gsi]u8 = .{0} ** maximum_gsi;
|
||||
|
||||
/// MSI bindings, keyed by **vector** (not GSI): an MSI has no I/O APIC redirection
|
||||
/// entry, it is a per-device edge-triggered vector the device raises by writing the
|
||||
/// (address, data) pair `msi_bind` hands back. Read from the ISR and written from
|
||||
/// syscalls, always under the big kernel lock, like `bound`.
|
||||
var msi_bound: [256]?*ipc_sync.Endpoint = .{null} ** 256;
|
||||
var msi_owner: [256]u32 = .{0} ** 256;
|
||||
|
||||
/// Set once the trampolines are installed.
|
||||
var installed = false;
|
||||
|
||||
@@ -75,9 +82,15 @@ var installed = false;
|
||||
fn dispatch(vector: u8) void {
|
||||
const gsi = vector_gsi[vector];
|
||||
if (gsi == no_gsi) {
|
||||
// Nothing is routed here. Acknowledge so the LAPIC doesn't wedge on an
|
||||
// in-service bit that never clears, but touch no redirection entry.
|
||||
// No GSI is routed here. It may be an MSI vector (edge-triggered, unshared, no
|
||||
// I/O APIC entry): just EOI and notify — there is no line to mask and no ack
|
||||
// cycle. Or it's spurious, and the EOI alone keeps the LAPIC from wedging on an
|
||||
// in-service bit. Same lock discipline as below: msi_bound[vector] is read and
|
||||
// used under the acquisition `msiRelease` writes it under.
|
||||
_ = sync.enter();
|
||||
defer sync.leaveIsr();
|
||||
architecture.irqEoi();
|
||||
if (msi_bound[vector]) |endpoint| ipc_sync.notifyLocked(endpoint, vector);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,9 +139,9 @@ pub fn init() void {
|
||||
fn allocVector() ?u8 {
|
||||
var v: u8 = architecture.irq_vector_base;
|
||||
while (v < architecture.irq_vector_base + architecture.irq_vector_count) : (v += 1) {
|
||||
var used = false;
|
||||
var used = msi_bound[v] != null; // taken by an MSI binding
|
||||
for (gsi_vector) |gv| {
|
||||
if (gv == v) used = true;
|
||||
if (gv == v) used = true; // taken by a GSI binding
|
||||
}
|
||||
if (!used) return v;
|
||||
}
|
||||
@@ -162,6 +175,21 @@ pub fn bind(gsi: u32, endpoint: *ipc_sync.Endpoint, owner: u32) BindError!void {
|
||||
architecture.irqUnmask(gsi);
|
||||
}
|
||||
|
||||
pub const MsiError = error{NoVector};
|
||||
|
||||
/// Allocate an interrupt vector and bind it to `endpoint` for task `owner`, returning
|
||||
/// the vector. The `msi_bind` mechanism: MSI is edge-triggered and unshared, so there
|
||||
/// is no GSI, no I/O APIC redirection entry, no mask, and no ack cycle — the device
|
||||
/// raises the interrupt by *writing* the (address, data) pair the syscall derives from
|
||||
/// this vector, and `dispatch` just EOIs and notifies. Caller holds the big kernel
|
||||
/// lock and has verified `owner` claimed the device. See docs/driver-model.md (M15).
|
||||
pub fn msiBind(endpoint: *ipc_sync.Endpoint, owner: u32) MsiError!u8 {
|
||||
const vector = allocVector() orelse return error.NoVector;
|
||||
msi_bound[vector] = endpoint;
|
||||
msi_owner[vector] = owner;
|
||||
return vector;
|
||||
}
|
||||
|
||||
/// Re-arm `gsi` after the driver has quieted the device. Caller holds the big lock
|
||||
/// and has verified ownership.
|
||||
pub fn ack(gsi: u32) bool {
|
||||
@@ -187,4 +215,13 @@ pub fn releaseOwner(owner: u32) void {
|
||||
gsi_vector[gsi] = 0;
|
||||
}
|
||||
}
|
||||
// MSI bindings carry no I/O APIC entry to mask — just drop them so a stale vector
|
||||
// stops notifying a freed endpoint. A late edge on a released vector is spurious
|
||||
// (msi_bound is null) and `dispatch` EOIs it harmlessly.
|
||||
for (&msi_bound, 0..) |*slot, vector| {
|
||||
if (slot.* != null and msi_owner[vector] == owner) {
|
||||
slot.* = null;
|
||||
msi_owner[vector] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user