1. 寄存器模型的常规方法(期望值、镜像值、真实值)
Read&write操作:无论通过前门还是后门访问对dut进行读写,在操作完成后,寄存器模型会根据读写的结果更新期望值和镜像值;
Peek&poke操作:在操作完成后,寄存器模型会根据读写的结果更新期望值和镜像值;
Get&set操作:set操作会更新期望值,但是镜像值不会改变;get操作会返回寄存器模型中前寄存器的期望值;
Update操作:该任务会检查期望值和镜像值是否一致,若不一致,那么将会把期望值写入dut中,并且更新镜像值;
Randomize操作:一般randomize和update一起使用;如dut上电复位后,需要配置一些寄存器的值。这些寄存器的值通过randomize获得,并使用updata任务配置到dut中
如果希望向寄存器中写入一个‘h1,方法一是直接调用write任务,将‘h1写入,这时期望值与镜像值都更新为‘h1;
另一种方法是通过set函数将期望值设置为‘h1,之后调用update任务,该任务会检查期望值和镜像值是否一致,若不一致,那么将会把期望值写入dut中,并且更新镜像值;
Get函数可以得到寄存器的期望值;
Get_mirrored_value可以得到镜像值;
2.Prediction的分类?
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连接。
3. 简述一下assertion的用法?
Assertion可以分为立即断言和并发断言。
立即断言的话就是和时序无关,比如我们在对激励随机化时,我们会使用立即断言,如果随机化出错我们就会触发断言报错。
并发断言的话主要是用来检测时序关系的,由于在很多模块或者总线中,单纯使用覆盖率或者事务check并不能完全检测多个时序信号之间的关系,但是并发断言却可以使用简洁的语言去监测,除此之外,还可以进行覆盖率检测。
并发断言的用法的话,主要是有三个层次:
序列sequence编写,将多个信号的关系用断言中特定的操作符进行表示;
属性property的编写,它可以将多个sequence和多个property进行嵌套,外加上触发事件;
assert的编写,调用property就可以。编写完断言后我们可以将它用在很多地方,比如DUT内部,或者在top层嵌入DUT 中,还可以在interface处进行编写,基本能够检测到信号的地方都可以进行断言检测。
4. 寄存器模型中一些内建的sequence?
a.uvm_reg_mem_hdl_paths_seq 用以检查hdl路径的正确性;
这个seq会试图读取hdl所指向的寄存器,如果无法读取,则给出错误提示;
b. uvm_reg_hw_reset_seq用以检查上电复位后寄存器模型与dut中寄存器的默认值是否相同
c. uvm_reg_access_seq用于检查寄存器的读写功能;
d. uvm_mem_access_seq用于检查存储器的读写功能;
e. UVM_REG_backdoor_access_seq:使用后门访问来执行寄存器的读写操作
f. UVM_REG_frontdoor_access_seq:使用前门访问,即通过模拟总线事务来执行寄存器的读写操作。
5. 项目中会考虑哪些coverage?
主要会考虑三个方面吧,代码覆盖率,功能覆盖率,断言覆盖率。
代码覆盖率,主要由行覆盖率、条件覆盖率、fsm覆盖率、跳转覆盖率、分支覆盖率,他们是否都是运行到的,比如 fsm,是否各个状态都运行到了,然后不同状态之间的跳转是否也都运行到了。
功能覆盖率的话主要是自己编写covergroup和coverpoint去覆盖我们想要覆盖的数据和地址或者其他控制信号。
断言覆盖率主要检测我们的时序关系是否都运行到了,比如总线的地址数据读写时序关系是否都有实现
6. Function coverage和 code coverage的区别,以及他们分别对项目的含义?
功能覆盖率主要是针对spec文档中功能点的覆盖检测 -code覆盖率主要是针对RTL设计代码的运行完备度的体现,其包括行覆盖率、条件覆盖率、FSM覆盖率、跳转覆盖率、分支覆盖率(只要仿真就可以,看看DUT的哪些代码没有动,如果有一部分代码一直没动,看一下是不是case没写到)。
功能覆盖率和代码覆盖率两者缺一不可,功能覆盖率表示着代设计是否具备这些功能,代码覆盖率表示我们的测试是否完备,代码是否冗余。当功能覆盖率高而代码覆盖率低时,表示covergroup是不是写少了,case写少了;或者代码冗余。当功能覆盖率很低而代码覆盖率高时,表示代码设计是不是全面,功能点遗漏;covergroup写的是不是冗余了。只有当两者覆盖率都高的时候才表明我们验证的大部分是可靠的。
代码覆盖率很难达到100%,一般情况下达到90%多已经非常不错了,如果有一部分代码没有被触动到,需要有经验的验证工程师去分析,如果确实没啥问题,就可以签字通过了
7. 你在做验证时的流程是怎么样的,你是怎么做的?
首先第一步我会先去查看spec文档,将模块的功能和接口总线时序搞明白,尤其是工作的时序,这对于后续写TB非常重要;
第二步我会根据功能点去划分我的TB应该怎么搭建,我的case大致会有哪些,这些功能点我应该如何去覆盖,时序应该如何去检查,总结列出这样的一个清单;
第三步开始去搭建我们的TB,包括各种组件,和一些基础的 sequence还有test,暂时先就写一两个基础的sequence,然后还有一些环境配置参数的确定等,最后能够将TB正常运行,保证无误;
第四步就是根据清单去编写sequence和 case,然后去仿真,保证仿真正确性,收集覆盖率;
第五步就是分析收集的覆盖率,然后查看覆盖率报告去分析还有哪些没有被覆盖,去写一些定向case,和更换不同的seed去仿真;
第六步就是回归测试regression,通过不同的 seed去跑,收集覆盖率和检测是否有其它bug;
第七步就是总结
8. 在进行验证的过程中,碰到过什么难点,重点是什么呢?
刚开始的难点还是TB的搭建,想要搭建出一个可重用性很高的TB,配置灵活的TB还是有一定困难,对于哪些参数应该放在配置类,哪些参数应该放在事务类的抉择,哪些单独配置。
除此之外,还有就是时序的理解,这对于driver和monitor还有sequence和 assertion的编写至关重要,只有正确理解时序才能编写出正确的TB。
最后就是实现覆盖率的尽可能高,这也是比较困难的,刚开始的case好写,也比较快就可以达到较高的覆盖率,但是那些边边角角的case需要自己去琢磨,去分析还需要写什么case。这些难点就是重点,还要能够自动化监测判断是否正确。
9. 通过工厂进行覆盖有什么要求?
a.无论是重载的类(parrot)还是被重载的类(bird),都要在定义时注册到factory机制中。
b.被重载的类(bird)在实例化时,要使用factory机制式的实例化方式,而不能使用传统的new方式。
c.最重要的是,重载的类(parrot)要与被重载的类(bird)有派生关系。重载的类必须派生自被重载的类,被重载的类必须是重载类的父类。
10.uvm_component_utils有什么作用?
factory机制的实现被集成在了一个宏中:uvm_component_utils。
这个宏最主要的任务是,将字符串登记在UVM内部的一张表中,这张表是factory功能实现的基础。只要在定义一个新的类时使用这个宏,就相当于把这个类注册到了这张表中。这样,factory机制可以实现:根据一个字符串自动创建一个类的实例,并且调用其中的函数(function)和任务(task),这个类的main_phase就会被自动调用。