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:
- The data lives long enough (potentially "forever"!)
- 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.