队列:队列结合了链表和数组的优点,可以在一个队列的任何位置进行增加或者删除元素;
定宽数组:属于静态数组,编译时便已经确定大小。其可以分为压缩定宽数组和非压缩定宽数组:压缩数组是定义在类型后面,名字前面;非压缩数组定义在名字后面。Bit [7:0][3:0] name; bit[7:0] name [3:0];
动态数组:其内存空间在运行时才能够确定,使用前需要用new[]进行空间分配。
关联数组:其主要针对需要超大空间但又不是全部需要所有数据的时候使用,类似于hash
,通过一个索引值和一个数据组成,索引值必须是唯一的。
Fork join
:内部 begin end
块并行运行,直到所有线程运行完毕才会进入下一个阶段。
Fork join_any
:内部 begin end
块并行运行,任意一个begin end
块运行结束就可以进入下一个阶段。
Fork join_none
:内部 begin end
块并行运行,无需等待可以直接进入下一个阶段。
wait fork
:会引起调用进程阻塞,直到它的所有子进程结束,一般用来确保所有子进程(调用进程产生的进程,也即一级子进程)执行都已经结束。
disable fork
:用来终止调用进程 的所有活跃进程, 以及进程的所有子进程。
多线程之间同步主要由mailbox、event、 semaphore
三种进行一个通信交互。
mailbox邮箱
:主要用于两个线程之间的数据通信,通过put函数和 get 函数还有peek函数进行数据的发送和获取。
Event
:事件主要用于两个线程之间的一个同步运行,通过事件触发和事件等待进行两个线程间的运行同步。使用@(event)或者 wait(event.trigger)进行等待,->进行触发。
Semaphore
:旗语主要是用于对资源访问的一个交互,通过key的获取和返回实现一个线程对资源的一个访问。使用put和 get函数获取返回key。一次可以多个。
函数能调用另一个函数,但不能调用任务,任务能调用另一个任务,也能调用另一个函数
函数总是在仿真时刻0就开始执行,任务可以在非零时刻执行
函数一定不能包含任何延迟、事件或者时序控制声明语句,任务可以包含延迟、事件或者时序控制声明语句
函数至少有一个输入变量,可以有多个输入变量,任务可以没有或者多个输入(input)、输出(output)和双向(inout)变量
函数只能返回一个值,函数不能有输出(output)或者双向(inout)变量,任务不返回任何值,任务可以通过输出(output)或者双向(inout)变量传递多个值
Interface
是一组接口,用于对信号进行一个封装,捆扎起来。如果像verilog
中对各个信号进行连接,每一层我们都需要对接口信号进行定义,若信号过多,很容易出现人为错误,而且后期的可重用性不高。因此使用interface
接口进行连接,不仅可以简化代码,而且提高可重用性,除此之外,interface
内部提供了其他一些功能,用于测试平台与DUT之间的同步和避免竞争。
Clocking block
:在interface
内部我们可以定义clocking
块,可以使得信号保持同步,对于接口的采样vrbg和驱动有详细的设置操作,从而避免TB
与 DUT
的接口竞争,减少我们由于信号竞争导致的错误。采样提前,驱动落后,保证信号不会出现竞争。
封装、继承和多态
封装:通过将一些数据和使用这些数据的方法封装在一个集合里,成为一个类。
继承:允许通过现有类去得到一个新的类,且其可以共享现有类的属性和方法。现有类叫做基类,新类叫做派生类或扩展类。
多态:得到扩展类后,有时我们会使用基类句柄去调用扩展类对象,这时候调用的方法如何准确去判断是想要调用的方法呢?通过对类中方法进行virtual
声明,这样当调用基类句柄指向扩展类时,方法会根据对象去识别,调用扩展类的方法,而不是基类中的。而基类和扩展类中方法有着同样的名字,但能够准确调用,叫做多态。
Factory
机制也叫工厂机制,其存在的意义就是为了能够方便的替换TB中的实例或者已注册的类型。一般而言,在搭建完TB后,我们如果需要对TB进行更改配置或者相关的类信息,我们可以通过使用factory
机制进行覆盖,达到替换的效果,从而大大提高TB的可重用性和灵活性。
要使用factory机制先要进行:
将类注册到factory表中
创建对象,使用对应的语句 (type_id::create)
编写相应的类对基类进行覆盖。
Interface
是一组接口,用于对信号进行一个封装,捆扎起来。如果像 verilog
中对各个信号进行连接,每一层我们都需要对接口信号进行定义,若信号过多,很容易出现人为错误,而且后期的可重用性不高。因此使用interface
接口进行连接,不仅可以简化代码,而且提高可重用性,除此之外,interface
内部提供了其他一些功能,用于测试平台与DUT
之间的同步和避免竞争。
Clocking block
:在interface
内部我们可以定义clocking
块,可以使得信号保持同步,对于接口的采样和驱动有详细的设置操作,从而避免TB
与 DUT
的接口竞争,减少我们由于信号竞争导致的错误。采样提前,驱动落后,保证信号不会出现竞争。
动态数组:其内存空间在运行时才能够确定,使用前需要用new[]进行空间分配。
关联数组:其主要针对需要超大空间但又不是全部需要所有数据的时候使用,类似于hash,通过一个索引值和一个数据组成: bit [63:0] name[bit[63:0]];索引值必须是唯一的。
【关联数组】可以用来保存稀疏矩阵的元素。当你对一个非常大的地址空间寻址时,该数组只为实际写入的元素分配空间,这种实现方法所需要的空间要小得多。
此外,关联数组有其它灵活的应用,在其它软件语言也有类似的数据存储结构,被称为哈希(Hash)或者词典(Dictionary),可以灵活赋予键值(key)和数值(value) 。
UVM的启动 总结:
在导入uvm_pkg文件时,会自动创建UVM_root所例化的对象UVM_top,UVM顶层的类会提供run_test()方法充当UVM世界的核心角色,通过UVM_top调用run_test()方法.
在环境中输入run_test来启动UVM验证平台,run_test语句会创建一个my_case0的实例,得到正确的test_name
依次执行uvm_test容器中的各个component组件中的phase机制,按照顺序:
build-phase(自顶向下构建UVM 树)
connet_phase(自低向上连接各个组件)
end_of_elaboration_phase
start_of_simulation_phase
run_phase() objection机制仿真挂起,通过start启动sequence(每个sequence都有一个body任务。当一个sequence启动后,会自动执行sequence的body任务),等到sequence发送完毕则关闭objection,结束run_phase()(UVM_objection提供component和sequence共享的计数器,当所有参与到objection机制中的组件都落下objection时,计数器counter才会清零,才满足run_phase()退出的条件)
执行后面的phase
传递virtual interface到环境中;
配置单一变量值,例如int、string、enum等;
传递配置对象(config_object)到环境;
传递virtual interface到环境中;
a) 虽然SV可以通过层次化的interface的索引完成传递,但是这种传递方式不利于软件环境的封装和复用。通过使用uvm_config_db配置机制来传递接口,可以将接口的传递与获取彻底分离开。
b) 接口传递从硬件世界到UVM环境可以通过uvm_config_db来实现,在实现过程中应当注意:
c) 接口传递应发生在run_test()之前。这保证了在进入build_phase之前,virtual interface已经被传递到uvm_config_db中。
d) 用户应当把interface与virtual interface区分开来,在传递过程中的类型应当为virtual interface,即实际接口的句柄。
配置单一变量值,例如int、string、enum等;在各个test中,可以在build_phase阶段对底层组件的各个变量加以配置,进而在环境例化之前完成配置,使得环境可以按照预期运行。
传递配置对象(config_object)到环境;
在test配置中,需要配置的参数不只是数量多,可能还分属于不同的组件。对这么多层次的变量做出类似上边的单一变量传递,需要更多的代码,容易出错且不易复用。
如果整合各个组件中的变量,将其放置在一个uvm_object中,再对中心化的配置对象进行传递,将有利于整体环境的修改维护,提升代码的复用性。
UVM
其实就是SV
的一个封装,将我们在搭建测试平台过程中的一些重复性和重要的工作进行封装,从而使我们能够快速的搭建一个需要的测试平台,并且可重用性还高。但是UVM
又不仅仅是封装。
Ref参数类型是引用
向子程序传递数组时应尽量使用ref
获取最佳性能,如果不希望子程序改变数组的值,可以使用const ref
类型
在任务里可以修改变量而且修改结果对调用它的函数随时可见。
UVM中component
也是由object
派生出来的,不过相比于object
, component
有很多其没有的属性,例如phase
机制和树形结构等。在UVM
中,不仅仅需要component
这种较为复杂的类,进行TB
的层次化搭建,也需要object
这种基础类进行TB的事务搭建和一些环境配置等。Item是object
Sequencer
:负责将数据转给driver
driver
负责数据的发送;driver
有时钟/时序的概念。
Agent
:其实只是简单的把driver
,monitor
和sequencer
封装在一起。
Agent
:对应的是物理接口协议,不同的接口协议对应不同的agent
,一个平台通常会有多个 agent
。
Env
:则相当于是一个特大的容器,将所有成员包含进去。
Virtual sequencer
主要用于对不同的agent
进行协调时,需要有一定顶层的sequencer
对内部各个agent
中的sequencer
进行协调
virtual sequencer
是面向多个sequencer
的多个sequence
群,而sequencer
是面向一个sequencer
的sequence
群。
Virtual sequencer
桥接着所有底层的sequencer
的句柄,其本身也不需要传递item
,不需要和driver
连接。只需要将其内部的底层sequencer
句柄和sequencer
实体对象连接。
在多个sequence
同时向sequencer
发送item
时,需要有ID信息表明该item
从哪个sequence
来,ID信息在sequence
创建item
时就赋值了。
代码覆盖率——是针对RTL设计代码的运行完备度的体现,包括行覆盖率、条件覆盖率、FSM覆盖率、跳转覆盖率、分支覆盖率,只要仿真就可以收集,可以看DUT的哪部分代码没有动,如果有一部分代码一直没动看一下是不是case没有写到。
功能覆盖率---与spec
比较来发现,design
是否行为正确,需要按verification plan
来比较进度。用来衡量哪些设计特征已经被测试程序测试过的一个指标
首要的选择是使用更多的种子来运行现有的测试程序;
其次是建立新的约束,只有在确实需要的时候才会求助于定向测试,改进功能覆盖率最简单的方法是仅仅增加仿真时间或者尝试新的随机种子。
验证的目的就是确保设计在实际环境中的行为正确。设计规范里详细说明了设备应该如何运行,而验证计划里则列出了相应的功能应该如何激励、验证和测量
断言覆盖率:用于检查几个信号之间的关系,常用在查找错误,主要是检查时序上的错误,测量断言被触发的频繁程度。
这个问题很重要,建议好好准备,面试的时候经常会问~
芯片架构-RTL设计-功能仿真-综合&扫描链的插入(DFT)-等价性检查-形式验证-静态时序分析(STA)-布局规划-布局布线-布线图和原理图比较-设计规则检查-GDII
find的队列应该是返回队列的值,一般的话是和with配合使用,find index应该是返回索引值
形式验证指从数学上完备地证明或验证电路的实现方案是否确实实现了电路设计所描述的功能。形式验证方法分为等价性验证、模型检验和定理证明等。
形式验证主要验证数字IC设计流程中的各个阶段的代码功能是否一致,包括综合前RTL代码和综合后网表的验证,因为如今IC设计的规模越来越大,如果对门级网表进行动态仿真,会花费较长的时间,而形式验证只用几个小时即可完成一个大型的验证。另外,因为版图后做了时钟树综合,时钟树的插入意味着进入布图工具的原来的网表已经被修改了,所以有必要验证与原来的网表是逻辑等价的
首先不可能百分百完全完备,即遍历所有信号的组合,这既不经济也不现实。
所以只能通过多种验证方法一起验证尽可能减少潜在风险,一般有这些验证流程:ip级验证、子系统级验证、soc级验证,除这些以外,还有upf验证、fpga原型验证等多种手段。
前端每走完一个阶段都需要跟设计以及系统一起review
验证功能点,测试用例,以及特殊情况下的波形等。
芯片后端也会做一些检查,像sta、formality、DFM、DRC检查等,也会插入一些DFT逻辑供流片回来测试用。流片归来进行测试,有些bug可以软件规避,有些不能规避,只能重新投片
严格意义上有2种:
通过sequence.start的方式显示启动
通过default sequence来隐式启动也可以通过‘uvm_do系列宏启动
易维护:采用面向对象思想设计的结构,可读性高,由于继承的存在,即使改变需求,那么维护也只是在局部模块,所以维护起来是非常方便和较低成本的。
质量高:在设计时,可重用现有的,在以前的项目的领域中已被测试过的类使系统满足业务需求并具有较高的质量。
效率高:在软件开发时,根据设计的需要对现实世界的事物进行抽象,产生类。使用这样的方法解决问题,接近于日常生活和自然的思考方式,势必提高软件开发的效率和质量。
易扩展:由于继承、封装、多态的特性,自然设计出高内聚、低耦合的系统结构,使得系统更灵活、更容易扩展,而且成本较低。
用来触发事件时,使用->;用来等待事件使用@或者wait。
权重约束 dist:有两种操作符::=n :/n 第一种表示每一个取值权重都是n,第二种表示每一个取值权重为n/num。
条件约束 if else 和->(case):if else 就是和正常使用一样;->通过前面条件满足后可以触发后面事件的发生。
范围约束inside:inside{[min:max]};范围操作符,也可以直接使用大于小于符号进行,但是不可以连续使用,如 min
除了driver、monitor、agent、model、scoreboard、env、test之外全部用uvm_object。
get_next_item()
是一个阻塞调用,直到存在可供驱动的sequence item
为止,并返回指向sequence item
的指针。
try_next_item()
是非阻塞调用,如果没有可供驱动的sequence item
,则返回空指针。
And
指的是两个序列具有相同的起始点,终点可以不同。
Intersect
指的是两个序列具有相同的起始点和终点。
Or
指的是两个序列只要满足一个就可以
Throughout
指的是满足前面要求才能执行后面的序列
break
语句结束整个循环。
continue
立即结束本次循环,继续执行下一次循环。
return
语句会终止函数的执行并返回函数的值(如果有返回值的话)。
return
之后,function
里剩下的语句不能执行,其是终止函数的执行,并返回函数的值。
触发器:时钟触发,受时钟控制,只有在时钟触发时才采样当前的输入,产生输出。
锁存器由电平触发,非同步控制。在使能信号有效时锁存器相当于通路,在使能信号无效时锁存器保持输出状态。触发器由时钟沿触发,同步控制。
锁存器对输入电平敏感,受布线延迟影响较大,很难保证输出没有毛刺产生;触发器则不易产生毛刺
主要是编写sequence
,然后在body
里面根据测试功能要求写相应的激励,然后再通过ref_model
和checker
判断功能是否实现?
可以写脚本让它们自动执行,例如makefile...
例如让你写abcd四个信号在时钟沿处监测,当cd同时为1时,在时钟的前两个周期要ab同时为1的断言
通过constraint_mode(0)
关闭默认范围的约束块
constraint_mode(1)
是打开约束
可以用soft
关键字修饰特定的约束语句,这样既可以让变量在一般的情况下取默认值,也可以直接给变量赋默认值范围外的取值。
队列的使用方法:insert,delete,push_back和pop_front
Push
插入,pop
取出Front
前边,back
后边
rand
修饰符:rand
修饰的变量,每次随机时,都在取值范围内随机取一个值,每个值被随机到的概率是一样的,就想掷骰子一样。
randc
修饰符:randc
表示周期性随机,即所有可能的值都取到过后,才会重复取值
通信分为,单向通信,双向通信和多向通信
单向通信:指的是从initiator
到target
之间的数据流向是单一方向的
双向通信:双向通信的两端也分为initiator
和target
,但是数据流向在端对端之间是双向的
多向通信:仍然是两个组件之间的通信,是指initiator
与target
之间的相同TLM端口数目超过一个时的处理解决办法。
blocking阻塞传输的方法包含:
Put():initiator
先生成数据Tt
,同时将该数据传送至target
。
Get():initiator
从target
获取数据Tt
,而target
中的该数据Tt
则应消耗。
Peek(): initiator
从target
获取数据Tt
,而target
中的该数据Tt
还应保留。
通信管道:
TLM FIFO
:可以进行数据缓存,功能类似于mailbox
,不同的地方在于uvm_tlm_fifo
提供了各种端口(put、get、peek)
供用户使用
analysis port
:一端对多端,用于多个组件同时对一个数据进行处理,如果这个数据是从同一个源的TLM
端口发出到达不同组件,则要求该端口能够满足一端到多端,如果数据源端发生变化需要通知跟它关联的多个组件时,我们可以利用软件的设计模式之一观察者模式实现,即广播模式
analysis TLM FIFO
a. 由于analysis
端口提出实现了一端到多端的TLM
数据传输,而一个新的数据缓存组件类uvm_tlm_analysis_fifo
为用户们提供了可以搭配uvm_analysis_port
端口uvm_analysis_imp
端口和write()
函数。
b.uvm_tlm_analysis_fifo
类继承于uvm_tlm_fifo
,这表明它本身具有面向单一TLM
端口的数据缓存特性,而同时该类又有一个uvm_analysis_imp
端口analysis_export
并且实现了write()
函数:
request & response通信管道 双向通信端口transport
,即通过在target
端实现transport()
方法可以在一次传输中既发送request
又可以接收response
。
浅拷贝可以使用列表自带的copy()
函数(如list.copy()
),或者使用copy
模块的copy()
函数。深拷贝只能使用copy
模块的deepcopy()
,所以使用前要导入:from copy import deepcopy
如果拷贝的对象里的元素只有值,没有引用,那浅拷贝和深拷贝没有差别,都会将原有对象复制一份,产生一个新对象,对新对象里的值进行修改不会影响原有对象,新对象和原对象完全分离开。
如果拷贝的对象里的元素包含引用(像一个列表里储存着另一个列表,存的就是另一个列表的引用),那浅拷贝和深拷贝是不同的,浅拷贝虽然将原有对象复制一份,但是依然保存的是引用,所以对新对象里的引用里的值进行修改,依然会改变原对象里的列表的值,新对象和原对象完全分离开并没有完全分离开。而深拷贝则不同,它会将原对象里的引用也新创建一个,即新建一个列表,然后放的是新列表的引用,这样就可以将新对象和原对象完全分离开。
如果没有指明访问类型,那么成员的默认类型是public,子类和外部均可以访问成员。
如果指明了访问类型是protected
,那么只有该类或者子类可以访问成员,而外部无法访问。
如果指明了访问类型是local
,那么只有该类可以访问成员,子类和外部均无法访问。
刚开始接触的时候,我认为UVM
其实就是SV
的一个封装,将我们在搭建测试平台过程中的一些重复性和重要的工作进行封装,从而使我们能够快速的搭建一个需要的测试平台,并且可重用性还高。因此我当时觉得它就是一个库。
不过,随着学习的不断深入,当我深入理解UVM
中各种机制和模型的构造和相互之间关系之后,我觉得其实UVM
方法学对于使用何种语言其实并不重要,重要的是他的思想,比如:在UVM
中有sequence
机制,以往如果我们使用SV
进行TB
搭建时,我们一般会采用driver
一个类进行数据的产生,转换,发送,或者使用generator
和driver
两个进行,这种方式可重用性很低,而且代码臃肿;但是在UVM中我们通过将sequence、sequencer、driver、sequence_item
拆开,相互独立而又有联系,因此我们只需关注每一个类需要做的工作就可以,可重用性高。我在学习sequence
时,我经常把sequence
比作蓄水池,sequence_item
就是水,sequencer
就是一个调度站,driver
就是总工厂,通过这种方式进行处理,我们的总工厂不需要管其他,只需处理运送过来的水资源就可以,而sequencer
只需要调度水资源,sequence
只需要产生不同的水资源。而这种处理方式和现实世界中的生产模式又是基本吻合的。除此之外,还有好多好多,其实UVM
方法学中很多思想就是来源于经验,来源于现实生活,而不在乎是何种语言。
画出UVM
的验证环境结构,如图所示
首先,UVM
测试平台基本是由object
和 component
组成的,其中 component
搭建了TB
的一个树形结构,其基本包含了driver、monitor、sequencer、agent、scoreboard、model、env、test、top
;然后object
一般包含sequence_item、config
和一些其他需要的类。各个组件相互独立,又通过TLM
事务级传输进行通信,除此之外,DUT
与driver
和 monitor
又通过interface
进行连接,实现驱动和采集,最后在top
层进行例化调用test
进行测试。
UVM
中有很多非常有趣的机制,例如factory
机制,field_automation
机制,phase
机制,打印机制,sequence
机制,config_db
机制等,这些机制使得我们搭建的UVM
能够有很好的可重用性和使得我们平台运行有秩序稳定。
例如phase
机制,phase
机制主要是使得UVM
的运行仿真层次化,使得各种例化先后次序正确。UVM
的phase
机制主要有9个,外加12个小phase
。主要的 phase
有build phase、connect phase、run phase、report phase、final phase
等,其中除了run phase
是** task**
,其余都是function
,然后build phase
和final phase
都是自顶向下运行,其余都是自底向上运行。Run phase
和12个小phase
( reset phase、configure phase、main phase、shutdown phase
)是并行运行的,有这12个小phase
主要是进一步将run phase
中的事务划分到不同的phase
进行,简化代码。注意,run phase
和 12个小phase
最好不要同时使用。从运行上来看,9个phase
顺序执行,不同组件中的同一个phase
执行有顺序,build phase
为自顶向下,只有同一个phase
全部执行完毕才会执行下一个phase
。
所有的phase
按照以下顺序自上而下自动执行:(九大phase,其中run phase又分为12个小phase)build_pase
connect_phase
end_of_elaboration_phase
start_of_simulation_phase
run_pase
extract_phase
check_phase
report_phase
final_phase
其中,run_phase按照以下顺序自上而下执行:
pre_reset_phase
reset_phase
post_reset_phase
pre_configure_phase
configure_phase
post_configure_phase
pre_main_phase
main_phase
post_main_phase
pre_shutdown_phase
shutdown_phase
post_shutdown_phase
Domain
是用来组织不同组件,实现独立运行的概率。默认情况下,UVM
的9个phase
属于 common_domain
,12个小phase
属于uvm_domain
。例如,如果我们有两个dirver
类,默认情况下,两个driver
类中的复位phase
和 main phase
必须同时执行,但是我们可以设置两个driver
属于不同的domain
,这样两个dirver
就是独立运行的了,相当于处于不同的时钟域(只针对12个小phase
有效)。
run_phase
和main phase
(动态运行)都是task phase
,且是并行运行的,后者称为动态运行(run-time
)的phase
。
如果想执行一些耗费时间的代码,那么要在此phase
下任意一个component
中至少提起一次objection
,这个结论只适用于12个run-time
的phase
。对于run_phase
则不适用,由于run_phase
与动态运行的phase
是并行运行的,如果12个动态运行的phase
有objection
被提起,那么run_phase
根本不需要raise_objection
就可以自动执行。
在main_phase
执行过程中,突然遇到reset
信号被置起,可以用jump()
实现从mian_phase
到reset_phase
的跳转:
UVM
中采用事务级传输机制进行组件间的通信,可以大大提高仿真的速度和使得我们简化组件间的数据传输,简化工作,TLM
独立于组件之外,降低组件间的依赖关系。UVM
接口主要由port、export、imp
;驱动这些接口方式有put、get、peek、transport、analysis
等。
其中peek
是查看端口内部的数据事务但是不删除,get
是获取后立即删除。我们一般会先使用peek
进行获取数据,但不删除(保证put
端不会立马又发送一个数据),处理完毕后再用get
删除。
lmp只能作为终点接口,transport表示双向通信,analysis可以连接多个imp(类似于广播)。
都可以。Analysis port
类似于广播,其可以同时对多个imp
进行事务通信,只需要在每一个对应的imp
端口申明write()
函数即可。对比 put,get,peek port,
他们都只能进行一对一传输,且也必须申明对应的函数如 put()、get()、peek()、can_put()/do_put()
等。Fifo
是可以不用申明操作函数的,其内部封装了很多的通信端口,如analysis_export
等,我们只需要将端口与其连接即可实现通信。
item
是基于uvm_object
类,这表明了它具备UVM
核心基类所必要的数据操作方法,例如copy、 clone、compare、record
等。
item
对象的生命应该开始于sequence
的body()
方法,而后经历了随机化并穿越sequencer
最终到达driver
,直到被driver
消化之后,它的生命一般来讲才会结束。
item与sequence的关系 一个sequence
可以包含一些有序组织起来的item
实例,考虑到item
在创建后需要被随机化,sequence
在声明时也需要预留一些可供外部随机化的变量,这些随机变量一部分是用来通过层级传递约束来最终控制item
对象的随机变量,一部分是用来对item
对象之间加以组织和时序控制的。
Sequence的分类:
扁平类(flat sequence)
:这一类往往只用来组织更细小的粒度,即item实例构成的组织。
层次类( hierarchical sequence)
:这一类是由更高层的sequence用来组织底层的sequence
,进而让这些sequence
或者按照顺序方式,或者按照并行方式,挂载到同一个sequencer
上。
虚拟类(virtual sequence)
:这一类则是最终控制整个测试场景的方式,鉴于整个环境中往往存在不同种类的sequencer
和其对应的sequence
,我们需要一个虚拟的sequence
来协调顶层的测试场景。之所以称这个方式为virtual sequence
,是因为该序列本身并不会固定挂载于某一种sequencer
类型上,而是将其内部不同类型sequence
最终挂载到不同的目标sequencer
上面。这也是virtual sequence
不同于hierarchical sequence
的最大一点。
sequence
机制用于产生激励,它是UVM
中最重要的机制之一。sequence
机制有两大组成部分:sequence
和sequencer
。
在整个验证平台中sequence
处于一个比较特殊的位置。sequence
不属于验证平台的任何一部分,但是它与sequencer
之间有着密切的关系。
只有在sequencer
的帮助下,sequence
产生的transaction
才能最终送给driver
;同样,sequencer
只有在sequence
出现的情况下才能体现出其价值,如果没有sequence
,sequencer
几乎没有任何作用。
除此之外,sequence
与sequencer
还有显著的区别。从本质上说,sequencer
是一个uvm_component
,而sequence
是一个uvm_object
。与my_transaction
一样,sequence
也有其生命周期。它的生命周期比my_transaction
要更长一点,其内部的transaction
全部发送完毕后,它的生命周期也就结束了。
Virtual
含义就是其sequencer
并不需要传递item
,也不会与driver
连接,其只是一个去协调各个sequencer
的中央路由器。通过virtual sequencer
我们可以实现多个agent
的多个sequencer
他们的 sequence
的调度和可重用。Virtual sequence
可以组织不同sequencer
的sequence
群落。
在UVM
中有sequence
机制,以往如果我们使用SV
进行TB
搭建时,我们一般会采用driver
一个类进行数据的参数,转换,发送,或者使用genetor
和driver
两个进行,这种方式可重用性很低,而且代码臃肿;
但是在UVM中我们通过将sequence、sequencer、driver、sequence_item
拆开,相互独立而又有联系,因此我们只需关注每一个类需要做的工作就可以,可重用性高。我在学习sequence
时,我经常把sequence
比作蓄水池,sequence_item
就是水,sequencer
就是一个调度站,driver
就是总工厂,通过这种方式进行处理,我们的总工厂不需要管其他,只需处理运送过来的水资源就可以,而sequencer
只需要调度水资源,sequence
只需要产生不同的水资源。
Interface
如果不进行virtual
声明的话是不能直接使用在dirver
中的,会报错,因为interface
声明的是一个实际的物理接口。一般在dirver
中使用virtual interface
进行申明接口,然后通过config_db
进行接口参数传递,这样我们可以从上层组件获得虚拟的interface
接口进行处理。
Config_db
传递时只能传递virtual
接口,即interface
的句柄,否则传递的是一个实际的物理接口,这在 driver
中是不能实现的,且这样的话不同组件中的接口一一对应一个物理接口,那么操作就没有意义了。
Factory
机制也叫工厂机制,其存在的意义就是为了能够方便的替换TB
中的实例或者已注册的类型。一般而言,在搭建完TB
后,我们如果需要对TB
进行更改配置或者相关的类信息,我们可以通过使用factory
机制进行覆盖,达到替换的效果,从而大大提高TB
的可重用性和灵活性。要使用factory
机制先要进行:
将类注册到factory
表中
创建对象,使用对应的语句 (type_id::create)
编写相应的类对基类进行覆盖。
Callback
机制其作用是提高TB
的可重用性,其还可进行特殊激励的产生等,与factory
类似,两者可以有机结合使用。与factory
不同之处在于 callback
的类还是原先的类,只是内部的callback
函数变了,而factory
是产生一个新的扩展类进行替换。
UVM
组件中内嵌callback
函数或者任务
定义一个常见的uvm_callbacks class
从UVM callback
空壳类扩展uvm_callback
类
在验证环境中创建并登记uvm_callback
field_automation
机制:可以自动实现copy、compare、print
等三个函数。当使用uvm_field
系列相关宏注册之后,可以直接调用以上三个函数,而无需自己定义。这极大的简化了验证平台的搭建,尤其是简化了driver
和monitor
,提高了效率。
UVM
中通过objection
机制来控制验证平台的关闭,需要在drop_objection
之前先raise_objection
。验证在进入到某一phase
时,UVM
会收集此phase
提出的所有objection
,并且实时监测所有objection
是否已经被撤销了,当发现所有都已经撤销后,那么就会关闭此phase
,开始进入下一个phase
。当所有的phase
都执行完毕后,就会调用$finish
来将整个验证平台关掉。如果UVM
发现此phase
没有提起任何objection
,那么将会直接跳转到 下一个phase
中。
UVM
的设计哲学就是全部由sequence
来控制激励生成,因此一般情况下只在sequence
中控制objection
。另外还需注意的是,raise_objection
语句必须在main_phase
中第一个消耗仿真时间的语句之前。
Config_db
机制主要作用就是传递参数使得TB
的可配置性高,更加灵活。Config_db
机制主要传递的有三种类型:
一种是interface
虚拟接口,通过传递virtual interface
使得dirver
和 monitor
能够与DUT
连接,并驱动接口和采集接口信号。
第二种是单一变量参数,如int,string,enum
等,这些主要就是为了配置某些循环次数,id
号是多少等等。
第三种是object
类,这种主要是当配置参数较多时,我们可以将其封装成一个object
类,去包含这些属性和相关的处理方法,这样传递起来就比较简单明朗,不易出错。
Config_db
的参数主要由四个参数组成,如下所示,第一个参数为父的根parent
,第二个参数为接下来的路径,对应的组件,第三个是传递时的名字(必须保持一致),第四个是变量名。uvm_config_db #(virtual interface) :: set(uvm_root:.get(),"uvm_test_top.c1",'vif",vif); uvm_config_db #(virtual interface) :: get(this,"”,"vif",vif);
Component
之间通过在new
函数创建时指定parent
参数指定子关系,通过这种方法来将TB
形成一个树形结构。UVM
中运行是通过Phase
机制进行层次化仿真的。从组件来看各个组件并行运行,从phase
上看是串行运行,有层次化的。Phase
机制的9个phase
是串行运行的,不同组件中的同一个phase
都运行完毕后才能进入下一个phase
运行,同一个phase
在不同组件中的运行也是由一定顺序的,build
和 final
是自顶向下。
启动sequence
有很多的方法:常用的方法有使用default sequence
进行调用,其会将对应的sequence
与 sequencer
绑定,当dirver
请求获得req
时,sequencer
就会调用对应的sequence
去运行body
函数,从而产生req
。
除此之外,还可以使用start
函数进行,其参数主要就是对应的需要绑定的sequencer
和该类的上层sequence
。如此,就可以实现启动sequence
的功能。
注意:一般仿真开始结束会在sequence
中 raise objection
和 drop objection
首先,我们要了解寄存器对于设计的重要性,其是模块间交互的窗口,我们可以通过读寄存器值去观察模块的运行状态,通过写寄存器去控制模块的配置和功能改变。
然后,为什么我们需要RAL呢?由于前面寄存器的重要性,我们可以知道,如果我们不能先保证我们寄存器的读写正确,那么就不用谈后续 DUT是否正确了,因此,寄存器的验证是排在首要位置的。
那么我们应该用什么方法去读写和验证寄存器呢?采用RAL寄存器模型去测试验证,是目前最成功的方法吧,寄存器模型独立于TB
之外,我们可以搭建一个测试寄存器的agent
,去通过前门或者后门访问去控制DUT
的寄存器,使得 DUT
按照我们的要求去运行。
除此之外,UVM
中内建了很多RAL
的sequence
,用于帮助我们去检测寄存器,除此之外,还有一些其他的类和变量去帮助我们搭建,以提高RAL
的可重用性和便捷性还有更全的覆盖率。
前门访问和后门访问的比较
前门访问,顾名思义指的是在寄存器模型上做的读写操作,最终会通过总线UVC来实现总线上的物理时序访问,因此是真实的物理操作。
后门访问,指的是利用UVM DPI (uvm_hdl_read()、uvm_hdl_deposit())
,将寄存器的操作直接作用到DUT内的寄存器变量,而不通过物理总线访问。
前门访问在使用时需要将path
设置为UVM_FRONTDOOR
在进行后门访问时,用户首先需要确保寄存器模型在建立时,是否将各个寄存器映射到了DUT
一侧的HDL
路径:使用add_hdl_path
5. 从上面的差别可以看出,后门访问较前门访问更便捷更快一些,但如果单纯依赖后门访问也不能称之为“正道”。6. 实际上,利用寄存器模型的前门访问和后门访问混合方式,对寄存器验证的完备性更有帮助。
在通过前门配置寄存器A之后,再通过后门访问来判断HDL地址映射的寄存器A变量值是否改变,最后通过前门访问来读取寄存器A的值。
mirror、desired、actual value()
我们在应用寄存器模型的时候,除了利用它的寄存器信息,也会利用它来跟踪寄存器的值。寄存器有很多域,每一个域都有两个值。
寄存器模型中的每一个寄存器,都应该有两个值,一个是镜像值( mirrored value
) , 一个是期望值(desired value
) 。
期望值是先利用寄存器模型修改软件对象值,而后利用该值更新硬件值;镜像值是表示当前硬件的已知状态值。
镜像值往往由模型预测给出,即在前门访问时通过观察总线或者在后门访问时通过自动预测等方式来给出镜像值
镜像值有可能与硬件实际值不一致
UVM提供了两种用来跟踪寄存器值的方式,我们将其分为自动预测(auto prediction
)和显式预测( explicit
)。
如果用户想使用自动预测的方式,还需要调用函数uvm_reg_map::set_auto predict()
两种预测方式的显著差别在于,显式预测对寄存器数值预测更为准确,我们可以通过下面对两种模式的分析得出具体原因。自动预测
如果用户没有在环境中集成独立的predictor
,而是利用寄存器的操作来自动记录每一次寄存器的读写数值,并在后台自动调用predict()
方法的话,这种方式被称之为自动预测。
这种方式简单有效,然而需要注意,如果出现了其它一些sequence
直接在总线层面上对寄存器进行操作(跳过寄存器级别的write/read
操作,或者通过其它总线来访问寄存器等这些额外的情况,都无法自动得到寄存器的镜像值和预期值。显式预测
更为可靠的一种方式是在物理总线上通过监视器来捕捉总线事务,并将捕捉到的事务传递给外部例化的predictor
,该predictor
由UVM
参数化类uvm_reg_predictor
例化并集成在顶层环境中。
在集成的过程中需要将adapter
与map
的句柄也一并传递给predictor
,同时将monitor
采集的事务通过analysis port
接入到predictor
一侧。
这种集成关系可以使得,monitor
一旦捕捉到有效事务,会发送给predictor
,再由其利用adapter
的桥接方法,实现事务信息转换,并将转化后的寄存器模型有关信息更新到map
中。
默认情况下,系统将采用显式预测的方式,这就要求集成到环境中的总线UVC monitor
需要具备捕捉事务的功能和对应的analysis port
,以便于同predictor
连接。
AHB(Advanced High-performance Bus)
高级高性能总线。APB(Advanced Peripheral Bus)
高级外围总线AXI (Advanced eXtensible Interface)
高级可拓展接口
AHB
主要是针对高效率、高频宽及快速系统模块所设计的总线,它可以连接如微处理器、芯片上或芯片外的内存模块和DMA
等高效率模块。
APB
主要用在低速且低功率的外围,可针对外围设备作功率消耗及复杂接口的最佳化。APB
在AHB
和低带宽的外围设备之间提供了通信的桥梁,所以APB
是AHB
的二级拓展总线。
AXI
高速度、高带宽,管道化互联,单向通道,只需要首地址,读写并行,支持乱序,支持非对齐操作,有效支持初始延迟较高的外设,连线非常多。
AHB协议
1. AHB的组成
Master:
能够发起读写操作,提供地址和控制信号,同一时间只有1个Master
会被激活。
Slave:
在给定的地址范围内对读写操作作响应,并对Master
返回成功、失败或者等待状态。
Arbiter:
负责保证总线上一次只有1个Master
在工作。仲裁协议是规定的,但是仲裁算法可以根据应用决定。
Decoder:
负责对地址进行解码,并提供片选信号到各Slave
。每个AHB
都需要1个仲裁器和1个中央解码器。
2. AHB基本信号(经常会问Htrans和Hburst,以及AHB的边界地址怎么确定)
HADDR:32位系统地址总线。
HTRANS:M指示传输状态,NONSEQ、SEQ、IDLE、BUSY。
HWRITE:传输方向1-写,0-读。
HSIZE:传输单位。
HBURST:传输的burst类型,SINGLE、INCR、WRAP4、INCR4等。
HWDATA:写数据总线,从M写到S。
HREADY:S应答M是否读写操作传输完成,1-传输完成,0-需延长传输周期。
HRESP:S应答当前传输状态,OKAY、ERROR、RETRY、SPLIT。
HRDATA:读数据总线,从S读到M。
APB协议及读写操作
1. APB的状态转移
2. APB写操作
Assertion
可以分为立即断言和并发断言。
立即断言的话就是和时序无关,比如我们在对激励随机化时,我们会使用立即断言,如果随机化出错我们就会触发断言报错。
并发断言的话主要是用来检测时序关系的,由于在很多模块或者总线中,单纯使用覆盖率或者事务check
并不能完全检测多个时序信号之间的关系,但是并发断言却可以使用简洁的语言去监测,除此之外,还可以进行覆盖率检测。
并发断言的用法的话,主要是有三个层次:
序列sequence
编写,将多个信号的关系用断言中特定的操作符进行表示;
属性property
的编写,它可以将多个sequence
和多个property
进行嵌套,外加上触发事件;
assert
的编写,调用property
就可以。编写完断言后我们可以将它用在很多地方,比如DUT
内部,或者在top
层嵌入DUT
中,还可以在interface
处进行编写,基本能够检测到信号的地方都可以进行断言检测。
a[*3]指的是:重复3次a,且其与前后其他序列不能有间隔,a中间也不可有间隔。
a[->3]指的是:重复3次,其 a中间可以有间隔,但是其后面的序列与a之间不可以有间隔。
a[=3]指的是:只要重复3次,中间可随意间隔。
主要会考虑三个方面吧,代码覆盖率,功能覆盖率,断言覆盖率。
代码覆盖率,主要由行覆盖率、条件覆盖率、fsm
覆盖率、跳转覆盖率、分支覆盖率,他们是否都是运行到的,比如 fsm
,是否各个状态都运行到了,然后不同状态之间的跳转是否也都运行到了。
功能覆盖率的话主要是自己编写covergroup
和coverpoint
去覆盖我们想要覆盖的数据和地址或者其他控制信号。
断言覆盖率主要检测我们的时序关系是否都运行到了,比如总线的地址数据读写时序关系是否都有实现。
Condition
又称为条件覆盖率,当条件覆盖率未被覆盖时,我们需要通过查看覆盖率报告去定位哪些条件没有被覆盖到,是因为没有满足该条件的前提条件还是因为根本就遗漏了这些情况,根据这个我们去编写相应的case
,进而将其覆盖到。
功能覆盖率主要是针对spec
文档中功能点的覆盖检测 -code
覆盖率主要是针对RTL
设计代码的运行完备度的体现,其包括行覆盖率、条件覆盖率、FSM
覆盖率、跳转覆盖率、分支覆盖率(只要仿真就可以,看看DUT
的哪些代码没有动,如果有一部分代码一直没动,看一下是不是case
没写到)。
功能覆盖率和代码覆盖率两者缺一不可,功能覆盖率表示着代设计是否具备这些功能,代码覆盖率表示我们的测试是否完备,代码是否冗余。当功能覆盖率高而代码覆盖率低时,表示covergroup
是不是写少了,case
写少了;或者代码冗余。当功能覆盖率很低而代码覆盖率高时,表示代码设计是不是全面,功能点遗漏;covergroup
写的是不是冗余了。只有当两者覆盖率都高的时候才表明我们验证的大部分是可靠的。
代码覆盖率很难达到100%,一般情况下达到90%多已经非常不错了,如果有一部分代码没有被触动到,需要有经验的验证工程师去分析,如果确实没啥问题,就可以签字通过了
对于流程的话
首先第一步我会先去查看spec
文档,将模块的功能和接口总线时序搞明白,尤其是工作的时序,这对于后续写TB
非常重要;
第二步我会根据功能点去划分我的TB
应该怎么搭建,我的case
大致会有哪些,这些功能点我应该如何去覆盖,时序应该如何去检查,总结列出这样的一个清单;
第三步开始去搭建我们的TB
,包括各种组件,和一些基础的 sequence
还有test
,暂时先就写一两个基础的sequence
,然后还有一些环境配置参数的确定等,最后能够将TB
正常运行,保证无误;
第四步就是根据清单去编写sequence
和 case
,然后去仿真,保证仿真正确性,收集覆盖率;
第五步就是分析收集的覆盖率,然后查看覆盖率报告去分析还有哪些没有被覆盖,去写一些定向case
,和更换不同的seed
去仿真;
第六步就是回归测试regression
,通过不同的 seed
去跑,收集覆盖率和检测是否有其它bug
;
第七步就是总结
刚开始的难点还是TB的搭建,想要搭建出一个可重用性很高的TB,配置灵活的TB还是有一定困难,对于哪些参数应该放在配置类,哪些参数应该放在事务类的抉择,哪些单独配置。
除此之外,还有就是时序的理解,这对于driver
和monitor
还有sequence
和 assertion
的编写至关重要,只有正确理解时序才能编写出正确的TB
。
最后就是实现覆盖率的尽可能高,这也是比较困难的,刚开始的case
好写,也比较快就可以达到较高的覆盖率,但是那些边边角角的case
需要自己去琢磨,去分析还需要写什么case
。这些难点就是重点,还要能够自动化监测判断是否正确。
这个问题面试的时候经常问,建议面试之前考虑一下,再做决定
我是使用UVM
验证方法学搭建的TB
,然后在VCS
平台进行仿真的。目录结构的话:主要由RTL
文件、doc
文件、tb
文件、sim
文件、script
文件这几部分。
UVM的优点:UVM有各个机制、促进验证平台的标准化,UVM中test sequence
和验证平台是隔离独立的,可以更好的控制激励而不需要重新设计agent
. 改变测试sequence
可以简单高效提高代码覆盖率。UVM
支持工业标准,这会促进验证平台标准化。此外,UVM
通过OOP
(面向对象编程)的特点(例如继承)以及使用覆盖组件提高了重复使用率。因此UVM环境方便移植,架构清晰,组件连接方便,有利于进行大规模的验证。
UVM的缺点:代码冗余,工作量大,运行速度有缺失
无论是重载的类(parrot
)还是被重载的类(bird
),都要在定义时注册到factory
机制中。
被重载的类(bird
)在实例化时,要使用factory
机制式的实例化方式,而不能使用传统的new
方式。
最重要的是,重载的类(parrot
)要与被重载的类(bird
)有派生关系。重载的类必须派生自被重载的类,被重载的类必须是重载类的父类。
UVM
更高的层次更接近用户,为了让用户少和底层组件打交道,所以层次越高优先级越高,高层次的set
会覆盖底层次的set
,如果是层次相同再看时间先后顺序,谁发生的晚谁有效,时间靠后的会覆盖之前的。
阶段1(定义)。
功能特性提取
特性覆盖率创建及映射
VIP的架构
阶段2(VIP基本搭建)
driver,sequencer,monitor (少量特性实现)。
实现基本的端到端的sequence
阶段3(完成monitor与scoreboard)
完成monitor -100%实现(checkers,assertions)
完成scoreboard -100%实现(数据完整性检查)
在monitor中,完成监测到的transaction与function coverage实现映射。
为映射更多的基本功能覆盖率,创建其它sequences。
阶段4(扩充test和sequence阶段)
实现更多sequences,从而获得80%的功能覆盖率
阶段5(完成标准)
Sequence最终可以实现100%的功能覆盖率。
回归测试结果和最终的总结报告。
验证流程:
看spec
文档和协议,将DUT
的功能和接口总线时序搞明白
制定验证计划和测试点分解
写VIP
或者是用别人给的VIP
,搭建验证环境和TB
,包括各种组件,各个模块的pkg
,基础的 sequence
还有test
,暂时先就写一两个基础的 sequence
,然后还有一些环境配置参数的确定等,最后能够将TB
正常运行,保证无误;
根据测试点编写sequence
和 case
,然后去仿真,保证仿真正确性,收集覆盖率;
分析收集的覆盖率,然后查看覆盖率报告去分析还有哪些没有被覆盖,去写一些定向case
,和更换不同的seed
去仿真;
回归测试regression
,通过不同的seed
去跑,收集覆盖率和检测是否有其它bug
;
总结
验证环境的搭建:
driver
给 DUT
发送激励,montior
监测 DUT
输出的数据,参考模型( reference model
)能实现与 DUT
相同的功能,scoreboard
把 monitor
接受到的数据和 reference model
的输出数据进行比对,如果比对成功就表示 DUT
能完成设计的功能,
factory
机制的实现被集成在了一个宏中:uvm_component_utils
。
这个宏最主要的任务是,将字符串登记在UVM
内部的一张表中,这张表是factory
功能实现的基础。只要在定义一个新的类时使用这个宏,就相当于把这个类注册到了这张表中。这样,factory
机制可以实现:根据一个字符串自动创建一个类的实例,并且调用其中的函数(function
)和任务(task
),这个类的main_phase
就会被自动调用。
TLM
通信的步骤:
分辨出initiator
和target,producer
和consumer
。
在target
中实现tlm
通信方法。
在俩个对象中创建tlm
端口。
在更高层次中将俩个对象进行连接。
端口类型有三种:
port
,一般是initiator
的发起端。
export
,作为initiator
和target
的中间端口。
imp
,只能作为target
接受request
的末端。
多个port
可以连接同一个export
或imp
,但是单个port
或export
不能连接多个imp
。
端口的连接:通过connect
函数进行连接,例如A(initiator)
与B
进行连接,可以使用A.port.connect(B.export)
uvm_*_imp#(T,IMP);IMP定义中第一个参数T是这个IMP传输的数据类型,第二个参数IMP是实现这个接口所在的component
。
你好,我是酒酒,毕业于成电,自学算法leetcode刷题,双修IC验证,斩获互联网 BAT offer 以及一些IC大厂offer:zeku、展锐、华为、寒武纪、地平线的程序媛 and IC媛一枚~。
日常分享高质量资料,输出面试、工作经验,欢迎围观。
(别问,图片就是本人啦~)