Time is a kernel concern in danos: the kernel owns the scheduling timer and already exposes monotonic time via the clock/sleep/timer_bind syscalls, so a userspace time service would be a redundant, slower path. This adds the generic runtime.time module over those syscalls, retires the two demonstration drivers, reorganizes the milestone docs, and makes the monotonic clock correct on Intel, AMD, and inside any VM. runtime.time (library/runtime/time.zig) - Instant/Duration interface: now, sleep, spin, after, monotonicNanos, available - a thin layer over system.clock/sleep/timerOnce; unit-tested arithmetic Remove the demo drivers hpet and bus (a teaching example belongs in the docs, not shipped in the tree) - system/drivers/ now holds only real drivers: pci-bus, ps2-bus, usb-xhci-bus - device-manager end-to-end test repointed to pci-bus (asserts on kernel state: the process table and the device tree, not a racy serial marker) - device_register containment moved to a new in-kernel `containment` test - the driver-model worked example moved inline into docs/drivers.md Reorganize milestone docs into topic docs - m17-m18 / m19-m20 / m21 plans dissolved into process-lifecycle, device-manager, discovery, and acpi docs; new docs/power.md and docs/timers.md; ~20 citations repointed; plan docs deleted TSC reliability (apic.zig, smp.zig, cpu.zig, kernel.zig) - check the invariant-TSC bit (CPUID 0x80000007 EDX[8]) on Intel and AMD - cross-core "warp" check at SMP bring-up, pairwise BSP<->AP as each core comes up - fall back to the HPET clocksource when the TSC is not invariant (a bare VM) or not synchronized (a warp), switched continuously so time never jumps - boot log reports the outcome; new tsc-sync test exercises the TSC + warp path Verified: zig build; zig build test; 60/60 QEMU cases (incl. new containment and tsc-sync).
173 lines
9.3 KiB
Markdown
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.
|