The first step in verifying a RTL design is defining what kind of data should be sent to the DUT. While the driver deals with signal activities at the bit level, it doesn’t make sense to keep this level of abstraction as we move away from the DUT, so the concept of transaction was created.
A transaction is a class object, usually extended from uvm_transaction or uvm_sequence_item classes, which includes the information needed to model the communication between two or more components.
Transactions are the smallest data transfers that can be executed in a verification model. They can include variables, constraints and even methods for operating on themselves. Due to their high abstraction level, they aren’t aware of the communication protocol between the components, so they can be reused and extended for different kind of tests if correctly programmed.
An example of a transaction could be an object that would model the communication bus of a master-slave topology. It could include two variables: the address of the device and the data to be transmitted to that device. The transaction would randomize these two variables and the verification environment would make sure that the variables would assume all possible and valid values to cover all combinations.
In order to drive a stimulus into the DUT, a driver component converts transactions into pin wiggles, while a monitor component performs the reverse operation, converting pin wiggles into transactions.
After a basic transaction has been specified, the verification environment will need to generate a collection of them and get them ready to be sent to the driver. This is a job for the sequence. Sequences are an ordered collection of transactions, they shape transactions to our needs and generate as many as we want. This means if we want to test just a specific set of addresses in a master-slave communication topology, we could restrict the randomization to that set of values instead of wasting simulation time in invalid values.
Sequences are extended from uvm_sequence and their main job is generating multiple transactions. After generating those transactions, there is another class that takes them to the driver: the sequencer. The code for the sequencer is usually very simple and in simple environments, the default class from UVM is enough to cover most of the cases.
A representation of this operation is shown in Figure 4.1.
Figure 4.1 - Relation between a sequence, a sequencer and a driver
The sequence englobes a group of transactions and the sequencer takes a transaction from the sequence and takes it to the driver.
To test our DUT we are going to define a simple transaction, extended fromuvm_sequence_item. It will include the following variables:
rand bit[1:0] ina
rand bit[1:0] inb
bit[2:0] out
The variables ina and inb are going to be random values to be driven to the inputs of the DUT and the variable out is going to store the result. The code for the transaction is represented in Code 4.1.
class simpleadder_transaction extends uvm_sequence_item;
rand bit[1:0] ina;
rand bit[1:0] inb;
bit[2:0] out;
function new(string name = "");
super.new(name);
endfunction: new
`uvm_object_utils_begin(simpleadder_transaction)
`uvm_field_int(ina, UVM_ALL_ON)
`uvm_field_int(inb, UVM_ALL_ON)
`uvm_field_int(out, UVM_ALL_ON)
`uvm_object_utils_end
endclass: simpleadder_transaction
Code 4.1 – Transaction for the simpleadder
An explanation of the code will follow:
- Lines 2 and 3 declare the variables for both inputs. The rand keyword asks the compiler to generate and store random values in these variables.
- Lines 6 to 8 include the typical class constructor.
- Lines 10 to 14 include the typical UVM macros.
These few lines of code define the information that is going to be exchanged between the DUT and the testbench.
To demonstrate the reuse capabilities of UVM, let’s imagine a situation where we would want to test a similar adder with a third input, a port named inc.
Instead of rewriting a different transaction to include a variable for this port, it would be easier just to extend the previous class to support the new input.
It’s possible to see an example in Code 5.2.
class simpleadder_transaction_3inputs extends simpleadder_transaction;
rand bit[1:0] inc;
function new(string name = "");
super.new(name);
endfunction: new
`uvm_object_utils_begin(simpleadder_transaction_3inputs)
`uvm_field_int(inc, UVM_ALL_ON)
`uvm_object_utils_end
endclass: simpleadder_transaction_3inputs
Code 5.2 – Extension of the previous transaction
As a result of the class simpleadder_transaction_3inputs being an extension of simpleadder_transaction, we didn’t need to declare again the other variables. While in small examples, like this one, this might not look like something useful, for bigger verification environments, it might save a lot of work.
Sequence
Now that we have a transaction, the next step is to create a sequence.
The code for the sequencer can be found in Code 5.3
class simpleadder_sequence extends uvm_sequence#(simpleadder_transaction);
`uvm_object_utils(simpleadder_sequence)
function new(string name = "");
super.new(name);
endfunction: new
task body();
simpleadder_transaction sa_tx;
repeat(15) begin
sa_tx = simpleadder_transaction::type_id::create(...
start_item(sa_tx);
assert(sa_tx.randomize());
finish_item(sa_tx);
end
endtask: body
endclass: simpleadder_sequence
Code 5.3 - Code for the sequencer
An explanation of the code will follow:
- Line 8 starts the task body(), which is the main task of a sequence
- Line 11 starts a cycle in order to generate 15 transactions
- Line 12 initializes a blank transaction
- Line 14 is a call that blocks until the driver accesses the transaction being created
- Line 15 triggers the rand keyword of the transaction and randomizes the variables of the transaction to be sent to the driver
- Line 16 is another blocking call which blocks until the driver has completed the operation for the current transaction
Sequencer
The only thing missing is the sequencer. The sequence will be extended from the class uvm_sequencer and it will be responsible for sending the sequences to the driver. The sequencer gets extended from uvm_sequencer. The code can be seen on Code 5.4.
typedef uvm_sequencer#(simpleadder_transaction) simpleadder_sequencer;
Code 5.4 – Extension of the previous transaction
The code for the sequencer is very simple, this line will tell UVM to create a basic sequencer with the default API because we don’t need to add anything else.
So, right now our environment has the following structure:
Figure 4.2 – State of the verification environment after the sequencer
You might have noticed two things missing:
- How does the sequence connects to the sequencer?
- How does the sequencer connects to the driver
The connection between the sequence and the sequencer is made by the test block, we will come to this later on chapter 10, and the connection between the sequencer and the driver will be explained on chapter 7.
For more information about transactions and sequences, you can consult:
- Accellera’s UVM 1.1 User’s Guide, page 48
- Verification Academy’s UVM Cookbook, pages 188 and 200