Two driver-model milestones plus a tree-wide naming pass. Suite 35/35 (QEMU) + host tests green. M11 — IRQ-as-IPC. A ring-3 driver now sleeps until its device interrupts it. New src/kernel/irq.zig: per-GSI endpoint bindings, comptime per-vector trampolines, dispatch = mask GSI -> LAPIC EOI -> notifyLocked, all under one lock region. irq_bind/irq_ack syscalls, gated by the device claim like mmio_map. interruptDispatch no longer EOIs — each handler owns its EOI, because a level line must be masked before it is acknowledged (irq_ack is the unmask). Bindings are keyed on the owning task and released on exit (a shared endpoint's siblings survive). hpetd rewritten interrupt-driven. Tests: hpet (rewritten, reads back the I/O APIC routing) and irqfree. M12 — bus drivers. DeviceDesc gains a parent, making the device table a tree. dev_register (device_register) lets a process publish children below a device it claimed; the kernel enforces resource containment (a child's resources must nest in its parent's), so a descriptor can't fabricate a window over kernel RAM. Descriptor copied in via copyFromUser (physmap walk — an unmapped user pointer fails the call instead of faulting the kernel). Per-parent child cap bounds table exhaustion. sbin/busd.zig is a worked bus driver. Test: bus. Naming — per docs/coding-standards.md: non-acronym abbreviations spelled out (message, descriptor, device_service, scheduler, runtime, physical, interpreter, ...); acronyms kept (IPC, MMIO, DMA, HCD, ...); files are kebab-case (ipc-synchronous.zig, device-service.zig, vfs-protocol.zig, ...). Exceptions: POSIX/C ABI names and Zig idioms (init/len/ptr) kept. Module collisions resolved by specific naming (config -> parameters, device.zig alias -> device_model). AML op/Op disambiguated: op = opcode, Op = operation; per-opcode parse handlers renamed opX -> parseX. New driver docs: drivers.md, driver-model.md (bus/class/HCD shapes + the proposed M13–M16 ABI), coding-standards.md.
188 lines
7.9 KiB
Zig
188 lines
7.9 KiB
Zig
//! /sbin/hpetd — 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
|
|
//! hpetd wake, clear GENERAL_INT_STATUS (deasserts the line), re-arm
|
|
//! hpetd 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 sbin/busd.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("hpetd: out of memory\n");
|
|
return;
|
|
};
|
|
|
|
const hpet = findHpet(buffer) orelse {
|
|
_ = runtime.system.write("hpetd: no HPET with an IRQ\n");
|
|
return;
|
|
};
|
|
|
|
if (!device.claim(hpet.device_id)) {
|
|
_ = runtime.system.write("hpetd: claim failed\n");
|
|
return;
|
|
}
|
|
const base = device.mmioMap(hpet.device_id, hpet.mmio) orelse {
|
|
_ = runtime.system.write("hpetd: 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("hpetd: 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("hpetd: 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("hpetd: irq_bind failed\n");
|
|
return;
|
|
}
|
|
_ = runtime.system.write("hpetd: 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);
|
|
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("hpetd: irq\n");
|
|
if (!device.irqAck(hpet.device_id, hpet.irq)) {
|
|
_ = runtime.system.write("hpetd: irq_ack failed\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
_ = runtime.system.write("hpetd: ok\n");
|
|
while (true) runtime.system.sleep(1000);
|
|
}
|
|
|
|
pub const panic = runtime.panic;
|
|
comptime {
|
|
_ = &runtime.start._start;
|
|
}
|