4、覆盖率驱动的测试平台架构
说实话,很多工程师做验证时,习惯把测试平台搭起来就跑用例。跑完一看覆盖率,哎呀,这里没测到,那里有漏洞。然后回头改环境、加激励,再来一轮。这种「先跑再说」的做法,效率其实很低。
我个人习惯是,在搭建测试平台之前,就想清楚一件事:覆盖率怎么收?收了之后怎么用?说白了,覆盖率不是验证的终点,而是验证的导航仪。它告诉你现在走到哪了,下一步该往哪走。
这一章,我们就聊聊覆盖率驱动的测试平台到底该怎么搭。我会从三个角度展开:验证环境的组件怎么划分、覆盖率收集器怎么设计、以及覆盖率反馈循环机制怎么落地。
4.1 验证环境组件划分
一个覆盖率驱动的验证环境,组件划分和传统验证环境其实大同小异。但有一个关键区别:每个组件都要为覆盖率收集服务。你不能把覆盖率收集当成一个「附加功能」,它应该是环境设计的一部分。
我一般把验证环境分成这么几块:
- 激励生成器(Generator):负责产生随机或定向的测试向量。这里要注意,激励生成器最好支持「权重调节」。为什么?因为覆盖率反馈回来之后,你可能需要让某些场景出现得更频繁。如果生成器不支持动态调权,那反馈循环就断了。
- 驱动器(Driver):把激励转换成接口时序。驱动器本身不关心覆盖率,但它要保证时序的准确性。我在一个项目中遇到过,驱动器把地址信号延迟了一个周期,结果覆盖率收集器采到的全是错误数据。排查了两天才发现,嗯,教训深刻。
- 监视器(Monitor):这是覆盖率收集的前哨站。监视器负责抓取接口上的信号变化,然后送给覆盖率收集器。监视器要做得「轻」,不要在里面做复杂的逻辑判断,否则容易影响仿真性能。
- 计分板(Scoreboard):负责比对预期结果和实际结果。计分板可以和覆盖率收集器联动——比如某个功能点覆盖率一直上不去,计分板可以主动报一个警告,提醒验证工程师关注。
4.2 覆盖率收集器的设计
覆盖率收集器,说白了就是一组 covergroup 和 coverpoint 的集合。但设计得好不好,直接影响验证效率。
我见过不少团队,把覆盖率收集器写得又长又臭。一个 covergroup 里塞了几十个 coverpoint,每个 coverpoint 又定义了几百个仓。跑一次仿真,覆盖率数据库大得吓人,分析的时候根本不知道看哪里。
我的经验是:覆盖率收集器要「精」而不是「多」。你想想看,真正需要关注的功能点其实没那么多。那些边界情况、异常场景、状态机跳转,才是覆盖率收集的重点。
下面是一个简单的覆盖率收集器示例:
// 覆盖率收集器示例
class coverage_collector;
// 定义覆盖组
covergroup cg_packet;
// 包长度覆盖点
coverpoint pkt_len {
bins small = {[0:63]};
bins medium = {[64:511]};
bins large = {[512:1023]};
bins huge = {[1024:1518]};
bins illegal = {[1519:$]};
}
// 包类型覆盖点
coverpoint pkt_type {
bins data = {0};
bins ctrl = {1};
bins err = {2};
bins rsvd = {3};
}
// 交叉覆盖:包长度 × 包类型
cross pkt_len, pkt_type {
// 排除非法组合
ignore_bins illegal_comb = binsof(pkt_len.illegal) &&
binsof(pkt_type.rsvd);
}
endgroup
// 构造函数
function new();
cg_packet = new();
endfunction
// 采样函数
function void sample(input int len, input int typ);
pkt_len = len;
pkt_type = typ;
cg_packet.sample();
endfunction
endclass
这段代码看起来简单,但有几个关键点:
- 仓的划分要合理:不要每个值都定义一个仓,那样没有意义。按业务逻辑分组,比如小包、中包、大包。
- 交叉覆盖要谨慎:交叉覆盖的仓数量是指数增长的。两个10仓的覆盖点交叉,就是100个仓。三个就是1000个。所以,只对真正有意义的组合做交叉。
- 非法组合要排除:用 ignore_bins 把不可能出现的组合排除掉,否则覆盖率永远达不到100%,你会被误导。
4.3 覆盖率反馈循环机制
这是整个架构的核心。没有反馈循环,覆盖率收集就只是个统计工具,而不是驱动验证的引擎。
反馈循环的流程大概是这样的:
- 数据采集:仿真结束后,从覆盖率数据库中提取数据。哪些覆盖点达到了?哪些没达到?
- 分析:找出覆盖率缺口。是激励不够?还是约束太死?还是设计本身就有问题?
- 调整:根据分析结果,调整激励生成器的参数。比如增加某些场景的权重,或者添加新的定向测试。
- 迭代:重新仿真,重复上述步骤,直到覆盖率收敛。
听起来很简单对吧?但实际做起来,有几个坑要注意。
第一个坑:反馈太慢。 如果每次都要等仿真全部跑完,再手动分析、手动调整,那效率太低了。我建议在仿真过程中就做「在线反馈」。比如,每跑完1000个包,就检查一次覆盖率。如果某个覆盖点一直没被采到,就动态调整随机约束,让后续的激励偏向那个方向。
第二个坑:反馈过度。 有些团队看到覆盖率上不去,就拼命往一个方向加激励。结果那个覆盖点达到了,其他覆盖点又掉下去了。这就像打地鼠,顾此失彼。我的做法是:设置一个「覆盖率平衡器」,每次调整时,综合考虑所有覆盖点的状态,而不是只盯着一个。
下面是一个简单的反馈循环伪代码:
// 覆盖率反馈循环伪代码
class feedback_controller;
// 覆盖率阈值
real threshold = 90.0;
// 反馈调整函数
function void adjust_generator(covergroup cg);
// 遍历所有覆盖点
foreach (cg.cp[i]) begin
// 如果覆盖率低于阈值
if (cg.cp[i].get_coverage() < threshold) begin
// 增加该覆盖点对应场景的权重
generator.increase_weight(cg.cp[i].get_name());
end
end
endfunction
// 在线反馈循环
task run();
while (simulation_running) begin
// 每1000个周期检查一次
repeat (1000) @(posedge clk);
// 检查覆盖率
if (cg_packet.get_coverage() < threshold) begin
adjust_generator(cg_packet);
end
end
endtask
endclass
嗯,说到这,我想起一个项目。当时我们验证一个网络交换芯片,功能点特别多。刚开始跑的时候,覆盖率只有60%多。我们按照反馈循环的思路,每跑一轮就分析一次,调整一次。大概跑了七八轮之后,覆盖率到了95%。剩下的5%是真正的边界情况,需要手动构造激励。最后,我们花了三天时间写了几个定向测试用例,终于把覆盖率推到了99.8%。
那个项目让我深刻体会到:覆盖率驱动不是自动化就能解决一切。工具能帮你找到盲区,但填补盲区,还是需要工程师的经验和判断力。
好了,这一章的内容就到这里。覆盖率驱动的测试平台架构,说白了就是三个字:分、收、馈。分清楚组件,收好覆盖率,用反馈驱动迭代。把这三点做好了,你的验证效率至少提升一倍。
可可精品资料库