diff --git a/doc/services/pm/device.rst b/doc/services/pm/device.rst index 379ebfbef74f..7d31c95dde6f 100644 --- a/doc/services/pm/device.rst +++ b/doc/services/pm/device.rst @@ -4,11 +4,10 @@ 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 (DEVICE PM) in Zephyr is a feature which makes +device driver's PM operations available outside of the device driver, +enabling power management of the device. This feature is enabled by +selecting :kconfig:option:`CONFIG_PM_DEVICE`. Zephyr supports two methods of device power management: @@ -158,8 +157,54 @@ 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`. +Each :c:enum:`pm_device_state` state is defined as follows: + +- ``OFF``: Device hardware is not powered. This is the initial state from which + a device driver is initialized. + + When a device driver is initialized, we do not know the state of the device. + As a result, the :c:enum:`pm_device_action` ``TURN_ON`` action should be able + to transition the device from any internal state into ``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 ``SUSPENDED``. For devices where this is not possible, the device driver + must presume the device is in either the ``OFF`` or ``SUSPENDED`` state at + time of initialization, as these are the states within which device drivers + may be deinitialized. + +- ``SUSPENDED``: Device hardware is powered, but the device is not needed by the + system. The device should be put into its lowest internal power state, + commonly named "disabled" or "stopped". The device will be powered off if its + power domain is no longer needed by the system. If the device itself is a + power domain, devices on this power domain will be powered off in this state. + + Note that 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 ``ACTIVE``. + + A device driver may be deinitialized in this state. Once the device driver + has been deinitialized, we implicitly move to the ``OFF`` state as the device + hardware may lose power, with no device driver to respond to the + corresponding :c:enum:`pm_device_action` ``TURN_OFF`` action. + +- ``ACTIVE``: Device hardware is powered, and the device is needed by the + system. The device should be enabled in this state. Any device driver API may + be called in this state. + +- ``SUSPENDING``: Device hardware is powered, but the device has been scheduled + to be suspended, as it is no longer needed by the system. This state is used + when delegating suspension of a device to the PM subsystem, optionally with + residency to avoid unnecessary suspend/resume cycles. 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 + :c:enum:`pm_device_action` is called as this state is entered) + +: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 +219,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"]; @@ -202,58 +249,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 - #define DT_DRV_COMPAT dummy_device + #include + #include - static int dummy_driver_pm_action(const struct device *dev, - enum pm_device_action action) - { - switch (action) { - case PM_DEVICE_ACTION_SUSPEND: - /* suspend the device */ - ... - break; - case PM_DEVICE_ACTION_RESUME: - /* resume the device */ - ... - break; - case PM_DEVICE_ACTION_TURN_ON: - /* - * powered on the device, used when the power - * domain this device belongs is resumed. - */ - ... - break; - case PM_DEVICE_ACTION_TURN_OFF: - /* - * power off the device, used when the power - * domain this device belongs is suspended. - */ - ... - break; - default: - return -ENOTSUP; - } + #define DT_DRV_COMPAT dummy_device - return 0; - } + 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 + */ - PM_DEVICE_DT_INST_DEFINE(0, dummy_driver_pm_action); + /* ... */ + + /* 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: + 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), + }; - 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'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 without 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: diff --git a/include/zephyr/pm/device.h b/include/zephyr/pm/device.h index f7c008f3cf2c..4ffe47346b44 100644 --- a/include/zephyr/pm/device.h +++ b/include/zephyr/pm/device.h @@ -628,6 +628,9 @@ bool pm_device_is_powered(const struct device *dev); * @ref PM_DEVICE_STATE_OFF, or @ref PM_DEVICE_STATE_SUSPENDED if device can * never be powered off. * + * @note If CONFIG_PM_DEVICE=n, PM_DEVICE_ACTION_TURN_ON followed by + * PM_DEVICE_ACTION_RESUME will be invoked on the device by this function. + * * @param dev Device instance. * @param action_cb Device PM control callback function. * @retval 0 On success. @@ -635,6 +638,23 @@ bool pm_device_is_powered(const struct device *dev); */ int pm_device_driver_init(const struct device *dev, pm_device_action_cb_t action_cb); +/** + * @brief Prepare PM device for device driver deinit + * + * @details Ensures device is either SUSPENDED or OFF, and will deinit the PM device + * context if this is the case. Must be called at the beginning of a driver's deinit + * function. + * + * @note If CONFIG_PM_DEVICE=n, PM_DEVICE_ACTION_SUSPEND will be invoked on the + * device by this function. + * + * @param dev Device instance. + * @param action_cb Device PM control callback function. + * @retval 0 if success. + * @retval -EBUSY Device is not SUSPENDED nor OFF + * @retval -errno code if failure. + */ +int pm_device_driver_deinit(const struct device *dev, pm_device_action_cb_t action_cb); #else static inline int pm_device_state_get(const struct device *dev, enum pm_device_state *state) @@ -731,6 +751,11 @@ static inline int pm_device_driver_init(const struct device *dev, pm_device_acti return 0; } +static inline int pm_device_driver_deinit(const struct device *dev, pm_device_action_cb_t action_cb) +{ + return action_cb(dev, PM_DEVICE_ACTION_SUSPEND); +} + #endif /* CONFIG_PM_DEVICE */ /** @} */ diff --git a/subsys/pm/device.c b/subsys/pm/device.c index a1a7e92ee925..2d7bb3176c36 100644 --- a/subsys/pm/device.c +++ b/subsys/pm/device.c @@ -401,3 +401,14 @@ int pm_device_driver_init(const struct device *dev, /* Return the PM_DEVICE_ACTION_RESUME result */ return rc; } + +int pm_device_driver_deinit(const struct device *dev, + pm_device_action_cb_t action_cb) +{ + struct pm_device_base *pm = dev->pm_base; + + return pm->state == PM_DEVICE_STATE_SUSPENDED || + pm->state == PM_DEVICE_STATE_OFF ? + 0 : + -EBUSY; +} diff --git a/tests/subsys/pm/device_driver_init/src/main.c b/tests/subsys/pm/device_driver_init/src/main.c index d38d6ed1429b..15253633078a 100644 --- a/tests/subsys/pm/device_driver_init/src/main.c +++ b/tests/subsys/pm/device_driver_init/src/main.c @@ -73,6 +73,8 @@ ZTEST(device_driver_init, test_device_driver_init) struct pm_transition_test_dev_data { enum pm_device_state state_turn_on; enum pm_device_state state_resume; + enum pm_device_state state_suspend; + enum pm_device_state state_turn_off; bool state_other; }; @@ -88,6 +90,12 @@ static int pm_transition_test_dev_pm_action(const struct device *dev, enum pm_de case PM_DEVICE_ACTION_RESUME: pm_device_state_get(dev, &data->state_resume); break; + case PM_DEVICE_ACTION_SUSPEND: + pm_device_state_get(dev, &data->state_suspend); + break; + case PM_DEVICE_ACTION_TURN_OFF: + pm_device_state_get(dev, &data->state_turn_off); + break; default: data->state_other = true; } @@ -100,14 +108,30 @@ static int pm_transition_test_dev_init(const struct device *dev) data->state_turn_on = UINT8_MAX; data->state_resume = UINT8_MAX; + data->state_suspend = UINT8_MAX; + data->state_turn_off = UINT8_MAX; data->state_other = false; return pm_device_driver_init(dev, pm_transition_test_dev_pm_action); } +static int pm_transition_test_dev_deinit(const struct device *dev) +{ + struct pm_transition_test_dev_data *data = dev->data; + + data->state_turn_on = UINT8_MAX; + data->state_resume = UINT8_MAX; + data->state_suspend = UINT8_MAX; + data->state_turn_off = UINT8_MAX; + data->state_other = false; + + return pm_device_driver_deinit(dev, pm_transition_test_dev_pm_action); +} + static struct pm_transition_test_dev_data dev_data; PM_DEVICE_DEFINE(pm_transition_test_dev_pm, pm_transition_test_dev_pm_action); -DEVICE_DEFINE(pm_transition_test_dev, "test_dev", pm_transition_test_dev_init, +DEVICE_DEINIT_DEFINE(pm_transition_test_dev, "test_dev", + pm_transition_test_dev_init, pm_transition_test_dev_deinit, PM_DEVICE_GET(pm_transition_test_dev_pm), &dev_data, NULL, POST_KERNEL, 0, NULL); @@ -116,11 +140,40 @@ ZTEST(device_driver_init, test_device_driver_init_pm_state) #ifdef CONFIG_PM_DEVICE zassert_equal(PM_DEVICE_STATE_OFF, dev_data.state_turn_on); zassert_equal(PM_DEVICE_STATE_SUSPENDED, dev_data.state_resume); + zassert_equal(UINT8_MAX, dev_data.state_suspend); + zassert_equal(UINT8_MAX, dev_data.state_turn_off); zassert_false(dev_data.state_other); #else /* pm_device_state_get always returns PM_DEVICE_STATE_ACTIVE */ zassert_equal(PM_DEVICE_STATE_ACTIVE, dev_data.state_turn_on); zassert_equal(PM_DEVICE_STATE_ACTIVE, dev_data.state_resume); + zassert_equal(UINT8_MAX, dev_data.state_suspend); + zassert_equal(UINT8_MAX, dev_data.state_turn_off); + zassert_false(dev_data.state_other); +#endif /* CONFIG_PM */ + +#ifdef CONFIG_PM_DEVICE + /* device_deinit() blocked if device is not suspended or off */ + zassert_not_ok(device_deinit(DEVICE_GET(pm_transition_test_dev))); + zassert_ok(pm_device_action_run(DEVICE_GET(pm_transition_test_dev), + PM_DEVICE_ACTION_SUSPEND)); +#endif + + zassert_ok(device_deinit(DEVICE_GET(pm_transition_test_dev))); + +#ifdef CONFIG_PM_DEVICE + /* no action called as device is already suspended or off */ + zassert_equal(UINT8_MAX, dev_data.state_turn_on); + zassert_equal(UINT8_MAX, dev_data.state_resume); + zassert_equal(UINT8_MAX, dev_data.state_suspend); + zassert_equal(UINT8_MAX, dev_data.state_turn_off); + zassert_false(dev_data.state_other); +#else + /* pm_device_state_get always returns PM_DEVICE_STATE_ACTIVE */ + zassert_equal(UINT8_MAX, dev_data.state_turn_on); + zassert_equal(UINT8_MAX, dev_data.state_resume); + zassert_equal(PM_DEVICE_STATE_ACTIVE, dev_data.state_suspend); + zassert_equal(UINT8_MAX, dev_data.state_turn_off); zassert_false(dev_data.state_other); #endif /* CONFIG_PM */ }