Testbench Architecture
UVM Testbench Architecture
UVM testbench 是使用SystemVerilog(动态)类对象与SystemVerilog(静态)接口和结构化层次结构中的模块交互构建的。层次结构由功能层组成,testbench 的中心是被测设计(DUT)。
事务处理器和testbench层次通常完全由SystemVerilog类构建。然而,这种构造风格只针对SystemVerilog仿真器,从而限制了可移植性。使用SystemVerilog类和SystemVerilog接口的另一种风格架构,可以提高执行引擎之间的可移植性。利用这两个SystemVerilog构造,可以在事务器层中进行自然的分离,将一端分组的事务级通信与另一端分组的时控、信号级通信分开。
SV作为验证语言,其最大优势就是利用interface将软件世界(仿真环境)和硬件世界(RTL代码)剥离开来。我们可以利用SV中高级语言属性的优势构造环境,又可以利用其全面继承于verilog的属性与硬件世界交互。利用SV的这一特点,来剥离环境中的事务级通信与RTL中的信号级通信,这就是验证方法学思考的事情。
事务器分区
将不同的抽象级别划分为类似的功能,就形成了一个Dual Top分区的testbench体系结构,其中一个顶层包含所谓的HDL域中所有的时控的、信号级代码,而另一个所谓的HVL/TB顶层包含所有的事务级testbench域代码。由于事务处理器通常位于两个域,因此它们也必须被划分为两个部分。这两部分是一个BFM接口和一个代理类。BFM接口处理信号级代码,而代理类处理常规事务器将执行的任何其他操作。BFM和代理之间通过函数和任务调用进行通信。
虽然这种双顶层测试平台架构可移植性好,但它也在一定程度上降低了建模灵活性。这主要是因为信号级代码被放置到SystemVerilog接口而不是类中。SystemVerilog类提供了强大的面向对象功能,包括SystemVerilog接口所忽略的继承和多态性。
关于双顶层测试平台架构的更多内容可以参考 https://verificationacademy.com/patterns-library/implementation-patterns/environment-patterns/dual-domain-hierarchy-pattern
UVM testbench构建和连接过程
关于构建UVM testbench的文章描述了配置和构建双顶层可移植测试台的所有层的过程。本文提供了一些示例来说明如何构建块级testbench,以及如何将多个块级testbench集成到更高级别的testbench中。
构建UVM Testbench
UVM testbench的首个phase是build phase。在此phase,组成testbench层次结构的uvm_component类被实例化为对象。例化过程自顶向下,在下一层之前构造和配置层次结构的每一层(延迟构造)。
当在HVL顶层模块的initial
块中调用run_test()
方法时,UVM testbench将被激活。这个UVM静态方法有一个字符串参数,该参数根据名称定义要运行的test,并通过UVM工厂构造它。然后,UVM通过调用test类的build方法开始build phase。在执行test的build phase期间,将准备各种testbench组件配置对象,并将这些配置对象中的虚接口分配给相关的testbench接口,然后将配置对象放入UVM配置数据库中。随后,构建下一层次结构。
这里简要提一下,UVM工厂机制的两大属性,一个是override,而另一个就是根据字符串实例化一个类,也就是上面说到的根据输入的test name,run_test去实例化验证环境开始仿真。再重复一点,所谓UVM配置数据库,就是放置所有config_db传递的属性的地方,而config_db的机制在UVM_basics中已经介绍过了。
在层次结构的下一层,将检索上一层准备的相应配置对象,并可能进行进一步的配置。在使用此配置对象来指导下一层次结构的构造和配置之前,可以在当前层次结构中修改它。(这也就是说控制是逐级的,上一层可以修改再往上的层级给以下层级的配置)这样的条件构造就会影响testbench的层次结构。
build phase是自顶向下工作的,因此对testbench层次结构的每个后续级别执行该过程,直到达到最底层级为止。
在build phase完成后,connect phase开始进行所有组件间的连接。与build phase相反,connect phase自底向上工作,从最底层到testbench层次结构的顶部。在connect phase之后,UVM的其余phase将运行到完成(也是自下而上的),在此基础上将控制传递回testbench模块。
注:UVM_basics中我已经提到过,final_phase其实也是自上而下的。
test是构建过程的起点
UVM testbench的构建过程从test类开始,并自顶向下工作。test类构建方法是在build phase第一个被调用的方法,它(即方法实现)决定在UVM testbench上构建什么。其功能是:
- 设置工厂覆盖,以便根据需要将配置对象或组件对象创建为其派生类型
- 创建并配置各个子组件所需的配置对象
- 通过HDL testbench模块给放入配置空间的虚接口句柄赋值
- 构建封装的env配置对象,并将其包含到配置空间中
- 在testbench层次结构中构建test的下层组件,通常是顶层env
对于所有test来说,对于给定的验证环境,在build方法中完成的大部分工作都是相同的,因此建议创建一个test base class,每个test case可以由其扩展。
下面显示的是一个模块级验证环境,用来帮助具体解释test的build过程是如何工作的。这是SPI主机接口DUT的环境,包含两个agent,一个用于APB总线接口,另一个用于SPI接口。关于此示例的build和connect phase的详细描述,请参见“Block Level Testbench Example ”。
Factory Overrides
UVM工厂允许在build过程中用另一个派生类替换其基类。这对于专门化(即定制或扩展)组件行为或配置对象非常有用。必须在构造目标对象之前指定Factory Override,因此在build过程开始时进行Override比较方便。
子组件配置对象
每个容器类组件(如agent或env)都应该有一个配置对象来定义其结构和行为。这些配置对象应该在test的build方法中创建并实现以适应test case的需求。如果子组件的配置很复杂或者很可能更改,那么建议添加一个实现基本(或默认)配置处理的虚函数,然后可以通过在base test类扩展的test case中重写该虚函数来更改配置。
代码语言:javascript复制//
// Class Description:
//
//
class spi_test_base extends uvm_test;
// UVM Factory Registration Macro
//
`uvm_component_utils(spi_test_base)
//------------------------------------------
// Data Members
//------------------------------------------
//------------------------------------------
// Component Members
//------------------------------------------
// The environment class
spi_env m_env;
// Configuration objects
spi_env_config m_env_cfg;
apb_agent_config m_apb_cfg;
spi_agent_config m_spi_cfg;
//------------------------------------------
// Methods
//------------------------------------------
// Standard UVM Methods:
extern function new(string name = "spi_test_base", uvm_component parent = null);
extern function void build_phase( uvm_phase phase );
extern virtual function void configure_apb_agent(apb_agent_config cfg);
extern function void set_seqs(spi_vseq_base seq);
endclass: spi_test_base
function spi_test_base::new(string name = "spi_test_base", uvm_component parent = null);
super.new(name, parent);
endfunction
// Build the env, create the env configuration
// including any sub configurations
function void spi_test_base::build_phase(uvm_phase phase);
// env configuration
m_env_cfg = spi_env_config::type_id::create("m_env_cfg");
// APB configuration
m_apb_cfg = apb_agent_config::type_id::create("m_apb_cfg");
configure_apb_agent(m_apb_cfg);
m_env_cfg.m_apb_agent_cfg = m_apb_cfg;
// The SPI is not configured as such
m_spi_cfg.has_functional_coverage = 0;
m_env_cfg.m_spi_agent_cfg = m_spi_cfg;
uvm_config_db #(spi_env_config)::set(this, "*", "spi_env_config", m_env_cfg);
m_env = spi_env::type_id::create("m_env", this);
endfunction: build_phase
function void spi_test_base::set_seqs(spi_vseq_base seq);
seq.m_cfg = m_env_cfg;
seq.spi = m_env.m_spi_agent.m_sequencer;
endfunction
从配置空间赋值虚接口
在调用UVM run_test()
方法之前,必须通过将 DUT 的顶层 I/O 上的信号连接到 SystemVerilog interface类型的的pin接口上来建立信号的连接。然后pin接口被连接到(driver和monitor)BFM接口,每个BFM接口的句柄由一个虚接口句柄赋值,这个虚接口句柄通过uvm_config_db::set调用传递给test。详细信息请参阅虚接口的文章。
在test的build()
方法中,这些虚接口句柄将被分配给相关组件配置对象中的虚接口句柄。然后,各个组件访问其配置对象中的虚接口句柄,通过方法调用来驱动或监视DUT。**为了保持组件的模块化和可重用性,driver和monitor不应该直接从配置空间检索它们的虚接口句柄,而只从它们的配置对象中检索。**test类中是确保通过配置对象将虚接口赋值给相应验证组件的正确位置。
以下代码显示了在 SPI testbench示例中使用 uvm_config_db::get 方法对 apb_agent 配置对象中的虚拟接口句柄进行赋值:
代码语言:javascript复制// The build method from earlier, adding the apb agent virtual interface assignment
// Build the env, create the env configuration including any sub configurations and
// assign virtual interfaces
function void spi_test_base::build_phase( uvm_phase phase );
// Create env configuration object
m_env_cfg = spi_env_config::type_id::create("m_env_cfg");
// Call function to configure the env
configure_env(m_env_cfg);
// Create apb agent configuration object
m_apb_cfg = apb_agent_config::type_id::create("m_apb_cfg");
// Call function to configure the apb_agent
configure_apb_agent(m_apb_cfg);
// Add the APB driver BFM virtual interface
if ( !uvm_config_db #(virtual apb_driver_bfm)::get(this, "", "APB_drv_bfm", m_apb_cfg.drv_bfm ) ) `uvm_error(...)
// Add the APB monitor BFM virtual interface
if ( !uvm_config_db #(virtual apb_monitor_bfm)::get(this, "", "APB_mon_bfm",
m_apb_cfg.mon_bfm ) ) `uvm_error(...)
...
endfunction: build_phase
嵌入子组件配置对象
配置对象从test通过UVM组件配置空间传递给子组件。它们可以单独传递,使用uvm_config_db::set方法中的path参数来控制哪些组件可以访问这些对象。然而,一个常见的需求是中间组件也需要做一些本地配置。
因此,通过testbench层次结构传递配置对象的一种有效方法是将配置对象以反映层次结构本身的方式嵌入到另一个配置对象中。在testbench上的每个中间级别,该级别的配置对象都被“展开”,以产生它的子配置对象,这些子配置对象被重新配置(如果有必要的话),然后使用uvm_config_db::set传递给相关的子组件。
依据层级嵌套配置对象是一种不错的整合方式,但是笔者觉得逐级传递可以选择直接逐级赋值配置对象句柄的方式,而没有必要调用uvm_config_db::set方法。对于封装好有复用需求的UVC,上文的这种方式无疑是最好的传递方式,所有配置的选择修改都放到test层进行。但是注意:若你的容器类组件并非是复用的最小单元,那么最好不要选用逐级传递的方式来配置以免影响复用。仍然可以依据层级嵌套配置对象,但是均在test层分别set到相应层级,相应层级组件内分别get即可。我觉得这可能是是比较适合复用的方式,灵活性极高。(也可与根据个人需求来在顶层组件中来进行底层组件配置对象的非直线获取)
按照SPI模块级环境示例,每个agent都有一个单独的配置对象。env配置对象有每个agent配置对象的句柄。在测试中,从test case的角度构造和配置所有三个配置对象,并将agent配置对象赋值给env配置对象中的相应agent配置对象句柄。随后,将env配置对象添加到配置空间中,以便在稍后构建env时检索。
对于更复杂的环境,则需要额外的封装级别。
代码语言:javascript复制//
// Configuration object for the spi_env:
//
//
// Class Description:
//
//
class spi_env_config extends uvm_object;
// UVM Factory Registration Macro
//
`uvm_object_utils(spi_env_config)
//------------------------------------------
// Data Members
//------------------------------------------
// Whether env analysis components are used:
bit has_functional_coverage = 0;
bit has_spi_functional_coverage = 1;
bit has_reg_scoreboard = 0;
bit has_spi_scoreboard = 1;
// Configurations for the sub_components
apb_config m_apb_agent_cfg;
spi_agent_config m_spi_agent_cfg;
//------------------------------------------
// Methods
//------------------------------------------
extern function new(string name = "spi_env_config");
endclass: spi_env_config
function spi_env_config::new(string name = "spi_env_config");
super.new(name);
endfunction
//
// Inside the spi_test_base class, the agent config handles are assigned:
//
// The build method from earlier, adding the apb agent virtual interface assignment
// Build the env, create the env configuration including any sub configurations and
// assign virtual interfaces
function void spi_test_base::build_phase( uvm_phase phase );
// Create env configuration object
m_env_cfg = spi_env_config::type_id::create("m_env_cfg");
// Call function to configure the env
configure_env(m_env_cfg);
// Create apb agent configuration object
m_apb_cfg = apb_agent_config::type_id::create("m_apb_cfg");
// Call function to configure the apb_agent
configure_apb_agent(m_apb_cfg);
// Adding the APB monitor BFM virtual interface:
if ( !uvm_config_db #(virtual apb_monitor_bfm)::get(this, "", "APB_mon_bfm", m_apb_cfg.mon_bfm ) ) `uvm_error(...)
// Adding the APB driver BFM virtual interface:
if ( !uvm_config_db #(virtual apb_driver_bfm)::get(this, "", "APB_drv_bfm", m_apb_cfg.drv_bfm ) ) `uvm_error(...)
// Assign the apb_agent config handle inside the env_config:
m_env_cfg.m_apb_agent_cfg = m_apb_cfg;
// Repeated for the spi configuration object
m_spi_cfg = spi_agent_config::type_id::create("m_spi_cfg");
configure_spi_agent(m_spi_cfg);
// Adding the SPI driver BFM virtual interface
if ( !uvm_config_db #(virtual spi_driver_bfm)::get(this, "", "SPI_drv_bfm", m_spi_cfg.drv_bfm ) ) `uvm_error(...)
// Adding the SPI monitor BFM virtual interface
if ( !uvm_config_db #(virtual spi_monitor_bfm)::get(this, "", "SPI_mon_bfm", m_spi_cfg.mon_bfm ) ) `uvm_error(...)
m_env_cfg.m_spi_agent_cfg = m_spi_cfg;
// Now env config is complete set it into config space
uvm_config_db #( spi_env_config )::set( this , "*", "spi_env_config",m_env_cfg) );
// Now we are ready to build the spi_env
m_env = spi_env::type_id::create("m_env", this);
endfunction: build_phase
构建下一层次结构
test构建过程的最后一个阶段是使用UVM工厂实例化下一级别的testbench层次结构。这通常意味着构建顶级env,但可能有多个env,或者可能存在条件构建,可以在几个env之间进行选择。
编码规范-工厂实例化方法的名称参数应匹配local句柄
create()
方法有两个参数,一个是名称字符串,另一个是指向父uvm_component类对象的指针。这些参数的值用于在链表中创建一个条目,UVM使用该条目在伪层次结构中定位uvm_component。此列表用于消息传递和配置机制。按照约定,name参数字符串应该与组件的声明句柄相同,parent参数应该是关键字“this”,以便它引用创建它的uvm_component。使用与句柄相同的名称有助于交叉引用路径和句柄。例如,在前面的代码片段中,使用声明句柄m_env在test中创建了spi_env,因此在build过程中,spi_env的UVM“动态路径”名称是“spi_test.m_env”。
分层build过程
UVM中的build phase自顶向下工作。一旦构造了test类,其build()
方法将被调用,然后是它的每个子组件的build()
方法,以此类推,直到构造了完整的环境层次结构。这种延迟的构造方法使每个build()
方法能够影响层次结构中较低层次的组件的bulid过程中执行的动作。例如,如果将agent配置为passive,则agent的build过程将省略实例化agent的sequencer和driver。
分层连接过程
一旦build_phase完成,UVM testbench组件层次结构就就位了,单个组件已经被构造并链接到组件层次结构链表中。UVM connect phase在build phase之后,从层次结构的底部向上工作。其目的是在组件之间建立TLM连接,赋值虚接口句柄,并为寄存器模型等资源进行其他赋值。
配置对象再次在连接过程中发挥作用,因为它们可能包含对虚接口或其他引导连接过程的信息的引用。例如,在一个agent内部,只有在agent是active的情况下,才会将虚接口赋给一个driver,以及driver与其sequencer之间的TLM连接。
例子
UVM build phase可以通过一些示例来很好地说明,这些示例说明了不同的组件层次结构是如何构建的:
- 包含agent的模块级testbench
- 集成级testbench
Sequencer-Driver连接|连接Sequencer和Driver
通过在sequencer中实现的双向TLM通信机制,可以促进sequence及其目标driver之间的请求和响应item的传输。uvm_driver类包含一个uvm_seq_item_pull_port,它应该被连接到与driver相关联的sequencer中的uvm_seq_item_pull_export。port和export类使用将用于请求和响应transaction的sequence_item的类型进行参数化。一旦建立了port-export连接,driver代码就可以使用export中实现的API从sequence中获取req sequence_item,并向其返回响应。
在 connect phase,使用TLM连接方法连接driver port和sequencer export:
代码语言:javascript复制// Driver parameterized with the same sequence_item for request & response
// response defaults to request
class adpcm_driver extends uvm_driver #(adpcm_seq_item);
....
endclass: adpcm_driver
// Agent containing a driver and a sequencer - uninteresting bits left out
class adpcm_agent extends uvm_agent;
adpcm_driver m_driver;
adpcm_agent_config m_cfg;
// uvm_sequencer parameterized with the adpcm_seq_item for request & response
uvm_sequencer #(adpcm_seq_item) m_sequencer;
// Sequencer-Driver connection:
function void connect_phase(uvm_phase phase);
if(m_cfg.active == UVM_ACTIVE) begin
// The agent is actively driving stimulus
// Driver-Sequencer TLM connection
m_driver.seq_item_port.connect(m_sequencer.seq_item_export);
m_driver.vif = cfg.vif;
// Virtual interface assignment
end
endfunction: connect_phase
driver和sequencer之间的连接通常是在agent的connect_phase()
方法中进行的。使用标准的UVM driver和sequencer基类,driver和sequencer之间的TLM连接是一对一的连接——多个driver不会连接到一个sequencer,多个sequencer也不会连接到一个driver。
除了这个双向TLM端口外,driver中还有一个analysis_port,可以连接到sequencer中的analysis_export,实现driver和sequencer之间的单向响应通信路径。这是一个历史工件,并提供了通常不使用的冗余功能。双向TLM接口提供了所需的所有功能。如果使用该analysis_port,则连接方式如下:
代码语言:javascript复制// Same agent as in the previous bidirectional example:
class adpcm_agent extends uvm_agent;
adpcm_driver m_driver;
uvm_sequencer #(adpcm_seq_item) m_sequencer;
adpcm_agent_config m_cfg;
// Connect method:
function void connect_phase(uvm_phase phase );
if(m_cfg.active == UVM_ACTIVE) begin
// Always need the driver-sequencer TLM connection
m_driver.seq_item_port.connect(m_sequencer.seq_item_export);
// Response analysis port connection
m_driver.rsp_port.connect(m_sequencer.rsp_export);
m_driver.vif = cfg.vif;
end
//...
endfunction: connect_phase
endclass: adpcm_agent
请注意,必须始终建立双向TLM连接才能影响req的通信。rsp_port一个可能的使用模型是在driver返回响应时通知其他组件,否则就不需要它了。
END