3、功能覆盖率入门:SystemVerilog covergroup语法、coverpoint与bins定义、cross覆盖率

功能覆盖率,说白了就是检查你的验证有没有「测到位」。

我刚开始做验证那会儿,总觉得只要波形对了就行。直到有一次,一个很隐蔽的地址越界bug逃逸到了后仿,老板把我叫去喝茶……嗯,从那以后,我彻底明白了:代码覆盖率只能告诉你「代码跑了多少」,功能覆盖率才能告诉你「该测的场景测了没有」

3.1 covergroup:功能覆盖率的容器

covergroup就像是一个篮子,你把所有想采集的覆盖点都扔进去。我个人习惯把covergroup定义在interface或者testbench的顶层,这样方便管理。

先看一个最简单的例子:

covergroup cg_alu_op;
    coverpoint alu_op;
endgroup : cg_alu_op

你可能会问:「这就完了?」对,就这么简单。但实际项目中,我们通常会在covergroup里加一些控制参数。

我常用的写法是这样的:

covergroup cg_alu_op @(posedge clk);
    coverpoint alu_op;
endgroup : cg_alu_op

加了个采样事件 @(posedge clk),这样每次时钟上升沿自动采样。省得你手动调用 sample() 方法。

我的习惯:如果DUT的接口信号变化和时钟同步,就用事件触发采样。如果是异步信号,我更喜欢手动调用sample(),控制更灵活。

3.2 coverpoint与bins:定义你的关注点

coverpoint就是你想观察的信号或表达式。但光有coverpoint还不够——你得告诉工具「哪些值是我关心的」,这就是bins的作用。

3.2.1 自动bins vs 手动bins

如果不写bins,工具会自动帮你分桶。比如一个3位的信号,自动生成8个bins,每个值一个桶。

但实际项目中,我们往往需要手动定义。举个例子:

covergroup cg_pkt_len;
    coverpoint pkt_len {
        bins small = {[0:63]};
        bins medium = {[64:127]};
        bins large = {[128:255]};
        bins others = default;
    }
endgroup

这里我把包长分成了4类。为什么要这么分?因为我在项目中遇到过,某个DUT对小包和大包的处理逻辑完全不同,中间值反而走的是默认路径。如果不这样分桶,你根本看不出来小包场景有没有测到。

3.2.2 忽略和非法bins

有些值你不想关心,或者根本不应该出现。这时候用 ignore_binsillegal_bins

covergroup cg_addr;
    coverpoint addr {
        ignore_bins reserved = {[16'hF000 : 16'hFFFF]};
        illegal_bins null_addr = {16'h0000};
    }
endgroup

注意:illegal_bins一旦命中,仿真会直接报错退出。我建议只在「绝对不可能出现」的场景下使用,否则调试起来很痛苦。

我曾经踩过的坑:有一次我把一个正常复位值设成了illegal_bins,结果每次复位仿真就崩。排查了半天才发现是bins定义的问题。所以,复位、初始化这些场景,千万别设成illegal。

3.3 cross覆盖率:组合场景的杀手锏

单个coverpoint只能看一个信号。但很多bug是在多个信号的组合下才暴露的。这时候就需要cross覆盖率。

看个例子:

covergroup cg_arbiter;
    req_type : coverpoint req {
        bins read = {0};
        bins write = {1};
    }
    grant_port : coverpoint port {
        bins port0 = {0};
        bins port1 = {1};
        bins port2 = {2};
        bins port3 = {3};
    }
    
    req_x_grant : cross req_type, grant_port;
endgroup

这个cross会生成 2 × 4 = 8 个交叉桶。每个桶代表一种组合:读请求+端口0、写请求+端口1……等等。

但问题来了——有些组合可能是不合法的。比如某个端口只支持读操作。这时候可以用 ignore_bins 或者 binsof 来过滤:

req_x_grant : cross req_type, grant_port {
    ignore_bins read_only = binsof(grant_port) intersect {2, 3} && 
                           binsof(req_type) intersect {1};
}

这样就把「端口2和端口3的写操作」排除掉了。覆盖率报告里不会显示这些组合,也不会影响你的覆盖率统计。

核心要点:cross覆盖率是发现「组合爆炸」类bug的利器。但要注意,cross的桶数会随着coverpoint数量指数增长。我一般控制在2-3个coverpoint交叉,再多就不好分析了。

3.4 覆盖率收集的实用技巧

讲几个我在项目中积累的经验:

  • 先跑起来,再优化:不要一开始就追求100%覆盖率。先搭个简单的covergroup跑通流程,再逐步细化bins。
  • 合理使用auto_bin_max:默认auto_bin_max=64,如果你的信号位宽很大(比如32位),自动分桶会生成海量bins。我建议手动分组,或者设置auto_bin_max为一个合理的值。
  • 注意采样时机:采样早了可能采到X态,采样晚了可能漏掉有效值。我一般会在协议握手成功的那个时钟沿采样。
  • 用type_option.weight控制权重:有些coverpoint你特别关心,可以加大权重。但别滥用,否则会掩盖真正的问题。
参数 说明 我的建议值
auto_bin_max 自动分桶的最大数量 8-16(信号位宽大时手动分组)
weight coverpoint的权重 默认1,关键场景可设2-3
at_least 每个bin最少命中次数 1(除非有特殊要求)

3.5 小结

功能覆盖率入门其实不难。记住三件事:

  • covergroup 是容器,定义好采样时机
  • coverpoint + bins 是核心,把信号分成有意义的区间
  • cross 是进阶,用来抓组合场景

我个人觉得,功能覆盖率最大的价值不是那个百分比数字,而是它逼着你去思考:「我的验证到底漏了什么?」想清楚这个问题,你的验证质量自然就上去了。

下一章我们聊聊覆盖率驱动的验证流程,以及如何用覆盖率来指导你的测试用例编写。到时候我会分享一个我实际项目中用过的「覆盖率闭环」方法,挺实用的。