MMIO vs PMIO in Linux Device Drivers: Complete Beginner Guide
Free Linux Kernel Programming Course • Hardware I/O Memory • Lecture 1
← Previous Lecture Next Lecture →
What You Will Learn
- What hardware I/O memory means from a Linux device driver’s point of view
- The difference between MMIO (Memory-Mapped I/O) and PMIO (Port-Mapped I/O)
- Why a driver cannot touch a device register directly using a physical address
- How modern (6.x) kernels expose device register ranges through the resource framework
- A simple, original example of reading a device’s memory resource in a platform driver
Prerequisites
- Basic C programming knowledge
- A general idea of what a Linux kernel module is
- Familiarity with the terms “physical address” and “virtual address” is helpful but not mandatory
Why Hardware I/O Memory Matters in a Linux Device Driver
Every peripheral chip on a board — a UART, an I2C controller, a GPIO block, a timer — exposes a small set of hardware registers to the CPU. A Linux device driver’s entire job, at the lowest level, is to read from and write to these registers in the correct sequence. This is exactly what we mean when we talk about hardware I/O memory in the context of Linux device driver development: the address range through which the processor talks to a peripheral, as opposed to normal RAM.
There are two broad hardware mechanisms a processor can use to expose these registers, and understanding both is the real starting point of any serious Linux kernel programming course: Memory-Mapped I/O (MMIO) and Port-Mapped I/O (PMIO).
Memory-Mapped I/O (MMIO) Explained
In MMIO, the SoC or processor designer carves out a slice of the normal physical address space and dedicates it to peripheral registers instead of RAM. From the CPU’s perspective there is no special instruction needed — a normal load/store to that address range talks to hardware instead of memory. Almost every modern ARM, ARM64, and RISC-V embedded platform relies on MMIO because it is simple, uniform, and works well with the compiler’s normal addressing modes.
Because MMIO registers live in the same address space as RAM, a driver author has to be extremely careful: the compiler is free to reorder or cache normal memory accesses, but register accesses must happen in program order and must never be cached. This is precisely why the kernel never lets you dereference a register address as a plain pointer — you always go through dedicated accessor functions, which we will cover in the next lectures of this free Linux device drivers course.
Port-Mapped I/O (PMIO) Explained
PMIO, sometimes called isolated I/O, is a legacy x86 mechanism. Instead of sharing the address space with
RAM, the processor has an entirely separate, smaller address space reserved for I/O ports, and dedicated
CPU instructions (IN and OUT on x86) are used to talk to it. PMIO is largely a
thing of the past on embedded and server platforms today, but you will still encounter it on classic PC
hardware and in some legacy x86 drivers, so it is worth knowing conceptually even in a modern kernel
programming course.
MMIO vs PMIO: Quick Comparison
| Aspect | MMIO | PMIO |
|---|---|---|
| Address space | Shared with system RAM | Separate, dedicated space |
| CPU instructions | Normal load/store | Special IN/OUT instructions |
| Common on | ARM, ARM64, RISC-V, modern x86 devices | Legacy x86 peripherals |
| Kernel accessor family | readl/writel and friends | inb/outb and friends |
How Modern Linux Kernels Describe I/O Memory
In current Linux kernels, every peripheral’s register range is described by a struct resource
with an IORESOURCE_MEM flag. This resource is normally populated from the device tree (on
embedded boards) or from PCI BARs (on PCIe devices), and a driver retrieves it through the standard
platform or PCI driver APIs rather than hardcoding any address. This is a big shift in mindset for
anyone coming from older tutorials: you should almost never see a raw hexadecimal address typed directly
inside a modern driver.
Here is a simple, original example showing how a platform driver reads its I/O memory resource today:
static int mychip_probe(struct platform_device *pdev)
{
struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "no MMIO resource found\n");
return -ENODEV;
}
dev_info(&pdev->dev, "MMIO region: start=0x%llx size=0x%llx\n",
(unsigned long long)res->start,
(unsigned long long)resource_size(res));
/* Mapping this resource into kernel VA space is covered
in the next lecture on ioremap() */
return 0;
}
Notice that the address is never a literal number in the source file — it comes from
platform_get_resource(), which reads it from the device tree entry that describes this
hardware block. This is standard practice across every modern driver in the kernel tree today.
Real-World Use Cases
- A UART driver reading its register base to program baud rate and enable interrupts
- A GPIO controller driver exposing pin direction and output registers to the gpiolib subsystem
- A network interface driver mapping its DMA descriptor rings and control registers
- A display controller driver mapping its framebuffer control registers
Common Mistakes and Troubleshooting
| Mistake | Why It’s a Problem |
|---|---|
| Hardcoding a physical address | Breaks portability across boards and SoC revisions |
| Treating a register pointer as normal memory | Compiler reordering/caching can corrupt hardware state |
| Ignoring resource_size() and using a guessed length | Can map too little or too much, causing crashes or conflicts |
Best Practices
- Always fetch the resource through the platform/PCI/device-tree APIs, never by hand
- Always check
resource_size()instead of assuming a fixed length - Keep MMIO and PMIO handling logically separate in your driver code
- Use
dev_err()/dev_info()instead of plainprintk()for diagnostics
Performance and Security Considerations
MMIO accesses are typically uncached and ordered, which makes every register access relatively expensive compared to a RAM access — batch reads/writes where the hardware allows it. From a security standpoint, never expose raw MMIO ranges to user space unless absolutely required (and only through a controlled, audited interface); an unprivileged process with direct register access can crash or damage hardware.
Summary / Key Takeaways
- MMIO shares the CPU’s normal address space with RAM; PMIO uses a separate, dedicated port space
- Modern ARM/ARM64/RISC-V platforms almost exclusively use MMIO
- Never hardcode register addresses — always retrieve them through the resource framework
- The next step is learning how to safely map this resource into kernel virtual address space with ioremap()
Conclusion
Understanding the distinction between MMIO and PMIO is the foundation for everything else in Linux
device driver development. Once you’re comfortable with how registers are exposed to the CPU, the next
logical step in this free Linux kernel programming course is learning how a driver actually maps that
physical register range into a usable kernel virtual address — which is exactly what the next lecture
covers using the ioremap() family of APIs.
Frequently Asked Questions
Q1. What is the difference between MMIO and PMIO in Linux?
MMIO maps device registers into the same physical address space as RAM and is accessed with normal load/store instructions, while PMIO uses a separate I/O port address space accessed with special instructions such as IN and OUT on x86.
Q2. Is PMIO still used on modern hardware?
PMIO is mostly a legacy x86 mechanism. Modern ARM, ARM64, and RISC-V based embedded systems rely almost entirely on MMIO.
Q3. Can I directly dereference an MMIO physical address in a driver?
No. A physical address is not directly usable by the CPU in kernel code; it must first be mapped into kernel virtual address space using APIs such as ioremap(), which is covered in the next lecture.
Q4. Where does a Linux driver get its MMIO address from?
On modern systems the address comes from the device tree (embedded boards) or PCI BAR configuration (PCIe devices), retrieved through APIs like platform_get_resource().
Q5. Why shouldn’t I hardcode a register address in my driver?
Hardcoding breaks portability across boards and SoC revisions, and prevents the kernel’s resource management from detecting conflicts between drivers.
Q6. What is a struct resource in the Linux kernel?
It’s the kernel’s generic data structure for describing a range of memory, I/O ports, IRQs, or DMA channels owned by a device, including its start address and size.
Q7. Is this tutorial applicable to Raspberry Pi and other SBCs?
Yes, the same MMIO concepts apply to any ARM-based SBC. The exact register layout differs by SoC, but the driver-side approach of retrieving resources through the kernel APIs remains the same.
Continue Your Free Linux Kernel Programming Course
This lecture is part of EmbeddedPathashala’s free Linux device drivers and kernel programming course.
Explore More Free Lectures
2 Comments