2、功能覆盖率基础:功能覆盖率的定义、covergroup与coverpoint、bin的创建与管理、交叉覆盖率
功能覆盖率,说白了就是验证你的设计到底有没有把该测的功能点都测到。代码覆盖率告诉你「代码跑了多少行」,但功能覆盖率告诉你「你想测的那些场景到底测了没」。我见过不少团队,代码覆盖率冲到95%以上,结果流片回来还是挂了——就是因为功能点没覆盖全。嗯,这玩意儿才是验证的灵魂。
2.1 功能覆盖率的定义
功能覆盖率衡量的是「设计意图」被验证的程度。它不是看代码执行了多少,而是看你关心的那些功能场景是否被触发了。
举个例子:你设计了一个FIFO,代码覆盖率可能告诉你所有状态机跳转都跑过了。但功能覆盖率会问你:「满标志置位后,写操作有没有被正确阻塞?」「空标志置位后,读操作有没有返回无效数据?」这些才是真正要命的问题。
我个人习惯把功能覆盖率分成两类:
- 显式覆盖率:你主动定义的那些covergroup和coverpoint
- 隐式覆盖率:比如断言覆盖率、属性覆盖率
今天咱们重点聊显式覆盖率,也就是你亲手写的那部分。
核心要点:功能覆盖率不是越高越好,而是「该覆盖的都覆盖了」就好。我曾经见过有人为了冲100%覆盖率,写了大量无意义的bin,结果真正重要的场景反而漏了。记住,覆盖率是手段,不是目的。
2.2 covergroup与coverpoint
covergroup是SystemVerilog里定义覆盖率的核心结构。你可以把它理解成一个「采样容器」——你把关心的信号放进去,它帮你统计这些信号的各种取值情况。
先看一个最简单的例子:
covergroup fifo_cg @(posedge clk);
wr_en_cp : coverpoint wr_en;
rd_en_cp : coverpoint rd_en;
fifo_level_cp : coverpoint fifo_level;
endgroup
这个covergroup会在每个时钟上升沿采样wr_en、rd_en和fifo_level这三个信号。每个coverpoint会自动创建bin来记录信号的各种取值。
但这里有个坑——自动创建的bin可能不是你想要的。比如fifo_level是个8位信号,取值范围0~255,自动bin会创建256个bin。你想想看,这有意义吗?大多数时候你只关心「空、半满、满」这几个状态。所以,手动定义bin才是常态。
我的经验:covergroup的采样时机很关键。我建议用@(posedge clk)或者@(negedge clk)来同步采样,避免在信号跳变沿采样到不确定值。另外,别忘了在covergroup里加上option.per_instance = 1,这样每个实例的覆盖率会被单独统计,调试时特别有用。
2.3 bin的创建与管理
bin是功能覆盖率的最小统计单元。每个bin对应一个你关心的取值或取值区间。创建bin的核心原则是:只创建你关心的,别搞那些没用的。
来看几种常见的bin创建方式:
| 方式 | 语法 | 适用场景 |
|---|---|---|
| 单个值 | bins zero = {0}; | 只关心某个特定值 |
| 区间 | bins low = {[1:10]}; | 关心一段连续范围 |
| 数组 | bins vals[] = {1, 3, 5, 7}; | 关心几个离散值 |
| 通配符 | bins wild = {4'b10??}; | 只关心部分比特位 |
| 忽略 | ignore_bins ign = {[16:255]}; | 排除不关心的取值 |
| 非法 | illegal_bins ill = {8'hFF}; | 标记不该出现的取值 |
这里我要特别强调一下ignore_bins和illegal_bins的区别。ignore_bins是「我不关心这个值,你统计时别算它」。illegal_bins是「这个值不应该出现,如果出现了就是设计bug」。我曾经在一个项目中,把某个保留位写成了illegal_bins,结果仿真一跑就报错——原来设计文档写错了,那个保留位其实已经被用了。你看,覆盖率还能帮你发现文档问题。
covergroup addr_cg @(posedge clk);
addr_cp : coverpoint addr {
// 只关心地址0x00~0x3F
bins low_addr = {[0:16]};
bins mid_addr = {[17:32]};
bins high_addr = {[33:63]};
// 0x40~0xFF不关心
ignore_bins unused = {[64:255]};
// 0xFF是保留地址,不该出现
illegal_bins reserved = {8'hFF};
}
endgroup
注意:bin的数量不是越多越好。每个bin都会占用内存,如果某个coverpoint有256个自动bin,而你又例化了100个实例,那就是25600个bin。仿真跑起来内存蹭蹭往上涨。我建议手动控制bin的数量,一般不超过64个。
2.4 交叉覆盖率
交叉覆盖率,说白了就是看多个信号组合在一起的情况。比如你单独测wr_en=1和fifo_full=1都没问题,但wr_en=1且fifo_full=1同时出现时,你的设计还能正确处理吗?这就是交叉覆盖率要回答的问题。
语法很简单,用cross关键字:
covergroup fifo_cg @(posedge clk);
wr_cp : coverpoint wr_en;
rd_cp : coverpoint rd_en;
full_cp : coverpoint fifo_full;
// 交叉覆盖率
wr_full_cross : cross wr_cp, full_cp;
all_cross : cross wr_cp, rd_cp, full_cp;
endgroup
交叉覆盖率有个大坑——组合爆炸。三个coverpoint,每个有4个bin,交叉后就是4×4×4=64个bin。如果每个有16个bin,三个交叉就是4096个bin。你想想看,这还只是三个信号。我见过有人交叉了6个信号,每个信号8个bin,结果生成了26万个bin——仿真直接跑不动了。
怎么避免?两个原则:
- 只交叉真正相关的信号。比如wr_en和fifo_full相关,但wr_en和data_out可能就不相关。
- 用binsof和intersect过滤。只关心那些有意义的组合。
cross wr_cp, full_cp {
// 只关心写使能且满的情况
bins wr_when_full = binsof(wr_cp) intersect {1} &&
binsof(full_cp) intersect {1};
// 忽略写禁止且空的情况
ignore_bins no_interest = binsof(wr_cp) intersect {0} &&
binsof(full_cp) intersect {0};
}
避坑指南:我曾经在一个AHB总线验证项目中,交叉了地址、读写控制、突发长度三个信号。结果发现大量组合根本不会出现——比如地址对齐时,突发长度只能是1、4、8。后来我用binsof+intersect把非法组合过滤掉,覆盖率从30%直接跳到85%。记住,交叉覆盖率不是让你测所有组合,而是测那些有意义的组合。
最后说一句,功能覆盖率不是一锤子买卖。我习惯的做法是:先定义核心功能点,跑一轮仿真看覆盖率,然后根据漏掉的点补充测试用例,再跑,再补充。一般迭代3~5轮,覆盖率就能稳定在95%以上。别指望一次搞定,那不可能。