Compare commits

...

10 Commits

Author SHA1 Message Date
Erki
f72a4e20c5 WIP10: maybe fixing CI isn't that hard
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 50s
CI & Unit Tests / Docs (push) Successful in 10s
2025-02-28 13:28:59 +02:00
Erki
acbdbf4f7f WIP9: some improvements
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 8s
CI & Unit Tests / Docs (push) Successful in 10s
2025-02-28 08:51:05 +02:00
Erki
5e5aee38dc WIP8: compositional awaiters are done
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 10s
CI & Unit Tests / Docs (push) Successful in 11s
we are. Almost done
2025-02-27 22:03:11 +02:00
Erki
0107d3e6e7 WIP7: Add async serial IO support
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 8s
CI & Unit Tests / Docs (push) Successful in 10s
2025-02-24 18:24:30 +02:00
Erki
105a387efc WIP6: Fix Button struct continuously reading a long press 2025-02-24 18:24:16 +02:00
Erki
08c23d6244 WIP5: format 2025-02-24 18:23:57 +02:00
Erki
8d49d2d446 WIP4: Add Signal entity
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 18s
CI & Unit Tests / Docs (push) Successful in 12s
2025-02-21 21:19:42 +02:00
Erki
66461af1e4 WIP3
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 13s
CI & Unit Tests / Docs (push) Successful in 10s
2025-02-06 21:27:51 +02:00
Erki
4d897ad5c6 WIP2
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 10s
CI & Unit Tests / Docs (push) Successful in 14s
2025-02-02 16:37:52 +02:00
Erki
5af059f4c6 WIP1
Some checks failed
CI & Unit Tests / Unit-Tests (push) Failing after 21s
CI & Unit Tests / Docs (push) Successful in 14s
2025-01-30 17:22:37 +02:00
22 changed files with 1596 additions and 29 deletions

View File

@ -14,6 +14,7 @@ jobs:
cmake . -B${{runner.workspace}}/build \
-G"Ninja" \
-DCMAKE_BUILD_TYPE=Release \
-DSKULLC_WITH_CORO=ON \
-DSKULLC_WITH_TESTS=ON
- name: Build tests + lib
@ -34,6 +35,7 @@ jobs:
cmake . -B${{runner.workspace}}/build \
-G"Ninja" \
-DCMAKE_BUILD_TYPE=Release \
-DSKULLC_WITH_CORO=ON \
-DSKULLC_WITH_DOCS=ON
- name: Build docs

View File

@ -16,10 +16,15 @@ option(SKULLC_WITH_TESTS "Enable unit testing." OFF)
option(SKULLC_WITH_HAL "Enable the compiling and deployment of the HAL dependent sections." OFF)
option(SKULLC_USE_HAL_ST "Enable the ST HAl when SKULLC_WITH_HAL is enabled." OFF)
option(SKULLC_USE_HAL_ESP "Enable the ESP HAL when SKULLC_WITH_HAL is enabled." OFF)
option(SKULLC_WITH_CORO "Enable coroutine support library." OFF)
option(SKULLC_WITH_DOCS "Enable documentation building." OFF)
include(skullc-install)
if(SKULLC_WITH_CORO)
add_subdirectory(coro)
endif()
add_subdirectory(Peripherals)
add_subdirectory(Utility)
add_subdirectory(Messaging)

View File

@ -10,5 +10,20 @@ target_include_directories(peripherals
$<INSTALL_INTERFACE:include>
)
if (DEFINED SKULLC_USE_HAL_ST)
set(PERIPHERALS_DEFINITIONS SKULLC_USE_HAL_ST)
endif ()
if (DEFINED SKULLC_USE_HAL_ESP)
set(PERIPHERALS_DEFINITIONS SKULLC_USE_HAL_ESP)
endif ()
if (DEFINED SKULLC_WITH_CORO)
list(APPEND PERIPHERALS_DEFINITIONS SKULLC_WITH_CORO)
endif ()
target_compile_definitions(peripherals
INTERFACE
${PERIPHERALS_DEFINITIONS}
)
## INSTALL
skullc_install_packages(skullc peripherals ${SKULLC_VERSION})

View File

@ -10,6 +10,14 @@
#include <cstdint>
#include "peripherals_hal_concepts.hpp"
#ifdef SKULLC_WITH_CORO
#include "skullc/coro/sleep.hpp"
#include "skullc/coro/task.hpp"
#include "skullc/coro/this_coro.hpp"
#endif
namespace Peripherals
{
@ -20,7 +28,7 @@ enum class ButtonPress : std::uint32_t
LONG_PRESS
};
template<typename G, typename H>
template<Hal::Gpio G, Hal::StaticHal H>
class Button
{
public:
@ -34,7 +42,7 @@ public:
Button() = delete;
explicit Button(const gpio& sw)
: sw(sw)
: sw(sw), was_pressed_(sw.read())
{}
void update()
@ -51,7 +59,15 @@ public:
else if (!is_pressed && was_pressed_)
{
if (time_held > TIMEOUT_SHORT_PRESS && time_held <= TIMEOUT_LONG_PRESS)
{
new_state = ButtonPress::SHORT_PRESS;
}
if (state_exhausted_)
{
current_state_ = ButtonPress::NOT_PRESSED;
state_exhausted_ = false;
}
time_pressed_down_ = 0;
}
@ -62,18 +78,48 @@ public:
}
was_pressed_ = is_pressed;
current_state_ = new_state;
if (!state_exhausted_)
current_state_ = new_state;
}
[[nodiscard]] ButtonPress getState() const
{
return current_state_;
if (!state_exhausted_)
{
if (current_state_ != ButtonPress::NOT_PRESSED && current_state_ != ButtonPress::SHORT_PRESS)
state_exhausted_ = true;
return current_state_;
}
else
{
return ButtonPress::NOT_PRESSED;
}
}
#ifdef SKULLC_WITH_CORO
skullc::coro::Task<ButtonPress> await_press()
{
ButtonPress result = ButtonPress::NOT_PRESSED;
while (result == ButtonPress::NOT_PRESSED)
{
update();
result = getState();
if (result == ButtonPress::NOT_PRESSED)
co_await skullc::coro::sleep(std::chrono::milliseconds(hal::getMillis()), std::chrono::milliseconds(1));
}
co_return result;
}
#endif
private:
bool was_pressed_ = false;
bool was_pressed_;
std::uint32_t time_pressed_down_ = 0;
ButtonPress current_state_ = ButtonPress::NOT_PRESSED;
mutable bool state_exhausted_ = false;
};
}// namespace Peripherals

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,16 @@ 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{});
};
}
template<typename T>
concept StaticHal = requires() {
{ T::getMillis() } -> std::same_as<std::uint32_t>;
};
}// namespace Peripherals::Hal

View File

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

@ -14,6 +14,10 @@
#include <array>
#ifdef SKULLC_WITH_CORO
#include "skullc/coro/peripheral_awaiters.hpp"
#endif
#define USE_DELAY_US
namespace Peripherals
@ -27,7 +31,7 @@ template<typename Origin>
using IsrCallbackFn = void (*)(Origin*);
template<typename Origin, typename Handler, void (Handler::*func)(),
typename Tag>
typename Tag = decltype([] {})>
IsrCallbackFn<Origin> createCallback(Handler& h_in)
{
static Handler* h = &h_in;
@ -61,8 +65,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
@ -93,6 +96,13 @@ struct Gpio
void toggle() { HAL_GPIO_TogglePin(port, pin); }
bool read() const { return inverted ? !bool(HAL_GPIO_ReadPin(port, pin)) : bool(HAL_GPIO_ReadPin(port, pin)); }
#ifdef SKULLC_WITH_CORO
auto await_edge(const Edge& edge)
{
return skullc::coro::PinEdgeAwaiter(this, edge);
}
#endif
};
#define CREATE_GPIO(name) \
@ -190,6 +200,104 @@ struct SerialInterfaceAsync
}
};
#ifdef SKULLC_WITH_CORO
template<typename T,
HAL_StatusTypeDef (*transmit_)(T*, std::uint8_t* data,
std::uint16_t data_len),
HAL_StatusTypeDef (*receive_)(T*, std::uint8_t* data,
std::uint16_t data_len)>
struct SerialInterfaceCoro
{
private:
void on_rx_complete()
{
if (rx_awaiter_)
rx_awaiter_->set_completed();
}
void on_tx_complete()
{
if (tx_awaiter_)
tx_awaiter_->set_completed();
}
std::optional<skullc::coro::ISRAwaiter> rx_awaiter_ = std::nullopt;
std::optional<skullc::coro::ISRAwaiter> tx_awaiter_ = std::nullopt;
public:
using underlying_handle_type = T;
underlying_handle_type* handle;
SerialInterfaceCoro() = delete;
template<typename = decltype([] {})>
explicit SerialInterfaceCoro(underlying_handle_type* handle)
: handle(handle)
{
handle->RxCpltCallback = createCallback<T,
std::decay_t<decltype(*this)>,
&std::decay_t<decltype(*this)>::on_rx_complete>(*this);
handle->TxCpltCallback = createCallback<T,
std::decay_t<decltype(*this)>,
&std::decay_t<decltype(*this)>::on_tx_complete>(*this);
}
skullc::coro::Task<bool> transmit(std::uint8_t* data, const std::uint32_t data_len)
{
tx_awaiter_.emplace();
const auto status = transmit_(handle, data, data_len);
if (status != HAL_StatusTypeDef::HAL_OK)
co_return false;
co_await *tx_awaiter_;
tx_awaiter_ = std::nullopt;
co_return true;
}
template<typename Td, std::size_t N>
skullc::coro::Task<bool> transmit(std::array<Td, N>& array)
{
static_assert(sizeof(Td) == sizeof(std::uint8_t), "Data is not a byte large.");
return transmit(reinterpret_cast<std::uint8_t*>(array.data()), std::uint32_t(N));
}
skullc::coro::Task<bool> receive(std::uint8_t* data, const std::uint32_t data_len)
{
rx_awaiter_.emplace();
const auto status = receive_(handle, data, data_len);
if (status != HAL_StatusTypeDef::HAL_OK)
co_return false;
co_await *rx_awaiter_;
rx_awaiter_ = std::nullopt;
co_return true;
}
template<typename Td, std::size_t N>
skullc::coro::Task<bool> receive(std::array<Td, N>& array)
{
static_assert(sizeof(Td) == sizeof(std::uint8_t), "Data is not a byte large.");
return receive(reinterpret_cast<std::uint8_t*>(array.data()), std::uint32_t(N));
}
template<std::size_t N>
skullc::coro::Task<std::array<std::uint8_t, N>> receive()
{
std::array<std::uint8_t, N> data;
data.fill(0);
co_await receive(data.data(), std::uint32_t(N));
co_return data;
}
};
#endif// SKULLC_WITH_CORO
#ifdef HAL_SPI_MODULE_ENABLED
using SpiInterface =
@ -286,6 +394,11 @@ inline HAL_StatusTypeDef uartTransmitDma(UART_HandleTypeDef* huart, std::uint8_t
return HAL_UART_Transmit_DMA(huart, data, size);
}
inline HAL_StatusTypeDef uartTransmitIt(UART_HandleTypeDef* huart, std::uint8_t* data, const std::uint16_t size)
{
return HAL_UART_Transmit_IT(huart, data, size);
}
}// namespace _Details
using UartInterface =
@ -293,6 +406,14 @@ using UartInterface =
using UartInterfaceDMA =
SerialInterfaceAsync<UART_HandleTypeDef, _Details::uartTransmitDma,
HAL_UART_Receive_DMA>;
#ifdef SKULLC_WITH_CORO
using UartInterfaceCoro =
SerialInterfaceCoro<UART_HandleTypeDef, _Details::uartTransmitIt,
HAL_UART_Receive_IT>;
using UartInterfaceCoroDma =
SerialInterfaceCoro<UART_HandleTypeDef, _Details::uartTransmitDma,
HAL_UART_Receive_DMA>;
#endif// SKULLC_WITH_CORO
#endif// HAL_UART_MODULE_ENABLED
@ -346,7 +467,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

@ -31,19 +31,22 @@ add_executable(tests
enum_helpers.cpp
bytes.cpp
filters.cpp
coro.cpp
this_coro.cpp
)
target_link_libraries(tests
PUBLIC
skullc::utility
skullc::messaging
skullc::coro
skullc::peripherals
Catch2::Catch2
)
set_target_properties(tests
PROPERTIES
CXX_STANDARD 17
CXX_STANDARD 23
)
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/contrib)

View File

@ -33,20 +33,26 @@ std::uint32_t HAL::millis = 1;
struct Gpio
{
static bool set;
static bool is_set;
Gpio()
{
set = false;
is_set = false;
}
bool read()
bool read() const
{
return set;
return is_set;
}
void set(const bool value)
{}
void toggle()
{}
};
bool Gpio::set = false;
bool Gpio::is_set = false;
}// namespace
@ -75,13 +81,13 @@ TEST_CASE("Button reads presses properly.", "[peripherals],[button]")
Button button{g};
Gpio::set = true;
Gpio::is_set = true;
button.update();
SECTION("Too short of a press is debounced.")
{
HAL::millis = Button::TIMEOUT_SHORT_PRESS - 1;
Gpio::set = false;
Gpio::is_set = false;
button.update();
@ -91,7 +97,7 @@ TEST_CASE("Button reads presses properly.", "[peripherals],[button]")
SECTION("Short press is identified properly.")
{
HAL::millis = Button::TIMEOUT_SHORT_PRESS + 2;
Gpio::set = false;
Gpio::is_set = false;
button.update();
@ -124,7 +130,7 @@ TEST_CASE("Button reads presses properly.", "[peripherals],[button]")
SECTION("Releasing keeps NOT_PRESSED state.")
{
HAL::millis += 1;
Gpio::set = false;
Gpio::is_set = false;
button.update();

409
Tests/coro.cpp Normal file
View File

@ -0,0 +1,409 @@
//
// Created by erki on 30/01/25.
//
#include <catch2/catch.hpp>
#include "skullc/coro/scheduler.hpp"
#include "skullc/coro/semaphore.hpp"
#include "skullc/coro/signal.hpp"
#include "skullc/coro/sleep.hpp"
#include "skullc/coro/task.hpp"
#include "skullc/coro/this_coro.hpp"
#include "skullc/coro/composition.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;
}
}// namespace
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_edge(TestGpio* pin, const Peripherals::Hal::Edge edge)
{
co_await skullc::coro::PinEdgeAwaiter(pin, edge);
test_coro_called = 1;
co_return;
}
}// namespace
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_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_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);
}
}
}
namespace
{
skullc::coro::Task<> await_signal(const int expected, skullc::coro::Signal<int>* signal)
{
const int value = co_await signal->receive();
REQUIRE(value == expected);
test_coro_called = 1;
co_return;
}
skullc::coro::Task<> await_signal_n(const auto& expecteds, skullc::coro::Signal<int>* signal)
{
for (const int e : expecteds)
{
const int value = co_await signal->receive();
REQUIRE(value == e);
test_coro_called++;
}
co_return;
}
skullc::coro::Task<> send_signal(const int value, skullc::coro::Signal<int>* signal)
{
co_await skullc::coro::sleep(0ms, 10ms);
signal->send(value);
co_return;
}
}// namespace
TEST_CASE("Signal awaiters work.", "[coro],[signal]")
{
using namespace skullc::coro;
Scheduler<std::vector> scheduler;
skullc::this_coro::scheduler.register_scheduler(scheduler);
test_coro_called = 0;
Signal<int> signal;
SECTION("Sending from main thread.")
{
scheduler.start_tasks(await_signal(10, &signal));
scheduler.loop(0);
scheduler.loop(1);
REQUIRE(test_coro_called == 0);
signal.send(10);
scheduler.loop(2);
REQUIRE(test_coro_called == 1);
}
SECTION("Sending from another coroutine.")
{
scheduler.start_tasks(await_signal(10, &signal), send_signal(10, &signal));
scheduler.loop(0);
scheduler.loop(1);
REQUIRE(test_coro_called == 0);
scheduler.loop(10);
REQUIRE(test_coro_called == 0);
scheduler.loop(10);
REQUIRE(test_coro_called == 1);
}
const std::vector<int> values = {10, 11, 13};
SECTION("Sending multiple values.")
{
scheduler.start_tasks(await_signal_n(values, &signal));
scheduler.loop(0);
scheduler.loop(1);
REQUIRE(test_coro_called == 0);
for (int i = 0; i < values.size(); i++)
{
signal.send(values[i]);
scheduler.loop(i + 1);
REQUIRE(test_coro_called == i + 1);
}
}
}
namespace
{
skullc::coro::Task<int> test_sleepy_coro_return(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 expected;
}
skullc::coro::Task<> test_wait_all()
{
auto val = co_await skullc::coro::wait_all(
test_sleepy_coro(0, 10ms),
test_sleepy_coro_return(1, 20ms)
);
REQUIRE(std::get<0>(val) == std::monostate{});
REQUIRE(std::get<1>(val) == 1);
co_return;
}
skullc::coro::Task<> testwait_first()
{
auto val = co_await skullc::coro::wait_first(
test_sleepy_coro_return(0, 10ms),
test_sleepy_coro_return(1, 20ms)
);
REQUIRE(std::get<0>(val) == 0);
REQUIRE(std::get<1>(val) == std::nullopt);
co_return;
}
}
TEST_CASE("Wait all awaiter works.", "[coro],[wait_all]")
{
using namespace skullc::coro;
Scheduler<std::vector> scheduler;
skullc::this_coro::scheduler.register_scheduler(scheduler);
test_coro_called = 0;
scheduler.start_tasks(test_wait_all());
scheduler.loop(0);
scheduler.loop(0);
scheduler.loop(0);
REQUIRE(test_coro_called == 0);
scheduler.loop(10);
scheduler.loop(10);
REQUIRE(test_coro_called == 1);
scheduler.loop(20);
scheduler.loop(20);
scheduler.loop(20);
REQUIRE(test_coro_called == 2);
}
TEST_CASE("Wait one awaiter works.", "[coro],[wait_first]")
{
using namespace skullc::coro;
Scheduler<std::vector> scheduler;
skullc::this_coro::scheduler.register_scheduler(scheduler);
test_coro_called = 0;
scheduler.start_tasks(testwait_first());
scheduler.loop(0);
scheduler.loop(0);
scheduler.loop(0);
REQUIRE(test_coro_called == 0);
scheduler.loop(10);
scheduler.loop(10);
REQUIRE(test_coro_called == 1);
scheduler.loop(20);
scheduler.loop(20);
scheduler.loop(20);
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);
}

19
coro/CMakeLists.txt Normal file
View File

@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
add_library(coro INTERFACE)
add_library(skullc::coro ALIAS coro)
target_include_directories(coro
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc>
$<INSTALL_INTERFACE:include>
)
target_compile_definitions(coro
INTERFACE
SKULLC_WITH_CORO
)
## INSTALL
skullc_install_packages(skullc coro ${SKULLC_VERSION})

View File

@ -0,0 +1,2 @@
include(${CMAKE_CURRENT_LIST_DIR}/skullc-coro-config-version.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/skullc-coro-targets.cmake)

View File

@ -0,0 +1,230 @@
//
// Created by erki on 25/02/25.
//
#pragma once
#include "skullc/coro/this_coro.hpp"
#include "skullc/coro/task.hpp"
#include <coroutine>
#include <variant>
namespace skullc::coro
{
namespace detail
{
template<typename Awaitable>
struct AwaitableResumeType
{
using raw_type = typename std::conditional<
std::is_void_v<typename Awaitable::value_type>,
std::monostate,
typename Awaitable::value_type>::type;
using value_type = std::optional<raw_type>;
};
template<typename... Awaitables>
struct WaitAllAwaitable
{
WaitAllAwaitable() = delete;
WaitAllAwaitable(Awaitables&&... args)
: WaitAllAwaitable(std::index_sequence_for<Awaitables...>(), std::forward<Awaitables>(args)...)
{}
template<std::size_t... Is>
WaitAllAwaitable(std::index_sequence<Is...>, Awaitables&&... args)
{
((std::get<Is>(result) = std::nullopt), ...);
(register_awaitable<Is>(std::forward<Awaitables>(args)), ...);
}
~WaitAllAwaitable()
{
if (continuation)
this_coro::scheduler().remove(continuation);
}
template<std::size_t I, typename Awaitable>
void register_awaitable(Awaitable&& task)
{
start_awaitable();
auto t = run_awaitable<I>(task);
this_coro::scheduler().schedule(t.get_handle(), 0);
t.detach();
}
template<std::size_t I, typename Awaitable>
Task<> run_awaitable(Awaitable&& awaitable)
{
using decayed = std::decay_t<Awaitable>;
if constexpr (std::is_void_v<typename decayed::value_type> == true)
{
co_await awaitable;
std::get<I>(result) = std::monostate{};
}
else
{
auto val = co_await awaitable;
std::get<I>(result) = std::move(val);
}
awaitable_completed();
co_return;
}
void awaitable_completed()
{
pending--;
if (pending == 0 && continuation)
this_coro::scheduler().schedule(continuation, 0);
}
void start_awaitable()
{
pending++;
}
auto operator co_await() noexcept
{
struct Awaitable
{
WaitAllAwaitable* wait_all;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h)
{
wait_all->continuation = h;
}
auto await_resume()
{
return std::move(wait_all->result);
}
};
return Awaitable{this};
}
std::tuple<typename AwaitableResumeType<Awaitables>::value_type...> result;
std::coroutine_handle<> continuation{};
int pending = 0;
};
template<typename... Awaitables>
struct WaitFirstAwaitable
{
WaitFirstAwaitable() = delete;
WaitFirstAwaitable(Awaitables&&... args)
: WaitFirstAwaitable(std::index_sequence_for<Awaitables...>(), std::forward<Awaitables>(args)...)
{}
template<std::size_t... Is>
WaitFirstAwaitable(std::index_sequence<Is...>, Awaitables&&... args)
{
((std::get<Is>(result) = std::nullopt), ...);
pending = true;
(register_awaitable<Is>(std::forward<Awaitables>(args)), ...);
}
~WaitFirstAwaitable()
{
if (continuation)
this_coro::scheduler().remove(continuation);
}
template<std::size_t I, typename Awaitable>
void register_awaitable(Awaitable&& task)
{
auto t = run_awaitable<I>(task);
this_coro::scheduler().schedule(t.get_handle(), 0);
coroutines[I] = t.get_handle();
t.detach();
}
template<std::size_t I, typename Awaitable>
Task<> run_awaitable(Awaitable&& awaitable)
{
using decayed = std::decay_t<Awaitable>;
if constexpr (std::is_void_v<typename decayed::value_type> == true)
{
co_await awaitable;
awaitable_completed<I>(std::monostate{});
}
else
{
auto val = co_await awaitable;
awaitable_completed<I>(std::move(val));
}
co_return;
}
template<std::size_t I>
void awaitable_completed(auto&& val)
{
if (pending)
{
pending = false;
for (auto j = 0; j < coroutines.size(); j++)
{
if (j != I)
{
this_coro::scheduler().remove(coroutines[j]);
coroutines[j].destroy();
}
}
std::get<I>(result) = val;
this_coro::scheduler().schedule(continuation, 0);
}
}
auto operator co_await() noexcept
{
struct Awaitable
{
WaitFirstAwaitable* wait_first;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h)
{
wait_first->continuation = h;
}
auto await_resume()
{
return std::move(wait_first->result);
}
};
return Awaitable{this};
}
std::array<std::coroutine_handle<>, sizeof...(Awaitables)> coroutines;
std::tuple<typename AwaitableResumeType<Awaitables>::value_type...> result;
std::coroutine_handle<> continuation{};
int pending = 0;
};
}
template<typename... Awaitables>
auto wait_all(Awaitables&&... args)
{
return detail::WaitAllAwaitable<Awaitables...>(std::forward<Awaitables>(args)...);
}
template<typename... Awaitables>
auto wait_first(Awaitables&&... args)
{
return detail::WaitFirstAwaitable<Awaitables...>(std::forward<Awaitables>(args)...);
}
}

View File

@ -0,0 +1,120 @@
//
// Created by erki on 01/02/25.
//
#pragma once
#include <atomic>
#include <coroutine>
#include <optional>
#include "peripherals_hal_concepts.hpp"
#include "skullc/coro/scheduler.hpp"
#include "skullc/coro/this_coro.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), previous_is_set(pin->read())
{}
~PinEdgeAwaiter() override
{
if (continuation)
this_coro::scheduler().remove(continuation);
if (stored_coro)
this_coro::scheduler().remove(stored_coro);
}
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
{
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;
}
};
struct ISRAwaiter
{
ISRAwaiter() = default;
~ISRAwaiter()
{
if (continuation)
this_coro::scheduler().remove(continuation);
}
bool await_ready()
{
return isr_completed;
}
void await_suspend(std::coroutine_handle<> h)
{
continuation = h;
suspended = true;
}
auto await_resume()
{
continuation = {};
}
void set_completed()
{
isr_completed = true;
if (suspended == true)
this_coro::scheduler().schedule(continuation, 0);
}
std::coroutine_handle<> continuation;
std::atomic<bool> isr_completed = false;
std::atomic<bool> suspended = false;
};
}// namespace skullc::coro

View File

@ -0,0 +1,130 @@
//
// Created by erki on 30/01/25.
//
#pragma once
#include <algorithm>
#include <coroutine>
#include <cstdint>
#include <utility>
#include "skullc/coro/scheduler_base.hpp"
#include "skullc/coro/task.hpp"
namespace skullc::coro
{
template<typename T>
concept TaskType = requires(T task) {
{
task.get_handle().resume()
};
};
template<template<typename X> typename C>
class Scheduler final : public SchedulerBase
{
constexpr static auto cmp = [](const auto& a, const auto& b) {
return a.first > b.first;
};
public:
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;
template<TaskType... T>
void start_tasks(T... tasks)
{
start(tasks...);
}
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<uint32_t, std::milli>(current_time_ms));
}
void loop(const std::chrono::duration<uint32_t, std::milli>& current_time)
{
if (!scheduled_.empty())
{
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_)
{
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
{
bool found = false;
for (auto& p : scheduled_)
if (p.second == handle)
{
std::swap(p, scheduled_.back());
scheduled_.pop_back();
std::make_heap(scheduled_.begin(), scheduled_.end(), cmp);
found = true;
break;
}
if (pollers_.size() > 0)
pollers_.erase(std::remove_if(pollers_.begin(), pollers_.end(),
[handle](const auto* poller) { return poller->stored_coro == handle; }));
return found;
}
private:
Container scheduled_;
PollerContainer pollers_;
template<typename T0, typename... R>
void start(T0& t0, R&... r)
{
t0.get_handle().resume();
if constexpr (sizeof...(r) > 0)
{
start(r...);
}
}
};
}// namespace skullc::coro

View File

@ -0,0 +1,32 @@
//
// Created by erki on 02/02/25.
//
#pragma once
#include <chrono>
#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;
};
}// namespace skullc::coro

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};
}
}// namespace skullc::coro

View File

@ -0,0 +1,92 @@
//
// Created by erki on 06/02/25.
//
#pragma once
#include <atomic>
#include <optional>
#include "skullc/coro/this_coro.hpp"
namespace skullc::coro
{
template<typename T>
class Signal
{
struct Awaiter
{
Signal* signal;
std::coroutine_handle<> continuation;
Awaiter() = delete;
Awaiter(const Awaiter&) = delete;
Awaiter(Awaiter&&) = delete;
explicit Awaiter(Signal* signal)
: signal(signal)
{}
Awaiter& operator=(const Awaiter&) = delete;
Awaiter& operator=(Awaiter&&) = delete;
~Awaiter()
{
if (continuation)
this_coro::scheduler().remove(continuation);
}
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h)
{
continuation = h;
}
T&& await_resume()
{
continuation = {};
return std::move(*signal->data_);
}
T&& get_return_object() noexcept
{
return std::move(*signal->data_);
}
void trigger_resume()
{
this_coro::scheduler().schedule(continuation, 0);
}
};
public:
void send(const T& value)
{
data_.emplace(value);
if (awaiter_.has_value())
awaiter_->trigger_resume();
}
void send(T&& value)
{
data_.emplace(value);
if (awaiter_.has_value())
awaiter_->trigger_resume();
}
auto& receive()
{
awaiter_.emplace(this);
return *awaiter_;
}
private:
std::optional<T> data_;
std::optional<Awaiter> awaiter_;
};
}// namespace skullc::coro

View File

@ -0,0 +1,59 @@
//
// Created by erki on 01/02/25.
//
#pragma once
#include "skullc/coro/scheduler.hpp"
#include "skullc/coro/this_coro.hpp"
#include "peripherals_hal_concepts.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()};
}
template<Peripherals::Hal::StaticHal Hal>
auto sleep(const std::chrono::duration<uint32_t, std::milli>& duration)
{
return detail::TimeAwaitable{Hal::getMillis() + duration.count()};
}
}// namespace skullc::coro

View File

@ -0,0 +1,160 @@
//
// Created by erki on 30/01/25.
//
#pragma once
#include <coroutine>
#include <memory>
#include <optional>
namespace skullc::coro
{
template<typename T = void, bool lazy = true>
struct [[nodiscard]] Task
{
using value_type = T;
struct task_promise;
using promise_type = task_promise;
Task() = delete;
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(const Task&) = delete;
Task& operator=(const Task&) = delete;
~Task()
{
if constexpr (!std::is_same_v<T, void>)
{
if (my_handle)
my_handle.destroy();
}
}
auto get_handle() const { return my_handle; }
void detach() { my_handle = nullptr; }
Task& operator=(Task&& other) noexcept
{
if (std::addressof(other) != this)
{
if (my_handle)
{
my_handle.destroy();
}
my_handle = other.my_handle;
other.my_handle = nullptr;
}
return *this;
}
auto operator co_await() noexcept
{
struct awaiter
{
bool await_ready() const noexcept
{
return !coro_handle || coro_handle.done();
}
std::coroutine_handle<>
await_suspend(std::coroutine_handle<> awaiting_coroutine) noexcept
{
coro_handle.promise().set_continuation(awaiting_coroutine);
return coro_handle;
}
T await_resume() noexcept
{
if constexpr (std::is_same_v<T, void>)
return;
else
return std::move(coro_handle.promise().data.value());
}
std::coroutine_handle<promise_type> coro_handle;
};
return awaiter{my_handle};
}
private:
std::coroutine_handle<promise_type> my_handle;
};
template<typename T>
struct promise_setter
{
template<typename U>
void return_value(U&& u)
{
data.emplace(std::forward<U>(u));
}
std::optional<T> data;
};
template<>
struct promise_setter<void>
{
void return_void() {}
};
template<typename T, bool lazy>
struct Task<T, lazy>::task_promise : public promise_setter<T>
{
Task get_return_object() noexcept
{
return Task{std::coroutine_handle<task_promise>::from_promise(*this)};
};
auto initial_suspend() const noexcept
{
if constexpr (lazy)
return std::suspend_always{};
else
return std::suspend_never{};
}
auto final_suspend() const noexcept
{
struct awaiter
{
bool await_ready() const noexcept { return false; }
void await_resume() const noexcept {}
std::coroutine_handle<>
await_suspend(std::coroutine_handle<Task::task_promise> h) noexcept
{
if constexpr (std::is_same_v<T, void>)
{
auto continuation = h.promise().continuation;
h.destroy();
return continuation;
}
else
return h.promise().continuation;
}
};
return awaiter{};
}
void unhandled_exception() noexcept { std::terminate(); }
void set_continuation(std::coroutine_handle<> handle) { continuation = handle; }
private:
std::coroutine_handle<> continuation = std::noop_coroutine();
};
}// namespace skullc::coro

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