本文基于《UVM实战》白皮书,记录UVM学习的过程,本章是对第二章:一个简单的UVM验证的学习与记录。
验证平台的组成
一个验证平台要实现如下基本功能:
- 验证平台要模拟DYT的各种真实使用情况,这意味着要给DUT施加各种激励。激励的功能是由driver来实现的。
- 验证平台要能够根据DUT的输出来判断DUT的行为是否与预期相符合,完成这个功能的是scoreboard
- 验证平台要能收集DUT的输出并把它们传递给scoreboard,完成这个功能的是monitor
- 验证平台要能够给出预期结果,在driver传递给DUT计算的同时,验证平台中也需要有一个模块能完成相应的计算结果,给出预期。完成这个功能的是reference model
一个简单的验证平台框图如下:
在UVM中,引入了agent和sequence的概念,因此UVM中验证平台的典型框图如下:
只有driver的验证平台
1.最简单的验证平台
driver是验证平台最基本的组件,是整个验证平台数据流的源泉。(本节以一个简单的DUT为例,说明一个只有driver的UVM验证平台是如何搭建的)
dut.sv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28module dut(clk,
rst_n,
rxd,
rx_dv,
txd,
tx_en);
input clk;
input rst_n;
input[7:0] rxd;
input rx_dv;
output [7:0] txd;
output tx_en;
reg[7:0] txd;
reg tx_en;
always @(posedge clk) begin
if(!rst_n) begin
txd <= 8'b0;
tx_en <= 1'b0;
end
else begin
txd <= rxd;
tx_en <= rx_dv;
end
end
endmoduleUVM是一个库,在这个库中,几乎所有的东西使用类(class)来实现。driver、monitor、reference model、scoreboard等组成部分都是类
类有函数(function),另外还可以有任务(task),还有成员变量,这些成员变量可以控制类的行为,如控制driver的行为
当要实现一个功能时,首先应该想到的是从UVM的某个类派生出一个新的类,在这个新的类中实现所期望的功能
使用UVM的第一条原则是:验证平台中所有的组件应该派生自UVM中的类
UVM验证平台中的driver应该派生自uvm_driver
一个简答的driver如下例所示:my_driver.sv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28`ifndef MY_DRIVER__SV
`define 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;
while(!top_tb.rst_n)
@(posedge top_tb.clk);
for(int i = 0; i < 256; i++)begin
@(posedge top_tb.clk);
top_tb.rxd <= $urandom_range(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;
endtask
`endif分析上述代码:
所有派生自uvm_driver的类的new函数有两个参数,一个是string类型的name,一个是uvm_component类型的parent。事实上,这两个参数是由uvm_component要求的,每个派生自uvm_component或其派生的类在new函数中都要声明name和parent。uvm_driver是一个派生自uvm_component的类
1
function new(string name = "my_driver", uvm_component parent = null);
driver所做的事情几乎都在main_phase中完成。UVM由phase来管理验证平台的运行,这些phase统一以xxx.phase命名,且都有一个类型为uvm_phase、名字为phase的参数。main_phase是uvm_driver中预先定义好的一个任务。因此几乎可以简单认为,实现一个driver等于实现其main_phase(感觉归根到底不就是实现了一个函数)
1
task my_driver::main_phase(uvm_phase phase);
代码中还出现了uvm_info宏。这个宏的功能与verilog中display语句的功能类似,但是它比display语句更加强大。它有三个参数:第一个参数是字符串,用于把打印的信息归类,第二个参数也是字符串,是具体需要打印的信息,第三个参数则是冗余级别(在验证平台中,某些信息是非常关键的,这样的信息可以设置为UVM_LOW,而有些信息可有可无,就可以设置为UVM_HIGH,介于两者之间的就是UVM_MEDIUM。UVM默认只显示UVM_MEDIUM或者UVM_HIGH的信息)。在搭建验证平台时应尽量使用uvm_info宏取代display语句。
1
`uvm_info("my_driver", "data is drived", UVM_LOW)
对my_driver实例化并且最终搭建的验证平台如下:(top_tb.sv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43`timescale 1ns/1ps
`include "uvm_macros.svh"
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] txd;
wire tx_en;
dut my_dut(.clk(clk),
.rst_n(rst_n),
.rxd(rxd),
.rx_dv(rx_dv),
.txd(txd),
.tx_en(tx_en));
initial begin
my_driver drv;
drv = new("drv", null);
drv.main_phase(null);
$finish();
end
initial begin
clk = 0;
forever begin
#100 clk = ~clk;
end
end
initial begin
rst_n = 1'b0;
#1000;
rst_n = 1'b1;
end
endmodule分析上述代码:
uvm_macro.svh文件通过include语句包含进来。这是UVM中的一个文件,里面包含了众多的宏定义,只需要包含一次
1
`include "uvm_macros.svh"
通过import语句将整个uvm_pkg导入验证平台中。只有导入了这个库,编译器在编译my_driver.sv文件时才会认识其中的uvm_driver等类名。
1
import uvm_pkg::*;
定义drv为my_driver的实例,并将其例化。调用new函数时,其传入的名字参数为drv(uvm_info宏的打印信息时出现的代表路径索引的drv就是在这里传入的参数drv)
1
2my_driver drv;
drv = new("drv", null);显示地调用my_driver的main_phase。在main_phase的声明中,有一个uvm_phase类型的参数phase(在真正的验证平台中,这个参数是不需要用户理会的,本节的验证平台还算不上一个完整的UVM验证平台,所以暂且传入null)
1
drv.main_phase(null);
该例最后的仿真打印结果如下:
“data is drived”被输出了256次
关于uvm_info宏打印的结果中有如下几项:
UVM_INFO关键字:表明这是一个uvM宏打印的结果
my_driver.sv(23):指明该条打印信息的来源,其中括号的数字表示原始的uvm_info打印语句在my_driver.sv中的行号
@后面的数字,比如51100表示此条信息的打印时间
drv:这是driver在UVM数中的路径索引(其实就是后面实例调用时new传入的第一个参数)。UVM采用树形结构,对于树中任何一个节点,都有一个与其相应的字符串类型的路径索引。路径索引可以通过get_full_name函数来获取,把下列代码加入任何UVM数的节点中就可以得知当前节点的路径索引:
1
$display("the full name of current component is: %s", get_full_name());
2.加入factory机制
factory机制的实现被集成在了一个宏中:uvm_component_utils。这个宏所做的事情非常多,其中之一就是将my_driver登记在UVM内部的一张表中,这张表是factory功能实现的基础。
只要在定义一个新的类时使用这个宏,就相当于把这个类注册到了这张表中。
1
2
3
4
5
6
7
8
9
10class my_driver extends uvm_driver;
`uvm_component_utils(my_driver)
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
`uvm_info("my_driver", "new is called", UVM_LOW);
endfunction
extern virtual task main_phase(uvm_phase phase);
endclass在给driver中加入factory机制后,还需要对top_tb做一些改动:
1
2
3initial begin
run_test("my_driver");
end- 这里使用一个run_test语句替换掉了前一小结中top_tb.sv中第23-28行的my_driver实例化以及main_phase的显示调用。
- 一个run_test语句会创建一个my_driver的实例,并且会自动调用my_driver的main_phase。
- 给run_test传递的是一个字符串,UVM根据这个字符串创建了其所代表类的一个实例
根据类名创建一个类的实例,这是uvm_component_utils宏所带来的效果,只有在类定义时声明了这个宏,才能使用这个功能。从某种程度上来说,这个宏起到了注册的作用。只有经过注册的类,才能使用这个功能,否则根本不能使用。
所有派生自uvm_component及其派生类的类都应该使用uvm_component_utils宏注册(所谓注册,就是在定义一个新类时加上``uvm_component_utils(my_driver)`)
在UVM验证平台中,只要一个类使用uvm_component_utils注册且此类被实例化(这里的实例化是通过run_test语句实现的)了,那么这个类的main_phase就会被自动调用。
经过上述修改后的代码(本书的代码开源,我就不一一粘贴了,网上随手一搜都能搜到),其仿真结果如下(并未打印256次data is driver,关于这个问题,牵扯UVM的objection机制):(仿真波形也卡住了)
3.加入objection机制
在上一节中,虽然输出了“main_phase is called”,但是“data is drived”并没有输出,而main_phase是一个完整的任务,没有理由只执行第一句,而后面的代码不执行。看上去似乎main_phase在执行的过程中被外力”杀死“了,事实上也确实如此。
UVM中通过objection机制来控制验证平台的关闭
在每个phase中,UVM会检查是否有objection被提起(raise_objection),如果有,那么等待这个objection被撤销(drop_objection)后停止仿真;如果没有,马上结束当前phase
1
2
3phase.raise_objection(this);
...
phase.drop_objection(this);在drop_objection语句之前必须先调用raise_objection语句,drop_objection和raise_objection总是成对出现的。加入objection机制后再运行验证平台,可以发现”data is drived”按照预期输出了256次。
raise_objection语句必须在main_phase中第一个消耗仿真时间的语句之前。如
$display
语句是不消耗仿真时间的,这些语句可以放在raise_objection之前,但是类似@(posedge top_tb.clk)等语句是要消耗仿真时间的。若my_driver.sv的代码修改为:1
2@(posedge top_tb.clk);
phase.raise_objection(this);其结果如下:(甚至连仿真波形都会卡住)
4.加入virtual interface
在前面的例子中,driver中等待时钟时间@(posedge top.clk)、给DUT中输入端口赋值(top.rx_dv<=1’b1)都是使用绝对路径,绝对路径的使用大大减弱了验证平台的可移植性。一个最简单的例子就是假设clk信号的层次从top.clk变成了top.clk_inst.clk,那么就需要对driver中的相关代码做大量修改。因此,从根本上来说,应该尽量杜绝在验证平台中使用绝对路径。
避免绝对路径的一个方法是使用宏:
1
`define TOP top_tb
这样,当路径修改时,只需要修改宏的定义即可。但是假设clk的冷酷就变为了top_tb.clk_inst.clk,而rst_n的路径变为了top_tb.rst_inst.rst_n,那么单纯地修改宏定义是无法起到作用的。
避免绝对路径的另外一种方式是使用interface。在SV中使用interface来连接验证平台与DUT的端口。
interface的定义比较简单:
1
2
3
4
5
6
7
8
9
10`ifndef MY_IF__SV
`define MY_IF__SV
interface my_if(input clk, input rst_n);
logic [7:0] data;
logic valid;
endinterface
`endif定义了interface后,在top_tb中实例化DUT时,就可以直接使用:
1
2
3
4
5
6
7
8
9my_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));在类中使用的是virtual interface
1
2class my_driver extends uvm_driver;
virtual my_if vif;在声明了vif后,就可以在main_phase中使用如下方式驱动其中的信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20task 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 <= $urandom_range(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);
endtaskUVM引进了config_db机制,来将top_tb中input_if和my_driver中的vif对应起来。
在config_db机制中,分为set和get两步操作。所谓set操作,可以简单地理解成是”寄信“,而set则相当于是”收信“。
在top_tb中执行set操作:
1
2
3initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
end在my_driver中,执行get操作:
1
2
3
4
5
6virtual 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!!!")
endfunction这里引入了build_phase。与main_phase一样,build_phase也是UVM中内建的一个phase。当UVM启动后,会自动执行build_phase,build_phase在new函数之后main_phase之前执行。
- 这里需要加入super.build_phase语句,因为在其父类的build_phase中执行了一些必要的操作,这里必须显式地调用并执行它
- build_phase与main_phase不同的一点在于,build_phase是一个函数phase,而main_phase是一个任务phase,build_phase是不消耗仿真时间的,build_phase总是在仿真时间为0时执行。
- 在build_phase中出现了uvm_fatal宏,uvm_fatal宏是一个类似于uvm_info的宏,但是它只有两个参数,这两个参数于uvm_info宏的前两个参数的意义完全一样。当uvm_fatal打印出第二个参数之后,会直接调用verilog的finish函数来结束仿真,其意味着验证平台出现了重大问题而无法继续下去,必须停止仿真并做相应的检查。
- config_db的set和get函数都有四个参数,这两个函数的第三个参数必须完全一致
- set函数:
- 第一个参数:当使用
null
作为这个参数时,它意味着设置的配置项是全局可用的。 - 第二个参数:表示路径索引,UVM通过run_test创建了一个my_driver的实例,其实例名即为uvm_test_top
- 第三个参数:指定配置项的名称,为一个与get第三个参数连接的虚拟接口
- 第四个参数:表示要将哪个interface通过config_db传递给my_driver(即把input_if接口给”vif”这个虚拟接口)
- 第一个参数:当使用
- get函数:
- 第一个参数:
this
。这是调用get
方法的组件的句柄。它定义了查询配置数据库的上下文。通常,它指的是希望获取配置信息的组件的实例。 - 第二个参数:
""
。这是一个字符串,定义了配置信息的作用域。空字符串表示全局作用域,即搜索整个UVM环境以找到匹配的配置项。如果这里指定了一个特定的层级路径,那么uvm_config_db
将只在指定的层级内搜索配置信息。 - 第三个参数:指定了要获取的配置项的名称,虚拟接口罢了
- 第四个参数:表示把得到的interface传递给哪个my_driver的成员变量(即从”vif”这个虚拟接口收到的接口传递给vif这个成员变量)
- 第一个参数:
- set函数:
- set和get函数前面使用双冒号的原因是这两个函数都是静态函数
uvm_config_db#(virtual my_if)
则是一个参数化的类,其参数就是要寄信的类型,这里是virtual my_if
,假设要传递一个int类型的数据,那么久写为uvm_config_db#(int)
最终,加入上述所述机制的代码如下:
my_driver.sv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44`ifndef MY_DRIVER__SV
`define MY_DRIVER__SV
class my_driver extends uvm_driver;
virtual my_if vif;
`uvm_component_utils(my_driver)
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
`uvm_info("my_driver", "new is called", UVM_LOW);
endfunction
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!!!")
endfunction
extern virtual task main_phase(uvm_phase phase);
endclass
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 <= $urandom_range(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
`endifmy_if.sv
1
2
3
4
5
6
7
8
9
10`ifndef MY_IF__SV
`define MY_IF__SV
interface my_if(input clk, input rst_n);
logic [7:0] data;
logic valid;
endinterface
`endiftop_tb.sv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48`timescale 1ns/1ps
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "my_if.sv"
`include "my_driver.sv"
module top_tb;
reg clk;
reg rst_n;
reg[7:0] rxd;
reg rx_dv;
wire[7:0] txd;
wire tx_en;
my_if input_if(clk, rst_n);
my_if output_if(clk, rst_n);
dut my_dut(.clk(input_if.clk),
.rst_n(input_if.rst_n),
.rxd(input_if.data),
.rx_dv(input_if.valid),
.txd(output_if.data),
.tx_en(output_if.valid));
initial begin
clk = 0;
forever begin
#100 clk = ~clk;
end
end
initial begin
rst_n = 1'b0;
#1000;
rst_n = 1'b1;
end
initial begin
run_test("my_driver");
end
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
end
endmodule仿真结果如下:
为验证平台加入各个组件
1.加入transaction
在UVM的各个组件之间,信息的传递时基于transaction的。一般来说,物理协议中的数据交换都是以帧或者包为单位的,通常在一帧或者一个包中要定义好各项参数,每个包的大小不一样。很少会有协议是以bit或者byte为单位来进行数据交换的,以以太网为例,每个包的大小至少是64byte,这个包中要包含源地址、目的地址、包的类型、整个包的CRC校验数据等。transaction就是用于模拟这种实际情况,一笔transaction就是一个包。
一个简单的transaction的定义如下:(my_transaction.sv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32`ifndef MY_TRANSACTION__SV
`define 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;
constraint pload_cons{
pload.size >= 46;
pload.size <= 1500;
}
function bit[31:0] calc_crc();
return 32'h0;
endfunction
function void post_randomize();
crc = calc_crc;
endfunction
`uvm_object_utils(my_transaction)
function new(string name = "my_transaction");
super.new();
endfunction
endclass
`endif- 其中dmac是48bit的以太网目的地址,smac是48bit的以太网源地址,ether_type是以太网类型,pload是其携带数据的大小(通过pload_cons约束可以看到,其大小被限制在46~1500byte),CRC是前面所有数据的校验值(这里只是在post_randomize中加了一个空函数calc_crc,post_randomize是SV中提供的一个函数,当某个类的实例的randomize函数被调用后,post_randomize会紧随其后无条件地被调用)
- my_transaction的基类是uvm_sequence_item,在UVM中,所有的transaction都是从uvm_sequence_item派生,只有从uvm_sequence_item派生的transaction才可以使用后文讲述的UVM中强大的sequence机制
- 从本质上来说,my_transaction与my_driver是有区别的,在整个仿真期间,my_driver是一直存在的,my_transaction不同,它有生命周期,它在仿真的某一个时间产生,经过driver驱动,再经过reference model处理,最终由scoreboard比较完成后,其生命周期就结束了
- 一般来说,这种类都是派生自uvm_objection或者uvm_objection的派生类,uvm_objection_item的祖先就是uvm_objection。UVM中具有这种特征的类都要使用uvm_objection_utils宏来实现。
当完成transaction的定义后,就可以在my_driver中实现基于transaction的驱动:(my_driver.sv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86`ifndef MY_DRIVER__SV
`define MY_DRIVER__SV
class my_driver extends uvm_driver;
virtual my_if vif;
`uvm_component_utils(my_driver)
function new(string name = "my_driver", 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_driver", "virtual interface must be set for vif!!!")
endfunction
extern task main_phase(uvm_phase phase);
extern task drive_one_pkt(my_transaction tr);
endclass
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
phase.raise_objection(this);
vif.data <= 8'b0;
vif.valid <= 1'b0;
while(!vif.rst_n)
@(posedge vif.clk);
for(int i = 0; i < 2; i++) begin
tr = new("tr");
assert(tr.randomize() with {pload.size == 200;});
drive_one_pkt(tr);
end
repeat(5) @(posedge vif.clk);
phase.drop_objection(this);
endtask
task my_driver::drive_one_pkt(my_transaction tr);
bit [47:0] tmp_data;
bit [7:0] data_q[$];
//push dmac to data_q
tmp_data = tr.dmac;
for(int i = 0; i < 6; i++) begin
data_q.push_back(tmp_data[7:0]);
tmp_data = (tmp_data >> 8);
end
//push smac to data_q
tmp_data = tr.smac;
for(int i = 0; i < 6; i++) begin
data_q.push_back(tmp_data[7:0]);
tmp_data = (tmp_data >> 8);
end
//push ether_type to data_q
tmp_data = tr.ether_type;
for(int i = 0; i < 2; i++) begin
data_q.push_back(tmp_data[7:0]);
tmp_data = (tmp_data >> 8);
end
//push payload to data_q
for(int i = 0; i < tr.pload.size; i++) begin
data_q.push_back(tr.pload[i]);
end
//push crc to data_q
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 pkt", UVM_LOW);
repeat(3) @(posedge vif.clk);
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 drive one pkt", UVM_LOW);
endtask
`endif在main_phase中,先使用randomize将tr随机化,之后通过drive_one_pkt任务将tr的内容驱动到DUT端口上
这里定义了一个48位的临时变量
tmp_data
和一个动态大小的队列(使用$
)data_q
,用于存储8位宽的数据1
2bit [47:0] tmp_data;
bit [7:0] data_q[$];获取随机的目的地址:这部分代码从传入的事务
tr
中取出目的MAC地址,将其每8位截取并放入data_q
。此循环执行6次,因为MAC地址总共有48位。1
2
3
4
5
6//push dmac to data_q
tmp_data = tr.dmac;
for(int i = 0; i < 6; i++) begin
data_q.push_back(tmp_data[7:0]);
tmp_data = (tmp_data >> 8);
end开始传输数据:传输开始前,等待三个时钟周期,然后通过
vif
接口逐字节传输data_q
中的数据。每个时钟上升沿,从队列前端弹出一个字节数据,直到队列为空。数据传输结束后,清除valid
信号,表示数据包传输完成。1
2
3
4
5
6
7
8
9
10repeat(3) @(posedge vif.clk);
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;其实是通过push操作,先存下通过transaction生成的一个以太网格式的数据包,然后再发送
最终仿真结果如下:(以太网数据包总共发送了两次)
2.加入env
在验证平台中加入reference model、scoreboard等之前,思考一个问题:假设这些组件已经定义好了,那么在验证平台的什么位置对它们进行实例化呢?
这个问题的解决方案是引入一个容器类,在这个容器类中实例化driver、monitor、reference model和scoreboard等。在调用run_test时,传递的参数不再是my_driver,而是这个容器类,即让UVM自动创建这个容器类的实例(my_env.sv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20`ifndef MY_ENV__SV
`define 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(phase);
drv = my_driver::type_id::create("drv", this);
endfunction
`uvm_component_utils(my_env)
endclass
`endif所有的env应该派生自uvm_env,且与my_driver一样,容器类在仿真中也是一直存在的,使用uvm_component_utils宏来实现factory的注册
验证平台中的组件在实例化时都应该使用
type_name::type_id::create
的方式在drv实例化时,传递了两个参数,一个名字drv,另一个是this指针
1
drv = my_driver::type_id::create("drv", this);
回顾一下my_driver的new函数:
1
2
3function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
endfunction这个new函数有两个参数,第一个参数是实例的名字,第二个则是parent。由于my_driver在uvm_env中实例化,所以my_driver的父节点(parent)就是my_env,通过parent的形式,UVM建立起了树形的组织结构。
在这种树形的组织结构中,由run_test创建的实例是数根(这里是my_env),并且树根的名字是固定的,为uvm_test_top,在树根之后会生长出枝叶(这里只有my_driver),长出枝叶的过程需要在my_env的build_phase中手动实现。无论是树根还是树叶,都必须由uvm_component或者其派生类继承而来。
当加入了my_env后,整个验证平台中存在两个build_phase,一个是my_env的,一个是my_driver的,其执行顺序:
- 在UVM的树形结构中,build_phase的执行遵照从树根到树叶的顺序,即先执行my_env的build_phase,再执行my_driver的build_phase。当把整棵树的build_phase都执行完毕后,再执行后面的phase。
my_driver在验证平台中的层次结构发生了变化,所以在top_tb中使用config_db机制传递virtual my_if时,要改变相应的路径;同时,run_test的参数也要从my_driver变为my_env
1
2
3
4
5
6
7initial begin
run_test("my_env");
end
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", input_if);
endset函数的第二个参数从uvm_test_top变为了uvm_test_top.drv,其中uvm_test_top是UVM自动创建的树根的名字,而drv则是在my_env的build_phase中实例化drv(new创建时)时传递过去的名字。如果在实例化drv时传递的名字时my_drv,那么set函数的第二个参数中也应该是my_drv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class my_env extends uvm_env;
...
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = my_driver::type_id::create("my_drv", this);
endfunction
`uvm_component_utils(my_env)
endclass
module top_tb;
...
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.my_drv", "vif", input_if);
end
endmodule发现UVM不管创建的类对象赋值给了哪个变量,但其实传递的时候都是用实例的名字(new的第一个参数字符串),与赋值给的等式左边的那个变量名字无关
仿真结果如下:
3.加入monitor
验证平台必须检测DUT的行为,只有知道DUT的输入输出信号变化之后,才能根据这些信号变化来判断DUT的行为是否正确
验证平台中实现监测DUT行为的组件是monitor
driver负责把transaction级别的数据转变成DUT的端口级别,并驱动给DUT,monitor的行为与其相对,用于收集DUT的端口数据,并将其转化成transaction交给后续的组件如reference model、scoreboard等处理
一个monitor的定义如下:(my_monitor.sv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71`ifndef MY_MONITOR__SV
`define 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 collect_one_pkt(my_transaction tr);
endclass
task my_monitor::main_phase(uvm_phase phase);
my_transaction tr;
while(1) begin
tr = new("tr");
collect_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
//pop dmac
for(int i = 0; i < 6; i++) begin
tr.dmac = {tr.dmac[39:0], data_q.pop_front()};
end
//pop smac
for(int i = 0; i < 6; i++) begin
tr.smac = {tr.smac[39:0], data_q.pop_front()};
end
//pop ether_type
for(int i = 0; i < 2; i++) begin
tr.ether_type = {tr.ether_type[7:0], data_q.pop_front()};
end
psize = data_q.size() - 4;
tr.pload = new[psize];
//pop payload
for(int i = 0; i < psize; i++) begin
tr.pload[i] = data_q.pop_front();
end
//pop crc
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
`endif所有的monitor类应该派生自uvm_monitor
与driver类似,在my_monitor中也需要有一个virtual my_if
uvm_monitor在整个仿真中是一直存在的,所以它是一个component,要使用uvm_component_utils宏注册
由于monitor需要时刻收集数据,永不停歇,所以在main_phase中使用while(1)循环来实现这一目的
1
2
3
4
5
6
7task my_monitor::main_phase(uvm_phase phase);
my_transaction tr;
while(1) begin
tr = new("tr");
collect_one_pkt(tr);
end
endtask当收集完一个transaction后,通过my_print函数将其打印出来,my_print在my_transaction中定义如下:
1
2
3
4
5
6
7
8
9function 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中对其进行实例化
1
2
3
4
5
6virtual function void build_phase(uvm_phase phase);
super.build_phase(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- 实例了两个monitor,一个用来监测DUT的输入口,一个用来监测DUT的输出口(书中作者推荐)
现在,整棵UVM数的结构如下:
仿真结果如下:(仿真打印数据太多就不粘贴了,有发收发收共四次打印的结果)
4.封装成agent
UVM中通常将driver和monitor二者封装在一起,成为一个agent
所有的agent都要派生自uvm_agent类,且其本身是一个component,应该使用uvm_component_utils宏来来实现factory注册
my_agent.sv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31`ifndef MY_AGENT__SV
`define MY_AGENT__SV
class my_agent extends uvm_agent ;
my_driver drv;
my_monitor mon;
function new(string name, uvm_component parent);
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
`endif在uvm_agent中,is_active的值默认为UVM_ACTIVE,在这种模式下,是需要实例化driver的(貌似也会自动实例化一个monitor),is_active=UVM_PASSIVE时只需要实例化monitor
在完成i_agt和o_agt的声明后,在my_env的build_phase中对它们进行实例化后,需要指定各自的工作模式是active模式还是passive模式(my_env.sv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23`ifndef MY_ENV__SV
`define MY_ENV__SV
class my_env extends uvm_env;
my_agent i_agt;
my_agent o_agt;
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(phase);
i_agt = my_agent::type_id::create("i_agt", this);
o_agt = my_agent::type_id::create("o_agt", this);
i_agt.is_active = UVM_ACTIVE;
o_agt.is_active = UVM_PASSIVE;
endfunction
`uvm_component_utils(my_env)
endclass
`endif现在,整课UVM树变为:
- 只有uvm_component才能作为树的结点,像my_transaction这种使用uvm_object_utils宏实现的类是不能作为UVM树的结点的
- UVM要求UVM树最晚在build_phase时段完成,一般都在build_phase中完成实例化
由于agent的加入,driver和monitor的层次结构改变了,在top_tb中使用config_db设置virtual my_if时要注意改变路径:
1
2
3
4
5initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.drv", "vif", input_if);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.mon", "vif", input_if);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_agt.mon", "vif", output_if);
end仿真结果如下:
5.加入reference model
reference model的输出被scoreboard接收,用于和DUT的输出相比较。DUT如果很复杂,那么reference model也会相当复杂。本章的DUT很简单,所以reference model也很简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39`ifndef MY_MODEL__SV
`define 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
`endif在my_model的main_phase中,只是单纯地复制一份从i_agt得到的tr,并传递给后级的scoreboard中
my_copy是一个在my_transaction中定义的函数(my_transaction.sv):
1
2
3
4
5
6
7
8
9
10
11
12function 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后,整棵UVM树变成了:
my_model是从i_agt中得到my_transaction,并把my_transaction传递给my_scoreboard
在UVM中,通常使用TLM(Transaction Level Modeling)实现component之间的transaction级别通信
在UVM的transaction级别的通信中,数据的发送有多种方式,其中一种是使用uvm_analysis_port,需要在my_monitor.sv中声明及实例化,并写入:
在monitor中的声明:
1
uvm_analysis_port #(my_transaction) ap;
- uvm_analysis_port是一个参数化的类,其参数就是这个analysis_port需要传递的数据类型,在本节中是my_transaction
在monitor的build_phase中将其实例化:
1
2
3
4virtual function void build_phase(uvm_phase phase);
...
ap = new("ap", this);
endfunction在monitor的main_phase中,当收集完一个transaction后,需要将其写入ap中:
1
2
3
4
5
6
7
8task 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- write是uvm_analysis_port的一个内建函数
UVM的transaction级别通信的数据接收方式也有多种,其中一种就是使用uvm_blocking_get_port。这也是一个参数化的类,其参数是要在其中传递的transaction的类型。需要在my_model.sv中声明及实例化,并发送:
在model中声明:
1
uvm_blocking_get_port #(my_transaction) port;
在model的build_phase中将其实例化:
1
2
3
4
5function void my_model::build_phase(uvm_phase phase);
super.build_phase(phase);
port = new("port", this);
...
endfunction在monitor的main_phase中,通过port.get任务来得到从i_agt的monitor中发出的transaction:
1
2
3
4
5
6
7
8
9
10
11
12
13task 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_monitor和my_model中定义并实现了各自的端口之后,通信的功能并没有实现,还需要在my_env中使用fifo将两个端口联系在一起
在my_env中定义一个fifo,并在build_phase中将其实例化:
1
2
3
4
5
6uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
...
virtual function void build_phase(uvm_phase phase);
...
agt_mdl_fifo = new("agt_mdl_fifo", this);
endfunction之后,在connect_phase中将fifo分别与my_monitor中的analysis_port和my_model中的blocking_get_port相连:
1
2
3
4
5function 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- 这里引入了connect_phase,与build_phase及main_phase类似,connect_phase也是UVM内建的一个phase,它在build_phase执行完成之后马上执行。但是与build_phase不同的是,它的执行顺序并不是从树根到树叶,而是从树叶到树根:先执行driver和monitor的connect_phase,再执行agent的connect_phase,最后执行env的connect_phase
在如上的连接中,用到了i_agt的一个成员变量ap,
它的定义与my_monitor中ap的定义完全一样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79uvm_analysis_port #(my_transaction) ap;
- 与my_monitor中的ap不同的是,**不需要对my_agent中的ap进行实例化,而只需要在my_agent的connect_phase中将monitor的值赋给它。换句话说,这相当于是一个指向my_monitor的ap的指针**
```systemverilog
function void my_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
ap = mon.ap;
endfunction
- 仿真结果如下:
![image-20240430160945765](UVM%E7%99%BD%E7%9A%AE%E4%B9%A6%E4%B9%8B%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84UVM%E9%AA%8C%E8%AF%81%E5%B9%B3%E5%8F%B0/image-20240430160945765.png)
![image-20240430161005072](UVM%E7%99%BD%E7%9A%AE%E4%B9%A6%E4%B9%8B%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84UVM%E9%AA%8C%E8%AF%81%E5%B9%B3%E5%8F%B0/image-20240430161005072.png)
## 6.加入scoreboard
- 在验证平台中加入reference model和monitor之后,最后一步是加入scoreboard,其代码如下:(my.scoreboard.sv)
```systemverilog
`ifndef MY_SCOREBOARD__SV
`define MY_SCOREBOARD__SV
class my_scoreboard extends uvm_scoreboard;
my_transaction expect_queue[$];
uvm_blocking_get_port #(my_transaction) exp_port;
uvm_blocking_get_port #(my_transaction) 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(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 SUCCESSFULLY", UVM_LOW);
end
else begin
`uvm_error("my_scoreboard", "Compare FAILED");
$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 Expect Queue is empty");
$display("the unexpected pkt is");
get_actual.my_print();
end
end
join
endtask
`endif
- my_scoreboard要比较的数据一是来源于reference model,二是来源于o_agt的monitor,前者通过exp_port获取,后者通过act_port获取
- 在main_phase中通过fork建立了两个进程:
- 一个进程处理exp_port的数据,当收到数据后,把数据放入expect_queue中
- 另一个进程处理act_port的数据,这是DUT的输出数据
- 当收集到这些数据后,从expect_queue中弹出之前从exp_port收到的数据,并调用my_transaction的my_compare函数
- 采用这种比较处理方式的前提是exp_port要比act_port先收到数据
- 由于DUT处理数据需要延时,而reference model是基于高级语言的处理,一般不需要延时,因此可以保证exp_port的数据在act_port的数据之前到来
o_agt和act_port的ap的连接方式及reference model和exp_port的ap的连接方式与前一节类似(my_env.sv)
1
2
3
4
5
6
7
8
9function 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);
mdl.ap.connect(mdl_scb_fifo.analysis_export);
scb.exp_port.connect(mdl_scb_fifo.blocking_get_export);
o_agt.ap.connect(agt_scb_fifo.analysis_export);
scb.act_port.connect(agt_scb_fifo.blocking_get_export);
endfunction- i_agt中monitor的transaction送给reference model,reference model处理后的transaction送给scoreboard的exp_port,o_agt中monitor的transaction送给scoreboard的act_port,最后比较exp_port与act_port这两个端口。
完成my_scoreboard的定义后,也需要在my_env中将其实例化,此时,整棵UVM树为:
仿真结果如下:
7.加入field_automation机制
通过field_automation可以实现自动调用一些函数,在my_transaction中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38`ifndef MY_TRANSACTION__SV
`define 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;
constraint pload_cons{
pload.size >= 46;
pload.size <= 1500;
}
function bit[31:0] calc_crc();
return 32'h0;
endfunction
function void post_randomize();
crc = calc_crc;
endfunction
`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
function new(string name = "my_transaction");
super.new();
endfunction
endclass
`endif- 这里使用
uvm_object_utils_begin
和uvm_object_utils_end
来实现my_transaction的factory注册,在这两个宏中,使用uvm_field宏注册所有字段。 - uvm_field系列宏随着transaction成员变量的不同而不同,如上面的定义中出现了针对bit类型的
uvm_field_int
及针对byte类型动态数组的uvm_field_array_int - 当上述宏注册之后,可以直接调用copy、compare、print等函数,无需自己定义
- 这里使用
引入field_automation机制的另外一大好处是简化了driver和monitor
my_driver.sv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17task my_driver::drive_one_pkt(my_transaction tr);
byte unsigned data_q[];
int data_size;
data_size = tr.pack_bytes(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);
endtaskpack_bytes
将tr中所有的字段变成了byte流放入data_q中,pack_bytes极大地减少了代码量,在把所有的字段变成byte流放入data_q中时,字段按照uvm_field系列书写的顺序排列。在上述代码中是先放入dmac,再依次放入smac、ether_type、pload、crc
my_monitor.sv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26task 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;
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
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]; //da sa, e_type, crc
data_size = tr.unpack_bytes(data_array) / 8;
`uvm_info("my_monitor", "end collect one pkt", UVM_LOW);
endtask- 这里使用unpack_bytes函数将data_q中的byte流转换成tr中的各个字段
- 在 Ethernet 数据包中,有一部分是以太网帧头(Ethernet Frame Header),其中包含了目的MAC地址(6字节)、源MAC地址(6字节)、以太网类型(2字节)和 CRC 校验码(4字节),共计18字节。那么包数量占data_size - 18个bytes,所以有这行代码
tr.pload = new[data_size - 18];
仿真结果:
UVM的终极大作:sequence
1.在验证平台中加入sequencer
sequence机制用于产生激励,它是UVM中最重要的机制之一
在一个规范化的UVM验证平台中,driver只负责驱动transaction,而不负责产生transaction
sequence机制有两大组成部分,一是sequence,二是sequencer
sequencer的定义非常简单,派生自uvm_sequencer,并使用uvm_component_utils宏来注册到factory中。uvm_sequencer是一个参数化的类,其参数是my_transaction,即此sequencer产生transaction的类型
1
2
3
4
5
6
7
8
9
10
11
12
13`ifndef MY_SEQUENCER__SV
`define MY_SEQUENCER__SV
class my_sequencer extends uvm_sequencer #(my_transaction);
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
`uvm_component_utils(my_sequencer)
endclass
`endifsequencer产生transaction,而driver负责接收transaction
由于uvm_driver也是一个参数化的类,应该在定义driver时指明此driver要驱动的transaction类型
1
class my_driver extends uvm_driver#(my_transaction);
这样定义的好处是可以直接使用uvm_driver中的某些预先定义好的成员变量,如uvm_driver中有成员变量req,它的类型就是传递给uvm_driver的参数,在这里就是my_transaction,可以直接使用req
1
2
3
4
5
6
7
8
9
10
11
12
13
14task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
vif.data <= 8'b0;
vif.valid <= 1'b0;
while(!vif.rst_n)
@(posedge vif.clk);
for(int i = 0; i < 2; i++) begin
req = new("req");
assert(req.randomize() with {pload.size == 200;});
drive_one_pkt(req);
end
repeat(5) @(posedge vif.clk);
phase.drop_objection(this);
endtask- 这里依然在driver中产生激励,下一节中将会把激励产生的功能从driver中移除
在完成sequencer的定义后,由于sequencer与driver的关系非常密切,因此要把其加入agent中
1
2
3
4
5
6
7
8function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
if (is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver::type_id::create("drv", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunction在加入sequencer后,整个UVM树的结构变成了如下图的形式:
验证平台的框图:
其仿真结果与前序一致
2.sequence机制
sequence不属于验证平台的任何一部分,但是它与sequencer之间有密切的联系
- 只有在sequencer的帮助下,sequence产生出的transaction才能最终送给driver
- 没有sequence,sequencer就几乎没有任何作用
- sequence就像是一个弹夹,里面的子弹就是transaction,而sequencer是一把枪。弹夹只有放入枪中才有意义,枪只有在放入弹夹后才能发挥威力
从本质上来说,sequencer是一个uvm_component,而sequence是一个uvm_object。与my_transaction一样,sequence也有其生命周期。它的生命周期比my_transaction要更长一些,其内的transaction全部发送完毕后,它的生命周期也就结束了。因此,一个sequence应该使用uvm_object_utils宏注册到factory中(my_sequence.sv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 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)
endclass- 每一个sequence都有一个body任务,当一个sequence启动之后,会自动执行body中的代码
- 在上面的例子中,用到了一个全新的宏:
uvm_do
,这个宏是UVM中最常见的宏之一,它用于:- 创建一个my_transaction的实例m_trans
- 将其随机化
- 最终将其送给sequencer
一个sequece在向sequencer发送transaction前,要先向sequencer发送一个请求,sequencer把这个请求放在一个仲裁队列中,作为sequencer,它需要做两件事情:
第一,检验仲裁队列里是否有某个sequence发送transaction的请求
第二,检测driver是否申请transaction
如果仲裁队列里有发送请求,但driver没有申请transaction,那么sequencer将会一直处于等待driver的状态,直到driver申请新的transaction。此时,sequencer同意sequence的发送请求,sequence在得到sequencer的批准后,产生出一个transaction并交给sequencer,后者把这个transaction交给driver
如果仲裁队列中没有发送请求,但driver向sequencer申请新的transaction,那么sequencer将会处于等待sequence的状态,一直到有sequence递交发送请求,sequencer马上同意这个请求,sequence产生的transaction并交给sequencer,最终driver获得这个transaction
如果仲裁队列中有发送请求,同时driver也在向sequencer申请新的transaction,那么将会同意发送请求,sequence产生transaction并交给sequencer,最终driver获得这个transaction
driver如何向sequencer申请transaction呢?
在uvm_driver中有成员变量seq_item_port,而在uvm_sequencer中有成员变量seq_item_export,这两者之间可以建立一个“通道”
在my_agent中,使用connect函数把两者联系在一起
1
2
3
4
5
6
7function void my_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (is_active == UVM_ACTIVE) begin
drv.seq_item_port.connect(sqr.seq_item_export);
end
ap = mon.ap;
endfunction当把二者连接好之后,就可以在driver中通过get_next_item任务向sequencer申请新的transaction
1
2
3
4
5
6
7
8
9
10
11task 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在如上的代码中,一个最显著的特征是使用了while(1)循环,因为driver只负责驱动transaction,而不负责产生,只要有transaction就驱动,所以必须做成一个无限循环的形式
通过get_next_item任务来得到一个新的req,并且驱动它,驱动完成后调用item_done通知sequencer(如果在下次调用get_next_item前,item_done被调用,那么sequencer就认为driver已经得到了这个transaction)
sequence中uvm_do宏产生了一个transaction并交给sequencer,driver取走这transaction后,uvm_do并不会立刻返回执行下一次的uvm_do宏,而是等待在那里,直到driver返回item_done信号。此时,uvm_do宏才算执行完毕,返回后开始执行下一个uvm_do,并产生新的transaction
其实,除get_next_item之外,还可以使用try_next_item。get_next_item是阻塞的,它会一直等到有新的transaction才会返回,try_next_item是非阻塞的,它尝试着询问sequencer是否有新的transaction,如果有,则得到此transaction,否则就直接返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15task 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
drive_one_pkt(req);
seq_item_port.item_done();
end
end
endtask- 相比于get_next_item,try_next_item的行为更加接近真实driver的行为,当有数据时,就驱动数据,否则总线将一直处于空闲状态
sequence如何向sequencer中发送transaction呢?
前面已经定义sequence,只需要在某个component(如my_sequencer、my_env)的main_phase中启动这个sequence即可,以在my_env中启动为例:
1
2
3
4
5
6
7task 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_sequence的实例seq
之后调用start任务,任务的参数是一个sequencer指针,如果不指明此指针,则sequence不知道将产生的transaction交给哪个sequencer
仿真结果如下:
3.default_sequence的使用
在上一节的例子中,sequence是在my_env的main_phase中手工启动的,但在实际应用中,使用最多的还是通过default_sequence的方式启动sequence
使用default_sequence的方式非常简单,只需要在某个component(如my_env)的build_phase中设置如下代码即可:
1
2
3
4
5
6
7
8
9virtual 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- 上述set参数的第二个参数,取决于在哪个地方启动sequence
如果在top_tb中设置default_sequence,则:
1
2
3
4
5
6
7
8
9module 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如果在my_agent的build_phase里:
1
2
3
4
5
6
7
8
9function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
...
uvm_config_db#(uvm_object_wrapper)::set(this,
"sqr.main_phase",
"default_sequence",
my_sequence::type_id::get());
endfunction在sequence中使用starting_phase进行提起和撤销objection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class my_sequence extends uvm_sequence #(my_transaction);
my_transaction m_trans;
function new(string name= "my_sequence");
super.new(name);
endfunction
virtual task body();
if(starting_phase != null)
starting_phase.raise_objection(this);
repeat (10) begin
`uvm_do(m_trans)
end
#1000;
if(starting_phase != null)
starting_phase.drop_objection(this);
endtask
`uvm_object_utils(my_sequence)
endclass
`endif仿真结果同上一节一致
建造测试用例
1.加入base_test
UVM使用的是一种树形结构,最初这棵树的树根是my_driver,后来由于要放置其他component,树根变成了my_env
但在一个实际应用的UVM验证平台中,my_env并不是树根,通常来说,树根是一个基于uvm_test派生的类。真正的测试用例都是基于base_test派生的一个类(base_test.sv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43`ifndef BASE_TEST__SV
`define 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_sequence",
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_severity_count(UVM_ERROR);
if (err_num != 0) begin
$display("TEST CASE FAILED");
end
else begin
$display("TEST CASE PASSED");
end
endfunction
`endifbase_test派生自uvm_test,使用uvm_component_utils宏来注册到factory中
在build_phase中实例化my_env,并设置sequencer的default_sequence
上面的代码中出现了report_phase,在report_phase中根据UVM_ERROR的数量来打印不同的信息。report_phase也是UVM内建的一个phase,它在main_phase结束之后执行
通常在base_test中做如下事情:
- 第一,设置整个验证平台的超时退出时间
- 第二,通过config_db设置验证平台中某些参数的值
在把my_env放入base_test中之后,UVM树的层次结构变成:
top_tb中run_test的参数从my_env变成了base_test,并且config_db中设置virtual interface的路径参数要做如下改变:
1
2
3
4
5
6
7
8
9initial begin
run_test("base_test");
end
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.i_agt.drv", "vif", input_if);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.i_agt.mon", "vif", input_if);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.o_agt.mon", "vif", output_if);
end仿真结果如下:
2.UVM中测试用例的启动
UVM会利用UVM_TESTNAME从命令行中寻找测试用例(以测试不同的sequence)的名字,创建它的实例并运行(注意
+UVM_TESTNAME=my_case1
)1
vsim -t ns -voptargs=+acc -sv_lib $UVM_DPI_DIR/uvm_dpi work_design.top_tb +UVM_TESTNAME=my_case1
整个启动及执行的流程:
启动后,整棵UVM树的结构:
仿真结果:
附加知识点
modelsim下运行UVM仿真平台
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17vlib ./lib/
vlib ./lib/work_design
vmap work_design ./lib/work_design
set UVM_HOME D:/SoftWare/ModelSim/verilog_src/uvm-1.1d/src
set WORK_HOME D:/App_Data_File/ModelSim_project/UVM_Sim/UVM_learning_project
set UVM_DPI_DIR D:/SoftWare/ModelSim/uvm-1.1d/win64
vlog +incdir+$UVM_HOME -L mtiAvm -L mtiOvm -L mtiUvm -L mtiUPF $UVM_HOME/uvm_pkg.sv
vlog -work work_design $WORK_HOME/ch2/dut/dut.sv $WORK_HOME/ch2/section2.2/2.2.1/sim/top_tb.sv
vsim -t ns -voptargs=+acc -sv_lib $UVM_DPI_DIR/uvm_dpi work_design.top_tb
add wave -position insertpoint sim:/top_tb/*
run -all有关-sv_lib的解释:
-sv_lib
:这个选项用于指定一个SystemVerilog的DPI(Direct Programming Interface)库。DPI允许SystemVerilog代码调用C/C++函数,常用于模拟外部设备或高级功能。(来自GPT4)-position
:这是一个选项,用来指定信号在波形窗口中的添加位置。insertpoint
:这个参数的值指定了具体的插入点。在ModelSim的波形窗口中,insertpoint
指的是当前选中的波形组或信号的位置。如果你在波形窗口中选中了一个特定的信号或波形组,并执行带有-position insertpoint
的命令,新添加的信号会被放置在选中的波形或组之后。- 举个例子:
- 如果你没有选中任何波形或信号,新的信号通常会被添加到波形列表的最前面或最后面,具体取决于ModelSim的默认行为或之前的配置。
- 如果你选中了一个信号,然后运行带有
-position insertpoint
的add wave
命令,新的信号将被插入到你选中的信号后面。(来自GPT4)
- 举个例子:
本人能力和时间有限,有关run.do中的一些脚本命令,我也不是很清楚,但用到的都会在此记录。
搭建UVM仿真平台时,一开始会一直报vlog(12110)以及vlog一行出错的命令,折腾了我很久(因为以为是vlog使用有问题,其实是modelsim自身的问题),最后参考vlog 12110错误及解决-CSDN博客,重启后解决(记得之前好像也是用脚本仿真出问题了,设置voptflow = 0才能仿真,但这次又重新设置为1才可以)