UVM_COOKBOOK学习【UVM基础】

关注微信公众号摸鱼范式,后台回复COOKBOOK获取COOKBOOK原本和译本 PDF度盘链接

这一部分主要介绍一些UVM的基础知识,其实《UVM实战》中已经有了足够的涉猎。所以这一章着重加注一些UVM的使用哲学探讨。

Testbench基础

UVM采用分层的、面向对象的方法进行testbench开发,允许在不同的团队成员之间进行“关注点分离”。UVM testbench中的每个组件都有特定的用途,以及和testbench其余部分的接口,以提高生产力并促进重用。把这些组件加入到testbench中,就得到一个模块化的可重用验证环境,它使编写测试用例的人在transaction级思考,专注于必须验证的功能,而testbench架构师则专注于test如何与DUT交互。

DUT被连接到一些事务器(driver、monitor、responder)上。这些事务器通过驱动和采样DUT信号在pin级与DUT通信,并通过传递transaction对象与UVM testbench的其余部分通信。它们在接口信号和transaction之间转换数据。在这些事务处理器之上的testbench的部分由专门在事务级进行交互的组件组成,如scoreboard, coverage collector, sequencer等。UVM testbench中的所有组件都是从uvm_component基类扩展而来。

UVM testbench的最低级别是特定于接口的。对每个接口而言,UVM提供一个uvm_agent,其中包括 driver, monitor, sequencer和coverage collector。因此,agent体现了与DUT的所有特定于协议的通信。agent和其他特定于设计的组件封装在uvm_env 组件中,该组件由顶层的uvm_test组件例化并配置。uvm_sequence_item是一个uvm_object,它包含实现协议和与DUT通信所需的数据字段。uvm_driver负责将sequence_item转换为信号级接口上的“pin wiggle”,以向DUT发送或从DUT接收数据。sequence_item由一个或多个uvm_sequence对象提供,这些对象在transaction级定义激励,并在agent的uvm_sequencer组件上执行。sequencer负责执行、仲裁sequences,并在sequences和driver之间传递sequence item。

uvm_sequence_item习惯上被称为transaction,实际上是由从uvm_object派生出的uvm_transaction类派生而来。

tb

uvm_monitor负责观察DUT接口上的信号级行为,并将其转换为sequence items,并将这些sequence items提供给agent或testbench上其他地方(如coverage collectors或scoreboard)中的analysis组件。UVM agent还有一个config object,该对象允许test编写人员在组装和执行testbench时配置agent。

通过为testbench提供统一的接口,UVM agent将testbench和UVM sequence与接口实现的细节隔离开来。例如,提供数据包的sequence可以在不同的UVM agent中重用,这些agent可能实现AHB、PCI或其他协议。UVM testbench通常为DUT的每个接口编写一个对应的agent。

对于给定的设计,UVM Agents和其他组件一起被封装在uvm_env组件中,该组件通常是特定于具体设计的。与agent一样,env通常也有一个与之相关联的config object,该对象允许test对env进行配置,包括在env中例化的agent。由于env本身就是UVM组件,所以也可以将其组装到更高级别的env中。模块级设计被组装成子系统和系统时,模块相关的模块级env可以作为子系统级env中的组件重用,而子系统级env本身也可以在系统级testbench上重用。

一旦定义了env,uvm_test将例化、配置并构建env,包括自定义整个testbench的关键方面,包括选择在env中使用的组件的变化。选择UVM sequences在后台运行或是作为test的主要部分。定义testbench上的env、sub_env(如果有)和agent的config object。

sequence包括验证环境执行的主体sequence以及一些用于响应DUT的挂在后台待调用的sequence。

UVM test通过在顶层initial块中调用run_test()启动。

UVM 组件

UVM testbench由扩展自uvm_component基类的组件对象构成。当创建uvm_component派生类的对象时,其成为testbench层次结构的一部分,会在整个仿真期间持续存在。这就与UVM类层次结构的sequence分支形成了对比,而后者涉及到瞬态对象(动态的),一旦解除引用,就会创建、使用和销毁的对象。

动态的: 笔者认为原文含义为,创建、使用的瞬态对象会在解除使用后(没有句柄指向该对象时,即原对象的句柄指向另一个新对象时)自动销毁。 例如,不同的sequencer上可能会挂载过多个sequence,而同一时间,只有一个sequence对象,执行过的sequence就会被自动销毁。同样地,sequence产生过的transaction(seq item),传递出去之后,就自动销毁。

uvm对象分类简介

UVM消息机制使用(准)静态uvm_component层次结构来打印发出报告消息的组件的作用域,配置过程使用它来确定哪些组件可以访问配置对象,UVM工厂使用它来应用工厂覆盖。这个组件层次结构由一个在创建每个组件时递增构建的链表表示。每个组件的层次位置由在构造时传递给其create方法的name和parent参数决定。

例如,在下面的代码片段中,在spi_env中创建了一个apb_agent组件。假设spi_env在顶层test组件中以名称“m_env”实例化,则agent的分层路径名是与spi_env组件名称“uvm_test_top.m_env”,(".")操作符,以及作为第一个参数传递给create()方法的name的拼接,最终agent的层次名称为"uvm_test_top.m_env.m_apb_agent"。对agent的任何引用都需要使用这个字符串名称。

即这个分层路径名由顶层test实例化名至当前组件的实例化名用(".")操作符逐级拼接。

// Hierarchical name example 

class spi_env extends uvm_env;

	apb_agent m_apb_agent;
// Declaration of the apb agent handle
// ... 

	function void build_phase(uvm_phase phase); 
// Create the apb_agent: 
// 
// Name string argument is the same as the handle name 
// The parent argument is 'this' - i.e. the spi_env 
// 
// The spi_env has a hierarchical path string "uvm_test_top.m_env" 
// is concatenated with the name string to arrive at 
// "uvm_test_top.m_env.m_apb_agent" as the
// hierarchical reference string for the apb_agent 
		m_apb_agent = apb_agent::type_id::create("m_apb_agent", this); 
// ... 
	endfunction: build_phase 
// ... 
endclass: spi_env

uvm_component类继承了uvm_report_object类,而该类位于UVM消息传递基础结构的核心。消息机制使用组件静态层次结构将组件的分层路径名添加到报告消息字符串中(即这条消息是由哪个组件(对象)报告的)。

uvm_component基类模板的每个UVM phase都有一个虚方法,用户可以根据需要实现这些方法。未实现的phase级虚方法将导致组件无法有效地参与该phase。

uvm_component基类中还嵌入了对配置表的支持,以存储与testbench层次结构中组件的子节点相关的配置对象。当使用uvm_config_db API时,这个静态层次结构(分层路径名)被用作路径机制的一部分,以控制哪些组件可以访问给定的配置对象。

为了在配置中提供灵活性并允许以智能的方式构建UVM testbench的层次结构,uvm_components需要被注册到UVM工厂。当在构建阶段创建UVM组件时,将会使用工厂来构造组件对象。UVM工厂允许使用工厂覆盖来把一个组件替换为它的一个兼容的派生类型。这种方式可以在不直接更改testbench源代码的情况下更改testbench的功能(直接更改testbench源代码需要重新编译并会阻碍重用)。工厂工作需要许多编码约定,在关于UVM工厂的内容中对这些约定做了概述。

UVM package包含许多uvm_component基类的扩展(即派生类),用于公共testbench组件。这些扩展中的大多数类都非常“瘦”,也就是说,它们实际上只是uvm_component类的一个小扩展,主要是用来添加一个新的名称空间。虽然这些不是很关键,原则上我们也仍然可以使用 uvm_component 类,但重要的是这些扩展类有助于实现组件的“自我分类”,因为其清楚地表明了实际表示的组件类型,例如driver或monitor。

通俗说就是是UVM强制为环境组件分类做引导,笔者认为UVM倡导的验证环境构建方式趋向于让单个组件处理单一类型的事务,这样环境结构清晰且易于分类维护

此外,还有一些可用的分析工具,它们也使用这些无关的基类来扩展,以帮助建立testbench层次结构的图像。另一方面,一些预构建的uvm_component扩展实际上是结构块,其通过实例化具体的子组件,来实现一组复杂的功能。下表总结了直接从uvm_component基类派生的可用的UVM组件类:

uvm_component

UVM 工厂

UVM工厂的目的是允许用派生类型的对象替换其基类的对象,且不需要更改testbench结构甚至testbench代码。这种机制称为覆盖(override)(按type或实例)。此功能对于更改sequence行为或用组件的一个版本替换另一个版本来说十分方便。但是,要交换的任何两个组件必须是多态兼容的。这就要求所有相同的TLM接口句柄和TLM对象必须由用来替换的派生组件声明及例化。此外,为了利用工厂的优势,我们必须遵循某些编码规范。

override的两种类型:替换这种class的实例,还是只替换该class的某个实例

工厂编码规范1:注册

组件或对象必须包含由以下元素组成的工厂注册代码:

  • 一个uvm_component_registry wrapper,typedef为type_id
  • 获取type_id的静态函数
  • 获取类名的函数

例如:


class my_component extends uvm_component; 

// Wrapper class around the component class that is used within the factory 

	typedef uvm_component_registry #(my_component, "my_component") type_id; 

// Used to get the type_id wrapper 

	static function type_id get_type(); 
		return type_id::get(); 
	endfunction 

// Used to get the type_name as a string 

	function string get_type_name(); 
		return "my_component"; 
	endfunction 
... 
endclass: my_component

注册代码有一个规则的模式,可以安全地用四个工厂注册宏中的一个生成:


// For a component 

class my_component extends uvm_component; 

// Component factory registration macro 

	`uvm_component_utils(my_component) 

// For a parameterized component 

class my_param_component #(int ADD_WIDTH=20, int DATA_WIDTH=23) extends uvm_component;

	typedef my_param_component #(ADD_WIDTH, DATA_WIDTH) this_t; 

// Parameterized component factory registration macro 

	`uvm_component_param_utils(this_t) 

// For a class derived from an object (i.e. uvm_object, uvm_transaction,

// uvm_sequence_item, uvm_sequence etc.)  

class my_item extends uvm_sequence_item; 
	`uvm_object_utils(my_item) 

// For a parameterized object class 

class my_item #(int ADD_WIDTH=20, int DATA_WIDHT=20) extends uvm_sequence_item; 
    
	typedef my_item #(ADD_WIDTH, DATA_WIDTH) this_t 
	`uvm_object_param_utils(this_t) 

工厂编码规范2:构造函数默认值

uvm_component和uvm_object构造函数是带有原型模板的虚方法,用户必须遵守。为了在构建阶段支持延迟构造,工厂构造函数应该包含构造函数参数的默认值。这允许使用默认值在工厂内构建一个工厂注册类,然后可以将类属性重新赋值给通过uvm_component_registry wrapper类的create方法传递的参数。组件和对象的默认值是不同的:


// For a component: 

class my_component extends uvm_component; 

	function new(string name = "my_component", uvm_component parent = null); 
		super.new(name, parent); 
	endfunction 

// For an object: 

class my_item extends uvm_sequence_item; 

	function new(string name = "my_item"); 
		super.new(name); 
	endfunction

工厂编码规范3:组件和对象创建

Testbench组件是在构建阶段使用uvm_component_registry的创建方法创建的。它先构造类,然后在正确地赋值name和parent参数之后,将类的句柄赋值给testbench中的声明句柄。对于组件而言,构建过程是自顶向下的,这就能允许更高级别的组件和配置来控制实际构建的内容。

根据需要使用create方法创建对象,示例见下面的代码:

class env extends uvm_env; 

	my_component m_my_component; 
	my_param_component #(.ADDR_WIDTH(32), .DATA_WIDTH(32)) m_my_p_component; 

// Constructor & registration macro left out 
// Component and parameterized component create examples 

	function void build_phase( uvm_phase phase ); 

		m_my_component = my_component::type_id::create("m_my_component", this); 
		m_my_p_component = my_param_component #(32, 32)::type_id::create("m_my_p_component", this); 
        
	endfunction: build 

	task run_phase( uvm_phase phase ); 

		my_seq test_seq;
		my_param_seq #(.ADDR_WIDTH(32), .DATA_WIDTH(32)) p_test_seq; 

// Object and parameterised object create examples 

		test_seq = my_seq::type_id::create("test_seq"); 
		p_test_seq = my_param_seq #(32,32)::type_id::create("p_test_seq"); 

// ... 

	endtask: run

Phasing

为了有一个一致的testbench执行顺序,UVM使用phases来对仿真期间的主要阶段进行排序。有三组phases,按以下顺序执行:

phase

  1. Build phases 配置和构造 testbench

  2. Run-time phases 在testbench上运行测试用例时消耗时间

  3. Clean up phases 收集和报告测试用例的结果

不同的组的phase如上图所示。uvm_component基类包含被每个不同phase方法调用的虚方法,这些方法由testbench组件创建者根据组件参与的phase来进行填充。使用已定义的phase可以独立地开发验证组件,由于对于每个phase应该进行的工作内容是有共同理解的,故组件间仍然可以互动,

类似于UVM对组件的分类,phase的划分,是UVM从环境的执行顺序角度出发,对仿真程序进行分类,也就是说,UVM不仅倡导在规定的组件中干特定的事,还倡导在相应的时间干相应的事。这样就从空间和时间的角度对testbench进行了良好的规划,最后得到的自然是结构清晰、易于维护的环境。

UVM Phase的执行

要启动UVM testbench,必须从其静态部分调用run_test()方法。我们通常是在testbench的top中的initial块中调用。

调用run_test()构造UVM环境的根组件,然后开始UVM phasing。run_test()方法可以传入一个字符串参数来定义uvm_component派生类的默认类型名,该类被用作testbench层次结构的根节点。而且,run_test()方法会检查一个名为UVM_TESTNAME的命令行附加参数,并使用该附加参数字符串来查找工厂注册的uvm_component,覆盖任何默认类型名称。按照惯例,根节点应该派生自uvm_test组件,但其实它也可以派生自别的任何uvm_component。根节点通过指定testbench组件的配置和由它们执行的激励来定义要执行的测试用例。

比如,指定my_test(这就是执行的测试用例名)为仿真环境根节点。

	`+UVM_TESTNAME=my_test `

Phase 描述

以下部分描述了每个UVM phase的用途。

Build Phases

Build Phases在仿真开始时执行,其主要目的是构造、配置以及连接testbench组件层次结构。所有的Build Phases方法都是函数,因此在零仿真时间内执行。

build

实例化了UVM testbench根节点组件(test)后,就开始执行build phase,它从上到下构造testbench组件层次结构。推迟每个组件的构造,以便于组件层次结构中的每级组件都可以由上面一级配置。build phase使用UVM工厂机制来间接实例化uvm_components。

connect

connect phase用于在组件之间建立TLM连接或为testbench资源赋值句柄。connect phase会在build方法将testbench组件按照层次结构例化之后自底向上执行。

end_of_elaboration

end_of_elaboration phase用于在仿真开始之前对testbench的结构、配置或连接性做出最终调整。它的实现可以假设testbench组件层次结构实现和内部连接已经就绪。也是自底向上执行。

Run Time Phases

testbench激励在build phases之后的Run Time Phases生成并执行。在start_of_simulation phase之后,UVM并行执行run phase和pre_reset到post_shutdown phase。run phase出现在OVM中,并被保留以允许OVM组件可以轻松迁移到UVM。这也是事务处理器运行的phase。UVM中添加了其他的run time phases,以便于为tests、scoreboards和其他类似组件提供更细的run time phase粒度。对绝大多数testbench来说只使用reset, configure, main和shutdown phase就足够了,没有必要使用相应的pre和post phase。

start_of_simulation

start_of_simulation phase是一个函数,它在testbench耗时的部分开始之前执行。它是用来打印(开始)标志,testbench的拓扑结构或配置信息的。同样是自底向上调用。

笔者不明白UVM cookbook为什么将属于function的start_of_simulation phase归类于Run Time Phases,或许是作为run time phases 开始的标志?

run

run phase在start_of_simulation phase之后执行,它用于生成激励和检查testbench的活动。run phase为 task phase,并且所有uvm_component的run_phase()都是并行执行的。driver和monitor等事务处理器几乎一直使用这个phase。

VIP的编写基本上都只使用run phase,而不使用其他run time phase,这主要是因为考虑到VIP和其所集成到的testbench的兼容性。

Parallel Run-Time Phases

注意:以下run-time phases 是与run_phase并行执行的。这些phase应该只在test和env中调用来启动sequence,而在driver、monitor和其他组件中则不应该实现这些phase。

pre_reset

这一phase的用途不大。

reset

reset phase 用于实现DUT或接口的特定复位行为。例如,此phase用于生成一个reset并将interface置于其default状态。

post_reset

这一phase的用途不大。

pre_configure

这一phase的用途不大。

configure

configure phase用于对testbench上的DUT和memory进行编程,便于准备执行测试用例。它还可用于将信号设置为准备执行测试用例的状态。

post_configure

这一phase的用途不大。

pre_main

这一phase的用途不大。

main

这是由测试用例指定的激励被生成并应用到DUT的地方。它在所有激励都耗尽或者发生超时后结束。最多的数据吞吐量将由这个phase开始的sequence来处理。(主要产生激励的phase)

post_main

这一phase的用途不大。

pre_shutdown

这一phase的用途不大。

shutdown

shutdown phase用于确保在main phase产生的激励已经施加给DUT且驱动完成,任何的结果数据都已经耗尽。它也可以用于执行读取状态寄存器的耗时sequence。

post_shutdown

这一phase的用途不大。

Clean Up Phases

clean up phases 用于从scoreboard和functional coverage monitor中提取信息,以确定测试用例是否已经通过并且达到了预期覆盖目标。clean up phases 是作为函数实现的,所以执行时间为零。它们自底向上执行。

查看UVM源码可以发现,final phase是派生自uvm_topdown_phase的(同build phase),应该是自上向下执行的。我觉得是因为UVM希望上层模块能够控制下层模块,若上层模块中调用$finish结束仿真,那么下层模块的final phase便不会执行。所以不太理解为什么原文说clean up phases 都是自底向上执行的。

extract

extract phase用于从scoreboard和functional coverage monitor中检索和处理信息。包括计算report phase所使用的统计信息。通常会在analysis组件中使用这个phase。

check

check phase用来检查DUT的行为是否正确,并检查在testbench运行期间可能发生的任何错误。这个phase通常在analysis组件中使用。

report

report phase用于打印仿真结果或是将结果写入文件中。这个phase通常在analysis组件中使用。

final

final phase可以用于完成testbench尚未完成的任何未完成的操作。

个人觉得可能除非是上层提前调用$finish结束仿真,不然感觉没什么用。report phase打印仿真结果后基本就认为仿真结束了。

UVM Driver

概述

sequence产生激励(sequence_item),sequencer与driver通过TLM 通信机制交互(发送sequence产生的激励给driver,接收driver回给的response),然后driver将激励转换为接口级的信号并通过虚接口与DUT交互。也就是说,环境中的信息传递是transaction级的,driver负责进行transaction级和接口信号格式之间的转换并按照接口协议驱动DUT。当然,一个driver也可以作为一个“responder”(类似“slave模式”),对接口中的pin级信号驱动作出响应,并与sequence通信,然后sequence产生一个response transaction回给driver,来完成协议。当driver的agent被配置为passive模式(只monitor,不drive)时,根据定义,driver不会在agent中实例化。

也就是说,driver可以只驱动DUT,也可以通过接受分析DUT驱动到接口上的信号来反馈给sequence,sequence来根据反馈决定发送何种transaction给driver来驱动DUT。driver是作为借助虚接口与DUT进行双向交互的媒介。

剖析 UVM Driver

用户定义的driver是一个派生自uvm_driver基类的代理类,它包含一个是SystemVerilog接口的BFM。uvm_driver基类提供了一个seq_item_port,该seq_item_port由agent连接到agent的uvm_sequencer的seq_item_export。通常,responses也通过seq_item_port传递回sequence,但某些应用程序可能需要通过driver的rsp_port将responses发送回sequencer。有关将driver连接到sequencer的详细讨论,可以参阅Sequencer-Driver的连接。

driver的seq_item_port和sequencer的seq_item_export是UVM做好的,而二者的连接工作则需要使用者手动进行。

UVM Driver API

uvm_driver最终被设计为与连接的uvm_sequencer上运行的uvm_sequence交互。本文中描述了这个API的细节。

UVM Driver Use Models

UVM中的激励产生依赖于sequence和driver之间的耦合。sequence只能在已知driver特征的情况下编写,否则sequence或driver就有可能陷入死锁,一直等待另一方提供item。我们可以通过提供一组可与driver一起使用的基本的实用的sequence,并记录driver行为的方式来缓和死锁的问题。

Sequencer-Driver组合存在大量潜在的激励的生成使用模型,本文将对其中的大部分进行讨论。

UVM monitor

概述

testbench分析部分的第一个任务是监测DUT上的活动。和driver一样,monitor也是agent的组成部分。类似于driver组件,执行的也是实际信号活动和该活动的抽象表示之间的转换(接口上的信号变化翻译成环境中的transaction)。Monitor和Driver之间的关键区别是Monitor总是被动的,不驱动接口上的任何信号。当agent处于passive模式时,Monitor仍将执行。

Monitor通过虚接口与DUT信号通信,并且包含识别信号活动中的协议模式的代码。一旦识别出协议模式,Monitor将构建表示该信号活动的transaction模型,并将transaction广播给任何感兴趣的组件。

构造

monitor由从uvm_monitor扩展而来的代理类和BFM (SystemVerilog接口)组成。这个代理应该有一个analysis port 和一个指向BFM接口的虚接口句柄。


class wb_bus_monitor extends uvm_monitor; 

	`uvm_component_utils(wb_bus_monitor) 
	uvm_analysis_port #(wb_txn) wb_mon_ap; 
	virtual wb_bus_monitor_bfm m_bfm; //BFM handle
	wb_config m_config; 

// Standard component constructor 

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

	function void build_phase( uvm_phase phase ); 

		wb_mon_ap = new("wb_mon_ap", this); 
		m_config = wb_config::get_config(this); // get config object
		m_bfm = m_config.WB_mon_bfm; // set local virtual if property
		m_bfm.proxy = this; //Set BFM proxy handle
      
	endfunction 

	task run_phase(uvm_phase phase); 
		m_bfm.run(); //Don't start the BFM until we get to the run_phase
	endtask 
    
	function void notify_transaction(wb_txn item); //Used by BFM to
		return transactions 
		wb_mon_ap.write(item); 
	endfunction : notify_transaction 
endclass 
interface wb_bus_monitor_bfm (wishbone_bus_syscon_if wb_bus_if);     
	import wishbone_pkg::*;     
//------------------------------------------ 
// Data Members 
//------------------------------------------    
	wb_bus_monitor proxy;     
//------------------------------------------ 
// Methods 
//------------------------------------------ 
	task run();     
		wb_txn txn;     
        forever @ (posedge wb_bus_if.clk)  begin    
//Capture protocol pin activity into txn     
		proxy.notify_transaction(txn);     
		end     
	endtask     
endinterface

Passive 监测

正如在科学实验中,观察的行为不应该影响所观察的活动,监测组件应该是passive的。它不应该向DUT注入任何激励。这也就意味着monitor代码在与DUT信号交互时应该是完全只读的(只采样DUT的信号)。此外,BFM除非被调用,否则将不会观察数据,以确保BFM与UVM phasing一致。

简单来说,这段话映射的还是UVM的组件分类哲学,monitor就只monitor,不进行除此之外的工作(比如驱动DUT,对比结果等)。另外就是其行为完全要由phase机制调度,来保证在特定的时间段进行特定的工作。

识别协议

monitor必须了解协议才能检测信号活动中的可识别模式。可以通过在monitor BFM 的 run() task中编写特定于协议的状态机代码来完成检测。 这部分代码通过监测虚接口来等待目标信号活动。

例如,最简单的,检测SoC验证中外设的中断信号,或是识别系统总线上的一笔读写操作。

创建事务级对象

一旦识别出接口上的特定操作,monitor将构建一个或多个抽象地表示信号活动的transaction。这可以通过调用一个函数并将特定于transaction的属性(例如数据和地址)作为参数传递给函数来实现,或者通过在检测到现有transaction时在其上设置属性来实现。

这也就是说,事务级对象的构建通过建立与特定信号活动之间的映射来实现。

即写即拷贝方针

由于SystemVerilog中的对象是基于句柄的,所以当Monitor从其分析端口写入transaction句柄时,只有该句柄被复制并广播给接收的组件。每次 Monitor 在 run() task中运行其正在进行的协议识别循环时,都会发生此写入操作。为了防止在循环的下一次迭代中覆盖相同的transaction对象内存,广播的句柄应该指向Monitor创建的transaction对象的单独副本。

这可以通过两种方式实现:

  • 在循环的每次迭代(即循环内部)中创建一个新的transaction对象
  • 在循环的每次迭代中重用相同的transaction对象,但在调用write()之前克隆该对象,并广播克隆的句柄。

笔者关于randc用法的tips:之前我在sequence中使用`uvm_do来例化、随机、发送transaction,发现randc不生效,并不是所有值都出现之后才开始重复下一轮。后来意识到,`uvm_do每次都会重新例化transaction,这样对于单个的transaction来说,相当于只随机了一次,而randc是只针对于同一transaction对象的(随机调用的是对象的randomize函数)。我当时采用的方式是只实例化transaction一次,但是并没有出现上文提到的transaction被覆盖的问题,显然这是因为driver未索取transaction时,sequence不会发新的transaction给driver,所以不存在会被覆盖的问题。但若是那种会缓存多笔transaction的通信场景(例如使用analysis fifo),那就会出现原文中的问题。如果此时我们还想randc生效,那显然应该采取上文中提到的第二种方式。每次随机都使用的是同个对象,但是采用clone对象的方式来发送transaction。

广播transaction

一旦构建了一个新的或克隆的transaction,就应该通过写入analysis port的方式广播给所有需要接收该transaction的组件。

Monitor 示例

//Full run task from monitor BFM 
task run(); 
	wb_txn txn;
    forever @ (posedge wb_bus_if.clk) begin
		if(wb_bus_if.s_cyc) begin
// Is there a valid wb cycle?
			txn = wb_txn::type_id::create("txn");
// create a new wb_txn
			txn.adr = wb_bus_if.s_addr;
// get address
			txn.count = 1;
// set count to one read or write
			if(wb_bus_if.s_we) begin 
// is it a write?
				txn.data[0] = wb_bus_if.s_wdata;
// get data
				txn.txn_type = WRITE;
// set op type
				while (!(wb_bus_if.s_ack[0] | wb_bus_if.s_ack[1]|wb_bus_if.s_ack[2])) 
				@ (posedge wb_bus_if.clk);
// wait for cycle to end
			end 
		else begin 
			txn.txn_type = READ;
// set op type
			case (1)
//Nope its a read, get data from correct slave
				wb_bus_if.s_stb[0]: begin 
					while (!(wb_bus_if.s_ack[0])) @ (posedge wb_bus_if.clk);
 //wait for ack 
    				txn.data[0] = wb_bus_if.s_rdata[0];
// get data
				end 
    			wb_bus_if.s_stb[1]: begin         
					while (!(wb_bus_if.s_ack[1])) @ (posedge wb_bus_if.clk);         
// wait for ack         
					txn.data[0] = wb_bus_if.s_rdata[1];        
// get data        
				end    
			endcase    
		end 
// else: !if(wb_bus_if.s_we)
	proxy.notify_transaction(txn); 
	end 
endtask 

UVM Agent

UVM agent是用于给定逻辑接口(如APB或USB)的验证组件“套件”。agent包括一个封装了相应的一组接口信号的interface,monitor和driver的BFM,以及一个package,其中包含组成整个agent组件的各种类。agent本身是一个包含sequencer、driver和monitor的容器,也包含其他相关的验证组件,比如functional coverage collector 以及scoreboard(一般不会放置在agent内)。代理只是提供与“普通”类的对象相同API的类的对象。driver代理和monitor代理以寻常的方式与UVM testbench的其余部分通信,同时还通过虚接口句柄分别访问driver和monitor的BFM接口。因此,一个完整的monitor是由monitor代理服务和monitor BFM成对工作组成的,对driver来说也一样。agent还有一个连接到monitor的analysis port 的analysis port ,用户不需要知道agent的内部结构,便可以将外部分析组件连接到agent。agent是testbench上最低级的层次结构块,其确切结构取决于其配置,每个agent的配置(config object)可能因test而异。这些类和接口共同构成一个可移植可重用的agent。

这也就是说agent是与DUT的一组接口强相关的,一般对应的是DUT的一类功能或是协议接口。显然,对于有相同接口的DUT,agent是可重用的。关于代理器和相应BFM的更多内容可参考https://verificationacademy.com/patterns-library/implementation-patterns/environment-patterns/bfm-proxy-pair-pattern

agent-active

接下来我们研究一下APB agent是如何组成、配置、构建和连接的。APB agent的pin接口apb_if编写在apb_if.sv文件中。命名为apb_monitor_bfm的monitor BFM接口有一个apb_if端口。同样地,命名为apb_driver_bfm的driver BFM接口也有一个apb_if端口。BFM定义了与apb_if引脚接口中信号交互的任务和函数。driver和monitor代理不直接访问BFM局域的pin。文件apb_agent_pkg.sv包含package和APB agent的各种类文件。任何使用这个package中文件的组件,比如env,都要import这个package。

package apb_agent_pkg; 

	import uvm_pkg::*; 

	`include "uvm_macros.svh" 
	`include "config_macro.svh" 
	`include "apb_seq_item.svh" 
	`include "apb_agent_config.svh" 
	`include "apb_driver.svh" 
	`include "apb_coverage_monitor.svh" 
	`include "apb_monitor.svh" 

	typedef uvm_sequencer#(apb_seq_item) apb_sequencer; 

	`include "apb_agent.svh" 
//Reg Adapter for UVM Register Model 
	`include "reg2apb_adapter.svh" 
// Utility Sequences 
	`include "apb_seq.svh" 
	`include "apb_read_seq.svh" 
	`include "apb_write_seq.svh" 

endpackage: apb_agent_pkg

注意, 这里的apb_sequencer 类型实际上是一个“typedef”,它使用“sequence_item”类型简单地参数化了“uvm_sequencer”基础组件。

Agent的config object

agent的config object定义了:

  • agent的子组件的拓扑结构(确定要构造哪些子组件)
  • driver和monitor代理使用的BFM虚接口的句柄
  • agent的行为

按照约定,UVM agent config类有一个类型为uvm_active_passive_enum的枚举类型变量,该变量定义了agent是构造了sequencer和driver的UVM_ACTIVE还是二者均未构造的UVM_PASSIVE。这个参数称为active,默认值为UVM_ACTIVE。

而是否构建其他子组件则应该由附加的配置属性来控制,这些属性应该具有描述性名称。例如,如果有一个 functional coverage collector,那么应该有一些控制位来控制是否构建它,比如适当地将其命名为has_functional_coverage。

config object包含driver和monitor使用的BFM虚接口句柄。config object是在test中构造和配置的,在这个顶层,把从testbench module传入的虚接口赋值给虚接口句柄。

agent config object还可以包含其他数据成员,来控制agent的配置方式或行为。例如,APB agent的config object具有数据成员,用于设置内存映射并确定通过关联地址映射拉高的APB PSEL信号。

config类应该将所有配置数据成员默认为通用值。

下面的代码示例展示了APB agent的config object。

// 
// Class Description: 
// 
// 
class apb_agent_config extends uvm_object; 

// UVM Factory Registration Macro 
// 
	`uvm_object_utils(apb_agent_config) 
// BFM Virtual Interfaces
	virtual apb_monitor_bfm mon_bfm;
	virtual apb_driver_bfm  drv_bfm; 
//------------------------------------------ 
// Data Members 
//------------------------------------------ 
// Is the agent active or passive 
	uvm_active_passive_enum active = UVM_ACTIVE; 
// Include the APB functional coverage collector 
	bit has_functional_coverage = 0; 
// Include the APB RAM based scoreboard 
	bit has_scoreboard = 0; 
// 
// Address decode for the select lines: 
// Address decode for the select lines: 
	int no_select_lines = 1; 
	int apb_index = 0;
// Which PSEL is the monitor connected to 
	logic[31:0] start_address[15:0]; 
	logic[31:0] range[15:0]; 
//------------------------------------------ 
// Methods 
//------------------------------------------ 
// Standard UVM Methods: 
	extern function new(string name = "apb_agent_config");

endclass: apb_agent_config 

function apb_agent_config::new(string name = "apb_agent_config"); 
	super.new(name); 
endfunction 

Agent的Build Phase

在agent的build phase发生的操作由其config object的内容决定。第一个操作是获取config object的句柄,然后,当要构造子组件时,相关配置字段的值来决定是否构造。

agent-passive

该规则的例外是monitor代理和monitor BFM,它们总是存在的,因为不管agent是active还是passive,都会使用它们。driver仅在agent处于active模式时构造。driver BFM则需要额外的思考,根据重用目标可以采取如下不同的操作。

Driver BFM 实例化

作为interface,driver BFM是在构造硬件部分代码时实例化和创建的。因此为了防止信号在RTL及testbench的其余部分准备就绪之前被驱动或采样,BFM状态机通常不应该自启动。相反,应该使用来自相应代理的启动指示作为BFM开始其活动的trigger。这就允许BFM被同步到标准的UVM Phasing机制中,并且如果agent被配置为passive,driver还会保持静默。除了保持静默,driver在passive模式下也不能驱动信号。这意味着要么driver必须使所有输出默认处于三态('hz)模式,要么driver在passive模式下不能被实例化。

要决定是否实例化driver,需要考虑所需的agent重用的级别。对于简单的block-to-top重用(RTL代码代替了driver的功能),driver在任何情况下都不需要,也不应该被实例化。如果driver BFM还可能需要驱动信号,那么必须实例化,以及使用三态信号值。要控制实例化,参数可以与生成语句一起使用。

Agent的Connect Phase

构建了agent的子组件后,就需要将它们连接起来。通常需要的agent连接有:

  • monitor的analysis port到agent的analysis port

  • sequencer的seq_item_pull_export到driver的seq_item_pull_port (仅对active agent而言)

  • 分析子组件的analysis export到monitor的analysis port(存在分析子组件的地方)

  • monitor/driver的代理虚接口到configobject的monitor/driver的BFM虚接口

注意:

  1. agent的analysis port句柄可以直接由monitor的analysis port赋值。这样就不必在agent中再例化单独的analysis port对象。(句柄传递,指向同一个analysis port对象)

  2. 在agent中给driver代理器和monitor代理器虚接口赋值移除了这些子组件具有配置表查找开销的需要。

    ​ 第二点这段话的意思是说在agent中赋值driver和monitor的虚接口,就不需要在这两个子组件中通过config_db的方式去get了。

APB agent的以下代码说明了config object如何确定在build phase和connect phase进行的工作:

// 
// Class Description: 
// 
// 
class apb_agent extends uvm_component; 
// UVM Factory Registration Macro 
// 
	`uvm_component_utils(apb_agent) 
//------------------------------------------ 
// Data Members 
//------------------------------------------ 
	apb_agent_config m_cfg; 
//------------------------------------------ 
// Component Members 
//------------------------------------------ 
	uvm_analysis_port #(apb_seq_item) ap; 
	apb_monitor m_monitor; 
	apb_sequencer m_sequencer; 
	apb_driver m_driver; 
	apb_coverage_monitor m_fcov_monitor; 
//------------------------------------------ 
// Methods 
//------------------------------------------ 
// Standard UVM Methods: 
	extern function new(string name = "apb_agent", uvm_component parent = null); 
	extern function void build_phase( uvm_phase phase ); 
	extern function void connect_phase( uvm_phase phase );

endclass: apb_agent
        
function apb_agent::new(string name = "apb_agent", uvm_component parent = null); 
	super.new(name, parent); 
endfunction 
        
function void apb_agent::build_phase( uvm_phase phase ); 
	if (m_cfg == null) 
	if( !uvm_config_db #( apb_agent_config )::get(this, "", 
                                                  "apb_agent_config",m_cfg) )  
		`uvm_fatal(...) 
// Monitor is always present 
	m_monitor = apb_monitor::type_id::create("m_monitor", this); 
// Only build the driver and sequencer if active 
	if(m_cfg.active == UVM_ACTIVE) begin 
		m_driver = apb_driver::type_id::create("m_driver", this); 
		m_sequencer = apb_sequencer::type_id::create("m_sequencer", this); 
	end 
	if(m_cfg.has_functional_coverage) begin 
		m_fcov_monitor = apb_coverage_monitor::type_id::create("m_fcov_monitor", this); 
	end 
endfunction: build_phase 
        
function void apb_agent::connect_phase(uvm_phase phase); 
	ap = m_monitor.ap; 
// Only connect the driver and the sequencer if active 
	if(m_cfg.active == UVM_ACTIVE) begin 
		m_driver.seq_item_port.connect(m_sequencer.seq_item_export); 
	end 
	if(m_cfg.has_functional_coverage) begin 
		m_monitor.ap.connect(m_fcov_monitor.analysis_export); 
	end 
endfunction: connect_phase

UVM Sequences

sequence概述

传统的Verilog或VHDL编写的testbench很难支持有约束的随机激励,test case通常都是'hard-wired' ,很难更改并运行一个新的test case,一般来说是要重新编译的。而UVM Sequences则带来了一种面向对象的激励生成的方式,十分灵活,为生成激励提供了新的选择。

stimulus_gen

sequence是一个被用作方法的对象。UVM sequence包含一个叫做body的task。当使用sequence时,实例化它,之后执行body方法,然后就可以丢弃该sequence。与uvm_component不同,sequence具有有限的仿真生命周期,因此可以称为瞬态对象。sequence的body方法可以用来实例化和执行其他sequence,或者用来生成sequence_item对象(这些对象通过sequencer发送到driver,用于驱动DUT。),当然也可以同时包含这两种使用方式。sequence_item对象也是瞬态对象,它们包含了driver与DUT进行交互所需要的信息。当DUT生成响应时,driver使用sequence_item将响应信息通过sequencer回传给原sequence。实例化和执行其他sequence实际上与能够调用常规子线程相同,因此可以通过将简单sequence链接在一起来构建复杂的功能。

在类的继承关系方面,uvm_sequence继承于uvm_sequence_item, uvm_sequence_item继承于uvm_object。这两个基类都被称为对象而并非组件。UVM testbench组件层次结构是由具有不同属性的uvm_component构建的,这些组件主要是在实例化时将它们绑定到静态组件层次结构中,并且组件层次结构在仿真的整个生命周期中都保持不变。

上文注释中也提到过,uvm_sequence_item实际上是派生于uvm_transaction的,而uvm_transaction才是直接由uvm_object派生。

uvm_inheritance_diagram

sequence是UVM中产生激励的主要手段。事实上,sequence和sequence_items都是对象,这意味着它们可以很容易地随机化来产生有趣的激励。其面向对象的特性也意味着可以像其他任何对象一样被操作。UVM架构也使这些类与testbench组件层次结构分开,优点是通过调用和执行来自库package的不同sequence组合,可以轻松定义新的test case,而不会被锁定到组件范围内可用的方法。但缺点是,sequence不能直接访问在组件层次结构中可用的testbench资源,比如配置信息,或是寄存器模型的句柄。故sequence使用sequencer作为进入组件层次结构的媒介来对testbench资源进行访问。

在UVM sequence体系结构中,sequence负责激励产生流程,并通过sequencer将sequence_items发送给driver。driver负责将sequence_items中包含的信息转换为pin级活动。sequencer是实现通信通道和仲裁机制的中间组件,来促进sequence和driver之间的交互。数据流是双向的,请求项通常会从序列路由到驱动程序,而响应项将从驱动程序返回到序列。通信接口的sequencer端在connect phase与driver端连接。

UVM Sequence Items

sequence中的UVM激励生成程序通过生成 sequence_items 并经由sequencer将它们发送给driver来控制driver的行为。 激励生成的框架是围绕sequence结构构建的,但是数据的生成则是使用sequence_items作为数据对象。 由于 sequence_items 是构建sequence的基础,因此在设计时需要注意一些问题。 sequence_item内容由driver驱动DUT所需的信息决定; 要易于生成新的数据对象内容,通常是通过支持随机约束生成;以及其他因素如analysis hooks。

数据属性成员

sequence_item的内容与driver的需求密切相关。driver依赖它接收到的sequence_items的内容来确定要执行哪种类型的驱动。sequence items属性成员将由表示以下信息类型的数据域组成:

  • control eg:传输类型,传输size
  • payload eg:传输的主要数据内容
  • configuration eg:建立新的操作模式,error行为等
  • analysis eg:有助于分析的域——时间戳、rolling checksums等

随机考虑因素

sequence_items在sequence中随机化,以生成数据流。因此,激励的数据属性通常应该声明为rand,并且sequence_item应该包含任何需要的约束,以确保默认生成的值是合法的,或者在合理的范围内。在sequence中,sequence_items通常使用内嵌约束进行随机化,内嵌约束便于对这些基本约束做扩展。

由于sequence_items用于驱动和响应DUT,所以我们最好约定驱动DUT的属性应该是rand,响应DUT的属性不是rand。这将随机化过程进行优化,来确保所收集到的响应信息不会被可能发生的随机化破坏。例如,考虑以下总线协议sequence_item:

class bus_seq_item extends uvm_sequence_item; 

// Request data properties are rand 
	rand logic[31:0] addr; 
	rand logic[31:0] write_data; 
	rand bit read_not_write; 
	rand int delay; 
// Response data properties are NOT rand 
	bit error; 
	logic[31:0] read_data; 
	`uvm_object_utils(bus_seq_item) 
	
	function new(string name = "bus_seq_item"); 
		super.new(name); 
	endfunction
// Delay between bus cycles is in a sensible range 

	constraint at_least_1 { delay inside {[1:20]};} 
// 32 bit aligned transfers 
	constraint align_32 {addr[1:0] == 0;} 
// etc 

endclass: bus_seq_item

Sequence Item方法

uvm_sequence_item通过uvm_transaction类继承了uvm_object。uvm_object有许多虚方法,用于实现常见的数据对象函数(copy、clone、compare、print、transaction recording),应该实现这些方法以使sequence_item更具通用性。sequence_item经常用于analysis traffic ,它可能有助于添加实用函数来帮助实现功能覆盖率或analysis。

UVM 配置数据库 (uvm_config_db)

uvm_config_db类是访问资源数据库的推荐方式。资源是两个或多个组件/对象之间共享的任何信息片段。使用uvm_config_db::set将信息放入数据库,使用uvm_config_db::get从数据库检索信息。uvm_config_db类是一个参数化类,因此这就好像它被划分为许多特定类型的“迷你数据库”。参数类型没有限制,可以是类、uvm_object、bit、byte或虚接口等内置类型。

uvm_config_db有两种典型用法。第一个是将虚接口从HDL/DUT域传递给环境,第二个是通过testbench层次结构向下传递配置对象。

这两种用法基本就是最常用的两种,config_db机制放置资源的位置是全局的,只要通过set,get方法匹配数据类型和资源名就可以完成信息传递。

set方法

set方法的完整签名为void uvm_config_db #( type T = int )::set( uvm_component cntxt , string
inst_name , string field_name , T value );

  • T是被添加的资源或元素的类型——通常是虚接口或配置对象。
  • cntxtinst_name一起形成一个作用域,用于在数据库中定位资源;它是通过将实例名附加到特定组件节点后的完整层次名称而形成的,即{cntxt.get_full_name(),".",inst_name}
  • field_name是给资源的名称。
  • value是放入数据库的实际值或引用。

如下示例,是将虚接口放入UVM配置数据库:

interface ahb_if data_port_if( clk , reset ); 
interface ahb_if control_port_if( clk , reset ); 
... 
uvm_config_db #( virtual ahb_if )::set( null , "uvm_test_top" , "data_port" , data_port_if ); 
uvm_config_db #( virtual ahb_if )::set( null , "uvm_test_top" ,"control_port" , control_port_if ); 

这段代码将两个AHB接口放入配置数据库的层次位置“uvm_test_top”,这是由run_test()创建的顶层test组件的默认位置。这两个不同的接口在数据库中使用两个不同的域名“data_port”和“control_port”。

注意

  • 使用“uvm_test_top”,因为它比“*”更精确,因此更有效; 通常没什么问题,除非你的顶层test组件在其构造函数中执行除 super.new( name , parent ) 之外的其他操作,在这种情况下,就必须相应地调整实例名称。
  • 第一个参数中使用"null",因为这段代码是在顶层module中而不是在uvm_component中。
  • 使用参数化的uvm_config_db::set()方法,而不使用在UVM1.2中弃用的set_config_[int,string,object]()方法。更多细节请参见UVM1.2摘要。

下面是在env中配置agent的示例:

class env extends uvm_env; 
	ahb_agent_config m_ahb_agent_config; 
	function void build_phase( uvm_phase phase ); 
	... 
	m_ahb_agent = ahb_agent::type_id::create("m_ahb_agent" , this ); 
	... 
	uvm_config_db #( ahb_agent_config )::set( this , "m_ahb_agent*" , "ahb_agent_config" , m_ahb_agent_config ); 
	... 
	endfunction 
endclass

这段代码设置AHB agent及其所有子组件的配置。有两点需要注意:

  • 使用"this"作为第一个参数,以确保只设置了这个agent的配置,而没有设置组件层次结构中任何其他ahb_agent的配置。
  • 使用“m_ahb_agent*”确保agent及其子组件都在查找范围内。如果没有'*',只有agent本身,它的driver、sequencer和monitor子组件将无法访问配置。

这里的原理是这样的,get配置生效的前提是第一个组件节点的字符串与第二个参数中的字符串用‘ . ’拼接后与set中设置的相同。这里使用了通配符使得driver、monitor、sequencer中使用(以driver为例)get( this , " " , "ahb_agent_config" , m_ahb_agent_config_drv );就可以直接匹配成功。其实就算这里只set到了m_ahb_agent,在driver中照样可以使用get( null , "uvm_test_top.env.m_ahb_agent" , "ahb_agent_config" , m_ahb_agent_config_drv );来获取成功。原因就是,config_db机制是全局的,实际上它依赖的只是第一和第二个参数组成的字符串能否匹配成功而已。而UVM中做这样的设定,UVM cookbook中这样引导,都是为了让使用者对自己set的目标有一个清晰的把握,避免同类型组件误用了并非自己的config_db的配置。

get方法

get方法的完整签名是bit uvm_config_db #( type T = int )::get( uvm_component cntxt , string
inst_name , string field_name , ref T value );

  • T是被添加的资源或元素的类型——通常是虚接口或配置对象。
  • cntxtinst_name一起形成一个作用域,用于在数据库中定位资源;它是通过将实例名附加到特定组件节点后的完整层次名称而形成的,即{cntxt.get_full_name(),".",inst_name}
  • field_name是给资源的名称。
  • value保存从数据库检索到的实际值或引用;如果检索成功,get()调用返回1,否则返回0来表示数据库中不存在这种类型且具有这种作用域名称的资源。

从配置数据库中获取虚接口的示例如下:

class test extends uvm_test; 
... 
	function void build_phase( uvm_phase phase ); 
	... 
	if( !uvm_config_db #( virtual ahb_if )::get( this , "" , "data_port" , 
		m_cfg.m_data_port_config.m_ahb_if ) ) begin 
		`uvm_error("Config Error", "uvm_config_db #( virtual ahb_if )::get cannot find resource data_port" ) ) 
	end 
	... 
	endfunction 
... 
endclass

上面的代码试图获取AHB数据端口的虚接口,并将其分配到正确的agent的配置对象中。当数据库查找失败时,将提供一个有意义的错误消息。

下面是一个为事务器检索配置的示例:

class ahb_monitor extends uvm_monitor; 
	ahb_agent_config m_cfg; 
	function void build_phase( uvm_phase phase ); 
	... 
	if( !uvm_config_db #( ahb_agent_config )::get( this , "" , "ahb_agent_config" , m_cfg ) ) begin 
	`uvm_error("Config Error" , "uvm_config_db #( ahb_agent_config )::get cannot find resource ahb_agent_config" ) 
	end 
	... 
	endfunction 
endclass

还有几点需要注意:

  • 使用“this”作为context参数。
  • 使用""作为inst_name。
  • 使用get()调用的返回值来检查是否获取失败,并给出有用的错误消息。
  • 使用参数化的uvm_config_db::get()方法,而不是在UVM1.2中弃用的get_config_[int,string,object]()方法。更多细节请参见UVM1.2摘要。

优先级规则

两组优先规则适用于uvm_config_db。首先,在build_phase,组件层次结构上层的context中的set()调用优先于层次结构下层的set()调用。其次,在相同的context中或在build_phase之后,最后的set()调用优先于前一个。有关这些规则的更多细节,可以参阅UVM参考手册或直接查看UVM源代码。

Using Packages

package是一种SystemVerilog语言构造,它支持将相关的声明和定义组合在其名称空间中。package可以包含类型定义、常量声明、函数和类模板。要在作用域内使用package,必须导入,然后才能引用package的内容。

SystemVerilog package是组织代码以及确保对类型、类等的引用一致的有用方法。UVM基类库包含在一个名为“uvm_pkg”的package中。应该使用package来开发UVM testbench,以收集和组织为实现agent、env、sequence库、test库等而开发的各种类定义。

UVM Package编码指南

命名package和package文件

package应该以_pkg后缀命名。包含package的文件的名称应该反映package的名称,并具有.sv后缀名。例如,文件spi_env_pkg.sv将包含package spi_env_pkg。

正当性: .sv扩展名是一种约定,表示package文件是一个独立的编译单元。后缀_pkg表示该文件包含一个package。这两种约定对人和机器解析脚本都很有用。

在package中包含类

在package的作用域内声明的类模板应该被分隔成具有.svh后缀名的单独文件。这些文件应该按照需要编译的顺序在包中`include。package文件是唯一应该使用`include的地方,也就是说,在被`include的文件本身中不应该再有`include语句。

正当性:在单独的文件中声明类更容易维护,也能更清楚地传达package的内容。

导入package和package元素

package的内容可以引用由package导入启用的其他package的内容。这样的外部package导入应该在package的顶部作为package“主体”的第一个语句声明。可能包含的类模板等单独文件不应该单独导入。

正当性:将所有package导入本地化到一个地方可以使package依赖关系更加清晰。将package导入放在package的其他部分或被`include的文件中是不太可见的,而且更有可能导致排序问题和潜在的类型冲突。

将package文件组织到一个目录中

一个package中包含的所有文件都应该放在一个目录中。这对于agent来说尤其重要,因为agent目录结构需要是一个完整独立的package。

正当性:一个package文件的包含目录有助于编译流程的设置,也有助于重用,这是因为一个package的所有文件都可以很容易地收集在一起。

下面是UVM env的package文件示例。这个env包含两个agent(SPI和APB)和一个寄存器模型,它们作为sub-package导入。与env相关的类模板是被`include的:

// Note that this code is contained in a file called spi_env_pkg.sv
// 
// In Questa it would be compiled using: 
// vlog +incdir+$UVM_HOME/src+<path_to_spi_env> <path_to_spi_env>/spi_env_pkg.sv
// 
// 
// Package Description: 
// 
package spi_env_pkg; 
// Standard UVM import & include: 
	import uvm_pkg::*; 
	`include "uvm_macros.svh" 
// Any further package imports: 
	import apb_agent_pkg::*; 
	import spi_agent_pkg::*; 
	import spi_register_pkg::*; 
// Includes: 
	`include "spi_env_config.svh" 
	`include "spi_virtual_sequencer.svh" 
	`include "spi_env.svh" 
endpackage: spi_env_pkg 

package范围

SystemVerilog package定义了一个作用域,这一事实常常让用户感到困惑。这意味着在package中声明的所有内容,以及导入到package中的其他package的元素,都只在该package的范围内可见。如果一个package被导入到另一个作用域(例如,另一个package或一个模块),则只有被导入package的元素是可见的,而被导入package本身所导入的任何package的元素都是不可见的。因此,如果在新范围中需要这些其他package的元素,则需要分别导入它们。

// 
// Package Scope Example 
// ---------------------------------------------------- 
// 
package spi_test_pkg; 
// The UVM package has to be imported, even though it is imported in the spi_env package. 
// This is because the import of the uvm_pkg is only visible within the current scope 
	import uvm_pkg::*; 
// The same is true of the `include of the uvm_macros 
	`include "uvm_macros.svh" 
// Import of uvm_pkg inside the spi_env package is not 
// visible within the current spi_test_pkg scope 
	import spi_env_pkg::*; 
// Other imports and `includes 
	`include spi_test_base.svh 
endpackage: spi_test_pkg

这也就是说package本身就是一个作用域,而导入外层package中的package只对外层package内可见。关于package的这部分内容,主要是帮助我们组织文件结构的。

posted @ 2021-08-07 23:26  空白MAX  阅读(882)  评论(0编辑  收藏  举报