Sequence的起源
最初的driver时有raise_objection 和 drop_objection
以及包含在这两者中间的driver(tr)
因为这样子写的话,每次要发不同的包,就要改一次代码,十分容易出错,扩展性也很差。
于是,可以将每次相同的代码放在driver里面,每次不同的代码独立出去,变成一个
Gen_pkt 函数
但是在使用gen_pkt的过程中就会出现问题,因为如果要使用名字相同的都是gen_pkt的函数,而其中的功能又不同,就需要用到虚函数和重载,将gen_pkt重载为virtual型,当要使用到这个gen_pkt的时候,需要重新实例化一个新的driver,里面带有一个重载后的gen_pkt函数,这想想就是相当麻烦的一件事情。
另一种就是写不同名字的函数,但是这样driver还是需要每一种测试用例就改一次代码来加入这个不同名的函数,而我们就是为了避免这个问题来进行探讨的,所以这明显是不可取的。
Sequence的启动和执行
Sequence的启动方式有
My_seq.start(sequencer);
或者
用default_sequence
在sequence启动后,会自动执行sequence的body任务,还有pre_body和post_body任务
这两者分别在body执行之前和之后
多个sequce的启动
实际上sequence是可以同时启动多个的,但是在这时,就需要考虑这些sequence的启动时间,这就是sequence的仲裁机制,sequence的仲裁算法有多种,如果要改变默认的仲裁算法,要用
env.i_agt.sqr.set_arbitration(SEQ_ARB_STRICT_FIFO);
arbitration:仲裁
将其改变。
在默认情况下sequencer的仲裁算法是SEQ_ARB_FIFO。它会严格遵循先入先出的顺序,而不会考虑优先级。SEQ_ARB_WEIGHTED是加权的仲裁;SEQ_ARB_RANDOM是完全随机选择;SEQ_ARB_STRICT_FIFO是严格按照优先级的,当有多个同一优先级的sequence时,按照先入先出的顺序选择;SEQ_ARB_STRICT_RANDOM是严格按照优先级的,当有多个同
一优先级的sequence时,随机从最高优先级中选择;SEQ_ARB_USER则是用户可以自定义一种新的仲裁算法。
其一,对于transaction来说,它存在优先级的概念
想对他的优先级进行操作,需要用到uvm_do_pri
其二,对于sequence来说,它也有优先级的概念
它的优先级,其实就是它里面的transaction的优先级,当你设置一个sequence的优先级时,你想当于设置了它里面的所有的transaction的优先级。
Sequencer的lock操作和grab操作
Lock是为了让一个sequence能够一直占用sequencer,直到其自己去unlock为止。
与lock操作一样,grab操作也用于暂时拥有sequencer的所有权,只是grab操作比lock操作优先级更高。lock请求是被插入
sequencer仲裁队列的最后面,等到它时,它前面的仲裁请求都已经结束了。grab请求则被放入sequencer仲裁队列的最前面,它几
乎是一发出就拥有了sequencer的所有权:
Sequence的有效性
sequencer在仲裁时,会查看sequence的is_relevant函数的返回结果。如果为1,说明此sequence有效,否则无效。因此可以通过重载is_relevant函数来使sequence失效:
is_relevant:有意义的
失效的意思就是说,sequencer并不会执行这个sequence,除非其自身变得有效。
Sequencer在发送完所有的transaction之后,如果此时有无效的sequence存在,则会自动调用这个无效的sequence的wait_for_relevant函数,这个函数里面必须包含使得这个无效sequence重新变得有效的操作。否则,整个系统会陷入死循环。
正常使用时,必须要同时重载is_relevant函数和wait_for_relevant函数。
Sequence相关宏
`uvm_do是最为傻瓜式的方法,用于产生包含随机数据的transaction
`uvm_do_with随机化时提供对某些字段的约束
`uvm_do_pri 用于给sequence设置优先级
`uvm_do_on 用于给sequence配对sequencer,当有多个sequencer时,这个宏就可以起作用了。
这里的话,其实知道 with、pri、on是什么意思就可以了,只要有with的就包含with的功能,其他也是如此,这样一眼就可以看出这个宏是干啥子的。
产生transaction除了可以用 uvm_do也可以用uvm_create和uvm_send
`uvm_create(my_transaction)生成一个transaction
assert(my_transaction)
my_transaction.num = num_set;
`uvm_send(my_transaction)
另外,还有uvm_send_pri,同理跟前文用法一样
`uvm_rand_send系列宏
my_transaction = new(“my_transaction“)
`uvm_rand_send(my_transaction)
uvm_rand_send系列宏及uvm_send系列宏的意义主要在于,如果一个transaction占用的内存比较大,那么很可能希望前后两次发送的transaction都使用同一块内存,只是其中的内容可以不同,这样比较节省内存。
换言之,当transaction占用的内存比较大的时候,我们应当用uvm_send而不是uvm——do。这样内存消耗小一点。如果transaction本身很小,那就不必要太纠结用哪个了。
宏是怎么做到这些事情的?
宏的操作 依赖两个task
start_item
finish_item
`uvm_do宏操作,相当于是
tr=new(“tr”);
start_item(tr,100);
assert(tr.randomize())
finish_item(tr,100);
这两个任务的第二个参数是优先级。
必须同时操作。
uvm_do相关的操作
uvm_do这个宏封装了太多操作步骤,因此其灵活性不足,为了弥补这些不足,uvm提供了是哪个接口用来拓展uvm_do
pre_do
mid_do
post_do
其执行顺序是
嵌套的sequence
假设,有两种产生crc错误包的sequence
现在要写一个新的sequence,它可以交替产生这两种不同的crc错误包
如果只是单纯将的代码整合,当代码比较长的时候,就很容易出错。
为了避免错误,sequence可以在自己的内容里启动另一个sequence
crc_seq cseq;
longcrc_seq lseq;
repeat(10) begin
cseq = new(“cseq”);
lseq = new(“lseq”);
cseq.start(m_sequencer);
lseq.start(m_sequencer);
end
m_sequencer是case0_sequence在启动后所使用的sequencer的指针
也可以用更简洁的做法:
`uvm_do(cseq);
`uvm_do(lseq);
这可真的太好用了。
uvm_send等系列宏都可以用来操作sequence。
前文介绍的,只有,start_item和finish_item的参数只能是transaction
在sequence里面使用rand类型变量
rand bit[47:0] ldmac;
意指在使用randomize时,这个字段会被随机化,
用的时候是这么用的
virtual task body();
my_transaction tr;
`uvm_do_with(tr,{tr.crc_err==0;
tr.pload.size()==1500;
tr.dmac=ldmac;})
tr.print();
endtask
这样,transaction里面的dmac就是ldmac这个随机的值
上面这个sequence可以被当作一个底层的sequence被使用,它的作用就是随机化transaction的ldmac
repeat(10) begin
`uvm_do_with(lseq,{lseq.ldmac==48’hFFFF;})
end
sequence里面可以添加任意多的rand修饰符,用以规范它产生的transaction。其实,transaction和sequence有众多相似之处,她们都可以用rand修饰符,也都可以用在uvm_do里面。
在使用sequence规范本身的transaction时要注意一点,sequence里面的rand变量名不能与transaction里面的变量名相同,因为如果他们相同,那么编译器在编译程序的时候,会先查找transaction里面有没有这个变量,如果有,那么它就不再去管sequence里面的变量了。
transaction类型的匹配问题
一种sequence只能产生一种transaction,如果另一个sequence想要在这个sequence里面被启动,那它的transaction必须是上层sequence的transaction或它的子类。
如果想要在一个sequence里面发送多种transaction,那么它的参数需要改成uvm_sequence_item,同时,driver里面也要有相应的处理。
p_sequencer的使用
前文有讲到一个m_sequencer,这个指针是在sequence启动后,所在sequencer的指针。它的类型是uvm_sequencer_base(uvm_sequencer的基类)
而不是my_sequencer类型的。
当存在这么一个情况,我在sequencer里面得到了一个dmac值,想要在sequence里面用它,也就是要在sequence里面访问sequencer中的成员变量。
如果用m_sequencer会十分麻烦,具体见书本pdf437页。
在实际的验证平台中,用到sequencer成员变量的情况非常多,uvm考虑到这个情况,内建了一个宏
uvm_declare_p_sequencer(SEQUENCER),
这个宏的本质是声明了一个SEQUENCER类型的成员变量。
可以在sequence里面这么写
`uvm_declare_p_sequencer(my_sequencer)
这个句子相当于是
my_sequencer p_sequencer;
写完这个句子之后,uvm自动将m_sequencer通过cast转换成p_sequencer,所以sequence里面已经可以直接用P_sequencer.dmac来得到dmac这个sequencer里面的成员变量
sequence的派生与继承
sequence既然是一个类,那么它也可以派生其他的sequence
对于其派生的sequence来说,如果想要访问其对应的sequencer里面的成员变量,只要在base_sequence里面用uvm_declare_p_sequence 声明p_sequencer,就可以了因为基类声明的成员变量,其子类是可以使用的。
Virtual sequence的使用
如果,一个dut不止有一个接口,而是有两个,或者多个接口。
这时候,需要在实例化另一个env来测试另一个接口,同时也要多增加一组my_if,设置多个default sequence分别向两个数据端口施加激励。
新的验证平台中,有两个driver,如果,我们想要先在driver0送一个长包,在此基础上,driver1才开始发送包,要怎么做到呢?
第一种是使用全局变量,但是我不喜欢举这个例子,容易带偏自己。而且其实是不能从根本上解决问题的。详细内容见uvm实战书本pdf page447
为了解决这个问题,uvm提供了virtual sequence,我叫他虚拟sequence。
它的作用就是用来统一调度sequence,这个功能也是相当的强大呀。Powerful
在最常见的寄存器模型的使用中就会用到这个功能。
首先,建立virtual sequence之前,需要先建立一个virtual sequencer
非常简单,我们可以用一个图来看用不用virtual sequence的区别
在virtual sequence中,可以用uvm_do_on来调度和发送transaction
`uvm_declare_p_sequence(my_vsqr);
其中的这个p_sqr0就是前面最先创建的那个virtual sequencer的作用
这里的seq0和seq1不用干什么事情,只需要跟最普通的seq一样就可以了,调度的工作已经给到了vseq.
启动sequence的工作也可以不用`uvm_do_on,改用手工启动,手工启动的一个优势是可以想其中传递一些值。