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