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
+21 -1
View File
@@ -38,6 +38,10 @@ fn boot() !noreturn {
.memory_map = undefined, // filled by exitBootServices, just below
.kernel_segments = undefined, // filled by loadKernel
.kernel_segment_count = 0,
// Read the ACPI RSDP from the UEFI configuration table now, while boot
// services are still up. The pointer lives in ACPI reclaim memory, which
// the kernel identity-maps, so the physical address stays valid afterward.
.acpi_rsdp = if (acpiRootSystemDescriptorPointer()) |p| @intFromPtr(p) else 0,
};
const entry = try loadKernel(bs, &boot_info);
@@ -321,7 +325,7 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
/// kernel image and these buffers) lands there and stays reserved.
fn classify(d: *const uefi.tables.MemoryDescriptor) danos.MemoryKind {
if (!d.attribute.wb) return .mmio;
return switch (d.@"type") {
return switch (d.type) {
.conventional_memory, .boot_services_code, .boot_services_data => .usable,
.acpi_reclaim_memory => .acpi_tables,
.acpi_memory_nvs => .acpi_nvs,
@@ -349,3 +353,19 @@ fn logBytes(bytes: []const u8) void {
buf[i] = 0;
_ = out.outputString(buf[0..i :0].ptr) catch {};
}
fn acpiRootSystemDescriptorPointer() ?*const anyopaque {
const table_entries = uefi.system_table.number_of_table_entries;
const config_tables = uefi.system_table.configuration_table;
const acpi2 = uefi.tables.ConfigurationTable.acpi_20_table_guid;
const acpi1 = uefi.tables.ConfigurationTable.acpi_10_table_guid;
for (0..table_entries) |i| {
const entry = config_tables[i];
if (entry.vendor_guid.eql(acpi2) or entry.vendor_guid.eql(acpi1)) {
return entry.vendor_table;
}
}
return null;
}
+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);
}
+208
View File
@@ -0,0 +1,208 @@
//! AML (ACPI Machine Language) — the bytecode in the DSDT and SSDTs that describes
//! the parts of the machine the static tables don't.
//!
//! This module has two stages. `parser.zig` walks the entire byte stream and
//! records every named object into a namespace tree (`namespace.zig`), capturing
//! method bodies and field/region layout. `interp.zig` then *evaluates* control
//! methods on demand — running operators, control flow, and OperationRegion field
//! access — so callers can resolve device status (`_STA`), current resource
//! settings (`_CRS`), sleep states (`_Sx`), and the like against the live namespace.
const std = @import("std");
const op = @import("opcodes.zig");
const parser = @import("parser.zig");
const namespace = @import("namespace.zig");
const interp = @import("interp.zig");
pub const Namespace = namespace.Namespace;
pub const Node = namespace.Node;
pub const NodeKind = namespace.NodeKind;
/// The AML evaluator: interprets control methods (and reads Names/Fields) far
/// enough for device discovery. See `interp.zig`.
pub const Interp = interp.Interp;
pub const Object = interp.Object;
pub const EvalHal = interp.Hal;
/// The SLP_TYP values written to PM1a/PM1b control to enter a sleep state.
pub const SleepType = struct {
slp_typ_a: u8,
slp_typ_b: u8,
};
pub const ParseResult = struct {
namespace: Namespace,
/// Bytes the parser consumed across all blocks...
consumed: usize,
/// ...out of this many. A clean full traversal has `consumed == total`.
total: usize,
};
/// Parse the given AML blocks (DSDT first, then SSDTs) into one namespace. Later
/// blocks extend the namespace built by earlier ones, exactly as ACPI intends.
pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseResult {
var ns = try Namespace.init(allocator);
var consumed: usize = 0;
var total: usize = 0;
for (blocks) |block| {
var p = parser.Parser.init(block, &ns);
consumed += p.parseAll();
total += block.len;
}
return .{ .namespace = ns, .consumed = consumed, .total = total };
}
/// Look up the `\_S{state}` sleep package in a parsed namespace and return its
/// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
pub fn sleepState(ns: *Namespace, state: u8) ?SleepType {
const seg = [4]u8{ '_', 'S', '0' + state, '_' };
const node = ns.resolve(ns.root, false, 0, &.{seg}) orelse return null;
if (node.kind != .name) return null;
return parseSleepPackage(node.value);
}
/// Decode a `Package(){ SLP_TYPa, SLP_TYPb, ... }` from the raw AML of a Name's
/// value. Returns the first two elements as bytes (missing elements default to 0).
fn parseSleepPackage(value: []const u8) ?SleepType {
if (value.len == 0 or value[0] != op.package_op) return null;
var p: usize = 1;
p += pkgLengthSize(value, p) orelse return null;
if (p >= value.len) return null;
const num_elements = value[p];
p += 1;
const a: u8 = if (num_elements >= 1) @truncate(readInteger(value, &p) orelse 0) else 0;
const b: u8 = if (num_elements >= 2) @truncate(readInteger(value, &p) orelse 0) else 0;
return .{ .slp_typ_a = a, .slp_typ_b = b };
}
/// Bytes a PkgLength field occupies at `p` (we only need to step over it here).
fn pkgLengthSize(bytes: []const u8, p: usize) ?usize {
if (p >= bytes.len) return null;
const follow: usize = bytes[p] >> 6;
if (p + 1 + follow > bytes.len) return null;
return 1 + follow;
}
/// Read one AML integer data object at `p`, advancing `p`.
fn readInteger(bytes: []const u8, p: *usize) ?u64 {
if (p.* >= bytes.len) return null;
const opcode = bytes[p.*];
p.* += 1;
return switch (opcode) {
op.zero_op => 0,
op.one_op => 1,
op.ones_op => 0xFF,
op.byte_prefix => readLittle(bytes, p, 1),
op.word_prefix => readLittle(bytes, p, 2),
op.dword_prefix => readLittle(bytes, p, 4),
op.qword_prefix => readLittle(bytes, p, 8),
else => null,
};
}
fn readLittle(bytes: []const u8, p: *usize, n: usize) ?u64 {
if (p.* + n > bytes.len) return null;
var v: u64 = 0;
var k: usize = 0;
while (k < n) : (k += 1) v |= @as(u64, bytes[p.* + k]) << @intCast(k * 8);
p.* += n;
return v;
}
// --- tests ------------------------------------------------------------------
test "parses a nested namespace and finds the sleep package" {
// A hand-assembled AML blob (all PkgLengths computed to be single-byte):
// Name(_S5, Package(2){0x05, 0x00})
// Scope(\_SB) { Device(PCI0) {
// Name(_HID, 0x11)
// Method(MTHD, 1) {}
// Method(CALL, 0) { MTHD(Zero) } // invocation of a 1-arg method
// } }
// OperationRegion(DBG0, SystemIO, 0x0402, 1)
// Field(DBG0, ...) { DBGB, 8 }
const blob = [_]u8{
// Name(_S5, Package(2){Byte 0x05, Byte 0x00})
0x08, 0x5F, 0x53, 0x35, 0x5F, 0x12, 0x06, 0x02, 0x0A, 0x05, 0x0A, 0x00,
// Scope(\_SB) pkglen=0x27
0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F,
// Device(PCI0) pkglen=0x1F
0x5B, 0x82, 0x1F, 0x50, 0x43, 0x49, 0x30,
// Name(_HID, 0x11)
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11,
// Method(MTHD, flags=1) empty, pkglen=0x06
0x14, 0x06, 0x4D, 0x54, 0x48, 0x44, 0x01,
// Method(CALL, flags=0) { MTHD(Zero) }, pkglen=0x0B
0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D, 0x54, 0x48, 0x44, 0x00,
// OperationRegion(DBG0, SystemIO, Word 0x0402, Byte 1)
0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B, 0x02, 0x04, 0x0A, 0x01,
// Field(DBG0, flags=1) { DBGB, 8 }, pkglen=0x0B
0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01, 0x44, 0x42, 0x47, 0x42, 0x08,
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var result = try parse(arena.allocator(), &.{&blob});
// Integrity: the parser consumed exactly the whole blob (no desync).
try std.testing.expectEqual(blob.len, result.consumed);
try std.testing.expectEqual(blob.len, result.total);
const ns = &result.namespace;
// Expected top-level nodes.
const sb = ns.resolve(ns.root, false, 0, &.{.{ '_', 'S', 'B', '_' }}) orelse return error.NoSB;
try std.testing.expectEqual(NodeKind.scope, sb.kind);
const pci0 = ns.resolve(sb, false, 0, &.{.{ 'P', 'C', 'I', '0' }}) orelse return error.NoPCI0;
try std.testing.expectEqual(NodeKind.device, pci0.kind);
_ = ns.resolve(pci0, false, 0, &.{.{ '_', 'H', 'I', 'D' }}) orelse return error.NoHID;
// The 1-arg method's arg count was parsed from its flags byte.
const mthd = ns.resolve(pci0, false, 0, &.{.{ 'M', 'T', 'H', 'D' }}) orelse return error.NoMTHD;
try std.testing.expectEqual(NodeKind.method, mthd.kind);
try std.testing.expectEqual(@as(u8, 1), mthd.arg_count);
// OperationRegion and the Field unit made it into the namespace.
_ = ns.resolve(ns.root, false, 0, &.{.{ 'D', 'B', 'G', '0' }}) orelse return error.NoRegion;
_ = ns.resolve(ns.root, false, 0, &.{.{ 'D', 'B', 'G', 'B' }}) orelse return error.NoField;
// The sleep package decoded.
const s5 = sleepState(ns, 5) orelse return error.NoS5;
try std.testing.expectEqual(@as(u8, 5), s5.slp_typ_a);
try std.testing.expectEqual(@as(u8, 0), s5.slp_typ_b);
}
fn noMap(_: u64, _: u64, _: bool) void {}
fn noRead(_: u8, _: u16) u32 {
return 0;
}
fn noWrite(_: u8, _: u16, _: u32) void {}
test "interpreter runs a method with args, arithmetic, and control flow" {
// Method(TST_, 1) {
// Store(Arg0, Local0); Add(Local0, 5, Local0)
// If (LGreater(Local0, 10)) { Return(One) }
// Return(Zero)
// }
const blob = [_]u8{
0x14, 0x18, 0x54, 0x53, 0x54, 0x5F, 0x01, // Method TST_, 1 arg
0x70, 0x68, 0x60, // Store(Arg0, Local0)
0x72, 0x60, 0x0A, 0x05, 0x60, // Add(Local0, 5, Local0)
0xA0, 0x07, 0x94, 0x60, 0x0A, 0x0A, 0xA4, 0x01, // If(LGreater(Local0,10)) { Return(One) }
0xA4, 0x00, // Return(Zero)
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var result = try parse(arena.allocator(), &.{&blob});
const ns = &result.namespace;
const tst = ns.resolve(ns.root, false, 0, &.{.{ 'T', 'S', 'T', '_' }}) orelse return error.NoMethod;
var ev = Interp.init(ns, .{ .mapMmio = noMap, .pioRead = noRead, .pioWrite = noWrite }, arena.allocator());
const hi = try ev.evaluate(tst, &.{.{ .integer = 7 }}); // 7+5=12 > 10 -> 1
try std.testing.expectEqual(@as(u64, 1), try hi.asInt());
const lo = try ev.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0
try std.testing.expectEqual(@as(u64, 0), try lo.asInt());
}
+737
View File
@@ -0,0 +1,737 @@
//! A tree-walking AML interpreter — the evaluation stage on top of the parser's
//! structural namespace. It executes control methods (their bodies captured by
//! the parser) far enough to serve device discovery: device status (`_STA`, is a
//! device present), current resource settings (`_CRS`), and the operators, control
//! flow, locals/args, and
//! OperationRegion field access those methods reach for.
//!
//! Scope: integers, buffers, strings, packages, and references; If/Else/While/
//! Return; the arithmetic/logic operators; method invocation; Name/Local/Arg
//! access; CreateField buffer patching (the common current-resource-settings
//! (`_CRS`) idiom); and field
//! reads/writes against SystemMemory and SystemIO regions. Opcodes outside this
//! set return `error.Unsupported`, which callers treat as "couldn't evaluate" and
//! fall back — never a hard failure.
const std = @import("std");
const op = @import("opcodes.zig");
const nsp = @import("namespace.zig");
const Node = nsp.Node;
const Namespace = nsp.Namespace;
/// Injected hardware access for OperationRegion reads/writes (the arch VMM + pio).
pub const Hal = struct {
mapMmio: *const fn (virt: u64, phys: u64, writable: bool) void,
pioRead: *const fn (width: u8, port: u16) u32,
pioWrite: *const fn (width: u8, port: u16, value: u32) void,
};
pub const Error = error{ Unsupported, Truncated, DivByZero } || std.mem.Allocator.Error;
/// A runtime AML value.
pub const Object = union(enum) {
uninitialized,
integer: u64,
buffer: []u8,
string: []u8,
package: []Object,
reference: *Node,
pub fn asInt(self: Object) Error!u64 {
return switch (self) {
.integer => |v| v,
.buffer => |b| blk: {
var v: u64 = 0;
for (b, 0..) |byte, i| {
if (i >= 8) break;
v |= @as(u64, byte) << @intCast(i * 8);
}
break :blk v;
},
else => error.Unsupported,
};
}
};
const max_segs = 16;
const NamePath = struct {
rooted: bool = false,
parents: u8 = 0,
segs: [max_segs][4]u8 = undefined,
count: usize = 0,
fn slice(self: *const NamePath) []const [4]u8 {
return self.segs[0..self.count];
}
};
const Cursor = struct {
b: []const u8,
i: usize = 0,
fn eof(self: *Cursor) bool {
return self.i >= self.b.len;
}
fn peek(self: *Cursor) ?u8 {
return if (self.eof()) null else self.b[self.i];
}
fn byte(self: *Cursor) Error!u8 {
if (self.eof()) return error.Truncated;
const v = self.b[self.i];
self.i += 1;
return v;
}
fn take(self: *Cursor, n: usize) Error![]const u8 {
if (self.i + n > self.b.len) return error.Truncated;
const s = self.b[self.i .. self.i + n];
self.i += n;
return s;
}
fn pkgLen(self: *Cursor) Error!usize {
const lead = try self.byte();
const follow: usize = lead >> 6;
if (follow == 0) return lead & 0x3F;
var value: usize = lead & 0x0F;
var k: usize = 0;
while (k < follow) : (k += 1) value |= @as(usize, try self.byte()) << @intCast(4 + k * 8);
return value;
}
fn nameString(self: *Cursor) Error!NamePath {
var np = NamePath{};
if (self.peek() == op.root_char) {
np.rooted = true;
self.i += 1;
} else {
while (self.peek() == op.parent_prefix_char) : (self.i += 1) np.parents += 1;
}
const lead = self.peek() orelse return np;
switch (lead) {
0x00 => self.i += 1,
op.dual_name_prefix => {
self.i += 1;
try self.seg(&np);
try self.seg(&np);
},
op.multi_name_prefix => {
self.i += 1;
const cnt = try self.byte();
var k: usize = 0;
while (k < cnt) : (k += 1) try self.seg(&np);
},
else => try self.seg(&np),
}
return np;
}
fn seg(self: *Cursor, np: *NamePath) Error!void {
const s = try self.take(4);
if (np.count < max_segs) {
np.segs[np.count] = s[0..4].*;
np.count += 1;
}
}
};
const Frame = struct {
args: [7]Object = .{.uninitialized} ** 7,
locals: [8]Object = .{.uninitialized} ** 8,
scope: *Node,
ret: Object = .uninitialized,
returned: bool = false,
broke: bool = false,
};
/// A CreateField binding: a name that indexes into a buffer object.
const BufField = struct { buf: *Node, byte_off: usize, bit_width: u32 };
pub const Interp = struct {
ns: *Namespace,
hal: Hal,
arena: std.mem.Allocator,
/// Runtime object overrides for Name nodes (Store targets, patched buffers).
dyn: std.AutoHashMapUnmanaged(*Node, Object) = .{},
/// CreateField bindings active for the current evaluation.
fields: std.AutoHashMapUnmanaged(*Node, BufField) = .{},
pub fn init(ns: *Namespace, hal: Hal, arena: std.mem.Allocator) Interp {
return .{ .ns = ns, .hal = hal, .arena = arena };
}
/// Evaluate a namespace object: invoke a Method, read a Name's value, or read a
/// Field. Resets per-evaluation runtime state first.
pub fn evaluate(self: *Interp, node: *Node, args: []const Object) Error!Object {
self.dyn.clearRetainingCapacity();
self.fields.clearRetainingCapacity();
return self.invoke(node, args);
}
fn invoke(self: *Interp, node: *Node, args: []const Object) Error!Object {
switch (node.kind) {
.method => {
var frame = Frame{ .scope = node };
for (args, 0..) |a, i| {
if (i < frame.args.len) frame.args[i] = a;
}
var cur = Cursor{ .b = node.value };
try self.execList(&cur, &frame);
return frame.ret;
},
.name => {
if (self.dyn.get(node)) |o| return o;
var cur = Cursor{ .b = node.value };
var frame = Frame{ .scope = node.parent orelse self.ns.root };
return self.term(&cur, &frame);
},
.field => return .{ .integer = try self.readField(node) },
else => return .{ .reference = node },
}
}
/// Execute a TermList until it ends or the frame returns/breaks.
fn execList(self: *Interp, cur: *Cursor, frame: *Frame) Error!void {
while (!cur.eof() and !frame.returned and !frame.broke) {
_ = try self.term(cur, frame);
}
}
/// Evaluate/execute one term, returning its value (`.uninitialized` for pure
/// statements).
fn term(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const lead = cur.peek() orelse return error.Truncated;
if (isNameStart(lead)) return self.nameRef(cur, frame);
_ = try cur.byte();
return switch (lead) {
op.zero_op => Object{ .integer = 0 },
op.one_op => Object{ .integer = 1 },
op.ones_op => Object{ .integer = ~@as(u64, 0) },
op.byte_prefix => Object{ .integer = try self.readConst(cur, 1) },
op.word_prefix => Object{ .integer = try self.readConst(cur, 2) },
op.dword_prefix => Object{ .integer = try self.readConst(cur, 4) },
op.qword_prefix => Object{ .integer = try self.readConst(cur, 8) },
op.string_prefix => try self.readString(cur),
op.buffer_op => try self.buffer(cur, frame),
op.package_op, op.var_package_op => try self.package(cur, frame, lead == op.var_package_op),
op.local0_op...op.local7_op => frame.locals[lead - op.local0_op],
op.arg0_op...op.arg6_op => frame.args[lead - op.arg0_op],
op.return_op => blk: {
frame.ret = try self.term(cur, frame);
frame.returned = true;
break :blk .uninitialized;
},
op.break_op => blk: {
frame.broke = true;
break :blk .uninitialized;
},
op.continue_op, op.noop_op => .uninitialized,
op.if_op => try self.ifElse(cur, frame),
op.while_op => try self.whileLoop(cur, frame),
op.store_op => try self.store(cur, frame),
op.increment_op => try self.incDec(cur, frame, 1),
op.decrement_op => try self.incDec(cur, frame, -1),
op.add_op => try self.binary(cur, frame, .add),
op.subtract_op => try self.binary(cur, frame, .sub),
op.multiply_op => try self.binary(cur, frame, .mul),
op.mod_op => try self.binary(cur, frame, .mod),
op.and_op => try self.binary(cur, frame, .band),
op.or_op => try self.binary(cur, frame, .bor),
op.xor_op => try self.binary(cur, frame, .bxor),
op.nand_op => try self.binary(cur, frame, .nand),
op.nor_op => try self.binary(cur, frame, .nor),
op.shift_left_op => try self.binary(cur, frame, .shl),
op.shift_right_op => try self.binary(cur, frame, .shr),
op.divide_op => try self.divide(cur, frame),
op.land_op => try self.logic2(cur, frame, .land),
op.lor_op => try self.logic2(cur, frame, .lor),
op.lequal_op => try self.logic2(cur, frame, .eq),
op.lgreater_op => try self.logic2(cur, frame, .gt),
op.lless_op => try self.logic2(cur, frame, .lt),
op.lnot_op => try self.lnot(cur, frame),
op.not_op => blk: {
const v = try self.evalInt(cur, frame);
const r = ~v;
try self.storeTarget(cur, frame, .{ .integer = r });
break :blk .{ .integer = r };
},
op.size_of_op => try self.sizeOf(cur, frame),
op.index_op => try self.index(cur, frame),
op.deref_of_op => try self.derefOf(cur, frame),
op.to_integer_op => blk: {
const v = try self.evalInt(cur, frame);
try self.storeTarget(cur, frame, .{ .integer = v });
break :blk .{ .integer = v };
},
op.to_buffer_op => try self.passThroughUnary(cur, frame),
op.ext_op_prefix => try self.ext(cur, frame),
// CreateXField: source, index, name (bit widths differ by op)
op.create_bit_field_op => try self.createField(cur, frame, 1),
op.create_byte_field_op => try self.createField(cur, frame, 8),
op.create_word_field_op => try self.createField(cur, frame, 16),
op.create_dword_field_op => try self.createField(cur, frame, 32),
op.create_qword_field_op => try self.createField(cur, frame, 64),
else => error.Unsupported,
};
}
// --- name references ----------------------------------------------------
fn nameRef(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const np = try cur.nameString();
const node = self.ns.resolve(frame.scope, np.rooted, np.parents, np.slice()) orelse
return .uninitialized; // unknown name -> treat as uninitialised
switch (node.kind) {
.method => {
var argbuf: [7]Object = undefined;
var i: usize = 0;
while (i < node.arg_count and i < argbuf.len) : (i += 1) argbuf[i] = try self.term(cur, frame);
return self.invoke(node, argbuf[0..@min(node.arg_count, argbuf.len)]);
},
.field => return .{ .integer = try self.readField(node) },
.name => return self.invoke(node, &.{}),
else => return .{ .reference = node },
}
}
// --- data objects -------------------------------------------------------
fn readConst(self: *Interp, cur: *Cursor, n: usize) Error!u64 {
_ = self;
const bytes = try cur.take(n);
var v: u64 = 0;
for (bytes, 0..) |b, i| v |= @as(u64, b) << @intCast(i * 8);
return v;
}
fn readString(self: *Interp, cur: *Cursor) Error!Object {
const start = cur.i;
while (cur.peek()) |c| {
cur.i += 1;
if (c == 0) break;
}
const raw = cur.b[start .. cur.i - 1];
const s = try self.arena.dupe(u8, raw);
return .{ .string = s };
}
fn buffer(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const start = cur.i;
const len = try cur.pkgLen();
const end = @min(start + len, cur.b.len);
const size = try self.evalInt(cur, frame);
const data = cur.b[@min(cur.i, end)..end];
const buf = try self.arena.alloc(u8, @intCast(size));
@memset(buf, 0);
@memcpy(buf[0..@min(buf.len, data.len)], data[0..@min(buf.len, data.len)]);
cur.i = end;
return .{ .buffer = buf };
}
fn package(self: *Interp, cur: *Cursor, frame: *Frame, variable: bool) Error!Object {
const start = cur.i;
const len = try cur.pkgLen();
const end = @min(start + len, cur.b.len);
const count: usize = if (variable) @intCast(try self.evalInt(cur, frame)) else try cur.byte();
const elems = try self.arena.alloc(Object, count);
var i: usize = 0;
while (i < count and cur.i < end) : (i += 1) elems[i] = try self.term(cur, frame);
while (i < count) : (i += 1) elems[i] = .uninitialized;
cur.i = end;
return .{ .package = elems };
}
// --- operators ----------------------------------------------------------
const BinOp = enum { add, sub, mul, mod, band, bor, bxor, nand, nor, shl, shr };
fn binary(self: *Interp, cur: *Cursor, frame: *Frame, kind: BinOp) Error!Object {
const a = try self.evalInt(cur, frame);
const b = try self.evalInt(cur, frame);
const r: u64 = switch (kind) {
.add => a +% b,
.sub => a -% b,
.mul => a *% b,
.mod => if (b == 0) return error.DivByZero else a % b,
.band => a & b,
.bor => a | b,
.bxor => a ^ b,
.nand => ~(a & b),
.nor => ~(a | b),
.shl => if (b >= 64) 0 else a << @intCast(b),
.shr => if (b >= 64) 0 else a >> @intCast(b),
};
try self.storeTarget(cur, frame, .{ .integer = r });
return .{ .integer = r };
}
fn divide(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const a = try self.evalInt(cur, frame);
const b = try self.evalInt(cur, frame);
if (b == 0) return error.DivByZero;
try self.storeTarget(cur, frame, .{ .integer = a % b }); // remainder target
try self.storeTarget(cur, frame, .{ .integer = a / b }); // quotient target
return .{ .integer = a / b };
}
const LogicOp = enum { land, lor, eq, gt, lt };
fn logic2(self: *Interp, cur: *Cursor, frame: *Frame, kind: LogicOp) Error!Object {
const a = try self.evalInt(cur, frame);
const b = try self.evalInt(cur, frame);
const r = switch (kind) {
.land => a != 0 and b != 0,
.lor => a != 0 or b != 0,
.eq => a == b,
.gt => a > b,
.lt => a < b,
};
return .{ .integer = if (r) ~@as(u64, 0) else 0 };
}
fn lnot(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
// 0x92 0x93/94/95 are the compound comparisons.
const b = cur.peek() orelse return error.Truncated;
switch (b) {
op.lnot.not_equal => {
cur.i += 1;
const x = try self.evalInt(cur, frame);
const y = try self.evalInt(cur, frame);
return .{ .integer = if (x != y) ~@as(u64, 0) else 0 };
},
op.lnot.less_equal => {
cur.i += 1;
const x = try self.evalInt(cur, frame);
const y = try self.evalInt(cur, frame);
return .{ .integer = if (x <= y) ~@as(u64, 0) else 0 };
},
op.lnot.greater_equal => {
cur.i += 1;
const x = try self.evalInt(cur, frame);
const y = try self.evalInt(cur, frame);
return .{ .integer = if (x >= y) ~@as(u64, 0) else 0 };
},
else => {
const x = try self.evalInt(cur, frame);
return .{ .integer = if (x == 0) ~@as(u64, 0) else 0 };
},
}
}
fn incDec(self: *Interp, cur: *Cursor, frame: *Frame, delta: i64) Error!Object {
// Operand is a SuperName that is both read and written.
const save = cur.i;
const cur_val = try self.term(cur, frame);
const v = try cur_val.asInt();
const r = if (delta > 0) v +% 1 else v -% 1;
var tcur = Cursor{ .b = cur.b, .i = save };
try self.storeInto(&tcur, frame, .{ .integer = r });
return .{ .integer = r };
}
fn sizeOf(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const o = try self.term(cur, frame);
return .{ .integer = switch (o) {
.buffer => |b| b.len,
.string => |s| s.len,
.package => |p| p.len,
else => 0,
} };
}
fn passThroughUnary(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const o = try self.term(cur, frame);
try self.storeTarget(cur, frame, o);
return o;
}
fn index(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const src = try self.term(cur, frame);
const idx: usize = @intCast(try self.evalInt(cur, frame));
// Optional target (a reference); we don't materialise references, so store
// the indexed value if a target is present.
const val: Object = switch (src) {
.buffer => |b| .{ .integer = if (idx < b.len) b[idx] else 0 },
.package => |p| if (idx < p.len) p[idx] else .uninitialized,
.string => |s| .{ .integer = if (idx < s.len) s[idx] else 0 },
else => .uninitialized,
};
try self.storeTarget(cur, frame, val);
return val;
}
fn derefOf(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const o = try self.term(cur, frame);
return switch (o) {
.reference => |n| self.invoke(n, &.{}),
else => o,
};
}
// --- control flow -------------------------------------------------------
fn ifElse(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const start = cur.i;
const end = @min(start + try cur.pkgLen(), cur.b.len);
const cond = try self.evalInt(cur, frame);
if (cond != 0) {
var body = Cursor{ .b = cur.b[0..end], .i = cur.i };
try self.execList(&body, frame);
cur.i = end;
// Skip a trailing Else.
if (cur.peek() == op.else_op) {
cur.i += 1;
const es = cur.i;
const ee = @min(es + try cur.pkgLen(), cur.b.len);
cur.i = ee;
}
} else {
cur.i = end;
if (cur.peek() == op.else_op) {
cur.i += 1;
const es = cur.i;
const ee = @min(es + try cur.pkgLen(), cur.b.len);
var body = Cursor{ .b = cur.b[0..ee], .i = cur.i };
try self.execList(&body, frame);
cur.i = ee;
}
}
return .uninitialized;
}
fn whileLoop(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const start = cur.i;
const end = @min(start + try cur.pkgLen(), cur.b.len);
const pred_at = cur.i;
var guard: usize = 0;
while (guard < 100_000) : (guard += 1) {
var pc = Cursor{ .b = cur.b[0..end], .i = pred_at };
const cond = try self.evalInt(&pc, frame);
if (cond == 0) break;
var body = Cursor{ .b = cur.b[0..end], .i = pc.i };
try self.execList(&body, frame);
if (frame.returned) break;
if (frame.broke) {
frame.broke = false;
break;
}
}
cur.i = end;
return .uninitialized;
}
// --- store --------------------------------------------------------------
fn store(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const value = try self.term(cur, frame);
try self.storeInto(cur, frame, value);
return value;
}
/// A Store *target* that may be NullName (no store).
fn storeTarget(self: *Interp, cur: *Cursor, frame: *Frame, value: Object) Error!void {
if (cur.peek() == 0x00) {
cur.i += 1; // NullName
return;
}
try self.storeInto(cur, frame, value);
}
fn storeInto(self: *Interp, cur: *Cursor, frame: *Frame, value: Object) Error!void {
const lead = cur.peek() orelse return error.Truncated;
if (isNameStart(lead)) {
const np = try cur.nameString();
const node = self.ns.resolve(frame.scope, np.rooted, np.parents, np.slice()) orelse return;
if (self.fields.get(node)) |bf| {
try self.writeBufField(bf, try value.asInt());
} else if (node.kind == .field) {
try self.writeField(node, try value.asInt());
} else {
try self.dyn.put(self.arena, node, value);
}
return;
}
_ = try cur.byte();
switch (lead) {
0x00 => {}, // NullName
op.local0_op...op.local7_op => frame.locals[lead - op.local0_op] = value,
op.arg0_op...op.arg6_op => frame.args[lead - op.arg0_op] = value,
op.index_op => {
const src = try self.term(cur, frame);
const idx: usize = @intCast(try self.evalInt(cur, frame));
switch (src) {
.buffer => |b| if (idx < b.len) {
b[idx] = @truncate(try value.asInt());
},
.package => |p| if (idx < p.len) {
p[idx] = value;
},
else => {},
}
},
else => return error.Unsupported,
}
}
// --- CreateField (buffer patching) --------------------------------------
fn createField(self: *Interp, cur: *Cursor, frame: *Frame, bit_width: u32) Error!Object {
const src = try self.term(cur, frame); // source buffer (as a reference or value)
const bit_index = try self.evalInt(cur, frame);
const np = try cur.nameString();
const node = self.ns.resolve(frame.scope, np.rooted, np.parents, np.slice()) orelse return .uninitialized;
// Bind the new name to the source buffer's node so stores land in it.
const buf_node: *Node = switch (src) {
.reference => |n| n,
else => return .uninitialized,
};
// Materialise the buffer into `dyn` so patches persist and are returned.
if (self.dyn.get(buf_node) == null) {
const val = try self.invoke(buf_node, &.{});
try self.dyn.put(self.arena, buf_node, val);
}
const byte_off: usize = @intCast(bit_index / 8);
try self.fields.put(self.arena, node, .{ .buf = buf_node, .byte_off = byte_off, .bit_width = bit_width });
return .uninitialized;
}
fn writeBufField(self: *Interp, bf: BufField, value: u64) Error!void {
const obj = self.dyn.get(bf.buf) orelse return;
const buf = switch (obj) {
.buffer => |b| b,
else => return,
};
const nbytes = (bf.bit_width + 7) / 8;
var k: usize = 0;
while (k < nbytes and bf.byte_off + k < buf.len) : (k += 1) {
buf[bf.byte_off + k] = @truncate(value >> @intCast(k * 8));
}
}
// --- OperationRegion field access ---------------------------------------
fn readField(self: *Interp, field: *Node) Error!u64 {
const region = field.region orelse return error.Unsupported;
if (field.bit_width == 0 or field.bit_width > 64) return error.Unsupported;
const base = try self.regionBase(region);
const start_byte = base + field.bit_offset / 8;
const shift: u7 = @intCast(field.bit_offset % 8);
const total = @as(usize, shift) + field.bit_width;
const nbytes = (total + 7) / 8;
var raw: u128 = 0;
var k: usize = 0;
while (k < nbytes) : (k += 1) {
raw |= @as(u128, try self.readRegionByte(region.region_space, start_byte + k)) << @intCast(k * 8);
}
const masked = (raw >> shift) & bitMask(field.bit_width);
return @truncate(masked);
}
fn writeField(self: *Interp, field: *Node, value: u64) Error!void {
const region = field.region orelse return error.Unsupported;
if (field.bit_width == 0 or field.bit_width > 64) return error.Unsupported;
const base = try self.regionBase(region);
const start_byte = base + field.bit_offset / 8;
const shift: u7 = @intCast(field.bit_offset % 8);
const total = @as(usize, shift) + field.bit_width;
const nbytes = (total + 7) / 8;
// Read-modify-write byte by byte.
var raw: u128 = 0;
var k: usize = 0;
while (k < nbytes) : (k += 1) {
raw |= @as(u128, try self.readRegionByte(region.region_space, start_byte + k)) << @intCast(k * 8);
}
const mask = bitMask(field.bit_width) << shift;
raw = (raw & ~mask) | ((@as(u128, value) << shift) & mask);
k = 0;
while (k < nbytes) : (k += 1) {
try self.writeRegionByte(region.region_space, start_byte + k, @truncate(raw >> @intCast(k * 8)));
}
}
fn regionBase(self: *Interp, region: *Node) Error!u64 {
var cur = Cursor{ .b = region.region_offset_aml };
var frame = Frame{ .scope = region.parent orelse self.ns.root };
return (try self.term(&cur, &frame)).asInt();
}
fn readRegionByte(self: *Interp, space: u8, addr: u64) Error!u8 {
switch (space) {
0 => { // SystemMemory
self.hal.mapMmio(addr & ~@as(u64, 0xFFF), addr & ~@as(u64, 0xFFF), true);
const p: *align(1) const volatile u8 = @ptrFromInt(addr);
return p.*;
},
1 => return @truncate(self.hal.pioRead(1, @intCast(addr & 0xFFFF))), // SystemIO
else => return error.Unsupported,
}
}
fn writeRegionByte(self: *Interp, space: u8, addr: u64, value: u8) Error!void {
switch (space) {
0 => {
self.hal.mapMmio(addr & ~@as(u64, 0xFFF), addr & ~@as(u64, 0xFFF), true);
const p: *align(1) volatile u8 = @ptrFromInt(addr);
p.* = value;
},
1 => self.hal.pioWrite(1, @intCast(addr & 0xFFFF), value),
else => return error.Unsupported,
}
}
// --- extended opcodes ---------------------------------------------------
fn ext(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object {
const e = try cur.byte();
switch (e) {
op.ext.debug => return .uninitialized,
op.ext.revision => return .{ .integer = 2 },
op.ext.timer => return .{ .integer = 0 },
// Mutex/Event ops are no-ops in this single-threaded evaluator.
op.ext.acquire => {
_ = try self.term(cur, frame); // mutex SuperName
_ = try cur.take(2); // timeout
return .{ .integer = 0 }; // acquired
},
op.ext.release, op.ext.reset, op.ext.signal => {
_ = try self.term(cur, frame);
return .uninitialized;
},
op.ext.wait => {
_ = try self.term(cur, frame);
_ = try self.term(cur, frame);
return .{ .integer = 0 };
},
op.ext.sleep, op.ext.stall => {
_ = try self.term(cur, frame);
return .uninitialized;
},
else => return error.Unsupported,
}
}
fn evalInt(self: *Interp, cur: *Cursor, frame: *Frame) Error!u64 {
return (try self.term(cur, frame)).asInt();
}
};
fn bitMask(width: u32) u128 {
if (width >= 128) return ~@as(u128, 0);
return (@as(u128, 1) << @intCast(width)) - 1;
}
fn isNameStart(b: u8) bool {
return (b >= op.name_char_start and b <= op.name_char_end) or
b == op.name_char_underscore or
b == op.root_char or
b == op.parent_prefix_char or
b == op.dual_name_prefix or
b == op.multi_name_prefix;
}
+181
View File
@@ -0,0 +1,181 @@
//! The ACPI namespace the AML parser builds: a tree of named nodes, plus the name
//! resolution rules the parser needs while it walks (so a method invocation can be
//! resolved to its declaration to learn its argument count).
//!
//! Nodes are individually allocated and linked intrusively (first-child /
//! next-sibling), the same shape as the device tree in `device.zig`.
const std = @import("std");
pub const NodeKind = enum {
root,
scope,
device,
method,
name,
region, // OperationRegion
field, // a Field unit
mutex,
event,
processor,
power_res,
thermal_zone,
alias,
external,
other,
};
pub const Node = struct {
/// The 4-byte NameSeg identifying this node within its parent. The root uses
/// all-zero.
seg: [4]u8 = .{ 0, 0, 0, 0 },
kind: NodeKind = .other,
/// For Method / External: the declared argument count (0..7). Used to resolve
/// how many TermArgs a method invocation consumes.
arg_count: u8 = 0,
/// For Name: the AML bytes of its DataRefObject (so a value like a sleep
/// state's (`_Sx`) Package can be parsed on demand). For Method: the AML bytes of the body,
/// interpreted on demand by the evaluator. Empty otherwise.
value: []const u8 = &.{},
// OperationRegion metadata (kind == .region): the address space, plus the AML
// of the offset/length expressions (evaluated lazily, usually constants).
region_space: u8 = 0,
region_offset_aml: []const u8 = &.{},
region_len_aml: []const u8 = &.{},
// Field-unit metadata (kind == .field): which region it lives in and its bit
// position/width/access, so the evaluator can read/write it.
region: ?*Node = null,
bit_offset: u32 = 0,
bit_width: u32 = 0,
access_type: u8 = 0,
parent: ?*Node = null,
first_child: ?*Node = null,
next_sibling: ?*Node = null,
/// Depth-first count of this node and everything under it.
pub fn subtreeCount(self: *const Node) usize {
var n: usize = 1;
var c = self.first_child;
while (c) |child| : (c = child.next_sibling) n += child.subtreeCount();
return n;
}
};
pub const Namespace = struct {
allocator: std.mem.Allocator,
root: *Node,
pub fn init(allocator: std.mem.Allocator) !Namespace {
const root = try allocator.create(Node);
root.* = .{ .kind = .root };
return .{ .allocator = allocator, .root = root };
}
pub fn nodeCount(self: *const Namespace) usize {
return self.root.subtreeCount();
}
fn findChild(parent: *Node, seg: [4]u8) ?*Node {
var c = parent.first_child;
while (c) |child| : (c = child.next_sibling) {
if (std.mem.eql(u8, &child.seg, &seg)) return child;
}
return null;
}
/// The direct child of `node` named `seg`, or null. Unlike `resolve`, this does
/// not apply the search-rule walk-up — it looks only at immediate children (for
/// reading a device's own hardware ID (`_HID`) / current resource settings (`_CRS`)).
pub fn childOf(node: *Node, seg: [4]u8) ?*Node {
return findChild(node, seg);
}
fn newChild(self: *Namespace, parent: *Node, seg: [4]u8, kind: NodeKind) !*Node {
const n = try self.allocator.create(Node);
n.* = .{ .seg = seg, .kind = kind, .parent = parent };
// Append at the tail so a dump reads in declaration order.
if (parent.first_child == null) {
parent.first_child = n;
} else {
var cur = parent.first_child.?;
while (cur.next_sibling) |sib| cur = sib;
cur.next_sibling = n;
}
return n;
}
/// Create a Field unit node directly under `scope` (field units live in the
/// scope of the Field/IndexField/BankField, not under the region).
pub fn newFieldUnit(self: *Namespace, scope: *Node, seg: [4]u8) !*Node {
return self.findOrCreate(scope, seg, .field);
}
fn findOrCreate(self: *Namespace, parent: *Node, seg: [4]u8, kind: NodeKind) !*Node {
if (findChild(parent, seg)) |existing| {
// Reopening a scope (e.g. Scope(\_SB) after Device \_SB) keeps the more
// specific kind rather than downgrading to a plain scope.
if (existing.kind == .scope and kind != .scope) existing.kind = kind;
return existing;
}
return self.newChild(parent, seg, kind);
}
/// The node a definition's NameString names, creating any intermediate scopes.
/// The final segment is created (or found) with `kind`; intermediates are
/// scopes. Returns the namespace root for a NullName (empty path).
pub fn place(
self: *Namespace,
current: *Node,
rooted: bool,
parents: u8,
segs: []const [4]u8,
kind: NodeKind,
) !*Node {
var base = startNode(self, current, rooted, parents);
if (segs.len == 0) return base;
var i: usize = 0;
while (i + 1 < segs.len) : (i += 1) {
base = try self.findOrCreate(base, segs[i], .scope);
}
return self.findOrCreate(base, segs[segs.len - 1], kind);
}
/// Resolve a NameString *reference* to an existing node, or null. A single
/// relative segment uses the ACPI search rule (walk up the ancestors); any
/// rooted, parented, or multi-segment path is resolved exactly.
pub fn resolve(
self: *Namespace,
current: *Node,
rooted: bool,
parents: u8,
segs: []const [4]u8,
) ?*Node {
if (segs.len == 0) return null;
if (!rooted and parents == 0 and segs.len == 1) {
// Search rule: this scope, then each ancestor up to the root.
var scope: ?*Node = current;
while (scope) |s| : (scope = s.parent) {
if (findChild(s, segs[0])) |n| return n;
}
return null;
}
var base = startNode(self, current, rooted, parents);
for (segs) |seg| {
base = findChild(base, seg) orelse return null;
}
return base;
}
fn startNode(self: *Namespace, current: *Node, rooted: bool, parents: u8) *Node {
if (rooted) return self.root;
var base = current;
var up = parents;
while (up > 0) : (up -= 1) base = base.parent orelse self.root;
return base;
}
};
+137
View File
@@ -0,0 +1,137 @@
//! AML opcode constants — the full ACPI Machine Language opcode table.
//!
//! Single-byte opcodes are plain values. Extended opcodes are a two-byte sequence
//! `ext_prefix` (0x5B) followed by a byte listed under `ext`. A few comparison
//! opcodes are `lnot_op` (0x92) followed by a second byte (see `lnot`).
// --- name / path characters -------------------------------------------------
pub const zero_op = 0x00;
pub const one_op = 0x01;
pub const alias_op = 0x06;
pub const name_op = 0x08;
pub const byte_prefix = 0x0A;
pub const word_prefix = 0x0B;
pub const dword_prefix = 0x0C;
pub const string_prefix = 0x0D;
pub const qword_prefix = 0x0E;
pub const scope_op = 0x10;
pub const buffer_op = 0x11;
pub const package_op = 0x12;
pub const var_package_op = 0x13;
pub const method_op = 0x14;
pub const external_op = 0x15;
pub const dual_name_prefix = 0x2E;
pub const multi_name_prefix = 0x2F;
pub const ext_op_prefix = 0x5B;
pub const root_char = 0x5C;
pub const parent_prefix_char = 0x5E;
pub const name_char_underscore = 0x5F;
pub const digit_char_start = 0x30;
pub const digit_char_end = 0x39;
pub const name_char_start = 0x41; // 'A'
pub const name_char_end = 0x5A; // 'Z'
// --- locals / args ----------------------------------------------------------
pub const local0_op = 0x60;
pub const local7_op = 0x67;
pub const arg0_op = 0x68;
pub const arg6_op = 0x6E;
// --- store / references / arithmetic ---------------------------------------
pub const store_op = 0x70;
pub const ref_of_op = 0x71;
pub const add_op = 0x72;
pub const concat_op = 0x73;
pub const subtract_op = 0x74;
pub const increment_op = 0x75;
pub const decrement_op = 0x76;
pub const multiply_op = 0x77;
pub const divide_op = 0x78;
pub const shift_left_op = 0x79;
pub const shift_right_op = 0x7A;
pub const and_op = 0x7B;
pub const nand_op = 0x7C;
pub const or_op = 0x7D;
pub const nor_op = 0x7E;
pub const xor_op = 0x7F;
pub const not_op = 0x80;
pub const find_set_left_bit_op = 0x81;
pub const find_set_right_bit_op = 0x82;
pub const deref_of_op = 0x83;
pub const concat_res_op = 0x84;
pub const mod_op = 0x85;
pub const notify_op = 0x86;
pub const size_of_op = 0x87;
pub const index_op = 0x88;
pub const match_op = 0x89;
pub const create_dword_field_op = 0x8A;
pub const create_word_field_op = 0x8B;
pub const create_byte_field_op = 0x8C;
pub const create_bit_field_op = 0x8D;
pub const object_type_op = 0x8E;
pub const create_qword_field_op = 0x8F;
pub const land_op = 0x90;
pub const lor_op = 0x91;
pub const lnot_op = 0x92; // may be followed by a second byte (see `lnot`)
pub const lequal_op = 0x93;
pub const lgreater_op = 0x94;
pub const lless_op = 0x95;
pub const to_buffer_op = 0x96;
pub const to_decimal_string_op = 0x97;
pub const to_hex_string_op = 0x98;
pub const to_integer_op = 0x99;
pub const to_string_op = 0x9C;
pub const copy_object_op = 0x9D;
pub const mid_op = 0x9E;
pub const continue_op = 0x9F;
pub const if_op = 0xA0;
pub const else_op = 0xA1;
pub const while_op = 0xA2;
pub const noop_op = 0xA3;
pub const return_op = 0xA4;
pub const break_op = 0xA5;
pub const break_point_op = 0xCC;
pub const ones_op = 0xFF;
/// Second bytes of the `lnot_op` (0x92) compound comparison opcodes.
pub const lnot = struct {
pub const not_equal = 0x93; // LNotEqualOp: 0x92 0x93
pub const less_equal = 0x94; // LLessEqualOp: 0x92 0x94
pub const greater_equal = 0x95; // LGreaterEqualOp: 0x92 0x95
};
/// Second bytes of extended opcodes (prefixed by `ext_op_prefix`, 0x5B).
pub const ext = struct {
pub const mutex = 0x01;
pub const event = 0x02;
pub const cond_ref_of = 0x12;
pub const create_field = 0x13;
pub const load_table = 0x1F;
pub const load = 0x20;
pub const stall = 0x21;
pub const sleep = 0x22;
pub const acquire = 0x23;
pub const signal = 0x24;
pub const wait = 0x25;
pub const reset = 0x26;
pub const release = 0x27;
pub const from_bcd = 0x28;
pub const to_bcd = 0x29;
pub const unload = 0x2A;
pub const revision = 0x30;
pub const debug = 0x31;
pub const fatal = 0x32;
pub const timer = 0x33;
pub const op_region = 0x80;
pub const field = 0x81;
pub const device = 0x82;
pub const processor = 0x83;
pub const power_res = 0x84;
pub const thermal_zone = 0x85;
pub const index_field = 0x86;
pub const bank_field = 0x87;
pub const data_region = 0x88;
};
+517
View File
@@ -0,0 +1,517 @@
//! Recursive-descent AML parser. Walks the entire byte stream — including method
//! bodies — building the ACPI namespace as it goes. It does not *evaluate*
//! anything (no OperationRegion reads, no arithmetic); it parses structure so the
//! cursor stays aligned and every named object is recorded.
//!
//! The one genuine ambiguity in AML is method invocation: a bare NameString in an
//! operand position is a call whose argument count is only known from the method's
//! (earlier) declaration. Because we build the namespace in the same in-order pass,
//! `resolve` finds that declaration and tells us how many operands to consume.
//!
//! Safety net: every object delimited by a PkgLength (Scope/Device/Method/If/While/
//! Field/Buffer/Package/…) is parsed within its known extent, and the cursor is
//! snapped to that extent afterwards. So a mis-resolved invocation can only desync
//! *within* one such object; the enclosing walk realigns at the boundary.
const std = @import("std");
const op = @import("opcodes.zig");
const ns = @import("namespace.zig");
const Namespace = ns.Namespace;
const Node = ns.Node;
pub const Error = error{ Truncated, Malformed } || std.mem.Allocator.Error;
const max_segs = 64;
/// A parsed NameString: an optional root anchor or some parent hops, then a list
/// of 4-byte segments.
const NamePath = struct {
rooted: bool = false,
parents: u8 = 0,
segs: [max_segs][4]u8 = undefined,
count: usize = 0,
fn slice(self: *const NamePath) []const [4]u8 {
return self.segs[0..self.count];
}
};
pub const Parser = struct {
aml: []const u8,
pos: usize = 0,
namespace: *Namespace,
pub fn init(aml: []const u8, namespace: *Namespace) Parser {
return .{ .aml = aml, .namespace = namespace };
}
/// Parse the whole block as a TermList under the namespace root. Returns the
/// number of bytes consumed — equal to `aml.len` for a clean full traversal.
pub fn parseAll(self: *Parser) usize {
self.termList(self.aml.len, self.namespace.root);
return self.pos;
}
// --- cursor primitives --------------------------------------------------
fn eof(self: *Parser) bool {
return self.pos >= self.aml.len;
}
fn peek(self: *Parser) ?u8 {
return if (self.eof()) null else self.aml[self.pos];
}
fn readByte(self: *Parser) Error!u8 {
if (self.eof()) return error.Truncated;
const b = self.aml[self.pos];
self.pos += 1;
return b;
}
fn skip(self: *Parser, n: usize) Error!void {
if (self.pos + n > self.aml.len) return error.Truncated;
self.pos += n;
}
fn skipCString(self: *Parser) Error!void {
while (true) {
const b = try self.readByte();
if (b == 0) return;
}
}
/// AML PkgLength: the lead byte's top two bits give how many extra bytes
/// follow; the value counts from the start of the PkgLength field.
fn readPkgLength(self: *Parser) Error!usize {
const lead = try self.readByte();
const follow: usize = lead >> 6;
if (follow == 0) return lead & 0x3F;
var value: usize = lead & 0x0F;
var i: usize = 0;
while (i < follow) : (i += 1) {
const b = try self.readByte();
value |= @as(usize, b) << @intCast(4 + i * 8);
}
return value;
}
fn readNameSeg(self: *Parser) Error![4]u8 {
if (self.pos + 4 > self.aml.len) return error.Truncated;
const seg = self.aml[self.pos..][0..4].*;
self.pos += 4;
return seg;
}
fn readNameString(self: *Parser) Error!NamePath {
var np = NamePath{};
// A NameString is either root-anchored or parent-relative, not both.
if (self.peek() == op.root_char) {
np.rooted = true;
self.pos += 1;
} else {
while (self.peek() == op.parent_prefix_char) : (self.pos += 1) np.parents += 1;
}
const lead = self.peek() orelse return np;
switch (lead) {
0x00 => self.pos += 1, // NullName
op.dual_name_prefix => {
self.pos += 1;
try self.appendSeg(&np);
try self.appendSeg(&np);
},
op.multi_name_prefix => {
self.pos += 1;
const cnt = try self.readByte();
var i: usize = 0;
while (i < cnt) : (i += 1) try self.appendSeg(&np);
},
else => {
if (isNameStart(lead)) try self.appendSeg(&np);
},
}
return np;
}
fn appendSeg(self: *Parser, np: *NamePath) Error!void {
const seg = try self.readNameSeg();
if (np.count < max_segs) {
np.segs[np.count] = seg;
np.count += 1;
}
}
// --- term list / object -------------------------------------------------
/// Parse objects until `end`, then snap to `end`. Any parse error resyncs to
/// the boundary rather than propagating — containment for the rare desync.
fn termList(self: *Parser, end: usize, scope: *Node) void {
while (self.pos < end) {
self.object(scope) catch break;
}
self.pos = end;
}
/// Parse exactly one object/term at the cursor. Used for both TermObjs and
/// operands (TermArg / SuperName / Target all reduce to "one object" for the
/// purpose of advancing the cursor).
fn object(self: *Parser, scope: *Node) Error!void {
const lead = self.peek() orelse return error.Truncated;
if (isNameStart(lead)) return self.nameInvocation(scope);
_ = try self.readByte();
switch (lead) {
// constants and no-operand statements
op.zero_op, op.one_op, op.ones_op => {},
op.noop_op, op.continue_op, op.break_op, op.break_point_op => {},
op.local0_op...op.local7_op => {},
op.arg0_op...op.arg6_op => {},
// literal data
op.byte_prefix => try self.skip(1),
op.word_prefix => try self.skip(2),
op.dword_prefix => try self.skip(4),
op.qword_prefix => try self.skip(8),
op.string_prefix => try self.skipCString(),
// data containers (contents skipped via their PkgLength)
op.buffer_op, op.package_op, op.var_package_op => try self.skipPkg(),
// namespace modifiers / named objects
op.name_op => try self.opName(scope),
op.alias_op => try self.opAlias(scope),
op.scope_op => try self.opScopeLike(scope, .scope),
op.method_op => try self.opMethod(scope),
op.external_op => try self.opExternal(scope),
op.ext_op_prefix => try self.opExt(scope),
// control flow
op.if_op => try self.opIf(scope),
op.else_op => try self.opElse(scope),
op.while_op => try self.opWhile(scope),
op.return_op => try self.object(scope),
op.notify_op => try self.args(scope, 2),
// stores / references / unary+target
op.store_op => try self.args(scope, 2),
op.ref_of_op, op.deref_of_op, op.size_of_op, op.object_type_op => try self.args(scope, 1),
op.increment_op, op.decrement_op => try self.args(scope, 1),
op.not_op, op.find_set_left_bit_op, op.find_set_right_bit_op => try self.args(scope, 2),
op.to_buffer_op, op.to_decimal_string_op, op.to_hex_string_op, op.to_integer_op => try self.args(scope, 2),
op.copy_object_op => try self.args(scope, 2),
// binary + target
op.add_op, op.subtract_op, op.multiply_op, op.mod_op => try self.args(scope, 3),
op.and_op, op.nand_op, op.or_op, op.nor_op, op.xor_op => try self.args(scope, 3),
op.shift_left_op, op.shift_right_op, op.concat_op, op.concat_res_op, op.index_op => try self.args(scope, 3),
op.divide_op => try self.args(scope, 4),
op.to_string_op => try self.args(scope, 3),
op.mid_op => try self.args(scope, 4),
// logical
op.land_op, op.lor_op => try self.args(scope, 2),
op.lequal_op, op.lgreater_op, op.lless_op => try self.args(scope, 2),
op.lnot_op => try self.opLnot(scope),
op.match_op => try self.opMatch(scope),
// CreateXField: <source> <index> NameString
op.create_dword_field_op,
op.create_word_field_op,
op.create_byte_field_op,
op.create_bit_field_op,
op.create_qword_field_op,
=> try self.opCreateField(scope, 2),
else => return error.Malformed,
}
}
/// Parse `n` operands.
fn args(self: *Parser, scope: *Node, n: usize) Error!void {
var i: usize = 0;
while (i < n) : (i += 1) try self.object(scope);
}
/// A NameString in operand/statement position: a method invocation (consuming
/// the callee's declared argument count) or a plain name reference.
fn nameInvocation(self: *Parser, scope: *Node) Error!void {
const np = try self.readNameString();
if (self.namespace.resolve(scope, np.rooted, np.parents, np.slice())) |node| {
if ((node.kind == .method or node.kind == .external) and node.arg_count > 0) {
try self.args(scope, node.arg_count);
}
}
}
/// Skip a PkgLength-delimited body wholesale (Buffer / Package / VarPackage):
/// the contents are pure data, never namespace declarations.
fn skipPkg(self: *Parser) Error!void {
const start = self.pos;
const len = try self.readPkgLength();
const end = start + len;
if (end > self.aml.len) return error.Truncated;
self.pos = end;
}
// --- namespace objects --------------------------------------------------
fn opName(self: *Parser, scope: *Node) Error!void {
const np = try self.readNameString();
const val_start = self.pos;
try self.object(scope); // the DataRefObject value
const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .name);
node.value = self.aml[val_start..self.pos];
}
fn opAlias(self: *Parser, scope: *Node) Error!void {
_ = try self.readNameString(); // source
const np = try self.readNameString(); // the alias name
_ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .alias);
}
fn opMethod(self: *Parser, scope: *Node) Error!void {
const start = self.pos;
const end = start + try self.readPkgLength();
const np = try self.readNameString();
const flags = try self.readByte();
const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .method);
node.arg_count = flags & 0x7;
// Capture the body for on-demand evaluation and skip it — objects declared
// inside a method are created at *runtime*, not at load, so they must not
// become permanent namespace nodes.
node.value = self.aml[self.pos..@min(end, self.aml.len)];
self.pos = end;
}
fn opExternal(self: *Parser, scope: *Node) Error!void {
const np = try self.readNameString();
_ = try self.readByte(); // object type
const arg_count = try self.readByte();
const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .external);
node.arg_count = arg_count;
}
/// Scope / Device / ThermalZone: PkgLength, NameString, then a nested TermList.
fn opScopeLike(self: *Parser, scope: *Node, kind: ns.NodeKind) Error!void {
const start = self.pos;
const end = start + try self.readPkgLength();
const np = try self.readNameString();
const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), kind);
self.termList(end, node);
}
fn opProcessor(self: *Parser, scope: *Node) Error!void {
const start = self.pos;
const end = start + try self.readPkgLength();
const np = try self.readNameString();
try self.skip(6); // ProcID(byte) + PblkAddr(dword) + PblkLen(byte)
const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .processor);
self.termList(end, node);
}
fn opPowerRes(self: *Parser, scope: *Node) Error!void {
const start = self.pos;
const end = start + try self.readPkgLength();
const np = try self.readNameString();
try self.skip(3); // SystemLevel(byte) + ResourceOrder(word)
const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .power_res);
self.termList(end, node);
}
/// OperationRegion: NameString, RegionSpace(byte), Offset(TermArg), Len(TermArg).
/// The offset/length expressions are kept as AML for lazy evaluation.
fn opRegion(self: *Parser, scope: *Node) Error!void {
const np = try self.readNameString();
const space = try self.readByte();
const off_start = self.pos;
try self.object(scope);
const off_end = self.pos;
try self.object(scope);
const len_end = self.pos;
const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .region);
node.region_space = space;
node.region_offset_aml = self.aml[off_start..off_end];
node.region_len_aml = self.aml[off_end..len_end];
}
fn opDataRegion(self: *Parser, scope: *Node) Error!void {
const np = try self.readNameString();
try self.args(scope, 3); // signature, oem id, oem table id (TermArgs)
_ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .region);
}
fn opMutex(self: *Parser, scope: *Node) Error!void {
const np = try self.readNameString();
try self.skip(1); // sync flags
_ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .mutex);
}
fn opEvent(self: *Parser, scope: *Node) Error!void {
const np = try self.readNameString();
_ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .event);
}
/// CreateXField: `count` TermArgs then the new field's NameString.
fn opCreateField(self: *Parser, scope: *Node, count: usize) Error!void {
try self.args(scope, count);
const np = try self.readNameString();
_ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .name);
}
/// Field / IndexField / BankField: a region/bank reference, flags, then a
/// FieldList whose NamedFields become nodes in the current scope. For a plain
/// Field, the first NameString is the backing region — captured so field units
/// carry a region + bit position the evaluator can read/write.
fn opField(self: *Parser, scope: *Node, name_strings: u8, bank: bool) Error!void {
const start = self.pos;
const end = start + try self.readPkgLength();
var region: ?*Node = null;
var i: u8 = 0;
while (i < name_strings) : (i += 1) {
const np = try self.readNameString();
// Only a plain Field's single NameString denotes an OperationRegion.
if (name_strings == 1) region = self.namespace.resolve(scope, np.rooted, np.parents, np.slice());
}
if (bank) try self.object(scope); // bank value TermArg
const flags = try self.readByte();
self.fieldList(end, scope, region, flags & 0x0F);
}
fn fieldList(self: *Parser, end: usize, scope: *Node, region: ?*Node, initial_access: u8) void {
var bit_offset: u32 = 0;
var access = initial_access;
while (self.pos < end) {
const lead = self.peek() orelse break;
switch (lead) {
0x00 => { // ReservedField: advances the bit position
self.pos += 1;
const width = self.readPkgLength() catch break;
bit_offset += @intCast(width);
},
0x01 => { // AccessField: AccessType (low nibble) + AccessAttrib
self.pos += 1;
const at = self.readByte() catch break;
self.skip(1) catch break;
access = at & 0x0F;
},
0x02 => { // ConnectField: NameString | BufferData
self.pos += 1;
self.object(scope) catch break;
},
0x03 => { // ExtendedAccessField: type + attrib + length
self.pos += 1;
self.skip(3) catch break;
},
else => { // NamedField: NameSeg + PkgLength (bit width)
const seg = self.readNameSeg() catch break;
const width = self.readPkgLength() catch break;
const unit = self.namespace.newFieldUnit(scope, seg) catch break;
unit.region = region;
unit.bit_offset = bit_offset;
unit.bit_width = @intCast(width);
unit.access_type = access;
bit_offset += @intCast(width);
},
}
}
self.pos = end;
}
// --- control flow -------------------------------------------------------
fn opIf(self: *Parser, scope: *Node) Error!void {
const start = self.pos;
const end = start + try self.readPkgLength();
try self.object(scope); // predicate
self.termList(end, scope);
if (self.peek() == op.else_op) {
self.pos += 1;
try self.opElse(scope);
}
}
fn opElse(self: *Parser, scope: *Node) Error!void {
const start = self.pos;
const end = start + try self.readPkgLength();
self.termList(end, scope);
}
fn opWhile(self: *Parser, scope: *Node) Error!void {
const start = self.pos;
const end = start + try self.readPkgLength();
try self.object(scope); // predicate
self.termList(end, scope);
}
fn opLnot(self: *Parser, scope: *Node) Error!void {
// 0x92 followed by 0x93/94/95 is a compound comparison (two operands);
// otherwise it is a plain LNot of one operand.
const b = self.peek() orelse return error.Truncated;
switch (b) {
op.lnot.not_equal, op.lnot.less_equal, op.lnot.greater_equal => {
self.pos += 1;
try self.args(scope, 2);
},
else => try self.object(scope),
}
}
fn opMatch(self: *Parser, scope: *Node) Error!void {
try self.object(scope); // search package
try self.skip(1); // match opcode 1
try self.object(scope); // operand 1
try self.skip(1); // match opcode 2
try self.object(scope); // operand 2
try self.object(scope); // start index
}
// --- extended opcodes (0x5B xx) -----------------------------------------
fn opExt(self: *Parser, scope: *Node) Error!void {
const e = try self.readByte();
switch (e) {
op.ext.mutex => try self.opMutex(scope),
op.ext.event => try self.opEvent(scope),
op.ext.op_region => try self.opRegion(scope),
op.ext.data_region => try self.opDataRegion(scope),
op.ext.field => try self.opField(scope, 1, false),
op.ext.index_field => try self.opField(scope, 2, false),
op.ext.bank_field => try self.opField(scope, 2, true),
op.ext.device => try self.opScopeLike(scope, .device),
op.ext.thermal_zone => try self.opScopeLike(scope, .thermal_zone),
op.ext.processor => try self.opProcessor(scope),
op.ext.power_res => try self.opPowerRes(scope),
op.ext.cond_ref_of => try self.args(scope, 2), // SuperName, Target
op.ext.create_field => try self.opCreateField(scope, 3),
op.ext.load_table => try self.args(scope, 6),
op.ext.load => try self.args(scope, 2), // NameString, Target
op.ext.stall, op.ext.sleep => try self.args(scope, 1),
op.ext.acquire => {
try self.object(scope); // mutex SuperName
try self.skip(2); // timeout WordData
},
op.ext.signal, op.ext.reset, op.ext.release, op.ext.unload => try self.args(scope, 1),
op.ext.wait => try self.args(scope, 2),
op.ext.from_bcd, op.ext.to_bcd => try self.args(scope, 2),
op.ext.fatal => {
try self.skip(5); // Type(byte) + Code(dword)
try self.object(scope); // Arg TermArg
},
op.ext.revision, op.ext.debug, op.ext.timer => {},
else => return error.Malformed,
}
}
};
fn isNameStart(b: u8) bool {
return (b >= op.name_char_start and b <= op.name_char_end) or
b == op.name_char_underscore or
b == op.root_char or
b == op.parent_prefix_char or
b == op.dual_name_prefix or
b == op.multi_name_prefix;
}
+204
View File
@@ -0,0 +1,204 @@
//! The backend-agnostic device model.
//!
//! Discovery backends (ACPI today, device-tree later) translate their native
//! hardware description into this one shape, so the rest of the kernel walks a
//! plain `Device` tree without knowing which firmware described the machine —
//! the same discipline `root.zig`'s `MemoryKind` applies to memory and `arch`
//! applies to the CPU.
//!
//! This is deliberately minimal: enough to *describe* what was discovered (a
//! named node, its class, and its hardware resources) and where it sits in the
//! bus hierarchy. Driver matching, families, and probing are a later layer built
//! on top of this — nothing here presumes them.
const std = @import("std");
/// The hardware primitives a discovery backend needs but can't express portably.
/// The kernel injects an implementation (the arch VMM + port I/O), so the device
/// layer touches hardware without importing `arch` — the same discipline that lets
/// it stay firmware-agnostic. `pioRead`/`pioWrite` take a width in bytes (1/2/4).
pub const Hal = struct {
mapMmio: *const fn (virt: u64, phys: u64, writable: bool) void,
pioRead: *const fn (width: u8, port: u16) u32,
pioWrite: *const fn (width: u8, port: u16, value: u32) void,
};
/// The kind of hardware resource a device occupies.
pub const ResourceKind = enum {
/// A memory-mapped I/O window: `start` is the physical base, `len` its size.
memory,
/// A legacy I/O-port range: `start` is the first port, `len` the count.
io_port,
/// An interrupt: `start` is the global system interrupt (GSI), `len` is 1.
irq,
/// A range of bus numbers owned by a bridge: `start`..`start+len`.
bus_range,
};
/// One hardware resource claimed by a device.
pub const Resource = struct {
kind: ResourceKind,
start: u64,
len: u64,
};
/// A coarse classification of a device, independent of the describing firmware.
/// Kept small on purpose; refine as real drivers arrive.
pub const DeviceClass = enum {
/// The synthetic root every discovered device hangs beneath.
root,
processor,
interrupt_controller,
timer,
/// A PCI(e) host bridge — the root of a PCI segment (owns an ECAM window).
pci_host_bridge,
/// A single PCI function.
pci_device,
/// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a
/// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`).
acpi_device,
unknown,
};
/// Firmware-independent identity. Each backend fills only the fields it knows;
/// the rest stay null. The generic layer never branches on *how* an id was
/// obtained, only on its value.
pub const Ids = struct {
/// The device's ACPI hardware ID (`_HID`), EISA-encoded into 4 bytes, when applicable.
acpi_hid: ?u32 = null,
/// PCI configuration-space identity, when this node is a PCI function.
pci_vendor: ?u16 = null,
pci_device: ?u16 = null,
/// PCI class/subclass/prog-if packed as 0xCCSSPP.
pci_class: ?u24 = null,
/// PCI bus/device/function packed as (bus << 8) | (dev << 3) | func — the key
/// the ACPI address (`_ADR`) merge uses to match a namespace device to this node.
pci_bdf: ?u16 = null,
};
/// Upper bound on resources tracked per device (6 PCI BARs + a couple of IRQs is
/// the busy case). Stored inline so a device is a single allocation.
pub const max_resources = 8;
/// One node in the device tree. Nodes are individually heap-allocated and linked
/// intrusively (first-child / next-sibling), the classic device-tree layout —
/// no per-node dynamic arrays to manage.
pub const Device = struct {
name_buf: [24]u8 = undefined,
name_len: u8 = 0,
class: DeviceClass = .unknown,
ids: Ids = .{},
/// Human-readable hardware id (e.g. "PNP0A03"), when known. Backed inline like
/// `name`; empty when unset. The generic layer stores/prints it without knowing
/// how a backend encoded it.
hid_buf: [8]u8 = undefined,
hid_len: u8 = 0,
resources: [max_resources]Resource = undefined,
resource_count: u8 = 0,
parent: ?*Device = null,
first_child: ?*Device = null,
next_sibling: ?*Device = null,
/// The device's short name (e.g. "cpu0", "pci0:00:1f.0"). Backed by an inline
/// buffer, so it stays valid for the life of the node with no extra allocation.
pub fn name(self: *const Device) []const u8 {
return self.name_buf[0..self.name_len];
}
fn setName(self: *Device, s: []const u8) void {
const n: u8 = @intCast(@min(s.len, self.name_buf.len));
@memcpy(self.name_buf[0..n], s[0..n]);
self.name_len = n;
}
/// The device's hardware id string, or empty if none is set.
pub fn hid(self: *const Device) []const u8 {
return self.hid_buf[0..self.hid_len];
}
pub fn setHid(self: *Device, s: []const u8) void {
const n: u8 = @intCast(@min(s.len, self.hid_buf.len));
@memcpy(self.hid_buf[0..n], s[0..n]);
self.hid_len = n;
}
/// Record a resource. Silently drops beyond `max_resources` — discovery logs
/// the truncation rather than failing the whole tree.
pub fn addResource(self: *Device, kind: ResourceKind, start: u64, len: u64) bool {
if (self.resource_count >= max_resources) return false;
self.resources[self.resource_count] = .{ .kind = kind, .start = start, .len = len };
self.resource_count += 1;
return true;
}
};
/// Owns the discovered device tree and the allocator its nodes came from.
pub const DeviceTree = struct {
allocator: std.mem.Allocator,
root: *Device,
/// Create a tree with just the synthetic root node.
pub fn init(allocator: std.mem.Allocator) !DeviceTree {
const root = try allocator.create(Device);
root.* = .{ .class = .root };
root.setName("root");
return .{ .allocator = allocator, .root = root };
}
/// Allocate a device and append it under `parent`, returning it so the caller
/// can attach resources/ids. Appended at the tail so a dump reads in the order
/// devices were discovered.
pub fn addChild(
self: *DeviceTree,
parent: *Device,
class: DeviceClass,
dev_name: []const u8,
) !*Device {
const d = try self.allocator.create(Device);
d.* = .{ .class = class, .parent = parent };
d.setName(dev_name);
if (parent.first_child == null) {
parent.first_child = d;
} else {
var cur = parent.first_child.?;
while (cur.next_sibling) |sib| cur = sib;
cur.next_sibling = d;
}
return d;
}
/// Walk the tree depth-first, emitting an indented, human-readable listing.
/// `emit` is a raw byte sink (e.g. the serial `debugWrite`), so this stays
/// independent of the kernel console.
pub fn dump(self: *const DeviceTree, emit: *const fn ([]const u8) void) void {
dumpNode(self.root, 0, emit);
}
};
fn dumpNode(dev: *const Device, depth: usize, emit: *const fn ([]const u8) void) void {
const indent = @min(depth * 2, 40);
var buf: [200]u8 = undefined;
@memset(buf[0..indent], ' ');
const body = if (dev.hid_len != 0)
std.fmt.bufPrint(buf[indent..], "{s} [{s}] hid={s}\n", .{ dev.name(), @tagName(dev.class), dev.hid() }) catch return
else
std.fmt.bufPrint(buf[indent..], "{s} [{s}]\n", .{ dev.name(), @tagName(dev.class) }) catch return;
emit(buf[0 .. indent + body.len]);
for (dev.resources[0..dev.resource_count]) |r| {
var rbuf: [200]u8 = undefined;
const pad = @min(indent + 2, 42);
@memset(rbuf[0..pad], ' ');
const rline = std.fmt.bufPrint(
rbuf[pad..],
"- {s} 0x{x} len 0x{x}\n",
.{ @tagName(r.kind), r.start, r.len },
) catch continue;
emit(rbuf[0 .. pad + rline.len]);
}
var child = dev.first_child;
while (child) |c| : (child = c.next_sibling) dumpNode(c, depth + 1, emit);
}
+16
View File
@@ -0,0 +1,16 @@
//! Device-tree (Flattened Device Tree / FDT) discovery backend — stub.
//!
//! This is the second backend the platform facade dispatches to, for machines
//! that describe hardware with a device-tree blob instead of ACPI (typically
//! ARM). It is intentionally unimplemented: the bootloader has no DTB handoff
//! field yet, so this path is currently unreachable. It exists so the facade
//! already routes to a backend rather than hard-coding ACPI — wiring the FDT
//! parser in later is a local change here, not an architectural one.
const device = @import("device.zig");
/// Populate `dt` from a device-tree blob. Not implemented yet.
pub fn discover(dt: *device.DeviceTree) !void {
_ = dt;
return error.Unsupported;
}
+67
View File
@@ -0,0 +1,67 @@
//! The firmware-agnostic discovery facade.
//!
//! The kernel calls `platform.discover()` and gets back a generic `DeviceTree`
//! without ever naming ACPI or device-tree — the same way it imports `arch`
//! without naming x86_64. Which backend runs is decided *at runtime* from what
//! the bootloader handed us (an ACPI RSDP today, a device-tree blob later),
//! because a single image — a future ARM kernel especially — may boot under
//! either firmware. That's a deliberate divergence from `arch`, which is a
//! compile-time choice.
const std = @import("std");
const danos = @import("danos");
const device = @import("device.zig");
const acpi = @import("acpi.zig");
const power = @import("power.zig");
const devicetree = @import("devicetree.zig");
pub const DeviceTree = device.DeviceTree;
pub const Device = device.Device;
pub const DeviceClass = device.DeviceClass;
pub const Hal = device.Hal;
pub const PowerInfo = acpi.PowerInfo;
pub const AmlStats = acpi.AmlStats;
/// The register map + sleep types discovery extracted, for logging/diagnostics.
pub fn powerInfo() PowerInfo {
return acpi.power_info;
}
/// AML parse integrity/diagnostics (namespace node count, bytes consumed).
pub fn amlStats() AmlStats {
return acpi.aml_stats;
}
/// Enumerate hardware into a fresh device tree. `hal` supplies the hardware
/// primitives the backend needs (MMIO mapping for PCIe config space, port I/O for
/// ACPI registers); pass the arch implementation. Errors leave nothing to clean up
/// beyond the tree's own allocations.
pub fn discover(
boot_info: *const danos.BootInfo,
allocator: std.mem.Allocator,
hal: Hal,
) !DeviceTree {
var dt = try DeviceTree.init(allocator);
if (boot_info.acpi_rsdp != 0) {
try acpi.discover(boot_info.acpi_rsdp, &dt, hal);
} else {
// No ACPI RSDP. A device-tree boot would parse its blob here; today that
// path is a stub, so this reports the machine described itself no way we
// understand yet.
try devicetree.discover(&dt);
}
return dt;
}
/// Restart the machine. Never returns on success; returns only if no reset method
/// worked (extremely unlikely). Backend-agnostic entry the kernel calls.
pub fn reboot(hal: Hal) void {
power.reboot(hal);
}
/// Power the machine off (ACPI S5). Never returns on success.
pub fn shutdown(hal: Hal) void {
power.shutdown(hal);
}
+106
View File
@@ -0,0 +1,106 @@
//! Machine power control: enter ACPI mode, reboot, and power off (ACPI S5).
//!
//! Built entirely on the register map `acpi` extracted from the FADT plus the
//! sleep-state (`_Sx`) types the AML submodule pulled from the DSDT, driven through the
//! injected `Hal` (port I/O and MMIO). Nothing here is x86-specific beyond the
//! well-known legacy reset fallbacks, which are guarded behind the ACPI methods.
//!
//! S3 (suspend-to-RAM) is stubbed: it needs a wake trampoline and device
//! re-initialisation, a milestone of its own.
const acpi = @import("acpi.zig");
const device = @import("device.zig");
const Hal = device.Hal;
const slp_en: u32 = 1 << 13; // SLP_EN: writing 1 triggers the sleep transition
const sci_en: u32 = 1 << 0; // SCI_EN in PM1 control: set once ACPI mode is active
/// Switch the platform into ACPI mode if it isn't already, so the PM1 control
/// register is live. A no-op when the firmware exposes no SMI command port (ACPI
/// already enabled, as under QEMU/OVMF) — we still verify SCI_EN first.
pub fn enable(hal: Hal) void {
const pi = acpi.power_info;
if (!pi.pm1a_cnt.present()) return;
if (readReg(hal, pi.pm1a_cnt) & sci_en != 0) return; // already in ACPI mode
if (pi.smi_cmd == 0 or pi.acpi_enable == 0) return; // no way to switch; assume fine
hal.pioWrite(1, pi.smi_cmd, pi.acpi_enable);
var spins: usize = 0;
while (readReg(hal, pi.pm1a_cnt) & sci_en == 0 and spins < 1_000_000) : (spins += 1) {}
}
/// Restart the machine. Tries the ACPI reset register first, then the two legacy
/// fallbacks. Returns only if every method failed (very unlikely).
pub fn reboot(hal: Hal) void {
const pi = acpi.power_info;
// 1. The FADT reset register, when the firmware advertises support.
if (pi.reset_supported and pi.reset.present()) {
writeReg(hal, pi.reset, pi.reset_value);
delay();
}
// 2. The PCI reset-control register at port 0xCF9 (RST_CPU | SYS_RST).
hal.pioWrite(1, 0xCF9, 0x0E);
hal.pioWrite(1, 0xCF9, 0x06);
delay();
// 3. Pulse the 8042 keyboard controller's reset line.
hal.pioWrite(1, 0x64, 0xFE);
delay();
}
/// Power the machine off via ACPI S5. Requires the soft-off (`_S5`) sleep type; if
/// it wasn't found in the AML, there is nothing safe to do and this returns.
pub fn shutdown(hal: Hal) void {
enable(hal);
const pi = acpi.power_info;
const s5 = pi.s5 orelse return;
if (pi.pm1a_cnt.present()) {
writeReg(hal, pi.pm1a_cnt, sleepValue(s5.slp_typ_a));
}
if (pi.pm1b_cnt.present()) {
writeReg(hal, pi.pm1b_cnt, sleepValue(s5.slp_typ_b));
}
delay();
}
/// S3 suspend-to-RAM — not implemented (needs a wake path + device re-init).
pub fn sleepS3(hal: Hal) error{Unsupported}!void {
_ = hal;
return error.Unsupported;
}
/// The PM1 control write that requests sleep type `slp_typ`: SLP_TYP in bits
/// [12:10], SLP_EN in bit 13.
fn sleepValue(slp_typ: u8) u32 {
return (@as(u32, slp_typ & 0x7) << 10) | slp_en;
}
fn readReg(hal: Hal, reg: acpi.RegAccess) u32 {
if (reg.mmio) {
hal.mapMmio(reg.address, reg.address, true);
const p: *align(1) volatile u32 = @ptrFromInt(reg.address);
return p.*;
}
return hal.pioRead(reg.width, @intCast(reg.address));
}
fn writeReg(hal: Hal, reg: acpi.RegAccess, value: u32) void {
if (reg.mmio) {
hal.mapMmio(reg.address, reg.address, true);
const p: *align(1) volatile u32 = @ptrFromInt(reg.address);
p.* = value;
} else {
hal.pioWrite(reg.width, @intCast(reg.address), value);
}
}
/// A short busy-wait so a reset/power-off takes effect before we fall through to
/// the next method. The empty asm is an arch-neutral barrier that keeps the loop
/// from being optimised away.
fn delay() void {
var i: usize = 0;
while (i < 50_000_000) : (i += 1) {
asm volatile ("" ::: .{ .memory = true });
}
}
+23
View File
@@ -11,6 +11,7 @@ const idt = @import("idt.zig");
const paging = @import("paging.zig");
const serial = @import("serial.zig");
const apic = @import("apic.zig");
const io = @import("io.zig");
/// The saved register/trap frame passed to a fault handler.
pub const CpuState = idt.CpuState;
@@ -177,6 +178,28 @@ pub fn vectorName(vector: u64) []const u8 {
return idt.vectorName(vector);
}
/// Read `width` bytes (1/2/4) from an I/O port. The generic device layer drives
/// ACPI registers through this rather than naming x86 port instructions; on an
/// MMIO-only architecture this would be implemented differently.
pub fn pioRead(width: u8, port: u16) u32 {
return switch (width) {
1 => io.inb(port),
2 => io.inw(port),
4 => io.inl(port),
else => 0,
};
}
/// Write `width` bytes (1/2/4) to an I/O port.
pub fn pioWrite(width: u8, port: u16, value: u32) void {
switch (width) {
1 => io.outb(port, @truncate(value)),
2 => io.outw(port, @truncate(value)),
4 => io.outl(port, value),
else => {},
}
}
/// CR2 holds the faulting linear address after a page fault (#PF, vector 14).
pub fn readCr2() u64 {
return asm volatile ("mov %%cr2, %[out]"
+30
View File
@@ -16,6 +16,36 @@ pub fn inb(port: u16) u8 {
);
}
pub fn outw(port: u16, value: u16) void {
asm volatile ("outw %[value], %[port]"
:
: [value] "{ax}" (value),
[port] "{dx}" (port),
);
}
pub fn inw(port: u16) u16 {
return asm volatile ("inw %[port], %[value]"
: [value] "={ax}" (-> u16),
: [port] "{dx}" (port),
);
}
pub fn outl(port: u16, value: u32) void {
asm volatile ("outl %[value], %[port]"
:
: [value] "{eax}" (value),
[port] "{dx}" (port),
);
}
pub fn inl(port: u16) u32 {
return asm volatile ("inl %[port], %[value]"
: [value] "={eax}" (-> u32),
: [port] "{dx}" (port),
);
}
/// Read a model-specific register (returns edx:eax combined).
pub fn rdmsr(msr: u32) u64 {
var low: u32 = undefined;
+33
View File
@@ -5,6 +5,7 @@ const console = @import("console.zig");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const scheduler = @import("scheduler.zig");
const platform = @import("platform");
const tests = @import("tests.zig");
const build_options = @import("build_options");
const BootInfo = danos.BootInfo;
@@ -102,6 +103,38 @@ fn kmain(boot_info: *const BootInfo) noreturn {
const s2 = pmm.stats();
serial0.debugPrint(" Kernel footprint: {d} KiB\n", .{kib(s1.free_frames - s2.free_frames)});
// Enumerate hardware from the firmware tables (ACPI here) into a generic
// device tree, then list it. Discovery walks ACPI memory directly (identity-
// mapped) and maps PCIe config space on demand via the VMM. A failure here is
// not fatal yet — log it and carry on.
const hal = platform.Hal{
.mapMmio = arch.mapPage,
.pioRead = arch.pioRead,
.pioWrite = arch.pioWrite,
};
if (platform.discover(boot_info, heap.allocator(), hal)) |devtree| {
var dt = devtree;
serial0.debugWrite("\ndanos: device discovery online\n");
dt.dump(console.SerialConsole.debugWrite);
// Power register map extracted from the FADT + AML, for confidence it parsed.
const pw = platform.powerInfo();
serial0.debugWrite("danos: power\n");
serial0.debugPrint(" pm1a_cnt : {s} 0x{x} (width {d})\n", .{ if (pw.pm1a_cnt.mmio) "mmio" else "io", pw.pm1a_cnt.address, pw.pm1a_cnt.width });
if (pw.s5) |s| {
serial0.debugPrint(" S5 slp_typ : a={d} b={d}\n", .{ s.slp_typ_a, s.slp_typ_b });
} else {
serial0.debugWrite(" S5 slp_typ : (not found)\n");
}
serial0.debugPrint(" reset : supported={} {s} 0x{x} val 0x{x}\n", .{ pw.reset_supported, if (pw.reset.mmio) "mmio" else "io", pw.reset.address, pw.reset_value });
// AML namespace parse integrity: consumed should equal total.
const am = platform.amlStats();
serial0.debugPrint(" aml : {d} namespace nodes, parsed {d}/{d} bytes\n", .{ am.nodes, am.consumed, am.total });
} else |err| {
serial0.debugPrint("\ndanos: device discovery failed: {s}\n", .{@errorName(err)});
}
// Register the current context as the first task before enabling preemption.
scheduler.init(4);
serial0.debugWrite("\ndanos: scheduler online\n");
+29
View File
@@ -12,6 +12,7 @@
const std = @import("std");
const danos = @import("danos");
const arch = @import("arch");
const platform = @import("platform");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const sched = @import("scheduler.zig");
@@ -77,11 +78,39 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
faultNoExecute();
} else if (eql(case, "fault-null")) {
faultNull();
} else if (eql(case, "poweroff")) {
powerTest(.off);
} else if (eql(case, "reboot")) {
powerTest(.reboot);
} else {
log("DANOS-TEST-RESULT: FAIL (unknown case '{s}')\n", .{case});
}
}
fn platformHal() platform.Hal {
return .{
.mapMmio = arch.mapPage,
.pioRead = arch.pioRead,
.pioWrite = arch.pioWrite,
};
}
/// Drive an ACPI power transition. On success the machine powers off or resets,
/// so QEMU exits — the harness observes the process exit. If control returns, the
/// transition failed and we emit a FAIL result.
fn powerTest(comptime action: enum { off, reboot }) void {
const name = if (action == .off) "poweroff" else "reboot";
log("DANOS-TEST-BEGIN: {s}\n", .{name});
const hal = platformHal();
log("DANOS-POWER: attempting {s}\n", .{name});
switch (action) {
.off => platform.shutdown(hal),
.reboot => platform.reboot(hal),
}
check("power transition took effect", false);
result();
}
const BootInfo = danos.BootInfo;
fn eql(a: []const u8, b: []const u8) bool {
+5
View File
@@ -95,4 +95,9 @@ pub const BootInfo = extern struct {
/// The kernel's own PT_LOAD segments (it has three: text, rodata, data).
kernel_segments: [8]KernelSegment,
kernel_segment_count: u32,
/// Physical address of the ACPI RSDP the firmware exposed, or 0 if none. The
/// kernel's device layer parses the ACPI tables from here to discover hardware.
/// A device-tree boot path leaves this 0 and (later) fills a `device_tree_blob`
/// field instead, so the kernel discovers devices without knowing what booted it.
acpi_rsdp: u64 = 0,
};