Commit 4a827fd6 authored by Bjarki Arge Andreasen's avatar Bjarki Arge Andreasen Committed by Chris Friedt
Browse files

doc: services: pm: update pm device init/deinit/states



- Specify "Device" Power Management Support in sections covering
  CONFIG_PM_DEVICE=y or n
- Extend the enum pm_device_state docstrings to define and detail
  expectations of devices in each enum pm_device_state state.
- Extend pm device driver code example with more concise comments
  and include pm_device_driver_init, pm_device_driver_deinit, and
  DEVICE_DT_INST_DEINIT_DEFINE() (device deinit feature)
- Describe the device driver PM states used if PM_DEVICE is not
  enabled.
- Note which states a device driver is initialized from and can
  be deinitialized in and why.

Signed-off-by: default avatarBjarki Arge Andreasen <bjarki.andreasen@nordicsemi.no>
parent f3af3627
Loading
Loading
Loading
Loading
+319 −50
Original line number Diff line number Diff line
@@ -4,11 +4,11 @@ Device Power Management
Introduction
************

Device power management (PM) on Zephyr is a feature that enables devices to
save energy when they are not being used. This feature can be enabled by
setting :kconfig:option:`CONFIG_PM_DEVICE` to ``y``. When this option is
selected, device drivers implementing power management will be able to take
advantage of the device power management subsystem.
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:

@@ -158,8 +158,14 @@ 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.

Each :c:enum:`pm_device_action` have a direct an unambiguous relationship with
a :c:enum:`pm_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
@@ -174,8 +180,10 @@ a :c:enum:`pm_device_state`.
            ACTIVE [label=PM_DEVICE_STATE_ACTIVE];
            OFF [label=PM_DEVICE_STATE_OFF];

            ACTIVE -> SUSPENDING;
            SUSPENDING -> ACTIVE;
            SUSPENDING -> SUSPENDED ["label"="PM_DEVICE_ACTION_SUSPEND"];

            ACTIVE -> SUSPENDING -> SUSPENDED;
            ACTIVE -> SUSPENDED ["label"="PM_DEVICE_ACTION_SUSPEND"];
            SUSPENDED -> ACTIVE ["label"="PM_DEVICE_ACTION_RESUME"];

@@ -191,8 +199,8 @@ 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 Power Management Support
******************************************
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
@@ -201,58 +209,319 @@ 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 state which is required by the power management subsystem.
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 state. These
: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`.

Here is some example code showing how to implement device power management
support in a device driver.
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 busses 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:
            /* suspend the device */
            ...
                   ret = dummy_driver_pm_suspend(dev);
                   break;
           case PM_DEVICE_ACTION_RESUME:
            /* resume the device */
            ...
                   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;

           /*
             * powered on the device, used when the power
             * domain this device belongs is resumed.
            * 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.
            */
            ...
            break;
        case PM_DEVICE_ACTION_TURN_OFF:
           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;

          /*
             * power off the device, used when the power
             * domain this device belongs is suspended.
           * 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.
           */
            ...
            break;
        default:
            return -ENOTSUP;
          return pm_device_driver_init(dev, dummy_driver_pm_action);
   }

        return 0;
   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);

    DEVICE_DT_INST_DEFINE(0, &dummy_init,
        PM_DEVICE_DT_INST_GET(0), NULL, NULL, POST_KERNEL,
        CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, NULL);
   /* 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 busses 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:

+62 −8
Original line number Diff line number Diff line
@@ -66,22 +66,76 @@ enum pm_device_flag {

/** @brief Device power states. */
enum pm_device_state {
	/** Device is in active or regular state. */
	/**
	 * @brief Device hardware is powered, and the device is needed by the system.
	 *
	 * @details The device should be enabled in this state. Any device driver API
	 * may be called in this state.
	 */
	PM_DEVICE_STATE_ACTIVE,
	/**
	 * Device is suspended.
	 * @brief Device hardware is powered, but the device is not needed by the
	 * system.
	 *
	 * @note
	 *     Device context may be lost.
	 * @details The device should be put into its lowest internal power state,
	 * commonly named "disabled" or "stopped".
	 *
	 * If a device has been specified as this device's power domain, and said
	 * device is no longer needed by the system, this device will be
	 * transitioned into the @ref PM_DEVICE_STATE_OFF state, followed by the
	 * power domain device being transitioned to the
	 * @ref PM_DEVICE_STATE_SUSPENDED state.
	 *
	 * A device driver may be deinitialized in this state. Once the device
	 * driver has been deinitialized, we implicitly move to the
	 * @ref PM_DEVICE_STATE_OFF state as the device hardware may lose power,
	 * with no device driver to respond to the corresponding
	 * @ref PM_DEVICE_ACTION_TURN_OFF action.
	 *
	 * @note This state is NOT a "low-power"/"partially operable" state,
	 * those are configured using device driver specific APIs, and apply only
	 * while the device is in the @ref PM_DEVICE_STATE_ACTIVE state.
	 */
	PM_DEVICE_STATE_SUSPENDED,
	/** Device is being suspended. */
	PM_DEVICE_STATE_SUSPENDING,
	/**
	 * Device is turned off (power removed).
	 * @brief Device hardware is powered, but the device has been scheduled to
	 * be suspended, as it is no longer needed by the system.
	 *
	 * @note
	 *     Device context is lost.
	 * @details This state is used when delegating suspension of a device to
	 * the PM subsystem, optionally with residency to avoid unnecessary
	 * suspend/resume cycles, resulting from a call to
	 * @ref pm_device_runtime_put_async. The device will be unscheduled in case
	 * the device becomes needed by the system.
	 *
	 * No device driver API calls must occur in this state.
	 *
	 * @note that this state is opaque to the device driver (no
	 * @ref pm_device_action is called as this state is entered) and is used
	 * solely by PM_DEVICE_RUNTIME.
	 */
	PM_DEVICE_STATE_SUSPENDING,
	/**
	 * @brief Device hardware is not powered. This is the initial state from
	 * which a device driver is initialized.
	 *
	 * @details When a device driver is initialized, we do not know the state
	 * of the device. As a result, the @ref PM_DEVICE_ACTION_TURN_ON action
	 * should be able to transition the device from any internal state into
	 * @ref PM_DEVICE_STATE_SUSPENDED, since no guarantees can be made across
	 * resets. This is typically achieved through toggling a reset pin or
	 * triggering a software reset through a register write before performing
	 * any additional configuration needed to meet the requirements of
	 * @ref PM_DEVICE_STATE_SUSPENDED. For devices where this is not possible,
	 * the device driver must presume the device is in either the
	 * @ref PM_DEVICE_STATE_OFF or @ref PM_DEVICE_STATE_SUSPENDED state at
	 * time of initialization, as these are the states within which device
	 * drivers may be deinitialized.
	 *
	 * If a device has been specified as this device's power domain, and said
	 * device becomes needed by the system, the power domain device will be
	 * transitioned into the @ref PM_DEVICE_STATE_ACTIVE state, followed by this
	 * device being transitioned to the
	 * @ref PM_DEVICE_STATE_SUSPENDED state.
	 */
	PM_DEVICE_STATE_OFF
};