尧图精选

用Makefile自动化VCS+Verdi仿真流程:数字IC验证效率提升实战

🕒 发布时间:2026/10/2 22:31:21 📁 来源:尧图网络
跑验证的第一天我就被反复敲命令这事折磨得够呛。每改一次RTL就得重新敲一遍VCS编译命令跑完了再敲一遍Verdi打开波形中间夹杂着各种路径切换和参数调整。后来带我的前辈瞟了一眼我的终端历史记录丢下一句这玩意儿不该靠人肉重复然后给我演示了一个Makefile脚本怎么把编译、仿真、看波形全部串起来。从那时起我才真正意识到Makefile在数字IC验证流程里的分量。今天这篇就围绕Makefile脚本启动VCSVerdi这个主题把我在实际项目中验证过、踩过坑之后沉淀下来的完整做法分享出来。这篇文章适合刚接触数字IC验证、想规范化自己仿真流程的初级工程师也适合已经在用VCS和Verdi但还在手动敲命令、想提高效率的验证同学。你会看到一套完整的Makefile设计方案包括变量怎么规划、编译仿真目标怎么拆、fsdb波形怎么自动生成、Verdi怎么一键启动以及我在实际使用中遇到的那些没人告诉你的坑。1. Makefile VCS Verdi 这套组合到底解决了什么问题1.1 验证工程师的日常真的不应该耗在敲命令上很多刚入门验证的同学包括当年的我会理所当然地认为跑仿真的流程就是敲命令—编译、跑仿真、打开Verdi完事。但当你面对的是一个真实的项目一个DUT动辄几千行代码testbench分层结构复杂还经常需要在不同测试用例之间切换时手动敲命令的方式会迅速变成灾难。首先是命令行长度失控。VCS编译一条命令往往要带十几个选项包括-sverilog、-debug_accessall、-timescale1ns/1ps、-f filelist.f、-o simv等等手一抖敲错一个字母编译失败重来。其次是参数不一致的问题。今天你顺手加了一个acc1明天别人跑的时候忘了加编译出来的仿真器行为可能就有差异波形里看不到内部信号排查半天发现是编译选项少了。再一个是文件清单管理的问题。RTL文件新增、删除、换路径如果每个文件都在命令行里手写每次变动都要去改那行命令错一个文件整个编译就挂了。这套流程跑下来你会发现真正有产出价值的不是敲命令这个动作而是分析波形、定位bug、收敛覆盖率。Makefile脚本的价值恰恰是把前面那些机械、重复、容易出错的部分抽象成几个固定目标把你从命令行里解放出来。1.2 为什么是Makefile而不是Shell脚本有人可能会问写个Shell脚本不也能做到吗为什么非得用Makefile我自己的体会是Shell脚本当然能做但它缺少Makefile最核心的一个能力——依赖关系管理。Makefile天然支持目标之间的依赖比如sim这个目标依赖compile你只需要make sim它会先检查编译产物是否存在、源码有没有更新如果源码比仿真器新它会自动重新编译再启动仿真。这个增量构建的判断逻辑用Shell脚本写起来很繁琐而Makefile是内置的能力。此外Makefile的变量机制非常适合工程化管理。工具路径、编译选项、文件列表、波形文件名全部收敛到文件顶部的变量区。改东西只改一处不用满脚本去翻去替换。而且Makefile本身的语法足够简单还能通过make -n预览要执行的命令通过make -j并行跑多任务这些都是Shell脚本用起来没那么顺手的地方。当然Shell脚本在复杂逻辑处理上也有它的优势比如循环、条件判断更灵活。但针对VCSVerdi这个固定的仿真流程Makefile的简洁和确定性是更合适的。我的建议是把Makefile作为主入口某些复杂的检查逻辑再单独抽一个Shell脚本让Makefile去调用各司其职。1.3 方案选型背后的几条关键考量在敲定这套方案之前我还考虑了另外几个点分享给正在做方案选型的同学参考。第一点是可移植性。公司里的EDA环境和本地可能不一样工具安装目录、license配置、库文件路径都有差异。所以在Makefile里把工具路径和关键目录抽成变量团队内共享时每个人只需要改机器相关的几行而不是全文替换。这也是我在后面详细展开变量的原因之一。第二点是标准化和可继承性。验证环境不是一个人的事新同学加入团队不需要从头理解编译该怎么敲、Verdi怎么连而是直接make compile、make verdi看懂了Makefile顶部的变量注释基本就理解了整个仿真流程。这让环境交接变得非常顺畅。第三点是和回归测试的衔接。有了Makefile之后跑回归不是挨个去跑每个test而是可以定义regress目标遍历测试用例目录逐个调用编译好的仿真器收集结果。这些在手动敲命令的场景下成本高到没人愿意去做但在Makefile体系下只是增加一个目标的事。2. 动手之前先把Makefile的变量规划和目录结构理清楚2.1 顶层变量怎么设计才不容易翻车很多新手写Makefile最大的问题是把路径写死在规则里改一次路径要大动干戈。我习惯在文件顶部用一个清晰的区域把所有可变项收敛起来关键变量用:直接赋值这样每一行都在定义时就确定下来后面引用时不会出现递归展开的意外。我当前项目里的一段变量定义大致是这样# 工具路径 VCS : /tools/synopsys/vcs/O-2018.09/bin/vcs VERDI : /tools/synopsys/verdi/V-2020.01/bin/verdi SIMV : ./simv # 仿真参数 VCS_OPTS : -sverilog v2k acc1 -debug_accessall -timescale1ns/1ps VCS_OPTS -f filelist.f -o simv -l compile.log VCS_OPTS -full64 # 运行选项 SIM_OPTS : fsdbautoflush fsdbdumpfile$(DUMP_DIR)/wave.fsdb SIM_OPTS -l sim.log用:而不是是因为我在定义SIM_OPTS时引用了后面的DUMP_DIR如果不是展开时立即取值变量的值在不同位置引用可能得到不一样的结果排起错来非常头大。另外工具路径这一项建议在MAKEFILE里加一个检查避免在工具链没配置好的机器上跑make报出来的错误让人摸不着头脑$(info 编译工具: $(VCS)) ifeq ($(wildcard $(VCS)),) $(error 未找到VCS工具请检查VCS路径: $(VCS)) endif这段代码用wildcard判断文件是否存在不存在则直接报错退出早发现早解决比等到编译阶段报command not found要友好得多。2.2 目录结构怎么规划才能支撑多目录源码编译关于目录结构我踩过不少坑尤其是多目录源码编译这个场景。一个中等规模的验证环境RTL代码通常按模块放在不同目录下testbench又在另一处公共VIP包又可能分散在不同地方。如果不做规划filelist.f就会变成一个巨型文件维护成本陡增。我的做法是分层维护文件清单. ├── Makefile ├── filelist.f ├── rtl/ │ ├── module_a/ │ │ ├── a_top.v │ │ └── a_sub.v │ └── module_b/ │ └── b_top.v ├── tb/ │ ├── tb_top.sv │ ├── testcase/ │ │ ├── test_basic.sv │ │ └── test_reg.sv │ └── vip/ └── dump/在这种结构下filelist.f里用相对路径或绝对路径把要编译的源文件按功能块组织并加上注释说明每个区块是干啥的// RTL 模块 rtl/module_a/a_top.v rtl/module_a/a_sub.v rtl/module_b/b_top.v // Testbench tb/tb_top.sv tb/testcase/test_basic.sv // VIP 组件 tb/vip/ahb_master.sv针对VCS的库文件处理如果用到的是编译好的加密IP或者单元库可以在filelist.f里用-v和-y选项引用VCS会按指定路径去搜索这些库。这种做法在多目录、多IP的项目里几乎必不可少Makefile里只需要固定引用filelist.f内部的组织由维护者按约定维护即可。2.3 目标设计compile、sim、verdi、clean各司其职Makefile的目标设计直接决定你日常使用是丝滑还是别扭。我的经验是按验证流程的语义切分目标不要贪多也不要太少。基础目标是这四个compile执行VCS编译产出仿真器可执行文件simv。sim运行编译好的simv执行仿真产出波形文件。verdi启动Verdi加载fsdb波形打开调试界面。clean清理编译产物和仿真中间文件。这之外可以按需增加regress回归、lint代码检查、synth等目标但核心流程我建议始终围绕上述四个目标来组织保持简单可预期。另外一定要声明.PHONY。不声明的话如果目录下恰好存在一个叫clean的同名文件make clean会被判定为无需执行。这是老生常谈但我见过不止一个同事因为这个诡异问题浪费了半天时间。.PHONY: compile sim verdi clean regress3. 完整Makefile脚本实现与VCS/Verdi联合仿真配置3.1 编译目标把一次性命令改成可持续复用的资产先看核心的编译目标。这一步本质上是把VCS的常用编译选项收敛到Makefile变量中并让后续的仿真目标依赖它的产物。compile: mkdir -p $(DUMP_DIR) $(VCS) $(VCS_OPTS)VCS_OPTS里面包含的选项每个都有它的针对性-sverilog启用SystemVerilog语法支持。v2k兼容Verilog-2001语法。acc1允许PLI/probe访问配合Verdi调试时经常需要。-debug_accessall生成绩效和波形调试所需的完整调试信息不加的话Verdi打开后的信号可见性会受限很多内部信号看不到。-full64以64位模式编译仿真器处理大规模设计和长仿真时间更稳尤其是在内存超过4GB的场景。-timescale1ns/1ps统一时间精度避免各文件精度不统一导致仿真时间异常。编译阶段我会习惯性地加上-l compile.log把日志写到文件里。编译报错时直接打开log搜索Error关键字配合编辑器跳转到代码行号比在终端里翻输出高效得多。一个实用的小技巧是在Makefile里定义COVERAGE_OPTS和DEBUG_OPTS两组可选项通过在命令行用make compile DEBUG1切换而不是把所有选项永远堆在一起DEBUG ? 0 ifeq ($(DEBUG),1) VCS_OPTS -debug_accessall else VCS_OPTS -debug_accesspp endif这里?表示如果命令行没有传入DEBUG变量就使用默认值0ifeq做条件判断按需加入不同的调试信息级别。这样一来快速跑冒烟测试时可以去掉重调试选项仿真速度能快不少。3.2 仿真目标仿真器运行与fsdb波形自动生成的关键写法编译完之后就是仿真。仿真的本质是运行./simv但关键在于波形文件怎么产生以及Makefile怎么组织才能让整个流程自动化。关于波形生成我遇到过两类场景。一是验证代码里已经通过$fsdbDumpfile和$fsdbDumpvars系统任务主动控制波形导出这种情况下仿真目标里不需要额外干预。二是代码里没有写dump逻辑尤其是一个通用的testbench要跑多个用例时我更倾向于不修改仿真代码而是通过仿真器的命令行参数来控制波形生成让Makefile来统一处理sim: compile $(SIMV) $(SIM_OPTS)其中SIM_OPTS包含了SIM_OPTS : fsdbautoflush fsdbdumpfile$(DUMP_DIR)/wave.fsdb SIM_OPTS ntb_random_seed1 -l sim.logfsdbautoflush让仿真边跑边往fsdb文件里刷数据避免仿真中途崩溃导致波形全部丢失这个在长仿真里非常重要。fsdbdumpfile指定路径可以在不改testbench的情况下把波形文件导出到指定目录并指定文件名。这两个参数配合起来即使代码里一行$fsdbDumpfile都没写照样能产出波形非常实用。如果你在testbench里用initial块主动控制波形层次那写法通常是initial begin $fsdbDumpfile($(DUMP_DIR)/wave.fsdb); $fsdbDumpvars(0, tb_top); end注意$fsdbDumpvars的第一个参数是层级数0表示dump当前模块以下所有层级的信号1只dump当前层级2dump下一层。全开最省心但文件体积会大很多这个取舍后面在问题部分细讲。仿真跑完别急着关终端。先看sim.log里面有没有Error、Failure之类的关键词确认没挂再进入下一步。这个检查动作也可以自动化比如在Makefile里加一个check目标check: grep -E Error|Failure sim.log || echo 日志检查通过我实际用的时候还会顺手把pass/fail状态统计出来。UVM的log默认自带UVM_INFO、UVM_ERROR这些标签用grep过滤一下UVM_ERROR : 0就能判断用例是否通过做成Makefile里的一个check_log目标回归的时候非常省事。3.3 Verdi启动目标波形加载与快捷键一个都不能少仿真跑完到了看波形的环节。在Makefile里把Verdi的启动也固化下来好处是打开的命令永远是同一套参数不会因为某次敲错路径而加载不到波形。verdi: $(VERDI) -f filelist.f -ssf $(DUMP_DIR)/wave.fsdb -top tb_top -f filelist.f让Verdi加载所有源码文件-ssf指定要打开的fsdb波形文件-top tb_top指定hierarchy顶层。每次执行make verdi就能直接看到源码和波形同时加载好的调试界面。这里用把Verdi放到后台启动避免终端卡在前台进程里退出Verdi之前无法继续敲命令。如果仿真还没有跑也可以启动Verdi先看代码结构、检查fsdb文件是否生成此时不带-ssf参数即可verdi_source: $(VERDI) -f filelist.f -top tb_top 当Verdi打开之后几个高频操作我得单独提一下。n和N键是波形窗口里跳转下一个/上一个上升沿非常常用在源文件窗口里直接选中信号然后按CtrlW可以快速添加波形按h键可以测光标间的时间差算时序延迟的时候特别方便。这些快捷键看着小实际用起来对手感和效率影响巨大属于知道的人觉得理所应当、不知道的人手动点半天的那类知识。Verdi界面打开后如果发现有些信号显示为x或者根本找不到十有八九是编译时的调试信息级别不够。解决方法是回到compile目标确认VCS_OPTS里带了-debug_accessall或者至少-debug_accesspp重新编译一次再仿真。3.4 clean目标与多目标联动的最终整合最后把clean目标补上整个基础流程就闭环了clean: rm -rf simv simv.daidir csrc verdiLog *.log $(DUMP_DIR) *.fsdb clobber: clean rm -rf *.vpd ucli.key注意我们在clean里加了*.log和$(DUMP_DIR)确保日志和波形这类体积大的中间产物能被彻底清掉否则几次迭代跑下来硬盘很容易被堆满。然后你日常的使用流程就非常简洁make compile # 编译生成 simv make sim # 仿真生成 wave.fsdb make verdi # 打开 Verdi加载源码和波形 make clean # 清理中间文件调试完一轮修改RTL之后直接重新make simMakefile会发现源文件更新了自动触发重编译无需手动分两步执行。这是我的高频用法也是增量构建最直观的收益。如果需要一次跑完整个流程可以把默认目标设置成allall: compile sim然后直接make一把梭。不过我个人建议日常还是分开执行编译和仿真分开能更早发现是哪一步出问题日志也更好对。至于all目标更适合自动化回归或CI场景。完整脚本放出来方便直接对照使用# # Makefile for VCS Verdi verification flow # 用法: # make compile 编译RTLTB生成simv # make sim 运行仿真生成fsdb波形 # make verdi 启动Verdi加载源码和波形 # make clean 清理中间文件 # # 工具路径 VCS : /tools/synopsys/vcs/O-2018.09/bin/vcs VERDI : /tools/synopsys/verdi/V-2020.01/bin/verdi SIMV : ./simv # 目录 DUMP_DIR : dump # 编译选项 VCS_OPTS : -sverilog v2k acc1 -debug_accessall VCS_OPTS -timescale1ns/1ps -full64 VCS_OPTS -f filelist.f -o simv -l compile.log # 仿真选项 SIM_OPTS : fsdbautoflush fsdbdumpfile$(DUMP_DIR)/wave.fsdb SIM_OPTS -l sim.log # 声明伪目标 .PHONY: all compile sim verdi clean # 默认目标 all: compile sim # VCS编译 compile: mkdir -p $(DUMP_DIR) $(VCS) $(VCS_OPTS) # 运行仿真 sim: compile $(SIMV) $(SIM_OPTS) # 启动Verdi并加载波形 verdi: $(VERDI) -f filelist.f -ssf $(DUMP_DIR)/wave.fsdb -top tb_top # 清理编译仿真产物 clean: rm -rf simv simv.daidir csrc verdiLog *.log $(DUMP_DIR) *.fsdb4. 常见问题排查与避坑实录4.1 波形打开后一片空白或信号全是x问题出在哪这是新手问得最多的问题之一根源大多出在编译选项上。Verdi要显示内部信号值依赖的是仿真时留下的调试信息而VCS默认的编译选项并不会把所有的调试信息都留下来。我在前面反复强调的-debug_accessall就是为这个场景准备的。还有同学反馈波形文件有信号也在但全是红色的x。这个通常不是Makefile的问题而是testbench里没有做正确的复位。复位信号拉高/拉低之后时序逻辑才会有确定的初值。如果复位逻辑本身写错了或者复位序列没有在波形里体现那信号自然是x。排查时先用Verdi的Get Signals功能把复位相关信号加进来看复位时序是否和预期一致。如果是加密模块或者第三方IP信号是x还可能因为模型本身的抽象级别就不支持内部信号观测这种只能靠IP厂商提供的接口和协议来调试不是Makefile层面能解决的。4.2 make: 没有指明目标并且找不到makefile是哪里没配置对这个经典报错本质是make在当前目录下找不到一个叫Makefile或makefile的文件。常见原因有这么几个文件名拼错成了Makefiles或者MAKEFILE当前工作目录不对比如进了rtl/子目录但Makefile在上一级或者Makefile被某种方式保护起来没权限读。遇到这个报错第一件事是ls -la看目录下到底有没有Makefile。如果没有看是不是压根没创建或者放在了其他目录。如果在子目录可以make -C .. compile指定在父目录执行。如果Makefile存在但有权限问题chmod r Makefile解决。这个看起来特别小白的问题之所以值得拿出来讲是因为它往往不是真的不懂Makefile而是目录切换和路径管理习惯的问题。养成每次都在项目根目录执行make的习惯能少踩很多坑。4.3 fsdb文件过大仿真跑了很久但dump阶段直接卡死波形文件动辄几个GB甚至几十个GB是长仿真的常见现象。fsdb文件过大的直接后果是Verdi打开极度卡顿仿真结束前的dump阶段还会因为磁盘IO写入速度跟不上而拖慢整体时间。我的解决方案是分层次dump。在Makefile里增加一个变量控制dump的层级数DUMP_LEVEL ? 0 ifeq ($(DUMP_LEVEL),0) SIM_OPTS fsdblevel0 else SIM_OPTS fsdblevel1 endif跑全芯片级的大回归时用make sim DUMP_LEVEL1只dump顶层快速确认整体行为定位某个模块内部的bug时再切回DUMP_LEVEL0全量dump。这样既保证了调试能力又不至于让每个用例的波形都大得吓人。还有一个技巧是如果你的验证环境里存在模拟模型或者PLL这类高频翻转信号可以在$fsdbDumpvars之后使用$fsdbDumpvars的补充参数来排除特定模块或者干脆在信号列表层面过滤。不过这个更细节一些等真正被波形撑爆硬盘的时候再研究也不迟。4.4 不同机器/不同VCS版本下Makefile行为不一致团队协作时最头疼的莫过于A机器上能跑B机器上报错。这种不一致大概率出在工具路径、VCS版本差异、库文件路径几方面。工具路径的问题通过把VCS、VERDI变量独立出来并在文件顶部加上(wildcard)检查可以提前拦截我在前面已经演示过。VCS版本的差异是最隐蔽的不同版本对SystemVerilog新特性的支持程度不一样同一个文件在VCS 2018版本能编译过到2020版本可能因为某个语法检查更严格直接报错。应对方法是在Makefile里增加一个版本声明变量和一个展示版本信息的目标VERSION_TAG : vcs_$(shell $(VCS) -ID | grep -i version | awk {print $$4})然后在编译日志里记录这个变量一旦后续仿真行为诡异先对照编译时用的VCS版本和验证环境配置文件里要求的版本是否一致。别小看这一步它能帮你避免大量玄学low-level的排查时间。4.5 增量编译不生效每次都在漫长全量编译中煎熬Makefile的依赖管理本质上是靠文件时间戳来判断的但集成到VCS里有个特殊情况VCS编译的源文件是在filelist.f里指定的而Makefile的compile目标本身只依赖filelist.f的更新不会感知到filelist.f里列出的文件是否更新。也就是说哪怕你改了一个.v文件直接在命令行跑make compile如果filelist.f的mtime没变Makefile会认为不需要重编译这是增量编译不生效的最常见原因。解决方案有几种但我用得最多的是让Makefile自动检测所有源文件SRC_FILES : $(shell cat filelist.f | grep -v ^// | grep -v ^\s*$)然后把compile目标的依赖改为$(SRC_FILES)compile: $(SRC_FILES) $(VCS) $(VCS_OPTS)这样任何一个源文件更新make compile都会感知到并重新编译。如果想让VCS内部的增量编译也生效编译选项里可以加上-Mupdate让VCS自动维护文件依赖关系重编译时只增量更新受影响的模块速度提升非常明显。4.6 调试大法make -n和日志逐层定位最后分享一个我个人最喜欢的排查思路。当Makefile行为不符合预期时先用make -n compile看Makefile打算执行哪些命令不要真的执行。这会把你从猜它为什么这样中解放出来直接看它准备干什么。如果命令本身没问题再去看对应的日志。编译问题看compile.log仿真问题看sim.logVerdi加载问题看verdiLog/目录下的日志。分层定位基本能覆盖90%的情况。还有一个习惯我会在Makefile里把调用的关键命令通过echo打印出来compile: echo VCS Compile Start $(VCS) $(VCS_OPTS) echo VCS Compile End 这样每次执行的时候屏幕上能清楚看到当前进行到哪一步配合-l compile.log什么时候挂的、在哪一步挂的一目了然。等环境稳定了再把echo注释掉也不迟。5. 把Makefile再往前推一步参数化、回归与日常养成前面对目标层级的讲解已经覆盖了标准的三目标流程我看到很多团队其实止步于这里但Makefile的能力远不止这些。我自己在用顺手之后又增加了两个目标把日常工作进一步简化。第一个是test参数化。在验证工作里经常需要给仿真器传test name比如UVM的UVM_TESTNAMEtest_basic。如果每次都在命令行手动写麻烦且容易错。在Makefile里定义一个TEST变量透传到SIM_OPTS里TEST ? test_basic SIM_OPTS UVM_TESTNAME$(TEST)日常跑用例只需要make sim TESTtest_reg第二个是回归目标。当用例多了之后逐个执行显然不现实我加了一个regress目标遍历一个测试用例清单文件逐个跑仿真regress: for test in cat testlist.txt; do \ $(SIMV) UVM_TESTNAME$$test fsdbautoflush -l log_$$test.log; \ done echo 回归完成请查看各 log 文件这里要注意回归里如果还每个用例都dump fsdb磁盘和耗时都受不了我一般会在regress的仿真参数里不携带fsdbdumpfile只靠log做结果判别。需要追查失败用例时再单独make sim TESTxxx重新生成fsdb。这种做法的意义在于让Makefile的脚本能力随着验证环境的成长而成长。它不是一个写完就固定不变的脚本而是一个可以持续增加目标来适配新需求的自动化入口。这也是Makefile和一次性Shell命令之间最本质的区别——前者是可持续积累的资产后者只是临时工具。回到开头那个问题用Makefile启动VCS和Verdi到底是为了什么我认为核心是为了把人的注意力从机械命令里释放出来让验证工程师把更多精力投入到真正有挑战性的调试和分析中去。当我看到新同事拿到这套脚本不用问任何问题就能make compile make sim make verdi跑通第一个用例时我觉得当初花时间把这些命令收敛成目标是非常值得的。以上是这套方案的全部内容。如果你在实际配置过程中遇到其他问题尤其是和VCS版本、Verdi加载、多目录源码相关的坑欢迎一起交流。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →