blob: e068e7d8b07eaf4f8a1d0f96b4a595e4e8757500 [file] [log] [blame]
Device Power Management
#######################
Introduction
************
Device Power Management in Zephyr is a feature which presents mechanisms
to coherently affect the control of power management actions to be taken
by device drivers. This control is based on unambiguous expectations
which could be set by any component of the system, and on power-related
dependencies that devices may have on each other.
Zephyr supports two methods of device power management:
- :ref:`Device Runtime Power Management <pm-device-runtime-pm>`
- :ref:`System-Managed Device Power Management <pm-device-system-pm>`
.. _pm-device-runtime-pm:
Device Runtime Power Management
===============================
Device runtime power management involves coordinated interaction between
device drivers, subsystems, and applications. While device drivers
play a crucial role in directly controlling the power state of
devices, the decision to suspend or resume a device can also be
influenced by higher layers of the software stack.
Each layer—device drivers, subsystems, and applications—can operate
independently without needing to know about the specifics of the other
layers because the subsystem uses reference count to check when it needs
to suspend or resume a device.
- **Device drivers** are responsible for managing the
power state of devices. They interact directly with the hardware to
put devices into low-power states (suspend) when they are not in
use, and bring them back (resume) when needed. Drivers should use the
:ref:`device runtime power management APIs <device_runtime_apis>` provided
by Zephyr to control the power state of devices.
- **Subsystems**, such as sensors, file systems,
and network, can also influence device power management.
Subsystems may have better knowledge about the overall system
state and workload, allowing them to make informed decisions about
when to suspend or resume devices. For example, a networking
subsystem may decide to keep a network interface powered on if it
expects network activity in the near future.
- **Applications** running on Zephyr can impact device
power management as well. An application may have specific
requirements regarding device usage and power consumption. For
example, an application that streams data over a network may need
to keep the network interface powered on continuously.
Coordination between device drivers, subsystems, and applications is
key to efficient device power management. For example, a device driver
may not know that a subsystem will perform a series of sequential
operations that require a device to remain powered on. In such cases,
the subsystem can use device runtime power management to ensure that
the device remains in an active state until the operations are
complete.
When using this Device Runtime Power Management, the System Power
Management subsystem is able to change power states quickly because it
does not need to spend time suspending and resuming devices that are
runtime enabled.
For more information, see :ref:`pm-device-runtime`.
.. _pm-device-system-pm:
System-Managed Device Power Management
======================================
The system managed device power management (PM) framework is a method where
devices are suspended along with the system entering a CPU (or SoC) power state.
It can be enabled by setting :kconfig:option:`CONFIG_PM_DEVICE_SYSTEM_MANAGED`.
When using this method, device power management is mostly done inside
:c:func:`pm_system_suspend()`.
If a decision to enter a CPU lower power state is made, the power management
subsystem will check if the selected low power state triggers device power
management and then suspend devices before changing state. The subsystem takes
care of suspending devices following their initialization order, ensuring that
possible dependencies between them are satisfied. As soon as the CPU wakes up
from a sleep state, devices are resumed in the opposite order that they were
suspended.
The decision about suspending devices when entering a low power state is done based on the
state and if it has set the property ``zephyr,pm-device-disabled``. Here is
an example of a target with two low power states with only one triggering device power
management:
.. code-block:: devicetree
/* Node in a DTS file */
cpus {
power-states {
state0: state0 {
compatible = "zephyr,power-state";
power-state-name = "standby";
min-residency-us = <5000>;
exit-latency-us = <240>;
zephyr,pm-device-disabled;
};
state1: state1 {
compatible = "zephyr,power-state";
power-state-name = "suspend-to-ram";
min-residency-us = <8000>;
exit-latency-us = <360>;
};
};
};
.. note::
When using :ref:`pm-system`, device transitions can be run from the idle thread.
As functions in this context cannot block, transitions that intend to use blocking
APIs **must** check whether they can do so with :c:func:`k_can_yield`.
This method of device power management can be useful in the following scenarios:
- Systems with no device requiring any blocking operations when suspending and
resuming. This implementation is reasonably simpler than device runtime
power management.
- For devices that can not make any power management decision and have to be
always active. For example a firmware using Zephyr that is controlled by an
external entity (e.g Host CPU). In this scenario, some devices have to be
always active and should be suspended together with the SoC when requested by
this external entity.
It is important to emphasize that this method has drawbacks (see above) and
:ref:`Device Runtime Power Management <pm-device-runtime-pm>` is the
**preferred** method for implementing device power management.
.. note::
When using this method of device power management, the CPU will not
enter a low-power state if a device cannot be suspended. For example,
if a device returns an error such as ``-EBUSY`` in response to the
``PM_DEVICE_ACTION_SUSPEND`` action, indicating it is in the middle of
a transaction that cannot be interrupted. Another condition that
prevents the CPU from entering a low-power state is if the option
:kconfig:option:`CONFIG_PM_NEED_ALL_DEVICES_IDLE` is set and a device
is marked as busy.
.. note::
Devices are suspended only when the last active core is entering a low power
state and devices are resumed by the first core that becomes active.
Device Power Management States
******************************
The power management subsystem defines device states in
:c:enum:`pm_device_state`. This method is used to track power states of
a particular device. It is important to emphasize that, although the
state is tracked by the subsystem, it is the responsibility of each device driver
to handle device actions(:c:enum:`pm_device_action`) which change device state.
Device drivers implement the :c:func:`pm_device_action_cb_t` hook internally
which receives the :c:enum:`pm_device_action` for the device driver to handle.
If the :kconfig:option:`CONFIG_PM_DEVICE` option is selected, the device
drivers implementations of the hooks are exposed to the PM subsystem, enabling
runtime power management of the devices.
:c:enum:`pm_device_action` actions have direct and unambiguous relationships with
:c:enum:`pm_device_state` states:
.. graphviz::
:caption: Device actions x states
digraph {
node [shape=circle];
rankdir=LR;
subgraph {
SUSPENDED [label=PM_DEVICE_STATE_SUSPENDED];
SUSPENDING [label=PM_DEVICE_STATE_SUSPENDING];
ACTIVE [label=PM_DEVICE_STATE_ACTIVE];
OFF [label=PM_DEVICE_STATE_OFF];
ACTIVE -> SUSPENDING;
SUSPENDING -> ACTIVE;
SUSPENDING -> SUSPENDED ["label"="PM_DEVICE_ACTION_SUSPEND"];
ACTIVE -> SUSPENDED ["label"="PM_DEVICE_ACTION_SUSPEND"];
SUSPENDED -> ACTIVE ["label"="PM_DEVICE_ACTION_RESUME"];
{rank = same; SUSPENDED; SUSPENDING;}
OFF -> SUSPENDED ["label"="PM_DEVICE_ACTION_TURN_ON"];
SUSPENDED -> OFF ["label"="PM_DEVICE_ACTION_TURN_OFF"];
}
}
As mentioned above, device drivers do not directly change between these states.
This is entirely done by the power management subsystem. Instead, drivers are
responsible for implementing any hardware-specific tasks needed to handle state
changes.
Device Model with Device Power Management Support
*************************************************
Drivers initialize devices using macros. See :ref:`device_model_api` for
details on how these macros are used. A driver which implements device power
management support must provide these macros with arguments that describe its
power management implementation.
Use :c:macro:`PM_DEVICE_DEFINE` or :c:macro:`PM_DEVICE_DT_DEFINE` to define the
power management resources required by a driver. These macros allocate the
driver-specific context which is required by the power management subsystem.
Drivers can use :c:macro:`PM_DEVICE_GET` or
:c:macro:`PM_DEVICE_DT_GET` to get a pointer to this context. These
pointers should be passed to ``DEVICE_DEFINE`` or ``DEVICE_DT_DEFINE``
to initialize the power management field in each :c:struct:`device`.
The following example code shows how to implement device power management
support in a device driver. Note that return values are explicitly ignored
for brevity, in real drivers they must be handled.
.. code-block:: c
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#define DT_DRV_COMPAT dummy_device
struct dummy_driver_data {
struct gpio_callback int_pin_callback;
const struct device *dev;
};
struct dummy_driver_config {
const struct device *bus;
const struct gpio_dt_spec int_pin;
const struct gpio_dt_spec enable_pin;
};
static void dummy_driver_int_pin_handler(const struct device *dev,
struct gpio_callback *cb,
uint32_t pins)
{
struct dummy_driver_data *dev_data =
CONTAINER_OF(cb, struct dummy_driver_data, int_pin_callback);
const struct device *dev = dev_data->dev;
const struct dummy_driver_config *dev_config = dev->config;
/* ... */
}
static int dummy_driver_pm_suspend(const struct device *dev)
{
struct dummy_driver_data *dev_data = dev->data;
const struct dummy_driver_config *dev_config = dev->config;
/* Request devices needed by device */
(void)pm_device_runtime_get(config->enable_pin.port);
/* Disable and remove interrupt pin interrupt */
(void)gpio_pin_interrupt_configure_dt(&config->int_gpio, GPIO_INT_DISABLED);
(void)gpio_remove_callback(config->int_pin.port, &data->int_pin_callback);
/* Disable the device. In this case, we use the enable pin */
(void)gpio_pin_set_dt(&config->enable_pin, 0);
/* Release devices currenty not needed by device */
(void)pm_device_runtime_put(config->enable_pin.port);
(void)pm_device_runtime_put(config->int_pin.port);
/*
* Note that we now have suspended the device and released all the
* devices this device depends on. We are ready for the power
* domain being suspended, the device being resumed again, or the
* device driver being deinitialized.
*/
return 0;
}
static int dummy_driver_pm_resume(const struct device *dev)
{
struct dummy_driver_data *dev_data = dev->data;
const struct dummy_driver_config *dev_config = dev->config;
/* Request devices needed by device */
(void)pm_device_runtime_get(config->enable_pin.port);
(void)pm_device_runtime_get(config->int_pin.port);
(void)pm_device_runtime_get(config->bus);
/* Enable the device. In this case, we use the enable pin */
(void)gpio_pin_set_dt(&config->enable_pin, 1);
/*
* Write initial commands to device, in this case configuring
* the device's interrupt output pin using the bus
*/
/* ... */
/* Add and enable interrupt pin interrupt */
(void)gpio_add_callback(config->int_pin.port, &data->int_pin_callback);
(void)gpio_pin_interrupt_configure_dt(&config->int_gpio, GPIO_INT_EDGE_TO_ACTIVE);
/*
* Release devices currenty not needed by device. In this case, we
* are releasing the bus and the enable pin.
*
* The device driver would keep the bus ACTIVE while the device is
* ACTIVE in cases of high throughput or unsolicitet data on the
* bus, to avoid inefficient RESUME/SUSPEND cycles of the bus
* for every transaction, and allowing reception of unsolicitet
* data on buses like UART.
*/
(void)pm_device_runtime_put(config->bus);
(void)pm_device_runtime_put(config->enable_pin.port);
/*
* Note that the interrupt pin's port is kept resumed as it
* it needs to service the GPIO interrupt we enabled.
*/
return 0;
}
static int dummy_driver_pm_turn_off(const struct device *dev)
{
const struct dummy_driver_config *dev_config = dev->config;
/* Request devices needed for configuring device */
(void)pm_device_runtime_get(config->enable_pin.port);
/*
* We prepare the device for being powered off. In this case, we
* have an active low enable pin, which could back power the device
* once the power domain is suspended, so we configure it as
* disconnected if supported, input otherwise.
*/
if (gpio_pin_configure_dt(&config->enable_pin, GPIO_DISCONNECTED)) {
(void)gpio_pin_configure_dt(&config->enable_pin, GPIO_INPUT);
}
/* Release devices needed for configuring device */
(void)pm_device_runtime_put(config->enable_pin.port);
/*
* We have now prepared the device for being powered off and have
* released all the devices this device depends on. We assume that
* the enable pin will retain its configuration, even as we have
* released the enable pin's port.
*/
return 0;
}
static int dummy_driver_pm_turn_on(const struct device *dev)
{
const struct dummy_driver_config *dev_config = dev->config;
/* Request devices needed for configuring device */
(void)pm_device_runtime_get(config->enable_pin.port);
(void)pm_device_runtime_get(config->int_gpio.port);
/*
* We ensure the device is suspended, and if possible in its reset
* state. In this case we are using an enable pin, for other devices
* we may need to reset them by toggling a reset pin, using an SoC
* reset controller, or writing a reset command to them using their
* bus.
*/
(void)gpio_pin_configure_dt(&config->enable_pin, GPIO_OUTPUT_INACTIVE);
/* We configure pins for suspended */
(void)gpio_pin_configure_dt(&config->int_gpio, GPIO_INPUT);
/* Release devices needed for configuring device */
(void)pm_device_runtime_put(config->int_gpio.port);
(void)pm_device_runtime_put(config->enable_pin.port);
return 0;
}
static int dummy_driver_pm_action(const struct device *dev,
enum pm_device_action action)
{
int ret;
switch (action) {
case PM_DEVICE_ACTION_SUSPEND:
ret = dummy_driver_pm_suspend(dev);
break;
case PM_DEVICE_ACTION_RESUME:
ret = dummy_driver_pm_resume(dev);
break;
case PM_DEVICE_ACTION_TURN_OFF:
ret = dummy_driver_pm_turn_off(dev);
break;
case PM_DEVICE_ACTION_TURN_ON:
ret = dummy_driver_pm_turn_on(dev);
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
static int dummy_init(const struct device *dev)
{
struct dummy_driver_data *dev_data = dev->data;
const struct dummy_driver_config *dev_config = dev->config;
/*
* We must ensure all devices we depend on, excluding a potential
* power domain, are initialized.
*
* If CONFIG_PM_DEVICE=n, this also ensures the devices are ACTIVE.
*/
if (!device_is_ready(dev_config->bus) ||
!gpio_is_ready_dt(&dev_config->int_pin) ||
!gpio_is_ready_dt(&dev_config->enable_pin)) {
return -ENODEV;
}
/* We then initialize the device driver data structure */
gpio_init_callback(&dev_data->int_pin_callback,
dummy_driver_int_pin_handler,
BIT(dev_config->int_pin.pin));
dev_data->dev = dev;
/*
* This call must be the last call of the device init function.
* It will initialize the device's PM_DEVICE context and use the
* dummy_driver_pm_action callback to initialize the device into
* the appropriate state.
*/
return pm_device_driver_init(dev, dummy_driver_pm_action);
}
static int dummy_deinit(const struct device *dev)
{
int ret;
/*
* This call must be the first call of the device deinit function.
* It will use the dummy_driver_pm_action callback to move the
* device into, or verify the device is already in, an appropriate
* state for deinitialization, and deinitialize the device's
* PM_DEVICE context.
*/
ret = pm_device_driver_deinit(dev, dummy_driver_pm_action);
if (ret) {
return ret;
}
/*
* The device is now either SUSPENDED or OFF, all the devices this
* device depends on have been released, and devices with persistent
* configurations like GPIO pins have been configured to match the
* device state.
*
* The device will be left in this state until a new "owner" takes
* over.
*/
/*
* If we had allocated memory, DMA channels or other resources, we would
* release them here.
*/
return ret;
}
static struct dummy_driver_data data0;
static struct dummy_driver_config config0 = {
.bus = DEVICE_DT_GET(DT_INST_PARENT(0)),
.int_pin = GPIO_DT_SPEC_INST_GET(0, int_gpios),
.enable_pin = GPIO_DT_SPEC_INST_GET(0, enable_gpios),
};
/* Define the device's PM DEVICE context */
PM_DEVICE_DT_INST_DEFINE(0, dummy_driver_pm_action);
/* Define the device, pointing to the device's PM DEVICE context */
DEVICE_DT_INST_DEINIT_DEFINE(
0,
&dummy_init,
&dummy_deinit,
PM_DEVICE_DT_INST_GET(0),
&data0,
&config0,
POST_KERNEL,
CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,
NULL
);
Device Model with Partial Device Power Management Support
*********************************************************
If :kconfig:option:`CONFIG_PM_DEVICE` is not enabled, The device
power state is tied to the devices initialization state.
Once a device is initialized, the device driver PM action hook is
used to move the device to the ``ACTIVE`` state through calling
:c:func:`pm_device_driver_init`. Following the
``Device actions x states`` graph and the definition of the ``OFF``
state, this results in a call to ``PM_DEVICE_ACTION_TURN_ON``
followed by ``PM_DEVICE_ACTION_RESUME``.
Given power domains and buses are "just devices", every power
domain and bus will be resumed before its child devices as they
are initialized according to the devicetree dependency ordinals.
Every device is assumed to be powered, and the devices a device
depends on are assumed to be ``ACTIVE``, when device is initialized.
Once a device is deinitialized, the device driver PM action hook
is used to move the device to the ``SUSPENDED`` state through
calling :c:func:`pm_device_driver_deinit`. Following the
``Device actions x states``, and assuming power domains are "always
on" this results in a call to ``PM_DEVICE_ACTION_SUSPEND``.
.. _pm-device-shell:
Shell Commands
**************
Power management actions can be triggered from shell commands for testing
purposes. To do that, enable the :kconfig:option:`CONFIG_PM_DEVICE_SHELL`
option and issue a ``pm`` command on a device from the shell, for example:
.. code-block:: console
uart:~$ device list
- buttons (active)
uart:~$ pm suspend buttons
uart:~$ device list
devices:
- buttons (suspended)
To print the power management state of a device, enable
:kconfig:option:`CONFIG_DEVICE_SHELL` and use the ``device list`` command, for
example:
.. code-block:: console
uart:~$ device list
devices:
- i2c@40003000 (active)
- buttons (active, usage=1)
- leds (READY)
In this case, ``leds`` does not support PM, ``i2c`` supports PM with manual
suspend and resume actions and it's currently active, ``buttons`` supports
runtime PM and it's currently active with one user.
.. _pm-device-busy:
Busy Status Indication
**********************
When the system is idle and the SoC is going to sleep, the power management
subsystem can suspend devices, as described in :ref:`pm-device-system-pm`. This
can cause device hardware to lose some states. Suspending a device which is in
the middle of a hardware transaction, such as writing to a flash memory, may
lead to undefined behavior or inconsistent states. This API guards such
transactions by indicating to the kernel that the device is in the middle of an
operation and should not be suspended.
When :c:func:`pm_device_busy_set` is called, the device is marked as busy and
the system will not do power management on it. After the device is no
longer doing an operation and can be suspended, it should call
:c:func:`pm_device_busy_clear`.
.. _pm-device-constraint:
Device Power Management X System Power Management
*************************************************
When managing power in embedded systems, it's crucial to understand
the interplay between device power state and the overall system power
state. Some devices may have dependencies on the system power
state. For example, certain low-power states of the SoC might not
supply power to peripheral devices, leading to problems if the device
is in the middle of an operation. Proper coordination is essential to
maintain system stability and data integrity.
To avoid this sort of problem, devices must :ref:`get and release lock <pm-policy-power-states>`
power states that cause power loss during an operation.
Zephyr provides a mechanism for devices to declare which power states cause power
loss and an API that automatically get and put lock on them. This feature is
enabled setting :kconfig:option:`CONFIG_PM_POLICY_DEVICE_CONSTRAINTS` to ``y``.
Once this feature is enabled, devices must declare in devicetree which
states cause power loss. In the following example, device ``test_dev``
says that power states ``state1`` and ``state2`` cause power loss.
.. code-block:: devicetree
power-states {
state0: state0 {
compatible = "zephyr,power-state";
power-state-name = "suspend-to-idle";
min-residency-us = <10000>;
exit-latency-us = <100>;
};
state1: state1 {
compatible = "zephyr,power-state";
power-state-name = "standby";
min-residency-us = <20000>;
exit-latency-us = <200>;
};
state2: state2 {
compatible = "zephyr,power-state";
power-state-name = "suspend-to-ram";
min-residency-us = <50000>;
exit-latency-us = <500>;
};
state3: state3 {
compatible = "zephyr,power-state";
power-state-name = "suspend-to-ram";
status = "disabled";
};
};
test_dev: test_dev {
compatible = "test-device-pm";
status = "okay";
zephyr,disabling-power-states = <&state1 &state2>;
};
After that devices can lock these state calling
:c:func:`pm_policy_device_power_lock_get` and release with
:c:func:`pm_policy_device_power_lock_put`. For example:
.. code-block:: C
static void timer_expire_cb(struct k_timer *timer)
{
struct test_driver_data *data = k_timer_user_data_get(timer);
data->ongoing = false;
k_timer_stop(timer);
pm_policy_device_power_lock_put(data->self);
}
void test_driver_async_operation(const struct device *dev)
{
struct test_driver_data *data = dev->data;
data->ongoing = true;
pm_policy_device_power_lock_get(dev);
/** Lets set a timer big enough to ensure that any deep
* sleep state would be suitable but constraints will
* make only state0 (suspend-to-idle) will be used.
*/
k_timer_start(&data->timer, K_MSEC(500), K_NO_WAIT);
}
Wakeup capability
*****************
Some devices are capable of waking the system up from a sleep state.
When a device has such capability, applications can enable or disable
this feature on a device dynamically using
:c:func:`pm_device_wakeup_enable`.
This property can be set on device declaring the property ``wakeup-source`` in
the device node in devicetree. For example, this devicetree fragment sets the
``gpio0`` device as a "wakeup" source:
.. code-block:: devicetree
gpio0: gpio@40022000 {
compatible = "ti,cc13xx-cc26xx-gpio";
reg = <0x40022000 0x400>;
interrupts = <0 0>;
status = "disabled";
label = "GPIO_0";
gpio-controller;
wakeup-source;
#gpio-cells = <2>;
};
By default, "wakeup" capable devices do not have this functionality enabled
during the device initialization. Applications can enable this functionality
later calling :c:func:`pm_device_wakeup_enable`.
.. note::
This property is **only** used by the system power management to identify
devices that should not be suspended.
It is responsibility of driver or the application to do any additional
configuration required by the device to support it.
Examples
********
Some helpful examples showing device power management features:
* :zephyr_file:`samples/subsys/pm/device_pm/`
* :zephyr_file:`tests/subsys/pm/power_mgmt/`
* :zephyr_file:`tests/subsys/pm/device_wakeup_api/`
* :zephyr_file:`tests/subsys/pm/device_driver_init/`