Recently I came around to generate some programmed digital waveforms for testing external hardware. Using a microcontroller was no option due to timing requirements. So why not using a FPGA? Time to take my old Arty Z7 out of the drawer and write some VHDL...
After finishing, I wanted to run the board stand-alone, so I needed to flash either SD card or the QSPI. While I did all this already with a Peta-Linux based project, I had no clue how to get it done for a PL only project. So, time to extend my knowledge a bit! And eventually also time to give the Python scripting capabilities a try.
After some internet research I found the video from Dom explaining how to export a PL only hardware. However, this was done on a older version and it did not work as expected.
Second, creating the Vitis platform posed also some problems, which I actually do not fully understand how they are solved.
In this project, I try to describe the full workflow and how use it efficiently when updating/testing things together with an ILA debugger. I will also try to explain, where I experienced problems and what might be possible solutions.
The whole project has been tested using Windows 11 25H2 Pro, but most things will also run on a Linux environment. Exception are the wrapping scripts, but which are not very complicated to adapt.
So, lets dive in...
Creating a Vivado ProjectOpen Vivado and create a new project named `PLProject`. You do not need to add sources, but you have to select the board you will use within this project.
Now add a constraints file, which you best import from a master xdc for your board. You can comment out everything except:
- a clock source
- 4 push button inputs
- 4 led outputs
## Clock signal
set_property -dict {PACKAGE_PIN H16 IOSTANDARD LVCMOS33} [get_ports clk]
create_clock -period 8.000 -name sys_clk_pin -waveform {0.000 4.000} -add [get_ports clk]
##LEDs
set_property -dict {PACKAGE_PIN R14 IOSTANDARD LVCMOS33} [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN P14 IOSTANDARD LVCMOS33} [get_ports {led[1]}]
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports {led[2]}]
set_property -dict {PACKAGE_PIN M14 IOSTANDARD LVCMOS33} [get_ports {led[3]}]
##Buttons
set_property -dict {PACKAGE_PIN D19 IOSTANDARD LVCMOS33} [get_ports {btn[0]}]
set_property -dict {PACKAGE_PIN D20 IOSTANDARD LVCMOS33} [get_ports {btn[1]}]
set_property -dict {PACKAGE_PIN L20 IOSTANDARD LVCMOS33} [get_ports {btn[2]}]
set_property -dict {PACKAGE_PIN L19 IOSTANDARD LVCMOS33} [get_ports {btn[3]}]Finally add 3 source files: `PLTest.vhd`, `debounce.vhd`, `EdgeDetect.vhd` and synthesize it. (You can find these files in the attachment)
Open the synthesized design and click on 'Set Up Debug' to instantiate an ILA core. Because in the code the nets to debug are already declared, these nets will be directly proposed and you can continue "Next" till "Finish" which will update the design. Now click on "Generate Bitstream".
This project is still pure PL, we will add a block diagram after debugging.
DebuggingRegular debug
When the bitstream has been written, you can connect to the device using the Hardware Manager (Be sure, your device is connected via JTAG) and download the bitstream onto the FPGA.
When selecting 'Program device' you will get presented a pop-up where you select the bitstream to download and the file for the debug probes definitions. Both are prefilled, so you can simply press 'Program'.
Now, add a trigger as shown and start recording. The system will just wait, till you press the first button. When the trigger occurs, the data is sampled and shown in the Waveform window.
Tracing start-up phase
So far the "normal" working. Because we have no reset signal input defined, an interesting point is now to see what happens during start-up of the design: we should check if all variables are set as expected in the first cycle.
To do so, we first change the trigger condition so it triggers immediately after start-up. You should test this condition before continuing.
Now switch to the Tcl console tab and enter:
run_hw_ila -force -file ila_trig.tas [get_hw_ilas hw_ila_1]Then you have to open the implemented design and - again in the Tcl console - enter 2 more commands:
apply_hw_ila_trigger ila_trig.tas
write_bitstream trig_at_startup.bit -forceThe last command will write a new bitstream containing your defined trigger. It will be written to "./trig_at_startup.bit", which is somewhere on your disk. Use "pwd" to find out your current folder. Then copy the file into the folder where "Program Device" will look for the bit file.
Now go back and click on "Program Device". In the pop-up, select the new bit file and keep the rest. Click at "Program" which will first program the device and then trigger directly a recording. Now you can check your variables in the moment of the first cycle.
Some remarks:
- when you change your code and add new variables to debug, don't forget to run the debug wizard after a first synthesis. Otherwise these variables are missing or the implementation will complain later.
- You also need to reload the implementation when doing changes on start-up trigger. When keeping it open, watch out for the "Reload" banner.
Everything till here is the core work, gaining a running PL. The steps which follow now, are only needed to create the boot file.
Preparing the Block design and exporting the designDuring the previous step, is is simpler to not have a block design in the project: things will run a bit quicker. Before we can create the boot files in Vitis, we first need to prepare the design so we can export the hardware (in other words: create a xsa file).
The steps here are:
1. Clean the code: remove the logic analyzer
2. Create a block design and import the RTL from our PL design
3. Create a hardware wrapper, set the new top level design, then create a bitstream. At this point, a last validation of the project can take place using JTAG.
4. Export the hardware
Cleaning the code
Open the synthesized design and then the debug wizard. Select "Disconnect all nets and remove debug cores", then "Next" and "Finish".
You might also put the "mark_debug" attributes in the code into comment.
Create block design
Now create a new block design and name in "dummy". Add a Zynq processor to the canvas and run the block automation. Connect the clock.
Then right click and select "Add Module". Select PLTest and clock OK. This will add your PL design to the canvas as RTL.
Now, make all connections external and rename them to match the names in your PLTest interface list. Save the design.
Create the HSL wrapper and set it as the top level module
Next step is to create the HDL wrapper. Let Vivado manage the wrapper and click OK. You will get a "dummy_wrapper" file, either VHDL or Verilog depending on the project settings (but which does not make a difference).
Before implementing the design, we need to set the wrapper as top level module, otherwise the export will fail. However, this change implies a recompilation of your block design each time you do a change in your PL design. So, when you need to redebug things in your PL, consider setting the top level back to "PLTest".
With Top level set to "dummy_wrapper", let the system create the bitstream. When this step is finished, it is then a good idea to load the program onto the board and make a last test.
Export the hardware
When everything is OK, export the project to "PLTest.xsa".
Those files will be created using Vitis development environment. I first explain the basic proceeding so you will understand the steps to take but at the end I will prepare a Python script automating everything. Please note, that we use the unified environment, not the 'classic' one!
Creating boot files will not necessarily need an application project - at least this is true when using the SD card. However, when you want to program the internal QSPI, the tools insist of having an application project defined. Otherwise, you just cannot select the flash device.
As usual, you first need to create a platform using the just created "PLTest.xsa" file, then - eventually - the application project and finally create the boot files.
Create the platform
When launching the tool, it asks you first for a workspace. Easiest is to create another folder (e.g. "PLProject.workspace" within your Vivado project folder and select this one as workspace.
Because its an empty workspace, the tool offers you some choices, select here "Create Platform component". Keep the proposed name "platform", using a name like "PLTest_platform" always resulted in an error. Then click "Next" and select the xsa you exported from Vivado.
When clicking "Next", Vitis imports the xsa and propose an OS and processor. Be sure, "Generate boot artifacts" is selected.
Click "Next", till the platform generation starts and wait till it has finished.
Add a dummy application project
Now, we add an application, so we can directly create the boot files from the application flow. Just follow the Digilent tutorial at this point: create a new "hello_world" application from the template in the examples, select the previously created platform and build the app.
From the flow, you can now select "Create boot image", which will open a window with everything prefilled. Remark: you cannot change the name "the_rom_image" for some reason.
Create a bin folder within your workspace, then select this folder and give a name to the bif file. Then create the image.
Program Flash device
In Vitis, select "Vitis -> Program Flash..." from the Menu. In the opening window, you just need to browse for your bin file. The fsbl-file should be prefilled and you can check the flash device selected to be sure its correct. Then click on "Program" and wait till flashing has finished.
Program a SD card
This is as simple as flashing: Ensure the card is formatted as FAT32. Then copy the files "boot.bin" and "fsbl.elf" in the root folder. Thats it.
Testing
Set your jumper for selecting the boot device to either QSPI or SD card and power off/on your board. When coming up, it should program itself and the "Done" LED should be lit. Test the behavior of your board.
Once after exporting the HW, its still some work in Vitis, till the final files are available. This takes time and probaly you make an error and loose more time. Now, in 2024.2 version, we can use some Python scripting. This makes this last step super easy.
I wrote some scripts to automatically create all needed files within a distinct folder. Then you just need to grab 2 files and copy them onto your SD card and you are ready to run.
Fortunately, AMD/Xilinx is moving away from TCL (a language I never really understood) and going towards Python. Unfortunately, they did not finish the job yet:
- needed commands (bootgen) are missing
- they are using Python 3.8, which was already end of life when Vitis 2024.2 was released. Here they should to step up to at least Python 3.12 for the next release.
Anyway, the current implementation complicates things a bit, but does not block it. The call structure is as follows:
- "CreatePL.cmd": wrapper calling Vitis and another script generating the boot files
- "settings64.bat": script provided by AMD for setting the environment. Copied from installation folder to project folder for convenience.
- "CreateBootFiles.py": main script executed within Vitis to setup the workspace, compile everything and create a BIF defining the boot contents
- "boot.bif.template": template file used to fill in the boot information
- "mkBoot.cmd": Script to run the bootgen utility and copy other files into the boot files folder. Automatically created when executing the Python script.
These scripts must reside in the root of the project folder (except "settings64.bat") and must be executed using the old DOS shell. Power Shell does not work! Within this project root, exactly one (!) xsa file must be found describing the HW you want to set (This is for simplicity reasons, but it is possible to pass arguments if you want to adapt it).
With these conditions, the scripts should run for any project without adaptions or other configuration change.
Currently, is is only tested within Windows. When running under Linux, the Python script should also run without change, except the creation of "mkBoot.cmd" which obviously need an adaptation for the shell used.
I hope you enjoy these scripts.









Comments