[UVM] Phase 机制详解为什么组件的生命周期要切成这么多段导读有次调试一个环境启动顺序的问题同事问了一句“为什么不能就在构造函数里把所有东西都建好、连好非要分成 build、connect、run 好几个阶段多绕一圈”这个问题问到了 UVM 框架一个很核心的设计动机。如果不理解 phase 机制解决的是什么问题写起验证环境来很容易在这段逻辑该放哪个阶段这件事上凭直觉瞎蒙蒙对了万事大吉蒙错了就会遇到一些看起来很诡异、其实是时序问题的 bug。这篇文章把 phase 机制的设计逻辑理清楚。一、为什么需要把生命周期切成阶段一个验证环境里有很多组件driver、monitor、scoreboard、各种 agent它们之间存在复杂的依赖关系——某个组件需要先拿到父组件传下来的配置才能决定要不要创建子组件某个组件需要先确认所有其他组件都已经创建完毕才能安全地建立起相互之间的连接引用。如果所有这些工作都堆在构造函数里做会立刻撞上一个根本性的时序问题构造和配置在语言层面是两个先后发生的动作构造函数执行的时候外部还没来得及把配置传进来。如果构造函数里还想着顺便把连接关系也建好会遇到同样的问题——你没法保证在自己构造的那一刻环境里其他组件也都已经构造完毕、可以被引用了。phase 机制的本质就是把验证环境从无到有、再到运行、再到收尾这个完整过程拆分成若干个先后顺序严格确定的阶段规定每个阶段该做什么、不该做什么用这种分阶段的方式解决组件之间错综复杂的依赖顺序问题。二、三大阶段族build-time、run-time、cleanupUVM 把整个仿真生命周期划分成三个大类每个大类里又包含若干个具体阶段。build-time 阶段族负责搭建验证环境的静态结构创建组件、把配置传递下去、建立组件之间的连接引用、做一些一次性的检查。这个阶段族的显著特点是零仿真时间——这里发生的所有事情都发生在仿真时钟的第 0 个时刻之前不存在等待这个概念一个组件的这个阶段做完才会轮到下一个组件做同样的阶段。run-time 阶段族是真正跑仿真的地方驱动激励、采样信号、检查结果都发生在这里。和 build-time 阶段族最大的不同是run-time 阶段族里的各个组件是并发执行的彼此之间不存在谁先做完谁再做的顺序关系而是大家同时开始跑、各自按自己的节奏往前推进仿真时间。cleanup 阶段族负责收尾确认所有该检查的都检查完了、统计信息该汇总的都汇总了、报告该打印的都打印出来。这个阶段族又变回了和 build-time 类似的零仿真时间、顺序执行的模式。三、build-time 内部的执行顺序为什么是自顶向下又是自底向上build-time 阶段族内部还分成好几个具体阶段每个阶段的遍历方向并不一样这个方向恰好是根据每个阶段要解决的问题反推出来的。创建组件的阶段是自顶向下执行的先是最外层的顶层环境执行这个阶段、创建自己的直接子组件然后依次往下子组件再创建自己的子组件一层一层往下铺开。这个顺序的道理很直接——父组件必须先存在才谈得上创建子组件配置信息也是父组件往下传子组件在创建自己、乃至创建更下一层子组件之前需要能拿到父组件已经准备好的配置。建立连接引用的阶段则是自底向上执行的最底层的叶子组件最先执行这个阶段然后逐层往上最后才轮到顶层环境。这个顺序也有清楚的道理——建立连接引用这件事前提是对方已经存在如果自顶向下做连接父组件想去引用子组件的时候子组件可能还没创建出来反过来自底向上等轮到某一层做连接的时候比它更深的所有子组件都已经在创建阶段里构造完毕引用关系自然是安全的。这两个方向合在一起看其实是同一条设计原则的两种体现每个阶段的遍历方向都服务于这一步真正需要依赖的东西此刻是否已经就绪这个约束。四、run-time 阶段族从谁先谁后到大家一起跑进入 run-time 阶段族之后执行模型发生了根本性的转变。build-time 阶段族里一个组件做完才轮到下一个的顺序关系彻底消失取而代之的是所有组件的 run 阶段几乎同时启动各自独立地往前推进仿真时间互不等待。这个设计对应的现实很直观真实的硬件系统里各个模块本来就是同时在运行的某个模块的时钟沿到来不会因为另一个模块还没处理完上一个事务就被迫停下来等待。如果 run 阶段还沿用 build-time 那种排队执行的模式就没办法真实模拟这种并发行为——driver 在驱动激励的同时monitor 需要同步在观察信号两者如果变成先后关系根本没法反映真实场景。run-time 阶段族内部其实还细分成好几个子阶段比如复位阶段、主体运行阶段、收尾等待阶段这些子阶段之间保留了顺序关系——所有组件必须一起做完复位阶段才会一起进入主体运行阶段——但同一个子阶段内部各个组件之间依然是并发的。这种阶段之间顺序、阶段内部并发的设计让环境既能在关键时间点上做到步调一致比如所有组件同时知道复位已经结束又不会牺牲仿真该有的并发真实性。五、组件如何声明我的 run 阶段还没结束run-time 阶段族并发执行带来一个新问题框架怎么知道什么时候可以结束当前这个阶段、进入下一个阶段如果没有一种机制来协调某个组件可能自己的事务还没做完阶段就被提前结束了。UVM 提供了一种举手机制来解决这个问题任何组件都可以声明我要objection反对当前阶段结束声明之后框架会等待直到所有声明过的组件都撤回自己的反对才会真正推进到下一个阶段。这个机制本质上是一种基于计数的同步原语——反对的数量从零变成正数阶段就会被阻塞数量重新归零阶段才会继续往前走。这也是为什么在实践中“忘记声明反对和声明了反对却忘记撤回是两类最常见、又截然相反的问题前者会导致某个组件的激励还没发完仿真就已经进入下一个阶段甚至直接结束后者会导致仿真永远停在当前阶段看起来像是卡死了”实际上是有一个反对一直没有被撤回。六、验证中值得关注的几个点不要在构造函数里做依赖父组件配置的判断这是最常见的一类时序问题构造函数执行时配置还没被传递下来任何需要读取父组件配置才能决定的逻辑都应该挪到 build-time 阶段族里执行而不是塞进构造函数。连接引用类的代码要放在正确的阶段而不是提前如果在创建组件的阶段就尝试引用兄弟组件或者子组件的引用大概率会拿到一个空引用因为对方可能还没有被创建出来这类代码应该放在连接阶段此时创建阶段已经全部完成。跨组件同步不要依赖运气run-time 阶段族里各组件默认是并发的如果两个组件之间存在时序上的先后依赖比如一个组件要等另一个组件完成某个动作才能继续不能假设它们碰巧会按预期的顺序执行必须用显式的同步机制去协调。反对机制要配对检查审查代码时每一处声明反对的地方都要能找到对应的撤回路径尤其是存在多个分支、异常退出路径的场景容易出现某条路径下反对被声明了、却没有被撤回这类问题会导致仿真在某些场景下才卡死回归里不一定每次都能复现。排查启动顺序问题时先确认组件当前处于哪个阶段很多看起来诡异的初始化 bug本质上是代码被放在了错误的阶段——排查这类问题的第一步不是急着看变量值对不对而是先确认当前执行到的是哪个阶段、这个阶段理论上应该已经具备了哪些前提条件。七、总结phase 机制解决的核心问题只有一个验证环境里的组件存在复杂的相互依赖关系需要一套严格定义的顺序来保证每一步执行的时候它所依赖的前提条件确实已经就绪。build-time 阶段族用顺序执行、方向明确的遍历创建自顶向下、连接自底向上来搭建静态结构run-time 阶段族转向并发执行真实反映硬件系统里各模块同时运行的现实再用反对机制来协调什么时候可以结束当前阶段这个问题cleanup 阶段族收尾重新变回顺序执行。理解了每个阶段族存在的原因、以及它们各自的执行顺序服务于什么约束遇到这段代码到底该放哪个阶段这类问题时就有了一个可以推导的依据而不必凭经验硬记。