diff --git a/issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md b/issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md index 745b4b3e90b..fb94e24b5b3 100644 --- a/issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md +++ b/issues/the-acpi-server-talks-to-the-embedded-controller-without-the-global-lock.md @@ -9,9 +9,15 @@ opened: 2026-10-04 A machine's AML names whether its firmware also reaches the embedded controller from SMM: the controller device's `_GLK` returns 1, and then every transaction is taken under the FACS global lock (ACPI 6.5 §6.5.7, §5.2.10.1). -`/system/bin/acpiserver` interprets no AML, so it cannot evaluate `_GLK`, and -it takes no lock: on such a machine a query of the server's and one of the -firmware's can interleave on the same two ports. +`/system/bin/acpiserver` evaluates `_GLK` only to leave the controller's +space unattached to AML, and so read no battery, wherever it is not 0 +(`attach`, `userland/acpiserver/src/main.rs`), and takes no lock around its +own queries: on such a machine a query of the server's and one of the +firmware's can interleave on the same two ports. The lock itself is there to +take: the interpreter's host takes it for a Lock field or an Acquire of +`\_GL`, waiting out the firmware's hold on its `GBL_STS` +(`userland/acpiserver/src/host.rs`); nothing takes it around a controller +transaction. On the T14 no table names a `_GLK` at all: a byte search of its DSDT and every SSDT for the name finds none (the tables captured before its wipe, read diff --git a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md index 72fced2b417..923a27c4d98 100644 --- a/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md +++ b/issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md @@ -12,7 +12,11 @@ every CPU every 2.2 s The ACPI server loads a machine's DSDT and SSDTs, evaluates `\_S5` and hands the kernel its sleep type, which the kernel powers the machine off with and -has from nowhere else; it evaluates nothing else of their AML yet. What else +has from nowhere else. Beyond that it evaluates only the devices it serves: +the walk's `_HID`, `_STA`, `_CRS` and `_GPE`, the embedded controller's +`_GLK` and `_REG`, its query methods where the power button is a control +method device, and the batteries' and AC adapters' `_STA`, `_BIX` or +`_BIF`, `_BST` and `_PSR`. What else ToyOS takes from them it takes another way: the loader asks UEFI for the root bridges' windows `_CRS` would name (`bootloader/src/rootbridge.rs`). Nothing reads `_CST`, which names a CPU's C-states. @@ -263,17 +267,35 @@ otherwise pay to find again: What the server's load of the tables, on the T14 and through the kernel's mediated access, leaves open: -- **The server waits for no release of the Global Lock** (the orchestrator's - ruling, not the owner's). A take that finds the firmware holding the lock - leaves it the request, as ACPI 6.5 §5.2.10.1 has it, and is then denied by - name and counted in the server's ledger; the access under it is not made, - and the table or method that asked is refused. The wait that section - describes, for the SCI the firmware raises with `GBL_STS`, is not in the - tree: no tier reached it, and the T14's load took the lock 241 times and - found the firmware holding it in none. Owner: this stage. **Exit**: a - machine's log carries the denial, `the Global Lock: the firmware holds it`, - which the `acpi_tables_loaded` row reds on as on every refusal; the wait - comes back with the test that reaches its port sequence. +- **The controller's `_REG(3, 1)` runs on a namespace no `_INI` has run in.** + The server tells the controller its space is there as it attaches it to + AML (`attach`, `userland/acpiserver/src/main.rs`), before the battery's + read, and runs no `_INI`: the T14's controller `_INI` calls the firmware + through `SMI_CMD`, which the host does not write. The scouts' dry run read that + the T14's `_REG` takes another branch before its `_INI` than after it, + reading and writing controller offset 0x03 where after it touches the + controller not at all; Linux runs it after. Owner: this stage. **Exit**: + the slice that runs the `_STA` and `_INI` walk runs it before `_REG`. + +- **A take of the Global Lock that the AML sets no bound on ends the server + where the firmware holds the lock past 1 s** (`RELEASE`, + `userland/acpiserver/src/host.rs`). The bound is this server's guess, not + a measurement. The firmware is meant to hold the lock for the run of one + SMI handler, and no hold has been seen. On the T14 the load takes the + lock 241 times, the namespace's `_HID` walk, the controller's `_GLK` and + `_REG` and the battery's first read together 3 times, and the battery's + and adapter's methods alone none; the firmware held it for none of them. + Linux's `ff_gbl_lock` counter there read 0 at 2 minutes of uptime, and a reading + after a longer session is still owed. A Lock field and an `Acquire(\_GL, + 0xFFFF)` take with no bound. An Acquire with a TimeoutValue waits that + long, at most what the evaluation has left of its 10 s, and the + TimeoutValue is charged to it. Owner: this + stage. **Exit**: a hold measured on the T14, either by a contended take + in the count the server logs, which covers the load and the power + sources' census alone, or by `ff_gbl_lock` after a long Linux session. + That measurement sets the bound, or replaces it with no bound if the + firmware never holds the lock. + - **A press during the load waits for it, and a power-off asked during it is refused.** The server arms the power button and then loads the tables before it serves an SCI, so a press in that time @@ -343,7 +365,8 @@ mediated access, leaves open: What the firmware call, which the kernel makes for the server where its AML stores a byte to `SMI_CMD`, leaves open. No AML the server runs makes one -now: its host asks the kernel for three port writes and denies every other +now: its host asks the kernel for three port writes, and for the embedded +controller's transactions where AML reaches its space, and denies every other write by name (`FORWARDED` in `userland/acpiserver/src/host.rs`), the index registers 0x72 and 0xCD6, a byte each, and the POST port's word. Those are what an AMD laptop's AML wrote, measured outside the tree on its diff --git a/tests/common/power.rs b/tests/common/power.rs index 07baa051bf8..83c7ada9354 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -760,3 +760,132 @@ pub fn acpi_power_button(test_config: &Path) -> Result<(), String> { eprintln!(" [power] the press: {ACPI_PRESSED}"); Ok(()) } + +/// The PM1a event block QEMU's q35 firmware sets, whose status +/// [`ssdt_acquiring_the_global_lock`] reads: the table is built before the +/// boot, which then says the block it found. +const Q35_PM1A_EVENTS: u16 = 0x600; + +/// The Acquire's TimeoutValue, in ms; the wait for the press before it has +/// the rest of the 10 s one evaluation may spend asleep, past which the +/// load refuses the table. +const ACQUIRE_MS: u16 = 2000; + +/// What the server says of the table's Notify, which runs only where the +/// Acquire before it timed out. +const ACQUIRE_TIMED_OUT: &str = "Notify(\\_SB_.GLW_, 0x80) for the first time"; + +/// What it says of the load's takes: one, and it found the firmware holding +/// the lock. +const ONE_TAKE_CONTENDED: &str = "; took the Global Lock 1 times, 1 of them from the firmware;"; + +/// An SSDT (ACPI 6.5 §5.2.11.2) whose definition block declares the device +/// `\_SB.GLW` and, as it loads, waits for `PWRBTN_STS` (Table 4.13) in the +/// PM1a event block at `pm1`, then Acquires `\_GL` within `ms`, giving it +/// back where that took it and Notifying `\_SB.GLW` with 0x80 where it timed +/// out: +/// +/// ```text +/// Device (\_SB.GLW) {} +/// OperationRegion (PM1S, SystemIO, pm1, 2) +/// Field (PM1S, WordAcc, NoLock, Preserve) { Offset (1), PBST, 1 } +/// While (!PBST) { Sleep (1) } +/// If (Acquire (\_GL, ms)) { Notify (\_SB.GLW, 0x80) } Else { Release (\_GL) } +/// ``` +fn ssdt_acquiring_the_global_lock(pm1: u16, ms: u16) -> Vec { + const GL: &[u8] = b"\\_GL_"; + // `\` and a DualNamePrefix (§20.2.2). + const GLW: &[u8] = b"\\\x2E_SB_GLW_"; + const PM1S: &[u8] = b"PM1S"; + const PBST: &[u8] = b"PBST"; + // A PkgLength of one byte (§20.2.4): itself and what follows, under 64. + let package = |op: &[u8], body: &[u8]| [op, &[u8::try_from(body.len() + 1).expect("a short package")], body].concat(); + // SystemIO (1) at a WordConst, of a ByteConst's length. + let region = [&[0x5B, 0x80], PM1S, &[0x01, 0x0B], &pm1.to_le_bytes(), &[0x0A, 0x02]].concat(); + // WordAcc (2); a ReservedField (0) of 8 bits, then PBST of 1. + let field = package(&[0x5B, 0x81], &[PM1S, &[0x02, 0x00, 0x08], PBST, &[0x01]].concat()); + // LNot (0x92) of PBST; Sleep of One (0x01). + let wait = package(&[0xA2], &[&[0x92], PBST, &[0x5B, 0x22, 0x01]].concat()); + let acquire = [&[0x5B, 0x23], GL, &ms.to_le_bytes()].concat(); + let notify = [&[0x86], GLW, &[0x0A, 0x80]].concat(); + let release = [&[0x5B, 0x27], GL].concat(); + let aml = [ + package(&[0x5B, 0x82], GLW), + region, + field, + wait, + package(&[0xA0], &[acquire, notify].concat()), + package(&[0xA1], &release), + ] + .concat(); + let length = u32::try_from(36 + aml.len()).expect("a short table"); + let mut table = [b"SSDT".as_slice(), &length.to_le_bytes(), &[2, 0], b"TOYOS ", b"GLWAIT ", &1u32.to_le_bytes(), b"TOYS", &1u32.to_le_bytes(), &aml].concat(); + // The byte at 9 makes the whole table sum to zero. + table[9] = table.iter().fold(0u8, |sum, &b| sum.wrapping_sub(b)); + table +} + +/// The number after `after` on a line, in hex. +fn hex_after(line: &str, after: &str) -> Result { + line.split_once(after) + .map(|(_, rest)| rest.trim_start_matches("0x")) + .and_then(|rest| u64::from_str_radix(&rest[..rest.find(|c: char| !c.is_ascii_hexdigit()).unwrap_or(rest.len())], 16).ok()) + .ok_or_else(|| format!("no number after {after:?} on {line:?}")) +} + +/// A press latched before the server's wait for the firmware's release of +/// the Global Lock is served once the wait ends at its Acquire's timeout: the +/// wait clears the press's enable once it finds its status latched, takes +/// the SCI the press raised, and puts every enable back after, so the press's +/// status raises the SCI again for `serve`. `tests/acpicase` with the test +/// kernel's actuator for the firmware's side of the lock, which +/// `acpi_mediated`'s `held` arm stages owned before it hands the server the +/// claim; the table QEMU adds waits as it loads for the press, sent once the +/// server has armed, and only then Acquires the lock within [`ACQUIRE_MS`]. +/// Then the press must power the machine off, after the Acquire said it timed +/// out. +pub fn acpi_press_across_a_lock_wait(probe: (String, Vec)) -> Result<(), String> { + let case = super::compile::repo_root().join("tests/acpicase"); + let mut qemu = QemuInstance::boot_with_options( + &case, + &[], + &[], + BootOptions { + // The test kernel, for the Global Lock's actuator, as `acpi_mediated_access` boots it. + kernel_params: &["i8042-withheld"], + ready_marker: "acpi: the Global Lock is the FACS's at ", + extra_root_files: vec![ + probe, + // The arm's name is the file's: the probe asks whether it is there. + ("share/acpi_mediated_held".to_string(), b"held\n".to_vec()), + ], + acpi_tables: vec![ssdt_acquiring_the_global_lock(Q35_PM1A_EVENTS, ACQUIRE_MS)], + qmp: true, + ..Default::default() + }, + ); + let boot = serial::Serial::named("the boot", qemu.boot_log().to_string()); + let pm1 = hex_after(boot.must_say("acpi: the ACPI row: PM1a events ")?, "PM1a events ")?; + if pm1 != u64::from(Q35_PM1A_EVENTS) { + return Err(format!("the PM1a event block is at {pm1:#x}, where the table QEMU was handed reads {Q35_PM1A_EVENTS:#x}")); + } + // Opened before the press: QMP delivers no event emitted before its + // client connected. + let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)); + let mut console = format!("{}\n", qemu.boot_log()); + qemu::await_marker(&mut qemu, &mut console, ACPI_ARMED, "the ACPI server arming")?; + stop.power_button(); + let pressed_at = console.len(); + ended(&mut qemu, &mut stop, &mut console, SHUTTING_DOWN, "guest-shutdown")?; + let after = serial::Serial::named("the press", console[pressed_at..].to_string()); + after.must_say(ONE_TAKE_CONTENDED)?; + after.must_say_after(ACQUIRE_TIMED_OUT, ACPI_PRESSED)?; + after.must_be_clean()?; + let whole = serial::Serial::named("the boot and the press", console); + if acpi_tables_loaded(&whole, &whole, Q35_S5_SUPPLIED)? < 2 { + return Err(format!("the server loaded no table beside the DSDT, so not the one QEMU was handed:\n{}", whole.text())); + } + eprintln!(" [power] {}", whole.must_say(ONE_TAKE_CONTENDED)?.trim()); + eprintln!(" [power] {}", whole.must_say_after(ACQUIRE_TIMED_OUT, ACPI_PRESSED)?.trim()); + Ok(()) +} diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 5c7e8ff2689..236957e529b 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -1103,6 +1103,10 @@ pub struct BootOptions { /// update's ROOT of this many bytes: a machine that updates itself. /// `None` for every guest whose subject is not the update. pub second_slot: Option, + /// Whole ACPI tables, header and checksum included, that QEMU lists + /// beside its own (`-acpitable`): firmware AML a guest's own tables do + /// not carry. q35's alone. + pub acpi_tables: Vec>, /// The instant the machine's RTC starts at, as QEMU's `-rtc base=` takes /// it, and the host's own UTC with `None`. pub rtc_base: Option<&'static str>, @@ -1144,6 +1148,7 @@ impl Default for BootOptions { extra_root_files: Vec::new(), psci_trace: None, second_slot: None, + acpi_tables: Vec::new(), rtc_base: None, } } @@ -1427,6 +1432,9 @@ impl QemuInstance { }; let sockets = Sockets::new(&options); + for (i, table) in options.acpi_tables.iter().enumerate() { + fs::write(acpi_table(&sockets.dir, i), table).expect("[qemu] write an added ACPI table"); + } let screendump = test_dir.join(format!("screen-{seq}.ppm")); // Per-instance, not a fixed /tmp path: a screen test waits on this @@ -2402,6 +2410,10 @@ fn qemu_command( if options.gdb_stub { qemu.arg("-s"); } + assert!(options.acpi_tables.is_empty() || arch == Arch::X86_64, "an added ACPI table is q35's"); + for i in 0..options.acpi_tables.len() { + qemu.arg("-acpitable").arg(format!("file={}", acpi_table(socket_dir, i).display())); + } if let Some(socket) = qmp_socket { qemu.arg("-qmp") .arg(format!("unix:{},server,nowait", socket.display())); @@ -2433,6 +2445,11 @@ impl Sockets { } } +/// The `i`th of [`BootOptions::acpi_tables`], as a file in a boot's own `dir`. +fn acpi_table(dir: &Path, i: usize) -> PathBuf { + dir.join(format!("acpi-{i}.aml")) +} + /// The QMP socket `options` asks for, named in `dir`. fn qmp_socket(dir: &Path, options: &BootOptions) -> Option { options.qmp.then(|| dir.join("qmp.sock")) diff --git a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs index 0ed9c5bfa9a..68b2fcb7fc8 100644 --- a/tests/toyos-rust-tests/src/bin/acpi_mediated.rs +++ b/tests/toyos-rust-tests/src/bin/acpi_mediated.rs @@ -42,12 +42,18 @@ //! [`OUTLIVED`] has a child supply this guest's own sleep type and exit, and //! the parent, which supplied none, ask for the power-off after the child's //! claim and one more have been released. +//! +//! **A press is served across the server's wait for the firmware's release** +//! ([`held`]), on a boot whose ROOT carries [`HELD`] and whose firmware lists +//! a table that takes the Global Lock at its load: the firmware's side of the +//! lock is staged owned before `/system/bin/acpiserver` is handed the claim. +//! The press, the wait's timeout and the power-off are the harness's to read. use std::os::toyos::process::CommandExt; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; -use toyos::endow::{Endowments, SYSCAP_LABEL}; +use toyos::endow::{Endowments, DEV_PREFIX, SYSCAP_LABEL}; use toyos::syscap::SysCap; use toyos::{AsHandle, Device}; use toyos_abi::acpi::{pci_address, Access, AcpiInfo, Refused, Space, Width, UNLISTED}; @@ -65,6 +71,9 @@ const CLAIM_LABEL: &str = "acpi-claim"; /// binary's name and takes no argument. const OUTLIVED: &str = "/system/share/acpi_mediated_outlived"; +/// The file whose presence on ROOT makes this boot [`held`]'s. +const HELD: &str = "/system/share/acpi_mediated_held"; + /// `EFI_MEMORY_TYPE`s: what the kernel hands out as RAM, and ACPI's two. const USABLE: [u8; 5] = [1, 2, 3, 4, 7]; const ACPI_RECLAIM: u8 = 9; @@ -112,6 +121,7 @@ impl Holder { fn main() { match std::env::args().nth(1).as_deref() { None if std::fs::exists(OUTLIVED).expect("acpi: ask ROOT for this boot's arm") => outlived(), + None if std::fs::exists(HELD).expect("acpi: ask ROOT for this boot's arm") => held(), None => probe(), Some("keeper") => keeper(), Some("supplier") => supplier(), @@ -629,3 +639,20 @@ fn outlived() { let refused = toyos::power::stop(toyos::power::Stop::Shutdown); panic!("acpi: the power-off was refused: {refused:?}"); } + +/// What [`held`] says once the server has the claim, with the firmware's +/// side of the lock owned. +const STAGED: &str = "acpi: the firmware's side of the Global Lock staged owned, and the server handed the claim"; + +fn held() { + let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("test-runner endows a device-minting capability"); + let claim = claim(&cap); + assert_eq!(syscall::debug_with(debug_action::ACPI_FIRMWARE_LOCK, debug_action::FIRMWARE_OWNS), 0, "acpi: the lock was not free before the server ran"); + let mut server = Command::new("/system/bin/acpiserver") + .endow(&format!("{DEV_PREFIX}{}", DeviceType::Acpi.class_name()), claim.into_raw().0) + .spawn() + .expect("acpi: spawn the server"); + println!("{STAGED}"); + let ended = server.wait().expect("acpi: wait for the server"); + panic!("acpi: the server ended {ended:?}, where a press powers the machine off"); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 8cedebc6c1e..802563d3940 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -182,7 +182,8 @@ const RUST_SKIP: &[&str] = &[ // It claims the fixed hardware itself, which needs a boot that starts no // server, and stages the firmware's side of the Global Lock and finds the // i8042's row another claim's, which need the test kernel and its - // `i8042-withheld`: `acpi_mediated_access` runs it on tests/acpicase. + // `i8042-withheld`: `acpi_mediated_access` and `acpi_press_across_a_lock_wait` + // run it on tests/acpicase. "acpi_mediated", // It powers the machine off: `machine_shutdown_short_stop` runs it. "stop_short", @@ -370,6 +371,12 @@ const MACHINE_TESTS: &[&str] = &[ // by stopping. No host test reaches either, and the T14 is never asked to // power off. "acpi_supply_outlives_holder", + // A press while acpiserver waits for the firmware's release of the Global + // Lock: the firmware's hold is the test kernel's actuator on the FACS's + // word, the table that takes the lock is one QEMU adds, and the press is + // QEMU's on demand. The T14's firmware holds the lock when it will, and + // nothing presses its button but a hand. + "acpi_press_across_a_lock_wait", // The power-off after a stop that left a thread running, in ACPI mode: it // ends the machine, so only one QEMU reports stopping can be asked, and // the T14 hands over in legacy mode, where no holder means no quieting. @@ -610,6 +617,17 @@ const METAL: &[(&str, metal::Metal)] = &[ judge: |b| acpi_tables_on_metal(b[0]), }, ), + ( + // The T14's battery and AC adapter as the server read them through + // its AML and its embedded controller: `battery_on_metal` says what + // is held, and prints the readings for Linux's on the same machine. + // The same boot as `acpi_server_events`, held past several polls. + "battery_read", + metal::Metal { + arms: TESTCASES_HELD, + judge: |b| battery_on_metal(b[0]), + }, + ), ( // The server killed: the kernel writes `ACPI_DISABLE` as its claim goes, // and `SCI_EN` reads clear after it. @@ -3878,6 +3896,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "acpi_mediated_access" => acpi_mediated_access(2), "acpi_lock_given_back_on_one_cpu" => acpi_mediated_access(1), "acpi_supply_outlives_holder" => acpi_supply_outlives_holder(), + "acpi_press_across_a_lock_wait" => power::acpi_press_across_a_lock_wait(suite_bin(toyos_build::arch::Arch::X86_64, "acpi_mediated")), "machine_shutdown_short_stop" => power::machine_shutdown_short_stop(test_config), "machine_shutdown_wire_held" => power::machine_shutdown_wire_held(test_config, false), "machine_shutdown_wire_kept" => power::machine_shutdown_wire_held(test_config, true), @@ -5563,6 +5582,50 @@ fn acpi_tables_on_metal(back: &metal::Readback) -> Result<(), String> { Ok(()) } +/// What the T14's tables name, by a census of them read outside the tree: +/// one control-method battery, one AC adapter, and one embedded controller, +/// attached to the server's AML. +const T14_POWER_SOURCES: &str = "acpiserver: power sources: 1 of 1 control-method batteries present, 1 AC adapter(s), with an embedded controller attached; the Global Lock taken "; + +/// The T14's battery as the server read it: the one battery its tables name +/// found present once the controller's `_REG` ran, its information read, +/// and at least one reading of it with the adapter's state; every method +/// answered. The census's Global Lock counts are printed and held to +/// nothing: an evaluation gives back the lock it still holds as it ends +/// (`finish`, `userland/acpiserver/aml/src/exec.rs`), and the real lock +/// word's exchange is `acpi_tables_on_metal`'s to hold. The lines are printed +/// whole: their numbers are what Linux's `/sys/class/power_supply` reads on +/// the same machine are compared against, by whoever holds both. +fn battery_on_metal(back: &metal::Readback) -> Result<(), String> { + back.job_passed("test_rs_acpi_hold")?; + let log = back.log(); + let lines: Vec<&str> = log + .text() + .lines() + .filter(|l| toyos_logstream::program_line(l).is_some_and(|said| said.tag == "acpiserver")) + .collect(); + if let Some(fired) = lines.iter().find(|l| l.contains("panicked")) { + return Err(format!("the server died: {fired}")); + } + let refused: Vec<&&str> = lines.iter().filter(|l| l.contains("acpiserver: refused") || l.contains(" refused: ")).collect(); + if !refused.is_empty() { + return Err(format!("the server refused something of this machine's AML: {refused:#?}")); + } + let found = log.must_say(T14_POWER_SOURCES)?; + let info = log.must_say(&format!("acpiserver: {}battery 1 of 1 from ", acpiserver_api::BATTERY_INFO))?; + let read: Vec<&&str> = lines.iter().filter(|l| l.contains(acpiserver_api::BATTERY_READ)).collect(); + if read.is_empty() { + return Err("the server said no reading of the battery".into()); + } + if let Some(unknown) = read.iter().find(|l| l.contains("unknown") || !(l.ends_with("; AC online") || l.ends_with("; AC offline"))) { + return Err(format!("a reading with a value unknown: {unknown}")); + } + for line in [found, info].into_iter().chain(read.iter().map(|l| **l)) { + eprintln!(" [battery] {}", line.trim()); + } + Ok(()) +} + /// The server's death on the T14: the kernel put the machine in ACPI mode for /// the job's claim, and when the killed server's claim went it wrote /// `ACPI_DISABLE` (the FADT's 0xf1) and read `SCI_EN` clear: the firmware has diff --git a/userland/acpiserver-api/src/lib.rs b/userland/acpiserver-api/src/lib.rs index a327b602bbc..fb09b38c7e0 100644 --- a/userland/acpiserver-api/src/lib.rs +++ b/userland/acpiserver-api/src/lib.rs @@ -13,3 +13,13 @@ pub const QUERIES_COUNTED: &str = "embedded controller queries taken: "; /// [`CONTROLLER_NONE`] and why where it serves none. pub const CONTROLLER_SERVED: &str = "acpiserver: embedded controller on GPE "; pub const CONTROLLER_NONE: &str = "acpiserver: embedded controller none: "; + +/// A present battery's static information, once after the controller is +/// armed: `battery of `, then its numbers. +pub const BATTERY_INFO: &str = "battery information: "; + +/// A battery's reading, on its first and wherever its whole percent or state +/// or the adapters' moved: `battery of : %`, its +/// remaining and full capacity, its state, rate, voltage and the power it +/// moves in mW, then the adapters' state. +pub const BATTERY_READ: &str = "battery read: "; diff --git a/userland/acpiserver/aml/src/exec.rs b/userland/acpiserver/aml/src/exec.rs index 20143bcb625..7c8f5c9dfb8 100644 --- a/userland/acpiserver/aml/src/exec.rs +++ b/userland/acpiserver/aml/src/exec.rs @@ -250,7 +250,7 @@ impl<'a> Machine<'a> { let mut released = Ok(()); if self.global > 0 { self.global = 0; - released = self.host.global_lock(false).map_err(|d| Error::Host(d.0)); + released = self.host.global_release().map_err(|d| Error::Host(d.0)); } let r = r?; released?; @@ -260,18 +260,33 @@ impl<'a> Machine<'a> { Ok(r) } - pub(crate) fn take_global(&mut self) -> Result<(), Error> { + /// Holds the Global Lock once more, taking it from the host where this + /// evaluation does not hold it yet: `Ok(false)` is a take `within` a + /// bound that timed out, charged as a wait, and nothing held. The take + /// waits no longer than the evaluation has left to wait, so a bound past + /// that is refused once what is left has run out. + pub(crate) fn take_global(&mut self, within: Option) -> Result { if self.global == 0 { - self.host.global_lock(true).map_err(|d| Error::Host(d.0))?; + match within { + None => self.host.global_take().map_err(|d| Error::Host(d.0))?, + Some(ms) => { + let left = (MAX_WAIT_US - self.waited_us) / 1000; + let bound = u16::try_from(left).map_or(ms, |left| ms.min(left)); + if !self.host.global_take_within(bound).map_err(|d| Error::Host(d.0))? { + self.wait(u64::from(ms) * 1000)?; + return Ok(false); + } + } + } } self.global += 1; - Ok(()) + Ok(true) } pub(crate) fn drop_global(&mut self) -> Result<(), Error> { self.global = self.global.checked_sub(1).expect("the Global Lock is given back only by who took it"); if self.global == 0 { - self.host.global_lock(false).map_err(|d| Error::Host(d.0))?; + self.host.global_release().map_err(|d| Error::Host(d.0))?; } Ok(()) } @@ -874,18 +889,20 @@ impl<'a> Machine<'a> { Ok(Flow::Next) } - fn acquire(&mut self, m: &Kept) -> Result<(), Error> { + /// `false` where `\_GL`'s firmware side held it past `within` + /// milliseconds, and nothing was acquired. + fn acquire(&mut self, m: &Kept, within: Option) -> Result { // §19.6.88: an Acquire's SyncLevel is equal to or above the current one. if m.held.get() == 0 && self.levels.last().is_some_and(|&l| m.sync < l) { return Err(Error::Rule("Acquire of a Mutex below the current SyncLevel (§19.6.88)")); } - if m.global && m.held.get() == 0 { - self.take_global()?; + if m.global && m.held.get() == 0 && !self.take_global(within)? { + return Ok(false); } m.held.set(m.held.get() + 1); self.held.push(m.clone()); self.levels.push(m.sync); - Ok(()) + Ok(true) } fn release(&mut self, m: &Kept) -> Result<(), Error> { @@ -1487,15 +1504,15 @@ impl<'a> Machine<'a> { fn acquire_op(&mut self, f: &mut Frame, c: &mut Cursor<'_>) -> Result { let t = self.super_name(f, c)?; - c.word()?; + // §19.6.2: 0xFFFF waits with no bound. + let within = Some(c.word()?).filter(|&ms| ms != 0xFFFF); let id = self.node_of(f, &t)?; let Object::Mutex(m) = self.node_object(id)? else { return Err(Error::Type("Acquire of an object that is not a Mutex (§19.6.2)")); }; - // One invocation runs at a time, so no Mutex is owned by another: - // every Acquire is granted, and none times out. - self.acquire(&m)?; - Ok(Object::Int(0)) + // One invocation runs at a time, so no Mutex is owned by another + // invocation: only `\_GL`'s firmware side can time an Acquire out. + Ok(Object::Int(if self.acquire(&m, within)? { 0 } else { self.w.ones() })) } fn wait_op(&mut self, f: &mut Frame, c: &mut Cursor<'_>) -> Result { diff --git a/userland/acpiserver/aml/src/field.rs b/userland/acpiserver/aml/src/field.rs index 28353ce1ec2..9aef5d90791 100644 --- a/userland/acpiserver/aml/src/field.rs +++ b/userland/acpiserver/aml/src/field.rs @@ -407,7 +407,7 @@ impl Machine<'_> { /// Runs `body` holding the Global Lock where `lock` says (§19.6.47). fn locked(&mut self, lock: bool, body: impl FnOnce(&mut Self) -> Result) -> Result { if lock { - self.take_global()?; + self.take_global(None)?; } let r = body(self); if lock { diff --git a/userland/acpiserver/aml/src/lib.rs b/userland/acpiserver/aml/src/lib.rs index f75947a350f..69fc21fab7c 100644 --- a/userland/acpiserver/aml/src/lib.rs +++ b/userland/acpiserver/aml/src/lib.rs @@ -21,7 +21,8 @@ //! SystemIO, PCI_Config and EmbeddedControl space; an access in any other //! space is refused as [`Error::Unsupported`], as are `Load`, `LoadTable` //! and `DataTableRegion`. Only one invocation runs at a time, so a Mutex is -//! never contended and an Event is never signalled by anyone else. +//! never contended and an Event is never signalled by anyone else; `\_GL`'s +//! other owner is the firmware, whose side the [`Host`] waits out. //! //! The predefined objects are the operating system's (§5.7), answered as //! Windows answers them, by the owner's rulings ("Like Windows, not Linux"; @@ -80,7 +81,8 @@ pub const MAX_LIVE: usize = 16 << 20; /// The bytes of work one step stands for: a step for every this many bytes /// an operation makes, copies, compares or walks. pub(crate) const WORK_PER_STEP: usize = 64; -/// The time one evaluation may ask to Sleep, Stall and Wait, together, in µs. +/// The time one evaluation may ask to Sleep, Stall and Wait, and wait out +/// in Acquires of `\_GL` that timed out, together, in µs. pub(crate) const MAX_WAIT_US: u64 = 10_000_000; /// What the Revision opcode answers (§19.6.119): this interpreter's revision. pub(crate) const REVISION: u64 = 1; @@ -181,9 +183,16 @@ pub trait Host { fn timer(&mut self) -> u64; /// Notify (§19.6.94) of the object at this absolute path. fn notify(&mut self, object: &str, value: u64); - /// Takes (`true`) or gives back (`false`) the firmware's Global Lock - /// (§5.2.10.1), around a Lock field's access and `\_GL`'s ownership. - fn global_lock(&mut self, take: bool) -> Result<(), Denied>; + /// Takes the firmware's Global Lock (§5.2.10.1), for a Lock field's + /// access or an Acquire of `\_GL` with no TimeoutValue (0xFFFF, + /// §19.6.2), waiting where the firmware holds it for its release. + fn global_take(&mut self) -> Result<(), Denied>; + /// Takes the Global Lock for an Acquire of `\_GL` whose TimeoutValue is + /// `ms`, waiting where the firmware holds it at most that long: `false` + /// is the timeout, and nothing taken. + fn global_take_within(&mut self, ms: u16) -> Result; + /// Gives back the Global Lock a take took. + fn global_release(&mut self) -> Result<(), Denied>; } /// An object as the caller receives or passes it. diff --git a/userland/acpiserver/aml/tests/common/mod.rs b/userland/acpiserver/aml/tests/common/mod.rs index fae63efcb5b..c9e8e54da0d 100644 --- a/userland/acpiserver/aml/tests/common/mod.rs +++ b/userland/acpiserver/aml/tests/common/mod.rs @@ -296,7 +296,8 @@ pub enum Event { Sleep(u64), Stall(u64), Notify(String, u64), - GlobalLock(bool), + GlobalTake(Option), + GlobalRelease, } /// A machine of bytes: every address space a sparse map, every request @@ -306,6 +307,8 @@ pub struct Machine { pub bytes: BTreeMap<(u8, u64), u8>, pub log: Vec, pub refuse: bool, + /// The firmware holds the Global Lock past every bound a take names. + pub firmware_holds: bool, } fn key(a: Address) -> (u8, u64) { @@ -389,8 +392,18 @@ impl Host for Machine { self.log.push(Event::Notify(object.into(), value)); } - fn global_lock(&mut self, take: bool) -> Result<(), Denied> { - self.log.push(Event::GlobalLock(take)); + fn global_take(&mut self) -> Result<(), Denied> { + self.log.push(Event::GlobalTake(None)); + Ok(()) + } + + fn global_take_within(&mut self, ms: u16) -> Result { + self.log.push(Event::GlobalTake(Some(ms))); + Ok(!self.firmware_holds) + } + + fn global_release(&mut self) -> Result<(), Denied> { + self.log.push(Event::GlobalRelease); Ok(()) } } @@ -465,7 +478,13 @@ impl Host for Sink { 0 } fn notify(&mut self, _: &str, _: u64) {} - fn global_lock(&mut self, _: bool) -> Result<(), Denied> { + fn global_take(&mut self) -> Result<(), Denied> { + Ok(()) + } + fn global_take_within(&mut self, _: u16) -> Result { + Ok(true) + } + fn global_release(&mut self) -> Result<(), Denied> { Ok(()) } } diff --git a/userland/acpiserver/aml/tests/evaluate.rs b/userland/acpiserver/aml/tests/evaluate.rs index c36740a0e4b..a0a7b3677b3 100644 --- a/userland/acpiserver/aml/tests/evaluate.rs +++ b/userland/acpiserver/aml/tests/evaluate.rs @@ -405,7 +405,43 @@ fn mutexes_follow_sync_levels_and_are_released_by_the_end() { assert_eq!(ip.evaluate(&mut m, "\\GOOD", &[]), i(1)); m.log.clear(); ip.evaluate(&mut m, "\\GL", &[]).unwrap(); - assert_eq!(m.log, vec![Event::GlobalLock(true), Event::GlobalLock(false)]); + assert_eq!(m.log, vec![Event::GlobalTake(None), Event::GlobalRelease]); +} + +/// §19.6.2: an Acquire of `\_GL` waits for the firmware's side at most its +/// TimeoutValue in milliseconds, 0xFFFF naming no bound; one that times out +/// returns True, holds nothing and is charged as a wait, and one this +/// evaluation already holds asks the host for nothing. The host waits no +/// longer than the evaluation has left to wait. +#[test] +fn an_acquire_of_the_global_lock_waits_as_its_timeout_says() { + let acquire = |ms: u16| cat(&[&[0x5B, 0x23], &name("\\_GL"), &ms.to_le_bytes()]); + let release = cat(&[&[0x5B, 0x27], &name("\\_GL")]); + let (mut ip, mut m) = loaded(&cat(&[ + &method("SOON", 0, &cat(&[&store(&acquire(5), &local(0)), &release, &ret(&local(0))])), + &method("LATE", 0, &ret(&acquire(5))), + &method("LONG", 0, &cat(&[&store(&acquire(6000), &local(0)), &ret(&acquire(6000))])), + &method("SPNT", 0, &cat(&[&[0x5B, 0x22], &int(10_000), &ret(&acquire(5))])), + &method("EVER", 0, &cat(&[&store(&acquire(0xFFFF), &local(0)), &store(&acquire(0), &local(1)), &release, &release, &ret(&add(&local(0), &local(1), ZERO))])), + ])); + assert_eq!(ip.evaluate(&mut m, "\\EVER", &[]), i(0)); + assert_eq!(m.log, vec![Event::GlobalTake(None), Event::GlobalRelease], "the second Acquire asked the host again"); + m.log.clear(); + assert_eq!(ip.evaluate(&mut m, "\\SOON", &[]), i(0)); + assert_eq!(m.log, vec![Event::GlobalTake(Some(5)), Event::GlobalRelease]); + assert_eq!(ip.usage().waited_us, 0, "an Acquire that took the lock was charged a wait"); + m.firmware_holds = true; + m.log.clear(); + assert_eq!(ip.evaluate(&mut m, "\\LATE", &[]), i(ONES), "an Acquire that timed out is True (§19.6.2)"); + assert_eq!(m.log, vec![Event::GlobalTake(Some(5))], "a lock the take did not get was given back"); + assert_eq!(ip.usage().waited_us, 5000); + m.log.clear(); + assert_eq!(ip.evaluate(&mut m, "\\LONG", &[]), Err(Error::Bound("more time asleep than one evaluation may spend"))); + assert_eq!(m.log, vec![Event::GlobalTake(Some(6000)), Event::GlobalTake(Some(4000))], "a take waited past what was left"); + assert_eq!(ip.usage().waited_us, 12_000_000); + m.log.clear(); + assert_eq!(ip.evaluate(&mut m, "\\SPNT", &[]), Err(Error::Bound("more time asleep than one evaluation may spend"))); + assert_eq!(m.log, vec![Event::Sleep(10_000), Event::GlobalTake(Some(0))], "a take with nothing left waited"); } #[test] diff --git a/userland/acpiserver/aml/tests/regions.rs b/userland/acpiserver/aml/tests/regions.rs index cbc0d7b298b..a56e59807df 100644 --- a/userland/acpiserver/aml/tests/regions.rs +++ b/userland/acpiserver/aml/tests/regions.rs @@ -361,10 +361,10 @@ fn a_lock_field_holds_the_global_lock_across_its_access() { assert_eq!( m.log, vec![ - Event::GlobalLock(true), + Event::GlobalTake(None), Event::Read(mem(0x1000), Access::Byte), Event::Read(mem(0x1001), Access::Byte), - Event::GlobalLock(false), + Event::GlobalRelease, ] ); } diff --git a/userland/acpiserver/src/aml.rs b/userland/acpiserver/src/aml.rs index a75b1b00f61..e06dd7dea5e 100644 --- a/userland/acpiserver/src/aml.rs +++ b/userland/acpiserver/src/aml.rs @@ -23,8 +23,9 @@ //! not hold — and that is said once, loudly ([`NO_S5_HANDED`]); the server //! goes on serving. **A namespace whose DSDT loaded is kept** ([`Aml`]), //! with the host that answered its load, for what the server evaluates in it -//! after: the devices it serves ([`crate::devices`]), and, where the power -//! button is a control method device, each embedded-controller query's method. +//! after: the devices it serves ([`crate::devices`]), the power sources +//! ([`crate::battery`]), and, where the power button is a control method +//! device, each embedded-controller query's method. //! //! A table's line says its place, whether it is the DSDT or an SSDT, and a //! refusal by its kind; what the firmware chose — a name, an offset, an @@ -245,7 +246,7 @@ fn s5(interpreter: &mut Interpreter, host: &mut dyn Host) -> Result<(u64, u64), pub mod tests { use super::*; use crate::host::tests::Scripted; - use crate::host::{Take, HELD}; + use crate::host::Take; fn load_only(kernel: &K, rsdp: u64) -> Loaded { load(kernel, rsdp).0 @@ -470,10 +471,10 @@ pub mod tests { } /// A Lock field's access takes the Global Lock through the kernel and - /// gives it back; one the firmware holds is the load's refusal, by name, - /// and the field is not read without it. + /// gives it back; one the firmware holds is waited for, and the load goes + /// on once the take after its release has it. #[test] - fn a_lock_field_read_at_load_takes_the_lock_and_a_held_lock_refuses_the_table() { + fn a_lock_field_read_at_load_takes_the_lock_and_waits_out_the_firmwares_hold() { const NVS: u64 = 0x7700_0000; // OperationRegion (REGN, SystemMemory, 0x77000000, 4); Field (REGN, // ByteAcc, Lock, Preserve) { FLDA, 8 }; Name (COPY, 0); Store (FLDA, COPY). @@ -509,8 +510,10 @@ pub mod tests { let kernel = with_nvs(); kernel.takes.borrow_mut().push_back(Take::Pending); - let loaded = load_only(&kernel, CRAFTED_RSDP); - assert_eq!(loaded, Loaded { blocks: vec![Err(format!("denied by this server: {HELD}"))], s5: None, handed: false }); - assert_ne!(kernel.asked.borrow().last(), Some(&field), "the field was read without the lock"); + kernel.releases.borrow_mut().push_back(true); + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((5, 0)), handed: true }); + assert!(!kernel.held.get(), "the load ended holding the Global Lock"); + assert_eq!(kernel.waited.borrow().len(), 1, "the firmware's hold was not waited out"); + assert_eq!(kernel.asked.borrow().last(), Some(&field)); } } diff --git a/userland/acpiserver/src/battery.rs b/userland/acpiserver/src/battery.rs new file mode 100644 index 00000000000..c1552048b6f --- /dev/null +++ b/userland/acpiserver/src/battery.rs @@ -0,0 +1,838 @@ +//! The machine's batteries and AC adapters, as its AML reports them (ACPI 6.5 +//! §10.2, §10.3), read in the namespace the load kept ([`crate::aml::Aml`]). +//! +//! **Once, after the embedded controller is armed** ([`Power::find`]): the +//! batteries (`PNP0C0A`) and adapters (`ACPI0003`) are the ones +//! [`crate::devices::find`] named, and their methods read the controller's +//! space only once the server has attached it and run its `_REG`. A battery +//! is kept where its `_STA` says one is present (§6.3.7, bit 4; a device with +//! no `_STA` has none), and its static information is read once, from +//! `_BIX` where it has one and `_BIF` where it has not (§10.2.2). +//! +//! The census line [`Power::find`] answers counts the Global Lock's takes of +//! the batteries' and adapters' own methods alone: the controller's `_REG` +//! and the walk that named the devices ran before it, and are not theirs. +//! +//! **Every [`POLL`]** each battery's `_BST` and each adapter's `_PSR` are +//! evaluated ([`Power::read`]), and a line is said on the first reading and +//! wherever a battery's whole percent or state, or the adapter's, moved since +//! the line before: a draining battery says a line a percent, never one a +//! poll. +//! +//! **What a line says is numbers.** The model, serial number, type and OEM +//! strings of `_BIX` and `_BIF` are never taken out of the package, and a +//! device's path is the firmware's choice, said only under [`OWN`]. A +//! refused evaluation is said under [`REFUSED`] the first time and counted +//! after; its battery or adapter reads as unknown until one answers. + +use std::fmt; +use std::time::Duration; + +use toyos_aml::{Error, Value}; + +use crate::aml::{kind, Aml}; +use crate::host::{Kernel, OWN}; +use crate::ledger::Ledger; + +/// How often each battery and adapter is read. +pub const POLL: Duration = Duration::from_secs(10); + +/// What opens the line of an evaluation this module asked for and the +/// interpreter or the host refused. +pub const REFUSED: &str = "acpiserver: a method read for the power sources refused: "; + +/// §10.2.2: a capacity, rate or voltage the battery does not know. +const UNKNOWN: u64 = 0xFFFF_FFFF; + +/// `_BST`'s Battery State bits (§10.2.2, "_BST"). +const DISCHARGING: u64 = 1 << 0; +const CHARGING: u64 = 1 << 1; +const CRITICAL: u64 = 1 << 2; +const LIMITING: u64 = 1 << 3; + +/// `_STA`'s battery-present bit (§6.3.7). +const BATTERY_PRESENT: u64 = 1 << 4; + +/// What `_BIF` and `_BIX` say a battery's capacities and rates are counted +/// in (their Power Unit). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Unit { + /// Capacities in mWh, rates in mW. + Power, + /// Capacities in mAh, rates in mA. + Current, +} + +impl Unit { + fn capacity(self) -> &'static str { + match self { + Unit::Power => "mWh", + Unit::Current => "mAh", + } + } + + fn rate(self) -> &'static str { + match self { + Unit::Power => "mW", + Unit::Current => "mA", + } + } +} + +/// A battery's static information, the numbers of `_BIX` or `_BIF`. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Info { + /// Which of the two it came from. + pub from: &'static str, + pub unit: Unit, + pub design: Option, + pub full: Option, + /// mV. + pub design_voltage: Option, + /// `_BIX`'s alone. + pub cycles: Option, +} + +/// One `_BST`. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Status { + /// The Battery State bits as the firmware returned them. + pub state: u64, + /// In the battery's [`Unit`]'s rate. + pub rate: Option, + /// In its capacity. + pub remaining: Option, + /// mV. + pub voltage: Option, +} + +/// One element of a package of DWORDs: an Integer no wider than 32 bits, +/// [`UNKNOWN`] read as `None`. +fn dword(elements: &[Value], at: usize, what: &str) -> Result, String> { + match elements.get(at) { + Some(&Value::Integer(UNKNOWN)) => Ok(None), + Some(&Value::Integer(value)) => u32::try_from(value).map(Some).map_err(|_| format!("its {what} is wider than a DWORD")), + Some(_) => Err(format!("its {what} is no Integer")), + None => Err(format!("it has no {what}")), + } +} + +fn package<'v>(value: &'v Value, least: usize) -> Result<&'v [Value], String> { + match value { + Value::Package(elements) if elements.len() >= least => Ok(elements), + Value::Package(elements) => Err(format!("a package of {} elements, where §10.2.2 has at least {least}", elements.len())), + _ => Err("no package".into()), + } +} + +fn unit(elements: &[Value], at: usize) -> Result { + match elements.get(at) { + Some(Value::Integer(0)) => Ok(Unit::Power), + Some(Value::Integer(1)) => Ok(Unit::Current), + Some(Value::Integer(other)) => Err(format!("its Power Unit is {other}, which §10.2.2 does not define")), + _ => Err("its Power Unit is no Integer".into()), + } +} + +/// `_BIF`: Power Unit, Design Capacity, Last Full Charge Capacity, Battery +/// Technology, Design Voltage, and eight more this server reads nothing of. +pub fn bif(value: &Value) -> Result { + let elements = package(value, 13)?; + Ok(Info { + from: "_BIF", + unit: unit(elements, 0)?, + design: dword(elements, 1, "Design Capacity")?, + full: dword(elements, 2, "Last Full Charge Capacity")?, + design_voltage: dword(elements, 4, "Design Voltage")?, + cycles: None, + }) +} + +/// `_BIX`: `_BIF`'s numbers behind a Revision, with the Cycle Count after +/// them. Revision 0 has 20 elements and revision 1 adds a 21st; the fields +/// read here are at the same places in both. +pub fn bix(value: &Value) -> Result { + let elements = package(value, 20)?; + Ok(Info { + from: "_BIX", + unit: unit(elements, 1)?, + design: dword(elements, 2, "Design Capacity")?, + full: dword(elements, 3, "Last Full Charge Capacity")?, + design_voltage: dword(elements, 5, "Design Voltage")?, + cycles: dword(elements, 8, "Cycle Count")?, + }) +} + +/// `_BST`: Battery State, Present Rate, Remaining Capacity, Present Voltage. +pub fn bst(value: &Value) -> Result { + let elements = package(value, 4)?; + let state = match elements[0] { + Value::Integer(state) if state & (CHARGING | DISCHARGING) == CHARGING | DISCHARGING => { + return Err("its state says charging and discharging at once, which §10.2.2 forbids".into()); + } + Value::Integer(state) => state, + _ => return Err("its Battery State is no Integer".into()), + }; + Ok(Status { + state, + rate: dword(elements, 1, "Battery Present Rate")?, + remaining: dword(elements, 2, "Battery Remaining Capacity")?, + voltage: dword(elements, 3, "Battery Present Voltage")?, + }) +} + +/// The charge in whole percent of the last full charge, rounded down. +pub fn percent(info: &Info, status: &Status) -> Option { + let full = info.full.filter(|&full| full > 0)?; + Some(u64::from(status.remaining?) * 100 / u64::from(full)) +} + +/// The power the battery moves, in mW: its rate where that is power, and +/// the rate times its present voltage where that is current. +pub fn milliwatts(info: &Info, status: &Status) -> Option { + let rate = u64::from(status.rate?); + match info.unit { + Unit::Power => Some(rate), + Unit::Current => Some(rate * u64::from(status.voltage?) / 1000), + } +} + +/// A battery state's bits by name. +fn state(bits: u64) -> String { + let mut said: Vec = [(DISCHARGING, "discharging"), (CHARGING, "charging"), (CRITICAL, "critical"), (LIMITING, "charge limiting")] + .iter() + .filter(|&&(bit, _)| bits & bit != 0) + .map(|&(_, name)| name.into()) + .collect(); + let reserved = bits & !(DISCHARGING | CHARGING | CRITICAL | LIMITING); + if reserved != 0 { + said.push(format!("reserved bits {reserved:#x}")); + } + if said.is_empty() { "neither charging nor discharging".into() } else { said.join(", ") } +} + +struct Known(Option, &'static str); + +impl fmt::Display for Known { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(value) => write!(f, "{value} {}", self.1), + None => f.write_str("unknown"), + } + } +} + +/// The information line's numbers. +pub fn said_info(info: &Info) -> String { + let unit = info.unit.capacity(); + format!( + "from {}: design {}, last full {}, design voltage {}, {}", + info.from, + Known(info.design, unit), + Known(info.full, unit), + Known(info.design_voltage, "mV"), + match info.cycles { + Some(cycles) => format!("{cycles} cycles"), + None => "cycles unknown".into(), + } + ) +} + +/// A reading line's numbers. +pub fn said_status(info: &Info, status: &Status) -> String { + format!( + "{}, {} of {}, {}, rate {}, {}, moving {}", + percent(info, status).map_or("charge unknown".into(), |percent| format!("{percent}%")), + Known(status.remaining, info.unit.capacity()), + Known(info.full, info.unit.capacity()), + state(status.state), + Known(status.rate, info.unit.rate()), + Known(status.voltage, "mV"), + milliwatts(info, status).map_or("unknown mW".into(), |mw| format!("{mw} mW")), + ) +} + +struct Battery { + path: String, + info: Info, + /// What its last line said: whole percent and state. + said: Option<(Option, u64)>, +} + +/// The machine's power sources, found once and read every [`POLL`]. +pub struct Power { + batteries: Vec, + adapters: Vec, + /// What the last line said of the adapters. + ac_said: Option>, + refused: Ledger, + /// The last evaluation found the machine stopping. + stopping: bool, +} + +impl Power { + /// An evaluation of `path`: `Ok(None)` where nothing is named there. + fn evaluate(&mut self, aml: &mut Aml<'_, K>, path: &str) -> Result, Error> { + let answer = match aml.interpreter.evaluate(&mut aml.host, path, &[]) { + Err(Error::NotFound(named)) if named == path => Ok(None), + answer => answer.map(Some), + }; + self.stopping = aml.host.stopping; + answer + } + + /// A refusal of `method` of the device at `device`, said the first time; + /// on a stopping machine every evaluation is refused, and none is said. + fn refused(&mut self, device: &str, method: &str, why: &str) { + if self.stopping { + return; + } + let what = format!("{method}: {why}"); + if self.refused.see(&what) { + println!("{REFUSED}{what}"); + println!("{OWN}that was {device}.{method}"); + } + } + + /// Keep each of `batteries` present, read its information and first + /// reading, with `adapters`' (the devices [`crate::devices::find`] named + /// by path); `None` where there is no battery to read, and none on a + /// stopping machine. The census line beside it is its caller's to say. + pub fn find(aml: &mut Aml<'_, K>, batteries: Vec, adapters: Vec) -> (Option, Option) { + let mut power = Power { batteries: Vec::new(), adapters, ac_said: None, refused: Ledger::default(), stopping: false }; + let (takes, contended, given_back) = (aml.host.takes, aml.host.contended, aml.host.given_back); + let count = batteries.len(); + for device in batteries { + let sta = match power.evaluate(aml, &format!("{device}._STA")) { + Ok(None) => 0x0F, + Ok(Some(Value::Integer(sta))) => sta, + Ok(Some(_)) => { + power.refused(&device, "_STA", "no Integer"); + continue; + } + Err(why) => { + power.refused(&device, "_STA", &kind(&why)); + continue; + } + }; + if sta & BATTERY_PRESENT == 0 { + println!("{OWN}{device} says no battery is present: _STA {sta:#x}"); + continue; + } + let info = match power.evaluate(aml, &format!("{device}._BIX")) { + Ok(None) => match power.evaluate(aml, &format!("{device}._BIF")) { + Ok(None) => Err(("_BIF", "the device has neither _BIX nor _BIF".to_owned())), + Ok(Some(value)) => bif(&value).map_err(|why| ("_BIF", why)), + Err(why) => Err(("_BIF", kind(&why))), + }, + Ok(Some(value)) => bix(&value).map_err(|why| ("_BIX", why)), + Err(why) => Err(("_BIX", kind(&why))), + }; + match info { + Ok(info) => power.batteries.push(Battery { path: device, info, said: None }), + Err((method, why)) => power.refused(&device, method, &why), + } + } + if power.stopping { + return (None, None); + } + + let present = power.batteries.len(); + for (n, battery) in power.batteries.iter().enumerate() { + println!("acpiserver: {}battery {} of {present} {}", acpiserver_api::BATTERY_INFO, n + 1, said_info(&battery.info)); + println!("{OWN}battery {} of {present} is {}", n + 1, battery.path); + } + if present > 0 { + for line in power.read(aml) { + println!("{line}"); + } + } + let found = format!( + "{present} of {count} control-method batteries present, {} AC adapter(s), with {} embedded controller attached; the Global Lock taken {} times on the way and given back {} times, the firmware found holding it by {} of the takes", + power.adapters.len(), + if aml.host.ec.is_some() { "an" } else { "no" }, + aml.host.takes - takes, + aml.host.given_back - given_back, + aml.host.contended - contended, + ); + if present == 0 { + return (None, Some(format!("acpiserver: no battery: {found}"))); + } + let line = format!("acpiserver: power sources: {found}, the first reading's included; each read every {} s", POLL.as_secs()); + (Some(power), Some(line)) + } + + /// Read every battery and adapter: the line of each battery whose + /// reading moved, for its caller to say. + pub fn read(&mut self, aml: &mut Aml<'_, K>) -> Vec { + let mut lines = Vec::new(); + let mut ac = None; + for at in 0..self.adapters.len() { + let device = self.adapters[at].clone(); + match self.evaluate(aml, &format!("{device}._PSR")) { + Ok(Some(Value::Integer(online))) => ac = Some(ac.unwrap_or(false) || online != 0), + Ok(Some(_)) => self.refused(&device, "_PSR", "no Integer"), + Ok(None) => self.refused(&device, "_PSR", "the adapter has no _PSR"), + Err(_) if self.stopping => return lines, + Err(why) => self.refused(&device, "_PSR", &kind(&why)), + } + } + let ac_moved = self.ac_said != Some(ac); + self.ac_said = Some(ac); + let ac = match ac { + Some(true) => "AC online", + Some(false) => "AC offline", + None if self.adapters.is_empty() => "no AC adapter", + None => "AC unknown", + }; + let count = self.batteries.len(); + for n in 0..count { + let device = self.batteries[n].path.clone(); + let status = match self.evaluate(aml, &format!("{device}._BST")) { + Ok(Some(value)) => bst(&value), + Ok(None) => Err("the battery has no _BST".into()), + Err(_) if self.stopping => return lines, + Err(why) => Err(kind(&why)), + }; + let status = match status { + Ok(status) => status, + Err(why) => { + self.refused(&device, "_BST", &why); + continue; + } + }; + let battery = &mut self.batteries[n]; + let now = (percent(&battery.info, &status), status.state); + if battery.said != Some(now) || ac_moved { + battery.said = Some(now); + lines.push(format!("acpiserver: {}battery {} of {count}: {}; {ac}", acpiserver_api::BATTERY_READ, n + 1, said_status(&battery.info, &status))); + } + } + lines + } +} + +/// The interpreter's own test encodings of §20.2, which the namespace +/// tests below assemble a machine's tables from. +#[cfg(test)] +#[path = "../aml/tests/common/mod.rs"] +mod common; + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::aml::tests::{crafted, sealed, CRAFTED_RSDP}; + use crate::devices::tests::{byte, crs, GPE0}; + use crate::devices::{self, Found}; + use crate::ec::tests::Emulated; + use crate::host::tests::{Scripted, EC_COMMAND, EC_DATA}; + use crate::host::Take; + + use super::common; + use common::{device, field, if_, index, int, lequal, method, op_region, package, ret, scope, skip, store, string, unit as field_unit}; + + /// The controller's space as the fixture's fields lay it out. + pub const CONNECTED: u8 = 0x03; + const STATE: u8 = 0x38; + pub const AC: u8 = 0x46; + const PAGE: u8 = 0x81; + const RATE: u8 = 0xA0; + const VOLTAGE: u8 = 0xA8; + + /// A laptop's namespace as §10.2.2, §10.3, §6.5.4 and §12.11 have one: an + /// embedded controller at [`EC_DATA`] and [`EC_COMMAND`] whose `_REG` + /// records that its space is there, in itself and, as the T14's writes + /// offset 0x03, in the controller, and reads a byte of firmware memory + /// under the Global Lock; a battery below it whose `_STA` says it is + /// present only once `_REG` has run, whose `_BIX` is constant and whose + /// `_BST` reads that byte too, then selects a page of the controller's + /// space with a write and reads it; and an adapter whose `_PSR` reads + /// the controller. + /// `glk` is the controller's `_GLK`, where it has one; with a `button` + /// the machine's power button is a control method device (§4.8.2.2.1.2) + /// and the controller's query 0x28's method reads the adapter's byte. + fn laptop(glk: Option, button: bool) -> Vec { + let fields = [ + skip(8 * usize::from(CONNECTED)), + field_unit("REGS", 8), + skip(8 * usize::from(STATE - CONNECTED - 1)), + field_unit("BSTA", 8), + skip(8 * usize::from(AC - STATE - 1)), + field_unit("ACST", 8), + skip(8 * usize::from(PAGE - AC - 1)), + field_unit("PAGE", 8), + skip(8 * usize::from(RATE - PAGE - 1)), + field_unit("RATE", 16), + field_unit("REMN", 16), + skip(8 * usize::from(VOLTAGE - RATE - 4)), + field_unit("VOLT", 16), + ]; + let mut bix: Vec> = [1, 0, 57000, 50000, 1, 11520, 5700, 570, 312, 95000, 0xFFFF_FFFF, 0xFFFF_FFFF, 1000, 1000, 100, 100] + .map(int) + .to_vec(); + bix.extend(["MODEL", "SERIAL", "LION", "OEM"].map(string)); + bix.push(int(1)); + let bst = [ + store(b"NVSB", &[0x60]), + store(&int(1), b"PAGE"), + store(b"BSTA", &index(b"BSTP", &int(0), &[0])), + store(b"RATE", &index(b"BSTP", &int(1), &[0])), + store(b"REMN", &index(b"BSTP", &int(2), &[0])), + store(b"VOLT", &index(b"BSTP", &int(3), &[0])), + ret(b"BSTP"), + ] + .concat(); + let battery = device( + "BAT0", + &[ + common::def_name("_HID", &int(0x0A0C_D041)), + method("_STA", 0, &[if_(b"ECOK", &ret(&int(0x1F))), ret(&int(0x0F))].concat()), + method("_BIX", 0, &ret(&package(&bix))), + common::def_name("BSTP", &package(&[int(0), int(0), int(0), int(0)])), + method("_BST", 0, &bst), + ] + .concat(), + ); + let controller = device( + "EC0", + &[ + common::def_name("_HID", &int(0x090C_D041)), + crs(&[EC_DATA as u16, EC_COMMAND as u16]), + byte(b"_GPE", 3), + op_region("NVSR", 0x00, &int(NVS), &int(1)), + // ByteAcc, Lock (§20.2.5.2). + field("NVSR", 0x11, &[field_unit("NVSB", 8)]), + op_region("ECOR", 0x03, &int(0), &int(0x100)), + field("ECOR", 0x01, &fields), + common::def_name("ECOK", &int(0)), + method("_REG", 2, &if_(&lequal(&[0x68], &int(3)), &[store(b"NVSB", &[0x60]), store(&[0x69], b"ECOK"), store(&[0x69], b"REGS")].concat())), + glk.map_or(Vec::new(), |glk| common::def_name("_GLK", &int(glk))), + if button { method("_Q28", 0, &store(b"ACST", &common::local(0))) } else { Vec::new() }, + battery, + ] + .concat(), + ); + let adapter = device("AC", &[common::def_name("_HID", &string("ACPI0003")), method("_PSR", 0, &ret(&common::name("\\_SB.EC0.ACST")))].concat()); + let button = if button { device("PWRB", &common::def_name("_HID", &int(0x0C0C_D041))) } else { Vec::new() }; + sealed(b"DSDT", &scope("\\_SB", &[controller, adapter, button].concat())) + } + + /// What the fixture's controller holds: discharging at 6210 mW with + /// 43511 of 50000 mWh left at 12312 mV, off AC. + fn space() -> [u8; 256] { + let mut space = [0; 256]; + space[usize::from(STATE)] = 1; + let at = usize::from(RATE); + space[at..at + 2].copy_from_slice(&6210u16.to_le_bytes()); + space[at + 2..at + 4].copy_from_slice(&43511u16.to_le_bytes()); + let at = usize::from(VOLTAGE); + space[at..at + 2].copy_from_slice(&12312u16.to_le_bytes()); + space + } + + /// A byte of firmware memory `_REG` and `_BST` read first under the + /// Global Lock, as real ones read their NVS. + const NVS: u64 = 0x7700_0000; + + /// The fixture's machine, its controller answering at its two ports. + pub fn laptop_machine(glk: Option) -> Scripted { + machine(laptop(glk, false)) + } + + /// The fixture's machine with a control-method power button, and query + /// 0x28 waiting on its controller. + pub fn button_laptop_machine() -> Scripted { + let kernel = machine(laptop(None, true)); + kernel.controller.borrow_mut().as_mut().expect("a controller").queries.push(0x28); + kernel + } + + fn machine(dsdt: Vec) -> Scripted { + let mut kernel = crafted(&dsdt, &[]); + kernel.memory.push((NVS, 10, vec![0])); + *kernel.controller.borrow_mut() = Some(Emulated::new(space())); + kernel + } + + /// The fixture loaded and its controller found and attached, as the + /// server arms it ([`crate::attach`]). + fn armed(kernel: &Scripted) -> (Aml<'_, Scripted>, Found) { + let (_, aml) = crate::aml::load(kernel, CRAFTED_RSDP); + let mut aml = aml.expect("the fixture's DSDT loads"); + let found = devices::find(&mut aml.interpreter, &mut aml.host, GPE0, false); + crate::attach(&mut aml, found.ec.as_ref().expect("the fixture's controller")).expect("no port of it refused"); + (aml, found) + } + + fn found(kernel: &Scripted) -> (Aml<'_, Scripted>, Option) { + let (aml, power, _) = census(kernel); + (aml, power) + } + + fn census(kernel: &Scripted) -> (Aml<'_, Scripted>, Option, Option) { + let (mut aml, found) = armed(kernel); + let (power, census) = Power::find(&mut aml, found.batteries, found.adapters); + (aml, power, census) + } + + fn done(kernel: &Scripted) -> Vec<(u8, u8, u8)> { + kernel.controller.borrow().as_ref().expect("a controller").done.clone() + } + + fn poke(kernel: &Scripted, at: u8, bytes: &[u8]) { + let mut controller = kernel.controller.borrow_mut(); + let ec = controller.as_mut().expect("a controller"); + ec.space[usize::from(at)..usize::from(at) + bytes.len()].copy_from_slice(bytes); + } + + pub const RD: u8 = 0x80; + pub const WR: u8 = 0x81; + + /// The controller is attached before its `_REG` runs, which writes it, + /// and told its space is there before the battery is asked whether it + /// is present, which here it says only after; its information + /// is read once and the battery read with it, through the controller's + /// transactions alone, each through the kernel: the adapter's byte, then + /// the page `_BST` selects, written before its fields are read, a byte at + /// a time. + #[test] + fn a_laptops_battery_is_found_after_reg_and_read_through_the_controller() { + let kernel = laptop_machine(None); + let (aml, power) = found(&kernel); + let power = power.expect("a battery"); + assert_eq!(power.adapters, ["\\_SB_.AC__"]); + assert_eq!(power.batteries.len(), 1); + assert_eq!(power.batteries[0].path, "\\_SB_.EC0_.BAT0"); + assert_eq!( + power.batteries[0].info, + Info { from: "_BIX", unit: Unit::Power, design: Some(57000), full: Some(50000), design_voltage: Some(11520), cycles: Some(312) } + ); + assert_eq!( + done(&kernel), + [ + (WR, CONNECTED, 1), + (RD, AC, 0), + (WR, PAGE, 1), + (RD, STATE, 0), + (RD, RATE, 0), + (RD, RATE + 1, 0), + (RD, RATE + 2, 0), + (RD, RATE + 3, 0), + (RD, VOLTAGE, 0), + (RD, VOLTAGE + 1, 0), + ] + ); + assert_eq!(power.batteries[0].said, Some((Some(87), DISCHARGING))); + assert_eq!(power.ac_said, Some(Some(false))); + assert!(power.refused.is_empty() && aml.host.refused.is_empty()); + let asked = kernel.asked.borrow(); + assert!(asked.iter().all(|access| access.write == 0 || [EC_DATA, EC_COMMAND].contains(&access.address)), "the kernel was asked for a write but the controller's"); + } + + /// The census counts the Global Lock's takes of the battery's and + /// adapter's methods alone: `_BST`'s, which finds the firmware holding + /// it and waits out its release, and not the controller's `_REG`'s, + /// made as it was attached. + #[test] + fn the_census_counts_the_global_lock_of_the_power_sources_methods_alone() { + let kernel = laptop_machine(None); + *kernel.takes.borrow_mut() = [Take::Taken, Take::Pending, Take::Taken].into(); + *kernel.releases.borrow_mut() = [true].into(); + let (aml, power, census) = census(&kernel); + assert!(power.is_some(), "a battery"); + let census = census.expect("a census"); + let counted = "the Global Lock taken 1 times on the way and given back 1 times, the firmware found holding it by 1 of the takes,"; + assert!(census.contains(counted), "{census}"); + assert_eq!((aml.host.takes, aml.host.given_back, aml.host.contended), (2, 2, 1), "the controller's _REG took no lock of its own"); + assert!(kernel.takes.borrow().is_empty() && kernel.releases.borrow().is_empty() && !kernel.held.get()); + } + + /// A line is said only where what it says moved: the percent, the state + /// or the adapter; find said the first. + #[test] + fn a_reading_is_said_only_where_its_percent_its_state_or_the_adapter_moves() { + let kernel = laptop_machine(None); + let (mut aml, power) = found(&kernel); + let mut power = power.expect("a battery"); + assert_eq!(power.read(&mut aml), Vec::::new(), "nothing moved since find's reading"); + poke(&kernel, RATE, &7000u16.to_le_bytes()); + poke(&kernel, RATE + 2, &43600u16.to_le_bytes()); + assert_eq!(power.read(&mut aml), Vec::::new(), "87% discharging at another rate is no new line"); + poke(&kernel, RATE + 2, &43000u16.to_le_bytes()); + assert_eq!( + power.read(&mut aml), + ["acpiserver: battery read: battery 1 of 1: 86%, 43000 mWh of 50000 mWh, discharging, rate 7000 mW, 12312 mV, moving 7000 mW; AC offline"] + ); + poke(&kernel, AC, &[1]); + assert_eq!( + power.read(&mut aml), + ["acpiserver: battery read: battery 1 of 1: 86%, 43000 mWh of 50000 mWh, discharging, rate 7000 mW, 12312 mV, moving 7000 mW; AC online"] + ); + poke(&kernel, STATE, &[CHARGING as u8]); + assert_eq!( + power.read(&mut aml), + ["acpiserver: battery read: battery 1 of 1: 86%, 43000 mWh of 50000 mWh, charging, rate 7000 mW, 12312 mV, moving 7000 mW; AC online"] + ); + assert_eq!(power.read(&mut aml), Vec::::new()); + } + + /// §6.5.7: a controller whose `_GLK` is 1 wants the Global Lock around + /// each transaction, which this server does not take: its space is not + /// attached to AML, its `_REG` is not run, and the battery, which says + /// it is present only after, is not read, nor is the controller reached. + #[test] + fn a_controller_that_wants_the_global_lock_is_not_attached_and_no_battery_is_read() { + for (glk, attached) in [(Some(1), false), (Some(0), true), (None, true)] { + let kernel = laptop_machine(glk); + let (mut aml, power) = found(&kernel); + assert_eq!(power.is_some(), attached, "_GLK {glk:?}"); + assert_eq!(aml.host.ec.is_some(), attached, "_GLK {glk:?}"); + let ecok = aml.interpreter.evaluate(&mut aml.host, "\\_SB_.EC0_.ECOK", &[]); + assert_eq!(ecok, Ok(Value::Integer(u64::from(attached))), "_GLK {glk:?}"); + assert_eq!(done(&kernel).is_empty(), !attached, "_GLK {glk:?}: {:x?}", done(&kernel)); + } + } + + /// A `_BST` that is no package of four is said once and counted, and the + /// battery's last line stands. + #[test] + fn a_refused_reading_is_said_once_and_counted() { + let kernel = laptop_machine(None); + let (mut aml, power) = found(&kernel); + let mut power = power.expect("a battery"); + let said = power.batteries[0].said; + poke(&kernel, STATE, &[(CHARGING | DISCHARGING) as u8]); + for _ in 0..3 { + power.read(&mut aml); + } + assert_eq!(power.batteries[0].said, said); + assert_eq!(power.refused.counts(), "_BST: its state says charging and discharging at once, which §10.2.2 forbids x3"); + } + + /// A machine that stops while `_BST` reads its firmware memory ends the + /// read with nothing refused, and its battery's last line stands. + #[test] + fn a_machine_that_stops_mid_read_refuses_nothing() { + let kernel = laptop_machine(None); + let (mut aml, power) = found(&kernel); + let mut power = power.expect("a battery"); + let said = power.batteries[0].said; + kernel.stops_after.set(Some(0)); + power.read(&mut aml); + assert!(aml.host.stopping, "the read reached the kernel"); + assert_eq!(power.batteries[0].said, said); + assert!(power.refused.is_empty(), "{}", power.refused.counts()); + } + + fn integers(values: &[u64]) -> Vec { + values.iter().map(|&value| Value::Integer(value)).collect() + } + + /// A `_BIF` of §10.2.2's 13 elements: the numbers given, then the + /// granularities and the four strings. + fn bif_of(numbers: [u64; 9]) -> Value { + let mut elements = integers(&numbers); + elements.extend(["MODEL", "SERIAL", "LION", "OEM"].map(|s| Value::String(s.as_bytes().to_vec()))); + Value::Package(elements) + } + + /// A `_BIX` of `len` elements, revision first: Power Unit, Design + /// Capacity, Last Full Charge Capacity, Technology, Design Voltage, + /// warning and low capacities, Cycle Count, then the rest. + fn bix_of(revision: u64, numbers: [u64; 8], len: usize) -> Value { + let mut elements = integers(&[revision]); + elements.extend(integers(&numbers)); + elements.extend(integers(&[95000, 0xFFFF_FFFF, 0xFFFF_FFFF, 1000, 1000, 100, 100])); + elements.extend(["MODEL", "SERIAL", "LION", "OEM"].map(|s| Value::String(s.as_bytes().to_vec()))); + if len == 21 { + elements.push(Value::Integer(1)); + } + assert_eq!(elements.len(), len); + Value::Package(elements) + } + + #[test] + fn a_bif_is_read_by_its_place_in_the_package() { + let info = bif(&bif_of([0, 57000, 51230, 1, 11520, 2850, 570, 100, 100])).expect("a _BIF"); + assert_eq!( + info, + Info { from: "_BIF", unit: Unit::Power, design: Some(57000), full: Some(51230), design_voltage: Some(11520), cycles: None } + ); + assert_eq!(said_info(&info), "from _BIF: design 57000 mWh, last full 51230 mWh, design voltage 11520 mV, cycles unknown"); + } + + #[test] + fn a_bix_of_either_revision_is_read_at_the_same_places() { + for (revision, len) in [(0, 20), (1, 21)] { + let info = bix(&bix_of(revision, [1, 4000, 3600, 1, 15400, 400, 200, 312], len)).expect("a _BIX"); + assert_eq!( + info, + Info { from: "_BIX", unit: Unit::Current, design: Some(4000), full: Some(3600), design_voltage: Some(15400), cycles: Some(312) } + ); + assert_eq!(said_info(&info), "from _BIX: design 4000 mAh, last full 3600 mAh, design voltage 15400 mV, 312 cycles"); + } + } + + /// §10.2.2: 0xFFFFFFFF is a value the battery does not know, and is said + /// so, never as a number. + #[test] + fn an_unknown_dword_is_unknown_and_never_a_number() { + let info = bix(&bix_of(1, [0, UNKNOWN, UNKNOWN, 1, UNKNOWN, 0, 0, UNKNOWN], 21)).expect("a _BIX"); + assert_eq!((info.design, info.full, info.design_voltage, info.cycles), (None, None, None, None)); + assert_eq!(said_info(&info), "from _BIX: design unknown, last full unknown, design voltage unknown, cycles unknown"); + let status = bst(&Value::Package(integers(&[DISCHARGING, UNKNOWN, UNKNOWN, UNKNOWN]))).expect("a _BST"); + assert_eq!(said_status(&info, &status), "charge unknown, unknown of unknown, discharging, rate unknown, unknown, moving unknown mW"); + } + + #[test] + fn a_bst_reads_its_four_dwords_and_names_its_state() { + let info = bif(&bif_of([0, 57000, 50000, 1, 11520, 0, 0, 1, 1])).expect("a _BIF"); + let status = bst(&Value::Package(integers(&[DISCHARGING, 6210, 43511, 12312]))).expect("a _BST"); + assert_eq!(status, Status { state: DISCHARGING, rate: Some(6210), remaining: Some(43511), voltage: Some(12312) }); + assert_eq!(percent(&info, &status), Some(87), "rounded down"); + assert_eq!(milliwatts(&info, &status), Some(6210), "a rate in mW is the power"); + assert_eq!(said_status(&info, &status), "87%, 43511 mWh of 50000 mWh, discharging, rate 6210 mW, 12312 mV, moving 6210 mW"); + + let charging = bst(&Value::Package(integers(&[CHARGING | CRITICAL | LIMITING | 0x30, 0, 50000, 13000]))).expect("a _BST"); + assert_eq!(state(charging.state), "charging, critical, charge limiting, reserved bits 0x30"); + assert_eq!(state(0), "neither charging nor discharging"); + assert_eq!(percent(&info, &charging), Some(100)); + } + + /// A rate in mA moves the rate times the present voltage: mA × mV / 1000 + /// is mW. With no voltage known there is no power, and no design voltage + /// stands in for it. + #[test] + fn a_current_is_power_only_at_its_present_voltage() { + let info = bix(&bix_of(1, [1, 4000, 3600, 1, 15400, 0, 0, 1], 21)).expect("a _BIX"); + let status = bst(&Value::Package(integers(&[DISCHARGING, 543, 1800, 13672]))).expect("a _BST"); + assert_eq!(milliwatts(&info, &status), Some(543 * 13672 / 1000)); + assert_eq!(percent(&info, &status), Some(50)); + assert_eq!(said_status(&info, &status), "50%, 1800 mAh of 3600 mAh, discharging, rate 543 mA, 13672 mV, moving 7423 mW"); + let unknown_voltage = Status { voltage: None, ..status }; + assert_eq!(milliwatts(&info, &unknown_voltage), None); + } + + #[test] + fn a_full_charge_of_zero_is_no_percent() { + let info = bif(&bif_of([0, 57000, 0, 1, 11520, 0, 0, 1, 1])).expect("a _BIF"); + let status = bst(&Value::Package(integers(&[0, 0, 100, 12000]))).expect("a _BST"); + assert_eq!(percent(&info, &status), None); + } + + #[test] + fn a_package_that_is_not_what_10_2_2_says_is_refused_by_name() { + assert_eq!(bst(&Value::Package(integers(&[CHARGING | DISCHARGING, 0, 0, 0]))), Err("its state says charging and discharging at once, which §10.2.2 forbids".into())); + assert_eq!(bst(&Value::Package(integers(&[0, 0, 0]))), Err("a package of 3 elements, where §10.2.2 has at least 4".into())); + assert_eq!(bst(&Value::Integer(0)), Err("no package".into())); + assert_eq!(bst(&Value::Package(integers(&[0, 1 << 32, 0, 0]))), Err("its Battery Present Rate is wider than a DWORD".into())); + let mut stringy = integers(&[0, 0, 0]); + stringy.push(Value::String(b"12000".to_vec())); + assert_eq!(bst(&Value::Package(stringy)), Err("its Battery Present Voltage is no Integer".into())); + assert_eq!(bif(&bif_of([2, 0, 0, 0, 0, 0, 0, 0, 0])), Err("its Power Unit is 2, which §10.2.2 does not define".into())); + assert_eq!(bix(&bix_of(0, [0; 8], 20)).map(|info| info.unit), Ok(Unit::Power)); + let Value::Package(mut short) = bix_of(0, [0; 8], 20) else { unreachable!() }; + short.pop(); + assert_eq!(bix(&Value::Package(short)), Err("a package of 19 elements, where §10.2.2 has at least 20".into())); + } +} diff --git a/userland/acpiserver/src/devices.rs b/userland/acpiserver/src/devices.rs index 3337a608feb..2e14e0d4afa 100644 --- a/userland/acpiserver/src/devices.rs +++ b/userland/acpiserver/src/devices.rs @@ -20,6 +20,11 @@ //! **Buttons are looked for only where the FADT says the power button is a //! control method device**; on a machine whose button is the fixed one, a //! PNP0C0C the firmware lists anyway is not another way to press it. +//! +//! **Batteries (PNP0C0A) and AC adapters (ACPI0003) are found by the same +//! walk** and handed on by path ([`crate::battery`]), their `_STA` unasked: +//! a battery's says whether one is present only once the controller's +//! space is there. use std::collections::BTreeSet; @@ -47,13 +52,19 @@ pub struct Found { pub ec: Option, /// Every control-method power button present, by path. pub buttons: Vec, + /// Every control-method battery and AC adapter, by path. + pub batteries: Vec, + pub adapters: Vec, } const EC: &str = "PNP0C09"; const POWER_BUTTON: &str = "PNP0C0C"; +const BATTERY: &str = "PNP0C0A"; +const ADAPTER: &str = "ACPI0003"; -/// Find the controller, and the buttons where `control_method`, in the -/// namespace `interpreter` holds; `gpe0` is the block every GPE is in. +/// Find the controller, the buttons where `control_method`, and every +/// battery and adapter, in the namespace `interpreter` holds; `gpe0` is the +/// block every GPE is in. pub fn find(interpreter: &mut Interpreter, host: &mut dyn Host, gpe0: Block, control_method: bool) -> Found { let mut found = Found::default(); let (devices, named) = match devices(interpreter) { @@ -78,6 +89,14 @@ pub fn find(interpreter: &mut Interpreter, host: &mut dyn Host, gpe0: Block, con } }; let wanted = match id.as_deref() { + Some(BATTERY) => { + found.batteries.push(device); + continue; + } + Some(ADAPTER) => { + found.adapters.push(device); + continue; + } Some(EC) => true, Some(POWER_BUTTON) => control_method, _ => false, @@ -311,8 +330,8 @@ pub mod tests { #[test] fn an_amd_laptops_controller_and_button_are_found_in_its_dsdt() { let ec = Ec { path: EC_PATH.into(), command: 0x66, data: 0x62, gpe: 3, queries: BTreeSet::new() }; - assert_eq!(found(&laptop(&[]), true), Found { ec: Some(ec.clone()), buttons: vec![PWRB.into()] }); - assert_eq!(found(&laptop(&[]), false), Found { ec: Some(ec), buttons: vec![] }, "a fixed button's machine looks for none"); + assert_eq!(found(&laptop(&[]), true), Found { ec: Some(ec.clone()), buttons: vec![PWRB.into()], ..Found::default() }); + assert_eq!(found(&laptop(&[]), false), Found { ec: Some(ec), ..Found::default() }, "a fixed button's machine looks for none"); } /// An AMD laptop's `_Q28`, decoded: its query's number stored to the POST diff --git a/userland/acpiserver/src/ec.rs b/userland/acpiserver/src/ec.rs index 3ce447d1725..d84019968e8 100644 --- a/userland/acpiserver/src/ec.rs +++ b/userland/acpiserver/src/ec.rs @@ -3,10 +3,11 @@ //! answers what its host does next, so how long a host waits, and how, is the //! host's. //! -//! A transaction is a command byte written to the command register and the one -//! byte read back, the write once the controller's input buffer is empty and -//! the read once its output buffer is full. Stage 1 asks only for queries; -//! reads and writes of the controller's space join when AML reaches it. +//! A transaction is a command byte written to the command register, then the +//! bytes its command takes written to the data register, each once the +//! controller's input buffer is empty; then a read of the one byte it answers +//! once its output buffer is full, or, where it answers none, a wait until +//! the controller has taken the last byte written. /// The status register's bits (Table 12.3). pub const OBF: u8 = 1 << 0; @@ -14,8 +15,12 @@ pub const IBF: u8 = 1 << 1; /// An event is waiting to be queried. pub const SCI_EVT: u8 = 1 << 5; +/// Table 12.6: read a byte of the controller's space (§12.3.1). +const RD_EC: u8 = 0x80; +/// Table 12.6: write a byte of the controller's space (§12.3.2). +const WR_EC: u8 = 0x81; /// Table 12.6: query, which answers the number of the event waiting, or 0 -/// for none. +/// for none (§12.3.5). const QR_EC: u8 = 0x84; /// What the host does next. @@ -24,9 +29,10 @@ pub enum Do { /// Read the status register again, until it says otherwise. Wait(Wait), WriteCommand(u8), + WriteData(u8), /// Read the data register and hand the byte to [`Transaction::read`]. ReadData, - /// Finished, with the byte read back. + /// Finished, with the byte read back, or the byte written. Done(u8), } @@ -39,34 +45,65 @@ pub enum Wait { #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Phase { - Command, + /// Writing the byte at this index of `bytes`. + Send(usize), Answer, + /// The last byte is written, and the controller has not taken it yet. + Settle, Done(u8), } /// One transaction, from its command to its answer. #[derive(Debug)] pub struct Transaction { - command: u8, + /// The command, then what it takes. + bytes: [u8; 3], + len: usize, + answers: bool, phase: Phase, } impl Transaction { + fn new(sent: &[u8], answers: bool) -> Self { + let mut bytes = [0; 3]; + bytes[..sent.len()].copy_from_slice(sent); + Self { bytes, len: sent.len(), answers, phase: Phase::Send(0) } + } + /// Ask for the waiting event's number. pub fn query() -> Self { - Self { command: QR_EC, phase: Phase::Command } + Self::new(&[QR_EC], true) + } + + /// The byte at `address` of the controller's space. + pub fn read_at(address: u8) -> Self { + Self::new(&[RD_EC, address], true) + } + + /// `value` to `address` of the controller's space. + pub fn write_at(address: u8, value: u8) -> Self { + Self::new(&[WR_EC, address, value], false) } /// What to do, given what the status register reads now. pub fn step(&mut self, status: u8) -> Do { match self.phase { - Phase::Command if status & IBF != 0 => Do::Wait(Wait::InputEmpty), - Phase::Command => { - self.phase = Phase::Answer; - Do::WriteCommand(self.command) + Phase::Send(_) | Phase::Settle if status & IBF != 0 => Do::Wait(Wait::InputEmpty), + Phase::Send(i) => { + self.phase = match i + 1 { + next if next < self.len => Phase::Send(next), + _ if self.answers => Phase::Answer, + _ => Phase::Settle, + }; + if i == 0 { Do::WriteCommand(self.bytes[0]) } else { Do::WriteData(self.bytes[i]) } } Phase::Answer if status & OBF == 0 => Do::Wait(Wait::OutputFull), Phase::Answer => Do::ReadData, + Phase::Settle => { + let written = self.bytes[self.len - 1]; + self.phase = Phase::Done(written); + Do::Done(written) + } Phase::Done(byte) => Do::Done(byte), } } @@ -79,11 +116,11 @@ impl Transaction { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; - /// A controller as Table 12.3 has it: the host's write fills the input - /// buffer, the controller takes it and later fills the output buffer. + /// A host that runs `tx` against a controller saying `statuses` in turn + /// and answering `answer` to a read of its data register. fn run(mut tx: Transaction, statuses: &[u8], answer: u8) -> (Vec, u8) { let mut said = Vec::new(); let mut statuses = statuses.iter().copied(); @@ -94,11 +131,106 @@ mod tests { match next { Do::ReadData => tx.read(answer), Do::Done(byte) => return (said, byte), - Do::Wait(_) | Do::WriteCommand(_) => {} + Do::Wait(_) | Do::WriteCommand(_) | Do::WriteData(_) => {} } } } + /// A controller as §12.2 and §12.3 have it: 256 bytes of space, and + /// SCI_EVT set while one of its `queries` waits; a byte + /// written fills the input buffer until the controller takes it, which + /// it does `slow` status reads later; a command's answer fills the output + /// buffer as it takes the command's last byte. It refuses, by panic, + /// a write while its input buffer is full, a read of its data register + /// while its output buffer is empty, and a data byte no command asked for. + #[derive(Debug)] + pub struct Emulated { + pub space: [u8; 256], + pub slow: u32, + pending: u32, + input: bool, + output: Option, + /// The command taken, and the bytes it has taken after it. + taken: Vec, + pub queries: Vec, + /// Every command taken whole: `(command, address, value)`. + pub done: Vec<(u8, u8, u8)>, + } + + impl Emulated { + pub fn new(space: [u8; 256]) -> Self { + Emulated { space, slow: 2, pending: 0, input: false, output: None, taken: Vec::new(), queries: Vec::new(), done: Vec::new() } + } + + pub fn status(&mut self) -> u8 { + if self.input { + if self.pending == 0 { + self.input = false; + self.take(); + } else { + self.pending -= 1; + } + } + u8::from(self.output.is_some()) * OBF | u8::from(self.input) * IBF | u8::from(!self.queries.is_empty()) * SCI_EVT + } + + fn take(&mut self) { + let wants = match self.taken[0] { + RD_EC => 2, + WR_EC => 3, + QR_EC => 1, + other => panic!("the controller was sent command {other:#04x}"), + }; + if self.taken.len() < wants { + return; + } + let (command, address, value) = (self.taken[0], self.taken.get(1).copied().unwrap_or(0), self.taken.get(2).copied().unwrap_or(0)); + match command { + RD_EC => self.output = Some(self.space[usize::from(address)]), + WR_EC => self.space[usize::from(address)] = value, + _ => self.output = Some(self.queries.pop().unwrap_or(0)), + } + self.done.push((command, address, value)); + self.taken.clear(); + } + + fn written(&mut self, byte: u8) { + assert!(!self.input, "a byte was written while the controller's input buffer was full"); + self.input = true; + self.pending = self.slow; + self.taken.push(byte); + } + + pub fn command(&mut self, byte: u8) { + assert!(self.taken.is_empty(), "a command was written inside another's transaction"); + self.written(byte); + } + + pub fn data(&mut self, byte: u8) { + assert!(!self.taken.is_empty(), "a data byte was written that no command asked for"); + self.written(byte); + } + + pub fn read(&mut self) -> u8 { + self.output.take().expect("the data register was read with the output buffer empty") + } + + /// One transaction, driven as the server's host drives it. + pub fn transact(&mut self, mut tx: Transaction) -> u8 { + for _ in 0..1000 { + let status = self.status(); + match tx.step(status) { + Do::Wait(_) => {} + Do::WriteCommand(byte) => self.command(byte), + Do::WriteData(byte) => self.data(byte), + Do::ReadData => tx.read(self.read()), + Do::Done(byte) => return byte, + } + } + panic!("the transaction never ended: {tx:?} against {self:?}"); + } + } + #[test] fn a_query_writes_its_command_once_the_input_is_empty_and_reads_once_the_output_is_full() { let (said, byte) = run(Transaction::query(), &[IBF, IBF, 0, IBF, 0, OBF, 0], 0x1c); @@ -117,9 +249,78 @@ mod tests { assert_eq!(byte, 0x1c); } + /// §12.3.1: RD_EC, then the address once the command is taken, then the + /// byte once the output buffer is full. + #[test] + fn a_read_writes_its_address_only_once_the_command_is_taken() { + let (said, byte) = run(Transaction::read_at(0xA8), &[IBF, 0, IBF, IBF, 0, 0, OBF, 0], 0x5A); + assert_eq!( + said, + [ + Do::Wait(Wait::InputEmpty), + Do::WriteCommand(0x80), + Do::Wait(Wait::InputEmpty), + Do::Wait(Wait::InputEmpty), + Do::WriteData(0xA8), + Do::Wait(Wait::OutputFull), + Do::ReadData, + Do::Done(0x5A), + ] + ); + assert_eq!(byte, 0x5A); + } + + /// §12.3.2: WR_EC, the address, the value, each once the input buffer is + /// empty, and done only once the controller has taken the value. + #[test] + fn a_write_ends_once_the_controller_has_taken_its_value() { + let (said, byte) = run(Transaction::write_at(0x81, 0x01), &[0, IBF, 0, 0, IBF, IBF, OBF], 0); + assert_eq!( + said, + [ + Do::WriteCommand(0x81), + Do::Wait(Wait::InputEmpty), + Do::WriteData(0x81), + Do::WriteData(0x01), + Do::Wait(Wait::InputEmpty), + Do::Wait(Wait::InputEmpty), + Do::Done(0x01), + ], + "a full output buffer is no answer a write waits for" + ); + assert_eq!(byte, 0x01); + } + + #[test] + fn every_transaction_runs_against_a_controller_that_takes_each_byte_late() { + let mut space = [0; 256]; + space[0xA0] = 0x34; + for slow in [0, 1, 5] { + let mut ec = Emulated::new(space); + ec.slow = slow; + assert_eq!(ec.transact(Transaction::read_at(0xA0)), 0x34); + assert_eq!(ec.transact(Transaction::write_at(0x81, 0x02)), 0x02); + assert_eq!(ec.space[0x81], 0x02); + ec.queries.push(0x1C); + assert_eq!(ec.transact(Transaction::query()), 0x1C); + assert_eq!(ec.transact(Transaction::query()), 0, "no event waiting"); + assert_eq!(ec.done, [(0x80, 0xA0, 0), (0x81, 0x81, 0x02), (0x84, 0, 0), (0x84, 0, 0)]); + } + } + #[test] #[should_panic(expected = "a byte read that nothing asked for")] fn a_byte_before_the_command_is_refused() { Transaction::query().read(0); } + + #[test] + #[should_panic(expected = "a byte read that nothing asked for")] + fn a_write_reads_no_byte() { + let mut tx = Transaction::write_at(0, 0); + for _ in 0..3 { + tx.step(0); + } + tx.read(0); + } } diff --git a/userland/acpiserver/src/host.rs b/userland/acpiserver/src/host.rs index d9a89e1dcf4..71086a49a11 100644 --- a/userland/acpiserver/src/host.rs +++ b/userland/acpiserver/src/host.rs @@ -12,14 +12,23 @@ //! nothing but an index or a POST code. Every other port write is denied by //! name and never reaches the kernel: a reset-capable port, `SMI_CMD`, a data //! register. The reset register is the kernel's to keep wherever it is. A -//! write to memory or to configuration space is denied by name, and so is -//! every access to the embedded controller's space, which no transaction -//! serves for AML yet. SystemCMOS never arrives: the interpreter refuses that -//! space itself. +//! write to memory or to configuration space is denied by name. SystemCMOS +//! never arrives: the interpreter refuses that space itself. //! -//! **The Global Lock is the kernel's to exchange** (ACPI 6.5 §5.2.10.1). A -//! take that finds the firmware holding it is denied by name ([`HELD`]) and -//! counted: this server waits for no release of the firmware's yet. +//! **The embedded controller's space is its own transactions'** ([`crate::ec`], +//! [`transact`]), a byte each, on the controller the server attached +//! ([`Firmware::ec`]) and through the kernel, which no row holds its ports +//! for. With none attached the space is denied by name. A port of it the +//! kernel refuses detaches the controller and is handed to the server +//! ([`Firmware::ended`]), which serves it no more. +//! +//! **The Global Lock's word is the kernel's to exchange** (ACPI 6.5 +//! §5.2.10.1). A take that finds the firmware holding it leaves the firmware +//! the pending bit, waits for the release that bit asks for +//! ([`Kernel::released`]) and takes again, and is counted. A take the AML +//! bounds, an Acquire's, comes back untaken at its bound; one it does not +//! ends this server loudly where the firmware holds the lock past +//! [`RELEASE`]. //! //! **A Notify of 0x80 to a control-method power button is a press** (ACPI //! 6.5 §4.8.2.2.1.2), counted in [`Firmware::presses`] for the server to @@ -42,14 +51,22 @@ use std::time::{Duration, Instant}; use toyos_abi::acpi::{pci_address, Access, Refused, Space, Width, UNLISTED}; use toyos_aml::{Address, Denied, Host}; +use crate::devices::Ec; +use crate::ec::{Do, Transaction, Wait}; use crate::ledger::Ledger; /// What opens a line that carries an address, a PCI function or a name the /// firmware chose: this machine's own, and quoted in no record. pub const OWN: &str = "acpiserver: (this machine's own, quoted in no record) "; -/// The denial of a take that found the firmware holding the Global Lock. -pub const HELD: &str = "the Global Lock: the firmware holds it, and this server waits for no release yet"; +/// The most the firmware may hold the Global Lock after a take the AML set no +/// bound on left it the pending bit: this server's bound, and no measurement, +/// over a lock SMM holds for one handler's run. +pub const RELEASE: Duration = Duration::from_secs(1); + +/// The most a controller is waited for at one step of a transaction: one that +/// has not moved in this will not. +const EC_STEP: Duration = Duration::from_millis(500); /// The port writes asked of the kernel, by port and width: the index /// registers of the CMOS's upper bank and of the AMD FCH's power-management @@ -89,12 +106,67 @@ pub trait Kernel { fn access(&self, access: Access) -> Result; fn lock_take(&self) -> Result; fn lock_release(&self) -> Result<(), Stopping>; + /// Waits until `until` for the firmware to give back a Global Lock a + /// take left it the pending bit in ([`crate::sci::await_release`]): + /// `false` is the deadline. + fn released(&self, until: Instant) -> bool; /// Hand the kernel `\_S5`'s `SLP_TYPa` for its power-off, which it takes /// once under a claim: `false` where it is wider than the register's /// field, and the kernel kept nothing. fn s5(&self, slp_typ_a: u64) -> Result; } +/// Why an access to one of the controller's ports was not made. +pub enum Unmade { + Stopping, + /// The kernel keeps the port from this server, said as it refused it. + Refused(String), +} + +/// A byte of one of the controller's ports, which no row holds. +pub fn port_in(kernel: &impl Kernel, port: u16) -> Result { + let answer = kernel.access(Access::read(Space::SystemIo, u64::from(port), Width::Byte)).map_err(|Stopping| Unmade::Stopping)?; + answer.made.map(|byte| byte as u8).map_err(|refused| Unmade::Refused(format!("the kernel refused a read of its port {port:#x}, {refused:?}"))) +} + +fn port_out(kernel: &impl Kernel, port: u16, byte: u8) -> Result<(), Unmade> { + let answer = kernel.access(Access::write(Space::SystemIo, u64::from(port), Width::Byte, u64::from(byte))).map_err(|Stopping| Unmade::Stopping)?; + answer.made.map(drop).map_err(|refused| Unmade::Refused(format!("the kernel refused a write of its port {port:#x}, {refused:?}"))) +} + +/// One transaction on the controller at `command` and `data`, waiting on it at +/// each step for at most [`EC_STEP`]. +pub fn transact(kernel: &impl Kernel, command: u16, data: u16, mut tx: Transaction) -> Result { + let mut waiting: Option<(Wait, Instant)> = None; + loop { + let status = port_in(kernel, command)?; + match tx.step(status) { + Do::Wait(wait) => { + let since = match waiting { + Some((was, since)) if was == wait => since, + _ => Instant::now(), + }; + assert!(since.elapsed() < EC_STEP, "acpiserver: the embedded controller kept {wait:?} unmet for {EC_STEP:?} (EC_SC {status:#04x})"); + waiting = Some((wait, since)); + std::thread::yield_now(); + } + Do::WriteCommand(byte) => { + waiting = None; + port_out(kernel, command, byte)?; + } + Do::WriteData(byte) => { + waiting = None; + port_out(kernel, data, byte)?; + } + Do::ReadData => { + waiting = None; + tx.read(port_in(kernel, data)?); + } + Do::Done(byte) => return Ok(byte), + } + } +} + /// Why a read was not made. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Refusal { @@ -172,10 +244,17 @@ pub struct Firmware<'k, K> { /// Reads made, by [`Space`]. pub reads: [u64; 3], pub pages: Pages, - /// Takes of the Global Lock, and how many found the firmware holding it. + /// Takes of the Global Lock, how many found the firmware holding it, + /// and gives back. pub takes: u64, pub contended: u64, + pub given_back: u64, pub notifies: u64, + /// The controller whose space AML reaches, once the server attached it. + pub ec: Option, + /// Why a port of the attached controller was refused, which detached it, + /// for the server to serve it no more. + pub ended: Option, pub refused: Ledger, notified: Ledger, /// The [`FORWARDED`] writes asked of the kernel, by port and width. @@ -197,7 +276,10 @@ impl<'k, K: Kernel> Firmware<'k, K> { pages: Pages::default(), takes: 0, contended: 0, + given_back: 0, notifies: 0, + ec: None, + ended: None, refused: Ledger::default(), notified: Ledger::default(), written: Ledger::default(), @@ -221,6 +303,52 @@ impl<'k, K: Kernel> Firmware<'k, K> { self.stopping = true; Denied(Refusal::Stopping.to_string()) } + + /// One transaction on the attached controller for AML's `what` at `at`, + /// or its denial by name where none is attached or the kernel refused + /// one of its ports, which detaches it. + fn controller(&mut self, what: &str, at: Address, width: toyos_aml::Access, tx: Transaction) -> Result { + assert_eq!(width, toyos_aml::Access::Byte, "the interpreter reaches EmbeddedControl a byte at a time (Table 19.34)"); + let Some(ec) = &self.ec else { + return Err(self.deny(format!("{what} {NO_CONTROLLER}"), format_args!("{at:x?}"))); + }; + match transact(self.kernel, ec.command, ec.data, tx) { + Ok(byte) => Ok(byte), + Err(Unmade::Stopping) => Err(self.stopped()), + Err(Unmade::Refused(why)) => { + self.ec = None; + let denied = self.deny(format!("{what} EmbeddedControl: the kernel refused a port of the controller"), format_args!("{at:x?}: {why}")); + self.ended = Some(why); + Err(denied) + } + } + } + + /// A take of the Global Lock, waiting for the firmware's release until + /// `until` wherever it finds the firmware holding it: `false` is the + /// deadline, and nothing taken. + fn take(&mut self, until: Instant) -> Result { + self.takes += 1; + let mut contended = false; + loop { + match self.kernel.lock_take() { + Err(Stopping) => return Err(self.stopped()), + Ok(Take::Taken) => return Ok(true), + Ok(Take::Unusable) => { + let what = "the Global Lock: the kernel exchanges no lock word in this machine's FACS"; + return Err(self.deny(what.into(), format_args!("a take"))); + } + Ok(Take::Pending) => {} + } + if !contended { + contended = true; + self.contended += 1; + } + if !self.kernel.released(until) { + return Ok(false); + } + } + } } fn wide(width: toyos_aml::Access) -> Width { @@ -232,7 +360,7 @@ fn wide(width: toyos_aml::Access) -> Width { } } -const NO_CONTROLLER: &str = "EmbeddedControl: this server runs no controller transaction for AML yet"; +const NO_CONTROLLER: &str = "EmbeddedControl: no controller is attached to this server's AML"; impl Host for Firmware<'_, K> { fn read(&mut self, at: Address, width: toyos_aml::Access) -> Result { @@ -240,7 +368,7 @@ impl Host for Firmware<'_, K> { Address::Memory(address) => (Space::SystemMemory, address), Address::Io(port) => (Space::SystemIo, u64::from(port)), Address::PciConfig { segment, bus, device, function, offset } => (Space::PciConfig, pci_address(segment, bus, device, function, offset)), - Address::EmbeddedControl(_) => return Err(self.deny(format!("a read of {NO_CONTROLLER}"), format_args!("{at:x?}"))), + Address::EmbeddedControl(address) => return self.controller("a read of", at, width, Transaction::read_at(address)).map(u64::from), }; match read(self.kernel, space, address, wide(width)) { Ok((value, memory_type)) => { @@ -273,7 +401,10 @@ impl Host for Firmware<'_, K> { } Address::Memory(_) => "a write to SystemMemory: this server writes no memory for AML".into(), Address::PciConfig { .. } => "a write to PCI_Config: this server writes no configuration space for AML".into(), - Address::EmbeddedControl(_) => format!("a write to {NO_CONTROLLER}"), + Address::EmbeddedControl(address) => { + let byte = u8::try_from(value).expect("a byte access writes a byte"); + return self.controller("a write to", at, width, Transaction::write_at(address, byte)).map(drop); + } }; Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) } @@ -308,22 +439,21 @@ impl Host for Firmware<'_, K> { } } - fn global_lock(&mut self, take: bool) -> Result<(), Denied> { - if !take { - return self.kernel.lock_release().map_err(|Stopping| self.stopped()); - } - self.takes += 1; - match self.kernel.lock_take() { - Err(Stopping) => Err(self.stopped()), - Ok(Take::Taken) => Ok(()), - Ok(Take::Unusable) => { - Err(self.deny("the Global Lock: the kernel exchanges no lock word in this machine's FACS".into(), format_args!("a take"))) - } - Ok(Take::Pending) => { - self.contended += 1; - Err(self.deny(HELD.into(), format_args!("a take the firmware was left the request for"))) - } + fn global_take(&mut self) -> Result<(), Denied> { + if self.take(Instant::now() + RELEASE)? { + return Ok(()); } + panic!("acpiserver: the firmware held the Global Lock {RELEASE:?} after a take left it the pending bit, and raised no GBL_STS"); + } + + fn global_take_within(&mut self, ms: u16) -> Result { + self.take(Instant::now() + Duration::from_millis(ms.into())) + } + + fn global_release(&mut self) -> Result<(), Denied> { + self.kernel.lock_release().map_err(|Stopping| self.stopped())?; + self.given_back += 1; + Ok(()) } } @@ -333,6 +463,11 @@ pub mod tests { use std::collections::VecDeque; use super::*; + use crate::ec::tests::Emulated; + + /// The controller's ports as an AMD laptop's `_CRS` names them. + pub const EC_DATA: u64 = 0x62; + pub const EC_COMMAND: u64 = 0x66; /// A kernel that answers reads from bytes at addresses, each range with a /// memory type, refuses `kept` ranges as the real one refuses memory of a @@ -349,6 +484,13 @@ pub mod tests { /// What each take answers, in turn; `Taken` once it runs out. pub takes: RefCell>, pub held: Cell, + /// What each wait for the firmware's release answers, in turn; the + /// deadline once it runs out. Each deadline asked. + pub releases: RefCell>, + pub waited: RefCell>, + /// A controller at [`EC_DATA`] and [`EC_COMMAND`], answering its two + /// ports before [`Self::ports`] is asked. + pub controller: RefCell>, /// Accesses and lock exchanges answered before the machine stops. pub stops_after: Cell>, /// The `SLP_TYPa` it was handed and kept. @@ -392,6 +534,23 @@ pub mod tests { }, } } + Space::SystemIo if self.controller.borrow().is_some() && [EC_DATA, EC_COMMAND].contains(&access.address) => { + let mut controller = self.controller.borrow_mut(); + let ec = controller.as_mut().expect("just seen"); + let read = match (access.address, access.write != 0) { + (EC_COMMAND, false) => ec.status(), + (_, false) => ec.read(), + (EC_COMMAND, true) => { + ec.command(access.value as u8); + 0 + } + (_, true) => { + ec.data(access.value as u8); + 0 + } + }; + Answer { made: Ok(u64::from(read)), memory_type: UNLISTED } + } Space::SystemIo => Answer { made: listed(&self.ports).ok_or(Refused::KernelPort), memory_type: UNLISTED }, Space::PciConfig => Answer { made: listed(&self.config).ok_or(Refused::ConfigUnreachable), memory_type: UNLISTED }, }) @@ -412,6 +571,12 @@ pub mod tests { Ok(()) } + fn released(&self, until: Instant) -> bool { + assert!(!self.held.get(), "a release waited for under a lock already held"); + self.waited.borrow_mut().push(until); + self.releases.borrow_mut().pop_front().unwrap_or(false) + } + fn s5(&self, slp_typ_a: u64) -> Result { self.stopping()?; assert!(!self.held.get(), "the sleep type was handed over under the Global Lock"); @@ -491,8 +656,8 @@ pub mod tests { /// A port write in [`FORWARDED`] is asked of the kernel, and made or /// refused as it answers; every other port and width, memory and - /// configuration space are denied by name, and the controller's space is - /// not reached, none of them asked of the kernel. + /// configuration space are denied by name, and so is the controller's + /// space with no controller attached, none of them asked of the kernel. #[test] fn only_a_forwarded_port_write_is_asked_of_the_kernel() { use toyos_aml::Access::{Byte, DWord, Word}; @@ -534,6 +699,55 @@ pub mod tests { assert_eq!(host.written.counts(), "0x72 Byte x1; 0x80 Word x1; 0xcd6 Byte x1"); } + /// The controller's space is its own transactions', one a byte, through + /// the kernel at the attached controller's ports: a read is RD_EC of its + /// address, a write WR_EC of its address and value. + #[test] + fn the_controllers_space_is_read_and_written_through_its_transactions() { + use toyos_aml::Access::Byte; + let kernel = machine(); + let mut space = [0; 256]; + space[0xA8] = 0x5A; + *kernel.controller.borrow_mut() = Some(Emulated::new(space)); + let mut host = Firmware::new(&kernel); + host.ec = Some(Ec { path: "\\_SB_.EC0_".into(), command: EC_COMMAND as u16, data: EC_DATA as u16, gpe: 3, queries: Default::default() }); + assert_eq!(host.read(Address::EmbeddedControl(0xA8), Byte), Ok(0x5A)); + assert_eq!(host.write(Address::EmbeddedControl(0x81), Byte, 0x01), Ok(())); + assert_eq!(host.read(Address::EmbeddedControl(0x81), Byte), Ok(0x01)); + assert_eq!(kernel.controller.borrow().as_ref().expect("a controller").done, [(0x80, 0xA8, 0), (0x81, 0x81, 0x01), (0x80, 0x81, 0)]); + let written: Vec<(u64, u64)> = kernel.asked.borrow().iter().filter(|access| access.write == 1).map(|access| (access.address, access.value)).collect(); + assert_eq!(written, [(EC_COMMAND, 0x80), (EC_DATA, 0xA8), (EC_COMMAND, 0x81), (EC_DATA, 0x81), (EC_DATA, 0x01), (EC_COMMAND, 0x80), (EC_DATA, 0x81)]); + assert_eq!(host.reads, [0, 0, 0], "a byte of the controller's space counted as a port read of AML's"); + assert!(host.refused.is_empty() && host.ended.is_none()); + } + + /// A port of the attached controller the kernel refuses denies the access + /// by name, detaches the controller and hands the server why; the next + /// access is denied as one with none attached, and asks the kernel + /// nothing. A stopping machine is no refusal and detaches nothing. + #[test] + fn a_controller_port_the_kernel_refuses_detaches_the_controller() { + use toyos_aml::Access::Byte; + let kernel = machine(); + let mut host = Firmware::new(&kernel); + let ec = Ec { path: "\\_SB_.EC0_".into(), command: EC_COMMAND as u16, data: EC_DATA as u16, gpe: 3, queries: Default::default() }; + host.ec = Some(ec.clone()); + let refused = host.read(Address::EmbeddedControl(0xA8), Byte); + assert_eq!(refused, Err(Denied("a read of EmbeddedControl: the kernel refused a port of the controller".into()))); + assert_eq!(host.ec, None); + assert_eq!(host.ended.as_deref(), Some("the kernel refused a read of its port 0x66, KernelPort")); + let asked = kernel.asked.borrow().len(); + assert_eq!(host.write(Address::EmbeddedControl(0x81), Byte, 1), Err(Denied(format!("a write to {NO_CONTROLLER}")))); + assert_eq!(kernel.asked.borrow().len(), asked, "a detached controller was asked again"); + + let kernel = machine(); + kernel.stops_after.set(Some(0)); + let mut host = Firmware::new(&kernel); + host.ec = Some(ec); + assert_eq!(host.read(Address::EmbeddedControl(0xA8), Byte), Err(Denied(Refusal::Stopping.to_string()))); + assert!(host.stopping && host.ec.is_some() && host.ended.is_none()); + } + /// A Notify of 0x80 to a button the server serves is a press, and to /// anything else, or of another value, is none. #[test] @@ -556,29 +770,63 @@ pub mod tests { fn the_lock_is_taken_and_given_back_through_the_kernel() { let kernel = machine(); let mut host = Firmware::new(&kernel); - assert_eq!(host.global_lock(true), Ok(())); + assert_eq!(host.global_take(), Ok(())); assert!(kernel.held.get()); - assert_eq!(host.global_lock(false), Ok(())); + assert_eq!(host.global_release(), Ok(())); assert!(!kernel.held.get()); - assert_eq!((host.takes, host.contended), (1, 0)); + assert_eq!((host.takes, host.contended, host.given_back), (1, 0, 1)); + assert!(kernel.waited.borrow().is_empty(), "a take the kernel answered taken waited"); assert!(host.refused.is_empty()); } + /// §5.2.10.1: a take that finds the firmware holding the lock leaves it + /// the pending bit and waits for the release that asks for; each release + /// is followed by a take again, which may find the firmware holding it + /// again. #[test] - fn a_lock_the_firmware_holds_is_denied_by_name_and_counted() { + fn a_lock_the_firmware_holds_is_waited_for_and_taken_after_its_release() { let kernel = machine(); kernel.takes.borrow_mut().extend([Take::Pending, Take::Pending, Take::Taken]); + kernel.releases.borrow_mut().extend([true, true]); let mut host = Firmware::new(&kernel); - for _ in 0..2 { - assert_eq!(host.global_lock(true), Err(Denied(HELD.into()))); - assert!(!kernel.held.get(), "a lock the firmware holds was reported taken"); + let asked = Instant::now(); + assert_eq!(host.global_take(), Ok(())); + assert!(kernel.held.get()); + assert!(kernel.takes.borrow().is_empty() && kernel.releases.borrow().is_empty()); + let waited = kernel.waited.borrow(); + assert_eq!(waited.len(), 2); + assert!(waited.iter().all(|&until| until == waited[0] && until >= asked + RELEASE), "{waited:?}: not one deadline RELEASE after the take"); + assert_eq!(host.global_release(), Ok(())); + assert_eq!((host.takes, host.contended, host.given_back), (1, 1, 1)); + assert!(host.refused.is_empty()); + } + + /// §19.6.2: an Acquire's TimeoutValue bounds the wait, however far past + /// [`RELEASE`], and one that meets it comes back untaken, holding nothing. + #[test] + fn an_acquires_own_bound_ends_the_wait_untaken() { + for ms in [5, 1000, 0xFFFE] { + let kernel = machine(); + kernel.takes.borrow_mut().push_back(Take::Pending); + let mut host = Firmware::new(&kernel); + let asked = Instant::now(); + assert_eq!(host.global_take_within(ms), Ok(false)); + let returned = Instant::now(); + assert!(!kernel.held.get()); + let until = kernel.waited.borrow()[0]; + let bound = Duration::from_millis(ms.into()); + assert!(until >= asked + bound && until <= returned + bound, "{until:?} is not {bound:?} after the take at {asked:?}"); + assert_eq!((host.takes, host.contended, host.given_back), (1, 1, 0)); } - assert_eq!(kernel.takes.borrow().len(), 1, "a denied take asked the kernel again"); - assert_eq!(host.refused.counts(), format!("{HELD} x2")); - // The firmware let go: the next take is the kernel's answer again. - assert_eq!(host.global_lock(true), Ok(())); - assert_eq!(host.global_lock(false), Ok(())); - assert_eq!((host.takes, host.contended), (3, 2)); + } + + #[test] + #[should_panic(expected = "acpiserver: the firmware held the Global Lock 1s after a take left it the pending bit, and raised no GBL_STS")] + fn a_firmware_that_holds_the_lock_past_the_bound_ends_the_server() { + let kernel = machine(); + kernel.takes.borrow_mut().push_back(Take::Pending); + let mut host = Firmware::new(&kernel); + let _ = host.global_take(); } #[test] @@ -587,10 +835,11 @@ pub mod tests { kernel.takes.borrow_mut().push_back(Take::Unusable); let mut host = Firmware::new(&kernel); assert_eq!( - host.global_lock(true), + host.global_take(), Err(Denied("the Global Lock: the kernel exchanges no lock word in this machine's FACS".into())) ); assert_eq!(host.contended, 0); + assert!(kernel.waited.borrow().is_empty()); } #[test] @@ -603,8 +852,8 @@ pub mod tests { let stopping = Denied(Refusal::Stopping.to_string()); assert_eq!(host.read(Address::Memory(NVS), toyos_aml::Access::Byte), Err(stopping.clone())); assert!(host.stopping); - assert_eq!(host.global_lock(true), Err(stopping.clone())); - assert_eq!(host.global_lock(false), Err(stopping)); + assert_eq!(host.global_take(), Err(stopping.clone())); + assert_eq!(host.global_release(), Err(stopping)); assert!(host.refused.is_empty(), "the stop was counted as something this machine's firmware was refused"); } } diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index f441c408e41..4860aeecf0a 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -20,10 +20,14 @@ //! kernel refuses every power-off. //! //! **Then the embedded controller is found in the namespace and armed** -//! ([`devices::find`]): its GPE enabled and its backlog drained. Its two +//! ([`devices::find`]): attached to the AML host, so its space reaches it, +//! and told so by its `_REG` (ACPI 6.5 §6.5.4), where its `_GLK` asks for no +//! Global Lock around its transactions (§6.5.7), which this server takes +//! around none; then its GPE enabled and its backlog drained. Its two //! ports are no row's, so every access to them is the kernel's to make -//! ([`Claim`]), and one it refuses ends the controller, said by name. A -//! machine whose DSDT did not load has no controller served. +//! ([`Claim`]), and one it refuses ends the controller, said by name, the +//! server's own or AML's. A machine whose DSDT did not load has no +//! controller served. //! **Where the FADT says the power button is a control method device**, the //! buttons the namespace names are found too, and where one is, each query //! is run as its method ([`run_query`]): a Notify of a press is a press. @@ -38,10 +42,19 @@ //! drain than [`QUERIES`], and [`sci::EMPTY_SCIS`] SCIs in a row that carried //! nothing are each a panic naming the registers. //! +//! Inside an evaluation, a take of the Global Lock the firmware holds waits +//! on the SCI for its release alone ([`sci::await_release`]), and what else +//! latched meanwhile is served after. +//! +//! **Then the machine's batteries and AC adapters are read** ([`battery`]), +//! once and every [`battery::POLL`] between SCIs: the controller's space is +//! reached there and in a query's method, never inside a drain. +//! //! The log carries each query number the first time it is taken and a count //! of every one at [`COUNTS`] intervals, never a line per event. mod aml; +mod battery; mod devices; mod ec; mod host; @@ -49,6 +62,7 @@ mod ledger; mod sci; mod tables; +use std::cell::Cell; use std::collections::{BTreeMap, VecDeque}; use std::time::{Duration, Instant}; @@ -57,19 +71,18 @@ use toyos::ioport::{in16, in8, out16, out8}; use toyos::poller::{Poller, READABLE}; use toyos::power::{self, Stop}; use toyos::AcpiDev; -use toyos_abi::acpi::{Access, AcpiInfo, Block, Space, Width, FIXED_POWER_BUTTON}; +use toyos_abi::acpi::{Access, AcpiInfo, Block, FIXED_POWER_BUTTON}; use toyos_abi::syscall::{DeviceType, SyscallError}; +use toyos_aml::Value; use acpiserver_api::{CONTROLLER_NONE, CONTROLLER_SERVED}; use aml::Aml; +use battery::Power; use devices::{Ec, Queried}; -use ec::{Do, Transaction, Wait}; -use host::{Answer, Kernel, Stopping, Take}; -use sci::{Event, Served, Unserved, Unstopped, PM1_STATUS, PWRBTN}; +use ec::Transaction; +use host::{port_in, transact, Answer, Kernel, Stopping, Take, Unmade, OWN}; +use sci::{Bits, Event, Fixed, Served, Unserved, Unstopped, PM1_STATUS, PWRBTN}; -/// The most a controller is waited for at one step of a transaction: one that -/// has not moved in this will not. -const EC_STEP: Duration = Duration::from_millis(500); /// The most queries one drain takes before the controller is a storm. const QUERIES: usize = 32; /// How often the log is told the queries' counts, where they moved. @@ -84,6 +97,8 @@ struct Server<'a, K, P> { ec: Option, /// The namespace the load kept. aml: Option>, + /// The batteries and adapters found in it, where one is a battery. + power: Option, /// Taken off the controller by the drain, run after it. queued: VecDeque, /// Every query number taken, and how often. @@ -102,14 +117,15 @@ fn main() { return; }; let info = dev.describe().expect("acpiserver: the claim's first read is its description"); - let claim = Claim(&dev); + let claim = Claim { dev: &dev, info, poller: Poller::new(1), watching: Cell::new(false) }; let mut server = Server::new(&claim, Machine, info); server.arm(&dev); let (loaded, kept) = aml::load(&claim, server.info.rsdp); server.power_off = loaded.handed; server.aml = kept; - server.arm_controller(); - server.serve(&dev); + let (batteries, adapters) = server.arm_controller(); + server.find_power(batteries, adapters); + server.serve(); } /// The row's PM1 event and GPE0 registers, which this server reads and writes @@ -143,10 +159,28 @@ impl Ports for Machine { } /// The claim as the tables' fetch and their AML ask it for what lies outside -/// its own ports. -struct Claim<'a>(&'a AcpiDev); +/// its own ports, and as the wait for the firmware's release of the Global +/// Lock reads its event blocks and its SCI; and the one poller its SCI is +/// waited on with, between SCIs and inside that wait. +struct Claim<'a> { + dev: &'a AcpiDev, + info: AcpiInfo, + poller: Poller, + /// A watch answers once: one still registered from a wait that timed out + /// is not registered again. + watching: Cell, +} impl Claim<'_> { + /// Waits until `until` for the claim's record to be readable. + fn readable(&self, until: Instant) { + if !self.watching.replace(true) { + self.poller.watch(self.dev, READABLE, 0); + } + let wait = until.saturating_duration_since(Instant::now()); + self.poller.wait(1, wait.as_nanos() as u64, |_| self.watching.set(false)); + } + /// The kernel's answer; a stopping machine's is the caller's to carry, /// and any other refusal of a call this server formed is this server's /// defect. @@ -161,44 +195,69 @@ impl Claim<'_> { impl Kernel for Claim<'_> { fn access(&self, access: Access) -> Result { - Self::answered("a mediated access", self.0.access(access)).map(|(made, memory_type)| Answer { made, memory_type }) + Self::answered("a mediated access", self.dev.access(access)).map(|(made, memory_type)| Answer { made, memory_type }) } fn lock_take(&self) -> Result { - match self.0.lock_take() { + match self.dev.lock_take() { Err(SyscallError::NotSupported) => Ok(Take::Unusable), answer => Self::answered("a take of the Global Lock", answer).map(|taken| if taken { Take::Taken } else { Take::Pending }), } } fn lock_release(&self) -> Result<(), Stopping> { - Self::answered("the Global Lock's release", self.0.lock_release()) + Self::answered("the Global Lock's release", self.dev.lock_release()) + } + + fn released(&self, until: Instant) -> bool { + sci::await_release(&mut Waiting(self), until) } fn s5(&self, slp_typ_a: u64) -> Result { - match self.0.s5(slp_typ_a) { + match self.dev.s5(slp_typ_a) { Err(SyscallError::InvalidArgument) => Ok(false), answer => Self::answered("the power-off's sleep type", answer).map(|()| true), } } } -/// Why an access to one of the controller's ports was not made. -enum Unmade { - Stopping, - /// The kernel keeps the port from this server, said as it refused it. - Refused(String), -} +/// The claim's event blocks and SCI for one wait for the firmware's release. +struct Waiting<'c, 'a>(&'c Claim<'a>); -/// A byte of one of the controller's ports, which no row holds. -fn port_in(kernel: &impl Kernel, port: u16) -> Result { - let answer = kernel.access(Access::read(Space::SystemIo, u64::from(port), Width::Byte)).map_err(|Stopping| Unmade::Stopping)?; - answer.made.map(|byte| byte as u8).map_err(|refused| Unmade::Refused(format!("the kernel refused a read of its port {port:#x}, {refused:?}"))) -} +impl Fixed for Waiting<'_, '_> { + fn status(&mut self) -> Bits { + Bits { pm1: in16(self.0.info.pm1_event.port), gpe0: bytes(self.0.info.gpe0).map(|(status, _)| in8(status)).collect() } + } -fn port_out(kernel: &impl Kernel, port: u16, byte: u8) -> Result<(), Unmade> { - let answer = kernel.access(Access::write(Space::SystemIo, u64::from(port), Width::Byte, u64::from(byte))).map_err(|Stopping| Unmade::Stopping)?; - answer.made.map(drop).map_err(|refused| Unmade::Refused(format!("the kernel refused a write of its port {port:#x}, {refused:?}"))) + fn pm1_clear(&mut self, bits: u16) { + out16(self.0.info.pm1_event.port, bits); + } + + fn enables(&mut self) -> Bits { + Bits { pm1: in16(self.0.info.pm1_event.enable()), gpe0: bytes(self.0.info.gpe0).map(|(_, enable)| in8(enable)).collect() } + } + + fn enable(&mut self, enables: &Bits) { + out16(self.0.info.pm1_event.enable(), enables.pm1); + for ((_, enable), &byte) in bytes(self.0.info.gpe0).zip(&enables.gpe0) { + out8(enable, byte); + } + } + + fn sci(&mut self, until: Instant) -> bool { + if until.saturating_duration_since(Instant::now()).is_zero() { + return false; + } + self.0.readable(until); + match self.0.dev.irq() { + Ok(_) | Err(SyscallError::WouldBlock) => true, + Err(other) => panic!("acpiserver: the claim's record answered {other:?} while the Global Lock was waited for"), + } + } + + fn ack(&mut self) { + self.0.dev.ack().expect("acpiserver: the claim's acknowledgement"); + } } /// Each byte of a status-and-enable block: its status port and its enable port. @@ -215,6 +274,7 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { served: Served { power_button: info.flags & FIXED_POWER_BUTTON != 0, ec_gpe: None }, ec: None, aml: None, + power: None, queued: VecDeque::new(), counts: BTreeMap::new(), counted: 0, @@ -244,29 +304,42 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { } /// Find the controller, and the buttons where they are control method - /// devices, in the namespace the load kept; enable the controller's GPE - /// and drain its backlog. - fn arm_controller(&mut self) { + /// devices, in the namespace the load kept; attach the controller to the + /// AML host ([`attach`]), enable its GPE and drain its backlog. What + /// comes back is the namespace's batteries and adapters, by path. + fn arm_controller(&mut self) -> (Vec, Vec) { let Some(aml) = &mut self.aml else { if !self.served.power_button { println!("acpiserver: no control-method power button served: no namespace was kept"); } - return println!("{CONTROLLER_NONE}no namespace was kept"); + println!("{CONTROLLER_NONE}no namespace was kept"); + return (Vec::new(), Vec::new()); }; let found = devices::find(&mut aml.interpreter, &mut aml.host, self.info.gpe0, !self.served.power_button); if !self.served.power_button { println!("acpiserver: {} control-method power button(s) served, each by the queries' Notify", found.buttons.len()); } aml.host.buttons = found.buttons; - let Some(ec) = found.ec else { return }; + let sources = (found.batteries, found.adapters); + let Some(ec) = found.ec else { return sources }; // Its status register, which a read leaves as it was; the data // register's read takes a byte, and a port refused later ends the // controller then ([`Server::unserve`]). match port_in(self.kernel, ec.command) { - Err(Unmade::Stopping) => return, - Err(Unmade::Refused(why)) => return println!("{CONTROLLER_NONE}{why}"), + Err(Unmade::Stopping) => return sources, + Err(Unmade::Refused(why)) => { + println!("{CONTROLLER_NONE}{why}"); + return sources; + } Ok(_) => {} } + if let Err(why) = attach(aml, &ec) { + println!("{CONTROLLER_NONE}{why}"); + return sources; + } + if aml.host.stopping { + return sources; + } let n = ec.gpe; let (status, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); self.ports.out8(status, 1 << (n % 8)); @@ -276,32 +349,39 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { self.ec = Some(ec); self.drain(); self.run_queued(); + sources } - fn serve(&mut self, dev: &AcpiDev) -> ! { - let poller = Poller::new(1); - let mut next_count = Instant::now() + COUNTS; - // A watch answers once: one still registered from a wait that timed - // out is not registered again. - let mut watching = false; - loop { - if !watching { - poller.watch(dev, READABLE, 0); - watching = true; - } - let wait = next_count.saturating_duration_since(Instant::now()); - poller.wait(1, wait.as_nanos() as u64, |_| watching = false); - if Instant::now() >= next_count { - self.log_counts(); - next_count = Instant::now() + COUNTS; - } - match dev.irq() { - Ok(record) => self.scis += u64::from(record.count), - Err(SyscallError::WouldBlock) => continue, - Err(other) => panic!("acpiserver: the claim's record answered {other:?}"), - } - self.take(); - dev.ack().expect("acpiserver: the claim's acknowledgement"); + /// Find and first read the batteries and adapters the namespace names, + /// once the controller is armed ([`Power::find`]). + fn find_power(&mut self, batteries: Vec, adapters: Vec) { + let Some(aml) = &mut self.aml else { return }; + let census; + (self.power, census) = Power::find(aml, batteries, adapters); + if let Some(census) = census { + println!("{census}"); + } + self.ended(); + } + + /// Read the batteries and adapters again ([`Power::read`]), and none + /// more on a machine that is stopping. + fn read_power(&mut self) { + let (Some(power), Some(aml)) = (&mut self.power, &mut self.aml) else { return }; + for line in power.read(aml) { + println!("{line}"); + } + if aml.host.stopping { + self.power = None; + } + self.ended(); + } + + /// Serve the controller no more where AML's access to it found one of + /// its ports refused ([`host::Firmware::ended`]). + fn ended(&mut self) { + if let Some(why) = self.aml.as_mut().and_then(|aml| aml.host.ended.take()) { + self.unserve(&why); } } @@ -392,6 +472,9 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { let (_, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the controller's GPE was bounded by the block"); self.ports.out8(enable, self.ports.in8(enable) & !(1 << (n % 8))); self.ec = None; + if let Some(aml) = &mut self.aml { + aml.host.ec = None; + } self.queued.clear(); println!("{CONTROLLER_NONE}{why}"); } @@ -401,6 +484,7 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { fn run_queued(&mut self) { while let Some(q) = self.queued.pop_front() { let served = run_query(self.aml.as_mut(), self.ec.as_ref(), q); + self.ended(); let count = self.counts.entry(q).or_insert(0); *count += 1; self.counted += 1; @@ -429,6 +513,64 @@ impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { } } +impl<'a, P: Ports> Server<'a, Claim<'a>, P> { + fn serve(&mut self) -> ! { + let mut next_count = Instant::now() + COUNTS; + let mut next_read = Instant::now() + battery::POLL; + loop { + if self.power.is_some() && Instant::now() >= next_read { + self.read_power(); + next_read = Instant::now() + battery::POLL; + } + self.kernel.readable(if self.power.is_some() { next_count.min(next_read) } else { next_count }); + if Instant::now() >= next_count { + self.log_counts(); + next_count = Instant::now() + COUNTS; + } + match self.kernel.dev.irq() { + Ok(record) => self.scis += u64::from(record.count), + Err(SyscallError::WouldBlock) => continue, + Err(other) => panic!("acpiserver: the claim's record answered {other:?}"), + } + self.take(); + self.kernel.dev.ack().expect("acpiserver: the claim's acknowledgement"); + } + } +} + +/// Attach `ec` to the AML host, so its space reaches the controller, where +/// its `_GLK` asks for no Global Lock around a transaction (ACPI 6.5 §6.5.7): +/// absent or 0. Then tell its `_REG` the space is there (§6.5.4: Arg0 3 the +/// EmbeddedControl space, Arg1 1 connect). `Err` is why a port of it the +/// kernel refused ended it. +fn attach(aml: &mut Aml<'_, K>, ec: &Ec) -> Result<(), String> { + let evaluate = |aml: &mut Aml<'_, K>, method: &str, args: &[Value]| { + let path = format!("{}.{method}", ec.path); + match aml.interpreter.evaluate(&mut aml.host, &path, args) { + Err(toyos_aml::Error::NotFound(named)) if named == path => Ok(None), + answer => answer.map(Some), + } + }; + let unattached: Option = match evaluate(aml, "_GLK", &[]) { + Ok(None | Some(Value::Integer(0))) => None, + Ok(Some(Value::Integer(_))) => Some("its _GLK asks for the Global Lock around each transaction, which this server takes around none".into()), + Ok(Some(_)) => Some("its _GLK is no Integer".into()), + Err(why) => Some(format!("its _GLK was refused, {}", aml::kind(&why))), + }; + if let Some(why) = unattached { + println!("acpiserver: the embedded controller's space is not attached to AML: {why}"); + return Ok(()); + } + aml.host.ec = Some(ec.clone()); + if let Err(why) = evaluate(aml, "_REG", &[Value::Integer(3), Value::Integer(1)]) { + if !aml.host.stopping { + println!("acpiserver: the embedded controller's _REG refused: {}", aml::kind(&why)); + println!("{OWN}that was {}._REG", ec.path); + } + } + aml.host.ended.take().map_or(Ok(()), Err) +} + /// What a query no method runs for is said to be. const UNRUN: &str = "served by nothing: no query's method runs where no control-method power button is served"; @@ -445,38 +587,10 @@ fn run_query(aml: Option<&mut Aml<'_, K>>, ec: Option<&Ec>, q: u8) -> } } -/// One transaction on the controller at `command` and `data`, waiting on it at -/// each step for at most [`EC_STEP`]. -fn transact(kernel: &impl Kernel, command: u16, data: u16, mut tx: Transaction) -> Result { - let mut waiting: Option<(Wait, Instant)> = None; - loop { - let status = port_in(kernel, command)?; - match tx.step(status) { - Do::Wait(wait) => { - let since = match waiting { - Some((was, since)) if was == wait => since, - _ => Instant::now(), - }; - assert!(since.elapsed() < EC_STEP, "acpiserver: the embedded controller kept {wait:?} unmet for {EC_STEP:?} (EC_SC {status:#04x})"); - waiting = Some((wait, since)); - std::thread::yield_now(); - } - Do::WriteCommand(byte) => { - waiting = None; - port_out(kernel, command, byte)?; - } - Do::ReadData => { - waiting = None; - tx.read(port_in(kernel, data)?); - } - Do::Done(byte) => return Ok(byte), - } - } -} - #[cfg(test)] mod tests { use super::*; + use toyos_abi::acpi::{Space, Width}; use crate::aml::load; use crate::aml::tests::{crafted, sealed, CRAFTED_RSDP}; use crate::devices::tests::{laptop_with_queries, GPE0}; @@ -547,6 +661,72 @@ mod tests { } } + /// The controller is attached to AML as it is armed, its `_REG` run and + /// the battery found through it. A port of it the kernel then refuses, + /// to AML at the battery's next read or to the server's own drain, ends + /// the controller for both: detached from AML, unserved, its GPE + /// disabled, and neither AML nor the drain asks the kernel for it again. + #[test] + fn a_controller_port_the_kernel_refuses_to_aml_or_the_drain_ends_it_for_both() { + for to_aml in [true, false] { + let kernel = crate::battery::tests::laptop_machine(None); + let info = AcpiInfo { rsdp: CRAFTED_RSDP, pm1_event: Block::NONE, gpe0: GPE0, flags: FIXED_POWER_BUTTON, reserved: [0; 3] }; + let mut server = Server::new(&kernel, Bytes::default(), info); + for (_, enable) in bytes(GPE0) { + server.ports.out8(enable, 0); + } + server.aml = load(&kernel, CRAFTED_RSDP).1; + let (batteries, adapters) = server.arm_controller(); + assert_eq!(server.served.ec_gpe, Some(3)); + assert!(server.aml.as_ref().is_some_and(|aml| aml.host.ec.is_some()), "the served controller is not attached to AML"); + server.find_power(batteries, adapters); + assert!(server.power.is_some(), "no battery read through the attached controller"); + assert_eq!(server.ec.as_ref().map(|ec| ec.gpe), Some(3)); + + // The kernel keeps the controller's ports from here on. + *kernel.controller.borrow_mut() = None; + if to_aml { + server.read_power(); + } else { + server.drain(); + } + assert_eq!((server.ec.is_some(), server.served.ec_gpe), (false, None), "refused to AML {to_aml}: the controller is still served"); + assert!( + server.aml.as_ref().is_some_and(|aml| aml.host.ec.is_none() && aml.host.ended.is_none()), + "refused to AML {to_aml}: the controller is still attached to AML" + ); + assert!(bytes(GPE0).all(|(_, enable)| server.ports.in8(enable) == 0), "refused to AML {to_aml}: the controller's GPE is left enabled"); + let asked = kernel.asked.borrow().len(); + server.read_power(); + server.drain(); + let ports = kernel.asked.borrow().iter().skip(asked).filter(|access| access.space == Space::SystemIo as u8).count(); + assert_eq!(ports, 0, "refused to AML {to_aml}: the controller was asked again"); + } + } + + /// A query waiting as the controller is armed, on a machine whose power + /// button is a control method device, is drained and its method run only + /// once the controller is attached to AML and its `_REG` run: the + /// method's read of the controller's space reaches the controller, and + /// is not denied as one no controller is attached for. + #[test] + fn a_query_waiting_at_arm_runs_its_method_on_the_attached_controller() { + use crate::battery::tests::{button_laptop_machine, AC, CONNECTED, RD, WR}; + let kernel = button_laptop_machine(); + let info = AcpiInfo { rsdp: CRAFTED_RSDP, pm1_event: Block::NONE, gpe0: GPE0, flags: 0, reserved: [0; 3] }; + let mut server = Server::new(&kernel, Bytes::default(), info); + for (_, enable) in bytes(GPE0) { + server.ports.out8(enable, 0); + } + server.aml = load(&kernel, CRAFTED_RSDP).1; + server.arm_controller(); + assert_eq!(server.counts, BTreeMap::from([(0x28, 1)]), "the query waiting was not drained"); + let refused = &server.aml.as_ref().expect("the namespace").host.refused; + assert!(refused.is_empty(), "{}", refused.counts()); + let done = kernel.controller.borrow().as_ref().expect("a controller").done.clone(); + assert_eq!(done, [(WR, CONNECTED, 1), (0x84, 0, 0), (RD, AC, 0)], "_REG's write, the drain's query, the method's read"); + } + /// The owner's ruling: a machine whose power button is a control method /// device and whose namespace was not kept stays in ACPI mode with the /// button dead. The server serves neither the button nor the controller, diff --git a/userland/acpiserver/src/sci.rs b/userland/acpiserver/src/sci.rs index c1d0a30cf14..5b228cf183a 100644 --- a/userland/acpiserver/src/sci.rs +++ b/userland/acpiserver/src/sci.rs @@ -1,7 +1,10 @@ //! What one SCI carried: every event whose status and enable bits are both //! set, read off the PM1 event block and the GPE0 block, and refused by name -//! where it is one this server never enabled; and what a press is whose -//! power-off came back refused ([`unstopped`]). +//! where it is one this server never enabled; what a press is whose +//! power-off came back refused ([`unstopped`]); and the wait for the +//! firmware's release of the Global Lock ([`await_release`]). + +use std::time::Instant; use toyos::power::Refused; use toyos_abi::syscall::SyscallError; @@ -9,6 +12,11 @@ use toyos_abi::syscall::SyscallError; /// PM1 status and enable (ACPI 6.5 Tables 4.13, 4.14): the power button. pub const PWRBTN: u16 = 1 << 8; +/// PM1 status and enable (Tables 4.13, 4.14): `GBL_STS`, which the firmware +/// sets when it gives back a Global Lock it found its pending bit set in +/// (§5.2.10.1), and `GBL_EN`, which lets that raise the SCI. +pub const GBL: u16 = 1 << 5; + /// PM1 status bits a write of one clears (Table 4.13): timer, bus master, /// global lock release, power button, sleep button, RTC, PCIe wake and wake. pub const PM1_STATUS: u16 = 1 << 0 | 1 << 4 | 1 << 5 | 1 << 8 | 1 << 9 | 1 << 10 | 1 << 14 | 1 << 15; @@ -60,6 +68,65 @@ pub fn events(served: &Served, pm1: (u16, u16), gpe0: &[(u8, u8)]) -> Result, +} + +/// The fixed hardware and the SCI, as the wait for the firmware's release +/// drives them. +pub trait Fixed { + fn status(&mut self) -> Bits; + /// Write `bits` to PM1 status, which clears each one set (Table 4.13). + fn pm1_clear(&mut self, bits: u16); + fn enables(&mut self) -> Bits; + fn enable(&mut self, enables: &Bits); + /// Waits for the SCI until `until` and takes its record: `false` is the + /// deadline, and a wake with no record is `true`. + fn sci(&mut self, until: Instant) -> bool; + /// Unmasks the SCI. + fn ack(&mut self); +} + +/// Waits until `until` for `GBL_STS`, after a take of the Global Lock left +/// the firmware the pending bit (§5.2.10.1): `true` where it was set, and is +/// now clear. `GBL_EN` is set beside every enable kept; an event that +/// latches meanwhile keeps its status for whoever serves it, and loses its +/// enable until the wait ends, so the level SCI it raised falls. An enable +/// is cleared only once its status has latched, so the wait holds whether a +/// status latches under a clear enable (Table 4.13) or, as QEMU's ICH9 power +/// button, only under a set one. A `GBL_STS` left from an earlier release +/// ends the wait too, which costs the caller one take more and loses no +/// release. +pub fn await_release(fixed: &mut impl Fixed, until: Instant) -> bool { + let kept = fixed.enables(); + let mut enabled = Bits { pm1: kept.pm1 | GBL, gpe0: kept.gpe0.clone() }; + let released = loop { + let status = fixed.status(); + let set = status.pm1 & GBL != 0; + if set { + fixed.pm1_clear(GBL); + } + enabled.pm1 &= !status.pm1; + for (enable, status) in enabled.gpe0.iter_mut().zip(&status.gpe0) { + *enable &= !status; + } + fixed.enable(&enabled); + // Acknowledged with every status latched disabled, and `GBL_STS` clear. + fixed.ack(); + if set { + break true; + } + if !fixed.sci(until) { + break false; + } + }; + fixed.enable(&kept); + released +} + /// SCIs in a row that carried no event this server serves, past which the /// line is a storm and not a machine with something to say. pub const EMPTY_SCIS: u32 = 64; @@ -128,6 +195,111 @@ mod tests { ); } + /// What the firmware or a device sets, at one wait for the SCI. + #[derive(Clone, Copy)] + enum Set { + Pm1(u16), + /// A GPE0 status byte, and its bits. + Gpe(usize, u8), + } + + /// A PM1 event block and a GPE0 block, whose status bits a write of one + /// clears and whose SCI is level: asserted while any status bit and its + /// enable are both set (§4.8.3.1), so an acknowledgement then is one the + /// line answers at once. A PM1 status latches only under its enable, as + /// QEMU's ICH9 power button does. A wait for the SCI lets the script set + /// one thing after another until the line is raised; past the script the + /// wait meets its deadline. + struct Block { + status: u16, + gpe0: Vec, + enables: Bits, + script: Vec, + sets: usize, + acks: usize, + } + + impl Block { + fn new(script: &[Set]) -> Self { + Block { + status: 0, + gpe0: vec![0; 3], + enables: Bits { pm1: PWRBTN, gpe0: vec![0x40, 0, 0x01] }, + script: script.to_vec(), + sets: 0, + acks: 0, + } + } + } + + impl Fixed for Block { + fn status(&mut self) -> Bits { + Bits { pm1: self.status, gpe0: self.gpe0.clone() } + } + fn pm1_clear(&mut self, bits: u16) { + self.status &= !bits; + } + fn enables(&mut self) -> Bits { + self.enables.clone() + } + fn enable(&mut self, enables: &Bits) { + self.enables = enables.clone(); + } + fn sci(&mut self, _: Instant) -> bool { + while let Some(&set) = self.script.get(self.sets) { + match set { + Set::Pm1(bits) => self.status |= bits & self.enables.pm1, + Set::Gpe(byte, bits) => self.gpe0[byte] |= bits, + } + self.sets += 1; + if self.raised() { + return true; + } + } + false + } + fn ack(&mut self) { + self.acks += 1; + assert!(!self.raised(), "acknowledged with PM1 status {:#06x} or GPE0 status {:x?} raising the line", self.status, self.gpe0); + } + } + + impl Block { + fn raised(&self) -> bool { + self.status & self.enables.pm1 != 0 || self.gpe0.iter().zip(&self.enables.gpe0).any(|(status, enable)| status & enable != 0) + } + } + + #[test] + fn the_wait_ends_on_the_firmwares_release_and_leaves_every_other_event_latched_and_enabled() { + // A press and the controller's GPE come while the firmware holds the + // lock; then it lets go. + let mut block = Block::new(&[Set::Pm1(PWRBTN), Set::Gpe(0, 0x40), Set::Pm1(GBL)]); + let kept = block.enables.clone(); + assert!(await_release(&mut block, Instant::now())); + assert_eq!(block.status, PWRBTN, "GBL_STS left set, or an event the wait does not serve cleared"); + assert_eq!(block.gpe0, [0x40, 0, 0], "the controller's GPE cleared by the wait"); + assert_eq!(block.enables, kept, "an enable was not put back"); + assert_eq!((block.sets, block.acks), (3, 4), "the wait woke other than once for each event"); + } + + #[test] + fn a_release_already_latched_ends_the_wait_without_one() { + let mut block = Block::new(&[]); + block.status = GBL; + assert!(await_release(&mut block, Instant::now())); + assert_eq!((block.status, block.sets, block.acks), (0, 0, 1)); + } + + #[test] + fn a_firmware_that_never_lets_go_meets_the_deadline_with_its_enables_back() { + let mut block = Block::new(&[Set::Pm1(PWRBTN)]); + let kept = block.enables.clone(); + assert!(!await_release(&mut block, Instant::now())); + assert_eq!(block.enables, kept); + assert_eq!(block.status, PWRBTN); + } + #[test] fn only_a_press_on_a_machine_handed_no_sleep_type_is_dropped() { let no_sleep_type = Refused::Kernel(SyscallError::NotSupported);