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.

ItemWhat 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.

ModeSCR.SLEEPDEEPPCON PM1:PM0What stopsTypical wake sources
Sleep000CPU clock onlyAny enabled interrupt
Deep-sleep100Main oscillator, PLL, most clocks; state retainedExternal interrupts, RTC, some peripherals
Power-down101As deep-sleep plus flash and internal oscillatorExternal interrupts, RTC
Deep power-down111Almost everything; RAM contents are lostRTC 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.

main.c: LPC1768 timer interrupt with sleep-on-exit and peripheral power gating
#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.