862 lines
40 KiB
Zig
862 lines
40 KiB
Zig
//! 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
|
||
/// (INIT–SIPI–SIPI); `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.*;
|
||
}
|