ipc_call and ipc_reply_wait grow a `send_cap` argument (r9) and a `received_cap` return (r8): an endpoint travels alongside a message, installed into the receiver's handle table. The transfer is a share, not a move — the endpoint's refcount is bumped and the sender keeps its handle. If the receiver's table is full the call fails -ENOSPC and the message is NOT delivered (a half-delivered capability is worse than a failed send); a bad handle fails -EBADF. Both directions carry a cap: a client's call hands one to the server (seen in the server's replyWait), and the server's reply hands one back (seen in the client's call return). This is the "open" primitive the driver model was blocked on: a bus driver mints a per-device endpoint and hands it to a class driver, giving it a private channel to one device without the 8-slot global name registry. Kernel: shareCapability in ipc-synchronous.zig at both copy points; new setSystemCallResult3 (r8, saved/restored by the syscall stub); Task gains ipc_send_cap / ipc_received_cap. Runtime: callCap + Reply, replyWait gains send_cap and Received.cap; plain call/replyWait delegate with no_cap. New abi.no_cap. New ipc-cap test (two kernel tasks exercise both directions, each verifying the endpoint it received is the same object shared, refcount bumped to 2). No class driver consumes callCap yet — it lands with the first one. Suite 37/37 plus host tests.
188 lines
8.0 KiB
Zig
188 lines
8.0 KiB
Zig
//! /system/drivers/hpet — a user-space HPET driver. It proves the whole driver model end to
|
|
//! end: enumerate the device table, find the HPET, claim it, map its registers into
|
|
//! this ring-3 address space (strong-uncacheable), **bind its interrupt to an IPC
|
|
//! endpoint**, then sit blocked in `replyWait` until the hardware wakes it.
|
|
//!
|
|
//! Nothing here polls. Between interrupts the process is `.blocked` and off every
|
|
//! scheduler queue; the core runs other work or idles. That is the point of the
|
|
//! exercise — a driver is a process that sleeps until its device has something to
|
|
//! say (see docs/drivers.md).
|
|
//!
|
|
//! The comparator is configured **level-triggered** on purpose. Edge would be
|
|
//! simpler, but level is the discipline every real device line needs, and it forces
|
|
//! the full cycle to be correct:
|
|
//!
|
|
//! kernel ISR mask the GSI -> EOI -> notify this endpoint
|
|
//! hpet wake, clear GENERAL_INT_STATUS (deasserts the line), re-arm
|
|
//! hpet irq_ack -> kernel unmasks the GSI
|
|
//!
|
|
//! Clear the status bit *before* acking, or the line is still asserted when the
|
|
//! kernel unmasks and the I/O APIC redelivers forever.
|
|
//!
|
|
//! Register map (HPET spec 1.0a):
|
|
//! 0x000 GENERAL_CAP [63:32] fs per tick, [12:8] number timers - 1
|
|
//! 0x010 GENERAL_CONFIGURATION bit0 ENABLE_CNF, bit1 LEG_RT_CNF
|
|
//! 0x020 GENERAL_INT_STATUS bit n = timer n asserted (write 1 to clear)
|
|
//! 0x0F0 MAIN_COUNTER
|
|
//! 0x100 TIMER0_CONFIGURATION bit1 INT_TYPE(1=level) bit2 INT_ENB bit3 TYPE(periodic)
|
|
//! bits[13:9] INT_ROUTE, [63:32] INT_ROUTE_CAP
|
|
//! 0x108 TIMER0_COMPARATOR
|
|
|
|
const runtime = @import("runtime");
|
|
const device = runtime.device;
|
|
const ipc = runtime.ipc;
|
|
|
|
const register_general_cap = 0x000;
|
|
const register_general_configuration = 0x010;
|
|
const register_int_status = 0x020;
|
|
const register_main_counter = 0x0F0;
|
|
const register_timer0_configuration = 0x100;
|
|
const register_timer0_comparator = 0x108;
|
|
|
|
const configuration_enable: u64 = 1 << 0; // GENERAL_CONFIGURATION.ENABLE_CNF
|
|
const configuration_leg_rt: u64 = 1 << 1; // GENERAL_CONFIGURATION.LEG_RT_CNF
|
|
const tn_int_type_level: u64 = 1 << 1;
|
|
const tn_int_enb: u64 = 1 << 2;
|
|
const tn_type_periodic: u64 = 1 << 3;
|
|
const tn_route_shift = 9;
|
|
const tn_route_mask: u64 = 0x1F << tn_route_shift;
|
|
|
|
/// Interrupts to observe before declaring victory.
|
|
const target_ticks = 5;
|
|
|
|
fn register(base: usize, off: usize) *volatile u64 {
|
|
return @ptrFromInt(base + off);
|
|
}
|
|
|
|
/// A timer-class device exposing both an MMIO window and an IRQ: its id, the two
|
|
/// resource indices, and the GSI discovery chose out of `Tn_INT_ROUTE_CAP`.
|
|
const Found = struct { device_id: u64, mmio: u64, irq: u64, gsi: u64 };
|
|
|
|
fn findHpet(buffer: []device.DeviceDescriptor) ?Found {
|
|
const total = device.enumerate(buffer);
|
|
const n = @min(total, buffer.len);
|
|
for (buffer[0..n]) |d| {
|
|
if (d.class != @intFromEnum(device.DeviceClass.timer)) continue;
|
|
// Skip comparator children a bus driver may have published below the block
|
|
// (see system/drivers/bus/bus.zig) — we want the register block itself.
|
|
if (d.parent != device.no_parent) continue;
|
|
var mmio: ?u64 = null;
|
|
var irq: ?u64 = null;
|
|
for (0..d.resource_count) |j| {
|
|
switch (d.resources[j].kind) {
|
|
@intFromEnum(device.ResourceKind.memory) => mmio = mmio orelse j,
|
|
@intFromEnum(device.ResourceKind.irq) => irq = irq orelse j,
|
|
else => {},
|
|
}
|
|
}
|
|
if (mmio) |m| if (irq) |i| {
|
|
return .{ .device_id = d.id, .mmio = m, .irq = i, .gsi = d.resources[i].start };
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn main() void {
|
|
// Enumerate into a heap buffer (too big for the one-page user stack).
|
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 32) catch {
|
|
_ = runtime.system.write("hpet: out of memory\n");
|
|
return;
|
|
};
|
|
|
|
const hpet = findHpet(buffer) orelse {
|
|
_ = runtime.system.write("hpet: no HPET with an IRQ\n");
|
|
return;
|
|
};
|
|
|
|
if (!device.claim(hpet.device_id)) {
|
|
_ = runtime.system.write("hpet: claim failed\n");
|
|
return;
|
|
}
|
|
const base = device.mmioMap(hpet.device_id, hpet.mmio) orelse {
|
|
_ = runtime.system.write("hpet: mmio_map failed\n");
|
|
return;
|
|
};
|
|
|
|
// The GSI discovery picked for us out of Tn_INT_ROUTE_CAP. Program the comparator
|
|
// to raise exactly this line — the kernel will only bind the one it recorded.
|
|
const gsi = hpet.gsi;
|
|
|
|
const endpoint = ipc.createEndpoint() orelse {
|
|
_ = runtime.system.write("hpet: create_endpoint failed\n");
|
|
return;
|
|
};
|
|
|
|
// --- program the hardware ------------------------------------------------
|
|
// Counter period, so we can arm the comparator a fixed wall-clock distance out.
|
|
const femtos_per_tick = register(base, register_general_cap).* >> 32;
|
|
if (femtos_per_tick == 0) {
|
|
_ = runtime.system.write("hpet: bad HPET period\n");
|
|
return;
|
|
}
|
|
const ticks_per_ms = 1_000_000_000_000 / femtos_per_tick;
|
|
|
|
// Stop the counter and take the legacy route off while we reconfigure.
|
|
register(base, register_general_configuration).* &= ~(configuration_enable | configuration_leg_rt);
|
|
|
|
// Timer 0: one-shot, level-triggered, routed to our GSI, interrupt enabled.
|
|
// One-shot (not periodic) sidesteps the HPET's Tn_value_SET accumulator quirk —
|
|
// we simply re-arm from the driver on each interrupt, which is what a tickless
|
|
// timer driver does anyway.
|
|
var t0 = register(base, register_timer0_configuration).*;
|
|
t0 &= ~(tn_route_mask | tn_type_periodic);
|
|
t0 |= tn_int_type_level | tn_int_enb | (gsi << tn_route_shift);
|
|
register(base, register_timer0_configuration).* = t0;
|
|
|
|
// Clear any stale assertion, then arm ~100 ms out and start the counter.
|
|
register(base, register_int_status).* = 1;
|
|
register(base, register_timer0_comparator).* = register(base, register_main_counter).* + ticks_per_ms * 100;
|
|
register(base, register_general_configuration).* |= configuration_enable;
|
|
|
|
if (!device.irqBind(hpet.device_id, hpet.irq, endpoint)) {
|
|
_ = runtime.system.write("hpet: irq_bind failed\n");
|
|
return;
|
|
}
|
|
_ = runtime.system.write("hpet: bound, sleeping until the hardware speaks\n");
|
|
|
|
// --- the driver loop -----------------------------------------------------
|
|
// Blocked in replyWait. No polling, no spinning: the next line of this function
|
|
// runs only because an interrupt fired.
|
|
var receive: [64]u8 = undefined;
|
|
var count: usize = 0;
|
|
while (count < target_ticks) {
|
|
// Blocked here. The task is `.blocked` and off every scheduler queue; the
|
|
// next line runs only because the HPET raised its line.
|
|
const r = ipc.replyWait(endpoint, &.{}, &receive, null);
|
|
if (!r.isNotification()) continue; // a client request, not our IRQ
|
|
|
|
// Quiet the device: write 1 to timer 0's status bit. Until this lands, the
|
|
// line is still asserted and unmasking would refire immediately.
|
|
register(base, register_int_status).* = 1;
|
|
count += 1;
|
|
|
|
if (count < target_ticks) {
|
|
register(base, register_timer0_comparator).* = register(base, register_main_counter).* + ticks_per_ms * 100;
|
|
} else {
|
|
// Last one: stop the source rather than re-arming, so the line is left
|
|
// both quiet *and* unmasked by the ack below. Re-arming here would leave
|
|
// a pending interrupt that nobody is waiting for, and the ISR would mask
|
|
// the line again a moment later.
|
|
register(base, register_timer0_configuration).* &= ~tn_int_enb;
|
|
}
|
|
|
|
_ = runtime.system.write("hpet: irq\n");
|
|
if (!device.irqAck(hpet.device_id, hpet.irq)) {
|
|
_ = runtime.system.write("hpet: irq_ack failed\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
_ = runtime.system.write("hpet: ok\n");
|
|
while (true) runtime.system.sleep(1000);
|
|
}
|
|
|
|
pub const panic = runtime.panic;
|
|
comptime {
|
|
_ = &runtime.start._start;
|
|
}
|