3. 功能覆盖率入门:SystemVerilog中的covergroup与coverpoint基础语法
功能覆盖率,说白了就是回答一个问题:「我们测了哪些功能点?」
代码覆盖率告诉你代码跑了多少行,但功能覆盖率告诉你——你关心的那些场景到底有没有被触发。我刚开始做验证那会儿,总觉得代码覆盖率达到100%就万事大吉了。结果有一次,一个关键的状态机跳转路径根本没走到,代码覆盖率却是绿的。嗯,从那以后,我再也不敢只看代码覆盖率了。
3.1 为什么需要功能覆盖率?
先想一个问题:你写了一个加法器,测试用例把所有的输入组合都跑了一遍,代码覆盖率100%。但你的设计文档里写着「加法器支持饱和截断」,这个功能你测了吗?
代码覆盖率不知道你在测什么功能。它只管「这行代码执行了没有」,不管「这个功能点验证了没有」。
功能覆盖率就是用来弥补这个鸿沟的。它让你把设计规范里的功能点,映射成可量化的覆盖率指标。
核心思想:功能覆盖率 = 你关心的场景 / 实际被触发的场景
3.2 covergroup 是什么?
covergroup 是 SystemVerilog 里用来定义功能覆盖率模型的容器。你可以把它理解成一个「采样篮子」,里面放着你想要监控的信号和变量。
我个人习惯把 covergroup 定义在 interface 或者 testbench 的顶层模块里。这样方便统一管理,也方便复用。
来看一个最简单的例子:
covergroup cg_adder;
// 这里放 coverpoint
endgroup
// 实例化
cg_adder cg_inst = new();
注意,covergroup 需要手动实例化。不像 module 那样例化就自动创建。我刚开始学的时候经常忘记 new(),结果覆盖率一直为零,查了半天才发现是没实例化。
3.3 coverpoint:定义你要采什么
coverpoint 就是 covergroup 里的「采样点」。你告诉它:帮我盯着这个信号,看看它都出现了哪些值。
covergroup cg_adder;
coverpoint sum; // 盯着 sum 信号
endgroup
这样写,仿真器会自动把 sum 的所有取值记录下来,然后统计每个值被命中了多少次。
但问题来了——如果 sum 是 32 位的,那就有 2^32 种可能。你不可能每个值都去覆盖。这时候就需要 bin 的概念。
3.4 自动分 bin 与手动分 bin
默认情况下,SystemVerilog 会自动为 coverpoint 创建 bin。对于枚举类型,每个枚举值一个 bin。对于整型,它会自动划分成若干个区间。
但我建议你手动分 bin。为什么?因为自动分 bin 可能把重要的边界值给吞掉了。
举个例子:
covergroup cg_adder;
coverpoint sum {
bins zero = {0}; // 单独一个 bin 给 0
bins small = {[1:10]}; // 1 到 10 归为一个 bin
bins large = {[11:255]}; // 11 到 255 归为一个 bin
bins overflow = {[256:$]}; // 256 以上归为 overflow
}
endgroup
这样写,你一眼就能看出:sum 有没有出现过 0?小数值有没有覆盖到?有没有溢出?
我的习惯:边界值单独建 bin,中间值可以合并。比如地址范围,0、最大值、中间值各一个 bin,剩下的按区间合并。
3.5 交叉覆盖率:组合场景的威力
单个 coverpoint 只能看一个信号。但很多 bug 出现在多个信号的组合上。比如:opcode 是加法,同时 carry 为 1,这种组合你测了吗?
交叉覆盖率用 cross 关键字来实现:
covergroup cg_alu;
coverpoint opcode;
coverpoint carry;
cross opcode, carry; // 自动生成所有组合的 bin
endgroup
这里有个坑:如果 opcode 有 4 个 bin,carry 有 2 个 bin,那交叉之后就是 8 个 bin。如果每个 coverpoint 都有几十个 bin,交叉之后可能爆炸。
我曾经在一个项目里,把两个各有 16 个 bin 的 coverpoint 做了交叉,结果生成了 256 个 bin。仿真跑了三天,大部分 bin 都是空的。后来我加了 ignore_bins 过滤掉无效组合,才把覆盖率跑上去。
3.6 选项与条件采样
有时候你不想在每个时钟周期都采样。比如只在 valid 为高的时候采样:
covergroup cg_data @(posedge clk);
coverpoint data iff (valid); // valid 为高时才采样
endgroup
还可以用 option.per_instance 来控制是否按实例统计覆盖率:
covergroup cg_fifo;
option.per_instance = 1; // 每个实例单独统计
coverpoint wr_ptr;
coverpoint rd_ptr;
endgroup
我个人习惯在调试阶段打开 per_instance,这样能定位到是哪个模块没覆盖到。回归测试时再关掉,合并统计。
3.7 知识体系总览
下面这张图帮你理清 covergroup 的核心结构:
3.8 一个完整的例子
把上面学到的串起来,写一个完整的 covergroup:
interface fifo_if;
logic [7:0] data_in;
logic [7:0] data_out;
logic wr_en;
logic rd_en;
logic full;
logic empty;
logic [3:0] fifo_count;
endinterface
class fifo_coverage;
fifo_if vif;
covergroup cg_fifo @(posedge vif.clk);
// 数据覆盖
coverpoint vif.data_in {
bins zero = {8'h00};
bins max = {8'hFF};
bins middle = {[8'h01:8'hFE]};
}
// 状态覆盖
coverpoint vif.fifo_count {
bins empty = {0};
bins one = {1};
bins full = {16};
bins partial = {[2:15]};
}
// 操作组合覆盖
coverpoint vif.wr_en;
coverpoint vif.rd_en;
cross vif.wr_en, vif.rd_en {
// 忽略同时读写的情况(如果设计不允许)
ignore_bins illegal = binsof(vif.wr_en) && binsof(vif.rd_en);
}
// 条件采样:只在复位释放后采样
coverpoint vif.data_out iff (!vif.rst_n);
endgroup
function new(fifo_if vif);
this.vif = vif;
cg_fifo = new();
endfunction
endclass
这个例子覆盖了:数据值、FIFO深度、读写操作组合、以及复位后的输出。基本上一个 FIFO 的核心功能点都覆盖到了。
3.9 避坑指南
最后分享几个我踩过的坑:
- bin 数量失控:交叉覆盖率不要无脑全组合。先用
ignore_bins过滤掉无效组合。 - 采样时机不对:默认是每次 covergroup 被触发时采样。如果你用
@(posedge clk),那每个时钟周期都采。有时候你只想在事务结束时采一次,记得用sample()方法手动控制。 - 忘记实例化:covergroup 定义好了,不 new() 就是空的。我见过有人写了 500 行的 covergroup,结果覆盖率一直是 0%,就是因为忘了 new。
- 忽略边界值:自动分 bin 可能把 0 和最大值合并到区间里。手动分 bin 时,一定要把边界值单独拎出来。
一个小技巧:在仿真日志里打印 $get_coverage(),可以实时看到总体覆盖率。我习惯在每个测试用例结束时打印一次,方便快速定位哪个用例覆盖率低。
功能覆盖率入门其实不难。记住三个核心:covergroup 是容器,coverpoint 是采样点,cross 是组合。先把这三个概念搞明白,后面再学 transition coverage、illegal_bins 这些进阶内容就顺了。
公众号:蓝海资料掘金营,微信deep3321