Files
Daniel Samson b7d97ebb5d acpi: discovery is handed its node like every other driver
The last claimant. The kernel seeds the acpi-tables node, so it sits in the
same boot snapshot the manager already scans to find the PCI host bridge —
there was never a bootstrap problem, only a lookup nobody had written. The
manager claims it and names it in the spawn; the service stops claiming.

Every driver in the system now receives its hardware rather than taking it.

Two failures on the way, both mine. addDriver puts the device id in argv[1],
and the acpi service read argv[1] as a self-verify device-count floor — so
handed device 7 it decided it was in test mode, printed "acpi-parse: ok",
and never reported a device. The test argument is now floor:N, which a bare
id cannot be mistaken for.

And acpi-parse spawns the service directly rather than through the manager,
so nothing handed it the node. That test now claims and transfers it exactly
as the manager does, which is the right shape: the test plays the manager's
role instead of the service reaching for hardware.

device_claim now has two callers left: the manager, which is the acquirer
and should have it, and the display service's GOP path. That is recorded as
question 10 — the framebuffer is not a device, so the answer is likely that
it leaves the device table rather than being exempted from its rules.

Suite 118/118.
2026-08-08 21:54:45 +01:00

725 lines
31 KiB
Zig

//! /system/services/acpi — the ACPI discovery service: the x86 firmware
//! interpreter, moved out of ring 0 (docs/discovery.md). Claims the
//! `acpi-tables` node the kernel publishes (the AML blobs, the broad io_port
//! grant, a broad irq window, the SCI), and runs the **shared AML module** in
//! ring 3 — the same parser and interpreter the kernel uses.
//!
//! It also owns the **event side** (M21): it registers the domain-named `.power`
//! service, binds the SCI (System Control Interrupt), and on a power-button
//! fixed event publishes `power_button` to subscribers — and on init's request
//! writes S5 to power the machine off. The device discovery (M20) and the event
//! handling both run in one `service.run` loop.
const std = @import("std");
const device = @import("driver");
const channel = @import("channel");
const ipc = @import("ipc");
const process = @import("process");
const service = @import("service");
const time = @import("time");
const memory = @import("memory");
const logging = @import("logging");
const aml = @import("aml");
const acpi_ids = @import("acpi-ids");
const device_manager_protocol = @import("device-manager-protocol");
const envelope = @import("envelope");
const power_protocol = @import("power-protocol");
/// AML opcode/prefix bytes by name (`zero_opcode`, `byte_prefix`, …) — so the `_HID`
/// integer decode names the opcodes instead of bare 0x0A/0x0B/… (docs/coding-standards.md).
const opcodes = aml.opcodes;
// The claimed acpi-tables node and the resource index of its broad io_port
// window — the Hal routes every port access through this one claim.
var node_id: u64 = 0;
var io_resource_index: u64 = 0;
// The SCI's irq resource index on the node (the len-1 irq, distinct from the
// broad [0,256) window), for irqBind / irqAck.
var sci_resource_index: u64 = 0;
var has_sci = false;
// PM1 event/control and GPE register ports, read from the FADT copy the kernel
// publishes on the node (M21). Port 0 means absent.
var pm1a_evt: u16 = 0;
var pm1b_evt: u16 = 0;
var pm1_evt_len: u8 = 0;
var pm1a_cnt: u16 = 0;
var pm1b_cnt: u16 = 0;
var gpe0_blk: u16 = 0;
var gpe0_len: u8 = 0;
var gpe1_blk: u16 = 0;
var gpe1_len: u8 = 0;
var smi_cmd: u16 = 0;
var acpi_enable_value: u8 = 0;
var s5_slp_typ_a: u8 = 0;
var s5_slp_typ_b: u8 = 0;
var s5_valid = false;
// PM1 event-register bits (ACPI): PWRBTN in the status/enable word is bit 8;
// the control word's SCI_EN is bit 0; SLP_EN is bit 13.
const pwrbtn_bit: u16 = 1 << 8;
const sci_en_bit: u32 = 1 << 0;
const slp_en: u32 = 1 << 13;
/// The `.power` subscribers, kept by the service harness (P4c): endpoints handed
/// over as capabilities, each receiving events as buffered messages, each swept
/// when its task dies. The table remembers which task subscribed, which is what
/// the shutdown gate below asks — a shutdown request is honored only from a
/// subscriber (init subscribes; a stray process does not), the soft gate that
/// stands in for "only the system supervisor may power off" without hardcoding a
/// pid the kernel's idle tasks would have taken.
const Subscriptions = service.Subscribers(power_protocol.Protocol, void);
// 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: process.Init) void {
// When the acpi-parse scenario spawns this directly, it passes `floor:N` — a
// device-count floor to self-verify against. The kernel no longer parses AML, so
// there is no exact count to match; proving the ring-3 parse found at least N
// Device objects is the check. Deterministic, no log-scraping.
//
// The `floor:` prefix matters. argv[1] is the assigned device id for every driver
// the manager spawns, so a bare number here would be read as a floor — which is
// exactly what happened when discovery started being given its node: it saw
// argv[1] = "7", decided it was in self-verify mode, and never reported a device.
const floor: ?usize = if (init.arguments.get(1)) |a| blk: {
if (!std.mem.startsWith(u8, a, "floor:")) break :blk null;
break :blk std.fmt.parseInt(usize, a["floor:".len..], 10) catch null;
} else null;
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
_ = logging.write("/system/services/acpi: out of memory\n");
return;
};
const node = findTablesNode(buffer) orelse {
_ = logging.write("/system/services/acpi: no acpi-tables node to claim\n");
return;
};
// The node arrived with the spawn: the manager holds it and names it in the call
// that creates this process. Discovery was the last thing in the system that
// acquired hardware by naming it rather than being given it
// (docs/os-development/device-authority.md).
node_id = node.id;
// Map the node's resources: the AML blobs (bytecode), the FADT (intact
// "FACP" header — decision 3), the io_port grant, and the SCI irq.
var blocks: [8][]const u8 = undefined;
var block_count: usize = 0;
var found_io = false;
var fadt: ?[]const u8 = null;
for (node.resources[0..@intCast(node.resource_count)], 0..) |resource, index| {
if (resource.kind == @intFromEnum(device.ResourceKind.io_port) and !found_io) {
io_resource_index = index;
found_io = true;
continue;
}
if (resource.kind == @intFromEnum(device.ResourceKind.irq) and resource.len == 1) {
sci_resource_index = index;
has_sci = true;
continue;
}
if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue;
const base = device.mmioMap(node_id, index) orelse continue;
const pointer: [*]const u8 = @ptrFromInt(base);
const bytes = pointer[0..@intCast(resource.len)];
if (bytes.len >= 4 and std.mem.eql(u8, bytes[0..4], "FACP")) {
fadt = bytes;
continue;
}
if (block_count == blocks.len) continue;
blocks[block_count] = bytes;
block_count += 1;
}
if (block_count == 0) {
_ = logging.write("/system/services/acpi: no AML blobs on the node\n");
return;
}
const result = aml.parse(memory.allocator(), blocks[0..block_count]) catch {
_ = logging.write("/system/services/acpi: AML parse failed\n");
return;
};
var namespace = result.namespace;
const devices = aml.deviceCount(&namespace);
std.log.info("parsed {d} AML blob(s), {d} namespace devices", .{ block_count, devices });
if (floor) |minimum| {
if (devices >= minimum) {
_ = logging.write("acpi-parse: ok\n");
} else {
std.log.info("acpi-parse: too few (ring-3 {d} < floor {d})", .{ devices, minimum });
}
// Self-verify mode is standalone (no manager); stop before reporting.
while (true) time.sleepMillis(1000);
}
// Register + report the present _HID devices (M20), then set up the power
// event side (M21), then serve — all in one harness loop. The interpreter
// and namespace outlive this frame (static), so the harness callbacks can
// reach them.
interpreter_arena = std.heap.ArenaAllocator.init(memory.allocator());
persistent_namespace = namespace;
global_interpreter = aml.Interpreter.init(&persistent_namespace, .{
.mapMmio = halMapMmio,
.pioRead = halPioRead,
.pioWrite = halPioWrite,
}, interpreter_arena.allocator());
readFadt(fadt);
s5_valid = readSleepS5(&persistent_namespace);
// Every name this service needs, resolved before it becomes a provider — see
// `manager_channel`. Best-effort, as it has always been: a standalone
// bring-up with no device manager still serves power.
manager_channel = channel.openEndpoint("device-manager");
service.run(power_protocol.message_maximum, .{
.service = "power",
.init = onInit,
.on_message = onMessage,
.on_notification = onNotification,
.subscribers = Subscriptions.hooks,
});
}
/// The device manager's channel, opened **before** this service binds its own
/// contract — deliberately, and load-bearing.
///
/// init is the registrar, and init is also this service's one subscriber: the
/// moment `power` is bound, init calls us to subscribe. init has a single thread,
/// so while it is blocked in that call it cannot answer anyone — including us. If
/// we opened a name after binding, the two could cross: init blocked calling us,
/// us blocked asking init to resolve a name, neither ever replying. Resolving
/// everything we need first makes that impossible, because after the bind this
/// service only ever talks to the device manager (which never calls init) and
/// then parks in the harness loop, where init's subscribe lands.
var manager_channel: ?ipc.Handle = null;
// Static so the harness callbacks (which run after main's stack frame is gone)
// can reach the namespace and interpreter.
var persistent_namespace: aml.Namespace = undefined;
var global_interpreter: aml.Interpreter = undefined;
var interpreter_arena: std.heap.ArenaAllocator = undefined;
/// Startup under the harness: register + report the discovered devices to the
/// manager (M20), then enable ACPI mode and arm the power button (M21).
fn onInit(endpoint: ipc.Handle) bool {
registered_count = 0;
walkDevices(persistent_namespace.root, &global_interpreter);
const manager = manager_channel;
var i: usize = 0;
while (i < registered_count) : (i += 1) {
const entry = registered[i];
const hid = entry.hid[0..entry.hid_len];
// The devices.csv columns (bus=acpi, hid) then the human-readable name — a
// would-be /system/configuration/devices.csv row read straight off the boot log.
const desc = acpi_ids.description(hid);
if (desc.len != 0)
std.log.info("device {d} bus=acpi hid={s} — {s} ({d} resources)", .{ entry.device_id, hid, desc, entry.resource_count })
else
std.log.info("device {d} bus=acpi hid={s} ({d} resources)", .{ entry.device_id, hid, entry.resource_count });
if (manager) |h| {
// The registered device id is the packet's target, so the body only
// says where on the firmware tree the node sits and what it is.
var report = device_manager_protocol.ChildAdded{
.bus = @intFromEnum(device_manager_protocol.BusKind.acpi),
.parent = node_id,
.bus_address = entry.device_id,
.identity = 0,
};
@memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]);
var packet: [device_manager_protocol.message_maximum]u8 = undefined;
if (device_manager_protocol.Protocol.encodeRequest(.child_added, entry.device_id, report, &.{}, &packet)) |framed| {
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
_ = ipc.call(h, framed, &reply) catch {};
}
}
}
std.log.info("reported {d} device(s) to the manager", .{registered_count});
armPowerButton(endpoint);
return true;
}
// --- power event side (M21) ---------------------------------------------------
/// Read the PM1 event/control and GPE register ports plus the SMI enable pair
/// from the FADT copy on the node. Offsets are from the FADT table start (the
/// SDT header is the first 36 bytes). Prefers the 32-bit port fields; QEMU's
/// FADT populates them.
fn readFadt(fadt: ?[]const u8) void {
const f = fadt orelse {
_ = logging.write("acpi: no FADT on the node — power events off\n");
return;
};
smi_cmd = @truncate(rd32(f, 48));
acpi_enable_value = f[52];
pm1a_evt = @truncate(rd32(f, 56));
pm1b_evt = @truncate(rd32(f, 60));
pm1a_cnt = @truncate(rd32(f, 64));
pm1b_cnt = @truncate(rd32(f, 68));
gpe0_blk = @truncate(rd32(f, 80));
gpe1_blk = @truncate(rd32(f, 84));
pm1_evt_len = if (f.len > 88) f[88] else 4;
gpe0_len = if (f.len > 92) f[92] else 0;
gpe1_len = if (f.len > 93) f[93] else 0;
}
fn readSleepS5(ns: *aml.Namespace) bool {
const st = aml.sleepState(ns, 5) orelse return false;
s5_slp_typ_a = st.slp_typ_a;
s5_slp_typ_b = st.slp_typ_b;
return true;
}
/// Enable ACPI mode if the firmware isn't already in it, then bind the SCI and
/// set PWRBTN_EN so the power button raises an interrupt we can see.
fn armPowerButton(endpoint: ipc.Handle) void {
if (pm1a_cnt != 0 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0 and smi_cmd != 0) {
// Switch to ACPI mode: write ACPI_ENABLE to the SMI command port, then
// spin (bounded) until SCI_EN latches.
halPioWrite(1, smi_cmd, acpi_enable_value);
var tries: u32 = 0;
while (tries < 1000 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0) : (tries += 1) {
time.sleepMillis(1);
}
}
if (!has_sci) {
_ = logging.write("acpi: no SCI resource — power button unavailable\n");
return;
}
if (!device.irqBind(node_id, sci_resource_index, endpoint)) {
_ = logging.write("acpi: SCI irq_bind failed\n");
return;
}
// PWRBTN_EN lives in the PM1 enable register at evt_blk + evt_len/2.
if (pm1a_evt != 0) {
const en_port = pm1a_evt + pm1_evt_len / 2;
halPioWrite(2, en_port, @as(u16, @truncate(halPioRead(2, en_port))) | pwrbtn_bit);
}
if (pm1b_evt != 0) {
const en_port = pm1b_evt + pm1_evt_len / 2;
halPioWrite(2, en_port, @as(u16, @truncate(halPioRead(2, en_port))) | pwrbtn_bit);
}
_ = logging.write("acpi: power button armed\n");
}
/// The SCI fired. Read PM1 status; a set PWRBTN_STS is the power button — clear
/// it (write-1), publish, log. Any other set status is cleared and logged
/// (GPE/Notify dispatch is M21.2). Always re-arm the line.
fn onSci() void {
var handled = false;
inline for (.{ pm1a_evt, pm1b_evt }) |evt_port| {
if (evt_port != 0) {
const sts: u16 = @truncate(halPioRead(2, evt_port));
if (sts & pwrbtn_bit != 0) {
halPioWrite(2, evt_port, pwrbtn_bit); // write-1-to-clear
handled = true;
} else if (sts != 0) {
halPioWrite(2, evt_port, sts); // clear whatever else latched
}
}
}
if (handled) {
_ = logging.write("power: button pressed\n");
publishButton();
}
handleGpe();
_ = device.irqAck(node_id, sci_resource_index);
}
/// General-purpose events: for each set+enabled GPE bit, evaluate its `\_GPE`
/// handler method (`_Lxx` level / `_Exx` edge), drain the Notify queue the
/// method produced, and publish an event per notified device. Then clear the
/// status bit. QEMU raises no GPEs on this config, so this path is exercised by
/// host unit tests (docs/acpi.md — ACPI events); on real hardware it carries
/// battery/AC/lid. The embedded controller's `_Qxx` queries are out of scope.
fn handleGpe() void {
handleGpeBlock(gpe0_blk, gpe0_len, 0);
handleGpeBlock(gpe1_blk, gpe1_len, gpe0_len * 4);
}
fn handleGpeBlock(blk: u16, len: u8, gpe_base: u32) void {
if (blk == 0 or len == 0) return;
const status_bytes = len / 2; // status half, then enable half
var byte_index: u8 = 0;
while (byte_index < status_bytes) : (byte_index += 1) {
const sts: u8 = @truncate(halPioRead(1, blk + byte_index));
const en: u8 = @truncate(halPioRead(1, blk + status_bytes + byte_index));
const active = sts & en;
if (active == 0) continue;
var bit: u3 = 0;
while (true) : (bit += 1) {
if (active & (@as(u8, 1) << bit) != 0) {
dispatchGpe(gpe_base + @as(u32, byte_index) * 8 + bit);
}
if (bit == 7) break;
}
halPioWrite(1, blk + byte_index, active); // write-1-to-clear the serviced bits
}
}
/// Evaluate the `\_GPE._L%02X` or `_E%02X` handler for GPE number `n`, then
/// publish an event for each device it notified.
fn dispatchGpe(n: u32) void {
const gpe_scope = aml.Namespace.resolve(&persistent_namespace, persistent_namespace.root, true, 0, &.{seg4("_GPE")}) orelse return;
var name: [4]u8 = .{ '_', 'L', 0, 0 };
writeHex2(name[2..4], n);
var method = aml.Namespace.childOf(gpe_scope, name);
if (method == null) {
name[1] = 'E';
method = aml.Namespace.childOf(gpe_scope, name);
}
const m = method orelse return; // no handler — the status bit was already cleared
_ = global_interpreter.evaluate(m, &.{}) catch return;
for (global_interpreter.takeNotifications()) |event| publishNotify(event.node, event.code);
}
fn publishNotify(node: *aml.Node, code: u64) void {
// Map the notified device's _HID to a domain event where we recognize it.
// The kind IS the packet's verb, so the mapping picks which event to frame
// rather than which tag to put in a payload.
var hid: [8]u8 = .{0} ** 8;
if (readHid(node, &global_interpreter)) |h| hid = h;
const notice = power_protocol.Notice{ .code = @truncate(code), .hid = hid };
std.log.info("power: notify {s} code {d}", .{ hid[0..7], code });
if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) {
Subscriptions.publish(.battery, 0, notice);
} else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) {
Subscriptions.publish(.ac, 0, notice);
} else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) {
Subscriptions.publish(.lid, 0, notice);
} else {
Subscriptions.publish(.notify, 0, notice);
}
}
/// Two lowercase hex digits of `n` into `out[0..2]`.
fn writeHex2(out: []u8, n: u32) void {
const digits = "0123456789ABCDEF";
out[0] = digits[(n >> 4) & 0xF];
out[1] = digits[n & 0xF];
}
fn publishButton() void {
// The kind is the packet's operation, so the harness frames it once and pushes
// the same bytes to every subscriber. There is no class here: a power event
// goes to everyone who asked for power events.
Subscriptions.publish(.power_button, 0, .{});
}
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
/// The kernel has no S5 path — soft-off is owned here, from the service's own
/// AML parse. Only reached from a PID-1 shutdown request (M21.3).
fn enterS5() void {
if (!s5_valid or pm1a_cnt == 0) {
_ = logging.write("power: S5 unavailable\n");
return;
}
_ = logging.write("power: entering S5\n");
halPioWrite(2, pm1a_cnt, (@as(u32, s5_slp_typ_a & 0x7) << 10) | slp_en);
if (pm1b_cnt != 0) halPioWrite(2, pm1b_cnt, (@as(u32, s5_slp_typ_b & 0x7) << 10) | slp_en);
// If control returns, the write did not take — say so instead of hanging.
time.sleepMillis(500);
_ = logging.write("power: S5 write did not take\n");
}
// --- harness callbacks --------------------------------------------------------
fn onNotification(badge: u64) void {
// Two kinds of notification reach this loop now. The SCI is the one this
// service binds; the published process exits are the harness's, which it has
// already used to sweep the subscriber table before calling here. Everything
// that is not a bare IRQ badge must therefore be ignored — treating a death
// as an interrupt would clear PM1 status the firmware never set.
if (badge & (ipc.notify_exit_bit | ipc.notify_timer_bit | ipc.notify_message_bit | ipc.notify_signal_bit) != 0) return;
onSci();
}
const Invocation = envelope.Invocation;
const Answer = envelope.Answer;
/// The power contract: the reserved `subscribe` (the subscriber's endpoint as
/// the call's capability, answered by the harness) and `shutdown` (subscribers
/// only). Device discovery uses a different endpoint — the device manager's — so
/// nothing here handles a tree report.
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
return Subscriptions.dispatch({}, handlers, message, sender, arrived, reply);
}
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both.
const handlers = Subscriptions.Handlers{ .shutdown = onShutdown };
/// Honored only from a power subscriber — init, which has already run the stop
/// sequence over everything else. The power service is mechanism (write S5);
/// deciding *when* to shut down and stopping the rest of the system first is
/// init's policy. The badge is the whole gate: it is kernel-stamped, so nothing
/// in the packet can claim to be init. The subscriber table moved into the
/// harness; the question it answers has not changed.
fn onShutdown(_: void, invocation: Invocation(void), _: Answer(void)) isize {
if (!Subscriptions.has(invocation.sender)) return -envelope.EPERM;
enterS5();
return 0;
}
/// Depth-first walk: register + report each present device with a _HID, then
/// descend. Scopes (\_SB, \_GPE …) are descended without producing a node.
fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter) void {
var child = node.first_child;
while (child) |c| : (child = c.next_sibling) {
if (c.kind != .device) {
walkDevices(c, interpreter);
continue;
}
if (!devicePresent(interpreter, c)) continue; // absent: skip it and its subtree
if (readHid(c, interpreter)) |hid| {
// Skip PCI roots — pci-bus already reports PCI functions; ACPI adds
// only the non-PCI _HID devices (docs/device-manager.md — matching). 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) catch |e| {
std.log.warn("register refused for {s}: {s}", .{ hid[0..@intCast(hid_len)], @errorName(e) });
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);
}