What is Linux Kernel MMIO I/O APIs: ioread, iowrite & Bulk Transfers Explained-Linux Device Driver Training

← Previous Lecture    Next Lecture →

Linux Kernel MMIO I/O APIs: ioread, iowrite & Bulk Transfers Explained
A beginner-friendly, updated-for-6.x-kernels guide to reading and writing device registers safely from a Linux device driver
Free Linux Kernel Programming Course
Free Linux Device Drivers Course
Free Embedded Systems Course

If you are building a Linux device driver for a memory-mapped peripheral, sooner or later you have to actually talk to the hardware — read a status register, flip a control bit, or push a block of bytes into a FIFO. This is exactly where the Linux kernel MMIO I/O APIs come in. In this lecture of our free Linux kernel programming course, we break down every commonly used MMIO I/O API family — single-register access, repeating (bulk) I/O, and memory-style helpers like memcpy_toio() — with simple explanations and driver-style code you can adapt directly into your own char or platform driver.

This tutorial is part of our free Linux device drivers course and free embedded systems course at EmbeddedPathashala, and every example here targets modern 6.x kernels rather than legacy conventions, so you won’t pick up outdated habits.

What You Will Learn

  • Single-width MMIO reads/writes (8/16/32/64-bit)
  • Repeating (bulk) MMIO I/O
  • memset_io / memcpy_fromio / memcpy_toio
  • Legacy readb/writeb style helpers
  • Full MMIO driver workflow
  • Common mistakes & best practices

Prerequisites

Before this lecture, you should be comfortable with:

  • Basic C programming
  • Writing a minimal Linux kernel module
  • The concept of memory-mapped I/O (MMIO) and ioremap()
  • Platform / char driver basics

If any of these feel unfamiliar, check the earlier lectures in this free Linux kernel programming course before continuing.

Key terms covered:
ioread32 / iowrite32 ioread8_rep / iowrite8_rep memcpy_toio memcpy_fromio memset_io devm_ioremap __iomem

Why MMIO I/O APIs Exist

Once a peripheral’s register space has been mapped into kernel virtual address space (typically with devm_ioremap_resource()), you get back a pointer marked with the __iomem annotation. You might be tempted to simply dereference that pointer like normal memory — but that’s risky. On many CPU architectures, a plain pointer dereference can be reordered by the compiler, cached incorrectly, or split into multiple bus transactions, none of which is acceptable for hardware registers that trigger side effects on every access.

The Linux kernel MMIO I/O APIs exist precisely to remove that risk. They wrap each register access in an architecture-specific implementation that guarantees the correct bus width, correct ordering, and no unwanted compiler optimizations. Using them instead of raw pointer access is considered mandatory in any driver meant to be portable and correct.

Single-Width MMIO Reads and Writes

The most frequently used MMIO I/O APIs operate on one register at a time, in a fixed bit-width. The kernel exposes four widths, declared in <linux/io.h>:

Width Read API Write API Typical Use
8-bit ioread8() iowrite8() Status/control byte registers
16-bit ioread16() iowrite16() Legacy peripheral counters
32-bit ioread32() iowrite32() Most SoC peripheral registers
64-bit ioread64() iowrite64() 64-bit counters/DMA descriptors on 64-bit kernels

Here is a small, self-contained example showing an original driver-style probe function that reads a device ID register and then enables the device by writing a control register. This is written fresh for this tutorial and targets a modern platform driver layout:

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

#define REG_DEVICE_ID   0x00
#define REG_CONTROL     0x04
#define CTRL_ENABLE_BIT BIT(0)

struct mydev_priv {
    void __iomem *base;
};

static int mydev_probe(struct platform_device *pdev)
{
    struct mydev_priv *priv;
    u32 dev_id;

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

    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    dev_id = ioread32(priv->base + REG_DEVICE_ID);
    dev_info(&pdev->dev, "device id = 0x%08x\n", dev_id);

    iowrite32(CTRL_ENABLE_BIT, priv->base + REG_CONTROL);

    platform_set_drvdata(pdev, priv);
    return 0;
}

Note: notice the pattern base + REG_OFFSET. Hardware designers lay out register banks sequentially so drivers can index into them just like an array — always confirm exact offsets against your peripheral’s datasheet, never guess.

Endianness-Aware Variants

Some peripherals (especially network and legacy PCI hardware) store register values in big-endian byte order regardless of host CPU endianness. For these cases the kernel provides big-endian variants such as ioread32be() and iowrite32be(), which perform the same MMIO access but handle the byte-swap internally. Use these only when your datasheet explicitly states the register bank is big-endian — using the wrong variant silently corrupts every value you read or write.

Repeating I/O for Bulk MMIO Transfers

Single-register APIs are fine for control and status bits, but what about moving a block of data — say, draining 64 bytes out of a hardware FIFO? You could loop over ioread8() manually, but the kernel already provides a tighter, more efficient primitive: the repeating I/O family, ioread[8|16|32|64]_rep() and iowrite[8|16|32|64]_rep(). Internally these use an optimized loop for the target architecture, so they outperform a hand-written C loop calling the single-width API repeatedly.

#define REG_FIFO_DATA 0x40

static void mydev_drain_fifo(struct mydev_priv *priv, u8 *dest, unsigned int len)
{
    ioread8_rep(priv->base + REG_FIFO_DATA, dest, len);
}

static void mydev_fill_fifo(struct mydev_priv *priv, const u8 *src, unsigned int len)
{
    iowrite8_rep(priv->base + REG_FIFO_DATA, src, len);
}

Notice that the destination address inside the repeating call stays fixed — every byte lands on or comes from the same FIFO port register, which is exactly the behavior a hardware FIFO expects. This is different from copying a range of distinct registers, which is what the next section covers.

memset_io, memcpy_fromio and memcpy_toio

When you need to clear or copy a genuine range of MMIO memory — such as a video framebuffer or a block of shared window memory — you must not use the standard memset()/memcpy() functions on an __iomem pointer. Instead the kernel provides dedicated MMIO-safe helpers:

Helper Purpose
memset_io() Fill an MMIO range with a fixed byte value
memcpy_fromio() Copy from MMIO region into normal kernel memory
memcpy_toio() Copy from kernel memory into an MMIO region
#define FB_OFFSET 0x1000
#define FB_SIZE   4096

static void mydev_clear_framebuffer(struct mydev_priv *priv)
{
    memset_io(priv->base + FB_OFFSET, 0x00, FB_SIZE);
}

static void mydev_load_framebuffer(struct mydev_priv *priv, const void *src)
{
    memcpy_toio(priv->base + FB_OFFSET, src, FB_SIZE);
}

Legacy readb/writeb Style Helpers

Older drivers in the kernel tree still use an earlier generation of MMIO I/O APIs: readb(), readw(), readl(), readq() and their write*() counterparts, where the suffix indicates byte (b), word (w), long (l), or quad-word (q) width. Functionally they behave the same as the modern ioread/iowrite family. New drivers targeting current kernels should prefer ioread32()/iowrite32() and friends, since that is the API surface actively maintained and recommended going forward — but you will still run into the legacy names when reading existing driver source, so it’s worth recognizing them.

The Complete MMIO Driver Workflow

MMIO Access Lifecycle in a Linux Device Driver
1. Request memory region
devm_request_mem_region()
↓
2. Map into kernel VAS
devm_ioremap_resource()
↓
3. Perform I/O
ioread32() / iowrite32() / _rep() / memcpy_io()
↓
4. Unmap (if not devm-managed)
iounmap()
↓
5. Release memory region
release_mem_region()

With the managed devm_* variants used throughout this tutorial, steps 4 and 5 are handled automatically by the kernel when the device is removed, which is why modern drivers rarely call iounmap() or release_mem_region() explicitly.

Real-World Use Case

A typical UART or SPI controller driver reads a status register with ioread32() to check if the transmit FIFO has space, then uses iowrite8_rep() to push a burst of bytes into the FIFO data register in one efficient call instead of looping byte-by-byte in C. Display controller drivers commonly use memcpy_toio() to push an entire rendered frame into video memory in a single call. Recognizing which MMIO I/O API fits which situation is a core skill for any embedded Linux driver author.

Common Mistakes and Troubleshooting

  • Dereferencing an __iomem pointer directly instead of using ioread/iowrite
  • Using memcpy() instead of memcpy_toio()/memcpy_fromio()
  • Wrong register offset — always verify against the datasheet
  • Ignoring endianness on big-endian peripherals
  • Forgetting a write actually needs a read-back to confirm hardware state

If a write via these MMIO I/O APIs appears to have no effect, first confirm the mapped base address and offset are correct by reading a known-value identification register, since these routines themselves never fail or return an error.

Best Practices

  • Prefer devm_ioremap_resource() over manual ioremap()
  • Always match access width to the register width in the datasheet
  • Use repeating APIs for FIFO-style bulk transfers
  • Keep register offsets as named macros, never magic numbers
  • Test with a write-then-read-back sanity check

Performance Considerations

Every call through these MMIO I/O APIs eventually becomes a real bus transaction, so batching matters. Prefer the repeating variants or memcpy_io() helpers over a manual per-byte loop whenever you’re moving more than a handful of bytes — the kernel’s internal implementation is tuned per architecture and consistently outperforms an equivalent hand-rolled loop.

Security Considerations

Never expose raw MMIO offsets to userspace without strict bounds checking — an out-of-range offset combined with these MMIO I/O APIs can let unprivileged code touch unrelated hardware registers. Validate every offset against the mapped resource size before issuing an ioread/iowrite call.

Summary / Key Takeaways

  • Use ioread/iowrite for single-register access
  • Use _rep() variants for FIFO-style bulk transfer
  • Use memset_io/memcpy_io for ranged memory-style regions
  • None of these APIs return errors — validate mapping upfront
  • Prefer devm-managed resource handling in new drivers

FAQ: Linux Kernel MMIO I/O APIs

Q1. Why can’t I just dereference an __iomem pointer directly?
Direct dereference bypasses compiler and architecture guarantees around ordering and access width, which can silently corrupt hardware state. Always go through the MMIO I/O APIs.

Q2. What’s the difference between ioread32() and readl()?
They perform the same operation; readl() is the older naming convention. New drivers should use ioread32().

Q3. When should I use the _rep() functions instead of a loop?
Whenever you’re moving several bytes to or from a single fixed FIFO-style port register — the _rep() functions are both simpler to write and faster to execute.

Q4. Can memcpy() be used on MMIO memory?
No. Always use memcpy_toio() or memcpy_fromio(), which are written specifically to be safe on __iomem regions.

Q5. Do I need to call iounmap() manually?
Not if you used devm_ioremap_resource() or devm_platform_ioremap_resource() — the kernel automatically unmaps on driver removal.

Q6. What happens if I use the wrong bit-width for a register?
You risk reading/writing adjacent registers unintentionally, since the bus transaction size won’t match what the hardware expects.

Q7. Are these MMIO I/O APIs architecture-independent?
Yes — that’s their whole purpose. The same driver source compiles correctly across architectures because each arch supplies its own low-level implementation.

Q8. Do ioread/iowrite calls ever fail?
No, they have no return value indicating failure. If your driver misbehaves, the problem is almost always an incorrect address, offset, or timing assumption — not the API itself.

Conclusion

Mastering the Linux kernel MMIO I/O APIs — from single-register ioread/iowrite calls to bulk _rep() transfers and memcpy_io() helpers — is a foundational skill for anyone writing device drivers on embedded Linux. Once you’re comfortable choosing the right API for a given register access pattern, you’re ready to move on to interrupt handling and DMA, which build directly on this MMIO foundation. Keep following this free Linux kernel programming course for the next lecture in the series.

Continue the Free Linux Kernel Programming Course

More lectures on device drivers, embedded systems, and Bluetooth/BLE are available on EmbeddedPathashala.

Explore the Course

← Previous Lecture    Next Lecture →

2 Comments

Leave a Reply

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