
Zephyr’s Power Management (PM) subsystem is not a single feature — it’s a layered architecture spanning CPU idle states, device runtime power management, and policy-driven orchestration. For battery-operated devices running on nRF52, STM32, or ESP32, understanding how these layers interact is the difference between weeks of battery life and days.
The System PM layer manages CPU power states. When the scheduler finds no runnable threads, it calls pm_system_suspend() which delegates to the PM policy. The policy selects a target state from the SoC’s supported states:
+--------------------------------------------------------------+| CPU POWER STATES |+--------------------------------------------------------------+| ACTIVE (RUNNING) || | || | pm_system_suspend() || v || IDLE (WFI/WFE) <-- Light sleep, instant wake || | || | Deeper sleep requested || v || STANDBY / DEEP SLEEP <-- SRAM retention, ~10-50us wake || | || | Deeper sleep requested || v || SHUTDOWN / OFF <-- Full power loss, cold boot |+--------------------------------------------------------------+
Each SoC defines its states in soc_power_states.h. For example, nRF52840 defines:
/* soc_power_states.h snippet for nRF52840 */enum power_states {SYS_POWER_STATE_ACTIVE = 0,SYS_POWER_STATE_CPU_IDLE, /* WFI - 1-2 cycle wake */SYS_POWER_STATE_DEEP_SLEEP, /* System ON idle - ~10us wake, RAM retained */SYS_POWER_STATE_SYSTEM_OFF, /* System OFF - ~1ms wake, cold boot */};
The PM policy’s job is to pick the deepest state that satisfies:
Device Runtime PM operates independently of system sleep. Each device driver implements pm_device_action_cb to handle state transitions:
/* Driver pm_action callback */static int my_device_pm_action(const struct device *dev,enum pm_device_action action){switch (action) {case PM_DEVICE_ACTION_RESUME:/* Restore registers, re-enable clocks, clear wake flags */my_device_restore_context(dev);return 0;case PM_DEVICE_ACTION_SUSPEND:/* Save context, gate clocks, configure wake sources */my_device_save_context(dev);return 0;case PM_DEVICE_ACTION_TURN_OFF:/* Power down completely, lose context */my_device_power_off(dev);return 0;case PM_DEVICE_ACTION_TURN_ON:/* Full reinitialization from reset state */return my_device_init(dev);default:return -ENOTSUP;}}
Devices advertise supported states via pm_device_init():
PM_DEVICE_DT_DEFINE(DT_NODELABEL(my_spi), my_device_pm_action);
The runtime PM framework tracks device usage with a reference count. Call pm_device_runtime_get(dev) before using a device (increments count, ensures ACTIVE), and pm_device_runtime_put(dev) after (decrements, may auto-suspend).
The PM policy is the brain. Zephyr ships with a default policy (pm_policy_default) but you can replace it. The policy implements:
/* pm_policy.h - simplified */struct pm_policy {int (*init)(void);enum pm_state (*next_state)(uint32_t idle_ticks);void (*post_sleep)(enum pm_state state, uint32_t sleep_ticks);};
A custom policy for an application with periodic sensor reads might look like:
#include <zephyr/pm/pm.h>#include <zephyr/pm/policy.h>#include <zephyr/kernel.h>#define MIN_DEEP_SLEEP_TICKS K_MSEC(10) /* 10ms minimum residency */static enum pm_state my_policy_next_state(uint32_t idle_ticks){/* Check if any device vetoes deep sleep */if (pm_device_any_busy()) {return SYS_POWER_STATE_CPU_IDLE;}/* Check wake sources - need at least one */if (!pm_wake_source_any_enabled()) {return SYS_POWER_STATE_CPU_IDLE;}/* Residency check */if (idle_ticks >= MIN_DEEP_SLEEP_TICKS) {return SYS_POWER_STATE_DEEP_SLEEP;}return SYS_POWER_STATE_CPU_IDLE;}static int my_policy_init(void){/* Register wake sources, configure policy-specific HW */return 0;}static struct pm_policy my_policy = {.init = my_policy_init,.next_state = my_policy_next_state,.post_sleep = NULL,};SYS_INIT(pm_policy_register, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
Register with pm_policy_register(&my_policy) — only one policy can be active.
Wake sources are hardware interrupts that can exit a sleep state. Each SoC defines which peripherals can wake from which states. The PM subsystem tracks enabled wake sources:
/* Enable RTC alarm as wake source for deep sleep */pm_wake_source_enable(PM_WAKE_SOURCE_RTC_ALARM);/* Enable GPIO interrupt as wake source */pm_wake_source_enable(PM_WAKE_SOURCE_GPIO);/* Check if any wake source is enabled */if (pm_wake_source_any_enabled()) {/* Safe to enter deep sleep */}
Wake latency is critical. The policy must account for:
For nRF52840 deep sleep: entry ~50us, exit ~10us. Minimum residency ~100us means you need >100us predicted idle to break even.
Devices can block system sleep via the runtime PM veto mechanism. When a device has pending DMA, ongoing flash write, or unprocessed interrupts, it calls:
/* In driver ISR or async completion */pm_device_busy_set(dev); /* Veto system sleep *//* Later, when safe */pm_device_busy_clear(dev); /* Allow sleep */
The policy checks pm_device_any_busy() before allowing deep sleep. This is why your SPI driver must call pm_device_busy_set() before starting a DMA transfer and pm_device_busy_clear() in the completion callback.
A typical BLE sensor node on nRF52840:
/* Main loop */void main(void){/* Initialize BLE, sensors, PM policy */ble_init();sensor_init();while (1) {/* Process BLE events - keeps CPU active during connection */ble_process_events();/* Read sensor periodically */if (k_uptime_get() - last_read > 10000) {sensor_read();last_read = k_uptime_get();}/* Idle - PM subsystem takes over */k_msleep(100); /* Or k_cpu_idle() in a dedicated idle thread */}}
During advertising intervals, the BLE controller handles RF, CPU sleeps in WFI. Between advertisements, policy sees ~900ms idle — deep sleep engaged. I2C sensor read takes ~2ms — device runtime PM suspends I2C controller after each read.
Enable PM debug tracing:
/* prj.conf */CONFIG_PM_DEBUG=yCONFIG_PM_DEVICE_RUNTIME_DEBUG=y
Log output shows state transitions:
[00:00:01.123] PM: Policy selected DEEP_SLEEP (idle=950ms)[00:00:01.124] PM: I2C_0: SUSPEND[00:00:01.125] PM: SPI_1: SUSPEND[00:00:01.125] PM: Entering DEEP_SLEEP[00:00:02.123] PM: Wake source: RTC_ALARM[00:00:02.124] PM: Exiting DEEP_SLEEP (slept=1000ms)[00:00:02.125] PM: SPI_1: RESUME[00:00:02.125] PM: I2C_0: RESUME
Common issues:
pm_device_runtime_put() after useZephyr’s PM subsystem is a three-layer stack: System PM (CPU states), Device Runtime PM (per-device states), and PM Policy (orchestration). The policy is the only component you typically customize — it decides when and how deep to sleep based on idle predictions, device constraints, and wake sources. Device drivers must implement proper pm_action callbacks and use runtime PM get/put pairs. For battery life, every microsecond counts — profile with CONFIG_PM_DEBUG and adjust residency thresholds to match your hardware’s wake latencies.
Quick Links
Legal Stuff





