Late resources

Some resources are initialized at runtime after the init function returns. It's important that these resources (static variables) are fully initialized before tasks are allowed to run, that is they must be initialized while interrupts are disabled.

The example below shows the kind of code that the framework generates to initialize late resources.


# #![allow(unused_variables)]
#fn main() {
#[rtic::app(device = ..)]
const APP: () = {
    struct Resources {
        x: Thing,
    }

    #[init]
    fn init() -> init::LateResources {
        // ..

        init::LateResources {
            x: Thing::new(..),
        }
    }

    #[task(binds = UART0, resources = [x])]
    fn foo(c: foo::Context) {
        let x: &mut Thing = c.resources.x;

        x.frob();

        // ..
    }

    // ..
};
#}

The code generated by the framework looks like this:

fn init(c: init::Context) -> init::LateResources {
    // .. user code ..
}

fn foo(c: foo::Context) {
    // .. user code ..
}

// Public API
pub mod init {
    pub struct LateResources {
        pub x: Thing,
    }

    // ..
}

pub mod foo {
    pub struct Resources<'a> {
        pub x: &'a mut Thing,
    }

    pub struct Context<'a> {
        pub resources: Resources<'a>,
        // ..
    }
}

/// Implementation details
const APP: () = {
    // uninitialized static
    static mut x: MaybeUninit<Thing> = MaybeUninit::uninit();

    #[no_mangle]
    unsafe fn main() -> ! {
        cortex_m::interrupt::disable();

        // ..

        let late = init(..);

        // initialization of late resources
        x.as_mut_ptr().write(late.x);

        cortex_m::interrupt::enable(); //~ compiler fence

        // exceptions, interrupts and tasks can preempt `main` at this point

        idle(..)
    }

    #[no_mangle]
    unsafe fn UART0() {
        foo(foo::Context {
            resources: foo::Resources {
                // `x` has been initialized at this point
                x: &mut *x.as_mut_ptr(),
            },
            // ..
        })
    }
};

An important detail here is that interrupt::enable behaves like a compiler fence, which prevents the compiler from reordering the write to X to after interrupt::enable. If the compiler were to do that kind of reordering there would be a data race between that write and whatever operation foo performs on X.

Architectures with more complex instruction pipelines may need a memory barrier (atomic::fence) instead of a compiler fence to fully flush the write operation before interrupts are re-enabled. The ARM Cortex-M architecture doesn't need a memory barrier in single-core context.