rtic/book/en/src/internals/critical-sections.md

14 KiB

Critical sections

When a resource (static variable) is shared between two, or more, tasks that run at different priorities some form of mutual exclusion is required to mutate the memory in a data race free manner. In RTIC we use priority-based critical sections to guarantee mutual exclusion (see the Immediate Ceiling Priority Protocol).

The critical section consists of temporarily raising the dynamic priority of the task. While a task is within this critical section all the other tasks that may request the resource are not allowed to start.

How high must the dynamic priority be to ensure mutual exclusion on a particular resource? The ceiling analysis is in charge of answering that question and will be discussed in the next section. This section will focus on the implementation of the critical section.

Resource proxy

For simplicity, let's look at a resource shared by two tasks that run at different priorities. Clearly one of the task can preempt the other; to prevent a data race the lower priority task must use a critical section when it needs to modify the shared memory. On the other hand, the higher priority task can directly modify the shared memory because it can't be preempted by the lower priority task. To enforce the use of a critical section on the lower priority task we give it a resource proxy, whereas we give a unique reference (&mut-) to the higher priority task.

The example below shows the different types handed out to each task:

#[rtic::app(device = ..)]
mut app {
    struct Resources {
        #[init(0)]
        x: u64,
    }

    #[interrupt(binds = UART0, priority = 1, resources = [x])]
    fn foo(c: foo::Context) {
        // resource proxy
        let mut x: resources::x = c.resources.x;

        x.lock(|x: &mut u64| {
            // critical section
            *x += 1
        });
    }

    #[interrupt(binds = UART1, priority = 2, resources = [x])]
    fn bar(c: bar::Context) {
        let mut x: &mut u64 = c.resources.x;

        *x += 1;
    }

    // ..
}

Now let's see how these types are created by the framework.

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

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

pub mod resources {
    pub struct x {
        // ..
    }
}

pub mod foo {
    pub struct Resources {
        pub x: resources::x,
    }

    pub struct Context {
        pub resources: Resources,
        // ..
    }
}

pub mod bar {
    pub struct Resources<'a> {
        pub x: &'a mut u64,
    }

    pub struct Context {
        pub resources: Resources,
        // ..
    }
}

mod app {
    static mut x: u64 = 0;

    impl rtic::Mutex for resources::x {
        type T = u64;

        fn lock<R>(&mut self, f: impl FnOnce(&mut u64) -> R) -> R {
            // we'll check this in detail later
        }
    }

    #[no_mangle]
    unsafe fn UART0() {
        foo(foo::Context {
            resources: foo::Resources {
                x: resources::x::new(/* .. */),
            },
            // ..
        })
    }

    #[no_mangle]
    unsafe fn UART1() {
        bar(bar::Context {
            resources: bar::Resources {
                x: &mut x,
            },
            // ..
        })
    }
}

lock

Let's now zoom into the critical section itself. In this example, we have to raise the dynamic priority to at least 2 to prevent a data race. On the Cortex-M architecture the dynamic priority can be changed by writing to the BASEPRI register.

The semantics of the BASEPRI register are as follows:

  • Writing a value of 0 to BASEPRI disables its functionality.
  • Writing a non-zero value to BASEPRI changes the priority level required for interrupt preemption. However, this only has an effect when the written value is lower than the priority level of current execution context, but note that a lower hardware priority level means higher logical priority

Thus the dynamic priority at any point in time can be computed as

dynamic_priority = max(hw2logical(BASEPRI), hw2logical(static_priority))

Where static_priority is the priority programmed in the NVIC for the current interrupt, or a logical 0 when the current context is idle.

In this particular example we could implement the critical section as follows:

NOTE: this is a simplified implementation

impl rtic::Mutex for resources::x {
    type T = u64;

    fn lock<R, F>(&mut self, f: F) -> R
    where
        F: FnOnce(&mut u64) -> R,
    {
        unsafe {
            // start of critical section: raise dynamic priority to `2`
            asm!("msr BASEPRI, 192" : : : "memory" : "volatile");

            // run user code within the critical section
            let r = f(&mut x);

            // end of critical section: restore dynamic priority to its static value (`1`)
            asm!("msr BASEPRI, 0" : : : "memory" : "volatile");

            r
        }
    }
}

Here it's important to use the "memory" clobber in the asm! block. It prevents the compiler from reordering memory operations across it. This is important because accessing the variable x outside the critical section would result in a data race.

It's important to note that the signature of the lock method prevents nesting calls to it. This is required for memory safety, as nested calls would produce multiple unique references (&mut-) to x breaking Rust aliasing rules. See below:

#[interrupt(binds = UART0, priority = 1, resources = [x])]
fn foo(c: foo::Context) {
    // resource proxy
    let mut res: resources::x = c.resources.x;

    res.lock(|x: &mut u64| {
        res.lock(|alias: &mut u64| {
            //~^ error: `res` has already been uniquely borrowed (`&mut-`)
            // ..
        });
    });
}

Nesting

Nesting calls to lock on the same resource must be rejected by the compiler for memory safety but nesting lock calls on different resources is a valid operation. In that case we want to make sure that nesting critical sections never results in lowering the dynamic priority, as that would be unsound, and we also want to optimize the number of writes to the BASEPRI register and compiler fences. To that end we'll track the dynamic priority of the task using a stack variable and use that to decide whether to write to BASEPRI or not. In practice, the stack variable will be optimized away by the compiler but it still provides extra information to the compiler.

Consider this program:

#[rtic::app(device = ..)]
mod app {
    struct Resources {
        #[init(0)]
        x: u64,
        #[init(0)]
        y: u64,
    }

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

    #[interrupt(binds = UART0, priority = 1, resources = [x, y])]
    fn foo(c: foo::Context) {
        let mut x = c.resources.x;
        let mut y = c.resources.y;

        y.lock(|y| {
            *y += 1;

            *x.lock(|x| {
                x += 1;
            });

            *y += 1;
        });

        // mid-point

        x.lock(|x| {
            *x += 1;

            y.lock(|y| {
                *y += 1;
            });

            *x += 1;
        })
    }

    #[interrupt(binds = UART1, priority = 2, resources = [x])]
    fn bar(c: foo::Context) {
        // ..
    }

    #[interrupt(binds = UART2, priority = 3, resources = [y])]
    fn baz(c: foo::Context) {
        // ..
    }

    // ..
}

The code generated by the framework looks like this:

// omitted: user code

pub mod resources {
    pub struct x<'a> {
        priority: &'a Cell<u8>,
    }

    impl<'a> x<'a> {
        pub unsafe fn new(priority: &'a Cell<u8>) -> Self {
            x { priority }
        }

        pub unsafe fn priority(&self) -> &Cell<u8> {
            self.priority
        }
    }

    // repeat for `y`
}

pub mod foo {
    pub struct Context {
        pub resources: Resources,
        // ..
    }

    pub struct Resources<'a> {
        pub x: resources::x<'a>,
        pub y: resources::y<'a>,
    }
}

mod app {
    use cortex_m::register::basepri;

    #[no_mangle]
    unsafe fn UART1() {
        // the static priority of this interrupt (as specified by the user)
        const PRIORITY: u8 = 2;

        // take a snashot of the BASEPRI
        let initial = basepri::read();

        let priority = Cell::new(PRIORITY);
        bar(bar::Context {
            resources: bar::Resources::new(&priority),
            // ..
        });

        // roll back the BASEPRI to the snapshot value we took before
        basepri::write(initial); // same as the `asm!` block we saw before
    }

    // similarly for `UART0` / `foo` and `UART2` / `baz`

    impl<'a> rtic::Mutex for resources::x<'a> {
        type T = u64;

        fn lock<R>(&mut self, f: impl FnOnce(&mut u64) -> R) -> R {
            unsafe {
                // the priority ceiling of this resource
                const CEILING: u8 = 2;

                let current = self.priority().get();
                if current < CEILING {
                    // raise dynamic priority
                    self.priority().set(CEILING);
                    basepri::write(logical2hw(CEILING));

                    let r = f(&mut y);

                    // restore dynamic priority
                    basepri::write(logical2hw(current));
                    self.priority().set(current);

                    r
                } else {
                    // dynamic priority is high enough
                    f(&mut y)
                }
            }
        }
    }

    // repeat for resource `y`
}

At the end the compiler will optimize the function foo into something like this:

fn foo(c: foo::Context) {
    // NOTE: BASEPRI contains the value `0` (its reset value) at this point

    // raise dynamic priority to `3`
    unsafe { basepri::write(160) }

    // the two operations on `y` are merged into one
    y += 2;

    // BASEPRI is not modified to access `x` because the dynamic priority is high enough
    x += 1;

    // lower (restore) the dynamic priority to `1`
    unsafe { basepri::write(224) }

    // mid-point

    // raise dynamic priority to `2`
    unsafe { basepri::write(192) }

    x += 1;

    // raise dynamic priority to `3`
    unsafe { basepri::write(160) }

    y += 1;

    // lower (restore) the dynamic priority to `2`
    unsafe { basepri::write(192) }

    // NOTE: it would be sound to merge this operation on `x` with the previous one but
    // compiler fences are coarse grained and prevent such optimization
    x += 1;

    // lower (restore) the dynamic priority to `1`
    unsafe { basepri::write(224) }

    // NOTE: BASEPRI contains the value `224` at this point
    // the UART0 handler will restore the value to `0` before returning
}

The BASEPRI invariant

An invariant that the RTIC framework has to preserve is that the value of the BASEPRI at the start of an interrupt handler must be the same value it has when the interrupt handler returns. BASEPRI may change during the execution of the interrupt handler but running an interrupt handler from start to finish should not result in an observable change of BASEPRI.

This invariant needs to be preserved to avoid raising the dynamic priority of a handler through preemption. This is best observed in the following example:

#[rtic::app(device = ..)]
mod app {
    struct Resources {
        #[init(0)]
        x: u64,
    }

    #[init]
    fn init() {
        // `foo` will run right after `init` returns
        rtic::pend(Interrupt::UART0);
    }

    #[task(binds = UART0, priority = 1)]
    fn foo() {
        // BASEPRI is `0` at this point; the dynamic priority is currently `1`

        // `bar` will preempt `foo` at this point
        rtic::pend(Interrupt::UART1);

        // BASEPRI is `192` at this point (due to a bug); the dynamic priority is now `2`
        // this function returns to `idle`
    }

    #[task(binds = UART1, priority = 2, resources = [x])]
    fn bar() {
        // BASEPRI is `0` (dynamic priority = 2)

        x.lock(|x| {
            // BASEPRI is raised to `160` (dynamic priority = 3)

            // ..
        });

        // BASEPRI is restored to `192` (dynamic priority = 2)
    }

    #[idle]
    fn idle() -> ! {
        // BASEPRI is `192` (due to a bug); dynamic priority = 2

        // this has no effect due to the BASEPRI value
        // the task `foo` will never be executed again
        rtic::pend(Interrupt::UART0);

        loop {
            // ..
        }
    }

    #[task(binds = UART2, priority = 3, resources = [x])]
    fn baz() {
        // ..
    }

}

IMPORTANT: let's say we forget to roll back BASEPRI in UART1 -- this would be a bug in the RTIC code generator.

// code generated by RTIC

mod app {
    // ..

    #[no_mangle]
    unsafe fn UART1() {
        // the static priority of this interrupt (as specified by the user)
        const PRIORITY: u8 = 2;

        // take a snashot of the BASEPRI
        let initial = basepri::read();

        let priority = Cell::new(PRIORITY);
        bar(bar::Context {
            resources: bar::Resources::new(&priority),
            // ..
        });

        // BUG: FORGOT to roll back the BASEPRI to the snapshot value we took before
        basepri::write(initial);
    }
}

The consequence is that idle will run at a dynamic priority of 2 and in fact the system will never again run at a dynamic priority lower than 2. This doesn't compromise the memory safety of the program but affects task scheduling: in this particular case tasks with a priority of 1 will never get a chance to run.