The puzzle
A sensor node wakes once a minute, reads a sensor for 20 ms and waits again. On the bench its battery should last years; in the field it lasts three weeks. The firmware “waits” in a loop that checks the time until the next reading is due. To the program that is idle. To the battery it is a processor running flat out, doing nothing, all minute long. What does it take to make a microcontroller actually sleep, and what wakes it again?
STEP 1
Idle is not asleep
A CMOS chip draws current in two ways. Every time a node switches, charge moves in and out of its capacitance: the dynamic power
grows with the clock frequency , the switched capacitance , the fraction of it that switches each cycle, and the square of the supply voltage . On top of that, transistors that are nominally off still leak: static power, present whenever the supply is on. An idle loop keeps every clock running, so it pays the full dynamic cost for nothing.
Three levers follow from the formula. Stop the clocks that are not needed (dynamic power falls to almost nothing for that block). Lower the voltage where the chip allows it; the pico-sdk’s vreg_set_voltage(), for example, sets the RP2040’s core regulator anywhere from 0.85 V to 1.30 V (1.10 V by default), and the term rewards every step down, though a lower voltage may not support the full clock speed. Switch power off to whole domains, which removes leakage too, at the cost of losing their state. Sleep states are packaged combinations of these levers.
STEP 2
A ladder of sleep states
On a Cortex-M the usual way in is one instruction: WFI (or WFE, unit 9). What the chip does when the core executes it depends on one bit, SLEEPDEEP in the System Control Register (SCB->SCR, bit 2), and on the vendor’s power controller, which decides what “deep” means on that chip. (The RP2040’s dormant state, below, is the exception: a register write enters it.)
↑ This step uses the figure at the top of the page.
The STM32F4 HAL’s power driver lists three states. Sleep (SLEEPDEEP clear) stops the core; peripherals keep running and any interrupt wakes it. Stop (SLEEPDEEP set) stops every clock in the 1.2 V core domain and the PLL and both high-speed oscillators, but keeps SRAM and registers; the regulator stays on, optionally in a low-power mode. Standby turns the core regulator off entirely, so SRAM and registers are lost apart from the RTC, its backup registers and the backup SRAM.
The RP2040 does it differently. It gates clocks through two registers, SLEEP_EN0 and SLEEP_EN1, that say which clocks stay on while the processor is in deep sleep; they reset to all ones, so a deep sleep with them untouched keeps every clock running. The sleep code in the pico-extras library first moves the system onto the crystal or ring oscillator and stops both PLLs, then clears the enables for everything except the RTC’s clock, sets SLEEPDEEP and executes WFI. Its dormant state goes further: writing a magic value to the oscillator’s DORMANT register stops the oscillator itself, and with it every clock derived from it.
STEP 3
What can wake it
The deeper the state, the less of the chip is awake to notice an event, so the list of wake-up sources shrinks:
| state | wakes on | resumes |
|---|---|---|
| Sleep (WFI) | any enabled interrupt | after the WFI |
| STM32F4 Stop | an EXTI line: a pin, or an RTC alarm, wake-up timer, tamper or time stamp | after the WFI, on the HSI oscillator |
| STM32F4 Standby | WKUP pin rising edge, RTC events, the NRST pin, an IWDG reset | from the reset vector |
| RP2040 sleep | whatever the clocks left running can raise (the RTC alarm in the SDK example) | after the WFI |
| RP2040 dormant | a GPIO edge or level set up as a dormant wake-up | after the oscillator restarts |
Choose the state from the event, not the other way round. A UART in Stop mode has no clock, so it cannot receive the byte that should wake the system; the pin can be an EXTI line instead, but that first byte is lost unless the protocol repeats it. And look at what must happen after waking: an STM32 leaving Stop runs from its internal RC oscillator until firmware restarts the crystal and PLL, so a UART or timer configured for the PLL clock runs at the wrong rate until then. Standby is not a pause at all: the chip restarts, and the start-up code must look at the standby flag (PWR_FLAG_SB) to know it is waking rather than powering up.
Pins need thought too. In Sleep and Stop the STM32F4 keeps every pin in its state; in Standby most pins are high-impedance, so an output that held a MOSFET off now floats, and only an external pull resistor (unit 1, lesson 3) keeps it defined.
STEP 4
Going to sleep without missing the wake-up
The obvious idle loop has a race:
while (!data_ready) { /* the interrupt sets data_ready */
__WFI(); /* if the interrupt fires between the test and here, */
} /* the core sleeps until the NEXT interrupt */
If the interrupt arrives after the test but before WFI, its handler runs, sets the flag, and returns; the core then sleeps with the flag already set and nothing left to wake it. The fix relies on a property of WFI from unit 9: it wakes on a pending interrupt even when PRIMASK masks it. Mask interrupts, test, sleep, then unmask, so the handler runs after the wake-up instead of in the gap:
__disable_irq(); /* PRIMASK = 1: interrupts pend but do not run */
if (!data_ready)
__WFI(); /* still wakes when the interrupt becomes pending */
__enable_irq(); /* the handler runs here */
FreeRTOS’s tickless idle for the Cortex-M0 does exactly this. When every task is blocked for at least a couple of ticks, vPortSuppressTicksAndSleep() executes cpsid i (the comment explains that its usual critical-section macro would mask the interrupts that must end the sleep), checks that no task has become ready, reprograms SysTick to fire at the end of the expected idle time instead of every tick, and only then executes wfi. Without tickless idle a 1 kHz tick would wake the chip a thousand times a second for nothing.
For designs that do everything in interrupt handlers, SLEEPONEXIT (SCR bit 1) sends the core back to sleep automatically on return from the last handler, without returning to the main loop at all.
STEP 5
The price of waking up
A deeper state saves current while asleep, but waking from it costs time and charge: the crystal must restart, the PLL must lock, a regulator coming out of low-power mode must settle (the STM32 HAL notes the extra start-up delay when the regulator is in low-power mode during Stop). If the chip draws for a wake-up time , then over an idle gap deep sleep beats a light sleep drawing only when
where is the deep-sleep current. The wake-up time also adds straight onto the response time, which may rule a state out regardless of the charge.
Operating systems encode the same trade-off. Each power state in Zephyr declares a minimum residency and an exit latency, and its residency policy enters the state that saves the most, provided the time to the next scheduled event is at least their sum. It also reminds the application that setting up a wake-up event is its job, and that not every wake-up source works in every power mode.
Waking from a deep sleep is not free: the oscillator and PLL must restart and the core supply must settle, and all that time the chip draws current without doing useful work. Averaged over one idle gap, deep sleep costs its sleep current plus the wake-up charge spread over the gap. For short gaps a light sleep, which wakes almost instantly, uses less. The currents are illustrative: 20 µA in deep sleep and 5 mA during the wake-up; measure your own part.
STEP 6
Worked example: events every 10 ms
A device must handle an event every 10 ms. Assume (illustratively) a light sleep at mA that wakes instantly, and a deep sleep at µA that takes ms at mA to wake.
The break-even gap is
so the 10 ms gap is long enough. Averaged over one gap, deep sleep draws mA against 2 mA: about half. With a 1 s gap the wake-up is spread thinner and deep sleep averages mA, some 67 times less than light sleep. With a 3 ms gap it would average 3.34 mA, worse than not bothering. And if each event had to be answered within 1 ms, the 2 ms wake-up rules deep sleep out whatever the charge.
MYTHS AND FACTS
Common misconceptions
WFI switches the chip off
It stops the core’s clock. Peripherals, oscillators and PLLs keep running unless SLEEPDEEP and the vendor’s power controller stop them.
A deeper sleep always saves energy
Only for idle gaps longer than the break-even time; below it the wake-up costs more than it saves.
After waking, the program carries on as before
From Stop the STM32 runs on its RC oscillator until the clocks are rebuilt; from Standby it starts again from reset.
Any interrupt will wake it
Only sources that are still clocked and routed to the wake-up logic: EXTI lines in Stop, a few pins and the RTC in Standby, configured GPIO in RP2040 dormant.
Check yourself
Answer in your head, then open the card.
With mA, µA, mA and ms, what is the break-even gap?
ms.
Code calls HAL_PWR_EnterSTOPMode() and, after waking, prints at 115 200 baud; the text is garbled. Why?
On leaving Stop the STM32F4 runs from the HSI oscillator, not the PLL. The UART’s divisor was computed for the PLL-derived clock, so its baud rate is wrong until firmware restores the clock configuration.
Why does the safe idle sequence disable interrupts before testing the flag, and why does that not stop WFI waking?
Masking closes the window in which an interrupt could set the flag between the test and WFI. WFI wakes on any pending interrupt of sufficient priority even while PRIMASK masks it, so the core still wakes; the handler then runs as soon as interrupts are re-enabled.
An RP2040 enters deep sleep with SLEEPDEEP set but the current barely changes. What was probably forgotten?
The SLEEP_EN0/SLEEP_EN1 registers, which reset to all ones and so keep every clock running in deep sleep; and the PLLs, which the pico-extras code stops by moving to the crystal or ring oscillator first.
Sources (7)
- STMicroelectronics, stm32f4xx-hal-driver, Src/stm32f4xx_hal_pwr.c and Inc/stm32f4xx_hal_pwr.h — “Sleep mode: Cortex-M4 core stopped, peripherals kept running. Stop mode: all clocks are stopped … Standby mode: 1.2V domain powered off”; Stop exits on “Any EXTI Line (Internal or External)”, and “the HSI RC oscillator is selected as system clock”; in Standby “SRAM and register contents are lost except for the RTC registers, RTC backup registers, backup SRAM”; HAL_PWR_EnterSTOPMode sets SCB_SCR_SLEEPDEEP then executes WFI or WFE; in Sleep and Stop “all I/O pins keep the same state as in Run mode”, in Standby “all I/O pins are high impedance” except a listed few; pwr.h: PWR_FLAG_SB “indicates that the system was resumed from StandBy mode”
- Raspberry Pi Ltd, pico-extras (sdk-1.5.1 tag), src/rp2_common/pico_sleep/sleep.c — “The difference between sleep and dormant is that ALL clocks are stopped in dormant mode”; sleep_run_from_dormant_source() moves clk_ref and clk_sys to the XOSC or ROSC and calls pll_deinit(); sleep_goto_sleep_until() writes sleep_en0 = CLOCKS_SLEEP_EN0_CLK_RTC_RTC_BITS, sleep_en1 = 0, sets M0PLUS_SCR_SLEEPDEEP_BITS and calls __wfi(); dormant wake-up through gpio_set_dormant_irq_enabled(). The master branch adds sleep_power_up() to restore the clocks after waking, and its include/pico/sleep.h notes that dormant wake-up by the RTC needs the RTC “driven by an external clock”.
- Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_xosc/xosc.c and rp2040/hardware_regs/include/hardware/regs/clocks.h — xosc_dormant(): “WARNING: This stops the xosc until woken up by an irq”, then waits for XOSC_STATUS_STABLE; CLOCKS_SLEEP_EN0 “enable clock in sleep mode”, reset value 0xffffffff (SLEEP_EN1 0x00007fff)
- Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_vreg/include/hardware/vreg.h — enum vreg_voltage from VREG_VOLTAGE_0_85 to VREG_VOLTAGE_1_30; VREG_VOLTAGE_DEFAULT = VREG_VOLTAGE_1_10, “Default voltage on power up”
- Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h — SCB->SCR bits SEVONPEND (bit 4), SLEEPDEEP (bit 2) and SLEEPONEXIT (bit 1)
- Zephyr Project, doc/services/pm/system.rst “System Power Management” — “It is the application's responsibility to set up a wake-up event … some wake-up sources may not be usable in all power modes”; the idle thread locks interrupts before pm_system_suspend(); residency policy: enter a state only if “time_to_next_scheduled_event >= (state.min_residency_us + state.exit_latency)”
- FreeRTOS-Kernel, portable/GCC/ARM_CM0/port.c and include/FreeRTOS.h — vPortSuppressTicksAndSleep() (configUSE_TICKLESS_IDLE = 1): “cpsid i” rather than taskENTER_CRITICAL(), “as that will mask interrupts that should exit sleep mode”; aborts if eTaskConfirmSleepModeStatus() says so; reprograms SysTick for the expected idle time, then dsb, wfi, isb and “cpsie i”; configEXPECTED_IDLE_TIME_BEFORE_SLEEP defaults to 2 and must not be less than 2