Alvaro M, Programming, Technology

Linux User Space Drivers with Interrupts

Writing device drivers in Linux requires some kernel knowledge and some rules must be followed. Writing a user space application that drives the device can be simpler to write and debug. This article explains how to implement a simple user space driver to a memory-mapped device.

Some time ago I needed to port a software that was driving a custom device. The interface between the operating system and the device had changed though. The old software had a custom interface to read/write to the device memory, and to handle the interrupts. The new device is a memory-mapped IP that sits on an ARM based SoC, so in theory the communication layers are simpler to deal with.

My first solution was to write the user space application using /dev/mem to access to physical addresses. This seemed to work fine, but I still had to poll a register value to check for the interrupt.

Then I came across the User I/O (UIO) driver, and a blog entry that explained how it was working. Even better, I figured out that there was a generic interrupt version of the driver, thus making it even easier to use. So here are are the steps I followed.

  1. In you kernel configuration, select UIO_PDRV_GENIRQ. I chose it as built-in, but you can build it as a separate module.
  2. In you kernel device tree source, you have to define you UIO device:
    mydev@70000000 {
     compatible = "my-uio";
     reg = <0x70000000 0x80000>, /* zone 1 and size */
           <0x80000000 0x40000>; /* zone 2 and size */
     reg-names = "zone1", "zone2"; /* Names of the zones */
     interrupts = <31>; /* interrupt the device generates */
    };
  3. Add “uio_pdrv_genirq.of_id=my-uio” to the boot parameters, so the uio_pdrv_genirq driver will look for the device marked compatible.

Note the “reg” section, that define the list of memory regions accessible via UIO driver. That’s all we need to do on the kernel side. If we compile and reboot, you should be able to see the mapped devices on /sys/devices/70000000.mydev/uio/uio0/maps/mapX/. A device node will be available on /dev/uio0.
In order to use the device, you need to open it, then map it using the mmap function. For example, this program writes the value 1 at the address 0x8000000, mapped in the zone 2:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define PAGE_SIZE 4096UL
#define PAGE_MASK (PAGE_SIZE - 1)

int main(int argc, char **argv) {
 int fd;
 void *map_base, *virt_addr;
 unsigned long writeval = 0x1;
 off_t target = 0x80000000;

 if ((fd = open("/dev/uio0", O_RDWR | O_SYNC)) == -1)
  return -1;
 /* Map one page */
 map_base = mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd,
            1 * PAGE_SIZE);
 if (map_base == (void *)-1)
   return -1;

 virt_addr = (void *)((off_t)map_base + (target & MAP_MASK));
 *((unsigned long *)virt_addr) = writeval;

 if (munmap(map_base, MAP_SIZE) == -1)
   return -1;
 close(fd);
 return 0;
}

Note that we map the device with offset 1 * PAGE_SIZE. This is to tell the UIO driver we want to access to the zone 2 (to access zone 1 we would have used offset at 0).

Finally, the interrupts. In order to enable the interrupt check, we need to write a signed 32 bit int value “1” to /dev/uio0. We can then check for the interrupt using the “select” function. When the interrupt kicks in, the UIO driver interrupt handler will notify the poll function that will wake up the select. All we need to do then is to read a signed 32 bit value int from /dev/uio0 to acknowledge the event.

TL; DR

If you want to write a software that drives a memory mapped device form user space, you can use the uio_pdrv_genirq driver. This will allow you to access the device from user space without writing a single line of C code in kernel space, and using a simple select to wait for the interrupt.