3、功能覆盖模型:覆盖组、覆盖点、交叉覆盖、覆盖仓的定义与使用
功能覆盖率,说白了就是验证的「体检报告」。
你写了那么多测试用例,跑了那么多仿真,到底测全了没有?哪些场景测到了,哪些还没测到?功能覆盖模型就是回答这些问题的。我个人习惯,在项目一开始就把覆盖模型搭好,而不是等到验证快结束了才补。为什么?因为覆盖模型能指导你写测试用例,而不是反过来。
3.1 覆盖组(covergroup)—— 你的「检查清单」
覆盖组是一个用户自定义的容器。你把想要检查的信号、状态、组合都放进去。它有点像你出差前的行李清单——哪些东西带了,哪些没带,一目了然。
我在项目中遇到过一种情况:团队里有人把所有的覆盖点都写在一个巨大的覆盖组里,结果维护起来特别痛苦。我建议按功能模块或协议状态机来划分覆盖组。比如一个覆盖组专门检查AXI的写通道,另一个专门检查读通道。
sample() 方法来采样数据。
// 一个简单的覆盖组示例
covergroup fifo_cg @(posedge clk);
// 覆盖点定义在这里
endgroup
// 在类中实例化
class fifo_monitor;
fifo_cg cg;
function new();
cg = new();
endfunction
task run();
// 采样
cg.sample();
endtask
endclass
@(posedge clk) 事件来触发采样,这样能保证采样时刻和时序逻辑同步。如果你用组合逻辑采样,很容易采到毛刺或中间态。
3.2 覆盖点(coverpoint)—— 盯住关键信号
覆盖点就是你要检查的具体信号或表达式。每个覆盖点可以定义多个仓(bins),用来划分你关心的取值区间。
你想想看,一个32位的数据总线,你不可能去覆盖每一个值——2的32次方,仿真到天荒地老也跑不完。所以我们要做的是「分仓」,把连续的值空间划分成几个有意义的区间。
covergroup fifo_cg @(posedge clk);
// 覆盖点:fifo的深度
depth: coverpoint fifo_depth {
bins empty = {0}; // 空
bins almost_empty = {1, 2}; // 几乎空
bins mid = {[3:12]}; // 中间区域
bins almost_full = {13, 14};// 几乎满
bins full = {15}; // 满
}
endgroup
这里要注意,{[3:12]} 这种写法表示从3到12的所有整数。如果你想要的是3或12两个值,应该写成 {3, 12}。嗯,这个细节我刚开始也搞混过。
3.3 覆盖仓(bins)—— 精细化管理
仓是覆盖点的基本单位。每个仓代表一组你关心的取值。除了上面提到的普通仓,还有几种特殊仓,我简单列一下:
| 仓类型 | 语法 | 说明 |
|---|---|---|
| 普通仓 | bins xxx = {value}; |
指定一个或多个值 |
| 范围仓 | bins xxx = {[low:high]}; |
指定一个连续区间 |
| 非法仓 | illegal_bins xxx = {value}; |
如果采样到该值,直接报错 |
| 忽略仓 | ignore_bins xxx = {value}; |
采样到该值时忽略,不计入覆盖率 |
| 自动仓 | bins xxx[] = {[low:high]}; |
自动将区间分成多个仓 |
// 非法仓和忽略仓的使用场景
covergroup packet_cg @(posedge clk);
pkt_type: coverpoint pkt_type {
// 合法类型
bins data = {0, 1, 2};
bins ctrl = {3, 4};
// 类型5是保留的,不应该出现
illegal_bins reserved = {5};
// 类型6和7在本次验证中不关心
ignore_bins dont_care = {6, 7};
}
endgroup
3.4 交叉覆盖(cross cover)—— 组合才是王道
单个信号的覆盖往往不够。很多bug是在特定组合下才出现的。交叉覆盖就是用来检查多个覆盖点之间的组合情况。
举个例子,你单独检查FIFO的读使能和写使能,可能覆盖率都是100%。但读和写同时为高的情况呢?这就是交叉覆盖要干的事。
covergroup fifo_cg @(posedge clk);
wr_en: coverpoint write_enable {
bins low = {0};
bins high = {1};
}
rd_en: coverpoint read_enable {
bins low = {0};
bins high = {1};
}
// 交叉覆盖:读写使能的四种组合
rw_cross: cross wr_en, rd_en;
endgroup
默认情况下,交叉覆盖会生成所有组合的仓。上面这个例子会生成4个仓:(low, low)、(low, high)、(high, low)、(high, high)。
我遇到过最夸张的一次,有人交叉了5个覆盖点,每个覆盖点有几十个仓,结果生成了上百万个交叉仓。仿真跑了整整一周,覆盖率报告打开就卡死。后来我帮他重新设计,只交叉了最关键的两个信号,问题一下就定位到了。
// 更精细的交叉覆盖控制
covergroup fifo_cg @(posedge clk);
wr_en: coverpoint write_enable {
bins low = {0};
bins high = {1};
}
rd_en: coverpoint read_enable {
bins low = {0};
bins high = {1};
}
depth: coverpoint fifo_depth {
bins empty = {0};
bins full = {15};
bins other = {[1:14]};
}
// 只交叉读写使能,不交叉depth
rw_cross: cross wr_en, rd_en;
// 也可以自定义交叉仓
rw_depth_cross: cross wr_en, rd_en, depth {
// 只关心同时读写且FIFO满的情况
bins rw_full = binsof(wr_en.high) && binsof(rd_en.high) && binsof(depth.full);
}
endgroup
binsof 和 intersect 操作符,结果写错了条件,导致某些组合永远达不到。检查了半天才发现是条件写反了。所以,写完交叉覆盖后,建议先跑几个简单的测试用例,手动验证一下仓的命中逻辑是否正确。
3.5 覆盖模型的设计原则
说了这么多,我总结几条实战中总结出来的原则:
- 先设计,后编码。 别上来就写代码。先在纸上画出状态机、数据流,标出哪些信号需要覆盖,哪些组合需要交叉。
- 覆盖组按功能划分。 一个覆盖组对应一个功能模块或一个协议通道。别把所有东西塞到一个覆盖组里。
- 仓的数量要可控。 单个覆盖点的仓数建议不超过20个。交叉覆盖的仓数建议不超过1000个。超过这个数,覆盖率报告就很难看了。
- 非法仓和忽略仓要用好。 非法仓帮你抓bug,忽略仓帮你过滤不关心的场景,两者都能让覆盖率报告更干净。
- 覆盖率不是越高越好。 100%的覆盖率不代表没有bug。但覆盖率太低,一定有问题。我个人习惯,功能覆盖率目标定在95%以上,剩下的5%留给那些确实无法达到的边界情况。
好了,关于功能覆盖模型的核心概念就讲到这里。覆盖组、覆盖点、交叉覆盖、覆盖仓,这四个概念是功能覆盖率验证的基石。你只要把它们的用法和设计原则搞清楚了,后面写覆盖模型就会顺手很多。
可可精品资料库