Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sharing Data With Globals

NOTE This content is partially taken (with permission) from the blog post Interrupts Is Threads by James Munns, which contains more discussion about this topic.

As I mentioned previously, when an interrupt occurs we aren't passed any arguments and cannot return any result. This makes it hard for our program to interact with peripherals and other main program state. Before worrying about this bare-metal embedded problem, it is likely worth thinking about threads in "std" Rust.

"std" Rust: Sharing Data With A Thread

In "std" Rust, we also have to think about sharing data when we do things like spawn a thread.

When you want to give something to a thread, you might pass it into a closure by ownership.

#![allow(unused)]
fn main() {
// Create a string in our current thread
let data = String::from("hello");

// Now spawn a new thread, and GIVE it ownership of the string
// that we just created
std::thread::spawn(move || {
    std::thread::sleep(std::time::Duration::from_millis(1000));
    println!("{data}");
});
}

If you want to share something, and still have access to it in the original thread, you usually can't pass a reference to it. If you do this:

use std::{thread::{sleep, spawn}, time::Duration};

fn main() {
    // Create a string in our current thread
    let data = String::from("hello");
    
    // make a reference to pass along
    let data_ref = &data;
    
    // Now spawn a new thread, and GIVE it ownership of the string
    // that we just created
    spawn(|| {
        sleep(Duration::from_millis(1000));
        println!("{data_ref}");
    });
    
    println!("{data_ref}");
}

you'll get an error like this:

error[E0597]: `data` does not live long enough
  --> src/main.rs:6:20
   |
3  |       let data = String::from("hello");
   |           ---- binding `data` declared here
...
6  |       let data_ref = &data;
   |                      ^^^^^ borrowed value does not live long enough
...
10 | /     spawn(|| {
11 | |         sleep(Duration::from_millis(1000));
12 | |         println!("{data_ref}");
13 | |     });
   | |______- argument requires that `data` is borrowed for `'static`
...
16 |   }
   |   - `data` dropped here while still borrowed

You need to make sure the data lives long enough for both the current thread and the new thread you are creating. You can do this by putting it in an Arc (Atomically Reference Counted heap allocation) like this:

use std::{sync::Arc, thread::{sleep, spawn}, time::Duration};

fn main() {
    // Create a string in our current thread
    let data = Arc::new(String::from("hello"));
    
    let handle = spawn({
        // Make a copy of the handle to GIVE to the new thread.
        // Both `data` and `new_thread_data` are pointing at the
        // same string!
        let new_thread_data = data.clone();
        move || {
            sleep(Duration::from_millis(1000));
            println!("{new_thread_data}");
        }
    });
    
    println!("{data}");
    // wait for the thread to stop
    let _ = handle.join();
}

This is great! You can now access the data in both the main thread as long as you'd like. But what if you want to mutate the data in both places?

For this, you will usually need some kind of "inner mutability" — a type that doesn't require an &mut to modify. On the desktop, you'd typically reach for a type like Mutex, lock()-ing it to gain mutable access to the data.

That might look something like this:

use std::{sync::{Arc, Mutex}, thread::{sleep, spawn}, time::Duration};

fn main() {
    // Create a string in our current thread
    let data = Arc::new(Mutex::new(String::from("hello")));
    
    // lock it from the original thread
    {
        let guard = data.lock().unwrap();
        println!("{guard}");
        // the guard is dropped here at the end of the scope!
    }
    
    let handle = spawn({
        // Make a copy of the handle, that you GIVE to the new thread.
        // Both `data` and `new_thread_data` are pointing at the
        // same `Mutex<String>`!
        let new_thread_data = data.clone();
        move || {
            sleep(Duration::from_millis(1000));
            {
                let mut guard = new_thread_data.lock().unwrap();
                // we can modify the data!
                guard.push_str(" | thread was here! |");                
                // the guard is dropped here at the end of the scope!
            }
        }
    });
    
    // wait for the thread to stop
    let _ = handle.join();
    {
        let guard = data.lock().unwrap();
        println!("{guard}");
        // the guard is dropped here at the end of the scope!
    }
}

If you run this code, you will see:

hello
hello | thread was here! |

Why does "std" Rust make us do this? Rust is helping us out by making us think about two things:

  1. The data lives long enough (potentially "forever"!)
  2. Only one piece of code can mutably access the data at a time

If Rust allowed us to access data that might not live long enough, like data borrowed from one thread into another, things might go wrong. We might get corrupted data if the original thread ends or panics and then the second thread tries to access the data that is now invalid. If Rust allowed two pieces of code to try to mutate the same data at the same, we could have a data race, or the data could end up corrupted.

Embedded Rust: Sharing Data With An ISR

In embedded Rust we care about the same things when it comes to sharing data with interrupt handlers! Similar to threads, interrupts can occur at any time, sort of like a thread waking up and accessing some shared data. This means that the data we share with an interrupt must live long enough, and we must be careful to ensure that our main code isn't in the middle of working with some data shared with an ISR when that ISR gets run and also tries to work with the data!

In fact, in embedded Rust, we model interrupts in a similar way that we model threads in Rust: the same rules apply, for the same reasons. However, in embedded Rust, we have some crucial differences:

  • Interrupts don't work exactly like threads: we set them up ahead of time, and they wait until some event happens (like a button being pressed, or a timer expiring). At that point they run, but without access to any passed-in context.

  • Interrupts can be triggered multiple times, once for each time that the event occurs.

Since we can't pass context to interrupts as function arguments, we need to find another place to store that data. In "bare metal" embedded Rust we don't have access to heap allocations: thus Arc and similar are not possibilities for us.

Without the ability to pass things by value, and without a heap to store data, that leaves us with one place to put our shared data that our ISR can access: static globals.

Embedded Rust ISR Data Sharing: The "Standard Method"

Global variables are very much second-class citizens in Rust, with many limitations compared to local variables. You can declare a global state variable like this:

#![allow(unused)]
fn main() {
static COUNTER: usize = 0;
}

Of course, this isn't super-useful: you want to be able to mutate the COUNTER. You can say

#![allow(unused)]
fn main() {
static mut COUNTER: usize = 0;
}

but now all accesses will be unsafe.

#![allow(unused)]
fn main() {
unsafe { COUNTER += 1 };
}

The unsafety here is for a reason: imagine that in the middle of updating COUNTER an interrupt handler runs and also tries to update COUNTER. The usual chaos will ensue. Clearly some kind of locking is in order.

The critical-section crate provides a sort of Mutex type, but with an unusual API and unusual operations. Examining the Cargo.toml for this chapter, you will see the feature critical-section-single-core on the cortex-m crate enabled. This feature asserts that there is only one processor core in this system, and that thus synchronization can be performed by simply disabling interrupts across the critical section. If not in an interrupt, this will ensure that only the main program has access to the global. If in an interrupt, this will ensure that the main program cannot be accessing the global (program control is in the interrupt handler) and that no other higher-priority interrupt handler can fire.

critical_section::Mutex is a bit weird in that it gives mutual exclusion but does not itself give mutability. To make the data mutable, you will need to protect an interior-mutable type — usually RefCell — with the mutex. This Mutex is also a bit weird in that you don't .lock() it. Instead, you initiate a critical section with a closure that receives a "critical section token" certifying that other program execution is prevented. This token can be passed to the Mutex's borrow() method to allow access.

Putting it all together gives you the ability to share state between ISRs and the main program (examples/count-once.rs).

#![no_main]
#![no_std]

use core::cell::RefCell;

use cortex_m::asm;
use cortex_m_rt::entry;
use critical_section::Mutex;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};

use microbit::{
    Board,
    hal::{
        gpiote,
        pac::{self, interrupt},
    },
};

static COUNTER: Mutex<RefCell<usize>> = Mutex::new(RefCell::new(0));

/// This "function" will be called when an interrupt is received. For now, just
/// report and panic.
#[interrupt]
fn GPIOTE() {
    critical_section::with(|cs| {
        let mut count = COUNTER.borrow(cs).borrow_mut();
        *count += 1;
        rprintln!("count: {}", count);
    });
    panic!();
}

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let board = Board::take().unwrap();
    let button_a = board.buttons.button_a.into_floating_input();

    // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO
    // wire goes low).
    let gpiote = gpiote::Gpiote::new(board.GPIOTE);
    let channel = gpiote.channel0();
    channel
        .input_pin(&button_a.degrade())
        .hi_to_lo()
        .enable_interrupt();
    channel.reset_events();

    // Set up the NVIC to handle GPIO interrupts.
    unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
    pac::NVIC::unpend(pac::Interrupt::GPIOTE);

    loop {
        // "wait for interrupt": CPU goes to sleep until an interrupt.
        asm::wfi();
    }
}

You still cannot safely return from your ISR, but now you are in a position to do something about that: share the GPIOTE with the ISR so that the ISR can clear the interrupt.

Sharing Peripherals (etc) With Globals

There's one more problem yet to solve: Rust globals must be initialized statically — before the program starts. For the counter that was easy — just initialize it to 0. If you want to share the GPIOTE peripheral, though, that won't work. The peripheral must be retrieved from the Board struct and set up once the program has started: there is no const initializer for this (nor can there reasonably be).

Let's rewrite the button counter a bit. First, move the actual count to be an AtomicUsize. This is a more natural type for this global anyhow. Next, add a global GPIOTE_PERIPHERAL variable using the LockMut type from the critical-section-lock-mut crate. This crate is a convenient wrapper for the pattern of the last section.

Now that the main program can set up the GPIOTE peripheral and then make it available to the interrupt handler, you can quit panicking and let the counter bump up on every button press. Move the count display into the main loop, to show that the count is shared between the interrupt handler and the rest of the program.

Give this example (examples/count.rs) a run and note that the count is bumped up 1 on every push of the MB2 A button.

#![no_main]
#![no_std]

use core::sync::atomic::{AtomicUsize, Ordering::AcqRel};

use cortex_m::asm;
use cortex_m_rt::entry;
use critical_section_lock_mut::LockMut;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};

use microbit::{
    Board,
    hal::{
        gpiote,
        pac::{self, interrupt},
    },
};

static COUNTER: AtomicUsize = AtomicUsize::new(0);
static GPIOTE_PERIPHERAL: LockMut<gpiote::Gpiote> = LockMut::new();

#[interrupt]
fn GPIOTE() {
    let count = COUNTER.fetch_add(1, AcqRel);
    rprintln!("ouch {}", count + 1);
    GPIOTE_PERIPHERAL.with_lock(|gpiote| {
        gpiote.channel0().reset_events();
    });
}

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let board = Board::take().unwrap();
    let button_a = board.buttons.button_a.into_floating_input();

    // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO
    // wire goes low).
    let gpiote = gpiote::Gpiote::new(board.GPIOTE);
    let channel = gpiote.channel0();
    channel
        .input_pin(&button_a.degrade())
        .hi_to_lo()
        .enable_interrupt();
    channel.reset_events();
    GPIOTE_PERIPHERAL.init(gpiote);

    // Set up the NVIC to handle GPIO interrupts.
    unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
    pac::NVIC::unpend(pac::Interrupt::GPIOTE);

    loop {
        // "wait for interrupt": CPU goes to sleep until an interrupt.
        asm::wfi();
    }
}

NOTE It is always a good idea to compile examples involving interrupt handling with --release. Long interrupt handlers can lead to a lot of confusion.

Really, though, that rprintln!() in the interrupt handler is bad practice: while the interrupt handler is running the printing code, nothing else can move forward. Let's move the reporting to the main loop, just after the wfi() "wait for interrupt". The count will then be reported every time an interrupt handler finishes (examples/count-bounce.rs).

#![no_main]
#![no_std]

use core::sync::atomic::{AtomicUsize, Ordering::{Acquire, AcqRel}};

use cortex_m::asm;
use cortex_m_rt::entry;
use critical_section_lock_mut::LockMut;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};

use microbit::{
    Board,
    hal::{
        gpiote,
        pac::{self, interrupt},
    },
};

static COUNTER: AtomicUsize = AtomicUsize::new(0);
static GPIOTE_PERIPHERAL: LockMut<gpiote::Gpiote> = LockMut::new();

#[interrupt]
fn GPIOTE() {
    let _ = COUNTER.fetch_add(1, AcqRel);
    GPIOTE_PERIPHERAL.with_lock(|gpiote| {
        gpiote.channel0().reset_events();
    });
}

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let board = Board::take().unwrap();
    let button_a = board.buttons.button_a.into_floating_input();

    // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO
    // wire goes low).
    let gpiote = gpiote::Gpiote::new(board.GPIOTE);
    let channel = gpiote.channel0();
    channel
        .input_pin(&button_a.degrade())
        .hi_to_lo()
        .enable_interrupt();
    channel.reset_events();
    GPIOTE_PERIPHERAL.init(gpiote);

    // Set up the NVIC to handle GPIO interrupts.
    unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
    pac::NVIC::unpend(pac::Interrupt::GPIOTE);

    loop {
        // "wait for interrupt": CPU goes to sleep until an interrupt.
        asm::wfi();
        let count = COUNTER.load(Acquire);
        rprintln!("ouch {}", count);
    }
}

In this example the count is bumped up 1 on every push of the MB2 A button. Maybe. Especially if your MB2 is old (!), you may see a single press bump the counter by several. This is not a software bug. Mostly. In the next section, I'll talk about what might be going on and how we should deal with it.