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
文件:src/ch2/section2.3/2.3.1/my_transaction.sv 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
文件:src/ch2/section2.3/2.3.1/my_driver.sv 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
文件:src/ch2/section2.3/2.3.2/my_env.sv 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所示。
图2-3 UVM树的生长:加入env
当加入了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
文件:src/ch2/section2.3/2.3.2/top_tb.sv 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
文件:src/ch2/section2.3/2.3.3/my_monitor.sv 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
文件:src/ch2/section2.3/2.3.3/my_transaction.sv 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所示。
图2-4 UVM树的生长:加入monitor
在env中实例化monitor后,要在top_tb中使用config_db将input_if和output_if传递给两个monitor:
代码清单 2-32
文件:src/ch2/section2.3/2.3.3/top_tb.sv 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
文件:src/ch2/section2.3/2.3.4/my_agent.sv 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可以不用实例化。
图2-5 DUT输入和输出端的agent
在把driver和monitor封装成agent后,在env中需要实例化agent,而不需要直接实例化driver和monitor了:
代码清单 2-36
文件:src/ch2/section2.3/2.3.4/my_env.sv 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所示形式。
图2-6 UVM树的生长:加入agent
由于agent的加入,driver和monitor的层次结构改变了,在top_tb中使用config_db设置virtual my_if时要注意改变路径:
代码清单 2-37
文件:src/ch2/section2.3/2.3.4/top_tb.sv 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
文件:src/ch2/section2.3/2.3.5/my_model.sv 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
文件:src/ch2/section2.3/2.3.5/my_transaction.sv 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所示的形式。
图2-7 UVM树的生长:加入reference model
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
文件:src/ch2/section2.3/2.3.5/my_monitor.sv 7 uvm_analysis_port #(my_transaction) ap;
uvm_analysis_port是一个参数化的类,其参数就是这个analysis_port需要传递的数据的类型,在本节中是my_transaction。
声明了ap后,需要在monitor的build_phase中将其实例化:
代码清单 2-44
文件:src/ch2/section2.3/2.3.5/my_monitor.sv 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
文件:src/ch2/section2.3/2.3.5/my_env.sv 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
文件:src/ch2/section2.3/2.3.5/my_env.sv 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
文件:src/ch2/section2.3/2.3.5/my_agent.sv 8 uvm_analysis_port #(my_transaction) ap;
与my_monitor中的ap不同的是,不需要对my_agent中的ap进行实例化,而只需要在my_agent的connect_phase中将monitor的值赋给它,换句话说,这相当于是一个指向my_monitor的ap的指针:
代码清单 2-49
文件:src/ch2/section2.3/2.3.5/my_agent.sv 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
文件:src/ch2/section2.3/2.3.6/my_scoreboard.sv 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
文件:src/ch2/section2.3/2.3.6/my_scoreboard.sv 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-8 UVM树的生长:加入scoreboard
*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
文件:src/ch2/section2.3/2.3.7/my_transaction.sv 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
文件:src/ch2/section2.3/2.3.7/my_model.sv 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
文件:src/ch2/section2.3/2.3.7/my_driver.sv 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函数才能正常工作。