Resources

The framework provides an abstraction to share data between any of the contexts we saw in the previous section (task handlers, init and idle): resources.

Resources are data visible only to functions declared within the #[app] pseudo-module. The framework gives the user complete control over which context can access which resource.

All resources are declared as a single struct within the #[app] pseudo-module. Each field in the structure corresponds to a different resource. Resources can optionally be given an initial value using the #[init] attribute. Resources that are not given an initial value are referred to as late resources and are covered in more detail in a follow up section in this page.

Each context (task handler, init or idle) must declare the resources it intends to access in its corresponding metadata attribute using the resources argument. This argument takes a list of resource names as its value. The listed resources are made available to the context under the resources field of the Context structure.

The example application shown below contains two interrupt handlers that share access to a resource named shared.


# #![allow(unused_variables)]
#fn main() {
//! examples/resource.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: () = {
    struct Resources {
        // A resource
        #[init(0)]
        shared: u32,
    }

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

    // `shared` cannot be accessed from this context
    #[idle]
    fn idle(_cx: idle::Context) -> ! {
        debug::exit(debug::EXIT_SUCCESS);

        // error: no `resources` field in `idle::Context`
        // _cx.resources.shared += 1;

        loop {}
    }

    // `shared` can be accessed from this context
    #[task(binds = UART0, resources = [shared])]
    fn uart0(cx: uart0::Context) {
        let shared: &mut u32 = cx.resources.shared;
        *shared += 1;

        hprintln!("UART0: shared = {}", shared).unwrap();
    }

    // `shared` can be accessed from this context
    #[task(binds = UART1, resources = [shared])]
    fn uart1(cx: uart1::Context) {
        *cx.resources.shared += 1;

        hprintln!("UART1: shared = {}", cx.resources.shared).unwrap();
    }
};

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

Note that the shared resource cannot accessed from idle. Attempting to do so results in a compile error.

lock

In the presence of preemption critical sections are required to mutate shared data in a data race free manner. As the framework has complete knowledge over the priorities of tasks and which tasks can access which resources it enforces that critical sections are used where required for memory safety.

Where a critical section is required the framework hands out a resource proxy instead of a reference. This resource proxy is a structure that implements the Mutex trait. The only method on this trait, lock, runs its closure argument in a critical section.

The critical section created by the lock API is based on dynamic priorities: it temporarily raises the dynamic priority of the context to a ceiling priority that prevents other tasks from preempting the critical section. This synchronization protocol is known as the Immediate Ceiling Priority Protocol (ICPP).

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, which cannot access the shared resource, 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]

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

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    struct Resources {
        #[init(0)]
        shared: u32,
    }

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

    // when omitted priority is assumed to be `1`
    #[task(binds = GPIOA, 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);
    }

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

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

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

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

Late resources

Late resources are resources that are not given an initial value at compile using the #[init] attribute but instead are initialized are runtime using the init::LateResources values returned by the init function.

Late resources are useful for moving (as in transferring the ownership of) peripherals initialized in init into interrupt handlers.

The example below uses late resources to stablish a lockless, one-way channel between the UART0 interrupt handler and the idle task. 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]

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

#[rtfm::app(device = lm3s6965)]
const APP: () = {
    // Late resources
    struct Resources {
        p: Producer<'static, u32, U4>,
        c: Consumer<'static, u32, U4>,
    }

    #[init]
    fn init(_: init::Context) -> init::LateResources {
        static mut Q: Queue<u32, U4> = Queue(i::Queue::new());

        let (p, c) = Q.split();

        // Initialization of late resources
        init::LateResources { p, 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);
            }
        }
    }

    #[task(binds = UART0, resources = [p])]
    fn uart0(c: uart0::Context) {
        c.resources.p.enqueue(42).unwrap();
    }
};

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

Only shared access

By default the framework assumes that all tasks require exclusive access (&mut-) to resources but it is possible to specify that a task only requires shared access (&-) to a resource using the &resource_name syntax in the resources list.

The advantage of specifying shared access (&-) to a resource is that no locks are required to access the resource even if the resource is contended by several tasks running at different priorities. The downside is that the task only gets a shared reference (&-) to the resource, limiting the operations it can perform on it, but where a shared reference is enough this approach reduces the number of required locks.

Note that in this release of RTFM it is not possible to request both exclusive access (&mut-) and shared access (&-) to the same resource from different tasks. Attempting to do so will result in a compile error.

In the example below a key (e.g. a cryptographic key) is loaded (or created) at runtime and then used from two tasks that run at different priorities without any kind of lock.


# #![allow(unused_variables)]
#fn main() {
//! examples/static.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: () = {
    struct Resources {
        key: u32,
    }

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

        init::LateResources { key: 0xdeadbeef }
    }

    #[task(binds = UART0, resources = [&key])]
    fn uart0(cx: uart0::Context) {
        let key: &u32 = cx.resources.key;
        hprintln!("UART0(key = {:#x})", key).unwrap();

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

    #[task(binds = UART1, priority = 2, resources = [&key])]
    fn uart1(cx: uart1::Context) {
        hprintln!("UART1(key = {:#x})", cx.resources.key).unwrap();
    }
};

#}
$ cargo run --example only-shared-access
UART1(key = 0xdeadbeef)
UART0(key = 0xdeadbeef)