Files
danos/system/devices/aml/aml.zig
Daniel Samson 452080e997 Add runtime.time, drop demo drivers, harden TSC timekeeping
Time is a kernel concern in danos: the kernel owns the scheduling timer and
already exposes monotonic time via the clock/sleep/timer_bind syscalls, so a
userspace time service would be a redundant, slower path. This adds the generic
runtime.time module over those syscalls, retires the two demonstration drivers,
reorganizes the milestone docs, and makes the monotonic clock correct on Intel,
AMD, and inside any VM.

runtime.time (library/runtime/time.zig)
- Instant/Duration interface: now, sleep, spin, after, monotonicNanos, available
- a thin layer over system.clock/sleep/timerOnce; unit-tested arithmetic

Remove the demo drivers hpet and bus (a teaching example belongs in the docs,
not shipped in the tree)
- system/drivers/ now holds only real drivers: pci-bus, ps2-bus, usb-xhci-bus
- device-manager end-to-end test repointed to pci-bus (asserts on kernel state:
  the process table and the device tree, not a racy serial marker)
- device_register containment moved to a new in-kernel `containment` test
- the driver-model worked example moved inline into docs/drivers.md

Reorganize milestone docs into topic docs
- m17-m18 / m19-m20 / m21 plans dissolved into process-lifecycle, device-manager,
  discovery, and acpi docs; new docs/power.md and docs/timers.md; ~20 citations
  repointed; plan docs deleted

TSC reliability (apic.zig, smp.zig, cpu.zig, kernel.zig)
- check the invariant-TSC bit (CPUID 0x80000007 EDX[8]) on Intel and AMD
- cross-core "warp" check at SMP bring-up, pairwise BSP<->AP as each core comes up
- fall back to the HPET clocksource when the TSC is not invariant (a bare VM) or
  not synchronized (a warp), switched continuously so time never jumps
- boot log reports the outcome; new tsc-sync test exercises the TSC + warp path

Verified: zig build; zig build test; 60/60 QEMU cases (incl. new containment and
tsc-sync).
2026-07-13 11:56:43 +01:00

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/discovery.md) 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);
}