Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Quick Start: Your First Green Build
  • Part 1: How Chips Get Made6
  • Part 2: Your Workstation5
  • Part 3: Specify and Architect3
  • Part 4: The Design Loop6
  • Part 5: The Verification Ladder6
  • Part 6: Automation and Reproducibility5
    • Makefile for the Loop
    • Exit Codes and Pass Markers
    • CI Smoke Test
    • Pinning Tool Versions
    • Smoke vs Nightly
  • Part 7: Hand-Off to the Back-End3
  • Part 8: When Things Go Wrong3
  • Part 9: Project and Next Steps3
  • Reference4
Loading lesson

‹Part 6: Automation and Reproducibility
  1. Topics
  2. Start Here: RTL and Verification
  3. Part 6: Automation and Reproducibility
  4. Makefile for the Loop

Part 6 · Automation and Reproducibility · Working (W)

Makefile for the Loop

Capture the lint, simulate, and synthesize loop in a Makefile: targets, prerequisites, recipes, variables, phony targets, and the directory that builds itself.

On this page

    Note

    Level: Working (W) · Answers: You type the same four commands every time. How do you make them one command that you can trust?

    Before this: First synthesis check

    You have typed the same four commands ten times this week: lint the design, compile it, run the simulation, synthesize it. Each one is a long line with flags you look up every time. Last Tuesday you mistyped one flag and spent an hour chasing a failure that was never in the design.

    You type the same four commands every time. How do you make them one command that you can trust?

    The answer is a build file that names every step, its inputs, and its exact command line. You type make all. The file remembers the flags so you do not have to.

    One file names every step and its inputs

    Make is a build tool: it reads a file called Makefile, works out which steps are needed, and runs them in an order that respects their inputs. A target is a named step, such as sim. A prerequisite is something the step needs first, such as a directory. A recipe is the list of shell commands that performs the step.

    Each recipe line runs in the shell exactly as written. That is the whole trick: the commands you already type by hand move into the file unchanged, and from then on there is one source of truth for how each step runs. When the flags change, the file changes, and every run after that uses the new flags.

    Read the project's file below. It is short, and every line earns its place. The top names the tools and files. The middle names the three checks. The bottom names two helpers.

    Diagram
    SHELL := /bin/bash
    IVERILOG ?= iverilog
    VVP      ?= vvp
    YOSYS    ?= yosys
    VERILATOR ?= verilator
    RTL := rtl/mini_controller.sv
    TB  := tb/tb_mini_controller.sv
    BUILD := build
    
    .PHONY: help sim lint synth all clean check-tools
    .DEFAULT_GOAL := all
    help:
    	@echo "Targets: sim lint synth all check-tools clean"
    
    all: lint sim synth
    
    $(BUILD):
    	mkdir -p $(BUILD)
    
    sim: | $(BUILD)
    	$(IVERILOG) -g2012 -Wall -s tb_mini_controller -o $(BUILD)/sim.vvp $(RTL) $(TB)
    	set -o pipefail; cd $(BUILD) && $(VVP) sim.vvp | tee sim.log
    	@grep -c "^PASS:" $(BUILD)/sim.log | grep -qx 1
    
    lint:
    	$(VERILATOR) --lint-only -Wall --top-module mini_controller $(RTL)
    
    synth: | $(BUILD)
    	$(YOSYS) -Q -T -p 'read_verilog -sv $(RTL); hierarchy -check -top mini_controller; proc; opt; check -assert; stat' > $(BUILD)/synth.log
    
    check-tools:
    	@bash scripts/check_tools.sh
    
    clean:
    	rm -rf $(BUILD)

    This is the complete build file for the running example: three checks, one default target that runs them all (set explicitly with .DEFAULT_GOAL, since the first target in the file is help), and the variables at the top.

    Variables keep tool names in one place

    The first four lines assign names: the simulator compiler, the simulator runner, the synthesis tool, and the linter. The ?= operator assigns only when the name has no value yet, so a caller can override from the command line without editing the file. The := operator assigns immediately and is used for paths that are fixed, such as the design file, the testbench, and the build directory.

    Because every recipe below refers to these names, switching a tool version or renaming a file means editing one line. Compare that with four hand-typed commands, where the same change must be remembered separately each time you type them.

    Targets, prerequisites, and the directory that builds itself

    The line all: lint sim synth says the all target needs three prerequisites, and make all runs those three steps. The sim target carries an order-only prerequisite, written | $(BUILD): make creates the build/ directory first, but the existence of that directory never counts as a reason to rerun the simulation. The directory is preparation, not an input.

    The .PHONY line lists targets that are actions rather than files: help, sim, lint, synth, all, clean, and check-tools. A phony target always runs when named, even if a file with the same name happens to exist. Without that line, a stray file called clean would convince make that there is nothing to do, and make clean would silently pass while deleting nothing.

    The help target answers the question every newcomer asks first: what can I type here? It shows the target names in one line. Write that target in every Makefile you own, and keep it accurate as targets are added.

    Which target needs which prerequisite

    The picture below draws the whole file as dependencies. Read it from top to bottom: all needs the three checks, and only sim and synth need the build/ directory created first. Lint reads the design file directly and writes nothing, so it points at nothing below it.

    Look at which boxes have arrows reaching build/: only sim and synth need the directory first, while lint needs nothing and all needs the three checks.

    Type make help, then make all

    The first command asks the file what it offers. The second runs the full loop: lint, then simulation, then synthesis, each showing its own tool line and the verdict line of the simulation.

    Diagram
    # captured terminal output
    $ make help
    Targets: sim lint synth all check-tools clean

    This output lists the available targets, so a newcomer learns the vocabulary of the project in one command.

    Diagram
    # captured terminal output
    $ make all 2>&1 | grep -E '^(PASS|iverilog|vvp|verilator|yosys)' | cut -c1-110
    verilator --lint-only -Wall --top-module mini_controller rtl/mini_controller.sv
    iverilog -g2012 -Wall -s tb_mini_controller -o build/sim.vvp rtl/mini_controller.sv tb/tb_mini_controller.sv
    PASS: mini_controller
    yosys -Q -T -p 'read_verilog -sv rtl/mini_controller.sv; hierarchy -check -top mini_controller; proc; opt; che

    This output runs the full loop on the good design: the linter line, the compile line, the verdict line starting with PASS:, and the synthesis line, measured with GNU Make 3.81 on macOS. The same make all passes on Ubuntu 24.04 with GNU Make 4.3, alongside that distribution's own Icarus 12.0, Verilator 5.020, and Yosys 0.33 tool packages.

    Predict first You run make all twice in a row without changing any file. What runs the second time?

    Everything runs again: lint, simulation, and synthesis. These targets are phony, which tells make they are actions rather than files, so make never considers them finished. That repetition is deliberate: the checks are fast enough to rerun, and rerunning removes any doubt that an old result is being mistaken for a fresh one.

    Break it on purpose: four spaces where a tab belongs

    Recipe lines must start with a tab character, not spaces. Copy the file into a scratch directory, replace the leading tab of one recipe line with four spaces, and run make. The tool refuses before running anything:

    Diagram
    # captured terminal output
    $ make
    Makefile:2: *** missing separator.  Stop.
    [exit status 2]

    This output shows the classic missing separator error, measured with GNU Make 3.81 on macOS: the recipe line the tool points at starts with spaces. Restore the tab and the file runs again. Set your editor to keep tabs in Makefiles so this failure stays a story rather than a habit.

    Three ways this step fools beginners

    • A target name that matches a real file, without phony cover. A log file called sim or a directory called lint convinces make the step is already done. Keep every action target in the .PHONY line, and be suspicious when a target finishes suspiciously fast.

    • Typing the tool command by hand with different flags. The moment you compile with flags the Makefile does not use, you have two builds and only one of them is the project's. Always go through make, even when the direct command feels shorter.

    • Running `make clean` on a file you did not read. The recipe removes a whole directory tree named by a variable. Read the clean recipe first, confirm the path names only the project's own build/ directory, and never broaden that removal to paths outside the project.

    One command you can rerun beats four you must remember

    Rule: if a step matters, it lives in the Makefile with its exact flags.

    Check yourself Add a wave target that shows the path of the recorded waveform without opening anything.

    Add two lines at the end of the file. The first names the target and the second starts with a tab character: wave: @echo build/sim.vcd The recipe shows the path of the value change dump (VCD), the file of signal changes the testbench records, so the viewer always opens the file this build wrote. If the second line starts with spaces instead of a tab, make answers with the missing separator error from this lesson.

    Check yourself Why does make sim rerun the simulation even when nothing changed?

    Because sim is listed as phony, which tells make it is an action rather than a file. Make never treats a phony target as up to date, so the simulation compiles and runs on every request instead of trusting a result from an earlier run.

    Check yourself Before you run make clean on a Makefile you did not write, what do you check?

    You read the clean recipe and confirm the removal path names only the project's own build directory. A variable holding an empty or wrong value would aim the removal elsewhere, so you verify the path before the recipe ever runs.

    Interview

    Asked: "What does .PHONY do?"

    Weak answer: It marks the important targets in the Makefile.

    Strong answer: It declares targets that are actions rather than files, so make runs them on every request instead of comparing timestamps. Without it, a file that happens to share a target's name, such as a log called sim, would make that step silently skip. All of lint, sim, synth, all, clean, help, and check-tools are phony in this project for exactly that reason.

    Follow-ups: (1) What happens if a file called clean exists and clean is not phony? (2) Why are order-only prerequisites not enough for this?

    Key takeaways

    • A Makefile names every step, its inputs, and its exact command line, so make all replaces four remembered commands.

    • Phony targets always run, variables keep tool names in one place, and order-only prerequisites prepare directories without triggering rebuilds.

    • Recipe lines start with a tab, and clean deserves a read before it ever runs.

    Keep going

    Next: Lesson 6.2, Exit Codes and Pass Markers. One command now runs the loop. The next lesson shows how a failing test makes that command fail loudly instead of passing quietly.

    Related topics

    • RTL course: Simulation and Testbenches

    • Next: Exit Codes and Pass Markers

    Previous

    30 / 47

    Next: Exit Codes and Pass Markers

    Up next

    Up nextExit Codes and Pass Markers

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • CI Smoke Test
    • Pinning Tool Versions
    • Smoke vs Nightly