UVM practice volume I learning notes 9 - sequence in UVM

sequence advanced application

Nested sequence

A new sequence can generate two packages alternately:

class case0_sequence extends uvm_sequence #(my_transaction);
	virtual task body();
		my_transaction tr;
		repeat (10) begin
			`uvm_do_with(tr, {tr.crc_err == 1; tr.dmac == 48'h980F;}) //Generate CRC error packet
			`uvm_do_with(tr, {tr.crc_err == 0; tr.pload.size() == 1500; //Generate long packet
								tr.dmac == 48'hF675;})
		end
	endtask
endclass

It seems that writing like this is particularly troublesome. In the two different packages generated, the first constraint has two and the second constraint has three. What if there are ten constraints? If 30 test cases in the whole verification platform use such two packages, it is necessary to add these codes to the sequence of these test cases, which is a terrible thing and especially error prone. Now that CRC has been defined_ Seq and long_seq, a simpler method is in the body of a sequence, except that UVM can be used_ In addition to the transaction generated by the do macro, you can also start other sequences, that is, start another sequence within one sequence, which is a nested sequence:

class case0_sequence extends uvm_sequence #(my_transaction);
	...
	virtual task body();
		crc_seq cseq;
		long_seq lseq;
		...
		repeat (10) begin
			cseq = new("cseq");
			cseq.start(m_sequencer);
			lseq = new("lseq");
			lseq.start(m_sequencer);
		end
		...
	endtask
	...
endclass

Call the well-defined sequence directly in the new body of sequence, so as to realize the reuse of sequence. This function is very powerful. M in the above code_ The sequencer is case0_sequence pointer to the sequencer used after startup. But generally speaking, it is not so troublesome. You can use uvm_do macro to do these things:

class case0_sequence extends uvm_sequence #(my_transaction);
	...
	virtual task body();
		crc_seq cseq;
		long_seq lseq;
		...
		repeat (10) begin
			`uvm_do(cseq)
			`uvm_do(lseq)
		end
		...
	endtask
	...
endclass

uvm_ The first parameter in the do series macro can be a pointer to a transaction or a sequence. When the first parameter is sequence, it calls the start task of this sequence.

Except UVM_ In addition to the do macro, the UVM described earlier_ Send macro, uvm_rand_send macro, UVM_ The first parameter of the create macro can be a pointer to sequence. The only exception is start_item and finish_item, the parameters of these two tasks must be pointers to transaction.

*Use the rand type variable in the sequence

In the definition of transaction, rand is usually used to modify variables, indicating that this field should be randomized when randomize is called. In fact, the rand modifier can also be used in the sequence. The following sequence has a member variable ldmac:

class long_seq extends uvm_sequence#(my_transaction);
	rand bit[47:0] ldmac;
	...
	virtual task body();
		my_transaction tr;
		`uvm_do_with(tr, {tr.crc_err == 0; tr.pload.size() == 1500; tr.dmac == ldmac;})
		tr.print();
	endtask
endclass

This sequence can be called by the top-level sequence as the bottom-level sequence:

class case0_sequence extends uvm_sequence #(my_transaction);
	...
	virtual task body();
		long_seq lseq;
		...
		repeat (10) begin
			`uvm_do_with(lseq, {lseq.dmac == 48'hFFFF;})
		end
		...
	endtask
	...
endclass

Any rand modifier can be added to the sequence to regulate the transaction generated by it. Both sequence and transaction can call randomize for randomization, and both can have member variables with rand modifier. To some extent, the boundary between them is vague. That's why UVM_ The reason why do series macros can accept sequence as their argument.

When defining rand type variables in sequence, pay attention to the naming of variables. Many people are used to the fact that the name of the variable is consistent with the name of the corresponding field in the transaction:

class long_seq extends uvm_sequence#(my_transaction);
	rand bit[47:0] dmac;
	...
	virtual task body();
		my_transaction tr;
#15		`uvm_do_with(tr, {tr.crc_err == 0; tr.pload.size() == 1500; tr.dmac == dmac;})
		tr.print();
	endtask
endclass

In case0_ Start the above sequence in sequence and restrict the dmac address to 48 'hFFFF. At this time, it will be found that the dmac of the generated transaction is not 48' hFFFF, but a random value! This is because when you run to the #15 line of the above code, the compiler first goes to my_transaction looks for dmac. If it is found, it will not be looked for. That is #15, the last lines of code are equivalent to:

`uvm_do_with(tr, {tr.crc_err == 0; tr.pload.size() == 1500; tr.dmac == tr.dmac;})

long_ dmac in SEQ does not work. Therefore, when defining a rand type variable in sequence to pass constraints to the generated transaction, the name of the variable must be different from the name of the corresponding field in the transaction.

*Match of transaction type

A sequencer can only generate one type of transaction. If a sequence wants to start on this sequencer, the type of transaction it generates must be this transaction or derived from this transaction.

If the type of transaction generated in a sequence is not such a transaction, an error will be reported:

class case0_sequence extends uvm_sequence #(my_transaction);
	your_transaction y_trans;
	virtual task body();
		repeat (10) begin
			`uvm_do(y_trans)
		end
	endtask
endclass

The premise of nested sequences is that the transaction s generated by all sequences in the set can be accepted by the same sequencer.

Is there any way to hand over two distinct transaction s to the same sequencer? Yes, just set the data type that the sequencer and driver can accept to uvm_sequence_item:

class my_sequencer extends uvm_sequencer #(uvm_sequence_item);
class my_driver extends uvm_driver#(uvm_sequence_item);

You can send my alternately in the sequence_ Transaction and your_transaction:

class case0_sequence extends uvm_sequence;
	my_transaction m_trans;
	your_transaction y_trans;
	...
	virtual task body();
		...
		repeat (10) begin
			`uvm_do(m_trans)
			`uvm_do(y_trans)
		end
		...
	endtask
	`uvm_object_utils(case0_sequence)
endclass

The problem is that the data type received in the driver is uvm_sequence_item, if you want to use my_transaction or your_ cast conversion must be used for member variables in transaction:

task my_driver::main_phase(uvm_phase phase);
	my_transaction m_tr;
	your_transaction y_tr;
	...
	while(1) begin
		seq_item_port.get_next_item(req);
		if($cast(m_tr, req)) begin
			drive_my_transaction(m_tr);
			`uvm_info("driver", "receive a transaction whose type is my_transaction", 
						UVM_MEDIUM)
		end
		else if($cast(y_tr, req)) begin
			drive_your_transaction(y_tr);
			`uvm_info("driver", "receive a transaction whose type is your_transaction", 	
						UVM_MEDIUM)
		end
		else begin
			`uvm_error("driver", "receive a transaction whose type is unknown")
		end
		seq_item_port.item_done();
	end
endtask

*p_ Use of sequencer

Consider a case where the following member variables exist in the sequencer:

class my_sequencer extends uvm_sequencer #(my_transaction);
	bit[47:0] dmac;
	bit[47:0] smac;
	...
	virtual function void build_phase(uvm_phase phase);
		super.build_phase(phase);
		void'(uvm_config_db#(bit[47:0])::get(this, "", "dmac", dmac));
		void'(uvm_config_db#(bit[47:0])::get(this, "", "smac", smac));
	endfunction
	`uvm_component_utils(my_sequencer)
endclass

In build_ Use config in phase_ DB:: get gets the values of these two member variables. When the sequence sends a transaction, the destination address must be set to dmac and the source address to smac. The problem is how to get the values of these two variables in the body of sequence?

M was introduced when the nested sequence was introduced earlier_ Sequencer is a member variable belonging to each sequence, but if M is used directly_ The sequencer obtains the values of these two variables:

virtual task body();
	...
	repeat (10) begin
		`uvm_do_with(m_trans, {m_trans.dmac == m_sequencer.dmac;
					m_trans.smac == m_sequencer.smac;})
	end
	... 
endtask

Writing as above will cause compilation errors. Its root is m_ The sequencer is uvm_sequencer_base (uv m_sequencer's base class) type, not my_ Of type sequencer. m_ The prototype of sequencer is:

protected uvm_sequencer_base m_sequencer;

But because case0_sequence in my_ Start on sequencer, where M_ The sequencer is essentially my_sequencer type, so it can be in my_ In sequence, M is converted by cast transformation_ Convert sequencer to my_ Type sequencer and reference dmac and smac in it:

virtual task body();
	my_sequencer x_sequencer;
	...
	$cast(x_sequencer, m_sequencer);
	repeat (10) begin
		`uvm_do_with(m_trans, {m_trans.dmac == x_sequencer.dmac;
		m_trans.smac == x_sequencer.smac;})
	end
	... 
endtask

The above process is a little troublesome. In the actual verification platform, there are many cases where the member variables in the sequencer are used. Considering this situation, UVM built a macro: uvm_declare_p_sequencer(SEQUENCER). The essence of this macro is to declare a member variable of sequence type. For example, when defining sequence, use this macro to declare the type of sequencer:

class case0_sequence extends uvm_sequence #(my_transaction);
	my_transaction m_trans;
	`uvm_object_utils(case0_sequence)
	`uvm_declare_p_sequencer(my_sequencer)
	...
endclass

It is equivalent to declaring the following member variables:

class case0_sequence extends uvm_sequence #(my_transaction);
	my_sequencer p_sequencer;
	... 
endclass

After the UVM, the M_ The sequencer is converted to P by cast_ sequencer. This process is in pre_body() was completed before. Therefore, the member variable p can be used directly in the sequence_ Sequencer to reference dmac and smac:

class case0_sequence extends uvm_sequence #(my_transaction);
	...
	virtual task body();
		...
		repeat (10) begin
			`uvm_do_with(m_trans, {m_trans.dmac == p_sequencer.dmac;
			m_trans.smac == p_sequencer.smac;})
		end
		...
	endtask
endclass

*Derivation and inheritance of sequence

As a class, sequence can derive other sequences from it:

class base_sequence extends uvm_sequence #(my_transaction);
	`uvm_object_utils(base_sequence)
	`uvm_declare_p_sequencer(my_sequencer)
	function new(string name= "base_sequence");
		super.new(name);
	endfunction
	//define some common function and task
endclass
class case0_sequence extends base_sequence;
	...
endclass

Because each sequence is similar in the same project, many common functions or tasks can be written in the base sequence, and other sequences are derived from this sequence.

There is no problem using ordinary sequence like this, but for those who use uvm_declare_p_sequence declaration p_ The base sequence of the sequencer. Do you want to call this macro declaration P in the derived sequence_ sequencer?

The answer to this question is no, because UVM_ declare_ p_ The essence of sequence is to declare a member variable p in base sequence_ sequencer. When another sequence derives from it, P_ The sequencer is still a member variable of the new sequence, so there is no need to declare it again. If you declare it again, the system will not report an error:

class base_sequence extends uvm_sequence #(my_transaction);
	`uvm_object_utils(base_sequence)
	`uvm_declare_p_sequencer(my_sequencer)
	... 
endclass
class case0_sequence extends base_sequence;
	`uvm_object_utils(case0_sequence)
	`uvm_declare_p_sequencer(my_sequencer)
	... 
endclass

Although this is equivalent to declaring two member variables p in succession_ Sequencer, but since one of the two member variables belongs to the parent class and the other belongs to the child class, there will be no error.

Posted by smudge on Wed, 03 Nov 2021 10:05:48 -0700