When I first started looking into Petalinux and learn the basics, I thought I should start my journey with a simple GPIO toggling and an interrupt. I've soon found out there was more to it than meets the eye. There were dozens of articles online and a lot of buzzwords, like device tree or binding. The simple task turned to be a challenging one, since I'm not the kind of engineer who likes to copy-paste the source code without understanding what I'm doing and gluing all parts together was not easy as I've expected.
In this article, and the others that will follow, I'll try to explain what GPIO drivers are all about. I'll give a full step-by-step guide to create a Petalinux image with UIO driver and interrupt support. Indeed, there are many tutors online, so I tried to give an added value in my tutors over the ones already exist. If you want to skip the theoretical part of the GPIO drivers, you can jump directly to part 2 (I'll post it in the following weeks). Otherwise, grab a beer, and without further ado, let's start at the very beginning:
User space and Kernel spaceA modern computer OS, like Linux, has two distinct, separated areas in the memory region called User space and Kernel space. Gaining a solid understanding of the two will help in writing and debugging a device driver for the specified task, in our case - toggle an IO and hit an interrupt. Sounds simple, isn't it? or maybe we should avoid writing our own driver? better keep reading...
We can divide this interface to three layers, as shown below. Each layer interacts with its neighboring layer, so eventually, when the user toggles an IO with the application, or turns on a LED, it goes through all layers:
When interacting with the hardware block, the kernel acts as a bridge between the hardware and the user-space (where all applications and programs reside). The kernel is in charge of scheduling processes to run, managing memory, interrupt handler, etc., and is located at the core of the host operating system with full control over the system's resources. The rest of the OS serves to boot and manage the user space and communicates constantly with the kernel. In Linux the user space has access to the virtual memory, which is managed by the kernel (virtually mapped). In term of IO - the kernel provides the lowest level abstraction layer for the resources (especially IO devices) that application software must control to perform its function.
The kernel and the user space communicate via 'system calls'. System call offers the OS its service to the user applications via API. It is the only way the kernel can interact with the user space; file and device management, communication, etc. You can see in the scheme above a few examples of common system calls used, like 'open' which is used when interfacing with a GPIO:
open("/sys/class/gpio/gpio101/value, O_RDWR).
This system call is executed by the application to open and assign a file descriptor to gpio101 value. The task itself is carried out by the kernel.
The conventional driver method is that all hardware is accessed by the kernel, which means one needs to write a kernel driver. This is NOT recommended and there are many reasons one should avoid that. For start, a bug in the kernel driver can bring all systems to a halt (compared to bug in user space), and also the debugging is more difficult and time consuming.
"Few" words on GPIO driversThere are many GPIO drivers out there, some of them are not supported by Xilinx (Petalinux), yet supported by other vendors (TI, for example, who manufactures the Beaglebone board, which has vast documentation online). Each driver has its good's and bad's and since I wanted to perform this very simple task with Petalinux and Zedboard, I eventually chose to work with the UIO driver, but there was a lot of interesting stuff along the way which was definitely worth talking about.
In Linux Everything is a file, which means that everything in the system (whether it is CPU's, directories, sockets, printers, called devices or nodes) is handled by a file descriptor whenever the file is opened.
There are 2 main types to interface with these devices; /dev and /sys.
'/sys' (abbreviated sysfs located at /sys/class/gpio), on the other hand, was added later and includes a complete hierarchy of the devices as attached to the computer, as well as an interface to a variety of device properties such as bus settings, bus speeds, device IDs, device names, etc. In /sys/class there is a directory for each different class of device. Opposed to /dev, /sys is a virtual file-system (RAM based).
- '/dev' exists from the earlier days of Unix system and includes the actual devices files created at run time by udev (udev creates nodes when devices are plugged in and remove them once plugged out). It is a real file-system, disk based.
Linux enables access to GPIO from kernel driver, but exposes GPIO to userspace applications through handles in sysfs (/sys/class/gpio). Obviously a kernel driver is not a recommended method for the average user, and I was looking for a much easier way through the user space. SysFs/GPIO driver is one method, explained nicely in Xilinx tutorial by Rob Armstronghere.
There are number of methods to access the GPIO in embedded Linux environment:
- SysFs driver: The SysFs interface (based on "gpiolib" framework), as mentioned above, is a very simple method accessing the various GPIO's in command line or code from the user space. Yet, this method should not be used where interrupts are required (the polling process to catch events, like interrupts from GPIO lines, is not reliable). I do recommend going over the xilinx-wiki page of SysFs and try accessing the IO's just to get the hang of it, as it is very straightforward method. I'll show later on few examples. Also, important to add this method should be removed from the Linux kernel as of version 4.8. Currently, Petalinux fully supports that.
- gpio-keys driver: A Linux Kernel driver. A powerful alternative to the SysFs interface, include interrupt support (only) to a pressed key.
- gpio-keys-polled driver: A Linux Kernel driver. It is used when GPIO line cannot generate interrupts, so it needs to be periodically polled by a timer.
- leds-gpio driver: A Linux Kernel driver. It will handle LEDs connected to GPIO lines from user space, giving the LED a sysfs interface. As the name implies, it is suitable for any output-only GPIO applications.
- UIO driver: The Userspace I/O framework is a simple and convenient way to implement a driver almost entirely in user space, and fully support interrupts. It seems almost tailored for engineers seeking to interface with FPGA's on their boards and running embedded Linux (e.g., Petalinux).
Other two user space interfaces used in embedded Linux are shown below. Both deprecates the legacy SysFs interface to GPIOs (gpiolib based):
- chardev - explained here, supported by kernel version 4.8 and above (Petalinux kernel version is 4.19). It is considered a recommended kernel API for GPIO as SysFs is deprecated as of Linux 4.8 . It prevents manipulating GPIO with standard command line tools such as echo and cat, so less convenient, though supports read and set multiple I/O lines at once. As far as I know, Petalinux does not support that.
- gpio-cdev - explained here, supported by kernel version 4.4 (Petalinux kernel version is 4.19). Used extensively in BeagleBone boards. As far as I know, Petalinux does not support that.
Now that I have gone through the various drivers, it's time to summarize the applicable methods. There are three main methods which we can choose to work with:
- Using one of the drivers above (kernel or user space).
- Write my own kernel driver.
- Use mmap with /dev/mem.
I've covered (1), so let's discuss (2) and (3):
Regarding (2), as I wrote earlier, usually, it is not recommended and better avoided. For many types of devices, creating a Linux kernel driver is an overkill. It is much more complicated compared to the other options, and usually requires code in the kernel. Nonetheless, it is considered the fastest of the three types. Compared to user space drivers, it is much more challenging to debug Kernel space drivers. Also, in user-space, one can write the driver in Python, or any other language, compared to C in kernel space.
Regarding (3), /dev/mem is a character device which is an image of the main memory (the physical one, not the virtual, which can be accessed using /dev/kmem). /dev/mem image includes the RAM and also memory mapped IO devices. To access the device from memory space, we use mmap to map the device to memory, and then we access the device using a pointer which points to the mapped memory. It is easy to set up and debug (can do it from shell), but there's no support with interrupts, and I did wanted to demonstrate an interrupt IO, remember?
A working code for /dev/mem method can be found here.
UIO driverSince my article is dedicated to the design of a user space driver based on UIO (User Space IO), I wanted to dedicate a section of its own. As I wrote earlier, it is not necessary and better avoided to design a custom driver (kernel based) just for accessing memory-mapped registers in a custom IP core. Also, when interrupts are needed (like in FPGA designs), UIO driver is the way to go with.
This type of driver enables writing the majority of the code in user space with very little code in the kernel itself. Just like the /dev/mem method, it is a character device (with little help from SysFs) that the user can open, memory map, and access the device using a pointer. The UIO enables the driver to be entirely in the user space and, compared to other methods mentioned above (including the /dev/mem), it fully support interrupts. I'll show later on few examples. To use generic UIO, we'll need to add few lines to the device tree (I'll show that either).
The UIO devices can be found in 2 places in Embedded Linux (or Petalinux for this guide), each location is used for different needs. To access the device, to clear interrupts, for instance, we need to go to /dev/UIOx (where 'x' is the running index of the device attached):
In the picture we see 2 drivers, uio0 & uio1 (they correspond to 2 PL components in my design I'll show in my future tutorials).
In /sys/class/UIOx there's much more information about the attached device, and many more goodies (in part 2, I'll dive deeply into these):
Is User Space driver (and UIO) always better?
To make things more complex, user space drivers are not always the best choice to go with. The kernel reserves some 'spare' memory for use during "emergency" cases, but that is not a viable option for users-space drivers, so in low-memory times, the kernel will kill random user-space programs, but will never kill kernel threads. Furthermore, resource sharing is an important factor in favor of the custom kernel driver, too. When multiple applications need to share a device between them (involving concurrent access to memory, etc.), user space drivers are better left behind.
When Ethernet or DMA application is needed, the kernel custom driver is the one to go with. Using a user space driver in that case would be much more complicated and that is because DMA capable memory can be allocated from kernel space much more easily than user space. Here's a nice solution to DMA from user space, though less recommended.
Going back to /dev/mem, what is the difference between UIO and /dev/mem, as both reside in the user space?
As a rule of thumb, it is better to use UIO driver. There are mainly 2 points to consider in this context:
- Interrupts: as discussed, supported only in UIO driver. Interrupts should be registered in the kernel, since the user space cannot deal with it, so a minimal small module exist in the kernel, containing ISR to acknowledge or disable the interrupt, yet, all other issues are handled in the user space.
- Permissions: /dev/mem enables access to all memory regions and open your system to security risks whereas UIO improved it by preventing full access to all memory regions.
There are many links and tutorials regarding the UIO. Here you can find a nice source code for the UIO and a thorough article here.
To set it all up we will need the following softwares and hardware:
- Vivado 2019.1
- Petalinux 2019.1
- Ubuntu 18.04.2 LTS installed on Virtualbox 6.0.4 (remember Petalinux 2019.1 & 2019.2 cannot work with Ubuntu versions above 18.04.2)
- Zedboard or Ultra96v2 (I've used Zedboard, but everything can be used also with Ultra96v2, with the suitable changes)
- A free and handy tool is MobaXterm, which helped me communicate between Linux and Windows (my base OS is Windows).
All source files can be downloaded in my Github page here.
Design the Hardware partSo, to get things going, I'll start with a very easy project in Vivado. It will probably seems like a really simple one for the common user. Yet, things will get more interesting as I'll go forward with the UIO debugging phase. Believe me, the owls are not what they seem. In general we'll need to design the Vivado project only once during the project lifetime, since the rest of our work will be in the software or Linux side.
I created 2 folders, one for the hardware Vivado project (named here: UIO_wIRQ) and one for Petalinux, including all device tree files, created images and so on.
Jumping to the end of the hardware design phase, you can this folder will have many sub-folders, as in any other Vivado project. I'll need the sdk folder since this is where the hw description file resides (HDF - I'll get to that):
We'll start with the Vivado project and before placing elements in the BD, the first step is to validate the ZedBoard is checked at Boards tab under Settings at Project Manager. Then we'll run the regular steps of creating a Zynq based project:
- Import the "Zynq Processing system".
- Double click it, and choose *Presets -> ZedBoard
- Run the 'Run Block Automation', to create the 2 ports for DDR and Fixed IO. This part is usually a questionable one, and I think Vivado engineers took for granted the fact that DDR is an essential part of the Zynq PS, since OCM is only 256KB, and to run something larger, you will have to use an external DDR.
- Import the AXI_GPIO core.
- Click 'Run Connection Automation' and check all checkboxes:
This will automatically add AXI IC and 'System Reset' components to the design, and connect all needed wires. Please not that the '5 bit buttons' were also connected directly to the AXI GPIO port during the automated process. I will also add the 8 LEDs to the AXI GPIO, so dragging them from the Board tab towards the AXI GPIO, will add them automatically and create the necessary port.
Next step would be connecting the interrupt port from the AXI GPIO to the Zynq processing system. For that we will enable the interrupt check box in the configuration box of the AXI GPIO, and check the interrupt box at the Zynq configuration menu:
Pay attention that we use one interrupt IRQ_F2P[0], which is assigned to IRQ#61 (LSb) out of 16 bit shared interrupts from the PL.
I've connected the interrupt of the AXI_GPIO to the IRQ_F2P interrupt input of the Zynq, as can be seen below.
Next steps, will be generating a bit stream and export to hardware. Now we're all set and ready to start creating our Petalinux image with UIO driver.
Creating a Petalinux imageFor the first step, we'll need a BSP or a template. Since the BSP of Zedboard need to work upon (can be downloaded at Xilinx website) I prefered to work with template according to the instruction mentioned in UG1144, which is actually the bible for Petalinux, and totally recommended for first time users:
so, let's do some coding:
cd /projects/linux_images/UIO_wIRQ_PL
petalinux-create --type project --template zynq --name UIO_wIRQ
Change location to created linux template:
cd /projects/linux_images/UIO_wIRQ_PL/UIO_wIRQ
Then import the HDF file →
petalinux-config --get-hw-description=/projects/UIO_wIRQ/UIO_wIRQ.sdk
Next step will be to change the DTG settings to Zed board:
Use shift+backspace to erase the ‘template’ and insert: zedboard (double esc to go back):
Validate the boot will be from SD card:
Subsystem Auto HW settings → Advanced bootable image storage settings → Boot image settings.
Also, need to verify we’re using the correct UART port (UART1 in my case):
SubSystem AUTO Hardware settings → Serial Settings
Adding support for UIO:
petalinux-config -c kernel
Then → device drivers → user space IO drivers →
We’ll check it as <*> so it will be loaded at boot time. Checking it as <M> means Module and in such case, it will be loaded only using Modprobe command.
Optional:
In case we'd want to add our own application, we'd first run the following command to create an application ("HelloWorld" in the example):
Petalinux-create -t apps --template c++ --name helloWorld
In such case, a cpp file will be created automatically, as a template file, and we can write our own code, to be later installed in the rootfs.
Then, when running:
petalinux-config -c rootfs
we can choose the application to be included in rootfs to load it at boot time.
We can also do it in a shorter way:
Petalinux-create -t apps --template c++ --name helloWorld --enable
(Enable will automatically add our helloWorld app to the rootfs)
Next we'll build our image to create the necessary device tree file, in order to change them, accordingly to our hardware components.
petalinux-build
And now, it'll take a few minutes, time to drink some coffee...
If it'll fail, you can run it again using the '-v' (for Verbose output):
petalinux-build -v
This will output all build log to the screen and can help figuring out what went wrong.
The Device TreeThere are many tutorials online regarding device trees. Adam Taylor wrote a nice one here, the "guide for dummies" is here and a really extensive one by NXP which covers everything one can think of is here. So, you have here enough reading material for a full weekend. I'll try not to repeat what all of them have covered.
Before the device tree methodology was used, all hardware description used to reside inside the kernel. This caused quite a headache for the ARM community (nice article about the evaluation of the device tree is here). There were many components, many boards, many header files for each configuration (even though the compilers were the same). At the end, a unique kernel for each type of hardware was a necessity. Not so elegant solution, isn't it?
So this is how the device tree method was born. The kernel no longer contains any hardware description. It is located at a different file called: Device Tree Blob (a compiled device tree file). The bootloader loads 2 files at power up: the DTB and the kernel image.
There can be many device tree files, all are read and parsed during Petalinux image compilation (order does matter. I'll explain shortly). Let's look at 'system-user.dtsi' and 'pl.dtsi', both are created during the build process of Petalinux image.
Since both files are buried deep inside the Petalinux folder, the best advice I could give you is using the 'locate' command in Linux for searching a file in a specific folder (there are other methods as well). It should be located at: "[project-name]/project-spec/meta-user/recipes-bsp/device-tree/files".
Looking first at the pl.dtsi, it holds all the info about our used hardware blocks in the design:
Specifically for the simple project I've designed for this tutorial, except the Zynq, system reset and AXI IC (which no interaction is needed), the AXI GPIO is the only one exists, and this is shown on the left.
I will not go over all the fields here, but there are some lines worth paying attention to.
Before doing so, I will go to the created file: system-user.dtsi, which is an empty one, used as a place-holder, This file allows one to overwrite properties in the Xilinx-generated device-tree pl.dtsi. All the dtsi files are parsed by a specific order, and the system-user.dtsi is the last one. This means if we add some properties to the system-user.dtsi, which are already exist in the pl.dtsi, the ones in the pl.dtsi will be ran over. Please keep that in mind.
Going back to the pl.dtsi, some of the lines are self explanatory, others are less important to our journey, and the most interesting ones are these:
Compatible is used twice here, one with a "simple-bus" value, which means a simple memory-mapped bus with no specific handling or driver (driver like I2C, SPI, etc.). The other one has 2 strings: "xlnx, axi-gpio-2.0" and "xlnx, xps-gpio-1.00.a".
These values are used to bind a device with the driver (simply put, to indicate the kernel which driver will handle this hw). The first string "xlnx, axi-gpio-2.0" is comprised of the vendor name: 'xlnx', and the IP name of the block in Vivado ('axi-gpio-2.0'). The second string "xlnx, xps-gpio-1.00.a" is comprised of the vendor name: 'xlnx' and the binding name of the GPIO block, 'xps-gpio-1.00.a'. I've seen device trees when using one or both, so I can't say what is prefered. I've used both.
Moving to the next interesting parameter, the 'reg'. It has a value of <41200000>, <0x10000>. Where did these numbers come from?
Going way back to my Vivado project, you can recall the address editor of Vivado, which gave a default address to the AXI_GPIO component:
Looks familiar, isn't it? So, the 2 values came from here; address and range:
0x4120_FFFF-0x4120_0000=0x10000 (including the start address).
Interrupts In Device TreeAnd last but not least, is the more famous, parameter, the interrupts:
Interrupts = <0 29 4>
It contains 3 numbers, as follows:
0 = is the first value, and it indicates whether the interrupt is defined as an SPI (Shared Peripheral Interrupt). There are 60 interrupts from various modules inside the Zynq which can be routed to the CPU or PL (or both). These are defined as SPI. A nonzero value means it is an SPI, but wait, this IS an SPI (I connected my interrupt to IRQ_F2P[0]). From UG585-Zynq-7000-TRM:
So, where does the zero value come from?
There are few interesting discussions on the web regarding this subject. You can see here, for one.
The bottom line is though it is defined in the TRM as an SPI, it receives a zero value. The arm-gic.h header files also shows a zero value for SPI.
So, it could be a mistake in the TRM. In any case, a '0' should be written as first parameter.
Moving to the next number '29' - this is the interrupt number and looking at IRQ ID# which fits to IRQ_F2P[0] (where I connected my interrupt to), the number is 61:
61-29 = 32, and this is the magic number. The kernel device tree parser adds 32 to the IRQ id: in other words, for each interrupt connected to IRQ_ID# inputs of the Zynq, we need to subtract 32 in order to find the interrupt index. In my next project I connected 2 interrupts, so IRQ #61, 62 were used (IRQF2P[1:0], accordingly), this in the pl.dtsi we'll see that 2 interrupts fields are created: <0 29 4> and <0 30 4>, but I'll get to that.
The last number is '4'. This is the interrupt type. There are 3 types we can use, based on the Kernel device tree documentation:
bits[3:0] trigger type and level flags
1 = low-to-high edge triggered
2 = high-to-low edge triggered
4 = active high level-sensitive
8 = active low level-sensitive
No.4 is the default one, which means it is a level sensitive triggered. We can also look at our Vivado project, since the device tree generator uses the pin properties from the IPI:
Now, this is a very important issue, which later on in the debug phase, will have a crucial implication on our interrupt status.
From AXI GPIO guide (PG144):
This means the AXI GPIO can make only a level sensitive signal using its interrupt. This is important. Stay tuned, to understand why!
Adding UIO to device treeThe above issues are part of the pl.dtsi, and as I mentioned earlier, if we'll add them to system-user.dtsi with a different values, it will override the pl values. In this tutorial there's no need to do that, so I'll concentrate on the only things need to add to system-user.dtsi, and there are 3 sections, both are related to UIO.
Jumping to the end, the system-user.dtsi should look like that:
/include/ "system-conf.dtsi"
/ {
gpio@41200000{
compatible = "axi_gpio_0, generic-uio, ui_pdrv";
status = "okay";
};
chosen {
bootargs = "console=ttyPS0,115200 earlyprintk uio_pdrv_genirq.of_id=generic-uio";
};
};
&axi_gpio_0
{
compatible = "generic-uio";
};
The first section tells the device tree generator that we're adding UIO capability to the component at address 0x4120_0000 (our axi-gpio block, which generates the interrupt). In device tree language, this means we added a node with generic-uio and ui_pdrv.
The second section is the 'chosen' one. This includes the bootargs command, a U-Boot variable. The bootargs allows the user to tell the kernel how to configure the device drivers used (UIO, for one). In my case:
console=ttyPS0, 115200:
This indicates a serial port, ttyPS0, for the UART on the Zynq PS.
earlyprintk
used to show early boot messages, before kernel started executing; helps for debugging in case of an error.
uio_pdrv_genirq.of_id=generic-uio
Earlier we've added the generic-uio to the node, we now need to allow the UIO driver to be compatible to this node. This string validates the UIO driver (uio_pdrv_genirq) is built into the kernel and be loaded at power up, otherwise, we'll need to use modprobe command to load the driver, obviously, less recommended:
modprobe uio
modprobe uio_pdrv_genirq
Regarding the last section, &axi_gpio_0, though it seems redundant to the first section (gpio@41200000), the Petalinux image did not work without it, so I suggest to leave it as is.
We can verify the bootargs were indeed inserted to the kernel, going back to:
petalinux-config --get-hw-description=/projects/UIO_wIRQ/UIO_wIRQ.sdk
And then, DTG settings:
And clicking on Kernel Bootargs:
we'll see our custom bootargs passed to the kernel:
Now, let's wrap it up, sending the following command:
petalinux-build
and after few minutes we'll receive the following message in our console:
which means all files are now located at 2 folders:
- /tftpboot - at the root folder, created after the build is finished.
- [project-folder]/UIO_wIRQ/images/linux - inside our main project folder.
Both folder are identical and it's up to the user to decide from where to package the image files.
Packaging the image fileUsing the following commands, we'll package the files needed in order to create the image.ub, boot.bin and our rootfs files (where all our filesystem is located at):
cd /fpga/projects/linux_images/UIO_wIRQ_PL/UIO_wIRQ/images/linux
petalinux-package --boot --fsbl zynq_fsbl.elf --u-boot --fpga system.bit --force
One can also create these files manually, but it is much easier do it through Petalinux commands (even a must, as there are many posts at Xilinx forums that undergoing this way manually not always works as expected).
The petalinux-package command simply takes the relevant files, all marked with double dash, in order to create the following files:
BOOT.bin - this is a compiled u-boot OS in binary format (both the first stage and second stage boot loader, as well as the bitstream, among other components).
image.ub - The Petalinux image which consists of kernel image, device tree blob and minimal rootfs.
rootfs.tar.gz - this is the most basic component of Linux. It contains all the applications (e.g., our main application, which runs at startup), devices, configurations, etc.
UG1157 has a lot of interesting info about petalinux-package and I recommend to go over it.
Important to note that the above command is used for booting from an SD card. In the case of using a flash to boot from, we'll need to alter this command and add also the kernel (this cab be dealt in a later tutorial).
Anyway, we've now reached the finish line and ready to place those 3 files in our SD card.
well, as always, not so fast...
copying Petalinux files into the SD cardWe'll first need to partition the SD card with 2 (or more) partitions. I've explained it thoroughly in my Hackster project page, where I wrote about a method for creating a dynamic-static IP (worth reading...). So, I'll not go over it again here.
Once the files are located at the correct folders, we can power on the board and Linux should power up.
Checking UIOOnce we're in, the next step is to validate we have the UIO driver installed, looking at the "/dev" folder which contains the installed Drivers:
We can also see it under /sys/class/uio, where other interesting stuff is hidden:
The 'maps' folder include all address mapping info regarding the component we used. We can go over the various parameters and see some familiar addresses, size, etc (from part 2 my tutorial).
All this clearly shows us the UIO was indeed installed correctly.
Checking UIO is functioning correctlyThis is the most interesting part, so I hope you all wide awake. If not, by all means, get yourself a strong cup of coffee, cause the fun begins and it is a bit complicated.
For testing the UIO, I will generate an interrupt in the Zed board by hitting the push button and I expect to see the interrupt counter increased at CPU side. In Linux it is located at /Proc/Interrupts.
/Proc/Interrupts file
Interrupts to CPU allows devices like keyboards, mouse, pushbutton (in our case) to signal the CPU it wishes to talk with it.
On Linux we have a file which holds that info, including number of times the CPU was informed, the type of interrupt raised and more. This file is located at /Proc/Interrupts
I will use the AXI GPIO IP block for that matter (the projects are in my Github page) and as a reference this link helped me a lot so I recommend go over it also.
For reaching the various AXI GPIO registers I'll use the devmem2 application. It is a small application very useful for reading and writing to almost any available address. This is a very handy tool for developers before you have a fancy GUI for that matter, or other form of user interface.
From this table taken from the AXI GPIO block manual (PG144):
Setting these registers turn on the corresponding LEDs connected to GPIO2 in my design:
Now, we’ll try to see the interrupt is incremented:
According to the below picture, we’ll write to the correct registers:
- Enable channel interrupt:
devmem 0x41200128 w 0x1 #for GPIO channel 1
devmem 0x41200128 w 0x2 #for GPIO channel 2
devmem 0x41200128 w 0x3 #for GPIO both channels
2. Enable global interrupt:
devmem 0x4120011c w 0x80000000 #for both GPIO channels
Now, before we hit the pushbutton to set the interrupt, let’s look at /proc/interrupts:
We can see here the 2 CPU's (Dual-core ARM Cortex-A9 in the case of the Zedboard), the trigger type ('level triggered' or 'edge triggered'), the functionality and the interrupt number.
Pay attention that at the 'gpio' interrupt value the interrupt number is 61:
46: 1 0 GIC-0 61 Level gpio
Why is it 61? back in part 2, where I've explained about the device tree, the number was 29! That is because 29 + 32 = 61 (as explained, the kernel device tree parser adds 32 to the IRQ id).
Now, we’ll click a pushbutton (any button) and the interrupt will fire. We can see that in the interrupt file. The counter has increased by 1:
Reading this address shows the Interrupt status register in the AXI GPIO:
devmem 0x41200120
> 0x00000001
Which means an interrupt has occurred. Writing 0x1 to the same register toggles the status of the bit:
devmem 0x41200120 w 0x1
> 0x00000000
The above scenario is based on the AXI GPIO manual:
An interesting issue is related to clearing GPIO line at /cat/interrupts. After the interrupt has fired, it is ‘stuck’ at ‘1’.
How do we clear it?
UIO-How-to explains:
So, we need to clear this bit, via source code, or any other method.
From the kernel UIO driver website:
So, I'll echo the UIO with '1' to clear the interrupt:
echo 0x1>/dev/uio0
But strangely, the interrupt counter at /proc/interrupts increments all the time. Why is that?
well, this took me a while to find out, and the reason is AXI GPIO supports only level triggered interrupts. The Disable bit was cleared indeed, but the interrupt level is still high (since it is defined as ‘Level'), and the interrupt counter keeps incrementing.
Going back to VivadoFiguring out my design is wrong, since the GPIO AXI is level triggered and I cannot clear the UIO interrupt, I went back to the PL and decided to design a new project, where I have an additional interrupt source, and this time, I will design it as 'Edge triggered':
I’ve deleted the push_buttons port and defined my own ports, per the locations stated in the Zedboard manual (button_center, button_left, etc).
Tip #1
The custom_ioblock is a modified version of the “Tools → create and Package New IP…”. A nice trick to avoid the cumbersome methodology of using the packaged IP in a new project (using right click → “edit in IP packager”) is to copy the code inside the created IP into a new RTL module (save it with a new name), then add it to the BD using Add Module (right click in BD). This is actually what I did, thus the RTL letters are shown inside the block.
In my vhd code I created a simple pulse (i.e., interrupt) whenever the user clicks one of the buttons.
Tip#2
to define the interrupt as a ‘rising_edge’ type, I’ve added these lines to my vhd code:
attribute X_INTERFACE_INFO : string;
attribute X_INTERFACE_INFO of interrupt_out : signal is "xilinx.com:signal:interrupt:1.0 interrupt_out INTERRUPT";
attribute X_INTERFACE_PARAMETER : string;
attribute X_INTERFACE_PARAMETER of interrupt_out : signal is "SENSITIVITY EDGE_RISING";
This makes the pin ‘interrupt_out’ an interrupt type signal of rising_edge:
So, the design now includes 2 sub-IP’s; my custom IP, which has an 'interrupt out' signal of rising_edge type, and the AXI GPIO original block which has an interrupt of type ‘level’.
Since I've 'concat'-ed both interrupts this is clearly shown in the BD:
Last, but not least, is to change the device tree, and add to system-user.dtsi the new created custom IP:
/include/ "system-conf.dtsi"
/ {
gpio@41200000{
compatible = "axi_gpio_0, generic-uio, ui_pdrv";
status = "okay";
};
chosen {
bootargs = "console=ttyPS0,115200 earlyprintk uio_pdrv_genirq.of_id=generic-uio";
};
};
&axi_gpio_0
{
compatible = "generic-uio";
};
/* adding my new custom rising edge triggered IP*/
&custom_IO_v1_0_S00_A_0
{
compatible = "generic-uio";
};
Now, next steps are similar (but not exact) to what I did before:
- Compiling the project
- Export hardware (include bit).
- petalinux-config --get-hw-description=<project>/<project.sdk, then double-esc to exit (the relevant files will be re-created with the new HW files).
- Petalinux-build (with the new updated dtsi files).
- Copying to SD card BOOT and DATA files.
In the first push button, examined the /proc/interrupts, to test the new interrupt type. We can see we have 2 IP's, our original AXI GPIO ('Level') and our new custom trigger source ('Edge'):
Then clearing the interrupt using:
echo 0x1>/dev/uio1 # --> since uio1 is my custom IP, while uio0 is the AXI GPIO.
Question arises, how do we know to which 'uio' should we write? we now have 2 UIO's!
TIP#3
Go to /sys/class/uio.
For example, see here from a different project we have various components, all UIO drivers based. Each one has its own UIO index. The key is to compare it with the system-conf-dtsi file:
we'll compare it with system-conf.dtsi file:
We'll follow the addresses on the dtsi file and compare the index of the UIO.
Back to our project, Clicking the button again:
Then clearing the interrupt using:
echo 0x1>/dev/uio1 # --> since uio1 is my custom IP, while uio0 is the AXI GPIO.
Clicking the button again:
Vuala! Exactly what I wished for!
To sum it all up:I've started with a design based on GPIO AXI for triggering an interrupt. This indeed worked well, but I could not disable the interrupt and the result was the interrupt counter at the /proc/interrupt kept increasing since the AXI GPIO supports only level triggered interrupts. Next, I've added my own interrupt trigger and made it rising edge one. It did worked flawlessly. This should remind us that we better use rising edge trigger type rather than Level.
And lastly, as a work of caution:
One may have other solution (maybe even simpler one) for this task, but from my point of view, this indeed did the trick...
Now I can say: Q.E.D!
Comments
Please log in or sign up to comment.