keepcalculator

06 Numbers and Powers

Memory-Mapped I/O: How CPUs Access Device Registers

Card No. 06·68239 · Updated · Standard formula, shown below

Memory-mapped I/O (MMIO) is a method CPUs use to communicate with hardware devices by assigning each device register a normal memory address, so the CPU reads and writes those registers with the same load and store instructions it uses for RAM. When you access such an address, the memory controller routes the request to the device instead of DRAM, letting software control hardware with ordinary pointer dereferences. This article explains how MMIO works, why it exists, how it compares to port I/O, and the practical issues that arise when using it.

Background: Why Devices Need Registers

Every hardware component—a network card, a GPU, a disk controller—has a set of control and status registers. These are small memory locations inside the device that hold configuration values, command codes, or status flags. For example, a serial port might have a register that holds the baud rate divisor and another that indicates whether a byte has been received. Software must be able to read and write these registers to make the device do anything useful.

The CPU needs a standardized way to reach those registers. Two main approaches have been used historically: dedicated I/O port instructions (in/out on x86) and memory-mapped I/O. In the memory-mapped scheme, the physical address space is partitioned so that a range of addresses is reserved for device registers rather than RAM. When the CPU issues a load or store to one of those addresses, the memory bus decodes the address and sends the transaction to the device, not to memory.

How Memory-Mapped I/O Works

In a typical system, the CPU generates an address on the memory bus. The memory controller (or the interconnect, such as PCIe) checks whether that address falls within a region assigned to a device. If it does, the transaction is forwarded to the device’s register file. The device then either returns data (on a read) or accepts data (on a write).

For example, suppose a network card has a register at physical address 0xFEBE0000 that holds the current link status. Software can read it with a single instruction:

uint32_t status = *(volatile uint32_t *)0xFEBE0000;

The volatile keyword is essential—it tells the compiler to actually perform the read each time and not to cache the value in a register or reorder accesses. Without volatile, the compiler might optimize away repeated reads, which would break device communication.

Writes work similarly. To send a command, software writes a value to a command register:

*(volatile uint32_t *)0xFEBE0004 = 0x1;

The device sees the write and acts on it. The CPU does not know or care that the address is not RAM; it just performs a store. The hardware routes the transaction correctly.

Memory-Mapped I/O vs. Port-Mapped I/O

The main alternative to MMIO is port-mapped I/O (PMIO), which uses a separate address space and special instructions. On x86, the IN and OUT instructions access up to 64K I/O ports. The CPU signals that the transaction is an I/O access, and a separate I/O bus (or a multiplexed bus) delivers it to the device.

Key differences:

  • Instruction set: PMIO requires dedicated instructions; MMIO uses the same load/store instructions as memory access.
  • Address space: PMIO has a small, separate address space; MMIO consumes part of the physical memory address space.
  • Access size: PMIO often limits access to 8-bit or 16-bit chunks; MMIO can use any width the CPU supports (8, 16, 32, 64 bits).
  • Hardware complexity: MMIO simplifies the CPU because it does not need separate I/O instructions, but it requires careful address decoding to avoid conflicts with RAM.

Most modern architectures (ARM, RISC-V, x86-64 in many cases) use MMIO almost exclusively. x86 still supports PMIO for legacy devices, but PCIe devices are typically memory-mapped.

Address Decoding and Resource Allocation

Every device must be assigned a unique range of physical addresses. This is done by the system firmware (BIOS/UEFI) or by the operating system during enumeration. For PCIe devices, the base address registers (BARs) define the size and location of the device’s register window. The firmware or OS writes a value to the BAR to set the base address, and then the device responds to accesses within that range.

The address decoding logic lives in the memory controller and in the device itself. The memory controller checks if the address falls into a hole reserved for devices; if so, it does not send the request to DRAM. The device checks if the address falls within its assigned window; if so, it accepts the transaction. This is why MMIO addresses are often in the high range (e.g., 0xF0000000 and above) on 32-bit systems—those regions are not used for RAM.

Common Problems and Pitfalls

Using MMIO correctly requires attention to several issues:

  • Compiler optimizations: Without volatile, the compiler may reorder or eliminate accesses. Always mark pointers to device registers as volatile.
  • Memory barriers: The CPU may reorder memory operations, especially on weakly ordered architectures like ARM. To ensure that a register write is seen before a subsequent DMA operation, use the appropriate memory barrier instructions (e.g., dsb on ARM, mfence on x86).
  • Unaligned access: Some devices require naturally aligned accesses. Accessing a 32-bit register at an odd address may cause a bus error or undefined behavior.
  • Write-combining: On x86, MMIO regions are often marked as uncacheable or write-combining to prevent the CPU from buffering writes. The OS sets the page table attributes (e.g., PAT or MTRR) accordingly. Incorrect caching can lead to stale reads or missed writes.
  • Address conflicts: If two devices are assigned overlapping ranges, the system may crash or misbehave. The OS must ensure unique allocations.

One common misconception is that MMIO is slow because it goes through the memory bus. In reality, MMIO accesses are typically faster than PMIO because they avoid the separate I/O bus and can use wider transfers. However, they are still slower than RAM accesses because devices may have latency and cannot benefit from caching.

How to Choose Between MMIO and PMIO

For a new hardware design, MMIO is almost always the better choice. It simplifies the CPU instruction set, supports larger data transfers, and is the standard for modern buses like PCIe. PMIO remains only for legacy compatibility on x86.

If you are writing a device driver, you do not usually choose—the hardware dictates the method. The OS provides APIs to map device memory (e.g., ioremap on Linux) and to read/write registers (e.g., readl/writel). These functions handle the necessary barriers and caching attributes.

Frequently Asked Questions

Why do MMIO addresses not conflict with RAM?

The physical address space is partitioned by the memory controller. RAM occupies only a portion of the address range; the rest is reserved for devices. The OS knows which ranges are RAM and which are MMIO, and it never uses MMIO ranges for normal memory allocation.

Can I access MMIO from user space?

Yes, but only with special permissions. On Linux, you can use mmap on /dev/mem or a device file to map physical addresses into user space, but this is dangerous and usually restricted to root. Most drivers access MMIO from kernel space.

Why do I need to use volatile?

Volatile tells the compiler that the memory location can change outside its control, so it must not optimize away accesses. Without it, the compiler might assume the value never changes and skip reads, or reorder writes, breaking device communication.

What is the difference between MMIO and DMA?

MMIO is how the CPU accesses device registers. DMA (Direct Memory Access) is a method for devices to read or write RAM without CPU involvement. DMA transfers data between a device and memory, while MMIO transfers control/status information between the CPU and the device. They are complementary.

Is MMIO slower than port I/O?

Generally, MMIO is faster because it can use the full memory bus width and avoids the overhead of special I/O instructions. However, the actual speed depends on the device and bus. On modern systems, MMIO is preferred for performance.

How does the CPU know an address is MMIO?

The CPU does not know—it just issues a load/store. The memory controller decodes the address and routes it appropriately. The OS sets up page tables with special caching attributes for MMIO regions, but the CPU itself does not distinguish between RAM and device memory at the instruction level.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →