Building a header-only C++20 coroutine runtime for bare-metal RISC-V
Designing a lightweight coroutine runtime for real-time tasks without an OS, featuring awaitable timers and static memory allocation
Creating the coroutine runtime infrastructure
A simple coroutine example was presented in “C++20 coroutines, header only, without an OS”. This post describes the runtime used for that example in detail.
Summary of the runtime files
The runtime for this example is a set of include files in include/coro
. These files are used:
nop_task.hpp
: Task structure includingpromise_type
to conform the C++ coroutines task concept.scheduler.hpp
: Generic scheduler class that can manage a set ofstd::coroutine_handle
to determine when they should resume and implement the resumption.awaitable_timer.hpp
: An "awaitable" class that can be used withco_await
to schedule a coroutines to wake up after a givenstd::chono
delay.static_list.hpp
: An alternative tostd::list
that uses custom memory allocation from a static region to avoid heap usage.awaitable_priority.hpp
: An alternative "awaitable" class for tasks to be scheduled to wake according to priority.
NOTE: All classes here are designed to not use the heap for allocation. They will allocate all memory from statically declared buffers.
The coroutine task concept
The nop_task
class in nop_task.hpp
file implements the coroutine task concept.
A coroutine task includes a promise concept with no return values. The important structures in this file are struct nop_task
/ struct nop_task::promise_type
. This is implemented as described in CPP Reference.
This task structure will be allocated each time a coroutine is called. To avoid heap allocation static memory allocation is used (to be described below). When using a memory constrained platform it is important to understand that the number of coroutines that can be called is restricted by the memory allocated for nop_task::task_heap_
.
The relationships between the task classes are shown in the following class diagram:
The awaitable concept
The classes in awaitable_timer.hpp
and awaitable_priority.hpp
represent asynchronous events that pause the coroutine task until an event occurs.
These classes are designed to be returned from a co_await
, this ensures a task can be scheduled to be resumed on a later event.
The awaitable_timer
class implements the awaitable concept described in CPP Reference, and also the co_await
operator that is overloaded to take the scheduler_delay
struct and return awaitable_timer
. An additional concept of the scheduler
class is being used to manage the coroutine handle and wake up conditions that are used to implement coroutine task pause.
The relationships between the awaitable classes are shown in the following class diagram:
The scheduler class
The classes in scheduler.hpp
are designed to manage the coroutines that are paused. It is a template class, parameterized according to the type of event that should be scheduled, and the maximum number of concurrent active tasks.
The scheduler does the work that would be done by an RTOS or General Purpose OS. It manages a task list of waiting tasks with wake conditions and resumes them on the wake event.
The awaitable classes, introduced above, will insert paused tasks via insert()
. The active execution context must call resume()
to resume the paused tasks. Each entry in the task list is a schedule_entry
structure. The classe are templates specialized by the wake up condition.
This scheduler class is not a concept required by C++ coroutines, but in this example it is needed as there is no operating system scheduler.
The relationships between scheduler classes are shown in the following class diagram:
Using the awaitable and scheduler classes to create a software timer
The awaitable class and scheduler are combined to implement the software timer feature. The following diagram shows how the classes relate.
Walk through of the detailed sequence of suspend and resume
Now the concrete classes have been defined, the sequence to suspend and resume a coroutine class can be shown.
It is shown below in 3 stages in relation to the simple timer example.
1. Setup a coroutine, and suspend on the first wait.
2. Resume and suspend, iterate over several time delays.
3. Complete iterating and exit coroutines.
Testing
The runtime has some basic unit testing implemented in test using the unity test framework. The tests are not comprehensive, but run independent of hardware as the runtime is host & OS independent. The tests are compiled for the host OS and run locally.
Summary
The runtime presented in this article is not meant for production usage and has the bare minimal functionality to implement a re-entrant function using a software timer.
However, it does show the potential of C++ coroutines to be applied to real time applications that are portable across different OS and target architectures.
Appendix
References
I won’t explain the details of C++ coroutines, there are much better resources. I used the following to understand coroutines:
- Lewis Baker’s “C++ coroutines: Understanding Symmetric-Transfer” -
- Lewis Baker’s “C++ coroutines: Understanding Symmetric-Transfer” — the code
- CPPcoro library, has not been updated for gcc support — however the documentation is great!
- The folly library has an updated coro from Lewis Baker, and some info on enabling coroutines
- Raymond Chen’s explanations, and demonstration of legacy callback integration
- Some nice examples here
- CPP Reference is incomplete, but a start
A Few implementation details
- Tasks are created at the first call to an asynchronous routine. The allocated task data structure is minimal.
- Tasks do not have a dedicated stack, they will be restored to the stack where they are woken.
- Context switching stack management is implemented by the compiler, no custom assembler routines are needed (such as portASM.s in FreeRTOS). This is possible as there is no pre-emptive context switching.
- The scheduler can be made a C++ object, and context switching can be controlled programmatically...
Originally published at http://five-embeddev.com on November 24, 2024.