One of the challenges I’ve faced in the ZPUDemos project is keeping the various targets up to date. When I add a peripheral to – for example – the SDBootstrap SOC, I have to modify each and every target’s project file to match, and it’s very easy to lose track of which ones have been updated and which ones haven’t.
ZPUDemos currently supports no fewer than eight different target boards, and contains eleven different projects – that’s a lot of project files!
In an attempt to make this more manageable, I’ve written some scripts to generate project files automatically, from a list of RTL files, and a board-specific template file. I’ve taken the opportunity to clean up the whole project, too, so the directory structure is more logical.
The directories are now as follows:
- Board: Contains a subdirectory for each target board which contains a toplevel file along with any board-specific supporting files, for instance the i2c drivers needed to talk to the DE1’s audio codec, or the low-level SD card and PS/2 emulation for the MIST board. Also includes any constraint files, and a template project file which references just the RTL files within the board’s own directory. Project-specific files are added by the scripts.
- RTL: The files within this directory are no longer loose, but categorised into subdirectories:
- DMA: The DMA controller and DMACache block RAM definition
- Peripherals: Basic IO controllers for RS232 serial, PS/2, SD card, etc.
- RAM: Anything memory-related: simple dual-port RAM definition, SDRAM controller, Cache, etc.
- Sound: Audio controller, providing very similar facilities to the Amiga’s sound chip.
- Util: Supporting circuits for debouncing switches and dividing clocks, etc.
- Video: VGA controller plus dither component.
- Apps: Various demo applications which can either be placed on SD card or uploaded to the ZPU over RS232. Many of these can be simulated too.
- Common_Firmware: C files and headers providing some barebones C library features, and access to hardware registers. Both the Apps and individual projects’ Firmware files make use of these.
- ZPUSim: A submodule containing a rudimentary simulator for the ZPU.
- ZPUFlex: The CPU itself (as a submodule), also contains ROM prologue / epilogue, startup code and linkscripts.
- Scripts: Build scripts which are used to create project files.
As well as these, each project has its own directory, containing the following:
- Firmware: The ROM(s) used by this project
- RTL: A <project>_Virtual_Toplevel file which is wrapped by the board-specific toplevel, as well as a Toplevel_Config file which is used by the board-specific toplevel to include or omit such things as audio support or VGA dithering, as appropriate. Any other project-specific RTL files go here, unless they could conceivably be useful in other projects, in which case they go in the root RTL directory.
- fpga: Each target board has its own directory in here, containing project files and any generated bitstreams.
- manifest.rtl: A file listing the RTL files that need to be included in the project. These will be merged with the board-specific project template.
- Makefile: this is basically the same for each project, all that varies is the project name.
The root contains a top level Makefile which, if the ZPU toolchain is correctly installed will generate project files for every board and demo, build each demo’s firmware, and build the demo Apps.
In writing the scripts to create the project files I learned quite a bit about bash scripting and Makefiles, including some subtleties of escaping and using shell variables within makefiles that I hadn’t encountered before.
The Scripts directory contains two bash scripts and three makefiles. Of the latter, each project calls the makefile called standard.mak, which in turn calls quartus.mak to build project files for Altera-based boards, and ise.mak to build projects for Xilinx-based boards. The vendor-specific makefiles are called once for each board, in a for loop.
The first subtlety I encountered was in getting this for loop to work within a makefile. It turns out a new shell is spawned for every command execute by a makefile, which means that no environment is preserved from line to line. Thus the obvious approach…
for BOARD in $(BOARDS) do do something done
…won’t work, because the four lines will be executed by different shells! To solve this, we use semi-colons after every command, and escape the newlines with a \ character, to make the loop appear as a single line which will be executed by a single shell, like so:
for BOARD in $(BOARDS); do \ do something; \ done
The second subtlety is in how to pass the BOARD variable to the executed command. Normally you’d just use $BOARD – but if you do that, make will expand the variable, and since it’s not defined until the shell executes the for loop (and never defined in a way that’s visible to make), the parameter doesn’t reach the command. Instead we have to escape the $ with a second $ – so the command inside the loop becomes
do something with $$BOARD; \
The actual makefile looks like this:
PROJECT= MANIFEST=manifest.rtl BOARDS_ALTERA = # Passed in from parent makefile BOARDS_XILINX = # Passed in from parent makefile ALL: fpga make -C Firmware for BOARD in ${BOARDS_ALTERA}; do \ make -f ../Scripts/quartus.mak PROJECT=$(PROJECT) MANIFEST=$(MANIFEST) BOARD=$$BOARD; \ done for BOARD in ${BOARDS_XILINX}; do \ make -f ../Scripts/ise.mak PROJECT=$(PROJECT) MANIFEST=$(MANIFEST) BOARD=$$BOARD; \ done clean: make -C Firmware clean for BOARD in ${BOARDS_ALTERA}; do \ make -f ../Scripts/quartus.mak PROJECT=$(PROJECT) MANIFEST=$(MANIFEST) BOARD=$$BOARD clean; \ done for BOARD in ${BOARDS_XILINX}; do \ make -f ../Scripts/quartus.mak PROJECT=$(PROJECT) MANIFEST=$(MANIFEST) BOARD=$$BOARD clean; \ done fpga: mkdir fpga
Note that we treat the two vendors separately. Generating a Quartus project file is very easy, since the .qsf file is just a text file that contains (among a great many other things) a list of files with no metadata beyond the file’s type – so we can copy a template .qsf and tack the project’s own files onto the end of it; this is what the expandtemplate_quartus.sh script does, like so:
#!/bin/bash cat $1 | while read a; do b=${a,,} if [ "${b: -4}" = ".vhd" ]; then echo set_global_assignment -name VHDL_FILE ../../${a} fi if [ "${b: -4}" = ".qip" ]; then echo set_global_assignment -name QIP_FILE ../../${a} fi if [ "${b: -2}" = ".v" ]; then echo set_global_assignment -name VERILOG_FILE ../../${a} fi done
This script loops through the manifest file, line by line, generating file entries for the .qsf file, detecting the file type as it goes. The script is bash-specific, and uses some constructs I’d not come across before until looking for a solution to this particular problem: Firstly, the b=${a,,}, which converts the current line to lower case which makes comparison easier. Secondly, the ${b: -4} which specifies a substring, the last four characters of the line, which we can then compare against “.vhd”, etc.
Things aren’t so simple for Xilinx chips; the .xise file is also a text file but each file listed within also has some metadata which we can’t easily generate in a shell script. Luckily there’s a shell command as part of ISE, called xtclsh which will execute a tcl script, and we can use this to add files to a template project. The principle is the same – the difference is in the echo commands; instead of emitting file names directly into a project file, the expandtemplate.ise script builds a .tcl script which is subsequently executed using xtclsh. The tcl script, once generated, looks like this:
project open RS232Bootstrap.xise xfile add "../../../RTL/RAM/DualPortRAM.vhd" xfile add "../../../RTL/RAM/TwoWayCache.v" ... xfile add "../../Firmware/RS232Bootstrap_ROM.vhd" project save project close