何谓验证平台?验证最基本的目的在于测试 DUT 的正确性,其最常使用的方法就是给 DUT 施加不同的输入(激励),所以一个验证平台最重要的的功能在于产生各种各样不同的激励,并且观测 DUT 的输出结果,把此结果与期望值比较一下,判断DUT 的正确性。注意,这里出现了一个词:期望值。什么是期望值?比如我们的DUT 是一个加法器,那么当我们输入 1+1 时,我们期望 DUT 输出是 2。当在 DUT计算 1+1 的结果时,验证平台也必须相应的执行同样的过程,即计算一次 1+1。 在UVM 中,完成这个过程的是参考模型(reference model);
到此为止,可以想像一下,一个基本的验证平台会有哪几部分组成?要有一个driver,用来把不同的激励施加给 DUT;要有一个 monitor,用来监测 DUT 的输出;要有一个 scoreboard,它专门的比较期望值与 monitor 监测到的 DUT 的输出;要有一个 reference model,它的输入跟 DUT 完全一样,它的输出送给 scoreboard,用于和 DUT 的输出比较。 下图中所有的实线框和实线箭头合起来组成了一个简单的验证平台;
验证用于找出DUT中的BUG,这个过程是把DUT放入一个验证平台来实现的,一个验证平台要实现如下基本功能:
(1)验证平台要模拟DUT的各种真实使用情况,这意味着要给DUT施加各种激励,激励的功能是由driver来实现的;
(2)验证平台要能根据DUT的输出来判断输出是否与预期相符合,完成这个功能的是记分板scoreboard;既然是判断,那么牵扯到两个方面:一是判断什么,这里是DUT的输出;二是判断的标准是什么;
(3)验证平台要收集DUT的输出并把它们传递给scoreboard,完成这个功能的是monitor;
(4)验证平台要能够给出预期成果,在scoreboard提到了判断标准,这里的就是预期结果,完成这个过程的是reference model,通常它是用于DUT不同的语言来写的;
一个简单的验证平台如下图所示:
在 UVM 的验证哲学中, driver, monitor, model, scoreboard 等组成部分都是由一个类(class)来实现的。为什么要使用一个类来实现?可以这么想,如果不用类来实现,那么用什么实现?类有函数(function),另外还可以有任务(task),通过这些function和 task 可以完成 driver 的输出激励功能,完成 monitor 的监测功能,完成 model 的计算功能,完成 scoreboard 的比较功能,类中可以有成员变量,这些成员变量可以控制类的行为,如控制 driver 的行为等。所以,类是实现这些验证平台组成部分的最好选择。 事实上,类也是像 systemverilog 这种面向对象编程语言中最伟大的一个发明,是面对对象的精髓所在。使用 systemverilog 而不使用其中的类就如同在中国只吃西餐不吃中餐,如同使用 windows 系统时不使用图形界面而只使用其提供的 dos界面。所以,有不用类来实现的理由吗?
UVM 预先定义好了一个类 uvm_component, driver、 monitor、 model、 scoreboard等都要从这个类来派生而来。 通过这种形式,把 driver、 monitor、 model、 scoreboard等 都 组 织 在 一 棵 树 上 , 这 样 UVM 就 可 以 方 便 的 执 行 后 面 的 操 作 。 如 果uvm_component 这个概念让你相当困惑的话,那么可以暂时先忘记它,只需要记住:UVM 使用树的形式来组织管理 driver, monitor, model, scoreboard 等,这些都是树上的一个结点。 整个 UVM 验证平台的各个部分就如同一棵倒置的树,如下图所示:
在上图中,出现了 sequencer,这是我们第一次碰到的一个概念,它是 UVM 中独有的一个概念, driver 负责向 DUT 发送数据,那么这些数据怎么来的呢? sequencer正是用于产生这些数据的,一个 sequencer 通过启动一个 sequence,从 sequence 获取
数据,并把这些数据转交给 driver。这种功能划分让 driver 不再关注数据的产生,而只负责数据的发送,职能更加清晰,更容易使用。上图中出现的另外的组件就是In_agent 和 Out_agent,它们是UVM 中的agent,所谓 的agent 其实只是简单的把driver,monitor 和 sequencer 封装在一起。通常来说, agent 对应的是物理接口协议,不同的接口协议对应不同的 agent,接口协议规定了数据的交换格式和方式,agent 通过 driver和 monitor 来实现接口协议的这些内容。在一个验证平台通常会有多个 agent,如上面所示的 In_agent,里面有 driver 和 monitor,用于向 DUT 发送数据,而 Out_agent中只有 monitor,用于监测 DUT 的输出。 上图中的 env 则相当于是一个特大的容器,它把所有的 uvm_component 都包含在其内部作为其成员变量。
与图 1-4 对应的验证平台如下图所示。在这幅图中,黑色的连线表示验证平台和 DUT 的物理接口,而蓝色线表示验证平台不同的 component 之间的数据连接:
2.2.1验证平台中的driver(1)
假设有如下的 DUT 定义:
module dut (clk,
rxd,
rx_dv,
txd,
tx_en);
input clk;
input [7:0] rxd;
input rx_dv;
output [7:0] txd;
output tx_en;
reg [7:0] txd;
reg tx_en;
always @(posedge clk) begin
txd <= rxd;
tx_en <= rx_dv;
end
endmodule
整个 dut 的功能非常简单,就是 rxd 接收数据,再直接通过 txd 发送出去。其中rx_dv 是接收的数据有效指示, tx_en 是发送的数据有效指示。验证要做的事情就是产生数据,然后发送到 rxd 上;在 txd 上监测输出并接收数据。检查这个数据是不是与我们之前发送过去的数据一样。如果一样,说明数据经过我们的 DUT 没有丢失, DUT 工作正常;如果不一样,说明 DUT 把数据给丢失了,那就需要仔细核查,找出问题所在。 UVM 验证平台中使用 driver 来发送数据。 一个实际的可以使用的 driver 如下:
`include "uvm_macros.svh"
import uvm_pkg::*;
class my_driver extends uvm_driver #(my_transaction);
virtual my_if vif;
uvm_analysis_port #(my_transaction) ap;//分析端口将总线上的数据发送到referencemodel中
`uvm_component_utils(my_driver)
extern function new (string name, uvm_component parent);
extern virtual function void build_phase(uvm_phase phase);
extern virtual task main_phase(uvm_phase phase);
extern task drive_one_pkt(my_transaction req);
extern task drive_one_byte(bit [7:0] data);
endclass
function my_driver::new (string name, uvm_component parent);
super.new(name, parent);
endfunction
function void my_driver::build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual my_if)::get(this, "", "my_if", vif))//获取虚接口
`uvm_fatal("my_driver", "Error in Getting interface");
ap = new("ap", this);
enfunction
2.2.2验证平台中的transaction
物理协议中的数据交换都是以帧或者包为单位的,通常一帧或者一个包中要定义好各项参数,每个包的大小不一样;一个 transaction 就是一个包;
`include "uvm_macros.svh"
import uvm_pkg::*;
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[] ; // size should be configurable
rand bit [31:0] crc;
constraint cons_pload_size {
pload.size >= 46;
pload.size <= 1500;
}
extern function new (string name = "my_transaction");
`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 // my_transaction
function my_transaction::new(string name = "my_transaction");
super.new(name);
endfunction
endclass
通常来说,一个 driver 只能接收一种 transaction,所以在 my_driver 被定义成了一个参数化的类,这个参数就是 my_driver 所能接收的 transaction 的类型;
2.2.3验证平台中的driver(2)
一个派生自 uvm_component 的类,其主要的行为发生在 main_phase(或者run_phase) 中,诸如driver, monitor, reference model, scoreboard 等最重要的是定义好其 main_phase。
2.2.4验证平台中的monitor
monitor 主要用于监测 DUT 的输出,其操作首先要获取virtual interface,然后接收接口上的数据,再将数据广播出去;一个简单的 monitor 如下:
function void my_monitor::build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual my_if)::get(this, "", "my_if", vif))
uvm_report_fatal("my_monitor","Error in Getting interface");
ap = new("ap", this);
endfunction
task my_monitor::main_phase(uvm_phase phase);
logic valid;
logic [7:0] data;
my_transaction tr;
super.main_phase(phase);
while(1) begin
tr = new()
receive_one_pkt(tr);
ap.write(tr);
end
endtask
my_monitor 与 driver 有些类似,它通过无限的循环来不断的监测 DUT 的输出;
2.2.5验证平台中的agent
agent 在 UVM 验证平台中的主要作用就是把 monitor 和 driver 封装在一起,因为 monitor 和 driver 都是直接和 DUT 的接口打交道的,当把它们封装成为一个 agent后,整个验证平台中就只有 agent 是与实际的物理接口打交道的;
在 UVM 验证平台的 agent 中,除了封装了 monitor 和 driver 外,还封装了sequencer。sequencer 在本质上是不与物理接口直接打交道的,但是 sequencer 与 driver关系非常密切, sequencer 的职能最初就是从 driver 中分离出来的,所以 UVM 中也把 sequencer 封装在一起。一个简单的 sequencer 的定义为:
`include "uvm_macros.svh"
import uvm_pkg::*;
class my_sequencer extends uvm_sequencer #(my_transaction);
//Component
extern function new( string name, uvm_component parent);
extern function void build_phase(uvm_phase phase);
//Register
`uvm_component_utils(my_sequencer)
endclass
function my_sequencer::new(string name, uvm_component parent);
super.new(name, parent);
endfunction // new
function void my_sequencer::build_phase(uvm_phase phase);
super.build_phase(phase);
endfunction
事实上, sequencer 确实几乎是 UVM 验证平台中最简单的一个 component,大部分验证平台中用到的sequencer 都跟这个定义差不多,但是像其它的 component,如 driver, monitor 在不同的验证平台中则会千差万别。 my_sequencer 是一个参数化的类,其参数是my_transaction,用于表明这个 sequencer 只能产生 my_transaction 类型的数据,这一点与 my_driver 类似。
sequencer 定义完成后,可以看 agent 的定义了:
function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
if (is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver::type_id::create("drv", this);
end
else begin
mon = my_monitor::type_id::create("mon", this);
end
endfunction // build_phase
function void my_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (is_active == UVM_ACTIVE) begin
drv.seq_item_port.connect(sqr.seq_item_export);
this.ap = drv.ap;
end
else begin
this.ap = mon.ap;
end
endfunction
agent主要完成三个组件的实例化以及它们之间的连接;其关键在于 build_phase。在这个函数中,通过 is_active 的值来实例化
不同的成员变量。 is_active 用来形容这个 agent 所扮演的角色。通常的,当一个 agent要驱动总线时,它会实例化一个 driver,这种情况下就是 UVM_ACTIVE;当一个 agent只是用于监测总线的输出时,它不需要把 monitor 实例化,而只需要把 monitor 实例化;
2.2.6 验证平台中的reference model
reference model 是整个 UVM 验证平台的关键与核心,因为 reference model 作的工作与 DUT 一致, scoreboard 将会根据 reference model 的输出和 DUT 进行比较,如果 reference model 中有错误,那么其输出也是不可信的,从而 scoreboard 的输出也是不可信的;
`include "uvm_macros.svh"
import uvm_pkg::*;
class my_model extends uvm_component;
uvm_blocking_get_port #(my_transaction) port;//获取来自fifo的总线数据
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 // my_model
function my_model::new(string name, uvm_component parent);
super.new(name, parent);
endfunction // new
function void my_model::build_phase(uvm_phase phase);
super.build_phase(phase);
port = new("port", this);
ap = new("ap", this);
endfunction // build_phase
task my_model::main_phase(uvm_phase phase);
my_transaction tr;
super.main_phase(phase);
while(1) begin
port.get(tr);
ap.write(tr);
end
endtask
2.2.7 验证平台中的scoreboard
scoreboard 主要用于比较 reference model 和 DUT 输出是否一致,并给出比较结果。一个简单的 scoreboard 如下:
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
exp_port.get(get_expect);
expect_queue.push_back(get_expect);
end
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);
if(result) begin
$display("Compare SUCCESSFULLY");
end
else begin
$display("Compare FAILED");
$display("the expect pkt is");
tmp_tran.print();
$display("the actual pkt is");
get_actual.print();
end
end
else begin
$display("ERROR::Received from DUT, while Expect Queue is empty");
get_actual.print();
end
end
join
endtask
main_phase 中使用 fork 开启了两个进程,其中一个进程用于从 reference model中获得数据,另外一个进程用于从 monitor 中获得数据。一般说来,同样的一组数据,经过 DUT 的处理后会有一定的延迟,但是在 reference model 中不会(除非刻意这做)。因此,从 scoreboard 的角度来看, reference model 中的数据总是先到达,所以先把 reference model 的数据先放入一个队列中。由于 DUT 的输出后到达, 当接收到 DUT 的输出后,先查看队列中是否有记录,如果没记录,说明 reference model 没有数据输出,而 DUT 输出了,这是不期望的, 给出错误信息。如果队列中有记录,那么 从队列中弹出第一个数据,并把此数据和 DUT 的输出比较;