← 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.
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.
Declares physical register range
Driver fetches the physical range
Kernel maps it into virtual address space
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 resourcepassed 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 manualdevmcleanup (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 anERR_PTRon failure — always check withIS_ERR()before use. - Forgetting region conflicts: if another driver already claimed the
same physical range, your request will fail;
/proc/iomemtells you who owns it.
Best Practices for MMIO in Modern Drivers
- Prefer
devm_ioremap_resource()over manualioremap()+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/iomemduring 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/iomemis 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.
More free lectures on Linux device drivers, kernel internals, and embedded systems.
← Previous Lecture Next Lecture →
2 Comments