Firmware Development

Using the UIO drivers on Petalinux

 

UIO Drivers

The UIO drivers are the ideal way to interface with your own FPGA blocks.  Generally speaking FPGA blocks are going to be configured through a group of registers from software, and then data will be passed through them, either from an external interface, or through streams to/from an axi-dma block.  We therefore need two things to be able to do almost anything from a linux application using our own FPGA design:  A way to access buffers at known physical memory addresses, and a way to read and write memory mapped registers.  The first is provided by u-dma-buf, and we’ll look at that in another post.  The second is provided by the UIO drivers, and we’ll write a simple driver to turn the LEDs on and off.

Device Tree Modification

The first thing required is to add the devices we want to map to uio devices to the device tree. 

&axi_gpio_0 {
compatible = “generic-uio”;
};

&axi_gpio_1 {
compatible = “generic-uio”;
};

&axi_dma_0 {
compatible = “generic-uio”;
};

When I added these and did a petalinux-build, nothing happened.  After googling it for a while I found an answer, I have to pass the uio_pdrv_genirq.of_id=generic-uio parameter to the command line.  When I added that to the device tree I got this error attempting to boot:

Kernel panic – not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
CPU1: stopping

This is fixed by adding the root device, type and “rw rootwait” to the bootargs in the “chosen” node.

/include/ "system-conf.dtsi" 
/ { 

      chosen { 
      bootargs = "earlycon clk_ignore_unused uio_pdrv_genirq.of_id=generic-uio cma=256M root=/dev/mmcblk0p2 rootfstype=ext4 rw rootwait"; 
      stdout-path = "serial0:115200ns"; 
      }; 
       usb_phy0: usb_phy@0 { 
      compatible = "ulpi-phy"; 
      #phy-cells = <0>; 
      reg = <0xe0002000 0x1000>; 
      view-port = <0x0170>; 
      drv-vbus; 
  }; 
}; 

&usb0 { 
  dr_mode = "host"; 
  usb-phy = <&usb_phy0>; 
}; 

&axi_gpio_0 { 
  compatible = "generic-uio"; 
}; 

&axi_gpio_1 { 
  compatible = "generic-uio"; 
}; 

&axi_dma_0 { 
  compatible = "generic-uio"; 
};

An Aside about Register Spaces

Reading and writing to the FPGA blocks is simple.  They are mapped to memory addresses, so read writes are the same as accessing any other variable in your code.  Just create a pointer and set it to the value of the address of the register you would like to read or write.  Then *addr = value.   But this does not mean that these values are actually stored in RAM.  It’s just how the CPU interacts with things connected to the AXI bus.  Reading large amounts of data this way will be much slower than using axi dma to allow the firmware to write data directly into RAM.

The simplest way to write and read these registers is to use the command “devmem”.  This will let you read or write any address on the system.   This is great for playing with your basic ip blocks, but it’s not recommended to go to production with software that interfaces that way.   Opening /dev/mem like that allows you to corrupt memory and crash the whole system meaning software bugs can be more serious.  The other reason to go with a kernel driver is that you then have interrupts.  Interrupt driven code is more efficient since you aren’t spinning and polling to see when the firmware is finished.  

In general you have 3 options:

  • Access through /dev/mem or the devmem shell command
    • Easy
    • Dangerous
    • No interrupts
  • Write your own kernel driver
    • Difficult
    • kernel driver bugs can crash the system
    • Best performance and customizability
  • Use UIO Drivers
    • interrupts available
    • almost as easy as using /dev/mem from within your software
    • requires device tree modifications but not much else

Using the UIO Drivers

The UIO drivers now create a number of files for us to use.  For reading and writing our register space we have /dev/uio0 /dev/uio1 and /dev/uio2.  These correspond to my axi dma, and two GPIOs.  If you look at your memory map in vivado we can see addresses for each region. 

We can get information about each device from the files in cd /sys/class/uio/.  The uio devices seem to be mapped in order of their base address in the memory map.  You can find and edit these addresses in the vivado block diagram. My GPIO1 is tied to uio1.  If we go to /sys/class/uio/uio1/maps/map0  we see the following files:

addr

name

offset

size

Size and name are both useful.  

LinuxBoot:/sys/class/uio/uio1/maps/map0$ cat name
gpio@41200000
LinuxBoot:/sys/class/uio/uio1/maps/map0$ cat size
0x00010000

So we see that this is gpio1 and it has a 64K space allocated to it. 

Building a “Hello world”

Now that that’s out of the way, lets see if make and gcc work.  The correct way develop for petalinux is to add the application to PetaLinux or use Vitis and build from the PC.  But this is rather time consuming and when I’m just hacking at small programs I’d much rather have a build environment on the target.  It turns out this is rather easy.  

petalinux-config -c rootfs 

Now within the menus:

Filesystem Packages ->  misc -> packagegroup-core-buildessential -> packagegroup-core-buildessential

Filesystem Packages ->  misc -> packagegroup-core-buildessential -> packagegroup-core-buildessential-dev

Petalinux Package Groups -> packagegroup-petalinux-self-hosted

Petalinux Package Groups -> packagegroup-petalinux-self-hosted-dev

Copy your new rootfs to your SD card and you will now see that you have vim, make, gcc and g++.

LinuxBoot:/sys/class/uio/uio1/maps/map0$ which vim; which make; which g++; which make 
/usr/bin/vim 
/usr/bin/make 
/usr/bin/g++ 
/usr/bin/make

Now lets make a Makefile for our led blinker:

mkdir ledblinker
cd ledblinker
vim Makefile

CC=gcc 
CXX=g++ 
CFLAGS=-I. 
CXXFLAGS=-I. 
DEPS= 
OBJ=ledblinker.o  

%.o: %.c $(DEPS) 
               $(CC) -c -o $@ $< $(CFLAGS) 

%.o: %.cpp $(DEPS) 
               $(CXX) -c -o $@ $< $(CXXFLAGS) 

ledblinker: $(OBJ) 
               $(CXX) -o $@ $^ $(CXXFLAGS)

The ledblinker.cpp file won’t be too much more complex. Important: When opening /dev/ files for mmap() always use the O_SYNC option fd = open((char *)”/dev/uio1″, O_RDWR |O_SYNC); Opening without O_SYNC can lead to strange and not wonderful results.

#include <unistd.h> 
#include <sys/mman.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <cstdint> 
#include <iostream> 

using namespace std; 

int main() 
{ 
 int fd;  
 uint32_t *addr; 

 fd = open((char *)"/dev/uio1", O_RDWR |O_SYNC); 
 if(fd == -1) 
 { 
   cout << "failed to open device" << endl; 
   exit(1); 
 } 
 addr = (uint32_t *)mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); 
 if(addr == MAP_FAILED) 
 { 
   cout << "mmap failed"; 
   exit(1); 
 } 
 uint32_t i = 0; 
 while(1) 
 { 
   addr[0] = i; 
   i++; 
   sleep(1); 
 } 
}

Now just make, then run with sudo:

LinuxBoot:~/ledblinker$ make
g++ -c -o ledblinker.o ledblinker.cpp -I.
g++ -o ledblinker ledblinker.o -I.
LinuxBoot:~/ledblinker$ sudo ./ledblinker

You should see the LEDs blinking and countint at 1Hz.

Next steps

Future posts:

  • Using the interrupts
  • Including a udev rule to allow non root users access to the uio devices
  • creating a userspace driver for the axi-dma blocks