
干数字IC验证这行的VCS和Verdi这对组合基本是绕不开的。VCS负责把RTL、testbench和整个UVM验证环境编译成可执行的仿真程序跑完仿真之后吐出fsdb格式的波形Verdi负责打开波形再配合源代码一层层往下追信号、看驱动关系、查时序违例。一个管怎么跑一个管怎么看两者搭配起来就是一套从编译到调试的完整闭环。这篇文章不想重复官方手册里的参数表而是从一次实际的联仿流程出发把环境准备、编译选项、波形生成、后仿memory初始化这些环节逐个拆开讲适合刚转数字IC验证方向、或者习惯用ModelSim/Questasim的人切换到VCS和Verdi时参考。1. 联合仿真整体思路与环境准备1.1 为什么默认选VCSVerdi而不是别的组合很多刚接触验证的同学会问市面上有开源仿真器、有Cadence的Xcelium、还有Mentor的Questa为什么工业界最常看到的组合还是Synopsys的VCS加Verdi。答案很简单不是其他工具不行而是VCS在大型Soc项目里的编译速度、随机约束求解性能和UVM兼容性做得很稳Verdi的FSDB波形加载速度和层次化调试体验也确实比传统波形工具更顺手。尤其在跑上亿门级芯片的回归用例时编译一次可能要几十分钟甚至更久仿真器本身的性能差距会被放大得很明显。Verdi并不是单纯的“波形查看器”它的核心能力在于把仿真波形、RTL源码、状态机视图和原理图关联起来。开发过程中最常见的场景是波形上看到一个信号在某个时刻跳变得不符合预期你点一下这个信号Verdi自动跳到RTL代码里对应的赋值语句再点一下驱动的上一层信号所有相关信号就高亮显示出来。这种从“现象”到“原因”的反推效率比对着波形再回头人肉翻代码高出一大截。还有一个现实问题是团队兼容性。流片公司里的验证环境、脚本、Makefile基本都是按VCSVerdi维护的新人进去之后要读的第一份文档往往就是“怎么编译、怎么看波形”。早点把这两个工具的组合用熟后面进项目会少踩很多坑。1.2 环境准备License、版本匹配和目录规划搭建一套能用的联仿环境前提条件是license可用而且VCS和Verdi的版本要匹配。这里说的“匹配”不是完全一致而是大版本不能差太远。比如VCS 2018版本和Verdi 2016配合能用但如果VCS是2014的老版本Verdi是新版本FSDB dump接口的库文件版本不一致很容易出现波形文件打不开或者某些信号类型识别错误的问题。最稳妥的做法是装同一个大版本号比如都是2018.09-SP2。Linux环境变量方面核心是PATH、VCS_HOME和VERDI_HOME。我常用的一段shell配置大概是export VCS_HOME/tools/synopsys/vcs/O-2018.09-SP2 export VERDI_HOME/tools/synopsys/verdi/2018.09-SP2 export PATH$VCS_HOME/bin:$VERDI_HOME/bin:$PATH export LM_LICENSE_FILE27020license-server export SNPSLMD_LICENSE_FILE27020license-server目录规划上建议从一开始就分清楚不要所有文件堆在同一个目录。我个人的习惯是rtl/存放设计源文件tb/testbench和UVM验证环境filelist/文件列表和宏定义sim/编译和仿真产生的中间文件wave/dump出来的fsdb波形log/编译日志和仿真日志这么做最大的好处是清理中间产物时只需要删掉sim/目录不会误删源文件。VCS编译时产生的中间文件非常多如果不隔离一两次迭代之后目录就会变得一团乱。注意VCS默认使用当前工作目录存放编译中间文件和simv可执行文件。如果多人共用同一份代码强烈建议每人用自己的sim目录否则不同编译选项之间的中间文件会互相覆盖。2. VCS编译全流程与关键参数选型2.1 直接单步编译与三步编译的区别VCS的编译方式大体分两类一种是直接把源文件和选项一次扔给VCS它内部自动完成分析elaboration并生成可执行文件这是最常见的单步用法另一种是先vlogan分析各个文件再vcs -top做elaboration更接近早期流程风格适合超大项目的分步排查。单步编译的核心命令长这样vcs -sverilog -debug_accessall \ -f filelist.f \ -timescale1ns/1ps \ -l compile.log \ -o simv这条命令做的事情是以SystemVerilog语法解析filelist里列出的所有文件按指定的时间精度生成仿真可执行文件simv同时开启全部调试访问能力——这里对应Verdi读波形的底层数据通道。-debug_accessall很关键如果项目对调试权限有顾虑可以按需拆成-debug_accesspp或者rn但自己本地验证没必要省。这里有个新手容易忽略的点-timescale参数。如果filelist里的每个文件都已经用timescale 1ns/1ps声明过那么命令行里的-timescale只作为兜底如果有些文件漏写了就按命令行里的值来算。但反过来如果不同文件的timescale不一致比如RTL是1ns/1ps、testbench是1ns/100ps仿真器在跨模块传信号时会出现精度对齐问题严重时会让你查到怀疑人生。务必统一。三步编译的流程是先把所有verilog文件分析成.vkp命令记录文件再做elaborationvlogan -sverilog -f filelist.f -l vlogan.log vcs -top tb_top -debug_accessall -l vcs_elab.log -o simv实际工作中单步更常见但遇到编译报错却定位不到是哪个文件的问题时拆成vlogan逐步分析能帮你更快缩小范围。另外vcs -top最好显式指定因为它决定设计顶层和testbench层次的挂载关系。2.2 filelist文件写法与宏定义管理filelist文件是整个编译流程的粘合剂写得好不好直接影响可维护性。我在项目里一般会建两个filelistrtl.f只放RTL文件tb.f放testbench和UVM基础类库做法是先建一个总入口文件用相对路径引用// top.f // 宏定义区 incdir../rtl incdir../tb // RTL文件 ../rtl/top_module.sv ../rtl/sub_module1.sv ../rtl/sub_module2.sv // UVM基类按依赖顺序 ../tb/uvm_pkg.sv ../tb/test_lib_pkg.sv ../tb/tb_top.sv加incdir的意思是告诉VCS去哪些目录找include的文件。相对路径比绝对路径好移植别人拿到工程后不用改任何配置。宏定义管理有个实用技巧VCS命令行上加define宏名值可以灵活控制编译内容。常用的做法是定义一个DUMP_FSDB宏只在需要dump波形时编译进去vcs -sverilog -debug_accessall \ defineDUMP_FSDB \ -f filelist.f -o simv然后在testbench里用条件编译包裹dump语句module tb_top; initial begin ifdef DUMP_FSDB $fsdbDumpfile(tb_top.fsdb); $fsdbDumpvars(0, tb_top, all); endif end endmodule这样在没有指定宏的情况下编译出来的仿真程序不会产生fsdb文件回归测试时速度更快磁盘占用也更小。2.3 仿真运行、seed管理和波形flush编译完成之后生成simv直接执行./simv -l sim.log ntb_random_seed12345 fsdbautoflushfsdbautoflush是个容易漏掉但很实用的选项。它让仿真在跑完一个指定阶段后自动调用$fsdbDumpflush把缓冲区里的波形数据强制写到磁盘上。如果不加仿真非正常退出时波形文件往往停留在缓存状态打开后信号不完整看起来就像“波形少了后半段”实际不是仿真没跑是数据没落盘。对于带UVM环境的仿真seed管理很重要。ntb_random_seed可以传固定值用于用例复现也可以传随机值做回归冒烟。还有一点很多公司会通过Makefile把seed统一写在命令行里而不是在环境里硬编码这样重建仿真环境时不会被旧seed坑到。跑完仿真后检查一下日志里的$finish是否正常出发再确认fsdb文件的大小和时间戳如果文件只有几十KB大概率dump没生效。这一步检查比直接打开Verdi来得快。3. Verdi波形加载与调试实操3.1 两种打开Verdi的方式与初始化流程Verdi启动通常有两种方式。一是先启动工具再手动加载设计database和波形二是命令行一次性把源码、顶层和波形文件都带上。我建议用第二种一句话把环境打开verdi -f filelist.f -top tb_top -ssf tb_top.fsdb \ -nologo -l verdi.log 其中-ssf指定fsdb波形文件-top指定设计顶层。打开之后Verdi会自动读取波形文件里的信号层次并在nWave窗口里列出所有可观察的信号。这里要注意Verdi读的不只是波形还包括编译时的RTL database所以第一次打开会比较慢再打开同一个文件会快很多。如果只是临时看一眼波形也可以不指定-ssf等进到GUI里再用File - Open Session打开fsdb但这样Verdi对源码层次的关联会弱一些有时候信号显示成未解析的字符串不方便追代码逻辑。完整的启动方式永远是最省事儿的。3.2 从波形快速反推RTL问题的三个操作Verdi最有价值的功能不是“看波形”而是“反推信号”。我在实际调试中几乎每天都会用到三个操作这里依次说一下。第一个是Source窗口里点击信号后按x键。这个操作的含义是“显示驱动该信号的所有可能来源”也就是把这条信号的赋值语句全部高亮出来。对组合逻辑来说你会看到所有参与赋值的输入信号对寄存器来说你会看到非阻塞赋值的右端表达式涉及哪些信号。比如一段简单的计数器加1逻辑高亮之后马上能看见是cnt_plus1和clk在驱动它定位赋值关系非常直接。第二个是nWave窗口里的“Find Signals”。当波形有三四百条信号时靠肉眼在窗口里找target很不现实。按CtrlF可以直接输入信号名模糊匹配支持*通配符。另一个更好用的是选中一条信号后按CtrlW所有与此信号相关的其他信号会在树形列表里高亮显示这相当于告诉你它被谁驱动、又在驱动谁。第三个是Get Drivers和Get Loads的批量追踪。选中一根嫌疑信号右键选择Get DriversVerdi会往上游展开所有驱动路径连组合逻辑中间几级都标出来。如果一个位宽很大的总线信号异常的bit比较多直接看drivers链路往往比纯靠波形推测快得多。这三个操作配合起来调试一个典型UART接收功能大约就是波形里找到rx_dataGet Drivers看串口协议层的接收逻辑再按x键对照RTL赋值几轮下来就能把问题圈定在一两行代码里。3.3 后仿Memory初始化怎么处理后仿相对前仿最大的差异就是带上了门级延迟和时序检查而最容易坑人的是Memory初始化。很多设计中用到的SRAM模型在复位后是随机值或者X态如果不做memory初始化后仿跑到开头几个周期就会出一堆X态传播现象和设计bug冲突在一起很难分辨。VCS环境下常见做法是在testbench的initial块里用$readmemh把预先生成的初始化数据灌进memory模型initial begin $readmemh(mem_init.hex, dut.u_sram.mem); end这里有个细节要确认memory模型内部数组名是否暴露到可层次访问的路径上。有些SRAM IP会用genvar变量加generate for结构实例化底层bit cell数组名可能是u_sram.genblk1.mem这种带生成块前缀的名字直接用dut.u_sram.mem访问会报找不到层次。较稳妥的办法是先打开Verdi的nSchema找到memory实例对应的物理层次路径再回到testbench里按路径初始化。如果不想在testbench里写死路径VCS还提供-initmem选项可以结合vcdpluson或fsdb里的memory模型自动选择初始化文件但这种方式对memory模型本身的仿真接口有要求老一点的SRAM模型不一定支持。实际项目里我还是推荐用testbench $readmemh手动控制文件路径和加载时机最可控。同时记得检查初始化操作是否在复位释放之前完成不然复位信号会把memory清零等于白初始化。4. 高频问题排查与经验技巧4.1 编译报错速查表联仿流程里八成时间都耗在编译报错上下面这张表是我自己平时遇到的高频错误和对应解决思路直接抄作业就行。报错信息常见原因解决方案Cannot find uvm_pkgUVM库未编译或未-ntb_opts uvm确认VCS带UVM环境命令行加-ntb_opts uvmtimescale not specified部分文件缺timescale声明统一在命令行加-timescale1ns/1psPort connection width mismatchRTL例化时位宽不匹配打开RTL代码检查例化端口优先用.*方式Unknown identifier宏名拼写或define没传对查宏定义位置确认编译命令行宏名称一致Failed to open filefilelist里的路径写错了以top.f所在目录为基准检查相对路径Task/function xxx not found调用了未定义或未包含的模块确认文件包含顺序依赖模块在前面对于UVM的工程-ntb_opts uvm几乎必须要有。新版VCS已经默认支持UVM但老版本需要显式指定否则uvm_pkg根本找不到。还有一种情况是UVM版本冲突比如你用了UVM 1.1d的源码包命令行又传了-ntb_opts uvmVCS自带的UVM库反而会优先加载导致一些类的定义重复。解决方法是明确自己的UVM source路径用-uvmhome指定vcs -sverilog -ntb_opts uvm -uvmhome CDNS-1.1d \ -f filelist.f -o simv实际项目中很多编译报错是“宏定义在A文件、引用在B文件、但B先被编译”导致的。文件和包的正确编译顺序应该是先编译不依赖其他包的底层再编译依赖相对复杂的。如果项目比较复杂建议用incdir统一管理include不要靠物理文件顺序碰运气。4.2 波形不生成、打不开或信号不完整的处理波形相关的问题排在编译报错之后是第二大类耗时点。最典型的是跑仿真后fsdb文件生成了但用Verdi打开发现提示“FSDB version mismatch”或者没有任何信号。出现这种情况通常只有一个原因编译VCS和启动Verdi所用的libfsdb动态链接库版本不一致。要么是环境变量指到了两套不同版本的工具要么是dump库路径被用户自定义的LD_LIBRARY_PATH覆盖。排查思路是先看which vcs和which verdi是否指向同一个版本目录再查echo $LD_LIBRARY_PATH里有没有别的fsdb动态库。另一种常见情况是测试用例里写了$fsdbDumpvars但仿真日志里没有dump相关的提示。按照我自己的经验问题多半出在$fsdbDumpvars(0, tb_top, all)的参数范围上当传0时表示dump所有层次传1表示只dump第一层。如果设计层次很深又传了比较小的层级数字波形里自然只有顶层几个信号。这里有个很简单的判据打开fsdb后如果只有tb_top下一级信号而整个设计树都看不到那就把$fsdbDumpvars的第一个参数改成0重新编译仿真。还有一类情况是仿真定时器和dump时机冲突。比如用例跑了10ms才dump而设计实际在1ms就已经出错了那么波形文件里前面的区间是空的。对付这种我习惯在testbench里多放几个周期性的$fsdbDumpflush比如每1000个时钟周期强制flush一次确保崩溃前的波形尽可能多地被保存下来。4.3 用Makefile固化整个编译仿真流程命令行手敲始终容易出错尤其在有多个项目、多套测试用例的情况下。成熟的做法是把联仿流程写进一个Makefile把编译选项、仿真选项、波形目录和清理逻辑固化下来。我自己的Makefile骨架如下# Makefile for VCS Verdi co-simulation VCS vcs VERDI verdi VCS_OPTS -sverilog -debug_accessall -timescale1ns/1ps -ntb_opts uvm TOP tb_top FSDB_FILE $(WAVE_DIR)/$(TOP).fsdb WAVE_DIR wave RTL_FILES -f filelist/top.f all: comp sim comp: mkdir -p sim log wave cd sim $(VCS) $(VCS_OPTS) $(RTL_FILES) -o simv -l ../log/compile.log sim: comp cd sim ./simv fsdbautoflush ntb_random_seed12345 -l ../log/sim.log verdi: comp $(VERDI) -f filelist/top.f -top $(TOP) -ssf $(FSDB_FILE) -nologo clean: rm -rf sim log wave这个Makefile有几个值得注意的地方将所有编译工作放到sim/目录下执行这样编译产生的中间文件都被隔离mkdir先建好wave目录否则Verdi打开波形时要手动选路径-o simv让可执行文件名固定方便回归脚本调用。对于要跑回归的场景可以再加一个regress目标自动遍历测试用例名称并把seed通过变量传入regress: for tc in $(TESTCASES); do \ mkdir -p sim log wave; \ cd sim ./simv UVM_TESTNAME$$tc fsdbautoflush \ ntb_random_seed$(SEED) -l ../log/$$tc.log; \ done凡是仿真环境和回归命令还在靠纯手敲的建议尽早改成Makefile或者脚本管理这一套东西在换电脑、换服务器、甚至换公司的场景下都是通用资产。4.4 回归调试里常见的“误判”经验最后一个部分谈谈我踩过几次坑之后总结出的经验不算标准教程里会写的东西但很实用。第一点后仿波形里出现X态时不要条件反射认为是RTL bug先排除初始化问题。我在一个外部SoC项目中遇到过连片X态从AHB总线扩散到UART发送模块查了两天最后定位在SRAM未初始化而不是设计逻辑问题。看到X态先检查memory初始化、再检查复位时序这个顺序能把排查时间缩得很短。第二点不要完全信任波形上的毛刺。某些信号在波形上看起来有一个非常窄的glitch但VCS的时序检查不一定报出来。这种时候值不值得查取决于这个毛刺是否沿着驱动链影响到功能输出。一个很好用的技巧是在nWave里针对目标信号做Search for Transition并且将时间范围缩小到毛刺附近再配合源代码高亮判断是否属于组合竞争。第三点编译选项不要贪多。-debug_accessall是最省事但也最耗性能的用法大规模回归里如果每个用例都带全量debug access仿真速度可能慢20%到30%。我的习惯是冒烟和定位bug时用all回归大批量测试时改用-debug_accesspp并关闭fsdb dump只留pass/fail信息。性能、调试能力和磁盘占用之间要有一个明确取舍。把编译命令、fsdb生成参数和Verdi的加载路径全部封装到Makefile里之后这一整套流程基本就是自己的肌肉记忆了。后来我遇到新机器或者给同事搭环境第一步永远是检查工具版本第二步是验证一个最小测试用例能不能出波形第三步才会开始干活。这套“最小闭环”的思路也推荐给你先用一个最简单的模块把VCS和Verdi的链路跑通再逐步加载真实的RTL和验证环境遇到问题定位起来会清晰很多。