add device platform module with ACPI support

This commit is contained in:
Daniel Samson
2026-07-08 09:23:41 +01:00
parent 53a33a7332
commit 2ee898a91e
21 changed files with 3545 additions and 5 deletions
+987
View File
@@ -0,0 +1,987 @@
//! 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 config-space pages are mapped on demand via
//! the `Hal.mapMmio` callback the caller supplies (the arch VMM's map primitive).
const std = @import("std");
const device = @import("device.zig");
const aml = @import("aml/aml.zig");
const DeviceTree = device.DeviceTree;
const Hal = device.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 RegAccess = 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: RegAccess) 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 PowerInfo = struct {
/// 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: RegAccess = .{},
pm1b_cnt: RegAccess = .{},
/// The FADT reset register and the value to write to it.
reset: RegAccess = .{},
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_info: PowerInfo = .{};
/// 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_phys: 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_phys: [32]u64 = undefined;
var aml_block_len: [32]usize = undefined;
var aml_block_count: usize = 0;
fn addAmlBlock(sdt_phys: u64) void {
if (aml_block_count >= aml_block_phys.len or sdt_phys == 0) return;
const h: *const SystemDescriptorTableHeader = @ptrFromInt(sdt_phys);
if (h.length <= @sizeOf(SystemDescriptorTableHeader)) return;
aml_block_phys[aml_block_count] = sdt_phys + @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 SSDT: [4]u8 = "SSDT".*;
/// 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),
};
// --- 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,
};
// --- PCI configuration-space header (first 64 bytes, common fields) ---------
const PciHeader = extern struct {
vendor_id: u16 align(1),
device_id: u16 align(1),
command: u16 align(1),
status: u16 align(1),
revision_id: u8,
prog_if: u8,
subclass: u8,
class_code: u8,
cache_line_size: u8,
latency_timer: u8,
/// bit 7 set => multi-function device.
header_type: u8,
bist: u8,
// 0x10 onward (BARs, etc.) depends on header_type; read separately.
};
// --- Entry point ------------------------------------------------------------
/// Discover hardware from the ACPI tables rooted at `rsdp_phys` and populate
/// `dt`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the
/// FADT and the AML sleep-state (`_Sx`) packages into `power_info` for the power service.
pub fn discover(rsdp_phys: u64, dt: *DeviceTree, hal: Hal) !void {
if (rsdp_phys == 0) return error.NoRsdp;
// Start clean so a re-run doesn't accumulate stale state.
power_info = .{};
aml_stats = .{};
namespace = null;
dsdt_phys = 0;
aml_block_count = 0;
const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(rsdp_phys);
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(rsdp_phys), 20)) return error.BadRsdpChecksum;
if (rsdp.revision >= 2) {
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(rsdp_phys);
if (!checksumOk(@ptrFromInt(rsdp_phys), xsdp.length)) return error.BadXsdpChecksum;
try walkRoot(u64, xsdp.extended_system_descriptor_table_address, dt, hal);
} else {
try walkRoot(u32, rsdp.root_system_description_table_address, dt, 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_phys.len][]const u8 = undefined;
for (0..aml_block_count) |i| {
blocks[i] = @as([*]const u8, @ptrFromInt(aml_block_phys[i]))[0..aml_block_len[i]];
}
const active = blocks[0..aml_block_count];
if (aml.parse(dt.allocator, active)) |pr| {
namespace = pr.namespace;
aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total };
power_info.s5 = aml.sleepState(&namespace.?, 5);
power_info.s3 = aml.sleepState(&namespace.?, 3);
// Fold the namespace's Device objects into the generic tree.
wireAcpiDevices(dt, &namespace.?, hal) catch {};
} else |_| {
// AML parse failed (e.g. out of memory); power stays best-effort with
// whatever the FADT alone provided.
}
}
/// 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_phys: u64, dt: *DeviceTree, hal: Hal) !void {
const header: *const SystemDescriptorTableHeader = @ptrFromInt(root_phys);
if (!checksumOk(@ptrFromInt(root_phys), header.length)) return error.BadRootChecksum;
const count = (header.length - @sizeOf(SystemDescriptorTableHeader)) / @sizeOf(Entry);
const base: [*]const u8 = @ptrFromInt(root_phys);
const entries: [*]align(1) const Entry = @ptrCast(base + @sizeOf(SystemDescriptorTableHeader));
for (entries[0..count]) |ent| {
const sdt_phys: u64 = ent; // u32 entries widen; u64 pass through
handleTable(dt, hal, sdt_phys) catch continue;
}
}
/// Dispatch a single SDT on its signature.
fn handleTable(dt: *DeviceTree, hal: Hal, sdt_phys: u64) !void {
const header: *const SystemDescriptorTableHeader = @ptrFromInt(sdt_phys);
const sig = header.signature;
if (std.mem.eql(u8, &sig, &APIC)) {
try parseMadt(dt, header);
} else if (std.mem.eql(u8, &sig, &MCFG)) {
try parseMcfg(dt, hal, header);
} else if (std.mem.eql(u8, &sig, &HPET)) {
try parseHpet(dt, header);
} else if (std.mem.eql(u8, &sig, &FACP)) {
parseFadt(header);
} else if (std.mem.eql(u8, &sig, &SSDT)) {
// Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan.
addAmlBlock(sdt_phys);
}
// 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(dt: *DeviceTree, header: *const SystemDescriptorTableHeader) !void {
const total: usize = header.length;
const base: [*]const u8 = @ptrCast(header);
var ioapic_index: usize = 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 dt.addChild(dt.root, .processor, nm);
}
},
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 dt.addChild(dt.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);
},
else => {},
}
off += rec.length;
}
}
/// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath.
fn parseMcfg(dt: *DeviceTree, hal: Hal, 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 dt.addChild(dt.root, .pci_host_bridge, nm);
// ECAM window: 1 MiB of config space per bus.
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
try enumeratePci(dt, bridge, hal, alloc.*);
}
}
/// Brute-force scan the ECAM window's bus range for present PCI functions. No
/// bridge recursion yet: on the ECAM path the host bridge decodes every bus in
/// the window, so scanning the declared range finds everything QEMU exposes.
fn enumeratePci(
dt: *DeviceTree,
bridge: *device.Device,
hal: Hal,
alloc: McfgAllocation,
) !void {
var bus: u16 = alloc.start_bus;
while (bus <= alloc.end_bus) : (bus += 1) {
var dev: u8 = 0;
while (dev < 32) : (dev += 1) {
const h0: *align(1) const PciHeader = @ptrCast(pciConfigPtr(alloc, hal, @intCast(bus), dev, 0));
if (h0.vendor_id == 0xFFFF) continue; // no function 0 => slot empty
const funcs: u8 = if (h0.header_type & 0x80 != 0) 8 else 1;
var func: u8 = 0;
while (func < funcs) : (func += 1) {
const cfg = pciConfigPtr(alloc, hal, @intCast(bus), dev, func);
const h: *align(1) const PciHeader = @ptrCast(cfg);
if (h.vendor_id == 0xFFFF) continue;
var nb: [24]u8 = undefined;
const nm = std.fmt.bufPrint(&nb, "{s}:{x:0>2}:{x:0>2}.{d}", .{
bridge.name(), bus, dev, func,
}) catch "pcidev";
const node = try dt.addChild(bridge, .pci_device, nm);
node.ids.pci_vendor = h.vendor_id;
node.ids.pci_device = h.device_id;
node.ids.pci_class = (@as(u24, h.class_code) << 16) |
(@as(u24, h.subclass) << 8) | h.prog_if;
node.ids.pci_bdf = (@as(u16, @intCast(bus)) << 8) | (@as(u16, dev) << 3) | func;
// BARs only exist in header type 0 (normal devices), not bridges.
if (h.header_type & 0x7F == 0) addBars(node, cfg);
}
}
}
}
/// Record and size the memory/IO windows named by a device's Base Address
/// Registers. Sizing is the standard probe: disable decode, write all-ones, read
/// back the writable (address) bits, restore. `size = ~mask + 1`.
fn addBars(node: *device.Device, cfg: [*]align(1) u8) void {
// Stop the device decoding its BARs while we transiently write all-ones.
const command = rd(u16, cfg, 0x04);
wr(u16, cfg, 0x04, command & ~@as(u16, 0b11));
var i: usize = 0;
while (i < 6) : (i += 1) {
const off = 0x10 + i * 4;
const orig = rd(u32, cfg, off);
if (orig == 0) continue;
if (orig & 1 != 0) {
// I/O-space BAR (16-bit address space on x86).
wr(u32, cfg, off, 0xFFFF_FFFF);
const readback = rd(u32, cfg, off);
wr(u32, cfg, off, orig);
const mask = readback & 0xFFFF_FFFC;
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
_ = node.addResource(.io_port, orig & 0xFFFF_FFFC, size);
} else if ((orig >> 1) & 0x3 == 2) {
// 64-bit memory BAR: this BAR pair spans two config slots.
const orig_hi = rd(u32, cfg, off + 4);
wr(u32, cfg, off, 0xFFFF_FFFF);
wr(u32, cfg, off + 4, 0xFFFF_FFFF);
const lo = rd(u32, cfg, off);
const hi = rd(u32, cfg, off + 4);
wr(u32, cfg, off, orig);
wr(u32, cfg, off + 4, orig_hi);
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
const addr = (@as(u64, orig_hi) << 32) | (orig & 0xFFFF_FFF0);
_ = node.addResource(.memory, addr, size);
i += 1; // consumed the high half
} else {
// 32-bit memory BAR.
wr(u32, cfg, off, 0xFFFF_FFFF);
const readback = rd(u32, cfg, off);
wr(u32, cfg, off, orig);
const mask = readback & 0xFFFF_FFF0;
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
_ = node.addResource(.memory, orig & 0xFFFF_FFF0, size);
}
}
wr(u16, cfg, 0x04, command); // restore decode
}
/// HPET -> a timer node with its register block as an MMIO resource.
fn parseHpet(dt: *DeviceTree, header: *const SystemDescriptorTableHeader) !void {
const hpet: *const Hpet = @ptrCast(header);
const d = try dt.addChild(dt.root, .timer, "hpet");
_ = d.addResource(.memory, hpet.address, 0x400);
}
// 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_pm1_cnt_len = 89; // u8 (bytes)
const fadt_flags = 112; // u32
const fadt_reset_reg = 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 flag_reset_reg_supported = 1 << 10;
/// FADT -> the power register map (into `power_info`) and the DSDT address, which
/// is queued for the AML sleep-state (`_Sx`) scan. No AML interpretation happens here.
fn parseFadt(header: *const SystemDescriptorTableHeader) void {
const base: [*]align(1) const u8 = @ptrCast(header);
const len: usize = header.length;
const pi = &power_info;
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 = readCntReg(base, len, fadt_x_pm1a_cnt_blk, fadt_pm1a_cnt_blk, cnt_width);
pi.pm1b_cnt = readCntReg(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_reg_supported != 0;
pi.reset = readGas(base, len, fadt_reset_reg) orelse .{};
pi.reset_value = fadt(u8, base, len, fadt_reset_value) orelse 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_phys = dsdt;
addAmlBlock(dsdt);
}
// --- AML namespace -> generic device tree -----------------------------------
/// The PCI bus context while descending the ACPI namespace: the generic host
/// bridge whose children ACPI address (`_ADR`) devices resolve against, and the bus number.
const PciCtx = struct { bridge: *device.Device, bus: u8 };
/// Mirror the ACPI namespace's Device objects into the generic tree, *merging*
/// them with the PCI-enumerated nodes: a PCI root bridge (`PNP0A03`/`PNP0A08`)
/// folds onto the existing `pci_host_bridge`, and each addressed (`_ADR`) device folds onto
/// the matching PCI function (annotating it with the ACPI hardware ID (`_HID`) and nesting the
/// ACPI-only children — keyboard, RTC, … — beneath it). Namespace devices with no
/// PCI match land under a synthetic `acpi` node.
fn wireAcpiDevices(dt: *DeviceTree, nsp: *aml.Namespace, hal: Hal) !void {
var arena = std.heap.ArenaAllocator.init(dt.allocator);
defer arena.deinit();
var ev = aml.Interp.init(nsp, .{
.mapMmio = hal.mapMmio,
.pioRead = hal.pioRead,
.pioWrite = hal.pioWrite,
}, arena.allocator());
const acpi_root = try dt.addChild(dt.root, .unknown, "acpi");
try mirrorDevices(dt, nsp.root, acpi_root, null, &ev);
}
fn mirrorDevices(dt: *DeviceTree, node: *aml.Node, parent_dev: *device.Device, ctx: ?PciCtx, ev: *aml.Interp) (error{OutOfMemory})!void {
var child = node.first_child;
while (child) |c| : (child = c.next_sibling) {
if (c.kind != .device) {
// A scope — the System Bus (\_SB), General Purpose Events (\_GPE), … —
// descend without adding a node.
try mirrorDevices(dt, c, parent_dev, ctx, ev);
continue;
}
// Skip devices the firmware reports as not present (via a device-status (`_STA`) method),
// along with their whole subtree — per the ACPI rules.
if (!devicePresent(ev, c)) continue;
var gdev: *device.Device = undefined;
var child_ctx = ctx;
if (isPciRootNode(c)) {
// The PCI root bridge folds onto the generic host bridge.
gdev = matchHostBridge(dt) orelse
try dt.addChild(parent_dev, .acpi_device, &c.seg);
child_ctx = .{ .bridge = gdev, .bus = 0 };
} else {
// An addressed device folds onto its matching PCI function; anything
// else becomes a fresh node under the current parent.
gdev = pick: {
if (ctx) |pc| {
if (readAdr(c)) |adr| {
if (findPciNode(pc.bridge, pc.bus, adr)) |pnode| break :pick pnode;
}
}
break :pick try dt.addChild(parent_dev, .acpi_device, &c.seg);
};
}
applyHid(gdev, c, ev);
applyCrs(gdev, c, ev);
try mirrorDevices(dt, c, gdev, child_ctx, ev);
}
}
/// Evaluate a device's status (`_STA`) to decide if it is present. An absent status
/// (`_STA`) means present by default; an evaluation failure is treated as present too (we'd
/// rather over-report than hide a device we couldn't introspect).
fn devicePresent(ev: *aml.Interp, node: *aml.Node) bool {
const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true;
const obj = ev.evaluate(sta, &.{}) catch return true;
const status = obj.asInt() catch return true;
return (status & 0x01) != 0; // bit 0 = present
}
/// The first PCI host bridge in the generic tree (segment 0).
fn matchHostBridge(dt: *DeviceTree) ?*device.Device {
var c = dt.root.first_child;
while (c) |ch| : (c = ch.next_sibling) {
if (ch.class == .pci_host_bridge) return ch;
}
return null;
}
/// The PCI function node under `bridge` at the address the device's address object
/// (`_ADR`) names (dev/func on
/// `bus`), or null.
fn findPciNode(bridge: *device.Device, bus: u8, adr: u32) ?*device.Device {
const dev: u16 = @truncate((adr >> 16) & 0x1F);
const func: u16 = @truncate(adr & 0x7);
const target: u16 = (@as(u16, bus) << 8) | (dev << 3) | func;
var c = bridge.first_child;
while (c) |ch| : (c = ch.next_sibling) {
if (ch.ids.pci_bdf) |bdf| {
if (bdf == target) return ch;
}
}
return null;
}
/// A device's address (`_ADR`) — a static integer Name — or null.
fn readAdr(node: *aml.Node) ?u32 {
const n = aml.Namespace.childOf(node, seg4("_ADR")) orelse return null;
if (n.kind != .name) return null;
var p: usize = 0;
return @truncate(readIntObj(n.value, &p) orelse return null);
}
/// Whether a namespace device is a PCI(e) host bridge (`PNP0A03` / `PNP0A08`).
fn isPciRootNode(node: *aml.Node) bool {
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return false;
if (hid.kind != .name or hid.value.len == 0) return false;
switch (hid.value[0]) {
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
var p: usize = 0;
const n = readIntObj(hid.value, &p) orelse return false;
return n == 0x030AD041 or n == 0x080AD041; // PNP0A03 / PNP0A08
},
0x0D => {
const s = cstr(hid.value[1..]);
return std.mem.eql(u8, s, "PNP0A03") or std.mem.eql(u8, s, "PNP0A08");
},
else => return false,
}
}
/// Read a device's hardware ID (`_HID`) into the generic device: an integer decodes as an EISA
/// id ("PNP0A03"), a string is taken verbatim. Handles both the common static
/// Name form and a Method form (evaluated).
fn applyHid(dev: *device.Device, node: *aml.Node, ev: *aml.Interp) void {
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return;
if (hid.kind == .method) {
const obj = ev.evaluate(hid, &.{}) catch return;
switch (obj) {
.integer => |n| setEisaHid(dev, @truncate(n)),
.string => |s| dev.setHid(s),
else => {},
}
return;
}
if (hid.kind != .name or hid.value.len == 0) return;
const v = hid.value;
switch (v[0]) {
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
var p: usize = 0;
const n = readIntObj(v, &p) orelse return;
setEisaHid(dev, @truncate(n));
},
0x0D => dev.setHid(cstr(v[1..])), // StringPrefix
else => {},
}
}
fn setEisaHid(dev: *device.Device, id: u32) void {
dev.ids.acpi_hid = id;
var buf: [8]u8 = undefined;
dev.setHid(eisaIdToStr(id, &buf));
}
/// Parse a device's current resource settings (`_CRS`). The evaluator handles both the static
/// `Buffer` form (a `Name`) and the method form uniformly, yielding the
/// ResourceTemplate bytes we then decode.
fn applyCrs(dev: *device.Device, node: *aml.Node, ev: *aml.Interp) void {
const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return;
const obj = ev.evaluate(crs, &.{}) catch return;
const buf = switch (obj) {
.buffer => |b| b,
else => return,
};
parseResourceTemplate(dev, buf);
}
/// Walk a ResourceTemplate byte list, adding recognised descriptors as resources.
fn parseResourceTemplate(dev: *device.Device, bytes: []const u8) void {
var i: usize = 0;
while (i < bytes.len) {
const tag = bytes[i];
if (tag & 0x80 == 0) {
// Small descriptor: length in low 3 bits, type in bits [6:3].
const len: usize = tag & 0x07;
const body = i + 1;
if (body + len > bytes.len) break;
switch ((tag >> 3) & 0x0F) {
0x04 => if (len >= 2) { // IRQ: a 16-bit mask, one resource per set bit
const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8);
var b: usize = 0;
while (b < 16) : (b += 1) {
if (mask & (@as(u16, 1) << @intCast(b)) != 0) _ = dev.addResource(.irq, b, 1);
}
},
0x08 => if (len >= 7) { // IO port: min at +1, length at +6
_ = dev.addResource(.io_port, rd16(bytes, body + 1), bytes[body + 6]);
},
0x09 => if (len >= 3) { // Fixed IO: base at +0, length at +2
_ = dev.addResource(.io_port, rd16(bytes, body), bytes[body + 2]);
},
0x0F => break, // EndTag
else => {},
}
i = body + len;
} else {
// Large descriptor: 16-bit length follows the tag.
if (i + 3 > bytes.len) break;
const len: usize = @intCast(rd16(bytes, i + 1));
const body = i + 3;
if (body + len > bytes.len) break;
switch (tag) {
0x85 => if (len >= 17) { // Memory32: min at +1, length at +13
_ = dev.addResource(.memory, rd32(bytes, body + 1), rd32(bytes, body + 13));
},
0x86 => if (len >= 9) { // Memory32Fixed: base at +1, length at +5
_ = dev.addResource(.memory, rd32(bytes, body + 1), rd32(bytes, body + 5));
},
0x89 => if (len >= 2) { // Extended IRQ: count at +1, then count u32s
const count = bytes[body + 1];
var k: usize = 0;
while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) {
_ = dev.addResource(.irq, rd32(bytes, body + 2 + k * 4), 1);
}
},
0x87, 0x88, 0x8A => parseAddressSpace(dev, tag, bytes[body .. body + len]),
else => {},
}
i = body + len;
}
}
}
/// Word/DWord/QWord address-space descriptors: resource type at [0], then
/// granularity/min/max/translation/length, each of width `w`.
fn parseAddressSpace(dev: *device.Device, tag: u8, body: []const u8) void {
const w: usize = switch (tag) {
0x88 => 2, // Word
0x87 => 4, // DWord
else => 8, // QWord (0x8A)
};
if (body.len < 3 + 5 * w) return;
const min = readN(body, 3 + w, w);
const length = readN(body, 3 + 4 * w, w);
const kind: device.ResourceKind = switch (body[0]) {
0 => .memory,
1 => .io_port,
else => .bus_range,
};
_ = dev.addResource(kind, min, length);
}
/// Decode a packed EISA id into its 7-char string (e.g. 0x030AD041 -> "PNP0A03").
fn eisaIdToStr(id: u32, buf: *[8]u8) []const u8 {
const b0: u16 = @intCast(id & 0xFF);
const b1: u16 = @intCast((id >> 8) & 0xFF);
const b2: u8 = @truncate(id >> 16);
const b3: u8 = @truncate(id >> 24);
const mfg = (b0 << 8) | b1;
buf[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F));
buf[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F));
buf[2] = '@' + @as(u8, @intCast(mfg & 0x1F));
buf[3] = hexDigit((b2 >> 4) & 0xF);
buf[4] = hexDigit(b2 & 0xF);
buf[5] = hexDigit((b3 >> 4) & 0xF);
buf[6] = hexDigit(b3 & 0xF);
return buf[0..7];
}
fn hexDigit(n: u8) u8 {
return if (n < 10) '0' + n else 'A' + (n - 10);
}
fn seg4(comptime s: *const [4:0]u8) [4]u8 {
return s[0..4].*;
}
fn cstr(bytes: []const u8) []const u8 {
const idx = std.mem.indexOfScalar(u8, bytes, 0) orelse bytes.len;
return bytes[0..idx];
}
const PkgLen = struct { value: usize, size: usize };
fn pkgLen(bytes: []const u8, p: usize) ?PkgLen {
if (p >= bytes.len) return null;
const lead = bytes[p];
const follow: usize = lead >> 6;
if (p + 1 + follow > bytes.len) return null;
if (follow == 0) return .{ .value = lead & 0x3F, .size = 1 };
var value: usize = lead & 0x0F;
var i: usize = 0;
while (i < follow) : (i += 1) value |= @as(usize, bytes[p + 1 + i]) << @intCast(4 + i * 8);
return .{ .value = value, .size = 1 + follow };
}
/// Read an AML integer object at `p`, advancing `p` past it.
fn readIntObj(bytes: []const u8, p: *usize) ?u64 {
if (p.* >= bytes.len) return null;
const opcode = bytes[p.*];
p.* += 1;
return switch (opcode) {
0x00 => 0,
0x01 => 1,
0xFF => 0xFF,
0x0A => readLE(bytes, p, 1),
0x0B => readLE(bytes, p, 2),
0x0C => readLE(bytes, p, 4),
0x0E => readLE(bytes, p, 8),
else => null,
};
}
fn readLE(bytes: []const u8, p: *usize, n: usize) ?u64 {
if (p.* + n > bytes.len) return null;
const v = readN(bytes, p.*, n);
p.* += n;
return v;
}
fn readN(bytes: []const u8, off: usize, n: usize) u64 {
var v: u64 = 0;
var k: usize = 0;
while (k < n and off + k < bytes.len) : (k += 1) v |= @as(u64, bytes[off + k]) << @intCast(k * 8);
return v;
}
fn rd16(bytes: []const u8, off: usize) u64 {
return readN(bytes, off, 2);
}
fn rd32(bytes: []const u8, off: usize) u64 {
return readN(bytes, off, 4);
}
// --- 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 `RegAccess`. 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) ?RegAccess {
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 readCntReg(base: [*]align(1) const u8, len: usize, xoff: usize, legacy_off: usize, width: u8) RegAccess {
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 };
}
/// The mapped config space of one PCI function (its 4 KiB ECAM page). Mapped
/// writable so BAR sizing can probe it; reads and writes both go through here.
fn pciConfigPtr(alloc: McfgAllocation, hal: Hal, bus: u8, dev: u8, func: u8) [*]align(1) u8 {
const phys = alloc.base_address +
(@as(u64, bus - alloc.start_bus) << 20) +
(@as(u64, dev) << 15) +
(@as(u64, func) << 12);
hal.mapMmio(phys, phys, true); // identity-map this config page (writable)
return @ptrFromInt(phys);
}
/// 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.*;
}
/// Write a little-endian integer at `off` through a (possibly unaligned) pointer.
fn wr(comptime T: type, bytes: [*]align(1) u8, off: usize, value: T) void {
const p: *align(1) T = @ptrCast(bytes + off);
p.* = value;
}
// --- tests ------------------------------------------------------------------
test "eisaIdToStr decodes a packed EISA id" {
var buf: [8]u8 = undefined;
// 0x030AD041 is the well-known encoding of "PNP0A03" (PCI root bridge).
try std.testing.expectEqualStrings("PNP0A03", eisaIdToStr(0x030AD041, &buf));
}
test "parseResourceTemplate extracts IO, IRQ, and fixed memory" {
// ResourceTemplate { IO(min 0x60, len 8), IRQ(4), Memory32Fixed(0xFED00000, 0x1000) }
const rt = [_]u8{
0x47, 0x01, 0x60, 0x00, 0x60, 0x00, 0x01, 0x08, // small IO descriptor
0x22, 0x10, 0x00, // small IRQ descriptor (mask bit 4 -> IRQ 4)
0x86, 0x09, 0x00, 0x01, 0x00, 0x00, 0xD0, 0xFE, 0x00, 0x10, 0x00, 0x00, // Memory32Fixed
0x79, 0x00, // EndTag
};
var dev = device.Device{};
parseResourceTemplate(&dev, &rt);
try std.testing.expectEqual(@as(u8, 3), dev.resource_count);
const rs = dev.resources[0..dev.resource_count];
try std.testing.expectEqual(device.ResourceKind.io_port, rs[0].kind);
try std.testing.expectEqual(@as(u64, 0x60), rs[0].start);
try std.testing.expectEqual(@as(u64, 8), rs[0].len);
try std.testing.expectEqual(device.ResourceKind.irq, rs[1].kind);
try std.testing.expectEqual(@as(u64, 4), rs[1].start);
try std.testing.expectEqual(device.ResourceKind.memory, rs[2].kind);
try std.testing.expectEqual(@as(u64, 0xFED00000), rs[2].start);
try std.testing.expectEqual(@as(u64, 0x1000), rs[2].len);
}