In this lesson, you will learn how to safely expose shared hardware peripherals as global singletons in a no_std embedded environment, ensuring both thread and interrupt safety.
The Challenge of Global Peripherals
In embedded systems, you often have a single hardware peripheral (like a UART controller or a Timer) that must be accessible from both your main application loop and your Interrupt Service Routines (ISRs). Because Rust’s ownership model typically requires moving ownership to a single caller, sharing a peripheral globally can feel impossible.
To solve this, we combine static storage with interior mutability and atomic access patterns.
The Pattern: Mutex-Protected Statics
A common approach is to wrap the peripheral in a Mutex that is specifically designed for single-core or multi-core interrupt scenarios. In no_std, we often use a RefCell combined with a Critical Section to manage access. This ensures that when the main code accesses the peripheral, it can temporarily "lock out" interrupts to prevent re-entrancy issues.
use core::cell::RefCell;
use cortex_m::interrupt::{self, Mutex};
use stm32f4::stm32f401::USART1;
// Global peripheral, hidden behind a Mutex and RefCell for interior mutability
static G_USART: Mutex<RefCell<Option<USART1>>> = Mutex::new(RefCell::new(None));
pub fn initialize(usart: USART1) {
interrupt::free(|cs| {
G_USART.borrow(cs).replace(Some(usart));
});
}
// Safe, re-entrant access
pub fn write_byte(byte: u8) {
interrupt::free(|cs| {
if let Some(ref mut usart) = *G_USART.borrow(cs).borrow_mut() {
// Perform hardware write
}
});
}
Why This Works
Mutex<RefCell<T>>: TheMutexprovides a synchronization primitive that requires aCriticalSection(cs) token to access. This ensures that code running in the main context cannot be interrupted by an ISR that tries to access the same peripheral.Option<T>: Since global statics must be initialized at compile time, we start withNoneand move the peripheral into the static after the hardware is powered up.- Interior Mutability: By wrapping the peripheral in a
RefCell, we can mutate the hardware state even though the static itself is immutable.
This approach prevents data races at compile time: the interrupt::free function ensures that hardware registers aren't accessed simultaneously by different contexts, maintaining the integrity of your peripheral state.
Key Takeaway: Use a Mutex coupled with RefCell to wrap global hardware peripherals, ensuring that access requires a critical section that prevents race conditions between main-line code and interrupt handlers.