Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
181: Interrupt Handling in Embedded Rust
Coming from a C background, the first instinct when dealing with interrupts is to throw everything into a global variable. You create a static mut variable for your peripheral, wrap your access in an unsafe block, and call it a day. It feels efficient, and for a while, it actually works. But in Rust, this is a recipe for disaster.
Take a look at this snippet. This is exactly how I see people try to implement a simple button-triggered LED toggle on a Cortex-M microcontroller:
// ⚠️ BUGGY CODE: The "C-Style" approach in Rust
static mut LED_PIN: Option<GpioPin> = None;
#[interrupt]
fn EXTI0() {
// I'm just trying to toggle the LED when the button is pressed!
unsafe {
if let Some(ref mut pin) = LED_PIN {
pin.toggle();
}
}
}
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take().unwrap();
let pin = peripherals.GPIOA.pa5.into_push_pull_output();
unsafe {
LED_PIN = Some(pin);
}
loop {
// Main loop does nothing, just waits for interrupt
asm::wfi();
}
}
The Danger of the Static Mut Trap
At first glance, this looks fine. But we have a massive problem: static mut is fundamentally unsafe because it provides no guarantee against data races. Even on a single-core microcontroller, an interrupt is essentially a second thread of execution that can preempt your main code at any millisecond.
If your main loop were doing something more complex with that LED_PIN—perhaps changing its configuration—and the EXTI0 interrupt fired right in the middle of that operation, you'd have two different parts of your program trying to mutate the same memory location simultaneously. This is Undefined Behavior (UB). The compiler might optimize the code in ways that assume LED_PIN never changes unexpectedly, leading to heisenbugs that only appear in --release mode.
Safe State Sharing with Mutex and RefCell
To do this the "Rust way," we need to ensure that the data is accessed exclusively. In embedded Rust, we use a combination of cortex_m::interrupt::Mutex and core::cell::RefCell. I know, it looks like a lot of nesting, but there's a very specific reason for each layer.
- Mutex: Ensures that the data can only be accessed inside a "critical section" (where interrupts are disabled), preventing the preemption race condition I mentioned.
- RefCell: Since the Mutex gives us a shared reference, we need
RefCellto provide "interior mutability," allowing us to mutate the pin even when it's stored in a static. - Option: Since we can't initialize the hardware peripheral at compile-time, we start with
Noneand move the peripheral into it duringmain.
Here is the corrected, thread-safe version of that logic:
use core::cell::RefCell;
use cortex_m::interrupt::{free, Mutex};
// We wrap the pin in a Mutex and a RefCell
static G_LED: Mutex<RefCell<Option<GpioPin>>> = Mutex::new(RefCell::new(None));
#[interrupt]
fn EXTI0() {
// 'free' creates a critical section, providing the 'cs' token
free(|cs| {
if let Some(ref mut pin) = G_LED.borrow(cs).borrow_mut().as_mut() {
pin.toggle();
}
});
}
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take().unwrap();
let pin = peripherals.GPIOA.pa5.into_push_pull_output();
// Safely move the pin into the global mutex
free(|cs| {
G_LED.borrow(cs).replace(Some(pin));
});
loop {
asm::wfi();
}
}
Why This Fix Actually Works
The magic here is the cs (Critical Section) token. You'll notice that G_LED.borrow(cs) requires that token to work. The only way to get a cs token is to call interrupt::free(|cs| { ... }).
Inside interrupt::free, the processor physically disables interrupts. This means the EXTI0 handler cannot fire while the main loop is accessing the G_LED. Because the interrupt is blocked, the "race" is impossible. Once the closure finishes, interrupts are re-enabled. We've traded a tiny bit of latency for a mathematical guarantee that our memory is safe.
I'll be honest: the syntax borrow(cs).borrow_mut() feels clunky at first. But it's a conscious design choice. It forces you to acknowledge exactly when you are entering a critical section and when you are mutating shared state. In the embedded world, that visibility is what keeps your firmware from crashing at 3:00 AM in a production environment.
📋 Practical Task
Exercise: Implementing a Heartbeat Toggle via Timer Interrupt
Your goal is to create a "heartbeat" LED that toggles every time a timer interrupt fires. Instead of toggling the LED in the main loop, you must move the LED peripheral into a global Mutex<RefCell<Option<T>>>>.
Requirements:
- Initialize a system timer (e.g.,
TIM2) to trigger an interrupt at a regular interval (e.g., 500ms). - Declare a global static
G_LEDusing theMutexandRefCellpattern. - In the timer's interrupt handler, use
interrupt::freeto access the LED and toggle its state. - In
main, initialize the LED and the timer, move the LED into the global storage, and then enter a low-power sleep loop usingwfi.
Verification: Your code should compile without any unsafe blocks in the interrupt handler or the main loop's peripheral assignment.
There are no comments for now.