The goal of this project was to understand the Zynq-7000 from initial powerup to executing a bare metal "hello world" application, and to free myself from the shackles of vendor-specific IDEs/applications (Vitis).
Embedded developers seem to forget that the only things needed for software to run on target platforms are source code, a compiler, a linker script, and a good understanding of how the platform works from power-on to code execution (a make system is nice too). This will serve as a baseline for any project involving a bare-metal application to be run on the Zynq's A9 application core, with a bitstream loaded into PL.
I'm using a Digilent Arty Z7 dev board, and their OOB demo HW and SW designs. But once this is understood, it should be seen how any application gets loaded, executed, debugged, and run on-target.
This includes all the details to the lowest level I could find. Some of Xilinx's documentation is a little vague (to me).
On the Zynq, there exists BootROM code burned into the chip during manufacuring. At poweron, the Cortex-A9 program counter starts at address 0x00000000, which is actually pointing to BootROM (because of the High Vector remap from the SCTLR register). The Xilinx BootROM code executes, copies our "packaged code" (our FSBL, bitstream, and application need to be packaged into a boot image such that the BootROM can execute our code). It reads the Boot Image's header, does whatever it needs to do, etc. One of the final things the BootROM does it remaps the memory so that 0x00000000 points to OCM.
When the BootROM code is done, we jump to our FSBL. (TODO explain the FSBL in more detail, and how it jumps to the next stage).
The BSP directory contains all the Xilinx libraries needed for the first stage bootloader (FSBL), the flasher application as well as the main application. It contains libraries for configuration and control of peripherals (PS and PL), clocking, register address definitions, etc. This is auto-generated in Vitis when creating a "Platform Project" and importing a HW design file (.xsa).
Once you import an XSA from Vivado into a Vitis Platform Project, Vitis auto-generates the libraries, xparameters.h (which contains register definitions, clocking information, and other defines generated from your .xsa hardware design) and other support files. The whole thing was lifted from <the_vitis_workspace>/ps7_cortexa9_0/standalone_domain/bsp.
I'm trying not to modify, pare-down, optimize, or touch these files in any way (including the Makefiles), as when the hardware design changes significantly (new IP is added, removed, addresses changed) it's handy to have these files auto-generated by Vitis so they can be plopped into this Vitis-free project.
the FSBL directory contains the source code and linker script for the First Stage Bootloader used on the Zynq-7000. The First Stage Bootloader handles initial configuration of the device including DDR configuration, timer configuration, other peripheral configuration, it moves the partitions of the bootimage into DDR for execution, or to program the FPGA. The final step of the FSBL is to handoff execution to the next stage in the boot process. In this project, that is the actual application itself contained in app/.
Once you import an XSA from Vivado into a Vitis Platform Project, Vitis auto-generates the FSBL along with everything else generated in the BSP section. The FSBL was similarly lifted from <the_vitis_workspace>/zynq_fsbl.
Also similar to the BSP, I'm trying not to modify the FSBL since a new FSBL can be generated from a newer version of Vitis which might fix any FSBL (or other) bugs. However, the FSBL is meant to be modified to fit your specific application goals.
The app is the actual application that the FSBL jumps to. I'm starting with a bare-metal hello, world example, adding FreeRTOS, and LWIP for networking.
FreeRTOS is included as a git submodule in the app/ directory. The app's Makefile handles compiling the relevant FreeRTOS source files. The files app/src/FreeRTOS_asm_vectors.S, app/src/FreeRTOSConfig.h, and a helper translation unit app/src/rtos_setup.c handle the relevant configuration and GIC setup to get FreeRTOS running in the application.
Additionally, the linkerscript (`app/lscript.ld) needs to be modified to include the FreeRTOS vector table.
LWIP is also included as a git submodule in the app/ directory. More to come when I wire it up.
The flasher application was born from the motivation to load code onto the Arty Z7's QSPI flash, again, without the bloated Xilinx tools. The way that Vitis does it (from what I can tell) is it loads some stripped-down version of u-boot onto the Zynq's OCM. Then commands are sent via JTAG to probe, erase, and write to the QSPI flash.
I couldn't get that u-boot version to work, I don't think I can write JTAG commands through the same channels as Vitis, and Xilinx doesn't provide the source code for that u-boot version. So what do we do? We write our own.
Flasher sits and waits for a couple register pokes: the size of the image to copy, and a "start" register poke. Upon receiving the "start" poke, it just copies data from a hard-coded address in memory to the QSPI flash chip starting at offset 0x00. This means something needs to be sitting at that source address to be copied to the QSPI flash, and a valid size needs to be written to the "size" address the flasher application checks.
The flasher writes diagnostic data via the USB serial interface.
The openocd directory contains the configuration files to debug the Zynq with OpenOCD. Specifically arty-z7.cfg is used with OpenOCD to configure and control the Zynq for debugging. The ps7_init.tcl configures the part without first running the FSBL. It seems that there are .tcl commands that are Xilinx-specific (not standard TCL commands) within this initialization file, so xilinx-tcl.cfg is provided and included in arty-z7.cfg to "translate" the Xilinx commands into standard TCL commands. This way ps7_init.tcl can be lifted from Vitis when the hardware description file (.xsa) is changed.
The boot.bif file is the Boot Image Format (BIF) file used by Xilinx's BootGen utility to describe how to build a Zynq boot image, what goes in it, what order, and how it's used during boot. The Makefile in the root directory uses this to create a BOOT.BIN image which can be loaded onto a boot peripheral (TODO add info here, which peripherals are supported for booting, etc).
The system.bit file is the FPGA configuration bitstream, generated from Vivado. It contains the configuration data needed to program the Programmable Logic (PL) portion of the Xilinx Zynq-7000 device.
In order to build the BSP, FSBL, and application, the only requirement is having arm-none-eabi on your $PATH, and GNU Make installed. As long as both of those requirements are satisfied, it will build on Windows or Linux.
The BSP, FSBL, and application each have their own Makefiles. Again, in the case of the BSP and FSBL, the Makefiles come without changes from Vitis. In Linux, everything should compile fine, in Windows, it's easiest to use a Linux-like shell to compile (Cygwin, Git Bash, WSL).
A Makefile exists in the root directory that calls each of the relevant Makefiles in each section, and also calls Xilinx's bootgen utility to generate a final BOOT.BIN image.
The BSP needs to be built first, as it contains all the libraries for various peripherals. The FSBL and application both rely on these libraries being built.
- VSCode tasks.json
- OpenOCD
- GDB
- VSCode launch.json