第 4 章 · 共 10 章
uvm_object 与事务(Transaction)
uvm_object 与 uvm_component 之间真正的关系(不只是它们的区别),每个 transaction 实际继承的 uvm_sequence_item 基类,以及手写 do_copy/do_compare/convert2string 而不是用 field 宏。
第 1 章已经做过一个简单的对比:uvm_component 有 phase、需要 parent;uvm_object 没有,通常只需要 name。这没错,但这么说容易让人以为它们是两个毫不相干的类别——其实不是。这一章讲它们之间真正的关系,以及自己动手写一个 uvm_object 是什么样子:一个事务(transaction)。
真正的关系:uvm_component 建立在 uvm_object 之上
uvm_component 不是 uvm_object 的平级关系——它是建立在 uvm_object 之上的:
uvm_object
└── uvm_report_object
└── uvm_component
第 2、3 章里你写的每一个 uvm_component,都已经免费继承了 uvm_object 提供的一切。具体列出来就是:create()(通过 factory,第 3、6 章)、clone()/copy()(这一章)、compare()、print(),以及一个 name。还有一个值得知道存在、尽管这个系列不会用到的方法:record(),它把一个 transaction 送进一个记录数据库,供波形查看器一类的工具和信号一起显示——这是一个调试/工具类功能,属于 dv-methodology 的范围,不是这个系列的。中间这层 uvm_report_object,正是让 uvm_component 拥有第 2 章那些 `uvm_info/`uvm_error 等报告能力的地方。uvm_component 在这之上又加了结构性的东西:一个 parent 句柄,以及在 phase 驱动的仿真生命周期里的一个固定位置。
所以真正的问题不是"谁的功能更多"——uvm_component 永远更多,因为它建立在 uvm_object 之上。真正的问题是在组件树里占一个永久位置、参与 phase 驱动的生命周期,到底要付出什么代价,以及什么时候这个代价不值得付。
这层关系带来的两个具体差异
这不只是一条抽象的继承链——它会在两个你实际会遇到的地方体现出来:
print()对组件会遍历整棵树,对象则不会。print()是从uvm_object继承来的,所以每个uvm_component也都有——但在组件上调用它,做的事情比只显示这一个对象的字段要多。因为uvm_component会追踪自己的子组件(这正是第 2 章那套parent记录机制的用途),内置的打印器会递归遍历你调用print()的那个组件下面的整棵子树。env.print()会同时显示 driver、monitor、scoreboard 的字段——而不只是env自己的(它通常也没有什么自己的字段可显示)。而在一个mux_transaction上调用print(),你只会看到这一个 transaction 的sel/a/b字段,因为uvm_object一开始就没有子节点可以遍历。- 注册宏决定了
create()的签名。 用`uvm_object_utils注册的类型,create()是SomeType::type_id::create(name)——没有parent参数,因为uvm_object本来就没有这个东西。用`uvm_component_utils注册的类型,create()是SomeType::type_id::create(name, parent),对应的正是第 3 章就依赖的那个双参数构造函数。给一个类用错宏(比如给一个本该是uvm_component的东西用了`uvm_object_utils),不会给你一个说明原因的报错——它会以别处一条令人费解的、关于参数缺失或多余的编译错误的形式冒出来,值得认清它实际上是什么问题。
什么时候该用纯粹的 uvm_object——以及为什么这是一条硬性规则,不只是代价问题
在 UVM 自己的通用词汇里,这个区分是有名字的:uvm_component 是静态(static)的——只构造一次,在整次运行中身份和树里的位置都固定不变;uvm_object 是动态(dynamic)的(也叫瞬态/transient)——需要多少次就创建、销毁多少次,没有任何固定的位置。这正是你在别处(Verification Academy、大多数 UVM 参考资料)会看到用来描述这同一个区分的术语;这一节接下来讲的是为什么这是对的,而不只是一条要背下来的定义。
一个 uvm_component 在组件树里的位置不只是簿记——phase 引擎真的依赖这棵树在 elaboration 结束之后保持固定。UVM 的 phase 机制假设整个层次结构在 build_phase/connect_phase 跑完之后就是完整的:打印 testbench 的拓扑结构,以及 connect_phase 依赖的那个自底向上的执行顺序(第 2 章),都假设这棵树之后不会再变形状。在 run_phase 期间构造一个新的 uvm_component——比如每个 transaction 都构造一个——不只是浪费,它是在和 phase 引擎真正依赖的一个假设对着干。这是一条真实的 UVM 规则,不是风格偏好:组件只在 build_phase 期间构造,别的地方都不行。
这正是"在树里占一个永久位置很浪费"这句话更尖锐的版本:不只是浪费,一个在 build_phase 之外构造出来的 uvm_component,做的是这套方法学根本不支持的事情。纯粹的 uvm_object 没有这个限制——每驱动一个 item 就新生成的激励事务、一个 sequence、一个短暂传递的小配置对象,都可以在 run_phase 期间任意时刻创建和丢弃,需要多少次都行,正是因为它们一开始就从没加入过那棵固定的树。
Transaction 具体是什么:uvm_sequence_item,不是纯 uvm_object
一个激励类不会直接写成 extends uvm_object——它会再具体一层:
uvm_object
└── uvm_transaction
└── uvm_sequence_item
uvm_sequence_item 就是 uvm_object 加上 sequencer 需要用来追踪一个 item 在 sequencer/driver 握手过程中状态的那些簿记信息(第 7 章会直接用到这个)。这里先把这条继承链点明,是为了让第 7 章不需要在讲到一半时再引入一个新的基类——到那时 uvm_sequence_item 已经是熟悉的东西了。
写一个 transaction:mux_transaction
class mux_transaction extends uvm_sequence_item;
`uvm_object_utils(mux_transaction)
rand bit sel;
rand bit a;
rand bit b;
function new(string name = "mux_transaction");
super.new(name);
endfunction
function void do_copy(uvm_object rhs);
mux_transaction rhs_;
if (!$cast(rhs_, rhs))
`uvm_fatal("DO_COPY", "rhs is not a mux_transaction")
super.do_copy(rhs);
sel = rhs_.sel;
a = rhs_.a;
b = rhs_.b;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
mux_transaction rhs_;
if (!$cast(rhs_, rhs)) return 0;
return super.do_compare(rhs, comparer) &&
(sel == rhs_.sel) && (a == rhs_.a) && (b == rhs_.b);
endfunction
function string convert2string();
return $sformatf("sel=%0b a=%0b b=%0b", sel, a, b);
endfunction
endclass几点值得和你已经掌握的知识对上号:
uvm_object的构造函数只接收name——没有parent,和uvm_component不一样。这就是第 1 章那句区别最直接的证据。do_copy/do_compare用$cast把通用的uvm_object rhs参数向下转型成mux_transaction,然后才能访问它的字段——和 SV 第 11 章(oop-inheritance-polymorphism)教的向下转型模式完全一样,只是这次用在 UVM 自己的基类上。do_copy正是 SV 第 10 章手写copy()的标准化版本。 SV 第 10 章教你写一个copy()方法,new()一个新对象、把每个字段复制过去,并且直接说过"UVM 的uvm_object::copy()/clone()正是这个想法的标准化版本"。这一章就是那个承诺的兑现:你只写do_copy()(只负责复制字段的那部分),uvm_object自己继承来的copy()——不是你写的——负责其余部分。要得到和 SV 第 10 章copy()一样的效果(一步分配新对象并复制进去),调用clone()并对结果做$cast:
mux_transaction tr2;
$cast(tr2, tr1.clone());convert2string()取代了散落在 driver 代码各处的手写$sformatf调用——把格式化逻辑写一次,之后任何需要打印一个 transaction 的地方(包括`uvm_info里)都调用tr.convert2string()。
Field 宏 vs 手写方法:这个系列用哪一种
你会在别的地方看到用 field 宏写的 transaction 类:
`uvm_object_utils_begin(mux_transaction)
`uvm_field_int(sel, UVM_ALL_ON)
`uvm_field_int(a, UVM_ALL_ON)
`uvm_field_int(b, UVM_ALL_ON)
`uvm_object_utils_end这会从一份字段列表自动生成 do_copy、do_compare、convert2string 等方法,而不用逐个手写。它在现有/较老的 UVM 代码里很常见,遇到时应该认得出来——但它也更不直观(自动生成的 convert2string() 输出格式不是你自己选的),在较新的代码库里,用它的场景已经在减少,转而更倾向于手写方法。这个系列使用手写的 do_copy/do_compare/convert2string,原因和 SV 第 10 章教手写 copy() 而不是依赖捷径一样:这样每个方法的具体行为都能在代码里一眼看到,而不是从一份宏列表生成出来的。(和 SV 第 2 章 `timescale vs timeunit/timeprecision 的处理方式是同一类决定——先说明较老、仍然常见的写法,然后往后都用更直观的那种。)
用起来:driver 驱动 transaction,而不是裸信号
第 3 章的 driver 直接用写死的字面量给 vif.sel/vif.a/vif.b 赋值。把它换成 mux_transaction 对象:
task run_phase(uvm_phase phase);
mux_transaction tr;
phase.raise_objection(this);
tr = new();
tr.sel = 0; tr.a = 1; tr.b = 0;
drive(tr);
tr = new();
tr.sel = 1; tr.a = 1; tr.b = 0;
drive(tr);
phase.drop_objection(this);
endtask
task drive(mux_transaction tr);
vif.sel = tr.sel;
vif.a = tr.a;
vif.b = tr.b;
#1;
`uvm_info("DRV", $sformatf("%s -> y=%0b", tr.convert2string(), vif.y), UVM_MEDIUM)
endtask还是第 3 章那两组写死的组合——激励本身还没变,变的只是它被表示的方式。这个区别很重要:一旦激励变成一个结构化对象,而不是几个零散的局部变量,它就已经准备好成为 sequence(第 7 章)生成、再交给 driver 的东西,而不再是 driver 自己凭空造出来的东西。
小结
uvm_component不是uvm_object的平级关系——它建立在uvm_object之上(uvm_object→uvm_report_object→uvm_component),免费继承了命名、print()/copy()/compare()、factory 注册,然后再加上parent句柄和在 phase 驱动生命周期里的位置。print()对uvm_component会递归遍历整棵子树(因为它追踪子组件),对纯粹的uvm_object则只显示这一个对象自己的字段;注册宏(`uvm_object_utils还是`uvm_component_utils)决定了type_id::create()只接收name,还是同时接收name和parent。uvm_component是静态的(只构造一次,身份和树里的位置固定),uvm_object是动态/瞬态的(需要多少次就创建、销毁多少次)——这是 UVM 的标准词汇,背后是一条有实际约束力的规则:组件必须在build_phase期间构造,别的地方都不行,因为 phase 引擎的一些保证(比如connect_phase自底向上的执行顺序)依赖这棵树在 elaboration 结束之后保持固定。- transaction 真正的基类是
uvm_sequence_item(uvm_object→uvm_transaction→uvm_sequence_item),它加上了 sequencer/driver 握手(第 7 章)需要的簿记信息。 do_copy/do_compare用$cast(SV 第 11 章)把通用的uvm_object参数向下转型之后才能访问字段;do_copy是 SV 第 10 章手写copy()的标准化版本,clone()加$cast则最接近那个copy()方法一步到位的效果。- 这个系列选择手写
do_copy/do_compare/convert2string,而不是用 field 宏(`uvm_field_int等)——它们在现有代码里很常见,但不如手写方法直观。
下面哪个最准确地描述了 uvm_object 与 uvm_component 之间真正的关系?
调用 env.print()(env 是一个 uvm_component)会连带显示它下面 driver、monitor、scoreboard 的字段,不只是 env 自己的。为什么 tr.print()(tr 是一个 mux_transaction)不会有类似的效果?
为什么 UVM 坚持 uvm_component 只能在 build_phase 期间构造,而不能比如说在 run_phase 里每个 transaction 构造一个?
为什么 transaction 类要写成 extends uvm_sequence_item,而不是直接 extends uvm_object?
在 do_copy(uvm_object rhs) 内部,为什么访问 rhs 的字段之前需要先 $cast?
哪个方法调用能一步得到一个全新、独立的 transaction 副本(最接近 SV 第 10 章手写 copy() 方法的效果)?(小写,不带括号)