3. UVM核心基类与工厂机制

在UVM中,几乎所有验证组件都派生自两个根基类:uvm_object和uvm_component。理解它们的差异,是掌握工厂机制的前提。简单说,uvm_object是"短命"的、可动态创建和销毁的数据与事务载体;uvm_component是"长命"的、在仿真开始时构建成树状层次结构的结构体。工厂机制正是围绕这两类对象的注册与创建展开的。

两类基类的生命周期差异

uvm_object的生命周期由new()和copy/clone等操作控制,它没有父节点,不参与层次化构建,典型代表是uvm_transaction、uvm_sequence_item、配置对象等。它们可以在仿真任意时刻被创建、复制、比较,用完即弃。而uvm_component在build_phase中通过new(name, parent)挂到组件树上,拥有固定的层次路径,并依次经历build → connect → end_of_elaboration → start_of_simulation → run → extract → check → report → final等phase。组件一旦构建,生命周期贯穿整个仿真,不能随意销毁重建。

这个差异直接决定了工厂的使用方式:组件通过type_id::create()在build阶段创建,事务对象通过type_id::create()在运行时创建。两者都走工厂,但前者受层次结构约束,后者完全自由。

工厂注册:让类型进入"可替换名录"

工厂要能替换类型,前提是类型先完成注册。UVM提供两种注册宏:`uvm_object_utils(T)用于uvm_object派生类,`uvm_component_utils(T)用于uvm_component派生类。注册宏内部会生成type_id、get_type()、create()等静态方法,并把类型登记到全局工厂表中。

class my_txn extends uvm_sequence_item;
  rand bit [7:0] data;
  `uvm_object_utils(my_txn)
  function new(string name = "my_txn");
    super.new(name);
  endfunction
endclass

class my_driver extends uvm_driver #(my_txn);
  `uvm_component_utils(my_driver)
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

注意:注册只是"登记",并不改变原有代码的创建方式。真正让替换生效的,是创建时必须使用type_id::create()而不是直接new()。如果代码里写my_txn t = new();,工厂覆盖就完全失效——这是初学者最常见的坑。

覆盖机制:type_override与inst_override

工厂覆盖分两种粒度。类型覆盖用set_type_override_by_type(original, override),影响所有该类型的创建请求;实例覆盖用set_inst_override_by_type(original, override, "path"),只影响指定层次路径下的实例。覆盖通常在build_phase之前或测试层的build_phase中设置,且必须在目标对象创建之前完成。

// 在测试的build_phase中
my_txn::type_id::set_type_override(err_txn::get_type());
// 或只覆盖env.agent.drv路径下的实例
my_txn::type_id::set_inst_override(err_txn::get_type(),
                                   "env.agent.drv");

覆盖生效的判断依据是:工厂在收到create()请求时,先查实例覆盖表,再查类型覆盖表,找到替换类型后调用其构造函数。若替换类型与原类型不兼容(例如字段不匹配、句柄类型不匹配),会在编译或运行时暴露问题,因此替换类型通常应派生自原类型,保证接口一致。

用替换事务类型验证工厂灵活性

下面用一个可操作的示例说明工厂的灵活性。假设基础事务base_txn只带data字段,我们想在不改动driver和sequence代码的前提下,切换到带crc字段的crc_txn。

class base_txn extends uvm_sequence_item;
  rand bit [7:0] data;
  `uvm_object_utils(base_txn)
  function new(string name="base_txn"); super.new(name); endfunction
endclass

class crc_txn extends base_txn;
  rand bit [7:0] crc;
  `uvm_object_utils(crc_txn)
  function new(string name="crc_txn"); super.new(name); endfunction
endclass

// driver中统一用工厂创建
task run_phase(uvm_phase phase);
  base_txn t;
  forever begin
    seq_item_port.get_next_item(t);
    `uvm_info("DRV", $sformatf("data=%0h crc=%0h", t.data, t.crc), UVM_LOW)
    seq_item_port.item_done();
  end
endtask

在测试层加入一行覆盖:base_txn::type_id::set_type_override(crc_txn::get_type());,然后运行仿真。观察日志:原本只打印data,覆盖后同一driver代码会打印出crc字段,说明工厂在create()时把base_txn请求替换成了crc_txn。若把覆盖改为set_inst_override并指定路径,则只有该路径下的driver看到crc_txn,其他实例仍用base_txn——这正是工厂"按需替换、局部生效"的价值。

判断覆盖是否生效的实用方法:在替换类型的构造函数中加一条`uvm_info("FACTORY", "crc_txn created", UVM_LOW),仿真日志出现该信息即证明工厂走了替换路径;若未出现,优先检查是否误用了new()或覆盖设置晚于创建时刻。
工厂创建与覆盖决策路径 注册宏登记类型 `uvm_object_utils 设置覆盖 type / inst override create() 请求 base_txn::type_id::create 命中覆盖表? 先inst后type 创建替换类型 crc_txn 创建原类型 base_txn driver 句柄仍为 base_txn,实际对象为替换类型 日志打印 crc 字段 → 证明工厂替换生效 是 否

工程实践中的关键约束

工厂虽灵活,但有几条硬约束必须遵守。第一,创建必须走工厂:组件和事务都应用type_id::create(),直接new()会绕过覆盖。第二,覆盖时机:覆盖必须在目标对象创建之前设置,通常放在测试层build_phase的最前面,或使用uvm_config_db在更早阶段传递。第三,类型兼容:替换类型应派生自原类型,保证句柄赋值和虚方法调用安全;若字段扩展,需在driver中通过$cast或基类虚方法访问,避免直接引用子类字段导致编译错误。第四,路径匹配:set_inst_override的路径是相对于调用者的层次路径,写错路径会导致覆盖静默失效,建议用get_full_name()打印实际路径核对。

把这几条落实后,工厂就不再是"背宏"的知识点,而是一个可验证的工程手段:改一行覆盖、看一条日志、确认对象类型变化,就能判断替换是否按预期生效。这也是UVM验证平台可复用、可配置的核心能力之一。