Here's a clear explanation of the key ideas in Section 5.
1: Introduction to Object-Oriented
Programming (OOP) in SystemVerilog, with some context and examples to help make it
practical.
📘 What’s the Problem with Traditional Verilog
(Procedural Style)?
Verilog and languages like C separate:
Data (e.g., addresses, commands, data values)
Code (the logic that acts on the data)
This leads to:
Scattered data structures (arrays for each field of a transaction)
Tight coupling between testbench and specific hardware signals
Static limitations (e.g., array size fixed at compile time)
🔧 Example in Verilog:
To store 100 transactions:
reg [31:0] addr_array [0:99];
reg [31:0] data_array [0:99];
reg [2:0] cmd_array [0:99];
// You must manage all arrays manually, index by index.
If your test needs 101 transactions? You must change the array size and recompile. Not
scalable.
💡 What Does OOP Offer in SystemVerilog?
Object-Oriented Programming allows:
Combining data and behavior into a single unit (a class).
Creating dynamic objects (no need to fix sizes at compile time).
Writing cleaner, higher-level code that manipulates transactions, not just bits.
🎯 Benefits of Using OOP in SystemVerilog Testbenches:
Procedural Verilog OOP in SystemVerilog
Manually manage arrays Use objects (dynamic)
Procedural Verilog OOP in SystemVerilog
Low-level signal toggling Transaction-based modeling
Code and data are separate Code and data are together (in classes)
Hard to maintain Easier to reuse and update
🧾 Simple SystemVerilog OOP Example
Define a transaction class:
class Transaction;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit [2:0] cmd;
function void print();
$display("Addr: %0h, Data: %0h, Cmd: %0d", addr, data, cmd);
endfunction
endclass
Use it in your testbench:
initial begin
Transaction t;
t = new();
t.randomize();
t.print();
end
🔍 What this does:
Defines a template for transactions.
Allows randomization, encapsulation, and reuse.
Replaces the need for 3 parallel arrays.
🧠 Summary
Traditional Verilog testbenches are low-level and rigid.
OOP in SystemVerilog introduces:
Classes to encapsulate transactions.
Dynamic memory allocation.
More abstract, maintainable, and scalable testbenches.
This shift is especially important in UVM (Universal Verification Methodology), where
everything is class-based.
Would you like a complete example of an OOP-based testbench using classes?
You're doing great—this is a very important conceptual section. Let's go through Section 5.2:
"Think of Nouns, Not Verbs" slowly and clearly.
🎯 Key Concept: Focus on Nouns, Not Verbs
What it Means:
Instead of thinking about what actions your testbench performs (like create, send, check,
report), you should think about what your testbench is dealing with—the objects (or
"nouns") such as:
Transactions
Packets
Bus cycles
Commands
Data samples
These are the core units of communication in your testbench and between modules.
🚗 Analogy: Testbench is Like a Modern Car
Old cars needed the driver to manually control many low-level things (like engine
timing or choke).
Modern cars hide the complexity—you just turn the key, press gas, and go.
🔁 In the same way, a well-designed testbench hides low-level details and allows you to
work with higher-level objects (like Packet or Transaction) instead of constantly
manipulating signals directly.
🧱 Structure of a Modern Testbench (in OOP style)
A testbench is broken into components, each responsible for one part of the verification
process.
Component Purpose
Generator Creates transactions
Driver Sends transactions to the DUT (Design Under Test)
Monitor Observes signals from DUT and creates received transactions
Scoreboard Compares expected vs. actual results
Transaction (or Packet) The data unit passed around and manipulated
These components are often implemented as classes in SystemVerilog.
🧾 Example: A Transaction-Oriented View
Instead of:
// Procedural style (verb-focused)
write_addr = 32'h1000;
write_data = 32'hDEADBEEF;
write_enable = 1;
You do:
// OOP style (noun-focused)
Packet pkt;
pkt = new();
pkt.addr = 32'h1000;
pkt.data = 32'hDEADBEEF;
pkt.write = 1;
driver.send(pkt);
The Packet class now bundles all related data together. The driver class is responsible for
knowing how to send that packet to the DUT.
🧠 Why Is This Better?
Cleaner and more abstract code
Easier to maintain and debug
Testbench components are reusable
You can randomize, print, and track a transaction object easily
Test logic is decoupled from design details
✅ Summary of This Step
Think in nouns: focus on what data you're working with (transactions).
Each class should model either:
o A data object (like a packet), or
o A control block (like a driver or monitor)
This way, your testbench becomes modular, abstract, and easy to scale.
Would you like to now move into actually writing a basic transaction class (Packet) and a
simple driver class to see this idea in practice?
You're progressing very well—now we're getting hands-on with your first real class in
SystemVerilog! Let’s break down Section 5.3: Your First Class and understand Sample 5.1
clearly and slowly.
📦 Sample 5.1: A Simple Transaction Class
Here’s the class from the book:
class Transaction;
bit [31:0] addr, csm, data[8];
function void display();
$display("Transaction: %h", addr);
endfunction: display
function void calc_csm();
csm = addr ^ data.xor;
endfunction: calc_csm
endclass: Transaction
🧠 What’s Happening Here?
✅ Class Definition
class Transaction;
This starts the definition of a new class called Transaction. It will bundle data and related
functionality into one reusable unit.
🧾 Variables / Properties
bit [31:0] addr, csm, data[8];
addr: A 32-bit address.
csm: A checksum value (calculated later).
data[8]: An array of eight 32-bit data values.
This class is modeling a generic "transaction" with address + data, common in bus protocols.
🔍 Function 1: display()
function void display();
$display("Transaction: %h", addr);
endfunction: display
Prints the value of addr.
Note: The label : display at the end helps match this endfunction to its function
name—very helpful when blocks are nested.
🔍 Function 2: calc_csm()
function void calc_csm();
csm = addr ^ data.xor;
endfunction: calc_csm
Calculates a simple checksum using XOR:
o addr ^ data.xor means XOR the address with the XOR of all elements in
data.
o data.xor is a built-in SystemVerilog reduction method that XORs all array
elements together.
The result is stored in csm.
✅ Class End Label
endclass: Transaction
This ends the class. The label : Transaction helps you trace which class block is closing,
especially when there are multiple nested blocks or long files.
📌 Style Note
Classes → Transaction, Packet → Start with uppercase.
Constants → DATA_WIDTH, MAX_SIZE → All uppercase.
Variables → addr, csm, opcode → Lowercase.
You’re free to follow any naming style, but consistency is key.
✅ Next Step: How to Use This Class
Would you like to see how to create an object from this class and use the display and
calc_csm methods in a testbench example?
Perfect—we're now learning how to organize your classes using packages, which is a
crucial skill in SystemVerilog for writing clean, modular code.
Let’s carefully walk through Samples 5.2 and 5.3, step by step.
📦 Sample 5.2: Declaring a Class Inside a Package
This example shows how to define a class (Transaction) inside a package.
// File: abc.svh
package abc;
class Transaction;
// Class body (data + functions/tasks)
endclass
endpackage
🧠 Why Use a Package?
Groups related definitions (classes, parameters, functions) into a single namespace
Makes your testbench modular and reusable
Allows multiple files to import and share the same class
🔁 How to Use the Package in Another File (Sample 5.3)
program automatic test;
import abc::*; // Bring in everything from package abc
Transaction tr; // Declare an object of type Transaction
// Test code goes here
endprogram
🧠 Explanation of Key Lines:
import abc::*;
o Brings in everything (*) from the package named abc.
o Now you can use Transaction without writing abc::Transaction.
Transaction tr;
o Declares a variable (handle) of class Transaction.
program automatic test;
o A program block is used in SystemVerilog testbenches (we'll cover this more
later).
o automatic means each execution gets its own storage (just like automatic
variables in a task).
✅ Summary So Far
Concept What it Does
package Groups classes and other definitions together
import abc::* Makes everything in abc available in your file
Transaction tr; Declares an object (handle) of class Transaction
🧪 Want to Try It?
Would you like me to show you a full example with:
A package file that defines the Transaction class, and
A test file that imports it, creates a transaction, and calls its methods?
This will help solidify what you've just learned.
You're doing an excellent job following along—this is a very important section because it
clarifies the core OOP terms you'll use constantly when writing SystemVerilog testbenches.
Let's walk through Section 5.5: OOP Terminology step by step, with simplified
explanations and comparisons to regular Verilog.
📘 OOP Terms in SystemVerilog — Simplified with Verilog
Comparison
OOP Term What It Means Verilog Analogy
Class A blueprint for creating objects. Like a module
Object An actual instance created from a class. Like an instantiated module
Handle A reference/pointer to an object. Like a module instance name
Property A variable inside a class. Like a reg or wire
Method A function or task inside a class. Like a task or function
Prototype The function/task header (no body yet). Like a function declaration
🏠 Analogy: Class vs Object vs Handle
Think of a Class Like a House Blueprint:
🏗️Class: The blueprint of a house (not a real house yet).
🏠 Object: A real house built using the blueprint.
📬 Handle: The house’s address. It uniquely identifies the object.
Inside the House:
💡 Properties: Lights, temperature, door locks — variables that store state.
🔘 Methods: The switches or remotes — functions that change or use those properties.
🔧 Example in SystemVerilog
class House;
bit porch_light;
function void turn_on_porch_light();
porch_light = 1;
endfunction
endclass
module test;
initial begin
House h1; // Declare a handle
h1 = new(); // Create the object
h1.turn_on_porch_light(); // Call a method
end
endmodule
🧠 Breakdown:
House is the class (like a blueprint).
h1 is a handle (like a house address).
new() constructs the actual object (the real house).
turn_on_porch_light() is a method that sets the state.
🔄 Verilog vs OOP: When They "Act" Matters
Feature Verilog Modules SystemVerilog Classes
Created at Compile time Run time
Used for Structural design Testbench behavior
Reusability Limited High (via OOP features)
✅ Summary of This Step
You now understand:
The key OOP terms and how they map to Verilog concepts
Why we use classes (blueprints) to build objects (real things)
What a handle is (a reference to an object)
The meaning of properties and methods
Would you like to move to the next step, where we’ll start using new() and dynamic object
creation (Section 5.6), or do you want a short exercise to practice what we’ve learned so far?
You're continuing to move through the OOP basics really well. Now, in Section 5.6.1, you're
learning about something fundamental to using classes in SystemVerilog: handles and the
new() function.
Let’s break this down clearly and slowly so you understand exactly what’s happening.
🧠 Key Concepts in This Section
🔹 What is a Handle?
A handle is a pointer to an object in SystemVerilog.
Example:
Transaction tr; // 'tr' is a handle to a Transaction object
But at this point, tr is not yet connected to an actual object. It’s just a reference and
is initialized to null by default.
🔹 What is new()?
The new() function constructs (creates) the object in memory and returns its address
(which the handle stores).
It:
o Allocates memory
o Initializes values (e.g., 0 for 2-state bit, X for 4-state logic)
o Returns a handle to the object
🔹 Analogy to C
If you've used C:
Transaction tr; is like Transaction* tr = NULL;
tr = new(); is similar to tr = malloc(sizeof(Transaction)); — it allocates
memory dynamically.
🧾 Sample 5.4 (Implied in the Text)
Let me write the code that this section is describing:
class Transaction;
bit [31:0] addr, csm;
bit [31:0] data[8];
endclass
module test;
initial begin
Transaction tr; // Declare a handle (set to null)
tr = new(); // Create (construct) a Transaction object in memory
end
endmodule
🧠 What Happens in Memory?
Assuming:
addr = 32 bits
csm = 32 bits
data[8] = 8 × 32 bits
That’s 10 × 32-bit words = 40 bytes.
But SystemVerilog also adds some internal housekeeping:
Type info
Virtual method pointers
Possibly more memory if you use logic (4-state)
So new() allocates enough space for all this, just like an object constructor in C++ or a
class instantiation in Java.
✅ Summary
Concept Meaning
tr A handle (pointer) to an object
new() Allocates memory and creates an object
null The initial value of any handle before new()
Instantiation The act of creating a new object
Constructor Another term for the new() method
Would you like to move on to custom constructors (user-defined new() functions) next?
Awesome! You're now getting into custom constructors in SystemVerilog, a key step
toward writing reusable and clean object-oriented testbenches. Let's go through this slowly
and clearly.
🧠 What is a Custom Constructor in SystemVerilog?
The new() function is a special method in SystemVerilog classes.
It’s used to initialize class properties at the time of object creation.
You don’t specify a return type for new() — SystemVerilog automatically returns
the correct object type.
✅ Sample 5.5: Basic Custom Constructor
class Transaction;
logic [31:0] addr, csm, data[8];
function new();
addr = 3; // set a fixed value
data = '{default:5}; // fill all elements with 5
endfunction
endclass
Explanation:
When you call Transaction tr = new();, it:
1. Allocates memory
2. Sets addr = 3
3. Sets all 8 data elements to 5
4. Leaves csm uninitialized (default = X)
✅ Sample 5.6: Constructor with Arguments
class Transaction;
logic [31:0] addr, csm, data[8];
function new(input logic [31:0] a = 3, d = 5);
addr = a;
data = '{default:d};
endfunction
endclass
module test;
initial begin
Transaction tr;
tr = new(.a(10)); // 'addr' becomes 10, 'data[]' is all 5 (default)
end
endmodule
Key Points:
You can pass arguments to new() just like to any function.
If you don't pass an argument, the default value is used.
🔎 How Does SV Know Which new() to Use?
SystemVerilog chooses the constructor based on the handle’s type.
See Sample 5.7:
class Transaction;
logic [31:0] addr, csm, data[8];
endclass
class Driver;
Transaction tr;
function new();
tr = new(); // This calls Transaction’s new(), NOT Driver’s
endfunction
endclass
Even though the Driver class has its own new() function, the call tr = new(); invokes
Transaction’s constructor because tr is a Transaction handle.
🧠 Summary of Section 5.6.2 and 5.6.3
Concept Explanation
new() Special function to construct and initialize objects
Custom constructor You define new() with your own initialization logic
Default values Let you avoid repeating code when default setup is fine
Constructor with args Lets you pass custom values when building an object
Handle type decides SV uses the class of the handle on the left to choose the constructor
new() to call
Would you like to try creating your own class with a constructor that takes arguments, or
move on to Section 5.6.3: Separating declaration and construction?
You're doing a great job going step by step through the SystemVerilog OOP chapter. Let's
walk through Sections 5.6.3 and 5.6.4 together, as both are very important for writing
predictable and maintainable code.
🔹 5.6.3: Separating the Declaration and Construction
🧠 Key Idea:
Avoid this:
Transaction tr = new(); // BAD practice
While legal, it's not recommended because:
1. It calls the constructor (new()) before the simulation enters the procedural block.
2. You might lose control over initialization order, especially when several objects
depend on each other.
3. If you’re in a non-automatic block, the object is constructed only once at time zero,
not each time the block runs.
✅ Recommended Practice:
Instead, separate declaration and construction like this:
Transaction tr; // Declare handle
initial begin
tr = new(); // Construct object inside a procedural block
end
This gives you:
Precise control over when objects are constructed.
Better clarity in simulation flow and debug.
🔹 5.6.4: The Difference Between new() and new[]
Both new() and new[] are used to allocate memory, but they serve very different purposes.
Expression Purpose Used For
new() Construct a single object Classes (objects)
new[] Allocate a dynamic array Arrays of values
✅ Example of new() (for classes):
Packet p;
p = new(); // Creates ONE Packet object
✅ Example of new[] (for arrays):
int my_array[]; // Declare dynamic array
my_array = new[10]; // Allocate space for 10 integers
⚠️Note: new() can take arguments (for constructors), but new[] can only take the array size.
🧠 Summary
Concept Bad Practice Good Practice
Transaction tr = Transaction tr; tr =
Object declaration + construction new(); new();
For creating one object Use new()
For creating array of
Use new[]
objects/values
Would you like to move on to the next section (likely about destructors or object usage), or
try writing your own example using both new() and new[]?
You're doing a great job staying focused through this chapter—Section 5.6.5 is crucial
because it teaches a concept that often confuses new SystemVerilog OOP users: the
difference between a handle and an object.
Let's walk through it step-by-step.
🔹 5.6.5: Getting a Handle on Objects
✅ Key Principle:
A handle is not the object itself. It’s just a reference (or pointer) to where the object lives in
memory.
📌 Sample 5.8 — Allocating Multiple Objects
Transaction t1, t2; // Declare two handles
t1 = new(); // Create 1st object
t2 = t1; // t2 now points to the same object as t1
t1 = new(); // Create 2nd object; t1 now points to a new object
🧠 What’s Happening?
Step Handle t1 Handle t2 Objects in Memory
After t1 = new(); → Obj1 null 1 object
After t2 = t1; → Obj1 → Obj1 still 1 object
After t1 = new(); → Obj2 → Obj1 now 2 objects
t2still points to the first object
t1now points to a second object
Both handles are pointing to separate memory locations
🧠 Real-World Analogy (from the book):
Think of badges at a conference.
People = Objects
Badges = Handles
You can hand your badge to someone else → like assigning t2 = t1
You can create a new badge → like t1 = new();
You can reuse or lose a badge → the handle can be reassigned or set to null
🔍 Why is this useful?
In traditional Verilog, you had to predefine a fixed number of "things" (like transactions):
Transaction tr_array[100]; // Fixed-size array
If you needed 101, you’d be stuck.
✅ But with dynamic objects, you can:
Create as many as needed during simulation:
for (int i = 0; i < num_trans; i++) begin
tr = new(); // Create on demand
// do something with tr
end
Reuse handles to point to different objects as needed
Dynamically manage memory (more on that in the next section: deallocation)
📌 Summary
Concept Meaning
Handle A reference or pointer to an object
Object The actual memory-allocated data structure
t2 = t1; Now both handles point to the same object
t1 = new(); Creates a new object; t1 points to it, t2 still points to old one
Dynamic object creation More flexible and memory-efficient than static arrays
Would you like to continue to Section 5.7: Object Deallocation, which explains what
happens to unused objects?
Great! You're now on Section 5.7 – Object Deallocation, which wraps up the basics of
memory management in SystemVerilog’s OOP model. Let’s break it down clearly and
slowly.
🔹 5.7 Object Deallocation – Reclaiming Memory
🧠 What’s the problem?
When your testbench creates thousands of transactions, memory usage can grow quickly. If
unused objects aren't cleaned up, your simulation may run out of memory.
✅ How does SystemVerilog handle this?
Through Garbage Collection — an automatic process that:
Tracks handles (pointers) to objects.
Frees the object’s memory when no handles point to it.
This is called reference counting (conceptually, though simulators might optimize it
differently).
📌 Example (Sample 5.9, explained in steps):
Transaction t; // Declare a handle
t = new(); // Creates object #1
t = new(); // Creates object #2, t now points to it, object #1 has no
handle → deleted
t = null; // t points to nothing, object #2 has no handle → deleted
So:
After t = new(); (second time), object #1 is unreachable → deleted.
After t = null; → object #2 is unreachable → deleted.
📍 Key Points:
Concept Meaning
Garbage Automatic memory cleanup of unreferenced objects
Concept Meaning
collection
Reference
Objects are deleted when no handles point to them
counting
null handle Explicitly clearing a handle (i.e., t = null;) helps GC
If objects are in linked structures or threads, you must clear those
Manual cleanup
references manually
🚫 When Objects Aren’t Deleted Automatically:
1. Still referenced in:
o Arrays
o Queues
o Linked lists
o Class properties
2. Active threads (forked processes inside the object)
To free them, you must set all handles to null or let the thread complete.
🔄 Analogy Recap (from 5.6.5):
Think of each object as a person at a conference, and the badge as the handle.
If nobody holds the badge, the organizer can free the seat (delete the object).
But if someone still holds the badge, even if the person is gone, the seat isn’t freed.
✅ Summary:
Objects live in memory until no handles point to them.
SystemVerilog automatically deallocates them — you don’t manually free() them
like in C.
But if you keep handles around (e.g., in arrays) or spawn threads, they stick
around — so be careful.
Would you like to try a small example using multiple objects and handle clearing? Or shall
we move on to Section 5.8: Class Members?
You're doing great so far! Now you're on Section 5.8 – Using Objects in SystemVerilog’s
OOP. This is a key part where everything you’ve learned—classes, objects, and handles—
starts being used in real code.
🔹 5.8 Using Objects
Once you've created an object using new(), you interact with it using the dot (.) operator
to access:
Properties (variables)
Methods (functions/tasks)
🧾 Sample 5.10 Explained
Transaction t; // Declare a handle
t = new(); // Construct a new object (allocate memory)
t.addr = 32'h42; // Set the value of the addr variable
t.display(); // Call a method (function) on the object
✅ This works just like how you'd access module members in Verilog (inst.signal)—but
here, you're using it on a class object.
🧠 OOP Best Practice: Encapsulation
In strict object-oriented programming (OOP), you're not supposed to access class variables
directly. Instead, you should use:
get_addr() – to read a variable
set_addr(val) – to set a variable
Why?
Because if you ever change how addr is calculated or stored, you don’t have to change
every place it's used—only the method logic.
But…
❗ Reality in Testbenches
SystemVerilog testbenches are different from full-blown software systems. So:
Direct variable access is allowed and encouraged in testbenches.
This is because:
o You want maximum control over your test data.
o You're often using randomization to generate values (addr = $urandom();).
o Hiding variables behind functions makes constrained-random testing
harder.
✅ When to Use Getters/Setters (get/put methods)
Only when you're:
Developing reusable Verification IP (VIP)
Selling or sharing your class with others as a black box
Trying to enforce data integrity
Example:
class PCI_Packet;
protected bit [15:0] address;
function void set_addr(bit [15:0] a);
address = a;
endfunction
function bit [15:0] get_addr();
return address;
endfunction
endclass
In your own testbench, this is not necessary unless you need encapsulation for reuse or
security.
🔄 Summary
Concept Real-world Advice
Use . to access members obj.addr = 5; and obj.display();
Avoid unnecessary get/set methods Unless you're building reusable IP
Testbench goal = control, not hiding data So direct access is OK! ✅
Ready to move on to 5.9 Class Methods, where we dive deeper into how to write and use
functions/tasks in your classes?
Great! You're now at Section 5.9 – Class Methods, a natural continuation of using objects.
Let's break this down clearly.
🔹 What Is a Method?
In SystemVerilog, a method is just a:
function or
task
…that is defined inside a class.
Methods are how you make objects do things — print themselves, calculate values, send data,
etc.
🧾 Sample 5.11 – Explained
class Transaction;
bit [31:0] addr, csm, data [8];
function void display();
$display("@%0t: TR addr=%h, csm=%h, data=%p", $time, addr, csm, data);
endfunction
endclass
This is a method display() inside class Transaction.
%p is used to print the whole array data.
class PCI_Tran;
bit [31:0] addr, data;
function void display();
$display("@%0t: PCI: addr=%h, data=%h", $time, addr, data);
endfunction
endclass
Another class with its own method named display().
Then, we have:
Transaction t;
PCI_Tran pc;
initial begin
t = new(); // Construct a Transaction object
t.display(); // Calls Transaction::display()
pc = new(); // Construct a PCI_Tran object
pc.display(); // Calls PCI_Tran::display()
end
🧠 Key Concepts
Even though both classes have a method named display(), SystemVerilog knows
which one to call based on the object handle.
This is an early look at something called polymorphism — the same method name
doing different things in different classes (we’ll go deeper into that in Chapter 8).
🧪 Output Example
The simulation output might look something like:
@0: TR addr=00000000, csm=xxxxxxxx, data='{0, 0, 0, 0, 0, 0, 0, 0}
@0: PCI: addr=00000000, data=00000000
(Assuming no initialization of variables, so they default to 0 or X.)
🔄 Summary
Concept Example
Method = function/task in a class function void display()
Same method name in multiple classes is
t.display() vs. pc.display()
OK
Handle t → class Transaction, so it uses that
Calls resolved by handle type
version
Would you like to move on to Section 5.10 – Prototypes and Out-of-Body Methods next?
Perfect! Let's move into Section 5.10 – Prototypes and Out-of-Body Methods — a key part
of writing clean, modular OOP code in SystemVerilog.
🔹 What Is a Prototype?
A prototype is just the function or task header — basically, the method's declaration,
without its body.
In SystemVerilog, especially for large classes, it's a good idea to:
Declare (prototype) the method inside the class
Define (write the full body) the method outside the class
This makes the class declaration cleaner and easier to read.
🧾 Example: Declaring vs. Defining a Method
✅ Prototype inside class:
class Transaction;
bit [31:0] addr;
function void display(); // Just a prototype (no body here)
endclass
✅ Out-of-body definition:
function void Transaction::display();
$display("Transaction addr = %h", addr);
endfunction
Transaction::display() tells SystemVerilog:
This is the method display() that belongs to the Transaction class.
🧠 Why Use This?
1. Cleaner class definitions: Especially if your class has lots of methods.
2. Easier maintenance: You can organize code like a header + source file model.
3. Clearer interface: You can see what the class does without reading every method
body.
📌 Syntax Breakdown
function [return_type] ClassName::FunctionName ([args]);
// function body
endfunction
Likewise for tasks:
task ClassName::TaskName ([args]);
// task body
endtask
🧪 Example With Task
class Packet;
bit [15:0] src, dest;
task print(); // Prototype only
endclass
task Packet::print(); // Body outside
$display("src = %h, dest = %h", src, dest);
endtask
✅ Summary
Term Meaning
Prototype Declares a method inside a class (no body)
Out-of-body method Defines the method body outside the class
Term Meaning
ClassName::methodName() Links the method to the class
Would you like to continue to Section 5.11 – Class Data Initialization?
Great — you're doing an excellent job progressing through the chapter!
🔹 5.11 Static Variables vs. Global Variables
This section introduces the static keyword — an essential tool for creating shared data
across all objects of a class without using global variables.
🔸 Local vs. Shared Variables
By default, every time you call new() to create an object, it gets its own copy of the
class variables.
Example:
Transaction t1 = new(); // has addr, csm, data
Transaction t2 = new(); // has its own addr, csm, data
These two objects do not share any of their variables.
🔸 What if you want to share?
Let’s say you want to keep a counter of how many transactions were created. If you used
a global variable, every part of your testbench could see and modify it — even by mistake.
That’s risky.
✅ Static Variable to the Rescue
A static variable in a class is:
Shared among all objects of the class
Stored only once, not per object
Not tied to any particular object
🧾 Example: Static Variable to Count Transactions
class Transaction;
static int count = 0; // Shared across all objects
function new();
count++; // Every time a new object is created, increment
endfunction
endclass
You could then print how many transactions were created:
$display("Total transactions: %0d", Transaction::count);
Note: We access static members using the class name like Transaction::count, not through
an object like t.count.
❗ Don’t Confuse With Global Variables
Global Variable Static Class Variable
Visible everywhere Visible only in the class
Pollutes namespace Encapsulated (tidier)
Anyone can modify Controlled access
📌 Summary
Use static to create shared variables within a class.
Avoid global variables unless absolutely necessary.
Access static variables using ClassName::variable.
Would you like to move on to 5.12 Static Methods, which continue building on this
concept?
Great — you’re moving through this OOP chapter in SystemVerilog smoothly!
🔹 5.11.1 A Simple Static Variable
This section expands on what we just discussed about static variables by applying it in a
practical use case: assigning unique IDs to each object when it's created.
🧾 Sample Code Breakdown: Assign Unique IDs to Each Object
Here's a corrected and cleaned-up version of the code from Sample 5.14:
class Transaction;
static int count = 0; // Shared among all objects
int id; // Unique to each object
function new();
id = count; // Assign current count as this object's ID
count++; // Increment count for the next object
endfunction
endclass
module test;
Transaction t1, t2;
initial begin
t1 = new(); // First object
$display("First: id = %0d, count = %0d", t1.id, Transaction::count);
t2 = new(); // Second object
$display("Second: id = %0d, count = %0d", t2.id, Transaction::count);
end
endmodule
🔍 Key Concepts
Feature What it does
static int count One shared variable for all objects
int id Each object has its own unique id
new() Constructor sets id = count, then increments count
Transaction::count Access static variable via class name
📊 Visualizing It
After t1 = new();:
t1.id = 0
Transaction::count = 1
After t2 = new();:
t2.id = 1
Transaction::count = 2
So:
count is shared and keeps incrementing.
id is unique per object.
This is very useful in testbenches for tracking which object is doing what (e.g., tracing
packets or transactions).
✅ Why this is Better Than Globals
No pollution of global scope
Easier to debug and trace
Self-contained logic within the class
Would you like to proceed to 5.12 Static Methods, where we learn how to define
functions/tasks that belong to the class rather than any object?
Great! You're now diving into static methods, a powerful OOP concept in SystemVerilog.
🔹 5.11.4 Static Methods
✅ What is a Static Method?
A static method is:
A method (function or task) that belongs to the class itself, not to any particular
object.
It can only access static variables.
It can be called even before any object is created.
🔧 Sample 5.17: Explained
class Config;
int num_trans;
function new(int val);
num_trans = val;
endfunction
endclass
class Transaction;
static Config cfg;
static int count = 0;
int id;
// Static method
static function void display_statics();
if (cfg == null)
$display("ERROR: configuration not set");
else
$display("Transaction cfg.num_trans=%0d, count=%0d", cfg.num_trans,
count);
endfunction
endclass
And the calling code:
module test;
Config cfg;
initial begin
cfg = new(42); // Create a Config object
Transaction::cfg = cfg; // Assign it to the static cfg
variable
Transaction::display_statics(); // Call static method without
creating any Transaction objects
end
endmodule
🔍 Key Points
Concept Description
static Config cfg; One shared config object for all Transaction instances
display_statics() Static method that reads cfg and count
Transaction::display_statics() Called using the class name, not an object
Because id is an instance variable, and no objects
Cannot access id
exist yet
❗ Why Can't Static Methods Access Instance Variables?
Instance variables (like id) are only created when you new an object. If no object exists,
there's no memory allocated for those variables — so trying to access them would cause
errors.
🧠 Think of It Like This:
Static methods are like announcements made on a loudspeaker — anyone can hear
them, even if no person is standing nearby.
Instance methods are like talking to someone — there has to be a specific person
(object) there.
Would you like to move on to 5.12 Inheritance, which introduces how one class can reuse
and extend another?
Great! You're now exploring SystemVerilog scoping rules, which are essential for writing
clean, bug-free testbenches. Let's summarize and clarify the key points from Section 5.12
using your provided content:
🔍 5.12 Scoping Rules — Summary
🔹 What Is a Scope?
A scope is any block of code that can contain variables:
Module
Program
Task
Function
Class
Begin-end block
for / foreach loop
Each of these can declare its own local variables.
✅ Variable Lookup in SystemVerilog
SystemVerilog resolves variable names using hierarchical lookup:
1. Starts at the current scope.
2. If not found, it goes upward through the enclosing scopes.
3. You can use $root to access a global scope.
$root.top.limit // Absolute path to variable
⚠️Danger of Name Clashes
If a variable is not declared in a local scope, SystemVerilog might find a matching variable
in a higher scope, often silently and incorrectly.
❌ Example (Sample 5.19):
program automatic test;
int i;
class Bad;
logic [31:0] data[];
function void display();
// Forgot to declare i
for (i = 0; i < data.size(); i++)
$display("data[%0d] = %x", i, data[i]);
endfunction
endclass
endprogram
iis not declared in the function, so it uses test.i.
Modifies a global variable unintentionally. Big debugging headache.
✅ Fix: Encapsulation in a Package
If you move the class to a package, it won’t see program-level variables like i.
✔️Example (Sample 5.20):
package Better;
class Bad;
logic [31:0] data[];
function void display();
for (i = 0; i < data.size(); i++) // ❌ Compile error now
$display("data[%0d] = %x", i, data[i]);
endfunction
endclass
endpackage
Now i must be declared inside display() — helps catch the bug at compile time.
🔄 Best Practices
Tip Why
Declare variables at the innermost scope Prevents accidental reuse/conflicts
Use different variable names in different scopes Improves clarity
Move classes to packages Encapsulates logic and avoids name collisions
Avoid depending on $root references They reduce modularity and portability
Would you like to proceed to 5.13 Inheritance, where we begin reusing and extending
classes?
Great, you're diving into 5.12.1: What is this? — a fundamental concept in object-oriented
programming (OOP), especially relevant when writing constructors or methods that deal with
name shadowing.
✅ What is this in SystemVerilog?
The keyword this is used to refer explicitly to a class member (variable or method) when
there’s a naming conflict with local variables or function arguments.
📌 Common Use Case: Constructor Argument Name == Class Variable
Here's the classic example:
🔁 Without this:
class Transaction;
string name;
function new(string name);
name = name; // ❌ Ambiguous: both refer to the argument!
endfunction
endclass
This assigns the parameter to itself, not to the class member.
✅ With this:
class Transaction;
string name;
function new(string name);
this.name = name; // ✅ Left = class variable, Right = argument
endfunction
endclass
this.name refers to the class variable.
name alone refers to the function argument.
Now it works as intended.
📦 Sample 5.21 Recap (paraphrased)
You're likely referencing this sample:
class Transaction;
bit [31:0] addr, crc, data[8];
Statistics stats;
endclass
class Statistics;
time startT, stopT;
static int ntrans = 0;
static time total_elapsed_time;
endclass
This shows one class using another class via a member object (Statistics stats;) —
covered more in 5.13, but here it introduces the this keyword for clear variable access.
🧠 Note on Readability
Some developers like using identical names for clarity:
function new(string name);
this.name = name;
endfunction
Others think it's confusing or lazy, and prefer:
function new(string name_input);
name = name_input;
endfunction
Both are valid — it’s a style choice, though using this is standard in many OOP languages
and helpful for auto-generated code and IDEs.
Would you like to move on to 5.13: Using One Class Inside Another?
Here’s the full SystemVerilog code for the example discussed in Section 5.13, including
both the Statistics class and the Transaction class that contains it:
✅ Full SystemVerilog Code: Class Containment Example
// Define the Statistics class
class Statistics;
time startT; // Start time of the transaction
static int ntrans = 0; // Total number of transactions
static time total_elapsed_time = 0; // Cumulative time
// Start the timer
function void start();
startT = $time;
endfunction
// Stop the timer and update counters
function void stop();
time how_long = $time - startT;
ntrans++;
total_elapsed_time += how_long;
endfunction
endclass
// Define the Transaction class that contains a Statistics object
class Transaction;
bit [31:0] addr, csm, data[8]; // Transaction data
Statistics stats; // Handle to Statistics object
// Constructor to instantiate Statistics
function new();
stats = new();
endfunction
// Simulate transmitting the transaction and track timing
task transmit_me();
stats.start(); // Record start time
#100; // Simulate transmission delay
stats.stop(); // Record stop time and update stats
endtask
endclass
// Testbench module
module test;
Transaction t;
initial begin
t = new(); // Create a Transaction object
t.transmit_me(); // Simulate sending the transaction
// Display the results
$display("Total Transactions = %0d", Statistics::ntrans);
$display("Total Time Elapsed = %0t", Statistics::total_elapsed_time);
end
endmodule
🔍 What This Code Demonstrates:
Transaction class uses a Statistics object (stats) to time the simulated
transmission.
Static members of Statistics (ntrans and total_elapsed_time) are shared across
all Transaction instances.
The testbench runs a single transaction and prints the statistics at the end.
Would you like me to modify this to show multiple transactions, or add assertions/debug
printouts inside the methods?
This section (5.13.1) is emphasizing good design principles for using classes in
SystemVerilog. Here's the core idea:
✅ How Big or Small Should a Class Be?
Too Small?
Avoid creating a class that only holds one or two variables unless there’s a
compelling reason (e.g., frequent reuse, encapsulated behavior like randomization
with constraints).
Example of too small:
class Addr;
rand bit [31:0] a;
endclass
Creating tiny classes like this for every field adds unnecessary complexity, extra
indirection, and harder debugging.
Too Big?
If a class has too many methods and variables (often more than a page long),
consider splitting it into logical units or sub-classes for clarity, reusability, and
maintenance.
🔁 Practical Guideline:
Do group related data and behavior into one class.
Do not over-engineer with too many tiny classes unless you're building reusable IP
or frameworks.
Use encapsulation and partitioning when the class grows organically large or covers
unrelated functionality.
📌 Summary Rule of Thumb:
"A class should represent a coherent concept. If it's too fine-grained or overly broad,
refactor."
Would you like a real-world SystemVerilog example that shows refactoring a too-small or
too-big class into a better structure?
5.13.2 Compilation Order Issue
When a class refers to another class that is not yet defined, the compiler will raise an error. You can
solve this by using a forward class declaration with typedef.
✅ Solution:
systemverilog
CopyEdit
typedef class Statistics; // Forward declaration
class Transaction;
Statistics stats; // OK now
endclass
class Statistics;
// Definition comes later
endclass
Great! Here's a complete SystemVerilog example that demonstrates:
✅ Class hierarchy
✅ typedef class forward declaration
✅ Dynamic object creation
✅ Static variables and methods
✅ Passing objects via handles
✅ Object composition (class inside another)
🌐 Full Example: Transaction, Statistics, Transmitter
// Typedef forward declaration to avoid compilation order issues
typedef class Statistics;
// Statistics class definition
class Statistics;
time startT;
static int count = 0;
static time total_elapsed_time;
function void start();
startT = $time;
endfunction
function void stop();
time elapsed = $time - startT;
count++;
total_elapsed_time += elapsed;
endfunction
static function void show_stats();
$display("Total transactions = %0d, Total time = %0t", count,
total_elapsed_time);
endfunction
endclass
// Transaction class that uses Statistics
class Transaction;
int id;
static int id_counter = 0;
Statistics stats;
function new();
this.id = id_counter++;
stats = new(); // instantiate inner object
endfunction
task execute();
$display("Transaction ID %0d starting...", id);
stats.start();
#10; // simulate some activity
stats.stop();
$display("Transaction ID %0d completed.", id);
endtask
endclass
// Transmitter class that sends transactions
class Transmitter;
function void send(Transaction t);
t.execute(); // handle is passed, modifies original object
endfunction
endclass
// Test module
module test;
Transaction t1, t2;
Transmitter tx;
initial begin
tx = new();
t1 = new();
t2 = new();
tx.send(t1);
tx.send(t2);
// Display aggregated stats from static method
Statistics::show_stats();
end
endmodule
🧠 Concepts Illustrated:
Concept Where it's used
typedef class Statistics; Forward declare Statistics
Object inside another class Transaction contains a Statistics
Static vars & methods Statistics::count, show_stats()
Dynamic object creation t1 = new();, stats = new();
Passing handles tx.send(t) passes handle to execute()
Would you like a version of this using constrained-random stimulus too?
Thanks! Based on Section 5.14.1 you’ve quoted, here’s a SystemVerilog example that
demonstrates the key concept:
✅ When you pass an object to a method, you're passing the handle, not the object itself.
✅ This allows the method to modify the object's contents—but not the handle itself
(unless passed by ref).
✅ Sample Code: Passing Objects to Methods
class Transaction;
rand bit [31:0] addr;
rand bit [31:0] data[4];
function void display();
$display("addr = %h, data = %p", addr, data);
endfunction
endclass
// Task to transmit a transaction
task transmit(input Transaction tr);
// Corrupt the first data word
tr.data[0] = ~tr.data[0];
$display("Inside transmit:");
tr.display();
endtask
module test;
Transaction t;
initial begin
t = new();
t.addr = 32'hDEADBEEF;
t.data = '{32'hAAAA_BBBB, 32'h1234_5678, 32'h0, 32'hFFFF_FFFF};
$display("Before transmit:");
t.display();
transmit(t); // Pass handle
$display("After transmit:");
t.display();
end
endmodule
🧠 Key Learning Points:
Concept Description
transmit(input Transaction
tr) The object is passed by value (i.e., the handle is copied).
Changes persist because tr and t both point to the same
Modifies tr.data[0]
object.
Would require ref, but not needed to change the contents of
Modifying handle itself
object.
🧪 Output Would Look Like:
Before transmit:
addr = deadbeef, data = '{aaaa_bbbb, 1234_5678, 0, ffff_ffff}
Inside transmit:
addr = deadbeef, data = '{5555_4444, 1234_5678, 0, ffff_ffff}
After transmit:
addr = deadbeef, data = '{5555_4444, 1234_5678, 0, ffff_ffff}
Would you like to see the ref version next, showing how to modify the handle itself?
Great — this section clearly explains the difference between modifying the object vs.
modifying the handle that points to the object in SystemVerilog.
Here’s a complete working example that shows both the wrong way and the correct way to
create and assign a new object from inside a function.
❌ Sample 5.26 — Incorrect (No ref → handle isn't updated)
class Transaction;
bit [31:0] addr;
endclass
function void create(Transaction tr); // ❌ Bug: missing ref
tr = new(); // Creates new object, but only
updates local copy
tr.addr = 42;
endfunction
module test_bad;
Transaction t;
initial begin
create(t); // Passes NULL to create()
if (t == null)
$display("t is still NULL after create()!"); // Will print this
else
$display("t.addr = %0d", t.addr);
end
endmodule
✅ Sample 5.27 — Correct (Use ref to update handle)
class Transaction;
bit [31:0] addr;
endclass
function void create(ref Transaction tr); // ✅ Correct: ref
tr = new(); // Now updates caller's handle
tr.addr = 42;
endfunction
module test_good;
Transaction t;
initial begin
create(t); // Updates t to point to a new Transaction
if (t == null)
$display("t is still NULL!");
else
$display("t.addr = %0d", t.addr); // Will print 42
end
endmodule
🧠 Summary
Situation Use input (default) Use ref
Modify object properties (fields) ✅ Yes Optional
Modify the handle itself (e.g., new) ❌ No ✅ Required
Would you like to see how to write a method that creates and returns a new object instead of
modifying one via ref?
Great follow-up — let’s clarify how the behavior changes with and without ref, especially
in terms of:
1. The handle itself (i.e., what the variable points to)
2. The object the handle points to (i.e., the actual data/fields in the object)
🔍 SystemVerilog Handle Behavior
🔸 Case 1: Without ref (Default: input)
When you pass a handle without ref, a copy of the handle is passed to the function or task.
That means:
✅ You can change the fields of the object the handle points to.
❌ You cannot reassign the caller’s handle to point to a new object — the change is
local.
💡 Example (Without ref)
class Transaction;
int addr;
endclass
function void update(Transaction tr); // no ref
tr.addr = 99; // ✅ Updates the object's field (works)
tr = new(); // ❌ Creates a new object, but only changes the local
copy
tr.addr = 42;
endfunction
Transaction t;
initial begin
t = new();
t.addr = 1;
update(t);
$display("t.addr = %0d", t.addr); // Output: 99, not 42
end
✅ Explanation:
The object field addr was updated to 99 using the original object.
But then a new object was created, and only the local copy of the handle (tr)
pointed to it.
So the change to addr = 42 went to the new object — and the caller’s handle t still
points to the original object.
🔸 Case 2: With ref
When you pass the handle with ref, any reassignment or modification affects the actual
handle in the caller’s scope.
💡 Example (With ref)
class Transaction;
int addr;
endclass
function void update(ref Transaction tr);
tr = new(); // ✅ Reassign the caller’s handle to a new object
tr.addr = 42; // ✅ Set data in the new object
endfunction
Transaction t;
initial begin
update(t);
$display("t.addr = %0d", t.addr); // Output: 42
end
✅ Explanation:
A new object was created.
The ref keyword allows the caller’s handle t to now point to the new object.
Changes to fields like addr are reflected in the caller as well.
🧠 Summary
Action Without ref With ref
Modify object fields ✅ Works ✅ Works
Reassign handle to new object ❌ Local only ✅ Global
Caller sees new object ❌ ✅
Caller sees field changes ✅ ✅
Would you like a visual diagram to help lock this in?
This section is about a very common and subtle bug in SystemVerilog testbenches when
dealing with dynamic objects like transactions. Let's break it down so it’s crystal clear.
🧠 Key Idea: Reusing a Single Object = BAD
If you create only one object and reuse it in a loop — and if any other process (e.g.,
transmit) takes time or uses threads — you're modifying the same object while it's still
being used.
This leads to corruption of data — for example, all your transactions might appear to have
the same addr.
🔴 Buggy Code: Only One Object Created
❌ generator_bad:
task generator_bad (input int n);
Transaction t;
t = new(); // ❗ Only one object created
repeat (n) begin
t.addr = $random(); // Update same object each time
$display ("Sending addr=%h", t.addr);
transmit(t); // Transmit same object
end
endtask
❗ What’s wrong:
t is created once.
Each iteration modifies the same object.
If transmit() uses the object across time (e.g., with #delays or fork), you end up
changing the object while it's still in use.
You may see different $display values, but all the data actually transmitted might be
the last value.
✅ Correct Code: New Object Per Iteration
✅ generator_good:
task generator_good (input int n);
Transaction t;
repeat (n) begin
t = new(); // ✅ New object each time
t.addr = $random();
$display ("Sending addr=%h", t.addr);
transmit(t); // Each transmit gets a fresh object
end
endtask
✔ What this fixes:
Each Transaction object is unique.
Even if transmit() uses threading/delay, its data won't be overwritten.
Prevents race conditions and hard-to-debug simulation bugs.
🧠 Why this matters in SystemVerilog
Unlike in static Verilog, objects in SystemVerilog are dynamically allocated and accessed
via handles. When multiple handles point to the same object, any change via one handle
affects them all.
This is powerful but dangerous when misused — just like passing around references or
pointers in C++/Java.
✅ Final Tip:
Always ask yourself:
"Is this object shared across time or threads? If yes — create a fresh one."
Would you like a visual flowchart or diagram to illustrate this handle vs object reuse issue?
Certainly! To help you visualize how SystemVerilog handles and objects interact, especially
in the context of object reuse in loops, here's a clear diagram illustrating the concepts:
🧩 SystemVerilog Handle vs. Object Diagram
+---------------------+ +---------------------+
| Handle (t) |---->| Object (Transaction)|
+---------------------+ +---------------------+
| |
| |
v v
+---------------------+ +---------------------+
| addr = 42 | | addr = 99 |
| data[0] = 0xFF | | data[0] = 0xAA |
+---------------------+ +---------------------+
🔍 Key Points:
Handle (t): A reference to an object. In SystemVerilog, handles are used to point to
dynamically allocated objects.
Object (Transaction): The actual instance of a class containing data members like
addr and data.
Reusing Handles: If you reuse the same handle (t) without creating a new object in
each iteration, all iterations will modify the same object, leading to potential data
corruption.
✅ Best Practice: Create a New Object in Each Iteration
To ensure each transaction is independent and avoids overwriting data, instantiate a new
object in each loop iteration:
task generator_good(input int n);
Transaction t;
repeat (n) begin
t = new(); // Create a new object each time
t.addr = $random(); // Assign random address
t.data[0] = $random(); // Assign random data
$display("Sending addr=%h, data[0]=%h", t.addr, t.data[0]);
transmit(t); // Send the transaction
end
endtask
By doing this, each transaction has its own unique object, preventing unintended data sharing
between iterations.
If you have further questions or need more detailed explanations on any part, feel free to ask!
You're absolutely right, and this section highlights an important concept in SystemVerilog
OOP: understanding the difference between objects and handles, especially in arrays.
✅ Let's Clarify: What Is an Array of Handles?
In SystemVerilog:
An object is an instance of a class, created using new().
A handle is a reference (or pointer) to that object.
You can create an array of handles, not actual objects.
Every element in the array starts as null unless you explicitly call new().
🔧 Example Code: Sample 5.30 – Array of Handles
class Transaction;
rand bit [31:0] addr;
function void display();
$display("Address = %h", addr);
endfunction
endclass
Transaction tarray[10]; // Array of 10 handles
initial begin
// Step 1: Create a new object at each index
foreach (tarray[i]) begin
tarray[i] = new();
tarray[i].addr = $random;
end
// Step 2: Use/display the objects
foreach (tarray[i]) begin
tarray[i].display();
end
end
⚠️Common Pitfalls
Mistake Consequence
Not calling new() for each index Accessing tarray[i] will cause a null pointer error
Assuming you can new() the whole Won’t work. You must initialize each handle
array individually
💡 Summary
An array of handles is like an array of pointers in C/C++.
You must individually allocate objects for each element.
You can use this array to keep track of many transactions, packets, or other objects in
your testbench.
Would you like to see how to use dynamic arrays or queues of handles for even more
flexibility?
Great! You're exploring an important concept in SystemVerilog OOP: how object copying
works, and its implications when your class contains handles to other objects.
🧠 Key Concept: Shallow Copy vs. Deep Copy
When you copy an object using new original_obj; in SystemVerilog, it performs a
shallow copy:
Shallow Copy: Only the handles and simple variables are copied. If your object
contains a handle to another object (e.g., Statistics inside Transaction), then only
the handle is copied — not the object it points to.
Deep Copy: You manually clone the lower-level objects to ensure both outer and
inner objects are independent.
✅ Sample: Shallow Copy – Problem Demonstration
class Statistics;
time startT;
endclass
class Transaction;
bit [31:0] addr, csm, data [8];
static int count = 0;
int id;
Statistics stats;
function new();
stats = new(); // Create a new Statistics object
id = count++; // Give unique ID
endfunction
endclass
Transaction src, dst;
initial begin
src = new();
src.stats.startT = 42;
dst = new src; // Shallow copy of src
dst.stats.startT = 96; // Modifies src.stats as well!
$display("src.stats.startT = %0t", src.stats.startT); // Displays 96
(unexpected!)
$display("dst.stats.startT = %0t", dst.stats.startT); // Displays 96
end
⚠️What Happened?
After dst = new src;:
dst.stats and src.stats point to the same Statistics object.
Changing dst.stats.startT also changes src.stats.startT.
✅ Solution: Implement a Deep Copy Manually
You need to explicitly create a new instance for each handle inside the class. Here's how:
🔁 Deep Copy Version
class Statistics;
time startT;
function Statistics copy();
Statistics s = new();
s.startT = this.startT;
return s;
endfunction
endclass
class Transaction;
bit [31:0] addr, csm, data [8];
static int count = 0;
int id;
Statistics stats;
function new();
stats = new();
id = count++;
endfunction
function Transaction copy();
Transaction t = new();
t.addr = this.addr;
t.csm = this.csm;
t.data = this.data;
t.stats = this.stats.copy(); // Deep copy!
return t;
endfunction
endclass
Transaction src, dst;
initial begin
src = new();
src.stats.startT = 42;
dst = src.copy(); // Deep copy
dst.stats.startT = 96;
$display("src.stats.startT = %0t", src.stats.startT); // 42
$display("dst.stats.startT = %0t", dst.stats.startT); // 96
end
✅ Summary
Copy Method Affects Only New Object? Handles Re-created?
new original_obj; ❌ No — shared handles ❌ No
Custom copy() ✅ Yes — deep copy ✅ Yes
Would you like help building a utility function that performs deep copying for more complex
nested classes or arrays of objects?
You're diving into a very practical topic in SystemVerilog OOP: visibility control —
specifically, when to use public, local, or protected access to class members.
Let’s break it down clearly:
🔍 Visibility Keywords in SystemVerilog
Keyword Meaning
public Accessible from anywhere (default in SystemVerilog)
local Only accessible from within the same class
protected Accessible within the class and its subclasses (children)
✅ SystemVerilog’s Default: public
Unlike C++/Java (which default to private), SystemVerilog makes everything public
unless stated otherwise.
This is intentional, and in the context of testbenches, it’s actually helpful:
You often need full control over object internals (e.g., to force errors, adjust state).
Making class members local or protected would add complexity, especially
during debugging or fault injection.
🔧 Example
class Transaction;
bit [31:0] addr;
bit [31:0] data;
bit [31:0] checksum;
// Local method (private-style)
local function void compute_checksum();
checksum = addr ^ data;
endfunction
// Public method
function void set_fields(bit [31:0] a, b);
addr = a;
data = b;
compute_checksum();
endfunction
endclass
Transaction t;
initial begin
t = new();
t.set_fields(32'hABCD, 32'h1234);
// Direct access to public field — allowed
t.checksum = 32'h0BAD_C0DE; // simulate error injection
end
🧠 Why Not Make Everything local?
In normal software development:
You protect internal data to avoid bugs and promote stable APIs.
But in testbenches:
You want full control.
You often intentionally create bad inputs (like a wrong checksum).
Limiting access (e.g., making checksum local) makes this harder.
🚫 If You Used local or protected
local bit [31:0] checksum; // Now this line prevents external access!
initial begin
t = new();
t.checksum = 32'h1234; // ❌ Error: not accessible
end
To change it, you'd have to write a specific method just for test purposes — extra work!
✅ Summary
When to Use Use Case
public (default) Best for testbenches – easy debugging, injecting faults, fast development
local Use only when you need strict encapsulation within the class
protected Use in class hierarchies (e.g., base/derived classes sharing internals)
Would you like an example showing how protected is used in a class hierarchy (like a base
class and derived one)?
You're now stepping into the core structure of SystemVerilog testbenches — the layered
testbench architecture, which is a foundational concept in UVM (Universal Verification
Methodology) and object-oriented testbench design in general.
Let’s walk through the concept from this section and what it means in practice.
🔷 Layered Testbench Overview (Figure 5.9)
+--------------------+
| Test | ← Defines the stimulus scenario (e.g., random or
directed)
+--------------------+
↓
+--------------------+
| Generator | ← Creates transactions (packets) and sends them to
the driver
+--------------------+
↓
+--------------------+
| Driver | ← Drives transactions to the DUT interface (e.g.,
via bus signals)
+--------------------+
↓
+--------------------+
| DUT | ← Design Under Test (your RTL block)
+--------------------+
↑
+--------------------+
| Monitor | ← Listens to DUT output signals and reconstructs
transactions
+--------------------+
↓
+--------------------+
| Scoreboard | ← Compares expected vs. actual output transactions
+--------------------+
↓
+--------------------+
| Checker | ← Performs deeper protocol or data integrity checks
+--------------------+
↓
+--------------------+
| Functional Coverage| ← Tracks functional metrics (Did we hit all
scenarios?)
+--------------------+
Each block is a class, and they communicate using transaction objects.
💬 How Do These Blocks Exchange Data?
You use transaction objects (like Transaction class) as the standard "currency" passed
between blocks.
There are two main ways:
1. Procedural Method Calls
o The generator calls the driver's method and passes a transaction.
o The driver sends the transaction to the DUT.
2. generator.generate_transaction(t);
3. driver.drive(t);
4. Data Structures (e.g., FIFOs / Mailboxes)
o More flexible and realistic.
o The generator puts a transaction in a mailbox.
o The driver gets the transaction from the mailbox.
5. class Generator;
6. mailbox gen2drv;
7. function void generate();
8. Transaction t = new();
9. // fill t
10. gen2drv.put(t);
11. endfunction
12. endclass
13.
14. class Driver;
15. mailbox gen2drv;
16. task run();
17. Transaction t;
18. gen2drv.get(t);
19. // drive it to DUT
20. endtask
21. endclass
✅ Why This Structure Is Powerful
You can isolate, reuse, and test each component.
You can inject faults, monitor behavior, check correctness, and collect coverage
independently.
It mirrors the physical and logical separation in real hardware verification workflows.
🔑 Key Concept
Every layer is a class. Every transaction is an object. Communication is done via
method calls or mailboxes (queues).
Would you like a complete code example showing a generator, driver, and mailbox in
SystemVerilog?