第 4 章 · 共 15 章
Code Coverage:Toggle
位级别的 0->1/1->0 活动,跟控制流完全无关——axil_regfile 自己的测试历史里有一个真实的发现:第 1 章专门点出、说很难实现的 AW/W 独立性逻辑,从来没有真正翻转过;s_axi_wstrb 在这个项目写过的每一个测试里都被钉死在同一个常量值上。
第 1 章那份模拟报告里写的是 Toggle Coverage: 65.0% (130/200 bits),然后就翻篇了。这一章把它打开——跟第 3 章的 line/branch 缺口不一样(那个缺口至少在源码里能看出来是一条没走到的分支),这一章的头号发现,对目前为止讲过的每一个指标来说都是隐形的:uvm-advanced 第 1 章亲手实现、亲手命名、并且明确说"比 mux2 需要的任何东西都真正更难"的一个功能,在这个项目的整个测试历史里,一次都没有被真正跑到过——不是因为哪条分支被跳过了,而是因为一个信号的值从来没有动过。
"Toggle" 到底数的是什么
Toggle coverage 会给设计里的每一个网络和寄存器插桩——端口、内部导线、内部触发器,一切有值的东西——并且分别给每一个 bit 统计两件事:它发生过多少次 0→1 的跳变,多少次 1→0 的跳变。一个 bit 只有在两个方向都至少发生过一次的时候,才算完全覆盖;一个只上升过、或者只下降过的 bit,不管那一个方向发生了多少次,都只算覆盖了一半。
这跟 line 或 branch coverage 问的是完全不同的问题,值得说清楚差在哪:line 和 branch 问的都是某一段控制流有没有执行过。Toggle 完全不问控制流的事——它问的是一个值有没有真正动过。一个信号完全可能待在一段每个时钟周期都会执行的分支里面,却依然显示 0% 的 toggle coverage——只要这段分支每次都把它驱动成同一个常量。
一份真实的、可以核实的报告
axil_regfile 写逻辑(uvm-advanced 第 1 章)里的四个信号,对照这个项目自己的测试代码实际发出过的那一串写操作——第 1 章的定向测试,以及后面每一章复用的同一个 axi_driver(uvm-advanced 第 2 章)——统计出来的结果:
Toggle Coverage Report -- axil_regfile.sv
====================================================
Signal Bits 0->1 1->0 Status
-------------------- ---- ---- ---- --------------------------
s_axi_bvalid 1 6 6 covered
ctrl_enable 1 1 0 PARTIAL -- 0->1 only
aw_have 1 0 0 NOT COVERED -- never toggled
w_have 1 0 0 NOT COVERED -- never toggled
s_axi_wstrb[3:0] 4 0 0 NOT COVERED -- held constant
s_axi_bvalid就是"完全覆盖"该有的样子:每完成一次写,write_fire 就把它置一次位;bready(这个项目写过的每一个 driver 都把它常年拉高)在下一个周期就把它清一次位——对应第 1 章的六次写,两个方向各发生了六次。这正是另外三行都缺的那个基准。
ctrl_enable:只覆盖了一个方向
ctrl_enable 恰好只发生过一次 0→1——第 1 章第一次写 CTRL(ENABLE=1)——从来没有发生过 1→0。这个项目里之后每一次写 CTRL(包括第 1 章定向测试自己的第二次写,ENABLE=1, IRQ_CLR=1)都把第 0 位置成了 1;从来没有人写过 ENABLE=0 的 CTRL。Line 和 branch coverage 对这件事完全看不出来——ctrl_enable <= w_data_eff[0]; 这条赋值(uvm-advanced 第 1 章)每次写 CTRL 都会执行,所以这一行会显示完全覆盖。Toggle coverage 是这三个指标里唯一能注意到这个 bit 本身只朝一个方向动过的那个。补上这个缺口很具体也很小:只需要一个测试在某处写一次 ENABLE 为 0 的 CTRL。
aw_have / w_have:一个真实的功能,一次都没有被真正跑到过
这是最值得停下来细想的发现。uvm-advanced 第 1 章引入 aw_have/w_have,是用来实现 AXI4-Lite 一条真实规则的机制:"master 可以按任意顺序发送地址和数据,也可以同时发送,slave 必须准备好先锁存不管哪个先到的一方,同时等待另一方……这是一段比 mux2 需要的任何东西都真正更难的握手时序。"这不是一个无关紧要的信号——它是这个 DUT 逻辑里专门用来处理 AW 和 W 在不同周期到达这种情况的那一部分。
这个项目发出过的每一次写——第 1 章手写的定向测试,以及 axi_driver 的 drive() 任务(uvm-advanced 第 2 章,之后每一章都原封不动地复用它)——每一次都在同一个时钟周期里同时拉高 awvalid 和 wvalid。因为在一次新的写操作开始之前 s_axi_awready/s_axi_wready 本来就已经是高电平,aw_fire 和 w_fire 永远落在同一个时钟沿上,write_fire 直接通过表达式里的 aw_fire/w_fire 那两项就变成了高电平,而 aw_have <= 1'b1/w_have <= 1'b1 这两条赋值——它们分别被 if (aw_fire && !write_fire)/if (w_fire && !write_fire) 保护着,而这两个条件在 AW 和 W 一起到达的情况下永远不成立——就干脆从来没有执行过。这两个 bit 在整个仿真过程里、在这个项目跑过的每一个测试里,都一直停在复位值 0 上。(aw_addr_q/w_data_q——本该保存先到那一方的值的寄存器——情况更糟:没有任何地方给它们赋过复位值,唯一可能碰到它们的赋值又都在那两个从没走到过的分支里面——它们从来就没有变成过 unknown 以外的任何值。)
Toggle coverage 正是能把这件事干净地暴露出来的那个指标。Branch coverage 本来也可能看出同样的缺口——那两条 guarding 的 if 语句本身就是没走到过的分支——但这个模块到目前为止还没有具体看过那两条 if,而且不管有没有人看过,toggle coverage 都用另一种方式抓到了它:直接注意到这个 bit 本身没动过,不需要谁先去盯着那条具体的代码行看。一个这个 DUT 专门设计、专门写文档说明要处理的功能,从被引入的那一章开始,就一直没被真正测过。
s_axi_wstrb:一个真实的缺口,但补上它其实没什么意义
s_axi_wstrb 也显示 0%,但原因不太一样,又有点相关。这个项目写过的每一个 driver 都把它驱动成常量 4'hF(uvm-advanced 第 1 章的定向测试,axi_driver 的 drive(),第 2 章)——所以跟 aw_have/w_have 一样,它也从来没有翻转过。但看看 axil_regfile 真正的写逻辑对它做了什么:什么都没做。s_axi_wstrb 被声明成一个端口,一路连到接口上,但在真正执行写操作的 always_ff 块里,一次都没有被引用过。这个 DUT 做的每一次写,不管 s_axi_wstrb 是什么值,都是完整的 32 位写——这是这个教程 DUT 的一个刻意简化,在这里第一次被明确点出来,跟之前那些简化(只支持组合逻辑的 driver、单笔未完成事务的限制)后来变得直接相关时才被点名的做法一样。
这个区别决定了补上这个具体缺口到底能换来什么。一个未来的测试如果在多次写操作里改变 s_axi_wstrb 的值,toggle 的数字确实会涨——那些 bit 确实动了。但这完全不会加强这个 DUT 的验证,因为设计里根本没有 byte-strobe 行为 可供测试去检查。要真正做对,得先有一个设计上的决定(真正实现按字节的写掩码),然后才谈得上一个既改变 s_axi_wstrb、又检查对应字节确实变了或没变的测试。就现在这个设计而言,光是翻转这几个 bit,会得到一份看起来已经补上、实际什么都没验证的覆盖率——这是第 1、3 章那个经典陷阱的一个 toggle 专属版本:100% 的 toggle coverage 只能证明 bit 动过,从来不能证明设计对它做出了什么有意义的响应。分清 aw_have/w_have 的缺口(没人测过的真实功能)和 s_axi_wstrb 的缺口(没有任何逻辑消费它)之间的不同,正是第 6 章要回头处理的那种判断。
关于验证可行性的提醒
跟这个模块其他章节一样的限制:Icarus Verilog 完全没有 toggle coverage 统计功能(第 1 章说过,这本来就是商业工具才有的特性),所以上面这份报告是示意性的——是靠对照真实的 RTL、追踪这个项目自己的示例代码实际发出过的每一次写构建出来的,不是靠工具跑出来的。要看到一份真正的、长这样的 toggle 报告,得去 EDA Playground 上换一个商业级仿真器。
小结
- Toggle coverage 统计的是每一个网络和寄存器上,每个 bit 的
0→1和1→0跳变——跟控制流完全无关。一个信号完全可能待在一段一直在执行的分支里,却依然显示 0% 的 toggle coverage,只要它每次都被驱动成同一个值。 - 一个 bit 需要两个方向都发生过才算完全覆盖;一个只上升过(或者只下降过)的 bit,不管那一个方向发生了多少次,都只算部分覆盖——
ctrl_enable的ENABLE位就是一个真实的例子,被置位过一次,从来没被这个项目写过的任何测试清零过。 aw_have/w_have——实现 AXI4-Lite AW/W 独立性规则的那个机制本身,被uvm-advanced第 1 章明确点出来说真正很难——从来没有真正翻转过,因为这个项目写过的每一个测试都同时拉高awvalid和wvalid。Toggle coverage 不需要任何人先去盯着那条具体保护它的if语句,就能把这件事暴露出来。s_axi_wstrb也从来没有翻转过,但原因不一样:RTL 根本没有读过它。补上这个缺口只会让 bit 动起来,却验证不了任何东西,因为设计里没有实现任何 byte-strobe 行为可供检查——这是"覆盖了但没意义"这个陷阱的一个 toggle 专属版本。- 分清一个真实、有价值的缺口(
aw_have/w_have)和一个技术上能补上、但补了也没意义的缺口(s_axi_wstrb),是一个判断题,不是覆盖率数字自己能回答的——这是第 6 章要处理的事。
一个信号待在一段每个时钟周期都会执行的分支里面,但这段分支每次都把它驱动成同一个常量值。Toggle coverage 会给这个信号报告什么?
uvm-advanced 第 1 章专门实现了 aw_have/w_have 来处理 AW 和 W 在不同时钟周期到达的情况,说这比 mux2 需要的任何东西都真正更难。为什么上面的 toggle 报告显示这两个信号都是 0%?
ctrl_enable 在 0->1 方向上有 1 次命中,1->0 方向上是 0 次命中。补上这个具体缺口需要什么?
一个未来的测试在多次写操作里改变了 s_axi_wstrb 的值,把它的 toggle coverage 补到了 100%。这真的加强了 axil_regfile 的验证吗?