danos/system/devices/acpi.zig

862 lines
40 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: MADT (CPUs / interrupt
//! controllers), MCFG (PCIe ECAM -> PCI enumeration), HPET (timer), and FADT
//! (power register map). The DSDT/SSDT bytecode is handed to the `aml` submodule
//! only to extract the sleep-state (`_Sx`) values for power management; full AML namespace
//! interpretation is a separate, larger subproject.
//!
//! 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 aml = @import("aml/aml.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;
}
};
/// Everything the power subsystem needs, extracted from the FADT and the AML
/// sleep packages during discovery. Populated by `discover`, read by `power`.
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,
/// SLP_TYP values for S5 (soft off) and S3 (suspend), from the AML sleep-state (`_Sx`)
/// packages.
s5: ?aml.SleepType = null,
s3: ?aml.SleepType = null,
};
/// Filled in by `discover`; the power service reads it to reboot/shutdown.
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).
/// Detection is the first step; per-device domain enforcement lands with the first
/// DMA driver.
iommu_present: bool = false,
/// MMIO base of the first DMA-remapping hardware unit (DMAR DRHD), when present.
iommu_base: u64 = 0,
/// The unit's Version register (offset 0x00) — its low byte is major.minor;
/// reading it back nonzero confirms a real, mappable VT-d unit.
iommu_version: u32 = 0,
/// The unit's Capability register (offset 0x08): supported address widths, number
/// of domains, etc. Recorded now; consumed when enforcement is built.
iommu_capabilities: u64 = 0,
};
/// 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 = .{};
/// Integrity/diagnostics for the AML parse. `consumed == total` means the parser
/// walked every byte of the DSDT/SSDTs without desyncing.
pub const AmlStats = struct {
nodes: usize = 0,
consumed: usize = 0,
total: usize = 0,
};
pub var aml_stats: AmlStats = .{};
/// The ACPI namespace built from the DSDT/SSDTs, kept for sleep-state (`_Sx`) lookup now and
/// device enumeration later. Null until `discover` runs successfully.
pub var namespace: ?aml.Namespace = null;
/// Physical address of the DSDT the FADT points at, or 0.
pub var dsdt_physical: u64 = 0;
// AML blocks (DSDT + any SSDTs) collected during the table walk, as physical
// address + length of each table's post-header bytecode. Scanned after the walk
// for the sleep-state (`_Sx`) packages.
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".*;
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 and the AML sleep-state (`_Sx`) packages into `power_information` for the power 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 = .{};
platform_information = .{};
aml_stats = .{};
namespace = null;
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);
}
// Now that the DSDT and any SSDTs are collected, build the AML namespace and
// read the sleep types from it.
var blocks: [aml_block_physical.len][]const u8 = undefined;
for (0..aml_block_count) |i| {
blocks[i] = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(aml_block_physical[i])))[0..aml_block_len[i]];
}
const active = blocks[0..aml_block_count];
if (aml.parse(device_tree.allocator, active)) |pr| {
namespace = pr.namespace;
aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total };
power_information.s5 = aml.sleepState(&namespace.?, 5);
power_information.s3 = aml.sleepState(&namespace.?, 3);
// The namespace's Device objects are no longer folded into the kernel
// tree (M20.3): the ring-3 acpi service claims the acpi-tables node
// (published below), re-parses the same blobs, and registers + reports
// the _HID devices itself. The kernel keeps the namespace only for the
// \_S5 sleep type above.
} else |_| {
// AML parse failed (e.g. out of memory); power stays best-effort with
// whatever the FADT alone provided.
}
// Publish the acpi-tables node (docs/m19-m20-plan.md M20): 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. Kept even when the kernel-side device building (above) retires
// in M20.3 — the kernel still owns the *static* tables and \_S5.
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/m19-m20-plan.md decision 5.
_ = 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 number of Device objects in the namespace built during discovery, or 0.
pub fn amlDeviceCount() usize {
if (namespace) |*ns| return aml.deviceCount(ns);
return 0;
}
/// 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)) {
parseFadt(header);
} else if (std.mem.eql(u8, &sig, &SPCR)) {
parseSpcr(header);
} else if (std.mem.eql(u8, &sig, &DMAR)) {
parseDmar(hal, header);
} else if (std.mem.eql(u8, &sig, &SSDT)) {
// Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan.
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/m19-m20-plan.md decision 2): 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, which
/// is queued for the AML sleep-state (`_Sx`) scan. No AML interpretation happens 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), whose 64-bit
// register base sits at offset 8 within it.
const dmar_structures_offset = 48; // 36-byte ACPI header + 12-byte DMAR header
const dmar_type_drhd: u16 = 0;
const drhd_register_base_offset = 8;
/// DMAR -> detect the IOMMU. Find the first DMA-remapping hardware unit, map its
/// register block, and record its version and capabilities. This is *detection only*:
/// it tells the system an IOMMU exists (so `device_claim` on a DMA device could one day
/// be gated by a per-device translation domain), but no domains are programmed yet —
/// enforcement is built with the first DMA driver, which is what there is to protect and
/// test against. See docs/driver-model.md (M16), the honest caveat.
fn parseDmar(hal: Hal, 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;
if (register_base != 0) {
const regs = hal.mapMmio(register_base, abi.page_size, true);
platform_information.iommu_present = true;
platform_information.iommu_base = register_base;
platform_information.iommu_version = @as(*const volatile u32, @ptrFromInt(regs + 0x00)).*;
platform_information.iommu_capabilities = @as(*const volatile u64, @ptrFromInt(regs + 0x08)).*;
return; // first unit is enough for detection; multi-unit is future
}
}
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.*;
}