UVM Sequencer
Previously we have seen how the test stimulus is created using sequence and sequence item. But how these stimulus is passed on to the DUT? UVM provides a component known as sequencer which acts as a bridge between sequence and driver (component which actually drives the signal to DUT). UVM sequencer can be thought of as traffic police which controls the traffic (sequence_item) coming from various directions (sequence) and ensure only the traffic from one direction (sequence) is allowed to proceed further.
What is UVM sequencer?
UVM sequencer is a UVM component which helps in managing the flow of transaction from different sequences. The sequencer doesn't create transactions; that's the job of the sequences. Instead, it arbitrates between different sequences, selects which transaction to send next, and delivers it to the driver upon request.
Custom UVM sequencer is a sub-class of uvm_sequencer
base class. uvm_sequencer
is again a sub-class of uvm_components
making the UVM sequencer a Component and an Object.
Defining a custom UVM sequencer
Definition of UVM sequencer classes are bit different from what we have seen for earlier classes. Lets dive into how UVM sequencer classes are defined.
1. Declaring the class
We define sequencer class by extending uvm_sequencer
class. This is a parametrized class which accepts the type of sequence item it will handle.
class my_sequencer extends uvm_sequencer#(my_sequence_item)
Some sequences also need a response transaction for creating the next transaction. Therefore, uvm_sequence
base class has 2 parameters, REQ
and RSP
. By default, the response has same type as that of request and thus, the above declaration works.
In case, the REQ
and RSP
types are different we can declare as the class as below:
class my_sequencer extends uvm_sequencer#(my_req_seq_item, my_rsp_seq_item);
Declaring a custom sequencer class is not always needed. For most of the cases, theuvm_sequencer
base class can be used directly to create a sequencer component.
2. Register with uvm factory
Components classes are registered using uvm_component_utils
macro.
`uvm_component_utils(my_sequencer)
3. Constructor
For any class which is derived from UVM component, we need to assign parent. This is because the components can be thought of as a physical entity in a test bench and it sits in a certain hierarchy and thus, will have a parent component.
If we don't want to assign a parent to a component class we can use `null`. Only one component in the test bench can be without a parent (which is generally the tb_top).
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
All component instances should have a different name and thus in constructor args it is not recommended to have a default value forname
. Also, all instance should have a parent that needs to be individually set. Hence,parent
should also not have a default value.
Sequencer-Driver Connection
UVM provides various TLM (transaction-level modelling) ports to facilitate the flow of transactions across various components of the test bench. We will learn more on TLM port in later part of the series.
UVM sequencer provides an in-built port seq_item_export
which can be connected to the driver's TLM port. This facilitates the flow of transaction from sequencer to driver.
m_sequencer vs. p_sequencer class
In UVM sequence article, we saw that each sequence is associated with a particular sequencer by passing the sequencer handle while using start()
method. This is how the sequencer is connected with a sequence and transaction flow happens.
We will deep down more into this in this article as we now have better understanding of sequencers. UVM sequence provide 2 sequencer handle which are of type m_sequencer
and p_sequencer
.
m_sequence handle
This is a generic uvm_sequencer_base
handle that is set using when we call start()
method of the sequence. This handle is declared automatically in the uvm_sequence
base class and thus, available by default in all sequences.
p_sequencer handle
This is a type-specific handle to a particular custom sequencer. This is not available by default and needs to be declared using the uvm_declare_p_sequencer
macro. This creates a new handle of the correct sequencer type.
When we set the sequencer using the start()
method, the m_sequencer
handles is internally type-casted to p_sequencer
using $cast
. If wrong sequencer class is passed with the start
method, it results into error.
Sequence will now use the p_sequencer
handle internally and all the custom implementation inside sequencer will be available to be used inside sequence.
class my_sequence extends uvm_sequence #(my_transaction);
`uvm_object_utils(my_sequence)
`uvm_declare_p_sequencer(my_sequencer) // Declares p_sequencer of type my_sequencer
// ...
endclass
Sequence Arbitration
In a complex testbench, there would be several sequences running on the same sequencer creating transactions of same type. For example, we can have one sequence generating read transaction and another generating write transaction. As the transaction class is same and driver will also be same, these 2 sequences will run with same sequencer.
Main idea of having an sequencer comes in this scenario. The sequencer is responsible to decide which sequence will send the transaction to the driver. In other words, sequence can only send a new transaction when it gets grant from sequencer.
UVM sequencer provides various in-built arbitration methods:
UVM_SEQ_ARB_FIFO
- This is the default arbitration method. It is based on first-in, first-out method. The sequencer grants access to the sequence in order they requested it.UVM_SEQ_ARB_WEIGHTED
- Sequencer grants access based on weights of sequences. Each sequence can be assigned priority which acts as weights. The sequencer calculates the sum of the weights of the running sequence. It then randomly selects a sequence, where the probability of a sequence being chosen is proportional to its weight relative to the total weight.UVM_SEQ_ARB_RANDOM
- Sequencer selects the next sequence randomly without considering any priority.UVM_SEQ_ARB_STRICT_FIFO
- Sequencer considers the priority of the sequence and grants access to sequence having higher priority. If there are 2 or more sequences with same priority then tie is resolved using first-in, first-out similar toUVM_SEQ_ARB_FIFO
.UVM_SEQ_ARB_STRICT_RANDOM
- In this method also, sequencer considers the priority of the sequence. If there are 2 or more sequences with same priority, the sequence is selected randomly.UVM_SEQ_ARB_USER
- This method allows the user to provide a custom arbitration logic. Custom logic can only be defined when we have implemented custom sequencer. In the custom sequencer we need to overrideuser_priority_arbitration()
method with the custom logic.
The arbitration method can be set using set_arbitration(<arbitration_method_enum>)
Advanced Sequencer usage
There may be some special scenarios where a sequence would need exclusive control over the sequencer. Like for example, if a sequence want to send a burst of transaction packet and there are multiple sequence running on same sequencer, then the sequence which wants to send bust of packets would need exclusive access of the sequencer without any interrupts.
UVM provides methods in sequence class so that they gain access to sequencer without interrupt.
lock()
- Sequence can call this method when they have got an grant from sequencer. Oncelock()
is called, no other sequence will get grant unlessunlock()
is called by the sequence.grab()
- This is a more forceful version oflock()
. This has more priority then lock and hence if a sequence callsgrab()
it will immediately get the access even if sequencer is locked by any other sequence. The sequence will have exclusive access untilungrab()
is called.
Example
In the previous article, we added random_seq
to our TB. Let's continue with that example and add custom sequencer to our test bench. In our custom sequencer we override the user_priority_arbitration
method to implement a custom arbitration logic.
`include "uvm_macros.svh"
import uvm_pkg::*;
class sequencer extends uvm_sequencer#(transaction);
`uvm_component_utils(sequencer)
// Constructor:new
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction: new
// function: build
function void build_phase(uvm_phase phase);
super.build_phase(phase);
`uvm_info(get_name(), "building custom sequencer...", UVM_LOW)
endfunction: build_phase
// task: run_phase
task run_phase(uvm_phase phase);
super.run_phase(phase);
`uvm_info(get_name(), "running custom sequencer...", UVM_LOW)
endtask: run_phase
// User-defined arbitration function
virtual function integer user_priority_arbitration(integer avail_sequences[$]);
// For this example, let's always pick the last sequence in the available list
if (avail_sequences.size() > 0)
return avail_sequences[avail_sequences.size()-1];
else
return -1;
endfunction
endclass
The custom sequencer definition in our case is minimal and used for demonstration purpose. We can use the uvm_sequencer
base class directly to create the sequencer. We will see both the ways when we will implement the higher layer.
Running the code
UVM is hierarchy based as we know and thus, we need a top level component to instance all other components before running the code.
We will create a temporary parent level component to visualize how the sequencer is instanced in the TB and how the TB hierarchy looks like. In the top level component, we start multiple sequences running on same sequencer. We also use different arbitration logic to see how the sequence are given grants by sequencer.
This is not a complete example and run would timeout because of hang. Main intent is to visualize how the hierarchy changes when we add more and more components in later articles. It will help appreciate how complex TBs are developed.
`include "package.svh"
`include "uvm_macros.svh"
class env extends uvm_component;
`uvm_component_utils(env)
sequencer custom_seqr;
uvm_sequencer#(transaction) base_seqr;
random_seq rnd_seq1, rnd_seq2, rnd_seq3;
int use_arbitratoin_method;
// Constructor:new
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction: new
// function: build
function void build_phase(uvm_phase phase);
super.build_phase(phase);
custom_seqr = sequencer::type_id::create("custom_seqr", this);
base_seqr = uvm_sequencer#(transaction)::type_id::create("base_seqr", this);
rnd_seq1 = random_seq::type_id::create("rnd_seq1");
rnd_seq2 = random_seq::type_id::create("rnd_seq2");
rnd_seq3 = random_seq::type_id::create("rnd_seq3");
endfunction: build_phase
// function: end_of_elaboration_phase
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
uvm_top.print_topology();
endfunction: end_of_elaboration_phase
// task: run_phase
task run_phase(uvm_phase phase);
super.run_phase(phase);
phase.raise_objection(this);
fork
rnd_seq1.start(custom_seqr, null, 20);
rnd_seq2.start(custom_seqr, null, 10);
rnd_seq3.start(custom_seqr, null, 20);
join
phase.drop_objection(this);
endtask: run_phase
// function: start_of_simulation_phase
function void start_of_simulation_phase(uvm_phase phase);
super.start_of_simulation_phase(phase);
// set the below variable (1-6) to try different arbitration methods
use_arbitratoin_method = 6;
// ---------------------------------------
case(use_arbitratoin_method)
1: custom_seqr.set_arbitration(UVM_SEQ_ARB_FIFO);
2: custom_seqr.set_arbitration(UVM_SEQ_ARB_RANDOM);
3: custom_seqr.set_arbitration(UVM_SEQ_ARB_WEIGHTED);
4: custom_seqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
5: custom_seqr.set_arbitration(UVM_SEQ_ARB_STRICT_RANDOM);
6: custom_seqr.set_arbitration(UVM_SEQ_ARB_USER);
default: custom_seqr.set_arbitration(UVM_SEQ_ARB_FIFO);
endcase
endfunction: start_of_simulation_phase
endclass
// TB Top
module temp_top();
initial begin
run_test("env");
end
endmodule
Try this code in EDA PlaygroundOutput
Let's observe the below output.
# KERNEL: UVM_INFO @ 0: reporter [RNTST] Running test env...
# KERNEL: UVM_INFO /home/runner/testbench.sv(20) @ 0: uvm_test_top [uvm_test_top] building env
# KERNEL: UVM_INFO /home/runner/sequencer.sv(14) @ 0: uvm_test_top.custom_seqr [custom_seqr] building custom sequencer...
# KERNEL: UVM_INFO /home/build/vlib1/vlib/uvm-1.2/src/base/uvm_root.svh(583) @ 0: reporter [UVMTOP] UVM testbench topology:
# KERNEL: ---------------------------------------------------------
# KERNEL: Name Type Size Value
# KERNEL: ---------------------------------------------------------
# KERNEL: uvm_test_top env - @335
# KERNEL: base_seqr uvm_sequencer - @487
# KERNEL: rsp_export uvm_analysis_export - @496
# KERNEL: seq_item_export uvm_seq_item_pull_imp - @614
# KERNEL: arbitration_queue array 0 -
# KERNEL: lock_queue array 0 -
# KERNEL: num_last_reqs integral 32 'd1
# KERNEL: num_last_rsps integral 32 'd1
# KERNEL: custom_seqr sequencer - @350
# KERNEL: rsp_export uvm_analysis_export - @359
# KERNEL: seq_item_export uvm_seq_item_pull_imp - @477
# KERNEL: arbitration_queue array 0 -
# KERNEL: lock_queue array 0 -
# KERNEL: num_last_reqs integral 32 'd1
# KERNEL: num_last_rsps integral 32 'd1
# KERNEL: ---------------------------------------------------------
# KERNEL:
# KERNEL: UVM_INFO /home/runner/testbench.sv(37) @ 0: uvm_test_top [uvm_test_top] running env
# KERNEL: UVM_INFO /home/runner/sequencer.sv(20) @ 0: uvm_test_top.custom_seqr [custom_seqr] running custom sequencer...
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq1 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq2 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_INFO /home/runner/rnd_sequence.sv(15) @ 0: uvm_test_top.custom_seqr@@rnd_seq3 [random_seq] Pre-body called (Seuqence started)
# KERNEL: UVM_FATAL /home/build/vlib1/vlib/uvm-1.2/src/base/uvm_phase.svh(1491) @ 9200000000000: reporter [PH_TIMEOUT] Default timeout of 9200000000000 hit, indicating a probable testbench issue
In the output observe the UVM test bench topology. We have the top class env
which is named uvm_test_top
. This has 2 child component base_seqr
which is instance of uvm base sequencer and custom_seqr
which is instance of our custom sequencer
class.
2 sequencer is created in this example to demonstrate usage of base sequencer class directly.
In this example we would not be able to appreciate the arbitration logic as there is no driver yet and hence no packet is generated. But the current implementation would be used later on to see how the arbitration logic changes the execution of sequence.
Conclusion
In this article we went through the basics of UVM sequencer, how it controls the transaction flow and how sequencers are attached to a sequence using m_sequencer
and p_sequencer
. We also explored how to define custom sequencers, connect them to drivers, and leverage powerful arbitration mechanisms to control stimulus generation. By effectively managing how and when transactions are sent, the sequencer plays a critical role in building complex and realistic verification scenarios.