refactor kernel to use device platform discovery
This commit is contained in:
@@ -41,12 +41,24 @@ its own, forever.
|
||||
|
||||
The reload count isn't picked arbitrarily — it's **calibrated to real time**,
|
||||
which the [real-time](vision.md) scheduling guarantees depend on. Since the LAPIC
|
||||
timer's raw rate is bus-clock dependent and unknown up front, `calibrate` measures
|
||||
it against the **PIT** (the legacy 8254, whose 1.193182 MHz is fixed): run the
|
||||
LAPIC timer one-shot from its maximum count while the PIT counts out a known 10 ms
|
||||
(polling channel 2, no interrupt needed), then see how far the LAPIC got. That
|
||||
yields its counts-per-millisecond, from which `initTimer(hz)` computes the reload
|
||||
count for any target frequency. danos runs it at **1000 Hz** (a 1 ms tick).
|
||||
timer's raw rate is bus-clock dependent and unknown up front, `calibrate` runs the
|
||||
LAPIC timer one-shot from its maximum count while a **reference clock** counts out a
|
||||
known 10 ms, then sees how far the LAPIC got — its counts-per-millisecond, from which
|
||||
`initTimer(hz)` computes the reload count for any target frequency. danos runs it at
|
||||
**1000 Hz** (a 1 ms tick).
|
||||
|
||||
The reference clock is chosen in order of preference, so danos calibrates on
|
||||
legacy-free **UEFI Class 3** hardware where the old 8254 PIT may be *absent* (polling
|
||||
a missing PIT would hang the boot):
|
||||
|
||||
1. **CPUID leaf 0x15** — the CPU's TSC frequency directly, needing no external timer
|
||||
at all (the LAPIC is then measured against the TSC).
|
||||
2. The **HPET**, discovered via ACPI (see [discovery](discovery.md) / [acpi](acpi.md)).
|
||||
3. The **ACPI PM timer** (a fixed 3.579545 MHz counter from the FADT).
|
||||
4. The **PIT** (legacy 8254, 1.193182 MHz) — last resort, and bounded so it can't hang.
|
||||
|
||||
All four yield the same rate; on QEMU (no CPUID crystal enumeration) it lands on the
|
||||
HPET, matching the PIT numbers to within measurement jitter.
|
||||
|
||||
## The high-resolution clock (TSC)
|
||||
|
||||
@@ -55,7 +67,7 @@ real-time system to *measure* with (interrupt latency, jitter, timeouts). So the
|
||||
same calibration also measures the **TSC** (Time Stamp Counter): a per-core cycle
|
||||
counter read with `rdtsc` in a couple of cycles, giving roughly **nanosecond**
|
||||
resolution — a million times finer than the tick. We snapshot the TSC across the
|
||||
same 10 ms PIT window to get its frequency (measured ~3.6 GHz on the test host).
|
||||
same 10 ms calibration window to get its frequency (measured ~1 GHz under QEMU).
|
||||
|
||||
The monotonic clock is exposed as one function per resolution — `nanos()`,
|
||||
`micros()`, `millis()` — each scaling the cycle delta directly at its unit (with a
|
||||
|
||||
@@ -56,6 +56,41 @@ pub const PowerInfo = struct {
|
||||
/// Filled in by `discover`; the power service reads it to reboot/shutdown.
|
||||
pub var power_info: PowerInfo = .{};
|
||||
|
||||
/// A legacy ISA IRQ remapped to a different global system interrupt (GSI), from a
|
||||
/// MADT Interrupt Source Override. `flags` are the MPS INTI polarity/trigger bits.
|
||||
pub const IsoEntry = struct {
|
||||
source: u8,
|
||||
gsi: u32,
|
||||
flags: u16,
|
||||
};
|
||||
|
||||
/// Firmware facts the arch layer needs to avoid legacy assumptions (so danos boots
|
||||
/// on legacy-free UEFI Class 3 machines). MMIO device *addresses* (HPET, IOAPIC)
|
||||
/// come from the device tree instead; this holds the scalar facts that have no
|
||||
/// natural device node.
|
||||
pub const PlatformInfo = struct {
|
||||
/// Whether the legacy 8259 PIC is present (MADT flags bit 0, PCAT_COMPAT). When
|
||||
/// false, the PIC must not be programmed (it may not exist).
|
||||
pic_present: bool = false,
|
||||
/// Local APIC MMIO base (MADT, honouring a type-5 address override).
|
||||
lapic_base: u64 = 0xFEE00000,
|
||||
/// The ACPI power-management timer — a fixed 3.579545 MHz counter usable as a
|
||||
/// calibration reference when no HPET is present.
|
||||
pm_timer: RegAccess = .{},
|
||||
/// true = 32-bit PM timer counter, false = 24-bit (FADT flag TMR_VAL_EXT).
|
||||
pm_timer_32bit: bool = false,
|
||||
/// The console UART the firmware points at (SPCR), if any — MMIO or I/O port.
|
||||
spcr_uart: ?RegAccess = null,
|
||||
/// SPCR interface type (0/1 = 16550/16450, …).
|
||||
spcr_kind: u8 = 0,
|
||||
/// ISA-IRQ-to-GSI remappings from the MADT (for future IOAPIC routing).
|
||||
overrides: [16]IsoEntry = undefined,
|
||||
override_count: usize = 0,
|
||||
};
|
||||
|
||||
/// Filled in by `discover`; the arch layer reads it during bring-up.
|
||||
pub var platform_info: PlatformInfo = .{};
|
||||
|
||||
/// Integrity/diagnostics for the AML parse. `consumed == total` means the parser
|
||||
/// walked every byte of the DSDT/SSDTs without desyncing.
|
||||
pub const AmlStats = struct {
|
||||
@@ -167,6 +202,8 @@ const SLIT: [4]u8 = "SLIT".*;
|
||||
const SRAT: [4]u8 = "SRAT".*;
|
||||
/// Secondary System Description Table (SSDT)
|
||||
const SSDT: [4]u8 = "SSDT".*;
|
||||
/// Serial Port Console Redirection table (SPCR) — the firmware's console UART.
|
||||
const SPCR: [4]u8 = "SPCR".*;
|
||||
/// Extended System Description Table (XSDT; 64-bit version of the RSDT)
|
||||
const XSDT: [4]u8 = "XSDT".*;
|
||||
|
||||
@@ -222,6 +259,22 @@ const MadtIoApic = extern struct {
|
||||
gsi_base: u32 align(1),
|
||||
};
|
||||
|
||||
/// MADT record type 2: an Interrupt Source Override (ISA IRQ -> GSI remap).
|
||||
const MadtIso = extern struct {
|
||||
record: MadtRecordHeader,
|
||||
bus: u8,
|
||||
source: u8,
|
||||
gsi: u32 align(1),
|
||||
flags: u16 align(1),
|
||||
};
|
||||
|
||||
/// MADT record type 5: Local APIC Address Override (64-bit MMIO base).
|
||||
const MadtLapicOverride = extern struct {
|
||||
record: MadtRecordHeader,
|
||||
reserved: u16 align(1),
|
||||
address: u64 align(1),
|
||||
};
|
||||
|
||||
// --- MCFG: PCIe ECAM configuration space (signature "MCFG") -----------------
|
||||
|
||||
const Mcfg = extern struct {
|
||||
@@ -286,6 +339,7 @@ pub fn discover(rsdp_phys: u64, dt: *DeviceTree, hal: Hal) !void {
|
||||
|
||||
// Start clean so a re-run doesn't accumulate stale state.
|
||||
power_info = .{};
|
||||
platform_info = .{};
|
||||
aml_stats = .{};
|
||||
namespace = null;
|
||||
dsdt_phys = 0;
|
||||
@@ -352,6 +406,8 @@ fn handleTable(dt: *DeviceTree, hal: Hal, sdt_phys: u64) !void {
|
||||
try parseHpet(dt, header);
|
||||
} else if (std.mem.eql(u8, &sig, &FACP)) {
|
||||
parseFadt(header);
|
||||
} else if (std.mem.eql(u8, &sig, &SPCR)) {
|
||||
parseSpcr(header);
|
||||
} else if (std.mem.eql(u8, &sig, &SSDT)) {
|
||||
// Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan.
|
||||
addAmlBlock(sdt_phys);
|
||||
@@ -361,10 +417,15 @@ fn handleTable(dt: *DeviceTree, hal: Hal, sdt_phys: u64) !void {
|
||||
|
||||
/// MADT -> one processor node per Local APIC, one interrupt_controller per I/O APIC.
|
||||
fn parseMadt(dt: *DeviceTree, header: *const SystemDescriptorTableHeader) !void {
|
||||
const madt: *const Madt = @ptrCast(header);
|
||||
const total: usize = header.length;
|
||||
const base: [*]const u8 = @ptrCast(header);
|
||||
var ioapic_index: usize = 0;
|
||||
|
||||
// MADT header: local APIC base + flags (bit 0 = 8259 PIC present).
|
||||
platform_info.lapic_base = madt.local_apic_address;
|
||||
platform_info.pic_present = madt.flags & 1 != 0;
|
||||
|
||||
var off: usize = @sizeOf(Madt);
|
||||
while (off + @sizeOf(MadtRecordHeader) <= total) {
|
||||
const rec: *const MadtRecordHeader = @ptrCast(base + off);
|
||||
@@ -389,6 +450,21 @@ fn parseMadt(dt: *DeviceTree, header: *const SystemDescriptorTableHeader) !void
|
||||
// The GSI range this I/O APIC handles, starting at gsi_base.
|
||||
_ = d.addResource(.irq, io.gsi_base, 0);
|
||||
},
|
||||
2 => {
|
||||
const iso: *const MadtIso = @ptrCast(base + off);
|
||||
if (platform_info.override_count < platform_info.overrides.len) {
|
||||
platform_info.overrides[platform_info.override_count] = .{
|
||||
.source = iso.source,
|
||||
.gsi = iso.gsi,
|
||||
.flags = iso.flags,
|
||||
};
|
||||
platform_info.override_count += 1;
|
||||
}
|
||||
},
|
||||
5 => {
|
||||
const ovr: *const MadtLapicOverride = @ptrCast(base + off);
|
||||
platform_info.lapic_base = ovr.address;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
off += rec.length;
|
||||
@@ -523,6 +599,7 @@ const fadt_acpi_enable = 52; // u8
|
||||
const fadt_acpi_disable = 53; // u8
|
||||
const fadt_pm1a_cnt_blk = 64; // u32 (I/O port)
|
||||
const fadt_pm1b_cnt_blk = 68; // u32 (I/O port)
|
||||
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
|
||||
const fadt_pm1_cnt_len = 89; // u8 (bytes)
|
||||
const fadt_flags = 112; // u32
|
||||
const fadt_reset_reg = 116; // GAS (12 bytes)
|
||||
@@ -530,7 +607,9 @@ const fadt_reset_value = 128; // u8
|
||||
const fadt_x_dsdt = 140; // u64
|
||||
const fadt_x_pm1a_cnt_blk = 172; // GAS
|
||||
const fadt_x_pm1b_cnt_blk = 184; // GAS
|
||||
const fadt_x_pm_tmr_blk = 208; // GAS
|
||||
const flag_reset_reg_supported = 1 << 10;
|
||||
const flag_tmr_val_ext = 1 << 8; // PM timer counter is 32-bit (else 24-bit)
|
||||
|
||||
/// FADT -> the power register map (into `power_info`) and the DSDT address, which
|
||||
/// is queued for the AML sleep-state (`_Sx`) scan. No AML interpretation happens here.
|
||||
@@ -552,6 +631,11 @@ fn parseFadt(header: *const SystemDescriptorTableHeader) void {
|
||||
pi.reset = readGas(base, len, fadt_reset_reg) orelse .{};
|
||||
pi.reset_value = fadt(u8, base, len, fadt_reset_value) orelse 0;
|
||||
|
||||
// The PM timer — a fixed-rate counter used as a calibration reference when no
|
||||
// HPET is present. Prefer the 64-bit-capable X_ GAS, fall back to the port.
|
||||
platform_info.pm_timer = readCntReg(base, len, fadt_x_pm_tmr_blk, fadt_pm_tmr_blk, 4);
|
||||
platform_info.pm_timer_32bit = flags & flag_tmr_val_ext != 0;
|
||||
|
||||
var dsdt: u64 = fadt(u32, base, len, fadt_dsdt) orelse 0;
|
||||
if (fadt(u64, base, len, fadt_x_dsdt)) |x| {
|
||||
if (x != 0) dsdt = x;
|
||||
@@ -560,6 +644,21 @@ fn parseFadt(header: *const SystemDescriptorTableHeader) void {
|
||||
addAmlBlock(dsdt);
|
||||
}
|
||||
|
||||
// SPCR field offsets (bytes from the table start).
|
||||
const spcr_interface_type = 36; // u8
|
||||
const spcr_base_address = 40; // GAS (12 bytes)
|
||||
|
||||
/// SPCR -> the console UART's address + interface type, so serial can target the
|
||||
/// firmware's actual debug port instead of assuming legacy COM1.
|
||||
fn parseSpcr(header: *const SystemDescriptorTableHeader) void {
|
||||
const base: [*]align(1) const u8 = @ptrCast(header);
|
||||
const len: usize = header.length;
|
||||
const gas = readGas(base, len, spcr_base_address) orelse return;
|
||||
if (gas.address == 0) return;
|
||||
platform_info.spcr_uart = gas;
|
||||
platform_info.spcr_kind = fadt(u8, base, len, spcr_interface_type) orelse 0;
|
||||
}
|
||||
|
||||
// --- AML namespace -> generic device tree -----------------------------------
|
||||
|
||||
/// The PCI bus context while descending the ACPI namespace: the generic host
|
||||
|
||||
@@ -131,6 +131,14 @@ pub const Device = struct {
|
||||
self.resource_count += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The device's first resource of `kind`, or null — e.g. a timer's MMIO base.
|
||||
pub fn firstResource(self: *const Device, kind: ResourceKind) ?Resource {
|
||||
for (self.resources[0..self.resource_count]) |r| {
|
||||
if (r.kind == kind) return r;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Owns the discovered device tree and the allocator its nodes came from.
|
||||
@@ -146,6 +154,12 @@ pub const DeviceTree = struct {
|
||||
return .{ .allocator = allocator, .root = root };
|
||||
}
|
||||
|
||||
/// The first device of `class` anywhere in the tree (depth-first), or null —
|
||||
/// how the kernel pulls e.g. the HPET or IOAPIC MMIO base out of discovery.
|
||||
pub fn firstOfClass(self: *const DeviceTree, class: DeviceClass) ?*Device {
|
||||
return firstOfClassIn(self.root, class);
|
||||
}
|
||||
|
||||
/// Allocate a device and append it under `parent`, returning it so the caller
|
||||
/// can attach resources/ids. Appended at the tail so a dump reads in the order
|
||||
/// devices were discovered.
|
||||
@@ -176,6 +190,15 @@ pub const DeviceTree = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn firstOfClassIn(node: *Device, class: DeviceClass) ?*Device {
|
||||
var child = node.first_child;
|
||||
while (child) |c| : (child = c.next_sibling) {
|
||||
if (c.class == class) return c;
|
||||
if (firstOfClassIn(c, class)) |found| return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn dumpNode(dev: *const Device, depth: usize, emit: *const fn ([]const u8) void) void {
|
||||
const indent = @min(depth * 2, 40);
|
||||
|
||||
|
||||
@@ -21,12 +21,21 @@ pub const DeviceClass = device.DeviceClass;
|
||||
pub const Hal = device.Hal;
|
||||
pub const PowerInfo = acpi.PowerInfo;
|
||||
pub const AmlStats = acpi.AmlStats;
|
||||
pub const PlatformInfo = acpi.PlatformInfo;
|
||||
pub const RegAccess = acpi.RegAccess;
|
||||
pub const IsoEntry = acpi.IsoEntry;
|
||||
|
||||
/// The register map + sleep types discovery extracted, for logging/diagnostics.
|
||||
pub fn powerInfo() PowerInfo {
|
||||
return acpi.power_info;
|
||||
}
|
||||
|
||||
/// The scalar firmware facts the arch layer needs to avoid legacy assumptions
|
||||
/// (8259 presence, LAPIC base, PM timer, SPCR UART, IRQ overrides).
|
||||
pub fn platformInfo() PlatformInfo {
|
||||
return acpi.platform_info;
|
||||
}
|
||||
|
||||
/// AML parse integrity/diagnostics (namespace node count, bytes consumed).
|
||||
pub fn amlStats() AmlStats {
|
||||
return acpi.aml_stats;
|
||||
|
||||
+171
-15
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ const idt = @import("idt.zig");
|
||||
const paging = @import("paging.zig");
|
||||
const serial = @import("serial.zig");
|
||||
const apic = @import("apic.zig");
|
||||
const ioapic = @import("ioapic.zig");
|
||||
const io = @import("io.zig");
|
||||
|
||||
/// The saved register/trap frame passed to a fault handler.
|
||||
@@ -64,9 +65,58 @@ pub fn readCr3() u64 {
|
||||
/// Kernel tick rate: 1000 Hz (1 ms), the scheduler's time quantum.
|
||||
pub const timer_hz = 1000;
|
||||
|
||||
/// Enable the Local APIC, calibrate its timer against the PIT, and start it firing
|
||||
/// at `timer_hz` — the kernel's real-time heartbeat. Interrupts still have to be
|
||||
/// unmasked with enableInterrupts() to be delivered.
|
||||
/// The ACPI PM timer, as a calibration reference (re-exported for the config).
|
||||
pub const PmTimer = apic.PmTimer;
|
||||
/// A MADT interrupt-source override (re-exported for the config).
|
||||
pub const IsoEntry = ioapic.IsoEntry;
|
||||
|
||||
/// Discovered platform facts the arch layer needs so it makes no legacy
|
||||
/// assumptions — sourced from the device tree + ACPI, passed in by the kernel.
|
||||
pub const PlatformConfig = struct {
|
||||
/// Whether the legacy 8259 PIC is present (skip programming it if not).
|
||||
pic_present: bool = true,
|
||||
/// HPET MMIO base (0 = none) — a calibration reference for the timer.
|
||||
hpet_base: u64 = 0,
|
||||
/// The ACPI PM timer, another calibration reference.
|
||||
pm_timer: ?PmTimer = null,
|
||||
/// I/O APIC MMIO base + its first global system interrupt (0 = none).
|
||||
ioapic_base: u64 = 0,
|
||||
ioapic_gsi_base: u32 = 0,
|
||||
/// MADT ISA-IRQ overrides, for I/O APIC routing.
|
||||
overrides: []const IsoEntry = &.{},
|
||||
};
|
||||
|
||||
/// Apply the discovered platform config. Must run before `startTimer` (the timer
|
||||
/// calibration reads `hpet_base`/`pm_timer`) and before any interrupt routing.
|
||||
/// Maps + masks the I/O APIC immediately.
|
||||
pub fn configurePlatform(cfg: PlatformConfig) void {
|
||||
apic.configure(cfg.pic_present, cfg.hpet_base, cfg.pm_timer);
|
||||
ioapic.configure(cfg.ioapic_base, cfg.ioapic_gsi_base, cfg.overrides);
|
||||
ioapic.init();
|
||||
}
|
||||
|
||||
/// Point the serial console at the UART ACPI's SPCR table named (MMIO or I/O port).
|
||||
pub fn serialReconfigure(is_mmio: bool, addr: u64) void {
|
||||
serial.reconfigure(is_mmio, addr);
|
||||
}
|
||||
|
||||
/// The reference clock the timer was calibrated against ("cpuid"/"hpet"/…).
|
||||
pub fn timerCalibrationSource() []const u8 {
|
||||
return apic.calibrationSource();
|
||||
}
|
||||
|
||||
/// I/O APIC diagnostics (for boot logging / verification).
|
||||
pub fn ioapicEntryCount() u32 {
|
||||
return ioapic.entryCount();
|
||||
}
|
||||
pub fn ioapicEntryLow(n: u32) u32 {
|
||||
return ioapic.entryLow(n);
|
||||
}
|
||||
|
||||
/// Enable the Local APIC, calibrate its timer against the best available reference
|
||||
/// (see apic.calibrate — no longer the PIT by default), and start it firing at
|
||||
/// `timer_hz` — the kernel's real-time heartbeat. Interrupts still have to be
|
||||
/// unmasked with enableInterrupts() to be delivered. Run `configurePlatform` first.
|
||||
pub fn startTimer() void {
|
||||
apic.init();
|
||||
apic.calibrate();
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//! I/O APIC — routes external device interrupts (a device's line) to a LAPIC
|
||||
//! vector on a chosen CPU. Its address and the ISA-IRQ-to-GSI remappings come from
|
||||
//! ACPI's MADT (via discovery), never assumed.
|
||||
//!
|
||||
//! Status: groundwork. The only interrupt danos handles today is the LAPIC's own
|
||||
//! timer, which needs no I/O APIC — so nothing calls `routeIrq` yet. What runs now
|
||||
//! is `init`, which maps the I/O APIC and **masks every input**, the correct
|
||||
//! quiescent state on a legacy-free machine. `routeIrq` is ready for the first real
|
||||
//! device driver (a keyboard, say).
|
||||
|
||||
const paging = @import("paging.zig");
|
||||
|
||||
/// A MADT Interrupt Source Override: an ISA IRQ that appears at a different global
|
||||
/// system interrupt, with its own polarity/trigger (MPS INTI `flags`).
|
||||
pub const IsoEntry = struct { source: u8, gsi: u32, flags: u16 };
|
||||
|
||||
var base: u64 = 0; // 0 = no I/O APIC discovered
|
||||
var gsi_base: u32 = 0;
|
||||
var max_entries: u32 = 0;
|
||||
var overrides: [16]IsoEntry = undefined;
|
||||
var override_count: usize = 0;
|
||||
|
||||
// The I/O APIC exposes an index register (IOREGSEL) and a data window (IOWIN).
|
||||
const reg_ioregsel = 0x00;
|
||||
const reg_iowin = 0x10;
|
||||
const reg_version = 0x01;
|
||||
const redir_base = 0x10; // redirection table: two 32-bit regs per entry
|
||||
const redir_mask = 1 << 16; // mask bit in the low dword
|
||||
|
||||
/// Supply the discovered I/O APIC location + the MADT IRQ overrides. Call before `init`.
|
||||
pub fn configure(ioapic_base: u64, ioapic_gsi_base: u32, isos: []const IsoEntry) void {
|
||||
base = ioapic_base;
|
||||
gsi_base = ioapic_gsi_base;
|
||||
override_count = @min(isos.len, overrides.len);
|
||||
for (isos[0..override_count], 0..) |iso, i| overrides[i] = iso;
|
||||
}
|
||||
|
||||
fn regRead(index: u32) u32 {
|
||||
@as(*volatile u32, @ptrFromInt(base + reg_ioregsel)).* = index;
|
||||
return @as(*volatile u32, @ptrFromInt(base + reg_iowin)).*;
|
||||
}
|
||||
fn regWrite(index: u32, value: u32) void {
|
||||
@as(*volatile u32, @ptrFromInt(base + reg_ioregsel)).* = index;
|
||||
@as(*volatile u32, @ptrFromInt(base + reg_iowin)).* = value;
|
||||
}
|
||||
|
||||
fn writeEntry(n: u32, low: u32, high: u32) void {
|
||||
regWrite(redir_base + 2 * n, low);
|
||||
regWrite(redir_base + 2 * n + 1, high);
|
||||
}
|
||||
|
||||
/// Map the I/O APIC and mask every redirection entry — the safe quiescent state.
|
||||
pub fn init() void {
|
||||
if (base == 0) return;
|
||||
paging.map(base & ~@as(u64, 0xFFF), base & ~@as(u64, 0xFFF), true);
|
||||
max_entries = ((regRead(reg_version) >> 16) & 0xFF) + 1;
|
||||
var n: u32 = 0;
|
||||
while (n < max_entries) : (n += 1) writeEntry(n, redir_mask, 0);
|
||||
}
|
||||
|
||||
/// Route ISA `irq` to `vector` on the LAPIC `apic_id`, honouring a MADT override
|
||||
/// for its GSI/polarity/trigger, and unmask it. No caller yet — groundwork for the
|
||||
/// first device driver.
|
||||
pub fn routeIrq(irq: u8, vector: u8, apic_id: u8) void {
|
||||
if (base == 0) return;
|
||||
|
||||
var gsi: u32 = irq;
|
||||
var flags: u16 = 0;
|
||||
for (overrides[0..override_count]) |o| {
|
||||
if (o.source == irq) {
|
||||
gsi = o.gsi;
|
||||
flags = o.flags;
|
||||
}
|
||||
}
|
||||
if (gsi < gsi_base) return;
|
||||
const n = gsi - gsi_base;
|
||||
if (n >= max_entries) return;
|
||||
|
||||
// Low dword: vector + delivery mode fixed(0) + physical dest(0), unmasked.
|
||||
// MPS INTI flags: bits [1:0] polarity (3 = active low), [3:2] trigger (3 = level).
|
||||
var low: u32 = vector;
|
||||
if (flags & 0x3 == 3) low |= (1 << 13);
|
||||
if ((flags >> 2) & 0x3 == 3) low |= (1 << 15);
|
||||
const high: u32 = @as(u32, apic_id) << 24; // destination APIC ID
|
||||
writeEntry(n, low, high);
|
||||
}
|
||||
|
||||
/// Number of redirection entries the I/O APIC advertises (0 until `init`).
|
||||
pub fn entryCount() u32 {
|
||||
return max_entries;
|
||||
}
|
||||
|
||||
/// The low dword of redirection entry `n` — for diagnostics/read-back.
|
||||
pub fn entryLow(n: u32) u32 {
|
||||
if (base == 0) return 0;
|
||||
return regRead(redir_base + 2 * n);
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
//! COM1 serial port (16550 UART) — the kernel's machine-readable output channel.
|
||||
//! Unlike the framebuffer console, serial text can be captured to a file by QEMU
|
||||
//! (`-serial file:...`), which is what the test harness asserts on. Each
|
||||
//! architecture has its own UART; this is the x86 one, driven by port I/O.
|
||||
//! Serial console (16550-compatible UART) — the kernel's machine-readable output
|
||||
//! channel. Unlike the framebuffer console, serial text can be captured to a file
|
||||
//! by QEMU (`-serial file:...`), which is what the test harness asserts on.
|
||||
//!
|
||||
//! The UART defaults to the legacy PC COM1 at I/O port `0x3F8`, but a UEFI Class 3
|
||||
//! (legacy-free) machine may have no COM1 — or its debug UART somewhere else, and
|
||||
//! reachable via MMIO rather than port I/O. So the location is a runtime value:
|
||||
//! `reconfigure` repoints it once ACPI's SPCR table has been read. Early boot logs
|
||||
//! optimistically to COM1 (harmless if absent); the framebuffer console is the
|
||||
//! always-present log.
|
||||
|
||||
const port = 0x3F8; // COM1 base
|
||||
/// How the UART registers are reached: legacy I/O ports or memory-mapped.
|
||||
const Access = enum { port, mmio };
|
||||
|
||||
fn outb(p: u16, value: u8) void {
|
||||
var access: Access = .port;
|
||||
var base: u64 = 0x3F8; // COM1
|
||||
|
||||
fn portOut(p: u16, value: u8) void {
|
||||
asm volatile ("outb %[value], %[p]"
|
||||
:
|
||||
: [value] "{al}" (value),
|
||||
@@ -13,28 +23,54 @@ fn outb(p: u16, value: u8) void {
|
||||
);
|
||||
}
|
||||
|
||||
fn inb(p: u16) u8 {
|
||||
fn portIn(p: u16) u8 {
|
||||
return asm volatile ("inb %[p], %[value]"
|
||||
: [value] "={al}" (-> u8),
|
||||
: [p] "{dx}" (p),
|
||||
);
|
||||
}
|
||||
|
||||
/// Read UART register `off` through the active access method.
|
||||
fn reg(off: u64) u8 {
|
||||
if (access == .mmio) return @as(*volatile u8, @ptrFromInt(base + off)).*;
|
||||
return portIn(@intCast(base + off));
|
||||
}
|
||||
|
||||
/// Write UART register `off` through the active access method.
|
||||
fn setReg(off: u64, value: u8) void {
|
||||
if (access == .mmio) {
|
||||
@as(*volatile u8, @ptrFromInt(base + off)).* = value;
|
||||
} else {
|
||||
portOut(@intCast(base + off), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the UART: 38400 baud, 8N1, FIFO on. Safe to call before anything
|
||||
/// else; it has no dependencies.
|
||||
/// else; it has no dependencies, and is a harmless no-op if the port is absent.
|
||||
pub fn init() void {
|
||||
outb(port + 1, 0x00); // disable interrupts
|
||||
outb(port + 3, 0x80); // enable DLAB (set baud divisor)
|
||||
outb(port + 0, 0x03); // divisor low: 38400 baud
|
||||
outb(port + 1, 0x00); // divisor high
|
||||
outb(port + 3, 0x03); // 8 bits, no parity, one stop bit; DLAB off
|
||||
outb(port + 2, 0xC7); // enable + clear FIFO, 14-byte threshold
|
||||
outb(port + 4, 0x0B); // RTS/DSR set
|
||||
setReg(1, 0x00); // disable interrupts
|
||||
setReg(3, 0x80); // enable DLAB (set baud divisor)
|
||||
setReg(0, 0x03); // divisor low: 38400 baud
|
||||
setReg(1, 0x00); // divisor high
|
||||
setReg(3, 0x03); // 8 bits, no parity, one stop bit; DLAB off
|
||||
setReg(2, 0xC7); // enable + clear FIFO, 14-byte threshold
|
||||
setReg(4, 0x0B); // RTS/DSR set
|
||||
}
|
||||
|
||||
/// Point the console at the UART ACPI's SPCR table names (MMIO or I/O port) and
|
||||
/// re-run the UART setup there. Called after discovery when an SPCR entry exists.
|
||||
pub fn reconfigure(is_mmio: bool, addr: u64) void {
|
||||
access = if (is_mmio) .mmio else .port;
|
||||
base = addr;
|
||||
init();
|
||||
}
|
||||
|
||||
fn writeByte(c: u8) void {
|
||||
while (inb(port + 5) & 0x20 == 0) {} // wait until the transmit holding register is empty
|
||||
outb(port, c);
|
||||
// Wait for the transmit-holding register to empty — but bounded, so an absent
|
||||
// UART (whose line-status register reads back as 0x00) can't hang the kernel.
|
||||
var guard: u32 = 0;
|
||||
while (reg(5) & 0x20 == 0 and guard < 100_000) : (guard += 1) {}
|
||||
setReg(0, c);
|
||||
}
|
||||
|
||||
/// Write bytes, translating LF to CRLF so terminals and logs line up.
|
||||
|
||||
+48
-1
@@ -131,6 +131,53 @@ fn kmain(boot_info: *const BootInfo) noreturn {
|
||||
// AML namespace parse integrity: consumed should equal total.
|
||||
const am = platform.amlStats();
|
||||
serial0.debugPrint(" aml : {d} namespace nodes, parsed {d}/{d} bytes\n", .{ am.nodes, am.consumed, am.total });
|
||||
|
||||
// Feed the arch layer the discovered addresses/facts so it makes no legacy
|
||||
// assumptions — the point of all this on UEFI Class 3 firmware. MMIO bases
|
||||
// (HPET, I/O APIC) come from the device tree; scalar facts from ACPI.
|
||||
const pinfo = platform.platformInfo();
|
||||
const hpet_base: u64 = if (dt.firstOfClass(.timer)) |t|
|
||||
(if (t.firstResource(.memory)) |r| r.start else 0)
|
||||
else
|
||||
0;
|
||||
var ioapic_base: u64 = 0;
|
||||
var ioapic_gsi: u32 = 0;
|
||||
if (dt.firstOfClass(.interrupt_controller)) |ic| {
|
||||
if (ic.firstResource(.memory)) |r| ioapic_base = r.start;
|
||||
if (ic.firstResource(.irq)) |r| ioapic_gsi = @intCast(r.start);
|
||||
}
|
||||
var isos: [16]arch.IsoEntry = undefined;
|
||||
const iso_n = @min(pinfo.override_count, isos.len);
|
||||
for (0..iso_n) |i| isos[i] = .{
|
||||
.source = pinfo.overrides[i].source,
|
||||
.gsi = pinfo.overrides[i].gsi,
|
||||
.flags = pinfo.overrides[i].flags,
|
||||
};
|
||||
const pm_timer: ?arch.PmTimer = if (pinfo.pm_timer.present())
|
||||
.{ .mmio = pinfo.pm_timer.mmio, .address = pinfo.pm_timer.address, .is_32bit = pinfo.pm_timer_32bit }
|
||||
else
|
||||
null;
|
||||
arch.configurePlatform(.{
|
||||
.pic_present = pinfo.pic_present,
|
||||
.hpet_base = hpet_base,
|
||||
.pm_timer = pm_timer,
|
||||
.ioapic_base = ioapic_base,
|
||||
.ioapic_gsi_base = ioapic_gsi,
|
||||
.overrides = isos[0..iso_n],
|
||||
});
|
||||
if (pinfo.spcr_uart) |u| arch.serialReconfigure(u.mmio, u.address);
|
||||
|
||||
serial0.debugWrite("danos: platform\n");
|
||||
serial0.debugPrint(" 8259 PIC : {s}\n", .{if (pinfo.pic_present) "present" else "absent"});
|
||||
serial0.debugPrint(" lapic base : 0x{x}\n", .{pinfo.lapic_base});
|
||||
serial0.debugPrint(" hpet base : 0x{x}\n", .{hpet_base});
|
||||
serial0.debugPrint(" pm timer : {s} 0x{x} ({s})\n", .{ if (pinfo.pm_timer.mmio) "mmio" else "io", pinfo.pm_timer.address, if (pinfo.pm_timer_32bit) "32-bit" else "24-bit" });
|
||||
if (pinfo.spcr_uart) |u| {
|
||||
serial0.debugPrint(" console UART: {s} 0x{x} (SPCR type {d})\n", .{ if (u.mmio) "mmio" else "io", u.address, pinfo.spcr_kind });
|
||||
} else {
|
||||
serial0.debugWrite(" console UART: none in SPCR -> legacy COM1\n");
|
||||
}
|
||||
serial0.debugPrint(" ioapic : base 0x{x}, {d} inputs (masked); entry0 low 0x{x}\n", .{ ioapic_base, arch.ioapicEntryCount(), arch.ioapicEntryLow(0) });
|
||||
} else |err| {
|
||||
serial0.debugPrint("\ndanos: device discovery failed: {s}\n", .{@errorName(err)});
|
||||
}
|
||||
@@ -143,7 +190,7 @@ fn kmain(boot_info: *const BootInfo) noreturn {
|
||||
// the timer preempts among tasks.
|
||||
arch.startTimer();
|
||||
arch.enableInterrupts();
|
||||
serial0.debugPrint("danos: timer online ({d} Hz tick; LAPIC {d} MHz, TSC {d} MHz measured)\n", .{ arch.timer_hz, arch.lapicHz() / 1_000_000, arch.tscHz() / 1_000_000 });
|
||||
serial0.debugPrint("danos: timer online ({d} Hz tick; LAPIC {d} MHz, TSC {d} MHz; calibrated via {s})\n", .{ arch.timer_hz, arch.lapicHz() / 1_000_000, arch.tscHz() / 1_000_000, arch.timerCalibrationSource() });
|
||||
|
||||
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
||||
// Normal builds fall through to the idle halt.
|
||||
|
||||
Reference in New Issue
Block a user