Software tasks

RTFM treats interrupt and exception handlers as hardware tasks. Hardware tasks are invoked by the hardware in response to events, like pressing a button. RTFM also supports software tasks which can be spawned by the software from any execution context.

Software tasks can also be assigned priorities and are dispatched from interrupt handlers. RTFM requires that free interrupts are declared in an extern block when using software tasks; these free interrupts will be used to dispatch the software tasks. An advantage of software tasks over hardware tasks is that many tasks can be mapped to a single interrupt handler.

Software tasks are declared by applying the task attribute to functions. To be able to spawn a software task the name of the task must appear in the spawn argument of the context attribute (init, idle, interrupt, etc.).

The example below showcases three software tasks that run at 2 different priorities. The three tasks map to 2 interrupts handlers.


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

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

extern crate panic_semihosting;

use cortex_m_semihosting::{debug, hprintln};

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    #[init(spawn = [foo])]
    fn init(c: init::Context) {
        c.spawn.foo().unwrap();
    }

    #[task(spawn = [bar, baz])]
    fn foo(c: foo::Context) {
        hprintln!("foo").unwrap();

        // spawns `bar` onto the task scheduler
        // `foo` and `bar` have the same priority so `bar` will not run until
        // after `foo` terminates
        c.spawn.bar().unwrap();

        // spawns `baz` onto the task scheduler
        // `baz` has higher priority than `foo` so it immediately preempts `foo`
        c.spawn.baz().unwrap();
    }

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

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

    #[task(priority = 2)]
    fn baz(_: baz::Context) {
        hprintln!("baz").unwrap();
    }

    // Interrupt handlers used to dispatch software tasks
    extern "C" {
        fn UART0();
        fn UART1();
    }
};

#}
$ cargo run --example task
foo
baz
bar

Message passing

The other advantage of software tasks is that messages can be passed to these tasks when spawning them. The type of the message payload must be specified in the signature of the task handler.

The example below showcases three tasks, two of them expect a message.


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

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

extern crate panic_semihosting;

use cortex_m_semihosting::{debug, hprintln};

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    #[init(spawn = [foo])]
    fn init(c: init::Context) {
        c.spawn.foo(/* no message */).unwrap();
    }

    #[task(spawn = [bar])]
    fn foo(c: foo::Context) {
        static mut COUNT: u32 = 0;

        hprintln!("foo").unwrap();

        c.spawn.bar(*COUNT).unwrap();
        *COUNT += 1;
    }

    #[task(spawn = [baz])]
    fn bar(c: bar::Context, x: u32) {
        hprintln!("bar({})", x).unwrap();

        c.spawn.baz(x + 1, x + 2).unwrap();
    }

    #[task(spawn = [foo])]
    fn baz(c: baz::Context, x: u32, y: u32) {
        hprintln!("baz({}, {})", x, y).unwrap();

        if x + y > 4 {
            debug::exit(debug::EXIT_SUCCESS);
        }

        c.spawn.foo().unwrap();
    }

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

#}
$ cargo run --example message
foo
bar(0)
baz(1, 2)
foo
bar(1)
baz(2, 3)

Capacity

Task dispatchers do not use any dynamic memory allocation. The memory required to store messages is statically reserved. The framework will reserve enough space for every context to be able to spawn each task at most once. This is a sensible default but the "inbox" capacity of each task can be controlled using the capacity argument of the task attribute.

The example below sets the capacity of the software task foo to 4. If the capacity is not specified then the second spawn.foo call in UART0 would fail.


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

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

extern crate panic_semihosting;

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

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    #[init]
    fn init(_: init::Context) {
        rtfm::pend(Interrupt::UART0);
    }

    #[interrupt(spawn = [foo, bar])]
    fn UART0(c: UART0::Context) {
        c.spawn.foo(0).unwrap();
        c.spawn.foo(1).unwrap();
        c.spawn.foo(2).unwrap();
        c.spawn.foo(3).unwrap();

        c.spawn.bar().unwrap();
    }

    #[task(capacity = 4)]
    fn foo(_: foo::Context, x: u32) {
        hprintln!("foo({})", x).unwrap();
    }

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

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

    // Interrupt handlers used to dispatch software tasks
    extern "C" {
        fn UART1();
    }
};

#}
$ cargo run --example capacity
foo(0)
foo(1)
foo(2)
foo(3)
bar