个人《UVM实战卷I》学习随手笔记,此书作者:张强 验证平台的组成 一个验证平台要实现的基本功能:
- 模拟DUT的各种真实使用情况——给DUT施加各种激励→driver实现激励功能;
- 根据DUT的输出判断DUT行为是否与预期符合→scoreboard(也称为checker)——需注意判断的东西和判断的标准是什么;
- 收集DUT的输出并传递给scoreboard→monitor;
- 给出预期结果——即scoreboard的判断标准就是预期→reference model;
简单验证平台框架: UVM引入agent和sequence的概念,UVM验证平台的典型框架图: 最简单的验证平台——只有driver
- driver是验证平台最基本的组件,是整个验证平台数据流的源泉;
DUT定义:通过rxd接收数据,由txd发送出去;rx_dv是接收的数据有效指示,tx_en是发送的数据有效指示。(此篇例子均基于该DUT设计)
1 module dut(clk,
2 rst_n,
3 rxd,
4 rx_dv,
5 txd,
6 tx_en);
7 input clk;
8 input rst_n;
9 input[7:0] rxd;
10 input rx_dv;
11 output [7:0] txd;
12 output tx_en;
13
14 reg[7:0] txd;
15 reg tx_en;
16
17 always @(posedge clk) begin
18 if(!rst_n) begin
19 txd <= 8'b0;
20 tx_en <= 1'b0;
21 end
22 else begin
23 txd <= rxd;
24 tx_en <= rx_dv;
25 end
26 end
27 endmodule
UVM如何搭建driver:
- UVM是一个库,库中几乎所有东西都是使用**类(class)**实现,类是OOP的精髓所在;
- 类里有function和task,可利用它们完成driver的输出激励功能、monitor的监测功能、ref_mod的计算功能、scoreboard的比较功能;
- 类中有成员变量,用来控制类的行为;
- 当要实现一个功能时,首先应想的是从UVM的某个类派生(extend)出一个新类,在新类中实现所期望的功能——故使用UVM的第一原则:验证平台中所有组件应派生自UVM中的类。
driver应派生自uvm_driver,简单的driver示例:
class my_driver extends uvm_driver;
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
endfunction
extern virtual task main_phase(uvm_phase phase);
endclass
task my_driver::main_phase(uvm_phase phase);
top_tb.rxd <= 8'b0;
top_tb.rx_dv <= 1'b0;
while(!top_tb.rst_n)
@(posedge top_tb.clk);
for(int i = 0; i < 256; i++)begin
@(posedge top_tb.clk);
top_tb.rxd <= $urandom_range(0, 255);
top_tb.rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW)
end
@(posedge top_tb.clk);
top_tb.rx_dv <= 1'b0;
endtask
- driver向rxd发送256个随机数据,将rx_dv信号置为高电平。数据发送完毕后则置位低电平;
- 所有派生自uvm_driver的类的new函数有两个参数:string类型的name和uvm_component类型的parent;
- 这两个参数是由uvm_component要求的, 每一个派生自uvm_component或其派生类的类在其new函数中要指明两个参数: name和parent,而uvm_driver是派生自uvm_component的类, 所以也有这两个参数;
- driver所做的事情几乎都在main_phase中完成。 UVM由phase来管理验证平台的运行, 这些phase统一以xxxx_phase来命名, 且都有一个类型为uvm_phase、 名字为phase的参数。 main_phase是uvm_driver中预先定义好的一个任务。 因此几乎可以简单地认为,实现一个driver等于实现其main_phase;
- 代码中出现了uvm_info宏,其功能与Verilog中display语句类似但更强大。 它有三个参数, 第一个参数是字符串, 用于把打印的信息归类; 第二个参数也是字符串, 是具体需要打印的信息; 第三个参数是冗余级别。 在验证平台中某些信息是非常关键的,可以设置为UVM_LOW, 而有些信息可有可无, 就设为UVM_HIGH, 介于两者之间的就是UVM_MEDIUM。 UVM默认只显示UVM_MEDIUM或者UVM_LOW的信息
示例打印结果为:UVM_INFO my_driver.sv(20) @ 48500000: drv [my_driver] data is drived
- UVM_INFO关键字: 表明是uvm_info宏打印的结果。 还有uvm_error宏、 uvm_warning宏;
- my_driver.sv(20): 指明此条打印信息的来源, 其中括号里的数字表示原始的uvm_info打印语句在my_driver.sv中的行号;
- 48500000: 表明此条信息的打印时间;
- drv: 这是driver在UVM树中的路径索引。 UVM采用树形结构, 对于树中任何一个结点, 都有一个与其相应的字符串类型的路径索引。 路径索引可以通过get_full_name函数来获取,如:$display(“the full name of current component is: %s”, get_full_name());
- [my_driver]:方括号中显示的信息即调用uvm_info宏时传递的第一个参数;
- data is drived:宏最终打印的信息。
uvm_info宏包含了打印信息的物理文件来源、 逻辑结点信息(UVM树中的路径索引)、 打印时间、 对信息的分类组织及打印的信息。 搭建验证平台时应尽量使用uvm_info宏取代display语句。
定义my_driver后需将其实例化,类的实例化指的是通过new创造出对应的实例。
A a_inst;
a_inst = new();
- 类的定义类似于在纸上写下一纸条文,然后把这些条文通知给SV仿真器:验证平台可能会用到这样的一个类,请做好准备工作;
- 类的实例化在于通过new()通知SV的仿真器创建一个A的实例。仿真器接到new的指令后就会在内存中划分一块空间,在划分前会检查是否已经预先定义过这个类;
- 在已经定义过的情况下,按照定义中所指定的“条文”分配空间,并且把这块空间的指针返回给a_inst,之后就可以通过a_inst来查看类中的各个成员变量,调用成员函数/任务等;
- 对大部分的类来说,如果只定义而不实例化,是没有任何意义的;而如果不定义就直接实例化,仿真器将会报错。
对my_driver实例化且最终搭建的验证平台如下:
`timescale 1ns/1ps
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "my_driver.sv"
module top_tb;
reg clk;
reg rst_n;
reg [7:0] rxd;
reg rx_dv;
wire [7:0] txd;
wire tx_en;
dut my_dut(.clk(clk),
.rst_n(rst_n),
.rxd(rxd),
.rx_dv(rx_dv),
.txd(txd),
.tx_en(tx_en));
initial begin
my_driver drv;
drv = new("drv", null);
drv.main_phase(null);
$finish();
end
initial begin
clk = 0;
forever begin
#100 clk = ~clk;
end
initial begin
rst_n = 1'b0;
#1000;
rst_n = 1'b1;
end
endmodule
加入factory机制 下面示例:自动创建一个类的实例并调用其中的function和task,使用这个功能需要引入UVM的factory机制。
class my_driver extends uvm_driver;
`uvm_component_utils(my_driver);
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
`uvm_info("my_driver", "new is called", UVM_LOW);
endfunction
extern virtual task main_phase(uvm_phase phase);
endclass
task my_driver::main_phase(uvm_phase phase);
`uvm_info("my_driver", "main_phase is called", UVM_LOW);
top_tb.rxd <= 8'b0;
top_tb.rx_dv <= 1'b0;
while(!top_tb.rst_n)
@(posedge top_tb.clk);
for(int i = 0; i < 256; i++)begin
@(posedge top_tb.clk);
top_tb.rxd <= $urandom_range(0, 255);
top_tb.rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW);
end
@(posedge top_tb.clk);
top_tb.rx_dv <= 1'b0;
endtask
factory机制的实现被集成在了一个宏中:uvm_component_utils。 该宏做的事情非常多, 其中之一就是将my_driver登记在UVM内部的一张表中, 这张表是factory功能实现的基础。 只要在定义一个新的类时使用这个宏, 就相当于把这个类注册到了这张表中;
给driver加入factory机制后,还需要对top_tb做改动:
module top_tb;
…
initial begin
run_test("my_driver");
end
endmodule
输出结果:
new is called
main_phased is called
- run_test语句会创建一个my_driver的实例,并会自动调用其main_phase;传递给run_test语句的是一个字符串,UVM根据该字符串创建了其所代表类的一个实例。
- 根据类名创建一个类的实例,这是uvm_component_utils宏所带来的效果。只有在类定义时声明了这个宏,才能使用这个功能。所以这个宏起到了注册的作用,只有经过注册的类,才能使用这个功能
- 所有派生自uvm_component及其派生类的类都应该使用uvm_component_utils宏注册。
- 在UVM中只要一个类使用uvm_component_utils注册且此类被实例化了,那么该类的main_phase就会被自动调用。(前面提到实现一个driver等于实现其main_phase,这就是关联之处)
- 输出结果没有输出“data is drived”,且代码没有显式调用finish语句来结束仿真,但实际上运行代码,仿真平台会自动关闭,这牵涉到UVM的objection机制。
加入objection机制
- 接上文,UVM中通过objection机制控制验证平台的关闭,在每个phase中UVM会检查是否有objection被挂起(raise_objection),如果有则会等待落下(drop_pbjection)后停止仿真;如果没有,则马上结束当前phase。
- 在drop_objection语句之前必须先调用raise_objection语句,两者总是成对出现,加入objection机制后运行结果是“data is drived”按照预期输出流256次。
- raise_objection语句必须在main_phase中第一个消耗仿真时间的语句之前。
- 所谓仿真时间是指$time函数打印出的时间;与之相对的是实际仿真中所消耗的CPU时间,通常说一个测试用例的运行时间是指CPU时间,为与仿真时间区分一般称为运行时间。
加入objection机制的driver如下:
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("my_driver", "main_phase is called", UVM_LOW);
top_tb.rxd <= 8'b0;
top_tb.rx_dv <= 1'b0;
while(!top_tb.rst_n)
@(posedge top_tb.clk);
for(int i = 0; i < 256; i++)begin
@(posedge top_tb.clk);
top_tb.rxd <= $urandom_range(0, 255);
top_tb.rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW);
end
@(posedge top_tb.clk);
top_tb.rx_dv <= 1'b0;
phase.drop_objection(this);
endtask
加入virtual interface
- 在前面例子中driver等待时钟事件(@posedge top.clk)、给DUT中输入端口赋值(top.rx_dv<=1‘b1) 都是使用绝对路径,绝对路径的使用大大减弱了验证平台的可移植性。
- 最简单的例子就是假如clk信号的层次从top.clk变成了top.clk_inst.clk,那么就需要对driver中的相关代码做大量修改。 因此从根本上来说,应尽量杜绝在验证平台中使用绝对路径。
避免绝对路径的一个方法是使用宏:当路径修改时只需修改宏的定义即可。但假如clk的路径变为top_tb.clk_inst.clk,而rst_n的路径变为top_tb.rst_inst.rst_n,那么修改宏定义是无法起作用的。
`define TOP top_tb
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("my_driver", "main_phase is called", UVM_LOW);
`TOP.rxd <= 8'b0;
`TOP.rx_dv <= 1'b0;
while(!`TOP.rst_n)
@(posedge `TOP.clk);
for(int i = 0; i < 256; i++)begin
@(posedge `TOP.clk);
`TOP.rxd <= $urandom_range(0, 255);
`TOP.rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW);
end
@(posedge `TOP.clk);
`TOP.rx_dv <= 1'b0;
phase.drop_objection(this);
endtask
另一种方法是使用interface:在SV中使用interface来连接验证平台与DUT的端口.
interface my_if(input clk, input rst_n);
logic [7:0] data;
logic valid;
endinterface
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));
接下来是如何在driver中使用interface的问题——第一种想法是在driver中声明如下语句,然后通过赋值的形式将top_tb中的input_if传递给它:
class my_driver extends uvm_driver;
my_if drv_if;
…
endclass
- 但这样是会报错的,因为my_driver是一个类,在类中不能使用下面方式声明一个interface,只有在类似top_tb这样的模块中才可以。
- 在类中使用的是virtual interface(因为interface是硬件概念):
class my_driver extends uvm_driver;
virtual my_if vif;
endclass
在声明了vif后,就可以在main_phase中使用如下方式(vif.xxx)驱动其中的信号:
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("my_driver", "main_phase is called", UVM_LOW);
vif.data <= 8'b0;
vif.valid <= 1'b0;
while(!vif.rst_n)
@(posedge vif.clk);
for(int i = 0; i < 256; i++)begin
@(posedge vif.clk);
vif.data <= $urandom_range(0, 255);
vif.valid <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW);
end
@(posedge vif.clk);
vif.valid <= 1'b0;
phase.drop_objection(this);
endtask
最后,如何把top_tb中的input_if和my_driver中的vif对应起来?
- 最简单的方法是直接赋值,但在top_tb中通过run_test语句建立一个my_driver的实例,应如何引用这个实例呢?不能像引用my_dut那样直接引用my_driver中的变量:top_tb.my_dut.xxx是可以的,但top_tb.my_driver.xxx是不可以的。这个问题的原因在于UVM通过run_test语句实例化了一个脱离了top_tb层次结构的实例,建立了一个新的层次结构。
- 对于这种脱离top_tb层次结构,同时又期望在top_tb中对其进行某些操作的实例,UVM引进了config_db机制。在config_db机制中,分为set和get两步操作。set操作可理解是“寄信”,而get则相当于是“收信”。
在top_tb中执行set操作:
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
end
在top_tb中执行get操作:
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
`uvm_info("my_driver", "build_phase is called", UVM_LOW);
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
`uvm_fatal("my_driver", "virtual interface must be set for vif!!")
endfunction
-
上例中的build_phase与main_phase一样,build_phase也是UVM中内建的一个phase。当UVM启动后会自动执行build_phase。build_phase在new函数之后main_phase之前执行,主要通过config_db的set和get操作来传递一些数据,以及实例化成员变量等。 -
上例中加入了super.build_phase语句,因为在其父类的build_phase中执行了一些必要的操作,必须显式地调用并执行它。 -
在build_phase中出现了uvm_fatal宏,该宏只有两个参数,与uvm_info宏的前两个参数的意义完全一样,但当它打印第二个参数的信息后会直接调用finish函数结束仿真。uvm_fatal的出现表示验证平台出现了重大问题而无法继续下去,必须停止仿真并做相应的检查,所以不需要uvm_info的第三个参数(冗余度),uvm_fatal打印的信息是非常关键的。 -
config_db的set和get函数都有四个参数,它们的第三个参数必须完全一致。set函数的第四个参数表示要将哪个interface通过config_db传递给my_driver,get函数的第四个参数表示把得到的interface传递给哪个my_driver的成员变量。set函数的第二个参数表示的是路径索引。 在top_tb中通过run_test创建了一个my_driver的实例,它的名字是uvm_test_top——UVM通过run_test语句创建该实例。 -
无论传递给run_test的参数是什么,创建的实例名都为uvm_test_top。由于set操作的目标是my_driver,所以set函数的第二个参数就是uvm_test_top。 set与get的写法比较独特——使用双冒号是因为它们都是静态函数,而uvm_config_db#( virtual my_if)是一个参数化的类,该参数是要“寄信”的类型,这里是virtual my_if。假如要向my_driver的var变量传递一个int类型的数据,可用如下方式:
initial begin
uvm_config_db#(int)::set(null, "uvm_test_top", "var", 100);
end
而在my_driver中应使用如下方式:
class my_driver extends uvm_driver;
int var;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
`uvm_info("my_driver", "build_phase is called", UVM_LOW);
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
`uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
if(!uvm_config_db#(int)::get(this, "", "var", var))
`uvm_fatal("my_driver", "var must be set!!!")
endfunction
从这里可以看出,可以向my_driver中“寄”许多信。 上面两个例子是top_tb向my_driver传递了两个不同类型的数据,其实也可以传递相同类型的不同数据。 假如my_driver中需要两个my_if, 那么可以在top_tb中这么做:
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif2", output_if);
end
在my_driver中这么做:
virtual my_if vif;
virtual my_if vif2;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
`uvm_info("my_driver", "build_phase is called", UVM_LOW);
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
`uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif2", vif2))
`uvm_fatal("my_driver", "virtual interface must be set for vif2!!!")
endfunction
|