UVM实战
上QQ阅读APP看书,第一时间看更新

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 modelscoreboard等。在调用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行为的组件是monitordriver负责把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根据这种协议收集数据,如果drivermonitor由不同人员实现,那么可以大大减少其中任何一方对协议理解的错误;第二,在后文将会看到,在实现代码重用时,使用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时,读者看到了drivermonitor之间的联系:两者之间的代码高度相似。其本质是因为二者处理的是同一种协议,在同样一套既定的规则下做着不同的事情。由于二者的这种相似性,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

在把drivermonitor封装成agent后,在env中需要实例化agent,而不需要直接实例化drivermonitor了:

代码清单 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的加入,drivermonitor的层次结构改变了,在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中,创建drivermonitor的实例也是在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函数中实例化drivermonitor

代码清单 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不同的是,它的执行顺序并不是从树根到树叶,而是从树叶到树根——先执行drivermonitor的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 modelmonitor之后,最后一步是加入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机制的另外一大好处是简化了drivermonitor。在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函数才能正常工作。