Timer queue

When the timer-queue feature is enabled the RTFM framework includes a global timer queue that applications can use to schedule software tasks to run at some time in the future.

NOTE: The timer-queue feature can't be enabled when the target is thumbv6m-none-eabi because there's no timer queue support for ARMv6-M. This may change in the future.

NOTE: When the timer-queue feature is enabled you will not be able to use the SysTick exception as a hardware task because the runtime uses it to implement the global timer queue.

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

The RTFM runtime includes a monotonic, non-decreasing, 32-bit timer which can be queried using the Instant::now constructor. A Duration can be added to Instant::now() to obtain an Instant into the future. The monotonic timer is disabled while init runs so Instant::now() always returns the value Instant(0 /* clock cycles */); the timer is enabled right before the interrupts are re-enabled and idle is executed.

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. 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)]
fn main() {
//! examples/schedule.rs

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

extern crate panic_semihosting;

use cortex_m_semihosting::hprintln;
use rtfm::{app, Instant};

// NOTE: does NOT work on QEMU!
#[app(device = lm3s6965)]
const APP: () = {
    #[init(schedule = [foo, bar])]
    fn init() {
        let now = Instant::now();

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

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

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

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

    #[task]
    fn bar() {
        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)

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)]
fn main() {
//! examples/periodic.rs

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

extern crate panic_semihosting;

use cortex_m_semihosting::hprintln;
use rtfm::{app, Instant};

const PERIOD: u32 = 8_000_000;

// NOTE: does NOT work on QEMU!
#[app(device = lm3s6965)]
const APP: () = {
    #[init(schedule = [foo])]
    fn init() {
        schedule.foo(Instant::now() + PERIOD.cycles()).unwrap();
    }

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

        schedule.foo(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 start, the baseline of software tasks is scheduled and the baseline of init is start = Instant(0). 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)]
fn main() {
//! examples/baseline.rs

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

extern crate panic_semihosting;

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use rtfm::app;

// NOTE: does NOT properly work on QEMU
#[app(device = lm3s6965)]
const APP: () = {
    #[init(spawn = [foo])]
    fn init() {
        hprintln!("init(baseline = {:?})", start).unwrap();

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

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

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

        if *ONCE {
            *ONCE = false;

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

    #[interrupt(spawn = [foo])]
    fn UART0() {
        hprintln!("UART0(baseline = {:?})", start).unwrap();

        // `foo` inherits the baseline of `UART0`: its `start` time
        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))

Caveats

The Instant and Duration APIs are meant to be exclusively used with the schedule API to schedule tasks with a precision of a single core clock cycle. These APIs are not, for example, meant to be used to time external events like a user pressing a button.

The timer queue feature internally uses the system timer, SysTick. This timer is a 24-bit counter and it's clocked at the core clock frequency so tasks scheduled more than (1 << 24).cycles() in the future will incur in additional overhead, proportional to the size of their Duration, compared to task scheduled with Durations below that threshold.

If you need periodic tasks with periods greater than (1 << 24).cycles() you likely don't need a timer with a resolution of one core clock cycle so we advise you instead use a device timer with an appropriate prescaler.

We can't stop you from using Instant to measure external events so please be aware that Instant.sub / Instant.elapsed will never return a Duration equal or greater than (1 << 31).cycles() so you won't be able to measure events that last more than 1 << 31 core clock cycles (e.g. seconds).

Adding a Duration equal or greater than (1 << 31).cycles() to an Instant will effectively overflow it so it's not possible to schedule a task more than (1 << 31).cycles() in the future. There are some debug assertions in place to catch this kind of user error but it's not possible to prevent it with 100% success rate because one can always write (instant + duration) + duration and bypass the runtime checks.