In short: Memory-mapped I/O (MMIO) places hardware peripheral registers into the same address space the CPU uses for RAM, so ordinary load and store instructions read and write device registers. It contrasts with port-mapped I/O, which uses a separate address space and dedicated instructions, and it is the dominant scheme on ARM, RISC-V, and most modern SoCs.
Memory mapped io is a hardware/software convention in which the control and data registers of peripheral devices are assigned fixed addresses inside the processor's unified physical address space. From the CPU's point of view there is no special "device" transaction: the same instructions that touch DRAM, an ordinary LDR/load or STR/store, also reach a UART, a timer, a GPIO block, or an interrupt controller. The address bus decode logic routes some ranges to the memory subsystem and other ranges to peripheral bus bridges.
Because the interface is just addresses, software accesses a register through a pointer. In C you typically declare a pointer to a volatile integer at the register's address and dereference it. The volatile qualifier is essential: it tells the compiler the location can change outside the program's control and must not be cached in a CPU register or optimised away, so every read and write produces a real bus cycle.
MMIO matters because it keeps the instruction set small and the hardware uniform. A RISC architecture that already has fast, pipelined load/store units gets device access "for free" without adding I/O-specific opcodes. The memory management unit (MMU), caches, and protection mechanisms can all be reused: an operating system can map a peripheral page into a driver's virtual address space and deny it to everyone else, giving per-device access control with the same page tables that protect normal memory.
Mechanically, a register access flows like this:
These side effects are exactly why caching and reordering must be disabled for device regions. Peripheral address ranges are marked as device or strongly-ordered memory in the MMU attributes so the core does not merge, reorder, speculate, or cache those accesses.
IN and OUT instructions. ARM and RISC-V have no such separate I/O space, so they rely entirely on MMIO.
The two classic schemes differ in where device registers live and how they are reached:
IN/OUT). A dedicated control line tells the bus which space a transaction targets. This keeps the memory map uncluttered but needs extra ISA support and generally cannot use rich addressing modes.Trade-offs: MMIO consumes part of the usable address range, which mattered on 16- and 32-bit systems where address space was scarce; a large graphics aperture can eat hundreds of megabytes. Port-mapped I/O avoids that but limits you to a few opcodes and is harder to protect and virtualise. Modern 64-bit designs have enormous address spaces, so the address-consumption drawback is minor and MMIO wins on simplicity and MMU integration.
A driver usually defines a struct whose fields line up with the peripheral's register layout, then points it at the base address, so dev->DATA and dev->STATUS read as named fields. Each field is volatile, and the struct's layout must match the datasheet exactly, including reserved padding words at the right offsets. Width also matters: many peripherals require a 32-bit access and will fault or misbehave on a byte or half-word access to a word register.
Real systems add a few more concerns that engineers must get right:
volatile stops the compiler from reordering accesses, but the CPU and interconnect can still reorder at runtime. Drivers insert barrier instructions (ARM DMB/DSB) between a configuration write and the action that depends on it, or use barrier-wrapped accessors like Linux's readl()/writel().In an operating system, a driver typically calls a mapping routine (ioremap() on Linux) to obtain a virtual pointer to the physical register block, because user and kernel code run under the MMU and cannot touch physical addresses directly. On bare-metal microcontrollers the physical address is used directly, which is why vendor headers (CMSIS for ARM Cortex-M) ship ready-made register structs.
At the SoC level the address map is defined in the interconnect IP. In the ARM ecosystem an AMBA AXI/AHB fabric decodes transactions and routes peripheral addresses to an APB bridge for slow registers. The map itself is usually described in a device tree (on Linux) or a vendor SVD/CMSIS file (on microcontrollers), so firmware and operating systems learn each peripheral's base address and interrupt without hard-coding magic numbers. PCI and PCIe devices expose MMIO through Base Address Registers (BARs); the OS enumerates each device and programs its BARs to place the device's register window somewhere in the physical map.
Verification and silicon teams model these regions in RTL and in SystemVerilog/UVM register abstraction layers, where a register model mirrors the datasheet's offsets, access types (RW, RO, W1C), and reset values so tests can poke registers symbolically. Getting that model to match the hardware exactly is a core part of SoC design and validation.
Real projects and industry tool flows, with certification.
Explore CourseTron Courses →Because the value at a device address can change independently of the program, and reads or writes can have hardware side effects. volatile forces the compiler to perform every access literally, in program order relative to other volatile accesses, and to never cache the value in a CPU register or eliminate a "redundant" read. Without it, the optimiser may drop polling loops or reuse a stale value.
Memory-mapped I/O puts device registers in the same address space as RAM, reachable by ordinary load/store instructions; port-mapped (isolated) I/O uses a separate address space accessed only by dedicated instructions such as x86 IN and OUT. MMIO is simpler and integrates with the MMU; port-mapped I/O saves address space but needs special ISA support.
volatile constrains only compile-time ordering. The processor and bus can still reorder or buffer device accesses at runtime. When one access must complete before another, for example configuring a DMA descriptor before writing the "start" bit, you insert a memory barrier (ARM DMB/DSB) or use barrier-aware accessors so the hardware observes the accesses in the intended order.
Caching assumes a location holds the last value written until overwritten. Device registers break that assumption: a status register changes on its own, and a write may trigger an action. If cached, reads could return stale data and writes could sit in the cache instead of reaching the device, so MMIO regions are marked device/non-cacheable in the MMU attributes.
Related: a related topic · the VLSI Glossary · all courses.