第 2 章 · 共 15 章
深入 Functional Coverage:bins、Cross 与配置项
第 1 章留到这里的 covergroup/coverpoint 完整语法:显式 bins、illegal_bins、ignore_bins、transition bins、带非法组合排除的 cross coverage、option.at_least/weight/goal,以及 get_coverage() 与 get_inst_coverage() 的区别——全部建立在 axil_regfile 真实的地址空间与响应码之上。
第 1 章只展示了刚好够认出形状的 covergroup/coverpoint,其余全部留到了这里:"完全没说怎么给这些值分桶(那是 bins),也没说跨多个字段的组合(那是 cross)"。这一章把这两样都补上——直接建立在 axi_txn(uvm-advanced 第 2 章)和 axil_regfile 真实的地址译码与响应逻辑(uvm-advanced 第 1 章)之上,不是一个用完就扔的教学示例,而是这个模块从一开始就定下来的同一个 DUT。
Bins:从每个值一个 bin,到有意义的分桶
一个没有 bins 子句的 coverpoint——正是第 1 章 cp_addr: coverpoint txn.addr; 那样——会给这个字段能取的每一个不同的值自动生成一个 bin。对一个 8 位地址来说,这最多是 256 个 bin,每一个都需要单独命中才能算到 100%。这根本不是一个覆盖率模型,而是一次意外:没有人决定过这些桶到底代表什么,而 100% 天生就几乎够不到。
显式 bins 的作用是把桶命名成真正有意义的东西——这里就是 axil_regfile 实际译码出来的那四个地址:
cp_addr: coverpoint txn.addr {
bins ctrl = {8'h00}; // ADDR_CTRL
bins status = {8'h04}; // ADDR_STATUS
bins data = {8'h08}; // ADDR_DATA
bins count = {8'h0C}; // ADDR_COUNT
bins unmapped = default;
}四个命名 bin,每个寄存器一个,再加上第五个:default 不是"没有声明 bins",而是一条显式指令——把前面所有 bin 都没认领的每一个值收进同一个桶里。地址 8'h10、地址 8'h55、地址 8'hFF 全都落进同一个 unmapped bin——这里这样做是对的,因为这个覆盖率模型关心的是"有没有走到过译码器的兜底路径",而不是具体是哪个未映射地址走到的。
illegal_bins 与 ignore_bins:两种不同的"不计入"
这两个关键字都会让某个值永远不计入 100%——面试里最常见的混淆点在于,它们是出于相反的原因这么做的:
ignore_bins的意思是"这个值合理地可能发生,但我不关心它——不要求我去覆盖它"。落在这里的值会被悄悄从模型里排除。illegal_bins的意思是"这个值绝对不应该发生——如果它发生了,那是个 bug,不是覆盖率缺口"。采样到一个illegal_bins里的值会触发一个运行时错误(默认是UVM_ERROR级别),跟一条断言触发的含义一样:说明出问题的是设计,不是测试没测到。
axil_regfile 的 2 位响应字段就是一个具体的例子:AXI4-Lite 实际上只会产生 4 个可能的 2 位取值中的 3 个(OKAY、SLVERR、DECERR——uvm-advanced 第 1 章)。第四个取值 2'b01(在完整 AXI4 里是 EXOKAY,但因为 AXI4-Lite 不支持独占访问,这里毫无意义)在结构上永远不会从这个 DUT 里出现。如果要求覆盖一个结构上根本不可能发生的值,会让 100% 因为一个错误的理由而永远够不到——所以这里用的是 ignore_bins,既不是放着不覆盖,也不是 illegal_bins(因为在这个 DUT 里看到它并不是 bug,只是不可能发生):
cp_resp: coverpoint txn.resp {
bins okay = {2'b00};
bins slverr = {2'b10};
bins decerr = {2'b11};
ignore_bins reserved = {2'b01};
}Transition bins:覆盖的是一个序列,不只是一个值
一个普通 bin 问的是"这个值有没有出现过"。一个 transition bin 问的是"这个值有没有紧跟在那个值后面出现过"——覆盖的是连续采样之间的关系,不是单个采样。语法是 bins name = (val_set_1 => val_set_2);。加到上面 cp_resp 里的这一条,问的是这个 DUT 有没有被连续打过两次错误响应——这是一个普通的按值 bin 完全表达不出来的、真正值得关心的压力场景:
bins back_to_back_err = ({2'b10, 2'b11} => {2'b10, 2'b11});只有当一次采样落在 {SLVERR, DECERR} 里,紧接着下一次采样也落在 {SLVERR, DECERR} 里时,这个 bin 才算命中——连续四次 OKAY 永远碰不到它,被 OKAY 包围的一次孤立错误同样碰不到它。
Cross Coverage:组合,以及不应该发生的组合
cross 问的是两个或多个 coverpoint 的组合——不是分别问"status 有没有被命中"和"slverr 有没有被命中",而是具体问"status 有没有跟 slverr 一起被命中过"。axil_regfile 的译码逻辑让好几种地址/响应的组合在结构上根本不可能发生,而 cross 自己的 illegal_bins 正是把这件事编码进去的地方——把目前只存在于 RTL case 语句里(uvm-advanced 第 1 章)的知识,变成覆盖率模型主动去检查的东西:
addr_x_resp: cross cp_addr, cp_resp {
// CTRL 和 DATA 既可读又可写——译码器保证访问它们中的任何一个
// 都得到 OKAY,绝不会是错误响应。
illegal_bins rw_regs_never_error =
binsof(cp_addr) intersect {8'h00, 8'h08} &&
binsof(cp_resp) intersect {2'b10, 2'b11};
// STATUS 和 COUNT 是已映射的寄存器——译码 miss(DECERR)绝不会
// 落在它们身上,只可能是 OKAY(读)或 SLVERR(写,因为两者都
// 是只读的)。
illegal_bins ro_regs_never_decerr =
binsof(cp_addr) intersect {8'h04, 8'h0C} &&
binsof(cp_resp) intersect {2'b11};
// 一个未映射的地址绝不可能返回 OKAY 或 SLVERR——译码器要么
// 在那里找到一个寄存器,要么找不到。
illegal_bins unmapped_never_ok_or_slverr =
binsof(cp_addr.unmapped) &&
binsof(cp_resp) intersect {2'b00, 2'b10};
}如果这三条里的任何一条真的被采样到,就说明覆盖率模型对译码器的理解,跟译码器实际的行为对不上——这值得报一个错误,跟 cp_resp 上 illegal_bins 的道理一样。反过来,unmapped 跟 decerr 交叉命中,恰恰是一次正常跑通的仿真最终应该做到的事——这个组合是合法的,只是没有被预先排除掉。
配置项:at_least、weight 与 goal
三个值得了解的 option,上面和下面的模型里都用到了:
option.at_least——一个 bin 需要被命中多少次才算"覆盖到了"。默认值是 1;把它调高(就像下面cp_resp上做的那样)是在说,一次运气好碰上的错误响应还不够被称为"测过了"——它得重复出现。option.weight——一个 coverpoint 或 cross 在覆盖率模型汇总百分比时占多重的权重。默认所有东西都是 1;一个有九个合法 bin 的 cross,如果跟两个各自只有四五个 bin 的 coverpoint 直接平均汇总,很容易把它们的比重压过去,所以调高它(或者调高那几个 coverpoint 自己)的权重,才能让汇总数字有意义,而不是意外地失衡。option.goal——一个 bin、coverpoint 或 covergroup 要达到多少百分比,工具才会在汇总报告里把它标记成"已完成"。默认值是 100;针对某个具体 bin 把它调低,是一个刻意的、需要留下记录的判断——某个组合确实存在,但足够罕见,不值得死磕到字面意义上的 100%。第 6 章会讲什么时候这个判断是对的——这里所有 bin 都保留默认值。
cp_resp: coverpoint txn.resp {
bins okay = {2'b00};
bins slverr = {2'b10};
bins decerr = {2'b11};
ignore_bins reserved = {2'b01};
bins back_to_back_err = ({2'b10, 2'b11} => {2'b10, 2'b11});
option.at_least = 2;
}接上去:从 monitor 现成的 analysis port 里采样
这一切都不需要一个新的 environment。axi_monitor 已经在把每一个跑完的 axi_txn 通过它的 analysis_port 广播出来了(uvm-advanced 第 2 章);覆盖率模型要做的只是再做一个订阅者——跟 irq_watcher 在 regfile_env(uvm-advanced 第 3 章)里已经用过的一模一样的 uvm_analysis_imp/write() 写法:
class regfile_coverage extends uvm_component;
`uvm_component_utils(regfile_coverage)
uvm_analysis_imp #(axi_txn, regfile_coverage) imp;
covergroup axi_cg with function sample(axi_txn txn);
option.per_instance = 1;
cp_addr: coverpoint txn.addr {
bins ctrl = {8'h00};
bins status = {8'h04};
bins data = {8'h08};
bins count = {8'h0C};
bins unmapped = default;
}
cp_resp: coverpoint txn.resp {
bins okay = {2'b00};
bins slverr = {2'b10};
bins decerr = {2'b11};
ignore_bins reserved = {2'b01};
bins back_to_back_err = ({2'b10, 2'b11} => {2'b10, 2'b11});
option.at_least = 2;
}
addr_x_resp: cross cp_addr, cp_resp {
option.weight = 2;
illegal_bins rw_regs_never_error =
binsof(cp_addr) intersect {8'h00, 8'h08} &&
binsof(cp_resp) intersect {2'b10, 2'b11};
illegal_bins ro_regs_never_decerr =
binsof(cp_addr) intersect {8'h04, 8'h0C} &&
binsof(cp_resp) intersect {2'b11};
illegal_bins unmapped_never_ok_or_slverr =
binsof(cp_addr.unmapped) &&
binsof(cp_resp) intersect {2'b00, 2'b10};
}
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
imp = new("imp", this);
axi_cg = new();
endfunction
function void write(axi_txn txn);
axi_cg.sample(txn);
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("REGFILE_COV",
$sformatf("axi_cg coverage: %0.1f%%", axi_cg.get_coverage()), UVM_LOW)
endfunction
endclassregfile_env(uvm-advanced 第 3 章)只需要加一个新成员和 connect_phase 里一行新代码就能接上它,其他地方都不变:
class regfile_env extends uvm_env;
// ... axi_agt、irq_agt、irqw 都不变 ...
regfile_coverage cov;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// ... 已有的 create 都不变 ...
cov = regfile_coverage::type_id::create("cov", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
irq_agt.mon.ap.connect(irqw.imp);
axi_agt.mon.ap.connect(cov.imp);
endfunction
endclasscovergroup ... with function sample(axi_txn txn) 直接把 transaction 当参数传进来,而不是先存到一个成员变量里——各个 coverpoint 直接引用参数上的 txn.addr/txn.resp,write() 里的 axi_cg.sample(txn) 是唯一发生采样的地方:每跑完一笔 transaction 采样一次,完全由 monitor 实际看到的东西驱动。
从代码里查询覆盖率:get_coverage() 与 get_inst_coverage() 的区别
上面 report_phase 里调用了 axi_cg.get_coverage()——值得精确说清楚它返回的到底是什么,因为 get_coverage() 和 get_inst_coverage() 回答的是两个不同的问题,只是在这一章的例子里恰好看起来一样:
get_inst_coverage()返回这一个 covergroup 实例自己的覆盖率——就是regfile_coverage里那一个axi_cg对象,别的都不算。get_coverage()返回的是仿真过程中曾经存在过的这个 covergroup 类型的所有实例汇总起来的覆盖率。
regfile_env 只创建了一个 regfile_coverage,它也只创建了一个 axi_cg——所以在这里,这两个调用返回的数字是一样的,很容易就以为它们可以互换。它们不能:一旦同一个 covergroup 被例化了不止一次——多 agent 的 testbench 里每个 agent 一份,一个 scoreboard 数组里每个元素一份,任何一个覆盖率模型被复用的地方——get_coverage() 会把所有实例混在一起算成一个汇总数字,而 get_inst_coverage() 依然只看其中一个实例。一旦存在不止一个实例,还用错了方法,正是那种会悄悄让覆盖率报告变得比实际情况更乐观(或更悲观)、而且不容易被发现的错误。
关于验证可行性的提醒
跟第 1 章说过的限制一样:Icarus Verilog 完全没有实现 covergroup,所以这一章的代码没有在这里被任何仿真器跑过——上面的代码是每个商业仿真器都支持的、标准而不出奇的 covergroup/bins/cross 语法,是靠仔细对照 axil_regfile 真实的译码逻辑读出来的,不是靠执行验证的。如果想真的看到这些 bin 命中、以及非法组合触发错误,去 EDA Playground 上换一个商业级仿真器(比如 Aldec Riviera-PRO)实际跑一遍。
小结
- 一个没有
bins子句的coverpoint会给每个不同的值自动生成一个 bin——通常不是你想要的。显式bins把桶命名成有意义的东西;default把剩下所有值收进一个桶,而不是给每个剩余值各自一个 bin。 ignore_bins排除掉一个合理可能发生、但不值得追踪的值。illegal_bins标记一个绝对不应该发生的值——采样到它是一个错误,不是覆盖率缺口。- Transition bin(
(a => b))覆盖的是连续采样之间的序列关系,不是单个值——适合"两次错误有没有连续发生"这类场景。 cross问的是多个 coverpoint 组合在一起的情况,不是各自分开的情况;cross 自己的illegal_bins编码了结构上不应该发生的组合,能抓出覆盖率模型的理解和 RTL 实际行为之间的不一致。option.at_least(一个 bin 要命中多少次才算覆盖到)、option.weight(某样东西在汇总时占多重)、option.goal(多少百分比算"完成")调的是覆盖率模型怎么报告,不改变它衡量的是什么。get_coverage()汇总一个 covergroup 类型的所有实例;get_inst_coverage()只看一个实例。只有一个实例时两者看起来一样,一旦有不止一个实例就会分道扬镳。
一个 DUT 在结构上保证绝对不会产生的值,应该用哪个构造来处理?
为什么这一章的覆盖率模型要把 cp_addr 和 cp_resp 交叉起来,而不是只分别覆盖它们各自?
一个 covergroup 在多 agent 的 testbench 里每个 agent 各例化了一份。在其中一个实例上调用 get_coverage() 和 get_inst_coverage() 有什么区别?