danos/docs/acpi.md

173 lines
9.3 KiB
Markdown

# 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 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](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 `boot-handoff` contract). So instead of a call, it
deposits a value in the handoff struct:
```zig
// 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.
## 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](discovery.md) 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_STS` bit in PM1 status.
The handler clears it (write-1-to-clear), logs the press, and publishes a
[`power`](power.md) `power_button` event 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 generic `notify` with 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`/`_CRS` work.
**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](power.md).
## 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, and how
ACPI enumeration and events moved to the ring-3 acpi service.
- [power.md](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](arch.md) — why the kernel reaches the device code through a `platform`
module and never names ACPI directly.