|  | /* | 
|  | * Copyright (c) 2018 Intel Corporation. | 
|  | * Copyright (c) 2021 Nordic Semiconductor ASA. | 
|  | * | 
|  | * SPDX-License-Identifier: Apache-2.0 | 
|  | */ | 
|  |  | 
|  | #include <zephyr/pm/device.h> | 
|  | #include <zephyr/pm/device_runtime.h> | 
|  | #include <zephyr/sys/__assert.h> | 
|  |  | 
|  | #include <zephyr/logging/log.h> | 
|  | LOG_MODULE_DECLARE(pm_device, CONFIG_PM_DEVICE_LOG_LEVEL); | 
|  |  | 
|  | #ifdef CONFIG_PM_DEVICE_POWER_DOMAIN | 
|  | #define PM_DOMAIN(_pm) \ | 
|  | (_pm)->domain | 
|  | #else | 
|  | #define PM_DOMAIN(_pm) NULL | 
|  | #endif | 
|  |  | 
|  | #define EVENT_STATE_ACTIVE	BIT(PM_DEVICE_STATE_ACTIVE) | 
|  | #define EVENT_STATE_SUSPENDED	BIT(PM_DEVICE_STATE_SUSPENDED) | 
|  |  | 
|  | #define EVENT_MASK		(EVENT_STATE_ACTIVE | EVENT_STATE_SUSPENDED) | 
|  |  | 
|  | /** | 
|  | * @brief Suspend a device | 
|  | * | 
|  | * @note Asynchronous operations are not supported when in pre-kernel mode. In | 
|  | * this case, the async flag will be always forced to be false, and so the | 
|  | * the function will be blocking. | 
|  | * | 
|  | * @funcprops \pre_kernel_ok | 
|  | * | 
|  | * @param dev Device instance. | 
|  | * @param async Perform operation asynchronously. | 
|  | * @param delay Period to delay the asynchronous operation. | 
|  | * | 
|  | * @retval 0 If device has been suspended or queued for suspend. | 
|  | * @retval -EALREADY If device is already suspended (can only happen if get/put | 
|  | * calls are unbalanced). | 
|  | * @retval -EBUSY If the device is busy. | 
|  | * @retval -errno Other negative errno, result of the action callback. | 
|  | */ | 
|  | static int runtime_suspend(const struct device *dev, bool async, | 
|  | k_timeout_t delay) | 
|  | { | 
|  | int ret = 0; | 
|  | struct pm_device *pm = dev->pm; | 
|  |  | 
|  | /* | 
|  | * Early return if device runtime is not enabled. | 
|  | */ | 
|  | if (!atomic_test_bit(&pm->base.flags, PM_DEVICE_FLAG_RUNTIME_ENABLED)) { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | if (k_is_pre_kernel()) { | 
|  | async = false; | 
|  | } else { | 
|  | ret = k_sem_take(&pm->lock, k_is_in_isr() ? K_NO_WAIT : K_FOREVER); | 
|  | if (ret < 0) { | 
|  | return -EBUSY; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (pm->base.usage == 0U) { | 
|  | LOG_WRN("Unbalanced suspend"); | 
|  | ret = -EALREADY; | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | pm->base.usage--; | 
|  | if (pm->base.usage > 0U) { | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | if (async) { | 
|  | /* queue suspend */ | 
|  | pm->base.state = PM_DEVICE_STATE_SUSPENDING; | 
|  | (void)k_work_schedule(&pm->work, delay); | 
|  | } else { | 
|  | /* suspend now */ | 
|  | ret = pm->base.action_cb(pm->dev, PM_DEVICE_ACTION_SUSPEND); | 
|  | if (ret < 0) { | 
|  | pm->base.usage++; | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | pm->base.state = PM_DEVICE_STATE_SUSPENDED; | 
|  | } | 
|  |  | 
|  | unlock: | 
|  | if (!k_is_pre_kernel()) { | 
|  | k_sem_give(&pm->lock); | 
|  | } | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static void runtime_suspend_work(struct k_work *work) | 
|  | { | 
|  | int ret; | 
|  | struct k_work_delayable *dwork = k_work_delayable_from_work(work); | 
|  | struct pm_device *pm = CONTAINER_OF(dwork, struct pm_device, work); | 
|  |  | 
|  | ret = pm->base.action_cb(pm->dev, PM_DEVICE_ACTION_SUSPEND); | 
|  |  | 
|  | (void)k_sem_take(&pm->lock, K_FOREVER); | 
|  | if (ret < 0) { | 
|  | pm->base.usage++; | 
|  | pm->base.state = PM_DEVICE_STATE_ACTIVE; | 
|  | } else { | 
|  | pm->base.state = PM_DEVICE_STATE_SUSPENDED; | 
|  | } | 
|  | k_event_set(&pm->event, BIT(pm->base.state)); | 
|  | k_sem_give(&pm->lock); | 
|  |  | 
|  | /* | 
|  | * On async put, we have to suspend the domain when the device | 
|  | * finishes its operation | 
|  | */ | 
|  | if ((ret == 0) && | 
|  | atomic_test_bit(&pm->base.flags, PM_DEVICE_FLAG_PD_CLAIMED)) { | 
|  | (void)pm_device_runtime_put(PM_DOMAIN(&pm->base)); | 
|  | } | 
|  |  | 
|  | __ASSERT(ret == 0, "Could not suspend device (%d)", ret); | 
|  | } | 
|  |  | 
|  | static int get_sync_locked(const struct device *dev) | 
|  | { | 
|  | int ret; | 
|  | struct pm_device_isr *pm = dev->pm_isr; | 
|  | uint32_t flags = pm->base.flags; | 
|  |  | 
|  | if (pm->base.usage == 0) { | 
|  | if (flags & BIT(PM_DEVICE_FLAG_PD_CLAIMED)) { | 
|  | const struct device *domain = PM_DOMAIN(&pm->base); | 
|  |  | 
|  | if (domain->pm_base->flags & PM_DEVICE_FLAG_ISR_SAFE) { | 
|  | ret = pm_device_runtime_get(domain); | 
|  | if (ret < 0) { | 
|  | return ret; | 
|  | } | 
|  | } else { | 
|  | return -EWOULDBLOCK; | 
|  | } | 
|  | } | 
|  |  | 
|  | ret = pm->base.action_cb(dev, PM_DEVICE_ACTION_RESUME); | 
|  | if (ret < 0) { | 
|  | return ret; | 
|  | } | 
|  | pm->base.state = PM_DEVICE_STATE_ACTIVE; | 
|  | } else { | 
|  | ret = 0; | 
|  | } | 
|  |  | 
|  | pm->base.usage++; | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | int pm_device_runtime_get(const struct device *dev) | 
|  | { | 
|  | int ret = 0; | 
|  | struct pm_device *pm = dev->pm; | 
|  |  | 
|  | if (pm == NULL) { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | SYS_PORT_TRACING_FUNC_ENTER(pm, device_runtime_get, dev); | 
|  |  | 
|  | /* | 
|  | * Early return if device runtime is not enabled. | 
|  | */ | 
|  | if (!atomic_test_bit(&pm->base.flags, PM_DEVICE_FLAG_RUNTIME_ENABLED)) { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | if (atomic_test_bit(&dev->pm_base->flags, PM_DEVICE_FLAG_ISR_SAFE)) { | 
|  | struct pm_device_isr *pm_sync = dev->pm_isr; | 
|  | k_spinlock_key_t k = k_spin_lock(&pm_sync->lock); | 
|  |  | 
|  | ret = get_sync_locked(dev); | 
|  | k_spin_unlock(&pm_sync->lock, k); | 
|  | goto end; | 
|  | } | 
|  |  | 
|  | if (!k_is_pre_kernel()) { | 
|  | ret = k_sem_take(&pm->lock, k_is_in_isr() ? K_NO_WAIT : K_FOREVER); | 
|  | if (ret < 0) { | 
|  | return -EWOULDBLOCK; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (k_is_in_isr() && (pm->base.state == PM_DEVICE_STATE_SUSPENDING)) { | 
|  | ret = -EWOULDBLOCK; | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * If the device is under a power domain, the domain has to be get | 
|  | * first. | 
|  | */ | 
|  | const struct device *domain = PM_DOMAIN(&pm->base); | 
|  |  | 
|  | if (domain != NULL) { | 
|  | ret = pm_device_runtime_get(domain); | 
|  | if (ret != 0) { | 
|  | goto unlock; | 
|  | } | 
|  | /* Check if powering up this device failed */ | 
|  | if (atomic_test_bit(&pm->base.flags, PM_DEVICE_FLAG_TURN_ON_FAILED)) { | 
|  | (void)pm_device_runtime_put(domain); | 
|  | ret = -EAGAIN; | 
|  | goto unlock; | 
|  | } | 
|  | /* Power domain successfully claimed */ | 
|  | atomic_set_bit(&pm->base.flags, PM_DEVICE_FLAG_PD_CLAIMED); | 
|  | } | 
|  |  | 
|  | pm->base.usage++; | 
|  |  | 
|  | /* | 
|  | * Check if the device has a pending suspend operation (not started | 
|  | * yet) and cancel it. This way we avoid unnecessary operations because | 
|  | * the device is actually active. | 
|  | */ | 
|  | if ((pm->base.state == PM_DEVICE_STATE_SUSPENDING) && | 
|  | ((k_work_cancel_delayable(&pm->work) & K_WORK_RUNNING) == 0)) { | 
|  | pm->base.state = PM_DEVICE_STATE_ACTIVE; | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | if (!k_is_pre_kernel()) { | 
|  | /* | 
|  | * If the device is already suspending there is | 
|  | * nothing else we can do but wait until it finishes. | 
|  | */ | 
|  | while (pm->base.state == PM_DEVICE_STATE_SUSPENDING) { | 
|  | k_sem_give(&pm->lock); | 
|  |  | 
|  | k_event_wait(&pm->event, EVENT_MASK, true, K_FOREVER); | 
|  |  | 
|  | (void)k_sem_take(&pm->lock, K_FOREVER); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (pm->base.usage > 1U) { | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | ret = pm->base.action_cb(pm->dev, PM_DEVICE_ACTION_RESUME); | 
|  | if (ret < 0) { | 
|  | pm->base.usage--; | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | pm->base.state = PM_DEVICE_STATE_ACTIVE; | 
|  |  | 
|  | unlock: | 
|  | if (!k_is_pre_kernel()) { | 
|  | k_sem_give(&pm->lock); | 
|  | } | 
|  |  | 
|  | end: | 
|  | SYS_PORT_TRACING_FUNC_EXIT(pm, device_runtime_get, dev, ret); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  |  | 
|  | static int put_sync_locked(const struct device *dev) | 
|  | { | 
|  | int ret; | 
|  | struct pm_device_isr *pm = dev->pm_isr; | 
|  | uint32_t flags = pm->base.flags; | 
|  |  | 
|  | if (!(flags & BIT(PM_DEVICE_FLAG_RUNTIME_ENABLED))) { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | if (pm->base.usage == 0U) { | 
|  | return -EALREADY; | 
|  | } | 
|  |  | 
|  | pm->base.usage--; | 
|  | if (pm->base.usage == 0U) { | 
|  | ret = pm->base.action_cb(dev, PM_DEVICE_ACTION_SUSPEND); | 
|  | if (ret < 0) { | 
|  | return ret; | 
|  | } | 
|  | pm->base.state = PM_DEVICE_STATE_SUSPENDED; | 
|  |  | 
|  | if (flags & BIT(PM_DEVICE_FLAG_PD_CLAIMED)) { | 
|  | const struct device *domain = PM_DOMAIN(&pm->base); | 
|  |  | 
|  | if (domain->pm_base->flags & PM_DEVICE_FLAG_ISR_SAFE) { | 
|  | ret = put_sync_locked(domain); | 
|  | } else { | 
|  | ret = -EWOULDBLOCK; | 
|  | } | 
|  | } | 
|  | } else { | 
|  | ret = 0; | 
|  | } | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | int pm_device_runtime_put(const struct device *dev) | 
|  | { | 
|  | int ret; | 
|  |  | 
|  | if (dev->pm_base == NULL) { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | SYS_PORT_TRACING_FUNC_ENTER(pm, device_runtime_put, dev); | 
|  |  | 
|  | if (atomic_test_bit(&dev->pm_base->flags, PM_DEVICE_FLAG_ISR_SAFE)) { | 
|  | struct pm_device_isr *pm_sync = dev->pm_isr; | 
|  | k_spinlock_key_t k = k_spin_lock(&pm_sync->lock); | 
|  |  | 
|  | ret = put_sync_locked(dev); | 
|  |  | 
|  | k_spin_unlock(&pm_sync->lock, k); | 
|  | } else { | 
|  | ret = runtime_suspend(dev, false, K_NO_WAIT); | 
|  |  | 
|  | /* | 
|  | * Now put the domain | 
|  | */ | 
|  | if ((ret == 0) && | 
|  | atomic_test_bit(&dev->pm_base->flags, PM_DEVICE_FLAG_PD_CLAIMED)) { | 
|  | ret = pm_device_runtime_put(PM_DOMAIN(dev->pm_base)); | 
|  | } | 
|  | } | 
|  | SYS_PORT_TRACING_FUNC_EXIT(pm, device_runtime_put, dev, ret); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | int pm_device_runtime_put_async(const struct device *dev, k_timeout_t delay) | 
|  | { | 
|  | int ret; | 
|  |  | 
|  | if (dev->pm_base == NULL) { | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | SYS_PORT_TRACING_FUNC_ENTER(pm, device_runtime_put_async, dev, delay); | 
|  | if (atomic_test_bit(&dev->pm_base->flags, PM_DEVICE_FLAG_ISR_SAFE)) { | 
|  | struct pm_device_isr *pm_sync = dev->pm_isr; | 
|  | k_spinlock_key_t k = k_spin_lock(&pm_sync->lock); | 
|  |  | 
|  | ret = put_sync_locked(dev); | 
|  |  | 
|  | k_spin_unlock(&pm_sync->lock, k); | 
|  | } else { | 
|  | ret = runtime_suspend(dev, true, delay); | 
|  | } | 
|  | SYS_PORT_TRACING_FUNC_EXIT(pm, device_runtime_put_async, dev, delay, ret); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | __boot_func | 
|  | int pm_device_runtime_auto_enable(const struct device *dev) | 
|  | { | 
|  | struct pm_device_base *pm = dev->pm_base; | 
|  |  | 
|  | /* No action needed if PM_DEVICE_FLAG_RUNTIME_AUTO is not enabled */ | 
|  | if (!pm || !atomic_test_bit(&pm->flags, PM_DEVICE_FLAG_RUNTIME_AUTO)) { | 
|  | return 0; | 
|  | } | 
|  | return pm_device_runtime_enable(dev); | 
|  | } | 
|  |  | 
|  | static int runtime_enable_sync(const struct device *dev) | 
|  | { | 
|  | int ret; | 
|  | struct pm_device_isr *pm = dev->pm_isr; | 
|  | k_spinlock_key_t k = k_spin_lock(&pm->lock); | 
|  |  | 
|  | if (pm->base.state == PM_DEVICE_STATE_ACTIVE) { | 
|  | ret = pm->base.action_cb(dev, PM_DEVICE_ACTION_SUSPEND); | 
|  | if (ret < 0) { | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | pm->base.state = PM_DEVICE_STATE_SUSPENDED; | 
|  | } else { | 
|  | ret = 0; | 
|  | } | 
|  |  | 
|  | pm->base.flags |= BIT(PM_DEVICE_FLAG_RUNTIME_ENABLED); | 
|  | pm->base.usage = 0U; | 
|  | unlock: | 
|  | k_spin_unlock(&pm->lock, k); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | int pm_device_runtime_enable(const struct device *dev) | 
|  | { | 
|  | int ret = 0; | 
|  | struct pm_device *pm = dev->pm; | 
|  |  | 
|  | SYS_PORT_TRACING_FUNC_ENTER(pm, device_runtime_enable, dev); | 
|  |  | 
|  | if (pm == NULL) { | 
|  | ret = -ENOTSUP; | 
|  | goto end; | 
|  | } | 
|  |  | 
|  | if (atomic_test_bit(&pm->base.flags, PM_DEVICE_FLAG_RUNTIME_ENABLED)) { | 
|  | goto end; | 
|  | } | 
|  |  | 
|  | if (pm_device_state_is_locked(dev)) { | 
|  | ret = -EPERM; | 
|  | goto end; | 
|  | } | 
|  |  | 
|  | if (atomic_test_bit(&dev->pm_base->flags, PM_DEVICE_FLAG_ISR_SAFE)) { | 
|  | ret = runtime_enable_sync(dev); | 
|  | goto end; | 
|  | } | 
|  |  | 
|  | if (!k_is_pre_kernel()) { | 
|  | (void)k_sem_take(&pm->lock, K_FOREVER); | 
|  | } | 
|  |  | 
|  | /* lazy init of PM fields */ | 
|  | if (pm->dev == NULL) { | 
|  | pm->dev = dev; | 
|  | k_work_init_delayable(&pm->work, runtime_suspend_work); | 
|  | } | 
|  |  | 
|  | if (pm->base.state == PM_DEVICE_STATE_ACTIVE) { | 
|  | ret = pm->base.action_cb(pm->dev, PM_DEVICE_ACTION_SUSPEND); | 
|  | if (ret < 0) { | 
|  | goto unlock; | 
|  | } | 
|  | pm->base.state = PM_DEVICE_STATE_SUSPENDED; | 
|  | } | 
|  |  | 
|  | pm->base.usage = 0U; | 
|  |  | 
|  | atomic_set_bit(&pm->base.flags, PM_DEVICE_FLAG_RUNTIME_ENABLED); | 
|  |  | 
|  | unlock: | 
|  | if (!k_is_pre_kernel()) { | 
|  | k_sem_give(&pm->lock); | 
|  | } | 
|  |  | 
|  | end: | 
|  | SYS_PORT_TRACING_FUNC_EXIT(pm, device_runtime_enable, dev, ret); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static int runtime_disable_sync(const struct device *dev) | 
|  | { | 
|  | struct pm_device_isr *pm = dev->pm_isr; | 
|  | int ret; | 
|  | k_spinlock_key_t k = k_spin_lock(&pm->lock); | 
|  |  | 
|  | if (pm->base.state == PM_DEVICE_STATE_SUSPENDED) { | 
|  | ret = pm->base.action_cb(dev, PM_DEVICE_ACTION_RESUME); | 
|  | if (ret < 0) { | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | pm->base.state = PM_DEVICE_STATE_ACTIVE; | 
|  | } else { | 
|  | ret = 0; | 
|  | } | 
|  |  | 
|  | pm->base.flags &= ~BIT(PM_DEVICE_FLAG_RUNTIME_ENABLED); | 
|  | unlock: | 
|  | k_spin_unlock(&pm->lock, k); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | int pm_device_runtime_disable(const struct device *dev) | 
|  | { | 
|  | int ret = 0; | 
|  | struct pm_device *pm = dev->pm; | 
|  |  | 
|  | SYS_PORT_TRACING_FUNC_ENTER(pm, device_runtime_disable, dev); | 
|  |  | 
|  | if (pm == NULL) { | 
|  | ret = -ENOTSUP; | 
|  | goto end; | 
|  | } | 
|  |  | 
|  | if (!atomic_test_bit(&pm->base.flags, PM_DEVICE_FLAG_RUNTIME_ENABLED)) { | 
|  | goto end; | 
|  | } | 
|  |  | 
|  | if (atomic_test_bit(&dev->pm_base->flags, PM_DEVICE_FLAG_ISR_SAFE)) { | 
|  | ret = runtime_disable_sync(dev); | 
|  | goto end; | 
|  | } | 
|  |  | 
|  | if (!k_is_pre_kernel()) { | 
|  | (void)k_sem_take(&pm->lock, K_FOREVER); | 
|  | } | 
|  |  | 
|  | if (!k_is_pre_kernel()) { | 
|  | if ((pm->base.state == PM_DEVICE_STATE_SUSPENDING) && | 
|  | ((k_work_cancel_delayable(&pm->work) & K_WORK_RUNNING) == 0)) { | 
|  | pm->base.state = PM_DEVICE_STATE_ACTIVE; | 
|  | goto clear_bit; | 
|  | } | 
|  |  | 
|  | /* wait until possible async suspend is completed */ | 
|  | while (pm->base.state == PM_DEVICE_STATE_SUSPENDING) { | 
|  | k_sem_give(&pm->lock); | 
|  |  | 
|  | k_event_wait(&pm->event, EVENT_MASK, true, K_FOREVER); | 
|  |  | 
|  | (void)k_sem_take(&pm->lock, K_FOREVER); | 
|  | } | 
|  | } | 
|  |  | 
|  | /* wake up the device if suspended */ | 
|  | if (pm->base.state == PM_DEVICE_STATE_SUSPENDED) { | 
|  | ret = pm->base.action_cb(dev, PM_DEVICE_ACTION_RESUME); | 
|  | if (ret < 0) { | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | pm->base.state = PM_DEVICE_STATE_ACTIVE; | 
|  | } | 
|  |  | 
|  | clear_bit: | 
|  | atomic_clear_bit(&pm->base.flags, PM_DEVICE_FLAG_RUNTIME_ENABLED); | 
|  |  | 
|  | unlock: | 
|  | if (!k_is_pre_kernel()) { | 
|  | k_sem_give(&pm->lock); | 
|  | } | 
|  |  | 
|  | end: | 
|  | SYS_PORT_TRACING_FUNC_EXIT(pm, device_runtime_disable, dev, ret); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | bool pm_device_runtime_is_enabled(const struct device *dev) | 
|  | { | 
|  | struct pm_device_base *pm = dev->pm_base; | 
|  |  | 
|  | return pm && atomic_test_bit(&pm->flags, PM_DEVICE_FLAG_RUNTIME_ENABLED); | 
|  | } |