We close out this mini-series in our free linux device drivers course with port-mapped I/O (PMIO), the second of the two hardware access styles introduced two lectures ago. PMIO is less common on modern hardware than memory-mapped I/O, but it still appears on x86 platforms and is a core topic in any thorough free linux kernel development course or free embedded systems course.
What You Will Learn
| ✅ What makes port-mapped I/O different from memory-mapped I/O |
| ✅ How to reserve a range of I/O ports before using them |
| ✅ The inb/outb family of accessor functions |
| ✅ When to choose PMIO versus MMIO on hardware that supports both |
Prerequisites
This lecture assumes you have completed the two earlier lectures in this mini-series covering I/O memory basics and memory-mapped I/O. Some familiarity with x86 architecture is helpful, since port-mapped I/O is primarily an x86 concept.
What Is Port-Mapped I/O?
Unlike memory-mapped I/O, where a peripheral’s registers simply occupy addresses inside the normal address space, port-mapped I/O uses a completely separate, smaller address space that the CPU can only reach through dedicated instructions. On x86 processors these are the IN and OUT assembly instructions, and the kernel exposes them to driver code through C wrapper functions rather than requiring you to write inline assembly yourself.
Step 1: Reserving an I/O Port Range
Just like with memory-mapped I/O, you must reserve a port range before using it, so the kernel’s bookkeeping reflects that your driver owns it. On kernel 6.x the managed variant is preferred:
#define MYDEV_IOPORT_BASE 0x300
#define MYDEV_IOPORT_SIZE 8
static int mydevice_probe_pmio(struct pci_dev *pdev)
{
if (!devm_request_region(&pdev->dev, MYDEV_IOPORT_BASE,
MYDEV_IOPORT_SIZE, "mydevice"))
return -EBUSY;
/* Port range is now reserved for this driver */
return 0;
}
As with the memory-mapped case, the devm_ prefixed version automatically releases the port range when the driver is detached, so you do not need a matching manual release call in a normal teardown path.
Step 2: Reading and Writing Ports
Once the range is reserved, you access individual ports with a small family of functions rather than a pointer. There is no mapping step here at all, since ports are not part of the regular address space.
| Port Width | Read Function | Write Function |
| 8-bit | inb(port) |
outb(val, port) |
| 16-bit | inw(port) |
outw(val, port) |
| 32-bit | inl(port) |
outl(val, port) |
A Simple Original PMIO Example
Here is a small, original status-check pattern showing the same read-modify-write idea from the MMIO lecture, adapted to ports instead of a mapped pointer:
#define STATUS_PORT_OFFSET 0x02
#define READY_BIT BIT(1)
static bool mydevice_is_ready(unsigned int base_port)
{
u8 status = inb(base_port + STATUS_PORT_OFFSET);
return status & READY_BIT;
}
static void mydevice_reset(unsigned int base_port)
{
outb(0x01, base_port + STATUS_PORT_OFFSET);
}
Real-World Use Case
Port-mapped I/O today mostly survives in legacy PC hardware paths: the classic PC parallel port, some older serial UART implementations, and a handful of very old PCI devices that never migrated to a pure memory-mapped BAR layout. Most new silicon, including new x86 peripherals, has moved entirely to memory-mapped I/O, so treat this lecture as important background knowledge rather than something you will use in every driver you write.
MMIO vs PMIO: Which Should You Use?
| Situation | Recommended Style |
| Writing a new driver for modern silicon | Memory-mapped I/O (MMIO) |
| Supporting legacy x86 hardware that only exposes ports | Port-mapped I/O (PMIO) |
| Targeting ARM, RISC-V, or most embedded SoCs | Memory-mapped I/O only; PMIO is unavailable |
Common Mistakes and Troubleshooting
| Mistake | Fix |
| Assuming inb/outb work the same on ARM | Port I/O instructions are x86-specific; check architecture support before relying on them |
| Skipping the region request step | Always call devm_request_region() first so the kernel tracks ownership correctly |
Key Takeaways
Frequently Asked Questions
It is far less common than memory-mapped I/O today, but it still appears on x86 systems for certain legacy peripherals, so understanding it remains useful for anyone working across a broad range of x86 hardware.
No. IN and OUT style port instructions are specific to the x86 architecture. ARM and most other embedded architectures only support memory-mapped I/O.
No. I/O ports are accessed directly through the inb/outb family of functions using the port number itself, with no mapping step required, unlike memory-mapped I/O.
Check your chip or board’s datasheet. Most modern peripherals, including nearly everything on ARM and RISC-V, use memory-mapped I/O, while port-mapped I/O is generally limited to specific legacy x86 devices.
Another driver could claim or conflict with the same ports, leading to unpredictable hardware behavior. Always reserve the range first with a request function before performing any inb/outb calls.
You now understand the full picture of hardware I/O memory access in Linux device drivers, from the underlying virtual memory theory through practical MMIO and PMIO driver code.

2 Comments