168 lines
8.2 KiB
Markdown
168 lines
8.2 KiB
Markdown
# Device discovery (ACPI / device tree), the agnostic way
|
|
|
|
"Device discovery" is how the kernel learns **what hardware exists and where** — the
|
|
MMIO addresses, IRQ numbers, CPU count, and interrupt controller it can't just
|
|
assume. On x86 that description comes from **ACPI** tables; on ARM from a **device
|
|
tree** (DTB). This note is a design plan, not built yet: *when* danos should tackle
|
|
it, and *how* to keep it architecture-agnostic — the same discipline the
|
|
[memory map](memory-map.md) and [arch split](arch.md) already follow.
|
|
|
|
## What the kernel assumes today
|
|
|
|
Right now danos discovers almost nothing — it coasts on legacy PC fixtures that are
|
|
guaranteed to exist under QEMU + UEFI:
|
|
|
|
- `arch/x86_64/apic.zig` assumes the **Local APIC** at the default `0xFEE0_0000` and
|
|
calibrates its timer against the **PIT** (the legacy 8254).
|
|
- `arch/x86_64/serial.zig` hardcodes **COM1** at I/O port `0x3F8`.
|
|
- The framebuffer and memory map come from **UEFI** — that *is* discovery, just done
|
|
by the firmware and handed over, not read from ACPI.
|
|
|
|
This works only because PC-compatible hardware promises those legacy pieces exist at
|
|
those addresses. It is a crutch, and it does not travel.
|
|
|
|
## The forcing functions: when to build it
|
|
|
|
Two things drive the need, and they set the timing:
|
|
|
|
1. **The second architecture makes it mandatory.** ARM has *no* legacy fixtures —
|
|
no PIT, no fixed serial port, no standard interrupt controller address. You can't
|
|
find the UART to print a character without reading the device tree. So on x86 we
|
|
can defer discovery a long time (until we want the IOAPIC, PCIe, or SMP), but on
|
|
**aarch64 it's required to boot at all**. The [aarch64 port](arm.md) is what
|
|
forces the issue.
|
|
|
|
2. **Isolated user-space drivers need it.** In the [microkernel vision](vision.md),
|
|
drivers live in user space — but something has to enumerate the hardware and hand
|
|
each driver its MMIO regions and IRQs. That enumeration *is* device discovery. So
|
|
discovery is a prerequisite for real drivers, **not** for user mode itself.
|
|
|
|
The conclusion on timing: **don't build full discovery before user mode.** User mode
|
|
+ address-space isolation needs none of it; the current assumptions are fine there.
|
|
Build the agnostic discovery layer **when the aarch64 port starts** — because that's
|
|
when a second, real implementation makes "agnostic" honest.
|
|
|
|
## Why build it *with* the second arch, not before
|
|
|
|
The same lesson as the arch split: an abstraction with only one implementation
|
|
quietly bends to that implementation. Build "agnostic discovery" x86-only first and
|
|
you'll get an ACPI-shaped interface with device tree bolted on afterward. Build it
|
|
when aarch64 lands and the small, clean DTB parser pulls the abstraction toward the
|
|
right neutral shape, which the x86 side then fills. Design it against two backends or
|
|
it isn't really agnostic.
|
|
|
|
## The one cheap step to take sooner
|
|
|
|
Have the **loader capture the description pointer** into `BootInfo` — a neutral
|
|
handle, no parsing:
|
|
|
|
```zig
|
|
pub const HardwareInfo = extern struct {
|
|
kind: enum(u32) { none, acpi, device_tree },
|
|
addr: u64, // ACPI RSDP, or the DTB blob
|
|
};
|
|
```
|
|
|
|
On x86-UEFI that's the ACPI **RSDP**, read from the UEFI configuration table *before*
|
|
`ExitBootServices` — the same "grab it before exit" pattern as the framebuffer and
|
|
memory map (already flagged in [memory-map.md](memory-map.md)). On ARM it's the DTB
|
|
pointer the firmware passes. This keeps the door open for near-zero cost without
|
|
committing to the parser.
|
|
|
|
## What "agnostic" looks like
|
|
|
|
The happy accident: **the device-tree data model is already a good neutral
|
|
representation.** A DTB is a tree of nodes, each with:
|
|
|
|
- a **`compatible`** string — what the device is,
|
|
- **`reg`** — its MMIO base(s) and size(s),
|
|
- **`interrupts`** — its IRQ number(s),
|
|
- other properties.
|
|
|
|
Even OSes running on ACPI hardware normalize into a unified device model shaped like
|
|
this. So the neutral layer is a **device model** — "here are the devices, each with a
|
|
type, MMIO regions, and IRQs" — fed by two backends behind it:
|
|
|
|
- a **DTB parser** (ARM) — a few hundred lines against a well-specified binary format;
|
|
- **static ACPI + PCI enumeration** (x86).
|
|
|
|
### The caveat that sets the effort: "ACPI" ≠ "AML interpreter"
|
|
|
|
A full ACPI namespace is **AML** (ACPI Machine Language) bytecode, and writing an AML
|
|
interpreter is an enormous undertaking. **We skip it.** Interrupt routing and PCIe
|
|
come entirely from the *static* tables:
|
|
|
|
- **MADT** — the APICs and the **IOAPIC** (interrupt routing),
|
|
- **MCFG** — PCIe configuration space (then walk the PCI bus to enumerate devices),
|
|
- **FADT**, **HPET** — power/reset and a precise timer.
|
|
|
|
Walking PCI config space finds most devices without any AML. So the x86 static-ACPI
|
|
backend is roughly comparable in scope to the DTB parser; it's full AML that's the
|
|
monster, and it isn't on the path.
|
|
|
|
## Where it sits in the seam
|
|
|
|
Discovery layers onto the existing loader↔kernel split cleanly:
|
|
|
|
| Layer | Responsibility | x86 | ARM |
|
|
|-------|----------------|-----|-----|
|
|
| **Capture** (loader) | grab the description pointer | RSDP from UEFI config table | DTB pointer from firmware |
|
|
| **Parse** (kernel, per-mechanism) | pointer → neutral device model | static ACPI + PCI | DTB parser |
|
|
| **Consume** (generic) | use the device model | — same code — | — same code — |
|
|
|
|
Boot-protocol-specific capture, mechanism-specific parse, generic consumption —
|
|
exactly like the memory map, where the loader classifies and the kernel just sees
|
|
neutral regions.
|
|
|
|
## Where it lives in a microkernel
|
|
|
|
For isolated drivers, discovery is not one lump — it splits by *who needs it and
|
|
when*:
|
|
|
|
- **Minimal, in-kernel: the interrupt controller and the timer.** The IOAPIC/GIC and
|
|
the timer are needed *before* user space exists (scheduling and preemption depend
|
|
on them), so the kernel must parse at least these from ACPI/DTB itself. This is also
|
|
the **first real consumer** of discovery — the first genuine reason to read MADT or
|
|
the DTB is "where is the interrupt controller and how do I route an IRQ?" It's the
|
|
point where x86 finally graduates from the PIT/legacy-LAPIC assumptions.
|
|
- **User-space enumeration: a device-manager server.** Everything else — PCI devices,
|
|
peripherals — is parsed (or queried from the kernel's parse) by a privileged
|
|
user-space server that hands each driver process its MMIO regions and IRQ rights
|
|
over [IPC](ipc.md). Combined with **interrupts-as-messages** (an IRQ delivered to a
|
|
driver as a message on a channel — a natural extension of the wait queues and
|
|
channels already built), that's what makes drivers genuinely isolated.
|
|
|
|
So the device-manager server depends on user mode + IPC; only the interrupt-controller
|
|
slice is unavoidably in-kernel.
|
|
|
|
## A nice ARM contrast
|
|
|
|
On ARMv8 the generic timer exposes its frequency directly via the `CNTFRQ` register —
|
|
no calibration needed. That's cleaner than the x86 side, where we measure the LAPIC
|
|
and TSC against the PIT because nothing tells us their frequency (see
|
|
[device-interrupts.md](device-interrupts.md)). Discovery on ARM hands you more for
|
|
free; discovery on x86 is partly about *finding* what ARM just tells you.
|
|
|
|
## Suggested ordering
|
|
|
|
1. **Now (cheap):** plumb the neutral `HardwareInfo` pointer through the loader into
|
|
`BootInfo`. No parser yet.
|
|
2. **Next milestone unchanged:** user mode + address-space isolation — needs no
|
|
discovery.
|
|
3. **With the aarch64 port:** build the agnostic discovery layer, **DTB first** (the
|
|
forcing function), then x86 static-ACPI to fill the same model — starting with the
|
|
**interrupt controller + timer**.
|
|
4. **Then:** a user-space device-manager server + interrupts-as-messages → real
|
|
isolated drivers (keyboard first).
|
|
|
|
## Related
|
|
|
|
- [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,
|
|
and the note about grabbing the RSDP before `ExitBootServices`.
|
|
- [device-interrupts.md](device-interrupts.md) — the LAPIC/timer bring-up that
|
|
discovery will eventually feed (IOAPIC, real IRQ routing).
|
|
- [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager
|
|
will ride on.
|
|
- [vision.md](vision.md) — why drivers belong in isolated user space at all.
|