第 2 章 · 共 10 章
UVM 组件树与 Phase 机制
uvm_component 如何在 class/extends 之上搭出一套标准化的类层次结构,UVM 按固定顺序执行 build_phase/connect_phase/run_phase 的机制,控制测试何时结束的 objection 机制,以及 UVM 的报告宏。
第 1 章提出的第一个差距很具体:SV 基础系列 capstone 里的 driver 和 generator() 只是一个类和一个 task,没有统一的命名规则,也没有任何工具能遍历它们做报告或调试。这一章正是 UVM 用来补上这个差距的地方——uvm_component,几乎所有 UVM testbench 结构性部件的基类。
uvm_component:一套标准化的 class
uvm_component 是一个 class,用的是 SV 第 10 章(oop-classes)介绍的关键字,继承方式也和 SV 第 11 章(oop-inheritance-polymorphism)教的一样:
class my_driver extends uvm_component;
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass每个 uvm_component 的构造函数都固定接收两个参数:name(字符串,这个实例的名字)和 parent(指向拥有它的组件的句柄,顶层组件传 null)。super.new(name, parent) 把这两个参数交给 uvm_component 自己的构造函数,真正搭建组件树的正是它——它记录 parent,并把这个实例的 name 拼接到 parent 自己路径的末尾,构造出像 uvm_test_top.env.agent.driver 这样的完整层次化名字。这就是第 1 章第一条承诺的全部兑现:只要继承 uvm_component 并正确调用 super.new(),每个组件都能免费获得一个名字和在标准化层次结构中的位置——不需要自己发明命名规则。
Phase 机制:UVM 按什么顺序执行什么
第 1 章的 capstone 用 initial(SV 第 8 章,always-blocks)和 fork...join(SV 第 14 章,interprocess-communication)手动安排 testbench 的执行顺序:先构造 driver,再用 fork 把 generator 和 driver 的 run() task 并发起来。UVM 用 phasing(分阶段执行) 替代了这种手动排序:一组固定的方法,UVM 自己会按固定顺序调用组件树上的每一个组件。你不需要主动调用这些方法——你只需要重写它们,UVM 会替你调用:
| Phase | 类型 | 典型用途 |
|---|---|---|
build_phase | function | 构造子组件、读取配置 |
connect_phase | function | 连接兄弟组件之间的 TLM 端口 |
run_phase | task | testbench 的真正行为——驱动、监测、检查 |
(还有几个 phase 存在——run_phase 之前的 end_of_elaboration_phase、start_of_simulation_phase,以及之后的 extract_phase/check_phase/report_phase/final_phase——分别用于精细化阶段的准备工作和跑完之后的收尾。这个系列不会直接用到它们,这里提一下只是为了以后在别处看到这些名字时不会觉得陌生。)
这些 phase 不会自己跑起来——顶层 module 必须调用 run_test() 才能真正启动整个 phase 引擎,这部分从第 3 章开始讲。
super.build_phase(phase):别忘了调用它
重写 build_phase 很常见——大多数组件都在这里构造自己的子组件。但 uvm_component 自己的 build_phase 也做了实际工作,跳过对它的调用会悄悄破坏这部分工作:
function void build_phase(uvm_phase phase);
super.build_phase(phase); // 永远先调用这一行
// ... 构造子组件、读取配置等 ...
endfunction这个疏漏很容易被忘记,而且不容易被发现——出问题的症状会出现在别的地方(比如某个配置值莫名其妙地没有传到),而不是在调用的地方直接报错。养成的习惯应该是:每个你重写的 phase 方法,第一行都先调用 super.<phase 名>(phase)。
Phase 的方向:build 自顶向下,connect 自底向上
build_phase 是自顶向下执行的——父组件的 build_phase 先于子组件的运行,这样父组件可以在子组件的 build_phase 去查找配置之前,先把配置发布出去。connect_phase 的方向正好相反,是自底向上——子组件先完成连接,这样等到父组件的 connect_phase 运行时,子组件已经有东西可以连了。
这不只是一个无关紧要的细节:它正是第 5 章 config_db 那套模式能生效的根本原因(在任何子组件存在之前,在顶层 set() 的值,到子组件的 build_phase 调用 get() 时一定已经可见)——第 5 章会直接引用这里的结论,不会重新推导一遍。
Objection 机制:run_phase 到底是怎么结束的
run_phase 是一个 task,不是 function——而且通常写成一个没有天然终点的 forever 循环,就像 SV 第 8 章那些 always 风格的进程一样不会自己结束。phase 开始时,每个组件的 run_phase 都会被 fork 成一个并发进程。那到底是什么让这个 phase——以及整个测试——真正结束呢?
答案是 objection 机制:phase.raise_objection(this) 告诉 UVM "先别结束这个 phase",phase.drop_objection(this) 则撤销这个保留。UVM 会在没有任何 objection 的那一刻立刻结束 run_phase:
task run_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("RUN", "starting work", UVM_MEDIUM)
#10;
`uvm_info("RUN", "work done", UVM_MEDIUM)
phase.drop_objection(this);
endtask两种典型的用错方式:
- 从未 raise 任何 objection:每个组件的
run_phase进程其实都被 fork 出来了(它们短暂地存在过),但因为没有任何 objection,UVM 几乎立刻就认为这个 phase 已经满足结束条件,在时刻 0 就把这些进程全部销毁——所以看起来像什么都没跑,尽管run_phase技术上确实启动过。 - raise 了 objection 但从不 drop:这个 phase——以及整个仿真——永远不会结束。
这正是 UVM 对 SV 第 8/14 章 fork...join/disable fork 材料已经非正式提出过的那个问题给出的标准化答案:一个并发进程怎么知道什么时候可以安全地停下来?不需要每个 testbench 自己发明一套答案,组件树上的每一个组件都用同一对 raise_objection/drop_objection。
报告宏:`uvm_info、`uvm_warning、`uvm_error、`uvm_fatal 与详细级别
SV 第 2 章(basic-syntax)讲过按严重级别分级的 $display/$warning/$error/$fatal。UVM 在任何 uvm_component 或 uvm_object 内部都有对应的一套:
| 宏 | 严重级别 | 是否停止仿真 |
|---|---|---|
`uvm_info | Info | 否 |
`uvm_warning | Warning | 否 |
`uvm_error | Error | 否(但会被计入失败) |
`uvm_fatal | Fatal | 是 |
`uvm_info("DRV", $sformatf("driving sel=%0b", sel), UVM_MEDIUM)
`uvm_error("DRV", "unexpected value on the DUT output")`uvm_info 的三个参数分别是 ID(一个简短的标签,通常是组件的角色,比如 "DRV")、消息内容,以及详细级别(verbosity)(UVM_LOW、UVM_MEDIUM 或 UVM_HIGH)。相比 SV 第 2 章的纯 $display 系列,这些宏多出了两样东西:
- 报告组件的完整层次化名字会自动包含在消息里——这正是本章命名树材料的直接回报。在
uvm_test_top.env.agent.driver内部调用的`uvm_info,会自动打印出这个路径,不需要你自己去拼接。 - 详细级别可以在运行时过滤输出,不需要改代码。 每条
`uvm_info消息只有在它的详细级别不超过当前运行配置的阈值时才会打印——在仿真器命令行加上+UVM_VERBOSITY=UVM_HIGH就能看到全部消息,或者保持默认(UVM_MEDIUM)看到较少的内容。
小结
uvm_component是一个class(SV 第 10 章),用extends继承(SV 第 11 章);它的构造函数固定接收name和parent,UVM 正是靠这个免费搭出一套标准化、完整命名的组件树。- UVM 会按固定顺序在每个组件上调用
build_phase、connect_phase、run_phase(以及几个不太常用的 phase),而不是让你用initial/fork...join(SV 第 8/14 章)手动排序。你重写这些方法,UVM 负责调用。 - 重写任何 phase 方法时,第一行永远先调用
super.build_phase(phase)(或对应 phase 的 super 调用)——跳过它会悄悄破坏父类自己的初始化逻辑。 build_phase自顶向下执行,connect_phase自底向上执行——第 5 章的config_db模式依赖这个执行顺序。run_phase是一个task,通常写成没有天然终点的forever循环。phase.raise_objection(this)/phase.drop_objection(this)控制它(以及整个测试)何时真正结束——零 objection 意味着 phase 在时刻 0 就结束,什么有意义的事都还没发生;objection 从不 drop 意味着它永远不结束。`uvm_info/`uvm_warning/`uvm_error/`uvm_fatal是 UVM 版本的 SV 第 2 章严重级别分级打印,多了两样东西:报告组件的层次化名字自动包含在内,`uvm_info的详细级别可以在运行时用+UVM_VERBOSITY过滤,不需要改代码。
每个 uvm_component 的构造函数固定接收哪两个参数?UVM 用它们做什么?
某个组件重写了 build_phase,但从未调用 super.build_phase(phase)。最准确的描述是?
一个 run_phase task 从未调用过 phase.raise_objection(this)。会发生什么?
要打印一条严重到应该立刻停止仿真的消息,应该用哪个宏?(小写,不带反引号)