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