关注微信公众号摸鱼范式
,后台回复COOKBOOK
获取COOKBOOK原本和译本
PDF度盘链接
将testbench连接到DUT
概述
本节,我们主要讨论将UVM testbench连接到RTL DUT的问题。
UVM testbench对象不能直接连接到DUT信号来驱动或采样。driver和monitor组件对象与DUT之间的连接是通过一个或多个具有静态信号端口的BFM组件间接实现的。这些BFM组件以module或interface的形式实现,为了完成到UVM monitor或driver组件类的连接,我们使用虚接口句柄来引用静态接口的内容。
UVM方法学使用uvm_config_db将一个虚接口句柄从静态testbench模块传递给UVM对象类。在使用虚接口实现BFM一节中可以找到两个相关示例。
概述显示了如何通过 uvm_config_db 中的虚接口句柄在monitor和driver中引用 hdl_top 中的具体 BFM 接口。
在书中的大多数示例中,我们都使用“Dual Top”方法。这意味着DUT和BFM实例化在一个静态模块层次结构,顶部通常称为“hdl_top”,而部分testbench负责启动UVM test是在一个单独的模块中,称为“hvl_top”。这种分离在有独立的团队负责项目的设计和验证方面时很有帮助,在转向testbench加速时也很有帮助,因为此时hdl_top的内容将是可合成的。
我们还讨论使用一个abstract-concrete class pair从class对象连接到静态BFM的另一种方法。这种方法使用多态性和类句柄通过API与BFM通信。
最后,我们讨论了DUT-TB连接中参数的处理,其中参数用于指定协议接口中信号的位宽。
Interfaces and Virtual Interfaces
SystemVerilog Interfaces
SystemVerilog接口提供了一种方便的方法,可以将相关信号组织到容器中,以简化模块之间的连接。一个接口可以包含:
- 端口声明
- 信号和变量声明
- 除了模块实例外的任何SystemVerilog代码
- Modport
- 其他interface
接口的声明可以带有端口,也可以不带有端口。如果它是用端口声明的,那么当接口实例化时,这些端口需要被赋值给信号。
所有声明为接口端口或接口内部的信号都可以通过一个接口实例在模块之间传递。一个模块可以有接口端口,并且可以将这些端口与其他类型的信号端口混合。接口内的信号可以被引用,也可以使用接口内的层次引用来赋值,例如,interface_name.signal_name。
接口可以包括除模块实例之外的任何SystemVerilog代码。这意味着它们可以包含initial
块、任务、函数、SystemVerilog断言(SVA)、covergroup和类,这些对于构造验证组件来说都是有用的。
SystemVerilog接口也可以包含modports,它允许根据使用视角组织接口信号的方向。例如,一个总线主机总是驱动一个地址和strobe信号,而一个总线从机总是接收这些信号。虽然从方法论的角度来看,这似乎是一个好想法,但围绕着modport构造的语言有笨拙的元素,在实践中并不是特别有用。
Virtual Interface
在SystemVerilog中,如果类没有在定义信号的模块或接口范围内声明,类就不能对信号进行引用。对于开发可重用的testbench来说,这是非常严格的。
但是,类可以通过虚接口句柄引用接口中的信号。这就允许类在接口内为信号赋值或采样信号的值,或者在接口内调用任务或函数。
为了从类中引用接口,它必须声明为虚接口:
class protocol_driver extends uvm_driver #(protocol_seq_item);
virtual protocol_interface vif;
// Virtual interface declaration
....
task run_phase(uvm_phase phase);
@(posedge vif.clock);
// Accessing the clock signal via the virtual interface handle
vif.send_packet(....);
// Calling a BFM method via the virtual interface handle
如果虚接口没有被赋值,其为默认值null,并且类访问它内部的任何尝试都会在仿真器中导致一个空指针异常。要使虚接口句柄有效,必须将testbench模块部分中的具体静态接口实例赋值给它。在SystemVerilog中有多种方法可以完成这个工作,但对于UVM,推荐的、最可扩展的方法是使用uvm_config_db。
UVM配置数据库(uvm_config_db)
uvm_config_db
uvm_config_db是一个UVM实用类,用于在UVM testbench上的组件对象之间传递配置对象。任何对uvm_config_db的调用都是用存储或检索的数据对象的类型参数化的。要在uvm_config_db中存储对象,使用它的set()方法;而要从uvm_config_db中检索对象,则使用它的get()方法。这两个方法都有赋值对象的参数;将对象与查找键关联;并定义UVM testbench层次结构中的哪些组件可以引用该对象。
还是之前说过的,这里的 ‘’定义UVM testbench层次结构中的哪些组件可以引用该对象‘’ ,实质上只是域名划分上的定义,并不是根据调用位置所处组件的类型来做相应确认的。第一章中我有提到过。
有关uvm_config_db的一般使用的更多信息,请参见下文。
使用uvm_config_db连接UVM testbench到DUT
要将基于testbench (hdl_top)的静态模块的虚接口句柄传递给UVM组件,请使用uvm_config_db,如下所示:
步骤1:在测试台的HDL部分,将静态接口赋值给uvm_config_db中的虚接口:
sfr_if SFR_IF();
// Declaration of SFR interface – This will be connected to modules
initial begin
uvm_config_db #(virtual sfr_if)::set(null, “uvm_test_top”, “SFR”, SFR_IF);
end
请注意以下几点:
- uvm_config_db是用virtual sfr_if类型参数化的
- set()方法的第一个参数是context,它是一个UVM组件对象句柄;在本例中,由于我们是在testbench的HDL部分,所以是一个null对象句柄。
- set()方法的第二个参数是一个字符串,用于标识可能访问数据对象的UVM testbench组件层次结构中的UVM组件实例名。这里使用“uvm_test_top”来限定只接受顶级UVM test对象的访问。它可以被分配一个通配符,比如“*”,这意味着UVM测试台上的所有组件都可以访问它,但这可能没什么帮助,而且在get()过程中可能会有查找开销。
前面说过的,第一个参数(组件对象实例名)和第二个参数用‘’.‘’拼接共同作为get函数两个参数拼接需要匹配的字符串。也只是表面上解释为限定可访问的组件
- set()方法的第三个参数是一个字符串,用作查找名,属于get方需要匹配上的‘’钥匙‘’
- set()的最后一个参数是赋值给在uvm_config_db中创建的虚接口句柄的静态接口。
步骤2:在UVM test组件中,将uvm_config_db条目中的虚接口句柄赋值给配置对象中的虚接口句柄:
// Virtual interface handle declared inside an agent configuration object
class sfr_config extends uvm_object;
...
virtual sfr_if sfr_vif;
...
endclass
class test extends uvm_test;
...
function void build_phase(uvm_phase phase);
sfr_config cfg = sfr_config::type_id::create("cfg");
// Assigning the SFR virtual interface handle via the uvm_config_db #(...)::get() method
if(!uvm_config_db #(virtual sfr_if)::get(this, "", "SFR", cfg.sfr_vif)) begin
`uvm_error("VIF_GET", "Unable to find the SFR virtual interface in the uvm_config_db", UVM_LOW)
end
请注意以下几点:
- uvm_config_db::get()方法是一个函数,它返回一个单bit值来指示对象检索是否成功;这将用于检查,以确保在查找失败时testbench不会继续运行。
- uvm_config_db::get()方法使用虚接口类型进行参数化,以便从数据库检索正确的对象类型。
- get()方法的第一个参数是context,传递的是进行调用的UVM组件的句柄——this
- 第二个参数instance_name传递了一个空字符串" ",这意味着仅根据组件的路径字符串用于标识访问数据对象的UVM testbench组件层次结构(即" uvm_test_top ")。
- 第三个参数是uvm_config_db查找字符串,它应该与在HDL模块中的查找字符串相匹配,在本例中是“SFR”。
- 第四个参数是虚接口句柄。在本例中,句柄位于将被传递给agent的uvm_config_object中。
UVM cookbook的内容排布有些问题,致使一些内容会反复出现,如config_db机制
如果你正在重用一个验证组件,那么你只需要知道它的接口名称以及如何将它指定给UVM验证组件配置对象。然后,就可以应用上面的方法来赋值虚接口。如果必须创建一个新的验证组件,那么就需要注意一些实现方面的考量。
Virtual Interface BFMs
为了使验证组件在testbench间重用,需要将其组织为带有相关信号接口的uvm_agent。这些组件也被称为UVC(通用验证组件)。在uvm_agent中,有两种类型的uvm_component与虚接口交互。driver负责testbench的激励部分,将sequence_item中包含的信息转换为接口活动,包括通过虚接口句柄驱动和采样信号值。monitor负责testbench的被动端,通过虚接口句柄来采样接口信号,以创建表示接口事件的sequence_item。
在设计验证组件时,有多种方式可以实现driver和monitor:
- 接口是一个信号容器,所有的信号驱动和采样都直接在UVM driver和monitor中进行。
- 接口具有进行接口传输或采样接口传输的方法,这些方法在UVM driver和monitor中被调用。
- 两者的混合
最简单的方法是将所有信号访问保持在UVM driver和monitor组件中,并将接口作为信号组。若接口相当简单,并且仿真是验证组件的唯一预期用途,那么这种方法是可以的。
然而,如果验证组件潜在的使用模型可能包括借助硬件加速平台模拟或一个FPGA原型平台,那么driver和monitor的实现必须改变使它们基于调用接口中的方法,而不是直接访问信号。这使得interface在某种程度上是可综合的。在第一个实例中,一个接口可能实现方法,使用只会在仿真器工作的non-synthesisable行为代码,但是在后面的第二个实例,这些方法可以使用synthesisable代码重新实现,且不需要改变UVM driver或monitor代码。
最终,最可扩展的方法是确定抽象协议的哪些部分最好利用面向对象编程的优势实现为UVM组件代码,哪些部分最好实现为在接口中调用的可综合task。这方面的一个例子是以太网协议,组装数据包中的所有位可能最好是在类中完成,以便于可以将发送数据包委托给接口中相对简单的可综合task。
为了说明不同的实现在实践中是如何工作的,让我们考虑实现相同验证组件的两种替代方法。在第一个示例中,接口是一个简单的信号容器,driver和monitor引用一个虚接口来驱动和监控接口传输。在第二个示例中,接口有驱动和监视接口传输的task,UVM driver和monitor引用一个虚接口来间接调用这些task。
这两个示例都将testbench的HDL域和HVL(或UVM)域分离为两个单独的模块。这被称为双顶层testbench架构。
对于最终可能需要与模拟器或FPGA原型共同仿真的testbench,双域划分需要将所有可综合的代码分组到可针对硬件平台编译的单独HDL模块层次结构中。所有不可综合的UVM启动代码都保留在HVL模块中。对于其他testbench,双顶层方法提供了一种方便的方法来分离关注点,使设计团队能够在不影响验证环境的情况下对HDL域进行更改,并使验证团队能够在不影响设计团队的情况下对HVL域进行更改。
示例1 -双顶层,仅仿真版本
在第一个示例中,一个仅用于仿真的agent,协议接口被声明为一个信号组,并在hdl_top testbench模块中实例化。
interface sfr_if(input clk, input reset);
logic[7:0] address;
logic[7:0] write_data;
logic[7:0] read_data;
logic we;
logic re;
endinterface: sfr_if
在agent的driver类的run_phase()任务中,将事务(sequence_item)内容转换为基于时钟周期的激励(pin wiggles),通过虚接口句柄驱动或采样信号。
task sfr_driver::run_phase(uvm_phase phase);
sfr_seq_item item;
forever begin
seq_item_port.get_next_item(item);
if(SFR.reset == 1) begin
SFR.re <= 0;
SFR.we <= 0;
SFR.address <= 0;
SFR.write_data <= 0;
wait(SFR.reset == 0);
end
else begin
@(posedge SFR.clk);
SFR.address = item.address;
SFR.we <= item.we;
SFR.write_data <= item.write_data;
SFR.re <= item.re;
@(posedge SFR.clk);
if(SFR.re == 1) begin
item.read_data = SFR.read_data;
SFR.re <= 0;
end
SFR.we <= 0;
end
seq_item_port.item_done();
end
endtask: run_phase
在agent的monitor类中,monitor在其run_phase()任务中通过虚接口句柄对接口信号进行采样,并在确定事务已完成时发布相应的analysis transaction(sequence_items)。
task sfr_monitor::run_phase(uvm_phase phase);
sfr_seq_item item;
forever begin
@(posedge SFR.clk);
if((SFR.we == 1) || (SFR.re == 1)) begin
item = sfr_seq_item::type_id::create("item");
item.we = SFR.we;
item.re = SFR.re;
item.address = SFR.address;
item.write_data = SFR.write_data;
item.read_data = SFR.read_data;
ap.write(item);
end
end
endtask: run_phase
testbench有两个顶层模块,即hdl_top,其中包含接口和DUT,以及hvl_top,其中包含启动UVM测试台的initial
块。下面的两个代码片段表示来自这两个模块的重要代码。
'''The hdl_top module'''
module hdl_top;
import uvm_pkg::*;
sfr_if SFR(.clk(clk), .reset(reset));
sfr_dut dut (.clk(clk),
.reset(reset),
.address(SFR.address),
.write_data(SFR.write_data),
.we(SFR.we),
.re(SFR.re),
.read_data(SFR.read_data));
initial begin
uvm_config_db #(virtual sfr_if)::set(null, "uvm_test_top", "SFR", SFR);
end
endmodule
'''The hvl_top module'''
module hvl_top;
import uvm_pkg::*;
import sfr_test_pkg::*;
initial begin
run_test();
end
endmodule: hvl_top
总之,这个示例只使用了一个接口,并且所有的信号处理都在基于类的UVM testbench域中进行。
示例2 -双顶层,模拟友好版本
在第二个示例中,有两个独立的接口,一个用于驱动激励(driver_bfm),另一个用于监视接口(monitor_bfm)。为了模拟的友好性,agent信号级driver和monitor功能已经作为方法转移到接口上,UVM agent driver和monitor对象通过相应的的虚接口句柄调用方法。
'''The Driver'''
task sfr_driver::run_phase(uvm_phase phase);
sfr_seq_item item;
forever begin
seq_item_port.get_next_item(item);
SFR.execute(item);
seq_item_port.item_done();
end
'''The Driver BFM'''
task execute(sfr_seq_item item);
if(reset == 1) begin
wait(reset == 0);
end
else begin
@(posedge clk);
address = item.address;
we <= item.we;
write_data <= item.write_data;
re <= item.re;
@(posedge clk);
if(re == 1) begin
item.read_data = read_data;
re <= 0;
end
we <= 0;
end
endtask: execute
'''The Monitor'''
task sfr_monitor::run_phase(uvm_phase phase);
sfr_seq_item item;
forever begin
item = sfr_seq_item::type_id::create("item");
SFR.monitor(item);
ap.write(item);
end
endtask: run_phase
'''The Monitor BFM'''
task monitor(sfr_seq_item item);
@(posedge clk);
if((we == 1) || (re == 1)) begin
item.we = we;
item.re = re;
item.address = address;
item.write_data = write_data;
item.read_data = read_data;
end
endtask: monitor
如果比较这两个示例,你应该注意到功能本质上是相同的,唯一的区别是将信号处理委托给BFM接口中的方法。虽然接口中的代码是不可综合的,但是执行transaction或监视总线传输所需的API调用已经就绪,可以用可综合的实现替换BFM接口。
在hdl_top testbench模块中现在有两个接口(master_bfm和monitor_bfm),这需要在uvm_config_db中设置两个虚接口:
module hdl_top;
import uvm_pkg::*;
sfr_master_bfm SFR_MASTER(.clk(clk),
.reset(reset),
.address(address),
.write_data(write_data),
.we(we),
.re(re),
.read_data(read_data));
sfr_monitor_bfm SFR_MONITOR(.clk(clk),
.reset(reset),
.address(address),
.write_data(write_data),
.we(we),
.re(re),
.read_data(read_data));
sfr_dut dut (.clk(clk),
.reset(reset),
.address(address),
.write_data(write_data),
.we(we),
.re(re),
.read_data(read_data));
initial begin
uvm_config_db #(virtual sfr_master_bfm)::set(null, "uvm_test_top","SFR_MASTER", SFR_MASTER);
uvm_config_db #(virtual sfr_monitor_bfm)::set(null, "uvm_test_top","SFR_MONITOR", SFR_MONITOR);
end
endmodule
hvl_top testbench模块与示例1中完全相同。
总之,示例的模拟友好版本使用了两个独立的接口,信号驱动和监视功能是通过这些接口中的API调用来启动的。此示例可用于仿真,并提供了一个通向实现的桥梁,在该实现中,BFM接口被运行在模拟器或硬件原型平台上的实现所取代。
书中的大多数其他示例将采用模拟友好方法。例外情况是那些为了说明某一点而简化为避免使用带有接口的agent的情况。
对于这种将可综合的代码分组到可针对硬件平台编译的单独HDL模块层次结构中(仿真中使用BFM if)的方式的更多内容,可参考 https://verificationacademy.com/resource/6648
参数化处理
参数通常用于配置设计IP和接口。从VIP的角度来看,参数通常会影响总线位宽或使用的通道数量。
SystemVerilog接口可以被参数化,当其被参数化时,其虚接口句柄也需要被参数化。因为agent的monitor和driver中的虚接口的参数值需要与hdl_top testbench模块中的静态接口的参数值相匹配,所以这会影响UVM VIP代码。将不同的参数值分配给接口有效地创建了一个新类型。
在RTL中处理参数很容易理解。它通常需要将顶层参数向下传递到设计层次结构,并进行一些可能的修改。参数可以在类库中以同样的方式处理,但是这样通常很不方便,因为在最坏的情况下,这意味着每个sequence item都必须参数化。如果有几个相同类型的参数化接口,但是拥有不同的参数,那么就会出现错误。这里有两种主要的策略用于处理VIP接口中的参数:
- 使用参数的最大可能值,并且只连接所使用的线和通道
- 使用参数package来管理参数和typedef参数化类
在本文附带的示例中,我们混合使用了这两种方法。
testbench parameter package
使用testbench参数package可以管理testbench参数。而将所有参数定义保存在一个package中可以确保在参数更改时只需要编辑一个文件。
下面的代码示例显示了testbench参数package的典型内容。这个package需要导入所有的VIP agent package,以便参数化类的特殊版本可以被typedef。对于每个接口,都应该定义一个类,该类包含定义为localparams的参数值。然后,这个类为这些参数值提供了一个限定范围的容器。之后,用户必须处理的参数化类被定义为使用类作用域的专用typedef。最后,声明专用虚接口句柄的typedef。
package tb_params_pkg;
// Import the VIP package to get access to all the VIP class types
import sfr_agent_pkg::*;
// Class declaration with localparams - this creates a defined namespace
// and is useful for handling multiple VIPs of the same type
class SFR;
localparam sfr_addr_width = 16;
localparam sfr_data_width = 32;
endclass
// typedefs for those parts of the agent code exposed to the user, using the typedefs
// avoids having to type in the parameters
typedef sfr_config_object #(SFR::sfr_addr_width, SFR::sfr_data_width) SFR_cfg_t;
typedef sfr_agent #(SFR::sfr_addr_width, SFR::sfr_data_width) SFR_agent_t;
// Declaration of virtual interface handles
typedef virtual sfr_monitor_bfm #(SFR::sfr_addr_width, SFR::sfr_data_width) SFR_monitor_bfm_t;
typedef virtual sfr_master_bfm #(SFR::sfr_addr_width, SFR::sfr_data_width) SFR_master_bfm_t;
endpackage
这个例子只显示了一个VIP的内容,但是当有多个VIP的参数时,他们应该被分组到同一个testbench参数package中。
使用uvm_config_db参数化接口
如果你的hdl_top testbench模块中有参数化的接口,那么需要使用有相同的参数的uvm_config_db #()::set()方法。有两种方法可以做到这一点,要么使用tb_params_pkg中的参数,要么为package中定义的虚接口使用专门的typedef:
import uvm_pkg::*;
import tb_params_pkg::*; // Import of the tb_params_pkg to access typedefs
logic clk;
logic reset;
wire[SFR::sfr_addr_width-1:0] address;
wire[SFR::sfr_data_width-1:0] write_data;
wire[SFR::sfr_data_width-1:0] read_data;
wire we;
wire re;
// Parameterised version of the sfr_master_bfm using the SFR scope to define the parameters:
sfr_master_bfm #(.ADDR_WIDTH(SFR::sfr_addr_width),
.DATA_WIDTH(SFR::sfr_data_width))
SFR_MASTER(.clk(clk),
.reset(reset),
.address(address),
.write_data(write_data),
.read_data(read_data),
.re(re),
.we(we));
sfr_monitor_bfm #(.ADDR_WIDTH(SFR::sfr_addr_width),
.DATA_WIDTH(SFR::sfr_data_width))
SFR_MONITOR(.clk(clk),
.reset(reset),
.address(address),
.write_data(write_data),
.read_data(read_data),
.re(re),
.we(we));
initial begin
// Using a parameterised virtual interface handle:
uvm_config_db #(virtual sfr_master_bfm #(.ADDR_WIDTH(SFR::sfr_addr_width), .DATA_WIDTH(SFR::sfr_data_width)))::set(null, "uvm_test_top", "SFR_MASTER", SFR_MASTER);
// Using the virtual interface type declared in the tb_params_pkg:
uvm_config_db #(SFR_monitor_bfm_t)::set(null, "uvm_test_top", "SFR_MONITOR", SFR_MONITOR);
end
在UVM test中,tb_params_pkg中的虚接口句柄typedef用于参数化uvm_config_db get()调用:
class sfr_test extends uvm_component;
sfr_env_config env_cfg;
SFR_cfg_t sfr_agent_cfg; // Typedef from tb_params_pkg
sfr_env env;
extern function void build_phase(uvm_phase phase);
extern task run_phase(uvm_phase phase);
endclass: sfr_test
function void sfr_test::build_phase(uvm_phase phase);
env_cfg = sfr_env_config::type_id::create("env_cfg");
sfr_agent_cfg = SFR_cfg_t:: type_id::create("sfr_agent_cfg");
//
// Getting the parameterised virtual interface handles from the uvm_config_db
//
if(!uvm_config_db #(SFR_master_bfm_t)::get(this, "", "SFR_MASTER", sfr_agent_cfg.SFR_MASTER)) begin
`uvm_error("BUILD_PHASE", "Unable to find virtual interface sfr_master_bfm in the uvm_config_db")
end
if(!uvm_config_db #(SFR_monitor_bfm_t)::get(this, "", "SFR_MONITOR", sfr_agent_cfg.SFR_MONITOR)) begin
`uvm_error("BUILD_PHASE", "Unable to find virtual interface sfr_master_bfm in the uvm_config_db")
end
sfr_agent_cfg.is_active = 1;
env_cfg.sfr_agent_cfg = sfr_agent_cfg;
env = sfr_env::type_id::create("env", this);
env.cfg = env_cfg;
endfunction: build_phase
在env_config和env类中,专用版本的sfr_agent_config和sfr_agent类的typedef用于在类层次结构中向下传递参数值。使用tb_params_pkg定义可确保所有类层次结构参数是一致的。
// The sfr_env_config class:
class sfr_env_config extends uvm_object;
SFR_cfg_t sfr_agent_cfg;
endclass: sfr_env_config
// The sfr_env
class sfr_env extends uvm_component;
sfr_env_config cfg;
sfr_scoreboard sb;
SFR_agent_t agent; // Using the SFR_agent_t typedef from tb_params_pkg
extern function void build_phase(uvm_phase phase);
extern function void connect_phase(uvm_phase phase);
endclass: sfr_env
function void sfr_env::build_phase(uvm_phase phase);
if(cfg == null) begin
if(!uvm_config_db #(sfr_env_config)::get(this, "", "CFG", cfg)) begin
`uvm_error("BUILD_PHASE", "Unable to find environment configuration object in the uvm_config_db")
end
end
sb = sfr_scoreboard::type_id::create("sb", this); agent = SFR_agent_t::type_id::create("agent", this); agent.cfg = cfg.sfr_agent_cfg;
endfunction: build_phase
// And the sfr_agent - to show how the parameters are passed through to the driver.
// Note that the parameters are defined more generically here to ensure that the agent can be reused
//
class sfr_agent #(SFR_ADDR_WIDTH = 8, SFR_DATA_WIDTH = 8) extends uvm_component;
typedef sfr_agent #(SFR_ADDR_WIDTH, SFR_DATA_WIDTH) this_t;
`uvm_component_param_utils(this_t)
uvm_analysis_port #(sfr_seq_item) ap;
uvm_sequencer #(sfr_seq_item) sequencer;
sfr_driver #(SFR_ADDR_WIDTH, SFR_DATA_WIDTH) driver;
sfr_monitor #(SFR_ADDR_WIDTH, SFR_DATA_WIDTH) monitor;
sfr_config_object #(SFR_ADDR_WIDTH, SFR_DATA_WIDTH) cfg;
function new(string name = "sfr_agent", uvm_component parent = null);
super.new(name, parent);
endfunction
extern function void build_phase(uvm_phase phase);
extern function void connect_phase(uvm_phase phase);
endclass: sfr_agent
function void sfr_agent::build_phase(uvm_phase phase);
if(cfg == null) begin
if(!uvm_config_db #(sfr_config_object #(SFR_ADDR_WIDTH, SFR_DATA_WIDTH))::get(this, "", "SFR_CFG", cfg)) begin
`uvm_error("BUILD_PHASE", "Unable to find sfr agent config object in the uvm_config_db")
end
end
ap = new("ap", this);
monitor = sfr_monitor #(SFR_ADDR_WIDTH, SFR_DATA_WIDTH)::type_id::create("monitor", this);
if(cfg.is_active == 1) begin
driver = sfr_driver #(SFR_ADDR_WIDTH, SFR_DATA_WIDTH)::type_id::create("driver", this);
sequencer = uvm_sequencer #(sfr_seq_item)::type_id::create("sequencer", this);
end
endfunction: build_phase
最后,为了确保可以在不处理参数的情况下编写任何sequence,我们对sequence_item项定义采用了“max width”方法。地址和数据总线的最大宽度是32位,所以我们使用它来确定sequence_item中的地址和数据字段的大小。任何未使用的位将被忽略。
class sfr_seq_item extends uvm_sequence_item;
`uvm_object_utils(sfr_seq_item)
function new(string name = "sfr_seq_item");
super.new(name);
endfunction
rand bit[31:0] address;
rand bit[31:0] write_data;
rand bit we;
rand bit re;
bit[31:0] read_data;
endclass: sfr_seq_item
UVM类必须采用参数的原因是虚接口句柄必须使用参数声明和赋值,这个问题的另一种可供替代的解决方案是使用抽象-具体类(Abstract-Concrete类)实现方法。
Abstract-Concrete类连接
注意:DUT-TB连接的这种方法仅是仿真技术
为DUT到UVM testbench连接使用虚接口句柄的另一种选择是使用抽象具体类对。类的抽象版本为BFM中可用的所有方法定义了模板。类的具体版本在BFM中声明,它继承于抽象版本,提供了方法的实现。具体类的句柄通过uvm_config_db赋值给agent类中的抽象句柄。类对象多态的属性允许抽象类句柄使用类的具体版本中的方法实现与BFM交互。
如果类是在接口或模块内定义的,那么它就可以引用接口或模块范围内的任何变量。这可以用来简化参数的处理。
下面的代码片段显示了sfr_master类的抽象和具体版本:
// The abstract class for the SFR master driver/BFM
class sfr_master_abstract;
virtual task execute(sfr_seq_item item);
endtask
endclass
// The implementation for the SFR master BFM, containing the concrete implementation of the sfr_master
module sfr_master_bfm #(ADDR_WIDTH = 8, DATA_WIDTH = 8)
(input clk,
input reset,
output logic[ADDR_WIDTH-1:0] address,
output logic[DATA_WIDTH-1:0] write_data,
input logic[DATA_WIDTH-1:0] read_data,
output logic we,
output logic re);
import uvm_pkg::*;
import sfr_agent_pkg::*;
always @(reset or posedge clk) begin
if(reset == 1) begin
re <= 0;
we <= 0;
address <= 0;
write_data <= 0;
end
end
// Definition of the concrete version of the sfr_master class:
class sfr_master_concrete extends sfr_master_abstract;
task execute(sfr_seq_item item);
if(reset == 1) begin
wait(reset == 0);
end
else begin
@(posedge clk);
address = item.address;
we <= item.we;
write_data <= item.write_data;
re <= item.re;
@(posedge clk);
if(re == 1) begin
item.read_data = read_data;
re <= 0;
end
we <= 0;
end
endtask: execute
endclass
// Declaration of the SFR_MASTER class handle
sfr_master_concrete SFR_MASTER;
// Initial block, constructing the SFR_MASTER object and assigning the handle into the uvm_config_db
// Note the use of the sfr_master_abstract handle as the uvm_config_db parameter
initial begin
SFR_MASTER = new();
uvm_config_db #(sfr_master_abstract)::set(null, "uvm_test_top", "SFR_MASTER", SFR_MASTER);
end
endmodule: sfr_master_bfm
注意,在上面的代码中,BFM已经从接口变成了模块,这是使用抽象-具体模式实现的另一种自由。模块允许BFM具有层次结构。
在SFR agent类中,任何对BFM的引用都是通过sfr_master_abstract类型的句柄进行的:
class sfr_driver extends uvm_driver #(sfr_seq_item);
// Abstract version of the sfr_master class:
sfr_master_abstract SFR;
extern task run_phase(uvm_phase phase);
endclass: sfr_driver
task sfr_driver::run_phase(uvm_phase phase);
sfr_seq_item item;
forever begin
seq_item_port.get_next_item(item);
SFR.execute(item);
seq_item_port.item_done();
end
endtask: run_phase
sfr_master_abstract对象的句柄是通过uvm_config_db通过agent配置对象传递的,请注意,在以下代码片段中,sfr_monitor使用相同的连接模式:
class sfr_test extends uvm_component;
sfr_env_config env_cfg;
sfr_config_object sfr_agent_cfg;
sfr_env env;
extern function void build_phase(uvm_phase phase);
extern task run_phase(uvm_phase phase);
endclass: sfr_test
function void sfr_test::build_phase(uvm_phase phase);
env_cfg = sfr_env_config::type_id::create("env_cfg");
sfr_agent_cfg = sfr_config_object:: type_id::create("sfr_agent_cfg");
// Assigning the handle to the concrete implementations of the sfr_master and sfr_monitor classes to their abstract counterparts
if(!uvm_config_db #(sfr_master_abstract)::get(this, "", "SFR_MASTER", sfr_agent_cfg.SFR_MASTER)) begin
`uvm_error("BUILD_PHASE", "Unable to find virtual interface sfr_master_bfm in the uvm_config_db")
end
if(!uvm_config_db #(sfr_monitor_abstract)::get(this, "", "SFR_MONITOR", sfr_agent_cfg.SFR_MONITOR)) begin
`uvm_error("BUILD_PHASE", "Unable to find virtual interface
sfr_master_bfm in the uvm_config_db")
end
sfr_agent_cfg.is_active = 1;
env_cfg.sfr_agent_cfg = sfr_agent_cfg;
env = sfr_env::type_id::create("env", this);
env.cfg = env_cfg;
endfunction: build_phase
当使用抽象-具体类连接模式时,agent不需要参数化,因为类句柄用于建立连接。
总之,抽象具体类方法是通过将类对象句柄从testbench的hdl_top部分传递给UVM testbench来工作的。
参数化test
介绍
SystemVerilog提供了许多方法来通过不同的代码结构传递可变的值。一些可变的值必须在elaboration时固定下来,而其他值则可以在开始仿真后的run-time更改。在elaboration时固定的可变值可以使用SystemVerilog参数或`define宏表示。在模块或接口的多个实例中,每个实例都需要不同的可变值,使用`define宏会导致复杂情况。而SystemVerilog参数则更合适,这也是传递可变“type”值以及指定数据位宽(如数据和地址宽度)的首选机制。
在设计中用于控制宽度的“size”参数值通常也需要在testbench中使用。本文的其余部分说明了在双顶层testbench体系结构中通过参数化UVM test共享参数的方法。
DVCon Papers
本文中的信息来自于Xilinx在DVCon 2011上的一篇会议论文,以及在DVCon 2016上的后续更新论文,该论文适合双顶层testbench架构。本文中的材料是Mentor和Xilinx合作的结果。
关于这两篇paper的更多信息,可参考[1] https://verificationacademy.com/resource/6648
[2] http://events.dvcon.org/2016/proceedings/papers/08_4.pdf
向基于字符串的工厂注册参数化类
参数化类使用`uvm_component_param_utils和`uvm_object_param_utils宏向UVM工厂注册。UVM实际上有两个工厂——一个基于字符串的工厂和一个基于类型的工厂。这些param_utils宏只执行基于类型的工厂的注册。
例如,给定一个名为alu_basic_test #(DATA_WIDTH)的参数化test类,宏调用`uvm_component_param_utils(alu_basic_test #(DATA_WIDTH))将展开为:
typedef uvm_component_registry #(alu_basic_test #(DATA_WIDTH)) type_id;
// <-- Default null string used for unspecified 2nd string parameter
static function type_id get_type();
return type_id::get();
endfunction
virtual function uvm_object_wrapper get_object_type();
return type_id::get();
endfunction
上面代码中的typedef创建了uvm_component_registry类型的专门化,但是该类型有两个参数,第一个是正在用基于类型的工厂注册的类型(在本例中是alu_basic_test #(DATA_WIDTH)),第二个是字符串名称,用于在基于字符串的注册表中唯一标识该类型。由于`uvm_component_param_utils宏没有为第二个参数提供值,所以它默认为空字符串,并且不执行基于字符串的注册。
有时,你可能希望使用基于字符串的工厂来创建组件(或对象)。使用基于字符串的工厂的最常见情况是在调用run_test()期间。run_test()调用使用它的字符串参数或+UVM_TESTNAME = 字符串值从基于字符串的工厂创建组件。
由于默认情况下,参数化的UVM组件没有向基于字符串的工厂注册(每个上面的示例),因此您需要为顶层test类实现基于字符串的注册,以便它们可以通过run_test()实例化。要实现这一点,您必须手动编写`uvm_component_param_utils宏执行的操作。
要执行基于字符串的注册,需要为第二个参数值提供一个字符串,该字符串对于test类的每个专门化都是唯一的。你可以将typedef重写为:
typedef uvm_component_registry #(alu_basic_test #(DATA_WIDTH), "basic_test1") type_id;
// <-- Unique string specified as 2nd parameter
此外,必须声明参数化test类的“dummy”专门化,以便上面指定的字符串名称与特定的参数值绑定。
module hvl_top();
parameter DATA_WIDTH = 4;
// Associate the string "basic_test1" with the value of DATA_WIDTH
typedef alu_basic_test #(DATA_WIDTH) dummy;
initial begin
run_test("basic_test1");
end
endmodule
注意:您可以使用下面描述的宏来生成字符串名称,例如“basic_test_#(8)”,而并非像上面的“basic_test1”这样的名称,并将实际参数值作为字符串的一部分。
双顶层共享参数
如上所示,hvl_top模块包含一个在模块中定义的参数DATA_WIDTH。看看hdl_top,它也有一个名为DATA_WIDTH的参数。
module hvl_top();
parameter DATA_WIDTH = 4;
//Pin and BFM interface instantiations using DATA_WIDTH
alu_rtl #(DATA_WIDTH) dut ( /* port connections */ );
...
endmodule
因此,如果这个参数更改,则需要在两个位置进行更新。这个问题可以通过使用共享参数package来解决,在一个package中定义参数,然后导入hvl_top和hdl_top。
保持参数列表的一致性
许多SystemVerilog参数可以自然地组合在一个概念性的“参数列表”中。这些参数往往一起声明,并在测试环境的许多地方使用。
对参数列表的任何更改,比如添加新参数,通常都需要仔细编辑许多不同文件中的许多不同类,这是一个很容易出错的过程。
可以采用的一种可选技术是创建一组宏,以减少出错的机会并加强一致性。
这些宏遵循最小化变更区域的重用哲学。通过使用宏,在参数列表中有一个定义良好的地方可以进行更改。