• UVM实战笔记(二)


    第二章 一个简单的UVM验证平台

    2.1 DUT

    本章的DUT(Design Under Test,待测设计,也可叫做DUV,Design Under Verification)定义如下:

    //my_dut.v
    
    module dut(	  clk,
    	 		rst_n,
    	 		  rxd,
    			rx_dv,
      	 		  txd,
    			tx_en);
    			
    input 				clk;
    input 	 	 	  rst_n;
    input   [7:0]       rxd;		    //receive data             
    input		 	  rx_dv;			//receive enable signal
    output  [7:0]   	txd;			//transfer data
    output  		  tx_en;			//transfer enable signal
    
    reg 	[7:0]       txd;
    reg  			  tx_en;
    
    always@(posedge clk)
    begin
        //reset
    	if(!rst_n)
    	begin
    		txd 	<= 8'b0;
    		tx_en   <= 1'b0;
    	end
    	else
    	begin
    		txd 	<= rxd;
    		tx_en   <= rx_dv;
    	end
    end
    endmodule
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    DUT通过rxd接收数据,通过txd发送出去。rx_dv为输入信号的有效性指示,tx_en是发送数据的有效性指示。

    2.2 验证平台的组成

    验证用于找出DUT的bug,这个过程通常是把DUT放入一个验证平台中实现的。验证平台的组件包括:

    • Driver:驱动器,用于产生激励。验证平台要模拟DUT的各种真实使用情况,这意味这要给DUT施加各种激励(正常/异常激励)。

    • Scoreboard/Checker(下文用Scoreboard,简写Scb):检查板,根据DUT的输出来判断DUT的行为是否与预期相符合。

    • Monitor:监测器,收集DUT的输出并把他们传递给Scb。

    • Reference Model(简写Rfm):参考模型,模拟硬件DUT的功能,给出期望值并发送给Scb。

    在这里插入图片描述

    2.3 只有driver的测试平台

    UVM是一个类库,几乎所有的东西都是使用类(class)来实现。driver,monitor,reference model,scoreboard等组成部分都是类。类中包含函数(function)和任务(task),通过这些函数和任务可以完成driver的输出激励功能,完成monitor的检测功能,完成参考模型的计算功能,完成scoreboard的比较功能。要实现一个功能时,首先应该想到的是从UVM的某个类派生一个新的类。所以使用UVM第一条准则验证平台中所有的组件应该派生自UVM中的类

    //my_driver.sv
    
    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;
        
        //wait until rst_n turns high
        while(!top_tb.rst_n)	@(posedge top_tb.rst_n);
        
        //rst_n is high, start to drive simulation
        //for cycle transfer one random value of 0~255 to rxd every clk
    	for(int i=0;i<256;i++)
    	begin
    		@(posedge top_tb.clk);
    		top_tb.rxd <= $random(0, 255);
    		top_tb.rx_dv <= 1'b1;
            //use uvm macro to print information, similar to $display in SV
    		`uvm_info("my_driver", "data is drived", UVM_LOW)
    	end
        
        //simulation finished turn the rx_dv low
    	@(posedge top_tb.clk);
    	top_tb.rx_dv <= 1'b0;
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 简单起见,先说明各组件的执行流程,至于为什么这么执行后续文章会说明。driver组件会执行其main_phase任务,激励就按照main_phase的代码进行发送。
    • `uvm_info为uvm的打印信息宏,在这里简单理解对标SV的$display函数,后文详细介绍。

    以上为my_driver的定义,如果要使用my_driver必须首先实例化my_driver(OOP特性不再赘述,对OOP特性不了解的可以找一个OOP语言学习下,推荐C++)。

    //top_tb.sv
    
    //pre-compile instruction
    //declare time unit and time precious
    `timesacle 1ns/1ps
    
    //include uvm macro head file(file replace)
    `include "uvm_macros.svh"
    
    //import UVM standard package
    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] rxd;
        wire tx_en;
        
        //instance dut(connect dut port to corresponding signal)
        dut my_dut(.clk(clk),
                   .rst_n(rst_n),
                   .rx_dv(rx_dv),
                   .txd(txd),
                   .tx_en(tx_en));
        
        //initial block to define simulation process
        initial
        begin
            my_driver drv;
            //instance my_dviver
            drv = new("drv", null);
    		//call drv.main_phase to run simulation
            drv.main_phase(null);
            $finish();
        end
        
        //initial block to generate system clock
        initial
         begin
             clk = 0;
             forever
             begin
                 #100 clk = ~clk;
             end
         end
        
        //initial block to generator DUT reset signal
        initial
        begin
            rst_n = 1'b0;
            #1000;
            rst_n = 1'b1;
        end
                
    endmodule
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    2.3.1 加入factory机制

    使用factory之前需要使用`uvm_component_utils()进行宏注册。宏注册完成的事情非常多,其中之一是将类登记在UVM内部维护的一张表中,这张表就是实现factory功能实现的基础。需要对driver.sv和top_tb.sv做以下修改。

    //my_driver.sv
    
    class my_driver extends uvm_driver;
    	
    	//macor utils
    	`uvm_component_utils(my_driver)
            
    	function new(string name = "my_driver", uvm_component parent = null);
    		super.new(name, parent);
    
    		//add debug information
    		`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)
         ...
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    //top_tb.sv
    
    module top_tb;
    ...
    	//call run_test to run simulation
    	initial
    	begin
    		run_test("my_driver");
    	end
    endmodule
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    //simulation output
    new_is called
    main_phase is called
    
    • 1
    • 2
    • 3
    • 首先在my_driver定义中使用`uvm_component_utils进行了宏注册。在top_tb中取代drv.main_phase(null)的直接调用方式,使用run_test直接运行仿真run_test会创建(隐式调用new函数)一个my_drvier的实例,并且会自动调用my_driver的main_phase。这种由一个类名(字符串)创建一个类的实例,这就是uvm_component_utils宏注册带来的效果。所有的uvm_component及其派生类都应该使用uvm_component_utils进行宏注册。
    • 另外一个地方时main_phase自动调用了。在UVM验证平台中,只要一个类使用uvm_component_utils注册并且此类被实例化了,那么这个类的main_phase就会被自动调用。其实实现driver等于实现其main_phase任务,所以在driver中最终要的是实现其main_phase任务。

    但是运行上述仿真,只输出了打印信息,main_phase中发送的值并没有被打印,这涉及到UVM中的objection机制。

    2.3.2 加入objection机制

    UVM中objection机制来控制验证平台的关闭。在第一个例子中调用了$finish来显式关闭平台。实际上在每个phase中,UVM会检查是否有objection被提起(raise_objection)。如果有那么等待这个objection被撤销(drop_objection)后停止仿真。如果没有,则马上结束当前phase

    //my_driver.sv
    
    //add objection to main_phase
    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.rst_n);
        
    	for(int i=0;i<256;i++)
    	begin
    		@(posedge top_tb.clk);
    		top_tb.rxd <= $random(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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在加入了objection之后,激励就会执行。这里可以简单理解为drop_objection是finish的替代形式,不同的是在drop_objection之前必须先调用raise_objection。raise_objection和drop_objection语句总是成对存在

    另外一个注意地方是raise_objection语句必须在main_phase中第一个消耗仿真时间的语句之前。如$display语句时不消耗仿真时间的,这些语句可以放在raise_objection之前,但是类似@(posedge top.clk)等语句是消耗放着年时间的,raise_objection之前。如果将消耗仿真时间的语句放在raise_objection之前,那么objection机制将失效,此phase不会执行。

    2.3.3 加入virtual interface

    在以上的例子中,driver中的等待时钟时间(@posedge top.clk),给DUT中输入端口赋值(top.rx_dv <= 1’b1)都是使用绝对路径绝对路径的使用大大减弱了验证平台的可移植性。从根本上说,应该尽量杜绝在验证平台中使用绝对路径。

    其一是使用宏(Macro)

    //my_driver.sv
    
    `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.rst_n);
        
    	for(int i=0;i<256;i++)
    	begin
    		@(posedge `TOP.clk);
    		`TOP.rxd <= $random(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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这样,当修改路径时,只需要修改宏的定义即可

    另一种方法是使用interface。在SV中使用interface来连接验证平台与DUT的端口

    //interface.sv
    
    //define interface
    interface my_if(input clk, input rst_n);
    	logic [7:0] data;
    	logic valid;
    endinterface
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    定义了interface后,在top_tb中实例化DUT时,就可以直接使用。

    //top_tb.sv
    ...
    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));
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    那么如何在driver中连接端口呢?由于driver是一个类,类中不能有interface,而应该使用virtual interface

    class my_driver extends uvm_driver;
    	virtual my_if vif;
    endclass
    
    • 1
    • 2
    • 3

    在class中声明了virtual interface后就可以在main_phase中使用如下方式驱动信号:

    //my_driver.sv
    
    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 <= $random(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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    加入了virtual interface后可以看出代码中的绝对路径已经全部消除,增加了代码的可移植性

    最后一个问题是,如何把top_tb中的input_if和my_driver中的vif对应起来?不能通过直接赋值。因为在top_tb中,通过run_test语句建立一个my_driver的实例,但是如何引用这个实例?不能直接引用,其原因是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
    
    • 1
    • 2
    • 3
    • 4

    在my_driver中,执行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!!!")
    endfuntion
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里引入了build_phase,与main_phase一样,build_phase也是UVM内建的一个phase。当UVM启动后,会自动执行build_phase。在build_phase中出现了uvm_fatal宏,uvm_fatal宏是一个类似uvm_info的宏,但是只有两个参数。当uvm_fatal打印完信息后,会直接调用个V的finish函数来结束仿真。uvm_fatal的出现表示验证平台出现了重大问题无法继续进行下去,必须停止仿真并做相应的检查。

    config_db的set和get都有四个参数。这两个函数的第三个参数必须完全一致set函数的第四个参数表示要将哪个interface通过config_db传递给my_driverget函数的第四个参数表示把得到的interface传递给my_driver的成员变量set函数的第二个参数表示的是路径的索引在top_tb中通过run_test创建了一个my_driver的实例,这个实例的名字是uvm_test_top无论传递给run_test的参数是什么,创建的实例名字都为uvm_test_top

    set和get函数使用::是因为这两个函数都属于静态函数。而uvm_config_db #(virtual my_if)则是一个参数化的类,其参数就是要寄信的类型,这里是virtual my_if。

    2.3 为验证平台加入其它组件

    2.3.1 加入transaction

    以上的操作都是基于信号级的,本节开始将引入reference model,monitor,scoreboard等组件,在这些组件间,信息的传递是基于transaction(事务)的。

    transaction是一个抽象的概念,一般来说,物理协议中的数据交换都是以帧或者包为单位的。通常在一帧或一个包中要定义好各参数,每个包的大小不一样。很少会有协议以bit或byte为单位进行数据交换,以以太网为例,每个包的大小至少是64byte,包中包括源地址,目的地址,包的类型,整个包的CRC校验数据等。transaction就是用来模拟这种实际情况,一个transaction就是一个包。不同的验证平台会有不同的transaction。

    //my_transaction.sv
    
    class my_transaction extends uvm_sequence_item;
    	rand bit [47:0] dmac;			//destination address
    	rand bit [47:0] smac;			//source address
    	rand bit [15:0] ether_type;		//internet type
    	rand byte 		pload[];		//load package size
    	rand bit [31:0] crc;			//crc check code
    	
    	constraint pload_cons{
    		pload.size >= 46;
    		pload.size <= 1500;
    	}
    	
    	function bit[31:0] calc_crc();
    		return 32'h0;
    	endfuntion
    	
    	function void post_randomize();		//callback function of randomize
    		crc = calc_crc;
    	endfunction
    	
    	`uvm_object_utils(my_transaction)
    	
    	function new(string name="my_transaction");
    		super.new(name);
    	endfunction
    endclass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    有两点需要注意:

    • UVM中所有的transaction基类都为uvm_sequence_item只有从uvm_sequence_item派生的transaction才可以使用UVM中强大的sequence机制
    • 没有使用uvm_component_utils来进行factory注册,而是使用了uvm_object_utils。从根本上来说,my_transaction与my_driver是不同的。在整个仿真期间,my_driver是一直存在的,my_transaction不同,它有生命周期。他在仿真的某一时间产生,经过driver驱动,在经过Rfm处理,最终由Scb比较完成后,其生命周期就结束了。一般来说,这种类都是派生自uvm_object或者uvm_object的派生类。uvm_transaction的祖先是uvm_object,所以使用uvm_object_utils宏来进行宏注册。
    //my_driver.sv
    
    task my_driver::main_phase(uvm_phase phase);
    	my_transaction tr;
    	for(int i=0;i<2;i++)
    	begin
    		asset(tr.randomize() with {pload.size == 200;});
    		driver_one_pkg(tr);
    	end
    endtask
    
    task my_driver::drive_one_pkg(my_transaction tr);
    	bit [47:0] tmp_data;
    	bit [7:0]  data_q[$];
    	
    	tmp_data = tr.dmac;
    	//store tmp_data to data_q by 8-bit
    	for(int i=0;i<6;i++)
    	begin
    		data_q.push_back(tmp_data[7:0]);
    		tmp_data = (tmp_data >> 8);
    	end
    	...
    	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 pkg", UVM_LOW)
    	repeat(3) @(posedge vif.clk);
    
    	//drive simulation to DUT
    	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 driver one pkg", UVM_LOW)
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    2.3.2 加入environment

    uvm_env是一个容器类。在这个容器类中实例化driver,monitor,Ref和Scb等。在调用run_test时,传递的参数不再是my_driver,而是这个uvm_env(即让UVM自动创建uvm_env的实例)。

    //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);
    		//use factory to instance drv
    		drv = my_driver::type_id::create("drv", this):
    	endfunction
    	
    	`uvm_component_utils(my_env)
    endclass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 所有env应该派生自uvm_env,且与my_driver一样,容器类在仿真中也是一直存在的,使用uvm_component_utils宏来实现factory的注册。
    • 在my_env的build_phase中使用了factory的实例化方法type_name::type_id::create来实例化drv。只有使用factory注册过的类才能使用这种方法来实例化,而后文中讲的factory重载也必须通过create的方法来创建(使用new方法创建的实例化不能使用factory重载)。create实例化时传入两个参数,“drv”表示实例的名称,this表示该实例的父节点(类似new函数中的parent参数)。通过父节点的形式,UVM建立了属性的组织结构。在这个树形的组织结构中,由run_test创建的实例是数根(root,这里是uvm_env),并且树根的名字是固定的,为uvm_test_top。
    //top_tb.sv
    
    initial
    begin
    	run_test("my_env");
    end
    
    initial
    begin
    	uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv","vif", input_if);
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    由于加入了my_env,使得验证平台的层次结构发生了变化。所以在top_tb中使用config_db机制传递vif时要改变相应的路径为(uvm_test_top.drv,其中uvm_test_top为run_test自动创建的实例名,而drv为my_driver实例化时传入的实例名),run_test的参数也变成了my_env。

    在树根之后会有树叶,树叶的实现需要在my_env的build_phase中手动实现。无论树根还是树叶,都必须由uvm_component或者其派生类继承而来。

    加入my_env后的验证平台树如下:

    在这里插入图片描述

    2.3.3 加入monitor

    monitor用于监测DUT的行为。driver负责把transaction级别的数据转换成DUT的端口级别并驱动DUT。而monitor的行为与其相反,用于收集DUT的端口数据,并将其转换成transaction交给后续的组件,如Rfm,Scb等处理。

    //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 collection_one_pkt(my_transaction tr);
    endclass
    
    task my_monitor::main_phase(uvm_phase phase);
    	my_transaction tr;
    	while(1)
    	begin
    		tr=new("tr");
    		collection_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
            
    	for(int i=0;i<6;i++)
    	begin
    		tr.dmac = {tr.dmac[39:0], data_q.pop_front()};
    	end
            
    	...
            
    	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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    需注意以下几点:

    • 所有的monitor都应该派生自uvm_monitor。
    • 与my_driver相似,my_monitor中也需要有一个virtual my_if。
    • uvm_monitor在整个仿真中是一直存在的,所以它是一个component,使用uvm_component_utils注册。
    • monitor需要时刻收集数据,所以main_phase使用while(1)循环来实现的。
    //my_transaction.sv
    
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    当完成monitor类的定义后,可以在env中对其实例化

    //my_env.sv
    
    class my_env extends uvm_env;
    	my_driver drv;
    	my_monitor i_mon;
    	my_monitor o_mon;
    	
    	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);
    		drv = my_driver::type_id::create("drv", this);
    		i_mon = my_monitor::type_id::create("i_mon", this);
    		o_mon = my_monitor::type_id::create("o_mon", this);
    	endfunction
    	
    	`uvm_component_utils(my_env)
    endclass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这里实例化两个monitor,一个用于监测DUT的输入,一个用于监测DUT的输出。其实driver也可以将输入数据发送给refm,这里推荐使用输入monitor来检测

    在env实例化monitor后,要在top_tb中使用config_db将input_if和output_if传递给两个monitor:

    //top_tb.sv
    
    initial
    begin
    	uvm_config_db #(virtual my_if)::set(null, "uvm_test_top.drv", "vif", input_if);
    	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", onput_if);
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    加入monitor后的验证平台树如下:

    在这里插入图片描述

    2.3.4 加入uvm_agent

    agent:代理类,属于容器类(就是一个空盒子,本身没什么作用,用于装东西),将DUT的输入部分和输出部分进行封装,使验证平台的层次结构更加明显,便于重用。

    上一节中,monitor和driver的代码高度相似,其本质是因为两者处理的是同一协议(在同样一套既定的规则下做着不同的事情)由于二者代码相似,UVM通常将二者封装在一起,成为一个agent,因此不同的agent就代表了不同的协议。

    //my_agent.sv
    
    class my_agent extends uvm_agent;
    	my_driver drv;
    	my_monitor mon;
    	
    	function new(string name, uvm_component parnet);
    		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)
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    所有的agent都要派生自uvm_agent,其本身是一个component。

    其中build_phase中根据is_active来决定是否要例化driveris_active是uvm_agent的唯一成员变量。其是一个枚举类型,有UVM_ACTIVE和UVM_PASSIVE两种模式。UVM_ACTIVE就是实例化driver和sequencer(后文中会讲解),monitor。而UVM_PASSIVE则只会实例化monitor,即该agent只具有监测功能。

    在这里插入图片描述

    在env中需要实例化agent而不需要实例化driver和monitor。

    //my_env.sv
    
    class my_env extends uvm_env;
    	my_agent i_agt;
    	my_agent o_agt;
    	
    	virtual function void build_phase(uvm_phase phase);
    		super.build_phase(phase);
    		i_agt = my_agent::type_id::create("i_agt", this);
    		o_agt = my_agent::type_id::create("o_agt", this);
    		
    		//specified is_active value
    		i_agt.is_active = UVM_ACTIVE;
    		o_agt.is_active = UVM_PASSIVE;
    	endfunction
    endclass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在加入了agent组件后,验证平台的层级结构发生了改变,使用config_db时需修改变量的路径(uvm_test_top_.i_agt.drv),不再赘述。

    需要注意的是:

    • 只有uvm_component才可作为树的节点uvm_object不可以作为UVM树的节点(new函数无父节点)
    • 其次,uvm_driver和uvm_monitor实例化过程是在my_agent的build_phase中进行(根据层级结构,子节点的实例化在父节点的build_phase中进行,而不能跨层级实例化)的而不是在my_env的build_phase进行。所有的实例化要在build_phase中进行,如果在build_phase之后的phase进行实例化,会报错。

    加入agent后的验证平台树如下:

    在这里插入图片描述

    2.3.5 加入reference model

    Rfm用于完成和DUT相同的功能。Rfm的输出被Scb接收,用于和DUT的输出相比较。DUT如果很复杂,那么Rfm也会相当复杂。

    //my_model.sv
    
    class my_model extends uvm_component;
    	uvm_blocking_get_port #(my_transaction) port;
    	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
    
    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);
    	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);
    		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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    在my_model的main_phase中,只是单纯复制一份从i_agt得到的tr,并传递给后级的scb中。这里实现了两个my_transaction的复制函数。

    //my_transaction.sv
    
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    其中需要说明的是my_model如何从i_agt中得到my_transaction,并把my_transaction传递给my_scoreboard。在UVM中,通常使用TLM(Transaction Level Modeling)实现component之间transaction级别的通信。在UVM的transaction级别的通信中,数据发送有多种方式,一种是使用uvm_analysis_port。UVM中组件的事务及通信通信一种是通过TLM进行的,如果要使用TLM通信,需要做三件事:

    • 在要通信的组件内声明端口,在其build_phase中实例化端口
    • 在通信的组件上一层组件的connect_phase进行端口连接
    • 调用端口的方法进行transaction传输

    声明uvm_analysis_port:

    //my_monitor.sv
    
    uvm_analysis_port #(my_transaction) ap;
    
    • 1
    • 2
    • 3

    uvm_analysis_port是一个参数化的类,其参数就是这个analysis_port需要传递的数据类型。声明ap后,需要在monitor的build_phase中实例化。

    //my_monitor.sv
    
    virtual function void build_phase(uvm_phase phase);
    	ap = new("ap", this);
    endfunction
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在main_phase中,当收集完了一个transaction后,需要将其通过uvm_analysis_port的一个内建函数write写入ap中。

    task my_monitor::main_phase(uvm_phase phase);
    	my_transaction tr;
    	while(1)
    	begin
    		tr = new("tr");
    		collect_one_pkt(tr);
    		ap.write(tr);
    	end
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    还需要将my_env中使用fifo将两个端口连接在一起。在my_env中定义一个fifo,并在build_phase中将其实例化。

    //my_env.sv
    
    uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
    	agt_mdl_fifo = new("agt_mdl_fifo", this);
    
    • 1
    • 2
    • 3
    • 4

    fifo的类型是uvm_tlm_analysis_fifo,本身是一个参数化的类,其参数是存储在其中的transaction类型。

    之后,在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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    与build_phase相似,connect_phase也是UVM内建的一个phase。他在build_phase执行之后马上执行。但是与build_phase不同的是,他的执行顺序不是从树根到树叶(从上到下),而是从树叶到树根(从下到上)。

    为什么要用fifo?因为analysis_port是非阻塞性质的,ap.write函数调用完成后马上返回,不会等待数据被接收。如果write被调用时,blocking_get_port正在忙于其他事情,而没有准备好接收新的数据时,此时被write函数写入的my_transaction就需要一个暂存的位置,这就是fifo。

    与my_monitor中的ap不同的是,不需要对my_agent中的ap进行实例化,只需要在my_agent的connect_phase的connect_phase中将monitor的值赋给它,换句话说,这就相当于是一个指向my_monitor的ap指针

    根据前面介绍的connect_phase的执行顺序,my_agent的connect_phase的执行顺序早于my_env的connect_phase的执行顺序,从而可以保证i_agt.ap.connect语句时,i_agt_ap不是一个空指针。

    加入Rfm后的验证平台树如下:

    在这里插入图片描述

    2.3.6 加入scoreboard

    加入Scb是最后一步。

    //my_scoreboard.sv
    
    class my_scoreboard extends uvm_scoreboard;
    	my_transaction expect_queue[$];
    	uvm_blocking_get_port #(my_transcation) exp_port;
    	uvm_blocking_get_port #(my_transcation) act_port;
    	`uvm_component_utils(my_scoreboard)
    	
    	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);
    	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);
    	my_transaction get_expect, get_actual, tmp_tran;
    	bit result;
    	super.main_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.my_compare(tmp_tran);
    				if(result)
    				begin
    					`uvm_info("my_scoreboard", "Compare SUCESSFULLY", UVM_LOW)
    				end
    				else
    				begin
    					`uvm_error("my_scoreboard", "Compare FAILD")
    					$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 Export Que is empty!")
    				$display("the unexpected pkt is");
    				get_actual.my_print();
    			end
    		end
    	join
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    my_scoreboard要比较的数据来源于refm和o_agt的monitor。前者通过exp_port获取,后者通过act_port获取。在main_phase中开辟两个线程,一个线程处理exp_port的数据,将接收的到数据缓存至队列。另一个处理act_port的数据。并使用my_compare函数进行数据比对。采用这种比较处理方式的前提是exp_prot要比act_port先收到数据由于DUT处理数据需要延时,而refm是基于高级语言,一般不需要延时,因此可以保证exp_port的数据在act_port的数据之前到来

    my_compare函数原型如下:

    //my_scoreboard.sv
    function bit my_compare(my_transaction tr);
    	bit result;
    	if(tr == null)
    		`uvm_fatal("my_transcation", "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
    	begin
    		for(int i=0;i<pload.size;i++)
    		begin
    			if(pload[i] != tr.pload[i])		result = 0;
    		end
    	end
    	return result;
    endfunction
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    加入Scb后的验证平台树如下:

    在这里插入图片描述

    2.3.7 加入field_automation机制

    引入my_monitor时,在my_transaction中加入了my_print函数;在引入refm时,加入了my_copy函数;引入scb时,加入了my_compare函数。上述三个函数虽然各不相同,但是对于不同的transaction来说,都是类似的,都需要逐字段对transaction进行某些操作

    UVM引入了field_automation机制,使用uvm_field系列宏实现:

    //my_transaction.sv
    
    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[];		
    	rand bit [31:0] crc;
        
        `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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    以上使用uvm_object_utils_begin和uvm_object_utils_end来实现my_transaction的注册。在这两个宏中间使用uvm_field宏注册所有的字段uvm_field系列宏随着transaction的成员不同而不同。当使用uvm_field注册成员变量后,可以直接调用copy,compare,print等函数,无需自己实现。极大的简化了验证平台的搭建,提高了效率引入field_automation机制的另一个好处是简化了driver和monitor(减少了重复性代码的使用)

    //my_driver.sv
    
    task my_driver::drive_one_pkt(my_transaction tr);
    	byte unsigned data_q[];
    	int data_size;
    	
    	data_size = tr.pack_types(data_q) / 8;
    	`uvm_info("my_driver", "begin to drive one pkt", UVM_LOW)
    	repeat(3) @(posedge vif.clk);
    	for(int i=0;i<data_size;i++)
    	begin
    		@(posedge vif.clk)
    		vif.valid <= 1'b1;
    		vif.data <= data_q[i];
    	end
    	@(posedge vif.clk);
    	vif.valid <= 1'b0;
    	`uvm_info("my_driver", "end drive one pkt", UVM_LOW)
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    pack_bytes将tr中的所有字段变成byte流放入data_q中。pack_bytes极大地减少代码量。在把所有的字段变成byte流放入data_q中时,字段按照uvm_field系列宏书写的顺序排列。

    //my_monitor.sv
    
    task my_monitor::collect_one_pkt(my_transaction tr);
    	byte unsigned data_q[$];
    	byte unsigned data_array[];
    	logic [7:0] data;
    	logic valid = 0;
    	int data_size;
    	
    	`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
    	data_size = data_q.size();
    	data_array = new[data_size];
    	for(int i=0;i<data_size;i++)
    	begin
    		data_array[i] = data_q[i];
    	end
    	tr.pload = new[data_size - 18];
    	data_size = tr.unpack_types(data_array) / 8;
    	`uvm_info("my_monitor", "end collect one pkt", UVM_LOW)
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    使用unpack_bytes函数将data_q中的byte流转换成tr中的各字段。unpack_bytes函数的输入参数必须是一个动态数组,所以需要先把收集到的,放在data_q中的数据复制到一个动态数组中。

    2.4 sequence机制(核心)

    之前的验证平台,激励都是在driver中产生的,但是一个规范化的UVM验证平台中,driver只负责驱动transaction,而不负责产生transactionsequence机制由两部分组成,一个是sequence,另一个是sequencer。

    2.4.1 加入sequencer
    sequencer:转发item类。在没有sequencer之前,激励的调度和加载全部由driver完成。模块化编程要求组件具有单一的功能,这样才能使平台具有最大的灵活度,所以将激励的调度这个功能从driver中剥离出来由sequencer担任,driver只负责加载机理。

    //my_sequencer.sv
    
    class my_sequencer extends uvm_sequencer #(my_transaction);
    	function new(string name, uvm_component parent);
    		super.new(name, parnet);
    	endfunction
    	`uvm_component_utils(my_sequencer)
    endclass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    sequencer的定义非常简单,继承自uvm_sequencer,并使用uvm_component_utils宏注册到factory中。uvm_sequencer是一个参数化的类,其参数是my_transaction,即此sequencer产生的transaction的类型。uvm_driver也是一个参数化的类,应该在定义driver时指明此driver要驱动的transaction类型。

    //my_driver.sv
    
    class my_driver extends uvm_driver #(my_tansaction);
    
    • 1
    • 2
    • 3

    这样定义的好处就是可以直接使用uvm_driver中的某些语定义的成员变量,如uvm_driver中(UVM源码)的成员变量req,他的类型就是传递给uvm_driver的参数,在这里也就是my_transaction。

    //my_agent.sv
    
    class my_agent extends uvm_agent;
    	my_driver drv;
    	my_monitor mon;
    	my_sequencer sqr;
    	
    	uvm_analysis_port #(my_transaction) ap;
    	...
    	
    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);
    		sqr = my_sequencer::type_id::create("sqr", this);
    	end
    	mon = my_monitor::type_id::create("mon", this);
    endfunction
        
    function void my_agent::connect_phase(uvm_phase phase);
    	super.connect_phase(phase);
    	ap = mon.ap;
    endfunction
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在加入sequencer后,整棵UVM树如下所示:

    在这里插入图片描述

    2.4.2 加入sequence

    sequence的位置比较特殊,sequence不属于验证平台的任何一部分,但是他与sequencer之间有着密切的联系只有在sequencer的帮助下,sequence的transaction才能最终转发给driver。sequence就像一个弹夹,里面的子弹是transaction,而sequencer是一把枪,三者相互关联

    除了联系外,sequence与sequencer还有显著的区别。sequencer是一个uvm_component,而sequence是一个uvm_object。**与transaction一样,sequence也有其生命周期,它的声明周期比my_transaction要更长一些,其内部的transaction全部发送完毕后,它的声明周期也就结束了。**sequence应该使用uvm_object_utils宏注册到factory中。

    //my_sequence.sv
    
    class my_sequence extends uvm_sequence #(my_transaction);
    	my_transaction m_trans;
    	function new(string name = "my_sequence");
    		super.new(name);
    	endfunction
    	
    	virtual task body();
    		repeat(10)
    		begin
    			`uvm_do(m_trans)
    		end
    		#1000;
    	endtask
    	`uvm_object_utils(my_sequence)
    	end
    endclass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    所有sequence都应该派生自uvm_sequence,并在定义时指定要产生的transaction类型(参数类)。每一个sequence都有一个body任务,当一个sequence启动之后,会自动执行body中的代码

    上述代码中uvm_do宏完成了三件事:

    • 创建一个my_transaction实例
    • 将其随机化
    • 最终将其送给sequencer

    如果不用uvm_do宏也可也可以使用start_item与finish_item的方式产生transaction

    以下讨论sequence,sequencer,driver交互的过程

    一个sequence在向sequencer发送transaction前,要先向sequencer发送一个请求,sequencer把这个请求放入一个仲裁队列中。sequencer需要做两件事:

    • 检测仲裁队列中是否有某个sequence发送transaction请求
    • 检测driver是否申请transaction

    根据是sequence,driver是否有transaction请求分为三种情况:

    • 如果仲裁队列有发送请求,但是driver没有申请transaction,那么sequence将会一直处于等待driver的状态。直到driver申请新的transction。此时sequencer同意sequence的发送请求,sequence在得到sequencer的批准后,产生一个transcation并交给sequencer,sequencer把这个transaction交给driver。
    • 如果仲裁队列中没有发送请求,但是driver向sequencer申请新的transaction,那么sequencer将会处于等待sequence状态,一直到有sequence递交发送请求,sequencer马上同意这个请求,sequence产生transaction并交给sequencer,最终driver获得这个transaction。
    • 如果仲裁队列中有发送请求,同时driver也在向sequencer申请新的transaction,那么将会同意发送请求,sequence产生transaction并交给sequencer,最终由driver获得这个transaction。

    driver是通过uvm_driver中的成员变量seq_item_port,而在uvm_sequencer中有成员变量seq_item_export,这两者可以建立一个通道(TLM端口连接),通道中传递的transaction类型就是定义my_sequencer和my_driver时指定的transaction类型。这里并不需要显式指定通道类型,标准UVM库已经定义好了,只需要在my_agent中使用connect函数把两者联系在一起

    //my_agent.sv
    
    function void my_agent::connect_phase(uvm_phase);
    	super.connect_phase(phase);
    	if(is_active == UVM_ACTIVE)
    	begin
    		drv.seq_item_port.connect(sqr.seq_item_export);
    	end
    endfuntion
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    当把两者联系好后,就可以在driver中通过get_next_item任务向sequencer申请新的transacton

    //my_driver.sv
    
    task my_driver::main_phase(uvm_phase phase);
    	vif.data <= 8'b0;
    	vif.valid <= 1'b0;
    	while(!vif.rst_n)	@(posedge vif.clk);
    	while(1)
    	begin
    		seq_item_port.get_next_item(req);
    		drive_one_pkt(req);
    		seq_item_port.item_done();
    	end
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    get_next_item任务用来得到一个新的req,并驱动它,驱动完成后调用item_done通知sequencer。当出现sequencer发送出去了transaction,而driver并没有得到的情况下时,sequencer会保留这份transaction。如果在driver下次调用get_next_item时,item_done被调用,那么sequencer就认为driver已经得到了这个transaction,并把这个transaction删除,item_done可以理解为增加可靠性的一种握手机制

    uvm_do宏产生了一个transaction并交给sequencer,driver取走这个transaction后,uvm_do宏并不会立刻返回执行下一次的uvm_do宏,而是等待在那里,直到driver返回item_done信号,此时,uvm_do宏才算是执行完毕,返回后并开始执行下一个uvm_do宏,产生新的transaction。

    sequence如何向sequencer中发送transaction?只需要在某个component(如my_sequencer,my_env)的main_phase中启动这个sequence即可。

    //my_env.sv
    
    task my_env::main_phase(uvm_phase phase);
    	my_sequence seq;
    	phase.raise_objection(this);
    	seq = my_sequence::type_id::create("seq");
    	seq.start(i_agt.sqr);
    	phase.drop_objection(this);
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    首先创建一个my_sequencer的实例,之后调用start任务start任务的参数是一个sequencer指针,如果不知名指针,则sequence不知道将产生的transaction交个哪个sequencer。

    在UVM中,objection一般伴随着sequence,通常只在sequence出现的地方才提起和撤销objection,当sequence发送完毕transacton就可以结束仿真了。可以在sequencer中启动sequence:

    //my_sequencer.sv
    
    task my_sequencer::main_phase(uvm_phase phase);
    	my_sequence seq;
    	phase.raise_objection(this);
    	seq = my_sequence::type_id::create("seq");
    	seq.start(this);
    	phase.drop_objection(this);
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    除了get_next_item中,还可以使用try_next_item。get_next_item是阻塞的,它会一直等到有新的transaction才会返回。try_next_item则是非阻塞的,它尝试着询问sequencer是否有新的transaction,如果有则得到此transaction,否则直接返回。以下为使用非阻塞的try_next_item的my_driver:

    //my_driver.sv
    
    task my_driver::main_phase(uvm_phase phase);
    	vif.data <= 8'b0;
    	vif.valid <= 1'b0;
    	while(!vif.rst_n)	@(posedge vif.clk);
    	while(1)
    	begin
    		seq_item_port.try_next_item(req);
    		if(req == null)	@(posedge vif.clk)
    		else
    		begin
    			driver_one_pkt(req);
    			seq_item_port.item_done();
    		end
    	end
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    相比于get_next_item,try_next_item的行为更加接近真实driver的行为,当有数据时,就驱动数据,否则总线将一直处于空闲状态。

    验证平台在加入sequence之后就变成一个完整的验证平台了,如下图:

    在这里插入图片描述

    2.4.3 加入default sequence

    上节中使用手动启动sequence,在实际应用中,多使用default_sequence的方式启动sequence

    //my_env.sv
    
    virtual function void build_phase(uvm_phase phase);
    	super.build_phase(phase);
    	uvm_config_db #(uvm_object_wrapper)::set(this,
    											 "i_agt.sqr.main_phase",
    											 "default_sequence",
    											 my_sequence::type_id::get());
    endfunction
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    uvm_config_db的第一个第二参数共同决定了配置参数“default_sequence”的路径。第二个参数中出现了main_phase,这是UVM在设置default_sequence时的要求,sequence便在该组件的main_phase启动sequence。

    除了在my_env的build_phase中设置default_sequence外,还可以在其他地方设置,比如top_tb中。

    //top_tb.sv
    module top_tb;
    	...
    	initial
    	begin
    		uvm_config_db#(uvm_object_wrapper)::set(null,
    												"uvm_test_top.i_agt.sqr.main_phase",
    												"default_sequence",
    												my_sequence::type_id::get());
    	end
    endmodule
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    config_db通常是成对出现的,但是这里不需要在sequencer中使用config_db::get的方式来获取default_sequence,因为UVM已经做好了这些

    使用default_sequence启动sequence的方式取代了上一节中的sequencer的main_phase手工启动sequence的相关语句。

    uvm_sequence这个基类中,有一个变量名为starting_phase,他的类型是uvm_phase,sequencer在启动default_sequence时,会自动做如下操作:

    task my_sequencer::main_phase(uvm_phase phase);
    	...
    	seq.starting_phase = phase;
    	seq.start(this);
    endtask
    
    • 1
    • 2
    • 3
    • 4
    • 5

    因此可以在sequence中使用starting_phase进行提起和撤销objection

    //my_sequence.sv
    
    class my_sequence extends uvm_sequence #(my_transaction);
    	my_transaction m_trans;
    	virtual task body();
    		if(starting_phase != null)	starting_phase.raise_objection(this);
    		repeat(10)	`uvm_do(m_trans)
    		#1000;
    		if(starting_phase != null)	starting_phase.drop_objection(this);
    		`uvm_object_utils(my_sequence)
    	endtask
    endclass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    从而,objection完全与sequence关联在一起,在其他任何地方都不需要再设置objection。

    2.5 构建测试用例

    2.5.1 加入base_test

    在一个实际的UVM验证平台中,uvm_env并不是树根。通常来说,树根是一个基于uvm_test派生的类。

    //base_test.sv
    
    class base_test extends uvm_test;
    	my_env env;
    	function new(string name="base_test", uvm_component parent = null);
    		super.new(name, parent);
    	endfunction
    	extern virtual function void build_phase(uvm_phase phase);
    	extern virtual function void report_phase(uvm_phase phase);
    	`uvm_component_utils(base_test)
    endclass
    
    function void base_test::build_phase(uvm_phase phase);
    	super.build_phase(phase);
    	env = my_env::type_id::create("env", this);
    	uvm_config_db #(uvm_object_wrapper)::set(this,
    											 "env.i_agt.sqr.main_phase",
    											 "default_sequencer",
    											 my_sequence::type_id::get());
    endfunction
    
    function void base_test::report_phase(uvm_phase phase);
    	uvm_report_server server;
    	int err_num;
    	super.report_phase(phase);
    
    	server = get_report_server();
    	err_num = server.get_serverity_count(UVM_ERROR);
    
    	if(err_num != 0)		$display("TEST CASE FAILED");
    	else					$display("TEST CASE PASSED");
    endfunction
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    base_test派生自uvm_test,使用uvm_component_utils宏注册到factory中。在build_phase中实例化my_env,并设置sequencer的default_sequence。

    除了以上操作,通常还在base_test中做如下事情:

    • 设置整个验证平台的超时退出时间
    • 通过config_db设置验证平台的某些参数的值

    在加入uvm_test后,整棵UVM树如下所示:
    在这里插入图片描述

    2.5.2 启动测试用例

    要测试一个DUT是否按照预期工作,需要对其施加不同的激励,这些激励被称为测试向量或者pattern一种激励作为一个测试用例,不同的激励就是不同的测试用例。测试用例的数量是衡量验证人员工作成果的最直接目标

    随着验证的进行,测试用例的数量一直在增加,在增加的过程中,很重要的一点是保证后加的测试用例不影响已经建好的测试用例。上文是通过default_sequence的形式启动my_sequence,如何在不影响my_sequence的前提下将其启动呢?最理想的办法是在命令行中指定参数来启动不同的测试用例

    class case0_sequence extends uvm_sequence #(my_transaction);
    	my_transaction m_trans;
    	...
    	virtual task body();
    		if(starting_phase != null) starting_phase.raise_objection(this);
    	repeat (10)
        begin
    	`uvm_do(m_trans)
    	end
        #100;
    	if(starting_phase != null)  starting_phase.drop_objection(this);
    	endtask
        ...
    endclass
            
    //my_case0.sv
            
    class my_case0 extends base_test;
    	function new(string name = "my_case0", uvm_component parent = null);
    		super.new(name,parent);
    	endfunction
            
        extern virtual function void build_phase(uvm_phase phase);
    	`uvm_component_utils(my_case0)
    endclass
    
    function void my_case0::build_phase(uvm_phase phase);
    	super.build_phase(phase);
    	uvm_config_db#(uvm_object_wrapper)::set(this,
    											"env.i_agt.sqr.main_phase",
    											"default_sequence",
    											case0_sequence::type_id::get());
    endfunction
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    //my_case1.sv
        
    class case1_sequence extends uvm_sequence #(my_transaction);
    	my_transaction m_trans;
    	...
    	virtual task body();
    		if(starting_phase != null)		starting_phase.raise_objection(this);
    		repeat (10)		`uvm_do_with(m_trans, { m_trans.pload.size() == 60;})
            #100;
    	if(starting_phase != null)		starting_phase.drop_objection(this);
    	endtask
        ...
    endclass
            
    class my_case1 extends base_test;
    	function new(string name = "my_case1", uvm_component parent = null);
    		super.new(name,parent);
    	endfunction
    	extern virtual function void build_phase(uvm_phase phase);
    	`uvm_component_utils(my_case1)
    endclass
            
    function void my_case1::build_phase(uvm_phase phase);
    	super.build_phase(phase);
    	uvm_config_db#(uvm_object_wrapper)::set(this,
    											"env.i_agt.sqr.main_phase",
    											"default_sequence",
    											case1_sequence::type_id::get());
    endfunction
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    如果需要启动my_case0,需要在top_tb中更改run_test的参数

    initial
    begin
    	run_test(“my_case0”);
    end
    
    • 1
    • 2
    • 3
    • 4

    如果需要启动my_case1,也需要更改run_test参数

    initial
    begin
    	run_test("my_case1");
    end
    
    • 1
    • 2
    • 3
    • 4

    当my_case0运行时需要修改代码,重新编译后才能运行。当my_case1运行时也许如此。这样相当不方便,为了解决这个问题,UVM提供了对不加参数的run_test的支持

    //top_tb.sv
    initial
    begin
    	run_test();
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在run_test不加参数时,UVM会利用UVM_TEST_NAME从命令行中寻找测试用例的名字,创建它的实例并运行

    <sim command> 			//run my_case0
    +UVM_TEST_NAME=my_case0
    
    <sim command> 			//run my_case1
    +UVM_TEST_NAME=my_case1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    整个验证平台的启动和执行的流程如下图:

    在这里插入图片描述

    • 首先执行top_tb模块,运行其中的initial块(系统时钟,复位,赋值,run_test等)
    • 其次运行全局的无参数的run_test任务(top_tb中的run_test的initial块),此时启动整个验证平台
    • 由于run_test()是无参数的,UVM通过命令行+UVM_TEST_NAME的来接收运行的实例名(在启动平台时通过命令行或者脚本指定UVM_TEST_NAME参数)
    • 依次执行UVM平台的各Phase
    • 所有phase执行完毕,结束仿真

    整棵UVM树如下所示(uvm_test_top为要运行的用例my_casen):
    在这里插入图片描述

    参考资料:

    UVM实战(卷I)张强 编著 机械工业出版社

  • 相关阅读:
    学员分享丨我与誉天:RHCA证书之旅
    ORACLE数据库查询所有索引的sql语句
    01背包-采药
    化繁为简,国内知名期货交易所依托 MogDB 数据库促信创改造项目提速
    栈和堆什么意思,Rust所有权机制又是什么
    Vue的进阶使用--模板语法应用拓展(表单及组件通信)
    Python使用SQLAlchemy操作sqlite
    【Win10】如何关闭Windows10自动更新
    怎样在LaTeX中方便输入带圆圈的数字
    【语音处理】基于自适应差分脉冲编码调制(ADPCM)的实现研究(Matlab代码实现)
  • 原文地址:https://blog.csdn.net/weixin_44590534/article/details/126901364