Make for simulation flows

Learning objectives
  • Describe a simulation flow as Make targets and prerequisites
  • Use variables, automatic variables and pattern rules
  • Mark phony targets
  • Build only what changed
  • Provide lint, sim, regress and clean targets that behave the same for every engineer and in CI
1 Objective→ 2 Learn→ 3 Example→ 4 Practice→ 5 Verify→ 6 Continue
How to study this lessonUnderstand the idea first. Use the example to see the behaviour, practise where provided, then make sure you can explain the result before continuing.

Explanation

Why Make for RTL work

A shell script runs every step every time. Make records how each output is produced from its inputs and rebuilds an output only when an input is newer. For a design with dozens of testbenches, that turns a five-minute full rebuild into a few seconds after a one-line change. A Makefile is also documentation: make sim TEST=fifo_full is a command anyone can repeat.

Rules

target: prerequisites
    recipe

The recipe lines must start with a tab, not spaces. Make runs the recipe when the target is missing or older than any prerequisite. Each recipe line runs in its own shell, so cd dir on one line does not affect the next.

Variables

Form Meaning
SIM ?= iverilog Set only if not already set (environment or command line can override)
RTL := $(wildcard rtl/*.v) Expand immediately, once
TEST = tb_counter Expand each time it is used
make TEST=tb_fifo Override on the command line

Automatic variables and pattern rules

Variable Value in a recipe
$@ The target being built
$< The first prerequisite
$^ All prerequisites, without duplicates
$* The stem matched by % in a pattern rule

A pattern rule describes a family of targets. This rule says how to build any build/X.vvp from tb/X.v plus the RTL:

build/%.vvp: tb/%.v $(RTL) | build
    iverilog -g2012 -Wall -s $* -o $@ $^

The | build part is an order-only prerequisite: the directory must exist, but its timestamp changing does not force rebuilds.

Phony targets

Targets like sim and clean are names of actions, not files. Declare them .PHONY so a stray file called clean cannot stop the recipe from running.

Failing correctly

Make stops at the first recipe line that exits non-zero. Keep that property: do not hide failures with - prefixes or || true unless a later step checks the result. Add .DELETE_ON_ERROR: so a half-written output from a failed recipe is removed instead of looking up to date on the next run.

Worked example

A small project has rtl/counter.v, rtl/fifo.v, and one testbench per test in tb/. Save this as Makefile:

SIM      ?= iverilog
RUN      ?= vvp -n
LINT     ?= verilator --lint-only -Wall
TEST     ?= tb_counter
SEED     ?= 1

RTL      := $(wildcard rtl/*.v)
TESTS    := $(basename $(notdir $(wildcard tb/tb_*.v)))
LOGS     := $(addprefix build/,$(addsuffix .log,$(TESTS)))

.PHONY: all sim regress lint clean
.DELETE_ON_ERROR:
.SECONDARY:

all: regress

build:
    mkdir -p $@

build/%.vvp: tb/%.v $(RTL) | build
    $(SIM) -g2012 -Wall -s $* -o $@ $^

build/%.log: build/%.vvp
    $(RUN) $< +seed=$(SEED) > $@ 2>&1
    grep -q 'TEST PASSED' $@

sim: build/$(TEST).log
    @cat $<

regress: $(LOGS)
    @echo "regression passed: $(words $(LOGS)) tests"

lint:
    $(LINT) $(RTL)

clean:
    rm -rf build

Typical use:

make lint
make sim TEST=tb_fifo
make -j4 regress
touch rtl/fifo.v && make regress     # rebuilds and reruns every test
make regress                          # nothing to do: all logs are up to date

build/%.log depends on the compiled .vvp, which depends on the testbench and every RTL file. Editing one testbench reruns only that test; editing RTL reruns them all. If grep -q 'TEST PASSED' fails, .DELETE_ON_ERROR removes the log, so the failing test runs again next time rather than being treated as done. Without .SECONDARY:, Make treats the .vvp files as intermediate files of a rule chain and deletes them after the logs are built; keeping them lets you rerun vvp by hand while debugging. make -j4 runs up to four independent tests at once because each log is a separate target. Verilator lint usually needs a top module per invocation in larger designs; for a project with several tops, lint each with --top-module.

Check yourself

Why must recipe lines start with a tab? What do $@, $< and $^ expand to in the .vvp rule? Why is build an order-only prerequisite? What would go wrong without .DELETE_ON_ERROR: when a test fails? Why are sim and clean declared .PHONY?

Engineering practice

Practice: Make for simulation flows

Task→Run / reason→Check result→Explain why

Add a waves target that runs one test with +dump so the testbench writes build/$(TEST).vcd, and make it depend on the same .vvp rule. Add a SEEDS ?= 1 2 3 variable and a seeds target that runs $(TEST) once per seed into build/$(TEST)_seed<N>.log. Prove that make regress does nothing on a second run and reruns only one test after you touch its testbench.

Practice work is non-certified and is not stored by ASIC Campus.