一、Driver驱动器

这段代码是用 SystemVerilog 编写的一个基于 UVM(Universal Verification Methodology)的驱动器(driver)组件,名为 my_driver。它继承自 uvm_driver 类,用于在验证环境中驱动 DUT(Design Under Test,被测设计)的输入信号。下面我将逐步解释这段代码的结构和功能。

1.1 整体结构

代码分为两个主要部分:

  • 类定义部分:定义了 my_driver 类,包括构造函数和一个外部声明的任务(main_phase)。
  • 任务实现部分:实现了 main_phase 任务,负责具体的信号驱动逻辑。

此外,代码使用 ifndef 和 define 宏来防止重复包含。

1.2 详细解释

1.2.1 宏定义保护

1
2
ifndef MY_DRIVER__SV
define MY_DRIVER__SV

检查是否已经定义了宏MY_DRIVER__SV。如果没有定义,则编译器会继续处理后面的代码。
最后,文件末尾的 endif 与开头的 ifndef 配对,结束条件编译块。

1.2.2 类定义

1
2
3
4
5
6
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

class my_driver extends uvm_driver:定义一个名为 my_driver 的类,并且表示 my_driver 继承自 UVM 提供的基类 uvm_driver。
uvm_driver 是 UVM 框架中的一个标准组件类,用于将事务级数据转换为 DUT 的引脚级信号。

事务级数据是指更高层次的抽象数据,通常以结构体或类的形式表示,而不是直接的硬件信号(0 和 1)。它描述的是“做什么”,而不是“怎么做”。
假设事务是一个 8 位数据 8’b10100101。
驱动器将其转换为:
top_tb.rxd <= 8’b10100101;(数据信号)
top_tb.rx_dv <= 1’b1;(有效信号)
并在 @(posedge top_tb.clk) 时更新这些信号。

function new:定义类的构造函数,用于创建 my_driver 对象。
super.new(name, parent):调用父类 uvm_driver 的构造函数,将 name 和 parent 参数传递给它。这是 UVM 中面向对象编程的标准做法,确保父类的初始化逻辑被执行。
extern:表示 main_phase 任务的实现不在类定义内部,而是在外部单独定义。
virtual:声明这是一个虚任务,允许子类重写(override)它。这是 UVM 中 phase 方法的常见做法。
task main_phase(uvm_phase phase):定义一个名为 main_phase 的任务,接收一个 uvm_phase 类型的参数 phase,表示 UVM 的仿真阶段(这里是 main_phase,通常用于主要的测试执行阶段)。

1.2.3 任务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

my_driver::main_phase:明确指定这个任务是 my_driver 类的一部分。
这是 main_phase 的具体实现,负责驱动 DUT 的信号。
uvm_info(“my_driver”, “data is drived”, UVM_LOW):
UVM 提供的日志记录宏,打印信息。
my_driver:消息来源(组件名)。
data is drived:消息内容。
UVM_LOW:日志级别,表示低详细程度。
作用:在 256 个时钟周期内,连续向 DUT 的 rxd 输入随机数据,并将 rx_dv 置为 1,同时记录日志。

所谓类的定义,就是用编辑器写下:

1
2
3
>class A
>...
>endclass

而所谓类的实例化指的是通过new创造出A的一个实例:

1
2
>A a_list;
>a_list = new();

1.2.4 factory机制

factory机制的实现被集成在了一个宏中:uvm_component_utils。这个宏所做的事情非常多,其中之一就是将my_driver登记在UVM内部的一张表中,这张表是 factory 功能实现的基础。只要在定义一个新的类时使用这个宏,就相当于把这个类注册到了这张表中。

1
`uvm_component_utils(my_driver)

在给driver中加入factory机制后,还需要对top_tb做一些改动:

1
2
3
initial begin
run_test("my_driver");
end

但是输出的结果只有两个,没有执行后面的代码,关于这个问题,牵涉UVM的objection机制。

1
2
3
UVM_INFO my_driver.sv(8) @ 0: uvm_test_top [my_driver] new is called
UVM_INFO @ 0: reporter [RNTST] Running test my_driver...
UVM_INFO my_driver.sv(14) @ 0: uvm_test_top [my_driver] main_phase is called

1.2.4 objection机制

UVM中通过objection机制来控制验证平台的关闭。细心的读者可能发现,在上节的例子中,并没有如2.2.1节所示显式地调用 finish 语句来结束仿真。但是在运行上节例子时,仿真平台确实关闭了。在每个phase中,UVM会检查是否有objection被提起 (raise_objection),如果有,那么等待这个objection被撤销(drop_objection)后停止仿真;如果没有,则马上结束当前 phase。

1
2
3
4
5
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
...
phase.drop_objection(this);
endtask

raise_objection语句必须在main_phase中第一个消耗仿真时间的语句之前。

1.2.5 加入virtual interface

使用该方法能够杜绝因为绝对路径所带来的不便,在SystemVerilog中使用interface来连接验证平台与DUT的端口,该端口可以认为是一种总线。
定义interface的方法如下:

1
2
3
4
5
6
7
8
9
`ifndef MY_IF__SV
`define MY_IF__SV

interface my_if(input clk, input rst_n);
logic [7:0] data;
logic valid;
endinterface

`endif

因为my_driver是一个类,在类中不能使用声明的方法定义一个 interface,只有在类似top_tb这样的模块(module)中才可以。在类中使用的是virtual interface:

1
virtual my_if vif;

因此在 中就可以使用该方法来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

下面的问题是,如何把top_tb中的input_if和my_driver中的vif对应起来。
针对该问题,UVM引进了config_db机制。在config_db机制中,分为set和get两步操作。set就是读取数据,get就是输出数据。在top_tb中的代码如下所示:

1
2
3
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
end

在my_driver中的代码如下所示:

1
2
3
4
5
6
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

首先build_phase也是内置函数,build_phase在new函数之后main_phase之前执行。其中的super.build_phase语句是因为在其父类的build_phase中执行了一些必要的操作。
其中还出现了uvm_fatal宏,其与uvm_info的作用类似。uvm_fatal的出现表示验证平台出现了重大问题而无法继续下去,必须停止仿真并做相应的检查。
config_db的set和get函数都有四个参数,这两个函数的第三个参数必须完全一致。

  • set函数的第四个参数表示要将哪个interface通过config_db传递给my_driver
  • get函数的第四个参数表示把得到的interface传递给哪个my_driver的成员变量。

set函数的第二个参数表示的是路径索引,UVM通过run_test语句创建一个名字为uvm_test_top的实例,因此需要输入uvm_test_top。无论传递给run_test的参数是什么,创建的实例的名字都为uvm_test_top。其他两个参数以后再说。
set函数与get函数使用双冒号是因为这两个函数都是静态函数,而前面的#键是要传递的类型,这里是virtual my_if。

二、transaction组件

transaction就是一个提供数据传输的打包操作。在不同的验证平台中,会有不同的transaction。一个简单的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
`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和smac模拟的就是发送地址和接受地址,ether_type是以太网类型,pload是其携带数据的大小。下面的函数是用于约束上述数据的。通过pload_cons约束将其大小被限制在46~1500byte,CRC暂且使用post_randomize中加的一个空函数calc_crc来对其定义,有兴趣的读者可以将其补充完整。

post_randomize是SystemVerilog中提供的一个函数,当某个类的实例的randomize函数被调用后,post_randomize会紧随其后无条件地被调用。

在transaction定义中,有两点值得引起注意:

  1. my_transaction的基类是uvm_sequence_item。
    在UVM中,所有的transaction都要从uvm_sequence_item派生
  2. 是这里没有使用uvm_component_utils宏来实现factory机制,而是使用了uvm_object_utils。

下面便是使用transaction的my_driver代码:

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
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
//push ether_type to data_q
//push payload to data_q
//push crc to data_q

`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

在main_phase中,先使用randomize将tr随机化,之后通过drive_one_pkt任务将tr的内容驱动到DUT的端口上。
在drive_one_pkt中,先将tr中所有的数据压入队列data_q中,之后再将data_q中所有的数据弹出输入到DUT端口上。

三、env组件

为了能够更好的实例化my_dirver等组件,需要有一个容器去把他们装在一起,这个容器就是env,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
`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

在my_env的定义中,使用了区别于new的方式,只有使用这种方式实例化的实例,验证平台中的组件在实例化时都应该使用type_name::type_id::create的方式。
回顾一下my_driver的new函数:

1
2
3
function new(string name = "my_driver", uvm_component parent = null); 
super.new(name, parent);
endfuncti

可以看出 my_driver 的父结点就是my_env。通过parent的形式,UVM建立起了树形的组织结构。在这种树形的组织结构中,由run_test创建的实例是树根,并且树根的名字是固定的为uvm_test_top,长出枝叶的过程需要在my_env的build_phase中手动实现。
无论是树根还是树叶,都必须由 uvm_component 或者其派生类继承而来。整棵UVM树的结构如图所示。

在UVM的树形结构中,build_phase的执行遵照从树根到树叶的顺序。 在top_tb中使用config_db机制传递virtual my_if时,要改变相应的路径;同时,run_test的参数也从my_driver变为了my_env。
1
2
3
4
5
6
7
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
set函数的第二个参数从uvm_test_top变为了uvm_test_top.drv,其中uvm_test_top是UVM自动创建的树根的名字,而drv则是在my_env的build_phase中实例化drv时传递过去的名字。 # 四、monitor组件 验证平台中实现监测DUT行为的组件是monitor,其主要功能起到一个监测作用。其将用于收集DUT的端口数据,并将其转换成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
39
40
41
42
43
44
`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
//pop smac
//pop ether_type
//pop payload
//pop crc
`uvm_info("my_monitor", "end collect one pkt, print it:", UVM_LOW);
tr.my_print();
endtask
`endif
该代码与my_driver非常相似。其主要工作与my_driver相反,my_driver用于产生驱动信号,而该模块则用于收集。 最后要在evn中进行实例化:
1
2
3
4
5
6
virtual 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
需要注意的是这里定义了两个my_monitor模块,一个收集输入的,另一个收集输出的。树形结构如下所示:

五、agent组件

因为my_monitor和my_dirver有相似性,因此可以将两者封装在一起,使用agent组件,代码如下:

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
`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

这里有一点比较疑惑,为什么build_phase和connect_phase要在外面定义?为什么不在里面?

里面的is_active相当于一个宏定义,用于判断是否实例化dirver,比如再输入的时候需要实例化去驱动,但是在输出就不需要。因此,env的代码就变成下面的样子:

1
2
3
4
5
6
7
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_ACTIVE和UVM_PASSIVE是两个枚举。UVM树形结构变成下面这样:

五、reference model组件

reference model用于完成和DUT相同的功能,用于与设计的验证平台在后面的计分板上做对比。改模块的代码如下所示:

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

可以看出,其主要就是复制了一份tr从ap到port。
但是其中的难点在于如何将 my_model 与其他模块进行通信。在UVM中,通常使用TLM(Transaction Level Modeling)实现component之间transaction级别 的通信。得到的UVM树形图如下所示:

这里需要注意数据流动的方向,是从i_agt流动到mdl,而数据是i_agt中的my_monitor。因此在 my_monitor 需要定义一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uvm_analysis_port #(my_transaction)  ap;
virtual function void build_phase(uvm_phase phase);
...
ap = new("ap", this);
endfunction

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

uvm_analysis_port是一个参数化的类,其参数就是这个analysis_port需要传递的数据的类型,在本节中是my_transaction。到此,在my_monitor中需要为transaction通信准备的工作已经全部完成。
UVM的transaction级别通信的数据接收方式也有多种,其中一种就是使用uvm_blocking_get_port。该接收端已经在 my_monitor 中定义好了。可以往前去看my_monitor的代码。
在 my_monitor 和 my_model 中定义并实现了各自的端口之后,通信的功能并没有实现,还需要在 my_env 中使用 fifo 将两个端口联系在一起。下面是my_env 中的代码:

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
class my_env extends uvm_env;
uvm_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
...
endclass

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

`endif
```java
fifo的类型是uvm_tlm_analysis_fifo,其参数是存储在其中的transaction的类型。
>这里引入了connect_phase。它的执行顺序并不是从树根到树叶,而是从树叶到树根——先执行driver和 monitor的connect_phase,再执行agent的connect_phase,最后执行env的connect_phase。

但是该连接是与i_agt进行连接,怎么打通i_agt与my_monitor之间的通道呢?就是使用指针的方式。i_agt中的代码如下:
```java
uvm_analysis_port #(my_transaction) ap;
function void my_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
ap = mon.ap;
endfunction

在这个代码里面没有实例化,直接将mon中的ap传给i_agt中的ap,就是用指针的形式,在访问i_agt中的ap时等价于访问mon中的ap。

六、scoreboard组件

该模块的作用就是比较DUT以及镜像模块的输出数值。代码如下:

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
`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需要比较两种数据,前者通过exp_port获取,而后者通过 act_port获取。在main_phase中通过fork建立起了两个进程:

  • 一个进程处理exp_port的数据,当收到数据后,把数据放入expect_queue中。
  • 另外一个进程处理act_port的数据,这是DUT的输出数据,当收集到这些数据后,将参考数据从队列里面弹出,并调用my_transaction的my_compare函数。

最终的UVM树形图如下所示:

my_transaction的my_compare函数很简单,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function bit my_compare(my_transaction tr);
bit result;

if(tr == null)
`uvm_fatal("my_transaction", "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
for(int i = 0; i < pload.size(); i++) begin
if(pload[i] != tr.pload[i])
result = 0;
end
return result;
endfunction

还有两个端口与外界的连接,在书里表示不在过多赘述,这里我简单说一下:首先有两个连接,一个是o_agt的数据,还有一个是my_model中的镜像数据,两者的输入接口都使用uvm_analysis_port #(my_transaction) ap;来定义。因此在本组件中uvm_blocking_get_port定义接受,连接代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
   uvm_tlm_analysis_fifo #(my_transaction) agt_scb_fifo;
uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
uvm_tlm_analysis_fifo #(my_transaction) mdl_scb_fifo;

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);
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

七、field_automation机制

在my_transaction有三个函数,分别为my_print、my_copy以及my_compare函数。使用UVM中的field_automation机制可以将以上三个函数进行整合,该机制使用uvm_field系列宏实现:

1
2
3
4
5
6
7
`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

这里使用uvm_object_utils_begin和uvm_object_utils_end来实现my_transaction的factory注册,在这两个宏中间,使用uvm_field宏注册所有字段。通过这样的操作可以直接调用copy、compare、print等函数,而无需自己定义。
引入field_automation机制的另外一大好处是简化driver和monitor。my_driver的drv_one_pkt任务和 my_monitor的collect_one_pkt任务代码很长,其作用主要是将数据通过tran连接到DUT上。使用field_automation机制后,drv_one_pkt任务可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
task 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);
endtask

其中调用pack_bytes将tr中所有的字段变成byte流放入data_q中,减少了代码量。同理,在monitor中的解析也是这样:

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
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;

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中的各个字段。但是这里值得注意的是,unpack_bytes函数的输入参数必须是一个动态数组,所以需要先把收集到的数据放在data_q中的数据复制到一个动态数组中。==由于tr中的pload是一个动态数组,所以需要在调用 unpack_bytes 之前指定其大小,这样unpack_bytes函数才能正常工作(这里看不太懂)==。

七、sequence组件

sequence实际上就是一个产生激励的工具,在之前激励都是由my_dirver产生的,这次变为了sequence。在 一个规范化的UVM验证平台中,driver只负责驱动transaction,而不负责产生transaction。sequence机制有两大组成部分,一是 sequence,二是sequencer。

7.1 sequencer

下面是sequencer的代码部分:

1
2
3
4
5
6
7
8
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

可以看到,uvm_sequencer是一个参数化的类,其参数是my_transaction,即此sequencer产生的transaction的类型。但是,我们上文中的dirver其实也是参数化的类,应该在定义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
14
task 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

然后将该模块加入agent,得到的图如下所示:

7.2 sequence机制

下面是前面提到的整个UVM的结构图,可以看见sequence的位置在比较偏的地方。这说明sequence并不是一个company而是一个object。

其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
endclass

可以看出,该模块在定义时同样要指定产生的transaction的类型,这里是my_transaction。每一个sequence都有一个body任务,当一个sequence启动之后,会自动执行body中的代码。
在上面的例子中,用到了uvm_do,其作用为:

  • 创建一个my_transaction的实例m_trans
  • 将其随机化
  • 最终将其送给sequencer

下一步就是要将uvm_driver和uvm_sequencer以及uvm_sequencer和uvm_sequencer连接起来。
在uvm_driver中有成员变量seq_item_port,而在uvm_sequencer中有成员变量seq_item_export,这两者之间可以建立一个“通道”,通道中传递的transaction类型就是定义my_sequencer和my_driver时指定的transaction类型。因此在my_agent中, 使用connect函数把两者联系在一起:

1
2
3
4
5
6
7
function 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

链接之后,dirver就可以向sequencer申请。代码如下:

1
2
3
4
5
6
7
8
9
10
11
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。这里为什么会有一个item_done呢,其主要作用就是让sequencer知道dirver已经接收到了这个req,形成一个类似于握手的机制。
uvm_do宏产生了一个transaction并交给sequencer,driver取走这个transaction后,uvm_do并不会立刻返回执行下一次的uvm_do宏,而是等待在那里,直到driver返回item_done信号。此时,uvm_do宏才算是执行完毕,返回后开始执行下一个uvm_do,并产生新的transaction。

然后就是最后一个问题就是将uvm_sequencer和uvm_sequencer连接起来,可以直接在UVM的根部进行定义:

1
2
3
4
5
6
7
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_sequence的实例seq,之后调用start任务。start任务的参数是一个sequencer指针。
当然其实还有另一种方法来让dirver获得tran,就是使用try_next_item函数,上文中的get_next_item是阻塞的,而try_next_item则是非阻塞的,这样大大提高了代码的灵活性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
drive_one_pkt(req);
seq_item_port.item_done();
end
end
endtask

7.3 default_sequence机制

在刚才,sequence是在my_env的main_phase中手工启动的,但是在实际应用中, 使用最多的还是通过default_sequence的方式启动sequence。default_sequence的启动方式很简单,只需要在任意地方加入如下代码(以my_env举例):

1
2
3
4
uvm_config_db#(uvm_object_wrapper)::set(this,
"i_agt.sqr.main_phase",
"default_sequence",
my_sequence::type_id::get());

该代码同样使用了uvm_config_db,但是这里是在类里面调用的,第二个参数是相对于第一个参数的相对路径,由于上述代码是在my_env中,所以第二个参数中就不需 要uvm_test_top了。在top_tb中设置virtual interface时,由于top_tb不是一个类,无法使用this指针,所以设置set的第一个参数为null,并且第二个参数使用绝对路径uvm_test_top.xxx。
在第二个路径参数中,出现了main_phase。这是因为该代码是在这个位置的main_phase启动的。
至于set的第三个和第四个参数,书上说记住就行。
还有一个问题就是,在上一节启动sequence前后,分别提起和撤销objection,这里也需要加上这两个操作。sequencer在启动default_sequence时,会自动将自己传给sequence的starting_phase,因此可以这样写:

1
2
3
4
5
6
7
8
9
10
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

ok,结束。

八、bast_test组件

没想到吧,其实uvm的树根不是env,而是这个东西。该模块的代码如下:

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
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

代码很常规,但需要注意的是,这里设置了default_sequence,其他地方就不需要再设置了。
上面的代码中出现了report_phase,在report_phase中根据UVM_ERROR的数量来打印不同的信息,其在main_phase结束之后执行。
除了上述操作外,还通常在base_test中做如下事情:

  • 设置整个验证平台的超时退出时间;
  • 通过config_db设置验证平台中某些参数的值。

最终得到的树形结构如下所示:

九、总结

到现在为止,一个基本的UVM结构已经完全构建完毕了,后面我会继续为大家分享uvm的相关知识以及项目。谢谢大家支持!!!