UVM 基础

第 4 章 · 共 10 章

uvm_object 与事务(Transaction)

uvm_object 与 uvm_component 之间真正的关系(不只是它们的区别),每个 transaction 实际继承的 uvm_sequence_item 基类,以及手写 do_copy/do_compare/convert2string 而不是用 field 宏。

第 1 章已经做过一个简单的对比:uvm_component 有 phase、需要 parentuvm_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_copydo_compareconvert2string 等方法,而不用逐个手写。它在现有/较老的 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_objectuvm_report_objectuvm_component),免费继承了命名、print()/copy()/compare()、factory 注册,然后再加上 parent 句柄和在 phase 驱动生命周期里的位置。
  • print()uvm_component 会递归遍历整棵子树(因为它追踪子组件),对纯粹的 uvm_object 则只显示这一个对象自己的字段;注册宏(`uvm_object_utils 还是 `uvm_component_utils)决定了 type_id::create() 只接收 name,还是同时接收 nameparent
  • uvm_component静态的(只构造一次,身份和树里的位置固定),uvm_object动态/瞬态的(需要多少次就创建、销毁多少次)——这是 UVM 的标准词汇,背后是一条有实际约束力的规则:组件必须在 build_phase 期间构造,别的地方都不行,因为 phase 引擎的一些保证(比如 connect_phase 自底向上的执行顺序)依赖这棵树在 elaboration 结束之后保持固定。
  • transaction 真正的基类是 uvm_sequence_itemuvm_objectuvm_transactionuvm_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() 方法的效果)?(小写,不带括号)