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

Interrupt Service Routines and Safety

Manage volatile memory access and shared state between interrupt handlers and the main execution thread.

In this lesson, you will learn how to safely bridge the gap between hardware interrupts and your main execution thread using volatile access and atomic synchronization.

When an interrupt service routine (ISR) fires, it preempts your main program at any point. Because the ISR operates in a different execution context, you cannot safely share standard Rust data structures between the ISR and your main code. Rust’s ownership rules still apply, but the compiler cannot see the hardware-level "interference."

The Volatile Requirement

When reading from or writing to hardware registers (or memory-mapped I/O shared with an ISR), the compiler’s optimizer assumes it has a complete view of the memory state. If you modify a variable in the main thread and the ISR modifies it, the compiler might "optimize away" re-reads of that memory, assuming the value hasn't changed. To prevent this, we must use core::ptr::read_volatile and core::ptr::write_volatile. These intrinsics force the compiler to perform a real memory access every time, ensuring the ISR and the main loop see the most current hardware state.

Managing Shared State

For data that must be shared—such as a flag indicating an event occurred—simple volatile access is often insufficient because it isn't atomic. If your main loop reads a multi-byte value while the ISR is halfway through writing it, you end up with a torn read. To resolve this, we use atomic types from core::sync::atomic.

use core::sync::atomic::{AtomicU32, Ordering};

static EVENT_COUNT: AtomicU32 = AtomicU32::new(0);

#[interrupt]
fn TIMER_ISR() {
    // Atomic operations are safe for ISR context
    EVENT_COUNT.fetch_add(1, Ordering::Relaxed);
}

fn main() {
    loop {
        let count = EVENT_COUNT.load(Ordering::Relaxed);
        // Process count safely without data races
    }
}

By using atomics, we guarantee that updates are completed in a single cycle, preventing the main thread from observing a partial or inconsistent state during the preemption. This pattern allows us to communicate across the boundary of the main loop and the interrupt handler without violating memory safety or triggering undefined behavior.

Key Takeaway: Use volatile pointers for direct memory-mapped I/O and atomic types for signaling between interrupt contexts to ensure hardware-level synchronization is both visible to the compiler and memory-safe.