4. 功能覆盖点定义:covergroup 与 coverpoint 的语法结构,bin 的自动创建与手动定义
好,咱们进入覆盖率的「心脏」部分。
前面聊了那么多概念,说白了,最终落地就靠两个东西:covergroup 和 coverpoint。你想想看,没有它们,覆盖率就是空中楼阁。
我个人习惯把 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, 值域大小)。
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
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。嗯,这里要小心,别搞出天文数字。
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 集成到验证环境中,以及如何用覆盖率驱动你的测试用例迭代。嗯,那才是真正体现「覆盖率驱动验证」精髓的地方。