VCS+Verdi仿真调试全攻略:从fsdb到UCLI的高效定位技巧

发布时间:2026/10/7 9:17:23
VCS+Verdi仿真调试全攻略:从fsdb到UCLI的高效定位技巧 刚带新人的时候我最常被问的问题不是“这段代码怎么写”而是“仿真跑完出错了接下来怎么办”。很多同事做完验证环境、跑完回归一看到 failure 就直接在 log 里疯狂搜 error或者把整段 fsdb 拉出来从头盯到尾一盯一下午。说实话工具本身能帮你的远比你想象的多尤其是 VCS 和 Verdi 这套组合真正用熟了以后很多问题能在十分钟内定位到信号级。这篇就把我在实际项目里常用的 verdi/vcs debug 功能从头到尾梳理一遍从联调环境怎么搭到波形怎么看、信号怎么追、断点什么用、坑怎么躲一次性讲透。不管你刚摸到这两个工具还是已经写了几年 testbench 但一直停留在“拉波形看电平”的阶段这篇应该都能给你一点能直接上手的东西。1. 两个工具在debug链路里的分工先想清楚谁干什么很多新人容易把 VCS 和 Verdi 当成一体的东西其实这是两个定位完全不同的工具。理解清楚它们的分工后面所有操作逻辑都能理顺。1.1 VCS负责“跑”Verdi负责“看”fsdb是中间产物VCS 是事件驱动的仿真器它做的事情是把 RTL、testbench、IP 模型这些 SystemVerilog/Verilog 代码编译成一个可执行的 simv 文件然后根据仿真时间推进事件队列逐个计算信号的变化最后把结果记录下来。Verdi 是调试与波形分析环境它本身不跑仿真而是把你已经跑出来的 waveform 文件读进来做显示、反标、溯源、对比、状态机提取这些事后分析工作。两者之间的桥梁就是 fsdb 文件。DUT 里每个信号在每个时间点的跳变都会按层次结构压缩记录在 fsdb 里。Verdi 读取它之后你才能在 nWave 窗口里看到一根根红色的期望波形、绿色的实际波形。打个比方VCS 是摄像机负责把现场完整录下来fsdb 是录像带Verdi 是带剪辑台和放大镜的播放器。摄像机没开播放器再高级也没画面摄像机开了但只录了一部分画面播放器能看到的内容也受限。1.2 为什么不是直接看VCD很多从学校出来的人第一反应是 dump VCD 文件因为 VCD 是 Verilog 标准里自带的、任何仿真器都能输出的格式。但真实项目里基本没人拿 VCD 做调试主力原因很简单VCD 是 ASCII 文本格式一个中等规模的模块跑几十微秒VCD 轻松上 GB打开和加载慢到怀疑人生。fsdb 是 Verdi 自己定义的二进制格式经过压缩和分层索引设计。同样一段仿真fsdb 往往只有 VCD 的十分之一甚至更小。而且 fsdb 支持按需加载——你只看顶层的几个信号它不会强制把整棵信号树全部载入内存。对于大型 SoC 验证这个差距就是“能用”和“不能用”的区别。所以项目里通常的做法是仿真阶段输出 fsdbVCD 只作为极少数需要对外交付波形时的备份格式日常调试一律走 Verdi。1.3 一条典型的debug工作流我自己的习惯流程是固定的编译、跑仿真生成 fsdb用 Verdi 打开源码列表和 fsdb先看失败用例的 log 和波形锚点在 nWave 里把可疑信号拖进来放大到出错时刻附近看整体行为看到某个信号有 X 态或者跳变不符合预期用 Trace Drivers 溯源驱动源必要的时候打开原理图、状态机图确认逻辑关系UCLI 交互模式里打断点、改信号值验证假设。这套流程跑顺以后从拿到失败用例到定位到根因通常不会超过半小时。后面几个章节我按这条链路展开。2. 联调环境搭建一小时跑通VCSVerdi联合流程很多新人死在不该死的地方——环境没搭对fsdb 根本出不来后面全是空谈。这一章节给出一套可以直接照抄的最小配置。2.1 从filelist到simv编译选项怎么给先看一个最简单的项目结构. ├── rtl │ └── counter.sv ├── tb │ └── tb_counter.sv ├── filelist.f └── run.shfilelist.f 里按设计、验证的顺序把所有文件列进去rtl/counter.sv tb/tb_counter.sv编译命令我建议固定用这个模板vcs -full64 -sverilog \ -debug_accessall \ -timescale1ns/1ps \ -f filelist.f \ -P $VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab \ $VERDI_HOME/share/PLI/VCS/LINUX64/pli.a \ -l compile.log \ -o simv这里最关键的是两个点-debug_accessall表示保留所有调试信息。它包含了几层含义允许访问内部信号、支持 VCS/UCLI 交互调试、保留层次化引用关系。如果没有这个选项后面 Verdi 的很多功能都会失效UCLI 里也看不到内部信号值。编译慢一点、simv 体积大一点都值得调试体验换来的收益远大于这点开销。-P 选项是让 VCS 在仿真时链接 Verdi 的 PLI 库这样才能识别$fsdbDumpfile、$fsdbDumpvars这些系统任务。novas.tab和pli.a的具体路径在不同版本里可能有一点差异一般在$VERDI_HOME/share/PLI/VCS/LINUX64/目录下。检查环境变量 VERDI_HOME 是否设置正确是这类问题的第一排查方向。2.2 把FSDB dump出来testbench里的三行代码编译只是开了通道真正产生波形文件要靠 testbench 里的系统任务。最简单的方式是在 initial 块里写三行initial begin $fsdbDumpfile(top.fsdb); $fsdbDumpvars(0, dut, all); end$fsdbDumpfile指定输出文件名$fsdbDumpvars的第一个参数 0 表示 dump 全部层次的信号如果写 1 就只 dump 指定模块下一层信号写 2 就是两层以此类推第二个参数指定从哪个模块往下 dump这里写的 dut 就是你的 DUT 实例名第三个参数加all表示把数组、memory 这些内部数据也一起 dump 出来。实际调试时我会建议这么写initial begin $fsdbDumpfile(run_dir/testcase.fsdb); $fsdbDumpvars(0, tb_top, all); $fsdbDumpflush; end从 tb_top 往下 dump这样连 testbench 里的 monitor、scoreboard 信号也能看到定位 testbench 自身问题的时候不用重新跑一遍。$fsdbDumpflush是强制把缓冲区的数据刷到磁盘一般在长时间仿真后再调一次避免最后 crash 导致 fsdb 不完整。2.3 一键打开的run脚本模板仿真跑完之后打开 Verdi 的命令建议写成一个脚本我一般是这样的#!/bin/bash # 编译 vcs -full64 -sverilog \ -debug_accessall \ -timescale1ns/1ps \ -f filelist.f \ -P $VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab \ $VERDI_HOME/share/PLI/VCS/LINUX64/pli.a \ -l compile.log \ -o simv # 仿真 ./simv -l run.log fsdbautoflush # 打开波形 verdi -f filelist.f -top tb_top -ssf run_dir/testcase.fsdb -nologo 注意-top tb_top这个参数很多人会漏掉。它告诉 Verdi 从哪个模块作为顶层来加载整个层次结构。如果不给Verdi 打开后层次树可能不完整很多溯源功能用不了这是新一代最容易踩的坑。2.4 新版本VCS里更省事的命令行方式如果你的 VCS 和 Verdi 版本比较新还有一种更省事的做法编译时只要-debug_accessall仿真时直接加运行选项./simv -l run.log fsdbautoflush fsdbassert不需要在 testbench 里写任何 dump 代码simv 会自动帮你输出 fsdb 文件fsdbassert会把断言信息也导进去方便看断言具体在哪个 cycle 失败。个人觉得这个方式在快速验证场景很好用但正式回归项目里我还是倾向于用$fsdbDumpvars手动控制因为整个 testbench 里能控制 dump 的开启时间、结束时间、dump 深度灵活度高很多。3. nWave里真正提效的波形查看技巧波形窗口nWave是 Verdi 里使用频率最高的地方。这一章我挑几个自己每天都在用的功能展开讲每个都能实实在在省时间。3.1 拿到海量波形后第一件事分层和过滤很多新人打开波形以后慌慌张张把整棵信号树全部拖到波形窗口里结果几百根信号挤在一起啥也看不清。正确做法是分三步走。第一步先只看关键接口信号。把时钟、复位、你要调试的那个功能相关的使能信号、数据信号加进来数量控制在十根以内。比如调 FIFO就先加 wr_en、rd_en、wptr、rptr、dout、满空标志这些看清楚了再逐层深入。第二步用“最近波形”锚点定位出错时刻。Verdi 打开后常常会直接跳到最后一个变化点或者停在断言失败的位置。如果你知道 log 里报错的时间戳直接在 nWave 的时间轴上用键盘输入跳转时间或者用菜单里的 “Jump to Time”输入类似 52300ns 这样的值让视图停在出错点附近。第三步用波形窗口里的放大功能聚焦细节。鼠标滚轮是时间轴缩放鼠标中键框选可以直接放大一个矩形区域。建议平时把一段完整的事务波形缩放到能看到全貌再框选出异常区域放大从宏观到微观逐步收敛。3.2 Bus分组、进制切换和X态定位总线信号如果一根一根地看数出来非常痛苦。比如一个 AXI 的 awaddr 是 32 bit默认展开成 32 根线你怎么盯都容易眼瞎。在信号名上右键选择“Radix”把显示格式从二进制切成十六进制瞬间就清爽了。更推荐的做法是把多根相关信号合成一个 bus 显示。选中 wptr[3:0]、rptr[3:0] 这些信号右键 “Group” 或者 “Add Bus”然后在显示属性里制定按十进制/十六进制显示。这样你看到的是一个“值”而不是四根线对比是否满足wptr rptr 1这种关系一目了然。X 态定位是我最常用的提效功能。波形里如果某个信号是 X在信号列表里它前面会有一个红色标记而且波形显示成红色波浪线。你可以直接点击菜单里的 “Search → Next X”从当前光标位置往后搜下一个 X 态出现的时刻连续按几次就能把整个仿真里所有出现 X 的地方扫一遍。产品逻辑里不该出现 X 的信号一旦被搜出来基本就是 bug 的锚点。3.3 两个波形文件差异比对这个功能是我后来才发现的用一次就离不开了。场景是RTL 里改了一行代码想确认修改后的行为除了目标功能以外没有任何副作用。你不必用眼睛去比对修改前后的波形到底哪里不一样nWave 直接支持加载两个 fsdb 做自动对比。操作路径是 nWave 里的 “File → Compare”选择参考波形和实际波形Verdi 会以分层方式逐信号比对把存在差异的信号用特殊颜色标记出来并且在两个波形叠加的地方显示差异位置。这比人工拉参考波形再一个信号一个信号地看不知道快多少倍。用这个功能的时候建议把两个 fsdb 的名字取得有区分度比如before_fix.fsdb和after_fix.fsdb对比结果一目了然。3.4 信号修改的假设验证有时候你怀疑是某个信号在某个时刻不应该变但你又不想改 RTL 重新跑一遍耗时几小时的仿真。nWave 里有个 “Force/Deposit” 的模拟修改功能可以直接在波形上选中一段区域右键选择修改信号值把它强制改成 0 或 1。这个操作只是在 Verdi 的显示层做修改不会真的改 RTL也不会更新 fsdb 里的数据但它能帮你快速验证一个假设如果这个信号在这个时刻保持为 1下游的输出是不是就正常了如果验证成立再去改 RTL 就有针对性不用盲试。这一类“虚拟修改”非常适合用来缩小问题范围。比如抓到一个事务错误怀疑是 valid 信号提前拉高了先虚拟改掉再往下看确认后续逻辑反应。注意一点它不适合用来做严谨的功能验证只是一种辅助判断思路。4. 用nTrace/nSchema/nState从波形反推RTL逻辑看完波形你要回答的其实就一个问题这根信号为什么会在这一刻变成这个值如果靠肉眼去代码里找模块一大就完蛋。Verdi 的价值在于把波形信号和 RTL 代码、模块连接、状态转移自动关联起来。4.1 Trace Drivers一根信号背后的完整驱动链在 nWave 里选中一根信号按快捷键通常是 c或者在右键菜单里选 “Trace Drivers”Verdi 会打开源代码窗口并直接跳到驱动这根信号的代码位置。如果是组合逻辑它会定位到对应的 assign 语句或者 always_comb 块如果是时序逻辑它会定位到对应的时序 always 块再配合右键 “Trace Loads”可以看到这根信号又驱动了哪些下游信号。这一个功能基本替代了我大部分的“CtrlShiftF”全文搜索。以前找一根信号的驱动关系要在代码里来回跳几十次现在从波形直接反向追踪鼠标点两下就到源头了。源码窗口和波形窗口是联动的。你在源码里选中一个变量名nWave 里会自动高亮它的波形反过来在 nWave 里切换信号源码里也会定位到对应的声明位置。这种双向反标让“边看代码边看波形”成为习惯后调试效率是几何级提升。4.2 原理图视图扇入扇出不再需要肉眼看nSchema 是 Verdi 的自动原理图视图。对于行为级 RTL它能自动提取模块间连接关系、生成层次化的框图和连线对于综合后的网表它就是真正的门级原理图。我的建议是当问题涉及跨模块信号交互时不要只盯着一个模块的代码打开 nSchema 看整体连接。比如一个中断信号从外设出来经过中断控制器、处理器中断接口最终影响到某个状态机的跳转。这种长链路用代码追要跨越五六个文件原理图上一眼就看清。用法是在 nTrace 源码窗口里选中一个模块实例点击右键选择 “Open Schematic”Verdi 会弹出对应的原理图窗口并且支持点击任意一根连线跳到下一级展开。4.3 状态机视图非法跳转一眼定位如果你在写状态机请务必打开 nState 试一试。选中一个 FSM 信号状态寄存器右键 “Open State Diagram”Verdi 会自动根据 RTL 里的 case 语句提取出状态转移图。图上每个圆圈是一个状态每条箭头是一次合法转移。当你怀疑状态机出错的时候先把状态机图打开再对照波形看实际跳转路径。如果波形显示出现了图上不存在的箭头那基本可以确定状态跳转逻辑有 bug如果状态停在一个不该停的地方也能立刻圈出问题区域。不过我得提醒一句nState 的提取依赖标准的状态机写法。如果你的状态编码是localparam加case的标准风格通常能正确提取如果状态都是用宏定义拼出来的或者 case 嵌套特别花哨提取效果可能打折扣。4.4 事务级波形的进阶思路对总线协议验证来说nArrage 是一个高级功能。它能把总线上的控制信号、数据信号按协议解析成一个个事务显示成类似“读请求”“写响应”这样的分层事务波形。比如调 AXI它能把 AW、W、B 通道自动组合成一个完整的写事务而不是让你手动去对五组信号的时序关系。使用 nArrage 通常需要先定义事务映射可以用 Verdi 自带的常见协议模板比如 AHB/AXI 的模板也可以写脚本自己定义。如果你主要做 IP 级验证这个功能值得花时间研究对纯模块级的随机验证不一定要强求。5. 断点、UCLI与脚本化把debug变成可复用的流程看波形的调试方式是被动的——先跑完仿真再有针对性地分析。但有些问题需要你在仿真过程中介入改变信号的走向这时候就要用到 VCS 的交互式调试能力和 UCLI 接口。5.1 UCLI交互仿真运行中改信号值UCLI 是 VCS 提供的一种统一命令行接口类似一个交互式 Shell让你能在仿真运行过程中查看信号值、改变信号值、设置断点、控制仿真的启停。启动方式是在编译完 simv 后./simv -ucli进入 UCLI 提示符后常用命令run 100ns get -value tb_top.u_dut.state set tb_top.u_dut.state 2 run 100ns stop这段命令的含义是先跑 100ns查看内部状态寄存器 state 的值把它强制改成 2然后再跑 100ns 看看后续行为变化。这种“中途改信号”的能力在验证某个假设时极其有用比改 RTL 重编译快几个量级。需要注意的是UCLI 里修改变量值同样不会改变 RTL 源码只影响当前仿真的内存值。用完之后要在测试报告里明确标注这是仿真期间强制修改的结果不能当作正常行为记录。5.2 断点设置与单步调试UCLI 也支持断点命令。比如你想在仿真时间走到某一行源码时停下来bp tb/tb_counter.sv:25 run到了断点位置仿真会暂停你可以在 UCLI 里查信号值、改信号值然后run继续或者step单步执行。这项功能在调试 testbench 自身的逻辑时特别好用。比如你怀疑 scoreboard 在某个时刻误报错误用断点停在相应的比较代码行看看当时从 monitor 拿到的数据和预期数据到底差在哪。比在代码里加$display然后重新编译一遍要舒服得多。还有一种是仿真过程中的“时间断点”。UCLI 里可以直接run -to 500ns让仿真跑到指定时间点停下来这配合周期性的问题复现场景非常直观。5.3 宏控dump开关前面提到过用$fsdbDumpvars控制 dump实际项目里我更推荐用宏包一层方便随时切换ifdef DUMP_FSDB initial begin $fsdbDumpfile(testcase.fsdb); $fsdbDumpvars(0, tb_top, all); end endif编译时加defineDUMP_FSDB就打开 dump不加就完全不影响性能。大数据量的回归测试里不是每个用例都需要 dump 波形默认关掉、只在定位问题时打开能省一大把磁盘空间和仿真时间。更进一步还可以把 dump 文件名设计成跟用例名绑定initial begin $fsdbDumpfile({TEST_NAME, .fsdb}); end这样每个失败用例一个独立 fsdb不会互相覆盖也方便自动化回归脚本收集。5.4 快捷键清单和效率心得最后列几个我几乎每天都用到的快捷键都是肌肉记忆级别的操作功能操作时间轴放大/缩小滚轮或键盘 i / o适配全部波形键盘 f光标跳转到指定时间键盘 g输入时间在nWave里追踪驱动源选中信号按 c在nWave里追踪负载选中信号按 C查找下一个X态右键菜单 Search X或快捷键标记/取消标记键盘 m数值显示进制切换信号右键 Radix这些快捷键看似简单但在你反复缩放、反复追驱动的时候鼠标点菜单的时间和键盘直按的时间差出来一天能省不少分钟一个月下来效率差距就很明显。6. 联合调试的常见坑与排查思路这一章写我在带团队过程中反复遇到的问题每一个都真实遇到、真实排查过按排查链路写清楚希望你能跳过这些坑。6.1 VCS编译时报PLI相关错误典型的报错是Error: Cannot open novas.tab: No such file or directory看到这个优先检查$VERDI_HOME环境变量是否设置正确。在 shell 里执行echo $VERDI_HOME ls $VERDI_HOME/share/PLI/VCS/LINUX64/如果目录不存在说明 Verdi 安装路径设置有问题或者这个版本 Verdi 的 PLI 目录结构不同。不同版本目录名可能带版本号找到novas.tab和pli.a的实际路径再改编译脚本即可。还有一个隐蔽问题文件列表里重复 include 了 PLI 的 tab 文件。有些人习惯把编译选项塞到一个全局脚本里后面又手动加了一遍 -P就会报重复定义把多余的那份去掉就行。6.2 simv跑完却没有fsdb这个问题有几个常见原因按概率排序第一编译时没加-debug_accessall或-P导致$fsdbDumpvars没有被识别。你可以在 testbench 里故意写错一个 dump 系统任务名如果编译不报错说明 PLI 库根本没生效。第二testbench 里压根没调用$fsdbDumpfile/$fsdbDumpvars。只加编译选项是不会自动输出 fsdb 的除非你用新版本 VCS 的fsdbautoflush运行选项。第三fsdb 文件确实生成了但不在当前目录。$fsdbDumpfile里写的路径如果是相对路径最终位置取决于仿真启动时的工作目录。检查 simv 启动目录下有没有.fsdb后缀文件没有的话再到 testbench 里写全路径看看。6.3 fsdb文件打不开/打开很慢/文件巨大fsdb 很大通常不是格式问题而是 dump 的范围太广。最直接的解法是缩小$fsdbDumpvars的 dump 深度和范围比如只 dumpdut下面某一个子模块而不是整个tb_top。如果已经在跑一个巨大的仿真不想重新跑一遍可以用 Verdi 自带的 fsdb 处理工具做二次提取把感兴趣的信号导出来。这个本质上是“波形减肥”对快速验证非常有用。打开慢的另一个常见原因是直接双击 fsdb不带源码列表。Verdi 单独打开一个几百 MB 的 fsdb 会尝试猜层次结构速度很慢。正确做法是像前面说的用verdi -f filelist.f -top tb_top -ssf xxx.fsdb打开让 Verdi 借助源码信息快速建立索引。6.4 Verdi中信号只有X态看波形时发现所有信号都是红色 X通常不是仿真出错而是你没有理解 X 态的传递逻辑。X 态在仿真里本身是一种合法状态它代表“未知”可能是未复位、位宽不匹配导致高位悬空、或者多驱动冲突。处理办法是先用 “Search → Next X” 找到最早出现 X 的时刻然后沿时间轴往前找看复位释放了没有、被 X 污染的那根信号是谁驱动的。最常见的根因是某个模块的复位逻辑没有在 testbench 里被正确拉起来。还有一种情况是初始值问题。有些信号在 initial 里没有赋初值仿真 0 时刻开始就是 X一直传播下去。这类问题用*初始化块或者在 testbench 的 reset 阶段统一赋值就能解决。6.5 高频踩坑对照表现象最可能原因处理方向编译找不到 novas.tabVERDI_HOME 未设置或路径不对检查环境变量修改 -P 路径simv 跑完没有 fsdbdump 系统任务没生效或没调用检查 -debug_accessall 和 $fsdbDumpvarsfsdb 文件巨大dump 范围过宽缩小 dump 深度按模块 dump信号全部为 X未复位或初始值未赋值检查 reset 时序补充 initial 赋值波形打开后没有源码未指定 -f/-top用完整命令打开 Verdi编译太慢/内存不足-debug_accessall 开销大确认 /tmp 空间足够必要时换更小粒度 debug 选项说实话工具层面的问题大部分都能靠搜索解决真正花时间的永远是逻辑本身。但反过来如果工具用不顺定位逻辑问题的效率会被拖得很低这也是我愿意花时间把 VCS/Verdi 这些 debug 功能彻底用熟的原因。我个人在实际项目里的体会是不要急着上来就全上高级功能先把 fsdb 正确产生、波形正确打开这条链路跑稳再逐步体验断点、交互、原理图、状态机这些能力。等这些功能变成肌肉记忆你会发现自己调试的速度快得不再像从前那样慌。