硬件工程 Makefile:sim 仿真、synth 综合与 clean 清理的三目标组织
更新时间:2026-11-04。单个文件的 iverilog 编译加 vvp 运行 可以手敲,但一个真实硬件工程每天都在三件事之间切换:改完 RTL 立刻仿真、仿真过了跑一遍 yosys 综合 看资源数、下班前清理产物。本篇把这三件事收进同一个 Makefile:变量集中管文件、文件目标带依赖自动增量、
.PHONY动作目标对外只暴露sim/synth/clean。仿真 Makefile 的双后端模板见 仿真工程 Makefile 通用模板,Makefile 与 Shell 脚本的分工见 Makefile 和 Shell 脚本哪个适合 EDA 工程管理。
本文要回答的问题
- 一个硬件工程的 Makefile,变量、文件目标、动作目标各管什么?
sim/synth/clean三个目标的依赖关系怎么写,才能改一个.v只重编必要的产物?- 仿真输入为什么走 filelist(
-f src.f),而不是把文件名散在每条命令里? - 综合工具装在 Docker 镜像里时,Makefile 里怎么写容器调用,才不把宿主机路径写死进日志?
先给一张总览卡,再逐段展开:
读这张卡抓住分层主线:变量回答「工程里有什么」,文件目标回答「哪个产物由谁生成」,.PHONY 目标回答「我想让 make 做什么动作」。新手最常见的毛病是把所有东西都写成动作目标,结果每次敲 make 都全量重编,增量机制完全闲置。
一、Makefile 在硬件流程里的位置
一个最小数字前端工程,源码只有两类:rtl/ 下的可综合设计与 tb/ 下的 testbench。Makefile 位于它们之上,不产生新的验证方法,只把同一份源码按两个方向加工:往仿真方向喂给 iverilog/vvp,往综合方向喂给容器里的 yosys;两个方向的产物都收进可整体删除的 build/。
读这张图注意两个边界:仿真和综合共享同一份 RTL_FILES,但仿真额外吃 testbench,综合绝不读 tb——这就是变量要把 RTL_FILES 与 TB 分开声明的原因;两条加工线的产物全部落在 build/,所以 clean 只需删一个目录,不存在满项目找临时文件的问题。
二、Makefile 逐段解读:变量、依赖与目标
完整 Makefile 不到五十行,分四段看。
变量段把工程里所有会变的东西提到文件顶部:
RTL_FILES := rtl/counter4.v
TB := tb/tb_counter.v
FILELIST := src.f
TOPLEVEL := counter4
BUILD_DIR := build
SIM_OUT := $(BUILD_DIR)/sim.vvp
NETLIST := $(BUILD_DIR)/$(TOPLEVEL).netlist.v
IVFLAGS := -g2012 -Wall
EDA_IMG := geek-eda:0.4-flow
DOCKER = docker run --rm -v "$$PWD":/work -w /work $(EDA_IMG)RTL_FILES 与 TB 分开,是因为仿真清单含 testbench、综合只读可综合代码;TOPLEVEL 被网表文件名和 yosys 的 -top 同时引用,换工程只改这一处。注意 DOCKER 用递归赋值 = 且写成 $$PWD:$$ 在 make 里转义成单个 $,真正展开交给 shell,make 回显的命令是 -v "$PWD":/work,宿主机绝对路径既不写死在 Makefile 里,也不会出现在终端日志中。
仿真文件目标是增量编译的核心:
$(SIM_OUT): $(RTL_FILES) $(TB) $(FILELIST)
mkdir -p $(BUILD_DIR)
iverilog $(IVFLAGS) -o $@ -f $(FILELIST)
sim: $(SIM_OUT)
vvp -n $(SIM_OUT)目标 build/sim.vvp 把 RTL、TB、filelist 三者列为依赖:make 先比较时间戳,只有依赖比目标新才重跑编译。-f $(FILELIST) 让文件清单独立成 src.f(一行一个文件,// 可写注释),工程从一个 RTL 文件涨到十几个时只改清单,不碰 Makefile;filelist 的组织细节见 iverilog 多文件工程组织。sim 是动作目标,本身不产出名为 sim 的文件,它只负责「确保 .vvp 最新,然后运行」。
综合目标复用同一套依赖思想,把命令换成容器内 yosys:
$(NETLIST): $(RTL_FILES)
mkdir -p $(BUILD_DIR)
$(DOCKER) yosys -p "read_verilog $(RTL_FILES); synth -top $(TOPLEVEL); stat; write_verilog $(NETLIST)"
synth: $(NETLIST)-p 把一串 yosys 命令塞进一次调用:读 RTL、以 TOPLEVEL 为顶层做通用综合、stat 打印资源统计、最后写出网表。容器以当前目录挂载为 /work,所以 yosys 看到的全是相对路径,产物通过挂载目录直接回到宿主的 build/。
收尾段声明清理、检查与波形目标,并把所有动作目标写进 .PHONY:
.PHONY: all sim synth lint wave clean help
lint:
iverilog $(IVFLAGS) -tnull -f $(FILELIST)
wave: sim
gtkwave $(BUILD_DIR)/wave.vcd
clean:
rm -rf $(BUILD_DIR)-tnull 是只编译不仿真的语法检查,适合绑到编辑器保存动作上。.PHONY 不可省:万一目录里真出现一个叫 sim 的文件,没有声明时 make 会认为「sim 已存在且无依赖,无需做事」,目标静默失效。
三、输入输出文件:吃什么、吐什么
| 文件 | 角色 | 被哪个目标消费 |
|---|---|---|
rtl/counter4.v | 可综合 DUT:4 位带使能计数器,异步复位、carry 进位 | sim 与 synth 共享 |
tb/tb_counter.v | 自校验 testbench,检查复位、保持、计数到 15、回卷四段,打印 TEST PASS | 仅 sim |
src.f | 仿真文件清单,列出 RTL 与 TB,注释说明顺序 | 仅 sim |
build/sim.vvp | iverilog 字节码,只能交同版本 vvp 执行 | 中间产物 |
build/wave.vcd | testbench 内 $dumpfile 产出的波形,供 make wave 打开 | 调试产物 |
build/counter4.netlist.v | yosys 综合后的门级网表,头部带 (* top = 1 *) 标记 | 下游交付物 |
DUT 只有十几行:always @(posedge clk or negedge rst_n) 描述异步复位与使能计数,assign carry = (q == 4'd15) & en; 描述进位。testbench 四个检查点全过才打印 TEST PASS tb_counter。
四、常见错与检查点
| 现象 | 先查什么 | 纠正 |
|---|---|---|
改了 RTL,make sim 却不重新编译 | 文件目标的依赖列表是否漏列源文件;改完是否在同一秒内(GNU Make 3.81 时间戳粒度为 1 秒) | 依赖列全 $(RTL_FILES) $(TB) $(FILELIST);脚本里两次构建间隔不足 1 秒时 sleep 1 |
make sim 提示 Nothing to be done 或目标莫名不执行 | 是否漏了 .PHONY,目录里恰好有同名文件 | 所有动作目标统一进 .PHONY |
综合把 testbench 也读进去,报 $dumpfile 不可综合 | yosys 的 read_verilog 是否用了仿真 filelist | 综合只读 $(RTL_FILES),TB 只属于仿真线 |
iverilog 告警 no explicit time unit | RTL 文件是否缺 `timescale | RTL 首行补 `timescale 1ns/1ps,或确认全工程统一从 TB 继承 |
别人的机器上 make synth 报挂载路径不存在 | Makefile 是否写死了本机绝对路径 | 用 $$PWD 经 shell 展开,镜像名用变量 EDA_IMG |
这些是正常流程上的检查点,工具自身报错的处理不在本篇范围;想给流程加批量用例,读 iverilog 回归脚本流程,想先做静态检查读 Verilator lint 首次上手。
五、真实跑一遍:三目标与增量重编
在 Icarus Verilog 13.0、GNU Make 3.81、容器内 Yosys 0.33 下实跑一轮:make clean 后连续两次 make sim,再 touch 一个 RTL 文件触发增量,随后 make lint 与 make synth,最后再次 make clean。仿真与增量段实录如下:
读这份实录抓三个点。首次 make sim 先执行 iverilog -o build/sim.vvp -f src.f 再 vvp,testbench 打印 TEST PASS tb_counter,退出码 0;第二次只回显 vvp -n build/sim.vvp——编译行消失,因为三个依赖都不比 .vvp 新,增量判断生效;touch rtl/counter4.v 后编译行重新出现,改一个文件只重建仿真产物,不碰任何其他东西。实录里的 timescale 告警是真实结果:RTL 没写 `timescale,对照上表补一行即可消除。
综合段 make synth 的资源统计原样摘录如下,13 个单元里 4 个带使能 DFF 正好对应 4 位寄存器:
=== counter4 ===
Number of wires: 11
Number of wire bits: 20
Number of cells: 13
$_ANDNOT_ 2
$_DFFE_PN0P_ 4
$_NAND_ 2
$_NOT_ 1
$_OR_ 1
$_XNOR_ 1
$_XOR_ 2实验环境
- 机器/系统:macOS(Darwin 25.5,Apple Silicon),Docker 24.0.7
- 工具版本:Icarus Verilog 13.0(stable,原生
/usr/local/bin);GNU Make 3.81;Yosys 0.33(git sha1 2584903a060,运行于geek-eda:0.4-flow容器) - 日期:2026-11-04
- 完整命令:
make clean→make sim(首次编译运行)→make sim(跳过编译)→sleep 1 && touch rtl/counter4.v && make sim(增量重编)→make lint→make synth(容器内 yosys 出网表)→make clean
代码获取
完整 RTL、自校验 testbench、filelist 与 Makefile 已同步到公开仓库:geek-doc.cn/experiments/flow-make-hardware-project,克隆后进入该目录执行 make sim 即可看到 TEST PASS;make synth 需要本机装有 Docker 并已拉取 geek-eda:0.4-flow 镜像,只跑仿真则 iverilog 12/13 均可,make clean 可随时回到干净状态。
这一篇在知识体系里的位置
- 单条仿真命令的四步闭环(写 TB、编译、vvp、VCD):读 仿真是什么;
- 仿真 Makefile 的 iverilog/verilator 双后端通用模板:读 仿真工程 Makefile 通用模板;
- filelist、
-f清单与-y库目录的完整组织:读 iverilog 多文件工程组织; - 三目标之上的批量用例循环与 PASS/FAIL 判定:读 iverilog 回归脚本流程;
- yosys 综合的第一步全流程:读 yosys 首次综合。
总结
硬件工程 Makefile 按三层组织:变量层用 RTL_FILES、TB、TOPLEVEL、BUILD_DIR 集中声明工程信息,仿真输入走独立 filelist,综合与仿真共享 RTL 但不共享 TB;文件目标 build/sim.vvp、build/counter4.netlist.v 把源文件列为依赖,make 凭时间戳自动决定是否重建,改一个 .v 只重编必要产物;sim、synth、lint、wave、clean 这些不产生同名文件的动作目标一律进 .PHONY,容器调用用 $$PWD 经 shell 展开以保持日志干净。全部产物只进 build/,清理永远只是一句 make clean——变量、依赖、动作三层分清,这个四十行的 Makefile 就能覆盖日常 90% 的仿真与综合操作。