UVM Begginer's Guide
UVM Begginer's Guide
UVM Begginer's Guide
Due to the lack of UVM tutorials for complete beginners, I decided to create a guide that will assist
a novice in building a verification environment using this methodology. I will not focus on
verification techniques nor in the best practices in verifying a digital design, this guide was thought
in helping you to understand the UVM API and in helping you to successfully compile a complete
environment.
The simulator used is Synopsys VCS but the testbench should compile in any HDL simulator that
supports SystemVerilog.
Introduction
As digital systems grow in complexity, verification methodologies get progressively more essential.
While in the early beginnings, digital designs were verified by looking at waveforms and
performing manual checks, the complexity we have today dont allow for that kind of verification
anymore and, as a result, designers have been trying to find the best way to automate this
process.
The SystemVerilog language came to aid many verification engineers. The language featured some
mechanisms, like classes, covergroups and constraints, that eased some aspects of verifying a
digital design and then, verification methodologies started to appear.
UVM is one of the methodologies that were created from the need to automate verification. The
Universal Verification Methodology is a collection of API and proven verification guidelines written
for SystemVerilog that help an engineer to create an efficient verification environment. Its an
open-source standard maintained by Accellera and can be freely acquired in their website.
All these aspects contributed for a reduced effort in developing new verification environments, as
designers can just reuse testbenches from previous projects and easily modify the components to
their needs.
These series of webpages will provide a training guide for verifying a basic adder block using UVM.
The guide will assume that you have some basic knowledge of SystemVerilog and will require
accompaniment of the following resources:
Book Comprehensive Functional Verification: The Complete Industry Cycle, by John Goss
The first part, starting on chapter 1, will explain the operation of the device under test
(DUT): the inputs, the outputs and the communication bus
The second part, starting on chapter 2, will give a brief overview of a generic verification
environment and the approach into verifying the DUT
The third part, starting on chapter 3, will start to describe a possible UVM testbench to be
used with our DUT with code examples. Its important to consult to the external material in
order to better understand the mechanism behind the testbench.
Chapter 1 The DUT
This training guide will focus on showing how we can build a basic UVM environment, so the
device under test was kept very simple in order to emphasize the explanation of UVM itself.
The DUT used is a simple ALU, limited to a single operation: the add operation. The inputs and
outputs are represented in Figure 1.1.
This DUT takes two values of 2 bits each, ina and inb, sums them and sends the result to the
output out. The inputs are sampled to the signal of en_i and the output is sent at the same
time en_o is signalled.
The operation of the DUT is represented as a timing diagram and as a state machine in Figure 1.2.
simpleadder.v
Chapter 2 Defining the verification
environment
Before understanding UVM, we need to understand verification.
Right now, we have a DUT and we will have to interact with it in order to test its functionality, so
we need to stimulate it. To achieve this, we will need a block that generates sequences of bits to
be transmitted to the DUT, this block is going to be named sequencer.
Usually sequencers are unaware of the communication bus, they are responsible for generating
generic sequences of data and they pass that data to another block that takes care of the
communication with the DUT. This block will be the driver.
While the driver maintains activity with the DUT by feeding it data generated from the sequencers,
it doesnt do any validation of the responses to the stimuli. We need another block that listens to
the communication between the driver and the DUT and evaluates the responses from the DUT.
This block is the monitor.
Monitors sample the inputs and the outputs of the DUT, they try to make a prediction of the
expected result and send the prediction and result of the DUT to another block, the scoreboard, in
order to be compared and evaluated.
All these blocks constitute a typical system used for verification and its the same structure used
for UVM testbenches.
To illustrate the advantage of this feature, lets imagine a situation where we are testing a another
DUT that uses SPI for communication. If, by any chance, we want to test a similar DUT but with I2C
instead, we would just need to add a monitor and a driver for I2C and override the existing SPI
blocks, the sequencer and the scoreboard could reused just fine.
UVM Classes
The previous example demonstrates one of the great advantages of UVM. Its very easy to replace
components without having to modify the entire testbench, but its also due to the concept of
classes and objects from SystemVerilog.
In UVM, all the mentioned blocks are represented as objects that are derived from the already
existent classes.
A class tree of the most important UVM classes can be seen in Figure 2.2.
Every each of these classes already have some useful methods implemented, so that the designer
can only focus on the important part, which is the functional part of the class that will verify the
design. These methods are going to addressed further ahead.
For more information about UVM classes, you can consult the document Accelleras UVM 1.1 Class
Reference.
UVM Phases
All these classes have simulation phases. Phases are ordered steps of execution implemented as
methods. When we derive a new class, the simulation of our testbench will go through these
different steps in order to construct, configure and connect the testbench component hierarchy.
The build phase is used to construct components of the hierarchy. For example, the build
phase of the agent class will construct the classes for the monitor, for the sequencer and
for the driver.
The connect is used to connect the different sub components of a class. Using the same
example, the connect phase of the agent would connect the driver to the sequencer and it
would connect the monitor to an external port.
The run phase is the main phase of the execution, this is where the actual code of a
simulation will execute.
And at last, the report phase is the phase used to display the results of the simulation.
There are many more phases but none of them are mandatory. If we dont need to have one in a
particular class, we can just omit it and UVM will ignore it.
More information about UVM phasing can be consulted in Verification Academys UVM Cookbook,
page 48.
UVM Macros
Another important aspect of UVM are the macros. These macros implement some useful methods
in classes and in variables. they are optional, but recommended.
`uvm_component_utils This macro registers the new class type. Its usually used when
deriving new classes like a new agent, driver, monitor and so on.
`uvm_field_int This macro registers a variable in the UVM factory and implements some
functions like copy(), compare() and print().
`uvm_info This a very useful macro to print messages from the UVM environment during
simulation time.
This guide will not go into much detail about macros, their usage is always the same for every
class, so its not worth to put much thought into it for now.
More information can be found in Accelleras UVM 1.1 Class Reference, page 405.
All this said, a typical UVM class will look a lot like the one described in Code 2.1.
The code listed here, is the most basic sample that all components will share as you will see from
now on.
After a brief overview of a UVM testbench, its time to start developing one. By the end of this
guide, we will have the verification environment from the Figure 2.4.
This guide will begin to approach the top block and the interface (chapter 3), then it will explain
what data will be generated with the sequences and sequencers on chapter 4.
Following the sequencers, it will explain how to drive the signals into the DUT and how to observe
the response in chapters 5 and 6 respectively.
Subsequently, it will explain how to connect the sequencer to the driver and the monitor to the
scoreboard in chapter 7. Then it will show to build a simple scoreboard in chapter 8.
A virtual interface
The top block will create instances of the DUT and of the testbench and the virtual interface will
act as a bridge between them.
The interface is a module that holds all the signals of the DUT. The monitor, the driver and the
DUT are all going to be connected to this module.
1interface simpleadder_if;
2 logic sig_clock;
3 logic sig_ina;
4 logic sig_inb;
5 logic sig_en_i;
6 logic sig_out;
7 logic sig_en_o;
8endinterface: simpleadder_if
Code 3.1: Interface module simpleadder_if.sv
After we have an interface, we will need the top block. This block will be a normal SystemVerilog
module and it will be responsible for:
Connecting the DUT to the test class, using the interface defined before.
Registering the interface in the UVM factory. This is necessary in order to pass this interface
to all other classes that will be instantiated in the testbench. It will be registered in the UVM
factory by using the uvm_resource_db method and every block that will use the same
interface, will need to get it by calling the same method. It might start to look complex, but
for now we wont need to worry about it too much.
1`include "simpleadder_pkg.sv"
2`include "simpleadder.v"
3`include "simpleadder_if.sv"
4
5module simpleadder_tb_top;
6 import uvm_pkg::*;
7
8 //Interface declaration
9 simpleadder_if vif();
10
//Connects the Interface to the DUT
11 simpleadder dut(vif.sig_clock,
12 vif.sig_en_i,
13 vif.sig_ina,
14 vif.sig_inb,
15 vif.sig_en_o,
16 vif.sig_out);
17 initial begin
18 //Registers the Interface in the configuration block
19 //so that other blocks can use it
20 uvm_resource_db#(virtual simpleadder_if)::set(.scope("ifs"),
21
.name("simpleadder_if"), .val(vif));
22
23
//Executes the test
24
run_test();
25
end
26
27
28 //Variable initialization
29 initial begin
30 vif.sig_clock = 1'b1;
31 end
32
33 //Clock generation
34 always
35 #5 vif.sig_clock = ~vif.sig_clock;
endmodule
Code 3.2: Top block simepladder_tb_top.sv
The lines 2 and 3 include the DUT and the interface into the top block, the line 5 imports
the UVM library, lines 11 to 16 connect the interface signals to the DUT.
Line 21 registers the interface in the factory database with the name simpleadder_if.
Line 24 runs one of the test classes defined at compilation runtime. This name is specified
in the Makefile.
Line 34 generates the clock with a period of 10 timeunits. The timeunit is also defined in
the Makefile.
For more information about interfaces, you can consult the book SystemVerilog for Verification: A
Guide to Learning the TestBench Language Features, chapter 5.3.
simpleadder_if.sv
simpleadder_tb_top.sv
Chapter 4 Sequences and sequencers
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 doesnt make sense to keep this level
of abstraction as we move away from the DUT, so the concept of transaction was created.
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 arent 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.
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 from uvm_sequence_item.
It will include the following variables:
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.
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.
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, lets 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.
Sequence
Line 8 starts the task body(), which is the main task of a sequence
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.
The code for the sequencer is very simple, this line will tell UVM to create a basic sequencer with
the default API because we dont need to add anything else.
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:
simpleadder_sequencer.sv
Chapter 5 Driver
The driver is a block whose role is to interact with the DUT. The driver pulls transactions from the
sequencer and sends them repetitively to the signal-level interface. This interaction will be
observed and evaluated by another block, the monitor, and as a result, the drivers functionality
should only be limited to send the necessary data to the DUT.
In order to interact with our adder, the driver will execute the following operations: control
the en_i signal, send the transactions pulled from the sequencer to the DUT inputs and wait for
the adder to finish the operation.
3. Get the item data from the sequencer, drive it to the interface and wait for the DUT
execution
In Code 5.1 you can find the base code pattern which is going to be used in our driver.
The code might look complex already but what its represented its the usual code patterns from
UVM. We are going to focus mainly on the run_phase() task which is where the behaviour of the
driver will be stated. But before that, a simple explanation of the existing lines will be given:
Line 1 derives a class named simpleadder_driver from the UVM class uvm_driver.
The #(simpleadder_transaction) is a SystemVerilog parameter and it represents the data
type that it will be retrieved from the sequencer.
Line 2 refers to the UVM utilities macro explained on chapter 2.
Line 11 starts the build phase of the class, this phase is executed before the run phase.
Line 13 gets the interface from the factory database. This is the same interface we
instantiated earlier in the top block.
Line 16 is the run phase, where the code of the driver will be executed.
Now that the driver class was explained, you might be wondering: What exactly should I write in
the run phase?
Consulting the state machine from the chapter 1, we can see that the DUT waits for the
signal en_i to be triggered before listening to the ina and inb inputs, so we need to emulate the
states 0 and 1. Although we dont intend to sample the output of the DUT with the driver, we still
need to respect it, which means, before we send another sequence, we need to wait for the DUT
to output the result.
To sum up, in the run phase the following actions must be taken into account:
4. Wait a few cycles for a possible DUT response and tell the sequencer to send the next
sequence item
The driver will end its operation the moment the sequencer stops sending transactions. This is
done automatically by the UVM API, so the designer doesnt need to to worry with this kind of
details.
In order to write the driver, its easier to implement the code directly as a normal testbench and
observe its behaviour through waveforms. As a result, in the next subchapter (chapter 5.1), the
driver will first be implemented as a normal testbench and then we will reuse the code to
implement the run phase (chapter 5.2).
For our normal testbench we will use regular Verilog code. We will need two things: generate the
clock and idesginate an end for the simulation. A simulation of 30 clock cycles was defined for this
testbench.
1//Generates clock
2initial begin
3 #20;
4 forever #20 clk = ! clk;
5end
6
7//Stops testbench after 30 clock cyles
8always@(posedge clk)
9begin
10 counter_finish = counter_finish + 1;
11
12 if(counter_finish == 30) $finish;
13end
Code 5.2 Clock generation for the normal testbench
1//Driver
2always@(posedge clk)
3begin
4 //State 0: Drives the signal en_o
5 if(counter_drv==0)
6 begin
7 en_i = 1'b1;
8 state_drv = 1;
9 end
10
11 if(counter_drv==1)
12 begin
13 en_i = 1'b0;
14 end
15
16 case(state_drv)
17 //State 1: Transmits the two inputs ina and inb
18 1: begin
19
ina = tx_ina[1];
20
inb = tx_inb[1];
21
22
tx_ina = tx_ina << 1;
23
tx_inb = tx_inb << 1;
24
25
26 counter_drv = counter_drv + 1;
27 if(counter_drv==2) state_drv = 2;
28 end
29
30 //State 2: Waits for the DUT to respond
31 2: begin
32 ina = 1'b0;
33 inb = 1'b0;
34 counter_drv = counter_drv + 1;
35
36 //After the supposed response, the TB starts over
37 if(counter_drv==6)
38 begin
39 counter_drv = 0;
40 state_drv = 0;
41
42 //Restores the values of ina and inb
43 //to send again to the DUT
44 tx_ina <= 2'b11;
45 tx_inb = 2'b10;
46 end
47 end
endcase
48
end
Code 5.3 Part of the driver
For this testbench, we are sending the values of tx_ina and tx_inb to the DUT, they are defined in
the beginning of the testbench (you can see the complete code attached to this guide).
We are sending the same value multiple times to see how the driver behaves by sending
consecutive transactions.
After the execution of the Makefile, a file named simpleadder.dump will be created by VCS. To see
the waveforms of the simulation, we just need to open it with DVE.
Its possible to see that the driver is working as expected: it drives the signal en_i on and off as well
the DUT inputs ina and inb and it waits for a response of the DUT before sending the transaction
again.
After we have verified that our driver behaves as expected, we are ready to move the code into the
run phase as seen in Code 5.4.
The ports of the DUT are acessed through the virtual interface with vif.<signal> as can be seen in
lines 4 to 6.
Lines 12 and 50 use a special variable from UVM, the seq_item_port to communicate with the
sequencer. The driver calls the method get_next_item() to get a new transaction and once the
operation is finished with the current transaction, it calls the method item_done(). If the driver
calls get_next_item() but the sequencer doesnt have any transactions left to transmit, the current
task returns.
This variable is actually a UVM port and it connects to the export from the sequencer
named seq_item_export. The connection is made by an upper class, in our case, the agent. Ports
and exports are going to be further explained in chapter 6.0.1.
This concludes our driver, the full code for the driver can be found in the
file simpleadder_driver.sv. In Figure 5.2, the state of the verification environment with the driver
can be seen.
Fig
ure 5.2 State of the verification environment with the driver
The monitor is a passive component, it doesnt drive any signals into the DUT, its purpose is to
extract signal information and translate it into meaningful information to be evaluated by other
components. A verification environment isnt limited to just one monitor, it can have multiple of
them.
The approach we are going to follow for this verification plan is: sample both inputs, make a
prediction of the expected result and compare it with the result of the DUT.
The first monitor, monitor_before, will look solely for the output of the device and it will
pass the result to the scoreboard.
The second monitor, monitor_after, will get both inputs and make a prediction of the
expected result. The scoreboard will get this predicted result as well and make a
comparison between the two values.
A portion of the code for both monitors can be seen in Code 6.1 and in Code 6.2.
The skeleton of both monitors is very similar to the driver, except for Lines 4. They represent one
of the existing UVM communication ports. These ports allow different objects to pass transactions
between them. In the section 6.0.1 you can consult a brief explanation of UVM ports.
The monitors will collect transactions from the virtual interface and use the analysis ports to send
those transactions to the scoreboard. The code for the run phase can be designed the same way
as for the driver but it was omitted in this section.
The full code for both monitors can be found in the file simpleadder_monitor.sv.
The the state of our verification environment after the monitors can be consulted in Figure 6.1.
Figure 6.1 State of the verification environment after the monitors
In chapter 4, it was mentioned that transactions are the most basic data transfer in a verification
environment but another question arises: how do transactions are moved between components?
We have already talked about TLM before when we were designing the driver. The way the driver
gets transactions from the sequencer, its the same way the scoreboard gets them from the
monitors: through TLM.
TLM stands for Transaction Level Modeling and its a high-level approach to modeling
communication between digital systems. This approach is represented by two main
aspects: portsand exports.
A TLM port defines a set of methods and functions to be used for a particular connection, while an
export supplies the implementation of those methods. Ports and exports use transaction objects
as arguments.
The communication is very easy to understand. The consumer implements a function that accepts
a transaction as an argument and the producer calls that very function while passing the expected
transaction as argument. The top block connects the producer to the consumer.
The class topclass connects the producers test_port to the consumers test_export using
the connect()method. Then, the producer executes the consumers
function testfunc() through test_port.
A particular characteristic of this kind of communication is that a port can only be connected to a
single export. But there are cases when we might be interested in having a special port that can be
plugged into several exports.
A third type of TLM port exists to cover these kind of cases: the analysis port.
An analysis port works exactly like a normal port but it can detect the number of exports that are
connected to it and every time a required function is asked through this port, all other
components whose exports are connected to an analysis port are going to be triggered.
The communication models mentioned here are part of Transaction Level Modeling 1.0. There is
another variant, TLM 2.0, that works with sockets instead of ports, but they arent going to be
mentioned in this training guide.
A brief summary of these ports and exports can be seen in Table 6.2.
An agent does not require a run phase, there is no simulation code to be executed in this block.
We will construct the monitors, the sequencer and the driver in the build phase. We will also need
to create two analysis ports, these ports will act as proxies for the monitors.
Note: We could have made the connection from the monitors directly to the scoreboard within the
'Env' class without passing through the agent's ports. There are no comments available for this
program. It's always up to the designer to decide the best option.
After we have constructed the components we need. Using the concepts learned in chapter
6.0.1 about TLM ports, we can connect each port to its destination.
A part of the code for the agent can be seen on Code 7.1.
Chapter 8 Scoreboard
The scoreboard is a crucial element in a self-checking environment, it verifies the proper operation
of a design at a functional level. This component is the most difficult one to write, it varies from
project to project and from designer to designer.
In our case, we decided to make the prediction of the DUT functionality in the monitors and let the
scoreboard compare the prediction with the DUTs response. But there are designers who prefer
to leave the prediction to the scoreboard. So the functionality of the scoreboard is very subjective.
In the agent, we created two monitors, as a result, we will have to create two analysis exports in
the scoreboard that are going to be used to retrieve transactions from both monitors. After that, a
method compare() is going to be executed in the run phase and compare both transactions. If they
match, it means that the testbench and the DUT both agree in the functionality and it will return
an OK message.
But we have a problem: we have two transaction streams coming from two monitors and we need
to make sure they are synchronized. This could be done manually by writing
appropriated write()functions but there is an easier and cleaner way of doing this: by using UVM
FIFO.
Chapter 9 Env
We are getting close to have a working testbench, there are two classes missing: the env and
the test.
The env is a very simple class that instantiates the agent and the scoreboard and connects them
together.
In Figure 9.1, its represented the current state of our testbench. There is only one component left
now: the test class.
Code 9.1 State of the testbench after the env
simpleadder_env.sv
Chapter 10 Test
At last, we need to create one more block: the test. This block will derive from the uvm_test class
and it will have two purposes:
You might be wondering why are we connecting the sequencer and the sequence in this block,
instead of the agent block or the sequence block.
The reason is very simple: by specifying in the test class which sequence will be going to be
generated in the sequencer, we can easily change the kind of data is transmitted to the DUT
without messing with the agents or sequences code.
Our testbench is finally ready, now its time to execute it and check the results.
simpleadder_test.sv
Chapter 11 Running the simulation
To run the simulation, we simply execute the provided Makefile in the GitHub repository:
$ make -f Makefile.vcs
The testbench will generate random inputs and then those inputs will be sent to the DUT. The
monitors will capture the data in the communication bus and make a prediction of the expected
result. Finally the scoreboard will evaluate the functionality by matching the DUTs response with
the prediction made by one of the monitors. If the DUT and the prediction match, an OK
message will be outputted, otherwise, we will se a Fail message.
So, in the output of the simulation, we must find for the messages starting
with UVM_INFO because the compare() method from the scoreboard is going to print a message
using the macro `uvm_info() with the result of the test.
This chapter concludes this beginners guide. You can access all the code in this repository.