sequence基础
从driver中剥离激励产生功能
前面例子中,激励最初产生在driver中,后来产生在sequence中。为什么会有这个过程呢?最开始时,driver的main_phase是这样的:
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
phase.raise_objection(this);
for(int i=0; i<10; i++) begin
tr = new;
assert(tr.randomize);
drive_one_pkt(tr);
end
phase.drop_objection(this);
endtask
如果只是施加上述一种激励,这样是可以的。但当要对DUT施加不同的激励时应该怎么办呢?上述代码中是施加了正确的包,而下一次测试中要在第9个transaction中加入CRC错误的包,那么可以这么写:
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
phase.raise_objection(this);
for(int i = 0; i < 10; i++) begin
tr = new;
if(i == 8)
assert(tr.randomize with {tr.crc_err == 1;});
else
assert(tr.randomize);
drive_one_pkt(tr);
end
phase.drop_objection(this);
endtask
这相当于将整个main_phase重新写了一遍。如果现在有新的需求,需要再测一个超长包。那么就需要再次改写main_phase,也就是每多测一种情况就要多改写一次main_phase。如果经常改写某个任务或函数,就很容易将之前对的地方改错。所以这种方法是不可取的,因为它的可扩展性太差,会经常带来错误。
仔细观察main_phase,其实只有从tr=new语句至drive_one_pkt之间的语句在变。有没有方法可以将这些语句从main_phase中独立出来呢? 最好的方法是在不同测试用例中决定这几行语句的内容。这种想法中已经包含了激励的产生与驱动的分离这个观点。drive_one_pkt是驱动,这是driver应该做的事情,但是像产生什么样的包、如何产生等这些事情应该从driver中独立出去。要实现上述目标,可以使用一个函数来实现:
function void gen_pkt(ref my_transaction tr);
tr = new;
assert(tr.randomize);
endfunction
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
bit send_crc_err = 0;
phase.raise_objection(this);
for(int i = 0; i < 10; i++) begin
gen_pkt(tr);
drive_one_pkt(tr);
end
phase.drop_objection(this);
endtask
如上所示可以定义产生正常包的gen_pkt函数,但如何定义一个CRC错误包的函数:
function void gen_pkt(ref my_transaction tr);
tr = new;
assert(tr.randomize with {crc_err == 1;});
endfunction
这样最大的问题是gen_pkt函数的重复定义,显然是不允许的。为避免重复定义,有两种策略:第一种是使用虚函数。将gen_pkt定义为virtual类型,然后在建造CRC错误的测试用例时,从my_driver派生一个新的crc_err_driver并重载gen_pkt函数。但新问题又出现了:如何在这个测试用例中实例化新的driver呢?似乎只能重新定义一个my_agent,为了实例化这个新的agent又只能重新定义一个my_env。这种方式显然不可取。第二种解决方式是使定义的函数名不一样,但在driver的main_phase中又无法执行这种具有不同名字的函数。
这个问题单纯用SV提供的一些接口根本无法实现。UVM为解决这个问题引入了sequence机制,在解决过程中还使用了factory机制、config机制。使用sequence机制在不同测试用例中,将不同sequence设置成sequencer的main_phase的default_sequence。当sequencer执行到main_phase时发现有default_sequence,那么它就启动sequence。
sequencer启动sequence并执行的过程相当于之前的gen_pkt,只是调用的位置从driver变到sequencer。sequencer将sequence产生的transaction交给driver,这其实与在driver里面调用gen_pkt没有本质的区别。
*sequence的启动与执行
当完成一个sequence的定义后,可使用start任务将其启动:
my_sequence my_seq;
my_seq = my_sequence::type_id::create("my_seq");
my_seq.start(sequencer);
除了直接启动之外,还可使用default_sequence启动。事实上default_sequence会调用start任务,它有两种调用方式,其中一种是前文已经介绍过的:
uvm_config_db#(uvm_object_wrapper)::set(this, "env.i_agt.sqr.main_phase",
"default_sequence", case0_sequence::type_id::get());
另外一种方式是先实例化要启动的sequence,之后再通过default_sequence启动:
function void my_case0::build_phase(uvm_phase phase);
case0_sequence cseq;
super.build_phase(phase);
cseq = new("cseq"); //先实例化要启动的sequence
uvm_config_db#(uvm_sequence_base)::set(this, "env.i_agt.sqr.main_phase",
"default_sequence", cseq);
endfunction
当一个sequence启动后会自动执行sequence的body任务。除body外还会自动调用sequence的pre_body与post_body:
class case0_sequence extends uvm_sequence #(my_transaction);
…
virtual task pre_body();
`uvm_info("sequence0", "pre_body is called!!!", UVM_LOW)
endtask
virtual task post_body();
`uvm_info("sequence0", "post_body is called!!!", UVM_LOW)
endtask
virtual task body();
…
#100;
`uvm_info("sequence0", "body is called!!!", UVM_LOW)
…
endtask
`uvm_object_utils(case0_sequence)
endclass
上述的sequence在执行时,会打印出:
# UVM_INFO my_case0.sv(11) @ 0: uvm_test_top.env.i_agt.sqr@@cseq [sequence0] pre_body is called!!!
# UVM_INFO my_case0.sv(22) @ 100000: uvm_test_top.env.i_agt.sqr@@cseq [sequence0] body is called!!!
# UVM_INFO my_case0.sv(15) @ 100000: uvm_test_top.env.i_agt.sqr@@cseq [sequence0] post_body is called!!
sequence的仲裁机制
在同一sequencer上启动多个sequence
在前文所有例子中,同一时刻在同一sequencer上只启动了一个sequence。UVM支持同一时刻在同一sequencer上启动多个sequence。在my_sequencer上同时启动两个sequence:sequence0和sequence1,代码如下所示:
task my_case0::main_phase(uvm_phase phase);
sequence0 seq0;
sequence1 seq1;
seq0 = new("seq0");
seq0.starting_phase = phase;
seq1 = new("seq1");
seq1.starting_phase = phase;
fork
seq0.start(env.i_agt.sqr);
seq1.start(env.i_agt.sqr);
join
endtask
其中sequence0和sequence1的定义为:
class sequence0 extends uvm_sequence #(my_transaction);
...
virtual task body();
repeat(5) begin
`uvm_do(m_trans)
`uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
end
#100;
...
endtask
`uvm_object_utils(sequence0)
endclass
class sequence1 extends uvm_sequence #(my_transaction);
…
virtual task body();
…
repeat (5) begin
`uvm_do_with(m_trans, {m_trans.pload.size < 500;})
`uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
end
#100;
…
endtask
`uvm_object_utils(sequence1)
endclass
运行如上代码后,会显示两个sequence交替产生transaction:
# UVM_INFO my_case0.sv(15) @ 85900: uvm_test_top.env.i_agt.sqr@@seq0 [sequence0] send one transaction
# UVM_INFO my_case0.sv(37) @ 112500: uvm_test_top.env.i_agt.sqr@@seq1 [sequence1] send one transaction
# UVM_INFO my_case0.sv(15) @ 149300: uvm_test_top.env.i_agt.sqr@@seq0 [sequence0] send one transaction
# UVM_INFO my_case0.sv(37) @ 200500: uvm_test_top.env.i_agt.sqr@@seq1 [sequence1] send one transaction
# UVM_INFO my_case0.sv(15) @ 380700: uvm_test_top.env.i_agt.sqr@@seq0 [sequence0] send one transaction
# UVM_INFO my_case0.sv(37) @ 436500: uvm_test_top.env.i_agt.sqr@@seq1 [sequence1] send one transaction
sequencer怎么选择使用哪个sequence的transaction呢?这是UVM的sequence机制中的仲裁问题。对于transaction来说存在优先级的概念,通常优先级越高越容易被选中。当使用uvm_do或uvm_do_with宏时,产生的transaction优先级是默认的优先级(即1)。可通过uvm_do_pri及uvm_do_pri_with改变所产生的transaction的优先级:
class sequence0 extends uvm_sequence #(my_transaction);
…
virtual task body();
…
repeat (5) begin
`uvm_do_pri(m_trans, 100)
`uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
end
#100;
…
endtask
…
endclass
class sequence1 extends uvm_sequence #(my_transaction);
…
virtual task body();
…
repeat (5) begin
`uvm_do_pri_with(m_trans, 200, {m_trans.pload.size < 500;})
`uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
end
…
endtask
…
endclass
uvm_do_pri与uvm_do_pri_with的第二个参数是优先级,其数值必须是一个大于等于-1的整数。数字越大优先级越高。
由于sequence1中transaction的优先级较高,按照预期应先选择sequence1产生的transaction。当sequence1的transaction全部生成完毕后,再产生sequence0的transaction。但运行代码发现并没有按预期进行,而是sequence0与sequence1交替产生transaction。
这是因为sequencer的仲裁算法有很多种:
- SEQ_ARB_FIFO 默认算法,按照“先入先出”的顺序,不考虑优先级
- SEQ_ARB_WEIGHTED 加权的仲裁
- SEQ_ARB_RANDOM 完全随机选择
- SEQ_ARB_STRICT_FIFO 严格按照优先级,同一优先级的sequence按先入先出的顺序
- SEQ_ARB_STRICT_RANDOM 严格按照优先级,同一优先级的sequence随机从最高优先级中选择
- SEQ_ARB_USER 用户可以自定义
要使优先级起作用,应设置为SEQ_ARB_STRICT_FIFO或SEQ_ARB_STRICT_RANDOM:
task my_case0::main_phase(uvm_phase phase);
…
env.i_agt.sqr.set_arbitration(SEQ_ARB_STRICT_FIFO);
fork
seq0.start(env.i_agt.sqr);
seq1.start(env.i_agt.sqr);
join
endtask
经过如上设置后会发现直到sequence1发送完transaction后,sequence0才开始发送。
除transaction有优先级外,sequence也有优先级的概念。可在sequence启动时指定其优先级:
task my_case0::main_phase(uvm_phase phase);
…
env.i_agt.sqr.set_arbitration(SEQ_ARB_STRICT_FIFO);
fork
seq0.start(env.i_agt.sqr, null, 100);
seq1.start(env.i_agt.sqr, null, 200);
join
endtask
start任务的第一个参数是sequencer,第二个参数是parent sequence(可设为null),第三个参数是优先级,不指定则此值为-1,不能设置为小于-1的数字。
如果前面sequence0和sequence1的代码中,不在uvm_do系列宏中指定它们的优先级。运行代码会发现sequence1中的transaction完全发送完后才发送sequence0中的transaction。所以,对sequence设置优先级的本质即设置其内产生的transaction的优先级。