UVM实战 卷I学习笔记1——简单的UVM验证平台:只有driver

目录

    • 验证平台的组成
    • 最简单的验证平台——只有driver
    • UVM如何搭建driver:
    • 加入factory机制
    • 加入objection机制
    • 加入virtual interface


个人《UVM实战卷I》学习随手笔记,此书作者:张强

验证平台的组成

一个验证平台要实现的基本功能:

  • 模拟DUT的各种真实使用情况——给DUT施加各种激励→driver实现激励功能;
  • 根据DUT的输出判断DUT行为是否与预期符合→scoreboard(也称为checker)——需注意判断的东西和判断的标准是什么;
  • 收集DUT的输出并传递给scoreboard→monitor
  • 给出预期结果——即scoreboard的判断标准就是预期→reference model

简单验证平台框架:
UVM实战 卷I学习笔记1——简单的UVM验证平台:只有driver_第1张图片
UVM引入agent和sequence的概念,UVM验证平台的典型框架图:
UVM实战 卷I学习笔记1——简单的UVM验证平台:只有driver_第2张图片

最简单的验证平台——只有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); //extern常用于封装
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"  //UVM文件,包含众多宏定义

import uvm_pkg::*;//导入该库,在编译my_driver.sv时才会识别其中的uvm_driver等类名
`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; //定义my_driver的实例并将其实例化
	drv = new("drv", null); //new函数传入的名字参数drv,前面uvm_info打印时出现的drv在此传入
	drv.main_phase(null); //显式调用my_driver的main_phase,括号里的参数phase不需用户理会
	$finish();//结束仿真,是Verilog的函数
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;//与上面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 //使用run_test语句替换了my_driver实例化及main_phase的显式调用
	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

//定义了interface后,在top_tb中实例化DUT时就可直接使用:
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

你可能感兴趣的:(UVM实战卷I,学习笔记,测试用例,功能测试,测试覆盖率)