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
| ✅ 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.
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
Frequently Asked Questions
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.
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.
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.
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.
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.
Next up: Port-Mapped I/O (PMIO) for legacy x86 hardware, with a complete driver walkthrough.

1 Comment