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

验证平台的组成

验证用于找出DUT的bug,这个过程通常是把DUT放入一个验证平台实现的。一个验证平台要实现的功能如下:

- 模拟DUT的各种真是使用情况,施加激励「正常激励和异常激励」。激励的功能是由driver实现的。
- 验证平台要能够根据DUT的输出来判断DUT的行为是否与预期相符合,完成这个功能的是计分板「scoreboard」。一是判断的是DUT的输出;二是判断的标准是什么。
- 验证平台要能收集DUT的输出并把它们传递给scoreboard,完成这个功能的是monitor。
- 验证平台要能够给出预期结果。在计分板中提到了判断的标准,判断的标准通常就是预期。(放到Deep Learning这里考虑,就是一个target,一个train model)。在验证平台中就是参考模型「reference model」

Untitled

在UVM中,引入了agent和sequence的概念,所以UVM验证平台的典型框图如2.2所示。

Untitled

只有driver的验证平台

driver是验证平台最基本的组件,是验证平台数据流的源泉。接下来以一个简单的DUT举例,说明只有driver的UVM验证平台是如何搭建的。

文件:src/ch2/dut/dut.sv[2]
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

这个DUT非常简单,就是把输入的数据传出去

那么UVM中的driver应该如何搭建?UVM是一个库,这个库中,几乎所有的东西都是使用类(class)来实现的。面向对象编程永远滴神。

UVM验证平台中的driver应该派生自uvm_driver,一个简单的driver如下例所示:

文件:src/ch2/section2.2/2.2.1/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);
        top_tb.rxd <= $urandom_range(0, 255);
        top_tb.rx_dv <= 1'b1;
        `uvm_info("my_driver", "data is drived", UVM_LOW)
    end
    @(posedge top_tb.clk);
    top_tb.rx_dv <= 1'b0;
endtask

这个driver的功能非常简单,就是向rxd上发送256个随机数据,并将rx_dv置为高电平。发送结束后将rx_dv置为低电平。

  • 派生自uvm_driver的类的new函数有两个参数,一个是name,一个是uvm_component类型的parent。
  • driver所做的事情几乎都在main_phase中完成。UVM由phase来管理验证平台的运行。

上述代码中出现的uvm_info 宏。这个宏的功能相当于Verilog中display语句的加强版。第一个字符串是信息类别,第二个字符串是要打印的信息。第三个参数是冗余级别。关键信息可以设置为UVM_LOW ,有些信息可有可无,就设置为UVM_HIGH ,介于两者之间是UVM_MEDIUM 。UVM默认只显示级别为LOW和MEIDIUM的信息。

打印出来的信息:

UVM_INFO my_driver.sv
(20
)@48500000
:drv[my_driver]data is drived
  • UVM_INFO关键字:表明这是uvm_info打印的结果。除此之外还有uvm_error宏、uvm_warning宏。
  • my_driver.sv(20):指明打印此条信息的来源,括号数字代表uvm_info打印语句在my_driver.sv中的行号。
  • 48500000:表明此条信息的打印时间。
  • drv:这事driver在UVM树中的路径索引。UVM采用树形结构,树中的任何一个结点,都有一个与之相应的字符串类型的路径索引。路径索引可以通过get_full_name函数来获取,下列的代码加入UVM数节点中就可以得知当前的路径索引。
$display("the full name of current component is: %s", get_full_name());
  • data is drived:最终打印的信息。

uvm_info十分强大(跟python的printf和log之间的差别类似,都是为了更全面的打印信息,方便debug),所以在搭建验证平台时应该尽量使用uvm_info替代display。

定义my_driver后需要将其实例化。这里需要注意类的定义与类的实例化的区别。所谓类的定义,就是用编辑器写下:

class A;
...
endclass

类的实例化则是new出一个A的实例。比如:

A a_inst;
a_inst = new();

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

文件:src/ch2/section2.2/2.2.1/top_tb.sv
`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);  // 显式调用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机制

自动创建一个类的实例并且调用其中的函数(function)和任务(task)

  • 插个题外话,当时面试的时候问到我function和task的区别了,这里再回忆一下.

    task和function区别

    1、function只能与主模块共用一个仿真时间单位,而task可以定义自己的仿真时间单位。

    2、function不能包含task,task可以包含其它的task和function。

    3、function至少有一个输入变量,而task可以没有或者有多个任意类型的变量。

    4、function返回一个值,而task则不返回值。

    目的

    function的目的是通过返回一个值来响应输入信号的值。

    task能支持多种目的,能计算多个结果值,这些结果值之间能通过调用的task的输出或者总线端口输出。

factory机制的实现被集成在了uvm_conponent_utils这个宏中,这个宏做的事情很多,其中之一就是把my_driver登记在UVM内部的一张表中(注册表?)

driver加入factory机制后,还需要对top_tb做一些改动:

文件:src/ch2/section2.2/2.2.2/top_tb.sv
7 module top_tb;
…
36 initial begin
37 run_test("my_driver");
38 end
39
40 endmodule

run_test 代替了my_driver的实例化和对main_phase的显式调用。运行新的验证会输出:

new is called
main_phased is called

请记住一点:所有派生自uvm_component及其派生类的类都应该使用uvm_component_utils宏注册。

上面的例子中,只输出到“main_phase is called”。令人沮丧的是,根本没有输出“data is drived”,而按照预期,它应该输出256次。关于这个问题,牵涉UVM的objection机制。

加入objection机制

讲道理上面加入factory机制,应该顺利地执行main_phase,但是实际结果是没有正确输出“human is dead, mismatch””data is drived”。

UVM通过objection机制来控制验证平台的关闭。上面的例子中,并没有调用finish语句结束仿真。但是运行时,仿真平台确实关闭了。在每个phase中,UVM会检查是否有objection被提起,如果有就等待objection被撤销后停止仿真;如果没有立即结束当前的phase。

加入objection机制的driver如下所示:

文件:src/ch2/section2.2/2.2.3/my_driver.sv
13 task my_driver::main_phase(uvm_phase phase);
14  phase.raise_objection(this); // drop_objection之前必须有raise_objection,成对出现
15  `uvm_info("my_driver", "main_phase is called", UVM_LOW);
16  top_tb.rxd <= 8'b0;
17  top_tb.rx_dv <= 1'b0;
18 while(!top_tb.rst_n)
19  @(posedge top_tb.clk);
20 for(int i = 0; i < 256; i++)begin
21  @(posedge top_tb.clk);
22  top_tb.rxd <= $urandom_range(0, 255);
23  top_tb.rx_dv <= 1'b1;
24  `uvm_info("my_driver", "data is drived", UVM_LOW);
25  end
26  @(posedge top_tb.clk);
27  top_tb.rx_dv <= 1'b0;
28  phase.drop_objection(this); // 可以简单的看成finish函数的替代
29 endtask

加入objection机制后,发现“data is drived”按照预期输出了256次。

raise_objection 语句必须在main_phase中第一个消耗仿真时间的语句之前(@posedge top.clk))

加入virtual interface

之前给DUT输入端口赋值都是用绝对路径,可移植性比较差。比如clk信号从top.clk变成了top.clk_inst.clk,那么就需要改driver代码。所以应该杜绝在验证平台使用绝对路径。

用宏呗。

`define TOP top_tb

另一种方法是使用interface:

文件:src/ch2/section2.2/2.2.4/my_if.sv
4 interface my_if(input clk, input rst_n);
5
6   logic [7:0] data;
7   logic valid;
8 endinterface

相当于有个warpper包了一层,给留个输入输出。

在top_tb例化就可以用。

文件:src/ch2/section2.2/2.2.4/top_tb.sv
17 my_if input_if(clk, rst_n);
18 my_if output_if(clk, rst_n);
19
20 dut my_dut(.clk(clk),
21  .rst_n(rst_n),
22  .rxd(input_if.data),
23  .rx_dv(input_if.valid),
24  .txd(output_if.data),
25  .tx_en(output_if.valid));

如何在driver中使用interface呢?因为driver是一个类,在类中不能以上述方式声明interface,只有在module里可以,所以在类中使用的是virtual interface:

文件:src/ch2/section2.2/2.2.4/my_driver.sv
3 class my_driver extends uvm_driver;
4
5 virtual my_if vif;

声明vif之后,就可以在main_phase中使用如下方式驱动其中的信号:

文件:src/ch2/section2.2/2.2.4/my_driver.sv
23 task my_driver::main_phase(uvm_phase phase);
24  phase.raise_objection(this);
25  `uvm_info("my_driver", "main_phase is called", UVM_LOW);
26  vif.data <= 8'b0;
27  vif.valid <= 1'b0;
28  while(!vif.rst_n)
29      @(posedge vif.clk);
30  for(int i = 0; i < 256; i++)begin
31      @(posedge vif.clk);
32      vif.data <= $urandom_range(0, 255);
33      vif.valid <= 1'b1;
34      `uvm_info("my_driver", "data is drived", UVM_LOW);
35  end
36  @(posedge vif.clk);
37  vif.valid <= 1'b0;
38  phase.drop_objection(this);
39 endtask

可以看到绝对路径已经消除了。

那么如何把top_tb中的input_if和my_driver中的vif对应起来呢?top_tb.my_driver.xxx是不可以的。原因是UVM通过run_test实例化了一个脱离top_tb层次结构的实例。

对于这种脱离top_tb的层次结构,有希望能够在top_tb中对其进行操作,UVM引进了config_db机制,这其中又分为set和get操作。(类似于hook?)

set操作:

文件:src/ch2/section2.2/2.2.4/top_tb.sv
44 initial begin
45 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
46 end

get操作

文件:src/ch2/section2.2/2.2.4/my_driver.sv
13 virtual function void build_phase(uvm_phase phase);
14  super.build_phase(phase);
15  `uvm_info("my_driver", "build_phase is called", UVM_LOW);
16  if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
17      `uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
18 endfunction

这里引入了build_phase,在new之后main_phase之前执行。在build_phase中主要通过config_db传递一些数据和实例化比成员变量等。需要注意的是,这里加入了super.bulid_phase 因为在其父类的build_phase执行了一些必要的操作,这里必须显式地调用并执行它。build_phase与main_phase的区别是,build_phase是一个函数,main_phase则是一个任务。build_phase不消耗仿真时间。

这里出现的uvm_fatal 宏类似于uvm_info 但是只有两个参数,并且与uvm_info的前两个参数完全一样。但是他会在打印第二个参数的信息后直接调用finish 结束仿真。uvn_fatel代表验证平台出现了重大问题无法继续,所以没必要设置第三个冗余参数。

config_dbsetget 都需要四个参数,这两个参数的第三个参数必须完全一致。get的第四个参数表示把interface传给哪一个my_driver的成员变量。set的第二个函数代表路径索引。

无论传递给run_test的参数是什么,创建实例的名字都是uvm_test_top 由于set的目标是my_driver,所以第二个参数就是uvm_test_top

使用双冒号是因为这两个函数都是静态函数,而uvm_config_db#(virtual my_if)则是一个参数化的类,其参数就是要寄信的类型,这里是virtual my_if。加入要向my_driver的var变量传递一个int类型的数据,可以使用如下方式:

initial begin
    uvm_config_db#(int)::set(null, "uvm_test_top", "var", 100);
end

而在my_driver中应该使用如下形式。

class my_driver extends uvm_driver;
    int var;
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        `uvm_info("my_driver", "build_phase is called", UVM_LOW);
    if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
        `uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
    if(!uvm_config_db#(int)::get(this, "", "var", var))
        `uvm_fatal("my_driver", "var must be set!!!")
endfunction

为验证平台加如各个组件

加入transaction

上一节介绍的操作是信号级的,但是在reference model、monitor、scoreboard等验证平台的其他组件之间,信息的传递是基于transaction的。所以介绍一下transaction的概念。

transaction比较抽象,可以理解成传递数据的包。

文件:src/ch2/section2.3/2.3.1/my_transaction.sv
4 class my_transaction extends uvm_sequence_item;
5
6   and bit[47:0] dmac; // 48bit以太网目的地址
7   and bit[47:0] smac; // 48bit以太网源地址
8   and bit[15:0] ether_type; // 以太网类型
9   and byte pload[]; // 携带数据的大小
10  rand bit[31:0] crc; // 前面所有数据的校验值
11  
12  constraint pload_cons{
13  pload.size >= 46;
14  pload.size <= 1500;
15 }
16
17  function bit[31:0] calc_crc();
18   return 32'h0;
19  endfunction
20  // 某个实例的randomize被调用后,post_radomize会紧随其后无条件地被调用。
21  function void post_randomize(); // 这里只是举例,CRC计算方法比较复杂,但是网上代码比较常规
22   crc = calc_crc; // 加了个空函数calc_crc
23  endfunction
24  
25  `uvm_object_utils(my_transaction)
26  
27  function new(string name = "my_transaction");
28   super.new(name);
29  endfunction
30 endclass

在transaction定义中,有两点值得引起注意:一是my_transaction的基类是uvm_sequence_item。在UVM中,所有的transaction都要从uvm_sequence_item派生,只有从uvm_sequence_item派生的transaction才可以使用后文讲述的UVM中强大的sequence机制。

第二是这里没有使用uvm_component_utils 实现factory机制,而是使用uvm_object_utils 。在整个仿真周期中,my_driver是一直存在的,my_transaction不同,它有生命周期,经过driver-reference model-scoreboard比较完成后,生命周期就结束了。

当完成transaction的定义后,就可以在my_driver中实现基于transaction的驱动:(懒得缩进了)

文件:src/ch2/section2.3/2.3.1/my_driver.sv
22 task my_driver::main_phase(uvm_phase phase);
23 my_transaction tr;
…
29 for(int i = 0; i < 2; i++) begin
30 tr = new("tr");
31 assert(tr.randomize() with {pload.size == 200;}); // 随机化tr
32 drive_one_pkt(tr); // 将tr内容驱动到DUT端口上
33 end
…
36 endtask
37
38 task my_driver::drive_one_pkt(my_transaction tr);
39 bit [47:0] tmp_data;
40 bit [7:0] data_q[$];
41
42 //push dmac to data_q
43 tmp_data = tr.dmac;
44 for(int i = 0; i < 6; i++) begin
45 data_q.push_back(tmp_data[7:0]);
46 tmp_data = (tmp_data >> 8);
47 end
48 //push smac to data_q
…
54 //push ether_type to data_q
…
60 //push payload to data_q
…
64 //push crc to data_q
65 tmp_data = tr.crc;
66 for(int i = 0; i < 4; i++) begin
67 data_q.push_back(tmp_data[7:0]);
68 tmp_data = (tmp_data >> 8);
69 end
70
71 `uvm_info("my_driver", "begin to drive one pkt", UVM_LOW);
72 repeat(3) @(posedge vif.clk);
73
74 while(data_q.size() > 0) begin
75 @(posedge vif.clk);
76 vif.valid <= 1'b1;
77 vif.data <= data_q.pop_front();
78 end
79
80 @(posedge vif.clk);
81 vif.valid <= 1'b0;
82 `uvm_info("my_driver", "end drive one pkt", UVM_LOW);
83 endtask

加入env

在验证平台中加入reference model、scoreboard等之前,思考一个问题:假设这些组件已经定义好了,那么在验证平台的什么 位置对它们进行实例化呢?在top_tb中使用run_test进行实例化显然是不行的,因为run_test函数虽然强大,但也只能实例化一个实 例;如果在top_tb中使用2.2.1节中实例化driver的方式显然也不可行,因为run_test相当于在top_tb结构层次之外建立一个新的结构 层次,而2.2.1节的方式则是基于top_tb的层次结构,如果基于此进行实例化,那么run_test的引用也就没有太大的意义了;如果在 driver中进行实例化则更加不合理。

这个问题的解决方案是引入一个容器类,在这个容器类中实例化driver、monitor、reference model和scoreboard等。在调用 run_test时,传递的参数不再是my_driver,而是这个容器类,即让UVM自动创建这个容器类的实例。在UVM中,这个容器类称为 uvm_env:

文件:src/ch2/section2.3/2.3.2/my_env.sv
4 class my_env extends uvm_env;
5
6 my_driver drv;
7
8 function new(string name = "my_env", uvm_component parent);
9 super.new(name, parent);
10 endfunction
11
12 virtual function void build_phase(uvm_phase phase);
13 super.build_phase(phase);
14 drv = my_driver::type_id::create("drv", this);
15 endfunction
16
17 `uvm_component_utils(my_env)
18 endclass

所有的env应该派生自uvm_env,且与my_driver一样,容器类在仿真中也是一直存在的,使用uvm_component_utils宏来实现factory的注册。

在my_env的定义中,最让人难以理解的是第14行drv的实例化。这里没有直接调用my_driver的new函数,而是使用了一种古怪的方式。这种方式就是factory机制带来的独特的实例化方式。只有使用factory机制注册过的类才能使用这种方式实例化;只有使用这种方式实例化的实例,才能使用后文要讲述的factory机制中最为强大的重载功能。验证平台中的组件在实例化时都应该使用type_name::type_id::create的方式。

drv实例化时,传递两个参数,一个是名字drv,另外一个是this指针,表示my_env。回顾my_driver的new函数:

function new(string name = "my_driver", uvm_component parent = null);
    super.new(name, parent);
endfunction

这个new函数有两个参数,第一个参数是实例名字,第二个是parent。由于my_driver是在uvm_env中实例化,所以my_driver的父结点就是my_env。所以通过parent的形式,UVM建立了树形组织结构。在这种树形的组织结构中,由run_test创建的实例为树根(这里为my_env),树根名字固定「uvm_test_top」,之后树根会开枝散叶,长出枝叶的过程需要在my_evn的build_phase中手动实现。无论树根还是树叶,都必须由uvm_component或者其派生类继承而来。

加入my_env后,整个验证平台存在两个build_phase。一个是my_env,一个是my_driver的。那么这两个执行顺序是什么?实际上是从根到树叶。先my_env再my_driver。

my_driver在平台中的层次结构发生了变化,从树根变为树叶,所以在top_tb中使用config_db的机制传递virtual my_if,也要改变路径;同时run_test的参数也从my_driver变成了my_env

文件:src/ch2/section2.3/2.3.2/top_tb.sv
42 initial begin
43     run_test("my_env");
44 end
45
46 initial begin
47     uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", inp ut_if);
48 end

set函数的第二个参数从uvm_test_top变为了uvm_test_top.drv,其中uvm_test_top是UVM自动创建的树根的名字,而drv则是在my_env的build_phase中实例化drv时传递过去的名字。如果在实例化drv时传递的名字是my_drv,那么set函数的第二个参数中也应该是my_drv:

加入monitor

验证平台需要检测DUT的行为,只有知道DUT的输入输出信号变化之后,才能根据这些信号变化判定DUT的行为是否正确。

而实现这一行为的组件是monitor。driver负责把transaction级别的数据转变成DUT端口级别,并驱动给DUT,monitor行为收集DUT端口数据,转换成transaction,交给后续的组件比如reference model、scoreboard等处理。

一个monitor定义如下:(太长懒得缩进)

文件:src/ch2/section2.3/2.3.3/my_monitor.sv
3 class my_monitor extends uvm_monitor;
4
5 virtual my_if vif;
6
7 `uvm_component_utils(my_monitor)
8 function new(string name = "my_monitor", uvm_component parent = null);
9 super.new(name, parent);
10 endfunction
11
12 virtual function void build_phase(uvm_phase phase);
13 super.build_phase(phase);
14 if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
75
 15 `uvm_fatal("my_monitor", "virtual interface must be set for vif!!!")
16 endfunction
17
18 extern task main_phase(uvm_phase phase);
19 extern task collect_one_pkt(my_transaction tr);
20 endclass
21
22 task my_monitor::main_phase(uvm_phase phase);
23 my_transaction tr;
24 while(1) begin
25 tr = new("tr");
26 collect_one_pkt(tr);
27 end
28 endtask
29
30 task my_monitor::collect_one_pkt(my_transaction tr);
31 bit[7:0] data_q[$];
32 int psize;
33 while(1) begin
34 @(posedge vif.clk);
35 if(vif.valid) break;
36 end
37
38 `uvm_info("my_monitor", "begin to collect one pkt", UVM_LOW);
39 while(vif.valid) begin
40 data_q.push_back(vif.data);
41 @(posedge vif.clk);
42 end
43 //pop dmac
44 for(int i = 0; i < 6; i++) begin
45 tr.dmac = {tr.dmac[39:0], data_q.pop_front()};
46 end
47 //pop smac
…
51 //pop ether_type
…
58 //pop payload
…
62 //pop crc
63 for(int i = 0; i < 4; i++) begin
64 tr.crc = {tr.crc[23:0], data_q.pop_front()};
65 end
66 `uvm_info("my_monitor", "end collect one pkt, print it:", UVM_LOW);
67 tr.my_print();
68 endtask
  • 所有monitor类应该派生自uvm_monitor;
  • 与driver类似,my_monitor中也需要有virtual my_if;
  • uvm_monitor在整个仿真中一直存在,所以它是一个component,需要使用uvm_compnent_utils注册;
  • 由于monitor需要时刻收集数据,所以在main_phase使用while(1)达到循环这一目的。

收集一个transaction后,通过my_print函数将其打印出来。my_print在my_transaction中定义如下:

文件:src/ch2/section2.3/2.3.3/my_transaction.sv
31 function void my_print();
32  $display("dmac = %0h", dmac);
33  $display("smac = %0h", smac);
34  $display("ether_type = %0h", ether_type);
35  for(int i = 0; i < pload.size; i++) begin
36      $display("pload[%0d] = %0h", i, pload[i]);
37  end
38  $display("crc = %0h", crc);
39 endfunction

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

文件:src/ch2/section2.3/2.3.3/my_env.sv
4 class my_env extends uvm_env;
5
6   my_driver drv;
7   my_monitor i_mon;
8   
9   my_monitor o_mon;
…   
15  virtual function void build_phase(uvm_phase phase);
16      super.build_phase(phase);
17      drv = my_driver::type_id::create("drv", this);
18      i_mon = my_monitor::type_id::create("i_mon", this);
19      o_mon = my_monitor::type_id::create("o_mon", this);
20  endfunction
…
23 endclass

实例化了两个monitor,一个监视DUT输出,一个监视DUT输入。

实例化之后还需要再top_tb中将端口传递给两个monitor:

文件:src/ch2/section2.3/2.3.3/top_tb.sv
47 initial begin
48     uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", input_if);
49     uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_mon", "vif", input_if);
50     uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_mon", "vif", output_if);
51 end

封装成agent

上一节我们看到driver与monitor很相似,因为两者处理的是同一协议。由于这种相似性,所以在UVM中通常将二者封装在一起,成为一个agent。

文件:src/ch2/section2.3/2.3.4/my_agent.sv
4 class my_agent extends uvm_agent ;
5 my_driver drv;
6 my_monitor mon;
7
8 function new(string name, uvm_component parent);
9 super.new(name, parent);
10 endfunction
11
12 extern virtual function void build_phase(uvm_phase phase);
13 extern virtual function void connect_phase(uvm_phase phase);
14
15 `uvm_component_utils(my_agent)
16 endclass
17
18
19 function void my_agent::build_phase(uvm_phase phase);
20 super.build_phase(phase);
21 if (is_active == UVM_ACTIVE) begin
22 drv = my_driver::type_id::create("drv", this);
23 end
24 mon = my_monitor::type_id::create("mon", this);
25 endfunction
26
27 function void my_agent::connect_phase(uvm_phase phase);
28 super.connect_phase(phase);
29 endfunction

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

Untitled

加入agent后,driver和monitor的层次结构改变了,top_tb中注意改变路径。

在加入了my_agent后,UVM的树形结构越来越清晰。首先,只有uvm_component才能作为树的结点,像my_transaction这种使用uvm_object_utils宏实现的类是不能作为UVM树的结点的。其次,在my_env的build_phase中,创建i_agt和o_agt的实例是在build_phase中;在agent中,创建driver和monitor的实例也是在build_phase中。按照前文所述的build_phase的从树根到树叶的执行顺序,可以建立一棵完整的UVM树。UVM要求UVM树最晚在build_phase时段完成,如果在build_phase后的某个phase实例化一个component:

那么是不是只能在build_phase中执行实例化的动作呢?答案是否定的。其实还可以在new函数中执行实例化的动作。如可以在my_agent的new函数中实例化driver和monitor,只不过不推荐。

加入reference model

reference model与DUT的复杂程度是一样的。

文件:src/ch2/section2.3/2.3.5/my_model.sv
4 class my_model extends uvm_component;
5
6 uvm_blocking_get_port #(my_transaction) port;
7 uvm_analysis_port #(my_transaction) ap;
8
9 extern function new(string name, uvm_component parent);
10 extern function void build_phase(uvm_phase phase);
11 extern virtual task main_phase(uvm_phase phase);
12
13 `uvm_component_utils(my_model)
14 endclass
15
16 function my_model::new(string name, uvm_component parent);
17 super.new(name, parent);
18 endfunction
19
20 function void my_model::build_phase(uvm_phase phase);
21 super.build_phase(phase);
22 port = new("port", this);
23 ap = new("ap", this);
24 endfunction
25
26 task my_model::main_phase(uvm_phase phase);
27 my_transaction tr;
28 my_transaction new_tr;
29 super.main_phase(phase);
30 while(1) begin
31 port.get(tr);
32 new_tr = new("new_tr");
33 new_tr.my_copy(tr);
34 `uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
35 new_tr.my_print();
36 ap.write(new_tr);
37 end
38 endtask

在connect_phase中将fifo分别与my_monitor中的analysis_port和my_model中的blocking_get_port相连:

文件:src/ch2/section2.3/2.3.5/my_env.sv
31 function void my_env::connect_phase(uvm_phase phase);
32 super.connect_phase(phase);
33 i_agt.ap.connect(agt_mdl_fifo.analysis_export);
34 mdl.port.connect(agt_mdl_fifo.blocking_get_export);
35 endfunction

这里引入了connect_phase。与build_phase及main_phase类似,connect_phase也是UVM内建的一个phase,它在build_phase执行完成之后马上执行。但是与build_phase不同的是,它的执行顺序并不是从树根到树叶,而是从树叶到树根——先执行driver和monitor的connect_phase,再执行agent的connect_phase,最后执行env的connect_phase。

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

加入scoreboard

文件:src/ch2/section2.3/2.3.6/my_scoreboard.sv
3 class my_scoreboard extends uvm_scoreboard;
4 my_transaction expect_queue[$];
5 uvm_blocking_get_port #(my_transaction) exp_port;
6 uvm_blocking_get_port #(my_transaction) act_port;
7 `uvm_component_utils(my_scoreboard)
8
9 extern function new(string name, uvm_component parent = null);
10 extern virtual function void build_phase(uvm_phase phase);
11 extern virtual task main_phase(uvm_phase phase);
12 endclass
13
14 function my_scoreboard::new(string name, uvm_component parent = null);
15 super.new(name, parent);
16 endfunction
17
18 function void my_scoreboard::build_phase(uvm_phase phase);
19 super.build_phase(phase);
20 exp_port = new("exp_port", this);
21 act_port = new("act_port", this);
22 endfunction
23
24 task my_scoreboard::main_phase(uvm_phase phase);
25 my_transaction get_expect, get_actual, tmp_tran;
26 bit result;
27
28 super.main_phase(phase);
29 fork
30 while (1) begin
31 exp_port.get(get_expect);
32 expect_queue.push_back(get_expect);
33 end
34 while (1) begin
35 act_port.get(get_actual);
36 if(expect_queue.size() > 0) begin
37 tmp_tran = expect_queue.pop_front();
38 result = get_actual.my_compare(tmp_tran);
39 if(result) begin
40 `uvm_info("my_scoreboard", "Compare SUCCESSFULLY", UVM_LOW);
41 end
42 else begin
43 `uvm_error("my_scoreboard", "Compare FAILED");
44 $display("the expect pkt is");
45 tmp_tran.my_print();
46 $display("the actual pkt is");
47 get_actual.my_print();
48 end
49 end
50 else begin
51 `uvm_error("my_scoreboard", "Received from DUT, while Expect Que ue is empty");
52 $display("the unexpected pkt is");
53 get_actual.my_print();
54 end
55 end
56 join
57 endtask

my_scoreboard要比较的数据一是来源于reference model,二是来源于o_agt的monitor。前者通过exp_port获取,而后者通过 act_port获取。在main_phase中通过fork建立起了两个进程,一个进程处理exp_port的数据,当收到数据后,把数据放入 expect_queue中;另外一个进程处理act_port的数据,这是DUT的输出数据,当收集到这些数据后,从expect_queue中弹出之前从 exp_port收到的数据,并调用my_transaction的my_compare函数。采用这种比较处理方式的前提是exp_port要比act_port先收到数 据。由于DUT处理数据需要延时,而reference model是基于高级语言的处理,一般不需要延时,因此可以保证exp_port的数据在 act_port的数据之前到来。

Untitled

加入field_automation

在2.3.3节中引入my_mointor时,在my_transaction中加入了my_print函数;在2.3.5节中引入reference model时,加入了my_copy 函数;在2.3.6节引入scoreboard时,加入了my_compare函数。上述三个函数虽然各自不同,但是对于不同的transaction来说,都是类似的:它们都需要逐字段地对transaction进行某些操作。

那么有没有某种简单的方法,可以通过定义某些规则自动实现这三个函数呢?答案是肯定的。这就是UVM中的field_automation机制,使用uvm_field系列宏实现:

不用自己定义,注册之后可以直接调用copy、compare、print等函数。

UVM终极大作:sequence

在验证平台中加入sequencer

sequence机制用于产生激励,它是UVM中最重要的机制之一。在本书前面所有的例子中,激励都是在driver中产生的,但是在一个规范化的UVM验证平台中,driver只负责驱动transaction,而不负责产生transaction。sequence机制有两大组成部分,一是sequence,二是sequencer。本节先介绍如何在验证平台中加入sequencer。一个sequencer的定义如下:

sequencer的定义非常简单,派生自uvm_sequencer,并且使用uvm_component_utils宏来注册到factory中。uvm_sequencer是一个参数化的类,其参数是my_transaction,即此sequencer产生的transaction的类型。

sequencer产生transaction,而driver负责接收transaction。在前文的例子中,定义my_driver时都是直接从uvm_driver中派生:

Untitled

sequence机制

在加入sequencer后,验证平台是一个完整的验证平台。但是在这个验证平台框图中,却找不到sequence的位置。sequence处于一个比较特殊的位置,如图所示。

Untitled

sequence不属于验证平台的任何一部分,但是它与sequencer之间有密切的联系,这点从二者的名字就可以看出来。只有在sequencer的帮助下,sequence产生出的transaction才能最终送给driver;同样,sequencer只有在sequence出现的情况下才能体现其价值,如果没有sequence,sequencer就几乎没有任何作用。sequence就像是一个弹夹,里面的子弹是transaction,而sequencer是一把枪。弹夹只有放入枪中才有意义,枪只有在放入弹夹后才能发挥威力。

除了联系外,sequence与sequencer还有显著的区别。从本质上来说,sequencer是一个uvm_component,而sequence是一个uvm_object。与my_transaction一样,sequence也有其生命周期。它的生命周期比my_transaction要更长一些,其内的transaction全部发送完毕后,它的生命周期也就结束了。这就好比一个弹夹,其里面的子弹用完后就没有任何意义了。因此,一个sequence应该使用uvm_object_utils宏注册到factory中:

一个sequence在向sequencer发送transaction前,要先向sequencer发送一个请求,sequencer把这个请求放在一个仲裁队列中。作为sequencer,它需做两件事情:第一,检测仲裁队列里是否有某个sequence发送transaction的请求;第二,检测driver是否申请transaction。

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

default_sequence的使用

在上一节的例子中,sequence是在my_env的main_phase中手工启动的,作为示例使用这种方式足够了,但是在实际应用中,使用最多的还是通过default_sequence的方式启动sequence。

使用default_sequence的方式非常简单,只需要在某个component(如my_env)的build_phase中设置如下代码即可:

文件:src/ch2/section2.4/2.4.3/my_env.sv
19 virtual function void build_phase(uvm_phase phase);
20 super.build_phase(phase);
…
30 uvm_config_db#(uvm_object_wrapper)::set(this,
31 "i_agt.sqr.main_phase",
32 "default_sequence",
33 my_sequence::type_id::get());
34
35 endfunction

建造测试用例

加入base_test

UVM使用的是一种树形结构,在本书的例子中,最初这棵树的树根是my_driver,后来由于要放置其他component,树根变成了my_env。但是在一个实际应用的UVM验证平台中,my_env并不是树根,通常来说,树根是一个基于uvm_test派生的类。本节先讲述base_test,真正的测试用例都是基于base_test派生的一个类。

base_test派生自uvm_test,使用uvm_component_utils宏来注册到factory中。在build_phase中实例化my_env,并设置sequencer的default_sequence。需要注意的是,这里设置了default_sequence,其他地方就不需要再设置了。

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

除了上述操作外,还通常在base_test中做如下事情:第一,设置整个验证平台的超时退出时间;第二,通过config_db设置验证平台中某些参数的值。这些根据不同的验证平台及不同的公司而不同,没有统一的答案。

Untitled

UVM中测试用例的启动

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

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

无论是在my_env中设置default_sequence,还是在base_test中或者top_tb中设置,都必须修改相关的设置代码才能启动my_sequence2,这与预期相去甚远。

Untitled

Untitled

Last modification:February 23, 2022
恰饭环节