- 就sequence挂载到sequencer的常用方法做出总结,可通过对这些常用方法和宏的介绍了解它们不同的使用场景。多个sequence需要同时挂载到sequencer时,那就面临仲裁的需要,uvm_sequencer自带仲裁特性,结合sequence的优先级设定,最终可以实现想要的效果。
sequence宏概述
- 可以正确区别方法
start()
和宏uvm_do
,就拿下了sequence发送和嵌套的半壁江山。本节系统性地阐述各种方法和宏之间的关系,以及讨论宏和方法何时可以使用。对于已习惯sequence宏使用的用户而言,当再切回到sequence方法或调试这些方法时会有不适感,但如果想要对sequence发送做出更准确的控制,还须正本清源,首先熟悉sequence的方法。
sequence和item发送实例
class bus_trans extends uvm_sequence_item;
rand int data;
`uvm_object_utils_begin(bus_trans)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_object_utils_end
...
endclass
class child_seq extends uvm_sequence;
`uvm_object_utils(child_seq);
...
task body();
uvm_sequence_item tmp;
bus_trans req;
tmp = create_item(bus_trans::get_type(), m_sequencer, "req");
void'($cast(req, tmp));
start_item(req);
req.randomize with {data == 10;};
finish_item(req);
endtask
endclass
class top_seq extends uvm_sequence;
`uvm_object_utils(top_seq)
...
task body();
uvm_sequence_item tmp;
child_seq cseq;
bus_trans req;
//create child sequence and items
cseq = child_seq::type_id::create("cseq");
tmp = create_item(bus_trans::get_type(), m_sequencer, "req");
//send child sequence via start()
cseq.start(m_sequencer, this);
//send sequence item
void'($cast(req, tmp));
start_item(req);
req.randomize with {data == 20;};
finish_item(req);
endtask
endclass
class sequencer extends uvm_sequencer;
`uvm_component_utils(sequencer)
...
endclass
class driver extends uvm_driver;
`uvm_component_utils(driver)
...
task run_phase(uvm_phase phase);
REQ tmp;
bus_trans req;
forever begin
seq_item_port.get_next_item(tmp);
void'($cast(req, tmp));
`uvm_info("DRV", $sformatf("got a item \n %s", req.sprint()), UVM_LOW)
seq_item_port.item_done();
end
endtask
endclass
class env extends uvm_env;
sequencer sqr;
driver drv;
`uvm_component_utils(env)
...
function void build_phase(uvm_phase phase);
sqr = sequencer::type_id::create("sqr", this);
drv = driver::type_id::create("drv", this);
endfunction
function void connect_phase(uvm_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
class test1 extends uvm_test;
env e;
`uvm_component_utils(test1)
...
function void build_phase(uvm_phase phase);
e = env::type_id::create("e", this);
endfunction
task run_phase(uvm_phase phase);
top_seq seq;
phase.raise_objection(phase);
seq = new();
seq.start(e.sqr);
phase.drop_objection(phase);
endtask
endclass
输出结果:
UVM_INFO @ 0: uvn_test_top.e.drv [DRV] got a item
...
UVM_INFO @ 0: uvm test_top.e.drv [DRV] got a item
...
发送sequence/item方法建议
上面代码主要使用了两种方法,第一个方法是针对将sequence挂载到sequencer上的应用:
- uvm_sequence::start(uvm_sequencer_base sequencer, uvm_sequence_base_parent_sequence = null, int this_priority = -1, bit call_pre_post = 1)
- 使用该方法的过程中首先应指明sequencer的句柄,如果该sequence是顶部的sequence,即没有更上层的sequence嵌套它,则它可以省略对第二个参数parent_sequence的指定。
- 第三个参数的默认值是-1,会使得该sequence如果有parent_sequence会继承其优先级值,如果它是顶部(root)sequence,则其优先级会被自动设定为100。
- 第四个参数默认值为1,默认
uvm_sequence::pre_body()
和uvm_sequence::post_body()
两个方法会在uvm_sequence::body()
的前后执行。 - child_seq被嵌套到top_seq中,继而在挂载时需要指定parent_sequence,而在test层调用top_seq时由于它是root sequence,则不用再指定parent sequence,这一点需注意。另外在调用挂载sequence时,需对这些sequence进行例化。
- 第二种发送方法是针对item挂载到sequencer上的应用:
uvm_sequence::start_item(uvm_sequence_item item, int set_priority = -1, uvm_sequencer_base_sequence = null);
uvm_sequence::finish_item(uvm_sequence_item item, int set_priority = -1);
- 对于
start_item()
,第三个参数需注意是否将item挂载到“非当前parent sequence挂载的sequencer”上,即如果将item和其parent sequence挂载到不同sequencer上面,就需要指定这个参数。
发送sequence/item方法解析
在使用这对方法时用户除了需记得创建item,例如通过uvm_object::create()
或uvm_sequence::create_item()
,还需要在它们之间完成item的随机化处理。
- 对于一个item的完整传送,sequence要在sequencer一侧获得通过权限,才可以顺利将item发送至driver。拆解这些步骤如下:
- 创建
item
。 - 通过
start_item()
方法等待获得sequencer
的授权许可,其后执行parent sequence
的方法pre_do()
。 - 对
item
进行随机化处理。 - 通过
finish_item()
方法在对item进行随机化处理之后执行parent sequence
的mid_do()
,以及调用uvm_sequencer::send_request()
和uvm_sequencer::wait_for_item_done()
将item发送至sequencer再完成与driver之间的握手。最后执行了parent_sequence的post_do()
。
- 创建
- 这些完整的细节有两个部分需要注意:
- 第一,
sequence
和item
自身的优先级,可以决定何时可以获取sequencer
的授权。 - 第二,
parent sequence
的虚方法pre_do()
、mid_do()
、post_do()
会发生在发送item
的过程中间。
- 第一,
- 如果对比
start()
方法和start_item()/finish_item()
,首先要分清它们面向的挂载对象不同:执行start()
过程中默认情况下会执行sequence的pre_body()
和post_body()
,但如果start()
的参数call_pre_post=0
则不会这样执行,所以一些场景UVM用户会奇怪为什么这两个函数没有执行,它们并非一定会被执行,这一点同UVM的phase顺序执行是有区别的。 - 下面代码是对
start()
方法执行过程的自然代码描述,可以看到它们执行的顺序关系和条件:
sub_seq.pre_start() (task)
sub_seq.pre_body() (task) if call_pre_post=1
parent_seq.pre_do(0) (task) if parent_sequence!=null
parent_seq.mid_do(this) (func) if parent_sequence!=null
sub_seq.body() (task) //your stimulus code
parent_seq.post_do(this)() (func) if parent_sequence!=null
sub_seq.post_body() (task) if call_pre_post=1
sub_seq.post_start() (task)
-
对于
pre_do()、mid_do()、post_do()
而言,子一级的sequence/item
在被发送过程中会间接调用parent sequence
的pre_do()
等方法。 -
只要在参数传递过程中确保子一级
sequence/item
与parent sequence
的联系,那么这些执行过程是会按照上面的描述依次执行的。 -
start_item()/finish_item()
的自然代码描述,表示执行发送item时的相关方法执行顺序:
发送序列的相关宏
- 通过这些sequence/item宏,可使用
'uvm_do
、'uvm_do_with
发送sequence和item。这种不区分对象的方式带来了不少便捷。使用之前需先了解它们背后的sequence和item各自发送的方法。 - 不同宏可能会包含创建对象的过程,也可能不包含。如
'uvm_do
、'uvm_do_with
会创建对象,而'uvm_send
不会创建对象,也不会将对象做随机处理,因此要了解它们各自包含的执行内容和顺序。 - 还有其它的宏,例如将优先级作为参数传递的
uvm_do_pri/uvm_do_on_prio
等,还有专门针对sequence的uvm_create_seq/uvm_do_seq/uvm_do_seq_with
等宏。
序列宏的示例
class child_seq extends uvm_sequence;
...
task body(); //这个任务等同于`uvm_do_with实现的功能
bus_trans req;
`uvm_create(req)
`uvm_rand_send_with(req, {data == 10;})
endtask
endclass
class top_seq extends uvm_sequence;
...
task body();
child_seq cseq;
bus_trans req;
//send child sequence via start()
`uvm_do(cseq)
//send sequence item
`uvm_do_with(req, {data == 20;})
endtask
endclass
- 关于发送sequence/item的建议:
- 无论sequence处于什么层次,都应该让sequence在test结束前执行完毕,还应保留一部分时间供DUT将所有发送的激励处理完毕,进入空闲状态才可结束测试。
- 尽量避免使用
fork_join_any
或fork_join_none
控制sequence的发送顺序。因此如果想终止在后台运行的sequence线程而简单使用disable方式,就可能在不恰当的时间点上锁住sequencer。 - 一旦sequencer被锁住且无法释放,接下来就无法发送其它sequence。所以如果用户想实现类似fork_join_any或fork_join_none的发送顺序,应当在使用disable前对各个sequence线程的后台运行保持关注,尽量在发送完item完成握手之后再终止sequence,才能避免sequencer被死锁的问题。
- 如果要使用fork_join方式,应确保有方法可以让sequence线程在满足一些条件后停止发送item,否则只要有一个sequence线程无法停止,则整个fork_join无法退出。这种情况需要用户考虑监测合适的事件或时间点,才能使用disable来关闭线程。
sequencer的仲裁特性
- uvm_sequencer类自建了仲裁机制用来保证多个sequence同时挂载到sequencer时,可以按照仲裁规则允许特定sequence中的item优先通过。
- 在实际使用中可通过
uvm_sequencer::set_arbitration(UVM_SEQ_ARB_TYPE val)
函数设置仲裁模式,这里的仲裁模式UVM_SEQ_ARB_TYPE
有几种选择:UVM_SEQ_ARB_FIFO
:默认模式。来自于sequences的发送请求,按照FIFO先进先出的方式被依次授权,和优先级没有关系。UVM_SEQ_ARB_WEIGHTED
:不同sequence的发送请求,将按照它们的优先级权重随机授权。UVM_SEQ_ARB_RANDOM
:不同的请求会被随机授权,而无视它们抵达顺序和优先级。UVM_SEQ_ARB_STRICT_FIFO
:不同的请求,会按照它们的优先级以及抵达顺序来依次授权,与优先级和抵达时间都有关系。UVM_SEQ_ARB_STRICT_RANDOM
:不同的请求会按照它们的最高优先级随机授权,与抵达时间无关。UVM_SEQ_ARB_USER
:可自定义仲裁方法user_priority_arbitration()
裁定哪个sequence的请求被优先授权。
- 与priority有关的模式有
UVM_SEQ_ARB_WEIGHTED、UVM_SEQ_ARB_STRICT_FIFO和UVM_SEO_ARB_STRICT_RANDOM
,它们的区别在于,UVM_SEQ_ARB_WEIGHTED
的授权可能会落到各个优先级sequence的请求上面,而UVM_SEO_ARB_STRICT_RANDOM
则只会将授权随机安排到最高优先级的请求上面,UVM_SEQ_ARB_STRICT_FIFO
则不会随机授权,而是严格按照优先级以及抵达顺序来依次授权。 - 没有特别的要求,用户不用再额外自定义授权机制,因此使用
UVM_SEQ_ARB_USER
这一模式的情况不多见,其它模式可以满足绝大多数的仲裁需求。 - 鉴于sequence传送的优先级可以影响sequencer的仲裁授权,结合sequencer的仲裁模式和sequence的优先级给出一段例码:
class top_seq extends uvm_sequence;
...
task body();
child_seq seq1, seq2, seq3;
m_sequencer.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
fork
`uvm_do_pri_with(seq1, 500, {base == 10;})
`uvm_do_pri_with(seq2, 500, {base == 20;})
`uvm_do_pri_with(seq3, 300, {base == 30;})
join
endtask
endclass
class sequencer extends uvm_sequencer;
...
endclass
class bus_trans extends uvm_sequence_item;
rand int data;
...
endclass
class child_seq extends uvm_sequence;
rand int base;
task body();
bus_trans req;
repeat(2) `uvm_do_with(req, {data inside {[base:base+9]};})
endtask
endclass
class driver extends uvm_driver;
...
task run_phase(uvm_phase phase);
REQ tmp;
bus_trans req;
forever begin
seq_item_port.get_next_item(tmp);
void'($cast(req, tmp));
`uvm_info("DRV", $sformatf("got a item %0d from parent sequence %s", req.data, req.get_parent_sequence().get_name()), UVM_LOW)
seq_item_port.item_done();
end
endtask
endclass
class env extends uvm_env;
sequencer sqr;
driver drv;
...
function void build_phase(uvm_phase phase);
sqr = sequencer::type_id::create("sqr", this);
drv = driver::type_id::create("drv", this);
endfunction
function void connect_phase(uvm_phase phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
class test1 extends uvm_test;
env e;
`uvm_component_utils(test1)
...
function void build_phase(uvm_phase phase);
e = env::type_id::create("e", this);
endfunction
task run_phase(uvm_phase phase);
top_seq seq;
phase.raise_objection(phase);
seq = new();
seq.start(e.sqr);
phase.drop_objection(phase);
endtask
endclass
输出结果:
- seq1、seq2、seq3在同一时刻发起传送请求,通过
'uvm_do_prio_with
的宏,在发送sequence时可以传递优先级参数。 - 由于将seq1与seq2设置为同样的高优先级,而seq3设置为较低的优先级,在随后的
UVM_SEQ_ARB_STRICT_FIFO
仲裁模式下,可以看到输出结果按优先级高低和传送请求时间顺序,先将seq1和seq2中的item发送完毕,随后将seq3发送完。 - 除了sequence遵循仲裁机制,一些特殊情况下有些sequence需要有更高权限取得sequencer的授权来访问driver。例如在需要响应中断的情况下,用于处理中断的sequence应该有更高的权限来获得sequencer的授权。
sequencer的锁定机制
- uvm_sequencer提供两种锁定机制,分别通过
lock()
和grab()
方法实现,它们的区别在于:lock()
与unlock()
可以为sequence提供排外的访问权限,但前提条件是该sequence需按照sequencer的仲裁机制获得授权。获得授权后则无需担心权限被收回,只有该sequence主动解锁它的sequencer,才可以释放锁定的权限,lock()
是一种阻塞任务,只有获得权限才会返回。grab()
与ungrab()
也可以为sequence提供排外的访问权限,而且它只需在sequencer下次授权周期时就可无条件获得权限。与lock方法相比,它无视同一时刻内发起传送请求的其它sequence,可以阻止它的只有预先获得授权的其它lock或grab的sequence。- 如果sequence使用了
lock()
或grab()
方法,必须在sequence结束前调用unlock()
或ungrab()
方法释放权限,否则sequencer会进入死锁状态而无法继续为其余sequence授权。
sequencer的锁定示例
class bus_trans extends uvm_sequence_item;
...
endclass
class child_seq extends uvm_sequence;
...
endclass
class lock_seq extends uvm_sequence;
...
task body();
bus_trans req;
#10ns;
m_sequencer.lock(this);
`uvm_info("LOCK", "get exclusive access by lock()", UVM_LOW)
repeat(3) #10ns `uvm_do_with(req, {data inside {[100:110]};})
m_sequencer.unlock(this);
endtask
endclass
class grab_seq extends uvm_sequence;
...
task body();
bus_trans req;
#20ns;
m_sequencer.grab(this);
`uvm_info("LOCK", "get exclusive access by grab()", UVM_LOW)
repeat(3) #10ns `uvm_do_with(req, {data inside {[200:210]};})
m_sequencer.ungrab(this);
endtask
endclass
class top_seq extends uvm_sequence;
...
task body();
child_seq seq1, seq2, seq3;
lock_seq locks;
grab_seq grabs;
m_sequencer.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
fork
`uvm_do_pri_with(seq1, 500, {base == 10;})
`uvm_do_pri_with(seq2, 500, {base == 20;})
`uvm_do_pri_with(seq3, 300, {base == 30;})
`uvm_do_pri(locks, 300)
`uvm_do(grabs)
join
endtask
endclass
输出结果:
sequencer的锁定解析
- 结合例码和输出结果,可以发现:
- 对于sequence locks,在10ns时它跟其它几个sequence一同向sequencer发起请求,按照仲裁模式,sequencer先后授权给seq1、seq2、seq3,最后才授权给locks。
- 而locks在获得授权之后,就可以一直享有权限而无需担心权限被sequencer收回,locks结束前需要通过
unlock()
方法返还权限。 - 对于sequence grabs,尽管在20ns时就发起了请求权限(实际上seq1、seq2、seq3也在同一时刻发起了权限请求),而由于权限已经被locks占用,所以它也无权收回权限。
- 因此只有当locks在40ns结束时,grabs才可以在sequencer没有被锁定的状态下获得权限,而grabs在此条件下获取权限是无视同一时刻发起请求的其它sequence的。
- 同样的在grabs结束前也应通过ungrab()方法释放权限,防止sequencer的死锁行为。