Makefile · Part II · Module 5 · RTL Simulation Automation
Makefiles in RTL and Verification Projects
Part I taught Make with text files, because the mechanics are the same whatever the commands are. Part II swaps in the real thing. This chapter maps a simulation flow onto the vocabulary you already have — which files are inputs a human wrote, which are artifacts a tool produced, and which names are actions somebody asks for — and then runs it. From here on the examples compile and execute an actual Verilog design.
Foundation21 min readMakefileGNU MakePart IIRTLSimulation
Part II · Module 5 · Chapter 5.1 · Makefiles in RTL and Verification Projects
1. What Changes in Part II
Part I used cat, grep and sort on purpose. Make does not care what a recipe runs, so the mechanics — dependency graphs, timestamps, pattern rules, exit statuses — are identical whether the command is cat or a simulator. Teaching them with tools you already have meant every example could be run.
Part II keeps that property and changes the tools. Every simulation in this module really compiles and really runs. The examples use Icarus Verilog, an open-source simulator:
iverilog -VIcarus Verilog version 13.0 (stable) (v13_0)That matters for a specific reason. A tutorial that shows a vcs command nobody reading it can execute is asking you to take the flow on trust. With Icarus you can type every command in this module and see the same output, and the structure you learn transfers directly to VCS, Xcelium or Questa — the options differ, the Makefile does not.
Where a commercial tool genuinely behaves differently, this module says so rather than pretending. §8 has the first such case.
2. What a Simulation Actually Involves
Strip a simulation down and there are four kinds of thing.
Sources a human wrote. The design, the testbench, and the header files they include. These are inputs: they live in version control, and nothing generates them.
A list of which sources to read. Real blocks have hundreds of files, in a definite order, with include paths and compile-time options. That list is itself a file — a filelist.
A compiled simulation image. The simulator turns the sources into something executable. This is a generated artifact.
The output of running it. A log, usually, plus optionally a waveform. Also generated.
Here is the project this module uses. It is small enough to read and real enough to compile:
make-sim/
├── rtl/
│ ├── include/
│ │ └── defines.vh # a width, included by the design
│ └── counter.v # the design
├── tb/
│ └── tb_counter.v # the testbench
├── filelists/
│ └── all.f # which sources, and the include path
└── MakefileThe design is an ordinary counter, written so it depends on the header:
`define DATA_W 8`include "defines.vh"
module counter (
input wire clk,
input wire rst_n,
output reg [`DATA_W-1:0] count
);
always @(posedge clk or negedge rst_n)
if (!rst_n) count <= 0;
else count <= count + 1'b1;
endmoduleAnd a testbench that drives it, checks it, and says so:
`timescale 1ns/1ps
module tb_counter;
reg clk = 0, rst_n = 0;
wire [7:0] count;
integer seed; reg [8*32:1] test;
counter dut (.clk(clk), .rst_n(rst_n), .count(count));
always #5 clk = ~clk;
initial begin
if (!$value$plusargs("SEED=%d", seed)) seed = 1;
if (!$value$plusargs("TEST=%s", test)) test = "smoke";
$display("TB: test=%0s seed=%0d", test, seed);
#12 rst_n = 1;
#100;
$display("TB: count=%0d", count);
if (count === 8'd10) $display("TB: PASSED");
else $display("TB: FAILED");
$finish;
end
endmoduleTwo things in the testbench are there for later chapters and are worth noticing now. $value$plusargs reads a value given on the run command line, which is how a test name and a seed get in without recompiling. And the testbench decides PASSED or FAILED itself and prints it — Chapter 4.3 showed why a flow has to read something like that rather than trust an exit status.
3. Running It by Hand First
Chapter 1.3 made a rule of this: run the commands in a shell before putting them in a Makefile. Two commands do the whole flow.
Compile. The simulator reads the filelist and writes an executable image:
iverilog -g2012 -f filelists/all.f -o build/simvRun. The image is a program; running it runs the simulation:
vvp build/simvTB: test=smoke seed=1
TB: count=10
TB: PASSED
tb/tb_counter.v:20: $finish called at 112000 (1ps)That is a real simulation of a real counter. The design counted ten clock edges after reset released, the testbench checked it and printed PASSED, and $finish ended the run at 112 ns.
Two commands, and both are slow and detailed enough that nobody wants to retype them — which is where Make comes in. But notice what you now have that you did not have before: a known-good command line. Everything after this is about wrapping it, not inventing it.
4. Which Tool Tells the Truth, Measured
Chapter 4.3 argued that a flow must read its own log because a simulator can exit zero after finding errors. With a real simulator in front of us, that is no longer an argument — it is a measurement, and the result is sharper than expected.
The compiler is honest. Break the design with a syntax error and iverilog says so, returns non-zero, and produces nothing:
iverilog -g2012 -f filelists/all.f -o build/simv
echo "exit: $?"
ls build/simvtb/tb_counter.v:2: syntax error
tb/tb_counter.v:2: Errors in port declarations.
I give up.
exit: 5
ls: build/simv: No such file or directoryA missing source named in the filelist behaves the same way:
rtl/nosuch.v: No such file or directory
Preprocessor failed with 1 errors.
exit: 1Non-zero, and no artifact. Make needs no help here: the recipe line fails, the rule stops, and there is no stale image left behind.
The simulator is not honest — not because it lies, but because it is answering a different question. Run the passing design, then narrow the counter to 3 bits so the testbench's check fails, and compare what vvp returns:
vvp build/simv > /dev/null; echo "PASS run -> exit $?"
# change `define DATA_W to 3, recompile, then:
vvp build/simv > /dev/null; echo "FAIL run -> exit $?"PASS run -> exit 0
FAIL run -> exit 0Identical. Zero both times, even though the second run printed TB: FAILED. From the simulator's point of view nothing went wrong: it elaborated the design, ran to $finish, and reported what it observed. "Did the test pass?" is a question about the content of the log, and no exit status can carry it.
So the two stages of this flow need different treatment, and the Makefile in §6 does exactly that:
| Stage | How failure shows up | What the rule must do |
|---|---|---|
| compile | non-zero exit, no artifact produced | nothing extra — Make already stops |
| run | exit 0 either way; verdict is in the log | read the log and exit 1 on failure |
That asymmetry is the single most important thing to understand about automating verification. A build system can be trusted to tell you the compile worked. Whether the test passed is something your flow has to decide, and Chapter 6.4 scales that decision up to a whole regression.
5. Mapping the Flow onto Make's Vocabulary
Now the translation, and it uses only Part I's words.
| In the flow | In Make's terms | Why |
|---|---|---|
rtl/counter.v, tb/tb_counter.v | prerequisites (inputs) | written by hand; nothing produces them |
rtl/include/defines.vh | prerequisite (input) | likewise — and the one Make cannot discover |
filelists/all.f | prerequisite (input) | a file somebody maintains |
build/simv | file target | generated; has a timestamp Make can compare |
build/run.log | file target | generated from the image |
compile, run, waves, clean | phony targets | names a person asks for; produce no file of that name |
Every row is a decision from Chapter 2.2, and getting them right is most of what makes a flow behave well:
- The artifacts must be file targets, or the compile re-runs every time and the whole point is lost.
- The actions must be phony, or the day somebody creates a directory called
runthe target stops working silently. - Every input must be a declared prerequisite, or the artifact goes stale — Chapter 2.3's bug, and §8 shows it is still waiting here.
6. The Flow as a Makefile
Here it is, and every line of it was executed:
.PHONY: all compile run clean
.DEFAULT_GOAL := all
.DELETE_ON_ERROR:
SHELL := /bin/bash
BUILD := build
FILELIST := filelists/all.f
SOURCES := rtl/counter.v tb/tb_counter.v
HEADERS := $(wildcard rtl/include/*.vh)
TEST ?= smoke
SEED ?= 1
all: $(BUILD)/run.log
$(BUILD)/simv: $(FILELIST) $(SOURCES) $(HEADERS)
@mkdir -p $(@D)
@echo " [compile] $(words $(SOURCES)) sources -> $@"
@iverilog -g2012 -f $(FILELIST) -o $@
$(BUILD)/run.log: $(BUILD)/simv
@echo " [run] test=$(TEST) seed=$(SEED) -> $@"
@set -o pipefail; vvp $< +TEST=$(TEST) +SEED=$(SEED) 2>&1 | tee $@
@if grep -q "TB: FAILED" $@; then echo " [run] FAILED"; exit 1; fi
compile: $(BUILD)/simv
run: $(BUILD)/run.log
clean:
@rm -rf $(BUILD)Running it from a clean tree:
make [compile] 2 sources -> build/simv
[run] test=smoke seed=1 -> build/run.log
TB: test=smoke seed=1
TB: count=10
TB: PASSED
tb/tb_counter.v:20: $finish called at 112000 (1ps)And again, with nothing touched:
makemake: Nothing to be done for `all'.Then after editing the design:
make [compile] 2 sources -> build/simv
[run] test=smoke seed=1 -> build/run.log
TB: test=smoke seed=1
TB: count=10
TB: PASSEDThat is the whole value of the exercise in three runs: a full flow, then nothing, then exactly the work the edit made necessary.
Nothing in that Makefile is new. .DELETE_ON_ERROR: and the pipefail guard and the log check are Chapter 4.3; ?= knobs are Chapter 3.2; $@, $< and $(@D) are Chapter 3.3; the phony aggregate and .DEFAULT_GOAL are Chapter 2.4. Part II is Part I applied, not Part I repeated.
7. Why compile and run Are Both Phony and File Targets
Look again at the last four rules of §6. There are two names for each stage, and that is deliberate:
$(BUILD)/simv: # the artifact — a file target
compile: $(BUILD)/simv # the action — phonyAn engineer types make compile, which is memorable. Make tracks build/simv, which has a timestamp. If compile were the only name, it would be phony, and a phony target can never be skipped — so the simulator would run on every invocation (Chapter 2.2).
Splitting the two gives you both: a friendly name for people and a comparable file for Make. It costs one line per stage and it is the single most common shape in real verification Makefiles.
The same applies to run and build/run.log. Note the artifact and the action have different names — run versus run.log — which is what keeps run honestly phony and avoids the collision Chapter 2.2 demonstrated.
8. What Make Contributes, and the Problem Still Open
Be precise about the division of labour, because it is smaller than it looks.
Make supplied the ordering, the timestamp comparisons, the skipping, the knobs, and the decision to stop when something failed.
Make supplied nothing about Verilog. It did not parse a module, it does not know what elaboration is, and it has no idea iverilog is a compiler. Every piece of that knowledge is in two recipe lines a person wrote — which is exactly what Chapter 1.1 said and is worth re-confirming now that the tools are real.
Two consequences, one practical and one still unsolved.
Icarus merges two stages that commercial tools separate. One iverilog command both reads the sources and produces a runnable image. VCS and Xcelium split that into compile (analyse each file into a library) and elaborate (bind the hierarchy into an image), which is why their flows have three stages rather than two. The Makefile shape barely changes; the stage count does. Chapter 5.3 handles the split properly.
The filelist is still a lie, dependency-wise. The rule above lists $(SOURCES) and $(HEADERS) by hand alongside the filelist, which works for three files and is exactly the duplication Chapter 1.2 warned about: the filelist says which files the simulator reads, and a separate hand-written list tells Make the same thing. Two lists, maintained by hand, guaranteed to drift.
That has been an open problem since Chapter 1.2. Chapter 5.2 closes it.
9. Common Mistakes
Making the compile a phony target. Then it re-runs on every invocation and the flow is a shell script with extra steps.
Giving an action and its artifact the same name. A run target that writes a file called run is a collision waiting to happen (Chapter 2.2).
Naming the filelist as the only prerequisite. Editing a source then changes nothing, and the next simulation runs the old design (Chapter 1.2, and §8 above).
Trusting the simulator's exit status. Many simulators exit zero after reporting errors, which is why the rule above greps the log (Chapter 4.3).
Piping the simulator into tee without pipefail. The compile's failure disappears behind tee's success.
Putting the test name in a compile-time option. A test should be a run-time plusarg, or every test change costs a recompile. §2's testbench uses $value$plusargs for exactly that reason, and Chapter 5.3 shows what goes wrong otherwise.
10. Check Your Understanding
Question 1. In the Makefile in §6, why is there both a compile target and a build/simv target? What breaks if you keep only compile?
Question 2. rtl/include/defines.vh is never named in the iverilog command line — the design reaches it through `include. Why is it still listed as a prerequisite?
Question 3. The run rule greps its own log for TB: FAILED even though vvp already returns an exit status. Give the reason, and name the chapter that established it.
Question 4. On Icarus, one iverilog call produces a runnable image. Name the two stages a commercial simulator would split that into, and say whether the Makefile's shape has to change.
11. Exercise
Set up the project from §2 — the four source files and the filelist — then write the Makefile yourself before looking at §6. It must:
- Compile
build/simvfrom the filelist and the sources. - Run it into
build/run.log, passingTESTandSEEDas plusargs. - Fail if the log contains
TB: FAILED. - Offer
compile,runandcleanas phony names. - Default to running the simulation.
Then answer, by running it: what does a second make do, and what does make do after you change rtl/include/defines.vh from 8 to 4?
12. Remember
- Part II's examples are real. Icarus Verilog compiles and runs the design, so every command can be reproduced; the Makefile structure transfers unchanged to commercial simulators.
- A simulation flow has four kinds of thing: sources a human wrote, a filelist naming them, a compiled image, and the output of running it.
- Artifacts are file targets; actions are phony. Giving each stage both names —
build/simvandcompile— gives Make something to compare and people something to type. - An action and its artifact need different names, like
runandrun.log. - Run the tool by hand first. A known-good command line turns Makefile writing into wrapping rather than inventing.
- A test name belongs in a run-time plusarg, not a compile-time option, or changing tests costs a recompile.
- Read the log, do not trust the exit status. Simulators routinely finish successfully after reporting errors.
- Make contributes ordering and timestamps, nothing about Verilog. Every tool fact in the flow is in a recipe line somebody wrote.
- Icarus merges compile and elaborate; commercial tools separate them. The stage count differs, the Makefile shape barely does.
The flow works, and it still contains the duplication this module exists to remove: the filelist names the sources for the simulator, and a second hand-written list names them again for Make. Chapter 5.2 makes the filelist the single source of truth — including for the header that no filelist mentions.
Where this fits
Part of the Makefile curriculum.
