验证方法论

第 6 章 · 共 15 章

覆盖率驱动验证:闭合这个环

没有新语法——讲的是一个验证工程师真正会拿第 1-5 章的发现去做的事:把覆盖率跨多个测试合并起来,判断每一个缺口到底是真的、结构上不可能、还是没什么价值,把值得补的补上,剩下的写成一条 waiver,而不是悄悄放着不管。

第 2 到 5 章各自教了一个语言特性:covergroup 语法、line/branch 报告、toggle 活动、FSM 的 state 和 transition 追踪。每一章应用到同一个真实的 axil_regfile 上,都找到了一个真实的缺口——写 STATUS/COUNT/未映射地址从来没测过(第 3 章)、aw_have/w_have 从来没翻转过(第 4 章)、AW_ONLY/W_ONLY 从来没被访问过(第 5 章)。重复这些不是这一章的工作。这一章讲的是一份报告产生之后会发生的事:把它跨不止一个测试合并起来,判断每一个缺口到底意味着什么,把值得补的补上,把剩下的写下来——这正是这个赛道自己的介绍里承诺过的“覆盖率驱动的判断力”,跟第 2-5 章“覆盖率这门语言特性”不是一回事。

跨一个回归合并覆盖率

第 3-5 章的每一份报告都来自一次跑一个测试。一个真正的回归会跑很多测试,合并出来的覆盖率数据库是所有测试的并集——一个 bin、一行、一个分支、一个状态,或者一次转移,只要任何一个测试在回归里命中过它,就算覆盖了。这正是第 2 章区分 get_coverage()get_inst_coverage() 时点出来的场景——跨回归合并正是让这两者真正不同的“不止一个实例”那种情况,现在从假设变成了具体的实践。

这个项目实际上有不止一个测试可以合并。这个项目的历史里存在四份各自独立驱动过 axil_regfile 的测试代码:第 1 章手写的定向仿真(uvm-advanced 第 1 章)、axi_smoke_test 背后的 axi_basic_sequvm-advanced 第 2 章)、regfile_virtual_seq 的默认场景(uvm-advanced 第 4 章)、以及 regfile_no_clear_sequvm-advanced 第 6 章)。把它们的覆盖率合并起来,看看会变成什么样:

Merged Coverage -- axil_regfile.sv, 4 tests (uvm-advanced ch1, ch2, ch4, ch6)
==============================================================================
Item                                    ch1 alone   Merged (4 tests)
---------------------------------------  ----------  ------------------
Write to STATUS/COUNT (SLVERR)                   0          0
Write to unmapped address (DECERR)               0          0
ctrl_enable: 1->0 transition                     0          0
aw_have / w_have toggle                          0          0
AW_ONLY / W_ONLY state visits                    0          0
Read of unmapped address (DECERR)                1          2  (ch1, ch2)

只有最后一行动了,而且它本来单靠第 1 章自己就已经覆盖到了。第 3-5 章找到的每一个缺口,还停在原地,一个都没变。这值得停下来想一想:这个项目确实有四个测试,分别写在四个不同的章节里,用了三种不同的测试基础设施(一个裸的定向 testbench、一个 UVM sequence、一个 virtual sequence)——但沿着每一条跟这些开放缺口相关的轴看过去,它们其实是同一个测试换了件衣服。每一个测试写 CTRL 的时候都是 ENABLE=1,从来不是 0。每一个测试都同时拉高 awvalid/wvalid,从来没有错开过。它们都没有写过 STATUSCOUNT,或者任何未映射地址。回归的规模不等于回归的质量——四个都跑同一个场景的测试,合并起来跟其中任何一个单独的覆盖率没什么两样,而在真正做这次合并之前,唯一能提前发现这件事的办法,就是本来就已经知道每个测试各自落在了同样的路径上。

读这份合并后的报告:到底还有什么是开放的

把第 2-5 章的每一个发现都汇总到一处,对照上面这份 4 测试合并后的结果:

  • Functional(第 2 章):addr_x_resp 这个 cross 里合法的 STATUS×SLVERRCOUNT×SLVERR、写路径上的 unmapped×DECERR 这几个 bin 都从来没被采样到过。这个 cross 的 illegal_bins 一次都没有真正触发过——DUT 的译码逻辑跟模型是对得上的。
  • Line/branch(第 3 章):写 case 语句的 STATUS/COUNT 分支和 default 分支命中次数都是 0。
  • Toggle(第 4 章):aw_have/w_have 从来没翻转过;ctrl_enable 只上升过;s_axi_wstrb 一直被钉在常量上。
  • FSM(第 5 章):AW_ONLY/W_ONLY 从来没被进入过;BOTH_LATCHED 被证明是不可达的。

把跨指标的重复项合并掉之后,一共是七个不同的条目(STATUS/COUNT/未映射地址写操作这个缺口同时出现在 functional cross 和 line/branch 报告里;AW/W 独立性这个缺口同时出现在 toggle 和 FSM 里)。

判断每一项:是三类,不是两类

很容易把每一个开放的条目简单分成"补上"或者"放着"两类——但事情比这个二分法更细。这份报告需要三个类别来覆盖:

  1. 一个真实的缺口。 可达、有意义,只是从来没被真正跑到过。写 STATUS/COUNT/未映射地址、清零 ctrl_enable、跑通 AW/W 独立性路径,都属于这一类——它们每一个都是 axil_regfile 设计上就要处理的真实场景,只是从来没有人真正试过。
  2. 结构上不可达。 根本不算缺口——从设计本身就能证明不可能发生,就像 BOTH_LATCHED 直接从 write_fire 自己的逻辑就能证明不可能(第 5 章),保留响应码 2'b01 永远不可能从这个 DUT 里出来(第 2 章)。追这些不叫严谨,是在做无用功;一个覆盖率模型应该把它们从自己的分母里排除掉(illegal_bins,第 2 章),而不是让它们悄悄把百分比永远压在 100% 以下。
  3. 可达,但没有意义。 s_axi_wstrb 属于这一类,也是最容易被误判的一类。它确实是可以补上的——明天写一个测试改变它的值,toggle 的数字确实会涨——但 axil_regfile 的写逻辑从来没有读过它(第 4 章),所以补上这个缺口验证不了任何真实的东西。这跟场景重不重要没关系;这是在判断设计现在有没有相应的行为可供检查。

把第 3 类误判成第 1 类,就是一份回归在某个信号上"100% 覆盖"、但其实没人真正测过任何有意义的东西的由来。把第 2 类误判成第 1 类,就是一个覆盖率目标永远、毫无意义地卡在 100% 以下、而且没人记得为什么的由来。

补上一个真实的缺口:约束、定向测试,还是都不是

不是每一个真实的缺口都用同一种方式补上,这个项目自己的测试历史——从第 1 章一路到第 6 章的 capstone 全都是定向测试,axi_txn 上声明了 rand 字段却从来没有真正 .randomize() 过——正好是个说明这一点的好案例。

三个已知的角落 → 定向测试是效率最高的办法。STATUS/COUNT/一个未映射地址,再加上一次 ENABLE=0CTRL 写,一共是四个具体的值。约束随机化存在的意义是探索大到没法手动枚举的空间;这不是那种空间:

write(8'h04, 32'h0);   // STATUS: writable-check -- expect SLVERR
write(8'h0C, 32'h0);   // COUNT:  writable-check -- expect SLVERR
write(8'h20, 32'h0);   // unmapped -- expect DECERR
write(8'h00, 32'h0);   // CTRL: ENABLE=0 -- closes ctrl_enable's 1->0 gap

四行代码,补上四个缺口,每一行都好读,出问题也好调——这么小的空间根本用不上约束求解器。

更宽的、真正组合爆炸的空间 → 约束随机化才是更好的工具。 如果这个 DUT 的地址空间是十六个寄存器而不是四个,像上面那四行一样手动枚举每一个角落就不再是高效的办法了。这正是 axi_txnrand bit [7:0] addr——从 uvm-advanced 第 2 章就声明了,这个项目里却从来没有真正通过 .randomize() 用过——终于能派上用场的地方:

constraint addr_dist_c {
  addr dist {
    8'h00 := 30, 8'h08 := 30,     // the two writable registers, weighted heavily
    8'h04 := 15, 8'h0C := 15,     // STATUS/COUNT -- reachable, currently untested on write
    [8'h10:8'hFF] :/ 10           // anything unmapped
  };
}

眼下这个缺口小到上面那四行定向测试才是今天更好的选择——但这条约束展示的正是同一种技术在枚举不再现实的规模上该长什么样,而且这会是这个项目的测试第一次真正调用 .randomize()

一个现有测试基础设施根本没有旋钮可以调的缺口。 AW/W 独立性这个缺口跟上面两种都不一样。axi_txn 没有任何字段能表示"把 W 拖到 AW 之后",axi_basic_seq/regfile_virtual_seqwrite() 任务(uvm-advanced 第 2、4 章)也永远是把一次写操作的两半一起发出去的——这个项目里没有任何一个 rand 变量的约束是可以放松来补上这个缺口的,因为它需要的那条轴(AW/W 各自独立的时序)从一开始就没有被建模过。要真正补上它,需要新的 sequence 或者 driver 能力——一个能把 AW 和 W 错开随机或者指定周期数的 write() 变体——而不是调整某个已经存在的东西。这一点值得跟另外两种分开看:"我们还没写这个测试"和"我们还没造出能写这个测试的东西"是完全不同工作量的两件事,把它们混为一谈,正是一个真实、有价值的缺口被估成五分钟就能改完、然后在每一轮回归里悄悄被拖过去的原因。

把判断写下来:一条覆盖率 waiver,而不是沉默

上面每一个判断,如果不写下来,对下一个冷不丁读到这份报告的人来说,价值等于零。覆盖率 waiver 就是这份记录——不是为了记录而记录的表格,而是"我们做过这个决定,理由是这个"和一个到下一轮回归都没人记得为什么、始终够不到 100% 的百分比之间的区别:

Coverage Waiver Log -- axil_regfile.sv
========================================================================================
# Item                                          Category            Decision
-- ----------------------------------------------  ------------------  --------------------------------
1  Write to STATUS/COUNT (SLVERR)                  Genuine gap         Close now -- directed test
2  Write to unmapped address (DECERR)               Genuine gap         Close now -- directed test
3  ctrl_enable: 1->0 transition                     Genuine gap         Close now -- directed test
4  AW/W arriving on different cycles                Genuine gap         Backlog -- needs new driver/
   (aw_have/w_have, AW_ONLY/W_ONLY)                                     sequence capability, not a
                                                                          quick fix
5  BOTH_LATCHED FSM state                           Unreachable         Excluded permanently -- proven
                                                                          impossible from write_fire
6  RESP reserved value 2'b01                        Unreachable         Excluded permanently -- AXI4-
                                                                          Lite never produces it
7  s_axi_wstrb full toggle range                    Reachable, hollow   Waived -- RTL doesn't read it;
                                                                          revisit only if byte-strobe
                                                                          support is added to the design

第 1-3 项在这一轮回归里就补上。第 4 项是真的,留在 backlog 上继续开放,把它为什么不是当天就能改完的原因写下来,而不是含糊带过。第 5、6 项被永久排除,附带证明,这样明年谁也不用从头再论证一遍"这个到底可不可达"。第 7 项是被 waive 的,既不算补上也不算不管——附带一个明确写出来的、能重新打开它的条件。

到什么程度才算够

补上第 1-3 项之后,这个 DUT 的覆盖率会真的涨——不是装饰性的涨,因为这三个都是真实的缺口,补上它们真的检查到了东西。但它依然不会到 100%:第 4 项留在 backlog 上继续开放,第 5、6 项被永久排除在目标之外而不是被追着补,第 7 项处于 waive 状态。这不是这一轮回归没达标——在补上这个缺口所需的 driver 能力还不存在之前就去追第 4 项,会花掉真实的工程时间去改一个根本还没准备好动手的东西;追第 5、6 项则是在追一个设计本身就保证不可能发生的东西;在还没决定 byte-strobe 支持到底算不算这个设计范围之内之前就去追第 7 项,只会得到一个盖在没被检查过的行为上面的绿色数字,正是第 4 章点名过的那个覆盖率剧场陷阱。"100%"从来都不是目标本身——一份每一个开放条目都写清楚了理由的报告才是,补上第 1-3 项之后,这份报告老老实实地做到了这一点。

关于验证可行性的提醒

跟这个模块每一章一样的限制:这里的一切都没有在 Icarus Verilog 上跑过,因为它既没有实现 covergroup,也完全没有 code coverage 统计功能(第 1 章)。这一章里的每一份报告、每一次合并,都是靠对照真实的 RTL、追踪这个项目真实的测试代码构建出来的,跟这个模块每一章坚持的做法一样。一个商业级仿真器会在 EDA Playground 上把这一切——合并、报告、waiver log——真正靠工具跑出来,而不是靠仔细读出来。

小结

  • 跨一个回归合并覆盖率,取的是每一个测试命中项的并集——正是让 get_coverage()get_inst_coverage()(第 2 章)不同的那种"不止一个实例"场景,现在被真正用上了。
  • 把这个项目实际的四个测试合并起来,没有补上第 3-5 章找到的任何一个缺口——每一个测试跑的都是同一个场景,只是用了不同的基础设施。回归的规模不等于回归的质量。
  • 一个开放条目其实是三种情况之一,不是两种:一个真实的缺口(可达、有意义、没测过)、结构上不可达(可以证明不可能,应该排除)、或者可达但没有意义(能补上,但设计目前根本没有相应的行为可供检查)。
  • 补一个真实的缺口,要让工具配得上缺口的形状:几个已知的角落是定向测试的活;一个真正很大的空间是约束随机化的活;一个现有测试基础设施根本没有旋钮可以调的缺口,得先有新能力,然后才谈得上写测试。
  • 覆盖率 waiver 把判断写下来——真实的缺口被补上或者放进 backlog,不可达的条目被永久排除并附带证明,没有意义的缺口被 waive 并附带能重新打开它的条件——这样下一个读这份报告的人不用把这一切从头再想一遍。
  • 100% 从来都不是真正的目标;一份剩下的每一个缺口都写清楚了理由的报告才是。

跨一个回归的多个测试合并覆盖率,算出来的到底是什么?

把这个项目实际的四个测试(uvm-advanced 第 1、2、4、6 章)合并起来,除了一个读路径的条目之外,第 3-5 章找到的每一个缺口都停在原地。这说明了什么?

s_axi_wstrb 的 toggle 缺口在技术上是可以补上的——明天写个测试改变它的值,数字就会涨。为什么这一章依然把它叫做“可达,但没有意义”,而不是一个真实的缺口?

为什么 AW/W 独立性这个缺口不能像 STATUS/COUNT/未映射地址写操作那个缺口一样,靠调整 axi_txn 已有的约束来补上?