2. FSDB波形生成与信号记录策略

FSDB(Fast Signal Database)是 Verdi 的原生波形格式,相比 VCD 具有更高的压缩率和更快的随机访问速度。但 FSDB 文件并不是仿真器自动产生的,它依赖仿真过程中由 PLI/VPI 接口注册的系统任务来驱动。理解 $fsdbDumpfile 与 $fsdbDumpvars 的语义,以及如何按层次、模块、信号做选择性记录,是控制波形规模与调试效率的第一道关口。

两个核心系统任务的分工

$fsdbDumpfile 负责“开文件”,它决定波形写到哪个路径、文件大小上限是多少;$fsdbDumpvars 负责“选内容”,它决定从哪个层次开始、往下递归几层、是否记录多维数组和内存、是否记录强度信息等。二者必须配合使用:只调用 $fsdbDumpfile 而不调用 $fsdbDumpvars,会得到一个空文件;反之,若未先指定文件名,$fsdbDumpvars 会使用默认文件名(通常为 novas.fsdb)并可能覆盖上一次结果。

initial begin
  $fsdbDumpfile("wave.fsdb", 2048);   // 文件名 + 大小上限(MB)
  $fsdbDumpvars(0, tb_top, "+all");   // 从 tb_top 起全层次记录
end

第一个参数 0 表示“不限深度”,即从指定层次递归到最底层;若写 1,则只记录该层次本身及其直接子层次;写 2 则再往下多一层。第二个参数是起始层次,通常用顶层 testbench 名或某个 DUT 实例名。"+all" 是常用选项,表示连同多维数组、memory、实数等一并记录;若只关心普通信号,可省略它以减小文件。

按层次与模块选择性 dump

全芯片 dump 在大型 SoC 中往往产生几十 GB 的 FSDB,既拖慢仿真又让 Verdi 打开缓慢。更实际的做法是分层记录:对关键模块(如 CPU 核、DMA 控制器、总线仲裁器)做完整 dump,对已验证稳定的外设只记录接口信号。实现方式有两种。

第一种是多次调用 $fsdbDumpvars,每次指定不同层次和深度:

$fsdbDumpfile("soc.fsdb", 4096);
$fsdbDumpvars(0, tb_top.u_cpu);        // CPU 全层次
$fsdbDumpvars(1, tb_top.u_dma);        // DMA 仅顶层+子层
$fsdbDumpvars(0, tb_top.u_bus, "+mda"); // 总线含 memory/数组

第二种是使用 $fsdbDumpvarsByFile,把要记录的信号清单写在一个文本文件里,每行一个层次路径,适合信号数量多、需要版本管理的场景。此外还有 $fsdbDumpoff / $fsdbDumpon 这对任务,可以在仿真运行中动态暂停和恢复记录,常用于跳过复位初始化阶段或只抓取特定事务窗口。

多时钟域与复位阶段的记录策略

多时钟域设计里,各域信号翻转频率差异很大。若统一按最快时钟采样,慢时钟域信号会被大量冗余采样点淹没;若统一按最慢时钟,快域又会丢失细节。FSDB 本身按事件记录,不强制统一采样率,但仿真器在 $fsdbDumpvars 时仍会受 +fsdb+ 类选项影响。实践中更稳妥的做法是:对每个时钟域分别指定 dump 范围,让 Verdi 在查看时按域分组,而不是在 dump 阶段强行统一。

复位阶段同样值得单独处理。复位期间大量寄存器被强制到已知值,波形上表现为密集跳变,但对调试价值有限。可以在复位释放后再调用 $fsdbDumpon,或把复位逻辑单独放在一个不 dump 的层次中。下面这张图展示了从仿真启动到波形落盘的完整信号链,以及各环节可施加的控制点。

FSDB 波形生成信号链与控制点 仿真内核 事件驱动 / 时钟沿 PLI/VPI 注册 $fsdbDumpfile $fsdbDumpvars 选择性记录 层次 / 深度 / +all Dumpon / Dumpoff FSDB 文件 压缩 / 随机访问 文件名 / 大小上限 默认 novas.fsdb 深度 0=全层次 +mda 记录数组/内存 快时钟域 高频翻转 / 细节多 慢时钟域 低频翻转 / 冗余少 复位阶段 密集跳变 / 可跳过 Dumpon 窗口 只抓有效事务 控制点作用于信号链的不同环节:文件级控制大小,层次级控制范围,时间级控制窗口

文件大小与性能的权衡

FSDB 的大小主要由三部分决定:被记录信号的数量、信号翻转的次数、以及每个信号的数据宽度。假设一个 32 位总线在 100 MHz 下每周期都翻转,仿真 1 ms 就产生约 105 次事件;若同时记录 1000 根这样的信号,文件很容易达到数 GB。压缩算法能降低存储,但无法消除记录本身带来的仿真开销——每次信号变化都要经过 PLI 回调写入缓冲区。

因此权衡的核心是“记录什么”而非“怎么压缩”。一个可操作的判断依据是:先只 dump 顶层接口和待调试模块,运行一次仿真观察 FSDB 大小和仿真耗时;若文件小于 2 GB 且仿真减速在 20% 以内,再逐步加入下一层信号。反之,若某模块加入后文件翻倍而调试中几乎不看,就应把它移出 dump 范围。

FSDB 文件大小上限参数($fsdbDumpfile 的第二个参数)单位是 MB。当文件达到上限时,仿真器会停止写入并给出警告,而不是自动滚动覆盖。若需要长时间仿真,建议配合 $fsdbDumpoff 分段记录,或使用 +fsdb+autoflush 选项控制缓冲区刷新频率。
不要在同一仿真中多次调用 $fsdbDumpfile 指向不同文件而不关闭前一个,这可能导致文件句柄泄漏或波形错乱。若确实需要分段,应先用 $fsdbDumpoff 暂停,再重新指定文件名并 $fsdbDumpon。

一个可复用的配置片段

下面给出一个面向多时钟域 SoC 的 dump 配置示例,假设顶层为 tb_top,含 CPU、DMA、总线三个主要子系统,复位信号为 rst_n。该片段在复位释放后才开始记录,并对不同子系统采用不同深度。

initial begin
  $fsdbDumpfile("soc_debug.fsdb", 4096);
  $fsdbDumpvars(0, tb_top.u_cpu, "+all");   // CPU 全量
  $fsdbDumpvars(2, tb_top.u_dma);           // DMA 两层
  $fsdbDumpvars(1, tb_top.u_bus);           // 总线接口层
  $fsdbDumpoff;                             // 先关闭
  @(posedge tb_top.rst_n);                  // 等复位释放
  #100;
  $fsdbDumpon;                              // 再开启记录
end

这段配置的效果是:复位期间不产生波形,复位释放 100 ns 后开始记录,CPU 内部所有信号可见,DMA 只看到两层,总线只看接口。实际项目中可根据调试目标调整层次和深度,但“先小后大、按需扩展”的原则普遍适用。