WIP2
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 10s
CI & Unit Tests / Docs (push) Successful in 14s

This commit is contained in:
Erki 2025-02-02 16:37:52 +02:00
parent 5af059f4c6
commit 4d897ad5c6
13 changed files with 621 additions and 135 deletions

View File

@ -11,15 +11,21 @@
namespace Peripherals::Hal
{
enum class Edge
{
Rising,
Falling
};
template<typename T>
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<bool>;
};
template<typename T>
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<bool>;
{ t.transmit(std::array<std::uint8_t, 10>{}) } -> std::same_as<bool>;
{ t.receive(data, len) } -> std::same_as<bool>;
@ -27,11 +33,11 @@ concept SerialInterface = requires (T t, std::uint8_t* data, const std::uint32_t
};
template<typename T, typename RegName>
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<std::uint8_t>;
t.readRegisterMultibyte(reg, data, len, std::uint32_t{});
};
}
}// namespace Peripherals::Hal

View File

@ -21,7 +21,7 @@ struct Gpio
Gpio() = delete;
explicit Gpio(gpio_num_t gpio, const bool inverted)
: gpio(gpio), inverted(inverted)
{ }
{}
void set(const bool& state)
{
@ -39,13 +39,13 @@ struct Gpio
static_assert(Peripherals::Hal::Gpio<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 */

View File

@ -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_ */

View File

@ -32,6 +32,7 @@ add_executable(tests
bytes.cpp
filters.cpp
coro.cpp
this_coro.cpp
)
target_link_libraries(tests

View File

@ -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 <semaphore>
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<uint32_t, std::milli>& duration = 10ms)
{
co_await skullc::coro::sleep(0ms, duration);
REQUIRE(expected == test_coro_called);
test_coro_called++;
co_return;
}
template<typename T>
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<std::vector> 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<uint32_t, std::milli> half_sleep = 5ms;
scheduler.loop(half_sleep.count());
REQUIRE(test_coro_called == 0);
const std::chrono::duration<uint32_t, std::milli> 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<std::vector> 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);
}
}
}

16
Tests/this_coro.cpp Normal file
View File

@ -0,0 +1,16 @@
//
// Created by erki on 30/01/25.
//
#include <catch2/catch.hpp>
#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<std::vector> scheduler;
skullc::this_coro::scheduler.register_scheduler(scheduler);
REQUIRE(&skullc::this_coro::scheduler() == &scheduler);
}

View File

@ -0,0 +1,86 @@
//
// Created by erki on 01/02/25.
//
#pragma once
#include <coroutine>
#include <optional>
#include "peripherals_hal_concepts.hpp"
#include "skullc/coro/this_coro.hpp"
#include "skullc/coro/scheduler.hpp"
namespace skullc::coro
{
template<Peripherals::Hal::Gpio G>
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<uint32_t, std::milli>&) 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<Peripherals::Hal::Edge> 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;
}
};
}

View File

@ -5,138 +5,119 @@
#pragma once
#include <algorithm>
#include <array>
#include <coroutine>
#include <cstdint>
#include <utility>
#include <vector>
#include "skullc/coro/task.hpp"
#include "skullc/coro/scheduler_base.hpp"
namespace skullc::coro
{
template <typename T>
concept Task = requires(T task)
{
template<typename T>
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<template<typename X> typename C>
class Scheduler final : public SchedulerBase
{
constexpr static auto cmp = [](const auto &a, const auto &b) {
constexpr static auto cmp = [](const auto& a, const auto& b) {
return a.first > b.first;
};
public:
Scheduler()
: Scheduler(16)
{ }
using Container = C<std::pair<uint32_t, std::coroutine_handle<>>>;
using PollerContainer = C<TaskPoller*>;
Scheduler() = default;
Scheduler(const Scheduler&) = delete;
Scheduler(Scheduler&&) = delete;
Scheduler &operator=(const Scheduler&) = delete;
Scheduler &operator=(Scheduler&&) = delete;
Scheduler& operator=(const Scheduler&) = delete;
Scheduler& operator=(Scheduler&&) = delete;
explicit Scheduler(uint num_tasks)
template<TaskType... T>
void start_tasks(T... tasks)
{
scheduled.reserve(num_tasks);
}
/** \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 <Task... T>
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)
void schedule(std::coroutine_handle<> handle, uint32_t when) override
{
scheduled.push_back({when, handle});
std::push_heap(scheduled.begin(), scheduled.end(), cmp);
scheduled_.push_back({when, handle});
std::push_heap(scheduled_.begin(), scheduled_.end(), cmp);
}
void loop(const uint64_t& current_time)
void loop(const uint32_t& current_time_ms)
{
if (!scheduled.empty())
loop(std::chrono::duration<uint32_t, std::milli>(current_time_ms));
}
void loop(const std::chrono::duration<uint32_t, std::milli>& current_time)
{
auto min_time = scheduled.front();
if (min_time.first < current_time)
if (!scheduled_.empty())
{
std::pop_heap(scheduled.begin(), scheduled.end(), cmp);
scheduled.pop_back();
auto min_time = scheduled_.front();
if (min_time.first <= current_time.count())
{
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)
for (auto* poller : pollers_)
{
if (poller->isReady())
if (poller->isReady(current_time))
{
schedule(poller->stored_coro, 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(),
pollers_.erase(std::remove_if(pollers_.begin(), pollers_.end(),
[](const auto* poller) { return poller->stored_coro == nullptr; }));
}
void add_poller(TaskPoller* poller)
void add_poller(TaskPoller* poller) override
{
pollers.push_back(poller);
pollers_.push_back(poller);
}
bool remove(std::coroutine_handle<> handle)
bool remove(std::coroutine_handle<> handle) override
{
for (auto &p : scheduled)
for (auto& p : scheduled_)
if (p.second == handle)
{
std::swap(p, scheduled.back());
scheduled.pop_back();
std::make_heap(scheduled.begin(), scheduled.end(), cmp);
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<std::pair<uint64_t, std::coroutine_handle<>>> scheduled;
std::vector<TaskPoller*> pollers;
Container scheduled_;
PollerContainer pollers_;
template <typename T0, typename... R>
void start(T0 &t0, R &...r)
template<typename T0, typename... R>
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) {
if constexpr (sizeof...(r) > 0)
{
start(r...);
}
}
};
}
}// namespace skullc::coro

View File

@ -0,0 +1,31 @@
//
// Created by erki on 02/02/25.
//
#pragma once
#include <coroutine>
#include <cstdint>
namespace skullc::coro
{
struct TaskPoller
{
virtual ~TaskPoller() = default;
virtual bool isReady(const std::chrono::duration<uint32_t, std::milli>& 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;
};
}

View File

@ -0,0 +1,59 @@
//
// Created by erki on 01/02/25.
//
#pragma once
#include <concepts>
#include <coroutine>
#include "skullc/coro/scheduler.hpp"
#include "skullc/coro/this_coro.hpp"
namespace skullc::coro
{
template<typename T>
requires requires (T t) { { t.try_acquire() } -> std::same_as<bool>; }
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<uint32_t, std::milli>&) override
{
return semaphore->try_acquire();
}
T* semaphore;
std::coroutine_handle<> continuation;
};
return Awaitable{&semaphore};
}
}

View File

@ -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<uint32_t, std::milli>& time_now, const std::chrono::duration<uint32_t, std::milli>& duration)
{
return detail::TimeAwaitable{time_now.count() + duration.count()};
}
}// namespace skullc::coro

View File

@ -12,25 +12,25 @@ namespace skullc::coro
{
template<typename T = void, bool lazy = true>
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<promise_type> handle)
explicit Task(std::coroutine_handle<promise_type> 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<T, void>)
{
@ -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<void>
template<typename T, bool lazy>
struct task<T, lazy>::task_promise : public promise_setter<T>
struct Task<T, lazy>::task_promise : public promise_setter<T>
{
task get_return_object() noexcept
Task get_return_object() noexcept
{
return task{std::coroutine_handle<task_promise>::from_promise(*this)};
return Task{std::coroutine_handle<task_promise>::from_promise(*this)};
};
auto initial_suspend() const noexcept
@ -134,7 +134,7 @@ struct task<T, lazy>::task_promise : public promise_setter<T>
void await_resume() const noexcept {}
std::coroutine_handle<>
await_suspend(std::coroutine_handle<task::task_promise> h) noexcept
await_suspend(std::coroutine_handle<Task::task_promise> h) noexcept
{
if constexpr (std::is_same_v<T, void>)
{

View File

@ -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