开始引入reference model、monitor、scoreboard等验证平台的组件,在这些组件之间信息的传递是基于transaction的。
1、加入transaction
- transaction是抽象的概念。物理协议中的数据交换都是以帧或包为单位的,在一帧或一个包中要定义好各项参数,每个包的大小不一样。很少会有协议是以bit或byte为单位进行数据交换。
- 以以太网为例,每个包的大小至少是64byte。这个包中要包括源地址、目的地址、包的类型、整个包的CRC校验数据等。
- transaction就是用于模拟这种实际情况,一笔transaction就是一个包。在不同的验证平台中会有不同的transaction。一个简单的transaction的定义如下:
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; //数据大小限制在46~1500byte
}
function bit[31:0] calc_crc();
return 32'h0;
endfunction
function void post_randomize();//当某个类的实例的randomize函数被调用后执行该函数
crc = calc_crc;
endfunction
`uvm_object_utils(my_transaction)
function new(string name = "my_transaction");
super.new(name);
endfunction
endclass
- UVM所有transaction都从uvm_sequence_item派生,只有这样才可使用sequence机制。
- 代码使用了uvm_object_utils实现factory机制。在整个仿真期间my_driver是一直存在的,而my_transaction有生命周期,在仿真的某一时间产生,经过driver驱动再经过reference model处理,最终由scoreboard比较完成后其生命周期就结束了。这种类都是派生自uvm_object或uvm_object的派生类,UVM中具有这种特征的类都要使用uvm_object_utils宏来实现。
当完成transaction的定义后, 就可以在my_driver中实现基于transaction的驱动:
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
…
for(int i = 0; i < 2; i++) begin
tr = new("tr");
assert(tr.randomize() with {pload.size == 200;}); //随机化tr
drive_one_pkt(tr);//将tr的内容驱动到DUT的端口上
end
…
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]);//将tr的数据压入队列data_q中,相当于打包成一个byte流
tmp_data = (tmp_data >> 8); //
end
//push smac to data_q
…
//push ether_type to data_q
…
//push payload to data_q
…
//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(); //将data_q中所有数据弹出并驱动
end
@(posedge vif.clk);
vif.valid <= 1'b0;
`uvm_info("my_driver", "end drive one pkt", UVM_LOW);
endtask
2、加入env
- 在验证平台加入组件之前假设这些组件已经定义好,需要考虑在验证平台的什么位置对它们进行实例化,可引入一个容器类解决这个问题——在它里面实例化各个组件。在调用run_test时传递的参数不再是my_driver而是这个容器类,即让UVM自动创建这个容器类的实例。
- 在UVM中这个容器类称为uvm_env:
claa 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); //factory机制的实例化方式
endfunction
`uvm_component_utils(my_env)
endclass
- 所有的env应该派生自uvm_env,且与my_driver一样是component类,应使用uvm_component_utils宏来实现factory的注册。
- 上例drv的实例化方式是只有使用factory机制注册过的类才能使用这种方式实例化,才能进而使用factory机制中的重载功能。在drv实例化时,传递了两个参数名字drv和this指针,表示my_env。
- 验证平台中的组件在实例化时都应该使用type_name::type_id::create的方式。
- 之前通过new函数进行实例化有两个参数:实例名和parent。上例my_driver在uvm_env中实例化,所以它的父结点(parent)就是my_env;UVM通过parent的形式建立起了树形的组织结构,由run_test创建的实例是树根(my_env),且树根名为uvm_test_top(固定名)。树根生长出枝叶(my_driver),这个过程需要在my_env的build_phase中手动实现。无论是树根/树叶,都必须由uvm_component或其派生类继承而来。
整棵UVM树的结构如图所示:
- 加入my_env后,整个验证平台中存在两个build_phase(my_env和my_driver)。在UVM的树形结构中,build_phase的执行遵照从树根到树叶的顺序,即先执行my_env的build_phase,再执行my_driver的build_phase。当把整棵树的build_phase都执行完毕后,再执行后面的phase。
- my_driver在验证平台中的层次结构发生了变化——从树根变成了树叶,所以在top_tb中使用config_db机制传递virtual my_if时要改变相应的路径;同时,run_test的参数也从my_driver变为了my_env:
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
set函数的第二个参数从uvm_test_top变为了uvm_test_top.drv,其中uvm_test_top是UVM自动创建的树根的名字,而drv则是在my_env的build_phase中实例化drv时传递过去的名字。如果在实例化drv时传递的名字是my_drv,那么set函数的第二个参数中也应该是my_drv:
class my_env extends uvm_env
…
drv = my_driver::type_id::create("my_drv", this);
…
endclass
module top_tb;
…
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.my_drv", "vif", inpu t_if);
end
endmodule
3、加入monitor
- 验证平台通过monitor组件监测DUT的行为,只有知道DUT的输入输出信号变化之后,才能根据这些信号变化来判定DUT的行为是否正确。
- driver负责把transaction级别的数据转变成DUT的端口级别并驱动给DUT,monitor的行为与其相对,用于收集DUT的端口数据并将其转换成transaction交给后续的组件处理。
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);
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
...
//pop ether_type
...
//pop payload
...
//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(); //收集完transaction后打印出来
endtask
- 所有的monitor类应该派生自uvm_monitor;
- 与driver类似,在my_monitor中也需要有一个virtual my_if;
- uvm_monitor在仿真中是一直存在的,它是一个component,要使用uvm_component_utils宏注册;
- 由于monitor需要时刻收集数据,所以在main_phase中使用while(1)循环来实现这一目的。
完成monitor的定义后,可以再env中对其进行实例化:
class my_env extends uvm_env;
my_driver drv;
my_monitor i_mon; //监测DUT的输入口
my_monitor o_mon; //监测DUT的输出口
...
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
...
endclass
使用monitor的原因:
- 第一,在一个大型的项目中,driver根据某一协议发送数据,而monitor根据这种协议收集数据, 如果driver和monitor由不同人员实现,那么可以大大减少其中任何一方对协议理解的错误;
- 第二,在实现代码重用时,使用monitor是非常有必要的。
加入monitor的UVM树结构如图所示:
在env中实例化monitor后,要在top_tb中使用config_db将input_if和output_if传递给两个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
4、封装成agent
可以发现driver和monitor的代码高度相似,其本质是因为它们处理的是同一种协议,在同样规则下做着不同的事情。由于两者的相似性,UVM中通常将二者封装在一起成为一个agent。因此不同的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注册。
- build_phase根据is_active的值决定是否创建driver的实例,它是uvm_agent的一个成员变量,从UVM的源代码中可以找到它的原型如下:uvm_active_passive_enum is_active = UVM_ACTIVE;
- uvm_active_passive_enum是一个枚举类型变量, 其定义为:typedef enum bit { UVM_PASSIVE=0, UVM_ACTIVE=1 } uvm_active_passive_enum;
- 这个枚举变量仅有两个值:UVM_PASSIVE和UVM_ACTIVE。在uvm_agent中is_active的值默认为UVM_ACTIVE,这种模式下是需要实例化driver的。 对于UVM_PASSIVE模式:如下图所示,在输出端口上不需要驱动任何信号,只需要监测信号。在这种情况下端口上只需要monitor,所以driver可以不用实例化。
在把driver和monitor封装成agent后,在env中需要实例化agent,而不需要直接实例化driver和monitor:
class my_env extends uvm_env;
my_agent i_agt; //声明i_agt和o_agt
my_agent o_agt;
...
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
i_agt = my_agent::type_id::create("i_agt", this); //实例化
o_agt = my_agent::type_id::create("o_agt", this);
i_agt.is_active = UVM_ACTIVE; //指定工作模式
o_agt.is_active = UVM_PASSIVE;
endfunction
...
endclass
此时UVM树更新为:
由于agent的加入,driver和monitor的层次结构改变了,在top_tb中使用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);
vm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_agt.mon", "vif", output_if);
end
在加入my_agent后,UVM的树形结构越来越清晰:
- 只有uvm_component才能作为树的结点,像my_transaction这种使用uvm_object_utils宏实现的类不能作为UVM树的结点。
- 在my_env的build_phase中创建i_agt和o_agt的实例;在agent中创建driver和monitor的实例也是在build_phase中。build_phase从树根到树叶的执行顺序建立一棵完整的UVM树。
- UVM要求UVM树最晚在build_phase时段完成,如果在build_phase后的某个phase实例化一个component,UVM会报错。但不是只能在build_phase中执行实例化的动作,还可以在new函数中执行实例化的动作:
function new(string name, uvm_component parent);
super.new(name, parent);
if (is_active == UVM_ACTIVE) begin
drv = my_driver::type_id::create("drv", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunction
但这样做无法通过直接赋值的方式向uvm_agent传递is_active的值。在my_env的build_phase(或new函数)中向i_agt和o_agt的is_active赋值不会产生效果。因此i_agt和o_agt都工作在active模式(is_active默认值是UVM_ACTIVE),这与预想相悖。可以在my_agent实例化之前使用config_db语句传递is_active的值解决这个问题:
class my_env extends uvm_env;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(uvm_active_passive_enum)::set(this,"i_agt","is_active",UVM_ACTIVE);
uvm_config_db#(uvm_active_passive_enum)::set(this,"o_agt","is_active",UVM_PASSIVE);
endfunction
endclass
class my_agent extends uvm_agent;
function new(string name, uvm_component parent);
super.new(name, parent);
uvm_config_db#(uvm_active_passive_enum)::get(this, "", "is_active", is_active);
if(is_active == UVM_ACTIVE) begin
drv = my_driver::type_id::create("drv", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunction
endclass
因此,建议仅在build_phase中完成实例化!