前言
某天白天在地铁上听鬼故事,结果晚上要睡觉时,故事里的情节都历历在目,给我鸡皮疙瘩起的~
不过我倒是没有吓得睡不着,而是转念一想,为啥我学知识忘得很快,随便听的鬼故事却记得这么清楚咧?
那如果能像听鬼故事那样去学知识,是不是可以记得更牢固呢?
经过一夜深思,我发现可以通过生活经验来学习专业知识,也就是通过形象的比喻来赋予知识活力。
而刚好最近想要重新看一下强哥《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。文章来源:https://www.toymoban.com/news/detail-474186.html
至此,一个简单的验证平台也就搭建完成了~文章来源地址https://www.toymoban.com/news/detail-474186.html
到了这里,关于诙谐有趣的《UVM实战》笔记——第二章 一个简单的UVM验证平台的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!