上篇文章讲了如何添加interface,用于充当DUT 和验证平台之间的数据接口,可以避免使用绝对路径来对数据进行驱动。这篇文章尝试搭建一个较为完整的UVM平台,实现数据的驱动和采集,添加transaction,monitor,agent,env等组件。

https://github.com/shirainbown/UVM-Step-by-Step/tree/master

一、添加 transaction

transaction可以看成数据传输的基本单元,在物理协议中,数据的传输单元一般为帧或者包,那么一个transaction中就包含了一帧或者一包的数据;如果DUT是一个RAM,那么传输的基本单元就是读写控制信号+读写数据,transaction中就至少包含这些数据。一个transaction的代码示例如下:

`ifndef MY_TRANSACTION__SV
`define MY_TRANSACTION__SV

class my_transaction extends uvm_sequence_item;  // 继承uvm_sequence_item类,属于uvm_object

   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)  // uvm_object 类的注册

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

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

这里有几个点需要注意:

  • 与driver不同,transaction 具有自己的生命周期,一个transaction发送完了就是下一个transaction了,属于uvm_object,需要用`uvm_object_utils注册,这个宏预定义了很多函数和机制,后续可以看到其特性。

  • void 函数没有返回值,这个是用于外部调用时之间给transaction内部变量赋值,例如这里的crc,是transaction内部的变量。其他带有return的函数是用于直接在外部调用然后赋值给外部变量使用。

  • constraint代码块是作为约束使用,因为dmac等变量声明为了rand类型,说明后续会在生成transaction的时候对其随机初始化,那么这里的constraint就可以对随机化的范围进行约束。

那么此时,dirver就不用自己一边随机生成一个一个数据,然后再发送到interface接口上,而只需要随机化一个transaction,然后把transaction中的数据发送到接口上即可。

`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   // 循环两次,发送两个transaction
      tr = new("tr");                  // 随机初始化一个transactino
      assert(tr.randomize() with {pload.size == 200;});  // 检查是否成功生成受约束的transaction
      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   // 一个transaction的所有数据打包完后,开始从队列里面取数据发送到接口上
      @(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

二、添加env

一个完整的UVM平台拥有很多组件,一般包括transaction,sequencer,sequence,driver,monitor,agent,scoreboard,reference model,为了让组件之间的层次结构清晰,还需要选择在合适的地方对组件进行例化。大多数组件都会选择在env中例化,此时env就作为了这些组件的根或者说是parent。在UVM中,env继承于uvm_env类。

`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);   //这里等价于 drv= new("drv",this)
// 这种type_name::type_id::create 的实例化方式就是使用了uvm_xx_utils注册带来的,会自动调用new函数。
// new函数是systemverilog语言本身的特性,而create是uvm平台封装后的特性
   endfunction

   `uvm_component_utils(my_env)  // env仿真期间都存在,因此用uvm_component注册
endclass
`endif

三、添加monitor

我们对DUT 进行验证的时候,需要知道DUT接收到的数据,也要知道DUT输出的数据,对DUT端口进行监控的组件就叫做monitor。入口处的monitor把DUT实际收到的数据发送给reference model,由RF计算出正确的输出结果,然后发送给scoreboard,出口处的monitor把DUT的输出数据发送给scoreboard,然后在scoreboard中对数据进行对比。

`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的代码,可以发现,它和driver的代码相似程度非常高,将driver发送的数据重新打包回了一个完整的transaction。

三、添加agent

通常在uvm中会将driver和monitor打包成一个agent,但是存在一个问题,即DUT的输出端口只需要monitor而不需要driver,因此在agent中例化driver时需要进行判断。一个agent的代码如下:

`ifndef MY_AGENT__SV
`define MY_AGENT__SV

class my_agent extends uvm_agent ;  // 继承自 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) // 用component注册
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

上述代码有几个需要注意的地方:

  • is_active是uvm_agent中的一个成员变量,因此在my_agent中没有定义就可以直接使用,该变量的类型是uvm_active_passive_enum,可选取值为UVM_ACTIVE和UVM_PASSIVE,默认值为UVM_ACTIVE。当取值为UVM_ACTIVE时选择在agent中例化driver。

四、将组件连接起来

我们此时面临几个问题:

  • 如何将这几个组件在env中例化?

  • 如何给is_active初始化值?即在env中例化agent时候怎么传入参数。

  • 之前使用的interface如何连接?即使用config_db时的几个参数怎么修改。

// env.sv
`ifndef MY_ENV__SV
`define 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);  // 在build函数中例化
      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;  // env 的build 函数执行完后执行 agent的 build函数,因此可以在此处赋值
//         i_agt = new("i_agt",this,UVM_ACTIVE);
//         o_agt = new("o_agt",this,UVM_PASSIVE);
   endfunction

   `uvm_component_utils(my_env)
endclass
`endif
// 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);
      if(is_active==UVM_ACTIVE)begin
         drv = my_driver::type_id::create("drv", this);
         `uvm_info("my_agent", "this my_agent has a driver", UVM_LOW);
      end
      mon = my_monitor::type_id::create("mon", this);
         `uvm_info("my_agent", "this my_agent has a my_monitor", UVM_LOW);
   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
`timescale 1ns/1ps
`include "uvm_macros.svh"

import uvm_pkg::*;
`include "my_if.sv"
`include "my_transaction.sv"
`include "my_driver.sv"
`include "my_monitor.sv"
`include "my_agent.sv"
`include "my_env.sv"

module top_tb;

reg clk;
reg rst_n;
reg[7:0] rxd;
reg rx_dv;
wire[7:0] txd;
wire tx_en;

my_if input_if(clk, rst_n);
my_if output_if(clk, rst_n);

dut my_dut(.clk(clk),
           .rst_n(rst_n),
           .rxd(input_if.data),
           .rx_dv(input_if.valid),
           .txd(output_if.data),
           .tx_en(output_if.valid));

initial begin
   clk = 0;
   forever begin
      #100 clk = ~clk;
   end
end

initial begin
   rst_n = 1'b0;
   #1000;
   rst_n = 1'b1;
end

initial begin
   run_test("my_env");// uvm_test_top 从my_drv 变成了 my_env
end

// 这里需要注意的是,config_db的参数变了,路径不再是“”,而是“uvm_test_top.i_agt.drv”等
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

endmodule

其他的文件没有变化,整个文件树为:

├── 2.3.4
│   ├── filelist.f
│   ├── Makefile
│   ├── my_agent.sv
│   ├── my_driver.sv
│   ├── my_env.sv
│   ├── my_if.sv
│   ├── my_monitor.sv
│   ├── my_transaction.sv
│   └── top_tb.sv
└── dut
    └── dut.sv

运行Makefile得到:

书中有提到,直接修改new函数无法通过直接赋值的方式向uvm_agent传递is_active的值,需要用config_db来传递,其实这个说法不正确,我们可以之间重载new函数,新增一个输入参数即可:

`ifndef MY_AGENT__SV
`define MY_AGENT__SV

class my_agent extends uvm_agent ;
   my_driver     drv;
   my_monitor    mon;
   // 给new函数新增一个输入变量
   function new(string name, uvm_component parent,uvm_active_passive_enum active=UVM_ACTIVE);
      super.new(name, parent);
      is_active = active;
      if(is_active==UVM_ACTIVE)begin
         drv = my_driver::type_id::create("drv", this);
         `uvm_info("my_agent", "this my_agent has a driver", UVM_LOW);
      end
      mon = my_monitor::type_id::create("mon", this);
         `uvm_info("my_agent", "this my_agent has a my_monitor", UVM_LOW);
   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

对应的env.sv:

`ifndef MY_ENV__SV
`define 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,UVM_ACTIVE);
      o_agt = new("o_agt",this,UVM_PASSIVE);   // 这两种方式等价
   endfunction

   `uvm_component_utils(my_env)
endclass
`endif