danos/system/kernel/acpi.zig

1017 lines
48 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! ACPI discovery backend.
//!
//! Walks the ACPI tables the firmware left in memory (starting from the RSDP the
//! bootloader handed us) and translates the static tables into the generic
//! `device` model, so the kernel enumerates hardware without knowing ACPI is the
//! source. This is deliberately the *static-table* path, and **only** that: MADT
//! (CPUs / interrupt controllers), MCFG (PCIe ECAM -> PCI enumeration), HPET
//! (timer), and FADT (power register map). The DSDT/SSDT bytecode is *not*
//! interpreted here — the kernel collects the blobs and publishes them on the
//! acpi-tables node for the ring-3 acpi service to parse (device enumeration and
//! soft-off). Keeping the ~0.5 MB AML interpretation out of kernel init keeps it
//! off the single-core critical path (nothing else runs alongside it there).
//!
//! ACPI tables live in `.acpi_tables` / `.acpi_nvs` memory, which the kernel
//! identity-maps, so table addresses are dereferenced directly. PCIe ECAM is MMIO
//! and is *not* mapped up front, so configuration-space pages are mapped on demand via
//! the `Hal.mapMmio` callback the caller supplies (the architecture VMM's map primitive).
const std = @import("std");
const boot_handoff = @import("boot-handoff");
const abi = @import("abi");
const parameters = @import("parameters");
const device_model = @import("device-model.zig");
const DeviceTree = device_model.DeviceTree;
const Hal = device_model.Hal;
/// A hardware register located either in MMIO or I/O-port space, as ACPI's
/// Generic Address Structure describes. `address == 0` means "not present".
pub const RegisterAccess = struct {
/// true = system memory (MMIO), false = system I/O port space.
mmio: bool = false,
address: u64 = 0,
/// Access width in bytes.
width: u8 = 0,
pub fn present(self: RegisterAccess) bool {
return self.address != 0;
}
};
/// The power register map, extracted from the FADT during discovery. Populated by
/// `discover`, read by `power` (kernel reboot). The **sleep-state (`_Sx`) values
/// live in AML**, which the kernel no longer parses — soft-off (S5) is owned by the
/// ring-3 acpi service (it re-parses the blobs on the published acpi-tables node and
/// writes the PM1 control register itself). So this holds only the FADT scalars.
pub const PowerInformation = struct {
/// The System Control Interrupt's GSI (FADT SCI_INT) — the line ACPI events
/// (power button, GPEs) arrive on. Published to the acpi service for M21.
sci_interrupt: u16 = 0,
/// The SMM command port and the value that switches the platform into ACPI mode.
smi_cmd: u16 = 0,
acpi_enable: u8 = 0,
acpi_disable: u8 = 0,
/// PM1 control registers — writing SLP_TYP|SLP_EN here enters a sleep state.
pm1a_cnt: RegisterAccess = .{},
pm1b_cnt: RegisterAccess = .{},
/// The FADT reset register and the value to write to it.
reset: RegisterAccess = .{},
reset_value: u8 = 0,
reset_supported: bool = false,
};
/// Filled in by `discover`; `reboot` (below) reads it to restart the machine.
pub var power_information: PowerInformation = .{};
/// 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 architecture 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 PlatformInformation = 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: RegisterAccess = .{},
/// true = 32-bit PM timer counter, false = 24-bit (FADT flag TMR_VALUE_EXT).
pm_timer_32bit: bool = false,
/// The console UART the firmware points at (SPCR), if any — MMIO or I/O port.
spcr_uart: ?RegisterAccess = 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,
/// Whether an IOMMU (VT-d DMA-remapping unit) was found in the ACPI DMAR table.
/// When false, `device_claim` on a DMA-capable device is equivalent to granting
/// ring 0 — a device can DMA to any physical address (docs/driver-model.md M16),
/// and the kernel says so at every boot (the fail-open platform log line). When
/// true, the IOMMU core builds per-device translation domains from this record.
iommu_present: bool = false,
/// true when the present unit is AMD-Vi (from IVRS) rather than Intel VT-d (DMAR).
/// The two are mutually exclusive on real hardware; the IOMMU core picks the backend.
iommu_is_amd: bool = false,
/// MMIO base of the selected DMA-remapping hardware unit — the VT-d DRHD with
/// INCLUDE_PCI_ALL (the catch-all unit; falls back to the first), or the AMD-Vi
/// IOMMU's control-register base from the first IVHD.
iommu_base: u64 = 0,
/// Whether the selected unit carries INCLUDE_PCI_ALL. False means every unit is
/// device-scoped (unusual) — the core still enables on the selected unit but
/// devices outside its scope remain untranslated.
iommu_include_all: bool = false,
/// DRHD units in the DMAR beyond the selected one. Devices scoped to those units
/// (typically the integrated GPU) are NOT translated by v1 — the boot log warns.
iommu_extra_units: u8 = 0,
/// Reserved-memory regions (DMAR RMRRs): firmware-owned buffers a named device
/// keeps DMAing into across the OS handoff (classically the xHC keyboard-emulation
/// buffer). These must be identity-mapped in the device's domain BEFORE translation
/// enables, or platform firmware breaks. Only single-path endpoint scopes are
/// recorded; anything fancier is skipped with a loud log at parse time.
rmrr: [maximum_rmrr]RmrrRegion = undefined,
rmrr_count: usize = 0,
/// RMRR device scopes the parser could not record (multi-hop paths, sub-hierarchy
/// types, or table overflow). Non-zero means a device keeps an unmapped firmware
/// buffer — the kernel boot log warns loudly (the platform module itself is
/// log-free by design; it records, the kernel reports).
rmrr_skipped: u8 = 0,
};
pub const maximum_rmrr = 8;
/// One recorded RMRR: the device (requester id) and the inclusive physical range it
/// must always be allowed to reach.
pub const RmrrRegion = struct {
bdf: u16,
base: u64,
limit: u64,
};
/// Filled in by `discover`; the architecture layer reads it during bring-up.
pub var platform_information: PlatformInformation = .{};
/// One usable logical processor, from a MADT type-0 (Local APIC) record. The
/// `apic_id` is the Local APIC ID that SMP bring-up targets to wake this core
/// (INITSIPISIPI); `processor_id` is the ACPI namespace handle. Only processors
/// the firmware marks *enabled* are recorded — a disabled one can't be started.
pub const Cpu = struct {
processor_id: u8,
apic_id: u8,
/// MADT flags bit 1: usable but firmware-started offline (hot-plug / deferred
/// bring-up), as opposed to already available. Informational for now.
online_capable: bool,
};
/// The set of usable logical processors the MADT listed — the hardware's degree of
/// parallelism. Includes the bootstrap processor danos already runs on; the rest
/// are the application processors SMP bring-up would start (see docs/smp.md).
pub const CpuInformation = struct {
/// A static pool sized well above any danos target (a desktop, two 4-core Pis).
/// If the MADT ever lists more, the surplus is dropped and counted in `dropped`
/// so the truncation is never silent.
cpus: [maximum_cpus]Cpu = undefined,
count: usize = 0,
dropped: usize = 0,
};
const maximum_cpus = parameters.maximum_cpus;
/// Filled in by `discover` (from the MADT); SMP bring-up reads it to wake the APs.
pub var cpu_information: CpuInformation = .{};
/// Physical address of the DSDT the FADT points at, or 0.
pub var dsdt_physical: u64 = 0;
/// The FADT itself (physical + length), published on the acpi-tables node so
/// the ring-3 acpi service can read the PM1 event and GPE blocks it needs for
/// the event side (docs/acpi.md — ACPI events). Distinguished from the AML
/// blob resources by its intact "FACP" header — the blobs are header-stripped.
var fadt_physical: u64 = 0;
var fadt_length: u64 = 0;
// AML blocks (DSDT + any SSDTs) collected during the table walk, as physical
// address + length of each table's post-header bytecode. The kernel does not
// interpret them — it publishes them on the acpi-tables node for the ring-3 acpi
// service to parse (device enumeration + soft-off). See publishAcpiTablesNode.
var aml_block_physical: [32]u64 = undefined;
var aml_block_len: [32]usize = undefined;
var aml_block_count: usize = 0;
fn addAmlBlock(sdt_physical: u64) void {
if (aml_block_count >= aml_block_physical.len or sdt_physical == 0) return;
const h: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(sdt_physical));
if (h.length <= @sizeOf(SystemDescriptorTableHeader)) return;
aml_block_physical[aml_block_count] = sdt_physical + @sizeOf(SystemDescriptorTableHeader);
aml_block_len[aml_block_count] = h.length - @sizeOf(SystemDescriptorTableHeader);
aml_block_count += 1;
}
/// RSDP structure for revision 0 (version 1.0)
const RootSystemDescriptionPointer = extern struct {
/// An 8 byte magic number used for locating the RSDP, containing RSD PTR.
signature: [8]u8,
/// A byte used to verify the first 20 bytes of the RSDP
checksum: u8,
/// An OEM-supplied string that identified the OEM.
oem_id: [6]u8,
/// The RSDP revision, used for determining which fields are available.
revision: u8,
/// A 32-bit physical address pointing to the RSDT.
root_system_description_table_address: u32 align(1),
};
/// XSDP structure for revision 2 (version 2.0+)
const ExtendedSystemDescriptorPointer = extern struct {
/// An 8 byte magic number used for locating the RSDP, containing RSD PTR.
signature: [8]u8,
/// A byte used to verify the first 20 bytes of the RSDP
checksum: u8,
/// An OEM-supplied string that identified the OEM.
oem_id: [6]u8,
/// The RSDP revision, used for determining which fields are available.
revision: u8,
/// deprecated since version 2.0. A 32-bit physical address pointing to the RSDT.
root_system_description_table_address: u32 align(1),
/// The size of the RSDP.
length: u32 align(1),
/// A 64-bit physical address pointing to the XSDT. If the revision is at least 2, the XSDT
/// should be used regardless of architecture, as the RSDT was deprecated.
extended_system_descriptor_table_address: u64 align(1),
/// A checksum used for the entire table.
extended_checksum: u8,
reserved: [3]u8,
};
/// Multiple APIC Description Table (MADT)
const APIC: [4]u8 = "APIC".*;
/// Boot Error Record Table (BERT)
const BERT: [4]u8 = "BERT".*;
/// Corrected Platform Error Polling Table (CPEP)
const CPEP: [4]u8 = "CPEP".*;
/// Differentiated System Description Table (DSDT)
const DSDT: [4]u8 = "DSDT".*;
/// Embedded Controller Boot Resources Table (ECDT)
const ECDT: [4]u8 = "ECDT".*;
/// Error Injection Table (EINJ)
const EINJ: [4]u8 = "EINJ".*;
/// Error Record Serialization Table (ERST)
const ERST: [4]u8 = "ERST".*;
/// Fixed ACPI Description Table (FADT)
const FACP: [4]u8 = "FACP".*;
/// Firmware ACPI Control Structure (FACS)
const FACS: [4]u8 = "FACS".*;
/// Hardware Error Source Table (HEST)
const HEST: [4]u8 = "HEST".*;
/// High Precision Event Timer table (HPET)
const HPET: [4]u8 = "HPET".*;
/// PCI Express memory-mapped configuration space table (MCFG)
const MCFG: [4]u8 = "MCFG".*;
/// Maximum System Characteristics Table (MSCT)
const MSCT: [4]u8 = "MSCT".*;
/// Memory Power State Table (MPST)
const MPST: [4]u8 = "MPST".*;
// Platform Memory Topology Table (PMTT)
const PMTT: [4]u8 = "PMTT".*;
/// Persistent System Description Table (PSDT)
const PSDT: [4]u8 = "PSDT".*;
/// ACPI RAS Feature Table (RASF)
const RASF: [4]u8 = "RASF".*;
/// Root System Description Table
const RSDT: [4]u8 = "RSDT".*;
/// Smart Battery Specification Table (SBST)
const SBST: [4]u8 = "SBST".*;
/// System Locality System Information Table (SLIT)
const SLIT: [4]u8 = "SLIT".*;
/// System Resource Affinity Table (SRAT)
const SRAT: [4]u8 = "SRAT".*;
/// Secondary System Description Table (SSDT)
const DMAR: [4]u8 = "DMAR".*;
/// I/O Virtualization Reporting Structure (IVRS) — the AMD-Vi analogue of DMAR.
const IVRS: [4]u8 = "IVRS".*;
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".*;
/// The header every system descriptor table (RSDT/XSDT and each SDT) begins with.
const SystemDescriptorTableHeader = extern struct {
/// A 4 byte signature used for identification (e.g. "RSDT", "APIC").
signature: [4]u8,
/// The length of the entire table, including the header.
length: u32 align(1),
/// The revision of the ACPI spec this table conforms to.
revision: u8,
/// An 8-bit checksum field for the whole table, inclusive of the header.
checksum: u8,
/// An OEM-supplied string that identified the OEM.
oem_id: [6]u8,
oem_table_id: [8]u8,
oem_revision: u32 align(1),
creator_id: u32 align(1),
creator_revision: u32 align(1),
};
// --- MADT: Multiple APIC Description Table (signature "APIC") ---------------
const Madt = extern struct {
header: SystemDescriptorTableHeader,
local_apic_address: u32 align(1),
flags: u32 align(1),
// Followed by a variable-length run of interrupt-controller records, each a
// MadtRecordHeader plus a type-specific body.
};
const MadtRecordHeader = extern struct {
type: u8,
length: u8,
};
/// MADT record type 0: a processor's Local APIC.
const MadtLocalApic = extern struct {
record: MadtRecordHeader,
processor_id: u8,
apic_id: u8,
/// bit 0 = enabled, bit 1 = online-capable.
flags: u32 align(1),
};
/// MADT record type 1: an I/O APIC.
const MadtIoApic = extern struct {
record: MadtRecordHeader,
io_apic_id: u8,
reserved: u8,
address: u32 align(1),
/// First global system interrupt this I/O APIC handles.
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 {
header: SystemDescriptorTableHeader,
reserved: u64 align(1),
// Followed by one or more McfgAllocation entries.
};
const McfgAllocation = extern struct {
/// Physical base of this segment group's ECAM window.
base_address: u64 align(1),
segment_group: u16 align(1),
start_bus: u8,
end_bus: u8,
reserved: u32 align(1),
};
// --- HPET (signature "HPET") ------------------------------------------------
const Hpet = extern struct {
header: SystemDescriptorTableHeader,
hardware_rev_id: u8,
flags: u8,
pci_vendor_id: u16 align(1),
// Generic Address Structure describing the register block.
address_space_id: u8,
register_bit_width: u8,
register_bit_offset: u8,
gas_reserved: u8,
address: u64 align(1),
hpet_number: u8,
minimum_tick: u16 align(1),
page_protection: u8,
};
// --- Entry point ------------------------------------------------------------
/// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate
/// `device_tree`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the
/// FADT into `power_information`, and publishes the AML blobs for the ring-3 acpi service.
pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryRegion, device_tree: *DeviceTree, hal: Hal) !void {
if (rsdp_physical == 0) return error.NoRsdp;
boot_memory_regions = memory_regions;
// Start clean so a re-run doesn't accumulate stale state.
power_information = .{};
fadt_physical = 0;
fadt_length = 0;
platform_information = .{};
dsdt_physical = 0;
aml_block_count = 0;
const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical));
if (!std.mem.eql(u8, &rsdp.signature, "RSD PTR ")) return error.BadRsdpSignature;
// Revision 0 checksums only the first 20 bytes (the v1.0 RSDP).
if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical)), 20)) return error.BadRsdpChecksum;
if (rsdp.revision >= 2) {
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical));
if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical)), xsdp.length)) return error.BadXsdpChecksum;
try walkRoot(u64, xsdp.extended_system_descriptor_table_address, device_tree, hal);
} else {
try walkRoot(u32, rsdp.root_system_description_table_address, device_tree, hal);
}
// The kernel does **not** interpret the DSDT/SSDTs. Static-table discovery
// above (MADT/HPET/FADT/MCFG) is all the kernel needs — CPUs, timers, PCIe,
// and the power register map. The AML bytecode (device enumeration and the
// sleep-state `_Sx` values for soft-off) is entirely the ring-3 acpi service's
// job: it claims the acpi-tables node published below, parses the same blobs,
// and both registers the `_HID` devices and owns S5. Not parsing ~0.5 MB of
// AML in the kernel keeps boot latency off the critical, single-core path.
// Publish the acpi-tables node (docs/discovery.md): the AML blobs as
// memory resources for the acpi service to map and parse in ring 3, a broad
// io_port grant for the OperationRegion access its interpreter needs, and
// the SCI for the events track (M21). Exactly one node, one trusted
// claimant — the sole path by which AML (devices + soft-off) reaches ring 3,
// now that the kernel keeps only the *static* tables for itself.
publishAcpiTablesNode(device_tree) catch {};
}
/// Build the acpi-tables node (see the call site in discover). Best-effort: a
/// failure here leaves the kernel-seeded tree working, only the ring-3 service
/// finds nothing to claim.
fn publishAcpiTablesNode(device_tree: *DeviceTree) !void {
const node = try device_tree.addChild(device_tree.root, .acpi_tables, "acpi-tables");
// One memory resource per AML block — page-aligned base down, length padded
// up to cover the bytecode, so mmio_map hands the service a pointer into it.
var i: usize = 0;
while (i < aml_block_count and i < device_model.maximum_resources - 2) : (i += 1) {
// mmio_map preserves the sub-page offset, so the service maps this and
// gets a pointer straight to the bytecode.
_ = node.addResource(.memory, aml_block_physical[i], aml_block_len[i]);
}
// The broad I/O grant: OperationRegions name whatever ports the firmware
// chose (EC, PM1, GPE, SMBus); which ports cannot be known before the AML
// that names them is parsed, so the grant is the whole space — the honest
// trust boundary of docs/discovery.md (the acpi service's one trusted node).
_ = node.addResource(.io_port, 0, 1 << 16);
// A broad interrupt window: ACPI _CRS names legacy ISA IRQs (the PS/2 lines
// 1 and 12, the RTC, …), and the service registers those devices under this
// node, so it must own a superset. The range [0, 256) covers every GSI; the
// SCI (recorded first, len 1) stays distinct so M21 can pick it out.
if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1);
_ = node.addResource(.irq, 0, 256);
// The FADT rides along (M21): the service reads the PM1 event / GPE blocks
// from its own copy, telling it apart from the AML blobs by signature.
if (fadt_physical != 0) _ = node.addResource(.memory, fadt_physical, fadt_length);
}
/// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch
/// each SDT it points at. A bad individual table is skipped, not fatal.
fn walkRoot(comptime Entry: type, root_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
const header: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(root_physical));
if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(root_physical)), header.length)) return error.BadRootChecksum;
const count = (header.length - @sizeOf(SystemDescriptorTableHeader)) / @sizeOf(Entry);
const base: [*]const u8 = @ptrFromInt(boot_handoff.physicalToVirtual(root_physical));
const entries: [*]align(1) const Entry = @ptrCast(base + @sizeOf(SystemDescriptorTableHeader));
for (entries[0..count]) |ent| {
const sdt_physical: u64 = ent; // u32 entries widen; u64 pass through
handleTable(device_tree, hal, sdt_physical) catch continue;
}
}
/// Dispatch a single SDT on its signature.
fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void {
const header: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(sdt_physical));
const sig = header.signature;
if (std.mem.eql(u8, &sig, &APIC)) {
try parseMadt(device_tree, header);
} else if (std.mem.eql(u8, &sig, &MCFG)) {
try parseMcfg(device_tree, header);
} else if (std.mem.eql(u8, &sig, &HPET)) {
try parseHpet(device_tree, hal, header);
} else if (std.mem.eql(u8, &sig, &FACP)) {
fadt_physical = sdt_physical;
fadt_length = header.length;
parseFadt(header);
} else if (std.mem.eql(u8, &sig, &SPCR)) {
parseSpcr(header);
} else if (std.mem.eql(u8, &sig, &DMAR)) {
parseDmar(header);
} else if (std.mem.eql(u8, &sig, &IVRS)) {
parseIvrs(header);
} else if (std.mem.eql(u8, &sig, &SSDT)) {
// Secondary namespace bytecode — collect it to publish for the ring-3 parse.
addAmlBlock(sdt_physical);
}
// Any other signature is recognised but left opaque for now.
}
/// MADT -> one processor node per Local APIC, one interrupt_controller per I/O APIC.
fn parseMadt(device_tree: *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_information.lapic_base = madt.local_apic_address;
platform_information.pic_present = madt.flags & 1 != 0;
var off: usize = @sizeOf(Madt);
while (off + @sizeOf(MadtRecordHeader) <= total) {
const rec: *const MadtRecordHeader = @ptrCast(base + off);
if (rec.length < @sizeOf(MadtRecordHeader)) break; // malformed; avoid a spin
switch (rec.type) {
0 => {
const la: *const MadtLocalApic = @ptrCast(base + off);
// bit 0 = enabled: skip processors the firmware marks unusable.
if (la.flags & 1 != 0) {
var nb: [24]u8 = undefined;
const nm = std.fmt.bufPrint(&nb, "cpu{d}", .{la.processor_id}) catch "cpu";
_ = try device_tree.addChild(device_tree.root, .processor, nm);
// Also record it as a schedulable core (with the APIC ID an AP
// wake needs, which the device node name doesn't preserve).
if (cpu_information.count < cpu_information.cpus.len) {
cpu_information.cpus[cpu_information.count] = .{
.processor_id = la.processor_id,
.apic_id = la.apic_id,
.online_capable = la.flags & 2 != 0,
};
cpu_information.count += 1;
} else {
cpu_information.dropped += 1;
}
}
},
1 => {
const io: *const MadtIoApic = @ptrCast(base + off);
var nb: [24]u8 = undefined;
const nm = std.fmt.bufPrint(&nb, "ioapic{d}", .{ioapic_index}) catch "ioapic";
ioapic_index += 1;
const d = try device_tree.addChild(device_tree.root, .interrupt_controller, nm);
_ = d.addResource(.memory, io.address, 0x20);
// 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_information.override_count < platform_information.overrides.len) {
platform_information.overrides[platform_information.override_count] = .{
.source = iso.source,
.gsi = iso.gsi,
.flags = iso.flags,
};
platform_information.override_count += 1;
}
},
5 => {
const ovr: *const MadtLapicOverride = @ptrCast(base + off);
platform_information.lapic_base = ovr.address;
},
else => {},
}
off += rec.length;
}
}
/// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath.
fn parseMcfg(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeader) !void {
const total: usize = header.length;
const base: [*]const u8 = @ptrCast(header);
var off: usize = @sizeOf(Mcfg);
while (off + @sizeOf(McfgAllocation) <= total) : (off += @sizeOf(McfgAllocation)) {
const alloc: *const McfgAllocation = @ptrCast(base + off);
const bus_count: u64 = @as(u64, alloc.end_bus - alloc.start_bus) + 1;
var nb: [24]u8 = undefined;
const nm = std.fmt.bufPrint(&nb, "pci{d}", .{alloc.segment_group}) catch "pci";
const bridge = try device_tree.addChild(device_tree.root, .pci_host_bridge, nm);
// ECAM window: 1 MiB of configuration space per bus.
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
addBridgeApertures(bridge);
// The bridge decodes the whole 16-bit I/O space toward its bus — the
// window functions' I/O BARs must register-contain within (M19.2).
_ = bridge.addResource(.io_port, 0, 1 << 16);
// The function walk itself retired to ring 3 (M19.3): the pci-bus
// driver claims this bridge, repeats the scan through its ECAM grant,
// and device_registers what it finds — the kernel seeds only the
// bridge. The scan's equivalence was proven before the hand-off
// (pci-scan), and the walk's history is in git if archaeology calls.
}
}
/// The boot memory map, stored at discover() entry for the aperture derivation
/// below (and, in M20, for the acpi-tables node's containment windows).
var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
/// The bridge's MMIO apertures, derived from the boot memory map's holes
/// (docs/discovery.md — apertures from the memory map): registered PCI functions carry BAR
/// resources, and `device_register` containment demands the bridge own windows
/// that cover them. Everything the firmware described is "not hole"; the low
/// aperture runs from the end of the described space below 4 GiB up to the
/// I/O-APIC region, the high one from 4 GiB (or the end of RAM above it) to
/// the 46-bit line. Coarse, mechanical, and AML-free — available at boot no
/// matter what later moved to user space.
fn addBridgeApertures(bridge: *device_model.Device) void {
// Below 4 GiB the described regions are sparse (RAM low, firmware flash
// and tables high), so the holes are the *gaps between* them — a single
// "after the last region" rule dies on OVMF's flash at the very top.
// Sort-merge the described ranges, then keep the three largest gaps
// (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
// high aperture fits). Above 4 GiB one aperture runs from the end of the
// described space to the 46-bit line.
const Range = struct { base: u64, end: u64 };
var below: [64]Range = undefined;
var below_count: usize = 0;
var high_end: u64 = 1 << 32;
for (boot_memory_regions) |region| {
const end = region.base + region.pages * 4096;
// Above 4 GiB only *usable RAM* blocks the aperture: OVMF describes
// its own 64-bit PCI window as a reserved region and then programs
// BARs inside it — honoring reserved there would exclude the very
// space BARs live in. Below 4 GiB every described region blocks (the
// kernel image, the tables, the ramdisk all live there). Bring-up
// trust: only the bridge's claimant can register into the aperture.
if (region.kind == .usable and end > high_end) high_end = end;
if (region.base >= (1 << 32) or below_count == below.len) continue;
below[below_count] = .{ .base = region.base, .end = @min(end, 1 << 32) };
below_count += 1;
}
// Insertion sort by base (the map is small and this runs once at boot).
for (1..below_count) |i| {
const key = below[i];
var j = i;
while (j > 0 and below[j - 1].base > key.base) : (j -= 1) below[j] = below[j - 1];
below[j] = key;
}
// Walk the sorted ranges, collecting inter-region gaps of at least 1 MiB.
var gaps: [3]Range = .{Range{ .base = 0, .end = 0 }} ** 3;
var cursor: u64 = 0;
var index: usize = 0;
while (index <= below_count) : (index += 1) {
const gap_end = if (index == below_count) (1 << 32) else below[index].base;
if (gap_end > cursor and gap_end - cursor >= (1 << 20)) {
// Keep the three largest, replacing the smallest kept so far.
var smallest: usize = 0;
for (gaps, 0..) |gap, gi| {
if (gap.end - gap.base < gaps[smallest].end - gaps[smallest].base) smallest = gi;
}
if (gap_end - cursor > gaps[smallest].end - gaps[smallest].base) {
gaps[smallest] = .{ .base = cursor, .end = gap_end };
}
}
if (index < below_count and below[index].end > cursor) cursor = below[index].end;
}
for (gaps) |gap| {
if (gap.end > gap.base) _ = bridge.addResource(.memory, gap.base, gap.end - gap.base);
}
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
}
/// HPET -> a timer node with its register block as an MMIO resource, plus the GSI
/// its comparators can raise.
///
/// Unlike a PCI device or an ACPI `_CRS` node, the HPET table carries **no interrupt
/// number**: which I/O APIC inputs a comparator may drive is advertised at runtime,
/// as a bitmask in `Tn_INT_ROUTE_CAP` (bits 63:32 of the Timer 0 configuration register).
/// So discovery maps the register block, reads the mask, and records one concrete
/// `irq` resource — the GSI a driver is entitled to bind. The driver commits to it
/// by writing `Tn_INT_ROUTE_CNF`; the kernel checks the binding against this
/// resource (see process.ownedGsi), which is what keeps `irq_bind` a capability
/// rather than a request for an arbitrary interrupt line.
fn parseHpet(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptorTableHeader) !void {
const hpet: *const Hpet = @ptrCast(header);
const d = try device_tree.addChild(device_tree.root, .timer, "hpet");
// The GAS tag must say System Memory (0) before we treat `address` as a physical
// address. The HPET spec mandates it, but firmware is not a thing to trust: a
// System I/O (1) tag here would have us map an arbitrary page and read a bogus
// route-capability mask out of it.
if (hpet.address_space_id != gas_system_memory) return;
_ = d.addResource(.memory, hpet.address, 0x400);
const regs = hal.mapMmio(hpet.address, 0x400, true);
const t0_configuration: *const volatile u64 = @ptrFromInt(regs + 0x100);
const route_cap: u32 = @truncate(t0_configuration.* >> 32);
if (hpetGsi(route_cap)) |gsi| _ = d.addResource(.irq, gsi, 1);
}
/// ACPI Generic Address Structure address-space ids we care about.
const gas_system_memory: u8 = 0;
/// Pick a GSI for the HPET out of its route-capability mask. Prefer an input at or
/// above 16: the low ones overlap the legacy ISA lines (2 = cascaded PIT, 8 = RTC),
/// which the MADT may separately override, whereas 16+ are the free upper inputs on
/// every I/O APIC we care about. Falls back to the lowest bit set if there are none.
fn hpetGsi(route_cap: u32) ?u32 {
if (route_cap == 0) return null;
var gsi: u32 = 16;
while (gsi < 32) : (gsi += 1) {
if (route_cap & (@as(u32, 1) << @intCast(gsi)) != 0) return gsi;
}
return @ctz(route_cap);
}
// FADT field offsets (bytes from the table start). The FADT grew across ACPI
// revisions, so every field is read through `fadt()` with a length guard rather
// than a fixed struct — an older/shorter FADT simply lacks the later (X_) fields.
const fadt_dsdt = 40; // u32
const fadt_smi_cmd = 48; // u32 (an I/O port)
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_sci_int = 46; // u16 (the SCI's GSI)
const fadt_flags = 112; // u32
const fadt_reset_register = 116; // GAS (12 bytes)
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_register_supported = 1 << 10;
const flag_tmr_value_ext = 1 << 8; // PM timer counter is 32-bit (else 24-bit)
/// FADT -> the power register map (into `power_information`) and the DSDT address,
/// whose bytecode is collected for the ring-3 parse. No AML interpretation here.
fn parseFadt(header: *const SystemDescriptorTableHeader) void {
const base: [*]align(1) const u8 = @ptrCast(header);
const len: usize = header.length;
const pi = &power_information;
pi.sci_interrupt = @truncate(fadt(u16, base, len, fadt_sci_int) orelse 0);
pi.smi_cmd = @truncate(fadt(u32, base, len, fadt_smi_cmd) orelse 0);
pi.acpi_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0;
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
const cnt_width = fadt(u8, base, len, fadt_pm1_cnt_len) orelse 2;
pi.pm1a_cnt = readCntRegister(base, len, fadt_x_pm1a_cnt_blk, fadt_pm1a_cnt_blk, cnt_width);
pi.pm1b_cnt = readCntRegister(base, len, fadt_x_pm1b_cnt_blk, fadt_pm1b_cnt_blk, cnt_width);
const flags = fadt(u32, base, len, fadt_flags) orelse 0;
pi.reset_supported = flags & flag_reset_register_supported != 0;
pi.reset = readGas(base, len, fadt_reset_register) 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_information.pm_timer = readCntRegister(base, len, fadt_x_pm_tmr_blk, fadt_pm_tmr_blk, 4);
platform_information.pm_timer_32bit = flags & flag_tmr_value_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;
}
dsdt_physical = dsdt;
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_information.spcr_uart = gas;
platform_information.spcr_kind = fadt(u8, base, len, spcr_interface_type) orelse 0;
}
// DMAR remapping-structure layout (Intel VT-d spec §8): the DMAR-specific header is 12
// bytes (host-address-width, flags, 10 reserved), then a list of {type u16, length u16}
// structures. Type 0 is a DRHD (DMA Remapping Hardware Unit Definition): flags byte at
// offset 4 (bit 0 = INCLUDE_PCI_ALL, the catch-all unit), 64-bit register base at
// offset 8. Type 1 is an RMRR (Reserved Memory Region Reporting): a physical range at
// offsets 8/16 (base / inclusive limit) that the device(s) named by the trailing
// device-scope entries keep DMAing into across the firmware→OS handoff.
const dmar_structures_offset = 48; // 36-byte ACPI header + 12-byte DMAR header
const dmar_type_drhd: u16 = 0;
const dmar_type_rmrr: u16 = 1;
const drhd_flags_offset = 4;
const drhd_include_pci_all: u8 = 1;
const drhd_register_base_offset = 8;
const rmrr_base_offset = 8;
const rmrr_limit_offset = 16;
const rmrr_scopes_offset = 24;
// Device-scope entry (within DRHD/RMRR structures): type 1 = PCI endpoint; the path is
// (device, function) byte pairs from offset 6, one pair per bridge hop plus the leaf.
const scope_type_pci_endpoint: u8 = 1;
const scope_start_bus_offset = 5;
const scope_path_offset = 6;
/// DMAR -> the VT-d unit(s) and reserved memory regions. Walks every remapping
/// structure: selects the INCLUDE_PCI_ALL DRHD (the catch-all covering all devices not
/// scoped elsewhere — commonly the SECOND unit on real machines, after an iGPU-scoped
/// one), counts the rest so the boot log can warn that their devices stay untranslated,
/// and records single-path endpoint RMRRs for the IOMMU core to pre-map before it
/// enables translation. Multi-hop RMRR scopes are skipped loudly: better a named gap
/// than a silent one.
fn parseDmar(header: *const SystemDescriptorTableHeader) void {
const base: [*]align(1) const u8 = @ptrCast(header);
const total: usize = header.length;
var off: usize = dmar_structures_offset;
while (off + 4 <= total) {
const kind = fadt(u16, base, total, off) orelse break;
const length = fadt(u16, base, total, off + 2) orelse break;
if (length < 4 or off + length > total) break; // malformed; stop rather than loop
if (kind == dmar_type_drhd) {
const register_base = fadt(u64, base, total, off + drhd_register_base_offset) orelse 0;
const include_all = ((fadt(u8, base, total, off + drhd_flags_offset) orelse 0) & drhd_include_pci_all) != 0;
if (register_base != 0) {
// Selection: the INCLUDE_PCI_ALL unit wins; otherwise keep the first
// seen. A later catch-all replaces an earlier scoped unit.
const replace = !platform_information.iommu_present or
(include_all and !platform_information.iommu_include_all);
if (replace) {
// Table facts only: the unit's registers are the IOMMU
// backend's business (it maps and validates them at
// detect) — discovery records where they live, never
// reads them.
if (platform_information.iommu_present) platform_information.iommu_extra_units += 1;
platform_information.iommu_present = true;
platform_information.iommu_base = register_base;
platform_information.iommu_include_all = include_all;
} else {
platform_information.iommu_extra_units += 1;
}
}
} else if (kind == dmar_type_rmrr) {
parseRmrr(base, total, off, length);
}
off += length;
}
}
/// One RMRR structure: record a {bdf, base, limit} per single-path endpoint scope.
fn parseRmrr(base: [*]align(1) const u8, total: usize, off: usize, length: usize) void {
const range_base = fadt(u64, base, total, off + rmrr_base_offset) orelse return;
const range_limit = fadt(u64, base, total, off + rmrr_limit_offset) orelse return;
if (range_limit < range_base) return;
var scope = off + rmrr_scopes_offset;
const end = off + length;
while (scope + 6 <= end) {
const scope_type = fadt(u8, base, total, scope) orelse break;
const scope_length = fadt(u8, base, total, scope + 1) orelse break;
if (scope_length < 6 or scope + scope_length > end) break;
if (scope_type == scope_type_pci_endpoint and scope_length == scope_path_offset + 2) {
// Single (device, function) pair: a directly-reachable endpoint.
const bus = fadt(u8, base, total, scope + scope_start_bus_offset) orelse 0;
const device = fadt(u8, base, total, scope + scope_path_offset) orelse 0;
const function = fadt(u8, base, total, scope + scope_path_offset + 1) orelse 0;
if (platform_information.rmrr_count < maximum_rmrr) {
platform_information.rmrr[platform_information.rmrr_count] = .{
.bdf = (@as(u16, bus) << 8) | (@as(u16, device) << 3) | function,
.base = range_base,
.limit = range_limit,
};
platform_information.rmrr_count += 1;
} else {
platform_information.rmrr_skipped +|= 1; // table full
}
} else {
platform_information.rmrr_skipped +|= 1; // multi-hop path or non-endpoint scope
}
scope += scope_length;
}
}
// IVRS layout (AMD I/O Virtualization spec): 36-byte ACPI header, IVinfo u32 @36,
// 8 reserved @40, then IVHD/IVMD blocks from @48. An IVHD common header is type u8 @0,
// flags u8 @1, length u16 @2, device id u16 @4, capability offset u16 @6, IOMMU base
// address u64 @8, PCI segment u16 @16, IOMMU info u16 @18.
const ivrs_blocks_offset = 48;
const ivhd_type_10: u8 = 0x10;
const ivhd_type_11: u8 = 0x11;
const ivhd_base_offset = 8;
/// IVRS -> detect an AMD-Vi IOMMU. Record the control-register base from the first IVHD
/// of type 0x10/0x11. Per-device entries and IVMD (the AMD analogue of RMRR) are ignored
/// in v1 — the default-deny device table is what we build anyway, and QEMU emits no IVMD;
/// a real machine that needs them is flagged untested on AMD regardless.
fn parseIvrs(header: *const SystemDescriptorTableHeader) void {
const base: [*]align(1) const u8 = @ptrCast(header);
const total: usize = header.length;
var off: usize = ivrs_blocks_offset;
while (off + 4 <= total) {
const kind = fadt(u8, base, total, off) orelse break;
const length = fadt(u16, base, total, off + 2) orelse break;
if (length < 4 or off + length > total) break;
if (kind == ivhd_type_10 or kind == ivhd_type_11) {
const iommu_base = fadt(u64, base, total, off + ivhd_base_offset) orelse 0;
if (iommu_base != 0) {
platform_information.iommu_present = true;
platform_information.iommu_is_amd = true;
platform_information.iommu_base = iommu_base;
return; // first IVHD is enough; multi-unit is future work
}
}
off += length;
}
}
// --- helpers ----------------------------------------------------------------
/// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero.
fn checksumOk(bytes: [*]const u8, len: usize) bool {
var sum: u8 = 0;
for (0..len) |i| sum +%= bytes[i];
return sum == 0;
}
/// Read a FADT field of type `T` at `off`, or null if the table is too short to
/// contain it (a legal state for older FADT revisions).
fn fadt(comptime T: type, base: [*]align(1) const u8, len: usize, off: usize) ?T {
if (off + @sizeOf(T) > len) return null;
return rd(T, base, off);
}
/// Decode a Generic Address Structure at `off` into a `RegisterAccess`. GAS layout:
/// address_space(u8), bit_width(u8), bit_offset(u8), access_size(u8), address(u64).
fn readGas(base: [*]align(1) const u8, len: usize, off: usize) ?RegisterAccess {
if (off + 12 > len) return null;
const address_space = rd(u8, base, off);
const bit_width = rd(u8, base, off + 1);
const address = rd(u64, base, off + 4);
return .{
.mmio = address_space == 0, // 0 = system memory, 1 = system I/O
.address = address,
.width = bit_width / 8,
};
}
/// A PM1 control register: prefer the 64-bit-capable X_ GAS form; fall back to the
/// legacy 32-bit I/O-port field. Width comes from PM1_CNT_LEN either way.
fn readCntRegister(base: [*]align(1) const u8, len: usize, xoff: usize, legacy_off: usize, width: u8) RegisterAccess {
if (readGas(base, len, xoff)) |g| {
if (g.address != 0) return .{ .mmio = g.mmio, .address = g.address, .width = width };
}
const port = fadt(u32, base, len, legacy_off) orelse 0;
return .{ .mmio = false, .address = port, .width = width };
}
/// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer.
/// x86 is little-endian and native, so an unaligned load suffices.
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
const p: *align(1) const T = @ptrCast(bytes + off);
return p.*;
}
// --- machine reboot -------------------------------------------------------------
// Restart via the FADT reset register (from `power_information` above), with legacy
// fallbacks, driven through the injected `Hal`. Soft-off (ACPI S5) and suspend (S3)
// are NOT here: they need the AML sleep-state (`_Sx`) values, which the kernel no
// longer parses — the ring-3 acpi service owns power management (it re-parses the
// blobs and writes the PM1 control register itself). Reboot stays in the kernel
// because it needs no AML — only the FADT reset register and the well-known legacy
// fallbacks — so it survives as a last-resort restart. See docs/power.md.
/// Restart the machine. Tries the ACPI reset register first, then the two legacy
/// fallbacks. Returns only if every method failed (very unlikely).
pub fn reboot(hal: device_model.Hal) void {
const pi = power_information;
// 1. The FADT reset register, when the firmware advertises support.
if (pi.reset_supported and pi.reset.present()) {
writeResetRegister(hal, pi.reset, pi.reset_value);
rebootDelay();
}
// 2. The PCI reset-control register at port 0xCF9 (RST_CPU | SYSTEM_RST).
hal.pioWrite(1, 0xCF9, 0x0E);
hal.pioWrite(1, 0xCF9, 0x06);
rebootDelay();
// 3. Pulse the 8042 keyboard controller's reset line.
hal.pioWrite(1, 0x64, 0xFE);
rebootDelay();
}
fn writeResetRegister(hal: device_model.Hal, register: RegisterAccess, value: u32) void {
if (register.mmio) {
const p: *align(1) volatile u32 = @ptrFromInt(hal.mapMmio(register.address, 4, true));
p.* = value;
} else {
hal.pioWrite(register.width, @intCast(register.address), value);
}
}
/// A short busy-wait so a reset takes effect before we fall through to the next
/// method. The empty asm is an architecture-neutral barrier that keeps the loop
/// from being optimised away.
fn rebootDelay() void {
var i: usize = 0;
while (i < 50_000_000) : (i += 1) {
asm volatile ("" ::: .{ .memory = true });
}
}