Posts in this series:
- Memory Mapped I/O Adventure
- Memory Mapped I/O on Microcontrollers
- Memory Mapped I/O and PCIe
- Userspace Memory Mapped I/O
Most devices have a “driver” - a piece of code that runs in the Kernel - for communicating with devices. These drivers typically use MMIO to control the device and get status and use whatever kernel subsystem is relevant to provide access to userspace. For example, network cards interface to the networking subsystem to provide network adapters. The biggest reason for this is code reuse and interface sharing - the common code is shared between all the drivers and the API presented to userspace is uniform.
But perhaps your device doesn’t really fit into one of these pre-existing categories. What do you do then?
One option is to define your own API, but you’re somewhat limited.
You can define your own syscalls, but that’s pretty difficult and frowned upon by Kernel developers.
So next option would be to use a character device and the standard I/O syscalls:
This can work well in many of situations.
The next stop would be
ioctl. This syscall can be used to move just about any kind of data between userspace and the kernel.
However, it can be tricky to get right and due to it’s flexible nature it can be the wild west of APIs.
Thus, many Kernel maintainers frown upon more
ioctl based interfaces as well.
Other options include
procfs is a huge dumpster fire (even more so than
sysfs is actually a really good option, but the semantics are “one value per file” and very device-hierarchy oriented, so making some things fit can be tricky.
Is there a simpler option? What if you don’t need all the facilities the Kernel provides? Enter: Userspace I/O (UIO)!
The UIO framework allows a kernel driver to “export” some MMIO registers into userspace.
The userspace side calls
mmap() on a character device file and the resulting memory area will poke the associated hardware registers directly.
There is no kernel involvement after that initial setup phase!
You can even have interrupts -
write() controls the interrupt mask,
read() blocks until an interrupt occurs (and records the number of times the interrupt has fired),
poll can be used to asynchronously wait for interrupts.
You can’t do DMA transfers, however, but that shouldn’t be a problem for most drivers that would likely use the UIO framework.
The links below will have more information about how to actually use/implement a driver that uses UIO.