acpid: complete Linux-compatible AML S-state sequence + s2idle stubs

Phase I (LG Gram 16 (2025) / Arrow Lake-H S-state work).

This commit implements the full Linux 7.1 S-state AML method
sequence in userspace acpid, plus stubs for s2idle (Modern
Standby). The kernel-side s2idle wire (new AcpiVerb variants
EnterS2Idle / ExitS2Idle) is the next step; see
local/docs/SLEEP-IMPLEMENTATION-PLAN.md for the gap analysis.

Changes:

* FACS: add set_waking_vector / set_x_waking_vector methods.
  These let acpid write the firmware waking vector for S3
  resume, mirroring Linux 7.1
  drivers/acpi/acpica/hwxfsleep.c:92
  (acpi_set_firmware_waking_vector).
* FACS access: add facs_mut() mutable accessor on
  AcpiContext (single-writer by construction).
* AML methods: add set_system_status_indicator() that calls
  \_SI._SST(n). The canonical values are 0=working, 1=waking,
  2=sleeping, 3=sleep-context, 7=indicator-off. Mirrors Linux
  ACPI 6.5 §6.5.1 (System Status Indicator).
* wake_from_s_state(): wrap \_WAK(n) with the full Linux wake
  sequence (\_SI._SST(2) before, \_SI._SST(1) after). Mirrors
  drivers/acpi/acpica/hwsleep.c:255-314.
* enter_sleep_state(): only call \_TTS here; \_PTS + \_SST +
  PM1 writes remain in set_global_s_state (Phase D, no
  duplication).
* s2idle: add enter_s2idle() and exit_s2idle() methods on
  AcpiContext. These prepare/finish the s2idle path on systems
  without \_S3 (LG Gram 2025). Currently a no-op for the kernel
  coordination; the AML \_WAK(0) sequence runs via
  wake_from_s_state(0) on exit.

Cross-references:
* drivers/acpi/sleep.c (Linux 7.1) — acpi_suspend_begin/enter
* drivers/acpi/acpica/hwxfsleep.c — acpi_enter_sleep_state_prep
* drivers/acpi/acpica/hwsleep.c — acpi_hw_legacy_wake
* kernel/power/suspend.c — s2idle_loop, s2idle_state
* drivers/acpi/acpica/hwesleep.c — acpi_hw_execute_sleep_method

Files changed:
  drivers/acpid/src/acpi.rs (+203 -14)
This commit is contained in:
Red Bear OS
2026-07-01 01:17:15 +03:00
parent c335553c7e
commit 5d2d114bf9
+203 -14
View File
@@ -531,6 +531,88 @@ impl AcpiContext {
self.facs.as_ref() 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 /// Returns the FACS 32-bit firmware_waking_vector, or `None` if no
/// FACS is present. The kernel-side S3 entry path uses this to /// FACS is present. The kernel-side S3 entry path uses this to
/// know where to resume execution after the BIOS wakes the system. /// 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). /// The System Status Indicator method (ACPI 6.5 §6.5.1) is an
/// Called by userspace on resume from a sleep state. The ACPI spec /// optional method that firmware uses to display the current
/// requires the OS to call `_WAK` on the same state that was passed /// system status to the user (e.g. via a power LED). The
/// to `_PTS` before the sleep. /// canonical values are:
/// ///
/// Returns the system state arg from `_WAK` if the method returns /// | Value | Name | Linux 7.1 Constant |
/// one, or `Ok(state)` if the method is missing or returns void. /// |-------|-------------------|----------------------------|
/// | 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<u64, AmlEvalError> { pub fn wake_from_s_state(&self, state: u8) -> Result<u64, AmlEvalError> {
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. /// Set global power state, generic.
/// ///
/// This is the public API that should be used by all callers. The /// This is the public API that should be used by all callers. The
/// kernel calls this via the `kstop_pipe` shim. Internally it /// kernel calls this via the `kstop_pipe` shim. Internally it
/// delegates to `set_global_s_state` (which is S5-specialized) or /// delegates to `set_global_s_state` which performs the AML +
/// the kernel's S3 entry point. /// 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) { pub fn enter_sleep_state(&self, state: u8) {
// Step 0 (Linux 7.1): evaluate _TTS(transition to state). // Step 1: _TTS(transition). Distinct from _PTS(target).
// acpi_sleep_tts_switch() runs at the start of the state // Linux's acpi_sleep_tts_switch runs at the START of the
// transition, separate from _PTS (which is the *target* state). // transition, before _PTS.
self.transition_to_s_state(state); 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); self.set_global_s_state(state);
} }
@@ -1127,6 +1269,53 @@ impl Facs {
.expect("FACS already validated in new()"); .expect("FACS already validated in new()");
facs.waking_vector 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 { impl Deref for Facs {