Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a53c2b0193 | ||
|
|
bf481c080c | ||
|
|
3a78dcab3f | ||
|
|
470f93a83d | ||
|
|
7798706b41 | ||
|
|
ad40de03c2 |
@@ -343,6 +343,20 @@ pub fn build(b: *std.Build) void {
|
|||||||
// what the driver-restart scenario drives the crash-loop cap with.
|
// what the driver-restart scenario drives the crash-loop cap with.
|
||||||
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
||||||
const device_list_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig");
|
const device_list_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig");
|
||||||
|
// The discovery service: one swappable process per firmware
|
||||||
|
// (docs/m19-m20-plan.md decision 7), bundled under the neutral ramdisk name
|
||||||
|
// "discovery" so the device manager never learns which firmware it is on.
|
||||||
|
// x86 boots describe hardware with ACPI; the Raspberry Pis hand over a
|
||||||
|
// flattened device tree — the aarch64 target flips the default when it
|
||||||
|
// lands (docs/arm.md). Both are placeholders until M20.1 (acpi) and the
|
||||||
|
// ARM bring-up (fdt).
|
||||||
|
const Discovery = enum { acpi, fdt };
|
||||||
|
const discovery = b.option(Discovery, "discovery", "Which discovery service fills the ramdisk's 'discovery' slot (default: acpi)") orelse Discovery.acpi;
|
||||||
|
const discovery_source: []const u8 = switch (discovery) {
|
||||||
|
.acpi => "system/services/acpi/acpi.zig",
|
||||||
|
.fdt => "system/services/fdt/fdt.zig",
|
||||||
|
};
|
||||||
|
const discovery_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source);
|
||||||
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
||||||
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
||||||
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
||||||
@@ -378,6 +392,8 @@ pub fn build(b: *std.Build) void {
|
|||||||
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
||||||
mk_run.addArg("device-list");
|
mk_run.addArg("device-list");
|
||||||
mk_run.addFileArg(device_list_exe.getEmittedBin());
|
mk_run.addFileArg(device_list_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("discovery");
|
||||||
|
mk_run.addFileArg(discovery_exe.getEmittedBin());
|
||||||
mk_run.addArg("device-manager");
|
mk_run.addArg("device-manager");
|
||||||
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
||||||
mk_run.addArg("input");
|
mk_run.addArg("input");
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ only when its definition of green holds.
|
|||||||
IPC MESSAGE_MAXIMUM (256); the startUserTask debug print removed — it
|
IPC MESSAGE_MAXIMUM (256); the startUserTask debug print removed — it
|
||||||
sheared concurrent serial lines and was the scenario-flake root cause;
|
sheared concurrent serial lines and was the scenario-flake root cause;
|
||||||
`device-list` scenario; suite 54/54)
|
`device-list` scenario; suite 54/54)
|
||||||
- [ ] **merge** `feat/usb-xhci-bus` → main, push — **loop ends here**
|
- [x] **merge** `feat/usb-xhci-bus` → main, push (merged 2026-07-13) — **plan complete**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# M19–M20 execution plan: discovery migration
|
||||||
|
|
||||||
|
The operational plan for [device-manager.md](device-manager.md)'s increment 8:
|
||||||
|
discovery leaves the kernel — a **pci-bus driver** (M19) and an **acpi service**
|
||||||
|
(M20), with the kernel's device enumeration retired behind them. Same rules as
|
||||||
|
[m17-m18-plan.md](m17-m18-plan.md): one phase at a time, each green before the
|
||||||
|
next; this file is the build order and the checklist.
|
||||||
|
|
||||||
|
**Definition of green, every phase:** `zig build` clean, `zig build test` clean,
|
||||||
|
`python3 test/qemu_test.py` passes (existing scenarios plus the phase's new
|
||||||
|
one), and the relevant design doc updated. Commit per green phase (no co-author
|
||||||
|
trailers). The full suite is the regression net — the existing
|
||||||
|
`driver-restart` / `usb-report` / `device-list` / `input` scenarios must stay
|
||||||
|
green *through* the migration, which is the whole point: the system must not be
|
||||||
|
able to tell who enumerated it.
|
||||||
|
|
||||||
|
**Workflow:** dedicated worktree; branches off `main` — `feat/pci-bus`
|
||||||
|
(M19.0–19.3), `feat/acpi-service` (M20.1–20.3); auto-merge to main when a
|
||||||
|
branch is green; keep branches; push everything.
|
||||||
|
|
||||||
|
## Settled decisions (2026-07-13 — veto before the loop starts)
|
||||||
|
|
||||||
|
1. **What "retiring the kernel scan" means.** The kernel keeps, forever, the
|
||||||
|
parses it needs before user space exists: RSDP/XSDT location, MADT (SMP),
|
||||||
|
the HPET table (the tick), FADT + the AML `\_S5` evaluation (poweroff — the
|
||||||
|
power tests prove it), and MCFG (the host bridge node). What retires is
|
||||||
|
**device enumeration**: the ECAM function walk (M19.3) and the DSDT/SSDT
|
||||||
|
namespace walk that builds device nodes (M20.3). The AML module stays a
|
||||||
|
shared build module compiled into both the kernel (for `\_S5`) and the acpi
|
||||||
|
service (for everything else) — same source, two builds, no fork.
|
||||||
|
2. **Bridge apertures come from the firmware memory map, not AML.** Registered
|
||||||
|
PCI functions carry BAR resources, and containment demands the bridge own
|
||||||
|
windows that cover them. The apertures are derived kernel-side from the
|
||||||
|
boot memory map's MMIO holes (regions that are neither RAM nor tables) —
|
||||||
|
mechanical, AML-free, and available at boot regardless of what later moved
|
||||||
|
to user space. (The bridge today carries only ECAM + bus range; this is the
|
||||||
|
prerequisite M19.0 exists for.)
|
||||||
|
3. **`device_register` becomes idempotent on exact match.** A re-registration
|
||||||
|
with identical (parent, class, resources) returns the existing id instead
|
||||||
|
of appending. The kernel table has no unregister, so without this a
|
||||||
|
restarted registering bus would duplicate its children on every respawn —
|
||||||
|
idempotence makes restart-and-re-report safe for every future bus, not just
|
||||||
|
PCI.
|
||||||
|
4. **The manager matches from reports.** `ChildAdded` gains a `device_id`
|
||||||
|
field (the kernel-registered id, `no_device` for unregistered leaves like
|
||||||
|
USB ports). After the M19.3 flip, PCI driver matching keys off reported
|
||||||
|
identity (the class triple) instead of the manager's boot-time snapshot —
|
||||||
|
the snapshot match remains only for what the kernel still seeds. One flip
|
||||||
|
phase changes both sides at once so no device is ever matched twice.
|
||||||
|
5. **The acpi service's authority is one node.** The kernel publishes an
|
||||||
|
`acpi-tables` device: memory resources covering the table blobs plus a
|
||||||
|
broad `io_port` resource — the documented trust grant to exactly one
|
||||||
|
process (AML OperationRegions reach EC/PM ports; the claim-gated
|
||||||
|
io_read/io_write calls already exist). The service claims it, maps the
|
||||||
|
tables, and runs the shared AML module in ring 3 behind a `Hal` backed by
|
||||||
|
`mmio_map` + `io_read`/`io_write`.
|
||||||
|
6. **Both new processes are protocol drivers** under the manager: hello,
|
||||||
|
supervision, restart with backoff — all inherited from M18.1 for free.
|
||||||
|
Registration idempotence (decision 3) is what makes their restarts sound.
|
||||||
|
7. **Firmware neutrality is the contract** (2026-07-13). The generic layer is
|
||||||
|
everything at and above the device-manager protocol — descriptors,
|
||||||
|
containment, reports, matching, supervision — and none of it may become
|
||||||
|
x86-specific. Discovery is one swappable process per firmware: the acpi
|
||||||
|
service on x86; an **fdt service** on the Raspberry Pis (claims a
|
||||||
|
`devicetree-blob` node, reports children from the flattened device tree —
|
||||||
|
pure data, no bytecode, no port grant, strictly simpler than ACPI). The
|
||||||
|
manager owns the tree as *data* and touches no hardware, ever — AML runs in
|
||||||
|
a crashable, supervised discoverer precisely so a firmware-bytecode fault
|
||||||
|
can never take down the supervisor. Two consequences recorded now:
|
||||||
|
`DeviceDescriptor`'s 8-byte `hid` cannot hold an FDT `compatible` string
|
||||||
|
("brcm,bcm2835-aux-uart") — identity widens before the fdt service exists;
|
||||||
|
and cross-firmware surfaces are named by **domain, not firmware** (M21
|
||||||
|
defines a *power* protocol, not an "ACPI events" protocol — PSCI/mailbox
|
||||||
|
sources feed the same subscribers on ARM). **Landed early (2026-07-13):**
|
||||||
|
both services exist as placeholders (system/services/acpi, system/services/
|
||||||
|
fdt) and the build's `-Ddiscovery=acpi|fdt` option fills the ramdisk's
|
||||||
|
neutral `discovery` slot — the manager will spawn "discovery" by that name
|
||||||
|
in M20.3 and never learn which firmware it is on.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- [ ] **M19.0** — prerequisites on `feat/pci-bus`: bridge MMIO apertures from
|
||||||
|
the memory-map holes; `device_register` idempotence (+ kernel unit
|
||||||
|
checks); `ChildAdded.device_id`; archive note on m17-m18-plan.md.
|
||||||
|
- [ ] **M19.1** — pci-bus driver, scan only: claim the host bridge, map the
|
||||||
|
ECAM window, walk bus/device/function headers, log what it finds.
|
||||||
|
Scenario `pci-scan`: the kernel test compares the driver's reported count
|
||||||
|
against the broker table's `pci_device` count — equivalence, per class.
|
||||||
|
- [ ] **M19.2** — register + report: each function registered under the bridge
|
||||||
|
(config-space slice + BARs, `pci_class` in the descriptor), reported with
|
||||||
|
`child_added { device_id, identity = class triple }`. Manager mirrors;
|
||||||
|
spawn-from-reports stays **off**. Scenario extends `pci-scan`:
|
||||||
|
registered ids resolve, no duplicates after a forced driver restart
|
||||||
|
(idempotence proven end to end).
|
||||||
|
- [ ] **M19.3** — the flip: kernel `enumeratePci` call removed (bridge node
|
||||||
|
stays); manager matches PCI drivers from reports. One commit. The
|
||||||
|
existing xHCI scenarios (`driver-restart`, `usb-report`, `device-list`)
|
||||||
|
are the assertion — xhci must come up spawned off a pci-bus report, and
|
||||||
|
the suite must not be able to tell the difference. discovery.md updated.
|
||||||
|
- [ ] **merge** `feat/pci-bus` → main, push.
|
||||||
|
- [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at
|
||||||
|
system/services/acpi/acpi.zig): kernel publishes `acpi-tables`
|
||||||
|
(decision 5) — memory over the table blobs, the broad io_port grant, and
|
||||||
|
**the SCI as an irq resource** (from the FADT; unused until M21 but free
|
||||||
|
to record now). The service claims it, maps the blobs, runs the shared
|
||||||
|
AML module in ring 3, logs the namespace device count and `_HID`s.
|
||||||
|
Scenario `acpi-parse`: user-space count equals the kernel walk's count.
|
||||||
|
- [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS`
|
||||||
|
resources registered under `acpi-tables` (its io_port + the memory-map
|
||||||
|
holes give containment), reported to the manager. Spawn-from-reports for
|
||||||
|
ACPI matches stays off. Scenario: the reported set includes the PS/2
|
||||||
|
keyboard and mouse nodes with their IRQ resources.
|
||||||
|
- [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static
|
||||||
|
tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers
|
||||||
|
(ps2-bus) from reports. The `input` and `device-manager` scenarios are
|
||||||
|
the assertion. discovery.md + acpi.md + device-manager.md updated;
|
||||||
|
device-manager.md increment 8 closed.
|
||||||
|
- [ ] **merge** `feat/acpi-service` → main, push — **loop ends here**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase notes
|
||||||
|
|
||||||
|
**M19.0 apertures:** the boot memory map already crosses the handoff
|
||||||
|
([boot-handoff]); the holes computation belongs where the bridge node is built
|
||||||
|
(`parseMcfg`). Sanity-check on QEMU q35: the xHCI BAR (`0xc0000000`-region
|
||||||
|
values seen in the M18 logs) must land inside a derived aperture, asserted in
|
||||||
|
the kernel unit test.
|
||||||
|
|
||||||
|
**M19.1 scanning without owning config access twice:** the driver reads config
|
||||||
|
space through its ECAM mmio_map grant of the *bridge* window — the same bytes
|
||||||
|
the kernel walk read. Vendor-id `0xFFFF` skip, header-type multifunction rule,
|
||||||
|
no bridge recursion (matches the kernel's current single-segment walk).
|
||||||
|
|
||||||
|
**M19.2 BAR sizing:** the classic size probe (write all-ones, read mask,
|
||||||
|
restore) is deferred — the BARs' current programmed values and types are
|
||||||
|
enough for containment-checked registration at bring-up; sizing lands with the
|
||||||
|
first driver that needs to *move* a BAR. Log what is registered so the
|
||||||
|
scenario can assert it.
|
||||||
|
|
||||||
|
**M19.3 what the manager still seeds from the snapshot:** everything the
|
||||||
|
kernel still enumerates (timers, ACPI nodes until M20.3). The PCI arm of
|
||||||
|
`pciDriverFor` switches source; `driverFor` doesn't move until M20.3.
|
||||||
|
|
||||||
|
**M20.1 Hal in ring 3:** `mapMmio` → `device.mmioMap` over the claimed
|
||||||
|
acpi-tables node (plus a table-offset map for blobs); `pioRead`/`pioWrite` →
|
||||||
|
`device.ioRead`/`ioWrite` against its io_port resource. The interpreter cannot
|
||||||
|
tell it moved — that is the assertion of `acpi-parse`.
|
||||||
|
|
||||||
|
**M20.2 containment for `_CRS`:** io ports fall inside the node's broad
|
||||||
|
io_port resource; MMIO windows (HPET, LAPIC ranges some firmwares list) fall
|
||||||
|
inside the memory-map holes added to the node in M20.1. Anything that doesn't
|
||||||
|
fit is logged and skipped, loudly — bring-up honesty over silent drops.
|
||||||
|
|
||||||
|
**M20.3 ps2 ordering:** ps2-bus binds nodes the acpi service now reports, so
|
||||||
|
its spawn moves behind the report (the manager's matching handles this once
|
||||||
|
the source flips); the `input` scenario proves the keyboard still types.
|
||||||
|
|
||||||
|
**Explicitly out of scope:** PCI bridge recursion (single segment, flat bus
|
||||||
|
walk stays); BAR reprogramming/sizing; disk/PCIe hotplug; interrupt routing
|
||||||
|
changes (`_PRT` stays wherever it is today); the USB descriptor track;
|
||||||
|
multi-segment ECAM; per-device power states (D-states, `_PSx`/`_PRx`,
|
||||||
|
suspend/resume — a future *lifecycle-vocabulary* extension, since "suspend"
|
||||||
|
has the shape of a signal every driver must answer, and it has no consumer
|
||||||
|
until laptop sleep); CPU P/C-states.
|
||||||
|
|
||||||
|
## M21 preview — ACPI events + system power (planned next, not in this loop)
|
||||||
|
|
||||||
|
The acpi service grows the event side (settled direction 2026-07-13; detailed
|
||||||
|
phases when M20 lands):
|
||||||
|
|
||||||
|
- **21.1 SCI + fixed events**: irq_bind the SCI (the resource M20.1 already
|
||||||
|
records), read/clear PM1 status, publish the power-button event to
|
||||||
|
subscribers (the same pub/sub shape the manager uses).
|
||||||
|
- **21.2 GPE + Notify**: Notify dispatch in the shared AML interpreter, GPE
|
||||||
|
block handling, `Notify(device, code)` published per reported node. The
|
||||||
|
acpi service is a **bus** here: battery (PNP0C0A), AC (ACPI0003), and lid
|
||||||
|
(PNP0C0D) nodes are reported children; small class drivers bind them and
|
||||||
|
speak an evaluate/subscribe protocol to the service — the xHCI split,
|
||||||
|
repeated. The embedded controller (`_Qxx` queries) rides this phase;
|
||||||
|
QEMU emulates no battery/EC, so those paths are interface-complete and
|
||||||
|
validated on real hardware (the laptop is the win condition), while the
|
||||||
|
plumbing is proven by the power button.
|
||||||
|
- **21.3 the capstone**: QEMU `system_powerdown` → acpi service event → init
|
||||||
|
runs the M17 stop sequence over its children → kernel `\_S5` — orderly
|
||||||
|
shutdown as the scenario that proves lifecycle + events compose. (The
|
||||||
|
harness grows a QMP poke to inject the event.)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! /system/services/acpi — the ACPI discovery service: the x86 firmware
|
||||||
|
//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). **Placeholder:
|
||||||
|
//! not implemented until M20.1** — it exists so the build's `-Ddiscovery`
|
||||||
|
//! option has both of its values and the ramdisk's neutral `discovery` slot is
|
||||||
|
//! wired before the implementation lands.
|
||||||
|
//!
|
||||||
|
//! What it becomes (the plan's decisions 5 and 7): claim the `acpi-tables`
|
||||||
|
//! node the kernel publishes (table blobs + the broad io_port grant + the SCI),
|
||||||
|
//! map the tables, and run the **shared AML module** in ring 3 behind a `Hal`
|
||||||
|
//! backed by `mmio_map` and `io_read`/`io_write` — the interpreter cannot tell
|
||||||
|
//! it moved. Then the bus-driver shape: `device_register` the namespace
|
||||||
|
//! devices (`_HID`, `_CRS` resources, containment against the node's
|
||||||
|
//! apertures), report each to the device manager, stay resident under its
|
||||||
|
//! supervision. M21 grows the event side on the same claim: the SCI, PM1 fixed
|
||||||
|
//! events, GPEs, Notify — published through the domain-named power protocol,
|
||||||
|
//! never an "ACPI events" protocol.
|
||||||
|
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
|
||||||
|
pub fn main() void {
|
||||||
|
// Not implemented: exit cleanly and silently (a bare spawn by the
|
||||||
|
// initial-ramdisk sweep must not derange other tests' markers). The
|
||||||
|
// supervisor reads a clean exit as "meant to stop" — correct for a
|
||||||
|
// placeholder.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//! /system/services/fdt — the devicetree discovery service: the ARM twin of the
|
||||||
|
//! acpi service (docs/m19-m20-plan.md decision 7). **Placeholder: not
|
||||||
|
//! implemented.** It exists so the build's `-Ddiscovery` option has both of its
|
||||||
|
//! values from day one; the implementation lands with the Raspberry Pi
|
||||||
|
//! bring-up (docs/arm.md).
|
||||||
|
//!
|
||||||
|
//! What it becomes: the per-firmware discoverer for boots that hand over a
|
||||||
|
//! flattened device tree instead of ACPI tables. It claims the
|
||||||
|
//! `devicetree-blob` node the kernel publishes (the FDT the loader received),
|
||||||
|
//! walks the tree — pure data, no bytecode, so unlike the acpi service it
|
||||||
|
//! needs no port grant and no interpreter — and, like any bus-shaped driver:
|
||||||
|
//! `device_register`s what it finds (containment against the blob node's
|
||||||
|
//! recorded apertures), reports each child to the device manager
|
||||||
|
//! (`child_added`, identity = the node's `compatible` string), and stays
|
||||||
|
//! resident under the manager's supervision (hello, restart, the usual
|
||||||
|
//! contract).
|
||||||
|
//!
|
||||||
|
//! Known prerequisite recorded in the plan: `DeviceDescriptor`'s 8-byte `hid`
|
||||||
|
//! cannot hold an FDT `compatible` string ("brcm,bcm2835-aux-uart") — identity
|
||||||
|
//! widens before this file grows a body.
|
||||||
|
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
|
||||||
|
pub fn main() void {
|
||||||
|
// Not implemented: exit cleanly and silently (a bare spawn by the
|
||||||
|
// initial-ramdisk sweep must not derange other tests' markers). The
|
||||||
|
// supervisor reads a clean exit as "meant to stop" — correct for a
|
||||||
|
// placeholder.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user