第 14 章 · 共 15 章
诊断方法:隔离、插桩、假设、确认
真正把第 2 章读特征给出的假设变成确认的那套可重复流程——通过对比它在一个真实的结构性 bug 和一个真实的时序 bug 上分别是怎么展开的来教这套方法,两个都已经记录在 axil_regfile 上,不重新完整讲一遍这两个事件。
第 2 章靠读一个失败的特征,形成了一个关于它属于第 1 章四个类别里哪一个的快速初步假设。一个假设不是一个诊断。这一章是把前者变成后者的方法——四个步骤,套用在这个网站已经记录在案的两个真实事件上:组合逻辑环(uvm-advanced 第 1 章)和 BVALID/irq 竞争(uvm-advanced 第 4 章)。两个事件在这里都是被引用,不是被重新讲一遍——这一章的工作是方法本身,而同样的四个步骤,套用在一个结构性 bug 和一个时序 bug 上,呈现出来的样子并不一样。
四个步骤
- 隔离 ——把失败缩小到系统里依然能显现出这个问题的最小那一部分,去掉一切不是触发它所必需的东西。
- 插桩 ——加上目前还不存在的可见性,让这个失败(或者怀疑存在的那个具体风险)变得能直接观察到,而不是靠推断。
- 假设 ——陈述一条关于根因的、具体的、可证伪的说法,精确到某个具体的改动就能证明或者推翻它。
- 用一个最小、有针对性的改动来确认 ——只做假设真正要求的那个最小改动,不是更大范围的重写,然后重新验证。
同样的四个步骤,两种不同的样子
| 步骤 | 组合逻辑环(结构性,DUT) | BVALID/irq 竞争(时序,testbench) |
|---|---|---|
| 隔离 | Testbench 本来就已经很精简了;隔离的意思是缩小到那条具体的信号依赖链本身——s_axi_awready → write_fire → aw_fire → s_axi_awready——而不是搭一个更小的测试。 | 隔离的意思是把两个具体的信号,bvalid 和 irq,从整个多 agent 环境的调度里抽出来,从跟它们一起跑的每一个别的 driver、monitor、sequence 里剥离开。 |
| 插桩 | 仿真器自己的调度器就是插桩——一个真正的组合逻辑环没有稳定解,所以仿真它(而不只是读它)立刻就把问题暴露出来了,没有加任何自定义的东西。 | 默认情况下没有任何东西会暴露相对事件顺序;这需要专门加上的可见性——两个 always 块,每个信号一个,各自在自己的触发条件上打印 $time。 |
| 假设 | 一条精确、可证伪的说法:s_axi_awready 组合逻辑上依赖 write_fire,write_fire 又依赖 aw_fire,aw_fire 又依赖 s_axi_awready——一个真正的零延迟环,没有稳定值,不是一个时序上的小毛病。 | 一条精确、可证伪的说法:driver 在 BVALID 上解除阻塞,跟 monitor 在 irq 上触发,是两个独立的进程,在同一个边沿上彼此的顺序没有任何保证,所以在 write() 返回之后才调用 wait_trigger() 可能已经太晚了。 |
| 用最小改动确认 | 只从两个 ready 信号的赋值里去掉 && !write_fire 这一项——别的什么都不碰——然后重新仿真,确认环消失了,而且之前每一个能通过的场景依然能通过。 | 把已有的 wait_trigger() 和 write() 调用包进 fork...join,而不是重新组织整个 sequence 的结构——然后重新跑一遍,确认结果不再取决于调度运气。 |
这两个根因是真正不同的——一个纯粹是结构性的,压根不需要任何运行时的时序就能存在;另一个纯粹是关于两个独立进程之间的相对时序,两边的代码在结构上都没有任何问题。方法在两者之间没有变。变的是每一步具体意味着什么。
为什么是最小的改动,不是感觉上最保险的改动
一次刚好让失败消失的大范围重写,实际后果比看起来更糟,不是更好——它可能会掩盖这个假设到底有没有真正对过。如果组合逻辑环的修复"为了保险起见"还顺手改了另外三个不相关的 always 块,一次通过的重新仿真就没办法专门确认那个环的假设了;它会留下一个悬而未决的问题:这个环到底是不是真的被修好了,还是只是被同时改动的别的什么东西碰巧盖住了。这个项目历史上这两次真实的修复都刻意做得很小——去掉一项,加上一个 fork——因为一个最小、有针对性的改动,才是唯一真正能确认它本来要测试的那个假设的改动,而不是什么都没确认、只是碰巧管用了。
小结
- 这套方法有四步:隔离出最小的复现场景,插桩让失败能被观察到,陈述一条可证伪的假设,用假设要求的最小改动来确认。
- "隔离"不总是意味着搭一个更小的测试——对组合逻辑环来说,testbench 本来就已经很精简了,隔离的意思是转而缩小到那条具体的信号依赖链。
- "插桩"可以只是运行仿真器、读它自己的抱怨(一个像组合逻辑环这样的结构性 bug 会自己暴露出来),也可以是专门加上自定义的可见性(一个竞争需要打印
$time的探针,因为默认情况下没有任何东西会标出相对顺序)。 - 一个好的假设要精确到可以被证伪——不是"写逻辑那块好像有点不对",而是一条具体的、点名了确切依赖关系或者确切顺序假设的说法。
- 一个最小、有针对性的修复才是真正确认一个假设的东西;一个刚好让症状消失的、更大范围的改动,会留下一个悬而未决的问题:真正的根因到底有没有被找到。
对组合逻辑环这个 bug 来说,“隔离”并不是指搭一个更小的 testbench——现有的那个本来就已经很精简了。在这种情况下,隔离指的是什么?
组合逻辑环是靠仿真 RTL、完全没加任何自定义插桩就被找到的,而 BVALID/irq 竞争需要专门加上打印 $time 的 always 块。为什么会有这个差别?
“driver 在 BVALID 上解除阻塞,跟 monitor 在 irq 上触发,是两个独立的进程,在同一个边沿上彼此的顺序没有任何保证”,比起“时序方面好像有点不对”,是什么让前者算得上一个好假设?
为什么这一章主张一个最小、有针对性的修复,比一个刚好也能让失败消失的更大范围改动要好?