
1. 覆盖率排查这件事为什么值得单独拿出来讲做数字IC验证的人都有一个共同的痛点仿真跑完了覆盖率报告一打开满屏的红色和黄色Code Coverage卡在85%上不去Toggle Coverage更是惨不忍睹Function Coverage看着还行但点进去一看全是边角case没打到。更让人头疼的是你明明知道有些TCTest Case跑了跟没跑一样但就是找不到哪个TC该为哪块未覆盖区域负责。VCS的覆盖率系统其实非常成熟URGUnified Report Generator能把code coverage、functional coverage、toggle coverage全部汇总成一份可交互的HTML报告。但问题在于很多人只会看总览页面那个百分比不会用urg的进阶功能去定位问题更不会用-report选项去生成针对性的分析报告。结果就是反复跑回归、反复看同样的报告效率极低。这篇文章要解决的核心问题就三个第一怎么快速定位到某个未覆盖点应该由哪个TC来负责第二Toggle Coverage收集效率低、数据量大的问题怎么优化第三在实际项目中怎么建立一套可复用的覆盖率排查流程。适合已经有一定VCS使用经验、正在被覆盖率收敛折磨的验证工程师也适合刚接触覆盖率分析、想少走弯路的同学。我做过几个大规模SoC项目的覆盖率收敛从最初的手忙脚乱到后来形成一套固定流程中间踩过的坑足够写一本小册子。下面把这些经验拆开来讲尽量做到你看完就能直接用到项目里。2. 覆盖率数据收集与URG报告生成的核心配置2.1 编译和仿真阶段的覆盖率选项怎么选很多人覆盖率数据不准问题出在最开始的编译阶段。VCS的覆盖率收集需要在compile和simulation两个阶段都加选项而且选项之间有关联不是随便加几个就行。编译阶段的核心选项vcs -full64 -sverilog -debug_accessall \ -cm linecondfsmtglbranch \ -cm_dir ./cov_compile \ -cm_hier ./cov_hier.cfg \ -f filelist.f -o simv这里-cm指定要收集的覆盖率类型。line是行覆盖率cond是条件覆盖率fsm是状态机覆盖率tgl是toggle覆盖率branch是分支覆盖率。实际项目中我建议至少开linecondfsmtglbranch少一个都可能在后期收敛时发现盲区。-cm_dir指定编译阶段覆盖率数据的存放目录。这个目录在仿真阶段还会用到所以路径要记清楚。-cm_hier是一个很多人忽略的选项它允许你通过配置文件精确控制哪些模块收集覆盖率、哪些不收集。这个后面会详细讲。仿真阶段的选项./simv -cm linecondfsmtglbranch \ -cm_dir ./cov_sim \ -cm_name tc_smoke_test \ -cm_log ./cov.log-cm_name非常关键它决定了这次仿真产生的覆盖率数据文件名。如果你跑多个TC每个TC的-cm_name必须不同否则后一次会覆盖前一次的数据。我见过有团队因为-cm_name没改跑了一整轮回归结果只保留了最后一个TC的覆盖率白白浪费了几天时间。注意-cm_dir在编译和仿真阶段可以指向同一个目录也可以不同。如果不同后续用urg合并时需要把两个目录都加进去。建议统一用一个目录减少合并时的麻烦。2.2 URG报告生成从原始数据到可读报告仿真跑完后每个TC会生成一个simv.vdb目录或者你指定的名字。这些原始数据不能直接看需要用urg转换成HTML报告。最基本的urg命令urg -dir ./cov_sim/simv.vdb \ -dir ./cov_sim/tc_smoke_test.vdb \ -report ./urg_report \ -format both-dir可以跟多个把不同TC的vdb目录都加进去urg会自动合并。-report指定输出目录-format both表示同时生成HTML和文本格式。文本格式在写脚本自动化分析时特别有用。但这里有个坑如果你有几十个TC每个都写一个-dir命令行会长到没法维护。正确的做法是用-dirfile# 先生成dirfile ls -d ./cov_sim/*.vdb ./vdb_list.f # 再用dirfile生成报告 urg -dirfile ./vdb_list.f \ -report ./urg_report \ -format both \ -show tests-show tests这个选项值得单独说。加上它之后URG报告里每个未覆盖的bin都会列出哪些TC覆盖了它、哪些没覆盖。这是定位哪个TC该负责哪块覆盖率的核心功能。没有这个选项你只能看到这里没覆盖看不到谁应该覆盖但没覆盖。2.3 用-cm_hier精确控制收集范围大型SoC项目里不是所有模块都需要收集覆盖率。比如第三方IP、已经验证过的模拟模块、纯寄存器配置模块收集它们的覆盖率只会让报告变得臃肿拖慢urg的生成速度。-cm_hier配置文件的基本语法tree tb_top.dut.cpu_core 0 tree tb_top.dut.dma_engine 1 tree tb_top.dut.analog_wrap 0 moduletree tb_top.dut.periph_ss 1tree后面跟模块层次路径最后的数字0表示不收集1表示收集。moduletree和tree的区别在于moduletree会递归包含子模块tree只针对指定层次。我通常的做法是先全量收集跑一轮看看哪些模块覆盖率已经100%且不再变化然后在-cm_hier里把它们关掉。这样后续回归的覆盖率数据量能减少30%到50%urg生成时间从十几分钟降到几分钟。实操心得-cm_hier的配置文件在编译阶段生效修改后需要重新编译。所以建议在项目初期就规划好不要等到覆盖率收敛阶段才想起来关模块那时候重新编译的成本很高。3. 未覆盖TC的高效排查方法论3.1 从URG报告反向定位责任TCURG生成的HTML报告里Tests页面会列出所有TC以及它们各自的覆盖率贡献。但真正有用的是Coverage页面下的Line、Condition、Toggle等子页面每个未覆盖点旁边会有一个Tests链接点进去能看到这个点被哪些TC覆盖了。但手动点HTML效率太低。我的做法是直接用urg的文本报告加grep# 生成文本格式的详细报告 urg -dirfile ./vdb_list.f \ -report ./urg_text \ -format text \ -show tests # 查找所有未覆盖的行 grep -n 0.00% ./urg_text/*.txt | grep -v 100.00%文本报告里每个模块的覆盖率数据是分文件存放的格式比较规整。你可以写个Python脚本解析这些文本自动提取出未覆盖点和相关TC的对应关系。我常用的一个Python脚本片段import re import os def parse_urg_text(report_dir): uncovered [] for root, dirs, files in os.walk(report_dir): for f in files: if f.endswith(.txt) and line in f.lower(): filepath os.path.join(root, f) with open(filepath, r) as fh: content fh.read() # 匹配未覆盖行 pattern r(\S)\s(\d)\s(\d)\s0\.00% matches re.findall(pattern, content) for m in matches: uncovered.append({ module: m[0], line: m[1], total: m[2] }) return uncovered这个脚本能快速把未覆盖的行提取出来然后你可以根据模块名去查对应的TC。3.2 用TC-Toggle矩阵快速定位盲区Toggle Coverage的特殊之处在于它跟TC的对应关系不像Line Coverage那么直接。一个TC可能覆盖了某个信号的0到1跳变另一个TC覆盖了1到0跳变你需要两个TC都跑才能达到100%。我习惯做一个TC-Toggle矩阵行是TC列是关键信号单元格填该TC是否覆盖了该信号的跳变。这个矩阵可以用urg的-show tests报告生成也可以用VCS的-cm_tgl相关选项在仿真时直接输出。生成矩阵的urg命令urg -dirfile ./vdb_list.f \ -report ./urg_tgl \ -format text \ -show tests \ -metric toggle然后在文本报告里搜索Toggle Coverage部分每个信号的0-1和1-0会分别列出覆盖它的TC。如果某个跳变没有任何TC覆盖那就是盲区需要新增TC或者修改现有TC的激励。避坑指南Toggle Coverage的收集会显著增加仿真时的内存开销和vdb文件大小。一个中等规模的SoC开Toggle Coverage后vdb文件可能从几百MB涨到几个GB。建议在项目初期就规划好Toggle收集范围用-cm_hier把不需要关注的信号关掉。3.3 用URG的-source选项定位RTL源码URG报告默认只显示模块和行号不显示具体的RTL代码。排查未覆盖问题时你经常需要看那行代码到底是什么逻辑。-source选项可以解决这个问题urg -dirfile ./vdb_list.f \ -report ./urg_src \ -format both \ -source \ -show tests加上-source后HTML报告里每个未覆盖的行会直接显示对应的RTL代码片段。这样你不需要在编辑器和报告之间来回切换效率提升非常明显。但-source有个前提编译时必须有-debug_accessall或者至少-debug_accessline否则URG找不到源码信息。另外如果RTL文件在编译后被移动或修改过-source也会失效。所以建议在覆盖率收敛阶段冻结RTL不要一边改代码一边看覆盖率报告。4. Toggle Coverage收集优化实战4.1 Toggle收集的性能瓶颈在哪里Toggle Coverage是覆盖率类型里最重的一种。它需要为每个被监控的信号维护两个状态位0-1和1-0在仿真过程中每次信号跳变都要更新这些状态位。一个大规模SoC可能有几十万个信号每个信号每个时钟周期都可能跳变累积起来的数据量非常恐怖。我实测过一个数据某项目DUT有约15万个信号开全量Toggle Coverage后仿真速度下降约40%vdb文件从200MB涨到3.5GB。urg生成报告的时间从2分钟涨到15分钟。这还只是一个TC的数据如果是几十个TC的回归存储和生成时间都是灾难。所以Toggle Coverage的优化核心就两个方向减少监控的信号数量减少需要合并的vdb文件数量。4.2 用-cm_tgl和-cm_hier精准控制Toggle范围VCS提供了-cm_tgl选项来细化Toggle收集行为# 只收集指定层次的toggle vcs -cm tgl -cm_tgl portsonly ... # 只收集模块端口不收集内部信号 vcs -cm tgl -cm_tgl mda ... # 收集多位信号的每一位跳变 vcs -cm tgl -cm_tgl full ...portsonly是最常用的优化选项。大多数情况下模块内部信号的Toggle Coverage意义不大真正需要关注的是模块端口和关键寄存器。用portsonly可以把Toggle数据量减少60%以上。更精细的控制还是靠-cm_hiertree tb_top.dut.cpu_core 1 tree tb_top.dut.cpu_core.alu 0 tree tb_top.dut.dma_engine 1 tree tb_top.dut.dma_engine.fifo 0这个配置表示cpu_core和dma_engine收集Toggle但它们的子模块alu和fifo不收集。这种粒度在实际项目中非常实用因为有些子模块的信号跳变频繁但验证价值低。实操心得我通常会把时钟树、复位树、电源管理相关的信号全部关掉Toggle收集。这些信号要么一直在跳时钟要么几乎不跳复位收集它们对验证收敛没有帮助只会增加数据量。4.3 多TC的Toggle数据合并策略当你跑了几十个TC后每个TC的vdb里都有Toggle数据。urg合并时会把所有TC的Toggle数据取并集这本身没问题但合并过程很慢。优化策略一只合并需要的TC。不是所有TC都对Toggle Coverage有贡献。你可以先用-show tests生成一份报告看看哪些TC对Toggle覆盖率的贡献最大然后只合并这些TC的vdb。优化策略二分阶段合并。先按模块分组合并再把模块级的合并结果做最终合并。比如# 第一阶段按子系统合并 urg -dirfile ./vdb_cpu.f -report ./urg_cpu -metric toggle urg -dirfile ./vdb_dma.f -report ./urg_dma -metric toggle # 第二阶段合并子系统报告 urg -dir ./urg_cpu -dir ./urg_dma -report ./urg_final -metric toggle这种分阶段合并的方式在TC数量超过50个时效果非常明显。因为urg的合并复杂度大致是O(n log n)分阶段可以把大问题拆成小问题。优化策略三用-cm_tgl的-cm_tgl_report选项在仿真阶段就生成Toggle摘要而不是等urg阶段再处理。这个选项会在仿真结束时直接输出一个Toggle覆盖率摘要文件你可以快速判断这个TC是否值得保留vdb。4.4 Toggle Coverage的常见误报与过滤Toggle Coverage报告里经常出现一些看起来没覆盖但实际不需要覆盖的信号。比如常量信号RTL里被tie到0或1的信号永远不会跳变复位信号只在复位时跳变一次之后保持不变时钟信号一直在跳但Toggle Coverage会显示100%未使用的信号综合时会被优化掉但仿真时仍然存在这些信号如果出现在未覆盖列表里会干扰你的判断。VCS提供了-cm_tgl_filter选项来过滤这些信号vcs -cm tgl -cm_tgl_filter ./tgl_filter.cfg ...tgl_filter.cfg的内容示例filter tb_top.dut.clk filter tb_top.dut.rst_n filter tb_top.dut.test_mode filter tb_top.dut.scan_en把时钟、复位、测试模式等信号过滤掉后Toggle报告会干净很多真正需要关注的未覆盖信号一目了然。5. 常见问题排查与避坑指南5.1 URG报告生成失败或数据缺失问题现象urg命令执行后报错Error: Cannot find vdb directory或者报告里某些模块的覆盖率显示为N/A。排查思路首先检查vdb目录是否存在。仿真时如果-cm_dir指定的路径不对或者仿真异常退出vdb目录可能没有生成。用ls -la确认目录存在且非空。其次检查编译和仿真阶段的-cm选项是否一致。如果编译时开了tgl但仿真时没开Toggle数据就不会被收集。两边必须完全一致。最后检查-cm_hier配置。如果某个模块在编译时被-cm_hier关掉了仿真时即使跑了也不会产生覆盖率数据。这种情况在报告里会显示为N/A。避坑技巧建议在Makefile里把编译和仿真的覆盖率选项定义成同一个变量避免手动输入时不一致。COV_OPTS -cm linecondfsmtglbranch compile: vcs $(COV_OPTS) -cm_dir ./cov ... sim: ./simv $(COV_OPTS) -cm_dir ./cov ...5.2 Toggle覆盖率长期卡在某个值上不去问题现象Toggle Coverage跑到95%就上不去了剩下的5%怎么跑新TC都不动。排查思路先确认这5%是哪些信号。用urg的-show tests报告找到未覆盖的Toggle信号列表。然后逐个分析如果是常量信号用-cm_tgl_filter过滤掉如果是复位信号确认是否需要在TC里显式触发复位如果是跨时钟域信号确认是否有时钟域交叉导致的采样问题如果是未使用的信号确认RTL里是否真的没用到我遇到过最隐蔽的一种情况某个信号的Toggle Coverage一直上不去查了半天发现是RTL里有个ifdef条件编译仿真时该条件不成立信号被tie住了。这种问题只能看RTL源码才能发现。避坑技巧对于长期不跳变的信号可以在testbench里加一个monitor在仿真结束时打印出该信号的值和跳变次数。如果跳变次数为0基本可以确定是常量或未使用信号。5.3 多TC覆盖率合并后数据不一致问题现象单独跑TC1和TC2覆盖率分别是80%和75%合并后应该是95%左右但urg报告显示只有85%。排查思路这种情况通常是-cm_name冲突导致的。如果两个TC的-cm_name相同后跑的TC会覆盖前一个的vdb数据。检查仿真日志里的-cm_name参数确保每个TC都不同。另一种可能是-cm_dir路径冲突。如果两个TC的-cm_dir指向同一个目录且-cm_name也相同数据覆盖是必然的。还有一种不太常见但很坑的情况urg合并时-dir的顺序会影响结果。虽然理论上取并集应该与顺序无关但某些VCS版本的urg实现有bug顺序不同结果不同。建议用-dirfile并保持文件列表顺序稳定。避坑技巧在回归脚本里强制每个TC的-cm_name包含TC名和随机种子比如-cm_name ${tc_name}_${seed}。这样绝对不会冲突。5.4 URG报告加载慢或浏览器卡死问题现象urg生成的HTML报告有几百MB浏览器打开后卡死或者滚动时严重卡顿。排查思路报告太大通常是因为收集了太多不需要的覆盖率数据。用-cm_hier关掉不需要的模块用-cm_tgl_filter过滤掉不需要的Toggle信号。另外urg的-format text生成的文本报告比HTML小很多而且可以用grep快速搜索。在排查阶段建议先用文本报告定位问题最后再生成HTML报告给团队看。避坑技巧urg支持-report选项指定只生成特定类型的报告。比如-report line只生成行覆盖率报告-report toggle只生成Toggle报告。分类型生成可以显著减小单个报告的大小。5.5 常见问题速查表问题现象可能原因解决方法vdb目录不存在仿真异常退出或-cm_dir路径错误检查仿真日志确认-cm_dir路径覆盖率显示N/A编译和仿真-cm选项不一致统一编译和仿真的-cm选项Toggle覆盖率上不去常量信号或未使用信号干扰用-cm_tgl_filter过滤合并后覆盖率偏低-cm_name冲突导致数据覆盖每个TC用唯一的-cm_nameHTML报告卡死报告太大用-cm_hier减少收集范围-source选项无效编译时未加-debug_access编译时加-debug_accessallurg合并速度慢TC数量太多分阶段合并或只合并关键TC6. 建立可复用的覆盖率收敛流程6.1 从回归到报告的自动化流水线覆盖率收敛不是一次性的工作而是一个持续迭代的过程。我建议把整个流程自动化减少手动操作。一个典型的自动化流程#!/bin/bash # run_cov_regression.sh # 1. 编译 vcs -full64 -sverilog -debug_accessall \ -cm linecondfsmtglbranch \ -cm_dir ./cov_compile \ -cm_hier ./cov_hier.cfg \ -f filelist.f -o simv # 2. 跑所有TC for tc in $(cat tc_list.txt); do ./simv -cm linecondfsmtglbranch \ -cm_dir ./cov_sim \ -cm_name ${tc} \ -cm_log ./logs/${tc}.log \ tc${tc} done wait # 3. 生成vdb列表 ls -d ./cov_sim/*.vdb ./vdb_list.f # 4. 生成URG报告 urg -dirfile ./vdb_list.f \ -report ./urg_report \ -format both \ -show tests \ -source # 5. 提取未覆盖点 python3 parse_uncovered.py ./urg_report ./uncovered.txt这个脚本可以每天定时跑生成最新的覆盖率报告和未覆盖点列表。团队只需要关注uncovered.txt里的新增项即可。6.2 覆盖率收敛的优先级排序不是所有未覆盖点都值得花时间。我通常按以下优先级排序第一优先级功能相关的未覆盖点。这些点直接影响芯片功能正确性必须覆盖。第二优先级边界条件和异常路径。这些点虽然不常触发但一旦出问题就是大问题。第三优先级Toggle Coverage的未覆盖信号。这些信号通常不影响功能但可能影响功耗和时序。第四优先级已经验证过的模块的覆盖率。如果某个模块在之前的项目里已经验证充分本项目的覆盖率要求可以适当放宽。实操心得我习惯在项目初期就跟设计团队和系统团队对齐覆盖率目标。哪些模块必须100%哪些模块80%即可哪些模块不要求。有了明确目标收敛过程会顺畅很多不会出现为了最后1%的覆盖率反复跑一周回归的情况。6.3 覆盖率数据的版本管理覆盖率数据是需要版本管理的。每次RTL变更后之前的覆盖率数据可能失效。我建议每次RTL冻结后打一个覆盖率基线标签每次回归后把vdb文件和urg报告归档到对应版本目录用Git LFS或者专门的存储管理大文件目录结构示例cov_archive/ ├── v1.0_r1/ │ ├── vdb/ │ ├── urg_report/ │ └── uncovered.txt ├── v1.0_r2/ │ ├── vdb/ │ ├── urg_report/ │ └── uncovered.txt └── v1.1_r1/ ├── vdb/ ├── urg_report/ └── uncovered.txt这样当覆盖率出现回退时可以快速对比两个版本的差异定位是哪次RTL变更导致的。6.4 团队协作中的覆盖率分工大规模项目的覆盖率收敛需要团队协作。我的做法是把覆盖率目标按模块拆分每个验证工程师负责自己模块的覆盖率收敛。具体分工方式每个工程师负责一个或多个模块的覆盖率每天生成一次全量覆盖率报告每个工程师从报告里提取自己模块的未覆盖点每周开一次覆盖率评审会同步进展和阻塞问题这种分工方式的关键是URG报告要支持按模块过滤。-cm_hier配置文件可以按模块生成不同的报告每个工程师只看自己模块的报告减少信息干扰。# 为每个模块生成独立报告 urg -dirfile ./vdb_list.f -report ./urg_cpu -moduletree tb_top.dut.cpu_core urg -dirfile ./vdb_list.f -report ./urg_dma -moduletree tb_top.dut.dma_engine urg -dirfile ./vdb_list.f -report ./urg_periph -moduletree tb_top.dut.periph_ss这样每个工程师只需要关注自己的报告效率更高。7. 一些零散但实用的经验关于VCS覆盖率还有一些零散的经验值得分享。第一-cm_dir的路径不要用相对路径。相对路径在不同工作目录下执行仿真时会指向不同位置导致vdb文件散落各处。统一用绝对路径或者基于项目根目录的固定相对路径。第二urg的-show tests选项在TC数量多时会让报告变大很多。如果只是看总体覆盖率可以不加这个选项。只有在需要定位责任TC时才加。第三Toggle Coverage的0-1和1-0是分开统计的。有时候你看到某个信号显示50%不是因为它没跳变而是只跳了一个方向。这种情况需要检查TC的激励是否只覆盖了单向跳变。第四VCS的覆盖率数据库vdb是二进制格式不同版本的VCS可能不兼容。如果团队里有人用不同版本的VCSurg合并时可能报错。建议统一VCS版本或者在urg命令里加-full64确保64位兼容。第五-cm_hier配置文件里的模块路径必须与RTL层次完全一致。如果RTL例化时改了名字配置文件也要同步更新。我习惯在RTL里给关键模块加(* keep_hierarchy yes *)属性防止综合工具优化掉层次结构。第六urg报告里的Condition Coverage经常出现部分覆盖的情况。比如一个if (a b)TC只覆盖了a1,b1和a0,b0没有覆盖a1,b0和a0,b1。这种部分覆盖在报告里会显示为黄色需要专门设计TC来覆盖所有条件组合。第七如果项目里用了UVM可以在UVM的report phase里直接调用$coverage_control和$coverage_save系统函数在仿真结束时自动保存覆盖率数据。这样不需要依赖命令行选项减少出错概率。// 在UVM report phase里保存覆盖率 function void report_phase(uvm_phase phase); $coverage_control(COVERAGE_SAVE, linecondfsmtglbranch); endfunction这些经验都是我在实际项目里踩过坑之后总结出来的有些看起来是小问题但在大规模回归时会被放大成严重的时间浪费。希望对你有所帮助。