The AML interpreter now handles the Notify opcode (0x86, previously unhandled): it resolves the target device, evaluates the code, and records the pair in a bounded per-evaluate queue the caller drains with takeNotifications. A host unit test with hand-encoded AML — a method that issues Notify(DEV_, 0x80) — proves the device and code come back; aml.zig joins the zig build test loop so the interpreter is covered on the host. The acpi service's SCI handler now services general-purpose events too: for each set-and-enabled GPE bit it evaluates the \_GPE._Lxx (level) or _Exx (edge) handler method, drains the Notify queue that produced, and publishes a domain event per notified device — PNP0C0A battery, ACPI0003 AC, PNP0C0D lid, else generic notify — then clears the status bit and acks. The embedded controller's _Qxx queries are out of scope (hardware track). QEMU raises no GPEs on this config, so the QEMU suite is the regression net (the power button still works with GPE servicing in the path); correctness is the unit test. Suite 59/59.
261 lines
11 KiB
Zig
261 lines
11 KiB
Zig
//! AML (ACPI Machine Language) — the bytecode in the DSDT and SSDTs that describes
|
|
//! the parts of the machine the static tables don't.
|
|
//!
|
|
//! This module has two stages. `parser.zig` walks the entire byte stream and
|
|
//! records every named object into a namespace tree (`namespace.zig`), capturing
|
|
//! method bodies and field/region layout. `interpreter.zig` then *evaluates* control
|
|
//! methods on demand — running operators, control flow, and OperationRegion field
|
|
//! access — so callers can resolve device status (`_STA`), current resource
|
|
//! settings (`_CRS`), sleep states (`_Sx`), and the like against the live namespace.
|
|
|
|
const std = @import("std");
|
|
const opcode = @import("opcodes.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 Node = @import("namespace.zig").Node;
|
|
pub const NodeKind = @import("namespace.zig").NodeKind;
|
|
|
|
/// The AML evaluator: interprets control methods (and reads Names/Fields) far
|
|
/// enough for device discovery. See `interpreter.zig`.
|
|
pub const Interpreter = @import("interpreter.zig").Interpreter;
|
|
pub const Object = @import("interpreter.zig").Object;
|
|
pub const EvaluateHal = @import("interpreter.zig").Hal;
|
|
|
|
/// The SLP_TYP values written to PM1a/PM1b control to enter a sleep state.
|
|
pub const SleepType = struct {
|
|
slp_typ_a: u8,
|
|
slp_typ_b: u8,
|
|
};
|
|
|
|
pub const ParseResult = struct {
|
|
namespace: Namespace,
|
|
/// Bytes the parser consumed across all blocks...
|
|
consumed: usize,
|
|
/// ...out of this many. A clean full traversal has `consumed == total`.
|
|
total: usize,
|
|
};
|
|
|
|
/// Parse the given AML blocks (DSDT first, then SSDTs) into one namespace. Later
|
|
/// blocks extend the namespace built by earlier ones, exactly as ACPI intends.
|
|
pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseResult {
|
|
var namespace = try Namespace.init(allocator);
|
|
var consumed: usize = 0;
|
|
var total: usize = 0;
|
|
for (blocks) |block| {
|
|
var p = parser.Parser.init(block, &namespace);
|
|
consumed += p.parseAll();
|
|
total += block.len;
|
|
}
|
|
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
|
|
/// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
|
|
pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType {
|
|
const segment = [4]u8{ '_', 'S', '0' + state, '_' };
|
|
const node = namespace.resolve(namespace.root, false, 0, &.{segment}) orelse return null;
|
|
if (node.kind != .name) return null;
|
|
return parseSleepPackage(node.value);
|
|
}
|
|
|
|
/// Decode a `Package(){ SLP_TYPa, SLP_TYPb, ... }` from the raw AML of a Name's
|
|
/// value. Returns the first two elements as bytes (missing elements default to 0).
|
|
fn parseSleepPackage(value: []const u8) ?SleepType {
|
|
if (value.len == 0 or value[0] != opcode.package_opcode) return null;
|
|
var p: usize = 1;
|
|
p += packageLengthSize(value, p) orelse return null;
|
|
if (p >= value.len) return null;
|
|
const number_elements = value[p];
|
|
p += 1;
|
|
|
|
const a: u8 = if (number_elements >= 1) @truncate(readInteger(value, &p) orelse 0) else 0;
|
|
const b: u8 = if (number_elements >= 2) @truncate(readInteger(value, &p) orelse 0) else 0;
|
|
return .{ .slp_typ_a = a, .slp_typ_b = b };
|
|
}
|
|
|
|
/// Bytes a PkgLength field occupies at `p` (we only need to step over it here).
|
|
fn packageLengthSize(bytes: []const u8, p: usize) ?usize {
|
|
if (p >= bytes.len) return null;
|
|
const follow: usize = bytes[p] >> 6;
|
|
if (p + 1 + follow > bytes.len) return null;
|
|
return 1 + follow;
|
|
}
|
|
|
|
/// Read one AML integer data object at `p`, advancing `p`.
|
|
fn readInteger(bytes: []const u8, p: *usize) ?u64 {
|
|
if (p.* >= bytes.len) return null;
|
|
const opcode_byte = bytes[p.*];
|
|
p.* += 1;
|
|
return switch (opcode_byte) {
|
|
opcode.zero_opcode => 0,
|
|
opcode.one_opcode => 1,
|
|
opcode.ones_opcode => 0xFF,
|
|
opcode.byte_prefix => readLittle(bytes, p, 1),
|
|
opcode.word_prefix => readLittle(bytes, p, 2),
|
|
opcode.dword_prefix => readLittle(bytes, p, 4),
|
|
opcode.qword_prefix => readLittle(bytes, p, 8),
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn readLittle(bytes: []const u8, p: *usize, n: usize) ?u64 {
|
|
if (p.* + n > bytes.len) return null;
|
|
var v: u64 = 0;
|
|
var k: usize = 0;
|
|
while (k < n) : (k += 1) v |= @as(u64, bytes[p.* + k]) << @intCast(k * 8);
|
|
p.* += n;
|
|
return v;
|
|
}
|
|
|
|
// --- tests ------------------------------------------------------------------
|
|
|
|
test "parses a nested namespace and finds the sleep package" {
|
|
// A hand-assembled AML blob (all PkgLengths computed to be single-byte):
|
|
// Name(_S5, Package(2){0x05, 0x00})
|
|
// Scope(\_SB) { Device(PCI0) {
|
|
// Name(_HID, 0x11)
|
|
// Method(MTHD, 1) {}
|
|
// Method(CALL, 0) { MTHD(Zero) } // invocation of a 1-arg method
|
|
// } }
|
|
// OperationRegion(DBG0, SystemIO, 0x0402, 1)
|
|
// Field(DBG0, ...) { DBGB, 8 }
|
|
const blob = [_]u8{
|
|
// Name(_S5, Package(2){Byte 0x05, Byte 0x00})
|
|
0x08, 0x5F, 0x53, 0x35, 0x5F, 0x12, 0x06, 0x02, 0x0A, 0x05, 0x0A, 0x00,
|
|
// Scope(\_SB) packagelen=0x27
|
|
0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F,
|
|
// Device(PCI0) packagelen=0x1F
|
|
0x5B, 0x82, 0x1F, 0x50, 0x43,
|
|
0x49, 0x30,
|
|
// Name(_HID, 0x11)
|
|
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11,
|
|
// Method(MTHD, flags=1) empty, packagelen=0x06
|
|
0x14, 0x06, 0x4D,
|
|
0x54, 0x48, 0x44, 0x01,
|
|
// Method(CALL, flags=0) { MTHD(Zero) }, packagelen=0x0B
|
|
0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D,
|
|
0x54, 0x48, 0x44, 0x00,
|
|
// OperationRegion(DBG0, SystemIO, Word 0x0402, Byte 1)
|
|
0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B,
|
|
0x02, 0x04, 0x0A, 0x01,
|
|
// Field(DBG0, flags=1) { DBGB, 8 }, packagelen=0x0B
|
|
0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01,
|
|
0x44, 0x42, 0x47, 0x42, 0x08,
|
|
};
|
|
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var result = try parse(arena.allocator(), &.{&blob});
|
|
|
|
// Integrity: the parser consumed exactly the whole blob (no desync).
|
|
try std.testing.expectEqual(blob.len, result.consumed);
|
|
try std.testing.expectEqual(blob.len, result.total);
|
|
|
|
const namespace = &result.namespace;
|
|
|
|
// Expected top-level nodes.
|
|
const sb = namespace.resolve(namespace.root, false, 0, &.{.{ '_', 'S', 'B', '_' }}) orelse return error.NoSB;
|
|
try std.testing.expectEqual(NodeKind.scope, sb.kind);
|
|
const pci0 = namespace.resolve(sb, false, 0, &.{.{ 'P', 'C', 'I', '0' }}) orelse return error.NoPCI0;
|
|
try std.testing.expectEqual(NodeKind.device, pci0.kind);
|
|
_ = namespace.resolve(pci0, false, 0, &.{.{ '_', 'H', 'I', 'D' }}) orelse return error.NoHID;
|
|
|
|
// The 1-arg method's arg count was parsed from its flags byte.
|
|
const mthd = namespace.resolve(pci0, false, 0, &.{.{ 'M', 'T', 'H', 'D' }}) orelse return error.NoMTHD;
|
|
try std.testing.expectEqual(NodeKind.method, mthd.kind);
|
|
try std.testing.expectEqual(@as(u8, 1), mthd.arg_count);
|
|
|
|
// OperationRegion and the Field unit made it into the namespace.
|
|
_ = namespace.resolve(namespace.root, false, 0, &.{.{ 'D', 'B', 'G', '0' }}) orelse return error.NoRegion;
|
|
_ = namespace.resolve(namespace.root, false, 0, &.{.{ 'D', 'B', 'G', 'B' }}) orelse return error.NoField;
|
|
|
|
// The sleep package decoded.
|
|
const s5 = sleepState(namespace, 5) orelse return error.NoS5;
|
|
try std.testing.expectEqual(@as(u8, 5), s5.slp_typ_a);
|
|
try std.testing.expectEqual(@as(u8, 0), s5.slp_typ_b);
|
|
}
|
|
|
|
fn noMap(physical: u64, _: u64, _: bool) u64 {
|
|
return physical;
|
|
}
|
|
fn noRead(_: u8, _: u16) u32 {
|
|
return 0;
|
|
}
|
|
fn noWrite(_: u8, _: u16, _: u32) void {}
|
|
|
|
test "interpreter runs a method with args, arithmetic, and control flow" {
|
|
// Method(TST_, 1) {
|
|
// Store(Arg0, Local0); Add(Local0, 5, Local0)
|
|
// If (LGreater(Local0, 10)) { Return(One) }
|
|
// Return(Zero)
|
|
// }
|
|
const blob = [_]u8{
|
|
0x14, 0x18, 0x54, 0x53, 0x54, 0x5F, 0x01, // Method TST_, 1 arg
|
|
0x70, 0x68, 0x60, // Store(Arg0, Local0)
|
|
0x72, 0x60, 0x0A, 0x05, 0x60, // Add(Local0, 5, Local0)
|
|
0xA0, 0x07, 0x94, 0x60, 0x0A, 0x0A, 0xA4, 0x01, // If(LGreater(Local0,10)) { Return(One) }
|
|
0xA4, 0x00, // Return(Zero)
|
|
};
|
|
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var result = try parse(arena.allocator(), &.{&blob});
|
|
const namespace = &result.namespace;
|
|
const tst = namespace.resolve(namespace.root, false, 0, &.{.{ 'T', 'S', 'T', '_' }}) orelse return error.NoMethod;
|
|
|
|
var interpreter = Interpreter.init(namespace, .{ .mapMmio = noMap, .pioRead = noRead, .pioWrite = noWrite }, arena.allocator());
|
|
|
|
const hi = try interpreter.evaluate(tst, &.{.{ .integer = 7 }}); // 7+5=12 > 10 -> 1
|
|
try std.testing.expectEqual(@as(u64, 1), try hi.asInteger());
|
|
const lo = try interpreter.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0
|
|
try std.testing.expectEqual(@as(u64, 0), try lo.asInteger());
|
|
}
|
|
|
|
test "interpreter records Notify(device, code)" {
|
|
// Device(DEV_) { Name(_HID, 0x030AD041) } // PNP0A03-ish placeholder
|
|
// Method(TST_, 0) { Notify(DEV_, 0x80); Return(Zero) }
|
|
// Encoded: a Device holding a Name, then a Method issuing Notify on it.
|
|
const blob = [_]u8{
|
|
0x5B, 0x82, 0x0F, 0x44, 0x45, 0x56, 0x5F, // Device(DEV_) len=0x0F (pkglen + DEV_ + Name)
|
|
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0C, 0x41, 0xD0, 0x0A, 0x03, // Name(_HID, DWord 0x030AD041)
|
|
0x14, 0x0F, 0x54, 0x53, 0x54, 0x5F, 0x00, // Method(TST_, 0) len=0x0F (pkglen + TST_ + flags + body)
|
|
0x86, 0x44, 0x45, 0x56, 0x5F, 0x0A, 0x80, // Notify(DEV_, 0x80)
|
|
0xA4, 0x00, // Return(Zero)
|
|
};
|
|
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
var result = try parse(arena.allocator(), &.{&blob});
|
|
const namespace = &result.namespace;
|
|
const tst = namespace.resolve(namespace.root, false, 0, &.{.{ 'T', 'S', 'T', '_' }}) orelse return error.NoMethod;
|
|
const dev = namespace.resolve(namespace.root, false, 0, &.{.{ 'D', 'E', 'V', '_' }}) orelse return error.NoDevice;
|
|
|
|
var interpreter = Interpreter.init(namespace, .{ .mapMmio = noMap, .pioRead = noRead, .pioWrite = noWrite }, arena.allocator());
|
|
_ = try interpreter.evaluate(tst, &.{});
|
|
|
|
const events = interpreter.takeNotifications();
|
|
try std.testing.expectEqual(@as(usize, 1), events.len);
|
|
try std.testing.expectEqual(dev, events[0].node);
|
|
try std.testing.expectEqual(@as(u64, 0x80), events[0].code);
|
|
}
|