The capstone. init becomes a real supervisor: it spawns its boot services supervised against one endpoint that also carries its signals, a re-arming heartbeat timer, and the power events it subscribes to. On the power button (or a terminate signal — same path) it logs the shutdown, runs the M17 stop sequence over its children in reverse spawn order (vfs last), then asks the power service for S5. The acpi service honors a shutdown request from a power subscriber — init is the one subscriber, a soft gate that stands in for 'only the system supervisor may power off' and, unlike a PID-1 check, survives the test harness where the kernel's idle tasks take the early ids. The power service is mechanism (write S5); deciding when to shut down and stopping everything else first is init's policy — the microkernel split applied to poweroff. The orderly-shutdown scenario injects a real QMP power-button event and watches the whole chain compose: button pressed -> init shutting down -> entering S5 -> QEMU powers off. That single scenario proves the M17 lifecycle and the M21 event side compose into a clean shutdown. Suite 60/60.
700 lines
28 KiB
Zig
700 lines
28 KiB
Zig
//! /system/services/acpi — the ACPI discovery service: the x86 firmware
|
|
//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). 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 `runtime.service.run` loop.
|
|
|
|
const std = @import("std");
|
|
const runtime = @import("runtime");
|
|
const aml = @import("aml");
|
|
const acpi_ids = @import("acpi-ids");
|
|
const device = runtime.device;
|
|
const protocol = runtime.device_manager_protocol;
|
|
const power = runtime.power_protocol;
|
|
/// AML opcode/prefix bytes by name (`zero_opcode`, `byte_prefix`, …) — so the `_HID`
|
|
/// integer decode names the opcodes instead of bare 0x0A/0x0B/… (docs/coding-standards.md).
|
|
const opcodes = aml.opcodes;
|
|
|
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
|
var line: [128]u8 = undefined;
|
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
|
}
|
|
|
|
// The claimed acpi-tables node and the resource index of its broad io_port
|
|
// window — the Hal routes every port access through this one claim.
|
|
var node_id: u64 = 0;
|
|
var io_resource_index: u64 = 0;
|
|
// The SCI's irq resource index on the node (the len-1 irq, distinct from the
|
|
// broad [0,256) window), for irqBind / irqAck.
|
|
var sci_resource_index: u64 = 0;
|
|
var has_sci = false;
|
|
|
|
// PM1 event/control and GPE register ports, read from the FADT copy the kernel
|
|
// publishes on the node (M21). Port 0 means absent.
|
|
var pm1a_evt: u16 = 0;
|
|
var pm1b_evt: u16 = 0;
|
|
var pm1_evt_len: u8 = 0;
|
|
var pm1a_cnt: u16 = 0;
|
|
var pm1b_cnt: u16 = 0;
|
|
var gpe0_blk: u16 = 0;
|
|
var gpe0_len: u8 = 0;
|
|
var gpe1_blk: u16 = 0;
|
|
var gpe1_len: u8 = 0;
|
|
var smi_cmd: u16 = 0;
|
|
var acpi_enable_value: u8 = 0;
|
|
var s5_slp_typ_a: u8 = 0;
|
|
var s5_slp_typ_b: u8 = 0;
|
|
var s5_valid = false;
|
|
|
|
// PM1 event-register bits (ACPI): PWRBTN in the status/enable word is bit 8;
|
|
// the control word's SCI_EN is bit 0; SLP_EN is bit 13.
|
|
const pwrbtn_bit: u16 = 1 << 8;
|
|
const sci_en_bit: u32 = 1 << 0;
|
|
const slp_en: u32 = 1 << 13;
|
|
|
|
// The `.power` subscribers: endpoints handed over as capabilities, each
|
|
// receiving events as buffered messages. Dropped on a failed send. The
|
|
// subscriber's task id is kept too — a shutdown request is honored only from a
|
|
// subscriber (init subscribes; a stray process does not), the soft gate that
|
|
// stands in for "only the system supervisor may power off" without hardcoding
|
|
// a pid the kernel's idle tasks would have taken.
|
|
const maximum_subscribers = 8;
|
|
var subscribers: [maximum_subscribers]?runtime.ipc.Handle = .{null} ** maximum_subscribers;
|
|
var subscriber_tasks: [maximum_subscribers]u32 = .{0} ** maximum_subscribers;
|
|
|
|
// Pass-1 registration record (see main): what pass 2 reports.
|
|
const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 };
|
|
var registered: [64]Registered = undefined;
|
|
var registered_count: usize = 0;
|
|
|
|
// A scratch page returned for SystemMemory OperationRegion maps: the service
|
|
// cannot map arbitrary physical memory from ring 3, so such regions are
|
|
// unsupported and degrade to harmless zeros rather than faulting. The M20.2
|
|
// targets (ps2, the legacy devices) use SystemIO and static templates.
|
|
var mmio_scratch: [4096]u8 align(4096) = .{0} ** 4096;
|
|
|
|
fn halMapMmio(physical: u64, len: u64, writable: bool) u64 {
|
|
_ = physical;
|
|
_ = len;
|
|
_ = writable;
|
|
return @intFromPtr(&mmio_scratch);
|
|
}
|
|
|
|
fn halPioRead(width: u8, port: u16) u32 {
|
|
return device.ioRead(node_id, io_resource_index, port, width) orelse 0;
|
|
}
|
|
|
|
fn halPioWrite(width: u8, port: u16, value: u32) void {
|
|
_ = device.ioWrite(node_id, io_resource_index, port, width, value);
|
|
}
|
|
|
|
fn findTablesNode(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor {
|
|
const total = device.enumerate(buffer);
|
|
for (buffer[0..@min(total, buffer.len)]) |d| {
|
|
if (d.class == @intFromEnum(device.DeviceClass.acpi_tables)) return d;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn main(init: runtime.process.Init) void {
|
|
// When the acpi-parse scenario spawns this directly, argv[1] is the kernel's
|
|
// own device count to self-verify against — deterministic, no log-scraping.
|
|
const expected: ?usize = if (init.arguments.get(1)) |a| (std.fmt.parseInt(usize, a, 10) catch null) else null;
|
|
|
|
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
|
_ = runtime.system.write("acpi: out of memory\n");
|
|
return;
|
|
};
|
|
const node = findTablesNode(buffer) orelse {
|
|
_ = runtime.system.write("acpi: no acpi-tables node to claim\n");
|
|
return;
|
|
};
|
|
node_id = node.id;
|
|
if (!device.claim(node_id)) {
|
|
_ = runtime.system.write("acpi: unable to claim acpi-tables\n");
|
|
return;
|
|
}
|
|
|
|
// Map the node's resources: the AML blobs (bytecode), the FADT (intact
|
|
// "FACP" header — decision 3), the io_port grant, and the SCI irq.
|
|
var blocks: [8][]const u8 = undefined;
|
|
var block_count: usize = 0;
|
|
var found_io = false;
|
|
var fadt: ?[]const u8 = null;
|
|
for (node.resources[0..@intCast(node.resource_count)], 0..) |resource, index| {
|
|
if (resource.kind == @intFromEnum(device.ResourceKind.io_port) and !found_io) {
|
|
io_resource_index = index;
|
|
found_io = true;
|
|
continue;
|
|
}
|
|
if (resource.kind == @intFromEnum(device.ResourceKind.irq) and resource.len == 1) {
|
|
sci_resource_index = index;
|
|
has_sci = true;
|
|
continue;
|
|
}
|
|
if (resource.kind != @intFromEnum(device.ResourceKind.memory)) continue;
|
|
const base = device.mmioMap(node_id, index) orelse continue;
|
|
const pointer: [*]const u8 = @ptrFromInt(base);
|
|
const bytes = pointer[0..@intCast(resource.len)];
|
|
if (bytes.len >= 4 and std.mem.eql(u8, bytes[0..4], "FACP")) {
|
|
fadt = bytes;
|
|
continue;
|
|
}
|
|
if (block_count == blocks.len) continue;
|
|
blocks[block_count] = bytes;
|
|
block_count += 1;
|
|
}
|
|
if (block_count == 0) {
|
|
_ = runtime.system.write("acpi: no AML blobs on the node\n");
|
|
return;
|
|
}
|
|
|
|
const result = aml.parse(runtime.allocator(), blocks[0..block_count]) catch {
|
|
_ = runtime.system.write("acpi: AML parse failed\n");
|
|
return;
|
|
};
|
|
var namespace = result.namespace;
|
|
const devices = aml.deviceCount(&namespace);
|
|
writeLine("acpi: parsed {d} AML blob(s), {d} namespace devices\n", .{ block_count, devices });
|
|
if (expected) |want| {
|
|
if (devices == want) {
|
|
_ = runtime.system.write("acpi-parse: ok\n");
|
|
} else {
|
|
writeLine("acpi-parse: mismatch (ring-3 {d} vs kernel {d})\n", .{ devices, want });
|
|
}
|
|
// Self-verify mode is standalone (no manager); stop before reporting.
|
|
while (true) runtime.system.sleep(1000);
|
|
}
|
|
|
|
// Register + report the present _HID devices (M20), then set up the power
|
|
// event side (M21), then serve — all in one harness loop. The interpreter
|
|
// and namespace outlive this frame (static), so the harness callbacks can
|
|
// reach them.
|
|
interpreter_arena = std.heap.ArenaAllocator.init(runtime.allocator());
|
|
persistent_namespace = namespace;
|
|
global_interpreter = aml.Interpreter.init(&persistent_namespace, .{
|
|
.mapMmio = halMapMmio,
|
|
.pioRead = halPioRead,
|
|
.pioWrite = halPioWrite,
|
|
}, interpreter_arena.allocator());
|
|
|
|
readFadt(fadt);
|
|
s5_valid = readSleepS5(&persistent_namespace);
|
|
|
|
runtime.service.run(power.message_maximum, .{
|
|
.service = .power,
|
|
.init = onInit,
|
|
.on_message = onMessage,
|
|
.on_notification = onNotification,
|
|
});
|
|
}
|
|
|
|
// Static so the harness callbacks (which run after main's stack frame is gone)
|
|
// can reach the namespace and interpreter.
|
|
var persistent_namespace: aml.Namespace = undefined;
|
|
var global_interpreter: aml.Interpreter = undefined;
|
|
var interpreter_arena: std.heap.ArenaAllocator = undefined;
|
|
|
|
/// Startup under the harness: register + report the discovered devices to the
|
|
/// manager (M20), then enable ACPI mode and arm the power button (M21).
|
|
fn onInit(endpoint: runtime.ipc.Handle) bool {
|
|
registered_count = 0;
|
|
walkDevices(persistent_namespace.root, &global_interpreter);
|
|
|
|
const manager = runtime.ipc.lookup(.device_manager);
|
|
var i: usize = 0;
|
|
while (i < registered_count) : (i += 1) {
|
|
const entry = registered[i];
|
|
const hid = entry.hid[0..entry.hid_len];
|
|
const desc = acpi_ids.description(hid);
|
|
if (desc.len != 0)
|
|
writeLine("acpi: reported {s} (device {d}, {d} resources) — {s}\n", .{ hid, entry.device_id, entry.resource_count, desc })
|
|
else
|
|
writeLine("acpi: reported {s} (device {d}, {d} resources)\n", .{ hid, entry.device_id, entry.resource_count });
|
|
if (manager) |h| {
|
|
var report = protocol.ChildAdded{ .parent = node_id, .bus_address = entry.device_id, .identity = 0, .device_id = entry.device_id };
|
|
@memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]);
|
|
var reply: [protocol.message_maximum]u8 = undefined;
|
|
_ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {};
|
|
}
|
|
}
|
|
writeLine("acpi: reported {d} device(s) to the manager\n", .{registered_count});
|
|
|
|
armPowerButton(endpoint);
|
|
return true;
|
|
}
|
|
|
|
// --- power event side (M21) ---------------------------------------------------
|
|
|
|
/// Read the PM1 event/control and GPE register ports plus the SMI enable pair
|
|
/// from the FADT copy on the node. Offsets are from the FADT table start (the
|
|
/// SDT header is the first 36 bytes). Prefers the 32-bit port fields; QEMU's
|
|
/// FADT populates them.
|
|
fn readFadt(fadt: ?[]const u8) void {
|
|
const f = fadt orelse {
|
|
_ = runtime.system.write("acpi: no FADT on the node — power events off\n");
|
|
return;
|
|
};
|
|
smi_cmd = @truncate(rd32(f, 48));
|
|
acpi_enable_value = f[52];
|
|
pm1a_evt = @truncate(rd32(f, 56));
|
|
pm1b_evt = @truncate(rd32(f, 60));
|
|
pm1a_cnt = @truncate(rd32(f, 64));
|
|
pm1b_cnt = @truncate(rd32(f, 68));
|
|
gpe0_blk = @truncate(rd32(f, 80));
|
|
gpe1_blk = @truncate(rd32(f, 84));
|
|
pm1_evt_len = if (f.len > 88) f[88] else 4;
|
|
gpe0_len = if (f.len > 92) f[92] else 0;
|
|
gpe1_len = if (f.len > 93) f[93] else 0;
|
|
}
|
|
|
|
fn readSleepS5(ns: *aml.Namespace) bool {
|
|
const st = aml.sleepState(ns, 5) orelse return false;
|
|
s5_slp_typ_a = st.slp_typ_a;
|
|
s5_slp_typ_b = st.slp_typ_b;
|
|
return true;
|
|
}
|
|
|
|
/// Enable ACPI mode if the firmware isn't already in it, then bind the SCI and
|
|
/// set PWRBTN_EN so the power button raises an interrupt we can see.
|
|
fn armPowerButton(endpoint: runtime.ipc.Handle) void {
|
|
if (pm1a_cnt != 0 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0 and smi_cmd != 0) {
|
|
// Switch to ACPI mode: write ACPI_ENABLE to the SMI command port, then
|
|
// spin (bounded) until SCI_EN latches.
|
|
halPioWrite(1, smi_cmd, acpi_enable_value);
|
|
var tries: u32 = 0;
|
|
while (tries < 1000 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0) : (tries += 1) {
|
|
runtime.system.sleep(1);
|
|
}
|
|
}
|
|
if (!has_sci) {
|
|
_ = runtime.system.write("acpi: no SCI resource — power button unavailable\n");
|
|
return;
|
|
}
|
|
if (!device.irqBind(node_id, sci_resource_index, endpoint)) {
|
|
_ = runtime.system.write("acpi: SCI irq_bind failed\n");
|
|
return;
|
|
}
|
|
// PWRBTN_EN lives in the PM1 enable register at evt_blk + evt_len/2.
|
|
if (pm1a_evt != 0) {
|
|
const en_port = pm1a_evt + pm1_evt_len / 2;
|
|
halPioWrite(2, en_port, @as(u16, @truncate(halPioRead(2, en_port))) | pwrbtn_bit);
|
|
}
|
|
if (pm1b_evt != 0) {
|
|
const en_port = pm1b_evt + pm1_evt_len / 2;
|
|
halPioWrite(2, en_port, @as(u16, @truncate(halPioRead(2, en_port))) | pwrbtn_bit);
|
|
}
|
|
_ = runtime.system.write("acpi: power button armed\n");
|
|
}
|
|
|
|
/// The SCI fired. Read PM1 status; a set PWRBTN_STS is the power button — clear
|
|
/// it (write-1), publish, log. Any other set status is cleared and logged
|
|
/// (GPE/Notify dispatch is M21.2). Always re-arm the line.
|
|
fn onSci() void {
|
|
var handled = false;
|
|
inline for (.{ pm1a_evt, pm1b_evt }) |evt_port| {
|
|
if (evt_port != 0) {
|
|
const sts: u16 = @truncate(halPioRead(2, evt_port));
|
|
if (sts & pwrbtn_bit != 0) {
|
|
halPioWrite(2, evt_port, pwrbtn_bit); // write-1-to-clear
|
|
handled = true;
|
|
} else if (sts != 0) {
|
|
halPioWrite(2, evt_port, sts); // clear whatever else latched
|
|
}
|
|
}
|
|
}
|
|
if (handled) {
|
|
_ = runtime.system.write("power: button pressed\n");
|
|
publishButton();
|
|
}
|
|
handleGpe();
|
|
_ = device.irqAck(node_id, sci_resource_index);
|
|
}
|
|
|
|
/// General-purpose events: for each set+enabled GPE bit, evaluate its `\_GPE`
|
|
/// handler method (`_Lxx` level / `_Exx` edge), drain the Notify queue the
|
|
/// method produced, and publish an event per notified device. Then clear the
|
|
/// status bit. QEMU raises no GPEs on this config, so this path is exercised by
|
|
/// host unit tests (docs/m21-plan.md decision 5); on real hardware it carries
|
|
/// battery/AC/lid. The embedded controller's `_Qxx` queries are out of scope.
|
|
fn handleGpe() void {
|
|
handleGpeBlock(gpe0_blk, gpe0_len, 0);
|
|
handleGpeBlock(gpe1_blk, gpe1_len, gpe0_len * 4);
|
|
}
|
|
|
|
fn handleGpeBlock(blk: u16, len: u8, gpe_base: u32) void {
|
|
if (blk == 0 or len == 0) return;
|
|
const status_bytes = len / 2; // status half, then enable half
|
|
var byte_index: u8 = 0;
|
|
while (byte_index < status_bytes) : (byte_index += 1) {
|
|
const sts: u8 = @truncate(halPioRead(1, blk + byte_index));
|
|
const en: u8 = @truncate(halPioRead(1, blk + status_bytes + byte_index));
|
|
const active = sts & en;
|
|
if (active == 0) continue;
|
|
var bit: u3 = 0;
|
|
while (true) : (bit += 1) {
|
|
if (active & (@as(u8, 1) << bit) != 0) {
|
|
dispatchGpe(gpe_base + @as(u32, byte_index) * 8 + bit);
|
|
}
|
|
if (bit == 7) break;
|
|
}
|
|
halPioWrite(1, blk + byte_index, active); // write-1-to-clear the serviced bits
|
|
}
|
|
}
|
|
|
|
/// Evaluate the `\_GPE._L%02X` or `_E%02X` handler for GPE number `n`, then
|
|
/// publish an event for each device it notified.
|
|
fn dispatchGpe(n: u32) void {
|
|
const gpe_scope = aml.Namespace.resolve(&persistent_namespace, persistent_namespace.root, true, 0, &.{seg4("_GPE")}) orelse return;
|
|
var name: [4]u8 = .{ '_', 'L', 0, 0 };
|
|
writeHex2(name[2..4], n);
|
|
var method = aml.Namespace.childOf(gpe_scope, name);
|
|
if (method == null) {
|
|
name[1] = 'E';
|
|
method = aml.Namespace.childOf(gpe_scope, name);
|
|
}
|
|
const m = method orelse return; // no handler — the status bit was already cleared
|
|
_ = global_interpreter.evaluate(m, &.{}) catch return;
|
|
for (global_interpreter.takeNotifications()) |event| publishNotify(event.node, event.code);
|
|
}
|
|
|
|
fn publishNotify(node: *aml.Node, code: u64) void {
|
|
// Map the notified device's _HID to a domain event where we recognize it.
|
|
var hid: [8]u8 = .{0} ** 8;
|
|
if (readHid(node, &global_interpreter)) |h| hid = h;
|
|
const which: power.Event = if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) .battery else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) .ac else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) .lid else .notify;
|
|
var event = power.EventMessage{ .event = @intFromEnum(which), .code = @truncate(code) };
|
|
event.hid = hid;
|
|
writeLine("power: notify {s} code {d}\n", .{ hid[0..7], code });
|
|
publishEvent(std.mem.asBytes(&event));
|
|
}
|
|
|
|
/// Two lowercase hex digits of `n` into `out[0..2]`.
|
|
fn writeHex2(out: []u8, n: u32) void {
|
|
const digits = "0123456789ABCDEF";
|
|
out[0] = digits[(n >> 4) & 0xF];
|
|
out[1] = digits[n & 0xF];
|
|
}
|
|
|
|
fn publishButton() void {
|
|
const event = power.EventMessage{ .event = @intFromEnum(power.Event.power_button) };
|
|
publishEvent(std.mem.asBytes(&event));
|
|
}
|
|
|
|
fn publishEvent(bytes: []const u8) void {
|
|
for (&subscribers) |*slot| {
|
|
if (slot.*) |handle| {
|
|
if (!runtime.ipc.send(handle, bytes)) slot.* = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn isSubscriber(task: u32) bool {
|
|
for (&subscribers, 0..) |*slot, si| {
|
|
if (slot.* != null and subscriber_tasks[si] == task) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
|
|
/// Mirrors the kernel's power.zig sleepValue. Only reached from a PID-1
|
|
/// shutdown request (M21.3).
|
|
fn enterS5() void {
|
|
if (!s5_valid or pm1a_cnt == 0) {
|
|
_ = runtime.system.write("power: S5 unavailable\n");
|
|
return;
|
|
}
|
|
_ = runtime.system.write("power: entering S5\n");
|
|
halPioWrite(2, pm1a_cnt, (@as(u32, s5_slp_typ_a & 0x7) << 10) | slp_en);
|
|
if (pm1b_cnt != 0) halPioWrite(2, pm1b_cnt, (@as(u32, s5_slp_typ_b & 0x7) << 10) | slp_en);
|
|
// If control returns, the write did not take — say so instead of hanging.
|
|
runtime.system.sleep(500);
|
|
_ = runtime.system.write("power: S5 write did not take\n");
|
|
}
|
|
|
|
// --- harness callbacks --------------------------------------------------------
|
|
|
|
fn onNotification(badge: u64) void {
|
|
// The only notification the service binds is the SCI (an IRQ badge).
|
|
_ = badge;
|
|
onSci();
|
|
}
|
|
|
|
/// The `.power` protocol: subscribe (endpoint as the call's capability),
|
|
/// shutdown (PID 1 only). Device discovery uses a different endpoint (the
|
|
/// device manager's), so nothing here handles ChildAdded.
|
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
|
if (message.len < 1) return 0;
|
|
switch (message[0]) {
|
|
@intFromEnum(power.Operation.subscribe) => {
|
|
var status: i32 = -1;
|
|
if (capability) |handle| {
|
|
for (&subscribers, 0..) |*slot, si| {
|
|
if (slot.* == null) {
|
|
slot.* = handle;
|
|
subscriber_tasks[si] = sender;
|
|
status = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const r = power.Reply{ .status = status };
|
|
@memcpy(reply[0..@sizeOf(power.Reply)], std.mem.asBytes(&r));
|
|
return @sizeOf(power.Reply);
|
|
},
|
|
@intFromEnum(power.Operation.shutdown) => {
|
|
// Honored only from a power subscriber — init, which has already run
|
|
// the stop sequence over everything else. The power service is
|
|
// mechanism (write S5); deciding *when* to shut down and stopping
|
|
// the rest of the system first is init's policy.
|
|
const allowed = isSubscriber(sender);
|
|
const r = power.Reply{ .status = if (allowed) 0 else -1 };
|
|
@memcpy(reply[0..@sizeOf(power.Reply)], std.mem.asBytes(&r));
|
|
if (allowed) enterS5();
|
|
return @sizeOf(power.Reply);
|
|
},
|
|
else => return 0,
|
|
}
|
|
}
|
|
|
|
/// Depth-first walk: register + report each present device with a _HID, then
|
|
/// descend. Scopes (\_SB, \_GPE …) are descended without producing a node.
|
|
fn walkDevices(node: *aml.Node, interpreter: *aml.Interpreter) void {
|
|
var child = node.first_child;
|
|
while (child) |c| : (child = c.next_sibling) {
|
|
if (c.kind != .device) {
|
|
walkDevices(c, interpreter);
|
|
continue;
|
|
}
|
|
if (!devicePresent(interpreter, c)) continue; // absent: skip it and its subtree
|
|
|
|
if (readHid(c, interpreter)) |hid| {
|
|
// Skip PCI roots — pci-bus already reports PCI functions; ACPI adds
|
|
// only the non-PCI _HID devices (docs/m19-m20-plan.md M20.2). The two
|
|
// roots are named through the shared registry, not bare _HID strings.
|
|
const id = acpi_ids.HardwareId.fromHid(hid[0..7]);
|
|
if (id != .pci_bus and id != .pci_express_root_bridge) {
|
|
registerDevice(c, hid, interpreter);
|
|
}
|
|
}
|
|
walkDevices(c, interpreter);
|
|
}
|
|
}
|
|
|
|
fn registerDevice(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter) void {
|
|
if (registered_count >= registered.len) return;
|
|
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
|
|
descriptor.class = @intFromEnum(device.DeviceClass.acpi_device);
|
|
descriptor.pci_class = device.no_pci_class;
|
|
const hid_len: u64 = std.mem.indexOfScalar(u8, &hid, 0) orelse hid.len;
|
|
descriptor.hid_len = hid_len;
|
|
@memcpy(descriptor.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]);
|
|
applyCrs(&descriptor, node, interpreter);
|
|
|
|
const id = device.register(node_id, &descriptor) orelse {
|
|
writeLine("acpi: register refused for {s}\n", .{hid[0..@intCast(hid_len)]});
|
|
return;
|
|
};
|
|
registered[registered_count] = .{ .hid = hid, .hid_len = @intCast(hid_len), .device_id = id, .resource_count = descriptor.resource_count };
|
|
registered_count += 1;
|
|
}
|
|
|
|
/// _STA bit 0 (present); absent method or a failed evaluation is treated as
|
|
/// present, per the ACPI rules.
|
|
fn devicePresent(interpreter: *aml.Interpreter, node: *aml.Node) bool {
|
|
const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true;
|
|
const obj = interpreter.evaluate(sta, &.{}) catch return true;
|
|
const status = obj.asInteger() catch return true;
|
|
return (status & 0x01) != 0;
|
|
}
|
|
|
|
/// The device's EISA-decoded _HID (e.g. "PNP0303"), or null.
|
|
fn readHid(node: *aml.Node, interpreter: *aml.Interpreter) ?[8]u8 {
|
|
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return null;
|
|
var buffer: [8]u8 = .{0} ** 8;
|
|
if (hid.kind == .method) {
|
|
const obj = interpreter.evaluate(hid, &.{}) catch return null;
|
|
switch (obj) {
|
|
.integer => |n| {
|
|
_ = eisaIdToStr(@truncate(n), &buffer);
|
|
return buffer;
|
|
},
|
|
else => return null,
|
|
}
|
|
}
|
|
if (hid.kind != .name or hid.value.len == 0) return null;
|
|
const v = hid.value;
|
|
switch (v[0]) {
|
|
// A static _HID names an integer EISA id: Zero/One/Ones or a Byte/Word/DWord/
|
|
// QWord integer prefix. Anything else is not an integer we can EISA-decode.
|
|
opcodes.zero_opcode, opcodes.one_opcode, opcodes.ones_opcode, opcodes.byte_prefix, opcodes.word_prefix, opcodes.dword_prefix, opcodes.qword_prefix => {
|
|
var p: usize = 0;
|
|
const n = readIntObj(v, &p) orelse return null;
|
|
_ = eisaIdToStr(@truncate(n), &buffer);
|
|
return buffer;
|
|
},
|
|
else => return null,
|
|
}
|
|
}
|
|
|
|
// --- _CRS resource-template decode (ported from the kernel's acpi.zig) --------
|
|
|
|
/// A resource template is a byte list of descriptors. Each starts with a tag byte whose
|
|
/// high bit picks the encoding: a *small* descriptor carries its type in bits [6:3] and
|
|
/// its length in bits [2:0]; a *large* descriptor is the whole tag byte, followed by a
|
|
/// 16-bit length. These are the descriptor types danos decodes into resources — named so
|
|
/// the walk below reads by descriptor, not by 0x04/0x85/… (docs/coding-standards.md).
|
|
const large_descriptor_bit: u8 = 0x80; // set in a tag byte => large descriptor
|
|
const small_length_mask: u8 = 0x07; // low 3 bits of a small tag = body length
|
|
const small_type_shift: u3 = 3; // small type sits in bits [6:3]
|
|
|
|
/// Small resource descriptor types (tag bits [6:3]). Non-exhaustive: an unhandled type
|
|
/// is skipped by its length, not misread.
|
|
const SmallResourceType = enum(u8) {
|
|
irq = 0x04,
|
|
io_port = 0x08,
|
|
fixed_io_port = 0x09,
|
|
end_tag = 0x0F,
|
|
_,
|
|
};
|
|
|
|
/// Large resource descriptor types (the whole tag byte). Non-exhaustive for the same reason.
|
|
const LargeResourceType = enum(u8) {
|
|
memory32 = 0x85,
|
|
memory32_fixed = 0x86,
|
|
extended_irq = 0x89,
|
|
_,
|
|
};
|
|
|
|
fn applyCrs(descriptor: *device.DeviceDescriptor, node: *aml.Node, interpreter: *aml.Interpreter) void {
|
|
const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return;
|
|
const obj = interpreter.evaluate(crs, &.{}) catch return;
|
|
const bytes = switch (obj) {
|
|
.buffer => |b| b,
|
|
else => return,
|
|
};
|
|
var i: usize = 0;
|
|
while (i < bytes.len) {
|
|
const tag = bytes[i];
|
|
if (tag & large_descriptor_bit == 0) {
|
|
const len: usize = tag & small_length_mask;
|
|
const body = i + 1;
|
|
if (body + len > bytes.len) break;
|
|
switch (@as(SmallResourceType, @enumFromInt((tag >> small_type_shift) & 0x0F))) {
|
|
.irq => if (len >= 2) { // IRQ mask
|
|
const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8);
|
|
var b: usize = 0;
|
|
while (b < 16) : (b += 1) {
|
|
if (mask & (@as(u16, 1) << @intCast(b)) != 0) addResource(descriptor, .irq, b, 1);
|
|
}
|
|
},
|
|
.io_port => if (len >= 7) addResource(descriptor, .io_port, rd16(bytes, body + 1), bytes[body + 6]),
|
|
.fixed_io_port => if (len >= 3) addResource(descriptor, .io_port, rd16(bytes, body), bytes[body + 2]),
|
|
.end_tag => break,
|
|
else => {},
|
|
}
|
|
i = body + len;
|
|
} else {
|
|
if (i + 3 > bytes.len) break;
|
|
const len: usize = @intCast(rd16(bytes, i + 1));
|
|
const body = i + 3;
|
|
if (body + len > bytes.len) break;
|
|
switch (@as(LargeResourceType, @enumFromInt(tag))) {
|
|
.memory32 => if (len >= 17) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 13)),
|
|
.memory32_fixed => if (len >= 9) addResource(descriptor, .memory, rd32(bytes, body + 1), rd32(bytes, body + 5)),
|
|
.extended_irq => if (len >= 2) {
|
|
const count = bytes[body + 1];
|
|
var k: usize = 0;
|
|
while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) {
|
|
addResource(descriptor, .irq, rd32(bytes, body + 2 + k * 4), 1);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
i = body + len;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn addResource(descriptor: *device.DeviceDescriptor, kind: device.ResourceKind, start: u64, len: u64) void {
|
|
if (descriptor.resource_count >= descriptor.resources.len) return;
|
|
descriptor.resources[@intCast(descriptor.resource_count)] = .{ .kind = @intFromEnum(kind), .start = start, .len = len };
|
|
descriptor.resource_count += 1;
|
|
}
|
|
|
|
// --- small helpers ported verbatim from the kernel's acpi.zig ----------------
|
|
|
|
fn seg4(comptime s: *const [4:0]u8) [4]u8 {
|
|
return s[0..4].*;
|
|
}
|
|
|
|
fn hexDigit(n: u8) u8 {
|
|
return if (n < 10) '0' + n else 'A' + (n - 10);
|
|
}
|
|
|
|
fn eisaIdToStr(id: u32, buffer: *[8]u8) []const u8 {
|
|
const b0: u16 = @intCast(id & 0xFF);
|
|
const b1: u16 = @intCast((id >> 8) & 0xFF);
|
|
const b2: u8 = @truncate(id >> 16);
|
|
const b3: u8 = @truncate(id >> 24);
|
|
const mfg = (b0 << 8) | b1;
|
|
buffer[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F));
|
|
buffer[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F));
|
|
buffer[2] = '@' + @as(u8, @intCast(mfg & 0x1F));
|
|
buffer[3] = hexDigit((b2 >> 4) & 0xF);
|
|
buffer[4] = hexDigit(b2 & 0xF);
|
|
buffer[5] = hexDigit((b3 >> 4) & 0xF);
|
|
buffer[6] = hexDigit(b3 & 0xF);
|
|
buffer[7] = 0;
|
|
return buffer[0..7];
|
|
}
|
|
|
|
fn readIntObj(bytes: []const u8, p: *usize) ?u64 {
|
|
if (p.* >= bytes.len) return null;
|
|
const op = bytes[p.*];
|
|
p.* += 1;
|
|
switch (op) {
|
|
opcodes.zero_opcode => return 0,
|
|
opcodes.one_opcode => return 1,
|
|
opcodes.ones_opcode => return 1,
|
|
opcodes.byte_prefix => {
|
|
if (p.* >= bytes.len) return null;
|
|
const v = bytes[p.*];
|
|
p.* += 1;
|
|
return v;
|
|
},
|
|
opcodes.word_prefix => {
|
|
if (p.* + 2 > bytes.len) return null;
|
|
const v = rd16(bytes, p.*);
|
|
p.* += 2;
|
|
return v;
|
|
},
|
|
opcodes.dword_prefix => {
|
|
if (p.* + 4 > bytes.len) return null;
|
|
const v = rd32(bytes, p.*);
|
|
p.* += 4;
|
|
return v;
|
|
},
|
|
else => return null,
|
|
}
|
|
}
|
|
|
|
fn rd16(bytes: []const u8, off: usize) u64 {
|
|
return @as(u64, bytes[off]) | (@as(u64, bytes[off + 1]) << 8);
|
|
}
|
|
|
|
fn rd32(bytes: []const u8, off: usize) u64 {
|
|
return rd16(bytes, off) | (rd16(bytes, off + 2) << 16);
|
|
}
|
|
|
|
pub const panic = runtime.panic;
|
|
comptime {
|
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
|
}
|