验证平台大致包括如下几个部分:
driver:实现激励。
scoreboard:用于将DUT与参考模型进行比较。
monitor:用于收集DUT的输出并传递给scoreboard
加入transaction
以上所有的操作都是基于信号级的,接下来是基于transaction的信息的传递。
// my_transaction.sv
// 定义transaction类
class my_transaction extends uvm_sequence_item; // my_transaction的基类是uvm_sequence_item
rand bit[47:0] dmac; // 48bit的以太网目的地址
rand bit[47:0] smac; // 48bit的以太网源地址
rand bit[15:0] ether_type; // ether_type是以太网类型
rand byte pload[]; // 动态数组pload
rand bit[31:0] crc; // 前面所有数据的校验值
// 在constraint中对随机的数据增加限制条件,动态数组pload大小被限制在46-1500bit
constraint pload_cons{
pload.size >= 46;
pload.size <= 1500;
}
function bit[31:0] calc_crc(); // 该函数返回32位bit
return 32'h0;
endfunction
function void post_randomize(); // post_randomize函数无返回
crc = calc_crc;
endfunction
`uvm_object_utils(my_transaction) // 这里没有使用uvm_component_utils宏来实现factory机制,而是使用了uvm_object_utils
function new(string name = "my_transaction");
super.new(name);
endfunction
// 当收集完一个transaction后,通过my_print函数将其打印出来。
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("pload[%0d] = %0h", i, pload[i]);
endfunction
endclass
在my_driver中实现基于transaction的驱动
// my_driver.sv
// 在main_phase中,先使用randomize将tr随机化
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
for(int i = 0; i < 2; i++) begin
tr = new("tr");
assert(tr.randomize() with {pload.size == 200;});
drive_one_pkt(tr);
end
endtask
// 通过drive_one_pkt将tr的内容驱动到DUT的端口
task my_driver::drive_one_pkt(my_transaction tr);
bit [47:0] tmp_data; // 暂存my_transaction数据
bit [7:0] data_q[$]; // 队列data_q
// ------将tr中所有的数据压入队列data_q中-------
//push 目的地址dmac to data_q
tmp_data = tr.dmac; // tr.dmac只有8bit,放在tmp_data低位
for(int i = 0; i < 6; i++) begin
data_q.push_back(tmp_data[7:0]);
tmp_data = (tmp_data >> 8); // tmp_data低位放tr.dmac的推出tmp_data
end
//push 源地址smac to data_q
... ...
//push 以太网种类ether_type to data_q
... ...
//push payload to data_q
... ...
//push crc to data_q , CRC是前面所有数据的校验值。
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
// ------再将data_q中所有的数据弹出并驱动-------
`uvm_info("my_driver", "begin to drive one pkt", UVM_LOW); // `uvm_info打印(打印信息的归类,需要打印的信息,冗余级别)
repeat(3) @(posedge vif.clk); // 延迟3个时钟周期
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); // `uvm_info打印
endtask
加入env
是引入一个容器类,在这个容器类中实例化driver、monitor、reference model和scoreboard等。在调用run_test时,传递的参数不再my_driver,而是这个容器类,即让UVM自动创建这个容器类的实例。在UVM中,这个容器类称为uvm_env。
当加入了my_env后,整个验证平台中存在两个build_phase,一个是my_env的,一个是my_driver的。中,build_phase的执行遵照从树根到树叶的顺序,即先执行my_env的build_phase,再执行my_driver的build_phase。
// my_env.sv
// my_driver在uvm_env中实例化,所以my_driver的父结点(parent)就是my_env。
class my_env extends uvm_env; // env派生自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 只有使用factory机制注册过的类才能使用这种方式实例化 type_name::type_id::create(名字drv,this指针)
endfunction
`uvm_component_utils(my_env) // 使用uvm_component_utils宏来实现factory的注册
// 在整个仿真中是一直存在的,所以它是一个component,要使用uvm_component_utils宏注册;
endclass
my_driver 在验证平台中的层次结构发生了变化,它一跃从树根变成了树叶,所以在 top_tb 中使用 config_db 机制传递 virtual my_if 时,要改变相应的路径。
//top_tb.sv
module top_tb;
... ...
initial begin
//run_test("my_driver");
run_test("my_env"); // run_test树根
end
initial begin
//uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if); // uvm_test_top是UVM自动创建的树根的名字
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", inp ut_if); // drv则是在my_env的build_phase中实例化drv时传递过去的名字
end
... ...
endmodule
加入monitor
加入monitor监测DUT的输入输出信号变化,判定DUT的行为是否正确。
driver负责把transaction级别的数据转变成DUT的端口级别,并驱动给DUT。monitor用于收集DUT的端口数据,并将其转换成transaction交给后续的组件如reference model、scoreboard等处理。
uvm_config_db的用途大概有如下三种:
传递virtual interface到环境中。
设置单一变量值,如int.string.enum等。
传递配置对象(config object)到环境中
可以把config_db当成一种邮箱功能,既有寄信方,又有收信方。送到哪的信,则必须去哪里取,所以收发信件的格式如下所示。
寄信方的邮件格式:
uvm_config_db#(邮件类别)::set(发信地址,“相对于发信地址的收件地址”,“邮件名字”,邮件内容);
收信方的邮件格式:
uvm_config_db#(邮件类别)::get(A,“B”,“邮件名字”,邮件内容);
其中AB合起来为收件地址
基于uvm的testbench,由许多uvm_component组成。为了让这些component能够同步的run在某个phase上,uvm设计了uvm_phase,由uvm_test_top依次往下逐级传递。
这样,只要某个component仍然run在某个phase上,也就是有“举手”示意我还没有结束,那么就不会跳到下一个phase
phase 阶段
// 工厂机制注册类通常需要三步:声明,注册,调用new()函数
eg:
class my_monitor extends uvm_monitor; // 声明
`uvm_component_utils(my_monitor) // 注册
function new // 调用 new() 函数
...
endfunction
// my_monitor.sv
class my_monitor extends uvm_monitor; // monitor类派生自uvm_monitor
virtual my_if vif; // my_monitor中也需要有一个virtual my_if;
`uvm_component_utils(my_monitor) // uvm_monitor在整个仿真中是一直存在的,所以它是一个component,要使用uvm_component_utils宏注册;
function new(string name = "my_monitor", uvm_component parent = null); // 树结构声明
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase); // uvm中将phase机制相关的功能独立封装在uvm_phase类中
super.build_phase(phase); // build_phase自动获取通过config_db::set/get设置的参数。
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif)) // 检查 virtual interface传递到环境中。
//uvm_config_db#(邮件类别)::get(A,“B”,“邮件名字”,邮件内容);
`uvm_fatal("my_monitor", "virtual interface must be set for vif!!!")
endfunction
// extern表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。
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 // monitor需要时刻收集数据,永不停歇,所以在main_phase中使用while(1)循环来实现
tr = new("tr");
collect_one_pkt(tr); // 调用函数 collect_one_pkt(my_transaction 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
//... ...
//pop ether_type
//... ...
//pop payload
//... ...
//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
当完成monitor的定义后,可以在env中对其进行实例化:
// my_env.sv
// my_driver在uvm_env中实例化,所以my_driver的父结点(parent)就是my_env。
// my_monitor i_mon/o_mon在uvm_env中实例化,父结点(parent)就是my_env。
class my_env extends uvm_env; // env派生自uvm_env
`uvm_component_utils(my_env) // ,使用uvm_component_utils宏来实现factory的注册。
my_driver drv; // 声名变量
my_monitor i_mon; // 监测DUT的输入口
my_monitor o_mon; // 于监测DUT的输出口
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 只有使用factory机制注册过的类才能使用这种方式实例化 type_name::type_id::create(名字drv,this指针)
i_mon = my_monitor::type_id::create("i_mon", this); // 实例化monitor
o_mon = my_monitor::type_id::create("o_mon", this);
endfunction
endclass
在 env 中实例化 monitor 后,要在 top_tb 中使用 config_db 将 input_if 和 output_if 传递给两个 monitor:
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", inp ut_if); // drv则是在my_env的build_phase中实例化drv时传递过去的名字
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_mon", "vif", input_if); // 监测dut输入
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_mon", "vif", output_if); // 监测dut输出
end
config_db邮箱功能,
寄信方的邮件格式:
uvm_config_db#(邮件类别)::set(发信地址,“相对于发信地址的收件地址”,“邮件名字”,邮件内容);
收信方的邮件格式:
uvm_config_db#(邮件类别)::get(A,“B”,“邮件名字”,邮件内容);
其中AB合起来为收件地址
封装成agent
driver和monitor两者之间的代码高度相似。其本质是因为二者
处理的是同一种协议,在同样一套既定的规则下做着不同的事情。由于二者的这种相似性,UVM中通常将二者封装在一起,成为一个agent。因此,不同的agent就代表了不同的协议。
// my_agent.sv
class my_agent extends uvm_agent ; //声明
`uvm_component_utils(my_agent) //注册
my_driver drv; // 准备在agent中实例化driver和monitor
my_monitor mon;
function new(string name, uvm_component parent); //new函数
super.new(name, parent);
endfunction
// extern表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。
extern virtual function void build_phase(uvm_phase phase);
extern virtual function void connect_phase(uvm_phase phase);
endclass
function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
// 根据is_active变量的值来决定是否创建driver的实例
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
在把driver和monitor封装成agent后,由agent实例化driver和monitor。在env中需要实例化agent,env不再需要直接实例化driver和monitor。
// my_env.sv
class my_env extends uvm_env; // env派生自uvm_env
`uvm_component_utils(my_env) // ,使用uvm_component_utils宏来实现factory的注册。
//my_driver drv; // 声名变量
//my_monitor i_mon; // 监测DUT的输入口
//my_monitor o_mon; // 于监测DUT的输出口
my_agent i_agt; // 声名my_agent类型变量 输入 (my_driver,my_monitor)
my_agent o_agt; // 输出 (my_driver,my_monitor)
function new(string name = "my_env", uvm_component parent);
super.new(name, parent);
endfunction
//在my_env的build_phase中对它们进行实例化
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
//drv = my_driver::type_id::create("drv", this); //实例化drv 只有使用factory机制注册过的类才能使用这种方式实例化 type_name::type_id::create(名字drv,this指针)
//i_mon = my_monitor::type_id::create("i_mon", this); // 实例化monitor
//o_mon = my_monitor::type_id::create("o_mon", this);
i_agt = my_agent::type_id::create("i_agt", this); //实例化
o_agt = my_agent::type_id::create("o_agt", this);
// 指定各自的工作模式是active模式还是passive模式。
i_agt.is_active = UVM_ACTIVE;
o_agt.is_active = UVM_PASSIVE; //my_agent来自uvm_agent,uvm_agent有is_active变量,该变量值有UVM_PASSIVE和UVM_ACTIVE。
endfunction
endclass
由于agent的加入,driver和monitor的层次结构改变了,在top_tb中使用config_db设置virtual my_if时要注意改变路径
// top_tb.sv
... ...
initial begin
//uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if); // uvm_test_top是UVM自动创建的树根的名字
//uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", inp ut_if); // drv则是在my_env的build_phase中实例化drv时传递过去的名字
//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);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.drv", "vif", inp ut_if); // drv则是在my_env的build_phase中实例化drv时传递过去的名字
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.i_mon", "vif", input_if);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_agt.o_mon", "vif", output_if);
end
... ...
加入reference model
reference model的输出被scoreboard接收,用于和DUT的输出相比较。
在my_monitor和my_model中定义并实现了各自的端口
// my_model.sv
// reference model
class my_model extends uvm_component;
`uvm_component_utils(my_model)
uvm_blocking_get_port #(my_transaction) port; // 声明port 数据接收
uvm_analysis_port #(my_transaction) ap; // 数据的发送 uvm_analysis_port是一个参数化的类
extern function new(string name, uvm_component parent);
extern function void build_phase(uvm_phase phase);
extern virtual task main_phase(uvm_phase phase);
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); // 实例化端口port
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); // 在main_phase中,通过port.get任务来得到从i_agt的monitor中发出的transaction。
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_transaction.sv
... ...
// my_copy函数 实现了两个my_transaction的复制
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_model的定义后,需要将其在my_env中实例化。其实例化方式与agent、driver相似。
my_model是从i_agt中得到my_transaction,并把my_transaction传递给my_scoreboard。
在my_monitor和my_model中定义并实现了各自的端口,还需要在my_env中使用fifo将两个端口联系在一起。
connect_phase也是UVM内建的一个phase,它在build_phase执行完成之后马上执行。但是与build_phase不同的是,它的执行顺序并不是从树根到树叶,而是从树叶到树根——先执行driver和monitor的connect_phase,再执行agent的connect_phase,最后执行env的connect_phase
// my_env.sv
... ...
// 假如当write函数调用时,blocking_get_port正在忙于其他事情,而没有准备好接收新的数据时,此时被write函数写入的my_transaction就需要一个暂存的位置,这就是fifo。
uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo; // 声明一个fifo
... ...
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
agt_mdl_fifo = new("agt_mdl_fifo", this); // 实例化agt_mdl_fifo
... ...
// 在connect_phase中将fifo分别与my_monitor中的analysis_port和my_model中的blocking_get_port相连
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
//my_agent.sv
// 数据传送端口
class my_agent extends uvm_agent ;
... ...
uvm_analysis_port #(my_transaction) ap; // 数据的发送 声明ap
// 不需要对my_agent中的ap进行实例化,而只需要在my_agent的connect_phase中将monitor的值赋给它,换句话说,这相当于是一个指向my_monitor的ap的指针:
endclass
... ...
function void my_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
ap = mon.ap; // 将monitor的值赋给它,相当于是一个指向my_monitor的ap的指针
endfunction
my_agent的connect_phase的执行顺序早于my_env的connect_phase的执行顺序,从而可以保证执行到i_agt.ap.connect语句时,i_agt.ap不是一个空指针。
加入scoreboard
my_scoreboard要比较的数据一是来源于reference model,二是来源于o_agt的monitor。前者通过exp_port获取,而后者通过act_port获取
// my_scoreboard.sv
// my_scoreboard要比较的数据一是来源于reference model,二是来源于o_agt的monitor。前者通过exp_port获取,而后者通过act_port获取
class my_scoreboard extends uvm_scoreboard; // 声明
`uvm_component_utils(my_scoreboard) // 注册
my_transaction expect_queue[$]; // 队列
uvm_blocking_get_port #(my_transaction) exp_port; // 声明数据接收端口(数据来源于reference model),连接reference model的ap
uvm_blocking_get_port #(my_transaction) act_port; // 声明数据接收端口(数据来源于o_agt的monitor),连接o_agt的ap
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); // new函数
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); // 在main_phase中通过fork建立起了两个进程,
my_transaction get_expect, get_actual, tmp_tran;
bit result;
super.main_phase(phase);
fork
while (1) begin // 一个进程处理exp_port的数据,来源于reference model,当收到数据后,把数据放入expect_queue中;
exp_port.get(get_expect);
expect_queue.push_back(get_expect);
end
while (1) begin // 另外一个进程处理act_port的数据,这是DUT的输出数据,来源于o_agt的monitor
act_port.get(get_actual);
if(expect_queue.size() > 0) begin
tmp_tran = expect_queue.pop_front();
result = get_actual.my_compare(tmp_tran); // 当收集到这些数据后,从expect_queue中弹出之前从exp_port收到的数据,并调用my_transaction的my_compare函数
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 Que ue is empty");
$display("the unexpected pkt is");
get_actual.my_print();
end
end
join
endtask
my_compare函数
// my_transaction.sv
class my_transaction extends uvm_sequence_item;
... ...
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
endclass
完成my_scoreboard的定义后,也需要在my_env中将其实例化。此时,整棵UVM树变为如图2-8所示的形式