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
+38 -1
View File
@@ -28,6 +28,26 @@ fn firstExisting(io: std.Io, candidates: []const []const u8) []const u8 {
return candidates[0]; return candidates[0];
} }
/// A UTC timestamp like "20260708-153045", for naming a per-run artifact so
/// repeated runs don't clobber each other's logs. Resolved when `build.zig` runs
/// (i.e. at `zig build` invocation), which is moments before QEMU launches.
fn timestamp(b: *std.Build) []const u8 {
const ns = std.Io.Clock.now(.real, b.graph.io).nanoseconds;
const secs: u64 = @intCast(@divFloor(ns, std.time.ns_per_s));
const es = std.time.epoch.EpochSeconds{ .secs = secs };
const yd = es.getEpochDay().calculateYearDay();
const md = yd.calculateMonthDay();
const ds = es.getDaySeconds();
return b.fmt("{d:0>4}{d:0>2}{d:0>2}-{d:0>2}{d:0>2}{d:0>2}", .{
yd.year,
md.month.numeric(),
@as(u32, md.day_index) + 1,
ds.getHoursIntoDay(),
ds.getMinutesIntoHour(),
ds.getSecondsIntoMinute(),
});
}
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
ensureZigVersion(); ensureZigVersion();
@@ -54,6 +74,18 @@ pub fn build(b: *std.Build) void {
// jumps/calls that Zig inline asm can't express (see the file's header). // jumps/calls that Zig inline asm can't express (see the file's header).
arch_mod.addAssemblyFile(b.path("src/kernel/arch/x86_64/isr.s")); arch_mod.addAssemblyFile(b.path("src/kernel/arch/x86_64/isr.s"));
// Firmware-agnostic device discovery. The generic kernel imports this as
// "platform" and asks it to enumerate hardware into a backend-neutral device
// tree, never naming ACPI (or, later, device-tree) — the same discipline the
// arch module applies to CPU code. The backend is selected at runtime from
// the boot handoff (see src/device/platform.zig).
const platform_mod = b.addModule("platform", .{
.root_source_file = b.path("src/device/platform.zig"),
.imports = &.{
.{ .name = "danos", .module = mod }, // BootInfo (carries the ACPI RSDP)
},
});
// Compile-time config the kernel reads as `@import("build_options")`. The // Compile-time config the kernel reads as `@import("build_options")`. The
// QEMU test harness sets -Dtest-case=<name> to run one self-test at boot. // QEMU test harness sets -Dtest-case=<name> to run one self-test at boot.
const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see src/kernel/tests.zig)"); const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see src/kernel/tests.zig)");
@@ -86,6 +118,7 @@ pub fn build(b: *std.Build) void {
.imports = &.{ .imports = &.{
.{ .name = "danos", .module = mod }, .{ .name = "danos", .module = mod },
.{ .name = "arch", .module = arch_mod }, .{ .name = "arch", .module = arch_mod },
.{ .name = "platform", .module = platform_mod },
.{ .name = "build_options", .module = build_options_mod }, .{ .name = "build_options", .module = build_options_mod },
}, },
}), }),
@@ -187,10 +220,14 @@ pub fn build(b: *std.Build) void {
"-device", "-device",
"VGA,edid=on,xres=1280,yres=720", "VGA,edid=on,xres=1280,yres=720",
}); });
// Always capture the guest's serial0 (the kernel's machine-readable log) to a
// timestamped file under zig-out, so each run leaves its own log behind.
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ b.install_path, timestamp(b) });
run_efi.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
run_efi.step.dependOn(&efi_install.step); run_efi.step.dependOn(&efi_install.step);
run_efi.step.dependOn(&kernel_install.step); run_efi.step.dependOn(&kernel_install.step);
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF)"); const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/run-x86-64-serial0-<timestamp>.log");
run_efi_step.dependOn(&run_efi.step); run_efi_step.dependOn(&run_efi.step);
// const run_cmd = b.addRunArtifact(exe); // const run_cmd = b.addRunArtifact(exe);
+6 -3
View File
@@ -59,9 +59,12 @@ Cutting across all of these:
- **[arm.md](arm.md) — ARM targets.** The Raspberry Pi landscape the arch split is - **[arm.md](arm.md) — ARM targets.** The Raspberry Pi landscape the arch split is
aiming at: `arm` (32-bit, Pi Zero W) vs `aarch64` (64-bit, Pi 3-5), UEFI vs aiming at: `arm` (32-bit, Pi Zero W) vs `aarch64` (64-bit, Pi 3-5), UEFI vs
device-tree boot, and what each layer needs. device-tree boot, and what each layer needs.
- **[discovery.md](discovery.md) — device discovery.** A design note (not built yet) - **[discovery.md](discovery.md) — device discovery.** A design note on learning what
on learning what hardware exists via ACPI (x86) or device tree (ARM) behind one hardware exists via ACPI (x86) or device tree (ARM) behind one neutral device model —
neutral device model — when to build it, and how to keep it architecture-agnostic. when to build it, and how to keep it architecture-agnostic.
- **[acpi.md](acpi.md) — finding the ACPI tables.** The concrete x86 locator chain:
how the loader captures the **RSDP**, hands its physical address across in `BootInfo`,
and how the platform derives the **RSDT/XSDT** from it and walks the SDTs.
- **[smp.md](smp.md) — multiple cores.** A design/research note on how microkernels - **[smp.md](smp.md) — multiple cores.** A design/research note on how microkernels
(L4, seL4) handle SMP — big kernel lock vs per-CPU vs multikernel — and how the (L4, seL4) handle SMP — big kernel lock vs per-CPU vs multikernel — and how the
right choice depends on whether danos is chasing real-time or resilience. right choice depends on whether danos is chasing real-time or resilience.
+118
View File
@@ -0,0 +1,118 @@
# ACPI: finding the tables (RSDP → RSDT/XSDT → SDTs)
ACPI describes the hardware the kernel can't assume — the interrupt controllers, the
PCIe config window, the timer, the power registers — in a set of **system
description tables** (SDTs). But before it can read any of them, danos has to *find*
them, and they aren't at a fixed address. Getting there is a short chain of pointers,
and this note explains it — in particular the question it's easy to trip on: **how
does the [platform / device module](arch.md) know where the RSDT is?**
Short answer: it doesn't receive the RSDT. The firmware hands over the **RSDP**, and
the RSDT's address is a field *inside* the RSDP. The platform follows that pointer.
## The locator chain
```
UEFI configuration table
│ the loader reads the RSDP's physical address
▼
BootInfo.acpi_rsdp (u64, in the shared `danos` module) src/root.zig
│ the kernel forwards the whole BootInfo
▼
platform.discover(boot_info, …) src/device/platform.zig
│ reads boot_info.acpi_rsdp, hands it to the ACPI backend
▼
acpi.discover(rsdp_phys, …) src/device/acpi.zig
│ dereferences the RSDP, reads the pointer it contains
▼
RSDP ──(a field in the struct)──► RSDT / XSDT ──► SDTs (MADT, MCFG, FADT, HPET, DSDT…)
```
The **RSDP** (Root System Description Pointer) is the root of the whole ACPI tree.
Its only job is to point at the root *table* — the **RSDT** (ACPI 1.0) or its 64-bit
successor the **XSDT** (ACPI 2.0+) — which in turn lists every other SDT.
## Step 1 — the loader finds the RSDP
Only the firmware knows where ACPI lives, so the RSDP must be grabbed while UEFI is
still up. `acpiRootSystemDescriptorPointer()` in `src/boot/efi.zig` walks the UEFI
**configuration table** for the ACPI GUID and returns the vendor pointer — the same
"grab it before `ExitBootServices`" pattern as the [framebuffer](framebuffer.md) and
the [memory map](memory-map.md).
## Step 2 — the handoff: a physical address in `BootInfo`
The loader can't just call the device module: the bootloader binary and the kernel
binary are compiled separately, and **the loader isn't linked against the `platform`
module at all** (it imports only the shared `danos` module). So instead of a call, it
deposits a value in the handoff struct:
```zig
// src/boot/efi.zig — while boot services are still up
.acpi_rsdp = if (acpiRootSystemDescriptorPointer()) |p| @intFromPtr(p) else 0,
```
Two things about what crosses the boundary:
- **It's a *physical* address, not a Zig pointer.** The loader and kernel don't share
an address space at the moment of the jump, so a raw `u64` physical address is the
only thing that survives the handoff. `BootInfo.acpi_rsdp` is `0` when the firmware
exposed no ACPI (e.g. a future device-tree machine, which would fill a different
field instead — the kernel never learns which firmware booted it).
- **The kernel can dereference it because it identity-maps ACPI memory.** The RSDP
lives in ACPI-reclaim memory, which [paging.zig](paging.md) identity-maps along with
the rest of RAM, so by the time discovery runs `@ptrFromInt(rsdp_phys)` is a valid
pointer.
This is the concrete form of the "capture the description pointer" step sketched in
[discovery.md](discovery.md) — a plain `acpi_rsdp: u64` rather than a tagged handle,
since x86 is the only backend wired up so far.
## Step 3 — the platform derives the RSDT from the RSDP
`acpi.discover` reinterprets the physical address as the RSDP struct, validates it,
and then reads the root-table pointer *out of it*. Which pointer depends on the ACPI
version, because the RSDP carries **both**:
```zig
const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(rsdp_phys);
if (!std.mem.eql(u8, &rsdp.signature, "RSD PTR ")) return error.BadRsdpSignature;
if (!checksumOk(@ptrFromInt(rsdp_phys), 20)) return error.BadRsdpChecksum;
if (rsdp.revision >= 2) {
// ACPI 2.0+: use the 64-bit XSDT pointer (the 32-bit RSDT is deprecated)
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(rsdp_phys);
try walkRoot(u64, xsdp.extended_system_descriptor_table_address, …);
} else {
// ACPI 1.0: use the 32-bit RSDT pointer
try walkRoot(u32, rsdp.root_system_description_table_address, …);
}
```
- The **`revision`** byte selects the root table. `root_system_description_table_address`
(32-bit, → RSDT) and `extended_system_descriptor_table_address` (64-bit, → XSDT) are
ordinary fields of the RSDP/XSDP structs — the platform never *receives* the RSDT
address, it *reads* it here. On QEMU q35 the RSDP is revision 2, so the XSDT path is
taken.
- The signature (`"RSD PTR "`) and one-byte checksum guard against a bad pointer before
anything downstream trusts it.
## After the root table
`walkRoot` treats the RSDT/XSDT as an array of physical pointers — 32-bit entries for
the RSDT, 64-bit for the XSDT — and hands each SDT to `handleTable`, which dispatches
on its 4-byte signature: **MADT** (CPUs + IOAPIC), **MCFG** (PCIe ECAM), **FADT**
(power registers, and the pointer to the DSDT), **HPET** (timer). That's where the
firmware-agnostic [device model](discovery.md) gets populated; this note stops at the
part that answers "where are the tables?" — everything past the RSDP is just following
more pointers the tables themselves provide.
## Related
- [efi.md](efi.md) — the loader that captures the RSDP before `ExitBootServices`.
- [memory-map.md](memory-map.md) — the same loader-captures / kernel-consumes seam, and
the ACPI-reclaim memory the RSDP lives in.
- [discovery.md](discovery.md) — the broader (still-evolving) plan for turning these
tables into one neutral device model shared with the ARM device-tree path.
- [arch.md](arch.md) — why the kernel reaches the device code through a `platform`
module and never names ACPI directly.
+2
View File
@@ -157,6 +157,8 @@ free; discovery on x86 is partly about *finding* what ARM just tells you.
## Related ## Related
- [acpi.md](acpi.md) — the built x86 side of the "capture" step: how the loader grabs
the RSDP and the platform follows it to the RSDT/XSDT and the SDTs.
- [arm.md](arm.md) — the aarch64 target that forces genuine discovery (DTB, GIC). - [arm.md](arm.md) — the aarch64 target that forces genuine discovery (DTB, GIC).
- [memory-map.md](memory-map.md) — the same loader-captures / kernel-consumes seam, - [memory-map.md](memory-map.md) — the same loader-captures / kernel-consumes seam,
and the note about grabbing the RSDP before `ExitBootServices`. and the note about grabbing the RSDP before `ExitBootServices`.
+80
View File
@@ -0,0 +1,80 @@
# System Calls
System calls (syscalls) are the bridge between your programs and the operating system's restricted core (kernel).
## The Mechanism of a Syscall
A system call follows a highly orchestrated, 6-step sequence to ensure hardware safety and process security:
1. Setting the Arguments: The program places a unique ID corresponding to the requested service (e.g., \(sys\_write\)) into a specific CPU register (like RAX in x86_64) along with its required parameters.
2. Executing the Trap: The program triggers a special CPU instruction, such as syscall or int 0x80. This acts as a software interrupt.
3. Mode Switch: The CPU atomically flips its execution privilege from unprivileged User Mode (Ring 3) to the highly privileged Kernel Mode (Ring 0).
4. Lookup and Execution: The kernel looks up the syscall ID in a dispatch table and runs the designated internal routine to perform the actual work (like fetching data from the hard drive).
5. Returning the Status: The kernel places the result of the operation—or an error code—back into the RAX register.
6. Return to User Mode: The kernel executes a return instruction (such as sysret), the CPU shifts back to User Mode, and your program continues executing.
## Approaches
There are two approaches to system calls, stable public ABI and unstable private ABI. A public ABI uses a standard defined set of numbers. It makes it easy to guess, easy to write compilers and tools for. private ABI tend to change the numbers to obscure the numbers, preventing attackers from bypassing libc using CPU instructions directly. Private ABIs force users to use a library like libSystem, which the kernel can inject the syscall numbers. An alternative solution is to just have 1 system number but make it generic by the address to a struct in memory.
Practical minimum primitives (Monolithic kernel)
| Category | Primitive | Detail |
|----------|--------------|---------------------------------------------------------------------------------------|
| lifecyle | spawn / exec | Loads a binary from storage into memory and begins execution. |
| lifecyle | exit | Terminates the current process and frees its memory back to the kernel. |
| I/O | read | Requests data from a hardware device or file descriptor into user memory. |
| I/O | write | Pushes data from user memory out to a device or file descriptor (like a screen). |
| Memory | brk / mmap | Requests the kernel to allocate more physical or virtual memory pages to the process. |
| Control | ioctl | A catch-all "escape hatch" call to send hardware-specific commands to device drivers. |
The Microkernel Minimum Set
Everything else---including`read()`,`write()`,`malloc()`, and`fork()`---will run in user space as servers (e.g., a VFS server, a memory manager server) that threads communicate with using these three calls:
1. **`IPC_Call(endpoint, message_buffer)`(Synchronous Send + Receive)**
- **What it does:**The calling thread sends a message block to a service endpoint and immediately blocks (sleeps) until that service processes the request and sends a reply back.
- **Why it's minimal:**Combining*Send*and*Receive*into a single atomic atomic system call eliminates the need for separate tracking and prevents a massive amount of context-switching overhead. This is the foundation of high-performance microkernels like[seL4](https://sel4.systems/)and L4.[[1](https://en.wikipedia.org/wiki/L4_microkernel_family),[2](https://www.microchip.com/en-us/products/microprocessors/64-bit-mpus/pic64hx/ecosystem)]
2. **`IPC_ReplyWait(endpoint, reply_buffer)`(Respond + Wait for Next)**
- **What it does:**Used strictly by your background user-space servers (like your disk driver or filesystem). It sends a reply to the last client that called it, and immediately puts the server to sleep until the next request arrives.[[1](https://news.ycombinator.com/item?id=33078441)]
3. **`Yield()`/`Thread_Ctrl()`**
- **What it does:**Allows a thread to voluntarily give up its CPU time slice, or allows a root task to spawn/kill threads.
* * * * *
Hardware Implementation: x86_64 vs. aarch64
Because you are targeting both platforms, you must design a clean**Architecture Abstraction Layer (AAL)**. Each architecture uses completely different assembly instructions, CPU registers, and privilege levels to jump from user space (Ring 3 / EL0) to kernel space (Ring 0 / EL1).[[1](https://android.googlesource.com/kernel/common/+/84d3e59750bbd/arch/arm64/Kconfig),[2](https://blog.codingconfessions.com/p/making-system-calls-in-x86-64-assembly),[3](https://alex.dzyoba.com/blog/os-segmentation/),[4](https://dev.to/ripan030/linux-kernel-interrupt-handling-part-2-fe1)]
Here is how you will map your bare minimum system calls on both architectures:
1\. x86_64 Implementation
On 64-bit Intel and AMD processors, you ignore the old`int 0x80`software interrupts. Instead, you use the high-performance`syscall`and`sysret`instructions.[[1](https://alex.dzyoba.com/blog/os-segmentation/)]
- **The Trap:**The user-space program executes the`syscall`instruction.[[1](https://namastedev.com/blog/kernel-vs-user-space-2/)]
- **The Registers:**The hardware automatically moves the instruction pointer, but you must pass your arguments in specific registers. A common microkernel convention mimics the System V AMD64 ABI:
- `rax`: System Call ID (e.g.,`0`for IPC_Call,`1`for IPC_ReplyWait)
- `rdi`: Argument 1 (Endpoint ID / Destination)
- `rsi`: Argument 2 (Pointer to the message payload buffer)
- `rdx`: Argument 3 (Size of the message)[[1](https://dev.to/kaamkiya/hello-world-in-assembly-x86-64-2kb8)]
- **Kernel Setup:**Your kernel must configure the Model Specific Registers (MSRs)---specifically`IA32_STAR`and`IA32_LSTAR`---during boot to point to your kernel's system call entry assembly code.[[1](https://nfil.dev/kernel/rust/coding/rust-kernel-to-userspace-and-back/),[2](https://johannst.github.io/notes/arch/x86_64.html)]
2\. aarch64 (ARM 64-bit) Implementation
On ARMv8-A and ARMv9-A architectures, privilege levels are called Exception Levels. User space runs at**EL0**, and your microkernel runs at**EL1**.[[1](https://community.nxp.com/pwmxy87654/attachments/pwmxy87654/imx-processors/183079/1/AN12212.pdf),[2](https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Learn%20the%20Architecture/Exception%20model.pdf?revision=a62f2bf2-b08a-4a4f-8cbe-38c67ddf4434),[3](https://people.kernel.org/linusw/),[4](https://drewdevault.com/blog/Helios-aarch64/)]
- **The Trap:**The user-space program executes the`svc #0`(Supervisor Call) instruction.[[1](https://hackmd.io/@xlYUTygoRkyuQQlwXuWDWQ/SJWQuIsIZe)]
- **The Registers:**ARM provides a clean, plentiful register set. You typically pass your arguments in the standard parameter registers:
- `x0`: System Call ID
- `x1`: Argument 1 (Endpoint ID / Destination)
- `x2`: Argument 2 (Pointer to message payload buffer)
- `x3`: Argument 3 (Size of the message)[[1](https://github.com/lelegard/arm-cpusysregs),[2](https://medium.com/@vincentcorbee/http-server-in-arm64-assembly-apple-silicon-m1-077a55bbe9ca)]
- **Kernel Setup:**Your kernel must set up an Exception Vector Table and write its base address to the`VBAR_EL1`register. When`svc`is executed, the CPU jumps to the synchronous exception offset in that table.[[1](https://dev.to/ripan030/linux-kernel-interrupt-handling-part-2-fe1),[2](https://eastrivervillage.com/blog/archive/2018/06/),[3](https://www.wadixtech.com/blog/armv8-a-exception-levels-el0-to-el3)]
* * * * *
Managing the Payload Challenge
Because it is a microkernel, performance lives or dies by how fast your`IPC_Call`can move data from Client to Server. You have two minimal choices for handling the`message_buffer`pointer:[[1](https://anazimzada2020.medium.com/microkernel-architectural-pattern-5e4e9184170e)]
- **The Copy Method (Simplest to start):**Your kernel pauses the client, reads the data from the client's memory space, switches page tables to the server, and copies the data into the server's buffer.
- **The Shared Memory Method (Fastest):**The kernel sets up a temporary, shared virtual memory page between the client and server. The client writes to it, calls`syscall`/`svc`, and the server reads it instantly without the kernel copying any bytes
+21 -1
View File
@@ -38,6 +38,10 @@ fn boot() !noreturn {
.memory_map = undefined, // filled by exitBootServices, just below .memory_map = undefined, // filled by exitBootServices, just below
.kernel_segments = undefined, // filled by loadKernel .kernel_segments = undefined, // filled by loadKernel
.kernel_segment_count = 0, .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); 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. /// kernel image and these buffers) lands there and stays reserved.
fn classify(d: *const uefi.tables.MemoryDescriptor) danos.MemoryKind { fn classify(d: *const uefi.tables.MemoryDescriptor) danos.MemoryKind {
if (!d.attribute.wb) return .mmio; if (!d.attribute.wb) return .mmio;
return switch (d.@"type") { return switch (d.type) {
.conventional_memory, .boot_services_code, .boot_services_data => .usable, .conventional_memory, .boot_services_code, .boot_services_data => .usable,
.acpi_reclaim_memory => .acpi_tables, .acpi_reclaim_memory => .acpi_tables,
.acpi_memory_nvs => .acpi_nvs, .acpi_memory_nvs => .acpi_nvs,
@@ -349,3 +353,19 @@ fn logBytes(bytes: []const u8) void {
buf[i] = 0; buf[i] = 0;
_ = out.outputString(buf[0..i :0].ptr) catch {}; _ = 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 paging = @import("paging.zig");
const serial = @import("serial.zig"); const serial = @import("serial.zig");
const apic = @import("apic.zig"); const apic = @import("apic.zig");
const io = @import("io.zig");
/// The saved register/trap frame passed to a fault handler. /// The saved register/trap frame passed to a fault handler.
pub const CpuState = idt.CpuState; pub const CpuState = idt.CpuState;
@@ -177,6 +178,28 @@ pub fn vectorName(vector: u64) []const u8 {
return idt.vectorName(vector); 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). /// CR2 holds the faulting linear address after a page fault (#PF, vector 14).
pub fn readCr2() u64 { pub fn readCr2() u64 {
return asm volatile ("mov %%cr2, %[out]" 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). /// Read a model-specific register (returns edx:eax combined).
pub fn rdmsr(msr: u32) u64 { pub fn rdmsr(msr: u32) u64 {
var low: u32 = undefined; var low: u32 = undefined;
+33
View File
@@ -5,6 +5,7 @@ const console = @import("console.zig");
const pmm = @import("pmm.zig"); const pmm = @import("pmm.zig");
const heap = @import("heap.zig"); const heap = @import("heap.zig");
const scheduler = @import("scheduler.zig"); const scheduler = @import("scheduler.zig");
const platform = @import("platform");
const tests = @import("tests.zig"); const tests = @import("tests.zig");
const build_options = @import("build_options"); const build_options = @import("build_options");
const BootInfo = danos.BootInfo; const BootInfo = danos.BootInfo;
@@ -102,6 +103,38 @@ fn kmain(boot_info: *const BootInfo) noreturn {
const s2 = pmm.stats(); const s2 = pmm.stats();
serial0.debugPrint(" Kernel footprint: {d} KiB\n", .{kib(s1.free_frames - s2.free_frames)}); 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. // Register the current context as the first task before enabling preemption.
scheduler.init(4); scheduler.init(4);
serial0.debugWrite("\ndanos: scheduler online\n"); serial0.debugWrite("\ndanos: scheduler online\n");
+29
View File
@@ -12,6 +12,7 @@
const std = @import("std"); const std = @import("std");
const danos = @import("danos"); const danos = @import("danos");
const arch = @import("arch"); const arch = @import("arch");
const platform = @import("platform");
const pmm = @import("pmm.zig"); const pmm = @import("pmm.zig");
const heap = @import("heap.zig"); const heap = @import("heap.zig");
const sched = @import("scheduler.zig"); const sched = @import("scheduler.zig");
@@ -77,11 +78,39 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
faultNoExecute(); faultNoExecute();
} else if (eql(case, "fault-null")) { } else if (eql(case, "fault-null")) {
faultNull(); faultNull();
} else if (eql(case, "poweroff")) {
powerTest(.off);
} else if (eql(case, "reboot")) {
powerTest(.reboot);
} else { } else {
log("DANOS-TEST-RESULT: FAIL (unknown case '{s}')\n", .{case}); 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; const BootInfo = danos.BootInfo;
fn eql(a: []const u8, b: []const u8) bool { 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). /// The kernel's own PT_LOAD segments (it has three: text, rodata, data).
kernel_segments: [8]KernelSegment, kernel_segments: [8]KernelSegment,
kernel_segment_count: u32, 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,
}; };