诙谐有趣的《UVM实战》笔记——第二章 一个简单的UVM验证平台

前言

某天白天在地铁上听鬼故事,结果晚上要睡觉时,故事里的情节都历历在目,给我鸡皮疙瘩起的~
不过我倒是没有吓得睡不着,而是转念一想,为啥我学知识忘得很快,随便听的鬼故事却记得这么清楚咧?
那如果能像听鬼故事那样去学知识,是不是可以记得更牢固呢?
经过一夜深思,我发现可以通过生活经验来学习专业知识,也就是通过形象的比喻来赋予知识活力。
而刚好最近想要重新看一下强哥《UVM实战》的第二章,所以决定试试看这种方法可不可行。
本周公司都没啥活,就花了四天大概把这章给看完了。
发现在看的过程中,通过打比方的方式确实可以加深对知识的理解,而把这些写下来的过程又可以促进自己继续往下看。总体来说感觉是挺不错的方法。

好了,正文开始。

只有driver的验证平台

最简单的验证平台

本章中,假设要验证的DUT如下:
其功能很简单,就是通过rxd接收数据,再通过txd发送出去。rx_dv是接收数据的有效指示,tx_en是发送数据有效指示。

module dut(clk,
           rst_n, 
           rxd,
           rx_dv,
           txd,
           tx_en);
input clk;
input rst_n;
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
   if(!rst_n) begin
      txd <= 8'b0;
      tx_en <= 1'b0;
   end
   else begin
      txd <= rxd;
      tx_en <= rx_dv;
   end
end
endmodule

driver的实现如下:

`ifndef MY_DRIVER__SV
`define 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;
   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
`endif

对my_driver实例化并且最终搭建的验证平台top_tb如下:

`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
end

initial begin
   rst_n = 1'b0;
   #1000;
   rst_n = 1'b1;
end

endmodule

加入factory机制

引入factory机制,主要也就是通过uvm_component_utils。其功能很丰富,其中一个就是将my_driver登记在表里面。
我们可以把uvm_component_utils形象记忆成工厂里负责登记小组(组件)的记录员

引入后,我们就不用显式地去创建driver组件、调用main_phase以及finish仿真了,用run_test来控制即可。

引入factory机制前:

initial begin
   my_driver drv;
   drv = new("drv", null);
   drv.main_phase(null);
   $finish();
end

引入factory机制后:

initial begin
   run_test("my_driver");
end

当一个组件被工厂小组记录员(uvm_component_utils)注册在案后,会有以下的福利:

  • 根据类名创建类的实例也就是你不需要自己辛苦地去声明、创建你的小组了。只需要直接告知你小组的名字(my_driver),人家直接通过内部流程(run_test)把小组给你建好。
  • 自动调用main_phase(实现一个driver等于实现其main_phase)。也就是你只要交代好driver需要做的项目(也就是你的代码),工厂就会直接帮你启动(调用),而不用你自己去喊“开始”。

加入objection机制

如果你想问,好端端的,为啥要加入objection机制?那我只能说,为了维护小组成员自由上班的权利!
啥玩意?
别急,让我先介绍一下接下来要出场的角色。

  • UVM:这位大家都认识啦,UVM帝国的大boss。
  • $finish:负责公司开关电的保安。这位可厉害了,关系到你能不能愉快地在公司里打代码。

背景介绍:

  • 大家都知道,疫情严重期间,要到公司的小组上班都需要向上级申请。如果没有申请,为了勒紧腰带度过苦日子,UVM大boss就会安排$finish保安去断掉该小组的电源,大家的代码也就不用跑了。
  • 所以,如果你今天想要去公司上班,那么你就要对关电提出异议(raise_objection)。这样你就能够愉快地去公司上班了。等到你的代码跑完了,下班了,你就不会反对断电了,这时就要drop_objection。$finish保安就会争分夺秒跑过来,把这个小组的电源给断了。

那如果你说,我忘记申请了,但是还想去一下公司,那咋办?这里要分两种情况:

  • 如果你只是到公司拿个东西就走,不耗时间,那么可以正常进行。
  • 如果你要到公司打代码或者其他比较耗时间的事情,那么不好意思,没有提前申请就不行,保安还是会强制断电

翻译成UVM的话,就是,raise_objection语句必须在main_phase中第一个消耗仿真时间的语句之前
耗时间的语句包括:

  • wait 语句
  • 延时语句,比如#100ps
  • @语句
task my_driver::main_phase(uvm_phase phase);
	@(posedge top_tb.clk);	
	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

一般不用在build_phase等function phase里进行objection的操作。 phase的引入是为了解决何时结束仿真的问题, 它更多面向main_phase等task phase, 而不是面向function phase。

加入virtual interface

上一节的代码里出现了绝对路径的写法,如下所示。绝对路径不利于tb的可移植性,比如这里clk的路径改成了top_tb.clk_inst.clk,就需要对driver中的代码进行大量修改,因此需要尽量杜绝绝对路径的使用。

@(posedge top_tb.clk);
top_tb.rx_dv <= 1'b0;

避免绝对路径的使用,有以下两种思路:

1、宏定义

  • 好处:当路径改变时,直接修改TOP的定义即可。
  • 局限性:如果clk的路径为top_tb.clk_inst.clk,而rst_n的路径为rop_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

2、使用interface
SV中,我们用interface来连接DUT和TB,它长这个样子:

interface my_if(input clk, input rst_n);
   logic [7:0] data;
   logic valid;
endinterface

使用起来也很简单。
在top_tb中使用时,先定义,后直接使用。如下所示:

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

NONONO,这样会报错滴。
SV语法规定,interface是不能定义在class里,只能定义在module里(比如top_tb)。那咋办咧?

这时,virtual interface长叹一口气,终于轮到我出场了!!

虽然在SV中,class里不能有interface,但却可以有virtual interface!(interface哭了,怎么假的就行?)

class my_driver extends uvm_driver;
   virtual my_if vif;
…
endclass

如上所示,在interface的定义前加一个virtual。

关于 virtual interface这里可以多说一些,加深理解。

1、定义一个interface,且实例化多个后,如果没有定义virtual,则在任何一个实例中修改了某个信号值,在其他实例中都会受到影响。
2、**如果定义了virtual,则每个实例独立。**如果该interface只有一个实例,可用可不用virtual,有多个实例,需要virtual。更好的办法是,都加上virtual。virtual interface只是声明一个handle, 就好像一个指针一样, 可以在程序进行中进行construct。
3、所以class里必须是virtual interface.

声明了vif后,就可在main_phase里使用如下方式驱动信号了:

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对应起来咧?

啥,直接赋值?

NONONO。

引用top_tb中的东西时,你可以通过top_tb.my_dut.xxx来引用my_dut里的变量,但是你没法用同样的办法去直接引用my_driver中的变量,也就是不能写成top_tb.my_driver.xxx。因为这个my_driver是UVM通过run_test语句例化出来的脱离了top_tb层次结构的实例,是一个新的层次结构。(后面会知道,例化后的结构层次是类的层次,其顶层是uvm_test_top,而不是这里的top_tb)

所以现在的问题就是一个脱离了top_tb层次的组件,想要在top_tb中进行某些操作。咋办咧?

这时,UVM引入了config_db机制。相当于设立了一个配置中转站来对接这两者。

在top_tb中,可以把要传到my_driver中的input_if通过set操作,先放到这个中转站里:

initial begin
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
end

随后,在my_driver中通过get操作去拿这个input_if:

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

set和get函数的第一个和第二个参数联合起来组成路径,第三个参数相当于编号,要完全一样才行;而set函数的第四个参数表示要将哪个interface(这里是input_if)通过中转站config_db传递给my_driver,get函数的第四个参数表示要将得到的interface传递给my_driver的哪个成员变量(此处是传给vif成员变量)。

关于set和get函数的具体内容可以等之后再了解。

值得注意的是,uvm_config_db#(virtual my_if)是一个参数化的类,而virtual my_if就是要传递的类型。

就是说,如果你要给my_driver中的var变量传递一个int类型的数据,那么set和get就应该这么写:

initial begin
	uvm_config_db#(int)::set(null, "uvm_test_top","var",100);
end
class my_deiver 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#(virtual my_if)::get(this, "", "var", var))
			`uvm_fatal("my_driver", "var must be set!!!")
	endfunction
endclass

对了,这个中转站挺大的,可以同时往里面放不同的东西。你可以放不同类型的东西,比如上面的vif和var,也可以放同样的东西,比如两个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

为验证平台加入各个组件

加入transaction

最近刚好双十一,想问一下大家是不是拿快递拿到手软?有没有想过要是快递可以打包到一起,用胶带封好,让我们一次性拿一个包裹就好了?

同样的,验证平台也有这个考虑。之前的很多操作都是一个信号一个信号地发送和接收,效率可太低了。

所以平台对此进行了优化,把若干个信号(比如源地址、目的地址、包的类型、包的CRC校验数据等)打包到一起,以包为单位来进行数据的交换。这个包的概念就是transaction。
以以太网为例,其中dmac是48bit的以太网目的地址, smac是48bit的以太网源地址, ether_type是以太网类型, pload是其携带数据的大小, 通 过pload_cons约束可以看到, 其大小被限制在46~1500byte, CRC是前面所有数据的校验值。如下所示:

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;

   constraint pload_cons{
      pload.size >= 46;
      pload.size <= 1500;
   }

   function bit[31:0] calc_crc();
      return 32'h0;
   endfunction

   function void post_randomize();
      crc = calc_crc;
   endfunction

   `uvm_object_utils(my_transaction)

   function new(string name = "my_transaction");
      super.new();
   endfunction
endclass
`endif

定义transaction,有两点要注意:

  • 自己定义的所有transaction类要派生于uvm_sequence_item。为啥?**相当于认门派拜师呗。**你只有认了门派,才有资格学门派的武功,用门派的武器。这里uvm_sequence_item门派的武功就是强大的sequence机制。
  • 定义完transaction类之后,也要用uvm_object_utils注册一下。你是不是这个门派的,可不是你说了算的,你要去注册到名单里。门派里负责登记是`uvm_object_utils。

引入了transaction之后,现在发送信号就可以一批一批地发啦!

task my_driver::main_phase(uvm_phase phase);
   my_transaction tr;
   phase.raise_objection(this);
   vif.data <= 8'b0;
   vif.valid <= 1'b0;
   while(!vif.rst_n)
      @(posedge vif.clk);
   for(int i = 0; i < 2; i++) begin 
      tr = new("tr");
      assert(tr.randomize() with {pload.size == 200;});
      drive_one_pkt(tr);
   end
   repeat(5) @(posedge vif.clk);
   phase.drop_objection(this);
endtask

task my_driver::drive_one_pkt(my_transaction tr);
   bit [47:0] tmp_data;
   bit [7:0] data_q[$]; 
  
   //push dmac to data_q
   tmp_data = tr.dmac;
   for(int i = 0; i < 6; i++) begin
      data_q.push_back(tmp_data[7:0]);
      tmp_data = (tmp_data >> 8);
   end
   //push smac to data_q
   tmp_data = tr.smac;
   for(int i = 0; i < 6; i++) begin
      data_q.push_back(tmp_data[7:0]);
      tmp_data = (tmp_data >> 8);
   end
   //push ether_type to data_q
   tmp_data = tr.ether_type;
   for(int i = 0; i < 2; i++) begin
      data_q.push_back(tmp_data[7:0]);
      tmp_data = (tmp_data >> 8);
   end
   //push payload to data_q
   for(int i = 0; i < tr.pload.size; i++) begin
      data_q.push_back(tr.pload[i]);
   end
   //push crc to data_q
   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 pkt", UVM_LOW);
   repeat(3) @(posedge vif.clk);

   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);
endtask

稍微解释一下上面的代码:

  • 在main_phase中,对tr进行随机化,然后通过drive_one_pkt任务将tr驱动到DUT端口上
  • 在drive_one_pkt中,将tr的很多数据压入队列data_q的过程,其实就是在把数据打包成byte流。

加入env

考虑到以后要加入工厂的小组(比如reference model、 scoreboard等)会渐渐多起来。所以UVM大boss决定分区操作,方便管理。
也就是引入一个容器类uvm_env,在这里面可以去实例化不同的组件。
要注意了,现在调用run_test,传递的参数可不是my_driver咯,而是这个uvm_env。

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(phase);
      drv = my_driver::type_id::create("drv", this); 
   endfunction

   `uvm_component_utils(my_env)
endclass

同样的,所有的env也要派生于uvm_env,并且使用uvm_component_utils宏来注册。
有同学看到代码中的type_id::create,觉得有些奇怪,这是啥东东啊?
其实这是工厂机制独特的实例化方式。
你可以理解为工厂在创建小组的时候,是有区别对待的。

  • 如果你不是会员,那么你这个小组就是通过new()的方式来创建,那么你就无法享受工厂的高级功能
  • 如果你是会员,那么你这个小组就会通过type_id::create的方式来创建,你就能享受工厂的高级功能。

至于这个高级功能是啥,想必大家也听说了,就是override功能,也就是覆盖,或者重载功能
简单介绍一下这个号称为工厂机制最伟大的地方——override功能。
override功能可以让我们方便地替代掉验证环境中的实例或者注册的类。
怎么样,这个介绍够简单吧!别急,详细的介绍之后再聊嘛~

我们原本只有小组component,现在又多了分区env的概念,接下来肯定就是要建立树形的组织架构,来表明这个组属于哪个区。

怎么分咧?

在小组创建的时候就给分好。

之前drv在实例化时,传递了两个参数。drv是实例名,而this,这里指的是my_env,就是parent,表示这个drv小组是归属于env片区的。

      drv = my_driver::type_id::create("drv", this); 

因此现在的组织架构长这样:
在这里插入图片描述
注意,dry也就是drv的别名。因为大家觉得他现在的功能很简单,干巴巴的,所以取名为dry。大家今后看到了dry可别以为是强哥打错了啊

其中,括号外是实例名,括号内是类名。
在这个树形结构中,由run_test创建的实例的名字是固定的,叫uvm_test_top。我们可以认为uvm_test_top=CEO。以前工厂比较小,CEO就是my_driver。现在厂子变大了,目前的CEO由my_env片区长来代理,而之前的CEO my_driver已经变成了一个小组。

因为有env的加入,验证平台的层次结构变了,所以今后在和中转站沟通时也要改变一下相应的说法,也就是set函数的第二个参数从uvm_test_top变为了uvm_test_top.drv;其中uvm_test_top是CEO的名字, 而drv则是在my_env的build_phase中实例化时传递过去的名字。

另外,run_test的参数也从my_driver变为了my_env,也就是说现在run_test可以支持以区域为单位的自动创建了~

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

加入monitor

验证平台需要去监测DUT的行为,用来实现这一功能的组件叫monitor。
之前提到的driver负责把transaction级别的数据转变成DUT的端口级别,并驱动给DUT,而monitor则是收集DUT的端口数据,并转换成transaction交给后续的组件,比如reference model、scoreboard等进行处理。
一个monitor的定义如下:

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 collect_one_pkt(my_transaction tr);
endclass

task my_monitor::main_phase(uvm_phase phase);
   my_transaction tr;
   while(1) begin
      tr = new("tr");
      collect_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
   //pop dmac
   for(int i = 0; i < 6; i++) begin
      tr.dmac = {tr.dmac[39:0], data_q.pop_front()};
   end
   //pop smac
   for(int i = 0; i < 6; i++) begin
      tr.smac = {tr.smac[39:0], data_q.pop_front()};
   end
   //pop ether_type
   for(int i = 0; i < 2; i++) begin
      tr.ether_type = {tr.ether_type[7:0], data_q.pop_front()};
   end

   psize = data_q.size() - 4;
   tr.pload = new[psize];
   //pop payload
   for(int i = 0; i < psize; i++) begin
      tr.pload[i] = data_q.pop_front();
   end
   //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类应该派生自uvm_monitor
  • 在my_monitor中也需要声明virtual my_if
  • uvm_monitor也需要使用uvm_component_utils宏注册
  • 由于monitor需要时刻收集数据,所以在main_phase中使用while(1)循环来实现这一目的。

加入monitor这个组件后,也需要在分区env中对其进行实例化:

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(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

这里,在env中实例化了两个monitor,分别用来监测DUT的输入和输出。

例化monitor后,要在top_tb中将input_if和output_if两个virtual interface传递给两个monitor:

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",output_if);
end

目前的树形架构图更新如下:
在这里插入图片描述

封装成agent

因为monitor和driver处理的是同一种协议,两者的代码高度相似,所以UVM将两者封装在一起,成为一个agent。换句话说,不同的agent就代表了不同的协议
如果把协议当做为语言的话,会好理解一些。如果我要和俄罗斯做生意,那么就成立一个俄罗斯agent,里面的小组monitor和driver也都精通俄罗斯语。而朝鲜agent,对接朝鲜业务。

class my_agent extends uvm_agent ;
   my_driver     drv;
   my_monitor    mon;
   
   function new(string name, uvm_component parent);
      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

和其他的组件一样,所有的agent都要派生自uvm_agent类, 且其本身是一个component, 应该使用uvm_component_utils宏来实现factory注册。

要注意,agent可以通过is_active变量进行灵活的配置。如果is_active = UVM_ACTIVE,就创建driver实例,如果是UVM_PASSIVE,就不创建driver,也就是这个agent里只有一个monitor。
一般情况下,输出端口只需要接收数据,无需驱动数据,所以一般是UVM_PASSIVE模式。
在这里插入图片描述
把driver和monitor封装好后,在env中就直接例化agent就好了。driver和monitor的例化已经包含在agent里了。

class my_env extends uvm_env;

   my_agent  i_agt;
   my_agent  o_agt;
   
   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);
      i_agt = my_agent::type_id::create("i_agt", this);
      o_agt = my_agent::type_id::create("o_agt", this);
      i_agt.is_active = UVM_ACTIVE;
      o_agt.is_active = UVM_PASSIVE;
   endfunction

   `uvm_component_utils(my_env)
endclass

现在树状结构更新如下:
在这里插入图片描述
同样的,路径变了之后config_db在配置virtual my_if时也要更改路径:

initial begin
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.drv", "vif", input_if);
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.mon", "vif", input_if);
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_agt.mon", "vif", output_if);
end

小结一下:

  • 只有uvm_component才有资格作为树形层次的节点,uvm_object就不行
  • 我们在my_env的build_phase中, 创建i_agt和o_agt的实例;而在agent的build_phase中, 创建driver和monitor的实例
  • 所有的实例创建要在build_phase里完成,在之后的phase,比如main_phase里创建实例会报错。

加入reference model

**要知道DUT的功能是否正确,那验证平台就需要心里有杠秤。**在UVM里,这杠秤就是reference model。

reference model和DUT功能是一样的,所以其复杂程度和DUT相当。只是前者用高级语言,一般没有延时,而后者需要有延时 (reference model会比DUT更快得到结果)。reference model的输出被scoreboard接收, 用于和DUT的输出相比较。

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

在my_model的main_phase中, 只是单纯地复制一份从i_agt得到的tr, 并传递给后级的scoreboard中。

大家在上面的代码里是不是看到了奇怪的东西,比如uvm_blocking_get_port ?

其实这是UVM的通信管道。下面来介绍一下my_transaction数据的传递方式~

my_model会从i_agt得到my_transaction,并将其传递给scoreboard。而这个数据的传递是通过TLM管道来实现的。

UVM的transaction级别的通信中,数据的发送方式有多种,其中之一就是uvm_analysis_port。它有个绰号,叫大嘴巴,会把它知道的东西广播给通信管道另一端的多个组件,简称为一对多。(有些数据的发送方式只会把transaction传给管道另一端的一个组件而已)

在my_monitor的定义如下:

uvm_analysis_port #(my_transaction)  ap;

uvm_analysis_port 也是一个参数化的类,其中my_transaction就是它传递的类型。可以理解为,uvm_analysis_port 这个大嘴巴也不是啥都广播,只是广播特定类型(my_transaction)的内容。

声明后就需要例化了

   virtual function void build_phase(uvm_phase phase);
……
      ap = new("ap", this);
   endfunction

在main_phase中收集完一个transaction后,就需要将其写入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

这里的write是uvm_analysis_port的唯一操作,用于my_monitor去广播收集到的transaction。

除了uvm_analysis_port,TLM管道还有uvm_blocking_get_port的方式来进行数据传输。使用时,需要在my_model中定义了该端口,并在build_phase中进行实例化。之后在main_phase中通过port.get任务来得到从i_agt的monitor中通过write函数发出的transaction。

uvm_blocking_get_port #(my_transaction)  port;

在my_monitor和my_model中实现了端口之后,并不等于他们就可以通信了,还需要在两者之间设立一个缓存仓库fifo,并且架起通信的管道将他们连接起来。

先说一下这个fifo。

好端端的,干嘛要引进来一个fifo,不能直接把my_monitor中的analysis_port和my_model中的blocking_get_port相连吗?

诶,还真不行。我们前面说了,analysis_port是个大嘴巴。这个大嘴巴还是个有个性的大嘴巴,它只会跟你说一遍,说完就走了,不会管你在不在忙,有没有听到

所以你想下,如果你戴着耳机在打代码,大嘴哥突然过来跟你说了几句话,说完就走了。等你忙完后,你压根就不知道他刚刚说了啥!

那咋办?

简单,直接让它发个手机语音给你,等你忙完了再听就好了。而这里的手机就相当于是一个fifo,可以进行缓存。

正经点说就是,因为analysis_port是非阻塞的,ap.write调用完成后马上返回,并不会等待数据被接收。如果调用write函数时,blocking_get_port在忙,没有准备好接收新的数据,那么此时被write函数写入的my_transaction就需要一个暂存的位置,也就是fifo。

在my_env中定义一个fifo,并在build_phase中进行实例化:

uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
…
agt_mdl_fifo = new("agt_mdl_fifo", this);

上面提到的fifo的类型是uvm_tlm_analysis_fifo,而my_transaction就是存储在其中的transaction的类型。

架起通信管道这个步骤需要通过在build_phase之后引入的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

上面的连接中用到了i_agt的成员变量ap,其定义与my_monitor中的ap定义完全一样:

uvm_analysis_port #(my_transaction) ap;

不同的是,在my_monitor中的ap需要实例化,而my_agent中的ap就不用实例化。只需要在my_agent中的connect_phase将monitor的ap赋给my_agent的ap即可。因为agent中的数据也是从monitor中来的。

这里引入了connect_phase,它和build_phase有点不同。

**build_phase的执行是自上而下。**先执行my_env的build_phase,然后才是agent的build_phase,最后才是driver和monitor。

为啥是这样咧? UVM的设计哲学就是在build_phase中做实例化的工作, driver和monitor都是agent的成员变量, 所以它们的实例化都要在agent的build_phase中执行。 如果在agent的build_phase之前执行driver的build_phase, 此时driver还根本没有实例化, 所以调用driver.build_phase只会引发错误。

除了build_phase之外, 所有不耗费仿真时间的phase( 即function phase) 都是自下而上执行的,比如这里的connect_phase。 先执行driver和monitor的connect_phase, 再执行agent的connect_phase, 最后执行env的connect_phase。
**这又是啥讲究咧?**让agent的connect_phase的执行顺序早于env的connect_phase,可以保证代码执行到i_agt.ap.connect语句时i_agt.ap不是一个空指针。

小结一下:
本节引入了reference model,并且介绍了组件之间的TLM通信管道。目前的UVM树状结构如下:
在这里插入图片描述

加入scoreboard

之前说过,我们要判断DUT的输出是否正确,就需要将其和reference model的输出进行比较,而负责比较的场所,就是在scoreboard里。

在验证平台中加入了reference model和monitor之后, 最后一步是加入scoreboard。其代码如下:

class my_scoreboard extends uvm_scoreboard;
   my_transaction  expect_queue[$];
   uvm_blocking_get_port #(my_transaction)  exp_port;
   uvm_blocking_get_port #(my_transaction)  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(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 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 Queue is empty");
            $display("the unexpected pkt is");
            get_actual.my_print();
         end 
      end
   join
endtask

my_scoreboard要比较reference model中exp_port端口里的数据以及o_agt.monitor中act_port里的数据。
可以看到main_phase中的fork …join中有两个进程:

  • 一个是处理exp_port的数据,也就是拿到数据就放进expect_queue中
  • 另一个是处理从DUT输出到act_port的数据。收集到这些数据后,从expect_queue中弹出之前从exp_port收到的数据,然后调用my_transaction的my_compare函数。

这里要注意一下,采用这种处理方式的前提是,exp_port要比act_port先收到数据,否则expect_queue.size() > 0条件就不成立,就会报错。

不过这一点不用担心,DUT处理数据需要延时,而reference model是基于高级语言处理的,一般不需要延时,所以可以保证exp_port的数据在act_port之前到来。

目前验证平台的树状层次如下。
在这里插入图片描述

加入field_automation机制

加入field_automation机制,可以让我们直接调用UVM自带的函数,节省了很多的代码。
略。

UVM的终极大作:sequence

在验证平台中加入sequencer

在前面的例子中,激励都是从driver中产生的,但是这不规范。一个规范的UVM验证平台中,driver只负责驱动transaction,产生transaction的事情应该由别的组件来干。于是乎,我们在验证平台中加入sequencer。

UVM的sequence机制最大的作用就是将test case和验证平台testbench分离开来,也就是把激励的产生和驱动给分离,否则每次修改test case就相当于重写了一次,容易将之前对的地方改错。扩展性太差,容易产生错误。

sequence机制有两大组成部分, 一是sequence, 二是sequencer。
一个sequencer的定义如下:

class my_sequencer extends uvm_sequencer #(my_transaction);
   
   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction 
   
   `uvm_component_utils(my_sequencer)
endclass

也是定义,注册,创建三步曲,还要记得注明sequencer产生的参数类型。

我们一直强调要注明参数类型,这样是有好处的。以uvm_driver为例。

class my_driver extends uvm_driver#(my_transaction);

好处就是我们可以直接使用uvm_driver中预先定义好的成员变量。比如uvm_driver中有成员变量req,它的默认类型就是你在定义时传递给uvm_driver的参数,也就是上面提到的my_transaction。

task my_driver::main_phase(uvm_phase phase);
   phase.raise_objection(this);
   vif.data <= 8'b0;
   vif.valid <= 1'b0;
   while(!vif.rst_n)
      @(posedge vif.clk);
   for(int i = 0; i < 2; i++) begin 
      req = new("req");
      assert(req.randomize() with {pload.size == 200;});
      drive_one_pkt(req);
   end
   repeat(5) @(posedge vif.clk);
   phase.drop_objection(this);
endtask

这样我们就可以节省掉下面的声明语句。

my_transaction req;

目前看好像没有多大的区别,但感觉这样可以避免我们自己一不小心声明错误req的类型,引起不必要的麻烦。

注意:这里依然是在driver中产生激励, 下一节中将会把激励产生的功能从driver中移除。

好了,差不多到了UVM树状层次的更新时刻了。本小节加入的sequencer,将会出现在哪个位置咧?答案是,sequencer将会被编入到agent里。原因如下:

之前将driver和monitor编入agent里,是因为他们的代码很像,都是基于同一个协议;
而现在sequencer产生数据,driver把这个数据发送出去。他们的关系也非常密切,因此也把sequencer编入到agent里。

class my_agent extends uvm_agent ;
   my_sequencer  sqr;
   my_driver     drv;
   my_monitor    mon;
   
   uvm_analysis_port #(my_transaction)  ap;
   
   function new(string name, uvm_component parent);
      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
      sqr = my_sequencer::type_id::create("sqr", this);
      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);
   ap = mon.ap;
endfunction

目前的UVM树状层次更新如下:

在这里插入图片描述

sequence机制

上一节讲了sequence机制有组成部分之一sequencer,本节就来介绍更重要的部分——sequence。下面是带sequence的UVM验证平台。

下图中,sequence用箭头指向了sequencer,表明sequence产生transaction,然后交给sequencer。

只有在 sequencer的帮助下, sequence产生出的transaction才能最终送给driver; 同样, sequencer只有在sequence出现的情况下才能体现其价值, 如果没有sequence, sequencer就几乎没有任何作用。

强哥还用了一个形象的比喻:

sequence就像是一个弹夹, 里面的子弹是transaction, 而sequencer是一把枪。 弹夹只有放入枪中才有意义, 枪只有在放入弹夹后才能发挥威力。

我还想到了另外一个比喻。如果把sequencer当做一个生产部门,那么生产的货物就是transaction,而sequence就可以当做是辛勤劳动的员工。只有在部门的帮助下,员工生产出的货物才能最终打包装箱送上货车(driver);同样,部门只有在员工的辛勤劳动下才能体现出价值。

可能有人要问了,为啥这个sequence画在在了方框的外面,而且还用虚线咧?
在这里插入图片描述
因为sequence和sequencer有着本质的区别。
sequencer是一个uvm_component,是可以构成环境层次的组件,从仿真开始到结束一直都在。
而sequence是一个uvm_object,是有生命周期的。sequence里的transaction都发送完毕之后,它的生命周期也就结束了。

强哥说:

这就好比一个弹夹, 其里面的子弹用完后就没有任何意义了。

资本家说:

这就好比是一个员工,他出不了货了,那么这个员工就没有价值了……扎心了

再回过头来说一下sequence。

一个sequence应该使用uvm_object_utils宏注册到factory中,而且要派生自uvm_sequence, 并且在定义时指定要产生的transaction的类型。

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)
endclass

每个sequence都有一个body任务,transaction的产生也就是在这个body任务里产生的。当一个sequence启动之后, 会自动执行body中的代码。 在上面的例子中, 用到了一个全新的宏: uvm_do。 这个宏是UVM中最常用的宏之一, 它用于:
①创建一个my_transaction的实例m_trans;
②将其随机化;
③最终将其送给 sequencer。

这个也很好记:一个员工(sequence)能够生产出什么样的货物(transaction),看他这副身体(body)做(uvm_do)了啥就知道了。

当然也可以用start_item和finish_item的方式来替代uvm_do去产生transaction。不过初学者使用uvm_do即可。

强哥用下面这段话来解释sequence和sequencer的交互。

一个sequence在向sequencer发送transaction前, 要先向sequencer发送一个请求, sequencer把这个请求放在一个仲裁队列中。 作 为sequencer, 它需做两件事情: 第一, 检测仲裁队列里是否有某个sequence发送transaction的请求; 第二, 检测driver是否申请 transaction。
1) 如果仲裁队列里有发送请求, 但是driver没有申请transaction, 那么sequencer将会一直处于等待driver的状态, 直到driver申 请新的transaction。 此时, sequencer同意sequence的发送请求, sequence在得到sequencer的批准后, 产生出一个transaction并交给 sequencer, 后者把这个transaction交给driver。
2) 如果仲裁队列中没有发送请求, 但是driver向sequencer申请新的transaction, 那么sequencer将会处于等待sequence的状态, 一直到有sequence递交发送请求, sequencer马上同意这个请求, sequence产生transaction并交给sequencer, 最终driver获得这个transaction。
3) 如果仲裁队列中有发送请求, 同时driver也在向sequencer申请新的transaction, 那么将会同意发送请求, sequence产生transaction并交给sequencer, 最终driver获得这个transaction。

翻译一下,就是:
员工(sequence)要把先向部门(sequencer)发送货物(transaction)前,要提前发送请求,这个请求会被部门记录在申请名单(仲裁队列)中。随后,部门要统筹安排两件事情:
第一,申请名单里有没有员工请求发送货物;
第二,有没有空闲的货车(driver)来申请发货。
1) 如果申请名单里有发货请求,但是没有空闲的货车可以用,那么部门就会一直等待货车发完货回来。等货车回来了,那么部门就会同意员工的发送请求。而员工在得到批准后,就可以生产出一件货物并交给部门,部门安排货车去送货;
2)如果申请名单里没有发货请求,但是有空闲的货车想要出货,那么部门就会处于等待员工的状态。等员工递交发货请求后,部门会立马同意。随后员工产生货物并交给部门,部门马上装货发车。
3)如果申请名单里有发货请求,而刚好也有空闲的货车想要出货,那么部门肯定很开心地同意请求,员工生产货物交给部门,随后马上装货发车。一刻也不用等,美滋滋。

话说货车是怎么向部门申请出货的咧?难不成这货车是人工智能,成妖了会说话?那倒不是。肯定是通过货车司机来沟通啦。这个货车司机名字也很潮,叫波特(port)。而部门那边和port对接的员工英文名叫export,翻译过来就是出口,够直白了吧。他俩这名字一听,就知道是好基友啦。

driver是如何向sequencer申请transaction的咧?
uvm_driver有成员变量seq_item_port,uvm_sequencer有成员变量seq_item_export。这两者之间可以建立一个“通道”, 通道中传递的transaction类型就是定义my_sequencer和my_driver时指定的 transaction类型, 在这里是my_transaction, 当然了, 这里并不需要显式地指定“通道”的类型, UVM已经做好了。

有了对接的人后,你还得给他俩发一个BB机,方便他们进行联系。这个步骤就对应着connect_phase里的connect动作:

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);
   end
   ap = mon.ap;
endfunction

连接好后,货车司机port就能向部门里的export请求出货了,这个请求的动作就是get_next_item:
“喂,出口啊,我货车有空了,给我点货呗”
如果export那边把货安排了,那么货车司机就麻溜地去干活了;但是如果export那边说现在还没货可以出,那么port就一直都不挂电话,跟好基友煲电话粥,直到有货安排了,才会挂断电话,去送货(drive_one_pkt(req))。

也就是说get_next_item是一个阻塞的函数,只有拿到req了才会继续往下执行。

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

这里用了while(1) 循环,因为货车(driver)只负责发货(transaction),不负责产生,所以只要你给货,货车就发车。因此写成无限循环的形式。这和monitor、reference model、scoreboard类似。

当然,port司机并不是一直都这么想要货,有时候也想要偷懒,那么这时他就只是打电话尝试去要货(try_next_item(req)),发现现在并没有货物,那他就挂了电话,等之后再打电话,省点电话费。
使用try_next_item的driver的代码如下:

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
			drive_one_pkt(req);
			seq_item_port.item_done();
		end
	end
endtask

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

区分get_next_item和try_next_item是阻塞还是非阻塞也很简单,直接看名字就好。如果是get,目的很明确,就是要拿到,不拿到不走(阻塞);
如果是try,那就只是尝试而已,有就拿走,没有就走人(非阻塞)。

不管是通过get_next_item还是try_next_item的方式,拿到货之后,货车就会把货拉到目的地(drive_one_pkt(req))。到了之后波特port就会打电话跟部门那边说一下:货送到了(item_done)。

关于item_done,摘抄一段内容。

这里为什么会有一个 item_done呢? 当driver使用get_next_item得到一个transaction时, sequencer自己也保留一份刚刚发送出的transaction。 当出现 sequencer发出了transaction, 而driver并没有得到的情况时, sequencer会把保留的这份transaction再发送出去。 那么sequencer如何知 道driver是否已经成功得到transaction呢? 如果在下次调用get_next_item前, item_done被调用, 那么sequencer就认为driver已经得到了这个transaction, 将会把这个transaction删除。 换言之, 这其实是一种为了增加可靠性而使用的握手机制。

在sequence中,产生transaction是通过uvm_do宏,它会产生一个transaction并交给sequencer,最后让driver取走这个transaction。但是之后,uvm_do并不会立刻返回执行下一次的uvm_do,而是等到driver返回item_done信号后,uvm_do才算执行完毕,才会返回并开始执行下一个uvm_do,产生新的transaction。

我们可以这么理解。
因为这批货物的保质期比较短,且要考虑路上运输的时间,所以员工(sequence)不会把货物一次性生产出来,而只会等货车(driver)把上一批货(transaction)送完了之后,员工才会继续干活(uvm_do),生产新的货。

另外,我们之前只是提到了员工把货交给部门,但是没有具体提到怎么交。具体如下:

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

首先,员工需要声明一下自己的名字(实例化的名字,这里是seq);
然后,你要通过start这一动作来表明你是哪个生产部门(这里是i_agt.sqr)的。因为厂里会有不同货物的生产部门,如果员工没有说自己属于哪个生产部门,那么其生产的货物就不知道要交给哪个部门去调度。

之前强哥说过,sequence是弹夹, 当弹夹里面的子弹用光之后, 可以结束仿真了。所以objection一般会伴随着sequence。通常只在sequence出现的地方才会提起和撤销objection。

除了在env里启动sequence外,还可以在sequencer中启动(之后还可以在test层次中添加),唯一区别是seq.start的参数变为了this。

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

default_sequence的使用

上一节中,sequence是通过my_env中的main_phase种手动启动的。但实际应用中,最多的还是通过default_sequence的方式启动sequence,只需要在某个component,比如my_env的build_phase中设置下面代码即可:

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

这是除了在top_tb中通过config_db设置virtual interface后再一次用到config_db的功能。有以下这些区别:

  • 在top_tb中,第一个参数为null,第二个参数是相对于第一个参数的相对路径,为uvm_test_top.xxx。
  • 而在my_env中,第一个参数为this,表示my_env,而my_env在此处就是uvm_test_top的角色。top_tb是一个module而不是class,所以不能使用this指针。

my_env的config_db::set第二个路径参数中,还需要告知sequencer在哪个phase中启动sequence,所以路径还需要具体到phase(这里是main_phase)。而第三、四个参数以及uvm_config_db#( uvm_object_wrapper) 是语法规定,照做即可。

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

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

或者在其他的component里设置, 如my_agent里,只需要将set的第一、二个参数改为this和sqr.main_phase即可。

有同学要问了,之前设置virtual interface时,config_db有set就有get。那sequencer的get要怎么写呢?
答案是,不用写。

UVM已经把这部分事情做了,这些小问题就不用我们考虑啦~

有同学又要objection一般会伴随着sequence。那现在使用default_sequence了,要怎么提起和撤销objection咧?

在uvm_sequence里,有一个类型为uvm_phase的starting_phase。sequencer在启动default_sequence时,会自动做如下操作:

task my_sequencer::main_phase(uvm_phase phase);
...
	seq.starting_phase = phase;
	seq.start(this);
...
endtask

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

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();
      if(starting_phase != null) 
         starting_phase.raise_objection(this);
      repeat (10) begin
         `uvm_do(m_trans)
      end
      #1000;
      if(starting_phase != null) 
         starting_phase.drop_objection(this);
   endtask

   `uvm_object_utils(my_sequence)
endclass

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

其实写到这里的时候感觉还是有点懵逼。有点不知道发生了啥的感觉,尤其是对这个starting_phase。于是我在强哥的《UVM1.1应用指南及源代码分析》里搜了一下这个starting_phase。

然后,一切就豁然开朗了~

driver 之所以能够控制验证平台的关闭,是因为 driver 同时有激励产生的功能。现在激励产生的功能已经转移到了 sequence 中,那么相应的,控制验证平台退出的功能也应该转移到 sequence 中,即在 sequence 中 raise_objection 和 drop_objection。但是在 sequence 中进行 raise_objection 的一个问题是, raise_objection 是属于phase 的一个函数,而 phase 是属于 component 的一个概念,是 component 专属的东西,而 sequence 的本质是一个 object,是没有 phase 的。那么怎么办?
这个问题其实非常简单。我们可以在 uvm_sequence 中加一个指向 phase 的指针, 然后当sequencer在main_phase中启动default_sequence时,把sequencer的main_phase 中的 phase 赋值给 sequence 中这个指针。(seq.starting_phase = phase)这样在 sequence 中就可以进行 objection 操 作了。UVM 中就是这么做的。在 sequence 中,这个指向 phase 的指针的名字是starting_phase。因此,我们可以在 sequence 中这么做:

task body();
	if(starting_phase != null)
		starting_phase.raise_objection(this);if(starting_phase != null)
		starting_phase.drop_objection(this);
endtask

建造测试用例

加入base_test

我们来回顾一下uvm_test_top(CEO)的变迁。最开始是my_driver当的,后面引进来和my_driver平级的其他组件,所以CEO变成了my_env。

不过从这节开始,CEO又要换成更高级别的组件来当了——也就是uvm_test。

本节先讲base_test,所有测试用例都派生于此。

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_sequence",
                                            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_severity_count(UVM_ERROR);

   if (err_num != 0) begin
      $display("TEST CASE FAILED");
   end
   else begin
      $display("TEST CASE PASSED");
   end
endfunction

uvm_test和其他的组件一样,都要通过uvm_component_utils注册到工厂里。从前的uvm_test_top,my_env,现在变成了base_test的一个组件,需要在build_phase中实例化,并且还要设置sequencer的default_sequence。这里设置完后,就不用在别的地方设置了。

除了实例化env,base_test做的事情因公司而异。不过通常会做如下事情:

  • 设置整个验证平台的超时退出时间
  • 通过config_db设置验证平台某些参数的值
    不过这些没有统一的答案,还是因公司而异。

上面的代码中出现了report_phase, 在report_phase中根据UVM_ERROR的数量来打印不同的信息。 一些日志分析工具可以根据打印的信息来判断DUT是否通过了某个测试用例的检查。report_phase也是UVM内建的一个phase, 它在main_phase结束之后执行。

现在UVM层次结构更新为下图:
在这里插入图片描述
因为顶层结构变了,所以run_test以及config_db中设置virtual interface的路径参数也要进行调整。

initial begin
   run_test("base_test");
end

initial begin
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.i_agt.drv", "vif", input_if);
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.i_agt.mon", "vif", input_if);
   uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.o_agt.mon", "vif", output_if);
end

UVM中测试用例的启动

要测试DUT是否按照预期正常工作,需要对其施加不同的激励。这些不同的激励就是不同的测试用例。

测试用例不断增加后,我们需要保证后加的测试用例不影响已经建好的测试用例。例如,前面例子中,我们通过设置default_sequence的形式启动my_sequence。那如果现在又多了一个my_sequence2,要如何在不影响my_sequence的前提下将其启动呢?

我们先来看一下原始的方法。下面是case0的定义,其中default_sequence设置为my_sequence。

class case0_sequence extends uvm_sequence #(my_transaction);
   my_transaction m_trans;

   function  new(string name= "case0_sequence");
      super.new(name);
   endfunction 
   
   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

   `uvm_object_utils(case0_sequence)
endclass


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

而对于case1,我们将default_sequence设置为my_sequence2。

class case1_sequence extends uvm_sequence #(my_transaction);
   my_transaction m_trans;

   function  new(string name= "case1_sequence");
      super.new(name);
   endfunction 

   virtual task body();
      if(starting_phase != null) 
         starting_phase.raise_objection(this);
      repeat (10) begin
         `uvm_do_with(m_trans, { m_trans.pload.size() == 60;})
      end
      #100;
      if(starting_phase != null) 
         starting_phase.drop_objection(this);
   endtask

   `uvm_object_utils(case1_sequence)
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

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

initial begin
	run_test("my_case0");
end

如果要启动my_case1,就把0改为1

initial begin
	run_test("my_case1");
end

这样的问题在于,每次修改代码后,都需要重新编译后才能运行。如果代码量很大,那么就会很耗时。

最理想的办法是在命令行中指定参数来启动不同的测试用例。
事实上, UVM提供对不加参数的run_test的支持。

initial begin
	run_test();
end

仿真的时候,只需要在仿真命令中添加下面的选项即可启动对应的case:

+UVM_TEST_NAME=case_name

case_name如果是my_case1,就会启动my_case1;如果是my_case0,就会启动my_case0。

整个启动、执行的过程如下:
在这里插入图片描述
而启动之后,UVM层次结构具体如下:
在这里插入图片描述
可以看出层次结构变化不大,只是uvm_test_top从base_test变成了my_casen。

至此,一个简单的验证平台也就搭建完成了~

  • 15
    点赞
  • 98
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hardworking_IC_boy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值