Files
danos/system/devices/aml/aml.zig
T
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

233 lines
9.9 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());
}