UVM phase的用法研究------个人总结
之所以UVM phase,是因为在UVM官方资料里面对UVM phase这块讲的东西比较少。
俗话说,好记忆不如烂笔头,发个帖子,一来是给自己写点东西,很多东西知道和能写出来讲清楚是两码事;二来是抛砖引玉,希望各位同行慢陆续把你们自己对UVM的研究使用心得也表达出来,共同进步;我始终相信只有研究-》实战-》交流讨论-》再研究才是学好一门东西的路径。
在写的过程中,我会按照点来讲,可能顺序性不强,请大家谅解。
phase只要记住:
1. build_phase是自上而下执行,其他至下而上执行
2. main_phase中,raise_objection
顶,小编好样的!
以后这个总结中的任何观点都是个人观点,对与不对希望大家结合自己的经历和理解进行反驳、讨论。
另外需要说明一点的就是,我相信很多朋友都在坛子里下载过一份《UVM1.1应用指南和源代码分析》的资料,我很佩服这位前辈,我也从中收益匪浅,但是可惜在讲解phase的时候对一些初学者来说有些东西跳跃性有点大,更主要的是没有用一个稍微全面而复杂的例子来进行进一步的总结,让读者知道在实际项目中如何构建一个user-defined的phase组织架构图。我在这里也是志在狗尾续貂吧,废话少说,开始吧。
一、什么是phase?
phase翻译成中文就是相、阶段的意思。在UVM中,官方的说法是:phase是使tb中各种各样的component按照各自的需求可以阶段性执行的一种自动化的机制。简单的说就是使验证组件能够按需自动化执行的一种机制。
二、OVM有phase的概念吗?
有。那为什么OVM中没有看到在各个function 或者task的形参表里看到(ovm_phase phase)呢?因为OVM已经为用户提供了现成的、固定的几个phase,它们是 new phase(对一个合格的tb来说,new phase是被 ovm root component在执行run_test()的时候通过factory机制调用的,也就是通过静态函数create调用的)、build phase、connect phase、end_of_elaboration phase、 start_of_simulation phase、run phase、extract phase、check phase、report phase。
OVM 没有提供API给用户进行一些操作,比如增加一个phase、让不同的component的同一个phase异步的运行等等.......
三、UVM为什么在OVM的基础上扩展、增强这个功能?
因为不够用所以才需要扩展,因为不完美用才需要增强。
不完美体现在哪里?我提一点,其余的大家可以挖掘。
一个正常的DUT,从上电到正常工作到关闭是一个相对比较通用的流程,所以把这些比较通用的流程放在一个run phase里面可以但不完美,那我把run phase按照这些通用流程拆分成几个子的阶段不是更好么?比如拆分成reset phase、configure phase、main phase、shutdown phase。
不够用体现在哪里?我同样提一点,其余的大家继续挖掘、跟帖。
一个大的chip里面有很多功能性比较独立的模块,这些功能性独立意味着一个模块A在run的时刻另一个模块B可以不run,也可以run,B运行不运行和A运行不运行关联度不大甚至没有关联,比如A是只负责处理发通路的而B只负责收通路;但是另一方面,功能性独立并不意味着什么都独立,举例来说,A模块和B模块功能性很独立,比如A是一个clock generation模块,B是一个processor模块,那在A没有正常work的前提下B是不能正常工作的。
由此我们至少可以提出两点需求:
(1)能不能让B的reset phase发生在A的reset phase之后,这样等待clock都稳定了再对B做reset操作或者release reset操作?
(2)能不能让A的main phase和B的main phase异步的运行?
上面两点至少需要用到UVM中的的如下机制:新建一个domain、给不同的component设置不同的domain、不同的domain之间phase的同步和异步。
再如,功能更复杂的tb可能需要新建一个user-defined的phase,让它在某一需要个时刻点运行,可以是function性质的或者task性质的。
今天先开个头,明天准备聊聊UVM phase的组织结构。
感谢大牛的细心总结和分享。
除了细分各个DUT模块的工作状态以外,个人还想到了一个实现phase转换的理由。那就是在很多config中的random items在一次simulation往往只是会被randomize一次,效率很低,function coverage的提高要消耗大量的regression testcase。个人感觉如果通过phase jump从extract phase或者report phase再跳转到reset phase/config phase,可以大大提高 verification efficiency。
四、UVM phase的组织架构
(1)在大学学习数据结构的时候,我们知道有几种基本的数据结构,其它的结构都可以分解成这几种的组合;它们分别是线性数据结构(比如链表)、树形结构(比如二叉树)、图形结构(有向图和无向图);
UVM的phase的组织结构也不例外,都可以分解成上述三种。总体上来说,是按照有向图结构进行组织的。
(2)既然是有向图形结构,那么这个数据结构中自然有若干节点和连接这些节点的有向边。另外,一个图也可以分解为若干个子图。
在默认情况下,这些节点就像铁路的一个个站点,每个节点都有自己的属性;这里所说的属性就是指的是UVM库中定义的如下几种:
UVM_PHASE_DOMAIN:它的含义是从我开始,到后面的某个节点为止,这期间经过的所有节点都是我的管辖范围。
UVM_PHASE_SCHEDULE:它的含义和UVM_PHASE_DOMAIN相同,唯一的区别就是它不具有独立行动的权利,它的外面至少需要套一层UVM_PHASE_DOMAIN;形象点说,UVM_PHASE_DOMAIN可以代表整个图形结构或者代表某个子图结构,但是,UVM_PHASE_SCHEDULE则只能代表某个子图结构,它必须属于某个DOMAIN,也就是它必须被某个DOMAINwrapper起来!
UVM_PHASE_NODE:它的含义是图形结构中的某一个节点,它是一个句柄,它自己不干具体的活(比如main phase具体要干什么)。
UVM_PHASE_IMP:它的含义就是说它所代表的就是具体干什么活,上述的UVM_PHASE_NODE就是会指向某一个UVM_PHASE_IMP。
UVM_PHASE_TERMINAL:它是用来标定UVM_PHASE_DOMAIN和UVM_PHASE_SCHEDULE的势力范围的!就是说我后面的节点就不是你们的管辖范围了,只有一个domain或者schedule才有这个东西。
UVM_PHASE_NODE和UVM_PHASE_IMP的关系如果不好理解的话,你可以对照TLM中的port、export、和imp来理解,这样就容易些了;像port、export只负责传话或者发号施令,而imp才比较苦逼,是真正干活的。
有向图中的边在UVM phase里则代表了运行的顺序,就是说这个节点过了,下个节点是哪些,我想这个应该比较好理解。
(3)默认UVM phase的具体的图形结构
common (UVM_PHASE_DOMAIN) id=204
|
build (UVM_PHASE_NODE) id=222
|
connect (UVM_PHASE_NODE) id=234
|
end_of_elaboration (UVM_PHASE_NODE) id=246
|
start_of_simulation (UVM_PHASE_NODE) id=258
/\
run (UVM_PHASE_NODE) id=270uvm (UVM_PHASE_DOMAIN) id=319
||
|uvm_sched (UVM_PHASE_SCHEDULE) id=331
||
|pre_reset (UVM_PHASE_NODE) id=349
||
|reset (UVM_PHASE_NODE) id=361
||
|post_reset (UVM_PHASE_NODE) id=373
||
|pre_configure (UVM_PHASE_NODE) id=385
||
|configure (UVM_PHASE_NODE) id=397
||
|post_configure (UVM _PHASE_NODE) id=409
||
|pre_main (UVM_PHASE_NODE) id=421
||
|main (UVM_PHASE_NODE) id=433
||
|post_main (UVM_PHASE_NODE) id=445
||
|pre_shutdown (UVM_PHASE_NODE) id=457
||
|shutdown (UVM_PHASE_NODE) id=469
||
|post_shutdown (UVM_PHASE_NODE) id=481
||
|uvm_sched_end (UVM_PHASE_TERMINAL) id=337
||
|uvm_end (UVM_PHASE_TERMINAL) id=325
\/
extract (UVM_PHASE_NODE) id=277
|
check (UVM_PHASE_NODE) id=289
|
report (UVM_PHASE_NODE) id=301
|
final (UVM_PHASE_NODE) id=313
|
common_end (UVM_PHASE_TERMINAL) id=210
这个图的每个节点是通过调用m_print_successors()函数得到的,当然中间的“|”这些符号是我自己编辑的,这个函数在UVM class reference的pdf资料里没有提到,应该是UVM开发人员自己调试用的,但是如果你自己阅读代码就知道有这个函数,因为它不是local的,所以我们可以随时调用它。
每个节点的名字,属性和id号都一清二楚,细心的朋友可能已经注意到了这些id号不是随机的,而是有顺序的,是的,它是按照这些节点的建立的先后顺序分配的,id越大表示越晚建立,实际上这个id号就是通过调用get_inst_id()得来的,这个函数是uvm_object的一个基本函数,我就不多讲这个了。
从这个图中,我们可以得到如下几个重要的信息:
(1)这张大的图形结构中有两个domain,一个叫做common,一个叫做uvm,这个概念很重要,以后的分析中会反复用到;
(2)UVM中新加的12个phase(不包括final phase),从pre reset到post shutdown都被一个叫做uvm_sched的 SCHEDULE包起来了,作为一个子图隶属于uvm domain;
(3)OVM和UVM中的run phase属于common domain;
(4)start_of_simulation phase节点有两个后继节点:run phase node和uvm domain;
明天准备讨论一下当我们在top运行run_test()函数是,UVM phase发生了什么。
五、UVM的phase是如何自动运行起来的
从build phase到最后的final phase,自动运行会经过以下几个主要步骤:
1、在tb_top.sv中的某个地方调用 run_test(),这个函数在uvm_root.sv中,是整个tb 树形组织结构的根节点,这个函数主要是干三件事情:
(1)通过factory模式create你自己希望运行的testcase instance,不管你的testcase叫神马名字,最后uvm都会给它重命名为
“uvm_test_top”
(2)调用uvm phase的一个函数,叫做m_run_phases(),等下再讲这个函数主要干的事情
(3)等待(2)结束,kill所有和(2)有关的进程,如果允许则调用$finish结束仿真,退出仿真器
2、m_run_phases()主要干这么两件事情:
(1)创建UVM默认的所有phases,即我们熟知的build phase、connect phase、run phase、main phase等等,并将其组织成我上
面所讲的那个有向图结构;实现这一步主要是靠调用uvm_domain class中的get_common_domain()
(2)开始运行(1)创建组织好的phases,当然如果有用户自定义的phase,也会在规定的点运行起来;实现这一步主要是靠调用对应
的phase的execute_phase()函数和一个无限循环的监控进程
3、uvm_domain中的get_common_domain()主要是干这么三件事情:
(1)创建我上述phase有向图结构的第一部分,即
build->connect->end_of_elaboration->start_of_simulation->run->extract->check->report->final。很自然的实现这
个功能需要用到两个函数:创建它们用new()函数,组织它们让它们有如此先后顺序用add()函数
(2)创建我上述phase有向图结构的第二部分,即
pre_reset->reset->post_reset->pre_configure->configure->post_configure->pre_main->main->post_main->
pre_shutdown->shutdown->post_shutdown。很自然的实现这个功能需也要用到两个函数:创建它们用new()函数,组织
它们让它们有如此先后顺序用add()函数
(3)正如phase有向图结构所示,(2)搞出来的东西是作为一个子图呈现在(1)所搞出来的结构中的;那么怎嘛让(2)搞出来的东西
就成了(1)所搞出来的东西的一部分了呢?而且还要和run phase并行的运行呢?还是要靠这个add()函数!只不过需要用到
add()函数形参表中一个叫做with_phase的东西
显而易见,这个定义在uvm phase class中的add()函数真是无比的重要,正式它让我们可以根据需要把一个个phase节点组织成一
个任意的被UVM 规则所允许的图形结构,所以既然它这么重要,我们下一贴就准备专门来谈谈它!
4、phase的execute_phase()函数主要干哪些事情呢?
简单点说就是按照phase的有向图结构一个一个节点的走下去,知道结束;当然这个走的过程是复杂的、曲折的,呵呵,此函数也是比较
复杂、曲折的;但是有一点,也是最主要最重要的一点,那就是它会调用每个节点所拥有的traverse()函数!正式通过这个函数,我们在
各个phase中所写的自己的代码才得意被调用、执行!
既然这个函数也是如此的重要,那我将用另一个帖子来专门讨论它一下。
5、traverse()函数主要干的事情就是根据每个节点的属性来调用我们自己重载的各个phase。
主要是两个函数一个是exec_func(uvm_component comp, uvm_phase phase),
一个是exec_task(uvm_component comp, uvm_phase phase)。
很自然的我们想到,执行build phase这种function性质的就会调用exec_func(),执行如main phase这种task性质的就会调用
exec_task(),没错,的确如此!
注意到了这两个函数的形参表了么?我想大家都猜出它们是干什么用的吧。比如component A的build phase在执行,那么形参comp
就是A,形参phase就是build。
这一贴就讨论到这里,很多细节我没有讲,我觉得抓住主线了,那么枝节的东西不难理解,而且UVM class的源代码随处可下,要想弄个清楚明白,读源代码是必不可少的。如果设计到这一贴中的某些细节需要了解,可以联系我,我们私下讨论。
不知道怎么回事,每次在编辑的时候对的都挺整齐的,发表之后就成了这个鸟样,请大家多包涵。
小编的帖使人受益良多,望坚持下去!
接下来两周有个chip要tapeout了,有几个ECO,会比较忙,我尽量抽空些,抱歉。
敢问LZ项目做ECO的原因,是否是因为使用UVM方法学做验证的时候还有没有cover的地方?如果是这个方面的原因,是否也能就这个问题,写一些心得,让大家也好好思考一下。
不是,是async的问题,在rtl仿真发现不了,只有做sdf仿真才能发现,在FPGA验证也发现不了,和memory有关的问题。
哦,那是sdf后仿发现的问题的CDC的问题,在跨clock domain的地方没有做正确的async的处理?FPGA速度太慢,而且走线延迟长,不容易出setup和hold的问题。和memory有关系。是dual port的sram? memory macro的内部都是固定的单元,不会出什么timing的问题啊。难道是一些外部的控制信号?呵呵,瞎猜的。
亚稳态问题
看了第一段,决定先顶在看
Thanks
顶,小编好样的!
软件好的搞这个有优势
写成文档应该就好了。
you can also get some detailed information in ZhangQiang's Book
最好有代码实力,简单的表示一下domain等划分,执行过程。
UVM中的很多东西的确需要大家的讨论和改善,提高验证的效率,看了收获还是很多的
我最后会有一个实例来说明,会尽可能多的涉及到UVM phase的一个例子。
我软件不太好,呵呵,虽然是学计算机专业的,但是是计算机系统结构方向的,本人对软件也是不感兴趣,除了计算机组成、系统结构、数据结构这、数电和模电之外,其它的课程都是70分左右,哈哈。只是说对CPU底层了解清楚一点的话,会对设计语言的理解比较容易一点而已。
毕业之后也是做FPGA做了4年,集中在数字中频方向,说白了就是捣腾滤波器设计的。
但是没办法,要想在verfication上有点造诣,以前不喜欢的C++, 面向对象设计模式还是逃不掉,要再捡起来,有点痛苦,主要是感觉时间不够用。
但是有一点,我现在能体会到计算机专业考研为什么一般都是在 《计算机组成原理》或《计算机系统结构》或《操作系统》或《编译原理》;
但是《数据结构》却是必须的原因了,因为确实它所描述的观念很有用。
六、UVM phaseclass中的add()函数
1、函数原型
function void uvm_phase::add(uvm_phase phase,
uvm_phase with_phase=null,
uvm_phase after_phase=null,
uvm_phase before_phase=null);
比如调用是 A.add(B,C)就表示把B加入到A的大家庭中,
在A这个大家庭中的位置如何取决于后面的三个参数,根据
它们的名字我想不难知道它们的意思。
2、函数主体一:参数有效性判断
(1)if (phase == null)
`uvm_fatal("PH/NULL", "add: phase argument is null")
这个含义很明显,如果你想加的这个phase还没有创建,
你做这个操作是毫无意义的,simulator会直接退出。
(2)if (with_phase != null
&& with_phase.get_phase_type() == UVM_PHASE_IMP)
begin
string nm = with_phase.get_name();
with_phase = find(with_phase);
if (with_phase == null)
`uvm_fatal("PH_BAD_ADD",{"cannot find with_phase '",nm,
"' within node '",get_name(),"'"})
end
这个检查理解起来也不困难,以A.add(B,C)来说,意思就是说你想
把B加入到A中来,并且想让B和C可以并行的运行,首先要确保在A中
确实有C这个人才行,不然这个操作肯定也会有问题,而且也是fatal。
(3)if (before_phase != null
&& before_phase.get_phase_type() == UVM_PHASE_IMP) begin
string nm = before_phase.get_name();
before_phase = find(before_phase);
if (before_phase == null)
`uvm_fatal("PH_BAD_ADD",{"cannot find before_phase '",nm,
"' within node '",get_name(),"'"})
end
(4)if (after_phase != null
&& after_phase.get_phase_type() == UVM_PHASE_IMP) begin
string nm = after_phase.get_name();
after_phase = find(after_phase);
if (after_phase == null)
`uvm_fatal("PH_BAD_ADD",{"cannot find after_phase '",nm,
"' within node '",get_name(),"'"})
end
有了(2)的解释,(3)和(4)想必就不用再解释了吧。
(5)if (with_phase != null &&(after_phase != null || before_phase != null))
`uvm_fatal("PH_BAD_ADD",
"cannot specify both 'with' and 'before/after' phase relationships")
这个含义就是说,你说明了用with_phase(或者说用after_phase/before_phase)
这个参数已经可以确定你想要放置的位置了,不需要也不允许在用后面两个参数进行
进一步框定了,这个主要是为了防止一些低级失误。
这个就好比说,数值A的值等于圆周率,并且数值A介于3和4之间;前半句
已经可以确定A是多少了,后面那句是废话;万一我后半句由于笔误写成了
并且数值A介于4和5之间,那不是麻烦了。
(6)if (before_phase == this || after_phase == m_end_node || with_phase == m_end_node)
`uvm_fatal("PH_BAD_ADD",
"cannot add before begin node, after end node, or with end nodes")
这个检查的意思是说:
before_phase == this,表示你想插入到我当前这个domain或者schedule的祖先节点
的前面,那是不允许的,this表示了这个domain或者schedule的树根。
after_phase == m_end_node,表示你想插入到我当前这个domain或者schedule
的最后一个终结节点的后面,m_end_node表示了这个domain或者schedule的终结
节点,注意这个终结节点的类型是UVM_PHASE_TERMINAL而不是UVM_PHASE_NDOE,
with_phase == m_end_node,表示你想和我的终结节点并行运行也是不行的,为什么?
因为UVM_PHASE_TERMINAL类型节点(和UVM_PHASE_DOMAIN类型节点以及
UVM_PHASE_SCHEDULE类型节点)都是不运行的,也就是说它们三个的运行时间是0。
为什么是这样,这个会在后面的execute_phase()函数中看到UVM是这么做的。
3、函数主体二:根据插入节点的类型来进行插入位置的框定
if (phase.get_phase_type() == UVM_PHASE_IMP) begin
new_node = new(phase.get_name(),UVM_PHASE_NODE,this);
new_node.m_imp = phase;
begin_node = new_node;
end_node = new_node;
end
else begin
begin_node = phase;
end_node= phase.m_end_node;
phase.m_parent = this;
end
这个ifelse看似不多,也不复杂,其实需要你对UVM phase的组织流程图、UVM phase
的各种类型的节点如NODE、DOMAIN、TERMINATER等真正含义的准确理解,不然的
话你会觉得这个ifelse 的内容很奇怪!
其实我个人觉得这个 if else是这个函数的难点,其它的很容易懂!
以A.add(B,C)来解释。
(1)首先解释几个变量的意思
begin_node,是这个函数的局部变量,它表示了B这个子结构中的开始节点是哪一个。
end_node,是这个函数的局部变量,它表示了B这个子结构中的结束节点是哪一个。
phase就是这里B。
m_parent是uvm phase class的一个变量,它表示了包含B的更大一层范畴的结构,
注意不是父节点的意思;比如说在图形结构中,子图A属于更大一点的子图B,而B又
属于整个图C的一部分,那么A的m_parent就是B,B的m_parent就是C,再次提请
注意,是范畴的概念不是单纯的父节点的概念。
m_imp也是uvm phase class的一个变量,我们知道在uvm节点图形结构中的每个
节点都有一个属性,或者是UVM_PHASE_DOMAIN,或者是UVM_PHASE_NODE,
或者是UVM_PHASE_TERMINAL等,但不会是UVM_PHASE_IMP类型;这其中的
UVM_PHASE_NODE就是一个指针,它所指向的那个东西的类型如果是UVM_PHASE_IMP
的话,那么这个指针的m_imp就是那个UVM_PHASE_IMP类型的节点;
那么有人可能会问,我也问过的一个问题就是,为什么要这么做,我直接把UVM_PHASE_IMP
类型的节点插进来不就得了,为什么要插入它的指针,然后需要的时候再用这个指针索引?
岂不麻烦和多此一举?我个人觉得可能这样做以后的
灵活性和可扩展性会更好,因为我们知道面向对象的最大的威力是组合而不是继承,
我们可以想想模式设计中的brige模式和strategy模式(当然还有很多),就是用这种
类似的机制完成了一些继承不能做到或者大大增强了以后的可扩展性和灵活性。
(2)if 分支
如果插入进入的子图(注意我改用子图而不是节点了)类型是UVM_PHASE_IMP,那么
new一个指向它的句柄,句柄本身的类型设定成UVM_PHASE_NODE,然后有三句
赋值语句,第一句的意思前面已经在解释变量的时候解释过了,那么第二句和第三局是神马
意思?其实意思很简单,只要你清楚一件事实,那就是只有一个节点的东西也叫做子图或图,
而不是说非要有很多节点,节点之间或串行或并行的那种复杂的结构才叫子图或者图;只有
一个节点的结构是最简单的图形结构。
在次情况下,它既是begin节点也是end节点,这样那两句赋值语句就了然了。
(3)else分支
有了对(2)的理解,这个分支就不难理解了,能进入else分支就表示要进入的这个东西或
者说子图肯定不是if分支中的简单情况,至少有两个节点或者以上。
那好了,begin节点就是这个子图的句柄,end节点就是这个子图的end节点,你加入了我,
那么我当然是你的上级,所以m_parent=this就是这个意思。
有人读上面(3)的某些话可能觉得读不懂,特别是“begin节点就是这个子图的句柄”这句话,对
这句话的理解涉及到对uvm phase图形结构的理解,比如说有个图形结构叫做A,它有两个节点
B和C,按道理来说这个图形结构就是 A->B就完了,但是UVM不是这么做的,它为了处理的方便
会在生成图形结构的时候多搞两个节点:一个开始节点,一个终结节点;也就是uvm会生成如下
图形结构:
A->B->C->终结节点。
如果觉得还不好理解的话,我再举个特别形象例子,C语言中的字符串。
比如说一个“hello”的C字符串,
上面的A就相当于取“hello”的地址,即A=&“hello”,
上面的终结节点就相当于C的 '\0',
上面的B和C就相当于字符串的真正的内容hello。
3、函数主体三:默认情况
if (with_phase == null && after_phase == null && before_phase == null) begin
before_phase = m_end_node;
end
就是说,如果你就是用A.add(B)这种形式的话,那么B会加到A的最后面,
就相当于C语言字符串中加入字符,如果A=“hello”,B=“uvm”,那么
A.add(B)执行时,上面的before_phase = m_end_node;就相当于
before_phase=‘\0’,执行完成之后变成了 "hellouvm"。
3、函数主体四:add操作的真正部分
这部分代码我就不贴了,这个add函数剩余的部分都是的,反而我觉得这部分
最没什么好讲的,难的反而是类型检查啊、位置定位啊这些前戏。
这部分代码无非就是一些后继和前驱的更新而已,如果你了解数据结构中的
链表的插入和删除操作,那么这段代码无非就是C换成了SV,没其他别的东西。
如有一个链表L,内容是A->B->C,如果想把D插入到B的前面A的后面,那么需要
step1:temp = A.后继
step2:D.后继 =temp
step3:D.前驱 = A
step4:A.后继 =D
如此而已。
add()函数讨论到这里我觉得对我们以后应用来说也差不多够了,到此为了吧。
非常支持小编, 钻研精神,可佩可敬。
期待一个实例。只为了说明如何使用phase.构建不同的phase关联和同步。