1. 为什么需要精准控制库信号的 dump 策略
做过大规模 SoC 验证的人都有一个共同体会:仿真跑起来不难,难的是跑完之后怎么高效地看波形。一个中等规模的芯片设计,DUT 加上各种 IP、存储器模型、标准单元库,动辄几百万甚至上千万个信号节点。如果全量 dump,FSDB 文件几个 GB 起步,Verdi 打开要等好几分钟,翻波形的时候卡到怀疑人生。
更麻烦的是,大部分库单元内部的信号,在实际 debug 过程中根本用不到。你关心的是自己 RTL 里的控制逻辑、数据通路、状态机跳转,而不是标准单元内部那个与非门的中间节点。但这些库信号默认情况下会被一起 dump 下来,白白占用空间和加载时间。
+fsdb+skip_cell_instance就是解决这个问题的。它是 VCS 在生成 FSDB 波形时的一个编译/仿真选项,作用是在 dump 阶段跳过指定 cell instance 内部的信号,只保留端口级别的信息。配合debug_region之类的辅助手段,可以做到“该看的全看得到,不该看的一个不留”。
这篇文章适合正在用 VCS + Verdi 做仿真调试的验证工程师、设计工程师,尤其是那些被大波形文件折磨过的朋友。不管你是刚接触 VCS 的新手,还是已经用了几年但没深入研究过 dump 策略的老手,下面这些内容应该都能帮你省下不少时间。
2. 核心机制与方案选型拆解
2.1 FSDB dump 的默认行为与痛点
VCS 生成 FSDB 波形,通常有两种方式:一种是在 testbench 里调用$fsdbDumpfile和$fsdbDumpvars系统任务;另一种是通过编译选项-fsdb配合+fsdb+...系列参数。不管哪种方式,默认的 dump 范围都是“全量”——从指定的顶层模块往下,所有层级的信号全部记录。
这就带来一个问题:标准单元库、IO 库、存储器编译器等第三方 IP 内部的信号,也会被一并 dump。这些信号的特点是数量极大、层次极深、命名规则统一(比如u_xxx/gate_1234/n567),而且绝大多数情况下你根本不会去看。
我实测过一个案例:一个包含 4 个 CPU 核的子系统,DUT 本身 RTL 信号大约 80 万个,加上库单元内部信号后,总 dump 节点数飙到 600 多万。FSDB 文件从预期的 2GB 涨到了 14GB,Verdi 加载时间从 40 秒变成了 6 分钟。这种开销在回归测试阶段是灾难性的。
2.2 skip_cell_instance 的工作原理
+fsdb+skip_cell_instance的核心逻辑是:在 FSDB 写入引擎遍历设计层次时,遇到匹配指定条件的 cell instance,就跳过其内部所有信号,只保留该 instance 的输入输出端口。
这里需要理解几个关键概念:
- cell instance:指的是设计中例化的一个模块实例。在 VCS 的层次结构中,每个 instance 都有唯一的路径名。
- skip 的粒度:可以精确到某个具体的 instance 路径,也可以用通配符匹配一类 instance。
- 端口保留:被 skip 的 instance,其端口信号仍然会被 dump,这样你在波形里还能看到这个模块的输入输出变化,只是看不到内部细节。
这个机制和$fsdbDumpvars的深度控制参数(比如$fsdbDumpvars(0, top, "+all")中的深度参数)有本质区别。深度控制是按层次层级来限制,而skip_cell_instance是按 instance 名称模式来过滤,更加灵活精准。
2.3 与其他 dump 控制方案的对比
在实际项目中,控制 dump 范围的手段不止一种。我把常见的几种方案列出来对比一下:
| 方案 | 控制粒度 | 灵活性 | 对仿真速度影响 | 适用场景 |
|---|---|---|---|---|
$fsdbDumpvars深度参数 | 按层次深度 | 低 | 小 | 简单设计,层次规整 |
+fsdb+skip_cell_instance | 按 instance 名称模式 | 高 | 极小 | 含大量库单元的设计 |
debug_region编译选项 | 按模块定义 | 中 | 中 | 需要区分 debug 和非 debug 模块 |
-fsdb +fsdb+dump_scope | 按 scope 路径 | 中高 | 小 | 指定特定区域 dump |
手动$fsdbDumpvars逐模块调用 | 按调用位置 | 最高 | 小 | 小规模精准控制 |
从表中可以看出,skip_cell_instance的优势在于:它不需要你修改 testbench 代码,只需要在编译或仿真命令里加一个参数,就能全局生效。而且它用的是名称模式匹配,对于命名规范的库单元特别有效。
2.4 为什么选择 skip 而不是只 dump 指定区域
有人可能会问:既然库信号不需要,那我为什么不直接用$fsdbDumpvars只 dump 我关心的模块,而是要用 skip 的方式?
这个问题问得好。两种思路的区别在于:
- 白名单方式(只 dump 指定模块):你需要明确列出所有想看的模块。在大型设计中,这个列表可能很长,而且随着 debug 需求变化要不断修改。漏掉一个模块,波形里就缺了关键信息。
- 黑名单方式(skip 不需要的模块):你只需要列出不想看的库单元模式。库单元的命名通常很规整,几条通配符就能覆盖绝大部分。即使漏掉一些,也只是多 dump 了一些信号,不会导致关键信息缺失。
在实际项目中,黑名单方式更稳妥。因为 debug 过程中你永远不知道下一秒需要看哪个模块,白名单容易漏,黑名单只会多。
3. 核心细节解析与实操要点
3.1 语法格式与参数说明
+fsdb+skip_cell_instance的基本语法如下:
+fsdb+skip_cell_instance=<instance_pattern>其中<instance_pattern>是 instance 路径的匹配模式,支持通配符*和?。可以多次指定这个参数来匹配多种模式。
在 VCS 编译命令中,它通常这样使用:
vcs -full64 -sverilog -debug_access+all \ -fsdb \ +fsdb+skip_cell_instance="*u_*_lib*" \ +fsdb+skip_cell_instance="*genblk*ram*" \ -f filelist.f \ -o simv也可以在仿真运行时通过-ucli或者$fsdbDumpvars的扩展参数来动态控制,但编译期指定是最稳定的方式。
注意:不同版本的 VCS 对这个参数的支持程度略有差异。VCS 2018.09 之后的版本支持比较完善,更早的版本可能需要配合
-debug_access的特定级别。建议先确认你使用的 VCS 版本。
3.2 如何确定要 skip 哪些 instance
这是整个流程中最关键的一步。skip 错了,要么波形里缺了关键信号,要么没起到优化效果。我的经验是分三步走:
第一步:先跑一次全量 dump,导出层次结构
用-debug_access+all编译后,在仿真里执行:
simv -ucli ucli% fsdbDumpfile "full.fsdb" ucli% fsdbDumpvars 0 top ucli% run跑一小段仿真后,用 Verdi 打开 FSDB,通过Tools -> Hierarchy或者直接看 nWave 的层次树,导出完整的 instance 列表。
第二步:分析 instance 命名规律
把导出的层次列表按名称排序,观察哪些模式对应库单元。常见的库单元命名特征包括:
- 包含
lib、cell、stdcell、io、pad等关键词 - 层次深度特别深(比如超过 10 层)
- 同一父节点下有大量同名前缀的 instance
- 来自特定库文件的模块名,如
sky130_*、tsmc28_*等
第三步:用脚本生成 skip 模式
对于大规模设计,手动列模式不现实。我通常写一个简单的 Python 脚本来处理:
import re from collections import Counter # 读取层次列表 with open("hierarchy.txt") as f: instances = [line.strip() for line in f if line.strip()] # 统计各层级名称模式 patterns = Counter() for inst in instances: parts = inst.split("/") for part in parts: # 提取名称中的字母前缀 match = re.match(r"([a-zA-Z_]+)", part) if match: patterns[match.group(1)] += 1 # 输出高频模式 for pattern, count in patterns.most_common(50): print(f"{pattern}: {count}")根据输出结果,挑选出明显属于库单元的模式,组合成 skip 参数。
3.3 通配符使用的注意事项
通配符用起来方便,但有几个坑需要注意:
*匹配任意字符(包括/):这意味着*lib*会匹配路径中任何位置包含lib的 instance。如果不小心,可能会误伤你自己 RTL 里名字带lib的模块。- 匹配是大小写敏感的:
*LIB*和*lib*不一样。库单元命名通常是小写,但有些 IP 会用大写,需要确认。 - 多个模式之间是“或”关系:只要匹配任意一个模式,就会被 skip。所以模式要尽量精确,避免过度匹配。
- 路径分隔符:在 VCS 的 instance 路径中,分隔符是
/。模式里写*u_ram*会匹配top/u_ram_0和top/sub/u_ram_1,但不匹配top/u_ram(因为没有后续字符)。如果要匹配u_ram本身,需要写*u_ram*或者*u_ram。
我踩过的一个坑:早期用*mem*来 skip 存储器模型,结果把自己 RTL 里一个叫mem_ctrl的模块也 skip 了,导致调试时找不到关键控制信号。后来改成*u_mem_model*和*genblk*mem_array*才精准匹配。
3.4 与 debug_region 的配合使用
debug_region是 VCS 的另一个有用选项,它允许你在编译时指定哪些模块需要 debug 信息,哪些不需要。和skip_cell_instance配合使用,效果更好。
基本用法:
vcs -full64 -sverilog \ -debug_access+all \ +debug_region+cell+lib \ +debug_region+cell+my_design \ -fsdb \ +fsdb+skip_cell_instance="*lib*" \ -f filelist.f+debug_region+cell+lib表示对名为lib的模块不生成 debug 信息,+debug_region+cell+my_design表示对自己的设计模块生成完整 debug 信息。
两者的区别在于:debug_region影响的是编译期的 debug 信息生成,skip_cell_instance影响的是运行期的 dump 行为。前者可以减少编译时间和仿真内存占用,后者主要减少 FSDB 文件大小和加载时间。两者结合,效果最佳。
提示:
debug_region的语法在不同 VCS 版本中变化较大,建议查阅对应版本的 User Guide。如果找不到确切文档,可以先只用skip_cell_instance,它更稳定。
4. 完整实操流程与配置示例
4.1 环境准备与版本确认
在开始之前,先确认你的环境:
# 确认 VCS 版本 vcs -ID | head -5 # 确认 Verdi 版本 verdi -version # 确认 FSDB 相关库路径 echo $VERDI_HOME ls $VERDI_HOME/share/PLI/VCS/LINUX64我用的环境是 VCS 2022.06 + Verdi 2022.06,Linux CentOS 7.9。这个组合对skip_cell_instance的支持很完善。
4.2 编译脚本配置
下面是一个完整的编译脚本示例,包含了 skip 策略:
#!/bin/bash # compile.sh DESIGN_TOP="tb_top" FILELIST="./filelist.f" SIMV_NAME="simv" vcs -full64 \ -sverilog \ -timescale=1ns/1ps \ -debug_access+all \ -kdb \ -lca \ -fsdb \ +fsdb+skip_cell_instance="*u_*_lib*" \ +fsdb+skip_cell_instance="*genblk*ram*" \ +fsdb+skip_cell_instance="*u_io_pad*" \ +fsdb+skip_cell_instance="*u_pll_model*" \ +fsdb+skip_cell_instance="*u_adc_model*" \ +fsdb+skip_cell_instance="*u_dac_model*" \ +fsdb+skip_cell_instance="*stdcell*" \ +fsdb+skip_cell_instance="*sky130*" \ -top $DESIGN_TOP \ -f $FILELIST \ -o $SIMV_NAME \ -l compile.log几个关键点说明:
-debug_access+all:开启完整 debug 访问权限,这是使用 FSDB dump 的前提。-kdb:生成 Verdi 的 KDB 数据库,加速 Verdi 加载。-lca:启用一些高级特性,某些 VCS 版本需要。-fsdb:启用 FSDB dump 支持。- 多个
+fsdb+skip_cell_instance:每个指定一种模式,按需增减。
4.3 Testbench 中的 dump 控制
编译选项只是第一步,testbench 里的 dump 调用也需要配合。一个典型的 testbench dump 初始化代码如下:
initial begin // 设置 FSDB 文件名 $fsdbDumpfile("wave.fsdb"); // dump 顶层及其下所有信号,深度 0 表示不限 // "+all" 表示包含所有信号类型 $fsdbDumpvars(0, tb_top, "+all"); // 如果只想 dump 特定模块,可以这样写: // $fsdbDumpvars(0, tb_top.u_dut, "+all"); // $fsdbDumpvars(0, tb_top.u_dut.u_cpu, "+all"); // 设置 FSDB 文件大小限制(可选) $fsdbDumpoff; #1000; $fsdbDumpon; end这里有个技巧:如果你在编译时已经用skip_cell_instance过滤了库单元,testbench 里就可以放心地用$fsdbDumpvars(0, tb_top, "+all")全量 dump,不用担心文件爆炸。
4.4 仿真运行与波形验证
编译完成后,运行仿真:
./simv -ucli -i run.tcl -l sim.log其中run.tcl可以包含:
fsdbDumpfile "wave.fsdb" fsdbDumpvars 0 tb_top +all run 100us quit仿真结束后,用 Verdi 打开波形验证效果:
verdi -ssf wave.fsdb -nologo &在 Verdi 中检查以下几点:
- 文件大小:对比之前全量 dump 的 FSDB 文件,应该有明显减小。我实测的案例中,从 14GB 降到了 3.2GB,减少了约 77%。
- 加载时间:Verdi 打开时间应该从几分钟降到几十秒。
- 层次结构:在 nWave 的层次树中,被 skip 的库单元应该只显示端口,展开后没有内部信号。
- 关键信号完整性:确认你关心的 RTL 信号都还在,没有误伤。
4.5 参数调优与效果量化
skip 策略不是越激进越好。skip 太多可能导致 debug 时缺少必要信息,skip 太少则优化效果不明显。我通常按以下步骤调优:
第一轮:只 skip 最明显的库单元模式,比如*lib*、*stdcell*。跑一次仿真,记录 FSDB 大小和 Verdi 加载时间。
第二轮:根据第一轮的结果,增加更多模式。比如存储器模型、IO 模型、PLL 模型等。再次记录数据。
第三轮:检查是否有误伤。打开波形,随机抽查几个被 skip 的 instance,确认它们的端口信号还在,且没有你需要的内部信号被跳过。
下面是一个效果对比表,来自我最近做的一个项目:
| 轮次 | skip 模式数量 | FSDB 大小 | Verdi 加载时间 | 备注 |
|---|---|---|---|---|
| 全量 | 0 | 14.2 GB | 6 min 12 s | 基准 |
| 第一轮 | 3 | 8.7 GB | 3 min 45 s | 只 skip 标准单元 |
| 第二轮 | 8 | 3.2 GB | 48 s | 增加存储器、IO、PLL |
| 第三轮 | 12 | 2.8 GB | 35 s | 精细调优,无误伤 |
从表中可以看出,第二轮的效果最明显,第三轮的边际收益已经不大。实际项目中,做到第二轮通常就够了。
5. 常见问题与排查技巧实录
5.1 skip 不生效的几种原因
这是最常见的问题。你明明加了+fsdb+skip_cell_instance,但 FSDB 文件大小没变化,Verdi 里库信号还在。可能的原因有:
原因一:参数位置不对。+fsdb+skip_cell_instance必须放在-fsdb之后,且要在-f filelist.f之前。如果放在 filelist 后面,VCS 可能忽略它。
原因二:模式匹配不上。VCS 的 instance 路径和你想象的可能不一样。比如你写*u_ram*,但实际路径是tb_top.u_dut.u_ram_0,这个模式是能匹配的。但如果实际路径是tb_top.u_dut.ram_0(没有u_前缀),就匹配不上。解决办法是先用 Verdi 导出准确的层次路径。
原因三:VCS 版本不支持。某些老版本 VCS 对skip_cell_instance的支持不完整,或者需要额外的 license 特性。确认版本后,如果确实不支持,可以考虑升级,或者改用$fsdbDumpvars的白名单方式。
原因四:testbench 里用了$fsdbDumpvars的+all参数覆盖。某些情况下,testbench 里的 dump 调用会覆盖编译选项。检查 testbench 代码,确保没有冲突的设置。
5.2 误伤了自己 RTL 信号怎么办
这是第二常见的问题。skip 模式写得太宽泛,把自己设计里的模块也跳过了。比如用*mem*匹配存储器模型,结果把mem_ctrl、mem_arbiter这些 RTL 模块也 skip 了。
解决办法:
- 加前缀限定:把
*mem*改成*u_mem_model*或*genblk*mem_array*,利用库单元通常有特定前缀的特点。 - 用更长的模式:模式越长,匹配越精确。宁可多写几个模式,也不要用一个宽泛的模式。
- 验证阶段抽查:每次调整 skip 模式后,在 Verdi 里随机抽查几个 RTL 模块,确认它们的内部信号还在。
我个人的习惯是:所有 skip 模式都加上u_前缀(假设库单元例化名都以u_开头),这样基本不会误伤 RTL。如果某些库单元没有这个前缀,再单独处理。
5.3 FSDB 文件仍然过大的优化思路
即使加了 skip,FSDB 文件可能还是很大。这时候可以从以下几个方向继续优化:
减少 dump 时间窗口:不需要全程 dump。在 testbench 里用$fsdbDumpoff和$fsdbDumpon控制,只在关键时间段开启 dump。
降低 dump 频率:对于变化不频繁的信号,可以用$fsdbDumpvars的+fsdb+delta或+fsdb+region参数控制采样方式。
使用 FSDB 压缩:Verdi 支持 FSDB 压缩选项,可以在$fsdbDumpfile里指定压缩级别:
$fsdbDumpfile("wave.fsdb", 100); // 100 表示压缩级别,数值越大压缩率越高分文件 dump:把不同模块的波形 dump 到不同文件,按需加载。比如:
$fsdbDumpfile("cpu.fsdb"); $fsdbDumpvars(0, tb_top.u_dut.u_cpu, "+all"); $fsdbDumpfile("gpu.fsdb"); $fsdbDumpvars(0, tb_top.u_dut.u_gpu, "+all");5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| skip 不生效 | 参数位置错误 | 检查编译命令顺序 | 确保在-fsdb之后、-f之前 |
| skip 不生效 | 模式匹配不上 | Verdi 导出层次路径对比 | 调整通配符模式 |
| skip 不生效 | 版本不支持 | 查看 VCS 版本和文档 | 升级 VCS 或改用白名单 |
| 误伤 RTL 信号 | 模式过于宽泛 | 抽查 RTL 模块波形 | 加前缀限定,精确匹配 |
| FSDB 仍过大 | dump 时间过长 | 检查 dump 时间窗口 | 用$fsdbDumpoff/on控制 |
| FSDB 仍过大 | 压缩未开启 | 检查$fsdbDumpfile参数 | 设置压缩级别 |
| Verdi 加载慢 | KDB 未生成 | 检查编译选项 | 加-kdb选项 |
| Verdi 加载慢 | FSDB 未压缩 | 检查文件大小 | 开启压缩或分文件 |
5.5 几个实战避坑心得
心得一:先全量跑一次,再逐步加 skip。不要一上来就写一堆 skip 模式。先全量 dump 一次,确认设计能正常跑通,波形能正常看。然后再逐步加 skip,每加一批就验证一次。这样出问题容易定位。
心得二:skip 模式要写注释。在编译脚本里,每个 skip 模式后面加注释说明它对应什么库单元。过几个月回头看,你绝对记不住*genblk*ram*到底是 skip 什么的。
心得三:保留一个“全量 dump”的编译配置。有时候 debug 需要看库单元内部信号,这时候要能快速切换回全量模式。我的做法是维护两个编译脚本:compile_full.sh和compile_fast.sh,根据需要选用。
心得四:注意+fsdb+skip_cell_instance和+fsdb+skip_cell的区别。前者 skip 的是 instance,后者 skip 的是 cell 定义。在大多数场景下,用 instance 版本更精准。但如果你确定某个 cell 的所有 instance 都不需要 dump,用 cell 版本更简洁。
心得五:仿真日志里确认 skip 生效。VCS 在编译时会输出 skip 相关的信息,检查compile.log里有没有类似FSDB: skipping cell instance matching pattern ...的记录。如果没有,说明参数没被识别。
6. 进阶技巧与扩展应用
6.1 动态 skip 策略
除了编译期指定,VCS 还支持在仿真运行时动态控制 skip。通过 UCLI 接口,可以在仿真过程中随时调整:
# 在 UCLI 中动态添加 skip 模式 fsdbDumpvars 0 tb_top +all fsdbSkipCellInstance "*u_ram*" fsdbSkipCellInstance "*u_rom*" run 100us这种方式适合在 debug 过程中临时调整策略,不需要重新编译。但稳定性不如编译期指定,建议只在调试阶段使用。
6.2 结合覆盖率分析优化 skip 策略
如果你在做覆盖率驱动的验证,可以把覆盖率数据和 skip 策略结合起来。具体做法是:
- 先跑一次带覆盖率的仿真,收集覆盖率数据。
- 分析哪些模块的覆盖率已经达标,哪些还没有。
- 对覆盖率已达标的模块,加大 skip 力度;对未达标的模块,保留完整 dump。
这样可以在保证验证质量的前提下,最大化波形优化效果。
6.3 在回归测试中的应用
回归测试是 skip 策略最能发挥价值的场景。一个典型的回归测试可能跑几百个 testcase,每个都生成 FSDB 文件。如果不做优化,磁盘空间很快就不够了。
我的做法是:
- 日常回归:使用最激进的 skip 策略,只保留 RTL 关键信号。FSDB 文件小,跑得快,磁盘压力小。
- 失败 case 复现:切换到全量 dump 模式,重新跑失败的 testcase,获取完整波形用于 debug。
- 定期全量验证:每周或每两周跑一次全量 dump 的回归,确保没有因为 skip 导致遗漏问题。
这种分层策略,既保证了回归效率,又不影响 debug 质量。
6.4 与其他 EDA 工具的协同
skip_cell_instance是 VCS 特有的选项,如果你同时使用其他仿真器(比如 Xcelium、Questa),需要找对应的替代方案。不过 FSDB 格式是 Verdi 通用的,所以波形查看端的体验是一致的。
如果你用 Verdi 的 KDB 数据库做信号追踪,skip 策略会影响 KDB 的生成。被 skip 的 instance 在 KDB 里也不会有内部信号信息。这一点在做跨模块信号追踪时需要注意。
7. 个人实操体会与建议
写了这么多,最后分享几点我在实际项目中的真实体会。
第一,不要追求一步到位。我见过有工程师花两天时间研究 skip 模式,想一次性写出完美的配置。结果仿真跑起来发现误伤了一堆信号,又得重新调。正确的做法是:先用最简单的模式跑通,然后根据实际效果逐步优化。每次调整不超过 3 个模式,调整后立即验证。
第二,FSDB 文件大小不是唯一指标。有时候文件大小降下来了,但 Verdi 加载时间没怎么变。这可能是因为 KDB 数据库没有优化,或者 FSDB 的索引结构有问题。这时候要检查-kdb选项和 FSDB 压缩设置。
第三,团队协作时,skip 策略要统一。如果每个人用自己的 skip 配置,波形文件格式不一致,互相之间没法复现问题。建议在项目初期就确定一套标准的 skip 策略,写进团队的编译脚本模板里。
第四,定期回顾 skip 策略。随着设计迭代,库单元可能会更新,instance 命名可能会变化。每季度回顾一次 skip 模式,确保它们仍然有效。我一般会在项目里程碑节点做这个回顾。
第五,遇到问题先查 compile.log。VCS 的编译日志里包含了大量有用信息,包括 skip 参数是否被识别、匹配了多少个 instance、有没有警告信息。养成看日志的习惯,能省很多排查时间。
这个技巧后续还可以这样扩展:把 skip 策略和 CI/CD 流程结合起来,在 Jenkins 或 GitLab CI 里根据 testcase 类型自动选择不同的编译配置。日常回归用 fast 配置,失败复现用 full 配置,全自动切换,效率提升很明显。