Skip to content
C++
Domain Deep-Dive
Intermediate

Power Management in Embedded C++

"Low-power C++ on ARM Cortex-M: sleep modes (WFI/WFE), run-mode clock gating, tickless idle, measuring current consumption, and wake-up sources."

TL;DR

Battery-powered IoT devices spend 99% of their time sleeping. ARM Cortex-M offers five sleep modes from light sleep (WFI, ~1mA) to deep stop (RTC only, ~1µA). The key pattern: compute quickly, configure wake sources, sleep, repeat. C++ RAII works well here — a SleepGuard can ensure the peripheral is left in the right state regardless of early returns.

ARM Cortex-M sleep modes (STM32 example)

cpp
Mode           Current  Wake latency  What stays on
─────────────────────────────────────────────────────
Run (168MHz)   ~50 mA   —             Everything
Sleep (WFI)    ~5 mA    ~1 µs         CPU off, peripherals on, SRAM on
Stop 0         ~300 µA  ~5 µs         Regulators on, SRAM on, low-speed clocks
Stop 1         ~100 µA  ~15 µs        Regulators reduced
Stop 2         ~10 µA   ~15 µs        Regulators minimal, only LP UART/I2C wake
Standby        ~2 µA    Reset         Only RTC, backup regs, wakeup pins
Shutdown       ~0.3 µA  Reset         Only wakeup pins

WFI — Wait For Interrupt (light sleep)

WFI halts the CPU until any enabled interrupt fires. Peripherals keep running.

cpp
void idleLoop() {
    while (true) {
        if (workQueue.empty()) {
            __WFI();  // CPU halts here until interrupt wakes it
        }
        processNextTask();
    }
}

// With explicit data/instruction sync
void enterSleep() {
    __DSB();  // ensure all writes are complete before sleeping
    __ISB();  // flush instruction pipeline
    __WFI();  // sleep
}

Stop mode — deep sleep with wake sources

Stop mode shuts down voltage regulators and most clocks. Requires explicit wake source:

cpp
#include "stm32l4xx_hal.h"  // STM32 HAL example

void enterStop2(uint32_t sleep_ms) {
    // Configure RTC wakeup timer as wake source
    HAL_RTCEx_SetWakeUpTimer_IT(&hrtc,
        sleep_ms * 2,        // 2 ticks per ms (1024 Hz clock / 512 = 2 Hz → adjust per config)
        RTC_WAKEUPCLOCK_RTCCLK_DIV16);

    // Disable SysTick interrupt (would prevent deep sleep)
    HAL_SuspendTick();

    // Enter Stop 2 — regulator reduced, SRAM retained
    HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);

    // Woke up — re-initialize clocks (Stop mode resets PLL)
    SystemClock_Config();
    HAL_ResumeTick();

    HAL_RTCEx_DeactivateWakeUpTimer(&hrtc);
}

RAII peripheral manager for power

cpp
class PeripheralClock {
public:
    enum class Device { USART1, SPI1, I2C1, ADC1 };

    explicit PeripheralClock(Device dev) : dev_(dev) {
        enable(dev_);
    }
    ~PeripheralClock() {
        disable(dev_);
    }
    PeripheralClock(const PeripheralClock&) = delete;

private:
    static void enable(Device d) {
        switch (d) {
        case Device::USART1: __HAL_RCC_USART1_CLK_ENABLE();  break;
        case Device::SPI1:   __HAL_RCC_SPI1_CLK_ENABLE();    break;
        case Device::I2C1:   __HAL_RCC_I2C1_CLK_ENABLE();    break;
        case Device::ADC1:   __HAL_RCC_ADC1_CLK_ENABLE();    break;
        }
    }
    static void disable(Device d) {
        switch (d) {
        case Device::USART1: __HAL_RCC_USART1_CLK_DISABLE(); break;
        case Device::SPI1:   __HAL_RCC_SPI1_CLK_DISABLE();   break;
        case Device::I2C1:   __HAL_RCC_I2C1_CLK_DISABLE();   break;
        case Device::ADC1:   __HAL_RCC_ADC1_CLK_DISABLE();   break;
        }
    }

    Device dev_;
};

// Usage — clock gated automatically even on early return or exception
void readSensor(SensorData& out) {
    PeripheralClock i2c{PeripheralClock::Device::I2C1};
    // I2C clock is now on

    if (!waitReady()) return;  // I2C clock off here — no leak
    readI2CData(out);
    // I2C clock off here
}

FreeRTOS tickless idle

FreeRTOS normally wakes every tick (1ms at 1kHz) to check for tasks. Tickless idle suppresses these wakes when no task is ready:

cpp
// FreeRTOSConfig.h
#define configUSE_TICKLESS_IDLE    1
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP  2  // min ticks to sleep

// Implement the tickless hook (called by FreeRTOS idle task)
extern "C" void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime) {
    uint32_t sleep_ms = xExpectedIdleTime * portTICK_PERIOD_MS;

    // Stop the SysTick
    portDISABLE_INTERRUPTS();
    if (eTaskConfirmSleepModeStatus() == eAbortSleep) {
        portENABLE_INTERRUPTS();
        return;
    }

    // Configure RTC wakeup
    setupRTCWakeup(sleep_ms);

    // Enter stop mode
    HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);

    // Woke up — may be from RTC or other interrupt
    uint32_t actual_sleep = getRTCElapsedMs();
    SystemClock_Config();  // re-init PLL

    // Compensate tick count
    TickType_t ticks_slept = actual_sleep / portTICK_PERIOD_MS;
    vTaskStepTick(ticks_slept);

    portENABLE_INTERRUPTS();
}

Clock scaling for run mode

Reduce clock speed when full performance isn't needed:

cpp
class ClockManager {
public:
    enum class Speed { Low_4MHz, Medium_16MHz, Full_80MHz };

    static void set(Speed s) {
        switch (s) {
        case Speed::Low_4MHz:
            // Switch to MSI 4MHz — lowest active power
            configMSI(RCC_MSIRANGE_6);  // 4.096 MHz
            break;
        case Speed::Medium_16MHz:
            // HSI 16MHz — good for I2C, UART
            configHSI();
            break;
        case Speed::Full_80MHz:
            // PLL from HSE — full performance
            configPLL_80MHz();
            break;
        }
    }
};

// Typical pattern: slow down during sleep-adjacent code
void processingLoop() {
    while (true) {
        // Wait for work at low power
        ClockManager::set(ClockManager::Speed::Low_4MHz);
        while (!hasWork())
            __WFI();

        // Switch to full speed for computation
        ClockManager::set(ClockManager::Speed::Full_80MHz);
        processWork();
    }
}

Measuring current consumption

cpp
// INA219 current sensor over I2C — measure your own board's consumption
class CurrentMeter {
public:
    float readMilliamps() {
        uint16_t raw = readRegister(INA219_REG_CURRENT);
        return static_cast<int16_t>(raw) * CURRENT_LSB_MA;
    }

    float readMilliwatts() {
        uint16_t raw = readRegister(INA219_REG_POWER);
        return raw * POWER_LSB_MW;
    }

private:
    static constexpr float CURRENT_LSB_MA = 0.1f;  // 0.1 mA/LSB
    static constexpr float POWER_LSB_MW   = 2.0f;  // 2 mW/LSB
};

Duty-cycle pattern for IoT sensors

cpp
// Typical IoT node: measure every 10s, active for ~5ms
void sensorNode() {
    while (true) {
        // Active phase (~5ms)
        {
            PeripheralClock adc{PeripheralClock::Device::ADC1};
            SensorData data = readADC();
            transmitLora(data);
        }

        // Sleep phase (~9995ms)
        enterStop2(9995);
    }
}

// Power calculation:
// Active:  5ms × 10mA   = 0.05 mAs
// Sleep:  9995ms × 0.01mA = 99.95 mAs
// Average: (0.05 + 99.95) / 10000ms = 10 µA average → years on coin cell

Wake sources summary

cpp
// Configure multiple wake sources
void configureWakeSources() {
    // RTC alarm — scheduled wake
    HAL_RTC_SetAlarm_IT(&hrtc, &alarm, FORMAT_BIN);

    // EXTI pin — button or external event
    HAL_GPIO_Init(GPIOC, &gpio_exti_cfg);
    HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);

    // LPUART1 — wake on serial data (Stop 2 only)
    __HAL_RCC_LPUART1_CLKSOURCE_CONFIG(RCC_LPUART1CLKSOURCE_LSE);
    HAL_UARTEx_EnableStopMode(&hlpuart1);

    // Wakeup pins (WKUP1-5) — for Standby mode
    HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
}
Edit on GitHubUpdated 2026-05-01T00:00:00.000Z