下面新的DUT相当于在前面DUT的基础上增加了一组数据口,这组新的数据口与原先的数据口功能完全一样。新的数据端口增加后,由于这组新的数据端口与原先的一模一样,所以可以在test中再额外实例化一个my_env:
class base_test extends uvm_test;
my_env env0;
my_env env1;
…
endclass
function void base_test::build_phase(uvm_phase phase);
super.build_phase(phase);
env0 = my_env::type_id::create("env0", this);
env1 = my_env::type_id::create("env1", this);
endfunction
在top_tb中做相应更改,多增加一组my_if,并通过config_db将其设置为新的env中的driver和monitor:
module top_tb;
…
my_if input_if0(clk, rst_n);
my_if input_if1(clk, rst_n);
my_if output_if0(clk, rst_n);
my_if output_if1(clk, rst_n);
dut my_dut(.clk(clk),
.rst_n(rst_n),
.rxd0(input_if0.data),
.rx_dv0(input_if0.valid),
.rxd1(input_if1.data),
.rx_dv1(input_if1.valid),
.txd0(output_if0.data),
.tx_en0(output_if0.valid),
.txd1(output_if1.data),
.tx_en1(output_if1.valid));
…
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env0.i_agt.drv", "vif",
input_if0);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env0.i_agt.mon","vif",
input_if0);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env0.o_agt.mon", "vif",
output_if0);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env1.i_agt.drv", "vif",
input_if1);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env1.i_agt.mon", "vif",
input_if1);
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env1.o_agt.mon", "vif",
output_if1);
end
endmodule
通过在测试用例中设置两个default sequence,可以分别向两个数据端口施加激励:
function void my_case0::build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(uvm_object_wrapper)::set(this, "env0.i_agt.sqr.main_phase",
"default_sequence", case0_sequence::type_id::get());
uvm_config_db#(uvm_object_wrapper)::set(this, "env1.i_agt.sqr.main_phase",
"default_sequence", case0_sequence::type_id::get());
endfunction
新的验证平台中有两个driver,它们原本是完全等价的,但出于某些原因的考虑,如DUT要求driver0必须先发送一个最大长度的包,在此基础上driver1才可以发送包。这是一个sequence之间同步的过程,一种很自然的想法是将这个同步的过程使用一个全局的事件来完成:
event send_over;//global event
class drv0_seq extends uvm_sequence #(my_transaction);
…
virtual task body();
…
`uvm_do_with(m_trans, {m_trans.pload.size == 1500;})
->send_over;
repeat (10) begin
`uvm_do(m_trans)
`uvm_info("drv0_seq", "send one transaction", UVM_MEDIUM)
end
…
endtask
endclass
class drv1_seq extends uvm_sequence #(my_transaction);
…
virtual task body();
…
@send_over;
repeat (10) begin
`uvm_do(m_trans)
`uvm_info("drv1_seq", "send one transaction", UVM_MEDIUM)
end
…
endtask
endclass
之后,通过uvm_config_db的方式分别将这两个sequence作为env0.i_agt.sqr和env1.i_agt.sqr的default_sequence:
function void my_case0::build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(uvm_object_wrapper)::set(this, "env0.i_agt.sqr.main_phase",
"default_sequence", drv0_seq::type_id::get());
uvm_config_db#(uvm_object_wrapper)::set(this, "env1.i_agt.sqr.main_phase",
"default_sequence", drv1_seq::type_id::get());
endfunction
当进入到main_phase时这两个sequence会同步启动,但由于drv1_seq要等待send_over事件的到来,所以它并不会马上产生transaction,而drv0_seq则会直接产生transaction。当drv0_seq发送完一个最长包后,send_over事件被触发,drv1_seq开始产生transaction。
上面解决同步的方法看起来简单实用。但有两个问题:一是使用了一个全局的事件send_over。全局变量对于初写代码的人来说非常受欢迎的,但除非有必要,否则尽量不要使用全局变量。使用全局变量的主要问题即它是全局可见的,本来只是打算在drv0_seq和drv1_seq中使用这个全局变量,但假如其他某个sequence也不小心使用了这个全局变量,在drv0_seq触发send_over事件之前,这个sequence已经触发了此事件,这是不允许的。所以应尽量避免全局变量的使用。
二是上面只是实现了一次同步,如果是有多次同步怎么办?如sequence A要先执行,之后是B,B执行后才能是C,C执行后才能是D,D执行后才能是E。这依然可以使用上面的全局方法解决,只是这会显得相当笨拙。
实现sequence之间同步的最好方式是使用virtual sequence。字面上理解是虚拟的sequence。虚拟的意思就是它根本就不发送transaction,只是控制其他的sequence,起统一调度的作用。
如下图,为了使用virtual sequence,一般需要一个virtual sequencer。virtual sequencer里包含指向其他真实sequencer的指针:(相当于路由器的作用)
class my_vsqr extends uvm_sequencer;
my_sequencer p_sqr0;
my_sequencer p_sqr1;
…
endclass
在base_test中实例化v_sqr,并将相应的sequencer赋值给v_sqr中的sequencer的指针:
class base_test extends uvm_test;
my_env env0;
my_env env1;
my_vsqr v_sqr;
…
endclass
function void base_test::build_phase(uvm_phase phase);
super.build_phase(phase);
env0 = my_env::type_id::create("env0", this);
env1 = my_env::type_id::create("env1", this);
v_sqr = my_vsqr::type_id::create("v_sqr", this);
endfunction
function void base_test::connect_phase(uvm_phase phase);
v_sqr.p_sqr0 = env0.i_agt.sqr;
v_sqr.p_sqr1 = env1.i_agt.sqr;
endfunction
在virtual sequene中可以使用uvm_do_on系列宏来发送transaction:virtual sequence是uvm_do_on宏用得最多的地方。
class case0_vseq extends uvm_sequence;
`uvm_object_utils(case0_vseq)
`uvm_declare_p_sequencer(my_vsqr)
…
virtual task body();
my_transaction tr;
drv0_seq seq0;
drv1_seq seq1;
…
`uvm_do_on_with(tr, p_sequencer.p_sqr0, {tr.pload.size == 1500;})
`uvm_info("vseq", "send one longest packet on p_sequencer.p_sqr0", UVM_MEDIUM)
fork
`uvm_do_on(seq0, p_sequencer.p_sqr0);
`uvm_do_on(seq1, p_sequencer.p_sqr1);
join
…
endtask
endclass
在case0_vseq中先使用uvm_do_on_with在p_sequencer.sqr0上发送一个最长包,当其发送完毕后再启动drv0_seq和drv1_seq。这里的drv0_seq和drv1_seq非常简单,两者之间不需要为同步做任何事情:
class drv0_seq extends uvm_sequence #(my_transaction);
…
virtual task body();
repeat (10) begin
`uvm_do(m_trans)
`uvm_info("drv0_seq", "send one transaction", UVM_MEDIUM)
end
endtask
endclass
class drv1_seq extends uvm_sequence #(my_transaction);
…
virtual task body();
repeat (10) begin
`uvm_do(m_trans)
`uvm_info("drv1_seq", "send one transaction", UVM_MEDIUM)
end
endtask
endclass
在使用uvm_do_on宏的情况下,虽然seq0是在case0_vseq中启动,但是它最终会被交给p_sequencer.p_sqr0,也即env0.i_agt.sqr而不是v_sqr。这个就是virtual sequence和virtual sequencer中virtual的来源。它们各自并不产生transaction,而只是控制其他的sequence为相应的sequencer产transaction。virtual sequence和virtual sequencer只是起一个调度的作用。由于根本不直接产生transaction,所以virtual sequence和virtual sequencer在定义时根本无需指明要发送的transaction数据类型。
如果不使用uvm_do_on宏,也可以手工启动sequence,其效果完全一样。手工启动sequence的一个优势是可以向其中传递一些值:
class read_file_seq extends uvm_sequence #(my_transaction);
my_transaction m_trans;
string file_name;
…
endclass
…
class case0_vseq extends uvm_sequence;
…
virtual task body();
my_transaction tr;
read_file_seq seq0;
drv1_seq seq1;
…
`uvm_do_on_with(tr, p_sequencer.p_sqr0, {tr.pload.size == 1500;})
`uvm_info("vseq", "send one longest packet on p_sequencer.p_sqr0",
UVM_MEDIUM)
seq0 = new("seq0");
seq0.file_name = "data.txt";
seq1 = new("seq1");
fork
seq0.start(p_sequencer.p_sqr0);
seq1.start(p_sequencer.p_sqr1);
join
…
endtask
endclass
在read_file_seq中需要一个字符串的文件名字,在手工启动时可指定文件名字,但uvm_do系列宏无法实现这个功能,因为string类型变量前不能使用rand修饰符。这就是手工启动sequence的优势。
在case0_vseq的定义中一般都要使用uvm_declare_p_sequencer宏,通过它可以引用sequencer的成员变量。
之前为了解决sequence的同步使用了send_over全局变量的方式来解决。那么在virtual sequence中是如何解决的呢?事实上在virtual sequence中这根本就不是个问题。由于virtual sequence的body是顺序执行,所以只需要先产生一个最长的包,产生完毕后再将其他的sequence启动起来,没有必要去刻意地同步。这只是virtual sequence强大的调度功能的一个小小的体现。
virtual sequence的使用可以减少config_db语句的使用。由于config_db::set函数的第二个路径参数是字符串(容易出错),所以减少config_db语句的使用可以降低出错的概率。上节使用两个uvm_config_db语句将两个sequence送给了相应的sequencer作为default_sequence。假如验证平台中的sequencer有多个,如10个,那么就需要写10个uvm_config_db语句,这是一件很令人厌烦的事情。使用virtual sequence后可以将这10句只压缩成一句:
function void my_case0::build_phase(uvm_phase phase);
…
uvm_config_db#(uvm_object_wrapper)::set(this, "v_sqr.main_phase",
"default_sequence", case0_vseq::type_id::get());
endfunction
virtual sequence作为一种特殊的sequence,也可以在其中启动其他的virtual sequence:
class case0_vseq extends uvm_sequence;
…
virtual task body();
cfg_vseq cvseq;
…
`uvm_do(cvseq)
…
endtask
endclass
其中cfg_vseq是另外一个已经定义好的virtual sequence。
在sequence中可使用starting_phase来控制验证平台的关闭。除了手工启动sequence时为starting_phase赋值外,只有将此sequence作为sequencer的某动态运行phase的default_sequence时,其starting_phase才不为null。
如果将某sequence作为uvm_do宏的参数,那么此sequence中的starting_phase是为null的。在此sequence中使用starting_phase.raise_objection是没有任何用处的:
class drv0_seq extends uvm_sequence #(my_transaction);
…
virtual task body();
if(starting_phase != null) begin
starting_phase.raise_objection(this);
`uvm_info("drv0_seq", "raise objection", UVM_MEDIUM)
end
else begin
`uvm_info("drv0_seq", "starting_phase is null, can't raise objection",
UVM_MEDIUM)
end
…
endtask
endclass
class case0_vseq extends uvm_sequence;
…
virtual task body();
drv0_seq seq0;
if(starting_phase != null)
starting_phase.raise_objection(this);
`uvm_do_on(seq0, p_sequencer.p_sqr0);
#100;
if(starting_phase != null)
starting_phase.drop_objection(this);
endtask
endclass
运行上述代码会发现drv0_seq中的starting_phase为null,从而不会对objection进行操作。要使drv0_seq的starting_phase不为null,只要将父sequence的starting_phase赋值给子sequence的starting_phase即可。只是uvm_do系列宏并不提供starting_phase的传递功能。
之前提到过要么在scoreboard中控制objection,要么在sequence中控制。关于在sequence中控制objection,在没有virtual sequence之前没有什么疑问。但是当virtual sequence存在时,尤其是virtual sequence中又可以启动其他virtual sequence时,有三个地方可以控制objection:一是普通的sequence,二是中间层的virtual sequence,三是最顶层的virtual sequence。那么应该在何处控制objection来最终控制验证平台的关闭呢?
一般来说只在最顶层的virtual sequence中控制objection。因为virtual sequence是起统一调度作用的,这种统一调度不只体现在transaction上,也应体现在objection的控制上。在验证平台中使用objection时经常会出现没有按照预期结束仿真的情况。这种情况下就需要层层地查找哪里有objection被提起了,哪里有objection被撤销了。虽然可以通过objection调试手段来辅助进行,但终归是一件比较麻烦的事情。如果约定俗成都只在最顶层的virtual sequence中控制objection,那么在遇到这样的问题时,只查找最顶层的virtual sequence即可,从而大大提高效率。
DUT的数据口扩展为4路,那么相应的验证平台中也要有4个完全相同的driver、sequencer。那么my_vsqr可以这样定义:
class my_vsqr extends uvm_sequencer;
my_sequencer p_sqr[4];
…
`uvm_component_utils(my_vsqr)
endclass
当DUT上电复位后,需要4个my_driver同时发送数据。在virtual sequence中可以使用fork来启动4个sequence:
class case0_vseq extends uvm_sequence;
virtual task body();
drv_seq dseq[4];
for(int i = 0; i < 4; i++)
fork
automatic int j = i; //notice
uvm_do_on(dseq[j], p_sequencer.p_sqr[j]);
join_none
endtask
endclass
这里使用了join_none,它的特性是系统并不等fork起来的进程结束就进入下一次的for循环,因此上面的for循环的展开后如下:
class case0_vseq extends uvm_sequence;
virtual task body();
drv_seq dseq[4];
fork
uvm_do_on(dseq[0], p_sequencer.p_sqr[0]);
join_none
fork
uvm_do_on(dseq[1], p_sequencer.p_sqr[1]);
join_none
fork
uvm_do_on(dseq[2], p_sequencer.p_sqr[2]);
join_none
fork
uvm_do_on(dseq[3], p_sequencer.p_sqr[3]);
join_none
endtask
endclass
这样的问题在于:当sequence启动后会自动执行它的body任务。当body执行完成时该sequence就相当于已经完成了其使命。如果使用fork join_none,由于其特性,当使用uvm_do_on宏将四个dseq分别放在四个p_sqr上执行时,系统会新启动4个进程,但并不等待这4个mseq执行完毕就直接返回。返回后就到了endtask,此时系统认为该sequence已经执行完成。执行完成后系统会清理这个sequence之前占据的内存空间,“杀死”掉由其启动的进程,于是这4个启动的dseq还没有完成就被“杀死”掉了。也就是说看似分别往4个p_sqr分别丢了一个sequence,但是事实上这个sequence根本没有执行。这是关键所在。
要避免这个问题有多种方法,一是使用wait fork语句:
class case0_vseq extends uvm_sequence;
…
virtual task body();
my_transaction tr;
drv_seq dseq[4];
…
for(int i = 0; i < 4; i++)
fork
automatic int j = i;
`uvm_do_on(dseq[j], p_sequencer.p_sqr[j]);
join_none
wait fork;
…
endtask
endclass
wait fork语句会等待前面被fork起来的进程执行完毕。
另外一种方法是使用fork join:只是这样就无法使用for循环了。
class case0_vseq extends uvm_sequence;
virtual task body();
drv_seq dseq[4];
fork
uvm_do_on(dseq[0], p_sequencer.p_sqr[0]);
uvm_do_on(dseq[1], p_sequencer.p_sqr[1]);
uvm_do_on(dseq[2], p_sequencer.p_sqr[2]);
uvm_do_on(dseq[3], p_sequencer.p_sqr[3]);
join
endtask
endclass