Vivado design with custom Programmable Logic
- Introduction
- Project Creation
- Block Design
- Creating a custom AXI IP
- Using the custom AXI IP
- Source Files Generation
- Constraining the Design
- Design Implementation
- Bitstream Generation
- Exporting the Hardware
Introduction
In this Vivado design example, we want to build a system in which the Programmable Logic includes actual IP Cores in order to test how we can build software that handles this custom peripherals. Specifically, our design will include two different IP Cores:
- An AXI Timer from the Xilinx libraries in Vivado, that will be used to generate periodical interrupts that will be served by the Processing System.
- A custom LED driver AXI Peripheral, that will allow us to learn the basics of custom peripheral creation and that can be used to handle the LEDs connected to the Ultra96-V2.
As a first step, we load the environment for Vitis and introduce the locale fix (your installation path my differ, adjust the command properly):
source /tools/Xilinx/Vitis/2019.2/settings64.sh
export LC_ALL="C"
After we have done this, we can launch Vivado:
vivado
Once done, we will see the Vivado opening page:
Project Creation
To create a new hardware design, just click on Create Project and the following screen will pop-up:
We click next and, in the next screen, we need to chose the folder path we created to store our Vivado projects and introduce the name for this project, i.e. ultra96v2_custom:
Once done, we click Next and a screen will appear in which we are asked for the type of project we want to create. Make sure RTL is selected and click Next again:
Now, the Add Sources screen appears. For this project, we don't need to add any HDL sources, but we need to choose the desired default HDL Language. In the design flow, Vivado will automatically generate some HDL sources from our design selections, so we need to choose Verilog or VHDL. For this project, there is not any functional difference, but we select VHDL as this is the most used HDL language at CERN and then click on Next:
The next screen asks for design constraints files. In this case, this is not necessary, so we just leave that empty and click Next:
Now, a screen will appear in which we can choose the default part for our project:
In this case, we won't use the default part, but will select the appropriated predefined board. In the initial bring-up, we included the procedure to provide support for Ultra96-V2 in Vivado, so this board should appear in the board selection tab. If this is the case, we just select this board entry and click Next:
Finally, as the last part of the project creation procedure, a Project Summary screen will appear in which our previous selection can be reviewed. After a quick look to check that everything is correct, we can click Finish to create the project:
If the process is successful, the Vivado GUI will appear with the initial contents for our fresh project:
Block Design
The core element in a Zynq UltraScale+ MPSoC design built with Vivado is the Block Design. In this way, we need to create a new block design from the Flow Navigator tab in the left of the GUI. Specifically, we need to click on:
- IP INTEGRATOR
- Create Block Design
Once done, a dialog window will pop-up in which we can select the name of our design. We can left that with the design_1 default without problems and click OK:
After doing this, Vivado will create an empty Diagram in which we can add IP components from the integrated default libraries:
Processing System Block
Now, as a first step, we need to add the Zynq UltraScale Processing System IP block in the diagram. In order to do that, we push in the + icon in the diagram and a menu with the list of the available IPs will appear:
In order to filter the available IPs, we can write zynq in the search field and this will limit the available IPs to the only one that matches the criteria, i.e. the Zynq UltraScale+ MPSoC:
After we push Enter to choose the MPSoC IP, the diagram will be updated and the default Processing System block will appear:
Board Preset
If we take a look to the top of the design area, we will see a message stating that the Designer Assistance is available. In order to access to this assistance wizard, we just need to click in the Run Block Automation link and this screen will appear:
In this dialog, we just make to be sure that Apply Board Preset option is checked and click OK. By doing this, all of the customizations in the MPSoC will be overridden by the defaults defined for the Ultra96-V2, as this is the board we selected when creating the project.
After the board presets has been applied, the Designer Assistance will disappear and we will see that the external block aspect for the MPSoC has been modified and new AXI bus interfaces have been added:
The changes generated by the presets are actually deeper, and we can check them by double-clicking the MPSoC IP Core again and entering the Re-customize IP dialog:
Now, in the PS block design map we can see that the following interfaces have been enabled:
- Low Power Domain:
- GPIO
- USB 0, USB 1
- SD/eMMC 0, SD/eMMC 1
- I2C 1
- UART 0, UART 1
- Full Power Domain:
- Display Port
Processing System Customization
In our custom design for the Ultra96-V2, we don't need graphical features. For this reason, we will use the re-customize menu to disable the Display Port peripheral. In order to do that, we can navigate the menu in the I/O Configuration or, alternatively, we can click the Display Port element in the Block Diagram map to access the specific configuration entry (we can configure all of the elements highlighted in green color). Once there, we will see that the Display Port is checked and the information about the used I/O is showed:
In order to disable this interface, we just need to un-check the Display Port entry:
Once this has been done, we can go back to the PS UltraScale+ Block Design and we will check that the Display Port is now not marked as used:
In this custom project, we are going to use the Programmable Logic, so we need to be sure that everything is properly configured.
First, we must go to the PS-PL Configuration menu by either using the Page Navigator or clicking in the associated Block Design element. Once done, we will need to expand the following entries to access the AXI Master interfaces enabled by default in the Ultra96-V2 presets:
- PS-PL Interfaces
- Master Interface
- AXI HPM0 FPD
- AXI HPM1 FPD
- Master Interface
In that configuration entry, as our design is going to be simple and we don't need a huge data-bandwith, we can safely un-check the HPM1 FPD master as shown in the image:
Now, we check that the PL to PS interrupts are enabled, so we need to access the General menu in PS-PL Configuration. We can check that these are the only signals enabled by the Ultra96-V2 presets in this menu:
Finally, we want to check the clock from the PS to the PL included in the Ultra96-V2 preset. In order to access this menu, we can accessing the Clock Configuration menu in the Page Navigator or by clicking in the Clocking element in the PS UltraScale+ Block Design map. In this menu, we can identify the following 100MHz from the PS to the PL:
- Output Clocks
- Low Power Domain Clocks
- PL Fabric Clocks
- PL0
- PL Fabric Clocks
- Low Power Domain Clocks
Once all of the PS to PL stuff has been reviewed, we can just click on OK to accept the re-customized changes and the block diagram design will be updated accordingly. Here, we can see that we have the following signals in the Processing System block:
-
Outputs
- M_AXI_HPM0_FPD: PS High Performance Master 0 from Full Power Domain.
- pl_resetn0: Reset from Processing System to Programmable Logic (active low).
- pl_clk0: Clock from Processing System to Programmable Logic (100MHz).
-
Inputs
- maxihpm0_fpd_aclk: Input clock signal for the HPM0 FPD Master.
- pl_ps_irq[0:0]: Interrupt Bus Lane from Programmable Logic to Processing System (1 bit width).
Adding the AXI Timer peripheral
Now, in order to test the PL to PS interrupts, we will add a new IP clicking on the *'+' button and will search for an AXI Timer:
When done, we will see that a new axi_timer_0 instance is added to the design. Note that we can click and drag the components for proper visualization and a more ordered block design:
Now, if we take a look to the upper side of the design view, we will see a message telling us that there is Design Assistance available to Run Connection Automation. If we click on this message, a new dialog will appear that will help us to connect the AXI Timer slave in Programmable Logic to the Master in Processing System:
We can review the different connections for the S_AXI AXI Slave in the axi_timer_0 instance, but we can safely left them as default:
- Master interface: only M_AXI_HPM0_FPD is available, so this is a fixed selection.
- Bridge IP: in order to connect one of more Master devices to one or more Slave devices, we need an AXI Interconnect block. As we don't have any AXI Interconnect in our design yet, the only option is to create New AXI Interconnect.
-
Clock sources: for all the clock sources in the design, we leave the Auto selection, but we have the following options:
- use the already available 100MHz pl_clock0 (this will be the default option).
- create a new Clocking wizard (a wrapper for on-device PLLs).
- use an external port to drive the AXI bus.
Once we click OK, we will see that the design is automatically wired and that there are two new IP blocks in the design:
- ps8_0_axi_periph: the new AXI Interconnect to bridge the PS master and the PL timer slave.
- rst_ps8_0_100M: a new Processor System Reset soft IP connected to 100MHz that provides a mechanism to handle the reset conditions for the system.
Here is what the connected design looks like after a little block re-arranging (the connections will be maintained, so feel free of composing the system as per your preferences):
Now, we want the AXI Timer to produce a periodic interrupt signal that will be served by the Processing System. In order to do this, we need to connect the interrupt output in the AXI Timer to the pl_ps_irq bus by click and hold the mouse in one of the signals and dragging to the other one (note that the widths match, otherwise we should use an intermediate Concat IP block):
In this point, we have a complete custom design including an AXI Timer in the Programmable Logic.
Partial Block Design Validation
To verify that everything is OK in our block design, we can click in the Validate Block Design button or use the F6 keyboard shortcut:
If everything is correct, we will see the following message:
Finally, to save the changes in our block design, just click the Save Block Design icon in the Vivado toolbar or use the Ctrl + S keyboard shortcut.
Creating a custom AXI IP
Before generating the outputs, synthesizing our design and generating a valid bitstream, we want to create a new custom AXI IP core to exemplify how this can be done. In order to do this, click in the following entry in the Vivado menu bar:
- Tools
- Create and Package New IP
When done, the Create and Package New IP wizard dialog will prompt:
After we click Next we will be offered two options:
- Packaging options: this is used to create a new IP block from an already existing design.
- Create AXI4 Peripheral: this is used to create a new AXI4 peripheral from scratch.
We will select Create AXI4 Peripheral and click Next:
Now, we will be offered a dialog to define the new peripheral details. For our example, we will insert this values and then click on Next:
- Name: we insert my_led_driver as the name of the peripheral.
- Version: we left version v1.0 unchanged.
- Display name: the display name will be updated automatically to my_led_driver_v1.0.
- Description: optional description for the peripheral, e.g. My new LED driver AXI IP.
- IP Location: the location for the custom IP repository, defaulted to ip_repo in the same folder the our custom project is located.
In the next dialog is used to add the interfaces to our IP. Depending on our selections the number of fields will change. In this dialog, the most important entry is the interface type, as it allows us to select the three supported types of AXI4 peripherals:
- Lite: memory mapped address/data interface with single data only cycle.
- Full: memory mapped address/data interface with support for data burst.
- Stream: non addressed data-only burst support.
For our very simple led driver custom IP we can select the following options and leave the default parameters unchanged and click Next:
- Interface Type: Lite
- Interface Mode: Slave
Finally, a peripheral creation summary will appear and we will be asked for the next action to be performed. Because our custom IP is empty now, we will choose Edit IP to give it some logic and then we click Finish:
When we do this, a new Vivado window will open to allow us to edit the IP we have just created:
In the Package IP view, we can change several configuration and identification parameters about the IP, e.g. the name of the vendor for our IP. For this simple project, we can safely leave all of them unchanged.
The important thing to note is that in the Package IP, there is an IP packaging flow with several steps from top to down and all of them are marked as done with a green tick.
Modifying the AXI slave code
Now, we need to modify the code for our custom IP in order to give it some functionality. As we were working with VHDL, we can see in the Sources Hierarchy that two VHDL files have been automatically created:
- my_led_driver_v1_0.vhd: the top level wrapper for our IP core.
- my_led_driver_v1_0_S00_AXI.vhd: entity declaration and architecture implementation for the Slave interface.
First, we start editing the slave interface by double clicking in the my_led_driver_v1_0_S00_AXI.vhd file. This will open the file in the editor. Now, we need to apply this modifications:
- Declare a led_driver output of two bit-width, as we have two LEDs connected to the PL in the Ultra96v2.
- Assign to the led_driver the less significant bits from the lower data register in the slave, i.e. slv_reg0.
Use this patch as a briefing or download the modified my_led_driver_v1_0_S00_AXI.vhd:
--- old/my_led_driver_v1_0_S00_AXI.vhd 2020-06-08 15:00:15.409863286 +0200
+++ new/my_led_driver_v1_0_S00_AXI.vhd 2020-06-08 15:20:10.906719669 +0200
@@ -16,7 +16,7 @@
);
port (
-- Users to add ports here
-
+ led_driver : out std_logic_vector(1 downto 0);
-- User ports ends
-- Do not modify the ports beyond this line
@@ -385,7 +385,7 @@
-- Add user logic here
-
+ led_driver <= slv_reg0(1 downto 0);
-- User logic ends
end arch_imp;
Then, we need to edit the top wrapper by double clicking in the my_led_driver_v1_0.vhd file. This will open the file in the editor. Now, we need to apply this modifications:
- Declare a led_driver output of two bit-width to access from the outside of the IP component.
- Add led_driver in the component declaration for my_led_driver_v1_0_S00_AXI.
- Connect led_driver output in the component instantiation for my_led_driver_v1_0_S00_AXI.
Use this patch as a briefing or download the modified my_led_driver_v1_0.vhd:
--- old/my_led_driver_v1_0.vhd 2020-06-08 15:00:15.409863286 +0200
+++ new/my_led_driver_v1_0.vhd 2020-06-08 15:20:10.907719652 +0200
@@ -16,7 +16,7 @@
);
port (
-- Users to add ports here
-
+ led_driver : out std_logic_vector(1 downto 0);
-- User ports ends
-- Do not modify the ports beyond this line
@@ -55,6 +55,7 @@
C_S_AXI_ADDR_WIDTH : integer := 4
);
port (
+ led_driver : out std_logic_vector(1 downto 0);
S_AXI_ACLK : in std_logic;
S_AXI_ARESETN : in std_logic;
S_AXI_AWADDR : in std_logic_vector(C_S_AXI_ADDR_WIDTH-1 downto 0);
@@ -88,6 +89,7 @@
C_S_AXI_ADDR_WIDTH => C_S00_AXI_ADDR_WIDTH
)
port map (
+ led_driver => led_driver,
S_AXI_ACLK => s00_axi_aclk,
S_AXI_ARESETN => s00_axi_aresetn,
S_AXI_AWADDR => s00_axi_awaddr,
Once we have finished editing the files, we save both of them.
Re-packaging the AXI IP
and will notice that in the Package IP some of the green ticks in the IP packaging flow have gone now, so we have to update and re-package our IP component:
This is the sequence of actions to re-package the IP component in each of the unmarked tabs:
-
File Groups: we need to click on:
- Merge changes from File Groups Wizard
-
Customization Parameters: we need to click on:
- Merge changes from Customization Parameters Wizard
-
Ports and Interfaces: this should be marked as green now and the led_driver signal added as an output port. Otherwise, click on:
- Merge changes from Ports and Interfaces Wizard
-
Customization GUI: this should be marked as green now and the led_driver port added to the schematic GUI component. Otherwise, click on:
- Merge changes from Customization GUI Wizard
Once we have finished this steps, all of the steps but Review and Package should be marked as green. If we go to this last entry, we see the following screen, in which we must click Re-Package IP to finish the process:
If the IP packaging process success, we will get a dialog reporting this and asking for confirmation to close the IP core project. We can safely click on Yes to end the IP Core creation (this project can be opened at any time later if we need to modify the IP):
Using the custom AXI IP
Once the packaging has finished, in our Vivado ultra96v2_custom project, we can go to the following menu entry to check that the new custom IP repository for my_led_driver is available:
- Tools
- Settings
Here, in the Project Settings, under IP > Repository, we can see the currently available IP repositories and add or remove new locations to a Vivado project:
After this verification, we can go to the block design view and check that the my_led_driver_v1.0 is available now by clicking in the add IP button "+" and searching for it:
We double click in the entry and a disconnected new instance of the LED driver will be added to the block design:
If we take a look to the upper side of the block design view, we can check that we have Designer Assistance available and we can click in Run Connect Automation to integrate our custom LED driver in the design. Once done, we will have the following dialog:
We can check that, because we have a working Bridge IP connected to the AXI Master, all of the options for connection are now grayed out but the Clock source for Slave interface, which is set to Auto. We can safely left this unchanged and click OK, and the my_led_driver_0 will be connected to the 100Mhz pl_clk0 signal. After a little of re-arranging blocks, this is what our system looks like:
AXI Address Space
Before going forward, we can take a look to the Adress Editor tab for informative purposes. Here, we can check and modify the address space for our AXI peripherals inside the AXI HPM0 FPD region of the memory map, but we will leave it unchanged for our example:
Adding an external port
Now, back in the Diagram tab, we need to add a port from the PL to the outside world so we can drive the LEDs in the Ultra96-V2 board. In order to do this, we click on the diagram with the secondary mouse button and select Create Port in the contextual menu -- alternatively, we can use the (Ctrl+K) keyboard shortcut:
This will open a dialog that allow us to define new ports for the block design. In our case, we will select this values and click OK:
- Port name: we will insert led_driver
- Direction: we will select Output
- Type: we will select Other, as this is not a special purpose signal.
- Create vector: we will create a vector from 1 to 0 to match the LED driver width.
Once done, a new output port named led_driver[1:0] will be added to the block design. Now, we need to connect this port to the led_driver[1:0] output in the my_led_driver_0 instance:
Now, our complete custom block design is ready.
Complete Block Design Validation
To verify that everything is OK in our block design, we can click in the Validate Block Design button or use the F6 keyboard shortcut:
If everything is correct, we will see the following message:
Finally, to save the changes in our block design, just click the Save Block Design icon in the Vivado toolbar or use the Ctrl + S keyboard shortcut.
Source Files Generation
Once we have finished and validated our standalone block design, we need to generate the source files associated to this design so that the Vivado tool is able to create our design.
If we take a look to the Sources panel Vivado in the Hierarchy tab, we can find our design_1.bd file:
Output Products
As a first step in the synthesis process, we need to generate the Output Products for our block design, i.e. the generated files produced for an IP based design block and its customization. They can include HDL, constraints, and simulation targets and many other stuff. Is important to note that the generated HDL files will use the default HDL language (Verilog or VHDL) we set for our Vivado project. In order to do this, just click with the secondary mouse button in the design_1.bd file and select Generate Output Products in the contextual menu:
Once done, a menu dialog will appear with the output products generation configuration and will ask for confirmation. Just click on Generate and the process will start:
After a while, a message dialog will appear stating that the generation process will be continue in an Out-of-Context way. This means that we will have access to Vivado while the process is being executed in the background:
In this way, in order to know when the project has actually finished, we need to observe the information messages that will appear in the top-right side of the Vivado GUI, e.g.:
When the process has finished, the message will be Ready.
HDL Wrapper
Now, we need to create the HDL Wrapper for our design, i.e. the file that will act as the top level HDL file for our design. Once again, the HDL language for this file will be the one we selected as default for our project (VHDL). In order to launch this process, just click with the secondary mouse button on the design_1.bd and select Create HDL Wrapper in the contextual menu:
Once done, a dialog will appear asking for the way in which we want to manage the HDL wrapper. Be sure that the Let Vivado manage wrapper and auto-update is selected and click OK:
Once the process has finished, we can check the Sources panel in the Hierarchy tab that the design_1_wrapper.vhd HDL file is now the top of the hierarchy and the design_1.bd appears just below it:
Constraining the Design
The next step before implementing our design, is specifying how to connect the led_driver port to the PL package pins connected to LEDs in the Ultra96-V2. In order to do this, we need to create a new design constraints file.
In order to add new source files, select the following entry in the Vivado menu bar or use the (Ctrl+A) keyboard shortcut:
- File
- Add Sources
In the Add Sources dialog, select the Add or created constraints option and click Next:
In Vivado, you can handle multiple constraints in a single set, so we select the active constraints set, i.e. constr_1 and click on Create File:
In the next dialog, we select the name of the constraints file as ultra96v2_custom and click OK:
Now, we have an updated list with the user constraints in the active set, including the only one, i.e.: ultra96v2_custom.xdc. We could add or create as many constraints as necessary, but this is all that we need for our project so we just click finish:
In the Sources Hierarchy we can find the newly created ultra96v2_custom.xdc file, that we will open by double clicking on it:
When the ultra96v2_custom.xdc file opens in the editor, we can check that it's totally empty. In order to connect our led_driver port to the LEDs connected to the PL in the Ultra96-V2, i.e. the blue and yellow ones for radio status, we copy this content in the constraints file and save by using the Save File icon in the editor or the (Ctrl+S) keyboard shortcut:
# Radio Led 1 in the Ultra96-V2 (Blue)
set_property -dict { PACKAGE_PIN B9 IOSTANDARD LVCMOS18 } [get_ports { led_driver[1] }];
# Radio Led 0 in the Ultra96-V2 (Yellow)
set_property -dict { PACKAGE_PIN A9 IOSTANDARD LVCMOS18 } [get_ports { led_driver[0] }];
Design Implementation
Now, in order to implement our design, we need to click on the following entry of the Flow Navigator:
- IMPLEMENTATION
- Run Implementation
After this, as we have not previously run the synthesis, the following informative dialog will appear:
We can safely accept and the Launch Runs dialog will appear. Be sure that we have selected Launch runs on local host option is selected and click OK to initiate the synthesis and implementation processes:
As the processes will be run in background and you can follow the progress in the information messages showed in the top-right of the Vivado GUI, e.g.:
When the implementation process has finished, a message dialog will pop-up asking for further steps:
In order to check the placement for the used logic resources, we can select the Open Implemented Design option. When done, we will see the following view of the device:
Bistream Generation
Once we are up with reviewing the device implementation, we can generate the bitstream. In order to do this, we use the Flow Navigator and click on the associated entry:
- PROGRAM AND DEBUG
- Generate Bitstream
After we do this, we will be asked for confirmation in the launch runs dialog:
We will get this confirmation if the bitstream generation successfully completed. We can click OK to view the report or just click Cancel to go back to Vivado with no further action:
Exporting the Hardware
Finally, in order to use our custom Zynq UltraScale+ MPSoC design in software development, we need to Export the Hardware to a XSA file. In order to do this, we select the following entry in the Vivado menu:
- File
- Export
- Export Hardware
- Export
After doing this, the Export Hardware dialog will appear. In this dialog, we will change the default name to ultra96v2_custom. Before clicking OK, we need to be sure that the Include bitstream option is checked, as part of the design is implemented in the Programmable Logic:
Once the process has finished, we are ready to use the generated ultra96v2_custom.xsa file for software development.