通用验证方法学

通用验证方法学（Universal Verification Methodology, UVM）是一个以SystemVerilog类库为主体的验证平台开发框架，验证工程师可以利用其可重用组件构建具有标准化层次结构和接口的功能验证环境。它是第一个由电子设计自动化领域三巨头（Cadence、Synopsys和Mentor Graphics）联合支持的验证方法学，其最新版本为1.2版。

## 概述

最原始、最简陋的Verilog测试平台通常会做以下几件事情：第一，在测试平台模块中例化被测设计；第二，创建若干变量（通常是`reg`类型），将它们连接到被测设计的输入端口，并在过程代码块（通常是`initial`块）中的不同时间点对它们进行赋值，使这些变量充当被测设计的输入激励；第三，采集被测设计输出端口的信号，或者直接采集被测设计的内部信号；第四，在测试平台模块内建立一些功能模块，将采集到的信号与根据之前设计规范预测的参考数值进行对比，然后输出一些信息以方便故障的排除。

这样的验证模式对于简单的被测设计尚可，但是如果被测设计的复杂程度较高，这样的测试平台将会变得十分臃肿，而且不利于工程师维护和重用。例如，如果验证人员想在原有输入激励基础上添加一系列新的测试序列，则很可能会牵一发而动全身，影响测试平台的其他部分。工程师需要将验证平台的不同组分，例如输入激励、输出采集、记分板对比等不同部分相互隔离。在超大规模集成电路已经是主流的今天，Verilog结构化的编程方式使代码的复用成为了一个严重的难题。

虽然SystemVerilog的面向对象编程特性提供了解决了上述问题的可能，但是仍然存在一些问题。工程师有了更灵活的语言，但是怎么用这种语言来搭建验证平台却是没有明确规范的，因而不同人搭建的验证平台在结构上会有差异。最明显的是，不同工程师对于验证平台功能的划分可能不一样，即使采用同样的划分，其中同类模块对外的接口（方法和数据成员）也没有统一的标准。这在一定程度上阻碍了大型验证平台之间的协作性、扩展性（假设某公司的两个团队分别负责验证平台的输入激励部分和输出信号比较部分，却各自使用了非标准化的事务接口，其结果是他们将耗费大量的时间对各自的代码进行修改，这将大大延缓芯片的上市时间）。验证方法学提供了一套基于SystemVerilog的类库，验证工程师以其中预定义的类作为起点，就可以建立起具有标准结构的验证平台。

由于行业竞争的关系，历史上不同厂商曾推出了数种略有差异的验证方法学。使用不同验证方法学的验证平台之间的互操作性仍有一定的障碍。为了进行实现验证方法学的标准化，早在2009年12月，Accellera（电子设计自动化行业的一个致力于标准化的组织）内部就通过投票，决定以之前的开放验证方法学2.1.1版为基础，构建一个新的功能验证方法学。在此之前，不同的电子设计自动化厂商相继推出了自己的验证方法学，例如开放验证方法学就是Cadence和Mentor Graphics合作推出的。这种情况就造成了不同验证方法学之间的协同工作需要工程师额外的精力。2011年2月21日，Accellera通过了通用验证方法学的1.0版，并得到了三大厂商的共同支持。目前最新版本为1.2版。

## 类库结构

以下列出了通用验证方法学的类库结构，注意这与验证平台中实例的层次关系有所不同

- uvm_void
- uvm_object
- uvm_transaction
- uvm_sequence_item
- uvm_sequence
- uvm_report_object
- uvm_component
- uvm_sequencer
- uvm_driver
- uvm_monitor
- uvm_agent
- uvm_scoreboard
- uvm_env
- uvm_test
- uvm_phase
- uvm_configuration

## 例化被测设计的顶层模块

采用通用验证方法学的验证平台最大的特点就是将其中不同功能的代码以标准化的规范相互分离。整个验证平台中最顶层的划分是被测设计实例化和测试本身的划分。划分之后，被测设计（通常是硬件描述语言）可以在一个顶层Verilog（或SystemVerilog）模块中被调用。除此之外，这个非面向对象的顶层模块还必须完成以下几个任务，来为之后建立被测设计和测试（面向对象部分）之间的联系做准备：

- 实例化一个或多个SystemVerilog接口（`interface`）
- 将接口中的若干信号和被测设计实例的对应信号端口（`port`）相连
- 执行`initial`过程代码块中的`run_test()`方法调用（这个方法的具体定义无需验证人员关注）
- 在配置数据库（`uvm_config_db`，有时也称之为工厂）里使用`set`静态方法注册用到的接口

## 顶层模块和测试类之间的关联

上面简历的这个顶层模块（非面向对象部分）不属于测试类所在的类、对象层次结构之中，一般使用常规的Verilog或SystemVerilog模块（`module`）来构建。这种结构不具备面向对象特性，它和测试类（面向对象部分）的联系是通过以下两步建立的。

### run_test()方法

首先，当顶层模块执行`run_test()`方法（这是个类库自带的方法，调用者无需关心其具体定义）时，通用验证方法学引擎会在后台执行一系列操作来建立测试环境。验证人员在启动类似Synopsys VCS之类的仿真工具之前需要制定测试名称，仿真工具执行到`run_test()`方法时会去查找名称与之相同的测试类（由`uvm_test`类继承），找到之后会创建这个测试类的一个对象（在整个面向对象部分，这个对象的实例化由通用验证方法学引擎而非用户完成，且该实例名称为`uvm_test_top`），然后根据人为的实例化语句来逐层创建测试类中的环境类、代理类、驱动器类等的对象层次结构）。这些对象所属的类都是从通用验证方法学预定义的基类继承而来，验证人员需要覆盖一些特定名称的方法，通用验证方法学的引擎在执行仿真时会自动调用这些方法，来达到驱动被测设计、采样所需信号的目的

### 接口

接口（`interface`）是连接验证平台顶层模块（非面向对象部分）和测试类（面向对象部分）的桥梁（参见总线功能模型）。

顶层模块还必须将所用的接口在工厂，即配置数据库（`uvm_config_db`，详见本条目后面相关章节）里注册（执行`set`静态方法），然后测试所在的面向对象部分可以在工厂里将其取出（执行`get`静态方法）。这是因为，顶层模块并不在测试部分的类属层次关系之中，因此顶层模块与测试之间的关联必须使用这样的特殊的方式建立起来。在配置数据库中注册的接口相当于一个大的全局变量，联系着整个验证平台的非面向对象硬件描述语言部分（被测设计实例）和面向对象硬件验证语言部分（真正测试部分各种类的实例，具有面向对象编程的若干层次）。

测试类获得这个接口之后，会负责将它在测试类这一端连接到测试类内部的相关组件（如驱动器、监视器等）。顶层模块的被测设计和测试类不直接通过信号名称匹配相连（类似Verilog中的端口连接），而是分别只对接口内的信号进行读写。

#### 虚接口

接口是在顶层模块被实例化，并与被测设计实例进行连接的。理论上，测试类部分的成分，例如驱动器、监视器，也需要实例化一个接口，并通过配置数据库与顶层模块中的接口实例进行匹配。测试类中的这个接口在实例化时，必须使用关键字`virtual`关键字进行声明。将配置数据库中的接口取出，并赋值给此虚接口，就可以将测试类和顶层模块联系起来。

## 测试类

由测试基类`uvm_test`继承。

整个验证平台中除去顶层模块、被测设计之外的面向对象部分，其顶级类被称为测试类。测试类通常负责实例化一个环境、一个序列，并调用序列实例的`start(sequencer)`方法来启动一个环境实例的代理实例的序列产生器。

### 环境类

由环境基类`uvm_env`继承。

记分板实例、订阅者实例（如功能覆盖统计模块）也在环境中被创建，它们通常不直接读写那个与顶层模块直接相连的接口。与那个接口发生直接关系的驱动器、监视器都由环境实例内的代理实例包裹。通过使用通信接口，事务包由代理生成，传输给记分板和覆盖统计模块。

#### 代理类

由代理基类`uvm_agent`继承。

##### 序列发生器类

由序列发生器基类`uvm_sequencer`继承。这一继承是参数化继承，需要指定与其相关的序列项目类的名称。

序列发生器不需要定义数据成员和方法。一旦高层次的测试中创建了一个序列实例，然后在测试类实例中执行了类似`sequence.start(env.agent.sequencer)`这样的开启方法，序列发生器实例就在后台将连串的数据包发送出去（开发人员无需关注数据包是怎么发送出去的，这些操作由`uvm_sequencer`这个基类中定义的方法来完成）。开发人员只需要在驱动器类中调用其基类成员之一的`seq_item_port`通讯端口的`get_next_item(sequence)`方法，驱动器就能获得数据包。

##### 驱动器类

由驱动器基类`uvm_driver`继承。这一继承是参数化继承，需要指定与其相关的序列项目类的名称。

驱动器是代理中直接与被测设计发生作用的部分。它本身并不知道如何产生激励信号，它只是调用`get_next_item(sequence)`方法，从序列发生器接收的一系列数据包，然后将包中对应的信号通过赋值施加到整个测试平台的公共总线接口上，从而间接地给被测设计提供了所需的输入激励。

##### 监视器类

由监视器基类`uvm_monitor`继承。

监视器是一个被动的模块，通过接口和被测设计中的信号相连。获得要监测的数据包（或者单独的信号）之后，监视器只负责将这些信息通过通讯端口发送出去，至于谁来接收这些信息则由其他模块类关心。通常的做法是，在监视器类里声明分析端口`uvm_analysis_port`类的实例，调用该实例的`write`方法将获取的信息放置到分析端口上。这相当于监视器向外界“广播”内容，然后由对此感兴趣的模块（订阅者）负责接收。

#### 记分板类

由记分板基类`uvm_scoreboard`继承。

记分板不直接与被测设计发生互动，而是通过通讯端口，从监视器那里获取后者提供的事务（数据包）。记分板的`run_phase()`方法（任务）通常会提取出数据包中的信号，然后将它们与预设的参考模型进行对比，从而验证被测设计的功能是否与预期相同。如果不同，验证人员可以选择在屏幕上显示提示或警告，或者直接中断仿真的运行。

#### 订阅者类

由订阅者基类`uvm_subscriber`继承。这一继承是参数化继承，需要指定与其相关的序列项目类的名称。

覆盖统计模块通常是一个订阅者。

### 序列类

由序列基类`uvm_sequence`继承。这一继承是参数化继承，需要指定与其相关的序列项目类的名称。

序列类不需要定义任何与`phase`有关的方法，开发人员只需要覆写`task body()`方法（任务）。此方法内部一般会调用`uvm_do(sequence)方法宏，其结果是使用定义好的序列项目来输出一系列事务序列（一连串事务包，或数据包），这些事务序列将有驱动器接收，进而施加给被测设计。如果序列项目类的数据成员被声明为随机变量（例如使用`rand`），随机化方法`randomize()`会在调用`uvm_do生成数据包时被隐式地调用。

## 序列项目类

由序列项目基类`uvm_sequence_item`继承。

序列项目定义了了数据包每一帧包含哪些数据成员，这些数据成员可以被声明为SystemVerilog的随机变量类型（`rand`或`randc`）。通常不需要在这个类中定义任何方法。这个类的实例会作为序列类的`uvm_do(sequence_item)方法的参数，自动进行数据的随机化（前面这个方法宏内嵌了`randomize()`这样的随机化方法，这些都在后台由验证方法学引擎控制运行），进一步产生连串的数据包。

## 使用虚序列产生器和虚序列

虚序列产生器、虚序列本质上是序列产生器类和序列类的实例。

有时存在多个序列来负责产生生针对被测设计不同部分的输入数据包，并进一步需要使用多个独立的序列产生器来连接不同的驱动器。这时通常会使用虚序列产生器和虚序列来调度多个这样的操作。主要需要以下几步：

- 定义一个虚序列产生器类，这个类内部包含若干实序列产生器类成员
- 定义一个虚序列类，这个类内部包含若干实序列类成员，并在`body()`方法（任务）中调用`uvm_do_on（sequence_item, sequencer)方法宏，来指定哪个实序列项目（数据包的一帧）对应哪个实序列发生器
- 在环境类或代理类中创建一个虚序列发生器类实例，并在合适的`phase`将实序列发生器的句柄赋值给虚序列发生器类中的实序列发生器成员
- 在测试类创建一个虚序列类实例，使用配置数据库来指定需要运行序列的`phase`的默认序列（`default_sequence`字段）。这里不需要再使用实序列的`start()`方法来启动对应的序列发生器

## Phase的自动运行机制

通用验证方法学测试类及其成员所属的类都是从预定义的基类继承，因此都具有有一系列以`uvm_phase`类的实例为参数的方法，分别对应该对象的建立、连接、运行等阶段。开发人员只需要在自己的类定义里覆写这些方法，验证方法学引擎就可以在运行测试时依次自动执行这些方法内的代码（这些方法不允许被开发人员在自己的代码里直接调用）。

主要的类方法（其中仅`run_phase`为任务，占用仿真时间）的原型：

- `function void build_phase(uvm phase phase);`：实例化测试类中的组件
- `function void connect_phase(uvm phase phase);`：连接事务通讯端口
- `function void end_of_elaboration_phase(uvm phase phase);`
- `function void start_of_simulation_phase(uvm phase phase);`
- `task run_phase(uvm phase phase);`：指定仿真运行时，此类的实例所执行的动作
- `function void extract_phase(uvm phase phase);`
- `function void check_phase(uvm phase phase);`
- `function void report_phase(uvm phase phase);`
- `function void final_phase(uvm phase phase);`

## 配置数据库

前面提到了在顶层模块中实例化和使用的接口可以通过配置数据库（`uvm_config_db`）传递给测试类。配置数据库本身是个参数化的类。配置数据库的使用方法通常为调用其静态方法而非实例化，最常用的两个静态方法分别为`set()`（存）和`get()`（取）。和实例方法使用`.`（点号）来连接实例名和实例方法名来调用不同，静态方法使用`::`（双冒号），类的作用域操作符）来连接类名和静态方法名来调用。

## 消息报告

Verilog和SystemVerilog都提供了一个`$display`方法用于字符串显示的。这对于简单的测试平台较为实用，但是试想当验证平台十分庞大，所运行的测试也极为复杂时，测试运行过程中会产生海量的字符串信息，验证人员将花费额外的经历从中筛选与某个故障相关的信息。通用验证方法学以已有的SystemVerilog消息显示方法为基础，建立了一套更加完善的消息报告机制。工程师不再直接使用SystemVerilog自带的消息显示方法，而是调用验证方法学定义的较高层次的宏来输出字符串。通过在启动测试时指定`+UVM_VERBOSITY=<VERBOSITY_LEVEL>`，可以对消息进行过滤。

通用验证方法学的消息报告主要是通过调用以下几个宏名称对应的方法（方法已由类库定义）来完成：

- `uvm_info(ID, MESSAGE, VERBOSITY)：普通消息，方法需要三个参数，分别为消息ID、消息主体（字符串）和消息级别（verbosity），如果不指定消息级别，则默认为`UVM_LOW`
- `uvm_warning(ID, MESSAGE)：警告，方法需要两个参数，分别为消息ID和消息主体
- `uvm_error(ID, MESSAGE)：错误，方法需要两个参数，分别为消息ID和消息主体。
- `uvm_fatal(ID, MESSAGE)：致命错误，方法需要两个参数，分别为消息ID和消息主体。直接中断仿真

消息级别从低到高有：`UVM_NONE`、`UVM_LOW`、`UVM_MEDIUM`、`UVM_HIGH`、`UVM_FULL`和`UVM_DEBUG`。如果调用消息方法时指定的消息级别低于启动测试时指定的消息过滤级别（前面提到的`<VERBOSITY_LEVEL>`，其值也是从低到高的消息级别中的一个），那么消息会被显示出来，否则会被过滤掉。