Why do we use Sequences and Transactions in UVM..?
I believe, most of us agree that the “Sequence” is the lifeline of UVM based constrained random verification methodology. Sequences defines the pattern of the stimulus to be applied to the DUT & plays a very significant role in stimulus generation. But importantly, before diving deep into the “Sequence” concept, one more another very fundamental item needs attention and that is called “Transaction“. Now, first lets discuss:
- What is meant by a “Transaction”?
- What is the role/application of a “Transaction”?
- What is the relationship of a “Sequence” with the “Transactions”?
“Transaction” is a primitive level unit (OOPs Class) in a UVM based Verification Environment aka ‘Testbench’ which defines following items as part of it:
- The data members which stimulates the DUT ports. Pass the values to the DUT ports & receive the response values from the DUT ports.
- Some items which controls the temporal behavior & control the dependencies of these data members on each other to create required verification scenarios. These items we may call as the ‘control knobs’.
- Constraints applied on the data members & control knobs are also used to be part of a “Transactions”. These constraints act as the default constraints which anyways can be over-written, if required, by the in-line constraints defined during Sequence activation and Transaction randomization. Most of the data members & control knobs (declared using ‘rand’ or ‘randc’ type) are defined as of random in nature to apply the SystemVerilog randomization capability to the Testbench.
Lets have a look at the example of a “Transaction”.
An Example “Transaction” Code:
/////////////// Transaction Declaration //////////////////
class my_txn extends uvm_sequence_item;
`uvm_object_utils(my_txn)
/// Data Members towards DUT (rand type)
rand logic [31:0] addr;
rand logic [31:0] wdata;
rand enum {WRITE, READ} kind;
rand int delay;
/// Data Members from DUT (NOT rand type)
logic [31:0] rdata;
bit error;
/// Constructor
function new (string name = “my_txn”);
super.new(name);
endfunction: new
/// Constraint Section
// Delay Between 1 and 20
constraint delay_1_20 {delay inside {(1:20)};}
// 32-bit Aligned Address
constraint 32bit_align {addr[1:0]==0;}
// Write Data Condition
constraint wdata_c {wdata < 32’h2000_0000;}
endclass: my_txn
Here, we can observe that a Transaction contains lots of information that is utilized by the constraint solver, randomization & mainly by the Driver to make the pin level activity to the DUT.
Now after getting familiar with the “Transaction” or “sequence_item”, lets move to the “Sequence”.
A “Sequence” in UVM is that dynamic object which is responsible to send the “Transactions” or “sequence_items” to the Driver & since its a dynamic object so it needs an static object/platform to support in the Sequence execution and that static object is called “Sequencer”.
Hence, the communication between Sequence and Driver happens via Sequencer. Sequencer’s primary tasks are establishing the communication channels and implementing arbitration mechanism between Sequences & Driver, as shown in Figure 1 below. The flow of data objects is bidirectional, request items will typically be routed from the sequence to the driver and response items will be returned to the sequence from the driver, that is also shown in the Figure 1.
Figure 1: Sequence to Driver Communication via Sequencer
To understand a “Sequence” in its simplest form – it’s a collection of “Transactions” or “Sequence_items”. Practically , a Sequence can trigger another Sequence or Sequences from with-in that depends upon the required verification scenarios or stimulus generation structure/stimulus generation flow. I’ll try to cover these complex Sequence structures and handling in my upcoming posts.
The following code will make it clear – how a sequence i.e. “my_seq” transmit a transaction of type “my_txn” “N” number of times, one transaction after another.
An Example “Sequence” Code:
////////////////// Sequence Declaration ///////////////////
class my_seq extends uvm_sequence #(my_txn);
`uvm_object_utils(my_seq)
// Constructor
function new (string name = “”);
super.new(name);
endfunction: new
// Body Task
task body;
repeat(N)
begin
my_txn txn;
// Step 1
start_item(txn);
// Step 2
txn = my_txn::type_id::create(“txn”, this);
// Step 3
assert(txn.randomize() with (cmd == 0) );
// Step 4
finish_item(txn);
end
endtask: body
endclass: my_seq
Sequences have 2 important properties to elaborate about here:
- Body() task – uvm_sequence contains a task called body(). It is the content of the body() method which determines what a Sequence actually does.
- The m_sequencer handle – Sequences are not able to directly access testbench resources such as configuration information, or handles to register models, which are available in the component hierarchy. Sequences access Testbench resources using a Sequencer as the key element into the component hierarchy. When a sequence is started, it is associated with a Sequencer. The “m_sequencer” handle contains the reference to the
Sequencer on which the sequence is running. The m_sequencer handle can be used to access configuration information and other resources in the UVM component hierarchy.
How a Sequencer communicates with a Driver to transmit the Transactions (Sequence)? is shown in one of my previous post called “UVM Driver and Sequencer Communication“. Please refer this post to get familiar with this process.
Starting a Sequence:
Till now, we saw – the structure of a Transaction, the structure of a Sequence, relationship between Sequence and Transaction or sequence_items, how to send a sequence_items to the Driver via the Sequencer.
In addition, Its highly important to understand – How to start a Sequence?
Starting a sequence is a 3 steps process:
- Step 1: Creation of the Sequence
- Step 2: Configuration of the Sequence
- Step 3: Start running a Sequence on a Sequencer
A Sequence is started using its start() method. The argument passed to the start() method is the Sequencer pointer of the Sequencer on which we want to run this Sequence.
How all it happens internally is this – the start() method assigns the passed Sequencer pointer to the Sequencer handle which is called “m_sequencer” within the Sequence & then calls the body() task within the Sequence. When the sequence body() task completes, the start() method returns. Since it requires the body() task to finish and this requires interaction with a driver, start() is a blocking method.
Lets see all these steps using the UVM code:
An example “Test” Code:
////////////////// Test Declaration ///////////////////
class my_test extends uvm_test;
`uvm_component_utils(my_test)
/// Constructor
function new (string name, uvm_component parent);
super.new(name, parent);
endfunction: new
/// Build Function
function void build_phase (uvm_phase phase);
super.build_phase(phase);
…
…
…
endfunction: build_phase
/// Run Task
virtual task run_phase(uvm_phase phase);
// Constructing the Sequence
my_seq seq;
seq = my_seq::type_id::create(“seq”, this);
// Sequence Configuration
seq.no_of_iteration = 10;
if (!seq.randomize() with {no_of_iteration inside {[7:15]};}) begin
`uvm_fatal(“FATAL_MSG”, “Randomization Failed”)
end
// Running Sequence
phase.raise_objection(this, “Starting Sequence”);
seq.start(env.agent_inst.sqnr);
phase.drop_objection(this, “Finishing Sequence”);
endtask: run_phase
endclass: my_test
To summarize this blog, what we came across to know is all about – Transactions, Sequences, important properties of Transaction & Sequence, relationship between Transaction & Sequence and finally how to start a Sequence on a Sequencer.
I believe, this effort will help you to get a fair information about the topic. I’ll try to put down additional Sequence topics very soon. I hope you enjoyed this blog. Thank you for your time.
See you again..Till then, keep learning & have fun!…bye