UVM 基础

第 6 章 · 共 10 章

Factory 机制:注册、create() 与替换

`uvm_component_utils`/`uvm_object_utils` 到底注册了什么,为什么 type_id::create() 是一次查找而不是单纯构造一个对象,以及 set_type_override() 如何在不碰结构代码的情况下替换实现——补上第 3 章埋下的那笔最早的欠条。

第 3 章用过 `uvm_component_utils(my_driver)my_driver::type_id::create("drv", this),并承诺以后会解释它们。这一章就是那个承诺的兑现——同时也是第 1 章第二条承诺的直接兑现:一种标准化的方式,不碰结构性代码就能替换某个实现。

这个宏到底注册了什么

`uvm_component_utils(my_driver)(以及它的对象版本 `uvm_object_utils,第 4 章的 mux_transaction 已经用过)做了两件事:

  1. 声明一个嵌套的 type_id——一个知道怎么专门构造 my_driver 的小助手类型。
  2. my_driver 注册到 factory:一张全局的、整个仿真共享的表,把"某个类型(或名字)"映射到"实际应该构造出什么"。

任何能通过 type_id::create() 构造的 UVM 类,都必须先经过这次注册——正是它让接下来的事情成为可能。

type_id::create():一次查找,不只是构造调用

my_driver::type_id::create("drv", this) 做的事情,不只是像 new("drv", this) 那样单纯构造一个 my_driver。它向 factory 提了一个问题:"现在有人要一个 my_driver,实际应该构造出什么?" 通常情况下,答案就是 my_driver 自己——所以 create() 的行为和 new() 完全一样。但这个答案是可以被改变的,全局生效,而且不需要修改调用 create() 那行代码的任何一个字。

这正是第 3 章一开始就用 create() 而不是 new() 的全部原因:new() 永远构造你写下的那个确切类型。create() 构造的是 factory 当前说该构造的类型——这正是下一节要讲的东西成为可能的原因。

替换:set_type_override()

class my_driver_alt extends my_driver;
  `uvm_component_utils(my_driver_alt)
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  task drive(mux_transaction tr);
    `uvm_info("DRV_ALT", $sformatf("(alt driver) %s", tr.convert2string()), UVM_MEDIUM)
    super.drive(tr);
  endtask
endclass

my_driver_alt 只重写了第 4 章的 drive() task——其余的东西(build_phase 里的 config_db::get()run_phase)都原样继承,super.drive(tr) 依然做真正的工作,重写只是多加了一行日志来宣告自己的存在。现在,在 test 的 build_phase 里、构造 drv 之前,告诉 factory 用它来代替 my_driver

function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  my_driver::type_id::set_type_override(my_driver_alt::get_type());
  drv = my_driver::type_id::create("drv", this);
endfunction

get_type()`uvm_component_utils 自动生成的另一个方法——它返回一个 factory 用来识别类型的句柄。这一行代码执行之后,testbench 里任何地方的 my_driver::type_id::create(...) 调用——不只是这一处——都会构造出一个 my_driver_altdrv 声明的类型依然是 my_driver,构造 driver 的代码也一个字都没变;变的只是它底下实际构造出来的东西。把那一行删掉,drv 就变回一个普通的 my_driver——不管哪种情况,别处的代码都不用跟着改。

这正是第 1 章第二条承诺的直接兑现:capstone 那个手写的 driver 类只能通过直接修改代码来改变行为。这里,为某个 test 替换行为只需要一行代码,只出现在一个地方,而且不碰 my_drivermy_test,或者任何其他结构性代码。

和 SV 第 11 章多态是同一个想法,只是查找方式不同

SV 第 11 章(oop-inheritance-polymorphism)已经讲过通过继承来替换行为:一个基类句柄可以指向一个子类对象,调用一个 virtual 方法时,实际执行的是句柄当时真正指向的那个子类版本。factory 背后是同一个想法——在运行时分发到真正应该执行的那个子类——只是这次是由你配置的一次类型查找来决定,而不是由你恰好为哪个子类写了 newtype_id::create() 本质上就是"由一张你可以从外部修改的表来决定的多态",而不是"由哪一行代码调用了 new 来决定的多态"。

一个更精准的兄弟机制:instance override

set_type_override() 会影响该类型所有create() 调用。有时候只想改变某一个特定实例——为此,set_inst_override() 接收的是一条层次化路径,而不是整个类型:

my_driver::type_id::set_inst_override(my_driver_alt::get_type(), "uvm_test_top.drv");

只有真正在那条确切路径上构造出来的组件才会被替换;树里其他地方任何一个 my_driver::type_id::create(...) 调用都不受影响。这个系列的例子只需要用到全类型的替换,但知道这个更精准的版本存在也是有用的。

小结

  • `uvm_component_utils/`uvm_object_utils 把一个类注册到 factory——一张全局的表,把"被要求构造的类型"映射到"实际该构造的类型"。
  • type_id::create(...) 在真正构造之前会先查这张表;默认情况下答案就是这个类型本身,和 new(...) 完全一样——直到某个替换改变了这个答案。
  • set_type_override()(通常在 test 的 build_phase 早期、在受影响的 create() 调用之前执行)会让之后所有针对某个类型的 create() 调用都改成构造另一个类型,而不需要碰调用 create() 的那些代码。
  • set_inst_override() 做同样的事,但只针对一个特定的实例路径,那个类型的其他实例都不受影响。
  • 这其实还是 SV 第 11 章基于继承的多态,只是这次通过一张可配置的表来分发,而不是由代码里某一行恰好写了哪个子类的 new 来决定。

`` `uvm_component_utils(my_driver) `` 到底做了什么?

my_driver::type_id::set_type_override(my_driver_alt::get_type()); 执行之后,下一次调用 my_driver::type_id::create('drv', this) 会发生什么?

set_type_override() 和 set_inst_override() 在实际使用上有什么区别?

factory 的替换机制和 SV 第 11 章的多态材料是什么关系?