本文版权归本公众号所有。

 

sequence介绍

 

 熟悉UVM的朋友应该都知道,sequence的作用是将测试数据的产生从driver中分离出来,使得driver能专注于驱动测试数据的功能。在不同的测试用例中,将不同的sequence设置成sequencer的main_phase的default_sequence,当sequencer执行到main_phase时,发现有default_sequence,那么它就会启动这个sequence。之后sequencer将启动的sequence产生的测试数据交给driver,再由driver驱动进入DUT完成仿真。

 

当完成一个sequence的定义后,就可以使用start任务将其启动,即在my_case的main_phase中创建自定义的sequence对应的实例,然后调用sequence.start(sequencer)来启动。除了直接启动之外,还可以使用default_sequence启动。一种是通过在my_case的build_phase使用uvm_config_db设置default_sequence给sequencer,另一种方式是先实例化要启动的sequence,之后再通过default_sequence启动。两者的区别就在于config_db::set的第四个参数,即要设置的值的获取方式不同,前者通过type_id::get获取,后者则直接用实例化后的变量即可。当一个sequence启动后会自动执行sequence的body任务。除body外,它还会调用sequence的pre_body和post_body任务。

 

sequence中的仲裁任务

 

UVM支持同一时刻在一个sequencer上启动多个sequence,则此时sequencer起到仲裁的作用,即决定使用哪个sequence产生的测试用例。sequencer是根据transaction的优先级来进行仲裁的,通常来说,优先级越高越容易被选中。使用uvm_do和uvm_do_with宏时产生的transaction为默认优先级,即-1,可以通过uvm_do_pri及uvm_do_pri_with来改变其优先级。uvm_do_pri(uvm_do_pri_with同理)第二个参数为优先级,用户可以自由指定任意大于-1的整数,数字越大,优先级越高。sequencer的仲裁算法有很多种:


 

SEQ_ARB_FIFO,SEQ_ARB_WEIGHTED,SEQ_ARB_RANDOM,SEQ_ARB_STRICT_FIFO,SEQ_ARB_STRICT_RANDOM,SEQ_ARB_USER

 

若想使得优先级设置起作用,应该设置仲裁算法为SEQ_ARB_STRICT_FIFO或SEQ_ARB_STRICT_RANDOM。具体可在测试用例的main_phase中对sequencer进行设置。

 

此外,除transaction有优先级外,sequence本身也有优先级概念,不过其本质还是设置该sequence产生的transaction的优先级。sequence的优先级可在启动sequence时指定,其中第三个参数即为优先级,如


 

seq0.start(env.i_agt.sqr,null,100);

 

除了设置优先级外,sequencer还可以通过sequence的lock操作来改变其行为。所谓lock可以理解为一个发送transaction的请求,当该请求前面的发送请求都被仲裁执行完毕,sequencer就开始响应lock请求,之后会连续发送发出该请求的sequence的transaction,直到unlock操作被调用。lock操作在sequence的body中被调用,当两个sequence都试图调用lock时,先获得所有权的sequence在执行完毕后才会将所有权交给另一个sequence。

 

在lock之上,还有优先级更高的grab操作,不同于lock操作在被调用后要等待仲裁队列里的请求执行完毕才会被执行,grab操作只要一发出立马就会被响应,即“插队”到仲裁队列最前面——特殊情况是当其他sequence的lock操作正在执行中时,grab会等该lock操作执行完毕再执行。grab操作同样要ungrab来释放所有权。当两个sequence都试图使用grab操作时,与同时调用lock操作的判断规则一致。

 

最后,UVM还可以设置sequence,使其在一段时间内失效不参与仲裁,这个功能可以通过重载sequence的is_relevant函数实现。当此函数返回1说明sequence有效,否则无效。sequence还有个函数wait_for_relevant也与有效性有关。wait_for_relevant在sequencer发现其启动的所有sequence都无效时被调用,此时sequencer会等待sequence变有效,换言之,可以通过wait_for_relevant来实现该sequence的无效变有效受其他sequence的状态影响。

 

在wait_for_relevant中,必须将使sequence无效的条件清除,否则会陷入死循环,即一直没有有效的sequence可以执行。因此,is_relevant与wait_for_relevant一般应成对重载。

 

与sequence相关的宏

 

最基础的sequence宏是uvm_do及其衍生,uvm_do系列宏主要包含以下八个:


 

uvm_douvm_do_priuvm_do_withuvm_do_pri_withuvm_do_onuvm_do_on_priuvm_do_on_withuvm_do_on_pri_with

 

从其组织形式可以看出,是uvm_do及pri,with,on等关键词的排列组合,这样有助于我们去记忆。回顾一下uvm_do宏的功能,它是UVM中最常用的宏之一,它用于创建一个transaction的实例,将该实例随机化,

 

最终将其送个sequencer with关键词给uvm_do宏增加了随机化的约束条件,pri关键词则设置了发送的transaction的优先级(在上一节有提到)。uvm_do_on宏用于显式地指定使用哪个sequencer发送此transaction,其参数形式为`uvm_do_on(SEQ_OR_ITEM,SEQR),第一个参数是transaction的指针,第二个是sequencer的指针。当使用uvm_do时,它实际等价于将uvm_do_on的第二个参数设置为了默认的sequencer,即此sequence启动时为其指定的sequencer。uvm_do_on_pri等在此基础上加上各关键词的功能,与uvm_do_pri与uvm_do之间的关系类似,因此uvm_do系列的宏本质上都是uvm_do_on_pri_with的特殊形式。

 

除了使用uvm_do宏自动产生,随机化并发送transaction之外,还可以通过uvm_create宏与uvm_send宏来实现这个过程。uvm_create宏的作用是实例化transaction,当一个transaction被实例化后,可以对其做更多的处理(如随机化),处理完毕后使用uvm_send宏发送出去。当然,也可以不用uvm_create而直接使用new函数来进行实例化。uvm_send也可以增加pri关键词以设置其优先级。

 

此外,还可以将随机化处理与发送合并为uvm_rand_send系列宏来实现。这个宏的使用前提是transaction已经被分配了空间,即已经实例化了。uvm_rand_send宏可增加pri,with关键字。设计uvm_rand_send系列宏的意义在于,如果一个transaction占用的内存很大,那么很可能希望前后两次发送的transaction都使用同一块内存,只是内容不同,这样子比较节约内存空间。

 

以上都是使用宏来完成transaction发送相关的工作,但这样子隐藏了具体的实现细节。不使用宏产生transaction的方式主要依赖于两个任务,start_item与finish_item。在使用这两个任务之前,必须要先实例化transaction后才可以调用。完整使用如上两个任务构建一个sequence的代码如下:


 

virtual task body(); repeat(10) begin tr = new("tr"); start_item(tr); finish_item(tr); endendtask

 

对transaction进行随机化的操作可放在实例化之后,finish_item之前的任意位置。start_item和finish_item都可以在调用时指定优先级。

 

了解了uvm_do宏实现的细节之后,为了增加uvm_do系列宏的灵活度,UVM提供了三个接口:pre_do,mid_do与post_do。

 

pre_do是一个任务,在start_item中被调用,是start_item返回前执行的最后一行代码,在它执行完成后才开始对transaction进行随机化。pre_do有一个1bit参数,用于表明uvm_do是对一个transaction还是一个sequence进行操作(详见下一节sequence的嵌套)。

 

mid_do是一个函数,在finish_item最开始被调用,在执行完此函数后才会进行finish_item中的其他操作。mid_do有一个参数,表示正在操作的sequence或transaction的指针,但其类型是uvm_sequence_item,需要通过cast转换成目标类型。

 

post_do也是一个函数,在finish_item中最后一行代码被调用。post_do也有一个参数,与mid_do类似。

 

sequence进阶应用

 

在一个sequence的body中,除了可以使用uvm_do产生transaction外,还可以启动其他的sequence,这就是sequence的嵌套。嵌套的方式也非常简单,直接在新的sequence的body任务中调用定义好的sequence即可,如下所示:

 

virtual task body();    SEQ0 seq0;\\定义化seq0    SEQ1 seq1;\\定义化seq1
    repeat(10) begin        `uvm_do(seq0);        `uvm_do(seq1);    endendtask

 

在上述代码中,使用了uvm_do宏。uvm_do宏的第一个参数可以是transaction的指针,此时其调用start_item和finish_item;该参数也可以是sequence的指针,此时其调用该sequence的start任务。除了uvm_do外,前述介绍的uvm_send,uvm_rand_send,uvm_create宏等,其第一个参数均可以是sequence的指针。

 

与transaction类似,sequence中也可以加入rand修饰的变量,用以进行对其产生的transaction进行约束。加入了rand变量的sequence可以通过uvm_do_with等宏添加约束条件。不过有一个需要注意的地方,在sequence中定义rand类型变量以向产生的transaction传递约束时,变量的名字一定要与transaction中相应字段的名字不同。这是因为如果名字一样,编译器就无法正确识别该变量究竟在sequence中还是在transaction中,会产生bug。

 

一般来说,嵌套sequence能正确的启动的条件之一是嵌套的所有sequence产生的所有transaction类型需与sequencer能发送的transaction类型一致。不过有一种方法将两个不同的transaction交给同一个sequencer处理,那就是将sequencer和driver能够接受的数据类型设置为uvm_sequence_item。


 

class my_sequencer extends uvm_sequencer #(uvm_sequence_item);class my_driver extends uvm_driver #(uvm_sequence_item);

 

这样子,在driver接收数据时,通过cast将其转换为不同的transaction类型,即可实现一个sequencer/driver发送多种类型transaction的功能了。

 

考虑一种特殊情况,即sequence中产生的transaction受对应的sequencer中变量的约束,此时该如何在sequence中获取该变量值呢?UVM内建了一个宏uvm_declare_p_sequencer(SEQUENCER),这个宏声明了一个SEQUENCER类型的成员变量,在定义sequence时,使用这个宏声明对应sequencer的类型如下


 

class case0_sequence extends uvm_sequence #(my_transaction); my_transaction my_trans; `uvm_object_utils(case0_sequence) `uvm_declare_p_sequencer(my_sequencer)endclass

 

这样UVM就会自动把m_sequencer(sequence默认sequencer变量,为uvm_sequencer_base类型)转换为my_sequencer类型,这个过程在pre_body()之前就完成了。这样在sequence中可以直接使用成员变量p_sequencer,从而获取my_sequencer中设置的变量值。这个概念比较难理解,读者可以参考《UVM实战》6.4.4节,结合具体代码理解。

 

由于在同一个项目中各sequence通常都是类似的,因此可以将很多公用的函数或者任务写在base_sequence中,其他sequence都从此类派生。sequence是支持派生与继承的。同时对于使用了uvm_declare_p_sequencer的base_sequence,在派生的sequence中不需要再次声明,p_sequencer直接成为新的sequence的成员变量。

 

参考文献:https://zhuanlan.zhihu.com/p/349791759