R0KernelPath
module 093 / 262

Accessing Hardware: MMIO & ioremap

Master the kernel's precise contracts for reaching hardware registers and making devices read and write RAM safely — the foundation every real device driver is built on.

~18h focused study11 concepts

Overview

When you plug a USB drive into your laptop, or when a network card receives a packet, the CPU needs a way to talk to that hardware. How does it do it? In the early days, hardware devices lived in a completely separate address space — the CPU had special IN and OUT instructions just for talking to them (port I/O). But as devices grew more capable and the software world needed a cleaner model, a better idea took over: memory-mapped I/O (MMIO). The device's internal registers are mapped into the same virtual address space as everything else. You just read and write memory — except it is not RAM on the other end. It is hardware.

This sounds simple, but it hides enormous complexity. The CPU and compiler are both aggressively optimistic: they reorder instructions, cache values in registers, and skip writes they think are "redundant." When you write to a hardware register, these optimizations become catastrophic bugs. The kernel's MMIO API — ioremap, readl/writel, memory barriers — exists precisely to tame this chaos. Every function and macro in this API is a carefully reasoned contract between software and silicon.

Layer on top of that the DMA problem: modern devices do not just sit there waiting for the CPU to poke them. They grab data directly from RAM — Direct Memory Access. Now the CPU and the device are both touching memory simultaneously, and without careful coordination, they will disagree about what is in that memory. The DMA mapping API (dma_alloc_coherent, dma_map_single) is the kernel's solution to this deep problem, spanning cache coherency, IOMMU translation, and bus addressing.

This module builds a complete, precise mental model of all of this — from the voltage levels on a PCIe lane all the way to the kernel code you will write in your own driver. By the end, you will understand not just how to use these APIs, but why each guard-rail exists and what disaster it is preventing.

why this matters for the kernel

Every real device driver — whether for a GPU, a network card, a sensor on an embedded board, or a custom FPGA — eventually has to access hardware registers. Without a deep understanding of MMIO and DMA, driver code compiles, looks reasonable, and then fails in ways that are nearly impossible to debug: silent data corruption, crashes only under load, hardware that gets stuck after a suspend/resume cycle. These bugs have caused real-world security vulnerabilities, data loss, and kernel panics in production systems. Understanding this material at a mechanistic level is what separates a driver author from a driver maintainer.

Beyond the immediate practical value, this topic sits at one of the most intellectually beautiful intersections in computing: virtual memory, cache hierarchy, hardware bus protocols, and the POSIX memory model all converge here. Mastering it means you will read kernel source code with new clarity — you will see why every wmb() is placed exactly where it is, and you will be equipped to spot bugs that reviewers with less background will miss. Kernel contributors who deeply understand MMIO and DMA are consistently among the most impactful, because so many subsystems — PCI, USB, platform drivers, DRM/GPU — depend on getting this right.

MMIO and DMA Address Translation Flow

rendering diagram…

Two separate translation units govern memory access: the CPU's MMU (virtual to physical for CPU reads/writes) and the IOMMU (bus address to physical for device DMA). MMIO register accesses bypass the CPU cache entirely via non-cacheable page table entries. The dma_addr_t you program into a device is the address after IOMMU translation, not the physical or virtual address.

The concepts

01Port I/O vs Memory-Mapped I/O: The Two Worlds

Before MMIO existed, x86 CPUs had a completely separate 16-bit I/O address space — 65,536 ports — accessed via dedicated IN and OUT instructions. A keyboard controller might sit at port 0x60, the PIC at 0x20. This worked, but it was limited: only 64K addresses, no way to DMA directly to port I/O space, and completely x86-specific.

Memory-mapped I/O solves this by mapping device registers into the normal physical address space. When the CPU issues a load or store to a physical address in an MMIO region, the memory controller routes it to the device bus — PCIe, AHB, APB, whatever — rather than to DRAM. The device's registers respond as if they were RAM, but with completely different semantics (reads can have side effects, some bits are read-only, writing certain values triggers hardware actions).

On ARM and RISC-V, MMIO is the only model — there are no port I/O instructions at all. On x86, modern devices support both, but nearly all use MMIO for their main register interface. Port I/O survives in x86 for legacy devices (the 8250 UART at 0x3F8, the IDE controller at 0x1F0, the PCI config space access mechanism at 0xCF8/0xCFC).

The kernel provides inb/outb/inw/outw/inl/outl for port I/O and readb/readw/readl/readq / writeb/writew/writel/writeq for MMIO. These are not interchangeable — using readl on a port address is a bug, as is using inl on a MMIO address.

/* Port I/O: x86 only, legacy UART status register */
u8 status = inb(0x3FD);  /* read Line Status Register */
if (status & 0x20)       /* TX holding register empty? */
    outb('A', 0x3F8);    /* write character to TX buffer */

/* MMIO: works everywhere, modern devices */
void __iomem *base = ioremap(0xFE000000, 0x1000);
u32 val = readl(base + 0x10);  /* read a 32-bit MMIO register */
writel(val | BIT(3), base + 0x10);  /* set bit 3 */
iounmap(base);

02Physical Addresses, Virtual Addresses, and ioremap

The CPU's MMU translates virtual addresses (what your code uses) into physical addresses (what actually goes on the memory bus). RAM occupies some physical address ranges; device registers occupy others. The kernel's direct mapping covers RAM, but MMIO regions are not RAM — they are not automatically mapped.

ioremap(phys_addr, size) creates a kernel virtual address mapping for a physical MMIO region. Under the hood it calls __get_vm_area to find unused virtual address space in the kernel's vmalloc region, then programs page table entries for those virtual pages pointing to the MMIO physical addresses, with the page table flags set to mark them as non-cacheable and (on modern kernels) with the correct memory type (on x86, this uses PAT — Page Attribute Table — to set WC or UC; on ARM, this sets the MT_DEVICE memory type in the PTE attributes).

The crucial detail: MMIO mappings must be non-cacheable (or at minimum use a device memory type). If you cached a register read, the CPU might return a stale value from its L1 cache instead of asking the hardware. If you cached a register write, the write might sit in a write buffer and never reach the hardware, or reach it in the wrong order.

ioremap returns a void __iomem * — the __iomem annotation is a sparse checker tag (not a real C type) that ensures you use readl/writel rather than raw pointer dereferences. If you do *(volatile u32 *)(base + offset) instead, sparse will warn you, and you may still get wrong behavior on some architectures.

iounmap(addr) tears down the mapping. Always call it in your driver's remove path. Forgetting leaks kernel virtual address space.

#include <linux/io.h>
#include <linux/ioport.h>

#define MY_DEV_BASE   0xFE200000UL
#define MY_DEV_SIZE   0x1000

static void __iomem *regs;

static int my_driver_probe(struct platform_device *pdev)
{
    /* preferred: get the resource from device tree / ACPI */
    struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENODEV;

    regs = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(regs))
        return PTR_ERR(regs);

    /* now safe to use readl/writel */
    u32 id = readl(regs + 0x00);   /* read chip ID register */
    dev_info(&pdev->dev, "HW id = 0x%08x\n", id);
    return 0;
}
/* devm_ variant auto-unmaps on driver detach — no iounmap needed */

03request_mem_region: Claiming Ownership

ioremap creates a virtual mapping, but it does not tell the rest of the kernel that your driver owns that physical range. If two drivers both tried to ioremap the same region, they would each get different virtual addresses pointing at the same hardware — and they would stomp on each other's register writes with no warning.

request_mem_region(start, len, name) registers your claim in the kernel's iomem tree — visible at /proc/iomem. If someone else has already claimed that range, the function returns NULL and you should fail your probe. release_mem_region releases it.

In practice, the modern and correct pattern is devm_ioremap_resource() which calls devm_request_mem_region() and devm_ioremap() atomically and ties both to the device's lifetime. For manual usage, always call request_mem_region before ioremap, and iounmap before release_mem_region.

For PCI devices, pci_request_regions() does the equivalent for all BARs (Base Address Registers) at once.

The /proc/iomem file is a goldmine for understanding your system's physical address layout — you can see where DRAM lives, where PCIe MMIO windows are, where the LAPIC is mapped, and so on.

/* Manual pattern (shown for clarity; prefer devm_ioremap_resource) */
if (!request_mem_region(MY_DEV_BASE, MY_DEV_SIZE, "my_device")) {
    pr_err("my_device: MMIO region busy\n");
    return -EBUSY;
}

void __iomem *regs = ioremap(MY_DEV_BASE, MY_DEV_SIZE);
if (!regs) {
    release_mem_region(MY_DEV_BASE, MY_DEV_SIZE);
    return -ENOMEM;
}

/* ... use regs ... */

/* cleanup (reverse order!) */
iounmap(regs);
release_mem_region(MY_DEV_BASE, MY_DEV_SIZE);

/* Check /proc/iomem to see all claimed regions:
   $ grep my_device /proc/iomem */

04readl/writel and Why Raw Pointer Derefs Are Wrong

Given a void __iomem *base, why can't you just do *(u32 *)(base + offset)? Three reasons:

1. Compiler optimization. The C compiler can eliminate or reorder memory accesses if it thinks they are pure reads/writes to ordinary memory. readl/writel compile to function calls or inline asm with compiler barriers (barrier() or __asm__ volatile) that prevent the compiler from moving accesses across them.

2. Architecture-specific requirements. On some architectures (notably IA-64 and some MIPS variants), MMIO accesses require specific instructions or explicit synchronization. readl/writel are architecture-specific macros/inline functions that emit the correct instruction sequence.

3. Endianness. readl/writel consistently give you the value in CPU native byte order (little-endian on x86/ARM-LE/RISC-V). The __raw_readl/__raw_writel variants skip the byte-swap for cases where you explicitly want to handle endianness yourself (rare).

The full family: readb (8-bit), readw (16-bit), readl (32-bit), readq (64-bit), and matching writeb/w/l/q. There are also ioread8/16/32/64 and iowrite8/16/32/64 which work for both MMIO and port I/O depending on how the region was mapped — useful for drivers that need to support both.

For read-modify-write sequences, there is no hardware-level atomicity guarantee — if the hardware can mutate a register between your readl and writel, you need a spinlock or the hardware provides an atomic set/clear mechanism (like set/clr register pairs, common in modern SoCs).

#include <linux/io.h>

/* Register offsets for a hypothetical DMA controller */
#define DMA_CTRL    0x00
#define DMA_SRC     0x04
#define DMA_DST     0x08
#define DMA_LEN     0x0C
#define DMA_STATUS  0x10

#define DMA_CTRL_START  BIT(0)
#define DMA_CTRL_RESET  BIT(1)
#define DMA_STATUS_DONE BIT(0)

static void start_dma(void __iomem *base, u32 src, u32 dst, u32 len)
{
    writel(src, base + DMA_SRC);
    writel(dst, base + DMA_DST);
    writel(len, base + DMA_LEN);
    /* WRONG without wmb() here — see next concept! */
    writel(DMA_CTRL_START, base + DMA_CTRL);
}

static bool dma_done(void __iomem *base)
{
    return (readl(base + DMA_STATUS) & DMA_STATUS_DONE) != 0;
}

05Memory Barriers for MMIO: rmb, wmb, mb, and mmiowb

This is the most subtle and most commonly misunderstood part of MMIO programming. Modern CPUs and memory systems execute instructions out of order — writes can be buffered and reach the bus in a different order than the program wrote them. For regular RAM this is mostly invisible because the CPU maintains coherence for its own threads. But for MMIO, out-of-order writes can mean you set DMA_CTRL_START before the hardware ever sees DMA_SRC, causing the hardware to DMA from the wrong address.

The barrier functions:

  • rmb() — read memory barrier: all prior reads complete before any subsequent reads. Use before reading a status register after reading data that the status confirms.
  • wmb() — write memory barrier: all prior writes complete before any subsequent writes. Use when you write configuration registers and then write a "go" bit.
  • mb() — full memory barrier: all prior reads and writes complete before any subsequent reads and writes. Heavier and less common.
  • barrier() — compiler barrier only, no CPU instructions emitted. Prevents compiler reordering but does nothing about CPU reordering. Almost always wrong for MMIO.

On x86: The CPU's store buffer is strongly ordered (TSO — Total Store Order), so wmb() compiles to just a compiler barrier (barrier()) in most configurations. On weakly-ordered architectures (ARM, PowerPC, MIPS), wmb() emits a dmb st (ARM) or sync (PowerPC) instruction.

readl/writel already include barriers on many architectures — specifically, writel on ARM includes a dmb after the store. But the ordering between consecutive writes (writing config before writing go) still requires an explicit wmb() between them if you need to be strictly correct across all architectures.

mmiowb(): A historical special-purpose barrier for spinlock-protected MMIO sequences where you need to ensure all writes are visible to the device before releasing the lock. As of Linux 5.2, mmiowb() is a no-op on most architectures because the locking primitives already provide the necessary guarantees. You may still see it in older drivers.

The golden rule: write configuration data, wmb(), write the kick register. When polling for completion: read the status register, rmb(), read the result data. When in doubt, use mb().

#include <linux/io.h>

/* Correct version of start_dma from previous concept */
static void start_dma_correct(void __iomem *base, u32 src, u32 dst, u32 len)
{
    writel(src, base + DMA_SRC);   /* write address config */
    writel(dst, base + DMA_DST);
    writel(len, base + DMA_LEN);
    wmb();  /* ensure src/dst/len are visible to device BEFORE start */
    writel(DMA_CTRL_START, base + DMA_CTRL);  /* kick */
}

/* Polling for completion with correct ordering */
static int wait_for_dma(void __iomem *base, void *result_buf, size_t result_size)
{
    u32 status;
    /* busy-wait (real code would use interrupts + wait_event) */
    do {
        status = readl(base + DMA_STATUS);
    } while (!(status & DMA_STATUS_DONE));

    rmb();  /* ensure status read completes before we read result data */
    /* now safe to read the result written by DMA hardware to result_buf */
    return 0;
}

06Physical Addresses vs Bus Addresses vs Virtual Addresses

This is the conceptual foundation for DMA. There are three different kinds of addresses in play, and conflating them causes subtle, devastating bugs.

Virtual address (VA): What the CPU sees after MMU translation. Your kernel pointer, like void *buf. This is what kmalloc returns.

Physical address (PA): The actual address on the memory bus, after MMU translation. You can get it from a virtual address with virt_to_phys(va) (only valid for linearly-mapped kernel memory).

Bus address (BA) / DMA address: The address that a device uses to refer to memory. On a simple system with no IOMMU, BA == PA. But with an IOMMU (Intel VT-d, AMD-Vi, ARM SMMU), the IOMMU sits between the PCIe bus and the memory controller and does address translation — just like the MMU does for the CPU. The device sees a "device virtual address" that the IOMMU maps to a physical address. Additionally, some architectures have PCI bus address translation where physical address X appears at bus address X + offset.

Why this matters: You must NEVER give a device a kernel virtual address or a physical address — you must give it the bus address that the DMA API computed for you. On a simple embedded system this might work by accident (VA == PA + constant, and no IOMMU). But your driver must work on server-class x86 with VT-d enabled, where those addresses are completely different. The DMA API abstracts this.

dma_addr_t is the kernel type for bus addresses. It is distinct from phys_addr_t (physical addresses) and from pointers. Mixing them up is a category error.

#include <linux/dma-mapping.h>

void address_types_demo(struct device *dev)
{
    char *kbuf = kmalloc(PAGE_SIZE, GFP_KERNEL);

    /* Virtual address: what the kernel code uses */
    pr_info("virtual:  %px\n", kbuf);

    /* Physical address: the hardware memory address */
    phys_addr_t pa = virt_to_phys(kbuf);
    pr_info("physical: %pa\n", &pa);

    /* Bus/DMA address: what you give to the device */
    dma_addr_t dma_handle;
    /* This is WRONG — don't use virt_to_phys for DMA! */
    /* dma_handle = virt_to_phys(kbuf);  <-- BUG on IOMMU systems */

    /* This is RIGHT — let the DMA API do the translation */
    dma_handle = dma_map_single(dev, kbuf, PAGE_SIZE, DMA_TO_DEVICE);
    pr_info("bus/dma:  %pad\n", &dma_handle);
    /* dma_handle is what you program into the device's DMA address register */

    dma_unmap_single(dev, dma_handle, PAGE_SIZE, DMA_TO_DEVICE);
    kfree(kbuf);
}

07Cache Coherency and DMA: The Core Problem

Modern CPUs have multiple levels of cache (L1/L2/L3). When you write to a memory location, the write may sit in the CPU's cache and not yet be in DRAM. When the device does DMA to read that memory, it reads from DRAM — and gets stale data. Similarly, when the device does DMA to write results into memory, the CPU's cache might have an old value and return it on the next read.

There are two solutions, and the Linux DMA API supports both:

Coherent DMA (dma_alloc_coherent): The kernel allocates memory and configures both the CPU and the IOMMU/bus so that there is no caching — every access goes straight to memory. Reads and writes by both CPU and device are immediately visible to the other. Coherent memory is safe to use at any time without explicit flushing. The cost: non-cached CPU accesses are slower (no L1/L2 benefit).

Streaming DMA (dma_map_single / dma_map_sg): You allocate normal cached memory (with kmalloc or similar), then call dma_map_* before giving the address to the device. This flush/invalidates the relevant cache lines. When the device is done, you call dma_unmap_* which flushes again in the other direction. Between map and unmap, the CPU must not touch the buffer — the device owns it. This is more complex but allows cached CPU access before and after the DMA window.

The ownership model: Think of it like passing a physical document to a colleague. While they have it, you do not touch it. When they hand it back, you can read their edits. dma_map_* is the handoff; dma_unmap_* is the return.

On cache-coherent architectures (most modern x86 with Intel VT-d), some of these flushes are no-ops because the hardware maintains coherency in hardware. But your driver must still call the API correctly — do not rely on architecture-specific behavior.

#include <linux/dma-mapping.h>

/* --- Coherent DMA: for descriptor rings, persistent shared memory --- */
struct my_desc {
    __le32 addr;
    __le32 len;
    __le32 flags;
    __le32 status;
};

struct my_desc *desc_ring;
dma_addr_t      desc_ring_dma;

int setup_descriptor_ring(struct device *dev, int n)
{
    /* Allocates n*sizeof(*desc_ring) bytes, returns both VA and bus addr */
    desc_ring = dma_alloc_coherent(dev,
                    n * sizeof(*desc_ring),
                    &desc_ring_dma,
                    GFP_KERNEL);
    if (!desc_ring)
        return -ENOMEM;
    /* Program device with desc_ring_dma (not desc_ring pointer!) */
    return 0;
}

void teardown_descriptor_ring(struct device *dev, int n)
{
    dma_free_coherent(dev, n * sizeof(*desc_ring), desc_ring, desc_ring_dma);
}

/* --- Streaming DMA: for per-packet transmit buffers --- */
int transmit_packet(struct device *dev, void *data, size_t len)
{
    dma_addr_t dma_addr = dma_map_single(dev, data, len, DMA_TO_DEVICE);
    if (dma_mapping_error(dev, dma_addr))
        return -EIO;
    /* program device: start DMA from dma_addr, length len */
    /* ... wait for completion ... */
    dma_unmap_single(dev, dma_addr, len, DMA_TO_DEVICE);
    return 0;
}

08dma_alloc_coherent Deep Dive

dma_alloc_coherent(dev, size, &dma_handle, gfp) is the Swiss Army knife of DMA allocation. It does several things atomically:

  1. Allocates physical memory — using the platform's DMA allocator. On x86 this is typically a CMA (Contiguous Memory Allocator) pool or alloc_pages. On ARM, it may allocate from a dedicated coherent DMA region configured in the device tree.

  2. Maps it for the device — programs the IOMMU (if present) to create a mapping, returning a dma_addr_t bus address.

  3. Maps it for the CPU — creates a virtual mapping with appropriate cache attributes. On non-coherent systems (many ARM SoCs without hardware cache coherency), this mapping will be uncached or write-combining. On x86, normal cached access is fine.

  4. Returns both addresses — the pointer (for CPU access) and dma_handle (for programming into the device).

Size considerations: The size must be a multiple of PAGE_SIZE in practice (though the API does not strictly require it). Large allocations can fail due to fragmentation — for very large DMA buffers, consider CMA, or scatter-gather lists.

GFP_KERNEL vs GFP_ATOMIC: Use GFP_KERNEL (can sleep) unless you are in interrupt context. In interrupt context, use GFP_ATOMIC. Note that GFP_ATOMIC allocations can fail more often.

Setting DMA masks: Before calling any DMA API, you must set the device's DMA mask to tell the kernel what physical address range the device can reach. A 32-bit DMA-capable device can only address 4 GB; a 64-bit device can address the full range. dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32)) configures both the streaming and coherent masks.

#include <linux/dma-mapping.h>

static int setup_dma_resources(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    void *cpu_addr;
    dma_addr_t dma_addr;
    size_t size = 4096; /* one page */

    /* Tell kernel the device's address range capability */
    if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
        dev_err(dev, "cannot set DMA mask\n");
        return -EINVAL;
    }

    /* Allocate coherent buffer: CPU virtual addr + device bus addr */
    cpu_addr = dma_alloc_coherent(dev, size, &dma_addr, GFP_KERNEL);
    if (!cpu_addr) {
        dev_err(dev, "dma_alloc_coherent failed for %zu bytes\n", size);
        return -ENOMEM;
    }

    dev_info(dev, "coherent: cpu=%px dma=%pad size=%zu\n",
             cpu_addr, &dma_addr, size);

    /* CPU writes to cpu_addr; device reads from dma_addr */
    memset(cpu_addr, 0xAB, size);  /* no cache flush needed — coherent */
    /* ... program device with dma_addr ... */

    /* Cleanup */
    dma_free_coherent(dev, size, cpu_addr, dma_addr);
    return 0;
}

09Streaming DMA: dma_map_single, dma_map_sg, and Directions

Streaming DMA is used when the CPU prepares a buffer, hands it to the device for a single operation, then gets it back. The key difference from coherent DMA: the memory is normally cached for CPU access; the dma_map_* call performs cache management at the boundary.

DMA direction tells the API which side writes:

  • DMA_TO_DEVICE: CPU writes data, device reads it (e.g., transmit buffer). dma_map flushes CPU cache to DRAM.
  • DMA_FROM_DEVICE: Device writes data, CPU reads it (e.g., receive buffer). dma_map invalidates CPU cache. dma_unmap also invalidates, so the CPU reads fresh device data.
  • DMA_BIDIRECTIONAL: Both sides can read and write. Performs both flush and invalidate. Heavier — avoid unless necessary.

dma_map_sg (scatter-gather): Most real payloads are not contiguous in physical memory (consider a network packet that spans multiple pages). A scatter-gather list (struct scatterlist) describes a set of physically discontiguous buffers. dma_map_sg(dev, sgl, n, dir) maps all of them and, if the IOMMU supports it, may merge adjacent entries into larger contiguous bus-address ranges, reducing the number of DMA descriptors the hardware needs. The device sees a (possibly shorter) list of contiguous bus-address ranges.

Sync functions: If you need the CPU to peek at the data while the DMA is in progress (unusual but sometimes needed for debugging), dma_sync_single_for_cpu and dma_sync_single_for_device transfer ownership temporarily without unmapping. Never do this in production without careful thought.

Error checking: dma_mapping_error(dev, dma_addr) MUST be called after dma_map_single. On low-memory systems or with an IOMMU that has run out of address space, mapping can fail and return a sentinel error value.

#include <linux/dma-mapping.h>
#include <linux/scatterlist.h>

/* Streaming DMA for a receive buffer */
static int receive_setup(struct device *dev, size_t buf_size)
{
    void *buf = kmalloc(buf_size, GFP_KERNEL | GFP_DMA);
    if (!buf) return -ENOMEM;

    dma_addr_t dma = dma_map_single(dev, buf, buf_size, DMA_FROM_DEVICE);
    if (dma_mapping_error(dev, dma)) {
        kfree(buf);
        return -EIO;
    }
    /* Give dma address to hardware RX descriptor */
    /* ... hardware writes received data to buf via DMA ... */
    dma_unmap_single(dev, dma, buf_size, DMA_FROM_DEVICE);
    /* NOW safe to read buf — cache has been invalidated */
    process_received_data(buf, buf_size);
    kfree(buf);
    return 0;
}

/* Scatter-gather for multi-page transmit */
static int transmit_sg(struct device *dev, struct page **pages, int n_pages)
{
    struct scatterlist sgl[n_pages]; /* or use sg_alloc_table for large n */
    sg_init_table(sgl, n_pages);
    for (int i = 0; i < n_pages; i++)
        sg_set_page(&sgl[i], pages[i], PAGE_SIZE, 0);

    int mapped = dma_map_sg(dev, sgl, n_pages, DMA_TO_DEVICE);
    if (mapped <= 0) return -EIO;
    /* mapped <= n_pages: IOMMU may have merged entries */
    struct scatterlist *sg;
    int i;
    for_each_sg(sgl, sg, mapped, i) {
        /* program device: sg_dma_address(sg), sg_dma_len(sg) */
    }
    dma_unmap_sg(dev, sgl, n_pages, DMA_TO_DEVICE);
    return 0;
}

10IOMMU: The Device-Side MMU

Just as the CPU's MMU protects processes from each other by giving each process its own virtual address space, the IOMMU (Input-Output Memory Management Unit) protects the rest of the system from misbehaving or compromised devices. Without an IOMMU, a buggy or malicious PCIe device could DMA to any physical address — including kernel text, page tables, or other processes' memory. With an IOMMU, the device sees a restricted address space and can only reach memory the kernel has explicitly mapped for it.

On Intel/AMD x86 systems, this is VT-d / AMD-Vi. On ARM systems, it is called an SMMU (System MMU). On RISC-V, it is the IOMMU defined in the RISC-V IOMMU specification.

How it interacts with the DMA API: When you call dma_alloc_coherent or dma_map_single, the kernel's DMA subsystem calls into the IOMMU driver to create a mapping in the IOMMU's page tables. The dma_addr_t returned is the IOMMU virtual address — what the device puts on the PCIe bus. The IOMMU translates this to the actual physical address.

Consequences for drivers: This is exactly why you cannot use virt_to_phys() to get a DMA address. That gives you the physical address, but the device needs the IOMMU virtual address. The two are equal only when no IOMMU is active or in passthrough mode.

SWIOTLB (Software I/O Translation Look-aside Buffer): On systems where the device cannot address all of RAM (e.g., a 32-bit DMA device on a system with 8 GB RAM, so the top 4 GB are unreachable by the device), the kernel may use a software bounce buffer in the low 4 GB. dma_map_single copies data to the bounce buffer and returns its address. dma_unmap_single copies it back. The driver sees none of this — it is fully transparent.

/* No direct kernel driver code — this is infrastructure the DMA API hides.
   But you can observe IOMMU mappings:

   $ dmesg | grep -i iommu
   [    0.458291] DMAR: IOMMU enabled
   [    0.459312] DMAR-IR: Enabled IRQ remapping in x2apic mode

   $ cat /sys/kernel/iommu_groups/  (shows device groupings)

   $ dmesg | grep swiotlb
   [    0.000000] software IO TLB: mapped [mem 0x...-0x...] (64MB)

   To test your driver with strict IOMMU:
   Boot with: intel_iommu=on iommu=pt  (passthrough, less protection)
   Or:        intel_iommu=on           (full translation — tests DMA API correctness)

   A driver that works only with intel_iommu=off but breaks with it on
   is almost certainly using virt_to_phys() or a physical address directly
   instead of the DMA API. */

11The devm_ Family: Resource-Managed Driver APIs

Writing correct cleanup paths in error paths is notoriously difficult. If probe() calls ioremap, then request_irq, then dma_alloc_coherent, and the third step fails, you need to call free_irq and iounmap in exactly the right order. Add five more resources and the cleanup code becomes a tangle of goto err_N labels.

The devm_ (device-managed) variants solve this by tying resource lifetimes to a device's lifetime. Every devm_ioremap, devm_kzalloc, devm_request_irq, etc. registers a destructor with the device. When the device is removed (or probe() returns an error), the kernel automatically calls all registered destructors in reverse order.

Key devm functions for MMIO/DMA:

  • devm_ioremap(dev, phys, size) — auto-unmaps on remove
  • devm_ioremap_resource(dev, res) — requests region + ioremap together
  • devm_ioremap_nocache(dev, phys, size) — explicitly non-cacheable (usually same as ioremap on modern kernels)
  • devm_kzalloc(dev, size, gfp) — memory freed on remove
  • devm_request_irq(dev, irq, handler, flags, name, data)

When NOT to use devm: If you need the resource to outlive the device (unusual), or if you are writing code that must run in a context where device lifetime tracking is not available.

devm_add_action(dev, fn, data): Register a custom cleanup function. Very useful for resources that do not have their own devm_ wrapper.

#include <linux/device.h>
#include <linux/io.h>
#include <linux/platform_device.h>

struct my_priv {
    void __iomem *regs;
    /* other fields */
};

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;

    struct my_priv *priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv) return -ENOMEM;
    /* devm_kzalloc: freed automatically on error or remove */

    struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->regs = devm_ioremap_resource(dev, res);
    /* devm_ioremap_resource: request_mem_region + ioremap, both auto-released */
    if (IS_ERR(priv->regs)) return PTR_ERR(priv->regs);

    int irq = platform_get_irq(pdev, 0);
    int ret = devm_request_irq(dev, irq, my_irq_handler, 0, "my_dev", priv);
    /* devm_request_irq: free_irq called automatically */
    if (ret) return ret;

    platform_set_drvdata(pdev, priv);
    return 0;
    /* No goto cleanup chains needed! devm handles everything. */
}

Key insights

  • ✦ioremap creates a virtual mapping with non-cacheable page attributes — hardware registers must never be cached, because reads have side effects and the CPU cache would lie to you.
  • ✦The three address types (virtual, physical, bus/DMA) are distinct concepts; confusing them is the root cause of most DMA bugs, especially on systems with an IOMMU where physical != bus address.
  • ✦readl/writel are not just stylistic preferences — they include compiler barriers and architecture-specific instructions that prevent both compiler and CPU reordering of MMIO accesses.
  • ✦The ownership model in streaming DMA is strict: between dma_map and dma_unmap, the device owns the buffer. The CPU must not read or write it. Violating this gives you cache incoherency bugs that only appear under load.
  • ✦dma_alloc_coherent solves the coherency problem by paying a performance cost (uncached access). dma_map_single preserves cached performance but requires explicit ownership transfer. Choose based on access pattern.
  • ✦Memory barriers exist because CPUs and compilers reorder operations for performance. For MMIO, this is not an optimization — it is a bug. wmb() before a 'kick' register write is not paranoia; it is a correctness requirement on weakly-ordered CPUs.
  • ✦The IOMMU is not just a performance feature; it is a security boundary. A driver that works without an IOMMU but breaks with one is almost certainly using a raw physical address instead of the DMA API — a real security vulnerability.
  • ✦The devm_ resource management API is not just convenience — it makes cleanup paths in probe() composable and correct. Using raw ioremap in modern drivers is a code smell unless there is a specific reason.

Common pitfalls

  • ⚠Using virt_to_phys() to get a DMA address and programming it into the device. This works on simple systems without an IOMMU but silently breaks on any server or modern embedded platform with an IOMMU. Always use the DMA API.
  • ⚠Dereferencing a void __iomem * directly with a cast (e.g., *(u32 *)ptr) instead of readl/writel. Sparse will warn, and on weakly-ordered architectures you will get reordering bugs.
  • ⚠Forgetting to call dma_mapping_error() after dma_map_single(). On IOMMU-constrained systems, mapping can fail, returning a poison value that looks like a valid address but causes the device to DMA to an invalid location.
  • ⚠Touching a DMA buffer after dma_map_single() but before dma_unmap_single(). The CPU's cached view and the device's DRAM view will diverge. The data the CPU reads back will be stale.
  • ⚠Calling iounmap before release_mem_region (or vice versa) in the cleanup path. The correct order is: iounmap first, then release_mem_region. With devm_, this is handled automatically.
  • ⚠Not setting the DMA mask (dma_set_mask_and_coherent) before any DMA allocation. The kernel will use a conservative default, which may result in allocations from low memory that are unnecessarily constrained.
  • ⚠Using barrier() (compiler-only barrier) instead of wmb()/rmb() when you need CPU-level ordering for MMIO. On x86 you might get away with it due to TSO, but the code will break on ARM/PowerPC.
  • ⚠Allocating DMA coherent memory in interrupt context with GFP_KERNEL. dma_alloc_coherent with GFP_KERNEL may sleep. In interrupt handlers, use GFP_ATOMIC or pre-allocate in probe().

Practice

  1. 01
    medium

    Write a minimal platform driver for a fictional 32-bit counter device at physical address 0x10000000. The device has two registers: COUNT_REG at offset 0x00 (read-only, 32-bit counter value) and CTRL_REG at offset 0x04 (write 1 to bit 0 to reset counter). Use devm_ioremap_resource and implement probe/remove. Test on QEMU virt machine with a custom device tree entry.

  2. 02
    medium

    Instrument an existing driver (pick any driver under drivers/net/ethernet/ or drivers/char/ in the kernel source) and identify every readl/writel call. For each writel followed by another writel, determine whether a wmb() is needed between them. Write a brief analysis: what register is being written, is the second write a 'kick', and what would go wrong on an ARM SoC without the barrier.

  3. 03
    hard

    Write a kernel module that allocates a DMA-coherent buffer of 4096 bytes using dma_alloc_coherent on a platform device, fills it with a known pattern from the CPU side, then reads it back and verifies the pattern. Print the virtual address, physical address (via virt_to_phys on a regular kmalloc allocation for comparison), and dma_addr_t. Boot with intel_iommu=on (or iommu.passthrough=0) and verify it still works.

  4. 04
    easy

    Explore /proc/iomem and /proc/ioports on a Linux machine (can be a VM). Document every entry: what device owns each region, what size it is, and whether it is MMIO or port I/O. Then look up one specific device in its kernel driver source and find the corresponding ioremap or request_mem_region call. Trace the full path from hardware address to readl call.

  5. 05
    hard

    Write a program (userspace, using /dev/mem or a small kernel module) that detects whether your system has an IOMMU active. Compare the dma_addr_t returned by dma_map_single with virt_to_phys of the same buffer. If they differ, the IOMMU is doing translation. Also boot with iommu=pt (passthrough) and observe the change.

Resources

📕Linux Device Drivers, 3rd Edition (LDD3)
bookfree

Chapters 9 (Communicating with Hardware) and 15 (Memory Mapping and DMA) are the canonical reference for ioremap and DMA. Free to read online.

📄Linux Kernel Documentation: DMA-API.rst
docsfree

The authoritative API reference for the entire DMA mapping API, including streaming vs coherent, masks, and error handling.

📄Linux Kernel Documentation: DMA-API-HOWTO.rst
docsfree

Step-by-step guide with rationale for when to use each DMA API variant. Essential companion to DMA-API.rst.

📜Memory Barriers: a Hardware View for Software Hackers (Paul McKenney)
paperfree

The definitive deep dive into why memory barriers exist, from CPU microarchitecture to the Linux barrier API. Dense but worth every page.

🎓Bootlin (Free Electrons) Embedded Linux Kernel and Driver Development Training Materials
coursefree

The slides and labs on MMIO and DMA are among the best practical references available. The labs use real ARM hardware and QEMU.

✎LWN.net: A DMA mapping update
articlefree

Explains the evolution of the Linux DMA API and why the older pci_map_* functions were replaced by the generic dma_map_* family.

📕Professional Linux Kernel Architecture
bookpaidWolfgang Mauerer, Wiley/Wrox

Chapter on memory management covers the relationship between virtual, physical, and bus addresses with exceptional depth.

📄ARM Architecture Reference Manual (ARM ARM)
docsfree

For understanding device memory types (MT_DEVICE_*), DMB/DSB/ISB barrier instructions, and why MMIO must use non-cacheable attributes on ARM.

📄Intel 64 and IA-32 Architectures Software Developer's Manual Vol 3A
docsfree

Chapters on PAT (Page Attribute Table), MTRR, and memory ordering (Section 8.2-8.3) explain why x86 MMIO works the way it does.

✎kernelnewbies.org: Device drivers
articlefree

Practical entry point with pointers to working minimal driver examples suitable for getting hands-on quickly.

Checkpoints — you can now…

  • ✓You can explain to a colleague why ioremap must return non-cacheable mappings, and what specific failure mode would occur if the mapping were cacheable.
  • ✓You can write a correct platform driver probe function using devm_ioremap_resource, and explain why devm_ variants are preferred over manual ioremap/release_mem_region.
  • ✓You can identify whether a given pair of writel calls requires a wmb() between them, and explain why the answer differs between x86 (TSO) and ARM (weakly ordered).
  • ✓You can distinguish between a virtual address, a physical address, and a DMA bus address, and write code that correctly obtains each for a given buffer.
  • ✓You can choose between dma_alloc_coherent and dma_map_single for a given use case, implement both correctly including error checking and cleanup, and explain the cache coherency tradeoff between them.
  • ✓You can explain what an IOMMU does, why it means virt_to_phys() is wrong for DMA, and how to test your driver's DMA correctness by booting with IOMMU enabled in full translation mode.
  • ✓You can read /proc/iomem and /proc/ioports on a running Linux system and trace a specific entry back to the kernel driver that owns it and the ioremap call that claimed it.