上篇文章讲了如何添加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