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%以上。别指望一次搞定,那不可能。