Vivado Simulator scripted flow Part 3: Makefiles
Series on Vivado Simulator Scripted Flow (Bash, Makefiles) ⌗
- Part I - Basic Vivado command-line tool usage
- Part II - Introduction to Bash scripting with Vivado tools
- Part III - you’re reading it
- Part IV - IP core and Block Design integration into scripted flow (coming soon)
Vivado and Makefiles, huh? ⌗
Welcome to my guide about automating your Vivado Simulation flow using Makefiles. If you’ve followed parts one and two, you should now be comfortable using Vivado Simulation command-line tools from a Linux terminal, as well capable of basic Bash scripting.
In this guide, you will learn everything you need to write advanced Makefiles for your flow automation. Most of the skills you learn here can be applied to any other simulators (both commercial, like VCS or Modelsim, or open source, like Icarus, GHDL, Verilator), so while this post is not the last (there will be a Part 4 about IP cores and Block Designs), it’s by far the longest and most in-depth in this series.
For the ladies and gents coming from a software background, even if you’re familiar with using make
for compiling c or c++, you’ll see that using make
with a HDL simulation flow is a bit different than you might expect, and I am confident anyone can find something useful to learn here, no matter the skill level.
Fig. 1: The end result is always the same
NOTE: This guide is long. And I mean seriously long. BUT, if you go through it, you will not be disappointed. Source: dude, trust me.
What are Makefiles, what is make
, and why should I use them? ⌗
I’m glad you asked!
make
is a tool used to automate various tasks by writing instructions for said tasks in Makefiles - scripts written in a syntax that make
can understand. Essentially, this means make
to Makefiles is like Bash to Bash scripts. But unlike Bash that’s a master of all things, make
’s sole focus is build automation.
If automating software builds was like taking pictures, then Bash would be the equivalent of a smartphone. You can make calls, send texts, browse the web, and take pictures. It’s not the best at taking photos, but it can take them, as well as do much, much more.
make
, on the other hand, is more akin to a professional camera that comes in a purpose-built housing packed to the brim with buttons, dials, and more buttons. Sadly, due to using the same sensor and lens, it won’t supersede the “bashphone” in terms of image quality, but it will sure as hell make the process easier and more fun.
But there’s a twist! The professional “make-amera” comes standard with a smartphone bolted on to it! And although you wouldn’t necessarily want to use this monstrosity to call your grandma, you could if you needed to.
Fig. 2: Thank heavens these didn't catch on...
To put the analogy into perspective, the most typical task make
is used for is building software by orchestrating compilers and linkers to build an executable binary from a big pool of source files, headers and libraries. The instructions to build things in Makefiles are still written using Bash commands, so anything that can be done using make
is automatically also possible using Bash scripts. However, make
provides additional constructs and functionality that help organize the build process, just like the camera chassis provides extra buttons and dials to make setting up your exposure easier.
Moving away from Bash and into make
for build automation is therefore a natural step forward - it does a lot of work for you that you would have to do manually if writing a Bash script (error checking, dependency checking, incremental builds), while also making your build automation scripts easier to modify, expand, reuse and repurpose. Honestly, the best way to understand the beauty of make
is to try using it out yourself, so let’s get started!
Rewriting our script with Make: First steps ⌗
First, ensure that you have make
installed on your machine:
[~/]$ which make
/usr/bin/make
If you run make
without any arguments, it searches for a Makefile:
[work_dir/SIM]$ make
make: *** No targets specified and no makefile found. Stop.
[work_dir/SIM]$
And while not obvious from the error message, to no one’s surprise, the default Makefile that make
searches for is named… Makefile
:) You can, of course, pass Makefiles with other names to make
, but that’s not required for this guide.
As before, you can either get the finished project sources and completed script of this guide by cloning my repository:
git clone -b part_3 https://github.com/n-kremeris/vivado-scripted-flow.git
and explore while reading the guide, or, if you wish to continue from Part II, you can follow the instructions below.
Go to work_dir/SIM
as before, create an empty file named Makefile
(Pay attention to the uppercase “M”)
[work_dir/SIM] touch Makefile
and then open both it and the xsim_flow.sh
Bash script from Part II in your favorite text editor.
Lastly, you need to source the configuration script provided in the Vivado installation directory. This must be done every time you open up a new terminal (make sure to adjust the path to match your installation directory!).
source /opt/Xilinx/Vivado/2020.2/settings64.sh
Variables ⌗
Makefiles have some similarities to Bash scripts. For one, the variables are defined in a similar way, however, we need to remove the quotes from the strings, as well as add a semicolon “:” before the equals sign.
(We do this because we want to use simply expanded variables - I don’t want to go into detail about it in this guide, so please take a look here if you’d like to learn more).
Taking the above into account, we copy the variable declarations from our xsim_flow.sh
script into our Makefile, and change the declarations from this:
SOURCES_SV=" \
../SRC/adder.sv \
../SRC/subtractor.sv \
../SRC/tb.sv \
"
COMP_OPTS_SV=" \
--incr \
--relax \
"
DEFINES_SV=" -d SUBTRACTOR_VHDL "
SOURCES_VHDL=" ../SRC/subtractor.vhdl "
COMP_OPTS_VHDL=" --incr --relax "
to this:
SOURCES_SV := \
../SRC/adder.sv \
../SRC/subtractor.sv \
../SRC/tb.sv \
COMP_OPTS_SV := \
--incr \
--relax \
DEFINES_SV := -d SUBTRACTOR_VHDL
SOURCES_VHDL := ../SRC/subtractor.vhdl
COMP_OPTS_VHDL := --incr --relax
Cooking with Make ⌗
Basic make
syntax ⌗
Before we start moving the simulation flow commands from the Bash script into the Makefile, we need to take a look at how Makefiles work.
Makefiles consist of a list of one or many build targets.
Each target can have dependencies that are files on the system or other targets. Each target contains a recipe that consists of Bash commands required to build said target (a sort of mini Bash script). The basic structure therefore looks like this:
target_name : dependency_1 dependency_2 dependency_3
bash_command_that_generates_the_target <parameters>
If you run make
from the terminal with no arguments, it builds the first target in your Makefile by default:
make
<-build the first target that appears in the Makefilemake target_name
<- build the target with the nametarget_name
You can imagine Makefiles as being a mix of Make syntax and Bash syntax, where Bash commands are used solely for writing instructions to build targets (and can’t be used outside of one), and Make syntax is used everywhere else.
Make distinguishes Bash commands by forcing us poor engineers to indent all Bash commands with at least one tab character (in contrast, you can use spaces to indent Make syntax, should you wish to do so).
Make has other constructs besides targets, like conditional statements or message printing commands. To give you a better understanding, this is how it looks visually when everything is put together:
ifeq (xxx,yyy)
<-SPACES-> $(info "some text")
endif
first_target : some_dependency
<-TAB-> some_bash_command
<-TAB-> another_bash_command
another_target : first_target some_other_dependency
<-TAB-> yet_another_bash_command
...
Targets and dependencies ⌗
When writing a Makefile, most targets are named exactly as the output file that the target recipe produces. Dependencies of targets can be either files or other targets. Once a target is built, it will not be rebuilt again unless the resulting file is deleted or the dependencies were modified.
To put it all into perspective, let’s automate the process of baking a cake with make
. To bake a cake, we ultimately need cake batter. For the batter, we need flour, water, cocoa powder, and egg whites. To get egg whites, we need to separate eggs into their constituent parts. Writing the entire process in make
could look something like this:
cake : batter
bake_in_oven --degrees 300C --time 1hr --input batter --output cake
batter : flour water cocoa_powder egg_whites
mix flour water cocoa_powder egg_whites --output batter
egg_whites : eggs
separate eggs egg_whites egg_yolks egg_shells
In the above Makefile, cake
, batter
, and egg_whites
are targets, and bake_in_oven
, mix
, separate
are imaginary Bash commands that call tools provided by our Kitchen SDK (sweets development kit).
By default, we have the following raw materials in our kitchen: eggs
, flour
, water
and cocoa powder
.
If we were to open a terminal on our kitchen counter and run make
, make
would parse our Makefile and attempt to build the first target it encounters, in this case that would be cake
. make
would then notice that we have not prepared any batter
, so it would move on to the batter
target. It would then see that the batter
target cannot be completed either, as the egg_whites
ingredient has not been prepared yet (remember, we have eggs, but not egg whites). make
would then try to run the egg_whites
target.
The only dependency of the egg_whites
target is eggs
, which make
sees is available in our kitchen, so it goes ahead and runs the separate
command to split the eggs into the shells, whites, and yolks.
At this point, we have egg_whites
available to us, so make
can now build the batter
target by mixing flour
, water
, cocoa_powder
and egg_whites
.
Once we have our batter
ready, make
can complete the cake
target by taking our batter
and baking it in the oven.
Note: You might be thinking it’s strange that the target list is, in some way, “backwards”, because instead of preparing all the ingredients and mixing the batter, we’re attempting to bake the cake right away. I suggest looking at it the other way around: imagine you’re asking make
to bake you a cake. make
then figures out what ingredients are missing, prepares all the ingredients, then the batter, and then bakes the cake for you.
Makefile targets for the Vivado Simulation flow ⌗
Moving away from our cooking analogy to running Vivado Simulations, I came up with the following list of targets for our simulation flow:
-
Graphical waveform display target - This target depends on a populated simulation tracing waveform database (.wdb), and it launches
xsim
in a graphical mode. It does not generate any files. -
Simulation - This target depends on a simulation snapshot, which is generated by the elaboration step. The simulation target generates the waveform database after it’s complete.
-
Elaboration - This target depends on a successful compilation of all our Verilog, Systemverilog and VHDL code, and generates a simulation snapshot.
-
VHDL, Verilog, SystemVerilog compilation targets - these three targets depend on all the required sources being available, and generate object files for elaboration.
And one extra step can be added for our convenience:
- Cleanup - This target does not have any dependencies, and does not generate any files. It simply removes all generated from our working directory.
Converting our Bash flow to Make ⌗
Waveform viewing ⌗
We can say that the ultimate goal of this flow is the viewing of simulated waveforms, so this is the first step we convert to a make
target, starting with this snippet from our xsim_flow.sh
script:
if [ "$1" == "waves" ]; then
echo
echo "### OPENING WAVES ###"
xsim --gui adder_tb_snapshot.wdb
fi
We don’t need the argument parsing anymore so the if
statement is dropped. Wrapping the xsim
command in a make
target gives us this:
.PHONY: waves
waves : adder_tb_snapshot.wdb
@echo "### OPENING WAVES ###"
xsim --gui adder_tb_snapshot.wdb
.PHONY: waves
marks thewaves
target as not generating any output files.
Remember how I said that targets generally create files of the same name? Sorry for starting off with the exact opposite. Declaring a target as phony
tells make
that this target name does not correspond to a generated file name, because… well… we don’t create a waves
file by running this target, we only display the waves on the screen.
- The part
: adder_tb_snapshot.wdb
tells waves that this file is a required dependency for this target.
You can’t bake a cake without batter, and you can’t draw waveforms if you have no waveform data to draw. If make
cannot find this dependency, then it will look for a target with the same name, and try to build it. If it cannot find a way to build this dependency, make
will throw an error.
- “@” character before
echo
prevents printout of the actualecho
command.
If you don’t prepend “@” before a recipe command, make
will show the command that it’s executing as well as the command’s output. If you do prepend “@”, it will only show the output.
Before continuing, we can see that that the name adder_tb
is used commonly, yet I picked it quite arbitrarily. We know that the name of the testbench module is ’tb’, so lets use just that as the prefix for the snapshot moving forward, and lets also store it in a variable:
TB_TOP := tb
.PHONY : waves
waves : $(TB_TOP).wdb
@echo
@echo "### OPENING WAVES ###"
xsim --gui $(TB_TOP)_snapshot.wdb
Then, if we ever change the name of the testbench, we will be able to edit the variable and not have to change every single instance of the testbench module name.
Simulation ⌗
Next, we take the simulation step
echo
echo "### RUNNING SIMULATION ###"
xsim adder_tb_snapshot --tclbatch xsim_cfg.tcl
convert it to a make
simulation target, and place it below the waves
target:
TB_TOP := adder_tb
.PHONY : waves
waves : $(TB_TOP)_snapshot.wdb
@echo
@echo "### OPENING WAVES ###"
xsim --gui $(TB_TOP)_snapshot.wdb
$(TB_TOP)_snapshot.wdb : .elab.timestamp
@echo
@echo "### RUNNING SIMULATION ###"
xsim $(TB_TOP)_snapshot --tclbatch xsim_cfg.tc
-
The target name
$(TB_TOP)_snapshot.wdb
tells make that this target will produce a file with this name. This is a dependency of thewaves
target, as mentioned before. -
.elab.timestamp
- This is a dependency for the simulation step
It’s a timestamp file that we will create manually at the end of the elaboration target. The reason for doing this is that elaboration creates various multiple files, and a custom made timestamp file will be easier for us to track. I chose to start the file name with a dot - this marks it as a hidden file on Linux-based systems.
Elaboration ⌗
Next we need convert the elaboration step. Starting off with this:
echo
echo "### ELABORATING ###"
xelab -debug all -top tb -snapshot adder_tb_snapshot
if [ $? -ne 0 ]; then
echo "### ELABORATION FAILED ###"
exit 12
fi
We wrap the xelab
command in a make
target, and drop the return code checking - checking is done automatically for us by make
.
.elab.timestamp : .comp_sv.timestamp .comp_v.timestamp .comp_vhdl.timestamp
@echo
@echo "### ELABORATING ###"
xelab -debug all -top $(TB_TOP) -snapshot $(TB_TOP)_snapshot
touch .elab.timestamp
The result is similar to the simulation target. We know that the elaboration depends on the successfull compilation of SystemVerilog, Verilog and VHDL sources, so we write a corresponding list of timestamps as the dependencies for this target.
On the last line, we create the elaboration timestamp with the touch
command. It is nothing more than an empty file. If a file with the same name already exists, then touching it will only update the last modified/accessed time values in the filesystem.
Compilation ⌗
Just like we did with the elaboration target, we want to convert our SystemVerilog and VHDL compilation steps to make
targets, as well as add a Verilog compilation step (by not passing the --sv
parameter to xvlog
), so we go from this (echo’s and exit’s removed for brevity):
xvlog --sv $COMP_OPTS_SV $DEFINES_SV $SOURCES_SV
xvhdl $COMP_OPTS_VHDL $SOURCES_VHDL
To this (echo’s removed for brevity):
#SystemVerilog
.comp_sv.timestamp : $(SOURCES_SV)
xvlog --sv $(COMP_OPTS_SV) $(DEFINES_SV) $(SOURCES_SV)
touch .comp_sv.timestamp
#Verilog
.comp_v.timestamp : $(SOURCES_V)
xvlog $(COMP_OPTS_V) $(DEFINES_V) $(SOURCES_V)
touch .comp_v.timestamp
#VHDL
.comp_vhdl.timestamp : $(SOURCES_VHDL)
xvhdl $(COMP_OPTS_VHDL) $(SOURCES_VHDL)
touch .comp_vhdl.timestamp
Ah, but now we have a big problem. Our SOURCES_V
variable is not declared, so it’s basically empty. That means our Verilog compilation target will run even though we have no Verilog sources.
While that might be desirable behavior in some cases, it is absolutely unacceptable in this scenario. We cannot have the compilation steps call xvlog
or xvhdl
without source files, as those tools will fail with an error like this:
[work_dir/SIM]$ xvlog
ERROR: [XSIM 43-3273] No HDL file(s) specified.
Therefore, we need to check if the corresponding source variable is set (or, in other words, we have sources to compile) with make
’s ifeq
command, and skip running the compilation tool if no sources are given. This is done as follows:
ifeq ($(SOURCES),)
some_target :
@echo "Print message saying that step was skipped"
else
some_target : $(SOURCES)
run_build_command $(SOURCES)
endif
The first line checks if the source variable is not set, or equal to nothing (there’s a comma “,” before the closing bracket, signifying that the right hand side of the comparison is nothing).
If the sources variable is not set, then we set the target recipe to just print a message that says this step was skipped due to no sources provided.
Otherwise, we set the target recipe to the actual build command that takes the sources as an argument. Note that in this case, the sources variable IS added to the target’s dependency list - this ensures that the target is rebuilt if it’s out of date (i.e. if the sources were modified).
Applying the above to our compilation targets, we get this:
ifeq ($(SOURCES_SV),)
.comp_sv.timestamp :
@echo
@echo "### NO SYSTEMVERILOG SOURCES GIVEN ###"
@echo "### SKIPPED SYSTEMVERILOG COMPILATION ###"
touch .comp_sv.timestamp
else
.comp_sv.timestamp : $(SOURCES_SV)
@echo
@echo "### COMPILING SYSTEMVERILOG ###"
xvlog --sv $(COMP_OPTS_SV) $(DEFINES_SV) $(SOURCES_SV)
touch .comp_sv.timestamp
endif
ifeq ($(SOURCES_V),)
.comp_v.timestamp :
@echo
@echo "### NO VERILOG SOURCES GIVEN ###"
@echo "### SKIPPED VERILOG COMPILATION ###"
touch .comp_v.timestamp
else
.comp_v.timestamp : $(SOURCES_V)
@echo
@echo "### COMPILING VERILOG ###"
xvlog --sv $(COMP_OPTS_V) $(DEFINES_V) $(SOURCES_V)
touch .comp_v.timestamp
endif
ifeq ($(SOURCES_VHDL),)
.comp_v.timestamp :
@echo
@echo "### NO VHDL SOURCES GIVEN ###"
@echo "### SKIPPED VHDL COMPILATION ###"
touch .comp_vhdl.timestamp
else
.comp_vhdl.timestamp : $(SOURCES_VHDL)
@echo
@echo "### COMPILING VHDL ###"
xvhdl $(COMP_OPTS_VHDL) $(SOURCES_VHDL)
touch .comp_vhdl.timestamp
endif
Note that unlike in our Bash script, we now need to wrap the variables in brackets: “$(xxx)”.
Cleanup ⌗
If you went through parts 1 and 2 of this guide, you should be aware of how much junk Vivado generates when running the simulation flow:
[work_dir/SIM]$ ls
Makefile webtalk_136530.backup.jou webtalk.jou xelab.log xsim.dir xsim.log xvhdl.pb xvlog.pb
tb_snapshot.wdb webtalk_136530.backup.log webtalk.log xelab.pb xsim.jou xvhdl.log xvlog.log
Now we could go and delete these files manually with rm -rf *.log
, rm -rf .jou
, etc., but that would be tedious. We know that the extensions of files generated by the flow are not going to change, so lets just create another phony
target for cleaning the SIM
directory (remember - phony targets are targets that do not generate files of the same name as the target itself).
Note that we also want to delete all of the hidden timestamps. To get rm
to delete those, we need to add the first dot .
to the expression.
.PHONY : clean
clean :
rm -rf *.jou *.log *.pb *.wdb xsim.dir # This deletes all files generated by Vivado
rm -rf .*.timestamp # This deletes all our timestamps
Additional phony targets ⌗
Imagine you’re editing a bunch of source files, adding new functionality, connecting modules, and you want to run a quick syntax check. You could invoke make
with the .comp_vhdl.timestamp
as a parameter
make .comp_vhdl.timestamp
this would have make
go and re-run the VHDL compilation step. But lets be honest, that kind of target name is neither intuitive nor memorable.
Instead, just like the waves
and clean
targets, we can create phony
targets that act as user-friendly aliases to invoke building specific targets.
For example, to recompile everything that’s been modified (remember - modifying sources that are dependencies to a target marks that target as being out of date) we can create a phony
compilation target:
.PHONY : compile
compile : .comp_sv.timestamp .comp_v.timestamp .comp_vhdl.timestamp
Now we can invoke make
like so:
make compile
and it will build all the targets that result in the timestamps in the dependency list.
Note how there are no commands in the target body - that is expected, we don’t want to do anything after the compilation is complete, so make
just exits immediately afterwards.
You could also write individual phony
targets for each of the source types if you wanted to.
Similarly, we may want to individually run the elaborate step to check for correct module connections, port widths, or other warnings:
.PHONY : elaborate
elaborate : .elab.timestamp
Lastly, running make
without any arguments will build everything and then launch waveform view (because it’s the first target in my Makefile), and this is not the kind of behavior we want by default. It would be best if by running make
without any arguments we would just run the simulation, so we add the following phony
target as the first target in the Makefile (above all other targets).
.PHONY : simulate
simulate : $(TB_TOP)_snapshot.wdb
Changing the DUT and rebuilding the snapshot ⌗
We’re almost there! Remember that we have two versions of subtractors, a VHDL one, and a SystemVerilog one? We specified which one to use in the following variable:
DEFINES_SV= -d SUBTRACTOR_VHDL
It would be great if we could somehow change this from the terminal when invoking make
without editing the Makefile - this would let us easily switch between the subtractor implementations.
One of the way to do this is to have a separate variable that we could set when invoking make
. We remove the subtractor define from the original variable (but keep the variable - this allows us to add more defines to it later if we want), declare a new variable called SUB
, set it’s default value to VHDL
, and append the required definition to the DEFINES_SV
variable based on the value of SUB
.
We do this by writing the following above all of the targets:
SUB ?= VHDL
ifeq ($(SUB), VHDL)
$(info Building with VHDL subtractor)
DEFINES_SV := $(DEFINES_SV) -d SUBTRACTOR_VHDL
else ifeq ($(SUB), SV)
$(info Building with SYSTEMVERILOG subtractor)
DEFINES_SV := $(DEFINES_SV) -d SUBTRACTOR_SV
else
$(info )
$(info BAD SUBTRACTOR TYPE)
$(info Available options:)
$(info make SUB=VHDL <target>)
$(info make SUB=SV <target>)
$(error )
endif
This will append the required definition to our DEFINES_SV
variable based on the SUB
variable. It will pick VHDL
by default, but we can change to SV
via the command line :
make SUB=SV
The syntax used in the assignment SUB ?= VHDL
is conditional. The ?=
assignment only sets SUB
to VHDL
if SUB
does not have a value (i.e. has not been set a value previously).
Lastly, we add a timestamp marker target for checking what type of subtractor was used when building the snapshot: the SV, or the VHDL one:
# Subtractor type marker
.adder_$(SUB).timestamp :
@rm -rf .sub_*.timestamp
@touch .sub_$(SUB).timestamp
This target deletes the existing marker, and creates either a .sub_SV.timestamp
or a .sub_VHDL.timestamp
based on what the SUB
variable is set to.
We then need to append this marker file to our SystemVerilog build line dependencies:
<...>
else
.comp_sv.timestamp : $(SOURCES_SV) .sub_$(SUB).timestamp
@echo
@echo "### COMPILING SYSTEMVERILOG ###"
xvlog --sv $(COMP_OPTS_SV) $(DEFINES_SV) $(SOURCES_SV)
touch .comp_sv.timestamp
endif
The way this will work is as follows: if we build everything from scratch with SUB=VHDL
, make
will create a marker file named .adder_VHDL.timestamp
in our SIM
directory.
If we re-run make
without modifying any sources, but this time with SUB=SV
as the parameter, the SystemVerilog compilation step will notice that the .adder_SV.timestamp
dependency is missing (remember, we have a marker with the name .adder_VHDL.timestamp
in our directory from the previous build), and as a result, will re-run the marker generation target and the compilation target.
Final results ⌗
Finally, after all the hard work, we add some comments to make everything more readable, and this is the Makefile we end up with:
SOURCES_SV := \
../SRC/adder.sv \
../SRC/subtractor.sv \
../SRC/tb.sv \
COMP_OPTS_SV := \
--incr \
--relax \
DEFINES_SV :=
SOURCES_VHDL := ../SRC/subtractor.vhdl
COMP_OPTS_VHDL := --incr --relax
TB_TOP := tb
SUB ?= VHDL
ifeq ($(SUB), VHDL)
$(info Building with VHDL subtractor)
DEFINES_SV := $(DEFINES_SV) -d SUBTRACTOR_VHDL
else ifeq ($(SUB), SV)
$(info Building with SYSTEMVERILOG subtractor)
DEFINES_SV := $(DEFINES_SV) -d SUBTRACTOR_SV
else
$(info )
$(info BAD SUBTRACTOR TYPE)
$(info Available options:)
$(info make SUB=VHDL <target>)
$(info make SUB=SV <target>)
$(error )
endif
#==== Default target - running simulation without drawing waveforms ====#
.PHONY : simulate
simulate : $(TB_TOP)_snapshot.wdb
.PHONY : elaborate
elaborate : .elab.timestamp
.PHONY : compile
compile : .comp_sv.timestamp .comp_v.timestamp .comp_vhdl.timestamp
#==== WAVEFORM DRAWING ====#
.PHONY : waves
waves : $(TB_TOP)_snapshot.wdb
@echo
@echo "### OPENING WAVES ###"
xsim --gui $(TB_TOP)_snapshot.wdb
#==== SIMULATION ====#
$(TB_TOP)_snapshot.wdb : .elab.timestamp
@echo
@echo "### RUNNING SIMULATION ###"
xsim $(TB_TOP)_snapshot -tclbatch xsim_cfg.tcl
#==== ELABORATION ====#
.elab.timestamp : .comp_sv.timestamp .comp_v.timestamp .comp_vhdl.timestamp
@echo
@echo "### ELABORATING ###"
xelab -debug all -top $(TB_TOP) -snapshot $(TB_TOP)_snapshot
touch .elab.timestamp
#==== COMPILING SYSTEMVERILOG ====#
ifeq ($(SOURCES_SV),)
.comp_sv.timestamp :
@echo
@echo "### NO SYSTEMVERILOG SOURCES GIVEN ###"
@echo "### SKIPPED SYSTEMVERILOG COMPILATION ###"
touch .comp_sv.timestamp
else
.comp_sv.timestamp : $(SOURCES_SV) .sub_$(SUB).timestamp
@echo
@echo "### COMPILING SYSTEMVERILOG ###"
xvlog --sv $(COMP_OPTS_SV) $(DEFINES_SV) $(SOURCES_SV)
touch .comp_sv.timestamp
endif
#==== COMPILING VERILOG ====#
ifeq ($(SOURCES_V),)
.comp_v.timestamp :
@echo
@echo "### NO VERILOG SOURCES GIVEN ###"
@echo "### SKIPPED VERILOG COMPILATION ###"
touch .comp_v.timestamp
else
.comp_v.timestamp : $(SOURCES_V)
@echo
@echo "### COMPILING VERILOG ###"
xvlog $(COMP_OPTS_V) $(DEFINES_V) $(SOURCES_V)
touch .comp_v.timestamp
endif
#==== COMPILING VHDL ====#
ifeq ($(SOURCES_VHDL),)
.comp_vhdl.timestamp :
@echo
@echo "### NO VHDL SOURCES GIVEN ###"
@echo "### SKIPPED VHDL COMPILATION ###"
touch .comp_vhdl.timestamp
else
.comp_vhdl.timestamp : $(SOURCES_VHDL)
@echo
@echo "### COMPILING VHDL ###"
xvhdl $(COMP_OPTS_VHDL) $(SOURCES_VHDL)
touch .comp_vhdl.timestamp
endif
.PHONY : clean
clean :
rm -rf *.jou *.log *.pb *.wdb xsim.dir
rm -rf .*.timestamp
#==== Subtractor type marker generation ===#
.sub_$(SUB).timestamp :
@rm -rf .sub_*.timestamp
@touch .sub_$(SUB).timestamp
Wow that’s a lot! Before wrapping up, let’s take a brief look at what this Makefile allows us to do.
Testing how our new Vivado Makefile flow works ⌗
Cleanup ⌗
Still in the work_dir/SIM
folder, we make sure we delete all of the old files, remove the previously used xsim_flow.sh
Bash script, and have only the Makefile (and the xsim config script) left in the directory:
[work_dir/SIM]$ rm -rf *jou *wdb *pb *log xsim.dir xsim_flow.sh .*.timestamp
[work_dir/SIM]$ ls
Makefile xsim_cfg.tcl
Simulation and Waves ⌗
Building and running the simulation is now easy. We simply type make
, and as the first target in our Makefile is simulation, that’s what gets built/run by default (output truncated for brevity):
[work_dir/SIM]$ make
Building with VHDL subtractor
### COMPILING SYSTEMVERILOG ###
xvlog --sv --incr --relax -d SUBTRACTOR_VHDL ../SRC/adder.sv ../SRC/subtractor.sv ../SRC/tb.sv
INFO: [VRFC 10-2263] Analyzing SystemVerilog file "work_dir/SRC/adder.sv" into library work
<...>
### NO VERILOG SOURCES GIVEN ###
### SKIPPED VERILOG COMPILATION ###
touch .comp_v.timestamp
### COMPILING VHDL ###
xvhdl --incr --relax ../SRC/subtractor.vhdl
INFO: [VRFC 10-163] Analyzing VHDL file "work_dir/SRC/subtractor.vhdl" into library work
<...>
### ELABORATING ###
xelab -debug all -top tb -snapshot tb_snapshot
<...>
### RUNNING SIMULATION ###
xsim tb_snapshot -R
<...>
$$$ TESTBENCH: Using VHDL subtractor
TB passed, adder and subtractor ready to use in production
exit
INFO: [Common 17-206] Exiting xsim at Sat Feb 27 20:44:19 2021...
If we run make
again, it will realize that nothing has changed in our sources, and thus our waveform database is still up to date so doesn’t have to be rebuilt:
[work_dir/SIM]$ make
Building with VHDL subtractor
make: Nothing to be done for 'simulate'.
We can then run make
with the waves
target passed to it as a parameter to take a look at the waveforms:
[work_dir/SIM]$ make waves
Fig. 3: Nice and easy
Building and simulating with the SystemVerilog subtractor version ⌗
We know that by default make
will build our simulation snapshot with the VHDL subtractor. Lets check if our SV version still works:
[work_dir/SIM]$ make SUB=SV
Building with SYSTEMVERILOG subtractor
### COMPILING SYSTEMVERILOG ###
xvlog --sv --incr --relax -d SUBTRACTOR_SV ../SRC/adder.sv ../SRC/subtractor.sv ../SRC/tb.sv
<...>
### ELABORATING ###
xelab -debug all -top tb -snapshot tb_snapshot
<...>
### RUNNING SIMULATION ###
xsim tb_snapshot -tclbatch xsim_cfg.tcl
<...>
$$$ TESTBENCH: Using SystemVerilog subtractor
<...>
Great, we have built and run the simulation with the SystemVerilog version of the subtractor, as can be seen from the output of the last step.
Additionally, pay attention how make
re-ran the SystemVerilog build target (because the subtractor marker dependency changed), but VHDL sources were not recompiled (because they haven’t changed) - this is sure to eventually save you time with bigger builds.
Syntax checking and partial rebuilds ⌗
Let’s update our SV subtractor in work_dir/SRC/subtractor.sv
by adding some highly useful debugging information:
<...>
initial begin
$display("Subtractor initial block, hell yeah!")
end
endmodule : subtractor_systemverilog
Rebuilding this with the SUB=SV
parameter tells us we might have have a syntax error:
[work_dir/SIM]$ make SUB=SV compile
Building with SYSTEMVERILOG adder
### COMPILING SYSTEMVERILOG ###
xvlog --sv --incr --relax -d SUBTRACTOR_SV ../SRC/adder.sv ../SRC/subtractor.sv ../SRC/tb.sv
<...>
ERROR: [VRFC 10-4982] syntax error near 'end' [/home/toby/Documents/blogs/vivado-scripted-flow/work_dir/SRC/subtractor.sv:13]
ERROR: [VRFC 10-2790] SystemVerilog keyword end used in incorrect context [/home/toby/Documents/blogs/vivado-scripted-flow/work_dir/SRC/subtractor.sv:13]
ERROR: [VRFC 10-2865] module 'subtractor_systemverilog' ignored due to previous errors [/home/toby/Documents/blogs/vivado-scripted-flow/work_dir/SRC/subtractor.sv:1]
make: *** [Makefile:73: .comp_sv.timestamp] Error 1
Yep, we’ve missed a semicolon at the end of the $display
statement. Fixing that and re-running gives us this:
[work_dir/SIM]$ make SUB=SV compile
Building with SYSTEMVERILOG adder
### COMPILING SYSTEMVERILOG ###
xvlog --sv --incr --relax -d SUBTRACTOR_SV ../SRC/adder.sv ../SRC/subtractor.sv ../SRC/tb.sv
INFO: [VRFC 10-2263] Analyzing SystemVerilog file "/home/toby/Documents/blogs/vivado-scripted-flow/work_dir/SRC/adder.sv" into library work
INFO: [VRFC 10-311] analyzing module adder
INFO: [VRFC 10-2263] Analyzing SystemVerilog file "/home/toby/Documents/blogs/vivado-scripted-flow/work_dir/SRC/subtractor.sv" into library work
INFO: [VRFC 10-311] analyzing module subtractor_systemverilog
INFO: [VRFC 10-2263] Analyzing SystemVerilog file "/home/toby/Documents/blogs/vivado-scripted-flow/work_dir/SRC/tb.sv" into library work
INFO: [VRFC 10-311] analyzing module tb
touch .comp_sv.timestamp
[work_dir/SIM]$
As you can see, the compile
target allows us to easily check for simple syntax errors. It is better than simply running make
, which would try running the simulation target by default, meaning if there were no syntax errors, it would next continue to elaboration, and then to simulation, prompting us to mash Ctrl+C
to stop it from continuing.
Final notes ⌗
Had this guide been written by someone else, it’s highly likely it would have looked completely different, because there are many ways to do the same things in Bash and Make. Your Makefiles do not have to look or be ordered or be formatted like mine, your dependencies and targets could be named differently, and you can skip using the phony targets if you wish. I do not want for any readers to assume that my way is the only way to implement Vivado Synthesis flow automation, so please experiment by modifying this flow or writing your own flow from scratch - that’s the only way to find out what works best for you.
Conclusion ⌗
Thank you for reading my guide up to the very end - it was exciting to write this, and I hope it was a useful read.
I do not think we need an actual conclusion here - if you went through this guide, you have probably formed a strong opinion on the make
based flow for Vivado simulations. It doesn’t matter whether that’s hate or love - I believe any opinion is fully valid here. What is important though, is that you now have a solid foundation on using Vivado command line tools, Bash scripting and writing Makefiles. Treat yourself to something nice tonight.
Now that all of the heavy stuff is out of the way, the next part will be a much easier read - In Part 4 (coming soon), we will be discussing how to deal with block designs and IP cores, in particular, how to move them out of the Vivado project and into your Makefile based flow.
If you have any questions or observations regarding this guide ⌗
Feel free to add me on my LinkedIn, i’d be happy to connect!
Or send me an email: