diff --git a/doc/releases/release-notes-4.2.rst b/doc/releases/release-notes-4.2.rst
index 136fa85d4cbfa..5a4bc0a5d7f67 100644
--- a/doc/releases/release-notes-4.2.rst
+++ b/doc/releases/release-notes-4.2.rst
@@ -173,6 +173,16 @@ New APIs and options
* :kconfig:option:`CONFIG_MQTT_VERSION_5_0`
+* Power management
+
+ * :kconfig:option:`CONFIG_PM_DEVICE_RUNTIME_USE_SYSTEM_WQ`
+ * :kconfig:option:`CONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ`
+ * :kconfig:option:`CONFIG_PM_DEVICE_DRIVER_NEEDS_DEDICATED_WQ`
+ * :kconfig:option:`CONFIG_PM_DEVICE_RUNTIME_DEDICATED_WQ_STACK_SIZE`
+ * :kconfig:option:`CONFIG_PM_DEVICE_RUNTIME_DEDICATED_WQ_PRIO`
+ * :kconfig:option:`CONFIG_PM_DEVICE_RUNTIME_DEDICATED_WQ_INIT_PRIO`
+ * :kconfig:option:`CONFIG_PM_DEVICE_RUNTIME_ASYNC`
+
* Sockets
* :kconfig:option:`CONFIG_NET_SOCKETS_INET_RAW`
diff --git a/doc/services/pm/device_runtime.rst b/doc/services/pm/device_runtime.rst
index 350874009b9f7..309b07ff22fe7 100644
--- a/doc/services/pm/device_runtime.rst
+++ b/doc/services/pm/device_runtime.rst
@@ -107,9 +107,26 @@ be a problem if the operation is fast, e.g. a register toggle. However, the
situation will not be the same if suspension involves sending packets through a
slow bus. For this reason the device drivers can also make use of the
:c:func:`pm_device_runtime_put_async` function. This function will schedule
-the suspend operation, again, if device is no longer used. The suspension will
-then be carried out when the system work queue gets the chance to run. The
-sequence diagram shown below illustrates this scenario.
+the suspend operation, again, if device is no longer used.
+
+
+By default, runtime PM operations are offloaded to the system work queue.
+However, device drivers must not perform any blocking operations during suspend, as
+this can stall the system work queue and negatively impact system responsiveness.
+
+To address this, applications can configure runtime PM to use a dedicated work queue
+by enabling :kconfig:option:`CONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ`.
+
+If blocking behavior is required—for example, when accessing a slow peripheral
+or waiting for a bus transaction—the PM subsystem work queue must be used instead.
+Drivers that require this behavior can explicitly request it by enabling
+:kconfig:option:`CONFIG_PM_DEVICE_DRIVER_NEEDS_DEDICATED_WQ`.
+
+For targets with constrained resources that do not need asynchronous
+operations, this functionality can be disabled altogether by
+de-selecting :kconfig:option:`CONFIOG_PM_DEVICE_RUNTIME_ASYNC`, reducing
+memory usage and system complexity.
+
.. figure:: images/devr-async-ops.svg
diff --git a/doc/services/pm/images/devr-async-ops.svg b/doc/services/pm/images/devr-async-ops.svg
index 8fe1f6e18239c..9cd8a02d8053b 100644
--- a/doc/services/pm/images/devr-async-ops.svg
+++ b/doc/services/pm/images/devr-async-ops.svg
@@ -1,4 +1,4 @@
-
\ No newline at end of file
+-->
diff --git a/include/zephyr/pm/device.h b/include/zephyr/pm/device.h
index f7c008f3cf2ce..5badda9fa68f3 100644
--- a/include/zephyr/pm/device.h
+++ b/include/zephyr/pm/device.h
@@ -170,8 +170,10 @@ struct pm_device {
struct k_sem lock;
/** Event var to listen to the sync request events */
struct k_event event;
+#if defined(CONFIG_PM_DEVICE_RUNTIME_ASYNC) || defined(__DOXYGEN__)
/** Work object for asynchronous calls */
struct k_work_delayable work;
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
#endif /* CONFIG_PM_DEVICE_RUNTIME */
};
diff --git a/subsys/pm/Kconfig b/subsys/pm/Kconfig
index 19078d02b8158..c2bca1c5f04c5 100644
--- a/subsys/pm/Kconfig
+++ b/subsys/pm/Kconfig
@@ -121,6 +121,61 @@ config PM_DEVICE_RUNTIME
enabled, devices can be suspended or resumed based on the device
usage even while the CPU or system is running.
+if PM_DEVICE_RUNTIME
+
+config PM_DEVICE_DRIVER_NEEDS_DEDICATED_WQ
+ bool
+
+config PM_DEVICE_RUNTIME_ASYNC
+ bool "Asynchronous device runtime power management"
+ default y
+ help
+ Use this option to enable support for asynchronous operation
+ in the power management device runtime.
+
+if PM_DEVICE_RUNTIME_ASYNC
+
+choice PM_DEVICE_RUNTIME_WQ
+ prompt "Work queue to be used by pm device runtime async"
+ default PM_DEVICE_RUNTIME_USE_DEDICATED_WQ if PM_DEVICE_DRIVER_NEEDS_DEDICATED_WQ
+ default PM_DEVICE_RUNTIME_USE_SYSTEM_WQ
+
+config PM_DEVICE_RUNTIME_USE_SYSTEM_WQ
+ bool "Use the system workqueue"
+ help
+ When this option is enabled the power management subsystem will
+ use the system worqueue instead of defining its own queue.
+
+config PM_DEVICE_RUNTIME_USE_DEDICATED_WQ
+ bool "Use a dedicated workqueue"
+ help
+ When this option is enabled the power management subsystem will
+ use a dedicated worqueue instead of the system work queue.
+
+if PM_DEVICE_RUNTIME_USE_DEDICATED_WQ
+config PM_DEVICE_RUNTIME_DEDICATED_WQ_STACK_SIZE
+ int "Stack size for pm runtime async workqueue"
+ default 1024
+ help
+ Defines the size of the stack on the workqueue used for
+ async operations.
+
+config PM_DEVICE_RUNTIME_DEDICATED_WQ_PRIO
+ int "PM device runtime workqueue priority. Should be pre-emptible."
+ default SYSTEM_WORKQUEUE_PRIORITY if PM_DEVICE_RUNTIME_USE_SYSTEM_WQ
+ default 0
+
+config PM_DEVICE_RUNTIME_DEDICATED_WQ_INIT_PRIO
+ int "PM device runtime workqueue init priority"
+ default 50
+ help
+ Init priority level to setup the device runtime workqueue.
+endif #PM_DEVICE_RUNTIME_USE_DEDICATED_WQ
+endchoice
+
+endif # PM_DEVICE_RUNTIME_ASYNC
+endif # PM_DEVICE_RUNTIME
+
config PM_DEVICE_SHELL
bool "Device Power Management shell"
depends on SHELL
diff --git a/subsys/pm/device_runtime.c b/subsys/pm/device_runtime.c
index 4533886dd2af5..cf58108fea444 100644
--- a/subsys/pm/device_runtime.c
+++ b/subsys/pm/device_runtime.c
@@ -1,6 +1,7 @@
/*
* Copyright (c) 2018 Intel Corporation.
* Copyright (c) 2021 Nordic Semiconductor ASA.
+ * Copyright (c) 2025 HubbleNetwork.
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -19,6 +20,13 @@ LOG_MODULE_DECLARE(pm_device, CONFIG_PM_DEVICE_LOG_LEVEL);
#define PM_DOMAIN(_pm) NULL
#endif
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
+#ifdef CONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ
+K_THREAD_STACK_DEFINE(pm_device_runtime_stack, CONFIG_PM_DEVICE_RUNTIME_DEDICATED_WQ_STACK_SIZE);
+static struct k_work_q pm_device_runtime_wq;
+#endif /* CONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ */
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
+
#define EVENT_STATE_ACTIVE BIT(PM_DEVICE_STATE_ACTIVE)
#define EVENT_STATE_SUSPENDED BIT(PM_DEVICE_STATE_SUSPENDED)
@@ -78,8 +86,14 @@ static int runtime_suspend(const struct device *dev, bool async,
if (async) {
/* queue suspend */
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
pm->base.state = PM_DEVICE_STATE_SUSPENDING;
+#ifdef CONFIG_PM_DEVICE_RUNTIME_USE_SYSTEM_WQ
(void)k_work_schedule(&pm->work, delay);
+#else
+ (void)k_work_schedule_for_queue(&pm_device_runtime_wq, &pm->work, delay);
+#endif /* CONFIG_PM_DEVICE_RUNTIME_USE_SYSTEM_WQ */
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
} else {
/* suspend now */
ret = pm->base.action_cb(pm->dev, PM_DEVICE_ACTION_SUSPEND);
@@ -99,6 +113,7 @@ static int runtime_suspend(const struct device *dev, bool async,
return ret;
}
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
static void runtime_suspend_work(struct k_work *work)
{
int ret;
@@ -128,6 +143,7 @@ static void runtime_suspend_work(struct k_work *work)
__ASSERT(ret == 0, "Could not suspend device (%d)", ret);
}
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
static int get_sync_locked(const struct device *dev)
{
@@ -225,6 +241,7 @@ int pm_device_runtime_get(const struct device *dev)
pm->base.usage++;
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
/*
* Check if the device has a pending suspend operation (not started
* yet) and cancel it. This way we avoid unnecessary operations because
@@ -250,6 +267,7 @@ int pm_device_runtime_get(const struct device *dev)
(void)k_sem_take(&pm->lock, K_FOREVER);
}
}
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
if (pm->base.usage > 1U) {
goto unlock;
@@ -348,6 +366,7 @@ int pm_device_runtime_put(const struct device *dev)
int pm_device_runtime_put_async(const struct device *dev, k_timeout_t delay)
{
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
int ret;
if (dev->pm_base == NULL) {
@@ -368,6 +387,10 @@ int pm_device_runtime_put_async(const struct device *dev, k_timeout_t delay)
SYS_PORT_TRACING_FUNC_EXIT(pm, device_runtime_put_async, dev, delay, ret);
return ret;
+#else
+ LOG_WRN("Function not available");
+ return -ENOSYS;
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
}
__boot_func
@@ -439,7 +462,9 @@ int pm_device_runtime_enable(const struct device *dev)
/* lazy init of PM fields */
if (pm->dev == NULL) {
pm->dev = dev;
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
k_work_init_delayable(&pm->work, runtime_suspend_work);
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
}
if (pm->base.state == PM_DEVICE_STATE_ACTIVE) {
@@ -512,6 +537,7 @@ int pm_device_runtime_disable(const struct device *dev)
(void)k_sem_take(&pm->lock, K_FOREVER);
}
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
if (!k_is_pre_kernel()) {
if ((pm->base.state == PM_DEVICE_STATE_SUSPENDING) &&
((k_work_cancel_delayable(&pm->work) & K_WORK_RUNNING) == 0)) {
@@ -529,6 +555,7 @@ int pm_device_runtime_disable(const struct device *dev)
(void)k_sem_take(&pm->lock, K_FOREVER);
}
}
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
/* wake up the device if suspended */
if (pm->base.state == PM_DEVICE_STATE_SUSPENDED) {
@@ -539,8 +566,9 @@ int pm_device_runtime_disable(const struct device *dev)
pm->base.state = PM_DEVICE_STATE_ACTIVE;
}
-
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
clear_bit:
+#endif
atomic_clear_bit(&pm->base.flags, PM_DEVICE_FLAG_RUNTIME_ENABLED);
unlock:
@@ -569,3 +597,25 @@ int pm_device_runtime_usage(const struct device *dev)
return dev->pm_base->usage;
}
+
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
+#ifdef CONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ
+
+static int pm_device_runtime_wq_init(void)
+{
+ const struct k_work_queue_config cfg = {.name = "PM DEVICE RUNTIME WQ"};
+
+ k_work_queue_init(&pm_device_runtime_wq);
+
+ k_work_queue_start(&pm_device_runtime_wq, pm_device_runtime_stack,
+ K_THREAD_STACK_SIZEOF(pm_device_runtime_stack),
+ CONFIG_PM_DEVICE_RUNTIME_DEDICATED_WQ_PRIO, &cfg);
+
+ return 0;
+}
+
+SYS_INIT(pm_device_runtime_wq_init, POST_KERNEL,
+ CONFIG_PM_DEVICE_RUNTIME_DEDICATED_WQ_INIT_PRIO);
+
+#endif /* CONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ */
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
diff --git a/tests/subsys/pm/device_runtime_api/prj.conf b/tests/subsys/pm/device_runtime_api/prj.conf
index 5708c297e9dab..f76825d13483b 100644
--- a/tests/subsys/pm/device_runtime_api/prj.conf
+++ b/tests/subsys/pm/device_runtime_api/prj.conf
@@ -3,3 +3,4 @@ CONFIG_PM=y
CONFIG_PM_DEVICE=y
CONFIG_PM_DEVICE_RUNTIME=y
CONFIG_MP_MAX_NUM_CPUS=1
+CONFIG_ZTEST_THREAD_PRIORITY=3
diff --git a/tests/subsys/pm/device_runtime_api/src/main.c b/tests/subsys/pm/device_runtime_api/src/main.c
index ba4482ac8f300..d9f5f35aed4b6 100644
--- a/tests/subsys/pm/device_runtime_api/src/main.c
+++ b/tests/subsys/pm/device_runtime_api/src/main.c
@@ -9,8 +9,12 @@
#include
#include "test_driver.h"
+#include "zephyr/sys/util_macro.h"
+
static const struct device *test_dev;
+
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
static struct k_thread get_runner_td;
K_THREAD_STACK_DEFINE(get_runner_stack, 1024);
@@ -31,6 +35,7 @@ static void get_runner(void *arg1, void *arg2, void *arg3)
ret = pm_device_runtime_get(test_dev);
zassert_equal(ret, 0);
}
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
void test_api_setup(void *data)
{
@@ -43,7 +48,11 @@ void test_api_setup(void *data)
ret = pm_device_runtime_put(test_dev);
zassert_equal(ret, 0);
ret = pm_device_runtime_put_async(test_dev, K_NO_WAIT);
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
zassert_equal(ret, 0);
+#else
+ zassert_equal(ret, -ENOSYS);
+#endif
/* enable runtime PM */
ret = pm_device_runtime_enable(test_dev);
@@ -130,6 +139,7 @@ ZTEST(device_runtime_api, test_api)
zassert_equal(ret, -EALREADY);
zassert_equal(pm_device_runtime_usage(test_dev), 0);
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
/*** get + asynchronous put until suspended ***/
/* usage: 0, +1, resume: yes */
@@ -200,8 +210,10 @@ ZTEST(device_runtime_api, test_api)
*/
k_thread_create(&get_runner_td, get_runner_stack,
K_THREAD_STACK_SIZEOF(get_runner_stack), get_runner,
- NULL, NULL, NULL, CONFIG_SYSTEM_WORKQUEUE_PRIORITY, 0,
- K_NO_WAIT);
+ NULL, NULL, NULL,
+ COND_CODE_1(CONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ,
+ (CONFIG_PM_DEVICE_RUNTIME_DEDICATED_WQ_PRIO),
+ (CONFIG_SYSTEM_WORKQUEUE_PRIORITY)), 0, K_NO_WAIT);
k_yield();
/* let driver suspend to finish and wait until get_runner finishes
@@ -259,6 +271,7 @@ ZTEST(device_runtime_api, test_api)
ret = pm_device_runtime_disable(test_dev);
zassert_equal(ret, 0);
zassert_equal(pm_device_runtime_usage(test_dev), -ENOTSUP);
+#endif /* CONFIG_PM_DEVICE_RUNTIME_ASYNC */
}
DEVICE_DEFINE(pm_unsupported_device, "PM Unsupported", NULL, NULL, NULL, NULL,
@@ -273,7 +286,11 @@ ZTEST(device_runtime_api, test_unsupported)
zassert_equal(pm_device_runtime_disable(dev), -ENOTSUP, "");
zassert_equal(pm_device_runtime_get(dev), 0, "");
zassert_equal(pm_device_runtime_put(dev), 0, "");
- zassert_false(pm_device_runtime_put_async(dev, K_NO_WAIT), "");
+#ifdef CONFIG_PM_DEVICE_RUNTIME_ASYNC
+ zassert_equal(pm_device_runtime_put_async(dev, K_NO_WAIT), 0, "");
+#else
+ zassert_equal(pm_device_runtime_put_async(dev, K_NO_WAIT), -ENOSYS, "");
+#endif
}
int dev_pm_control(const struct device *dev, enum pm_device_action action)
diff --git a/tests/subsys/pm/device_runtime_api/testcase.yaml b/tests/subsys/pm/device_runtime_api/testcase.yaml
index 80ce7bd9bebaf..b4ab9e1bd66ae 100644
--- a/tests/subsys/pm/device_runtime_api/testcase.yaml
+++ b/tests/subsys/pm/device_runtime_api/testcase.yaml
@@ -10,3 +10,13 @@ tests:
- native_sim
extra_configs:
- CONFIG_TEST_PM_DEVICE_ISR_SAFE=y
+ pm.device_runtime.async_dedicated_wq.api:
+ platform_allow:
+ - native_sim
+ extra_configs:
+ - CONFIG_PM_DEVICE_RUNTIME_USE_DEDICATED_WQ=y
+ pm.device_runtime.async_disabled.api:
+ platform_allow:
+ - native_sim
+ extra_configs:
+ - CONFIG_PM_DEVICE_RUNTIME_ASYNC=n