In embedded Rust, sharing data between the main thread and interrupt handlers requires careful consideration to ensure memory safety and avoid data races, particularly in single-core systems like Cortex-M. This document summarizes three common patterns—static Atomic<T>
, static Option<T>
, and Mutex<RefCell<Option<T>>>
—comparing their use cases, safety, and complexity, and evaluates whether the "move to context" trick applies.
Pattern | Data Types | Requires unsafe |
Synchronization | Overhead | Use Case | Move to Context Trick |
---|---|---|---|---|---|---|
static Atomic<T> |
Primitives (bool , u32 ) |
No | Atomic operations (lock-free) | Minimal (no critical sections) | Flags, counters, simple state | No |
static Option<T> |
Any (e.g., peripherals) | Yes | None (manual or unsafe ) |
Low but risky | Resource hand-off, complex types | Yes |
Mutex<RefCell<Option<T>>> |
Any (e.g., peripherals) | No | Critical sections (Mutex ) |
Higher (critical sections) | Shared access, complex types | No |
- Description: Uses atomic types (
AtomicBool
,AtomicU32
) for lock-free, safe access to primitive data. Best for simple state like flags or counters, with nounsafe
or critical sections. - Example:
use core::sync::atomic::{AtomicBool, Ordering}; static FLAG: AtomicBool = AtomicBool::new(false); fn main() { loop { FLAG.store(true, Ordering::Relaxed); // Set flag atomically } } #[interrupt] fn TIMER0() { if FLAG.load(Ordering::Relaxed) { FLAG.store(false, Ordering::Relaxed); // Reset flag // Handle interrupt } }
- Description: Uses a
static mut Option<T>
to store a resource (e.g., a peripheral), typically for one-time hand-off to an interrupt handler. Requiresunsafe
for access/mutation, suitable when interrupts are enabled after initialization. - Example:
use core::option::Option; static mut UART: Option<Uart> = None; fn main() { unsafe { UART = Some(Uart::new()); // Initialize UART } enable_uart_interrupt(); loop {} } #[interrupt] fn UART0() { unsafe { if let Some(uart) = UART.as_mut() { uart.handle_interrupt(); // Use UART } } }
- Move to Context Trick: Yes, this pattern supports the "move to context" trick, where the interrupt handler uses
Option::take()
to move the resource into its ownstatic
for exclusive use. This reduces the scope to unsafe {} to initial setup and one-time access in interrupt handler, e.g.:#[interrupt] fn UART0() { static mut UART_LOCAL: Option<Uart> = None; // Move to local storage if not already moved cortex_m::interrupt::free(|cs| { unsafe { UART_LOCAL.replace(UART.take().unwrap()); } }); // Use as a regular Option<T> if let Some(uart) = UART_LOCAL.as_mut() { uart.handle_interrupt(); } }
- Description: Uses a
Mutex
for critical sections andRefCell
for runtime borrow checking, allowing safe shared access to complex types. Ideal for resources needing concurrent access or uncertain initialization timing. - Example:
use cortex_m::interrupt::{self, Mutex}; use core::cell::RefCell; use core::option::Option; static UART: Mutex<RefCell<Option<Uart>>> = Mutex::new(RefCell::new(None)); fn main() { interrupt::free(|cs| { UART.borrow(cs).replace(Some(Uart::new())); // Initialize UART }); enable_uart_interrupt(); loop {} } #[interrupt] fn UART0() { interrupt::free(|cs| { if let Some(uart) = UART.borrow(cs).borrow_mut().as_mut() { uart.handle_interrupt(); // Use UART } }); }