In this lesson, you will learn how to bridge the gap between volatile hardware signals and Rust’s strict safety guarantees by creating robust, idiomatic interfaces for interrupt service routines (ISRs).
The Interrupt Challenge
Hardware interrupts are inherently asynchronous; the CPU pauses execution abruptly to jump to a vector address when a peripheral demands attention. Because this happens outside the control of the Rust runtime, you cannot rely on standard synchronization primitives or heap allocation within an ISR. If you accidentally attempt to acquire a Mutex that is already held by the interrupted thread, you will trigger a deadlocked system.
Designing Safe Wrappers
To handle interrupts safely, we must encapsulate the unsafe hardware interface behind a pattern that ensures state remains consistent. A common strategy involves using a static interrupt handler that delegates work to a pre-defined, thread-safe structure.
Consider a simple interrupt-driven timer. Since the interrupt can fire at any moment, the shared state must be protected by a mechanism that is safe to use in an interrupt context, such as a lock-free Atomic type or a critical section that masks interrupts.
use core::sync::atomic::{AtomicU32, Ordering};
// Shared state, accessed via atomic to avoid locks
static COUNTER: AtomicU32 = AtomicU32::new(0);
#[no_mangle]
pub extern "C" fn TimerInterruptHandler() {
// We increment without a lock to remain safe during re-entrancy
COUNTER.fetch_add(1, Ordering::Relaxed);
// Signal end-of-interrupt to the hardware (Platform-specific)
unsafe { EoiRegister::write(0); }
}
Enforcing Invariants
The core of this pattern is Encapsulation. By keeping the static variables private to your driver module and exposing only high-level, atomic operations, you prevent the rest of your application from inadvertently violating memory safety. When dealing with complex hardware state, use an InterruptGuard—a RAII struct that disables interrupts when constructed and re-enables them when dropped—to create "critical sections." This ensures that when you modify complex data structures (like a queue of packets), the interrupt cannot preempt you mid-mutation, preventing data races that the compiler cannot see.
Key Takeaway: Always minimize logic inside an ISR and use lock-free synchronization or interrupt-masking RAII guards to bridge the gap between asynchronous hardware events and your synchronous application state.