Makefile · Part II · Module 7 · EDA Tool Integration
Simulator Selection and Configuration
Chapter 7.1 ended with three parallel sets of recipes and a warning that shipping it that way is a mistake. This chapter fixes it: one flow, one variable, and the per-tool differences isolated in a file each. Four back ends — including one real simulator — driven through exactly the same target.
Advanced26 min readMakefileGNU MakePart IISimulatorsConfiguration
Three sets of recipes for three simulators works for exactly as long as somebody runs all three regularly. In practice one tool is the daily driver, and the other two are used in a crunch — which is when you discover they stopped working four months ago, because a change to the main flow was never mirrored into them.
The fix is the ordinary engineering answer: one flow, with the differences isolated. Chapter 7.1 §5 already established that the differences are small — a command name, some option spellings, and whether there are one or two commands. That is little enough to put in a file per tool and forget about.
1. The Selection Variable
One variable, one default, and a list of what is allowed:
SIM ?= icarus
SIM_LIST := icarus vcs xrun questa?= so the command line wins (Chapter 3.2), and a default that works for whoever cloned the repository five minutes ago. Choosing the free tool as the default is deliberate: a new starter should be able to run make run and see a simulation without a license.
icarus vcs xrun questaA sims target that lists the choices costs two lines and saves the question. Same argument as Chapter 6.1 §8's make tests.
2. Guarding the Choice
A wrong SIM must fail immediately, before any tool is launched and before any directory is created. Two guards, and the order matters:
$(if $(strip $(SIM)),,$(error SIM is empty -- choose one of: $(SIM_LIST)))
$(if $(filter $(SIM),$(SIM_LIST)),,$(error unknown SIM "$(SIM)" -- choose one of: $(SIM_LIST)))The empty check comes first so that an empty SIM gets a message about being empty. It is worth being precise about why, because the two guard forms behave differently and it is easy to carry the wrong lesson across from Chapter 6.1 §5.
| Form | With SIM= (empty) |
|---|---|
ifneq ($(SIM),$(filter $(SIM),$(SIM_LIST))) | passes — empty equals empty, so the condition never fires |
$(if $(filter $(SIM),$(SIM_LIST)),,$(error ...)) | fires — the filter result is empty, so the else branch runs |
6.1 used the first form and measured an empty test name sailing straight through it. The form used here is the second, which does catch it — measured, with the empty guard deliberately removed:
Makefile.72:10: *** unknown SIM "" -- choose one of: icarus vcs xrun questa. Stop.So the first guard is not load-bearing here; it is there for the message. unknown SIM "" is a puzzle, and SIM is empty is an instruction. When the cause is a wrapper script passing an unset variable, the difference is how long somebody stares at it.
Both guards, measured:
$ make run SIM=vsim
Makefile.72:11: *** unknown SIM "vsim" -- choose one of: icarus vcs xrun questa. Stop.
$ make run SIM=
Makefile.72:10: *** SIM is empty -- choose one of: icarus vcs xrun questa. Stop.Note the first one. vsim is a perfectly real command — it is the Questa binary — but it is not the name this flow uses for that back end, which is questa. That is a mistake somebody will make, so the error lists the four names they could have meant rather than just rejecting theirs.
3. A File Per Tool
Now the differences. Each back end gets its own small file under mk/, holding only what is specific to that tool:
IVERILOG ?= iverilog
VVP ?= vvp
SIM_LABEL := Icarus Verilog (real)
define SIM_COMPILE
mkdir -p $(RUNDIR)
$(IVERILOG) -g2012 -f $(FILELIST) -o $(RUNDIR)/simv
endef
define SIM_RUN
$(VVP) $(RUNDIR)/simv +UVM_TESTNAME=$(TEST) +ntb_random_seed=$(SEED)
endefVCS ?= vcs
SIM_LABEL := Synopsys VCS
define SIM_COMPILE
mkdir -p $(RUNDIR)
$(VCS) -full64 -sverilog -f $(FILELIST) -o $(RUNDIR)/simv -l $(RUNDIR)/compile.log
endef
define SIM_RUN
$(RUNDIR)/simv +UVM_TESTNAME=$(TEST) +ntb_random_seed=$(SEED)
endefXRUN ?= xrun
SIM_LABEL := Cadence Xcelium
define SIM_COMPILE
mkdir -p $(RUNDIR)
@true
endef
define SIM_RUN
$(XRUN) -64bit -sv -f $(FILELIST) +UVM_TESTNAME=$(TEST) -svseed $(SEED) -l $(RUNDIR)/run.log
endefVSIM ?= vsim
SIM_LABEL := Siemens Questa
define SIM_COMPILE
mkdir -p $(RUNDIR)
printf '%s\n' 'onerror {quit -code 1}' 'run -all' 'quit -code 0' > $(RUNDIR)/run.do
endef
define SIM_RUN
$(VSIM) -c -do $(RUNDIR)/run.do tb_top +UVM_TESTNAME=$(TEST) -sv_seed $(SEED)
endefFour files, none longer than ten lines, each answering the same two questions: how do I build? and how do I run? Note how 7.1's shape differences disappear into the answers rather than into the flow — Xcelium's compile is @true because there is nothing to compile, and Questa's "compile" writes a script.
define ... endef is how a variable holds multiple lines. A plain VAR := cmd1; cmd2 would work for two short commands and become unreadable at four; define keeps each command on its own line, exactly as it will run.
4. One Recipe for All of Them
With the definitions in place, the flow itself has no idea which tool it is driving:
BUILD := build
FILELIST := filelists/all.f
TEST ?= axi_smoke
SEED ?= 1
RUNDIR = $(BUILD)/$(SIM)/$(TEST)/$(SEED)
SIM ?= icarus
SIM_LIST := icarus vcs xrun questa
$(if $(strip $(SIM)),,$(error SIM is empty -- choose one of: $(SIM_LIST)))
$(if $(filter $(SIM),$(SIM_LIST)),,$(error unknown SIM "$(SIM)" -- choose one of: $(SIM_LIST)))
$(if $(wildcard mk/$(SIM).mk),,$(error SIM "$(SIM)" is listed but mk/$(SIM).mk is missing))
include mk/$(SIM).mk
.PHONY: run sims which
sims:
@echo " $(SIM_LIST)"
which:
@echo " SIM=$(SIM) -> $(SIM_LABEL)"
run:
@echo " [$(SIM)] $(SIM_LABEL): $(TEST) seed=$(SEED)"
@$(SIM_COMPILE)
@$(SIM_RUN) | tee $(RUNDIR)/run.log | sed 's/^/ /'RUNDIR uses = rather than := here on purpose: it has to be re-expanded after SIM is known and whenever TEST or SEED is overridden on the command line.
All four back ends, through that single run target:
[icarus] Icarus Verilog (real): axi_smoke seed=1
UVM_INFO @ 0: reporter [RNTST] Running test axi_smoke...
UVM_INFO @ 0: reporter [SEED] seed=1
UVM_INFO @ 55000: uvm_test_top [TEST] reg0 readback ok [vcs] Synopsys VCS: axi_smoke seed=1
MOCK simv: UVM_TESTNAME=axi_smoke seed=1
UVM_ERROR : 0 [xrun] Cadence Xcelium: axi_smoke seed=1
MOCK xrun: filelist='filelists/all.f' test=axi_smoke seed=1 (compile+run in one step)
UVM_ERROR : 0 [questa] Siemens Questa: axi_smoke seed=1
MOCK vsim: running do-file 'build/questa/axi_smoke/1/run.do' test=axi_smoke seed=1
MOCK vsim: do-file contents:
| onerror {quit -code 1}
| run -all
| quit -code 0
UVM_ERROR : 0One target, four tools, and the only thing that changed on the command line was SIM=. The which target reports the label the included file set, which is a cheap way to confirm the right file was read:
SIM=icarus -> Icarus Verilog (real)5. Guarding the Include
include mk/$(SIM).mk has a failure mode of its own: a name that passes the SIM_LIST check but has no file behind it, which happens the moment somebody adds an entry to the list and forgets the file. Unguarded, Make says this:
Makefile.72:13: mk/vcs.mk: No such file or directory
make: *** No rule to make target `mk/vcs.mk'. Stop.Two lines, and the second one is actively misleading — it invites you to go looking for a rule, when the problem is a missing file. (Make says that because a missing include is something it will try to build, which is Chapter 4.2 §9's mechanism being helpful in the wrong situation.)
The third guard from §4 replaces both lines with one:
Makefile.72:13: *** SIM "vcs" is listed but mk/vcs.mk is missing. Stop.6. A Build Directory Per Tool
Look again at RUNDIR:
RUNDIR = $(BUILD)/$(SIM)/$(TEST)/$(SEED)$(SIM) is in the path, and that is not tidiness. One simulator cannot execute another's compiled image. A simv built by VCS is not a simv that vvp can run, and vice versa. If both wrote to build/axi_smoke/1/simv, switching SIM would leave the wrong binary sitting at the expected path, and the error you would get is whatever a tool says when handed a foreign executable — which is never "you switched simulator".
With the tool in the path, they coexist. Measured, after running the same test under two back ends:
build/icarus/axi_smoke/1/run.log
build/icarus/axi_smoke/1/simv
build/vcs/axi_smoke/1/run.log
build/vcs/axi_smoke/1/simv
build/vcs/axi_smoke/1/simv.argv
build/vcs/axi_smoke/1/vcs.argvTwo images, both named simv, both intact, neither able to run under the other's simulator and neither needing to. Switching back and forth costs nothing and recompiles nothing.
This is the same reasoning as Chapter 5.3 §6's directory per run, applied one level up: whatever can make two artifacts incompatible belongs in the path. Test name, seed, and now the tool.
7. The Conditional Form, and When to Prefer It
include is not the only way. The same selection can be written with conditionals in a single file, and GNU Make supports an else if chain — confirmed working on the 3.81 used throughout this track:
ifeq ($(SIM),icarus)
SIM_LABEL := Icarus Verilog
SIM_CMD = $(VVP) $(RUNDIR)/simv +UVM_TESTNAME=$(TEST)
else ifeq ($(SIM),vcs)
SIM_LABEL := Synopsys VCS
SIM_CMD = $(RUNDIR)/simv +UVM_TESTNAME=$(TEST) +ntb_random_seed=$(SEED)
else ifeq ($(SIM),xrun)
SIM_LABEL := Cadence Xcelium
SIM_CMD = $(XRUN) -64bit -sv -f $(FILELIST) +UVM_TESTNAME=$(TEST) -svseed $(SEED)
else
$(error unknown SIM "$(SIM)")
endif X=a -> branch a
X=b -> branch b
X=z -> branch otherBoth forms work. The choice is about size:
Prefer conditionals when the per-tool part is two or three variables and you want the whole flow readable in one file. For a two-tool flow this is the lighter option, and there is no mk/ directory to explain to anyone.
Prefer a file per tool when the per-tool part is more than a few lines, when the tools have genuinely different shapes (7.1's one-step against two-step), or when different people own different tools — a file each means two engineers adding two simulators do not touch the same lines.
And prefer the file for a long list, because an else ifeq chain of six tools puts the thing you are looking for in the middle of a hundred-line conditional, while mk/xrun.mk is where you would have looked anyway.
8. What Belongs in the Tool File, and What Does Not
The division only stays clean if you are strict about it. Over time the pressure is always to move one more thing into the per-tool file, and each one makes the flow less comparable between tools.
Belongs in mk/<sim>.mk: the tool's command name as an overridable variable; the option spellings; how many commands the tool needs; the artifact names it produces; a human-readable label.
Does not belong there: the test list, the seed policy, the run-directory layout, the report format, what counts as a pass. All of those are flow decisions, and if two tools answer them differently then a regression's results are no longer comparable — which defeats the purpose of having a second simulator.
The sharpest version of that rule is the pass criterion. One place decides whether a run passed (Chapter 6.4 §8), and it reads the log the testbench wrote. Since the testbench is the same under every tool, so is the criterion. The moment mk/vcs.mk contains its own notion of passing, a VCS pass and an Xcelium pass are two different claims.
run: $(RUNDIR)/passed.stamp
$(RUNDIR)/passed.stamp: $(FILELIST)
@$(SIM_COMPILE)
@$(SIM_RUN) > $(RUNDIR)/run.log 2>&1
@grep -q "^UVM_ERROR : 0" $(RUNDIR)/run.log \
|| { echo " [FAIL] $(SIM) $(TEST) seed=$(SEED)"; exit 1; }
@touch $@That is Chapter 5.4 §6's stamp-not-log rule and Chapter 6.4's single decision point, unchanged, with $(SIM_COMPILE) and $(SIM_RUN) as the only tool-aware lines in the file.
9. Common Mistakes
Keeping one Makefile per simulator. The one nobody runs daily stops working, and you find out in a crunch (§1).
Comparing a variable to its own $(filter) result. ifneq ($(X),$(filter $(X),$(LIST))) passes the empty string, because empty equals empty — measured in Chapter 6.1 §5. Testing the filter result for emptiness instead, with $(if $(filter ...),,$(error ...)), catches it (§2).
Listing a tool without adding its file. Unguarded, Make reports a missing rule rather than a missing file — two lines, neither naming the real problem (§5).
Leaving the tool out of the build path. One simulator's image cannot be run by another, so switching SIM leaves an incompatible binary at the expected path (§6).
Using := for RUNDIR. It must be re-expanded after SIM is known and when TEST or SEED is overridden (§4).
Semicolon-joining a multi-line tool command. Use define ... endef, so each command stays on the line it will run on (§3).
Moving the pass criterion into the tool file. Then a pass under one tool and a pass under another are different claims, and the regressions are not comparable (§8).
Defaulting SIM to a licensed tool. A new starter should be able to run make run with no license at all (§1).
An else ifeq chain for six tools. It works, but the branch you want is buried; a file per tool is where you would have looked (§7).
10. Check Your Understanding
Question 1. A flow validates SIM with ifneq ($(SIM),$(filter $(SIM),$(SIM_LIST))) and nothing else. A wrapper script runs make run SIM=$UNSET_VAR. What happens, and why?
Question 2. Why does the tool name belong in the build path, when the test name and seed already make each run unique?
Question 3. RUNDIR is defined with = rather than :=. Give one thing that would break if it were :=.
Question 4. A colleague proposes moving the pass criterion into each mk/<sim>.mk, arguing that each tool formats its log differently. What is wrong with the argument?
Question 5. Unguarded, a SIM that is in SIM_LIST but has no mk/ file produces "No rule to make target `mk/vcs.mk'". Why does Make phrase it that way?
11. Exercise
Build the flow from §4 with the four files from §3. Every transcript is real output.
Step 1. List the choices and confirm the default:
make sims
make which icarus vcs xrun questa
SIM=icarus -> Icarus Verilog (real)Step 2. Run the same test under all four back ends:
for s in icarus vcs xrun questa; do make run SIM=$s; done [icarus] Icarus Verilog (real): axi_smoke seed=1
[vcs] Synopsys VCS: axi_smoke seed=1
[xrun] Cadence Xcelium: axi_smoke seed=1
[questa] Siemens Questa: axi_smoke seed=1Step 3. Confirm two tools' artifacts coexist:
make clean
make run SIM=icarus > /dev/null
make run SIM=vcs > /dev/null
find build -type f | sortbuild/icarus/axi_smoke/1/run.log
build/icarus/axi_smoke/1/simv
build/vcs/axi_smoke/1/run.log
build/vcs/axi_smoke/1/simv
build/vcs/axi_smoke/1/simv.argv
build/vcs/axi_smoke/1/vcs.argvStep 4. Try all three ways of getting SIM wrong:
make run SIM=vsim
make run SIM=
mv mk/vcs.mk /tmp/ && make which SIM=vcs ; mv /tmp/vcs.mk mk/Makefile.72:11: *** unknown SIM "vsim" -- choose one of: icarus vcs xrun questa. Stop.
Makefile.72:10: *** SIM is empty -- choose one of: icarus vcs xrun questa. Stop.
Makefile.72:13: *** SIM "vcs" is listed but mk/vcs.mk is missing. Stop.Step 5. Now take the guards away one at a time and watch the message degrade. Remove only the empty check:
Makefile.72:10: *** unknown SIM "" -- choose one of: icarus vcs xrun questa. Stop.Still caught, but now you have to work out that the empty quotes are the point. Remove all three:
Makefile.72:11: mk/.mk: No such file or directory
make: *** No rule to make target `mk/.mk'. Stop.Two lines, a filename nobody typed, and an invitation to go looking for a rule. That progression — clear message, puzzling message, misleading message — is what the guards buy.
Then add a fifth back end. Pick any name, add it to SIM_LIST, and run make which SIM=<name> before writing the file: you should get the third guard. Then write mk/<name>.mk with the two define blocks pointing at iverilog, and confirm make run SIM=<name> works and writes into build/<name>/. Adding a simulator should be one list entry and one short file — if it needed a change anywhere in the flow, something tool-specific has leaked out of the tool file.
12. Remember
- One flow, several tools. Parallel Makefiles drift, and the one nobody runs daily is the one that breaks when you need it.
SIM ?= <free tool>, so somebody with no license can runmake runon their first day.- Three guards, three different mistakes: empty, unrecognised, and listed-without-a-file. All fire at parse time.
- The empty check must come first, because a
$(filter)membership test passes the empty string. - An unguarded missing include reports a missing rule, which sends you looking for the wrong thing.
- Put each tool's differences in
mk/<sim>.mk— command name, option spellings, how many commands, artifact names, a label. - Use
define ... endeffor multi-line tool commands, so each command stays on its own line. - One generic recipe expands
$(SIM_COMPILE)and$(SIM_RUN), and the flow never learns which tool it is driving. - Put the tool name in the build path. Measured: two
simvimages coexisting, neither runnable by the other's simulator. RUNDIRneeds=, not:=, so it re-expands afterSIMis known and whenTESTorSEEDis overridden.- Keep the flow decisions out of the tool files — test list, seed policy, directory layout, report format, and above all what counts as a pass.
- Conditionals or a file per tool, by size.
else ifeqis fine for two or three; a file each wins for more, for different shapes, and for separate owners. - Adding a simulator should cost one list entry and one short file. If it costs more, something tool-specific has leaked into the flow.
The flow now runs any simulator, selected by a variable, with the differences in one file each. The next boundary is not between tools but between languages. A recipe is a shell script, and a shell script is the wrong place for anything that needs a data structure — parsing a report, merging coverage, computing a summary. Real flows hand that work to Bash, Tcl or Python, and every handoff is a place to lose a value or, worse, a failure. Chapter 7.3 measures what survives each crossing.
Where this fits
Part of the Makefile curriculum.
