Resources

One of the limitations of the attributes provided by the cortex-m-rt crate is that sharing data (or peripherals) between interrupts, or between an interrupt and the entry function, requires a cortex_m::interrupt::Mutex, which always requires disabling all interrupts to access the data. Disabling all the interrupts is not always required for memory safety but the compiler doesn't have enough information to optimize the access to the shared data.

The app attribute has a full view of the application thus it can optimize access to static variables. In RTFM we refer to the static variables declared inside the app pseudo-module as resources. To access a resource the context (init, idle, interrupt or exception) one must first declare the resource in the resources argument of its attribute.

In the example below two interrupt handlers access the same resource. No Mutex is required in this case because the two handlers run at the same priority and no preemption is possible. The SHARED resource can only be accessed by these two handlers.


# #![allow(unused_variables)]
#fn main() {
//! examples/resource.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: () = {
    // A resource
    static mut SHARED: u32 = 0;

    #[init]
    fn init(_: init::Context) {
        rtfm::pend(Interrupt::UART0);
        rtfm::pend(Interrupt::UART1);
    }

    #[idle]
    fn idle(_: idle::Context) -> ! {
        debug::exit(debug::EXIT_SUCCESS);

        // error: `SHARED` can't be accessed from this context
        // SHARED += 1;

        loop {}
    }

    // `SHARED` can be access from this context
    #[interrupt(resources = [SHARED])]
    fn UART0(mut c: UART0::Context) {
        *c.resources.SHARED += 1;

        hprintln!("UART0: SHARED = {}", c.resources.SHARED).unwrap();
    }

    // `SHARED` can be access from this context
    #[interrupt(resources = [SHARED])]
    fn UART1(mut c: UART1::Context) {
        *c.resources.SHARED += 1;

        hprintln!("UART1: SHARED = {}", c.resources.SHARED).unwrap();
    }
};

#}
$ cargo run --example resource
UART0: SHARED = 1
UART1: SHARED = 2

Priorities

The priority of each handler can be declared in the interrupt and exception attributes. It's not possible to set the priority in any other way because the runtime takes ownership of the NVIC peripheral thus it's also not possible to change the priority of a handler / task at runtime. Thanks to this restriction the framework has knowledge about the static priorities of all interrupt and exception handlers.

Interrupts and exceptions can have priorities in the range 1..=(1 << NVIC_PRIO_BITS) where NVIC_PRIO_BITS is a constant defined in the device crate. The idle task has a priority of 0, the lowest priority.

Resources that are shared between handlers that run at different priorities require critical sections for memory safety. The framework ensures that critical sections are used but only where required: for example, no critical section is required by the highest priority handler that has access to the resource.

The critical section API provided by the RTFM framework (see Mutex) is based on dynamic priorities rather than on disabling interrupts. The consequence is that these critical sections will prevent some handlers, including all the ones that contend for the resource, from starting but will let higher priority handlers, that don't contend for the resource, run.

In the example below we have three interrupt handlers with priorities ranging from one to three. The two handlers with the lower priorities contend for the SHARED resource. The lowest priority handler needs to lock the SHARED resource to access its data, whereas the mid priority handler can directly access its data. The highest priority handler is free to preempt the critical section created by the lowest priority handler.


# #![allow(unused_variables)]
#fn main() {
//! examples/lock.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: () = {
    static mut SHARED: u32 = 0;

    #[init]
    fn init(_: init::Context) {
        rtfm::pend(Interrupt::GPIOA);
    }

    // when omitted priority is assumed to be `1`
    #[interrupt(resources = [SHARED])]
    fn GPIOA(mut c: GPIOA::Context) {
        hprintln!("A").unwrap();

        // the lower priority task requires a critical section to access the data
        c.resources.SHARED.lock(|shared| {
            // data can only be modified within this critical section (closure)
            *shared += 1;

            // GPIOB will *not* run right now due to the critical section
            rtfm::pend(Interrupt::GPIOB);

            hprintln!("B - SHARED = {}", *shared).unwrap();

            // GPIOC does not contend for `SHARED` so it's allowed to run now
            rtfm::pend(Interrupt::GPIOC);
        });

        // critical section is over: GPIOB can now start

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

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

    #[interrupt(priority = 2, resources = [SHARED])]
    fn GPIOB(mut c: GPIOB::Context) {
        // the higher priority task does *not* need a critical section
        *c.resources.SHARED += 1;

        hprintln!("D - SHARED = {}", *c.resources.SHARED).unwrap();
    }

    #[interrupt(priority = 3)]
    fn GPIOC(_: GPIOC::Context) {
        hprintln!("C").unwrap();
    }
};

#}
$ cargo run --example lock
A
B - SHARED = 1
C
D - SHARED = 2
E

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!

Late resources

Unlike normal static variables, which need to be assigned an initial value when declared, resources can be initialized at runtime. We refer to these runtime initialized resources as late resources. Late resources are useful for moving (as in transferring ownership) peripherals initialized in init into interrupt and exception handlers.

Late resources are declared like normal resources but that are given an initial value of () (the unit value). init must return the initial values of all late resources packed in a struct of type init::LateResources.

The example below uses late resources to stablish a lockless, one-way channel between the UART0 interrupt handler and the idle function. A single producer single consumer Queue is used as the channel. The queue is split into consumer and producer end points in init and then each end point is stored in a different resource; UART0 owns the producer resource and idle owns the consumer resource.


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

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

extern crate panic_semihosting;

use cortex_m_semihosting::{debug, hprintln};
use heapless::{
    consts::*,
    spsc::{Consumer, Producer, Queue},
};
use lm3s6965::Interrupt;

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    // Late resources
    static mut P: Producer<'static, u32, U4> = ();
    static mut C: Consumer<'static, u32, U4> = ();

    #[init]
    fn init(_: init::Context) -> init::LateResources {
        // NOTE: we use `Option` here to work around the lack of
        // a stable `const` constructor
        static mut Q: Option<Queue<u32, U4>> = None;

        *Q = Some(Queue::new());
        let (p, c) = Q.as_mut().unwrap().split();

        // Initialization of late resources
        init::LateResources { P: p, C: c }
    }

    #[idle(resources = [C])]
    fn idle(c: idle::Context) -> ! {
        loop {
            if let Some(byte) = c.resources.C.dequeue() {
                hprintln!("received message: {}", byte).unwrap();

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

    #[interrupt(resources = [P])]
    fn UART0(c: UART0::Context) {
        c.resources.P.enqueue(42).unwrap();
    }
};

#}
$ cargo run --example late
received message: 42

static resources

static variables can also be used as resources. Tasks can only get & (shared) references to these resources but locks are never required to access their data. You can think of static resources as plain static variables that can be initialized at runtime and have better scoping rules: you can control which tasks can access the variable, instead of the variable being visible to all the functions in the scope it was declared in.

In the example below a key is loaded (or created) at runtime and then used from two tasks that run at different priorities.


# #![allow(unused_variables)]
#fn main() {
//! examples/static.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: () = {
    static KEY: u32 = ();

    #[init]
    fn init(_: init::Context) -> init::LateResources {
        rtfm::pend(Interrupt::UART0);
        rtfm::pend(Interrupt::UART1);

        init::LateResources { KEY: 0xdeadbeef }
    }

    #[interrupt(resources = [KEY])]
    fn UART0(c: UART0::Context) {
        hprintln!("UART0(KEY = {:#x})", c.resources.KEY).unwrap();

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

    #[interrupt(priority = 2, resources = [KEY])]
    fn UART1(c: UART1::Context) {
        hprintln!("UART1(KEY = {:#x})", c.resources.KEY).unwrap();
    }
};

#}
$ cargo run --example static
UART1(KEY = 0xdeadbeef)
UART0(KEY = 0xdeadbeef)