Memory-Mapped I/O (MMIO) in Linux Device Drivers (Kernel 6.x)-Free Linux Device Driver Course Online

Free Linux Kernel Programming Course
Lecture 8: Memory-Mapped I/O (MMIO) in Linux Device Drivers (Kernel 6.x)
100% Free
Hands-On Code
ARM & x86 Ready
⬅ Previous Lecture: Next Lecture: ➡

This lecture is the practical, hands-on follow-up in our free linux device drivers course. In the previous lecture we learned why a driver cannot touch hardware memory directly. Here we put that theory to work and actually map a peripheral’s registers into kernel space using memory-mapped I/O (MMIO), the dominant access style on almost every modern SoC. This lecture fits naturally into any free embedded systems course or free linux kernel development course study plan.

What You Will Learn

Lecture Roadmap
✅ How ioremap() creates a usable kernel virtual mapping for hardware memory
✅ The correct accessor functions for reading and writing registers safely
✅ A complete, modern devm_-based driver skeleton using MMIO
✅ Common pitfalls that corrupt hardware state or crash the kernel

Prerequisites

Please complete the previous lecture on I/O memory access basics first, since this lecture builds directly on the request-use-release pattern and the MMIO vs PMIO distinction explained there.

Step 1: Reserve the Memory Region

Before mapping anything, a well-behaved driver reserves the physical address range so no other driver can claim it. On a modern kernel 6.x driver this is almost always done with the managed helper inside your probe function:

static int mydevice_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *regs;

    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);

    /* regs now points to a safe kernel virtual mapping */
    return 0;
}

Notice that devm_ioremap_resource() does two jobs in one call: it requests the memory region and maps it, and it automatically tears both down when the driver is removed. This single-call pattern is why almost all new platform drivers written for modern kernels use it instead of separately calling request_mem_region() followed by ioremap().

Understanding the __iomem Pointer

You will notice the returned pointer is typed void __iomem *, not a plain void *. This is not a small detail; the __iomem annotation tells the kernel’s static checker (sparse) that this pointer refers to device memory, not regular RAM. That means you are not allowed to dereference it directly with *ptr the way you would a normal C pointer. Instead, you must go through dedicated accessor functions.

Why Accessor Functions Instead of a Raw Pointer?
Ordering Guarantees
Accessors insert the correct memory barriers so register writes happen in the exact order your hardware expects
Portability
The same driver source compiles correctly on architectures with very different memory models
Compiler Safety
Prevents the compiler from optimizing away or reordering what looks like a “redundant” memory access

The Core MMIO Accessor Functions

Register Width Read Function Write Function
8-bit readb(addr) writeb(val, addr)
16-bit readw(addr) writew(val, addr)
32-bit readl(addr) writel(val, addr)
64-bit readq(addr) writeq(val, addr)

Pick the accessor width that matches what your hardware’s datasheet specifies for that register. Using the wrong width is a common source of subtle bugs; writing a 32-bit accessor to a register documented as 16-bit can silently corrupt an adjacent register.

A Complete Minimal MMIO Register Toggle Example

Below is a small, original example showing a typical pattern: read a control register, flip one bit, and write it back. This mirrors what you would do to enable a peripheral clock or turn on a GPIO bank on many real SoCs, without tying it to any specific chip.

#define CTRL_REG_OFFSET   0x04
#define ENABLE_BIT        BIT(0)

static void mydevice_enable(void __iomem *regs)
{
    u32 val;

    val = readl(regs + CTRL_REG_OFFSET);
    val |= ENABLE_BIT;
    writel(val, regs + CTRL_REG_OFFSET);
}

static void mydevice_disable(void __iomem *regs)
{
    u32 val;

    val = readl(regs + CTRL_REG_OFFSET);
    val &= ~ENABLE_BIT;
    writel(val, regs + CTRL_REG_OFFSET);
}

This read-modify-write pattern is one of the most common operations you will write in real driver code, so it is worth typing it out a few times yourself until it feels natural.

Real-World Use Case: A Platform Driver Skeleton

Putting the pieces together, here is how a minimal but realistic platform driver structure looks on kernel 6.x using everything covered above:

struct mydevice_priv {
    void __iomem *regs;
};

static int mydevice_probe(struct platform_device *pdev)
{
    struct mydevice_priv *priv;
    struct resource *res;

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

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

    platform_set_drvdata(pdev, priv);
    mydevice_enable(priv->regs);

    return 0;
}

static void mydevice_remove(struct platform_device *pdev)
{
    struct mydevice_priv *priv = platform_get_drvdata(pdev);

    mydevice_disable(priv->regs);
}

Because everything was allocated and mapped through devm_ helpers, there is no manual cleanup of the memory region or the mapping in the remove function; the kernel handles that automatically once remove() returns.

Performance Considerations

MMIO reads and writes are far slower than accessing regular RAM because each one may have to travel across a peripheral bus and wait for the hardware to respond. Avoid polling a register in a tight loop without any delay; use proper timeout helpers instead. Where your hardware allows it, batch related register writes together instead of interleaving them with unrelated logic, which keeps your code easier to reason about and closer to the ordering your datasheet expects.

Security Considerations

Never expose raw MMIO regions to user space unless absolutely necessary, and if you do, restrict it tightly through the device model and appropriate permission checks. Validate any register offsets that come from user-controlled input before adding them to your mapped base pointer, since an unchecked offset can let user space read or write memory well outside the intended peripheral’s range.

Common Mistakes and Troubleshooting

Mistake Fix
Dereferencing the __iomem pointer directly Always use readl/writel style accessors instead of *ptr
Ignoring the return value of devm_ioremap_resource() Always check with IS_ERR() before using the pointer
Using the wrong accessor width for a register Cross-check the exact bit width against your chip’s datasheet

Key Takeaways

devm_ioremap_resource maps and manages cleanup Always use readl/writel accessors __iomem marks device memory pointers Match accessor width to your datasheet

Frequently Asked Questions

What does the __iomem annotation actually do?

It marks a pointer as referring to device memory rather than normal RAM so the kernel’s sparse checker can flag any direct dereference as a bug, and so developers know to use readl/writel style accessors instead.

Why use devm_ioremap_resource instead of plain ioremap?

devm_ioremap_resource both reserves the memory region and maps it in a single call, and automatically releases both when the driver is removed, which eliminates a common class of cleanup bugs in classic driver code.

Can I just cast the __iomem pointer to a normal pointer and read it?

You technically can on some architectures, but it is unsafe and unportable because it skips the ordering guarantees and compiler barriers that the accessor functions provide, so it should be avoided in real driver code.

Do I need different code for ARM versus x86 when using MMIO?

No. The accessor functions like readl and writel are implemented per architecture inside the kernel, so the same driver source works correctly across architectures without any changes.

What is the read-modify-write pattern used for?

It is used whenever you need to change a single bit or field in a register without disturbing the other bits, which is extremely common for control and status registers on real hardware.

Continue This Free Linux Kernel Programming Course

Next up: Port-Mapped I/O (PMIO) for legacy x86 hardware, with a complete driver walkthrough.

⬅ Previous Lecture: Next Lecture: ➡

1 Comment

Leave a Reply

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