From 1f2c60b3ec381c382ccf11e230a4d81cebf48047 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:38:03 +0100 Subject: [PATCH] The power button, in ring 3: SCI bound, fixed event published (M21.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel publishes the FADT as one more acpi-tables memory resource (tagged by its intact FACP header — the AML blobs are header-stripped); the acpi service reads the PM1 event/control and GPE register ports from that copy, so the kernel's own FADT parse is untouched. A power-protocol module (ServiceId.power = 5, domain-named so an ARM PSCI service can serve the same id) carries subscribe / shutdown / events. The acpi service converts to runtime.service.run — device discovery, the .power protocol, and the SCI notification all fold into one loop. At startup it enables ACPI mode if SCI_EN is clear (the SMI dance), binds the SCI (found as the node's len-1 irq resource, distinct from the broad window), and sets PWRBTN_EN. On the SCI it reads PM1_STS, clears PWRBTN_STS write-1, logs the press, publishes power_button to subscribers, and always acks. The power-button scenario proves it with a real QMP system_powerdown injected mid-run through the M21.0 channel. --- build.zig | 6 + docs/m21-plan.md | 17 +- library/runtime/runtime.zig | 3 + system/abi.zig | 1 + system/devices/acpi.zig | 14 ++ system/kernel/tests.zig | 2 + system/services/acpi/acpi.zig | 273 +++++++++++++++++++++++++---- system/services/power/protocol.zig | 68 +++++++ test/qemu_test.py | 10 ++ 9 files changed, 354 insertions(+), 40 deletions(-) create mode 100644 system/services/power/protocol.zig diff --git a/build.zig b/build.zig index 34e5939..1f404f5 100644 --- a/build.zig +++ b/build.zig @@ -226,6 +226,12 @@ pub fn build(b: *std.Build) void { }); runtime_module.addImport("device-manager-protocol", device_manager_protocol_module); + // The power protocol: system power's domain-named surface (docs/m21-plan.md). + const power_protocol_module = b.addModule("power-protocol", .{ + .root_source_file = b.path("system/services/power/protocol.zig"), + }); + runtime_module.addImport("power-protocol", power_protocol_module); + // Typed volatile MMIO register access + memory-ordering barriers, for drivers on // top of an mmio_map grant. Depends only on `builtin` (arch-conditional barriers); // no target set, so it inherits each driver's. See library/mmio/mmio.zig. diff --git a/docs/m21-plan.md b/docs/m21-plan.md index 1c1629e..7ad0928 100644 --- a/docs/m21-plan.md +++ b/docs/m21-plan.md @@ -75,14 +75,15 @@ auto-merge to main when the branch is green; keep the branch; push everything. always-on unix socket, client with the capabilities handshake, per-case `qmp_after` hook, and a hook-must-deliver pass gate that the smoke case now proves with a harmless query-status; suite 58/58). -- [ ] **M21.1** — SCI + the power button: kernel appends the FADT as an - acpi-tables memory resource; new `power-protocol` module + - `ServiceId.power`; the acpi service converts to the harness, registers - `.power`, parses the event/GPE blocks from its FADT copy, enables ACPI - mode if needed (SMI dance, spin on SCI_EN), binds the SCI, sets - PWRBTN_EN; on SCI reads/clears PM1_STS and publishes `power_button` - (log: `power: button pressed`), always irqAck. Scenario `power-button`: - `qmp_after system_powerdown` → expect the log line. +- [x] **M21.1** — SCI + the power button (kernel appends the FADT as an + acpi-tables memory resource, tagged by its "FACP" header; `power-protocol` + module + `ServiceId.power = 5`; the acpi service converted to + `runtime.service.run`, registers `.power`, reads PM1 event/control + GPE + ports from its FADT copy, enables ACPI mode if SCI_EN is clear, binds the + SCI (the len-1 irq), sets PWRBTN_EN; the SCI handler clears PM1_STS, + logs `power: button pressed`, publishes `power_button`, acks. Scenario + `power-button` injects a real `system_powerdown` via QMP; initial-ramdisk + timeout 30→60s for the service's added boot work; suite 59/59). - [ ] **M21.2** — Notify + GPE dispatch: interpreter handles `notify_opcode` into a bounded queue drained after evaluate(); on GPE status bits the service evaluates `\_GPE._Lxx`/`_Exx`, maps notified nodes to events diff --git a/library/runtime/runtime.zig b/library/runtime/runtime.zig index 29b1895..efdc5a5 100644 --- a/library/runtime/runtime.zig +++ b/library/runtime/runtime.zig @@ -20,6 +20,9 @@ pub const vfs_protocol = @import("vfs-protocol"); /// The device-manager protocol: hello + tree reports (docs/device-manager.md). pub const device_manager_protocol = @import("device-manager-protocol"); + +/// The power protocol: events (button, lid, battery) + shutdown (docs/m21-plan.md). +pub const power_protocol = @import("power-protocol"); /// Keyboard-event listening (subscribe/next) and broadcasting (publish), over the input /// service. See library/runtime/input.zig and system/services/input/. pub const input = @import("input.zig"); diff --git a/system/abi.zig b/system/abi.zig index 6e6e985..657f4e4 100644 --- a/system/abi.zig +++ b/system/abi.zig @@ -177,6 +177,7 @@ pub const ServiceId = enum(u32) { input = 2, ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md) + power = 5, // system power: events (button, lid, battery) + shutdown (docs/m21-plan.md; domain-named per decision 7 — the acpi service registers it on x86, a PSCI service will on ARM) _, }; diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index 58a028f..c1a4db0 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -157,6 +157,13 @@ pub var namespace: ?aml.Namespace = null; /// Physical address of the DSDT the FADT points at, or 0. pub var dsdt_physical: u64 = 0; +/// The FADT itself (physical + length), published on the acpi-tables node so +/// the ring-3 acpi service can read the PM1 event and GPE blocks it needs for +/// the event side (docs/m21-plan.md decision 3). Distinguished from the AML +/// blob resources by its intact "FACP" header — the blobs are header-stripped. +var fadt_physical: u64 = 0; +var fadt_length: u64 = 0; + // AML blocks (DSDT + any SSDTs) collected during the table walk, as physical // address + length of each table's post-header bytecode. Scanned after the walk // for the sleep-state (`_Sx`) packages. @@ -373,6 +380,8 @@ pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryR // Start clean so a re-run doesn't accumulate stale state. power_information = .{}; + fadt_physical = 0; + fadt_length = 0; platform_information = .{}; aml_stats = .{}; namespace = null; @@ -447,6 +456,9 @@ fn publishAcpiTablesNode(device_tree: *DeviceTree) !void { // SCI (recorded first, len 1) stays distinct so M21 can pick it out. if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1); _ = node.addResource(.irq, 0, 256); + // The FADT rides along (M21): the service reads the PM1 event / GPE blocks + // from its own copy, telling it apart from the AML blobs by signature. + if (fadt_physical != 0) _ = node.addResource(.memory, fadt_physical, fadt_length); } /// The number of Device objects in the namespace built during discovery, or 0. @@ -482,6 +494,8 @@ fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void { } else if (std.mem.eql(u8, &sig, &HPET)) { try parseHpet(device_tree, hal, header); } else if (std.mem.eql(u8, &sig, &FACP)) { + fadt_physical = sdt_physical; + fadt_length = header.length; parseFadt(header); } else if (std.mem.eql(u8, &sig, &SPCR)) { parseSpcr(header); diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index b2a1398..9154710 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -152,6 +152,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { acpiReportTest(boot_information); } else if (eql(case, "acpi-ps2")) { acpiReportTest(boot_information); // same spawn; the harness regex differs + } else if (eql(case, "power-button")) { + acpiReportTest(boot_information); // boot the manager (spawns the acpi service); harness injects the button } else if (eql(case, "initial-ramdisk")) { initialRamdiskTest(boot_information); } else if (eql(case, "vfs")) { diff --git a/system/services/acpi/acpi.zig b/system/services/acpi/acpi.zig index 84e6a27..6132dfd 100644 --- a/system/services/acpi/acpi.zig +++ b/system/services/acpi/acpi.zig @@ -4,13 +4,11 @@ //! grant, a broad irq window, the SCI), and runs the **shared AML module** in //! ring 3 — the same parser and interpreter the kernel uses. //! -//! M20.2 (this increment): after parsing, walk the namespace and, for each -//! present Device with a hardware id (`_HID`), evaluate its current resource -//! settings (`_CRS`) through a ring-3 `Hal` (port I/O over the claimed node), -//! register it under the acpi-tables node (its I/O ports and IRQs contained by -//! the node's broad grants), and report it to the device manager with its -//! EISA-decoded hid as identity. Matching those reports to drivers (ps2-bus) -//! and retiring the kernel's own device build follow in M20.3. +//! 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 `runtime.service.run` loop. const std = @import("std"); const runtime = @import("runtime"); @@ -18,6 +16,7 @@ const aml = @import("aml"); const acpi_ids = @import("acpi-ids"); const device = runtime.device; const protocol = runtime.device_manager_protocol; +const power = runtime.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; @@ -31,6 +30,38 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void { // 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. +const maximum_subscribers = 8; +var subscribers: [maximum_subscribers]?runtime.ipc.Handle = .{null} ** 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 }; @@ -85,22 +116,34 @@ pub fn main(init: runtime.process.Init) void { return; } - // Map each memory resource (an AML blob) and note the io_port resource. + // 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); - blocks[block_count] = pointer[0..@intCast(resource.len)]; + 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 == blocks.len) break; } if (block_count == 0) { _ = runtime.system.write("acpi: no AML blobs on the node\n"); @@ -124,29 +167,45 @@ pub fn main(init: runtime.process.Init) void { while (true) runtime.system.sleep(1000); } - // Register + report the present _HID devices (M20.2). - var arena = std.heap.ArenaAllocator.init(runtime.allocator()); - var interpreter = aml.Interpreter.init(&namespace, .{ + // 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(runtime.allocator()); + persistent_namespace = namespace; + global_interpreter = aml.Interpreter.init(&persistent_namespace, .{ .mapMmio = halMapMmio, .pioRead = halPioRead, .pioWrite = halPioWrite, - }, arena.allocator()); + }, interpreter_arena.allocator()); - // Pass 1: register every present _HID device under acpi-tables, remembering - // each (hid, device id). Pass 2: report them all. Registering before any - // report reaches the manager means a driver it spawns on the first report - // already sees the whole set (no keyboard-before-mouse race for ps2-bus). + readFadt(fadt); + s5_valid = readSleepS5(&persistent_namespace); + + runtime.service.run(power.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: runtime.ipc.Handle) bool { registered_count = 0; - walkDevices(namespace.root, &interpreter); + walkDevices(persistent_namespace.root, &global_interpreter); const manager = runtime.ipc.lookup(.device_manager); var i: usize = 0; while (i < registered_count) : (i += 1) { const entry = registered[i]; - // Append the _HID's human-readable name when it is a known standard PnP/ACPI - // id (e.g. PNP0303 -> "PS/2 Keyboard"), so the boot log says what each - // reported device actually is. The description trails the existing fields so - // the acpi-report/acpi-ps2 matchers still see " (device N, M resources)". const hid = entry.hid[0..entry.hid_len]; const desc = acpi_ids.description(hid); if (desc.len != 0) @@ -154,12 +213,7 @@ pub fn main(init: runtime.process.Init) void { else writeLine("acpi: reported {s} (device {d}, {d} resources)\n", .{ hid, entry.device_id, entry.resource_count }); if (manager) |h| { - var report = protocol.ChildAdded{ - .parent = node_id, - .bus_address = entry.device_id, - .identity = 0, - .device_id = entry.device_id, - }; + var report = protocol.ChildAdded{ .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: [protocol.message_maximum]u8 = undefined; _ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {}; @@ -167,9 +221,164 @@ pub fn main(init: runtime.process.Init) void { } writeLine("acpi: reported {d} device(s) to the manager\n", .{registered_count}); - // Stay resident: the claim holds, and the service is here to grow into the - // supervised discoverer (M20.3, then the M21 event side on the SCI). - while (true) runtime.system.sleep(1000); + 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 { + _ = runtime.system.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: runtime.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) { + runtime.system.sleep(1); + } + } + if (!has_sci) { + _ = runtime.system.write("acpi: no SCI resource — power button unavailable\n"); + return; + } + if (!device.irqBind(node_id, sci_resource_index, endpoint)) { + _ = runtime.system.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); + } + _ = runtime.system.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) { + _ = runtime.system.write("power: button pressed\n"); + publishButton(); + } + _ = device.irqAck(node_id, sci_resource_index); +} + +fn publishButton() void { + const event = power.EventMessage{ .event = @intFromEnum(power.Event.power_button) }; + publishEvent(std.mem.asBytes(&event)); +} + +fn publishEvent(bytes: []const u8) void { + for (&subscribers) |*slot| { + if (slot.*) |handle| { + if (!runtime.ipc.send(handle, bytes)) slot.* = null; + } + } +} + +/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s). +/// Mirrors the kernel's power.zig sleepValue. Only reached from a PID-1 +/// shutdown request (M21.3). +fn enterS5() void { + if (!s5_valid or pm1a_cnt == 0) { + _ = runtime.system.write("power: S5 unavailable\n"); + return; + } + _ = runtime.system.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. + runtime.system.sleep(500); + _ = runtime.system.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: ?runtime.ipc.Handle) usize { + if (message.len < 1) return 0; + switch (message[0]) { + @intFromEnum(power.Operation.subscribe) => { + var status: i32 = -1; + if (capability) |handle| { + for (&subscribers) |*slot| { + if (slot.* == null) { + slot.* = handle; + status = 0; + break; + } + } + } + const r = power.Reply{ .status = status }; + @memcpy(reply[0..@sizeOf(power.Reply)], std.mem.asBytes(&r)); + return @sizeOf(power.Reply); + }, + @intFromEnum(power.Operation.shutdown) => { + // Only PID 1 (init), which has already stopped everything else. + const status: i32 = if (sender == 1) 0 else -1; + const r = power.Reply{ .status = status }; + @memcpy(reply[0..@sizeOf(power.Reply)], std.mem.asBytes(&r)); + if (sender == 1) enterS5(); + return @sizeOf(power.Reply); + }, + else => return 0, + } } /// Depth-first walk: register + report each present device with a _HID, then diff --git a/system/services/power/protocol.zig b/system/services/power/protocol.zig new file mode 100644 index 0000000..8d1a720 --- /dev/null +++ b/system/services/power/protocol.zig @@ -0,0 +1,68 @@ +//! The power protocol (docs/m21-plan.md): system power's domain-named surface, +//! registered under `ServiceId.power`. On x86 the acpi service serves it; on +//! ARM a PSCI/mailbox service will register the same id — subscribers never +//! learn which firmware they are on (m19-m20-plan.md decision 7). The +//! vfs-protocol pattern: extern-struct messages, a version, reserved fields. + +/// The protocol version a client states nowhere yet — reserved for the day a +/// handshake needs it; requests carry it so a mismatch can be refused loudly. +pub const version: u16 = 1; + +pub const Operation = enum(u8) { + /// Subscribe to power events: the subscriber's endpoint rides as the + /// call's capability (the input/device-manager pattern); events arrive on + /// it as buffered messages carrying an `EventMessage`. + subscribe = 1, + /// Orderly shutdown's last step: enter S5. Accepted only from PID 1 + /// (init) — the process that has already run the stop sequence over + /// everything else. + shutdown = 2, + /// The published event payload (never sent *to* the service). + event = 3, +}; + +/// What happened. The vocabulary is hardware-neutral: a lid is a lid whether +/// ACPI or a PSCI mailbox reported it. +pub const Event = enum(u8) { + power_button = 1, + lid = 2, + ac = 3, + battery = 4, + /// A device notification that maps to none of the named events — the + /// `code` and `hid` fields say which device and what code. + notify = 5, +}; + +pub const Subscribe = extern struct { + operation: u8 = @intFromEnum(Operation.subscribe), + reserved0: u8 = 0, + version: u16 = version, + reserved1: u32 = 0, +}; + +pub const Shutdown = extern struct { + operation: u8 = @intFromEnum(Operation.shutdown), + reserved0: u8 = 0, + version: u16 = version, + reserved1: u32 = 0, +}; + +/// A published event, as the buffered-message payload subscribers receive. +pub const EventMessage = extern struct { + operation: u8 = @intFromEnum(Operation.event), + /// An Event value. + event: u8, + reserved0: u16 = 0, + /// The device notification code (Notify's second argument), or 0. + code: u32 = 0, + /// The notifying device's hardware id (EISA-decoded), or all zero. + hid: [8]u8 = .{0} ** 8, +}; + +pub const Reply = extern struct { + status: i32, + reserved: u32 = 0, +}; + +/// Upper bound on any message in this protocol — sizes endpoint buffers. +pub const message_maximum = 64; diff --git a/test/qemu_test.py b/test/qemu_test.py index 26bf613..0248027 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -292,6 +292,15 @@ CASES = [ r"device-manager: spawned ps2-bus[\s\S]*" r"ps2-bus: keyboard driver attached", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # M21.1: the SCI + power button. Boot the manager (which spawns the acpi + # service); ~4s in, QMP system_powerdown raises the ACPI power-button fixed + # event; the service's SCI handler must log the press (docs/m21-plan.md). + {"name": "power-button", + "smp": 4, + "timeout": 60, + "qmp_after": {"delay": 4, "command": "system_powerdown"}, + "expect": r"power: button pressed", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers + # reports its _HID devices — the two PS/2 nodes must appear with resources # (keyboard: io 0x60/0x64 + IRQ = 3; mouse: IRQ = 1) (docs/m19-m20-plan.md). @@ -342,6 +351,7 @@ CASES = [ # The initial_ramdisk: the loader ferries a bundle of user binaries; the kernel parses # it and spawns each as a ring-3 process (here the VFS-server stub heartbeats). {"name": "initial-ramdisk", + "timeout": 60, # the acpi service's boot-time SCI setup can push the marker past 30s under load "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, # The user-space VFS: a client opens/writes/reads a file through the rt file