仿真工程Makefile通用模板:iverilog/verilator 双后端一键切换
更新时间:2026-10-29。模板卡,属于 芯片设计入门系列。仿真器命令行一旦超过三行就该收进 Makefile——这套仿真工程Makefile通用模板把「选哪个后端、编哪些源文件、传什么参数」集中到文件头部变量,日常工作只剩一句
make。后端选型的取舍见 makefile-vs-shell-eda-mgmt,本篇给的是可直接复制进工程的那一份。
首屏答案:用一个 SIM ?= iverilog 变量做后端开关,make 默认走 Icarus Verilog(编译成 vvp 映像再执行),make SIM=verilator 走 Verilator --binary 直接编译出原生可执行仿真器;lint 目标固定用 Verilator -Wall 做静态检查,wave 看波形,regress 做多随机种子回归。模板在本机 iverilog 13.0 与 Verilator 5.020(docker 镜像)双后端实测全部 PASS,完整记录见文末终端实录。
本文要回答的问题
- 这份模板有哪些目标(target),日常 80% 的操作是哪几条?
- iverilog 与 verilator 两个后端的编译路径差异在 Makefile 里怎么表达?
.PHONY、模式规则、命令行变量覆盖各起什么作用?- 想加一个新目标(如多随机种子回归)时改哪里、加几行?
一、使用场景与日常工作流
这套模板面向的是「RTL 小工程日常仿真」:源文件一只手数得过来,但每天要在两个仿真器之间来回切换——iverilog 上手快、对 testbench 语法包容,适合边写边调;verilator 编译出的仿真器跑得快、-Wall 检查严格,适合提交前把关。模板把两条路径收进同一个 Makefile,用 SIM 变量切换。
日常工作流就四步,颜色按角色区分:蓝是日常编辑与编译动作,橙是执行仿真,绿是验证通过,紫是回归沉淀:
读这张图抓住一个分工:iverilog 是调试期的主力(改一行代码几秒就能看到波形),verilator 是提交前的裁判(-Wall 能抓出位宽、未驱动、阻塞赋值时序等问题)。两个后端在同一个 tb 上都 PASS,说明设计不依赖某个仿真器的调度癖好——这一点在「注意事项」一节有真实教训。
二、完整 Makefile 模板
下面就是模板本体,直接复制进工程根目录即可用。配套的最小示例是一个参数化计数器 counter.v 加自检 testbench tb_counter.v(代码获取见文末):
# 仿真工程通用 Makefile —— iverilog / verilator 双后端
# 用法:make 默认 iverilog 编译 + vvp 跑仿真
# make SIM=verilator 换 Verilator --binary 后端
# make lint verilator -Wall 静态检查
# make wave gtkwave 看波形
# make clean
# ---- 可配置变量(命令行可覆盖;注意行尾不要带空格再接注释)----
SIM ?= iverilog
TOP := tb_counter
SRCS := counter.v tb_counter.v
BUILD := build
WIDTH ?= 8
SEED ?= 1
# ---- 后端工具 ----
IVERILOG := iverilog
VVP := vvp
VERILATOR := verilator
.PHONY: all sim lint wave clean regress
all: sim
ifeq ($(SIM),iverilog)
# ---- Icarus Verilog 后端:先编译成 vvp 映像再执行 ----
sim: $(BUILD)/sim.vvp
$(VVP) -l $(BUILD)/sim.log $<
$(BUILD)/sim.vvp: $(SRCS) | $(BUILD)
$(IVERILOG) -g2012 -o $@ -s $(TOP) -P$(TOP).WIDTH=$(WIDTH) -P$(TOP).SEED=$(SEED) $(SRCS)
else
# ---- Verilator 后端:--binary 直接编译出可执行仿真器 ----
sim: $(BUILD)/V$(TOP)
$< | tee $(BUILD)/sim.log
$(BUILD)/V$(TOP): $(SRCS) | $(BUILD)
$(VERILATOR) --binary --timing -j 4 -Wno-fatal --Mdir $(BUILD) -GWIDTH=$(WIDTH) -GSEED=$(SEED) --top-module $(TOP) $(SRCS)
endif
# ---- 静态检查(始终走 verilator)----
lint:
$(VERILATOR) --lint-only -Wall --timing -Wno-BLKSEQ -GWIDTH=$(WIDTH) -GSEED=$(SEED) --top-module $(TOP) $(SRCS)
# ---- 看波形(tb 里需 dumpfile/dumpvars)----
wave: $(BUILD)/dump.vcd
gtkwave $< &
# ---- 多随机种子回归 ----
SEEDS ?= 1 2 3 4 5
regress:
@for s in $(SEEDS); do \
echo "== SEED=$$s =="; \
$(MAKE) -s clean; \
$(MAKE) -s sim SEED=$$s | grep -E 'PASS|FAIL' || exit 1; \
done
@echo "regress done: $(words $(SEEDS)) seeds"
$(BUILD):
mkdir -p $(BUILD)
clean:
rm -rf $(BUILD)整个文件就三块:头部变量区(所有可调项集中在这里)、ifeq 分出的两个后端实现、以及一组通用目标。换工程只需要改头部四行——TOP、SRCS、再加你工程自己的参数。
配套示例的 tb 写法也值得抄:counter.v 是一个带 WIDTH 参数的同步计数器,tb_counter.v 用下降沿驱动激励、自检后打印 [tb] PASS 或 [tb] FAIL——Makefile 只管「编译并跑起来」,跑没跑对这个判断交给 tb 自己下结论。这样 regress 这类自动化目标才能用一行 grep 收敛结果,CI 里也是同一套约定。
三、逐段讲解
头部变量区。SIM ?= iverilog 里的 ?= 是「命令行没给就用默认值」,所以 make 和 make SIM=verilator 都合法。SRCS 是唯一的源文件清单,两个后端共用——源文件只在头部出现一次,加文件不用动规则。WIDTH/SEED 这类参数同样用 ?=,命令行 make WIDTH=16 直接覆盖。
两个后端的分歧点。iverilog 的流程是「先编译成 vvp 映像、再执行映像」两步,所以 sim 依赖 $(BUILD)/sim.vvp 这个中间产物;参数用 -P<顶层>.<参数>=<值> 传入。verilator 走 --binary(5.020 支持)把 Verilog 直接编成原生可执行文件,产物是 $(BUILD)/V$(TOP),跑起来就是一条命令;参数用 -G<参数>=<值>。testbench 里有时延 # 和 @(negedge) 事件控制,所以 verilator 侧必须带 --timing,否则报 %Error-NEEDTIMINGOPT。日志收集两边都落 build/sim.log:iverilog 用 vvp -l 让仿真器自己写日志,verilator 侧用 tee 分流。
.PHONY 与目录依赖。sim/lint/wave/clean/regress 都是不产文件的「动作目标」,必须登记进 .PHONY,否则目录里一旦出现同名文件 make 就会误判「已是最新」而拒绝执行。$(BUILD) 目录作为 order-only 依赖(| 之后)出现:目录不存在就建,但目录的时间戳变化不会触发重编译。
lint 为什么固定走 verilator。iverilog 的定位是仿真器,静态检查能力有限;verilator -Wall 能报位宽、隐式 wire、多驱动等几十类问题,和 rtl-project-directory-layout 里讲的目录规范搭配使用,就是一套最小可用的工程纪律。-Wno-BLKSEQ 是因为 testbench 里 always #5 clk = ~clk 这类阻塞赋值是惯例写法,不属于设计缺陷。
四、双后端实测记录
模板配着一个真实的最小工程跑过全流程:计数器 DUT 加自检 tb,依次执行 make SIM=iverilog、make SIM=verilator、make lint、make regress。本机没有 verilator 二进制,verilator 通过 docker 镜像执行,因此把镜像调用整体传进 VERILATOR 变量覆盖默认值:
读这段实录抓四个点:第一,iverilog 后端先 iverilog -g2012 编译、再 vvp 执行,两步命令都能在上屏回显里对回 Makefile 规则;第二,verilator 后端一条 --binary --timing 编译后直接跑出同样的 [tb] PASS,两个仿真器对同一设计的判定一致;第三,make lint 在 -Wall 下零告警退出;第四,regress 目标连跑 5 个种子、每轮都重编重跑,全部 PASS 后才输出 regress done: 5 seeds。tb 里带 $display 的 PASS/FAIL 自检是这套流程的裁判,仿真器只负责跑,结论由 tb 打印。
五、注意事项
变量行尾注释前的空格会被并进变量值。模板第一版我写成 TOP := tb_counter # 顶层模块名,结果 make 把 TOP 展开成带 8 个尾随空格的串,-P$(TOP).WIDTH 被 shell 拆成两个参数,iverilog 直接报 .WIDTH=8: No such file or directory。GNU Make 只去行首空格、保留注释前的尾随空格——变量定义和注释之间不要留空格,或把注释单独成行(模板里就是这么处理的)。
testbench 激励要避开调度竞态。初版 tb 用 en <= 1'b1 非阻塞赋值、又在 @(posedge clk) 后立刻读 count,iverilog 判 PASS、verilator 判 FAIL——两种调度语义对同一拍的赋值顺序不同。改用「激励在下降沿驱动、检查也在下降沿」后双后端一致。凡是跨仿真器复用的 tb,都按这个原则写。
docker 跑 verilator 时产物也要在容器里执行。--binary 编出的是 Linux ELF,宿主是 macOS 时直接 ./build/Vtb_counter 会报 cannot execute binary file,执行这一步同样套 docker。另外 Makefile 里引用当前目录要写 $(CURDIR),写 $PWD 会被 make 当成变量 P 展开成 WD。
verilator 版本与参数风格。--binary 需要 Verilator 5.020 及以上,老版本退回「--cc + --exe + 手写 main」或只做 --lint-only;iverilog 侧 -P 参数绑定时,参数必须真实存在于被例化的模块里,绑不上会直接报错退出。
六、修改示范:加 SEED 随机种子回归
模板最后一节就是示范本身——regress 目标演示了「加一个新能力只要动三处」:
- 头部加
SEED ?= 1,并在两个后端的编译命令里分别补-P$(TOP).SEED=$(SEED)与-GSEED=$(SEED),tb 里声明同名parameter SEED接收; - 文件尾加
regress目标:shell for 循环遍历SEEDS列表,每个种子递归调$(MAKE) -s sim SEED=$$s(注意 Makefile 里字面$要写成$$),用grep -E 'PASS|FAIL'截住关键行,任何一轮失败立即exit 1; - 把
regress登记进.PHONY。
这套「递归 make + 命令行变量覆盖」的组合是回归类目标的通用写法,换成遍历 WIDTH 位宽、遍历测试用例名都一样成立。更多回归节奏的工程实践见 regression-run-frequency。
实验环境
- 机器/系统:Mac mini M4 / macOS 15(verilator 经 docker 镜像 geek-eda:0.2 执行)
- 工具版本:Icarus Verilog version 13.0 (stable) / Verilator 5.020 / GNU Make 3.81
- 日期:2026-10-29
- 完整命令:
make SIM=iverilog、make SIM=verilator VERILATOR='docker run --rm -v $(CURDIR):/work -w /work geek-eda:0.2 verilator'、make lint(同 docker 覆盖)、make regress
代码获取
完整工程(Makefile + counter.v + tb_counter.v + README)在公开仓 experiments/tpl-sim-makefile,克隆后 make 即可复现本文全部输出。
这一篇在知识体系里的位置
本篇是模板卡:向上接 makefile-vs-shell-eda-mgmt 的「为什么用 Makefile 管理仿真」选型讨论,向下给 exp-sync-fifo-verilog-iverilog 这类实验篇提供可复用的构建骨架;仿真报错类问题(编译失败、参数绑定、timescale 冲突)走 ts- 系列排查卡。
总结
仿真工程Makefile通用模板的核心是三件事:SIM ?= 一个变量切换 iverilog/verilator 双后端,SRCS 一份清单供所有目标共用,-P/-G 把参数从命令行送进 DUT。日常 80% 的操作收敛成 make、make lint、make regress 三条;iverilog 负责调试期快速迭代,verilator -Wall 负责提交前严格把关,双后端都 PASS 的设计才不依赖某个仿真器的调度癖好。