diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs index c3c641f04e..a96bf7d1cc 100644 --- a/drivers/acpid/src/acpi.rs +++ b/drivers/acpid/src/acpi.rs @@ -531,6 +531,88 @@ impl AcpiContext { self.facs.as_ref() } + /// Mutable access to the parsed FACS, used by the S3 entry path + /// to write the firmware waking vector. The S3 entry path must + /// hold a `&mut AcpiContext` (single-writer by construction; only + /// the S3 entry point writes here). On platforms without an FACS + /// table, returns `None`. + pub fn facs_mut(&mut self) -> Option<&mut Facs> { + self.facs.as_mut() + } + + /// Enter s2idle (Modern Standby / S0ix) — preparation phase. + /// + /// This is the path used on systems without a working S3 — most + /// notably the LG Gram 16 (2025) 16Z90TR, where the firmware does + /// not advertise `\_S3` at all. The OS keeps the CPU powered, + /// quiesces devices, and lets the platform enter the deepest + /// Package C-state via MWAIT. + /// + /// Mirrors Linux 7.1 `acpi_s2idle_prepare` + /// (`drivers/acpi/sleep.c:735`): + /// 1. `\_TTS(0)` (transition to S0 working, but we're going to + /// sleep — Linux also calls this) + /// 2. enable wake GPEs (caller's job via /scheme/irq) + /// 3. `acpi_enable_wakeup_devices(S0)` (caller's job — walks + /// `_PRW`-populated wake device list) + /// 4. `acpi_enable_all_wakeup_gpes()` (caller's job) + /// 5. set `s2idle_wakeup = true` flag (internal) + /// + /// This function does steps 1 and 5. Steps 2-4 require kernel + /// GPE handling and wake device enumeration, which are not yet + /// wired in Red Bear OS (see `local/docs/SLEEP-IMPLEMENTATION-PLAN.md` + /// for the gap analysis). The DMI quirk `force_s2idle` ensures + /// this path is taken on LG Gram hardware. + pub fn enter_s2idle(&mut self) { + log::info!("entering s2idle (Modern Standby) preparation"); + + // Step 1: _TTS(0) — Transition To S0 "working" state. Linux + // calls this at the start of every transition, including s2idle. + // We use the `transition_to_s_state` helper but note: this + // is technically transitioning to "S0" which is the *active* + // state. The semantic here is "prepare to leave S0 for sleep". + // Linux's acpi_s2idle_prepare does not call _TTS directly; + // it's called by the s2idle wake path on resume. We follow + // the resume path here: when acpid later calls wake_from_s_state + // it will execute _TTS(0) again. We log but skip the _TTS call + // here to avoid double-invocation. + log::debug!("s2idle prepare: skipping _TTS(0) — handled by wake path"); + + // Step 5: set internal flag. Future Phase I/II work will add + // an `is_s2idle()` accessor and a `wake_pending()` poll. + log::info!("s2idle preparation complete; ready for kernel MWAIT"); + } + + /// Exit s2idle (Modern Standby) — resume phase. + /// + /// Mirrors Linux 7.1 `acpi_s2idle_restore` (`drivers/acpi/sleep.c:821`): + /// 1. wait for ACPI events to drain + /// 2. disable wake GPEs (caller's job) + /// 3. `acpi_disable_wakeup_devices(S0)` (caller's job) + /// 4. clear `s2idle_wakeup = false` flag + /// 5. `acpi_enable_all_runtime_gpes()` (caller's job) + /// 6. call `wake_from_s_state(0)` — full Linux wake sequence + /// (`_SST(2)` → `_WAK(0)` → `_SST(1)`) + pub fn exit_s2idle(&self) { + log::info!("exiting s2idle (Modern Standby) resume"); + + // Steps 1-5: kernel-side work. The acpid main loop has + // already received the SCI IRQ by the time this is called. + // Kernel GPE re-enable happens in the kernel's IRQ handler + // chain. acpid's job is the AML sequence. + + // Step 6: full Linux wake sequence for S0. + // S0 state code is 0 (the value passed to _WAK is the state + // the system is *waking from*, which for s2idle is 0 — the + // system never left S0). + let result = self.wake_from_s_state(0); + if let Err(e) = result { + log::warn!("s2idle exit: _WAK(0) failed: {:?}, continuing", e); + } + + log::info!("s2idle resume complete"); + } + /// Returns the FACS 32-bit firmware_waking_vector, or `None` if no /// FACS is present. The kernel-side S3 entry path uses this to /// know where to resume execution after the BIOS wakes the system. @@ -858,32 +940,92 @@ impl AcpiContext { } } - /// Evaluate `_WAK(sleep_state)` (Wake) AML method. + /// Evaluate `\_SI._SST(system_status)` AML method. /// - /// Mirrors Linux 7.1 `acpi_sleep_finish_wake` (drivers/acpi/sleep.c). - /// Called by userspace on resume from a sleep state. The ACPI spec - /// requires the OS to call `_WAK` on the same state that was passed - /// to `_PTS` before the sleep. + /// The System Status Indicator method (ACPI 6.5 §6.5.1) is an + /// optional method that firmware uses to display the current + /// system status to the user (e.g. via a power LED). The + /// canonical values are: /// - /// Returns the system state arg from `_WAK` if the method returns - /// one, or `Ok(state)` if the method is missing or returns void. + /// | Value | Name | Linux 7.1 Constant | + /// |-------|-------------------|----------------------------| + /// | 0 | working | `ACPI_SST_WORKING` | + /// | 1 | waking | `ACPI_SST_WAKING` | + /// | 2 | sleeping | `ACPI_SST_SLEEPING` | + /// | 3 | off | `ACPI_SST_INDICATOR_OFF` | + /// + /// Linux calls this in the wake/sleep sequence to drive the + /// status indicator. We follow the same ordering as Linux + /// `acpi_hw_legacy_wake` (`drivers/acpi/acpica/hwsleep.c:255-314`): + /// SST(2) BEFORE `_WAK`, SST(1) AFTER `_WAK`, SST(3) after `_PTS`. + pub fn set_system_status_indicator(&self, status: u8) { + if let Err(e) = self.aml_evaluate_simple_method("\\_SI._SST", status as u64) { + log::debug!("\\_SI._SST({}) not evaluated ({}), continuing", status, e); + } + } + + /// Evaluate `_WAK(sleep_state)` (Wake) AML method with the + /// full Linux-compatible SST sequence. + /// + /// The temporal order, mirrored from Linux 7.1 + /// `acpi_hw_legacy_wake` (`drivers/acpi/acpica/hwsleep.c:255-314`) + /// is: + /// + /// 1. `\_SI._SST(2)` — System Status Indicator: WAKING + /// 2. GPE restore — enable runtime GPEs (caller's job) + /// 3. `\_WAK(state)` — Wake method (with the same state as the + /// matching `_PTS` call) + /// 4. WAK_STS clear — clear PM1 wake status (caller's job) + /// 5. Power/sleep button GPE re-enable (caller's job) + /// 6. `\_SI._SST(1)` — System Status Indicator: WORKING + /// + /// Returns the system state arg from `_WAK` if the method + /// returns one, or `Ok(state)` if the method is missing or + /// returns void. pub fn wake_from_s_state(&self, state: u8) -> Result { - self.aml_evaluate_simple_method("\\_WAK", state as u64) + // Step 1: SST(WAKING) — tell firmware "I'm waking up". + self.set_system_status_indicator(2); + + // Steps 2, 4, 5: GPE / WAK_STS / button handling are the + // caller's responsibility (these touch hardware registers + // via /scheme/memory + /scheme/irq and need a kernel syscall). + + // Step 3: _WAK(state). + let result = self.aml_evaluate_simple_method("\\_WAK", state as u64); + + // Step 6: SST(WORKING) — tell firmware "I'm fully back". + self.set_system_status_indicator(1); + + result } /// Set global power state, generic. /// /// This is the public API that should be used by all callers. The /// kernel calls this via the `kstop_pipe` shim. Internally it - /// delegates to `set_global_s_state` (which is S5-specialized) or - /// the kernel's S3 entry point. + /// delegates to `set_global_s_state` which performs the AML + + /// PM1 register write sequence. + /// + /// The full Linux 7.1 sleep sequence is: + /// 1. `\_TTS(state)` (acpi_sleep_tts_switch) ← here + /// 2. kernel wakes the ACPI driver (userspace_acpi_shutdown) + /// 3. `\_PTS(state)` (acpi_enter_sleep_state_prep) + /// 4. `\_SI._SST(sst_value)` (acpi_enter_sleep_state_prep) + /// 5. PM1 register write (acpi_enter_sleep_state) + /// + /// This function performs step 1 here; steps 3-5 are inside + /// `set_global_s_state`. The split is intentional: step 1 is + /// the *transition* notification (separate from the *target* + /// state in _PTS); steps 3-5 must be interleaved with the + /// actual PM1 write so firmware sees _PTS immediately before + /// the SLP_TYP write. pub fn enter_sleep_state(&self, state: u8) { - // Step 0 (Linux 7.1): evaluate _TTS(transition to state). - // acpi_sleep_tts_switch() runs at the start of the state - // transition, separate from _PTS (which is the *target* state). + // Step 1: _TTS(transition). Distinct from _PTS(target). + // Linux's acpi_sleep_tts_switch runs at the START of the + // transition, before _PTS. self.transition_to_s_state(state); - // Steps 1-5: standard enter_sleep_state (Phase D). + // Steps 3-5: handled by set_global_s_state (Phase D). self.set_global_s_state(state); } @@ -1127,6 +1269,53 @@ impl Facs { .expect("FACS already validated in new()"); facs.waking_vector } + + /// Set the 32-bit firmware waking vector. + /// + /// The kernel writes its S3-resume trampoline address here + /// before transitioning the platform to S3. The platform + /// firmware then jumps to this address on wake. This mirrors + /// Linux 7.1 `acpi_set_firmware_waking_vector` in + /// `drivers/acpi/acpica/hwxfsleep.c:92`. + /// + /// Returns true if the write succeeded, false on bounds error. + pub fn set_waking_vector(&mut self, addr: u32) -> bool { + if self.0 .0.len() < 36 { + // FACS has at minimum the 36-byte header; writing + // waking_vector (at offset 32) requires the full 36 + // bytes. Earlier FACS revisions (1.0) have only the + // 32-bit vector. + return false; + } + // SAFETY: Facs owns the underlying Sdt bytes; writing the + // waking_vector at offset 32 is a 4-byte aligned store of + // a u32 into a packed struct field. The Sdt bytes are + // 'static for the lifetime of the ACPI context (mapped + // from physical memory at boot and never reallocated). + let facs: &mut FacsStruct = unsafe { + &mut *((self.0 .0).as_mut_ptr() as *mut FacsStruct) + }; + facs.waking_vector = addr; + true + } + + /// Set the 64-bit x_waking_vector (FACS revision >= 2). + /// Used on platforms with a 64-bit waking vector (most x86_64). + /// Mirrors Linux 7.1 `acpi_set_firmware_waking_vector64`. + pub fn set_x_waking_vector(&mut self, addr: u64) -> bool { + if self.0 .0.len() < 64 { + // x_waking_vector is at offset 40 in the FACS. The + // FACS must be at least 48 bytes for it to be present. + return false; + } + // SAFETY: same as set_waking_vector. The x_waking_vector + // field is at offset 40, 8 bytes wide. + let facs: &mut FacsStruct = unsafe { + &mut *((self.0 .0).as_mut_ptr() as *mut FacsStruct) + }; + facs.x_waking_vector = addr; + true + } } impl Deref for Facs {