What are Memory-Mapped I/O (MMIO) in Linux Device Drivers-Free Linux Device Drivers Course

Memory-Mapped I/O (MMIO) in Linux Device Drivers
Free Linux Kernel Programming Course — Free Linux Device Drivers Course | EmbeddedPathashala

← Previous Lecture  |  Next Lecture →

Every device driver that talks to real hardware eventually has to touch a register. On modern SoCs and PCI/PCIe devices, those registers live in a chunk of physical address space that the CPU cannot use directly — the kernel has to map it into its own virtual address space first. That process is called Memory-Mapped I/O (MMIO), and it’s one of the most important building blocks in Linux device driver development. In this free Linux kernel programming course lecture, we’ll build a clear, up-to-date, kernel 6.x-accurate mental model of how MMIO works, which managed APIs to use, and how to verify your mapping actually worked.

What You Will Learn
Physical vs virtual I/O addressing ioremap() and its variants devm_ioremap_resource() platform_get_resource() pattern readl()/writel() accessors /proc/iomem verification Common MMIO mistakes Security & performance notes
Prerequisites

Before this lecture, you should be comfortable with:

  • Basic Linux kernel module structure (module_init/module_exit)
  • The Linux Device Model basics — platform devices and the probe() routine
  • The difference between physical and virtual memory at a conceptual level

If any of these are new to you, check the earlier lectures in this free Linux device drivers course first (linked at the top and bottom of this page).

What Is Memory-Mapped I/O?

Most peripherals on ARM, x86, and RISC-V based Linux systems expose their control and status registers as a block of physical address space, not as RAM. The CPU itself doesn’t know or care that a given physical address belongs to a UART, a GPIO controller, or an audio codec — from the CPU’s point of view, it’s just an address it can read from or write to. This is what makes MMIO so elegant: the same load/store instructions used for ordinary memory access also work for talking to hardware, as long as the kernel has first mapped that physical region into a usable virtual address.

On a running kernel, this physical-to-virtual mapping doesn’t happen automatically for every device. A driver has to explicitly request the region and map it during its probe() function, using APIs from the ioremap family.

MMIO Address Flow
Device Tree / ACPI
Declares physical register range
→
platform_get_resource()
Driver fetches the physical range
→
devm_ioremap_resource()
Kernel maps it into virtual address space
→
readl()/writel()
Driver safely accesses registers

The ioremap() Family of APIs

ioremap() is the classic, low-level API for mapping a physical I/O range into kernel virtual address space. Its problem is lifecycle management: whatever calls ioremap() must remember to call iounmap() on every exit path, including error paths inside probe(). Miss one, and you have a resource leak that only shows up when the module is repeatedly loaded and unloaded.

Modern kernels solve this with the devm_* (device-managed) family. A devm-based mapping is tied to the lifetime of the struct device; the kernel automatically undoes it when the device is detached or the driver’s probe fails, so you no longer need explicit cleanup code for it. devm_ioremap() is the managed counterpart of the classic call, but you still have to separately request the memory region and validate the resource pointer yourself.

devm_ioremap_resource(): The Recommended Managed API

For nearly all platform drivers today, the API you actually want is devm_ioremap_resource(). In one call it:

  • Validates that the struct resource passed in is sane
  • Requests the memory region so no other driver can claim it
  • Performs the ioremap and ties the mapping to the device’s lifetime

Here is a minimal, kernel-6.x-style example of the typical pattern used inside a platform driver’s probe() function:

static int mypdev_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_priv *priv;
    struct resource *res;

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->regs = devm_ioremap_resource(dev, res);
    if (IS_ERR(priv->regs))
        return PTR_ERR(priv->regs);

    platform_set_drvdata(pdev, priv);
    dev_info(dev, "registers mapped successfully\n");
    return 0;
}

Notice there is no matching iounmap() anywhere — the devm core handles that automatically. This pattern is the standard approach used across the vast majority of in-tree platform drivers today.

Reading Registers Safely: readl() and writel()

A common beginner mistake is to treat the mapped pointer like ordinary RAM and dereference it directly with *ptr. This can be unsafe: the compiler is free to reorder, cache, or combine such accesses, and on many architectures that breaks strict register-access ordering that hardware depends on. Instead, always use the portable accessor functions:

u32 val = readl(priv->regs + REG_STATUS_OFFSET);
writel(0x1, priv->regs + REG_CONTROL_OFFSET);
Accessor Width Typical Use
readb() / writeb() 8-bit Byte-wide control registers
readw() / writew() 16-bit Legacy peripheral registers
readl() / writel() 32-bit Most SoC peripheral registers
readq() / writeq() 64-bit PCIe / high-speed controllers

Verifying the Mapping via /proc/iomem

Once request_mem_region() succeeds internally (which happens as part of devm_ioremap_resource()), a corresponding entry appears under the read-only pseudo-file /proc/iomem, listing the physical address range and the owning driver’s name. This is the fastest way to confirm that your driver actually claimed the region it expected, and that no other driver is fighting over the same address range:

$ sudo cat /proc/iomem | grep mypdev
3f200000-3f2000ff : mypdev

If your entry never shows up, the mapping call failed silently somewhere — check your device tree node’s reg property and your driver’s error path.

Common Mistakes and Troubleshooting

  • Mixing managed and unmanaged calls: pairing ioremap() with a manual devm cleanup (or vice versa) leads to double-frees or leaks.
  • Dereferencing the pointer directly: always use readl()/writel(), never raw pointer access.
  • Ignoring the return value: devm_ioremap_resource() returns an ERR_PTR on failure — always check with IS_ERR() before use.
  • Forgetting region conflicts: if another driver already claimed the same physical range, your request will fail; /proc/iomem tells you who owns it.

Best Practices for MMIO in Modern Drivers

  • Prefer devm_ioremap_resource() over manual ioremap() + iounmap() unless you have a specific reason to manage the lifetime yourself.
  • Always validate resources with IS_ERR()/PTR_ERR().
  • Use the correctly-sized accessor (readl/readw/readb) matching your hardware’s register width.
  • Cross-check your mapping against /proc/iomem during bring-up and debugging.

Performance and Security Considerations

MMIO accesses are typically uncached by default on most architectures, which is correct for register access but means each readl()/writel() call has real bus latency — avoid tight polling loops without a timeout, and prefer interrupt-driven designs where possible. From a security standpoint, MMIO regions should never be exposed to userspace unless absolutely necessary (for example via a carefully audited mmap() in a char driver); an unprivileged process with direct register access can crash or damage hardware, and on production kernels /proc/iomem intentionally hides real addresses from non-root users for exactly this reason.

Real-World Use Cases

This exact pattern — platform_get_resource() followed by devm_ioremap_resource() — is used across the kernel tree for GPIO controllers, UART drivers, display/video mixers, DMA engines, and countless SoC peripheral drivers. If you look at almost any ARM SoC platform driver in the mainline kernel today, you’ll find this same skeleton at the top of its probe() function.

Summary / Key Takeaways

  • MMIO lets the CPU talk to hardware registers using ordinary load/store instructions, after the kernel maps the physical region into virtual address space.
  • devm_ioremap_resource() is the modern, recommended, all-in-one managed API for this mapping in platform drivers.
  • Always access mapped registers through readl()/writel() style accessors, never raw pointers.
  • /proc/iomem is your quickest tool to confirm a mapping succeeded.

Conclusion

Memory-Mapped I/O is the foundation that almost every Linux device driver is built on top of. Once you’re comfortable with the physical-to-virtual mapping flow, the devm_ioremap_resource() pattern, and the correct register accessor functions, you have the core skill needed to bring up register access for practically any peripheral on any SoC. This is a cornerstone lecture in our free Linux kernel programming course and free Linux device drivers course — make sure you’re comfortable writing this probe pattern from memory before moving to the next lecture.

Frequently Asked Questions (FAQ)

1. What is the difference between ioremap() and devm_ioremap_resource()?
ioremap() only maps memory and requires manual iounmap() on cleanup. devm_ioremap_resource() additionally validates and requests the resource, and automatically unmaps it when the device is removed.

2. Can I read MMIO registers using a plain pointer dereference?
It’s technically possible but discouraged and unsafe on many architectures; always use readl()/writel() and matching width variants.

3. Why doesn’t my device show up in /proc/iomem?
Either the mapping call failed, the region conflicts with another driver, or you’re viewing it as a non-root user (addresses show as zero for security).

4. Is devm_ioremap_resource() safe to use in interrupt context?
No, it should only be called from process context, typically inside probe().

5. What happens if platform_get_resource() returns NULL?
Passing a NULL resource into devm_ioremap_resource() will be caught by its internal validation, and it will return an error pointer — always check with IS_ERR().

6. Do I still need iounmap() with devm_ioremap_resource()?
No. The devm core automatically tears down the mapping when the device detaches or probe fails.

7. Are MMIO registers cached like normal RAM?
No, MMIO mappings are uncached by default so that every access reaches the hardware directly.

Continue the Free Linux Kernel Programming Course

More free lectures on Linux device drivers, kernel internals, and embedded systems.

← Previous Lecture Next Lecture →

2 Comments

Leave a Reply

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