Linux MMIO And Ioremap Guide
Free Linux Kernel Development Course — Chapter 11, Lecture 14
Keywords covered in this lecture
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 api —
ioremap()andiounmap() - Why modern drivers prefer
devm_ioremap_resource()over manual mapping - How to read and write registers safely with
ioread32()/iowrite32() - What the
__iomemannotation 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.
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 bypci_iomap()orioport_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()forNULL/IS_ERR() - Use
ioread*()/iowrite*()when the same driver may run over MMIO or PCI I/O space; usereadl()/writel()for pure MMIO drivers - Keep
__iomemannotations intact and build withC=1occasionally 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()) mapsdevm_ioremap_resource()is the preferred modern one-call approach- Always access mapped memory through
ioread*()/iowrite*()orreadl()/writel(), never by direct dereference __iomemplus 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().
