UVM Actual Volume I Learning Notes 9 - sequence in UVM

Use of virtual sequence

*DUT with dual I/O port

The new DUT below corresponds to the addition of a set of data ports based on the previous DUT, which perform exactly the same functions as the original data ports. With the new data ports added, since this new set of data ports is the same as the original, you can instantiate an additional my_in test 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

At top_ Make changes in TB to add an additional set of my_if, and through config_db sets it to driver and monitor in the new env:

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

By setting two default sequence s in the test case, two data ports can be motivated separately:

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

*Simple synchronization between sequence s

There are two drivers in the new validation platform, which were originally equivalent, but for some reason, such as DUT requiring driver0 to send a maximum-length package before it can send a package. This is a synchronization process between sequence s, and it is natural to think that this synchronization process is done using a global event:

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

After that, use uvm_config_db uses these two sequences as env0.i_agt.sqr and env1.i_ Default_of agt.sqr 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

When entering main_ When phase, both sequence s start synchronously, but because drv1_seq waits for send_over event arrives, so it doesn't generate transaction immediately, but drv0_seq produces transactions directly. When drv0_ After SEQ sends a maximum packet, send_ The over event is triggered at drv1_seq starts generating transactions.

*Complex synchronization between sequence s

The approach to resolving synchronization in the previous section seems very simple and practical. There are two issues here, however, the first one is to use a global event send_over. Global variables are popular with first-time code writers, but almost all teachers and books say this: try not to use global variables unless necessary. The main problem with global variables is that they are globally visible and were meant to be in drv0_seq and drv1_ This global variable is used in seq, but if one of the other
Sequence also accidentally uses this global variable, at drv0_seq trigger send_ This sequence triggered the event before the over event, which is not allowed. Therefore, global variables should be avoided as much as possible.

The second question is that there is only one synchronization, what if there are multiple synchronizations? For example, sequence A must be executed before B, B can be executed before C can be executed before D can be executed before E. This can still be done using the global approach above, but it can be quite clumsy.

The best way to synchronize between sequences is to use virtual sequence. Literally, it is a virtual sequence. Virtual means that it does not send transaction s at all, it just controls other sequences and plays the role of unified scheduling.

As shown in Figure 6-1, a virtual sequencer is generally required to use a virtual sequence. Virtual sequencer contains pointers to other real sequencers:

class my_vsqr extends uvm_sequencer;
	my_sequencer p_sqr0;
	my_sequencer p_sqr1;
	...
endclass

In base_ Instantiate vsqr in test and assign the corresponding sequencer to the sequencer's pointer in vsqr:

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


In virtual sequence, you can use uvm_do_on series macros to send transaction s:

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

Uvm_is introduced in section 6.3.1 Do_ On Macro, the reader was confused about its usefulness, and now he finally found the answer. virtual sequence is uvm_ Do_ Where on macros are most used.

In case0_ In vseq, use uvm_first Do_ On_ With at p_ Send a maximum packet on sequencer.sqr0 and start drv0_after it has been sent Seq and drv1_seq. Drv0_here Seq and drv1_ The SEQ is very simple, and there is nothing to do between them for synchronization:

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

Using uvm_ Do_ In the case of on macro, although seq0 is case0_ Start in vseq, but it will eventually be handed over to p_sequencer.p_sqr0, also known as env0.i_agt.sqr instead of v_sqr. This is the source of virtual in virtual sequence and virtual sequencer. Each of them does not produce a transaction, but simply controls the other sequences to produce a transaction for the corresponding sequencer. Virtual sequence and virtual sequencer only play a scheduling role. Since transactions are not directly generated at all, virtual sequence and virtual sequencer are defined without specifying the type of transaction data to be sent.

If uvm_is not used Do_ On macro, you can also start the sequence manually, which works exactly the same. One advantage of starting a sequence manually is that you can pass some values to it:

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

In read_ File_ In seq, a string file name is required, which can be specified on manual startup, but uvm_ The do series macros cannot do this because the rand modifier cannot be used before a string type variable. This is the advantage of starting sequence manually.

In case0_ Usually uvm_is used in the definition of vseq Declare_ P_ The sequencer macro. This is described earlier by referencing member variables of sequencer.

Recall that in order to solve the synchronization of sequence, send_was used before Over is a global variable to solve. So how is it solved in virtual sequence? In fact, this is not a problem at all in virtual sequence. Since the body of the virtual sequence executes sequentially, it is not necessary to deliberately synchronize other sequences as long as one of the longest packages is generated before the other sequences are started. This is only a small reflection of the powerful scheduling capabilities of virtual sequence.

Use of virtual sequence can reduce config_ Use of DB statement. Because config_db:: The second path parameter to the set function is a string, which is very error prone, so reduce config_ The use of DB statements reduces the probability of error. In the previous section, two uvm_s were used Config_ The DB statement gives two sequences to the corresponding sequencer as default_sequence. If there are more than one sequencer in the validation platform, such as 10, then 10 uvm_s need to be written config_db statement, which is a very boring thing. These 10 sentences can be compressed into one sentence after using virtual sequence:

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

As a special sequence, virtual sequence can also start other virtual sequences in it:

class case0_vseq extends uvm_sequence;
	...
	virtual task body();
		cfg_vseq cvseq;
		...
		`uvm_do(cvseq)
		...
	endtask
endclass

Where cfg_vseq is another virtual sequence that has already been defined.

Added by rgilchrist on Wed, 03 Nov 2021 18:54:40 +0200