SystemVerilog supports templates for writing generic code using parameterized classes. Here, we’ll describe some of the in-code design patterns that make up the UVM base class library. Users who write test beds with the SystemVerilog Universal Verification Methodology (UVM) or any type of class-based methodology can learn from these techniques.
Design templates are optimized, reusable solutions to common programming problems. They are more than simple class definitions or a set of routines: they are language independent models for writing code.
The notion of design templates specifically for object-oriented programming languages (OOP) SystemVerilog was popularized in 1994 by the book “Design Patterns: Elements of Reusable Object-Oriented Software”. OOP allows you to write reusable code. OOP design models take a new step in reuse.
There are many types of design patterns. This article covers two important categories:
Singleton models – Restrict instantiation of a class to a single object.
Factory bosses – Provide an interface to create families of linked or dependent objects and specify a policy to create them.
Before explaining them in more detail, we need to understand how SystemVerilog supports generic code writing patterns using parameterized classes.
A parameter is a type of constant that represents a value or a data type. You cannot modify a parameter once the simulation has started. The compiler evaluates parameter expressions as part of its construction and code generation phases before the simulation begins. So you can use a parameter as part of the declaration of another type or use the value of a parameter in, for example, the range of an array declaration.
SystemVerilog uses a pound sign (#) with a list of parameter names in the class header to define a generic class. When you reference the generic class, you also use the pound sign to provide a list of parameter assignments or overrides.
Figure 1 Parameterized class declaration and reference
In this example, we need to provide a type parameter, T, because it has no default value; the value parameter, W , is optional because we provided a default value of 5.
A generic class referenced with its actual parameter values is called a specialization . When referencing a generic parameterized class in the declaration of a class variable or in the declaration of another type, the parameterization of the generic class is fixed. Each combination of a generic class with its final parameter overrides becomes a unique class specialization: a new class type.
Static class properties are allocated and initialized before time 0. But this is no longer true as soon as you declare a generic class with parameters. The distinction between a class parameterized as a generic model and the specialization of this class becomes very important. Static class properties are not allocated unless their enclosing class is specialized. And there is a unique instance and initialization of its static properties for each specialization.
Figure 2 Parameterized classes with static properties
SystemVerilog allocates a static property of a generic class after there is a reference to a specialization of that class. It is not necessary to construct an object of this class. The two type definitions in figure 2 create two instances of the statics to counter variable. Declaration and construction of the two class variables S1 and S2 all share the same statics to count variable with the typedef count_byte .
Just like with unparameterized classes, we can use the class scope operator to reference static properties and methods. We use unique specialization to reference the unique member we want. Just like when defining a new specialization, you must provide a parameter override for each parameter that does not have a default value, otherwise it will not be compiled.
The singleton design pattern is used to ensure that only one instance of a class type is built. Suppose we are trying to build a tree structure and want to define the structure so that there is only one root object in the tree. To control the number of objects built, we need to make the class constructor a local method. This means that no one outside of this class can call it, and no one can build an extended class because the extended class would need to call super.new ().
figure 3 Singleton model with local qualifiers
We also create a local static property m_root which holds a handle to our singleton object. Since we can’t call the constructor directly, we define a static method to have() which applies our singleton model. Call to have() checks m_root and only calls New() if it sucks. Since no one else can access m_root, there is no way New() could never be called more than once. The to have() The method demonstrates the “create-it-first-use” model.
When many people first think of singletons, they imagine defining a static variable “const” which can only be written once by calling New() at initialization before time 0 and never written again. The problem is that as long as New() has no local qualifier, anyone can set another root variable while building multiple root objects.
Another problem is the so-called “static variable initialization order fiasco”. All static variables in a design are initialized before time 0. But there is no way to know in what order the static variables are initialized because there is no procedural flow of instructions yet. These static variable initializations can be scattered throughout the code.
To avoid the initialization order fiasco, we use an access method to get the value of a local static variable that initializes it at the first reference, instead of directly referencing the static variable. Now it doesn’t matter which variable is initialized first.
The factory model makes the construction of objects polymorphic. This allows you to delegate construction to another object which can decide what type of object to create. The factory also provides a policy for determining the type of item returned by the factory. If we use the factory model when creating our classes, we can decouple the class type building an object from the actual class type we want to build.
How does this work? Suppose we have a class A which builds an object of class B and a class B which builds an object of class C. Later we might want to extend class C to D. But then we need to either modify or extend the class B to build an object D instead of an object C. If we extend B, we also have to modify or extend A. But we don’t want to modify A or B. The factory model comes to the rescue by allowing us to replace the call to New() in class B which constructs a C object with a method that would normally return a C object, but could instead return an object of any type derived from C. So class A or B no longer has to be modified when we want to change what she built.
Figure 4 Polymorphic factory model construction
The factory model involves a number of different concepts that work together. There are many types of factories that produce different types of things.
For example, a factory can be used to produce a single object derived from an abstract base. The factory uses another kind of abstract class, known as proxy class , which has a virtual method that returns a handle to an object of the desired class type. For each class we want to create with the factory, we need to extend the proxy class and implement an object creation method that constructs an object of the requested type. We then build the extended object proxy class and get a handle to a proxy object that can be used to build the requested object.
Figure 5 Factory proxy objects
A proxy object is therefore a lightweight substitute for the entire requested object with all of its properties and methods. We can pass the proxy object descriptor or store a number of different proxy object descriptors in a database, like in the associative array, factory , in Figure 5 . Then we can call the virtual create object method which chooses an implementation based on the type of handle stored in the array.
There are a few other things we can do to automate the factory registration process. The factory model can take advantage of static property initialization with a specialized class to do factory registration for us. We can also make the specialized proxy object a singleton pattern, me, which allows us to change the factory associative array index from a string type to the proxy base class type.
Figure 6 Static factory registration
Once you’ve done all the work of creating the object registry class, the actual use of this factory record becomes pretty straightforward. Just by referring to a specialization in a typedef , the static properties in the objectRegister the class is allocated and initialized. So all we have to do is put a typedef inside the classes we want to register at the factory like we did here inside the C and D classes.
Figure 7 User registration and exemptions
Now instead of calling New(), we call the static method create inside the specialized class named typeId, which is inside the requested class type, C. This is what C :: typeId :: create () Is. We don’t need to use a dynamic $ cast here because the return type of the static create () method is set to the requested C type. Finally, we overwrite the factory by replacing the proxy object C with a singleton of proxy object D. Now, whenever someone calls the create method while waiting for a C object, they get a D object instead.
This completes our three-part series on SV OOP for UVM (read part 1 and part 2). Hope this gives you enough information to start taking advantage of OOP design principles. For more information I recommend the paper, Using parameterized classes and factories: the Yin and Yang of object-oriented verification, and be sure to check out the library of articles and articles on SystemVerilog and UVM at Verification Academy.
Dave Rich is a senior audit consultant in the consulting division of Mentor Graphics.