2、搭建你的第一个UVM环境:从零开始创建一个空的UVM testbench,理解`uvm_component`和`uvm_object`

好,咱们正式开始动手了。

上一章我们聊了UVM是什么,能干什么。这一章,我带你亲手搭一个最简的UVM验证环境。说白了,就是让UVM跑起来,哪怕它什么都不做。

你可能会问:“空的环境有什么意义?” 嗯,我刚开始学的时候也这么想。后来发现,理解一个框架最好的方式,就是看它最干净的样子。就像学编程,第一个程序永远是“Hello World”。

2.1 两个核心概念:`uvm_component` 和 `uvm_object`

在UVM里,所有东西都继承自两个基类:uvm_objectuvm_component。这两个概念,你绕不开。

先说说 `uvm_object`

它是UVM中最基本的类。所有数据对象、事务、配置对象,都继承自它。它没有时间概念,不参与仿真运行。你可以把它想象成一个“数据包”或者“配置项”。

举个例子,你定义的一个transaction,里面包含地址、数据、控制信号,它就继承自 uvm_object

再说说 `uvm_component`

它继承自 uvm_object,但多了两个关键东西:层次结构生命周期

每个 uvm_component 都有一个名字,一个父节点,可以挂载子节点。整个验证环境就是一棵树,从顶层的test,到driver、monitor、scoreboard,都是 uvm_component

而且,uvm_component 有固定的执行阶段:build、connect、run、report……这些阶段由UVM自动调度。

一句话总结:

  • uvm_object:数据对象,无层次,无生命周期。
  • uvm_component:组件对象,有层次,有生命周期。

我在项目中见过有人把driver直接继承自 uvm_object,结果跑不起来。为什么?因为 uvm_object 没有 run_phase,没法自动执行驱动逻辑。所以,记住:凡是需要参与仿真、有层次关系的,都用 uvm_component

2.2 搭建一个空的UVM testbench

好,理论说完了,咱们直接上代码。

一个最简的UVM环境,至少需要三个组件:

  • test:顶层测试用例,负责创建环境、启动sequence。
  • env:验证环境,包含所有验证组件。
  • driver:驱动接口信号(这里我们只让它打印一句话)。

另外,还需要一个 top module,用来启动UVM并控制仿真时间。

第一步:写一个最简单的driver

// my_driver.sv
class my_driver extends uvm_driver #(uvm_sequence_item);
  `uvm_component_utils(my_driver)

  function new(string name = "my_driver", uvm_component parent = null);
    super.new(name, parent);
  endfunction

  virtual task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    `uvm_info("DRV", "Hello from my_driver!", UVM_LOW)
    #100ns;
    phase.drop_objection(this);
  endtask
endclass

这里我用了 uvm_driver,它本身继承自 uvm_component。注意 run_phase 里的 raise_objectiondrop_objection,这是UVM控制仿真结束的机制。没有它,仿真会瞬间结束,你什么都看不到。

小技巧: 我习惯在 run_phase 一开始就 raise_objection,在最后 drop_objection。这样能确保仿真不会提前结束。

第二步:写一个空的env

// my_env.sv
class my_env extends uvm_env;
  `uvm_component_utils(my_env)

  my_driver drv;

  function new(string name = "my_env", uvm_component parent = null);
    super.new(name, parent);
  endfunction

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

build_phase 里,我们用 type_id::create 来创建driver。这是UVM推荐的工厂模式,不要直接用 new()。为什么?因为工厂模式允许你在不修改代码的情况下替换组件,这在后期复用和覆盖测试用例时非常有用。

第三步:写一个test

// my_test.sv
class my_test extends uvm_test;
  `uvm_component_utils(my_test)

  my_env env;

  function new(string name = "my_test", uvm_component parent = null);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = my_env::type_id::create("env", this);
  endfunction

  virtual task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    `uvm_info("TEST", "Starting test...", UVM_LOW)
    #200ns;
    `uvm_info("TEST", "Test done!", UVM_LOW)
    phase.drop_objection(this);
  endtask
endclass

第四步:写top module

// top.sv
module top;
  import uvm_pkg::*;
  `include "uvm_macros.svh"
  `include "my_driver.sv"
  `include "my_env.sv"
  `include "my_test.sv"

  initial begin
    run_test("my_test");
  end
endmodule

这里 run_test("my_test") 是UVM的入口。它会自动创建 my_test 实例,然后启动整个仿真流程。

2.3 编译与运行

假设你用VCS或Questa,命令大概是这样:

# 编译
vlog -sv top.sv

# 仿真
vsim -c -do "run -all; quit" work.top

运行后,你应该能看到类似这样的输出:

UVM_INFO @ 0: drv [DRV] Hello from my_driver!
UVM_INFO @ 0: test [TEST] Starting test...
UVM_INFO @ 200: test [TEST] Test done!

看到了吗?你的第一个UVM环境跑起来了!

注意: 如果你看不到任何输出,检查一下是否忘了加 +UVM_VERBOSITY=UVM_LOW 或者仿真时间太短。我曾经有一次忘了 raise_objection,结果仿真瞬间结束,什么都没打印。排查了半天才发现。

2.4 理解组件树

刚才我们创建了三个组件:my_testmy_envmy_driver。它们之间的关系是这样的:

组件 父节点 层次路径
my_test null (顶层) uvm_test_top
my_env my_test uvm_test_top.env
my_driver my_env uvm_test_top.env.drv

这个树形结构有什么用?

  • UVM通过这个树自动管理组件的创建和销毁。
  • 你可以通过层次路径访问任何组件,比如 uvm_test_top.env.drv
  • 配置机制(config_db)也是基于这个层次路径工作的。

你想想看,如果没有这个树,你要手动管理所有组件的生命周期,那得多麻烦?UVM帮你做了这件事。

2.5 避坑指南

我刚开始用UVM时,踩过不少坑。这里分享几个最常见的:

  • 忘记 `uvm_component_utils` 宏:没有这个宏,UVM的工厂机制无法识别你的组件。编译不会报错,但运行时会找不到组件。
  • 在 `build_phase` 里用 `new()` 而不是 `type_id::create()`:这样会绕过工厂,导致后续无法覆盖组件。
  • 忘记调用 `super.build_phase(phase)`:父类的build_phase会做一些初始化工作,不调用可能导致奇怪的问题。
  • objection管理不当:要么忘了raise,仿真瞬间结束;要么忘了drop,仿真永远不结束。

我的习惯: 每次写新组件,我都会先检查三件事:

  1. 有没有加 uvm_component_utils
  2. build_phase里有没有用 type_id::create
  3. run_phase里有没有配对 raise/drop_objection

这三步走完,基本不会出大问题。

2.6 小结

这一章,我们从零开始搭建了一个空的UVM环境。你学会了:

  • uvm_objectuvm_component 的区别
  • 如何创建driver、env、test
  • 如何用top module启动UVM仿真
  • 组件树的概念和层次路径

别看这个环境简单,它包含了UVM最核心的骨架。后面所有的内容,都是在这个骨架上添砖加瓦。

下一章,我们会给这个空环境加上接口和transaction,让它真正能驱动信号。到时候,你就能看到UVM和DUT之间的交互了。

嗯,今天就到这里。动手试试吧,有问题随时回来翻翻这一章。