十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VCS+Verdi联合仿真环境搭建与Makefile避坑指南

VCS+Verdi联合仿真环境搭建与Makefile避坑指南 做了几年数字IC验证我最大的感受就是环境搭建这件事看着不起眼但坑起来是真的坑。你花三天搭好的VCSVerdi验证环境可能只为了能正常跑出一个fsdb波形结果卡在编译参数上就劝退了一堆新人。VCS是Synopsys家的仿真器Verdi是配套的调试工具两者联合仿真是目前数字IC前端验证用得最普遍的一套组合。这篇文章不做理论堆砌直接带你过一遍从RTL到testbench、从VCS编译到Verdi看波形的完整路径重点讲清楚Makefile里那些“抄了就报错、改了就乱套”的玄学问题让新人能快速上手也让老手可以回头审视自己的脚本还有没有优化空间。这套环境解决什么问题说白了就是让你写的Verilog/SystemVerilog代码能在服务器上真正跑起来把内部信号变化以波形形式dump出来再用Verdi打开做debug定位。不管是验证一个计数器还是跑一整个UVM平台底层逻辑都一样编译、仿真、看波形、找问题、改代码。你会发现只要环境跑通一次后面的工作全是重复劳动所以前期的“地基”必须打得稳。1. 这套验证环境到底在解决什么1.1 一次完整仿真要经历哪几步先理清整个流程。一次标准的VCSVerdi联合仿真大致分四步。第一步是编译。VCS会把你的RTL源码和testbench文件统一读进来做语法检查、代码分析、elaboration展开层次最后生成一个可执行的仿真文件默认叫simv。很多人以为vcs和gcc一样是“编出个可执行文件就完了”其实VCS这一步还包括SystemVerilog的DPI导入、UVM库的编译、以及和Verdi的PLI接口绑定。第二步是仿真运行。执行./simvtestbench里的时钟、复位、激励开始生效model跑起来在指定时间点$fsdbDumpvar会把信号变化记录到fsdb文件里。这一步结束后你得到的不是“pass/fail”而是一个波形文件和一份仿真日志。第三步是调试。用Verdi打开fsdb波形查看信号时序、状态机跳转、接口协议是否符合预期。如果需要追溯某个信号为什么在这个时刻变成某个值可以用Verdi的nTrace功能顺着信号驱动关系往回查。第四步是回归和迭代。改完代码后重新编译、仿真、看波形循环往复直到功能满足要求。这四步看着简单但每一步都有各自的坑编译参数写错、PLI库路径不对、fsdb文件没dump出来、Verdi打不开数据库每个坑都能卡你好几个小时。所以环境搭建的核心任务就是把上面四步用一套可复用的脚本固化下来把变化的部分和不变的部分分开管理。1.2 方案选型VCSVerdi为什么是标配可能有人会问仿真器有VCS、Questa、Xcelium调试工具有Verdi、DVE、GVIM为什么偏偏选VCSVerdi说实话VCS在当前数字IC验证里的地位有点类似于“行业默认语言”。从学校实验室到公司主流流程大量团队都用VCS做回归用Verdi做波形调试。你用别的工具做出来的环境到了别人机器上可能跑不起来但VCSVerdi这套组合适配性是最好的。尤其是带上UVM之后VCS对UVM库的支持非常成熟很多老项目直接就是围着VCS的编译选项来写的比如UVM_VERBOSITY、UVM_TESTNAME这类运行时选项。Verdi的优势则集中在“看波形快”和“查源码方便”。Verdi打开GB级fsdb波形的速度比传统工具快一截配合它读取源码并建立信号数据库的能力调试时可以做到“从波形点一个信号直接跳到RTL里对应变量”也能做驱动的反向追溯。VCS自带的DVE虽然也能看波形但在大规模验证平台下明显不如Verdi顺手。所以这套选型不是“哪家最强”而是“用了它你能直接进入主流验证流程”。后面所有代码和脚本我都会围绕VCSVerdi来讲。2. 前置条件与工程规划2.1 确认工具链可用动手之前先确认你机器上的VCS和Verdi是能用的。最简单的方式是执行vcs -ID verdi -ID如果你能看到类似“VCS XXXX.XX”和“VERDI XXXX.XX”的版本输出且没有明显的license告警说明工具基础环境正常。实际中我见过很多次“编译能过但verdi起不来”的情况十有八九是license相关的环境变量没source对比如LM_LICENSE_FILE或SNPSLMD_LICENSE_FILE指到了错误端口。如果你有多个版本的Synopsys工具还需要确认source的settings.sh是同一个目录下的避免VCS和Verdi各自关联不同的license。另外要注意UVM库和VCS版本的搭配。VCS编译UVM时如果版本不匹配会报一堆“cannot find”一类的类定义错误。强烈建议先用一个最简单的、不带UVM的例子把环境跑通再引入UVM这样能确定问题出在工具链还是UVM配置上。2.2 目录结构怎么摆环境搭建的第一步不是写代码而是规划目录。我的习惯是这样project/ ├── rtl/ │ └── counter.v ├── tb/ │ └── tb_counter.sv ├── sim/ ├── scripts/ │ ├── Makefile │ └── filelist.f └── log/每个目录的职责要清楚rtl/放被测设计DUV/DUT也就是你要验证的模块。多人协作时可以按IP或子模块再分一层。tb/放testbench顶层、接口驱动、测试用例。UVM项目里还会细分agents、tests、sequences等。sim/编译和仿真产生的所有中间文件都放这里。simv、csrc、simv.daidir、fsdb波形、仿真日志一股脑扔进去源码目录始终保持干净。scripts/集中放Makefile、filelist、辅助脚本。log/如果仿真日志很多可以独立一个log目录方便归档。这套目录结构的好处是清理方便——make clean直接清sim/迁移方便——整个工程打包带走路径相对固定多人协作方便——大家拿到代码后只要在Makefile里改一个PROJ_ROOT就能各自跑各自的仿真的。我见过不少新人喜欢把RTL、tb、生成的simv、fsdb全堆在同一个目录下理由是“省事儿”。但等工程一复杂ls -l出来的文件连你自己都分不清哪些是源码、哪些是中间产物这时候再想起来规整成本已经很高了。2.3 环境变量和PLI路径的关系VCSVerdi联合仿真里最绕不开的就是PLI库路径。VCS是仿真器Verdi是调试工具两者要通信VCS必须通过PLIProgramming Language Interface把Verdi的C库加载进来才能真正执行$fsdbDumpfile这样的Verdi系统任务。这个PLI库并不是装完工具就自动生效的它由两个文件组成novas.tabPLI任务/函数的注册表告诉VCS有哪些系统任务属于Verdi。pli.a对应的静态库实现那些任务的底层逻辑。通常这两个文件在$VERDI_HOME/share/PLI/VCS/LINUX64/下。但要注意不同版本的VERDI路径结构可能不一样。早期版本可能在share/PLI/VCS/LINUX64新版本可能挪到了$VERDI_HOME/apps/PLI/VCS/LINUX64之类的地方。所以不要死记路径先执行echo $VERDI_HOME find $VERDI_HOME -name novas.tab 2/dev/null确认实际路径后再把它填到脚本里。如果你发现find能找到但编译还是报“PLI has no tsk”之类的错误那就要检查是不是有多个VERDI版本找到的是旧版本的PLI。3. 核心实操从写代码到出波形3.1 一个最小可用的计数器与testbench空谈环境没意思我们用一个最简单的计数器来把整个流程跑通。RTL代码长这样// rtl/counter.v module counter #( parameter WIDTH 8 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] cnt ); always (posedge clk or negedge rst_n) begin if (!rst_n) cnt b0; else if (en) cnt cnt 1b1; end endmodule对应testbench我写成SystemVerilog尽量贴近实际验证的写法// tb/tb_counter.sv timescale 1ns/1ps module tb_counter; parameter WIDTH_L 8; logic clk; logic rst_n; logic en; logic [WIDTH_L-1:0] cnt; // DUT例化 counter #( .WIDTH(WIDTH_L) ) u_counter ( .clk (clk), .rst_n (rst_n), .en (en), .cnt (cnt) ); // 时钟 initial clk 0; always #5 clk ~clk; // 10ns周期 // 复位与激励 initial begin rst_n 0; en 0; repeat(3) (negedge clk); rst_n 1; en 1; repeat(20) (negedge clk); en 0; repeat(5) (negedge clk); $finish; end // 导出FSDB波形给Verdi initial begin $fsdbDumpfile(counter.fsdb); $fsdbDumpvars(0, tb_counter, all); end endmodule注意testbench里那个initial块它就是VCSVerdi联合仿真的关键通过$fsdbDumpfile指定波形文件名通过$fsdbDumpvars指定要记录哪些层次和信号。参数0代表从当前scope开始往下全部记录第二个参数tb_counter是顶层实例名建议显式写出来避免某些版本默认dump层级不对导致波形文件打开是空的。3.2 VCS编译命令行与参数拆解之前说过如果用命令行直接敲最容易出问题的就是这个编译指令。最标准的写法是vcs -sverilog \ -debug_accessall \ -P $VERDI_HOME/share/PLI/VCS/LINUX64/novas.tab \ $VERDI_HOME/share/PLI/VCS/LINUX64/pli.a \ -f scripts/filelist.f \ -o simv这里每个选项都必须理解透-sverilog开启SystemVerilog语法支持。如果你用了logic、always_ff、interface这类SV语法而不加它VCS会当成普通Verilog去解析然后报一堆莫名其妙的语法错误。-debug_accessall开启所有调试权限。没有这个VCS编译出来的simv不支持Verdi的连接运行时也写不出fsdb。很多新人用老教程里的-debug或-debug_pp在VCS 2020之后已经不够用了统一建议用-debug_accessall省心。-P novas.tab pli.a注册Verdi的PLI。这是VCS能识别$fsdbDumpfile的前提。如果漏掉编译时会直接报“system task $fsdbDumpfile is undefined”之类的错误。-f filelist.f从文件列表读入所有源文件路径而不是在命令行里一个个写。这是保持命令行简洁的最佳方式。-o simv指定生成的可执行文件名。默认就是simv我习惯显式写出防止什么人改过默认配置。我们还需要写一个filelist.f文件内容如下// scripts/filelist.f ../rtl/counter.v ../tb/tb_counter.sv incdir../rtl这里incdir../rtl是用来指定include路径的。虽然计数器例程里没用到include但实际项目几乎必用提前加上能避免后续“include file not found”的问题。注意filelist里的相对路径是相对你执行make或vcs时所在目录的这正是后面Makefile容易出坑的地方——我建议统一在执行目录上做文章让所有路径都相对工程根目录去计算。3.3 跑仿真和加载波形编译完成后会生成simv可执行文件。运行./simv -l sim.log-l sim.log指定仿真日志文件名所有$display、报错、$finish的信息都会写进去。这一步如果testbench里没有$finish或者仿真时间设置太长你会看到shell卡在那里不动。正常的话仿真会在$finish处正常退出。接着检查一下当前目录有没有生成counter.fsdbls -lh counter.fsdb如果看到文件存在且大小不是0恭喜你最关键的波形已经出现了。接下来用Verdi打开它verdi -f scripts/filelist.f -top tb_counter 这种打开方式会重新elaborate一遍设计加载源码和层次关系然后再通过Verdi界面的fsdb窗口手动导入counter.fsdb。还有一种方式是把编译时生成的数据库直接给Verdiverdi -dbdir simv.daidir counter.fsdb 使用-dbdir simv.daidir的好处是快不用重新elaborate。但前提是simv.daidir和fsdb是同一个编译版本生成的。实际使用中我更习惯第一种方式因为能让Verdi自动关联到当前测试平台的完整层次调试UVM环境时需要这一步。3.4 把流程固化到脚本上面几步全在终端里手工敲太脆弱了。为了把“可复现”落到实处我会把编译和仿真封装成一个最基础的Makefile先别急着做复杂抽象能跑、能看波形就行。下一节我专门讲Makefile的用法和坑。4. Makefile避坑指南4.1 一份可以直接套用的Makefile模板Makefile的价值在于把命令和依赖关系固化下来避免每次手敲一长串编译参数。我先把一份能用的模板放出来后面再逐条讲坑。# Makefile for VCS VERDI demo PROJ_ROOT : $(PWD) SIM_DIR : $(PROJ_ROOT)/sim FILELIST : $(PROJ_ROOT)/scripts/filelist.f TOP : tb_counter SIMV : simv VERDI_HOME ? /opt/Synopsys/VERDI PLI_PATH : $(VERDI_HOME)/share/PLI/VCS/LINUX64 VCS_OPTS : -sverilog -debug_accessall \ -P $(PLI_PATH)/novas.tab $(PLI_PATH)/pli.a \ -f $(FILELIST) \ -o $(SIMV) export VERDI_HOME .PHONY: all comp sim verdi clean all: comp comp: pre cd $(SIM_DIR) vcs $(VCS_OPTS) pre: mkdir -p $(SIM_DIR) sim: comp cd $(SIM_DIR) ./simv -l sim.log verdi: comp cd $(SIM_DIR) verdi -f $(FILELIST) -top $(TOP) clean: cd $(SIM_DIR) rm -rf simv simv.daidir csrc *.key sim.log counter.fsdb ucli.key novas.rc模板里几个细节是刻意加进去的pre目标用于创建sim目录防止第一次编译时报“目录不存在”。编译和仿真都cd $(SIM_DIR)保证所有中间产物包括fsdb落在sim目录下源码目录不脏。verdi目标依赖comp确保打开Verdi前设计已是最新编译结果。这个模板跑通没问题但实际用下来你会遇到几个隐藏很深的坑下面逐个讲。4.2 坑一找不到Makefile和目标“make: *** No rule to make target...”和“make: *** No targets specified and no makefile found”是我在各论坛见过人问得最多的两个错误。原因很简单你在一个没有Makefile的目录执行了make。这种问题甚至不算代码问题而是使用习惯问题。有人习惯cd scripts之后执行make但Makefile不在scripts下于是直接报错。解决办法我总结有三种把Makefile放在工程根目录统一在根目录执行make。如果Makefile放在scripts下就执行make -C scripts或者make -f scripts/Makefile。在Makefile内部用CURDIR和MAKEFILE_LIST动态拼接路径。我个人强烈推荐第一种把Makefile放工程根目录所有相对路径都以它所在目录为基准。这样无论你在哪个子目录执行make只要Makefile能找到路径就不会乱。如果你非要把Makefile放在子目录那么在Makefile顶部加一行PROJ_ROOT : $(abspath $(dir $(lastword $(MAKEFILE_LIST))))用它来定位工程根目录也不容易错。4.3 坑二PLI路径失效网上搜Makefile的时候经常能搜到别人脚本里写死的路径比如PLI_PATH : /home/xxx/verdi/share/PLI/VCS/LINUX64你直接抄过来大概率跑不通。因为每个人VERDI_HOME都不一样版本也不一样。PLI路径失效的典型症状是编译时$fsdbDumpfile报未定义或者运行时verdi连不上仿真fsdb文件根本没生成。我的处理思路是不要写死绝对路径而是通过环境变量VERDI_HOME推导。VERDI_HOME ? /opt/Synopsys/VERDI PLI_PATH : $(VERDI_HOME)/share/PLI/VCS/LINUX64但这里还有个隐患新版本Verdi的PLI目录从share/PLI/VCS/LINUX64变成了别的位置比如apps/PLI/VCS/LINUX64。所以更稳一点是先确认一次find $VERDI_HOME -name novas.tab 2/dev/null确认完再把路径填进Makefile。如果你的服务器上安装了多套Verdi建议在~/.bashrc里把VERDI_HOME指向你常用的那一套并顺便加上下面两个变量export VCS_HOME... export VERDI_HOME...这样Makefile里用$(VERDI_HOME)时就不会找错工具了。4.4 坑三增量编译的副作用VCS默认采用增量编译模式第一次全量编译之后只编译变化的部分。这对大工程帮助巨大能省大量编译时间。但副作用是如果你改了filelist里的文件列表或者换了UVM宏定义甚至只是删掉一个文件增量编译可能不会那么聪明仍沿用旧的编译状态最后行为很怪特别是“我明明改了代码为什么仿真结果没变化”。避坑方法是把“全量重编”和“增量编译”做成两个目标comp: cd $(SIM_DIR) vcs $(VCS_OPTS) recomp: clean cd $(SIM_DIR) vcs $(VCS_OPTS)平时小改一个RTL文件直接用make comp享受增量编译的速度一旦发现仿真结果和预期完全对不上怀疑是编译缓存闹鬼就make recomp全量重来一次。这个策略在真实项目中非常实用。另外如果你用Makefile的依赖关系来做自动增量编译必须非常清楚VCS的中间产物机制。simv.daidir、csrc、simv.db这些目录都是编译缓存不该手动去改。一旦手动删了一部分很容易触发VCS报“fatal error: default toolchain is corrupted”之类的奇怪问题。4.5 坑四filelist里的路径到底相对谁Filelist里的路径不是相对filelist文件本身而是相对执行vcs命令时的当前工作目录。这句话是新手最容易忽略的。假设你执行cd sim vcs -f ../scripts/filelist.f那filelist里写../rtl/counter.v就错了因为从sim目录出发../rtl/counter.v指向的是project/rtl/counter.v这反而是对的——但如果你从根目录执行vcs -f scripts/filelist.f那filelist里../rtl/counter.v就指向了工程上一级会找不到文件。解决方式有两个保持统一的执行目录比如Makefile里全部cd $(SIM_DIR)再执行vcs同时filelist里的相对路径也按$(SIM_DIR)为基准写。filelist里直接写绝对路径或者用Makefile变量传进去。比如在filelist里写$(PROJ_ROOT)/rtl/counter.v但Makefile里需要加上-pfile之类的方式做宏替换比较麻烦。最省心的做法是Makefile里统一cd $(SIM_DIR)执行vcs命令时用-f $(PROJ_ROOT)/scripts/filelist.ffilelist内部写相对$(SIM_DIR)的路径比如../rtl/counter.v和../tb/tb_counter.sv。因为$(SIM_DIR)是$(PROJ_ROOT)/sim所以../rtl/counter.v正好回到$(PROJ_ROOT)/rtl/counter.v。这个一致性你只要想清楚一次以后就不会再被路径绕晕。4.6 坑五制表符、行尾和续行的隐形坑Makefile对格式极其挑剔规则里的命令必须以Tab开头不能用空格。这个问题在论坛上被问了十几年还是不断有人踩。如果你复制一份别人博客上的Makefile粘贴到编辑器里时自动把Tab转成了空格执行时就会报“missing separator”或者“recipe commences before first target”。另外命令行续行符\后面不能有空格或多余字符。我见过有人把VCS_OPTS : -sverilog \ -debug_accessall \ -P ...里的续行符后面不小心多了一个空格导致Make解析时把选项切断出现诡异错误。这种问题肉眼很难发现建议用cat -A Makefile看一下行尾有^I表示Tab有\$$表示以$结尾\后不应有额外内容。还有一个小提醒Makefile里设置变量时:和有区别。:是立即展开是递归展开。在VCS操作里建议用:尤其是涉及$(PWD)这类动态变量时能确保它在你定义的那一刻被固化成当前路径避免后续cd操作导致$(PWD)改变。5. 常见问题排查速查5.1 问题速查表这里整理了一份我踩过的坑合集直接对着现象查就行。现象可能原因解决办法make: *** No rule to make target当前路径没找到Makefile或目标名拼错检查Makefile所在目录或使用make -f xxx/Makefilemissing separator或recipe commences before first targetMakefile里的命令用空格代替了Tab用vim或cat -A检查把命令行的缩进改成真正的Tab编译时$fsdbDumpfile is undefined没有添加-P novas.tab pli.a或PLI路径不对确认VERDI_HOME检查PLI路径补上-P编译时报Unknown identifier但代码看着没问题忘记加-sverilogSV语法没有被识别编译选项中增加-sverilogfsdb文件存在但大小为0$fsdbDumpvars的层次参数不对或者PLI版本不匹配在tb顶层写$fsdbDumpvars(0, tb_counter, all)确认PLI库与VERDI版本匹配仿真日志显示Time limit exceeded或卡死不动testbench里没有合理的$finish仿真时间太长检查激励的结束条件必要时加#时间 $finish;vcs -j报错说不支持受license或版本限制去掉-j顺序编译虽然慢但稳定打开Verdi后看不到源码层次-f filelist.f没有包含全部源文件或-top指定错误检查filelist完整性确认-top指向testbench顶层模块名counter.fsdb没生成在预期目录在当前目录执行simvfsdb生成在执行目录在Makefile里统一cd $(SIM_DIR)再仿真5.2 fsdb波形为空或打不开怎么排查fsdb波形是验证流程里最容易被忽视的一环问题频发。我总结了一套排查顺序第一步看仿真日志里有没有$fsdbDumpfile相关报错。如果VCS编译时没注册好PLI仿真时会在日志里打印类似“Unresolved system task”的信息这时候先回头补-P参数。第二步确认$fsdbDumpvars的层次参数。$fsdbDumpvars(0, tb_counter, all)这里的tb_counter必须是你testbench的实例名不是模块名也不是字符串。如果你写错了名字或者少了这个参数波形文件虽然会创建但里面可能没有数据。第三步检查fsdb文件到底生成到了哪里。前面说过simv运行时fsdb默认生成在当前工作目录。如果你在Makefile里没有统一cd $(SIM_DIR)仿真日志显示“open counter.fsdb”成功了但这个文件可能落在了sim目录之外。这时候直接find . -name *.fsdb找一下比在预期目录傻等要快得多。第四步如果fsdb有大小但Verdi打开后一片空白多半是VCS和Verdi版本不匹配导致fsdb格式内部分层发生变化。这种问题没有捷径只能重新确认工具版本必要时全量重编。5.3 编译报错分类与快速定位VCS的编译报错信息确实不友好但看多了能总结出规律。一类是语法错误比如漏分号、端口连接错误这类错误通常会明确告诉你文件、行号、出错token直接跳到对应位置改就行。另一类是SV关键字的“Unknown identifier”这类几乎都是因为编译命令里没加-sverilog。如果你用了logic、always_ff、interface、class而没加-sverilogVCS就会把这些当普通标识符解析。还有一类是“Packed dimension”或“Unexpected token”这类和宏定义相关的错误。实际项目中很多代码依赖include或宏定义如果filelist里没写全incdir或者定义了不同版本的宏就会出现一堆看似和“语法不搭边”的报错。建议优先检查filelist里所有include文件能否被找到再考虑是否是代码问题。5.4 仿真挂死是有规律可循的仿真卡死是验证里最耗时间的坑之一。常见的诱因有三个第一个是时钟和复位的死锁。比如testbench里复位一直无效DUT内部状态机一直等不到复位释放而testbench又在等待某个信号于是两边互相等。第二个是UVM里常见的timeout机制没配好。如果case挂死UVM的watchdog没有自动退出仿真就像陷入死循环一样。调试方法是给顶层仿真加一个$finish的兜底比如用initial #100000 $finish;做个最长时间的保险。第三个是大型仿真的performance问题尤其是涉及UVM的objection机制没drop干净导致run_test结束不了。这类问题通常不怪环境而是验证平台的设计问题。新手可以先跑最简单的RTL testbenchverify基础环境没问题再引入UVM这样能快速缩小排查范围。6. 写在后面我的几点实操感受这套环境搭了好几次之后我最大的感受是环境搭建不是写代码而是建立“可控性”。你需要的不是把一条命令敲出来而是确保每一次编译、仿真、看波形都能以同样的方式复现。Makefile的价值就是帮你锁住这些不确定性把精力留给真正的功能验证。最后分享一个我自己常用的小技巧在Makefile里加一个help目标把常用的几个命令用echo打出来比如make comp、make sim、make verdi、make clean。这样即使隔了两个月没碰一个工程回来也不会忘了怎么跑。项目多了之后你还会发现把每个模块的filelist单独管理、共用一套Makefile模板能让整个团队的工作方式非常统一。环境搭得越稳验证效率越高这件事真的会直接体现在你交付bug的速度上。
返回列表