
做芯片验证这些年覆盖率分析一直是回邮件、写报告、找领导签字时绕不开的一道坎。很多时候我们花在仿真的时间并不少测试用例也跑了不少但一问到这版覆盖率多少哪些逻辑没被踩到就有点心虚。VCS作为数字IC前端验证用得最广的仿真器之一自带的覆盖率收集能力其实非常成熟配合Verdi和DVE这两套GUI基本覆盖了从跑完回归看数字到定位某个具体未覆盖分支的全流程。这篇东西就围绕我在实际项目里用VCS做覆盖率分析的完整操作从最基础的编译选项到后面的数据合并且把能落地的细节一次讲透给正在被覆盖率报告折磨的同行一个可以直接抄的作业。1. 覆盖率分析的整体思路1.1 为什么必须做覆盖率分析芯片验证做得到不到位不能光靠我觉得测了很多来证明。设计代码里可能有一百个分支你的用例可能只走到了六十个剩下四十个也许藏着bug也许只是冗余逻辑但你没跑到就是没跑到投片前这就是风险。代码覆盖率分析就是用来量化这件事的工具每一行代码有没有执行过、每一个条件分支有没有翻转、每一条状态机路径有没有跳进去全部用数字告诉你。我用VCS做覆盖率分析时最常见的做法是收集行覆盖率、条件覆盖率、分支覆盖率、状态机覆盖率、翻转覆盖率这几类。不同的覆盖率类型对应不同的验证盲区比如行覆盖率只能告诉你这一行代码被执行了但一个if(a b)这样的条件行覆盖率不会告诉你a为真b为假的情况有没有测过所以还得配合条件覆盖率来看。在Subsystem和SoC级别的验证环境里覆盖率报告往往是验证完备性的最重要依据手头没有一份成型的覆盖率报告项目评审的时候根本说不过去。1.2 VCS覆盖率工具链的架构VCS做覆盖率分析用的是内置的覆盖率引擎不需要额外安装单独的覆盖率工具但需要配合GUI或者命令行工具来查看和分析。整体链路是这样的编译的时候加覆盖率开关仿真的时候动态收集覆盖率数据数据落盘到指定目录最后再用Verdi或者DVE打开数据做可视化分析或者用urg命令生成报告。Verdi和DVE这两套GUI在我的实际使用中各有各的长处。Verdi是现在主流的选择启动快、界面现代化、代码窗口联动好用特别是FSM状态机覆盖率可以直接状态转移图上标红标绿非常适合排查状态机问题。DVE是老牌的覆盖率分析工具虽然界面看起来比较朴素但在大规模回归数据的汇总、层级结构遍历这些场景下依然非常稳定很多老项目里还是习惯了DVE的操作方式。两个工具读取的是同一套覆盖率数据库所以不存在谁的数据更准的问题只是操作习惯和使用场景的差别。提示VCS和Verdi是同一家公司工具链仿真时收集的覆盖率数据可以同时被Verdi和DVE读取不用做任何格式转换。这点在实际项目里省了不少事。2. 仿真前的参数准备覆盖率不是白来的2.1 编译阶段必须加的选项覆盖率收集的第一步是在编译阶段告诉VCS要收集哪些类型的覆盖率。我见过不少同事第一次接触覆盖率时跑完仿真发现simv.cm目录是空的或者是压根没有这个目录原因基本都是在编译和运行阶段漏了参数。编译阶段最核心的选项是-cm后面跟要收集的覆盖率种类。平时用得最多的是下面几种vcs -sverilog -full64 \ -cm linecondbranchtglfsm \ -cm_name test_top \ -cm_dir ./cov_dir/test_top.cm \ -f filelist.f \ -l vcs_compile.logline行覆盖率代码的每一行是否被执行cond条件覆盖率条件表达式里各种逻辑组合是否被覆盖branch分支覆盖率if/else、case等分支是否都走过了tgl翻转覆盖率信号是否发生了0到1、1到0的翻转fsm状态机覆盖率状态机状态是否都进入过、转移路径是否完整这里要注意-cm后面跟的覆盖率种类在编译阶段就要定好仿真阶段可以缩小范围但不能扩大范围。我自己的习惯是直接把linecondbranchtglfsm全开了虽然仿真时间会有一些额外开销但总比后面发现某类覆盖率没收集要重新跑回归强得多。仿真时间的开销实测下来大概是15%到30%左右的额外运行时间但换来的是覆盖率数据的完整性这个代价完全值得。-cm_name是给这组覆盖率数据起个名字。这个参数在merge多个覆盖目录时特别有用每一组用例的覆盖率数据源都能从名字上区分出来。-cm_dir指定覆盖率数据的落盘目录建议每个测试用例或者每个种子单独建一个目录方便后面做merge。2.2 仿真阶段的数据落盘编译通过之后运行仿真的时候还需要再加一次-cm参数告诉仿真器在这个用例的运行过程中要收集覆盖率数据./simv -cm linecondbranchtglfsm \ -cm_name test_case_001 \ -cm_dir ./cov_dir/test_case_001.cm \ -l simv_test_case_001.log仿真跑完以后VCS会在当前目录下生成一个simv.cm目录里面是本次运行收集到的覆盖率原始数据。如果指定了-cm_dir数据就会写到自定义路径下。这里有个坑simv.cm这个目录是增量累加的下一次仿真如果不清理新数据会和旧数据混在一起导致覆盖率明显虚高。我一般是写一个清理脚本在每次回归开始前把simv.cm目录删掉保证每次回归的数据都是干净的。有些场景下跑的是带SDF延迟的后仿真VCS同样支持覆盖率收集。但后仿真速度慢、时间开销大而且很多内部节点在门级网表里已经不叫原来的名字了覆盖率数据对应的源代码位置依然可以映射回RTL代码不过分析和定位的粒度会比前仿稍微粗糙一些。后仿覆盖率我建议重点看行覆盖率和翻转覆盖率条件覆盖率在后仿里的参考价值相对有限。2.3 一个容易被忽略的细节rst_dbVCS在收集覆盖率时默认会把顶层模块的名字记录在覆盖率数据里。如果编译时没有设置-cm_top有些情况下覆盖率报告里会出现奇怪的顶层名字特别是当验证环境的testbench顶层和DUT顶层不一致时。我通常用-cm_top显式指定DUT的顶层模块名这样在Verdi和DVE里看覆盖率层次结构的时候直接就能从DUT开始往下钻不用在一堆testbench代码的覆盖率上浪费时间。再看一个实际项目的Makefile片段这套配置我用了很多年稳定可靠COV_CMD -cm linecondbranchtglfsm COV_DIR $(COV_ROOT)/$(TC_NAME).cm compile: vcs -sverilog -full64 \ $(COV_CMD) \ -cm_top $(DUT_TOP) \ -cm_name $(TC_NAME) \ -cm_dir $(COV_DIR) \ -f $(FILELIST) \ -l compile.log run: ./simv \ $(COV_CMD) \ -cm_name $(TC_NAME) \ -cm_dir $(COV_DIR) \ -l $(TC_NAME).log这样每个测试用例跑完覆盖率数据都独立存在$(TC_NAME).cm目录下。后面无论是用Verdi单独看某个用例的盲点还是用urg把所有用例的覆盖率合并成一份总报告操作起来都很顺手。3. Verdi查看覆盖率实操3.1 启动Verdi并加载覆盖率数据Verdi加载覆盖率数据有两种典型方式。第一种是在启动时就指定覆盖率文件直接进入覆盖率分析界面verdi -cov -covdir ./cov_dir/test_case_001.cm -f filelist.f 第二种是先打开Verdi的源码界面再通过菜单Tools - Coverage Analysis加载覆盖率数据。我个人更推荐第一种一步到位。启动之后Verdi会打开覆盖率分析窗口左侧是覆盖率类型的页签包括Line、Condition、Branch、Toggle、FSM等右侧是覆盖率汇总面板显示每一个模块的覆盖率百分比。这里要注意Verdi打开覆盖率数据的时候需要源代码文件。如果编译时用的文件列表路径和当前路径不一致Verdi可能会找不到源文件需要在启动时通过-f filelist.f指定源代码文件列表。我自己就吃过这个亏从别人手里接手一个验证环境直接把覆盖率文件拷过来用Verdi打开结果源码路径全乱了所有覆盖率都定位不到代码行。后来养成了习惯覆盖率数据文件和源代码文件列表放在一起拷贝打开的时候两个一起指定。3.2 核心操作从覆盖盲区定位到源头逻辑Verdi覆盖率分析窗口里最有价值的功能是在源代码窗口里直接显示覆盖率彩色标注。点击任意一个模块源代码窗口会在每行代码前面用红绿黄色块标注覆盖率状态绿色表示这一行已经被执行覆盖红色表示从未执行过黄色表示部分覆盖比如条件分支只走了一半。这样定位未覆盖逻辑的速度非常快不用对着表格数字猜代码位置。比如你看某个模块条件覆盖率只有70%点击Condition页签源代码里出现红色标注的条件表达式鼠标移上去能看到具体是哪几种逻辑组合没有测到。如果是if (a b)这样的条件Verdi会告诉你覆盖到了a真b真、a真b假这些组合中的哪些还没覆盖到哪些。结合这个信息再回去看测试用例的激励约束往往很快就能发现是因为某个信号的取值范围受限导致特定逻辑分支根本进不去。FSM覆盖率这块是Verdi的强项。点开FSM页签Verdi会以图形化状态转移图的形式展示状态机的状态和跳转关系。状态用圆圈表示转移用箭头表示已经覆盖到的状态和转移是绿色没覆盖到的是红色。对于状态机这种跑没跑到某个状态一目了然的需求这个图比看代码有效率得多。我在实际项目中靠这个功能抓出过好几次时钟复位期间状态机被异常复位到某个非法状态却没被测试用例观察到的问题。3.3 Verdi的层级树和过滤技巧在Chip级别验证中整个设计的模块层次非常深覆盖率分析如果从顶层一个个点下去效率很低。Verdi的覆盖率窗口左侧有一个层次树默认按设计层次展开。我通常在层次树的过滤框里输入模块名关键字快速跳到关心的模块如果是子模块的所有实例都要看直接在过滤框里输入模块名加通配符能一次列出所有实例的覆盖率数据做横向对比。排障场景下我经常用到一个对比操作跑一组用例之前先记录覆盖率偏低的一组模块跑完回归后直接按覆盖率增量排序看看哪些模块在这次回归里覆盖率没有增长。这些模块就是测试盲区需要补充激励。Verdi覆盖率窗口支持按模块、按覆盖率类型的多维排序灵活用的话一份庞大的覆盖率报告很快就能提炼出接下来要在哪里努力的结论。4. DVE查看覆盖率实操4.1 DVE的启动方式和界面布局DVE作为传统的VCS配套GUI工具至今在很多老项目和客户的参考流程里依然占有一席之地。如果你所在的团队已经习惯用Verdi可以跳过这一章但如果你手上只有DVE环境下面的操作流程可以直接照用。DVE启动加载覆盖率数据的命令是dve -cov -covdir ./cov_dir/test_case_001.cm 启动后DVE会打开覆盖率分析主窗口。界面风格比较朴素但功能分区很清晰左侧是设计层次树右侧是覆盖率详情列表顶部是菜单和工具条。DVE的覆盖率汇总视图默认会以表格形式展示当前设计所有模块的行覆盖率、条件覆盖率、分支覆盖率、翻转覆盖率等按模块层级分组每一行都可以展开一直到具体的实例和信号。4.2 从汇总到深挖DVE的操作路径DVE看覆盖率最直接的操作是分级下钻。在层次树里点击一个模块右侧表格显示这个模块的覆盖率汇总双击某个指标DVE会自动跳到对应的源码窗口并用颜色标注未覆盖的代码行。这个联动效果和Verdi的体验是类似的只是视觉呈现更老派一些对于看惯了现代IDE界面的人来说会有个习惯过程但用顺手了以后一样能快速定位问题。DVE有一个比较实用的特性是Coverage Group View。这个视图把所有同一类型的数据比如所有模块的条件覆盖率聚合到一个表格里按百分比从低到高排序一眼就能看出哪些模块覆盖率拖了后腿。对于回归结束后要统计挂起风险模块的场景我习惯直接在Coverage Group View里按最低覆盖率排序把排名靠后的几个模块列入重点补充测试清单。4.3 Verdi和DVE如何选择在我工作过的几个项目里Verdi和DVE的选择往往取决于团队习惯和项目历史。新项目基本都切到了Verdi用户体验和功能都更占优但有一些存量IP验证环境和客户交付流程里DVE依然是标准配置。两个工具读取同一套VCS覆盖率数据库分析结果理论上是一致的所以选择哪个更多是看个人熟练度和团队标准没必要因为工具不同而纠结数据差异。真正要注意的是复现环境当你拿到一个别人的覆盖率数据打开之前先确认对方用的VCS版本和你要用的GUI版本是否差距过大。跨大版本打开覆盖率数据偶尔会出现兼容性警告但一般不影响查看。如果遇到数据完全打不开的情况优先考虑用urg命令把数据导出成文本或网页报告这招在应急场景下很管用。5. merge技巧多场景覆盖率数据这样合并不踩坑5.1 为什么需要merge何时需要merge覆盖率分析的最终目标是评估整个验证计划下设计代码被覆盖的完整程度。单个测试用例跑出来的覆盖率再高也只是局部视角真正要交付的是一份所有用例合并之后的总体覆盖率报告。Merge就是把多个独立的覆盖率数据库合并成一份综合数据库反映的是所有测试用例叠加之后的总覆盖率。什么时候必须merge最典型的是约束随机验证同一个测试用例用不同的随机种子跑几十次每次跑出来的覆盖组合都不同最终要合并成一份数据看总覆盖率。跨功能场景的合并也一样比如一个模块的复位测试、正常功能测试、异常路径测试分别由不同用例覆盖回归跑完后必须合起来看总体的功能覆盖率盲区。SoC级验证里还有IP级和系统级的合并需求IP验证的覆盖率数据要汇到SoC级验证报告里也需要通过merge来完成。5.2 urg命令实战VCS提供的merge工具就是urg命令全称是Unified Report Generator。它既可以用来生成可读的报告也可以用来合并覆盖率数据并输出一个新的合并后的数据库。基础用法很简单urg -dir ./cov_dir/test_case_001.cm \ -dir ./cov_dir/test_case_002.cm \ -dir ./cov_dir/test_case_003.cm \ -dbname merged.cm \ -report merged_report执行后urg会把这三个目录的覆盖率数据合并合并结果写入merged.cm目录同时生成一份文本报告在merged_report目录里。之后用Verdi或DVE打开merged.cm就能看到合并后的总覆盖率。当用例数量很多的时候命令行一个个写-dir参数不现实。urg支持用-f参数指定一个文件列表文件里每行写一个覆盖率数据目录urg -f cov_dir_list.txt -dbname merged.cm -report merged_report这里cov_dir_list.txt的内容格式就是每一行一个目录路径。我在实际回归环境里都是自动生成这个文件回归结束后脚本扫描整个回归目录把所有.cm目录路径写进列表文件然后调用urg做合并。整个流程完全不需要人工介入覆盖率总报告在回归结束后的几分钟内就能自动生成。5.3 merge时的关键注意事项Merge看起来简单实际操作中有几个关键细节踩过坑之后我才特别注意。第一个是时间戳问题。VCS编译生成的仿真可执行文件如果每次编译时间不同生成的覆盖率数据里会带有编译时间戳信息。合并不同时间编译产生的覆盖率数据时urg有时候会报警告或者直接跳过不兼容的数据。解决办法是在做多轮回归比较时保持同一个编译版本不要中间重新编译。如果确实需要重新编译那编译完成后所有用例都要重新跑一遍回归否则新旧数据不能混着merge。第二个是覆盖率数据目录的命名和管理。我见过有人把所有用例的覆盖率都指定到同一个-cm_dir结果每个用例跑完都把之前的数据累加进去了最终的merge完全失去了意义。正确做法是每一个用例或者每一组随机种子使用独立的目录在Makefile或者回归脚本里用变量区分这样可以保证merge的每一份数据都是单一用例的原始结果。第三个是针对随机验证场景的merge策略。约束随机验证跑多个种子时有时候你关心的不是所有种子合并后的总覆盖率而是单一种子能达到的最高覆盖率以及几个种子分别的差异覆盖。这种情况下我会分两层merge第一层把同一个测试用例的所有种子merge成一份用例级覆盖率第二层把所有用例级覆盖率再merge成回归总覆盖率。这样做的好处是既能看整体达标情况又能定位某个测试用例覆盖贡献的基数避免出现总覆盖率达标了但其实某个用例对总覆盖率完全没贡献的水分数据。下面是一个帮助理解merge组合方式的表格merge维度输入数据输出用途典型场景种子级merge同一用例不同随机种子反映该用例稳定的覆盖率能力约束随机验证统计单一用例覆盖贡献用例级merge同一回归内所有用例反映本次回归的总覆盖率项目节点回归交付前的覆盖率评估跨回归merge多轮回归的覆盖率数据反映多个回归叠加后的覆盖率长时间验证周期多批次测试数据汇总5.4 merge后的报告解析urg生成的报告里有几个关键指标要会看。Line Coverage和Condition Coverage是最常被领导问到的两个数字一个是代码行执行覆盖一个是条件逻辑组合覆盖。Branch Coverage和Toggle Coverage用于评估控制流和信号翻转的完备性。FSM Coverage在状态机比较多的设计里是不可或缺的如果这个数字偏低意味着状态机还有很多状态跳转没有被实际激励到这种情况往往需要针对状态机增加定向测试用例。报告里的Total Coverage是各类覆盖率的加权综合不同的覆盖率类型权重默认相同也可以自己定义权重。但我想强调的是Total Coverage这个数字只能作为宏观参考真正找问题的时候还是要逐个模块逐个覆盖率类型去钻不要只盯着一个综合数字看。6. 常见问题与排查技巧实录6.1 仿真结束却没有覆盖率数据这是我被问过最多的问题命令行加了-cm参数仿真也正常跑完退出了但是目录下没有simv.cm或者有目录但里面是空的。排查思路很简单先看编译时候有没有加-cm再确认仿真的可执行文件是不是最新编译的。如果编译时没加-cm参数仿真时即使加了也不会生成覆盖率数据。还有一种不太容易发现的情况仿真在某个用例里面通过系统任务$finish或者$fatal结束覆盖率数据可能没有及时flush。正常跑完的仿真会在结束阶段自动把覆盖率数据写盘但如果是被外部机制强行杀掉的进程最后的覆盖率数据往往会丢失。解决办法有两个一是尽量让用例正常退出二是在代码或者命令行层面设置周期性的覆盖率数据保存。VCS提供-cm_log参数来记录覆盖率数据写盘日志出现了数据不完整的情况可以看日志定位是哪个阶段出了问题。6.2 Merge时的时间和版本错配不同编译版本甚至不同VCS版本生成的覆盖率数据在merge时可能不兼容。urg会报类似timestamp mismatch或者version mismatch的警告。遇到这个问题不要硬merge先检查哪些目录是同一个编译版本生成的把能合并的先合并不能合并的单独保留并在报告中注明原因这是最稳妥的处理方式。6.3 仿真后处理Memory初始化对验证环境而言覆盖率分析和memory初始化是两个相对独立的话题但做后仿真时经常要一起处理。跑带时序的后仿真通常需要做memory初始化避免仿真开始时的X态传播导致覆盖率收集异常。VCS处理memory初始化的常见做法是使用$readmemh或者$readmemb系统任务在后仿测试代码里把memory内容加载到指定数组里。如果memory是综合后的RAM模型初值可能要通过force或者deposit的方式初始化。我的经验是初始化步骤越早做越好否则仿真开始阶段就会有大量的X态出现在覆盖率结果里影响行覆盖率和条件覆盖率的准确度。有些人会用编译选项-xprop来处理X态传播问题但针对覆盖率分析我建议重点关注翻转覆盖率是否因为X态导致大量误报。翻转覆盖率的原理是记录信号从0到1、从1到0的变化如果信号一开始就是X那翻转计数往往会被跳过这类信号在覆盖率报告里会长期是未覆盖状态。排查这类问题时先确认仿真开始时信号有没有被正确初始化再决定要不要关注这些翻转覆盖率数据。6.4 后仿Memory初始化的实操技巧后仿阶段要让memory初始化常见的手段是把RTL里的memory替换成带初始化能力的仿真模型或者在测试代码里直接对memory区域做deposit。实际项目里我比较常用的是VCS的-cm结合defineMEM_INIT条件编译的方式在RTL或者仿真模型里预留初始化分支仿真时通过宏定义打开。这个做法的好处是可以灵活控制哪些仿真阶段做初始化、哪些阶段做真正的功能仿真不至于把初始化逻辑带到正式仿真里影响事务时序。在system verilog的interface或者module里加一个初始化块用#0的方式在0时刻deposit所有memory单元对几百KB量级的memory仿真启动的额外开销很小但能有效避免后仿初期出现海量X态导致的覆盖率失真。初始化的数据是否可以复用$readmemh加载的hex文件取决于你手头有没有对应的初始化文件没有的话全写0也是可以的主要目的是让信号脱离X态。6.5 Verdi和DVE都打不开覆盖率数据这种情况下我习惯先跑一下urg试试因为urg的兼容性要比GUI工具更强一些如果urg能正常生成报告那数据本身没有坏问题出在GUI工具的版本或者环境上。urg跑出来如果是空的那说明数据文件确实有问题这时候检查simv.cm目录下面的具体文件看simv.cm目录里有没有完整的header数据和具体的覆盖率数据文件。覆盖率数据文件一般比较大如果发现某些文件是0字节大概率是仿真被中途强杀了需要重新跑相应用例。6.6 覆盖率一直卡在某个百分比上不去这种情况在项目后期非常常见。就是因为该覆盖的逻辑在现有测试架构下很难被激励到不是你用例数量不够而是激励约束限制了输入的多样性。我排查这种覆盖率平台期的标准做法是用Verdi打开当前覆盖率数据按模块排序找到覆盖率最低的几个模块逐个点进源码看红色标注的逻辑分析这些逻辑在什么输入条件下才可能执行然后检查现有用例的约束是否屏蔽了这些输入条件。大多数情况下是约束过死解约束之后重新跑回归就有明显改善。这里放一个我常用的排查路径表帮助快速定位不同覆盖率类型偏低的原因现象可能原因优先排查项行覆盖率偏低大量代码分支未被激励到检查测试用例的输入约束是否限制了合法输入的取值范围条件覆盖率偏低if/else条件组合未覆盖全查看Verdi标注的具体条件组合缺口翻转覆盖率偏低信号长期保持恒定值检查复位逻辑和初始化逻辑确认信号是否从未被驱动FSM覆盖率偏低状态机某些状态或转移未进用Verdi的FSM视图定位红色状态设计对应场景激励总覆盖率卡住不涨激励空间受限或测试目标固化解约束、新增边界场景、考虑加入定向用例写在最后的实操心得覆盖率分析做到最后你会发现真正值钱的不只是会敲那几条命令而是拿到一份覆盖率报告后知道下一步该干什么。VCS的覆盖率收集、Verdi的图形化定位、DVE的老牌稳定、urg的高效合并这四样东西组合起来已经能覆盖绝大多数验证项目的覆盖率交付需求。我个人在实际项目里最受益的一个习惯是写一套自动化的覆盖率回归脚本编译、跑用例、按用例收集独立覆盖率目录、自动merge、自动生成报告、自动邮件通知结果。整个流程里我只需要在第二天早上看报告然后根据覆盖率盲区去补充和调整用例。希望这篇东西能帮你把覆盖率分析的整个链路理顺少踩几个我当年踩过的坑。