Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the choice
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make sims
  icarus vcs xrun questa

A 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — both guards, in this order
$(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.

FormWith 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — first guard removed, make run SIM=
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
mk/icarus.mk — the one real tool here
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)
endef
Azvya Education Pvt. Ltd.VLSI Mentor
mk/vcs.mk
VCS ?= 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)
endef
Azvya Education Pvt. Ltd.VLSI Mentor
mk/xrun.mk — one step, so nothing to compile separately
XRUN ?= 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
endef
Azvya Education Pvt. Ltd.VLSI Mentor
mk/questa.mk — the compile step generates the do-file
VSIM ?= 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)
endef

Four 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the whole flow
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make run SIM=icarus (the real tool)
  [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
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make run SIM=vcs
  [vcs] Synopsys VCS: axi_smoke seed=1
      MOCK simv: UVM_TESTNAME=axi_smoke seed=1
      UVM_ERROR :  0
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make run SIM=xrun
  [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
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make run SIM=questa
  [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 :  0

One 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make which
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — the unguarded include, with mk/vcs.mk missing
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — guarded
Makefile.72:13: *** SIM "vcs" is listed but mk/vcs.mk is missing.  Stop.

6. A Build Directory Per Tool

Look again at RUNDIR:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — find build -type f | sort
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.argv

Two 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.

Four rows, each flowing left to right. Row one, the SIM variable, is validated by three guards before anything runs. Row two, the matching file under mk, supplies the compile and run definitions for that tool. Row three, one generic recipe, expands those definitions so the flow itself is tool-agnostic. Row four, the build path containing the tool name, keeps each tool's artifacts separate because one simulator cannot execute another's image.SIM=<name>one knobThree guardsempty, unknown, no filemk/$(SIM).mkall the differencesCOMPILE and RUNtwo define blocksOne recipeexpands bothTool-agnostic flownothing else changesbuild/$(SIM)/...tool in the pathNo collisionimages are not portable12
Figure 1 — one flow, four back ends. The middle rows are what the SIM variable selects; everything outside them is shared. The bottom row is the reason the tool name has to appear in the build path: two simulators produce images with the same name that cannot be run by each other.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the conditional form
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
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — the chain, exercised with three values
  X=a ->   branch a
  X=b ->   branch b
  X=z ->   branch other

Both 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Makefile — the pass criterion stays in the flow, not in the tool file
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make sims
make which
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
  icarus vcs xrun questa
  SIM=icarus -> Icarus Verilog (real)

Step 2. Run the same test under all four back ends:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for s in icarus vcs xrun questa; do make run SIM=$s; done
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — the first line of each
  [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=1

Step 3. Confirm two tools' artifacts coexist:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make clean
make run SIM=icarus > /dev/null
make run SIM=vcs    > /dev/null
find build -type f | sort
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
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.argv

Step 4. Try all three ways of getting SIM wrong:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
make run SIM=vsim
make run SIM=
mv mk/vcs.mk /tmp/ && make which SIM=vcs ; mv /tmp/vcs.mk mk/
Azvya Education Pvt. Ltd.VLSI Mentor
Actual output
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make run SIM=
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Actual output — make run SIM=
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 run make run on 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 ... endef for 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 simv images coexisting, neither runnable by the other's simulator.
  • RUNDIR needs =, not :=, so it re-expands after SIM is known and when TEST or SEED is 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 ifeq is 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.