refactor kernel to use device platform discovery

This commit is contained in:
Daniel Samson
2026-07-08 10:06:46 +01:00
parent 2ee898a91e
commit 7d3417fe86
9 changed files with 572 additions and 43 deletions
+171 -15
View File
@@ -10,6 +10,30 @@
//! the LAPIC won't deliver the next one.
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 cfg_pic_present: bool = true;
var cfg_hpet_base: u64 = 0; // 0 = no HPET discovered
var cfg_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 {
cfg_pic_present = pic_present;
cfg_hpet_base = hpet_base;
cfg_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;
@@ -83,10 +107,11 @@ fn remapAndMaskPic() void {
io.outb(0xA1, 0xFF);
}
/// Enable the Local APIC: mask the PIC, set the global-enable MSR bit, and
/// 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 {
remapAndMaskPic();
if (cfg_pic_present) remapAndMaskPic();
const msr = io.rdmsr(ia32_apic_base_msr);
base = @intCast(msr & 0xFFFFF000); // physical base is bits 12+
@@ -95,23 +120,91 @@ pub fn init() void {
write(reg_spurious, 0x100 | spurious_vector); // bit 8 = software enable
}
/// Measure the LAPIC timer's and the TSC's rates against the PIT (channel 2, which
/// can be polled without interrupts). We run the LAPIC timer one-shot from its max
/// count and snapshot the TSC while the PIT counts out a known 10 ms, then see how
/// far each got. This gives real time, which the RTOS timing guarantees depend on.
/// 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 cfg_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 (cfg_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)
}
/// Run the LAPIC timer one-shot from its max 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(reg_timer_divide, timer_divide_16);
write(reg_lvt_timer, lvt_masked);
write(reg_timer_initial, 0xFFFFFFFF);
const ref0 = refNow();
const tsc0 = rdtsc();
while (((refNow() -% ref0) & ref_mask) < calib_ticks) {}
const tsc1 = rdtsc();
const elapsed = 0xFFFFFFFF - read(reg_timer_current);
write(reg_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 calib_ms = 10;
const pit_count: u16 = @intCast(pit_hz / 1000 * calib_ms);
// LAPIC timer: divide 16, masked (no interrupt — we just want the count),
// counting down from the maximum.
write(reg_timer_divide, timer_divide_16);
write(reg_lvt_timer, lvt_masked);
write(reg_timer_initial, 0xFFFFFFFF);
// PIT channel 2, mode 0 (interrupt on terminal count): load the count with the
// gate low, then raise the gate to start it counting.
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));
@@ -119,15 +212,78 @@ pub fn calibrate() void {
const tsc_start = rdtsc();
io.outb(0x61, (io.inb(0x61) & 0xFC) | 0x01); // gate high -> start
while (io.inb(0x61) & 0x20 == 0) {} // poll channel-2 output until terminal count
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(reg_timer_current);
write(reg_timer_initial, 0); // stop the timer
write(reg_timer_initial, 0);
ticks_per_ms = elapsed / calib_ms;
tsc_hz = (tsc_end -% tsc_start) * (1000 / calib_ms); // cycles/10ms -> cycles/s
tsc_base = rdtsc(); // the clock's zero point (boot)
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).
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;
}
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 config at +0x10, main counter at +0xF0.
fn hpetRead64(off: usize) u64 {
return @as(*volatile u64, @ptrFromInt(cfg_hpet_base + off)).*;
}
fn hpetWrite64(off: usize, value: u64) void {
@as(*volatile u64, @ptrFromInt(cfg_hpet_base + off)).* = value;
}
/// Map + enable the HPET and return its tick frequency, or null if unusable.
fn hpetHz() ?u64 {
paging.map(cfg_hpet_base & ~@as(u64, 0xFFF), cfg_hpet_base & ~@as(u64, 0xFFF), 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 = cfg_pm_timer.?;
if (pt.mmio) return @as(*volatile u32, @ptrFromInt(pt.address)).*;
return io.inl(@intCast(pt.address));
}
/// Arm the LAPIC timer to fire on `timer_vector` at `hz` (periodic). Requires