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 4e10af18689..72fced2b417 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 @@ -124,16 +124,21 @@ in ACPI mode; that no T14 row reads it is Moving it out of this exit is the orchestrator's placement, not the owner's. **Ruled** (owner, 2026-10-04, "Stopgap, delete later"): "Stage 1 uses the -extra table so the T14 switches to ACPI mode now." Stage 1 reads the -embedded controller from the ECDT, and that path is a stopgap: it is deleted -the day the interpreter reads the controller from the DSDT's own device. A -machine without an ECDT stays in legacy mode until then; such a machine keeps -its firmware interrupts, and this stage's exit cannot be met on it. +extra table so the T14 switches to ACPI mode now." Stage 1 read the +embedded controller from the ECDT as that stopgap, to be deleted the day the +interpreter reads the controller from the DSDT's own device. It is deleted: +the server finds the PNP0C09 device in the namespace its load kept and takes +its ports from `_CRS` and its GPE from `_GPE`, and reaches the ports through +the kernel's mediated access, since no row names them +(`userland/acpiserver/src/devices.rs`); the kernel reads no ECDT. -Stage 1's design, not a ruling: a machine whose power button is a control -method device stays in legacy mode too, refused by name; and a machine its -firmware hands over in ACPI mode is served whatever it has, since nothing is -written. +The design, not a ruling: a machine whose power button is a control method +device is claimed too, and its button served by the Notify the controller's +query method makes of it (ACPI 6.5 §4.8.2.2.1.2), which the server runs for +every query only on such a machine and only once it found the button; a +button raised by a GPE's own `_Lxx` or `_Exx` is not served, since the +server enables no GPE but the controller's. A machine its firmware hands +over in ACPI mode is served whatever it has, since the mint writes nothing. **Stage: the interpreter** (the orchestrator's placement of "The AML stage closes it"). ToyOS's own AML interpreter, written from the specification, run @@ -283,16 +288,29 @@ mediated access, leaves open: power-off is made before the load ends, and what bounds the refusal is the load's time. That time is held on the T14 once the `acpi_tables_loaded` row reads it under a bound the owner names. -- **A machine with no holder of the `acpi` claim has no power-off.** The - kernel reads no AML, so a machine whose claim it refuses, one in legacy - mode with no ECDT or with a control-method power button, has nobody to - evaluate `\_S5`: `SYS_SHUTDOWN` is refused there, where the kernel's own - scan of the DSDT powered such a machine off before. Neither the T14 nor q35 - is one: the T14 has an ECDT and a fixed button, and OVMF hands q35 over in - ACPI mode. Owner: this stage. **Exit**: the ECDT stopgap is deleted, so a - machine is claimed for what its DSDT names; what a machine with a - control-method button does for a power-off is ruled with that button's - device. +- **A control-method power button's press powers the machine off unruled.** + The claim is no longer refused for a missing ECDT or a control-method + button, so such a machine has a server to evaluate `\_S5`, and the + server's design makes the button's Notify a press, which stops the + machine through the one path. What such a machine does for a power-off + was to be "ruled with that button's device", and the owner has not ruled + it. Owner: this stage. **Exit**: the owner rules it, and the server does + what he rules. +- **A control-method button the server does not serve is dead in ACPI + mode.** The kernel puts the machine in ACPI mode when it mints the claim, + before the server has read whether it serves the button. Where it then + serves none, because no namespace was kept, no controller is served, no + present PNP0C0C was found or the button's `_Qxx` is refused, a short press + reaches nothing; in legacy mode the firmware served it, and before #845 + the kernel refused such a machine the claim and left it there. The server + says each of those by name. Owner: this stage. **Exit, met**: the owner + ruled (2026-10-10), asked "When ToyOS switches a machine into ACPI mode but + its ACPI server can't load the firmware's tables (so it can't handle a + power button the firmware reports through ACPI code), what should + happen?": "Stay in ACPI mode, button dead". The host test + `no_namespace_leaves_the_button_and_the_controller_unserved_and_keeps_the_claim` + (`userland/acpiserver/src/main.rs`) reads the server serving neither the + button nor the controller and keeping the claim. Two more machines have no power-off, and that is the ruled state and no weakness with an exit. A boot whose config starts no server: "Power-off @@ -324,9 +342,23 @@ mediated access, leaves open: then supplies a sleep type that is not the machine's and reads it refused. What the firmware call, which the kernel makes for the server where its AML -stores a byte to `SMI_CMD`, leaves open. The server's AML makes none yet: its -host denies every write AML asks for and passes none to the kernel -(`userland/acpiserver/src/host.rs`). +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 +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 +tables with nothing cleared between the load, the device search and all +102 of its query methods (#845): its DSDT writes both indexes at its load +and is refused without either, the search writes nothing, and the queries +write only the POST port. + +- **A port write outside the three ends what made it.** A machine whose + AML writes another port at its load has that table refused by name, and + a method that does has its evaluation refused. The T14's load writes + none: it loaded with every write denied. Owner: this stage. **Exit**: a + machine's reading of a write outside the three, which the list takes or + the owner refuses, and a slice that runs a method which stores to + `SMI_CMD` takes the call with the readings below. - **What a call does there is the firmware's**, and the kernel bounds who, when, where, which byte and how often: diff --git a/kernel/src/arch/aarch64/boot.rs b/kernel/src/arch/aarch64/boot.rs index eab35e3c0fe..024db38f703 100644 --- a/kernel/src/arch/aarch64/boot.rs +++ b/kernel/src/arch/aarch64/boot.rs @@ -366,7 +366,7 @@ pub fn timer() { /// The platform's own devices that are not PCI functions: none this kernel /// drives on an ACPI Arm machine. -pub fn platform_devices(_rsdp_addr: u64) {} +pub fn platform_devices(_platform: &Platform, _rsdp_addr: u64) {} /// Every other CPU, running. pub fn start_other_cpus(platform: &Platform, _args: &KernelArgs) { diff --git a/kernel/src/arch/x86_64/acpi_mode.rs b/kernel/src/arch/x86_64/acpi_mode.rs index 19ad1db2b5f..e6ed42d187e 100644 --- a/kernel/src/arch/x86_64/acpi_mode.rs +++ b/kernel/src/arch/x86_64/acpi_mode.rs @@ -13,19 +13,16 @@ //! disable again. Neither is written once the stop has begun: `smi_cmd` //! refuses it. //! -//! **A machine stays in legacy mode where its firmware serves something no -//! holder could**: an embedded controller the ECDT does not name, or a power -//! button that is a control method device, which only AML serves. Both are -//! refused at the mint, by name; a machine the firmware handed over in ACPI -//! mode is claimed whatever it has, since nothing is written. +//! **What only the firmware's AML names is the holder's to find**: the +//! embedded controller is a device of the DSDT, and a power button that is a +//! control method device is served by the AML that notifies it. This kernel +//! reads no AML, so neither keeps a machine in legacy mode; the controller's +//! ports are the holder's to reach as [`access`] reaches any port no row +//! names, and no table this kernel reads names them. //! -//! **The ECDT is a stopgap**: the embedded controller is read from it until the -//! interpreter reads the controller's own device from the DSDT, and then this -//! path and `toyos_acpi::ecdt` go -//! (`issues/toyos-runs-the-machine-in-acpi-mode-and-interprets-its-aml.md`). -//! -//! The row is the FADT's PM1a event and GPE0 blocks and the ECDT's two -//! ports, filled once at boot; the SCI is its one line, level. +//! The row is the FADT's PM1a event and GPE0 blocks, filled once at boot; +//! the SCI is its one line, level. `SMI_CMD` is declared before it, with the +//! reset register (`power::init_reset`). //! //! **What the firmware's AML names outside the row, this kernel reads and //! writes for the claim's holder, one access at a time** ([`access`]): memory @@ -83,7 +80,7 @@ use core::sync::atomic::AtomicU32; use toyos_abi::acpi::{Access, AcpiInfo, Block, Refused, Space, Width, FIXED_POWER_BUTTON}; use toyos_abi::syscall::SyscallError; -use toyos_acpi::{Ec, EcamWindow, FixedHardware, LegacyMode, PowerButton}; +use toyos_acpi::{EcamWindow, FixedHardware, LegacyMode, PowerButton}; use toyos_userbound::firmware::{ self, CallRate, FirmwareCall, Function as PciFunction, LockWordAt, Memory, MemoryAt, MemoryVerdict, PortAt, PortVerdict, }; @@ -117,8 +114,6 @@ const HANDBACK: Duration = Duration::from_millis(100); struct Hardware { fixed: FixedHardware, control: Declared, - /// Or why it is none a holder can be handed. - ec: Result, rsdp: u64, /// The windows configuration space is reached through, as the MCFG bounds them. ecam: Vec, @@ -223,10 +218,10 @@ fn hardware() -> Option<&'static Hardware> { (!at.is_null()).then(|| unsafe { &*at }) } -/// Decode the fixed hardware, declare `SMI_CMD`, and fill the row; or say by -/// name why this machine has none. After the i8042's row, whose lines the -/// SCI may not share. -pub fn init(rsdp_addr: u64) { +/// Decode the fixed hardware and fill the row; or say by name why this +/// machine has none. After the i8042's row, whose lines the SCI may not +/// share; `facs` is what `power::init_reset` read of the FACS. +pub fn init(rsdp_addr: u64, facs: Option>) { let fadt = match toyos_acpi::find_table( crate::drivers::acpi::direct_phys(), rsdp_addr, @@ -243,17 +238,9 @@ pub fn init(rsdp_addr: u64) { let Some(control) = super::power::pm1a_control() else { return log!("acpi: no ACPI row — no PM1a control block declared"); }; - let facs = toyos_acpi::facs(fadt.phys(), &fadt); - // A FACS this kernel cannot read leaves `S4BIOS_F` unread, and the byte kept. - let s4bios = match facs { - Ok(facs) => facs.s4bios, - Err(toyos_acpi::FacsRefused::Absent) => false, - Err(_) => true, - }; - if let Some(Err(why)) = fixed.smi_cmd.map(|named| smi_cmd::declare(named.port, named.named(s4bios))) { - return log!("acpi: no ACPI row — SMI_CMD not declared: {why:?}"); + if fixed.smi_cmd.is_some() && smi_cmd::declared().is_none() { + return log!("acpi: no ACPI row — the FADT's SMI_CMD was not declared"); } - let ec = embedded_controller(rsdp_addr, fixed.gpe0); let Some(sci) = super::ioapic::sci(fixed.sci_int) else { return log!("acpi: no ACPI row — no I/O APIC carries the SCI"); }; @@ -265,11 +252,8 @@ pub fn init(rsdp_addr: u64) { if fixed.gpe0.len != 0 { runs.push(run(fixed.gpe0)); } - if let Ok(ec) = &ec { - runs.extend([Ports::one(ec.command), Ports::one(ec.data)]); - } log!( - "acpi: the ACPI row: PM1a events {:#x}+{}, GPE0 {:#x}+{}, SCI {}, {}, embedded controller {}; the firmware handed over in {} mode", + "acpi: the ACPI row: PM1a events {:#x}+{}, GPE0 {:#x}+{}, SCI {}, {}; the firmware handed over in {} mode", fixed.pm1a_event.port, fixed.pm1a_event.len, fixed.gpe0.port, @@ -279,15 +263,12 @@ pub fn init(rsdp_addr: u64) { PowerButton::Fixed => "the fixed-hardware power button", PowerButton::ControlMethod => "a control-method power button", }, - match &ec { - Ok(ec) => format!("at {:#x}/{:#x} on GPE {:#x}", ec.command, ec.data, ec.gpe), - Err(why) => format!("none ({why})"), - }, if cpu::inw(control.port(0)) & SCI_EN != 0 { "ACPI" } else { "legacy" }, ); isa::fill(ROW, Function { name: "the ACPI fixed hardware", runs, irqs: vec![], wires: vec![sci] }); + let facs = facs.expect("power::init_reset read the FACS of the FADT this decoded"); let (ecam, lock) = (crate::drivers::pci::windows(), global_lock(facs)); - let hardware = Hardware { fixed, control, ec, rsdp: rsdp_addr, ecam, lock }; + let hardware = Hardware { fixed, control, rsdp: rsdp_addr, ecam, lock }; let was = HARDWARE.swap(Box::into_raw(Box::new(hardware)), Ordering::Release); assert!(was.is_null(), "acpi: init ran twice"); } @@ -326,17 +307,6 @@ fn run(block: Block) -> Ports { Ports::new(block.port, block.len).expect("`fixed_hardware` bounded every block by the port space") } -fn embedded_controller(rsdp_addr: u64, gpe0: Block) -> Result { - let ecdt = toyos_acpi::find_table(crate::drivers::acpi::direct_phys(), rsdp_addr, b"ECDT", toyos_acpi::ECDT_NEEDED) - .map_err(|e| format!("the ECDT is unusable: {e:?}"))?; - let ec = toyos_acpi::ecdt(&ecdt).map_err(|refused| format!("the ECDT names none: {refused:?}"))?; - // §5.2.9: a GPE block's status half holds eight GPEs a byte. - if u16::from(ec.gpe) >= gpe0.len / 2 * 8 { - return Err(format!("the ECDT puts it on GPE {:#x}, outside GPE0's {}", ec.gpe, gpe0.len / 2 * 8)); - } - Ok(ec) -} - /// The row, claimed, with the machine in ACPI mode; or the refusal that /// leaves it in legacy mode, said by name. pub fn claim() -> Result<(isa::Row, AcpiInfo), ClaimError> { @@ -348,19 +318,12 @@ pub fn claim() -> Result<(isa::Row, AcpiInfo), ClaimError> { } fn info(hardware: &Hardware) -> AcpiInfo { - let (ec_command, ec_data, ec_gpe) = match &hardware.ec { - Ok(ec) => (Block { port: ec.command, len: 1 }, Block { port: ec.data, len: 1 }, u16::from(ec.gpe)), - Err(_) => (Block::NONE, Block::NONE, 0), - }; AcpiInfo { rsdp: hardware.rsdp, pm1_event: hardware.fixed.pm1a_event, gpe0: hardware.fixed.gpe0, - ec_command, - ec_data, - ec_gpe, flags: if hardware.fixed.power_button == PowerButton::Fixed { FIXED_POWER_BUTTON } else { 0 }, - reserved: 0, + reserved: [0; 3], } } @@ -392,15 +355,6 @@ fn enter(hardware: &Hardware) -> Result<(), ClaimError> { return refuse("the FADT names no SMI_CMD, ACPI_ENABLE and ACPI_DISABLE to leave it and come back with"); }; let enable = legacy.acpi_enable.get(); - if let Err(why) = &hardware.ec { - return refuse(&format!( - "its firmware serves an embedded controller no holder could ({why})" - )); - } - if hardware.fixed.power_button == PowerButton::ControlMethod { - return refuse("its power button is a control method device, which only AML serves"); - } - let Some(write) = smi_cmd::write(enable) else { return refuse("the machine is stopping"); }; diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 616a43dc160..a590578a1a6 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -76,6 +76,8 @@ pub fn reserved() -> Region { /// What the boot learns bringing interrupts up and hands later steps. pub struct Platform { madt: MadtInfo, + /// The FACS as `power::init_reset` read it, for `acpi_mode::init`. + facs: Option>, } /// Interrupt delivery, this CPU's per-CPU block and the syscall gate. @@ -87,14 +89,14 @@ pub fn interrupts(rsdp_addr: u64) -> Platform { // Off the same tables as the MADT, and before the IDT below makes a panic // reportable: a panic that can be reported but not ended leaves the machine // holding its panel for a hand that may not be in the room. - super::power::init_reset(rsdp_addr); + let facs = super::power::init_reset(rsdp_addr); apic::init(); percpu::init_bsp(apic::id()); super::power::init_control(rsdp_addr); ioapic::init(&madt); idt::enable_interrupts(); super::syscall::init(); - Platform { madt } + Platform { madt, facs } } /// The clock: the TSC, calibrated against the HPET, and the CMOS wall clock. @@ -132,10 +134,10 @@ pub fn timer() { } /// The platform's own devices that are not PCI functions. -pub fn platform_devices(rsdp_addr: u64) { +pub fn platform_devices(platform: &Platform, rsdp_addr: u64) { super::i8042::init(rsdp_addr); super::pio::fill_i8042_row(); - super::acpi_mode::init(rsdp_addr); + super::acpi_mode::init(rsdp_addr, platform.facs); } /// Every other CPU, running. diff --git a/kernel/src/arch/x86_64/power.rs b/kernel/src/arch/x86_64/power.rs index 8c71f215615..ccf1c79f903 100644 --- a/kernel/src/arch/x86_64/power.rs +++ b/kernel/src/arch/x86_64/power.rs @@ -1,6 +1,8 @@ //! Reset and power-off through the FADT: its reset register, and S5 soft-off //! through the PM1a control block with the `SLP_TYPa` the holder of the `acpi` -//! claim supplied. +//! claim supplied. The reset register is declared with `SMI_CMD`, the other +//! port the FADT has a write command, since the two may be one port +//! ([`init_reset`]). //! //! **This kernel reads no AML, so it knows no sleep type of its own.** `\_S5` //! is the firmware's AML to evaluate, and the claim's holder does @@ -16,7 +18,7 @@ use core::sync::atomic::{AtomicU8, Ordering}; -use toyos_acpi::Reset; +use toyos_acpi::{Facs, FacsRefused, Reset}; use toyos_userbound::firmware::SleepType; use toyos_userbound::{Mediated, Ports}; @@ -40,31 +42,64 @@ const UNSUPPLIED: u8 = u8::MAX; static RESET: Slot = Slot::empty(); static RESET_VALUE: AtomicU8 = AtomicU8::new(0); -/// Record the FADT's reset register, or say by name why this machine has none. +/// Declare the FADT's two ports a write commands, `SMI_CMD` and the reset +/// register, and record the reset register; or say by name why this machine +/// has either not. The FACS the FADT names is read here once, and handed to +/// `acpi_mode::init`: `None` where the FADT is unusable. +/// +/// **One port may be both** (an AMD laptop's 0xb0 is): it is declared once, +/// as `SMI_CMD`, whose kept bytes then hold the reset's value too +/// (`SmiCmd::named`), and the reset register is that declaration. /// /// Before `percpu::init_bsp` loads the IDT: from then on every panic can be /// reported, and a panic that can be reported but not ended is a machine that /// still needs a hand. Walking these tables inside the panic handler instead is /// refused — a table walk on a machine that has already failed once is how a /// panic becomes a triple fault. -pub fn init_reset(rsdp_addr: u64) { - let fadt = match toyos_acpi::find_table(direct_phys(), rsdp_addr, b"FACP", toyos_acpi::FADT_FOR_RESET) { +pub fn init_reset(rsdp_addr: u64) -> Option> { + let fadt = match toyos_acpi::find_table(direct_phys(), rsdp_addr, b"FACP", toyos_acpi::FADT_FOR_FIXED_HARDWARE) { Ok(table) => table, Err(e) => { - log!("ACPI: FADT unusable: {e:?} — no reboot, a panic will hold the panel"); - return; + log!("ACPI: FADT unusable: {e:?} — no reboot, a panic will hold the panel, and no SMI_CMD"); + return None; } }; - match toyos_acpi::reset_register(&fadt) { - Reset::Port { port, value } => match pio::declare("the reset register", Ports::one(port), Mediated::Kept) { - Ok(declared) => { - RESET_VALUE.store(value, Ordering::Relaxed); - RESET.set(declared); - log!("ACPI: reset register SystemIO {port:#x} <- {value:#04x}"); - } - Err(why) => log!("ACPI: reset register {port:#x} not declared ({why:?}) — no reboot"), - }, - other => log!("ACPI: no reset register this kernel writes ({other:?}) — no reboot"), + let facs = toyos_acpi::facs(fadt.phys(), &fadt); + declare(&fadt, facs); + Some(facs) +} + +fn declare(fadt: &toyos_acpi::Table

, facs: Result) { + let reset = toyos_acpi::reset_register(fadt); + // Fixed hardware this kernel does not serve has no ACPI row, and so no use for `SMI_CMD`. + let smi_cmd = toyos_acpi::fixed_hardware(fadt).ok().and_then(|fixed| fixed.smi_cmd); + if let Some(named) = smi_cmd { + // A FACS this kernel cannot read leaves `S4BIOS_F` unread, and the byte kept. + let s4bios = match facs { + Ok(facs) => facs.s4bios, + Err(FacsRefused::Absent) => false, + Err(_) => true, + }; + if let Err(why) = super::smi_cmd::declare(named.port, named.named(s4bios, reset)) { + log!("ACPI: SMI_CMD {:#x} not declared ({why:?}) — no ACPI row", named.port); + } + } + let Reset::Port { port, value } = reset else { + return log!("ACPI: no reset register this kernel writes ({reset:?}) — no reboot"); + }; + let shared = smi_cmd.is_some_and(|named| named.port == port); + let declared = if shared { + super::smi_cmd::declared().ok_or_else(|| "SMI_CMD, which it is, was not declared".into()) + } else { + pio::declare("the reset register", Ports::one(port), Mediated::Kept).map_err(|why| alloc::format!("{why:?}")) + }; + match declared { + Ok(declared) => { + RESET_VALUE.store(value, Ordering::Relaxed); + RESET.set(declared); + log!("ACPI: reset register SystemIO {port:#x} <- {value:#04x}{}", if shared { ", which is SMI_CMD too" } else { "" }); + } + Err(why) => log!("ACPI: reset register {port:#x} not declared ({why}) — no reboot"), } } @@ -116,7 +151,10 @@ pub fn pm1a_control() -> Option { } /// Write the reset register and nothing else: no lock, nothing but the port -/// the FADT named. A machine with no reset register halts. +/// the FADT named. A machine with no reset register halts. Where the register +/// is `SMI_CMD` this `out` is no write of `smi_cmd`'s: Table 5.9 asks the boot +/// processor of a command to `SMI_CMD` and nothing of the reset, which is +/// made from whichever CPU ends the machine. // No fallback: 0xCF9, the keyboard controller and anything else are written only where a table named them. pub fn reset() -> ! { if let Some(reset) = RESET.get() { diff --git a/kernel/src/arch/x86_64/smi_cmd.rs b/kernel/src/arch/x86_64/smi_cmd.rs index e0aea0018e2..3f17585892f 100644 --- a/kernel/src/arch/x86_64/smi_cmd.rs +++ b/kernel/src/arch/x86_64/smi_cmd.rs @@ -30,7 +30,9 @@ //! `issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md`'s. //! //! The port is this module's alone, so the `out` in [`answer`] is the only -//! one the kernel can make to it, and [`answer`] reads which CPU it is on +//! command the kernel can make to it, the reset's `out` aside where the +//! FADT's reset register is this port (`power::reset`), which ends the +//! machine; and [`answer`] reads which CPU it is on //! with interrupts closed beside that `out`: no caller's state decides it. //! It is counted there too, with the time it held the boot processor //! ([`counted`]), whoever asked for it. @@ -38,7 +40,7 @@ //! **The `acpi` claim's holder writes no byte here: it asks for one**, and the //! declaration says which bytes are never written for it //! (`toyos_userbound::Mediated::Command`): those the FADT gives a meaning, -//! which are this kernel's own commands. +//! which are this kernel's own commands and its reset's value. //! //! **None is made once the stop has begun**: the power-off waits out a write //! in flight ([`settle`]) and then owns the hardware, and an SMI it did not @@ -50,7 +52,7 @@ use core::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering::{Acquire, Rel use toyos_userbound::{KeptCommands, Mediated, Ports, Undeclared}; -use super::pio::{self, Slot, TakenBack}; +use super::pio::{self, Declared, Slot, TakenBack}; use super::{apic, cpu, percpu, IrqGuard}; use crate::shootdown::Shootdown; use crate::sync::Lock; @@ -113,11 +115,16 @@ impl fmt::Display for Written { /// Declare the port the FADT names, and the values it names for it. Boot's. /// The `acpi` claim's holder reads it and never writes it: a write is a /// command to the firmware, and [`write`] makes every one. -pub fn declare(port: u16, named: [Option; 5]) -> Result<(), Undeclared> { +pub fn declare(port: u16, named: [Option; 6]) -> Result<(), Undeclared> { PORT.set(pio::declare("SMI_CMD", Ports::one(port), Mediated::Command(KeptCommands(named)))?); Ok(()) } +/// The port, where [`declare`] declared one. +pub fn declared() -> Option { + PORT.get() +} + /// How many writes this CPU has made and the nanoseconds they held it, on /// the boot processor of a machine that names the port; `None` on every /// other CPU, which makes none. diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 800ba26daf7..1c8980c4d79 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -515,7 +515,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { // First in the device phase, after storage: its lines are the diagnostic // boot's answer for a dead keyboard, and a panel shows the log's tail. - arch::boot::platform_devices(kernel_args.rsdp_addr); + arch::boot::platform_devices(&platform, kernel_args.rsdp_addr); #[cfg(feature = "boot-actuators")] arch::boot::interrupt_selftests(); diff --git a/tests/common/power.rs b/tests/common/power.rs index 7ffbbea66a0..07baa051bf8 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -676,8 +676,8 @@ pub fn usb_reset_on_metal(arms: &[&super::metal::Readback]) -> Result<(), String } /// What `/system/bin/acpiserver` says once it serves q35's power button, -/// whole: q35 has no embedded controller. -const ACPI_ARMED: &str = "acpiserver: armed: power button served, embedded controller none"; +/// whole, before it loads the tables. +const ACPI_ARMED: &str = "acpiserver: armed: power button served"; /// The server's line for the press, naming the SCI it came on. const ACPI_PRESSED: &str = diff --git a/tests/toyos-rust-tests/src/bin/counters_metal.rs b/tests/toyos-rust-tests/src/bin/counters_metal.rs index fc58389d0c0..15ce754a0d9 100644 --- a/tests/toyos-rust-tests/src/bin/counters_metal.rs +++ b/tests/toyos-rust-tests/src/bin/counters_metal.rs @@ -12,7 +12,7 @@ //! through the `/log` fileserver on that fileserver's CPU. //! //! **`idle0` waits for the ACPI server's first lines** ([`acpi_said`]): -//! `acpiserver` logs its arming, and a query number the first time the +//! `acpiserver` logs whether it serves an embedded controller, and a query number the first time the //! embedded controller raises it, and the kernel the claim's first interrupt, //! at times the machine chooses. A query number first raised later, on a quiet //! machine, is the `counters` row's to refuse. @@ -63,8 +63,8 @@ const LOADED: Duration = Duration::from_secs(20); /// at its write budget (`userland/logkeeper/src/policy.rs`, 5 s). const SETTLE_BOUND: Duration = Duration::from_secs(10); -/// How long [`acpi_said`] waits for the server to arm and the controller's -/// first query and SCI to come. +/// How long [`acpi_said`] waits for the server to say its controller and the +/// controller's first query and SCI to come. const ACPI_BOUND: Duration = Duration::from_secs(10); /// What this binary's own children are asked to do: exit at once. @@ -168,16 +168,19 @@ fn print(phase: &str, read: &Read) { } } -/// Wait until `acpiserver` has armed and, where it serves an embedded -/// controller, the kernel has logged the claim's first interrupt and the server -/// its first query. +/// Wait until `acpiserver` has said whether it serves an embedded controller +/// and, where it does, the kernel has logged the claim's first interrupt and +/// the server its first query. fn acpi_said(log: &mut Log) { let mut armed: Option = None; let (mut interrupt, mut query) = (false, false); - log.until("the ACPI server armed, and its controller's first SCI and query", ACPI_BOUND, |line| { + log.until("the ACPI server's controller, and its first SCI and query", ACPI_BOUND, |line| { if let Some(said) = program_line(line).filter(|said| said.tag == "acpiserver") { - if let Some(rest) = said.text.strip_prefix("acpiserver: armed: ") { - armed = Some(!rest.ends_with("embedded controller none")); + if said.text.starts_with(acpiserver_api::CONTROLLER_SERVED) { + armed = Some(true); + } + if said.text.starts_with(acpiserver_api::CONTROLLER_NONE) { + armed = Some(false); } query |= said.text.starts_with("acpiserver: embedded controller query ") && said.text.contains(" taken for the first time"); diff --git a/tests/toyos.rs b/tests/toyos.rs index cee905639e5..c05e948e69d 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5536,11 +5536,12 @@ fn counters_on_metal(back: &metal::Readback) -> Result<(), String> { Ok(()) } -/// The T14's ACPI row as the kernel filled it from the machine's FACP, ECDT -/// and APIC (Linux on the same machine: `EC_CMD/EC_SC=0x66, EC_DATA=0x62`, -/// `GPE=0x6e`, `INT_SRC_OVR (bus 0 bus_irq 9 global_irq 9 high level)`), the -/// server armed on it, at least one embedded-controller query taken and a -/// count of them logged, and no guard of the server's fired. +/// The T14's ACPI row as the kernel filled it from the machine's FACP and +/// APIC (Linux on the same machine: `INT_SRC_OVR (bus 0 bus_irq 9 global_irq +/// 9 high level)`), the server armed on it, the embedded controller it found +/// in the DSDT the one Linux used (`EC_CMD/EC_SC=0x66, EC_DATA=0x62`, +/// `GPE=0x6e`), at least one of its queries taken and a count of them +/// logged, and no guard of the server's fired. fn acpi_events_on_metal(back: &metal::Readback) -> Result<(), String> { back.job_passed("test_rs_acpi_hold")?; let (log, kernel) = (back.log(), back.kernel()); @@ -5549,12 +5550,10 @@ fn acpi_events_on_metal(back: &metal::Readback) -> Result<(), String> { eprintln!(" [acpi] {}", enabled.trim()); kernel.must_say( "acpi: the ACPI row: PM1a events 0x1800+4, GPE0 0x1860+32, SCI gsi 9 level/high, the \ - fixed-hardware power button, embedded controller at 0x66/0x62 on GPE 0x6e; the firmware \ - handed over in legacy mode", - )?; - log.must_say( - "acpiserver: armed: power button served, embedded controller on GPE 0x6e at 0x66/0x62", + fixed-hardware power button; the firmware handed over in legacy mode", )?; + log.must_say("acpiserver: armed: power button served")?; + log.must_say(&format!("{}0x6e at 0x66/0x62", acpiserver_api::CONTROLLER_SERVED))?; let lines: Vec<&str> = log .text() .lines() diff --git a/toyos-abi/src/acpi.rs b/toyos-abi/src/acpi.rs index d4ce49f49a7..43a2cb48173 100644 --- a/toyos-abi/src/acpi.rs +++ b/toyos-abi/src/acpi.rs @@ -3,8 +3,8 @@ //! //! The kernel puts the machine in ACPI mode when it mints the claim and back //! in the mode its firmware handed over when the claim goes; the holder gets -//! the register blocks the FADT and the ECDT name, as ports it may `in` and -//! `out`, and the SCI as records on its own claim handle. +//! the register blocks the FADT names, as ports it may `in` and `out`, and +//! the SCI as records on its own claim handle. //! //! **The SCI is a level line, masked by the kernel each time it is taken.** The //! holder clears the status bits behind it and then acknowledges the claim @@ -64,20 +64,8 @@ crate::user_safe! { pub pm1_event: Block, /// The GPE0 block, its status half then its enable half. pub gpe0: Block, - /// The embedded controller's command/status and data ports, as the ECDT - /// names them; `len` 0 where the machine named none. - pub ec_command: Block, - pub ec_data: Block, - /// The GPE the embedded controller raises, inside [`Self::gpe0`]. - pub ec_gpe: u16, pub flags: u16, - pub reserved: u32, - } -} - -impl AcpiInfo { - pub fn has_ec(&self) -> bool { - self.ec_command.len != 0 + pub reserved: [u16; 3], } } @@ -216,7 +204,8 @@ pub enum Refused { RangeRegistersDiffer = 16, /// A byte for `SMI_CMD` that the FADT gives a meaning (ACPI 6.5 Table /// 5.9: `ACPI_ENABLE`, `ACPI_DISABLE`, `S4BIOS_REQ`, `PSTATE_CNT`, - /// `CST_CNT`): the kernel's to write, or nobody's. + /// `CST_CNT`, and `RESET_VALUE` where the reset register is `SMI_CMD`): + /// the kernel's to write, or nobody's. KernelCommand = 17, /// A write that reaches `SMI_CMD` and is not one byte to it. CommandSpan = 18, diff --git a/toyos-acpi/src/ecdt.rs b/toyos-acpi/src/ecdt.rs deleted file mode 100644 index f430c3a62a8..00000000000 --- a/toyos-acpi/src/ecdt.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! The ECDT: ACPI 6.5 §5.2.16, Table 5.88 — the embedded controller's ports -//! and its GPE, named for an OS that reaches the controller before any AML. - -use crate::{Phys, Table, SDT_HEADER_LEN}; - -/// Table 5.88: `EC_CONTROL` and `EC_DATA` are Generic Address Structures, -/// then `UID` (4) and `GPE_BIT` (1). -const EC_CONTROL: usize = SDT_HEADER_LEN; -const EC_DATA: usize = EC_CONTROL + 12; -const GPE_BIT: usize = EC_DATA + 12 + 4; - -/// The bytes [`ecdt`] reads to the end of. -pub const ECDT_NEEDED: usize = GPE_BIT + 1; - -const SPACE_SYSTEM_IO: u8 = 1; - -/// The embedded controller as the ECDT names it. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Ec { - /// The command register when written and the status register when read. - pub command: u16, - pub data: u16, - /// The GPE it raises. - pub gpe: u8, -} - -/// Which register an [`EcRefused`] is about. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Register { - Command, - Data, -} - -/// Why the ECDT names no controller this kernel hands out. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum EcRefused { - /// The table ends before `GPE_BIT`. - Short { len: usize }, - /// Not in the System I/O space: an MMIO controller is not served. - NotSystemIo { register: Register, space: u8 }, - /// Not one byte wide at bit 0. - Width { register: Register, bit_width: u8, bit_offset: u8 }, - /// Zero, or past the 16-bit port space. - Address { register: Register, address: u64 }, -} - -/// The controller the ECDT names. -pub fn ecdt(table: &Table

) -> Result { - let short = EcRefused::Short { len: table.len() }; - let port = |register, at: usize| { - let field = |offset| table.byte(at + offset).ok_or(short); - let (space, bit_width, bit_offset) = (field(0)?, field(1)?, field(2)?); - let address = table.u64_at(at + 4).ok_or(short)?; - if space != SPACE_SYSTEM_IO { - return Err(EcRefused::NotSystemIo { register, space }); - } - if (bit_width, bit_offset) != (8, 0) { - return Err(EcRefused::Width { register, bit_width, bit_offset }); - } - match u16::try_from(address) { - Ok(port) if port != 0 => Ok(port), - _ => Err(EcRefused::Address { register, address }), - } - }; - Ok(Ec { - command: port(Register::Command, EC_CONTROL)?, - data: port(Register::Data, EC_DATA)?, - gpe: table.byte(GPE_BIT).ok_or(short)?, - }) -} diff --git a/toyos-acpi/src/fadt.rs b/toyos-acpi/src/fadt.rs index f6cef2936c5..e1fc086ec90 100644 --- a/toyos-acpi/src/fadt.rs +++ b/toyos-acpi/src/fadt.rs @@ -23,10 +23,6 @@ const FADT_X_DSDT: usize = 140; const PSCI_COMPLIANT: u16 = 1 << 0; const PSCI_USE_HVC: u16 = 1 << 1; -/// The bytes [`reset_register`] reads to the end of, so a caller opening the -/// table for that decode alone asks for what it needs and nothing after it. -pub const FADT_FOR_RESET: usize = FADT_RESET_VALUE + size_of::(); - /// Fixed feature flags bit 10, `RESET_REG_SUP` (Table 5.10); then the address space IDs of Table 5.1. const RESET_REG_SUP: u32 = 1 << 10; const SPACE_SYSTEM_MEMORY: u8 = 0; @@ -263,8 +259,11 @@ impl SmiCmd { /// The five values, each `None` where the tables name none: `S4BIOS_REQ` /// where `s4bios`, the FACS's `S4BIOS_F`, is clear, whatever the field - /// holds, and each other where its field is zero. - pub fn named(&self, s4bios: bool) -> [Option; 5] { + /// holds, and each other where its field is zero; then `RESET_VALUE` + /// where `reset`, the FADT's reset register, is this very port, a write + /// of which resets the machine (Table 5.9, `RESET_REG`) and is a sixth + /// meaning. + pub fn named(&self, s4bios: bool, reset: Reset) -> [Option; 6] { let nonzero = |value: u8| (value != 0).then_some(value); [ nonzero(self.acpi_enable), @@ -272,6 +271,10 @@ impl SmiCmd { s4bios.then_some(self.s4bios_req), nonzero(self.pstate_cnt), nonzero(self.cst_cnt), + match reset { + Reset::Port { port, value } if port == self.port => Some(value), + _ => None, + }, ] } } diff --git a/toyos-acpi/src/lib.rs b/toyos-acpi/src/lib.rs index 2693ecc281e..9a3f2479589 100644 --- a/toyos-acpi/src/lib.rs +++ b/toyos-acpi/src/lib.rs @@ -15,7 +15,6 @@ #![no_std] #![forbid(unsafe_code)] -mod ecdt; mod facs; mod fadt; mod gtdt; @@ -28,13 +27,12 @@ mod spcr; use toyos_bootmap::DirectMapEnd; -pub use ecdt::{ecdt, Ec, EcRefused, Register, ECDT_NEEDED}; pub use facs::{acquire, facs, release, Facs, FacsRefused, FACS_GLOBAL_LOCK, OWNED, PENDING}; pub use fadt::{ century_of, dsdt_address, fixed_hardware, iapc_boot_arch, pm1a_control, psci, reset_register, rtc_century, Century, Field, FixedHardware, FixedRefused, LegacyMode, PowerButton, Psci, Reset, SmiCmd, CMOS_RAM, - FADT_FOR_FIXED_HARDWARE, FADT_FOR_RESET, FADT_PM1A_CNT_BLK, + FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, }; use fadt::FADT_DSDT; pub use iort::{iort, Iort, IortRefused, ItsDevice, Node, Route, Smmuv3}; @@ -47,7 +45,7 @@ pub use madt::{ }; pub use gtdt::{gtdt, Gtdt, TimerInterrupt, GTDT_NEEDED}; pub use mcfg::{ecam_allocations, AllocationRefused, Allocations, ConfigRegister, EcamWindow, SEGMENT_GROUP}; -pub use resource::{memory_windows, ResourceError, MAX_LIST_BYTES}; +pub use resource::{io_ports, memory_windows, ResourceError, MAX_LIST_BYTES}; pub use spcr::{spcr, Gas, SerialInterface, Spcr, GAS_SYSTEM_MEMORY, SPCR_NEEDED}; /// Physical memory, as this decoder reads it. @@ -62,6 +60,18 @@ pub trait Phys: Copy { fn byte(self, phys: u64) -> u8; } +/// Bytes an evaluation handed over, `_CRS`'s buffer among them, read as if +/// they began at address 0. +impl Phys for &[u8] { + fn readable(self, phys: u64, len: usize) -> bool { + usize::try_from(phys).ok().and_then(|at| at.checked_add(len)).is_some_and(|end| end <= self.len()) + } + + fn byte(self, phys: u64) -> u8 { + self[phys as usize] + } +} + /// Physical memory as a kernel reads it through its direct map, one byte at a /// time. /// diff --git a/toyos-acpi/src/resource.rs b/toyos-acpi/src/resource.rs index 26273cce3bc..15bb21f8760 100644 --- a/toyos-acpi/src/resource.rs +++ b/toyos-acpi/src/resource.rs @@ -1,5 +1,6 @@ //! ACPI resource descriptors, decoded into the memory windows a PCI root -//! bridge decodes. +//! bridge decodes, and into the I/O ports a device names its registers by +//! ([`io_ports`]). //! //! The bytes are the ones ACPI 6.5 §6.4.3.5 defines and firmware emits in two //! places for the same bridge: `_CRS` in the DSDT, which is what Linux reads, @@ -71,8 +72,13 @@ pub enum ResourceError { /// A memory range the bridge consumes rather than forwards — its own /// registers, not a range anything behind it decodes. Consumed { min: u64 }, - /// More memory windows than the caller has room for. + /// More memory windows, or I/O runs, than the caller has room for. TooMany { room: usize }, + /// An I/O Port Descriptor naming a range of bases the OS chooses among, + /// not one fixed run. + Relocatable { min: u16, max: u16 }, + /// An I/O run of no ports. + NoPorts { port: u16 }, } impl core::fmt::Display for ResourceError { @@ -97,8 +103,10 @@ impl core::fmt::Display for ResourceError { write!(f, "the range at {min:#x} is consumed by the bridge, not forwarded") } Self::TooMany { room } => { - write!(f, "more memory windows than the {room} there is room for") + write!(f, "more memory windows or I/O runs than the {room} there is room for") } + Self::Relocatable { min, max } => write!(f, "the I/O run is relocatable between {min:#x} and {max:#x}"), + Self::NoPorts { port } => write!(f, "the I/O run at {port:#x} is no ports long"), } } } @@ -208,3 +216,56 @@ pub fn memory_windows(phys: P, at: u64, out: &mut [RootBridgeWindow]) - offset = through; } } + +/// ACPI 6.5 §6.4.2.5: the I/O Port Descriptor, a small item of seven bytes: +/// its decode, its range's minimum and maximum base, alignment and length. +const IO_PORT: u8 = 0x47; +/// ACPI 6.5 §6.4.2.6: the Fixed Location I/O Port Descriptor, a small item of +/// three bytes: a 10-bit base and a length. +const FIXED_IO_PORT: u8 = 0x4B; + +/// The first port of each I/O run the list at `at` names, in its order, +/// written into `out`, and how many: what a device that names its own +/// registers by `_CRS` and by nothing else names them with (ACPI 6.5 §12.11, +/// the embedded controller's). +/// +/// **Only a run firmware fixed is a port**: an I/O Port Descriptor whose +/// minimum and maximum base differ names a range the OS chooses within, and +/// is refused, as is a run of no ports and any descriptor but these two. +pub fn io_ports(phys: P, at: u64, out: &mut [u16]) -> Result { + let mut offset = 0usize; + let mut found = 0usize; + loop { + if offset >= MAX_LIST_BYTES { + return Err(ResourceError::Unterminated); + } + let item = item(phys, at, offset)?; + let head = at + offset as u64; + let byte = |i: u64| phys.byte(head + i); + let (min, max, length) = match item.tag { + END_TAG => return Ok(found), + IO_PORT => { + let base = |i| u16::from(byte(i)) | u16::from(byte(i + 1)) << 8; + (base(2), base(4), byte(7)) + } + FIXED_IO_PORT => { + let base = (u16::from(byte(1)) | u16::from(byte(2)) << 8) & 0x3FF; + (base, base, byte(3)) + } + tag => return Err(ResourceError::UnknownTag { tag }), + }; + if min != max { + return Err(ResourceError::Relocatable { min, max }); + } + if length == 0 { + return Err(ResourceError::NoPorts { port: min }); + } + let room = out.len(); + match out.get_mut(found) { + Some(slot) => *slot = min, + None => return Err(ResourceError::TooMany { room }), + } + found += 1; + offset += item.len; + } +} diff --git a/toyos-acpi/tests/amd_laptop.rs b/toyos-acpi/tests/amd_laptop.rs new file mode 100644 index 00000000000..df5a77222d1 --- /dev/null +++ b/toyos-acpi/tests/amd_laptop.rs @@ -0,0 +1,92 @@ +//! An AMD laptop's fixed hardware and reset register, decoded from an extract +//! of its FACP: the machine whose reset register is its `SMI_CMD`, and whose +//! power button is a control method device. +//! +//! **An extract, never the table**: the FACP here is laid out from the fields +//! `fixed_hardware`, `pm1a_control` and `reset_register` read, at their +//! offsets and with the machine's own bytes, and zeros everywhere else, then +//! sealed. The whole table was checked against these fields outside the tree. +//! +//! **No field here is an OEM string or identifier**, the condition the +//! orchestrator ruled an extract of this laptop's FACP is committed under: +//! the header's OEM ID, OEM table ID, OEM revision and creator fields are +//! zeros (`common::sdt`), and nothing names the machine but "an AMD laptop". + +mod common; + +use common::{sdt, Machine}; +use toyos_abi::acpi::Block; +use toyos_acpi::{ + fixed_hardware, pm1a_control, reset_register, FixedHardware, PowerButton, Reset, SmiCmd, Table, FADT_FOR_FIXED_HARDWARE, +}; + +/// The FACP's fields: (offset, the machine's bytes there). Revision 5, 268 +/// bytes long; a Generic Address Structure is space, bit width, bit offset, +/// access size, then the address. +const FACP_LEN: usize = 268; +const FACP_REVISION: u8 = 5; +const FACP_FIELDS: &[(usize, &[u8])] = &[ + (46, &[0x09, 0x00]), + (48, &[0xb0, 0x00, 0x00, 0x00]), + (52, &[0xa0, 0xa1, 0x00, 0x00]), + (56, &[0x00, 0x04, 0x00, 0x00]), + (64, &[0x04, 0x04, 0x00, 0x00]), + (80, &[0x20, 0x04, 0x00, 0x00]), + (88, &[0x04, 0x02]), + (92, &[0x08, 0x00]), + (112, &[0xbd, 0xc5, 0x20, 0x00]), + (116, &[0x01, 0x08, 0x00, 0x01, 0xb0, 0, 0, 0, 0, 0, 0, 0]), + (128, &[0xfb]), + (148, &[0x01, 0x20, 0x00, 0x03, 0x00, 0x04, 0, 0, 0, 0, 0, 0]), + (172, &[0x01, 0x10, 0x00, 0x02, 0x04, 0x04, 0, 0, 0, 0, 0, 0]), + (220, &[0x01, 0x40, 0x00, 0x04, 0x20, 0x04, 0, 0, 0, 0, 0, 0]), +]; + +const AT: u64 = 0x9000_0000; + +fn fadt() -> Table> { + let mut body = vec![0u8; FACP_LEN - 36]; + for &(at, bytes) in FACP_FIELDS { + body[at - 36..at - 36 + bytes.len()].copy_from_slice(bytes); + } + let bytes: &'static [u8] = Box::leak(sdt(b"FACP", FACP_REVISION, &body).into_boxed_slice()); + let regions: &'static [(u64, &[u8])] = Box::leak(Box::new([(AT, bytes)])); + Table::open(Machine { regions }, AT, b"FACP", FADT_FOR_FIXED_HARDWARE).expect("a sealed extract") +} + +/// `SMI_CMD` 0xb0 with `ACPI_ENABLE` 0xa0 and `ACPI_DISABLE` 0xa1, PM1a +/// events at 0x400, control at 0x404, eight bytes of GPE0 at 0x420, and +/// flags 0x0020c5bd, whose bit 4 makes the power button a control method +/// device. +#[test] +fn the_fadt_names_a_control_method_power_button() { + let fadt = fadt(); + assert_eq!( + fixed_hardware(&fadt), + Ok(FixedHardware { + sci_int: 9, + smi_cmd: Some(SmiCmd { port: 0xb0, acpi_enable: 0xa0, acpi_disable: 0xa1, s4bios_req: 0, pstate_cnt: 0, cst_cnt: 0 }), + pm1a_event: Block { port: 0x400, len: 4 }, + gpe0: Block { port: 0x420, len: 8 }, + power_button: PowerButton::ControlMethod, + }) + ); + assert_eq!(pm1a_control(&fadt), Ok(Block { port: 0x404, len: 2 })); +} + +/// The reset register is SystemIO 0xb0 with `RESET_VALUE` 0xfb: `SMI_CMD`'s +/// own port. So 0xfb is a sixth value the tables give that port a meaning, +/// and is among the bytes kept from whoever asks for a write there; a reset +/// register anywhere else adds none. +#[test] +fn the_reset_register_is_smi_cmd_and_its_value_is_one_the_tables_name() { + let fadt = fadt(); + let reset = reset_register(&fadt); + assert_eq!(reset, Reset::Port { port: 0xb0, value: 0xfb }); + let smi_cmd = fixed_hardware(&fadt).expect("the extract decodes").smi_cmd.expect("a port"); + assert_eq!(smi_cmd.named(false, reset), [Some(0xa0), Some(0xa1), None, None, None, Some(0xfb)]); + assert_eq!(smi_cmd.named(false, Reset::Port { port: 0xcf9, value: 0xfb }), [Some(0xa0), Some(0xa1), None, None, None, None]); + for elsewhere in [Reset::Absent, Reset::Unsupported, Reset::SystemMemory, Reset::Address(0xb0_0000)] { + assert_eq!(smi_cmd.named(false, elsewhere)[5], None, "{elsewhere:?}"); + } +} diff --git a/toyos-acpi/tests/corpus.rs b/toyos-acpi/tests/corpus.rs index 7dc6e13fd68..caad92af233 100644 --- a/toyos-acpi/tests/corpus.rs +++ b/toyos-acpi/tests/corpus.rs @@ -12,10 +12,10 @@ use common::{declare_len, entry, madt, rsdp, sdt, t14_root_bridge, xsdt, Machine use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ - definition_blocks, dsdt_address, ecam_allocations, ecdt, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, + definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, - EcRefused, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, - Register, Reset, SourceOverride, Table, TableError, Trigger, ECDT_NEEDED, + Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, + Reset, SourceOverride, Table, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, MAX_TABLE_LEN, }; @@ -865,8 +865,8 @@ fn the_values_the_fadt_names_for_smi_cmd_are_each_read_from_their_own_byte() { .smi_cmd .expect("a port"); assert_eq!(all, SmiCmd { port: 0xb2, acpi_enable: 0xa0, acpi_disable: 0xa1, s4bios_req: 0xa2, pstate_cnt: 0xa3, cst_cnt: 0xa4 }); - assert_eq!(all.named(true), [Some(0xa0), Some(0xa1), Some(0xa2), Some(0xa3), Some(0xa4)]); - assert_eq!(all.named(false), [Some(0xa0), Some(0xa1), None, Some(0xa3), Some(0xa4)]); + assert_eq!(all.named(true, Reset::Absent), [Some(0xa0), Some(0xa1), Some(0xa2), Some(0xa3), Some(0xa4), None]); + assert_eq!(all.named(false, Reset::Absent), [Some(0xa0), Some(0xa1), None, Some(0xa3), Some(0xa4), None]); for (at, named) in [(52, 0), (53, 1), (54, 2), (55, 3), (95, 4)] { let one = fixed(|t| { t[52..56].fill(0); @@ -876,16 +876,16 @@ fn the_values_the_fadt_names_for_smi_cmd_are_each_read_from_their_own_byte() { .expect("a FADT naming one") .smi_cmd .expect("a port"); - let mut want = [None; 5]; + let mut want = [None; 6]; want[named] = Some(0x5a); // With the flag set `S4BIOS_REQ` names the byte it holds, a zero where it holds one. let mut flagged = want; flagged[2] = Some(if named == 2 { 0x5a } else { 0 }); - assert_eq!(one.named(true), flagged, "the byte at {at}, S4BIOS_F set"); + assert_eq!(one.named(true, Reset::Absent), flagged, "the byte at {at}, S4BIOS_F set"); if named == 2 { want[2] = None; } - assert_eq!(one.named(false), want, "the byte at {at}, S4BIOS_F clear"); + assert_eq!(one.named(false, Reset::Absent), want, "the byte at {at}, S4BIOS_F clear"); } } @@ -906,44 +906,6 @@ fn a_revision_1_fadt_is_read_at_its_32_bit_fields_alone() { assert_eq!(fixed_hardware(&fadt).map(|f| f.pm1a_event), Ok(Block { port: 0x600, len: 4 })); } -fn ec_table(edit: impl FnOnce(&mut [u8])) -> Result { - let mut body = vec![0u8; ECDT_NEEDED - 36]; - body[..12].copy_from_slice(&[1, 8, 0, 0, 0x66, 0, 0, 0, 0, 0, 0, 0]); - body[12..24].copy_from_slice(&[1, 8, 0, 0, 0x62, 0, 0, 0, 0, 0, 0, 0]); - body[28] = 0x6e; - edit(&mut body); - let t = sdt(b"ECDT", 1, &body); - let regions: &[(u64, &[u8])] = &[(TABLE_AT, &t)]; - ecdt(&Table::open(Machine { regions }, TABLE_AT, b"ECDT", 36).expect("ECDT")) -} - -#[test] -fn an_ecdt_naming_no_port_controller_is_refused_by_name() { - assert_eq!(ec_table(|_| {}), Ok(toyos_acpi::Ec { command: 0x66, data: 0x62, gpe: 0x6e })); - assert_eq!( - ec_table(|b| b[0] = 0), - Err(EcRefused::NotSystemIo { register: Register::Command, space: 0 }), - "a controller in memory" - ); - assert_eq!( - ec_table(|b| b[13] = 16), - Err(EcRefused::Width { register: Register::Data, bit_width: 16, bit_offset: 0 }) - ); - assert_eq!( - ec_table(|b| b[4] = 0), - Err(EcRefused::Address { register: Register::Command, address: 0 }), - "an ECDT of zeros, which some firmware publishes" - ); - assert_eq!( - ec_table(|b| b[16 + 2] = 1), - Err(EcRefused::Address { register: Register::Data, address: 0x1_0062 }) - ); - let short = sdt(b"ECDT", 1, &[1, 8, 0, 0, 0x66, 0, 0, 0, 0, 0, 0, 0]); - let regions: &[(u64, &[u8])] = &[(TABLE_AT, &short)]; - let table = Table::open(Machine { regions }, TABLE_AT, b"ECDT", 36).expect("ECDT"); - assert_eq!(ecdt(&table), Err(EcRefused::Short { len: 48 })); -} - /// Table 5.9: the OS treats the SCI as level and active low. Where no override /// names it, and where one names it conforming, that is what it is; where one /// says otherwise, what it says. diff --git a/toyos-acpi/tests/resource.rs b/toyos-acpi/tests/resource.rs index 2a32804ce75..5143196b739 100644 --- a/toyos-acpi/tests/resource.rs +++ b/toyos-acpi/tests/resource.rs @@ -5,7 +5,7 @@ mod common; use common::{qword_descriptor, resource_list as list, Machine, BUS, IO, MEMORY}; use toyos_abi::boot::RootBridgeWindow; -use toyos_acpi::{memory_windows, ResourceError, MAX_LIST_BYTES}; +use toyos_acpi::{io_ports, memory_windows, ResourceError, MAX_LIST_BYTES}; /// Where a firmware pool allocation sits in the crafted machines below. const AT: u64 = 0x7f00_1234; @@ -193,3 +193,59 @@ fn a_list_that_runs_off_the_end_of_what_can_be_read_is_refused() { bytes.truncate(40); assert_eq!(windows(&bytes, 8), Err(ResourceError::Unreadable { at: AT, len: 46 })); } + +/// An I/O Port Descriptor (ACPI 6.5 §6.4.2.5): 16-bit decode, a run of `len` +/// ports whose base is anywhere from `min` to `max`. +fn io(min: u16, max: u16, len: u8) -> Vec { + let [min_lo, min_hi] = min.to_le_bytes(); + let [max_lo, max_hi] = max.to_le_bytes(); + vec![0x47, 0x01, min_lo, min_hi, max_lo, max_hi, 0x00, len] +} + +/// A Fixed Location I/O Port Descriptor (§6.4.2.6). +fn fixed_io(base: u16, len: u8) -> Vec { + let [lo, hi] = base.to_le_bytes(); + vec![0x4B, lo, hi, len] +} + +/// The first port of every run `bytes` names, read from [`AT`]. +fn ports(bytes: &[u8], room: usize) -> Result, ResourceError> { + let regions: &[(u64, &[u8])] = &[(AT, bytes)]; + let mut out = vec![0; room]; + let count = io_ports(Machine { regions }, AT, &mut out)?; + out.truncate(count); + Ok(out) +} + +/// An AMD laptop's embedded controller, as its `_CRS` names it, decoded: +/// two I/O Port Descriptors of one port each, 0x62 then 0x66, each fixed by +/// a minimum equal to its maximum. ACPI 6.5 §12.11 puts the data register +/// first and the command/status register second; the order is the list's. +#[test] +fn an_amd_laptops_controller_is_two_fixed_ports_in_the_lists_order() { + let crs = list(&[io(0x62, 0x62, 1), io(0x66, 0x66, 1)]); + assert_eq!(ports(&crs, 2), Ok(vec![0x62, 0x66])); + // The same list as an evaluation hands the buffer over, from address 0. + let mut out = [0; 2]; + assert_eq!(io_ports(&crs[..], 0, &mut out), Ok(2)); + assert_eq!(out, [0x62, 0x66]); + // The fixed form names the same two, its base in ten bits. + assert_eq!(ports(&list(&[fixed_io(0x62, 1), fixed_io(0xFC66, 1)]), 2), Ok(vec![0x62, 0x66])); +} + +/// A run the OS would choose within, a run of no ports, any other descriptor +/// and more runs than there is room for are each refused by name, never +/// stepped over. +#[test] +fn an_io_run_that_is_no_fixed_port_is_refused_by_name() { + assert_eq!(ports(&list(&[io(0x62, 0x6A, 1)]), 2), Err(ResourceError::Relocatable { min: 0x62, max: 0x6A })); + assert_eq!(ports(&list(&[io(0x62, 0x62, 0)]), 2), Err(ResourceError::NoPorts { port: 0x62 })); + assert_eq!(ports(&list(&[fixed_io(0x66, 0)]), 2), Err(ResourceError::NoPorts { port: 0x66 })); + // IRQ (§6.4.2.1) beside the two ports. + assert_eq!(ports(&list(&[io(0x62, 0x62, 1), vec![0x22, 0x01, 0x00]]), 2), Err(ResourceError::UnknownTag { tag: 0x22 })); + let three = list(&[io(0x62, 0x62, 1), io(0x66, 0x66, 1), io(0x68, 0x68, 1)]); + assert_eq!(ports(&three, 2), Err(ResourceError::TooMany { room: 2 })); + assert_eq!(ports(&[0x47, 0x01, 0x62], 2), Err(ResourceError::Unreadable { at: AT, len: 8 })); + let unterminated = io(0x62, 0x62, 1).repeat(MAX_LIST_BYTES / 8 + 1); + assert_eq!(ports(&unterminated, MAX_LIST_BYTES), Err(ResourceError::Unterminated)); +} diff --git a/toyos-acpi/tests/thinkpad_t14.rs b/toyos-acpi/tests/thinkpad_t14.rs index 7f05a10bdb7..3d4237a68ff 100644 --- a/toyos-acpi/tests/thinkpad_t14.rs +++ b/toyos-acpi/tests/thinkpad_t14.rs @@ -1,9 +1,9 @@ -//! The T14's fixed hardware, embedded controller and SCI, decoded from an -//! extract of its FACP, ECDT and APIC and held against what Linux read on the +//! The T14's fixed hardware and SCI, decoded from an +//! extract of its FACP and APIC and held against what Linux read on the //! same machine (`fixtures/thinkpad-t14/SOURCE`). //! //! **An extract, never the tables**: each table here is laid out from the -//! fields `fixed_hardware`, `pm1a_control`, `ecdt` and `sci_line` read, at their offsets and +//! fields `fixed_hardware`, `pm1a_control` and `sci_line` read, at their offsets and //! with the machine's own bytes, and zeros everywhere else, then sealed. The //! whole tables are checked against these fields outside the tree. @@ -12,8 +12,8 @@ mod common; use common::{entry, madt, sdt, Machine}; use toyos_abi::acpi::Block; use toyos_acpi::{ - ecdt, fixed_hardware, madt_entries, pm1a_control, sci_line, Ec, FixedHardware, Line, MadtEntry, - Polarity, PowerButton, SmiCmd, SourceOverride, Table, Trigger, ECDT_NEEDED, FADT_FOR_FIXED_HARDWARE, + fixed_hardware, madt_entries, pm1a_control, sci_line, FixedHardware, Line, MadtEntry, + Polarity, PowerButton, SmiCmd, SourceOverride, Table, Trigger, FADT_FOR_FIXED_HARDWARE, MADT_ENTRIES, }; @@ -41,15 +41,6 @@ const FACP_FIELDS: &[(usize, &[u8])] = &[ (232, &[0x01, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0, 0, 0]), ]; -/// The ECDT's `EC_CONTROL`, `EC_DATA`, `UID` and `GPE_BIT`. -const ECDT_REVISION: u8 = 1; -const ECDT_FIELDS: &[(usize, &[u8])] = &[ - (36, &[0x01, 0x08, 0x00, 0x00, 0x66, 0, 0, 0, 0, 0, 0, 0]), - (48, &[0x01, 0x08, 0x00, 0x00, 0x62, 0, 0, 0, 0, 0, 0, 0]), - (60, &[0x00, 0x00, 0x00, 0x00]), - (64, &[0x6e]), -]; - /// The APIC's two interrupt source overrides, whole. const APIC_OVERRIDES: &[[u8; 10]] = &[ [0x02, 0x0a, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00], @@ -97,14 +88,6 @@ fn the_t14s_fadt_names_the_blocks_linux_served_its_sci_through() { assert_eq!(pm1a_control(&fadt), Ok(Block { port: 0x1804, len: 2 })); } -/// Linux: `ACPI: EC: EC_CMD/EC_SC=0x66, EC_DATA=0x62` and `ACPI: EC: GPE=0x6e`. -#[test] -fn the_t14s_ecdt_names_the_controller_linux_used() { - let bytes = laid_out(b"ECDT", ECDT_REVISION, ECDT_NEEDED, ECDT_FIELDS); - let table = open(bytes, b"ECDT", ECDT_NEEDED); - assert_eq!(ecdt(&table), Ok(Ec { command: 0x66, data: 0x62, gpe: 0x6e })); -} - /// Linux: `ACPI: INT_SRC_OVR (bus 0 bus_irq 9 global_irq 9 high level)`. #[test] fn the_t14s_madt_names_its_sci_level_and_active_high() { diff --git a/toyos-userbound/src/port.rs b/toyos-userbound/src/port.rs index d66740e3736..bfc27410789 100644 --- a/toyos-userbound/src/port.rs +++ b/toyos-userbound/src/port.rs @@ -125,9 +125,11 @@ pub enum Mediated { } /// The bytes a port that commands the firmware takes from the kernel alone: -/// those the machine's tables give a meaning, `None` where they name none. +/// those the machine's tables give a meaning, `None` where they name none: +/// the FADT's five for `SMI_CMD`, and its reset register's value where that +/// register is the same port, so no write asked of it resets the machine. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct KeptCommands(pub [Option; 5]); +pub struct KeptCommands(pub [Option; 6]); impl KeptCommands { pub fn holds(self, value: u8) -> bool { diff --git a/toyos-userbound/tests/firmware.rs b/toyos-userbound/tests/firmware.rs index 85625194a5a..7284b11dea8 100644 --- a/toyos-userbound/tests/firmware.rs +++ b/toyos-userbound/tests/firmware.rs @@ -536,8 +536,8 @@ fn the_lock_word_is_exchanged_only_where_all_four_bytes_are_the_firmwares_own() } /// What a crafted FADT names for `SMI_CMD`: `ACPI_ENABLE`, `ACPI_DISABLE`, -/// `S4BIOS_REQ` and `CST_CNT`, and no `PSTATE_CNT`. -const KEPT: KeptCommands = KeptCommands([Some(0xF0), Some(0xF1), Some(0xF2), None, Some(0x85)]); +/// `S4BIOS_REQ` and `CST_CNT`, no `PSTATE_CNT`, and a reset register elsewhere. +const KEPT: KeptCommands = KeptCommands([Some(0xF0), Some(0xF1), Some(0xF2), None, Some(0x85), None]); /// The kernel's declarations as q35 boots with them, with [`KEPT`] for /// `SMI_CMD`'s, and the i8042's row. @@ -603,16 +603,35 @@ fn a_byte_for_smi_cmd_is_a_firmware_call_unless_the_fadt_names_it() { } } // A FADT that names none keeps none. - let unnamed = |_| Standing::Declared(Mediated::Command(KeptCommands([None; 5]))); + let unnamed = |_| Standing::Declared(Mediated::Command(KeptCommands([None; 6]))); for value in [0u64, 0xF0, 0xFF] { assert!(matches!(port(unnamed, 0xB2, Width::Byte, Some(value)), PortVerdict::FirmwareCall(call) if u64::from(call.value()) == value)); } // And one that names zero keeps zero. - let zero = |_| Standing::Declared(Mediated::Command(KeptCommands([None, None, Some(0), None, None]))); + let zero = |_| Standing::Declared(Mediated::Command(KeptCommands([None, None, Some(0), None, None, None]))); assert_eq!(port(zero, 0xB2, Width::Byte, Some(0)), refused_port(Refused::KernelCommand)); assert!(matches!(port(zero, 0xB2, Width::Byte, Some(1)), PortVerdict::FirmwareCall(_))); } +/// An AMD laptop's `SMI_CMD` is its reset register too, 0xb0 with +/// `RESET_VALUE` 0xfb beside `ACPI_ENABLE` 0xa0 and `ACPI_DISABLE` 0xa1: the +/// one declaration keeps the reset's byte with the FADT's own, so no write +/// asked of the port resets the machine, and every other byte is a call. +#[test] +fn a_smi_cmd_that_is_the_reset_register_keeps_the_resets_byte() { + let shared = |port| match port { + 0xB0 => Standing::Declared(Mediated::Command(KeptCommands([Some(0xA0), Some(0xA1), None, None, None, Some(0xFB)]))), + _ => Standing::Free, + }; + for kept in [0xA0u64, 0xA1, 0xFB] { + assert_eq!(port(shared, 0xB0, Width::Byte, Some(kept)), refused_port(Refused::KernelCommand), "{kept:#04x}"); + } + for called in [0x00u64, 0x28, 0xFA, 0xFC, 0xFF] { + assert!(matches!(port(shared, 0xB0, Width::Byte, Some(called)), PortVerdict::FirmwareCall(call) if u64::from(call.value()) == called), "{called:#04x}"); + } + assert_eq!(through(port(shared, 0xB0, Width::Byte, None)), Some((0xB0, Width::Byte))); +} + /// A command is one byte to the one port: a wider write that reaches it, /// from below or from it, is refused, whatever byte would land there, and so /// is a value no byte holds. diff --git a/toyos/src/device.rs b/toyos/src/device.rs index c34783084a7..d5e1900e74a 100644 --- a/toyos/src/device.rs +++ b/toyos/src/device.rs @@ -190,7 +190,7 @@ fn irq_record(dev: &Device) -> Result { + pub interpreter: Interpreter, + pub host: Firmware<'k, K>, +} /// What became of the machine's definition blocks: the load's verdict, which /// its lines say. Whoever evaluates a method after the load asks `blocks` @@ -56,7 +59,7 @@ pub struct Loaded { } /// What a refused evaluation or load is called in a line anyone may quote. -fn kind(why: &Error) -> String { +pub fn kind(why: &Error) -> String { match why { Error::Malformed { why, .. } => format!("malformed AML: {why}"), Error::NotFound(_) => "a name that does not resolve".into(), @@ -97,8 +100,16 @@ pub const NO_S5_HANDED: &str = "acpiserver: no \\_S5 was handed to the kernel, which powers off only on one an ACPI server supplied and refuses a shutdown without one"; /// Load the machine's definition blocks from the RSDP at `rsdp`, and -/// evaluate `\_S5`. -pub fn load(kernel: &K, rsdp: u64) -> Loaded { +/// evaluate `\_S5`; the namespace, where its DSDT loaded, is kept. +pub fn load(kernel: &K, rsdp: u64) -> (Loaded, Option>) { + let mut host = Firmware::new(kernel); + let mut interpreter = Interpreter::new(); + let loaded = load_into(kernel, &mut interpreter, &mut host, rsdp); + let kept = loaded.blocks.first().is_some_and(|dsdt| dsdt.is_ok()) && !host.stopping; + (loaded, kept.then_some(Aml { interpreter, host })) +} + +fn load_into(kernel: &K, interpreter: &mut Interpreter, host: &mut Firmware<'_, K>, rsdp: u64) -> Loaded { let began = Instant::now(); let mut loaded = Loaded::default(); let tables = Tables::new(kernel); @@ -124,14 +135,12 @@ pub fn load(kernel: &K, rsdp: u64) -> Loaded { return loaded; } - let mut host = Firmware::new(kernel); - let mut interpreter = Interpreter::new(); let count = blocks.len(); for (place, block) in blocks.iter().enumerate() { let name = if place == 0 { "DSDT" } else { "SSDT" }; let place = place + 1; let done = match block { - Ok(table) => interpreter.load(&mut host, table).map_err(|why| { + Ok(table) => interpreter.load(host, table).map_err(|why| { println!("{OWN}table {place} was refused {why:x?}"); kind(&why) }), @@ -184,7 +193,7 @@ pub fn load(kernel: &K, rsdp: u64) -> Loaded { ); if loaded.blocks.first().is_some_and(|dsdt| dsdt.is_ok()) { - match s5(&mut interpreter, &mut host) { + match s5(interpreter, host) { Ok((a, b)) => { println!("acpiserver: \\_S5 evaluated: SLP_TYPa={a} SLP_TYPb={b}"); loaded.s5 = Some((a, b)); @@ -233,11 +242,15 @@ fn s5(interpreter: &mut Interpreter, host: &mut dyn Host) -> Result<(u64, u64), } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use crate::host::tests::Scripted; use crate::host::{Take, HELD}; + fn load_only(kernel: &K, rsdp: u64) -> Loaded { + load(kernel, rsdp).0 + } + /// QEMU's own tables where its guest held them /// (`toyos-acpi/fixtures/qemu-11.1.1/SOURCE`), as ACPI reclaim memory. const RSDP: u64 = 0x7fb7_e014; @@ -260,11 +273,18 @@ mod tests { /// What every guest boot does, against the kernel's stand-in: QEMU's /// DSDT is fetched, loads asking the machine for nothing, and its `\_S5` - /// is the `SLP_TYPa=0` the kernel logged on the boot the tables are of. + /// is the `SLP_TYPa=0` the kernel logged on the boot the tables are of; + /// its namespace names no embedded controller, and no control-method + /// button where one is looked for. #[test] fn qemus_tables_load_and_s5_is_what_its_kernel_decoded() { let kernel = machine(QEMU); - assert_eq!(load(&kernel, RSDP), Loaded { blocks: vec![Ok(())], s5: Some((0, 0)), handed: true }); + let (loaded, kept) = load(&kernel, RSDP); + assert_eq!(loaded, Loaded { blocks: vec![Ok(())], s5: Some((0, 0)), handed: true }); + let Aml { mut interpreter, mut host } = kept.expect("QEMU's DSDT loads"); + for control_method in [false, true] { + assert_eq!(crate::devices::find(&mut interpreter, &mut host, crate::devices::tests::GPE0, control_method), crate::devices::Found::default()); + } assert!(kernel.asked.borrow().iter().all(|access| access.write == 0 && access.space == 0)); assert!(!kernel.held.get()); assert_eq!(kernel.handed.get(), Some(0)); @@ -273,7 +293,7 @@ mod tests { // The builders below are `toyos-aml`'s test encodings of §20.2, the few a // definition block here needs. - fn sealed(signature: &[u8; 4], body: &[u8]) -> Vec { + pub fn sealed(signature: &[u8; 4], body: &[u8]) -> Vec { let mut table = vec![0u8; 36]; table[..4].copy_from_slice(signature); table[4..8].copy_from_slice(&(36 + body.len() as u32).to_le_bytes()); @@ -283,22 +303,22 @@ mod tests { table } - fn cat(parts: &[&[u8]]) -> Vec { + pub fn cat(parts: &[&[u8]]) -> Vec { parts.concat() } /// `Name (, )` for a four-character name and a byte. - fn name(name: &[u8; 4], value: u8) -> Vec { + pub fn name(name: &[u8; 4], value: u8) -> Vec { cat(&[&[0x08], name, &[0x0A, value]]) } /// `Name (_S5_, Package (2) { a, b })`. - fn s5_package(a: u8, b: u8) -> Vec { + pub fn s5_package(a: u8, b: u8) -> Vec { cat(&[&[0x08], b"_S5_", &[0x12, 0x06, 0x02, 0x0A, a, 0x0A, b]]) } /// A machine whose XSDT lists a FADT naming `dsdt` and then `ssdts`. - fn crafted(dsdt: &[u8], ssdts: &[&[u8]]) -> Scripted { + pub fn crafted(dsdt: &[u8], ssdts: &[&[u8]]) -> Scripted { const AT: u64 = 0x10_0000; let at = |n: usize| AT + 0x1_0000 * n as u64; let mut fadt_body = vec![0u8; 116 - 36]; @@ -324,7 +344,7 @@ mod tests { Scripted { memory, ..Default::default() } } - const CRAFTED_RSDP: u64 = 0x10_0000 - 0x1000; + pub const CRAFTED_RSDP: u64 = 0x10_0000 - 0x1000; #[test] fn a_refused_ssdt_is_one_block_refused_and_the_rest_load() { @@ -335,7 +355,7 @@ mod tests { let good = sealed(b"SSDT", &name(b"CCCC", 4)); let kernel = crafted(&dsdt, &[&collides, &unsummed, &good]); assert_eq!( - load(&kernel, CRAFTED_RSDP), + load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(()), Err("a name defined twice".into()), Err("its bytes do not sum to zero".into()), Ok(())], s5: Some((5, 7)), @@ -350,7 +370,7 @@ mod tests { let dsdt = sealed(b"DSDT", &[0x08, b'A']); let ssdt = sealed(b"SSDT", &name(b"CCCC", 4)); let kernel = crafted(&dsdt, &[&ssdt, &ssdt]); - let loaded = load(&kernel, CRAFTED_RSDP); + let loaded = load_only(&kernel, CRAFTED_RSDP); assert_eq!(loaded.blocks.len(), 1, "an SSDT was loaded onto no DSDT: {loaded:?}"); assert!(matches!(&loaded.blocks[0], Err(kind) if kind.starts_with("malformed AML: ")), "{loaded:?}"); assert_eq!(loaded.s5, None); @@ -359,12 +379,12 @@ mod tests { #[test] fn tables_the_kernel_will_not_read_are_refused_by_its_name_for_it() { // No RSDP where the claim says: RAM, to the stand-in. - assert_eq!(load(&machine(QEMU), 0x1000), Loaded::default()); + assert_eq!(load_only(&machine(QEMU), 0x1000), Loaded::default()); // The DSDT's own bytes are not the firmware's to read. let kernel = machine(&QEMU[..QEMU.len() - 1]); let ram = "its bytes could not be read: a SystemMemory read the kernel refused UsableMemory, in memory of type 7"; - assert_eq!(load(&kernel, RSDP), Loaded { blocks: vec![Err(ram.into())], s5: None, handed: false }); + assert_eq!(load_only(&kernel, RSDP), Loaded { blocks: vec![Err(ram.into())], s5: None, handed: false }); } /// The shape the first load on the real machine had: its RSDP and XSDT @@ -381,17 +401,17 @@ mod tests { let listed = kernel.memory.split_off(2); kernel.kept = listed.iter().map(|(at, _, bytes)| (*at, at + bytes.len() as u64, 5)).collect(); let kept = "its bytes could not be read: a SystemMemory read the kernel refused MemoryType, in memory of type 5"; - assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Err(kept.into())], s5: None, handed: false }); + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Err(kept.into())], s5: None, handed: false }); } #[test] fn an_s5_that_is_no_package_of_two_integers_is_refused_by_kind() { let absent = sealed(b"DSDT", &name(b"AAAA", 1)); - assert_eq!(load(&crafted(&absent, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); + assert_eq!(load_only(&crafted(&absent, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); let integer = sealed(b"DSDT", &name(b"_S5_", 5)); - assert_eq!(load(&crafted(&integer, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); + assert_eq!(load_only(&crafted(&integer, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); let short = sealed(b"DSDT", &cat(&[&[0x08], b"_S5_", &[0x12, 0x04, 0x01, 0x0A, 0x05]])); - assert_eq!(load(&crafted(&short, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); + assert_eq!(load_only(&crafted(&short, &[]), CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: None, handed: false }); } /// `SLP_TYPa` is the package's first element and the only one handed @@ -401,20 +421,20 @@ mod tests { #[test] fn only_an_s5_the_kernel_kept_is_a_power_off() { let kernel = crafted(&sealed(b"DSDT", &s5_package(7, 9)), &[]); - assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((7, 9)), handed: true }); + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((7, 9)), handed: true }); assert_eq!(kernel.handed.get(), Some(7)); let kernel = crafted(&sealed(b"DSDT", &s5_package(8, 0)), &[]); - assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((8, 0)), handed: false }); + assert_eq!(load_only(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((8, 0)), handed: false }); assert_eq!(kernel.handed.get(), None); for dsdt in [sealed(b"DSDT", &name(b"AAAA", 1)), sealed(b"DSDT", &[0x08, b'A'])] { let kernel = crafted(&dsdt, &[]); - assert!(!load(&kernel, CRAFTED_RSDP).handed); + assert!(!load_only(&kernel, CRAFTED_RSDP).handed); assert_eq!(kernel.handed.get(), None); } let kernel = machine(QEMU); - assert!(!load(&kernel, 0x1000).handed); + assert!(!load_only(&kernel, 0x1000).handed); assert_eq!(kernel.handed.get(), None); } @@ -423,7 +443,7 @@ mod tests { for answered in [0, 3, 40] { let kernel = machine(QEMU); kernel.stops_after.set(Some(answered)); - assert_eq!(load(&kernel, RSDP), Loaded::default(), "after {answered} reads"); + assert_eq!(load_only(&kernel, RSDP), Loaded::default(), "after {answered} reads"); } } @@ -483,13 +503,13 @@ mod tests { let field = toyos_abi::acpi::Access::read(toyos_abi::acpi::Space::SystemMemory, NVS, toyos_abi::acpi::Width::Byte); let kernel = with_nvs(); - assert_eq!(load(&kernel, CRAFTED_RSDP), Loaded { blocks: vec![Ok(())], s5: Some((5, 0)), handed: 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.asked.borrow().last(), Some(&field)); let kernel = with_nvs(); kernel.takes.borrow_mut().push_back(Take::Pending); - let loaded = load(&kernel, CRAFTED_RSDP); + 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"); } diff --git a/userland/acpiserver/src/devices.rs b/userland/acpiserver/src/devices.rs new file mode 100644 index 00000000000..3337a608feb --- /dev/null +++ b/userland/acpiserver/src/devices.rs @@ -0,0 +1,400 @@ +//! The devices this server serves that only the machine's namespace names: +//! the embedded controller (ACPI 6.5 §12.11), whose ports and GPE no table the +//! kernel reads gives, and a power button that is a control method device +//! (§4.8.2.2.1.2), which the AML that notifies it is the only account of. +//! +//! **A device is one whose `_HID` names it** (§6.1.5): an EISA ID's integer +//! or a string, PNP0C09 for the controller and PNP0C0C for the button, and +//! **present where its `_STA` says so** (§6.3.7), bit 0, a device with none +//! being present. +//! +//! **The controller is its `_CRS`'s two fixed ports and its `_GPE`'s +//! integer**: §12.11 puts the data register first and the command/status +//! register second, and a `_GPE` that is a package names a GPE block device, +//! which this server does not serve. The GPE is held to the FADT's GPE0 +//! block, eight a byte of its status half. A machine with more than one +//! controller present has none served. Whatever keeps a controller from +//! being served is said once, by kind; what the firmware chose — a path, a +//! number — goes under [`OWN`]. +//! +//! **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. + +use std::collections::BTreeSet; + +use toyos_abi::acpi::Block; +use acpiserver_api::CONTROLLER_NONE; +use toyos_aml::{Host, Interpreter, Kind, Value}; + +use crate::aml::kind; +use crate::host::OWN; + +/// The embedded controller, as its device names it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Ec { + pub path: String, + pub command: u16, + pub data: u16, + pub gpe: u16, + /// The queries its device defines a method for. + pub queries: BTreeSet, +} + +/// What was found. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Found { + pub ec: Option, + /// Every control-method power button present, by path. + pub buttons: Vec, +} + +const EC: &str = "PNP0C09"; +const POWER_BUTTON: &str = "PNP0C0C"; + +/// Find the controller, and the buttons where `control_method`, 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) { + Ok(walked) => walked, + Err(why) => { + println!("acpiserver: no device of the namespace is served: its walk was refused, {why}"); + return found; + } + }; + let mut controllers = Vec::new(); + for device in devices { + let hid = format!("{device}._HID"); + if !named.contains(&hid) { + continue; + } + let id = match interpreter.evaluate(host, &hid, &[]) { + Ok(value) => id(&value), + Err(why) => { + println!("{OWN}{hid} was refused {why:x?}"); + println!("acpiserver: a device's _HID did not evaluate, so it is no device this server serves: {}", kind(&why)); + continue; + } + }; + let wanted = match id.as_deref() { + Some(EC) => true, + Some(POWER_BUTTON) => control_method, + _ => false, + }; + if !wanted || !present(interpreter, host, &named, &device) { + continue; + } + match id.as_deref() { + Some(EC) => controllers.push(device), + _ => { + println!("{OWN}{device} is the control-method power button"); + found.buttons.push(device); + } + } + } + found.ec = match controllers.as_slice() { + [] => { + println!("{CONTROLLER_NONE}no PNP0C09 device is present"); + None + } + [device] => match controller(interpreter, host, device, gpe0, &named) { + Ok(ec) => { + println!("{OWN}{device} is the embedded controller"); + Some(ec) + } + Err(why) => { + println!("{CONTROLLER_NONE}its device {why}"); + None + } + }, + more => { + println!("{CONTROLLER_NONE}{} PNP0C09 devices are present, and this server serves one", more.len()); + None + } + }; + found +} + +/// What became of an embedded-controller query's method. +#[derive(Debug, PartialEq, Eq)] +pub enum Queried { + Ran, + /// The controller's device defines none for the query. + Absent, + /// By the kind of its refusal. + Refused(String), +} + +/// Run the method the controller's device defines for query `q` (§12.11, +/// `_Qxx`, the number in two hex digits): its Notifies are the host's. +pub fn query(interpreter: &mut Interpreter, host: &mut dyn Host, ec: &Ec, q: u8) -> Queried { + if !ec.queries.contains(&q) { + return Queried::Absent; + } + match interpreter.evaluate(host, &format!("{}._Q{q:02X}", ec.path), &[]) { + Ok(_) => Queried::Ran, + Err(why) => { + println!("{OWN}{}._Q{q:02X} was refused {why:x?}", ec.path); + Queried::Refused(kind(&why)) + } + } +} + +/// Every device's path, and every name's, by one walk. +fn devices(interpreter: &Interpreter) -> Result<(Vec, BTreeSet), String> { + let mut walk = interpreter.walk().map_err(|why| kind(&why))?; + let (mut devices, mut named) = (Vec::new(), BTreeSet::new()); + while let Some(entry) = walk.next() { + if entry.kind == Kind::Device { + devices.push(walk.path().to_string()); + } + named.insert(walk.path().to_string()); + } + Ok((devices, named)) +} + +/// A `_HID` as the text it names (§6.1.5): a string as it is, and an integer +/// as the EISA ID it compresses, three letters of five bits each and four +/// hex digits, its bytes in the order AML stores them. +fn id(value: &Value) -> Option { + match value { + Value::String(text) => String::from_utf8(text.clone()).ok(), + &Value::Integer(compressed) => { + let [vendor_hi, vendor_lo, product_hi, product_lo] = u32::try_from(compressed).ok()?.to_le_bytes(); + let vendor = u16::from_be_bytes([vendor_hi, vendor_lo]); + let letter = |shift: u16| char::from(b'@' + (vendor >> shift & 0x1F) as u8); + Some(format!("{}{}{}{product_hi:02X}{product_lo:02X}", letter(10), letter(5), letter(0))) + } + _ => None, + } +} + +/// Whether `device` is present: bit 0 of its `_STA`, which it is where it has +/// none; one whose `_STA` does not evaluate to an integer is said and is not. +fn present(interpreter: &mut Interpreter, host: &mut dyn Host, named: &BTreeSet, device: &str) -> bool { + let sta = format!("{device}._STA"); + if !named.contains(&sta) { + return true; + } + match interpreter.evaluate(host, &sta, &[]) { + Ok(Value::Integer(status)) => status & 1 != 0, + Ok(_) => { + println!("{OWN}{sta} is no integer"); + println!("acpiserver: a device's _STA is no integer, so it is taken as absent"); + false + } + Err(why) => { + println!("{OWN}{sta} was refused {why:x?}"); + println!("acpiserver: a device's _STA did not evaluate, so it is taken as absent: {}", kind(&why)); + false + } + } +} + +/// The controller `device` names, or why it names none this server serves. +fn controller(interpreter: &mut Interpreter, host: &mut dyn Host, device: &str, gpe0: Block, named: &BTreeSet) -> Result { + let crs = match interpreter.evaluate(host, &format!("{device}._CRS"), &[]) { + Ok(Value::Buffer(list)) => list, + Ok(_) => return Err("names its resources by no buffer".into()), + Err(why) => return Err(format!("names no resources: {}", kind(&why))), + }; + let mut ports = [0u16; 2]; + match toyos_acpi::io_ports(&crs[..], 0, &mut ports) { + Ok(2) => {} + Ok(n) => return Err(format!("names {n} I/O run(s), not the two of §12.11")), + Err(why) => return Err(format!("names its ports by a list this server does not read: {why}")), + } + let [data, command] = ports; + // §5.2.9: a GPE block's status half holds eight GPEs a byte. + let gpes = u64::from(gpe0.len / 2 * 8); + let gpe = match interpreter.evaluate(host, &format!("{device}._GPE"), &[]) { + Ok(Value::Integer(gpe)) if gpe < gpes => gpe as u16, + Ok(Value::Integer(gpe)) => { + println!("{OWN}{device}._GPE is {gpe:#x}"); + return Err(format!("raises a GPE outside GPE0's {gpes}")); + } + Ok(Value::Package(_)) => return Err("raises its GPE in a GPE block device, which this server does not serve".into()), + Ok(_) => return Err("names its GPE by no integer".into()), + Err(why) => return Err(format!("names no GPE: {}", kind(&why))), + }; + let queries = (0..=u8::MAX).filter(|q| named.contains(&format!("{device}._Q{q:02X}"))).collect(); + Ok(Ec { path: device.into(), command, data, gpe, queries }) +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::aml::tests::{cat, crafted, sealed, CRAFTED_RSDP}; + use crate::aml::{load, Aml}; + + /// GPE0 as an AMD laptop's FADT names it: eight bytes, 32 GPEs. + pub const GPE0: Block = Block { port: 0x420, len: 8 }; + + /// A NameString of segments from the root: `\` and a MultiNamePrefix. + pub fn path(segments: &[&[u8; 4]]) -> Vec { + let mut out = vec![b'\\']; + match segments.len() { + 1 => {} + 2 => out.push(0x2E), + n => out.extend_from_slice(&[0x2F, n as u8]), + } + segments.iter().for_each(|segment| out.extend_from_slice(*segment)); + out + } + + /// A PkgLength of one to four bytes, for `len` bytes after it (§20.2.4). + pub fn pkg(body: &[u8]) -> Vec { + let mut out = match body.len() + 1 { + n if n < 0x40 => vec![n as u8], + _ => { + let n = body.len() + 2; + assert!(n < 0x1000, "a test package past two length bytes"); + vec![0x40 | (n & 0xF) as u8, (n >> 4) as u8] + } + }; + out.extend_from_slice(body); + out + } + + /// `Device () { }`. + pub fn device(at: &[&[u8; 4]], body: &[u8]) -> Vec { + cat(&[&[0x5B, 0x82], &pkg(&cat(&[&path(at), body]))]) + } + + /// `Name (, )`, the form `EisaId` and `_STA` take. + pub fn dword(name: &[u8; 4], value: u32) -> Vec { + cat(&[&[0x08], name, &[0x0C], &value.to_le_bytes()]) + } + + /// `Name (, )`. + pub fn byte(name: &[u8; 4], value: u8) -> Vec { + cat(&[&[0x08], name, &[0x0A, value]]) + } + + /// `Name (_CRS, ResourceTemplate () { IO (Decode16, p, p, 0, 1) ... })`. + pub fn crs(ports: &[u16]) -> Vec { + let mut list: Vec = ports.iter().flat_map(|p| { let [lo, hi] = p.to_le_bytes(); [0x47, 0x01, lo, hi, lo, hi, 0x00, 0x01] }).collect(); + list.extend_from_slice(&[0x79, 0x00]); + let buffer = cat(&[&[0x0A, list.len() as u8], &list]); + cat(&[&[0x08], b"_CRS", &[0x11], &pkg(&buffer)]) + } + + /// `EisaId ("PNP0C09")` and `EisaId ("PNP0C0C")`, as an AMD laptop's DSDT + /// stores them. + pub const PNP0C09: u32 = 0x090C_D041; + pub const PNP0C0C: u32 = 0x0C0C_D041; + + /// An AMD laptop's controller and power button, decoded: the controller + /// at `\_SB.PCI0.LPC0.EC` with `_HID` PNP0C09, an `_STA` of 0x0F, its + /// `_CRS` 0x62 then 0x66 and `_GPE` 3; and the button at `\_SB.PWRB` + /// with `_HID` PNP0C0C and an `_STA` of 0x0B. + pub fn laptop(extra_ec: &[u8]) -> Vec { + cat(&[ + &device(&[b"_SB_", b"PCI0"], &[]), + &device(&[b"_SB_", b"PCI0", b"LPC0"], &[]), + &device(&[b"_SB_", b"PCI0", b"LPC0", b"EC__"], &cat(&[&dword(b"_HID", PNP0C09), &byte(b"_STA", 0x0F), &crs(&[0x62, 0x66]), &byte(b"_GPE", 3), extra_ec])), + &device(&[b"_SB_", b"PWRB"], &cat(&[&dword(b"_HID", PNP0C0C), &byte(b"_STA", 0x0B)])), + ]) + } + + pub const EC_PATH: &str = "\\_SB_.PCI0.LPC0.EC__"; + pub const PWRB: &str = "\\_SB_.PWRB"; + + fn found(dsdt: &[u8], control_method: bool) -> Found { + let kernel = crafted(&sealed(b"DSDT", dsdt), &[]); + let (_, kept) = load(&kernel, CRAFTED_RSDP); + let Aml { mut interpreter, mut host } = kept.expect("the DSDT loads"); + find(&mut interpreter, &mut host, GPE0, control_method) + } + + #[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"); + } + + /// An AMD laptop's `_Q28`, decoded: its query's number stored to the POST + /// port as a word, then `Notify (\_SB.PWRB, 0x80)`; beside it `_Q2A`, + /// whose Notify is the lid's. + pub fn queries() -> Vec { + let region = cat(&[&[0x5B, 0x80], b"P80R", &[0x01, 0x0A, 0x80, 0x0A, 0x02]]); + let field = cat(&[&[0x5B, 0x81], &pkg(&cat(&[b"P80R", &[0x02], b"P80H", &[0x10]]))]); + let method = |name: &[u8; 4], q: u8, notified: &[u8]| { + let store = cat(&[&[0x70, 0x0A, q], b"P80H"]); + let notify = cat(&[&[0x86], notified, &[0x0A, 0x80]]); + cat(&[&[0x14], &pkg(&cat(&[name, &[0x00], &store, ¬ify]))]) + }; + cat(&[®ion, &field, &method(b"_Q28", 0x28, &path(&[b"_SB_", b"PWRB"])), &method(b"_Q2A", 0x2A, &path(&[b"_SB_", b"LID_"]))]) + } + + /// The laptop's DSDT with [`queries`] in its controller, and the lid. + pub fn laptop_with_queries() -> Vec { + cat(&[&device(&[b"_SB_", b"LID_"], &dword(b"_HID", 0x0D0C_D041)), &laptop(&queries())]) + } + + /// Query 0x28's method runs, writes its word to the POST port through the + /// kernel and notifies the button, which the host counts as a press; 0x2A's + /// notifies the lid, which is none; and a query the controller defines no + /// method for runs nothing. + #[test] + fn query_0x28_on_an_amd_laptop_is_a_press_of_its_power_button() { + let mut kernel = crafted(&sealed(b"DSDT", &laptop_with_queries()), &[]); + kernel.ports = vec![(0x80, 0)]; + let (_, kept) = load(&kernel, CRAFTED_RSDP); + let Aml { mut interpreter, mut host } = kept.expect("the DSDT loads"); + let found = find(&mut interpreter, &mut host, GPE0, true); + let ec = found.ec.expect("the controller"); + assert_eq!(ec.queries, BTreeSet::from([0x28, 0x2A])); + host.buttons = found.buttons; + assert_eq!(query(&mut interpreter, &mut host, &ec, 0x2A), Queried::Ran); + assert_eq!(host.presses, 0, "the lid's Notify is no press"); + assert_eq!(query(&mut interpreter, &mut host, &ec, 0x28), Queried::Ran); + assert_eq!(host.presses, 1); + let wrote = toyos_abi::acpi::Access::write(toyos_abi::acpi::Space::SystemIo, 0x80, toyos_abi::acpi::Width::Word, 0x28); + assert_eq!(kernel.asked.borrow().iter().filter(|access| access.write == 1).last(), Some(&wrote)); + assert_eq!(query(&mut interpreter, &mut host, &ec, 0x29), Queried::Absent); + assert_eq!(host.presses, 1); + } + + /// A port the kernel refuses ends the method before its Notify: no press, + /// and the refusal by its kind. + #[test] + fn a_query_whose_write_the_kernel_refuses_is_no_press() { + let kernel = crafted(&sealed(b"DSDT", &laptop_with_queries()), &[]); + let (_, kept) = load(&kernel, CRAFTED_RSDP); + let Aml { mut interpreter, mut host } = kept.expect("the DSDT loads"); + let found = find(&mut interpreter, &mut host, GPE0, true); + host.buttons = found.buttons; + let refused = query(&mut interpreter, &mut host, found.ec.as_ref().expect("the controller"), 0x28); + assert_eq!(refused, Queried::Refused("denied by this server: a SystemIO write the kernel refused KernelPort".into())); + assert_eq!(host.presses, 0); + } + + #[test] + fn an_eisa_id_is_its_three_letters_and_four_digits() { + assert_eq!(id(&Value::Integer(u64::from(PNP0C09))).as_deref(), Some("PNP0C09")); + assert_eq!(id(&Value::Integer(u64::from(PNP0C0C))).as_deref(), Some("PNP0C0C")); + assert_eq!(id(&Value::String(b"PNP0C09".to_vec())).as_deref(), Some("PNP0C09")); + assert_eq!(id(&Value::Integer(1 << 32)), None); + assert_eq!(id(&Value::Buffer(b"PNP0C09".to_vec())), None); + } + + /// Absent devices are not served, and a controller whose resources or + /// GPE are none this server serves is none, by name. + #[test] + fn a_controller_this_server_cannot_serve_is_none() { + let absent = cat(&[&device(&[b"_SB_", b"EC__"], &cat(&[&dword(b"_HID", PNP0C09), &byte(b"_STA", 0x0E), &crs(&[0x62, 0x66]), &byte(b"_GPE", 3)]))]); + assert_eq!(found(&absent, true), Found::default()); + let ec = |crs_ports: &[u16], gpe: u8| cat(&[&device(&[b"_SB_", b"EC__"], &cat(&[&dword(b"_HID", PNP0C09), &crs(crs_ports), &byte(b"_GPE", gpe)]))]); + assert_eq!(found(&ec(&[0x62, 0x66], 32), true).ec, None, "GPE 32 is past an eight-byte GPE0"); + assert_eq!(found(&ec(&[0x62, 0x66], 31), true).ec.map(|ec| ec.gpe), Some(31)); + assert_eq!(found(&ec(&[0x62], 3), true).ec, None, "one port"); + assert_eq!(found(&ec(&[0x62, 0x66, 0x68], 3), true).ec, None, "three ports"); + let two = cat(&[&ec(&[0x62, 0x66], 3), &device(&[b"_SB_", b"EC2_"], &cat(&[&dword(b"_HID", PNP0C09), &crs(&[0x68, 0x6C]), &byte(b"_GPE", 4)]))]); + assert_eq!(found(&two, true).ec, None, "two controllers present"); + let no_button = cat(&[&device(&[b"_SB_", b"PWRB"], &cat(&[&dword(b"_HID", PNP0C0C), &byte(b"_STA", 0)]))]); + assert_eq!(found(&no_button, true).buttons, Vec::::new(), "a button whose _STA says absent"); + } +} diff --git a/userland/acpiserver/src/host.rs b/userland/acpiserver/src/host.rs index a76ddab746a..d9a89e1dcf4 100644 --- a/userland/acpiserver/src/host.rs +++ b/userland/acpiserver/src/host.rs @@ -3,18 +3,29 @@ //! mediated access ([`toyos_abi::acpi`]), and [`Kernel`] is that access as //! this server asks for it, so a host test answers in the kernel's place. //! -//! **Nothing is written.** Loading a machine's tables writes nothing, and -//! this server evaluates nothing that does yet: a write in any 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. A read of memory, of a port or -//! of a function's configuration space is the kernel's to make or refuse, and -//! a refusal it names is denied under that name. +//! **A read of memory, of a port or of a function's configuration space is +//! the kernel's to make or refuse**, and a refusal it names is denied under +//! that name. +//! +//! **Only the port writes in [`FORWARDED`] are asked of the kernel**, each +//! said the first time it is asked: a write to a register that changes +//! 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. //! //! **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. //! +//! **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 +//! serve, once [`Firmware::buttons`] names the button; every other Notify is +//! said the first time and served by nothing. +//! //! **The power-off's sleep type is the one thing handed to the kernel** //! ([`Kernel::s5`]): the kernel writes the register, and reads no AML. //! @@ -40,6 +51,17 @@ 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 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 +/// block, a byte each, and the POST port's word. No other: a port write this +/// server has no account of can be a reset (0x92, 0xCF9), a call into the +/// firmware (`SMI_CMD`) or a data register's. +pub const FORWARDED: [(u16, toyos_aml::Access); 3] = + [(0x72, toyos_aml::Access::Byte), (0x80, toyos_aml::Access::Word), (0xCD6, toyos_aml::Access::Byte)]; + +/// The denial of a port write not in [`FORWARDED`]. +pub const UNFORWARDED: &str = "a SystemIO write of a port and width this server asks of the kernel for no AML"; + /// The kernel does nothing more for this claim: the machine is stopping. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Stopping; @@ -156,6 +178,12 @@ pub struct Firmware<'k, K> { pub notifies: u64, pub refused: Ledger, notified: Ledger, + /// The [`FORWARDED`] writes asked of the kernel, by port and width. + written: Ledger, + /// The paths of the control-method power buttons this server serves. + pub buttons: Vec, + /// Presses of one of them notified and not yet served. + pub presses: u64, /// The kernel answered that the machine is stopping. pub stopping: bool, } @@ -172,6 +200,9 @@ impl<'k, K: Kernel> Firmware<'k, K> { notifies: 0, refused: Ledger::default(), notified: Ledger::default(), + written: Ledger::default(), + buttons: Vec::new(), + presses: 0, stopping: false, } } @@ -226,9 +257,22 @@ impl Host for Firmware<'_, K> { fn write(&mut self, at: Address, width: toyos_aml::Access, value: u64) -> Result<(), Denied> { let what = match at { - Address::Memory(_) => "a write to SystemMemory: this server writes nothing for AML yet".into(), - Address::Io(_) => "a write to SystemIO: this server writes nothing for AML yet".into(), - Address::PciConfig { .. } => "a write to PCI_Config: this server writes nothing for AML yet".into(), + Address::Io(port) if !FORWARDED.contains(&(port, width)) => UNFORWARDED.into(), + Address::Io(port) => { + if self.written.see(&format!("{port:#x} {width:?}")) { + println!("acpiserver: a SystemIO write of {port:#x}, {width:?}, asked of the kernel for the first time"); + } + return match self.kernel.access(Access::write(Space::SystemIo, u64::from(port), wide(width), value)) { + Err(Stopping) => Err(self.stopped()), + Ok(Answer { made: Ok(_), .. }) => Ok(()), + Ok(Answer { made: Err(refused), .. }) => Err(self.deny( + format!("a SystemIO write the kernel refused {refused:?}"), + format_args!("{width:?} {value:#x} to {at:x?}"), + )), + }; + } + 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}"), }; Err(self.deny(what, format_args!("{width:?} {value:#x} to {at:x?}"))) @@ -253,8 +297,14 @@ impl Host for Firmware<'_, K> { fn notify(&mut self, object: &str, value: u64) { self.notifies += 1; + // §5.6.6's notification values of a power button: 0x80 is a press in S0. + let press = value == 0x80 && self.buttons.iter().any(|button| button == object); + if press { + self.presses += 1; + } if self.notified.see(&format!("{object} {value:#x}")) { - println!("{OWN}Notify({object}, {value:#x}) for the first time; nothing serves a Notify yet"); + let served = if press { "a press of the power button" } else { "served by nothing" }; + println!("{OWN}Notify({object}, {value:#x}) for the first time: {served}"); } } @@ -322,7 +372,7 @@ pub mod tests { fn access(&self, access: Access) -> Result { self.stopping()?; self.asked.borrow_mut().push(access); - assert_eq!(access.write, 0, "this server asks the kernel for no write"); + assert!(access.write == 0 || access.space == Space::SystemIo as u8, "this server asks the kernel for no write but a port's"); let width = Width::from_raw(access.width).expect("a width").bytes(); let listed = |values: &[(u64, u64)]| values.iter().find(|(at, _)| *at == access.address).map(|&(_, value)| value); Ok(match Space::from_raw(access.space).expect("a space") { @@ -439,20 +489,67 @@ pub mod tests { assert_eq!(host.pages.by_type(), "none"); } + /// 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. #[test] - fn nothing_is_written_and_the_controllers_space_is_not_reached() { - let kernel = machine(); + fn only_a_forwarded_port_write_is_asked_of_the_kernel() { + use toyos_aml::Access::{Byte, DWord, Word}; + let mut kernel = machine(); + kernel.ports = vec![(0x72, 0), (0x80, 0)]; let mut host = Firmware::new(&kernel); + assert_eq!(host.write(Address::Io(0x80), Word, 0x28), Ok(())); + assert_eq!(host.write(Address::Io(0x72), Byte, 0x08), Ok(())); + let refused = host.write(Address::Io(0xCD6), Byte, 0x60); + assert_eq!(refused, Err(Denied("a SystemIO write the kernel refused KernelPort".into()))); + assert_eq!( + *kernel.asked.borrow(), + [ + Access::write(Space::SystemIo, 0x80, Width::Word, 0x28), + Access::write(Space::SystemIo, 0x72, Width::Byte, 0x08), + Access::write(Space::SystemIo, 0xCD6, Width::Byte, 0x60), + ] + ); + // Reset-capable ports with the values that reset a machine, the + // reset register an AMD laptop shares with `SMI_CMD`, a data + // register, and a forwarded port at a width it is not forwarded at. + for (port, width, value) in + [(0xCF9, Byte, 0x06), (0xCF9, Byte, 0x0E), (0x92, Byte, 0x01), (0xB0, Byte, 0xFB), (0xB2, Byte, 0xA0), (0x73, Byte, 0), (0xCD7, Byte, 0), (0x72, Word, 0), (0x80, Byte, 0), (0x80, DWord, 0)] + { + assert_eq!(host.write(Address::Io(port), width, value), Err(Denied(UNFORWARDED.into())), "{port:#x} {width:?}"); + } let function = Address::PciConfig { segment: 0, bus: 0, device: 0x1F, function: 3, offset: 0x40 }; - for (at, space) in [(Address::Memory(NVS), "SystemMemory"), (Address::Io(0xB2), "SystemIO"), (function, "PCI_Config")] { - let denied = host.write(at, toyos_aml::Access::Byte, 0).expect_err("a write was made"); - assert_eq!(denied.0, format!("a write to {space}: this server writes nothing for AML yet")); + for (at, denied) in [ + (Address::Memory(NVS), "a write to SystemMemory: this server writes no memory for AML"), + (function, "a write to PCI_Config: this server writes no configuration space for AML"), + ] { + assert_eq!(host.write(at, Byte, 0), Err(Denied(denied.into()))); } let controller = Address::EmbeddedControl(0x38); - assert_eq!(host.write(controller, toyos_aml::Access::Byte, 1), Err(Denied(format!("a write to {NO_CONTROLLER}")))); - assert_eq!(host.read(controller, toyos_aml::Access::Byte), Err(Denied(format!("a read of {NO_CONTROLLER}")))); - assert!(kernel.asked.borrow().is_empty(), "the kernel was asked for an access this server denies itself"); + assert_eq!(host.write(controller, Byte, 1), Err(Denied(format!("a write to {NO_CONTROLLER}")))); + assert_eq!(host.read(controller, Byte), Err(Denied(format!("a read of {NO_CONTROLLER}")))); + assert_eq!(kernel.asked.borrow().len(), 3, "the kernel was asked for an access this server denies itself"); assert_eq!(host.reads, [0, 0, 0]); + assert_eq!(host.written.counts(), "0x72 Byte x1; 0x80 Word x1; 0xcd6 Byte x1"); + } + + /// A Notify of 0x80 to a button the server serves is a press, and to + /// anything else, or of another value, is none. + #[test] + fn only_a_notify_of_a_press_to_a_served_button_is_a_press() { + let kernel = machine(); + let mut host = Firmware::new(&kernel); + host.notify("\\_SB_.PWRB", 0x80); + assert_eq!(host.presses, 0, "a press of a button the server does not serve yet"); + host.buttons.push("\\_SB_.PWRB".into()); + for (object, value) in [("\\_SB_.PWRB", 0x02), ("\\_SB_.SLPB", 0x80), ("\\_SB_.PWRB_", 0x80), ("\\_SB_.LID_", 0x80)] { + host.notify(object, value); + } + assert_eq!(host.presses, 0); + host.notify("\\_SB_.PWRB", 0x80); + host.notify("\\_SB_.PWRB", 0x80); + assert_eq!((host.presses, host.notifies), (2, 7)); } #[test] diff --git a/userland/acpiserver/src/main.rs b/userland/acpiserver/src/main.rs index 958f777578c..f441c408e41 100644 --- a/userland/acpiserver/src/main.rs +++ b/userland/acpiserver/src/main.rs @@ -8,10 +8,9 @@ //! again. //! //! **At start** every fixed and general-purpose event is disabled and its -//! status cleared, then the power button and the embedded controller's GPE -//! are enabled, the controller's backlog is drained, and the SCI -//! acknowledged: a press after the clear latches and is served, one -//! before it is lost. +//! status cleared, then the fixed power button is enabled where the FADT +//! says the machine has one, and the SCI acknowledged: a press after the +//! clear latches and is served, one before it is lost. //! //! **Then the machine's tables are loaded** ([`aml::load`]), through the //! kernel's mediated access ([`Claim`]): after the arming, so a press during @@ -20,11 +19,21 @@ //! way. The load hands the kernel `\_S5`'s sleep type, without which the //! 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 +//! 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. +//! **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. +//! Elsewhere no query's method runs. +//! //! **Each SCI** is read off both blocks ([`sci::events`]): a press stops the //! machine through the supervisor, or is said and dropped on a machine with //! no power-off, and the controller's GPE drains the -//! controller of every query waiting, which are then run, one by one, as -//! [`aml::query`] says, after the drain that took them. An event this server +//! controller of every query waiting, which are then run, one by one, after +//! the drain that took them. An event this server //! never enabled, a controller that does not answer, more queries in one //! drain than [`QUERIES`], and [`sci::EMPTY_SCIS`] SCIs in a row that carried //! nothing are each a panic naming the registers. @@ -33,6 +42,7 @@ //! of every one at [`COUNTS`] intervals, never a line per event. mod aml; +mod devices; mod ec; mod host; mod ledger; @@ -47,9 +57,12 @@ 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, FIXED_POWER_BUTTON}; +use toyos_abi::acpi::{Access, AcpiInfo, Block, Space, Width, FIXED_POWER_BUTTON}; use toyos_abi::syscall::{DeviceType, SyscallError}; +use acpiserver_api::{CONTROLLER_NONE, CONTROLLER_SERVED}; +use aml::Aml; +use devices::{Ec, Queried}; use ec::{Do, Transaction, Wait}; use host::{Answer, Kernel, Stopping, Take}; use sci::{Event, Served, Unserved, Unstopped, PM1_STATUS, PWRBTN}; @@ -62,10 +75,15 @@ const QUERIES: usize = 32; /// How often the log is told the queries' counts, where they moved. const COUNTS: Duration = Duration::from_secs(30); -struct Server { - dev: AcpiDev, +struct Server<'a, K, P> { + kernel: &'a K, + ports: P, info: AcpiInfo, served: Served, + /// The controller the namespace names, if it names one this server serves. + ec: Option, + /// The namespace the load kept. + aml: Option>, /// Taken off the controller by the drain, run after it. queued: VecDeque, /// Every query number taken, and how often. @@ -84,25 +102,44 @@ fn main() { return; }; let info = dev.describe().expect("acpiserver: the claim's first read is its description"); - let served = Served { - power_button: info.flags & FIXED_POWER_BUTTON != 0, - ec_gpe: info.has_ec().then_some(info.ec_gpe), - }; - let mut server = Server { - dev, - info, - served, - queued: VecDeque::new(), - counts: BTreeMap::new(), - counted: 0, - logged: 0, - scis: 0, - empty: 0, - power_off: false, - }; - server.arm(); - server.power_off = aml::load(&Claim(&server.dev), server.info.rsdp).handed; - server.serve(); + let claim = Claim(&dev); + 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); +} + +/// The row's PM1 event and GPE0 registers, which this server reads and writes +/// itself. +trait Ports { + fn in8(&self, port: u16) -> u8; + fn out8(&self, port: u16, byte: u8); + fn in16(&self, port: u16) -> u16; + fn out16(&self, port: u16, word: u16); +} + +/// The machine's own ports, which the claim opened to this process. +struct Machine; + +impl Ports for Machine { + fn in8(&self, port: u16) -> u8 { + in8(port) + } + + fn out8(&self, port: u16, byte: u8) { + out8(port, byte) + } + + fn in16(&self, port: u16) -> u16 { + in16(port) + } + + fn out16(&self, port: u16, word: u16) { + out16(port, word) + } } /// The claim as the tables' fetch and their AML ask it for what lies outside @@ -146,43 +183,102 @@ impl Kernel for Claim<'_> { } } +/// 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), +} + +/// 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:?}"))) +} + +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:?}"))) +} + /// Each byte of a status-and-enable block: its status port and its enable port. fn bytes(block: Block) -> impl Iterator { (0..block.len / 2).map(move |i| (block.port + i, block.enable() + i)) } -impl Server { - fn arm(&mut self) { +impl<'a, K: Kernel, P: Ports> Server<'a, K, P> { + fn new(kernel: &'a K, ports: P, info: AcpiInfo) -> Self { + Server { + kernel, + ports, + info, + served: Served { power_button: info.flags & FIXED_POWER_BUTTON != 0, ec_gpe: None }, + ec: None, + aml: None, + queued: VecDeque::new(), + counts: BTreeMap::new(), + counted: 0, + logged: 0, + scis: 0, + empty: 0, + power_off: false, + } + } + + fn arm(&mut self, dev: &AcpiDev) { let pm1 = self.info.pm1_event; - out16(pm1.enable(), 0); - out16(pm1.port, PM1_STATUS); + self.ports.out16(pm1.enable(), 0); + self.ports.out16(pm1.port, PM1_STATUS); for (status, enable) in bytes(self.info.gpe0) { - out8(enable, 0); - out8(status, 0xFF); + self.ports.out8(enable, 0); + self.ports.out8(status, 0xFF); } if self.served.power_button { - out16(pm1.enable(), PWRBTN); + self.ports.out16(pm1.enable(), PWRBTN); } - if let Some(n) = self.served.ec_gpe { - let (_, enable) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the kernel bounded every GPE by the block"); - out8(enable, in8(enable) | 1 << (n % 8)); - } - if self.info.has_ec() { - self.drain(); - self.run_queued(); - } - self.dev.ack().expect("acpiserver: the claim's acknowledgement"); + dev.ack().expect("acpiserver: the claim's acknowledgement"); println!( - "acpiserver: armed: power button {}, embedded controller {}", - if self.served.power_button { "served" } else { "not the fixed one, so not served" }, - match self.served.ec_gpe { - Some(gpe) => format!("on GPE {gpe:#x} at {:#x}/{:#x}", self.info.ec_command.port, self.info.ec_data.port), - None => "none".into(), - }, + "acpiserver: armed: power button {}", + if self.served.power_button { "served" } else { "a control method device, served once the namespace names it" }, ); } - fn serve(&mut self) -> ! { + /// 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) { + 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"); + }; + 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 }; + // 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}"), + Ok(_) => {} + } + 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)); + self.ports.out8(enable, self.ports.in8(enable) | 1 << (n % 8)); + println!("{CONTROLLER_SERVED}{n:#x} at {:#x}/{:#x}", ec.command, ec.data); + self.served.ec_gpe = Some(n); + self.ec = Some(ec); + self.drain(); + self.run_queued(); + } + + 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 @@ -190,7 +286,7 @@ impl Server { let mut watching = false; loop { if !watching { - poller.watch(&self.dev, READABLE, 0); + poller.watch(dev, READABLE, 0); watching = true; } let wait = next_count.saturating_duration_since(Instant::now()); @@ -199,21 +295,21 @@ impl Server { self.log_counts(); next_count = Instant::now() + COUNTS; } - match self.dev.irq() { + 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(); - self.dev.ack().expect("acpiserver: the claim's acknowledgement"); + dev.ack().expect("acpiserver: the claim's acknowledgement"); } } /// Everything one SCI carried, each event served and its status cleared. fn take(&mut self) { let pm1 = self.info.pm1_event; - let gpe0: Vec<(u8, u8)> = bytes(self.info.gpe0).map(|(status, enable)| (in8(status), in8(enable))).collect(); - let events = match sci::events(&self.served, (in16(pm1.port), in16(pm1.enable())), &gpe0) { + let gpe0: Vec<(u8, u8)> = bytes(self.info.gpe0).map(|(status, enable)| (self.ports.in8(status), self.ports.in8(enable))).collect(); + let events = match sci::events(&self.served, (self.ports.in16(pm1.port), self.ports.in16(pm1.enable())), &gpe0) { Ok(events) => events, Err(Unserved::Pm1 { status, enable }) => panic!( "acpiserver: PM1 status {status:#06x} under enable {enable:#06x} carries an event this server never enabled" @@ -226,7 +322,7 @@ impl Server { self.empty < sci::EMPTY_SCIS, "acpiserver: {} SCIs in a row carried no event this server serves (PM1 {:#06x}, GPE0 {gpe0:x?})", self.empty, - in16(pm1.port), + self.ports.in16(pm1.port), ); return; } @@ -234,15 +330,15 @@ impl Server { for event in events { match event { Event::PowerButton => { - out16(pm1.port, PWRBTN); + self.ports.out16(pm1.port, PWRBTN); self.press(); } Event::Ec => { // The controller's GPE is an edge: cleared before the drain, // so an event the drain does not see raises it again. - let n = self.info.ec_gpe; - let (status, _) = bytes(self.info.gpe0).nth(usize::from(n / 8)).expect("the kernel bounded every GPE by the block"); - out8(status, 1 << (n % 8)); + let n = self.served.ec_gpe.expect("an event of the controller's GPE is one this server enabled"); + let (status, _) = 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)); self.drain(); } } @@ -261,65 +357,64 @@ impl Server { } } - /// Take every query the controller has waiting off it, queued for after. + /// Take every query the controller has waiting off it, queued for after; + /// a port the kernel refuses ends the controller by name. fn drain(&mut self) { + match self.take_queries() { + Ok(()) | Err(Unmade::Stopping) => {} + Err(Unmade::Refused(why)) => self.unserve(&why), + } + } + + fn take_queries(&mut self) -> Result<(), Unmade> { + let Some(ec) = &self.ec else { return Ok(()) }; + let (command, data) = (ec.command, ec.data); let mut taken = 0; - while in8(self.info.ec_command.port) & ec::SCI_EVT != 0 { - let q = self.transact(Transaction::query()); + loop { + let status = port_in(self.kernel, command)?; + if status & ec::SCI_EVT == 0 { + return Ok(()); + } + let q = transact(self.kernel, command, data, Transaction::query())?; if q == 0 { - break; + return Ok(()); } taken += 1; - assert!( - taken <= QUERIES, - "acpiserver: the embedded controller had more than {QUERIES} queries waiting at once (EC_SC {:#04x})", - in8(self.info.ec_command.port) - ); + assert!(taken <= QUERIES, "acpiserver: the embedded controller had more than {QUERIES} queries waiting at once (EC_SC {status:#04x})"); self.queued.push_back(q); } } + /// Serve the controller no more: its GPE disabled, what it had waiting + /// dropped, and why said. + fn unserve(&mut self, why: &str) { + let n = self.served.ec_gpe.take().expect("a controller served has its GPE enabled"); + 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; + self.queued.clear(); + println!("{CONTROLLER_NONE}{why}"); + } + + /// Each query the drain took, run as its method where a control-method + /// power button is served ([`run_query`]), and counted. fn run_queued(&mut self) { while let Some(q) = self.queued.pop_front() { - aml::query(q); + let served = run_query(self.aml.as_mut(), self.ec.as_ref(), q); let count = self.counts.entry(q).or_insert(0); *count += 1; self.counted += 1; if *count == 1 { - println!("acpiserver: embedded controller query {q:#04x} taken for the first time, served by nothing: no query's method is evaluated yet"); + let said = match &served { + None => UNRUN.into(), + Some((Queried::Ran, _)) => "its method ran".into(), + Some((Queried::Absent, _)) => "the controller defines no method for it".into(), + Some((Queried::Refused(why), _)) => format!("its method did not finish: {why}"), + }; + println!("acpiserver: embedded controller query {q:#04x} taken for the first time; {said}"); } - } - } - - /// One transaction, waiting on the controller at each step for at most - /// [`EC_STEP`]. - fn transact(&self, mut tx: Transaction) -> u8 { - let (command, data) = (self.info.ec_command.port, self.info.ec_data.port); - let mut waiting: Option<(Wait, Instant)> = None; - loop { - let status = in8(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; - out8(command, byte); - } - Do::ReadData => { - waiting = None; - tx.read(in8(data)); - } - Do::Done(byte) => return byte, + if served.is_some_and(|(_, presses)| presses != 0) { + self.press(); } } } @@ -333,3 +428,166 @@ impl Server { println!("acpiserver: {} SCIs; {}{}", self.scis, acpiserver_api::QUERIES_COUNTED, counts.join(", ")); } } + +/// 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"; + +/// Query `q`'s method run, and the presses its Notifies made, where a +/// control-method power button is served; `None` elsewhere. +fn run_query(aml: Option<&mut Aml<'_, K>>, ec: Option<&Ec>, q: u8) -> Option<(Queried, u64)> { + match (aml, ec) { + (Some(aml), Some(ec)) if !aml.host.buttons.is_empty() => { + let queried = devices::query(&mut aml.interpreter, &mut aml.host, ec, q); + let presses = std::mem::take(&mut aml.host.presses); + Some((queried, presses)) + } + _ => None, + } +} + +/// 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 crate::aml::load; + use crate::aml::tests::{crafted, sealed, CRAFTED_RSDP}; + use crate::devices::tests::{laptop_with_queries, GPE0}; + use crate::host::tests::Scripted; + use std::cell::RefCell; + + /// The row's registers as bytes a write leaves as written. + #[derive(Default)] + struct Bytes(RefCell>); + + impl Ports for Bytes { + fn in8(&self, port: u16) -> u8 { + *self.0.borrow().get(&port).unwrap_or_else(|| panic!("a read of {port:#x}, which nothing wrote")) + } + + fn out8(&self, port: u16, byte: u8) { + self.0.borrow_mut().insert(port, byte); + } + + fn in16(&self, port: u16) -> u16 { + panic!("a PM1 read of {port:#x}, which no test here makes") + } + + fn out16(&self, port: u16, _: u16) { + panic!("a PM1 write of {port:#x}, which no test here makes") + } + } + + /// A controller port the kernel refuses, its status port or, inside a + /// query's transaction, its data port, ends the controller: its GPE + /// disabled and no other of the block's, what it had waiting dropped, and + /// nothing asked of the kernel for it again. + #[test] + fn a_controller_port_the_kernel_refuses_ends_the_controller() { + let (command, data, gpe) = (0x66, 0x62, 3); + let status = || Access::read(Space::SystemIo, command, Width::Byte); + for (ports, asked) in [ + (vec![], vec![status()]), + ( + // A query waits and its answer is ready, at a data port the kernel keeps. + vec![(command, u64::from(ec::SCI_EVT | ec::OBF))], + vec![ + status(), + status(), + Access::write(Space::SystemIo, command, Width::Byte, 0x84), + status(), + Access::read(Space::SystemIo, data, Width::Byte), + ], + ), + ] { + let kernel = Scripted { ports, ..Default::default() }; + let info = AcpiInfo { rsdp: 0, pm1_event: Block::NONE, gpe0: GPE0, flags: 0, reserved: [0; 3] }; + let mut server = Server::new(&kernel, Bytes::default(), info); + let enable = GPE0.enable(); + server.ports.out8(enable, 0b1000_1001); + server.served.ec_gpe = Some(gpe); + server.ec = Some(Ec { path: "\\_SB_.EC0_".into(), command: command as u16, data: data as u16, gpe, queries: Default::default() }); + // Taken by an earlier event of the same SCI, and not yet run. + server.queued.push_back(0x28); + server.drain(); + assert_eq!(*kernel.asked.borrow(), asked); + assert_eq!(server.ec, None, "a refused controller is still served"); + assert_eq!(server.served.ec_gpe, None); + assert_eq!(server.ports.in8(enable), 0b1000_0001, "the controller's GPE is left enabled, or another with it disabled"); + assert!(server.queued.is_empty(), "a refused controller's queries are still run"); + server.drain(); + assert_eq!(kernel.asked.borrow().len(), asked.len(), "a refused controller was asked again"); + } + } + + /// 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, + /// enables no GPE, asks the kernel nothing, and goes on to serve the + /// claim rather than end and give it back. + #[test] + fn no_namespace_leaves_the_button_and_the_controller_unserved_and_keeps_the_claim() { + let kernel = Scripted::default(); + let info = AcpiInfo { rsdp: 0, 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.arm_controller(); + assert!(!server.served.power_button, "a control-method button is served as the fixed one"); + assert_eq!((server.ec.is_some(), server.served.ec_gpe), (false, None), "a controller is served with no namespace"); + assert!(bytes(GPE0).all(|(_, enable)| server.ports.in8(enable) == 0), "a GPE is enabled with no namespace"); + assert!(kernel.asked.borrow().is_empty(), "the kernel was asked for an access with no namespace"); + } + + /// An AMD laptop's controller and `_Q28` under both kinds of power + /// button: the method runs, writes the POST port and presses only where + /// the FADT says the button is a control method device; on a machine + /// whose button is the fixed one no query's method runs and nothing is + /// written. + #[test] + fn a_querys_method_runs_only_where_a_control_method_button_is_served() { + for control_method in [false, true] { + let mut kernel = crafted(&sealed(b"DSDT", &laptop_with_queries()), &[]); + kernel.ports = vec![(0x80, 0)]; + let (_, kept) = load(&kernel, CRAFTED_RSDP); + let mut aml = kept.expect("the DSDT loads"); + let found = devices::find(&mut aml.interpreter, &mut aml.host, GPE0, control_method); + aml.host.buttons = found.buttons; + let ec = found.ec.expect("the controller"); + let served = run_query(Some(&mut aml), Some(&ec), 0x28); + let wrote = kernel.asked.borrow().iter().filter(|access| access.write == 1).count(); + match control_method { + true => assert_eq!((served, wrote), (Some((Queried::Ran, 1)), 1)), + false => assert_eq!((served, wrote), (None, 0), "a query's method ran on a machine whose power button is the fixed one"), + } + } + } +}