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)
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 pinsWFI — Wait For Interrupt (light sleep)
WFI halts the CPU until any enabled interrupt fires. Peripherals keep running.
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:
#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
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:
// 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:
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
// 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
// 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 cellWake sources summary
// 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);
}