Tasks with delay

A convenient way to express miniminal timing requirements is by means of delaying progression.

This can be achieved by instantiating a monotonic timer:

#![allow(unused)]
fn main() {
...
rtic_monotonics::make_systick_handler!();

#[init]
fn init(cx: init::Context) -> (Shared, Local) {
    hprintln!("init");

    Systick::start(cx.core.SYST, 12_000_000);
    ...
}

A software task can await the delay to expire:

#![allow(unused)]
fn main() {
#[task]
async fn foo(_cx: foo::Context) {
    ...
    Systick::delay(100.millis()).await;
    ...
}

}

Technically, the timer queue is implemented as a list based priority queue, where list-nodes are statically allocated as part of the underlying task Future. Thus, the timer queue is infallible at run-time (its size and allocation is determined at compile time).

Similarly the channels implementation, the timer-queue implementation relies on a global Critical Section (CS) for race protection. For the examples a CS implementation is provided by adding --features test-critical-section to the build options.

For a complete example:

#![allow(unused)]
fn main() {
//! examples/async-delay.rs

#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
#![feature(type_alias_impl_trait)]

use panic_semihosting as _;

#[rtic::app(device = lm3s6965, dispatchers = [SSI0, UART0], peripherals = true)]
mod app {
    use cortex_m_semihosting::{debug, hprintln};
    use rtic_monotonics::systick::*;

    #[shared]
    struct Shared {}

    #[local]
    struct Local {}

    #[init]
    fn init(cx: init::Context) -> (Shared, Local) {
        hprintln!("init");

        let systick_token = rtic_monotonics::make_systick_handler!();
        Systick::start(cx.core.SYST, 12_000_000, systick_token);

        foo::spawn().ok();
        bar::spawn().ok();
        baz::spawn().ok();

        (Shared {}, Local {})
    }

    #[idle]
    fn idle(_: idle::Context) -> ! {
        // debug::exit(debug::EXIT_SUCCESS);
        loop {
            // hprintln!("idle");
            cortex_m::asm::wfi(); // put the MCU in sleep mode until interrupt occurs
        }
    }

    #[task]
    async fn foo(_cx: foo::Context) {
        hprintln!("hello from foo");
        Systick::delay(100.millis()).await;
        hprintln!("bye from foo");
    }

    #[task]
    async fn bar(_cx: bar::Context) {
        hprintln!("hello from bar");
        Systick::delay(200.millis()).await;
        hprintln!("bye from bar");
    }

    #[task]
    async fn baz(_cx: baz::Context) {
        hprintln!("hello from baz");
        Systick::delay(300.millis()).await;
        hprintln!("bye from baz");

        debug::exit(debug::EXIT_SUCCESS);
    }
}
}
$ cargo run --target thumbv7m-none-eabi --example async-delay --features test-critical-section 
init
hello from bar
hello from baz
hello from foo
bye from foo
bye from bar
bye from baz

Timeout

Rust Futures (underlying Rust async/await) are composable. This makes it possible to select in between Futures that have completed.

A common use case is transactions with associated timeout. In the examples shown below, we introduce a fake HAL device which performs some transaction. We have modelled the time it takes based on the input parameter (n) as 350ms + n * 100ms).

Using the select_biased macro from the futures crate it may look like this:

#![allow(unused)]
fn main() {
// Call hal with short relative timeout using `select_biased`
select_biased! {
    v = hal_get(1).fuse() => hprintln!("hal returned {}", v),
    _ = Systick::delay(200.millis()).fuse() =>  hprintln!("timeout", ), // this will finish first
}
}

Assuming the hal_get will take 450ms to finish, a short timeout of 200ms will expire.

#![allow(unused)]
fn main() {
// Call hal with long relative timeout using `select_biased`
select_biased! {
    v = hal_get(1).fuse() => hprintln!("hal returned {}", v), // hal finish first
    _ = Systick::delay(1000.millis()).fuse() =>  hprintln!("timeout", ),
}
}

By extending the timeout to 1000ms, the hal_get will finish first.

Using select_biased any number of futures can be combined, so its very powerful. However, as the timeout pattern is frequently used, it is directly supported by the RTIC [rtc-monotonics] and [rtic-time] crates. The second example from above using timeout_after:

#![allow(unused)]
fn main() {
// Call hal with long relative timeout using monotonic `timeout_after`
match Systick::timeout_after(1000.millis(), hal_get(1)).await {
    Ok(v) => hprintln!("hal returned {}", v),
    _ => hprintln!("timeout"),
}
}

In cases you want exact control over time without drift. For this purpose we can use exact points in time using Instance, and spans of time using Duration. Operations on the Instance and Duration types are given by the fugit crate.

#![allow(unused)]
fn main() {
// get the current time instance
let mut instant = Systick::now();

// do this 3 times
for n in 0..3 {
    // absolute point in time without drift
    instant += 1000.millis();
    Systick::delay_until(instant).await;

    // absolute point it time for timeout
    let timeout = instant + 500.millis();
    hprintln!("now is {:?}, timeout at {:?}", Systick::now(), timeout);

    match Systick::timeout_at(timeout, hal_get(n)).await {
        Ok(v) => hprintln!("hal returned {} at time {:?}", v, Systick::now()),
        _ => hprintln!("timeout"),
    }
}
}

instant = Systick::now() gives the baseline (i.e., the absolute current point in time). We want to call hal_get after 1000ms relative to this absolute point in time. This can be accomplished by Systick::delay_until(instant).await;. We define the absolute point in time for the timeout, and call Systick::timeout_at(timeout, hal_get(n)).await. For the first loop iteration n == 0, and the hal_get will take 350ms (and finishes before the timeout). For the second iteration n == 1, and hal_get will take 450ms (and again succeeds to finish before the timeout). For the third iteration n == 2 (hal_get will take 5500ms to finish). In this case we will run into a timeout.

The complete example:

#![allow(unused)]
fn main() {
//! examples/async-timeout.rs

#![no_main]
#![no_std]
#![deny(warnings)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
#![feature(type_alias_impl_trait)]

use cortex_m_semihosting::{debug, hprintln};
use panic_semihosting as _;
use rtic_monotonics::systick::*;

#[rtic::app(device = lm3s6965, dispatchers = [SSI0, UART0], peripherals = true)]
mod app {
    use super::*;
    use futures::{future::FutureExt, select_biased};
    use rtic_monotonics::Monotonic;

    #[shared]
    struct Shared {}

    #[local]
    struct Local {}

    #[init]
    fn init(cx: init::Context) -> (Shared, Local) {
        hprintln!("init");

        let systick_token = rtic_monotonics::make_systick_handler!();
        Systick::start(cx.core.SYST, 12_000_000, systick_token);

        foo::spawn().ok();

        (Shared {}, Local {})
    }

    #[task]
    async fn foo(_cx: foo::Context) {
        // Call hal with short relative timeout using `select_biased`
        select_biased! {
            v = hal_get(1).fuse() => hprintln!("hal returned {}", v),
            _ = Systick::delay(200.millis()).fuse() =>  hprintln!("timeout", ), // this will finish first
        }

        // Call hal with long relative timeout using `select_biased`
        select_biased! {
            v = hal_get(1).fuse() => hprintln!("hal returned {}", v), // hal finish first
            _ = Systick::delay(1000.millis()).fuse() =>  hprintln!("timeout", ),
        }

        // Call hal with long relative timeout using monotonic `timeout_after`
        match Systick::timeout_after(1000.millis(), hal_get(1)).await {
            Ok(v) => hprintln!("hal returned {}", v),
            _ => hprintln!("timeout"),
        }

        // get the current time instance
        let mut instant = Systick::now();

        // do this 3 times
        for n in 0..3 {
            // absolute point in time without drift
            instant += 1000.millis();
            Systick::delay_until(instant).await;

            // absolute point it time for timeout
            let timeout = instant + 500.millis();
            hprintln!("now is {:?}, timeout at {:?}", Systick::now(), timeout);

            match Systick::timeout_at(timeout, hal_get(n)).await {
                Ok(v) => hprintln!("hal returned {} at time {:?}", v, Systick::now()),
                _ => hprintln!("timeout"),
            }
        }

        debug::exit(debug::EXIT_SUCCESS);
    }
}

// Emulate some hal
async fn hal_get(n: u32) -> u32 {
    // emulate some delay time dependent on n
    let d = 350.millis() + n * 100.millis();
    hprintln!("the hal takes a duration of {:?}", d);
    Systick::delay(d).await;
    // emulate some return value
    5
}
}
$ cargo run --target thumbv7m-none-eabi --example async-timeout --features test-critical-section 
init
the hal takes a duration of Duration { ticks: 45 }
timeout
the hal takes a duration of Duration { ticks: 45 }
hal returned 5
the hal takes a duration of Duration { ticks: 45 }
hal returned 5
now is Instant { ticks: 210 }, timeout at Instant { ticks: 260 }
the hal takes a duration of Duration { ticks: 35 }
hal returned 5 at time Instant { ticks: 245 }
now is Instant { ticks: 310 }, timeout at Instant { ticks: 360 }
the hal takes a duration of Duration { ticks: 45 }
hal returned 5 at time Instant { ticks: 355 }
now is Instant { ticks: 410 }, timeout at Instant { ticks: 460 }
the hal takes a duration of Duration { ticks: 55 }
timeout