第 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 里那个 mux2 和 mux2_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
endmoduleDriver:一个近乎空的 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
endclassuvm_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_info、uvm_component、uvm_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_test 的 build_phase 构造出 drv、drv 自己的 build_phase 再去调用 get() 时,值早已经在那里等着了。
这一章还没讲清楚的东西(正好两件)
`uvm_component_utils/type_id::create()——这是 UVM 的 factory 机制;第 6 章讲它到底做了什么、为什么重要。uvm_config_db#(...)::set()/get()——第 5 章讲完整的机制:context/路径/字段名这些参数到底是什么意思,以及为什么build_phase的执行顺序能保证它生效。
除此之外——`uvm_info、run_phase、raise_objection/drop_objection、super.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)?