UVM实战 卷I学习笔记9——UVM中的sequence(5)

目录

  • virtual sequence的使用
    • *带双路输入输出端口的DUT
    • *sequence之间的简单同步
    • *sequence之间的复杂同步
    • 仅在virtual sequence中控制objection
    • *在sequence中慎用fork join_none


virtual sequence的使用

*带双路输入输出端口的DUT

下面新的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

*sequence之间的简单同步

新的验证平台中有两个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。

*sequence之间的复杂同步

上面解决同步的方法看起来简单实用。但有两个问题:一是使用了一个全局的事件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

UVM实战 卷I学习笔记9——UVM中的sequence(5)_第1张图片
在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。

仅在virtual sequence中控制objection

在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即可,从而大大提高效率

*在sequence中慎用fork join_none

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

你可能感兴趣的:(UVM实战卷I,学习笔记,测试用例,功能测试)