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.
3.2 Designing Large Software Applications
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.
3.4 Distributed Development Environment
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 object’s 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.
Figure 3-1 OOP Interface Objects Enable Parallel Development
3.5 Separation of Concerns
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.
3.6 Classes, Objects, and Programs
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.
3.7 Using Generalization and Inheritance
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.
Figure 3-3 Using Inheritance and Generalization
Example 3–1 Generalization and Inheritance
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 run() endclass: plorsche
3.8 Creating Compact Reusable Code
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.
Figure 3-4 The Overlap between Request and Response
Figure 3-5 Using an Abstract Class to Model the Request and Response
3.9 Polymorphism in OOP
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.
Example 3–2 Inheritance and Polymorphism
typedef enum {RED, BLUE, BLACK, WHITE} color_t;
virtual class sports_car;
rand color_t color;
virtual function void print(); // virtual keyword
$display("I'm a %s sports car", color.name());
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.
Example 3–3 Downcast
class flying_car extends sports_car;
class flying_car extends sports_car;
virtual function void fly();
...
endfunction
endclass: flying_car module top;
task fly_if_you_can(sports_car cars[]);
flying_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
endpmodule: top
3.11 Class Libraries
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.
Figure 3-6 Class Library Usage
3.12 Static Methods and Attributes
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.
Example 3–4 Static Methods and Attributes
virtual class car;
static int counter = 0; // shared by all instances static local function void increment_counter();
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 car’s static method like this:
car::increment_counter()
3.13 Parameterized Classes
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
stack int_stack; // default: stack of ints
stack #(bit[9:0]) bit_stack; // stack of 10-bit vectors stack #(real) real_stack; // stack of real numbers
3.14 Packages and Namespaces
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
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 endpackage: branda_pkg // file: brandz_pkg.sv package brandz_pkg;
class convertible;...endclass: convertible endpackage: 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.
3.15 Unified Modeling-Language Diagrams
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.
Figure 3-7 Class Box in a Class Diagram
Figure 3-8 shows a few UML graphical notations, relationships between classes, including containment of classes and derivation of classes
Figure 3-8 shows a few UML graphical notations, relationships between classes, including containment of classes and derivation of classes from parent classes.
Figure 3-8 UML Graphical Notations
3.16 Software Design Patterns
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.
3.16.1 Software Design Anti-Patterns
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-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.
3.17 Why Isn’t the Existing OOP Methodology Enough?
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
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