Home How to Setup a Zynq UltraScale+ Vivado Project and Run a C-Code Example Accessing an AXI Slave on the FPGA
Post
Cancel

How to Setup a Zynq UltraScale+ Vivado Project and Run a C-Code Example Accessing an AXI Slave on the FPGA

Prerequisits

This project was set up with AMD/Xilinx Vivado in version 2023.2 and Vitis Classic 2023.2 under Ubuntu 20.04 using an Avnet UltraZed-3EG IO Carrier Card. Generally, the set-up described should also work in older and newer versions of Vivado and under other operating systems with similar Zynq UltraScale+ devices. Be aware that Windows 10/11 has problems running Makefiles which contain wildcard expressions such as *.c or *.h instead of the wildcard command of make. A fix for this can be found here.

Make sure to install picocom, which is used to communicate with the ARM processor on the evaluation board, using:

1
2
sudo apt update
sudo apt install -y picocom

And add yourself to the dailout group with:

1
sudo usermod -a -G dialout <YOUR_USERNAME>

Afterward, logout and login again such that the groups changes take effect.

Creating a Vivado project

Start Vivado by clicking on its icon in the application menu. When Vivado has opened click on Quick Start -> Create Project

Vivado Start Menu

In the New Project dialog, click on Next and choose a project name and location. For this example project, set the project name axi_hello_world and click on Next. Choose RTL Project and check Do not specify sources at this time and uncheck Project is an extensible Vitis platform.

Vivado Project Type

In the next step, you need to select the platform for which this project is intended. As this tutorial uses an Avnet evaluation board, choose Boards and then press Refresh to download the list of available boards from AMD/Xilinx. Then select the Vendor avnet.com from the drop-down menu and choose the matching board, for this tutorial this is the Avnet UltraZed-3EG IO Carrier Card. If you haven’t downloaded the board files yet, click on the download symbol in the Status column and press Next to continue.

Vivado Default Part

Finish the Vivado project creation by pressing Finish.

Create a Synthesizable Vivado Block Design

After the project creation is finished, Vivado presents you with a project overview. To implement anything on the FPGA (also called PL, for Programmable Logic) a Block Design needs to be created. In the Flow Navigator -> IP Integrator press Create Block Design.

Vivado Create Block Design

In the Create Block Design dialog, leave the Design name at design_1 and press OK. In the Diagram view press on the + icon to add an IP block. The first IP block that needs to be added is the Zynq UltraScale+ MPSoC, double-click on it to add it to the block design.

Vivado Block Design Add Zynq UltraScale+ MPSoC

Vivado is now providing some help to configure the Zynq IP block, Click on Run Block Automation.

Vivado Block Design Run Block Automation

In the Run Block Automation dialog, check All Automation and Apply Board Preset and press OK.

Afterward, an AXI IP that can address the GPIO pins connected to the PL-LEDs needs to be added. Press + to add another IP block and search for AXI GPIO and double-click on it to add it to the block design.

Vivado Block Design add AXI GPIO

When the AXI GPIO block has been added to the block diagram, double-click on the newly added block to configure it. In the Re-customize IP dialog, set GPIO to led 8bit and press OK.

Vivado Block Design customize AXI GPIO

To make this tutorial a bit more interesting another custom AXI IP block will be added. In the Vivado menu, choose Tools -> Create and Package New IP.

Vivado Create new IP

In the Create and Package new IP dialog, press Next, then choose Create AXI4 Peripheral and press Next.

Vivado Create AXI4 Peripheral

Set the Name axi_slave_test, and Description AXI Slave Test and press Next.

Vivado set AXI Slave Name

In the next step leave everything at the default settings and press Next and then choose Add IP to the repository and press Finish. This AXI Slave Test IP can now be added to the block design. Press on the + icon in the block design diagram and search for the AXI Slave Test to add it.

Vivado Block Design add AXI GPIO

When the AXI Slave Test was added to the block design, the Verilog code of it can be customized. Right-click on the axi_slave_test_0 block and select Edit in IP Packager.

Vivado Block Design Edit in IP Packager

In the Edit in IP Packager leave everything at the default settings and press OK. Now a second Vivado window opened in which the Verilog code of the AXI Slave Test can be edited. In the Sources tree navigate to Design Sources -> axi_slave_test_v1_0 -> axi_slave_test_v1_0_0_S00_AXI_inst ... and double-click it.

Vivado Open AXI Slave Verilog File

In the Verilog file axi_slave_test_v1_0_0_S00_AXI.v you can see the complete logic of an AXI Lite Slave. This slave allows you to write to the registers slv_reg0 to slv_reg3 using memory mapped IO on the ARM CPU (PS). Currently, the register values do not trigger an action or change the data written to the registers. For the purpose of this tutorial slv_reg3 should always return 0xdeadbeef when reading from it and slv_reg0 should return the written value increased by 1.

Firstly, the write logic for slv_reg3 needs to be commented out (line 253 to 259).

Vivado comment out write logic

Secondly, the value of slv_reg3 must be set to 32'hdeadbeef when the AXI Slave is reset (line 226).

Vivado Change Reset Value of `slv_reg3`

To increase the value of slv_reg0 by 1 when reading from the register, add a +1 to line 373.

Vivado increase `slv_reg0` by `1`

When you finished editing the Verilog code save it by pressing [CTRL]+[S] and close the Vivado window in which the AXI Slave Test was edited in. In the Vivado window in which the block design is opened, the message IP Catatlog is out-of-date should appear. Press Refresh IP Catalog to update it with the changes that have been applied to AXI Slave Test.

Vivado Refresh IP Catalog

Below the block design diagram, the IP Status will appear. Make sure to select the axi_slave_test_0 and press Upgrade Selected and press Skip when asked to Generate Output Products.

Vivado Block Design Upgrade Selected

Now, two AXI slaves are in the block design diagram. However, they aren’t connected to the ARM CPU (PS), to do this use the connection automation by clicking on Run Connection Automation.

Vivado Block Design Run Connection Automation

In the Run Connection Automation dialog check All Automation such that all blocks get connected to each other and press OK.

Vivado Block Design All Automation

Afterward, the block design diagram should look like the following image (the individual blocks can be at different locations, but the wires should match).

Vivado Block Design Connected

To be able to synthesize the block diagram and generate a FPAG bitstream, Verilog code needs to be generated. This can be done by Vivado automatically. In the block design view, go to Sources on the left-hand side, unfold Design Sources right-click on design_1 (this would be named differently if you set a different name for the block design) and choose Create HDL Wrapper.

Vivado Block Design Create HDL Wrapper

In the Create HDL Wrapper dialog, select Let Vivado manage wrapper and auto-update and press OK.

Vivado Block Design Auto-update

With the block design completed it can now be synthesized. In the Flow Navigator on the left-hand side unfold Systhesis and press Run Synthesis.

Vivado Run Synthesis

In the Launch Runs dialog, leave everything at its default and press OK. Depending on your hardware, the synthesis could take a couple of minutes. When the synthesis has finished, you are presented with the Synthesis Completed dialog. Select Run Implementation and press OK to implement the synthesized design, which will place and route everything for the FPGA.

Vivado Synthesis Completed Dialog

The Launch Runs dialog will appear again, leave everything at its default and press OK. When the implementation has completed, you are presented with the Implementation Completed dialog, select Generate Bitstream and press OK.

Vivado Implementation Completed Dialog

The Launch Runs dialog will appear a third time, leave everything at its default and press OK. When the bitstream generation completed the Bitstream Generation Completed appears, you can press Cancel as the floor plan etc. is not relevant at the moment.

This concludes the creation of a Vivado project and implementing a block design for the PL. The next step is the implementation of the software on the PS (ARM CPU).

Implementing a C Program to Communicate with the FPGA (PL) Design using Vitis Classic

When the bitstream has been generated go to the Vivado menu and choose File -> Export -> Export Hardware

Vivado Export Hardware

In the Export Hardware Platform dialog press Next and on the next step select Include Bitstream and then press Next.

Vivado Export Hardware Include Bitstream

In the next step of the Export Hardware Platform dialog, leave the XSA file name and Export to path at their default values and press Next and then Finish. The XSA file and the path to it is later on needed in Vitis to set up the C code project.

Vivado XSA File Path

Now open Vitis Classic and choose a path for your workspace this is the path where all your Vitis projects will be stored and then press Launch. I create a new workspace for every Vivado project, but that is up to your preference.

In the Vitis overview, press Create Application Project.

Vitis Create Application Project

In the first step of the New Application Project dialog press Next. In the second step select the tab Create a new platform from hardware (XSA) and press Browse and select the XSA file exported in Vivado and leave the settings for Boot Components at its default values and then press Next.

Vitis Select XSA File

Vitis Select XSA File

Next, set an Application project name, for this tutorial set it to axi_hello_world, and leave everything else at its default settings and press Next.

Vitis Application Name

In the next step of the New Application Project dialog, press Next without changing anything. In the last step, select the Hello World template and press Finish.

Vitis Select Template

Now there are two projects in Vitis axi_hello_world_system and design_1_wrapper. The axi_hello_world_system is the project containing your code while the design_1_wrapper contains all libraries to compile your project. To get to the main function of the example Hello World program unfold axi_hello_world_system -> axi_hello_world -> src -> helloworld.c

Vitis Hello World C-File

To test that creating the project was successful, build it the first time by pressing the build icon (hammer), while the helloworld.c file is opened and currently in focus. If the build was successful, you are going to see the message Finished building: axi_hello_world.elf.size in the Console below the code view. The program you just built runs bare-metal on the ARM CPU (PS) and sets up the CPU using init_platform() prints Hello World and Sucessfully ran Hello World application through the UART and then ends itself.

Before running the Hello World program, connect the UltraZed board to power and the DUAL USB UART and the SMT3 JTAG using micro USB cables to your computer.

UltraZed Connectors

Before turning the board on, make sure the dip switch SW2 is set to ON-ON-ON-ON which set the bootloader into bare-metal mode.

UltraZed SW2

Now turn the board on and then open a terminal and use picocom to connect to the UART of the UltraZed board using the following command:

1
picocom /dev/ttyUSB1 -b 115200 -d 8 -y n -p 1

Now you are almost ready to run the Hello World program on the ARM CPU (PS) of the UltraZed board. Click on the drop-down menu right of the run icon and choose Run Configurations.

Vitis Run Configurations

In the Run Configurations dialog double-click on System Project Debug.

Vitis System Debug Project

The newly opened run configuration in the Run Configurations can now be run by pressing Run. This will load the bitstream onto the FPGA (PL) and load the compiled code onto the ARM CPU (PS) and starts executing it.

Vitis Run

When the Hello World program ran successful you will see the following message in the terminal:

Picocom Hello World

Communicating with the AXI Slaves on the PL using C Code

The last step is to change the C code to communicate with the two AXI slaves added to the block design in Vivado. Each AXI slave has registers which can be addressed through memory mapped IO. Since the C code runs bare-metal, there are no virtual addresses and the physical addresses of the AXI slaves can be found in the xparameters.h header file. Add the following line to the includes of helloworld.c:

1
#include "xparameters.h"

And then press [CTRL] and click on the newly added line to open the xparameters.h file. In the header file you can find a section dedicated to the AXI Slave Test which you generated and modified in Vivado:

1
2
3
4
5
6
7
8
/******************************************************************/
/* Definitions for driver AXI_SLAVE_TEST */
#define XPAR_AXI_SLAVE_TEST_NUM_INSTANCES 1

/* Definitions for peripheral AXI_SLAVE_TEST_0 */
#define XPAR_AXI_SLAVE_TEST_0_DEVICE_ID 0
#define XPAR_AXI_SLAVE_TEST_0_S00_AXI_BASEADDR 0x80010000
#define XPAR_AXI_SLAVE_TEST_0_S00_AXI_HIGHADDR 0x8001FFFF

The important definition in this block is XPAR_AXI_SLAVE_TEST_0_S00_AXI_BASEADDR which is the address of the slv_reg0 of the AXI Slave Test. To access and modify the register values add the include #include "xbasic_types.h after #include "xparameters.h" to the helloworld.c and the following code block after the Hello World print:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Address AXI slave test
volatile Xuint32* axi_slave_test_baseaddr_p = (Xuint32*) XPAR_AXI_SLAVE_TEST_0_S00_AXI_BASEADDR;

volatile Xuint32* axi_slave_test_reg_0_p = axi_slave_test_baseaddr_p;
volatile Xuint32* axi_slave_test_reg_1_p = axi_slave_test_baseaddr_p+1;
volatile Xuint32* axi_slave_test_reg_2_p = axi_slave_test_baseaddr_p+2;
volatile Xuint32* axi_slave_test_reg_3_p = axi_slave_test_baseaddr_p+3;

xil_printf("READ: axi_slave_test_reg_0 0x%x\n\r", *axi_slave_test_reg_0_p);
xil_printf("READ: axi_slave_test_reg_1 %d\n\r", *axi_slave_test_reg_1_p);
xil_printf("READ: axi_slave_test_reg_2 %d\n\r", *axi_slave_test_reg_2_p);
xil_printf("READ: axi_slave_test_reg_3 0x%x\n\r", *axi_slave_test_reg_3_p);

xil_printf("WRITE\n\r");
*axi_slave_test_reg_0_p = 0xFF00FF00;
*axi_slave_test_reg_1_p = 23;
*axi_slave_test_reg_2_p = 42;
*axi_slave_test_reg_3_p = 37;

xil_printf("READ: axi_slave_test_reg_0 0x%x\n\r", *axi_slave_test_reg_0_p);
xil_printf("READ: axi_slave_test_reg_1 %d\n\r", *axi_slave_test_reg_1_p);
xil_printf("READ: axi_slave_test_reg_2 %d\n\r", *axi_slave_test_reg_2_p);
xil_printf("READ: axi_slave_test_reg_3 0x%x\n\r", *axi_slave_test_reg_3_p);

This C code block assigns the registers slv_reg0 to slv_reg3 of the AXI Slave Test to the axi_slave_test_reg_0_p to axi_slave_test_reg_3_p pointer variables. The Xuint32* dataype makes sure that increasing the address by 1 returns the next correct address considering the register length. Since axi_slave_test_reg_0_p are pointers which contain addresses, reading and writing to registers requires dereferencing the pointers using the asterisk operator *.

The first register read accesses happen in the first four xil_printf statements. Those return the initial values of the registers. When running the program on the ARM CPU (PS) you should get the following output:

1
2
3
4
5
Hello World
READ: axi_slave_test_reg_0 0x1
READ: axi_slave_test_reg_1 0
READ: axi_slave_test_reg_2 0
READ: axi_slave_test_reg_3 0xDEADBEEF

slv_reg0 returns 1 because of the changes done in the Verilog code of AXI slave test which always adds 1 to the value stored in slv_reg0. slv_reg1 and slv_reg2 return 0 which is the initial register value after a reset which happens every time a program is transferred to the board using Vitis. slv_reg3 returns 0xDEADBEEF because this register has been set to a constant value in the Verilog code.

After the initial read from the AXI Slave Test registers, new values are written and immediately read again to check if the write was successful. The output after the write should be:

1
2
3
4
5
6
WRITE
READ: axi_slave_test_reg_0 0xFF00FF01
READ: axi_slave_test_reg_1 23
READ: axi_slave_test_reg_2 42
READ: axi_slave_test_reg_3 0xDEADBEEF
Successfully ran Hello World application

slv_reg0 returns the written value plus 1, slv_reg1 and slv_reg2 return the actually written values, and slv_reg3 returns the constant value 0xdeadbeef. This validates the correct function of the AXI Slave Test and the C code.

To address the PL-LEDs the AXI GPIO slave has to be addressed. Looking into the xparameters.h file, you can find a set of defines for AXI GPIO slave:

1
2
3
4
5
6
7
8
9
/* Definitions for driver GPIO */
#define XPAR_XGPIO_NUM_INSTANCES 1

/* Definitions for peripheral AXI_GPIO_0 */
#define XPAR_AXI_GPIO_0_BASEADDR 0x80000000
#define XPAR_AXI_GPIO_0_HIGHADDR 0x8000FFFF
#define XPAR_AXI_GPIO_0_DEVICE_ID 0
#define XPAR_AXI_GPIO_0_INTERRUPT_PRESENT 0
#define XPAR_AXI_GPIO_0_IS_DUAL 0

Again using the definition for the base address XPAR_AXI_GPIO_0_BASEADDR allows for writing to the GPIO register which is connected to the 8 PL-LEDs. Add the following code to after the previously added block turns on the PL LEDs 5 (D13) and 8 (D17):

1
2
3
4
5
6
7
8
// Address PL LEDs
volatile Xuint8* axi_gpio_baseaddr_p = (Xuint8*) XPAR_AXI_GPIO_0_BASEADDR;

xil_printf("WRITE\n\r");
*axi_gpio_baseaddr_p |= 0x1 << 4;
*axi_gpio_baseaddr_p |= 0x1 << 7;

xil_printf("READ: axi_gpio_baseaddr_p 0x%x\n\r", *axi_gpio_baseaddr_p);

PL-LEDs

The register of the AXI GPIO slave is 8 bits long, while each bit corresponds to one LED.

This concludes this tutorial on how to setup a Vivado and Vitis project for the Zynq UltraScale+ board using AXI slaves and addressing them in C code.

PS: if you want to leave Picocom press [CTRL]+[A] and then [CTRL]+[X] in the terminal in which Picocom is running

Complete C Program Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xbasic_types.h"


int main() {
    init_platform();

    print("Hello World\n\r");

    // Address AXI slave test
    volatile Xuint32* axi_slave_test_baseaddr_p = (Xuint32*) XPAR_AXI_SLAVE_TEST_0_S00_AXI_BASEADDR;

    volatile Xuint32* axi_slave_test_reg_0_p = axi_slave_test_baseaddr_p;
    volatile Xuint32* axi_slave_test_reg_1_p = axi_slave_test_baseaddr_p+1;
    volatile Xuint32* axi_slave_test_reg_2_p = axi_slave_test_baseaddr_p+2;
    volatile Xuint32* axi_slave_test_reg_3_p = axi_slave_test_baseaddr_p+3;

    xil_printf("READ: axi_slave_test_reg_0 0x%x\n\r", *axi_slave_test_reg_0_p);
    xil_printf("READ: axi_slave_test_reg_1 %d\n\r", *axi_slave_test_reg_1_p);
    xil_printf("READ: axi_slave_test_reg_2 %d\n\r", *axi_slave_test_reg_2_p);
    xil_printf("READ: axi_slave_test_reg_3 0x%x\n\r", *axi_slave_test_reg_3_p);

    xil_printf("WRITE\n\r");
    *axi_slave_test_reg_0_p = 0xFF00FF00;
    *axi_slave_test_reg_1_p = 23;
    *axi_slave_test_reg_2_p = 42;
    *axi_slave_test_reg_3_p = 37;

    xil_printf("READ: axi_slave_test_reg_0 0x%x\n\r", *axi_slave_test_reg_0_p);
    xil_printf("READ: axi_slave_test_reg_1 %d\n\r", *axi_slave_test_reg_1_p);
    xil_printf("READ: axi_slave_test_reg_2 %d\n\r", *axi_slave_test_reg_2_p);
    xil_printf("READ: axi_slave_test_reg_3 0x%x\n\r", *axi_slave_test_reg_3_p);


    // Address PL LEDs
    volatile Xuint8* axi_gpio_baseaddr_p = (Xuint8*) XPAR_AXI_GPIO_0_BASEADDR;

    xil_printf("WRITE\n\r");
    *axi_gpio_baseaddr_p |= 0x1 << 4;
    *axi_gpio_baseaddr_p |= 0x1 << 7;

    xil_printf("READ: axi_gpio_baseaddr_p 0x%x\n\r", *axi_gpio_baseaddr_p);

    print("Successfully ran Hello World application");
    cleanup_platform();
    return 0;
}
This post is licensed under CC BY 4.0 by the author.