原文地址:http://bbs.eetop.cn/viewthread.php?tid=383872&extra=&authorid=828160&page=1
我相信很多朋友都在坛子里下载过一份《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的组织架构
(1)在大学学习数据结构的时候,我们知道有几种基本的数据结构,其它的结构都可以分解成这几种的组合;它们分别是线性数据结构(比如链表)、树形结构(比如二叉树)、图形结构(有向图和无向图);UVM的phase的组织结构也不例外,都可以分解成上述三种。总体上来说,是按照有向图结构进行组织的。
(2)既然是有向图形结构,那么这个数据结构中自然有若干节点和连接这些节点的有向边。另外,一个图也可以分解为若干个子图。
在默认情况下,这些节点就像铁路的一个个站点,每个节点都有自己的属性;这里所说的属性就是指的是UVM库中定义的如下几种:
UVM_PHASE_DOMAIN:它的含义是从我开始,到后面的某个节点为止,这期间经过的所有节点都是我的管辖范围。
UVM_PHASE_SCHEDULE:它的含义和UVM_PHASE_DOMAIN相同,唯一的区别就是它不具有独立行动的权利,它的外面至少需要套一UVM_PHASE_DOMAIN;形象点说,UVM_PHASE_DOMAIN可以代表整个图形结构或者代表某个子图结构,但是,UVM_PHASE_SCHEDULE则只能代表某个子图结构,它必须属于某个DOMAIN,也就是它必须被某个DOMAIN wrapper起来!
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=270 uvm (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;
五、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 phase class中的add()函数
1、函数原型
1 function void uvm_phase::add(uvm_phase phase, 2 uvm_phase with_phase=null, 3 uvm_phase after_phase=null, 4 uvm_phase before_phase=null);
比如调用是 A.add(B,C)就表示把B加入到A的大家庭中,在A这个大家庭中的位置如何取决于后面的三个参数,根据它们的名字我想不难知道它们的意思。
2、函数主体一:参数有效性判断
(1)
1 if (phase == null) 2 `uvm_fatal("PH/NULL", "add: phase argument is null");
这个含义很明显,如果你想加的这个phase还没有创建,你做这个操作是毫无意义的,simulator会直接退出。
(2)
1 if (with_phase != null && with_phase.get_phase_type() == UVM_PHASE_IMP) 2 begin 3 string nm = with_phase.get_name(); 4 with_phase = find(with_phase); 5 if (with_phase == null) 6 `uvm_fatal("PH_BAD_ADD",{"cannot find with_phase ‘",nm,"‘ within node ‘",get_name(),"‘"}); 7 end
这个检查理解起来也不困难,以A.add(B,C)来说,意思就是说你想把B加入到A中来,并且想让B和C可以并行的运行,首先要确保在A中确实有C这个人才行,不然这个操作肯定也会有问题,而且也是fatal。
(3)
1 if (before_phase != null && before_phase.get_phase_type() == UVM_PHASE_IMP) begin 2 string nm = before_phase.get_name(); 3 before_phase = find(before_phase); 4 if (before_phase == null) 5 `uvm_fatal("PH_BAD_ADD",{"cannot find before_phase ‘",nm,"‘ within node ‘",get_name(),"‘"}); 6 end
(4)
1 if (after_phase != null && after_phase.get_phase_type() == UVM_PHASE_IMP) begin 2 string nm = after_phase.get_name(); 3 after_phase = find(after_phase); 4 if (after_phase == null) 5 `uvm_fatal("PH_BAD_ADD",{"cannot find after_phase ‘",nm,"‘ within node ‘",get_name(),"‘"}); 6 end
有了(2)的解释,(3)和(4)想必就不用再解释了吧。
(5)
1 if (with_phase != null && (after_phase != null || before_phase != null)) 2 `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)
1 if (before_phase == this || after_phase == m_end_node || with_phase == m_end_node) 2 `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、函数主体二:根据插入节点的类型来进行插入位置的框定
1 if (phase.get_phase_type() == UVM_PHASE_IMP) begin 2 new_node = new(phase.get_name(),UVM_PHASE_NODE,this); 3 new_node.m_imp = phase; 4 begin_node = new_node; 5 end_node = new_node; 6 end 7 else begin 8 begin_node = phase; 9 end_node = phase.m_end_node; 10 phase.m_parent = this; 11 end
这个if else看似不多,也不复杂,其实需要你对UVM phase的组织流程图、UVM phase的各种类型的节点如NODE、DOMAIN、TERMINATER等真正含义的准确理解,不然的话你会觉得这个if else 的内容很奇怪!
其实我个人觉得这个 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。
4、函数主体三:默认情况
1 if (with_phase == null && after_phase == null && before_phase == null) begin 2 before_phase = m_end_node; 3 end
就是说,如果你就是用A.add(B)这种形式的话,那么B会加到A的最后面,就相当于C语言字符串中加入字符,如果A=“hello”,B=“uvm”,那么A.add(B)执行时,上面的before_phase = m_end_node;就相当于before_phase=‘\0’,执行完成之后变成了 "hellouvm"。
5、函数主体四:add操作的真正部分
这部分代码我就不贴了,这个add函数剩余的部分都是的,反而我觉得这部分最没什么好讲的,难的反而是类型检查啊、位置定位啊这些前戏。
这部分代码无非就是一些后继和前驱的更新而已,如果你了解数据结构中的链表的插入和删除操作,那么这段代码无非就是C换成了SV,没其他别的东西。如有一个链表L,内容是A->B->C,如果想把D插入到B的前面A的后面,那么需要
step1:temp = A.后继
step2:D.后继 = temp
step3:D.前驱 = A
step4:A.后继 = D
如此而已。
add()函数讨论到这里我觉得对我们以后应用来说也差不多够了,到此为了吧。
这一贴就讨论到这里,很多细节我没有讲,我觉得抓住主线了,那么枝节的东西不难理解,而且UVM class的源代码随处可下,要想弄个清楚明白,读源代码是必不可少的。如果设计到这一贴中的某些细节需要了解,可以联系我,我们私下讨论。