From ea24c3d26c31dd47e7f127357461e079799a20c1 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:17:33 +0100 Subject: [PATCH] Update device/aml/aml - Auto-committed by Claude Code Co-Authored-By: Claude Haiku 4.5 --- src/device/aml/aml.zig | 208 ++++++++++ src/device/aml/interp.zig | 737 +++++++++++++++++++++++++++++++++++ src/device/aml/namespace.zig | 181 +++++++++ src/device/aml/opcodes.zig | 137 +++++++ src/device/aml/parser.zig | 517 ++++++++++++++++++++++++ src/device/device.zig | 204 ++++++++++ src/device/devicetree.zig | 16 + src/device/platform.zig | 67 ++++ src/device/power.zig | 106 +++++ 9 files changed, 2173 insertions(+) create mode 100644 src/device/aml/aml.zig create mode 100644 src/device/aml/interp.zig create mode 100644 src/device/aml/namespace.zig create mode 100644 src/device/aml/opcodes.zig create mode 100644 src/device/aml/parser.zig create mode 100644 src/device/device.zig create mode 100644 src/device/devicetree.zig create mode 100644 src/device/platform.zig create mode 100644 src/device/power.zig diff --git a/src/device/aml/aml.zig b/src/device/aml/aml.zig new file mode 100644 index 0000000..e04380a --- /dev/null +++ b/src/device/aml/aml.zig @@ -0,0 +1,208 @@ +//! 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. `interp.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 op = @import("opcodes.zig"); +const parser = @import("parser.zig"); +const namespace = @import("namespace.zig"); +const interp = @import("interp.zig"); + +pub const Namespace = namespace.Namespace; +pub const Node = namespace.Node; +pub const NodeKind = namespace.NodeKind; + +/// The AML evaluator: interprets control methods (and reads Names/Fields) far +/// enough for device discovery. See `interp.zig`. +pub const Interp = interp.Interp; +pub const Object = interp.Object; +pub const EvalHal = interp.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 ns = try Namespace.init(allocator); + var consumed: usize = 0; + var total: usize = 0; + for (blocks) |block| { + var p = parser.Parser.init(block, &ns); + consumed += p.parseAll(); + total += block.len; + } + return .{ .namespace = ns, .consumed = consumed, .total = total }; +} + +/// 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(ns: *Namespace, state: u8) ?SleepType { + const seg = [4]u8{ '_', 'S', '0' + state, '_' }; + const node = ns.resolve(ns.root, false, 0, &.{seg}) 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] != op.package_op) return null; + var p: usize = 1; + p += pkgLengthSize(value, p) orelse return null; + if (p >= value.len) return null; + const num_elements = value[p]; + p += 1; + + const a: u8 = if (num_elements >= 1) @truncate(readInteger(value, &p) orelse 0) else 0; + const b: u8 = if (num_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 pkgLengthSize(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 = bytes[p.*]; + p.* += 1; + return switch (opcode) { + op.zero_op => 0, + op.one_op => 1, + op.ones_op => 0xFF, + op.byte_prefix => readLittle(bytes, p, 1), + op.word_prefix => readLittle(bytes, p, 2), + op.dword_prefix => readLittle(bytes, p, 4), + op.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) pkglen=0x27 + 0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F, + // Device(PCI0) pkglen=0x1F + 0x5B, 0x82, 0x1F, 0x50, 0x43, 0x49, 0x30, + // Name(_HID, 0x11) + 0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11, + // Method(MTHD, flags=1) empty, pkglen=0x06 + 0x14, 0x06, 0x4D, 0x54, 0x48, 0x44, 0x01, + // Method(CALL, flags=0) { MTHD(Zero) }, pkglen=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 }, pkglen=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 ns = &result.namespace; + + // Expected top-level nodes. + const sb = ns.resolve(ns.root, false, 0, &.{.{ '_', 'S', 'B', '_' }}) orelse return error.NoSB; + try std.testing.expectEqual(NodeKind.scope, sb.kind); + const pci0 = ns.resolve(sb, false, 0, &.{.{ 'P', 'C', 'I', '0' }}) orelse return error.NoPCI0; + try std.testing.expectEqual(NodeKind.device, pci0.kind); + _ = ns.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 = ns.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. + _ = ns.resolve(ns.root, false, 0, &.{.{ 'D', 'B', 'G', '0' }}) orelse return error.NoRegion; + _ = ns.resolve(ns.root, false, 0, &.{.{ 'D', 'B', 'G', 'B' }}) orelse return error.NoField; + + // The sleep package decoded. + const s5 = sleepState(ns, 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(_: u64, _: u64, _: bool) void {} +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 ns = &result.namespace; + const tst = ns.resolve(ns.root, false, 0, &.{.{ 'T', 'S', 'T', '_' }}) orelse return error.NoMethod; + + var ev = Interp.init(ns, .{ .mapMmio = noMap, .pioRead = noRead, .pioWrite = noWrite }, arena.allocator()); + + const hi = try ev.evaluate(tst, &.{.{ .integer = 7 }}); // 7+5=12 > 10 -> 1 + try std.testing.expectEqual(@as(u64, 1), try hi.asInt()); + const lo = try ev.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0 + try std.testing.expectEqual(@as(u64, 0), try lo.asInt()); +} diff --git a/src/device/aml/interp.zig b/src/device/aml/interp.zig new file mode 100644 index 0000000..3647198 --- /dev/null +++ b/src/device/aml/interp.zig @@ -0,0 +1,737 @@ +//! A tree-walking AML interpreter — the evaluation stage on top of the parser's +//! structural namespace. It executes control methods (their bodies captured by +//! the parser) far enough to serve device discovery: device status (`_STA`, is a +//! device present), current resource settings (`_CRS`), and the operators, control +//! flow, locals/args, and +//! OperationRegion field access those methods reach for. +//! +//! Scope: integers, buffers, strings, packages, and references; If/Else/While/ +//! Return; the arithmetic/logic operators; method invocation; Name/Local/Arg +//! access; CreateField buffer patching (the common current-resource-settings +//! (`_CRS`) idiom); and field +//! reads/writes against SystemMemory and SystemIO regions. Opcodes outside this +//! set return `error.Unsupported`, which callers treat as "couldn't evaluate" and +//! fall back — never a hard failure. + +const std = @import("std"); +const op = @import("opcodes.zig"); +const nsp = @import("namespace.zig"); +const Node = nsp.Node; +const Namespace = nsp.Namespace; + +/// Injected hardware access for OperationRegion reads/writes (the arch VMM + pio). +pub const Hal = struct { + mapMmio: *const fn (virt: u64, phys: u64, writable: bool) void, + pioRead: *const fn (width: u8, port: u16) u32, + pioWrite: *const fn (width: u8, port: u16, value: u32) void, +}; + +pub const Error = error{ Unsupported, Truncated, DivByZero } || std.mem.Allocator.Error; + +/// A runtime AML value. +pub const Object = union(enum) { + uninitialized, + integer: u64, + buffer: []u8, + string: []u8, + package: []Object, + reference: *Node, + + pub fn asInt(self: Object) Error!u64 { + return switch (self) { + .integer => |v| v, + .buffer => |b| blk: { + var v: u64 = 0; + for (b, 0..) |byte, i| { + if (i >= 8) break; + v |= @as(u64, byte) << @intCast(i * 8); + } + break :blk v; + }, + else => error.Unsupported, + }; + } +}; + +const max_segs = 16; +const NamePath = struct { + rooted: bool = false, + parents: u8 = 0, + segs: [max_segs][4]u8 = undefined, + count: usize = 0, + fn slice(self: *const NamePath) []const [4]u8 { + return self.segs[0..self.count]; + } +}; + +const Cursor = struct { + b: []const u8, + i: usize = 0, + + fn eof(self: *Cursor) bool { + return self.i >= self.b.len; + } + fn peek(self: *Cursor) ?u8 { + return if (self.eof()) null else self.b[self.i]; + } + fn byte(self: *Cursor) Error!u8 { + if (self.eof()) return error.Truncated; + const v = self.b[self.i]; + self.i += 1; + return v; + } + fn take(self: *Cursor, n: usize) Error![]const u8 { + if (self.i + n > self.b.len) return error.Truncated; + const s = self.b[self.i .. self.i + n]; + self.i += n; + return s; + } + fn pkgLen(self: *Cursor) Error!usize { + const lead = try self.byte(); + const follow: usize = lead >> 6; + if (follow == 0) return lead & 0x3F; + var value: usize = lead & 0x0F; + var k: usize = 0; + while (k < follow) : (k += 1) value |= @as(usize, try self.byte()) << @intCast(4 + k * 8); + return value; + } + fn nameString(self: *Cursor) Error!NamePath { + var np = NamePath{}; + if (self.peek() == op.root_char) { + np.rooted = true; + self.i += 1; + } else { + while (self.peek() == op.parent_prefix_char) : (self.i += 1) np.parents += 1; + } + const lead = self.peek() orelse return np; + switch (lead) { + 0x00 => self.i += 1, + op.dual_name_prefix => { + self.i += 1; + try self.seg(&np); + try self.seg(&np); + }, + op.multi_name_prefix => { + self.i += 1; + const cnt = try self.byte(); + var k: usize = 0; + while (k < cnt) : (k += 1) try self.seg(&np); + }, + else => try self.seg(&np), + } + return np; + } + fn seg(self: *Cursor, np: *NamePath) Error!void { + const s = try self.take(4); + if (np.count < max_segs) { + np.segs[np.count] = s[0..4].*; + np.count += 1; + } + } +}; + +const Frame = struct { + args: [7]Object = .{.uninitialized} ** 7, + locals: [8]Object = .{.uninitialized} ** 8, + scope: *Node, + ret: Object = .uninitialized, + returned: bool = false, + broke: bool = false, +}; + +/// A CreateField binding: a name that indexes into a buffer object. +const BufField = struct { buf: *Node, byte_off: usize, bit_width: u32 }; + +pub const Interp = struct { + ns: *Namespace, + hal: Hal, + arena: std.mem.Allocator, + /// Runtime object overrides for Name nodes (Store targets, patched buffers). + dyn: std.AutoHashMapUnmanaged(*Node, Object) = .{}, + /// CreateField bindings active for the current evaluation. + fields: std.AutoHashMapUnmanaged(*Node, BufField) = .{}, + + pub fn init(ns: *Namespace, hal: Hal, arena: std.mem.Allocator) Interp { + return .{ .ns = ns, .hal = hal, .arena = arena }; + } + + /// Evaluate a namespace object: invoke a Method, read a Name's value, or read a + /// Field. Resets per-evaluation runtime state first. + pub fn evaluate(self: *Interp, node: *Node, args: []const Object) Error!Object { + self.dyn.clearRetainingCapacity(); + self.fields.clearRetainingCapacity(); + return self.invoke(node, args); + } + + fn invoke(self: *Interp, node: *Node, args: []const Object) Error!Object { + switch (node.kind) { + .method => { + var frame = Frame{ .scope = node }; + for (args, 0..) |a, i| { + if (i < frame.args.len) frame.args[i] = a; + } + var cur = Cursor{ .b = node.value }; + try self.execList(&cur, &frame); + return frame.ret; + }, + .name => { + if (self.dyn.get(node)) |o| return o; + var cur = Cursor{ .b = node.value }; + var frame = Frame{ .scope = node.parent orelse self.ns.root }; + return self.term(&cur, &frame); + }, + .field => return .{ .integer = try self.readField(node) }, + else => return .{ .reference = node }, + } + } + + /// Execute a TermList until it ends or the frame returns/breaks. + fn execList(self: *Interp, cur: *Cursor, frame: *Frame) Error!void { + while (!cur.eof() and !frame.returned and !frame.broke) { + _ = try self.term(cur, frame); + } + } + + /// Evaluate/execute one term, returning its value (`.uninitialized` for pure + /// statements). + fn term(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const lead = cur.peek() orelse return error.Truncated; + if (isNameStart(lead)) return self.nameRef(cur, frame); + _ = try cur.byte(); + + return switch (lead) { + op.zero_op => Object{ .integer = 0 }, + op.one_op => Object{ .integer = 1 }, + op.ones_op => Object{ .integer = ~@as(u64, 0) }, + op.byte_prefix => Object{ .integer = try self.readConst(cur, 1) }, + op.word_prefix => Object{ .integer = try self.readConst(cur, 2) }, + op.dword_prefix => Object{ .integer = try self.readConst(cur, 4) }, + op.qword_prefix => Object{ .integer = try self.readConst(cur, 8) }, + op.string_prefix => try self.readString(cur), + op.buffer_op => try self.buffer(cur, frame), + op.package_op, op.var_package_op => try self.package(cur, frame, lead == op.var_package_op), + + op.local0_op...op.local7_op => frame.locals[lead - op.local0_op], + op.arg0_op...op.arg6_op => frame.args[lead - op.arg0_op], + + op.return_op => blk: { + frame.ret = try self.term(cur, frame); + frame.returned = true; + break :blk .uninitialized; + }, + op.break_op => blk: { + frame.broke = true; + break :blk .uninitialized; + }, + op.continue_op, op.noop_op => .uninitialized, + + op.if_op => try self.ifElse(cur, frame), + op.while_op => try self.whileLoop(cur, frame), + op.store_op => try self.store(cur, frame), + op.increment_op => try self.incDec(cur, frame, 1), + op.decrement_op => try self.incDec(cur, frame, -1), + + op.add_op => try self.binary(cur, frame, .add), + op.subtract_op => try self.binary(cur, frame, .sub), + op.multiply_op => try self.binary(cur, frame, .mul), + op.mod_op => try self.binary(cur, frame, .mod), + op.and_op => try self.binary(cur, frame, .band), + op.or_op => try self.binary(cur, frame, .bor), + op.xor_op => try self.binary(cur, frame, .bxor), + op.nand_op => try self.binary(cur, frame, .nand), + op.nor_op => try self.binary(cur, frame, .nor), + op.shift_left_op => try self.binary(cur, frame, .shl), + op.shift_right_op => try self.binary(cur, frame, .shr), + op.divide_op => try self.divide(cur, frame), + + op.land_op => try self.logic2(cur, frame, .land), + op.lor_op => try self.logic2(cur, frame, .lor), + op.lequal_op => try self.logic2(cur, frame, .eq), + op.lgreater_op => try self.logic2(cur, frame, .gt), + op.lless_op => try self.logic2(cur, frame, .lt), + op.lnot_op => try self.lnot(cur, frame), + + op.not_op => blk: { + const v = try self.evalInt(cur, frame); + const r = ~v; + try self.storeTarget(cur, frame, .{ .integer = r }); + break :blk .{ .integer = r }; + }, + + op.size_of_op => try self.sizeOf(cur, frame), + op.index_op => try self.index(cur, frame), + op.deref_of_op => try self.derefOf(cur, frame), + op.to_integer_op => blk: { + const v = try self.evalInt(cur, frame); + try self.storeTarget(cur, frame, .{ .integer = v }); + break :blk .{ .integer = v }; + }, + op.to_buffer_op => try self.passThroughUnary(cur, frame), + + op.ext_op_prefix => try self.ext(cur, frame), + + // CreateXField: source, index, name (bit widths differ by op) + op.create_bit_field_op => try self.createField(cur, frame, 1), + op.create_byte_field_op => try self.createField(cur, frame, 8), + op.create_word_field_op => try self.createField(cur, frame, 16), + op.create_dword_field_op => try self.createField(cur, frame, 32), + op.create_qword_field_op => try self.createField(cur, frame, 64), + + else => error.Unsupported, + }; + } + + // --- name references ---------------------------------------------------- + + fn nameRef(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const np = try cur.nameString(); + const node = self.ns.resolve(frame.scope, np.rooted, np.parents, np.slice()) orelse + return .uninitialized; // unknown name -> treat as uninitialised + switch (node.kind) { + .method => { + var argbuf: [7]Object = undefined; + var i: usize = 0; + while (i < node.arg_count and i < argbuf.len) : (i += 1) argbuf[i] = try self.term(cur, frame); + return self.invoke(node, argbuf[0..@min(node.arg_count, argbuf.len)]); + }, + .field => return .{ .integer = try self.readField(node) }, + .name => return self.invoke(node, &.{}), + else => return .{ .reference = node }, + } + } + + // --- data objects ------------------------------------------------------- + + fn readConst(self: *Interp, cur: *Cursor, n: usize) Error!u64 { + _ = self; + const bytes = try cur.take(n); + var v: u64 = 0; + for (bytes, 0..) |b, i| v |= @as(u64, b) << @intCast(i * 8); + return v; + } + + fn readString(self: *Interp, cur: *Cursor) Error!Object { + const start = cur.i; + while (cur.peek()) |c| { + cur.i += 1; + if (c == 0) break; + } + const raw = cur.b[start .. cur.i - 1]; + const s = try self.arena.dupe(u8, raw); + return .{ .string = s }; + } + + fn buffer(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const start = cur.i; + const len = try cur.pkgLen(); + const end = @min(start + len, cur.b.len); + const size = try self.evalInt(cur, frame); + const data = cur.b[@min(cur.i, end)..end]; + const buf = try self.arena.alloc(u8, @intCast(size)); + @memset(buf, 0); + @memcpy(buf[0..@min(buf.len, data.len)], data[0..@min(buf.len, data.len)]); + cur.i = end; + return .{ .buffer = buf }; + } + + fn package(self: *Interp, cur: *Cursor, frame: *Frame, variable: bool) Error!Object { + const start = cur.i; + const len = try cur.pkgLen(); + const end = @min(start + len, cur.b.len); + const count: usize = if (variable) @intCast(try self.evalInt(cur, frame)) else try cur.byte(); + const elems = try self.arena.alloc(Object, count); + var i: usize = 0; + while (i < count and cur.i < end) : (i += 1) elems[i] = try self.term(cur, frame); + while (i < count) : (i += 1) elems[i] = .uninitialized; + cur.i = end; + return .{ .package = elems }; + } + + // --- operators ---------------------------------------------------------- + + const BinOp = enum { add, sub, mul, mod, band, bor, bxor, nand, nor, shl, shr }; + + fn binary(self: *Interp, cur: *Cursor, frame: *Frame, kind: BinOp) Error!Object { + const a = try self.evalInt(cur, frame); + const b = try self.evalInt(cur, frame); + const r: u64 = switch (kind) { + .add => a +% b, + .sub => a -% b, + .mul => a *% b, + .mod => if (b == 0) return error.DivByZero else a % b, + .band => a & b, + .bor => a | b, + .bxor => a ^ b, + .nand => ~(a & b), + .nor => ~(a | b), + .shl => if (b >= 64) 0 else a << @intCast(b), + .shr => if (b >= 64) 0 else a >> @intCast(b), + }; + try self.storeTarget(cur, frame, .{ .integer = r }); + return .{ .integer = r }; + } + + fn divide(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const a = try self.evalInt(cur, frame); + const b = try self.evalInt(cur, frame); + if (b == 0) return error.DivByZero; + try self.storeTarget(cur, frame, .{ .integer = a % b }); // remainder target + try self.storeTarget(cur, frame, .{ .integer = a / b }); // quotient target + return .{ .integer = a / b }; + } + + const LogicOp = enum { land, lor, eq, gt, lt }; + + fn logic2(self: *Interp, cur: *Cursor, frame: *Frame, kind: LogicOp) Error!Object { + const a = try self.evalInt(cur, frame); + const b = try self.evalInt(cur, frame); + const r = switch (kind) { + .land => a != 0 and b != 0, + .lor => a != 0 or b != 0, + .eq => a == b, + .gt => a > b, + .lt => a < b, + }; + return .{ .integer = if (r) ~@as(u64, 0) else 0 }; + } + + fn lnot(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + // 0x92 0x93/94/95 are the compound comparisons. + const b = cur.peek() orelse return error.Truncated; + switch (b) { + op.lnot.not_equal => { + cur.i += 1; + const x = try self.evalInt(cur, frame); + const y = try self.evalInt(cur, frame); + return .{ .integer = if (x != y) ~@as(u64, 0) else 0 }; + }, + op.lnot.less_equal => { + cur.i += 1; + const x = try self.evalInt(cur, frame); + const y = try self.evalInt(cur, frame); + return .{ .integer = if (x <= y) ~@as(u64, 0) else 0 }; + }, + op.lnot.greater_equal => { + cur.i += 1; + const x = try self.evalInt(cur, frame); + const y = try self.evalInt(cur, frame); + return .{ .integer = if (x >= y) ~@as(u64, 0) else 0 }; + }, + else => { + const x = try self.evalInt(cur, frame); + return .{ .integer = if (x == 0) ~@as(u64, 0) else 0 }; + }, + } + } + + fn incDec(self: *Interp, cur: *Cursor, frame: *Frame, delta: i64) Error!Object { + // Operand is a SuperName that is both read and written. + const save = cur.i; + const cur_val = try self.term(cur, frame); + const v = try cur_val.asInt(); + const r = if (delta > 0) v +% 1 else v -% 1; + var tcur = Cursor{ .b = cur.b, .i = save }; + try self.storeInto(&tcur, frame, .{ .integer = r }); + return .{ .integer = r }; + } + + fn sizeOf(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const o = try self.term(cur, frame); + return .{ .integer = switch (o) { + .buffer => |b| b.len, + .string => |s| s.len, + .package => |p| p.len, + else => 0, + } }; + } + + fn passThroughUnary(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const o = try self.term(cur, frame); + try self.storeTarget(cur, frame, o); + return o; + } + + fn index(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const src = try self.term(cur, frame); + const idx: usize = @intCast(try self.evalInt(cur, frame)); + // Optional target (a reference); we don't materialise references, so store + // the indexed value if a target is present. + const val: Object = switch (src) { + .buffer => |b| .{ .integer = if (idx < b.len) b[idx] else 0 }, + .package => |p| if (idx < p.len) p[idx] else .uninitialized, + .string => |s| .{ .integer = if (idx < s.len) s[idx] else 0 }, + else => .uninitialized, + }; + try self.storeTarget(cur, frame, val); + return val; + } + + fn derefOf(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const o = try self.term(cur, frame); + return switch (o) { + .reference => |n| self.invoke(n, &.{}), + else => o, + }; + } + + // --- control flow ------------------------------------------------------- + + fn ifElse(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const start = cur.i; + const end = @min(start + try cur.pkgLen(), cur.b.len); + const cond = try self.evalInt(cur, frame); + if (cond != 0) { + var body = Cursor{ .b = cur.b[0..end], .i = cur.i }; + try self.execList(&body, frame); + cur.i = end; + // Skip a trailing Else. + if (cur.peek() == op.else_op) { + cur.i += 1; + const es = cur.i; + const ee = @min(es + try cur.pkgLen(), cur.b.len); + cur.i = ee; + } + } else { + cur.i = end; + if (cur.peek() == op.else_op) { + cur.i += 1; + const es = cur.i; + const ee = @min(es + try cur.pkgLen(), cur.b.len); + var body = Cursor{ .b = cur.b[0..ee], .i = cur.i }; + try self.execList(&body, frame); + cur.i = ee; + } + } + return .uninitialized; + } + + fn whileLoop(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const start = cur.i; + const end = @min(start + try cur.pkgLen(), cur.b.len); + const pred_at = cur.i; + var guard: usize = 0; + while (guard < 100_000) : (guard += 1) { + var pc = Cursor{ .b = cur.b[0..end], .i = pred_at }; + const cond = try self.evalInt(&pc, frame); + if (cond == 0) break; + var body = Cursor{ .b = cur.b[0..end], .i = pc.i }; + try self.execList(&body, frame); + if (frame.returned) break; + if (frame.broke) { + frame.broke = false; + break; + } + } + cur.i = end; + return .uninitialized; + } + + // --- store -------------------------------------------------------------- + + fn store(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const value = try self.term(cur, frame); + try self.storeInto(cur, frame, value); + return value; + } + + /// A Store *target* that may be NullName (no store). + fn storeTarget(self: *Interp, cur: *Cursor, frame: *Frame, value: Object) Error!void { + if (cur.peek() == 0x00) { + cur.i += 1; // NullName + return; + } + try self.storeInto(cur, frame, value); + } + + fn storeInto(self: *Interp, cur: *Cursor, frame: *Frame, value: Object) Error!void { + const lead = cur.peek() orelse return error.Truncated; + if (isNameStart(lead)) { + const np = try cur.nameString(); + const node = self.ns.resolve(frame.scope, np.rooted, np.parents, np.slice()) orelse return; + if (self.fields.get(node)) |bf| { + try self.writeBufField(bf, try value.asInt()); + } else if (node.kind == .field) { + try self.writeField(node, try value.asInt()); + } else { + try self.dyn.put(self.arena, node, value); + } + return; + } + _ = try cur.byte(); + switch (lead) { + 0x00 => {}, // NullName + op.local0_op...op.local7_op => frame.locals[lead - op.local0_op] = value, + op.arg0_op...op.arg6_op => frame.args[lead - op.arg0_op] = value, + op.index_op => { + const src = try self.term(cur, frame); + const idx: usize = @intCast(try self.evalInt(cur, frame)); + switch (src) { + .buffer => |b| if (idx < b.len) { + b[idx] = @truncate(try value.asInt()); + }, + .package => |p| if (idx < p.len) { + p[idx] = value; + }, + else => {}, + } + }, + else => return error.Unsupported, + } + } + + // --- CreateField (buffer patching) -------------------------------------- + + fn createField(self: *Interp, cur: *Cursor, frame: *Frame, bit_width: u32) Error!Object { + const src = try self.term(cur, frame); // source buffer (as a reference or value) + const bit_index = try self.evalInt(cur, frame); + const np = try cur.nameString(); + const node = self.ns.resolve(frame.scope, np.rooted, np.parents, np.slice()) orelse return .uninitialized; + + // Bind the new name to the source buffer's node so stores land in it. + const buf_node: *Node = switch (src) { + .reference => |n| n, + else => return .uninitialized, + }; + // Materialise the buffer into `dyn` so patches persist and are returned. + if (self.dyn.get(buf_node) == null) { + const val = try self.invoke(buf_node, &.{}); + try self.dyn.put(self.arena, buf_node, val); + } + const byte_off: usize = @intCast(bit_index / 8); + try self.fields.put(self.arena, node, .{ .buf = buf_node, .byte_off = byte_off, .bit_width = bit_width }); + return .uninitialized; + } + + fn writeBufField(self: *Interp, bf: BufField, value: u64) Error!void { + const obj = self.dyn.get(bf.buf) orelse return; + const buf = switch (obj) { + .buffer => |b| b, + else => return, + }; + const nbytes = (bf.bit_width + 7) / 8; + var k: usize = 0; + while (k < nbytes and bf.byte_off + k < buf.len) : (k += 1) { + buf[bf.byte_off + k] = @truncate(value >> @intCast(k * 8)); + } + } + + // --- OperationRegion field access --------------------------------------- + + fn readField(self: *Interp, field: *Node) Error!u64 { + const region = field.region orelse return error.Unsupported; + if (field.bit_width == 0 or field.bit_width > 64) return error.Unsupported; + const base = try self.regionBase(region); + const start_byte = base + field.bit_offset / 8; + const shift: u7 = @intCast(field.bit_offset % 8); + const total = @as(usize, shift) + field.bit_width; + const nbytes = (total + 7) / 8; + var raw: u128 = 0; + var k: usize = 0; + while (k < nbytes) : (k += 1) { + raw |= @as(u128, try self.readRegionByte(region.region_space, start_byte + k)) << @intCast(k * 8); + } + const masked = (raw >> shift) & bitMask(field.bit_width); + return @truncate(masked); + } + + fn writeField(self: *Interp, field: *Node, value: u64) Error!void { + const region = field.region orelse return error.Unsupported; + if (field.bit_width == 0 or field.bit_width > 64) return error.Unsupported; + const base = try self.regionBase(region); + const start_byte = base + field.bit_offset / 8; + const shift: u7 = @intCast(field.bit_offset % 8); + const total = @as(usize, shift) + field.bit_width; + const nbytes = (total + 7) / 8; + // Read-modify-write byte by byte. + var raw: u128 = 0; + var k: usize = 0; + while (k < nbytes) : (k += 1) { + raw |= @as(u128, try self.readRegionByte(region.region_space, start_byte + k)) << @intCast(k * 8); + } + const mask = bitMask(field.bit_width) << shift; + raw = (raw & ~mask) | ((@as(u128, value) << shift) & mask); + k = 0; + while (k < nbytes) : (k += 1) { + try self.writeRegionByte(region.region_space, start_byte + k, @truncate(raw >> @intCast(k * 8))); + } + } + + fn regionBase(self: *Interp, region: *Node) Error!u64 { + var cur = Cursor{ .b = region.region_offset_aml }; + var frame = Frame{ .scope = region.parent orelse self.ns.root }; + return (try self.term(&cur, &frame)).asInt(); + } + + fn readRegionByte(self: *Interp, space: u8, addr: u64) Error!u8 { + switch (space) { + 0 => { // SystemMemory + self.hal.mapMmio(addr & ~@as(u64, 0xFFF), addr & ~@as(u64, 0xFFF), true); + const p: *align(1) const volatile u8 = @ptrFromInt(addr); + return p.*; + }, + 1 => return @truncate(self.hal.pioRead(1, @intCast(addr & 0xFFFF))), // SystemIO + else => return error.Unsupported, + } + } + + fn writeRegionByte(self: *Interp, space: u8, addr: u64, value: u8) Error!void { + switch (space) { + 0 => { + self.hal.mapMmio(addr & ~@as(u64, 0xFFF), addr & ~@as(u64, 0xFFF), true); + const p: *align(1) volatile u8 = @ptrFromInt(addr); + p.* = value; + }, + 1 => self.hal.pioWrite(1, @intCast(addr & 0xFFFF), value), + else => return error.Unsupported, + } + } + + // --- extended opcodes --------------------------------------------------- + + fn ext(self: *Interp, cur: *Cursor, frame: *Frame) Error!Object { + const e = try cur.byte(); + switch (e) { + op.ext.debug => return .uninitialized, + op.ext.revision => return .{ .integer = 2 }, + op.ext.timer => return .{ .integer = 0 }, + // Mutex/Event ops are no-ops in this single-threaded evaluator. + op.ext.acquire => { + _ = try self.term(cur, frame); // mutex SuperName + _ = try cur.take(2); // timeout + return .{ .integer = 0 }; // acquired + }, + op.ext.release, op.ext.reset, op.ext.signal => { + _ = try self.term(cur, frame); + return .uninitialized; + }, + op.ext.wait => { + _ = try self.term(cur, frame); + _ = try self.term(cur, frame); + return .{ .integer = 0 }; + }, + op.ext.sleep, op.ext.stall => { + _ = try self.term(cur, frame); + return .uninitialized; + }, + else => return error.Unsupported, + } + } + + fn evalInt(self: *Interp, cur: *Cursor, frame: *Frame) Error!u64 { + return (try self.term(cur, frame)).asInt(); + } +}; + +fn bitMask(width: u32) u128 { + if (width >= 128) return ~@as(u128, 0); + return (@as(u128, 1) << @intCast(width)) - 1; +} + +fn isNameStart(b: u8) bool { + return (b >= op.name_char_start and b <= op.name_char_end) or + b == op.name_char_underscore or + b == op.root_char or + b == op.parent_prefix_char or + b == op.dual_name_prefix or + b == op.multi_name_prefix; +} diff --git a/src/device/aml/namespace.zig b/src/device/aml/namespace.zig new file mode 100644 index 0000000..ea0ed76 --- /dev/null +++ b/src/device/aml/namespace.zig @@ -0,0 +1,181 @@ +//! The ACPI namespace the AML parser builds: a tree of named nodes, plus the name +//! resolution rules the parser needs while it walks (so a method invocation can be +//! resolved to its declaration to learn its argument count). +//! +//! Nodes are individually allocated and linked intrusively (first-child / +//! next-sibling), the same shape as the device tree in `device.zig`. + +const std = @import("std"); + +pub const NodeKind = enum { + root, + scope, + device, + method, + name, + region, // OperationRegion + field, // a Field unit + mutex, + event, + processor, + power_res, + thermal_zone, + alias, + external, + other, +}; + +pub const Node = struct { + /// The 4-byte NameSeg identifying this node within its parent. The root uses + /// all-zero. + seg: [4]u8 = .{ 0, 0, 0, 0 }, + kind: NodeKind = .other, + /// For Method / External: the declared argument count (0..7). Used to resolve + /// how many TermArgs a method invocation consumes. + arg_count: u8 = 0, + /// For Name: the AML bytes of its DataRefObject (so a value like a sleep + /// state's (`_Sx`) Package can be parsed on demand). For Method: the AML bytes of the body, + /// interpreted on demand by the evaluator. Empty otherwise. + value: []const u8 = &.{}, + + // OperationRegion metadata (kind == .region): the address space, plus the AML + // of the offset/length expressions (evaluated lazily, usually constants). + region_space: u8 = 0, + region_offset_aml: []const u8 = &.{}, + region_len_aml: []const u8 = &.{}, + + // Field-unit metadata (kind == .field): which region it lives in and its bit + // position/width/access, so the evaluator can read/write it. + region: ?*Node = null, + bit_offset: u32 = 0, + bit_width: u32 = 0, + access_type: u8 = 0, + + parent: ?*Node = null, + first_child: ?*Node = null, + next_sibling: ?*Node = null, + + /// Depth-first count of this node and everything under it. + pub fn subtreeCount(self: *const Node) usize { + var n: usize = 1; + var c = self.first_child; + while (c) |child| : (c = child.next_sibling) n += child.subtreeCount(); + return n; + } +}; + +pub const Namespace = struct { + allocator: std.mem.Allocator, + root: *Node, + + pub fn init(allocator: std.mem.Allocator) !Namespace { + const root = try allocator.create(Node); + root.* = .{ .kind = .root }; + return .{ .allocator = allocator, .root = root }; + } + + pub fn nodeCount(self: *const Namespace) usize { + return self.root.subtreeCount(); + } + + fn findChild(parent: *Node, seg: [4]u8) ?*Node { + var c = parent.first_child; + while (c) |child| : (c = child.next_sibling) { + if (std.mem.eql(u8, &child.seg, &seg)) return child; + } + return null; + } + + /// The direct child of `node` named `seg`, or null. Unlike `resolve`, this does + /// not apply the search-rule walk-up — it looks only at immediate children (for + /// reading a device's own hardware ID (`_HID`) / current resource settings (`_CRS`)). + pub fn childOf(node: *Node, seg: [4]u8) ?*Node { + return findChild(node, seg); + } + + fn newChild(self: *Namespace, parent: *Node, seg: [4]u8, kind: NodeKind) !*Node { + const n = try self.allocator.create(Node); + n.* = .{ .seg = seg, .kind = kind, .parent = parent }; + // Append at the tail so a dump reads in declaration order. + if (parent.first_child == null) { + parent.first_child = n; + } else { + var cur = parent.first_child.?; + while (cur.next_sibling) |sib| cur = sib; + cur.next_sibling = n; + } + return n; + } + + /// Create a Field unit node directly under `scope` (field units live in the + /// scope of the Field/IndexField/BankField, not under the region). + pub fn newFieldUnit(self: *Namespace, scope: *Node, seg: [4]u8) !*Node { + return self.findOrCreate(scope, seg, .field); + } + + fn findOrCreate(self: *Namespace, parent: *Node, seg: [4]u8, kind: NodeKind) !*Node { + if (findChild(parent, seg)) |existing| { + // Reopening a scope (e.g. Scope(\_SB) after Device \_SB) keeps the more + // specific kind rather than downgrading to a plain scope. + if (existing.kind == .scope and kind != .scope) existing.kind = kind; + return existing; + } + return self.newChild(parent, seg, kind); + } + + /// The node a definition's NameString names, creating any intermediate scopes. + /// The final segment is created (or found) with `kind`; intermediates are + /// scopes. Returns the namespace root for a NullName (empty path). + pub fn place( + self: *Namespace, + current: *Node, + rooted: bool, + parents: u8, + segs: []const [4]u8, + kind: NodeKind, + ) !*Node { + var base = startNode(self, current, rooted, parents); + if (segs.len == 0) return base; + var i: usize = 0; + while (i + 1 < segs.len) : (i += 1) { + base = try self.findOrCreate(base, segs[i], .scope); + } + return self.findOrCreate(base, segs[segs.len - 1], kind); + } + + /// Resolve a NameString *reference* to an existing node, or null. A single + /// relative segment uses the ACPI search rule (walk up the ancestors); any + /// rooted, parented, or multi-segment path is resolved exactly. + pub fn resolve( + self: *Namespace, + current: *Node, + rooted: bool, + parents: u8, + segs: []const [4]u8, + ) ?*Node { + if (segs.len == 0) return null; + + if (!rooted and parents == 0 and segs.len == 1) { + // Search rule: this scope, then each ancestor up to the root. + var scope: ?*Node = current; + while (scope) |s| : (scope = s.parent) { + if (findChild(s, segs[0])) |n| return n; + } + return null; + } + + var base = startNode(self, current, rooted, parents); + for (segs) |seg| { + base = findChild(base, seg) orelse return null; + } + return base; + } + + fn startNode(self: *Namespace, current: *Node, rooted: bool, parents: u8) *Node { + if (rooted) return self.root; + var base = current; + var up = parents; + while (up > 0) : (up -= 1) base = base.parent orelse self.root; + return base; + } +}; diff --git a/src/device/aml/opcodes.zig b/src/device/aml/opcodes.zig new file mode 100644 index 0000000..eb894dd --- /dev/null +++ b/src/device/aml/opcodes.zig @@ -0,0 +1,137 @@ +//! AML opcode constants — the full ACPI Machine Language opcode table. +//! +//! Single-byte opcodes are plain values. Extended opcodes are a two-byte sequence +//! `ext_prefix` (0x5B) followed by a byte listed under `ext`. A few comparison +//! opcodes are `lnot_op` (0x92) followed by a second byte (see `lnot`). + +// --- name / path characters ------------------------------------------------- +pub const zero_op = 0x00; +pub const one_op = 0x01; +pub const alias_op = 0x06; +pub const name_op = 0x08; +pub const byte_prefix = 0x0A; +pub const word_prefix = 0x0B; +pub const dword_prefix = 0x0C; +pub const string_prefix = 0x0D; +pub const qword_prefix = 0x0E; +pub const scope_op = 0x10; +pub const buffer_op = 0x11; +pub const package_op = 0x12; +pub const var_package_op = 0x13; +pub const method_op = 0x14; +pub const external_op = 0x15; + +pub const dual_name_prefix = 0x2E; +pub const multi_name_prefix = 0x2F; +pub const ext_op_prefix = 0x5B; +pub const root_char = 0x5C; +pub const parent_prefix_char = 0x5E; +pub const name_char_underscore = 0x5F; + +pub const digit_char_start = 0x30; +pub const digit_char_end = 0x39; +pub const name_char_start = 0x41; // 'A' +pub const name_char_end = 0x5A; // 'Z' + +// --- locals / args ---------------------------------------------------------- +pub const local0_op = 0x60; +pub const local7_op = 0x67; +pub const arg0_op = 0x68; +pub const arg6_op = 0x6E; + +// --- store / references / arithmetic --------------------------------------- +pub const store_op = 0x70; +pub const ref_of_op = 0x71; +pub const add_op = 0x72; +pub const concat_op = 0x73; +pub const subtract_op = 0x74; +pub const increment_op = 0x75; +pub const decrement_op = 0x76; +pub const multiply_op = 0x77; +pub const divide_op = 0x78; +pub const shift_left_op = 0x79; +pub const shift_right_op = 0x7A; +pub const and_op = 0x7B; +pub const nand_op = 0x7C; +pub const or_op = 0x7D; +pub const nor_op = 0x7E; +pub const xor_op = 0x7F; +pub const not_op = 0x80; +pub const find_set_left_bit_op = 0x81; +pub const find_set_right_bit_op = 0x82; +pub const deref_of_op = 0x83; +pub const concat_res_op = 0x84; +pub const mod_op = 0x85; +pub const notify_op = 0x86; +pub const size_of_op = 0x87; +pub const index_op = 0x88; +pub const match_op = 0x89; +pub const create_dword_field_op = 0x8A; +pub const create_word_field_op = 0x8B; +pub const create_byte_field_op = 0x8C; +pub const create_bit_field_op = 0x8D; +pub const object_type_op = 0x8E; +pub const create_qword_field_op = 0x8F; + +pub const land_op = 0x90; +pub const lor_op = 0x91; +pub const lnot_op = 0x92; // may be followed by a second byte (see `lnot`) +pub const lequal_op = 0x93; +pub const lgreater_op = 0x94; +pub const lless_op = 0x95; +pub const to_buffer_op = 0x96; +pub const to_decimal_string_op = 0x97; +pub const to_hex_string_op = 0x98; +pub const to_integer_op = 0x99; +pub const to_string_op = 0x9C; +pub const copy_object_op = 0x9D; +pub const mid_op = 0x9E; +pub const continue_op = 0x9F; +pub const if_op = 0xA0; +pub const else_op = 0xA1; +pub const while_op = 0xA2; +pub const noop_op = 0xA3; +pub const return_op = 0xA4; +pub const break_op = 0xA5; +pub const break_point_op = 0xCC; +pub const ones_op = 0xFF; + +/// Second bytes of the `lnot_op` (0x92) compound comparison opcodes. +pub const lnot = struct { + pub const not_equal = 0x93; // LNotEqualOp: 0x92 0x93 + pub const less_equal = 0x94; // LLessEqualOp: 0x92 0x94 + pub const greater_equal = 0x95; // LGreaterEqualOp: 0x92 0x95 +}; + +/// Second bytes of extended opcodes (prefixed by `ext_op_prefix`, 0x5B). +pub const ext = struct { + pub const mutex = 0x01; + pub const event = 0x02; + pub const cond_ref_of = 0x12; + pub const create_field = 0x13; + pub const load_table = 0x1F; + pub const load = 0x20; + pub const stall = 0x21; + pub const sleep = 0x22; + pub const acquire = 0x23; + pub const signal = 0x24; + pub const wait = 0x25; + pub const reset = 0x26; + pub const release = 0x27; + pub const from_bcd = 0x28; + pub const to_bcd = 0x29; + pub const unload = 0x2A; + pub const revision = 0x30; + pub const debug = 0x31; + pub const fatal = 0x32; + pub const timer = 0x33; + pub const op_region = 0x80; + pub const field = 0x81; + pub const device = 0x82; + pub const processor = 0x83; + pub const power_res = 0x84; + pub const thermal_zone = 0x85; + pub const index_field = 0x86; + pub const bank_field = 0x87; + pub const data_region = 0x88; +}; diff --git a/src/device/aml/parser.zig b/src/device/aml/parser.zig new file mode 100644 index 0000000..6d16271 --- /dev/null +++ b/src/device/aml/parser.zig @@ -0,0 +1,517 @@ +//! Recursive-descent AML parser. Walks the entire byte stream — including method +//! bodies — building the ACPI namespace as it goes. It does not *evaluate* +//! anything (no OperationRegion reads, no arithmetic); it parses structure so the +//! cursor stays aligned and every named object is recorded. +//! +//! The one genuine ambiguity in AML is method invocation: a bare NameString in an +//! operand position is a call whose argument count is only known from the method's +//! (earlier) declaration. Because we build the namespace in the same in-order pass, +//! `resolve` finds that declaration and tells us how many operands to consume. +//! +//! Safety net: every object delimited by a PkgLength (Scope/Device/Method/If/While/ +//! Field/Buffer/Package/…) is parsed within its known extent, and the cursor is +//! snapped to that extent afterwards. So a mis-resolved invocation can only desync +//! *within* one such object; the enclosing walk realigns at the boundary. + +const std = @import("std"); +const op = @import("opcodes.zig"); +const ns = @import("namespace.zig"); +const Namespace = ns.Namespace; +const Node = ns.Node; + +pub const Error = error{ Truncated, Malformed } || std.mem.Allocator.Error; + +const max_segs = 64; + +/// A parsed NameString: an optional root anchor or some parent hops, then a list +/// of 4-byte segments. +const NamePath = struct { + rooted: bool = false, + parents: u8 = 0, + segs: [max_segs][4]u8 = undefined, + count: usize = 0, + + fn slice(self: *const NamePath) []const [4]u8 { + return self.segs[0..self.count]; + } +}; + +pub const Parser = struct { + aml: []const u8, + pos: usize = 0, + namespace: *Namespace, + + pub fn init(aml: []const u8, namespace: *Namespace) Parser { + return .{ .aml = aml, .namespace = namespace }; + } + + /// Parse the whole block as a TermList under the namespace root. Returns the + /// number of bytes consumed — equal to `aml.len` for a clean full traversal. + pub fn parseAll(self: *Parser) usize { + self.termList(self.aml.len, self.namespace.root); + return self.pos; + } + + // --- cursor primitives -------------------------------------------------- + + fn eof(self: *Parser) bool { + return self.pos >= self.aml.len; + } + + fn peek(self: *Parser) ?u8 { + return if (self.eof()) null else self.aml[self.pos]; + } + + fn readByte(self: *Parser) Error!u8 { + if (self.eof()) return error.Truncated; + const b = self.aml[self.pos]; + self.pos += 1; + return b; + } + + fn skip(self: *Parser, n: usize) Error!void { + if (self.pos + n > self.aml.len) return error.Truncated; + self.pos += n; + } + + fn skipCString(self: *Parser) Error!void { + while (true) { + const b = try self.readByte(); + if (b == 0) return; + } + } + + /// AML PkgLength: the lead byte's top two bits give how many extra bytes + /// follow; the value counts from the start of the PkgLength field. + fn readPkgLength(self: *Parser) Error!usize { + const lead = try self.readByte(); + const follow: usize = lead >> 6; + if (follow == 0) return lead & 0x3F; + var value: usize = lead & 0x0F; + var i: usize = 0; + while (i < follow) : (i += 1) { + const b = try self.readByte(); + value |= @as(usize, b) << @intCast(4 + i * 8); + } + return value; + } + + fn readNameSeg(self: *Parser) Error![4]u8 { + if (self.pos + 4 > self.aml.len) return error.Truncated; + const seg = self.aml[self.pos..][0..4].*; + self.pos += 4; + return seg; + } + + fn readNameString(self: *Parser) Error!NamePath { + var np = NamePath{}; + // A NameString is either root-anchored or parent-relative, not both. + if (self.peek() == op.root_char) { + np.rooted = true; + self.pos += 1; + } else { + while (self.peek() == op.parent_prefix_char) : (self.pos += 1) np.parents += 1; + } + + const lead = self.peek() orelse return np; + switch (lead) { + 0x00 => self.pos += 1, // NullName + op.dual_name_prefix => { + self.pos += 1; + try self.appendSeg(&np); + try self.appendSeg(&np); + }, + op.multi_name_prefix => { + self.pos += 1; + const cnt = try self.readByte(); + var i: usize = 0; + while (i < cnt) : (i += 1) try self.appendSeg(&np); + }, + else => { + if (isNameStart(lead)) try self.appendSeg(&np); + }, + } + return np; + } + + fn appendSeg(self: *Parser, np: *NamePath) Error!void { + const seg = try self.readNameSeg(); + if (np.count < max_segs) { + np.segs[np.count] = seg; + np.count += 1; + } + } + + // --- term list / object ------------------------------------------------- + + /// Parse objects until `end`, then snap to `end`. Any parse error resyncs to + /// the boundary rather than propagating — containment for the rare desync. + fn termList(self: *Parser, end: usize, scope: *Node) void { + while (self.pos < end) { + self.object(scope) catch break; + } + self.pos = end; + } + + /// Parse exactly one object/term at the cursor. Used for both TermObjs and + /// operands (TermArg / SuperName / Target all reduce to "one object" for the + /// purpose of advancing the cursor). + fn object(self: *Parser, scope: *Node) Error!void { + const lead = self.peek() orelse return error.Truncated; + if (isNameStart(lead)) return self.nameInvocation(scope); + + _ = try self.readByte(); + switch (lead) { + // constants and no-operand statements + op.zero_op, op.one_op, op.ones_op => {}, + op.noop_op, op.continue_op, op.break_op, op.break_point_op => {}, + op.local0_op...op.local7_op => {}, + op.arg0_op...op.arg6_op => {}, + + // literal data + op.byte_prefix => try self.skip(1), + op.word_prefix => try self.skip(2), + op.dword_prefix => try self.skip(4), + op.qword_prefix => try self.skip(8), + op.string_prefix => try self.skipCString(), + + // data containers (contents skipped via their PkgLength) + op.buffer_op, op.package_op, op.var_package_op => try self.skipPkg(), + + // namespace modifiers / named objects + op.name_op => try self.opName(scope), + op.alias_op => try self.opAlias(scope), + op.scope_op => try self.opScopeLike(scope, .scope), + op.method_op => try self.opMethod(scope), + op.external_op => try self.opExternal(scope), + op.ext_op_prefix => try self.opExt(scope), + + // control flow + op.if_op => try self.opIf(scope), + op.else_op => try self.opElse(scope), + op.while_op => try self.opWhile(scope), + op.return_op => try self.object(scope), + op.notify_op => try self.args(scope, 2), + + // stores / references / unary+target + op.store_op => try self.args(scope, 2), + op.ref_of_op, op.deref_of_op, op.size_of_op, op.object_type_op => try self.args(scope, 1), + op.increment_op, op.decrement_op => try self.args(scope, 1), + op.not_op, op.find_set_left_bit_op, op.find_set_right_bit_op => try self.args(scope, 2), + op.to_buffer_op, op.to_decimal_string_op, op.to_hex_string_op, op.to_integer_op => try self.args(scope, 2), + op.copy_object_op => try self.args(scope, 2), + + // binary + target + op.add_op, op.subtract_op, op.multiply_op, op.mod_op => try self.args(scope, 3), + op.and_op, op.nand_op, op.or_op, op.nor_op, op.xor_op => try self.args(scope, 3), + op.shift_left_op, op.shift_right_op, op.concat_op, op.concat_res_op, op.index_op => try self.args(scope, 3), + op.divide_op => try self.args(scope, 4), + op.to_string_op => try self.args(scope, 3), + op.mid_op => try self.args(scope, 4), + + // logical + op.land_op, op.lor_op => try self.args(scope, 2), + op.lequal_op, op.lgreater_op, op.lless_op => try self.args(scope, 2), + op.lnot_op => try self.opLnot(scope), + + op.match_op => try self.opMatch(scope), + + // CreateXField: NameString + op.create_dword_field_op, + op.create_word_field_op, + op.create_byte_field_op, + op.create_bit_field_op, + op.create_qword_field_op, + => try self.opCreateField(scope, 2), + + else => return error.Malformed, + } + } + + /// Parse `n` operands. + fn args(self: *Parser, scope: *Node, n: usize) Error!void { + var i: usize = 0; + while (i < n) : (i += 1) try self.object(scope); + } + + /// A NameString in operand/statement position: a method invocation (consuming + /// the callee's declared argument count) or a plain name reference. + fn nameInvocation(self: *Parser, scope: *Node) Error!void { + const np = try self.readNameString(); + if (self.namespace.resolve(scope, np.rooted, np.parents, np.slice())) |node| { + if ((node.kind == .method or node.kind == .external) and node.arg_count > 0) { + try self.args(scope, node.arg_count); + } + } + } + + /// Skip a PkgLength-delimited body wholesale (Buffer / Package / VarPackage): + /// the contents are pure data, never namespace declarations. + fn skipPkg(self: *Parser) Error!void { + const start = self.pos; + const len = try self.readPkgLength(); + const end = start + len; + if (end > self.aml.len) return error.Truncated; + self.pos = end; + } + + // --- namespace objects -------------------------------------------------- + + fn opName(self: *Parser, scope: *Node) Error!void { + const np = try self.readNameString(); + const val_start = self.pos; + try self.object(scope); // the DataRefObject value + const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .name); + node.value = self.aml[val_start..self.pos]; + } + + fn opAlias(self: *Parser, scope: *Node) Error!void { + _ = try self.readNameString(); // source + const np = try self.readNameString(); // the alias name + _ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .alias); + } + + fn opMethod(self: *Parser, scope: *Node) Error!void { + const start = self.pos; + const end = start + try self.readPkgLength(); + const np = try self.readNameString(); + const flags = try self.readByte(); + const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .method); + node.arg_count = flags & 0x7; + // Capture the body for on-demand evaluation and skip it — objects declared + // inside a method are created at *runtime*, not at load, so they must not + // become permanent namespace nodes. + node.value = self.aml[self.pos..@min(end, self.aml.len)]; + self.pos = end; + } + + fn opExternal(self: *Parser, scope: *Node) Error!void { + const np = try self.readNameString(); + _ = try self.readByte(); // object type + const arg_count = try self.readByte(); + const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .external); + node.arg_count = arg_count; + } + + /// Scope / Device / ThermalZone: PkgLength, NameString, then a nested TermList. + fn opScopeLike(self: *Parser, scope: *Node, kind: ns.NodeKind) Error!void { + const start = self.pos; + const end = start + try self.readPkgLength(); + const np = try self.readNameString(); + const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), kind); + self.termList(end, node); + } + + fn opProcessor(self: *Parser, scope: *Node) Error!void { + const start = self.pos; + const end = start + try self.readPkgLength(); + const np = try self.readNameString(); + try self.skip(6); // ProcID(byte) + PblkAddr(dword) + PblkLen(byte) + const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .processor); + self.termList(end, node); + } + + fn opPowerRes(self: *Parser, scope: *Node) Error!void { + const start = self.pos; + const end = start + try self.readPkgLength(); + const np = try self.readNameString(); + try self.skip(3); // SystemLevel(byte) + ResourceOrder(word) + const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .power_res); + self.termList(end, node); + } + + /// OperationRegion: NameString, RegionSpace(byte), Offset(TermArg), Len(TermArg). + /// The offset/length expressions are kept as AML for lazy evaluation. + fn opRegion(self: *Parser, scope: *Node) Error!void { + const np = try self.readNameString(); + const space = try self.readByte(); + const off_start = self.pos; + try self.object(scope); + const off_end = self.pos; + try self.object(scope); + const len_end = self.pos; + const node = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .region); + node.region_space = space; + node.region_offset_aml = self.aml[off_start..off_end]; + node.region_len_aml = self.aml[off_end..len_end]; + } + + fn opDataRegion(self: *Parser, scope: *Node) Error!void { + const np = try self.readNameString(); + try self.args(scope, 3); // signature, oem id, oem table id (TermArgs) + _ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .region); + } + + fn opMutex(self: *Parser, scope: *Node) Error!void { + const np = try self.readNameString(); + try self.skip(1); // sync flags + _ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .mutex); + } + + fn opEvent(self: *Parser, scope: *Node) Error!void { + const np = try self.readNameString(); + _ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .event); + } + + /// CreateXField: `count` TermArgs then the new field's NameString. + fn opCreateField(self: *Parser, scope: *Node, count: usize) Error!void { + try self.args(scope, count); + const np = try self.readNameString(); + _ = try self.namespace.place(scope, np.rooted, np.parents, np.slice(), .name); + } + + /// Field / IndexField / BankField: a region/bank reference, flags, then a + /// FieldList whose NamedFields become nodes in the current scope. For a plain + /// Field, the first NameString is the backing region — captured so field units + /// carry a region + bit position the evaluator can read/write. + fn opField(self: *Parser, scope: *Node, name_strings: u8, bank: bool) Error!void { + const start = self.pos; + const end = start + try self.readPkgLength(); + var region: ?*Node = null; + var i: u8 = 0; + while (i < name_strings) : (i += 1) { + const np = try self.readNameString(); + // Only a plain Field's single NameString denotes an OperationRegion. + if (name_strings == 1) region = self.namespace.resolve(scope, np.rooted, np.parents, np.slice()); + } + if (bank) try self.object(scope); // bank value TermArg + const flags = try self.readByte(); + self.fieldList(end, scope, region, flags & 0x0F); + } + + fn fieldList(self: *Parser, end: usize, scope: *Node, region: ?*Node, initial_access: u8) void { + var bit_offset: u32 = 0; + var access = initial_access; + while (self.pos < end) { + const lead = self.peek() orelse break; + switch (lead) { + 0x00 => { // ReservedField: advances the bit position + self.pos += 1; + const width = self.readPkgLength() catch break; + bit_offset += @intCast(width); + }, + 0x01 => { // AccessField: AccessType (low nibble) + AccessAttrib + self.pos += 1; + const at = self.readByte() catch break; + self.skip(1) catch break; + access = at & 0x0F; + }, + 0x02 => { // ConnectField: NameString | BufferData + self.pos += 1; + self.object(scope) catch break; + }, + 0x03 => { // ExtendedAccessField: type + attrib + length + self.pos += 1; + self.skip(3) catch break; + }, + else => { // NamedField: NameSeg + PkgLength (bit width) + const seg = self.readNameSeg() catch break; + const width = self.readPkgLength() catch break; + const unit = self.namespace.newFieldUnit(scope, seg) catch break; + unit.region = region; + unit.bit_offset = bit_offset; + unit.bit_width = @intCast(width); + unit.access_type = access; + bit_offset += @intCast(width); + }, + } + } + self.pos = end; + } + + // --- control flow ------------------------------------------------------- + + fn opIf(self: *Parser, scope: *Node) Error!void { + const start = self.pos; + const end = start + try self.readPkgLength(); + try self.object(scope); // predicate + self.termList(end, scope); + if (self.peek() == op.else_op) { + self.pos += 1; + try self.opElse(scope); + } + } + + fn opElse(self: *Parser, scope: *Node) Error!void { + const start = self.pos; + const end = start + try self.readPkgLength(); + self.termList(end, scope); + } + + fn opWhile(self: *Parser, scope: *Node) Error!void { + const start = self.pos; + const end = start + try self.readPkgLength(); + try self.object(scope); // predicate + self.termList(end, scope); + } + + fn opLnot(self: *Parser, scope: *Node) Error!void { + // 0x92 followed by 0x93/94/95 is a compound comparison (two operands); + // otherwise it is a plain LNot of one operand. + const b = self.peek() orelse return error.Truncated; + switch (b) { + op.lnot.not_equal, op.lnot.less_equal, op.lnot.greater_equal => { + self.pos += 1; + try self.args(scope, 2); + }, + else => try self.object(scope), + } + } + + fn opMatch(self: *Parser, scope: *Node) Error!void { + try self.object(scope); // search package + try self.skip(1); // match opcode 1 + try self.object(scope); // operand 1 + try self.skip(1); // match opcode 2 + try self.object(scope); // operand 2 + try self.object(scope); // start index + } + + // --- extended opcodes (0x5B xx) ----------------------------------------- + + fn opExt(self: *Parser, scope: *Node) Error!void { + const e = try self.readByte(); + switch (e) { + op.ext.mutex => try self.opMutex(scope), + op.ext.event => try self.opEvent(scope), + op.ext.op_region => try self.opRegion(scope), + op.ext.data_region => try self.opDataRegion(scope), + op.ext.field => try self.opField(scope, 1, false), + op.ext.index_field => try self.opField(scope, 2, false), + op.ext.bank_field => try self.opField(scope, 2, true), + op.ext.device => try self.opScopeLike(scope, .device), + op.ext.thermal_zone => try self.opScopeLike(scope, .thermal_zone), + op.ext.processor => try self.opProcessor(scope), + op.ext.power_res => try self.opPowerRes(scope), + + op.ext.cond_ref_of => try self.args(scope, 2), // SuperName, Target + op.ext.create_field => try self.opCreateField(scope, 3), + op.ext.load_table => try self.args(scope, 6), + op.ext.load => try self.args(scope, 2), // NameString, Target + op.ext.stall, op.ext.sleep => try self.args(scope, 1), + op.ext.acquire => { + try self.object(scope); // mutex SuperName + try self.skip(2); // timeout WordData + }, + op.ext.signal, op.ext.reset, op.ext.release, op.ext.unload => try self.args(scope, 1), + op.ext.wait => try self.args(scope, 2), + op.ext.from_bcd, op.ext.to_bcd => try self.args(scope, 2), + op.ext.fatal => { + try self.skip(5); // Type(byte) + Code(dword) + try self.object(scope); // Arg TermArg + }, + op.ext.revision, op.ext.debug, op.ext.timer => {}, + + else => return error.Malformed, + } + } +}; + +fn isNameStart(b: u8) bool { + return (b >= op.name_char_start and b <= op.name_char_end) or + b == op.name_char_underscore or + b == op.root_char or + b == op.parent_prefix_char or + b == op.dual_name_prefix or + b == op.multi_name_prefix; +} diff --git a/src/device/device.zig b/src/device/device.zig new file mode 100644 index 0000000..9c3ba07 --- /dev/null +++ b/src/device/device.zig @@ -0,0 +1,204 @@ +//! The backend-agnostic device model. +//! +//! Discovery backends (ACPI today, device-tree later) translate their native +//! hardware description into this one shape, so the rest of the kernel walks a +//! plain `Device` tree without knowing which firmware described the machine — +//! the same discipline `root.zig`'s `MemoryKind` applies to memory and `arch` +//! applies to the CPU. +//! +//! This is deliberately minimal: enough to *describe* what was discovered (a +//! named node, its class, and its hardware resources) and where it sits in the +//! bus hierarchy. Driver matching, families, and probing are a later layer built +//! on top of this — nothing here presumes them. + +const std = @import("std"); + +/// The hardware primitives a discovery backend needs but can't express portably. +/// The kernel injects an implementation (the arch VMM + port I/O), so the device +/// layer touches hardware without importing `arch` — the same discipline that lets +/// it stay firmware-agnostic. `pioRead`/`pioWrite` take a width in bytes (1/2/4). +pub const Hal = struct { + mapMmio: *const fn (virt: u64, phys: u64, writable: bool) void, + pioRead: *const fn (width: u8, port: u16) u32, + pioWrite: *const fn (width: u8, port: u16, value: u32) void, +}; + +/// The kind of hardware resource a device occupies. +pub const ResourceKind = enum { + /// A memory-mapped I/O window: `start` is the physical base, `len` its size. + memory, + /// A legacy I/O-port range: `start` is the first port, `len` the count. + io_port, + /// An interrupt: `start` is the global system interrupt (GSI), `len` is 1. + irq, + /// A range of bus numbers owned by a bridge: `start`..`start+len`. + bus_range, +}; + +/// One hardware resource claimed by a device. +pub const Resource = struct { + kind: ResourceKind, + start: u64, + len: u64, +}; + +/// A coarse classification of a device, independent of the describing firmware. +/// Kept small on purpose; refine as real drivers arrive. +pub const DeviceClass = enum { + /// The synthetic root every discovered device hangs beneath. + root, + processor, + interrupt_controller, + timer, + /// A PCI(e) host bridge — the root of a PCI segment (owns an ECAM window). + pci_host_bridge, + /// A single PCI function. + pci_device, + /// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a + /// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`). + acpi_device, + unknown, +}; + +/// Firmware-independent identity. Each backend fills only the fields it knows; +/// the rest stay null. The generic layer never branches on *how* an id was +/// obtained, only on its value. +pub const Ids = struct { + /// The device's ACPI hardware ID (`_HID`), EISA-encoded into 4 bytes, when applicable. + acpi_hid: ?u32 = null, + /// PCI configuration-space identity, when this node is a PCI function. + pci_vendor: ?u16 = null, + pci_device: ?u16 = null, + /// PCI class/subclass/prog-if packed as 0xCCSSPP. + pci_class: ?u24 = null, + /// PCI bus/device/function packed as (bus << 8) | (dev << 3) | func — the key + /// the ACPI address (`_ADR`) merge uses to match a namespace device to this node. + pci_bdf: ?u16 = null, +}; + +/// Upper bound on resources tracked per device (6 PCI BARs + a couple of IRQs is +/// the busy case). Stored inline so a device is a single allocation. +pub const max_resources = 8; + +/// One node in the device tree. Nodes are individually heap-allocated and linked +/// intrusively (first-child / next-sibling), the classic device-tree layout — +/// no per-node dynamic arrays to manage. +pub const Device = struct { + name_buf: [24]u8 = undefined, + name_len: u8 = 0, + class: DeviceClass = .unknown, + ids: Ids = .{}, + /// Human-readable hardware id (e.g. "PNP0A03"), when known. Backed inline like + /// `name`; empty when unset. The generic layer stores/prints it without knowing + /// how a backend encoded it. + hid_buf: [8]u8 = undefined, + hid_len: u8 = 0, + resources: [max_resources]Resource = undefined, + resource_count: u8 = 0, + + parent: ?*Device = null, + first_child: ?*Device = null, + next_sibling: ?*Device = null, + + /// The device's short name (e.g. "cpu0", "pci0:00:1f.0"). Backed by an inline + /// buffer, so it stays valid for the life of the node with no extra allocation. + pub fn name(self: *const Device) []const u8 { + return self.name_buf[0..self.name_len]; + } + + fn setName(self: *Device, s: []const u8) void { + const n: u8 = @intCast(@min(s.len, self.name_buf.len)); + @memcpy(self.name_buf[0..n], s[0..n]); + self.name_len = n; + } + + /// The device's hardware id string, or empty if none is set. + pub fn hid(self: *const Device) []const u8 { + return self.hid_buf[0..self.hid_len]; + } + + pub fn setHid(self: *Device, s: []const u8) void { + const n: u8 = @intCast(@min(s.len, self.hid_buf.len)); + @memcpy(self.hid_buf[0..n], s[0..n]); + self.hid_len = n; + } + + /// Record a resource. Silently drops beyond `max_resources` — discovery logs + /// the truncation rather than failing the whole tree. + pub fn addResource(self: *Device, kind: ResourceKind, start: u64, len: u64) bool { + if (self.resource_count >= max_resources) return false; + self.resources[self.resource_count] = .{ .kind = kind, .start = start, .len = len }; + self.resource_count += 1; + return true; + } +}; + +/// Owns the discovered device tree and the allocator its nodes came from. +pub const DeviceTree = struct { + allocator: std.mem.Allocator, + root: *Device, + + /// Create a tree with just the synthetic root node. + pub fn init(allocator: std.mem.Allocator) !DeviceTree { + const root = try allocator.create(Device); + root.* = .{ .class = .root }; + root.setName("root"); + return .{ .allocator = allocator, .root = root }; + } + + /// Allocate a device and append it under `parent`, returning it so the caller + /// can attach resources/ids. Appended at the tail so a dump reads in the order + /// devices were discovered. + pub fn addChild( + self: *DeviceTree, + parent: *Device, + class: DeviceClass, + dev_name: []const u8, + ) !*Device { + const d = try self.allocator.create(Device); + d.* = .{ .class = class, .parent = parent }; + d.setName(dev_name); + if (parent.first_child == null) { + parent.first_child = d; + } else { + var cur = parent.first_child.?; + while (cur.next_sibling) |sib| cur = sib; + cur.next_sibling = d; + } + return d; + } + + /// Walk the tree depth-first, emitting an indented, human-readable listing. + /// `emit` is a raw byte sink (e.g. the serial `debugWrite`), so this stays + /// independent of the kernel console. + pub fn dump(self: *const DeviceTree, emit: *const fn ([]const u8) void) void { + dumpNode(self.root, 0, emit); + } +}; + +fn dumpNode(dev: *const Device, depth: usize, emit: *const fn ([]const u8) void) void { + const indent = @min(depth * 2, 40); + + var buf: [200]u8 = undefined; + @memset(buf[0..indent], ' '); + const body = if (dev.hid_len != 0) + std.fmt.bufPrint(buf[indent..], "{s} [{s}] hid={s}\n", .{ dev.name(), @tagName(dev.class), dev.hid() }) catch return + else + std.fmt.bufPrint(buf[indent..], "{s} [{s}]\n", .{ dev.name(), @tagName(dev.class) }) catch return; + emit(buf[0 .. indent + body.len]); + + for (dev.resources[0..dev.resource_count]) |r| { + var rbuf: [200]u8 = undefined; + const pad = @min(indent + 2, 42); + @memset(rbuf[0..pad], ' '); + const rline = std.fmt.bufPrint( + rbuf[pad..], + "- {s} 0x{x} len 0x{x}\n", + .{ @tagName(r.kind), r.start, r.len }, + ) catch continue; + emit(rbuf[0 .. pad + rline.len]); + } + + var child = dev.first_child; + while (child) |c| : (child = c.next_sibling) dumpNode(c, depth + 1, emit); +} diff --git a/src/device/devicetree.zig b/src/device/devicetree.zig new file mode 100644 index 0000000..a1cfccb --- /dev/null +++ b/src/device/devicetree.zig @@ -0,0 +1,16 @@ +//! Device-tree (Flattened Device Tree / FDT) discovery backend — stub. +//! +//! This is the second backend the platform facade dispatches to, for machines +//! that describe hardware with a device-tree blob instead of ACPI (typically +//! ARM). It is intentionally unimplemented: the bootloader has no DTB handoff +//! field yet, so this path is currently unreachable. It exists so the facade +//! already routes to a backend rather than hard-coding ACPI — wiring the FDT +//! parser in later is a local change here, not an architectural one. + +const device = @import("device.zig"); + +/// Populate `dt` from a device-tree blob. Not implemented yet. +pub fn discover(dt: *device.DeviceTree) !void { + _ = dt; + return error.Unsupported; +} diff --git a/src/device/platform.zig b/src/device/platform.zig new file mode 100644 index 0000000..d9765f4 --- /dev/null +++ b/src/device/platform.zig @@ -0,0 +1,67 @@ +//! The firmware-agnostic discovery facade. +//! +//! The kernel calls `platform.discover()` and gets back a generic `DeviceTree` +//! without ever naming ACPI or device-tree — the same way it imports `arch` +//! without naming x86_64. Which backend runs is decided *at runtime* from what +//! the bootloader handed us (an ACPI RSDP today, a device-tree blob later), +//! because a single image — a future ARM kernel especially — may boot under +//! either firmware. That's a deliberate divergence from `arch`, which is a +//! compile-time choice. + +const std = @import("std"); +const danos = @import("danos"); +const device = @import("device.zig"); +const acpi = @import("acpi.zig"); +const power = @import("power.zig"); +const devicetree = @import("devicetree.zig"); + +pub const DeviceTree = device.DeviceTree; +pub const Device = device.Device; +pub const DeviceClass = device.DeviceClass; +pub const Hal = device.Hal; +pub const PowerInfo = acpi.PowerInfo; +pub const AmlStats = acpi.AmlStats; + +/// The register map + sleep types discovery extracted, for logging/diagnostics. +pub fn powerInfo() PowerInfo { + return acpi.power_info; +} + +/// AML parse integrity/diagnostics (namespace node count, bytes consumed). +pub fn amlStats() AmlStats { + return acpi.aml_stats; +} + +/// Enumerate hardware into a fresh device tree. `hal` supplies the hardware +/// primitives the backend needs (MMIO mapping for PCIe config space, port I/O for +/// ACPI registers); pass the arch implementation. Errors leave nothing to clean up +/// beyond the tree's own allocations. +pub fn discover( + boot_info: *const danos.BootInfo, + allocator: std.mem.Allocator, + hal: Hal, +) !DeviceTree { + var dt = try DeviceTree.init(allocator); + + if (boot_info.acpi_rsdp != 0) { + try acpi.discover(boot_info.acpi_rsdp, &dt, hal); + } else { + // No ACPI RSDP. A device-tree boot would parse its blob here; today that + // path is a stub, so this reports the machine described itself no way we + // understand yet. + try devicetree.discover(&dt); + } + + return dt; +} + +/// Restart the machine. Never returns on success; returns only if no reset method +/// worked (extremely unlikely). Backend-agnostic entry the kernel calls. +pub fn reboot(hal: Hal) void { + power.reboot(hal); +} + +/// Power the machine off (ACPI S5). Never returns on success. +pub fn shutdown(hal: Hal) void { + power.shutdown(hal); +} diff --git a/src/device/power.zig b/src/device/power.zig new file mode 100644 index 0000000..53574fb --- /dev/null +++ b/src/device/power.zig @@ -0,0 +1,106 @@ +//! Machine power control: enter ACPI mode, reboot, and power off (ACPI S5). +//! +//! Built entirely on the register map `acpi` extracted from the FADT plus the +//! sleep-state (`_Sx`) types the AML submodule pulled from the DSDT, driven through the +//! injected `Hal` (port I/O and MMIO). Nothing here is x86-specific beyond the +//! well-known legacy reset fallbacks, which are guarded behind the ACPI methods. +//! +//! S3 (suspend-to-RAM) is stubbed: it needs a wake trampoline and device +//! re-initialisation, a milestone of its own. + +const acpi = @import("acpi.zig"); +const device = @import("device.zig"); +const Hal = device.Hal; + +const slp_en: u32 = 1 << 13; // SLP_EN: writing 1 triggers the sleep transition +const sci_en: u32 = 1 << 0; // SCI_EN in PM1 control: set once ACPI mode is active + +/// Switch the platform into ACPI mode if it isn't already, so the PM1 control +/// register is live. A no-op when the firmware exposes no SMI command port (ACPI +/// already enabled, as under QEMU/OVMF) — we still verify SCI_EN first. +pub fn enable(hal: Hal) void { + const pi = acpi.power_info; + if (!pi.pm1a_cnt.present()) return; + if (readReg(hal, pi.pm1a_cnt) & sci_en != 0) return; // already in ACPI mode + if (pi.smi_cmd == 0 or pi.acpi_enable == 0) return; // no way to switch; assume fine + + hal.pioWrite(1, pi.smi_cmd, pi.acpi_enable); + var spins: usize = 0; + while (readReg(hal, pi.pm1a_cnt) & sci_en == 0 and spins < 1_000_000) : (spins += 1) {} +} + +/// Restart the machine. Tries the ACPI reset register first, then the two legacy +/// fallbacks. Returns only if every method failed (very unlikely). +pub fn reboot(hal: Hal) void { + const pi = acpi.power_info; + + // 1. The FADT reset register, when the firmware advertises support. + if (pi.reset_supported and pi.reset.present()) { + writeReg(hal, pi.reset, pi.reset_value); + delay(); + } + // 2. The PCI reset-control register at port 0xCF9 (RST_CPU | SYS_RST). + hal.pioWrite(1, 0xCF9, 0x0E); + hal.pioWrite(1, 0xCF9, 0x06); + delay(); + // 3. Pulse the 8042 keyboard controller's reset line. + hal.pioWrite(1, 0x64, 0xFE); + delay(); +} + +/// Power the machine off via ACPI S5. Requires the soft-off (`_S5`) sleep type; if +/// it wasn't found in the AML, there is nothing safe to do and this returns. +pub fn shutdown(hal: Hal) void { + enable(hal); + const pi = acpi.power_info; + const s5 = pi.s5 orelse return; + + if (pi.pm1a_cnt.present()) { + writeReg(hal, pi.pm1a_cnt, sleepValue(s5.slp_typ_a)); + } + if (pi.pm1b_cnt.present()) { + writeReg(hal, pi.pm1b_cnt, sleepValue(s5.slp_typ_b)); + } + delay(); +} + +/// S3 suspend-to-RAM — not implemented (needs a wake path + device re-init). +pub fn sleepS3(hal: Hal) error{Unsupported}!void { + _ = hal; + return error.Unsupported; +} + +/// The PM1 control write that requests sleep type `slp_typ`: SLP_TYP in bits +/// [12:10], SLP_EN in bit 13. +fn sleepValue(slp_typ: u8) u32 { + return (@as(u32, slp_typ & 0x7) << 10) | slp_en; +} + +fn readReg(hal: Hal, reg: acpi.RegAccess) u32 { + if (reg.mmio) { + hal.mapMmio(reg.address, reg.address, true); + const p: *align(1) volatile u32 = @ptrFromInt(reg.address); + return p.*; + } + return hal.pioRead(reg.width, @intCast(reg.address)); +} + +fn writeReg(hal: Hal, reg: acpi.RegAccess, value: u32) void { + if (reg.mmio) { + hal.mapMmio(reg.address, reg.address, true); + const p: *align(1) volatile u32 = @ptrFromInt(reg.address); + p.* = value; + } else { + hal.pioWrite(reg.width, @intCast(reg.address), value); + } +} + +/// A short busy-wait so a reset/power-off takes effect before we fall through to +/// the next method. The empty asm is an arch-neutral barrier that keeps the loop +/// from being optimised away. +fn delay() void { + var i: usize = 0; + while (i < 50_000_000) : (i += 1) { + asm volatile ("" ::: .{ .memory = true }); + } +}