640 lines
25 KiB
Zig
640 lines
25 KiB
Zig
//! Local APIC and its timer — the source of device interrupts.
|
||
//!
|
||
//! Modern x86 routes interrupts through the per-CPU Local APIC (the legacy 8259
|
||
//! PIC is remapped out of the way and masked). The LAPIC also has a built-in
|
||
//! timer, which is the simplest device interrupt to bring up: it needs no
|
||
//! external routing, just a vector and a count. We use it as danos's heartbeat.
|
||
//!
|
||
//! The LAPIC is memory-mapped (default physical 0xFEE00000, inside our identity
|
||
//! map). Every interrupt must be acknowledged with an end-of-interrupt write, or
|
||
//! the LAPIC won't deliver the next one.
|
||
|
||
const boot_handoff = @import("boot-handoff");
|
||
const io = @import("io.zig");
|
||
const paging = @import("paging.zig");
|
||
|
||
/// The ACPI PM timer, as a calibration reference: an I/O port or MMIO counter.
|
||
pub const PmTimer = struct { mmio: bool, address: u64, is_32bit: bool };
|
||
|
||
// Platform facts from discovery (set by `configure` before bring-up). Defaults are
|
||
// the legacy-safe assumptions so the code still works if discovery never ran.
|
||
var configuration_pic_present: bool = true;
|
||
var configuration_hpet_base: u64 = 0; // 0 = no HPET discovered
|
||
var configuration_pm_timer: ?PmTimer = null;
|
||
/// Which reference the last calibration used, for logging.
|
||
var cal_source: []const u8 = "none";
|
||
|
||
/// Hand the LAPIC bring-up the discovered platform facts. Call before `init`.
|
||
pub fn configure(pic_present: bool, hpet_base: u64, pm_timer: ?PmTimer) void {
|
||
configuration_pic_present = pic_present;
|
||
configuration_hpet_base = hpet_base;
|
||
configuration_pm_timer = pm_timer;
|
||
}
|
||
|
||
/// The calibration reference the timer was measured against ("cpuid"/"hpet"/…).
|
||
pub fn calibrationSource() []const u8 {
|
||
return cal_source;
|
||
}
|
||
|
||
/// IDT vector the timer fires on (in the device range, >= 32).
|
||
pub const timer_vector = 32;
|
||
/// Spurious-interrupt vector. Low nibble 0xF by convention; also in our gate
|
||
/// range so a stray spurious interrupt lands on a valid (no-op) handler.
|
||
const spurious_vector = 47;
|
||
|
||
// LAPIC register offsets.
|
||
const register_spurious = 0x0F0;
|
||
const register_eoi = 0x0B0;
|
||
const register_id = 0x020; // this core's LAPIC id, in bits 24-31
|
||
const register_icr_low = 0x300; // interrupt command register, low dword (writing it sends)
|
||
const register_icr_high = 0x310; // ICR high dword (destination APIC id in bits 24-31)
|
||
const register_lvt_timer = 0x320;
|
||
const register_timer_initial = 0x380;
|
||
const register_timer_current = 0x390;
|
||
const register_timer_divide = 0x3E0;
|
||
|
||
const icr_delivery_pending = 1 << 12; // ICR low bit 12: a previous IPI is still in flight
|
||
|
||
const lvt_masked = 1 << 16;
|
||
const lvt_periodic = 1 << 17;
|
||
const timer_divide_16 = 0x3;
|
||
|
||
const ia32_apic_base_msr = 0x1B;
|
||
|
||
/// LAPIC MMIO base. A runtime var (not a constant) both because we read it from
|
||
/// the MSR and so register writes compile to normal stores rather than a
|
||
/// `mov moffs`, which the self-hosted backend can't encode.
|
||
var base: usize = 0xFEE00000;
|
||
|
||
var tick_count: u64 = 0;
|
||
|
||
/// LAPIC timer counts per millisecond, measured against the PIT (see calibrate).
|
||
/// At divide-by-16, this is the effective counting rate.
|
||
var ticks_per_ms: u32 = 0;
|
||
/// The periodic-interrupt frequency the timer is armed at, once initTimer runs.
|
||
var timer_hz: u32 = 0;
|
||
|
||
/// TSC (Time Stamp Counter) calibration: cycles per second, and the count at boot.
|
||
/// The TSC is a per-core cycle counter, giving a ~nanosecond high-resolution
|
||
/// monotonic clock — far finer than the millisecond timer tick.
|
||
var tsc_hz: u64 = 0;
|
||
var tsc_base: u64 = 0;
|
||
|
||
/// Whether the TSC is architecturally **invariant** — a constant rate regardless of
|
||
/// P/C-state transitions, and thus valid as a clocksource (CPUID leaf 0x80000007,
|
||
/// EDX bit 8). AMD and modern Intel set it; the bare qemu64 model does not. Measured
|
||
/// frequency alone is not enough: a non-invariant TSC speeds up and slows down with
|
||
/// the core clock, so reading it as wall time would drift.
|
||
var tsc_invariant: bool = false;
|
||
/// Cleared if the cross-core warp check (checkWarpSource) ever sees the TSC read
|
||
/// lower on one core than the max another core has already published — i.e. the
|
||
/// per-core TSCs are not synchronized, and a task migrating cores could see time go
|
||
/// backward. Starts true (assume synchronized until proven otherwise).
|
||
var tsc_synced: bool = true;
|
||
/// The worst backward skew the warp check observed, in TSC cycles (0 = none).
|
||
var tsc_warp_cycles: u64 = 0;
|
||
|
||
/// The monotonic clock's source. The TSC when it is invariant *and* synchronized —
|
||
/// the fast `rdtsc` path taken on real Intel/AMD and modern VMs. Otherwise the HPET
|
||
/// main counter: a single fixed-rate counter, immune to both per-core skew and
|
||
/// frequency scaling, so it stays accurate on a bare VM or a warped machine.
|
||
const ClockSource = enum { tsc, hpet };
|
||
var clock_source: ClockSource = .tsc;
|
||
|
||
/// HPET standby clocksource, set up in calibrate() whenever an HPET exists (whether
|
||
/// or not calibration itself measured against it): its frequency, the counter value
|
||
/// chosen as the zero point, and its width mask. Only a 64-bit HPET is used as a
|
||
/// clocksource — a 32-bit one wraps too fast to be monotonic without accumulation.
|
||
var hpet_clock_hz: u64 = 0;
|
||
var hpet_clock_base: u64 = 0;
|
||
var hpet_clock_mask: u64 = ~@as(u64, 0);
|
||
|
||
/// Read the 64-bit Time Stamp Counter.
|
||
fn rdtsc() u64 {
|
||
var low: u32 = undefined;
|
||
var high: u32 = undefined;
|
||
asm volatile ("rdtsc"
|
||
: [low] "={eax}" (low),
|
||
[high] "={edx}" (high),
|
||
);
|
||
return (@as(u64, high) << 32) | low;
|
||
}
|
||
|
||
fn read(register: u32) u32 {
|
||
return @as(*volatile u32, @ptrFromInt(base + register)).*;
|
||
}
|
||
fn write(register: u32, value: u32) void {
|
||
@as(*volatile u32, @ptrFromInt(base + register)).* = value;
|
||
}
|
||
|
||
/// Move the legacy 8259 PIC's vectors to 0x20-0x2F (clear of the CPU exception
|
||
/// vectors) and mask every line, so it can't deliver interrupts behind the APIC.
|
||
fn remapAndMaskPic() void {
|
||
io.outb(0x20, 0x11); // start init (cascade mode)
|
||
io.outb(0xA0, 0x11);
|
||
io.outb(0x21, 0x20); // master offset 0x20
|
||
io.outb(0xA1, 0x28); // slave offset 0x28
|
||
io.outb(0x21, 0x04); // tell master about slave on IRQ2
|
||
io.outb(0xA1, 0x02);
|
||
io.outb(0x21, 0x01); // 8086 mode
|
||
io.outb(0xA1, 0x01);
|
||
io.outb(0x21, 0xFF); // mask all
|
||
io.outb(0xA1, 0xFF);
|
||
}
|
||
|
||
/// Enable the Local APIC: mask the PIC (only if one is present — a legacy-free
|
||
/// UEFI Class 3 machine may have none), set the global-enable MSR bit, and
|
||
/// software-enable the APIC via its spurious-vector register.
|
||
pub fn init() void {
|
||
if (configuration_pic_present) remapAndMaskPic();
|
||
|
||
const msr = io.rdmsr(ia32_apic_base_msr);
|
||
// Reach the LAPIC through the physmap (paging.init maps its page there).
|
||
base = @intCast(boot_handoff.physicalToVirtual(msr & 0xFFFFF000)); // physical base is bits 12+
|
||
io.wrmsr(ia32_apic_base_msr, msr | (1 << 11)); // global enable
|
||
|
||
write(register_spurious, 0x100 | spurious_vector); // bit 8 = software enable
|
||
}
|
||
|
||
/// Software-enable *this* core's Local APIC — the application-processor counterpart
|
||
/// of `init`, minus the one-time PIC remap (the BSP already masked it) and minus
|
||
/// calibration (the timer rate is a shared hardware constant, measured once). Each
|
||
/// core has its own LAPIC at the same MMIO address, so no per-core base is needed.
|
||
pub fn initSecondary() void {
|
||
const msr = io.rdmsr(ia32_apic_base_msr);
|
||
io.wrmsr(ia32_apic_base_msr, msr | (1 << 11)); // global enable
|
||
write(register_spurious, 0x100 | spurious_vector); // software enable
|
||
}
|
||
|
||
// --- application-processor wakeup (INIT–SIPI–SIPI) --------------------------
|
||
|
||
/// Send an INIT IPI to the core with Local APIC id `apic_id` — the first step of
|
||
/// the wake sequence. Blocks until the LAPIC reports the IPI was delivered.
|
||
pub fn sendInit(apic_id: u32) void {
|
||
write(register_icr_high, apic_id << 24);
|
||
write(register_icr_low, 0x4500); // INIT, physical destination, assert, edge-triggered
|
||
waitIcrIdle();
|
||
}
|
||
|
||
/// Send a STARTUP IPI (SIPI) telling the target core to begin executing at physical
|
||
/// address `vector << 12` (in real mode). Per the Intel bring-up protocol this is
|
||
/// sent twice after the INIT; both calls block until delivery completes.
|
||
pub fn sendStartup(apic_id: u32, vector: u8) void {
|
||
write(register_icr_high, apic_id << 24);
|
||
write(register_icr_low, 0x4600 | @as(u32, vector)); // STARTUP with the page vector
|
||
waitIcrIdle();
|
||
}
|
||
|
||
fn waitIcrIdle() void {
|
||
while (read(register_icr_low) & icr_delivery_pending != 0) {}
|
||
}
|
||
|
||
/// Send this core a fixed interrupt at `vector` (the "self" destination shorthand). A
|
||
/// device raises an MSI by writing its (address, data) to the LAPIC; with no such
|
||
/// device on QEMU's HPET, a self-IPI is the stand-in that lets the MSI vector-routing
|
||
/// path be tested end to end. Shorthand self (bits 19:18 = 01) | assert (bit 14).
|
||
pub fn selfIpi(vector: u8) void {
|
||
write(register_icr_low, 0x4_4000 | @as(u32, vector));
|
||
waitIcrIdle();
|
||
}
|
||
|
||
/// The calibration window: we time everything against a 10 ms reference interval.
|
||
const calib_ms = 10;
|
||
|
||
/// Measure the LAPIC timer's and the TSC's rates. The PIT (legacy 8254) can be
|
||
/// absent on UEFI Class 3 firmware — and polling it would hang — so we pick a
|
||
/// reference clock in order of preference: the CPU's own TSC frequency (CPUID leaf
|
||
/// 0x15, no external timer needed), then the discovered HPET, then the ACPI PM
|
||
/// timer, and only the PIT as a last resort. Each path yields the same two rates.
|
||
pub fn calibrate() void {
|
||
var done = false;
|
||
|
||
// 1. CPUID leaf 0x15 gives the TSC frequency directly — measure the LAPIC
|
||
// against the TSC itself, needing no external timer at all.
|
||
if (cpuidTscHz()) |hz| {
|
||
measure(hz, ~@as(u64, 0), rdtsc);
|
||
tsc_hz = hz; // keep the exact enumerated value
|
||
cal_source = "cpuid";
|
||
done = true;
|
||
}
|
||
|
||
// 2. The discovered HPET.
|
||
if (!done and configuration_hpet_base != 0) {
|
||
if (hpetHz()) |hpet_hz| {
|
||
measure(hpet_hz, hpetMask(), readHpet);
|
||
cal_source = "hpet";
|
||
done = true;
|
||
}
|
||
}
|
||
|
||
// 3. The ACPI PM timer (fixed 3.579545 MHz).
|
||
if (!done) {
|
||
if (configuration_pm_timer) |pt| {
|
||
measure(3_579_545, if (pt.is_32bit) 0xFFFF_FFFF else 0xFF_FFFF, readPmTimer);
|
||
cal_source = "pm-timer";
|
||
done = true;
|
||
}
|
||
}
|
||
|
||
// 4. The legacy PIT, last resort.
|
||
if (!done) {
|
||
calibratePit();
|
||
cal_source = "pit";
|
||
}
|
||
|
||
// A bad measurement (no reference actually ticked) leaves nonsense; fall back.
|
||
if (ticks_per_ms == 0 or tsc_hz == 0) {
|
||
calibratePit();
|
||
cal_source = "pit";
|
||
}
|
||
|
||
tsc_base = rdtsc(); // the clock's zero point (boot)
|
||
|
||
// Decide whether the TSC is trustworthy as a clocksource. Frequency (measured
|
||
// above, possibly against the HPET/PIT) is necessary but not sufficient: the TSC
|
||
// must also be *invariant* (CPUID 0x80000007 EDX[8]). AMD and modern Intel set
|
||
// this; the bare qemu64 model does not.
|
||
tsc_invariant = tscIsInvariant();
|
||
|
||
// Bring up the HPET as a standby clocksource whenever one exists — even on the
|
||
// CPUID-0x15 path where calibration never touched it — so a non-invariant TSC
|
||
// (here) or an unsynchronized one (checkWarpSource, during SMP bring-up) can fall
|
||
// back to a source that is immune to both. hpetHz() maps + enables the counter
|
||
// and is idempotent if calibration already used it.
|
||
if (configuration_hpet_base != 0) {
|
||
if (hpetHz()) |hz| {
|
||
hpet_clock_mask = hpetMask();
|
||
if (hpet_clock_mask == ~@as(u64, 0)) { // only a 64-bit HPET is monotonic enough
|
||
hpet_clock_hz = hz;
|
||
hpet_clock_base = readHpet();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Select the source: the fast TSC when invariant, else the HPET if we have one.
|
||
// (checkWarpSource may still demote TSC -> HPET later if the cores' TSCs skew.)
|
||
if (!tsc_invariant and hpet_clock_hz != 0) clock_source = .hpet;
|
||
}
|
||
|
||
/// Run the LAPIC timer one-shot from its maximum count while a monotonic reference
|
||
/// clock (frequency `ref_hz`, counter width `ref_mask`) counts out `calib_ms`, and
|
||
/// snapshot the TSC across the same window. Yields `ticks_per_ms` and `tsc_hz`.
|
||
fn measure(ref_hz: u64, ref_mask: u64, refNow: *const fn () u64) void {
|
||
const calib_ticks = ref_hz / (1000 / calib_ms); // reference ticks in calib_ms
|
||
|
||
write(register_timer_divide, timer_divide_16);
|
||
write(register_lvt_timer, lvt_masked);
|
||
write(register_timer_initial, 0xFFFFFFFF);
|
||
|
||
const ref0 = refNow();
|
||
const tsc0 = rdtsc();
|
||
while (((refNow() -% ref0) & ref_mask) < calib_ticks) {}
|
||
const tsc1 = rdtsc();
|
||
|
||
const elapsed = 0xFFFFFFFF - read(register_timer_current);
|
||
write(register_timer_initial, 0);
|
||
|
||
ticks_per_ms = elapsed / calib_ms;
|
||
tsc_hz = (tsc1 -% tsc0) * (1000 / calib_ms);
|
||
}
|
||
|
||
/// The PIT fallback (legacy 8254 channel 2, polled). Only reached when no better
|
||
/// reference exists — on a legacy-free machine this path isn't taken.
|
||
fn calibratePit() void {
|
||
const pit_hz = 1_193_182;
|
||
const pit_count: u16 = @intCast(pit_hz / 1000 * calib_ms);
|
||
|
||
write(register_timer_divide, timer_divide_16);
|
||
write(register_lvt_timer, lvt_masked);
|
||
write(register_timer_initial, 0xFFFFFFFF);
|
||
|
||
io.outb(0x61, io.inb(0x61) & 0xFC); // speaker off, gate low
|
||
io.outb(0x43, 0xB0); // channel 2, lo/hi byte, mode 0
|
||
io.outb(0x42, @truncate(pit_count));
|
||
io.outb(0x42, @truncate(pit_count >> 8));
|
||
|
||
const tsc_start = rdtsc();
|
||
io.outb(0x61, (io.inb(0x61) & 0xFC) | 0x01); // gate high -> start
|
||
var guard: u64 = 0;
|
||
while (io.inb(0x61) & 0x20 == 0 and guard < 100_000_000) : (guard += 1) {} // bounded
|
||
const tsc_end = rdtsc();
|
||
|
||
const elapsed = 0xFFFFFFFF - read(register_timer_current);
|
||
write(register_timer_initial, 0);
|
||
|
||
ticks_per_ms = elapsed / calib_ms;
|
||
tsc_hz = (tsc_end -% tsc_start) * (1000 / calib_ms);
|
||
}
|
||
|
||
// --- reference clocks ------------------------------------------------------
|
||
|
||
/// TSC frequency from CPUID leaf 0x15 (crystal_hz * numerator / denominator), or
|
||
/// null if the CPU doesn't enumerate it (common under QEMU, and on AMD).
|
||
fn cpuidTscHz() ?u64 {
|
||
if (cpuid(0).eax < 0x15) return null;
|
||
const r = cpuid(0x15);
|
||
if (r.eax == 0 or r.ebx == 0 or r.ecx == 0) return null; // ratio/crystal not given
|
||
return @as(u64, r.ecx) * r.ebx / r.eax;
|
||
}
|
||
|
||
/// Whether the CPU advertises an **invariant** TSC (CPUID leaf 0x80000007, EDX
|
||
/// bit 8) — the architectural guarantee, on both Intel and AMD, that the TSC ticks
|
||
/// at a constant rate across P/C-states and never stops. Requires the extended-leaf
|
||
/// range to reach 0x80000007 first.
|
||
fn tscIsInvariant() bool {
|
||
if (cpuid(0x80000000).eax < 0x80000007) return false;
|
||
return (cpuid(0x80000007).edx & (1 << 8)) != 0;
|
||
}
|
||
|
||
const CpuidRegs = struct { eax: u32, ebx: u32, ecx: u32, edx: u32 };
|
||
|
||
fn cpuid(leaf: u32) CpuidRegs {
|
||
var a: u32 = undefined;
|
||
var b: u32 = undefined;
|
||
var c: u32 = undefined;
|
||
var d: u32 = undefined;
|
||
asm volatile ("cpuid"
|
||
: [a] "={eax}" (a),
|
||
[b] "={ebx}" (b),
|
||
[c] "={ecx}" (c),
|
||
[d] "={edx}" (d),
|
||
: [leaf] "{eax}" (leaf),
|
||
[sub] "{ecx}" (@as(u32, 0)),
|
||
);
|
||
return .{ .eax = a, .ebx = b, .ecx = c, .edx = d };
|
||
}
|
||
|
||
// HPET registers: capabilities at +0x00 (period in the high dword, in fs; bit 13 =
|
||
// 64-bit-counter capable), general configuration at +0x10, main counter at +0xF0.
|
||
fn hpetRead64(off: usize) u64 {
|
||
return @as(*volatile u64, @ptrFromInt(configuration_hpet_base + off)).*;
|
||
}
|
||
fn hpetWrite64(off: usize, value: u64) void {
|
||
@as(*volatile u64, @ptrFromInt(configuration_hpet_base + off)).* = value;
|
||
}
|
||
|
||
/// Whether the HPET has been mapped into the physmap yet, so `configuration_hpet_base`
|
||
/// already holds the virtual address. `hpetHz` is called more than once (calibration
|
||
/// may use the HPET, and the standby-clocksource setup asks for it again), and mapping
|
||
/// an already-mapped base a second time would double-offset it into an overflow.
|
||
var hpet_mapped: bool = false;
|
||
|
||
/// Map + enable the HPET and return its tick frequency, or null if unusable.
|
||
/// Maps the HPET into the physmap and switches configuration_hpet_base to that virtual
|
||
/// address, so the register accessors reach it without the identity map. Idempotent.
|
||
fn hpetHz() ?u64 {
|
||
if (!hpet_mapped) {
|
||
configuration_hpet_base = paging.mapMmio(configuration_hpet_base, 0x400, true);
|
||
hpet_mapped = true;
|
||
}
|
||
const caps = hpetRead64(0x00);
|
||
const period_fs = caps >> 32; // femtoseconds per tick
|
||
if (period_fs == 0) return null;
|
||
hpetWrite64(0x10, hpetRead64(0x10) | 1); // ENABLE_CNF: start the main counter
|
||
return 1_000_000_000_000_000 / period_fs; // 1e15 fs/s ÷ fs/tick
|
||
}
|
||
|
||
/// The HPET counter width mask (64- or 32-bit, per caps bit 13).
|
||
fn hpetMask() u64 {
|
||
return if (hpetRead64(0x00) & (1 << 13) != 0) ~@as(u64, 0) else 0xFFFF_FFFF;
|
||
}
|
||
|
||
fn readHpet() u64 {
|
||
return hpetRead64(0xF0);
|
||
}
|
||
|
||
fn readPmTimer() u64 {
|
||
const pt = configuration_pm_timer.?;
|
||
// MMIO PM timer via the physmap (mapMmio is idempotent); the common case is
|
||
// a legacy I/O port.
|
||
if (pt.mmio) return @as(*volatile u32, @ptrFromInt(paging.mapMmio(pt.address, 4, false))).*;
|
||
return io.inl(@intCast(pt.address));
|
||
}
|
||
|
||
/// Arm the LAPIC timer to fire on `timer_vector` at `hz` (periodic). Requires
|
||
/// calibrate() to have run.
|
||
pub fn initTimer(hz: u32) void {
|
||
timer_hz = hz;
|
||
const count = @as(u64, ticks_per_ms) * 1000 / hz; // counts per (1/hz) second
|
||
write(register_timer_divide, timer_divide_16);
|
||
write(register_lvt_timer, timer_vector | lvt_periodic);
|
||
write(register_timer_initial, @intCast(count));
|
||
}
|
||
|
||
/// Configured periodic-interrupt frequency (Hz).
|
||
pub fn frequencyHz() u32 {
|
||
return timer_hz;
|
||
}
|
||
|
||
/// Measured LAPIC timer frequency (Hz), for reporting/sanity checks.
|
||
pub fn lapicHz() u64 {
|
||
return @as(u64, ticks_per_ms) * 1000;
|
||
}
|
||
|
||
/// Measured TSC frequency (Hz).
|
||
pub fn tscHz() u64 {
|
||
return tsc_hz;
|
||
}
|
||
|
||
// Monotonic high-resolution clock. A function per resolution, each scaling the
|
||
// counter delta directly at its unit (the 128-bit intermediate avoids overflow
|
||
// across a long uptime). nanos() resolves to a few ns on the TSC; millis() is what
|
||
// the scheduler uses for sleep deadlines. The source is the TSC when it is invariant
|
||
// and synchronized, else the HPET counter (see clock_source) — the branch is one
|
||
// global load and the TSC path is unchanged from before.
|
||
|
||
/// The selected source's counter delta since its zero point.
|
||
fn clockCount() u64 {
|
||
return switch (clock_source) {
|
||
.tsc => rdtsc() -% tsc_base,
|
||
// A 64-bit HPET (the only kind we select) never wraps in any realistic
|
||
// uptime, so the wrapping subtraction is exact.
|
||
.hpet => readHpet() -% hpet_clock_base,
|
||
};
|
||
}
|
||
|
||
/// The selected source's frequency (0 if the clock is unavailable/uncalibrated).
|
||
fn clockHertz() u64 {
|
||
return switch (clock_source) {
|
||
.tsc => tsc_hz,
|
||
.hpet => hpet_clock_hz,
|
||
};
|
||
}
|
||
|
||
pub fn nanos() u64 {
|
||
const hz = clockHertz();
|
||
if (hz == 0) return 0;
|
||
return @intCast(@as(u128, clockCount()) * 1_000_000_000 / hz);
|
||
}
|
||
|
||
pub fn micros() u64 {
|
||
const hz = clockHertz();
|
||
if (hz == 0) return 0;
|
||
return @intCast(@as(u128, clockCount()) * 1_000_000 / hz);
|
||
}
|
||
|
||
pub fn millis() u64 {
|
||
const hz = clockHertz();
|
||
if (hz == 0) return 0;
|
||
return @intCast(@as(u128, clockCount()) * 1_000 / hz);
|
||
}
|
||
|
||
/// Whether the CPU advertises an invariant TSC (CPUID 0x80000007 EDX[8]).
|
||
pub fn tscInvariant() bool {
|
||
return tsc_invariant;
|
||
}
|
||
|
||
/// Test hook: force the TSC clocksource on, as if the CPU had advertised an invariant
|
||
/// TSC. QEMU's TCG accelerator (the only one for an x86 guest on an Apple-Silicon
|
||
/// host) does not expose the invariant-TSC bit — its emulated TSC isn't invariant — so
|
||
/// the tsc-sync test can't reach the real-Intel/AMD/KVM path through CPUID. This lets
|
||
/// that test exercise the TSC clocksource and the cross-core warp check anyway. tsc_base
|
||
/// is left as-is so the switch from the HPET is continuous.
|
||
pub fn forceTscClocksourceForTest() void {
|
||
tsc_invariant = true;
|
||
clock_source = .tsc;
|
||
}
|
||
|
||
/// How many per-AP warp checks actually ran (a rendezvous completed) — lets a test
|
||
/// confirm the cross-core check executed rather than being skipped.
|
||
pub fn warpChecksRun() u32 {
|
||
return warp_checks;
|
||
}
|
||
|
||
/// Whether the per-core TSCs are synchronized (no backward warp seen at bring-up).
|
||
pub fn tscSynced() bool {
|
||
return tsc_synced;
|
||
}
|
||
|
||
/// The active monotonic clocksource, for the boot log and tests.
|
||
pub fn clockSourceName() []const u8 {
|
||
return switch (clock_source) {
|
||
.tsc => "tsc",
|
||
.hpet => "hpet",
|
||
};
|
||
}
|
||
|
||
// --- cross-core TSC synchronization ("warp") check -------------------------
|
||
// Two cores hammer a shared "max seen" TSC value under a lock; if either reads a
|
||
// value below that max, its TSC lags the other's, and time would run backward for a
|
||
// task migrating between them (Linux calls this a warp). danos brings APs up one at a
|
||
// time, so this runs pairwise: the BSP (source) against each AP (target) as it comes
|
||
// online. It only matters — and only runs — while the TSC is the clocksource; on a
|
||
// machine already on the HPET (a bare VM) the whole rendezvous is skipped.
|
||
|
||
var warp_lock: u32 = 0;
|
||
var warp_last: u64 = 0;
|
||
var warp_bsp_ready: u32 = 0;
|
||
var warp_ap_ready: u32 = 0;
|
||
var warp_stop: u32 = 0;
|
||
var warp_checks: u32 = 0; // completed per-AP rendezvous count (for the tsc-sync test)
|
||
|
||
const warp_rounds: u32 = 1 << 20; // locked reads on the BSP: ~1 ms at GHz rates
|
||
const warp_spin_limit: u64 = 1 << 32; // bound every rendezvous wait so a lost core can't hang boot
|
||
|
||
fn warpTick() void {
|
||
while (@cmpxchgWeak(u32, &warp_lock, 0, 1, .acquire, .monotonic) != null) asm volatile ("pause");
|
||
const t = rdtsc();
|
||
if (t < warp_last) {
|
||
const delta = warp_last - t;
|
||
if (delta > tsc_warp_cycles) tsc_warp_cycles = delta;
|
||
tsc_synced = false;
|
||
} else {
|
||
warp_last = t;
|
||
}
|
||
@atomicStore(u32, &warp_lock, 0, .release);
|
||
}
|
||
|
||
/// Spin (bounded) until `flag` is nonzero; false on timeout.
|
||
fn warpAwait(flag: *u32) bool {
|
||
var spins: u64 = 0;
|
||
while (@atomicLoad(u32, flag, .acquire) == 0) : (spins += 1) {
|
||
if (spins >= warp_spin_limit) return false;
|
||
asm volatile ("pause");
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/// BSP side of the pairwise TSC warp check, run once per AP as it reports in. No-op
|
||
/// unless the TSC is the active clocksource. If the AP's TSC proves to lag, demote
|
||
/// the monotonic clock to the HPET without a discontinuity.
|
||
pub fn checkWarpSource() void {
|
||
if (clock_source != .tsc) return;
|
||
warp_last = 0;
|
||
@atomicStore(u32, &warp_stop, 0, .release);
|
||
@atomicStore(u32, &warp_ap_ready, 0, .release);
|
||
@atomicStore(u32, &warp_bsp_ready, 1, .release);
|
||
if (!warpAwait(&warp_ap_ready)) { // AP never joined the rendezvous; skip, don't hang
|
||
@atomicStore(u32, &warp_bsp_ready, 0, .release);
|
||
return;
|
||
}
|
||
var i: u32 = 0;
|
||
while (i < warp_rounds) : (i += 1) warpTick();
|
||
@atomicStore(u32, &warp_stop, 1, .release);
|
||
@atomicStore(u32, &warp_bsp_ready, 0, .release);
|
||
warp_checks += 1;
|
||
|
||
if (!tsc_synced and hpet_clock_hz != 0) demoteToHpet();
|
||
}
|
||
|
||
/// AP side: join the BSP's warp check, then return so the core can enter the
|
||
/// scheduler. Bounded so a missing BSP can't strand the core.
|
||
pub fn checkWarpTarget() void {
|
||
if (clock_source != .tsc) return;
|
||
if (!warpAwait(&warp_bsp_ready)) return;
|
||
@atomicStore(u32, &warp_ap_ready, 1, .release);
|
||
var spins: u64 = 0;
|
||
while (@atomicLoad(u32, &warp_stop, .acquire) == 0) : (spins += 1) {
|
||
if (spins >= warp_spin_limit) return;
|
||
warpTick();
|
||
}
|
||
}
|
||
|
||
/// Switch the clocksource from the TSC to the HPET without a discontinuity: choose
|
||
/// the HPET zero point so it reads the same nanosecond value the TSC does right now,
|
||
/// so time neither jumps nor runs backward across the switch. Called when the warp
|
||
/// check proves the per-core TSCs unsynchronized.
|
||
fn demoteToHpet() void {
|
||
const now_ns = @as(u128, rdtsc() -% tsc_base) * 1_000_000_000 / tsc_hz;
|
||
const equivalent_ticks: u64 = @intCast(now_ns * hpet_clock_hz / 1_000_000_000);
|
||
hpet_clock_base = readHpet() -% equivalent_ticks;
|
||
clock_source = .hpet;
|
||
}
|
||
|
||
/// Acknowledge the current interrupt so the LAPIC will deliver the next one.
|
||
pub fn eoi() void {
|
||
write(register_eoi, 0);
|
||
}
|
||
|
||
/// Optional callback run each tick (the scheduler registers it for preemption).
|
||
var on_tick: ?*const fn () void = null;
|
||
|
||
pub fn setTickHook(hook: *const fn () void) void {
|
||
on_tick = hook;
|
||
}
|
||
|
||
/// The timer interrupt handler: advance the monotonic tick count, then run the
|
||
/// tick hook (which may switch tasks). The interrupt is already acknowledged by
|
||
/// the dispatcher before we get here, so a task switch here doesn't stall it.
|
||
pub fn timerTick() void {
|
||
// Acknowledge before the tick hook: `on_tick` is the scheduler, which may switch
|
||
// tasks and not return promptly, and the LAPIC mustn't wait on it to deliver the
|
||
// next interrupt. (Each device handler now owns its own EOI — see
|
||
// `idt.interruptDispatch` — because a *routed* interrupt must be masked at the
|
||
// I/O APIC before it is acknowledged, an ordering the dispatcher can't impose.)
|
||
eoi();
|
||
tick_count +%= 1;
|
||
if (on_tick) |hook| hook();
|
||
}
|
||
|
||
/// This core's Local APIC id — the interrupt destination for `routeGsi`.
|
||
pub fn localId() u8 {
|
||
return @truncate(read(register_id) >> 24);
|
||
}
|
||
|
||
/// Number of timer ticks so far. Volatile load: the count is bumped
|
||
/// asynchronously by the interrupt handler, so callers must re-read memory.
|
||
pub fn ticks() u64 {
|
||
return @as(*const volatile u64, &tick_count).*;
|
||
}
|