Cortex-M Low-Power Modes: WFI, Sleep-on-Exit and Deep Sleep in Practice
Most embedded programs spend the majority of their time waiting: for the next timer tick, for a button press, for a sensor reading. A typical main loop that polls a flag keeps the CPU running at full speed doing nothing useful, and on a battery-powered device that wasted energy decides the battery life. Cortex-M cores provide standard instructions and control bits for sleeping, and every vendor adds deeper modes on top. This tutorial explains the core mechanisms, shows how they map to a real chip, the NXP LPC1768, and how to measure the improvement yourself.
The Core Sleep Mechanisms
The Cortex-M architecture defines two instructions that stop the processor clock until something happens, and three bits in the System Control Register (SCB->SCR at 0xE000ED10) that change their behaviour.
| Item | What it does |
|---|---|
| WFI (Wait For Interrupt) | Sleeps until an enabled interrupt becomes pending, or a debug request arrives |
| WFE (Wait For Event) | Sleeps until an event: an interrupt with SEVONPEND, an SEV instruction, or an external event signal. Returns immediately if the event register is already set |
| SCR.SLEEPONEXIT (bit 1) | After the last interrupt handler returns, go back to sleep instead of returning to main |
| SCR.SLEEPDEEP (bit 2) | Selects deep sleep; the vendor decides what deep sleep switches off |
| SCR.SEVONPEND (bit 4) | Any newly pending interrupt counts as an event for WFE, even if it is disabled |
In normal sleep only the CPU clock stops. Peripherals, timers, DMA and memories keep running, and wake-up is almost instant. In deep sleep the vendor typically stops the main oscillator, the PLL and most peripheral clocks, so current drops much further, but only certain sources such as external interrupt pins, the RTC or a watchdog can wake the chip, and the clock tree must be restored afterwards.
Power Modes on the LPC1768
NXP combines the SLEEPDEEP bit with the PM bits of its PCON register (in the system control block, LPC_SC->PCON) to select four modes. All of them are entered by executing WFI.
| Mode | SCR.SLEEPDEEP | PCON PM1:PM0 | What stops | Typical wake sources |
|---|---|---|---|---|
| Sleep | 0 | 00 | CPU clock only | Any enabled interrupt |
| Deep-sleep | 1 | 00 | Main oscillator, PLL, most clocks; state retained | External interrupts, RTC, some peripherals |
| Power-down | 1 | 01 | As deep-sleep plus flash and internal oscillator | External interrupts, RTC |
| Deep power-down | 1 | 11 | Almost everything; RAM contents are lost | RTC alarm or reset pin; restarts like a reset |
Check the LPC17xx user manual for the exact list of wake-up sources and current figures for each mode, since they depend on the chip revision and which blocks you leave enabled. The same pattern of core SLEEPDEEP plus a vendor register applies to TM4C, SAMD, nRF52 and other families with different names.
Example: Interrupt-Driven Program with Sleep-on-Exit
This program toggles an LED once per second from the Timer0 interrupt and sleeps the rest of the time. Unused peripherals are powered off through PCONP, which gates their clocks. With SLEEPONEXIT set, the core never returns to the main loop at all: it wakes, runs the handler and immediately sleeps again, saving the instructions needed to return and re-enter WFI.
#include "LPC17xx.h"
#define LED1 (1u << 18) /* P1.18 */
void TIMER0_IRQHandler(void)
{
LPC_TIM0->IR = 1u; /* clear MR0 flag */
if (LPC_GPIO1->FIOPIN & LED1)
LPC_GPIO1->FIOCLR = LED1;
else
LPC_GPIO1->FIOSET = LED1;
}
static void power_down_unused(void)
{
/* Keep only Timer0 (bit 1) and GPIO (bit 15). Every other
* peripheral clock in PCONP is switched off. Re-enable a bit
* before using that peripheral again. */
LPC_SC->PCONP = (1u << 1) | (1u << 15);
}
static void timer0_start_1s(void)
{
uint32_t pclk = SystemCoreClock / 4u; /* default PCLKSEL0 = CCLK/4 */
LPC_TIM0->TCR = 0x02;
LPC_TIM0->PR = (pclk / 1000u) - 1u; /* 1 ms tick */
LPC_TIM0->MR0 = 1000u - 1u; /* 1 s period */
LPC_TIM0->MCR = 0x03; /* interrupt + reset */
NVIC_EnableIRQ(TIMER0_IRQn);
LPC_TIM0->TCR = 0x01;
}
int main(void)
{
SystemCoreClockUpdate();
power_down_unused();
LPC_GPIO1->FIODIR |= LED1;
timer0_start_1s();
/* Normal sleep (SLEEPDEEP = 0, PCON PM = 00), and sleep on exit */
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
LPC_SC->PCON &= ~0x3u;
SCB->SCR |= SCB_SCR_SLEEPONEXIT_Msk;
__DSB(); /* finish all writes first */
__WFI(); /* from now on only the ISR runs */
while (1) /* reached only if SLEEPONEXIT is cleared */
__WFI();
}
The DSB() before WFI() makes sure outstanding memory writes, such as the SCR update, complete before the core stops. To leave sleep-on-exit and return to main, clear SCB_SCR_SLEEPONEXIT_Msk inside a handler; when it returns, execution continues after the WFI.
Other Ways to Reduce Current
- Lower the clock. Dynamic current rises roughly in proportion to frequency, so run the PLL slower or bypass it when full speed is not needed, then call SystemCoreClockUpdate.
- Do not leave input pins floating. A floating CMOS input can sit in the middle of its switching range and draw extra current; enable a pull-up or pull-down or drive unused pins as outputs.
- Turn off external loads such as LEDs, sensor modules and voltage regulators with a GPIO-controlled switch when idle.
- Use a timer interrupt or RTC alarm to wake up periodically instead of polling in a loop.
- After waking from deep sleep or power-down, re-run the clock setup, because the PLL and main oscillator were stopped and the chip runs from its internal oscillator.
Measuring the Result
Never trust a power optimisation you have not measured. Break the supply line to the microcontroller, often a jumper on development boards, and insert a digital multimeter in its current range. Record the current with the original polling loop, with WFI added, and with unused peripherals gated. On boards with a USB debug interface, the interface chip and power LED draw current too, so measure only the target supply. A debugger connection can also keep debug clocks running during sleep, so disconnect it for final measurements.
Battery life (hours) = Battery capacity (mAh) / Average current (mA) Average current = (I_active x t_active + I_sleep x t_sleep) / (t_active + t_sleep)
The formula shows why sleep current matters so much. If the device is active for 10 ms every second, the sleep current is present 99 percent of the time, and reducing it often matters more than making the active code faster.
Summary
Cortex-M cores sleep with WFI or WFE, controlled by SLEEPONEXIT, SLEEPDEEP and SEVONPEND in SCB->SCR. Vendors build deeper modes on SLEEPDEEP; on the LPC1768 the PCON PM bits choose deep-sleep, power-down or deep power-down. Combine interrupt-driven design, sleep-on-exit, peripheral clock gating, lower clock speeds and correctly terminated pins, and confirm each step with a current measurement.