owned this note
owned this note
Published
Linked with GitHub
# RTIC + `async/await` = :rocket:
## Example repo
A working example project with manual codegen: https://github.com/korken89/dwm1001-async/blob/master/src/bin/minimal.rs
## API
Inside the `app` macro we define API as in this example:
```rust
#[rtic::app(device = hal::pac)]
mod app {w
#[monotonic(binds = Tim1, default = true)]
type MyMono = hal::Tim1Monotonic;
/// ------- non-`Send` spawn support and 0-priority async tasks
#[init]
fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
let param = NotSend {};
// With the `only_same_priority_spawn_please_fix_me` marker we can send
// non-`Send` types to tasks with the same priority.
cx.spawn.idle_prio_task1(param).unwrap();
// ...
}
// 0-priority async software tasks can be used in place of `#[idle]`,
// they can however not be used at the same time as `#[idle]`
// #[idle]
// fn idle(_: idle::Context) -> ! {
// // ...
// }
// This task is restricted to only be spawned from the same priority
#[task(only_same_priority_spawn_please_fix_me, priority = 0)]
async fn idle_prio_task1(cx: idle_prio_task1::Context, param: NotSend) {
cx.spawn.idle_prio_task2(param).unwrap();
}
// This task is restricted to only be spawned from the same priority
#[task(only_same_priority_spawn_please_fix_me, priority = 0)]
async fn idle_prio_task2(cx: idle_prio_task2::Context, param: NotSend) {
cx.spawn.idle_prio_task1(param).unwrap();
}
/// ------- Normal async tasks
#[task]
async fn task1(_: task1::Context) {
// Delay provided by monotonic
monotonics::delay(123.ms()).await;
// Timeout provided by monotonic
match monotonics::timeout(thing_returns_a_future(), 500.ms()).await {
Ok(val) => {
// No timeout
}
Err(_) => {
// Timeout
}
}
}
#[task]
async fn looping_task(_: looping_task::Context) {
// Having infinite loops with await points are OK!
loop {
monotonics::delay(123.ms()).await;
}
}
// Hardware bound async task is not allowed for now
// #[task(binds = UART1)]
// async fn task2(_: task1::Context) {
// // ...
// }
}
```
## Under the hood
In essence, each `async` task will become an single future executor. One could make it so there is an executor per priority level, however that seems overly complicated when we have the software task system already.
Example codegen for an async task:
```rust
#[rtic::app(device = hal::pac)]
mod app {
// #[task(yada yada)]
async fn task(cx: task_executor::Context<'_>) {
loop {
delay(123.ms()).await;
}
}
////////////////////////////////////////////////////////////////
//
// Example codegen, this should be inside the `dispatcher` later
//
////////////////////////////////////////////////////////////////
type F = impl Future + 'static;
static mut TASK: AsyncTaskExecutor<F> = AsyncTaskExecutor::new();
#[task(binds = SWI2_EGU2)]
fn task_executor(cx: task_executor::Context) {
let task_storage = unsafe { &mut TASK };
match task_storage {
AsyncTaskExecutor::Idle => {
// This will be part of the `task::spawn` API later
task_storage.spawn(task(unsafe { mem::transmute(cx) }));
}
_ => {}
}
task_storage.poll(|| rtic::pend(hal::pac::Interrupt::SWI2_EGU2));
}
}
//=============
// Waker
static WAKER_VTABLE: RawWakerVTable =
RawWakerVTable::new(waker_clone, waker_wake, waker_wake, waker_drop);
unsafe fn waker_clone(p: *const ()) -> RawWaker {
RawWaker::new(p, &WAKER_VTABLE)
}
unsafe fn waker_wake(p: *const ()) {
// The only thing we need from a waker is the function
// to call to pend the async dispatcher.
let f: fn() = mem::transmute(p);
f();
}
unsafe fn waker_drop(_: *const ()) {
// nop
}
//============
// AsyncTaskExecutor
enum AsyncTaskExecutor<F: Future + 'static> {
Idle,
Running(F),
}
impl<F: Future + 'static> AsyncTaskExecutor<F> {
const fn new() -> Self {
Self::Idle
}
fn spawn(&mut self, future: F) {
*self = AsyncTaskExecutor::Running(future);
}
fn poll(&mut self, wake: fn()) {
match self {
AsyncTaskExecutor::Idle => {}
AsyncTaskExecutor::Running(future) => unsafe {
let waker_data: *const () = mem::transmute(wake);
let waker =
Waker::from_raw(RawWaker::new(waker_data, &WAKER_VTABLE));
let mut cx = Context::from_waker(&waker);
let future = Pin::new_unchecked(future);
match future.poll(&mut cx) {
Poll::Ready(_) => *self = AsyncTaskExecutor::Idle,
Poll::Pending => {}
};
}
}
}
}
```
## Async helpers from `Monotonic`
### `delay`
An async delay implementation driven by the `Monotonic`.
Example impl:
```rust
/// Main delay method
#[inline(always)]
pub fn delay(duration: <MyMono as rtic::Monotonic>::Duration) -> Delay {
let until = monotonics::now() + duration;
Delay { until }
}
pub struct Delay {
until: <MyMono as rtic::Monotonic>::Instant,
}
impl Future for Delay {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let s = self.as_mut();
let now = monotonics::now();
if now >= s.until {
Poll::Ready(())
} else {
let waker = cx.waker().clone();
// Here we use the existing `spawn_after` API to queue a waker,
// however this will be specialized in the codegen
delay_handler::spawn_after(s.until - now, waker)
.expect("Out of monotonic queue slots");
Poll::Pending
}
}
}
```
### `timeout`
An async timeout implementation driven by the `Monotonic`.
Example impl:
```rust
/// Main timeout method
#[inline(always)]
pub fn timeout<F: Future>(
future: F,
duration: <MyMono as rtic::Monotonic>::Duration
) -> Timeout<F> {
let until = monotonics::now() + duration;
Timeout {
future,
until,
cancel_handle: None,
}
}
/// Error definition
[derive(Copy, Clone, Debug, defmt::Format)]
pub struct TimeoutError;
pub struct Timeout<F: Future> {
future: F,
until: <MyMono as rtic::Monotonic>::Instant,
cancel_handle: Option<delay_handler::SpawnHandle>,
}
impl<F> Future for Timeout<F>
where
F: Future,
{
type Output = Result<F::Output, TimeoutError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let now = monotonics::now();
// SAFETY: We don't move the underlying pinned value.
let mut s = unsafe { self.get_unchecked_mut() };
let future = unsafe { Pin::new_unchecked(&mut s.future) };
match future.poll(cx) {
Poll::Ready(r) => {
if let Some(ch) = s.cancel_handle.take() {
ch.cancel().ok();
}
Poll::Ready(Ok(r))
}
Poll::Pending => {
if now >= s.until {
Poll::Ready(Err(TimeoutError))
} else if s.cancel_handle.is_none() {
let waker = cx.waker().clone();
// Here we use the existing `spawn_after` API to queue a waker,
// however this will be specialized in the codegen
let sh = delay_handler::spawn_after(s.until - now, waker)
.expect("Out of monotonic queue slots");
s.cancel_handle = Some(sh);
Poll::Pending
} else {
Poll::Pending
}
}
}
}
}
```
## Idle async executor loop
```rust
fn idle_executor_loop() {
loop {
if executor1.run() // returns true when finished
|| !executor1.is_running()
{
// the executor is ready to accept a new spawn
if let Some(val) = executor1_ready_queue.dequeue() {
executor1.spawn(val);
}
}
if executor2.run() ...
}
}
```
## Open questions
* ...