Virtual Sequence and Sequencers:
Today I am going to talk about virtual sequences and the virtual sequencer. Common questions I hear from users include: why do we need a virtual sequence? How can we use it effectively?
Most UVM testbenches are composed of reusable verification components unless we are working on block-level verification of a simple protocol like MIPI-CSI. Consider a scenario of verifying a simple protocol; In this case, we can live with just one sequencer sending the stimulus to the driver. The top-level test will use this sequencer to process the sequences. Here we may not need a virtual sequence (or a virtual sequencer).
But when we are trying to integrate this IP into our SOC (or top-level block), we surely want to consider reusing out testbench components, which have been used to verify these blocks. Let us consider a simple case where we are integrating two such blocks: two sequencers driving these two blocks. From top-level test, we will need a way to control these two sequencers.
This can be achieved by using a virtual sequencer and virtual sequences. Other way of doing it is to call sequence’s start method explicitly from the top-level test by passing the sequencer to the start method.
Another scenario let’s consider the DUT is having 2 different interface ports, in this situation there would be 2 Agents serving each interface port inside the UVM Testbench. Virtual Sequence will co-ordinate & synchronize the Transactions for the 2 Agents to generate the simulation uses cases using the corresponding Sub-Sequences. Virtual Sequence decides which Agent’s Sequence will start first and the order of Sub-Sequences execution. We can say, Virtual Sequence acts like a Controller of the simulation data being generated for the DUT.
So by now, we’re clear that Virtual Sequence is a Top-level Sequence which controls the whole show of generating stimulus data/simulation data with the help of available Sub-Sequences and Primitive Sequences. Next, lets see the implementation & initialization approaches of the Virtual Sequence.
Virtual Sequence Implementation:
In UVM, Virtual Sequence can be implemented using 2 approaches.
- In the 1st approach, Virtual Sequence will contain itself the handles of the Agent’s Sequencers on which the Sub-Sequences are to be executed.
- In the 2nd approach, Virtual Sequence will run on a Virtual Sequencer which is of type uvm_sequencer. In this approach, Virtual Sequencer will contain the target Agent Sequencer’s handle.
Virtual Sequence Implementation (1st approach):
Fundamental thing to understand in 1st approach is that the Agent’s target Sequencer handle are contained by the Virtual Sequence itself.
Apart from this, there are following points to focus upon in terms of Virtual Sequence implementation:
- Virtual Sequence declaration which includes target Sequencers handles
- The way a Virtual Sequence starts the Sub-Seqs on target Sequencers
- The way a Virtual Sequence is started from a Test class
As shown in the diagram above, Virtual Sequence contains two Sequencer handles i.e. SQR_AHB & SQR_AXI. There are 2 Agents i.e. AHB Agent & AXI Agent which physically contains 2 Sequencers. These 2 Sequencers are assigned to the Sequencer handles inside Virtual Sequence in a Test. As per the shown diagram above, Virtual Sequence also creates two Sequences which are to be run on the Sequencers of the respective Agents.
Lets now dive deep into the points being mentioned above to understand the Virtual Sequence implementation using 1st approach
// Base Virtual Sequence Class
class base_vseq extends uvm_sequence #(uvm_sequence_item);
`uvm_object_utils(base_vseq)
// Target Agent Sequencers
uvm_sequencer #(ahb_txn) SQR_AHB;
uvm_sequencer #(axi_txn) SQR_AXI;
// Constructor
function new (string name = "base_vseq");
super.new(name);
endfunction: new
endclass: vseq_base
// Virtual Sequence Class
class my_vseq extends base_vseq;
`uvm_object_utils(my_vseq)
// Constructor
function new (string name = "my_vseq");
super.new(name);
endfunction: new
// Sequence Body Task
task body();
ahb_seqeunce ahb_seq;
axi_sequence axi_seq;
ahb_seq = ahb_sequence::type_id::create("ahb_seq");
axi_seq = axi_sequence::type_id::create("axi_seq");
fork
abh_seq.start(SQR_AHB);
axi_seq.start(SQR_AXI);
join
endclass: my_vseq
In the UVM code above, we got two classes i.e. & my_vseq class. base_vseq is the base virtual sequence class and my_vseq is the intended Virtual Sequence. Base virtual sequence contains the handle of the two target Sequencers i.e. SQR_AHB & SQR_AXI.
Virtual Sequence class is extended from the Base Virtual Sequenceclass. It creates the two Sub-Sequences i.e. ahb_seq & axi_seq using the Factory mechanism. Later inside the body() task the Sub-Sequences are started on the target Agent’s Sequencer by the Virtual Sequence.
Starting the Virtual Sequence:
Next we need to see how the Sequencer handle assignment happens inside the Test and how the Virtual Sequence is started from the Test?
Lets further see the required UVM code for it:
// Base Test Class
class base_test extends uvm_test;
`uvm_component_utils(base_test);
// Environment Class Instantiation
top_env Env;
// Constructor
function new (string name = "base_test", uvm_component parent = null);
super.new(name, parent);
endfunction: new
// Build Phase
function void build_phase (uvm_phase phase);
Env = top_env::type_id::create("Env");
endfunction: build_phase
// Method to Connect Sequencer Handles in VSEQ
function void init_vseq (base_vseq vseq);
vseq.SQR_AHB = test.env.ahb_agent.SQR_AHB;
vseq.SQR_AXI = test.env.axi_agent.SQR_AXI;
endfunction: init_vseq
endclass: base_test
In the test base class i.e. base_test, shown UVM code above, a method i.e. init_vseq() is created which is used to assign the sequencer handles to the handles in classes derived from the virtual sequence base class.
// Main Test
class test extends base_test;
`uvm_component_utils(test)
// Constructor
function new (string name = "test", uvm_component parent = null);
super.new(name, parent);
endfunction: new
// Run Phase
task run_phase (uvm_phase phase);
// Create the Virtual Sequence
my_vseq vseq = my_vseq::type_id::create("vseq");
phase.raise_objection(this);
// Virtual Sequence Initialization
init_vseq(vseq);
// Start the Virtual Sequence
vseq.start(null);
phase.drop_objection(this);
endtask: run_phase
endclass: test
Inside the main test which is derived from the base test, Virtual Sequence is created using Factory. Later, the initialization method i.e. init_vseq() is being called to connect the Sequencers handle. Finally Virtual Sequence is started using “Null” since this Virtual Sequence is NOT started on any particular Sequencer.
Virtual Sequence Implementation (2nd approach):
To understand the 2nd approach in a better way, first it’s important to understand about Virtual Sequencer.
Virtual Sequencer is a Sequencer that is not connected to the UVM Driver itself, but contains the handles of the target Sequencer in the Testbench hierarchy.In the Diagram below, there is an example UVM Testbench environment to show the Virtual Sequencer’s application and 2nd approach of Virtual Sequence Implementation:
As shown in the above diagram, Virtual Sequencer is the part of the Environment i.e. “Env”. Virtual Sequencer contains the handles of the target Sequencers i.e. & which are physically located inside the Agents i.e. AHB Agent & AXI Agent respectively. These target Sequencers handles assignment will be done during connect phase of the Environment class.
Virtual Sequence is located outside the Environment class & it is created in the run_phase() method of the Test. The Virtual Sequence is designed to run on the Virtual Sequencer & Virtual Sequence also gets the handles of the target Sequencers from the Virtual Sequencer.
Lets see the applicable UVM code for the Virtual Sequence Implementation, 2nd approach, as shown below:
// Virtual Sequencer Class
class virtual_seqr extend uvm_sequencer;
`uvm_component_utils(virtual_seqr)
// Target Sequencer Handles
ahb_seqr SQR_AHB;
axi_seqr SQR_AXI;
// Constructor
function new (string name = "virtual_seqr", uvm_component parent);
super.new(name, parent);
endfunction: new
endclass: virtual_seqr
Virtual Sequencer i.e. “virtual_seqr” class is declared by extended the UVM base class uvm_sequencer. Target Sequencer handles are also declared inside it.
Now lets see the implementation of the Virtual Sequence. First a Base Virtual Sequence will be declared & later Virtual Sequence will be derived from the base virtual sequence. Lets see how it’s being done:
// Base Virtual Sequence
class base_vseq extends uvm_sequence #(uvm_sequence_item);
`uvm_object_utils(base_vseq)
// Virtual Sequencer Handle
virtual_seqr v_sqr;
// Target Sequencers Handle
ahb_seqr SQR_AHB;
axi_seqr SQR_AXI;
// Constructor
function new (string name = "base_vseq");
super.new(name);
endfunction: new
// Body Task (Assign target sequencers handle)
task body();
if (!$cast(v_sqr, m_sequencer)) begin
`uvm_error(get_full_name(), "Virtual Seqr pointer cast failed")
end
SQR_AHB = v_sqr.SQR_AHB;
SQR_AXI = v_sqr.SQR_AXI;
endtask: body
endclass: base_vseq
// Virtual Sequence
class my_vseq extends base_vseq;
`uvm_object_utils(my_vseq)
// Constructor
function new (string name = "my_vseq");
super.new(name);
endfunction: new
// Body Task(starting the sub-sequences)
task body();
// Assigning the Sub-Sequencer Handles
super.body;
// Sub-Sequence Creation & Execution
ahb_sequence ahb_seq;
axi_sequence axi_seq;
ahb_seq = ahb_sequence::type_id::create("ahb_seq");
axi_seq = axi_sequence::type_id::create("axi_seq");
repeat(30) begin
ahb_seq.start(SQR_AHB);
axi_seq.start(SQR_AXI);
end
endtask: body
endclass: my_vseq
Now it’s turn to see the UVM code for the Environment class which instantiates Virtual Sequencer as well as both the Agents.
// Environment Class
class Environment extends uvm_env;
`uvm_component_utils(Environ)
// Virtual Sequencer Handle
virtual_seqr v_sqr;
// Agents Handles
ahb_agent AHB_AGNT;
axi_agent AXI_AGNT;
// Constructor
function new (string name = "Environ", uvm_component parent);
super.new(name, parent);
endfunction: new
// Build Phase
function void build_phase (uvm_phase phase);
v_sqr = virtual_seqr::type_id::create("v_sqr");
AHB_AGNT = ahb_agent::type_id::create("AHB_AGNT");
AXI_AGNT = axi_agent::type_id::create("AXI_AGNT");
endfunction: build_phase
// Connect Phase
function void connect_phase (uvm_phase phase);
v_sqr.SQR_AHB = AHB_AGNT.m_sequencer;
v_sqr.SQR_AXI = AXI_AGNT.m_sequencer;
endfunction: connect_phase
endclass: Environment
In the above Environment class i.e. “Environment”, Virtual Sequencer is instantiated & built along with two Agents i.e. “AHB_AGNT” & “AXI_AGNT”. Target Sequencer handles are also assigned in the connect_phase(). Usage of a flexible & handy feature of UVM i.e. “m_sequencer” is being shown which by default points to the UVM Sequencer derived from the uvm_sequencer.
Finally lets see how the Virtual Sequence is started on the Virtual Sequencer from the Test:
// Main Test
class Test extends uvm_test;
`uvm_component_utils(Test)
// Instantiations
my_vseq vseq;
Environment Env;
// Constructor
function new (string name = "Test", uvm_component parent = null);
super.new(name, parent);
endfunction: new
// Build Phase
function void build_phase (uvm_phase phase);
Env = Environ::type_id::create("Env");
endfunction: build_phase
// Run Phase
task run_phase (uvm_phase phase);
// Create the Virtual Sequence & Environment
vseq = my_vseq::type_id::create("vseq");
phase.raise_objection(this);
// Start the Virtual Sequence
vseq.start(Env.v_sqr);
phase.drop_objection(this);
endtask: run_phase
endclass: Test
In the Test class i.e. “Test”, both Environment & Virtual Sequence i.e. “Environment” & “my_vseq” are instantiated and created. Finally Virtual Sequence is started on the Virtual Sequencer which exists inside the Environment.
So that’s how, the Virtual Sequence can be implemented using the 2nd approach in UVM.
Thanks for the beautiful post!
Waiting for more such posts.
Thank you so much for going through the blog posts 🙂