The Ultra96-v2 is a fantastic little board that enables people on a budget to explore MPSoC architecture. The multi-processor system on the Ultra96 has several core types, APUs (application precessors), RTP (real-time processors), DSPs, GPUs and most importantly an FPGA programmable logic section. An application can be knitted together to optimally use each of these core types to enable the most efficient use of the board.
Many projects that use the Ultra96-v2 concentrate on accelerating ML models using the FPGA such as fruit detection and LiDAR filtering. Whilst this is great. What happens when your robot detects an obstruction in front of it? It has to somehow MOVE out of the way. This is done using the GPIOs and is very customisable but a bit convoluted if you are using a custom Vivado block design, and PetaLinux.
In this article, I'm going to walk through the steps required to set up & control GPIO lines from the Vivado block design, to the PetaLiunx build, and finally their use in python with the mraa library.
VivadoThroughout this project we are going to use the Xilinx 2019.2 toolchain. This is nice and stable but has been superseded recently with 2020.2. Luckily, GPIOs are pretty fundamental so the steps should be roughly the same even in the later versions. One thing to bear in mind is that you need to use the same version of the toolchain for both Vivado and PetaLinux.
1. Create a new Vivado project, select the Ultra96-v2 board as the project device. The board files can be downloaded from here: https://github.com/Avnet/bdf/tree/master/ultra96v2/1.1 and need to be installed into
<Install path>/Vivado/<version>/data/boards/board_files/
Add the ZynqMP IP and run block automation. This sets up all the board presets.
2. Add a AXI-GPIO block to the diagram. Then run the block connection wizard. This will automatically add the missing PSR and AXI-interconnect IPs. Delete the port gpio_rtl
. We are going to add our own custom one next.
3. To add an output port right click on the block diagram background, outside of any IP block. Select Create port...
. Give it the name pin_out
, and set its direction to out
. This port will represent all the GPIOs we have in the system.
4. We need to customise the AXI-GPIO IP before we can connect the port, so double click it. The bus width tells Vivado how many GPIO lines we want. I'm going to use 2 here. But each GPIO bus can accept up to 32. You will need to add constraints for each line you use (both a package pin and voltage standard, see below).
Your block diagram should now look like this.
The final step in Vivado is to set the constraints. This is where we define which pins on the device are to be used for which GPIO. Looking at the Ultra96 schematic
I'm going to set pins 3, and 5 of the low-speed (LS) header to be my output GPIO lines. These are attached to the signals HD_GPIO_0 & HD_GPIO_1. Following these signals back, we can see that they are attached to package pins D7 and F8 respectively. So we need to set the constraints in Vivado to fix the two output GPIO lines to these pins, and also limit the voltage through the lines so we don't damage our Ultra96.
set_property IOSTANDARD LVCMOS18 [get_ports {pin_out[0]}]
set_property PACKAGE_PIN D7 [get_ports {pin_out[0]}]
set_property PACKAGE_PIN F8 [get_ports {pin_out[1]}]
set_property IOSTANDARD LVCMOS18 [get_ports {pin_out[1]}]
Alternatively, the above constraints could be set in the GUI, using the IO planning tab. Now we have finished the hardware design we need to validate it, generate the bitstream and export the.xsa.
The hardware setupIn the above picture, you can see the two boot modes of the Ultra96, and we can also see the low-speed header. The small white arrow to the left of the LS header indicates LS header pin 1. Our current hardware design has two, pin out, GPIO lines. The first is connected to package pin D7 which is physically wired to pin 3 of the LS header on the Ultra96. The second is connected to package pin F8 which is physically wired to pin 5 on the LS header.
Before we start, remember to source
all the necessary paths. I usually use a script like this before I start a Xilinx session.
#!/bin/sh
source ~/tools/Xilinx/Vitis/2019.2/settings64.sh
source ~/tools/Xilinx/Vivado/2019.2/settings64.sh
source ~/tools/Xilinx/Petalinux/settings.sh
source /opt/xilinx/xrt/setup.sh
Navigate to a project root folder, create a PetaLinux project and cd
into it.
petalinux-create --type project --name plnx_gpio_demo --template zynqMP
In my normal project setup, I usually have a project root folder and within this, I have a folder for Vivado and the exported.xsa hardware file, and a folder for my PetaLinux project.
We import the.xsa hardware file into the PetaLinux project and this largely configures everything needed bar a few tweaks.
petalinux-config --get-hw-description=../vivado
I'm not using the Xilinx JTAG programmer so we are going to set this up to boot and run from the SD card (make sure the DIP switches are selected for SD card boot). There are two things that need to be set in the main PetaLinux configuration. The first is the stdin/stdout need to be set to UART 1, not the default UART 0. This can be found in Subsystem AUTO Hardware Settings => Serial Settings
The next thing to change is the kernel boot args. This is found in DTG Settings => Kernel Bootargs
. DO NOT forget to uncheck the box generate boot args automatically
. If you forget to do this, PetaLinux will constantly overwrite any options you set. The boot args required are
earlycon clk_ignore_unused root=/dev/ram rw
When we imported our hardware file. PetaLinux automatically included all the drivers we need to use the GPIO. It also fills in the device tree for us. All that's left for us to do is include any missing packages for our project. Namely, mraa and Python 3.
mraa is a hardware-independent way of interacting with GPIOs. It has bindings in multiple languages and makes our life easier. It's use is also recommended by 96Boards. You can read more about it here https://github.com/eclipse/mraa
petalinux-config -c rootfs
Python 3 is buried in Filesystem Packages => misc => python3
mraa is found in Petalinux Package Groups => packagegroup-petalinux-mraa
That's it. Go and put the kettle on and run:
petalinux-build
Once that has finished, we need to package the.BIN file.
petalinux-package --boot --format BIN --fsbl images/linux/zynqmp_fsbl.elf --fpga images/linux/system.bit --u-boot --pmufw images/linux/pmufw.elf --force
Now we need to copy the files over to an SD card and boot up the Ultra96. Open a Python 3 command line and import mraa. Next, we need to define a pin.
pin1 = mraa.Gpio(510, raw=True)
At first, this confused me. Where does 510
come from? Well, this assignment is done by the Linux kernel. Specifically, in.../drivers/gpio/gpiolib.c. You can check it in /sys/class/gpio
,
root@plnx_gpio_demo:~# ls /sys/class/gpio
export gpiochip336 gpiochip510 unexport
root@plnx_gpio_demo:~# cd gpiochip510
or
root@plnx_gpio_demo:/sys/class/gpio/gpiochip510# dmesg | grep gpio
[ 8.522488] XGpio: gpio@a0000000: registered, base is 510
or we can calculate it ahead of time using:
ARCH_NR_GPIO - numberGPIOUsed.
For the Ultra96, ARCH_NR_GPIO = 512
and our design has two GPIOs defined. So the base number is 512 - 2 =
510
. If we had 6 GPIOs in our project, our base number would be 506, and we would have pins 506, 507, 508, 509, 510, 511 to set and use.
Pin assignment in this project with two output GPIOs is therefore 510 and 511. These are connected to LS header pins 3 and 5 respectively.
We also need to add raw=True
to tell mraa that we are using the kernel's numbering system.
Now you can integrate some GPIO lines to your custom designs. Happy building!
Thanks to Sundance for supplying me with the Ultra96 used in this project.
Comments
Please log in or sign up to comment.