UVMDemo学习笔记

UVM实战:一个简单的UVM验证平台

[TOC]

跑通Demo

使用2.5.2的例子作为文件。需要对应自己的环境进行修改,修改如下。

1、setup.vcs修改

对应修改到bash shell下,参照~/.bashrc修改即可,很简单。

对应修改VCS_HOME等自己实际的环境变量

1691055817490

bash和csh都是shell脚本语言。

Linux终端的核心组件是Shell,它是一个解释器,负责解析用户输入的命令并将其发送给操作系统执行。Linux中有多种Shell可用,其中最常见的是Bash(Bourne Again Shell),它是许多Linux发行版的默认Shell。其他常见的Shell包括csh。

bash 的 shell 默认用户下面的配置文件是:.bashrc, 用户登陆之后,默认执行该配置文件内容,让环境变量生效;csh 的 shell 默认用户下面的配置文件是:.cshrc, 用户登陆之后,默认执行该配置文件内容。很多脚本文件在第一行都需要指定所使用的shell,如:#!/bin/bash

为什么有的terminal使用bash,而有的终端使用csh?

Linux发行决定。也可以通过指定更换shell。

2、修改运行脚本

加入-full64选项

好奇:执行脚本指定csh,但是也可以执行,是以为语法正确的原因吗?配置环境的语法有问题所以不能执行而已。

还要注意,使用虚拟机时不要把代码放在共享文件夹内编译

修改后,运行一下demo:

执行source setup.vcs 使环境变量生效。进入代码目录,执行run_tc my_case0,编译运行demo

1691057408627

Demo学习

根据几张图,对应例程代码来学习理解这个验证平台。

  • 典型UVM验证平台框图(P8)
  • UVM树状图/路径(P55,66,86)
  • UVM常用类的继承关系(P57)
  • UVM验证平台执行流程(P55)
    • UVM中的phase(P132)

学习整理:

  • 验证平台各模块基本功能(P7)
  • 代码与框图的对应(不涉及寄存器模型)
  • 代码注释讲解(重点)

第一张图:典型UVM验证平台框架

1691456208017

先介绍平台中每一个模块的功能作用(P7)

  • DUT是我们的待测设计模块,reference model就是参考模型,与DUT完成相同的过程。
  • sequence负责产生激励。sequencer则是负责将sequence生产的激励发送到driver。
  • driver将sequencer发送过来的数据,按照数据协议驱动到DUT上。
  • monitor则是采集激励和响应数据。
  • scoreboard是计分板,自动对比DUT输出和参考模型输出。
  • env:提供一个容器,

当然除了这些模块,还会涉及到一些其他的。如虚接口(virtual interface)、agent等以及一些机制。

第二张图:UVM树状图/路径

这张图在P55,66也都出现过,这张图信息最完整,以此图UVM树进行学习理解。

1691456481828

这个便于理解代码的层次结构,也和执行顺序是相关的。

矩形框内第一行是模块名,实例名,也是每个模块路径。第二行括号内是UVM每个模块的类。矩形框旁边的new是例化模块时指定的名字。也是通过树的结点去访问UVM树中的每个模块。

UVM是以树的形式组织在一起的,作为一棵树来说,UVM中真正的树根是一个称为uvm_top的东西,在测试用例里实例化env,在env里实例化scoreboard、reference model、agent,在agent里面实例化sequencer、driver和monitor。scoreboard、reference model、sequencer、driver和monitor都是树的叶子。

uvm_top:

uvm_top是uvm_root类的唯一实例,由UVM创建和管理。所在的域是uvm_pkg。
uvm_top是所有test组件的顶层。所有验证环境中的组件在创建时都需要指明它的父一级(Parent)。 如果某些组件在创建时指定父一级的参数为“null”,那么它将直接隶属于uvm_top。 在实例化变量时指定parent变量,可以形成一个树状结构从而起到建立层次的作用
uvm_top提供一系列的方法来控制仿真,例如phase机制、objection防止仿真退出机制等。

uvm_test:

test类是用户自定义类的顶层结构。所有的test类都应该继承于uvm_test,否则,uvm_top将不识别。
test的目标包括:

  • 提供不同的配置,包括环境结构配置、测试模式配置等,然后再创建验证环境。
  • 例化测试序列,并且挂载(attach)到目标sequencer,使其命令driver发送激励。

UVM树对应的框图上,也是自顶向下的。

1691458438716

树的作用就是便于组织验证。

第三张图:UVM常用类的继承关系

1691458685403

这是描述UVM类的,对应的宏也要学习一下

可以在第二张图上标注出每个模块的父类

如图所示,验证平台的所有类都继承于uvm_objectuvm_commponent,而uvm_commponent又是继承于uvm_object的。

派生与uvm_object类的是transaction、sequence这些非模块的东西。

派生于uvm_component类的才可以是UVM树的节点。

白皮书上分子、血液、生命的例子比较好。

对应下这个平台各个成员的父类,如下图所示。

uvm_component这边:reference直接派生与uvm_component,sequencer派生于uvm_component的子类uvm_sequencer。其他的都直接派生与uvm_component的直接子类。

uvm_object这边:其实都是transaction服务的,有uvm_transaction继续向下派生。

1691461402331

分析代码里面的继承关系:

1691475056855

相关的宏:

用的再补充

第四张图:UVM验证平台执行流程

==这里要深入分析,结合前后的知识内容。==

UVM测试用例的启动及执行流程如下图所示:

1691462920655

由顶层的测试模块top_tb开始,

1)top_tb:

例化接口;例化DUT;生成仿真时钟、复位信号;通过uvm_config_db将接口传递给driver和monitor

import uvm_pkg::*;在导入uvm_pkg文件包时,会自动创建一个顶层类uvm_root所例化的对象,及uvm_top

执行run_test(): run_test是在uvm_globals.svh中定义的一个task,用于启动UVM,创建测试用例的实例并运行。也就启动了验证平台。 run_test语句会创建一个my_case0的实例,得到正确的test_name 。然后依次执行uvm_test容器中的各个component组件中的phase机制。具体工作见:uvm中run_test

2)build_phase:

依次执行build_phase(自顶而下),形成完整的UVM树。

在phase机制和objection机制的控制下,执行phase。

phase机制:

UVM验证平台由phase机制和objection机制来控制验证平台的运行。

phase自上而下执行(时间):安装P132图,不同phase的执行顺序。

phase自上而下执行/自下而上(空间):按照UVM树的路径,每个phase,如build_phase,先执行my_case中的build_phase,其次是env中的build_phase,一层层往下执行。对于tast phase,不同component中同名phase会同时运行P136。深度优先,一条支路走到叶子才会回头头别的P139。同级别则按字典顺序执行。

所以结合起来,phase执行顺序就是,先自顶向下执行build_phase。完成后再自顶向下执行connect_phase。

UVM的设计哲学就是在build_phase中做实例化工作。

phase执行顺序

img

1691476193111

objection机制:

只是对task phase的控制,一般在sequence里面挂起和取消。

3)执行UVM树各节点的其他phase:

自顶而下还是自下而上看上面的图

4)所有phase执行完毕,结束仿真

等待所有的phase执行结束,关闭phase控制进程。

报告总结和仿真结束。

关于Transaction

transaction:事务,个人理解为激励、数据。叫事务,会不会是因为一个transaction除了data还有function。

P20页有详细介绍:

transaction是一组数据或者是一个数据包,其基类是uvm_sequence_item。

transaction有生命周期,从sequence产生到scoreboard比较后,transaction的生命周期就结束了。那需要追踪下产生一个激励需要构建了多少个transaction,transaction的传输路径是怎样的,分析从产生到结束的过程。

用到transaction的代码文件:(可以看出事务的传输流程,框图中线上都是transaction?)

==在uvm环境中,所有组件之间的通信都是通过transaction类型连通。==

1691138882141

分析transaction传输过程:(基于验证平台执行过程)

img

tr到DUT端口的转换:(driver)

将tr打包到数组中,在放入DUT的端口中。

1691479410630

1691479455942

DUT端口到tr的转换:(monitor)

将DUT的端口数据打包成tr的形式。

1691479633235

关于Vif

dut和验证平台之间的端口使用虚接口连接(P16)

好处就是避免绝对路径。

以一个interface为例进行理解分析:

1
2
3
4
5
interface my_if(input clk, input rst_n);

logic [7:0] data;
logic valid;
endinterface

按照module进行理解的话,它只声明了接口,但是没有使用。而且,接口的参数是input clk, input rst_n,而接口内部是两个信号。那么接口参数和接口内部的信号怎么区分理解呢?

想了想我的理解是这样:同样的用电路思维去考虑。一个接口电路要连接到两个module上,接口内部信号就是模块直接的连接信号,而输入的参数信号可以理解为接口模块外部给接口的信号。同样,接口作为module直接连接的模块,那这些模块的clk也是外部给进来的。

使用时是先定义interface接口interface my_if,在类中使用时虚接口virtual my_if vif

自己写代码的顺序-自己搭建平台

那知道了代码的层次结构,继承关系以及执行机制和顺序。那么要是自己搭建一个UVM验证平台,设计的顺序应该是怎么样的呢?

其实还是从DUT出发,为了测试DUT,我们要如何如测试它。

首先应该想到给DUT施加激励,构建driver模块。此时为了测试top_tb模块也可以先建立起来。在driver给DUT施加激励时又考虑到端口路径问题,还要设计一个接口interface。然后要考虑加入monitor和sequence,那么就要考虑组件之间信息传递,设计一个transaction模块。然后就可以设计monitor模块sequencer模块。在此基础上设计agent,设计大的容器env模块。最后再加上reference model模块和scoreboard模块,整个模块就成型了。为了测试,还要写测试用例my_casen,这里面会包含sequence

补充一下transaction,在和DUT交互时,主要时driver和monitor。driver是将tr转换成DUT的端口级数据,而monitor则是将DUT的端口级数据转换成tr形式的。

factory机制:自动创建一个类的实例并调用其中的function和task。

不同派生类使用不同的宏去注册。

uvm_conponent使用uvm_component_utils()去注册

派生于uvm_object的使用uvm_object_utils()去注册

factory注册的组件例化时也要用以下形式,验证平台的组件都是:

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

==Todo:ch8==

自己从头搭,一步步来: 从零开始,搭建一个简单的UVM验证平台(一)

UVM相关宏

在UVM库里能看到

运行相关机制

ch5 已在上方整理

UVM验证平台执行流程

代码阅读+注释+思考:

带着疑问去学会看的更仔细,解决自己的疑问,这个知识点也就了解明白许多了。

按运行顺序还是构建顺序去注释。

构建吧,也就是按构建顺序的逻辑来的。执行顺序还会设计到很多的机制。

DUT:

很简单rxd接受数据,然后通过txd发送出去。还有发送使能信号和接受有效信号。

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
//dut.sv
module dut(clk, //DUT功能,通过rxd接受数据,再通过txd发送出去
rst_n, //复位信号
rxd, //8bit的接收到的数据
rx_dv, //接受的数据有效位
txd, //8bit的发送的数据
tx_en); //发送信号的数据有效位
input clk;
input rst_n; //对于这样一个简单的dut,使用UVM环境进行验证
input[7:0] rxd; //要造随机的测试用例作为激励给到dut,然后检测dut的输出信号,如果输出等于输入的话,那么说明dut功能正常
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:

driver的功能就是将sequencer传递过来的数据驱动到DUT上。与DUT使用虚接口进行连接。

拿到sequencer是数据是:seq_item_port.get_next_item(req);

驱动是通过:drive_one_pkt(req);

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
`ifndef MY_DRIVER__SV  // 防止重复编译
`define MY_DRIVER__SV
class my_driver extends uvm_driver#(my_transaction); // 继承与uvm_driver,#(my_transaction)和verilog一样,是参数化的类/模块。这里是传入transaction类。
virtual my_if vif; // 通过虚接口来连接UVM中的类(driver、monitor)和DUT

`uvm_component_utils(my_driver) // factory机制:通过宏注册my_driver类。宏不要分号结尾
function new(string name = "my_driver", uvm_component parent = null); // 构造函数,派生自uvm_component的类在例化时,要指定name和parent两个参数。是为了确定在UVM树中的位置。
super.new(name, parent); // super是调用基类中的方法。扩展类的构造函数。如果父类的构造函数是有参数的,那么必须在子类中有一个构造函数函数,而且必须在子类的构造函数第一行调用父类的构造函数,包括sv的new,UVM的build_phase。(规则)

endfunction

virtual function void build_phase(uvm_phase phase); // phase机制:build_phase主要用于传递虚接口和例化类。
super.build_phase(phase); // 按规则来的
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif)) //uvm_config_db是一个收寄新机制,此处是获取别处传递来的vif(此处参数不介绍),给上面定义的虚接口
`uvm_fatal("my_driver", "virtual interface must be set for vif!!!") //信息打印
endfunction

extern task main_phase(uvm_phase phase); // extern是在类外定义方法,缩减类的长度。main_phase是task phase,是主要动作执行的位置
extern task drive_one_pkt(my_transaction tr); // 用户定义的task,负责驱动一个数据包到DUT
endclass

task my_driver::main_phase(uvm_phase phase); // 通过类名调用类中的任务。此处是定义task
vif.data <= 8'b0; //
vif.valid <= 1'b0;
while(!vif.rst_n) // 非复位状态时,等待时钟沿。就是说复位后才会执行下面的。
@(posedge vif.clk);
while(1) begin // driver执行的任务时一直进行的。
seq_item_port.get_next_item(req); // P45 seq_item_port是driver中的TLM传输的方法,使用get_next_item从sequencer中得到数据包req(P42)
drive_one_pkt(req); // 发送数据包到dut
seq_item_port.item_done(); // 这里driver调用了get_next_item和item_done是driver和sequencer之间的握手机制,调用item_done表示driver成功的驱动了req,sequencer可以发送新数据了,如果没有调用item_done的话,sequencer会重新发送之前的数据,直到item_done被调用
end
endtask

task my_driver::drive_one_pkt(my_transaction tr); // driver驱动数据包到DUT
byte unsigned data_q[]; // 8bit位宽的无符号动态数组
int data_size; // 数据量(byte)

data_size = tr.pack_bytes(data_q) / 8; //pack_bytes是将tr数据打包成bytes数据,放到data_q中。
`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); // 在时钟上升沿将tr数据驱动给虚接口。
vif.valid <= 1'b1;
vif.data <= data_q[i]; // 每个时钟传输8bit
end

@(posedge vif.clk); // 数据传输完成后将使能信号拉低。
vif.valid <= 1'b0;
`uvm_info("my_driver", "end drive one pkt", UVM_LOW);
endtask


`endif

Interface:

好处是使用信号接口不受路径变化影响。

在类中使用要是虚拟接口。

1
2
3
4
5
6
7
8
9
10
`ifndef MY_IF__SV
`define MY_IF__SV

interface my_if(input clk, input rst_n); // 接口信号,需要外部给
// 接口内信号
logic [7:0] data; // 8bit数据信号
logic valid; // 使能信号
endinterface

`endif

那他是如何和DUT的数据对应上的呢?看下DUT例化部分就明白了。

DUT也是用了两组接口,input一组,output一组。

那么接口信号更复杂的话,我们该如何设计接口,需要学习一下。多看几个例子应该能明白。

1
2
3
4
5
6
7
8
9
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));

Transaction:

定义数据包内容,随机约束和相关处理方法。

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
`ifndef MY_TRANSACTION__SV
`define MY_TRANSACTION__SV

class my_transaction extends uvm_sequence_item; // 派生于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(); // 假装是crc计算
return 32'h0;
endfunction

function void post_randomize(); // sv提供的函数,此类被随机化后,随后无条件执行此方法。
crc = calc_crc;
endfunction

`uvm_object_utils_begin(my_transaction) //通过field_automation机制,注册transation中的成员变量,之后就能使用uvm中的宏来处理这些数据

`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

function new(string name = "my_transaction"); // 不是UVM树上的节点,所有不需要parent这个参数
super.new();
endfunction

endclass
`endif

Monitor:

monitor是负责采集DUT的输入输出,用于scoreboard的自动比较。

重点还是下面几行代码?手机DUT的数据,转换成tr,再写出去。

1
2
3
tr = new("tr");
collect_one_pkt(tr);
ap.write(tr);
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
`ifndef MY_MONITOR__SV
`define MY_MONITOR__SV
class my_monitor extends uvm_monitor;

virtual my_if vif; // 虚接口

uvm_analysis_port #(my_transaction) ap; //TLM的发送数据的参数化的类,其参数就是需要传递的数据的类型,这里声明一个句柄。(需要进一步了解)


`uvm_component_utils(my_monitor) // factory注册
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!!!")
ap = new("ap", this); // 例化TLM发送数据的类
endfunction

extern task main_phase(uvm_phase phase);
extern task collect_one_pkt(my_transaction tr); // 抓取数据包port到tr
endclass

task my_monitor::main_phase(uvm_phase phase);
my_transaction tr;
while(1) begin // 持续抓取数据包
tr = new("tr");
collect_one_pkt(tr); // 抓取一个数据包
ap.write(tr); // 写入一个tr,写给了谁?谁会收到?
end
endtask

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;

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
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]; //da sa, e_type, crc // 根据实际数据大小,给pload分配内存
data_size = tr.unpack_bytes(data_array) / 8; // 把bytes流转换成tr中的各个字段.unpack_bytes的参数必须为动态数组
`uvm_info("my_monitor", "end collect one pkt", UVM_LOW);
endtask


`endif

Sqeuencer

sequencer是负责发送sequence的,代码比较简单。

重点在于sequence的设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
`ifndef MY_SEQUENCER__SV
`define MY_SEQUENCER__SV

class my_sequencer extends uvm_sequencer #(my_transaction); //my_sequencer只是driver和sequence之间的桥梁,在其他层次设定了在此处启动sequence

function new(string name, uvm_component parent);
super.new(name, parent);
endfunction

`uvm_component_utils(my_sequencer) // factory注册
endclass

`endif

Agent:

agent这里是将sqr、drv、mon三个组件封装到一起了。再agent里根据uvm成员变量,决定哪些组件是需要的。然后再通过connect_phase将组件连接再一起。

通过agent也可以知道,之前sequencer的sequencer是发给drv的,或者说drv从sqr处拿到tr。

mon生产的tr也是通过ap向上发送的。

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
`ifndef MY_AGENT__SV
`define MY_AGENT__SV

class my_agent extends uvm_agent ;
//agent中封装了三个组件 sequence driver monitor,他们三个因为关系密切被封装到一起,driver从sequencer中拿到激励

my_sequencer sqr;
//monitor和drv处理的是同一种协议,drv把transation的数据转换为dut端口可使用的数据,而monitor把dut的端口数据转换为 transaction中能检测的数据类型
my_driver drv;
my_monitor mon;

uvm_analysis_port #(my_transaction) ap; //uvm_analysis_port是TLM传输级别的一种发送方式,是一个参数化的类,参数就是发送数据的类型.
// 发送给谁? 看连接.


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) // factory 注册
endclass


function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
if (is_active == UVM_ACTIVE) begin // is_active是uvm_agent中的成员变量,默认是UVM_ACTIVE, 例化全部.当is_active == UVM_PASSIVE时,只需要例化monitor.
// 这个成员变量在env里设置了
sqr = my_sequencer::type_id::create("sqr", this); // 例化成员变量,UVM
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);
if (is_active == UVM_ACTIVE) begin
drv.seq_item_port.connect(sqr.seq_item_export); // drv和sqr相连接,这个port在drv里有出现,sqr里没有,回头看看.
end
ap = mon.ap; // 这里的ap和mon中的ap相连接.
// 也就是说,mon写的数据,发送到agent了.在env还会有connect_phase,ap还会再连接,数据还会再发送.
endfunction

`endif


Reference model:

参考模型这里还存在一些疑问:

1、高层次语言写的,时序关系需要吗?能实现到时序级别吗?

2、参考模型如何保证正确性。

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
`ifndef MY_MODEL__SV
`define 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); // 例化,非factory注册(非验证平台组件),用new构造.
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); // 获取数据(iagt.mon)
new_tr = new("new_tr");
new_tr.copy(tr); // 参考模型实体.收到什么数据发什么数据(copy)
`uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
new_tr.print();
ap.write(new_tr); // 输出数据(到scoreboard),发送对象实在env层次指定的.
end
endtask
`endif

Scoreboard:

scoreboard比较固定化,没有什么要特殊处理的。

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
`ifndef MY_SCOREBOARD__SV
`define MY_SCOREBOARD__SV
class my_scoreboard extends uvm_scoreboard;
my_transaction expect_queue[$];
uvm_blocking_get_port #(my_transaction) exp_port; // 期望数据(来自ref model)
uvm_blocking_get_port #(my_transaction) act_port; // 实际数据(来自oagt.mon)
`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 // fork...join,分别处理两个来向的数据,其中一个还负责比较
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.compare(tmp_tran); // compare是哪来的
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.print();
$display("the actual pkt is");
get_actual.print();
end
end
else begin
`uvm_error("my_scoreboard", "Received from DUT, while Expect Queue is empty");
$display("the unexpected pkt is");
get_actual.print();
end
end
join
endtask
`endif

Env:

主要做了两件事,例化和连接。平台的所有组件都是在此处例化的。env实在哪里呢?

还用到了tlm中的fifo就行数据传输。

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
`ifndef MY_ENV__SV
`define MY_ENV__SV

class my_env extends uvm_env; //是一个容器,用于例化/连接各个组件,都是TLM级别传输

my_agent i_agt;
my_agent o_agt;
my_model mdl;
my_scoreboard scb;

// tlm的fifo
uvm_tlm_analysis_fifo #(my_transaction) agt_scb_fifo; // 声明一个参数化类,参数是传输数据的类型.
uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
uvm_tlm_analysis_fifo #(my_transaction) mdl_scb_fifo;

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); //factory注册,特有的例化形式.this是my_env
o_agt = my_agent::type_id::create("o_agt", this);
i_agt.is_active = UVM_ACTIVE; // is_active设置为UVM_ACTIVE,则例化sqr,drv,和mon
o_agt.is_active = UVM_PASSIVE; // is_active设置为UVM_PASSIVE,则只例化mon
mdl = my_model::type_id::create("mdl", this);
scb = my_scoreboard::type_id::create("scb", this);
agt_scb_fifo = new("agt_scb_fifo", this);
agt_mdl_fifo = new("agt_mdl_fifo", this);
mdl_scb_fifo = new("mdl_scb_fifo", this);

endfunction

extern virtual function void connect_phase(uvm_phase phase);

`uvm_component_utils(my_env)
endclass

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);
mdl.ap.connect(mdl_scb_fifo.analysis_export);
scb.exp_port.connect(mdl_scb_fifo.blocking_get_export);
o_agt.ap.connect(agt_scb_fifo.analysis_export);
scb.act_port.connect(agt_scb_fifo.blocking_get_export);
endfunction

`endif

Case:

base_test,测试用例基于base_test。

属于测试用例的基础,是下面的顶层,例化env。同时还有打印信息。用例发送不同的激励在每个case中去实现。

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
`ifndef BASE_TEST__SV
`define 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); // 例化env
endfunction

//report_phase再main_phase结束后执行,打印一些错误信息等
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

`endif

my_case0:

这里面有两个类,一个是sequence一个是case。

case启动:

1
2
3
4
uvm_config_db#(uvm_object_wrapper)::set(this, 
"env.i_agt.sqr.main_phase",
"default_sequence",
case0_sequence::type_id::get());

执行sequence中的``uvm_do(m_trans) 宏。就将tr发送到sequencer。driver通过seq_item_port.get_next_item(req);` 获取sequencer中的tr。

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
`ifndef MY_CASE0__SV
`define MY_CASE0__SV
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(); // 每个sequence都有一个body任务,sequence启动是会自动执行body任务.
if(starting_phase != null) // default_sequence启动后,会得到一个starting_phase
starting_phase.raise_objection(this); // objection机制,控制phase执行与结束.
repeat (10) begin
`uvm_do(m_trans) //P45. 1.实例化m_trans,2.将其随机化,3.把m_trans传给sequencer
end
#100; // 留有时间余量,sequence结束,其他动作未必结束.时间根据激励自行确定.
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);
// 启动sequence,发送给sqr的default_sequence,值是sequence的id: case0_sequence.
// P48 只需set,get UVM做了. #(uvm_object_wrapper)也是约定的,main_phase也是
// 固定要求.
// 下面语句就启动了sequence
uvm_config_db#(uvm_object_wrapper)::set(this,
"env.i_agt.sqr.main_phase",
"default_sequence",
case0_sequence::type_id::get());
endfunction

`endif

Top:

top_tb.sv

这是仿真的最顶层

设置:时间精度,引入宏、库、包,发送虚接口

例化:例化DUT

执行:执行run_test(),启动仿真

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
`timescale 1ns/1ps             // 定义仿真单位/仿真精度
`include "uvm_macros.svh" // UVM宏

import uvm_pkg::*; // UVM库,导入后会自动创建一个顶层类uvm_root所例化的对象
`include "my_if.sv" // 仿真文件调用
`include "my_transaction.sv"
`include "my_sequencer.sv"
`include "my_driver.sv"
`include "my_monitor.sv"
`include "my_agent.sv"
`include "my_model.sv"
`include "my_scoreboard.sv"
`include "my_env.sv"
`include "base_test.sv"
`include "my_case0.sv"
`include "my_case1.sv"

module top_tb; // 测试最顶层,包含UVM顶层.

reg clk;
reg rst_n;
reg[7:0] rxd;
reg rx_dv;
wire[7:0] txd;
wire tx_en;

my_if input_if(clk, rst_n); // 接口
my_if output_if(clk, rst_n);

dut my_dut(.clk(clk), // 例化DUT
.rst_n(rst_n),
.rxd(input_if.data),
.rx_dv(input_if.valid),
.txd(output_if.data),
.tx_en(output_if.valid));

initial begin // 生成clk
clk = 0;
forever begin
#100 clk = ~clk;
end
end

initial begin // 生成复位信号
rst_n = 1'b0;
#1000;
rst_n = 1'b1;
end

initial begin
//括号中如果有参数的话,应该是一个字符串类型如,run_test("my_case0")。将会自动创建一个my_case0的实例,例化名为uvm_test_top。并自动"调用该my_case0中的main_phase"。case中没有main_phase呢?
//这里括号中没有参数,是为了多个用例的选择,通过仿真时指定 +UVM_TESTNAME = my_case1来指定测试用例。
run_test();
end
// 将接口发送给drv,mon模块.
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

endmodule