Notes on Vivado, SDK and PetaLinux

use with a grain of salt

Setting up the PetaLinux Environment:

see UG976

needed tools:

  • dos2unix
  • ip
  • gawk
  • gcc
  • git
  • make
  • netstat
  • ncurses
  • tftp
  • zlib
  • flex
  • bison
  • 32bit libs

vivado is incompatible with ncurses 6 so you will need to find a 5er version.

locale

Vivado has problems with the German locale, so switch to English before you start. If you do not want to switch your hole system, add:

export LC_CTYPE="en_US.UTF-8"
export LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=en_US.UTF-8

at the top of the scripts.

64bit and 32bit

If you are installing vivado 2015.2:

  • no 32bit support anymore
  • install script has a bug and hence always fail claiming you are not x86_64 compatible
  • the problem is, they are using uname -m instead of uname -i to check the system architecture
  • to fix this:
    • run the install script with --keep -> extracted data will not be deleted after the script closes
    • cd into the created folder and open the xsetup file
    • search for uname -m and change it to uname -i
    • run xsetup and the installation should properly detect your system architecture

accessing the tools

vivado, SDK and PetaLinux are shipped with a settings script. Once you source it, your PATH variables will be modified and you can just call the tools supplied by these packages.

So instead

/opt/Xilinx/Vivado/2015.2/bin/vivado
you can just call
vivado
after you have executed
source /opt/Xilinx/Vivado/2015.2/settings32.sh

The problem is that they also supply a lot of recompiled binaries which might be incompatible with your systems libraries and hence will not work. So only source the setting files if you really need them. For example, on my system, nmap is broken if I source the Xilinx tools.

Basic petalinux design flow

  • Finish your block design and VHDL, generate the bitstream and export the hardware
  • create a new petalinux project with

    petalinux-create --type project --name $PROJECTNAME --template Zynq

    This will create a new folder with $PROJECTNAME. Cd into it.

  • configurate the project with your exported hardware

    petalinux-config --get-hw-description $PATHTOHWARDWAREDESCRIPTION_FOLDER
  • do all the modifications you need

  • build the image with

    petalinux-build
  • create the actual image files

    petalinux-package --boot --format BIN --fsbl ./image/linux/zynq_fsbl.elf --fpga $PATHTOBITSTREAM --u-boot

    add --force if you want to overwrite an old image

  • copy the data in ./images/linux/ to an SD card

  • boot the zynq with the SD card

Using VHDL components from VHDL

BRAM

Use a DUAL port BRAM with a single port AXI BRAM controller on port A and your VHDL on port B of the BRAM.

The AXI BRAM controller will automatically map the BRAM into the address space of the CPU (which address and size can be controlled via the address space manager in vivado) and propagate these settings to the BRAM. Make sure to run the BRAM in the BRAM controller mode. This way, you can be pretty sure that the access from the CPU side to the BRAM is working.

Note: The AXI BRAM controller has a minimal memory depth of 1024 so your BRAM will at least be as big as 1024 * word size on port A. The word size can be set in the control options of the AXI BRAM controller. I am not sure if the word size has any other effect here beside setting the minimal size of the BRAM.

For port B of the BRAM. As we are using the BRAM controller mode, the port will learn its parameters due to parameter propagation from the connected port. Mark the port external (right click on the big port marker, mark external). The newly created external port has properties for the memory depth and word size which you can adjust to your liking.

Note: even if the port is marked external, only unconnected wires will be presented in the VHDL wrapper later on so you can still connect wires in the block schematic as you like.

Note: in BRAM controller mode, the memory address is always 32bits long and byte oriented! This means: if you have a word size of n bytes, you will always get the same word if address1 // n == address2 // n!!!

After you have created your block scheme and vhdl code, generate the bitstream, export the hardware with the bitstream. Proceed with the normal petalinux flow

Afterwards, use the /dev/mem device and mmap to access the BRAM.

GPIO

The AXI GPIO can be accessed two ways. 1. /sys/class approach 2. /dev/mem approach

In the block scheme, modify the AXI GPIO block to your liking (port with, in/out).

Check its address in the address manager. For some reason, GPIO blocks always use 64k. No idea why so just leave the default size as it is.

Create the petalinux image.

The /sys/class approach should be obvious. The only note worthy information is that in this approach, you manipulate single bits! not bytes or words of the GPIO. So each bit becomes one file.

The /dev/mem approach is also very straight forward. Use the memory address of the GPIO (lets say 0x40000000). The pins of the GPIO are directly mapped to this address. So writing 0b00000001 to 0x40000000 would order the AXI GPIO to set the lowest bit to 1, and bit 7 to 1 to 0.

SPI

I will only speak about the SPI cadence driver here (that is the SPI which is directly incorporated into the ARM core).

Enable the SPI by modifying the Zynq block in the block scheme. The SPI is quad SPI ready so there is a lot of tri-state control. Just mark the port external and let the VHDL wrapper do its job (or just extract the right pins for single SPI if you want)

Export the bitstream and the hardware.

In your petalinux project, call

petalinux-config -c kernel

look for the spidev driver and the cadence driver. The cadence driver should be enabled by default, the spidev driver is not enabled by default so enable it.

Afterwards, modify the device tree to contain an spi dev slave.

The device tree files are stored in

%PETALINUX_PROJECT_FOLDER%/subsystems/linux/configs/device-tree

Look through the files for an entry with "SPI". In my case, the file was pcw.dtsi and the entry looked like this:

&spi1 {
is-decoded-cs = <0>;
num-cs = <3>;
status = "okay";
};

This means that there is a SPI, it is enabled (status = okay) and it has 3 chip select lines.

Add a spi dev device as a slave to this master spi. In my case, the result looked like this:

&spi1 {
is-decoded-cs = <0>;
num-cs = <3>;
status = "okay";
spi_slave_dev@0 {
compatible = "spidev";
spi-max-frequency = <12000000>;
reg = <0>;
};
};

Save the file and build your petalinux image.

After booting the image, you should see a /dev/spi0 device. You can now read and write to this file and the data will be read and written from the SPI.

/dev/mem

/dev/mem is a special device in Linux which represents the complete address space of the system as one file.

Hence, this file can be used to write to a physical address.

Most devices attached via AXI are mapped to the physical address space with a fixed address. Due to this, manipulating the right addresses in /dev/mem will manipulate the AXI bound devices.

As we have access to all of memory with /dev/mem, be careful where you read and especially write as you might overwrite any data currently stored in any memory of Zynq.

Below is an example how to use /dev/mem to map physical address space into the local address space of the process.

#include <stdio.h>
#include <sys/mman.h> //mmap and munmap
#include <fcntl.h> //options for open
#include <unistd.h> // close, getpagesize
int map(volatile void **ptr, size_t size, int prot, off_t offset){
int fd;
fd = open("/dev/mem", O_RDWR);
if(fd < 1) return STATUS_OPENING_DEV_MEM_FAILED;
*ptr = (volatile void *) mmap(NULL, size, prot,
MAP_SHARED, fd, offset);
if(*ptr == MAP_FAILED) return STATUS_MAPPING_FAILED;
if(!close(fd)){
//we can close the file descriptor as mmap keeps it automatically open until we munmap
return STATUS_OK;
}else{
return STATUS_CLOSING_DEV_MEM_FAILED;
}
}
int unmap(volatile void **ptr, size_t size){
if(!(munmap(*((void **)ptr), size) == 0)){
return STATUS_UNMAPPING_FAILED;
}
*ptr = NULL;
return STATUS_OK;
}
int example_main(){
voaltile char *bram0;
map((void **) &bram0, %BRAM_SIZE_IN_BYTES%,
PROT_READ|PROT_WRITE, %BRAM_BASE_ADDESS (z.B: 0x40000000)%)
bram[1] = "a";
//a is now written to bram
unmap((void **) &bram0);
}

Be careful, I am not sure how caching influences how the data is transferred. For our purpose this does not matter but if we were doing high speed handshakes like the guys in Mainz, we would most likely have to flush the caches(read/write cache, L1, L2, and so on) somehow (from that I read this can only be done via some special assembler commands).