//! /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 mmio = @import("mmio"); 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; /// Read/write a 64-bit HPET register through the typed volatile MMIO layer (/lib/mmio). /// The HPET is pure MMIO with no DMA, and on x86 its grant is strong-uncacheable (so /// UC writes are already ordered) — no barriers are needed here; the point is the /// typed, arch-portable access every driver should use. inline fn rd(base: usize, off: usize) u64 { return mmio.read(u64, base + off); } inline fn wr(base: usize, off: usize, value: u64) void { mmio.write(u64, base + off, value); } /// 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_index: ?u64 = null; var irq: ?u64 = null; for (0..d.resource_count) |j| { switch (d.resources[j].kind) { @intFromEnum(device.ResourceKind.memory) => mmio_index = mmio_index orelse j, @intFromEnum(device.ResourceKind.irq) => irq = irq orelse j, else => {}, } } if (mmio_index) |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.createIpcEndpoint() orelse { _ = runtime.system.write("hpet: create_ipc_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 = rd(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. wr(base, register_general_configuration, rd(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 = rd(base, register_timer0_configuration); t0 &= ~(tn_route_mask | tn_type_periodic); t0 |= tn_int_type_level | tn_int_enb | (gsi << tn_route_shift); wr(base, register_timer0_configuration, t0); // Clear any stale assertion, then arm ~100 ms out and start the counter. wr(base, register_int_status, 1); wr(base, register_timer0_comparator, rd(base, register_main_counter) + ticks_per_ms * 100); wr(base, register_general_configuration, rd(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. wr(base, register_int_status, 1); count += 1; if (count < target_ticks) { wr(base, register_timer0_comparator, rd(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. wr(base, register_timer0_configuration, rd(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; }