git: 7669cbd0f064 - main - acpi: Suspend-to-idle support (s2idle)

From: Aymeric Wibo <obiwac_at_FreeBSD.org>
Date: Tue, 13 Jan 2026 22:43:11 UTC
The branch main has been updated by obiwac:

URL: https://cgit.FreeBSD.org/src/commit/?id=7669cbd0f06402573fe627e9e6f86c69cfd3d12b

commit 7669cbd0f06402573fe627e9e6f86c69cfd3d12b
Author:     Aymeric Wibo <obiwac@FreeBSD.org>
AuthorDate: 2025-06-14 15:28:49 +0000
Commit:     Aymeric Wibo <obiwac@FreeBSD.org>
CommitDate: 2026-01-13 22:21:05 +0000

    acpi: Suspend-to-idle support (s2idle)
    
    Implement STYPE_SUSPEND_TO_IDLE sleep type added in c43473dc9b83
    ("sys/power: Generic sleep types").
    
    This is a prerequisite for the firmware to enter the S0ix states. When
    suspending to idle, the system stays in an ACPI S0 state, but the CPUs
    are idled and devices are suspended/resumed before and after this as
    they would be when entering any other sleep type (except for AWAKE and
    POWEROFF).
    
    Factor out do_standby, do_sleep, and add a new do_idle function for
    idling the CPU (a future patch will make this an idle loop and not just
    a simple cpu_idle() call). In do_idle, SCIs (interrupt 9) are enabled to
    allow wake events to break the CPU out of idle.
    
    Record all the steps made instead of just the last one in slp_state,
    which allows for more flexible unwinding (will be useful to not have to
    goto breakout if the SPMC entry call fails when that is committed).
    
    A lot of this borrows from Ben Widawsky's patch: D17675. The main
    functional difference with that patch is that suspend-to-idle is a
    wholly separate sleep type in this one as opposed to being an
    alternative implementation for s2mem (S3).
    
    Reviewed by:    emaste, olce
    Approved by:    olce
    Sponsored by:   The FreeBSD Foundation
    Differential Revision:  https://reviews.freebsd.org/D48734
---
 sys/dev/acpica/acpi.c | 278 +++++++++++++++++++++++++++++++++-----------------
 1 file changed, 185 insertions(+), 93 deletions(-)

diff --git a/sys/dev/acpica/acpi.c b/sys/dev/acpica/acpi.c
index 8380f701d226..9657938371f0 100644
--- a/sys/dev/acpica/acpi.c
+++ b/sys/dev/acpica/acpi.c
@@ -58,6 +58,7 @@
 
 #if defined(__i386__) || defined(__amd64__)
 #include <machine/clock.h>
+#include <machine/intr_machdep.h>
 #include <machine/pci_cfgreg.h>
 #include <x86/cputypes.h>
 #include <x86/x86_var.h>
@@ -679,15 +680,19 @@ acpi_attach(device_t dev)
 #endif
 
     /*
-     * Probe all supported ACPI sleep states.  Awake (S0) is always supported.
+     * Probe all supported ACPI sleep states.  Awake (S0) is always supported,
+     * and suspend-to-idle is always supported on x86 only (at the moment).
      */
-    acpi_supported_sstates[ACPI_STATE_S0] = TRUE;
+    acpi_supported_sstates[ACPI_STATE_S0] = true;
     acpi_supported_stypes[POWER_STYPE_AWAKE] = true;
+#if defined(__i386__) || defined(__amd64__)
+    acpi_supported_stypes[POWER_STYPE_SUSPEND_TO_IDLE] = true;
+#endif
     for (state = ACPI_STATE_S1; state <= ACPI_STATE_S5; state++)
 	if (ACPI_SUCCESS(AcpiEvaluateObject(ACPI_ROOT_OBJECT,
 	    __DECONST(char *, AcpiGbl_SleepStateNames[state]), NULL, NULL)) &&
 	    ACPI_SUCCESS(AcpiGetSleepTypeData(state, &TypeA, &TypeB))) {
-	    acpi_supported_sstates[state] = TRUE;
+	    acpi_supported_sstates[state] = true;
 	    acpi_supported_stypes[acpi_sstate_to_stype(state)] = true;
 	}
 
@@ -705,13 +710,24 @@ acpi_attach(device_t dev)
     else if (acpi_supported_sstates[ACPI_STATE_S2])
 	sc->acpi_standby_sx = ACPI_STATE_S2;
 
-    /* Pick the first valid sleep type for the sleep button default. */
+    /*
+     * Pick the first valid sleep type for the sleep button default.  If that
+     * type was hibernate and we support s2idle, set it to that.  The sleep
+     * button prefers s2mem instead of s2idle at the moment as s2idle may not
+     * yet work reliably on all machines.  In the future, we should set this to
+     * s2idle when ACPI_FADT_LOW_POWER_S0 is set.
+     */
     sc->acpi_sleep_button_stype = POWER_STYPE_UNKNOWN;
     for (stype = POWER_STYPE_STANDBY; stype <= POWER_STYPE_HIBERNATE; stype++)
 	if (acpi_supported_stypes[stype]) {
 	    sc->acpi_sleep_button_stype = stype;
 	    break;
 	}
+    if (sc->acpi_sleep_button_stype == POWER_STYPE_HIBERNATE ||
+	sc->acpi_sleep_button_stype == POWER_STYPE_UNKNOWN) {
+	if (acpi_supported_stypes[POWER_STYPE_SUSPEND_TO_IDLE])
+	    sc->acpi_sleep_button_stype = POWER_STYPE_SUSPEND_TO_IDLE;
+    }
 
     acpi_enable_fixed_events(sc);
 
@@ -3315,7 +3331,8 @@ acpi_ReqSleepState(struct acpi_softc *sc, enum power_stype stype)
 
     return (0);
 #else
-    /* This platform does not support acpi suspend/resume. */
+    device_printf(sc->acpi_dev, "ACPI suspend not supported on this platform "
+	"(TODO suspend to idle should be, however)\n");
     return (EOPNOTSUPP);
 #endif
 }
@@ -3330,13 +3347,13 @@ acpi_ReqSleepState(struct acpi_softc *sc, enum power_stype stype)
 int
 acpi_AckSleepState(struct apm_clone_data *clone, int error)
 {
+    struct acpi_softc *sc = clone->acpi_sc;
+
 #if defined(__amd64__) || defined(__i386__)
-    struct acpi_softc *sc;
     int ret, sleeping;
 
     /* If no pending sleep type, return an error. */
     ACPI_LOCK(acpi);
-    sc = clone->acpi_sc;
     if (sc->acpi_next_stype == POWER_STYPE_AWAKE) {
     	ACPI_UNLOCK(acpi);
 	return (ENXIO);
@@ -3379,7 +3396,8 @@ acpi_AckSleepState(struct apm_clone_data *clone, int error)
     }
     return (ret);
 #else
-    /* This platform does not support acpi suspend/resume. */
+    device_printf(sc->acpi_dev, "ACPI suspend not supported on this platform "
+	"(TODO suspend to idle should be, however)\n");
     return (EOPNOTSUPP);
 #endif
 }
@@ -3418,27 +3436,133 @@ acpi_sleep_disable(struct acpi_softc *sc)
 }
 
 enum acpi_sleep_state {
-    ACPI_SS_NONE,
-    ACPI_SS_GPE_SET,
-    ACPI_SS_DEV_SUSPEND,
-    ACPI_SS_SLP_PREP,
-    ACPI_SS_SLEPT,
+    ACPI_SS_NONE	= 0,
+    ACPI_SS_GPE_SET	= 1 << 0,
+    ACPI_SS_DEV_SUSPEND	= 1 << 1,
+    ACPI_SS_SLP_PREP	= 1 << 2,
+    ACPI_SS_SLEPT	= 1 << 3,
 };
 
+static void
+do_standby(struct acpi_softc *sc, enum acpi_sleep_state *slp_state,
+    register_t rflags)
+{
+    ACPI_STATUS status;
+
+    status = AcpiEnterSleepState(sc->acpi_standby_sx);
+    intr_restore(rflags);
+    AcpiLeaveSleepStatePrep(sc->acpi_standby_sx);
+    if (ACPI_FAILURE(status)) {
+	device_printf(sc->acpi_dev, "AcpiEnterSleepState failed - %s\n",
+	    AcpiFormatException(status));
+	return;
+    }
+    *slp_state |= ACPI_SS_SLEPT;
+}
+
+static void
+do_sleep(struct acpi_softc *sc, enum acpi_sleep_state *slp_state,
+    register_t rflags, int state)
+{
+    int sleep_result;
+    ACPI_EVENT_STATUS power_button_status;
+
+    MPASS(state == ACPI_STATE_S3 || state == ACPI_STATE_S4);
+
+    sleep_result = acpi_sleep_machdep(sc, state);
+    acpi_wakeup_machdep(sc, state, sleep_result, 0);
+
+    if (sleep_result == 1 && state == ACPI_STATE_S3) {
+	/*
+	 * XXX According to ACPI specification SCI_EN bit should be restored
+	 * by ACPI platform (BIOS, firmware) to its pre-sleep state.
+	 * Unfortunately some BIOSes fail to do that and that leads to
+	 * unexpected and serious consequences during wake up like a system
+	 * getting stuck in SMI handlers.
+	 * This hack is picked up from Linux, which claims that it follows
+	 * Windows behavior.
+	 */
+	AcpiWriteBitRegister(ACPI_BITREG_SCI_ENABLE, ACPI_ENABLE_EVENT);
+
+	/*
+	 * Prevent misinterpretation of the wakeup by power button
+	 * as a request for power off.
+	 * Ideally we should post an appropriate wakeup event,
+	 * perhaps using acpi_event_power_button_wake or alike.
+	 *
+	 * Clearing of power button status after wakeup is mandated
+	 * by ACPI specification in section "Fixed Power Button".
+	 *
+	 * XXX As of ACPICA 20121114 AcpiGetEventStatus provides
+	 * status as 0/1 corresponding to inactive/active despite
+	 * its type being ACPI_EVENT_STATUS.  In other words,
+	 * we should not test for ACPI_EVENT_FLAG_SET for time being.
+	 */
+	if (ACPI_SUCCESS(AcpiGetEventStatus(ACPI_EVENT_POWER_BUTTON,
+	    &power_button_status)) && power_button_status != 0) {
+	    AcpiClearEvent(ACPI_EVENT_POWER_BUTTON);
+	    device_printf(sc->acpi_dev, "cleared fixed power button status\n");
+	}
+    }
+
+    intr_restore(rflags);
+
+    /* call acpi_wakeup_machdep() again with interrupt enabled */
+    acpi_wakeup_machdep(sc, state, sleep_result, 1);
+
+    AcpiLeaveSleepStatePrep(state);
+
+    if (sleep_result == -1)
+	return;
+
+    /* Re-enable ACPI hardware on wakeup from sleep state 4. */
+    if (state == ACPI_STATE_S4)
+	AcpiEnable();
+    *slp_state |= ACPI_SS_SLEPT;
+}
+
+#if defined(__i386__) || defined(__amd64__)
+static void
+do_idle(struct acpi_softc *sc, enum acpi_sleep_state *slp_state,
+    register_t rflags)
+{
+
+    intr_suspend();
+
+    /*
+     * The CPU will exit idle when interrupted, so we want to minimize the
+     * number of interrupts it can receive while idle.  We do this by only
+     * allowing SCI (system control interrupt) interrupts, which are used by
+     * the ACPI firmware to send wake GPEs to the OS.
+     *
+     * XXX We might still receive other spurious non-wake GPEs from noisy
+     * devices that can't be disabled, so this will need to end up being a
+     * suspend-to-idle loop which, when breaking out of idle, will check the
+     * reason for the wakeup and immediately idle the CPU again if it was not a
+     * proper wake event.
+     */
+    intr_enable_src(AcpiGbl_FADT.SciInterrupt);
+
+    cpu_idle(0);
+
+    intr_resume(false);
+    intr_restore(rflags);
+    *slp_state |= ACPI_SS_SLEPT;
+}
+#endif
+
 /*
  * Enter the desired system sleep state.
  *
- * Currently we support S1-S5 but S4 is only S4BIOS
+ * Currently we support S1-S5 and suspend-to-idle, but S4 is only S4BIOS.
  */
 static ACPI_STATUS
 acpi_EnterSleepState(struct acpi_softc *sc, enum power_stype stype)
 {
     register_t intr;
     ACPI_STATUS status;
-    ACPI_EVENT_STATUS power_button_status;
     enum acpi_sleep_state slp_state;
     int acpi_sstate;
-    int sleep_result;
 
     ACPI_FUNCTION_TRACE_U32((char *)(uintptr_t)__func__, stype);
 
@@ -3498,7 +3622,7 @@ acpi_EnterSleepState(struct acpi_softc *sc, enum power_stype stype)
 
     /* Enable any GPEs as appropriate and requested by the user. */
     acpi_wake_prep_walk(sc, stype);
-    slp_state = ACPI_SS_GPE_SET;
+    slp_state |= ACPI_SS_GPE_SET;
 
     /*
      * Inform all devices that we are going to sleep.  If at least one
@@ -3509,113 +3633,81 @@ acpi_EnterSleepState(struct acpi_softc *sc, enum power_stype stype)
      * bus interface does not provide for this.
      */
     if (DEVICE_SUSPEND(root_bus) != 0) {
-	device_printf(sc->acpi_dev, "device_suspend failed\n");
-	goto backout;
+        device_printf(sc->acpi_dev, "device_suspend failed\n");
+        goto backout;
     }
-    slp_state = ACPI_SS_DEV_SUSPEND;
+    slp_state |= ACPI_SS_DEV_SUSPEND;
 
-    status = AcpiEnterSleepStatePrep(acpi_sstate);
-    if (ACPI_FAILURE(status)) {
-	device_printf(sc->acpi_dev, "AcpiEnterSleepStatePrep failed - %s\n",
-		      AcpiFormatException(status));
-	goto backout;
+    if (stype != POWER_STYPE_SUSPEND_TO_IDLE) {
+	status = AcpiEnterSleepStatePrep(acpi_sstate);
+	if (ACPI_FAILURE(status)) {
+	    device_printf(sc->acpi_dev, "AcpiEnterSleepStatePrep failed - %s\n",
+		AcpiFormatException(status));
+	    goto backout;
+	}
     }
-    slp_state = ACPI_SS_SLP_PREP;
+    slp_state |= ACPI_SS_SLP_PREP;
 
     if (sc->acpi_sleep_delay > 0)
 	DELAY(sc->acpi_sleep_delay * 1000000);
 
     suspendclock();
     intr = intr_disable();
-    if (stype != POWER_STYPE_STANDBY) {
-	sleep_result = acpi_sleep_machdep(sc, acpi_sstate);
-	acpi_wakeup_machdep(sc, acpi_sstate, sleep_result, 0);
-
-	/*
-	 * XXX According to ACPI specification SCI_EN bit should be restored
-	 * by ACPI platform (BIOS, firmware) to its pre-sleep state.
-	 * Unfortunately some BIOSes fail to do that and that leads to
-	 * unexpected and serious consequences during wake up like a system
-	 * getting stuck in SMI handlers.
-	 * This hack is picked up from Linux, which claims that it follows
-	 * Windows behavior.
-	 */
-	if (sleep_result == 1 && stype != POWER_STYPE_HIBERNATE)
-	    AcpiWriteBitRegister(ACPI_BITREG_SCI_ENABLE, ACPI_ENABLE_EVENT);
-
-	if (sleep_result == 1 && stype == POWER_STYPE_SUSPEND_TO_MEM) {
-	    /*
-	     * Prevent mis-interpretation of the wakeup by power button
-	     * as a request for power off.
-	     * Ideally we should post an appropriate wakeup event,
-	     * perhaps using acpi_event_power_button_wake or alike.
-	     *
-	     * Clearing of power button status after wakeup is mandated
-	     * by ACPI specification in section "Fixed Power Button".
-	     *
-	     * XXX As of ACPICA 20121114 AcpiGetEventStatus provides
-	     * status as 0/1 corressponding to inactive/active despite
-	     * its type being ACPI_EVENT_STATUS.  In other words,
-	     * we should not test for ACPI_EVENT_FLAG_SET for time being.
-	     */
-	    if (ACPI_SUCCESS(AcpiGetEventStatus(ACPI_EVENT_POWER_BUTTON,
-		&power_button_status)) && power_button_status != 0) {
-		AcpiClearEvent(ACPI_EVENT_POWER_BUTTON);
-		device_printf(sc->acpi_dev,
-		    "cleared fixed power button status\n");
-	    }
-	}
-
-	intr_restore(intr);
-
-	/* call acpi_wakeup_machdep() again with interrupt enabled */
-	acpi_wakeup_machdep(sc, acpi_sstate, sleep_result, 1);
-
-	AcpiLeaveSleepStatePrep(acpi_sstate);
-
-	if (sleep_result == -1)
-		goto backout;
-
-	/* Re-enable ACPI hardware on wakeup from hibernate. */
-	if (stype == POWER_STYPE_HIBERNATE)
-	    AcpiEnable();
-    } else {
-	status = AcpiEnterSleepState(acpi_sstate);
-	intr_restore(intr);
-	AcpiLeaveSleepStatePrep(acpi_sstate);
-	if (ACPI_FAILURE(status)) {
-	    device_printf(sc->acpi_dev, "AcpiEnterSleepState failed - %s\n",
-			  AcpiFormatException(status));
-	    goto backout;
-	}
+    switch (stype) {
+    case POWER_STYPE_STANDBY:
+	do_standby(sc, &slp_state, intr);
+	break;
+    case POWER_STYPE_SUSPEND_TO_MEM:
+    case POWER_STYPE_HIBERNATE:
+	do_sleep(sc, &slp_state, intr, acpi_sstate);
+	break;
+    case POWER_STYPE_SUSPEND_TO_IDLE:
+#if defined(__i386__) || defined(__amd64__)
+	do_idle(sc, &slp_state, intr);
+	break;
+#endif
+    case POWER_STYPE_AWAKE:
+    case POWER_STYPE_POWEROFF:
+    case POWER_STYPE_COUNT:
+    case POWER_STYPE_UNKNOWN:
+	__unreachable();
     }
-    slp_state = ACPI_SS_SLEPT;
 
     /*
      * Back out state according to how far along we got in the suspend
      * process.  This handles both the error and success cases.
      */
 backout:
-    if (slp_state >= ACPI_SS_SLP_PREP)
+    if ((slp_state & ACPI_SS_SLP_PREP) != 0) {
 	resumeclock();
-    if (slp_state >= ACPI_SS_GPE_SET) {
+	slp_state &= ~ACPI_SS_SLP_PREP;
+    }
+    if ((slp_state & ACPI_SS_GPE_SET) != 0) {
 	acpi_wake_prep_walk(sc, stype);
 	sc->acpi_stype = POWER_STYPE_AWAKE;
+	slp_state &= ~ACPI_SS_GPE_SET;
     }
-    if (slp_state >= ACPI_SS_DEV_SUSPEND)
+    if ((slp_state & ACPI_SS_DEV_SUSPEND) != 0) {
 	DEVICE_RESUME(root_bus);
-    if (slp_state >= ACPI_SS_SLP_PREP)
+	slp_state &= ~ACPI_SS_DEV_SUSPEND;
+    }
+    if (stype != POWER_STYPE_SUSPEND_TO_IDLE && (slp_state & ACPI_SS_SLP_PREP) != 0) {
 	AcpiLeaveSleepState(acpi_sstate);
-    if (slp_state >= ACPI_SS_SLEPT) {
+	slp_state &= ~ACPI_SS_SLP_PREP;
+    }
+    if ((slp_state & ACPI_SS_SLEPT) != 0) {
 #if defined(__i386__) || defined(__amd64__)
 	/* NB: we are still using ACPI timecounter at this point. */
 	resume_TSC();
 #endif
 	acpi_resync_clock(sc);
 	acpi_enable_fixed_events(sc);
+	slp_state &= ~ACPI_SS_SLEPT;
     }
     sc->acpi_next_stype = POWER_STYPE_AWAKE;
 
+    MPASS(slp_state == ACPI_SS_NONE);
+
     bus_topo_unlock();
 
 #ifdef EARLY_AP_STARTUP