From 7d3417fe86f58acbada4ae53f7f7b269d9aa69bf Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:06:46 +0100 Subject: [PATCH] refactor kernel to use device platform discovery --- docs/device-interrupts.md | 26 +++-- src/device/acpi.zig | 99 ++++++++++++++++ src/device/device.zig | 23 ++++ src/device/platform.zig | 9 ++ src/kernel/arch/x86_64/apic.zig | 186 +++++++++++++++++++++++++++--- src/kernel/arch/x86_64/cpu.zig | 56 ++++++++- src/kernel/arch/x86_64/ioapic.zig | 97 ++++++++++++++++ src/kernel/arch/x86_64/serial.zig | 70 ++++++++--- src/kernel/main.zig | 49 +++++++- 9 files changed, 572 insertions(+), 43 deletions(-) create mode 100644 src/kernel/arch/x86_64/ioapic.zig diff --git a/docs/device-interrupts.md b/docs/device-interrupts.md index 7f7dc5d..048e9ad 100644 --- a/docs/device-interrupts.md +++ b/docs/device-interrupts.md @@ -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 diff --git a/src/device/acpi.zig b/src/device/acpi.zig index e45a414..7632e6c 100644 --- a/src/device/acpi.zig +++ b/src/device/acpi.zig @@ -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 diff --git a/src/device/device.zig b/src/device/device.zig index 9c3ba07..2684ca3 100644 --- a/src/device/device.zig +++ b/src/device/device.zig @@ -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); diff --git a/src/device/platform.zig b/src/device/platform.zig index d9765f4..3729c5c 100644 --- a/src/device/platform.zig +++ b/src/device/platform.zig @@ -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; diff --git a/src/kernel/arch/x86_64/apic.zig b/src/kernel/arch/x86_64/apic.zig index aff0ce6..d9d83c8 100644 --- a/src/kernel/arch/x86_64/apic.zig +++ b/src/kernel/arch/x86_64/apic.zig @@ -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 diff --git a/src/kernel/arch/x86_64/cpu.zig b/src/kernel/arch/x86_64/cpu.zig index 108d01c..e2989c3 100644 --- a/src/kernel/arch/x86_64/cpu.zig +++ b/src/kernel/arch/x86_64/cpu.zig @@ -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(); diff --git a/src/kernel/arch/x86_64/ioapic.zig b/src/kernel/arch/x86_64/ioapic.zig new file mode 100644 index 0000000..e785432 --- /dev/null +++ b/src/kernel/arch/x86_64/ioapic.zig @@ -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); +} diff --git a/src/kernel/arch/x86_64/serial.zig b/src/kernel/arch/x86_64/serial.zig index cda7337..9b2ce07 100644 --- a/src/kernel/arch/x86_64/serial.zig +++ b/src/kernel/arch/x86_64/serial.zig @@ -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. diff --git a/src/kernel/main.zig b/src/kernel/main.zig index cd5902c..8a5a0c4 100644 --- a/src/kernel/main.zig +++ b/src/kernel/main.zig @@ -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=`), run that case and stop. // Normal builds fall through to the idle halt.