
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_accessall、-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_accessall也不管项目是什么场景。短期看没问题但大工程仿真性能会被拖慢因为保留 debug 信息意味着仿真器在信号变化时有额外开销。VCS 的 debug 选项按粒度分好几档选项作用适合场景-debug_accessall所有可观测性包括信号访问、内部逻辑、UCLI 等日常 debug信号要全部能 dump、能 force-debug_accessr可读权限能 dump 信号但不能随意 force回归仿真想保留波形但不想让 debug 干扰行为-debug_accesspp极简只保留部分能力回归跑流量基本不看波形acc1允许 VPI/PLI 访问配合某些 dump 工具有些老脚本习惯用它但它和-debug_access不是替代关系我的经验是本地 debug 用-debug_accessall回归或者长时间流量用-debug_accessr或干脆不带 debug但要提前确认不需要 FSDB。别迷信全拉满真实项目里一个大型 SOC 跑 regressionall对仿真速度的影响有时候能到 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_accessall -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 专用 MakefileVERDI_HOME ? /opt/synopsys/verdi VCS_HOME ? /opt/synopsys/vcs FSDB_NAME ? tb_top.fsdb comp: vcs -sverilog v2k \ -debug_accessall -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 fsdbautoflush fsdbfsdbname$(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 阶段加fsdbautoflush。交互式 debug 时你很可能 CtrlC 中断仿真或者用 UCLI 停到某个时间点。没有 autoflushFSDB 文件不一定把最后一段时间的数据写进去。加上它数据会持续刷盘中断也不容易丢波形。另外我习惯用fsdbfsdbname...从命令行直接传 FSDB 文件名避免为了改文件名又去动 testbench、重新编译。verdi命令打开时直接指定-ssf指向波形文件并在后台运行。如果编译时带了-kdbVerdi 读取工程结构会非常快打开工程后波形、层次树、RTL 源码之间基本能无缝切换。3. Verdi 里那些真正的 debug 高频功能一次过一遍3.1 nWave波形不只是看要会搜和量Verdi 的波形窗口叫 nWave。很多人的用法就是全选信号、Add to Waveform然后眼睛盯着密密麻麻的波形这在 debug 小模块时勉强够用信号一多就抓瞎。我常用的操作有这么几个。添加信号时用Add - Signals - By Name快捷键ShiftN。想看tb_top.u_cpu.u_alu.*下所有信号直接输通配符*u_cpu.u_alu.*能省掉大半鼠标点击。波形窗口里用CtrlF按信号名搜索输入支持*通配符。缩放方面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 会显示它的驱动和负载逻辑并高亮起来。如果你在某根信号上按CtrlW或者右键选择Trace - Load/DirveVerdi 会直接跳到驱动这根信号的逻辑块比如一个 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_accessall。如果打开 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或者快捷键CtrlShiftD。对 debug 来说nSchema 最大的作用是快速理解模块内部的互联关系。尤其是一个新拿过来的模块光看 RTL 很难快速建立“哪个信号进哪个 mux、哪个 reg 是状态位”的概念。在 nSchema 里点住任意信号它会高亮并显示 fan-in/fan-out也可以直接在原理图上选中某个门再右键 trace 到 source code。这种交互体验比对着 Verilog 文本硬啃人性化得多。FSM 视图则更进一步。选中一段状态机逻辑后通过Tools - Extract FSMVerdi 会自动提取状态转移图每个状态一个圆圈转移条件标在边上。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 块里多个非阻塞赋值是否有依赖关系以及多个进程对同一信号的驱动。这里我要特别强调编译选项的作用。最怕的是编译时只acc1或者没开 debug波形里一堆信号都是黑的那就什么都查不了。用all把内部信号都 dump 下来X 态源头在 nTrace 里几乎可以一路点回去。4.2 仿真挂死CtrlC 是买回来的现场要会看现场仿真挂死最常见的是死循环或者永远在等一个信号。我 debug 挂死 case 分两步。第一步让仿真环境停下来。在运行 simv 的终端按CtrlCVCS 默认会进入 UCLI 交互模式或者直接打印当前执行的源码位置。注意看它停在哪一行如果是停在某个wait上说明在等信号如果停在一个while或for循环里大概率是死循环如果停在 SV 的final或某个后门访问可能是 VCS 自己的状态异常。第二步把停止时的波形保存下来。如果已经在运行命令里加了fsdbautoflush此时 FSDB 里已经有停止前的全部波形。如果没有可以再手动调用$fsdbDumpflush前提是编译时保留了这个能力。然后在 Verdi 里看停止前哪些信号一直不变尤其看ready、valid这类握手信号。我遇到过一起典型的死锁两个模块互相等对方置位ready形成时钟级别的握手循环。当时波形里valid和ready都一直是 1看起来没问题但仔细看使能信号发现某个active在上升沿时时机刚好错过导致状态机永远不推进。这种问题只在波形里对照时钟边沿逐拍分析才能发现肉眼扫波形几乎扫不出来。4.3 约束随机 fail先看求解器输出再看 failing propertyUVM 环境里随机约束失败randomize 返回 0是常见痛点。VCS 默认会打印约束求解失败的详细信息但输出通常很长很多人直接忽略转而在波形里找。其实第一步应该是看 log 里的约束求解结果通常会标注是哪个类的哪个 randomize 失败以及哪个约束最有可能冲突。如果是 SVA 断言在随机激励下 failed那就用 Verdi 的断言窗口打开可以看到失败时间点同时把相关信号自动加入波形再结合 nTrace 回溯一般都能找到是激励产生了非法序列还是 DUT 对某个边界输入处理不对。这里有个很容易被忽视的坑随机种子导致的不稳定。即使同一个 seedVCS 版本变了或者编译选项变了随机序列也可能变化。遇到这种情况我会先固定 seed并在回归脚本里显式写死ntb_random_seed20250101保证可复现。然后在 Verdi 里用 compare 功能对比 pass 和 fail 的波形定位分支点。5. 高频坑FSDB 为什么不生成、Verdi 为什么 trace 不了5.1 FSDB 没波形或波形不全先按这个顺序排查FSDB 相关的问题占了 debug 环节里很大一部分。常见的现象和原因现象一UCLI 或终端没有任何报错但 FSDB 文件没出现。八成是 testbench 里没调用$fsdbDumpfile/$fsdbDumpvars或者被ifdef包裹且未打开宏。确认方法编译时打开defineDUMP_FSDB或者在 testbench 里直接写死。现象二报了$fsdbDumpfile is not a system task。PLI 库没加载。检查-P参数路径是否对tab 文件中的版本号是否和当前 VCS 匹配混合版本经常出问题。现象三有 FSDB但打开后信号很少。大多是$fsdbDumpvars深度参数没设对。第一个参数如果写成某个非 0 数字表示只 dump 到该层深度。想 dump 全部子树用 0。现象四仿真崩溃后没有波形。加上fsdbautoflush或者手动定期调用$fsdbDumpflush。我习惯在最开始就写好这个 testbench 片段并且用宏来控制 dump 开关ifdef DUMP_FSDB initial begin $fsdbDumpfile(tb_top.fsdb); $fsdbDumpvars(0, tb_top, all); end endif跑长回归时不带宏纯仿真调试时单独编译一次带defineDUMP_FSDB很快就能拿到一份现场波形。5.2 Verdi 里 trace 不了、schematic 是空的、信号全是黑的这个问题的链条很长但排查起来往往就是几个选项。我的经验顺序是确认编译命令里有-debug_accessall至少-debug_accessr并且有-kdb。没有这些Verdi 反标必然不完整。确认打开 Verdi 用的 filelist 和 VCS 编译用的是同一份。很多项目有几份 filelist仿真用、综合用、覆盖率用用错了路径自然对不上。确认当前目录下有simv.daidir或编译产物。如果只有 FSDB 没有 daidirVerdi 也能开 FSDB但 trace 到源码的能力会大打折扣。如果只有波形、没有 RTLVerdi 只能当波形浏览器用不能做 nTrace。这是正常限制不是 bug。这类问题往往不是一步出错的而是多个小配置叠加最后呈现为“Verdi 打不开 trace”。我踩过最深刻的坑是把 PLI 库路径写成了另一个项目布局下的旧路径编译能过但运行时 FSDB 接口根本没生效仿真一切正常但没波形。查了整整一个下午最后用find $VERDI_HOME -name novas.tab重新定位问题5 分钟解决。5.3 波形文件太大会拖垮 Verdi怎么控制 dump 范围大型 SOC 的 FSDB 动不动几十甚至上百 GBVerdi 打开很慢还容易卡死。真正聪明的 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_accessr让仿真器处于“可读但低开销”的状态。还有命令行开关fsdbregiontb_top.u_dut0可以控制 dump 的子树密度不过这个写法比较老不同版本支持情况有差异建议以当前版本文档为准。如果确实需要保留全部波形但只想让 Verdi 打开时快一点可以在Tools - Options里限制加载层级或者只加载需要的信号。总之别把 1T 的数据全塞进 Verdi它能做不代表你应该这么做。5.4 一套我日常工作里很顺手的 debug 流程最后总结一下我现在实际用的 debug 流程给各位一个参考。先判断问题类型是编译不过、仿真跑挂、波形异常、断言失败还是性能问题。如果仿真本身没问题先跑一次带完整-debug_accessall -kdb的编译加上fsdbautoflush确保能稳定产生 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”“波形变黑”都来自脏缓存。与其浪费时间研究玄学不如重来一次干净编译通常能解决大半环境类问题。