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.
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!).
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 "
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 ⌗
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 Makefile
make target_name<- build the target with the 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,
egg_whites are targets, and
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:
If we were to open a terminal on our kitchen counter and run
make would parse our Makefile and attempt to build the first target it encounters, in this case that would be
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
The only dependency of the
egg_whites target is
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
Once we have our
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
xsimin 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
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: wavesmarks the
wavestarget 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
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.wdbtells 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
echoprevents printout of the actual
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.
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
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.wdbtells make that this target will produce a file with this name. This is a dependency of the
wavestarget, 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.
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
.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.
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
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
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)”.
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
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
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:
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
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
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 :
The syntax used in the assignment
SUB ?= VHDL is conditional. The
?= assignment only sets
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
make will create a marker file named
.adder_VHDL.timestamp in our
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 ⌗
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.
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: