Logo

UVM Sequence

24 Nov 2024
6 mins

In the last article we learnt to implement sequence item. Continuing further in our UVM track, in this article we will explore UVM sequences, their purpose, and how to define and use them effectively.

UVM sequence is a fundamental element in UVM testbenches that generates transactions for drivers. Sequence creates instances of UVM sequence item class and sends it as a stimulus to driver.

What is UVM sequence?

A UVM sequence is a class derived from uvm_sequence base class. It is responsible for creation of new sequence item objects and maintains the order in which the transactions or packets are sent to the driver.

Transactions or packets here are referred to the objects of UVM sequence item class. This is a general term used in TLM (transaction level modelling) test bench.

Sequences allow us to generate complex stimulus as multiple sequences can be orchestrated together to generate varieties of transactions.

Key Characteristics of UVM Sequences

UVM sequences have below characteristics:

  • Transaction Control: Determines the flow of transactions in the testbench.
  • Reusability: Can be reused across multiple drivers and test scenarios.
  • Customizability: Allows for custom behavior and dynamic transaction generation.

Defining UVM sequence

Defining a UVM sequence is similar to UVM sequence item.

1. Defining class

We extend uvm_sequence base class to define the UVM sequence class.

class my_sequence extends uvm_sequence#(my_seq_item);

Do note that the uvm_sequence base class is a parameterized class and we need to pass the uvm_sequence_item class which will be used in the sequence.

2. Register using UVM factory

Just like UVM sequence item, UVM sequence also needs to be registered to UVM factory using the uvm_object_utils macro.

`uvm_object_utils(my_sequence)

3. Constructor definition

Constructor definition is same as uvm_sequence_item.

function new(string name = "my_sequence");
	super.new(name);
	// additional code goes here
endfunction`

Defining the body of sequence

At this point we are done with the basic definition of the UVM sequence class. Now, we need to define the sequence's behavior and the transaction flow. UVM sequence provides set of methods, which acts a entry point of the sequence class and defines the flow. These methods are unique to class extended from uvm_sequence base class.

body() method

This is the main method where all the transaction flow is defined. Inside this, a sequence item is created, randomized and sent to the driver.

Before a new sequence item is randomized and sent to driver, we need to ensure that the driver is not busy. Until the driver is busy, sequence does not get a grant and waits for the grant. UVM sequencer is the component responsible for issuing grant to UVM sequence. We will learning more about sequencer later.

Various Callback methods

UVM sequence base class provides various other callback methods, which can be used to perform activities before and after main body execution. All methods are called sequentially starting with pre_start().

MethodDescription
task pre_start()this is the entry point of the UVM sequence clas
task pre_body()called only if call_pre_post is set to 1 and sequence is started with start
task pre_do()called on the parent sequence only when parent_sequence is not null
task body()always called and is the main method where transaction flow is defined
task post_do()called on the parent sequence only when parent_sequence is not null
task post_body()called only if call_pre_post is set to 1 and sequence is started with start
task post_start()this is the last method to be called and used for housekeeping activites

All the above methods are task because they can consume time. Also, all of these methods are callback methods and hence called automatically when sequence is started.

The above methods should not be called directly.

Implementing transaction flow

Inside body method, we need to create objects of uvm_sequence_item known as transactions and pass them to the driver. To achieve this, UVM provides various methods. Lets see them step by step

1. Create item

First we need to create object of uvm_sequence_item. This is known as transaction or packet and holds the stimulus that will be sent to the driver. This can be done either through create_item method or using the create method of UVM.

my_trans = my_seq_item::type_id::create("my_trans") //preferred method
// or
my_trans = create_item(my_seq_item)

Creating a new object for each packet/stimulus should be carefully thought off as in big test benches, we would generate thousands of stimulus. As each object takes memory, it can lead to higher memory consumption while running the TB. If possible the objects can be re-used.

2. Generate stimulus

Once transaction is created, we need to assign values to the various fields present inside uvm_sequence_item. The values can either be assigned manually for directed scenarios or randomized for random cases.

Generation of stimulus can be before grant or after grant has been given. When stimulus is generated before grant it is known as early generation and when generated after grant, it is known as late generation.

3. Waiting for grant

As sequence can send a stimulus only when driver is free, thus we need to wait for the grant. UVM has a in-built mechanism for this and can be initiated by calling start_item(). After this method is called, UVM waits for the grant and till then blocks further execution. Thus, start_time() is blocking method and may consume time.

4. Sending packet

After calling the start_item(), we need to call finish_item(). Packet is sent when finish_item is called. There should be no delay below these two calls to ensure that the packet is sent immediately when grant is issued.

Implementation example

virtual task body();
    my_trans = my_seq_item::type_id::create("my_trans")
    repeat(10) begin
        start_item(my_trans)
        if(!my_trans.randomize())
           `uvm_error("SEQ", "transaction randomization failed!")
        finish_item(my_trans)
    end
endtask

In above example, we have re-used the same object to create multiple stimulus.

Starting a sequence

After the sequence is defined, we need to execute it from a top level component so that the sequence starts sending transaction flow.

  1. Create a object of sequence class in a top level component
  2. Use start() method call on the sequence object to start the sequence.

start()

The start() methods take following arguments:

  • sequencer - the sequencer which is associated with this sequence. All the arbitration is done by the associated sequencer.
  • parent_sequence - parent sequence of the sequence.
  • this_priority - sets the priority of the sequence. If sequencer is associated with multiple sequence.
  • call_pre_post - if set to 1 calls pre_do, post_do methods.

Functional signature of start() method:

virtual task start(uvm_sequencer_base sequencer, uvm_sequence_base parent_sequence = null, int this_priority = -1, bit call_pre_post = 1)

Example

In last article we created sequence item , namedtransaction for our Test bench. We will continue working on the same test bench and create sequences for our TB.

`include "uvm_macros.svh"
import uvm_pkg::*;
class random_seq extends uvm_sequence#(transaction);
    `uvm_object_utils(random_seq)

    transaction trans;
    int no_rnd_test_cases;

    function new(string name = "rnd_seq");
        super.new(name);
    endfunction //new()

    task pre_start();
        // below variable can be fetched from config DB which helps
        // in better scability. We will see that in future articles
        no_rnd_test_cases = 200;
    endtask //pre_start()

    task body();
        trans = transaction::type_id::create("trans");
        for (int i=0; i < no_rnd_test_cases; i++) begin
            start_item(trans);
            if(!trans.randomize())
                `uvm_fatal(get_type_name(), "Randomization failed")
            trans.isRandom  = 1;
            finish_item(trans);
            `uvm_info("RND_SEQ", "Sent packet", UVM_HIGH)
        end
    endtask //body()
endclass //random_seq extends uvm_sequence

Observations

  • We have used pre_start() to initialize some of the variables needed in the sequence
  • no_rnd_test_cases is used to generate the expected number of transactions. This variable is hardcoded but can be fetched using configDB which increases scalability and flexibility.
  • Late generation methods is used to generate transactions.

Conclusion

This article outlines the essentials of UVM sequences, including their definition, utilities, and usage. Sequences are vital for creating reusable and scalable transaction flows in UVM testbenches. In the next article, we will explore layered sequences and advanced sequence control mechanisms.