← Previous Lecture Next Lecture →
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.
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
| 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.
More lectures on device drivers, embedded systems, and Bluetooth/BLE are available on EmbeddedPathashala.
Explore the Course
2 Comments