Logo

UVM Sequencer

18 Jun 2025
7 mins

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, the uvm_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 for name. 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 to UVM_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 override user_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. Once lock() is called, no other sequence will get grant unless unlock() is called by the sequence.
  • grab() - This is a more forceful version of lock(). This has more priority then lock and hence if a sequence calls grab() it will immediately get the access even if sequencer is locked by any other sequence. The sequence will have exclusive access until ungrab() 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 Playground
Output

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.