本节书摘来自华章社区《UVM实战》一书中的第2章,第2.3节为验证平台加入各个组件,作者 张 强,更多章节内容可以访问云栖社区“华章社区”公众号查看
2.3 为验证平台加入各个组件
2.3.1 加入transaction
在2.2节中,所有的操作都是基于信号级的。从本节开始将引入reference model、monitor、scoreboard等验证平台的其他组件。在这些组件之间,信息的传递是基于transaction的,因此,本节将先引入transaction的概念。
transaction是一个抽象的概念。一般来说,物理协议中的数据交换都是以帧或者包为单位的,通常在一帧或者一个包中要定义好各项参数,每个包的大小不一样。很少会有协议是以bit或者byte为单位来进行数据交换的。以以太网为例,每个包的大小至少是64byte。这个包中要包括源地址、目的地址、包的类型、整个包的CRC校验数据等。transaction就是用于模拟这种实际情况,一笔transaction就是一个包。在不同的验证平台中,会有不同的transaction。一个简单的transaction的定义如下:
代码清单 2-23
4 class my_transaction extends uvm_sequence_item;
5
6 rand bit[47:0] dmac;
7 rand bit[47:0] smac;
8 rand bit[15:0] ether_type;
9 rand byte pload[];
10 rand bit[31:0] crc;
11
12 constraint pload_cons{
13 pload.size >= 46;
14 pload.size <= 1500;
15 }
16
17 function bit[31:0] calc_crc();
18 return 32'h0;
19 endfunction
20
21 function void post_randomize();
22 crc = calc_crc;
23 endfunction
24
25 `uvm_object_utils(my_transaction)
26
27 function new(string name = "my_transaction");
28 super.new(name);
29 endfunction
30 endclass
其中dmac是48bit的以太网目的地址,smac是48bit的以太网源地址,ether_type是以太网类型,pload是其携带数据的大小,通过pload_cons约束可以看到,其大小被限制在46~1500byte,CRC是前面所有数据的校验值。由于CRC的计算方法稍显复杂,且其代码在网络上随处可见,因此这里只是在post_randomize中加了一个空函数calc_crc,有兴趣的读者可以将其补充完整。post_randomize是SystemVerilog中提供的一个函数,当某个类的实例的randomize函数被调用后,post_randomize会紧随其后无条件地被调用。
在transaction定义中,有两点值得引起注意:一是my_transaction的基类是uvm_sequence_item。在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驱动,再经过reference model处理,最终由scoreboard比较完成后,其生命周期就结束了。一般来说,这种类都是派生自uvm_object或者uvm_object的派生类,uvm_sequence_item的祖先就是uvm_object。UVM中具有这种特征的类都要使用uvm_object_utils宏来实现。
当完成transaction的定义后,就可以在my_driver中实现基于transaction的驱动:
代码清单 2-24
22 task my_driver::main_phase(uvm_phase phase);
23 my_transaction tr;
…
29 for(int i = 0; i < 2; i++) begin
30 tr = new("tr");
31 assert(tr.randomize() with {pload.size == 200;});
32 drive_one_pkt(tr);
33 end
…
36 endtask
37
38 task my_driver::drive_one_pkt(my_transaction tr);
39 bit [47:0] tmp_data;
40 bit [7:0] data_q[$];
41
42 //push dmac to data_q
43 tmp_data = tr.dmac;
44 for(int i = 0; i < 6; i++) begin
45 data_q.push_back(tmp_data[7:0]);
46 tmp_data = (tmp_data >> 8);
47 end
48 //push smac to data_q
…
54 //push ether_type to data_q
…
60 //push payload to data_q
…
64 //push crc to data_q
65 tmp_data = tr.crc;
66 for(int i = 0; i < 4; i++) begin
67 data_q.push_back(tmp_data[7:0]);
68 tmp_data = (tmp_data >> 8);
69 end
70
71 `uvm_info("my_driver", "begin to drive one pkt", UVM_LOW);
72 repeat(3) @(posedge vif.clk);
73
74 while(data_q.size() > 0) begin
75 @(posedge vif.clk);
76 vif.valid <= 1'b1;
77 vif.data <= data_q.pop_front();
78 end
79
80 @(posedge vif.clk);
81 vif.valid <= 1'b0;
82 `uvm_info("my_driver", "end drive one pkt", UVM_LOW);
83 endtask
在main_phase中,先使用randomize将tr随机化,之后通过drive_one_pkt任务将tr的内容驱动到DUT的端口上。在drive_one_pkt中,先将tr中所有的数据压入队列data_q中,之后再将data_q中所有的数据弹出并驱动。将tr中的数据压入队列data_q中的过程相当于打包成一个byte流的过程。这个过程还可以使用SystemVerlog提供的流操作符实现。具体请参照SystemVerilog语言标准IEEE Std 1800TM—2012(IEEE Standard for SystemVerilog—Unified Hardware Design, Specification, and Verification Language)的11.4.14节。
2.3.2 加入env
在验证平台中加入reference model、scoreboard等之前,思考一个问题:假设这些组件已经定义好了,那么在验证平台的什么位置对它们进行实例化呢?在top_tb中使用run_test进行实例化显然是不行的,因为run_test函数虽然强大,但也只能实例化一个实例;如果在top_tb中使用2.2.1节中实例化driver的方式显然也不可行,因为run_test相当于在top_tb结构层次之外建立一个新的结构层次,而2.2.1节的方式则是基于top_tb的层次结构,如果基于此进行实例化,那么run_test的引用也就没有太大的意义了;如果在driver中进行实例化则更加不合理。
这个问题的解决方案是引入一个容器类,在这个容器类中实例化driver、monitor、reference model和scoreboard等。在调用run_test时,传递的参数不再是my_driver,而是这个容器类,即让UVM自动创建这个容器类的实例。在UVM中,这个容器类称为uvm_env:
代码清单 2-25
4 class my_env extends uvm_env;
5
6 my_driver drv;
7
8 function new(string name = "my_env", uvm_component parent);
9 super.new(name, parent);
10 endfunction
11
12 virtual function void build_phase(uvm_phase phase);
13 super.build_phase(phase);
14 drv = my_driver::type_id::create("drv", this);
15 endfunction
16
17 `uvm_component_utils(my_env)
18 endclass
所有的env应该派生自uvm_env,且与my_driver一样,容器类在仿真中也是一直存在的,使用uvm_component_utils宏来实现factory的注册。
在my_env的定义中,最让人难以理解的是第14行drv的实例化。这里没有直接调用my_driver的new函数,而是使用了一种古怪的方式。这种方式就是factory机制带来的独特的实例化方式。只有使用factory机制注册过的类才能使用这种方式实例化;只有使用这种方式实例化的实例,才能使用后文要讲述的factory机制中最为强大的重载功能。验证平台中的组件在实例化时都应该使用type_name::type_id::create的方式。
在drv实例化时,传递了两个参数,一个是名字drv,另外一个是this指针,表示my_env。回顾一下my_driver的new函数:
代码清单 2-26
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
endfuncti
这个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或者其派生类继承而来。整棵UVM树的结构如图2-3所示。
当加入了my_env后,整个验证平台中存在两个build_phase,一个是my_env的,一个是my_driver的。那么这两个build_phase按照何种顺序执行呢?在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:
代码清单 2-27
42 initial begin
43 run_test("my_env");
44 end
45
46 initial begin
47 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", inp ut_if);
48 end
set函数的第二个参数从uvm_test_top变为了uvm_test_top.drv,其中uvm_test_top是UVM自动创建的树根的名字,而drv则是在my_env的build_phase中实例化drv时传递过去的名字。如果在实例化drv时传递的名字是my_drv,那么set函数的第二个参数中也应该是my_drv:
代码清单 2-28
class my_env extends uvm_env
…
drv = my_driver::type_id::create("my_drv", this);
…
endclass
module top_tb;
…
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.my_drv", "vif", inpu t_if);
end
endmodule
2.3.3 加入monitor
验证平台必须监测DUT的行为,只有知道DUT的输入输出信号变化之后,才能根据这些信号变化来判定DUT的行为是否正确。
验证平台中实现监测DUT行为的组件是monitor。driver负责把transaction级别的数据转变成DUT的端口级别,并驱动给DUT,monitor的行为与其相对,用于收集DUT的端口数据,并将其转换成transaction交给后续的组件如reference model、scoreboard等处理。
一个monitor的定义如下:
代码清单 2-29
3 class my_monitor extends uvm_monitor;
4
5 virtual my_if vif;
6
7 `uvm_component_utils(my_monitor)
8 function new(string name = "my_monitor", uvm_component parent = null);
9 super.new(name, parent);
10 endfunction
11
12 virtual function void build_phase(uvm_phase phase);
13 super.build_phase(phase);
14 if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
15 `uvm_fatal("my_monitor", "virtual interface must be set for vif!!!")
16 endfunction
17
18 extern task main_phase(uvm_phase phase);
19 extern task collect_one_pkt(my_transaction tr);
20 endclass
21
22 task my_monitor::main_phase(uvm_phase phase);
23 my_transaction tr;
24 while(1) begin
25 tr = new("tr");
26 collect_one_pkt(tr);
27 end
28 endtask
29
30 task my_monitor::collect_one_pkt(my_transaction tr);
31 bit[7:0] data_q[$];
32 int psize;
33 while(1) begin
34 @(posedge vif.clk);
35 if(vif.valid) break;
36 end
37
38 `uvm_info("my_monitor", "begin to collect one pkt", UVM_LOW);
39 while(vif.valid) begin
40 data_q.push_back(vif.data);
41 @(posedge vif.clk);
42 end
43 //pop dmac
44 for(int i = 0; i < 6; i++) begin
45 tr.dmac = {tr.dmac[39:0], data_q.pop_front()};
46 end
47 //pop smac
…
51 //pop ether_type
…
58 //pop payload
…
62 //pop crc
63 for(int i = 0; i < 4; i++) begin
64 tr.crc = {tr.crc[23:0], data_q.pop_front()};
65 end
66 `uvm_info("my_monitor", "end collect one pkt, print it:", UVM_LOW);
67 tr.my_print();
68 endtask
有几点需要注意的是:第一,所有的monitor类应该派生自uvm_monitor;第二,与driver类似,在my_monitor中也需要有一个virtual my_if;第三,uvm_monitor在整个仿真中是一直存在的,所以它是一个component,要使用uvm_component_utils宏注册;第四,由于monitor需要时刻收集数据,永不停歇,所以在main_phase中使用while(1)循环来实现这一目的。
在查阅collect_one_pkt的代码时,可以与my_driver的drv_one_pkt对比来看,两者代码非常相似。当收集完一个transaction后,通过my_print函数将其打印出来。my_print在my_transaction中定义如下:
代码清单 2-30
31 function void my_print();
32 $display("dmac = %0h", dmac);
33 $display("smac = %0h", smac);
34 $display("ether_type = %0h", ether_type);
35 for(int i = 0; i < pload.size; i++) begin
36 $display("pload[%0d] = %0h", i, pload[i]);
37 end
38 $display("crc = %0h", crc);
39 endfunction
当完成monitor的定义后,可以在env中对其进行实例化:
代码清单 2-31
文件:src/ch2/section2.3/2.3.3/my_env.sv
4 class my_env extends uvm_env;
5
6 my_driver drv;
7 my_monitor i_mon;
8
9 my_monitor o_mon;
…
15 virtual function void build_phase(uvm_phase phase);
16 super.build_phase(phase);
17 drv = my_driver::type_id::create("drv", this);
18 i_mon = my_monitor::type_id::create("i_mon", this);
19 o_mon = my_monitor::type_id::create("o_mon", this);
20 endfunction
…
23 endclass
需要引起注意的是这里实例化了两个monitor,一个用于监测DUT的输入口,一个用于监测DUT的输出口。DUT的输出口设置一个monitor没有任何疑问,但是在DUT的输入口设置一个monitor有必要吗?由于transaction是由driver产生并输出到DUT的端口上,所以driver可以直接将其交给后面的reference model。在2.1节所示的框图中,也是使用这样的策略。所以是否使用monitor,这个答案仁者见仁,智者见智。这里还是推荐使用monitor,原因是:第一,在一个大型的项目中,driver根据某一协议发送数据,而monitor根据这种协议收集数据,如果driver和monitor由不同人员实现,那么可以大大减少其中任何一方对协议理解的错误;第二,在后文将会看到,在实现代码重用时,使用monitor是非常有必要的。
现在,整棵UVM树的结构如图2-4所示。
在env中实例化monitor后,要在top_tb中使用config_db将input_if和output_if传递给两个monitor:
代码清单 2-32
47 initial begin
48 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", input_if);
49 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_mon", "vif", input_if);
50 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_mon", "vif", output_if);
51 end
*2.3.4 封装成agent
上一节在验证平台中加入monitor时,读者看到了driver和monitor之间的联系:两者之间的代码高度相似。其本质是因为二者处理的是同一种协议,在同样一套既定的规则下做着不同的事情。由于二者的这种相似性,UVM中通常将二者封装在一起,成为一个agent。因此,不同的agent就代表了不同的协议。
代码清单 2-33
4 class my_agent extends uvm_agent ;
5 my_driver drv;
6 my_monitor mon;
7
8 function new(string name, uvm_component parent);
9 super.new(name, parent);
10 endfunction
11
12 extern virtual function void build_phase(uvm_phase phase);
13 extern virtual function void connect_phase(uvm_phase phase);
14
15 `uvm_component_utils(my_agent)
16 endclass
17
18
19 function void my_agent::build_phase(uvm_phase phase);
20 super.build_phase(phase);
21 if (is_active == UVM_ACTIVE) begin
22 drv = my_driver::type_id::create("drv", this);
23 end
24 mon = my_monitor::type_id::create("mon", this);
25 endfunction
26
27 function void my_agent::connect_phase(uvm_phase phase);
28 super.connect_phase(phase);
29 endfunction
所有的agent都要派生自uvm_agent类,且其本身是一个component,应该使用uvm_component_utils宏来实现factory注册。
这里最令人困惑的可能是build_phase中为何根据is_active这个变量的值来决定是否创建driver的实例。is_active是uvm_agent的一个成员变量,从UVM的源代码中可以找到它的原型如下:
代码清单 2-34
来源:UVM源代码
uvm_active_passive_enum is_active = UVM_ACTIVE;
而uvm_active_passive_enum是一个枚举类型变量,其定义为:
代码清单 2-35
来源:UVM源代码
typedef enum bit { UVM_PASSIVE=0, UVM_ACTIVE=1 } uvm_active_passive_enum;
这个枚举变量仅有两个值:UVM_PASSIVE和UVM_ACTIVE。在uvm_agent中,is_active的值默认为UVM_ACTIVE,在这种模式下,是需要实例化driver的。那么什么是UVM_PASSIVE模式呢?以本章的DUT为例,如图2-5所示,在输出端口上不需要驱动任何信号,只需要监测信号。在这种情况下,端口上是只需要monitor的,所以driver可以不用实例化。
在把driver和monitor封装成agent后,在env中需要实例化agent,而不需要直接实例化driver和monitor了:
代码清单 2-36
4 class my_env extends uvm_env;
5
6 my_agent i_agt;
7 my_agent o_agt;
…
13 virtual function void build_phase(uvm_phase phase);
14 super.build_phase(phase);
15 i_agt = my_agent::type_id::create("i_agt", this);
16 o_agt = my_agent::type_id::create("o_agt", this);
17 i_agt.is_active = UVM_ACTIVE;
18 o_agt.is_active = UVM_PASSIVE;
19 endfunction
…
22 endclass
完成i_agt和o_agt的声明后,在my_env的build_phase中对它们进行实例化后,需要指定各自的工作模式是active模式还是passive模式。现在,整棵UVM树变为了如
图2-6所示形式。
由于agent的加入,driver和monitor的层次结构改变了,在top_tb中使用config_db设置virtual my_if时要注意改变路径:
代码清单 2-37
48 initial begin
49 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.drv", "vif", input_if);
50 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.mon", "vif", input_if);
51 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_agt.mon", "vif", output_if);
52 end
在加入了my_agent后,UVM的树形结构越来越清晰。首先,只有uvm_component才能作为树的结点,像my_transaction这种使用uvm_object_utils宏实现的类是不能作为UVM树的结点的。其次,在my_env的build_phase中,创建i_agt和o_agt的实例是在build_phase中;在agent中,创建driver和monitor的实例也是在build_phase中。按照前文所述的build_phase的从树根到树叶的执行顺序,可以建立一棵完整的UVM树。UVM要求UVM树最晚在build_phase时段完成,如果在build_phase后的某个phase实例化一个component:
代码清单 2-38
class my_env extends uvm_env;
…
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
endfunction
virtual task main_phase(uvm_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;
endtask
endclass
如上所示,将在my_env的build_phase中的实例化工作移动到main_phase中,UVM会给出如下错误提示:
UVM_FATAL @ 0: i_agt [ILLCRT] It is illegal to create a component ('i_agt' under 'uvm_test_top') after the build phase has ended.
那么是不是只能在build_phase中执行实例化的动作呢?答案是否定的。其实还可以在new函数中执行实例化的动作。如可以在my_agent的new函数中实例化driver和monitor:
代码清单 2-39
function new(string name, uvm_component parent);
super.new(name, parent);
if (is_active == UVM_ACTIVE) begin
drv = my_driver::type_id::create("drv", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunction
这样引起的一个问题是无法通过直接赋值的方式向uvm_agent传递is_active的值。在my_env的build_phase(或者new函数)中,向i_agt和o_agt的is_active赋值,根本不会产生效果。因此i_agt和o_agt都工作在active模式(is_active的默认值是UVM_ACTIVE),这与预想差距甚远。要解决这个问题,可以在my_agent实例化之前使用config_db语句传递is_active的值:
代码清单 2-40
class my_env extends uvm_env;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(uvm_active_passive_enum)::set(this, "i_agt", "is_active", UVM_ACTIVE);
uvm_config_db#(uvm_active_passive_enum)::set(this, "o_agt", "is_active", UVM_PASSIVE);
i_agt = my_agent::type_id::create("i_agt", this);
o_agt = my_agent::type_id::create("o_agt", this);
endfunction
endclass
class my_agent extends uvm_agent ;
function new(string name, uvm_component parent);
super.new(name, parent);
uvm_config_db#(uvm_active_passive_enum)::get(this, "", "is_active", is_active);
if (is_active == UVM_ACTIVE) begin
drv = my_driver::type_id::create("drv", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunction
endclass
只是UVM中约定俗成的还是在build_phase中完成实例化工作。因此,强烈建议仅在build_phase中完成实例化。
2.3.5 加入reference model
在2.1节中讲述验证平台的框图时曾经说过,reference model用于完成和DUT相同的功能。reference model的输出被scoreboard接收,用于和DUT的输出相比较。DUT如果很复杂,那么reference model也会相当复杂。本章的DUT很简单,所以reference model也相当简单:
代码清单 2-41
4 class my_model extends uvm_component;
5
6 uvm_blocking_get_port #(my_transaction) port;
7 uvm_analysis_port #(my_transaction) ap;
8
9 extern function new(string name, uvm_component parent);
10 extern function void build_phase(uvm_phase phase);
11 extern virtual task main_phase(uvm_phase phase);
12
13 `uvm_component_utils(my_model)
14 endclass
15
16 function my_model::new(string name, uvm_component parent);
17 super.new(name, parent);
18 endfunction
19
20 function void my_model::build_phase(uvm_phase phase);
21 super.build_phase(phase);
22 port = new("port", this);
23 ap = new("ap", this);
24 endfunction
25
26 task my_model::main_phase(uvm_phase phase);
27 my_transaction tr;
28 my_transaction new_tr;
29 super.main_phase(phase);
30 while(1) begin
31 port.get(tr);
32 new_tr = new("new_tr");
33 new_tr.my_copy(tr);
34 `uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
35 new_tr.my_print();
36 ap.write(new_tr);
37 end
38 endtask
在my_model的main_phase中,只是单纯地复制一份从i_agt得到的tr,并传递给后级的scoreboard中。my_copy是一个在my_transaction中定义的函数,其代码为:
代码清单 2-42
41 function void my_copy(my_transaction tr);
42 if(tr == null)
43 `uvm_fatal("my_transaction", "tr is null!!!!")
44 dmac = tr.dmac;
45 smac = tr.smac;
46 ether_type = tr.ether_type;
47 pload = new[tr.pload.size()];
48 for(int i = 0; i < pload.size(); i++) begin
49 pload[i] = tr.pload[i];
50 end
51 crc = tr.crc;
52 endfunction
这里实现了两个my_transaction的复制。
完成my_model的定义后,需要将其在my_env中实例化。其实例化方式与agent、driver相似,这里不具体列出代码。在加入my_model后,整棵UVM树变成了如图2-7所示的形式。
my_model并不复杂,这其中令人感兴趣的是my_transaction的传递方式。my_model是从i_agt中得到my_transaction,并把my_transaction传递给my_scoreboard。在UVM中,通常使用TLM(Transaction Level Modeling)实现component之间transaction级别的通信。
要实现通信,有两点是值得考虑的:第一,数据是如何发送的?第二,数据是如何接收的?在UVM的transaction级别的通信中,数据的发送有多种方式,其中一种是使用uvm_analysis_port。在my_monitor中定义如下变量:
代码清单 2-43
7 uvm_analysis_port #(my_transaction) ap;
uvm_analysis_port是一个参数化的类,其参数就是这个analysis_port需要传递的数据的类型,在本节中是my_transaction。
声明了ap后,需要在monitor的build_phase中将其实例化:
代码清单 2-44
14 virtual function void build_phase(uvm_phase phase);
…
18 ap = new("ap", this);
19 endfunction
在main_phase中,当收集完一个transaction后,需要将其写入ap中:
代码清单 2-45
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
write是uvm_analysis_port的一个内建函数。到此,在my_monitor中需要为transaction通信准备的工作已经全部完成。
UVM的transaction级别通信的数据接收方式也有多种,其中一种就是使用uvm_blocking_get_port。这也是一个参数化的类,其参数是要在其中传递的transaction的类型。在my_model的第6行中,定义了一个端口,并在build_phase中对其进行实例化。在main_phase中,通过port.get任务来得到从i_agt的monitor中发出的transaction。
在my_monitor和my_model中定义并实现了各自的端口之后,通信的功能并没有实现,还需要在my_env中使用fifo将两个端口联系在一起。在my_env中定义一个fifo,并在build_phase中将其实例化:
代码清单 2-46
10 uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
…
23 agt_mdl_fifo = new("agt_mdl_fifo", this);
fifo的类型是uvm_tlm_analysis_fifo,它本身也是一个参数化的类,其参数是存储在其中的transaction的类型,这里是my_transaction。
之后,在connect_phase中将fifo分别与my_monitor中的analysis_port和my_model中的blocking_get_port相连:
代码清单 2-47
31 function void my_env::connect_phase(uvm_phase phase);
32 super.connect_phase(phase);
33 i_agt.ap.connect(agt_mdl_fifo.analysis_export);
34 mdl.port.connect(agt_mdl_fifo.blocking_get_export);
35 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。
为什么这里需要一个fifo呢?不能直接把my_monitor中的analysis_port和my_model中的blocking_get_port相连吗?由于analysis_port是非阻塞性质的,ap.write函数调用完成后马上返回,不会等待数据被接收。假如当write函数调用时,blocking_get_port正在忙于其他事情,而没有准备好接收新的数据时,此时被write函数写入的my_transaction就需要一个暂存的位置,这就是fifo。
在如上的连接中,用到了i_agt的一个成员变量ap,它的定义与my_monitor中ap的定义完全一样:
代码清单 2-48
8 uvm_analysis_port #(my_transaction) ap;
与my_monitor中的ap不同的是,不需要对my_agent中的ap进行实例化,而只需要在my_agent的connect_phase中将monitor的值赋给它,换句话说,这相当于是一个指向my_monitor的ap的指针:
代码清单 2-49
29 function void my_agent::connect_phase(uvm_phase phase);
30 super.connect_phase(phase);
31 ap = mon.ap;
32 endfunction
根据前面介绍的connect_phase的执行顺序,my_agent的connect_phase的执行顺序早于my_env的connect_phase的执行顺序,从而可以保证执行到i_agt.ap.connect语句时,i_agt.ap不是一个空指针。
2.3.6 加入scoreboard
在验证平台中加入了reference model和monitor之后,最后一步是加入scoreboard。my_scoreboard的代码如下:
代码清单 2-50
3 class my_scoreboard extends uvm_scoreboard;
4 my_transaction expect_queue[$];
5 uvm_blocking_get_port #(my_transaction) exp_port;
6 uvm_blocking_get_port #(my_transaction) act_port;
7 `uvm_component_utils(my_scoreboard)
8
9 extern function new(string name, uvm_component parent = null);
10 extern virtual function void build_phase(uvm_phase phase);
11 extern virtual task main_phase(uvm_phase phase);
12 endclass
13
14 function my_scoreboard::new(string name, uvm_component parent = null);
15 super.new(name, parent);
16 endfunction
17
18 function void my_scoreboard::build_phase(uvm_phase phase);
19 super.build_phase(phase);
20 exp_port = new("exp_port", this);
21 act_port = new("act_port", this);
22 endfunction
23
24 task my_scoreboard::main_phase(uvm_phase phase);
25 my_transaction get_expect, get_actual, tmp_tran;
26 bit result;
27
28 super.main_phase(phase);
29 fork
30 while (1) begin
31 exp_port.get(get_expect);
32 expect_queue.push_back(get_expect);
33 end
34 while (1) begin
35 act_port.get(get_actual);
36 if(expect_queue.size() > 0) begin
37 tmp_tran = expect_queue.pop_front();
38 result = get_actual.my_compare(tmp_tran);
39 if(result) begin
40 `uvm_info("my_scoreboard", "Compare SUCCESSFULLY", UVM_LOW);
41 end
42 else begin
43 `uvm_error("my_scoreboard", "Compare FAILED");
44 $display("the expect pkt is");
45 tmp_tran.my_print();
46 $display("the actual pkt is");
47 get_actual.my_print();
48 end
49 end
50 else begin
51 `uvm_error("my_scoreboard", "Received from DUT, while Expect Que ue is empty");
52 $display("the unexpected pkt is");
53 get_actual.my_print();
54 end
55 end
56 join
57 endtask
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的数据之前到来。
act_port和o_agt的ap的连接方式及exp_port和reference model的ap的连接方式与2.3.5节讲述的i_agt的ap和reference model的端口的连接方式类似,这里不再赘述。
代码清单2-50中的第38行用到了my_compare函数,这是一个在my_transaction中定义的函数,其原型为:
代码清单 2-51
54 function bit my_compare(my_transaction tr);
55 bit result;
56
57 if(tr == null)
58 `uvm_fatal("my_transaction", "tr is null!!!!")
59 result = ((dmac == tr.dmac) &&
60 (smac == tr.smac) &&
61 (ether_type == tr.ether_type) &&
62 (crc == tr.crc));
63 if(pload.size() != tr.pload.size())
64 result = 0;
65 else
66 for(int i = 0; i < pload.size(); i++) begin
67 if(pload[i] != tr.pload[i])
68 result = 0;
69 end
70 return result;
71 endfunction
它逐字段比较两个my_transaction,并给出最终的比较结果。
完成my_scoreboard的定义后,也需要在my_env中将其实例化。此时,整棵UVM树变为如图2-8所示的形式。
2.3.7 加入field_automation机制
在2.3.3节中引入my_mointor时,在my_transaction中加入了my_print函数;在2.3.5节中引入reference model时,加入了my_copy函数;在2.3.6节引入scoreboard时,加入了my_compare函数。上述三个函数虽然各自不同,但是对于不同的transaction来说,都是类似的:它们都需要逐字段地对transaction进行某些操作。
那么有没有某种简单的方法,可以通过定义某些规则自动实现这三个函数呢?答案是肯定的。这就是UVM中的field_automation机制,使用uvm_field系列宏实现:
代码清单 2-52
4 class my_transaction extends uvm_sequence_item;
5
6 rand bit[47:0] dmac;
7 rand bit[47:0] smac;
8 rand bit[15:0] ether_type;
9 rand byte pload[];
10 rand bit[31:0] crc;
…
25 `uvm_object_utils_begin(my_transaction)
26 `uvm_field_int(dmac, UVM_ALL_ON)
27 `uvm_field_int(smac, UVM_ALL_ON)
28 `uvm_field_int(ether_type, UVM_ALL_ON)
29 `uvm_field_array_int(pload, UVM_ALL_ON)
30 `uvm_field_int(crc, UVM_ALL_ON)
31 `uvm_object_utils_end
…
37 endclass
这里使用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。3.3.1节列出了所有的uvm_field系列宏。
当使用上述宏注册之后,可以直接调用copy、compare、print等函数,而无需自己定义。这极大地简化了验证平台的搭建,提高了效率:
代码清单 2-53
26 task my_model::main_phase(uvm_phase phase);
27 my_transaction tr;
28 my_transaction new_tr;
29 super.main_phase(phase);
30 while(1) begin
31 port.get(tr);
32 new_tr = new("new_tr");
33 new_tr.copy(tr);
34 `uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
35 new_tr.print();
36 ap.write(new_tr);
37 end
38 endtask
代码清单 2-54
文件:src/ch2/section2.3/2.3.7/my_scoreboard.sv
…
34 while (1) begin
35 act_port.get(get_actual);
36 if(expect_queue.size() > 0) begin
37 tmp_tran = expect_queue.pop_front();
38 result = get_actual.compare(tmp_tran);
39 if(result) begin
40 `uvm_info("my_scoreboard", "Compare SUCCESSFULLY", UVM_LOW);
41 end
引入field_automation机制的另外一大好处是简化了driver和monitor。在2.3.1节及2.3.3节中,my_driver的drv_one_pkt任务和my_monitor的collect_one_pkt任务代码很长,但是几乎都是一些重复性的代码。使用field_automation机制后,drv_one_pkt任务可以简化为:
代码清单 2-55
38 task my_driver::drive_one_pkt(my_transaction tr);
39 byte unsigned data_q[];
40 int data_size;
41
42 data_size = tr.pack_bytes(data_q) / 8;
43 `uvm_info("my_driver", "begin to drive one pkt", UVM_LOW);
44 repeat(3) @(posedge vif.clk);
45 for ( int i = 0; i < data_size; i++ ) begin
46 @(posedge vif.clk);
47 vif.valid <= 1'b1;
48 vif.data <= data_q[i];
49 end
50
51 @(posedge vif.clk);
52 vif.valid <= 1'b0;
53 `uvm_info("my_driver", "end drive one pkt", UVM_LOW);
54 endtask
第42行调用pack_bytes将tr中所有的字段变成byte流放入data_q中,在2.3.1节中是手工地将所有字段放入data_q中的。pack_bytes极大地减少了代码量。在把所有的字段变成byte流放入data_q中时,字段按照uvm_field系列宏书写的顺序排列。在上述代码中是先放入dmac,再依次放入smac、ether_type、pload、crc。假如my_transaction定义时各个字段的顺序如下:
代码清单 2-56
`uvm_object_utils_begin(my_transaction)
`uvm_field_int(smac, UVM_ALL_ON)
`uvm_field_int(dmac, 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
那么将会先放入smac,再依次放入dmac、ether_type、pload、crc。
my_monitor的collect_one_pkt可以简化成:
代码清单 2-57
文件:src/ch2/section2.3/2.3.7/my_monitor.sv
34 task my_monitor::collect_one_pkt(my_transaction tr);
35 byte unsigned data_q[$];
36 byte unsigned data_array[];
37 logic [7:0] data;
38 logic valid = 0;
39 int data_size;
…
46 `uvm_info("my_monitor", "begin to collect one pkt", UVM_LOW);
47 while(vif.valid) begin
48 data_q.push_back(vif.data);
49 @(posedge vif.clk);
50 end
51 data_size = data_q.size();
52 data_array = new[data_size];
53 for ( int i = 0; i < data_size; i++ ) begin
54 data_array[i] = data_q[i];
55 end
56 tr.pload = new[data_size - 18]; //da sa, e_type, crc
57 data_size = tr.unpack_bytes(data_array) / 8;
58 `uvm_info("my_monitor", "end collect one pkt", UVM_LOW);
59 endtask
这里使用unpack_bytes函数将data_q中的byte流转换成tr中的各个字段。unpack_bytes函数的输入参数必须是一个动态数组,所以需要先把收集到的、放在data_q中的数据复制到一个动态数组中。由于tr中的pload是一个动态数组,所以需要在调用unpack_bytes之前指定其大小,这样unpack_bytes函数才能正常工作。