A Practical Guide To Adopting The Universal Verfication Methodology
A Practical Guide To Adopting The Universal Verfication Methodology
The ultimate goal of the UVM is to help you find more bugs earlier in the design process. In over 14 years of working
with customers who have been building verification environments, we have learned that the best way uncover
unanticipated bugs is by using controlled randomness. In fact, we have seen more users have ultimate success in finding
unanticipated bugs using controlled randomness than any other verification technique. In many technical engagements,
we would work with users who had already verified their design and we would ask them, “Will you let us try to find a bug
in a week?”
Most engineers agreed right away. But how can you find bugs in a system that you are not familiar with, that was
tested for a long period of time by capable engineers who are much more experienced in the specific verification
requirements of the project than you? As you can probably guess, the solution is randomness! We would consult the local
engineers, abstract a protocol and system attributes that can be randomized, and let the random solver do the rest.
Success was inevitable.
UVM also provides the best framework to achieve coverage-driven verification (CDV). CDV combines automatic test
generation, self-checking testbenches, and coverage metrics to significantly reduce the time spent verifying a design. The
purpose of CDV is to:
Ensure thorough verification using up-front goal setting
Eliminate the effort and time spent manually creating hundreds of tests
Use run-time self-checking to simplify error analysis and debugging, and to receive error notifications as early as
possible
The CDV flow is different from the traditional directed-testing flow. With CDV, you start by setting verification goals
using an organized planning process. You then create a smart verification environment—one that generates random
legal stimuli and sends it to the DUT. Coverage monitors are added to the environment to measure progress and
identify non-exercised functionality. Checkers are added to identify undesired DUT behavior. Finally, simulations are
launched when both the coverage model and testbench are implemented. Then a more complete verification can be
achieved.
Using CDV, you can thoroughly verify your design by changing testbench parameters or changing the randomization seed.
Test constraints can be added on top of the smart infrastructure to guide stimulus generation to meet verification goals
sooner. Ranking technology allows you to identify the tests and seeds that contribute to the verification goals, and to
remove redundant tests from a test-suite regression. Figure 1-1 is a graphical representation of this flow.
CDV environments support both directed and automated testing. The recommended approach is to focus first on
automated testing and let it do most of the work, before devoting effort to writing time-consuming, deterministic tests
intended to reach specific scenarios that are too difficult to reach randomly. Proper planning can enable significant
efficiencies and high visibility into the verification process. Creating an executable verification plan containing concrete
metrics enables you to accurately measure your progress and thoroughness throughout a design and verification project.
Using this proven method, sources of coverage can be planned, observed, ranked, and reported at the feature level. Using
an abstracted, feature-based approach (and not relying on implementation details) results in a more readable, scalable,
and reusable verification plan. You can find out more about this well-defined process and relevant automation capabilities
in various vendor-specific materials.
Multi-language design and verification is usually not a project or corporate goal, but it is a fact of life and it provides an
opportunity for companies to share and leverage more proven verification assets. No one really starts a new project with
the goal of writing a multi-language verification environment from scratch. But many users want to save time by leveraging
available verification code without rewrites. Depending on whether you already have multiple internal VIP components
implemented in multiple languages, or if you may run into this requirement in the future, you probably need to consider a
multi-language methodology now. With the number of acquisitions and joint-development activities in the industry today,
we have seen that many users who have never dealt with reuse before now find it a common and necessary activity.
UVM uses Transaction-Level Modeling (TLM) APIs to facilitate transaction-level communication between verification
components written in SystemVerilog as well as between components written in other languages such as e and SystemC.
Using the same API in all languages, it reduces the need to learn and bridge between facilities with different semantics.
However, TLM is just the basics of multi-language (ML) interoperability. The UVM uses TLM for all standard languages.
Examples of multi-language usage for both TLM 1.0 and TLM 2.0 are demonstrated in the Cadence OVM contribution and
in Cadence product releases. Other facilities required for practical multi-language simulation include: a central
configuration mechanism, traffic randomization and coordination, messages, and more.
For some users, books are the preferred way to build knowledge; they provide an easy entry point for learning a topic.
This book is based on many years of experience in multiple application domains and saves the users the expensive
trial-and-error process to adopt a solution that works. It provides real-life tips and guidelines for successful methodology
deployment, and suggests effective coding habits you can adopt from the beginning. The book does not cover the entire
class library features, but focuses on the important ones that enable the concepts that make UVM users successful.
When users go through multiple deployments of UVM, guidelines that looked insignificant at first can make a big
difference. For example, it is important to establish a standard directory structure for reusable components. While the
nature of the directory structure is always arguable, experience shows that this seemingly-small matter can have a huge
impact on team collaboration. Another example is the need for consistency. While some guidelines look less critical for a
single user or small scale projects, they quickly grow into real pain points during collaboration, or when training new
engineers.
A typical verification team consists of multiple contributors with different skill sets and responsibilities. The different roles
of developers and environment users require different depths of verification knowledge. The organization of this user
guide is based on the way a typical verification team divides its responsibilities:
UVC developers create the reusable testbench infrastructure.
Environment users (or integrators) write tests for and configure the testbench infrastructure created by the developers
to meet project-specific verification goals. We also differentiate between testbench
integrators who instantiate and configure testbenches and test writers who create the tests on top of an .
already-assembled testbench.
In some companies the UVC developer, testbench integrator and the test writer are the same person. To gain a thorough
understanding of UVM, we highly recommend that you read this entire book regardless of your individual role. However,
the organization of the book allows environment users to quickly learn what they need to know without having to read and
understand all of the methodology.
This book is structured as follows:
To Learn About Read
The structure of UVM testbenches and components Chapter 2 “UVM Overview”
The basics of object-oriented programming Chapter 3 “Object-Oriented Programming (OOP)”
The mechanics and basic facilities of the UVM library Chapter 4 “UVM Library Basics"
The basic concepts and components that make up a standard Chapter 5 “Interface UVCs”
reusable interface environment
A technology and development flow that expedites UVC Chapter 6 “Automating UVC Creation”
development and usage
The creation of simple testbenches—collections of reusable Chapter 7 “Simple Testbench Integration”
components with additional glue logic
Note This is about initial interface UVC integration concepts and
usage. Chapter 10 “System UVCs and Testbench Integration”
covers the complete integration and how to leverage the register
package and other testbench elements in a reusable way.
Various techniques for sequence and randomization control Chapter 8 “Stimulus Generation Topics”
A methodology and automation to enable productive and Chapter 9 “Register and Memory Package”
reusable register-related verification logic
How to wrap device-specific logic and reuse it in block, Chapter 10 "System UVCs and Testbench
subsystem and system integration Integration”
An overview of both the short-term and longer term areas in Chapter 11 “The Future of UVM”
which the UVM will likely be enhanced
The main example used throughout this book is the development of a testbench for a UART Controller device. This
design and testbench are based on the UART module testbench from the UVM Reference Flow, available in the
contributions area of www.uvmworld.org. The UVM Reference Flow applies the UVM to the block and cluster verification in
a SoC design.
The smaller examples are also downloadable from www.uvmworId.org and are available in the contributions area,
listed as UVM Book Examples. Each chapter has its own directory, and example numbers are on the files. The examples are
self contained and easy to run. To run the examples with IUS, use the following command:
% irun -f run.f example-name.sv
Some examples provided in the example files are not fully described in the chapters. These are still a good source of
reference. A complete list of examples is provided at the beginning of this book (see page xiii).
We encourage you to download the reference flow and examples, review them, run them, and experiment with them.
This book uses typeface variations to help you locate and interpret information easily. These type variations are
explained in the following table.
Table 1-1 Typographical Conventions
Typeface Represents
courier The Courier font indicates code. For example, the following line is UVM code:
font uvm object utils(uart config reg)
courier In examples, Courier bold highlights important lines of code to which you should pay attention.
bold // Control field - does not translate into signal data rand apb_delay_enum delay
kind;
italic The italic font represents either user-defined variables or titles of books. In this example, exampk-name
is the beginning of the filename for your SystemVerilog file:
% irun -f run.f example-name.sv
%
1.4.3 Abbreviations
Abbreviation Definition
Data items represent stimulus transactions that are input to the DUT, Examples include networking packets, bus
transactions, and instructions. The fields and attributes of a data item are derived from the data items specification. For
example, the Ethernet protocol specification defines valid values and attributes for an Ethernet data packet. In a typical
test, many data items are generated and sent to the DUT. By intelligently randomizing data item fields using SystemVerilog
constraints, you can create a large number of meaningful tests and maximize coverage.
A driver is an active entity which emulates logic that drives the DUT. A typical driver repeatedly pulls data items
generated by a sequencer (advanced generator) and drives it to the DUT by sampling and driving the DUT signals. For
example, a driver controls the read/write signal, address bus, and data bus for a number of clock cycles to perform a write
transfer. (If you have created a verification environment in the past, you probably have implemented driver functionality.)
2.2.3 Sequencer
A sequencer is an advanced stimulus generator that controls the items provided to the driver for execution. By default,
a sequencer behaves similarly to a simple stimulus generator and returns a random data item upon request from the driver.
This default behavior allows you to add constraints to the data item class in order to control the distribution of randomized
values. Unlike generators that randomize arrays of transactions or one transaction at a time, a sequencer includes many
important built-in features. A partial list of the sequencer’s built-in capabilities includes:
Ability to react to the current state of the DUT for every data item generated
Capture of the order between data items in user-defined sequences, which forms a more structured and meaningful
stimulus pattern
Enabling time modeling in reusable scenarios
Support for declarative and procedural constraints for the same scenario
System-level synchronization and control of multiple interfaces
2.2.4 Monitor
A monitor is a passive entity that samples DUT signals but does not drive them. Monitors collect coverage information
and perform checking. Even though reusable drivers and sequencers drive bus traffic, they are not used for coverage and
checking—monitors are used instead. A monitor performs the following functions:
Collects transactions (data items). A monitor extracts signal information from a bus and translates the information
into a transaction that can be made available to other components and to the test writer. Note that this activity might
be performed by a collector component that is described below.
Extracts events. The monitor detects the availability of information (such as a transaction), structures the data, and
emits an event to notify other components of the availability of the transaction. A monitor also captures status
information so it is available to other components and to the test writer.
Performs checking and coverage
Checking typically consists of protocol and data checkers to verify that the DUT output meets the protocol
specification.
Coverage is also collected in the monitor.
Optionally prints trace information
Note We recommend splitting the monitor activity into signal- and transaction-level activities. This is done by splitting
the monitor class into a low-level collector class and a high-level monitor that does transaction-level coverage and
checking. See more information about the collector in “Collector” on page 17.
On a protocol-by-protocol basis, an environment can instantiate a separate monitor per device (for example, a monitor per
master or slave in an AHB bus), or a single bus monitor. A bus monitor handles all the signals and transactions on a bus,
while an agent monitor handles only signals and transactions relevant to a specific device.
Typically, drivers and monitors are built as separate entities (even though they may use the same signals) so they can work
independently of each other. However, you can reuse code that is common between a driver and a monitor to save time.
Note To enable an agent to operate passively when only the monitor is present, do not make monitors depend on drivers
for information.
2.2.5 Collector
In use models such as transaction-level modeling or acceleration, the signal-level activity is abstracted away completely, or
placed into the accelerator box. In addition, advanced protocols include transaction-level state machines, which need to be
checked and covered. When driving stimuli, the UVM enforces a good separation between the transaction level (sequencer)
and the signal-level activity (driver). The collector enables a similar separation for the monitoring path. The collector is also
a passive entity. It follows the specific protocol in order to collect bits and bytes and form transactions. An analysis port is
used to send the collected transaction to the monitor, where coverage and checking are performed. While not mandatory,
We recommend that you dedicate the monitoring path to a monitor and a collector. Figure 2-2, Monitor-Collector
Interaction, demonstrates the interaction between the monitor and the collector.
Note UVM 1.0 EA does not currently include a separate collector class.
TLM:uvm_analysis_imp
Monitor
Data transfer
Collector TLM:uvm_analysis_port
2.2.6 Agents
Sequencers, drivers, monitors, and collectors can be reused independently, but this requires the environment
integrator to learn the names, roles, configuration, and hookup of each of these entities. To reduce the amount of work
and knowledge required by the test writer, UVM recommends that environment developers create a more abstract
container called an agent.
Agents can emulate and verify DUT devices. They encapsulate a driver, sequencer, monitor, and collector (when
applicable). UVCs can contain more than one agent. Some agents are proactive (for example, master or transmit agents)
and initiate transactions to the DUT, while other agents (slave or receive agents) react to transaction requests. Agents
should be configurable so that they can be either active or passive. Active agents emulate devices and drive transactions
according to test directives. Passive agents only monitor DUT activity.
The environment (env) is the top-level component of the UVC. It contains one or more agents, as well as other
components such as a bus monitor. The env contains configuration properties that enable you to customize the topology
and behavior to make it reusable. For example, active agents can be changed into passive agents when the verification
environment is reused for system verification.
Figure 2-3, Typical Interface UVC, illustrates the structure of a reusable verification environment. Notice that a
UVM-compliant UVC may contain an environment-level monitor. This bus-level monitor performs checking and coverage
for activities that are not necessarily related to a single agent. An agents monitors can leverage data and events collected
by the global monitor.
UVM Environment
Sequencer Sequencer
Monitor Monitor
Bus Monitor
checks
uvm_driver uvm_driver
coverage Collector Driver Collector Driver
DUT
bus
2.2.8 Testbench
A testbench is an environment that instantiates the rest of the reusable verification components. Interface, system,
and SW environments are instantiated, configured, and connected in a testbench component. If the next generation of a
device uses exactly the same interfaces of an existing device, the testbench can be re-applied and reused. But usually the
testbench is specific to the DUT and to the project, and is typically not reused in vertical reuse scenarios. Refer to Figure
2-1 to see the integration of multiple UVCs in a UVM testbench.
As already discussed, an interface UVC captures protocol-related verification aspects. But where would you place a
device-specific set of verification aspects? For example, where would you place end-to-end coverage of multiple interfaces?
Or a DUT-specific checker? For these purposes, a module UVC is needed. A module UVC is an environment that contains
verification logic for a specific module or subsystem. It assists in verifying this subsystem, as it is vertically reused in a larger
environment.
A system UVC makes use of interface, module, and software UVCs (UVCs that can dynamically control software
execution). A system UVC might not contain its own agents, in which case it merely instantiates other UVCs and connects
them to create a larger verification environment. System UVCs are targeted at system verification—for example, a CPU,
bridge, or a cell phone. Relatively small system UVCs sometimes are called module UVCs. A system UVC typically makes use
of other UVCs, including:
Interface UVCs for connecting to the system interfaces
Other module or system UVCs, which are subsystems or modules of the current system being verified
Any system UVC can he reused in a larger system verification environment. The system UVC architecture is similar to
the interface UVC architecture and contains:
A collector for signals that connect to the DUT.
A monitor that is fed by the collector and other interface UVCs’monitors. Virtual (multiple channel) sequencers
connected to the sequencers of the interface and subsystem UVCs. While interface UVC sequencers control a single
interface, a virtual sequencer controls multiple sequencers and orchestrates the system-level scenario. Its sequence
library defines multiple-channel sequences, which simultaneously control all of the system interfaces.
A scoreboard—an untimed reference model that allows end to end checking. The scoreboard receives input from the
interface UVCs and compares the DUT responses to the testbench prediction for the same input. We recommend
using a scoreboard in the system UVC.
An address map and register files. Most systems use memory and registers, which can be modeled using a register
package. With UVM, you will have a pointer to the register database address map and register files.
Verification poses requirements such as How can you ensure that an interrupt occurred while visiting all the software
states? Or, How can you steer the simulation to visit this specific HW/SW corner cases? SW UVCs arc- elegant solutions for
requirements such as these.
A SW UVC expands the coverage-driven verification (CDV) capabilities of a testbench to control and monitor software.
The software UVC provides run-time control to the execution of driver routines. It can control the type, time, and
parameters of routine calls, and can collect desired coverage for software state variables. Virtual sequences can be used to
seamlessly control HW interfaces and SW execution. Coverage monitors can collect coverage for HW, SW and their
associations.
Note We do not cover SW UVCs in this book, if you are interested in additional information regarding these, please
contact a Cadence representative.
Figure 2-4, Typical System-Level UVM Testbench, shows how interface, module, and system UVCs can be reused to
compose a specific verification environment, including HW/SW co-verification. Later in this book we cover the details of
how to develop this type of environment.
Figure 2-3 Typical System-Level UVM Testbench
The SystemVerilog UVM Class Library provides all of the building blocks you need to quickly develop well-constructed,
reusable, verification components and test environments (see Figure 2-5). The library consists of base classes, utilities, and
macros. Components may be encapsulated and instantiated hierarchically and are controlled through an extendable set of
phases to initialize, run, and complete each test. These phases are defined in the base class library but can be extended to
meet specific project needs. See the UVM Class Reference for more information.
uvm_agent uvm_agent
Config uvm_agent uvm_agent
name
has... Config Config
uvm_sequencer uvm_sequencer
uvm_monitor sequences uvm_monitor sequences
uvm_monitor
checks
uvm_driver uvm_driver
coverage uvm_collector uvm_driver uvm_collector uvm_driver
DUT
The SystemVerilog UVM Class Library also provides various utilities to simplify the development and use of
verification environments. These utilities support debugging by providing a user-controllable messaging utility. They
support development by providing a standard communication infrastructure between verification components (TLM) and
flexible verification environment construction (UVM factory).
The SystemVerilog UVM Class Library provides global messaging facilities that can be used for failure reporting and
general reporting purposes. Both messages and reporting are important aspects of ease of use.
The factory method is a classic software design pattern used to create generic code, deferring to run time the exact
specification of the object that will be created. In functional verification, introducing class variations is frequently needed.
For example, in many tests you might want to derive from the generic data item definition and add more constraints or
fields to it; or you might want to use the new derived class in the entire environment or only in a single interface; or
perhaps you must modify the way data is sent to the DUT by deriving a new driver. The factory allows you to substitute the
verification component without having to change existing reusable code.
The SystemVerilog UVM Class Library provides a built-in central factory that allows:
Controlling object allocation in the entire environment or for specific objects,
Modifying stimulus data items, as well as infrastructure components (for example, a driver)
Use of the UVM built-in factory reduces the effort of creating an advanced factory or implementing factory methods in
class definitions. It facilitates reuse and adjustment of predefined verification IP in the end-user’s environment. One of the
biggest advantages of the factory is that it is transparent to the test writer and reduces the object-oriented expertise
required from both developers and users. See “UVM Factory” on page 55 for more information and examples of the UVM
factory.
UVM components communicate by way of standard TLM interfaces, which improves reuse. Using a SystemVerilog
implementation of TLM in UVM, a component may communicate, by way of its interface, to any other component which
implements that interface. Each TLM interface consists of one or more methods used to transport data. TLM specifies the
required behavior (semantic) of each method but does not define their implementation. Classes inheriting a TLM interface
must provide an implementation that meets the specified semantic. Thus, one component may be connected at the
transaction level to others that are implemented at multiple levels of abstraction. The common semantics of TLM
communication permit components to be swapped in and out without affecting the rest of the environment. See
“Transaction-Level Modeling in UVM” on page 47 for more information.
Object-Oriented Programming (OOP)
Unless you are an OOP expert, we encourage you to review this chapter and make sure you understand the
terminology. While you can learn the UVM library and pick up the details as you go along, it is more efficient to build some
OOP foundations before going through the verification methodology and the use model.
Many books have been written about OOP; this brief chapter covers only the basics of OOP. Our goals are to show the
motivation behind OOP, introduce basic concepts, and demonstrate these concepts using simple SystemVerilog examples
in a HW verification context.
This chapter includes:
Distributed development environments
Polymorphism in OOP
Libraries and inheritance reuse
UML block diagrams
SW design patterns
Why OOP isn’t enough
Aspect-Oriented Programming
3.1 Introduction
In the early days of software engineering there were epic struggles with project complexity, growth, and the challenge
of managing large development teams. Object-Oriented Programming (OOP) introduced a revolution to the software
community to help deal with these problems. OOP focuses on modularity, change tolerance, code reuse, ease of
understanding, and distributed development. Most projects today use object-oriented concepts.
Since C++ introduced OOP, user experience has generated more knowledge about how best to make use of OOP.
There is a lot of agreement and even more debate about OOP. Which features should be encouraged? Does C++ define
which features are object-oriented?
As reuse and verification IP become prevalent in hardware verification, OOP is applicable. In fact, functional
verification provides ideal conditions for reuse, as standard protocols, systems, and even tests and interesting sequences of
transactions take advantage of OOP.
Traditionally, a program is seen as a collection of functions. This works well for small applications, but hinders reusing a
subset of the program or making use of distributed parallel development process in which different engineers own
different aspects of the program. So how can we design a large application? OOP suggests using a “divide and conquer”
approach in which applications are a set of related, interacting objects. For example, an application that emulates traffic
would involve cars, drivers, and traffic lights, instead of designing an application for the complete traffic system, we should
focus on separate modules to capture cars, drivers, and traffic light operations. The same is true for a testbench or
verification project. Instead of focusing on the complete application, we focus on data items or protocol-specific
components that eventually comprise the testbench.
3.3 What is an Object in OOP?
An object is an entity that holds data and methods that operate on that data. Each object can be viewed as an
independent little machine or actor with a distinct role or responsibility. In our traffic emulation example, the cars, drivers,
and traffic lights are all objects.
One of the challenges in OOP is defining the objects and the interaction between them. This initial agreement is key
for allowing individuals and teams to collaborate. The objects provide services via an agreed upon public interface
(contract). Other objects can use this public API. The objects internal implementation can be independently developed and
refined. Languages provide information-hiding facilities to limit object use to its public methods. Figure 3-1 demonstrates
two developers working in an OOP environment, using an agreed-upon interface between separate objects.
Can we use the same objects to create other systems? Or copy one object and leverage it in a different system? This
requires building independent objects. No functionality overlap should exist between the objects. For instance, our traffic
emulation example may also include a race track. In a typical race track, there are no traffic lights, no turn signals, and the
cars are much faster. If the implementation of a car assumes the existence of traffic lights, it cannot take part in a race
track system that has no traffic lights. In the same vein, a functional verification example can have a packet that should not
send itself or commit to a specific protocol/driver. If it does, it cannot be used in an environment that requires a different
driving protocol or protocol layering,
A class defines the abstract characteristics (attributes) and behavior (methods) of an object. It is a blueprint that
allows creating one or more objects of the same type. For example, there might be a class for cars that defines all of
the things a car object can contain. Then, a particular make of car, say a ... Plorsche, is a sub-class (specialization) of the
car class, with its own particular attributes above and beyond a general car class. Finally, a car object is a particular
instance of a car with specific color and engine attributes, such as a silver Plorsche coupe. There could be many
Plorsche cars that share these attributes; all these would be objects of the Plorsche sub-class, or specialization, of cars.
Objects hold run-time data and are used as a building block of a program. A program or application instantiates
objects and triggers their interaction.
Figure 3-2 below illustrates how a program is made of class object instantiations.
Figure 3-2 Program and Object Interaction
class car; // class definition
local lock my_lock; // internal implementation local engine
my_engine;
task void run(); // public interface task void open();
endclass: car
module top;
car my_car = new; // object of instance creation
my_car.run();
endmodule: top
Note SystemVerilog classes are different from the Verilog module instances in their dynamic nature. The module
instances, their number, and hierarchy are created at the time of elaboration in Verilog, and are static throughout the
simulation. Objects can be created during run time upon request. This allows for the creation of modular and optimized
data structures.
In functional verification, the build process of a testbench is dynamic, which makes it more flexible. It uses procedural
flow control to instantiate the needed testbench structure. Dynamic objects are used to represent data items such as
packets, frames or transactions that are created during run time, as required, and often take into account other
environment variable values.
Humans perceive the world using generalization. An abstract notion of a car means: four wheels, an engine, at least two
doors, a steering wheel, and so on. This ability to abstract allows us to organize data and enables efficient communication.
For example, you can say “I drove my car to work yesterday” and listeners would understand without the need for you to
define a car, and talk about the specific car you drove, or the way that you “drive.” These details are unnecessary to
understanding the intent of your simple statement.
Object-oriented programming allows us to do the same thing with software design. You can define a generic class
notion and, using inheritance, create specializations of that abstract class. A sports car could be a specialization of the
generic car notion. As well as having all car attributes, it has more horsepower, better handling, and often attracts
attention. A user can express the desired functionality—for example, drive() a car—without knowing the specific details of
what driving means in a specific car.
Using inheritance allows objects with sufficiently similar interfaces to share implementation code. A parent class that
should never be instantiated—meaning it exists only for modeling purposes to enable reuse and abstraction—is declared
as a virtual class.
//define virtual classes
Changing the car car Virtual class car;
abstract type changes
engine ...
all of the derived types. Endclass
wheels
run() Virtual class truck
Extends car;
open() ...
endclass
Virtual class
Flerrari Plorsche
Implements its
own unique
method for
open_roof()
open_roof() open_roof()
virtual class car; // note that car is a virtual class local lock
my_lock;
local engine my_engine;
task run();
endtask
task open();
endtask
endclass: car
virtual class sports_car extends car;
task run () ;
...
endtask;
endclass: sports_car
class plorsche extends sports_car;
task run () ;
...
endtask; // plorsche’s runt()
endclass: plorsche
If you have an overlap between objects, consider creating an abstract class to hold the common functionality and
derive a class to capture the variation. This will reduce the amount of code needed for development and maintenance. For
example, request and response packets may have similar attributes, but the data generated in the request is collected at
the response, and vice versa.
Request Response
byte data[ ] byte data[ ]
byte delay byte delay
bit legal_par bit legal_par Overlap
calc_par() calc_par()
ls_consist() ls_consist()
constraints1 constraints2
Request Response
constraint1 constraint2
Figure 3-5 Using an Abstract Class to Model the Request and Response
Programs may require manipulating sets of objects in a procedural way. For example, you might want to have an
abstract handle to car, and in a loop call the run() method of all the cars in an array. Each car class has a different run()
method implementation. The ability to execute run() on an abstract class and have the specific run() executed is called
“polymorphism.” The programmer does not have to know the exact type of the object in advance. Example 3-2 below
demonstrates how Plorsche and Flerrari cars are derived from an abstract sport car class. Polymorphism allows the
print() task of the program to hold a pointer to the virtual class array that may consist of both Plorsche and Flerrari
cars, but execute the correct print() function for each item.
3.10 Downcast
Imagine that a new car with an ability to fly is invented, while other cars cannot fly. A car user would not want to
discover that their car cannot fly after they drive it into a chasm. An early warning is needed before you start driving to
ensure that your car has the ability to fly.
In these kinds of situations, you need to get a compile time error message before the simulation starts. For example,
you do not want an illegal assignment to break the simulation after two days of execution. Compilers force users to
explicitly downcast a generic reference before using one of its subtype features or attributes. (The term downcast means
going down in the inheritance tree.)
Note In our car example, the user needs to explicitly state that the pointer holds a car with a fly() method. Assigning a
generic handle into a specific handle is called downcast.
module top;
task fly_if_you_can(sports_car cars[] ) ;
fly_car this_car
for(int i=0;i<cars.size();i++)
begin
//cars[i].fly(); a compile-time error!
if($cast(this_car,cars[i]))
this_car.fly();
end
endtask
endmodule :top
A class library is a collection of classes to expedite system implementation. Instead of starting the implementation
from scratch, a user can derive new classes from the library classes by customizing them, or simply instantiate and use
them. Examples include mathematical libraries, graphical libraries and even verification-oriented libraries.
UVM has a class library. An important benefit of a class library is that it codifies best practices and enables
standardization and reuse. A downside of using a class library is the need to learn the class library specifications. Though
class libraries come with API specifications, it is a good practice to go through class library implementation and get a sense
of the capabilities and suggested implementation.
Figure 3-6 below shows class library usage, as library classes are directly instantiated or derived and instantiated to
build the final program.
PROGRAM Object1 Class1
Object3 Class3
Objects have attributes that hold their specific data and status. For example, one Plorsche status can be “being driven”
while a different Plorsche can be “parked.” However, SystemVerilog supports static attributes that belong to a class and not
a specific instance. These attributes can be accessed even if no instance exists and allows multi-instance booking and
settings. For example, you can count the number of instances of a specific class using a static attribute. You can also have
static methods that can manipulate static attributes. The static methods cannot access non-static properties, as these may
not even exist.
function new();
increment_counter(); // increment on construction
endfunction: new
endclass: car
Note Calling a static method does not involve a specific instance and is achieved by using the
class_name::method_name(). For example, we can use the cars static method like this:
car::increment_counter()
Parameterized classes are used when similar logic for different data types or sizes are needed. Classic usage of
parameterized classes is for containers. The following example shows a container of different types of objects:
class stack #(type T = int); // parameterized class syntax
local T items[$];
task push( T a ); ... endtask
task pop( ref T a ); ... endtask
endclass
A typical application leverages many classes from multiple origins. Chances are that some type, class, or function
names may collide and result in a compilation error. The cost of modifying class and type names for uniqueness can be
substantial. Furthermore, the issue is more challenging when using encrypted code, such as for verification IP (VIP).
To keep the global namespace free of collisions, object-oriented languages add the package or namespace feature.
This creates a different scope for each package definition. In cases where no collision exists, users can import all of the
definitions into the joint namespace using the import command. If collisions exist and inclusive import is not possible,
users can import specific types or use the fully-qualified type name of the class (a combination of the package name and
the class name).
For example, let’s assume that in our system, two types of car classes (sports_car) were developed, one by Brand A
and another by Brand Z. Without packages, two classes called “sports_car” (one each from Brand A and Brand Z) cannot be
compiled:
// file: branda_pkg.sv
package branda_pkg;
class sports_car;... endclass
endpaekage: branda_pkg
// file: brandz_pkg.sv
package brandz_pkg;
class sports_car; ... endclass
endpackage: brandz_pkg
// file: race.sv;
`include "brandz_pkg.sv"
`include "branda_pkg.sv";
module race;
branda_pkg::sports_car my_m5 = new; // use the full name of package::class
brandz_pkg::sports_car my_convertible = new;
endmodule: race
In the following example, we import the package into a single module scope. This is possible because within the
package, there are no collisions with the names. You can import all the symbols in a package or just use selected ones that
do not collide.
// file: branda_pkg.sv
package branda_pkg;
class m5;... endclass: m5
endpaekage: branda_pkg
// file: brandz_pkg.sv
package brandz_pkg;
class convertible; ... endclass: convertible
endpaekage: brandz_pkg
// file: race.sv;
`include "brandz_pkg.sv"
`include "branda_pkg.sv"
module race;
import brandz_pkg::*;
import branda_pkg::*;
convertible my_convertible = new; // use class name directly m5
my_m5 = new;
endmodule: race
It is critical to use packages for all reusable code to enable coexistence with other similar classes in other packages
and prevent code modifications.
The Unified Modeling Language (UML) defines standardized graphical notation to create an abstract model of a
system. It includes several diagram types to describe applications. It includes structure, behavior and interaction diagrams.
The most important diagram in UML, and the only diagram we cover in this book, is for class diagrams. A class diagram is a
type of static structure diagram that describes the structure of a system. It shows the system’s classes, their attributes, and
the relationships between the classes.
Class diagrams are used both for system modeling and design, and to understand an existing application. In a class
diagram, a class is represented by a rectangle. The class name is on top, the attributes are in the middle, and the list of
methods is on the bottom.
Creative solutions to software problems can produce both random benefits and consequences. Alternatively,
already-proven solutions produce more consistent results. We are not against creativity, but most verification challenges
require both creativity and common sense without the need to reinvent the wheel.
Design patterns are general, repeatable solutions to commonly occurring problems in software design. The book
《Design Patterns: Elements of Reusable Object-Oriented Software》 (often referred to as the “Gang
of Four,” or “GoF” book), published in 1995, coined the term “design patterns" and became an important source for
object-oriented design theory and practice. These best known practices are captured in a template that includes names,
challenge, code samples, and so on.
For more information, read the book 《Design Patterns: Elements of Reusable Object-Oriented
Software》, by Gamma, Helm, Johnson, and Vlissides.
SW Design Patterns: A Singleton Design Pattern
Many systems require system resources that should be instantiated only once. This could be a memory manager,
global coordinators, or other system-level facilitators. The question is how to create a class with a built-in restriction to be
instantiated only once? The singleton solution works by making the constructor function private to the class, which
prevents the class from being instantiated. The only way to instantiate the class is by calling a static method that, on first
call, allocates a local instance of the class and returns an object handle for the caller. Consecutive calls to this static method
return the instantiated instance handle without allocating a new class. An example of a singleton in the UVM library is the
reporting server that has a protected constructor to limit the creation of the server to a single instance.
Anti-patterns, also called pitfalls, are commonly reinvented bad software solutions to problems. The term was coined
in 1995 by Andrew Koenig inspired by the Gang of Four book. There are multiple reasons why bad practices are being used.
In some cases, it involves lack of planning; in others, a perfectly good solution is used under the wrong circumstances,
making it counter-productive. Like design patterns, anti-patterns have a formal template with the name, cause,
implications and more. Anti-patterns also include recovery process (refactoring) to mend the misused practice. An example
of an anti-pattern is “spaghetti” code-source code that has a complex and tangled control structure.
For more information, read the book 《AntiPatterns: Refactoring Software, Architectures, and
Projects in Crisis 》,by Brown, Marley, McCormick, and Mowbray.
Mentioned above are some books that document knowledge from years of aggregated and proven experience in
Object-Oriented Programing. So, why do we need more methodology? Most of the generic concepts are applicable and
valid for the functional verification task. But functional verification introduces unique challenges and is different from
software development. As opposed to programs that are released every six months, the result of functional verification is
the final test, several of which may be created in a single day.
Consider the testbench in Figure 3-9 and the three tests that use it. Each test touches different aspects of the
testbench. It may change the configuration, introduce different sequences, use constraints on top of the generated data
items and more to achieve its goals. Object-oriented programming was not intended for such extreme reuse, nor it was
taking into account the openness of classes to be further modified by tests.
Figure 3-9 Unique Tests and Testbench Reuse in Functional Verification
Recently, software design patterns have been criticized for suggesting complex recipes, as opposed to improving the
tools or the languages. OOPSLA (Object-Oriented Programming, Systems, Languages & Applications), an annual ACM
conference mainly in the United States, conducted a panel that was called “The Trial of the Gang of Four" on exactly this
topic.
The e language was introduced by Verisity to solve verification challenges and uses this approach of built-in solutions
rather than design patterns. Without expanding too much on the aspect-oriented nature of e, which is a superset of OOP,
aspect-oriented programming offers simple solutions to many of the SystemVerilog language limitations. In SystemVerilog
UVM, we use the factory solution, callbacks, field macros, configuration mechanism and much more; in e these are not
required. Also, e allows other capabilities, such as coverage and enumerated-type extensions, much stronger modeling
capabilities for data-item randomization, and other language features that significantly simplify the verification task. There
were several efforts in the industry to provide limited AOP extensions to other HVLs, but these were limited and missed
the target, both in understanding the challenges and the solutions. We can only hope that at some point, SystemVerilog
will adopt these capabilities and simplify the verification effort for the benefit for the SystemVerilog user community.
Summary
Object-oriented programming introduces a way to handle the complexity of large software applications. It focuses on
modularity, accommodating changes, and reuse that is key for functional verification. Knowing the theory and the
motivation is beneficial, but there is no need to master the theory in order to use the UVM library. One of the advantages
of using the UVM library is that it codifies the best verification practices into the library base classes, enabling you to do
the right things just by following the recommended instructions.
UVM Library Basks
The UVM is first and foremost a methodology and collection of best practices for functional verification. As
mentioned before, the UVM library is a capable and mature enabler of this high-level methodology. While the library
classes and engines can be used in arbitrary ways, we highly recommend following the UVM as prescribed in the following
chapters, as they suggest a proven recipe for successful verification.
This chapter covers the mechanics and basic facilities of the library. It focuses on the features that are needed for
most verification environments and their use model.
Note For simplification and educational purposes, the examples here do not necessarily follow UVM-recommended
architecture or methodology.
The topics that are covered in the chapter are:
Using the UVM Library
Library Base Classes
TLM ports
Factory
Messages and reporting facilities
Configuration mechanism
The following example displays the message “Hello World!” to the screen:
1 // Compile the UVM package
2 `include "uvm_pkg.sv"
3 module hello_world_example;
4 // Import the UVM library and include the UVM macros
5 import uvm_pkg::1;
6 `include "uvm_macros.svh"
7 initial begin
8 `uvm_info("infol","Hello World!", UVM_LOW)
9 end
10 endmodule: hello_world_example
Lines 1 -2: The comment is a reminder to compile the UVM library. The uvm_pkg.sv is the top UVM library file that
includes the rest of the UVM files into a single SystemVerilog package. Note that SystemVerilog packages need to be
compiled outside of modules or other scopes. We recommend using a command line to compile it, as packages tend to be
shared by multiple scopes and you do not want to bind the compilation of the package to a specific scope.
Line 5: When the library has been compiled, the user imports the package into any scopes that use the library features.
Line 6: The UVM macros need to be included separately because they are compiler directives that do not survive
multiple compilation steps. To avoid recompiling the entire library multiple times, they are included separately.
Line 8: The ` uvm_info macro is part of the UVM message capabilities that allow printing, formatting and controlling
screen messages. In this case, we just print the message “Hello World!”
To prevent name collisions, avoid importing the uvm_pkg into the global scope. This is true for any package being
imported.
The top UVM files are typically enclosed by the following:
`ifndef <FILE_NAME>_SVH
`define <FILE_NAME>_SVH
... body of the file
`endif
This allows including the UVM library from multiple locations and avoids multiple declarations by compiling the files
only once. We recommend using this technique in the user’s UVC files.
% irun -uvmhome $UVM_HOME myfile.sv
The Figure 4-1 class diagram below represents selected classes of the UVM library. These classes provide automation
that can be leveraged by deriving user-defined classes and customizing their behavior. In addition to automation, the base
classes provide an API that enables uniformity and thus reuse.
Note We do not show the uvm_transaction class in this diagram. While this class is still part of the library, it was
voted to be deprecated by the Accellera TSC.
In the following sections, we discuss the uvm_object and uvm_component abstract classes.
4.3 The uvm_object Class
The abstract uvm_object class is the base class for all UVM data and hierarchical classes. Its primary role is to define
and automate a set of methods for common operations such as create, copy, pack/unpack, compare, print, and record.
Classes deriving from uvm_object must implement the pure virtual methods such as create() and
get_type_name(). The code below demonstrates a simple example of an AMBA advanced peripheral bus (APB)
transfer class definition that does not use the UVM object class definition.
It is desirable to derive objects from uvm_sequence_item. Such derivation adds a few extra fields to the
object, but allows the object to be randomized as part of a uvm_sequence,
Use UVM_DEFAULT as the flag argument (instead of UVM_ALL_ON) for all `uvm_field_* macros. This allows
the UVM architects to add automation that by default might not be enabled by default. Other flags are used to
remove undesired automation.
Set the class name as the default value in the constructor argument.
Do not forget to call super.new(name) as the first statement in every constructor declaration.
There is a different macro for classes that have type parameters `uvm_object_param_utils*
`uvm_field_object defaults to deep operations. If you are using a reference to another object, make sure to
use the UVM_REFERENCE flag so that deep operations are not done.
Example 4-3 demonstrates additional usage of the field automation, including class composition and dynamic arrays. In
this example, the yapp_packet has a pkt_header field.
The following example demonstrates the built-in automation features in the UVM library. It assumes that apb_transfer
was defined as shown in Example 4-2.
Note The table printer uses the most formatting and therefore is the most expensive in run time. If your simulation
will be printing a large number of transactions, it is a good idea to use the tree format.
4.3.4 Using UVM Field Automation
Using the UVM field automation is optional. Though users can implement the set of routines on their own, we
recommend using the automation macros. Using the macros provide:
Productivity—It takes time to implement the routines and the rich set of control options that they provide.
Extensibility—Adding a field in case of nested objects requires understanding the UVC original implementation and
sometimes the logic is tricky, as opposed to the single line of field declaration of the macros.
Implementation consistency that is important for reuse—For example, if each developer implements their own
format for the print() method, the poor integrator who uses multiple packages will have to analyze an inconsistent log
file, where the field headers, values, radices, and structure are subject to the developer’s imagination.
Maintainability-Maintaining fewer lines of code simplifies the UVC developers job.
Code correctness—For large data objects, it is difficult to get all the logic for all the routines correct. A bug may show
up at an inopportune time.
Keep in mind that these macros are an internal implementation of the library, are very mature, and should never be
debugged by users. Debugging the default object print () methods is equivalent to debugging the internal $display function
in the Verilog simulator.
In summary, we have seen users who initially were concerned about the usage of macros, but later became fans once
they tried them, saying, “They just work!”
All of the infrastructure components in a UVM verification environment, including environments and tests, are derived
either directly or indirectly from the uvm_component class. The component class is quasi-static and should only be created
at the beginning of the simulation (build phase). User-defined classes derived from this class inherit additional built-in
automation.
Phasing and execution control
Hierarchy information functions
Configuration methods
Factory convenience methods
Hierarchical reporting control
In HDL, such as Verilog and VHDL, static elaboration of the instances hierarchy occurs before simulation starts. This
ensures that all,instances are in place and connected properly before run-time simulation. In SystemVerilog, classes are
instantiated at run time. This raises a few questions: When is it safe to start traffic generation and execution? When is a
good time to assume that all the UVC components have been created? and that TLM ports can be connected?
While each developer can devise their own synchronization points for these operations, the SystemVerilog UVM Class
Library provides a set of standard built-in simulation phase methods that allow synchronization of environments without
up-front planning. These phases are hooks for users to include logic to be executed at critical points in time. For example, if
you need checking logic to be executed at the end of the simulation, you can extend the check() phase and embed
procedural code in it. Your code will then be executed at the desired time during simulation. All built-in phases are
executed in zero time except the run phase. See uvm_phase documentation in the UVM Class Reference for more
information on using built-in phases.
From a high-level view, the existing simulation phases (in simulation older) are:
new()—While this is not a UVM phase, the component constructor is the first function to be executed when a component
is being allocated. The uvm_component constructor includes two parameters: a string name parameter that holds the
component name, and a reference to the components parent. We recommend against using default values for constructor
parameters of components. This ensures that the user provides meaningful values for the name and parent. The
constructor signature should look as follows:
class street extends uvm_component;
`uvm_component_utils(street)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
endclass : street
Because the component tree is static throughout the simulation, components are aware of their hierarchy location
and can query for their parent class, child classes or their location within the hierarchy. Some of the functions that
uvm_component class provides are:
get_parent()—Returns a handle to this components parent, or null if it has no parent
get_full_name()—Returns the full hierarchical name of this object. The default implementation concatenates the
hierarchical name of the parent, if any, with the leaf name of this object.
get_child(string_name)—Returns a handle to this components child, based on its name
The following example demonstrates the UVM simulation phases and component hierarchy information methods. It
includes a composition of street, city, and state, and illustrates the get_child(), get_parent(), and
get_full_name () capabilities. Allocating components using new() is discouraged. See more on this in “UVM Factory”
on page 55.
This example illustrates a hierarchy of components that includes state, city, and street instances. Note the use of the
constructor and the build(), r u n ( ) , a n d end_of_elaboration () phases. The user implements the desired
logic within the phases hooks. The run_test() call at Line 50 triggers the automatic phases execution. The example
also demonstrates the hierarchical information functions get_child(), get_parent(), and get_full_name().
1 module test;
2 import uvm_pkg:: *;
3 `include "uvm_macros.svh"
4 class street extends uvm_component;
5 `uvm_component_utils(street)
6 function new(string name, uvm_component parent);
7 super.new(name, parent);
8 endfunction : new
9 task run();
10 `uvm_info("INFOl", "I vacationed here in 2010", UVM_LOW)
11 endtask : run
12 endclass : street
13 class city extends uvm_component;
14 street::Main_St;
15 `uvm_component_utils(city)
16 function new(string name, uvm_component parent);
17 super.new(name, parent);
18 endfunction : new
19 function void build();// note that we allocate in the build()
20 super.build();
21 Main_St = street::type_id::create("Main_St", this);
22 endfunction : build
23 task run () ;
24 uvm_component child, parent;
25 string pr, ch;
26 child = get_child("Main_St");
27 parent= get_parent();
28 pr= parent.get_full_name();
29 ch = child.get_name();
30 `uvm_info(get_type_name(),$psprintf("Parent:%s
child:%s",pr,ch),UVM_LOW);
31 endtask :run
32 endclass:city
33 class state extends uvm_component;
34 city Capital_city;
35 `uvm_component_utils(state)
36 function new(string name, uvm_component parent);
37 super.new(name, parent);
38 endfunction : new
39 function void build();
40 super.build();
41 Capital_city city::type_id::create("Capital_city", this);
42 endfunction : build
43 function void end_of_elaboration();
44 this.print();
45 endfunction : end_of_elaboration
46 endclass:state
47 state Florida= state::type_id::create("Florida", null);
48 state New_York=state::type_id::create("New_York", null);
49 // Start UVM Phases
50 initial run_test();
51 initial #100 global_stop_request();
52 endmodule : test
Line 4: A street class definition. Since the street has no child components, a build() method is not needed. The new()
and the build() methods call super.new() and super.build().
Line 9: The run method prints an informational message.
Line 13: The city component contains a street child component.
Line 19: The street is created in the build() phase using the factory.
Line 26: From within the run() phase, the city calls get_child() to receive information about the street child
component.
Line 27: get_parent() returns the parent component of the city that is in this case the state.
Line 28: get_full_name() returns the fall hierarchical path of the parent all the way to the top.
Lines 47-48: Instantiation and creation of two states; these are also created using the factory.
Line 50: run_test() call to start the UVM phases.
The result of executing the code above looks like this:
There is a special component in UVM called uvm_top. This component is a singleton instance of the uvm_root type
which is derived from uvm_component. The uvm_top component is responsible for the phasing of all components, and
provides searching services (uvm_top.find() and uvm_top.find_all()) to allow you to locate specific
components or groups of components in the verification hierarchy. The uvm_top component also provides a place to
store global configuration settings; calling set_config_* from outside of a component is equivalent to calling
uvm_top.set_config_*. This is true for messages as well; calling uvm_report_* (or
`uvm_info/warning/error/fatal) from outside of a component is equivalent to calling uvm_top.
uvm_report*
An important aspect of a generic reusable verification component is the ability to configure it for a desired operation
mode. The UVM provides a flexible configuration mechanism that allows configuring run-time attributes and component
topology without derivation or use of the factory. The configuration of component attributes is achieved via
set_config* API that includes three functions:
virtual function void set_config_int (string inst_name,string field_name,
uvm_bitstream_t value )
virtual function void set_config_string (string inst_name,string field_name,string
value )
virtual function void set_config_object (string inst_name,string
field_name,uvm_object value, bit clone = 1 )
Argument description:
inst_name: A hierarchical path to a component instance. Use of wildcards is allowed as part of the instance name.
field_name: The name of the field that needs to be configured. This field is defined in the instance.
value:The value to be assigned to the field. Depending on the set_config call, it can be either uvm_bitstream,
string, or uvm_object.
clone: Valid only for set_config object and indicates whether a different copy of the object is provided or just a
reference to the value object.
Properties that are registered as UVM fields using the uvm_field_* macros are automatically updated by the
components super.build() method. These properties can then be used to determine the build() execution for the
component. You can also use the manual get_config_* method to get the configuration values before the build()
phase (for example, in the component constructor).
Note To influence the testbench topology, the configuration settings must be provided before the component build
occurs.
The following example illustrates the use of the configuration mechanism. Instead of using class derivation, we use
the configuration mechanism to set the state names and number of votes for each state.
1 module test;
2 class state extends uvm_component;
3 string state_name;
4 int unsigned num_votes;
5 // Configurable fields must use the field declaration macros. Use the
6 // UVM_READONLY flag if the property should not be configurable.
7 `uvm_component_utils_begin(state)
8 `uvm_field_string(state_name, UVM_DEFAULT)
9 `uvm_field_int(num_votes, UVM_DEFROLT | UVM_DEC)
10 `uvm_component_utils_end
11 function new (string name, uvm_component parent);
12 super.new(name, parent);
13 endfunction : new
14 function void end_of_elaboration();
15 this.print();
16 endfunction : end_of_elaboration
17 endclass : state
18 state my_state1, my_state2;
19 initial begin
20 // configure BEFORE create
21 set_config_string ( "my_state1" , "state_name" , "GEORGIA") ;
22 set_config_string("my_state2", "state_name", "CONNECTICUT");
23 set_config_int("*", "num_votes", 7),
24 // create
25 my_state1 = state::type_id::create("my_statel", null);
26 my_state2 = state::type_id::create("my_state2", null);
27 fork
28 run_test();
29 #100 global_stop_request();
30 join
32 endmodule : test
Line 23: We used the “*” notation to configure the num_votes field. All states will use that configuration value. The
log information below shows the component configurations in the end_of_elaboration phase().
Over two decades ago, designers shifted from gate-level to RTL design. This shift was driven by the development of
standard Verilog and VHDL RTL coding styles, as well as the availability of RTL synthesis and implementation tools. A major
benefit of moving to RTL was that it enabled designers to focus more on the intended cycle level behavior designs and
design correctness at this level, and much less on gate-level considerations.
Transaction-level modeling (TLM) is a similar shift in abstraction levels that has been occurring for both design and
verification engineers. With transaction-level models, the focus is on modeling distinct transactions flowing through a
system, and less on clock cycle level behavior.
TLM has been used within testbenches for many years. In general, any testbench is at the transaction-level if it has
components that create stimulus or that do coverage or checks using transactions rather than using clock cycle level
behavior. To verify RTL DUTs, such testbenches use transactors (sometimes called "bus functional models” or BFMs) to
convert between the transaction level and the RTL. In UVM, such transactors are also called drivers and collectors.
In TLM, transactions in the system are modeled using method calls and class objects. There are several significant
benefits that come from modeling at the transaction level rather than at the signal level:
TLM models are more concise and simulate faster than RTL models.
TLM models are at a higher level of abstraction, and more closely match the level of abstraction at which the
verification engineer or design engineer thinks about the intended functionality. This makes it easier to write the
models and easier for other engineers to understand them.
TLM models tend to be more reusable since unnecessary details which hinder reuse are moved outside of the models.
Also, TLM enables use of object-oriented techniques such as inheritance and separation of interfaces from
implementation.
The adoption of TLM is dependent on the availability of standard TLM modeling approaches, just as the adoption of
RTL synthesis flows was dependent on the availability of standard RTL coding styles. Fortunately, in recent years, several
important standardized TLM APIs have been defined. Two particularly prominent standards in the EDA and ESL area are the
Open SystemC Initiative (OSCI) TLM 1.0 and TLM 2.0 standards.
The OSCI TLM 1.0 standard is a simple, general-purpose TLM API that is designed to model message passing. In
message passing, objects (in this case transactions) are passed between components in a manner similar to packets being
passed on a network.
In message passing as in packet transmission, there is no shared state between sender and receiver—rather, the only
communication is contained within the messages that are passed.
The OSCI TLM 2.0 standard is designed to enable the development of high speed virtual platform models in SystemC.
The TLM 2.0 standard is specifically targeted at modeling on-chip memory mapped buses, and contains many features to
enable integration and reuse of components which connect to on-chip buses.
OSCI TLM 1.0 and TLM 2.0 are separate and distinct standards, each serving different purposes. Some people may
assume that TLM 2.0 supersedes TLM 1.0 due to their naming, but this is not the case.
The UVM provides TLM APIs and classes which are based on the TLM 1.0 standard. This is because the general purpose
message passing semantics of TLM 1.0 are well suited to the needs of transaction-level modeling for many verification
components. TLM 1.0 is also well suited to modeling communication between different languages, for example between
models in SystemVerilog, SystemC, and e. The TLM 1.0 interfaces in UVM can even be used to communicate with TLM 2.0
models in SystemC.
This section presents some of the key concepts for using TLM within UVM so that you can understand how to use TLM
to construct reusable verification components. More detailed information on the various TLM classes in UVM is also
available within the UVM Reference Manual.
In UVM, a transaction is any class that extends from uvm_sequence_item. A transaction is defined by the user to
contain whatever fields and methods are required to model the information that needs to be communicated between
different components within the verification environment. For example, a simple packet might be modeled as follows:
1 class simple_packet extends uvm_sequence_item;
2 rand int src_addr;
3 rand int dst_addr;
4 rand byte unsigned data[];
5 constraint addr_constraint { src_addr != dst_addr; }
6 . . .
7 endclass
A transaction typically contains sufficient data fields to enable a driver or transactor to create the actual signal-level
activity that is required to represent the transaction data. A transaction may also contain additional data fields to control
how randomization occurs, or for any other purposes within the verification environment. You can extend from
transactions to include additional data members, functions, and constraints. Later sections show how you can use the
concept of extending from transactions to accomplish specific verification tasks with minimum effort.
The TLM API in UVM specifies a set of methods that are called by models to communicate transactions between
components. In UVM, a port object specifies the set of methods that can be called, while an export object provides the
implementation of the methods. Ports and exports are connected together via connect() calls when the verification
environment is constructed, and subsequent calls to the TLM methods on the port will execute the TLM methods
implemented within the export.
In UVM TLM, the put interface can be used to send a transaction from a producer to a consumer. A simple example of
such a producer in UVM follows·
class producer extends uvm_component;
uvm_blocking_put_port #(simple_packet) put_port;
function new(string name, uvm_component parent);
put_port = new("put_port", this);
endfunction
virtual task r u n ( ) ;
simple_packet p = n e w ( ) ;
put_port.put(p);
endtask
endclass
As mentioned before, the put port is connected via a connect() call to a put export. The implementation of the put
method that is called above is provided in the consumer component:
class consumer extends uvm_component;
uvm_blocking_put_imp #(simple_packet, consumer) put_export;
task put(simple_packet p);
// consume the packet
endtask
endclass
The result of connecting the port to the export and then calling the put method in the producer above is that the put
implementation in the consumer will execute with the simple_packet object passed from producer to consumer.
TLM also include standard graphical notation to illustrate different type of communication. The put communication flow is
captured in the block diagram below:
In the example above, the port-to-export connection needs to be created by calling the connect() method. To make
the connection, the user needs to invoke the connect method within the user-supplied connect callback in the parent
component of the producer and consumer components:
class parent_comp extends uvm_component;
producer producer_inst;
consumer consumer_inst;
. . .
virtual function void connect();
producer_inst.put_port.connect(consumer_inst.put_export);
endfunction
endclass
The general rule is that when connecting ports to exports, the connect method of the child component port is always
invoked with the child component export as the argument.
In Verilog RTL, modules have ports which represent their signal level interface to the outside world. Verilog RTL
modules also may contain internal structure such as child modules, which themselves may have signal ports. However, it is
the ports that exist on the parent module which represent the interface specification of the parent module—any child
modules and ports of the child modules are considered implementation details that should be kept hidden.
Similarly, in UVM TLM, it is the ports and exports of a given component that represent that component’s TLM interface
to the outside world. Any child components and ports and exports of such child components should be considered
implementation details that should be hidden. Such hiding of internal structure enhances modularity of the overall
verification environment and enables easier reuse and swapping of particular components.
But what if you have a port or an export on a child component within a parent component that needs to be made
visible as a port or an export of a parent component? In this case, you need to connect a child port to a parent port, or a
child export to a parent export.
In the original producer and consumer example above, there is a single process located in the producer, and the
consumer does not contain any process—rather, the put method in the consumer is executed when put is called in the
producer.
In the subsequent producer_2 and consumer_2 example above, there is a process in consumer_2, and consumer_2 has
a get_port that it calls to get each packet.
You might encounter modeling situations where you need to connect components such as the first producer with
components such as consumer_2. Such situations arise frequently because many TLM components require their own
processes for modeling purposes, and yet they still must be connected together to pass transactions.
How can components such as producer and consumer_2 be connected? A very common way to connect them is to use
the uvm_tlm_fifo component within UVM. The UVM uvm_tlm_fifo is a parameterized FIFO that has both put
and get exports. The uvm_tlm_fifo is instantiated with a parameter that indicates the type of objects that are stored in
the fifo, and the constructor for uvm_tlm_fifo has an argument that indicates the maximum depth of the fifo (which
defaults to one).
The put and get ports described so far require that exactly one export be connected to them before simulation starts. If
the ports are left unconnected, you get an error message from UVM that tells you to connect them.
Sometimes when you are constructing components such as monitors, you may need a port that can either be left
unconnected, or connected to one or more components. This is because monitors are typically passive components in the
overall verification environment, and do not directly affect the generation of stimulus or affect any synchronization with
the DUT. Instead, monitors passively collect data transactions and pass them on to any other components which have
registered an interest in the data transactions.
Analysis ports exist in UVM for this type of situation. Analysis ports arc similar to regular TLM ports, but it is okay to
leave them unconnected, and they also allow any number of analysis exports to be connected to them.
For those familiar with callbacks, analysis ports are essentially structured callbacks (callbacks with port connectivity).
An analysis port has a single void function write() that can be called with a single transaction argument. Every
analysis port maintains a list of analysis exports that have been connected to it. When the write method of an analysis port
is called with a transaction, the analysis port calls the write method of every connected analysis export with the same
transaction. Because the write method is a function, it is guaranteed that the analysis port write will return without
blocking. And, because the write method is a void function, the component containing the analysis port does not have any
status returned after the write is done. The overall effect is that from the perspective of the component containing the
analysis port, one does not need to know or care about any downstream components that may be connected to the
analysis port.
Sometimes there are situations where a component needs to have multiple implementations of the same interface. A
classic example of this is a scoreboard that has to monitor multiple interfaces (for instance, two inputs and one output).
When such situations arise, you must provide some means for dealing with the multiple interfaces. There are three
potential solutions:
Create a component for each implementation with that component having responsibility for the specific interface.
If the transaction type is the same for each interface, use a single implementation; this requires that the transaction
object provide some way of disambiguating where the transaction came from.
Create additional _imp types for each port where each _imp type calls to a different implementation function
In UVM, the third option is the simplest because of the `uvm_*_imp_decl macros. These macros are used to
create new implementation types which forward to different implementation functions. For example, if you use
`uvm_analysis_imp_decl(_1), you will get a new implementation class call uvm_analysis_imp_1 #(type T)
which will have an implementation function called write_1().
As part of the verification task and in order to follow the verification plan, a user may need to extend the generic
verification environment behavior beyond its original intent. Unlike design, where specifications can capture the desired
functionality in a complete and deterministic form, the verification process is fluid, dynamic and unpredictable. A reusable
environment developer cannot foresee each and every corner case test for a certain project, or future projects. The global
UVM factory is an advanced implementation of the classic software factory design pattern that is used to create generic
code, deferring to run-time to decide the exact subtype of the object that will be allocated. Consider the following classes
definitions that may be targeted for reuse:
This first example uses a direct allocation using new(). After the example we explain why calling the new() is not
recommended and limits reuse.
1 class driver extends uvm_component
2 `uvm_component_utils(driver)
3 function new(string name, uvm_component parent);
4 super.new(name, parent);
5 endfunction
6 virtual task drive_transfer();
7 ...
8 endtask
9 endclass: driver
10 class agent extends uvm_component; // bad example that uses new()
11 `uvm_component_utils(agent)
12 driver my_driver;
13 function new(string name, uvm_component parent);
14 super.new(name, parent);
15 // create the driver
16 my_driver = new ("my_driver", this) ; // using new()
17 endfunction
18 endclass: agent
For our discussion purposes let’s say that the user wants to integrate an agent that is part of an interface UVC into a
testbench and would like to specialize the drive_transfer() task implementation to print an information message. OOP
recommends the user derive a new driver component, as follows.
19 class my_project_driver extends driver;
20 `uvm_component_utils(my_project_driver)
21 virtual task drive_transfer();
22 super.drive_transfer();
23 `uvm_info("MYIHF01","Finished driving transfer", UVM_LOW)
24 endtask
25 function new(string name, uvm_component parent);
26 . . .
27 endfunction
28 endclass: my_project_driver
This new class extension is not enough to cause the desired effect, since the agent instantiates the previous definition
of the driver and not the extended my_project_driver component. To fix this, the integrator is forced to extend the
definition of the agent class and potentially other classes that instantiate the driver. Since the driver parent components
definition needs to be extended, the interface UVC definition needs to be extended and we experience a ripple effect of
many code modifications all over the testbench. UVM factory introduces an elegant solution that allows overriding the
exact driver from outside the agent class. Instead of allocating using new(), the user uses a create() method, which uses a
central facility that allocates the driver and returns a handle. A more reusable version of the agent that uses the factory is
illustrated below:
This example defines a state component that has florida and new_york sub-types. We demonstrate how the factory is used
to control the allocated state using a create() call, and type overrides.
1 class state extends uvm_component;
2 `uvm_component_utils(state)
3 function new(string name, uvm_component parent);
4 super.new(name, parent);
5 endfunction : new
6 function void end_of_elaboration();
7 this.print();
8 endfunction : end_of_elaboration
9 endclass : state
10 class florida extends state;
11 `uvm_component_utils(florida)
12 function new(string name, uvm_component parent);
13 super.new(name, parent);
14 endfunction : new
15 endclass : florida
16 class new_york extends state;
17 `uvm_component_utils(new_york)
18 function new(string name, uvm_component parent);
19 super.new(name, parent);
20 endfunction : new
21 endclass : new_york
22 state my_state1, my_state2;
23 // Start UVM Phases
24 initial begin
25 my_state1 = state::type_id::create("my_state1", null);
26 `uvm_info("INF01",{“my_state1.type=", my_state1.get_type_name()},UVM_LOW)
27 // set factory to allocate new_york state whenever a state is created
28 factory.set_type_override_by_type(state::get_type(), new_york::get_type());
29 my_state2 = state::type_id::create("my_state2", null);
30 `uvm_info("INF02",{"my_state2.type=",my_state2.get_type_name()},UVM_LOW)
31 #100 $finish;
32 end
Line 1: Definition of a state base class
Lines 10-21: florida and new_york are derived from the state component.
Line 22: Declares two states handles, state1 and state2.
Lines 25-26: Allocating a state using the factory API state::type_id::create(...). Since no overrides were
provided to the factory a state object will be allocated and printed in line 30.
Line 28: The factory is used to return the new_york sub-type upon a request to create a state.
Line 29: Create the state using the factory.
Line 30: Now the printed component name is new_york.
And the resulting printout is:
UVM_INFO ./test.sv(51) @ 0: reporter [INFO1] my_state1.type=state
UVM_INFO ./test.sv(54) @ 0: reporter [INFO2] my_state2.type=new_york
Note We cannot stress enough how important it is to use the factory to create both components and objects. For some
users, the factory may be a new concept, but using the factory is simple, and the implementation is built in within the
UVM library.
While developing or using environments, users will need to print information or issue error messages. A user may want
to get trace messages from a suspect component or filter out undesired errors. Verilog’s $display does not allow
nonintrusive filtering and control of messages. Changing the verbosity on the command line or via Tcl run-time commands
does NOT require that you recompile and re-elaborate the design to observe different trace messages. UVM reporting
services are built into all components and are derived from a patent uvm_report_object class. The services can also
be used in SystemVerilog modules, interfaces and programs. The mechanism provides a simple solution for most
requirements and the ability to customize them with flags, parameters and user-defined hooks.
Note When using the message macros, verbosity is ignored for warnings, errors and fatal errors. This prevents verbosity
modifications from hiding bugs. The verbosity parameter is retained in the routines for backward compatibility purposes. If
a warning, error, or fatal error needs to be turned off, this can be done by setting the action to UVM_NOACTION. For
example, to turn off a specific message in component mycomp that is an UVM_ERROR with id MYTAG, you might do the
following in a test or testbench:
mycomp.set_report_severity_id_action(UVM_ERROR, "MYTAG", UVM_NO_ACTION);
The run-time differences between using the macro API and the routines has proven to be significant in many
testbenches. Use the macros API whenever you can.
To enable easy message control of a testbench that contains multiple components, use only the enumerated values.
Also use this standard interpretation of verbosity levels:
UVM_NONE—For non-maskable and critical message
UVM_LOW—For maskable messages that are printed only rarely during simulation (for example, when is reset done)
UVM_MEDIUM—Short messages that happen once per data item or sequence
UVM_HIGH—More detailed per-data-item information, including printing the actual value of the packet and printing
sub-transaction details
UVM_FULL— Anything else, including message prints in specific methods (just to follow the algorithm of that
method)
UVM_DEBUG— Debug mode with internal developer debug messages
For information messages we recommend using the get_type_name () as the id argument. It is easy to see how many
messages came from the different components during a simulation run, and it can be a quick sanity check that all
components operated properly. This usage also allows filtering by type.
Sometimes users need to ignore warnings for a particular project needs or a certain test requirements. For
malfunction-related reports (warnings, errors, and fatals), use a unique name as the id argument. This allows fine
control of ignoring errors.
Note Changing the verbosity on the command line or by way of Tcl run-time commands does not require that you
recompile and re-elaborate the design to see different trace message recording.
You can use the same message macros in the context of non class-based code too. The following example illustrates this
in the context of an initial block inside a module:
4.9 Callbacks
Callbacks, as with the factory, are a way to effect or monitor an environment from the outside. Callbacks are different
from the factory in that an object developer must insert calls to specific callback methods/tasks inside of this class at
points where he deems it appropriate for the user to take action. The callback users can then create their derived callback
class and attach to one or more desired objects.
The use model of callbacks is broken into two parts, the developer piece and the user piece.
4.9.1.1 Developer
The first thing the developer must do is decide on an interface to make available. Often the callback function will include
a reference to the object that is issuing the callback. And, it will include other information that is necessary for the callback
User
Using our example from “Hierarchy Information Functions” on page 42, we may want to add a callback to the city class
which gets executed before it prints its parent/child information. To do this we would do something like:
1 typedef class city;
2 virtual class city_cbs extends uvm_callback;
3 function new(string name="city_cb");
4 super.new(name);
5 endfunction
6 pure virtual function void pre_info (city c);
7 endclass
Next, the developer must register their callback type with the type which will use the callback. This registration enables
UVM to do type checking when a user tries to add a callback to a specific object. If the registration is left out, the user will
get a warning from UVM that the callback type was not registered with the object type they are attempting to add it to.
1 class city extends uvm_component ;
2 `uvn_register_cb(city, city_cbs)
3 ...
4 endclass
Note If the type using the callback is derived from a class which also used callbacks, you must use the
`uvm_set_super type macro so that UVM is aware of the class hierarchy and can properly manage the callbacks.
Finally, the developer inserts a call to the callback functions in their code. A utility macro, `uvm_do_callbacks, is
provided to simplify the process. A developer may choose to manually iterate the callbacks using the
uvm_callback_i t e m class as well; this provides ultimate flexibility as to how the callbacks are executed.
1 task run();
2 uvm_component child, parent;
3 string pr, ch;
4 child = get_child("Main_St") ;
5 parent = get_parent();
6 pr = parent.get_full_name();
7 ch = child.get_name();
8 `uvm_do_callbacks(city, city_cbs, pre_info(this))
9 `uvm_info(get_type_name (), $psprintf("Parent:%s Child:%s", pr, ch),
UVM_LOW)
10 endtask : run
For convenience, the developer should also create a callback typedef.
typedef uvm_callbacks#(city, city_cbs) city_cb;
4.9.1.2 User
Now that a callback class has been defined and used within an object, a user may use the callback as necessary. For the
end user, the first thing to do is to extend the callback class with the desired behavior.
1 class my_city_cb extends city_cbs;
2 function new(string name="my_city_cb");
3 super. new (name);
4 endfunction
5 virtual function void pre_info(city c);
6 `uvm_info("my_city_cb", $psprintf("pre_info called for city %s",
c.get_full_name()), UVM_LOW)
7 Endfunction
8 endclass
Next, the user attaches the callback to a specific instance or to all instances of the type. A null handle specifies a
type-wide callback.
1 my_city_cb cb = new;
2 initial begin
3 city_cb::add(null, cb) ;
4 run_test();
5 end
Running this code with the previous example results in the following:
UVM_INFO @ 0: reporter [my_city_cb] pre_info called for city New_York.capital_city
UVM_INFO @ 0: New_York.capital_city [city] Parent:New_York Child:Main_St
UVM_INFO @ 0: New_York.capital_city.Main_St [INFO1] I vacationed here in 2010
UVM_INFO @ 0: reporter [my_city_cb] pre_info called for city Florida.capital_city
UVM_INFO @ 0: Florida.capital_city [city] Parent:Florida Child:Main_St
UVM_INFO @ 0: Florida.capital_city,Main_St [INFO1] I vacationed here in 2010
Create an abstract (virtual) class for the callbacks with pure virtual methods for the interface.
Pass a reference of the object making the callback call.
Take care when passing references to data objects, as they can be changed (this is okay as long as that is your intent).
Provide a typedef for the uvm_callbacks# (T, CB) class with the name T_cb.
Use the `uvm_set_super_type macro if a super class utilizes callbacks.
Create a callback implementation class which implements all of the pure virtual methods in the callback class.
Allocate a new callback object for each callback you add,
Use type-wide callbacks if you want your callback to effect all types and instance specific callbacks if you only want to
affect specific instances of an object.
Note Instance-specific callbacks cause the instance handle of the object to be stored in a queue until all objects are
removed. Care must be taken when associating instance specific callbacks with transient data objects because those data
objects cannot be garbage collected until all references are removed. For callbacks to transient objects, prefer using
type-wide callbacks if they meet your needs.
UVM has a handful of built-in tailbacks. One such callback is the report catcher. The report catcher callback is called
just before a message is to be issued. In this callback, it is possible to catch a message so that it is not processed further;
modify the message type (demote errors, and so forth), or modify the message text.
Below is an example of how a report catcher can be used to demote a specific type of error message to an info
message. This may be done in a test that is specifically attempting to create an error condition.
1 module test;
2 import uvm_pkg::*;
3 `include "uvm_macros.svh"
4 class my_catcher extends uvm_report_catcher;
5 virtual function action_e catch();
6 if(get_severity() == UVM ERROR && get_id() == "MYID") begin
7 set_severity(UVM_INFO);
8 end
9 return THROW;
10 endfunction
11 endclass
12 my_catcher catcher = new;
13 initial begin
14 uvm_report_cb: : add (null, catcher ) ;
15 `uvm_error("MYID”, "This one should be demoted")
16 catcher.callback_mode(0); //disable the catcher
17 `um_error ("MYID", "This one should not be demoted")
18 end
19 endmodule
Summary
The chapter introduces key feature of the UVM library. While the UVM features are capable and flexible, they were
created as enablers to the higher-level methodology that is needed to speed up verification for large and small designs.
The next chapters introduce the UVM methodology for developing reusable verification components for design interface
protocols.
Interface UVCs
Interface UVCs are key building blocks of a verification testbench. Extra effort and design at the interface UVC level
simplifies the integration and test writing and enables large-scale reuse. Many library capabilities, introduced in the
previous chapter, were designed to enable the creation of interface UVCs. This chapter describes the basic concepts and
steps needed to create a standard reusable interface UVC.
The first step in developing a verification component is describing the data item class. Data items are high-level
transactions that will eventually be used to control how a particular interface on the DUT is driven. When generating this
stimulus, realistic values that follow the protocol are generated and sent. Examples of data items are transactions, packets,
and instructions. You may want to generate a stream of stimulus up front and apply the transactions one by one, or
generate transactions whenever they are needed. Many times you want to generate sequences of transactions that relate
to each other.
To create a user-defined data item, first you will want to review the interfaces transaction specification and identify the
data fields for that transaction. Some transaction attributes are derived from the protocol specifications and are required
to specify how the transaction is represented on the bus. Other attributes are there to enable interesting data and easy
control of the data randomization. Users can control how the value of those fields are set via constraints, control fields,
and methods associated with the transaction. The UVM SystemVerilog class library provides the uvm_sequence_item base
class for describing data items. Every user-defined data item should be derived directly or indirectly from this base class.
The UVM SystemVerilog class library also includes macros that automatically implement the print, copy, clone, compare,
pack/unpack methods, and more. The example below illustrates an APB transfer.
Note The transfer is derived from uvm_sequence_item and not from uvm_object, as demonstrated in the
previous chapter. Deriving the data items from a uvm_sequence_item allows them to be randomized as part of a
sequence where the order to the data items makes a difference.
Notes
Control knobs that are added for generation and coverage purposes should not be compared, packed or unpacked
since they are not sent to the DUT.
We do not discuss here data-item attributes that are added for coverage purpose only, such as adding a start_time
and an end_time to cover a transfer processing time. These attributes are best captured by the monitor to enable
passive operation mode and are discussed in the coverage section below.
for a specific test, you might want to adjust the randomness, disable existing constraints or further change the
generation using additional constraints. This is achieved using class inheritance. You create a new class which extends from
the original class. All the original fields, methods and constraints are available in the extended class. You override an
existing constraint by adding a constraint with the same constraint name. Or you can add a constraint with a different
name to specify additional constraints:
class short_delay_transfer extends apb_transfer;
// further constrain the transmit_delay
constraint c_short_delay {transmit_delay < = 3 ; }
endclass : short_delay_transfer
Note In the code snippet above, the constructor and UVM automation macros have been omitted for brevity. In future
examples, the constructor and macros will be shown only when needed. Please remember to include a constructor and
UVM automation macros for all components and data items derived from uvm_object.
In this example, the constraint block in the class short_delay_transfer will be solved together with the constraint blocks
from the class apb_transfer during randomization, and the value of transmit_delay will be constrained between 0 and 3. In
a larger environment, you will often want to replace apb_transfers with short delay transfers. The UVM factory greatly
simplifies the replacement of a type with a derived type such as short_delay_transfer. This is one of the major reasons for
using the factory. Such constraint layering requires usage of the UVM factory.
To enable this type of extensibility:
Make sure constraint blocks are organized so that users are able to override or disable constraints for a random
variable without having to rewrite a large constraint block.
Note Many users find that writing a larger number of short constraint blocks is easier than a small number of long
constraint blocks.
Do not use the protected or local keywords to restrict access to properties that may be constrained by the user. This
will limit your ability to constrain them with an inline constraint.
Use descriptive names for constraints. This will make it easier for users not familiar with the original definition to
understand and override constraints in derived types.
A user can leave in a class an empty constraint block that later can be filled with constraints by the test writer. This
technique is not System Verilog-compliant, but is supported by all of the simulators. Using an empty constraint block is an
ad hoc layering technique. As well as not being part of the language reference manual (LRM), it has the limitations of not
supporting instance overrides; not supporting multiple extensions, as no new class is created; and no ability to turn off or
override existing constraint blocks. Further, you can only compile one set of constraints at a time, so it makes it difficult to
manage for all but the smallest projects. We recommend using the factory solution over external implementation of
constraint blocks. For more information on the factory, see “UVM Factory" on page 55.
When you have captured your data item with its control knobs and constraints, you can write a simple test to create,
randomize and print a few items to verify the results you expect are produced.
7 apb_transfer my_xfer;
8 initial begin
9 my_xfer = apb_transfer::type_id::create("my_xfer");
10 repeat (5) begin
11 if (!my_xfer.randomize())
12 `uvm_fatal ("RANDFAIL", "Can not randomize my_xfer")
13 my_xfer.print();
14 end
15 endmodule : test
Notes
To run this test on the Cadence IES simulator, first set UVM_HOME to the location of your UVM library installation,
then run the following command:
% irun $UVM_HOME/src/uvm_pkg.sv -incdir $UVM_HOME/src test.sv
This simple test will also verify that you have access to the UVM library and macros.
If you forgot to specify a field with the uvm_field_* macros, it will not be printed.
Check your constraints by randomizing with additional constraints:
if(!my_xfer.randomize() with { addr inside {{‘hOOOO:’hFFFF]);
direction == APB_WRITE; })
... additional code ...
As well as looking at the generated values, try to look at the distribution of the values. Some constraint sets can
create an undesired bias to the generated values. It is useful to define a transaction coverage group and use it for such
analysis. Randomize and cover the transaction multiple times in a loop and review the generation result. Do you have
too many reads vs. write transfers? Is the length delay between transactions reasonable? Are the generate
transaction kinds evenly distributed? Fix the constraints needed to achieve the desired results.
When data items are generated, the drivers role is to drive them to the bus following the interface protocol The driver
obtains data items from the sequencer for execution. Once the driver is finished sending the data item, it may return a
value back to the sequencer. For example, the sequencer can randomize a read bus transaction and send it to the driver for
delivery to the DUT. The driver performs the read operation on the bus and returns the result value back to the sequencer.
The SystemVerilog UVM Class Library provides the uvm_driver base class, from which all driver classes should be
extended. The uvm_driver class is parameterized by both the request type (the data that is randomized by the
sequencer) and the response (the DUT result that goes back to the sequencer). Built-in TLM ports are provided to
communicate with the sequencer.
To create a driver:
Derive a driver class from the uvm_driver base class. It is parameterized with the request and response data item
type(s), so ensure that your driver class provides types for these parameters.
Declare a virtual interface in the driver to connect the driver to the DUT interface.
Add the UVM infrastructure and automation macros for class properties to provide implementations for print(), copy(),
compare(), and so on.
In the run() task, get the next data item from the sequencer and drive it on the virtual interface.
Refer to “Configuring the Sequencer’s Default Sequence” on page 94 for a description of how a sequencer, driver, and
sequences synchronize with each other to generate constrained random data.
The example below defines an AMBA advanced peripheral bus (APB) master driver. The example derives
apb_master_driver from uvm_driver (parameterized to use the apb_transfer transaction type) and uses the methods in
the seq_item_port object to communicate with the sequencer. As always, include a constructor and the
`uvm_component_utils macro to register the driver type with the common factory.
16 task apb_master_driver::run();
17 fork
18 get_and_drive() ;
19 reset_signals() ;
20 join
21 endtask : run
22 task apb_master_driver::get_and_drive();
23 forever begin
24 // Get the next data item from sequencer (may block).
25 seq_item_port.get_next_item(req);
26 drive_transfer(req);// Execute the item
27 seq_item_port.item_done(req); // Return the request.
28 end
29 endtask : get_and_drive
Notes
By default, the response field accepts the same type as the request field. So this driver accepts apb_transfer types
and returns apb_transfers back to the sequencer. For a different request and response type, use:
class apb_driver extends uvm_driver #(apb_transfer_req, apb_transfer_res);
Your driver class can optionally provide a second parameter for the type of response sent back to the sequencer. If
you chose not to provide a second parameter, the response type will be the same as the request type. In this example,
we are providing only a single parameter for the request type.
Line 5: Add the UVM component utilities macro.
Line 7: The constructor for uvm_components has two arguments: a string name and a reference to the component’s
parent.
Note In the data item section, we had several fields registered with macros. It is common for components to have a small
number of fields. And in many instances, there are no fields registered at all.
Line 25: Call get_next_item() to get the next data item for execution from the sequencer.
Line 27: Signal the sequencer that the execution of the current data item is done.
Line 31: Add your application-specific logic to the drive_transfer() method to execute the data item. When first
developing the UVC, you may want to only print the transaction until you have integrated a SystemVerilog interface.
More flexibility exists on connecting the drivers and the sequencer. See “Connecting the Driver and Sequencer” on
page 73.
We recommend that you use extern virtual protected tasks in the driver; this allows a concise view of the drivers API.
We also recommend that you place the external implementation of these in the same file. This allows a quick review
of the class interfaces without introducing too many files.
A driver should not randomize parameters and, as much as possible, the traffic control should be reserved for the
sequencer. This allows reusing the test logic, even if the driver does not exist (for example, when verifying the TLM
model). Timing and injection-related parameters, such as delay between data items or timing-related error injection,
should be randomized by the sequencer and delivered to the driver as part of the request.
The driver interacts with the DUT using a virtual interface. The SystemVerilog interface construct is used to bundle the
DUT signals that will be connected to the UVM testbench. The interface definition is part of the reusable UVC.
SystemVerilog interfaces are defined outside a module boundary, are instantiated in a module, and the interface signals
are references via a “operator (interface. sig_name). In addition to signals, the interface can contain assertions, parameters,
functions, tasks, module ports(modports) and clocking blocks. The example code below shows a simple interface
declaration for an APB bus.
A sequencer is an advanced generator that returns data items to the driver upon request. To understand the sequencer
and its implementation, let’s walk through some typical randomization requirements and implement a simple non-UVM
generator.
Consistent stimuli control—In a typical testbench, the integrator needs to assemble reusable verification IP created by
multiple individuals and for quite different protocols. After initial configuration of UVCs, the main verification activities
are creating and debugging tests. It is important that the stimuli control of the different reusable environments will be as
consistent as possible. Specifically, the solution should have a proven solution for various scenarios such as: interrupt
modeling, looking at previous generated data, protocol layering, latency and timing control, and much more.
Infinity minus—Randomness has proven as an efficient tool for uncovering unanticipated bugs. As you implement your
testbench it is a good practice to use all random values. The infinity minus flow means that a wide variety of values are
randomized by default (infinity). Individual tests use constraints to trim the possible legal values to test-specific values. The
opposite for infinity-minus is to start with directed tests or a structured stimuli and finish with randomness at the end.
Reactive generation—Data items should be generated as close as possible to when they are needed. This permits
techniques like reactive generation where the contents of the data items can be influenced by the state of the DUT. In
some cases, you randomize the traffic regardless of the DUT state but may want to coordinate the traffic to a simulation
event in a specific test.
Pre-generation—In some environments, the stimuli must be created before the run phase. A classic example can be a
program that needs to be created in advance and placed in memory. An ideal verification engine should support both
reactive and pre-generation.
Ordered and individual transaction generation—In some cases, generation of individual data items is enough to create
all legal stimuli or to fill coverage holes. In others, a specific sequence of data items is needed. For example, generating
random register accesses is unlikely to properly initialize your DUT. In many environments, both ordered and semi-ordered
transaction randomization is needed. An optimal solution will allow randomizing both individual data items and sequences
of data items.
Enable horizontal and vertical reuse—Many of the transactions generated can be leveraged for different devices or as
you integrate a device into a larger system. It is helpful to create a list of reusable data item sequences that have random
parameters which can be further constrained for particular project needs.
The example below illustrates a generator and a driver. The driver requests an item from the generator by calling a
function directly in the generator. The generator “wakes up” and creates a new item when required. When the generator
hits a maximum number of items, it stops creating new items.
The sequencer generates stimulus data and passes it to a driver for execution. In typical operation, the sequencer waits
for a get_next_item() call from a driver, randomizes the data item, and then sends the data item to the driver. The
SystemVerilog UVM Class Library provides the uvm_sequencer base class, which, like the uvm_driver class, is
parameterized by the req and resp item types. The uvm_sequencer is much more capable than the simple loop
sequencer described above. Deriving your sequencer from uvm_sequencer allows you to leverage these built-in
capabilities,
To create a sequencer:
Derive a sequencer from the uvm_sequencer base class and specify the req and resp (request and response) type
parameters.
Use`uvm_sequencer_utils and `uvm_update_sequence_lib_and_item to indicate the generated
data item type and field desired automation.
This is all that is required to define the baseline behavior for a sequencer. The rest of the sequencers behavior is built in,
and we explore the details later. The default behavior of the sequencer is to be a random stimulus generator. Refer to
“Configuring the Sequencer’s Default Sequence” on page 94 for a description of how a sequencer, driver, and sequences
synchronize with each other to generate constrained-random data.
The class apb_master_sequencer in Example 5-7 on page 72 defines a sequencer class. The example derives it from
uvm_sequencer and parameterizes it to use the apb_transfer type.
Notes
In the class definition, by default, the response type is the same as the request type. If a different response type is
desired, the optional second parameter must be specified for the uvm_sequencer base type:
class apb_master_sequencer extends uvm_sequencer #(apb_transfer, apb_rsp);
Use the `uvm_sequencer_utils macro instead of the `uvm_component_utils macro. The
`uvm_sequencer_utils macro provides additional functionality for sequences. When using the
uvm_seqeuencer_utils macro, you should still use the normal UVM field registration macros
( u v m _ f i e l d _ i n t , and so on). Refer to “UVM Macros” in the UVM Class Reference for more information.
Call the `uvm_update_sequence_lib_and_item macro from the constructor of your sequencer class. This
macro registers all of the sequence types associated with the current sequencer, and indicates the sequencer’s
generated transaction type as a parameter. Refer to “UVM Macros” in the UVM Class Reference for more information.
Never instantiate the uvm_sequencer class directly. A user may need to extend this class to add their own
attributes. For example, the user may decide to raise an end-of-test object from all AHB masters or add a channel
number to all the APB slaves.
To better understand the sequencer and the recommended way to generate stimuli in UVM, we review the sequences
and their capabilities in Chapter 8 “Stimulus Generation Topics”.
The driver and the sequencer are connected via TLM, with the driver’s seq_item_port connected to the
sequencers seq_item_export (see Figure 5-2, below). The sequencer produces data items to provide via the export.
The driver consumes data items through its seq_item_port, and optionally provides responses back. The component
that contains the instances of the driver and sequencer makes the connection between them. See Figure 5-2,
Sequencer-Driver Interaction, below :
The seq_item_port in uvm_driver defines the set of methods used by the driver to obtain the next item in the
sequence. An important part of this interaction is the driver’s ability to synchronize to the bus, and to interact with the
sequencer to generate data items at the appropriate time. The sequencer implements the set of methods that allows
flexible and modular interaction between the driver and the sequencer.
Sequencer
Produces data
sequences
uvm_seq_item_pull_export seq_item_export
uvm_seq_item_pull_port seq_item_port
Driver
Consumes and
sends data to the
DUT
vif
Basic handshake between the driver and the sequencer is done using the tasks get_next_item() and
item_done(), As demonstrated in the example in “Creating the Driver” on page 67, the driver uses the task to request
a randomized item from the sequencer. The driver will block waiting for the sequencer to have an item available. When the
sequencer has an item available, it will provide it and the get_next_item() task will return this item. After sending it
to the DUT, the driver signals the sequencer that the item was processed using item_done(). Typically, the main loop
within a driver resembles the following pseudo-code.
seq_item_port.get_next_item(req) ;
// Send item following the protocol.
send_to_dut(req);
seq_item_port.item_done();
In some protocols, the driver can simply block and do nothing until an item is available to be sent. The
get_next_item() is blocking and will serve this need. In some protocols, the driver may be required to send idle
transactions while waiting for a meaningful one. For this purpose, the uvm_seq_item_pull_port class provides another task:
try_next_item(). This task will return immediately, even if no data items are available for execution. Your driver
code can use this task to test if there are valid items ready to be sent. If there are no items available, your driver can send
idle transactions. The following example shows a revised implementation of the get_and_drive() task in the previous
example (in “Creating the Driver” on page 67)—this time using try_next_item () to drive idle transactions as long, as
there is no real data item to execute:
1 task apb_master_driver::get_and_drive();
2 forever begin
3 // Try the next data item from sequencer (does not block).
4 seq_item_port.try_next_item(transfer) ;
5 if (transfer == null) begin
6 // No data item to execute, send a simple idle transaction.
7 ...
8 end
9 else begin
10 // Got a valid item from the sequencer, execute it.
11 ...
12 // Signal the sequencer; we are done.
13 seq_item_port.item_done () ;
14 end
15 end
16 endtask: get_and_drive
In some protocols, such as pipelined protocols, the driver begins multiple data items before the earlier items are
completely processed. In these cases, the driver calls item_done() without providing the response to the sequencer. In
this scenario, the driver logic may look like the code example below.
1 class simple_driver extends uvm_driver #(simple_transfer) ;
2 simple_transfer req_list [$] ;
3 int max_pipeline_depth=4 ;
4 // Component utils, constructor
5 . . .
6 task run();
7 fork
8 get_and_dxive() ;
9 drive_transfers() ;
10 join
11 endtask : run
12 // Fetch items from the sequencer to fill the pipeline
13 task get_and_drive();
14 while (req_list.size() < max_pipeline_depth) begin
15 seq_item_port.get_next_item(req);
16 $cast(rsp, req.clone());
17 rsp.set_id_info(req);
18 req_list.push_front(rsp) ;
19 seq_item_port.item_done();
20 end
21 endtask : get_and_drive
22 //Process items in req_list and execute according to the protocol
23 task drive_transfers();
24 simple_transfer cur_req;
25 while (1) begin
26 wait (req_list.size ()>0);
27 cur_req = req_list.pop_back() ;
28 send_to_dut(cur_req);
29 rsp_port.write(cur_req);
30 end
31 endtask : drive_transfers
32 task send_to_dut(simple_transfer trans);
33 // Logic to handle sending multiple data items
34 endtask : send_to_dut
35 endclass : simple_driver
Note The send_to_dut() task may require forked processes to execute multiple items in parallel to fill the pipeline.
In some sequences, a generated value depends on the response to previously generated data. By default, the data
items between the driver and the sequencer are copied by reference, which means that changes made to the driver data
item will be visible inside the sequencer. At times, a reference between the sequencer and the driver cannot be
maintained. In such scenarios—for example, across different languages or between simulated and accelerated
components—the user needs to return the processed response back to the sequencer. Do this using the optional
argument to item_done().
1 task apb_master_driver::get_and_drive();
2 forever begin
3 seq_item_port.get_next_item(req);
4 $cast(rsp, req.clone());
5 rsp.set_id_info(req);
6 drive_transfer(rsp);
7 seq_item_port.item_done(rsp);
8 end
9 endtask : get_and_drive
Note Before providing the response, the responses sequence and transaction id must be set to correspond to the
In Chapter 2 “UVM Overview”, we discuss a split-monitor approach to passively capture transactions on an interface
and do checking and coverage. Here, we will use this alternate approach, using a collector and monitor component.
The collector is responsible for extracting signal information from the bus and translating it into transactions. This
component essentially reverse engineers transactions sent on a SystemVerilog interface. We recommend using a
SystemVerilog interface with SystemVerilog assertions for protocol-specific checking, but you can also add signal-level
checking and coverage to the collector when appropriate. The monitor is responsible for doing transaction-level checking
and coverage. Once the monitor has decoded the transaction, it sends the transaction to other components (that is, UVCs,
scoreboards, or TLM models) via standard TLM connections. The monitor and collector should never rely on state
information collected by other components, such as a driver. Checking and coverage should be configurable and
configuration control information should be included in both components. An analysis port allows communication
between the collector and monitor components.
The following example shows a simple collector with the following functions:
The collector captures bus information through a virtual interface (vif).
The collected data is exported to the monitor on a TLM analysis port (item_collected_port).
Detailed code for the collector is not shown in this simplified example. A complete example can be found in the APB
example in apb_collector.sv.
1 class apb_collector extends uvm_component;
2 virtual apb_if vif; // SystemVerilog virtual interface
3 bit checks_enable = 1; // Controls checking in collector and interface
4 bit coverage_enable =1; // Controls coverage in collector and interface
5 // TLM port for sending the collected transfer to the monitor
6 uvm_analysis_port #(apb_transfer) item_collected_port;
7 // UVM automation and utility macros
8 function new (string name, uvm_component parent) ;
9 super.new(name, parent);
10 item_collected_port = new("item_collected_port", this);
11 endfunction : new
12 extern virtual task run();
13 extern virtual function void perform_coverage() ;
14 extern virtual function void perform_checks();
15 endclass : apb_collector
16 task apb_collector::run();
17 apb_transfer trans_collected:
18 trans_collected = new("trans_collected") ;
19 forever begin
20 @(posedge vif.sig_clock);
21 ...// Capture signal data from the bus into trans_collected.
22 if (checks_enable) perform_checks();
23 if (coverage_enable) perform_coverage();
24 item_collected_port.write(trans_collected);
25 end
26 endtask : run
27 ...
The run() task executes a forever loop and collects the data as soon as the signals indicate that the data is available
on the bus.
As soon as the data is available, it is sent via the analysis port (item_collected_port) to the monitor. Example
5-10 APB Monitor
The following example shows a simple monitor which has the following functions:
The monitor has a coverage group that captures interesting functional coverage information.
The transaction is exported via a TLM analysis port (item_collected_port).
1 class apb_monitor extends uvm_monitor;
2 // flags used to control whether checks and coverage are executed.
3 bit checks_enable = 1;
4 bit coverage_enable = 1;
5 // TLM port for sending transactions OUT to the scoreboard, reg db, etc
6 uvm_analysis_port #(apb_transfer) item_collected_port;
7 // TLM port connection to the collector
8 uvm_analysis_imp #(apb_transfer, apb_monitor) coll_mon_port;
9 // Current APB transfer
10 apb_transfer trans_collected;
11 // UVM automation and utility macros
12 // Functional coverage groups declarations
13 covergroup apb_transfer_cg;
14 TRANS_ADDR: coverpoint trans_collected.addr {
15 bins ZERO = {0};
16 bins NON_ZERO = {[1:8’h7f] } ; }
17 TRANS_DIRECTION : coverpoint trans_collecred.direction;
18 TRANS_ADDR_X_TRANS_DIRECTION : cross TRANS_ADDR, TRANS_DIRECTION;
19 endgroup
20 // Constructor - UVM Required syntax
21 function new (string name, uvm_component parent);
22 super.new(name, parent);
23 apb_transfer_cg = new();
24 item_collected_port = new("item_collected_port", this);
25 coll_mon_port = new("coll_mon_port", this);
26 endfunction : new
27 // Additional class methods
28 extern virtual function void write(apb_transfer transfer);
29 extern virtual function void perform_checks();
30 extern virtual function void perform_coverage();
31 endclass : apb_monitor
32 // Transaction interface to the collector via TLM write()
33 function void apb_monitor::write(apb_transfer transfer);
34 trans_collected = transfer;
35 if (checks_enable) perform_checks();
36 if (coverage_enable) perform_coverage();
37 // Broadcast transaction to the rest of the environment
38 item_collected_port.write(trans_collected);
39 endfunction : write
40 function void apb_monitor::perform_checks();
41 // Add checks here
42 endfunction : perform_checks
43 function void apb_monitor::perform_coverage();
44 apb_transfer_cg.sample(); // Sample monitor covergroups
45 endfunction : perform_coverage
46 endclass : apb_monitor
Coverage collection and checking are conditional because they can affect simulation run-time performance. If not
needed, they can be turned off by setting coverage_enable or checks_enable to 0, using the configuration mechanism. For
example:
set_config_int(“collector", "checks_enable", 0);
set_config_int("*.monitor", "checks_enable", 0);
If checking is enabled, the write task calls the perform_checks function, which performs the necessary checks on the
collected data (trans_collected). If coverage collection is enabled, the write task calls the sample() method of the
transaction coverage groups.
Section 5.12, “Implementing Protocol-Specific Coverage and Checks,” on page 102 provides more details and examples
of creating and controlling checks and coverage in an interface UVC.
The collector and the monitor are connected via TLM, with the collectors uvm_analysis_port connected to the
monitors uvm_analysis_imp (see Figure 5-3, below). The collector forms transactions from signal activity on the
interface and delivers the transactions via the analysis port to the monitor.
Transactions are made
available to other
testbench components
uvm_analysis_port
uvm_analysis_imp
uvm_analysis_port
Testbench Sequencer
Produces data
Monitor sequences
checking
coverage seq_item_export
seq_item_port
Driver
Collecotor Consumes and
sends data to the
DUT
vif vif
apb if
DUT
Testbench
Agent
Config Sequencer
Produces data
Monitor sequences
checking
coverage seq_item_export
seq_item_port
Driver
Collecotor Consumes and
sends data to the
DUT
vif vif
apb if
DUT
The agent (Figure 5-6 on page 80) instantiates and connects together a driver, collector, monitor, and sequencer using
TLM connections as described in the preceding sections. To provide greater flexibility, the agent also contains configuration
information and other parameters. A UVM-compliant agent will provide protocol-specific stimuli creation, checking, and
coverage for a device. In a bus-based environment, an agent typically models either a master or a slave component. An
agent has two basic operating modes:
Passive mode—The agent does not instantiate a driver or sequencer and operates passively. Only the collector and
monitor are instantiated and configured. Use this mode when only checking and coverage collection is desired.
Agent
Sequencer
Is_active=ACTIVE Sequencer and driver are
... not created in
sequences passive mode
tlm port
Monitor
Produces data
Collecotor Driver
Consumes and
sends data to
the DUT
vif vif
interface
Note Even if you do not have immediate plans for using passive mode, it is often easy to add this support during
agent implementation. Retrofitting it is much more difficult later in the project when coding has finished.
The class apb_master_agent in the example below instantiates a sequencer, a driver, collector, and a monitor in the
recommended way. Instead of using the constructor, the UVM build() method is used to configure and construct the
subcomponents of the agent. Unlike constructors, this function can be overridden without any limitations. Also, instead of
hard-coding the allocation, type_id::create() is used to instantiate the sub-components. The second part of
Example 4-14 on page 55 illustrates how you can override existing behavior using extends.
Note You should always call super.build() (see Line 23) to update the given components configuration overrides. This
is crucial to providing the capability for an enclosing component to be able to override settings of an instance of this
component.
Lines 26-29: The if condition tests the is_active property to determine whether the driver and sequencer are created in
this agent. If the agent is set to active (is_active == UVM_ACTIVE), the driver and sequencer are created using additional
create() calls.
Both the sequencer and the driver follow the same creation pattern as the monitor.
This example shows the is_active flag as a configuration property for the agent. You can define any control flags that
determine the components topology.
Calling create() from the build() method is the recommended way to create any hierarchical component.
Line 34: The connection between the monitor and collector is made using connect().
Lines 35-36: The if condition should be checked to see if the agent is active, and if so, the connection between the
sequencer and driver is made using connect().
The agent above uses a connect() phase to connect the driver to the sequencer and the monitor to the collector. Using
the phases ensure that the components creation that took place in the build() phase is done. UVM simulation phases are
further explained in Chapter 4 “UVM Library Basics".
The agent sub-components can share the same configuration options. Some agent configuration attributes are
recommended by UVM (that is, is_active) while others are user-defined (for example, bus speed or a sub-protocol such as
GMII versus SGMII Ethernet). The UVM configuration mechanism allows users to define configuration attributes and
provides a mechanism to change them in a testbench or test without modifying individual components. The details of the
configuration mechanism are discussed later in this chapter.
In multi-drop bus interfaces such as APB, AHB, or a multi-layer verification component such as PCI-Express, multiple
agents need to share common resources and settings and need to instantiate and connect in an environment-specific way.
Examples could be common configuration options, similar virtual interface, joint sparse memory, and more.
A UVC is a reusable environment for a specific protocol. The idea here is similar to agents but involves a complete
interface encapsulation level—we ask the UVC developer to instantiate and configure the multiple agents and other
resources together as needed. In addition to agents, the verification component may contain a bus-level monitor and/or
arbiter. Figure 5-7, Typical UVM Interface UVC Architecture, below, shows an example of a typical UVM bus-based
verification component. The UVC users receive this highly encapsulated environment, configures and instantiates it using
the configuration mechanism. The following sections describe how to create and connect environment sub-components.
By following these guidelines, users can ensure that their environment will be architecturally correct, consistent with other
UVCs, and reusable.
UVC Environment (uvm_env)
uvm_agent uvm_agent
Config uvm_agent uvm_agent
name
has... Config Config
uvm_sequencer uvm_sequencer
uvm_monitor sequences uvm_monitor sequences
uvm_monitor
checks
uvm_driver uvm_driver
coverage uvm_collector uvm_driver uvm_collector uvm_driver
DUT
The environment class is the top container of reusable UVCs. It instantiates and configures all of its sub-components
(agents, bus monitors, and so on). Most verification reuse occurs at the environment level, where the user instantiates an
environment class and configures it and its agents for specific verification tasks. For example, a user might want to change
the number of slaves in an APB bus environment as shown below.
1 class apb_env extends uvm_env;
2 apb_config cfg; // configuration class for this environment
3 apb_master_agent master, // APB master (Bridge)
4 apb_slave_agent slaves[]; // APB can have multiple slave agents
5 apb_bus_monitor bus_monitor; // Shared bus monitor
6 `uvm_component_utils_begin(apb_env)
7 `uvm_field_object(cfg, UVM_DEFAULT)
8 `uvm_component_utils_end
9 function new(string name, uvm_component parent);
10 super.new (name, parent) ;
11 endfunction : new
12 // Additional class methods
13 extern virtual function void build();
14 extern virtual function void assign_vi (virtual apb_if vif);
15 endclass : apb_env
16 function void apb_env::build
17 uvm_object config_obj;
18 super.build();
19 if (cfg == null)begin
20 `uvm_info(get_type_name(), "Using default_apb_config", UVM_MEDIUM)
21 $cast(cfg.factory.create_object_by_name("default_apb_config","cfg"));
22 end
23 if (cfg.has_bus_monitor) begin
24 bus_monitor = apb_bus_monitor::type_id::create("bus_monitor",this);
25 bus_monitor.cfg = cfg;
26 end
27 master = ahb_master_agent::type_id::create("master",this);
28 master.cfg = cfg;
29 // Build slaves
30 slaves = new[cfg.slave_configs.size()] ;
31 // Create multiple slaves and give each a unique name
32 foreach slaves[i] begin
33 slaves[i] =
ahb_slave_agent::type_id::createt($psprintf("slaves[%0d]",i) ,this);
34 slaves[i].cfg = cfg.slave_configs[i];
35 end
36 endfunction : build
37 function void apb_env::assign_vi(virtual apb_if vif);
38 // Based on the configuration, assign master, slave and bus monitor
39 // signals.
40 endfunction : assign_vi
Note Similarly to the agent, create is used to allocate the environment sub-components. This allows introducing
derivations of the sub-components later. For the APB protocol, there is a single master and a variable number of slaves.
The user is not required to call build() explicitly. The SystemVerilog UVM Class Library will do this for all created
components. When all of the components`build () functions are complete, the UVM will call each components connect()
function. Any connections between child components should be made in the connect() function of the parent component.
This ensures that all components have been created before they are connected .
In point-to-point protocols, such as transmit-receive communication protocols or buses that define a single master and
slave interaction, we need a reusable environment class. Beyond consistency (an integrator always uses environments and
their standard attributes), the agents in such environment may share a single monitor.
Block diagrams of point-to-point environments are illustrated in Figure 5-8, Point-to-Point UVC Configured to Verify a
Master or a Slave Device. A point-to-point UVC needs to support verifying both master and slave agents for check and
coverage. This is achieved by using the agent is_nactive attribute. To verify a slave, you instantiate the environment with
an active master agent and a passive slave agent to check and collect coverage on the agent. To verify a master, the slave
will be active and the master will be passive. If you want to collect coverage or check an internal bus, both the master and
slave should be set to passive mode.
Passive Passive
The interesting part in a point-to-point environment is the monitor. This is highly protocol-dependent, but if the same
signals are being used for both master and slave, then one collector and monitor are enough. This means that the bus
monitor is both the slave and master monitor. For consistency, and keeping the same use model, we recommend adding a
pointer from both the master and slave to this joint monitor. Figure 5-9, Interface UVC with Shared Monitor and Collector
depicts this configuration.
Config:ACTIVE Config:ACTIVE
Monitor
Sequencer Sequencer
sequences sequences
Monitor Monitor
Collector
vif vif
APB IF
A UVC is developed on a per-protocol basis for general purpose, protocol-related use. It may support various features or
operation modes that are not required for a particular project. The UVM provides a standard configuration mechanism
which allows you to define the UVCs configuration to suit the current project’s requirements. The UVC can get this
configuration information during the build phase or run phase, and within the constructor. The UVM configuration
mechanism is tailored to enable reuse, but additional guidelines should be followed to achieve reusability.
There are times when the developer knows the context in which the developing UVC will be used. In such cases,
developers should take care to separate the requirements of the UVCs protocol from those of the project and avoid
hard-coding configuration parameters with project specific values. We strongly recommend using only the
interface-protocol documentation in developing the UVC. Later, developers can consult their project’s documentation to
see if there are some generic features which might be useful to implement. For example, they should be able to configure
slave devices to reside at various locations within an address space, instead of hard-coding the address to a specific
location used in the project.
As another example, if within a protocol frame a few bits are defined as reserved, they should stay reserved within the
UVC. The verification logic that understands how a specific implementation uses these bits should be kept outside the
global generic code.
As a developer, it is critical to identify these generic parameters and document them for the environment users.
5.8.3.2 Configuration Requirements
There are many verification component configuration requirements. The configuration mechanism is used by the
testbench integrator and the test writer who wishes to modify the default configuration for a specific test case. The
verification component should check the validity of the configuration, to make sure proper settings are applied before
simulation run time, A user should be able to randomize the configuration and collect coverage on the randomized values.
It should also be possible to use configuration of a subsystem in a - larger system, set the system default configuration and
leave it up to the user to further control the configuration. Configuration should be extensible, as some users may want to
add a new project-specific configuration attribute or constraints. While project specifics and modification are always
possible, the following sections describe the recommend configuration flow.
The UVM allows users to specify configuration objects for components either as separate fields or as a group contained
within a configuration object. Using the configuration attribute for scalar settings is simpler than allocating a configuration
class. However, for most real-world applications, users find a configuration object to be more practical. The number of
attributes in real-world UVCs quickly grows beyond the one or two planned attributes. Creating a configuration object
allows for a place to capture dependencies between individual attributes in constraints. Users can easily print the
attributes, pass them by value or by reference to a different object (even in a different language), wrap existing
sub-environments configuration within a larger class, and much more.
Both agents and environments should have their own configuration class. For example, agents have a configuration class
that holds the standard and protocol-specific attributes. The environment class will have a configuration class that contains
the agent config classes (may be an array in the case of a multi-drop BUS). Testbench configuration classes should wrap the
multiple environment configuration classes. Each layer can impose constraints on its field configuration classes or between
siblings. An example of an interface environment configuration class is illustrated below.
Example 5-12 APB Configuration Classes (slave, master and env config)
1 class apb_slave_config extends uvm_object;
2 string name;
3 uvm_active_passive_enum is_active = UVM_ACTIVE;
4 rand int start_address;
5 rand int end_address;
6 rand int psel_index;
7 constraint c_addr { start_address <= end_address; }
8 constraint c_psel { psel_index inside {[0:15]}; }
9 // UVM object_utils, field macros and constructor
10 extern virtual function bit (check_address_range(int unsigned addr);
11 endclass : apb_slave_config
12 class apb_master_config extends uvm_object;
13 string name;
14 rand uvm_active_passive enum is active = UVM_ACTIVE;
15 // UVM object utils, field macros and constructor syntax
16 endclass : apb_master_config
17 class apb_config extends uvm_object;
18 // APB has one master and N slaves
19 rand apb_mastar_config master_config;
20 rand apb_slave_config slave_configs[];
21 rand bit has_bus_monitor = 1;
22 // constraints on the composite configuration
23 //UVM object_utils, field macros and constructor
24 extern virtual function void add_slave(string name, int start_addr,
25 int psel_indx, uvm_active_passive_enum is_active=UVM_ACTIVE);
26 extern virtual function int get_slave_psel_by_addr(int addr);
27 endclass : apb_config
The nature of the configuration mechanism allows a testbench to define default constraints and, for specific tests, to
override these for corner-case purposes. Each layer, by calling the get config_object, can receive its configuration object,
check its validity, and use the set_config_object mechanism to configure its sub-components.
Note The config object is automatically retrieved in the build() method if it is set in the test or testbench. It is up to the
developer to check the configuration object, and in the case of a null object, to randomize a default config. The example
below illustrates how a UVC checks the validity of a the configuration object, using randomize (null), and sets its agents to
behave accordingly.
Example 5-15 Testbench build() Method Overrides the Default APB Configuration
24 function void demo_tb::build() ;
25 super.build() ;
26 demo_cfg = demo_config::type_id::create ("demo_cfg") ;
27 set_config_object("*.apb0", "cfg", demo_cfg);
28 apb0 = apb_env::type_id::create("apb0", this);
29 ...
30 endfunction : build
In this simple case, the testbench only shows the creation of the apb_cfg. In a real test environment the apb_cfg class is
just one sub-class of a complete testbench configuration class. Within the testbench, users can randomize the
configuration class or build it procedurally. Using randomization is more powerful than using procedural settings, as users
can re-randomize with a different seed and get a different configuration that follows the requirements. An example of a
testbench config class is described in “Testbench Configuration” on page 120.
5.8.3.4 Reconfiguring a Device
In some cases, after initial configuration, devices can be reconfigured to different operation modes. Regardless of whether
this is initiated by the testbench or by another component, these modifications need to be reflected in the testbench.
Some of the device states should remain the same and should not be configured to their default values. Whether you can
deploy reconfiguration randomly or procedurally, using an encapsulated configuration object helps. You can re-randomize
a configuration class while maintaining the existing values of some of the configuration attributes by using the
SystemVerilog rand_mode() construct.
When a new configuration is generated or detected, components need to be notified to fetch a new configuration object
and update their attributes or behavior accordingly. This is done via a recursive call of the user-defined
update_config(uvm_object cfg) function. In the update_config() that is defined in every level of the hierarchy, the
component typically:
Checks the validity of the configuration. In specific the function checks that no topology parameters were modified
(for example, the number of slaves was changed), and that a legal configuration was provided.
Apply the desired configuration on its own attributes. In some cases, variables and queues need to be reset and
threads need to be killed and resumed (for example, if a transaction is being sent when reconfig appears).
Calls its sub-component update_config () method.
The driver and the collector access the DUT signals via a virtual interface which is a reference to a physical interface. When
setting the virtual interfaces, users may want to separate the DUT hierarchy from the testbench hierarchy. This allows, for
example, swapping the DUT or a subsystem with a different one that uses the same interfaces. To set the virtual interface,
a user can use two schemes, use an assign_vi on agents and the environment, or encapsulate the virtual interface in a
generic parameterized container class. While assign_vi is simpler, we recommend using the containers that decouple the
hierarchy between the two. Both techniques are described below.
the testbench. The assign_vi() also does not use the set_config which is the standard way to configure reusable
components.
In most cases, randomizing transactions in a loop cannot capture the high-level intention of a test and an ordered
stream of stimulus is required. For example, you might want to configure a device before sending “random” traffic. Or you
might want to generate related back-to-back transactions.
The UVM sequencer controls the generation of random stimulus by controlling and executing sequences. Most of the
intelligence of UVM traffic generation is in the sequences (and the sequence item) and their capabilities, not in the
sequencer. Some sequences are predefined and pre-registered to a sequencer. For example, every sequencer has a
“random” sequence that generates other registered sequences in a loop. Users can define other interesting scenarios by
creating user-defined sequences. Defining a sequence is always preferred over creating a new test because a sequence is
tagged with a name and later can be reused in multiple tests or to build new sequences.
Each sequence produces a stream of data items or can leverage other sequences to create more abstract sequences.
The leaves of the sequence tree are always data items that are sent to the driver and eventually to the DUT. Figure 5-10
below illustrates an example of how sequenceA combines both data items and sequenceB. SequenceA starts by doing data
1, followed by sequenceB, and finishing with data4. SequenceB does data2 and data3. Eventually, as the sequences unfold,
what is sent to the DUT is an ordered stream of data1, data2, data3, and data4.
Some sequences are provided as part of a reusable component and others are created to test a specific DUT, For
example, a bus-based protocol UVC may include predefined sequences for a single write, a single read, a burst write, and a
burst read. The reusable component developer can also include a library of protocol-specific interesting sequences that
examine certain protocol scenarios. Other sequences may include a sequence to write random data to a DUT-specific
address range and then read it back.
The test writer controls the environment-generated patterns by configuring its sequencers. The test writer can:
Add a user-defined sequence of transactions to a sequencer
Override the sequencers default starting sequence to start with a user-defined sequence
sequenceA
data1
sequenceB
data2
data3
data4
In this section, we describe how to create a library of reusable sequences that later can be leveraged and enhanced by
the environment users.
Verification components can include a library of basic sequences, which test writers can leverage. This approach
enhances reuse of common stimulus patterns and reduces the length of tests. In addition, a sequence can call upon other
sequences, thereby creating more complex scenarios.
The UVM class library provides the uvm_sequence base class. You should derive all sequence classes directly or
indirectly from this class. Sequences allow reactive generation (reaction to the current state of the DUT), have many
built-in capabilities like interrupt support, prioritization schemes and automatic factory support. Sequences can be called
by other sequences and are reusable at higher levels of design integration.
To create a user-defined sequence:
1. Derive a sequence from the uvm_sequence base class and specify the request and response item type parameters.
In the example below, only the request type is specified, apb_transfer. This will result in the response type also being
of type apb_transfer
2. Use the `uvm_sequence_utils macro to associate the sequence with the relevant sequencer type and to
provide the various automation utilities. This macro also adds a p_sequencer variable that is a pointer to the specific
sequencer invoking that sequence. Static sequencer properties such as hierarchical path, channel number (if there are
multiple channels), end-of-test control and more are accessible through the p_sequencer variable. The macro
provides a downcasted version of this variable using the second argument of this macro.
3. Implement the sequences body() task with the specific scenario you want the sequence to execute. In the body task,
you can execute data items and other sequences using “`uvm_do” on page 93 and “`uvm_do_with” on page 93.
A simple APB transfer sequence declaration is shown below. The body() of the sequence creates, randomizes and sends a
single data item of type apb_transfer.
1 class apb_transfer_seq extends uvm_sequence #(apb_transfer);
2 // Constructor and UVM automation macros - note the second ARG
3 `uvm_sequence_utils(apb_transfer_seq, apb_master_sequencer)
4 function new(string name="apb_transfer_seq");
5 super.new(name);
6 endfunction : new
7 virtual task body();
8 `uvm_do (req)
9 endtask : body
10 endclass : apb_transfer_seq
Line 1: Defines a sequence called apb_transfer_seq, with apb_transfer data items. A sequence
variable req of type apb_transfer is built into the base UVM_sequence class.
Line 3: The `uvm_sequence_utils macro registers the sequence to be executed with a specific sequencer type. This
allows only the intended sequencer type to execute this sequence. This macro is similar to the `uvm_object_utils
macro (and its variations) except that it takes a second argument, which is the sequencer type name this sequence is
associated with.
Note Do not use the `uvm_object_utils macro when using the `uvm_sequence_utils macro. The
Note Timing, flow control, looping and forking are allowed in the body() task.
This second sequence example creates a higher-level sequence by executing multiple apb_transfer_seq sequences.
1 class multi_apb_transfer_seq extends uvm_sequence #(apb_transfer);
2 rand int num_seq;
3 constraint c_num_seq {num_seq inside {[1:10]}; }
4 // Constructor and UVM automation macros
5 `uvm_sequence_utils(multi_apb_transfer_seq, apb_master_sequencer)
6 function new(string name= "multi_apb_transfer_seq");
7 super.new(name);
8 endfunction : new
9 task body();
10 apb_transfer_seq apb_seq; // Instance of another sequence type
11 repeat (num_seq)
12 `uvm_do(apb_seq)
13 endtask : body
14 endclass : multi_apb_transfer_seq
Line 1: This sequence also extends from uvm_sequence and is parameterized by the apb_transfer type.
Lines 2-3: Sequences can optionally contain random fields with constraints. These fields are randomized when the
sequence is created and executed. In this example, the sequence will execute the apb_transfer sequence num_seq times,
and num_seq is a random integer between 1 and 10
Line 5: All sequences should use the `uvm_sequence_utils macro to register the sequence to the appropriate
sequencer.
Line 10: An instance of the apb_transfer_seq. This sequence is executed in the uvm_do call.
Lines 9-13: The body() task uses a repeat loop, and the `uvm_do macro will execute an apb_transfer_seq sequence
num_seq times.
In many cases, users will want to generate sequences that are a bit more complex and have more built-in control. The
class apb_write_read_word_seq in the following example defines another APB sequence to write a transaction and read it
back from the same address. This sequence includes a field constraint (start_addr) and inline constraints.
1 class apb_write_read_word_seq extends uvm_sequence #(apb_transfer);
2 rand bit [31:0] start_addr;
3 constraint c_addr { start_addr[l:O] == 2'b00; }
4 //Constructor and uvm automation macros for sequences
5 `uvm_sequence_utils(apb_write_read_word_seq, apb_master_sequencer)
6 //The body() task executes 2 APB transfers with the same address.
7 task body();
8 `uvm_do_with(req, {req.addr == start_addr; req.direction == APB WRITE;})
9 `uvm_do_with(req, {req.addr == start_addr; req.direction == APB_READ;})
10 endtask : body
11 endclass : apb_writ_read_word_seq
Line 2: This sequence has a random start_addr field. The address is randomized once and is used for both the write and
read operations.
Line 3: The field constraint ensures that the randomized addresses are byte-aligned.
Line 8: In this example, the body uses a `uvm_do_with macro to execute a req (apb_transfer) and to specify inline
constraints to be used when the item is randomized. The syntax for the constraint block is the same syntax used for an
inline constraint. The first transaction processed is an APB_WRITE transaction with start_addr as the address.
Line 9 : The second `uvm_do_with macro executes an APB_READ transfer from the same address.
1 class apb_traffic_seq extends uvm_sequence #(apb_transfer);
2 multi_apb_transfer_seq multi_seq; // rand param is num_seq
3 apb_write_read_word_seq wr_rd_seg; // rand param: start_addr
4 // Constructor and uvm_automation macros for sequences
5 `uvm_sequence_utils(apb_write_read_word_seq, apb_master_sequencer)
6 // The body executes two pre-defined sequences with constraints.
7 task body();
8 repeat (5)
9 `uvm_do_with(wr_rd_seq, {start_addr inside {[‘hOOOO:’h1fff]};})
10 `uvm_do_with(multi_seq, {num_seq = 5;})
11 repeat (5)
12 `uvm_do_with(wr_rd_seq, {start_addr inside {[‘h2000:’hffff]};})
13 endtask : body
14 endclass : apb_traffic_seq
Lines 2-3: Declarations of previously defined sequences. The multi_apb_transfer_seq has a random parameter for
num_seq. When we “do” this sequence on line 10, we specify the value of the num_seq parameter to be 5. The
apb_write_read_word_seq has a random parameter for start_addr.
Lines 9 and 12: The sequence is called with a constraint specified on start_addr. This constraint will be solved in
conjunction with the apb_write_read_seq constraint (start_addr [1:0] ==0)
So far, we have introduced two macros. But what do they do? Let’s take a look at each.
`uvm_do
This macro takes as an argument either a variable of type uvm_sequence item or of type uvm_sequence.
An object is created using the factory and assigned to the specified variable. When the driver requests an item from the
sequencer, the item is randomized and provided to the driver.
The do operation translates into a number of steps:
1. The sequence waits until an item is requested from the driver.
2. The transaction (or sequence) is allocated using the factory. The transaction (or sequence) is randomized with built-in
data item or sequence field constraints.
3. If you are doing a transaction, it is sent via TLM to the driver for execution. In the case of a sequence, it starts
executing the body() task of the sequence (repeating steps 1-3, above).
4. Block code execution until the item was sent. The driver indicates via TLM that the item is done.
Explicit calls to execute each of the steps above results in more code to maintain and can yield human errors, for example,
if you forget to use the factory to allocate a data item, you may get unexpected results when a test overrides the default
data item to address a coverage hole. UVM introduces one-line macros to simplify this.
`uvm_do_with
This macro is similar to “`uvm_do” on page 93. The first argument is a variable of a type derived from
uvm_sequence_item, which includes items and sequences. The second argument can be any valid inline constraints
that would be legal if used in arg1.randomize() with inline constraints. This enables adding different inline constraints,
while still using the same item or sequence variable.
`uvm_do_with(req, {req.addr == start_addr;
req.direction == APB_WRITE;})
More options exist for creating and executing data items and sequences. For more information, see Chapter 8 “Stimulus
Generation Topics”.
5.9.2.1 uvm_random_sequence
This built-in sequence is the default sequence to be executed by the sequencer. This sequence randomly selects and
executes one or more sequences from the sequencer’s queue of registered sequences (excluding
uvm_random_sequence and uvm_exhaustive_sequence). The number of sequences executed depends on the
count field of the sequencer. If count is set to -1, the random sequence will randomize a number between 0 and
uvm_sequencer::max_random_count. If count is not -1, then count sequences will be executed by
uvm_random_sequence.
Note For a sequencer to not execute any items, set the default sequence to uvm_random_sequence and its count to
0. Users often do this in conjunction with a virtual sequencer (see “Controlling Other Sequencers” on page 133 for more
information).
5.9.2.2 uvm_exhaustive_sequence
This built-in sequence is also predefined in the sequencer. This sequence will exhaustively execute all user-defined
sequences for the current sequencer. The predefined uvm_simple_sequence will also be executed, but the other two
predefined sequence types (uvm_random_sequence and uvm_exhaustive_sequence) will not. The sequences
are executed exactly once and in a random order.
The example below shows the body for the uvm_exhaustive_sequence.
1 task uvm_exhaustive_sequence::body();
2 l_count = m_sequencer.sequences.size() - 2;
3 max_kind = m_sequencer.sequences.size();
4 l_exhaustive_seq_kind = m_sequencer.get_seq_kind ("uvm_exhaustive_sequence") ;
5 repeat (l_count) begin
6 assert(randomize(l_kind) with {l_kind > l_exhaustive_seq_kind && l_kind <
max_kind; });
7 // l_kind is randc.
8 do_sequence_kind(l_kind);
9 end
10 endtask
Note The l_ kind variable is declared as randc in order to randomize without replacement.
5.9.2.3 uvm_simple_sequence
This is the third built-in sequence which is predefined in the sequencer. This sequence is provided to allow default
execution of the UVC without any user-defined sequences.
task uvm_simple_sequence::body();
`uvm_do(item)
endtask
By default, all sequencers execute their uvm_random_sequence object that, in a loop, selects a sequence from its
sequence library and executes it. The sequencer has a string property named default_sequence which can be set to any
sequence listed in a sequencer’s queue. This sequence is used as the default sequence for that instance of the sequencer.
To change the default sequence:
1. Declare a user-defined sequence class which derives directly or indirectly from uvm_sequence.
2. Configure the default_sequence property for a specific sequencer or a group of sequencers. Typically, this is done
inside the test or testbench class before creating the component that includes the relevant sequencer(s). For
example,
set_config_string("*.master.sequencer","default_sequence","apb_traffic_seq");
The first argument uses a wildcard mechanism. Here, any instance name containing .master.sequencer will have its
default_sequence property (if it exists) set to the value apb_traffic_seq.
More information about how to configure the sequencer is provided in Chapter 7 “Simple Testbench Integration”.
Libraries of sequences are compiled as part of UVM testbench. The `uvm_sequence_utils associates each sequence
with a sequencer. When the testbench is created, the sequencer builds an array of sequences to be executed in its
constructor using the `uvm_update_sequence_lib_and_item() macro. Figure 5-11 on page 94 shows the
Sequencer/Driver/Sequences Array interaction.
Sequences Array Sequencer controls
Sequencer sequence execution and
random sends data items to the
Build-in exhaustive read_word driver(one at a time)
sequences
simple
read_word
User-defined Driver controls when the
write_word Driver next data item is
sequences
configure_dut requested and signals
vif when the item is finished
Sequence captures intent of scenarios and
can reuse other predefined sequences
In a user-defined uvm_test—for example, apb_base_test (discussed in “Creating the Base Test” on page 122)—users can
configure the simulation environment to use a modified version of an existing sequence or sequence item by using the
common factory to create instances of sequence and sequence-item classes. See “UVM Factory” on page 55 for more
information.
To override any reference to a specific sequence or sequence-item type:
1. Declare a user-defined sequence and/or sequence item class. The example in the next step assumes the declaration
of a basic sequence item of type apb_transfer, and a derived item of type word_aligned_transfer.
2. Invoke the appropriate uvm_factory override method, depending on whether you are doing a global or
instance-specific override. For example, assume the apb_write_read_word_seq sequence is executed by a sequencer
of type apb_master_sequencer (both defined in "User-Defined Sequences” on page 91). You can choose to replace all
usage of apb_transfer types with word_aligned_transfer types. This can be selected for all requests for apb_transfer
types from the factory, or for specific instances of apb_transfer. From within an UVM component, a user can execute
the following:
1 // Affect all factory requests for type apb_transfer.
2 set_type_override_by_type(apb_transfer::get_type() ,
word_aligned_transfer::get_type());
3 // Affect requests for type apb_transfer on only a given sequencer,
4 set_inst_override_by_type("env0.master.sequencer.*",
apb_transfer::get_type(), world_aligned_transfer::get_type());
5 // Alternatively, affect requests for type apb_transfer for all
6 // sequencers of a specific env.
7 set_inst_override_by_type("env0.*.sequencer.*",
apb_transfer::get_type(),word_aligned_transfer::get_type());
3. Use any of the sequence macros that allocate an object (as defined in “Sequence and Sequence Item Macros” on page
93); for example, the uvm_do macro.
Because the sequence macros call the common factory to create the data item object, existing override requests will take
effect and a word_aligned_item will be created instead of an apb_transfer.
A reusable sequence library is a set of user-defined sequences. Creating a UVC reusable sequence library is an efficient
way to facilitate reuse. The environment developer can create a meaningful set of sequences to be leveraged by the test
writer. Such sequence libraries avoid code duplication in tests, making them more maintainable, readable, and concise.
Tips
Try to think of interesting protocol scenarios that many test writers can use.
As some users may not want to use the reusable sequence library (because the sequences may not match the design
requirements of the user), do not `include a reusable sequence library within the UVC package file list. Leave it to the
user to decide whether to use them.
UVM provides an objection mechanism to allow hierarchical status coordination between components. The built-in
objection, uvm_test_done, provides a way for components and sequences to synchronize their activity and indicate
when it is safe to end the test.
In general, the process is for a component or object in the components context (for example, a sequence) to raise a uvm
test done objection at the beginning of an activity that must be completed before the simulation stops, and to drop the
objection at the end of the activity. When all the raised objections are dropped, the simulation terminates. The end of test
API includes raise_objection, drop_objection, and all_dropped calls that are described below:
raise_objection
The syntax of raise_objection is as follows:
function void raise_objection (uvm_object obj = null,
string description ="", int count = 1 )
Raises the number of objections for the source object by count, which defaults to 1. The object is usually the handle of the
caller. If object is not specified or is null, the implicit top-level component, uvm_top, is chosen. The description
parameter allows users to specify a user message that typically provides the reason for raising the objection. Rasing an
objection causes the following:
The source and total objection counts for object are increased by count.
The raised objection’s virtual method is called, which calls the uvm_component:raised method for all of the
components up the hierarchy.
A trace message including the description may be issued if the trace bit is set.
drop_objection
The syntax for dropping an objection is as follows:
function void drop_objection (uvm_object obj = null,
string description =””, int count = 1 )
The drop_objection drops the number of objections for the source object by count, which defaults to 1. The object is
usually the handle of the caller. If object is not specified or is null, the implicit top-level component, uvm_top, is chosen.
The description parameter allows users to specify a user message that typically provides the reason for dropping the
objection. Dropping an objection causes the following:
The source and total objection counts for object are decreased by count. It is an error to drop the objection count for
object below zero.
A trace message including the description string might be issued.
If the total count is non-zero at for a hierarchical level, the drop is propagated up the hierarchy immediately.
If the total count is zero, the drain time is initiated. If the drain time completes and the count is still zero, the drop is
propagate up the hierarchy.
5.11.2 End-of-Test Objection Mechanism Usage
Proactive agents may have a meaningful agenda to be achieved before the test goals can be declared as done. For example,
a master agent may need to complete all its read and write operations before the run phase should be allowed to stop. A
re-active slave agent may not object end-of-test as it is merely serving requests as they appear without a well-defined
agenda.
A typical use is for a sequence to raise an objection when it is started as a root sequence (a sequence that has no parent
sequence), and to drop the objection when it is finished as a root sequence.
1 class apb_read_block_seq extends uvm_sequence#(apb_transfer);
2 task pre_body();
3 // raise objection if started as a root sequence
4 uvm_test_done.raiae_objection(this, "APB read block");
5 endtask
6 task body();
7 //read block activity
8 ...
9 endtask
10 task post_body();
11 // drop objection if started as a root sequence
12 uvm_test_done.drop_objection(this,
13 "APB read block”);
14 endtask
15 endclass
To avoid re-implementing the raise and drop, you might want to create a parent sequence that implements the pre_body()
and post_body() for its derivatives. Keep in mind that the pre_body() and post_body() are called only when the sequence is
started. So if the sequence is started as the default sequence, the raise and drop will be activated; but if it is a
sub-sequence of a more abstract sequence, then, as desired, no raises and drops occur.
Note It is beneficial to associate the raise description string with the associated string for drop. We recommend using the
same string for both. In the future, UVM might leverage this information to produce more descriptive log files to
determine why a simulation stopped or did not stop as intended.
In some contexts or tests, a master could be part of the main simulation agenda or some supporting logic for a different
high-level desired. In such contexts the user may want to configure the sequencer to object or avoid objecting end of test.
We recommend adding a switch to the sequencer to enable and disable end-of-test objection. This allows the user to use
this kind of style:
// in a test
set_config_string("tb.env0.master.sequencer",
"default_sequence", "uvm_exhaustive_sequence");
set_config_int ("tb.env0.master.sequencer", "object_test_done", 1);
The best way to implement this technique is by deriving a new sequencer that takes the configuration attribute into
account and changes the behavior as needed.
1 class apb_sequencer extends uvm_sequencer #(apb_transfer);
2 bit object_test_done;
3 // UVM automation macro for sequencers
4 `uvm_sequencer_utils_begin(apb_sequencer)
5 `uvm_field_int(objecting_test_done, UVM_ALL_ON)
6 `uvm_sequencer_utils_end
7 // Constructor - required UVM Syntax
8 function new (string name, uvm_component parent);
9 super.new(name, parent);
10 `uvm_update_sequence_lib_and_item(apb_trausfer;
11 endfunction : new
12 //a test-done objection
13 task start_default_sequence();
14 if (object_test_done)
15 uvm_test_done.raise_objection(this);
16 super.start_default_sequence();
17 if (object_test_done)
18 uvm_test_done.drop_objection(this) ;
19 endtask
20 endclass : apb_sequencer
Lines 3-5: Adding a configurable attribute to enable objection raising/dropping including UVM automation.
Lines 13-19: Raising and dropping the uvm_test_done objection before and after the default.
Debugging the raise and drop of objections is a critical part of the objection mechanism. For debug, a user can query the
components about objection status and also get run-time trace information about objections activity.
A user might want to analyze trace information of objections that are raised or dropped. This is achieved using the
set_trace function:
uvm_objection::set_trace(bit trace_objection)
Setting the trace_objection to 1 will issue trace information to the screen. Trace information is displayed using the
standard reporting structure. Each specific type of trace information has its own tag so that users can control the specific
type of information they are concerned about.
Vertical reuse allows building larger systems out of existing ones. Often, when all the subsystem objections are dropped,
there is still some outstanding activity that must complete before the simulation stops (for example, internal propagation
of data within the device). The UVM objection mechanism allows subsystem environment developers to define a drain
time per subsystem. The full system testbench integrator does not need to worry about the drain-time of
sub-environments and can add full system requirements on top.
The reusable nature of objections is achieved by having objection information propagated up the hierarchy. This allows
components to get a notification any time one of their children raise or drop an objection, or when all of the object ions at
a current level have been dropped. A typical usage example is to set a drain time on all_objections dropped when the
uvm_test _done. set_drain_time () is insufficient; for example, when the maximum time through the DUT is 15 clock cycles,
the all objections dropped task can be used in the environment to perform the drain calculation like:
class myenv extends uvm_env;
task all_dropped (uvm_objection objection, uvm_object source_obj, int count);
if(objection == uvm_test_done);
repeat(15) @(posedge vif.clk);
endtask endclass: myenv
Note UVM components have a drain-time built-in property. We recommend extending all_dropped and not using the
drain-time attribute of a component and not using the component drain-time. Using clocks or other user-defined events
provides much more control and are scalable in vertical reuse as clocks or time scales change.
The heartbeat capability is useful to determine when objects are no longer functioning properly. For example, a testbench
may wish to define a heartbeat that detects when a deadlock condition occurs and the simulation cannot advance. The
mechanism works by registering components that need to indicate viability within a window of two-event occurrences.
The objection mechanism provides built-in support for heartbeats.
There are three modes that a heartbeat can have:
UVM_ALL_ACTIVE—All registered components must emit a heartbeat during the heartbeat window.
UVM_ANY_ACTIVE—One or more of the registered components must emit the heartbeat during the window.
UVM_ONE_ACTIVE—Exactly one of the components must emit the heartbeat during the window.
This is an example of the heartbeat usage of the objection mechanism. In this scenario we have:
1. A child_component that:
Can be configured to emit N number of heartbeat objections
Can be configured with the delay between the heartbeat objections
2. A parent_component that:
Uses the event specified in #4 as the event for the heartbeat objection
Contains three child_component objects
3. A #100 clock
4. A uvm_event that is triggered every posedge of #3
5. A test that:
Configures child_0 of parent_component to emit 3 heartbeats #90 apart
Configures child_l of parent_component to emit 5 heartbeats #90 apart
Configures child_2 of parent_component to emit 2 heartbeats #90 apart.
This child will cause the heartbeat timeout,
Note The heartbeat mechanism should be set by the reusable environment development, but its activation should be left
to the testbench integrator. Only the integrator knows the what activity is mandatory for each project, and it is not
desirable to end a long simulation just because a heartbeat mechanism was active but improperly configured.
Checks and coverage are crucial in a coverage-driven verification flow. As the traffic is randomized and not explicitly called
for, it is critical to use a metric to ensure that the verification goals were achieved and no area was overlooked. Functional
coverage is user-defined and should represent precisely (no more and no less) the coverage goals. If a user thinks of a
scenario that was not sufficiently exercised, they can add a coverage attribute to their testbench.
Interface UVCs are focused on protocol-specific coverage. The coverage groups are implemented by the UVC developer,
who knows the protocol and what kind of interesting scenarios need to be observed. An interface UVC coverage model
includes signal and assertion-level coverage and transaction-level coverages, such as the type and nature of data items
that are injected, interesting transition of these and more.
Some of the questions related to coverage collection are:
Where do I place the coverage items?
When and how to sample coverage?
How do I enable and disable coverage items?
How do I collect instance versus type coverage?
How do I know how many coverage items I need?
The following section discusses such coverage-related concerns.
Coverage and checking are done in the monitor and collector. Coverage is collected on injected or observed transactions
(passive mode). After sampling the observed transaction values, the monitor adds these to the coverage database.
Sampling the generated data directly in the driver is not recommended. Sampling coverage that was collected from the bus
ensures that the data reached the bus and did not get dropped between the randomization and the final injection. It also
allows for better reuse of coverage code. Placing coverage in a driver allows you to collect coverage only when creating
transactions, while placing coverage in a collector and monitor allows you to capture coverage in every simulation.
Note SystemVerilog allows sampling the coverage via event or by calling the sample () attribute. For many reasons, using
the event can be tricky; it is quite possible that between the time the event was emitted and the actual sampling, the
values of the transaction properties have changed. We highly recommend using the sample() call.
Transaction coverage involves a transactions physical properties, information abstracted from these properties (such as
ranges of values) and crosses between the properties. Some of the transaction attributes are added for coverage purposes.
(For example, a transaction may collect start_time and end_time to capture the processing start and end time). You may
want to collect instance-based or type-based transaction coverage. Instance coverage creates a different coverage result
for each instance of a monitor or collector. For example, for a router with multiple ports, you may want to collect coverage
of packet types that were sent to port1, as opposed to the type of packets that were sent to all ports. Transition coverage
is often more meaningful on instance coverage. For example, type-based coverage may indicate a good distribution of
reads on writes on multiple ports, but—when used in conjunction with instance-based coverage—transition coverage will
indicate an important requirement that you execute a read on a specific port directly following a write to that port.
Transaction coverage groups can be placed either in the data item class or in the monitor. If you follow the object-oriented
principle of loose coupling between classes you may believe the best place for transaction coverage groups is in the
transaction itself. If the same transaction is used by multiple components, each component can sample the transaction
attributes without duplicating the properties or spreading its attributes across multiple components. Coverage groups
added to items are more difficult to manage and often cause additional performance penalties. We recommend that you
declare and instantiate coverage groups within the monitor and collector.
The example below illustrates the APB monitor coverage group:
1 //From: class apb_bus_monitor extends uvm_monitor;
2 covergroup apb_transfer_cg;
3 TRANS_ADDR : coverpoint: trans_collected.addr {
4 bins ZERO = {0};
5 bins NON_ZERO = {[1:8’h7f]};
6 }
7 TRANS_DIRECTION : coverpoint trans_collected.direction ;
8 TRANS_DATA : coverpoint trans_coilected.data {
9 bins ZERO = {0};
10 bins NON_ZERO = { [1:8’hfe]};
11 bins ALL_ONES = (8’hff);
12 }
13 TRANS_ADDR_X_TRANS_DIRECTION: cross TRANS_ADDR, TRANS_DIRECTION;
14 Endgroup
Placing a single coverage group in the monitor allows a single instance of the coverage group to be used for all data items
that the monitor collects. The System Verilog language requires that coverage groups must be created in the class
constructor, so you cannot use the build() method. For performance reasons, you may want to disable creation of the
coverage group when coverage enable is set to 0. Thus, we recommend:
Instantiating the desired covergroup in the monitor constructor by calling get_config_int and only creating the
covergroup if coverage_enable is set to 1.
Setting the instance name of the coverage group to be the monitor hierarchical path
// apb_master_monitor constructor
function new (string name, uvm_component parents;
super.new(name, parent);
// Create covergroup only if coverage is enabled
void`(get_config_int("coverage_enable`, coverage_enable));
if (coverage_enable) begin
apb_transfer_cg = new();
apb_transfer_cg.set_inst_name((get_full_name(), ".apb_transfer_cg"));
end
endfunction : new
Note While some attributes in the transaction are needed only for coverage, it is typically not recommended to create a
base transaction and derive from it a coverage_transaction and a sequencer_transaction. While this could be the right
modeling and might have positive memory and run-time impacts, it prevents the user from further extending the
definition of both the monitor and the sequencer sub-types.
The embedded covergroup uses the sample () method as its sampling trigger. The sampling of the covergroup is done in
a perform_coverage function which is called after a transfer has been collected.
function void apb_bus_monitor::perform_coverage();
cov_apb_transfer.sample();
endfunction : perform_coverage
This function covers several properties of the transfer. The perform_coverage () function is called procedurally after the
item has been collected by the monitor.
if (coverage_enable)
perform_coverage ();
Timing-related coverage such as inter-packet gap, should be done by the collector. In order to allow transaction-level and
even untimed execution mode, the monitor should not contain the concept of time.
Class checks should be implemented in the classes derived from uvm_monitor and uvm_collector. The derived classes of
uvm_monitor and uvm_collector are always present in the agent, and thus always contain the necessary checks. The bus
monitor is created by default in an env, and if the checks are enabled, the bus monitor will perform these functions. The
remainder of this section uses the master monitor as an example of how to implement class checks, but they apply to the
bus monitor as well.
You can write class checks as procedural code or System Verilog immediate assertions.
Tip Use immediate assertions for simple checks that can be written in a few lines of code, and use functions for complex
checks that require many lines of code. The reason is that as the check becomes more complicated, so does the debug of
that check.
Note Concurrent assertions are not allowed in SystemVerilog classes per the IEEE1800 LRM. These are allowed only
within an interface.
Following is a simple example of an assertion check. This assertion verifies that the least-significant bits of the address are
2`b00 for word addressing. Otherwise, the assertion fails.
function void bus_monitor::check_address();
check_address : assert(trans_collected.addr[1:0] == 2`bOC) else
`uvm_error("ADDRERR", "Invalid transfer address!") end
endfunction : check_address
Following is a simple example of a function check for a bus transfer of variable size (not part of the APB protocol). This
function verifies that the size field value matches the size of the data dynamic array. While this example is not complex, it
illustrates a procedural code example of a check.
function void bus_monitor::check_transfer_data_size();
if (trans_collected,size != trans_collected.data.size() )
// Call DDT error: Transfer size field / data size mismatch,
endfunction : check_transfer_data_size
The proper time to execute these checks depends on the implementation. You should determine when to make the call to
the check functions shown above. For the above example, both checks should be executed after the transfer is collected by
the monitor. Since these checks happen at the same instance in time, a wrapper function can be created so that only one
call has to be made. This wrapper function follows.
function void bus_monitor::perform_checks() ;
check_address() ; check_transfer_data_size();
endfunction : perform_checks
The perform_checks () function is called procedurally after the item has been collected by the monitor.
The monitor and the collector should have coverage_enable and checks_enable configuration fields.
Yet, sometimes these flags are not enough, as you may wish to have a finer granularity of control over which coverage and
checks are enabled. Some of the requirements on enabling and disabling coverage include:
Enabling and disabling coverage and checking for agents
For performance reasons you may want to avoid collecting the coverage (as opposed to post simulation filtering).
Disabling coverage and checks from the command line without recompilation or re-elaboration
To address coverage and checking filtering at the component level, you should provide a means to control whether the
checks are enforced and the coverage is collected. Use the coverage_enable and checks_enable fields for this purpose or
provide this information as part of an agent configuration class. The field can be controlled using the uvm_component
set_config2 interface. The following is an example of using the ehecks_enable bit to control the checks.
if (checks_enable)
perform_checks();
If checks enable is set to 0, the function that performs the checks is not called, thus disabling the checks. The following
example shows how to turn off the checks for all master.monitor components:
set_config_int("*.master.monitor", "checks_enable", 0);
The same capabilities exist for the coverage_enable field in the APB agent monitors and bus monitor.
The developer can provide an additional flag for each coverage group to prevent or enable its instantiation. Once this is
provided, the user can enable and disable these separately.
A protocol may have multiple operation modes. As well as separate flags, coverage groups and checks can be relevant or
irrelevant for these. For example, if we configure a UVC to have a single master, some coverages and checks of the
arbitration may not be necessary. The UVC developer should capture these configuration dependency modes in their UVC.
For example, the UVC environment may accept parameters, and based on that data, determine which checkers and
coverage attributes should be enabled and configure the agents or monitors accordingly.
In a UVC, checks and coverage are defined in multiple locations depending on the category of functionality being analyzed.
In Figure 5-7 on page 82, checks and coverage are depicted in the uvm_monitor, uvm_collector, and SystemVerilog
interface. The following sections describe how the cover, covergroup, and assert constructs are used in the UVM APB UVC
example.
Interface checks are implemented as assertions. Assertions are added to check the signal activity for a protocol. The
assertions related to the physical interface are placed in the env’s interface. For example, an assertion might check that an
address is never X or Y during a valid transfer. Use assert as well as assume properties to express these interface checks.
An assert directive is used when the property expresses the behavior of the device under test. An assume directive is used
when the property expresses the behavior of the environment that generates the stimulus to the DUT.
The mechanism to enable or disable the physical checks works by mirroring the coverage and checks settings in the
physical interfaces and propagating the data into the interfaces. This initial configuration values are copied in the run ()
phase and their changes are propagated to the interface within a forever loop.
Resets are typically done for a system or a subsystem level. When a reset appears, each testbench component should
return itself to its initial state. Because multiple resets can occur in a single simulation, ensure that the DUT can handle
multiple reset conditions. Primary UVC reset requirements are:
Graceful shutdown and reinvocation of assertions, threads and checkers.
Smooth resetting of variables, scoreboard and other data structures. Not all values need to be reset (for example,
memories may retain their existing values).
No false coverage information, no missed DUT errors, no coverage errors.
Easy/flexible hooks for users to do what they want upon reset (for example, drive chosen reset values into signals
during reset).
Note The testbench should be able to react to the DUT reset or trigger a reset on its own; but this is a system UVC
responsibility and not an interface UVC.
clk
reset
Note The current UVM reset methodology will be updated once the run-time phases features will be added to UVM. At
the same time, the main concepts of multiple resets in a single simulation remain the same, and much of the logic
recommended here will stay valid.
Every sub-component should have a reset task that resets signals and testbench variable to their default reset values.
The reset can optionally invoke sub-components of the current component.
1 task apb_master_driver::reset() ;
2 //If the reset is not active, then wait for it to become active before
3 // resetting the interface.
4 wait(!vif.preset);
5 vif.paddr <= ‘h0;
6 vif.pwdata <= ‘h0;
7 vif.prwd <= ‘b0;
8 vif.psel <= ‘b0;
9 vif.penable <= ‘b0;
10 endtask : reset
The driver’s main get_and_drive loop forks reset and regular activity in an endless loop. If reset appears, the driver is
stopped, reset () is called, and another iteration of the while loop is executed.
11 // This task manages the interaction between the sequencer and driver
12 task apb_master_driver::get_and_drive();
13 while (1) begin
14 reset ();
15 fork
16 @(negedge vif.preset)
17 `uvm_info(get_type_name() , "get_and_drive: Reset Asserted",
UVM_MEDIUM)
18 begin
19 // This thread will be killed on reset
20 forever begin
21 @(posedge vif.pclock iff (vif.preset))
22 seq_item_port. get_next_item (req) ;
23 drive_transfer (req) ;
24 seq_item_port.item_done(req);
25 end
26 end
27 join_any
28 disable fork;
29 //If we are in the middle of a transfer, need to end the tx. Also,
30 //do any reset cleanup here. The only way we got to this point is via
31 //a reset.
32 if(req.is_actxve()) this.end_tr(req);
33 end
34 endtask : get_and_drive
Guidelines about reset:
In some examples, soft, hard or other user-defined resets need to be implemented. You can implement different
styles of reset as an input parameter to the reset () task.
Many times, a reset needs to be triggered by the testbench. However, this is not the task of the interface UVC and is
discussed in Chapter 10 “System UVCs and Testbench Integration”.
Resets and Assertions
Concurrent assertions reside within the SystemVerilog interface. You might want to kill running assertions when a reset
appears or has_checks is dropped within the simulation. This could be achieved using the following:
interface yapp_if (input clock, input reset );
// SVA default reset
default disable iff (reset && !has_checks);
...
endinterface
Resetting the Transaction-Level Components
The transaction-level components do not have access to the virtual interface and cannot use the reset signal to reset
themselves. The solution for the transaction-level component is:
For every agent ,define a reset() function that reset the agent and calls the sub-components reset. As needed, each
sub-component can have a default reset function.Remember that the signal-level components trigger their own reset.
Define a reset analysis port in the monitor. Upon a reset event, the gaten rest function is called.
The reset function can be called by the monitor or can be invoked from above by a higher-level component.
Reset of the sequencer might require one of the following behaviors:
Discard the current item,but continue the current sequences.
Reset the sequencer by calling the stop_sequenees () function. After this function is called, the default sequence or
any other sequence that was started at time zero needs to be restarted. Use the get_config to get the
default_sequence that needs to be executed.
Note There is no need to modify the seed on a reset (), as the random stability rules in SystemVerilog cause the
As a user adopts a new package they may want to find the docs, examples, and other files. They may also want to run a
quick test to make sure that the package is complete and no files are missing. UVM recommends a standard directory
structure that addresses many users requirements. Directory structure and filename consistency may seem trivial, but as
you integrate and share verification components, the standard directory structure becomes a big factor in reducing the
support effort within and between teams. The packaging guidelines of UVM include:
Standard directory structure
File organization and naming conventions
Package and name space recommendations
Each interface UVC resides within a top root directory that is named for its interface protocol. The following figure
illustrates the standard directory structure for the APB UVC:
NOTES
All reusable code resides within the sv sub-directory. The bulk of the UVC files are placed here, with the SystemVerilog
interface file(s) for the UVC. The sv directory may optionally contain subdirectories for coverage, checking, or
sequences.
The docs directory contains the UVC documentation.
The examples directory contains one or more usage examples. This may include a connection to a dummy
DUT and/or examples to demonstrate different UVC configurations.
A demo.sh scripts allows standalone checking of the completeness of the UVC by running one of the provided
examples.
Other optional directories may also be included. You may include a SystemC, or e directory for UVCs that contain
components written in those languages. You may also include a data directory to store memory data for sequences,
ROMs and other memory requirements in a UVC.
apb/
demo.sh APB Interface UVC Directory
examples/
w_dut Example test and files with ABV connected to the DUT
Using a standard file naming convention makes it easy to identify and find files during UVC development and debug. Each
component should be specified in its own file, and the filename should match the class name, which in turn should include
the name of the component. For example, the APB master driver class will be found in the apb_master_driver.sv file.
Figure 5-14 on page 109 shows an exam plefrom the AMBA APB sv directory:
apb_collector.sv apb_master_seq_lib.sv apb_slave_seq_lib.sv
apb_config.sv apb_master_sequencer.sv apb_slave_sequencer.sv
apb_env.sv apb_monitor.sv apb_transfer.sv
apb_if.sv apb_pkg.sv apb_types.sv
apb_master_agent.sv apb_slave_agent.sv
apb_master_driver.sv apb_slave_driver.sv
Finally, we recommend capturing all of the reusable data declarations in a SystemVerilog package and importing the
package into a testbench that uses it. The package file should contain only definitions and class declarations that will be
included as part of a reusable component. These files are included in the package file (apb_pkg. sv) as shown below:
package apb_pkg;
// Import the UVM package and include the UVM macro definitions import
uvm_pkg::*;
`include "uvmjnacros.svh"
//Data types and data item declaration `include "apb_types. sv"
`include "apb_transfer.sv"
//Shared files
`include "apb_collector.sv"
`include "apb_monitor.sv"
// APB master files `include "apb_master_driver.sv"
`include "apb_master_sequencer.sv"
`include "apb_master_agent.sv"
// APB slave files
`include "apb_slave_driver. sv"
`include "apb_slave_sequencer.sv"
`include "apb_slave_agent.sv"
`include "apb_env.sv"
endpackage : apb_pkg
This package is compiled and imported into any test or testbench that uses the component.
Note In previous sections we recommended that you do not include the sequence library files in the UVC package. If you
have standard sequences that will be used in many applications it makes sense to capture these sequences in a seq_lib file
and include these files in the package. For example the APB interface may have; apb_read_byt_seq and
apb_read_word_seq as standard UVC sequences. Otherwise, the sequence files will be included into the testbench.
Summary
This chapter covers the details of developing interface UVCs, which are key building blocks of a verification testbench. Each
UVC follows a consistent architecture and contains a complete set of elements for generating stimulus, as well as checking
correctness and collecting coverage information for a specific design protocol. Following this recommended architecture
will lead to increased productivity because UVCs can be reused for verification at the block level, cluster level and system
level, and can also be reused for follow-on projects with the same interface protocols.
Automating UVC Creation
The following are common reactions most verification engineers have to the encapsulation and hook-up recommendations
of the UVM reuse methodology:
Creating reusable UVCs introduces a lot of overhead.
How many rules do you need to memorize?
We don’t need all that extra encapsulation—it just introduces the opportunity for syntax errors.
Our experience has shown that users who follow the UVM methodology recommendations experience a speed
improvement in their first project and a more significant improvement in follow-up integrations. In addition, the standard
hierarchy and commonality allows further automation. Code generators can leverage the canonical structure and produce
bug-free, well-commented code, with more consistency between components. A compliance checklist and
methodology-level linters can ensure seamless integration and eliminate expensive support or code modifications. This
chapter discusses a technology and development flow that expedites UVC development and usage.
Before we discuss automation, we need to look at the development flow. Protocols specify the handshake requirements of
components. For example, to interact with slave devices you need to implement master component logic. In PCI Express,
to communicate with a downstream device you need an upstream testbench component.
Two approaches can be taken in building verification IP:
Build the verification component against the DUT.
Build the verification IP in isolation and connect it to the DUT once it has been developed.
Developing the UVC in isolation requires implementation of both the device and the testbench logic (master/slave, Tx/Rx,
and so on).
The advantage of building the verification IP against the DUT is that the DUT IP might already be implemented and only the
counter logic needs to be developed. However, usually it is more efficient to use
distributed VIP development. In this development flow, an individual or a team owns developing a UVC for a given protocol.
Even though only a master agent may be needed for the project, both the slave and master agents are developed, checked
against each other, verified that the waveform follows the protocols, and only later is the UVC configured to verify the
project DUT. Figure 6-1 shows this flow.
Config:ACTIVE Config:ACTIVE
Monitor Monitor
APB I/F
Config:ACTIVE Config:PASSIVE
As noted in the introduction to this book, the commonality and the generality of the methodology allow code generators
to produce a lot of initial skeleton code and infrastructure code. The key in using code generators is to ensure that they
generate compliant and agreed-upon code, and support an acceptable level of customization before the unavoidable
manual typing begins.
One example of a good code generator is the Cadence Incisive Verification Builder. The Builder is a wizard technology that
produces UVM standard environments. The user provides initial information about the
desired verification IP environment. Based on the initial input, more queries are asked until the system is ready to produce
a verification environment skeleton. A set of finalization action items is provided to guide users on how to complete the
protocol specific UVC environment. The Builder technology benefits both novice users (there is no need to know all the
guidelines and rules) and experts alike (as a productivity tool that answers the question “ Why should I start from
scratch?"). The actual code produced by the Builder is readable and well-commented, and the implementation is
constantly updated as new methodology features and implementation techniques are introduced. The figures below
illustrate the Builder input forms and the. generated result.
Figure 6-3 Incisive Verification Builder Initial Form for Creating an Interface UVC
Code generators are efficient productivity tools. But if you did not purchase commercial VIP, you still need to go through
the protocol specifications and implement the desired UVC. While template-driven solutions can start a verification
engineer on the right path, it is still easy to stray from the methodology. It is the UVM compliance checklist and associated
automatic checkers that ensure the developer builds, and the integrator receives, UVCs compliant to the methodology,
which leads to reuse without rework.
A compliance checklist is also efficient if commercial VIPs are used. A compliance checklist is needed by an integrator to
qualify commercial VIP and by VIP vendors who want a way to measure and prove to potential users the level of their
methodology compliance.
A UVM compliance checklist is provided by Cadence and is also provided on www.uvmworld.org. These requirements were
collected and tuned over years by many customers on many projects. If the reasoning for a check is not clear, or if you have
an enhancement suggestion, it is best to follow the checklist and in parallel, express your concerns to the UVM
development team.
The UVM compliance checklist contains the following categories:
Packaging and Name Space—Provides guidelines on how to package environments in a consistent way for easy
shipping and delivery
Architecture—Includes checks to ensure similar high-level topology for UVM environments. This is critical for
understanding a new UVC, its configuration, and class hierarchy.
Reset and Clock—Has various reset and clocking topics
Checking—Touches the self checking aspects of reusable UVC
Sequences—Provides good practices on creating a reusable sequence library
Messaging—Defines message methodology to allow for an efficient debug environment and reduces support
requirements
Documentation—Satisfies the requirement for complete documentation
General Deliverable—Includes more delivery requirements
End of Tests—Gives minimal end-of-test requirements
UVM SystemVerilog specific compliance checks—Supplies checks that are specific to UVM SystemVerilog
implementation.
Each check has a unique identification code, which is used for documentation, queries that supply more detail, and
cross-tool consistency.
Many users of the eRM/OVM/UVM who apply the compliance checklist have requested an automated tool for checking
verification IP (VIP) against the checklist. This allows them to use qualifying third-party VIP or internally developed IP.
These tools exist today in the industry. for example, AMIQ, an EDA company that is part of the growing UVM ecosystem,
provides a tool called DVT, which automates UVM compliance checking. DVT is an Eclipse-based integrated development
environment (IDE) targeted at increasing productivity for SystemVerilog and e developers. In addition to UVM compliance
checking, users can also benefit from UVM-dedicated wizards, UVM-simulation log recognition, code and project
templates, and in-line documentation. For more information on DVT, refer to www.dvteclipse.com.
Summary
In this chapter, wc introduce options for automating the task of UVC creation. We discuss the benefits of implementing
both the master and slave testbench logic so that UVCs can be developed independently of the DUT and configured for
project-specific requirements. Then, we show an example of a code generator that produces the standard infrastructure
and connections for a reusable UVC. The generated code follows the consistent UVC architecture, and can be a
productivity tool for both new users to UVM and experienced developers who want to quickly get started. Finally, we
introduce a compliance checklist and compliance-checking tools that allow verification IP developers and project teams to
qualify their code against methodology guidelines.
Simple Testbench Integration
Chapters 5 and 6 covered the process of creating interface UVM-compliant UVCs. The UVM views a testbench as a
collection of reusable components with additional glue logic. An advanced verification environment contains more than
interface UVCs. For example, we recommend adding the DUT verification logic (scoreboard and coverage) in a reusable
module UVC. To gradually introduce the integration tasks, we start in this chapter with simple testbenches that do not use
a module UVC. In Chapter 10 “System UVCs and Testbench Integration” we expand on the concepts covered here and
integrate DUT-related components into a module UVC. In this chapter, a distinction is made between the UVC integrator
and the test writer, who might have less knowledge about verification and wants to create tests on top of an already
existing testbench.
As illustrated in Figure 7-1, A Simple Integrated Testbench Example, the UVC integrator instantiates and configures
reusable components to build a testbench infrastructure. Then tests are created to steer the stimulus generation to visit
interesting areas. In UVM, a test creates the testbench infrastructure, and using the configuration mechanism, can
customize the testbench operation and stimulus generation.
Top(module)
default_sequence test_config
Test(class)
Testbench
(class)
Virtual Config
Sequencer
APB UART
UVC Scoreboard UVC
The testbench defines the verification environment topology. It instantiates the reusable verification IP and defines the
configuration of that IP as required by the application. Why do we need a testbench class instead of creating everything in
a test? Instantiating the reusable components directly inside each test has several drawbacks:
The test writer must know how to instantiate, configure and connect the environment.
Changes to the topology require updating multiple test files, which can become a huge task.
The tests are not reusable because they incorporate a specific environment structure.
Separating the instantiation and hook-up of the environment from the stimulus generation control allows a single
testbench to be used by multiple tests, and allows multiple tests to be used with different testbenches and configurations.
7.1.2 The Test Classes
A UVM test enables configuration of the testbench and interface UVCs, and defines the default scenario for the testbench
to execute. Although the testbench class provides default configuration values for the interface UVCs, a test can modify
both topological and run-time configuration properties as needed. Configuration customization is done via the advanced
UVM configuration mechanism provided in the UVM library. Note that UVM tests are relatively small in content. Much of
the reuse is achieved via a collection of sequences developed as part of the reusable interface components and the
testbench. The test writer uses these sequences as building blocks for each test scenario. The UVC developer can provide
user-defined sequences in a file or package, which is included or imported by the test class. A test provides data and
sequence generation and inline constraints. Test files are typically associated with a single configuration; however, because
the testbench class can be extended and type-overridden by the factory, the same test can be used with different though
similar configurations.
For usage examples of test classes, refer to “Creating a Test” on page 122.
If you search your top-level module, you will not be able find the test class instantiation. This is because the test is
instantiated through the top module in a simple call to a global function called run_test(). The name of the test class is
provided as an argument to the run_test() function or can be specified as a simulator command-line argument. This way,
you can use the command-line selection and the fact that tests are classes, to compile the entire test suite for a regression
run without the need to re-elaborate the testbench or DUT for every test. This global function is described in “Test
Selection” on page 123.
We do not recommend using program blocks in testbenches, as they provide little value in UVM environments. The
semantics of package threads activated within a program block was unclear and resolved only in the SystemVerilog 2009
LRM. At this point, the correct behavior was not implemented on all simulators and will result different behavior on
different simulators.
The steps you need to perform to create a simple testbench using reusable UVCs are:
Identify the UVCs you will need for your testbench and review their configuration parameters and built-in sequence
libraries.
Create a testbench class and instantiate reusable UVCs along with other control and checking logic.
Configure, create, and hook up the testbench components.
Create additional reusable sequences for interface UVCs (optional).
Add a virtual sequencer and create virtual sequences (optional).
Add checking (scoreboard) and functional coverage extensions.
When your testbench is complete, create tests to exercise the sequence sets and achieve coverage goals.
Figure 7-2, Simple UVM Testbench for a UART Controller Device, shows a typical verification environment for a UART
Controller device. It also includes the test class containing the uart_ctrl testbench class.
Top(module)
a2u_u2a_rand_sequence uart_ctrl_cfg
Test
Testbench(UART_CTRL) Config
Apb UVC
Virtual
Uart UVC
Slave (passive) Sequencer Rx (passive)
Master (active) Mon Tx (active)
Seqr Seqr
Mon Rx and Tx
Driver Coll Scoreboard Mon Driver
vif vif
Uart control DUT
AHB Receiver
apb_if I/F Transmiter uart_if
Registers and controls
The build method described above configures both the APB UVC and UART UVC before they are created. Reusable
testbench components have built-in configuration information that can be controlled using the UVM configuration
mechanism.
Based on the protocols used in a device, the integrator instantiates the needed environment classes and configures them
for a desired operation mode. Some standard configuration parameters are recommended to address common verification
needs. Other parameters are protocol- and implementation-specific.
Chapter 5 “Interface UVCs” introduces examples of standard UVC component configuration parameters:
A monitor, collector and env component have two standard configuration fields: checks_enable and coverage_enable.
An agent has an is active field to configure that component as active or passive. In active mode, the agent drives
traffic to the DUT. In passive mode, the agent passively checks and collects coverage for a device. A rule of thumb to
follow is to use an active agent per device that needs to be emulated, and a passive agent for every RTL device that
needs to be verified.
Examples of user-defined parameters:
The env (UVC) may have configuration fields to indicate the number of masters (num_masters), number of slaves
(num_slaves) and if a bus_monitor is present (has_bus_monitor).
A driver, sequencer, monitor and collector may need DUT-specific configuration information to generate and/or
capture transactions. For example, the operation mode of a device, the maximum payload size or the speed of a bus.
A UVM UVC should support the standard configuration parameters and provide user-defined configuration parameters as
needed.
Figure 7-3, Standard Configuration Fields and Locations, shows some of the configuration fields for a typical bus-based
UVM verification component. UVM provides a configuration mechanism to allow integrators to configure an environment
without needing to know the UVC implementation and hook-up scheme. Following are some examples.
Do not collect coverage for slaves [0] agent:
set_config_int("apb0.slaves[0].monitor", "coverage_enable", 0);
Set all APB slaves (using a wildcard) to be passive:
set_config_int("apb0.slaves*", "is_active", OVM_PASSIVE);
Specify the default config class for the APB:
set_config_object("apb0", "apb_config", default_apb_config);
The order of configuration is that the container component always takes precedence (and thus overrides) the contained
component settings. This allows the testbench to override the UVC configuration and allows the tests to override the
testbench settings. Using a configuration class for UVCs is recommended, but this decision is made by the UVC developer.
Environment
Int unsigned num_masters
Intunsigned num_slaves
bit has_bus_monitor
Sequencer Sequencer
sequences sequences
interface interface
interface interface
Monitor Monitor
Driver Driver Monitor
bit coverage_enable bit coverage_enable
bit checks_enable bit checks_enable bit coverage_enable
bit checks_enable
We recommend that UVCs randomize configuration attributes inside a configuration class. Dependencies between these
attributes are captured using constraints within the configuration object. In such cases, users can extend the configuration
class to add new constraints, or layer additional constraints on the class using inline constraints. Once the configuration is
randomized, the test writer can use set_config_object() to assign the configuration object to one or more
environments within the testbench. Similar to set_config_int(), set_config_object() allows you to set the
configuration to multiple environments in the testbench, regardless of their location, and impact the build process of the
testbench .
Configuration classes are also important for vertical reuse. A system configuration class may include two subsystem
configuration classes, with a few extra system-level attributes and constraints. Chapter 5 “Interface UVCs” introduced a
config class for an APB UVC. That code is repeated here for your reference:
Example 7-2 APB Configuration Classes (slave_config, master config and config)
1 class apb_slave_config extends uvm_object;
2 string name;
3 uvm_active_passive_enum is_active = UVM_ACTIVE;
4 rand int start_address;
5 rand int end_address;
6 rand int psel_index;
7 constraint addr_cst { start_address <= end_address; }
8 constraint psel_cst { psel_index inside {[0:15]}; }
9 // UVM object_utils, field macros and constructor
10 extern virtual function bitcheck_address_range(int unsigned addr);
11 endclass : apb_slave_config
12 class apb_master_config extends uvm_object;
13 string name;
14 uvm_active_passive_enum is_active = UVM_ACTIVE;
15 // UVM object utils, field macros and constructor syntax
16 endclass : apb_master_config
17 class apb_config extends uvm_object;
18 // APB has one master and N slaves
19 rand apb_mastex_config master_config;
20 rand apb_slave_config slave_configs[$];
21 // constraints on the composite configuration
22 //UVM object_utils, field macros and constructor
23 extern function void add_slave(string name, int start_addr,int psel_indx,
uvm_active_passive_enum is_active=UVM_ACTIVE) ;
24 extern function int get_slave_psel_by_addr(int addr) ;
25 endclass : apb_config
The next example shows a subset of the config class for a UART interface UVC which also is used to configure the UART
controller device:
In UVM, a test is a class that encapsulates test-specific instructions written by the test writer. This section describes how to
create and select a test. It also describes how to create a test family base class to verify a topology configuration.
Tests in UVM are classes that are derived from an uvm_test base class. Using classes, instead of modules or program
blocks, allows inheritance and reuse of tests.
The following example shows a base test that uses the uart_ctrl_tb defined in “UART Controller Testbench” on page 118.
This base test is a starting point for all derivative tests that will use the uart_ctri_tb. The complete test class is shown here:
1 class uart_ctrl_base_test extends uvm_test;
2 uart_ctrl_tb uart_ctrl_tb0; //Testbench Instance
3 `uvm_component_utils(uart_ctcl_base_test)
4 // Update this component’s properties and create the testbench.
5 virtual function void build ();
6 super.build();
7 uart_ctrl_tb0 = uart_ctrl_tb::type_id::create("uart_ctrl_tb0", this);
8 endfunction
9 // Constructor
10 function new (input string name, uvm_component parent = null);
11 super.new(name, parent);
12 endfunction
13 endclass : uart_ctrl_base_test
The build() function of the base test creates the uart_ctrl_tb. The System Verilog UVM Class Library will execute the build()
function of the uart_ctrl_base_test for the user when cycling through the
simulation phases of the components. This creates the testbench environment because each sub-component will create
components that will create more components in their build() functions.
All of the definitions in the base test will be inherited by any test that derives from uart_ctrl_base_test. This means that
any derivative test will not have to build the testbench if the test calls super.build(). Likewise, the run() task behavior can be
inherited. If the current implementation does not meet the needs of a specific test, you can redefine both the build() and
run() methods because they are both virtual.
You can derive from the base test defined in “Creating the Base Test” on page 121 to create tests that reuse the same
topology. Because the testbench is created by the base test’s build() function and the run() task defines the run phase, the
derivative tests can make minor adjustments. (For example, changing the default sequence executed by the agents in the
environment.) Below is an example of a simple test that inherits from uart_ctrl_base_test.
1 class u2a_a2u_rand_test extends uart_ctrl_base_test;
2 `uvm_component_utils(u2a_a2u_rand_test)
3 virtual function void build();
4 super.build();
5 // Substitute the default sequences for the APE and UART OVCs.
6 set_config_string("uart_ctrl_tb0.apb0.master.sequencer","default_sequence",
"apb_write_to_uart_seq");
7 set_config_string ("uart_ctrl_tb0. uart0. Tx.sequencer","de
8 fault_sequence", "uart_write_to_apb_seq");
9 endfunction
10 //Constructor ...
11 endclass : u2a_a2u_rand_test
This test changes the default sequence executed by the APB master agent and the UART Tx agent. It is important that the
settings for the default_sequence be set after calling super.build(), which allows the set_config directives of this class to
override the base_test set_config directives. When super. build() is called, the uart_ctrl_tb0 and all of its sub-components
are created, if you need to set configuration items for the testbench, these set_config_* calls must be placed before
the super.buid() call.
After you have declared a user-defined test (described in “Creating a Test Library Using a Base Test” on page 123), invoke
the global UVM run_test () task in the top-level module to select a test to be simulated. Its prototype is:
task run_test(string test_name="");
When a test name is provided to the run_test () task, the factory is called to create an instance of the test with that type
name. Simulation then starts and cycles through the simulation phases.
A test name is provided to run_test() via a simulator command-line argument. If the top module calls run test()
without an argument, the +UVM_TESTNAME=test_name simulator command-line argument is checked. If present,
run_test() will use test. name. Using the simulator command-line argument avoids having to hard-code the test name
in the run_test() task. For example, in the top-level module, call the run_tes () as follows:
module uart_ctrl_top;
// DUT, interfaces, and all non-testbench code
initial
run_test();
endmodule
To select a test of type u2a_a2u_rand_test (described in “Creating a Test Library Using a Base Test” on page 124) using
simulator command-line option, use the following command:
% simulator-command other-options +UVM_TESTNAME=u2a_a2u_rand_test
If the test name provided to run_test() does not exist, the simulation will exit immediately via a call to $fatal. If this occurs,
it is likely the name was typed incorrectly or the `uvm_component_utils macro was not used.
By using this method and only changing the +UVM_TESTNAME argument, you can run multiple tests without having to
recompile or re-elaborate the design or testbench.
The previous sections show how test classes are put together. At this point, random traffic is created and sent to the DUT.
The user can change the randomization seed to achieve new test patterns. To achieve verification goals in a systematic way,
the user will need to control test creation to cover specific areas.
Users can control test creation as follows:
Add constraints to control individual data items. This method provides basic functionality. It is described in
“Constraining Data Items” on page 123.
Use UVM sequences to control the order of multiple data items. This method provides more flexibility and control. It
is described in “Sequences and Sequencer Control” on page 125.
Use the configuration mechanism to override a configuration attribute that changes a UVC or testbench configuration.
Use a UVM callback to modify the code.
By default, sequencers repeatedly generate random data items. At this level, the test writer can control the number of
generated data items and add constraints to data items to control their generated values.
To constrain data items:
1. Identify the data item classes and their generated fields in the UVC.
2. Create a derivation of the data item class that adds or overrides default constraints.
3. In a test, adjust the environment (or a subset of it) to use the newly-defined data items.
4. Run the simulation using a command-line option to specify the test name.
The following example, uart_frame, is created by the UART UVC developer.
In tests, the user may want to change the way data items are generated. For example, the test writer may want to have
short delays. This can be achieved by deriving a new data item class and adding constraints or other class members as
needed.
1 //A derived data item example
2 class short_delay_frame extends uart_frams;
3 // This constraint further limits the delay values.
4 constraint c_testl_txmit_delay {transmit_delay < 10;}
5 // Constructor and uvm_object_utils macro
6 endclass: short_delay_frame
Deriving the new class is not enough to get the desired effect. You also need to have the environment use the new class
(short_delay_frame) rather than the UVC frame. The UVM factory mechanism allows you to introduce the derived class to
the environment without the need to rewrite or change anything in it.
1 class short_delay_test extends uvm_test;
2 uart_ctrl_tb uart_ctrl_tb0;
3 // Constructor and uvm_object_utils macro
4 virtual function void build();
5 super.build();
6 // Use short_delay_frame throughout the environment.
7 factory.set_type_override_by_type(uart_frame::get_type
(),short_delay_frame::get_type());
8 uart_ctrl_tb0 = uart_ctrl_tb: :type_id::create("uart_ctrl_tb0", this);
9 endfunction
10 virtual task run();
11 uvm_top.print_popology();
12 endtask
13 endclass: short_delay_test
Calling the factory function set_type_override_by_type() (in bold above) instructs the environment to use short-delay
frames.
At times, a user may want to send special traffic to one interface but keep sending the regular traffic to other interfaces.
This can be achieved by using set_inst_override_by_type() inside an UVM component.
set_inst_override_by_type("uart 0.Tx.sequencer.*",
uart_frame::get_type(5, short_delay_frame::get_type());
You can also use wildcards to override the instantiation of a few components.
set_isnt_override_by_type("uart*.Tx.sequencer.*",
uart_frame::get_type(), short_delay_frame::get_type());
7.5.2 Sequences and Sequencer Control
Constraint layering is an efficient way of uncovering bugs in your DUT. Having the constraint solver randomly select values
ensures a non-biased sampling of the legal input space. However, constraint layering does not allow a user to control the
order between consecutive data items. Many high-level scenarios can only be captured using a stream of ordered
transactions.
User-defined sequences can be added to the sequencer’s sequence library and randomly executed. If no user-defined
sequences are added, then the only executed sequence is the built-in sequence called uvm_random_sequence. The
uvm_random sequence executes a random number of the simple_sequence that executes a single data item.
This section introduces ways to control the sequencer, including:
Controlling the number of sequences created by uvm_random_sequence
Creating and adding a new sequence to be executed
Adjusting the sequencer to start from a sequence other than the predefined random sequence
The default number of generated sequences is a random number between 0 and uvm_sequencer::max_random_count.
The user can modify the number of generated sequences (count). Use the configuration mechanism to change the value of
count. For example, to generate and send 10 sequences, use:
set_config_int ("*.cpu_seqr", "count", 10);
You can disable a sequencer from generating any sequences by setting the count to 0.
set_config_int("*.cpu_seqr" , "count", 0);
Note Having more data items than count is not necessarily a bug. The sequencer does not generate data items directly.
By default, it generates count number of simple sequences that translate into count number of items. If user-defined
sequences are registered to the sequencer, they are executed by the uvm_random_sequence and can result in many
more data items than the sequencer count. The sequencer has more built-in capabilities, which are described in the next
section.
The three built-in sequences were introduced in “Predefined Sequences” on page 93.
In “User-Defined Sequences” on page 91, we described how to create sequences. Some sequences are created by the UVC
developer and provided as part of the reusable UVC package.
The testbench developer can create additional sequences for specific tests, or for random selection and execution by the
uvm_random built-in sequence.
The class retry_seq in the example below defines a new sequence. It is derived from uvm_sequence and uses the
`uvm_sequence_utils macro to associate this sequence with uart_tx_sequencer and to declare the various utilities
`uvm_object_utils provides.
You can define more abstract sequences using existing sequences. Doing so provides additional reuse and makes it easier
to maintain the test suite. For example, after defining the configuration sequence per device in a block-level testbench, the
user may define a system-level configuration sequence which is a combination of the already-defined sequences.
As was described in Chapter 5 “Interface UVCs”, the sequencer has a string property named default_sequence which can
be set to a user-defined sequence type. This sequence type is used as the default sequence for the current instance of the
sequencer (Figure 7-4, Sequencer with a Sequence Library). The sequencer starts the default sequence during the run
phase, and allows it to finish, after which the sequencer is idle. By setting the default sequence, you control the root of the
sequence tree that will be generated by that sequencer through the entire test.
To override the default sequence, configure the default_sequence property for a specific sequencer or a group of
sequencers. This is typically done inside the test class, before creating the component that includes the relevant
sequencer(s). For example,
set_config_string("*.Tx.sequencer", "default_sequence","retry_seq");
The first argument uses a wildcard to match any instance name containing .master0. sequencer to set the
default_sequence property (if it exists) to the value retry_seq.
Sequencer
uvm_random
default_sequence uvm_exhaustive
uvm_simple
retry_seq
rand_retry_seq
interface
Sequencer
uvm_random
default_sequence uvm_exhaustive
uvm_simple
retry_seq
rand_retry_seq
interface
Use of sequences is an important part of UVC reuse. The environment developer who knows and understands the UVC
protocol specifications can create interesting parameterized reusable sequences. This library of sequences enables the
environment user to leverage interesting scenarios to achieve coverage goals more quickly. Check to see if your UVCs
sequencer comes with a library of sequences. The example below shows the output of a sequencer. print () statement.
In some cases, you may need to disable a sequencer. This may be required for reset purposes or for virtual sequence
operation in which a system-level sequence controls the sequencer. The safest way to deactivate a sequencer is by calling
the stop_sequences() function of the sequencer. This function kills the active sequences and child sequences, and removes
all requests, locks, and responses that are currently queued.
To disable a sequencer for the entire simulation, we recommend:
Deriving from uvm_sequencer and adding an enable_default_sequence configurable bit field
Overriding the start_def ault_sequence() task to do:
virtual start_default_sequenee();
if (enable_default_sequence) super.start_default_sequence();
endtask
If you want to disable a sequencer for the entire simulation, and the start_default_sequence was not extended, you need
to call the stop_sequences() after it was started. For example, if a specific testbench calls for an idle sequencer in one of
the interfaces, it can call the stop_sequences() function in the run phase.
28 class uart_ctrl_tb extends uvm_env;
29 // UVC Components
30 apb_env apb0; // APB UVC
31 uart_env uart0; // UART UVC
32 ...
33 virtual task run() ;
34 #0;
35 apb0.master.sequencer.stop_sequences() ;
36 endtask: run
37 endclass : uart_ctrl_tb
Line 7: We add a #0 delay to make sure that the sequencer started before we disable it.
Note We recommend stopping the sequencer using stop_sequences() and not using the count, since user-defined
The sequence style discussed in “Sequences and Sequencer Control” on page 125 is the recommended way to create and
execute tests. Focus is placed on creating reusable sequences that you can use across many tests, instead of placing
stimulus scenarios directly inside the test. Each sequencer is pre-loaded with the default traffic that will be generated at
run time and sent to the DUT. Inside the tests, the test writer needs to touch only the sequencers that need to be
modified.
Some test writers, however, are accustomed to writing directed tests. In directed tests, you write procedural code in which
you explicitly request each interface to generate and send items. While directed tests are not the recommended method
of test creation, the UVM does support it, using the sequencer’s execute_item() task. Before using directed tests, consider
the following:
Directed tests require more code to write and maintain. This becomes critical in system-level environments.
In directed tests, the high-level intention of the code is not as clear or as easy to read and understand. In the
recommended method, the code is focused on test-specific needs and other system-related aspects are present by
default. For example, the arbitration logic for slaves that service requests does not need to be coded in every test.
Directed tests are less reusable because they contain specific and un-reusable information.
In the recommended method, tests are random by default. All declared sequences are candidates for execution by
default. You must explicitly exclude a sequence from being executed. This prevents the problem of missing sequences
and creates a more random pattern that can expose more bugs.
In the recommended method for many protocols, you should never have to touch the high-level sequence, which
serves as a template for other sub-sequences to be executed in a certain order.
Notes
The execute_item() task can execute a data item or a sequence. It blocks until the item or the sequence is executed by
the sequencer. You can use regular SystemVerilog constructs such as fork/join to model concurrency.
The default activity in the sequencers is disabled by setting the count parameters of all sequencers to 0. The
execute_item() task is used to send traffic in a deterministic way.
Using default random activity is a good practice. It is straightforward and a good investment. The use of
execute_item() should be minimized and limited to specific scenarios.
“Creating Meaningful Tests” on page 124 describes how to efficiently control a single-interface UVC generation pattern.
However, in a system-level environment multiple components are generating stimuli in parallel. The user might want to
coordinate timing and data between the multiple channels. Also, a user may want to define a reusable system-level
scenario. Virtual sequences are associated with a virtual sequencer and are used to coordinate stimulus generation in a
testbench hierarchy. In general, a virtual sequencer contains references to the testbench sub-sequencers, that is,
sequencers or other virtual sequencers in which it will invoke sequences. Virtual sequences can invoke other virtual
sequences associated with their sequencer, as well as sequences in each of the sub-sequencers. Virtual sequences can
execute items on other sequencers that can execute items.
Virtual sequences enable centralized control over the activity of multiple verification components that are connected to
the various interfaces of the BUT, By creating virtual sequences, you can easily reuse existing sequence libraries of the
underlying interface components and block-level environments to create coordinated system-level scenarios.
In Figure 7-5, Virtual Sequencer Controlling Multiple UVCs, the virtual sequencer invokes sequences on the APB and UART
UVCs. The configuration sequences are developed during block-level testing.
Testbench(UART_CTRL) Config
Virtual Sequencer
u2a_a2u_seq
config_seq
Apb UVC Uart UVC
Slave (passive)
u2a_seq Tx (active)
Master (active)
Seqr Seqr
Mon a2u_seq
Driver Coll Mon Driver
vif vif
For high-level control of multiple sequencers from a single sequencer, use a sequencer that is not attached to a driver and
does not process items itself. A sequencer acting in this role is referred to as a virtual sequencer.
To create a virtual sequencer that controls several sub-sequencers:
Derive a virtual sequencer class from the uvm_sequencer class.
Add references to the sequencers on which the virtual sequences will coordinate the activity. These references will be
assigned by a higher-level component (typically the testbench).
The following example declares a virtual sequencer with two sub-sequencer references (apb_master_sequencer and
uart_sequencer), which will be hooked up to the actual sub-sequencers.
Note The uvm_update_sequence_lib macro is used in the constructor when defining a virtual sequencer. This is different
from (non-virtual) sequencers, which have an associated data item type. When this macro is used, the uvm_simple
sequence is not added to the sequencers sequence library. This is important because the simple sequence only does items,
and a virtual sequencer is not connected to a driver that can process the items. For regular sequencers, use the
`uvm_update_sequence_lib_and_item macro. See “Creating the Sequencer” on page 70 for more information.
Sub-sequencers can be regular sequencers or other virtual sequencers. The connection of the actual sub-sequencer
instances via reference shown in “Connecting a Virtual Sequencer to Sub-Sequencers” on page 133.
Creating a virtual sequence is similar to creating an interface UVC sequence, with the following differences:
A virtual sequence uses `uvm_do_on or `uvm_do_on_with to execute sequences on any of the sub-sequencers
connected to the current virtual sequencer.
A virtual sequence uses `uvm_do or `uvm_do_with to execute other virtual sequences of this virtual sequencer. A
virtual sequence cannot use `uvm_do or `uvm_do_with to execute items. Virtual sequencers do not have items
associated with them, only sequences.
To create a virtual sequence:
1. Declare a sequence class by deriving it from uvm_sequence, just like a regular sequence.
2. Define a body() method that implements the desired logic of the sequence,
3. Use the `uvm_do_on (or `uvm_do_on_with) macro to invoke sequences in the underlying sub-sequencers.
4. Use the `uvm_do (or `uvm_do_with) macro to invoke other virtual sequences in the current virtual sequencer.
The following example shows a UART controller virtual sequence controlling two sub-sequencers: an APB bus sequencer
and an UART sequencer. Assume that the APB sequencer has a config_seq and an a2u_seq sequence in its library, and the
UART sequencer provides a u2a_seq sequence in its library. The following sequence example invokes these three
sequencers.
When using a virtual sequencer, you need to consider how you want the sub-sequencers to behave in relation to the
virtual sequence behavior being defined. There are three basic possibilities:
Business as usual-You want the virtual sequencer and the sub-sequencers to generate traffic at the same time, using
the built-in capability of the original sub-sequencers. The data items resulting from the sub-sequencers’default
behavior—along with those injected by sequences invoked by the virtual sequencer—will be inter-mixed and
executed in an arbitrary order by the driver. This is the default behavior, so there is no need to do anything to achieve
this.
Disable the sub-sequencers—Using the stop_sequences () function of the sequencer as described in “Disabling a
Sequencer” on page 129. The following code snippet disables the sub-sequencers in the example in “Connecting a
Virtual Sequencer to Sub-Sequencers” on page 133.
Use grab () and ungrab () —Using grab () and ungrab (), a virtual sequence can achieve full control over its
sub-sequencers, fur a limited time, and then let the original sequences continue working.
Note Only (non-virtual) driver sequencers can be grabbed. Therefore, you should make sure that a given sub-sequencer
is not a virtual sequencer before you attempt to grab it. The following example illustrates this using the functions grab ()
and ungrab () in the sequence consumer interface.
18 virtual task body();
19 // Grab the apb sequencer if not virtual,
20 if (p_sequencer.apb_seqr != null)
21 p_sequencer.apb_seqr.grab(this) ;
22 // Execute a sequence.
23 `uvm_do_on(config_seq, p_sequencer.apb_seqr)
24 // Ungrab.
25 if (p_sequencer.apb_seqr != null)
26 p_sequencer.apb_seqr.ungrab(this);
27 endtask
Note When grabbing several sequencers, work conventionally to avoid deadlocks. For example, always grab in a standard
order.
To connect a virtual sequencer to its sub-sequencers, assign the sequencer references specified in the virtual sequencer
to instances of the sequencers. This is a simple reference assignment and should be done only after all components are
created. For example, this can be done with the testbench connect () method:
virtual_sequencer.apb_seqr = apb0.master.sequencer;
virtual_sequencer.usrt_seqr = uart0.Tx.sequencer;
The following more-complete example shows a top-level testbench, which instantiates the UART and APB components and
the virtual sequencer that controls the two. At the testbench level, the path to the sequencers inside the various
components is known and that path is used to get a handle to them and connect them to the `virtual sequencer.
Example 7-10 UART Controller Testbench with Virtual Sequencer
1 class uart_ctrl_tb extends uvm_env;
2 apb_env apb0; // APB UVC
3 uart_env uart0; // UART UVC
4 uart_ctrl virtual sequencer virtual_sequencer;
5 ... // Constructor and UVM automation macros
6 virtual function void build();
7 super.build();
8 // Configuration: Disable subsequencer sequences.
9 set_config_int("apb0.master.sequencer", "count", 0);
10 set_config_int("uart0.Tx.sequencer", "count", 0);
11 // Build envs with subsequencers.
12 apb0 = apb_env::type_id::create("apb0", this);
13 uart0 = uart_env::type_id::create("uart0", this);
14 // Build the virtual sequencer.
15 virtual_sequencer =
uart_ctrl_virtual_sequencer::type_id::create("virtual_sequencer", this);
16 endfunction : build
17 // UVM connect() phase.
18 function void connect();
19 virtual_sequencer.apb_seqr = apb0.master.sequencer;
20 virtuai_sequencer,uart_seqr = uart0.Tx.sequencer;
21 endfunction : connect
22 endclass: uart_ctrl_tb
Getting the device into desired states is a significant part of verification. The environment should verity valid responses
from the DUT before a feature is declared verified. Two types of auto-checking mechanisms can be used:
Assertions—Derived from the specification or from the implementation and ensure correct timing behavior.
Assertions typically focus on signal-level activity.
Data checkers—Ensure overall device correctness.
As we mentioned in “Monitor” on page 16, checking and coverage should be done in the monitor regardless of the driving
logic. Reusable assertions are part of reusable components. Designers can also place assertions in the DUT RTL. Refer to
your ABV documentation for more information.
7.7.1 Scoreboards
A crucial element of a self-checking environment is the scoreboard. Typically, a scoreboard verifies the proper operation of
your design at a functional level. The responsibility of a scoreboard varies greatly depending on the implementation. This
section shows an example of a scoreboard that verifies the uart_ctrl transmit and receive paths. While the details of the
scoreboard functionality are specific to the UART controller device, you should focus on the steps necessary to create and
use a scoreboard in an environment so those steps can be repeated for any scoreboard application.
7.7.1.1 UART Controller Scoreboard Example
Testbench(UART_CTRL) Config
Before the scoreboard can be added to the uart_ctrl_tb, the scoreboard component must be defined. To define the
scoreboard:
1. Add the TLM export necessary to communicate with the environment monitor(s).
2. Implement the necessary functions and tasks required by the TLM export.
3. Define the action taken when the export is called.
In the example shown in Figure 7-6 on page 134, each scoreboard requires two ports to communicate with the
environment. Since the monitors in the environment have provided an analysis port write () interface via the TLM
uvm_analysis_port(s), the scoreboard will provide the TLM uvm_analysis_imp.
The uart_ctrl_tx_scbd component derives from the uvm_scoreboard and declares and instantiates two analysis_imp ports.
For more information on TLM interfaces, see “TLM Interfaces” in the UVM Class Reference. The declaration and creation is
done inside the constructor
1 // Declaration of TLM analysis imp ports
2 `uvm_analysis_imp_decl(_apb)
3 `uvm_analysis_imp_decl(_uart)
4 class uart_ctrl_tx_scbd extends uvm_scoreboard;
5 uvm_analysis_imp_apb #(apb_transfer, uart_ctrl_tx_scbd) apb_match;
6 uvm_analysis_imp_uart #(uart_frame, uart_ctrl scbd) uart add;
7 ...
8 function new (string name, uvm_componenL parent);
9 super.new(name, parent);
10 apb_match = new("apb_match", this);
11 uart_add = new("uart_add", this);
12 endfunction : new
13 ...
Line 5 declares the uvm analysis_imp for the APB connection. The parameter, apb_transfer, defines the uvm object
communicated via this TLM interface. The second parameter defines the type of this implementation’s parent. This is
required so that the parent’s write () method can be called by the export.
Line 10 creates the implementation instance. The constructor arguments define the name of this implementation instance
and its parent.
Because the scoreboard provides an uvm_analysis_imp, the scoreboard must implement all interfaces required by that
export. This means you must define the implementation for the write virtual function. For the uart_ctrl_tx_scbd, you will
need two write () functions, one for each imp declaration:
virtual function void wrxte_uart(uart_frame frame);
// frame.payload to the queue for checking;
endfunction : write_uart
virtual function void write_apb(apb_transfer transfer);
...// Retrieve frame data from the queue and compare against transfer.data
endfunction : write_uart
The implementation functions make the appropriate calls and comparisons needed to add and check the result. This
information is DUT specific and is not crucial to the communication of the scoreboard with the rest of the environment and
will not be discussed. The uart_ctrl_scoreboard. sv file shows the implementation.
Once the scoreboard is defined, the scoreboard can be added to the testbench. First, declare the TX and RX scoreboards
inside the uart_ctrl_tb class.
uart_ctrl_tx_scbd tx_scbd
uart_ctrl_rx_scbd rx_scbd;
After the scoreboard is declared, you can construct the scoreboard inside the build () phase:
function void uart_ctrl_tb::build();
tx_scbd = uart_ctrl_tx_scbd::type_id:screate(”tx_scbd", this);
rx_scbd = uart_ctrl_rx_scbd::type_id::create(”rx_scbd", this);
endfunction
Here, the scoreboards are created using the create () function and given the names tx_scbd and rx_scbd.
After the scoreboard is created, the uart_ctrl_tb can connect the ports in the testbench,
function void uart_ctrl_tb::connect();
uart0.Rx.monitor.frame_collected_port.connect(rx_scbd.uart_match);
apb0.bus_monitor.item_collected_port.connect(rx_scbd.apb_add);
uart0.Tx.monitor,frame_colleeted_port.connect(tx_scbd.uart_add);
apb0.busjmonitor.item_collected_port.connect(tx_scbd.apb_match);
endfunction
This uart_ctrl_tb’s connect() function code makes the connection, using the TLM ports connect() interface, between the
port in the monitor of the APB and UART agents inside the 0 environment and the implementation in the scoreboards. For
more information on the use of TLM ports, see “TLM Interfaces” in the UVM Class Reference.
To ensure thorough verification, you need observers to represent your verification goals. SystemVerilog provides a rich set
of functional coverage features.
No single coverage metric ensures completeness. There are two categories of coverage:
Explicit coverage is user-defined coverage. The user specifies the coverage goals, the needed values, and collection
time. As such, analyzing these goals is straightforward. Achieving 100% of your coverage goals means that your
verification has been completed. An example of such a metric is SystemVerilog functional coverage. The disadvantage
of such metrics is that missing goals are not taken into account.
Implicit coverage is done with automatic metrics that are derived from the RTL or other metrics already existing in the
code. Typically, creating an implicit coverage report is straightforward and does not require a lot of effort. For example,
code coverage, expression coverage, and FSM (finite-state machine) coverage are types ofimplicit coverage. The
disadvantage of implicit coverage is that it is difficult to map the coverage requirements to the verification goals. It
also is difficult to map coverage holes into unexecuted high-level features. In addition, implicit coverage is not
complete, since it does not take into account high-level abstract events and does not create associations between
parallel threads (that is, two or more events occurring simultaneously).
Starting with explicit coverage is recommended. You should build a coverage model that represents your high-level
verification goals. Later, you can use implicit coverage as a “safety net” to check and balance the explicit coverage.
Note Reaching 100% functional coverage with very low code-coverage typically means that the functional coverage
A UVC should come with a protocol-specific functional-coverage model. You may want to disable some coverage aspects
that are not important or do not need to be verified. For example, you might not need to test all types of bus transactions
in your system, or you might want to remove that goal from the coverage logic that specifies all types of transactions as
goals. You might also want to extend the functional coverage model and create associations between the UVC coverage
and other attributes in the system or other interface UVCs. For example, you might want to ensure proper behavior when
all types of transactions are sent and the FIFO in the system is full. This would translate into crossing the transaction type
with the FIFO status variable. This type of functional coverage is discussed in Chapter 10 “System UVCs and Testbench
Integration”.
The verification IP developer should provide configuration properties that allow you to control the interesting aspects of
the coverage (see “Enabling and Disabling Coverage and Checks” on page 105). The VIP documentation will tell you what
properties can be set to affect coverage. The most basic of controls would determine whether coverage is collected at all.
The APB monitors and collectors demonstrate this level of control. If the you want to disable coverage before the
environment is created, use the set_config_int() interface.
set_config_int("apbO.master.monitor", "coverage_enable", 0);
set_ccmfig_ini: ("apb0.master.collector", "coverage_enable", 0);
Once the environment is created, you can set this property directly.
apb0.master.monitor,coverage_enable = 0;
apb0.master.collector.coverage_enable = 0;
This is a simple Verilog assignment to a class property (or variable).
Summary
This chapter covers basic UVC integration concepts. The integration in this chapter is not reusable and is introduced here
for educational purposes and phased introduction of the material. In Chapter 10 “System UVCs and Testbench Integration”
we introduce a more appropriate integration component.
Stimulus Generation Topics
The UVM sequencer and sequences provide an efficient solution for many basic and advanced modeling needs. A subset of
the sequences capability was introduced in Chapter 5 “Interface UVCs” and in Chapter 7 “Simple Testbench Integration”.
Much of the UVC reuse capability is due to the sequence feature, as many UVCs from different application domains use
sequences for creating the required stimuli. This section discusses various techniques for sequence and randomization
control.
In “User-Defined Sequences” on page 91, we introduce two sequence macros: `uvm_do and `uvm_do_with. These
macros perform several steps sequentially, including the allocation of an object (sequence or sequence item),
synchronization with the driver (if needed), randomization, sending to the driver, and so on. In a typical testbench, the
macros provide all of the required functionality and ensure that reactive generation, factory usage, and TLM are being
used. Occasionally, you might need more control over the way sequences and items are created, randomized, and sent.
The SystemVerilog UVM Class Library provides additional sequence macros to enable finer control of these steps. This set
of macros enables important use cases such as:
Randomizing a sequence item without committing to drive it
Avoiding the re-allocation of a sequence item
Allowing procedural assignments of sequence items, reading items from a file, and much more The following sections
describe these macros in detail.
`uvm_create
The `uvm_create macro allocates an object using the common factory and initializes its properties. Its argument is a
variable of type uvm_sequence_item or uvm_sequence. You can use this macro with the SystemVerilog
constraint_mode() and rand_mode() functions in order to enable or disable constraints.
In the following example, apb_byte_seq is similar to sequences that have been discussed previously. The main difference
involves the use of the `uvm_create(req) call.
After the macro call, the rand_mode() and constraint_mode() functions can be used, as well as some direct assignments to
properties of req. The manipulation of the req object is possible since memory has been allocated for it, but randomization
has not yet taken place. Subsequent sections review the possible options for sending this pre-generated item to the driver.
1 class apb_byte_seq extends uvm_sequence #(apb_transfer);
2 rand bit [31:0] start_addr;
3 ... // Constructor and UVM automation macros
4 virtual task body() ;
5 `uvm_create(req)
6 req.addr.rand_mode(0); // Disables randomization of addr
7 req.c_addr.constraint_mode(0); // Disables constraint c_addr
8 req.addr = start_addr;
9 ...// Additional sequence macros to follow
10 endtask : body
11 endclass: my_seq
You can also use a sequence variable as an argument to `uvm_create.
Note Sometimes you need to provide an object as a parameter to a sequence (for example, a user may want a
configuration object to a certain sequence to affect its behavior). Specifying an object as a constraint value is not
supported in SystemVerilog. Users can use `uvm_create, assign the object to the sequence field, randomize the object,
and send it.
`uvm_send
The `uvm_send macro processes the uvm_sequence_item or uvm_sequence class handle argument without any
allocation or randomization. Sequence items are placed in the sequencers queue to await processing, while sequences are
processed immediately. The parent sequence’s pre_do(), mid_do(), and post_do() methods still occur.
In the following example, we show the use of `uvm_create() to pre-allocate a sequence item along with `uvm_send,
which processes it without allocation or randomization.
1 class apb_write_byte_seq extends uvm_sequence #(apb_transfer);
2 rand bit [31:0] start_addr;
3 rand bit [7:0] byte_data;
4 ...// Constructor and UVM automation macros
5 virtual task body();
6 `uvm_create (req)
7 req.addr = start_addr;
8 req.data = byte_data;
9 req.direction == APB_WRITE;
10 //No randomization. Use a purely pre-generated item.
11 `uvm_send(req)
12 endtask : body
13 endclass: apb_write_byte_seq
Similarly, a sequence variable could be provided to the `uvm_create and the `uvm send calls, above, in which case
the sequence would be processed without allocation or randomization.
`uvm_rand_send and `uvm_rand_send_with
These macros are identical to the `uvm_send macro, with the single difference of randomizing the given class handle
before processing it. This enables you to adjust an object as required while still using class constraints with late
randomization; that is, randomization on the cycle for which the driver is requesting the item.
The`uvm_rand_send() macro takes just the object handle, while the `uvm_rand_send_with() macro takes an
extra argument, which can be any valid inline constraint to be used for the randomization.
The following example shows the use of `uvm create to pre-allocate a sequence item along with the `uvm_rand_send*
macros. The `uvm_rand_send* macros process an item or a sequence previously allocated using `uvm_create. The
rand_mode() and constraint_mode() constructs are used to show fine-grain control on the randomization of an
object.
1 class apb_write_read_byte_seq extends uvm_sequence #(apb_transfer); ...//
Constructor and UVM automation macros
2 virtual task body();
3 `uvm_create(req)
4 req.direction.rand_mode(0); // don’t randomize direction
5 req.addr_c.constraint_mode(0) ; //disables constraint on addr[1:0]
6 req.direction = APB_WRITE;
7 // Randomize and process the item.
8 `uvm_rand_send_with(req, {data[31:8] = 24’h000000;})
9 req.addr.rand_mode(0); // don’t randomize addr
10 req.direction = APB_READ;
11 // Randomize and process again, this time with inline constraints.
12 `uvm_rand_send(req)
13 endtask : body
14 endclass: apb_write_read_byte_seq
Executing Sequences and Items on Other Sequencers: uvm_do_on, `uvm_do_on_with,
uvm_do_on_pri, `uvm_do_on_pri_with
In the preceding sections, all uvm_do macros (and their variants) execute the specified item or sequence on the current
p_sequener. To allow sequences to execute items or other sequences on specific sequencers, additional macro variants,
which are described in the following section, are included. These macros allow for the specification of a desired sequencer.
All of these macros are exactly the same as their root versions, except that they all take an additional argument (always the
second argument) which is a reference to a specific sequencer.
`uvm_do_on(s_seq, that_sequencer);
`uvm_do_on_with(s_seq, that_sequencer, {s_seq.foo == 32’h3;})
These macros are used in virtual sequences, as shown in Example 7-8 on page 131.
More details about the UVM sequence macros can be found in the UVM SystemVerilog Reference Manual.
Because the body() sequence is a task, it allows spawning and waiting for threads. In this example, the sequences are
executed with fork/join. The simulator schedules the sequences that request interaction with the sequencer. The
sequencer schedules the items that are provided to the driver and selects them one at a time, arbitrating between the
sequences willing to provide an item for execution. The write_seq and read_seq sequences are sub-sequences of
fork_join_do_seq.
1 class fork_join_do_seq extends uvm_sequence #(apb_transfer);
2 ...//Constructor and `uvm_sequence_utils macro go here.
3 rand_write_seq write_seq;
4 rand_read_seq read_seq;
5 virtual task body();
6 fork
7 `uvm_do (write_seq)
8 `uvm_do(read_seq)
9 join
10 endtask : body
11 endclass : fork_join_do_seq
In this example, the concurrent_start_seq sequence activates two sequences in parallel. The process does not wait for the
sequences to complete. Instead, it immediately finishes after activating the sequences. Also, the write_seq and read_seq
sequences are started as root sequences because no parent sequence reference is passed to the start() task.
1 class concurrent_start_seq extends uvm_sequence #(apb_transfer);
2 ... // Constructor and `uvm_sequence_utils macro go here.
3 rand_write_seq write_seq;
4 rand_read_seq read_seq;
5 virtual task body();
6 // Create the sequences using the factory.
7 `uvn_create(write_seq)
8 `uvm_create(read_seq)
9 fork // Start each subsequence as a new thread.
10 write_seq.start(p_sequencer);
11 read_seq.start(p_sequencer);
12 join
13 endtask : body
14 endclass : concurrent_start_seq
Note The sequence.start() method allows the sequence to be started on any sequencer.
8.3 Using p_sequencer
We discuss p_sequencer in a few chapters of this book—using it in virtual sequences to access the interface UVC
sequencers, and, in the previous section, to start a sequence.
The p_sequencer field is present in every sequence, pointing to its sequencer component. It is automatically initialized by
the UVM library when a sequence is created by the user, which makes it a handy reference for test writers. This is useful
for accessing objects residing elsewhere in the environment from the sequence, as well as sequencer fields and methods.
In addition to those mentioned, some other useful applications of p_sequencer include using get_config() to fetch
configuration properties set in a test or testbench, and using find() to search the component tree and look up
information in other UVCs.
The SystemVerilog UVM Class Library provides two additional sequence tasks, pre_body() and post_body(), which
are invoked before and after the sequences body() task, respectively. These methods are invoked only when a sequence
is started by its sequencers start_sequence() task or the sequences start() task.
Examples for using the pre_body() and post_body() methods include:
Synchronization to some event before the body() task starts.
Calling a cleanup task when the body() task ends.
The following example declares a new sequence and implements its pre_body and post_body methods.
1 class revised_seq extends fork_join_do_seq;
2 //Constructor and `uvm_sequence_utils macro go here.
3 task pre_body();
4 super.pre_body();
5 @p_sequencer.init_done; // Wait until initialization is done
6 endtask : pre_body
7 task post_body();
8 super.post_body();
9 do_cleanup();
10 endtask : post_body
11 endclass : revised_seq
Notes
The pre_body() and post_body() methods are not invoked in a sequence that is executed by one of the
`uvm_do macros.
The init_done event declared in the sequencer can be accessed directly by way of the p_sequencer variable. The
p_sequencer variable is available, since the `uvm_sequence_utils macro was used. This prevents the user from
having to declare a variable of the appropriate type and initialize it using $cast.
At times, there may be several sequences processing items concurrently. However, the driver can handle only one item at a
time. Therefore, the sequencer maintains a queue of do actions. When the driver requests an item, the sequencer chooses
a single do action to perform from all of the do actions waiting in its queue. Therefore, when a sequence is doing an item,
the do action is blocked until the sequencer is ready to choose it.
The scheduling algorithm works on a first-come-first-served basis. In addition, the sequencer provides an arbitration mode
that determines how a do action is selected among those pending. You can affect the algorithm using: grab(),
ungrab(), lock(), unlock(), and is_relevant().
If a sequence is grabbing the sequencer, then the sequencer chooses the first do action that satisfies the following
conditions:
The do action is done by the grabbing sequence or its descendants.
The is_relevant() method of the sequence doing it returns 1.
If no sequence is grabbing the sequencer, then the sequencer will choose the first do action that satisfies the following
condition:
The is_relevant() method of the sequence doing it returns 1.
If there is no do action to choose, then get_next_item() is blocked, and the sequencer will try to choose again (that is,
reactivate the scheduling algorithm) when one of the following conditions happens:
Another do action is added to the queue.
A new sequence grabs the sequencer, or the current grabber ungrabs the sequencer.
Any one of the blocked sequences wait_for_relevant() task returns.
When calling try_next_item(), if the sequencer does not succeed in choosing a do action before the time
specified by uvm_driver:: wait_for_sequences() elapses, then uvm_driver::try_next_item()
returns with null.
A DUT might include an interrupt option. Typically, an interrupt should be coupled with some response by the agent. Once
the interrupt is serviced, activity prior to the interrupt should be resumed from the point where it was interrupted. Your
verification environment can support interrupts using sequences.
To handle interrupts using sequences:
1. Define an interrupt handler sequence that will do the following:
a) Wait for the interrupt event to occur.
b) Grab the sequencer for exclusive access.
c) Execute the interrupt service operations using the proper items or sequences.
d) Ungrab the sequencer.
2. Start the interrupt handler sequence in the sequencer or in the default sequence. (You can configure the sequencer to
run the default sequence when the simulation begins.)
Some verification environments require the layering of data items of different protocols. Examples include TCP over IP, and
ATM over Sonet. Other applications such as register and memory packages provide for traffic to be sent by way of a
lower-layer bus component. Sequence layering and virtual sequences are two ways in which sequencers can be composed
to create a layered protocol implementation.
Upper
Env Monitor Sequencer Sequence
Library
Driver
Collector
(optinal)
Lower
Env Monitor Sequencer Sequence
Library
Driver
Collector
(optinal)
vif
DUT
The classic example of protocol layering can be described by generic upper- and lower-levels (or layers) of a protocol. An
array of bytes may be meaningless to the lower-level protocol, while in the upper-level protocol context, the array provides
control and data messages to be processed appropriately.
For example, assume that there are two environments/sequencers that can be layered on top of each other as illustrated
in Figure 8-1. The requirements for such layering include:
Layering protocol environments without up-front planning of the lower and upper layers. This refers to the need to
decouple layers without dependencies or knowledge of what needs to be connected.
Layer randomization control. While the traffic from the upper layer is used by the lower layer, you still need to control
randomization of all layers simultaneously. For example, you need to allow randomization, allow for directed and
mixed traffic, and you need to be able to leverage a sequence library at each sequencer layer.
Multi-sequencer operations and control. For example, multiple high layers driving a single low-layer sequencer.
Performance optimization. In some cases, layers are mainly mediators between other layers and do cot require
allocation or randomization of data.
Upper layers have access to lower layers. Upper layers may need to grab control or reset activity of lower layers.
Layering is best implemented with sequences. There are two ways to do layering using sequences:
Layering inside a single sequencer, which applies to simple cases. See “Layering Inside One Sequencer” on page 145
for details.
Layering multiple sequencers, which applies to all layering cases. See “Layering of Several Sequencers” on page 145
for details.
Consider a low-layer sequencer driving lower_env_items, which are defined as:
class lower_env_item extends uvm_sequence_item;
rand bit[‘dATA_SIZE-1:0] payload[];
rand int pl_size;
constraint c_pload ( pl_size < `MAX_PL;
payload.size() == pl_size; )
... // Constructor and OVM automation macros go here,
endclass : lower_env_item
The lower environment will have its own sequence library capable of generating low-level random sequences of lower_env
items. This set of random sequences will be used to verify the lower-layer protocol and connection to the DUT. A low-level
sequence base class is defined as:
class lower_env_base_seq extends uvm_sequence #(lower_env_item);
... // Constructor and `uvm_sequence_utils macro
virtual task body();
…
endtask : body
endclass : lower_env_base_seq
Now consider a simple upper-layer data item which contains one or more lower env items.
class upper_env_item extends uvm_sequence_item;
rand int unsigned max_item_size; // maximum pl_size of lower item
rand int unsigned num_items; // maximum number of lower items
rand lower_env_item item[]; // array of lower items
constraint c_itein { num_items == item.size();
num_items inside { [1:10]}; }
constraint c_item_size ( max_item_size inside {[10:20]}; }
... // Constructor and UVM automation macros go here,
function void post_randomize();
foreach(item[i]) begin
item[i] = lower_env_item::type_id::create{$psprintf("item[%0d]", i));
assert(item[i].randomize with {pl_size <= max_item_size; }))
else `uvm_error ("RNDFML", "item randomization failed")
end
endclass : upper_env_item
Let’s consider the options for layering the upper_env_item in a verification environment.
For simple cases, you can layer inside one sequencer by generating a data item of the upper layer within a lower-layer
sequence. Do this by creating a new sequence for the lower-layer sequencer. Figure 8-2 below depicts the architecture for
using a single sequencer to layer the stimulus.
The sequence
Lower
creates and
Env
seq randomizes an
seq upper-env item
seq and translates it
Signal-Layer
into lower-env
Archiecture
items.
Lower Driver
DUT
This approach to layering several sequencers uses multiple sequencers as shown in Figure 8-3, below.
Upper Sequencer
seq
Multi-Layer
seq
Archiecture
seq
This lower-layer
sequence pulls
information
Lower Sequencer directly form the
upper-layer
seq sequencer.
seq
seq
Lower Driver
DUT
The most general case of layering consists of the driver accepting layer1 items, in which:
The layer1 items are constructed from layer2 items in some way. The layer2 items are, in turn, constructed from
layer3 items, and so on.
For every layerN and layerN+1, there is a mechanism that takes layerN+1 items and converts them into layerN items.
You can also have multiple kinds of layer1 and layer2 items. In different configurations, you might want to layer any kind of
layer2 item over any kind of layer1 item.
The remainder of this section describes possible variations, and complications, depending on the particular protocol or on
the desired test-writing flexibility.
fielda=f(fielde,fileldf);
fieldb=f(fielde,fieldf);
fieldc=f(fielde,fieldf);
A conversion mechanism might need to cope with the following situations (see Figure 8-5 on page 147):
One-to-one—One upper-layer item must be converted into one low-layer item.
One-to-many—One large upper-layer item must be broken down into many low-layer items.
Many-to-one—Many upper-layer items must be combined into one large low-layer item (as in Sonet, for example).
Many-to-many—Multiple upper-layer items must be taken in and converted into multiple lower-layer items. For example,
upper-layer packets are 10 bytes long, and low-layer packets are three to 35 bytes long- In this case, there could be
remainders.
A system might need to support different modes of operation as defined by topology, data type, or other
application-specific requirements. For example, in one environment, you may have only layer 1 items. In another
environment, layer1 items might be dictated by layer2 items. You may also want to decouple the layers further, for
example, so that layer2 items could drive either layer1 items or layer1 cells (on another interface) or both.
At times, you might have a mix of inputs from multiple sources at run time. For example, you may want to have one
low-layer sequencer send items that come from several high-layer sequencers.
One-to-One
layer1 address Payload(31bytes)
Many-to-One
layer1 address Payload(31bytes) address Payload(31bytes)
Many-to-One
In some configurations, the upper-layer items drive the timing completely. When upper-layer items are created, they are
immediately converted into lower-layer items.
In other configurations, the lower-layer sequences pace the operation. When a lower-layer `uvm_do macro is executed,
the corresponding upper-layer item should appear immediately, in zero time.
Finally, there is the case where items are driven to the DUT according to the timing of the lower-layer sequences, but the
higher-layer sequences are not reacting in zero time. If there is no data available from the high-layer sequences, then some
default value (for example, a zero filler) is used instead. In this case, the upper sequencer try_next_item() would be
used by the lower-level sequence.
In some configurations, the upper-layer items completely dictate which low-layer items reach the DUT—in which case, the
lower layer simply acts as a slave.
Often, however, both layers influence what reaches the DUT. For example, the high layer might influence the data in the
payload while the lower layer influences other attributes of the items reaching the DUT. In these cases, the choice of
sequences for both layers is meaningful.
In the most general case, you have a graph consisting of several sequencers, some of which may control sequence
execution on other sequencers and some may generate items directly. Some low-layer driver sequencers are connected to
the DUT, some upper-layer driver sequencers are layered above them, and some top-level sequencers feed into all of the
driver sequencers below them.
In the example configuration shown in Figure 8-6, Most-General Case of Using Virtual Sequencers,a low-layer sequencer
(LIB) receives input from multiple high-layer sequencers (two instances of L2A) as well as from a controlling sequencer.
Control Sequencer
seq
L2B seq
seq seq
L2B
seq
L2A seq
seq seq
seq
seq L2A L2A
seq seq
seq seq
seq seq
L2B driver seq seq
Sequencer
L1A L2B
Layering with seq seq
seq seq
connector
seq seq
sequences
Layering with
virtual L1A driver L1B driver
sequences
DUT
For this example, the lower-layer components are likely to be encapsulated inside an agent modeling the interface protocol.
This example shows how to achieve layering without introducing the recommended reuse structure, to keep the code
compact.
1 // Upper-layer classes
2 class upper_item extends uvm_sequence_item;
3 ...
4 endclass : upper_item
5 class upper_sequencer extends uvm_saquencer #(upper_item);
6 ...
7 endclass : upper_sequencer
8 // Lower-layer classes
9 class lower_item extends uvm_sequence_item;
10 ...
11 endclass : lower_item
12 class lower_sequencer extends uvm_sequencer #(lower_item);
13 uvm_seq_item_pull_port #(upper_item) upper_seq_item_port;
14 ...
15 function new (string name, uvm_component parent);
16 super.new(name, parent);
17 upper_seq_item_port = new ("upper_seq_item_port", this) ;
18 `uvm_update_sequence_lib_and_item(...)
19 endfunction : new
20 ...
21 endclass : lower_sequencer
22 class lower_driver extends uvm_driver #(lower_item);
23 ...
24 endclass : lower_driver
Now create a lower-layer sequence that pulls upper-layer items and translates them to lower-layer items:
1 class upper_to_lower_seq extends uvm_sequence # (lower_item) ;
2 ... // Constructor and UVM automation macros go here.
3 upper_xtem u_item;
4 Lower_item
5 virtual task body();
6 forever begin
7 `uvm _do_with(1_item,(...))// Constraints based on u_item
8 end
9 endtask : body
10 //In the pre_do task, pull an upper item from upper sequencer.
11 virtual task pre_do(bit is_item);
12 if (is_item) p_sequencer.upper_seq_item_port.get_next_item(u_item) ;
13 endtask : pre_do
14 // In the post_do task, signal the upper sequencer we are done.
15 // And, if desired, update the upper-item properties for the
16 // upper-sequencer to use.
17 virtual function void post_do(uvm_sequence_item this_item);
18 p_sequencer.upper_seq_item_port.item_done(this_item);
19 endfunction : post_do
20 endclass : upper_to_lower_seq
The following example illustrates connecting a lower-layer sequencer with an upper-layer sequencer.
Note The lower-layer sequencer is likely to be encapsulated inside an interface UVC, therefore, it will be encapsulated
in an env and an agent. This does not change the layering scheme but changes the path used to connect the sequencers to
each other in the tb file. The connection of the upper sequencer to the lower sequencer will typically happen in the tb env,
whereas the connection from the lower sequencer to its driver will occur in the connect() phase of the agent.
1 // This code resides in an env class.
2 lower_driver 1_driver0;
3 lower_sequencer 1_sequencer0;
4 upper_sequencer u_sequencer0;
5 function void build();
6 super.build();
7 // Make lower sequencer execute upper-to-lower translation sequence.
8 set_config_string("1_sequencer0",
"default_sequence","upper_to_lower_seq");
9 // Build the components.
10 1_driver0 = lower_driver::type_id::create("1_driver0", this);
11 1_sequencer0 = lower_sequencer::type_id::create(("1_sequencer0", this);
12 u_sequencer0 = upper_sequencer::type_id::create({"u_sequencer0", this);
13 endfunction : build
14 // Connect the components
15 function void connect();
16 // Connect the upper and lower sequencers.
17 1_sequencer0.upper_seq_item_port.connect(u_sequencer0.seq_item_export);
18 // Connect the lower sequencer and driver.
19 1_driver0.seq_item_port.connect(1_sequencer0.seq_item_export);
20 endfunction : connect
Summary
This chapter describes additional techniques for generation of random stimulus in UVM verification environments. We
introduce fine-grain control of sequence generation, techniques for creating and executing sequences in parallel, and
suggest how to handle interrupts. We also discuss options for protocol layering when conversions of one-to-one,
one-to-many, many-to-one, and many-to-many are required.
Register and Memory Package
Almost all devices have registers and memories that need to be controlled, checked, and covered as part of the verification
task. In verifying a device under test, you often need to perform the following tasks:
Capture the attributes and dependencies of a register.
Randomize the device configuration and the initial register values.
Execute bus transactions to write an initial configuration to the DUT.
Read and write registers and memories as part of the normal run -time operation.
Debug and analyze register activities.
Check the device by comparing register and memory values to a shadow device model.
Collect coverage metrics of device modes.
Register Packages provide a methodology and automation to enable productive and reusable register-related verification
logic. For example, you may want to integrate register models and configuration sequences of some subsystems into a full
system. If each of the subsystems use a different register solution, or if the register package does not support vertical reuse,
then a full system integration may require significant source code editing.
Another advantage to incorporating a register model into your verification environment is the fact that your registers are
not tied to a protocol. You define the model and can drive it from your current protocol using layering, and then a year
from now, if the protocol changes, your register model won't change.
While you may be able to introduce a simple register array as a register package, implementing a complete set of
requirements for a register package can be a seemingly endless task. At the beginning of the project, a simple user-created
register package may be fine, but a eomplete package like Cadence's uvm rgm provides the ability to expand to meet the
needs of a growing project, and can save users tremendous effort.
Note Since (at the time of this writing) no package was selected to be the formal UVM register package, we are using the
uvm_rgm package to demonstrate how to use register packages. This register package is a scalable solution and is
employed by many users in multiple application domains. The Cadence register package is built on open standards,
delivered in open source format, and is accessible from the contribution area at www.uvmworld.org
In addition to the basic UVM elements (env, agents, and sequencers), register packages introduce new terminology with
which you should be familiar. The following table lists common register package terms.
Term Description
Register Field A collection of one or more consecutive bits holding a value,Fileds have many attributes,such as
hard reset value, access policy (read-only,write-only,read-write,read-clear),and so on.
Register Represent a single register in the DUT. In the uvm_rgm package, a register is a collection of one
or more register fields, which are typically accessed by a single transaction on the DUT bus.
Register File Represents a number of DUT registers. A register file contains a collection of registers at different
addresses. A register file can also contain other register files.
Memory Represents a DUT memory. A memory contains a specified number of address locations, all of the
same size and characteristics (the memory element).
Address Map Represents the address space. The address map contains the register files and the memory blocks
(if any) in the address space. In a simple environment with only one register file, an address map
may seem redundant. However, address maps gain importance in environments with multiple
register files and memories, structuring the objects in a tree-like hierarchy. An address map can
hold other address maps, which hold register files and memories.
Register Database The root component for all registers and memories in the design. The RGM_DB inherits from
(RGM_DB) uvm_component and is instantiated in the testbench class. The RGM_DB contains the root
address maps of a verification environment.
Register Operation RGM operations are classes that model read and write accesses to registers and memories.
(RGM_OP)
Register Sequence A sequence of read/write operations that is intended to be applied to registers and memories
(RGM_SEQ) using the register sequencer.
Register Sequencer A dedicated sequencer for register (and memory) operations. The functionality of he RGM_SEQR
(RGM_SEQR) resembles that of a sequencer.
Like in many class libraries, using a register package requires that you become familiar with the library APIs and use model.
The following sections give you an overview of the flow, main concepts, and components of register packages.
The procedure you follow to use register packages is illustrated in Figure 9-1, The goal is to automate the process of
specifying changes to a running simulation. In general, the steps you take to accomplish this are as follows:
1. From the device specification, capture the device registers in Accellera IP-XACT format (the standard for capturing and
integrating design IP).
This can be done manually using an XML editor, though some companies choose to write scripts that parse their
internal documentation and produce standard IP-XACT,
Note For more information, see “Creating an IP-XACT File” on page 155.
2. Use the IP-XACT converter utility in the uvm_rgm package to create SystemVerilog register database classes,
including: register fields, registers, register files, address maps, and a register database. This utility is provided as part
of the register package. The generated register classes are building blocks for memory and register read and write
sequences.
3. Connect the generated register classes to the existing testbench.
4. Write register and memory sequences to generate stimulus.
5. Add coverage and checking to your register code.
Note The Cadence uvm_rgm package helps you to generate automatic coverage and checking. These built-in facilities
can be customized and tuned, as needed.
IP-XACT or
Converter
system RDL Testbench
utility
IP-XACT SV reg
editor classes
There are many ways to use the automation provided in the Cadence uvm_rgm register package. However, you can
maximize your productivity and increase the robustness and reuse of your testbenches by following the proven guidelines
outlined in this section.
Figure 9-2 illustrates the proper hookup of uvm_rgm elements in a testbench. As recommended by the UVM, the
illustration shows monitoring done independent of the randomization and injection components. This means that
coverage and checking are accomplished once a register’s activity is collected by the monitor. The separation of stimulus
injection from monitoring provides the following benefits:
Allows coverage collection and checking, regardless of the device that is accessing registers.
Ensures that only traffic which made it to the DUT is taken into account.
A further discussion of the merits of separating checking from generation can be found in Chapter 5.
Master Agent
Passive
Sequencer Slave
adapter
seq
Bus/DUT
The entire register and memory model is generated within an RGM database component. Unlike the address map and
register files—which are UVM objects and in some cases need to be allocated dynamically—the RGM_DB is a UVM
component that can be placed directly into your testbench. The RGM_DB is a uvm_component that provides all of the
traditional services of any other uvm_component, such as the configuration mechanism, test phases and messaging
control. The RGM_DB serves as the root of the register tree and the top-most address map is contained within the
RGM_DB.
The register database serves as a shadow model of the registers and memory in the DUT. Each register in the register
database shadows a register in the DUT and reflects the current state of the DUT registers as they are written and read.
9.2.4 Randomization and Injection
The uvm_rgm package uses the familiar UVM sequence mechanism to randomize and drive register and memory
sequences. Consequently, register and memory operation sequences behave much like any other UVM sequence. This
means you can randomly select a register object from the RGM_DB, randomize it, set the access direction (read or write),
and execute the register access operation. An API and set of macros are defined to perform read and write operations on
registers.
Some of the operations the sequence mechanism allows you to implement include:
Creating reusable sequences that support different configuration modes
Using an existing sequence as a sub-sequence
Traversing through all of the register in the address range
The uvm_rgm sequencer is layered on top of an existing master sequencer. The master emulates the CPU as it programs,
controls, and supervises other devices on the bus. Every read and write operation is translated into protocol-specific bus
transactions. This isolation between register operation and protocol specific bus transactions allows reuse of the same
register operation sequences, even if the specification changes to use a different bus. The bus interface sequencer is
extended to support register sequence operations using the UVM factory.
9.2.5 Monitoring
As mentioned before, the monitoring path is independent from the injection facilities. We use the protocol-specific bus
monitor to detect a bus transaction. At that point, the transaction information is sent to the module UVC via a TLM
connection. The interface UVC monitor, used as a reusable component, is unaware of whether the transaction information
collected is general bus traffic or a specific register access. This knowledge should be partitioned into the module UVC.
It is recommended that you use a module UVC in this flow. The module UVC receives the transactions collected by the
interface UVC monitor and decides what action to execute on the register and memory model. In the case of a write access
to a register or memory location, the shadow register, or memory location is updated in the RGM_DB structure. Therefore,
the content of the shadow register in the RGM_DB structure is synchronized with the DUT registers. When a read access is
detected on the bus, the monitor accesses the RGM_DB structure and compares the read result from the DUT to the value
in the RGM_DB shadow model. If it does not match, a DUT error is issued. In addition, the RGM_DB structure can collect
coverage on the register accesses and values (when coverage collection is enabled).
9.3 Using the uvm_rgm Package
This section provides a step-by-step introduction to using the uvm_rgm register package for register and memory
verification.
First, you need to define the uvm_rgm model (register fields, registers, register files, address map, and register database).
This is done using an XML IP-XACT file. The input format, which is a SPIRIT consortium standard, is translated into
corresponding SystemVerilog classes and used in the testbench.
IP-XACT files are a standard format for capturing and integrating design IP. They are structural in nature and allow for
describing a desired register database name, address spaces, register files, registers, and fields. Like any XML file, you can
read and edit these files with an ASCII editor. In addition, you can read these files using free or commercial editors that
provide a table view and extra editing and validation capabilities.
Cadence has extended the IP-XACT to enable more automation than is usually specified for register and memory
verification. For example, you can specify coverage directives, constraints (for register randomization), an hdl_path
variable (for directly accessing the register signals), and more.
Note If you already have a standard register specification format, you might want to create a script that parses the
specifications and creates the corresponding IP-XACT file. Some companies start with an IP-XACT format and generate
their specifications, C header files, register description, and even HW implementation.
XML is a text markup language like HTML, but was created to store data rather than just display text. The XML format is
hierarchical and includes start tags followed by end tags, both contained between angle brackets. End tags are named
exactly like the corresponding start tags but are preceded by a forward slash. For example:
<note>
<to>Ben</to>
<from>Gal</from>
<heading>Reminder for Shai</heading>
<body>Use UVM and prosper!</body>
</note>
IP-XACT introduces standard tags to capture design and register model attributes.
Notes
The easiest way to create an IP-XACT format is to start from an existing IP-XACT file.
Use an XML editor for easy visualization of IP-XACT files (see the following section for details).
Some companies use XML as the format for describing design blocks and leverage this for their register descriptions and
some write scripts to convert their own register specification format to XML.
The IP-XACT specification is not complete for register automation needs. Some of the missing capabilities include:
constraints, coverage directives, and backdoor paths. That said, the standard does support extensions that enable us to
add the missing functionality. For example:
<spirit:vendorExtensions>
<vendorExtensions type>ua_lcr_c</vendorExtensions:type>
<vendorExtensions hdl_path>lcr</vendorExtensions:hdl_path>
<vendorExtensions:constraint>c_char_lngth {value.char_lngth != 2’b00;}
</vendorExtensions:constraint>
</spirit:vendorExtensions>
Note The uvm_rgm package introduces useful vendor extensions. We strongly recommend that you leverage the
One of the benefits of using a standard is the number of free, commercial third-party applications which support that
standard.
Some of the benefits of using applications that support the XML standard are that they:
Ensure a clean XML syntax before running the parser script.
Provide an intuitive view that allows collapsing and enhancing sub-trees.
Support edit-time and on-demand standard validation.
Enable automatic correlation between the Spirit and generated SystemVerilog classes.
In some cases, you will need to build the register model using the full procedural language capabilities. For example, you
may need to create arrays with complex iterator logic that is not supported by IP-XACT. The uvm_rgm package provides
you with the means to capture such models. Please review the Register and Memory Model User Guide for additional
information.
The following example shows the XML description for the line control register for the UART Controller device.
Example 9-2 UART Controller XML Description for the Reg File, Address Map
1 <?xml version="1. 0"?>
2 <spirit:component
3 xmlns:spirit="http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.4"
4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5 xmlns:vendorExtensions="$UVM_RGM_HOME/builder/ipxact/schema"
6 xsi:schemaLocation="http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.4
7 http://www.spiritconsortium.org/XMLSchema/SPIRIT/1.4/index.xsd
8 $UVM_RGM_HOME/builder/ipxact/schema
9 $UVM_RGM_HQME/builder/ipxact/schema/vendorExtensions.xsd">
10 <spirit:vendor>Cadence_Design_Systetns</spirit:vendor>
11 <spirit:library>uart_ctrl_lib</spirit:library>
12 <spirit:name>uart_cfcrl_reg_db</spirit:name>
13 <spirit:version>1.0</spirit:version>
14 <!--START OF ADDRESS MAP DEFINITION -->
15 <spirit:memoryMaps>
16 <spirit:memoryMap>
17 <spirit:name >addr_map</spirit: name >
18 <spirit:addressBlock>
19 <!-- UART Controller Register File -->
20 <spirit: name>uaxt_ctrl_rf</spirit: name>
21 <spirit:baseAddress>0x00</spirit:baseAddress>
22 <spirit:range>0xFFFF</spirit:range>
23 <spirit:width>8</spirit:width>
24 <!-- Include each register description for the register file-->
25 <spirit:register> <!--LINE CONTROL REG--> </spirit:register>
26 <spirit:register> <!--INTERRUPT ENAB REG--> </spirit:register>
27 <spirit:vendorExtensions>
28 <vendorExtensions:type>uart_ctrl_rf_c</vendorExtensions:type>
29 <vendorExtensions:hdl_path>rf1</vendorExtensions:ndl_path>
30 </spirit:vendorExtensions>
31 </spirit:addressBlock>
32 </spirit:memoryMap>
33 </spirit:memoryMaps>
34 <spirit:vendorExtensions>
35 <vendorExtensions:type>uart_ctrl_addr_map_c</vendorExtensions:type>
36 <vendorExtensions:hdl_path>addr_map</vendorExtensions:hdl_path>
37 </spirit:vendorExtensions>
38 </spirit: component>
The uvm_rgm package provides a Java utility to automate the creation of SystemVerilog classes using the IP-XACT
description. To convert the input IP-XACT to SystemVerilog classes, use the following command:
$JAVA_HOME/bin/java -jar $UVM_RGM_HOME/builder/ipxact/uvmrgm_ipxact2sv_parser.jar
-i INPUT_FILE [Options]
The uvmrgm_ipxact2sv_parser.jar script also has an option to create a simple test to do a quick sanity check of your
register database files. The option is: -qt testname. It will create an instance of the register database, execute a hard reset
and print the register database and its sub-components. We suggest you use this option to test your register descriptions
while you are mastering the IPXACT format.
The IP-XACT utility, uvmrgm_ipxact2sv_parser.jar, converts the IP-XACT register description file into a set of SystemVerilog
classes for register verification. Included in the output file are the following:
Enumerated types—A type declaration for each unique enumeration
Field declarations
Registers—A class for every unique register type
Register files—Contain instances cf registers for each register file
Address maps-Contain one or more register files
A top-level RGM_DB class (example) that contains the top-level address map
The generated code is UVM-compliant and tuned for performance and the memory footprint of large systems. It leverages
much of the UVM automation and adds additional register-related automation by deriving the classes from the uvm_rgm
base classes. Most of the code is not intended to be read by humans, but some of the public attributes are accessible for
further user-defined coverage and generation. The code below illustrates an example of a generated register class:
Example 9-4 UART Controller Register File, Address Map and Register Database
1 // Register File Definition
2 class uart_ctrl_rf_c extends uvm_rgm_register_file;
3 // Register definitions (Only TWO are shown)
4 rand ua_ier_e ua_ier;
5 rand ua_lcr_a ua_lcr;
6 `uvm_object_utils(uart_ctrl_rf_c)
7 function new(input string name="unnamed-uart_ctrl_rf");
8 super.new(name);
9 set_size(`UVM_RGM_AWIDTH 'hffff);
10 ua_ier = ua_ier_c::type_id::create("ua_ier");
11 ua_ier.set_hdl_path("ua_ier_reg");
12 add_register(`UVM_RGM_AWIDTH'h8, ua_ier, "ua_ier");
13 ua_lcr = ua_lcr_c::type_id::create("ua_lcr");
14 ua_lcr.set_hdl_path("ua_lcr_reg");
15 add_register(`UVM_RGM_AWXDTH 'h3, ua_lcr, "ua_lcr");
16 endfunction
17 endclass : uart_ctrl_rf_c
18 // Address Map definition
19 class uart_ctrl_addr_map_c extends uvm_rgm_address_map;
20 rand uart_ctrl_rf_c uart_ctrl_rf;
21 `uvm_object_utils(uart_ctrl_addr_map_c)
22 function new(input string name="unnamed-uart_ctrl_addr_map");
23 super.new(name);
24 set_size(`UVM_RGM_AWIDTH 'h10000) ;
25 uart_ctrl_rf = uart_ctrl_rf_c::type_id::create("uart_ctrl_rf");
26 uart_ctrl_rf.set_hdl_path("rf1") ;
27 add_register_file( `UVM_RGM_AWIDTH’h0, uart_ctrl_rf, "uart_ctrl_rf”),
28 endfunction
29 endclass : uart_ctrl_addr_map_c
30 // Register Database Definition
31 class uart_ctrl_reg_db extends uvm_rgm_db;
32 rand uart_ctrl_addr_map_c addr_map;
33 `uvm_component_utils(uart_ctrl_reg_db)
34 function new(string name = "unnamed-rgm_rdb", uvm_component parent);
35 super.new(name,parent);
36 endfunction : new
37 virtual function void build();
38 super.build() ;
39 // Create the address map
40 addr_map = uart_ctrl_addr_map_c::type_id::create("addr_map", this);
41 uart_ctrl_addr_map.set_hdl_path("addr_map");
42 add_addr_map(addr_map) ;
43 endfunction : build
44 endclass : uart_ctrl_reg_db
You may want to extend the generated register classes to modify the coverage or to add environment specific fields. We do
not recommend modifying the automatic generated SystemVerilog code. The reasons for keeping the code untouched are:
In the event of specification changes, you will want to regenerate the register model.
uvm_rgm generated code may be modified over time for the purposes of code optimization and adding additional
features. Directly editing the code prevents you from leveraging future uvm_rgm releases.
We recommend using the UVM factory to extend auto-generated code. The built-in UVM factory allows for code
enhancements without intrusively modifying the generated code. This is done by deriving a class from the generated base
class and updating the derivative as needed. (See "UVM Factory” on page 55 for more details.)
The next section describes how to integrate the generated classes into your verification environment.
Registers provide a great opportunity for reuse, as most devices are targeted for reuse, or comprised of pre-verified
sub-components. The appropriate hook-up of register components to a testbench should preserve the potential for
planned and unexpected reuse needs. Only the register package integrator needs to know how to connect the uvm_rgm
components to the testbench. The uvm_rgm integration should allow test writers to focus on writing sequences and tests
using the register package.
In “Using the IP-XACT Utility” on page 158, you generated your SystemVerilog registers, register file(s), address map(s), and
register database from your IP-XACT file. The uvm_rgm library provides a register sequencer that you can use. By this
point you will also need a functioning interface bus UVC. With these pieces available, you can connect the register
database components to your verification environment.
1. Add the register database component (uart_ctrl_reg_db) in your testbench component.
2. Create an adapter sequence for the master sequencer. This sequence will take register operations and convert them
to bus-specific protocol on the interface UVC bus.
3. Extend the interface bus UVC master sequencer to add the infrastructure required (this will be used to provide
register operations to be driven to the bus).
4. Instantiate the register components in the testbench and connect the TLM ports between the components.
The following sections cover the steps needed to connect register sequences to a testbench.
9.4.2 Adding the Necessary Infrastructure to the Bus Master Sequencer
As mentioned earlier, the register sequencer provides register operations to an interface UVC master sequencer. Upon
receiving the register operation, the master sequencer will convert the register operation to the appropriate bus transfer,
which will then be sent to the interface driver. Traditional UVM sequencers do not posses the ability to receive the register
operations or perform the necessary conversion of the register operation to the bus transfer. Your existing UVCs master
sequencer must be modified to add some new TLM ports to enable this communication. You must also write some new
logic to perform the register operation to bus transfer conversion. To minimize the knowledge and level of effort required
by the user, the uvm_rgm package employs a standard (consistent with normal UVM sequence layering) means of layering
the register sequencer. Figure 9-3 on page 163 illustrates the connections between the register sequencer and the bus
master sequencer.
The master sequencer is extended to have:
A uvm_blocking_put_imp. This will allow the interface sequencer to receive register operations from the
register sequencer. You will need to implement a put() function that will be called when those operations are
received.
An uvm_analysis_port which provides a port that the interface sequencer can use to send responses back to
the register sequencer. This allows the register sequences running on the register sequencer to receive the results for
read operations and make decisions based on the result of those read values.
An adapter sequence that provides the method to:
Convert the register operation requests into bus transfers, and send them to the interface driver—commonly
called execute_op()
Process the responses to the bus transfers sent to the driver, and generate and return those responses to the
register sequencer by way of the analysis port mentioned above.
You can have the register sequencer take full control over the bus master sequencer by setting the bus master sequencers
count attribute to zero, or you can interleave register operations with other bus transactions.
Register Sequencer
Testbench
RGM_OP
Master Agent
Passive
Sequencer Slave
adapter
seq
DUT
Note This conversion from register operation to bus data item will vary and is specific to the bus interface protocol used.
Note Instead of using the `uvm_sequence_utils, we use the `uvm_object_utils and the
`uvm_dedarc_p_sequencer in order to avoid registering the adapter sequence in the sequencer. This prevents it
from being randomly picked and executed again in the sequencer random traffic.
Following the UVM guidelines, the reusable component instantiation and connection is done in the testbench class. The
steps you take to instantiate and connect the testbench are:
1. Instantiate the register database, the register sequencer, the bus UVC, and any other interface or module UVC you
need.
2. Instantiate an analysis port implementation in order to retrieve bus transactions from the bus monitor and update or
compare (and update) the value with the register database shadow value. This is typically done inside the module
UVC.
3. Connect the component’s ports and exports as shown in Figure 9-4, Testbench Instantiation and Connection, below.
4. Set up an initial reset operation for the shadow model. See “Reset Handling” on page 165 for an example.
Master Agent
Passive
Sequencer Slave
adapter
seq
Bus/DUT
DUT
Notes
“Updating the Register Database Model” on page 170 describes how the module UVC is used to update the shadow
register database based on the bus operations seen in the bus monitor. The module UVC contains whitebox
knowledge and references to the device, which enables it to make intelligent decisions about whether a value should
be compared to and updated into the register database shadow. We recommend the use of a module UVC for this
purpose. However, it is possible to perform that function within the testbench class by creating the analysis_imp
locally in the testbench. See “Updating the Register Database Model” on page 170 for more details on updating the
shadow register database.
It is possible to update the database from multiple agents, as needed.
Whenever a device is going through a hard reset, its registers are set to predefined values. In this case, the shadow
registers should keep in sync with the actual values. Reset values for each field are provided in IP-XACT format and, using
the IP-XACT utility, added to the uvm_rgm classes. You can reset a register, register file, or even the entire address map
using the built-in reset method and specifying which type of reset to execute. The example below shows an initial shadow
model reset that is typically required for all devices.
1 // Testbench task to reset the register database
2 task uart_ctrl_tb::reset_rdb();
3 forever begin
4 wait (top.reset === 1);
5 `uvm_info(get_type_name(), "Resetting RDB Shadow Registers", UVM_LOW)
6 rdb.uart_ctrl_addr_map.reset(uvm_rgm_hard_reset);
7 end
8 endtask : reset_rdb
9 // Start the reset_rdb() task in the testbench run() task
10 task uart_ctrl_tb::run();
11 fork
12 super.run()
13 reset_rdb();
14 join_none
15 endtask : run
Note Failing to reset the shadow model will cause a run-time error message when the first comparison between the
shadow value and the actual device value takes place. This is probably the most common mistake users make in initial
integration.
As part of the regular register activity, you might need to apply the following to the device:
Drive the device’s initial configuration to enable operation modes.
The device configuration may need to be changed multiple times during a run. For example, a DMA may be
configured for several different tasks on the same run
Normal run-time operations of reading and moving values between registers and other memory locations. For
example, some devices require polling status registers or serving interrupts by reading and writing to a certain
register.
Users typically want to write and read registers using the registers name or address (ideally, the registers name, since
addresses can change throughout the course of a project). Instead of inventing a new solution, uvm_rgm leverages the
standard sequence mechanism. To do this, you create UVM sequences of register reads and write that leverage the
uvm_rgm register operation class. This allows you to easily model read and write scenarios with the desired mix of
randomness and directed operations. The following section describes basic register sequence capabilities.
The data item for register sequences is a uvm_rgm_reg_op class that contains the register, the address on which to
operate, and attributes, such as direction (read or write) and the access mode (through the bus—frontdoor, or using the
hdl_path—backdoor).
Note Setting the access mode to frontdoor sends a transaction on the bus to access a register. Setting the access mode
to backdoor bypasses the bus logic and goes direcdy into the HDL signal that holds the register value. In reality, such access
is not possible but it can save simulation cycles and help testing in verification.
The following code snippet shows the fields present in the uvm_rgm_reg_op class definition (accessor functions are
provided but not shown here for the properties):
16 class uvm_rgm_reg_op extends uvm_sequence_item;
17 // operation hdl connection
18 protected uvm_rgm_hdl_connection_t hdl_connection = FRONTDOOR;
19 // operation address
20 protected uvm_rgm_addr_t address;
21 // operation direction
22 protected uvm_rgm_op_direction_t direction;
23 // operation register variable
24 protected uvm_rgm_register_base reg_var[$];
25 `uvm_object_utils_begin (uvm_rgm_reg_op)
26 `uvm_field_int(address, UVM_DE_FAULT)
27 `uvm_field_enum(op_direction_t, direction, UVM_DEFAULT)
28 `uvm_field_object (reg_var, UVM_DEFAULT)
29 `uvm_object_utils_end
30 ...
31 endclass : uvm_rgm_reg_op
Note Although a register operation is executed, we are really randomizing the defined registers and their field values. For
performance and capacity requirements, the register database types (rdb, address map, register file) only include the
register-specific information and do not include all the rgm_reg_op fields an methods.
Register sequences are derived from the class uvm_rgm sequence (created to provide additional automation and
convenience when writing register sequences), which in turn is derived from the uvm_sequence. You can read or write
registers by name or by address. In a register sequence, you may nee to apply constraints for constrained random tests or
in a directed way, set a field to a specific value. A set of functions and macros allow you to perform register operations with
or without additional constraints.
There are multiple ways to write and read registers:
Single-step operation mode, which is easiest to write and understand
Two-step operation mode (not shown here)
Low-level manual means to cause a register operation to be executed (not shown here).
Notes
The rgm_write_by_name and rgm_write_by_name_with macros perform a write operation after finding the register
in the string argument ("uart_ctrl_rf.ua_ler").
The rgm_read macro performs a read operation using`its register argument. The rgm_read_by_name() macro will find
the register in the database and then execute the read into the register argument.
Within the register database, you can traverse through the address space and register-file hierarchies and use the
get_reg_by_addr for a specific register file. For example, to get the register in offset 5 within the DMA and print it, use
the following code:
uvm_rgm_reuister_base base_reg;
base_reg = rdb.addr_map.dma_rf.get_reg_by_addr(5) ;
base_reg.print();
The function call returns the register at that address. When you print it, you will get all the fields defined for that register,
To get all the registers in the register file and print, use the following code:
uvin_rgm_register_base base_regs[$];
rdb.addr_map.dma_rf.get_all_regs (basse_regs) ;
foreach (base_regs[i]) base_regs[i].print();
get_all_regs() is a void function which returns a queue of registers via the base_regs reference argument to the
function.
You can call the get_reg_response() task to retrieve the response to a specific register read. This is shown in
“Read-Modify-Write Sequence” below.
A full set of register sequence tasks and macros can be found in the UVM Register and Memory Reference provided
with the uvm_rgm register package.
Many times you need to read a certain address, review the field values, modify a few fields, and write the result back to
the register. The following example illustrates how to create this scenario:
1 class read_modify_write_seq extends uvm_rgm_reg_sequence;
2 ua_lcr_c lcr_reg;
3 uvm_rgm_register_base reg_rsp_var;
4 `uvm_sequence_utils(read_modify_write_seq, reg_sequencer)
5 function new(string name="read_modify_write_seq");
6 super.new(name);
7 endfunction
8 virtual task body();
9 `uvm_info(get_type_name(),"Executing...", UVM_MEDIUM)
10 `rgm_read_by_name (lcr_reg, "uart_ctrl_rf. ua_lcr")
11 get_reg_response(req);
12 $cast(lcr_reg, req)
13 // invert the div_latch_access bit
14 lcr_reg.value.div_lafcch_access = !lcr_reg.value.div_latoh_aocess;
15 rgm_write_send(lcr_reg)
16 endtask
17 endclass : read_modify_write_seq
Line 10: The rgm_read_by_name call executes a read on the APB bus.
Lines 11-12: The get_response() and $cast() calls retrieve the result into the lcr_reg register.
Line 14: Then the div_latch_access field of that register is inverted.
Line 15: The rgm_write send call will execute a write operation without randomizing the value of lcr_reg
In many cases, you want to perform operations on a subset of registers, or on all registers in the address map or register
file. For example, perhaps a certain sequence would like to write to all writable registers in a register file. To achieve this,
all of the containers (register files and address map) need to provide a get_all_regs() function, as follows:
Syntax
virtual function void get_all_regs(ref uvm_rgm_register base regs[$], input bit clone = 1);
Description
regs Returned queue of all the registers in the container
clone Returns the clone of all the registers if 1
Example 9-8 Multi-Register Sequence
The following example shows how to fetch all of the registers in an address map and read their values:
18 class read_all_reg_seq extends uvm_rgm_built_in_base_seq;
19 `uvm_sequence_utils(read_all_reg_seq, uvm_rgm_sequsncer)
20 function new(string name="unnamed-rgm_read_all_reg_seq");
21 super.new(name);
22 endfunction
23 uvm_rgm_register_base r_list[$];
24 virtual task body();
25 `uvm_info(get_type_name(), "Read all registers sequence starting...")
26 uart_ctrl_rf.get_all_regs(r_list);
27 // Perform the reads
28 foreach(r_list[]))
29 `rgm_read(r_list[j])
30 endtask
31 endclass : read_all_reg_seq
Notes
By default, get_all_regs() creates a copy of all registers. You can also access these registers by reference by setting the
clone bit to 0. We recommend that you avoid getting a register by reference and randomizing it. Avoiding this is a
good practice that prevents corruption of the shadow model and overlooking bugs.
A full set of built-in functions are provided in the uvm_rgm package. Please see the UVM Register and Memory
Reference.
UVM sequences are reusable, and register sequences are a perfect candidate for reuse. In module-level environments, you
create multiple configuration sequences for multiple devices. In an integrated system-level environment, it is desirable to
use these configuration sequences and combine them into system configuration sequences. The uvm_rgm sequences can
be executed on multiple register file instances or with a different address offset in a larger system. To direct a sequence for
a different location in the address hierarchy, use the set_config statement. This insulates the body() of the sequence
from becoming invalid when the hierarchical context is added to a larger address map at some offset or instance name.
The uvm_rgm package provides built-in checking capabilities, which are flexible enough to accommodate many checking
strategy requirements.
The register model gets updated as read/write transfers are identified by the bus monitor, sent to the module UVC monitor
and compared/updated against the register database shadow model.
In some cases, a register is modified by the DUT and should not be compared. In that case, you need to set the comparison
mask in the IP-XACT file to zero (0). The register compare mask can be set to any value of masking and compare selected
fields as needed.
Many times, the DUT will modify register values during simulation time (status registers, for example). When this is the
case, the shadow model is not in sync with the actual DUT register values. Masking the comparison will remove the error
message, but still the question remains: How do we verify status registers or read-only registers? Also, a scoreboard may
need to know the DUT configuration for a certain check. This can be achieved in the following manner.
For these types of checks, a reference model or a predictor is required to determine the expected value. This
predictor can be.
Cycle accurate, updating the shadow model to always match the DUT registers
Partially accurate, only checking equivalency at certain times or on a subset of the registers
For implementing the reference model or for updating the shadow register model values, the uvm_rgm containers provide
a set of functions to set or get register values.
The following list shows the syntax for the get_reg_by_name () and get_reg_by_addr () methods:
virtual function uvm_rgm_register_base get_reg_by_name(string name, bit clone_bit=1);
virtual function uvm_rgm_register_base get_reg_by_addr(uvm_rgm_addr_t addr, bit clone_bit=1);
Note For other get_* methods, see the uvm_rgm User Guide
The following example demonstrates how to update a field inside of the lcr_reg register on an interrupt event signal (Users
integrating an example like this could trigger the interrupt event when an interface sees the appropriate interrupt signal
transition.):
32 class uart_ctrl_env extends uvm_env;
33 ...
34 event interrupt;
35 virtual task update_reg_model () ;
36 forever begin
37 uvm_rgm_register_base lcr_reg;
38 @interrupt;
39 lcr_reg = addr_map_ptr.uart_ctrl_rf.get_reg_by_name("ua_lcr");
40 lcr_reg.set_field_value("div_lat,ch_aaeess", 0);
41 end
42 endtask : update_reg_model
43 endclass
Every operation (reads and writes) performed on the DUT’s registers through the bus, is observed by the bus monitor,
which sends the transaction to the module UVC. The module UVC then performs a corresponding update, or compare (and
update) of the shadow model. That way, there is constant alignment between the DUT’s status and the registers’model,
which resides inside the register database.
To keep the model up to date with the DUT, do the following:
1. Update the module UVC (or create a new one if it does not exist). The module UVC is in charge of updating the
register model after each transaction that is captured by the bus interface UVC monitor.
2. Using a uvm_analysis port, connect the bus interface UVC monitor to the module UVC such that the transactions
collected are forwarded to the module UVC.
3. When each write transaction is received by the module UVC, the module UVC should update the register model by
calling the RGM_DB update_bv() method, providing the address and data. This keeps the register model in sync
with what was written into the DUT.
4. When each read transaction is received by the module UVC, the module UVC should compare the data to what is
contained in the register model by calling the RGM_DB compare_and_update_bv() method. The
compare_and_update_bv() method, as indicated by the name, also updates the register model to the data that
was provided after the compare has taken place.
The module UVC is the component in charge of updating the register model after each interface UVC transaction related to
the registers is detected. This task should be done by the integrator.
The module UVC keeps the shadow model aligned with the DUT’s register status by notifying the register model after each
UVC register transaction is received. This is done by calling the update_bv() method after each write transaction and
calling compare_and_update_bv() after each read transaction.
The following example shows updates to the module UVC we introduce in Chapter 10 “System UVCs and Testbench
Integration” in the section “Module UVC Architecture” on page 176.
When an APB transaction is received the register model is updated according to the operation direction (read or write).
44 class uart_ctrl_env extends uvm_env;
45 // Instances of the uart_ctrl config, monitor are not shown here
46 uvm_analysis_imp # (apb_transfer, uart_ctrl_env) apb_in;
47 uart_ctrl_addr_map_c addr_map_ptr; // pointer to the address map
48 `uvm_component_utils (uart_ctrl_em/)
49 function new (string name, uvm_component parent);
50 super.new(name, parent);
51 apb_in = new ("apb_in", this);
52 endfunction
53 function void write(apb_transfer transfer);
54 if(apb_slave_cfg.check_address_range(transfer.addr)) begin
55 if (transfer.diretion == APB_WRITE) begin
56 `uvm_info(get_type_name() ,
57 $psprintf("Write: calling update() with addr=%0h data=%0h",
transfer.addr, transfer.data), UVM_MEDIUM)
58 addr_map_ptr.update_bv(transfer.addr, transfer.data, 1);
59 end
60 else if (transfer.direction == AP3_READ) begin
61 `uvm_info(get_type_name(),
62 $psprintf("Read: calling compare_and_update() with addr=%0h,
data=40h",transfer.addr, transfer.data), UVM_MEDIUM)
63 addr_map_ptr.compare_and_update_bv(transfer.addr,transfer.data,
1);
64 end
65 else `uvm_error("ex_rgm_mod_uvc", "Unsupported access!!!")
66 end
67 endfunction : write
68 endclass : uart_ctrl_env
Many device operation modes translate directly into register control modes. This makes register field value: coverage a
good indicator of a complete verification.
9.8.1 Using uvm_rgm Automatic Coverage Facilities
The uvm_rgm automatically creates coverage groups for fields that are marked to be covered by way of the vendor
extensions in IP-XACT.
Syntax
<vendorExtensions:coverage_en>true</vendorExtensions:coverage_en>
Once a field is tagged for coverage, a coverage point is created in the register coverage groups.
What if you want to cross the register field values with other testbench attributes? Or describe a sophisticated coverage
model while exploiting SystemVerilog coverage attributes? As was mentioned before, we do not recommend editing the
auto-generated code. For such needs, you can extend the register class, define a desired coverage group, and use the UVM
factory to instantiate the new register class.
1 class ua_lcr_cov_c extends ua_lcr_c;
2 covergroup lcr_modes_cg;
3 coverpoint num_stop_bits;
4 coverpoint char_lngth;
5 cross num_stop_bits, char_lngth;
6 endgroup
7 function update_sample ();
8 super.update_sarople();
9 lcr_modes_cg.sample();
10 endfunction
11 endclass : ua_lcr_cov_c
Once this is done, you can use the standard factory overrides (type and/or instance) to replace the ua_lcr_c instances
with ua_lcr_cov_c instances.
Sampling registers as soon as they are written may not be correct for some applications. For example, if a DUT is
configured for a certain operation mode, but an abort function is called right after, then the question arises: Was this
operation mode really tested?
The solution for this issue is to not add the . sample() function of the covergroup to the update_sample() (and/or
the compare_and_update_sample() method. Rather, you should manually trigger the sample at the appropriate
time. Whatever block is aware of this condition can use the get_reg_by_name() or get_reg_by_address() to
get the desired register object and then call the coverage group .sample() function.
Summary
This chapter is a quick overview of a register package. It uses the Cadence uvm_rgm package as an example for register
packages. Many more capabilities exist in uvm_rgm than are shown here. For example, a user can:
Do backdoor operations for both Verilog and VHDL relying on a hdl_path string. This can enable you to write
directly to the field or the string.
Define in IP-XACT write and read FIFO registers that hold multiple values and indirect registers (registers that are not
mapped in the address map and can only be accessed indirectly).
Write sequences in complete register-level or field-level granularity.
Take advantage of an advanced tagging mechanism to control register-related activities.
Use the pre_access() and post_access() hooks, which are provided to implement side effect behaviors
triggered by accessing a register.
Implement a shadow memory model.
Use built-in tests that allow for the execution of predefined register tests without writing SystemVerilog code (which
writes to all registers inside a container). Various filters allows flexible control over these built-in tests.
For more information about register packages and the uvm_rgm, please read the documentation provided with the
Cadence uvm_rgm package on the www.uvmworld.org website.
System UVCs and Testbench Infearation
Chapter 7 “Simple Testbench Integration” introduced a testbench that instantiates scoreboard checking and coverage
directly within the top testbench class. But what if you want to reuse this logic in a cluster-level environment, which later
might be reused in a larger subsystem that is eventually part of an even larger system? This chapter reviews this
requirement and the solution of how to keep device-specific verification encapsulated for reuse, and which logic should be
placed within the testbench top and the module UVC container.
Note In this chapter we use the term system to describe system-level hardware integration. The topic of software UVCs
and hardware and software co-verification is beyond the scope of this book.
10.1 Introduction
Interface UVCs are developed for a designated protocol and can be used in verification of any device which uses that
protocol. But where do you place device-specific coverage? Where do reference models or scoreboards reside? These
concerns and many more are not related to an interface and usually involve an association across multiple interfaces.
Module UVCs are verification component containers that organize DUT-specific verification logic and enable integration of
modules or clusters into a larger environment. The types of reuse that module UVCs enable are:
Blocks to module, modules to cluster, clusters to system
Between projects with whitebox and blackbox reuse
TLM to cycle-accurate to post-silicon
Similar to the interface UVC, the module UVC has a standard architecture and configuration attributes. Like interface UVCs,
module UVCs provide a proven recipe to the module UVC developer and a consistent, easy integration for the testbench
integrator. Figure 10-1, UART Controller DUT and UART Controller Module UVC, depicts the architecture of a reusable
module UVC. The terms module UVC and system UVC are interchangeable. A module UVC is a system UVC for a single
module, and the way to configure and instantiate them is the same. Both module UVCs and system UVCs support further
integration into a lager system.
Figure 10-1 UART Controller DUT and UART Controller Module UVC
Figure 10-2, UART DUT and Module UVC Integrated into a Larger System, illustrates a module UVC created for a UART
controller device. When the UART is integrated into a larger system, some of the external interfaces of the module UVC
become internal to the larger system.
System UVC
System
UART Controller Module UVC
DUT CPU DMA Memory
Monitor Reg_file
Figure 10-2 UART DUT and Module UVC Integrated into a Larger System
Figure 10-3, Testbench Architecture with UART Controller Module UVC, shows a module UVC in passive mode integrated
within a testbench. A module UVC can also be active in stand-in mode. In this mode the module UVC emulates a
subsystem design, stand-in mode is used to enable simulation before all of the DUT is implemented, in some cases to
speed up the simulation. This chapter focuses on a module UVC architecture that maximizes reuse in the event of vertical
reuse. Later we show how the testbench integrator leverages the module and system UVCs for system-level integration.
Top(module)
Test uart_ctrl_cfg
a2u_u2a_rand_sequence
Testbench(UART_CTRL)
Virtual
Sequencer
Config
AHB Receiver
apb_if I/F Transmiter uart_if
Registers and controls
Module and system UVCs share the same high-level topology and standard attributes. Both can be further reused in larger
systems. Their architecture and sub-components are similar with the only difference being that system UVCs integrate
more than a single module. The following sections review the module UVC structure and then show how it scales to a
larger system.
The role of the module or system UVC is to encapsulate knowledge about its related device and add the components
needed to verify the system. The goal is to follow a solid formula that allows planned and unplanned reuse. In an interface
UVC, we reuse traffic generation and protocol-specific checks and coverage. In a module UVC, we reuse:
Device-specific checks
Scoreboards and internal checks
Reference model
Device specific coverage
Fine-tuning of interface UVCs`coverage definitions
Cross-coverage
Interna! coverage
Relationships between participating UVCs
Configuration dependencies
Connections between scoreboards and appropriate monitors
Note The virtual sequencer is not part of the module UVC, as some of the interfaces controlled in the module level might
become internal to the DUT and cannot be driven in the larger system.
Note The module UVC does not contain the interface UVCs. The module UVC is connected via references and TLM ports
to other UVCs (in particular to the related interface UVC s monitors). If two subsystems integrate all of their external
interfaces, the interfaces that connect the subsystems are duplicated in the integrated system. For example, if our UART
controller module UVC contains an instance of the APB interface UVC, a system that instantiates two UART controller
devices on the same APB bus ends up with two instances of the APB UVC. A block diagram of the UART controller module
UVC is presented below.
UART Controller Module UVC
Config
virtual sequencer
vs Monitor Reg_file
Scoreboard ACTIVE
stand-in
Cov/Checks mode
vs
vs
Ref_model
Figure 10-5 illustrates a system UVC architecture that instantiates two module UVCs.
Note From an integrator s point of view, it does not matter if the integrated subsystem is an atomic module UVC or a
composition of other sub-components. In order to achieve this, a system UVC contains the same sub-components and
configuration attributes as the module UVC. The main difference between these components is that a system UVC contains
more than a single DUT module integration.
The next sections describe in more detail each of the module UVC sub-components.
10.3.1 Monitor
The monitor in a system or module UVC is a critical component in the verification environment. It collects data related to
the activity in the module or system. This information is made available to checkers, scoreboards, and coverage collectors.
The monitor collects most of its information from the relevant interface UVCs via TLM connections. Some additional
information relating to the internal state of the module or system may be derived either from a virtual interface to the DUT
(whitebox access) or from a reference model. The connection from the monitor to the corresponding interface UVC
monitors is done using the interface UVC TLM analysis ports, as illustrated in Figure 10-6, below.
The following code example, taken from the UART controller module UVC, shows how the module UVC monitor uses
information collected from other monitors.
Figure 10-6 Connecting the UART Controller UVC and Monitor with Interface UVC TLM
Ports
10.3.1.1 Scoreboard
In Chapter 7 “Simple Testbench Integration”, the scoreboard components were placed directly in the testbench. For
module-to-system reuse, scoreboards are placed within the monitor that can access multiple interface UVCs monitor
information. If a broader end-to-end checker exists, it should be possible to turn off local scoreboards to speed up the
simulation. The monitor build() method creates and configures the scoreboard(s) and the monitor connect()
method is used to connect the monitor TLM ports to the scoreboard ports, as shown in the example below.
10.3.1.2 Coverage
Module and system UVCs have access to the interface UVCs transactions, state variables in the testbench, or reference
model and DUT internals. As part of the testbench integration, further constraints and extensions on the interface
UVC-provided coverage may be required.
For more information, see “Coverage” on page 187
A reference model emulates the DUT functionality and, given the input to the DUT, can predict the ecxpected result. The
reference model can also provide status and state information. This information is useful for generating traffic, coverage
and checking.
A reference model can be implemented at three levels of precision:
It can provide transaction-level functionality. Such reference models provide transaction-level valid output for a given
input. This information can be used for scoreboards to compare the actual to predicted, or for functional stand-in
mode. The transaction level functionality is the most commonly used reference model; this model is provided by the
architects as a golden model to the DUT behavior.
Reference models can also be cycle accurate. Such models perform cycle-accurate emulation and provide the
expected result per cycle. These can be used in stand-in mode and scoreboard, but typically are overkill for
scoreboarding.
It can provide rough justification by checking the validity of the DUT behavior given some input and output. The DUT
internals might be used for justification
10.3.2 Memory Blocks and Register Files
A device is typically mapped into the system memory map. The module UVC can be configured from a higher-level
environment to its actual location within the system map. To be reusable, all module UVC operations work on a local
context and assume a correct context to be set from outside. Vertical reuse of register and memory configuration
sequences also relies on the register package ability to provide the context (typically an address offset) to configuration
sequences.
In some cases, the system is not fully available. You may still want verify the rest of the system or start developing your
testbench. For this, a user needs to emulate the DUT behavior. The active stand-in mode of operation uses a virtual
sequencer, buffers, and sequences to control the ports of the DUT and data structures. Section 10.8, “Stand-In Mode,” on
page 190 has further details on how to implement the module UVC stand-in mode.
The example below shows the class definition of the UART controller module UVC.
The UVC configuration is a reusable aspect that must be encapsulated with the UVC. When a UVC becomes part of a
system, some of its configuration is fixed according to the specific use in the system. Configuration of large systems can be
challenging, as the system may contain multiple components and interfaces that need to be configured in a coherent way.
The integrator of a system may not be familiar with all the subsystem configuration options that need to be integrated.
Similar to the recommendation in interface UVCs, the module UVC and DUT configuration attributes are encapsulated
within a configuration object. The object allows randomizing legal settings by capturing configuration attribute
dependencies in constraints. As modules and system UVCs are integrated, their configuration classes are integrated as well
and the system attributes and dependencies are controlled via constraints. Both the testbench integrator and the test
writers can apply more constraints on the configuration class.
Both the DUT and the module UVC need to be coherently configured for the desired operation mode. As well as
DUT-specific configuration attributes such as bus speed or number of channels, some configuration attributes are generic
and commonly used in all module UVCs. Standardizing these provides a consistent user experience for the integrator and
test writers. The reuse attributes in a module UVC are active and passive modes, and whitebox versus blackbox
configuration modes. As discussed, it is best to capture the configuration class within an object—to be randomized, printed,
sent between components and encapsulated as needed. The module UVC configuration class contains the interface UVCs
configuration classes. The standard attributes of a module UVC include:
Active and Passive Modes—The active mode implements the stand-in mode, where the module UVC plays the role of
the DUT (for example, when the DUT is not available). The active mode enables an additional virtual sequencer used
to drive the external interfaces on the DUT’s behalf.
Whitebox and Blackbox Modes—The UVC also supports whitebox versus blackbox modes. In a blackbox verification
mode, the UVC ignores the internal implementation of the DUT and assumes only knowledge of the interfaces and
generics that are not implementation specific. In this mode, the module UVC is reusable for a different
implementation of the DUT. In a whitebox mode, the UVC is aware of the DUT implementation whether is it RTL,
SystemC or other implementation. For example the UVC can access FIFOs and signals and cover their state. The
verification in this mode is both more thorough and more specific.
Level of Abstraction—The same module UVC allows verifying a DUT that is going through a refinement process from
high-level models to RTL and into gate-level description. The is achieved by using a level of abstraction knob on every
component.
The example below shows a partial implementation of the UART UVC config class. This configuration object is included in
the reusable files for the UART interface UVC. Example 10-5 on page 182 shows the composite UART controller
configuration object.
Note The APB configuration class is shown in the Interface UVCs Chapter, Example 5-12 on page 85.
As the device is being configured in run time, the testbench and module UVCs might require reconfiguration. Typically
module UVCs acquire their configuration directly from the bus, exactly like the DUT. This ensures that the module UVC and
the DUT are in synch, and that the reconfiguration works in passive mode. For example, module UVC senses configuration
changes by translating bus transactions to register operations and adjusting its operation as needed.
Users may need to reflect configuration changes to a module UVC. In the case of reconfiguring an existing module, use of
set_config_* is not appropriate because it does not trigger the field modification. (For example, someone still needs
to call the apply_config_settings or get_config to update the configurable values.) Similar to interface UVCs,
we recommend defining an update_config() task that receives a configuration object argument, verifies that the
configuration is consistent, and applies the configuration on the components and its sub-components. Shown below are
the update_config() methods for the UART controller module UVC and the UART controller monitor.
The testbench class instantiates, configures, and connects the interface UVCs, module UVCs, register sequencer and
database, and the virtual sequencer. The reusable components are set for a default operation mode that can be altered by
a specific test for verification purposes. With a UVM testbench, instantiation is relatively easy, as each component provides
a set of high-level configuration parameters to build itself. At the same time, designing a testbench requires understanding
of the system verification needs and can involve a lot of creativity.
Figure 10-7 below illustrates the components and connections for the UART controller example testbench.
Top(module)
Test uart_ctrl_cfg
a2u_u2a_rand_sequence
Testbench(UART_CTRL)
Virtual
Sequencer
Config Register Register
Sequencer Database
AHB Receiver
apb_if I/F Transmiter uart_if
Registers and controls
Observe the testbench topology in Figure 10-7, UART Controller Testbench Topology. All UVM users are likely to draw this
testbench topology if asked. The great thing about UVM is that the high-level encapsulation and guidelines spell out much
of the correct architecture for users. Many times, the users already have this high-level picture in mind, and they just need
to understand all the capabilities and flexibility that this architecture provides for their projects.
The example below illustrates a testbench class declaration.
Example 10-7 UART Controller Testbench
1 class uart_ctrl_tb extends uvm_env;
2 // UVC Components
3 apb_env apb0; // APB UVC
4 uart_env uart0; // UART UVC
5 uart_etrl_env uart_ctrl0; // Module UVC
6 // Virtual sequencer
7 uart_ctrl_virtual_sequencer virtual_sequencer;
8 // Configuration Class for the testbench
9 uart_ctrl_config cfg;
10 // The register database
11 uart_ctrl_rdb rdb;
12 // register_seqencer for read/write operations on registers and memories
13 uvm_rgm_sequencer reg_sequencer;
14 // Provide implementations of virtual methods: get_type_name and create
15 `uvm_component_utils(uart_ctrl_tb)
16 // Constructor - required UVM syntax
17 function new(input string name, input uvm_component parent=null);
18 super.new(name,parent);
19 endfunction
20 // Additional class methods
21 extern virtual function void build();
22 extern virtual function void connect ();
23 extern virtual task run();
24 endclass : uart_ctrl_tb
As mentioned above, it is best to repeatedly randomize a configuration object, configure the testbench and the DUT
accordingly, and send external traffic to verify this configuration. The configuration includes attributes that determine
topology and run-time operation modes. The topology-related configuration attributes are randomized before build, and
cannot be modified after the testbench is created. Other run-time parameters can be re-randomized to reconfigured, but
need to be consistent with the fixed topology settings. One solution is to build a class that has config and reconfig
randomization modes. In the config mode, both topology and run-time attributes are randomized. In the reconfig mode,
the topology attributes are have their rand_mode() setting on 0, and only the run-time parameters are randomized.
The constraints between all attributes maintain the configuration consistency.
The initial testbench configuration is randomized as part of the testbench build() phase in order to set the testbench
topology. The reconfiguration needs to happen during run time and to propagate to the rest of the testbench. This makes
the virtual sequencer that controls the entire environment the ideal location for configuration randomization.
The virtual sequencer of the testbench should have a top sequence that takes a system configuration object and translates
it to the needed testbench operations. For example, the sequence:
Re-randomizes the configuration run-time attributes as needed
Executes a register sequence that programs the DUT’s register file and initiates memories
Configures the module and interface UVCs as needed using update_config()
Executes sequences on interface UVCs. This can be optional, as the UVCs should have a project-specific default.
Coordinates parallel threads that execute some test specific operation (for example, to wait for some event and
trigger a soft reset to check correct DUT reaction to this scenario.)
The following example illustrates a typical UART controller virtual sequence:
Example 10-8 UART Controller Virtual Sequence
1 class concmrrent_u2a_a2u_rand_trans_vseq extends uvm_sequence;
2 rand int unsigned num_a2u;
3 rand int unsigned num_u2a;
4 function new(string name="concurrent_u2a_a2u_rand_trans_vseq");
5 super.new(name);
6 endfunction : new
7 // Register sequence with a sequencer
8
`uvm_sequence_utils(concurrent_u2a_a2u_rand_trans_vseq,uart_ctrl_virtual_sequenc
er)
9 constraint c_num_a2u {(num_a2u <= 20);}
10 constraint c_num_u2a {(num_u2a <= 10};}
11 // REGISTER sequence
12 uart_cfg_rgm_seq uart_cfg_dut; // CONFIGURE DOT
13 // APB sequences
14 apb_to_uart_wr a2u_seq;
15 read_rx_fifo_seq td_rx_fifo;
16 // UART sequence
17 uart_transmit_seq u2a_seq;
18 virtual task body();
19 uvm_test_done.raise_objection(this);
20 // configure UART DUT
21 `uvm_do_on(uart_cfg_dut, p_sequencer.rgm_seqr)
22 fork
23 // Write UART DUT TX FIFO from APB UVC and transmit data from UART UVC
24 `uvm_do_on_with ( a2u_seq, p_sequencer.apb_seqr,{num_of_wr =
num_a2u;})
25 `uvm_do_on_wifch (u2a_aeq, p_aequencer. uart_seqr,{num_of_tx =
num_u2a;})
26 join
27 //Read UART DUT RX FIFO from APB UVC
28 `uvm_do_on_with(rd_rx_fifo, p_sequencer.apb_seqr,{num_of_rd =
num_u2a;))
29 uvm_test_done.drop_objection(this);
30 endtask : body
31 endclass : concurrent_u2a_a2u_rand_trans_vseq
A main register sequence accepts the testbench configuration and translates it into register operations in a procedural way.
Some register attributes come directly from the configuration class, while others are randomized on the register level.
Because SystemVerilog does not allow constraining an object handle, the virtual sequence creates the top register
sequence, assigns the configuration objects and then randomizes and sends the register sequence for execution.
These components are defined with the UVC code but instantiated in the testbench. The assumption is that the virtual
sequencer is not reusable at the next level of integration because some of the external interfaces become internal to the
larger system (exceptions are discussed later). The configuration sequences and the register model are reusable, but only
the top-level DUT knows when and how to hierarchically locate the register models and when to program the subsystem
using the configuration sequences.
10.5.3 Interface UVC Extensions
As you move to a system or project specific environment, some system constraints may be required on the interfaces UVCs.
For example, the system may not support all types of transactions on the bus. The module UVC extends the interface UVC,
and adds fields, constraints or other desired logic to adjust the interface UVC logic for system needs.
10.6 Sequences
Driving the verification environment is done using sequences. Reusing sequences from the various components of the
environment saves effort when building the verification environment. For multi-interface and system-level control, we use
a virtual sequencer.
The virtual sequencer coordinates the stimulus generation across multiple UVCs. Figure 10-8, UVCs within SoC Design,
illustrates a larger SoC design with a virtual sequencer driving stimulus on each interface UVC.
CPU Bus
System
APB Bridge Chip
Peripheral Bus
VIP developers familiar with a protocol should be encouraged to create sequences representing interesting,
protocol-specific scenarios. We recommended using random fields to parameterize the sequence activities to make them
generic. The integrator chooses which sequences will be reused per specific requirements. The developer needs to help
the integrator understand which sequences are applicable to what scenarios and what is fundamental to the UVC work.
The default sequence should be set by the developer (whether random or by a template method). Infinity minus should be
implemented here (that is, make all the possibilities applicable).
Module-level sequences are not always reusable on the system level. In general, mixing random sequences can break a
design. For example, inputs that are accessible at the module level might not be accessible at the system level. However, if
you know the interfaces that are likely to remain external on the next level of integration, you can create a dedicated
virtual sequencer for these and another virtual sequencer to control these and the rest of the sequencers. When you move
to the next level of integration, the virtual sequences of the external interfaces are still usable. Figure 10-9, Reusable
Virtual Sequences, illustrates this concept.
Figure 10-9 Reusable Virtual Sequences
Register sequences are highly reusable and are, by default, context-independent. The register sequencer resides in the
testbench outside of the module UVC, as typically there is one CPU that programs the assembled sub-devices.
10.7 Coverage
Coverage definitions can be reused from module to system, while adding system-specific coverage definitions. Interface
UVCs collect coverage of traffic on the specific protocol they monitor. Module UVC coverage is collected in its monitor,
based on information from interface UVCs and from the DUT/reference model. System-level coverage is based on
information in all module and interface UVCs in the system. Coverage results can be crossed to show interactions and
combined states. In modules and systems, a massive amount of coverage can be quickly collected via reuse, crosses, and
association. The challenges are to identify the meaningful coverage that represents the verification goals, and avoid
defining combinations that are sometimes not reachable by the system. Special attention should be paid regarding the
impact of coverage on performance.
The module and system UVCs provide coverage data in different categories, such as:
Basic traffic coverage for each of the interfaces—Note that not all interface coverage is relevant or interesting to the
system. The system may not make use of all interface capabilities. As SystemVerilog does not allow coverage
extensions, duplicating the coverage groups and inserting the needed modifications might be required. Eliminating
existing coverage can be done at collection time or at review time (using filter on the existing database); for
performance reasons, the first is recommended.
Crossing traffic coverage for all interfaces
Internal coverage (for example, internal states)
Sequence coverage—Have we tried all of the sequences that we were targeting?
Register coverage—Register coverage is interesting, as it reflects the device operation modes. Specifically, it is
important to identify crosses of operation modes that should be verified. It is also important to check that the device
was sufficiently exercised before declaring the device operation mode to be verified
Performance-related coverage, such as delays and throughput
Configuration coverage, such as all operation modes
Reset and recovery
Hot configuration (where applicable)
Error conditions and recovery
The following example shows functional coverage collection for the UART Rx FIFO. This coverage group is placed in a
coverage component, uart_ctrl_cover, which is instantiated and connected in the UART controller monitor. Creating a
separate UVM component for coverage collection is optional. For simplicity, only the Tx coverage group is shown.
1 class uart_ctrl_cover extends uvm_component ;
2 virtual interface uart_ctrl_internal_if vif;
3 bit coverage_enable = 1;
4 int unsigned mod_tx_fifo_ptr;
5 // Required macro for UVM automation and utilities
6 `uvm_component_utils(uart_ctrl_cover)
7 virtual task run ();
8 fork
9 collect_tx_coverage();
10 join
11 endtask : run
12 virtual task collect_tx_coverage();
13 // Extract & re-arrange to give a more useful input to covergroups
14 // Calculate percentage fill level of TX FIFO
15 forever begin
16 @ (vif. tx_fifo_ptr)
17 mod_tx_fifo_ptr = ( vif.tx_fifo_ptr * 100 / `UA_TX_FIFO_DEPTH);
18 if (coverage_enable) dut_tx_fifo_cg.sample();
19 end
20 endtask : collect_tx_coverage
21 // DOT TX covergroup
22 covergroup dut_tx_fifo_cg;
23 tx_level : coverpoint mod_tx_fifo_ptr {
24 bins EMPTY = {0}
25 bins HALF_FULL = {[50:99]};
26 bins FOLL = {100}; }
27 endgroup
28 function new (string name , um_component parent);
29 super.new(name, parent);
30 if (coverage_enable) begin
31 dut_tx_fifo_cg = new();
32 dut_tx_fifo_cg.set_inst_name ({get_full name(),".dut_tx_fifo_cg"});
33 end
34 endfunction
35 function void assign_vi(virtual interface uart_ctrl_internal_if uart_int);
36 vif = uart_int;
37 endfunction : assign_vi
38 endclass : uart_ctrl_cover
In early stages of the testbench development, the design code may not be available or may be only partially implemented.
You can start the testbench integration, sequence library creation, coverage collection and more if a stand-in (active) mode
is supported. In stand-in mode, the system UVC drives the simulation by playing the role of the DUT. A reference model is
used to emulate the device behavior. The active mode is helpful to create a system-level testbench when the DUT and
pieces of it are not available. This is highly device-specific, but for some devices with the availability of interface UVCs, the
stand-in mode is surprisingly easy to implement.
The following figures illustrate both passive (normal) and stand-in (active) modes.
A system verification environment is assembled from numerous lower-level verification environments. In large systems,
the performance of the verification environment might become an issue. Proper preparation in the interface UVCs and in
the module UVC allows trading off controllability for performance. For example, you may want to disable assertions and
leave the end-to-end checkers to identify system malfunctions. This section discusses various techniques to speed up the
simulation for project-specific needs.
Random generation is a very powerful tool for productivity (reducing the manual work of configuring the environment or
DUT and creating stimulus) and improved quality (hunting for bugs in areas that are hard to foresee). However, excess
use of randomization might result in scalability problems. When building a large verification environment, consider
carefully how to avoid unnecessarily complex generation that might slow down the entire simulation.
The following tips show how to avoid some common pitfalls related to large verification environments. The overall effect
depends on the size and complexity of the DUT.
10.9.1.1 Recycle Allocated Items
The `uvm_do macro allocates a sequence item using the factory. You can avoid the allocation using the `uvm_create
once for allocation and uvm_sendor `uvm_rand_send multiple times, as mentioned in Chapter 8 “Stimulus Generation
Topics”.
In some scenarios, expensive randomization provides diminishing value. In such cases, it is helpful to use procedural
checking and avoid complex constraint solving. Hard-coding values can hide bugs; thus the use of this technique should be
limited to scenarios in which variation is not an option.
Examples for such scenarios include:
Iterations in the sequence body() when the same values or small deterministic value variations need to be created.
Using a single randomization, followed by a `uvm_send can enable this.
In layering, the randomization of the lower item might not be necessary, as it is best to only program the register on
the local bus and not to exercise the local bus capabilities.
Slaves can randomize replies that include split, retry, and other. If these scenarios were exercised on the module level,
you can create a simple sequence that always returns the desired value without randomization in the slave sequence.
10.9.2 Coverage
Coverage is handled similar to checking. While it serves an important role during module verification, some coverage may
be traded off for performance during system-level verification.
All coverage definitions and related code should be implemented under a coverage_enable monitor flag.
As in checking, coverage definitions can also be structured in logical groups, under various has_*_coverage flags, like
has_whitebox_coverage or has_error_coverage. Error coverage and whitebox coverage are important at the
module level. On the other hand, for system-level verification, they could create holes in the coverage. There might be no
simple way to create all error cases or activate all internal states in a specific system. Therefore, this kind of coverage
should be implemented under special flags so that it can be disabled at the system level, using the configuration
mechanism.
System verification has its own specific coverage goals implemented in the testbench and various subsystem UVCs. These
include connectivity and interoperability, which must be active during system verification, but can be turned off (for
example, for HW/SW co-verification).
10.9.3 Messages
UVCs typically produce a lot of useful information for module-level verification. Some of that information might be
redundant in system verification. Extensive use of messages and string manipulation can significantly slow down your
simulation. Make sure you use
The figure below illustrates the recommended directory structure of a module or a system UVC
the message macros and do
not directly call the message routines.
The module UVC is developed and maintained as reusable UVM package with additional subdirectories. The objective is to
encapsulate all reusable code, examples, and documentation for verification of the system in a consistent manner so that it
can be shared within and between projects.
In particular, the module UVC package contains examples of how to reuse this as a component in a larger system. The
examples are contained in one or more testbench directories, the contents of which are described in Figure 10-12 on page
193. A testbench example should be used as the basis for integration of the module UVC in a larger system. A testbench is
sometimes called an SVE (Simulation and Verification Environment).
Note The standalone verification testbench and test is part of the reusable module UVC package. Many times a bug is
uncovered in a large integration that requires further isolated tests. It is useful in such cases to have available the tests and
sequences used at the subsystem level.
Summary
This chapter tackles the requirements of developing complex testbenches and provides recommendations on how to make
testbench components reusable for cluster-level and system-level verification. We introduce the module UVC verification
component, which organizes DUT-specific verification logic, follows a consistent architecture, and allows configuration
using the UVM configuration mechanism. We also describe the creation of reusable virtual sequences, DUT-specific
coverage, and DUT-specific checking. All of these are collected into a common package and directory structure similar to
the package we introduced for interface UVCs.
The Future of UVM
Any discussion of the UVM must also look toward the future. The UVM is a great step forward for the industry for enabling
SystemVerilog VIP to be shared and reused within and between companies. It also provides a standard way in which
commercial VIP should be developed so that it can easily be plugged into any project which is based on the UVM. As seen
in the previous chapters, the UVM covers most of the fundamental methodology required for building reusable
SystemVerilog Verification Environments for RTL designs. There are still some fundamental capabilities that are required to
complete this methodology, as well as some more significant areas in which the UVM can be extended in order to truly be
“universal" and have an even bigger impact on the industry. This chapter gives an overview for both the short-term and
longer term areas in which the UVM will likely be enhanced.
Throughout this book, we review the process of creating UVCs and how they might be reused within a central company
repository. An additional opportunity to accelerate testbench creation is to adopt commercial VIP for standard protocols,
because many of the standard protocols are extremely complex and require a large investment to learn the protocol and
develop a UVC from scratch. Cadence Design Systems and other VIP providers can leverage the UVM to offer
commercial-grade plug-and-play UVCs for standard industry protocols. The typical advantage of a commercial UVC is that it
will be used on projects in different companies to verify many designs, which increases the maturity of the UVC much
more quickly than what can typically be achieved by an internally-developed UVC. Just like design IP that has already been
silicon-proven is of more value, verification IP which has been “hardened” by being used to successfully verify lots of
designs provides similar value.
In addition to being used to verify many different designs, there are several other considerations for choosing the right
commercial VIP to ensure the most productivity benefits.
In order to easily integrate the VIP into your verification environment, it should be checked to ensure it is truly UVM
compliant. A VIP that is based on a different methodology or that only loosely follows the UVM will likely require a lot
of work to integrate into a UVM verification environment.
UVCs for specific interface protocols should adhere to the standard UVM architecture; use randomization and
sequences for stimulus, including a sequence library of common, useful sequences; provide functional coverage,
protocol checks, and monitoring of transactions on the interface for higher-level scoreboard and reference model
checks.
It is often the case when designing a new IP with interfaces based on standard protocols that the design may not
support all of the features of the protocol. Therefore, it is very important for a commercial UVC to be highly
configurable so that it is easy to enable or disable various features of driving, checking, and covering the protocol.
The OVM has been used successfully on countless projects by hundreds of companies over the past few years. The feature
set of UVM is a superset of OVM, so it is reasonable to say that the UVM provides a comprehensive solution for building
reusable verification environments. However, there are outstanding requests from users for further enhancements to the
class library and methodology with various priority levels. Some of the higher priority enhancements that have been
requested are described in this section.
11.2.1 Register Package
Since practically all designs are configured and controlled by writing and reading the registers to program the device for
specific operation modes, an essential verification activity requires being able to generate constrained-random stimulus for
writing registers, as well as the ability to check and cover registers in order to ensure they have been programmed
correctly. There are currently multiple register packages available for the UVM that provide various capabilities for
modeling registers and generating register configuration stimulus. In order to ensure further reuse and consistency of
modeling register configuration, the UVM should include a single register package or at least specify the API for a single
register package. At the time of the writing of this book, no single register package has been selected as the official UVM
package or API. The IP-XACT format includes register-related features and allows facilitating reuse of register models, so it
should be a requirement for the UVM register package to leverage IP-XACT for describing the definition of registers to be
modeled. This book includes the Cadence-contributed register package and has concepts that can be universally applied.
Run-time phases are arguably the most needed enhancement to complete the UVM class library. They are required to
coordinate the execution at simulation run time of components that were designed in isolation and without up-front
planning. The point of run-time phases is to provide hooks for testbench developers to define logic that will be executed
during specific “phases” of simulation. For example, the UVM might define a specific phase for reset that could call a task
at a specific phase in simulation. A testbench developer could then define all the reset-related logic for a specific DUT to
execute during the reset phase and place this logic inside this task. These built-in tasks will then be called automatically in
a specific order during simulation to execute each phase and the corresponding logic defined within that phase. Some of
the requirements regarding run-time phases include the following:
Ability to reset the phases and re-execute them for the entire system or a subsystem.
Ability to assign verification components to part of a specific “domain” so that phases can be coordinated for
verification components that are part of the same domain. This might be needed, for example, to reset a subset of
the system.
Ability to extend standard phases for specific needs, which is important for seamless simulation coordination.
Ability to synchronize an arbitrary set of components without advanced planning
Support for run-time phases by the sequencers and sequences, as different traffic may be required at different phases
Today, practically all designs are designed or verified using more than a single language. One of the main reasons for this
fact is that different languages are optimized for different tasks. For example:
Verilog and VHDL are optimized for RTL design.
SystemVerilog and e are optimized for constrained-random verification.
SystemC is optimized for high-level modeling.
PSL and SVA are optimized for assertion-based verification.
Furthermore, there may be cases where one team is more efficient and has more expertise in using a specific language for
a specific task. As we continue to evolve design and verification, we will continue to define new languages to further
optimize these tasks. Therefore, for UVM to be truly “universal,” and applicable for use in most real-world verification
environments, it is essential for UVM to expand beyond SystemVerilog to also support other languages and integration of
verification IP and models that are not written in SystemVerilog—namely e verification IP and SystemC models. The figure
below shows an example of a typical multi-language environment where the UART UVC and the testbench might be
written in SystemVerilog and the desire is to reuse an existing APB UVC that is written in e.
How to How to drive data
connect to and synchronize
the monitor? sequences on both
TESTBENCH interfaces?
sv Virtual Sequencer
Mon Driver
UART DUT
sv
UART APB e
The main focus of the UVM to date has been for RTL verification. Over the last several years, more of the verification has
expanded beyond RTL simulation to include more formal verification; more verification earlier in the design cycle, based
on high-level SystemC models; more need for robust analog mixed-signal verification; and more use of hardware
acceleration for system-level verification, including hardware/software verification. A significant issue in this expanding
verification flow is that there little to no consistency in terminology, testbench infrastructure, or tests. Furthermore, as you
move from verifying your high-level SystemC model, to RTL simulation, through formal verification—and later,
hardware/software acceleration and emulation—there is very little to no verification reuse. The momentum behind the
OVM brought unrelated experts from such areas as ESL, hardware acceleration/emulation, formal verification, and
analog-mixed signal verification to discuss a consistent flow that enables further reuse and consistency across these
different domains. We expect this momentum to carry under the wider UVM umbrella. The UVM was architected to
consider future extension into many of these domains, so it is important not to compromise the UVM concepts, as it is
enhanced to support these different domains. For example, in acceleration, the interface UVC and agent encapsulation
should be preserved. There is great promise to further optimize and unify verification beyond RTL simulation by extending
the application of UVM to other domains.
Summary
A lot of verification expertise has been accumulated throughout the last decade which has been leveraged to define the
UVM as a highly productive and reusable solution for constructing SystemVerilog Verification Environments. The core
methodology of UVM has been in practical use by many customers for many years and has been proven in both
SystemVerilog and e to be a very efficient approach for building testbenches and reusing verification components. The
convergence that UVM provides within the industry is a new opportunity to end debate about competing methodologies
and enable a worldwide VIP ecosystem. As we look toward the future, UVM offers a strong foundation for further unifying
and optimizing verification across multiple languages and domains.