9.3 KiB
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 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 loader↔kernel handoff) system/boot-handoff.zig
│ the kernel forwards the whole BootInfo
▼
platform.discover(boot_info, …) system/devices/platform.zig
│ reads boot_info.acpi_rsdp, hands it to the ACPI backend
▼
acpi.discover(rsdp_phys, …) system/devices/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 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 and
the memory map.
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 boot-handoff contract). So instead of a call, it
deposits a value in the handoff struct:
// 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
u64physical address is the only thing that survives the handoff.BootInfo.acpi_rsdpis0when 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 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 — 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:
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
revisionbyte selects the root table.root_system_description_table_address(32-bit, → RSDT) andextended_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 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.
ACPI events: the SCI, the power button, and GPEs (M21)
The tables above are static description; ACPI is also a live channel. Hardware raises the SCI (System Control Interrupt) — one shared, level-triggered line whose vector the FADT names — and the OS reads status registers to learn what happened: a fixed event like the power button, or a General-Purpose Event (GPE) whose handler is an AML method. Since discovery moved AML to ring 3, the event side lives there too, in the same acpi service — the device discoverer and the event source are one process, because both need the namespace and the port grant.
The kernel hands the service what it needs and no more. Reading PM1 event
blocks and GPE blocks requires the FADT, which the kernel already parses for its
own \_S5 poweroff. Rather than re-parse, the kernel appends the FADT as one
more memory resource on the acpi-tables node; the service tells it apart
from the AML blob resources by signature — the FADT keeps its intact "FACP"
header, while the blob resources are header-stripped bytecode that starts with
no signature. The kernel's own FADT parse is untouched; the service reads the
PM1 event blocks (which the kernel never parsed — it only needs PM1 control
for \_S5) and the GPE0/GPE1 blocks straight from its copy. The SCI itself
arrives as the node's one len == 1 irq resource (distinct from the broad
[0, 256) window that covers children's legacy lines), which is how the service
finds the line to irq_bind.
With those in hand the service enables ACPI mode (only if SCI_EN is clear —
some firmwares boot with it already set), sets PWRBTN_EN, and on each SCI:
- The power button is a fixed event: a set
PWRBTN_STSbit in PM1 status. The handler clears it (write-1-to-clear), logs the press, and publishes apowerpower_buttonevent to subscribers. - GPEs are the general path: for each set-and-enabled GPE bit
n, the service evaluates its\_GPE._L%02X(level) or_E%02X(edge) handler method, drains the Notify queue that method produced, maps each notified device to an event (battery, AC, lid, or a genericnotifywith its code), and clears the status bit. A missing handler method is clear-and-log, not an error. Making GPEs work required teaching the interpreter one opcode it never handled —Notify(0x86) — which it now folds into a bounded queue drained per evaluation; everything else a handler needs (field access, control flow, method calls) was already proven by the ring-3_STA/_CRSwork.
How this is tested. QEMU cannot raise GPEs deterministically on this config,
so GPE/Notify correctness is proven by host unit tests — hand-encoded AML
with a Notify inside a method body, run under zig build test. The QEMU
power-button scenario proves the fixed-event path end to end: a QMP
system_powerdown injects a real ACPI power-button press, and the service's SCI
handler must log it. Battery/AC/lid and the embedded controller's _Qxx queries
are interface-complete but validated on real hardware later.
The service surface these events are published on — subscription, the event vocabulary, and orderly shutdown — is the power service, power.md.
Related
- efi.md — the loader that captures the RSDP before
ExitBootServices. - memory-map.md — the same loader-captures / kernel-consumes seam, and the ACPI-reclaim memory the RSDP lives in.
- discovery.md — the broader (still-evolving) plan for turning these tables into one neutral device model shared with the ARM device-tree path, and how ACPI enumeration and events moved to the ring-3 acpi service.
- power.md — the domain-named power service the ACPI event side publishes to (button, lid, battery) and its orderly-shutdown path into S5.
- arch.md — why the kernel reaches the device code through a
platformmodule and never names ACPI directly.