UVM 基础

第 3 章 · 共 10 章

搭建并跑通你的第一个 UVM Test

复用 SV 基础系列 capstone 里的 mux2 DUT,把它包进一个最简 UVM 骨架并真正跑起来——一个近乎空的 driver、一个 test,以及把虚接口从 module 世界交接到 UVM 世界的 config_db 握手。

第 2 章讲了 uvm_component、phase 机制、objection、报告宏——全是概念,还没有真正跑起来任何东西。这一章要解决这个问题:还是 SV 基础系列 capstone(第 16 章)里你亲手驱动过的那个 mux2 DUT,现在换成一个真正的 UVM 骨架来驱动,真正跑起来。这一章扮演的角色和 SV 第 3 章(first-simulation)在那个系列里一样——先跑起来点什么,再往深处讲。

开始之前:先选一个支持 UVM 的仿真器

SV 第 3 章告诉你 EDA Playground 上任何免费仿真器(包括 Icarus Verilog)都能覆盖那整个系列。这句话到这里不再成立——Icarus Verilog 不支持 UVM 类库,用它跑这一章的例子会得到一个不知所云的类似"找不到 uvm_pkg"的报错,而不是有帮助的提示。

在 EDA Playground 的 Tools & Simulators 面板里,选一个标注支持 UVM 的仿真器(比如 Aldec 的 Riviera-PRO,EDA Playground 历来把它作为不需要你自己拥有商业授权就能用的选项——具体列表可能会变化,建议以当前页面为准),如果这个仿真器的选项里能选 UVM 库版本,选一个(UVM 1.2 是个安全的默认选择)。从这里开始的所有例子都假设你已经这样设置好了。

DUT 与接口:和 capstone 完全一样

还是第 16 章 capstone 里那个 mux2mux2_if——这里没有新东西,只是重新贴一遍加深印象:

interface mux2_if;
  logic sel, a, b, y;
 
  modport dut_mp (input sel, a, b, output y);
  modport tb_mp  (output sel, a, b, input y);
endinterface
 
module mux2 (mux2_if.dut_mp bus);
  always_comb begin
    bus.y = bus.sel ? bus.b : bus.a;
  end
endmodule

Driver:一个近乎空的 uvm_component

capstone 里的 driver 类持有一个 virtual mux2_if.tb_mp vif,通过构造函数赋值。这里的 driver 持有同样的句柄,但获取方式不一样——通过 uvm_config_db,UVM 把 virtual interface 从纯 SystemVerilog 的 module 世界 交接到 UVM 的 class 世界 的标准方式。这套模式为什么能生效,要等到第 5 章才完整解释;现在先把它当成一个可以直接照抄的标准模式:

class my_driver extends uvm_component;
  `uvm_component_utils(my_driver)
 
  virtual mux2_if.tb_mp vif;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(virtual mux2_if.tb_mp)::get(this, "", "vif", vif))
      `uvm_fatal("DRV", "no virtual interface set for vif -- check the config_db::set() call in the top module")
  endfunction
 
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
 
    vif.sel = 0; vif.a = 1; vif.b = 0;
    #1;
    `uvm_info("DRV", $sformatf("sel=%0b a=%0b b=%0b -> y=%0b", vif.sel, vif.a, vif.b, vif.y), UVM_MEDIUM)
 
    vif.sel = 1; vif.a = 1; vif.b = 0;
    #1;
    `uvm_info("DRV", $sformatf("sel=%0b a=%0b b=%0b -> y=%0b", vif.sel, vif.a, vif.b, vif.y), UVM_MEDIUM)
 
    phase.drop_objection(this);
  endtask
endclass

两个熟悉的东西,加一个新模式:

  • super.build_phase(phase),再调用 config_db::get()——正是第 2 章讲过的那个习惯。get() 返回一个 bit:成功是 1,如果这个路径从没被 set() 过就是 0,这也是为什么它被包在 if (!...) 里配一个 `uvm_fatal——一个虚接口从没被真正赋值、却悄悄驱动出垃圾信号的 driver,是比立刻停下并给出清晰报错糟糕得多的失败方式。
  • run_phase 只做两件事,包在 raise_objection/drop_objection(还是第 2 章)里面:驱动一组 sel/a/b 组合,等一个时间单位让组合逻辑稳定下来(和 capstone 一样的 #1 简化处理),然后用 `uvm_info 打印结果。

Test:只负责构造 driver

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
 
  my_driver drv;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    drv = my_driver::type_id::create("drv", this);
  endfunction
endclass

uvm_test 是 UVM 提供的一个 uvm_component 子类,按惯例作为整个层次结构的顶端——它的行为和你在第 2 章已经认识的任何 uvm_component没有任何不同。my_driver::type_id::create("drv", this) 代替了普通的 new(...) 调用来构造这个 driver——这正是 UVM factory 在起作用,为什么要这样写要等到第 6 章才讲。现在只需要记住一条规则:在 build_phase 内部构造子组件,用 某个类::type_id::create("实例名", this),不要用 new(...)

把它接起来:顶层 module

`include "uvm_macros.svh"
import uvm_pkg::*;
 
module tb_top;
  mux2_if bus_if();
  mux2 dut (bus_if);
 
  initial begin
    uvm_config_db#(virtual mux2_if.tb_mp)::set(null, "*", "vif", bus_if);
    run_test("my_test");
  end
endmodule

任何用到 UVM 的文件,最前面都要有 `include "uvm_macros.svh"import uvm_pkg::*;——正是这两行让 `uvm_infouvm_componentuvm_config_db 以及所有 UVM 提供的东西变得可用。

这个 initial 块按顺序做了两件事:uvm_config_db#(virtual mux2_if.tb_mp)::set(null, "*", "vif", bus_if) 把真正的 bus_if 实例以字段名 "vif" 发布出去,任何来查询的组件都能看到("*" 表示"任意实例路径");然后 run_test("my_test") 启动整个 phase 引擎,构造唯一一个 my_test 作为组件树的根(按惯例叫 uvm_test_top),并按顺序跑完每一个 phase。set() 必须发生在 run_test() 之前——这正是第 2 章"build_phase 自顶向下执行"那条结论的用武之地:等到 my_testbuild_phase 构造出 drvdrv 自己的 build_phase 再去调用 get() 时,值早已经在那里等着了。

这一章还没讲清楚的东西(正好两件)

  • `uvm_component_utils/type_id::create()——这是 UVM 的 factory 机制;第 6 章讲它到底做了什么、为什么重要。
  • uvm_config_db#(...)::set()/get()——第 5 章讲完整的机制:context/路径/字段名这些参数到底是什么意思,以及为什么 build_phase 的执行顺序能保证它生效。

除此之外——`uvm_inforun_phaseraise_objection/drop_objectionsuper.build_phase——全都是第 2 章已经讲过、这里只是拿来用的东西。

跑起来之后,日志里应该能看到 build_phase/connect_phase/run_phase 的活动,接着是来自 DRV 的两行 `uvm_info 输出,各自报告一组 sel/a/b/y 组合——和 capstone 里那个手写 driver 得到的结果一样,只不过这次是从一整套真正的 UVM 组件树里跑出来的。

小结

  • Icarus Verilog 不支持 UVM——跑任何 UVM 例子之前,先在 EDA Playground 上选一个支持 UVM 的仿真器(比如 Riviera-PRO)。
  • DUT 和接口和 SV 基础系列 capstone 完全一样;新增的是一个基于 uvm_component 的 driver,它通过 uvm_config_db::get() 而不是构造函数参数拿到自己的 virtual interface
  • config_db::get() 失败时返回 0——要检查这个返回值并用 `uvm_fatal 报错,而不是悄悄地继续用一个没被赋值的句柄。
  • my_driver::type_id::create("drv", this)build_phase 里构造子组件,而不是用 new(...)——背后的 factory 机制是第 6 章的内容。
  • uvm_config_db#(...)::set(...)(在顶层 module 里、run_test() 之前调用)发布一个值,任何匹配的 get() 都能取到——完整机制是第 5 章的内容。
  • run_test("my_test") 启动 phase 引擎,构造唯一一个指定类的实例作为组件树的根。
  • 任何用到 UVM 的文件都以 `include "uvm_macros.svh"import uvm_pkg::*; 开头。

在 tb_top 的 initial 块里,为什么 uvm_config_db#(...)::set(...) 必须在 run_test(...) 之前执行?

如果 driver 的 build_phase 里 uvm_config_db#(virtual mux2_if.tb_mp)::get(...) 失败了(返回 0),而且 `uvm_fatal 那个检查被去掉了,实际会发生什么?

为什么 my_test 用 my_driver::type_id::create('drv', this) 来构造 driver,而不是 my_driver drv = new('drv', this)?