Tips & tricks

Generics

Resources shared between two or more tasks implement the Mutex trait in all contexts, even on those where a critical section is not required to access the data. This lets you easily write generic code that operates on resources and can be called from different tasks. Here's one such example:


#![allow(unused)]
fn main() {
//! examples/generics.rs

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

extern crate panic_semihosting;

use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
use rtfm::{app, Mutex};

#[app(device = lm3s6965)]
const APP: () = {
    static mut SHARED: u32 = 0;

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

    #[interrupt(resources = [SHARED])]
    fn UART0() {
        static mut STATE: u32 = 0;

        hprintln!("UART0(STATE = {})", *STATE).unwrap();

        advance(STATE, resources.SHARED);

        rtfm::pend(Interrupt::UART1);

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

    #[interrupt(priority = 2, resources = [SHARED])]
    fn UART1() {
        static mut STATE: u32 = 0;

        hprintln!("UART1(STATE = {})", *STATE).unwrap();

        // just to show that `SHARED` can be accessed directly and ..
        *resources.SHARED += 0;
        // .. also through a (no-op) `lock`
        resources.SHARED.lock(|shared| *shared += 0);

        advance(STATE, resources.SHARED);
    }
};

fn advance(state: &mut u32, mut shared: impl Mutex<T = u32>) {
    *state += 1;

    let (old, new) = shared.lock(|shared| {
        let old = *shared;
        *shared += *state;
        (old, *shared)
    });

    hprintln!("SHARED: {} -> {}", old, new).unwrap();
}
}
$ cargo run --example generics
UART1(STATE = 0)
SHARED: 0 -> 1
UART0(STATE = 0)
SHARED: 1 -> 2
UART1(STATE = 1)
SHARED: 2 -> 4

This also lets you change the static priorities of tasks without having to rewrite code. If you consistently use locks to access the data behind shared resources then your code will continue to compile when you change the priority of tasks.

Conditional compilation

You can use conditional compilation (#[cfg]) on resources (static [mut] items) and tasks (fn items). The effect of using #[cfg] attributes is that the resource / task will not be injected into the prelude of tasks that use them (see resources, spawn and schedule) if the condition doesn't hold.

The example below logs a message whenever the foo task is spawned, but only if the program has been compiled using the dev profile.


#![allow(unused)]
fn main() {
//! examples/cfg.rs

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

extern crate panic_semihosting;

#[cfg(debug_assertions)]
use cortex_m_semihosting::hprintln;
use rtfm::app;

#[app(device = lm3s6965)]
const APP: () = {
    #[cfg(debug_assertions)] // <- `true` when using the `dev` profile
    static mut COUNT: u32 = 0;

    #[init]
    fn init() {
        // ..
    }

    #[task(priority = 3, resources = [COUNT], spawn = [log])]
    fn foo() {
        #[cfg(debug_assertions)]
        {
            *resources.COUNT += 1;

            spawn.log(*resources.COUNT).ok();
        }

        // this wouldn't compile in `release` mode
        // *resources.COUNT += 1;

        // ..
    }

    #[cfg(debug_assertions)]
    #[task]
    fn log(n: u32) {
        hprintln!(
            "foo has been called {} time{}",
            n,
            if n == 1 { "" } else { "s" }
        )
        .ok();
    }

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

Running tasks from RAM

The main goal of moving the specification of RTFM applications to attributes in RTFM v0.4.x was to allow inter-operation with other attributes. For example, the link_section attribute can be applied to tasks to place them in RAM; this can improve performance in some cases.

IMPORTANT: In general, the link_section, export_name and no_mangle attributes are very powerful but also easy to misuse. Incorrectly using any of these attributes can cause undefined behavior; you should always prefer to use safe, higher level attributes around them like cortex-m-rt's interrupt and exception attributes.

In the particular case of RAM functions there's no safe abstraction for it in cortex-m-rt v0.6.5 but there's an RFC for adding a ramfunc attribute in a future release.

The example below shows how to place the higher priority task, bar, in RAM.


#![allow(unused)]
fn main() {
//! examples/ramfunc.rs

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

extern crate panic_semihosting;

use cortex_m_semihosting::{debug, hprintln};
use rtfm::app;

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

    #[inline(never)]
    #[task]
    fn foo() {
        hprintln!("foo").unwrap();

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

    // run this task from RAM
    #[inline(never)]
    #[link_section = ".data.bar"]
    #[task(priority = 2, spawn = [foo])]
    fn bar() {
        spawn.foo().unwrap();
    }

    extern "C" {
        fn UART0();

        // run the task dispatcher from RAM
        #[link_section = ".data.UART1"]
        fn UART1();
    }
};
}

Running this program produces the expected output.

$ cargo run --example ramfunc
foo

One can look at the output of cargo-nm to confirm that bar ended in RAM (0x2000_0000), whereas foo ended in Flash (0x0000_0000).

$ cargo nm --example ramfunc --release | grep ' foo::'
20000100 B foo::FREE_QUEUE::ujkptet2nfdw5t20
200000dc B foo::INPUTS::thvubs85b91dg365
000002c6 T foo::sidaht420cg1mcm8
$ cargo nm --example ramfunc --release | grep ' bar::'
20000100 B bar::FREE_QUEUE::lk14244m263eivix
200000dc B bar::INPUTS::mi89534s44r1mnj1
20000000 T bar::ns9009yhw2dc2y25

binds

NOTE: Requires RTFM ~0.4.2

You can give hardware tasks more task-like names using the binds argument: you name the function as you wish and specify the name of the interrupt / exception in the binds argument. Types like Spawn will be placed in a module named after the function, not the interrupt / exception. Example below:


#![allow(unused)]
fn main() {
//! examples/binds.rs

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

extern crate panic_semihosting;

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

// `examples/interrupt.rs` rewritten to use `binds`
#[app(device = lm3s6965)]
const APP: () = {
    #[init]
    fn init() {
        rtfm::pend(Interrupt::UART0);

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

    #[idle]
    fn idle() -> ! {
        hprintln!("idle").unwrap();

        rtfm::pend(Interrupt::UART0);

        debug::exit(debug::EXIT_SUCCESS);

        loop {}
    }

    #[interrupt(binds = UART0)]
    fn foo() {
        static mut TIMES: u32 = 0;

        *TIMES += 1;

        hprintln!(
            "foo called {} time{}",
            *TIMES,
            if *TIMES > 1 { "s" } else { "" }
        )
        .unwrap();
    }
};
}
$ cargo run --example binds
init
foo called 1 time
idle
foo called 2 times

Indirection for faster message passing

Message passing always involves copying the payload from the sender into a static variable and then from the static variable into the receiver. Thus sending a large buffer, like a [u8; 128], as a message involves two expensive memcpys. To minimize the message passing overhead one can use indirection: instead of sending the buffer by value, one can send an owning pointer into the buffer.

One can use a global allocator to achieve indirection (alloc::Box, alloc::Rc, etc.), which requires using the nightly channel as of Rust v1.34.0, or one can use a statically allocated memory pool like heapless::Pool.

Here's an example where heapless::Pool is used to "box" buffers of 128 bytes.


#![allow(unused)]
fn main() {
//! examples/pool.rs

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

extern crate panic_semihosting;

use cortex_m_semihosting::{debug, hprintln};
use heapless::{
    pool,
    pool::singleton::{Box, Pool},
};
use lm3s6965::Interrupt;
use rtfm::app;

// Declare a pool of 128-byte memory blocks
pool!(P: [u8; 128]);

#[app(device = lm3s6965)]
const APP: () = {
    #[init]
    fn init() {
        static mut MEMORY: [u8; 512] = [0; 512];

        // Increase the capacity of the memory pool by ~4
        P::grow(MEMORY);

        rtfm::pend(Interrupt::I2C0);
    }

    #[interrupt(priority = 2, spawn = [foo, bar])]
    fn I2C0() {
        // claim a memory block, leave it uninitialized and ..
        let x = P::alloc().unwrap().freeze();

        // .. send it to the `foo` task
        spawn.foo(x).ok().unwrap();

        // send another block to the task `bar`
        spawn.bar(P::alloc().unwrap().freeze()).ok().unwrap();
    }

    #[task]
    fn foo(x: Box<P>) {
        hprintln!("foo({:?})", x.as_ptr()).unwrap();

        // explicitly return the block to the pool
        drop(x);

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

    #[task(priority = 2)]
    fn bar(x: Box<P>) {
        hprintln!("bar({:?})", x.as_ptr()).unwrap();

        // this is done automatically so we can omit the call to `drop`
        // drop(x);
    }

    extern "C" {
        fn UART0();
        fn UART1();
    }
};
}
$ cargo run --example binds
bar(0x2000008c)
foo(0x20000110)