13 Commits
Author SHA1 Message Date
Daniel Samson d5d15cefcb Decode PCI/ACPI device identities and name their class codes as enums
Two related changes to make device identities legible in the boot log and in
the code that matches on them.

Logging: the pci-bus driver decodes each function's class/subclass/prog-IF
triple to human names (via the existing pci-class module), and the acpi
service appends each _HID's human name (via acpi-ids) to its report line. So
"class 0x01 (Mass Storage Controller) subclass 0x06 (Serial ATA Controller)
progif 0x01 (AHCI 1.0)" reads straight off the log when writing a driver.

Naming: a new coding standard ("Named values, not magic numbers") says a value
with meaning gets a name, prefer an enum for value sets. Applied:
- pci-class is refactored from u8-switch tables into a BaseClass enum plus
  per-class SubClass/ProgIf enums with name() methods (the usb-ids shape). The
  public className/subclassName/progIfName(u8...) API is unchanged, so the
  hardware-byte decoders (pci-bus, the kernel dump) are untouched; output is
  byte-identical.
- the device-manager builds the xHCI class triple from named parts instead of
  a bare 0x0C0330.
- the acpi service's _CRS walk names its resource-descriptor tags as
  SmallResourceType/LargeResourceType enums, and the _HID integer decode uses
  the AML module's existing *_opcode constants (now re-exported from aml.zig)
  rather than bare 0x0A/0xFF/... literals.
2026-07-13 05:05:25 +01:00
Daniel Samson fd96a35eb9 Decode the xHCI port speed in the usb-xhci-bus log
The root-hub scan logged the raw PORTSC port-speed class ("speed class 3").
Decode it to a human name — Low/Full/High/SuperSpeed/SuperSpeedPlus with the
USB generation and line rate — so the boot log says what enumerated on each
port, the USB analog of the pci-bus class line. This is the link speed only;
the device class/subclass/protocol needs descriptor reads (the USB track).
2026-07-13 05:05:13 +01:00
Daniel Samson e3fe3f3f45 Boot zig-out directly in the qemu test harness
The FHS-shaped zig-out IS the boot volume (docs/efi.md), and `zig build
run-x86-64` already presents it to the guest with fat:rw:zig-out. The test
harness instead assembled a separate ESP by copying the boot-critical files
out of zig-out into zig-out/qemu-test/esp — but every (dest, src) pair was
identical, so the copy was pure redundancy.

Drop make_esp and point QEMU straight at zig-out, matching run-x86-64 and the
docs. Removes the now-dead efi_app/kernel/extra arch-config entries.
2026-07-13 05:05:08 +01:00
Daniel Samson 60da667b42 Merge claude/vigilant-swanson-073c72: retire dead kernel AML device-building path (M20.3 cleanup) 2026-07-13 03:54:06 +01:00
Daniel Samson 36145e623b Delete the retired kernel AML device-building path (M20.3 cleanup)
The M20.3 flip moved ACPI namespace enumeration to the ring-3 acpi
service; the kernel now builds the namespace only for the \_S5 sleep
type. That left the kernel's AML-to-device helpers unreferenced.

Remove the dead cluster (wireAcpiDevices, mirrorDevices, applyHid,
setEisaHid, applyCrs, parseResourceTemplate, parseAddressSpace,
devicePresent, matchHostBridge, findPciNode, readAdr, isPciRootNode,
isPciRootHid, PciContext) and every AML-decoding helper it alone used
(eisaIdToStr, seg4, cstr, hexDigit, rd16, rd32, readN, readLE,
readIntObj, packageLength/PkgLen) plus their tests and the now-orphaned
acpi-ids import. The static-table path keeps checksumOk, fadt, readGas,
readCntRegister, and rd. Also tidies two stale comments.
2026-07-13 03:53:04 +01:00
Daniel Samson 565415327d Mark the M19-M20 discovery migration complete 2026-07-13 03:33:00 +01:00
Daniel Samson bf6bdb389d Merge feat/acpi-service: ACPI interpretation in ring 3 (M20)
The AML interpreter as a shared build module, the acpi-tables node, the
acpi service (parse, evaluate _CRS/_STA, register + report), and the flip
that retired the kernel's ACPI device build — discovery's second and final
subsystem to leave ring 0.
2026-07-13 03:32:54 +01:00
Daniel Samson 0628944b15 Docs: close the discovery migration (M20.3)
discovery.md records ACPI enumeration leaving the kernel; device-manager.md
increment 8 marked done — enumeration now runs entirely in ring 3.
2026-07-13 03:32:53 +01:00
Daniel Samson e6d0bb7ef0 The flip: ACPI enumeration leaves the kernel (M20.3)
The kernel no longer folds AML Device objects into the device tree — the
ring-3 acpi service is the sole builder of _HID device nodes. The kernel
keeps building the namespace only for the \_S5 sleep type, and still
seeds the static tables (MADT, HPET, MCFG, FADT) and the acpi-tables node.

The device manager matches ps2-bus from the service's _HID reports
(PNP0303 / PNP0F13, singleton-deduped) instead of boot-snapshot nodes;
its dead boot-snapshot ps2 arm is gone. The service registers every
device before reporting any, so a driver the manager spawns on the first
report already sees the full set — no keyboard-before-mouse race. The
acpi-ps2 scenario proves the whole chain: report -> spawn -> ps2-bus
finds the controller and attaches its keyboard, entirely in ring 3. The
ioport test moved to the acpi-tables I/O window, since the kernel-built
PS/2 node it used to scan for no longer exists. The retired
device-building functions in acpi.zig are dead but retained (a botched
mechanical deletion is worse mid-migration than a follow-up sweep, which
is flagged as a task). Suite 58/58.
2026-07-13 03:32:15 +01:00
Daniel Samson 5ca804d827 The acpi service evaluates _CRS/_STA in ring 3 and reports devices (M20.2)
AML method evaluation now runs in userspace touching real hardware: the
service builds an interpreter with a ring-3 Hal (port I/O routed through
its claimed acpi-tables node; a scratch page backs SystemMemory maps so a
stray OperationRegion degrades to zeros instead of faulting a process
that cannot map arbitrary physical memory). It walks the namespace and,
for each present _HID device that is not a PCI root, evaluates _CRS,
registers it under acpi-tables, and reports it with its EISA-decoded hid.

Containment for this needed the broker's irq check to become range-based
— an interrupt line is still indivisible, but a parent may own a range,
so the acpi-tables node's broad irq window contains its children's legacy
lines (a length-1 range is exactly the old equality, so single-irq
parents are unaffected). ChildAdded gained a hid field for firmware
string identity. Matching those reports to drivers stays off until M20.3,
so ps2-bus still comes up via the kernel path — no regression. The
acpi-report scenario proves the PS/2 keyboard (io 0x60/0x64 + IRQ) and
mouse (IRQ) are reported with their resources. Suite 57/57.
2026-07-13 03:19:39 +01:00
Daniel Samson a299363b59 The AML interpreter runs in ring 3: the acpi service parses (M20.1)
The AML module becomes a build module compiled into both the kernel (for
the \_S5 sleep state it still needs) and the new acpi service — one
source, two builds, no fork. The kernel publishes a single acpi-tables
node: the DSDT/SSDT blobs as memory resources, a broad io_port grant (the
honest trust boundary — firmware AML names whatever ports it chose, known
only after parsing), and the SCI for the M21 event track. The acpi
service claims the node, maps each blob through the ordinary mmio grant
(which preserves the sub-page offset onto the bytecode), and runs the
same parser the kernel does. It self-verifies its namespace Device count
against the kernel's — 34 = 34 — deterministically via an argv the
acpi-parse test passes, so no racing the shared serial buffer. Parse-only
touches no hardware; OperationRegion evaluation waits for _CRS/_STA in
M20.2. The manager spawns 'discovery' (the neutral ramdisk name) at
startup. Suite 56/56.
2026-07-13 03:07:11 +01:00
Daniel Samson d8dd62c639 Mark the feat/pci-bus merge done in the M19-M20 plan 2026-07-13 02:55:03 +01:00
Daniel Samson d106b6e8dc Merge feat/pci-bus: PCI enumeration in ring 3 (M19)
The host bridge apertures, idempotent device_register, the pci-bus driver
(scan, register, report), and the flip that retired the kernel's PCI walk
— discovery's first subsystem to leave ring 0.
2026-07-13 02:55:03 +01:00
19 changed files with 1280 additions and 656 deletions
+13
View File
@@ -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"),
}); });
@@ -340,6 +347,9 @@ pub fn build(b: *std.Build) void {
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"); 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");
@@ -358,7 +368,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");
+26
View File
@@ -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
+4 -2
View File
@@ -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)
+14
View File
@@ -177,3 +177,17 @@ window). The per-function walk moved to the ring-3 `pci-bus` driver
ECAM scan through its mmio grant, and `device_register`s what it finds, which 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 device manager mirrors and matches. The ACPI namespace walk follows in M20;
the static tables (MADT, HPET, MCFG, FADT + `\\_S5`) stay kernel-side. 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.
+27 -19
View File
@@ -105,25 +105,33 @@ branch is green; keep branches; push everything.
read is under the big lock and the arithmetic is guarded, and pci-bus read is under the big lock and the arithmetic is guarded, and pci-bus
skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart skips size-0 BARs. discovery.md updated; suite 55/55 (driver-restart
hammered 6×). hammered 6×).
- [ ] **merge** `feat/pci-bus` → main, push. - [x] **merge** `feat/pci-bus` → main, push (merged 2026-07-13).
- [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at - [x] **M20.1** — acpi service, parse only: the AML interpreter is now a build
system/services/acpi/acpi.zig): kernel publishes `acpi-tables` module compiled into both kernel and service; the kernel publishes the
(decision 5) — memory over the table blobs, the broad io_port grant, and `acpi-tables` node (AML blobs as memory resources, the broad io_port grant,
**the SCI as an irq resource** (from the FADT; unused until M21 but free the SCI); the service claims it, maps the blobs, runs the shared parser in
to record now). The service claims it, maps the blobs, runs the shared ring 3, and self-verifies its Device count against the kernel's (34 = 34,
AML module in ring 3, logs the namespace device count and `_HID`s. deterministic via argv, no log-scraping); the manager spawns `discovery`
Scenario `acpi-parse`: user-space count equals the kernel walk's count. at startup. Parse-only touches no hardware. Suite 56/56.
- [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS`
resources registered under `acpi-tables` (its io_port + the memory-map - [x] **M20.2** — register + report: the service evaluates `_STA`/`_CRS` in
holes give containment), reported to the manager. Spawn-from-reports for ring 3 (interpreter Hal = port I/O over the claimed node; a scratch page
ACPI matches stays off. Scenario: the reported set includes the PS/2 backs SystemMemory maps so a stray region can't fault it) and registers +
keyboard and mouse nodes with their IRQ resources. reports each present `_HID` device under `acpi-tables`. Containment: the
- [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static broker's irq check became range-based (len-1 == the old equality) so the
tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers node's broad irq window covers children's legacy lines; io ports fall in
(ps2-bus) from reports. The `input` and `device-manager` scenarios are the broad io grant. ChildAdded gained `hid`. Matching stays off. The
the assertion. discovery.md + acpi.md + device-manager.md updated; `acpi-report` scenario asserts the PS/2 keyboard (3 resources) and mouse
device-manager.md increment 8 closed. (1 resource) among the reports. Suite 57/57.
- [ ] **merge** `feat/acpi-service` → main, push — **loop ends here**. - [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**.
--- ---
+50 -372
View File
@@ -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,
@@ -402,12 +404,55 @@ pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryR
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 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
@@ -670,6 +715,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
@@ -687,6 +733,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;
@@ -766,342 +813,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.
@@ -1142,42 +853,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.
/// 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.*;
} }
// --- 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);
}
+19
View File
@@ -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 {
+5
View File
@@ -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
View File
@@ -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());
} }
+7
View File
@@ -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;
} }
+18 -1
View File
@@ -14,12 +14,29 @@ const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const protocol = runtime.device_manager_protocol; const protocol = runtime.device_manager_protocol;
const device = runtime.device; const device = runtime.device;
const pci_class = @import("pci-class");
fn writeLine(comptime fmt: []const u8, arguments: anytype) void { fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined; var line: [128]u8 = undefined;
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); _ = 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 bridge_id: u64 = protocol.no_device;
var ecam_base: usize = 0; var ecam_base: usize = 0;
var ecam_physical: u64 = 0; var ecam_physical: u64 = 0;
@@ -137,7 +154,7 @@ fn scan() void {
if (vendor_device & 0xFFFF == 0xFFFF) continue; if (vendor_device & 0xFFFF == 0xFFFF) continue;
const class_revision = configRead(bus, dev, function, 0x08); const class_revision = configRead(bus, dev, function, 0x08);
found += 1; found += 1;
writeLine("pci-bus: {d}:{d}.{d} class 0x{x:0>6}\n", .{ bus, dev, function, class_revision >> 8 }); logFunction(bus, dev, function, class_revision >> 8);
registerAndReport(bus, dev, function, class_revision >> 8); registerAndReport(bus, dev, function, class_revision >> 8);
} }
} }
+17 -1
View File
@@ -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,
+10 -1
View File
@@ -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;
+94 -7
View File
@@ -146,6 +146,12 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
deviceListTest(boot_information); deviceListTest(boot_information);
} else if (eql(case, "pci-scan")) { } else if (eql(case, "pci-scan")) {
pciScanTest(boot_information); 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, "initial-ramdisk")) { } else if (eql(case, "initial-ramdisk")) {
initialRamdiskTest(boot_information); initialRamdiskTest(boot_information);
} else if (eql(case, "vfs")) { } else if (eql(case, "vfs")) {
@@ -1130,12 +1136,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;
@@ -1143,18 +1155,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.
@@ -1912,6 +1924,81 @@ fn pciScanTest(boot_information: *const BootInformation) void {
result(); 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,
+397 -19
View File
@@ -1,27 +1,405 @@
//! /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` //! M20.2 (this increment): after parsing, walk the namespace and, for each
//! node the kernel publishes (table blobs + the broad io_port grant + the SCI), //! present Device with a hardware id (`_HID`), evaluate its current resource
//! map the tables, and run the **shared AML module** in ring 3 behind a `Hal` //! settings (`_CRS`) through a ring-3 `Hal` (port I/O over the claimed node),
//! backed by `mmio_map` and `io_read`/`io_write` — the interpreter cannot tell //! register it under the acpi-tables node (its I/O ports and IRQs contained by
//! it moved. Then the bus-driver shape: `device_register` the namespace //! the node's broad grants), and report it to the device manager with its
//! devices (`_HID`, `_CRS` resources, containment against the node's //! EISA-decoded hid as identity. Matching those reports to drivers (ps2-bus)
//! apertures), report each to the device manager, stay resident under its //! and retiring the kernel's own device build follow in M20.3.
//! 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;
/// 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;
// 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 each memory resource (an AML blob) and note the io_port resource.
var blocks: [8][]const u8 = undefined;
var block_count: usize = 0;
var found_io = false;
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.memory)) continue;
const base = device.mmioMap(node_id, index) orelse continue;
const pointer: [*]const u8 = @ptrFromInt(base);
blocks[block_count] = pointer[0..@intCast(resource.len)];
block_count += 1;
if (block_count == blocks.len) break;
}
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.2).
var arena = std.heap.ArenaAllocator.init(runtime.allocator());
var interpreter = aml.Interpreter.init(&namespace, .{
.mapMmio = halMapMmio,
.pioRead = halPioRead,
.pioWrite = halPioWrite,
}, arena.allocator());
// Pass 1: register every present _HID device under acpi-tables, remembering
// each (hid, device id). Pass 2: report them all. Registering before any
// report reaches the manager means a driver it spawns on the first report
// already sees the whole set (no keyboard-before-mouse race for ps2-bus).
registered_count = 0;
walkDevices(namespace.root, &interpreter);
const manager = runtime.ipc.lookup(.device_manager);
var i: usize = 0;
while (i < registered_count) : (i += 1) {
const entry = registered[i];
// Append the _HID's human-readable name when it is a known standard PnP/ACPI
// id (e.g. PNP0303 -> "PS/2 Keyboard"), so the boot log says what each
// reported device actually is. The description trails the existing fields so
// the acpi-report/acpi-ps2 matchers still see "<hid> (device N, M resources)".
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});
// Stay resident: the claim holds, and the service is here to grow into the
// supervised discoverer (M20.3, then the M21 event side on the SCI).
while (true) runtime.system.sleep(1000);
}
/// 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;
@@ -74,12 +74,16 @@ pub const ChildAdded = extern struct {
/// 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; for PCI: /// Bus-specific identity (for USB: the PORTSC port-speed class; for PCI:
/// the class triple). /// 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 /// 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` /// manager hands a matched driver as its argv assignment — or `no_device`
/// for an unregistered leaf (a USB port before the descriptor track). /// for an unregistered leaf (a USB port before the descriptor track).
device_id: u64 = no_device, 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,21 +35,21 @@ 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) {
.ps2_keyboard, .ps2_mouse => "ps2-bus",
else => null,
};
} }
/// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller: /// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller —
/// Serial Bus Controller (0x0C) / USB Controller (0x03) / XHCI (0x30) — the names /// Serial Bus Controller / USB Controller / XHCI — named from pci-class.zig rather
/// pci-class.zig decodes. /// than written as the bare 0x0C0330 (docs/coding-standards.md, "Named values").
const xhci_pci_class: u64 = 0x0C_03_30; 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 /// 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 /// from the boot snapshot to the bus reports), or null. A machine can carry
@@ -61,6 +62,16 @@ fn pciDriverForIdentity(identity: u64) ?[]const u8 {
}; };
} }
/// The driver that serves a *reported* ACPI device by its `_HID` (M20.3:
/// ps2-bus now binds the PS/2 nodes the acpi service reports, not boot-snapshot
/// nodes the kernel used to build). ps2-bus is a singleton that finds both its
/// 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;
}
/// Whether some driver entry already serves registered device `device_id` — /// Whether some driver entry already serves registered device `device_id` —
/// a re-report after a bus restart must not spawn a second instance. /// a re-report after a bus restart must not spawn a second instance.
fn driverForDevice(device_id: u64) bool { fn driverForDevice(device_id: u64) bool {
@@ -151,7 +162,7 @@ const Child = struct {
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
@@ -347,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,
@@ -409,6 +426,14 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
if (pciDriverForIdentity(report.identity)) |child_driver| { if (pciDriverForIdentity(report.identity)) |child_driver| {
if (!driverForDevice(report.device_id)) addDriver(child_driver, report.device_id, true); 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;
+2 -1
View File
@@ -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
+38 -30
View File
@@ -55,20 +55,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",
@@ -274,6 +270,35 @@ 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"},
# 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 # 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. # grant) finds exactly the functions the kernel's own walk recorded.
{"name": "pci-scan", {"name": "pci-scan",
@@ -375,24 +400,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
@@ -416,7 +423,8 @@ def run_case(arch, case):
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")
@@ -426,7 +434,7 @@ 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