Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
446f655c69 | ||
|
|
a785efa4a3 | ||
|
|
767a2a9a7c | ||
|
|
1f2c60b3ec | ||
|
|
dfc7d6a609 | ||
|
|
738f6aa697 | ||
|
|
01e56e3f36 | ||
|
|
d5d15cefcb | ||
|
|
fd96a35eb9 | ||
|
|
e3fe3f3f45 | ||
|
|
60da667b42 | ||
|
|
36145e623b | ||
|
|
565415327d | ||
|
|
bf6bdb389d | ||
|
|
0628944b15 | ||
|
|
e6d0bb7ef0 | ||
|
|
5ca804d827 | ||
|
|
a299363b59 | ||
|
|
d8dd62c639 | ||
|
|
d106b6e8dc | ||
|
|
af2c766f42 | ||
|
|
d26262bf56 | ||
|
|
10b89c06ff | ||
|
|
a2a05d0b3d |
@@ -131,6 +131,13 @@ pub fn build(b: *std.Build) void {
|
|||||||
});
|
});
|
||||||
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
|
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
|
||||||
// nodes. Also shared reference data.
|
// nodes. Also shared reference data.
|
||||||
|
// The AML interpreter, a build module so the ring-3 acpi service can run the
|
||||||
|
// same parser the kernel does (docs/m19-m20-plan.md decision 1). Pure Zig,
|
||||||
|
// no kernel imports — one source, two builds.
|
||||||
|
const aml_module = b.addModule("aml", .{
|
||||||
|
.root_source_file = b.path("system/devices/aml/aml.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
const acpi_ids_module = b.addModule("acpi-ids", .{
|
const acpi_ids_module = b.addModule("acpi-ids", .{
|
||||||
.root_source_file = b.path("system/devices/acpi-ids.zig"),
|
.root_source_file = b.path("system/devices/acpi-ids.zig"),
|
||||||
});
|
});
|
||||||
@@ -219,6 +226,12 @@ pub fn build(b: *std.Build) void {
|
|||||||
});
|
});
|
||||||
runtime_module.addImport("device-manager-protocol", device_manager_protocol_module);
|
runtime_module.addImport("device-manager-protocol", device_manager_protocol_module);
|
||||||
|
|
||||||
|
// The power protocol: system power's domain-named surface (docs/m21-plan.md).
|
||||||
|
const power_protocol_module = b.addModule("power-protocol", .{
|
||||||
|
.root_source_file = b.path("system/services/power/protocol.zig"),
|
||||||
|
});
|
||||||
|
runtime_module.addImport("power-protocol", power_protocol_module);
|
||||||
|
|
||||||
// Typed volatile MMIO register access + memory-ordering barriers, for drivers on
|
// Typed volatile MMIO register access + memory-ordering barriers, for drivers on
|
||||||
// top of an mmio_map grant. Depends only on `builtin` (arch-conditional barriers);
|
// top of an mmio_map grant. Depends only on `builtin` (arch-conditional barriers);
|
||||||
// no target set, so it inherits each driver's. See library/mmio/mmio.zig.
|
// no target set, so it inherits each driver's. See library/mmio/mmio.zig.
|
||||||
@@ -339,6 +352,10 @@ pub fn build(b: *std.Build) void {
|
|||||||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||||
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
||||||
const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig");
|
const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig");
|
||||||
|
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
||||||
|
// The PCI bus driver decodes each function's class triple to human names in its
|
||||||
|
// boot log (class/subclass/prog-IF), so pull in the shared pci-class reference.
|
||||||
|
pci_bus_exe.root_module.addImport("pci-class", pci_class_module);
|
||||||
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
||||||
// 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");
|
||||||
@@ -357,7 +374,10 @@ pub fn build(b: *std.Build) void {
|
|||||||
.fdt => "system/services/fdt/fdt.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 discovery_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source);
|
||||||
|
if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module);
|
||||||
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");
|
||||||
|
// Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330.
|
||||||
|
device_manager_exe.root_module.addImport("pci-class", pci_class_module);
|
||||||
// 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.
|
||||||
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig");
|
const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig");
|
||||||
@@ -388,6 +408,8 @@ pub fn build(b: *std.Build) void {
|
|||||||
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
||||||
mk_run.addArg("usb-xhci-bus");
|
mk_run.addArg("usb-xhci-bus");
|
||||||
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
|
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("pci-bus");
|
||||||
|
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
||||||
mk_run.addArg("crash-test");
|
mk_run.addArg("crash-test");
|
||||||
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
||||||
mk_run.addArg("device-list");
|
mk_run.addArg("device-list");
|
||||||
@@ -561,6 +583,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
"system/devices/device-abi.zig",
|
"system/devices/device-abi.zig",
|
||||||
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
|
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
|
||||||
"system/devices/acpi-ids.zig", // _HID name decoding
|
"system/devices/acpi-ids.zig", // _HID name decoding
|
||||||
|
"system/devices/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch (M21)
|
||||||
"system/devices/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
|
"system/devices/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
|
||||||
"system/devices/usb-ids.zig", // class/subclass/protocol code assignments
|
"system/devices/usb-ids.zig", // class/subclass/protocol code assignments
|
||||||
"library/mmio/mmio.zig", // barriers assemble + registers round-trip
|
"library/mmio/mmio.zig", // barriers assemble + registers round-trip
|
||||||
|
|||||||
@@ -142,6 +142,32 @@ conventions above — `snake_case` — because it's an identifier, not a filenam
|
|||||||
*directory* (`system/services/init`, `library/runtime`), with the repeated leaf
|
*directory* (`system/services/init`, `library/runtime`), with the repeated leaf
|
||||||
resolving away. See the repository-layout section of [README.md](README.md).
|
resolving away. See the repository-layout section of [README.md](README.md).
|
||||||
|
|
||||||
|
## Named values, not magic numbers
|
||||||
|
|
||||||
|
The naming rule has a twin: **a value with meaning gets a name, too.** The same
|
||||||
|
principle drives both — a reader should never have to leave the code to understand it.
|
||||||
|
An abbreviated *name* forces a reader to guess; a bare *number* forces them worse, out
|
||||||
|
to a spec or a header or a comment three files away, to learn what the value even *is*.
|
||||||
|
If `0x0C` is the PCI serial-bus class, the code says `BaseClass.serial_bus`, not `0x0C`;
|
||||||
|
if `0x04` is the ACPI IRQ resource descriptor, it says `SmallResourceType.irq`, not
|
||||||
|
`0x04`. The number is an implementation detail of the name — recorded once, where the
|
||||||
|
name is defined, and never spelled again at a use site.
|
||||||
|
|
||||||
|
**Prefer an `enum`** when the values form a set (device classes, AML opcodes, resource
|
||||||
|
descriptor types, states): the type then also says *which* set a value belongs to, and
|
||||||
|
the compiler rejects a value from the wrong one. A lone `pub const` with a descriptive
|
||||||
|
name suffices for a one-off (`const large_descriptor_bit = 0x80`). Reach for the enum
|
||||||
|
the moment code elsewhere compares against, packs, or produces the value — a packed PCI
|
||||||
|
class triple is written from named parts (`.serial_bus`, `.usb`, `.xhci`), never as
|
||||||
|
`0x0C_03_30` under a comment that decodes the bytes.
|
||||||
|
|
||||||
|
The exceptions are the numbers that carry no hidden meaning: `0` and `1` as plain zero
|
||||||
|
and one, an index step, a field width, a bit shift. `x + 1`, `buffer[0]`, and `<< 8`
|
||||||
|
need no christening — there is nothing to look up. The test is exactly the naming test:
|
||||||
|
*would a reader have to look this up to know what it means?* If yes, name it. This is
|
||||||
|
what `opcodes.zig`'s `*_opcode` constants, `acpi-ids`'s `HardwareId`, and `pci-class`'s
|
||||||
|
class enums already are — reference data defined once and named everywhere it is used.
|
||||||
|
|
||||||
## Why acronyms are the line
|
## Why acronyms are the line
|
||||||
|
|
||||||
Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped
|
Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped
|
||||||
|
|||||||
@@ -136,8 +136,10 @@ published exit events, signals + `runtime.process`). On top of those:
|
|||||||
the mouse and keyboard QEMU already hangs off it.
|
the mouse and keyboard QEMU already hangs off it.
|
||||||
7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats
|
7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats
|
||||||
to a manager-internal seam.
|
to a manager-internal seam.
|
||||||
8. **Discovery migration**: pci-bus driver first, acpi service second, kernel scan
|
8. **Discovery migration** — DONE (M19–M20, 2026-07-13): pci-bus driver (M19)
|
||||||
retired last. (AML-in-user-space is its own track.)
|
then the acpi service (M20) moved enumeration to ring 3; the kernel seeds
|
||||||
|
only the host bridge and the acpi-tables node. See
|
||||||
|
[m19-m20-plan.md](m19-m20-plan.md).
|
||||||
|
|
||||||
## Settled questions (2026-07-12)
|
## Settled questions (2026-07-12)
|
||||||
|
|
||||||
|
|||||||
@@ -167,3 +167,27 @@ free; discovery on x86 is partly about *finding* what ARM just tells you.
|
|||||||
- [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager
|
- [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager
|
||||||
will ride on.
|
will ride on.
|
||||||
- [vision.md](vision.md) — why drivers belong in isolated user space at all.
|
- [vision.md](vision.md) — why drivers belong in isolated user space at all.
|
||||||
|
|
||||||
|
## Update (M19.3, 2026-07-13): PCI enumeration left the kernel
|
||||||
|
|
||||||
|
The kernel now seeds only the `pci_host_bridge` node (ECAM window, MMIO
|
||||||
|
apertures derived from the memory map's holes, bus range, and the 16-bit I/O
|
||||||
|
window). The per-function walk moved to the ring-3 `pci-bus` driver
|
||||||
|
([device-manager.md](device-manager.md)): it claims the bridge, repeats the
|
||||||
|
ECAM scan through its mmio grant, and `device_register`s what it finds, which
|
||||||
|
the device manager mirrors and matches. The ACPI namespace walk follows in M20;
|
||||||
|
the static tables (MADT, HPET, MCFG, FADT + `\\_S5`) stay kernel-side.
|
||||||
|
|
||||||
|
## Update (M20.3, 2026-07-13): ACPI enumeration left the kernel too
|
||||||
|
|
||||||
|
The kernel no longer folds the AML namespace's Device objects into the device
|
||||||
|
tree. It still parses the *static* tables (MADT for SMP, HPET for the tick, MCFG
|
||||||
|
for the host bridge, FADT) and still builds the AML namespace — but only to read
|
||||||
|
the `\\_S5` sleep type for poweroff. Device discovery is the ring-3 **acpi
|
||||||
|
service** ([device-manager.md](device-manager.md)): it claims the `acpi-tables`
|
||||||
|
node the kernel publishes (the AML blobs, a broad io_port grant, the SCI),
|
||||||
|
re-parses the same blobs with the shared AML module, evaluates `_STA`/`_CRS`,
|
||||||
|
and registers + reports each `_HID` device — the device manager matches drivers
|
||||||
|
(ps2-bus) from those reports. With M19's pci-bus driver, discovery now runs
|
||||||
|
entirely in user space; the kernel seeds only the host bridge and the
|
||||||
|
acpi-tables node.
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# M17–M18 execution plan: process lifecycle + device manager
|
# M17–M18 execution plan: process lifecycle + device manager
|
||||||
|
|
||||||
|
**Archived — completed 2026-07-13** (every item checked; suite ended 54/54).
|
||||||
|
Kept as the record of how M17–M18 landed; the successor is
|
||||||
|
[m19-m20-plan.md](m19-m20-plan.md).
|
||||||
|
|
||||||
The operational plan for building [process-lifecycle.md](process-lifecycle.md)
|
The operational plan for building [process-lifecycle.md](process-lifecycle.md)
|
||||||
(M17) and [device-manager.md](device-manager.md) increments 5–7 (M18). Design is
|
(M17) and [device-manager.md](device-manager.md) increments 5–7 (M18). Design is
|
||||||
settled in those documents; this file is the build order — one phase at a time,
|
settled in those documents; this file is the build order — one phase at a time,
|
||||||
|
|||||||
+57
-57
@@ -79,43 +79,59 @@ branch is green; keep branches; push everything.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
- [ ] **M19.0** — prerequisites on `feat/pci-bus`: bridge MMIO apertures from
|
- [x] **M19.0** — prerequisites (bridge apertures from the memory map's
|
||||||
the memory-map holes; `device_register` idempotence (+ kernel unit
|
*gaps* — the single-hole rule died on OVMF's flash at the top of 4 GiB,
|
||||||
checks); `ChildAdded.device_id`; archive note on m17-m18-plan.md.
|
caught by the new every-BAR-contained assert in `discovery`; idempotent
|
||||||
- [ ] **M19.1** — pci-bus driver, scan only: claim the host bridge, map the
|
`device_register` proven in `bus`; `ChildAdded.device_id`;
|
||||||
ECAM window, walk bus/device/function headers, log what it finds.
|
m17-m18-plan.md archived; suite 54/54).
|
||||||
Scenario `pci-scan`: the kernel test compares the driver's reported count
|
- [x] **M19.1** — pci-bus driver, scan only (claims the bridge, maps ECAM
|
||||||
against the broker table's `pci_device` count — equivalence, per class.
|
through its grant, brute-force walk with the multifunction rule; the
|
||||||
- [ ] **M19.2** — register + report: each function registered under the bridge
|
manager matches pci_host_bridge → pci-bus per device with the full
|
||||||
(config-space slice + BARs, `pci_class` in the descriptor), reported with
|
protocol contract; `pci-scan` builds its expected marker from the
|
||||||
`child_added { device_id, identity = class triple }`. Manager mirrors;
|
kernel's own count — equivalence on the first run; suite 55/55).
|
||||||
spawn-from-reports stays **off**. Scenario extends `pci-scan`:
|
- [x] **M19.2** — register + report (BAR probe mirrored byte-for-byte from the
|
||||||
registered ids resolve, no duplicates after a forced driver restart
|
kernel's addBars so dedupe returns the kernel's node ids during
|
||||||
(idempotence proven end to end).
|
coexistence; the bridge gained the io_port aperture I/O BARs need;
|
||||||
- [ ] **M19.3** — the flip: kernel `enumeratePci` call removed (bridge node
|
reports carry the registered device_id; pci-scan drills a forced restart
|
||||||
stays); manager matches PCI drivers from reports. One commit. The
|
and asserts the PCI node count never grows — plus harness hardening: a
|
||||||
existing xHCI scenarios (`driver-restart`, `usb-report`, `device-list`)
|
failing case now preserves its serial as <case>-failed-serial.log, and
|
||||||
are the assertion — xhci must come up spawned off a pci-bus report, and
|
the heavy scenarios run at 150s; suite 55/55).
|
||||||
the suite must not be able to tell the difference. discovery.md updated.
|
- [x] **M19.3** — the flip: kernel `enumeratePci`/`addBars`/`PciHeader` all
|
||||||
- [ ] **merge** `feat/pci-bus` → main, push.
|
deleted (bridge node stays); manager matches PCI drivers from reported
|
||||||
- [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at
|
identity, deduped by registered id. Surfaced and fixed a real SMP race the
|
||||||
system/services/acpi/acpi.zig): kernel publishes `acpi-tables`
|
flip created — ring-3 device_register made the broker table concurrent, so
|
||||||
(decision 5) — memory over the table blobs, the broad io_port grant, and
|
mmio_map's lock-free read intermittently tore hpet's resource length
|
||||||
**the SCI as an irq resource** (from the FADT; unused until M21 but free
|
(user fault) and overflowed `r.len-1` into a kernel panic; now the broker
|
||||||
to record now). The service claims it, maps the blobs, runs the shared
|
read is under the big lock and the arithmetic is guarded, and pci-bus
|
||||||
AML module in ring 3, logs the namespace device count and `_HID`s.
|
skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart
|
||||||
Scenario `acpi-parse`: user-space count equals the kernel walk's count.
|
hammered 6×).
|
||||||
- [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS`
|
- [x] **merge** `feat/pci-bus` → main, push (merged 2026-07-13).
|
||||||
resources registered under `acpi-tables` (its io_port + the memory-map
|
- [x] **M20.1** — acpi service, parse only: the AML interpreter is now a build
|
||||||
holes give containment), reported to the manager. Spawn-from-reports for
|
module compiled into both kernel and service; the kernel publishes the
|
||||||
ACPI matches stays off. Scenario: the reported set includes the PS/2
|
`acpi-tables` node (AML blobs as memory resources, the broad io_port grant,
|
||||||
keyboard and mouse nodes with their IRQ resources.
|
the SCI); the service claims it, maps the blobs, runs the shared parser in
|
||||||
- [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static
|
ring 3, and self-verifies its Device count against the kernel's (34 = 34,
|
||||||
tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers
|
deterministic via argv, no log-scraping); the manager spawns `discovery`
|
||||||
(ps2-bus) from reports. The `input` and `device-manager` scenarios are
|
at startup. Parse-only touches no hardware. Suite 56/56.
|
||||||
the assertion. discovery.md + acpi.md + device-manager.md updated;
|
|
||||||
device-manager.md increment 8 closed.
|
- [x] **M20.2** — register + report: the service evaluates `_STA`/`_CRS` in
|
||||||
- [ ] **merge** `feat/acpi-service` → main, push — **loop ends here**.
|
ring 3 (interpreter Hal = port I/O over the claimed node; a scratch page
|
||||||
|
backs SystemMemory maps so a stray region can't fault it) and registers +
|
||||||
|
reports each present `_HID` device under `acpi-tables`. Containment: the
|
||||||
|
broker's irq check became range-based (len-1 == the old equality) so the
|
||||||
|
node's broad irq window covers children's legacy lines; io ports fall in
|
||||||
|
the broad io grant. ChildAdded gained `hid`. Matching stays off. The
|
||||||
|
`acpi-report` scenario asserts the PS/2 keyboard (3 resources) and mouse
|
||||||
|
(1 resource) among the reports. Suite 57/57.
|
||||||
|
- [x] **M20.3** — the flip: the kernel's `wireAcpiDevices` call is gone (the
|
||||||
|
device-building helpers are retained-but-dead pending a focused sweep,
|
||||||
|
spawned as a task; static tables + `\_S5` + the acpi-tables node stay).
|
||||||
|
The manager matches ps2-bus from ACPI `_HID` reports; the service
|
||||||
|
registers all devices before reporting any (no keyboard-before-mouse
|
||||||
|
race). The `acpi-ps2` scenario proves report → spawn → ps2-bus attaches
|
||||||
|
its keyboard; `ioport` retargeted to the acpi-tables I/O window (the
|
||||||
|
kernel-built PS/2 node is gone). Suite 58/58.
|
||||||
|
- [x] **merge** `feat/acpi-service` → main, push (merged 2026-07-13) — **discovery migration complete**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -173,24 +189,8 @@ suspend/resume — a future *lifecycle-vocabulary* extension, since "suspend"
|
|||||||
has the shape of a signal every driver must answer, and it has no consumer
|
has the shape of a signal every driver must answer, and it has no consumer
|
||||||
until laptop sleep); CPU P/C-states.
|
until laptop sleep); CPU P/C-states.
|
||||||
|
|
||||||
## M21 preview — ACPI events + system power (planned next, not in this loop)
|
## M21 — ACPI events + system power — DONE
|
||||||
|
|
||||||
The acpi service grows the event side (settled direction 2026-07-13; detailed
|
Built and merged (docs/m21-plan.md, 2026-07-13): the SCI + power button, Notify/GPE
|
||||||
phases when M20 lands):
|
dispatch, and orderly shutdown (init's stop cascade into a ring-3 S5 write).
|
||||||
|
See that plan for the phase record.
|
||||||
- **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,139 @@
|
|||||||
|
# M21 execution plan: ACPI events + system power
|
||||||
|
|
||||||
|
The operational plan for the event side of the acpi service and orderly
|
||||||
|
shutdown — the capstone [m19-m20-plan.md](m19-m20-plan.md) previewed. Same
|
||||||
|
rules as its predecessors: 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). Failing cases preserve their serial logs
|
||||||
|
(`<case>-failed-serial.log`).
|
||||||
|
|
||||||
|
**Workflow:** dedicated worktree; branch `feat/power-events` off `main`;
|
||||||
|
auto-merge to main when the branch is green; keep the branch; push everything.
|
||||||
|
|
||||||
|
## Settled decisions (2026-07-13, approved)
|
||||||
|
|
||||||
|
1. **S5 is executed by the acpi service from ring 3.** No new syscall: the
|
||||||
|
broad port grant (M20 decision 5) already made this physically possible —
|
||||||
|
the service holds the PM1 control ports in its io grant and derives `_S5`
|
||||||
|
from its own namespace (`aml.sleepState`). Formalizing it adds no
|
||||||
|
authority. The kernel keeps `power.zig` for its own test paths and
|
||||||
|
panic-time use.
|
||||||
|
2. **The power surface is domain-named** (decision 7 of the last plan): a
|
||||||
|
`power-protocol` module + `ServiceId.power = 5`, registered by the acpi
|
||||||
|
service — on ARM, a PSCI/mailbox service registers the same id and
|
||||||
|
subscribers never know the difference. Messages: `subscribe` (endpoint as
|
||||||
|
the call's capability, the input/manager pattern), `shutdown` (accepted
|
||||||
|
only from PID 1 — init), and events published as buffered messages:
|
||||||
|
`power_button`, `lid`, `ac`, `battery`, generic `notify` with a code.
|
||||||
|
3. **The service learns event ports from its own FADT copy**: the kernel adds
|
||||||
|
the FADT as one more memory resource on the acpi-tables node; the service
|
||||||
|
tells it apart from the AML blobs by signature ("FACP" header — the blob
|
||||||
|
resources are header-stripped bytecode and start with no signature). The
|
||||||
|
kernel's own FADT parse is untouched.
|
||||||
|
4. **The acpi service converts to the harness** (`runtime.service.run`):
|
||||||
|
protocol messages (subscribe/shutdown), the SCI notification, and the
|
||||||
|
existing report flow fold into one loop — the shape it was always meant
|
||||||
|
to have.
|
||||||
|
5. **GPE/Notify correctness is proven by host unit tests** (synthetic AML
|
||||||
|
with a Notify inside a method body; aml.zig joins the `zig build test`
|
||||||
|
loop). The QEMU scenario proves the power button — a *fixed* event,
|
||||||
|
deterministically injectable via QMP `system_powerdown` — because QEMU
|
||||||
|
cannot raise GPEs deterministically on this config. Battery/AC/lid and the
|
||||||
|
embedded controller (`_Qxx`) are interface-complete here and validated on
|
||||||
|
real hardware (the laptop) later.
|
||||||
|
|
||||||
|
## Ground truth the phases build on (verified 2026-07-13)
|
||||||
|
|
||||||
|
- `system/devices/power.zig` `shutdown()` is the kernel's S5 write
|
||||||
|
(SLP_TYP|SLP_EN to PM1a/PM1b control); there is no power syscall.
|
||||||
|
- init (`system/services/init/init.zig`) spawns vfs/input/device-manager
|
||||||
|
fire-and-forget — no child ids kept, no signals, no event loop. The whole
|
||||||
|
stop toolkit exists in `runtime.process` (stop/sendSignal/bindSignals).
|
||||||
|
- `test/qemu_test.py` has no QMP channel (serial is a one-way file).
|
||||||
|
- The kernel parses PM1 *control* blocks and SCI_INT from the FADT; the PM1
|
||||||
|
**event** blocks (offsets 56/60, len at 88) and **GPE0/GPE1** blocks
|
||||||
|
(offsets 80/84, lens 92/93) are unparsed — the service reads them from its
|
||||||
|
FADT copy (decision 3).
|
||||||
|
- The acpi-tables node carries the SCI as its only `len == 1` irq resource
|
||||||
|
(the broad window is len 256) — that is how the service finds it to
|
||||||
|
`irqBind`.
|
||||||
|
- `notify_opcode = 0x86` exists in `system/devices/aml/opcodes.zig` but the
|
||||||
|
interpreter never handles it — a GPE `_Lxx` body containing Notify fails
|
||||||
|
evaluation today. Everything else a GPE handler needs (field access,
|
||||||
|
control flow, method calls) is proven by the ring-3 `_STA`/`_CRS` work.
|
||||||
|
- The dead-code sweep (spawned task) also edits `system/devices/acpi.zig`;
|
||||||
|
M21.0 checks whether it landed and rebases before touching that file.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- [x] **M21.0** — baseline (dead-code sweep confirmed landed on main — no
|
||||||
|
acpi.zig conflict; `feat/power-events` cut; QMP channel in the harness:
|
||||||
|
always-on unix socket, client with the capabilities handshake, per-case
|
||||||
|
`qmp_after` hook, and a hook-must-deliver pass gate that the smoke case
|
||||||
|
now proves with a harmless query-status; suite 58/58).
|
||||||
|
- [x] **M21.1** — SCI + the power button (kernel appends the FADT as an
|
||||||
|
acpi-tables memory resource, tagged by its "FACP" header; `power-protocol`
|
||||||
|
module + `ServiceId.power = 5`; the acpi service converted to
|
||||||
|
`runtime.service.run`, registers `.power`, reads PM1 event/control + GPE
|
||||||
|
ports from its FADT copy, enables ACPI mode if SCI_EN is clear, binds the
|
||||||
|
SCI (the len-1 irq), sets PWRBTN_EN; the SCI handler clears PM1_STS,
|
||||||
|
logs `power: button pressed`, publishes `power_button`, acks. Scenario
|
||||||
|
`power-button` injects a real `system_powerdown` via QMP; initial-ramdisk
|
||||||
|
timeout 30→60s for the service's added boot work; suite 59/59).
|
||||||
|
- [x] **M21.2** — Notify + GPE dispatch (interpreter handles `notify_opcode`
|
||||||
|
into a bounded queue, cleared per-evaluate, drained via
|
||||||
|
`takeNotifications`; the service walks GPE status/enable bytes, evaluates
|
||||||
|
`\_GPE._Lxx`/`_Exx` per active bit, maps notified nodes to events
|
||||||
|
(battery/ac/lid/generic), clears GPE_STS write-1, acks. EC `_Qxx` out.
|
||||||
|
Host unit test with hand-encoded AML proves the queue; aml.zig joined the
|
||||||
|
`zig build test` loop. QEMU raises no GPEs — suite is regression net,
|
||||||
|
59/59).
|
||||||
|
- [x] **M21.3** — orderly shutdown (init supervises its children on one
|
||||||
|
endpoint that also carries signals, power events, and a re-arming
|
||||||
|
heartbeat timer; on `power_button` or a `terminate` signal it logs
|
||||||
|
`init: shutting down`, runs `stop(child, 2000, endpoint)` in reverse
|
||||||
|
order, then requests `.power` shutdown; the acpi service honors shutdown
|
||||||
|
from a subscriber — init is the one subscriber, a soft gate that survives
|
||||||
|
testing where PID 1 isn't init — and writes SLP_TYP|SLP_EN from ring 3.
|
||||||
|
`orderly-shutdown` scenario proves button → shutting-down → S5 → QEMU
|
||||||
|
exit; suite 60/60).
|
||||||
|
- [ ] **merge** `feat/power-events` → main, push, keep the branch — **loop
|
||||||
|
ends here**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase notes
|
||||||
|
|
||||||
|
**M21.0 QMP:** open the unix socket after Popen, complete the
|
||||||
|
`qmp_capabilities` handshake, then send the hook's command (for these
|
||||||
|
scenarios: `{"execute": "system_powerdown"}`). The socket is additive — no
|
||||||
|
existing case may notice it. Note e3fe3f3 recently reworked how the harness
|
||||||
|
boots; adapt to its current shape rather than the pre-rework description.
|
||||||
|
|
||||||
|
**M21.1 SCI details:** PM1_STS is at the event block base (write-1-to-clear);
|
||||||
|
PM1_EN at base + block_len/2; PWRBTN bit is 8 in both. If PM1b exists, mirror
|
||||||
|
reads/writes to both blocks. Enable ACPI mode only when SCI_EN (PM1 control
|
||||||
|
bit 0) is clear — OVMF boots may already have it set. The publish path reuses
|
||||||
|
the manager's subscriber table pattern (bounded, drop-on-failed-send).
|
||||||
|
|
||||||
|
**M21.2 GPE walk:** GPE0_STS bytes live at the GPE0 block base, GPE0_EN in
|
||||||
|
the block's upper half; for a set+enabled bit n, the handler method is
|
||||||
|
`_L%02X` (level) or `_E%02X` (edge) under `\_GPE`. Evaluate, drain the notify
|
||||||
|
queue, clear the status bit, ack. A missing handler method is clear-and-log,
|
||||||
|
not an error.
|
||||||
|
|
||||||
|
**M21.3 ordering:** init subscribes with retries — the acpi service registers
|
||||||
|
`.power` well after init starts. The stop sequence runs vfs last (other
|
||||||
|
services may flush through it). The S5 write mirrors `power.zig`'s
|
||||||
|
`sleepValue` (SLP_TYP bits [12:10], SLP_EN bit 13); if the write returns, log
|
||||||
|
`power: S5 write did not take` so the scenario fails loudly instead of
|
||||||
|
hanging.
|
||||||
|
|
||||||
|
**Explicitly out of scope:** the embedded controller and `_Qxx` queries,
|
||||||
|
battery `_BST`/`_BIF` evaluation beyond the interface stubs, lid/AC on QEMU
|
||||||
|
(no emulation), reboot over the power protocol, S3 sleep, per-device D-states
|
||||||
|
(a future lifecycle-vocabulary extension), thermal zones.
|
||||||
@@ -20,6 +20,9 @@ pub const vfs_protocol = @import("vfs-protocol");
|
|||||||
|
|
||||||
/// The device-manager protocol: hello + tree reports (docs/device-manager.md).
|
/// The device-manager protocol: hello + tree reports (docs/device-manager.md).
|
||||||
pub const device_manager_protocol = @import("device-manager-protocol");
|
pub const device_manager_protocol = @import("device-manager-protocol");
|
||||||
|
|
||||||
|
/// The power protocol: events (button, lid, battery) + shutdown (docs/m21-plan.md).
|
||||||
|
pub const power_protocol = @import("power-protocol");
|
||||||
/// Keyboard-event listening (subscribe/next) and broadcasting (publish), over the input
|
/// Keyboard-event listening (subscribe/next) and broadcasting (publish), over the input
|
||||||
/// service. See library/runtime/input.zig and system/services/input/.
|
/// service. See library/runtime/input.zig and system/services/input/.
|
||||||
pub const input = @import("input.zig");
|
pub const input = @import("input.zig");
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ pub const ServiceId = enum(u32) {
|
|||||||
input = 2,
|
input = 2,
|
||||||
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
|
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
|
||||||
device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md)
|
device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md)
|
||||||
|
power = 5, // system power: events (button, lid, battery) + shutdown (docs/m21-plan.md; domain-named per decision 7 — the acpi service registers it on x86, a PSCI service will on ARM)
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+138
-504
@@ -17,7 +17,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const boot_handoff = @import("boot-handoff");
|
const boot_handoff = @import("boot-handoff");
|
||||||
const abi = @import("abi");
|
const abi = @import("abi");
|
||||||
const acpi_ids = @import("acpi-ids");
|
|
||||||
const parameters = @import("parameters");
|
const parameters = @import("parameters");
|
||||||
const device_model = @import("device-model.zig");
|
const device_model = @import("device-model.zig");
|
||||||
const aml = @import("aml/aml.zig");
|
const aml = @import("aml/aml.zig");
|
||||||
@@ -41,6 +40,9 @@ pub const RegisterAccess = struct {
|
|||||||
/// Everything the power subsystem needs, extracted from the FADT and the AML
|
/// Everything the power subsystem needs, extracted from the FADT and the AML
|
||||||
/// sleep packages during discovery. Populated by `discover`, read by `power`.
|
/// sleep packages during discovery. Populated by `discover`, read by `power`.
|
||||||
pub const PowerInformation = struct {
|
pub const PowerInformation = struct {
|
||||||
|
/// The System Control Interrupt's GSI (FADT SCI_INT) — the line ACPI events
|
||||||
|
/// (power button, GPEs) arrive on. Published to the acpi service for M21.
|
||||||
|
sci_interrupt: u16 = 0,
|
||||||
/// The SMM command port and the value that switches the platform into ACPI mode.
|
/// The SMM command port and the value that switches the platform into ACPI mode.
|
||||||
smi_cmd: u16 = 0,
|
smi_cmd: u16 = 0,
|
||||||
acpi_enable: u8 = 0,
|
acpi_enable: u8 = 0,
|
||||||
@@ -155,6 +157,13 @@ pub var namespace: ?aml.Namespace = null;
|
|||||||
/// Physical address of the DSDT the FADT points at, or 0.
|
/// Physical address of the DSDT the FADT points at, or 0.
|
||||||
pub var dsdt_physical: u64 = 0;
|
pub var dsdt_physical: u64 = 0;
|
||||||
|
|
||||||
|
/// The FADT itself (physical + length), published on the acpi-tables node so
|
||||||
|
/// the ring-3 acpi service can read the PM1 event and GPE blocks it needs for
|
||||||
|
/// the event side (docs/m21-plan.md decision 3). Distinguished from the AML
|
||||||
|
/// blob resources by its intact "FACP" header — the blobs are header-stripped.
|
||||||
|
var fadt_physical: u64 = 0;
|
||||||
|
var fadt_length: u64 = 0;
|
||||||
|
|
||||||
// AML blocks (DSDT + any SSDTs) collected during the table walk, as physical
|
// AML blocks (DSDT + any SSDTs) collected during the table walk, as physical
|
||||||
// address + length of each table's post-header bytecode. Scanned after the walk
|
// address + length of each table's post-header bytecode. Scanned after the walk
|
||||||
// for the sleep-state (`_Sx`) packages.
|
// for the sleep-state (`_Sx`) packages.
|
||||||
@@ -360,35 +369,19 @@ const Hpet = extern struct {
|
|||||||
page_protection: u8,
|
page_protection: u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- PCI configuration-space header (first 64 bytes, common fields) ---------
|
|
||||||
|
|
||||||
const PciHeader = extern struct {
|
|
||||||
vendor_id: u16 align(1),
|
|
||||||
device_id: u16 align(1),
|
|
||||||
command: u16 align(1),
|
|
||||||
status: u16 align(1),
|
|
||||||
revision_id: u8,
|
|
||||||
prog_if: u8,
|
|
||||||
subclass: u8,
|
|
||||||
class_code: u8,
|
|
||||||
cache_line_size: u8,
|
|
||||||
latency_timer: u8,
|
|
||||||
/// bit 7 set => multi-function device.
|
|
||||||
header_type: u8,
|
|
||||||
bist: u8,
|
|
||||||
// 0x10 onward (BARs, etc.) depends on header_type; read separately.
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Entry point ------------------------------------------------------------
|
// --- Entry point ------------------------------------------------------------
|
||||||
|
|
||||||
/// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate
|
/// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate
|
||||||
/// `device_tree`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the
|
/// `device_tree`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the
|
||||||
/// FADT and the AML sleep-state (`_Sx`) packages into `power_information` for the power service.
|
/// FADT and the AML sleep-state (`_Sx`) packages into `power_information` for the power service.
|
||||||
pub fn discover(rsdp_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryRegion, device_tree: *DeviceTree, hal: Hal) !void {
|
||||||
if (rsdp_physical == 0) return error.NoRsdp;
|
if (rsdp_physical == 0) return error.NoRsdp;
|
||||||
|
boot_memory_regions = memory_regions;
|
||||||
|
|
||||||
// Start clean so a re-run doesn't accumulate stale state.
|
// Start clean so a re-run doesn't accumulate stale state.
|
||||||
power_information = .{};
|
power_information = .{};
|
||||||
|
fadt_physical = 0;
|
||||||
|
fadt_length = 0;
|
||||||
platform_information = .{};
|
platform_information = .{};
|
||||||
aml_stats = .{};
|
aml_stats = .{};
|
||||||
namespace = null;
|
namespace = null;
|
||||||
@@ -420,12 +413,58 @@ pub fn discover(rsdp_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
|||||||
aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total };
|
aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total };
|
||||||
power_information.s5 = aml.sleepState(&namespace.?, 5);
|
power_information.s5 = aml.sleepState(&namespace.?, 5);
|
||||||
power_information.s3 = aml.sleepState(&namespace.?, 3);
|
power_information.s3 = aml.sleepState(&namespace.?, 3);
|
||||||
// Fold the namespace's Device objects into the generic tree.
|
// The namespace's Device objects are no longer folded into the kernel
|
||||||
wireAcpiDevices(device_tree, &namespace.?, hal) catch {};
|
// tree (M20.3): the ring-3 acpi service claims the acpi-tables node
|
||||||
|
// (published below), re-parses the same blobs, and registers + reports
|
||||||
|
// the _HID devices itself. The kernel keeps the namespace only for the
|
||||||
|
// \_S5 sleep type above.
|
||||||
} else |_| {
|
} else |_| {
|
||||||
// AML parse failed (e.g. out of memory); power stays best-effort with
|
// AML parse failed (e.g. out of memory); power stays best-effort with
|
||||||
// whatever the FADT alone provided.
|
// whatever the FADT alone provided.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish the acpi-tables node (docs/m19-m20-plan.md M20): the AML blobs as
|
||||||
|
// memory resources for the acpi service to map and parse in ring 3, a broad
|
||||||
|
// io_port grant for the OperationRegion access its interpreter needs, and
|
||||||
|
// the SCI for the events track (M21). Exactly one node, one trusted
|
||||||
|
// claimant. Kept even when the kernel-side device building (above) retires
|
||||||
|
// in M20.3 — the kernel still owns the *static* tables and \_S5.
|
||||||
|
publishAcpiTablesNode(device_tree) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the acpi-tables node (see the call site in discover). Best-effort: a
|
||||||
|
/// failure here leaves the kernel-seeded tree working, only the ring-3 service
|
||||||
|
/// finds nothing to claim.
|
||||||
|
fn publishAcpiTablesNode(device_tree: *DeviceTree) !void {
|
||||||
|
const node = try device_tree.addChild(device_tree.root, .acpi_tables, "acpi-tables");
|
||||||
|
// One memory resource per AML block — page-aligned base down, length padded
|
||||||
|
// up to cover the bytecode, so mmio_map hands the service a pointer into it.
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < aml_block_count and i < device_model.maximum_resources - 2) : (i += 1) {
|
||||||
|
// mmio_map preserves the sub-page offset, so the service maps this and
|
||||||
|
// gets a pointer straight to the bytecode.
|
||||||
|
_ = node.addResource(.memory, aml_block_physical[i], aml_block_len[i]);
|
||||||
|
}
|
||||||
|
// The broad I/O grant: OperationRegions name whatever ports the firmware
|
||||||
|
// chose (EC, PM1, GPE, SMBus); which ports cannot be known before the AML
|
||||||
|
// that names them is parsed, so the grant is the whole space — the honest
|
||||||
|
// trust boundary of docs/m19-m20-plan.md decision 5.
|
||||||
|
_ = node.addResource(.io_port, 0, 1 << 16);
|
||||||
|
// A broad interrupt window: ACPI _CRS names legacy ISA IRQs (the PS/2 lines
|
||||||
|
// 1 and 12, the RTC, …), and the service registers those devices under this
|
||||||
|
// node, so it must own a superset. The range [0, 256) covers every GSI; the
|
||||||
|
// SCI (recorded first, len 1) stays distinct so M21 can pick it out.
|
||||||
|
if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1);
|
||||||
|
_ = node.addResource(.irq, 0, 256);
|
||||||
|
// The FADT rides along (M21): the service reads the PM1 event / GPE blocks
|
||||||
|
// from its own copy, telling it apart from the AML blobs by signature.
|
||||||
|
if (fadt_physical != 0) _ = node.addResource(.memory, fadt_physical, fadt_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of Device objects in the namespace built during discovery, or 0.
|
||||||
|
pub fn amlDeviceCount() usize {
|
||||||
|
if (namespace) |*ns| return aml.deviceCount(ns);
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch
|
/// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch
|
||||||
@@ -451,10 +490,12 @@ fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void {
|
|||||||
if (std.mem.eql(u8, &sig, &APIC)) {
|
if (std.mem.eql(u8, &sig, &APIC)) {
|
||||||
try parseMadt(device_tree, header);
|
try parseMadt(device_tree, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &MCFG)) {
|
} else if (std.mem.eql(u8, &sig, &MCFG)) {
|
||||||
try parseMcfg(device_tree, hal, header);
|
try parseMcfg(device_tree, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &HPET)) {
|
} else if (std.mem.eql(u8, &sig, &HPET)) {
|
||||||
try parseHpet(device_tree, hal, header);
|
try parseHpet(device_tree, hal, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &FACP)) {
|
} else if (std.mem.eql(u8, &sig, &FACP)) {
|
||||||
|
fadt_physical = sdt_physical;
|
||||||
|
fadt_length = header.length;
|
||||||
parseFadt(header);
|
parseFadt(header);
|
||||||
} else if (std.mem.eql(u8, &sig, &SPCR)) {
|
} else if (std.mem.eql(u8, &sig, &SPCR)) {
|
||||||
parseSpcr(header);
|
parseSpcr(header);
|
||||||
@@ -536,7 +577,7 @@ fn parseMadt(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeade
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath.
|
/// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath.
|
||||||
fn parseMcfg(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptorTableHeader) !void {
|
fn parseMcfg(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeader) !void {
|
||||||
const total: usize = header.length;
|
const total: usize = header.length;
|
||||||
const base: [*]const u8 = @ptrCast(header);
|
const base: [*]const u8 = @ptrCast(header);
|
||||||
|
|
||||||
@@ -551,109 +592,85 @@ fn parseMcfg(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptor
|
|||||||
// ECAM window: 1 MiB of configuration space per bus.
|
// ECAM window: 1 MiB of configuration space per bus.
|
||||||
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
|
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
|
||||||
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
|
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
|
||||||
|
addBridgeApertures(bridge);
|
||||||
|
// The bridge decodes the whole 16-bit I/O space toward its bus — the
|
||||||
|
// window functions' I/O BARs must register-contain within (M19.2).
|
||||||
|
_ = bridge.addResource(.io_port, 0, 1 << 16);
|
||||||
|
|
||||||
try enumeratePci(device_tree, bridge, hal, alloc.*);
|
// The function walk itself retired to ring 3 (M19.3): the pci-bus
|
||||||
|
// driver claims this bridge, repeats the scan through its ECAM grant,
|
||||||
|
// and device_registers what it finds — the kernel seeds only the
|
||||||
|
// bridge. The scan's equivalence was proven before the hand-off
|
||||||
|
// (pci-scan), and the walk's history is in git if archaeology calls.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Brute-force scan the ECAM window's bus range for present PCI functions. No
|
/// The boot memory map, stored at discover() entry for the aperture derivation
|
||||||
/// bridge recursion yet: on the ECAM path the host bridge decodes every bus in
|
/// below (and, in M20, for the acpi-tables node's containment windows).
|
||||||
/// the window, so scanning the declared range finds everything QEMU exposes.
|
var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
|
||||||
fn enumeratePci(
|
|
||||||
device_tree: *DeviceTree,
|
|
||||||
bridge: *device_model.Device,
|
|
||||||
hal: Hal,
|
|
||||||
alloc: McfgAllocation,
|
|
||||||
) !void {
|
|
||||||
var bus: u16 = alloc.start_bus;
|
|
||||||
while (bus <= alloc.end_bus) : (bus += 1) {
|
|
||||||
var device: u8 = 0;
|
|
||||||
while (device < 32) : (device += 1) {
|
|
||||||
const h0: *align(1) const PciHeader = @ptrCast(pciConfigurationPtr(alloc, hal, @intCast(bus), device, 0));
|
|
||||||
if (h0.vendor_id == 0xFFFF) continue; // no function 0 => slot empty
|
|
||||||
|
|
||||||
const funcs: u8 = if (h0.header_type & 0x80 != 0) 8 else 1;
|
/// The bridge's MMIO apertures, derived from the boot memory map's holes
|
||||||
var function: u8 = 0;
|
/// (docs/m19-m20-plan.md decision 2): registered PCI functions carry BAR
|
||||||
while (function < funcs) : (function += 1) {
|
/// resources, and `device_register` containment demands the bridge own windows
|
||||||
const configuration = pciConfigurationPtr(alloc, hal, @intCast(bus), device, function);
|
/// that cover them. Everything the firmware described is "not hole"; the low
|
||||||
const h: *align(1) const PciHeader = @ptrCast(configuration);
|
/// aperture runs from the end of the described space below 4 GiB up to the
|
||||||
if (h.vendor_id == 0xFFFF) continue;
|
/// I/O-APIC region, the high one from 4 GiB (or the end of RAM above it) to
|
||||||
|
/// the 46-bit line. Coarse, mechanical, and AML-free — available at boot no
|
||||||
var nb: [24]u8 = undefined;
|
/// matter what later moved to user space.
|
||||||
const nm = std.fmt.bufPrint(&nb, "{s}:{x:0>2}:{x:0>2}.{d}", .{
|
fn addBridgeApertures(bridge: *device_model.Device) void {
|
||||||
bridge.name(), bus, device, function,
|
// Below 4 GiB the described regions are sparse (RAM low, firmware flash
|
||||||
}) catch "pcidev";
|
// and tables high), so the holes are the *gaps between* them — a single
|
||||||
const node = try device_tree.addChild(bridge, .pci_device, nm);
|
// "after the last region" rule dies on OVMF's flash at the very top.
|
||||||
// Resource 0 is the function's own 4 KiB ECAM configuration space. A
|
// Sort-merge the described ranges, then keep the three largest gaps
|
||||||
// claimed PCI driver mmio_maps this to reach its command register,
|
// (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
|
||||||
// BARs, and — the point — its capability list (MSI/MSI-X, PCIe
|
// high aperture fits). Above 4 GiB one aperture runs from the end of the
|
||||||
// extended caps), without any new syscall. Physical address per the
|
// described space to the 46-bit line.
|
||||||
// ECAM formula (same as pciConfigurationPtr).
|
const Range = struct { base: u64, end: u64 };
|
||||||
const config_physical = alloc.base_address +
|
var below: [64]Range = undefined;
|
||||||
(@as(u64, @as(u8, @intCast(bus)) - alloc.start_bus) << 20) +
|
var below_count: usize = 0;
|
||||||
(@as(u64, device) << 15) + (@as(u64, function) << 12);
|
var high_end: u64 = 1 << 32;
|
||||||
_ = node.addResource(.memory, config_physical, abi.page_size);
|
for (boot_memory_regions) |region| {
|
||||||
node.ids.pci_vendor = h.vendor_id;
|
const end = region.base + region.pages * 4096;
|
||||||
node.ids.pci_device = h.device_id;
|
// Above 4 GiB only *usable RAM* blocks the aperture: OVMF describes
|
||||||
node.ids.pci_class = (@as(u24, h.class_code) << 16) |
|
// its own 64-bit PCI window as a reserved region and then programs
|
||||||
(@as(u24, h.subclass) << 8) | h.prog_if;
|
// BARs inside it — honoring reserved there would exclude the very
|
||||||
node.ids.pci_bdf = (@as(u16, @intCast(bus)) << 8) | (@as(u16, device) << 3) | function;
|
// space BARs live in. Below 4 GiB every described region blocks (the
|
||||||
|
// kernel image, the tables, the ramdisk all live there). Bring-up
|
||||||
// BARs only exist in header type 0 (normal devices), not bridges.
|
// trust: only the bridge's claimant can register into the aperture.
|
||||||
if (h.header_type & 0x7F == 0) addBars(node, configuration);
|
if (region.kind == .usable and end > high_end) high_end = end;
|
||||||
|
if (region.base >= (1 << 32) or below_count == below.len) continue;
|
||||||
|
below[below_count] = .{ .base = region.base, .end = @min(end, 1 << 32) };
|
||||||
|
below_count += 1;
|
||||||
|
}
|
||||||
|
// Insertion sort by base (the map is small and this runs once at boot).
|
||||||
|
for (1..below_count) |i| {
|
||||||
|
const key = below[i];
|
||||||
|
var j = i;
|
||||||
|
while (j > 0 and below[j - 1].base > key.base) : (j -= 1) below[j] = below[j - 1];
|
||||||
|
below[j] = key;
|
||||||
|
}
|
||||||
|
// Walk the sorted ranges, collecting inter-region gaps of at least 1 MiB.
|
||||||
|
var gaps: [3]Range = .{Range{ .base = 0, .end = 0 }} ** 3;
|
||||||
|
var cursor: u64 = 0;
|
||||||
|
var index: usize = 0;
|
||||||
|
while (index <= below_count) : (index += 1) {
|
||||||
|
const gap_end = if (index == below_count) (1 << 32) else below[index].base;
|
||||||
|
if (gap_end > cursor and gap_end - cursor >= (1 << 20)) {
|
||||||
|
// Keep the three largest, replacing the smallest kept so far.
|
||||||
|
var smallest: usize = 0;
|
||||||
|
for (gaps, 0..) |gap, gi| {
|
||||||
|
if (gap.end - gap.base < gaps[smallest].end - gaps[smallest].base) smallest = gi;
|
||||||
|
}
|
||||||
|
if (gap_end - cursor > gaps[smallest].end - gaps[smallest].base) {
|
||||||
|
gaps[smallest] = .{ .base = cursor, .end = gap_end };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (index < below_count and below[index].end > cursor) cursor = below[index].end;
|
||||||
}
|
}
|
||||||
|
for (gaps) |gap| {
|
||||||
|
if (gap.end > gap.base) _ = bridge.addResource(.memory, gap.base, gap.end - gap.base);
|
||||||
}
|
}
|
||||||
|
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
|
||||||
/// Record and size the memory/IO windows named by a device's Base Address
|
|
||||||
/// Registers. Sizing is the standard probe: disable decode, write all-ones, read
|
|
||||||
/// back the writable (address) bits, restore. `size = ~mask + 1`.
|
|
||||||
fn addBars(node: *device_model.Device, configuration: [*]align(1) u8) void {
|
|
||||||
// Stop the device decoding its BARs while we transiently write all-ones.
|
|
||||||
const command = rd(u16, configuration, 0x04);
|
|
||||||
wr(u16, configuration, 0x04, command & ~@as(u16, 0b11));
|
|
||||||
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < 6) : (i += 1) {
|
|
||||||
const off = 0x10 + i * 4;
|
|
||||||
const orig = rd(u32, configuration, off);
|
|
||||||
if (orig == 0) continue;
|
|
||||||
|
|
||||||
if (orig & 1 != 0) {
|
|
||||||
// I/O-space BAR (16-bit address space on x86).
|
|
||||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
|
||||||
const readback = rd(u32, configuration, off);
|
|
||||||
wr(u32, configuration, off, orig);
|
|
||||||
const mask = readback & 0xFFFF_FFFC;
|
|
||||||
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
|
|
||||||
_ = node.addResource(.io_port, orig & 0xFFFF_FFFC, size);
|
|
||||||
} else if ((orig >> 1) & 0x3 == 2) {
|
|
||||||
// 64-bit memory BAR: this BAR pair spans two configuration slots.
|
|
||||||
const orig_hi = rd(u32, configuration, off + 4);
|
|
||||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
|
||||||
wr(u32, configuration, off + 4, 0xFFFF_FFFF);
|
|
||||||
const lo = rd(u32, configuration, off);
|
|
||||||
const hi = rd(u32, configuration, off + 4);
|
|
||||||
wr(u32, configuration, off, orig);
|
|
||||||
wr(u32, configuration, off + 4, orig_hi);
|
|
||||||
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
|
|
||||||
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
|
|
||||||
const address = (@as(u64, orig_hi) << 32) | (orig & 0xFFFF_FFF0);
|
|
||||||
_ = node.addResource(.memory, address, size);
|
|
||||||
i += 1; // consumed the high half
|
|
||||||
} else {
|
|
||||||
// 32-bit memory BAR.
|
|
||||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
|
||||||
const readback = rd(u32, configuration, off);
|
|
||||||
wr(u32, configuration, off, orig);
|
|
||||||
const mask = readback & 0xFFFF_FFF0;
|
|
||||||
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
|
|
||||||
_ = node.addResource(.memory, orig & 0xFFFF_FFF0, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wr(u16, configuration, 0x04, command); // restore decode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HPET -> a timer node with its register block as an MMIO resource, plus the GSI
|
/// HPET -> a timer node with its register block as an MMIO resource, plus the GSI
|
||||||
@@ -712,6 +729,7 @@ const fadt_pm1a_cnt_blk = 64; // u32 (I/O port)
|
|||||||
const fadt_pm1b_cnt_blk = 68; // u32 (I/O port)
|
const fadt_pm1b_cnt_blk = 68; // u32 (I/O port)
|
||||||
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
|
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
|
||||||
const fadt_pm1_cnt_len = 89; // u8 (bytes)
|
const fadt_pm1_cnt_len = 89; // u8 (bytes)
|
||||||
|
const fadt_sci_int = 46; // u16 (the SCI's GSI)
|
||||||
const fadt_flags = 112; // u32
|
const fadt_flags = 112; // u32
|
||||||
const fadt_reset_register = 116; // GAS (12 bytes)
|
const fadt_reset_register = 116; // GAS (12 bytes)
|
||||||
const fadt_reset_value = 128; // u8
|
const fadt_reset_value = 128; // u8
|
||||||
@@ -729,6 +747,7 @@ fn parseFadt(header: *const SystemDescriptorTableHeader) void {
|
|||||||
const len: usize = header.length;
|
const len: usize = header.length;
|
||||||
const pi = &power_information;
|
const pi = &power_information;
|
||||||
|
|
||||||
|
pi.sci_interrupt = @truncate(fadt(u16, base, len, fadt_sci_int) orelse 0);
|
||||||
pi.smi_cmd = @truncate(fadt(u32, base, len, fadt_smi_cmd) orelse 0);
|
pi.smi_cmd = @truncate(fadt(u32, base, len, fadt_smi_cmd) orelse 0);
|
||||||
pi.acpi_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0;
|
pi.acpi_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0;
|
||||||
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
|
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
|
||||||
@@ -808,342 +827,6 @@ fn parseDmar(hal: Hal, header: *const SystemDescriptorTableHeader) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- AML namespace -> generic device tree -----------------------------------
|
|
||||||
|
|
||||||
/// The PCI bus context while descending the ACPI namespace: the generic host
|
|
||||||
/// bridge whose children ACPI address (`_ADR`) devices resolve against, and the bus number.
|
|
||||||
const PciContext = struct { bridge: *device_model.Device, bus: u8 };
|
|
||||||
|
|
||||||
/// Mirror the ACPI namespace's Device objects into the generic tree, *merging*
|
|
||||||
/// them with the PCI-enumerated nodes: a PCI root bridge (`PNP0A03`/`PNP0A08`)
|
|
||||||
/// folds onto the existing `pci_host_bridge`, and each addressed (`_ADR`) device folds onto
|
|
||||||
/// the matching PCI function (annotating it with the ACPI hardware ID (`_HID`) and nesting the
|
|
||||||
/// ACPI-only children — keyboard, RTC, … — beneath it). Namespace devices with no
|
|
||||||
/// PCI match land under a synthetic `acpi` node.
|
|
||||||
fn wireAcpiDevices(device_tree: *DeviceTree, aml_namespace: *aml.Namespace, hal: Hal) !void {
|
|
||||||
var arena = std.heap.ArenaAllocator.init(device_tree.allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
var interpreter = aml.Interpreter.init(aml_namespace, .{
|
|
||||||
.mapMmio = hal.mapMmio,
|
|
||||||
.pioRead = hal.pioRead,
|
|
||||||
.pioWrite = hal.pioWrite,
|
|
||||||
}, arena.allocator());
|
|
||||||
|
|
||||||
const acpi_root = try device_tree.addChild(device_tree.root, .unknown, "acpi");
|
|
||||||
try mirrorDevices(device_tree, aml_namespace.root, acpi_root, null, &interpreter);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mirrorDevices(device_tree: *DeviceTree, node: *aml.Node, parent_device: *device_model.Device, context: ?PciContext, interpreter: *aml.Interpreter) (error{OutOfMemory})!void {
|
|
||||||
var child = node.first_child;
|
|
||||||
while (child) |c| : (child = c.next_sibling) {
|
|
||||||
if (c.kind != .device) {
|
|
||||||
// A scope — the System Bus (\_SB), General Purpose Events (\_GPE), … —
|
|
||||||
// descend without adding a node.
|
|
||||||
try mirrorDevices(device_tree, c, parent_device, context, interpreter);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip devices the firmware reports as not present (via a device-status (`_STA`) method),
|
|
||||||
// along with their whole subtree — per the ACPI rules.
|
|
||||||
if (!devicePresent(interpreter, c)) continue;
|
|
||||||
|
|
||||||
var mirrored_device: *device_model.Device = undefined;
|
|
||||||
var child_context = context;
|
|
||||||
|
|
||||||
if (isPciRootNode(c)) {
|
|
||||||
// The PCI root bridge folds onto the generic host bridge.
|
|
||||||
mirrored_device = matchHostBridge(device_tree) orelse
|
|
||||||
try device_tree.addChild(parent_device, .acpi_device, &c.segment);
|
|
||||||
child_context = .{ .bridge = mirrored_device, .bus = 0 };
|
|
||||||
} else {
|
|
||||||
// An addressed device folds onto its matching PCI function; anything
|
|
||||||
// else becomes a fresh node under the current parent.
|
|
||||||
mirrored_device = pick: {
|
|
||||||
if (context) |pc| {
|
|
||||||
if (readAdr(c)) |adr| {
|
|
||||||
if (findPciNode(pc.bridge, pc.bus, adr)) |pnode| break :pick pnode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break :pick try device_tree.addChild(parent_device, .acpi_device, &c.segment);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
applyHid(mirrored_device, c, interpreter);
|
|
||||||
applyCrs(mirrored_device, c, interpreter);
|
|
||||||
try mirrorDevices(device_tree, c, mirrored_device, child_context, interpreter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Evaluate a device's status (`_STA`) to decide if it is present. An absent status
|
|
||||||
/// (`_STA`) means present by default; an evaluation failure is treated as present too (we'd
|
|
||||||
/// rather over-report than hide a device we couldn't introspect).
|
|
||||||
fn devicePresent(interpreter: *aml.Interpreter, node: *aml.Node) bool {
|
|
||||||
const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true;
|
|
||||||
const obj = interpreter.evaluate(sta, &.{}) catch return true;
|
|
||||||
const status = obj.asInteger() catch return true;
|
|
||||||
return (status & 0x01) != 0; // bit 0 = present
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The first PCI host bridge in the generic tree (segment 0).
|
|
||||||
fn matchHostBridge(device_tree: *DeviceTree) ?*device_model.Device {
|
|
||||||
var c = device_tree.root.first_child;
|
|
||||||
while (c) |ch| : (c = ch.next_sibling) {
|
|
||||||
if (ch.class == .pci_host_bridge) return ch;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The PCI function node under `bridge` at the address the device's address object
|
|
||||||
/// (`_ADR`) names (device/function on
|
|
||||||
/// `bus`), or null.
|
|
||||||
fn findPciNode(bridge: *device_model.Device, bus: u8, adr: u32) ?*device_model.Device {
|
|
||||||
const device: u16 = @truncate((adr >> 16) & 0x1F);
|
|
||||||
const function: u16 = @truncate(adr & 0x7);
|
|
||||||
const target: u16 = (@as(u16, bus) << 8) | (device << 3) | function;
|
|
||||||
var c = bridge.first_child;
|
|
||||||
while (c) |ch| : (c = ch.next_sibling) {
|
|
||||||
if (ch.ids.pci_bdf) |bdf| {
|
|
||||||
if (bdf == target) return ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A device's address (`_ADR`) — a static integer Name — or null.
|
|
||||||
fn readAdr(node: *aml.Node) ?u32 {
|
|
||||||
const n = aml.Namespace.childOf(node, seg4("_ADR")) orelse return null;
|
|
||||||
if (n.kind != .name) return null;
|
|
||||||
var p: usize = 0;
|
|
||||||
return @truncate(readIntObj(n.value, &p) orelse return null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a `_HID` string names a PCI(e) host bridge.
|
|
||||||
fn isPciRootHid(hid: []const u8) bool {
|
|
||||||
const id = acpi_ids.HardwareId.fromHid(hid) orelse return false;
|
|
||||||
return id == .pci_bus or id == .pci_express_root_bridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether a namespace device is a PCI(e) host bridge. A packed EISA id is decoded
|
|
||||||
/// to its string form first, so both encodings answer through the one registry.
|
|
||||||
fn isPciRootNode(node: *aml.Node) bool {
|
|
||||||
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return false;
|
|
||||||
if (hid.kind != .name or hid.value.len == 0) return false;
|
|
||||||
switch (hid.value[0]) {
|
|
||||||
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
|
|
||||||
var p: usize = 0;
|
|
||||||
const n = readIntObj(hid.value, &p) orelse return false;
|
|
||||||
var buffer: [8]u8 = undefined;
|
|
||||||
return isPciRootHid(eisaIdToStr(@truncate(n), &buffer));
|
|
||||||
},
|
|
||||||
0x0D => return isPciRootHid(cstr(hid.value[1..])),
|
|
||||||
else => return false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a device's hardware ID (`_HID`) into the generic device: an integer decodes as an EISA
|
|
||||||
/// id ("PNP0A03"), a string is taken verbatim. Handles both the common static
|
|
||||||
/// Name form and a Method form (evaluated).
|
|
||||||
fn applyHid(device: *device_model.Device, node: *aml.Node, interpreter: *aml.Interpreter) void {
|
|
||||||
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return;
|
|
||||||
if (hid.kind == .method) {
|
|
||||||
const obj = interpreter.evaluate(hid, &.{}) catch return;
|
|
||||||
switch (obj) {
|
|
||||||
.integer => |n| setEisaHid(device, @truncate(n)),
|
|
||||||
.string => |s| device.setHid(s),
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (hid.kind != .name or hid.value.len == 0) return;
|
|
||||||
const v = hid.value;
|
|
||||||
switch (v[0]) {
|
|
||||||
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
|
|
||||||
var p: usize = 0;
|
|
||||||
const n = readIntObj(v, &p) orelse return;
|
|
||||||
setEisaHid(device, @truncate(n));
|
|
||||||
},
|
|
||||||
0x0D => device.setHid(cstr(v[1..])), // StringPrefix
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setEisaHid(device: *device_model.Device, id: u32) void {
|
|
||||||
device.ids.acpi_hid = id;
|
|
||||||
var buffer: [8]u8 = undefined;
|
|
||||||
device.setHid(eisaIdToStr(id, &buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a device's current resource settings (`_CRS`). The evaluator handles both the static
|
|
||||||
/// `Buffer` form (a `Name`) and the method form uniformly, yielding the
|
|
||||||
/// ResourceTemplate bytes we then decode.
|
|
||||||
fn applyCrs(device: *device_model.Device, node: *aml.Node, interpreter: *aml.Interpreter) void {
|
|
||||||
const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return;
|
|
||||||
const obj = interpreter.evaluate(crs, &.{}) catch return;
|
|
||||||
const buffer = switch (obj) {
|
|
||||||
.buffer => |b| b,
|
|
||||||
else => return,
|
|
||||||
};
|
|
||||||
parseResourceTemplate(device, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk a ResourceTemplate byte list, adding recognised descriptors as resources.
|
|
||||||
fn parseResourceTemplate(device: *device_model.Device, bytes: []const u8) void {
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < bytes.len) {
|
|
||||||
const tag = bytes[i];
|
|
||||||
if (tag & 0x80 == 0) {
|
|
||||||
// Small descriptor: length in low 3 bits, type in bits [6:3].
|
|
||||||
const len: usize = tag & 0x07;
|
|
||||||
const body = i + 1;
|
|
||||||
if (body + len > bytes.len) break;
|
|
||||||
switch ((tag >> 3) & 0x0F) {
|
|
||||||
0x04 => if (len >= 2) { // IRQ: a 16-bit mask, one resource per set bit
|
|
||||||
const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8);
|
|
||||||
var b: usize = 0;
|
|
||||||
while (b < 16) : (b += 1) {
|
|
||||||
if (mask & (@as(u16, 1) << @intCast(b)) != 0) _ = device.addResource(.irq, b, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0x08 => if (len >= 7) { // IO port: minimum at +1, length at +6
|
|
||||||
_ = device.addResource(.io_port, rd16(bytes, body + 1), bytes[body + 6]);
|
|
||||||
},
|
|
||||||
0x09 => if (len >= 3) { // Fixed IO: base at +0, length at +2
|
|
||||||
_ = device.addResource(.io_port, rd16(bytes, body), bytes[body + 2]);
|
|
||||||
},
|
|
||||||
0x0F => break, // EndTag
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
i = body + len;
|
|
||||||
} else {
|
|
||||||
// Large descriptor: 16-bit length follows the tag.
|
|
||||||
if (i + 3 > bytes.len) break;
|
|
||||||
const len: usize = @intCast(rd16(bytes, i + 1));
|
|
||||||
const body = i + 3;
|
|
||||||
if (body + len > bytes.len) break;
|
|
||||||
switch (tag) {
|
|
||||||
0x85 => if (len >= 17) { // Memory32: minimum at +1, length at +13
|
|
||||||
_ = device.addResource(.memory, rd32(bytes, body + 1), rd32(bytes, body + 13));
|
|
||||||
},
|
|
||||||
0x86 => if (len >= 9) { // Memory32Fixed: base at +1, length at +5
|
|
||||||
_ = device.addResource(.memory, rd32(bytes, body + 1), rd32(bytes, body + 5));
|
|
||||||
},
|
|
||||||
0x89 => if (len >= 2) { // Extended IRQ: count at +1, then count u32s
|
|
||||||
const count = bytes[body + 1];
|
|
||||||
var k: usize = 0;
|
|
||||||
while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) {
|
|
||||||
_ = device.addResource(.irq, rd32(bytes, body + 2 + k * 4), 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0x87, 0x88, 0x8A => parseAddressSpace(device, tag, bytes[body .. body + len]),
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
i = body + len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Word/DWord/QWord address-space descriptors: resource type at [0], then
|
|
||||||
/// granularity/minimum/maximum/translation/length, each of width `w`.
|
|
||||||
fn parseAddressSpace(device: *device_model.Device, tag: u8, body: []const u8) void {
|
|
||||||
const w: usize = switch (tag) {
|
|
||||||
0x88 => 2, // Word
|
|
||||||
0x87 => 4, // DWord
|
|
||||||
else => 8, // QWord (0x8A)
|
|
||||||
};
|
|
||||||
if (body.len < 3 + 5 * w) return;
|
|
||||||
const minimum = readN(body, 3 + w, w);
|
|
||||||
const length = readN(body, 3 + 4 * w, w);
|
|
||||||
const kind: device_model.ResourceKind = switch (body[0]) {
|
|
||||||
0 => .memory,
|
|
||||||
1 => .io_port,
|
|
||||||
else => .bus_range,
|
|
||||||
};
|
|
||||||
_ = device.addResource(kind, minimum, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode a packed EISA id into its 7-char string (e.g. 0x030AD041 -> "PNP0A03").
|
|
||||||
fn eisaIdToStr(id: u32, buffer: *[8]u8) []const u8 {
|
|
||||||
const b0: u16 = @intCast(id & 0xFF);
|
|
||||||
const b1: u16 = @intCast((id >> 8) & 0xFF);
|
|
||||||
const b2: u8 = @truncate(id >> 16);
|
|
||||||
const b3: u8 = @truncate(id >> 24);
|
|
||||||
const mfg = (b0 << 8) | b1;
|
|
||||||
buffer[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F));
|
|
||||||
buffer[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F));
|
|
||||||
buffer[2] = '@' + @as(u8, @intCast(mfg & 0x1F));
|
|
||||||
buffer[3] = hexDigit((b2 >> 4) & 0xF);
|
|
||||||
buffer[4] = hexDigit(b2 & 0xF);
|
|
||||||
buffer[5] = hexDigit((b3 >> 4) & 0xF);
|
|
||||||
buffer[6] = hexDigit(b3 & 0xF);
|
|
||||||
return buffer[0..7];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hexDigit(n: u8) u8 {
|
|
||||||
return if (n < 10) '0' + n else 'A' + (n - 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn seg4(comptime s: *const [4:0]u8) [4]u8 {
|
|
||||||
return s[0..4].*;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cstr(bytes: []const u8) []const u8 {
|
|
||||||
const index = std.mem.indexOfScalar(u8, bytes, 0) orelse bytes.len;
|
|
||||||
return bytes[0..index];
|
|
||||||
}
|
|
||||||
|
|
||||||
const PkgLen = struct { value: usize, size: usize };
|
|
||||||
|
|
||||||
fn packageLength(bytes: []const u8, p: usize) ?PkgLen {
|
|
||||||
if (p >= bytes.len) return null;
|
|
||||||
const lead = bytes[p];
|
|
||||||
const follow: usize = lead >> 6;
|
|
||||||
if (p + 1 + follow > bytes.len) return null;
|
|
||||||
if (follow == 0) return .{ .value = lead & 0x3F, .size = 1 };
|
|
||||||
var value: usize = lead & 0x0F;
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < follow) : (i += 1) value |= @as(usize, bytes[p + 1 + i]) << @intCast(4 + i * 8);
|
|
||||||
return .{ .value = value, .size = 1 + follow };
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read an AML integer object at `p`, advancing `p` past it.
|
|
||||||
fn readIntObj(bytes: []const u8, p: *usize) ?u64 {
|
|
||||||
if (p.* >= bytes.len) return null;
|
|
||||||
const opcode = bytes[p.*];
|
|
||||||
p.* += 1;
|
|
||||||
return switch (opcode) {
|
|
||||||
0x00 => 0,
|
|
||||||
0x01 => 1,
|
|
||||||
0xFF => 0xFF,
|
|
||||||
0x0A => readLE(bytes, p, 1),
|
|
||||||
0x0B => readLE(bytes, p, 2),
|
|
||||||
0x0C => readLE(bytes, p, 4),
|
|
||||||
0x0E => readLE(bytes, p, 8),
|
|
||||||
else => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readLE(bytes: []const u8, p: *usize, n: usize) ?u64 {
|
|
||||||
if (p.* + n > bytes.len) return null;
|
|
||||||
const v = readN(bytes, p.*, n);
|
|
||||||
p.* += n;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn readN(bytes: []const u8, off: usize, n: usize) u64 {
|
|
||||||
var v: u64 = 0;
|
|
||||||
var k: usize = 0;
|
|
||||||
while (k < n and off + k < bytes.len) : (k += 1) v |= @as(u64, bytes[off + k]) << @intCast(k * 8);
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rd16(bytes: []const u8, off: usize) u64 {
|
|
||||||
return readN(bytes, off, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rd32(bytes: []const u8, off: usize) u64 {
|
|
||||||
return readN(bytes, off, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- helpers ----------------------------------------------------------------
|
// --- helpers ----------------------------------------------------------------
|
||||||
|
|
||||||
/// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero.
|
/// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero.
|
||||||
@@ -1184,58 +867,9 @@ fn readCntRegister(base: [*]align(1) const u8, len: usize, xoff: usize, legacy_o
|
|||||||
return .{ .mmio = false, .address = port, .width = width };
|
return .{ .mmio = false, .address = port, .width = width };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mapped configuration space of one PCI function (its 4 KiB ECAM page). Mapped
|
|
||||||
/// writable so BAR sizing can probe it; reads and writes both go through here.
|
|
||||||
fn pciConfigurationPtr(alloc: McfgAllocation, hal: Hal, bus: u8, device: u8, function: u8) [*]align(1) u8 {
|
|
||||||
const physical = alloc.base_address +
|
|
||||||
(@as(u64, bus - alloc.start_bus) << 20) +
|
|
||||||
(@as(u64, device) << 15) +
|
|
||||||
(@as(u64, function) << 12);
|
|
||||||
// Map the configuration page (writable, for BAR sizing) and use the virtual
|
|
||||||
// address the HAL hands back.
|
|
||||||
return @ptrFromInt(hal.mapMmio(physical, abi.page_size, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer.
|
/// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer.
|
||||||
/// x86 is little-endian and native, so an unaligned load suffices.
|
/// x86 is little-endian and native, so an unaligned load suffices.
|
||||||
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
|
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
|
||||||
const p: *align(1) const T = @ptrCast(bytes + off);
|
const p: *align(1) const T = @ptrCast(bytes + off);
|
||||||
return p.*;
|
return p.*;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a little-endian integer at `off` through a (possibly unaligned) pointer.
|
|
||||||
fn wr(comptime T: type, bytes: [*]align(1) u8, off: usize, value: T) void {
|
|
||||||
const p: *align(1) T = @ptrCast(bytes + off);
|
|
||||||
p.* = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- tests ------------------------------------------------------------------
|
|
||||||
|
|
||||||
test "eisaIdToStr decodes a packed EISA id" {
|
|
||||||
var buffer: [8]u8 = undefined;
|
|
||||||
// 0x030AD041 is the well-known encoding of "PNP0A03" (PCI root bridge).
|
|
||||||
try std.testing.expectEqualStrings("PNP0A03", eisaIdToStr(0x030AD041, &buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parseResourceTemplate extracts IO, IRQ, and fixed memory" {
|
|
||||||
// ResourceTemplate { IO(minimum 0x60, len 8), IRQ(4), Memory32Fixed(0xFED00000, 0x1000) }
|
|
||||||
const runtime = [_]u8{
|
|
||||||
0x47, 0x01, 0x60, 0x00, 0x60, 0x00, 0x01, 0x08, // small IO descriptor
|
|
||||||
0x22, 0x10, 0x00, // small IRQ descriptor (mask bit 4 -> IRQ 4)
|
|
||||||
0x86, 0x09, 0x00, 0x01, 0x00, 0x00, 0xD0, 0xFE, 0x00, 0x10, 0x00, 0x00, // Memory32Fixed
|
|
||||||
0x79, 0x00, // EndTag
|
|
||||||
};
|
|
||||||
var device = device_model.Device{};
|
|
||||||
parseResourceTemplate(&device, &runtime);
|
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(u8, 3), device.resource_count);
|
|
||||||
const rs = device.resources[0..device.resource_count];
|
|
||||||
try std.testing.expectEqual(device_model.ResourceKind.io_port, rs[0].kind);
|
|
||||||
try std.testing.expectEqual(@as(u64, 0x60), rs[0].start);
|
|
||||||
try std.testing.expectEqual(@as(u64, 8), rs[0].len);
|
|
||||||
try std.testing.expectEqual(device_model.ResourceKind.irq, rs[1].kind);
|
|
||||||
try std.testing.expectEqual(@as(u64, 4), rs[1].start);
|
|
||||||
try std.testing.expectEqual(device_model.ResourceKind.memory, rs[2].kind);
|
|
||||||
try std.testing.expectEqual(@as(u64, 0xFED00000), rs[2].start);
|
|
||||||
try std.testing.expectEqual(@as(u64, 0x1000), rs[2].len);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ const std = @import("std");
|
|||||||
const opcode = @import("opcodes.zig");
|
const opcode = @import("opcodes.zig");
|
||||||
const parser = @import("parser.zig");
|
const parser = @import("parser.zig");
|
||||||
|
|
||||||
|
/// The named AML opcode/prefix bytes (`zero_opcode`, `byte_prefix`, …). Re-exported so
|
||||||
|
/// callers that decode raw AML bytes — e.g. the acpi service reading a `_HID` integer —
|
||||||
|
/// name the opcodes instead of writing bare 0x0A/0x0B/… literals (docs/coding-standards.md).
|
||||||
|
pub const opcodes = @import("opcodes.zig");
|
||||||
|
|
||||||
pub const Namespace = @import("namespace.zig").Namespace;
|
pub const Namespace = @import("namespace.zig").Namespace;
|
||||||
pub const Node = @import("namespace.zig").Node;
|
pub const Node = @import("namespace.zig").Node;
|
||||||
pub const NodeKind = @import("namespace.zig").NodeKind;
|
pub const NodeKind = @import("namespace.zig").NodeKind;
|
||||||
@@ -50,6 +55,20 @@ pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseRes
|
|||||||
return .{ .namespace = namespace, .consumed = consumed, .total = total };
|
return .{ .namespace = namespace, .consumed = consumed, .total = total };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Count the Device objects in a parsed namespace — what the acpi service
|
||||||
|
/// (docs/m19-m20-plan.md M20) reports, and what the kernel's own parse counts
|
||||||
|
/// so the two can be checked equal across the ring-3 move.
|
||||||
|
pub fn deviceCount(namespace: *const Namespace) usize {
|
||||||
|
return countKind(namespace.root, .device);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn countKind(node: *const Node, kind: NodeKind) usize {
|
||||||
|
var n: usize = if (node.kind == kind) 1 else 0;
|
||||||
|
var c = node.first_child;
|
||||||
|
while (c) |child| : (c = child.next_sibling) n += countKind(child, kind);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
/// Look up the `\_S{state}` sleep package in a parsed namespace and return its
|
/// Look up the `\_S{state}` sleep package in a parsed namespace and return its
|
||||||
/// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
|
/// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
|
||||||
pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType {
|
pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType {
|
||||||
@@ -211,3 +230,31 @@ test "interpreter runs a method with args, arithmetic, and control flow" {
|
|||||||
const lo = try interpreter.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0
|
const lo = try interpreter.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0
|
||||||
try std.testing.expectEqual(@as(u64, 0), try lo.asInteger());
|
try std.testing.expectEqual(@as(u64, 0), try lo.asInteger());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "interpreter records Notify(device, code)" {
|
||||||
|
// Device(DEV_) { Name(_HID, 0x030AD041) } // PNP0A03-ish placeholder
|
||||||
|
// Method(TST_, 0) { Notify(DEV_, 0x80); Return(Zero) }
|
||||||
|
// Encoded: a Device holding a Name, then a Method issuing Notify on it.
|
||||||
|
const blob = [_]u8{
|
||||||
|
0x5B, 0x82, 0x0F, 0x44, 0x45, 0x56, 0x5F, // Device(DEV_) len=0x0F (pkglen + DEV_ + Name)
|
||||||
|
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0C, 0x41, 0xD0, 0x0A, 0x03, // Name(_HID, DWord 0x030AD041)
|
||||||
|
0x14, 0x0F, 0x54, 0x53, 0x54, 0x5F, 0x00, // Method(TST_, 0) len=0x0F (pkglen + TST_ + flags + body)
|
||||||
|
0x86, 0x44, 0x45, 0x56, 0x5F, 0x0A, 0x80, // Notify(DEV_, 0x80)
|
||||||
|
0xA4, 0x00, // Return(Zero)
|
||||||
|
};
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
var result = try parse(arena.allocator(), &.{&blob});
|
||||||
|
const namespace = &result.namespace;
|
||||||
|
const tst = namespace.resolve(namespace.root, false, 0, &.{.{ 'T', 'S', 'T', '_' }}) orelse return error.NoMethod;
|
||||||
|
const dev = namespace.resolve(namespace.root, false, 0, &.{.{ 'D', 'E', 'V', '_' }}) orelse return error.NoDevice;
|
||||||
|
|
||||||
|
var interpreter = Interpreter.init(namespace, .{ .mapMmio = noMap, .pioRead = noRead, .pioWrite = noWrite }, arena.allocator());
|
||||||
|
_ = try interpreter.evaluate(tst, &.{});
|
||||||
|
|
||||||
|
const events = interpreter.takeNotifications();
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), events.len);
|
||||||
|
try std.testing.expectEqual(dev, events[0].node);
|
||||||
|
try std.testing.expectEqual(@as(u64, 0x80), events[0].code);
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ const Frame = struct {
|
|||||||
/// A CreateField binding: a name that indexes into a buffer object.
|
/// A CreateField binding: a name that indexes into a buffer object.
|
||||||
const BufferField = struct { buffer: *Node, byte_off: usize, bit_width: u32 };
|
const BufferField = struct { buffer: *Node, byte_off: usize, bit_width: u32 };
|
||||||
|
|
||||||
|
/// One Notify(device, code) the interpreter executed.
|
||||||
|
pub const NotifyEvent = struct { node: *Node, code: u64 };
|
||||||
|
|
||||||
pub const Interpreter = struct {
|
pub const Interpreter = struct {
|
||||||
namespace: *Namespace,
|
namespace: *Namespace,
|
||||||
hal: Hal,
|
hal: Hal,
|
||||||
@@ -149,6 +152,11 @@ pub const Interpreter = struct {
|
|||||||
dynamic_overrides: std.AutoHashMapUnmanaged(*Node, Object) = .{},
|
dynamic_overrides: std.AutoHashMapUnmanaged(*Node, Object) = .{},
|
||||||
/// CreateField bindings active for the current evaluation.
|
/// CreateField bindings active for the current evaluation.
|
||||||
fields: std.AutoHashMapUnmanaged(*Node, BufferField) = .{},
|
fields: std.AutoHashMapUnmanaged(*Node, BufferField) = .{},
|
||||||
|
/// Notify(device, code) operations the last evaluation executed — a GPE or
|
||||||
|
/// EC handler tells the OS "look at this device" this way. Bounded; the
|
||||||
|
/// caller drains it with `takeNotifications` after `evaluate` (M21).
|
||||||
|
notify_queue: [16]NotifyEvent = undefined,
|
||||||
|
notify_count: usize = 0,
|
||||||
|
|
||||||
pub fn init(namespace: *Namespace, hal: Hal, arena: std.mem.Allocator) Interpreter {
|
pub fn init(namespace: *Namespace, hal: Hal, arena: std.mem.Allocator) Interpreter {
|
||||||
return .{ .namespace = namespace, .hal = hal, .arena = arena };
|
return .{ .namespace = namespace, .hal = hal, .arena = arena };
|
||||||
@@ -157,6 +165,7 @@ pub const Interpreter = struct {
|
|||||||
/// Evaluate a namespace object: invoke a Method, read a Name's value, or read a
|
/// Evaluate a namespace object: invoke a Method, read a Name's value, or read a
|
||||||
/// Field. Resets per-evaluation runtime state first.
|
/// Field. Resets per-evaluation runtime state first.
|
||||||
pub fn evaluate(self: *Interpreter, node: *Node, args: []const Object) Error!Object {
|
pub fn evaluate(self: *Interpreter, node: *Node, args: []const Object) Error!Object {
|
||||||
|
self.notify_count = 0;
|
||||||
self.dynamic_overrides.clearRetainingCapacity();
|
self.dynamic_overrides.clearRetainingCapacity();
|
||||||
self.fields.clearRetainingCapacity();
|
self.fields.clearRetainingCapacity();
|
||||||
return self.invoke(node, args);
|
return self.invoke(node, args);
|
||||||
@@ -267,6 +276,8 @@ pub const Interpreter = struct {
|
|||||||
},
|
},
|
||||||
opcode.to_buffer_opcode => try self.passThroughUnary(current, frame),
|
opcode.to_buffer_opcode => try self.passThroughUnary(current, frame),
|
||||||
|
|
||||||
|
opcode.notify_opcode => try self.notify(current, frame),
|
||||||
|
|
||||||
opcode.extended_opcode_prefix => try self.ext(current, frame),
|
opcode.extended_opcode_prefix => try self.ext(current, frame),
|
||||||
|
|
||||||
// CreateXField: source, index, name (bit widths differ by op)
|
// CreateXField: source, index, name (bit widths differ by op)
|
||||||
@@ -542,6 +553,36 @@ pub const Interpreter = struct {
|
|||||||
try self.storeInto(current, frame, value);
|
try self.storeInto(current, frame, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notify(SuperName, NotifyValue): resolve the named device, evaluate the
|
||||||
|
/// code, and record the pair for the caller to dispatch. AML control flow
|
||||||
|
/// continues (Notify returns nothing).
|
||||||
|
fn notify(self: *Interpreter, current: *Cursor, frame: *Frame) Error!Object {
|
||||||
|
const lead = current.peek() orelse return error.Truncated;
|
||||||
|
var target: ?*Node = null;
|
||||||
|
if (isNameStart(lead)) {
|
||||||
|
const name_path = try current.nameString();
|
||||||
|
target = self.namespace.resolve(frame.scope, name_path.rooted, name_path.parents, name_path.slice());
|
||||||
|
} else {
|
||||||
|
// A non-name SuperName (Local/Arg holding a reference).
|
||||||
|
const obj = try self.term(current, frame);
|
||||||
|
if (obj == .reference) target = obj.reference;
|
||||||
|
}
|
||||||
|
const code = try self.evaluateInteger(current, frame);
|
||||||
|
if (target) |node| {
|
||||||
|
if (self.notify_count < self.notify_queue.len) {
|
||||||
|
self.notify_queue[self.notify_count] = .{ .node = node, .code = code };
|
||||||
|
self.notify_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .uninitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Notify events the last `evaluate` produced. Valid until the next
|
||||||
|
/// `evaluate` clears the queue.
|
||||||
|
pub fn takeNotifications(self: *Interpreter) []const NotifyEvent {
|
||||||
|
return self.notify_queue[0..self.notify_count];
|
||||||
|
}
|
||||||
|
|
||||||
fn storeInto(self: *Interpreter, current: *Cursor, frame: *Frame, value: Object) Error!void {
|
fn storeInto(self: *Interpreter, current: *Cursor, frame: *Frame, value: Object) Error!void {
|
||||||
const lead = current.peek() orelse return error.Truncated;
|
const lead = current.peek() orelse return error.Truncated;
|
||||||
if (isNameStart(lead)) {
|
if (isNameStart(lead)) {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ pub const DeviceClass = enum(u32) {
|
|||||||
/// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a
|
/// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a
|
||||||
/// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`).
|
/// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`).
|
||||||
acpi_device,
|
acpi_device,
|
||||||
|
/// The ACPI tables themselves, published as one node for the user-space acpi
|
||||||
|
/// service (docs/m19-m20-plan.md M20): memory resources over the AML blobs,
|
||||||
|
/// a broad io_port grant for OperationRegion access, and the SCI interrupt.
|
||||||
|
/// The one node whose claimant is trusted to run firmware bytecode.
|
||||||
|
acpi_tables,
|
||||||
unknown,
|
unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+496
-189
@@ -7,6 +7,16 @@
|
|||||||
//! apart. Pure reference data (from the PCI spec; see https://wiki.osdev.org/PCI) — no
|
//! apart. Pure reference data (from the PCI spec; see https://wiki.osdev.org/PCI) — no
|
||||||
//! hardware access — so it is shared by kernel discovery (the device-tree dump) and any
|
//! hardware access — so it is shared by kernel discovery (the device-tree dump) and any
|
||||||
//! user-space tool (a future lspci, driver matching).
|
//! user-space tool (a future lspci, driver matching).
|
||||||
|
//!
|
||||||
|
//! The taxonomy is named, not numbered (docs/coding-standards.md, "Named values"): the
|
||||||
|
//! base class is a `BaseClass` enum, and each class with defined subclasses gets a
|
||||||
|
//! namespace holding its `SubClass` enum (and, where the spec defines them, per-subclass
|
||||||
|
//! `ProgIf` enums) — the same shape as `usb-ids.zig`. Code that *means* a specific class
|
||||||
|
//! names it (`BaseClass.serial_bus`, `serial_bus.usb.ProgIf.xhci`) rather than writing a
|
||||||
|
//! bare 0x0C/0x03/0x30. The `className`/`subclassName`/`progIfName` functions still take
|
||||||
|
//! the raw bytes a function reports in its header, because that is what hardware hands us.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
/// The three bytes of a PCI class code, unpacked from the `0xCCSSPP` value discovery
|
/// The three bytes of a PCI class code, unpacked from the `0xCCSSPP` value discovery
|
||||||
/// records in `Device.ids.pci_class` (CC = base class, SS = subclass, PP = prog-IF).
|
/// records in `Device.ids.pci_class` (CC = base class, SS = subclass, PP = prog-IF).
|
||||||
@@ -22,148 +32,465 @@ pub const ClassCode = struct {
|
|||||||
.prog_if = @intCast(packed_code & 0xFF),
|
.prog_if = @intCast(packed_code & 0xFF),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-pack the triple into the `0xCCSSPP` form. Lets code name a whole class code
|
||||||
|
/// from its parts — `pack(.{ .base = @intFromEnum(BaseClass.serial_bus), … })` —
|
||||||
|
/// instead of writing the literal 0x0C0330.
|
||||||
|
pub fn pack(self: ClassCode) u24 {
|
||||||
|
return (@as(u24, self.base) << 16) | (@as(u24, self.subclass) << 8) | self.prog_if;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Base class (config byte 0x0B). Non-exhaustive: an unlisted code is a real but
|
||||||
|
/// unnamed class, decoded as "Unknown" rather than rejected.
|
||||||
|
pub const BaseClass = enum(u8) {
|
||||||
|
unclassified = 0x00,
|
||||||
|
mass_storage = 0x01,
|
||||||
|
network = 0x02,
|
||||||
|
display = 0x03,
|
||||||
|
multimedia = 0x04,
|
||||||
|
memory = 0x05,
|
||||||
|
bridge = 0x06,
|
||||||
|
simple_communication = 0x07,
|
||||||
|
base_system_peripheral = 0x08,
|
||||||
|
input_device = 0x09,
|
||||||
|
docking_station = 0x0A,
|
||||||
|
processor = 0x0B,
|
||||||
|
serial_bus = 0x0C,
|
||||||
|
wireless = 0x0D,
|
||||||
|
intelligent = 0x0E,
|
||||||
|
satellite_communication = 0x0F,
|
||||||
|
encryption = 0x10,
|
||||||
|
signal_processing = 0x11,
|
||||||
|
processing_accelerator = 0x12,
|
||||||
|
non_essential_instrumentation = 0x13,
|
||||||
|
co_processor = 0x40,
|
||||||
|
unassigned = 0xFF,
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub fn name(self: BaseClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.unclassified => "Unclassified",
|
||||||
|
.mass_storage => "Mass Storage Controller",
|
||||||
|
.network => "Network Controller",
|
||||||
|
.display => "Display Controller",
|
||||||
|
.multimedia => "Multimedia Controller",
|
||||||
|
.memory => "Memory Controller",
|
||||||
|
.bridge => "Bridge",
|
||||||
|
.simple_communication => "Simple Communication Controller",
|
||||||
|
.base_system_peripheral => "Base System Peripheral",
|
||||||
|
.input_device => "Input Device Controller",
|
||||||
|
.docking_station => "Docking Station",
|
||||||
|
.processor => "Processor",
|
||||||
|
.serial_bus => "Serial Bus Controller",
|
||||||
|
.wireless => "Wireless Controller",
|
||||||
|
.intelligent => "Intelligent Controller",
|
||||||
|
.satellite_communication => "Satellite Communication Controller",
|
||||||
|
.encryption => "Encryption Controller",
|
||||||
|
.signal_processing => "Signal Processing Controller",
|
||||||
|
.processing_accelerator => "Processing Accelerator",
|
||||||
|
.non_essential_instrumentation => "Non-Essential Instrumentation",
|
||||||
|
.co_processor => "Co-Processor",
|
||||||
|
.unassigned => "Unassigned Class (Vendor specific)",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Per-class subclass (and prog-IF) taxonomies --------------------------------------
|
||||||
|
// One namespace per base class that has defined subclasses, named after the class. Each
|
||||||
|
// holds an exhaustive `SubClass` enum (so an unlisted code decodes to the class default,
|
||||||
|
// not a wrong name), and, where the spec assigns them, per-subclass `ProgIf` enums.
|
||||||
|
|
||||||
|
pub const mass_storage = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
scsi_bus = 0x00,
|
||||||
|
ide = 0x01,
|
||||||
|
floppy = 0x02,
|
||||||
|
ipi_bus = 0x03,
|
||||||
|
raid = 0x04,
|
||||||
|
ata = 0x05,
|
||||||
|
serial_ata = 0x06,
|
||||||
|
serial_attached_scsi = 0x07,
|
||||||
|
non_volatile_memory = 0x08,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.scsi_bus => "SCSI Bus Controller",
|
||||||
|
.ide => "IDE Controller",
|
||||||
|
.floppy => "Floppy Disk Controller",
|
||||||
|
.ipi_bus => "IPI Bus Controller",
|
||||||
|
.raid => "RAID Controller",
|
||||||
|
.ata => "ATA Controller",
|
||||||
|
.serial_ata => "Serial ATA Controller",
|
||||||
|
.serial_attached_scsi => "Serial Attached SCSI Controller",
|
||||||
|
.non_volatile_memory => "Non-Volatile Memory Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const serial_ata = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
vendor_specific = 0x00,
|
||||||
|
ahci = 0x01,
|
||||||
|
serial_storage_bus = 0x02,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.vendor_specific => "Vendor Specific Interface",
|
||||||
|
.ahci => "AHCI 1.0",
|
||||||
|
.serial_storage_bus => "Serial Storage Bus",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const non_volatile_memory = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
nvmhci = 0x01,
|
||||||
|
nvm_express = 0x02,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.nvmhci => "NVMHCI",
|
||||||
|
.nvm_express => "NVM Express",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const network = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
ethernet = 0x00,
|
||||||
|
token_ring = 0x01,
|
||||||
|
fddi = 0x02,
|
||||||
|
atm = 0x03,
|
||||||
|
isdn = 0x04,
|
||||||
|
picmg_multi_computing = 0x06,
|
||||||
|
infiniband = 0x07,
|
||||||
|
fabric = 0x08,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.ethernet => "Ethernet Controller",
|
||||||
|
.token_ring => "Token Ring Controller",
|
||||||
|
.fddi => "FDDI Controller",
|
||||||
|
.atm => "ATM Controller",
|
||||||
|
.isdn => "ISDN Controller",
|
||||||
|
.picmg_multi_computing => "PICMG 2.14 Multi Computing Controller",
|
||||||
|
.infiniband => "Infiniband Controller",
|
||||||
|
.fabric => "Fabric Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const display = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
vga_compatible = 0x00,
|
||||||
|
xga = 0x01,
|
||||||
|
three_dimensional = 0x02,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.vga_compatible => "VGA Compatible Controller",
|
||||||
|
.xga => "XGA Controller",
|
||||||
|
.three_dimensional => "3D Controller (Not VGA-Compatible)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const vga_compatible = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
vga = 0x00,
|
||||||
|
compatible_8514 = 0x01,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.vga => "VGA Controller",
|
||||||
|
.compatible_8514 => "8514-Compatible Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const multimedia = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
video = 0x00,
|
||||||
|
audio = 0x01,
|
||||||
|
telephony = 0x02,
|
||||||
|
audio_device = 0x03,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.video => "Multimedia Video Controller",
|
||||||
|
.audio => "Multimedia Audio Controller",
|
||||||
|
.telephony => "Computer Telephony Device",
|
||||||
|
.audio_device => "Audio Device",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const memory = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
ram = 0x00,
|
||||||
|
flash = 0x01,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.ram => "RAM Controller",
|
||||||
|
.flash => "Flash Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const bridge = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
host = 0x00,
|
||||||
|
isa = 0x01,
|
||||||
|
eisa = 0x02,
|
||||||
|
mca = 0x03,
|
||||||
|
pci_to_pci = 0x04,
|
||||||
|
pcmcia = 0x05,
|
||||||
|
nubus = 0x06,
|
||||||
|
cardbus = 0x07,
|
||||||
|
raceway = 0x08,
|
||||||
|
pci_to_pci_semi_transparent = 0x09,
|
||||||
|
infiniband_to_pci = 0x0A,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.host => "Host Bridge",
|
||||||
|
.isa => "ISA Bridge",
|
||||||
|
.eisa => "EISA Bridge",
|
||||||
|
.mca => "MCA Bridge",
|
||||||
|
.pci_to_pci => "PCI-to-PCI Bridge",
|
||||||
|
.pcmcia => "PCMCIA Bridge",
|
||||||
|
.nubus => "NuBus Bridge",
|
||||||
|
.cardbus => "CardBus Bridge",
|
||||||
|
.raceway => "RACEway Bridge",
|
||||||
|
.pci_to_pci_semi_transparent => "PCI-to-PCI Bridge (Semi-Transparent)",
|
||||||
|
.infiniband_to_pci => "InfiniBand-to-PCI Host Bridge",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const pci_to_pci = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
normal_decode = 0x00,
|
||||||
|
subtractive_decode = 0x01,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.normal_decode => "Normal Decode",
|
||||||
|
.subtractive_decode => "Subtractive Decode",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const simple_communication = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
serial = 0x00,
|
||||||
|
parallel = 0x01,
|
||||||
|
multiport_serial = 0x02,
|
||||||
|
modem = 0x03,
|
||||||
|
gpib = 0x04,
|
||||||
|
smart_card = 0x05,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.serial => "Serial Controller",
|
||||||
|
.parallel => "Parallel Controller",
|
||||||
|
.multiport_serial => "Multiport Serial Controller",
|
||||||
|
.modem => "Modem",
|
||||||
|
.gpib => "IEEE 488.1/2 (GPIB) Controller",
|
||||||
|
.smart_card => "Smart Card Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const serial = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
compatible_8250 = 0x00,
|
||||||
|
compatible_16450 = 0x01,
|
||||||
|
compatible_16550 = 0x02,
|
||||||
|
compatible_16650 = 0x03,
|
||||||
|
compatible_16750 = 0x04,
|
||||||
|
compatible_16850 = 0x05,
|
||||||
|
compatible_16950 = 0x06,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.compatible_8250 => "8250-Compatible (Generic XT)",
|
||||||
|
.compatible_16450 => "16450-Compatible",
|
||||||
|
.compatible_16550 => "16550-Compatible",
|
||||||
|
.compatible_16650 => "16650-Compatible",
|
||||||
|
.compatible_16750 => "16750-Compatible",
|
||||||
|
.compatible_16850 => "16850-Compatible",
|
||||||
|
.compatible_16950 => "16950-Compatible",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const base_system_peripheral = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
pic = 0x00,
|
||||||
|
dma = 0x01,
|
||||||
|
timer = 0x02,
|
||||||
|
rtc = 0x03,
|
||||||
|
pci_hot_plug = 0x04,
|
||||||
|
sd_host = 0x05,
|
||||||
|
iommu = 0x06,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.pic => "PIC",
|
||||||
|
.dma => "DMA Controller",
|
||||||
|
.timer => "Timer",
|
||||||
|
.rtc => "RTC Controller",
|
||||||
|
.pci_hot_plug => "PCI Hot-Plug Controller",
|
||||||
|
.sd_host => "SD Host Controller",
|
||||||
|
.iommu => "IOMMU",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const input_device = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
keyboard = 0x00,
|
||||||
|
digitizer_pen = 0x01,
|
||||||
|
mouse = 0x02,
|
||||||
|
scanner = 0x03,
|
||||||
|
gameport = 0x04,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.keyboard => "Keyboard Controller",
|
||||||
|
.digitizer_pen => "Digitizer Pen",
|
||||||
|
.mouse => "Mouse Controller",
|
||||||
|
.scanner => "Scanner Controller",
|
||||||
|
.gameport => "Gameport Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const serial_bus = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
firewire = 0x00,
|
||||||
|
access_bus = 0x01,
|
||||||
|
ssa = 0x02,
|
||||||
|
usb = 0x03,
|
||||||
|
fibre_channel = 0x04,
|
||||||
|
smbus = 0x05,
|
||||||
|
infiniband = 0x06,
|
||||||
|
ipmi = 0x07,
|
||||||
|
sercos = 0x08,
|
||||||
|
canbus = 0x09,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.firewire => "FireWire (IEEE 1394) Controller",
|
||||||
|
.access_bus => "ACCESS Bus Controller",
|
||||||
|
.ssa => "SSA",
|
||||||
|
.usb => "USB Controller",
|
||||||
|
.fibre_channel => "Fibre Channel",
|
||||||
|
.smbus => "SMBus Controller",
|
||||||
|
.infiniband => "InfiniBand Controller",
|
||||||
|
.ipmi => "IPMI Interface",
|
||||||
|
.sercos => "SERCOS Interface (IEC 61491)",
|
||||||
|
.canbus => "CANbus Controller",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const usb = struct {
|
||||||
|
pub const ProgIf = enum(u8) {
|
||||||
|
uhci = 0x00,
|
||||||
|
ohci = 0x10,
|
||||||
|
ehci = 0x20,
|
||||||
|
xhci = 0x30,
|
||||||
|
unspecified = 0x80,
|
||||||
|
device = 0xFE,
|
||||||
|
|
||||||
|
pub fn name(self: ProgIf) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.uhci => "UHCI Controller",
|
||||||
|
.ohci => "OHCI Controller",
|
||||||
|
.ehci => "EHCI (USB2) Controller",
|
||||||
|
.xhci => "XHCI (USB3) Controller",
|
||||||
|
.unspecified => "Unspecified",
|
||||||
|
.device => "USB Device (not a host controller)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const wireless = struct {
|
||||||
|
pub const SubClass = enum(u8) {
|
||||||
|
irda = 0x00,
|
||||||
|
consumer_ir = 0x01,
|
||||||
|
rf = 0x10,
|
||||||
|
bluetooth = 0x11,
|
||||||
|
broadband = 0x12,
|
||||||
|
ethernet_802_1a = 0x20,
|
||||||
|
ethernet_802_1b = 0x21,
|
||||||
|
|
||||||
|
pub fn name(self: SubClass) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.irda => "iRDA Compatible Controller",
|
||||||
|
.consumer_ir => "Consumer IR Controller",
|
||||||
|
.rf => "RF Controller",
|
||||||
|
.bluetooth => "Bluetooth Controller",
|
||||||
|
.broadband => "Broadband Controller",
|
||||||
|
.ethernet_802_1a => "Ethernet Controller (802.1a)",
|
||||||
|
.ethernet_802_1b => "Ethernet Controller (802.1b)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Raw-byte decoding (what a function reports in its header) -------------------------
|
||||||
|
|
||||||
|
/// The name of an exhaustive class-code enum member, or null if `value` is not one — the
|
||||||
|
/// bridge from a raw config byte to a named taxonomy above.
|
||||||
|
fn enumName(comptime Enum: type, value: u8) ?[]const u8 {
|
||||||
|
return (std.enums.fromInt(Enum, value) orelse return null).name();
|
||||||
|
}
|
||||||
|
|
||||||
/// Name of the base class (byte 0x0B), e.g. `0x06` -> "Bridge".
|
/// Name of the base class (byte 0x0B), e.g. `0x06` -> "Bridge".
|
||||||
pub fn className(base: u8) []const u8 {
|
pub fn className(base: u8) []const u8 {
|
||||||
return switch (base) {
|
return @as(BaseClass, @enumFromInt(base)).name();
|
||||||
0x00 => "Unclassified",
|
|
||||||
0x01 => "Mass Storage Controller",
|
|
||||||
0x02 => "Network Controller",
|
|
||||||
0x03 => "Display Controller",
|
|
||||||
0x04 => "Multimedia Controller",
|
|
||||||
0x05 => "Memory Controller",
|
|
||||||
0x06 => "Bridge",
|
|
||||||
0x07 => "Simple Communication Controller",
|
|
||||||
0x08 => "Base System Peripheral",
|
|
||||||
0x09 => "Input Device Controller",
|
|
||||||
0x0A => "Docking Station",
|
|
||||||
0x0B => "Processor",
|
|
||||||
0x0C => "Serial Bus Controller",
|
|
||||||
0x0D => "Wireless Controller",
|
|
||||||
0x0E => "Intelligent Controller",
|
|
||||||
0x0F => "Satellite Communication Controller",
|
|
||||||
0x10 => "Encryption Controller",
|
|
||||||
0x11 => "Signal Processing Controller",
|
|
||||||
0x12 => "Processing Accelerator",
|
|
||||||
0x13 => "Non-Essential Instrumentation",
|
|
||||||
0x40 => "Co-Processor",
|
|
||||||
0xFF => "Unassigned Class (Vendor specific)",
|
|
||||||
else => "Unknown",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name of the subclass within its base class, e.g. `(0x06, 0x01)` -> "ISA Bridge".
|
/// Name of the subclass within its base class, e.g. `(0x06, 0x01)` -> "ISA Bridge".
|
||||||
/// Subclass `0x80` is "Other" by PCI convention; anything unlisted is "Unknown".
|
/// Subclass `0x80` is "Other" by PCI convention; anything unlisted is "Unknown".
|
||||||
pub fn subclassName(base: u8, subclass: u8) []const u8 {
|
pub fn subclassName(base: u8, subclass: u8) []const u8 {
|
||||||
return switch (base) {
|
const named: ?[]const u8 = switch (@as(BaseClass, @enumFromInt(base))) {
|
||||||
0x01 => switch (subclass) {
|
.mass_storage => enumName(mass_storage.SubClass, subclass),
|
||||||
0x00 => "SCSI Bus Controller",
|
.network => enumName(network.SubClass, subclass),
|
||||||
0x01 => "IDE Controller",
|
.display => enumName(display.SubClass, subclass),
|
||||||
0x02 => "Floppy Disk Controller",
|
.multimedia => enumName(multimedia.SubClass, subclass),
|
||||||
0x03 => "IPI Bus Controller",
|
.memory => enumName(memory.SubClass, subclass),
|
||||||
0x04 => "RAID Controller",
|
.bridge => enumName(bridge.SubClass, subclass),
|
||||||
0x05 => "ATA Controller",
|
.simple_communication => enumName(simple_communication.SubClass, subclass),
|
||||||
0x06 => "Serial ATA Controller",
|
.base_system_peripheral => enumName(base_system_peripheral.SubClass, subclass),
|
||||||
0x07 => "Serial Attached SCSI Controller",
|
.input_device => enumName(input_device.SubClass, subclass),
|
||||||
0x08 => "Non-Volatile Memory Controller",
|
.serial_bus => enumName(serial_bus.SubClass, subclass),
|
||||||
else => defaultSubclass(subclass),
|
.wireless => enumName(wireless.SubClass, subclass),
|
||||||
},
|
else => null,
|
||||||
0x02 => switch (subclass) {
|
|
||||||
0x00 => "Ethernet Controller",
|
|
||||||
0x01 => "Token Ring Controller",
|
|
||||||
0x02 => "FDDI Controller",
|
|
||||||
0x03 => "ATM Controller",
|
|
||||||
0x04 => "ISDN Controller",
|
|
||||||
0x06 => "PICMG 2.14 Multi Computing Controller",
|
|
||||||
0x07 => "Infiniband Controller",
|
|
||||||
0x08 => "Fabric Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x03 => switch (subclass) {
|
|
||||||
0x00 => "VGA Compatible Controller",
|
|
||||||
0x01 => "XGA Controller",
|
|
||||||
0x02 => "3D Controller (Not VGA-Compatible)",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x04 => switch (subclass) {
|
|
||||||
0x00 => "Multimedia Video Controller",
|
|
||||||
0x01 => "Multimedia Audio Controller",
|
|
||||||
0x02 => "Computer Telephony Device",
|
|
||||||
0x03 => "Audio Device",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x05 => switch (subclass) {
|
|
||||||
0x00 => "RAM Controller",
|
|
||||||
0x01 => "Flash Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x06 => switch (subclass) {
|
|
||||||
0x00 => "Host Bridge",
|
|
||||||
0x01 => "ISA Bridge",
|
|
||||||
0x02 => "EISA Bridge",
|
|
||||||
0x03 => "MCA Bridge",
|
|
||||||
0x04 => "PCI-to-PCI Bridge",
|
|
||||||
0x05 => "PCMCIA Bridge",
|
|
||||||
0x06 => "NuBus Bridge",
|
|
||||||
0x07 => "CardBus Bridge",
|
|
||||||
0x08 => "RACEway Bridge",
|
|
||||||
0x09 => "PCI-to-PCI Bridge (Semi-Transparent)",
|
|
||||||
0x0A => "InfiniBand-to-PCI Host Bridge",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x07 => switch (subclass) {
|
|
||||||
0x00 => "Serial Controller",
|
|
||||||
0x01 => "Parallel Controller",
|
|
||||||
0x02 => "Multiport Serial Controller",
|
|
||||||
0x03 => "Modem",
|
|
||||||
0x04 => "IEEE 488.1/2 (GPIB) Controller",
|
|
||||||
0x05 => "Smart Card Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x08 => switch (subclass) {
|
|
||||||
0x00 => "PIC",
|
|
||||||
0x01 => "DMA Controller",
|
|
||||||
0x02 => "Timer",
|
|
||||||
0x03 => "RTC Controller",
|
|
||||||
0x04 => "PCI Hot-Plug Controller",
|
|
||||||
0x05 => "SD Host Controller",
|
|
||||||
0x06 => "IOMMU",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x09 => switch (subclass) {
|
|
||||||
0x00 => "Keyboard Controller",
|
|
||||||
0x01 => "Digitizer Pen",
|
|
||||||
0x02 => "Mouse Controller",
|
|
||||||
0x03 => "Scanner Controller",
|
|
||||||
0x04 => "Gameport Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x0C => switch (subclass) {
|
|
||||||
0x00 => "FireWire (IEEE 1394) Controller",
|
|
||||||
0x01 => "ACCESS Bus Controller",
|
|
||||||
0x02 => "SSA",
|
|
||||||
0x03 => "USB Controller",
|
|
||||||
0x04 => "Fibre Channel",
|
|
||||||
0x05 => "SMBus Controller",
|
|
||||||
0x06 => "InfiniBand Controller",
|
|
||||||
0x07 => "IPMI Interface",
|
|
||||||
0x08 => "SERCOS Interface (IEC 61491)",
|
|
||||||
0x09 => "CANbus Controller",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
0x0D => switch (subclass) {
|
|
||||||
0x00 => "iRDA Compatible Controller",
|
|
||||||
0x01 => "Consumer IR Controller",
|
|
||||||
0x10 => "RF Controller",
|
|
||||||
0x11 => "Bluetooth Controller",
|
|
||||||
0x12 => "Broadband Controller",
|
|
||||||
0x20 => "Ethernet Controller (802.1a)",
|
|
||||||
0x21 => "Ethernet Controller (802.1b)",
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
},
|
|
||||||
else => defaultSubclass(subclass),
|
|
||||||
};
|
};
|
||||||
|
return named orelse defaultSubclass(subclass);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn defaultSubclass(subclass: u8) []const u8 {
|
fn defaultSubclass(subclass: u8) []const u8 {
|
||||||
@@ -175,68 +502,34 @@ fn defaultSubclass(subclass: u8) []const u8 {
|
|||||||
/// Returns "" when the prog-IF carries no standard meaning for this class/subclass —
|
/// Returns "" when the prog-IF carries no standard meaning for this class/subclass —
|
||||||
/// callers just print the hex byte in that case.
|
/// callers just print the hex byte in that case.
|
||||||
pub fn progIfName(base: u8, subclass: u8, prog_if: u8) []const u8 {
|
pub fn progIfName(base: u8, subclass: u8, prog_if: u8) []const u8 {
|
||||||
return switch (base) {
|
const named: ?[]const u8 = switch (@as(BaseClass, @enumFromInt(base))) {
|
||||||
0x01 => switch (subclass) {
|
.mass_storage => switch (std.enums.fromInt(mass_storage.SubClass, subclass) orelse return "") {
|
||||||
0x06 => switch (prog_if) { // Serial ATA
|
.serial_ata => enumName(mass_storage.serial_ata.ProgIf, prog_if),
|
||||||
0x00 => "Vendor Specific Interface",
|
.non_volatile_memory => enumName(mass_storage.non_volatile_memory.ProgIf, prog_if),
|
||||||
0x01 => "AHCI 1.0",
|
else => null,
|
||||||
0x02 => "Serial Storage Bus",
|
|
||||||
else => "",
|
|
||||||
},
|
},
|
||||||
0x08 => switch (prog_if) { // Non-Volatile Memory
|
.display => switch (std.enums.fromInt(display.SubClass, subclass) orelse return "") {
|
||||||
0x01 => "NVMHCI",
|
.vga_compatible => enumName(display.vga_compatible.ProgIf, prog_if),
|
||||||
0x02 => "NVM Express",
|
else => null,
|
||||||
else => "",
|
|
||||||
},
|
},
|
||||||
else => "",
|
.bridge => switch (std.enums.fromInt(bridge.SubClass, subclass) orelse return "") {
|
||||||
|
.pci_to_pci => enumName(bridge.pci_to_pci.ProgIf, prog_if),
|
||||||
|
else => null,
|
||||||
},
|
},
|
||||||
0x03 => switch (subclass) {
|
.simple_communication => switch (std.enums.fromInt(simple_communication.SubClass, subclass) orelse return "") {
|
||||||
0x00 => switch (prog_if) { // VGA Compatible
|
.serial => enumName(simple_communication.serial.ProgIf, prog_if),
|
||||||
0x00 => "VGA Controller",
|
else => null,
|
||||||
0x01 => "8514-Compatible Controller",
|
|
||||||
else => "",
|
|
||||||
},
|
},
|
||||||
else => "",
|
.serial_bus => switch (std.enums.fromInt(serial_bus.SubClass, subclass) orelse return "") {
|
||||||
|
.usb => enumName(serial_bus.usb.ProgIf, prog_if),
|
||||||
|
else => null,
|
||||||
},
|
},
|
||||||
0x06 => switch (subclass) {
|
else => null,
|
||||||
0x04 => switch (prog_if) { // PCI-to-PCI Bridge
|
|
||||||
0x00 => "Normal Decode",
|
|
||||||
0x01 => "Subtractive Decode",
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
0x07 => switch (subclass) {
|
|
||||||
0x00 => switch (prog_if) { // Serial Controller
|
|
||||||
0x00 => "8250-Compatible (Generic XT)",
|
|
||||||
0x01 => "16450-Compatible",
|
|
||||||
0x02 => "16550-Compatible",
|
|
||||||
0x03 => "16650-Compatible",
|
|
||||||
0x04 => "16750-Compatible",
|
|
||||||
0x05 => "16850-Compatible",
|
|
||||||
0x06 => "16950-Compatible",
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
0x0C => switch (subclass) {
|
|
||||||
0x03 => switch (prog_if) { // USB Controller
|
|
||||||
0x00 => "UHCI Controller",
|
|
||||||
0x10 => "OHCI Controller",
|
|
||||||
0x20 => "EHCI (USB2) Controller",
|
|
||||||
0x30 => "XHCI (USB3) Controller",
|
|
||||||
0x80 => "Unspecified",
|
|
||||||
0xFE => "USB Device (not a host controller)",
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
else => "",
|
|
||||||
},
|
|
||||||
else => "",
|
|
||||||
};
|
};
|
||||||
|
return named orelse "";
|
||||||
}
|
}
|
||||||
|
|
||||||
test "decodes the common class codes" {
|
test "decodes the common class codes" {
|
||||||
const std = @import("std");
|
|
||||||
const eq = std.testing.expectEqualStrings;
|
const eq = std.testing.expectEqualStrings;
|
||||||
|
|
||||||
const isa = ClassCode.unpack(0x06_01_00);
|
const isa = ClassCode.unpack(0x06_01_00);
|
||||||
@@ -251,11 +544,25 @@ test "decodes the common class codes" {
|
|||||||
try eq("AHCI 1.0", progIfName(ahci.base, ahci.subclass, ahci.prog_if));
|
try eq("AHCI 1.0", progIfName(ahci.base, ahci.subclass, ahci.prog_if));
|
||||||
|
|
||||||
const xhci = ClassCode.unpack(0x0C_03_30);
|
const xhci = ClassCode.unpack(0x0C_03_30);
|
||||||
|
try eq("Serial Bus Controller", className(xhci.base));
|
||||||
try eq("USB Controller", subclassName(xhci.base, xhci.subclass));
|
try eq("USB Controller", subclassName(xhci.base, xhci.subclass));
|
||||||
try eq("XHCI (USB3) Controller", progIfName(xhci.base, xhci.subclass, xhci.prog_if));
|
try eq("XHCI (USB3) Controller", progIfName(xhci.base, xhci.subclass, xhci.prog_if));
|
||||||
|
}
|
||||||
|
|
||||||
// Unknowns and the "Other" convention.
|
test "unlisted codes fall back without a wrong name" {
|
||||||
try eq("Other", subclassName(0x02, 0x80));
|
const eq = std.testing.expectEqualStrings;
|
||||||
try eq("Unknown", subclassName(0x06, 0x7E));
|
try eq("Unknown", className(0x77)); // no such base class
|
||||||
try eq("", progIfName(0x06, 0x00, 0x00)); // host bridge: prog-IF has no standard name
|
try eq("Other", subclassName(0x01, 0x80)); // 0x80 is the PCI "Other" convention
|
||||||
|
try eq("Unknown", subclassName(0x01, 0x7A)); // unlisted mass-storage subclass
|
||||||
|
try eq("", progIfName(0x01, 0x06, 0x7F)); // no standard SATA prog-IF for 0x7F
|
||||||
|
try eq("", progIfName(0x02, 0x00, 0x00)); // class with no prog-IF taxonomy at all
|
||||||
|
}
|
||||||
|
|
||||||
|
test "named parts pack to the raw triple" {
|
||||||
|
const xhci = ClassCode{
|
||||||
|
.base = @intFromEnum(BaseClass.serial_bus),
|
||||||
|
.subclass = @intFromEnum(serial_bus.SubClass.usb),
|
||||||
|
.prog_if = @intFromEnum(serial_bus.usb.ProgIf.xhci),
|
||||||
|
};
|
||||||
|
try std.testing.expectEqual(@as(u24, 0x0C_03_30), xhci.pack());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ pub fn platformInformation() PlatformInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// AML parse integrity/diagnostics (namespace node count, bytes consumed).
|
/// AML parse integrity/diagnostics (namespace node count, bytes consumed).
|
||||||
|
/// The number of Device objects in the kernel's own AML namespace, or 0 if the
|
||||||
|
/// parse produced none — the `acpi-parse` test compares the ring-3 service's
|
||||||
|
/// count against this.
|
||||||
|
pub fn amlDeviceCount() usize {
|
||||||
|
return acpi.amlDeviceCount();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn amlStats() AmlStats {
|
pub fn amlStats() AmlStats {
|
||||||
return acpi.aml_stats;
|
return acpi.aml_stats;
|
||||||
}
|
}
|
||||||
@@ -72,7 +79,8 @@ pub fn discover(
|
|||||||
var device_tree = try DeviceTree.init(allocator);
|
var device_tree = try DeviceTree.init(allocator);
|
||||||
|
|
||||||
if (boot_information.acpi_rsdp != 0) {
|
if (boot_information.acpi_rsdp != 0) {
|
||||||
try acpi.discover(boot_information.acpi_rsdp, &device_tree, hal);
|
const memory_regions = @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.memory_map.regions)))[0..boot_information.memory_map.len];
|
||||||
|
try acpi.discover(boot_information.acpi_rsdp, memory_regions, &device_tree, hal);
|
||||||
} else {
|
} else {
|
||||||
// No ACPI RSDP. A device-tree boot would parse its blob here; today that
|
// No ACPI RSDP. A device-tree boot would parse its blob here; today that
|
||||||
// path is a stub, so this reports the machine described itself no way we
|
// path is a stub, so this reports the machine described itself no way we
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
//! /system/drivers/pci-bus — the PCI bus driver: enumeration moved out of ring 0
|
||||||
|
//! (docs/m19-m20-plan.md, M19). The device manager matches the `pci_host_bridge`
|
||||||
|
//! node and spawns one instance per bridge, the bridge's device id as argv[1] —
|
||||||
|
//! the same per-device contract as usb-xhci-bus.
|
||||||
|
//!
|
||||||
|
//! M19.1 (this increment): claim the bridge, map its ECAM window (resource 0;
|
||||||
|
//! the bus range and the MMIO apertures follow it), walk every
|
||||||
|
//! bus/device/function config header, and log what the walk finds — ending
|
||||||
|
//! with "pci-bus: N functions found", which the `pci-scan` scenario compares
|
||||||
|
//! against the kernel's own enumeration. Registration and reports (M19.2), and
|
||||||
|
//! the kernel walk's retirement (M19.3), build on this proven-equivalent scan.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
const protocol = runtime.device_manager_protocol;
|
||||||
|
const device = runtime.device;
|
||||||
|
const pci_class = @import("pci-class");
|
||||||
|
|
||||||
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
|
var line: [128]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log a discovered function with its (class / subclass / prog-IF) triple decoded
|
||||||
|
/// to human names — the boot-log breadcrumb that says *what* the hardware is, so
|
||||||
|
/// "class 0x01 (Mass Storage Controller) subclass 0x06 (Serial ATA Controller)
|
||||||
|
/// progif 0x01 (AHCI 1.0)" reads straight off the log when writing a new driver.
|
||||||
|
/// A dedicated wider buffer than `writeLine`'s, since the decoded names are long.
|
||||||
|
fn logFunction(bus: u64, dev: u64, function: u64, class_triple: u32) void {
|
||||||
|
const cc = pci_class.ClassCode.unpack(@truncate(class_triple));
|
||||||
|
const pif = pci_class.progIfName(cc.base, cc.subclass, cc.prog_if);
|
||||||
|
var line: [200]u8 = undefined;
|
||||||
|
const text = if (pif.len != 0)
|
||||||
|
std.fmt.bufPrint(&line, "pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2} ({s})\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if, pif }) catch return
|
||||||
|
else
|
||||||
|
std.fmt.bufPrint(&line, "pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2}\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if }) catch return;
|
||||||
|
_ = runtime.system.write(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bridge_id: u64 = protocol.no_device;
|
||||||
|
var ecam_base: usize = 0;
|
||||||
|
var ecam_physical: u64 = 0;
|
||||||
|
var start_bus: u64 = 0;
|
||||||
|
var bus_count: u64 = 0;
|
||||||
|
var manager_handle: runtime.ipc.Handle = 0;
|
||||||
|
|
||||||
|
/// One aligned 32-bit read from a function's configuration space.
|
||||||
|
fn configRead(bus: u64, dev: u64, function: u64, offset: u64) u32 {
|
||||||
|
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
|
||||||
|
const register: *volatile u32 = @ptrFromInt(address);
|
||||||
|
return register.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configWrite(bus: u64, dev: u64, function: u64, offset: u64, value: u32) void {
|
||||||
|
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
|
||||||
|
const register: *volatile u32 = @ptrFromInt(address);
|
||||||
|
register.* = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configRead16(bus: u64, dev: u64, function: u64, offset: u64) u16 {
|
||||||
|
const word = configRead(bus, dev, function, offset & ~@as(u64, 3));
|
||||||
|
return @truncate(word >> @intCast((offset & 3) * 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configWrite16(bus: u64, dev: u64, function: u64, offset: u64, value: u16) void {
|
||||||
|
const aligned = offset & ~@as(u64, 3);
|
||||||
|
const shift: u5 = @intCast((offset & 3) * 8);
|
||||||
|
const word = configRead(bus, dev, function, aligned);
|
||||||
|
const mask = @as(u32, 0xFFFF) << shift;
|
||||||
|
configWrite(bus, dev, function, aligned, (word & ~mask) | (@as(u32, value) << shift));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claim the bridge, map the ECAM, hello the manager, then scan.
|
||||||
|
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||||
|
_ = endpoint;
|
||||||
|
if (!device.claim(bridge_id)) {
|
||||||
|
writeLine("pci-bus: unable to claim bridge device {d}\n", .{bridge_id});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
|
_ = runtime.system.write("pci-bus: out of memory\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const total = device.enumerate(buffer);
|
||||||
|
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||||
|
if (d.id == bridge_id) break d;
|
||||||
|
} else {
|
||||||
|
writeLine("pci-bus: device {d} not in the device tree\n", .{bridge_id});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// Resource 0 is the ECAM window (1 MiB of config space per bus); the bus
|
||||||
|
// range rides beside it. The MMIO apertures (M19.0) come after both.
|
||||||
|
if (descriptor.resource_count < 2 or descriptor.resources[0].kind != @intFromEnum(device.ResourceKind.memory)) {
|
||||||
|
_ = runtime.system.write("pci-bus: bridge has no ECAM window\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bus_range = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| {
|
||||||
|
if (resource.kind == @intFromEnum(device.ResourceKind.bus_range)) break resource;
|
||||||
|
} else {
|
||||||
|
_ = runtime.system.write("pci-bus: bridge has no bus range\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
start_bus = bus_range.start;
|
||||||
|
bus_count = bus_range.len;
|
||||||
|
ecam_physical = descriptor.resources[0].start;
|
||||||
|
ecam_base = device.mmioMap(bridge_id, 0) orelse {
|
||||||
|
_ = runtime.system.write("pci-bus: ECAM mmio_map failed\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The handshake, then the scan (reports join in M19.2).
|
||||||
|
var manager: ?runtime.ipc.Handle = null;
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (manager == null and tries < 100) : (tries += 1) {
|
||||||
|
manager = runtime.ipc.lookup(.device_manager);
|
||||||
|
if (manager == null) runtime.system.sleep(20);
|
||||||
|
}
|
||||||
|
const h = manager orelse {
|
||||||
|
_ = runtime.system.write("pci-bus: no device manager to hello\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = bridge_id };
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
|
||||||
|
_ = runtime.system.write("pci-bus: hello call failed\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
|
||||||
|
_ = runtime.system.write("pci-bus: hello refused\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
manager_handle = h;
|
||||||
|
|
||||||
|
scan();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The brute-force walk the kernel does today, from ring 3: every bus in the
|
||||||
|
/// range, 32 devices, 8 functions; vendor id FFFFh means nothing decodes there,
|
||||||
|
/// and only multifunction devices get their functions 1..7 probed.
|
||||||
|
fn scan() void {
|
||||||
|
var found: u32 = 0;
|
||||||
|
var bus: u64 = start_bus;
|
||||||
|
while (bus < start_bus + bus_count) : (bus += 1) {
|
||||||
|
var dev: u64 = 0;
|
||||||
|
while (dev < 32) : (dev += 1) {
|
||||||
|
const first = configRead(bus, dev, 0, 0);
|
||||||
|
if (first & 0xFFFF == 0xFFFF) continue;
|
||||||
|
const multifunction = (configRead(bus, dev, 0, 0x0C) >> 16) & 0x80 != 0;
|
||||||
|
var function: u64 = 0;
|
||||||
|
while (function < 8) : (function += 1) {
|
||||||
|
if (function != 0 and !multifunction) break;
|
||||||
|
const vendor_device = configRead(bus, dev, function, 0);
|
||||||
|
if (vendor_device & 0xFFFF == 0xFFFF) continue;
|
||||||
|
const class_revision = configRead(bus, dev, function, 0x08);
|
||||||
|
found += 1;
|
||||||
|
logFunction(bus, dev, function, class_revision >> 8);
|
||||||
|
registerAndReport(bus, dev, function, class_revision >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeLine("pci-bus: {d} functions found\n", .{found});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register one function under the bridge and report it to the manager. The
|
||||||
|
/// descriptor mirrors the kernel's own recording byte for byte — config slice
|
||||||
|
/// as resource 0, then the sized BARs — so during coexistence the idempotent
|
||||||
|
/// device_register (M19.0) returns the kernel's existing node id rather than
|
||||||
|
/// growing a duplicate, and the report carries the id drivers already use.
|
||||||
|
fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void {
|
||||||
|
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
|
||||||
|
descriptor.class = @intFromEnum(device.DeviceClass.pci_device);
|
||||||
|
descriptor.pci_class = class_triple;
|
||||||
|
descriptor.resources[0] = .{
|
||||||
|
.kind = @intFromEnum(device.ResourceKind.memory),
|
||||||
|
.start = ecam_physical + (((bus - start_bus) << 20) | (dev << 15) | (function << 12)),
|
||||||
|
.len = 4096,
|
||||||
|
};
|
||||||
|
descriptor.resource_count = 1;
|
||||||
|
|
||||||
|
// The standard BAR-sizing probe, exactly as the kernel does it: decode off,
|
||||||
|
// write all-ones, read the writable mask back, restore. Header type 0 only.
|
||||||
|
const header_type = (configRead(bus, dev, function, 0x0C) >> 16) & 0x7F;
|
||||||
|
if (header_type == 0) {
|
||||||
|
const command = configRead16(bus, dev, function, 0x04);
|
||||||
|
configWrite16(bus, dev, function, 0x04, command & ~@as(u16, 0b11));
|
||||||
|
var i: u64 = 0;
|
||||||
|
while (i < 6) : (i += 1) {
|
||||||
|
if (descriptor.resource_count >= 8) break;
|
||||||
|
const off = 0x10 + i * 4;
|
||||||
|
const original = configRead(bus, dev, function, off);
|
||||||
|
if (original == 0) continue;
|
||||||
|
const slot: usize = @intCast(descriptor.resource_count);
|
||||||
|
if (original & 1 != 0) {
|
||||||
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||||
|
const readback = configRead(bus, dev, function, off);
|
||||||
|
configWrite(bus, dev, function, off, original);
|
||||||
|
const mask = readback & 0xFFFF_FFFC;
|
||||||
|
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
|
||||||
|
if (size == 0) continue; // unimplemented BAR — nothing to register
|
||||||
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.io_port), .start = original & 0xFFFF_FFFC, .len = size };
|
||||||
|
descriptor.resource_count += 1;
|
||||||
|
} else if ((original >> 1) & 0x3 == 2) {
|
||||||
|
const original_high = configRead(bus, dev, function, off + 4);
|
||||||
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||||
|
configWrite(bus, dev, function, off + 4, 0xFFFF_FFFF);
|
||||||
|
const lo = configRead(bus, dev, function, off);
|
||||||
|
const hi = configRead(bus, dev, function, off + 4);
|
||||||
|
configWrite(bus, dev, function, off, original);
|
||||||
|
configWrite(bus, dev, function, off + 4, original_high);
|
||||||
|
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
|
||||||
|
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
|
||||||
|
i += 1; // consumed the high half regardless
|
||||||
|
if (size == 0) continue;
|
||||||
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = (@as(u64, original_high) << 32) | (original & 0xFFFF_FFF0), .len = size };
|
||||||
|
descriptor.resource_count += 1;
|
||||||
|
} else {
|
||||||
|
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||||
|
const readback = configRead(bus, dev, function, off);
|
||||||
|
configWrite(bus, dev, function, off, original);
|
||||||
|
const mask = readback & 0xFFFF_FFF0;
|
||||||
|
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
|
||||||
|
if (size == 0) continue;
|
||||||
|
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = original & 0xFFFF_FFF0, .len = size };
|
||||||
|
descriptor.resource_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configWrite16(bus, dev, function, 0x04, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
const registered = device.register(bridge_id, &descriptor) orelse {
|
||||||
|
writeLine("pci-bus: register refused for {d}:{d}.{d}\n", .{ bus, dev, function });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const report = protocol.ChildAdded{
|
||||||
|
.parent = bridge_id,
|
||||||
|
.bus_address = (bus << 8) | (dev << 3) | function,
|
||||||
|
.identity = class_triple,
|
||||||
|
.device_id = registered,
|
||||||
|
};
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
_ = runtime.ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch {
|
||||||
|
writeLine("pci-bus: child report for {d}:{d}.{d} failed\n", .{ bus, dev, function });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
_ = message;
|
||||||
|
_ = reply;
|
||||||
|
_ = sender;
|
||||||
|
_ = capability;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
const argument = init.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
|
||||||
|
bridge_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||||
|
writeLine("pci-bus: malformed bridge device id '{s}'\n", .{argument});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
runtime.service.run(protocol.message_maximum, .{
|
||||||
|
.init = initialise,
|
||||||
|
.on_message = onMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
@@ -72,17 +72,17 @@ fn modifierWord(modifiers: scancode.ModifierSnapshot) u32 {
|
|||||||
pub fn main(init: runtime.process.Init) void {
|
pub fn main(init: runtime.process.Init) void {
|
||||||
const hid = init.arguments.get(1).?;
|
const hid = init.arguments.get(1).?;
|
||||||
if (hid.len == 0) {
|
if (hid.len == 0) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: no HID argument\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no HID argument\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeLine("system/drivers/ps2-bus/keyboard: starting for hid {s}\n", .{hid});
|
writeLine("/system/drivers/ps2-bus/keyboard: starting for hid {s}\n", .{hid});
|
||||||
|
|
||||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: out of memory\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: out of memory\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
||||||
writeLine("system/drivers/ps2-bus/keyboard: no device for hid {s}\n", .{hid});
|
writeLine("/system/drivers/ps2-bus/keyboard: no device for hid {s}\n", .{hid});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,38 +90,38 @@ pub fn main(init: runtime.process.Init) void {
|
|||||||
// absent (as today) it defaults to us.
|
// absent (as today) it defaults to us.
|
||||||
const layout_name = init.arguments.get(2) orelse "us";
|
const layout_name = init.arguments.get(2) orelse "us";
|
||||||
const layout = xkb.byName(layout_name) orelse xkb.us;
|
const layout = xkb.byName(layout_name) orelse xkb.us;
|
||||||
writeLine("system/drivers/ps2-bus/keyboard: layout {s}\n", .{layout.name});
|
writeLine("/system/drivers/ps2-bus/keyboard: layout {s}\n", .{layout.name});
|
||||||
|
|
||||||
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
||||||
// keyboard sends (it owns the controller; we own the decoding).
|
// keyboard sends (it owns the controller; we own the decoding).
|
||||||
const bus = lookupBus() orelse {
|
const bus = lookupBus() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ps2-bus service unavailable\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: ps2-bus service unavailable\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: no endpoint\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no endpoint\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.keyboard) };
|
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.keyboard) };
|
||||||
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||||
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: attach call failed\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: attach call failed\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (attached.len < @sizeOf(ps2.AttachReply) or
|
if (attached.len < @sizeOf(ps2.AttachReply) or
|
||||||
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
|
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
|
||||||
{
|
{
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: attach refused\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: attach refused\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast keyboard events through the input service so programs can listen
|
// Broadcast keyboard events through the input service so programs can listen
|
||||||
// for them (docs/input.md).
|
// for them (docs/input.md).
|
||||||
var source = runtime.input.connectSource() orelse {
|
var source = runtime.input.connectSource() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: input service unavailable\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: input service unavailable\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/keyboard: ok\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: ok\n");
|
||||||
|
|
||||||
var decoder = scancode.Decoder{};
|
var decoder = scancode.Decoder{};
|
||||||
var state = scancode.KeyboardState{};
|
var state = scancode.KeyboardState{};
|
||||||
|
|||||||
@@ -51,50 +51,50 @@ pub fn main(init: runtime.process.Init) void {
|
|||||||
const hid = init.arguments.get(1).?;
|
const hid = init.arguments.get(1).?;
|
||||||
|
|
||||||
if (hid.len == 0) {
|
if (hid.len == 0) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: no HID argument\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: no HID argument\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
writeLine("system/drivers/ps2-bus/mouse: starting for hid {s}\n", .{hid});
|
writeLine("/system/drivers/ps2-bus/mouse: starting for hid {s}\n", .{hid});
|
||||||
|
|
||||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: out of memory\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: out of memory\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
||||||
writeLine("system/drivers/ps2-bus/mouse: no device for hid {s}\n", .{hid});
|
writeLine("/system/drivers/ps2-bus/mouse: no device for hid {s}\n", .{hid});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
||||||
// mouse sends (it owns the controller; we own the decoding).
|
// mouse sends (it owns the controller; we own the decoding).
|
||||||
const bus = lookupBus() orelse {
|
const bus = lookupBus() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: ps2-bus service unavailable\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: ps2-bus service unavailable\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: no endpoint\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: no endpoint\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.mouse) };
|
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.mouse) };
|
||||||
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||||
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: attach call failed\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: attach call failed\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (attached.len < @sizeOf(ps2.AttachReply) or
|
if (attached.len < @sizeOf(ps2.AttachReply) or
|
||||||
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
|
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
|
||||||
{
|
{
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: attach refused\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: attach refused\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast mouse events through the input service so programs can listen
|
// Broadcast mouse events through the input service so programs can listen
|
||||||
// for them (docs/input.md).
|
// for them (docs/input.md).
|
||||||
var source = runtime.input.connectSource() orelse {
|
var source = runtime.input.connectSource() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: input service unavailable\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: input service unavailable\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus/mouse: ok\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: ok\n");
|
||||||
|
|
||||||
var assembler = mouse_packet.Assembler{};
|
var assembler = mouse_packet.Assembler{};
|
||||||
var buttons: u32 = 0;
|
var buttons: u32 = 0;
|
||||||
|
|||||||
@@ -30,19 +30,19 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
|||||||
/// attaches, or null if nothing was spawned.
|
/// attaches, or null if nothing was spawned.
|
||||||
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) ?ps2.DeviceType {
|
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) ?ps2.DeviceType {
|
||||||
const device_type = controller.identifyDevice(port) orelse {
|
const device_type = controller.identifyDevice(port) orelse {
|
||||||
writeLine("system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
|
writeLine("/system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
const driver_name = device_type.driverName() orelse {
|
const driver_name = device_type.driverName() orelse {
|
||||||
writeLine("system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
|
writeLine("/system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
const hid = device_type.hid() orelse "";
|
const hid = device_type.hid() orelse "";
|
||||||
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
|
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
|
||||||
writeLine("system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
|
writeLine("/system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
|
||||||
return device_type;
|
return device_type;
|
||||||
}
|
}
|
||||||
writeLine("system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
writeLine("/system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ fn handleAttach(message: []const u8, got: ipc.Received, out: []u8) usize {
|
|||||||
const device_type = maybe_type orelse continue;
|
const device_type = maybe_type orelse continue;
|
||||||
if (@intFromEnum(device_type) != request.device_type) continue;
|
if (@intFromEnum(device_type) != request.device_type) continue;
|
||||||
port_endpoints[port_index] = endpoint;
|
port_endpoints[port_index] = endpoint;
|
||||||
writeLine("system/drivers/ps2-bus: {s} driver attached\n", .{@tagName(device_type)});
|
writeLine("/system/drivers/ps2-bus: {s} driver attached\n", .{@tagName(device_type)});
|
||||||
return reply.write(out, .ok);
|
return reply.write(out, .ok);
|
||||||
}
|
}
|
||||||
return reply.write(out, .no_such_device);
|
return reply.write(out, .no_such_device);
|
||||||
@@ -91,7 +91,7 @@ fn handleAttach(message: []const u8, got: ipc.Received, out: []u8) usize {
|
|||||||
|
|
||||||
pub fn main() void {
|
pub fn main() void {
|
||||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: out of memory\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: out of memory\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,16 +103,16 @@ pub fn main() void {
|
|||||||
// is on which port is decided later by identify, not by this HID.
|
// is on which port is decided later by identify, not by this HID.
|
||||||
const maybe_controller_device_descriptor = device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_keyboard.hid());
|
const maybe_controller_device_descriptor = device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_keyboard.hid());
|
||||||
if (maybe_controller_device_descriptor) |controller_device_descriptor| {
|
if (maybe_controller_device_descriptor) |controller_device_descriptor| {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: found PS/2 controller\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: found PS/2 controller\n");
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: initializing controller\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: initializing controller\n");
|
||||||
|
|
||||||
if (!device.claim(controller_device_descriptor.id)) {
|
if (!device.claim(controller_device_descriptor.id)) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: unable to claim controller \n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: unable to claim controller \n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = ps2.Controller.init(controller_device_descriptor) orelse {
|
const controller = ps2.Controller.init(controller_device_descriptor) orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IO ports\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller is missing its IO ports\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
maybe_controller = controller;
|
maybe_controller = controller;
|
||||||
@@ -123,7 +123,7 @@ pub fn main() void {
|
|||||||
controller.flushOutputBuffer();
|
controller.flushOutputBuffer();
|
||||||
|
|
||||||
const current = controller.readConfigurationByte() orelse {
|
const current = controller.readConfigurationByte() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,49 +132,49 @@ pub fn main() void {
|
|||||||
ps2.configuration_first_port_translation);
|
ps2.configuration_first_port_translation);
|
||||||
|
|
||||||
if (controller.writeConfigurationByte(update) == null) {
|
if (controller.writeConfigurationByte(update) == null) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controller.performSelfTest()) |reply| {
|
if (controller.performSelfTest()) |reply| {
|
||||||
if (reply != ps2.response_controller_test_passed) {
|
if (reply != ps2.response_controller_test_passed) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: perform controller self test failed\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: perform controller self test failed\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller self test timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller self test timed out\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
has_two_channels = controller.hasTwoChannels() orelse {
|
has_two_channels = controller.hasTwoChannels() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller channels timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller channels timed out\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (has_two_channels) {
|
if (has_two_channels) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: has two channels\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: has two channels\n");
|
||||||
// keep the bus quiet until we have tested the ports and are ready to use them
|
// keep the bus quiet until we have tested the ports and are ready to use them
|
||||||
controller.disablePort(.two);
|
controller.disablePort(.two);
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: has one channel\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: has one channel\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface tests: always test port 1, test port 2 only if it exists
|
// interface tests: always test port 1, test port 2 only if it exists
|
||||||
const port_one_works = (controller.testPort(.one) orelse {
|
const port_one_works = (controller.testPort(.one) orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: port 1 test timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: port 1 test timed out\n");
|
||||||
return;
|
return;
|
||||||
}) == ps2.response_port_test_passed;
|
}) == ps2.response_port_test_passed;
|
||||||
|
|
||||||
var port_two_works = false;
|
var port_two_works = false;
|
||||||
if (has_two_channels) {
|
if (has_two_channels) {
|
||||||
port_two_works = (controller.testPort(.two) orelse {
|
port_two_works = (controller.testPort(.two) orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: port 2 test timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: port 2 test timed out\n");
|
||||||
return;
|
return;
|
||||||
}) == ps2.response_port_test_passed;
|
}) == ps2.response_port_test_passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!port_one_works and !port_two_works) {
|
if (!port_one_works and !port_two_works) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: no usable ports\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: no usable ports\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,16 +188,16 @@ pub fn main() void {
|
|||||||
// abort bring-up of the other one
|
// abort bring-up of the other one
|
||||||
if (port_one_works) {
|
if (port_one_works) {
|
||||||
if (controller.resetDevice(.one)) |passed| {
|
if (controller.resetDevice(.one)) |passed| {
|
||||||
if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset failed\n");
|
if (!passed) _ = runtime.system.write("/system/drivers/ps2-bus: port 1 device reset failed\n");
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: port 1 device reset timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: port 1 device reset timed out\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (port_two_works) {
|
if (port_two_works) {
|
||||||
if (controller.resetDevice(.two)) |passed| {
|
if (controller.resetDevice(.two)) |passed| {
|
||||||
if (!passed) _ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset failed\n");
|
if (!passed) _ = runtime.system.write("/system/drivers/ps2-bus: port 2 device reset failed\n");
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: port 2 device reset timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: port 2 device reset timed out\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,13 +207,13 @@ pub fn main() void {
|
|||||||
if (port_one_works) port_device_types[@intFromEnum(ps2.Port.one)] = spawnIdentifiedDriver(controller, .one);
|
if (port_one_works) port_device_types[@intFromEnum(ps2.Port.one)] = spawnIdentifiedDriver(controller, .one);
|
||||||
if (port_two_works) port_device_types[@intFromEnum(ps2.Port.two)] = spawnIdentifiedDriver(controller, .two);
|
if (port_two_works) port_device_types[@intFromEnum(ps2.Port.two)] = spawnIdentifiedDriver(controller, .two);
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: no PS/2 controller found\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: no PS/2 controller found\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = maybe_controller.?;
|
const controller = maybe_controller.?;
|
||||||
const interrupt_index = maybe_interrupt_index orelse {
|
const interrupt_index = maybe_interrupt_index orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller is missing its IRQ\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller is missing its IRQ\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -221,11 +221,11 @@ pub fn main() void {
|
|||||||
// well-known id so the children can find it, the way input subscribers find
|
// well-known id so the children can find it, the way input subscribers find
|
||||||
// the input service.
|
// the input service.
|
||||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: no endpoint\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: no endpoint\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (!ipc.register(.ps2_bus, endpoint)) {
|
if (!ipc.register(.ps2_bus, endpoint)) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: register failed\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: register failed\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ pub fn main() void {
|
|||||||
// let the controller raise them — an interrupt with nobody bound is lost.
|
// let the controller raise them — an interrupt with nobody bound is lost.
|
||||||
controller.drainOutputBuffer();
|
controller.drainOutputBuffer();
|
||||||
if (!device.irqBind(controller.device_id, interrupt_index, endpoint)) {
|
if (!device.irqBind(controller.device_id, interrupt_index, endpoint)) {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: irq_bind failed\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: irq_bind failed\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,21 +253,21 @@ pub fn main() void {
|
|||||||
.gsi = descriptor.resources[auxiliary_index].start,
|
.gsi = descriptor.resources[auxiliary_index].start,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: auxiliary irq_bind failed\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: auxiliary irq_bind failed\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var configuration = controller.readConfigurationByte() orelse {
|
var configuration = controller.readConfigurationByte() orelse {
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: controller configuration timed out\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if (port_device_types[@intFromEnum(ps2.Port.one)] != null) configuration |= ps2.Port.one.interruptBit();
|
if (port_device_types[@intFromEnum(ps2.Port.one)] != null) configuration |= ps2.Port.one.interruptBit();
|
||||||
if (maybe_auxiliary_interrupt != null) configuration |= ps2.Port.two.interruptBit();
|
if (maybe_auxiliary_interrupt != null) configuration |= ps2.Port.two.interruptBit();
|
||||||
_ = controller.writeConfigurationByte(configuration);
|
_ = controller.writeConfigurationByte(configuration);
|
||||||
|
|
||||||
_ = runtime.system.write("system/drivers/ps2-bus: ok\n");
|
_ = runtime.system.write("/system/drivers/ps2-bus: ok\n");
|
||||||
|
|
||||||
// The forwarding loop: an IRQ1 notification drains the output buffer, routing
|
// The forwarding loop: an IRQ1 notification drains the output buffer, routing
|
||||||
// each byte to the attached driver of the port it came from; a client message
|
// each byte to the attached driver of the port it came from; a client message
|
||||||
|
|||||||
@@ -108,6 +108,22 @@ fn readRegister(offset: usize) u32 {
|
|||||||
return register.*;
|
return register.*;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10)
|
||||||
|
/// decoded to human names — the boot-log breadcrumb for what actually enumerated on
|
||||||
|
/// a port, the USB analog of the pci-bus class-code line. A controller may redefine
|
||||||
|
/// these through its Supported Protocol capability, but the defaults cover every
|
||||||
|
/// speed QEMU and real hardware report at this (pre-descriptor) stage.
|
||||||
|
fn speedName(speed: u32) []const u8 {
|
||||||
|
return switch (speed) {
|
||||||
|
1 => "Full-speed (USB 2.0, 12 Mb/s)",
|
||||||
|
2 => "Low-speed (USB 2.0, 1.5 Mb/s)",
|
||||||
|
3 => "High-speed (USB 2.0, 480 Mb/s)",
|
||||||
|
4 => "SuperSpeed (USB 3.0, 5 Gb/s)",
|
||||||
|
5 => "SuperSpeedPlus (USB 3.1, 10 Gb/s)",
|
||||||
|
else => "unknown speed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// The root-hub port scan: read the capability registers for the port count
|
/// The root-hub port scan: read the capability registers for the port count
|
||||||
/// and the operational-register offset, then one PORTSC per port. The connect
|
/// and the operational-register offset, then one PORTSC per port. The connect
|
||||||
/// bit (CCS) and the speed field reflect hardware state directly — no
|
/// bit (CCS) and the speed field reflect hardware state directly — no
|
||||||
@@ -129,7 +145,7 @@ fn scanPorts(manager: runtime.ipc.Handle) void {
|
|||||||
if (port_status & 1 == 0) continue; // CCS: nothing connected
|
if (port_status & 1 == 0) continue; // CCS: nothing connected
|
||||||
connected += 1;
|
connected += 1;
|
||||||
const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class
|
const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class
|
||||||
writeLine("usb-xhci-bus: port {d} connected (speed class {d})\n", .{ port, speed });
|
writeLine("usb-xhci-bus: port {d} connected — {s} (speed class {d})\n", .{ port, speedName(speed), speed });
|
||||||
|
|
||||||
const report = protocol.ChildAdded{
|
const report = protocol.ChildAdded{
|
||||||
.parent = controller_id,
|
.parent = controller_id,
|
||||||
|
|||||||
@@ -131,7 +131,16 @@ pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
|
|||||||
/// and would otherwise vacuously "fit" anywhere.
|
/// and would otherwise vacuously "fit" anywhere.
|
||||||
fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool {
|
fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool {
|
||||||
if (parent.kind != child.kind) return false;
|
if (parent.kind != child.kind) return false;
|
||||||
if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) return parent.start == child.start;
|
if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) {
|
||||||
|
// Range containment: an interrupt line is still indivisible (a child owns
|
||||||
|
// exactly one GSI), but a parent may own a *range* of lines so a broad
|
||||||
|
// owner — the acpi-tables node, whose firmware names any legacy IRQ —
|
||||||
|
// can contain its children's specific lines. A length-1 parent range is
|
||||||
|
// exactly the old equality rule, so existing single-IRQ parents are
|
||||||
|
// unaffected.
|
||||||
|
const span = if (parent.len == 0) 1 else parent.len;
|
||||||
|
return child.start >= parent.start and child.start < parent.start + span;
|
||||||
|
}
|
||||||
if (child.len == 0 or parent.len == 0) return false;
|
if (child.len == 0 or parent.len == 0) return false;
|
||||||
// No overflow: a resource that wraps the address space is not containable.
|
// No overflow: a resource that wraps the address space is not containable.
|
||||||
const child_end = std.math.add(u64, child.start, child.len) catch return false;
|
const child_end = std.math.add(u64, child.start, child.len) catch return false;
|
||||||
@@ -180,6 +189,26 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.Device
|
|||||||
if (!ok) return error.NotContained;
|
if (!ok) return error.NotContained;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idempotent on exact match (docs/m19-m20-plan.md decision 3): a restarted
|
||||||
|
// registering bus re-registers what it rediscovers, and the table has no
|
||||||
|
// unregister — an identical (class, identity, resources) child under the
|
||||||
|
// same parent returns the existing id instead of appending a duplicate.
|
||||||
|
for (devices[0..count]) |*existing| {
|
||||||
|
if (existing.parent != parent_id) continue;
|
||||||
|
if (existing.class != descriptor.class) continue;
|
||||||
|
if (existing.pci_class != descriptor.pci_class) continue;
|
||||||
|
if (existing.hid_len != descriptor.hid_len) continue;
|
||||||
|
if (!std.mem.eql(u8, existing.hid[0..@intCast(existing.hid_len)], descriptor.hid[0..@intCast(descriptor.hid_len)])) continue;
|
||||||
|
if (existing.resource_count != descriptor.resource_count) continue;
|
||||||
|
var same = true;
|
||||||
|
for (0..@intCast(descriptor.resource_count)) |i| {
|
||||||
|
const a = existing.resources[i];
|
||||||
|
const b = descriptor.resources[i];
|
||||||
|
if (a.kind != b.kind or a.start != b.start or a.len != b.len) same = false;
|
||||||
|
}
|
||||||
|
if (same) return existing.id;
|
||||||
|
}
|
||||||
|
|
||||||
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||||
d.id = count;
|
d.id = count;
|
||||||
d.parent = parent_id;
|
d.parent = parent_id;
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
|||||||
|
|
||||||
/// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process.
|
/// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process.
|
||||||
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
||||||
|
const claim_flags = sync.enter();
|
||||||
|
defer sync.leave(claim_flags);
|
||||||
if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id))
|
if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id))
|
||||||
architecture.setSystemCallResult(state, 0)
|
architecture.setSystemCallResult(state, 0)
|
||||||
else
|
else
|
||||||
@@ -312,10 +314,22 @@ fn systemMmioMap(state: *architecture.CpuState) void {
|
|||||||
const resource_index = architecture.systemCallArg(state, 1);
|
const resource_index = architecture.systemCallArg(state, 1);
|
||||||
const t = scheduler.current();
|
const t = scheduler.current();
|
||||||
if (t.aspace == 0) return fail(state);
|
if (t.aspace == 0) return fail(state);
|
||||||
|
// Read the broker table under the lock: ring-3 device_register (M19) now
|
||||||
|
// mutates it concurrently on other cores, so a lock-free read here could
|
||||||
|
// see a torn resource (and a torn length used to panic the arithmetic
|
||||||
|
// below on integer overflow).
|
||||||
|
const r = blk: {
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
const owner = devices_broker.ownerOf(device_id) orelse return fail(state);
|
const owner = devices_broker.ownerOf(device_id) orelse return fail(state);
|
||||||
if (owner != t.id) return fail(state); // not claimed by this process
|
if (owner != t.id) return fail(state); // not claimed by this process
|
||||||
const r = devices_broker.resourceOf(device_id, resource_index) orelse return fail(state);
|
break :blk devices_broker.resourceOf(device_id, resource_index) orelse return fail(state);
|
||||||
|
};
|
||||||
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) return fail(state);
|
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) return fail(state);
|
||||||
|
// A zero-length or wrapping window is not mappable — fail cleanly rather
|
||||||
|
// than underflow `r.len - 1`.
|
||||||
|
if (r.len == 0) return fail(state);
|
||||||
|
if (@addWithOverflow(r.start, r.len)[1] != 0) return fail(state);
|
||||||
|
|
||||||
if (t.device_map_next == 0) t.device_map_next = device_arena_base;
|
if (t.device_map_next == 0) t.device_map_next = device_arena_base;
|
||||||
const first = r.start & ~@as(u64, page_size - 1);
|
const first = r.start & ~@as(u64, page_size - 1);
|
||||||
@@ -451,6 +465,11 @@ fn systemDeviceRegister(state: *architecture.CpuState) void {
|
|||||||
var descriptor: device_abi.DeviceDescriptor = undefined;
|
var descriptor: device_abi.DeviceDescriptor = undefined;
|
||||||
if (!ipc.copyFromUser(t.aspace, descriptor_ptr, std.mem.asBytes(&descriptor))) return fail(state);
|
if (!ipc.copyFromUser(t.aspace, descriptor_ptr, std.mem.asBytes(&descriptor))) return fail(state);
|
||||||
|
|
||||||
|
// Under the big kernel lock: the broker's table is also mutated by the
|
||||||
|
// death sweep (releaseAllOwnedBy) and read by enumerate on other cores —
|
||||||
|
// ring-3 registration (M19) made those genuinely concurrent.
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
const id = devices_broker.register(parent_id, t.id, &descriptor) catch return fail(state);
|
const id = devices_broker.register(parent_id, t.id, &descriptor) catch return fail(state);
|
||||||
architecture.setSystemCallResult(state, id);
|
architecture.setSystemCallResult(state, id);
|
||||||
}
|
}
|
||||||
|
|||||||
+304
-16
@@ -144,6 +144,18 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
usbReportTest(boot_information);
|
usbReportTest(boot_information);
|
||||||
} else if (eql(case, "device-list")) {
|
} else if (eql(case, "device-list")) {
|
||||||
deviceListTest(boot_information);
|
deviceListTest(boot_information);
|
||||||
|
} else if (eql(case, "pci-scan")) {
|
||||||
|
pciScanTest(boot_information);
|
||||||
|
} else if (eql(case, "acpi-parse")) {
|
||||||
|
acpiParseTest(boot_information);
|
||||||
|
} else if (eql(case, "acpi-report")) {
|
||||||
|
acpiReportTest(boot_information);
|
||||||
|
} else if (eql(case, "acpi-ps2")) {
|
||||||
|
acpiReportTest(boot_information); // same spawn; the harness regex differs
|
||||||
|
} else if (eql(case, "power-button")) {
|
||||||
|
acpiReportTest(boot_information); // boot the manager (spawns the acpi service); harness injects the button
|
||||||
|
} else if (eql(case, "orderly-shutdown")) {
|
||||||
|
orderlyShutdownTest(boot_information);
|
||||||
} else if (eql(case, "initial-ramdisk")) {
|
} else if (eql(case, "initial-ramdisk")) {
|
||||||
initialRamdiskTest(boot_information);
|
initialRamdiskTest(boot_information);
|
||||||
} else if (eql(case, "vfs")) {
|
} else if (eql(case, "vfs")) {
|
||||||
@@ -268,20 +280,56 @@ fn discoveryTest() void {
|
|||||||
|
|
||||||
// M15: every PCI function now carries its own 4 KiB ECAM configuration space as
|
// M15: every PCI function now carries its own 4 KiB ECAM configuration space as
|
||||||
// resource 0 — the window a driver mmio_maps to walk its capability list (MSI etc).
|
// resource 0 — the window a driver mmio_maps to walk its capability list (MSI etc).
|
||||||
|
// M19.3: the kernel seeds only the bridge; functions arrive by the ring-3
|
||||||
|
// scan (proven equivalent in pci-scan before the walk retired).
|
||||||
var buffer: [64]device_abi.DeviceDescriptor = undefined;
|
var buffer: [64]device_abi.DeviceDescriptor = undefined;
|
||||||
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
|
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
|
||||||
var pci_functions: u32 = 0;
|
var bridges: u32 = 0;
|
||||||
var pci_config_ok = true;
|
var bridge_shape_ok = false;
|
||||||
|
for (buffer[0..n]) |d| {
|
||||||
|
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_host_bridge)) continue;
|
||||||
|
bridges += 1;
|
||||||
|
var has_bus_range = false;
|
||||||
|
var has_io = false;
|
||||||
|
var memory_windows: u32 = 0;
|
||||||
|
for (d.resources[0..@intCast(d.resource_count)]) |resource| {
|
||||||
|
if (resource.kind == @intFromEnum(device_abi.ResourceKind.bus_range)) has_bus_range = true;
|
||||||
|
if (resource.kind == @intFromEnum(device_abi.ResourceKind.io_port)) has_io = true;
|
||||||
|
if (resource.kind == @intFromEnum(device_abi.ResourceKind.memory)) memory_windows += 1;
|
||||||
|
}
|
||||||
|
// ECAM plus at least one MMIO aperture, the bus range, the I/O window.
|
||||||
|
if (has_bus_range and has_io and memory_windows >= 2) bridge_shape_ok = true;
|
||||||
|
}
|
||||||
|
check("a PCI host bridge was seeded (MCFG)", bridges >= 1);
|
||||||
|
check("the bridge carries ECAM, apertures, bus range, and the I/O window", bridge_shape_ok);
|
||||||
|
|
||||||
|
// M19.0: every PCI memory resource (config slice and BARs alike) must be
|
||||||
|
// contained in one of its parent bridge's windows — the aperture derivation
|
||||||
|
// from the memory map is what makes a future user-space device_register of
|
||||||
|
// these functions pass containment. This is the assert that catches a
|
||||||
|
// too-coarse hole computation before M19.2 would.
|
||||||
|
var bars_contained = true;
|
||||||
for (buffer[0..n]) |d| {
|
for (buffer[0..n]) |d| {
|
||||||
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_device)) continue;
|
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_device)) continue;
|
||||||
pci_functions += 1;
|
if (d.parent >= n) {
|
||||||
const has_config = d.resource_count >= 1 and
|
bars_contained = false;
|
||||||
d.resources[0].kind == @intFromEnum(device_abi.ResourceKind.memory) and
|
continue;
|
||||||
d.resources[0].len == abi.page_size;
|
|
||||||
if (!has_config) pci_config_ok = false;
|
|
||||||
}
|
}
|
||||||
check("PCI functions were enumerated (MCFG/ECAM)", pci_functions >= 1);
|
const bridge = buffer[@intCast(d.parent)];
|
||||||
check("each PCI function exposes its ECAM config space as resource 0", pci_config_ok);
|
for (d.resources[0..@intCast(d.resource_count)]) |r| {
|
||||||
|
if (r.kind != @intFromEnum(device_abi.ResourceKind.memory)) continue;
|
||||||
|
var inside = false;
|
||||||
|
for (bridge.resources[0..@intCast(bridge.resource_count)]) |w| {
|
||||||
|
if (w.kind != @intFromEnum(device_abi.ResourceKind.memory)) continue;
|
||||||
|
if (r.start >= w.start and r.start + r.len <= w.start + w.len) inside = true;
|
||||||
|
}
|
||||||
|
if (!inside) {
|
||||||
|
bars_contained = false;
|
||||||
|
log(" escaping BAR: 0x{x}+0x{x} on device {d}\n", .{ r.start, r.len, d.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check("every PCI BAR lies inside a bridge aperture (M19.0)", bars_contained);
|
||||||
|
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
@@ -1092,12 +1140,18 @@ fn ioPortTest() void {
|
|||||||
var buffer: [64]device_abi.DeviceDescriptor = undefined;
|
var buffer: [64]device_abi.DeviceDescriptor = undefined;
|
||||||
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
|
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
|
||||||
|
|
||||||
|
// Post-M20.3 the PS/2 node is registered at runtime by the ring-3 acpi
|
||||||
|
// service, so it is absent from this boot snapshot. Exercise the same
|
||||||
|
// io_port claim/resolve mechanism against the acpi-tables node's broad I/O
|
||||||
|
// grant — the window that now carries port authority (the service uses it
|
||||||
|
// for exactly this). The PS/2 status port 0x64 is offset 0x64 within it.
|
||||||
var found_id: ?u64 = null;
|
var found_id: ?u64 = null;
|
||||||
var found_res: u64 = 0;
|
var found_res: u64 = 0;
|
||||||
outer: for (buffer[0..n]) |d| {
|
outer: for (buffer[0..n]) |d| {
|
||||||
|
if (d.class != @intFromEnum(device_abi.DeviceClass.acpi_tables)) continue;
|
||||||
for (0..d.resource_count) |ri| {
|
for (0..d.resource_count) |ri| {
|
||||||
const r = d.resources[ri];
|
const r = d.resources[ri];
|
||||||
if (r.kind == @intFromEnum(device_abi.ResourceKind.io_port) and r.start == 0x64 and r.len >= 1) {
|
if (r.kind == @intFromEnum(device_abi.ResourceKind.io_port) and r.start == 0 and r.len > 0x64) {
|
||||||
found_id = d.id;
|
found_id = d.id;
|
||||||
found_res = ri;
|
found_res = ri;
|
||||||
break :outer;
|
break :outer;
|
||||||
@@ -1105,18 +1159,18 @@ fn ioPortTest() void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const id = found_id orelse {
|
const id = found_id orelse {
|
||||||
check("discovered the PS/2 status port (io_port 0x64)", false);
|
check("discovered the acpi-tables I/O window", false);
|
||||||
result();
|
result();
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
check("discovered the PS/2 status port (io_port 0x64)", true);
|
check("discovered the acpi-tables I/O window", true);
|
||||||
|
|
||||||
const me = scheduler.current();
|
const me = scheduler.current();
|
||||||
check("claimed the io_port device", devices_broker.claim(id, me.id));
|
check("claimed the io_port device", devices_broker.claim(id, me.id));
|
||||||
check("an in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0, 1) == 0x64);
|
check("an in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0x64, 1) == 0x64);
|
||||||
check("an over-wide access is refused", process.resolveIoPort(me, id, found_res, 0, 2) == null);
|
check("a 4-byte access at the last port is refused", process.resolveIoPort(me, id, found_res, 0xFFFF, 4) == null);
|
||||||
check("an out-of-range offset is refused", process.resolveIoPort(me, id, found_res, 1, 1) == null);
|
check("an out-of-range offset is refused", process.resolveIoPort(me, id, found_res, 0x10000, 1) == null);
|
||||||
check("an unclaimed device id is refused", process.resolveIoPort(me, 0xDEAD_BEEF, found_res, 0, 1) == null);
|
check("an unclaimed device id is refused", process.resolveIoPort(me, 0xDEAD_BEEF, found_res, 0x64, 1) == null);
|
||||||
|
|
||||||
// The kernel actually issues the `in`. Reaching this line at all proves it didn't
|
// The kernel actually issues the `in`. Reaching this line at all proves it didn't
|
||||||
// fault; a width-1 read must return a single byte.
|
// fault; a width-1 read must return a single byte.
|
||||||
@@ -1765,6 +1819,211 @@ fn deviceListTest(boot_information: *const BootInformation) void {
|
|||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// M19.1: the ring-3 PCI scan agrees with the kernel's. The manager spawns
|
||||||
|
/// pci-bus for the host bridge; the driver walks the same ECAM window through
|
||||||
|
/// its mmio_map grant and must find exactly the functions the kernel's own
|
||||||
|
/// enumeration recorded — the equivalence that licenses retiring the kernel
|
||||||
|
/// walk in M19.3.
|
||||||
|
fn pciScanTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: pci-scan\n", .{});
|
||||||
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
|
check("bootloader handed over an initial_ramdisk", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||||
|
check("initial_ramdisk image is valid", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Post-flip (M19.3) ground truth: the kernel no longer enumerates PCI
|
||||||
|
// functions, so equivalence inverts — the broker's function count after
|
||||||
|
// the scan must equal what the driver itself reported finding.
|
||||||
|
var buffer: [64]device_abi.DeviceDescriptor = undefined;
|
||||||
|
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
|
||||||
|
var boot_pci: u32 = 0;
|
||||||
|
for (buffer[0..n]) |d| {
|
||||||
|
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) boot_pci += 1;
|
||||||
|
}
|
||||||
|
check("the kernel seeded no PCI functions (the walk retired)", boot_pci == 0);
|
||||||
|
|
||||||
|
process.setInitialRamdisk(image);
|
||||||
|
process.write_count = 0;
|
||||||
|
var manager: u32 = 0;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < rd.count) : (i += 1) {
|
||||||
|
const item = rd.entry(i) orelse continue;
|
||||||
|
if (!eql(item.name, "device-manager")) continue;
|
||||||
|
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-pci-restart" }, scheduler.currentId(), null) catch 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("device-manager spawned (test-pci-restart mode)", manager != 0);
|
||||||
|
|
||||||
|
// First scan: wait for the driver's count line and parse the number.
|
||||||
|
const count_prefix = "pci-bus: ";
|
||||||
|
const count_suffix = " functions found";
|
||||||
|
var reported: u32 = 0;
|
||||||
|
scheduler.setPriority(1);
|
||||||
|
var deadline = architecture.millis() + 15000;
|
||||||
|
while (architecture.millis() < deadline and reported == 0) {
|
||||||
|
if (process.write_len > count_prefix.len + count_suffix.len and eql(process.write_buffer[0..count_prefix.len], count_prefix)) {
|
||||||
|
const line = process.write_buffer[0..process.write_len];
|
||||||
|
const digits_end = std.mem.indexOf(u8, line, count_suffix) orelse {
|
||||||
|
scheduler.yield();
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
reported = std.fmt.parseInt(u32, line[count_prefix.len..digits_end], 10) catch 0;
|
||||||
|
}
|
||||||
|
scheduler.yield();
|
||||||
|
}
|
||||||
|
scheduler.setPriority(4);
|
||||||
|
check("the ring-3 scan reported a function count", reported >= 1);
|
||||||
|
|
||||||
|
// Every reported function was registered: the broker holds exactly them.
|
||||||
|
var registered: [64]device_abi.DeviceDescriptor = undefined;
|
||||||
|
const r = @min(devices_broker.enumerate(®istered), registered.len);
|
||||||
|
var registered_pci: u32 = 0;
|
||||||
|
for (registered[0..r]) |d| {
|
||||||
|
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) registered_pci += 1;
|
||||||
|
}
|
||||||
|
check("the broker holds exactly the reported functions", registered_pci == reported);
|
||||||
|
const kernel_count = reported; // the no-duplicate check below reuses it
|
||||||
|
|
||||||
|
// The restart drill: the manager kills pci-bus after its reports; the
|
||||||
|
// respawn re-claims, re-scans, and re-registers.
|
||||||
|
const restart_marker = "device-manager: restarting pci-bus";
|
||||||
|
scheduler.setPriority(1);
|
||||||
|
deadline = architecture.millis() + 15000;
|
||||||
|
var restarted = false;
|
||||||
|
while (architecture.millis() < deadline and !restarted) {
|
||||||
|
if (process.write_len >= restart_marker.len and eql(process.write_buffer[0..restart_marker.len], restart_marker)) restarted = true;
|
||||||
|
scheduler.yield();
|
||||||
|
}
|
||||||
|
scheduler.setPriority(4);
|
||||||
|
check("the manager restarted pci-bus", restarted);
|
||||||
|
|
||||||
|
var marker_buffer: [48]u8 = undefined;
|
||||||
|
const marker = std.fmt.bufPrint(&marker_buffer, "pci-bus: {d} functions found", .{reported}) catch "";
|
||||||
|
scheduler.setPriority(1);
|
||||||
|
deadline = architecture.millis() + 15000;
|
||||||
|
var seen = false;
|
||||||
|
while (architecture.millis() < deadline and !seen) {
|
||||||
|
if (process.write_len >= marker.len and eql(process.write_buffer[0..marker.len], marker)) seen = true;
|
||||||
|
scheduler.yield();
|
||||||
|
}
|
||||||
|
scheduler.setPriority(4);
|
||||||
|
check("the respawned scan reported the same count", seen);
|
||||||
|
|
||||||
|
// No duplicates: the registrations deduped against the kernel's own nodes
|
||||||
|
// on the first pass, and against themselves on the second.
|
||||||
|
var after: [64]device_abi.DeviceDescriptor = undefined;
|
||||||
|
const m = @min(devices_broker.enumerate(&after), after.len);
|
||||||
|
var after_count: u32 = 0;
|
||||||
|
for (after[0..m]) |d| {
|
||||||
|
if (d.class == @intFromEnum(device_abi.DeviceClass.pci_device)) after_count += 1;
|
||||||
|
}
|
||||||
|
check("no duplicate PCI nodes after register + restart + re-register", after_count == kernel_count);
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// M21.3 capstone: orderly shutdown. Boot init with the initial-ramdisk
|
||||||
|
/// published, so init spawns the full service tree (vfs, input, device-manager
|
||||||
|
/// -> discovery/acpi); the harness injects a real power-button event via QMP;
|
||||||
|
/// the acpi service publishes it; init runs the stop sequence over its children
|
||||||
|
/// and asks the power service for S5; the machine powers off (QEMU exits). The
|
||||||
|
/// kernel test only spawns init — the ordered chain is the harness assertion.
|
||||||
|
fn orderlyShutdownTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: orderly-shutdown\n", .{});
|
||||||
|
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
|
||||||
|
check("bootloader handed over init and the initial_ramdisk", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
process.setInitialRamdisk(ramdisk);
|
||||||
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||||
|
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
|
||||||
|
check("init spawned as PID root of user space", spawned);
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// M20.2: the acpi service registers + reports its _HID devices. Boot normally
|
||||||
|
/// (the manager spawns discovery); the harness's expect regex requires the two
|
||||||
|
/// PS/2 nodes among the service's report lines, each with its _CRS resources —
|
||||||
|
/// the ring-3 _CRS/_STA evaluation working end to end. The kernel test only
|
||||||
|
/// starts the manager.
|
||||||
|
fn acpiReportTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: acpi-report\n", .{});
|
||||||
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
|
check("bootloader handed over an initial_ramdisk", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||||
|
check("initial_ramdisk image is valid", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
process.setInitialRamdisk(image);
|
||||||
|
var spawned = false;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < rd.count) : (i += 1) {
|
||||||
|
const item = rd.entry(i) orelse continue;
|
||||||
|
if (!eql(item.name, "device-manager")) continue;
|
||||||
|
_ = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||||
|
spawned = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("device-manager spawned", spawned);
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// M20.1: the ring-3 AML parse agrees with the kernel's. The manager spawns
|
||||||
|
/// the discovery service (the acpi build variant); it claims the acpi-tables
|
||||||
|
/// node, maps the blobs, parses them, and logs its Device count — which must
|
||||||
|
/// equal what the kernel's own parse produced (the equivalence that licenses
|
||||||
|
/// retiring the kernel's device build in M20.3).
|
||||||
|
fn acpiParseTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: acpi-parse\n", .{});
|
||||||
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
|
check("bootloader handed over an initial_ramdisk", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||||
|
check("initial_ramdisk image is valid", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The kernel's own count, from the namespace it already built for \_S5.
|
||||||
|
const kernel_devices = platform.amlDeviceCount();
|
||||||
|
check("the kernel namespace has devices to compare against", kernel_devices >= 1);
|
||||||
|
|
||||||
|
// Spawn the discovery service directly with that count as argv: it parses
|
||||||
|
// the same blobs in ring 3 and self-verifies, printing "acpi-parse: ok" iff
|
||||||
|
// the counts match. The harness's expect regex is that marker — deterministic,
|
||||||
|
// no racing the shared serial buffer.
|
||||||
|
process.setInitialRamdisk(image);
|
||||||
|
var count_text: [16]u8 = undefined;
|
||||||
|
const count_arg = std.fmt.bufPrint(&count_text, "{d}", .{kernel_devices}) catch "0";
|
||||||
|
var spawned = false;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < rd.count) : (i += 1) {
|
||||||
|
const item = rd.entry(i) orelse continue;
|
||||||
|
if (!eql(item.name, "discovery")) continue;
|
||||||
|
_ = process.spawnProcessSupervised(item.blob, 4, &.{ "discovery", count_arg }, scheduler.currentId(), null) catch 0;
|
||||||
|
spawned = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("discovery service spawned", spawned);
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
/// The whole user-side surface at once: spawn process-test's supervisor role,
|
/// The whole user-side surface at once: spawn process-test's supervisor role,
|
||||||
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two
|
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two
|
||||||
/// children supervised, sees them in process_enumerate, kills them (one blocked,
|
/// children supervised, sees them in process_enumerate, kills them (one blocked,
|
||||||
@@ -2085,6 +2344,35 @@ fn hpetGsi() ?u32 {
|
|||||||
/// land in the device table with the containment invariant intact.
|
/// land in the device table with the containment invariant intact.
|
||||||
fn busTest(boot_information: *const BootInformation) void {
|
fn busTest(boot_information: *const BootInformation) void {
|
||||||
log("DANOS-TEST-BEGIN: bus\n", .{});
|
log("DANOS-TEST-BEGIN: bus\n", .{});
|
||||||
|
|
||||||
|
// M19.0: device_register is idempotent on exact match — a restarted
|
||||||
|
// registering bus must not duplicate its children. Driven directly against
|
||||||
|
// the broker: claim an unclaimed node, register the same (class, hid,
|
||||||
|
// resourceless) child twice, expect one id and one table entry.
|
||||||
|
{
|
||||||
|
const me = scheduler.currentId();
|
||||||
|
var probe: [1]device_abi.DeviceDescriptor = undefined;
|
||||||
|
const total = devices_broker.enumerate(&probe);
|
||||||
|
check("device tree is seeded for the idempotence check", total >= 1);
|
||||||
|
if (devices_broker.ownerOf(0) == null) {
|
||||||
|
check("claimed device 0 for the idempotence check", devices_broker.claim(0, me));
|
||||||
|
var child = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||||
|
child.class = @intFromEnum(device_abi.DeviceClass.unknown);
|
||||||
|
child.pci_class = device_abi.no_pci_class;
|
||||||
|
child.hid_len = 4;
|
||||||
|
child.hid[0..4].* = "idem".*;
|
||||||
|
const first = devices_broker.register(0, me, &child) catch 0;
|
||||||
|
check("first register succeeded", first != 0);
|
||||||
|
const before = devices_broker.enumerate(&probe);
|
||||||
|
const second = devices_broker.register(0, me, &child) catch 0;
|
||||||
|
check("re-register returned the same id", second == first);
|
||||||
|
check("re-register grew nothing", devices_broker.enumerate(&probe) == before);
|
||||||
|
devices_broker.releaseAllOwnedBy(me);
|
||||||
|
} else {
|
||||||
|
check("device 0 unexpectedly claimed before the idempotence check", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (boot_information.initial_ramdisk_len == 0) {
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
check("bootloader handed over an initial_ramdisk", false);
|
check("bootloader handed over an initial_ramdisk", false);
|
||||||
result();
|
result();
|
||||||
|
|||||||
+688
-19
@@ -1,27 +1,696 @@
|
|||||||
//! /system/services/acpi — the ACPI discovery service: the x86 firmware
|
//! /system/services/acpi — the ACPI discovery service: the x86 firmware
|
||||||
//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). **Placeholder:
|
//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). Claims the
|
||||||
//! not implemented until M20.1** — it exists so the build's `-Ddiscovery`
|
//! `acpi-tables` node the kernel publishes (the AML blobs, the broad io_port
|
||||||
//! option has both of its values and the ramdisk's neutral `discovery` slot is
|
//! grant, a broad irq window, the SCI), and runs the **shared AML module** in
|
||||||
//! wired before the implementation lands.
|
//! ring 3 — the same parser and interpreter the kernel uses.
|
||||||
//!
|
//!
|
||||||
//! What it becomes (the plan's decisions 5 and 7): claim the `acpi-tables`
|
//! It also owns the **event side** (M21): it registers the domain-named `.power`
|
||||||
//! node the kernel publishes (table blobs + the broad io_port grant + the SCI),
|
//! service, binds the SCI (System Control Interrupt), and on a power-button
|
||||||
//! map the tables, and run the **shared AML module** in ring 3 behind a `Hal`
|
//! fixed event publishes `power_button` to subscribers — and on init's request
|
||||||
//! backed by `mmio_map` and `io_read`/`io_write` — the interpreter cannot tell
|
//! writes S5 to power the machine off. The device discovery (M20) and the event
|
||||||
//! it moved. Then the bus-driver shape: `device_register` the namespace
|
//! handling both run in one `runtime.service.run` loop.
|
||||||
//! 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 std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
|
const aml = @import("aml");
|
||||||
|
const acpi_ids = @import("acpi-ids");
|
||||||
|
const device = runtime.device;
|
||||||
|
const protocol = runtime.device_manager_protocol;
|
||||||
|
const power = runtime.power_protocol;
|
||||||
|
/// AML opcode/prefix bytes by name (`zero_opcode`, `byte_prefix`, …) — so the `_HID`
|
||||||
|
/// integer decode names the opcodes instead of bare 0x0A/0x0B/… (docs/coding-standards.md).
|
||||||
|
const opcodes = aml.opcodes;
|
||||||
|
|
||||||
pub fn main() void {
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
// Not implemented: exit cleanly and silently (a bare spawn by the
|
var line: [128]u8 = undefined;
|
||||||
// initial-ramdisk sweep must not derange other tests' markers). The
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
// supervisor reads a clean exit as "meant to stop" — correct for a
|
}
|
||||||
// placeholder.
|
|
||||||
|
// The claimed acpi-tables node and the resource index of its broad io_port
|
||||||
|
// window — the Hal routes every port access through this one claim.
|
||||||
|
var node_id: u64 = 0;
|
||||||
|
var io_resource_index: u64 = 0;
|
||||||
|
// The SCI's irq resource index on the node (the len-1 irq, distinct from the
|
||||||
|
// broad [0,256) window), for irqBind / irqAck.
|
||||||
|
var sci_resource_index: u64 = 0;
|
||||||
|
var has_sci = false;
|
||||||
|
|
||||||
|
// PM1 event/control and GPE register ports, read from the FADT copy the kernel
|
||||||
|
// publishes on the node (M21). Port 0 means absent.
|
||||||
|
var pm1a_evt: u16 = 0;
|
||||||
|
var pm1b_evt: u16 = 0;
|
||||||
|
var pm1_evt_len: u8 = 0;
|
||||||
|
var pm1a_cnt: u16 = 0;
|
||||||
|
var pm1b_cnt: u16 = 0;
|
||||||
|
var gpe0_blk: u16 = 0;
|
||||||
|
var gpe0_len: u8 = 0;
|
||||||
|
var gpe1_blk: u16 = 0;
|
||||||
|
var gpe1_len: u8 = 0;
|
||||||
|
var smi_cmd: u16 = 0;
|
||||||
|
var acpi_enable_value: u8 = 0;
|
||||||
|
var s5_slp_typ_a: u8 = 0;
|
||||||
|
var s5_slp_typ_b: u8 = 0;
|
||||||
|
var s5_valid = false;
|
||||||
|
|
||||||
|
// PM1 event-register bits (ACPI): PWRBTN in the status/enable word is bit 8;
|
||||||
|
// the control word's SCI_EN is bit 0; SLP_EN is bit 13.
|
||||||
|
const pwrbtn_bit: u16 = 1 << 8;
|
||||||
|
const sci_en_bit: u32 = 1 << 0;
|
||||||
|
const slp_en: u32 = 1 << 13;
|
||||||
|
|
||||||
|
// The `.power` subscribers: endpoints handed over as capabilities, each
|
||||||
|
// receiving events as buffered messages. Dropped on a failed send. The
|
||||||
|
// subscriber's task id is kept too — a shutdown request is honored only from a
|
||||||
|
// subscriber (init subscribes; a stray process does not), the soft gate that
|
||||||
|
// stands in for "only the system supervisor may power off" without hardcoding
|
||||||
|
// a pid the kernel's idle tasks would have taken.
|
||||||
|
const maximum_subscribers = 8;
|
||||||
|
var subscribers: [maximum_subscribers]?runtime.ipc.Handle = .{null} ** maximum_subscribers;
|
||||||
|
var subscriber_tasks: [maximum_subscribers]u32 = .{0} ** maximum_subscribers;
|
||||||
|
|
||||||
|
// Pass-1 registration record (see main): what pass 2 reports.
|
||||||
|
const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 };
|
||||||
|
var registered: [64]Registered = undefined;
|
||||||
|
var registered_count: usize = 0;
|
||||||
|
|
||||||
|
// A scratch page returned for SystemMemory OperationRegion maps: the service
|
||||||
|
// cannot map arbitrary physical memory from ring 3, so such regions are
|
||||||
|
// unsupported and degrade to harmless zeros rather than faulting. The M20.2
|
||||||
|
// targets (ps2, the legacy devices) use SystemIO and static templates.
|
||||||
|
var mmio_scratch: [4096]u8 align(4096) = .{0} ** 4096;
|
||||||
|
|
||||||
|
fn halMapMmio(physical: u64, len: u64, writable: bool) u64 {
|
||||||
|
_ = physical;
|
||||||
|
_ = len;
|
||||||
|
_ = writable;
|
||||||
|
return @intFromPtr(&mmio_scratch);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn halPioRead(width: u8, port: u16) u32 {
|
||||||
|
return device.ioRead(node_id, io_resource_index, port, width) orelse 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn halPioWrite(width: u8, port: u16, value: u32) void {
|
||||||
|
_ = device.ioWrite(node_id, io_resource_index, port, width, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor {
|
||||||
|
const total = device.enumerate(buffer);
|
||||||
|
for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||||
|
if (d.class == @intFromEnum(device.DeviceClass.acpi_tables)) return d;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
// When the acpi-parse scenario spawns this directly, argv[1] is the kernel's
|
||||||
|
// own device count to self-verify against — deterministic, no log-scraping.
|
||||||
|
const expected: ?usize = if (init.arguments.get(1)) |a| (std.fmt.parseInt(usize, a, 10) catch null) else null;
|
||||||
|
|
||||||
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
|
_ = runtime.system.write("acpi: out of memory\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const node = findTablesNode(buffer) orelse {
|
||||||
|
_ = runtime.system.write("acpi: no acpi-tables node to claim\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
node_id = node.id;
|
||||||
|
if (!device.claim(node_id)) {
|
||||||
|
_ = runtime.system.write("acpi: unable to claim acpi-tables\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the node's resources: the AML blobs (bytecode), the FADT (intact
|
||||||
|
// "FACP" header — decision 3), the io_port grant, and the SCI irq.
|
||||||
|
var blocks: [8][]const u8 = undefined;
|
||||||
|
var block_count: usize = 0;
|
||||||
|
var found_io = false;
|
||||||
|
var fadt: ?[]const u8 = null;
|
||||||
|
for (node.resources[0..@intCast(node.resource_count)], 0..) |resource, index| {
|
||||||
|
if (resource.kind == @intFromEnum(device.ResourceKind.io_port) and !found_io) {
|
||||||
|
io_resource_index = index;
|
||||||
|
found_io = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resource.kind == @intFromEnum(device.ResourceKind.irq) and resource.len == 1) {
|
||||||
|
sci_resource_index = index;
|
||||||
|
has_sci = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue;
|
||||||
|
const base = device.mmioMap(node_id, index) orelse continue;
|
||||||
|
const pointer: [*]const u8 = @ptrFromInt(base);
|
||||||
|
const bytes = pointer[0..@intCast(resource.len)];
|
||||||
|
if (bytes.len >= 4 and std.mem.eql(u8, bytes[0..4], "FACP")) {
|
||||||
|
fadt = bytes;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (block_count == blocks.len) continue;
|
||||||
|
blocks[block_count] = bytes;
|
||||||
|
block_count += 1;
|
||||||
|
}
|
||||||
|
if (block_count == 0) {
|
||||||
|
_ = runtime.system.write("acpi: no AML blobs on the node\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = aml.parse(runtime.allocator(), blocks[0..block_count]) catch {
|
||||||
|
_ = runtime.system.write("acpi: AML parse failed\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var namespace = result.namespace;
|
||||||
|
const devices = aml.deviceCount(&namespace);
|
||||||
|
writeLine("acpi: parsed {d} AML blob(s), {d} namespace devices\n", .{ block_count, devices });
|
||||||
|
if (expected) |want| {
|
||||||
|
if (devices == want) {
|
||||||
|
_ = runtime.system.write("acpi-parse: ok\n");
|
||||||
|
} else {
|
||||||
|
writeLine("acpi-parse: mismatch (ring-3 {d} vs kernel {d})\n", .{ devices, want });
|
||||||
|
}
|
||||||
|
// Self-verify mode is standalone (no manager); stop before reporting.
|
||||||
|
while (true) runtime.system.sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register + report the present _HID devices (M20), then set up the power
|
||||||
|
// event side (M21), then serve — all in one harness loop. The interpreter
|
||||||
|
// and namespace outlive this frame (static), so the harness callbacks can
|
||||||
|
// reach them.
|
||||||
|
interpreter_arena = std.heap.ArenaAllocator.init(runtime.allocator());
|
||||||
|
persistent_namespace = namespace;
|
||||||
|
global_interpreter = aml.Interpreter.init(&persistent_namespace, .{
|
||||||
|
.mapMmio = halMapMmio,
|
||||||
|
.pioRead = halPioRead,
|
||||||
|
.pioWrite = halPioWrite,
|
||||||
|
}, interpreter_arena.allocator());
|
||||||
|
|
||||||
|
readFadt(fadt);
|
||||||
|
s5_valid = readSleepS5(&persistent_namespace);
|
||||||
|
|
||||||
|
runtime.service.run(power.message_maximum, .{
|
||||||
|
.service = .power,
|
||||||
|
.init = onInit,
|
||||||
|
.on_message = onMessage,
|
||||||
|
.on_notification = onNotification,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static so the harness callbacks (which run after main's stack frame is gone)
|
||||||
|
// can reach the namespace and interpreter.
|
||||||
|
var persistent_namespace: aml.Namespace = undefined;
|
||||||
|
var global_interpreter: aml.Interpreter = undefined;
|
||||||
|
var interpreter_arena: std.heap.ArenaAllocator = undefined;
|
||||||
|
|
||||||
|
/// Startup under the harness: register + report the discovered devices to the
|
||||||
|
/// manager (M20), then enable ACPI mode and arm the power button (M21).
|
||||||
|
fn onInit(endpoint: runtime.ipc.Handle) bool {
|
||||||
|
registered_count = 0;
|
||||||
|
walkDevices(persistent_namespace.root, &global_interpreter);
|
||||||
|
|
||||||
|
const manager = runtime.ipc.lookup(.device_manager);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < registered_count) : (i += 1) {
|
||||||
|
const entry = registered[i];
|
||||||
|
const hid = entry.hid[0..entry.hid_len];
|
||||||
|
const desc = acpi_ids.description(hid);
|
||||||
|
if (desc.len != 0)
|
||||||
|
writeLine("acpi: reported {s} (device {d}, {d} resources) — {s}\n", .{ hid, entry.device_id, entry.resource_count, desc })
|
||||||
|
else
|
||||||
|
writeLine("acpi: reported {s} (device {d}, {d} resources)\n", .{ hid, entry.device_id, entry.resource_count });
|
||||||
|
if (manager) |h| {
|
||||||
|
var report = protocol.ChildAdded{ .parent = node_id, .bus_address = entry.device_id, .identity = 0, .device_id = entry.device_id };
|
||||||
|
@memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]);
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
_ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeLine("acpi: reported {d} device(s) to the manager\n", .{registered_count});
|
||||||
|
|
||||||
|
armPowerButton(endpoint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- power event side (M21) ---------------------------------------------------
|
||||||
|
|
||||||
|
/// Read the PM1 event/control and GPE register ports plus the SMI enable pair
|
||||||
|
/// from the FADT copy on the node. Offsets are from the FADT table start (the
|
||||||
|
/// SDT header is the first 36 bytes). Prefers the 32-bit port fields; QEMU's
|
||||||
|
/// FADT populates them.
|
||||||
|
fn readFadt(fadt: ?[]const u8) void {
|
||||||
|
const f = fadt orelse {
|
||||||
|
_ = runtime.system.write("acpi: no FADT on the node — power events off\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
smi_cmd = @truncate(rd32(f, 48));
|
||||||
|
acpi_enable_value = f[52];
|
||||||
|
pm1a_evt = @truncate(rd32(f, 56));
|
||||||
|
pm1b_evt = @truncate(rd32(f, 60));
|
||||||
|
pm1a_cnt = @truncate(rd32(f, 64));
|
||||||
|
pm1b_cnt = @truncate(rd32(f, 68));
|
||||||
|
gpe0_blk = @truncate(rd32(f, 80));
|
||||||
|
gpe1_blk = @truncate(rd32(f, 84));
|
||||||
|
pm1_evt_len = if (f.len > 88) f[88] else 4;
|
||||||
|
gpe0_len = if (f.len > 92) f[92] else 0;
|
||||||
|
gpe1_len = if (f.len > 93) f[93] else 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readSleepS5(ns: *aml.Namespace) bool {
|
||||||
|
const st = aml.sleepState(ns, 5) orelse return false;
|
||||||
|
s5_slp_typ_a = st.slp_typ_a;
|
||||||
|
s5_slp_typ_b = st.slp_typ_b;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable ACPI mode if the firmware isn't already in it, then bind the SCI and
|
||||||
|
/// set PWRBTN_EN so the power button raises an interrupt we can see.
|
||||||
|
fn armPowerButton(endpoint: runtime.ipc.Handle) void {
|
||||||
|
if (pm1a_cnt != 0 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0 and smi_cmd != 0) {
|
||||||
|
// Switch to ACPI mode: write ACPI_ENABLE to the SMI command port, then
|
||||||
|
// spin (bounded) until SCI_EN latches.
|
||||||
|
halPioWrite(1, smi_cmd, acpi_enable_value);
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (tries < 1000 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0) : (tries += 1) {
|
||||||
|
runtime.system.sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!has_sci) {
|
||||||
|
_ = runtime.system.write("acpi: no SCI resource — power button unavailable\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!device.irqBind(node_id, sci_resource_index, endpoint)) {
|
||||||
|
_ = runtime.system.write("acpi: SCI irq_bind failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// PWRBTN_EN lives in the PM1 enable register at evt_blk + evt_len/2.
|
||||||
|
if (pm1a_evt != 0) {
|
||||||
|
const en_port = pm1a_evt + pm1_evt_len / 2;
|
||||||
|
halPioWrite(2, en_port, @as(u16, @truncate(halPioRead(2, en_port))) | pwrbtn_bit);
|
||||||
|
}
|
||||||
|
if (pm1b_evt != 0) {
|
||||||
|
const en_port = pm1b_evt + pm1_evt_len / 2;
|
||||||
|
halPioWrite(2, en_port, @as(u16, @truncate(halPioRead(2, en_port))) | pwrbtn_bit);
|
||||||
|
}
|
||||||
|
_ = runtime.system.write("acpi: power button armed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The SCI fired. Read PM1 status; a set PWRBTN_STS is the power button — clear
|
||||||
|
/// it (write-1), publish, log. Any other set status is cleared and logged
|
||||||
|
/// (GPE/Notify dispatch is M21.2). Always re-arm the line.
|
||||||
|
fn onSci() void {
|
||||||
|
var handled = false;
|
||||||
|
inline for (.{ pm1a_evt, pm1b_evt }) |evt_port| {
|
||||||
|
if (evt_port != 0) {
|
||||||
|
const sts: u16 = @truncate(halPioRead(2, evt_port));
|
||||||
|
if (sts & pwrbtn_bit != 0) {
|
||||||
|
halPioWrite(2, evt_port, pwrbtn_bit); // write-1-to-clear
|
||||||
|
handled = true;
|
||||||
|
} else if (sts != 0) {
|
||||||
|
halPioWrite(2, evt_port, sts); // clear whatever else latched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handled) {
|
||||||
|
_ = runtime.system.write("power: button pressed\n");
|
||||||
|
publishButton();
|
||||||
|
}
|
||||||
|
handleGpe();
|
||||||
|
_ = device.irqAck(node_id, sci_resource_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// General-purpose events: for each set+enabled GPE bit, evaluate its `\_GPE`
|
||||||
|
/// handler method (`_Lxx` level / `_Exx` edge), drain the Notify queue the
|
||||||
|
/// method produced, and publish an event per notified device. Then clear the
|
||||||
|
/// status bit. QEMU raises no GPEs on this config, so this path is exercised by
|
||||||
|
/// host unit tests (docs/m21-plan.md decision 5); on real hardware it carries
|
||||||
|
/// battery/AC/lid. The embedded controller's `_Qxx` queries are out of scope.
|
||||||
|
fn handleGpe() void {
|
||||||
|
handleGpeBlock(gpe0_blk, gpe0_len, 0);
|
||||||
|
handleGpeBlock(gpe1_blk, gpe1_len, gpe0_len * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleGpeBlock(blk: u16, len: u8, gpe_base: u32) void {
|
||||||
|
if (blk == 0 or len == 0) return;
|
||||||
|
const status_bytes = len / 2; // status half, then enable half
|
||||||
|
var byte_index: u8 = 0;
|
||||||
|
while (byte_index < status_bytes) : (byte_index += 1) {
|
||||||
|
const sts: u8 = @truncate(halPioRead(1, blk + byte_index));
|
||||||
|
const en: u8 = @truncate(halPioRead(1, blk + status_bytes + byte_index));
|
||||||
|
const active = sts & en;
|
||||||
|
if (active == 0) continue;
|
||||||
|
var bit: u3 = 0;
|
||||||
|
while (true) : (bit += 1) {
|
||||||
|
if (active & (@as(u8, 1) << bit) != 0) {
|
||||||
|
dispatchGpe(gpe_base + @as(u32, byte_index) * 8 + bit);
|
||||||
|
}
|
||||||
|
if (bit == 7) break;
|
||||||
|
}
|
||||||
|
halPioWrite(1, blk + byte_index, active); // write-1-to-clear the serviced bits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate the `\_GPE._L%02X` or `_E%02X` handler for GPE number `n`, then
|
||||||
|
/// publish an event for each device it notified.
|
||||||
|
fn dispatchGpe(n: u32) void {
|
||||||
|
const gpe_scope = aml.Namespace.resolve(&persistent_namespace, persistent_namespace.root, true, 0, &.{seg4("_GPE")}) orelse return;
|
||||||
|
var name: [4]u8 = .{ '_', 'L', 0, 0 };
|
||||||
|
writeHex2(name[2..4], n);
|
||||||
|
var method = aml.Namespace.childOf(gpe_scope, name);
|
||||||
|
if (method == null) {
|
||||||
|
name[1] = 'E';
|
||||||
|
method = aml.Namespace.childOf(gpe_scope, name);
|
||||||
|
}
|
||||||
|
const m = method orelse return; // no handler — the status bit was already cleared
|
||||||
|
_ = global_interpreter.evaluate(m, &.{}) catch return;
|
||||||
|
for (global_interpreter.takeNotifications()) |event| publishNotify(event.node, event.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publishNotify(node: *aml.Node, code: u64) void {
|
||||||
|
// Map the notified device's _HID to a domain event where we recognize it.
|
||||||
|
var hid: [8]u8 = .{0} ** 8;
|
||||||
|
if (readHid(node, &global_interpreter)) |h| hid = h;
|
||||||
|
const which: power.Event = if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) .battery else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) .ac else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) .lid else .notify;
|
||||||
|
var event = power.EventMessage{ .event = @intFromEnum(which), .code = @truncate(code) };
|
||||||
|
event.hid = hid;
|
||||||
|
writeLine("power: notify {s} code {d}\n", .{ hid[0..7], code });
|
||||||
|
publishEvent(std.mem.asBytes(&event));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two lowercase hex digits of `n` into `out[0..2]`.
|
||||||
|
fn writeHex2(out: []u8, n: u32) void {
|
||||||
|
const digits = "0123456789ABCDEF";
|
||||||
|
out[0] = digits[(n >> 4) & 0xF];
|
||||||
|
out[1] = digits[n & 0xF];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publishButton() void {
|
||||||
|
const event = power.EventMessage{ .event = @intFromEnum(power.Event.power_button) };
|
||||||
|
publishEvent(std.mem.asBytes(&event));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publishEvent(bytes: []const u8) void {
|
||||||
|
for (&subscribers) |*slot| {
|
||||||
|
if (slot.*) |handle| {
|
||||||
|
if (!runtime.ipc.send(handle, bytes)) slot.* = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isSubscriber(task: u32) bool {
|
||||||
|
for (&subscribers, 0..) |*slot, si| {
|
||||||
|
if (slot.* != null and subscriber_tasks[si] == task) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
|
||||||
|
/// Mirrors the kernel's power.zig sleepValue. Only reached from a PID-1
|
||||||
|
/// shutdown request (M21.3).
|
||||||
|
fn enterS5() void {
|
||||||
|
if (!s5_valid or pm1a_cnt == 0) {
|
||||||
|
_ = runtime.system.write("power: S5 unavailable\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ = runtime.system.write("power: entering S5\n");
|
||||||
|
halPioWrite(2, pm1a_cnt, (@as(u32, s5_slp_typ_a & 0x7) << 10) | slp_en);
|
||||||
|
if (pm1b_cnt != 0) halPioWrite(2, pm1b_cnt, (@as(u32, s5_slp_typ_b & 0x7) << 10) | slp_en);
|
||||||
|
// If control returns, the write did not take — say so instead of hanging.
|
||||||
|
runtime.system.sleep(500);
|
||||||
|
_ = runtime.system.write("power: S5 write did not take\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- harness callbacks --------------------------------------------------------
|
||||||
|
|
||||||
|
fn onNotification(badge: u64) void {
|
||||||
|
// The only notification the service binds is the SCI (an IRQ badge).
|
||||||
|
_ = badge;
|
||||||
|
onSci();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `.power` protocol: subscribe (endpoint as the call's capability),
|
||||||
|
/// shutdown (PID 1 only). Device discovery uses a different endpoint (the
|
||||||
|
/// device manager's), so nothing here handles ChildAdded.
|
||||||
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
if (message.len < 1) return 0;
|
||||||
|
switch (message[0]) {
|
||||||
|
@intFromEnum(power.Operation.subscribe) => {
|
||||||
|
var status: i32 = -1;
|
||||||
|
if (capability) |handle| {
|
||||||
|
for (&subscribers, 0..) |*slot, si| {
|
||||||
|
if (slot.* == null) {
|
||||||
|
slot.* = handle;
|
||||||
|
subscriber_tasks[si] = sender;
|
||||||
|
status = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const r = power.Reply{ .status = status };
|
||||||
|
@memcpy(reply[0..@sizeOf(power.Reply)], std.mem.asBytes(&r));
|
||||||
|
return @sizeOf(power.Reply);
|
||||||
|
},
|
||||||
|
@intFromEnum(power.Operation.shutdown) => {
|
||||||
|
// Honored only from a power subscriber — init, which has already run
|
||||||
|
// the stop sequence over everything else. The power service is
|
||||||
|
// mechanism (write S5); deciding *when* to shut down and stopping
|
||||||
|
// the rest of the system first is init's policy.
|
||||||
|
const allowed = isSubscriber(sender);
|
||||||
|
const r = power.Reply{ .status = if (allowed) 0 else -1 };
|
||||||
|
@memcpy(reply[0..@sizeOf(power.Reply)], std.mem.asBytes(&r));
|
||||||
|
if (allowed) enterS5();
|
||||||
|
return @sizeOf(power.Reply);
|
||||||
|
},
|
||||||
|
else => return 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Depth-first walk: register + report each present device with a _HID, then
|
||||||
|
/// descend. Scopes (\_SB, \_GPE …) are descended without producing a node.
|
||||||
|
fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter) void {
|
||||||
|
var child = node.first_child;
|
||||||
|
while (child) |c| : (child = c.next_sibling) {
|
||||||
|
if (c.kind != .device) {
|
||||||
|
walkDevices(c, interpreter);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!devicePresent(interpreter, c)) continue; // absent: skip it and its subtree
|
||||||
|
|
||||||
|
if (readHid(c, interpreter)) |hid| {
|
||||||
|
// Skip PCI roots — pci-bus already reports PCI functions; ACPI adds
|
||||||
|
// only the non-PCI _HID devices (docs/m19-m20-plan.md M20.2). The two
|
||||||
|
// roots are named through the shared registry, not bare _HID strings.
|
||||||
|
const id = acpi_ids.HardwareId.fromHid(hid[0..7]);
|
||||||
|
if (id != .pci_bus and id != .pci_express_root_bridge) {
|
||||||
|
registerDevice(c, hid, interpreter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walkDevices(c, interpreter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registerDevice(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter) void {
|
||||||
|
if (registered_count >= registered.len) return;
|
||||||
|
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
|
||||||
|
descriptor.class = @intFromEnum(device.DeviceClass.acpi_device);
|
||||||
|
descriptor.pci_class = device.no_pci_class;
|
||||||
|
const hid_len: u64 = std.mem.indexOfScalar(u8, &hid, 0) orelse hid.len;
|
||||||
|
descriptor.hid_len = hid_len;
|
||||||
|
@memcpy(descriptor.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]);
|
||||||
|
applyCrs(&descriptor, node, interpreter);
|
||||||
|
|
||||||
|
const id = device.register(node_id, &descriptor) orelse {
|
||||||
|
writeLine("acpi: register refused for {s}\n", .{hid[0..@intCast(hid_len)]});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
registered[registered_count] = .{ .hid = hid, .hid_len = @intCast(hid_len), .device_id = id, .resource_count = descriptor.resource_count };
|
||||||
|
registered_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// _STA bit 0 (present); absent method or a failed evaluation is treated as
|
||||||
|
/// present, per the ACPI rules.
|
||||||
|
fn devicePresent(interpreter: *aml.Interpreter, node: *aml.Node) bool {
|
||||||
|
const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true;
|
||||||
|
const obj = interpreter.evaluate(sta, &.{}) catch return true;
|
||||||
|
const status = obj.asInteger() catch return true;
|
||||||
|
return (status & 0x01) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The device's EISA-decoded _HID (e.g. "PNP0303"), or null.
|
||||||
|
fn readHid(node: *aml.Node, interpreter: *aml.Interpreter) ?[8]u8 {
|
||||||
|
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return null;
|
||||||
|
var buffer: [8]u8 = .{0} ** 8;
|
||||||
|
if (hid.kind == .method) {
|
||||||
|
const obj = interpreter.evaluate(hid, &.{}) catch return null;
|
||||||
|
switch (obj) {
|
||||||
|
.integer => |n| {
|
||||||
|
_ = eisaIdToStr(@truncate(n), &buffer);
|
||||||
|
return buffer;
|
||||||
|
},
|
||||||
|
else => return null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hid.kind != .name or hid.value.len == 0) return null;
|
||||||
|
const v = hid.value;
|
||||||
|
switch (v[0]) {
|
||||||
|
// A static _HID names an integer EISA id: Zero/One/Ones or a Byte/Word/DWord/
|
||||||
|
// QWord integer prefix. Anything else is not an integer we can EISA-decode.
|
||||||
|
opcodes.zero_opcode, opcodes.one_opcode, opcodes.ones_opcode, opcodes.byte_prefix, opcodes.word_prefix, opcodes.dword_prefix, opcodes.qword_prefix => {
|
||||||
|
var p: usize = 0;
|
||||||
|
const n = readIntObj(v, &p) orelse return null;
|
||||||
|
_ = eisaIdToStr(@truncate(n), &buffer);
|
||||||
|
return buffer;
|
||||||
|
},
|
||||||
|
else => return null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- _CRS resource-template decode (ported from the kernel's acpi.zig) --------
|
||||||
|
|
||||||
|
/// A resource template is a byte list of descriptors. Each starts with a tag byte whose
|
||||||
|
/// high bit picks the encoding: a *small* descriptor carries its type in bits [6:3] and
|
||||||
|
/// its length in bits [2:0]; a *large* descriptor is the whole tag byte, followed by a
|
||||||
|
/// 16-bit length. These are the descriptor types danos decodes into resources — named so
|
||||||
|
/// the walk below reads by descriptor, not by 0x04/0x85/… (docs/coding-standards.md).
|
||||||
|
const large_descriptor_bit: u8 = 0x80; // set in a tag byte => large descriptor
|
||||||
|
const small_length_mask: u8 = 0x07; // low 3 bits of a small tag = body length
|
||||||
|
const small_type_shift: u3 = 3; // small type sits in bits [6:3]
|
||||||
|
|
||||||
|
/// Small resource descriptor types (tag bits [6:3]). Non-exhaustive: an unhandled type
|
||||||
|
/// is skipped by its length, not misread.
|
||||||
|
const SmallResourceType = enum(u8) {
|
||||||
|
irq = 0x04,
|
||||||
|
io_port = 0x08,
|
||||||
|
fixed_io_port = 0x09,
|
||||||
|
end_tag = 0x0F,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Large resource descriptor types (the whole tag byte). Non-exhaustive for the same reason.
|
||||||
|
const LargeResourceType = enum(u8) {
|
||||||
|
memory32 = 0x85,
|
||||||
|
memory32_fixed = 0x86,
|
||||||
|
extended_irq = 0x89,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn applyCrs(descriptor: *device.DeviceDescriptor, node: *aml.Node, interpreter: *aml.Interpreter) void {
|
||||||
|
const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return;
|
||||||
|
const obj = interpreter.evaluate(crs, &.{}) catch return;
|
||||||
|
const bytes = switch (obj) {
|
||||||
|
.buffer => |b| b,
|
||||||
|
else => return,
|
||||||
|
};
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < bytes.len) {
|
||||||
|
const tag = bytes[i];
|
||||||
|
if (tag & large_descriptor_bit == 0) {
|
||||||
|
const len: usize = tag & small_length_mask;
|
||||||
|
const body = i + 1;
|
||||||
|
if (body + len > bytes.len) break;
|
||||||
|
switch (@as(SmallResourceType, @enumFromInt((tag >> small_type_shift) & 0x0F))) {
|
||||||
|
.irq => if (len >= 2) { // IRQ mask
|
||||||
|
const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8);
|
||||||
|
var b: usize = 0;
|
||||||
|
while (b < 16) : (b += 1) {
|
||||||
|
if (mask & (@as(u16, 1) << @intCast(b)) != 0) addResource(descriptor, .irq, b, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.io_port => if (len >= 7) addResource(descriptor, .io_port, rd16(bytes, body + 1), bytes[body + 6]),
|
||||||
|
.fixed_io_port => if (len >= 3) addResource(descriptor, .io_port, rd16(bytes, body), bytes[body + 2]),
|
||||||
|
.end_tag => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
i = body + len;
|
||||||
|
} else {
|
||||||
|
if (i + 3 > bytes.len) break;
|
||||||
|
const len: usize = @intCast(rd16(bytes, i + 1));
|
||||||
|
const body = i + 3;
|
||||||
|
if (body + len > bytes.len) break;
|
||||||
|
switch (@as(LargeResourceType, @enumFromInt(tag))) {
|
||||||
|
.memory32 => if (len >= 17) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 13)),
|
||||||
|
.memory32_fixed => if (len >= 9) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 5)),
|
||||||
|
.extended_irq => if (len >= 2) {
|
||||||
|
const count = bytes[body + 1];
|
||||||
|
var k: usize = 0;
|
||||||
|
while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) {
|
||||||
|
addResource(descriptor, .irq, rd32(bytes, body + 2 + k * 4), 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
i = body + len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addResource(descriptor: *device.DeviceDescriptor, kind: device.ResourceKind, start: u64, len: u64) void {
|
||||||
|
if (descriptor.resource_count >= descriptor.resources.len) return;
|
||||||
|
descriptor.resources[@intCast(descriptor.resource_count)] = .{ .kind = @intFromEnum(kind), .start = start, .len = len };
|
||||||
|
descriptor.resource_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- small helpers ported verbatim from the kernel's acpi.zig ----------------
|
||||||
|
|
||||||
|
fn seg4(comptime s: *const [4:0]u8) [4]u8 {
|
||||||
|
return s[0..4].*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hexDigit(n: u8) u8 {
|
||||||
|
return if (n < 10) '0' + n else 'A' + (n - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eisaIdToStr(id: u32, buffer: *[8]u8) []const u8 {
|
||||||
|
const b0: u16 = @intCast(id & 0xFF);
|
||||||
|
const b1: u16 = @intCast((id >> 8) & 0xFF);
|
||||||
|
const b2: u8 = @truncate(id >> 16);
|
||||||
|
const b3: u8 = @truncate(id >> 24);
|
||||||
|
const mfg = (b0 << 8) | b1;
|
||||||
|
buffer[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F));
|
||||||
|
buffer[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F));
|
||||||
|
buffer[2] = '@' + @as(u8, @intCast(mfg & 0x1F));
|
||||||
|
buffer[3] = hexDigit((b2 >> 4) & 0xF);
|
||||||
|
buffer[4] = hexDigit(b2 & 0xF);
|
||||||
|
buffer[5] = hexDigit((b3 >> 4) & 0xF);
|
||||||
|
buffer[6] = hexDigit(b3 & 0xF);
|
||||||
|
buffer[7] = 0;
|
||||||
|
return buffer[0..7];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readIntObj(bytes: []const u8, p: *usize) ?u64 {
|
||||||
|
if (p.* >= bytes.len) return null;
|
||||||
|
const op = bytes[p.*];
|
||||||
|
p.* += 1;
|
||||||
|
switch (op) {
|
||||||
|
opcodes.zero_opcode => return 0,
|
||||||
|
opcodes.one_opcode => return 1,
|
||||||
|
opcodes.ones_opcode => return 1,
|
||||||
|
opcodes.byte_prefix => {
|
||||||
|
if (p.* >= bytes.len) return null;
|
||||||
|
const v = bytes[p.*];
|
||||||
|
p.* += 1;
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
opcodes.word_prefix => {
|
||||||
|
if (p.* + 2 > bytes.len) return null;
|
||||||
|
const v = rd16(bytes, p.*);
|
||||||
|
p.* += 2;
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
opcodes.dword_prefix => {
|
||||||
|
if (p.* + 4 > bytes.len) return null;
|
||||||
|
const v = rd32(bytes, p.*);
|
||||||
|
p.* += 4;
|
||||||
|
return v;
|
||||||
|
},
|
||||||
|
else => return null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rd16(bytes: []const u8, off: usize) u64 {
|
||||||
|
return @as(u64, bytes[off]) | (@as(u64, bytes[off + 1]) << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rd32(bytes: []const u8, off: usize) u64 {
|
||||||
|
return rd16(bytes, off) | (rd16(bytes, off + 2) << 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const panic = runtime.panic;
|
pub const panic = runtime.panic;
|
||||||
|
|||||||
@@ -73,8 +73,17 @@ pub const ChildAdded = extern struct {
|
|||||||
parent: u64,
|
parent: u64,
|
||||||
/// Where on the bus (for USB: the root port number, 1-based).
|
/// Where on the bus (for USB: the root port number, 1-based).
|
||||||
bus_address: u64,
|
bus_address: u64,
|
||||||
/// Bus-specific identity (for USB: the PORTSC port-speed class).
|
/// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI:
|
||||||
|
/// the class triple; for ACPI devices, 0 — identity is the hid below).
|
||||||
identity: u64,
|
identity: u64,
|
||||||
|
/// The kernel device id this child was `device_register`ed as — what the
|
||||||
|
/// manager hands a matched driver as its argv assignment — or `no_device`
|
||||||
|
/// for an unregistered leaf (a USB port before the descriptor track).
|
||||||
|
device_id: u64 = no_device,
|
||||||
|
/// The ACPI hardware id (`_HID`), EISA-decoded (e.g. "PNP0303"), for devices
|
||||||
|
/// discovered by firmware string rather than a numeric bus identity. Empty
|
||||||
|
/// (all zero) otherwise. Widens for FDT `compatible` strings later.
|
||||||
|
hid: [8]u8 = .{0} ** 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const child_added_size = @sizeOf(ChildAdded);
|
pub const child_added_size = @sizeOf(ChildAdded);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
const acpi_ids = @import("acpi-ids");
|
const acpi_ids = @import("acpi-ids");
|
||||||
|
const pci_class = @import("pci-class");
|
||||||
const protocol = runtime.device_manager_protocol;
|
const protocol = runtime.device_manager_protocol;
|
||||||
const device = runtime.device;
|
const device = runtime.device;
|
||||||
const system = runtime.system;
|
const system = runtime.system;
|
||||||
@@ -34,31 +35,50 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
|||||||
/// this comes from a manifest (docs/device-manager.md: the third bus type
|
/// this comes from a manifest (docs/device-manager.md: the third bus type
|
||||||
/// triggers it); for now a static map. `null` = no driver for this class yet.
|
/// triggers it); for now a static map. `null` = no driver for this class yet.
|
||||||
fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
fn driverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
||||||
// detect device via DeviceClass
|
// The HPET timer node is still kernel-seeded (from the HPET table, not AML).
|
||||||
|
// PS/2 and other _HID devices now arrive as acpi-service reports and match
|
||||||
|
// in onChildAdded (M20.3), not from this boot snapshot.
|
||||||
if (d.class == @intFromEnum(device.DeviceClass.timer)) return "hpet";
|
if (d.class == @intFromEnum(device.DeviceClass.timer)) return "hpet";
|
||||||
// detect device via hid
|
return null;
|
||||||
const hid = d.hid[0..@intCast(d.hid_len)];
|
}
|
||||||
const id = acpi_ids.HardwareId.fromHid(hid) orelse return null;
|
|
||||||
return switch (id) {
|
/// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller —
|
||||||
.ps2_keyboard, .ps2_mouse => "ps2-bus",
|
/// Serial Bus Controller / USB Controller / XHCI — named from pci-class.zig rather
|
||||||
|
/// than written as the bare 0x0C0330 (docs/coding-standards.md, "Named values").
|
||||||
|
const xhci_pci_class: u64 = pci_class.ClassCode.pack(.{
|
||||||
|
.base = @intFromEnum(pci_class.BaseClass.serial_bus),
|
||||||
|
.subclass = @intFromEnum(pci_class.serial_bus.SubClass.usb),
|
||||||
|
.prog_if = @intFromEnum(pci_class.serial_bus.usb.ProgIf.xhci),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The driver that serves a *reported* PCI function (M19.3: matching moved
|
||||||
|
/// from the boot snapshot to the bus reports), or null. A machine can carry
|
||||||
|
/// several identical controllers — one driver instance per reported device,
|
||||||
|
/// its registered id as argv[1].
|
||||||
|
fn pciDriverForIdentity(identity: u64) ?[]const u8 {
|
||||||
|
return switch (identity) {
|
||||||
|
xhci_pci_class => "usb-xhci-bus",
|
||||||
else => null,
|
else => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller:
|
/// The driver that serves a *reported* ACPI device by its `_HID` (M20.3:
|
||||||
/// Serial Bus Controller (0x0C) / USB Controller (0x03) / XHCI (0x30) — the names
|
/// ps2-bus now binds the PS/2 nodes the acpi service reports, not boot-snapshot
|
||||||
/// pci-class.zig decodes.
|
/// nodes the kernel used to build). ps2-bus is a singleton that finds both its
|
||||||
const xhci_pci_class: u64 = 0x0C_03_30;
|
/// devices by hid once spawned, so keyboard and mouse map to the same name.
|
||||||
|
fn hidDriverFor(hid: []const u8) ?[]const u8 {
|
||||||
|
if (std.mem.eql(u8, hid, "PNP0303")) return "ps2-bus"; // PS/2 keyboard
|
||||||
|
if (std.mem.eql(u8, hid, "PNP0F13")) return "ps2-bus"; // PS/2 mouse
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// The bus driver that serves a PCI function, or null. A machine can carry
|
/// Whether some driver entry already serves registered device `device_id` —
|
||||||
/// several identical controllers — one driver instance per device, the id as
|
/// a re-report after a bus restart must not spawn a second instance.
|
||||||
/// argv[1]. These drivers speak the protocol: a hello is expected.
|
fn driverForDevice(device_id: u64) bool {
|
||||||
fn pciDriverFor(d: device.DeviceDescriptor) ?[]const u8 {
|
for (&drivers) |*driver| {
|
||||||
if (d.class != @intFromEnum(device.DeviceClass.pci_device)) return null;
|
if (driver.used and driver.device_id == device_id) return true;
|
||||||
return switch (d.pci_class) {
|
}
|
||||||
xhci_pci_class => "usb-xhci-bus",
|
return false;
|
||||||
else => null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- supervision -------------------------------------------------------------
|
// --- supervision -------------------------------------------------------------
|
||||||
@@ -108,6 +128,7 @@ var manager_endpoint: runtime.ipc.Handle = 0;
|
|||||||
var test_restart_mode = false;
|
var test_restart_mode = false;
|
||||||
var test_usb_restart_mode = false;
|
var test_usb_restart_mode = false;
|
||||||
var test_usb_killed = false;
|
var test_usb_killed = false;
|
||||||
|
var test_pci_restart_mode = false;
|
||||||
var test_kill_pid: u32 = 0;
|
var test_kill_pid: u32 = 0;
|
||||||
var test_kill_due_ns: u64 = 0;
|
var test_kill_due_ns: u64 = 0;
|
||||||
|
|
||||||
@@ -136,27 +157,30 @@ const Child = struct {
|
|||||||
parent: u64 = 0,
|
parent: u64 = 0,
|
||||||
bus_address: u64 = 0,
|
bus_address: u64 = 0,
|
||||||
identity: u64 = 0,
|
identity: u64 = 0,
|
||||||
|
// The kernel device id (registered by the reporter), or protocol.no_device.
|
||||||
|
device_id: u64 = 0,
|
||||||
reporter: u32 = 0, // the reporting driver instance's process id
|
reporter: u32 = 0, // the reporting driver instance's process id
|
||||||
};
|
};
|
||||||
|
|
||||||
const maximum_children = 32;
|
const maximum_children = 64; // ACPI adds ~34 device nodes (M20.2), plus PCI + USB
|
||||||
var children: [maximum_children]Child = .{Child{}} ** maximum_children;
|
var children: [maximum_children]Child = .{Child{}} ** maximum_children;
|
||||||
|
|
||||||
/// Record (or refresh) a reported child. Refreshing matters: a restarted bus
|
/// Record (or refresh) a reported child. Refreshing matters: a restarted bus
|
||||||
/// driver re-reports what it rediscovers, and the same (parent, port) must not
|
/// driver re-reports what it rediscovers, and the same (parent, port) must not
|
||||||
/// duplicate.
|
/// duplicate.
|
||||||
fn addChild(parent: u64, bus_address: u64, identity: u64, reporter: u32) bool {
|
fn addChild(parent: u64, bus_address: u64, identity: u64, device_id: u64, reporter: u32) bool {
|
||||||
var free: ?*Child = null;
|
var free: ?*Child = null;
|
||||||
for (&children) |*child| {
|
for (&children) |*child| {
|
||||||
if (child.used and child.parent == parent and child.bus_address == bus_address) {
|
if (child.used and child.parent == parent and child.bus_address == bus_address) {
|
||||||
child.identity = identity;
|
child.identity = identity;
|
||||||
|
child.device_id = device_id;
|
||||||
child.reporter = reporter;
|
child.reporter = reporter;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!child.used and free == null) free = child;
|
if (!child.used and free == null) free = child;
|
||||||
}
|
}
|
||||||
const slot = free orelse return false;
|
const slot = free orelse return false;
|
||||||
slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .reporter = reporter };
|
slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .device_id = device_id, .reporter = reporter };
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,11 +339,15 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|||||||
|
|
||||||
var matched: usize = 0;
|
var matched: usize = 0;
|
||||||
for (buffer[0..count]) |descriptor| {
|
for (buffer[0..count]) |descriptor| {
|
||||||
if (pciDriverFor(descriptor)) |driver_name| {
|
if (descriptor.class == @intFromEnum(device.DeviceClass.pci_host_bridge)) {
|
||||||
|
// The PCI bus driver: enumeration in ring 3 (M19), one instance
|
||||||
|
// per bridge, the bridge id as its assignment.
|
||||||
matched += 1;
|
matched += 1;
|
||||||
addDriver(driver_name, descriptor.id, true);
|
addDriver("pci-bus", descriptor.id, true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// PCI functions no longer appear in the boot snapshot (M19.3): the
|
||||||
|
// pci-bus driver reports them, and onChildAdded matches from reports.
|
||||||
const driver_name = driverFor(descriptor) orelse continue;
|
const driver_name = driverFor(descriptor) orelse continue;
|
||||||
matched += 1;
|
matched += 1;
|
||||||
// Skip a singleton that is already alive (the initial-ramdisk sweep test
|
// Skip a singleton that is already alive (the initial-ramdisk sweep test
|
||||||
@@ -330,6 +358,12 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The discovery service (docs/m19-m20-plan.md M20): one per firmware, packed
|
||||||
|
// under the neutral name "discovery", spawned once at startup. It finds and
|
||||||
|
// claims the acpi-tables (or devicetree-blob) node itself. Not a per-device
|
||||||
|
// match — it is the discoverer, not a driver bound to one device.
|
||||||
|
addDriver("discovery", protocol.no_device, false);
|
||||||
|
|
||||||
if (test_restart_mode) {
|
if (test_restart_mode) {
|
||||||
// The driver-restart scenario's fixture: claims device 0 (the tree
|
// The driver-restart scenario's fixture: claims device 0 (the tree
|
||||||
// root, otherwise unclaimed), hellos, then faults — driving backoff,
|
// root, otherwise unclaimed), hellos, then faults — driving backoff,
|
||||||
@@ -382,22 +416,57 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
|
|||||||
const report = std.mem.bytesToValue(protocol.ChildAdded, message[0..protocol.child_added_size]);
|
const report = std.mem.bytesToValue(protocol.ChildAdded, message[0..protocol.child_added_size]);
|
||||||
var status: i32 = 0;
|
var status: i32 = 0;
|
||||||
if (driverByProcess(sender)) |driver| {
|
if (driverByProcess(sender)) |driver| {
|
||||||
if (!addChild(report.parent, report.bus_address, report.identity, sender)) status = -1;
|
if (!addChild(report.parent, report.bus_address, report.identity, report.device_id, sender)) status = -1;
|
||||||
writeLine("device-manager: child added (device {d} port {d}, identity {d}) by {s}\n", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
writeLine("device-manager: child added (device {d} port {d}, identity {d}) by {s}\n", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
||||||
if (status == 0) publishEvent(message[0..protocol.child_added_size]);
|
if (status == 0) publishEvent(message[0..protocol.child_added_size]);
|
||||||
|
// Matching from reports (M19.3): a registered child whose identity
|
||||||
|
// names a driver gets one, once — re-reports after a bus restart
|
||||||
|
// dedupe on the registered id, exactly like the registrations do.
|
||||||
|
if (status == 0 and report.device_id != protocol.no_device) {
|
||||||
|
if (pciDriverForIdentity(report.identity)) |child_driver| {
|
||||||
|
if (!driverForDevice(report.device_id)) addDriver(child_driver, report.device_id, true);
|
||||||
|
}
|
||||||
|
// ACPI _HID match (M20.3): ps2-bus is a singleton that finds its own
|
||||||
|
// devices by hid, so spawn it once, without a device assignment.
|
||||||
|
const hid_len = std.mem.indexOfScalar(u8, &report.hid, 0) orelse report.hid.len;
|
||||||
|
if (hid_len != 0) {
|
||||||
|
if (hidDriverFor(report.hid[0..hid_len])) |hid_driver| {
|
||||||
|
if (!alreadySupervised(hid_driver)) addDriver(hid_driver, protocol.no_device, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
status = -1;
|
status = -1;
|
||||||
}
|
}
|
||||||
const report_reply = protocol.ReportReply{ .status = status };
|
const report_reply = protocol.ReportReply{ .status = status };
|
||||||
@memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
@memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
||||||
|
if (test_pci_restart_mode and !test_usb_killed) {
|
||||||
|
if (driverByProcess(sender)) |driver| {
|
||||||
|
if (std.mem.eql(u8, driver.name(), "pci-bus") and childCountOf(sender) >= 3) {
|
||||||
|
// The pci restart drill: kill the enumerator after it has
|
||||||
|
// reported; the respawn must re-register without duplicates
|
||||||
|
// (M19.0 idempotence, proven end to end by pci-scan).
|
||||||
|
test_usb_killed = true;
|
||||||
|
test_kill_pid = sender;
|
||||||
|
test_kill_due_ns = system.clock() + 1_000_000_000;
|
||||||
|
_ = system.timerOnce(manager_endpoint, 1100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) {
|
if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) {
|
||||||
// Delayed, not immediate: the device-list scenario's subscriber needs a
|
// Only the xHCI reporter is the drill's victim — pci-bus also reports
|
||||||
// window to enumerate and subscribe before the events start.
|
// now, and whichever finishes second must not trigger the kill.
|
||||||
|
if (driverByProcess(sender)) |driver| {
|
||||||
|
if (std.mem.eql(u8, driver.name(), "usb-xhci-bus")) {
|
||||||
|
// Delayed, not immediate: the device-list scenario's subscriber
|
||||||
|
// needs a window to enumerate and subscribe before the events.
|
||||||
test_usb_killed = true;
|
test_usb_killed = true;
|
||||||
test_kill_pid = sender;
|
test_kill_pid = sender;
|
||||||
test_kill_due_ns = system.clock() + 2_000_000_000;
|
test_kill_due_ns = system.clock() + 2_000_000_000;
|
||||||
_ = system.timerOnce(manager_endpoint, 2100);
|
_ = system.timerOnce(manager_endpoint, 2100);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return @sizeOf(protocol.ReportReply);
|
return @sizeOf(protocol.ReportReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,6 +535,7 @@ pub fn main(init: runtime.process.Init) void {
|
|||||||
if (init.arguments.get(1)) |mode| {
|
if (init.arguments.get(1)) |mode| {
|
||||||
test_restart_mode = std.mem.eql(u8, mode, "test-restart");
|
test_restart_mode = std.mem.eql(u8, mode, "test-restart");
|
||||||
test_usb_restart_mode = std.mem.eql(u8, mode, "test-usb-restart");
|
test_usb_restart_mode = std.mem.eql(u8, mode, "test-usb-restart");
|
||||||
|
test_pci_restart_mode = std.mem.eql(u8, mode, "test-pci-restart");
|
||||||
}
|
}
|
||||||
runtime.service.run(protocol.message_maximum, .{
|
runtime.service.run(protocol.message_maximum, .{
|
||||||
.service = .device_manager,
|
.service = .device_manager,
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
|
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
|
|
||||||
pub fn main() void {
|
pub fn main(init: runtime.process.Init) void {
|
||||||
|
_ = init;
|
||||||
// Not implemented: exit cleanly and silently (a bare spawn by the
|
// Not implemented: exit cleanly and silently (a bare spawn by the
|
||||||
// initial-ramdisk sweep must not derange other tests' markers). The
|
// initial-ramdisk sweep must not derange other tests' markers). The
|
||||||
// supervisor reads a clean exit as "meant to stop" — correct for a
|
// supervisor reads a clean exit as "meant to stop" — correct for a
|
||||||
|
|||||||
@@ -6,12 +6,20 @@
|
|||||||
//!
|
//!
|
||||||
//! It proves the C-convention heap works, then — as PID 1 — acts as the system's
|
//! It proves the C-convention heap works, then — as PID 1 — acts as the system's
|
||||||
//! **service supervisor**: it spawns the user-space services danos brings up at boot
|
//! **service supervisor**: it spawns the user-space services danos brings up at boot
|
||||||
//! (the VFS server, the device manager), and settles into a heartbeat so it stays
|
//! (the VFS server, the device manager), and settles into an event loop as the root
|
||||||
//! alive as the root of user space. Drivers are *not* its job: the device manager
|
//! of user space. Drivers are *not* its job: the device manager discovers the
|
||||||
//! discovers the hardware and spawns those. This is the service half of the
|
//! hardware and spawns those. This is the service half of the service/driver spawn
|
||||||
//! service/driver spawn split (docs/driver-model.md).
|
//! split (docs/driver-model.md).
|
||||||
|
//!
|
||||||
|
//! M21: init also owns **orderly shutdown**. It supervises its children (keeping
|
||||||
|
//! their ids and an exit endpoint), subscribes to the power service, and on a
|
||||||
|
//! power-button event runs the stop sequence over its children in reverse order
|
||||||
|
//! before asking the power service to enter S5 — lifecycle (M17) and events (M21)
|
||||||
|
//! composing into a clean poweroff.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
|
const power = runtime.power_protocol;
|
||||||
|
|
||||||
/// The system services init brings up at boot, in order. This is init's policy — the
|
/// The system services init brings up at boot, in order. This is init's policy — the
|
||||||
/// microkernel keeps such choices in user space, not the kernel. Drivers are absent
|
/// microkernel keeps such choices in user space, not the kernel. Drivers are absent
|
||||||
@@ -19,6 +27,10 @@ const runtime = @import("runtime");
|
|||||||
/// manifest under /system/services instead of a hardcoded list.)
|
/// manifest under /system/services instead of a hardcoded list.)
|
||||||
const boot_services = [_][]const u8{ "vfs", "input", "device-manager" };
|
const boot_services = [_][]const u8{ "vfs", "input", "device-manager" };
|
||||||
|
|
||||||
|
var children: [boot_services.len]u32 = .{0} ** boot_services.len;
|
||||||
|
var child_count: usize = 0;
|
||||||
|
var supervision_endpoint: runtime.ipc.Handle = 0;
|
||||||
|
|
||||||
pub fn main() void {
|
pub fn main() void {
|
||||||
// Prove the heap end to end: allocate through the runtime allocator (which
|
// Prove the heap end to end: allocate through the runtime allocator (which
|
||||||
// mmaps pages from the kernel and carves them with the free list), write into
|
// mmaps pages from the kernel and carves them with the free list), write into
|
||||||
@@ -34,17 +46,92 @@ pub fn main() void {
|
|||||||
gpa.free(buffer);
|
gpa.free(buffer);
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
|
|
||||||
// Bring up the boot services. Best-effort and silent: each service announces its
|
// One endpoint carries everything init waits on: children's exit
|
||||||
// own readiness (`vfs: ready`, ...), and in an isolation test that runs init with
|
// notifications (they are spawned supervised against it), init's own
|
||||||
// no initial-ramdisk the spawns simply no-op rather than deranging the heartbeat.
|
// signals, and power events it subscribes to. All arrive in the loop below.
|
||||||
|
supervision_endpoint = runtime.ipc.createIpcEndpoint() orelse {
|
||||||
|
_ = runtime.system.write("init: no endpoint\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = runtime.process.bindSignals(supervision_endpoint);
|
||||||
|
|
||||||
|
// Bring up the boot services, supervised so init can stop them cleanly.
|
||||||
|
// Best-effort and silent: each service announces its own readiness, and in
|
||||||
|
// an isolation test with no initial-ramdisk the spawns simply no-op.
|
||||||
for (boot_services) |service| {
|
for (boot_services) |service| {
|
||||||
_ = runtime.system.spawn(service);
|
if (runtime.system.spawnSupervised(service, &.{}, supervision_endpoint)) |id| {
|
||||||
|
children[child_count] = id;
|
||||||
|
child_count += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe to power events (retry: the power service registers well after
|
||||||
|
// init starts). Best-effort — without it, a `terminate` signal still
|
||||||
|
// triggers the same shutdown path.
|
||||||
|
subscribePower();
|
||||||
|
|
||||||
|
// A re-arming timer drives the liveness heartbeat: proof PID 1 is alive
|
||||||
|
// (the init test's marker) while the loop stays free to receive signals,
|
||||||
|
// power events, and children's exit notifications.
|
||||||
|
_ = runtime.system.timerOnce(supervision_endpoint, 1000);
|
||||||
|
|
||||||
|
var receive: [power.message_maximum]u8 = undefined;
|
||||||
while (true) {
|
while (true) {
|
||||||
_ = runtime.system.write("init: heartbeat\n");
|
const got = runtime.ipc.replyWait(supervision_endpoint, &.{}, &receive, null);
|
||||||
runtime.system.sleep(1000);
|
if (runtime.process.signalsFrom(got.badge)) |signals| {
|
||||||
|
if (signals.has(.terminate)) shutDown();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if (got.isTimer()) {
|
||||||
|
_ = runtime.system.write("init: heartbeat\n");
|
||||||
|
_ = runtime.system.timerOnce(supervision_endpoint, 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (got.isMessage() and got.len >= 2 and receive[0] == @intFromEnum(power.Operation.event)) {
|
||||||
|
// A power event (the only buffered messages init receives).
|
||||||
|
if (receive[1] == @intFromEnum(power.Event.power_button)) shutDown();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Child-exit notifications and anything else: keep waiting.
|
||||||
|
if (got.isNotification()) continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up the power service and subscribe our endpoint (handed over as the
|
||||||
|
/// call's capability) so events arrive as buffered messages here.
|
||||||
|
fn subscribePower() void {
|
||||||
|
var handle: ?runtime.ipc.Handle = null;
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (handle == null and tries < 200) : (tries += 1) {
|
||||||
|
handle = runtime.ipc.lookup(.power);
|
||||||
|
if (handle == null) runtime.system.sleep(20);
|
||||||
|
}
|
||||||
|
// A missing power service is not fatal — init proceeds to its heartbeat and
|
||||||
|
// a `terminate` signal still drives shutdown. Silent so the no-ramdisk init
|
||||||
|
// test's heartbeat marker is the next line written.
|
||||||
|
const h = handle orelse return;
|
||||||
|
const request = power.Subscribe{};
|
||||||
|
var reply: [power.message_maximum]u8 = undefined;
|
||||||
|
_ = runtime.ipc.callCap(h, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stop sequence: terminate each child in reverse spawn order (vfs last —
|
||||||
|
/// other services may flush through it), waiting up to a deadline for each to
|
||||||
|
/// exit before killing it, then ask the power service to enter S5.
|
||||||
|
fn shutDown() void {
|
||||||
|
_ = runtime.system.write("init: shutting down\n");
|
||||||
|
var i = child_count;
|
||||||
|
while (i > 0) {
|
||||||
|
i -= 1;
|
||||||
|
if (children[i] != 0) runtime.process.stop(children[i], 2000, supervision_endpoint);
|
||||||
|
}
|
||||||
|
if (runtime.ipc.lookup(.power)) |h| {
|
||||||
|
const request = power.Shutdown{};
|
||||||
|
var reply: [power.message_maximum]u8 = undefined;
|
||||||
|
_ = runtime.ipc.call(h, std.mem.asBytes(&request), &reply) catch {};
|
||||||
|
}
|
||||||
|
// If S5 did not take, init has nothing left to do but idle.
|
||||||
|
while (true) runtime.system.sleep(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const panic = runtime.panic;
|
pub const panic = runtime.panic;
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//! The power protocol (docs/m21-plan.md): system power's domain-named surface,
|
||||||
|
//! registered under `ServiceId.power`. On x86 the acpi service serves it; on
|
||||||
|
//! ARM a PSCI/mailbox service will register the same id — subscribers never
|
||||||
|
//! learn which firmware they are on (m19-m20-plan.md decision 7). The
|
||||||
|
//! vfs-protocol pattern: extern-struct messages, a version, reserved fields.
|
||||||
|
|
||||||
|
/// The protocol version a client states nowhere yet — reserved for the day a
|
||||||
|
/// handshake needs it; requests carry it so a mismatch can be refused loudly.
|
||||||
|
pub const version: u16 = 1;
|
||||||
|
|
||||||
|
pub const Operation = enum(u8) {
|
||||||
|
/// Subscribe to power events: the subscriber's endpoint rides as the
|
||||||
|
/// call's capability (the input/device-manager pattern); events arrive on
|
||||||
|
/// it as buffered messages carrying an `EventMessage`.
|
||||||
|
subscribe = 1,
|
||||||
|
/// Orderly shutdown's last step: enter S5. Accepted only from PID 1
|
||||||
|
/// (init) — the process that has already run the stop sequence over
|
||||||
|
/// everything else.
|
||||||
|
shutdown = 2,
|
||||||
|
/// The published event payload (never sent *to* the service).
|
||||||
|
event = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// What happened. The vocabulary is hardware-neutral: a lid is a lid whether
|
||||||
|
/// ACPI or a PSCI mailbox reported it.
|
||||||
|
pub const Event = enum(u8) {
|
||||||
|
power_button = 1,
|
||||||
|
lid = 2,
|
||||||
|
ac = 3,
|
||||||
|
battery = 4,
|
||||||
|
/// A device notification that maps to none of the named events — the
|
||||||
|
/// `code` and `hid` fields say which device and what code.
|
||||||
|
notify = 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Subscribe = extern struct {
|
||||||
|
operation: u8 = @intFromEnum(Operation.subscribe),
|
||||||
|
reserved0: u8 = 0,
|
||||||
|
version: u16 = version,
|
||||||
|
reserved1: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Shutdown = extern struct {
|
||||||
|
operation: u8 = @intFromEnum(Operation.shutdown),
|
||||||
|
reserved0: u8 = 0,
|
||||||
|
version: u16 = version,
|
||||||
|
reserved1: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A published event, as the buffered-message payload subscribers receive.
|
||||||
|
pub const EventMessage = extern struct {
|
||||||
|
operation: u8 = @intFromEnum(Operation.event),
|
||||||
|
/// An Event value.
|
||||||
|
event: u8,
|
||||||
|
reserved0: u16 = 0,
|
||||||
|
/// The device notification code (Notify's second argument), or 0.
|
||||||
|
code: u32 = 0,
|
||||||
|
/// The notifying device's hardware id (EISA-decoded), or all zero.
|
||||||
|
hid: [8]u8 = .{0} ** 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Reply = extern struct {
|
||||||
|
status: i32,
|
||||||
|
reserved: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Upper bound on any message in this protocol — sizes endpoint buffers.
|
||||||
|
pub const message_maximum = 64;
|
||||||
+122
-35
@@ -18,9 +18,11 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -55,20 +57,16 @@ ARCHES = {
|
|||||||
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", # macOS Homebrew (Apple Silicon)
|
"/opt/homebrew/share/qemu/edk2-i386-vars.fd", # macOS Homebrew (Apple Silicon)
|
||||||
"/usr/local/share/qemu/edk2-i386-vars.fd", # macOS Homebrew (Intel)
|
"/usr/local/share/qemu/edk2-i386-vars.fd", # macOS Homebrew (Intel)
|
||||||
],
|
],
|
||||||
# zig-out is a FHS-shaped image and the boot volume; the harness copies the
|
# zig-out is itself the FHS-shaped boot volume (docs/efi.md): the build
|
||||||
# boot-critical files from their FHS paths into a fresh ESP with the same
|
# installs BOOTX64.efi, the kernel, init, and the initial-ramdisk at their
|
||||||
# layout. (dest in ESP, source path under zig-out) — identical here.
|
# boot paths. The harness presents zig-out to the guest directly — exactly
|
||||||
"efi_app": ("EFI/BOOT/BOOTX64.efi", "EFI/BOOT/BOOTX64.efi"),
|
# as `zig build run-x86-64` does — so there is no separate ESP to assemble.
|
||||||
"kernel": ("system/kernel", "system/kernel"),
|
|
||||||
# The init user program and the initial-ramdisk (VFS server + drivers).
|
|
||||||
"extra": [("system/services/init", "system/services/init"),
|
|
||||||
("boot/initial-ramdisk.img", "boot/initial-ramdisk.img")],
|
|
||||||
# Built as a function so we can splice in per-run paths.
|
# Built as a function so we can splice in per-run paths.
|
||||||
"qemu_args": lambda a, esp, vars_fd, serial: [
|
"qemu_args": lambda a, boot_volume, vars_fd, serial: [
|
||||||
"-machine", "q35", "-m", "128M",
|
"-machine", "q35", "-m", "128M",
|
||||||
"-drive", f"if=pflash,format=raw,readonly=on,file={a['ovmf_code']}",
|
"-drive", f"if=pflash,format=raw,readonly=on,file={a['ovmf_code']}",
|
||||||
"-drive", f"if=pflash,format=raw,file={vars_fd}",
|
"-drive", f"if=pflash,format=raw,file={vars_fd}",
|
||||||
"-drive", f"format=raw,file=fat:rw:{esp}",
|
"-drive", f"format=raw,file=fat:rw:{boot_volume}",
|
||||||
"-net", "none",
|
"-net", "none",
|
||||||
"-vga", "none", "-device", "VGA,edid=on,xres=1280,yres=720",
|
"-vga", "none", "-device", "VGA,edid=on,xres=1280,yres=720",
|
||||||
"-display", "none",
|
"-display", "none",
|
||||||
@@ -84,7 +82,10 @@ ARCHES = {
|
|||||||
# `expect`: a regex that must appear in serial output => pass.
|
# `expect`: a regex that must appear in serial output => pass.
|
||||||
# `fail`: optional regex whose appearance => immediate fail.
|
# `fail`: optional regex whose appearance => immediate fail.
|
||||||
CASES = [
|
CASES = [
|
||||||
|
# smoke also proves the QMP channel: the harmless query must be delivered
|
||||||
|
# (handshake + command) before the case may pass — see run_case.
|
||||||
{"name": "smoke",
|
{"name": "smoke",
|
||||||
|
"qmp_after": {"delay": 2, "command": "query-status"},
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
{"name": "discovery",
|
{"name": "discovery",
|
||||||
@@ -168,7 +169,7 @@ CASES = [
|
|||||||
# Stress the big kernel lock across cores; heavier, so a longer timeout.
|
# Stress the big kernel lock across cores; heavier, so a longer timeout.
|
||||||
{"name": "smp-stress",
|
{"name": "smp-stress",
|
||||||
"smp": 4,
|
"smp": 4,
|
||||||
"timeout": 90,
|
"timeout": 150,
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# Retry: a forced first-wake failure must still bring every core online.
|
# Retry: a forced first-wake failure must still bring every core online.
|
||||||
@@ -263,7 +264,7 @@ CASES = [
|
|||||||
# death, and the respawned driver re-reports (docs/device-manager.md).
|
# death, and the respawned driver re-reports (docs/device-manager.md).
|
||||||
{"name": "usb-report",
|
{"name": "usb-report",
|
||||||
"smp": 4,
|
"smp": 4,
|
||||||
"timeout": 90,
|
"timeout": 150,
|
||||||
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||||
"-device", "usb-kbd,bus=xhci.0",
|
"-device", "usb-kbd,bus=xhci.0",
|
||||||
"-device", "usb-mouse,bus=xhci.0"],
|
"-device", "usb-mouse,bus=xhci.0"],
|
||||||
@@ -274,16 +275,74 @@ CASES = [
|
|||||||
r"device-manager: restarting usb-xhci-bus[\s\S]*"
|
r"device-manager: restarting usb-xhci-bus[\s\S]*"
|
||||||
r"device-manager: child added",
|
r"device-manager: child added",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M20.1: the ring-3 AML parse (the acpi service maps the blobs and parses
|
||||||
|
# them) finds exactly the Device count the kernel's own parse produced.
|
||||||
|
{"name": "acpi-parse",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 60,
|
||||||
|
"expect": r"acpi-parse: ok",
|
||||||
|
"fail": r"acpi-parse: mismatch|DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M20.3: the flip — ps2-bus now comes up from the acpi service's report, not
|
||||||
|
# a kernel-built node. Ordered: report -> spawn -> the driver attaches its
|
||||||
|
# keyboard, proving discovery runs entirely in ring 3 (docs/m19-m20-plan.md).
|
||||||
|
{"name": "acpi-ps2",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"expect": r"acpi: reported PNP0303[\s\S]*"
|
||||||
|
r"device-manager: spawned ps2-bus[\s\S]*"
|
||||||
|
r"ps2-bus: keyboard driver attached",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M21.1: the SCI + power button. Boot the manager (which spawns the acpi
|
||||||
|
# service); ~4s in, QMP system_powerdown raises the ACPI power-button fixed
|
||||||
|
# event; the service's SCI handler must log the press (docs/m21-plan.md).
|
||||||
|
{"name": "power-button",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 60,
|
||||||
|
"qmp_after": {"delay": 4, "command": "system_powerdown"},
|
||||||
|
"expect": r"power: button pressed",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M21.3 capstone: orderly shutdown. Boot init (the full tree comes up);
|
||||||
|
# ~5s in, QMP system_powerdown raises the power button; the acpi service
|
||||||
|
# publishes it, init stops its children then requests S5, and QEMU exits.
|
||||||
|
# The ordered regex proves button -> shutting-down -> entering-S5; the case
|
||||||
|
# passes on QEMU's self-exit through S5 (docs/m21-plan.md).
|
||||||
|
{"name": "orderly-shutdown",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 90,
|
||||||
|
"qmp_after": {"delay": 5, "command": "system_powerdown"},
|
||||||
|
"expect": r"power: button pressed[\s\S]*"
|
||||||
|
r"init: shutting down[\s\S]*"
|
||||||
|
r"power: entering S5",
|
||||||
|
"fail": r"power: S5 write did not take|DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers +
|
||||||
|
# reports its _HID devices — the two PS/2 nodes must appear with resources
|
||||||
|
# (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/m19-m20-plan.md).
|
||||||
|
{"name": "acpi-report",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||||
|
"-device", "usb-kbd,bus=xhci.0",
|
||||||
|
"-device", "usb-mouse,bus=xhci.0"],
|
||||||
|
"expect": r"acpi: reported PNP0303 \(device \d+, 3 resources\)[\s\S]*"
|
||||||
|
r"acpi: reported PNP0F13 \(device \d+, 1 resources\)",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M19.1: the ring-3 PCI scan (pci-bus walks the ECAM through its mmio_map
|
||||||
|
# grant) finds exactly the functions the kernel's own walk recorded.
|
||||||
|
{"name": "pci-scan",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 60,
|
||||||
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# M18.3: the application surface — device-list enumerates the tree over IPC,
|
# M18.3: the application surface — device-list enumerates the tree over IPC,
|
||||||
# subscribes (endpoint as capability), and observes the removed/added events
|
# subscribes (endpoint as capability), and observes the removed/added events
|
||||||
# the reporter's test-kill produces (docs/device-manager.md).
|
# the reporter's test-kill produces (docs/device-manager.md).
|
||||||
{"name": "device-list",
|
{"name": "device-list",
|
||||||
"smp": 4,
|
"smp": 4,
|
||||||
"timeout": 90,
|
"timeout": 150,
|
||||||
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||||
"-device", "usb-kbd,bus=xhci.0",
|
"-device", "usb-kbd,bus=xhci.0",
|
||||||
"-device", "usb-mouse,bus=xhci.0"],
|
"-device", "usb-mouse,bus=xhci.0"],
|
||||||
"expect": r"device-list: 2 devices[\s\S]*"
|
"expect": r"device-list: \d+ devices[\s\S]*"
|
||||||
r"device-list: subscribed[\s\S]*"
|
r"device-list: subscribed[\s\S]*"
|
||||||
r"device-manager: test mode: killing the reporter[\s\S]*"
|
r"device-manager: test mode: killing the reporter[\s\S]*"
|
||||||
r"device-list: removed \(device[\s\S]*"
|
r"device-list: removed \(device[\s\S]*"
|
||||||
@@ -294,7 +353,7 @@ CASES = [
|
|||||||
# each time), and hits the crash-loop cap (docs/device-manager.md).
|
# each time), and hits the crash-loop cap (docs/device-manager.md).
|
||||||
{"name": "driver-restart",
|
{"name": "driver-restart",
|
||||||
"smp": 4,
|
"smp": 4,
|
||||||
"timeout": 90,
|
"timeout": 150,
|
||||||
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||||
"-device", "usb-kbd,bus=xhci.0",
|
"-device", "usb-kbd,bus=xhci.0",
|
||||||
"-device", "usb-mouse,bus=xhci.0"],
|
"-device", "usb-mouse,bus=xhci.0"],
|
||||||
@@ -305,6 +364,7 @@ CASES = [
|
|||||||
# The initial_ramdisk: the loader ferries a bundle of user binaries; the kernel parses
|
# The initial_ramdisk: the loader ferries a bundle of user binaries; the kernel parses
|
||||||
# it and spawns each as a ring-3 process (here the VFS-server stub heartbeats).
|
# it and spawns each as a ring-3 process (here the VFS-server stub heartbeats).
|
||||||
{"name": "initial-ramdisk",
|
{"name": "initial-ramdisk",
|
||||||
|
"timeout": 60, # the acpi service's boot-time SCI setup can push the marker past 30s under load
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# The user-space VFS: a client opens/writes/reads a file through the rt file
|
# The user-space VFS: a client opens/writes/reads a file through the rt file
|
||||||
@@ -368,24 +428,6 @@ def build(arch, case):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def make_esp(arch):
|
|
||||||
"""Assemble a fresh EFI System Partition from the freshly built binaries."""
|
|
||||||
esp = os.path.join(WORK, "esp")
|
|
||||||
if os.path.exists(esp):
|
|
||||||
shutil.rmtree(esp)
|
|
||||||
efi_dest, efi_src = arch["efi_app"]
|
|
||||||
kern_dest, kern_src = arch["kernel"]
|
|
||||||
fhs = os.path.join(REPO, "zig-out") # zig-out is the FHS image
|
|
||||||
os.makedirs(os.path.join(esp, os.path.dirname(efi_dest)), exist_ok=True)
|
|
||||||
os.makedirs(os.path.join(esp, os.path.dirname(kern_dest)), exist_ok=True)
|
|
||||||
shutil.copy(os.path.join(fhs, efi_src), os.path.join(esp, efi_dest))
|
|
||||||
shutil.copy(os.path.join(fhs, kern_src), os.path.join(esp, kern_dest))
|
|
||||||
for dest, src in arch.get("extra", []):
|
|
||||||
os.makedirs(os.path.join(esp, os.path.dirname(dest)), exist_ok=True)
|
|
||||||
shutil.copy(os.path.join(fhs, src), os.path.join(esp, dest))
|
|
||||||
return esp
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_firmware(arch):
|
def resolve_firmware(arch):
|
||||||
"""Collapse the ovmf_code/ovmf_vars candidate lists to the first path that
|
"""Collapse the ovmf_code/ovmf_vars candidate lists to the first path that
|
||||||
exists on this machine. Mutates `arch` in place; idempotent (a resolved
|
exists on this machine. Mutates `arch` in place; idempotent (a resolved
|
||||||
@@ -404,12 +446,34 @@ def resolve_firmware(arch):
|
|||||||
+ "\nInstall OVMF (edk2-ovmf / ovmf) or add its path above.")
|
+ "\nInstall OVMF (edk2-ovmf / ovmf) or add its path above.")
|
||||||
|
|
||||||
|
|
||||||
|
def qmp_send(path, command):
|
||||||
|
"""One QMP command: connect, capabilities handshake, execute. Raises on any
|
||||||
|
failure — the caller retries until the guest's socket is ready. This is how
|
||||||
|
a case injects a host-side event (system_powerdown = the ACPI power button)
|
||||||
|
into the running guest (docs/m21-plan.md)."""
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(5)
|
||||||
|
try:
|
||||||
|
sock.connect(path)
|
||||||
|
stream = sock.makefile("rw")
|
||||||
|
stream.readline() # the QMP greeting
|
||||||
|
stream.write(json.dumps({"execute": "qmp_capabilities"}) + "\n")
|
||||||
|
stream.flush()
|
||||||
|
stream.readline() # {"return": {}}
|
||||||
|
stream.write(json.dumps({"execute": command}) + "\n")
|
||||||
|
stream.flush()
|
||||||
|
stream.readline()
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
def run_case(arch, case):
|
def run_case(arch, case):
|
||||||
err = build(arch, case["name"])
|
err = build(arch, case["name"])
|
||||||
if err:
|
if err:
|
||||||
return False, "build failed:\n" + err
|
return False, "build failed:\n" + err
|
||||||
|
|
||||||
esp = make_esp(arch)
|
# zig-out is the FHS boot volume; hand it to the guest as-is (see qemu_args).
|
||||||
|
boot_volume = os.path.join(REPO, "zig-out")
|
||||||
vars_fd = os.path.join(WORK, "vars.fd")
|
vars_fd = os.path.join(WORK, "vars.fd")
|
||||||
shutil.copy(arch["ovmf_vars"], vars_fd)
|
shutil.copy(arch["ovmf_vars"], vars_fd)
|
||||||
serial = os.path.join(WORK, "serial.log")
|
serial = os.path.join(WORK, "serial.log")
|
||||||
@@ -419,17 +483,32 @@ def run_case(arch, case):
|
|||||||
expect = re.compile(case["expect"])
|
expect = re.compile(case["expect"])
|
||||||
fail = re.compile(case["fail"]) if case.get("fail") else None
|
fail = re.compile(case["fail"]) if case.get("fail") else None
|
||||||
|
|
||||||
cmd = [arch["qemu"]] + arch["qemu_args"](arch, esp, vars_fd, serial)
|
cmd = [arch["qemu"]] + arch["qemu_args"](arch, boot_volume, vars_fd, serial)
|
||||||
if case.get("smp"): # some cases need more than one core (e.g. parallelism)
|
if case.get("smp"): # some cases need more than one core (e.g. parallelism)
|
||||||
cmd += ["-smp", str(case["smp"])]
|
cmd += ["-smp", str(case["smp"])]
|
||||||
if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case
|
if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case
|
||||||
cmd += case["qemu_extra"]
|
cmd += case["qemu_extra"]
|
||||||
|
# A QMP control socket, always present (additive): how a case's `qmp_after`
|
||||||
|
# hook injects host-side events into the guest mid-run.
|
||||||
|
qmp_path = os.path.join(WORK, "qmp.sock")
|
||||||
|
if os.path.exists(qmp_path):
|
||||||
|
os.remove(qmp_path)
|
||||||
|
cmd += ["-qmp", f"unix:{qmp_path},server,nowait"]
|
||||||
|
qmp_after = case.get("qmp_after") # {"delay": seconds, "command": "..."}
|
||||||
|
qmp_sent = False
|
||||||
|
started = time.monotonic()
|
||||||
qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
try:
|
try:
|
||||||
timeout = case.get("timeout", TIMEOUT)
|
timeout = case.get("timeout", TIMEOUT)
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
|
if qmp_after and not qmp_sent and time.monotonic() - started >= qmp_after["delay"]:
|
||||||
|
try:
|
||||||
|
qmp_send(qmp_path, qmp_after["command"])
|
||||||
|
qmp_sent = True
|
||||||
|
except OSError:
|
||||||
|
pass # socket not up yet; retry next tick
|
||||||
text = ""
|
text = ""
|
||||||
if os.path.exists(serial):
|
if os.path.exists(serial):
|
||||||
with open(serial, "r", errors="replace") as f:
|
with open(serial, "r", errors="replace") as f:
|
||||||
@@ -437,6 +516,8 @@ def run_case(arch, case):
|
|||||||
if fail and fail.search(text):
|
if fail and fail.search(text):
|
||||||
return False, "hit failure marker"
|
return False, "hit failure marker"
|
||||||
if expect.search(text):
|
if expect.search(text):
|
||||||
|
if qmp_after and not qmp_sent:
|
||||||
|
continue # the hook must deliver before the case may pass
|
||||||
return True, "matched " + repr(case["expect"])
|
return True, "matched " + repr(case["expect"])
|
||||||
if qemu.poll() is not None: # QEMU exited on its own
|
if qemu.poll() is not None: # QEMU exited on its own
|
||||||
if expect.search(text):
|
if expect.search(text):
|
||||||
@@ -472,6 +553,12 @@ def main():
|
|||||||
for case in selected:
|
for case in selected:
|
||||||
print(f" {case['name']:<12} ... ", end="", flush=True)
|
print(f" {case['name']:<12} ... ", end="", flush=True)
|
||||||
ok, detail = run_case(arch, case)
|
ok, detail = run_case(arch, case)
|
||||||
|
if not ok:
|
||||||
|
# Keep the evidence: serial.log is otherwise overwritten by the
|
||||||
|
# next case, and an intermittent failure's log is unrecoverable.
|
||||||
|
source = os.path.join(WORK, "serial.log")
|
||||||
|
if os.path.exists(source):
|
||||||
|
shutil.copy(source, os.path.join(WORK, f"{case['name']}-failed-serial.log"))
|
||||||
print(("PASS" if ok else "FAIL") + f" ({detail})")
|
print(("PASS" if ok else "FAIL") + f" ({detail})")
|
||||||
if not ok:
|
if not ok:
|
||||||
failures += 1
|
failures += 1
|
||||||
|
|||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# sort-lines-group-by-start — cluster lines that share their first
|
||||||
|
# whitespace-separated field ($1). Keys appear in first-seen order, and lines
|
||||||
|
# within a key keep their original order. It groups; it does NOT sort.
|
||||||
|
#
|
||||||
|
# Pass the log file as an argument; result is written to stdout:
|
||||||
|
#
|
||||||
|
# tools/sort-lines-group-by-start.sh filename.log
|
||||||
|
#
|
||||||
|
# Useful for a serial/boot log where several sources interleave and each line is
|
||||||
|
# prefixed with its source (the first field): this pulls every source's lines
|
||||||
|
# back together, in the order the sources first appeared, without reordering
|
||||||
|
# within a source.
|
||||||
|
#
|
||||||
|
# input output
|
||||||
|
# pci-bus: scan start pci-bus: scan start
|
||||||
|
# acpi: reported PNP0303 pci-bus: 5 functions
|
||||||
|
# pci-bus: 5 functions acpi: reported PNP0303
|
||||||
|
# acpi: reported PNP0501 acpi: reported PNP0501
|
||||||
|
|
||||||
|
awk '{lines[$1] = lines[$1] ? lines[$1] ORS $0 : $0; if (!seen[$1]++) order[++count] = $1} END {for (i=1; i<=count; i++) print lines[order[i]]}' "$@"
|
||||||
Reference in New Issue
Block a user