Timer queue

In contrast with the spawn API, which immediately spawns a software task onto the scheduler, the schedule API can be used to schedule a task to run some time in the future.

To use the schedule API a monotonic timer must be first defined using the monotonic argument of the #[app] attribute. This argument takes a path to a type that implements the Monotonic trait. The associated type, Instant, of this trait represents a timestamp in arbitrary units and it's used extensively in the schedule API -- it is suggested to model this type after the one in the standard library.

Although not shown in the trait definition (due to limitations in the trait / type system) the subtraction of two Instants should return some Duration type (see core::time::Duration) and this Duration type must implement the TryInto<u32> trait. The implementation of this trait must convert the Duration value, which uses some arbitrary unit of time, into the "system timer (SYST) clock cycles" time unit. The result of the conversion must be a 32-bit integer. If the result of the conversion doesn't fit in a 32-bit number then the operation must return an error, any error type.

For ARMv7+ targets the rtfm crate provides a Monotonic implementation based on the built-in CYCle CouNTer (CYCCNT). Note that this is a 32-bit timer clocked at the frequency of the CPU and as such it is not suitable for tracking time spans in the order of seconds.

To be able to schedule a software task from a context the name of the task must first appear in the schedule argument of the context attribute. When scheduling a task the (user-defined) Instant at which the task should be executed must be passed as the first argument of the schedule invocation.

Additionally, the chosen monotonic timer must be configured and initialized during the #[init] phase. Note that this is also the case if you choose to use the CYCCNT provided by the cortex-m-rtfm crate.

The example below schedules two tasks from init: foo and bar. foo is scheduled to run 8 million clock cycles in the future. Next, bar is scheduled to run 4 million clock cycles in the future. Thus bar runs before foo since it was scheduled to run first.

IMPORTANT: The examples that use the schedule API or the Instant abstraction will not properly work on QEMU because the Cortex-M cycle counter functionality has not been implemented in qemu-system-arm.


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

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

use cortex_m::peripheral::DWT;
use cortex_m_semihosting::hprintln;
use panic_halt as _;
use rtfm::cyccnt::{Instant, U32Ext as _};

// NOTE: does NOT work on QEMU!
#[rtfm::app(device = lm3s6965, monotonic = rtfm::cyccnt::CYCCNT)]
const APP: () = {
    #[init(schedule = [foo, bar])]
    fn init(mut cx: init::Context) {
        // Initialize (enable) the monotonic timer (CYCCNT)
        cx.core.DCB.enable_trace();
        // required on Cortex-M7 devices that software lock the DWT (e.g. STM32F7)
        DWT::unlock();
        cx.core.DWT.enable_cycle_counter();

        // semantically, the monotonic timer is frozen at time "zero" during `init`
        // NOTE do *not* call `Instant::now` in this context; it will return a nonsense value
        let now = cx.start; // the start time of the system

        hprintln!("init @ {:?}", now).unwrap();

        // Schedule `foo` to run 8e6 cycles (clock cycles) in the future
        cx.schedule.foo(now + 8_000_000.cycles()).unwrap();

        // Schedule `bar` to run 4e6 cycles in the future
        cx.schedule.bar(now + 4_000_000.cycles()).unwrap();
    }

    #[task]
    fn foo(_: foo::Context) {
        hprintln!("foo  @ {:?}", Instant::now()).unwrap();
    }

    #[task]
    fn bar(_: bar::Context) {
        hprintln!("bar  @ {:?}", Instant::now()).unwrap();
    }

    extern "C" {
        fn UART0();
    }
};

#}

Running the program on real hardware produces the following output in the console:

init @ Instant(0)
bar  @ Instant(4000236)
foo  @ Instant(8000173)

When the schedule API is being used the runtime internally uses the SysTick interrupt handler and the system timer peripheral (SYST) so neither can be used by the application. This is accomplished by changing the type of init::Context.core from cortex_m::Peripherals to rtfm::Peripherals. The latter structure contains all the fields of the former minus the SYST one.

Periodic tasks

Software tasks have access to the Instant at which they were scheduled to run through the scheduled variable. This information and the schedule API can be used to implement periodic tasks as shown in the example below.


# #![allow(unused_variables)]
#fn main() {
//! examples/periodic.rs

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

use cortex_m_semihosting::hprintln;
use panic_semihosting as _;
use rtfm::cyccnt::{Instant, U32Ext};

const PERIOD: u32 = 8_000_000;

// NOTE: does NOT work on QEMU!
#[rtfm::app(device = lm3s6965, monotonic = rtfm::cyccnt::CYCCNT)]
const APP: () = {
    #[init(schedule = [foo])]
    fn init(cx: init::Context) {
        // omitted: initialization of `CYCCNT`

        cx.schedule.foo(Instant::now() + PERIOD.cycles()).unwrap();
    }

    #[task(schedule = [foo])]
    fn foo(cx: foo::Context) {
        let now = Instant::now();
        hprintln!("foo(scheduled = {:?}, now = {:?})", cx.scheduled, now).unwrap();

        cx.schedule.foo(cx.scheduled + PERIOD.cycles()).unwrap();
    }

    extern "C" {
        fn UART0();
    }
};

#}

This is the output produced by the example. Note that there is zero drift / jitter even though schedule.foo was invoked at the end of foo. Using Instant::now instead of scheduled would have resulted in drift / jitter.

foo(scheduled = Instant(8000000), now = Instant(8000196))
foo(scheduled = Instant(16000000), now = Instant(16000196))
foo(scheduled = Instant(24000000), now = Instant(24000196))

Baseline

For the tasks scheduled from init we have exact information about their scheduled time. For hardware tasks there's no scheduled time because these tasks are asynchronous in nature. For hardware tasks the runtime provides a start time, which indicates the time at which the task handler started executing.

Note that start is not equal to the arrival time of the event that fired the task. Depending on the priority of the task and the load of the system the start time could be very far off from the event arrival time.

What do you think will be the value of scheduled for software tasks that are spawned instead of scheduled? The answer is that spawned tasks inherit the baseline time of the context that spawned it. The baseline of hardware tasks is their start time, the baseline of software tasks is their scheduled time and the baseline of init is the system start time or time zero (Instant::zero()). idle doesn't really have a baseline but tasks spawned from it will use Instant::now() as their baseline time.

The example below showcases the different meanings of the baseline.


# #![allow(unused_variables)]
#fn main() {
//! examples/baseline.rs

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

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use panic_semihosting as _;

// NOTE: does NOT properly work on QEMU
#[rtfm::app(device = lm3s6965, monotonic = rtfm::cyccnt::CYCCNT)]
const APP: () = {
    #[init(spawn = [foo])]
    fn init(cx: init::Context) {
        // omitted: initialization of `CYCCNT`

        hprintln!("init(baseline = {:?})", cx.start).unwrap();

        // `foo` inherits the baseline of `init`: `Instant(0)`
        cx.spawn.foo().unwrap();
    }

    #[task(schedule = [foo])]
    fn foo(cx: foo::Context) {
        static mut ONCE: bool = true;

        hprintln!("foo(baseline = {:?})", cx.scheduled).unwrap();

        if *ONCE {
            *ONCE = false;

            rtfm::pend(Interrupt::UART0);
        } else {
            debug::exit(debug::EXIT_SUCCESS);
        }
    }

    #[task(binds = UART0, spawn = [foo])]
    fn uart0(cx: uart0::Context) {
        hprintln!("UART0(baseline = {:?})", cx.start).unwrap();

        // `foo` inherits the baseline of `UART0`: its `start` time
        cx.spawn.foo().unwrap();
    }

    extern "C" {
        fn UART1();
    }
};

#}

Running the program on real hardware produces the following output in the console:

init(baseline = Instant(0))
foo(baseline = Instant(0))
UART0(baseline = Instant(904))
foo(baseline = Instant(904))