SV、UVM与验证思想

写在前面:本文是笔者最近学习SV和UVM总结所得,主要参考资料为SV官方文档以及UVM白皮书,部分为网上其他博客摘录整理,之后也会陆续更新。若有错误之处,请在评论区或私信给我指出。感谢!

1 SV基础

待补充:
全局变量和局部变量的作用域是什么,静态变量和动态变量的区别是什么
package的作用使用,使用package和include有什么区别
SV的覆盖率如何声明,如何采集覆盖率,bin的数量如何确定,交叉覆盖率的作用是什么,ignore和illegal的bin如果触发了有什么区别

1.0 阻塞赋值和非阻塞赋值

  1. 阻塞赋值:=。指同一个always块中,后面的赋值语句在前面一句赋值结束后再开始赋值,顺序执行。
  2. 非阻塞赋值:=>。赋值时先计算非阻塞赋值符号右边的表达式,之后前后的语句是同时赋值的,并行执行,一般时序逻辑用非阻塞。

1.0 SV和V的比较

SV是验证语言,建模能力更强,有点像V和C的结合;V是设计语言,更侧重对硬件电路的具体描述。

  1. SV有接口,接口把各种信号封装在一起,来进行不同模块之间的信号交流
  2. SV加了断言,来做验证
  3. SV还有一些C语言的数据类型,比如用户自定义类型,typedef;枚举类型,还有引用和const常量
  4. 还有一些比如C里的跳转语句,break,continue,return等

1.1 SV数据类型

  1. 内建数据类型:(四值逻辑有)interger,logic,reg,线网(wire, tri),time(unsigned);(二值逻辑有)byte, short int, int, long int, bit(unsigned),real(双精度浮点数)
  2. 定宽数组:宽度要定义好,在编译时确定

1.2 定宽数组

  1. 定宽数组:声明时定义数组宽度,编译时是知道宽度的
  2. 定宽数组的类别:
    (1)合并数组:如 bit [3:0] [7:0] array,数组大小的定义必须是[MSB: LSB],不能是[Size] 。在存储时是连续的
    (2)非合并数组:如 bit [7:0] array2[3:0] 或 bit[7:0] array2[4],在内存里是不连续存储的

1.3 动态数组

  1. [] 来声明,之后用 new[length] 来分配空间,用delete来删除。在运行时再确定数组大小,编译时不知道宽度。
  2. 常用方法:
    声明:int dyn [];
    分配空间:dyn = new[5];
    删除所有元素/清空数组:dyn = dyn.delete();
    返回数组大小:dyn.size()

1.4 队列

  1. [$] 来声明,队列下标为0到$。是FIFO逻辑,先进先出的,可以在任何地方添加、删除元素,索引任一元素。
  2. 常用方法:
    插入:q.insert(1,5); // 在1号位插入5(最左边的是0号位)
    删除:q.delete(1); // 删除1号位元素 q.delete(); // 清空整个队列
    排序:sort
    搜索:search
    在队列头插入数据:q.push_front(5);
    在队列尾插入数据:q.push_back(5);
    从队列头输出数据:data = q.pop_fount;
    从队列尾输出数据:data = q.pop_back;
    add
    remove

1.5 关联数组

  1. 一般用来存储超大容量的稀疏数组,因为大容量数组里一般有很多空间不会被存储和访问。关联数组可以保存有效的数据元素,只为实际写入的数据分配空间。声明时在方括号里放用来索引的数据类型 [数据类型],其中索引作为key,索引对应的数据作为value
  2. 常用方法:
    声明:bit [31:0] a_array [string]; // 用string作索引
    返回元素个数:a_array. num()
    删除:a_array.delete(‘string0’); // 删除string0对应的key和value a_array.delete(); // 清空队列
    检查是否存在:a_array.exists(‘string0’);
    输出第一个key:a_array. first(var); // 输出第一个key赋给var
    输出最后一个key:a_array. last(var);
    将上个key输出:a_array. prev(var); // 如果前面没有key,就返回第一个key
    将下个key输出:a_array. next(var); // 如果后面没有key,就返回最后一个key

1.6 数组方法:定位排序

  1. 常用定位方法:返回值都是队列,关联数组返回的队列的数据类型和关联数组的索引的类型一样,其他数组返回int类型的队列
    返回满足条件的所有元素find with(…)
    返回满足条件的所有元素索引find_index with(…)
    返回满足条件的第一个元素find_first with(…)
    返回满足条件的第一个元素索引:find_first_index with(…)
    返回满足条件的最后一个元素find_last with(…)
    返回满足条件的最后一个元素索引:find_last_index with(…)
    返回最大/小元素max() / min()
    返回只出现过一次的元素(唯一值)unique()
    返回唯一值的索引:unique_index()
  2. 常用排序方法:
    逆序颠倒reverse() // 不能加with
    升序排列(从小到大):sort()
    降序排列(从大到小):rsort()
    随机打乱shuffle() // 不能加with
  3. 常用缩减/运算方法:
    求和:sum()
    求积:product()
    相与:and()
    相或:or()
    异或:xor()

1.7 类型转换与$cast

  1. 隐式转换:不需要具体的转化的操作符,直接用赋值。比如把4bit的数据转换到5bit,先做位宽拓展,再赋值。
  2. 显式转换:需要操作符或者转换函数
    (1)静态转换:在转换的表达式前加上单引号,不对转换值做检查,可能会转换失败,而且不报错
    (2)动态转换:用 $cast(target,source),是一个向下类型转换。比如一个类的句柄指向了子类对象,可以用cast来把新的子类句柄指向这个子类对象。还可以用来检查enum枚举类型有没有越界,如果越界了就返回0。

1.8 ref 和 const

  1. ref:表示引用,而不是复制,如果直接传递参数,参数会被复制到堆栈区,但是用ref进行参数传递,比如传到function或者task里,在内部修改这个参数,传递进来的原参数也会跟着改变。
  2. 如果只是希望利用ref这个引用,而不复制到堆栈区的优点,又怕不小心改动了这个参数的值,就在前面加const

1.9 任务task,函数function,虚函数virtual

  1. (verilog和SV中)任务task可以消耗时间而函数function不能。即function内部不能带如 #100 的延时,或如 @(posedge clock), wait(ready) 等阻塞语句
  2. (verilog中)函数function内部不能调用任务task(因为task消耗时间);(SV中)函数function可以调用任务task,但一定要在fork…join_none语句生成的线程中调用
  3. (verilog中)函数function必须有返回值return且返回值必须被使用(如用到赋值语句中);(SV中)函数function可以没有返回值,需要定义为void function且内部不消耗时间
  4. 虚函数:对于普通的函数,子类中重写的方法在父类中不可见,而如果在父类中定义一个虚函数,子类对应的重写就在父类中可见了,主要是用来父类看看子类干了什么的。当定义了virtual时,在子类中调用某task/function时,会先查找在子类中是否定义了该 task/function,如果子类没有定义,则在父类中查找。未定义virtual时,只在子类中查找,没有定义就是编译器报错。所以如果某一class会被继承,则用户定义的task/function(除new(),randomized(),per_randomize(),pose_randomize()外),一般就会加上virtual关键字,以备后续扩展

1.10 class和module的区别

  1. class:动态对象,可以在仿真的生命周期中销毁
  2. module:静态对象,在仿真期间始终存在。
  3. interface也是静态对象,所以只能用在module这种静态对象中;driver等components为动态的对象类,所以driver中用virtual interface,需要通过指针指向实际的interface

1.11 断言

  1. 立即断言immediate assertion:非时序的,在某一时刻检测当前的值或者判断一个条件,
  2. 并发断言concurrent assertion:是时序性的,可以有sequence和property,用蕴含操作符,需要遵循时钟周期。
  3. 断言中的sequence:描述了跟时钟周期相关的行为,可以包含布尔表达式,还有一些操作符,##延时时钟周期、重复操作[*n]等。可以在module,interface里声明,但是不能在class里。可以构成property。
  4. property:里面可以用sequence,还可以用蕴含操作符。
  5. 交叠蕴含和非交叠蕴含:交叠蕴含的符号是 |-> ,如果前面的为true,就在当前周期马上计算后面的内容。非交叠蕴含的符号是 |=> ,如果前面的为true,就在下一个周期计算后面的内容。
  6. sequence和property的区别和联系
    (1)任何在sequence中的表达式都可以放到property中
    (2)任何在property中的表达式也可以放到sequence中,但只有property中才能使用蕴含操作符
    (3)property中可以实例化其他property和sequence,sequence中也可以调用其他sequence,但不能实例化property
    (4)property需要用cover/assert/assume等关键词进行实例化,但sequence直接调用即可

1.12 fork线程

  1. fork…join:内部语句并发执行,但内部若有begin…end,则begin…end中的语句还是顺序执行。等内部语句都执行完了,再继续执行join后面的语句
  2. fork…join_none:不会等创建的线程完成,而是直接执行join_none后面的语句(父线程和子线程同时运行(父线程优先级更高))
  3. fork…join_any:等待至少一个线程完成,再执行join_any后面的语句(任何一个子线程被调用,父线程都会继续运行)

1.13 wait和@的区别

  1. -> 用来触发事件
  2. wait@ 用来等待事件:
    (1)@ 是阻塞的,只有等到@的事件被触发了,对应的进程才会往后执行,而且它类似于边沿触发,如果@等待事件和->触发事件在同一个时间开始执行,也就是会产生竞争,因为@是边沿等待,时间很短,在竞争中可能就等不到这个事件。
    (2)而wait基本相反,是非阻塞的,还会和triggered函数搭配使用,triggered来检测这个事件有没有被触发过,或者正在触发,如果wait执行的时候这个事件已经触发了,wait就不等了,而且wait是电平等待,哪怕有竞争,也就是事件的触发和wait是同时执行的,wait也能等到这个事件。不过在循环里等待事件的时候要用@,因为wait是电平触发,这个循环触发了,下个循环还是这个电平,继续触发,会形成一个零延时循环

1.14 事件event、旗语semaphore和信箱mailbox

  1. 事件event:用来实现线程的同步,用**->**符号来触发事件,用wait或@来等待事件,还有triggerred函数来检测这个事件有没有被触发过或者正在触发。
  2. 旗语semaphore:用来实现对同一资源的访问控制,避免两个并行进程同时对这个数据进行修改,比如一条总线有多个Master,但只允许一个来驱动,就可以用旗语。用new来创建带有单个或者多个钥匙的旗语,用get来获取钥匙,用put来放钥匙,返回钥匙,还有try_get来非阻塞地获取钥匙,返回1表示有足够的钥匙,返回0表示钥匙不够
  3. 信箱mailbox:类似一个用来连接发送端和接收端的FIFO,相当于直接在不同的进程中搭建了一个桥梁,在桥上可以传东西;用new来实例化,例化的时候有一个可选参数size,表示信箱的容量,也可以不指定,信箱容量就是无限大的;用put把数据放进信箱里,用get来从信箱里读数据,它们都是阻塞的,如果信箱满,put会被阻塞,信箱空,get会被阻塞。

1.15 随机化和约束randomization&constraints

  1. 为什么要随机化和约束:相比于定向测试,随机测试可以减少很多代码量,产生更多样的激励,提高验证的效率。而没有约束的随机化会在产生有效激励的同时产生很多无效激励,非法激励。所以需要约束来限定激励的合法取值范围,还可以指定各个数值的随机权重分布,来提高效率。
  2. 随机化的对象:(1)基本配置:通过寄存器来随机化一些基本的模块或系统本身的配置;(2)外部环境配置:通过随机化验证环境,比如时钟信号,外部的一些信号像触发信号等;(3)原始的参数数据:比如数据位宽。
  3. 随机化的方法:(1)rand关键词:对变量进行随机化;(2)randc关键词:对变量进行周期随机化,在限定范围内,所有值都赋值了一圈后才可以重复,实现的效果类似于用rand对这个变量在给定范围内随机赋值,每次生成一个值都存到队列里,下次用rand随机的时候要先判断随机出的值跟队列里有没有重复,如果不重复才对把值赋给这个变量,直到所有值都随机过一遍,就清空这个队列,再继续下一轮的随机;(3)std::randomize() 函数:对一个类进行随机,这个类内部可以放约束constraint;(4)dist关键词:在约束中产生随机数的权重分布,:= 表示范围内的权重都等于这个值, : / 表示权重值均分到范围里的每一个值;(5)inside关键词:表示变量在某个集合里随机化;(6)randomize() with:增加额外的约束,和类内的约束是等效的,但如果内外约束冲突,随机化的求解会失败;(7)->if-else来表示条件约束:->前的条件若真,则执行->后面的约束
  4. 常用的随机函数:(1)$random() :均匀分布,返回32bit有符号随机数;(2)$urandom() :均匀分布,返回32bit无符号随机数;(3)$urandom_range() : 指定范围内的均匀分布,有两个参数,一个是范围的上限值,一个是可选的下限值。
  5. 随机化和约束的控制:(1)constraint_mode() 用来打开或关闭约束;(2)soft来修饰软约束,当与其他约束冲突时,软约束的优先级更低。
rand int data1, data2;
constraint c0 {
	data1 dist {0 := 40, [1:3] := 60}; // weight = 40, 60, 60, 60 for 0, 1, 2, 3
	data2 dist {0 :/ 40, [1:3] :/ 60}; // weight = 40, 20, 20, 20 for 0, 1, 2, 3
}
rand int data3;
constraint c1{
	data3 inside { [0:3] };
	}

1.16 Program和Module

  1. program:跟module类似,但是不能有module,always块,interface,program,可以调用其他module块里的task或function
  2. module:内部可以有program,但是不能调用其他program块里的task或function
  3. 为什么要有program:program在SV的仿真调度机制里是在Reactive Region集合里执行的,而Module是在Active Region集合里执行的,为了把验证代码和RTL代码分开,减少竞争冒险。

1.17 SV的仿真调度机制Scheduler

  1. 为什么有仿真调度机制:写SV的时候很多进程都是并行的,但是实际在软件里里仿真的时候,都是CPU上串行执行的。所以SV需要一个从并行到串行的转换机制,来规定一些并行的语句在CPU串行执行的时候实际上的先后顺序,做一个顺序或者说是优先级的调度,就叫做仿真调度机制。
  2. 离散事件执行模型Discrete event execution:仿真是基于时间片进行的,是离散的。这个模型就是仿真基于事件的建模,还有事件的优先级的调度。事件包括了:
    (1)更新事件update event:仿真过程中,信号每次的变化就是一次更新事件,进程是对更新事件敏感的。
    (2)求值事件evaluation event:当一个更新事件被执行时,所有对该更新事件敏感的进程都会被求值,这个顺序是任意的,求值过程本身就是一种事件,叫做求值事件。
    仿真的时候,更新事件和求值事件交替执行。
  3. 时间片Time slot/step:仿真是基于离散的时刻点来进行的,各种事件都是在某一个时间片被触发,一个时间片上的事件全部执行完之后,再执行下一个时间片。
    #1step:step的时间单位是定义的最小的时间精度,比如1ps,就是仿真的时候最小的时间单位
  4. 事件区域Event Regions:通过划分事件区域来确定执行顺序。(按顺序:)
    (1)Pre-poned Region:对于一个新的SV指令,采样数据,为断言做准备
    (2)Active Region set:有3个(Active Region, Inactive Region, NBA Region),执行RTL代码的module里定义的各种事件。Active Region计算阻塞赋值,还有非阻塞赋值的右侧表达式的结果,传给后面NBA Region;Inactive Region计算#0延迟下的阻塞赋值;NBA Region把非阻塞赋值的右侧表达式的结果赋值给左边
    (3)Observed Region:断言相关的区域,把第一个Preponed Region采样到的数据拿来计算断言
    (4)Reactive Region set:有3个(Reactive Region, Re-Inactive Region, Re-NBA Region),验证相关的区域。 Reactive Region要执行program里的阻塞赋值,还有计算非阻塞赋值右边的表达式的值,并且作为更新事件调度到后面的Re-NBA Region;Re-Inactive Region执行program里#0延迟的进程,比如fork…join_none后面加一个#0,本来父线程和子线程的优先级是一样的,这样内部的子线程可以优先于父线程执行;Re-NBA Region执行前面Reactive Region传过来的非阻塞赋值右边的值赋值到左边。
    (5)Post-poned Region:调用monitor函数,输出更新值;收集功能覆盖率(strobe函数采样的)

2 UVM基础

2.1 UVM的优势和劣势、方法学的演变

  1. 优势:相比于用SV直接搭验证平台,优势主要是可复用性,相当于构建了一个验证平台的框架,之后往里填内容,不用从头搭起,大大减少了搭建验证平台初期的工作量,像phase机制,把各种行为写到对应的phase里去,它就会自己按照顺序执行,而且UVM的验证平台和测试用例是分开的,验证平台搭好之后其实一般不会大改,但是测试用例,激励,sequences这些在验证的过程中可能需要很多修改,更新,迭代,把它们分开来比较好单独处理。此外还有:目前一些主流的EDA工具,Cadence/Mentor/Synopsys的都支持UVM,可移植性比较强;支持覆盖率驱动的验证;支持寄存器模型
  2. 劣势:需要另外学,学起来需要时间
  3. 验证方法学的演变
    (1)最开始Cadence发布了eRM,用e语言,包括了激励和check的策略,还有覆盖率模型;
    (2)之后Synopsys发布了RVM
    (3)再往后Mentor发布了AVM,是用systemverilog和systemC实现的,提出了TLM事务级建模;
    (4)AVM的同一年,Synopsys发布了VMM,是改进版的RVM,RVM是基于vera语言,Synopsys可能觉得SystemVerilog比较有前景,就发布了基于SystemVerilog的VMM,这个VMM里还加了callback机制;
    (5)之后差不多的时间,Cadence也发布了基于SystemVerilog的方法学,叫URM,里面也有TLM,还有factory,config_db;
    (6)后来Cadence和Mentor合作发布了一个OVM
    (7)再然后就是UVM了,是Accellera公司在OVM的基础上推出的,并且加上了VMM的callback机制。

2.2 UVM树形结构

  1. UVM树:
    在这里插入图片描述
  2. UVM常用类的继承关系:
    在这里插入图片描述

2.3 UVM组件Components

  1. driver:从sequencer接收transaction,再传给DUT,也就是给DUT提供各种激励驱动,来模拟DUT的真实使用情况
  2. sequencer:将sequence产生的transaction传给driver
  3. scoreboard:check DUT的输出是否符合预期
  4. monitor:driver负责把transaction级别的数据转变成DUT的端口级别,并驱动给DUT;monitor的行为与其相对,用于收集DUT的端口数据,并将其转换成transcation交给后续的组件如reference model, scoreboard等处理
  5. env:一个容器类,把各个组件打包在一起,然后在env里例化这些组件
  6. agent:有的组件如driver和monitor的部分代码高度相似,本质是因为二者处理同一种协议。由于这种相似性,UVM将二者封装到一起,成为一个agent。不同的agent代表了不同的协议
  7. reference model:当DUT在执行某指令时,验证平台也必须相应完成同样的指令,得到输出。完成这个过程的即参考模型
  8. transaction:各组件之间信息的传递是基于transaction的。一般来说,物理协议中的数据交换都是以帧或包为单位的,通常在一帧或者一个包里要定义好各项参数,每个包的大小不一样。transaction就是用于模拟这种实际情况,一笔transaction就是一个包

2.4 启动测试(用例)的方式

  1. 在顶层模块的run_test()中添加测试用例名
  2. 是在命令行中指定测试用例(如+UVM_TESTNAME = test1)

2.5 Components和Objects的联系与区别

  1. 联系:uvm_component派生自uvm_object。因此components继承了许多objects的特性,同时又有一些自己的特质。
  2. 区别:(1)components有两个特性是objects没有的,一是通过在new的时候指定parent参数来形成树形的组织结构,所有UVM树的结点都是有component组成的,二是具有phase机制的自动执行特点。(2)components是静态实体,没有生命周期的,从仿真开始一直存在到仿真结束,而objects有生命周期,是在中途产生,中途结束的;(3)components始终连接到硬件或者TLM端口,而objects不会连接这些

2.6 uvm_component_utils和uvm_object_utils的区别

  1. utils宏定义注册机制保证了object/components进行正确factory操作所需的基础结构。

2.7 接口interface和virtual interface

  1. interface接口:主要是用来简化模块之间的连接,还有实现类和模块之间的通信。它是对模块端口信号还有功能进行一个标准化的封装。可以在接口里定义变量,任务,和函数,modport, clocking,assertion,还可以用always和initial语句,写一些可能会复用的东西。比如总线接口,就会定义成interface。
  2. 接口中的modport:主要是提供了方向信息(信号的输入输出方向),来支持主从类型的通信。对于同一个interface,不同的组件可能有不同的视角,比如对于driver是输出的信号,而对于monitor可能是输入信号。所以引入modport来进行方向的支持。
  3. 接口中的clocking:规定了信号之间的时序关系。默认情况下interface里的信号是异步的,就可以通过clocking定义一组跟时钟同步的信号,还可以为clocking块中的信号设置建立时间与保持时间(默认都是1ns)
  4. 接口是可综合
  5. virtual interface:interface是静态对象,所以只能用在module这种静态对象中声明,不能在class这种动态对象里声明,所以就有了virtual interface,它可以用在class中。但是它是virtual的嘛,virtual指的就是指针,句柄,它本身没有什么内容,是要指向实际的interface的,在class中声明一个virtual interface,然后通过config_db机制来连接它们两,在module里set,在class里get,就把interface的内容赋给virtual interface了。

2.8 sequence机制

  1. 什么是sequence机制:sequence产生transaction,发给sequencer,再由driver驱动,传给DUT。一个sequence发送transaction前,要先发送一个请求,sequencer把这个请求放在一个仲裁队列中。sequencer要:检测仲裁队列里是否有某个sequence发送tr的请求;检测driver是否申请要tr
  2. 为什么要sequence:sequence机制是为了将test case和testbench分离开,对于一个项目而言,testbench是相对稳定的测试平台,而针对具体模块的test case差别很大,引入sequence可以比较灵活地分别处理这两部分。
  3. layer sequence:通过构建层次化的sequence,来与register model进行访问操作。
  4. virtual sequence:主要起到一个调度的作用。因为driver只能驱动一种实际的接口,对应一种transaction的sequence,如果要对多个接口同时激励,就需要virtual sequence/sequencer。这个virtual的意思是不产生具体的某种transaction,而是控制其他的sequence为相应地sequencer产生transaction,virtual sequence用来做不同类型sequence之间的调度的:(1)定义virtual sequencer,里面有各个env里的子sequencer类型的指针;(2)在base_test里实现virtual sequence的例化,通过function connect_phase实现子sequence和对应sequencer的连接;(3)定义virtual sequence,里面对各个sequence实例化,然后用uvm_do宏来进行sequencer的调度;(最后在具体的test定义里,用config_db机制把virtual sequencer的default_sequence设置为具体的virtual sequence)

2.9 phase机制

  1. 什么是phase机制:UVM提供的一个梳理执行顺序的方案,把代码写到对应的phase里,这些phase就会按照一定顺序自动执行。
  2. . phase有哪些:按照执行顺序有build_phase(进行组件的实例化,构建UVM树),connect_phase(通过类中定义的各种数据接口来连接各个组件),end_of_elaboration_phase(测试环境的微调,比如显示环境结构,打开文件等),start_of_simulation_phase(准备testbench的仿真,设置断点,设置初始配置值),run_phase,extract_phase(从tb中提取数据来观察设计的最终状态),check_phase(检查不期望的数据),report_phase(根据UVM_ERROR的数量来打印不同的信息,写到日志报告里),final_phase(关闭文件,结束仿真)
  3. run_phase:pre_reset_phase, reset phase(对DUT进行复位、初始化), post_reset_phase;pre_configure_phase, configure_phase(进行DUT的配置), post_configure_phase; pre_main_phase, main_phase(最主要的,DUT的运行), post_main_phase; pre_shutdown_phase, shutdown_phase(做一些跟DUT关机,断电相关的操作), post_shutdown_phase。(run_phase都是并行执行,其他的是串行。run_phase运行时间最长,从仿真开始到仿真结束,测试用例产生激励是在run_phase)
  4. function phase(不消耗仿真时间):build_phase(自顶向下,因为是最先执行,用来构建UVM树的,需要从根结点出发往叶子结点一步步构建,否则下层的类还没有通过上层类来进行实例化),其他(均自下向上)。比如connect_phase也是自下向上,因为需要先连好下层模块才能继续连接上层模块。
    task phase(消耗仿真时间):run_phase(自顶向下)
  5. 与phase相关的类:uvm_phase(定义了phase的行为、状态、内容),uvm_domain(phase的进度结点),uvm_bottomup_phase(实现bottomup函数),uvm_topdown_phase(实现topdown函数),uvm_task_phase(实现task phase,也就是run_phase)

2.10 Objection机制

  1. Objection机制:用来控制run_phase这种消耗仿真时间的phase的启动和结束,run_phase通过raise_objection来启动,最后通过drop_objection来结束,中间比如说,通过start来启动sequence,自动执行sequence里的body任务,等到sequence发送完后drop_objection。进入到某一个run_phase的时候,UVM会收集这个phase所有的raise_objection,并且实时监测它们有没有对应的drop_objection,如果所有的objection都被drop了,就关闭这个phase,到下一个phase,所有的phase都执行完了,就会调用$finish结束(关闭testbench)

2.11 factory工厂机制

  1. 什么是工厂机制:对testbench中的实例进行重写,提高代码的可重用性。
  2. 工厂机制的步骤
    (1)registration注册,对component类型用uvm_component_utils宏注册,对object类型用uvm_object_utils宏注册,如果component类或者object类带了参数的话,就用uvm_component_param_utils或uvm_object_param_utils宏注册;
    (2)construction对象的实例化,用component或object类型的对象用create方法来实例化;
    (3)overriding重写覆盖,对component或object类型的覆盖,来提高代码的可重用性,比如说用set_inst_override_by_type(original_type, override_type, full_inst_path)方法来局部覆盖,set_type_override_by_type(original_type, override_type)方法来全局覆盖。
    (4)之后可以用factory.print来检查覆盖的结果,也可以用uvm_top.print_topology来显示UVM的拓扑结构

2.12 config_db机制

  1. config_db的作用:uvm_config_db是一个参数化类,用于将不同类型的参数配置到uvm数据库中。通过set和get函数来在UVM各个模块间传递参数、接口,一般在build_phase使用。
  2. set函数:有4个参数:uvm_component实例的指针;相对于第一个参数的相对路径;传递参数的标记,和get的第三个参数一样;传递参数的值
  3. get函数:有4个参数:前两个参数含义和set一样;第3个参数也是传递参数的标记,和set的第三个参数一模一样;第4个参数是收信的参数变量名
  4. 省略get的情况:要用到field automation机制。首先收信的模块要在factory里注册;将收信模块中的目标变量在field automation机制实现;之后就睡自动调用build_phase里的super.build_phase()语句,自动执行相应的get函数
  5. set函数的优先级(不同模块对同一个参数有多个set):对于不同模块,取决于set函数的第一个参数的层次,越靠近UVM树的顶部,优先级越高;如果是同一个模块有多个set函数,后执行的set函数优先级更高
  6. uvm_config_db和uvm_resource_db的区别:二者都是参数化的类,用来将不同类型的参数配置到uvm数据库中,uvm_config_db 采取的策略是“parent wins”,会按照UVM树的层次,TOP层的配置优先写入。uvm_resource_db 采取的是“last write wins”,是最后写入的配置优先,如果top层和bottom层都对同一个变量进行配置,在build_phase因为是top-down的执行顺序,最后写入的是bottom层的配置,就会被当成有效值写入,这个在UVM树形层次结构里用着不方便。

2.13 field_automation域的自动化机制

  1. 什么是field_automation:可以在factory机制注册UVM类的时候,同时声明一些后面会用到的成员变量,比如用来做对象拷贝、克隆、打印之类的变量。
  2. field_automation使用方法:在factory机制注册一个component或object时,比如在uvm_component_utils_begin和end之间,用uvm_field_int宏来声明int变量,uvm_field_object宏来声明object句柄。这些宏都有两个参数(ARGument, FLAG),Argument是被声明的成员变量,FLAG是之后会参与的数据操作类型,比如UVM_ALL_ON, UVM_DEFAULT, UVM_COPY, UVM_COMPARE, UVM_PRINT等等

2.14 UVM的层次引用

build_phase构建UVM树的层次,之后层次引用是用来指定对象,比如要把sequencer类的对象连到driver的实例,可以在i_agt的connect_phase里用seq_item_port接口和seq_item_export接口把二者连起来(drv.eq_item_port.connect(sqr.seq_item_export);)

2.15 UVM的消息管理

  1. 信息的安全级别severity:反应的是信息的安全级别
    (1)UVM_INFO宏:用来打印信息,有3个参数,(1)id,字符串,用于把打印的信息归类;(2)message,字符串,具体需要打印的信息;(3)冗余级别Verbosity,UVM_NONE, UVM_LOW, UVM_MEDIUM(默认), UVM_HIGH,UVM_FULL, UVM_DEBUG,冗余级别越低越关键。
    (2) UVM_WARNING宏:只有前两个参数id和message,verbosity是UVM_NONE。用来打印一些warning信息
    (3) UVM_ERROR宏:只有前两个参数id和message,verbosity是UVM_NONE。用来打印一些error信息。可以在phase里借助set_report_max_quit_count函数来设置阈值,比如设置10个,当出现10个UVM_ERROR就会自动结束仿真
    (4) UVM_FATAL宏: 只有前两个参数id和message,verbosity是UVM_NONE。用来打印致命错误信息,如果出现了UVM_FATAL,仿真会马上停止。使用场景之一:
  2. 对冗余级别verbosity的打印控制:sim指令时加上+uvm_set_verbosity=veribosity,其中verbosity表示要打印信息的冗余程度,或者说是打印时容易被过滤的程度,比如UVM_LOW,那么所有冗余度低于UVM_LOW的都会被打印

2.16 uvm_do系列宏

  1. uvm_do的作用:创建一个transaction的实例,把它随机化,之后送给sequencer,等driver取走这个transaction后,返回一个item_done信号,uvm_do就执行完毕并返回,再执行下一次uvm_do,产生新的transaction
  2. 一系列的uvm_do宏
    在这里插入图片描述
    第一类:uvm_do类
    uvm_do:只有一个参数,即产生的transaction
    uvm_do_pri(ority):除了第一个参数transaction外,还有一个参数priority优先级,sequencer的仲裁机制根据transaction的优先级来进行选择。没指定优先级时默认为-1,指定时的数值是大于等于-1的整数,数字越大,优先级越高。
    uvm_do_with:第一个参数transaction,第二个参数constraints,在随机化时对transaction的某些字段进行约束。
    uvm_do_pri_with:前两者的结合
    第二类:uvm_do_on类
    uvm_do_on:on表示展示出来,就是显式地指定产生的这个transaction具体是哪个sequencer来发送。它有两个参数,对应的就是transaction的指针和sequencer的指针。而对于uvm_do而言,它默认的sequencer就是调用uvm_do宏的这个sequence在启动时指定的sequencer

2.17 uvm_create和uvm_send

  1. 除了用uvm_do来产生txn,还可以用uvm_create和uvm_send来产生。
  2. uvm_create 用来实例化txn,uvm_send 把txn发送出去,还有uvm_rand_send宏是对一个已经实例化的txn,先进行随机化,再发送出去。
  3. uvm_send宏的主要作用 (相比于uvm_do):如果一个txn占用的内存比较大,前后的txn可以使用同一块内存,只是里面的内容不一样,可以节省内存。

2.18 事务级建模TLM

  1. TLM的基本介绍
    (1)Transaction Level Model,最早是在AVM里被提出的,用来在模块之间比如monitor和scoreboard之间直接进行通信的一种模型,本来如果没有TLM的话,需要用config_db机制,在base_test里set,在monitor和scoreboard里get,如果它们两要通信,monitor就对应地改这个config_object,scoreboard监测它,看它变没变,但是这个过程很麻烦,而且需要base_test的参与,可能base_test有一个子类不小心改到这个变量就会有问题,所以就引入了TLM,给monitor和scoreboard专门搭了一个桥梁,进行通信。
    (2)TLM有两个通信的对象,叫initiatortarget,发起通信的叫initiator,响应的叫target,这个指的是控制方向,不是数据传输的方向。而根据数据传输的方向对应有productorconsumer,数据传输,也就是transaction从productor传到consumer。
  2. TLM的操作:有put,get,transportput就是initiator把一个transaction发给target,get就是initiator向target索取一个transaction,transport相当于put+get,initiator先发一个请求,put过去,然后get,从target那里回来一个response。这三种操作都对应的有阻塞,非阻塞,和又可以阻塞又可以非阻塞(比如uvm_blocking_put_port #(txn), uvm_nonblocking_put_port #(txn), uvm_put_port #(txn))。阻塞就是,比如说monitor来了一个txn,scoreboard可能在忙,那monitor就等着scoreboard忙完了,接收了这个txn,monitor再返回;非阻塞就是,monitor发了一下,要是scoreboard忙,它不会等,直接返回。
  3. TLM的端口与连接
    (1)基本端口PORT, EXPORT, IMP三种类型,在initiator端例化PORT,在中间层次例化EXPORT,在target端例化IMP,例化的时候要指定好自己的端口,对应的操作,阻塞还是非阻塞(比如uvm_blocking_put_port #(txn), uvm_nonblocking_put_port #(txn), uvm_put_port #(txn))。然后在env的connect_phase里用connect函数来连接,比如initiator.port.connect (target.export),PORT连到EXPORT,再EXPORT连到IMP;或者PORT直接连到IMP。
    (2)analysis系列端口:analysis_port和analysis_export端口:一个analysis端口可以连接多个IMP,类似广播,但是基本端口(put, get)默认是一对一的通信;analysis端口没有阻塞和非阻塞,因为是广播嘛,没什么等不等的,发了就返回了。
    (3)analysis端口只有write操作:在analysis_imp对应的component,要定义一个write函数,参数是传递的transaction。
    (4)uvm_tlm_analysis_fifo:比如在monitor和scoreboard之间加一个FIFO,FIFO做一个缓存的作用。没有FIFO的时候,Monitor发过来,scoreboard只能被动地选择接收或者忙的时候不接收,但是中间有了一个FIFO,就可以把数据先存到FIFO里,让scoreboard可以主动选择在不忙的时候接收transaction。monitor还是analysis_port,FIFO对着monitor和scoreboard的端口都是analysis_imp,而scoreboard也用port端口。还有几个FIFO相关的函数is_empty / is_full可以查FIFO现在是否空 / 满,used函数可以查FIFO里目前有多少个txn,还有flush是复位的时候可以用来清空FIFO。(FIFO的好处)用了FIFO之后,就不用在scoreboard里写write函数了,它需要transaction的时候就找FIFO拿(get操作),可以主动选择接收txn。

2.19 Register Model寄存器模型

  1. 什么是Register Model:是对DUT的register建模,把各种register和对应的操作封装在一起,然后这个register model通过一个中间变量uvm_reg_bus_op来对DUT进行Register控制。
  2. RAL:Register Abstraction Layer。
    (1)首先有一个uvm_reg_block:一般相同的base_address的register会放在同一个reg_block里。reg_block里有uvm_reg,对应的是DUT的每个register。uvm_reg里会有多个uvm_reg_field,对应的是这个register的每个bit field。
    (2)uvm_reg_block 里还有uvm_reg_map来做地址映射,通过base_address和各个register的offset,对应到实际的memory里的地址。还可以进行前门访问。
    (3)uvm_reg_block里还有uvm_reg_adapter,前门访问的时候会通过sequence产生一个uvm_reg_bus_op类型的变量,它需要在adapter里通过reg2bus和bus2reg的函数来转换成bus的transaction,再传给DUT。
    实现bus需要的txn和uvm_reg_bus_op之间的转换
    (4)另外还有uvm_memory,就是对DUT中的memory来建模用的。
    (5)uvm_reg_predictor:用来观察DUT的register值的变化,把register的值送给scoreboard。reg_predictor也需要map来跟register model连在一起,还要adapter来进行bus2reg的转换。
  3. 前门访问:register model产生sequence,发给bus,之后bus传给DUT,对DUT进行register的操作,它是真实的物理时序访问,要消耗仿真时间。用来验证所有register访问DUT的物理通路正常工作。
  4. 后门访问是不经过bus,通过一些HDL函数(add_hdl_path,uvm_hdl_read, uvm_hdl_deposit),把register的操作直接传给DUT,不消耗仿真时间。前门访问没问题的基础上,用后门访问来节省访问register的时间。

3 验证通识/思想

有哪些验证手段?动态仿真、形式验证都有什么优缺点
门级仿真的作用是什么,STA的作用是什么
给一个模块,要能够根据SPEC进行功能点的分解,提出覆盖率
受约束的随机验证优缺点是什么

3.1 验证方法与流程

功能验证

  1. 读specs文档还有接口文档,并提取功能点:写一个excel,根据specs列出具体的一个个小的功能点features和对应的各种寄存器的配置
  2. 根据spec文档和列出来的功能点,写testplan,对应各个功能点,列出之后需要写的各种testcase,收集覆盖率时要用到的断言,covergroup,coverpoint等
  3. 设计验证平台的结构,先确定可以复用的一些验证VIP,一般有总线的,时钟和复位的VIP,都会用上,然后确定为了这个IP本身在将来的复用,还需要把它的一些功能封装成VIP,每个VIP的对应一个agent,这个是最上层的,在这个agent里还有一些其他,比如基本的配置文件,sequencer,driver,monitor,这些VIP和DUT通过接口连接,所以在VIP内部还会有virtual interface来配套。还一定要有scoreboard,之后等DUT输出数据了,通过monitor送过去要和reference model的结果作比较,判断DUT输出的对不对
  4. 设计好验证平台的结构后,还要设计给DUT提供的激励有哪些,比如DUT的配置的随机化(在配置文件里一般会有register model,里面需要对各种register进行随机化,还有一些约束),还有一些总线,时钟复位VIP的配置随机化(比如时钟频率等,一般还会有约束,inside几种时钟频率);最主要的激励是sequences,给DUT输入的各种数据,控制信号等等,一种情形的激励要写成一个sequence class,所有的sequences放到一个大的文件里作为sequence library。
  5. 除了确定激励生成策略,还要设计check策略,主要是包括scoreboard和覆盖率,断言等等。scoreboard和监测DUT输出的monitor一般是通过TLM连接,scoreboard收到DUT的输出,要跟reference model的结果比较,判断DUT对不对。这个过程中还要收集覆盖率,需要写一些断言,covergroup,每个covergroup里还有一些coverpoint,每个coverpoint里会有一些bins,可能有的信号的值达不到,就写成ignore_bins。这些功能覆盖率相关的内容,在前面的testplan也会列出来,方便之后检查不能漏了。还有一些信号的组合需要写成cross
  6. 确定好这些之后,需要做一个review,和team讨论,修改验证计划
  7. 之后就可以开始用UVM搭建验证环境了
  8. 搭建好环境,需要再具体写详细的sequence,testcase,定义功能覆盖率
  9. 进行仿真,调试,每个testcase都要测过去,分析覆盖率,之后跑回归regression测试,反复修改迭代直到代码覆盖率和功能覆盖率都达到100%。过程中可能会测出DUT的bug,就跟设计那边沟通讨论
  10. 结束之后再开一次review,相比于第一次的review,这次要补充更多细节,包括过程中验出来的DUT的修改点

3.2 代码覆盖率与功能覆盖率

覆盖率用于评估验证的进度和testbench的完备性。

  1. 代码覆盖率: 评估的是代码实现上的覆盖率,结合exclusion机制,代码覆盖率一般可以达到100%,但不代表testbench是完善的
    分类:1. 从代码本身来看,有branch coverage, condition coverage, statement coverage, expression coverage, trigger coverage; 2. 从状态机上看,有state coverage, transition coverage, Multi state - transition coverage; 3. 从仿真波形上看,有toggle coverage。(加粗的是QuestaSim支持的coverage)
  2. 功能覆盖率:代码覆盖率只能体现目前已经实现的设计需求有多少是被测试过的,但是不能体现在测试计划里,有哪些设计需求是根本没被实现的。因此引入了功能覆盖率,它指设计需求的覆盖率。
    功能覆盖率通过covergroup来测试,每个covergroup里有若干个coverpoint,每个coverpoint有一组显式的bins值(功能覆盖率的衡量单位,最终的覆盖率等于采样覆盖到的bins数量除以总的bins数量)
  3. 如果代码覆盖率低,功能覆盖率高:说明写出来的设计需求或者covergroup基本都覆盖到了,但是代码实现里可能存在冗余,比如DUT有一些冗余的代码,导致这些冗余代码没有hit到。还有可能要检查一下功能覆盖里,会不会有covergroup本身写得不完善,比如有些采样值漏了,导致功能覆盖率是虚高,而相应地代码覆盖率是把这个问题给暴露出来了。
  4. 如果代码覆盖率高,功能覆盖率低:代码覆盖率高说明实现了的设计需求基本都正确实现了,那可以检查一下,有可能是有些设计需求根本就没实现;还有可能是测试激励数量少了,可以试试提高随机化次数或者增加一些随机化的约束;还有可能有的功能是一般情况下达不到的,是需要加到exclusion里的;还有可能有的cross自动生成了bins,但是我们不会hit到其中一部分bins,就可以用ignore_bins把这部分择出去。

3.3 OOP思想:三大特性

  1. OOP:指的是面向对象编程,有三大特性:封装,继承,和多态。
  2. 封装指把对象的属性、方法封装在内部,外部访问时需要通过get或set方法来调用;
  3. 继承指子类继承父类的属性和方法,并且可以另外定义自己的属性和方法,提高代码的可复用性。被声明为local的数据成员和方法只能对自己可见,对外部和子类都不可见;对声明为protected的数据成员和方法,对外部不可见,对自身和子类可见
  4. 多态指方法的重写(覆盖)和重载,重写(覆盖)的参数列表和返回值不能变,重载是定义了多个同名函数,并且有不同的参数列表,返回值也可以不同
  5. SV中的封装:静态变量/静态方法static和动态变量/动态方法automatic。static是仿真开始时就会被创建,直到仿真结束,可以被多个方法共享;automatic是进入该方法后自动创建,离开该方法后就被销毁
  6. SV中的继承:extends,父类子类
  7. SV中的多态:virtual关键字,比如virtual function在子类中就可以被重写(覆盖)。

虚函数:对于普通的函数,子类中重写的方法在父类中不可见,而如果在父类中定义一个虚函数,子类对应的重写就在父类中可见了,主要是用来父类看看子类干了什么的。当定义了virtual时,在子类中调用某task/function时,会先查找在子类中是否定义了该task/function,如果子类没有定义,则在父类中查找。未定义virtual时,只在子类中查找,没有定义就是编译器报错。所以如果某一class会被继承,则用户定义的task/function(除new(),randomized(),per_randomize(),pose_randomize()外),一般就会加上virtual关键字,以备后续扩展


版权声明:本文为Dabie_haze原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。