3. 覆盖率收集:代码覆盖率与功能覆盖率开关、覆盖率数据库合并、排除文件设置

覆盖率这东西,说白了就是检验你仿真到底跑没跑透。我见过不少团队,仿真跑得轰轰烈烈,结果一查覆盖率,关键分支根本没走到。嗯,今天咱们就把覆盖率收集的坑一个个填平。

3.1 代码覆盖率 vs 功能覆盖率:到底该开哪个?

很多新手一上来就把所有覆盖率开关全打开,觉得越多越安全。其实不是这样。代码覆盖率和功能覆盖率,完全是两码事。

类型 衡量对象 自动收集? 典型坑点
代码覆盖率 RTL代码的行、分支、条件、状态机跳转 工具自动统计,无需额外代码 高覆盖率≠功能正确,空转也能刷高
功能覆盖率 设计规范中定义的功能点、协议场景 需要手动写covergroup/coverpoint 写不全等于白测,漏场景是常态

我个人习惯的做法是:代码覆盖率全程开着,因为它不费事,跑仿真时顺手就收了。功能覆盖率则要精打细算——只针对关键功能点写covergroup,别试图覆盖所有信号。

我的经验:代码覆盖率达到90%以上才算及格,但别盲目追求100%。有些死代码(比如case语句的default分支)永远走不到,强行覆盖反而浪费时间。

3.2 覆盖率开关设置:别让工具白干活

不同仿真器的开关写法不一样,但核心逻辑相通。我以Synopsys VCS为例,给你看一套我常用的配置:

# 开启所有代码覆盖率类型
cover report +line +branch +cond +tgl +fsm

# 关闭某些模块的覆盖率(后面会细讲)
-cover_off -module uart_top
-cover_off -file exclude_list.txt

# 功能覆盖率开关
-cm_assertion +covergroup

这里有个坑:条件覆盖率(cond)默认只覆盖条件表达式整体为真/假,不会拆开看每个子条件。你想想看,一个 if (a && b) 语句,如果a和b同时为1的情况没测到,工具不会告诉你。我曾经因为这个漏掉了一个握手信号的组合逻辑bug,查了三天才定位到。

避坑指南:如果你用VCS,记得加 -cm_cond_allops 开关,让工具把每个子条件单独统计。否则条件覆盖率就是自欺欺人。

3.3 覆盖率数据库合并:别让数据打架

仿真通常跑很多testcase,每个case生成一个覆盖率数据库。最后需要合并成一个总库,才能看到全局覆盖情况。

合并的坑主要在两点:增量合并 vs 全量合并,以及合并顺序。

我推荐的做法是:

  1. 每个testcase独立生成一个 .vdb 目录(VCS)或 .ucdb 文件(Questa)
  2. 用工具提供的合并命令统一合并,不要手动复制文件
  3. 合并时指定 -full 参数,避免增量合并导致数据丢失
# VCS 合并示例
urg -dir test1.vdb test2.vdb test3.vdb -full -dbname merged.vdb

# Questa 合并示例
vcover merge -full merged.ucdb test1.ucdb test2.ucdb test3.ucdb

为什么强调 -full?因为增量合并默认只保留最后一次运行的数据,如果你某个testcase跑了两遍,前面的数据会被覆盖。嗯,我吃过这个亏,合并完发现覆盖率反而比单个case还低,查了半天才发现是增量合并搞的鬼。

关键指标:合并后的总覆盖率应该 ≥ 每个case覆盖率的并集,而不是平均值。如果发现合并后覆盖率下降,立刻检查合并参数。

3.4 排除文件设置:该忽略的就别测

不是所有代码都需要覆盖。有些代码天生就是“不可测”或“没必要测”的。比如:

  • 仿真专用的调试代码(如 $display、$monitor 相关逻辑)
  • 死代码(设计遗留的未使用分支)
  • 第三方IP(你无权修改,也不需要你测)
  • 断言检查器(本身是验证组件,不是设计)

排除文件怎么写?我一般用两种方式:

方式一:按模块排除

# exclude_modules.txt
module uart_top
module i2c_slave
module ddr_phy

方式二:按文件/行号排除

# exclude_lines.txt
./rtl/debug_monitor.v:1-50
./rtl/clock_gating.v:120-130

然后在仿真命令中引用:

urg -dir merged.vdb -exclude exclude_modules.txt -exclude exclude_lines.txt
避坑指南:排除文件一定要经过代码评审。我曾经见过有人为了刷高覆盖率,把难覆盖的模块全排除了,结果覆盖率报表漂亮得很,实际功能验证一塌糊涂。排除不是偷懒的借口。

3.5 覆盖率收集的完整流程

把上面这些串起来,一个标准的覆盖率收集流程长这样:

覆盖率收集完整流程图 覆盖率收集完整流程 RTL设计代码 仿真配置(开关+排除文件) 运行多个testcase 每个case生成独立覆盖率库 合并(-full参数) 生成覆盖率报告 关键检查点 • 代码覆盖率开关是否完整 • 排除文件是否经过评审 • 合并参数是否用了 -full 常见错误 • 增量合并覆盖数据丢失 • 排除文件写错路径 • 功能覆盖率未关联covergroup

这个流程里,最容易出问题的就是合并和排除这两步。我建议你在回归测试脚本里加一个自动化检查:合并后自动对比每个case的覆盖率,如果某个case的覆盖率在合并后反而下降了,立刻报错。

3.6 覆盖率收集的常见误区

  • 误区一:代码覆盖率100% = 验证充分
    错。代码覆盖率只告诉你代码被执行了,不告诉你执行得对不对。功能覆盖率才是验证质量的真正标尺。
  • 误区二:功能覆盖率写得越多越好
    错。写太多coverpoint会导致仿真速度变慢,而且很多冗余覆盖点根本用不上。我建议只覆盖设计规范中明确的功能场景。
  • 误区三:排除文件可以随便写
    错。排除文件必须经过评审,并且要有明确的排除理由。我习惯在排除文件里加注释,说明为什么排除,谁批准的。
一句话总结:代码覆盖率是底线,功能覆盖率是天花板。排除文件是工具,别让它变成遮羞布。