Monotonic & spawn_

The understanding of time is an important concept in embedded systems, and to be able to run tasks based on time is essential. The framework provides the static methods task::spawn_after(/* duration */) and task::spawn_at(/* specific time instant */). spawn_after is more commonly used, but in cases where it's needed to have spawns happen without drift or to a fixed baseline spawn_at is available.

The #[monotonic] attribute, applied to a type alias definition, exists to support this. This type alias must point to a type which implements the rtic_monotonic::Monotonic trait. This is generally some timer which handles the timing of the system. One or more monotonics can coexist in the same system, for example a slow timer that wakes the system from sleep and another which purpose is for fine grained scheduling while the system is awake.

The attribute has one required parameter and two optional parameters, binds, default and priority respectively. The required parameter, binds = InterruptName, associates an interrupt vector to the timer's interrupt, while default = true enables a shorthand API when spawning and accessing time (monotonics::now() vs monotonics::MyMono::now()), and priority sets the priority of the interrupt vector.

The default priority is the maximum priority of the system. If your system has a high priority task with tight scheduling requirements, it might be desirable to demote the monotonic task to a lower priority to reduce scheduling jitter for the high priority task. This however might introduce jitter and delays into scheduling via the monotonic, making it a trade-off.

The monotonics are initialized in #[init] and returned within the init::Monotonic( ... ) tuple. This activates the monotonics making it possible to use them.

See the following example:

#![allow(unused)]
fn main() {
//! examples/schedule.rs

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

use panic_semihosting as _;

#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
    use cortex_m_semihosting::{debug, hprintln};
    use systick_monotonic::*;

    #[monotonic(binds = SysTick, default = true)]
    type MyMono = Systick<100>; // 100 Hz / 10 ms granularity

    #[shared]
    struct Shared {}

    #[local]
    struct Local {}

    #[init]
    fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
        let systick = cx.core.SYST;

        // Initialize the monotonic (SysTick rate in QEMU is 12 MHz)
        let mono = Systick::new(systick, 12_000_000);

        hprintln!("init");

        // Schedule `foo` to run 1 second in the future
        foo::spawn_after(1.secs()).unwrap();

        (
            Shared {},
            Local {},
            init::Monotonics(mono), // Give the monotonic to RTIC
        )
    }

    #[task]
    fn foo(_: foo::Context) {
        hprintln!("foo");

        // Schedule `bar` to run 2 seconds in the future (1 second after foo runs)
        bar::spawn_after(1.secs()).unwrap();
    }

    #[task]
    fn bar(_: bar::Context) {
        hprintln!("bar");

        // Schedule `baz` to run 1 seconds from now, but with a specific time instant.
        baz::spawn_at(monotonics::now() + 1.secs()).unwrap();
    }

    #[task]
    fn baz(_: baz::Context) {
        hprintln!("baz");
        debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
    }
}
}
$ cargo run --target thumbv7m-none-eabi --example schedule
init
foo
bar
baz

A key requirement of a Monotonic is that it must deal gracefully with hardware timer overruns.

Canceling or rescheduling a scheduled task

Tasks spawned using task::spawn_after and task::spawn_at returns a SpawnHandle, which allows canceling or rescheduling of the task scheduled to run in the future.

If cancel or reschedule_at/reschedule_after returns an Err it means that the operation was too late and that the task is already sent for execution. The following example shows this in action:

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

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

use panic_semihosting as _;

#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
    use cortex_m_semihosting::{debug, hprintln};
    use systick_monotonic::*;

    #[monotonic(binds = SysTick, default = true)]
    type MyMono = Systick<100>; // 100 Hz / 10 ms granularity

    #[shared]
    struct Shared {}

    #[local]
    struct Local {}

    #[init]
    fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
        let systick = cx.core.SYST;

        // Initialize the monotonic (SysTick rate in QEMU is 12 MHz)
        let mono = Systick::new(systick, 12_000_000);

        hprintln!("init");

        // Schedule `foo` to run 1 second in the future
        foo::spawn_after(1.secs()).unwrap();

        (
            Shared {},
            Local {},
            init::Monotonics(mono), // Give the monotonic to RTIC
        )
    }

    #[task]
    fn foo(_: foo::Context) {
        hprintln!("foo");

        // Schedule `bar` to run 2 seconds in the future (1 second after foo runs)
        let spawn_handle = baz::spawn_after(2.secs()).unwrap();
        bar::spawn_after(1.secs(), spawn_handle, false).unwrap(); // Change to true
    }

    #[task]
    fn bar(_: bar::Context, baz_handle: baz::SpawnHandle, do_reschedule: bool) {
        hprintln!("bar");

        if do_reschedule {
            // Reschedule baz 2 seconds from now, instead of the original 1 second
            // from now.
            baz_handle.reschedule_after(2.secs()).unwrap();
            // Or baz_handle.reschedule_at(/* time */)
        } else {
            // Or cancel it
            baz_handle.cancel().unwrap();
            debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
        }
    }

    #[task]
    fn baz(_: baz::Context) {
        hprintln!("baz");
        debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
    }
}
}
$ cargo run --target thumbv7m-none-eabi --example cancel-reschedule
init
foo
bar