1. 数字芯片验证环境搭建的核心思路
数字IC验证这行干久了,你会发现一个很有意思的现象:很多人写testbench写得飞快,激励生成、覆盖率收集、断言检查一套一套的,但一到波形调试环节就卡壳。代码跑完了,仿真也过了,可一旦出现mismatch或者X态传播,打开波形工具的那一刻整个人是懵的——信号几百上千条,时间轴拉到几万个cycle,到底从哪儿看起?
这就是我今天想聊的核心话题:VCS+Verdi联合仿真。这套组合在数字IC验证领域基本是标配,VCS负责编译仿真,Verdi负责波形查看和调试。但很多人只是“会用”,远谈不上“用好”。我见过工作两三年的验证工程师,Verdi里还只会拖信号看波形,HiERMAN、nTrace、信号追踪这些功能完全没碰过,调试效率低得令人发指。
这篇文章面向的是有一定Verilog/SystemVerilog基础、正在做或准备做数字IC验证的工程师,不管你是刚入行的新人还是想提升调试效率的老手,我都会从环境搭建、编译选项、波形生成、Verdi调试技巧到常见问题排查,把整套流程拆开揉碎讲清楚。核心关键词就几个:VCS编译仿真、Verdi波形调试、FSDB文件生成、联合仿真环境配置、常见问题排查。
先说清楚一个基本认知:VCS和Verdi是Synopsys验证工具链里的两个核心组件。VCS是编译型仿真器,把RTL代码和testbench编译成可执行文件,跑仿真出结果;Verdi是调试分析工具,读取仿真产生的波形文件和源代码,帮你定位问题。两者通过FSDB(Fast Signal Database)文件衔接——这是Verdi的专有波形格式,比VCD文件小得多,信号加载速度也快得多。
为什么不用VCD?VCD是通用格式,任何仿真器都支持,但文件体积大、加载慢,一个中等规模的SoC仿真跑下来VCD能到几十GB,Verdi打开直接卡死。FSDB压缩率高,同样规模的仿真可能只有几百MB到几GB,而且Verdi对FSDB的支持是原生的,加载和跳转都流畅。所以实际项目中,只要用Verdi调试,FSDB就是默认选择。
注意:FSDB是Verdi专属格式,其他波形工具(如GTKWave、DVE)不直接支持。如果团队里有人用DVE看波形,需要额外生成VCD或者用VCS的DVE波形格式。
整套流程的核心逻辑是这样的:写好RTL和testbench之后,用VCS编译时链接Verdi的PLI接口,仿真运行时通过系统函数$fsdbDumpfile和$fsdbDumpvars把信号变化记录到FSDB文件,仿真结束后用Verdi打开FSDB文件和对应的源代码进行调试。听起来简单,但每一步都有坑,下面我逐个拆解。
2. VCS与Verdi环境配置及编译选项详解
2.1 工具安装与License配置要点
VCS和Verdi的安装本身不算复杂,但License配置是新手最容易翻车的地方。Synopsys的工具链共用一套License管理机制,通常用lmgrd守护进程来管理。安装完成后,你需要确认几个关键环境变量:
# 典型的环境变量配置(根据实际安装路径调整) export VCS_HOME=/opt/synopsys/vcs/O-2018.09-SP2 export VERDI_HOME=/opt/synopsys/verdi/O-2018.09-SP2 export PATH=$VCS_HOME/bin:$VERDI_HOME/bin:$PATH export LM_LICENSE_FILE=27000@license_server # 或者用SNPSLMD_LICENSE_FILE export SNPSLMD_LICENSE_FILE=27000@license_server这里有个细节:LM_LICENSE_FILE和SNPSLMD_LICENSE_FILE的区别。前者是通用License变量,后者是Synopsys专属的。如果机器上同时装了其他EDA工具(比如Cadence的),建议用SNPSLMD_LICENSE_FILE避免冲突。端口号27000是默认的,实际项目中IT部门可能会改,拿到环境先确认。
验证License是否正常,直接跑:
vcs -ID verdi -version如果VCS报License错误,先检查SNPSLMD_LICENSE_FILE指向是否正确,再用lmstat -a -c 27000@license_server看License池里VCS和Verdi的feature是否还有余量。大公司里License经常被抢光,尤其是项目冲刺阶段,这个排查思路能帮你快速定位是环境问题还是License资源问题。
2.2 VCS编译选项的取舍逻辑
VCS的编译选项非常多,但做联合仿真时,核心就那几个。我先把最常用的编译命令列出来,然后逐个解释为什么这么选:
vcs -full64 -sverilog -debug_access+all \ -kdb -lca \ -timescale=1ns/1ps \ -f filelist.f \ -top tb_top \ -o simv \ -l compile.log-full64:编译64位版本。现在服务器基本都是64位系统,不加这个选项编译出来的32位可执行文件在内存超过4GB时会出问题。SoC级别的仿真动辄需要几十GB内存,必须64位。
-sverilog:支持SystemVerilog。现在testbench基本都用SV写,这个选项必加。如果代码里用了UVM,还需要加-ntb_opts uvm-1.2之类的选项指定UVM版本。
-debug_access+all:这是联合仿真的关键选项。它开放了所有信号的调试访问权限,Verdi才能读取到完整的信号层次和数值。不加这个选项,Verdi打开FSDB后可能看不到某些内部信号。有人为了加快编译速度只加-debug_access+pp(只开放端口),结果调试时发现关键内部信号看不到,又得重新编译,得不偿失。
-kdb:生成Knowledge Database,这是Verdi用来做源代码和波形关联的数据库。没有kdb,Verdi打开FSDB后无法自动关联到RTL源代码,你只能看波形不能追代码。-lca是启用一些高级特性的选项,某些VCS版本需要它来配合kdb使用。
-timescale=1ns/1ps:指定时间单位和精度。这个必须和RTL代码里的`timescale一致,否则波形的时间轴会错乱。我遇到过有人RTL里写1ns/100ps,编译选项写1ns/1ps,结果波形里信号跳变的时间点全部偏移,查了半天以为是逻辑问题。
-f filelist.f:用文件列表的方式指定源文件。项目大了之后,源文件几十上百个,直接写在命令行里不现实。filelist里可以用+incdir+指定include路径,用-v指定库文件。
-top tb_top:指定顶层模块。VCS默认会自动找顶层,但项目里如果有多个候选顶层,显式指定避免歧义。
-o simv:指定输出的可执行文件名。默认是simv,也可以改成项目相关的名字,比如simv_uart_test。
-l compile.log:把编译日志输出到文件。编译报错时,日志文件比终端回滚要方便得多。
2.3 Verdi启动配置与novas.rc实用设置
Verdi启动时会读取novas.rc配置文件,这个文件可以放在当前目录、home目录或者通过-rcFile选项指定。合理配置能省很多事:
# novas.rc 常用配置 # 设置信号默认进制 database -open fsdb -shm # 设置波形窗口默认显示的信号进制 waveform -format hex # 设置源代码字体大小 source -font {Courier 12} # 自动加载信号分组 # 设置时间轴单位实际项目中,我习惯在项目根目录放一个novas.rc,把常用的信号分组、颜色配置、进制设置都写进去。这样每次打开Verdi都是熟悉的环境,不用重复配置。信号分组尤其有用——把APB总线的信号放一组、AXI信号放一组、中断信号放一组,调试时直接展开对应分组,比在几百条信号里翻找快得多。
Verdi的信号分组文件是.rc格式,可以在GUI里配置好后导出,也可以手写。手写的话大概长这样:
# signal_group.rc addGroup -name "APB_BUS" -color yellow addSignal -group "APB_BUS" tb_top.pclk addSignal -group "APB_BUS" tb_top.presetn addSignal -group "APB_BUS" tb_top.paddr addSignal -group "APB_BUS" tb_top.pwrite这个文件可以在Verdi里通过File -> Load Signal Group加载,或者启动时用-ssf选项自动加载。
3. FSDB波形生成与联合仿真全流程实操
3.1 testbench中FSDB dump的代码实现
FSDB文件的生成靠的是Verdi提供的PLI接口,在testbench里调用系统函数。最基础的写法:
initial begin $fsdbDumpfile("wave.fsdb"); $fsdbDumpvars(0, tb_top); end$fsdbDumpfile指定FSDB文件名,$fsdbDumpvars指定dump的范围和层级。第一个参数0表示dump所有层级,第二个参数tb_top是起始模块。如果只想dump某个子模块,可以写$fsdbDumpvars(0, tb_top.u_dut)。
但实际项目中,这样写有几个问题。第一,仿真时间长了FSDB文件会非常大,一个跑几毫秒的仿真可能产生几十GB的FSDB。第二,有些信号你根本不关心,dump了纯属浪费空间和加载时间。第三,调试时可能需要动态控制dump的时机。
所以更实用的写法是加上条件控制和分层dump:
initial begin // 只在特定条件下dump if ($test$plusargs("DUMP_FSDB")) begin $fsdbDumpfile("wave.fsdb"); // 只dump DUT内部信号,不dump testbench $fsdbDumpvars(0, tb_top.u_dut); // 单独dump某些关键信号 $fsdbDumpvars(1, tb_top.u_dut.u_core.reg_file); // 设置FSDB文件大小限制,超过后自动分割 $fsdbDumpvars(0, tb_top, "+fsdbopt+size_limit+2048"); end end用$test$plusargs的好处是,仿真时通过+DUMP_FSDB来决定是否dump。跑回归测试时不需要波形,不加这个plusarg,仿真速度能快不少。需要调试时再加上,灵活控制。
+fsdbopt+size_limit+2048这个选项设置单个FSDB文件最大2048MB,超过后自动分割成多个文件。对于长时间仿真很有用,避免单个文件过大导致Verdi打开缓慢。
还有一个实用技巧:在仿真过程中动态开启和关闭dump。比如只关心第1000到2000个cycle的波形:
initial begin $fsdbDumpfile("wave.fsdb"); $fsdbDumpvars(0, tb_top); $fsdbDumpoff; // 先关闭dump #1000ns; $fsdbDumpon; // 第1000ns开始dump #1000ns; $fsdbDumpoff; // 第2000ns停止dump end$fsdbDumpoff和$fsdbDumpon配合使用,可以精确控制dump窗口。这在调试特定时间段的问题时非常有用,FSDB文件能小一个数量级。
3.2 编译仿真一体化脚本编写
实际项目中,编译和仿真通常写成一个脚本,方便重复执行。我常用的Makefile风格脚本:
# Makefile for VCS+Verdi co-simulation VCS = vcs VERDI = verdi SIMV = simv FSDB = wave.fsdb LOG = sim.log # 编译选项 VCS_OPTS = -full64 -sverilog -debug_access+all -kdb -lca VCS_OPTS += -timescale=1ns/1ps VCS_OPTS += -f filelist.f VCS_OPTS += -top tb_top VCS_OPTS += -o $(SIMV) VCS_OPTS += -l compile.log # 仿真选项 SIM_OPTS = +DUMP_FSDB SIM_OPTS += +fsdbopt+size_limit+2048 # 默认目标:编译+仿真 all: compile run compile: $(VCS) $(VCS_OPTS) run: ./$(SIMV) $(SIM_OPTS) -l $(LOG) # 只编译不仿真 comp: $(VCS) $(VCS_OPTS) # 打开Verdi看波形 verdi: $(VERDI) -ssf $(FSDB) -nologo & # 清理 clean: rm -rf $(SIMV) $(SIMV).daidir csrc *.log $(FSDB) novas.* verdiLog .PHONY: all compile run comp verdi clean这个Makefile把编译、仿真、看波形、清理都串起来了。make comp只编译,make run只仿真,make verdi打开波形,make clean清理所有生成文件。项目里新人拿到这个脚本,改一下filelist路径就能跑,降低上手成本。
提示:
-debug_access+all会显著增加编译时间和可执行文件大小。如果项目很大,编译一次要半小时以上,可以考虑只在需要调试的模块上加-debug_access+all,其他模块用-debug_access+pp。VCS支持按模块指定debug级别,具体用法查VCS手册的-debug_access章节。
3.3 仿真运行与FSDB文件生成验证
编译通过后,运行仿真:
./simv +DUMP_FSDB +fsdbopt+size_limit+2048 -l sim.log仿真结束后,检查FSDB文件是否正常生成:
ls -lh wave.fsdb如果文件大小为0或者根本没生成,排查方向有几个:testbench里$fsdbDumpfile和$fsdbDumpvars是否被调用;编译时是否加了-debug_access+all和-kdb;Verdi的PLI库是否被正确链接。VCS编译日志里搜fsdb关键字,如果有Undefined system task $fsdbDumpfile之类的警告,说明PLI没链接上。
PLI链接的问题通常是因为VCS找不到Verdi的库路径。检查VCS_HOME和VERDI_HOME环境变量,确认$VERDI_HOME/share/PLI/VCS/LINUX64目录下有novas.so和novas.tab文件。VCS编译时会自动去这个路径找,如果路径不对就链接失败。
还有一种情况:编译没报错,仿真也没报错,但FSDB文件就是空的。这通常是因为$fsdbDumpvars的层级参数写错了。比如写$fsdbDumpvars(0, tb_top.u_dut),但实际层次是tb_top.dut,那Verdi找不到对应模块,自然dump不出信号。用$fsdbDumpvars(0, tb_top)从顶层dump,或者先用$fsdbDumpvars(0)dump所有,确认层次后再缩小范围。
4. Verdi波形调试核心功能与高效技巧
4.1 nTrace与源代码关联调试
Verdi最强大的功能之一是nTrace——源代码追踪。打开FSDB后,在波形窗口选中一个信号,按Ctrl+W或者点击nTrace按钮,Verdi会自动跳转到该信号在RTL中的定义位置。这个功能的前提是编译时加了-kdb,并且Verdi能正确找到源代码路径。
实际操作中,源代码路径经常对不上。比如编译在/project/sim目录,RTL代码在/project/rtl目录,Verdi默认只在当前目录找,找不到就报“Source code not found”。解决办法是在Verdi启动时用-sv选项指定源代码搜索路径:
verdi -ssf wave.fsdb -sv /project/rtl -sv /project/tb -nologo &或者在Verdi的GUI里通过Tools -> Preferences -> Source添加路径。更彻底的办法是在编译时用-kdb生成kdb文件后,kdb里会记录源文件的绝对路径,只要源文件没被移动,Verdi就能自动找到。
nTrace的另一个实用功能是“驱动追踪”(Driver Trace)。选中一个信号,右键选择Trace Driver,Verdi会自动追踪这个信号的驱动源,一层层往上找,直到找到最原始的驱动逻辑。这在调试X态传播时特别有用——X态从哪儿来的,一路追上去,比手动翻代码快得多。
对应的还有Trace Load,追踪信号的负载,看这个信号驱动了哪些下游逻辑。调试fanout过大或者信号被意外修改时很有用。
4.2 HiERMAN层次化调试与信号分组管理
HiERMAN是Verdi的层次化调试工具,全称Hierarchical Manager。它把设计的层次结构以树形展示,你可以像文件管理器一样展开/折叠各个模块,选择性地把信号拖到波形窗口。
我见过很多人用Verdi就是打开FSDB,然后在nWave里手动输入信号名搜索。信号少还行,信号多了效率极低。HiERMAN的正确用法是:先在层次树里定位到关心的模块,然后把这个模块的所有信号一次性拖到波形窗口,再配合信号分组功能,把相关的信号归到一组。
信号分组的具体操作:在nWave窗口里选中一组信号,右键Group -> Create Group,给分组起个名字(比如AXI_READ、APB_CONFIG),设置颜色。分组可以嵌套,大组里可以套小组。调试时直接展开/折叠分组,比在几百条信号里滚动快得多。
分组配置可以保存成.rc文件,下次打开Verdi直接加载。团队里可以共享这个文件,统一调试环境。我们项目组就是把信号分组文件放在项目根目录,新人进来直接用,省得每个人自己配一遍。
HiERMAN还有一个“Bookmark”功能,可以给常用的信号组合打书签。比如调试DMA时,把DMA相关的信号打一个书签,下次直接点书签加载,不用重新找信号。
4.3 波形对比与信号搜索高级用法
Verdi的波形对比功能在调试回归失败时特别有用。比如同一个testcase,上一个版本通过了,新版本失败了,你可以把两个版本的FSDB同时加载到Verdi,用Waveform -> Compare功能对比两个波形的差异。Verdi会自动标出信号值不同的时间点,直接定位到问题发生的位置。
信号搜索方面,除了基本的按名称搜索,Verdi还支持按值搜索。比如你想找某个信号值为32'hdeadbeef的时间点,可以用Signal -> Search by Value,输入值,Verdi会列出所有匹配的时间点。调试特定数据包处理时很有用。
还有一个容易被忽略的功能:Event Search。可以设置条件,比如“信号A上升沿且信号B为高”,Verdi会搜索所有满足条件的时间点。这在调试握手协议、状态机跳转时非常高效。
nWave窗口的快捷键也值得花时间记一下:
| 快捷键 | 功能 | 使用场景 |
|---|---|---|
Ctrl+W | 打开nTrace | 从波形跳转到源代码 |
Ctrl+G | 信号分组 | 整理波形窗口 |
Ctrl+F | 搜索信号 | 快速定位信号 |
Ctrl+Left/Right | 时间轴缩放 | 查看细节或全局 |
Ctrl+Shift+Left/Right | 时间轴平移 | 快速浏览 |
F3 | 添加信号到波形 | 从nTrace添加 |
F5 | 重新加载FSDB | 仿真更新后刷新 |
这些快捷键用熟了,调试效率至少提升一倍。我刚开始用Verdi时也是鼠标点来点去,后来强迫自己记快捷键,一周之后就形成肌肉记忆了。
5. 联合仿真常见问题排查与避坑指南
5.1 编译阶段典型报错与解决
问题一:Undefined system task $fsdbDumpfile
这是最常见的问题,原因是VCS编译时没有链接Verdi的PLI库。排查步骤:
- 确认
VERDI_HOME环境变量设置正确 - 确认
$VERDI_HOME/share/PLI/VCS/LINUX64目录存在且包含novas.so和novas.tab - 编译命令中加上
-P $VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab $VERDI_HOME/share/PLI/VCS/LINUX64/novas.so
有些VCS版本会自动链接,有些需要显式指定。如果自动链接失败,就手动加上-P选项。
问题二:-kdb选项报错“unknown option”
-kdb是较新版本VCS才支持的选项。如果VCS版本较老(比如2016以前的),需要用-lca加上-debug_access+all来替代。或者升级VCS版本。查VCS版本用vcs -ID。
问题三:编译时间过长
-debug_access+all会显著增加编译时间。如果项目很大,可以考虑分层编译:顶层用-debug_access+all,底层模块用-debug_access+pp。VCS支持在filelist里对单个文件指定debug级别:
# filelist.f -debug_access+all ./tb/tb_top.sv -debug_access+pp ./rtl/core/*.v -debug_access+all ./rtl/peripherals/uart.v这样只有需要调试的模块开放完整访问权限,其他模块用端口级访问,编译速度能快不少。
5.2 仿真阶段FSDB生成异常排查
问题:FSDB文件生成但Verdi打开后没有信号
可能原因有几个:$fsdbDumpvars的层级参数写错;$fsdbDumpfile和$fsdbDumpvars的调用顺序反了(必须先Dumpfile再Dumpvars);仿真时间太短,信号还没来得及变化就结束了。
排查方法:在testbench里加$display打印$fsdbDumpvars的返回值,确认调用成功。或者在Verdi里用File -> Load Simulation手动加载FSDB,看是否有信号层次。
问题:FSDB文件过大导致Verdi卡顿
前面提到的+fsdbopt+size_limit可以限制单文件大小。另外,只dump关心的信号,不要$fsdbDumpvars(0, tb_top)一把梭。用$fsdbDumpvars(0, tb_top.u_dut)限定在DUT内部,testbench的信号通常不需要dump。
还有一个技巧:用$fsdbDumpvars的+fsdbopt+pack选项,对多位信号进行压缩存储。比如一个32位计数器,大部分时间值不变,pack选项能显著减小文件体积。
问题:仿真速度明显变慢
dump FSDB本身会拖慢仿真速度,这是正常的。但如果慢得离谱(比如慢了10倍以上),检查是否dump了过多信号。另外,+fsdbopt+size_limit设置过小会导致频繁分割文件,也会影响速度。一般设置在1GB到4GB之间比较合适。
5.3 Verdi使用中的高频问题速查
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| Verdi打开FSDB报“no such file” | FSDB路径不对或文件未生成 | 检查仿真日志,确认FSDB生成路径 |
| 波形窗口信号显示为红色X | 信号存在X态 | 用Trace Driver追踪X态来源 |
| nTrace跳转不到源代码 | 源文件路径变更或kdb未生成 | 重新编译加-kdb,或用-sv指定路径 |
| 信号值显示为问号 | 信号未被dump | 检查$fsdbDumpvars范围是否包含该信号 |
| Verdi启动报License错误 | License被占用或配置错误 | 用lmstat检查License状态 |
| 波形时间轴不对 | timescale不一致 | 统一RTL和编译选项的timescale |
| 信号分组丢失 | 未保存rc文件 | 配置好后导出信号分组文件 |
| Verdi响应缓慢 | FSDB文件过大或信号过多 | 缩小dump范围,用size_limit分割 |
5.4 后仿memory初始化与X态排查
后仿(门级仿真)中memory初始化是个高频问题。RTL仿真时memory默认初始化为0或X,但后仿中memory的行为取决于工艺库的模型。如果memory没有正确初始化,读出的数据可能是X,导致仿真失败。
排查思路:首先确认memory的初始化方式。如果是initial块初始化,后仿中initial块是否被执行取决于工艺库。有些工艺库的memory模型不支持initial初始化,需要用$readmemh从文件加载。其次,检查memory的使能信号和读写时序,后仿中时序要求更严格,setup/hold不满足会导致读出X。
Verdi中排查X态传播,用Trace Driver一路追到源头。如果追到memory输出就是X,那问题在memory初始化或读写控制;如果memory输出正常但下游是X,那问题在下游逻辑。
注意:后仿中X态传播比RTL仿真复杂得多,因为门级网表中存在大量的未初始化寄存器和组合逻辑竞争。建议在后仿前先跑一遍RTL仿真确认功能正确,后仿主要关注时序问题和X态传播。
5.5 提升联合仿真效率的实战经验
最后分享几个我实际项目中总结的效率技巧:
第一,编译一次,多次仿真。VCS编译出来的simv是可执行文件,只要RTL没改,可以反复运行不同的testcase。把编译和仿真分开,用make comp编译,make run仿真,改testbench不需要重新编译RTL。
第二,用plusarg控制dump。跑回归时不需要波形,不加+DUMP_FSDB,仿真速度能快30%以上。只在调试特定case时加这个plusarg。
第三,FSDB文件按testcase命名。不要所有case都dump到wave.fsdb,用$fsdbDumpfile($sformatf("wave_%s.fsdb", test_name))按testcase名生成不同文件,避免覆盖。
第四,Verdi的-nologo选项。启动时跳过logo画面,能省几秒钟。虽然不多,但每天启动几十次,累积起来也可观。
第五,信号分组文件纳入版本管理。团队共享信号分组配置,新人入职直接加载,不用自己摸索。我们项目组的信号分组文件有几百行,覆盖了所有主要接口和内部模块,调试时直接展开对应分组,效率极高。
第六,定期清理旧的FSDB文件。FSDB文件很占磁盘,一个项目跑下来可能几百GB。写个脚本定期清理超过一周的FSDB,或者只保留失败case的波形。
这套VCS+Verdi的联合仿真流程,从环境配置到编译选项,从FSDB生成到Verdi调试,再到问题排查,基本上覆盖了日常工作中90%以上的场景。工具是死的,人是活的,同样的Verdi,有人只能看波形,有人能十分钟定位问题,差距就在对这些功能的理解和熟练度上。多练、多试、多踩坑,调试效率自然就上来了。