Verilator 波形导出 FST/VCD:--trace-fst、harness 跟踪接口与体积实测
Verilator 与 iverilog 最大的使用差异之一,是它没有 testbench 系统任务:仿真激励和波形记录都写在 C++ harness 里,$dumpfile/$dumpvars 那一套在这里不存在。波形导不出来,不是少写了哪行 Verilog,而是 verilator 命令行开关和 harness 代码要成对配齐。本篇按正常流程走通 Verilator 的两条波形轨:--trace 产出经典 VCD 文本,--trace-fst 产出压缩二进制 FST(Fast Signal Tracer),并用同一设计真跑一遍对比两者体积。仿真波形的基本概念见 仿真是什么,iverilog 侧的对应做法见 VCD 波形导出与 GTKWave 查看。
本文要回答的问题
- Verilator 里没有
$dumpvars,波形导出靠什么开关和什么代码打开? --trace(VCD)与--trace-fst(FST)两条命令差在哪,harness 为什么要跟着换头文件和类?trace()、dump(main_time)、close()各管什么,时间不推进会怎样?- FST 是二进制,不开图形界面怎么确认文件没坏?FST 比 VCD 小多少?
这张卡是全篇主线:Verilator 的波形导出是「命令行开关 + harness 跟踪代码」双层配合,开关不同,链接进来的运行库和 harness 里实例化的类都不同;验证手段也因文本与二进制之分而不同。
一、这一步在仿真流程里的位置
Verilator 先把 Verilog 编译成 C++,再用本机 g++ 把生成代码和你写的 harness 一起编成可执行文件。波形不是仿真器自己写的,而是 harness 在每个时间点主动「喂」给跟踪对象的。
读图抓住颜色分界:蓝色是你要提供的两类输入,灰色编译只做翻译,橙色运行阶段才产波形,绿色是产物与查看。与 iverilog「testbench 里 $dumpvars 决定一切」不同,Verilator 的 testbench 就是 harness 本身,记录调用是普通 C++ 语句。
二、逐段解读:命令行开关与 harness 跟踪调用
两条编译命令只差一个参数,其余完全一致:
verilator --cc --exe --build -Wall --top-module counter \
--Mdir obj_vcd --trace counter.v tb_main.cpp # VCD 轨
verilator --cc --exe --build -Wall --top-module counter \
--Mdir obj_fst --trace-fst counter.v tb_main.cpp # FST 轨--cc 生成 C++ 而非直接执行;--exe 把 harness 当作可执行程序的一部分;--build 自动调 g++ 完成编译链接;--top-module counter 指定顶层;--Mdir 给两种格式各开一个输出目录,避免同一 harness 用不同宏重复编译时互相污染。关键差异在构建日志里看得很清楚:--trace 链接 verilated_vcd_c.o,--trace-fst 链接 verilated_fst_c.o 并额外带 -lz——FST 用 zlib 压缩,这就是它体积小的来源。
harness 用编译宏 VM_TRACE_FST 一次写好两种形态,跟踪部分固定是「全局开关、建对象、循环 dump、收尾关闭」四件事:
Verilated::traceEverOn(true); // 全局总开关,不调则 trace() 为空操作
#if VM_TRACE_FST
VerilatedFstC* tfp = new VerilatedFstC;
#else
VerilatedVcdC* tfp = new VerilatedVcdC;
#endif
dut->trace(tfp, 99); // 99 = 层次记录深度上限
tfp->open("wave.fst"); // 定输出文件名
while (main_time < MAX_TIME) {
dut->clk = !dut->clk;
dut->eval(); // 先求值
tfp->dump(main_time); // 再把当前值记录到 main_time 时刻
main_time += 5; // 固定 5ns 步长推进
}
tfp->close(); // flush 并落盘两个新手最容易漏的点:第一,traceEverOn(true) 必须在建跟踪对象之前调,否则对象建了也收不到数据;第二,dump() 的时间参数必须单调递增,且每次都要推进——忘记自加时间,所有事件挤在同一时刻,波形就是一条没有时间宽度的直线。另外赋值只在时间网格点上生效,本例在 25ns 释放复位,写成 21ns 这种永不落点的时刻,复位永远不会撤销。
三、输入输出文件:两种波形各长什么样
输入是 counter.v(8 位带使能计数器)和 tb_main.cpp;输出分别是 wave.vcd 与 wave.fst。VCD 是 ASCII 文本,开头的声明段登记层次和信号短标识,结尾的值变化段按时间戳追加,末值 b00011101 即十进制 29,与 harness 打印的 cnt=29 对上。FST 是二进制容器,file 只认得出 data,文件头也不是可读字符串,不能用 grep 验收。
不开 GUI 验证 FST 有两个可靠手段:看文件头的二进制魔数,或用 GTKWave 自带的 fst2vcd 把它转回 VCD 文本。下面这张终端实录是同一次运行的真实记录:40000 个落盘点下,VCD 658740 字节、FST 1519 字节,相差 433 倍;FST 文件头以定长二进制段开头,fst2vcd 转回的文本里 $version fstWriter、层次与信号声明齐全。
433 倍这个数字要按条件读:仿真 200000ns、每 5ns 一个 dump 点、信号只有十几个,VCD 里 95% 以上的字节是时钟在两点之间反复翻转的文本记录,而 FST 对这类规则信号做块压缩。设计越大、时钟翻转越占篇幅,FST 的优势越明显;几十信号、几千周期的小实验两者差别可能只有几倍。镜像还自带反向工具 vcd2fst,历史 VCD 资产可以转成 FST 存档。
再看数据完整性与查看侧证据:VCD 末段时间戳到 #199995,FST 转回文本的信号表与 VCD 完全对应;容器内直接跑 gtkwave 会报 Could not initialize GTK!(没有 DISPLAY),套一层 xvfb-run 即可正常起版本,镜像内为 GTKWave 3.3.116。
日常查看两条路:有桌面的机器上 gtkwave wave.fst(FST 打开比同内容 VCD 更快,查看器不用解析几十万行文本);纯命令行环境用 fst2vcd wave.fst | less 或重定向成 .vcd 再做文本比对。容器里需要图形界面时,统一写成 xvfb-run -a gtkwave wave.fst。
四、常见错与检查点
| 现象 | 检查 | 纠正 |
|---|---|---|
| 运行结束没有任何波形文件 | harness 是否漏了 traceEverOn(true)/open();编译是否带了 --trace 或 --trace-fst | 两者是 AND 关系:开关与代码必须同时配齐 |
用 --trace-fst 编译却报 verilated_fst_c.h: No such file | 头文件是否写成了无条件 include | 用 #if VM_TRACE_FST 分支选择头文件,宏由 verilator 在生成的编译参数里定义 |
| 波形只有一条竖线,所有事件同一时刻 | dump() 后的 main_time += 步长 是否漏掉或写成常量 | 时间严格单调递增,步长取时钟半周期 |
复位/激励不生效,cnt 全程为 0 | 赋值时刻是否落在时间网格上(如网格 5ns,赋在 21ns) | 改成网格点时刻(如 25ns),先 eval 再 dump |
| 两种格式分开编译出现链接混乱 | 是否两次都用默认 obj_dir | 用 --Mdir 分目录,或两次之间 make clean |
| FST 不知道坏没坏 | fst2vcd wave.fst 是否能转出带 $scope/$var 的文本 | 转不出即文件未正常 close(),检查仿真是否异常退出 |
五、真实跑一遍:最小计数器双格式对照
实验设计就是前文的 8 位计数器:时钟周期 10ns、25ns 撤销复位、使能常高,仿真到 200000ns 结束,共 20000 个时钟周期、40000 个 dump 点。两种格式用各自独立的构建目录编译运行,harness 末尾打印最终计数。完整跑法在 geek-eda:0.3-sta 容器里一条脚本完成:
docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta bash run.sh两次运行都打印 sim finished at 200000 ns, cnt=29 co=0:从 30ns 后第一个上升沿开始计数,19997 个有效上升沿对 256 取模得 29,两种格式行为完全一致,差别只在落盘格式与体积。这个实验也顺带验证了 Verilator「编译期决定波形格式」的模型——同一份 harness 源码零修改,只换命令行开关,产物就从 658740 字节的文本变成 1519 字节的压缩二进制。
工程上的选型建议很直接:日常回归和长仿真默认开 --trace-fst,磁盘与查看器加载时间都明显更省,现代波形工具(GTKWave、Surfer、Signals Viewer 等)对 FST 都是一等支持;只有两类场景保留 VCD——一是下游脚本要直接 grep/diff 波形文本做自动化检查,二是对接只认 VCD 的老旧工具链。另外 VCD 是边仿真边追加写入的,仿真中途被杀也能保留前半段;FST 的索引在 close() 时才收尾,异常退出可能得到打不开的残文件,长跑时可以在检查点主动 close() 再重新 open() 分段。
实验环境
- 机器/系统:Mac mini / macOS,Docker 运行
geek-eda:0.3-sta镜像(Debian bookworm) - 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1);GTKWave Analyzer v3.3.116;fst2vcd/vcd2fst 随 GTKWave 提供
- 日期:2026-11-03
- 完整命令:
bash run.sh(依次verilator --cc --exe --build --trace与--trace-fst构建运行,随后ls -l、file、xxd、fst2vcd、xvfb-run gtkwave --version取证)
代码获取
本文全部实验代码(counter.v、tb_main.cpp、Makefile、run.sh)已同步到公开仓库 geek-doc.cn/experiments/flow-verilator-wave-fst,克隆后在装有 geek-eda:0.3-sta 镜像的机器上执行 make vcd && make fst && make check 即可复现两种波形与体积对照。
这一篇在知识体系里的位置
本篇是开源 EDA 仿真流程中 Verilator 路线的「波形导出」正向操作说明:仿真是什么、testbench 与 DUT 的关系见 仿真是什么;iverilog 路线的波形导出与 GTKWave 布局见 VCD 波形导出与 GTKWave 查看,VCD 导不出时的排查见 iverilog 仿真跑完无波形输出;写 Verilator 测试平台时另一类高频坑——不支持延迟语句——见 Verilator timing delay unsupported。
总结
Verilator 波形导出 FST/VCD 靠「命令行开关 + harness 跟踪代码」成对配合:--trace 链接 verilated_vcd_c 出文本 VCD,--trace-fst 链接 verilated_fst_c 与 -lz 出压缩 FST,harness 里用 VM_TRACE_FST 宏切换 VerilatedVcdC/VerilatedFstC,固定走 traceEverOn(true)、trace(tfp, 99)、open()、循环 eval() 后 dump(main_time) 且时间单调推进、结尾 close() 的套路。实测同一 8 位计数器、20000 周期下 VCD 658740 字节、FST 1519 字节,相差 433 倍;FST 是二进制,用 fst2vcd 转文本或在 GTKWave 3.3.116(容器内套 xvfb-run)中打开查看。