aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelix Morgner <felix.morgner@ost.ch>2026-08-17 15:20:57 +0200
committerFelix Morgner <felix.morgner@ost.ch>2026-08-17 15:20:57 +0200
commit824cc5a96f8a6f666a5c963897c8ab9076b48373 (patch)
tree3c0f28e79702487f4ebcc12cf5d94bc3ef751887
parent44f28c5ceafafd88b68de6fe2c05fac332ff3c33 (diff)
downloadkernel-824cc5a96f8a6f666a5c963897c8ab9076b48373.tar.xz
kernel-824cc5a96f8a6f666a5c963897c8ab9076b48373.zip
kapi: add power management support to drivers
-rw-r--r--kapi/kapi/devices.hpp1
-rw-r--r--kapi/kapi/devices/driver.hpp15
-rw-r--r--kapi/kapi/devices/power.hpp38
-rw-r--r--kernel/CMakeLists.txt1
-rw-r--r--kernel/kapi/devices/driver.cpp12
-rw-r--r--kernel/kapi/devices/power.cpp93
-rw-r--r--kernel/kapi/devices/power.tests.cpp203
7 files changed, 363 insertions, 0 deletions
diff --git a/kapi/kapi/devices.hpp b/kapi/kapi/devices.hpp
index b20e3546..0781357c 100644
--- a/kapi/kapi/devices.hpp
+++ b/kapi/kapi/devices.hpp
@@ -9,6 +9,7 @@
#include <kapi/devices/driver_registry.hpp> // IWYU pragma: export
#include <kapi/devices/error.hpp> // IWYU pragma: export
#include <kapi/devices/facet_registry.hpp> // IWYU pragma: export
+#include <kapi/devices/power.hpp> // IWYU pragma: export
#include <kapi/devices/resource.hpp> // IWYU pragma: export
#include <kstd/memory.hpp>
diff --git a/kapi/kapi/devices/driver.hpp b/kapi/kapi/devices/driver.hpp
index 20f66536..b46091fe 100644
--- a/kapi/kapi/devices/driver.hpp
+++ b/kapi/kapi/devices/driver.hpp
@@ -46,6 +46,21 @@ namespace kapi::devices
//! @param device The device to release.
virtual auto unbind(device & device) -> void = 0;
+ //! Suspend a device bound by this driver.
+ //!
+ //! This function will be invoked in in a depth-first manner, meaning any children of the bound device have already
+ //! been suspended. If suspension fails, the device must remain in, or be returned to, a non-suspended state by this
+ //! function.
+ //!
+ //! @param device The device to suspend.
+ //! @return Nothing on success, a error otherwise.
+ [[nodiscard]] virtual auto suspend(device & device) -> kstd::result<void>;
+
+ //! Resume a device bound by this driver.
+ //!
+ //! @param device The device to resume.
+ [[nodiscard]] virtual auto resume(device & device) -> kstd::result<void>;
+
//! Retrieve this drivers implementation of a given capability facet.
//!
//! @return A pointer to this drivers implementation of the requested facet, nullptr if this driver does not
diff --git a/kapi/kapi/devices/power.hpp b/kapi/kapi/devices/power.hpp
new file mode 100644
index 00000000..44837a65
--- /dev/null
+++ b/kapi/kapi/devices/power.hpp
@@ -0,0 +1,38 @@
+#ifndef TEACHOS_KAPI_DEVICES_POWER_HPP
+#define TEACHOS_KAPI_DEVICES_POWER_HPP
+
+// IWYU pragma: private, include <kapi/devices.hpp>
+
+#include <kapi/devices/bus.hpp>
+
+#include <kstd/result.hpp>
+
+namespace kapi::devices
+{
+
+ //! @addtogroup kapi-devices-kernel-defined
+ //! @{
+
+ //! Suspend a device (sub-)tree rooted in a given bus.
+ //!
+ //! This function ensures a depth-first traversal and suspension of the given tree. In case suspension of a device
+ //! fails, a roll-back will be performed, resuming any previously suspended devices. Children will be suspended before
+ //! their parents are.
+ //!
+ //! @param root The root of the tree to suspend.
+ //! @return nothing on success, the first error to occur otherwise.
+ auto suspend_tree(bus & root) -> kstd::result<void>;
+
+ //! Resume a device (sub-)tree rooted in a given bus.
+ //!
+ //! This function resumes the tree in the reverse order of suspension performed by suspend_tree. On error, resumption
+ //! continues and error codes and messages are logged.
+ //!
+ //! @param root The root of the tree to resume.
+ auto resume_tree(bus & root) -> void;
+
+ //! @}
+
+} // namespace kapi::devices
+
+#endif \ No newline at end of file
diff --git a/kernel/CMakeLists.txt b/kernel/CMakeLists.txt
index dd72051d..d286b2aa 100644
--- a/kernel/CMakeLists.txt
+++ b/kernel/CMakeLists.txt
@@ -19,6 +19,7 @@ target_sources("kernel_lib" PRIVATE
"kapi/devices/driver_registry.cpp"
"kapi/devices/error.cpp"
"kapi/devices/facet_registry.cpp"
+ "kapi/devices/power.cpp"
"kapi/filesystem.cpp"
"kapi/interrupts.cpp"
"kapi/memory.cpp"
diff --git a/kernel/kapi/devices/driver.cpp b/kernel/kapi/devices/driver.cpp
index 4f0ce0e4..5ca92dc8 100644
--- a/kernel/kapi/devices/driver.cpp
+++ b/kernel/kapi/devices/driver.cpp
@@ -1,6 +1,8 @@
#include <kapi/capabilities/facet_id.hpp>
#include <kapi/devices.hpp>
+#include <kstd/result.hpp>
+
#include <cstdint>
#include <optional>
@@ -17,6 +19,16 @@ namespace kapi::devices
return std::nullopt;
}
+ auto driver::suspend(device &) -> kstd::result<void>
+ {
+ return kstd::success();
+ }
+
+ auto driver::resume(device &) -> kstd::result<void>
+ {
+ return kstd::success();
+ }
+
auto driver::query_facet(kapi::capabilities::facet_id) -> void *
{
return nullptr;
diff --git a/kernel/kapi/devices/power.cpp b/kernel/kapi/devices/power.cpp
new file mode 100644
index 00000000..3bc66848
--- /dev/null
+++ b/kernel/kapi/devices/power.cpp
@@ -0,0 +1,93 @@
+#include <kapi/devices/power.hpp>
+
+#include <kapi/devices.hpp>
+
+#include <kstd/memory.hpp>
+#include <kstd/print.hpp>
+#include <kstd/result.hpp>
+#include <kstd/vector.hpp>
+
+#include <ranges>
+
+namespace kapi::devices
+{
+
+ namespace
+ {
+ auto resume_subtree(device & node) -> void;
+
+ // NOLINTNEXTLINE(misc-no-recursion)
+ auto suspend_subtree(device & node) -> kstd::result<void>
+ {
+ auto suspended_children = kstd::vector<kstd::shared_ptr<device>>{};
+
+ if (auto as_bus = node.facet<bus>())
+ {
+ for (auto const & child : as_bus->children())
+ {
+ auto result = suspend_subtree(*child);
+ if (!result)
+ {
+ for (auto const & already_suspended : std::views::reverse(suspended_children))
+ {
+ resume_subtree(*already_suspended);
+ }
+
+ return result;
+ }
+
+ suspended_children.push_back(child);
+ }
+ }
+
+ if (auto driver = node.bound_driver())
+ {
+ auto result = driver->suspend(node);
+ if (!result)
+ {
+ for (auto const & already_suspended : std::views::reverse(suspended_children))
+ {
+ resume_subtree(*already_suspended);
+ }
+
+ return result;
+ }
+ }
+
+ return kstd::success();
+ }
+
+ // NOLINTNEXTLINE(misc-no-recursion)
+ auto resume_subtree(device & node) -> void
+ {
+ if (auto driver = node.bound_driver())
+ {
+ auto result = driver->resume(node);
+ if (!result)
+ {
+ kstd::println(kstd::print_sink::stderr, "[OS:PM] failed to resume device '{}': {}", node.name(),
+ result.error().message());
+ }
+ }
+
+ if (auto as_bus = node.facet<bus>())
+ {
+ for (auto const & child : as_bus->children())
+ {
+ resume_subtree(*child);
+ }
+ }
+ }
+ } // namespace
+
+ auto suspend_tree(bus & root) -> kstd::result<void>
+ {
+ return suspend_subtree(root);
+ }
+
+ auto resume_tree(bus & root) -> void
+ {
+ resume_subtree(root);
+ }
+
+} // namespace kapi::devices \ No newline at end of file
diff --git a/kernel/kapi/devices/power.tests.cpp b/kernel/kapi/devices/power.tests.cpp
new file mode 100644
index 00000000..8f522040
--- /dev/null
+++ b/kernel/kapi/devices/power.tests.cpp
@@ -0,0 +1,203 @@
+#include <kapi/devices/power.hpp>
+
+#include <kapi/devices.hpp>
+
+#include <kstd/format.hpp>
+#include <kstd/memory.hpp>
+#include <kstd/result.hpp>
+#include <kstd/string.hpp>
+#include <kstd/system_error.hpp>
+#include <kstd/vector.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <algorithm>
+#include <cstddef>
+#include <string_view>
+
+namespace
+{
+
+ struct test_device : kapi::devices::device
+ {
+ using kapi::devices::device::device;
+ };
+
+ struct recording_driver final : kapi::devices::driver
+ {
+ explicit recording_driver(kstd::vector<kstd::string> & log, kstd::string fail_on_suspend = {})
+ : m_log{&log}
+ , m_fail_on_suspend{fail_on_suspend}
+ {}
+
+ [[nodiscard]] auto probe(kapi::devices::device &) -> kstd::result<void> override
+ {
+ return kstd::success();
+ }
+
+ auto unbind(kapi::devices::device &) -> void override {}
+
+ [[nodiscard]] auto suspend(kapi::devices::device & device) -> kstd::result<void> override
+ {
+ if (!m_fail_on_suspend.empty() && device.name() == m_fail_on_suspend)
+ {
+ return kstd::failure(make_error_code(kstd::errc::io_error));
+ }
+
+ m_log->push_back(kstd::format("suspend:{}", device.name()));
+ return kstd::success();
+ }
+
+ [[nodiscard]] auto resume(kapi::devices::device & device) -> kstd::result<void> override
+ {
+ m_log->push_back(kstd::format("resume:{}", device.name()));
+ return kstd::success();
+ }
+
+ [[nodiscard]] auto name() const noexcept -> std::string_view override
+ {
+ return "recording_driver";
+ }
+
+ private:
+ kstd::vector<kstd::string> * m_log;
+ kstd::string m_fail_on_suspend;
+ };
+
+ auto index_of(kstd::vector<kstd::string> const & log, std::string_view entry) -> std::size_t
+ {
+ auto found = std::ranges::find(log, entry);
+ REQUIRE(found != log.end());
+ return static_cast<std::size_t>(found - log.begin());
+ }
+
+} // namespace
+
+SCENARIO("Suspension and resumption happens in the correct order", "[devices][power]")
+{
+ GIVEN("a bus with two bound lead devices, itself bound to its own driver, attached to a root")
+ {
+ auto log = kstd::vector<kstd::string>{};
+
+ auto root = kstd::make_shared<kapi::devices::bus>("power_test_root");
+ auto middle = kstd::make_shared<kapi::devices::bus>("power_test_middle");
+ auto leaf_1 = kstd::make_shared<test_device>("power_test_leaf_1");
+ auto leaf_2 = kstd::make_shared<test_device>("power_test_leaf_2");
+
+ auto middle_driver = kstd::make_shared<recording_driver>(log);
+ auto leaf_1_driver = kstd::make_shared<recording_driver>(log);
+ auto leaf_2_driver = kstd::make_shared<recording_driver>(log);
+
+ root->add_child(middle);
+ middle->add_child(leaf_1);
+ middle->add_child(leaf_2);
+
+ middle->bind_driver(middle_driver);
+ middle->set_state(kapi::devices::state::bound);
+
+ leaf_1->bind_driver(leaf_1_driver);
+ leaf_1->set_state(kapi::devices::state::bound);
+
+ leaf_2->bind_driver(leaf_2_driver);
+ leaf_2->set_state(kapi::devices::state::bound);
+
+ WHEN("the bus is suspended")
+ {
+ auto result = kapi::devices::suspend_tree(*root);
+
+ THEN("it succeeds")
+ {
+ REQUIRE(result);
+ }
+
+ THEN("the leaves were suspended before the middle bus")
+ {
+ REQUIRE(log.size() == 3);
+
+ auto middle_index = index_of(log, "suspend:power_test_middle");
+ auto leaf_1_index = index_of(log, "suspend:power_test_leaf_1");
+ auto leaf_2_index = index_of(log, "suspend:power_test_leaf_2");
+
+ REQUIRE(leaf_1_index < middle_index);
+ REQUIRE(leaf_2_index < middle_index);
+ }
+
+ AND_WHEN("the bus is resumed")
+ {
+ kapi::devices::resume_tree(*root);
+
+ THEN("the leaves were resumed after the middle bus")
+ {
+ auto middle_index = index_of(log, "resume:power_test_middle");
+ auto leaf_1_index = index_of(log, "resume:power_test_leaf_1");
+ auto leaf_2_index = index_of(log, "resume:power_test_leaf_2");
+
+ REQUIRE(middle_index < leaf_1_index);
+ REQUIRE(middle_index < leaf_2_index);
+ }
+ }
+ }
+ }
+
+ GIVEN("a subtree where one leaf will fail to suspend")
+ {
+ auto log = kstd::vector<kstd::string>{};
+
+ auto root = kstd::make_shared<kapi::devices::bus>("power_test_root_failing");
+ auto middle = kstd::make_shared<kapi::devices::bus>("power_test_middle_failing");
+ auto leaf_1 = kstd::make_shared<test_device>("power_test_leaf_1_failing");
+ auto leaf_2 = kstd::make_shared<test_device>("power_test_leaf_2_failing");
+
+ auto middle_driver = kstd::make_shared<recording_driver>(log);
+ // leaf_1 suspends normally; leaf_2 is told to fail.
+ auto leaf_1_driver = kstd::make_shared<recording_driver>(log);
+ auto leaf_2_driver = kstd::make_shared<recording_driver>(log, kstd::string{"power_test_leaf_2_failing"});
+
+ root->add_child(middle);
+ middle->add_child(leaf_1);
+ middle->add_child(leaf_2);
+
+ middle->bind_driver(middle_driver);
+ middle->set_state(kapi::devices::state::bound);
+ leaf_1->bind_driver(leaf_1_driver);
+ leaf_1->set_state(kapi::devices::state::bound);
+ leaf_2->bind_driver(leaf_2_driver);
+ leaf_2->set_state(kapi::devices::state::bound);
+
+ WHEN("the subtree is suspended")
+ {
+ auto result = kapi::devices::suspend_tree(*root);
+
+ THEN("the failure is reported")
+ {
+ REQUIRE_FALSE(result);
+ }
+
+ THEN("the leaf that suspended successfully before the failure was resumed again")
+ {
+ REQUIRE(log.size() == 2);
+ REQUIRE(log[0] == "suspend:power_test_leaf_1_failing");
+ REQUIRE(log[1] == "resume:power_test_leaf_1_failing");
+ }
+
+ THEN("the middle bus, whose child failed to suspend, was never suspended at all")
+ {
+ REQUIRE(std::ranges::find(log, "suspend:power_test_middle_failing") == log.end());
+ }
+ }
+ }
+
+ GIVEN("a bus with a child that has no bound driver at all")
+ {
+ auto root = kstd::make_shared<kapi::devices::bus>("power_test_root_unbound");
+ auto unbound_child = kstd::make_shared<test_device>("power_test_unbound_child");
+
+ root->add_child(unbound_child);
+
+ THEN("suspending and resuming it does not crash, and simply does nothing for that device")
+ {
+ REQUIRE(kapi::devices::suspend_tree(*root));
+ REQUIRE_NOTHROW(kapi::devices::resume_tree(*root));
+ }
+ }
+} \ No newline at end of file