第 15 章 · 共 15 章
把 Bug 写下来,闭合这个环
确认了根因不是一次调试的终点——一份写得好的记录才是,用 uvm-advanced 第 1 章自己已经发表过的那条 bug 说明作为范例。收尾这个模块,也收尾整个 dv-methodology 赛道,把从 spec 到计划、到覆盖率、到测试、到 bug、到书面记录、再回到计划的整个环追一遍。
第 3 章以一个确认过的根因和一个最小、有针对性的修复收尾。那其实还不是一次调试的真正终点——一个没有人写下来的修复只是一份没有附带记忆的 diff,等着六个月之后被某个人(很可能就是同一个人)用更痛苦的方式重新发现一遍。这最后一章同时做两件事:怎么把一个 bug 写好,以及——因为这是最后一个模块的最后一章——三个读起来像是三个独立主题的模块,原来是同一种能力,在同一个循环里的三个不同位置分别被用到而已。
一份写得好的 bug 报告长什么样
uvm-advanced 第 1 章早就发表过一份了,就在引入 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——根本没有稳定解,正是 spec(A3.2)里明确写的"输入和输出信号之间不能有组合路径"这条规则要防止的情况。上面这版只依赖寄存器状态(aw_have/w_have/s_axi_bvalid),结果发现这已经足够防止重叠了。这个教训能推广到这一个信号之外:一个"感觉上"应该对"刚刚是不是发生了一次传输"做出反应的握手 ready 信号,天然就是容易不小心闭合出组合环的地方——因为"刚刚是不是发生了传输"这件事,往往正是从这同一个 ready 信号算出来的。
六样东西,按顺序排列,各自做着不同的工作:
- 一开始看起来合理的地方——那个有问题的版本自己的理由,公平地陈述出来,不是一个稻草人:"当前这次写一旦触发,就不接受新的 AW/W"听起来是一个讲得通的意图。
- 根因——精确点名的那条依赖链:
write_fire依赖aw_fire,aw_fire又依赖s_axi_awready。 - 为什么光读代码看不出来——"真正跑一遍仿真(而不只是读代码)立刻就暴露了问题"。这个 bug 成功地躲过了代码审查;它没能躲过仿真器。
- 对照 spec 落地——不只是"这里坏了",而是它违反了哪一条具体的规则(A3.2),可以被引用,不是凭空断言的。
- 修复方案——平实地陈述出来,而且精确说明了它为什么够用,不只是说它碰巧管用了。
- 可以推广的教训——那句能活得比这一个信号更久的话:任何一个从"刚刚是不是发生了一次传输"算出来的握手 ready 信号,在这个 DUT 或者任何别的 DUT 上,都是结构上类似的陷阱。
这六样里的每一样都在做真正的工作。少了任何一样,这份报告的质量都会明显下降:去掉"一开始看起来合理的地方",下一个读者就没办法明白当初为什么会这么写;去掉可以推广的教训,这份报告就只保护了一个信号、一个 DUT,仅此而已,永远如此。
为什么要把它写下来
Test-Planning 第 1 章为验证计划提出过的那个论据,在这里换了一个对象,同样成立:一份计划让作者之外的人能在代码写出来之前核实策略;一份 bug 报告让发现者之外的人能在同样的错误再犯一次之前学到这个教训。一个没有附带任何说明的修复,一旦没有人还记得背后的来龙去脉,就等于不存在——diff 只说明了改了什么,从来不说明为什么要改,而下一个工程师看着另一个恰好也引用了"刚刚是不是发生了一次传输"的 ready/valid 信号时,完全没有办法知道这个具体的陷阱其实早就有名字了。
闭合这个环
三个模块从头到尾读下来,原来追的是同一个循环,而不是三个互不相干的主题:
Spec → 计划。 Test-Planning 第 2 章把 axil_regfile 的 spec 拆解成了一份功能清单;第 3 章系统地生成场景,而不是想到什么写什么;第 4 章按风险给它们排了序。AXI4-Lite 的 A3.2 规则——输入输出信号之间不能有组合路径——也是同一份 spec 的一部分,不过它的形状值得诚实地说清楚:它是对RTL 该怎么搭的一条结构性约束,不是一个定向测试能像打一次寄存器写操作那样去驱动、去检查的激励场景。不是每一条 spec 要求都能整整齐齐地套进第 2 章的功能清单格式里;这正是一个套不进去的真实例子,假装它能套进去,会是一堂比直接点名这个例外更差的课。
计划 → 搭建 → 测量。 uvm-advanced 第 1 章搭出了 axil_regfile;Coverage 模块一章一章地,对照真实的代码,测量了实际测到了什么。
测量/搭建 → 诊断。 真正仿真这个 DUT——而不只是读它——正是这样才把这个组合逻辑环真正暴露出来的,恰好就是这个模块第 1-3 章点名过的那套纪律:隔离出那条依赖链,让仿真器自己的调度器充当插桩,对那个具体的环形成假设,用能去掉它的最小改动来确认。
诊断 → 写下来。 上面引用的那条说明——早就发表出来了,只要还有人在读,它就一直在做它该做的事。
写下来 → 回到计划里。 这一步最容易被跳过,也是这一章能拿出具体东西、而不只是讲讲理论的地方:那条说明里可以推广的教训——一个从"刚刚是不是发生了一次传输"算出来的握手 ready 信号是一个结构性陷阱——应该变成这个网站以后任何一个新 DUT 的 ready/valid 逻辑都要对照检查的一条常设评审条目,而不是继续只当一个信号、一章内容里的历史脚注。这正是"回到计划里"在这里的具体意思:不是这一章硬说自己又找到了一个新 bug(这个模块真实的历史里没有任何东西支持编造一个),而是一个已经真实赚到的教训,变成了下一个设计在重新赚一次同样的教训之前就会被拿去核对的一份清单。
那整个循环——spec、计划、搭建、测量、诊断、写下来、回到计划里——正是"覆盖率驱动的判断力"、"验证计划思维"、"调试直觉"原来的样子:同一种底层能力的三个名字,从它这个循环里三个不同的位置看过去而已。这个赛道的三个模块,没有一个需要靠编造例子来教这件事。axil_regfile 自己的历史里,早就有一个现成的了。
小结
- 一份写得好的 bug 报告有六个部分:一开始看起来合理的地方、精确的根因、为什么光读代码看不出来、违反了 spec 里哪一条规则、修复方案以及它为什么够用、还有一个能推广到这一个信号之外的教训。
- 把一个 bug 写下来是一个可传递性的论据,跟 Test-Planning 第 1 章为什么要把计划写下来是同一个道理:它让发现者之外的人能学到这个教训,而不是等着同一个错误被重新发现一遍。
- 不是每一条 spec 要求都能整整齐齐地套进一个功能清单场景里——AXI4-Lite 不能有组合逻辑环这条规则(A3.2)是对 RTL 本身的一条结构性约束,不是一个定向测试能驱动的激励,诚实地点名这个例外,比硬把它塞进一个不合适的形状里要好。
- "回到计划里"不需要编造一个新 bug——这个组合逻辑环可以推广的教训,变成了以后任何一个 ready/valid 信号都要对照检查的一条具体评审条目,把一次事件的教训变成了一道常设的保障。
- 覆盖率驱动的判断力、验证计划思维、调试直觉,是同一种能力在同一个循环里三个不同位置的应用——spec、计划、搭建、测量、诊断、写下来、回到计划里——不是三种碰巧共用了同一个 DUT 的独立技能。
那条组合逻辑环的说明里写道“真正跑一遍仿真,而不只是读代码,立刻就暴露了问题”。这句话在那六个组成部分里,起的是哪一部分的作用?
除了修好一个 bug 之外,这一章主张把它写下来还有什么意义?
这一章“回到计划里”这一步,并没有主张在 axil_regfile 里又找到了一个新 bug。它提议的是什么?
读完 dv-methodology 的全部三个模块(Coverage、Test-Planning Mindset、Debugging Intuition)之后,这一章收尾时主张它们之间真正的关系是什么?