ARTICLE DETAIL

资讯详情

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

VCS编译与仿真选项实战:数字IC验证避坑与覆盖率收口

VCS编译与仿真选项实战:数字IC验证避坑与覆盖率收口 我做了几年数字 IC 验证换过几家公司也用过不止一款仿真器但每天在终端里敲得最多的还是那两行一行vcs带一长串编译选项一行simv带一长串仿真选项。VCS 这套工具用久了会发现它真正的门槛既不在 SystemVerilog 语法也不在 UVM 框架而在那些看起来零碎的编译和仿真选项上——选项配错了轻则波形 dump 不出来重则回归跑一晚上结果全是假的更糟的是覆盖率怎么都收不上去你还以为是激励写得不够。这篇东西我打算把 VCS 的编译和仿真选项从头到尾捋一遍。不讲手册目录那种罗列而是按为什么需要它、不加会怎样、加了要注意什么这个顺序讲。主要面向刚入行的验证工程师、从设计转验证的朋友以及需要自己搭仿真环境的人。看完之后你至少应该能独立写出一份能跑通、能出波形、能收覆盖率的编译脚本并且在遇到FSDB 没波形随机种子每次都一样这类问题时知道该从哪个选项开始查。1. VCS 编译与仿真的整体流程和选项分类思路1.1 为什么必须把编译期和仿真期分清楚VCS 是典型的两段式仿真器。vcs命令做的事情是把所有源文件——RTL、testbench、UVM 库、DPI-C 代码——解析、展开、优化最后链接成一个可执行文件默认名字是simv。simv做的事情是在运行时加载命令行参数、读入随机种子、打开波形文件、执行仿真。这个分工带来一个非常实用的特性编译一次仿真多次。回归的时候几百个 case 共用同一个simv只改运行时参数省下的时间非常可观。但这也意味着选项天然分成两拨。vcs后面的选项在编译那一刻就固定了比如语言的版本、宏定义、调试信息的详细程度、覆盖率要采哪几类simv后面的选项每次运行都可以不一样比如随机种子、波形文件名、覆盖率数据库名。把define写到simv后面或者把-cm_name写到vcs后面都是新手经常犯的错而且这类错误往往不报错只是没效果查起来特别费劲。提示判断一个选项属于哪一拨最简单的办法是看它会不会改变编译产物的内容。会改的必须在vcs阶段给只影响运行时行为的放simv阶段。1.2 选项按功能分三组记忆负担立刻减半我习惯把 VCS 的选项分成三组来记这样即使记不住具体拼写也能大概知道去哪找。第一组是语言与解析组负责告诉编译器你正在读的东西是什么语言、去哪里找其他文件。典型成员有-sverilog、-v2k、-f、-F、-y、libext、incdir、define。这组选项错了编译直接失败报错信息通常比较明确反而好解决。第二组是编译产物与调试组决定生成的simv带不带调试能力。典型成员有-debug_accessall、-kdb、-lca、-o、-Mdir、-j。这组选项直接影响仿真速度给得越多simv越慢因为它要维护更多元信息供调试器查询。很多团队抱怨加了 Verdi 支持之后仿真慢了一倍原因就在这里。第三组是覆盖率与优化组典型成员有-cm、-cm_dir、-cm_hier以及仿真期的-cm_name。这组最容易出问题的地方是编译期和仿真期的-cm必须匹配编译期采了哪几类仿真期必须写一模一样的字符串否则覆盖率数据库会缺项或者干脆不生成。1.3 一份能直接抄的编译脚本骨架先给一个我常用的最小可用版本后面再逐项拆解。假设 RTL 放在./rtlTB 放在./tbUVM 用 1.2 版本vcs -full64 -sverilog -timescale1ns/1ps \ -ntb_opts uvm-1.2 \ incdir./tb incdir./rtl/include \ defineSIM_TOP defineDUMP_FSDB \ -f ./flist/rtl.f -f ./flist/tb.f \ -debug_accessall -kdb -lca \ -cm linecondfsmtglbranch \ -cm_dir ./cov/simv.vdb \ -j 8 \ -l ./log/compile.log \ -o simv这份脚本里-j 8让编译用 8 个线程并行大型 SoC 的编译时间能从二三十分钟压到几分钟-l把编译日志落盘出问题时有据可查-o simv显式指定输出名避免默认名带来的覆盖风险。这几行组合下来一个中等规模的 UVM 环境基本可以跑起来。1.4 选项之间有优先级别在不同地方重复定义VCS 的选项不是完全平级的。有些选项会互相覆盖后出现的覆盖先出现的有些是开关式的开了就不好降级比如-debug_accessall一旦给了想通过后面加别的选项把调试能力减下来基本没用只能重新编译。我遇到过一次团队为了加快仿真想把调试等级降下来但 Makefile 里定义调试开关的地方有两处一处改了另一处没改结果编译出来的simv还是带完整调试信息的白折腾一下午。从那以后我给自己定了个规矩同一个语义的开关整个工程里只允许出现一处。2. 编译阶段核心选项逐个拆解2.1 文件组织三件套-f、-F、-y 到底怎么选源文件少的时候直接跟在命令后面就行文件一多就必须用文件列表。-f filename是最常用的写法文件里一行一个路径支持注释和换行。-F filename和它长得像区别在于-F在解析时会先做一轮展开处理路径里带变量、带相对层级的时候更省心。我的一般建议是纯路径列表用-f需要在列表里嵌宏或者写相对路径的用-F别混着用出问题不好定位。-y和libext是配套的。当你的 RTL 里例化了一个模块但编译命令里没显式列出这个模块的文件VCS 会去-y指定的目录里找找的时候按libext给出的后缀挨个试。比如vcs -sverilog -y ./rtl/common libext.v.sv libext.svh \ ./tb/top_tb.sv这行的意思是如果top_tb.sv里例化了fifo_sync编译器会去./rtl/common下依次找fifo_sync.v、fifo_sync.sv、fifo_sync.svh。这个机制方便但也是双刃剑——一旦目录里有同名文件编译器选哪个不由你决定容易出我明明改了这个文件为什么没生效的鬼故事。我个人更倾向老老实实维护文件列表-y只在第三方 IP 那种文件一大堆又不想逐个列的地方用。2.2 define 的三种用法和一整套条件编译体系define是编译期选项里性价比最高的一个。它有三种写法用途完全不同写法示例适用场景纯开关defineDUMP_FSDB代码里用ifdef判断控制某段逻辑编不编进去带值defineDATA_WIDTH64参数化配置替代改代码带引号的值defineTOP_NAMEtb_top字符串宏常用于$display前缀或文件命名实际项目里我常用它来做三件事。第一是区分仿真和综合defineSIMULATION让 RTL 里的仿真专用分支生效。第二是切换波形格式代码里写成ifdef DUMP_FSDB ... $fsdbDumpvars ... else $vcdpluson ... endif编译时换宏就换格式不用改代码。第三是控制断言和打印级别这个用得最多尤其是回归时把冗余打印关掉跑得快很多。注意define的定义顺序会影响结果。如果同一个宏被定义了两次VCS 默认会用最后一次的定义。所以别在多个文件列表里重复定义同一个宏真要覆盖就在命令行末尾统一给。2.3 时序反标-sdf 与那几个配套开关门级仿真或者带时序的后仿绕不开 SDF 反标。基本写法是vcs -sverilog -full64 -f ./flist/gate.f \ -sdf max:tb_top.u_dut:./sdf/dut_max.sdf \ -sdf typ:tb_top.u_dut:./sdf/dut_typ.sdf \ neg_tchk -negdelay \ -o simv_gate这里max:instance:file的意思是把这个 SDF 文件反标到指定实例上用 max 角。neg_tchk允许负的建立保持时间检查先进工艺里很常见不加会报一堆 warning 甚至过滤掉检查-negdelay允许负延时。这两个开关不加后仿往往跑得太干净时序违例一个都报不出来等于白跑。我踩过的一个坑是 SDF 的路径匹配。SDF 文件里写的实例层次和网表里的层次必须严格一致差一个genblk或者数组下标就匹配不上。VCS 反标失败的时候默认只给 warning不给 error很容易被淹没在日志里。我的做法是编译后一定 grep 一遍日志里的SDF关键字确认反标条数和预期差不多。2.4 覆盖率编译选项-cm 家族的配合关系覆盖率选项是编译期给一半、仿真期给一半的典型。编译期要做的是决定采哪几类-cm line行覆盖最基础-cm cond条件覆盖看 if 里的每个条件取值-cm fsm状态机覆盖状态和状态跳转-cm tgl翻转覆盖信号 0→1、1→0-cm branch分支覆盖if/else、case 分支-cm assert断言覆盖SVA 的 cover 语句常用的组合是-cm linecondfsmtglbranch后仿或者功耗相关再加tgl。-cm_dir指定数据库目录一个工程所有 case 的覆盖率会写进同一个目录最后合并。-cm_hier是我强烈推荐的一个选项它接受一个文件里面可以精确指定哪些模块采、哪些不采。大型 SoC 里如果全采覆盖率数据库能到几十 GB合并一次要半小时。用-cm_hier把第三方 IP、工艺库、纯连接模块排除掉数据库能瘦身一个数量级。// cov.cfg 示例 tree tb_top.u_dut 0 tree tb_top.u_dut.u_core 1 tree tb_top.u_dut.u_ddr 0 -moduletree tb_top.u_phy 0tree是精确到实例-moduletree是按模块名全局排除。上面这段的意思是u_dut整体不采但里面的u_core要采u_ddr不采u_phy整个模块树都不采。2.5 调试信息与编译加速-debug_access、-kdb、-lca、-j这四个选项经常一起出现但作用完全不同。-debug_accessall开启所有调试能力包括信号、类、断言、事务。-kdb生成 Verdi 需要的知识库配合-lca一起用。-lca是启用一些高级特性的开关很多调试相关的选项不加它会直接被拒。调试等级是有梯度的从快到慢大致是不给调试选项 -debug_accesspp-debug_accessclass-debug_accessall。回归阶段如果不需要看波形建议用最低等级重新编译一个版本速度差异在大型设计上可能有百分之二三十。-j N是编译加速选项N 取核心数的一半到全部之间。我们机器 32 核一般给-j 16给满反而因为内存争抢变慢。还有-Mdir指定中间文件目录我习惯把它和-o一起用让不同配置的编译产物互不干扰vcs -f ./flist/all.f -Mdir ./csrc_dbg -o simv_dbg -debug_accessall -kdb -lca vcs -f ./flist/all.f -Mdir ./csrc_reg -o simv_reg -debug_accesspp -j 16这样一份源码能同时维护调试版和回归版两个simv互不覆盖。3. 仿真阶段核心选项实战3.1 运行控制和随机种子为什么你的随机每次都一样simv运行时的基本形式是./simv -l ./log/test1.log \ ntb_random_seed12345 \ -cm linecondfsmtglbranch \ -cm_name test1 \ -cm_dir ./cov/simv.vdbntb_random_seed控制 UVM 和$urandom的随机种子。这里有个很多人不知道的细节如果不显式指定种子VCS 每次运行会用同一个默认值也就是说你以为在跑随机其实每次波形一模一样回归等于在做无用功。要做真正的随机回归要么每个 case 用不同种子要么用ntb_random_seed_automatic让种子随时间自动生成。-l指定日志文件别看它简单回归出问题时第一件事就是看日志日志没落盘就只能重跑。-cm_name给这次运行起个名字覆盖率数据库里每条记录对应一个 case名字重复会覆盖所以一般直接用 case 名或者跑号。3.2 波形 dumpVCD、VPD、FSDB 三者的取舍波形格式的选择直接决定调试体验和磁盘占用这是我见过最容易顺手做错的地方。格式生成方式优点缺点VCD$dumpvars/dumpvars通用任何工具都能读文件巨大无压缩无层次过滤VPD$vcdpluson/vpdfileVCS 原生速度快基本只能 VCS 系工具读FSDB$fsdbDumpvars压缩率高Verdi 原生支持精细过滤依赖 Verdi 的 PLI 库实际项目里九成以上用 FSDB原因是它能在 dump 的时候做过滤。控制 FSDB 的关键在$fsdbDumpvars的参数initial begin $fsdbDumpfile(./wave/test1.fsdb); $fsdbDumpvars(0, tb_top, all); $fsdbDumpvars(0, tb_top.u_dut, mda); $fsdbDumpMDA(); end第一个参数是深度0表示不限深度第二个是起始实例后面的字符串是附加控制all打开全部信号mda支持多维数组。$fsdbDumpMDA用于把多维数组展开成 Verdi 能看的格式不加的话数组只能看到一坨。注意$fsdbDumpvars的第一个参数千万别随手写大数字。写$fsdbDumpvars(2, tb_top)表示只 dump 两层看起来省空间实际调试时往往正好差那一层信号只能重跑。我的习惯是回归时用深度限制debug 时用0。3.3 覆盖率在仿真期的收口仿真期的覆盖率选项必须和编译期对齐字符串一模一样。常见的错误写法是编译期写了-cm linecondfsmtglbranch仿真期只写-cm line结果 fsm 和 tgl 的数据全丢。更隐蔽的是顺序问题有的版本对字符串顺序敏感所以最保险的做法是把这段字符串抽成一个变量编译和仿真都用它CM_OPTlinecondfsmtglbranch vcs ... -cm $CM_OPT -cm_dir ./cov/simv.vdb ./simv -cm $CM_OPT -cm_name $CASE -cm_dir ./cov/simv.vdb合并覆盖率用urg跑之前最好先urg -dir ./cov/simv.vdb -report ./cov/report看一遍确认各 case 的数据都进去了再合并。3.4 断言、UCLI 和交互式调试断言相关有两个常用开关-assert svaext启用 SVA 扩展语法-assert enable_diag让断言失败时打印更多信息。仿真期可以用-assert finish_maxfailN限制失败次数避免一个错断言刷屏几万行把日志撑爆。UCLI 是 VCS 的命令行调试接口用法是./simv -ucli -i ./script/run.do ntb_random_seed1run.do里可以写任意 Tcl 命令比如run 1us、dump -add tb_top.u_dut、scope、call。我最常用的组合是先跑一段让 DUT 进入工作状态再用dump -add只打开关心信号的波形这样能大幅减小 FSDB 体积。比起在initial里一开始就全量 dump这种方式在长仿真里能省掉几个 GB。4. VCS 与 Verdi 联合仿真的配置细节4.1 两种联动方式选错了会多编译一次VCS 和 Verdi 的联动本质上只有两种方式。第一种是编译期就把 Verdi 需要的知识库生成好也就是-kdb -lca -debug_accessall这种方式生成的simv自带 FSDB 能力运行时直接调$fsdbDumpvars就行速度也最快是现在的主流做法。第二种是老式的 PLI 方式在编译命令里显式指定 Verdi 的novas.tab和pli.avcs -sverilog -f ./flist/all.f \ -P ${VERDI_HOME}/share/PLI/VCS/LINUX64/novas.tab \ ${VERDI_HOME}/share/PLI/VCS/LINUX64/pli.a老方式的问题是每次升级 Verdi 版本都要重新编译路径写死还容易在换机器时挂掉。所以只要 VCS 版本不是太老我都建议用第一种方式具体就是三件套-kdb -lca -debug_accessall。有个细节值得说-fsdb这个简化选项在较新版本的 VCS 里可以直接用效果等价于上面那一串 PLI 路径写起来干净很多。但它对版本有要求团队里如果 VCS 版本不统一还是用-kdb最稳。4.2 FSDB 体积控制几个真正管用的手段一个跑了 10ms 的 SoC 仿真全量 dump 出来几十 GB 是常事。控制体积我有几个固定手法。第一是分阶段 dump。前面用 UCLI 跑一段不 dump到关键窗口再dump -add。第二是分层 dump顶层只 dump 接口信号内部模块按需展开。第三是用$fsdbAutoSwitchDumpfile自动分文件避免单个文件过大打不开initial begin $fsdbAutoSwitchDumpfile(500, ./wave/t1.fsdb, 20); $fsdbDumpvars(0, tb_top, all); end这行的意思是每个文件最大 500MB最多切 20 个文件。超过之后自动停止不会把磁盘写满把整个回归拖垮。第四是用fsdbautoflush让 FSDB 定期刷盘防止仿真中途崩溃导致波形全废——这个选项加了会稍微降速但换来的安全感很值。4.3 UCLI 脚本化调试实战UCLI 的价值在于把重复的调试动作脚本化。我常用的一个debug.do长这样# 打开波形只 dump 顶层和 DUT 核心 fsdbDumpfile ./wave/dbg.fsdb fsdbDumpvars 0 tb_top.u_dut.u_core run 200us # 跑到 200us 后检查某个状态 scope tb_top.u_dut.u_core.u_fsm show -value state run 10us quit把这段存成文件用./simv -ucli -i debug.do直接跑省去每次手动点 GUI。回归里出现某个 case 失败把这个脚本稍微改一下就能复现比在波形里手动找快得多。提示UCLI 的run命令是按仿真时间推进不是按墙钟时间所以脚本里可以用run 1ms这种大跨度命令不用担心它卡住。真正卡住的时候用 CtrlC 中断UCLI 会停住但仍然可用可以继续输入命令查状态。5. 常见报错与排查技巧实录5.1 编译期高频错误编译期的报错一般有编号比如Error-[MFNF]、Error-[SE]按编号搜比按文字搜准。我整理了几个最常见的报错关键字大致原因排查方向Module not found/ MFNF模块没进文件列表或-y路径不对检查-f列表和libextSyntax error语言模式没开比如 SV 代码没加-sverilog确认源文件是不是.svDuplicate declaration同名模块被编译两次检查文件列表有没有重复行Undefined system task $fsdbDumpvars没加-kdb或没带 PLI补-kdb -lcaRe-definition of macro宏被定义了两次用define统一在一处这里我想重点说Module not found。这个报错有时会指向一个看起来完全无关的模块原因是某个include文件里例化了它而你的incdir没包含那个目录。VCS 的报错只告诉你找不到不告诉你谁在找所以排查时可以从报错的模块名反查整个工程用grep -rn找出所有引用它的地方。5.2 仿真期高频问题仿真期的问题通常不报错只是行为不对更难查。我列几个踩过的。第一种是仿真跑得飞快、什么波形都没有。大概率是initial里的 dump 语句没被执行或者ifdef的宏没定义那段代码根本没编进去。检查方法是看编译日志里有没有 FSDB 相关 PLI 的初始化信息或者干脆在 dump 语句前加一句$display确认执行到。第二种是随机每次一样。前面说过就是没给ntb_random_seed。这个问题在 regression 里特别隐蔽因为每个 case 都通过了但实际覆盖的激励空间极小。第三种是仿真卡死不动。可能是组合逻辑环也可能是while循环没退出条件。先用 UCLI 的run 1us一小段一小段推在哪一段卡住就能定位到时间点再用波形反查那段时间哪些信号在震荡。第四种是覆盖率数据库为空。十有八九是编译期和仿真期的-cm字符串不一致或者-cm_dir路径两边指向了不同位置。我一般会在跑之前ls一下-cm_dir指向的目录确认上一轮的旧数据已经清掉避免新旧混在一起看不懂。5.3 一份我常翻的排查速查表现象第一检查项第二检查项编译报语法错误是否加了-sverilog文件后缀和-v2k是否冲突找不到模块文件列表-y与libextFSDB 无波形-kdb -lca$fsdbDumpvars是否被执行波形文件过大dump 深度是否全量 dump 顶层随机种子固定ntb_random_seed是否用了 automatic覆盖率不生成编译/仿真-cm一致-cm_dir路径后仿时序不报neg_tchk -negdelaySDF 是否反标成功仿真速度骤降调试等级覆盖率类目是否过多6. 工程组织与实操心得6.1 Makefile 分层组织别把所有选项堆成一行我见过最痛苦的 Makefile 是一行有八百个字符改一个选项要在里面用眼睛找半天。做了几年之后我现在固定用分层写法VCS vcs -full64 -sverilog -timescale1ns/1ps LANG_OPT incdir./tb incdir./rtl/include DBG_OPT -debug_accessall -kdb -lca CM_OPT linecondfsmtglbranch FAST_OPT -j 16 -Mdir ./csrc -o simv compile: $(VCS) $(LANG_OPT) -f ./flist/all.f \ $(DBG_OPT) -cm $(CM_OPT) -cm_dir ./cov/simv.vdb \ $(FAST_OPT) -l ./log/compile.log run: ./simv -l ./log/$(CASE).log \ ntb_random_seed$(SEED) \ -cm $(CM_OPT) -cm_name $(CASE) -cm_dir ./cov/simv.vdb这样改调试等级只动DBG_OPT一处改覆盖率类目只动CM_OPT一处编译和仿真自动保持一致。这个组织方式的收益在项目后期特别明显因为那时会有十几个人在同一个 Makefile 上改来改去。6.2 性能与调试的平衡分两套 simv 管理回归阶段和 debug 阶段的需求是矛盾的。回归要快、不要波形、覆盖率要全debug 要慢、要全量波形、要完整调试信息。硬用一套simv满足两边结果一定是两边都不满意。我的做法是维护两套编译目标。simv_reg用-debug_accesspp、不开波形、-j 16并行编译专门用来跑回归。simv_dbg用-debug_accessall -kdb -lca只编译一次平时不用只在需要看波形时跑。两者共用源码和文件列表互不干扰。实测下来回归用的simv比调试用的快 20% 到 30%一天下来能多跑很多 case。6.3 几个我踩过的坑说出来让你绕开第一个坑是define和-f的顺序。如果把define放在-f前面某些老版本 VCS 在处理文件列表时会丢掉这个定义导致ifdef分支不生效。正确做法是把所有define放在文件列表之后或者干脆放进一个专门的选项文件里。第二个坑是-y目录里的备份文件。有人习惯把xxx.v.bak留在目录里libext.v不会匹配.bak但如果备份文件名是xxx_old.v就可能被误匹配进去造成改了一个文件却编译了另一个的灵异现象。我现在的规矩是-y目录只放正式文件。第三个坑是覆盖率合并时的-cm_name重名。回归脚本里如果用测试平台名而不是 case 名作为-cm_name同一个平台下的多个 case 会互相覆盖最后合并出来只有最后一个的数据。一定要保证-cm_name全局唯一。第四个坑是 SDF 反标静默失败。前面提过VCS 默认只给 warning。我的做法是在编译脚本最后加一句grep -i sdf ./log/compile.log | wc -l把这个数字和预期对比数量对不上就说明有反标失败先解决再往下跑。6.4 关于选项的一个整体心态最后说点务实的。VCS 的选项有几万个没有任何人能全记住也没必要。真正高频使用的就那么三四十个把它们的为什么搞清楚剩下的遇到再查。我自己的习惯是维护一个私人的选项笔记每解决一个诡异的报错就记一条记到现在也就两百多条但覆盖了这些年九成以上的问题。工具是死的问题场景是活的把每次踩坑的过程沉淀下来比背手册有用得多。
返回列表