Series on Vivado Simulator Scripted Flow (Bash, Makefiles)

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.

itsembedded.com 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.

itsembedded.com 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 Makefile
  • make target_name <- build the target with the name target_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 the waves 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 actual echo 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 the waves 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

itsembedded.com Vivado Simulator Xsim waves makefile 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: