The app attribute

This is the smallest possible RTFM application:


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

#![no_main]
#![no_std]

use panic_semihosting as _; // panic handler
use rtfm::app;

#[app(device = lm3s6965)]
const APP: () = {};

#}

All RTFM applications use the app attribute (#[app(..)]). This attribute must be applied to a const item that contains items. The app attribute has a mandatory device argument that takes a path as a value. This path must point to a peripheral access crate (PAC) generated using svd2rust v0.14.x or newer. The app attribute will expand into a suitable entry point so it's not required to use the cortex_m_rt::entry attribute.

ASIDE: Some of you may be wondering why we are using a const item as a module and not a proper mod item. The reason is that using attributes on modules requires a feature gate, which requires a nightly toolchain. To make RTFM work on stable we use the const item instead. When more parts of macros 1.2 are stabilized we'll move from a const item to a mod item and eventually to a crate level attribute (#![app]).

init

Within the pseudo-module the app attribute expects to find an initialization function marked with the init attribute. This function must have signature fn(init::Context) [-> init::LateResources] (the return type is not always required).

This initialization function will be the first part of the application to run. The init function will run with interrupts disabled and has exclusive access to Cortex-M and, optionally, device specific peripherals through the core and device fields of init::Context.

static mut variables declared at the beginning of init will be transformed into &'static mut references that are safe to access.

The example below shows the types of the core and device fields and showcases safe access to a static mut variable. The device field is only available when the peripherals argument is set to true (it defaults to false).


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

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

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

#[rtfm::app(device = lm3s6965, peripherals = true)]
const APP: () = {
    #[init]
    fn init(cx: init::Context) {
        static mut X: u32 = 0;

        // Cortex-M peripherals
        let _core: cortex_m::Peripherals = cx.core;

        // Device specific peripherals
        let _device: lm3s6965::Peripherals = cx.device;

        // Safe access to local `static mut` variable
        let _x: &'static mut u32 = X;

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

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

#}

Running the example will print init to the console and then exit the QEMU process.

$ cargo run --example init
init

idle

A function marked with the idle attribute can optionally appear in the pseudo-module. This function is used as the special idle task and must have signature fn(idle::Context) - > !.

When present, the runtime will execute the idle task after init. Unlike init, idle will run with interrupts enabled and it's not allowed to return so it must run forever.

When no idle function is declared, the runtime sets the SLEEPONEXIT bit and then sends the microcontroller to sleep after running init.

Like in init, static mut variables will be transformed into &'static mut references that are safe to access.

The example below shows that idle runs after init.


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

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

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

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    #[init]
    fn init(_: init::Context) {
        hprintln!("init").unwrap();
    }

    #[idle]
    fn idle(_: idle::Context) -> ! {
        static mut X: u32 = 0;

        // Safe access to local `static mut` variable
        let _x: &'static mut u32 = X;

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

        debug::exit(debug::EXIT_SUCCESS);

        loop {}
    }
};

#}
$ cargo run --example idle
init
idle

Hardware tasks

To declare interrupt handlers the framework provides a #[task] attribute that can be attached to functions. This attribute takes a binds argument whose value is the name of the interrupt to which the handler will be bound to; the function adornated with this attribute becomes the interrupt handler. Within the framework these type of tasks are referred to as hardware tasks, because they start executing in reaction to a hardware event.

The example below demonstrates the use of the #[task] attribute to declare an interrupt handler. Like in the case of #[init] and #[idle] local static mut variables are safe to use within a hardware task.


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

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

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

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    #[init]
    fn init(_: init::Context) {
        // Pends the UART0 interrupt but its handler won't run until *after*
        // `init` returns because interrupts are disabled
        rtfm::pend(Interrupt::UART0); // equivalent to NVIC::pend

        hprintln!("init").unwrap();
    }

    #[idle]
    fn idle(_: idle::Context) -> ! {
        // interrupts are enabled again; the `UART0` handler runs at this point

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

        rtfm::pend(Interrupt::UART0);

        debug::exit(debug::EXIT_SUCCESS);

        loop {}
    }

    #[task(binds = UART0)]
    fn uart0(_: uart0::Context) {
        static mut TIMES: u32 = 0;

        // Safe access to local `static mut` variable
        *TIMES += 1;

        hprintln!(
            "UART0 called {} time{}",
            *TIMES,
            if *TIMES > 1 { "s" } else { "" }
        )
        .unwrap();
    }
};

#}
$ cargo run --example hardware
init
UART0 called 1 time
idle
UART0 called 2 times

So far all the RTFM applications we have seen look no different that the applications one can write using only the cortex-m-rt crate. From this point we start introducing features unique to RTFM.

Priorities

The static priority of each handler can be declared in the task attribute using the priority argument. Tasks can have priorities in the range 1..=(1 << NVIC_PRIO_BITS) where NVIC_PRIO_BITS is a constant defined in the device crate. When the priority argument is omitted the priority is assumed to be 1. The idle task has a non-configurable static priority of 0, the lowest priority.

When several tasks are ready to be executed the one with highest static priority will be executed first. Task prioritization can be observed in the following scenario: an interrupt signal arrives during the execution of a low priority task; the signal puts the higher priority task in the pending state. The difference in priority results in the higher priority task preempting the lower priority one: the execution of the lower priority task is suspended and the higher priority task is executed to completion. Once the higher priority task has terminated the lower priority task is resumed.

The following example showcases the priority based scheduling of tasks.


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

#![no_main]
#![no_std]

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

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

    #[task(binds = GPIOA, priority = 1)]
    fn gpioa(_: gpioa::Context) {
        hprintln!("GPIOA - start").unwrap();
        rtfm::pend(Interrupt::GPIOC);
        hprintln!("GPIOA - end").unwrap();
        debug::exit(debug::EXIT_SUCCESS);
    }

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

    #[task(binds = GPIOC, priority = 2)]
    fn gpioc(_: gpioc::Context) {
        hprintln!(" GPIOC - start").unwrap();
        rtfm::pend(Interrupt::GPIOB);
        hprintln!(" GPIOC - end").unwrap();
    }
};

#}
$ cargo run --example preempt
GPIOA - start
 GPIOC - start
 GPIOC - end
 GPIOB
GPIOA - end

Note that the task gpiob does not preempt task gpioc because its priority is the same as gpioc's. However, once gpioc terminates the execution of task gpiob is prioritized over gpioa's due to its higher priority. gpioa is resumed only after gpiob terminates.

One more note about priorities: choosing a priority higher than what the device supports (that is 1 << NVIC_PRIO_BITS) will result in a compile error. Due to limitations in the language the error message is currently far from helpful: it will say something along the lines of "evaluation of constant value failed" and the span of the error will not point out to the problematic interrupt value -- we are sorry about this!