【UVM实战】第二章:一个简单的UVM验证平台(3)为验证平台加入各个组件【时序解读;使用流操作符解包driver数据的代码;修正原书monitor数据的拼包错误】

15 篇文章 ¥19.90 ¥99.00

DUT的基本功能:采集输入端口的RX和RX_EN,打一拍后输出到TX和TX_EN。

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
`ifndef MY_TRANSACTION__SV
`define MY_TRANSACTION__SV

class my_transaction extends uvm_sequence_item;

   rand bit[47:0] dmac;
   rand bit[47:0] smac;
   rand bit[15:0] ether_type;
   rand byte      pload[];
   rand bit[31:0] crc;

   constraint pload_cons{
      pload.size >= 46;
      pload.size <= 1500;
   }

   function bit[31:0] calc_crc();
      return 32'h0;
   endfunction

   function void post_randomize();
      crc = calc_crc;
   endfunction

   `uvm_object_utils(my_transaction)

   function new(string name = "my_transaction");
      super.new();
   endfunction
endclass
`endif

其中dmac是48bit的以太网目的地址, smac是48bit的以太网源地址, ether_type是以太网类型, pload是其携带数据的大小, 通过pload_cons约束可以看到, 其大小被限制在46~1500byte, CRC是前面所有数据的校验值。 由于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
`ifndef MY_DRIVER__SV
`define MY_DRIVER__SV
class my_driver extends uvm_driver;

   virtual my_if vif;

   `uvm_component_utils(my_driver)
   function new(string name = "my_driver", uvm_component parent = null);
      super.new(name, parent);
   endfunction

   virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
         `uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
   endfunction

   extern task main_phase(uvm_phase phase);
   extern task drive_one_pkt(my_transaction tr);
endclass

task my_driver::main_phase(uvm_phase phase);
   my_transaction tr;
   phase.raise_objection(this);
   vif.data <= 8'b0;
   vif.valid <= 1'b0;
   while(!vif.rst_n)
      @(posedge vif.clk);
   for(int i = 0; i < 2; i++) begin 
      tr = new("tr");
      assert(tr.randomize() with {pload.size == 200;});
      drive_one_pkt(tr);
   end
   repeat(5) @(posedge vif.clk);
   phase.drop_objection(this);
endtask

task my_driver::drive_one_pkt(my_transaction tr);
   bit [47:0] tmp_data;
   bit [7:0] data_q[$]; 
  
   //push dmac to data_q
   tmp_data = tr.dmac;
   for(int i = 0; i < 6; i++) begin
      data_q.push_back(tmp_data[7:0]);
      tmp_data = (tmp_data >> 8);
   end
   //push smac to data_q
   tmp_data = tr.smac;
   for(int i = 0; i < 6; i++) begin
      data_q.push_back(tmp_data[7:0]);
      tmp_data = (tmp_data >> 8);
   end
   //push ether_type to data_q
   tmp_data = tr.ether_type;
   for(int i = 0; i < 2; i++) begin
      data_q.push_back(tmp_data[7:0]);
      tmp_data = (tmp_data >> 8);
   end
   //push payload to data_q
   for(int i = 0; i < tr.pload.size; i++) begin
      data_q.push_back(tr.pload[i]);
   end
   //push crc to data_q
   tmp_data = tr.crc;
   for(int i = 0; i < 4; i++) begin
      data_q.push_back(tmp_data[7:0]);
      tmp_data = (tmp_data >> 8);
   end

   `uvm_info("my_driver", "begin to drive one pkt", UVM_LOW);
   repeat(3) @(posedge vif.clk);

   while(data_q.size() > 0) begin
      @(posedge vif.clk);
      vif.valid <= 1'b1;
      vif.data <= data_q.pop_front(); 
   end

   @(posedge vif.clk);
   vif.valid <= 1'b0;
   `uvm_info("my_driver", "end drive one pkt", UVM_LOW);
endtask


`endif

在main_phase中, 先使用randomize将tr随机化, 之后通过drive_one_pkt任务将tr的内容驱动到DUT的端口上。 在drive_one_pkt中, 先将tr中所有的数据压入队列data_q中, 之后再将data_q中所有的数据弹出并驱动。 将tr中的数据压入队列data_q中的过程相当于打包成一个byte流的过程。 这个过程还可以使用SystemVerlog提供的流操作符实现。 具体请参照SystemVerilog语言标准IEEE Std1800 TM—2012( IEEE Standard for SystemVerilog—Unified Hardware Design, Specification, and Verification Language) 的11.4.14节。


流操作符的实现数据解包方法

将代码清单2-24的43-70行,替换为如下几行代码:

   bit [7:0] data_q1[$]; 
   bit [7:0] data_q2[$];
   bit [7:0] data_q3[$];
   bit [7:0] data_q4[$];
   bit [7:0] data_q5[$];

   //流操作符
   data_q1 = {<<8{tr.dmac}};
   data_q2 = {<<8{tr.smac}};
   data_q3 = {<<8{tr.ether_type}};
   data_q4 = {<<8{tr.pload}};
   data_q5 = {<<8{tr.crc}};

   data_q = {data_q1, data_q2, data_q3, data_q4, data_q5};

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
`ifndef MY_ENV__SV
`define MY_ENV__SV

class my_env extends uvm_env;

   my_driver drv;

   function new(string name = "my_env", uvm_component parent);
      super.new(name, parent);
   endfunction

   virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      drv = my_driver::type_id::create("drv", this); 
   endfunction

   `uvm_component_utils(my_env)
endclass
`endif

所有的env应该派生自uvm_env, 且与my_driver一样, 容器类在仿真中也是一直存在的, 使用uvm_component_utils宏来实现factory的注册。

在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
图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
...
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时传递过去的名字。 如果在实例化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
`ifndef MY_MONITOR__SV
`define MY_MONITOR__SV
class my_monitor extends uvm_monitor;

   virtual my_if vif;

   `uvm_component_utils(my_monitor)
   function new(string name = "my_monitor", uvm_component parent = null);
      super.new(name, parent);
   endfunction

   virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
         `uvm_fatal("my_monitor", "virtual interface must be set for vif!!!")
   endfunction

   extern task main_phase(uvm_phase phase);
   extern task collect_one_pkt(my_transaction tr);
endclass

task my_monitor::main_phase(uvm_phase phase);
   my_transaction tr;
   while(1) begin
      tr = new("tr");
      collect_one_pkt(tr);
   end
endtask

task my_monitor::collect_one_pkt(my_transaction tr);
   bit[7:0] data_q[$]; 
   int psize;
   while(1) begin
      @(posedge vif.clk);
      if(vif.valid) break;
   end

   `uvm_info("my_monitor", "begin to collect one pkt", UVM_LOW);
   while(vif.valid) begin
      data_q.push_back(vif.data);
      @(posedge vif.clk);
   end
   //pop dmac
   for(int i = 0; i < 6; i++) begin
      tr.dmac = {tr.dmac[39:0], data_q.pop_front()};
   end
   //pop smac
   for(int i = 0; i < 6; i++) begin
      tr.smac = {tr.smac[39:0], data_q.pop_front()};
   end
   //pop ether_type
   for(int i = 0; i < 2; i++) begin
      tr.ether_type = {tr.ether_type[7:0], data_q.pop_front()};
   end

   psize = data_q.size() - 4;
   tr.pload = new[psize];
   //pop payload
   for(int i = 0; i < psize; i++) begin
      tr.pload[i] = data_q.pop_front();
   end
   //pop crc
   for(int i = 0; i < 4; i++) begin
      tr.crc = {tr.crc[23:0], data_q.pop_front()};
   end
   `uvm_info("my_monitor", "end collect one pkt, print it:", UVM_LOW);
    tr.my_print();
endtask


`endif

有几点需要注意的是:

  • 第一, 所有的monitor类应该派生自uvm_monitor
  • 第二, 与driver类似, 在my_monitor中也需要有一个virtual my_if
  • 第三, uvm_monitor在整个仿真中是一直存在的, 所以它是一个component, 要使用uvm_component_utils宏注册;
  • 第四, 由于monitor需要时刻收集数据, 永不停歇, 所以在main_phase中使用while(1)循环来实现这一目的

在查阅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
   function void my_print();
      $display("dmac = %0h", dmac);
      $display("smac = %0h", smac);
      $display("ether_type = %0h", ether_type);
      for(int i = 0; i < pload.size; i++) begin
         $display("pload[%0d] = %0h", i, pload[i]);
      end
      $display("crc = %0h", crc);
   endfunction

当完成monitor的定义后, 可以在env中对其进行实例化:

代码清单 2-31

文件: src/ch2/section2.3/2.3.3/my_env.sv
class my_env extends uvm_env;

   my_driver drv;
   my_monitor i_mon;
   
   my_monitor o_mon;

   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); 
      i_mon = my_monitor::type_id::create("i_mon", this);
      o_mon = my_monitor::type_id::create("o_mon", this);
   endfunction

   `uvm_component_utils(my_env)
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
图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
initial begin
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", input_if);
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_mon", "vif", input_if);
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_mon", "vif", output_if);
end

修正原书monitor数据的拼包错误

原书中的monitor数据拼包主要错在了移位上。

搞清楚这个问题,先来回顾下数据解包方式:transaction分为5个数据,依次为48bit的dmac、48bit的smac、16bit的ether_type、动态数组pload、32bit的crc。在driver发包时,每次只能发送8bit。driver选择是按照顺序依次发送这5个数据:dmac -> smac -> ehter_type -> pload -> crc,对于每个数据是从低位开始,8bit为一个整体发送的。

但数据在monitor拼包时,原书程序是先将实时采集到的8bit数据,先push_back到了一个队列中,然后通过pop_front弹出数据,然后向左移位的方式放到各个数据中(dmac/smac/ehter_type/pload/crc)。但是注意,dmac的低八位最先通过push_back压入队列,所以pop_front最先弹出的是dmac的低八位。这个低八位也应该放到monitor拼包后的低八位。但原书中的tr.dmac = {tr.dmac[39:0], data_q.pop_front()};写法是将它放到了dmac的高八位处,显然是不对的,我们也可以从打印结果看到这一点,如下图:

在这里插入图片描述

正确的学法是: tr.dmac = {data_q.pop_front(), tr.dmac[47:8]};,打印结果如下图:

在这里插入图片描述

UVM_INFO my_driver.sv(33) @ 1100000: uvm_test_top.drv [my_driver_data] tr.dmac = d19a334456b0, tr.smac = fa4a5778d7f9, tr.ether_type = 7063, tr.crc = 0
UVM_INFO my_driver.sv(33) @ 45300000: uvm_test_top.drv [my_driver_data] tr.dmac = d39113885dfc, tr.smac = eb94d9eb3e9b, tr.ether_type = 6cb6, tr.crc = 0
UVM_INFO my_monitor.sv(85) @ 45500000: uvm_test_top.i_mon [my_monitor_data] tr.dmac = d19a334456b0, tr.smac = fa4a5778d7f9, tr.ether_type = 7063, tr.crc = 0
UVM_INFO my_monitor.sv(85) @ 45700000: uvm_test_top.o_mon [my_monitor_data] tr.dmac = d19a334456b0, tr.smac = fa4a5778d7f9, tr.ether_type = 7063, tr.crc = 0
UVM_INFO my_monitor.sv(85) @ 89700000: uvm_test_top.i_mon [my_monitor_data] tr.dmac = d39113885dfc, tr.smac = eb94d9eb3e9b, tr.ether_type = 6cb6, tr.crc = 0
UVM_INFO my_monitor.sv(85) @ 89900000: uvm_test_top.o_mon [my_monitor_data] tr.dmac = d39113885dfc, tr.smac = eb94d9eb3e9b, tr.ether_type = 6cb6, tr.crc = 0

更改完毕后的核心程序如下:

   for(int i = 0; i < 6; i++) begin
      tr.dmac = {data_q.pop_front(), tr.dmac[47:8]}; //这种写法相当于把低八位放到了低八位(右移),顺序是对的
   end
   //pop smac
   for(int i = 0; i < 6; i++) begin
      tr.smac = {data_q.pop_front(), tr.smac[47:8]};
   end
   //pop ether_type
   for(int i = 0; i < 2; i++) begin
      tr.ether_type = {data_q.pop_front(), tr.ether_type[15:8]};
   end

   psize = data_q.size() - 4;
   tr.pload = new[psize];
   //pop payload
   for(int i = 0; i < psize; i++) begin
      tr.pload[i] = data_q.pop_front();
   end
   //pop crc
   for(int i = 0; i < 4; i++) begin
      tr.crc = {data_q.pop_front(), tr.crc[31:8]};
   end

分析完这个问题,我们稍稍在修正后的打印信息上再做一点探究,为什么会有两个monitor拼包后的数据?以及为什么两者相差一拍?

数据全览如下图,可以看到发送了两帧数据:

在这里插入图片描述

在第一帧和第二帧中间位置处放大,可以看到在UVM_INFO my_monitor.sv(85) @ 45500000: uvm_test_top.i_mon [my_monitor_data] tr.dmac = d19a334456b0, tr.smac = fa4a5778d7f9, tr.ether_type = 7063, tr.crc = 0的打印时间处,是rx_en刚刚拉低了一个时钟周期后的上升沿处,此处是i_monitor采集到的输入接口数据。

在这里插入图片描述

而在UVM_INFO my_monitor.sv(85) @ 45700000: uvm_test_top.o_mon [my_monitor_data] tr.dmac = d19a334456b0, tr.smac = fa4a5778d7f9, tr.ether_type = 7063, tr.crc = 0打印时间处,是dx_en刚刚拉低一个时钟周期后的上升沿处,o_monitor采集到的输出接口数据。

这两个数据自然都是第一帧driver发送的数据,只是时序上差了一拍!

温馨提示:调试建议使用uvm_info,如:

  • uvm_info("my_monitor_data", $sformatf("tr.dmac = %0h, tr.smac = %0h, tr.ether_type = %0h, tr.crc = %0h", tr.dmac, tr.smac, tr.ether_type, tr.crc), UVM_NONE);(别忘了uvm_info加前面的点)
  • uvm_info("my_driver_data", $sformatf("tr.dmac = %0h, tr.smac = %0h, tr.ether_type = %0h, tr.crc = %0h", tr.dmac, tr.smac, tr.ether_type, tr.crc), UVM_NONE);(别忘了uvm_info加前面的点)

2.3.4、封装成agent

上一节在验证平台中加入monitor时, 读者看到了driver和monitor之间的联系: 两者之间的代码高度相似。 其本质是因为二者处理的是同一种协议, 在同样一套既定的规则下做着不同的事情。 由于二者的这种相似性, UVM中通常将二者封装在一起, 成为一个agent。 因此, 不同的agent就代表了不同的协议。

代码清单 2-33

文件: src/ch2/section2.3/2.3.4/my_agent.sv
`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

所有的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_PASSIVEUVM_ACTIVE。 在uvm_agent中, is_active的值默认为UVM_ACTIVE, 在这种模式下, 是需要实例化driver的。 那么什么是UVM_PASSIVE模式呢? 以本章的DUT为例, 如图2-5所示, 在输出端口上不需要驱动任何信号, 只需要监测信号。 在这种情况下, 端口上是只需要monitor的, 所以driver可以不用实例化。

图2-5 DUT输入和输出端的agent
图2-5 DUT输入和输出端的agent

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

代码清单 2-36

文件: src/ch2/section2.3/2.3.4/my_env.sv
...
class my_env extends uvm_env;

   my_agent  i_agt;
   my_agent  o_agt;
   
   function new(string name = "my_env", uvm_component parent);
      super.new(name, parent);
   endfunction

   virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      i_agt = my_agent::type_id::create("i_agt", this);
      o_agt = my_agent::type_id::create("o_agt", this);
      i_agt.is_active = UVM_ACTIVE;
      o_agt.is_active = UVM_PASSIVE;
   endfunction

   `uvm_component_utils(my_env)
endclass
...

完成i_agt和o_agt的声明后, 在my_env的build_phase中对它们进行实例化后, 需要指定各自的工作模式是active模式还是passive模式。 现在, 整棵UVM树变为了如图2-6所示形式。

图2-6 UVM树的生长: 加入agent
图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
...
initial begin
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.drv", "vif", input_if);
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.mon", "vif", input_if);
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_agt.mon", "vif", output_if);
end
...

在加入了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
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

在my_model的main_phase中, 只是单纯地复制一份从i_agt得到的tr, 并传递给后级的scoreboard中。 my_copy是一个在my_transaction中定义的函数, 其代码为:

代码清单 2-42

文件: src/ch2/section2.3/2.3.5/my_transaction.sv
   function void my_copy(my_transaction tr);
      if(tr == null)
         `uvm_fatal("my_transaction", "tr is null!!!!")
      dmac = tr.dmac;
      smac = tr.smac;
      ether_type = tr.ether_type;
      pload = new[tr.pload.size()];
      for(int i = 0; i < pload.size(); i++) begin
         pload[i] = tr.pload[i];
      end
      crc = tr.crc;
   endfunction

这里实现了两个my_transaction的复制。

完成my_model的定义后, 需要将其在my_env中实例化。 其实例化方式与agent、 driver相似, 这里不具体列出代码。 在加入my_model后, 整棵UVM树变成了如图2-7所示的形式。

图2-7 UVM树的生长: 加入reference model
图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
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
   virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);
	  ...
      ap = new("ap", this);
   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的uvm_analysis_port端口中发送transaction,在my_model的uvm_blocking_get_port端口中接收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
class my_env extends uvm_env;

   my_agent  i_agt;
   my_agent  o_agt;
   my_model  mdl;
   
   uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
   
   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);
	  ...
      agt_mdl_fifo = new("agt_mdl_fifo", this);
   endfunction

   extern virtual function void connect_phase(uvm_phase phase);
   
   `uvm_component_utils(my_env)
endclass

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

这里引入了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
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
function void my_agent::connect_phase(uvm_phase phase);
   super.connect_phase(phase);
   ap = mon.ap;
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
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 //进程1
         exp_port.get(get_expect);
         expect_queue.push_back(get_expect);
      end
      while (1) begin //进程2
         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

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

逐字段比较两个my_transaction, 并给出最终的比较结果。

完成my_scoreboard的定义后, 也需要在my_env中将其实例化。 此时, 整棵UVM树变为如图2-8所示的形式。

图2-8 UVM树的生长: 加入scoreboard
图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
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;
   
   ...
   `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
   ...

endclass

这里使用uvm_object_utils_beginuvm_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
task my_model::main_phase(uvm_phase phase);
   super.main_phase(phase);
   while(1) begin
	  ...
      new_tr.copy(tr);// 自带copy函数
	  ...

代码清单 2-54

文件: src/ch2/section2.3/2.3.7/my_scoreboard.sv
task my_scoreboard::main_phase(uvm_phase phase);
   ...
   super.main_phase(phase);
   fork 
	  ...
      while (1) begin
         act_port.get(get_actual);
         if(expect_queue.size() > 0) begin
            tmp_tran = expect_queue.pop_front(); 
            result = get_actual.compare(tmp_tran); // 自带compare函数
			...

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

第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
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 = 6 + 6 + 2 + 4 = 18
   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函数才能正常工作。

driver时序解读

driver发送数据,建议是将打拍放在发送数据后,如下图79行打拍放在77和78行后。如果放在了发送数据之前,需要对最后一拍数据再进行打拍处理,如下图需要加上83行。【这两种方式,先发数据再打拍,整体时序会比先打拍再发数据早一拍。】

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MoorePlus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值