kernel: s2idle MWAIT wake signal (Phase I.5)

Phase I.5: complete the acpid <-> kernel s2idle wire. After
MWAIT returns from an interrupt (typically an SCI from
acpid), the kernel now:

1. Clears S2IDLE_REQUESTED (via s2idle_request_clear)
2. Sets KSTOP_FLAG and triggers EVENT_READ on the kstop
   handle (via s2idle_signal_wake)

This is the kernel-side analog of Linux 7.1
`acpi_s2idle_wake` in `drivers/acpi/sleep.c:758`. The
existing irq_trigger in generic_irq has already routed the
SCI to acpid's listener (which opened /scheme/irq/{sci}
earlier in the boot sequence), so the AML interpretation
is done by acpid asynchronously.

The s2idle flow now:
1. acpid: enter_s2idle() (\_TTS(0), \_PTS(0), \_SST(3))
2. acpid: write 's2idle\n' to /scheme/sys/kstop
   -> kernel sets S2IDLE_REQUESTED, returns
3. Kernel idle path: mwait_loop() at deepest C-state
4. SCI breaks MWAIT (any interrupt, not just SCI)
5. Kernel mwait_loop post-handler (this commit):
   - s2idle_request_clear()
   - s2idle_signal_wake() -> KSTOP_FLAG set, EVENT_READ
6. acpid main loop: wakes from kstop handle read
7. acpid: exit_s2idle() (\_SST(2), \_WAK(0), \_SST(1))

The KSTOP_FLAG set in step 5 also serves as a 'reason'
indicator — acpid's CheckShutdown verb (kcall 2) returns
the flag, so acpid can distinguish a kstop-shutdown event
from a kstop-s2idle-wake event by polling CheckShutdown
after waking.

Hardware-agnostic: the same flow works for any platform
with Modern Standby firmware (Dell, HP, Lenovo, LG Gram,
etc.). The s2idle is the universal mechanism for low-power
idle; only the wake source (SCI, GPIO, RTC, ...) varies
per OEM.
This commit is contained in:
2026-07-01 07:10:28 +03:00
parent 75c7618313
commit 8d9f9e552f
2 changed files with 35 additions and 0 deletions
+13
View File
@@ -107,6 +107,19 @@ pub unsafe fn mwait_loop(eax_hint: u32, ecx_hint: u32) {
);
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
let _ = (eax_hint, ecx_hint);
// MWAIT returns when an interrupt fires (ecx=0 means
// "break on any interrupt"). The interrupt dispatcher
// has already run and returned by the time we get here.
// If the CPU was in s2idle (S2IDLE_REQUESTED was set by
// the kstop handler), we now know an SCI or other wake
// event has occurred. Clear the s2idle flag and trigger
// the kstop handle's EVENT_READ so acpid's main loop
// wakes up and runs the _WAK AML sequence.
if crate::scheme::acpi::s2idle_requested() {
crate::scheme::acpi::s2idle_request_clear();
crate::scheme::acpi::s2idle_signal_wake();
}
}
/// Probe MWAIT support and enter the deepest available C-state
+22
View File
@@ -73,6 +73,28 @@ pub fn s2idle_requested() -> bool {
S2IDLE_REQUESTED.load(Ordering::Acquire)
}
/// Phase I: signal acpid that s2idle MWAIT was broken by an
/// interrupt. Called from `mwait_loop` after MWAIT returns.
/// Triggers the kstop handle's EVENT_READ so acpid's main loop
/// wakes and runs the \_SST(2) → \_WAK(0) → \_SST(1) AML
/// sequence on resume.
///
/// Mirrors Linux 7.1 `acpi_s2idle_wake` in
/// `drivers/acpi/sleep.c:758` — the kernel clears
/// s2idle_state and signals the userspace ACPI driver.
pub fn s2idle_signal_wake() {
let mut token = CleanLockToken::new();
*KSTOP_FLAG.lock(token.token()) = true;
if EXISTS_KSTOP_HANDLE.load(Ordering::Relaxed) {
crate::event::trigger(
GlobalSchemes::Acpi.scheme_id(),
HandleBits::KSTOP_HANDLE.bits(),
EventFlags::EVENT_READ,
&mut token,
);
}
}
pub fn register_kstop(token: &mut CleanLockToken) -> bool {
*KSTOP_FLAG.lock(token.token()) = true;