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

Memory-Mapped I/O Atomicity

Ensure atomic writes to hardware registers in scenarios where the peripheral requires multi-step updates.

In this lesson, you will learn how to perform multi-step register updates in Memory-Mapped I/O (MMIO) without triggering hardware side effects before the sequence is complete.

When working with specialized hardware targets, peripheral registers often require a specific sequence of writes to change state—for example, writing an address to a DATA register and then a command to an ADDR_CTRL register. If the hardware observes these as separate, non-atomic events, it might attempt to process the command with stale or incomplete data, leading to a peripheral hard fault or unpredictable behavior.

In standard Rust, a series of write_volatile operations does not guarantee that the hardware sees them as a single transaction. To ensure atomicity in these scenarios, we rely on a combination of core::ptr::write_volatile and, crucially, specific ordering constraints enforced by the compiler and CPU architecture.

Consider a case where a peripheral requires a 64-bit value to be written via two 32-bit register slots:

pub fn update_peripheral_reg(base: *mut u32, value: u64) {
    let low = (value & 0xFFFF_FFFF) as u32;
    let high = (value >> 32) as u32;

    // Use a critical section or disabling interrupts 
    // if other threads/IRQs might access this memory.
    unsafe {
        core::ptr::write_volatile(base.add(0), low);
        core::ptr::write_volatile(base.add(1), high);
    }
}

The danger here is that the compiler or the CPU's load/store unit might reorder these writes. To prevent this, we must introduce memory barriers. In Rust, while write_volatile prevents the compiler from optimizing away the writes, it does not always prevent the hardware from reordering them on weakly-ordered architectures (like ARM). We use core::sync::atomic::fence(Ordering::SeqCst) before or after our volatile operations to ensure the visibility of these steps occurs in the strict order expected by the peripheral.

Furthermore, if your peripheral is shared across threads, you must wrap these multi-step operations in a synchronization primitive—like a Mutex or by disabling interrupts—to ensure no other execution context interleaves its own register writes between your steps. If the hardware doesn't support a "commit" bit, the only way to achieve true atomicity is to gate the hardware access behind a software lock.

Key Takeaway: To ensure atomic multi-step MMIO, you must combine write_volatile with explicit memory barriers to prevent hardware reordering and encapsulate the sequence within a critical section to prevent interleaved access.