HomeAbout UsContact Us

Zephyr PM Subsystem: Deep Sleep, Device Runtime PM, and Policy-Driven Power Control

By Jithin Tom
Published in Embedded OS
August 16, 2026
3 min read
Zephyr PM Subsystem: Deep Sleep, Device Runtime PM, and Policy-Driven Power Control

Table Of Contents

01
System Power Management: CPU Idle States
02
Device Runtime PM: Per-Device Power Control
03
The PM Policy: Orchestrating System Sleep
04
Wake Sources and Latency
05
Device Veto: Preventing Unsafe Sleep
06
Practical Example: BLE Sensor Node
07
Debugging PM Issues
08
Summary
09
Related Reading
10
References
11
Frequently Asked Questions

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.

System Power Management: CPU Idle States

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:

  1. Residency requirement: Predicted idle time > (target state wake latency + entry latency)
  2. Device constraints: No device has vetoed the transition via runtime PM
  3. Wake source availability: At least one enabled wake source can exit the state

Device Runtime PM: Per-Device Power Control

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: Orchestrating System Sleep

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 and Latency

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:

  • Entry latency: Cycles to gate clocks, save context, enter sleep
  • Exit latency: Cycles to restore clocks, context, resume execution
  • Total residency = entry + exit + minimum time in state

For nRF52840 deep sleep: entry ~50us, exit ~10us. Minimum residency ~100us means you need >100us predicted idle to break even.

Device Veto: Preventing Unsafe Sleep

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.

Practical Example: BLE Sensor Node

A typical BLE sensor node on nRF52840:

  • Advertises every 1s (connection interval)
  • Reads sensor via I2C every 10s
  • Sleeps between events
/* 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.

Debugging PM Issues

Enable PM debug tracing:

/* prj.conf */
CONFIG_PM_DEBUG=y
CONFIG_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:

  • Device never suspends: Missing pm_device_runtime_put() after use
  • System never deep sleeps: Device veto not cleared, or no wake source enabled
  • Wake latency too high: Policy residency threshold too aggressive

Summary

Zephyr’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.

References

  1. Zephyr Project, “Power Management”, https://docs.zephyrproject.org/latest/services/pm/index.html (accessed 2026-08-16)
  2. Zephyr Project, “Device Runtime Power Management”, https://docs.zephyrproject.org/latest/services/pm/device_runtime.html (accessed 2026-08-16)
  3. Nordic Semiconductor, “nRF52840 Product Specification v1.6”, https://docs.nordicsemi.com/bundle/nRF52840_PS_v1.6/resource/nRF52840_PS_v1.6.pdf (accessed 2026-08-16)
  4. STMicroelectronics, “STM32L4 Series Reference Manual RM0351”, https://www.st.com/resource/en/reference_manual/rm0351-stm32l47xxx-stm32l48xxx-stm32l49xxx-and-stm32l4axxx-advanced-armbased-32bit-mcus-stmicroelectronics.pdf (accessed 2026-08-16)
  5. Liu, C. L., & Layland, J. W., “Scheduling Algorithms for Multiprogramming in a Hard-Real-Time Environment”, Journal of the ACM, 20(1), 46-61, 1973.
  6. Ganssle, J., “The Art of Designing Embedded Systems”, 2nd Ed., Newnes, 2008, Ch. 10: Power Management.

Frequently Asked Questions

What are the two main power management domains in Zephyr?

Zephyr separates power management into System Power Management (PM) for CPU/idle states like deep sleep, and Device Runtime PM for per-device power state transitions (Active, Suspend, Off). The PM subsystem coordinates both via policies.

How does the PM policy decide when to enter deep sleep?

The PM policy evaluates idle duration against a minimum residency threshold. If the predicted idle time exceeds the threshold plus wake latency, and no device vetoes the transition via runtime PM constraints, the policy requests the CPU to enter the deepest available sleep state.

What is the role of device runtime PM in Zephyr?

Device Runtime PM allows drivers to advertise supported power states and transition between them based on usage. Devices can block system sleep (via veto) if they have pending operations, or request specific states like PM_DEVICE_STATE_SUSPEND when idle.

How do you implement a custom PM policy in Zephyr?

Implement the `pm_policy` API: define a `pm_policy_init` function, register it with `SYS_INIT`, and implement `pm_policy_next_state` to return the target CPU power state based on idle ticks, wake sources, and device constraints.

What is the difference between PM_DEVICE_STATE_SUSPEND and PM_DEVICE_STATE_OFF?

SUSPEND retains device context (registers, RAM) for fast resume but draws some leakage current. OFF powers down the device completely, losing context — the driver must fully reinitialize on resume. SUSPEND is for short idle periods; OFF for long durations.

Tags

zephyrpower-managementpmdeep-sleepdevice-runtime-pmpolicy

Share


Previous Article
Optimizing Cortex-M Tail-Chaining for Sub-Microsecond Latency
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Getting Started with Zephyr RTOS: A Practical Guide for Embedded Engineers
Getting Started with Zephyr RTOS: A Practical Guide for Embedded Engineers
July 05, 2026
2 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media