第二章 一个简单的UVM验证平台
2.1 DUT
本章的DUT(Design Under Test,待测设计,也可叫做DUV,Design Under Verification)定义如下:
//my_dut.v
module dut( clk,
rst_n,
rxd,
rx_dv,
txd,
tx_en);
input clk;
input rst_n;
input [7:0] rxd; //receive data
input rx_dv; //receive enable signal
output [7:0] txd; //transfer data
output tx_en; //transfer enable signal
reg [7:0] txd;
reg tx_en;
always@(posedge clk)
begin
//reset
if(!rst_n)
begin
txd <= 8'b0;
tx_en <= 1'b0;
end
else
begin
txd <= rxd;
tx_en <= rx_dv;
end
end
endmodule
DUT通过rxd接收数据,通过txd发送出去。rx_dv为输入信号的有效性指示,tx_en是发送数据的有效性指示。
2.2 验证平台的组成
验证用于找出DUT的bug,这个过程通常是把DUT放入一个验证平台中实现的。验证平台的组件包括:
-
Driver:驱动器,用于产生激励。验证平台要模拟DUT的各种真实使用情况,这意味这要给DUT施加各种激励(正常/异常激励)。
-
Scoreboard/Checker(下文用Scoreboard,简写Scb):检查板,根据DUT的输出来判断DUT的行为是否与预期相符合。
-
Monitor:监测器,收集DUT的输出并把他们传递给Scb。
-
Reference Model(简写Rfm):参考模型,模拟硬件DUT的功能,给出期望值并发送给Scb。
2.3 只有driver的测试平台
UVM是一个类库,几乎所有的东西都是使用类(class)来实现。driver,monitor,reference model,scoreboard等组成部分都是类。类中包含函数(function)和任务(task),通过这些函数和任务可以完成driver的输出激励功能,完成monitor的检测功能,完成参考模型的计算功能,完成scoreboard的比较功能。要实现一个功能时,首先应该想到的是从UVM的某个类派生一个新的类。所以使用UVM第一条准则:验证平台中所有的组件应该派生自UVM中的类。
//my_driver.sv
class my_driver extends uvm_driver;
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
endfunction
extern virtual task main_phase(uvm_phase phase);
endclass
task my_driver::main_phase(uvm_phase phase);
top_tb.rxd <= 8'b0;
top_tb.rx_dv <= 1'b0;
//wait until rst_n turns high
while(!top_tb.rst_n) @(posedge top_tb.rst_n);
//rst_n is high, start to drive simulation
//for cycle transfer one random value of 0~255 to rxd every clk
for(int i=0;i<256;i++)
begin
@(posedge top_tb.clk);
top_tb.rxd <= $random(0, 255);
top_tb.rx_dv <= 1'b1;
//use uvm macro to print information, similar to $display in SV
`uvm_info("my_driver", "data is drived", UVM_LOW)
end
//simulation finished turn the rx_dv low
@(posedge top_tb.clk);
top_tb.rx_dv <= 1'b0;
endtask
- 简单起见,先说明各组件的执行流程,至于为什么这么执行后续文章会说明。driver组件会执行其main_phase任务,激励就按照main_phase的代码进行发送。
- `uvm_info为uvm的打印信息宏,在这里简单理解对标SV的$display函数,后文详细介绍。
以上为my_driver的定义,如果要使用my_driver必须首先实例化my_driver(OOP特性不再赘述,对OOP特性不了解的可以找一个OOP语言学习下,推荐C++)。
//top_tb.sv
//pre-compile instruction
//declare time unit and time precious
`timesacle 1ns/1ps
//include uvm macro head file(file replace)
`include "uvm_macros.svh"
//import UVM standard package
import uvm_pkg::*;
`include "my_driver.sv"
module top_tb;
reg clk;
reg rst_n;
reg [7:0] rxd;
reg rx_dv;
wire [7:0] rxd;
wire tx_en;
//instance dut(connect dut port to corresponding signal)
dut my_dut(.clk(clk),
.rst_n(rst_n),
.rx_dv(rx_dv),
.txd(txd),
.tx_en(tx_en));
//initial block to define simulation process
initial
begin
my_driver drv;
//instance my_dviver
drv = new("drv", null);
//call drv.main_phase to run simulation
drv.main_phase(null);
$finish();
end
//initial block to generate system clock
initial
begin
clk = 0;
forever
begin
#100 clk = ~clk;
end
end
//initial block to generator DUT reset signal
initial
begin
rst_n = 1'b0;
#1000;
rst_n = 1'b1;
end
endmodule
2.3.1 加入factory机制
使用factory之前需要使用`uvm_component_utils()进行宏注册。宏注册完成的事情非常多,其中之一是将类登记在UVM内部维护的一张表中,这张表就是实现factory功能实现的基础。需要对driver.sv和top_tb.sv做以下修改。
//my_driver.sv
class my_driver extends uvm_driver;
//macor utils
`uvm_component_utils(my_driver)
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
//add debug information
`uvm_info("my_driver", "new is called", UVM_LOW);
endfunction
extern virtual task main_phase(uvm_phase phase);
endclass
task my_driver::main_phase(uvm_phase phase);
`uvm_info("my_driver", "main_phase is called", UVM_LOW)
...
endtask
//top_tb.sv
module top_tb;
...
//call run_test to run simulation
initial
begin
run_test("my_driver");
end
endmodule
//simulation output
new_is called
main_phase is called
- 首先在my_driver定义中使用`uvm_component_utils进行了宏注册。在top_tb中取代drv.main_phase(null)的直接调用方式,使用run_test直接运行仿真。run_test会创建(隐式调用new函数)一个my_drvier的实例,并且会自动调用my_driver的main_phase。这种由一个类名(字符串)创建一个类的实例,这就是uvm_component_utils宏注册带来的效果。所有的uvm_component及其派生类都应该使用uvm_component_utils进行宏注册。
- 另外一个地方时main_phase自动调用了。在UVM验证平台中,只要一个类使用uvm_component_utils注册并且此类被实例化了,那么这个类的main_phase就会被自动调用。其实实现driver等于实现其main_phase任务,所以在driver中最终要的是实现其main_phase任务。
但是运行上述仿真,只输出了打印信息,main_phase中发送的值并没有被打印,这涉及到UVM中的objection机制。
2.3.2 加入objection机制
UVM中objection机制来控制验证平台的关闭。在第一个例子中调用了$finish来显式关闭平台。实际上在每个phase中,UVM会检查是否有objection被提起(raise_objection)。如果有那么等待这个objection被撤销(drop_objection)后停止仿真。如果没有,则马上结束当前phase。
//my_driver.sv
//add objection to main_phase
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("my_driver", "main_phase is called", UVM_LOW)
top_tb.rxd <= 8'b0;
top_tb.rx_dv <= 1'b0;
while(!top_tb.rst_n) @(posedge top_tb.rst_n);
for(int i=0;i<256;i++)
begin
@(posedge top_tb.clk);
top_tb.rxd <= $random(0, 255);
top_tb.rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW)
end
@(posedge top_tb.clk);
top_tb.rx_dv <= 1'b0;
phase.drop_objection(this);
endtask
在加入了objection之后,激励就会执行。这里可以简单理解为drop_objection是finish的替代形式,不同的是在drop_objection之前必须先调用raise_objection。raise_objection和drop_objection语句总是成对存在。
另外一个注意地方是raise_objection语句必须在main_phase中第一个消耗仿真时间的语句之前。如$display语句时不消耗仿真时间的,这些语句可以放在raise_objection之前,但是类似@(posedge top.clk)等语句是消耗放着年时间的,raise_objection之前。如果将消耗仿真时间的语句放在raise_objection之前,那么objection机制将失效,此phase不会执行。
2.3.3 加入virtual interface
在以上的例子中,driver中的等待时钟时间(@posedge top.clk),给DUT中输入端口赋值(top.rx_dv <= 1’b1)都是使用绝对路径,绝对路径的使用大大减弱了验证平台的可移植性。从根本上说,应该尽量杜绝在验证平台中使用绝对路径。
其一是使用宏(Macro):
//my_driver.sv
`define TOP top_tb
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("my_driver", "main_phase is called", UVM_LOW)
`TOP.rxd <= 8'b0;
`TOP.rx_dv <= 1'b0;
while(!`TOP.rst_n) @(posedge `TOP.rst_n);
for(int i=0;i<256;i++)
begin
@(posedge `TOP.clk);
`TOP.rxd <= $random(0, 255);
`TOp.rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW)
end
@(posedge `TOP.clk);
`TOP.rx_dv <= 1'b0;
phase.drop_objection(this);
endtask
这样,当修改路径时,只需要修改宏的定义即可。
另一种方法是使用interface。在SV中使用interface来连接验证平台与DUT的端口。
//interface.sv
//define interface
interface my_if(input clk, input rst_n);
logic [7:0] data;
logic valid;
endinterface
定义了interface后,在top_tb中实例化DUT时,就可以直接使用。
//top_tb.sv
...
my_if input_if(clk, rst_n);
my_if output_if(clk, rst_n);
dut my_dut(.clk(clk),
.rst_n(rst_n),
.rxd(input_if.data),
.rx_dv(input_if.valid),
.txd(output_if.data),
.tx_en(output_if.valid));
...
那么如何在driver中连接端口呢?由于driver是一个类,类中不能有interface,而应该使用virtual interface。
class my_driver extends uvm_driver;
virtual my_if vif;
endclass
在class中声明了virtual interface后就可以在main_phase中使用如下方式驱动信号:
//my_driver.sv
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("my_driver", "main_phase is called", UVM_LOW)
vif.data <= 8'b0;
vif.valid <= 1'b0;
while(!vif.rst_n) @(posedge vif.clk);
for(int i=0;i<256;i++)
begin
@(posedge vif.clk);
vif.data <= $random(0, 255);
vif.valid <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW)
end
@(posedge vif.clk);
vif.valid <= 1'b0;
phase.drop_objection(this);
endtask
加入了virtual interface后可以看出代码中的绝对路径已经全部消除,增加了代码的可移植性。
最后一个问题是,如何把top_tb中的input_if和my_driver中的vif对应起来?不能通过直接赋值。因为在top_tb中,通过run_test语句建立一个my_driver的实例,但是如何引用这个实例?不能直接引用,其原因是UVM通过run_test语句实例化了一个脱离top_tb层次结构的实例,建立了一个新的层次结构。
对于这种脱离了top_tb层次结构,同时又期望在top_tb中对其进行赋值操作的实例,UVM引入了config_db机制。在config_db机制中,分为set和get两步操作,所谓set操作,可以简单理解为“寄信”,而get操作则相当于“收信”。在top_tb中执行set操作:
initial
begin
uvm_config_db #(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
end
在my_driver中,执行get操作:
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
`uvm_info("my_driver", "build_phase is called", UVM_LOW)
if(!uvm_config_db #(virtual my_if)::get(this, "", "vif", vif))
`uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
endfuntion
这里引入了build_phase,与main_phase一样,build_phase也是UVM内建的一个phase。当UVM启动后,会自动执行build_phase。在build_phase中出现了uvm_fatal宏,uvm_fatal宏是一个类似uvm_info的宏,但是只有两个参数。当uvm_fatal打印完信息后,会直接调用个V的finish函数来结束仿真。uvm_fatal的出现表示验证平台出现了重大问题无法继续进行下去,必须停止仿真并做相应的检查。
config_db的set和get都有四个参数。这两个函数的第三个参数必须完全一致。set函数的第四个参数表示要将哪个interface通过config_db传递给my_driver。get函数的第四个参数表示把得到的interface传递给my_driver的成员变量。set函数的第二个参数表示的是路径的索引。在top_tb中通过run_test创建了一个my_driver的实例,这个实例的名字是uvm_test_top。无论传递给run_test的参数是什么,创建的实例名字都为uvm_test_top。
set和get函数使用::
是因为这两个函数都属于静态函数。而uvm_config_db #(virtual my_if)则是一个参数化的类,其参数就是要寄信的类型,这里是virtual my_if。
2.3 为验证平台加入其它组件
2.3.1 加入transaction
以上的操作都是基于信号级的,本节开始将引入reference model,monitor,scoreboard等组件,在这些组件间,信息的传递是基于transaction(事务)的。
transaction是一个抽象的概念,一般来说,物理协议中的数据交换都是以帧或者包为单位的。通常在一帧或一个包中要定义好各参数,每个包的大小不一样。很少会有协议以bit或byte为单位进行数据交换,以以太网为例,每个包的大小至少是64byte,包中包括源地址,目的地址,包的类型,整个包的CRC校验数据等。transaction就是用来模拟这种实际情况,一个transaction就是一个包。不同的验证平台会有不同的transaction。
//my_transaction.sv
class my_transaction extends uvm_sequence_item;
rand bit [47:0] dmac; //destination address
rand bit [47:0] smac; //source address
rand bit [15:0] ether_type; //internet type
rand byte pload[]; //load package size
rand bit [31:0] crc; //crc check code
constraint pload_cons{
pload.size >= 46;
pload.size <= 1500;
}
function bit[31:0] calc_crc();
return 32'h0;
endfuntion
function void post_randomize(); //callback function of randomize
crc = calc_crc;
endfunction
`uvm_object_utils(my_transaction)
function new(string name="my_transaction");
super.new(name);
endfunction
endclass
有两点需要注意:
- UVM中所有的transaction基类都为uvm_sequence_item,只有从uvm_sequence_item派生的transaction才可以使用UVM中强大的sequence机制。
- 没有使用uvm_component_utils来进行factory注册,而是使用了uvm_object_utils。从根本上来说,my_transaction与my_driver是不同的。在整个仿真期间,my_driver是一直存在的,my_transaction不同,它有生命周期。他在仿真的某一时间产生,经过driver驱动,在经过Rfm处理,最终由Scb比较完成后,其生命周期就结束了。一般来说,这种类都是派生自uvm_object或者uvm_object的派生类。uvm_transaction的祖先是uvm_object,所以使用uvm_object_utils宏来进行宏注册。
//my_driver.sv
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
for(int i=0;i<2;i++)
begin
asset(tr.randomize() with {pload.size == 200;});
driver_one_pkg(tr);
end
endtask
task my_driver::drive_one_pkg(my_transaction tr);
bit [47:0] tmp_data;
bit [7:0] data_q[$];
tmp_data = tr.dmac;
//store tmp_data to data_q by 8-bit
for(int i=0;i<6;i++)
begin
data_q.push_back(tmp_data[7:0]);
tmp_data = (tmp_data >> 8);
end
...
tmp_data = tr.crc;
for(int i=0;i<4;i++)
begin
data_q.push_back(tmp_data[7:0]);
tmp_data = (tmp_data >> 8);
end
`uvm_info("my_driver", "begin to drive one pkg", UVM_LOW)
repeat(3) @(posedge vif.clk);
//drive simulation to DUT
while(data_q.size() > 0)
begin
@(posedge vif.clk);
vif.valid <= 1'b1;
vif.data <= data_q.pop_front();
end
@(posedge vif.clk);
vif.valid <= 1'b0;
`uvm_info("my_driver", "end driver one pkg", UVM_LOW)
endtask
2.3.2 加入environment
uvm_env是一个容器类。在这个容器类中实例化driver,monitor,Ref和Scb等。在调用run_test时,传递的参数不再是my_driver,而是这个uvm_env(即让UVM自动创建uvm_env的实例)。
//my_env.sv
class my_env extends uvm_env;
my_driver drv;
function new(string name = "my_env", uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build(phase);
//use factory to instance drv
drv = my_driver::type_id::create("drv", this):
endfunction
`uvm_component_utils(my_env)
endclass
- 所有env应该派生自uvm_env,且与my_driver一样,容器类在仿真中也是一直存在的,使用uvm_component_utils宏来实现factory的注册。
- 在my_env的build_phase中使用了factory的实例化方法type_name::type_id::create来实例化drv。只有使用factory注册过的类才能使用这种方法来实例化,而后文中讲的factory重载也必须通过create的方法来创建(使用new方法创建的实例化不能使用factory重载)。create实例化时传入两个参数,“drv”表示实例的名称,this表示该实例的父节点(类似new函数中的parent参数)。通过父节点的形式,UVM建立了属性的组织结构。在这个树形的组织结构中,由run_test创建的实例是数根(root,这里是uvm_env),并且树根的名字是固定的,为uvm_test_top。
//top_tb.sv
initial
begin
run_test("my_env");
end
initial
begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv","vif", input_if);
end
由于加入了my_env,使得验证平台的层次结构发生了变化。所以在top_tb中使用config_db机制传递vif时要改变相应的路径为(uvm_test_top.drv,其中uvm_test_top为run_test自动创建的实例名,而drv为my_driver实例化时传入的实例名),run_test的参数也变成了my_env。
在树根之后会有树叶,树叶的实现需要在my_env的build_phase中手动实现。无论树根还是树叶,都必须由uvm_component或者其派生类继承而来。
加入my_env后的验证平台树如下:
2.3.3 加入monitor
monitor用于监测DUT的行为。driver负责把transaction级别的数据转换成DUT的端口级别并驱动DUT。而monitor的行为与其相反,用于收集DUT的端口数据,并将其转换成transaction交给后续的组件,如Rfm,Scb等处理。
//my_monitor.sv
class my_monitor extends uvm_monitor;
virtual my_if vif;
`uvm_component_utils(my_monitor)
function new(string name = "my_monitor", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
`uvm_fatal("my_monitor", "virtual interface must be set for vif!!!")
endfunction
extern task main_phase(uvm_phase phase);
extern task collection_one_pkt(my_transaction tr);
endclass
task my_monitor::main_phase(uvm_phase phase);
my_transaction tr;
while(1)
begin
tr=new("tr");
collection_one_pkt(tr);
end
endtask
task my_monitor::collect_one_pkt(my_transaction tr);
bit [7:0] data_q[$];
int psize;
while(1)
begin
@(posedge vif.clk);
if(vif.valid) break;
end
`uvm_info("my_monitor", "begin to collect one pkt", UVM_LOW);
while(vif.valid)
begin
data_q.push_back(vif.data);
@(posedge vif.clk);
end
for(int i=0;i<6;i++)
begin
tr.dmac = {tr.dmac[39:0], data_q.pop_front()};
end
...
for(int i=0;i<4;i++)
begin
tr.crc = {tr.crc[23:0], data_q.pop_front()};
end
`uvm_info("my_monitor", "end collect one pkt, print it:", UVM_LOW)
tr.my_print();
endtask
需注意以下几点:
- 所有的monitor都应该派生自uvm_monitor。
- 与my_driver相似,my_monitor中也需要有一个virtual my_if。
- uvm_monitor在整个仿真中是一直存在的,所以它是一个component,使用uvm_component_utils注册。
- monitor需要时刻收集数据,所以main_phase使用while(1)循环来实现的。
//my_transaction.sv
function void my_print();
$display("dmac = %0h", dmac);
$display("smac = %0h", smac);
$display("ether_type = %0h", ether_type);
for(int i=0;i<pload.size;i++)
begin
$display("pload[%0d]=%0h", i, pload[i]);
end
$display("crc=%0h", crc);
endfunction
当完成monitor类的定义后,可以在env中对其实例化
//my_env.sv
class my_env extends uvm_env;
my_driver drv;
my_monitor i_mon;
my_monitor o_mon;
function new(string name = "my_env", uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build(phase);
drv = my_driver::type_id::create("drv", this);
i_mon = my_monitor::type_id::create("i_mon", this);
o_mon = my_monitor::type_id::create("o_mon", this);
endfunction
`uvm_component_utils(my_env)
endclass
这里实例化两个monitor,一个用于监测DUT的输入,一个用于监测DUT的输出。其实driver也可以将输入数据发送给refm,这里推荐使用输入monitor来检测。
在env实例化monitor后,要在top_tb中使用config_db将input_if和output_if传递给两个monitor:
//top_tb.sv
initial
begin
uvm_config_db #(virtual my_if)::set(null, "uvm_test_top.drv", "vif", input_if);
uvm_config_db #(virtual my_if)::set(null, "uvm_test_top.i_mon", "vif", input_if);
uvm_config_db #(virtual my_if)::set(null, "uvm_test_top.o_mon", "vif", onput_if);
end
加入monitor后的验证平台树如下:
2.3.4 加入uvm_agent
agent:代理类,属于容器类(就是一个空盒子,本身没什么作用,用于装东西),将DUT的输入部分和输出部分进行封装,使验证平台的层次结构更加明显,便于重用。
上一节中,monitor和driver的代码高度相似,其本质是因为两者处理的是同一协议(在同样一套既定的规则下做着不同的事情)。由于二者代码相似,UVM通常将二者封装在一起,成为一个agent,因此不同的agent就代表了不同的协议。
//my_agent.sv
class my_agent extends uvm_agent;
my_driver drv;
my_monitor mon;
function new(string name, uvm_component parnet);
super.new(name, parent);
endfunction
extern virtual function void build_phase(uvm_phase phase);
extern virtual function void connect_phase(uvm_phase phase);
`uvm_component_utils(my_agent)
endclass
function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
if(is_active == UVM_ACTIVE)
begin
drv = my_driver::type_id::create("drv", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunction
function void my_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
endfunction
所有的agent都要派生自uvm_agent,其本身是一个component。
其中build_phase中根据is_active来决定是否要例化driver。is_active是uvm_agent的唯一成员变量。其是一个枚举类型,有UVM_ACTIVE和UVM_PASSIVE两种模式。UVM_ACTIVE就是实例化driver和sequencer(后文中会讲解),monitor。而UVM_PASSIVE则只会实例化monitor,即该agent只具有监测功能。
在env中需要实例化agent而不需要实例化driver和monitor。
//my_env.sv
class my_env extends uvm_env;
my_agent i_agt;
my_agent o_agt;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
i_agt = my_agent::type_id::create("i_agt", this);
o_agt = my_agent::type_id::create("o_agt", this);
//specified is_active value
i_agt.is_active = UVM_ACTIVE;
o_agt.is_active = UVM_PASSIVE;
endfunction
endclass
在加入了agent组件后,验证平台的层级结构发生了改变,使用config_db时需修改变量的路径(uvm_test_top_.i_agt.drv),不再赘述。
需要注意的是:
- 只有uvm_component才可作为树的节点。uvm_object不可以作为UVM树的节点(new函数无父节点)。
- 其次,uvm_driver和uvm_monitor实例化过程是在my_agent的build_phase中进行(根据层级结构,子节点的实例化在父节点的build_phase中进行,而不能跨层级实例化)的而不是在my_env的build_phase进行。所有的实例化要在build_phase中进行,如果在build_phase之后的phase进行实例化,会报错。
加入agent后的验证平台树如下:
2.3.5 加入reference model
Rfm用于完成和DUT相同的功能。Rfm的输出被Scb接收,用于和DUT的输出相比较。DUT如果很复杂,那么Rfm也会相当复杂。
//my_model.sv
class my_model extends uvm_component;
uvm_blocking_get_port #(my_transaction) port;
uvm_analysis_port #(my_transaction) ap;
extern function new(string name, uvm_component parent);
extern function void build_phase(uvm_phase phase);
extern virtual task main_phase(uvm_phase phase);
`uvm_component_utils(my_model)
endclass
function my_model::new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void my_model::build_phase(uvm_phase phase);
super.build_phase(phase);
port = new("port", this);
ap = new("ap", this);
endfunction
task my_model::main_phase(uvm_phase phase);
my_transaction tr;
my_transaction new_tr;
super.main_phase(phase);
while(1)
begin
port.get(tr);
new_tr = new("new_tr");
new_tr.my_copy(tr);
`uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
new_tr.my_print();
ap.write(new_tr);
end
endtask
在my_model的main_phase中,只是单纯复制一份从i_agt得到的tr,并传递给后级的scb中。这里实现了两个my_transaction的复制函数。
//my_transaction.sv
function void my_copy(my_transaction tr);
if(tr==null) `uvm_fatal("my_transaction", "tr is null!!!")
dmac = tr.dmac;
smac = tr.smac;
ether_type = tr.ether_type;
pload = new[tr.pload.size()];
for(int i=0; i < pload.size(); i++)
begin
pload[i] = tr.pload[i];
end
crc = tr.crc;
endfunction
其中需要说明的是my_model如何从i_agt中得到my_transaction,并把my_transaction传递给my_scoreboard。在UVM中,通常使用TLM(Transaction Level Modeling)实现component之间transaction级别的通信。在UVM的transaction级别的通信中,数据发送有多种方式,一种是使用uvm_analysis_port。UVM中组件的事务及通信通信一种是通过TLM进行的,如果要使用TLM通信,需要做三件事:
- 在要通信的组件内声明端口,在其build_phase中实例化端口
- 在通信的组件上一层组件的connect_phase进行端口连接
- 调用端口的方法进行transaction传输
声明uvm_analysis_port:
//my_monitor.sv
uvm_analysis_port #(my_transaction) ap;
uvm_analysis_port是一个参数化的类,其参数就是这个analysis_port需要传递的数据类型。声明ap后,需要在monitor的build_phase中实例化。
//my_monitor.sv
virtual function void build_phase(uvm_phase phase);
ap = new("ap", this);
endfunction
在main_phase中,当收集完了一个transaction后,需要将其通过uvm_analysis_port的一个内建函数write写入ap中。
task my_monitor::main_phase(uvm_phase phase);
my_transaction tr;
while(1)
begin
tr = new("tr");
collect_one_pkt(tr);
ap.write(tr);
end
endtask
还需要将my_env中使用fifo将两个端口连接在一起。在my_env中定义一个fifo,并在build_phase中将其实例化。
//my_env.sv
uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
agt_mdl_fifo = new("agt_mdl_fifo", this);
fifo的类型是uvm_tlm_analysis_fifo,本身是一个参数化的类,其参数是存储在其中的transaction类型。
之后,在connect_phase中将fifo分别与my_monitor中的analysis_port和my_model的blocking_get_port相连。
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
i_agt.ap.connect(agt_mdl_fifo.analysis_export);
mdl.port.connect(agt_mdl_fifo.blocking_get_export);
endfunction
与build_phase相似,connect_phase也是UVM内建的一个phase。他在build_phase执行之后马上执行。但是与build_phase不同的是,他的执行顺序不是从树根到树叶(从上到下),而是从树叶到树根(从下到上)。
为什么要用fifo?因为analysis_port是非阻塞性质的,ap.write函数调用完成后马上返回,不会等待数据被接收。如果write被调用时,blocking_get_port正在忙于其他事情,而没有准备好接收新的数据时,此时被write函数写入的my_transaction就需要一个暂存的位置,这就是fifo。
与my_monitor中的ap不同的是,不需要对my_agent中的ap进行实例化,只需要在my_agent的connect_phase的connect_phase中将monitor的值赋给它,换句话说,这就相当于是一个指向my_monitor的ap指针。
根据前面介绍的connect_phase的执行顺序,my_agent的connect_phase的执行顺序早于my_env的connect_phase的执行顺序,从而可以保证i_agt.ap.connect语句时,i_agt_ap不是一个空指针。
加入Rfm后的验证平台树如下:
2.3.6 加入scoreboard
加入Scb是最后一步。
//my_scoreboard.sv
class my_scoreboard extends uvm_scoreboard;
my_transaction expect_queue[$];
uvm_blocking_get_port #(my_transcation) exp_port;
uvm_blocking_get_port #(my_transcation) act_port;
`uvm_component_utils(my_scoreboard)
extern function new(string name, uvm_component parent=null);
extern virtual function void build_phase(uvm_phase phase);
extern virtual task main_phase(uvm_phase phase);
endclass
function my_scoreboard::new(string name, uvm_component parent = null);
super.new(name, parent);
endfunction
function void my_scoreboard::build_phase(uvm_phase phase);
super.build_phase(phase);
exp_port = new("exp_port", this);
act_port = new("act_port", this);
endfunction
task my_scoreboard::main_phase(uvm_phase phase);
my_transaction get_expect, get_actual, tmp_tran;
bit result;
super.main_phase();
fork
while(1)
begin
exp_port.get(get_expect);
expect_queue.push_back(get_expect);
end
while(1)
begin
act_port.get(get_actual);
if(expect_queue.size() > 0)
begin
tmp_tran = expect_queue.pop_front();
result = get_actual.my_compare(tmp_tran);
if(result)
begin
`uvm_info("my_scoreboard", "Compare SUCESSFULLY", UVM_LOW)
end
else
begin
`uvm_error("my_scoreboard", "Compare FAILD")
$display("the expect pkt is");
tmp_tran.my_print();
$display("the actual pkt is");
get_actual.my_print();
end
end
else
begin
`uvm_error("my_scoreboard", "Received from DUT, while Export Que is empty!")
$display("the unexpected pkt is");
get_actual.my_print();
end
end
join
endtask
my_scoreboard要比较的数据来源于refm和o_agt的monitor。前者通过exp_port获取,后者通过act_port获取。在main_phase中开辟两个线程,一个线程处理exp_port的数据,将接收的到数据缓存至队列。另一个处理act_port的数据。并使用my_compare函数进行数据比对。采用这种比较处理方式的前提是exp_prot要比act_port先收到数据。由于DUT处理数据需要延时,而refm是基于高级语言,一般不需要延时,因此可以保证exp_port的数据在act_port的数据之前到来。
my_compare函数原型如下:
//my_scoreboard.sv
function bit my_compare(my_transaction tr);
bit result;
if(tr == null)
`uvm_fatal("my_transcation", "tr is null!")
result = ((dmac == tr.dmac) &&
(smac == tr.smac) &&
(ether_type == tr.ether_type) &&
(crc == tr.crc));
if(pload.size() != tr.pload.size()) result = 0;
else
begin
for(int i=0;i<pload.size;i++)
begin
if(pload[i] != tr.pload[i]) result = 0;
end
end
return result;
endfunction
加入Scb后的验证平台树如下:
2.3.7 加入field_automation机制
引入my_monitor时,在my_transaction中加入了my_print函数;在引入refm时,加入了my_copy函数;引入scb时,加入了my_compare函数。上述三个函数虽然各不相同,但是对于不同的transaction来说,都是类似的,都需要逐字段对transaction进行某些操作。
UVM引入了field_automation机制,使用uvm_field系列宏实现:
//my_transaction.sv
class my_transaction extends uvm_sequence_item;
rand bit [47:0] dmac;
rand bit [47:0] smac;
rand bit [15:0] ether_type;
rand byte pload[];
rand bit [31:0] crc;
`uvm_object_utils_begin(my_transaction)
`uvm_field_int(dmac, UVM_ALL_ON)
`uvm_field_int(smac, UVM_ALL_ON)
`uvm_field_int(ether_type, UVM_ALL_ON)
`uvm_field_array_int(pload, UVM_ALL_ON)
`uvm_field_int(crc, UVM_ALL_ON)
`uvm_object_utils_end
endclass
以上使用uvm_object_utils_begin和uvm_object_utils_end来实现my_transaction的注册。在这两个宏中间使用uvm_field宏注册所有的字段。uvm_field系列宏随着transaction的成员不同而不同。当使用uvm_field注册成员变量后,可以直接调用copy,compare,print等函数,无需自己实现。极大的简化了验证平台的搭建,提高了效率。引入field_automation机制的另一个好处是简化了driver和monitor(减少了重复性代码的使用)。
//my_driver.sv
task my_driver::drive_one_pkt(my_transaction tr);
byte unsigned data_q[];
int data_size;
data_size = tr.pack_types(data_q) / 8;
`uvm_info("my_driver", "begin to drive one pkt", UVM_LOW)
repeat(3) @(posedge vif.clk);
for(int i=0;i<data_size;i++)
begin
@(posedge vif.clk)
vif.valid <= 1'b1;
vif.data <= data_q[i];
end
@(posedge vif.clk);
vif.valid <= 1'b0;
`uvm_info("my_driver", "end drive one pkt", UVM_LOW)
endtask
pack_bytes将tr中的所有字段变成byte流放入data_q中。pack_bytes极大地减少代码量。在把所有的字段变成byte流放入data_q中时,字段按照uvm_field系列宏书写的顺序排列。
//my_monitor.sv
task my_monitor::collect_one_pkt(my_transaction tr);
byte unsigned data_q[$];
byte unsigned data_array[];
logic [7:0] data;
logic valid = 0;
int data_size;
`uvm_info("my_monitor", "begin to collect one pkt", UVM_LOW)
while(vif.valid)
begin
data_q.push_back(vif.data);
@(posedge vif.clk);
end
data_size = data_q.size();
data_array = new[data_size];
for(int i=0;i<data_size;i++)
begin
data_array[i] = data_q[i];
end
tr.pload = new[data_size - 18];
data_size = tr.unpack_types(data_array) / 8;
`uvm_info("my_monitor", "end collect one pkt", UVM_LOW)
endtask
使用unpack_bytes函数将data_q中的byte流转换成tr中的各字段。unpack_bytes函数的输入参数必须是一个动态数组,所以需要先把收集到的,放在data_q中的数据复制到一个动态数组中。
2.4 sequence机制(核心)
之前的验证平台,激励都是在driver中产生的,但是一个规范化的UVM验证平台中,driver只负责驱动transaction,而不负责产生transaction。sequence机制由两部分组成,一个是sequence,另一个是sequencer。
2.4.1 加入sequencer
sequencer:转发item类。在没有sequencer之前,激励的调度和加载全部由driver完成。模块化编程要求组件具有单一的功能,这样才能使平台具有最大的灵活度,所以将激励的调度这个功能从driver中剥离出来由sequencer担任,driver只负责加载机理。
//my_sequencer.sv
class my_sequencer extends uvm_sequencer #(my_transaction);
function new(string name, uvm_component parent);
super.new(name, parnet);
endfunction
`uvm_component_utils(my_sequencer)
endclass
sequencer的定义非常简单,继承自uvm_sequencer,并使用uvm_component_utils宏注册到factory中。uvm_sequencer是一个参数化的类,其参数是my_transaction,即此sequencer产生的transaction的类型。uvm_driver也是一个参数化的类,应该在定义driver时指明此driver要驱动的transaction类型。
//my_driver.sv
class my_driver extends uvm_driver #(my_tansaction);
这样定义的好处就是可以直接使用uvm_driver中的某些语定义的成员变量,如uvm_driver中(UVM源码)的成员变量req,他的类型就是传递给uvm_driver的参数,在这里也就是my_transaction。
//my_agent.sv
class my_agent extends uvm_agent;
my_driver drv;
my_monitor mon;
my_sequencer sqr;
uvm_analysis_port #(my_transaction) ap;
...
endclass
function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
if(is_active == UVM_ACTIVE)
begin
drv = my_driver::type_id::create("drv", this);
sqr = my_sequencer::type_id::create("sqr", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunction
function void my_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
ap = mon.ap;
endfunction
在加入sequencer后,整棵UVM树如下所示:
2.4.2 加入sequence
sequence的位置比较特殊,sequence不属于验证平台的任何一部分,但是他与sequencer之间有着密切的联系。只有在sequencer的帮助下,sequence的transaction才能最终转发给driver。sequence就像一个弹夹,里面的子弹是transaction,而sequencer是一把枪,三者相互关联。
除了联系外,sequence与sequencer还有显著的区别。sequencer是一个uvm_component,而sequence是一个uvm_object。**与transaction一样,sequence也有其生命周期,它的声明周期比my_transaction要更长一些,其内部的transaction全部发送完毕后,它的声明周期也就结束了。**sequence应该使用uvm_object_utils宏注册到factory中。
//my_sequence.sv
class my_sequence extends uvm_sequence #(my_transaction);
my_transaction m_trans;
function new(string name = "my_sequence");
super.new(name);
endfunction
virtual task body();
repeat(10)
begin
`uvm_do(m_trans)
end
#1000;
endtask
`uvm_object_utils(my_sequence)
end
endclass
所有sequence都应该派生自uvm_sequence,并在定义时指定要产生的transaction类型(参数类)。每一个sequence都有一个body任务,当一个sequence启动之后,会自动执行body中的代码。
上述代码中uvm_do宏完成了三件事:
- 创建一个my_transaction实例
- 将其随机化
- 最终将其送给sequencer
如果不用uvm_do宏也可也可以使用start_item与finish_item的方式产生transaction。
以下讨论sequence,sequencer,driver交互的过程:
一个sequence在向sequencer发送transaction前,要先向sequencer发送一个请求,sequencer把这个请求放入一个仲裁队列中。sequencer需要做两件事:
- 检测仲裁队列中是否有某个sequence发送transaction请求
- 检测driver是否申请transaction
根据是sequence,driver是否有transaction请求分为三种情况:
- 如果仲裁队列有发送请求,但是driver没有申请transaction,那么sequence将会一直处于等待driver的状态。直到driver申请新的transction。此时sequencer同意sequence的发送请求,sequence在得到sequencer的批准后,产生一个transcation并交给sequencer,sequencer把这个transaction交给driver。
- 如果仲裁队列中没有发送请求,但是driver向sequencer申请新的transaction,那么sequencer将会处于等待sequence状态,一直到有sequence递交发送请求,sequencer马上同意这个请求,sequence产生transaction并交给sequencer,最终driver获得这个transaction。
- 如果仲裁队列中有发送请求,同时driver也在向sequencer申请新的transaction,那么将会同意发送请求,sequence产生transaction并交给sequencer,最终由driver获得这个transaction。
driver是通过uvm_driver中的成员变量seq_item_port,而在uvm_sequencer中有成员变量seq_item_export,这两者可以建立一个通道(TLM端口连接),通道中传递的transaction类型就是定义my_sequencer和my_driver时指定的transaction类型。这里并不需要显式指定通道类型,标准UVM库已经定义好了,只需要在my_agent中使用connect函数把两者联系在一起。
//my_agent.sv
function void my_agent::connect_phase(uvm_phase);
super.connect_phase(phase);
if(is_active == UVM_ACTIVE)
begin
drv.seq_item_port.connect(sqr.seq_item_export);
end
endfuntion
当把两者联系好后,就可以在driver中通过get_next_item任务向sequencer申请新的transacton。
//my_driver.sv
task my_driver::main_phase(uvm_phase phase);
vif.data <= 8'b0;
vif.valid <= 1'b0;
while(!vif.rst_n) @(posedge vif.clk);
while(1)
begin
seq_item_port.get_next_item(req);
drive_one_pkt(req);
seq_item_port.item_done();
end
endtask
get_next_item任务用来得到一个新的req,并驱动它,驱动完成后调用item_done通知sequencer。当出现sequencer发送出去了transaction,而driver并没有得到的情况下时,sequencer会保留这份transaction。如果在driver下次调用get_next_item时,item_done被调用,那么sequencer就认为driver已经得到了这个transaction,并把这个transaction删除,item_done可以理解为增加可靠性的一种握手机制。
uvm_do宏产生了一个transaction并交给sequencer,driver取走这个transaction后,uvm_do宏并不会立刻返回执行下一次的uvm_do宏,而是等待在那里,直到driver返回item_done信号,此时,uvm_do宏才算是执行完毕,返回后并开始执行下一个uvm_do宏,产生新的transaction。
sequence如何向sequencer中发送transaction?只需要在某个component(如my_sequencer,my_env)的main_phase中启动这个sequence即可。
//my_env.sv
task my_env::main_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(i_agt.sqr);
phase.drop_objection(this);
endtask
首先创建一个my_sequencer的实例,之后调用start任务。start任务的参数是一个sequencer指针,如果不知名指针,则sequence不知道将产生的transaction交个哪个sequencer。
在UVM中,objection一般伴随着sequence,通常只在sequence出现的地方才提起和撤销objection,当sequence发送完毕transacton就可以结束仿真了。可以在sequencer中启动sequence:
//my_sequencer.sv
task my_sequencer::main_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(this);
phase.drop_objection(this);
endtask
除了get_next_item中,还可以使用try_next_item。get_next_item是阻塞的,它会一直等到有新的transaction才会返回。try_next_item则是非阻塞的,它尝试着询问sequencer是否有新的transaction,如果有则得到此transaction,否则直接返回。以下为使用非阻塞的try_next_item的my_driver:
//my_driver.sv
task my_driver::main_phase(uvm_phase phase);
vif.data <= 8'b0;
vif.valid <= 1'b0;
while(!vif.rst_n) @(posedge vif.clk);
while(1)
begin
seq_item_port.try_next_item(req);
if(req == null) @(posedge vif.clk)
else
begin
driver_one_pkt(req);
seq_item_port.item_done();
end
end
endtask
相比于get_next_item,try_next_item的行为更加接近真实driver的行为,当有数据时,就驱动数据,否则总线将一直处于空闲状态。
验证平台在加入sequence之后就变成一个完整的验证平台了,如下图:
2.4.3 加入default sequence
上节中使用手动启动sequence,在实际应用中,多使用default_sequence的方式启动sequence。
//my_env.sv
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db #(uvm_object_wrapper)::set(this,
"i_agt.sqr.main_phase",
"default_sequence",
my_sequence::type_id::get());
endfunction
uvm_config_db的第一个第二参数共同决定了配置参数“default_sequence”的路径。第二个参数中出现了main_phase,这是UVM在设置default_sequence时的要求,sequence便在该组件的main_phase启动sequence。
除了在my_env的build_phase中设置default_sequence外,还可以在其他地方设置,比如top_tb中。
//top_tb.sv
module top_tb;
...
initial
begin
uvm_config_db#(uvm_object_wrapper)::set(null,
"uvm_test_top.i_agt.sqr.main_phase",
"default_sequence",
my_sequence::type_id::get());
end
endmodule
config_db通常是成对出现的,但是这里不需要在sequencer中使用config_db::get的方式来获取default_sequence,因为UVM已经做好了这些。
使用default_sequence启动sequence的方式取代了上一节中的sequencer的main_phase手工启动sequence的相关语句。
uvm_sequence这个基类中,有一个变量名为starting_phase,他的类型是uvm_phase,sequencer在启动default_sequence时,会自动做如下操作:
task my_sequencer::main_phase(uvm_phase phase);
...
seq.starting_phase = phase;
seq.start(this);
endtask
因此可以在sequence中使用starting_phase进行提起和撤销objection。
//my_sequence.sv
class my_sequence extends uvm_sequence #(my_transaction);
my_transaction m_trans;
virtual task body();
if(starting_phase != null) starting_phase.raise_objection(this);
repeat(10) `uvm_do(m_trans)
#1000;
if(starting_phase != null) starting_phase.drop_objection(this);
`uvm_object_utils(my_sequence)
endtask
endclass
从而,objection完全与sequence关联在一起,在其他任何地方都不需要再设置objection。
2.5 构建测试用例
2.5.1 加入base_test
在一个实际的UVM验证平台中,uvm_env并不是树根。通常来说,树根是一个基于uvm_test派生的类。
//base_test.sv
class base_test extends uvm_test;
my_env env;
function new(string name="base_test", uvm_component parent = null);
super.new(name, parent);
endfunction
extern virtual function void build_phase(uvm_phase phase);
extern virtual function void report_phase(uvm_phase phase);
`uvm_component_utils(base_test)
endclass
function void base_test::build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
uvm_config_db #(uvm_object_wrapper)::set(this,
"env.i_agt.sqr.main_phase",
"default_sequencer",
my_sequence::type_id::get());
endfunction
function void base_test::report_phase(uvm_phase phase);
uvm_report_server server;
int err_num;
super.report_phase(phase);
server = get_report_server();
err_num = server.get_serverity_count(UVM_ERROR);
if(err_num != 0) $display("TEST CASE FAILED");
else $display("TEST CASE PASSED");
endfunction
base_test派生自uvm_test,使用uvm_component_utils宏注册到factory中。在build_phase中实例化my_env,并设置sequencer的default_sequence。
除了以上操作,通常还在base_test中做如下事情:
- 设置整个验证平台的超时退出时间
- 通过config_db设置验证平台的某些参数的值
在加入uvm_test后,整棵UVM树如下所示:
2.5.2 启动测试用例
要测试一个DUT是否按照预期工作,需要对其施加不同的激励,这些激励被称为测试向量或者pattern。一种激励作为一个测试用例,不同的激励就是不同的测试用例。测试用例的数量是衡量验证人员工作成果的最直接目标。
随着验证的进行,测试用例的数量一直在增加,在增加的过程中,很重要的一点是保证后加的测试用例不影响已经建好的测试用例。上文是通过default_sequence的形式启动my_sequence,如何在不影响my_sequence的前提下将其启动呢?最理想的办法是在命令行中指定参数来启动不同的测试用例。
class case0_sequence extends uvm_sequence #(my_transaction);
my_transaction m_trans;
...
virtual task body();
if(starting_phase != null) starting_phase.raise_objection(this);
repeat (10)
begin
`uvm_do(m_trans)
end
#100;
if(starting_phase != null) starting_phase.drop_objection(this);
endtask
...
endclass
//my_case0.sv
class my_case0 extends base_test;
function new(string name = "my_case0", uvm_component parent = null);
super.new(name,parent);
endfunction
extern virtual function void build_phase(uvm_phase phase);
`uvm_component_utils(my_case0)
endclass
function void my_case0::build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(uvm_object_wrapper)::set(this,
"env.i_agt.sqr.main_phase",
"default_sequence",
case0_sequence::type_id::get());
endfunction
//my_case1.sv
class case1_sequence extends uvm_sequence #(my_transaction);
my_transaction m_trans;
...
virtual task body();
if(starting_phase != null) starting_phase.raise_objection(this);
repeat (10) `uvm_do_with(m_trans, { m_trans.pload.size() == 60;})
#100;
if(starting_phase != null) starting_phase.drop_objection(this);
endtask
...
endclass
class my_case1 extends base_test;
function new(string name = "my_case1", uvm_component parent = null);
super.new(name,parent);
endfunction
extern virtual function void build_phase(uvm_phase phase);
`uvm_component_utils(my_case1)
endclass
function void my_case1::build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(uvm_object_wrapper)::set(this,
"env.i_agt.sqr.main_phase",
"default_sequence",
case1_sequence::type_id::get());
endfunction
如果需要启动my_case0,需要在top_tb中更改run_test的参数
initial
begin
run_test(“my_case0”);
end
如果需要启动my_case1,也需要更改run_test参数
initial
begin
run_test("my_case1");
end
当my_case0运行时需要修改代码,重新编译后才能运行。当my_case1运行时也许如此。这样相当不方便,为了解决这个问题,UVM提供了对不加参数的run_test的支持。
//top_tb.sv
initial
begin
run_test();
end
在run_test不加参数时,UVM会利用UVM_TEST_NAME从命令行中寻找测试用例的名字,创建它的实例并运行。
<sim command> //run my_case0
+UVM_TEST_NAME=my_case0
<sim command> //run my_case1
+UVM_TEST_NAME=my_case1
整个验证平台的启动和执行的流程如下图:
- 首先执行top_tb模块,运行其中的initial块(系统时钟,复位,赋值,run_test等)
- 其次运行全局的无参数的run_test任务(top_tb中的run_test的initial块),此时启动整个验证平台
- 由于run_test()是无参数的,UVM通过命令行+UVM_TEST_NAME的来接收运行的实例名(在启动平台时通过命令行或者脚本指定UVM_TEST_NAME参数)
- 依次执行UVM平台的各Phase
- 所有phase执行完毕,结束仿真
整棵UVM树如下所示(uvm_test_top为要运行的用例my_casen):
参考资料:
UVM实战(卷I)张强 编著 机械工业出版社