2、搭建你的第一个UVM环境:从零开始创建一个空的UVM testbench,理解`uvm_component`和`uvm_object`
好,咱们正式开始动手了。
上一章我们聊了UVM是什么,能干什么。这一章,我带你亲手搭一个最简的UVM验证环境。说白了,就是让UVM跑起来,哪怕它什么都不做。
你可能会问:“空的环境有什么意义?” 嗯,我刚开始学的时候也这么想。后来发现,理解一个框架最好的方式,就是看它最干净的样子。就像学编程,第一个程序永远是“Hello World”。
2.1 两个核心概念:`uvm_component` 和 `uvm_object`
在UVM里,所有东西都继承自两个基类:uvm_object 和 uvm_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_objection 和 drop_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_test、my_env、my_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,仿真永远不结束。
我的习惯: 每次写新组件,我都会先检查三件事:
- 有没有加
uvm_component_utils? - build_phase里有没有用
type_id::create? - run_phase里有没有配对
raise/drop_objection?
这三步走完,基本不会出大问题。
2.6 小结
这一章,我们从零开始搭建了一个空的UVM环境。你学会了:
uvm_object和uvm_component的区别- 如何创建driver、env、test
- 如何用top module启动UVM仿真
- 组件树的概念和层次路径
别看这个环境简单,它包含了UVM最核心的骨架。后面所有的内容,都是在这个骨架上添砖加瓦。
下一章,我们会给这个空环境加上接口和transaction,让它真正能驱动信号。到时候,你就能看到UVM和DUT之间的交互了。
嗯,今天就到这里。动手试试吧,有问题随时回来翻翻这一章。