The TE0726 Zynqberry FPGA Development board from Trenz Electronic is a fun little Zynq-based board that comes in the form factor of a Raspberry Pi 2. Because of its Raspberry Pi form factor, I think it's a great little board to really figure out how to upgrade projects with an FPGA by walking through the exercise of creating designs to utilize any of the vast number of Pi pHATs available on the market today.
So this project serves as a starting point for a basic hardware design to utilize the Zynqberry's USB ports, Ethernet, and 40-pin GPIO header to make it compatible with Raspberry Pi pHATs. It is also worth noting that the Zynqberry has a few different versions available.
The Zynq-7000 chip from AMD that the Zynqberry uses is a SoC (System on Chip) meaning that the programmable logic of the FPGA also has a physical ARM Cortex processor built into it. There are versions of the Zynq that have a dual-core ARM Cortex-A9 and there are versions of the Zynq that have a single-sore ARM Cortex-A9. This accounts for a difference in some of the available versions of the Zynqberry: there is a version available with the single-core Zynq and other versions available with the dual-core Zynq. The hardware design I'll be creating in this project post can be applied to either, the only difference being which Zynqberry board selected as the target in the initial project creation.
Note: I'm using Vivado 2022.1 in this project. The following steps should be applicable back to 2021.1 and up to 2022.2, but I have not explicitly tested it as such.
Create Vivado ProjectLaunch Vivado and walk through the project creation wizard to give the project the desired name and directory location. The project type will be an RTL project that is not a Vitis extensible platform.
Select the Zynqberry board version being used in the Boards tab for the project to target. I happen to be using the commercial grade dual-core version with 512MB of DDR, but double-check your part number for whichever version you happen to be using.
This is very important to select the correct version of the Zynqberry you are using because even though you'll be able to successfully generate a bitstream, that bitstream won't work on the board as it was not compiled for that Zynq part.
Confirm the project settings in the summary page of the New Project Wizard, then click Finish to generate the new project.
Block DesignThere are two ways to instantiate HDL IPs in a Vivado. Either manually in HDL source files or using the graphical block design GUI. Using the block design is ideal due to its auto-connection capabilities for standard interfaces like AXI.
Select Create Block Design from the Flow Navigator window. Name the block design as desired:
First, add the Zynq Processing System for configuration of the ARM Cortex in the Zynq-7000 chip. Click the + button and search for "Zynq" and it'll be the only option.
A green banner will appear with the option to Run Block Automation. This is where Vivado will apply the configuration to the Zynq PS IP according to what the target FPGA development board is set to for the project. In this case, since the Zynqberry was chosen as the target development board when the project was created, Vivado will apply the Zynq PS configuration for the Zynqberry (ie - which MIO peripherals are connected, external clock source speeds, etc.)
Click the Run Block Automation hyperlink in the green banner, and click OK in the pop up window (leaving all selections as their defaults):
While Vivado has already taken care of configuring the Zynq PS for everything that is hardwired to it on the Zynqberry PCB, there is still the option to utilize the unused peripherals via the programmable logic (PL) by connecting them via the Zynq's EMIO. In this case, we're going to connect GPIO, SPI, and I2C interfaces to the EMIO so we can route them to the 40-pin GPIO header on the Zynqberry so it'll be the exact same as on a Raspberry Pi.
Double-click on the Zynq PS IP to open its configuration window. Enable SPI0, SPI1, and I2C0 and connect them to the EMIO:
Enable 18 GPIO pins connected through the EMIO as well:
Click OK to save and close the new configuration for the Zynq PS. You'll notice that new ports will have appeared for the GPIO, SPI, and I2C interfaces:
To make the interfaces available to the world outside of the block design, ports need to be created for each signal/bus. This can be done with the "Make External" option accessible by either right-clicking on the signal/bus then selecting "Make External" or single left-clicking on the signal/bus to highlight it and pressing crtl+T.
First, make the GPIO bus external:
Renaming it is optional, but I decided to rename it to RPi_GPIO for clarity.
Make the I2C interface bus external and optionally rename it to RPi_I2C.
For the SPI1 interface, click the + button next to to it to expand the interface bus and reveal the signal list. Since I don't need the functionality for any of my Raspberry Pi pHATs to switch between being the master/slave on the SPI bus, I can save myself some configuration here in the FPGA design by only connecting the pins on the SPI bus for the Zynqberry to be the master and the Raspberry Pi pHAT to be the slave.
- SPI SCLK output
- SPI MOSI output
- SPI MISO input
- SPI SS output
Again, I renamed each to RPI_SPI_*:
The SPI SS input does need to be tied high to a voltage rail however. So add a constant IP to the block design with an output value of 1.
Then connect it to SPI_SS_I:
Finally, connect FCLK_CLK0 back to M_AXI_GP0_ACLK on the Zynq PS.
Validate the block design to check for any critical warnings or errors by clicking the little checkbox icon at the top of the Diagram window.
If there are no errors or critical warnings, save the block design and generate it.
Select Generate Block Design from the Flow Navigator window. Select Global for Synthesis Options in the pop up window then click Generate.
Wait for the output products of the block design to be successfully generated.
With the block design complete, it now needs to be instantiated in the project via an HDL file. This file can be auto-generated in Vivado by right-clicking on the block design file in the Sources window and selecting Create HDL Wrapper...
Keep in mind that Vivado will create this wrapper HDL file in whichever language is set as the default in the project settings (VHDL or Verilog). The default is Verilog so if you want the HDL wrapper to be generated in VHDL, select Settings from the Flow Navigator window and change Target language under the General tab to VHDL.
A pop-up window will ask if you prefer to allow Vivado to auto-manage the wrapper or not (ie - Vivado will automatically update it if/when external ports are added). I select the option to allow Vivado to auto-manage the wrapper.
Wait for the HDL wrapper file to be generated and the hierarchy in the Sources window to update:
Since we connected the GPIO, SPI, and I2C interfaces through the PL via the Zynq EMIO, we need to specify which package pins on the Zynq FPGA the go to via a constraints file.
Note: Only signals connected via the PL/EMIO have to be specified in a constraints file. MIO peripheral connections are hardwired straight to the ARM Cortex processor.
Select Add Sources from the Flow Navigator window.
In the pop-up window, select to Add or create constraints. On the next page, create a new constraints file with the desired name.
Click Finish and wait for the new constraints file to appear in the Sources window.
Open the file and the pin assignments for the Zynqberry's GPIO header based on its schematic (be sure to use the signal names you assigned in the block design if they are different from mine):
# Common BITGEN related settings for TE0726
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]
set_property BITSTREAM.CONFIG.UNUSEDPIN PULLUP [current_design]
# RPi GPIO Pins
## GPIO2
#set_property PACKAGE_PIN K15 [get_ports {RPi_GPIO_tri_io[0]}]
## GPIO3
#set_property PACKAGE_PIN J14 [get_ports {RPi_GPIO_tri_io[1]}]
# GPIO4
set_property PACKAGE_PIN H12 [get_ports {RPi_GPIO_tri_io[0]}]
# GPIO5
set_property PACKAGE_PIN N14 [get_ports {RPi_GPIO_tri_io[1]}]
# GPIO6
set_property PACKAGE_PIN R15 [get_ports {RPi_GPIO_tri_io[2]}]
# GPIO7
set_property PACKAGE_PIN L14 [get_ports {RPi_GPIO_tri_io[3]}]
## GPIO8
#set_property PACKAGE_PIN L15 [get_ports {RPi_GPIO_tri_io[6]}]
## GPIO9
#set_property PACKAGE_PIN J13 [get_ports {RPi_GPIO_tri_io[7]}]
## GPIO10
#set_property PACKAGE_PIN H14 [get_ports {RPi_GPIO_tri_io[8]}]
## GPIO11
#set_property PACKAGE_PIN J15 [get_ports {RPi_GPIO_tri_io[9]}]
# GPIO12
set_property PACKAGE_PIN M15 [get_ports {RPi_GPIO_tri_io[4]}]
# GPIO13
set_property PACKAGE_PIN R13 [get_ports {RPi_GPIO_tri_io[5]}]
# GPIO16
set_property PACKAGE_PIN L13 [get_ports {RPi_GPIO_tri_io[6]}]
# GPIO17
set_property PACKAGE_PIN G11 [get_ports {RPi_GPIO_tri_io[7]}]
# GPIO18
set_property PACKAGE_PIN H11 [get_ports {RPi_GPIO_tri_io[8]}]
# GPIO19
set_property PACKAGE_PIN R12 [get_ports {RPi_GPIO_tri_io[9]}]
# GPIO20
set_property PACKAGE_PIN M14 [get_ports {RPi_GPIO_tri_io[10]}]
# GPIO21
set_property PACKAGE_PIN P15 [get_ports {RPi_GPIO_tri_io[11]}]
# GPIO22
set_property PACKAGE_PIN H13 [get_ports {RPi_GPIO_tri_io[12]}]
# GPIO23
set_property PACKAGE_PIN J11 [get_ports {RPi_GPIO_tri_io[13]}]
# GPIO24
set_property PACKAGE_PIN K11 [get_ports {RPi_GPIO_tri_io[14]}]
# GPIO25
set_property PACKAGE_PIN K13 [get_ports {RPi_GPIO_tri_io[15]}]
# GPIO26
set_property PACKAGE_PIN L12 [get_ports {RPi_GPIO_tri_io[16]}]
# GPIO27
set_property PACKAGE_PIN G12 [get_ports {RPi_GPIO_tri_io[17]}]
set_property IOSTANDARD LVCMOS33 [get_ports {RPi_GPIO_tri_io[*]}]
# I2C SDA (GPIO2)
set_property PACKAGE_PIN K15 [get_ports RPi_I2C_sda_io]
# I2C SCL (GPIO3)
set_property PACKAGE_PIN J14 [get_ports RPi_I2C_scl_io]
set_property IOSTANDARD LVCMOS33 [get_ports RPi_I2C_*]
# SS/CE0 (GPIO8)
set_property PACKAGE_PIN L15 [get_ports RPi_SPI_SS]
# MISO (GPIO9)
set_property PACKAGE_PIN J13 [get_ports RPi_SPI_MISO]
# MOSI (GPIO10)
set_property PACKAGE_PIN H14 [get_ports RPi_SPI_MOSI]
# SCLK (GPIO11)
set_property PACKAGE_PIN J15 [get_ports RPi_SPI_SCLK]
set_property IOSTANDARD LVCMOS33 [get_ports RPi_SPI_*]
Save and close the constraints file.
Generate BitstreamThe with design complete and the signals routed to the proper pins it's time to synthesize the design, run place & route it (called implementation in Vivado), and generate a bitstream for it.
So save myself a few button clicks, I just immediately select Generate Bitstream from the Flow Navigator window and Vivado will take care of automatically running synthesis and implementation for me (as the pop-up window below is telling me):
Leave the default options to launch all of the runs on the local host and click OK:
Wait for Vivado to run through everything and output a bitstream, the time for which will depend on the specs of your host PC.
You can open the implemented design to get a better idea of how the design translated onto the Zynq FPGA.
With the hardware design complete, the last thing to do is export it in AMD's XSA format to be able to develop the software to run on the Zynq's ARM processors in Vitis and/or PetaLinux.
Select File > Export > Export Hardware... Be sure to select the option to include the bitstream in the exported hardware on the second page.
Give the XSA file the desired name (it will default to the same name as the HDL wrapper file) and desired export location (default is the top level directory of the Vivado project itself, which I usually where I put it).
Once the XSA file is exported, it can be imported into either Vitis or PetaLinux to build the software design to run on the ARM Cortex processors of the Zynq-7000.
Comments