Memory-Mapped I/O on Embedded Linux: Control Hardware with a Pointer
Your Linux program is just a process. It lives in its own cozy virtual world, shuffled around RAM by the kernel, blissfully unaware of the hardware underneath. And yet — somehow — a few lines of C code can flip a GPIO pin on a Raspberry Pi, light an LED, and talk directly to a hardware register. No kernel module. No magic. Just a pointer. That's memory-mapped I/O, and once you understand it, embedded Linux will never feel the same again.
The Concept — What Memory-Mapped I/O Actually Is
Every processor has an address space — a giant map of locations it can read from or write to. Most of that map points to RAM. But hardware designers are clever: they also wire peripheral registers — the control knobs of your UART, GPIO, SPI, and timers — into that same map. The CPU doesn't know or care whether address 0x3F200000 is RAM or a GPIO controller. It just reads and writes. The silicon handles the rest.
This is memory-mapped I/O in one sentence: peripheral hardware registers live at specific addresses in the CPU's address space, so software controls hardware the same way it accesses memory. No special CPU instructions needed. Just load and store operations — the most fundamental thing a processor does.
The alternative — port-mapped I/O (used on x86) — requires dedicated IN and OUT instructions and a separate I/O address space. ARM processors, which power virtually every embedded Linux board you'll ever touch, don't have that. Memory-mapped I/O isn't a workaround on ARM. It's the only way.
How It Actually Works — The Engineering Underneath
When your CPU issues a read or write to an address, that address travels down the system bus. A hardware component called the memory controller (or bus fabric, in modern SoCs) decodes the address and routes it to the correct destination. Addresses in one range go to DRAM. Addresses in another range are wired directly to peripheral register files inside the SoC.
Those peripheral registers are typically just flip-flops or latches — a few bits wide, physically inside the chip. Write a 1 to bit 17 of the BCM2835's GPSET0 register at 0x3F20001C, and a transistor switches. A pin goes high. An LED lights up. The path from your C pointer to that physical pin is entirely electrical — and it happens in nanoseconds.
In userspace Linux, there's one crucial bridge: /dev/mem and the mmap() system call. The kernel maintains strict separation between virtual and physical memory. mmap() lets you map a physical address range into your process's virtual address space. After that, a normal pointer dereference hits the hardware register directly — with the kernel's blessing, but without any further kernel involvement.
Step-By-Step — Controlling a GPIO Pin from Userspace
Here's a minimal walkthrough for a Raspberry Pi running embedded Linux. No kernel module. No device tree overlay. Just raw memory access.
- Open the memory device: Call
open("/dev/mem", O_RDWR | O_SYNC). You'll need root privileges.O_SYNCdisables caching — critical for hardware registers. - Map the GPIO base: Call
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x3F200000). This maps4096bytes of GPIO register space into your process. On Pi 4, use base address0xFE200000. - Cast to a pointer: Store the return value as
volatile uint32_t *gpio. Thevolatilekeyword tells the compiler: don't optimize these accesses away — every read and write matters. - Configure the pin as output: GPIO function select registers use 3 bits per pin. For GPIO 17, write
0b001into bits 21–23 ofGPFSEL1(offset0x04). - Set the pin high: Write
(1 << 17)toGPSET0at offset0x1C. Your LED lights up. - Clean up: Call
munmap()andclose(fd). Good citizens free their mappings.
The entire working example fits in under 30 lines of C. No libraries. No abstractions. Just you, a pointer, and the hardware.
Real-World Applications — Where This Shows Up
Memory-mapped I/O isn't a curiosity for hobbyists — it's the backbone of real embedded systems everywhere.
- Linux BSPs (Board Support Packages): Every hardware driver in the Linux kernel uses
ioremap()— the kernel's equivalent of userspacemmap()— to access device registers. - GPU programming: The BCM VideoCore IV GPU on Raspberry Pi is entirely register-controlled through mapped memory regions.
- Industrial controllers: PLCs running embedded Linux configure ADCs, DACs, and fieldbus interfaces by writing directly to SoC peripheral registers.
- Bootloaders: U-Boot initializes DRAM controllers, UARTs, and clocks purely through memory-mapped register writes — before any OS exists.
- FPGA interfacing: Xilinx Zynq and Intel Cyclone SoCs expose custom FPGA logic as memory-mapped peripheral regions, allowing Linux userspace to talk to custom hardware fabric.
Common Mistakes — Hard-Won Wisdom
This technique is powerful precisely because it bypasses normal abstractions. That power will hurt you if you're careless.
- Forgetting
volatile: Without it, the compiler may cache register reads in CPU registers and never re-fetch from hardware. Your loop that waits for a status bit will spin forever. - Wrong base address: Pi 2/3 use
0x3F000000. Pi 4 uses0xFE000000. Writing to the wrong address corrupts memory or locks up the system silently. - Read-modify-write race conditions: GPIO function select registers control multiple pins. Always read first, mask, then write — or you'll accidentally reconfigure neighboring pins.
- Skipping memory barriers: On weakly-ordered architectures, the CPU may reorder memory accesses. Use
__sync_synchronize()or ARM'sdsbinstruction when sequence matters. - Using
/dev/memin production: It's a security hole. For production, write a proper kernel driver or use the/dev/gpiomemrestricted interface where available.
Key Takeaways
- Memory-mapped I/O places hardware registers directly in the CPU address space — no special instructions needed.
mmap()on/dev/memgives userspace programs direct physical access to those registers.- Always declare hardware register pointers as
volatile— the compiler must not optimize those accesses. - Base addresses are chip-specific — always verify against the official Technical Reference Manual for your SoC.
- This is how every Linux hardware driver works under the hood — you're not doing something exotic, you're doing what the kernel does.
- Understanding memory-mapped I/O is the difference between using embedded Linux and truly controlling it.