Course contentsShow
← Rust
Lesson 1828 of 1,91016. Specialized Runtime TargetsFree lesson

Managing Global Singletons Safely

Implement thread-safe, interrupt-safe global access to shared hardware peripherals.

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

  1. Mutex<RefCell<T>>: The Mutex provides a synchronization primitive that requires a CriticalSection (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.
  2. Option<T>: Since global statics must be initialized at compile time, we start with None and move the peripheral into the static after the hardware is powered up.
  3. 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.