验证方法论

第 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_txnuvm-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 个(OKAYSLVERRDECERR——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_respillegal_bins 的道理一样。反过来,unmappeddecerr 交叉命中,恰恰是一次正常跑通的仿真最终应该做到的事——这个组合是合法的,只是没有被预先排除掉。

配置项: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_watcherregfile_envuvm-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
endclass

regfile_envuvm-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
endclass

covergroup ... with function sample(axi_txn txn) 直接把 transaction 当参数传进来,而不是先存到一个成员变量里——各个 coverpoint 直接引用参数上的 txn.addr/txn.respwrite() 里的 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() 有什么区别?