Sign In
Forgot Password?
Sign In | | Create Account

Verification Horizons

Dealing With UVM and OVM Sequences

by Hari Patel & Dhaval Prajapati, eInfochips

UUVM/OVM methodologies are the first choice in the semiconductor industry today for creating verification environments. Because UVM/OVM are TLM-based (Transaction Level Modeling), sequence and sequence items play vital roles and must be created in the most efficient way possible in order to reduce rework and simulation time, and to make the verification environment user friendly. This article covers how to write generic and reusable sequences so that it’s easy to add a new test case or sequence. We use SRIO (Serial Rapid IO) protocol as an example.

Introduction

In UVM- and OVM-based environments, sequences are the basic building blocks directing the scenario generation process. Scenario generation consists of a sequence of transactions generated using UVM/OVM sequencers, so it’s important to write sequences efficiently. Key to keep in mind is the fact that sequences are just not for the generating one random scenario for a specific test case. Sequences should be user- and protocol-friendly, utilizable at the test case level, and flexible enough to allow for maximum reuse in randomly generating scenarios.

Type of Sequences

There are three types of Sequences:

  • Base Sequence
  • Basic Sequence
  • Complex Sequence

Base Sequence

Writing a sequence for any transaction does not require knowing details (i.e., agent, sequencer, etc.) about the environment since basic configuration details can be included in a base sequence. All other sequences should be derived from this base sequence, which contains many reusable functions or tasks. For example, consider the SRIO protocol, where we need to turn off the constraint of many fields in many sequences. For convenience, we can declare one function in this base sequence that can be called from derived sequences to turn off constraints for the required fields. We also can include a function to receive input from command line itself and perform the operation. A base sequence can also control the rise and fall of objections for sequences, so a user need not worry about such control while developing basic or complex sequences.

Basic Sequence

Basic sequences are basic blocks for writing test cases for any UVM/OVM-based verification environment, so reuse is similarly important. The SRIO protocol allows for performing different operations like NWRITE, NREAD, etc. Each operation should be an associated basic or primitive sequence (i.e., nwrite sequence, nread sequence, etc.).

The goal is to expose desired parameters to specific values within the base constraint. These fields (parameters) must be provided as ‘rand’ data members in the sequence itself, so that the value is randomized as part of the sequence randomization and can be controlled by the user if so desired. If the variable is not declared as ‘rand’ then it will be not accessible through uvm_do/ovm_ do macros in the parent sequence. The user must also use care in naming the variables or else constraints written in the sequence will be ignored. (Please refer to the section Declaration of Variable Name for more information.)

When we declare any variable with ‘rand’ inside the basic sequence, then it will be randomized based on the baselevel constraint. If we want to randomize it with a specific value (or range), then we can also apply a constraint on the variable within the sequence itself, known as a sequencelevel constraint. The sequence designer should avoid giving access to all fields. The objective of the primitive sequences is to provide sequences for easy generation of ‘random’ traffic.

Complex Sequence

Sequences written using a collection of basic sequences are called complex sequences. For SRIO, we might have a scenario like NWRITE followed by NREAD operation.

To perform these operations we’d string together the NWRITE and NREAD sequences. For clarity, the name of the complex sequence should be tied to the scenario. In this case, “uvm_srio_nwrite_nread_sequence” would make sense so, based on the name alone, a user could understand the intention of scenario/sequence.

Constraint

A constraint is simply bounding the variable within a certain range. Suppose we have some variable that must have a value less than 5. The constraint or boundary for that variable might then read: int a; a>0 and a<=5 / /value of “a” within 0 to 5.

There are three types of constraints

  • Base Level Constraint
  • Sequence Level Constraint
  • Test Case Level Constraint

Base Level Constraint

Base level constraints are written inside the data class (a sequence item, or configuration/transaction class) and contain all possible values of that variable (field), as defined by protocol. Consider one variable (int a) which is declared in a sequence item class with the following constraint:

// Constraint on ‘a’ in Base class
rand int a;

constraint reasonable_a{
a >= 0 && a < 8;

}

The base level constraint on variable ‘a’ is that the value must be between 0 to 7, so when we randomize this variable, the value will stay within this range. Note that it’s not necessary to apply a constraint from the sequence and test case. Any new constraint on variable ‘a’ should be within the base level constraint.

Sequence Level Constraint

As shown in Figure 1, the sequence level constraint should be within the base level constraint. So if a user wants to apply a constraint on variable ‘a,’ then it should be within 0 to 7; a user cannot apply a constraint outside the 0 to 7 range.

// Sequence Level Constraint (within base level constraint)

constraint reasonable_sequence_a{
a > 0/1/2..6 && a < 5/6/7/8;
}

Figure 1:

Test Case Level Constraint

As shown in Figure 1, the test case level constraint s hould be within the base level and sequence level constraint. A user can choose to go with base- and/ or sequence-level constraint or can narrow down those constraints in the test case.

Constraint for Dynamic Array:

Generally arrays are used to model payload, data, data_ mask, etc. We can constrain the dynamic array by size and iteration for every element of the array. The size constraints always solve before iterative constraints. Consider one example:

// Constraint for Dynamic Array
rand bit[10:0] dyn_array[] ;

constraint reasonable_dyn_array {
// Size Constraint
dyn_array.size () inside {4,8,16};

// Iterative Constraint
foreach (dyn_array[i]) {
dyn_array[i] = inside {10, 20, 30};
}
}

Writing the Iterative constraint above can degrade the performance because if the size of the array is large— let’s say 500 — then 500 constraints will be created for each loop. Definitely solving this constraint will boost CPU time consumption, perhaps by as much as 5X. We can overcome this by writing a function to randomize the dynamic array called the post_randomize () method. This approach is three to five times faster than writing the constraint:

// Dynamic Array
rand bit[10:0] dyn_array[] ;

// Size Constraint
constraint reasonable_dyn_array {
dyn_array.size() inside {4,8,16};
}

function void randomize_dyn_array ();
// Iterative Constraint
foreach (dyn_array[i])
{ dyn_array[i] = $random;
Or
dyn_array[i] = $urandom_range(100,0);
}
endfunction

//Above function should be called by post
// randomize method

function void post_randomize ();
randomize_dyn_array ();
endfunction

Declaration of Variable Name

Naming the variable plays a vital role in the sequences and test cases. For the sequences being used in a test bench, the user should not be worrying about naming the variables. Ideally, the naming convention should be unique at the top of a sequence or we should be taken care of in the basic sequence. For example, in the SRIO protocol there are some transactions (sequence/command) that require common fields like data, destination_address and payload. These payload fields are required during NWRITE, SWRITE, MAINTAINANCE_WRITE so we should consider “payload” as the unique name for all the sequences wherever these are required. The alternative of using different names like nwrite_payload, swrite_payload, maintainance_write_payload and so on for the respective sequences would require the user to inspect the name of each sequence for it’s use, an onerous process.

To overcome this, we might declare the nwrite command sequence (uvm_srio_nwrite_sequence) with two fields called payload and destination_address, which is the same name the transaction class (uvm_srio_transaction) has. But the main problem with this is we can’t use the same variable name while randomizing the sequence using uvm_ do_with or ovm_do_with as these will ignore sequence level constraints and randomize the variables with a base level constraint because of the same name. To resolve this issue, we should declare a local variable within the body and then assign a sequence variable. For example:

task uvm_srio_nwrite_sequence::body();

// Declared local variable
int nwrite_payload = payload;
int nwrite_destination_addr = destination_address;

`uvm_do_with (req,
{
req.packet_type == uvm_srio_
transaction::NWRITE;

req.payload == payload; // Wrong Method

req.payload == nwrite_payload; // Correct Method
})

It’s important to take care when naming the constraint. For example, in the SRIO protocol we have multiple sequences like nwrite, swrite, maintainance_write, and so on, allowing for performance of the write operation with a specific payload. We should have a proper base level constraint for the payload variable so there is a valid range value for the payload after randomization. If the constraint name for this variable is something like ‘reasonable_payload_constraint’ for all the sequences (nwrite, swrite, maintainance_write), and if you turn off the payload constraint using nwrite sequence, then you will also turn off the constraint of payload for all other sequences. This is why it’s critical to use a different name for each constraint, such as reasonable_nwrite_payload_constraint, reasonable_ swrite_payload_constraint, and so on.

How to Control the Randomization and Constraint

Generally sequences don’t afford direct access to turn off the constraint and randomization. But if the user wants to apply a constraint (other than the TOP LEVEL constraint to drive the ERROR scenario) then the top level constraint must be turned off. The following macros can be used to do this:

1. `uvm_do/`ovm_do: A user who does not want to change the value /constraint of a field or variable(s) defined in the sequence should use this macro. It will randomize the value of variables based on base-/toplevel constraint.

uvm_srio_nwrite_sequence nwrite_seq;
`uvm_do (nwrite_seq)

2. `uvm_do_with/`ovm_do_with: Use this macro to change the value/constraint of field or variable(s) defined in the sequence. This macro works mostly the same as `uvm_do/`ovm_do. The difference is that using this macro we can apply the constraint on the field(s) declared inside that class but cannot change the base constraint of that field.

// `uvm_do_with
//Payload within 0 to 4

uvm_srio_nwrite_sequence nwrite_seq;

`uvm_do_with (nwrite_seq,
{
nwrite_seq.payload >=’d0;
nwrite_seq.payload <=’d4;
})

// Note: `ovm_do_with also work in same way

Control on Randomization and Constraint from Sequence

The macros `uvm_do/`uvm_do_with don’t allow for turning off the constraint and randomization from sequence. However, this can be achieved with the following macros:

  1. `uvm_create/`ovm_create: creates the object of class and allocates memory to that object;
  2. `uvm_rand_send/`ovm_rand_send: works same as `uvm_do/`ovm_do (except for creating the object)
  3. `uvm_rand_send_with/`ovm_rand_send_with: works same as `uvm_do_with/`ovm_do_with (except for creating the object)

NOTE: Examples are given with UVM macros, but OVM macros work in same way.

Here’s how to turn on and turn off the constraint.

Syntax:
sequence_object.constraint_name.constraint_
mode (); // 0-turn off ,1-turn on

// uvm_srio_nwrite_sequence nwrite_seq
// create object and allocate memory to it
‘uvm_create (nwrite_seq)

nwrite_seq.reasonable_payload_constraint.
constraint_mode (0); // Turn Off the constraint

nwrite_seq.reasonable_nwrite_payload_constraint.
constraint_mode (1); // Turn ON the constraint

Here’s how to change the constraint after turning it off as shown above:

// After turning off the constraint user can give
his/her own constraint as below:

// uvm_srio_nwrite_sequence nwrite_seq

‘uvm_rand_send_with (nwrite_seq,
{
nwrite_seq.payload > 0;
nwrite_seq.payload < 9;
})

Base Test

Base Test serves as the base class for all other test cases. Generally, a person writing test cases won’t bother with the complete environment, so the base test should have all details related to the environment and configuration. Indeed, if a user wants to configure some elements commonly across all test cases, he can do so via the base test case. And if a user wants to be able to adjust the configuration (e.g., the global timeout) from the command line, then the appropriate code should be included in base test case.

Additional Information

Accelera’s Universal Verification Methodology (UVM)
1.1 Class Reference (UVM_1.1_Class_Reference
_Final_06062011.pdf)

Accelera’s Universal Verification Methodology (UVM) 1.1
User’s Guide (uvm_users_guide_1.1.pdf)

  • OVM_Reference.pdf (2.1.2)
  • OVM_UserGuide.pdf (2.1.2)
 
Online Chat