4. 功能覆盖点定义:covergroup 与 coverpoint 的语法结构,bin 的自动创建与手动定义

好,咱们进入覆盖率的「心脏」部分。

前面聊了那么多概念,说白了,最终落地就靠两个东西:covergroupcoverpoint。你想想看,没有它们,覆盖率就是空中楼阁。

我个人习惯把 covergroup 看作一个「容器」,里面装着你关心的各种信号和状态。而 coverpoint 就是容器里的一个个「观察窗口」,盯着某个信号的变化。

4.1 covergroup 的基本骨架

先看一个最简单的例子,感受一下语法长什么样:

covergroup cg_arbiter @(posedge clk);
    // 这里放 coverpoint
    grant_cp : coverpoint grant;
    req_cp   : coverpoint req;
endgroup

嗯,就这么简单。一个 covergroup 可以包含多个 coverpoint。那个 @(posedge clk) 是采样事件,告诉仿真器在时钟上升沿去采样。

小技巧: 采样事件不一定要用时钟。我在项目中遇到过用握手信号 @(posedge valid & ready) 做采样,专门抓数据传输阶段的覆盖率。效果不错。

4.2 coverpoint 的自动 bin 创建

如果你不指定 bin,SystemVerilog 会帮你自动创建。怎么个自动法?

对于 2 值信号(比如 bit、logic),自动创建 2 个 bin:0 和 1。
对于枚举类型,每个枚举值一个 bin。
对于整数类型(比如 int),情况就复杂了——默认会创建 64 个 bin,均匀覆盖整个值域。

举个例子:

bit [3:0] data;  // 0~15
covergroup cg_data @(posedge clk);
    data_cp : coverpoint data;
endgroup

这里 data 是 4 位宽,总共 16 个值。自动 bin 会怎么分?

bin 编号 覆盖范围 说明
auto[0] 0 单个值
auto[1] 1 单个值
... ... ...
auto[15] 15 单个值

等等,不是说默认 64 个 bin 吗?这里只有 16 个值,所以实际 bin 数 = min(64, 值域大小)。

注意: 自动 bin 虽然省事,但有个坑——如果信号值域很大(比如 32 位 int),自动 bin 只覆盖 64 个区间,很多中间值根本不会单独统计。我曾经吃过这个亏,查了半天才发现覆盖率报告里有个大区间覆盖了 90%,但实际关键边界值根本没跑到。

4.3 手动定义 bin:精准控制

自动 bin 适合「扫一遍看看」,但真正做验证,你得手动定义 bin。为什么?因为只有你知道哪些值是关键的。

手动定义 bin 的语法:

covergroup cg_addr @(posedge clk);
    addr_cp : coverpoint addr {
        bins low_range  = {[0:63]};       // 低地址范围
        bins mid_range  = {[64:127]};      // 中地址范围
        bins high_range = {[128:255]};     // 高地址范围
        bins specific   = {255};           // 特定边界值
    }
endgroup

这里我定义了 4 个 bin,每个 bin 覆盖一个值区间或单个值。仿真结束后,报告会告诉你每个 bin 被 hit 了多少次。

还可以用 bins[] 创建数组形式的 bin:

covergroup cg_opcode @(posedge clk);
    op_cp : coverpoint opcode {
        bins op[] = {[0:15]};  // 每个值一个 bin,共 16 个
    }
endgroup

这个 bins op[] 和自动 bin 效果类似,但好处是你可以混搭手动和自动:

covergroup cg_mixed @(posedge clk);
    data_cp : coverpoint data {
        bins zero  = {0};          // 手动:0 单独一个 bin
        bins small = {[1:10]};     // 手动:1~10 一个 bin
        bins others[] = default;   // 自动:剩下的值自动分 bin
    }
endgroup
核心原则: 手动定义 bin 时,优先覆盖边界值、非法值、以及协议规定的特殊状态。自动 bin 只用来兜底,防止遗漏。

4.4 非法 bin 和忽略 bin

这两个东西很实用,但很多人容易搞混。

非法 bin (illegal_bins):如果采样到该 bin 的值,仿真直接报错终止。用来抓设计 bug。

忽略 bin (ignore_bins):采样到该 bin 的值,直接忽略,不计入覆盖率。用来排除不关心的值。

看代码:

covergroup cg_state @(posedge clk);
    state_cp : coverpoint state {
        illegal_bins bad_state = {3, 7};    // 这两个状态不应该出现
        ignore_bins dont_care  = {0, 15};   // 这两个状态不关心
        bins good[] = {[1:14]};             // 剩下的正常状态
    }
endgroup

我在项目中遇到过这样的情况:某个状态机有 16 个状态,但实际只用了 14 个。如果不加 ignore_bins,那 2 个未用状态永远覆盖不到,覆盖率永远到不了 100%。加了 ignore_bins 后,报告就干净了。

4.5 交叉覆盖:cross 的威力

单个 coverpoint 只能看一个信号。但很多 bug 是多个信号组合出来的。这时候就需要 cross。

covergroup cg_transaction @(posedge clk);
    addr_cp : coverpoint addr { bins low = {[0:63]}; bins high = {[64:127]}; }
    rw_cp   : coverpoint rw   { bins read = {0}; bins write = {1}; }
    
    // 交叉覆盖:地址 × 读写
    addr_x_rw : cross addr_cp, rw_cp;
endgroup

这个 cross 会生成 2 × 2 = 4 个 bin:

交叉 bin addr_cp rw_cp
bin[0] low read
bin[1] low write
bin[2] high read
bin[3] high write

交叉覆盖的 bin 数量 = 各 coverpoint bin 数的乘积。如果每个 coverpoint 有 10 个 bin,3 个交叉就是 1000 个 bin。嗯,这里要小心,别搞出天文数字。

我的经验: 交叉覆盖不要贪多。一般 2~3 个 coverpoint 交叉就够了。我曾经试过 5 个 coverpoint 交叉,结果生成了几万个 bin,仿真跑完一看,大部分 bin 都是空的,根本分析不过来。后来学乖了,只交叉真正有组合关系的信号。

4.6 条件覆盖:iff 和 start/stop

有时候你不想在每个时钟周期都采样。比如复位期间,信号值没意义。可以用 iff

covergroup cg_data @(posedge clk iff !rst_n);
    data_cp : coverpoint data;
endgroup

这个 iff !rst_n 表示:只有在复位无效(rst_n=1)时才采样。复位期间直接跳过。

还可以用 covergroup.start().stop() 动态控制:

covergroup cg_fifo @(posedge clk);
    // ...
endgroup

initial begin
    cg_fifo cg = new();
    cg.stop();  // 先停掉
    // 等 fifo 初始化完成
    #100;
    cg.start(); // 再开启采样
end

这个技巧在测试复杂初始化序列时特别有用。我习惯在测试用例开始前 stop 掉 covergroup,等 DUT 进入稳定状态后再 start,避免采集到无效的过渡状态。

4.7 总结一下

covergroup 和 coverpoint 的语法其实不复杂,复杂的是怎么用好它们。我个人的建议是:

  • 先自动,后手动:刚开始用自动 bin 快速验证,发现覆盖率缺口后再手动定义关键 bin。
  • 非法 bin 和忽略 bin 一定要用:它们能让覆盖率报告更干净,更容易定位问题。
  • 交叉覆盖要克制:只交叉真正有意义的组合,别搞成排列组合游戏。
  • 条件覆盖别忘掉:复位、空闲、低功耗模式下的采样控制,直接影响覆盖率质量。

下一节咱们聊聊怎么把这些 covergroup 集成到验证环境中,以及如何用覆盖率驱动你的测试用例迭代。嗯,那才是真正体现「覆盖率驱动验证」精髓的地方。