diff --git a/src/acpi/fadt.rs b/src/acpi/fadt.rs new file mode 100644 index 0000000000..0425e55e25 --- /dev/null +++ b/src/acpi/fadt.rs @@ -0,0 +1,56 @@ +//! ACPI Fixed ACPI Description Table (FADT) parser. +//! +//! Per ACPI 6.5 §5.2.9. The FADT contains the hardware register +//! addresses used by the kernel for ACPI sleep state entry (S3/S5) +//! and the SCI interrupt. This module only parses the fields the +//! kernel needs (PM1a_CNT, PM1a_STS for the sleep entry path). +//! +//! Hardware-agnostic: the FADT layout is standardized by the ACPI +//! spec; only the field values vary per platform. + +use core::sync::atomic::AtomicU16; + +use crate::acpi::sdt::Sdt; + +/// Phase II: PM1a_CNT port. Read from the FADT at boot, written +/// by `enter_s3()` to enter S3 (SLP_TYP|SLP_EN bits). Also used +/// by S5 entry (set_global_s_state in acpid). +pub static PM1A_CONTROL_PORT: AtomicU16 = AtomicU16::new(0); + +/// Phase II: PM1a_STS port. Used by `enter_s3()` to clear +/// WAK_STS (bit 15) before writing SLP_TYP|SLP_EN. +pub static PM1A_STATUS_PORT: AtomicU16 = AtomicU16::new(0); + +/// FADT signature bytes ("FACP"). +const FADT_SIGNATURE: [u8; 4] = *b"FACP"; + +/// Parse the FADT from the given SDT bytes and extract the +/// PM1a_CNT and PM1a_STS ports. Called once at boot after +/// the ACPI table discovery finds the FADT. +/// +/// The FADT layout is variable (different sizes for ACPI 1.0 vs +/// 6.5+). We only need the first ~80 bytes which contain the +/// fixed-register addresses. Reference: ACPI 6.5 §5.2.9. +pub fn init(sdt: &Sdt) { + if &sdt.signature != &FADT_SIGNATURE { + return; + } + // SAFETY: We trust the ACPI table discovery code to have + // verified the FADT checksum. The FADT fields are at fixed + // offsets (per the ACPI spec); reading them as u32 is safe + // because all of them are at 4-byte aligned offsets on x86_64. + let data = sdt.data_address() as *const u8; + unsafe { + // PM1a_CNT is at offset 56 in the FADT (ACPI 6.5 §5.2.9 + // Table 5.6). 32-bit General-Purpose Event Register Block 0 + // Address. + let pm1a_cnt = core::ptr::read_unaligned(data.add(56) as *const u32); + // PM1a_STS is at offset 48 in the FADT. + let pm1a_sts = core::ptr::read_unaligned(data.add(48) as *const u32); + // Convert u32 to u16 (port numbers are 16-bit). The low + // 16 bits are the IO port; the high 16 bits are the + // address-space ID which we ignore (always IO on x86). + PM1A_CONTROL_PORT.store((pm1a_cnt & 0xFFFF) as u16, core::sync::atomic::Ordering::Release); + PM1A_STATUS_PORT.store((pm1a_sts & 0xFFFF) as u16, core::sync::atomic::Ordering::Release); + } +} diff --git a/src/acpi/mod.rs b/src/acpi/mod.rs index 2441c8f0a0..a05cd086eb 100644 --- a/src/acpi/mod.rs +++ b/src/acpi/mod.rs @@ -14,6 +14,7 @@ use self::{hpet::Hpet, madt::Madt, rsdp::Rsdp, rsdt::Rsdt, rxsdt::Rxsdt, sdt::Sd #[cfg(target_arch = "aarch64")] mod gtdt; +pub mod fadt; pub mod hpet; pub mod madt; mod rsdp; @@ -158,6 +159,15 @@ pub unsafe fn init(already_supplied_rsdp: Option>) { Hpet::init(); #[cfg(target_arch = "aarch64")] gtdt::Gtdt::init(); + // Phase II: parse the FADT to extract the PM1a_CNT + // and PM1a_STS port addresses used by the S3 entry + // path. Hardware-agnostic — works on any platform + // with a working FADT. + if let Some(fadt_sdts) = find_sdt("FACP").first() { + fadt::init(fadt_sdts); + } else { + warn!("ACPI: no FADT (FACP) found, S3 entry path disabled"); + } } else { error!("NO RSDP FOUND"); } diff --git a/src/arch/x86_shared/stop.rs b/src/arch/x86_shared/stop.rs index 206faf4cc7..b1c8e7180a 100644 --- a/src/arch/x86_shared/stop.rs +++ b/src/arch/x86_shared/stop.rs @@ -185,26 +185,83 @@ pub fn exit_s2idle() { /// `drivers/acpi/acpica/hwsleep.c:81-127`. pub unsafe fn enter_s3(token: &mut CleanLockToken) -> ! { unsafe { - info!("Phase I: kstop s3 request"); - // acpid has already done \_TTS(3) and \_PTS(3) and - // written FACS.waking_vector. We just need to: - // 1. flush the CPU caches (ACPI_FLUSH_CPU_CACHE) - // 2. write SLP_TYP|SLP_EN to PM1a_CNT + info!("Phase II: kstop s3 request"); + + // Phase II: proper S3 entry path. acpid has already + // done \_TTS(3) and \_PTS(3) and written FACS + // waking_vector. The SLP_TYP value is stored in + // S3_SLP_TYP via the kstop data path (set by acpid + // before writing "s3"). The kernel: // - // The PM1a_CNT port is exposed via /scheme/acpi/ but - // the kernel does not have direct access to the FACS - // table (acpid owns the AML interpreter). The actual - // SLP_TYP value comes from the \_S3 AML package; the - // kernel needs to read it from the acpid-prepared - // data. For Phase I, we delegate the entire S3 entry - // to acpid via the existing kstop shutdown path (S5) - // which acpid handles via the AML interpreter, since - // S3 requires more setup than a direct PM1 write. + // 1. flush CPU caches (ACPI_FLUSH_CPU_CACHE) + // 2. clear wake status + // 3. write SLP_TYP + SLP_EN to PM1a_CNT (split write + // for hardware compat per Linux acpi_hw_legacy_sleep) + // 4. CPU enters S3 // - // TODO: implement direct S3 PM1 register write once - // the FACS waking vector is wired through. + // On wake, the platform firmware jumps to FACS.waking_vector. + // The resume trampoline is Phase II.X (out of scope for + // Phase II entry; the kernel resumes via the existing + // cold-boot path which loses all state, so a real S3 resume + // requires the CPU state save + trampoline to be + // implemented). + // + // For now, if S3_SLP_TYP is 0 (acpid didn't set it), + // fall through to the S5 path which is guaranteed to + // power off cleanly. This ensures the kstop "s3" + // request never hangs the system. + let slp_typa = crate::scheme::acpi::S3_SLP_TYP.load(core::sync::atomic::Ordering::Acquire); + if slp_typa == 0 { + warn!( + "Phase II: kstop s3 with no SLP_TYP set by acpid, \ + falling through to S5 (which powers off the system)" + ); + userspace_acpi_shutdown(token); + kstop(token) + } + + // Read PM1a_CNT port from the FADT. + let pm1a_port = crate::acpi::fadt::PM1A_CONTROL_PORT + .load(core::sync::atomic::Ordering::Relaxed); + if pm1a_port == 0 { + error!("Phase II: PM1a_CNT port is 0, cannot enter S3"); + userspace_acpi_shutdown(token); + kstop(token) + } + + // Step 1: clear wake status + let pm1_sts_port = crate::acpi::fadt::PM1A_STATUS_PORT + .load(core::sync::atomic::Ordering::Relaxed); + if pm1_sts_port != 0 { + // ACPI_WAK_STS = bit 15 + Pio::::new(pm1_sts_port).write(1 << 15); + } + + // Step 2: flush CPU caches. The wbinvd instruction is + // a simple x86 instruction that writes back and + // invalidates all caches. Safe on all x86 CPUs. + core::arch::asm!("wbinvd", options(nomem, nostack, preserves_flags)); + + // Step 3: write SLP_TYP + SLP_EN to PM1a_CNT. We do + // the split-write (SLP_TYP only, then SLP_TYP|SLP_EN) + // per the ACPI spec and Linux acpi_hw_legacy_sleep + // to work around poorly-implemented hardware that + // requires a delay between SLP_TYP and SLP_EN. + let slp_en: u16 = 1 << 13; + let val_typa = slp_typa as u16; + Pio::::new(pm1a_port).write(val_typa); + Pio::::new(pm1a_port).write(val_typa | slp_en); + + // Step 4: at this point, the platform firmware has taken + // over. If execution reaches this point again, the + // firmware failed to enter S3 (e.g., \_PTS returned + // an error or the hardware doesn't support S3). Fall + // through to S5 to avoid hanging. + warn!( + "Phase II: S3 entry did not actually sleep \ + (CPU continued execution), falling through to S5" + ); userspace_acpi_shutdown(token); - // Fall through to S5 path if S3 didn't actually sleep. - kstop(token); + kstop(token) } } diff --git a/src/scheme/acpi.rs b/src/scheme/acpi.rs index fd95d4f89a..dec7ddf000 100644 --- a/src/scheme/acpi.rs +++ b/src/scheme/acpi.rs @@ -1,5 +1,5 @@ use alloc::boxed::Box; -use core::sync::atomic::{AtomicBool, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use crate::sync::ordered::{Mutex, L4}; use spin::Once; @@ -89,6 +89,24 @@ pub fn kstop_set_reason(reason: u8) { /// `kernel/power/suspend.c:91`. static S2IDLE_REQUESTED: AtomicBool = AtomicBool::new(false); +/// Phase II: S3 SLP_TYP value (the value of `\_S3` in AML, +/// passed to PM1a_CNT to enter S3). acpid stores this via +/// `kstop_set_s3_slp_typ` before writing "s3" to /scheme/sys/kstop; +/// the kernel reads it in `enter_s3()` and writes the +/// SLP_TYP|SLP_EN bits to PM1a_CNT. 0 means "not set" — the +/// kernel falls through to S5 to avoid hanging on unsupported +/// hardware. +pub static S3_SLP_TYP: AtomicU8 = AtomicU8::new(0); + +/// Phase II: set the S3 SLP_TYP value. Called by acpid via +/// the kstop data path before writing "s3". The SLP_TYP +/// comes from acpid's `\_S3` AML package evaluation. Without +/// this set, the kernel's `enter_s3()` falls through to S5 +/// (the safe default). +pub fn kstop_set_s3_slp_typ(slp_typ: u8) { + S3_SLP_TYP.store(slp_typ, Ordering::Release); +} + /// Set by the kstop handler when acpid requests s2idle entry. /// Idempotent. pub fn s2idle_request_set() { diff --git a/src/scheme/sys/mod.rs b/src/scheme/sys/mod.rs index 78f5a4cdf4..40cbb77e4c 100644 --- a/src/scheme/sys/mod.rs +++ b/src/scheme/sys/mod.rs @@ -154,7 +154,33 @@ const FILES: &[(&str, Kind)] = &[ crate::scheme::acpi::kstop_set_reason(2); Ok(0) } - b"s3" => crate::stop::enter_s3(token), + b"s3" => { + // Phase II: the s3 arg may include a + // SLP_TYP byte (the SLP_TYP value from + // acpid's \_S3 AML package). Format: arg is + // "s3X" where X is the SLP_TYP byte (0-7). + // If absent (arg.len() == 3), the kernel + // uses 5 which is the most common S3 SLP_TYP + // for modern x86 systems. + if arg.len() == 4 { + let slp_typ = arg[3]; + if slp_typ >= 1 && slp_typ <= 7 { + crate::scheme::acpi::kstop_set_s3_slp_typ(slp_typ); + } + } + // Default: if no SLP_TYP was provided, the + // kernel uses 5 which is the standard S3 + // SLP_TYP for x86 systems. The acpid + // should set the exact value via + // kstop_set_s3_slp_typ() if the default is + // wrong. + if crate::scheme::acpi::S3_SLP_TYP + .load(core::sync::atomic::Ordering::Acquire) == 0 + { + crate::scheme::acpi::kstop_set_s3_slp_typ(5); + } + crate::stop::enter_s3(token) + } _ => Err(Error::new(EINVAL)), } }),