第 12 章 · 共 15 章
失败到底出在哪:先查这四个地方
“说说你会怎么调一个失败的测试”背后真正问的是什么——在碰任何波形之前,一个失败在结构上只可能出在四个地方之一:DUT、testbench、环境,或者压根不是 bug——全部立足于 axil_regfile 上三个真实的、已经记录在案的事件。
"说说你会怎么调一个失败的测试"是 DV 面试里的常见问题,而那个天真的回答——"我会打开波形"——恰好跳过了那个真正能缩小范围的问题。波形显示的是发生了什么;它不会告诉你错误来自哪里。在看任何一个信号之前,一个失败在结构上只可能出在四个地方之一,而这个模块——跟前两个一样——不需要为其中任何一个编造例子。axil_regfile 自己的历史里,四个里已经有三个各有一个真实的、记录在案的事件,第四个正躺在这个项目自己的待办清单上。
四个地方,不是一个
- DUT ——RTL 本身做了 spec 不允许的事。修复属于设计本身。
- Testbench ——DUT 其实表现正确,但驱动或者检查它的代码——一个 sequence、一个 driver、一个 monitor、一个 scoreboard 的期望值——出了错:一次竞争,一个关于时序的错误假设,一个搞错了的检查。修复属于测试基础设施,不属于设计。
- 环境 ——DUT 和 testbench 的代码都没有错。是某个工具不支持正在使用的某个语言特性,或者表现跟假设的不一样。修复不是任何一处代码的改动——是工具或者流程的改动。
- 哪里都不是 ——根本没有 bug。这个"失败"其实是测试正确地观察到了 DUT 完全按照 spec 说的那样在做事,意外出在读日志的人身上,不在任何一处代码里。
每一类,都立足于一个真实的事件
DUT:组合逻辑环。 axil_regfile 的 s_axi_awready/s_axi_wready 曾经有一版也在 !write_fire 上做了门控——听起来挺合理,"当前这次刚触发就别接受新的 AW/W"。但 write_fire 依赖 aw_fire,aw_fire 又依赖 s_axi_awready 自己:awready → write_fire → awready,一个真正的组合逻辑环,没有稳定解(uvm-advanced 第 1 章)。这是设计里一个真实的缺陷,是靠仿真 RTL、而不是靠读它找到的,而修复方式——只依赖寄存器状态(aw_have/w_have/s_axi_bvalid)——改的是 DUT,不是 testbench。
Testbench:BVALID/irq 竞争。 在一次跨过阈值的写操作返回之后立刻调用 irq_event.wait_trigger(),看起来很安全,其实不然:BVALID 和 irq 在完全相同的时钟边沿变化,这是靠给两个信号都装上打印 $time 的 always 块直接确认的——两者落在完全一样的时间戳上(uvm-advanced 第 4 章)。axil_regfile 自己在这件事上从头到尾都没有错——driver 的解除阻塞和 monitor 的 trigger() 是两个独立的进程,彼此的相对顺序没有任何保证,这是一个刻在virtual sequence里的时序假设,不是刻在 DUT 里的。修复方式——把等待跟触发它的写操作 fork 在一起——改的是 testbench,不是设计。
环境:Icarus 和 clocking block。 Icarus Verilog 完全没有实现 SystemVerilog 的 clocking block,这是靠试最简单的独立用例直接确认的(SV 第 13 章,uvm-advanced 第 1 章)。DUT 和 testbench 都没有任何问题——这个项目历史上每一个基于 clocking block 的接口,都是一个合规的仿真器能顺利跑通、不会有任何怨言的正确 SystemVerilog 代码。修复方式是把这个限制明确说出来,改用一个商业仿真器(EDA Playground 上的 Aldec Riviera-PRO)——没有任何地方的代码被改动,因为没有任何代码是坏的。
哪里都不是:这个项目还没写的那些测试。 Coverage 第 6 章和 Test-Planning 第 5 章都点过同一个还开着的场景:写 STATUS、写 COUNT、写一个未映射地址。等到有一天真的有测试跑到这几种情况,正确的、spec 规定的结果是 SLVERR 或者 DECERR——不是 OKAY。日志里冒出一个不是 OKAY 的响应码,对任何没先去查过 spec 的人来说,长得完全就是一次失败该有的样子。但它不是。把它当成 DUT 的 bug 来处理,或者更糟——把 DUT"修"成永远返回 OKAY——会把一个正确的设计给弄坏。
先查哪一个
这四个类别排除起来的成本并不一样,而成本正是先查某些、后查另一些的理由。确认"哪里都不是"——这个响应到底是不是 spec 真正要求的——通常是成本最低的检查:重新读一遍相关的那节 spec,确认期望值,几分钟就能做完。确认"环境"几乎一样便宜:这个具体的构造有没有在别的地方顺利跑过,这个工具的文档到底有没有声称支持它。这两项都值得先排除掉,再去花真正的诊断力气去假设一个真实的 bug 存在——DUT bug 和 testbench bug 这两类才是真正、深入的诊断工作所在的地方(第 3 章的主题),在还没确认真的有 bug、或者它是不是其实躺在工具里而不是代码里之前,就一头扎进那里,是在做无用功。
小结
- 一个失败在结构上只可能出在四个地方之一:DUT、testbench、环境,或者哪里都不是——在碰任何波形之前先缩小到是哪一个,才是真正加快调试速度的办法。
- 组合逻辑环(
uvm-advanced第 1 章)是一个真实的 DUT bug:RTL 本身违反了一条 spec 规则,靠仿真它被找到,修复落在了设计里。 BVALID/irq竞争(uvm-advanced第 4 章)是一个真实的 testbench bug:DUT 自始至终都是对的,virtual sequence 的时序假设错了,修复落在了 testbench 里。- Icarus 没有实现
clockingblock(SV 第 13 章)是一个真实的环境不匹配:DUT 和 testbench 都没有任何问题,修复是换仿真器,不是改代码。 STATUS/COUNT/未映射地址的写测试,一旦被写出来,会产出SLVERR/DECERR——这是正确的结果,不是失败。确认"哪里都不是"和"环境"通常是成本最低的检查,值得在假设真有 bug、一头扎进 DUT 或者 testbench 之前先排除掉。
一个测试失败了。在看任何波形之前,这一章主张第一个该问的有用问题是什么?
s_axi_awready/s_axi_wready 早期版本里的那个组合逻辑环(uvm-advanced 第 1 章),被归类成一个 DUT bug,而不是 testbench bug。为什么?
一个未来的测试写了 STATUS,收到了 SLVERR。为什么这一章主张,在假设真的存在一个缺陷之前,先确认“这可能压根不是 bug”通常是值得做的?