Linux MMIO And Ioremap Guide-Free Linux Device Drivers Tutorial

Linux MMIO And Ioremap Guide

Free Linux Kernel Development Course — Chapter 11, Lecture 14

Keywords covered in this lecture

linux mmio ioremap api
free linux kernel development course
free linux device drivers course
free embedded linux course
free embedded systems course
request_mem_region
devm_ioremap_resource
ioread32 iowrite32

If you are following our free Linux kernel development course, you already know that devices can be accessed through Port I/O (PIO) using inb()/outb(). In this lecture we cover the second, far more common way a Linux driver talks to hardware — Memory Mapped I/O (MMIO) — and the linux mmio ioremap api that makes it safe to use. This is a core topic in any free embedded linux course because almost every modern SoC peripheral (UART, I2C controller, GPIO block, DMA engine) is programmed through MMIO registers.

What You Will Learn

  • How Memory Mapped I/O places device registers inside the normal address space
  • How to reserve an I/O memory region with request_mem_region()
  • How to use the linux mmio ioremap apiioremap() and iounmap()
  • Why modern drivers prefer devm_ioremap_resource() over manual mapping
  • How to read and write registers safely with ioread32()/iowrite32()
  • What the __iomem annotation is, and why Sparse checks it
  • A complete, original MMIO demo driver you can build and load today

Prerequisites

Before this lecture, you should be comfortable with the earlier lectures in this free linux device drivers course: basic kernel module structure, platform devices, and Port I/O (PIO) covered in the previous lecture. A Linux VM or single-board computer with kernel headers installed is enough to run every example below.

Memory Mapped I/O In Simple Words

With Port I/O, device registers live in a completely separate address space that only inb()/outb() can reach. MMIO takes a different approach: the hardware designer places device registers at specific physical addresses inside the same address space normally used for RAM. From the CPU’s point of view, talking to a device becomes exactly like reading or writing a memory location — no special instructions needed.

Physical Address Space Layout
RAM (normal system memory)
Reserved MMIO Region (UART / I2C / GPIO registers)
More RAM / other devices

Because device registers sit at ordinary-looking physical addresses, the kernel must be careful: it cannot let two drivers map the same region, and it cannot let a driver treat that memory exactly like RAM (caching, reordering, and speculative reads can all break real hardware). That is exactly the problem the linux mmio ioremap api solves.

Step 1: Reserving The Region

Just like PIO ports, an MMIO physical range should be reserved before use. This is a courtesy check, not a hardware lock — it stops two drivers from silently fighting over the same registers.

struct resource *request_mem_region(unsigned long start,
                                     unsigned long len,
                                     const char *name);

void release_mem_region(unsigned long start, unsigned long len);

Every region currently reserved this way is visible under /proc/iomem:

$ cat /proc/iomem | grep -i uart
3f215040-3f215fff : uart-pl011

Step 2: Mapping With The Linux MMIO Ioremap API

Reserving a region only marks it as “in use” — it does not make the memory accessible to the CPU yet. Before you can read or write a register, the physical range must be mapped into the kernel’s virtual address space using ioremap():

void __iomem *ioremap(phys_addr_t offset, unsigned long size);
void iounmap(void __iomem *addr);

ioremap() builds page table entries for the region (much like vmalloc() does) and marks the mapping as non-cacheable, so the CPU always talks directly to the device instead of serving stale data from cache. It returns a special __iomem pointer — more on that shortly.

The Modern Way: devm_ioremap_resource()

Manually pairing request_mem_region() with ioremap(), and remembering to undo both in every error path, is exactly the kind of bookkeeping current kernels try to eliminate. Almost every platform and I2C/SPI driver merged today instead uses the managed, resource-based helper:

void __iomem *devm_ioremap_resource(struct device *dev,
                                     const struct resource *res);

void __iomem *devm_platform_ioremap_resource(struct platform_device *pdev,
                                              unsigned int index);

devm_ioremap_resource() validates the resource, calls devm_request_mem_region() internally, then maps it — and the mapping is automatically released when the driver detaches. This single call replaces the old two-step reserve-then-map pattern and is the pattern you should reach for in new drivers.

API Reserves region? Maps memory? Needs manual cleanup?
ioremap() alone No Yes Yes — call iounmap()
request_mem_region() + ioremap() Yes Yes Yes — two calls to undo
devm_ioremap_resource() Yes (via devres) Yes No — auto-released on detach
devm_platform_ioremap_resource() Yes (via devres) Yes No — also fetches the resource for you

Step 3: Reading And Writing Registers

Once a region is mapped, you must never directly dereference the returned pointer. The kernel provides dedicated accessor functions instead:

unsigned int ioread8(const volatile void __iomem *addr);
unsigned int ioread16(const volatile void __iomem *addr);
unsigned int ioread32(const volatile void __iomem *addr);

void iowrite8(u8 value,  volatile void __iomem *addr);
void iowrite16(u16 value, volatile void __iomem *addr);
void iowrite32(u32 value, volatile void __iomem *addr);

Modernization Note: ioread/iowrite vs readl/writel

Older material sometimes claims that readl()/writel() are “deprecated and insecure” in favour of ioread32()/iowrite32(). That claim does not hold up against current kernel documentation and mailing-list history. Both families are actively maintained:

  • readl()/writel() compile to the fewest instructions and are the right default when your driver is purely MMIO-based.
  • ioread32()/iowrite32() add a small runtime check so the same code also works with I/O-space tokens returned by pci_iomap() or ioport_map() — useful for drivers that must support both MMIO and PIO hardware variants from one code path.
  • Neither family is “insecure”; choose based on portability needs, not on a myth.

The __iomem Cookie And Sparse

Every pointer returned by ioremap() is tagged with __iomem. This is a compile-time annotation understood by Sparse, the kernel’s static semantic checker, defined roughly as:

#define __iomem __attribute__((noderef, address_space(2)))

__iomem stops you from accidentally dereferencing a device pointer directly (*addr = value;) instead of going through ioread*()/iowrite*(). Direct dereference can skip barriers and byte-order handling that the accessor functions guarantee. To enable the check when building an out-of-tree module:

sudo apt-get install sparse
make C=1 M=$(pwd) modules

If you ever dereference a __iomem pointer directly, Sparse will warn:

warning: incorrect type in initializer (different address spaces)
warning: dereference of noderef expression

Original Demo Driver: ep_mmio_demo

The following original driver, written for this free linux kernel development course, allocates a fake MMIO-style memory buffer with kzalloc() to simulate a device register block, maps it with ioremap()-style access helpers, and demonstrates a safe read-modify-write cycle. On real hardware you would replace the simulated physical address with the one from your platform’s Device Tree resource.

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

#define EP_MMIO_SIZE   0x100
#define EP_REG_STATUS  0x00
#define EP_REG_CONTROL 0x04

static void __iomem *ep_base;
static struct resource *ep_res;
static phys_addr_t ep_phys_addr;

static int __init ep_mmio_demo_init(void)
{
    void *sim_mem;

    /* Simulate a device register block using normal RAM */
    sim_mem = kzalloc(EP_MMIO_SIZE, GFP_KERNEL);
    if (!sim_mem)
        return -ENOMEM;

    ep_phys_addr = virt_to_phys(sim_mem);

    ep_res = request_mem_region(ep_phys_addr, EP_MMIO_SIZE, "ep_mmio_demo");
    if (!ep_res) {
        pr_err("ep_mmio_demo: region already in use\n");
        kfree(sim_mem);
        return -EBUSY;
    }

    ep_base = ioremap(ep_phys_addr, EP_MMIO_SIZE);
    if (!ep_base) {
        pr_err("ep_mmio_demo: ioremap failed\n");
        release_mem_region(ep_phys_addr, EP_MMIO_SIZE);
        kfree(sim_mem);
        return -ENOMEM;
    }

    iowrite32(0x1, ep_base + EP_REG_CONTROL);
    pr_info("ep_mmio_demo: control = 0x%x\n",
            ioread32(ep_base + EP_REG_CONTROL));

    pr_info("ep_mmio_demo: loaded, mapped at phys 0x%llx\n",
            (unsigned long long)ep_phys_addr);
    return 0;
}

static void __exit ep_mmio_demo_exit(void)
{
    iounmap(ep_base);
    release_mem_region(ep_phys_addr, EP_MMIO_SIZE);
    pr_info("ep_mmio_demo: unloaded\n");
}

module_init(ep_mmio_demo_init);
module_exit(ep_mmio_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Original MMIO ioremap demo driver");

Build And Run Steps

make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
sudo insmod ep_mmio_demo.ko
dmesg | tail -5
sudo rmmod ep_mmio_demo

Expected dmesg Output

[ 1234.567890] ep_mmio_demo: control = 0x1
[ 1234.567895] ep_mmio_demo: loaded, mapped at phys 0x1a2b3c00
[ 1240.112233] ep_mmio_demo: unloaded

Real-World Use Cases

  • UART, I2C, SPI, and GPIO controller drivers on ARM/ARM64 SoCs
  • PCIe device BAR access via pci_iomap() built on the same accessor family
  • DMA controller and interrupt controller register blocks
  • Framebuffer and display controller drivers

Common Mistakes

Mistake Why it breaks
Dereferencing a __iomem pointer directly Skips ordering/barrier guarantees; Sparse will flag it
Forgetting iounmap()/mismatched devm_ usage Leaks kernel virtual address space
Calling ioremap() from interrupt context It can sleep while building page tables
Reusing a region without request_mem_region() Silent conflict with another driver

Best Practices

  • Prefer devm_platform_ioremap_resource() in new platform drivers
  • Always check the return value of ioremap() for NULL / IS_ERR()
  • Use ioread*()/iowrite*() when the same driver may run over MMIO or PCI I/O space; use readl()/writel() for pure MMIO drivers
  • Keep __iomem annotations intact and build with C=1 occasionally to catch mistakes

Performance Considerations

MMIO accesses are far slower than RAM accesses because each read or write is forwarded to real hardware, often across a slower peripheral bus. Batch related register updates where the hardware allows it, and use _relaxed accessor variants only when you fully understand the ordering guarantees you are giving up.

Security Considerations

An unreserved or unchecked MMIO mapping can let a buggy or malicious module remap and corrupt registers owned by another driver, or expose one device’s memory-mapped registers to code that has no business touching them. Always reserve the region first, keep mappings as narrow as the resource requires, and release them promptly.

Summary / Key Takeaways

  • MMIO places device registers inside the normal physical address space
  • request_mem_region() reserves; the linux mmio ioremap api (ioremap()/iounmap()) maps
  • devm_ioremap_resource() is the preferred modern one-call approach
  • Always access mapped memory through ioread*()/iowrite*() or readl()/writel(), never by direct dereference
  • __iomem plus Sparse (C=1) catches unsafe direct access at compile time

Conclusion

Memory Mapped I/O is how the overwhelming majority of Linux device drivers talk to hardware today. Understanding the full chain — reserve with request_mem_region(), map with the linux mmio ioremap api, and access only through the dedicated accessor functions — is essential groundwork for every later chapter in this free linux device drivers course. In the next lecture we return to kmap_local_page() and high-memory mapping to close out the memory management chapter.

FAQ

What is the difference between PIO and MMIO?

PIO uses dedicated CPU instructions (inb/outb) and a separate I/O address space. MMIO places device registers inside the normal physical address space so ordinary load/store style accessors can reach them.

Do I always need request_mem_region() before ioremap()?

It is strongly recommended. It is a cooperative check that prevents two drivers from mapping the same physical region, and it is what shows up under /proc/iomem.

Why can’t I just dereference the pointer from ioremap()?

The pointer is tagged __iomem and may not behave like normal memory on every architecture. Always use ioread*()/iowrite*() or readl()/writel() instead.

Is devm_ioremap_resource() mandatory in new drivers?

It is not mandatory, but it is the current best practice for platform, I2C, and SPI drivers because it removes manual cleanup and reduces error-path bugs.

What does the __iomem cookie actually do?

It is a Sparse address-space annotation. It has no runtime effect by itself, but when the kernel is built with C=1, Sparse uses it to catch invalid direct dereferences at compile time.

Are readl()/writel() deprecated in favour of ioread32()/iowrite32()?

No. Both are current and maintained. readl()/writel() are typically faster for pure MMIO drivers; ioread32()/iowrite32() add portability for code that may also run over PCI I/O space.

Can ioremap() be called from an interrupt handler?

No. Building page tables can sleep, so ioremap() must only be called from process context, typically during driver probe.

Where can I see currently mapped MMIO regions on a running system?

cat /proc/iomem lists every region reserved through request_mem_region() or its managed equivalents, along with the owning driver name.

Continue This Free Linux Kernel Development Course

Next up: high-memory mapping with kmap_local_page() and kunmap_local().

 

Leave a Reply

Your email address will not be published. Required fields are marked *