Memory-mapped I/O (MMIO) is a technique in which peripheral registers and other hardware resources are placed at specific addresses within the processor's normal address space, allowing them to be accessed using the same load/store instructions used for RAM. No special I/O instructions are required; reading or writing a peripheral register looks syntactically identical to reading or writing a variable.
In practice
On virtually all ARM Cortex-M MCUs (STM32, nRF52, LPC, SAM, RP2040, and others), the vast majority of the peripheral set is accessed through MMIO. The vendor-supplied header files (often generated via CMSIS-SVD) define each peripheral's base address and the offsets of its registers as C structs or preprocessor macros. Code like `GPIOA->ODR = 0x01;` is simply a write to a known physical address. On 8-bit architectures such as AVR and PIC, a mix of MMIO and dedicated I/O instructions (IN/OUT on AVR) is common, and the address space reserved for peripherals is typically much smaller.
In C and C++, MMIO registers are accessed through pointers to `volatile`-qualified types. The `volatile` qualifier tells the compiler that the value at that address can change or have side effects outside the program's visible state, preventing the optimizer from caching values in registers or reordering accesses. Omitting `volatile` is one of the most common bugs when working with MMIO; the compiler may silently eliminate or reorder peripheral accesses that look redundant from a purely computational perspective. The blog post "Memory Mapped I/O in C" covers the correct use of `volatile` with MMIO in detail.
A key pitfall is that even with `volatile`, the C standard does not guarantee the access width (byte, halfword, word) the compiler will generate, since access width is also influenced by the target ABI, alignment, and backend code generation. Many peripherals require 32-bit accesses; writing to them with a `uint8_t` pointer may produce a byte-wide bus transaction that the peripheral ignores or misinterprets. Casting to the correct pointer type (typically `volatile uint32_t *`) and consulting the reference manual for access-size requirements is essential.
Concurrency is another concern: MMIO register accesses are not atomic at the software level. A read-modify-write sequence on a peripheral register (e.g., toggling a single GPIO pin by reading ODR, masking, and writing back) can be interrupted between the read and the write, leading to race conditions. Many MCUs provide dedicated set/clear/toggle registers (such as the STM32 GPIOx_BSRR) specifically to make single-bit operations atomic without disabling interrupts. The blog post "Scorchers, Part 3: Bare-Metal Concurrency With Double-Buffering and the Revolving Fireplace" discusses bare-metal concurrency issues relevant to this kind of hardware interaction.
Discussed on EmbeddedRelated
Frequently asked
Why must MMIO pointers be declared volatile in C?
The compiler has no visibility into hardware behavior. Without
volatile, it may assume that a memory location that has not been written by the program cannot have changed, and it may
cache a previously read value in a CPU register or eliminate a write it considers redundant. volatile marks each access as an observable side effect, requiring the compiler to issue an actual load or store for each access rather than optimizing them away or reordering them, which is required for correct peripheral interaction.
What is the difference between memory-mapped I/O and port-mapped I/O (PMIO)?
Port-mapped I/O (also called isolated I/O) uses a separate address space and dedicated instructions (such as x86 IN and OUT) to access peripherals. MMIO places peripherals in the same address space as memory and uses ordinary load/store instructions. Most modern 32-bit
MCU architectures (ARM,
RISC-V, MIPS) typically use MMIO as the primary or exclusive mechanism for peripheral access. x86 supports both. Some 8-bit architectures like AVR use a hybrid approach where low peripheral addresses are reachable via special I/O instructions and also via normal memory addresses.
What happens if I access an MMIO register with the wrong access width?
Behavior is peripheral-specific and defined by the hardware, not by software convention. Many 32-bit peripheral
registers only respond correctly to 32-bit (word) accesses; a byte or halfword access may read back 0, be ignored, or trigger a bus fault depending on the
MCU's bus fabric. Always check the reference manual for the required access size for each register.
How do vendor header files map MMIO registers to C structs?
Vendor headers typically define a C struct whose members correspond to a peripheral's
registers in order, then place a pointer (or macro expanding to a cast pointer) at the peripheral's base address. For example, on STM32 parts, a
GPIO peripheral is commonly defined as a cast of its base address constant to a pointer to a struct of
volatile uint32_t fields, though exact macro names and typedefs vary across families and CMSIS-generated headers. This pattern, standardized for Cortex-M parts by CMSIS, lets the compiler calculate register offsets from struct member layout rather than requiring every offset to be specified manually.
Can the CPU cache MMIO regions, and does that cause problems?
On processors with data caches (such as Cortex-M7, or application-class cores like Cortex-A series), caching MMIO regions causes serious problems: a cached read may return a stale hardware value, and a cached write may never reach the peripheral. These architectures provide memory-type attributes (via the MPU or
MMU) to mark regions as Device or Strongly-Ordered memory, which disables caching and enforces access ordering for those addresses. Simpler Cortex-M0/M0+/M3/M4 cores have no data
cache, so this is not a concern on those parts.
Differentiators vs similar concepts
MMIO is often contrasted with port-mapped I/O (PMIO or PIO), which uses a separate I/O address space and dedicated instructions (such as IN/OUT on x86 or AVR). MMIO should also be distinguished from
DMA: DMA is a mechanism by which a controller moves data between memory and peripherals autonomously; MMIO is simply the addressing scheme by which the CPU (or DMA controller) locates peripheral
registers. The two concepts are complementary, not alternatives.