The Vitis High-Level Synthesis tool (HLS) is a handy tool for converting high level C/C++ code into RTL that can be packaged and exported as an IP block to use in a Vivado block design to instantiated directly in an RTL source file. As someone that’s proficient in RTL (VHDL/Verilog), I find HLS to be super handy for certain operations/functions that are a pain to code directly in RTL, the prime example of which being division by any number other than a power of 2.
So for this project, I’m using Vitis HLS to code a simple scalar division IP block with an ALX4-Lite interface to write the dividend and divisor into the IP block and read the quotient out. To test and verify the functionality of the IP block, I’ll be running Pynq in an embedded Linux image on the target FPGA and using Pynq’s Overlay design methodology.
The term “overlay” in this context comes from the concept of device tree overlays in Linux which is where a device tree overlay file is a device tree fragment that can be loaded/unloaded dynamically at runtime to add/remove device nodes for the kernel to access.
Coupled with the FPGA manager in embedded Linux on an FPGA, this allows new bitstreams to also be loaded/unloaded dynamically at runtime of the Linux OS. This is what the Pynq Overlay design flow does: dynamically reprograms the PL of the FPGA with the bitstream containing the target IP for testing, memory maps the IP, and connects it to GPIO so that it can be accessed with Python function calls (read more here).
Create IP in Vitis HLSSo to start, source the Vitis HLS environment then launch Vitis HLS and create a new project:
~$ source /tools/Xilinx/Vitis_HLS/2021.2/settings64.sh
~$ vitis_hls
Select desired project name and project directory location. I personally like to create a folder within the top level directory of the Vivado project I’m targeting the IP for (So I actually create the Vivado project first even though I have that step listed later here - it’s completely up to your personal preference).
Unlike Vivado and Vitis (SDK) where I usually create new source files from within the GUI, I’ve found that with Vitis HLS, it’s easier to create the C/C++ source files separately using your preferred text editor. So I went ahead and coded the divide function up in C in the text editor and saved it to then import into the new HLS project:
void divide(int a, int b, int& c) {
#pragma HLS INTERFACE ap_ctrl_none port=return
#pragma HLS INTERFACE s_axilite port=a
#pragma HLS INTERFACE s_axilite port=b
#pragma HLS INTERFACE s_axilite port=c
c = a / b;
}
Note: the extension you save the source file in determines the syntax it’s compiled in in HLS, so.c for C and.cpp for C++.
After importing the file, select the top function (effectively designating that function as main).
Usually you would use a test bench in HLS for your design, which importing/specifying the test bench is the next step in creating a new HLS project. But I’m being lazy here and I’m just going to roll that into the prove-in I’m doing using Pynq, so I didn’t import any test benches (just click next).
Next, select the target FPGA chip for the project. I’m using a Pynq-Z1 board, and the Pynq-Z1 isn’t available in Boards tab so I just specified the FPGA chip the Pynq-Z1 uses directly under the Parts tab (xc7z020-clg400-1). In HLS, it’s not really necessary to specify the specific development FPGA board. So don’t panic if you don’t see your FPGA dev board listed, just specify the part number of its FPGA directly.
Finally, specify the desired solution name and clock speed you plan to run the IP at when it is instantiated in the Vivado deign. I plan to run my IP at 100MHz, so the clock period is 10ns. You can also specify your own clock uncertainty, if you’re not sure just leave it blank and HLS with use the default of 12.5%.
Click Finish and HLS will load the new project:
The pragmas in the top level function have created the AXI4-Lite interface for the IP and will create a control register for each of the ports specified, so we’re ready to run c synthesis (select C Synthesis from the drop down for the green play button icon in the toolbar).
Upon successful completion, a summary report of the C synthesis will pop up:
To package the C++ application into an IP and export it to use in Vivado, select Export RTL from the drop down for the green play button icon in the toolbar.
Note: I found that instead of applying the Y2K22 patch to Vitis HLS 2021.2, I could simply add an extra dot to the version. So setting the Version to 1.0 caused me to get the invalid parameter error, but setting the Version to 1.0.1 allowed it to generate and export the RTL successfully.
With the IP ready to go, the next step is to create a Vivado project targeting the Pynq-Z1. I’m using Vivado 2021.2 so adding board files is a bit different (which I outlined in my past tutorial on the ZynqberryZero board here).
- Pynq-Z1 Vivado board files & master XDC constraints file: https://pynq.readthedocs.io/en/v2.3/overlay_design_methodology/board_settings.html
Unzip the exported IP (export.zip) from HLS into a directory then add that directory to the Vivado project as an IP repository (Settings in Flow Navigator > IP > Repository > +):
Next, create a new block design:
Add the Zynq PS and desired board peripherals, running and block automation and connection automation that appears.
Add the IP from HLS to the design (it’s not available in Vivado just like the rest of the IP blocks since we added it as an IP repository). Run the resultant connection automation to connect its AXI interface to the Zynq PS.
Validate the block design & save it. Then create the HDL wrapper to instantiate it in the design.
Run synthesis, implementation, & generate a bitstream. Pynq will need the bitstream file, block design TCL script, & hardware handoff file from Vivado in order to deploy the overlay.
Export Files from VivadoThe hardware handoff file is located in /<Vivado project directory>/<Vivado project>.gen/sources_1/bd/<bd name>/hw_handoff/<bd_name>.hwh
Then export the bitstream & block design TCL files by selecting File > Export > Export Bitstream File… and Export Block Design…
Note: the block design must be open for the Export Block Design… option to be available.
I exported everything to the same spot to make it easy to find later to upload to the Jupyter notebook on the Pynq-Z1 (it is a requirement that everything does need to be renamed to have the same name):
Prep the SD card with Pynq-Z1 image then hook up & power it on. Connect to Jupyter in browser on the host PC per the setup guide linked below.
- Pynq-Z1 SD card image: http://www.pynq.io/board.html
- Pynq-Z1 setup guide: https://pynq.readthedocs.io/en/latest/getting_started/pynq_z1_setup.html
In Jupyter, create a new folder to upload the bitstream, hardware handoff, & block design files exported in the previous steps.
Once all of the files have been uploaded, open new a python command line and start by importing the Overlay library.
from pynq import Overlay
overlay = Overlay('/home/xilinx/jupyter_notebooks/scalar_divider/scalar_divider.bit')
Query the Overlay loaded:
overlay?
Now create a function to make calls to the HLS IP easy. You need to know the AXI register offset values for the dividend, divisor, and quotient, as well as the VLNV value in Vivado.
Look up register offset values in HLS C synthesis report, and VLNV in Vivado block design.
Return to Jupyter and create the Python function:
from pynq import DefaultIP
class DivideDriver(DefaultIP):
def __init__(self, description):
super().__init__(description=description)
bindto = ['Knitronics:hls:divide:1.0']
def divide_func(self, a, b):
self.write(0x10, a)
self.write(0x18, b)
return self.read(0x20)
Then reload the Overlay with new function for the HLS IP:
from pynq import Overlay
overlay = Overlay('/home/xilinx/jupyter_notebooks/scalar_divider/scalar_divider.bit')
Query reloaded Overlay to verify driver is now associated:
overlay?
And finally test the function:
overlay.divide_0.divide_func(10,5)
And that’s it!
Comments