
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 |+--------------------------------------------------------------+| PM_STATE_ACTIVE || | || | pm_system_suspend() || v || PM_STATE_SUSPEND_TO_IDLE <-- Light sleep, instant wake || | || | Deeper sleep requested || v || PM_STATE_STANDBY <-- SRAM retention, ~10-50us wake || | || | Deeper sleep requested || v || PM_STATE_SOFT_OFF <-- Full power loss, cold boot |+--------------------------------------------------------------+
Zephyr defines the standard set of system power states in zephyr/pm/state.h:
/* zephyr/pm/state.h - snippet of pm_state enum */enum pm_state {PM_STATE_ACTIVE,PM_STATE_RUNTIME_IDLE,PM_STATE_SUSPEND_TO_IDLE, /* WFI - 1-2 cycle wake */PM_STATE_STANDBY, /* System ON idle - ~10us wake, RAM retained */PM_STATE_SUSPEND_TO_RAM,PM_STATE_SUSPEND_TO_DISK,PM_STATE_SOFT_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 register their PM action callback via the PM_DEVICE_DT_DEFINE macro:
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 residency-based policy but you can replace it by enabling CONFIG_PM_POLICY_CUSTOM and implementing pm_policy_next_state().
The application-provided function must match this signature:
/* Returns information about the selected power state */struct pm_state_info pm_policy_next_state(uint8_t cpu, int32_t 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_ms_to_ticks_ceil32(10) /* 10ms minimum residency */struct pm_state_info pm_policy_next_state(uint8_t cpu, int32_t ticks){struct pm_state_info state = { .state = PM_STATE_ACTIVE };/* Check if any device vetoes system sleep */if (pm_device_is_any_busy()) {state.state = PM_STATE_SUSPEND_TO_IDLE;return state;}/* Residency check */if (ticks >= MIN_DEEP_SLEEP_TICKS) {state.state = PM_STATE_STANDBY;return state;}state.state = PM_STATE_SUSPEND_TO_IDLE;return state;}
By providing this function, the PM subsystem will automatically invoke your policy logic during the idle thread loop.
Wake sources are hardware interrupts that can exit a sleep state. Each SoC defines which peripherals can wake from which states. In Zephyr, devices with the wakeup-source devicetree property can be enabled as wake sources via the pm_device_wakeup_enable() API:
/* Enable device as a system wake-up source */pm_device_wakeup_enable(rtc_dev, true);/* Enable GPIO button as wake source */pm_device_wakeup_enable(gpio_dev, true);/* The system PM subsystem checks wake-capable devicesbefore entering deep sleep */if (pm_device_wakeup_is_enabled(rtc_dev)) {/* 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 system checks pm_device_is_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 */int 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 STANDBY (idle=950ms)[00:00:01.124] PM: I2C_0: SUSPEND[00:00:01.125] PM: SPI_1: SUSPEND[00:00:01.125] PM: Entering STANDBY[00:00:02.123] PM: Wake source: RTC_ALARM[00:00:02.124] PM: Exiting STANDBY (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





