1. 先想清楚:VCS 和 Verdi 在 debug 里各扛哪块活
1.1 它们不是一个工具的两种形态,而是“跑仿真”和“看仿真”的分工
干验证的对这个组合肯定不陌生。VCS 负责把 testbench、DUT 和断言按事件驱动的规则跑起来,产出波形、log、覆盖率数据;Verdi 则负责把这些仿真产物变成可以交互观察的形式。严格说,VCS 是执行引擎,Verdi 是分析前端,两者通过 FSDB 波形文件搭桥。
我以前带过几个应届生,上来就在 Verdi 里找按钮希望它帮忙跑仿真,其实职责差异很清晰。VCS 的核心产物是 simv,也就是可执行仿真器。它根据编译时的 debug 选项决定自己保留多少可观测性。Verdi 的核心输入是 FSDB 波形和 RTL 源码,它自己不会执行任何 RTL 行为,所有的波形和数据都来自 VCS 的 dump。
所以 debug 链路严格讲是三步:编译时通过-debug_access+all、-kdb等参数让 VCS 把内部信号、内存、状态机的可见信息保留下来;运行 simv 时通过 PLI 接口调用 Verdi 提供的$fsdbDumpfile/$fsdbDumpvars等函数,把信号变化写入 FSDB 文件;最后 Verdi 打开 FSDB,结合 RTL 源码做信号级、逻辑级、状态机级的分析。
很多新手一上来只跑vcs ... -o simv && ./simv,完全不加 debug 选项,编译是过了,波形也是拿来主义,结果固定 test 稍微复杂一点,内部总线一塌糊涂,根本没法透视,只能靠打印 pin 脚碰运气。这就是没搞懂“编译期就要埋下 debug 种子”这个核心原则。
1.2 为什么推荐 VCS + Verdi 联合 debug,而不是单点工具
我知道有人会用 VCS 自带的 DVE 或者 ModelSim / Questa 来 debug,这没有错。但当一个模块有几十万行 RTL、TB 里又堆满 UVM 组件,我更倾向 Verdi。原因很实际。
FSDB 格式对信号变化做了压缩,同样一段仿真,FSDB 往往比 VPD 或 VCD 容量小很多,打开和搜索速度也快。Verdi 的 nTrace 会把 RTL 源码里的信号和波形里的信号互链,点击波形里一根红线,源码里对应的驱动点立刻高亮;选中源码里一个变量,右键就能追溯所有 load 和 driver。nSchema 能直接生成 RTL 的原理图视图,状态机也能提取出来,遇到控制路径的 bug,比对着波形数周期快得多。
所以这篇文章后面所有建议,都围绕“VCS 编译 + 仿真 + FSDB dump + Verdi 分析”这条主线展开。理解这条链路,后面遇到的很多诡异现象都能手到擒来。
2. 编译期做对三件事,debug 才不会半路崩
2.1 debug 相关编译选项怎么选,别一上来就无脑 +all
我见过太多 Makefile 里固定写-debug_access+all,也不管项目是什么场景。短期看没问题,但大工程仿真性能会被拖慢,因为保留 debug 信息意味着仿真器在信号变化时有额外开销。
VCS 的 debug 选项按粒度分好几档:
| 选项 | 作用 | 适合场景 |
|---|---|---|
-debug_access+all | 所有可观测性,包括信号访问、内部逻辑、UCLI 等 | 日常 debug,信号要全部能 dump、能 force |
-debug_access+r | 可读权限,能 dump 信号,但不能随意 force | 回归仿真,想保留波形但不想让 debug 干扰行为 |
-debug_access+pp | 极简,只保留部分能力 | 回归跑流量,基本不看波形 |
+acc+1 | 允许 VPI/PLI 访问,配合某些 dump 工具 | 有些老脚本习惯用它,但它和-debug_access不是替代关系 |
我的经验是:本地 debug 用-debug_access+all,回归或者长时间流量用-debug_access+r或干脆不带 debug,但要提前确认不需要 FSDB。别迷信全拉满,真实项目里一个大型 SOC 跑 regression,+all对仿真速度的影响有时候能到 20%~40%。
另外一个很重要的选项是-kdb,它生成 Verdi 专用的知识库,相当于把 RTL 的结构关系预先建立索引。加上它之后,Verdi 打开工程、trace 信号、提取 FSM 都会快不少,缺点是编译时间会更长、磁盘占用更大。如果项目对编译时序特别敏感,建议至少在自己的 debug 目录里开 kdb。
2.2 FSDB dump 的 PLI 接口配置,照这个模板改就行了
FSDB 波形是 Verdi 的母语。要在 VCS 仿真时输出 FSDB,关键是在编译命令里告诉 VCS,从 Verdi 安装目录加载 PLI 接口。
典型做法是找novas.tab和pli.a。Verdi 安装后,这两类文件一般在:
$VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab $VERDI_HOME/share/PLI/VCS/LINUX64/pli.a不同 Verdi 版本可能稍有差异,有的在lib/linux64/下。最稳妥的办法是用find $VERDI_HOME -name "novas.tab"定位。
然后在 VCS 编译命令里加上:
vcs -sverilog +v2k -debug_access+all -kdb \ -P $VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab \ $VERDI_HOME/share/PLI/VCS/LINUX64/pli.a \ -f filelist.f -top tb_top -o simv-P后面跟两个文件:tab 文件和库文件,顺序不能反。如果这一步没配对,最典型的报错是:
Error: $fsdbDumpfile is not a system task or function.看到这个,先检查两件事:第一,-P有没有写;第二,路径下有没有这两个文件,八成就是 PLI 库没对上。
之后在 testbench 里加 dump 逻辑:
initial begin $fsdbDumpfile("tb_top.fsdb"); $fsdbDumpvars(0, tb_top, "+all"); end$fsdbDumpvars的第一个参数是层次深度,0 表示往下全部 dump;第二个参数是顶层实例名,不是 module 名;第三个"+all"表示除了常规信号,还把变量的变化也记录下来。这里有个容易踩的坑:第二个参数一定要写实例名,如果你顶层 module 名和实例名不一致,就直接写 testbench 顶层里能看到的实例路径。最安全的方法就是在 top testbench 内部直接写$fsdbDumpvars(0, tb_top, "+all"),这个tb_top就是你当前所在的这个顶层实例自身。
2.3 用 Makefile 串联编译、仿真和打开,效率能高一大截
很多验证工程师的习惯是命令行敲一串。短项目无所谓,项目一复杂,命令越来越长,敲错一个选项就得重新编译,非常浪费时间。我个人的做法是维护一个 debug 专用 Makefile:
VERDI_HOME ?= /opt/synopsys/verdi VCS_HOME ?= /opt/synopsys/vcs FSDB_NAME ?= tb_top.fsdb comp: vcs -sverilog +v2k \ -debug_access+all -kdb \ -P $(VERDI_HOME)/share/PLI/VCS/LINUX64/novas.tab \ $(VERDI_HOME)/share/PLI/VCS/LINUX64/pli.a \ -f filelist.f -top tb_top -o simv run: comp ./simv +fsdb+autoflush +fsdb+fsdbname=$(FSDB_NAME) verdi: verdi -sv -f filelist.f -top tb_top \ -ssf $(FSDB_NAME) & clean: rm -rf simv simv.daidir csrc *.fsdb *.vpd *.log verdiLog novas.rc new*这里有一个细节非常关键:run 阶段加+fsdb+autoflush。交互式 debug 时,你很可能 Ctrl+C 中断仿真,或者用 UCLI 停到某个时间点。没有 autoflush,FSDB 文件不一定把最后一段时间的数据写进去。加上它,数据会持续刷盘,中断也不容易丢波形。另外,我习惯用+fsdb+fsdbname=...从命令行直接传 FSDB 文件名,避免为了改文件名又去动 testbench、重新编译。
verdi命令打开时直接指定-ssf指向波形文件,并在后台运行。如果编译时带了-kdb,Verdi 读取工程结构会非常快,打开工程后波形、层次树、RTL 源码之间基本能无缝切换。
3. Verdi 里那些真正的 debug 高频功能,一次过一遍
3.1 nWave:波形不只是看,要会搜和量
Verdi 的波形窗口叫 nWave。很多人的用法就是全选信号、Add to Waveform,然后眼睛盯着密密麻麻的波形,这在 debug 小模块时勉强够用,信号一多就抓瞎。
我常用的操作有这么几个。添加信号时用Add -> Signals -> By Name,快捷键Shift+N。想看tb_top.u_cpu.u_alu.*下所有信号,直接输通配符*u_cpu.u_alu.*,能省掉大半鼠标点击。波形窗口里用Ctrl+F按信号名搜索,输入支持*通配符。缩放方面,F是全视图适配,Z放大,z缩小,用顺手之后就回不去鼠标点按钮了。
光标测量也很常用。在波形区拖动鼠标选中一段,nWave 底部会显示这段的 start、end、span、delta 等信息,我经常用它来量总线两次握手之间的周期数。进制切换:选中总线信号,右键Radix选 Hex/Dec/Bin,或者直接按R循环切换,查看地址、计数值时非常方便。
还有一个实用小技巧:如果波形里看到某根信号变成红色(X 态),别急着往下翻。选中这根信号,执行View -> Zoom -> Zoom to Event(快捷键E),波形会自动缩放到第一个跳变事件。这个在 debug X 态时特别好用。
nWave 还支持把一组信号拖到同一个 bus 组里,选中一组信号按G可以创建 Group。我一般按接口分组:AXI 一组、寄存器接口一组、中断一组,看复杂交互时比单根信号来回找强太多了。
3.2 nTrace:源码和波形之间来回跳,这是 debug 的基本功
nTrace 是 Verdi 打开 RTL 后的主代码窗口,它最大的价值在于反标和 trace 路径。
反标的意思是:当你打开 FSDB 波形后,回到 nTrace 源码窗口,很多信号下面会有彩色标记线。点住一根信号,Verdi 会显示它的驱动和负载逻辑,并高亮起来。如果你在某根信号上按Ctrl+W或者右键选择Trace -> Load/Dirve,Verdi 会直接跳到驱动这根信号的逻辑块,比如一个 always 块或者一个实例的输出端口,同时用箭头把数据流路径画出来。
这个能力对找 bug 太关键了。我 debug 一个 FSM 卡状态的案例时,流程是这样的:先在波形里找到state信号,发现它卡在IDLE;然后点击波形里的state,在 nTrace 里右键跳转到state的 driver;看到 driver 的 always 块里下一个 state 依赖start && data_valid;再去波形里查start和data_valid,发现data_valid始终为 0;继续追踪data_valid的 driver,一路往前,最后定位到某个模块的复位逻辑没被释放。
这种“波形点到源码、源码点到驱动、驱动再回波形”的循环,是 Verdi debug 的核心套路。新手建议每天练一练,比乱看波形快很多。
注意,trace 功能依赖编译期的-kdb和-debug_access+all。如果打开 Verdi 之后发现 trace 不可用、信号没有任何反标,大概率是这两个选项没开。另外,Verdi 打开 RTL 时用的文件列表要和 VCS 编译时一致,路径乱了 trace 也会断。所以我一般建议直接用同一个 filelist 给 Verdi,也就是verdi -sv -f filelist.f -top tb_top -ssf xxx.fsdb。
3.3 nSchema 和 FSM:适合看控制逻辑和数据通路的上帝视角
nSchema 是 Verdi 的电路原理图视图,它会把 RTL 模块内部生成成标准的逻辑门、选择器、寄存器和连线图。打开方式:菜单Tools -> Schematic,或者快捷键Ctrl+Shift+D。
对 debug 来说,nSchema 最大的作用是快速理解模块内部的互联关系。尤其是一个新拿过来的模块,光看 RTL 很难快速建立“哪个信号进哪个 mux、哪个 reg 是状态位”的概念。在 nSchema 里点住任意信号,它会高亮并显示 fan-in/fan-out,也可以直接在原理图上选中某个门,再右键 trace 到 source code。这种交互体验比对着 Verilog 文本硬啃人性化得多。
FSM 视图则更进一步。选中一段状态机逻辑后,通过Tools -> Extract FSM,Verdi 会自动提取状态转移图,每个状态一个圆圈,转移条件标在边上。debug 控制类 bug 的时候,我可以直接看到状态到底漏掉了哪个转移条件,然后回波形里逐条核对。尤其是状态变量用 one-hot 编码时,波形里一排 0/1 根本没法人脑解码,FSM 图能瞬间把状态名显示出来。
我个人的经验是,nSchema 对结构化模块有效,比如流水线、编解码器。但那种全是assign的 glue logic 或巨型组合逻辑,原理图反而会乱成一团。这时候老老实实回 nTrace 和波形里看,别硬用护眼原理图。
3.4 Assertion、Memory、以及那些容易被忽略的小工具
断言(SVA)在验证里越来越重要。Verdi 有专门的断言窗口,统一显示assert property、cover property的 pass/fail 状态。失败的断言会以红色高亮,点击就能跳到对应的 property 源码,并且还能在波形里自动拉出相关信号。我调试随机约束失败时,经常先用断言窗口快速筛出哪个 property 挂了,再结合约束错误信息去定位,比全仿真看波形找异常快很多。
Memory 查看器(nMemory)在处理 SRAM、寄存器堆时很有用。通过File -> Open -> Memory,或者直接点击 FSDB 里的 memory 信号,可以加载整个 RAM 的初始内容和仿真过程中的变化。当你 debug 一个“读错地址”的问题时,能直接查目标地址在某个时刻存的是什么值,比一条条比对写事务队列容易得多。
另外还有几个不太知名但实际很顺手的小功能。Tools -> SimControl可以打开 VCS 的实时交互界面(UCLI),在仿真运行过程中用force、deposit、run等命令动态干预信号:
> force tb_top.u_dut.sig_a 1 > run 100ns > deposit tb_top.u_dut.cnt = 5 > run 200ns > quitTools -> Compare是波形比较器,可以拿两次仿真的 FSDB 做差分,快速找到回归新增的差异点。回归挂了却不知道哪个波形点开始分叉时,把 pass 和 fail 的波形同时打开,用 compare 定位第一个差异时间点,再按时间点去追,效率极高。
4. 调起 debug 的实战链条:三类高频问题怎么查
4.1 信号 X 态:先找第一次出现 X 的时间,别在 X 遍布全网时硬刚
X 态(未知值)是数字仿真最经典的问题。常见原因包括:寄存器没复位、多驱动冲突、case 不完整、非阻塞赋值竞争等。
我 debug X 态的思路有固定顺序。第一步,找到第一个 X 时刻。在 nWave 里选一根 X 信号,按E跳到第一个活动边沿。如果 X 已经传播到全设计,你需要往回看它起源于哪里。第二步,顺着数据流回溯 driver。在 nTrace 里对 X 信号执行 trace,上溯到驱动的 always 块,看它的条件表达式里哪些值可能是 X。第三步,确认复位条件。绝大多数 X 态都是因为某个寄存器没被复位或者复位释放时序不对。打开复位信号和该寄存器的时钟,对比释放时间。第四步,排除竞争。检查同一个 always 块里多个非阻塞赋值是否有依赖关系,以及多个进程对同一信号的驱动。
这里我要特别强调编译选项的作用。最怕的是编译时只+acc+1或者没开 debug,波形里一堆信号都是黑的,那就什么都查不了。用+all把内部信号都 dump 下来,X 态源头在 nTrace 里几乎可以一路点回去。
4.2 仿真挂死:Ctrl+C 是买回来的现场,要会看现场
仿真挂死最常见的是死循环,或者永远在等一个信号。我 debug 挂死 case 分两步。
第一步,让仿真环境停下来。在运行 simv 的终端按Ctrl+C,VCS 默认会进入 UCLI 交互模式,或者直接打印当前执行的源码位置。注意看它停在哪一行:如果是停在某个wait上,说明在等信号;如果停在一个while或for循环里,大概率是死循环;如果停在 SV 的final或某个后门访问,可能是 VCS 自己的状态异常。
第二步,把停止时的波形保存下来。如果已经在运行命令里加了+fsdb+autoflush,此时 FSDB 里已经有停止前的全部波形。如果没有,可以再手动调用$fsdbDumpflush,前提是编译时保留了这个能力。然后在 Verdi 里看停止前哪些信号一直不变,尤其看ready、valid这类握手信号。
我遇到过一起典型的死锁:两个模块互相等对方置位ready,形成时钟级别的握手循环。当时波形里valid和ready都一直是 1,看起来没问题,但仔细看使能信号,发现某个active在上升沿时时机刚好错过,导致状态机永远不推进。这种问题只在波形里对照时钟边沿逐拍分析才能发现,肉眼扫波形几乎扫不出来。
4.3 约束随机 fail:先看求解器输出,再看 failing property
UVM 环境里随机约束失败(randomize 返回 0)是常见痛点。VCS 默认会打印约束求解失败的详细信息,但输出通常很长,很多人直接忽略,转而在波形里找。其实第一步应该是看 log 里的约束求解结果,通常会标注是哪个类的哪个 randomize 失败,以及哪个约束最有可能冲突。
如果是 SVA 断言在随机激励下 failed,那就用 Verdi 的断言窗口打开,可以看到失败时间点,同时把相关信号自动加入波形,再结合 nTrace 回溯,一般都能找到是激励产生了非法序列,还是 DUT 对某个边界输入处理不对。
这里有个很容易被忽视的坑:随机种子导致的不稳定。即使同一个 seed,VCS 版本变了或者编译选项变了,随机序列也可能变化。遇到这种情况我会先固定 seed,并在回归脚本里显式写死+ntb_random_seed=20250101,保证可复现。然后在 Verdi 里用 compare 功能对比 pass 和 fail 的波形,定位分支点。
5. 高频坑:FSDB 为什么不生成、Verdi 为什么 trace 不了
5.1 FSDB 没波形或波形不全,先按这个顺序排查
FSDB 相关的问题占了 debug 环节里很大一部分。常见的现象和原因:
现象一:UCLI 或终端没有任何报错,但 FSDB 文件没出现。八成是 testbench 里没调用$fsdbDumpfile/$fsdbDumpvars,或者被ifdef包裹且未打开宏。确认方法:编译时打开+define+DUMP_FSDB,或者在 testbench 里直接写死。
现象二:报了$fsdbDumpfile is not a system task。PLI 库没加载。检查-P参数路径是否对,tab 文件中的版本号是否和当前 VCS 匹配,混合版本经常出问题。
现象三:有 FSDB,但打开后信号很少。大多是$fsdbDumpvars深度参数没设对。第一个参数如果写成某个非 0 数字,表示只 dump 到该层深度。想 dump 全部子树,用 0。
现象四:仿真崩溃后没有波形。加上+fsdb+autoflush,或者手动定期调用$fsdbDumpflush。
我习惯在最开始就写好这个 testbench 片段,并且用宏来控制 dump 开关:
`ifdef DUMP_FSDB initial begin $fsdbDumpfile("tb_top.fsdb"); $fsdbDumpvars(0, tb_top, "+all"); end `endif跑长回归时不带宏,纯仿真;调试时单独编译一次带+define+DUMP_FSDB,很快就能拿到一份现场波形。
5.2 Verdi 里 trace 不了、schematic 是空的、信号全是黑的
这个问题的链条很长,但排查起来往往就是几个选项。
我的经验顺序是:
- 确认编译命令里有
-debug_access+all,至少-debug_access+r,并且有-kdb。没有这些,Verdi 反标必然不完整。 - 确认打开 Verdi 用的 filelist 和 VCS 编译用的是同一份。很多项目有几份 filelist,仿真用、综合用、覆盖率用,用错了路径自然对不上。
- 确认当前目录下有
simv.daidir或编译产物。如果只有 FSDB 没有 daidir,Verdi 也能开 FSDB,但 trace 到源码的能力会大打折扣。 - 如果只有波形、没有 RTL,Verdi 只能当波形浏览器用,不能做 nTrace。这是正常限制,不是 bug。
这类问题往往不是一步出错的,而是多个小配置叠加,最后呈现为“Verdi 打不开 trace”。我踩过最深刻的坑是把 PLI 库路径写成了另一个项目布局下的旧路径,编译能过但运行时 FSDB 接口根本没生效,仿真一切正常但没波形。查了整整一个下午,最后用find $VERDI_HOME -name "novas.tab"重新定位问题,5 分钟解决。
5.3 波形文件太大会拖垮 Verdi,怎么控制 dump 范围
大型 SOC 的 FSDB 动不动几十甚至上百 GB,Verdi 打开很慢还容易卡死。真正聪明的 debug 不是什么都 dump,而是按需 dump。
推荐的控制方式:
initial begin $fsdbDumpfile("tb_top.fsdb"); $fsdbDumpvars(0, tb_top.u_dut, "+all"); // 对超大 memory,默认不 dump 或按需 dump // $fsdbDumpMDA(tb_top.u_dut.ram.u_mem, "+all"); end或者编译时只用-debug_access+r,让仿真器处于“可读但低开销”的状态。还有命令行开关+fsdb+region+tb_top.u_dut=0可以控制 dump 的子树密度,不过这个写法比较老,不同版本支持情况有差异,建议以当前版本文档为准。
如果确实需要保留全部波形,但只想让 Verdi 打开时快一点,可以在Tools -> Options里限制加载层级,或者只加载需要的信号。总之,别把 1T 的数据全塞进 Verdi,它能做不代表你应该这么做。
5.4 一套我日常工作里很顺手的 debug 流程
最后总结一下我现在实际用的 debug 流程,给各位一个参考。
- 先判断问题类型:是编译不过、仿真跑挂、波形异常、断言失败还是性能问题。
- 如果仿真本身没问题,先跑一次带完整
-debug_access+all -kdb的编译,加上+fsdb+autoflush,确保能稳定产生 FSDB。 - 在 Verdi 里打开 FSDB 和 RTL。先看波形整体轮廓,锁定异常时间窗。
- 从异常信号开始,用 nTrace 做 driver/load 回溯。每回溯一层,回波形里验证该层信号的时序是否符合预期。
- 如果涉及状态机,直接提取 FSM 图看状态跳转条件。
- 最终确定根因后,要么改 TB、要么改 DUT。改完重新编译仿真,对比差异。
- 如果改动和随机相关,固定 seed 并保存 pass/fail 两套波形,用 compare 定位差异时间点。
这套流程看起来简单,但实际 debug 时非常抗造。尤其是回溯 driver 的环节,几乎能解决我日常 80% 的功能问题。
最后再分享一个小技巧:VCS 和 Verdi 的版本兼容性问题非常现实,新版本 Verdi 对旧版本 PLI 库往往不兼容,出现诡异现象时,优先检查$VERDI_HOME和$VCS_HOME是不是同一套 EDA 版本配套的。换了环境之后最省事的方法,是把编译缓存(csrc、simv.daidir)清理掉重新编译一次,很多莫名其妙的“断 trace”“波形变黑”都来自脏缓存。与其浪费时间研究玄学,不如重来一次干净编译,通常能解决大半环境类问题。