MMIO (Memory-Mapped I/O)
MMIO (Memory-Mapped I/O) is a way for the central processing unit (CPU) to interact with peripheral hardware devices (e.g. UART
, timers
, GPIO
, SPI
, etc.) by "sticking" the registers of these devices to specific addresses in the CPU memory.
These addresses in the CPU do not store regular data, but allow reading and writing to the device control registers.
This means that the peripheral device can be accessed in the same way as regular memory - via regular memory read/write instructions (LDR
, STR
, MOV
, etc.). at specific addresses.
How does it work inside CPU?
- The CPU has a single address bus, which is used to access both RAM and MMIO devices.
- When the CPU accesses an address that belongs to the MMIO area:
- In this case, the control logic understands that this is not ordinary memory, but a device register.
- Signals go to the periphery, not to RAM/ROM.
π§· Key features
- It is necessary to work through
volatile
, so that the compiler does not optimize access to the device. - Addresses and devices are specified by the platform manufacturer (e.g.
ARM Cortex-M
,x86
)
Characteristic | volatile |
atomic |
mutex |
---|---|---|---|
Scenario | Read/write MMIO registers Check flag or register status | Interthread counter, ready flag Safe logical condition (compare, increment) | Work with buffer, list, structure |
Ensures visibility of changes | β Yes (to the compiler) | β Yes (across threads/cores) | β Yes |
Ensures atomicity | β No | β
Yes (e.g., fetch_add ) |
β Yes (via locking) |
Protects from data races | β No | β Yes (on primitive types) | β Yes (on any resource) |
Blocks other threads | β No | β No | β Yes |
Supports complex operations | β No | β οΈ Limited (e.g., arithmetic, flags) | β Yes (conditions, buffers, etc.) |
Use in Embedded | β MMIO (memory-mapped I/O) | β Counters, flags, semaphores | β RTOS, critical sections |
Rust usage | ptr::read_volatile , PAC |
core::sync::atomic::* |
critical-section , Mutex<T> |
Performance | π Fast | β‘ Fast | π’ Slower (due to blocking) |
πΈ volatile
Compilers like to optimize code: if a variable doesn't change in the current context, they can not re-read it.
- The device can change the register itself
- The variable can change in another context
volatile
tells the compiler: "don't optimize access to this variable - read/write it each time, because it can change at any time outside the current code."
- It is only used with MMIO.
- Rust doesn't have a built-in
volatile
keyword, but provides functionality viacore::ptr
andvolatile-safe
types:
use core::ptr::{read_volatile, write_volatile};
// unsafe is required for any direct volatile access
let ptr = 0x4000_0000 as *mut u32;
// Write the value without buffering
unsafe {
write_volatile(ptr, 0x1234);
}
// Read the value with optimizations disabled
let value = unsafe {
read_volatile(ptr)
};
πΈ atomic
- Provides atomic access to data (e.g. counters, flags).
- Suitable for thread synchronization on multiprocessor systems.
- Can be used without locks (lock-free).
- AtomicBool, AtomicU8, AtomicUsize, etc.
use core::sync::atomic::{AtomicBool, Ordering};
static READY: AtomicBool = AtomicBool::new(false);
fn main() {
READY.store(true, Ordering::Release);
if READY.load(Ordering::Acquire) {
// safe to read
}
}
πΈ mutex
- Provides mutual exclusion.
- Suitable for large objects consisting of many variables or structures.
- More commonly used in multitasking systems (
RTOS
,OS
). - In embedded:
- critical-section (interrupt lock),
- Mutex
from embassy, ββRTIC, or spin::Mutex without an OS.
use critical_section::Mutex;
use core::cell::RefCell;
static SHARED: Mutex<RefCell<Option<u32>>> = Mutex::new(RefCell::new(None));
fn example() {
critical_section::with(|cs| {
*SHARED.borrow(cs).borrow_mut() = Some(42);
});
}