A short course on SystemVerilog classes for UVM verification

Article By : Dave Rich

A frustrating element of SystemVerilog is the indiscriminate use of the world “class,” which can mean any one of several things, depending on context. A quick review might help veteran users avoid inadvertently creating confusion.

Learning any language can be difficult when so many words take on different meanings in different contexts. “Why does a farmer produce produce?” These homonyms can be confusing even for native speakers. After several years working on the SystemVerilog IEEE 1800 standard, I understand this even more clearly now. The word class is used many times in different contexts to mean slightly different things that the reader is expected to understand.

This article attempts to clarify the meaning of a small but important part of the SystemVerilog language: the class. For some of you, this will be an introduction to SystemVerilog, object-oriented programming (OOP), and the meanings of class in this context. For others, it will help you over some of the homonymous hurdles related to class.

The class data type in SystemVerilog is the construct that enables OOP, which is found in many other languages. To put things in perspective, we will begin with a brief history of OOP, where it all begins.

A history of object oriented programming
OOP came from an early hardware description language (HDL) called Simula, developed in Norway in the 1960s. HDLs are very conducive to objects because you can think of hardware as a collection of connected components, with a component forming the basis of an object. Verilog is in many ways an early OOP language with a module being the basic object that gets instantiated to make up a design.

C++ was created by merging concepts from both Simula and C, but one of the big problems with C and C++ is its poor memory management. It’s too easy to reference invalid memory or corrupt someone else’s memory. You wind up spending a lot of time debugging someone else’s code instead of your own.

In the 1990s, Sun Microsystems developed Java as a highly reliable alternative to C++. It takes care of all of the memory management for you and makes it very difficult to write corruptible code. (It was originally called C++–, meaning it was C++ but subtracting the bad parts of C++.)

Around the same time, Sun’s hardware teams began to realize that even though Verilog was designed as both a design and testbench language, it was running out of steam as testbench language. So Sun merged a lot of the concepts from Java with Verilog and created the Vera language. However, Vera was a separate, proprietary language that bolted onto a separate Verilog Simulator. Eventually, Vera was folded into Verilog and became what is now known as the IEEE SystemVerilog standard.

OOP supports writing reusable code, especially for verification environments. Writing a test for hardware is a software problem, and OOP is a proven methodology for writing abstract, highly reusable, and highly maintainable software code. Relevantly, classes are used to model reusable verification environments and the abstract data and methods that operate on them. OOP encompasses the following key concepts:

  • Encapsulation – creating containers of data along with their associated behaviors, i.e., the code that operates on that data
  • Inheritance – extending or overriding containers with additional data and new behaviors
  • Data hiding – hiding implementation details to reduce complexity and raise the level of abstraction
  • Generic programming – writing code that can be reused for a wide range of applications; similar to overriding parameters on a module instance in Verilog
  • Polymorphism – the reuse of the same code to take on many different behaviors based the type of object at hand. Polymorphism ties a lot of the other concepts together.

SystemVerilog class terminology
In the same fashion as many other OOP languages, SystemVerilog uses the term class to define what makes up an object. In the process of encapsulation, we divide things up into smaller classifications. We might use one class to represent an audio stream, and another class to represent a video stream.

Once we have a class definition, we construct a class object dynamically. We always construct classes dynamically, which means they don’t exist until someone calls a routine to have them constructed. Then we store its handle in a class variable. Some other programming languages use the term pointer, as in a pointer to a specific area in memory, but we prefer to use the term handle to mean an opaque pointer or safe pointer. When we have a handle to an object, we do not know the value of the handle, we just know that it refers to a specific object and its collection of data. SystemVerilog never lets us look at the value of a handle, we can only use it to refer to an object and its contents.

As can be seen, the word class has many different meanings based on the context, and that takes some getting used to. Yet many people just use class when they mean class type, class object, class handle, or class variable. In this article, the word class will not be used all by itself unless the meaning is blatantly obvious.

Class basics
A class is a data type, just like a structure or the enum type shown in Figure 1. Class declarations do not allocate any storage, they only create new types. Thus, there is no storage associated with a class type.

Figure 1 The class definition is bounded by the class/endclass keywords, with the Command, Status, and Data variables as properties and the function GetStatus and the task SetCommand as methods.

The software world refers to the variables declared inside the class type as properties. SystemVerilog uses the keyword property for use with assertions, which is something entirely different. So don’t confuse it with that construct.

Subroutines (known as tasks and functions in Verilog) are methods of a class. Both properties and methods are members of a class, and this is what forms the basis of encapsulation.

Every class has a built-in method with the name new(), which you must call to create an object. The new() method is also known as the constructor. Constructing an object allocates the space needed to hold the properties of an object. You can define your own constructor that overrides the built-in version and put whatever procedural code you want inside it. You can also define arguments to pass to the constructor. Note that the function new() has no return type. The new method implicitly returns a handle to an object of the class type (Packet, in Figure 2). That handle is stored in a class variable, myPkt. Now myPkt refers to a chunk of memory containing the three class properties defined in Figure 1.

Figure 2 Calling new()creates an instance and allocates memory

SystemVerilog handles have a few interesting qualities. C/C++ programmers tend to think of handles as pointers, but they are really safe pointers. What that means is a class variable can only refer to a valid object or have the special value NULL when it refers to nothing. The handle value cannot be manipulated or even seen. And the kinds of operations that can be performed on a handle are limited: you can assign a handle to a class variable from another variable or a constructor; you can compare a class variable with another class variable to see if they refer to the same object; and you can compare a variable to the special value null. That’s it.

SystemVerilog takes care of all the memory management of objects by allocating memory for an object as it is constructed and de-allocating them when they are no longer being used. The end result is that there is no way a class variable can refer to a “bad” area of memory.

The variables inside a class object are class properties. Properties begin their lives when constructing the class object. Class properties have a dynamic lifetime; i.e., the life of the object. The properties of a class can be variables of any data type, including other class variables. Class properties are accessed using the object handle in a class variable. Referencing a property with a null variable results in a fatal error. In fact, referencing a null object is one of only a few ways you could write code that results in a fatal error. Luckily, fatal null object references are usually easy to locate and fix.

The tasks and functions we put into a class become methods of the class. We refer to them the same way we reference properties. Tasks and functions have the same meaning they have in Verilog. Tasks can block consuming time, and functions must execute in 0 time and not block. Functions can also return values, so they may be used as part of an expression.

I hope this short course on classes will make things clearer the next time you read or hear the word class, and you will be clearer to your colleagues! We will dig deeper into the key concepts of OOP and SystemVerilog in upcoming articles about inheritance and polymorphism and OOP pattern examples.

Dave Rich is a senior verification consultant in Mentor Graphics’ consulting division.

Virtual Event - PowerUP Asia 2024 is coming (May 21-23, 2024)

Power Semiconductor Innovations Toward Green Goals, Decarbonization and Sustainability

Day 1: GaN and SiC Semiconductors

Day 2: Power Semiconductors in Low- and High-Power Applications

Day 3: Power Semiconductor Packaging Technologies and Renewable Energy

Register to watch 30+ conference speeches and visit booths, download technical whitepapers.

Leave a comment