UVM学习笔记(一)

本文

芯片验证系列文章主要分为三部分:验证工程师与芯片验证、SV学习笔记、UVM学习笔记。此为 UVM学习笔记 部分第一篇,主要介绍验证方法学、类库地图、工厂机制和覆盖方法。

版本 说明
0.1 初版发布

参考

名称 作者 来源
《芯片验证漫游指南》 刘斌 书籍
《UVM实战》 张强 书籍

专业术语与缩略语

缩写 全称 说明

类库地图

写在前头

  • 在SV模块中,验证环境整体的构建,是从底层模块的验证组件搭建到通信和激励生成。
  • 这些元素无论是软件对象的创建、访问、修改、配置,还是组件之间的通信等都是通过用户自定义的方式来实现的。
  • UVM验证方法学作为之前所有方法学的融合版本, 从自身初衷而言,就是将验证过程中可以重用和标准化的部分都规定在其方法学的类库当中,通过标准化的方式减轻了验证人员构建环境的负担。

对验证环境的共同需求

  • 在可以看到对验证环境的共同需求是:
    1. 组件的创建和访问
    2. 环境的结构创建、组件之间的连接和运行
    3. 不同阶段的顺序安排
    4. 激励的生成、传递和控制
    5. 测试的报告机制
  • 由于软件环境中对象的生成是动态的,验证环境中的组件也需要UVM提供底层功能来完成对象的创建和访问。
  • 在组件创建之外,UVM也需要提供环境上下层次中 创建、连接和运行 组件的顺序控制方法,只有在底层机制上有效地保证这一点,才会避免可能发生的句柄悬空问题。
  • 在组件通信中, UVM也提供了功能更丰富的TLM(Transaction Level Model) 接口,这可以保证相邻组件的通信不再通过显式句柄引用,而是独立于组件的通信方式。
  • 对于测试序列(sequence) 的生成和传输也是利用了TLM传输在sequence和driver之间完成。而对于不同sequence的发送顺序控制, 也类似于SV测试MCDF子系统的要求, 需要实现sequence之间的灵活调度。
  • 为了便于验证环境的调试, UVM的报告机制可以将来自于不同组件不同级别的信息并且加以过滤.最终生成测试报告。

UVM世界观

  • UVM类库地图按照UVM的核心机制将地图进行了分块
    1. 核心基类
    2. 工厂(factory) 类
    3. 事务(transaction) 和序列(sequence) 类
    4. 结构创建(structure creation) 类
    5. 环境组件(environment component) 类
    6. 通信管道(channel) 类
    7. 信息报告(message report) 类
    8. 寄存器模型(register model) 类
    9. 线程同步(thread synchronization) 类
    10. 事务接口(transaction interface) 类

验证方法学

所处的验证时代

  • 国内验证起步较晚,历史包袱不重,在验证整合的尾期才开始发育,因此 验证技术的更替较少
  • 超过20年的IC公司,存在几代的验证结构和较难彼此复用的测试代码,由于项目复用和进度的考虑,这些本该淘汰的技术依然在使用中,这对工程师造成了额外的负担。
  • 原有的HDL描述语言受限于静态例化,无法随着仿真场景做动态变换。同时,天生地随机约束短板也让后期发展的功能覆盖率驱动验证方式没有可以依靠的专用验证语言。
  • 平台限定性语言在一开始符合IC研发的封闭生态特点,但验证工程师们的交流远甚于设计工程师(DVC on、SNUG、CDN Live等),而且在技术交流和人员流动的过程中,逐渐提出了统一验证语言的要求。
  • SystemVerilog从早先的Accellera 2002年的SystemVerilog 3.0标准逐步发展到IEEE-1800 SystemVerilog 2017标准, 经历了十五年的更新和完善,已经全面雄起为IC验证领域的霸主。
  • 上层的高级验证方法学也在2011年2月份之后逐步得到了融合,即UVM(Universal Verification Methodology)1.0的发布。
  • 现在步入芯片验证,可以躲过AVM、VMM、OVM的学习, 而直接学习融合以后的验证方法学UVM。
  • UVM融合的积极意义在于, 打通了各个EDA公司和IC设计公司的验证技能通道,便于验证技术交流和人才流动,也方便了IC设计公司的技术及工具选择。
  • 用户不再受限于使用何种仿真器、使用哪一家的验证IP,而只需要将主要精力着眼于设计的功能验证,由此也提升了验证效率。
  • SV的核心特性包括面向对象、随机约束、线程通信、功能覆盖率收集等,这些特性也为建立一个验证环境提供了足够多的便利。
  • UVM方法学的验证方法学通过吸取eRM(Spec man/e验证方法学) , AVM, OVM, UVM等之前不同方法学的优点, 可谓集众家之所长。

UVM的优势

  • 所有的验证方法学服务目的都在于 提供一些可以重用的类来减轻在项目之间水平复用和垂直复用的工作量 ,而同时对于验证新人又能提供一套可靠的框架,帮助他们摆脱搭建房子构思图纸的苦恼。
  • UVM面向所有数字设计,涵盖了从模块级到芯片级,ASIC到FPGA,以及控制逻辑、数据通路到处理器验证对象的全部场景。
  • UVM中的Universal(通用) 的含义代表的是该方法学可以适用于大多数的验证项目,而它自身提供的 基础类库(basic class library) 和基本验证结构 可以让具有不同软件编程经验的verifier们能够快速构建起一个结构可靠的验证框架。
  • UVM自定义的框架构建类和测试类能够帮助verifier 减轻环境构建的负担 ,进而 将更多的精力集中在如何制定验证计划和创建测试场景

UVM的发展历程

  • 2010年, 发布了UVM 1.0EA(Early Adopter) 版本。
  • 目前已经发展到UVM 1.2, 并且在2017年被IEEE宣布为正式标准, 即IEEE 1800.2
  • UVM1.1之前的演变进化更多地是在于汲取OVM的方法学框架以及创建UVM的寄存器模型。
  • 在UVM1.2版本的重要变化是UVM的消息机制更新和transaction记录能力的增强。

UVM的学习

  • 在UVM的演变发展历史中, 在新版本虫新的构建平台方式和测试方式可以同旧的方法并存。UVM的新版本在兼容老版本用法的同时,也注毁了一些之前的陈旧用法。
  • 在探索UVM世界时, 我们会遵循着下面的结构来帮助读者全面认识业界统一的验证方法学:
    1. 认识UVM世界的版图(类库) 和核心机制
    2. 学习核心的UVM组件和层次构建方式
    3. 了解常见的UVM组件间的通信方式
    4. 深入UVM测试场景的构成
    5. UVM的寄存器模型应用

工厂机制

写在前头

  • 工厂(factory) 机制是UVM的真正魅力所在。
  • 工厂机制也是软件的一种典型设计模式(design pattern)。

工厂的意义

  • UVM工厂的存在就是 为了更方便地替换验证环境中的实例或者注册了的类型 ,同时工厂的注册机制也带来了 配置的灵活性
  • 这里的实例或者类型替代,在UVM中称作 覆盖(override) ,而被用来替换的对象或者类型,应该满足注册(registration) 和多态(polymorphism) 的要求。
  • UVM的验证环境构成可以分为两部分, 一部分构成了环境的层次 ,这部分代码是通过uvm_component类完成, 另外一部分构成了环境的属性(例如配置)和数据传输 ,这一部分通过uvm_object类完成。
  • 这两种类的集成关系从UVM类库地图可以看到,uvm_component类继承于uvm_object类,而这两种类也是进出工厂的主要模具和生产对象。
  • 之所以称为模具,是因为通过注册,可以利用工厂完成对象创建。
  • 之所以对象由工厂生产,也是利用了工厂生产模具可灵活替代的好处,这使得在不修改原有验证环境层次和验证包的同时,实现了对环境内部组件类型或者对象的覆盖。

uvm_component和uvm_object

  • uvm_component和uvm_object将会伴随着整个UVM的学习。
  • 参照SV模块学习中的组件概念,即验证环境的不动产,大致包含:
    1. generator
    2. stimulator
    3. monitor
    4. agent
    5. checker/reference model
    6. environment
    7. test
  • 这些组件在uvm_component的子类中均有对应的组件。
  • SV中的非固定资产即那些TLM transaction,从generator流向stimulator的数据包,而这些类在UVM中统一由uvm_object表示。

uvm_{component/object}的例化

  • 每一个uvm_{component/object} 在例化的时候都应该给予一个名字(string)。
  • “fullname”指的是component所处的完整层次结构。
  • 在每个层次中例化的组件名称,应该独一无二(unique) 。
  • 创建component或者object的方法如下:
1
2
3
4
//创建uvm_component对象时:
comp_type::type_id::create(string name, uvm_component parent);
//创建uvm_object对象时:
object_type::type_id::create(string name);

工厂提供的便利–创建(create)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class comp1 extends uvm_component;
    `uvm_component_utils(comp1) //固定注册格式
    function new(string name="comp1", uvm_component parent=null);
        super.new(name, parent);
        $display($sformatf("%s is created", name));
    endfunction:new
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
    endfunction:build_phase
endclass
1
2
3
4
5
6
7
class obj1 extends uvm_object;
    uvm_object_utils(obj1)
    function new(string name="obj1");
        super.new(name);
        $display($sformatf("%s is created", name));
    endfunctionnew
endclass
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
comp1 c1, c2;
obj1 o1, o2

initial begin
    //SV的创建方式
    cl=new("cl");
    o1=new("o1");

    //UVM工厂的创建方式
    c2=comp1::type_id::create("c2", null);
    o2=obj1::type_id::create("o2");
end
  • c2和o2的例化方式也是最后通过调用new() 函数实现的。毕竟对于任何对象的例化,最终都要通过new()构建函数来实现的。
  • 一般来说运用factory的步骤可分为:
    1. 将类注册到工厂
    2. 在例化前设置覆盖对象和类型(可选的)
    3. 对象创建
  • 在两种类comp1和objl的注册中,分别使用了UVM宏uvm_component_utils和uvm_object_utils。
  • 这两个宏做的事情就是 将类注册到factory中 。在解释注册函数之前,我们需要懂得在整个仿真中,factory是独有的,即有且只有一个,这保证了所有类的注册都在一个“机构”中。

uvm_coreservice_t类

  • 该类内置了UVM世界核心的组件和方法, 它们主要包括
    • 唯一的uvm_factory, 该组件用来注册、覆盖和例化
    • 全局的report_server, 该组件用来做消息统筹和报告
    • 全局的tr_database, 该组件用来记录transaction记录
    • get_root() 方法用来返回当前UVM环境的结构顶层对象
  • 而在UVM-1.2中, 明显的变化是通过uvm_coreservice_t将最重要的机制(也是必须做统一例化处理的组件)都放置在了uvm_core serice_t类中。
  • 该类并不是uvm_component或者uvm_object, 它也并没有例化在UVM环境中, 而是独立于UVM环境之外的。
  • uvm_coreservice_t只会被UVM系统在仿真开始时例化一次。用户无需,也不应该自行再额外例化该核心服务组件。
  • 这个核心组件如同一个随时待命的仆人,做好服务的准备。
  • 理论上,用户可以获取核心服务类中的任何一个对象,例如uvm_default_factory对象, 继而直接利用factory来实现创建和覆盖。当然,创建和覆盖也可以由其它方式完成。

注册宏`uvm_{component/object}_utils

  • `uvm_component_utils用来注册组件类uvm_component
  • `uvm_object_utils用来注册核心基类uvm_object
  • 利用工厂注册机制的细节请参考红宝书P265,而对于验证工程师会使用uvm工厂注册是必要的,而具体实现机制细节不是必要的。
  • 无论对于uvm_component或者uvm_object,在UVM世界中, 请养成习惯使用注册宏`uvm_component/object_utils
  • 对于注册,并不是真正地将一个抽象的类型(空壳)放置在什么地方,而是通过例化该类的对象来完成。
  • 由于一种类型在通过宏调用时只注册一次,那么在不考虑覆盖的情况下,uvm_default_factory就将每一个类对应的对象都放置到了factory的字典当中。
  • uvm_default_factory::create_component_by_type() 经过代码简化,读者可以看到关键语句,它们首先检查处在该层次路径中需要被例化的对象,是否受到了“类型覆盖”或者“实例覆盖”的影响,进而将最终类型对应的对象句柄(正确的产品模板)交给工厂。
  • 有了正确的产品模板,接下来就可以通过uvm_component_registry::create_component() 来完成例化。

注册后的对象创建

  • uvm_component和uvm_object在创建时虽然都需要调用create()函数, 但最终创建出来的uvm_component是会表示在UVM层次结构中的, 而uvm_object则不会显示在层次中。
  • 这一点也可以从uvm_component::new(name, parent) 和uvm_object::new(name) 中看得出来。
  • uvm_component::new(name, parent) 保留两个参数, 就是为了通过类似“钩子”的做法,一层层由底层勾住上一层,这样就能够将整个UVM结构串接起来了。
  • uvm_object::new(name) 则没有parent参数, 因此也不会显示在UVM层次中, 只能作为configuration或者transaction等用来做传递的配置结构体或者抽象数据传输的数据结构体,成为uvm_component的成员变量。
  • 创建对象时,需要结合工厂的注册和覆盖机制来决定,应该使用哪一个类型来创建。

工厂创建compon/obiect的方法

  • 除了使用component/object来创建实例, 也可以利用factory来创建:
    • create_component_by_name()
    • create_component_by_type()
    • create_obj eat_by_name()
    • create_object_by_type()
  • 为了避免不必要的麻烦, 我们在使用宏`uvm_component_utils和`uvm_object_utils注册类型时, 宏内部就将类型T作为类型名Tname=‘T’注册到factory中去。这就使得通过上面的任何一种方法在创建对象时,不会受困于类型与类型名不同的苦恼。

component/object与工厂有关的方法

  • 配合工厂的注册、创建和覆盖的相关方法:
    • create()
    • create_component()
    • get()
    • get_type_name()
    • set_inst_override()
    • set_type_override()
  • 每一个uvm_component的类在注册时, 会定义一个新的uvm_component_registry类, 其如同一个外壳, 一个包装模板的纸箱, 在factory中注册时, 该纸箱中容纳的是被注册类的“图纸”,并没有一个“实例”。

建议

对于创建对象的方法,以上有工厂提供的创建对象的方法,有组件提供的创建对象的方法,并不需要全部掌握,而只需要记住 工厂提供的便利–创建(create) ,之所以讲这么多是在看到别人的环境中出现这些方法时,要知道是做什么的。

1
2
c2=comp1::type_id::create("c2", null);
o2=obj1::type_id::create("o2");

覆盖方法

工厂提供的便利–覆盖(override)

  • 覆盖机制可以将其原来所属的类型替换为另外一个新的类型。
  • 在覆盖之后,原本用来创建原属类型的请求,将由工厂来创建新的替换类型。
    1. 无需再修改原始代码,继而保证了原有代码的封装性。
    2. 新的替换类型必须与被替换类型相兼容,否则稍后的句柄赋值将失败,所以 新的类型要继承于原有的类型
  • 做顶层修改时,非常方便!
    1. 允许灵活的配置,例如 可使用子类来覆盖原本的父类
    2. 可使用不同的对象来修改其代码行为
  • 要想实现覆盖特性, 原有类型和新类型均需要注册
  • 当使用create()来创建对象时:
    1. 工厂会检查,是否原有类型被覆盖。
    2. 如果是,那么它会创建一个新类型的对象。
    3. 如果不是,那么它会创建一个原有类型的对象。
  • 覆盖发生时,可以使用 “类型覆盖”或者“实例覆盖”
    1. 类型覆盖指,UVM层次结构下的所有原有类型都被覆盖类型所替换。
    2. 实例覆盖指,在某些位置中的原有类型会被覆盖类型所替换。

set_type_override() 替换类型

1
static function void set_type_override(uvm_object_wrapper override_type, bit replace=1);
  • uvm_object_wrapper override_type 这是什么? 并不是某一个具体实例的句柄,实际上是注册过后的某一个类在工厂中注册时的句柄。怎么找到它呢? 就使用new_type::get_type() 。
  • bit replace=1
    1. 1: 如果已经有覆盖存在,那么新的覆盖会替代旧的覆盖。
    2. 0: 如果已经有覆盖存在,那么该覆盖将不会生效。
  • set_type_override是一个静态函数
1
2
ori_type::type_id::set_type_override(new_type::get_type())
//---typedef------|----静态函数-------|------静态函数--------|

set_inst_override() 替换实例

1
static function void set_inst_override(uvm_object_wrapper override_type, string inst_path, uvm_component parent=null);
  • string inst_path指向的是组件结构的路径字符串
  • uvm_component parent=null 如果缺省,表示使用inst_path内容为绝对路径,如果有值传递,则使用(parent.get_full_name(), ‘.', inst_path)来作为目标路径。
  • set_type_override是一个静态函数
1
2
ori_type::type_id::set_inst_override(new_type::get_type(), "ori_inst_path")
//----typedef-----|------静态函数-----|------静态函数-------|

如何使用覆盖相关的函数

  • 首先需要知道, 有不止一个类提供与覆盖有关的函数,然而名称与参数列表可能各不相同
    1. uvm_component::set_{type/inst}_override{_by_type}
    2. uvm_component_registry::set_{type/inst}_override
    3. uvm_object_registry::set_{type/inst}_override
    4. uvm_factory::set_{type/inst}_override
  • 因此,想要实现类型替换,也有不止一种方式。包括上述给的例子中通过ori_type::type_id来调用覆盖函数,还可以在uvm_component的域中直接调用,或者使用uvm_factory来做覆盖。

覆盖实例

 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
module factory_override;
    import uvm_pkg::*;
    `include "uvm_macros.svh"

    class comp1 extends uvm_component;
        `uvm_component_utils(comp1)
        function new(string name="comp1", uvm_component parent=null);
            super.new(name, parent);
            $display($sformatf("comp1:: %s is created", name));
    endfunction:new

    virtual function void hello(string name);
        $display($sformatf("comp1:: %s said hello!", name)) ;
    endfunction
endclass

class comp2 extends comp1; // 必须继承于comp1
    `uvm_component_utils(comp2)

    function new(string name="comp2", uvm_component parent=null);
        super.new(name, parent);
        $display($sformatf("comp2:: %s is created", name) );
    endfunction:new

    function void hello(string name);
        $display($sformatf("comp2:: %s said hello!", name));
    endfunction
endclass

//c1 c2 都是comp1
comp1 c1, c2;
initial begin
    // 覆盖
    comp1::type_id::set_type_override(comp2::get_type());

    // 两种例化方式
    c1=new("c1");
    c2=comp1::type_id::create("c2", null);

    //调用hello函数
    c1.hello("c1");
    c2.hello("c2");
end
endmodule

//输出结果
// comp1:: c1 is created
// comp1:: c2 is created
// comp2:: c2 is created
// comp1:: c1 said hello!
// comp2:: c2 said hello!

//可见new例化的对象没有实现覆盖, create例化的对象实现了覆盖

确保正确覆盖的代码要求

  • 将UVM环境中 所有的类都注册到工厂中, 并通过工厂来创建对象
  • 在使用某些类的时候,确保该类已经被 导入(import) 到当前域(scope) 中
  • 通过工厂创建对象时, 句柄名称应该同传递到create()方法中的字符串名称相同 。无论是通过层次路径名称来覆盖还是配置,将例化组件的句柄名称同创建时create()方法中的字符串名称保持一致。
  • 由于覆盖是采用 parent wins模式 ,也就是高层次的替换优先级高,因此要注意在同一个顶层build_phase()中 覆盖方法应发生在对象创建之前
  • 为了尽量保证运行时覆盖类可以替换原始类, 覆盖类最好是原始类的子类,而调用成员方法也应当声明为虚方法
  • 另外一种确保运行时覆盖类型句柄正确使用的方式,需要通过$cast()进行动态类型转换。

总结

  • 一旦把那些组件装载盒子注册之后,接下来的UVM环境搭建就变得更加容易、更方便日后维护了。
  • 整个UVM世界的构建, 离不开factory的三个核心要素: 注册、创建和覆盖
    1. `uvm_{component/object} _utils
    2. uvm_{component/object}::type_id::create()
    3. set_{type/inst} _override{_by_type}()
  • UVM学习中的一大阻碍就是,实现某一种效果的方法有很多种,但是对于初学者,你只需要掌握最常用的一种实现方式,就足够了!

文章原创,可能存在部分错误,欢迎指正,联系邮箱 cao_arvin@163.com。