zephyr-power-management-bajo-consumo

Power Management in Zephyr and Low Power Modes

  • 4 min

The Power Management in Zephyr is the set of mechanisms that coordinates the power states of the CPU and devices when the system has no immediate work to do.

If you leave your “Blinky” running as we have programmed it so far, the battery will likely drain in a couple of days. Why? Because the microcontroller, even though it’s not doing “anything”, is still powered on, consuming current.

For this, we use Zephyr’s Power Management (PM) subsystem, designed to coordinate different low-power levels.

The secret: tickless idle

In an older RTOS (like old versions of FreeRTOS), there is a system “Tick”. Every millisecond, an interrupt wakes up the CPU to check if a thread switch is needed. This prevents the processor from entering deep sleep because we are constantly “bothering” it.

Zephyr uses a Tickless Kernel architecture.

  1. When you call k_msleep(1000), the Kernel knows it has nothing to do for the next 1000ms.
  2. Instead of waking up every 1ms to check the time, it programs a hardware timer to fire exactly 1000ms from now.
  3. Immediately afterward, it powers down the CPU and goes to sleep.

The kernel can leave the CPU in idle when threads are blocked. Entering deeper states requires platform support, a power policy, and the corresponding configuration; it does not depend solely on calling k_sleep.

Power states (PM states)

It’s not the same as taking a 5-minute nap as it is sleeping for 8 hours. Microcontrollers have different sleep levels:

  1. PM_STATE_ACTIVE: The system is active.
  2. PM_STATE_RUNTIME_IDLE: Light inactivity state, if the platform implements it.
  3. PM_STATE_SUSPEND_TO_IDLE and PM_STATE_STANDBY: They preserve different context and offer different latencies depending on the SoC.
  4. PM_STATE_SUSPEND_TO_RAM and PM_STATE_SOFT_OFF: Deeper states, with wake sources and retention dependent on hardware.

Zephyr abstracts this into PM_STATES. When the system detects that the “Idle Thread” (the thread that runs when no one else is working) is active, it queries the Power Manager for which sleep state to choose based on how much free time we have.

  • If we have 100µs free -> Idle.
  • If we have 1000ms free -> Suspend.

Basic configuration

To activate this machinery, we need to modify our prj.conf.

# Enable the power management subsystem
CONFIG_PM=y

# Enable device power management
CONFIG_PM_DEVICE=y
Copied!

Just with CONFIG_PM=y, the system will already attempt to enter low-power states when all threads are sleeping.

Device power management: the peripherals

Here is the catch. You can have the CPU asleep and still drain the battery if you leave a GNSS receiver or a Wi-Fi module powered on.

Zephyr has a centralized system to tell the peripherals: “Hey, I’m going to sleep, you power down too.”

When the system enters suspend, Zephyr iterates through the list of active devices and calls their PM function.

Runtime power management

With CONFIG_PM_DEVICE_RUNTIME=y, compatible drivers can request the device when needed and release it afterward. The usage counter decides when to resume or suspend it.

It can also be automatically enabled for a compatible node:

&sensor0 {
    zephyr,pm-device-runtime-auto;
};
Copied!

The calls pm_device_runtime_get() and pm_device_runtime_put() normally belong to the driver that knows the operation cycle, not the application. If a driver does not implement PM, enabling the options will not power down the peripheral by itself.

For this to work, the device driver must support Power Management. Most native Zephyr drivers (especially those from Nordic and NXP) support it very well.

The debugger problem

Some deep states stop the clocks or domains needed by the debug interface. The SWD/JTAG connection can be lost, although the behavior depends on the SoC, the probe, and the selected state.

It is very frustrating. You try to debug why your code fails, but as soon as it reaches k_sleep, the IDE tells you “Target disconnected”.

For development, we often have to disable PM temporarily or instruct it not to use the deepest states.

In prj.conf for debugging:

CONFIG_PM=n  # Disable PM while developing the logic
Copied!

Or in code, we can temporarily lock a specific state. If your board offers several deep states, you will need to acquire a lock for each one you want to prevent:

#include <zephyr/pm/policy.h>

int main(void) {
    /* Block SUSPEND_TO_IDLE while debugging */
    pm_policy_state_lock_get(PM_STATE_SUSPEND_TO_IDLE, PM_ALL_SUBSTATES);
    
    /* ... code to debug ... */

    /* When finished, allow this state again */
    pm_policy_state_lock_put(PM_STATE_SUSPEND_TO_IDLE, PM_ALL_SUBSTATES);
    return 0;
}
Copied!