diff --git a/Peripherals/Inc/peripherals_hal_concepts.hpp b/Peripherals/Inc/peripherals_hal_concepts.hpp index 13edce9..05c989d 100644 --- a/Peripherals/Inc/peripherals_hal_concepts.hpp +++ b/Peripherals/Inc/peripherals_hal_concepts.hpp @@ -11,15 +11,21 @@ namespace Peripherals::Hal { +enum class Edge +{ + Rising, + Falling +}; + template -concept Gpio = requires (T t, const T ct) { +concept Gpio = requires(T t, const T ct) { t.set(true); t.toggle(); { ct.read() } -> std::same_as; }; template -concept SerialInterface = requires (T t, std::uint8_t* data, const std::uint32_t len) { +concept SerialInterface = requires(T t, std::uint8_t* data, const std::uint32_t len) { { t.transmit(data, len) } -> std::same_as; { t.transmit(std::array{}) } -> std::same_as; { t.receive(data, len) } -> std::same_as; @@ -27,11 +33,11 @@ concept SerialInterface = requires (T t, std::uint8_t* data, const std::uint32_t }; template -concept RegisterInterface = requires (T t, RegName reg, std::uint8_t* data, const std::uint32_t len) { +concept RegisterInterface = requires(T t, RegName reg, std::uint8_t* data, const std::uint32_t len) { t.writerRegister(reg, std::uint8_t{}); t.writeRegisterMultibyte(reg, data, len); { t.readRegister(reg, std::uint32_t{}) } -> std::same_as; t.readRegisterMultibyte(reg, data, len, std::uint32_t{}); }; -} +}// namespace Peripherals::Hal diff --git a/Peripherals/Inc/peripherals_hal_esp.hpp b/Peripherals/Inc/peripherals_hal_esp.hpp index 53130f2..a487520 100644 --- a/Peripherals/Inc/peripherals_hal_esp.hpp +++ b/Peripherals/Inc/peripherals_hal_esp.hpp @@ -20,8 +20,8 @@ struct Gpio Gpio() = delete; explicit Gpio(gpio_num_t gpio, const bool inverted) - : gpio(gpio), inverted(inverted) - { } + : gpio(gpio), inverted(inverted) + {} void set(const bool& state) { @@ -39,13 +39,13 @@ struct Gpio static_assert(Peripherals::Hal::Gpio); #define CREATE_GPIO(name) \ - Peripherals::Hal::Esp::Gpio{name, false} + Peripherals::Hal::Esp::Gpio { name, false } #define CREATE_INV_GPIO(name) \ - Peripherals::Hal::Esp::Gpio{name, true} + Peripherals::Hal::Esp::Gpio { name, true } -} +}// namespace Peripherals::Hal::Esp #else -# warning "ESP HAL included without SKULLC_USE_HAL_ESP being defined." +#warning "ESP HAL included without SKULLC_USE_HAL_ESP being defined." #endif /* SKULLC_USE_HAL_ESP */ diff --git a/Peripherals/Inc/peripherals_hal_st.hpp b/Peripherals/Inc/peripherals_hal_st.hpp index 499fdb0..fa034b9 100644 --- a/Peripherals/Inc/peripherals_hal_st.hpp +++ b/Peripherals/Inc/peripherals_hal_st.hpp @@ -61,8 +61,7 @@ struct StaticHal const std::uint32_t tick_start = DWT->CYCCNT; const std::uint32_t ticks_delay = micros * (SystemCoreClock / 1'000'000); - while (DWT->CYCCNT - tick_start < ticks_delay) - ; + while (DWT->CYCCNT - tick_start < ticks_delay); #else (void) micros; #endif @@ -346,7 +345,7 @@ struct ItmSerialInterface }// namespace Peripherals #else -# warning "ESP HAL included without SKULLC_USE_HAL_ESP being defined." +#warning "ST HAL included without SKULLC_USE_HAL_ST being defined." #endif /* SKULLC_USE_HAL_ST */ #endif /* SKULLC_PERIPHERALS_HAL_ST_HPP_ */ diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 430d92f..7cac236 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(tests bytes.cpp filters.cpp coro.cpp + this_coro.cpp ) target_link_libraries(tests diff --git a/Tests/coro.cpp b/Tests/coro.cpp index 7a74ab1..e45d49a 100644 --- a/Tests/coro.cpp +++ b/Tests/coro.cpp @@ -6,4 +6,230 @@ #include "skullc/coro/task.hpp" #include "skullc/coro/scheduler.hpp" +#include "skullc/coro/this_coro.hpp" +#include "skullc/coro/sleep.hpp" +#include "skullc/coro/semaphore.hpp" +#include + +namespace +{ + +int test_coro_called = 0; + +using namespace std::chrono_literals; + +skullc::coro::Task<> test_coro(const int expected) +{ + REQUIRE(expected == test_coro_called); + test_coro_called++; + co_return; +} + +skullc::coro::Task<> test_sleepy_coro(const int expected, const std::chrono::duration& duration = 10ms) +{ + co_await skullc::coro::sleep(0ms, duration); + REQUIRE(expected == test_coro_called); + test_coro_called++; + co_return; +} + +template +skullc::coro::Task<> test_semaphore_coro(T* semaphore, const int expected) +{ + co_await skullc::coro::await_semaphore(*semaphore); + REQUIRE(expected == test_coro_called); + test_coro_called++; + co_return; +} + +} + +TEST_CASE("Scheduler runs coroutines.", "[coro]") +{ + using namespace skullc::coro; + + + Scheduler scheduler; + skullc::this_coro::scheduler.register_scheduler(scheduler); + + test_coro_called = 0; + + SECTION("With a single coroutine.") + { + scheduler.start_tasks(test_coro(0)); + scheduler.loop(0); + + REQUIRE(test_coro_called == 1); + } + + + SECTION("With two coroutines.") + { + scheduler.start_tasks(test_coro(0), test_coro(1)); + scheduler.loop(0); + + REQUIRE(test_coro_called == 2); + } + + SECTION("With a single sleepy coroutine.") + { + using namespace std::chrono_literals; + + scheduler.start_tasks(test_sleepy_coro(0)); + scheduler.loop(0); + + REQUIRE(test_coro_called == 0); + + const std::chrono::duration half_sleep = 5ms; + scheduler.loop(half_sleep.count()); + REQUIRE(test_coro_called == 0); + + const std::chrono::duration full_sleep = 11ms; + scheduler.loop(full_sleep.count()); + REQUIRE(test_coro_called == 1); + } + + SECTION("With a semaphore awaiter.") + { + std::binary_semaphore semaphore{0}; + std::counting_semaphore<10> counting_semaphore{0}; + + scheduler.start_tasks( + test_semaphore_coro(&semaphore, 0), + test_semaphore_coro(&counting_semaphore, 1) + ); + scheduler.loop(0); + + REQUIRE(test_coro_called == 0); + + semaphore.release(1); + scheduler.loop(1); + scheduler.loop(2); + + REQUIRE(test_coro_called == 1); + + counting_semaphore.release(1); + scheduler.loop(3); + scheduler.loop(4); + + REQUIRE(test_coro_called == 2); + } + + SECTION("Coros scheduled for the same millisecond get executed with the same time.") + { + scheduler.start_tasks( + test_sleepy_coro(0, std::chrono::milliseconds{2}), + test_sleepy_coro(1, std::chrono::milliseconds{2}) + ); + + scheduler.loop(0); + scheduler.loop(0); + scheduler.loop(0); + + REQUIRE(test_coro_called == 0); + + scheduler.loop(2); + scheduler.loop(2); + + REQUIRE(test_coro_called == 2); + } +} + +#include "skullc/coro/peripheral_awaiters.hpp" + +namespace +{ + +struct TestGpio +{ + bool is_set = false; + + void set(const bool value) + { + is_set = value; + } + + void toggle() + { + is_set = !is_set; + } + + bool read() const + { + return is_set; + } +}; + +skullc::coro::Task<> await_pin_edge(TestGpio* pin, const Peripherals::Hal::Edge edge) +{ + co_await skullc::coro::PinEdgeAwaiter(pin, edge); + test_coro_called = 1; + + co_return; +} + +} + +TEST_CASE("Peripheral awaiters work.", "[coro],[awaiters]") +{ + using namespace skullc::coro; + Scheduler scheduler; + skullc::this_coro::scheduler.register_scheduler(scheduler); + + test_coro_called = 0; + + SECTION("Pin edge awaiter works with rising edge.") + { + TestGpio gpio{ false }; + scheduler.start_tasks(await_pin_edge(&gpio, Peripherals::Hal::Edge::Rising)); + scheduler.loop(0); + + REQUIRE(test_coro_called == 0); + + SECTION("Keeping low generates no edge.") + { + scheduler.loop(1); + scheduler.loop(2); + scheduler.loop(3); + + REQUIRE(test_coro_called == 0); + } + + SECTION("Raising generated a rising edge.") + { + gpio.is_set = true; + scheduler.loop(1); + scheduler.loop(2); + + REQUIRE(test_coro_called == 1); + } + } + + SECTION("Pin edge awaiter works with falling edge.") + { + TestGpio gpio{ true }; + scheduler.start_tasks(await_pin_edge(&gpio, Peripherals::Hal::Edge::Falling)); + scheduler.loop(0); + + REQUIRE(test_coro_called == 0); + + SECTION("Keeping high generates no edge.") + { + scheduler.loop(1); + scheduler.loop(2); + scheduler.loop(3); + + REQUIRE(test_coro_called == 0); + } + + SECTION("Falling generated a falling edge.") + { + gpio.is_set = false; + scheduler.loop(1); + scheduler.loop(2); + + REQUIRE(test_coro_called == 1); + } + } +} diff --git a/Tests/this_coro.cpp b/Tests/this_coro.cpp new file mode 100644 index 0000000..a2482e4 --- /dev/null +++ b/Tests/this_coro.cpp @@ -0,0 +1,16 @@ +// +// Created by erki on 30/01/25. +// + +#include + +#include "skullc/coro/scheduler.hpp" +#include "skullc/coro/this_coro.hpp" + +TEST_CASE("this_coro returns the correct scheduler.", "[coro],[this_coro]") +{ + skullc::coro::Scheduler scheduler; + skullc::this_coro::scheduler.register_scheduler(scheduler); + + REQUIRE(&skullc::this_coro::scheduler() == &scheduler); +} diff --git a/coro/inc/skullc/coro/peripheral_awaiters.hpp b/coro/inc/skullc/coro/peripheral_awaiters.hpp new file mode 100644 index 0000000..5a48c58 --- /dev/null +++ b/coro/inc/skullc/coro/peripheral_awaiters.hpp @@ -0,0 +1,86 @@ +// +// Created by erki on 01/02/25. +// + +#pragma once + +#include +#include + +#include "peripherals_hal_concepts.hpp" + +#include "skullc/coro/this_coro.hpp" +#include "skullc/coro/scheduler.hpp" + +namespace skullc::coro +{ + +template +struct PinEdgeAwaiter : TaskPoller +{ + PinEdgeAwaiter() = delete; + explicit PinEdgeAwaiter(G* pin, const Peripherals::Hal::Edge& edge) + : pin(pin) + , awaited_edge(edge) + { + if (awaited_edge == Peripherals::Hal::Edge::Rising) + previous_is_set = false; + else + previous_is_set = true; + } + + ~PinEdgeAwaiter() override + { + if (continuation) + { + this_coro::scheduler().remove(continuation); + } + } + + bool await_ready() { return false; } + void await_suspend(std::coroutine_handle<> h) + { + continuation = h; + stored_coro = continuation; + skullc::this_coro::scheduler().add_poller(this); + } + auto await_resume() + { + continuation = {}; + } + + bool isReady(const std::chrono::duration&) override + { + const bool is_set = pin->read(); + const auto found_edge = calculateEdge(is_set); + + if (found_edge && *found_edge == awaited_edge) + { + return true; + } + else + { + previous_is_set = is_set; + return false; + } + } + + G* pin; + Peripherals::Hal::Edge awaited_edge; + std::coroutine_handle<> continuation; + +private: + bool previous_is_set; + + std::optional calculateEdge(const bool new_is_set) const + { + if (new_is_set && !previous_is_set) + return Peripherals::Hal::Edge::Rising; + else if (!new_is_set && previous_is_set) + return Peripherals::Hal::Edge::Falling; + else + return std::nullopt; + } +}; + +} diff --git a/coro/inc/skullc/coro/scheduler.hpp b/coro/inc/skullc/coro/scheduler.hpp index 4732332..03416e8 100644 --- a/coro/inc/skullc/coro/scheduler.hpp +++ b/coro/inc/skullc/coro/scheduler.hpp @@ -5,138 +5,119 @@ #pragma once #include -#include #include #include #include -#include #include "skullc/coro/task.hpp" +#include "skullc/coro/scheduler_base.hpp" namespace skullc::coro { -template -concept Task = requires(T task) -{ - { - task.get_handle().resume() - }; +template +concept TaskType = requires(T task) { + { + task.get_handle().resume() + }; }; -struct TaskPoller { - virtual ~TaskPoller() = default; - - virtual bool isReady() = 0; - std::coroutine_handle<> stored_coro; -}; - -class Scheduler +template typename C> +class Scheduler final : public SchedulerBase { - constexpr static auto cmp = [](const auto &a, const auto &b) { - return a.first > b.first; - }; + constexpr static auto cmp = [](const auto& a, const auto& b) { + return a.first > b.first; + }; public: - Scheduler() - : Scheduler(16) - { } + using Container = C>>; + using PollerContainer = C; - Scheduler(const Scheduler&) = delete; - Scheduler(Scheduler&&) = delete; - Scheduler &operator=(const Scheduler&) = delete; - Scheduler &operator=(Scheduler&&) = delete; + Scheduler() = default; - explicit Scheduler(uint num_tasks) - { - scheduled.reserve(num_tasks); - } + Scheduler(const Scheduler&) = delete; + Scheduler(Scheduler&&) = delete; + Scheduler& operator=(const Scheduler&) = delete; + Scheduler& operator=(Scheduler&&) = delete; - /** \brief Start a new task. The task can terminate by calling co_return; . */ - void start_task(task<> task, uint64_t when = 0) - { - schedule(task.get_handle(), when); - } - - template - void start_tasks(T... tasks) { - start(tasks...); - } - - /** \brief Schedule a coroutine for future restart. - * \details This routine should only be called on the same core and from - * regular execution code. Do not call this from an interrupt. - * @param handle - * @param when - */ - void schedule(std::coroutine_handle<> handle, uint64_t when) - { - scheduled.push_back({when, handle}); - std::push_heap(scheduled.begin(), scheduled.end(), cmp); - } - - void loop(const uint64_t& current_time) - { - if (!scheduled.empty()) - { - auto min_time = scheduled.front(); - if (min_time.first < current_time) - { - std::pop_heap(scheduled.begin(), scheduled.end(), cmp); - scheduled.pop_back(); - if (min_time.second) - min_time.second.resume(); - } - } - - bool pollers_need_updating = false; - for (auto* poller : pollers) - { - if (poller->isReady()) - { - schedule(poller->stored_coro, current_time); - poller->stored_coro = nullptr; - pollers_need_updating = true; - } - } - - if (pollers_need_updating) - pollers.erase(std::remove_if(pollers.begin(), pollers.end(), - [](const auto* poller) { return poller->stored_coro == nullptr; })); - } - - void add_poller(TaskPoller* poller) + template + void start_tasks(T... tasks) { - pollers.push_back(poller); - } + start(tasks...); + } - bool remove(std::coroutine_handle<> handle) - { - for (auto &p : scheduled) - if (p.second == handle) + void schedule(std::coroutine_handle<> handle, uint32_t when) override + { + scheduled_.push_back({when, handle}); + std::push_heap(scheduled_.begin(), scheduled_.end(), cmp); + } + + void loop(const uint32_t& current_time_ms) + { + loop(std::chrono::duration(current_time_ms)); + } + + void loop(const std::chrono::duration& current_time) + { + if (!scheduled_.empty()) + { + auto min_time = scheduled_.front(); + if (min_time.first <= current_time.count()) { - std::swap(p, scheduled.back()); - scheduled.pop_back(); - std::make_heap(scheduled.begin(), scheduled.end(), cmp); - return true; - } - return false; - } + std::pop_heap(scheduled_.begin(), scheduled_.end(), cmp); + scheduled_.pop_back(); + if (min_time.second) + min_time.second.resume(); + } + } + + bool pollers_need_updating = false; + for (auto* poller : pollers_) + { + if (poller->isReady(current_time)) + { + schedule(poller->stored_coro, current_time.count()); + poller->stored_coro = nullptr; + pollers_need_updating = true; + } + } + + if (pollers_need_updating) + pollers_.erase(std::remove_if(pollers_.begin(), pollers_.end(), + [](const auto* poller) { return poller->stored_coro == nullptr; })); + } + + void add_poller(TaskPoller* poller) override + { + pollers_.push_back(poller); + } + + bool remove(std::coroutine_handle<> handle) override + { + for (auto& p : scheduled_) + if (p.second == handle) + { + std::swap(p, scheduled_.back()); + scheduled_.pop_back(); + std::make_heap(scheduled_.begin(), scheduled_.end(), cmp); + return true; + } + return false; + } private: - /// \brief Heap of scheduled coroutines. Top of the heap is earliest time - std::vector>> scheduled; - std::vector pollers; + Container scheduled_; + PollerContainer pollers_; - template - void start(T0 &t0, R &...r) - { - /// Start the first task. As soon as it co_await anything, it will re-queue itself. - t0.get_handle().resume(); - if constexpr (sizeof...(r) > 0) { - start(r...); - } - } + template + void start(T0& t0, R&... r) + { + t0.get_handle().resume(); + if constexpr (sizeof...(r) > 0) + { + start(r...); + } + } }; -} \ No newline at end of file +}// namespace skullc::coro \ No newline at end of file diff --git a/coro/inc/skullc/coro/scheduler_base.hpp b/coro/inc/skullc/coro/scheduler_base.hpp new file mode 100644 index 0000000..0fab197 --- /dev/null +++ b/coro/inc/skullc/coro/scheduler_base.hpp @@ -0,0 +1,31 @@ +// +// Created by erki on 02/02/25. +// + +#pragma once + +#include +#include + +namespace skullc::coro +{ + +struct TaskPoller +{ + virtual ~TaskPoller() = default; + + virtual bool isReady(const std::chrono::duration& current_time) = 0; + std::coroutine_handle<> stored_coro; +}; + +class SchedulerBase +{ +public: + virtual ~SchedulerBase() = default; + + virtual void schedule(std::coroutine_handle<> handle, uint32_t when_ms) = 0; + virtual bool remove(std::coroutine_handle<> handle) = 0; + virtual void add_poller(TaskPoller* poller) = 0; +}; + +} \ No newline at end of file diff --git a/coro/inc/skullc/coro/semaphore.hpp b/coro/inc/skullc/coro/semaphore.hpp new file mode 100644 index 0000000..dc7e79e --- /dev/null +++ b/coro/inc/skullc/coro/semaphore.hpp @@ -0,0 +1,59 @@ +// +// Created by erki on 01/02/25. +// + +#pragma once + +#include +#include + +#include "skullc/coro/scheduler.hpp" +#include "skullc/coro/this_coro.hpp" + +namespace skullc::coro +{ + +template + requires requires (T t) { { t.try_acquire() } -> std::same_as; } +auto await_semaphore(T& semaphore) +{ + struct Awaitable : TaskPoller + { + Awaitable() = delete; + explicit Awaitable(T* semaphore) + : semaphore{semaphore} + {} + + ~Awaitable() override + { + if (continuation) + { + this_coro::scheduler().remove(continuation); + } + } + + bool await_ready() { return false; } + void await_suspend(std::coroutine_handle<> h) + { + continuation = h; + stored_coro = continuation; + this_coro::scheduler().add_poller(this); + } + auto await_resume() + { + continuation = {}; + } + + bool isReady(const std::chrono::duration&) override + { + return semaphore->try_acquire(); + } + + T* semaphore; + std::coroutine_handle<> continuation; + }; + + return Awaitable{&semaphore}; +} + +} diff --git a/coro/inc/skullc/coro/sleep.hpp b/coro/inc/skullc/coro/sleep.hpp new file mode 100644 index 0000000..b863114 --- /dev/null +++ b/coro/inc/skullc/coro/sleep.hpp @@ -0,0 +1,51 @@ +// +// Created by erki on 01/02/25. +// + +#pragma once + +#include "skullc/coro/scheduler.hpp" +#include "skullc/coro/this_coro.hpp" + +namespace skullc::coro +{ + +namespace detail +{ + +struct TimeAwaitable +{ + ~TimeAwaitable() + { + if (continuation) + { + this_coro::scheduler().remove(continuation); + } + } + + bool await_ready() { return false; } + + void await_suspend(std::coroutine_handle<> h) + { + continuation = h; + this_coro::scheduler().schedule(h, resume_time); + } + + auto await_resume() + { + continuation = {}; + return resume_time; + } + + uint32_t resume_time; + std::coroutine_handle<> continuation{}; +}; + +}// namespace detail + +inline auto sleep(const std::chrono::duration& time_now, const std::chrono::duration& duration) +{ + return detail::TimeAwaitable{time_now.count() + duration.count()}; +} + +}// namespace skullc::coro diff --git a/coro/inc/skullc/coro/task.hpp b/coro/inc/skullc/coro/task.hpp index 9c1d53a..1bfcaa0 100644 --- a/coro/inc/skullc/coro/task.hpp +++ b/coro/inc/skullc/coro/task.hpp @@ -12,25 +12,25 @@ namespace skullc::coro { template -struct [[nodiscard]] task +struct [[nodiscard]] Task { using value_type = T; struct task_promise; using promise_type = task_promise; - task() = delete; + Task() = delete; - explicit task(std::coroutine_handle handle) + explicit Task(std::coroutine_handle handle) : my_handle(handle) { } - task(task&& t) noexcept : my_handle(t.my_handle) { t.my_handle = nullptr; } + Task(Task&& t) noexcept : my_handle(t.my_handle) { t.my_handle = nullptr; } - task(const task&) = delete; - task& operator=(const task&) = delete; + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; - ~task() + ~Task() { if constexpr (!std::is_same_v) { @@ -42,7 +42,7 @@ struct [[nodiscard]] task auto get_handle() const { return my_handle; } void detach() { my_handle = nullptr; } - task& operator=(task&& other) noexcept + Task& operator=(Task&& other) noexcept { if (std::addressof(other) != this) { @@ -109,12 +109,12 @@ struct promise_setter template -struct task::task_promise : public promise_setter +struct Task::task_promise : public promise_setter { - task get_return_object() noexcept + Task get_return_object() noexcept { - return task{std::coroutine_handle::from_promise(*this)}; + return Task{std::coroutine_handle::from_promise(*this)}; }; auto initial_suspend() const noexcept @@ -134,7 +134,7 @@ struct task::task_promise : public promise_setter void await_resume() const noexcept {} std::coroutine_handle<> - await_suspend(std::coroutine_handle h) noexcept + await_suspend(std::coroutine_handle h) noexcept { if constexpr (std::is_same_v) { diff --git a/coro/inc/skullc/coro/this_coro.hpp b/coro/inc/skullc/coro/this_coro.hpp new file mode 100644 index 0000000..a7878eb --- /dev/null +++ b/coro/inc/skullc/coro/this_coro.hpp @@ -0,0 +1,30 @@ +// +// Created by erki on 30/01/25. +// + +#pragma once + +#include "skullc/coro/scheduler_base.hpp" + +namespace skullc::this_coro +{ + +struct SchedulerGetter +{ + void register_scheduler(coro::SchedulerBase& scheduler) + { + scheduler_ = &scheduler; + } + + coro::SchedulerBase& operator()() + { + return *scheduler_; + } + +private: + coro::SchedulerBase* scheduler_ = nullptr; +}; + +inline static SchedulerGetter scheduler; + +}// namespace skullc::this_coro