
做数字验证这几年我越来越觉得VCS和Verdi这套Synopsys的组合拳就是吃饭的家伙。很多刚入行的朋友总把精力放在写testbench、搭UVM环境上结果一到调试阶段就卡壳波形出不来、信号找不到、断点不会下一个bug查半天。其实debug才是验证工程师每天花时间最多的地方VCS负责把仿真跑起来Verdi负责把问题看清楚两者配合好了效率能翻一倍。这篇文章我就把自己平时用得最顺手的VCS/Verdi调试功能整理一遍把那些容易踩坑的细节也一并说清楚。如果你是刚接触数字IC验证的新手或者在用Verdi看波形时总觉得别扭这篇应该能帮你省不少事。1. 环境准备与联合仿真链路搭建1.1 为什么大多数团队选择VCSVerdi组合先说个很多人问过我的问题仿真器那么多Mentor的Questa、Cadence的Xcelium都能用为什么偏偏是VCS和Verdi的组合在数字IC验证里占据统治地位。答案其实很简单VCS的编译速度和仿真性能在大型SoC项目里确实有优势而Verdi对FSDB格式波形的解析效率非常高Debug Trace的追踪能力也强两者同属Synopsys生态配合天然紧密。Verdi本身不仅能看波形它还集成了源代码分析、断言调试、覆盖率查看等功能相当于一个完整的调试工作台。你想想看如果仿真器用Questa波形用Verdi中间需要做格式转换虽然也能跑通但每次dump波形、重新加载、跨工具跳转的损耗都在浪费你时间。同一个生态里VCS直接生成FSDBVerdi直接打开省去一层转换而且在层次化信号追踪、UVM对象查看这些功能上Verdi和VCS的联动深度是其他组合比不了的。还有一个关键点是很多公司流片前的sign-off流程就是基于VCS跑的回归你在本地用VCS调试最终提交到服务器上跑的也是VCS环境一致性最好不会出现“本地仿真通过、回归失败”的尴尬。所以我一直建议刚入行的朋友如果公司有VCS和Verdi的license就踏踏实实把这套工具链用熟这属于投入产出比很高的技能。1.2 VCS编译选项速查与FSDB波形生成配置要让VCS正确编译并生成Verdi能看的FSDB波形核心是两件事编译时加上对应的dump库支持仿真时在testbench里调用$fsdbDumpvars任务。这里我直接给你一个最常用的、经过验证的编译命令模板适用于大部分纯RTL或者带UVM环境的验证平台。vcs -sverilog -debug_accessall -f filelist.f -timescale1ns/1ps \ -ntb_opts uvm -l com.log -o simv \ -P $VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab \ $VERDI_HOME/share/PLI/VCS/LINUX64/pli.a解释一下几个关键选项。-debug_accessall 表示打开完整的调试访问能力包括信号存取、断点、单步等如果没有这个选项Verdi里很多交互功能都用不了。-ntb_opts uvm 必须加它会自动带上UVM相关的宏定义和库文件省得你手动去include一堆东西。-P参数后面跟的是Verdi的PLI接口文件这个必须指向你安装Verdi的对应路径否则仿真时调用$fsdbDumpvars会报找不到任务的错误。编译通过后会生成simv可执行文件运行simv之前还要在testbench里写好dump波形的代码。我一般放在顶层模块的initial块里这样最直观initial begin $fsdbDumpfile(top.fsdb); $fsdbDumpvars(0, top_tb, all); end第一个参数0代表dump整个设计层次如果改成1就只dump当前层级不往下展开。第二个参数是顶层实例名第三个参数all表示把所有信号类型都dump出来。有些项目为了省空间会只dump指定信号比如$fsdbDumpvars(0, top_tb.dut.u_cpu, all)只抓CPU子模块的信号这样波形文件会小很多仿真速度也能上来。另外还有两个实用任务$fsdbDumpflush用于在仿真过程中手动刷新波形缓冲区$fsdbDumpoff和$fsdbDumpon则用来临时暂停和恢复波形记录。我在做长时间仿真时经常会用$fsdbDumpoff把无关紧要的阶段跳过去只dump关键阶段的波形既省硬盘又省调试时间。1.3 启动Verdi的几种方式与文件输入Verdi启动方式不外乎两种先启动Verdi再通过菜单加载文件或者直接在命令行里把文件路径带进去。命令行方式我推荐大家养成习惯效率高得多。verdi -f filelist.f -ssf top.fsdb -top top_tb -f指定RTL文件列表-ssf指定FSDB波形文件-top指定顶层模块。这样打开后nWave波形窗口和源代码窗口会同时加载好直接就能开始分析。-ssf这种写法不仅支持fsdb也支持vcd、evcd等格式如果团队里有人用Questa仿真但是想用Verdi看波形你可以让他导出VCD文件然后用这个命令打开也能凑合着用。还有一种更灵活的方式是在Verdi源码窗口里通过菜单File-Open来加载文件或者用快捷键CtrlL快速打开文件。对于经常改代码的调试场景我建议用命令行启动加载waveform save文件的方式Verdi允许你在nWave里保存当前的波形配置包括哪些信号、什么颜色、什么进制保存成.rc文件下次调试时直接verdi -ssf top.fsdb -wsd top.rc所有显示配置一键恢复省去每次重新拉信号的麻烦。2. 波形调试的核心操作技巧2.1 快速定位信号与层次导航Verdi打开波形后我最常用的功能就是nWave窗口里的信号搜索。按键盘上的/键直接弹出搜索框输入信号名字的一部分比如clk或者axi_awready它能瞬间把匹配信号列出来点击就能添加到波形窗口。这个方法在处理几千个信号的SoC验证时特别管用比在波形图里人工翻工整多了。层次化导航是Verdi的另一大杀器。在源代码窗口里双击一个模块实例名会直接跳转进入这个子模块在波形窗口里右键信号选择Highlight或者Find Declaration源代码窗口自动跳到这个信号的声明位置。这个功能在追RTL bug时极其顺手从波形里看到axi_awready在某个周期拉低了右键跳到代码一眼就能看到对应的赋值逻辑然后按F8添加书签继续看下一个信号整个流程非常紧凑。Verdi还支持用Draw Bus把多根bit信号拼成总线显示。比如一个32位的数据线如果拆成32根单bit信号看人眼根本分不清。选中这些信号后右键选择Bus Operations-Create Bus设置基数和位宽就能合并成一根总线信号。反过来也可以把总线信号展开成bit来看这在定位某个bit翻转异常时非常有用。2.2 总线拆分与Memory波形查看说到Memory很多人在Verdi里看寄存器堆或者SRAM内容时不知道从哪里下手。其实nWave窗口有专门的Memory功能。在信号列表里右键一个memory类型的信号选择Open Memory Instance会弹出Memory窗口能按地址查看存储内容。更厉害的是你可以在Memory窗口里设置Memory DifferencesVerdi会自动对比两个不同时间点的存储内容把变化的地方高亮出来。这对于调试初始化类问题特别有用。我之前遇到过一个caseCPU启动后DMA搬运的数据总是少几个字节排查了很久。后来就是靠Memory窗口对比仿真时间和100us时刻的SRAM内容发现某一段地址的数据在搬运前就被覆盖了顺着改写的毛刺信号一路追下去最后定位到是总线仲裁器的优先级配置问题。总线拆分方面还有个小技巧在nWave窗口里选中总线信号后可以在Value栏直接改成十六进制、二进制、十进制显示快捷键是直接按H/B/D。十六进制看地址和数据最直观二进制看控制信号的bit状态最清楚。这个切换操作我一天要用几十次可以说是最基础但最容易忽略的效率点。2.3 波形比较与光标测量Verdi的波形比较功能在回归调试里非常实用。比如同一个测试用例修改代码前后各跑一次仿真各出一份fsdb然后在nWave里选择File-Load Simulation把两边的波形都加载进来。Verdi会用波形叠加的方式把两个仿真结果画在同一个窗口里信号发生跳变差异的地方会用特殊颜色高亮你一眼就能看出代码改动影响了哪些信号。光标测量也是日常调试的高频操作。nWave波形窗口底部有当前光标所在的时间戳按字母m可以放一个覆盖整个窗口的参考线用来量两个信号沿之间的间隔。测量时钟周期、计算延迟都靠它。还有一个细节当你把鼠标悬停在信号翻转沿上时Verdi会显示当前的时间和值这在确认某个信号是否在预期时刻拉高时非常方便。我个人最常用的组合是在波形窗口按t把时间线缩放到合适范围然后按住鼠标中键拖动来缩放比去点工具栏上的缩放按钮快得多。这些快捷键一旦手熟了调试节奏会顺滑很多。2.4 常用快捷键快查表这里整理一份我日常靠肌肉记忆完成的操作新手可以打印出来贴在显示器边上操作快捷键说明在nWave中搜索信号/弹出信号搜索框在源代码中查找CtrlF代码内文本搜索跳转到信号声明位置右键-Find Declaration快速定位RTL源码标记断点/书签F8源码和波形窗口均可添加光标m用来测量信号间隔波形缩放鼠标中键拖动 或 t横向缩放时间轴信号值进制切换H/B/D16进制/2进制/10进制打开信号列表窗口CtrlW管理已添加的波形说实话你不需要刻意去背每天打开Verdi反复用几周这些键位就自然记住了。关键是用的时候有意识去按快捷键而不是每次都去点菜单栏。3. 交互式调试断点、TCL与UVM调试技巧3.1 $stop断点与VCS交互式运行调试过程中如果波形文件太大或者仿真时间太长手动在RTL里加$stop是最高效的办法。$stop的作用是让仿真停在当前时刻并把控制权交回给用户这时候你就可以直接在VCS的交互shell里操作。具体做法是在怀疑有问题的代码位置加上$stop;重新编译运行simv仿真会在执行到这一行时暂停。这时候你会看到类似“Stopping at time ... in scope ...”的提示然后进入VCS的交互命令行。你可以输入run 500ns继续跑500纳秒或者输入run -all跑完剩余仿真也可以输入指令查看信号当前值比如call \$display(%m :: axi_awready %b, top_tb.dut.u_axi.axi_awready)Tcl命令里的$转义要特别注意。如果你直接写call $display(...)VCS会尝试把$display当成变量展开然后报错所以要么转义成$display要么用花括号把命令包起来call {$display(...)}。这也是我踩过好几回坑之后才记住的细节。3.2 Verdi的Debug模式与单步调试Verdi不仅仅是看波形的工具它还内置了交互式调试模式。当你通过VCS仿真停在$stop时也可以打开Verdi的连接模式在Verdi界面里直接做单步执行、查看变量值、查看寄存器状态相当于把仿真器嵌进了图形界面里。具体操作是用下面的命令启动Verdiverdi -f filelist.f -ssf top.fsdb -top top_tb -debug加上-debug参数再配合VCS侧把仿真器设为交互模式通常需要在编译时加-debug_accessall然后Verdi的Debug菜单下会有类似Run、Step Over、Step Into、Continue的选项点击后Verdi会控制仿真器运行。这个模式在调试顺序逻辑、状态机跳转、协议握手的时序关系时特别直观你能一边看代码执行到哪一行一边看波形实时变化。不过说实话大多数做验证的朋友平时用得更多的是纯波形调试$display打印很少会专门开Verdi的debug模式因为交互式调试对仿真性能有一定影响大型设计跑起来会比较慢。但如果是定位状态机卡死、确认某段RTL是否执行到这类问题这个方法比大海捞针看波形要快得多。我的建议是简单问题用波形打印难点问题才开debug模式不要所有场景都用重武器。3.3 TCL命令行脚本化调试Verdi的TCL命令支持是容易被忽略但价值很高的功能。当你需要反复对多个信号做同样的操作时手戳GUI会非常低效写成脚本一次搞定才是正道。举一个我实际用过的例子调试AXI总线的outstanding传输时我需要在波形里频繁查看awaddr、wdata、bready等几十根信号的跳变沿。手动添加信号太浪费时间于是我写了一个简单的TCL脚本set sigs { top_tb.dut.u_axi.awaddr top_tb.dut.u_axi.awvalid top_tb.dut.u_axi.awready top_tb.dut.u_axi.wdata top_tb.dut.u_axi.wvalid top_tb.dut.u_axi.wready top_tb.dut.u_axi.bvalid top_tb.dut.u_axi.bready } foreach s $sigs { add -var $s } set radix -hex保存成add_sigs.tcl之后每次打开Verdi就在命令行里输入source add_sigs.tcl信号就批量加好了还能把每个信号的进制统一设成16进制。在长期调试同一个模块的时候这套方法非常节省时间。Verdi还支持在TCL脚本中控制仿真的暂停和继续配合$stop断点使用可以实现“跑一段→检查一组信号→决定是否继续跑”的半自动调试流程。我自己在做长时间的功耗验证时常用这个模式脚本里一个循环把不同场景跑完只保留关键波形能比纯手工操作省好几倍时间。3.4 UVM环境调试的实用套路UVM验证环境最头疼的问题之一就是sequence的运行轨迹和transaction的数据内容。光靠看波形你很难把一个个uvm_sequence_item对应到RTL总线上具体的行为。Verdi对UVM有专门的支持它能识别UVM的object hierarchy并能在仿真过程中dump出UVM的transaction记录。在UVM环境里你可以在base test或者某个关键组件里加上uvm_info(DEBUG, $sformatf(Transaction content\n%s, tr.sprint()), UVM_LOW)用uvm_info打印transaction的完整内容然后在Verdi源码窗口里看Log信息在波形窗口里对照对应的总线信号。这种方法虽然朴素但定位问题的效率极高一边是协议层的消息流一边是信号层的时序。另外Verdi也支持查看UVM的factory override关系。在仿真结束后Verdi会把UVM组件层次和对象关系显示出来你可以直接在GUI里看到某个uvm_agent被override成了哪个类型这在排查sequence乱入、组件被错误替换的问题时特别有用。如果你所在的项目UVM用得比较重一定要把这个功能用起来。4. 常见问题排查与避坑实录4.1 FSDB波形dump不出来的几种原因我见过太多人卡在仿真跑完了结果fsdb文件是0字节这类问题上。这里我把常见原因和对应排查思路整理成一个表格希望能帮大家少走弯路现象大概率原因排查方法编译报无法解析$fsdbDumpvarsPLI接口没有正确添加检查编译命令中的-P参数路径是否正确仿真能跑但fsdb文件为0字节$fsdbDumpfile没有被执行到确认是否把dump任务写在了永远不会被执行的initial块中或者模块名拼错fsdb文件生成了但Verdi打不开版本不匹配确认VCS和Verdi版本兼容跨大版本时fsdb格式可能有差异波形文件巨大Verdi加载卡死dump层次太深、信号太多用$fsdbDumpvars的层次参数限制范围或用$fsdbDumpoff跳过无关阶段打开Verdi后波形和代码对不上编译选项不一致确保Verdi加载的filelist和VCS编译时用的filelist完全一致特别是宏定义部分我在实际工作中发现“宏定义不一致”是最隐蔽的一个坑。有时候RTL文件里有ifdef SIMULATION相关的代码VCS编译时开了这个宏但Verdi加载的filelist里没把这个宏加上导致Verdi看到的代码状态和实际仿真相符不符找bug时容易被带偏。所以务必在filelist的开头或者Verdi的加载命令里同步所有宏定义。4.2 Verdi打开卡死或波形不刷新的处理如果你打开Verdi的时候界面卡死大多数情况是因为文件太大或者内存不够。这里优先建议在加载waveform前先关掉一些不必要的窗口尤其是不要一次性打开多个超大的FSDB。Verdi加载大文件时会把数据预读进内存你通过命令行启动时加个 -nolog 参数可以关闭日志记录减少I/O开销加载速度能明显变快。还有一种情况是仿真过程中波形不刷新。有时候你在VCS里跑了几个小时想实时打开Verdi看进度却看不到新数据。这是因为VCS默认的FSDB缓冲机制不会实时把数据写进文件。解决办法是在testbench里调用$fsdbDumpflush强制把缓冲区里的波形数据刷进文件。或者等仿真结束再打开fsdb就肯定能看到完整波形了。另外在Linux服务器上如果显示不了Verdi图形界面通常是DISPLAY环境变量没设好或者X11 forwarding没开。用ssh连服务器时加 -X 参数或者执行export DISPLAY:0.0 指向本地X server这些问题基本都能解决。4.3 VCS编译报错的典型场景与修复方向VCS编译报错的问题五花八门但有几个高频场景值得单独提一下。第一个是UVM相关报错比如找不到uvm_pkg。这种情况大多是因为编译命令里没写-ntb_opts uvm或者编译顺序不对。VCS编译UVM环境时需要先编译UVM库再编译testbench文件。如果你用的是makefile可以检查一下文件列表的顺序把uvm相关的文件放在最前面。第二个高频报错是文件路径问题。大型项目的filelist里经常用相对路径但VCS在不同的工作目录下编译时相对路径的基准目录可能不同。建议统一用绝对路径生成filelist或者写一个脚本动态生成filelist避免路径错位。第三个容易踩坑的是timescale冲突。不同文件如果用了不同的timescale声明VCS在编译时会报warning或error。我习惯在所有RTL和tb文件里统一加上timescale 1ns/1ps防止因为精度不一致导致的时序偏差。这个细节对抓时序相关的bug非常重要。4.4 层次信号命名与宏定义导致的分析问题在Verdi里手动添加深层信号时一个常见问题就是信号全路径名写错。Verdi的波形窗口里默认显示的是信号的短名字比如axi_awvalid但它内部的全路径可能是top_tb.dut.u_axi.axi_awvalid。如果你在TCL脚本里用短名字去add -varVerdi可能找不到信号因为脚本执行时是按全路径匹配的。解决办法是在Verdi的源代码窗口选中一个信号后看底部的信息栏会显示它的完整层次路径然后照着路径写TCL脚本就不会错了。也可以直接在波形窗口里右键信号选择Properties里面能看到全名。这个细节看起来土但真能让你的脚本调试效率明显提升。还有人经常遇到的问题是代码里用了宏定义来生成信号但Verdi打开后这些信号找不到。比如RTL里写define WIDTH 32然后调用某个模块时传入WIDTHVerdi的源代码分析虽然能识别宏但如果你没有在Verdi里设置宏定义某些条件下信号会被当作未解析的表达式导致无法查看。解决办法是在Verdi启动命令里加-macro参数或者在filelist前面用define声明宏。5. 覆盖率分析与调试的结合使用5.1 查看覆盖率的基本操作调试不止是找bug还要衡量验证是否充分覆盖率分析就是这两个目标之间的桥梁。VCS跑完回归后通常会有几个不同维度的覆盖率数据行覆盖率、条件覆盖率、状态机覆盖率、翻转覆盖率、以及功能覆盖率。Verdi能直接把VCS生成的覆盖率数据打开以可视化的形式展示出来。在VCS编译时加上-cm linecondfsmasserttgl仿真后就会生成simv.vdb目录里面存着覆盖率数据。然后在Verdi里用一行命令打开verdi -cov -f filelist.f -top top_tb打开后左侧的覆盖率浏览器会展示各个模块的覆盖率百分比。你可以按功能模块、按文件、按实例一层层下钻看到底是哪个模块的哪一行代码没有被执行到。这个功能比看一份干巴巴的覆盖率报告文件要好用得多因为你可以直接在源代码窗口看到哪些行是红色(未覆盖)、哪些是绿色(已覆盖)。5.2 用覆盖率辅助debug定位盲区有时候一个功能看起来正常但模块内部某个分支从来没执行过这可能就是潜在bug的温床。我曾经遇到过一个AXI slave模块在连续突发传输超过8拍时数据出错。一开始怎么都查不到原因后来打开覆盖率一看发现RTL里处理burst length大于8的分支语句覆盖率为0说明在以往所有的测试用例里这个分支就从来没被真正触发过。回头审视测试用例确实所有burst长度都被限制在8以内。这种基于覆盖率找验证盲区的方法比漫无目的地看波形高效得多。所以我的习惯是每次回归完除了检查有哪些testbench失败以外还会用Verdi过一遍覆盖率看看最近新增的feature对应代码有没有被覆盖到。如果覆盖率有明显下降即使所有用例都通过我也会怀疑是不是某些场景没跑到回去补测试用例。6. 值得养成的调试习惯与效率建议6.1 调试前先确认3个前提开动之前先把这三个问题搞清楚能省掉一大半冤枉时间第一这个bug是稳定复现还是偶尔出现。稳定复现的问题用波形单步调试最合适偶尔出现的问题可能需要加日志、加大仿真时间或者用seed随机化来增大概率。第二问题定位在哪个层次。如果是UVM环境本身的问题优先看transaction日志如果是RTL时序问题优先看FSDB波形。第三你的waveform是否记录了足够的信息。有的bug在仿真中期才出现但前面的波形没dump后面分析时想追溯根因就没有依据了。我见过太多人一上来就开Verdi狂加信号结果信号是加了但看半天不知道自己在找什么。所以动手之前先在记事本里写下几个问题这个信号在正确行为下应该是多少哪个时刻开始出现异常异常出现的条件是什么带着问题去看波形效率完全不一样。6.2 常用调试命令和脚本归档调试技巧这东西用一次就忘不如沉淀下来。我习惯在项目里建一个debug_scripts目录把所有用过的TCL脚本、makefile编译选项和Verdi配置都放进去。每次遇到新问题先翻一翻这个目录看看上次类似场景是怎么配置的。举一个具体的例子我们的项目里有一个脚本叫view_wave.tcl用来加载常见总线信号到一个统一的波形窗口包括AXI、APB、SPI、UART这些接口。不管谁去调试只要source一下这个脚本所有常用的总线信号就都铺好了省得每个新同事都要重新摸索一遍。这种沉淀经验的做法对个人对团队的价值都很大。另外一定要学会看VCS的编译日志和仿真日志不要一看到ERROR就慌。日志里通常有很多线索比如warning信息往往预示着潜在问题UVM的report summary也会提示匹配了多少个transaction这些信息在debug时经常比波形更直接。6.3 我踩过的坑和至今保留的习惯最后分享几个我在实战中踩过的坑以及后来养成的对应习惯。第一个坑是fsdb文件乱放。之前有段时间我所有仿真都在同一个目录下跑结果是fsdb文件互相覆盖想回头翻旧数据都找不到。现在我在脚本里强制每次仿真前建立一个带时间戳的目录把fsdb、log、vdb统一放在里面干净又不容易出错。第二个坑是不看warning。VCS编译的时候经常会冒出一堆warning尤其是位宽不匹配、隐式net声明这些当时觉得不影响功能结果最后出问题往往就是这些地方。现在我要求自己至少把编译日志里的error和主要的warning都过一遍位宽不匹配的warning尤其要仔细看很多时候它就是bug的源头。第三个坑是拿Verdi当纯波形工具用忽略了它自带的协议分析功能。其实Verdi支持AXI、APB、DDR等常见总线的协议波形自动解析它能把握手事件、读写事务整理成高层视图直接显示每一次传输的地址和数据。调试总线问题时这个功能比手动去数波形有效得多。如果你还没用过这个功能下次debug总线相关问题的时候一定要试试。