摘要 通用验证方法(UVM)是一种功能强大的验证方法,其设计目的是能够验证各种设计规模和设计类型。UVM 名称中的“Universal”之一层含义,是该方法能够验证宇宙中的任何事物,至少是集成电路领域的所有事物。该通用能力有价值,但也意味着 UVM 中的许多能力对特定项目可能并非必要。事实上,作者认为 UVM 中的许多功能对多数项目来说并不需要。此外,UVM 起源于其他验证方法的融合,如 OVM 和 VMM。这些渊源意味着 UVM 中有些部分是从其他方法继承而来,但在纯 UVM 测试平台中并非真正需要。本文重点定义 UVM 基类、方法和宏的一个子集,使工程师更快学习 UVM,并在使用 UVM 验证以 VHDL、Verilog 或 SystemVerilog 建模的各种类型和规模的数字设计时提高效率。您可能会惊讶地发现,要有效验证复杂设计,真正需要的 UVM 子集其实很小。
引言 通用验证方法(通常称为 UVM)的设计目的是具备验证所有类型数字逻辑设计的能力,无论设计规模大小,是 FPGA 或 ASIC 或全定制设计,还是面向控制、面向数据或面向处理器的设计。UVM 的这种通用性意味着,该方法中的一些构造和功能虽然可能对某些验证项目有用,但对大多数项目来说并非必要。UVM 的传统进一步增加了方法论的复杂性。UVM 并不是从零开始创建的。相反,UVM 的基础类库和通用测试平台架构是从其他已被证明对数字设计验证有效的方法中提取出来的。 UVM 的通用性和传统性导致了类库、方法和宏的臃肿。通常情况下,有多个类、方法或宏做着几乎相同的事情,只有细微的差别,而且往往是不明显的差别。例如,UVM 同时提供资源数据库和配置数据库。在互联网上快速搜索一下,就会发现对于哪个数据库最好,或者在这种情况下应该使用哪个数据库,或者在那种情况下应该使用哪个数据库,有很多相互矛盾的意见。冗余的另一个例子是 UVM 如何提供多种 “报告 “信息的方法。有几乎相同的报告方法,如 uvm_report_error(),也有报告宏,如 `uvm_error。 面向对象编程的本质增加了 UVM 类库的复杂性。只有《UVM 类参考手册》中列出的部分类是供编写基于 UVM 的测试平台的人员使用的。这些类遵循良好的 OOP 实践,扩展并继承自其他基类。遗憾的是,《UVM 类参考手册》记录了所有这些类(这是应该的),但手册并没有明确说明哪些类是供 UVM 的最终用户在 UVM 测试平台中使用的,哪些类是供 UVM 方法内部使用的。 本文的目的是通过在存在冗余的情况下提出优先选择,并明确哪些类是供 UVM 最终用户使用的,从而简化学习和采用 UVM 的过程。我们将说明,几乎所有验证项目都可以通过 UVM 类、方法和宏的简单子集来实现。这种 “UVM-light “子集将有助于使 UVM 更容易学习、更容易维护、更容易重复使用,并且不易出现编码错误或非理想的编码风格。
历史与发展 UVM 在数字逻辑验证方面拥有悠久而丰富的历史,其中包括超过 15 年的组织有序的文档化方法,以及更多年的公司特定临时方法。vAdvisor, eRm, RVM, VMM, AVM, URM 和 OVM 等成熟的验证方法都对 UVM 所使用的验证方法做出了贡献。虽然这些方法学都有一个共同的目标,即验证数字逻辑设计,但这些方法学往往采用截然不同的方法来实现这一目标。本文无意提供这些遗产的细节,而是要强调这些遗产影响了 UVM 中存在的臃肿和冗余。 作为起点,UVM 开发委员会在很大程度上借鉴了开放验证方法(OVM)的方法来构建验证测试平台。事实上,UVM 的第一个初步版本 UVM 1.0EA (”EA “代表 “早期采用者”)于 2010 年发布,主要是对 OVM 的直接移植,只有少量改动或添加。UVM 从这个早期采用者版本迅速发展,增加了来自 VMM 的功能(如寄存器层),以及来自其他文档和临时方法的成熟验证概念。UVM 1.0 版本在 1.0EA 版本发布 9 个月后发布,仅 6 个月后,UVM 1.1 版本于 2011 年 6 月发布。在这短短约 15 个月的时间里,UVM 已经发展成与 OVM 截然不同的版本,尽管两者的传承非常明显。 在接下来的三年中,UVM 1.1 有了一些小的更新,但总体而言,并没有重大的进化变化。UVM 的下一次重大变化发生在 2014 年 6 月,UVM 1.2 发布。UVM 1.2 为 UVM 增加了几项实质性的新功能,本文稍后将讨论其中的一些功能。演变尚未完成。UVM 目前正处于成为 IEEE 标准(指定为 IEEE P1800.2)的早期阶段,在这一过程中很可能会有更多的发展。 从 OVM 的起点到 UVM 1.1,再到现在的 UVM 1.2,这种快速发展是 UVM 变得臃肿的主要原因。随着 UVM 的发展,新的测试平台架构和测试编写方法已被添加到 UVM 中,但旧的方法仍然可用。一些旧的方法论特性已被正式弃用,另一些已被修改,但旧特性的包袱仍然存在于 UVM 类库和宏的实现中,仍然在现有的 UVM 测试平台和验证知识产权(VIP)中使用,并且仍然出现在书籍、论文和培训课程中。UVM 的演进过程导致编写 UVM 测试平台的方法既有旧的,也有新的。 如前所述,本文的目的是将这些多余的包袱搬到柜子里,并展示一个简单的 UVM 类、方法和宏子集,它几乎可以处理所有的验证项目。这个推荐子集基于 UVM 1.2,但只使用向后兼容 UVM 1.1 的构造。
我们撰写本文的方法 本文从测试编写人员、环境编写人员和序列编写人员这三个角度来研究 UVM。注意:这是一篇“如何使用”的论文,而不是“如何编写”的论文!我们不会在本文中教授如何编写 UVM 测试、驱动程序、监视器和记分板,也不讨论 UVM 结构的定义、语法和语义。我们将重点放在为大多数验证项目编写有效的 UVM 测试、环境和激励所真正需要的 UVM 结构上。 UVM 测试平台可分为三个主要部分:测试、环境和序列(刺激)。UVM 的结构使不同的工程师可以专注于编写测试平台的不同部分,而对其他部分知之甚少。在本文中,我们将通过首先考虑一个简单但完整的 UVM 测试、环境和序列来研究 “用什么”。我们将展示在这个简单的 UVM 验证项目中,测试编写器、环境编写器和序列编写器需要确切了解哪些 UVM 结构。然后,我们将探讨几种高级验证情况,并讨论处理这些更困难的测试平台所需的其他 UVM 结构。 虽然本文的重点是简单和更高级的 UVM 验证需要哪些 UVM 结构,但我们也将讨论一些不需要的结构。不过,对不必要构造的讨论并不全面。也许我们可以用另一种简单的方式来看待这份 UVM 实用子集指南:如果某个构造没有列在推荐子集中,那么您可能并不需要它。 不需要的 UVM 结构体一般分为以下四类中的一类或几类: 1)该结构体是 UVM 的 OVM 传统遗留下来的,但已被更受欢迎的 UVM 结构体所取代; 2)该结构体与我们推荐的 UVM 子集中的结构体冗余(在这种情况下,我们选择我们认为最简单或最通用的结构体); 3)该结构体仅对少数罕见的验证需求有用,并非绝大多数项目需要; 4)该结构体并非面向 UVM 最终用户,而是用于 UVM 方法的内部实现。
测试编写指南 测试编写负责使用可用的环境和序列来指定刺激和结果检查,以验证被测设备(DUT)的特定功能。UVM 的设置使测试编写者无需了解太多环境或 UVM 本身的底层细节,就能指定有用的测试。从测试编写者的角度来看,UVM 环境主要是一个黑盒子。在本节中,我们将讨论交给测试编写者的具体任务,并说明测试编写者需要哪些 UVM 结构。
连接到 DUT
目标
将基于类的 UVM 测试平台与顶层模块中的 DUT 连接起来
使用 SystemVerilog 接口传递“虚拟接口”到 UVM 层次
顶层模块操作步骤
实例化接口与 DUT,并完成端口连接
通过 uvm_config_db::set() 将虚拟接口句柄写入配置数据库
调用 run_test() 启动 UVM
set() 参数说明
第1参:null(作用域对象)
第2参:”uvm_test_top”(顶层测试实例路径)
第3参:”DUT_IF”(字段名,唯一标识某接口)
第4参:虚拟接口句柄(接口实例)
多接口场景
为每个接口选择唯一“字段名”,按需多次 set()
建议统一命名规范:如 “AXI_IF”、”GPIO_IF”、”DBG_IF”
在 test/env 中获取
使用 uvm_config_db::get(this, “”, “DUT_IF”, m_env_cfg.dut_if)
失败时报告 uvm_error/uvm_fatal,避免空句柄
运行流程
run_test() 根据 +UVM_TESTNAME 创建测试实例并驱动各阶段
接口 set() 与 run_test() 顺序如上保持不变
1 2 3 4 5 6 7 8 9 10 11 12 13 *> module test_top(修正端口连接与缩进) module test_top; import uvm_pkg::*; import my_test_pkg::*; my_dut_interface my_dut_if(); my_dut_rtl my_dut(.if (my_dut_if)); initial begin uvm_config_db #(virtual my_dut_interface)::set(null, "uvm_test_top", "DUT_IF", my_dut_if) ; run_test(); end endmodule
导入 uvm_pkg 包是所有 UVM 测试平台的标准配置。UVM 环境的代码通常包含在一个或多个用户定义包中(本例中为 my_test_pkg)。导入该用户定义包以及测试平台接口(my_dut_interface)的实例化和连接都是标准的 SystemVerilog 编程。当我们使用 uvm_config_db 数据库将虚拟接口句柄传递给 UVM 测试(并最终传递到环境中)时,UVM 本身在这个顶层模块中的使用就开始发挥作用了。
uvm_config_db 数据库是一个参数化类,其中一个参数用于表示传入数据库的对象类型–在本例中是 my_dut_interface 类型的虚拟接口。uvm_config_db 类包含一个名为 set 的静态方法。测试编写者只需要知道调用该方法的语法,而不需要知道数据库的实现。在顶层,set() 方法的前两个参数始终分别为 null 和 “uvm_test_top”。在 UVM 中,”uvm_test_top” 始终代表顶层测试的实例名称。第三个参数是数据库中的 “字段名”,是一个字符串,用于识别 UVM 测试中的特定接口(本例中为 “DUT_IF”)。大型验证项目通常会使用多个接口将 UVM 测试平台与设计连接起来。每个接口在数据库中都有一个唯一的字段名。最后一个参数是接口实例的名称。实例名称被称为该实例的虚拟接口句柄。环境写入程序将使用前三个参数的组合,从配置数据库中获取虚拟接口的信息。
run_test() 方法用于启动 UVM 运行。它将创建一个顶级测试类实例(在命令行中通过 +UVM_TESTNAME= 指定),并相应运行其方法。为测试平台和 DUT 之间使用的每个接口创建一个实例,并使用 set() 调用通过 uvm_config_db 将它们传递到测试中,然后使用 run_test()启动 UVM,这就是测试编写者在顶层模块中启动 UVM 测试所需要做的全部工作。
UVM test UVM test 有几个基本职责:(1)从配置数据库获取虚拟接口句柄 (2)实例化 UVM 环境 (3)使用配置数据库将虚拟接口句柄和其他信息传递给环境 (4)实例化并启动测试用例所需的任何序列 (5)管理阶段异议,确保测试成功完成。 测试必须在工厂注册,并且必须实现 build_phase() 和 run_phase() 方法。它还可以选择实现其他阶段方法,但这些方法通常不是实现有效测试所必需的。以下可视为 UVM 测试的模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 *> UVM test 模板(排版优化) class my_test extends uvm_test; `uvm_component_utils(my_test) my_env m_env; my_env_config_obj m_env_cfg; function new (string name = "my_test" , uvm_component parent = null ); super .new (name, parent); endfunction function void build_phase(uvm_phase phase); ... endfunction task run_phase(uvm_phase phase); ... endtask endclass
uvm_component_utils 宏用于将测试类注册到工厂中,以便在顶层模块中调用 run_test(),创建并运行该测试实例。UVM 提供的工厂注册宏的优势是您无需关心在 UVM 工厂中注册类的细节。只需在测试中首先调用宏,并将测试类名称作为参数即可。注意,宏调用行尾不加分号。
所有 UVM 组件的类 new() 构造函数都是一样的。new() 的参数是默认值,当组件被工厂实例化时,这些参数可能(而且很可能)被重写。不过,由于测试是通过调用 run_test()(如例 1 所示)实例化的,因此将使用其默认值。因此,我们将名称指定为类的名称(以便于识别),并将 null 指定为父类(因为 uvm_test 位于 UVM 层次结构的顶层)。
实例化和设置环境 test使用 build_phase 来实例化和配置环境。
1 2 3 4 5 6 7 8 9 10 11 *> build_phase 片段(排版与对齐) function void build_phase(uvm_phase phase); m_env_cfg = my_env_config_obj::type_id::create("m_env_cfg" ); m_env = my_env::type_id::create("my_env" , this ); if (!uvm_config_db#(virtual my_dut_interface)::get(this, "", "DUT_IF", m_env_cfg.dut_if)) `uvm_error("TEST" , "Failed to get virtual interface in test" ) uvm_config_db#(my_env_config_obj)::set(this, "my_env", "m_env_cfg", m_env_cfg) ; endfunction
UVM 对象和组件是使用工厂 create() 方法构造的,而不是使用传统的类 new() 构造函数。测试编写者只需知道 create() 方法的使用语法。它是一个静态方法调用,静态路径由被构造类的类型名和静态 type_id 句柄组成。从 uvm_object 基类扩展而来的类的 create() 方法需要一个参数,即名称字符串。从 uvm_component 基类扩展而来的类的 create() 方法需要两个参数,一个是名称字符串,另一个是组件句柄,该组件将包含正在创建的组件实例。第二个参数始终是这个–被创建的组件存在于调用 create() 方法的组件中。 使用配置对象 my_env_config_obj 只是一种方便的机制,它允许将环境的所有信息封装在一个对象中,从而只需调用一次 uvm_config_db 就能将信息传递给环境。配置对象本身只是 uvm_object 的一个扩展,因此它和其他 uvm_object 一样,通过 `uvm_object_utils 宏创建并在工厂注册。 为了最大限度地提高重用性,我们建议 UVM 层次结构中的每一层都应从其父层获取配置信息,并将信息下传给下一层。 env本身只是一个 UVM 组件,因此它是由工厂创建的,就像其他组件一样。实例化和配置环境等基本操作通常在 “base test “中完成,然后对其进行扩展,以便针对特定测试目标进行额外定制。在这种情况下,我们可以扩展基础测试,并使用工厂实例化不同的环境。
1 2 3 4 5 6 7 8 *> 扩展测试 class my_extended_test extends my_test; `uvm_component_utils(my_extended_test) function void build_phase(uvm_phase phase); my_env::type_id::set_type_override(my_env2::get_type()); super .build_phase (phase); endfunction
关于扩展测试,有几点需要注意:在调用 super.build_phase() 之前,先在 build_phase() 中完成工厂重载,这样在创建环境之前就生效(如重载配置对象,也同理)。虽然类型重写更常见,但 UVM 工厂也可对特定实例进行重写。其次,我们在扩展测试中调用了 super.build_phase(),但在基础测试中不要这样做。UVM 一般规则如下:切勿在从 UVM 库基类扩展的组件中调用 super.build_phase()。
启动序列 除了设置env外,test还主要负责在 UVM 中启动特定的测试sequences,以定义实际执行的行为。序列本身将由序列编写器编写,但由测试编写器来启动它们。
1 2 3 4 5 6 7 8 9 10 11 12 *> 在 test 中启动序列(修正 32 'h 字面量与缩进) class my_extended_test extends my_test; `uvm_component_utils(my_extended_test) ... task run_phase(uvm_phase phase); ... my_seq seq = my_seq::type_id::create("seq" ); assert (seq.randomize () with { src_addr == 32'h0100_0800 ; xfer_size == 128 ; }); seq.start (m_env.m_agent .m_sequencer ); ... endtask
1 2 3 4 5 6 7 8 9 10 11 12 *> 启动虚拟序列(修正引号样式) class my_virtual_test extends my_test; `uvm_component_utils(my_extended_test) ... task run_phase(uvm_phase phase); ... my_vseq virt_seq = my_seq::type_id::create("virt_seq" ); virt_seq.init (.bus_seqr (m_env.m_agent1 .m_sequencer ), .gpio_seqr (m_env.m_agent2 .m_sequencer )); virt_seq.start (null ); ... endtask
不建议 采用 “default sequence” 机制;建议通过配置对象传入后台序列。
1 2 3 4 5 6 7 8 9 10 11 12 *> 通过配置对象传入背景序列 class my_extended_test extends my_test;`uvm_component_utils(my_extended_test) ... function void build_phase(uvm_phase phase); my_env::type_id::set_type_override(my_env2::get_type()); super .build_phase (phase); my_bkgrnd_seq bkgrnd_seq = my_bkgrnd_seq::type_id::create("bkgrnd_seq" ); assert (bkgrnd_seq.randomize () with {burst_len == 16 ;}); my_env_cfg.background_seq = bkgrnd_seq; endfunction
1 2 3 4 5 6 7 8 9 10 11 12 *> 启动虚拟序列(无需 virtual sequencer 组件) class my_virtual_test extends my_test; `uvm_component_utils(my_extended_test) ... task run_phase(uvm_phase phase); ... my_vseq virt_seq = my_seq::type_id::create("virt_seq" ); virt_seq.init (.bus_seqr (m_env.m_agent1 .m_sequencer ), .gpio_seqr (m_env.m_agent2 .m_sequencer )); virt_seq.start (null ); ... endtask
虚拟时序器只会增加组件间的连接和环境的复杂性 ,不推荐引入。
Phasing and Objections run_phase 是唯一耗时的执行阶段。
1 2 3 4 5 6 7 8 9 10 11 12 13 *> 异议管理 class my_extended_test extends my_test; `uvm_component_utils(my_extended_test) ... task run_phase(uvm_phase phase); phase.raise_objection ("Starting test" ); my_seq seq = my_seq::type_id::create("seq" ); assert (seq.randomize () with { src_addr == 32'h0100_0800 ; xfer_size == 128 ; }); seq.start (m_env.m_agent .m_sequencer ); phase.drop_objection ("Ending test" ); endtask
在该阶段开始前调用 **raise_objection()**,序列完成后调用 **drop_objection()**。
环境编写人员 环境编写人员负责将测试生成的刺激输入被测设备(DUT),并验证被测设备对刺激的正确响应。为此,环境编写人员定义 UVM 环境(env)、代理(agent)和记分板(SCB)。
UVM Environment 如前所述,UVM env是由 UVM test 创建和配置的 UVM 组件。env将刺激驱动到 DUT 中,monitor DUT 的输入和输出,并验证 DUT 是否按预期运行。 UVM env封装了 UVM 测试平台的结构方面。UVM 环境包含: 一个或多个agent,每个agent负责驱动 DUT 输入并monitor特定 DUT interface上的 DUT 输入和输出 记分板SCB,用于验证 DUT 对刺激的响应 覆盖范围收集器(可选),用于记录交易信息以进行覆盖范围分析 配置组件,使测试能够设置环境和代理,以满足特定的测试要求。
更复杂的 UVM 环境(如图 1 所示)可能包含更多组件,如多个agent和寄存器模型。本文后面的第 5.0 节 “高级示例 “将对此进行讨论。剧透警报!我们将看到,在这些更复杂的环境中,与基本 UVM 环境中使用的简单结构集相比,env编辑人员只需知道很少的 UVM 结构集即可。
例 9 展示了一个简单 UVM 环境的代码,该环境只有一个代理和记分牌。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 *> 简单环境(缩进与对齐) class my_env extends uvm_env; `uvm_component_utils(my_env) function new (string name, uvm_component parent); super .new (name, parent); endfunction : new my_agent agent; my_scoreboard scoreboard; function void build_phase(uvm_phase phase); agent = my_agent::type_id::create("agent" , this ); scoreboard = my_scoreboard::type_id::create("scoreboard" , this ); endfunction : build_phase function void connect_phase(uvm_phase phase); agent.dut_inputs_port .connect (scoreboard.dut_in_imp_export ); agent.dut_outputs_port .connect (scoreboard.dut_out_imp_export ); endfunction : connect_phase endclass : my_env
The Environment Declaration 声明 环境是一个用户定义的类,在本例中称为 my_env。环境类是从 uvm_env 基类扩展而来的,而 uvm_env 基类又是从 uvm_component 基类派生而来。UVM 组件是构成测试平台层次结构的对象,通常在仿真开始时创建,并在整个仿真过程中保持不变。
类的顶部是所有从 uvm_component 扩展而来的类都需要的模板代码。这些代码与本文第 2.3 节讨论的一般代码相同,包括使用宏 uvm_component_utils() 向工厂注册类名,以及定义类的 new() 构造函数。如上所述,使用 UVM 的自动配置和字段自动化功能没有任何好处,因此没有理由使用 uvm_component_utils_begin/end 宏来设置字段自动化。
The Environment Build Phase UVM build_phase 用于构建环境中包含的组件。在这个简单的例子中,这些组件是一个代理和一个记分牌。这些组件是通过前面第 2.3 节中讨论的 create() 方法构建的。关于代理和记分牌的细节将在本文后面讨论。
The Environment Connect Phase UVM connect_phase 用于建立代理与记分板之间的连接。UVM 使用 TLM 1.0 通信协议在类对象间传递信息,其源自 SystemC 标准。主要通信协议:
port/export 对:1 对 1,用于推送 uvm_sequence_item 句柄(序列器与驱动常用)
analysis port / analysis imp 导出:1 对多,用于广播 uvm_sequence_item 句柄(监视器常用,可同时连记分板和覆盖率收集器)
TLM port和export是 UVM 基类库中定义的类。每个port和每个export都是其中一个类的对象(实例)。port或export的名称实际上就是该端口或导出对象的句柄. 每个port或analysis port都有一个 connect() 方法。该 connect 方法的参数是与该port 配对的相应export或 imp export。 在env类级别,connect() 方法是完成环境所需的唯一 UVM 结构。在例 9 中,agent包含两个analysis port,它们与SCB上的两个 imp export相连。(env编辑人员还需要了解其他 TLM 方法,以便编写驱动程序和监视器,这将在下文有关这些主题的讨论中介绍)。
UVM Agents UVM agent是一个低级构件,与一组特定的 DUT I/O 引脚和这些引脚的通信协议相关联。例如,DUT 的 USB 总线将有一组端口 agent。同样,AXI 总线也有一个专门针对该总线的agent。代理包含三个必要组件:sequencer、driver和monitor。此外,代理还可包含一个可选的覆盖范围收集器组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 *> agent 声明与旋钮(修正 ' 方向与排版) class my_agent extends uvm_agent;`uvm_component_utils(my_agent) function new (string name, uvm_component parent);super .new (name, parent);endfunction : new my_sequencer sqr; my_driver drv; my_monitor mon; my_coverage_collector cov; my_config m_config; localparam OFF = 1'b0 , ON = 1'b1 ;bit enable_coverage = OFF; uvm_active_passive_enum is_active = UVM_ACTIVE; uvm_analysis_port #(my_tx) dut_inputs_port; uvm_analysis_port #(my_tx) dut_outputs_port; function void build_phase(uvm_phase phase);... endfunction : build_phasefunction void connect_phase(uvm_phase phase);... endfunction : connect_phaseendclass : my_agent
is_active 取值:UVM_ACTIVE / UVM_PASSIVE 。
The Agent 声明 代理是一个用户定义的类,本例中称为 my_agent,它是从 uvm_agent 基类扩展而来的。我们使用宏 `uvm_component_utils() 在工厂注册了类名,并定义了类的 new() 构造函数,输入了它的名称和父类
agent 配置按钮 代理需要进行配置,以满足特定测试的要求。配置 UVM 组件的控件通常称为 “旋钮”。这些旋钮可能具有简单的开/关值(通常用 1 位 0 表示关,1 位 1 表示开),或者旋钮可能被设置为一个值,如序列应生成的事务数。 启用覆盖范围旋钮用于配置代理收集或不收集功能覆盖范围数据。该旋钮的默认值为 OFF(禁用)。 is_active 旋钮用于将代理配置为主动或被动。主动代理可以监控和驱动 DUT 端口。被动代理只能监控 DUT 端口。is_active 的数据类型是 UVM 基类库中定义的枚举类型,称为 uvm_active_passive_enum,它有两个可能的枚举值:UVM_ACTIVE 和 UVM_PASSIVE。该旋钮的默认值是代理处于活动状态。 uvm_agent 基类中已经定义了一个 is_active 节点,但《UVM 类参考手册》中并没有将该属性作为该基类的变量之一进行记录。相反,《类参考手册》记录了一个 get_is_active() 方法,该方法可返回基类 is_active 属性的值。然而,《类参考手册》并未提供设置该旋钮的方法。由于基类 is_active 旋钮没有正式文档记录,也没有设置该变量的方法,因此作者建议明确定义一个本地 is_active 旋钮,而不是使用未记录的 is_active 基类属性。有了本地 is_active 属性,还可以明确定义默认值。从 uvm_agent 基类扩展代理没有任何功能上的理由。代理也可以从 uvm_component 基类扩展而来。
The Agent Port Handles 环境需要将代理连接到记分板,以便记分板接收监视器捕获的 DUT 输入和输出值信息。要做到这一点,需要在代理层声明与监视器上相同端口相对应的 UVM 分析端口。其中一个端口将传递 sequence_item 对象的句柄,该对象包含该代理使用的 DUT 输入端口的值。另一个端口将传递到 sequence_item 对象的句柄,其中包含该代理使用的 DUT 输出端口的值。UVM 分析端口是基类 uvm_analysis_port 的实例。uvm_agent 基类包含一个类型参数,该参数定义了将通过端口通信的 sequence_item 类类型。在本例中,两个端口都传递 my_tx sequence_item 类型的句柄。代理端口名称 dut_inputs_port 和 dut_outputs_port 是对象句柄,将在代理的 connect_phase 中赋值。
The Agent Build Phase class my_agent extends uvm_agent; … function void build_phase(uvm_phase phase); if (uvm_config_db #(my_config)::get(this, “”, “test_config”, m_config)) begin *> get() succeeded this.is_active = this.m_config.is_active; this.enable_coverage = this.m_config.enable_coverage; end else `uvm_warning(“LAB”, “Failed to access config_db – using defaults instead.\n”)
mon = my_monitor::type_id::create("mon", this);
if (is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver::type_id::create("drv", this);
end
if (enable_coverage)
cov = my_coverage_collector::type_id::create("cov", this);
endfunction: build_phase
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 UVM build_phase 用于构建代理中包含的组件。在这个简单的示例中,这些组件包括:my_monitor、driver、my_sequencer、my_coverage_collector 和 my_config。这些组件是使用前面讨论过的工厂 create() 方法构建的。本文稍后将讨论这些组件的细节。 在构建代理组件之前,构建阶段首先会尝试使用 uvm_config_db:get() 静态方法从配置数据库中获取配置对象。配置对象为测试编写程序提供了一种配置环境和代理的方法,以满足特定测试的要求,本文前面的第 2.3 节对此进行了讨论。 在上面的例 11 中,配置对象句柄从数据库中获取并存储到 m_config 属性中。代理使用的配置 "旋钮 "是 is_active 和 enable_coverage 配置变量,它们作为测试类的一部分由测试编写程序设置。这些旋钮用于确定是否构建代理中的驱动程序、序列器和覆盖范围收集器组件。如果测试将代理配置为 "被动",则没有驱动程序/定序器对。如果测试将代理配置为不收集覆盖率,则没有覆盖率收集器组件。 ## UVM Drivers UVM driver从其关联的sequencer中请求一个sequence_item对象的句柄,并将sequence_item属性中的值分配给SystemVerilog接口中的相应信号,从而驱动DUT输入。 ```verilog *> UVM driver示例 class my_driver extends uvm_driver #(my_tx); `uvm_component_utils(my_driver) function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual tb_if tb_vif; // virtual interface pointer function void build_phase(uvm_phase phase); if (!uvm_config_db #(virtual my_dut_interface)::get(this, "", "DUT_IF", tb_vif)) `uvm_fatal("NOVIF", "Failed to get virtual interface from uvm_config_db.\n") endfunction: build_phase task run_phase(uvm_phase phase); my_tx tx; forever begin @tb_vif.clk // drive values synchronized to interface clock seq_item_port.get_next_item(tx); // get a transaction tb_vif.operand_a <= tx.operand_a; tb_vif.operand_b <= tx.operand_b; tb_vif.opcode <= tx.opcode; seq_item_port.item_done(); end endtask: run_phase endclass: my_driver ```verilog 驱动程序的主要任务是在其 run_phase() 方法中执行。驱动程序通过从其关联的序列器请求 sequence_item 对象的句柄来启动对 DUT 的刺激。然后,驱动程序将这些值分配给 SystemVerilog 接口中的信号,从而驱动 DUT 输入。 驱动程序通过为 SystemVerilog 接口中的变量赋值,将数值发送到 DUT。该接口针对代理,封装了 DUT 使用的特定总线协议所需的 DUT 信号。例如,如果 DUT 有一条 USB 总线和一条 AXI 总线,那么每条总线都会有一个单独的接口和相应的代理(及其驱动程序)。 为了让类定义为接口变量赋值,类需要有一个指向接口实例的指针。这个指针被称为虚拟接口。本文前面讨论的顶层模块(见第 2.1 节)将接口实例化,并将虚拟接口复制到 UVM 的配置数据库中。驱动程序使用静态 uvm_config_db::get()方法检索虚拟接口。该方法在前面的第 2.3 节中讨论过。 ### The Driver’s Run Phase 驱动程序的真正工作发生在 UVM run_phase 阶段。通常,UVM 驱动程序被写成一个无限循环,可以使用 SystemVerilog 的 forever 循环或 while(1) 循环来实现。虽然驱动程序被编码为一个无限循环,但 run_phase 本身并不会一直运行到无穷大。测试编写程序使用 UVM 的反对标志来控制 run_phase 的实际运行时间,这在本文前面已经讨论过。 在驱动程序的运行阶段(run_phase)内,驱动程序将与 DUT 时钟和生成刺激值的 UVM 序列同步。下文将讨论这种双重同步。 ### driver/DUT和避免竞争 为了协调从驱动程序到接口变量的数值传输,驱动程序使用接口中的时钟信号与 DUT 同步。这与 DUT 使用的时钟相同,用于 UVM 代理处理的特定总线协议。 在所有数字验证中,无论采用何种验证方法,测试平台都必须避免与 DUT 发生竞赛。简单地说,测试平台必须在 DUT 的时钟边沿之前足够早地驱动刺激,以便在 DUT 寄存器触发和存储输入值(设置时间)时刺激保持稳定。SystemVerilog 方法希望(但不强制)由 UVM 驱动程序和 DUT 之间的接口处理测试平台和 DUT 之间的同步。将这种同步转移到接口具有重要优势: 驱动程序代码大大简化--所有驱动程序需要做的就是为接口信号赋值,并由接口来避免与 DUT 之间的竞赛条件。 驱动程序的变体可以从用户定义的驱动程序类中扩展,而无需重复复杂的同步时序。 监控器也需要避免与 DUT 发生竞赛,仅在 DUT 输出稳定时观察 DUT 输出。 请注意,驱动程序和监控器可以直接访问虚拟接口中的信号,也可以调用接口中的任务,以进一步简化测试平台,并将细节留给接口编写者[1]。可以使用 SystemVerilog 时钟块来帮助避免竞赛,但这种结构通常与仿真不兼容,在设计驱动程序和监视器时应考虑到这一点。让这些组件调用接口任务,可以将同步机制的选择权也交给接口。 ### driver/sequence同步 UVM 驱动程序将驱动 DUT 的值存储在 UVM sequence_item 对象中。该对象由 UVM 序列创建,其值由 UVM 序列生成。当驱动程序获取 sequence_item 句柄时,需要与序列同步。这种同步是通过一对方法(get_next_item() 和 item_done())完成的。这些方法被定义为驱动程序 TLM 端口的一部分,该端口与相应的序列器相连。环境写入器只需知道调用 get_next_item()将阻塞驱动程序的执行,并等待序列创建 sequence_item 对象。与一个或多个序列激励发生器实际握手的复杂代码,完全由驱动程序和序列使用的 UVM 基类方法处理。图 3 展示了驱动程序与序列之间的基本握手过程。 使 driver 和 sequence 同步的握手步骤是: 1. 测试编写人员引发反对标志,并调用序列的 start(),该方法会进入序列的 body() 并在其退出前阻塞。 2. 序列在 body() 中调用 start_item(),阻塞等待驱动请求事务。 3. 驱动调用 seq_item_port.get_next_item() 请求事务,并在获得事务前阻塞。 4. 序列生成值并调用 finish_item() 发送给驱动,随后阻塞等待驱动完成该事务。 5. 驱动将事务值驱动到接口后,调用 seq_item_port.item_done() 解除序列阻塞。随后可继续步骤 2–5。 6. 序列完成后 body() 退出,解除测试中 start() 的阻塞;测试继续,包含放弃反对标志以结束阶段。 ## UVM Monitors UVM 监视器观察特定接口的 DUT 输入和输出,将观察到的值捕获到一个或多个 sequence_items 中,并将这些 sequence_items 的句柄广播给其他 UVM 组件(如记分板和覆盖范围收集器)。UVM 严格规定了监视器应做的事情,但在如何实现该功能方面允许充分的灵活性--也许是灵灵活性过大,导致监视器的实现风格和如何最好地实现监视器的意见五花八门。UVM 对监控器的唯一规定是1) 监视器是代理的一部分,因此与该代理处理的特定总线协议密切相关。2)监控器是严格意义上的被动组件--监控器只能观察活动,而不能改变值。3) 监控程序同时观察 DUT 的输入和输出。尽管驱动程序知道 DUT 的驱动值,但 UVM 要求只有监控器将这些输入值传达给记分板或其他组件。之所以有这样的要求,是因为可以将代理配置为被动代理,而被动代理有一个监视器,但没有驱动程序或定序器。例 15 显示了基本 UVM 监视器的代码。 class my_monitor extends uvm_monitor; `uvm_component_utils(my_monitor) function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual my_dut_interface tb_vif; // virtual interface uvm_analysis_port #(my_tx) dut_inputs_port; // analysis port for DUT inputs uvm_analysis_port #(my_tx) dut_outputs_port; // analysis port for DUT outputs function void build_phase(uvm_phase phase); dut_inputs_port = new("dut_inputs_port", this); // construct the analysis port dut_outputs_port = new("dut_outputs_port", this); // construct the analysis port if (!uvm_config_db #(virtual my_dut_interface)::get(this, "", "DUT_IF", tb_vif)) `uvm_fatal("NOVIF", Failed to get virtual interface from uvm_config_db.\n") endfunction: build_phase task run_phase(uvm_phase phase); my_tx tx_in, my_tx tx_out, tx_copy; fork // monitor DUT inputs synchronous to the interface clock forever @(tb_vif.clk) begin // create a new tx_write object for this cycle tx_in = my_tx::type_id::create("tx_in"); tx_in.operand_a = tb_vif.operand_a; tx_in.operand_b = tb_vif.operand_b; tx_in.opcode = tb_vif.opcode; dut_inputs_port.write(tx_in); end // monitor DUT outputs synchronous to the cb2 clocking block // create a tx_tmp object to reuse each pass of the loop tx_out = my_tx::type_id::create("tx_out"); // tx_out is reused each loop pass forever @(tb_vif.clk2) begin tx_out.result = tb_vif.result; tx_out.exception = tb_vif.exception; // create a copy of tx_out to send to the scoreboard $cast(tx_copy, tx_out.clone()); dut_outputs_port.write(tx_copy); end join endtask: run_phase endclass: my_monitor ### The Monitor 声明 UVM 监视器是从 uvm_monitor 基类扩展而来的。该基类没有参数化,因此不需要重新定义参数。与所有 UVM 测试台组件一样,类名使用 `uvm_component_utils()宏向工厂注册,并且定义了类 new() 构造函数,其中包含所需的名称和父输入。 ### monitor 通信 UVM 监视器使用 TLM 端口与记分板和覆盖率收集器通信。TLM 端口将句柄传递给 sequence_items。UVM 方法允许环境写入器决定通过 TLM 端口通信的 sequence_items 类型。最常见的类型有 同一 sequence_item 类型用于捕获 DUT 输入值和 DUT 输出值。虽然使用了相同的 sequence_item 类型,但输入和输出被捕获到不同的对象中,因为这些值是在不同的仿真时间捕获的。这就是例 15 中显示的样式。 不同的 sequence_item 类型用于捕获 DUT 输入和 DUT 输出。例如,可能会有一个 my_tx_in sequence_item 类,该类只有用于存储 DUT 输入的变量,而 my_tx_out sequence_item 类只有用于存储特定接口 DUT 输出的变量。 UVM 还允许 Environment Writer 确定监控器需要多少个通信端口,以便将信息传递给记分板和覆盖率收集器。常见的编码方式是使用两个端口,一个用于包含 DUT 输入的 sequence_item 对象句柄,另一个用于包含 DUT 输出的 sequence_item 对象句柄。例 15 中的监视器使用了两个端口。这两个端口都与 my_tx 序列项类型的句柄通信,该类型的变量可捕获 DUT 输入值和 DUT 输出值。根据协议的不同,也可以使用请求和响应项目类型,或任何合适的项目集。 这些端口是 TLM 分析端口,允许一对多连接。这样,包含 DUT 输入值的 sequence_item 对象的句柄就可以同时传递给记分板和覆盖范围收集器。uvm_analysis_port 是一个参数化的类,必须与特定的 sequence_item 类类型(本例中为 my_tx)配合使用。 端口是在监视器的 build_phase 阶段构建的。请注意,端口是使用类的 new() 构造函数而不是工厂构造的。uvm_analysis_port 基类名称没有在工厂注册,因此不能由工厂构造。也没有理由这样做。工厂的目的是允许测试编写者用一种类类型替换另一种类类型(例如,用 USB 3.0 sequence_items 替换所有 USB 2.0 sequence_items)。对于一个测试来说,改变监视器使用的端口类型是没有意义的,因此不需要工厂来构造端口。 ### Monitor/Interface 通信 监控器通过读取顶层模块实例化并连接到 DUT 的接口中的变量来观察 DUT 的输入和输出值。监控器使用静态 uvm_config_db::get()方法检索指向接口的虚拟接口,与驱动程序检索虚拟接口的方式完全相同。 ### The Monitor’s Run Phase DUT 输入和输出值的实际监控在 UVM 运行阶段进行。通常情况下,输入和输出会在不同的仿真时间捕获,也可能在不同的时钟上捕获。为了与不同的时间和时钟同步,run_phase 会分叉出两个无限循环,这两个循环并行运行。与驱动程序一样,监控器也采用无限循环编码,因此只要 run_phase 在运行,监控就会一直持续。测试编写程序通过使用 UVM 的反对标志来控制 run_phase 的实际执行时间。 在这些循环的每个周期中,监控器都会创建一个 sequence_item 对象,将 DUT 值捕获到该对象中,然后使用 write() 方法将该对象的句柄广播到分析端口。写()方法的实际功能将在分析端口所连接的每个分析 imp 导出中实现。我们将在覆盖率收集器(第 3.6 节)和记分板(第 3.7 节)组件的代码中看到写入()方法的实现。 重要的是,监控器要为监控器循环的每次传递创建一个新的 sequence_item 对象。记分板将收到 sequence_item 对象的句柄。如果记分板保存了该句柄以便在未来某个时间进行评估,而监测仪没有为循环的每次传递创建一个新对象,则会出现问题。每次循环都会捕获新的 DUT 值。如果这些新值保存在上一循环中使用的同一对象中,记分板最终将保存多个对象句柄,这些句柄都引用同一对象,而该对象仅存储最后一次捕获的一组值。有两种编码方式可用于在每次循环中创建新的 sequence_item 对象:在每次循环开始时使用 factory create() 方法,或在每次循环结束时使用 sequence_item clone() 方法。为便于说明,例 15 中的代码展示了这两种方式,不过大多数环境编写者可能会使用其中一种方式。 ## UVM 覆盖范围收集器 功能覆盖是验证复杂设计的一个重要方面,尤其是在使用约束随机验证时。功能覆盖是 SystemVerilog 语言不可分割的一部分,包括 covergroup、coverpoint 和 bins 定义以及 sample() 方法。UVM 并未规定在何处使用覆盖构造。它们只是 SystemVerilog 结构,可以在任何 SystemVerilog 代码中使用。尽管方法论没有强制要求,但 UVM 代理是在 UVM 测试平台中收集功能覆盖信息的理想位置。代理的监控器会观察 DUT 针对特定总线协议的输入和输出,因此可以收集针对该总线观察到的值的覆盖率。 UVM 也没有规定应在代理的哪个位置收集覆盖。有些工程师喜欢将覆盖率作为监控器的一个组成部分。这是一种简单的方法,不需要使用 TLM 端口将值传递给覆盖率收集代码。不过,作者建议定义一个可在代理中实例化的独立覆盖率收集器组件。使用单独的组件需要更多的编码(虽然不多),但也有一些优点。覆盖率收集器组件将代理的覆盖率定义和功能封装在一个地方,而不是将覆盖率代码分散在监控器中。此外,单独的组件可以由工厂构建,这使得使用工厂定制覆盖范围以满足特定测试要求变得更加容易。 ```verilog *> 覆盖率收集器 class my_coverage_collector extends uvm_subscriber #(my_tx); my_tx tx; *> the transaction object on which value changes will be covered covergroup dut_inputs; option.per_instance = 1; *> track coverage for each instance Opc: coverpoint tx.opcode; Opa: coverpoint tx.operand_a; Opb: coverpoint tx.operand_b; endgroup `uvm_component_utils(my_coverage_collector) function new(string name, uvm_component parent); super.new(name, parent); dut_inputs = new(); *> construct the covergroup endfunction: new function void write(my_tx t); tx = t; *> copy transaction handle received from the monitor dut_inputs.sample(); *> collect coverage endfunction: write function void report_phase(uvm_phase phase); `uvm_info("DEBUG", $sformatf("\n\n Coverage for instance %s = %2.2f%%\n\n", this.get_full_name(), this.dut_inputs.get_inst_coverage()), UVM_HIGH) endfunction: report_phase endclass: my_coverage_collector
UVM Scoreboards UVM 中记分板的主要作用是验证实际 DUT 输出是否与预测输出值相匹配。UVM 规定这种验证功能是 UVM 环境的一部分,但这也是方法论的终点。如何预测预期结果和验证 DUT 输出是 SystemVerilog 编程的一部分,完全取决于环境编写者的创造力。这种灵活性使 UVM 能够真正通用于处理任何规模和类型的设计,但也导致 UVM “专家 “的建议存在很大差异。本文提出了一些 UVM 记分牌的指导原则,这些原则遵循著名的 KISS 原则–”保持简单,愚蠢”。 KISS 准则规定,记分板功能应封装到在环境级实例化的 UVM 组件中。环境提供与一个或多个代理的连接,使记分板组件能够访问这些代理观察到的 DUT 输入和输出值。测试编写者可配置环境,包括配置记分板组件,以满足特定测试的需要。当记分板功能被封装到 UVM 组件中时,记分板可以更容易地重复使用,并可扩展以添加特定测试功能。UVM 工厂可以将标准记分板组件与扩展版本互换。这种 KISS 准则还可以验证更复杂的设计,利用多个环境来验证设计的不同部分。 相对简单的记分牌功能可直接在记分板组件(扁平层次结构)中编码。扁平化的计分板组件通常包含一个预测预期结果的方法和一个将预测结果与实际 DUT 输出进行比较的方法。记分板组件内部也可以有层次结构。预测算法和验证算法可在与记分板实例化的类中定义。这些子块也可配置,并可在工厂注册,以实现工厂创建和覆盖功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 *> 记分板声明与基本结构 `uvm_analysis_imp_decl(_verify_outputs) class my_scoreboard extends uvm_subscriber #(my_tx) ; `uvm_component_utils(my_scoreboard) function new (string name, uvm_component parent); super .new (name, parent); endfunction : new uvm_analysis_imp_verify_outputs #(my_tx, my_scoreboard) dut_out_imp_export; mailbox #(my_tx) expected_fifo = new ; int num_passed, num_failed; function void build_phase(uvm_phase phase); dut_out_imp_export = new ("dut_out_imp_export" , this ); endfunction function void write(my_tx t); my_tx expected_tx; $cast (expected_tx, t.clone ()); case (t.opcode ) ADD: expected_tx.result = t.operand_a + t.operand_b ; SUB: expected_tx.result = t.operand_a - t.operand_b ; endcase expected_tx.exception = ...; expected_fifo.put (expected_tx); endfunction : write function void write_verify_outputs(my_tx t); ... endfunction : write_verify_outputs function void report_phase(uvm_phase phase); ... endfunction : report_phase endclass : my_scoreboard
这个小例子使用了扁平化层次结构——预测与验证算法均直接编码在记分板类中。