SystemVerilog · Module 9
Introduction to OOP in SystemVerilog
Why OOP belongs in verification, what classes give you over structs and modules, and the OO mindset every modern UVM environment is built on.
Module 9 · Page 9.1
The Problem That OOP Solves
Let's say you are verifying an APB slave. You have five test scenarios. Each one needs an address, some data, a direction, and a few status flags. So you start writing variables.
// Five transactions. Five of everything.
bit [31:0] trans1_addr, trans2_addr, trans3_addr, trans4_addr, trans5_addr;
bit [7:0] trans1_data, trans2_data, trans3_data, trans4_data, trans5_data;
bit trans1_write, trans2_write, trans3_write, trans4_write, trans5_write;
bit trans1_valid, trans2_valid, trans3_valid, trans4_valid, trans5_valid;
bit [1:0] trans1_resp, trans2_resp, trans3_resp, trans4_resp, trans5_resp;
// Now send the first one...
trans1_addr = 32'h4000_0000;
trans1_data = 8'hAB;
trans1_write = 1;
trans1_valid = 1;
// You need 1000 transactions? Good luck.This works for five transactions. It falls apart completely at fifty. You end up with hundreds of variables, no structure, and code only you can read — and even you can't read it after two weeks.
Now here is the same thing written with a class:
// Define the blueprint once
class ApbTransaction;
rand bit [31:0] addr;
rand bit [7:0] data;
rand bit write;
bit valid;
bit [1:0] resp;
endclass
// Create as many as you need — instantly
ApbTransaction trans_q[$];
repeat (1000) begin
ApbTransaction t = new();
void'(t.randomize());
trans_q.push_back(t);
end
// 1000 transactions. Zero extra variables.That is the core value of OOP in verification. One clean blueprint. As many objects as you need. No mess.
What is Object-Oriented Programming?
OOP is a way of organising code around objects instead of just functions and variables. Each object is a self-contained unit that holds data (called properties) and knows what to do with that data (called methods).
The simplest way to think about it: a class is the blueprint. An object is something you build from that blueprint. You write the blueprint once. You can build a hundred objects from it. Each object has the same structure but its own independent data.
In SystemVerilog, you write a class to describe what a transaction looks like. Then every time you need a new transaction, you create a new object from that class. All objects share the same structure but hold different values.
The Six Pillars of OOP
OOP is built on six core ideas. You will not use all six on day one, but they are all covered in this module. Here is a quick preview so you know where things are heading.
1 — Encapsulation
Bundle data and the code that works with it into one place. Decide what the outside world can see and what it cannot. → Page 9.5: Encapsulation
2 — Abstraction
Show only what matters. Hide the internal complexity. A caller just calls pkt.send() without knowing the 40 lines behind it.
3 — Inheritance
Build a new class on top of an existing one. The child gets everything the parent has — and adds its own bits. → Page 9.8: Inheritance
4 — Polymorphism
One interface, many implementations. The same method call routes to the right code at runtime depending on the actual object type. → Page 9.10: Polymorphism
5 — Reusability
Write a class once. Use it across dozens of tests and multiple projects. Extend it without touching the original.
6 — Modularity
Break a complex testbench into small, focused classes. Each class has one job. Easy to test, debug, and maintain.
Your First Class — Every Part Explained
Here is a complete class from scratch. Every part is labelled. Read through it once before reading the explanation below.
// ─── 1. Class definition (the blueprint) ──────────────────────────
class Packet;
// ─── 2. Properties (data the object holds) ────────────────────
int packet_id;
bit [7:0] data;
bit parity;
// ─── 3. Method: display the packet ────────────────────────────
function void display();
$display("Packet %0d: data=0x%h parity=%b",
packet_id, data, parity);
endfunction
// ─── 4. Method: compute even parity ───────────────────────────
function void calc_parity();
parity = ^data; // XOR of all 8 bits
endfunction
endclass
// ─── 5. Using the class inside a testbench ────────────────────────
module tb;
Packet pkt; // ← handle only — no object yet, pkt is null
initial begin
pkt = new(); // ← object created in memory
pkt.packet_id = 1;
pkt.data = 8'hA5;
pkt.calc_parity(); // calls the method on this object
pkt.display(); // Packet 1: data=0xa5 parity=1
end
endmoduleLet's walk through what each part does:
The class keyword
class Packet; declares the blueprint. Everything between class and endclass belongs to this class. The name Packet is what you use later to declare handles and create objects.
Properties
Properties are just variables that live inside the class. Every object you create from Packet gets its own independent copy of packet_id, data, and parity. Changing one object's data has no effect on another object's data.
& 4. Methods
Methods are functions (or tasks) defined inside the class. They have direct access to all the class properties without needing to pass them as arguments. When you call pkt.calc_parity(), the method reads data and writes parity from the same object automatically.
Handle vs. Object — the most important distinction
This is where nearly every beginner makes their first mistake.
Packet pkt; gives you a handle. Think of it as a name tag. Before new() is called, the handle points to nothing — it is null. The new() call actually builds the object in simulation memory and makes pkt point to it.
Classes vs. Modules vs. Structs
If you come from an RTL background, you are used to modules and maybe structs. Here is a quick reference so you always know which one to use.
| Feature | Module | Struct | Class |
|---|---|---|---|
| Purpose | Describe hardware | Group data fields | Verification objects with behaviour |
| Synthesisable? | Yes | Yes | No — testbench only |
| Dynamic creation? | No — fixed at elaboration | No | Yes — new() at any time |
| Has methods? | Tasks and functions only | No | Yes — full task and function support |
| Inheritance? | No | No | Yes — extends keyword |
| Randomisation? | No | Limited | Yes — rand, randc, constraints |
| Use it for | RTL — adders, FSMs, counters | Packed register fields, packet headers | Transactions, drivers, monitors, scoreboards |
Simple rule: if it goes into silicon, use a module. If it is a named group of data with no behaviour, use a struct. If it is something your testbench creates, randomises, and drives — use a class.
Why OOP Actually Matters in Verification
The six pillars sound great on paper. Here is what they actually buy you on a real project.
Your testbench can scale
A flat procedural testbench with 2000 lines works fine at IP level. At subsystem level, you need ten of them stitched together. Without classes, you copy-paste and manually synchronise everything. With classes, you reuse the same driver, monitor, and scoreboard objects across all ten environments — just pointing them at different interfaces.
Bugs become easier to find
When every transaction is an object with a display() method and a unique ID, your error messages tell you exactly which transaction failed and what its fields looked like. Compare that to fishing through raw waveforms and matching timestamps by hand.
UVM is built entirely on OOP
Every component in UVM — driver, monitor, scoreboard, sequencer — is a class. Every transaction is a class. The factory, the phasing mechanism, the register abstraction layer — all classes. Learning OOP in SystemVerilog is not a step before UVM. It is the same thing.
What the Rest of This Module Covers
This page was just the warm-up. Here is the roadmap for the full OOP module:
- 9.2 Classes & Objects — Basics How to declare a class, create objects, access properties, and call methods. The full mechanics of new().
- 9.3 Properties & Methods Deep dive into class variables, functions, and tasks. When to use rand. How methods access properties.
- 9.4 Constructors & new() How to write constructors with default arguments. What happens if you don't write one.
- 9.5 – 9.7 Encapsulation, this, and Static Members Access control, the this keyword for disambiguation, and class-level (static) variables.
- 9.8 – 9.11 Inheritance, super, Polymorphism, Abstract Classes The advanced OOP topics that make UVM possible. Covered one step at a time.
- 9.12 – 9.16 Parameterised Classes, Nested Classes, and More The less-common but important topics you will encounter in production testbenches.
Quick Reference — OOP Syntax at a Glance
Bookmark this. You will come back to it when you forget a keyword.
// ── Declare a class ───────────────────────────────────────────
class MyClass;
endclass
// ── Declare a handle (no object yet) ──────────────────────────
MyClass handle;
// ── Create an object ──────────────────────────────────────────
handle = new();
// ── Access a property ─────────────────────────────────────────
handle.property_name = 5;
// ── Call a method ─────────────────────────────────────────────
handle.method_name();
// ── Inherit from a parent ─────────────────────────────────────
class ChildClass extends ParentClass;
endclass
// ── Call parent constructor from child ────────────────────────
function new();
super.new();
endfunction
// ── Mark a method as overridable ──────────────────────────────
virtual function void display();
endfunction
// ── Abstract class (cannot be instantiated directly) ──────────
virtual class BaseClass;
pure virtual function void do_something();
endclassVerification Usage — Building a Minimal Class-Based Testbench
The full UVM framework is covered later in the curriculum — but you don't need UVM to start using classes for verification. A surprising amount of real testbench machinery — packet generators, scoreboards, coverage objects — can be written with plain SystemVerilog classes. Once you've internalised these patterns, UVM stops being a framework you "learn" and becomes a library of conventions you already understand.
// ── Transaction class — encapsulates one bus operation ─────────
class apb_xact;
rand bit [31:0] paddr;
rand bit [31:0] pwdata;
rand bit pwrite;
constraint addr_aligned { paddr[1:0] == 2'b00; }
function string to_string();
return $sformatf("APB %s @0x%08h data=0x%08h",
pwrite ? "WR" : "RD", paddr, pwdata);
endfunction
endclass
// ── Driver class — pulls transactions, drives the bus ─────────
class apb_driver;
mailbox #(apb_xact) inbox;
virtual apb_if vif;
task run();
forever begin
apb_xact tr;
inbox.get(tr);
@(posedge vif.clk);
vif.paddr <= tr.paddr;
vif.pwdata <= tr.pwdata;
vif.psel <= 1;
@(posedge vif.clk);
vif.penable <= 1;
while (!vif.pready) @(posedge vif.clk);
vif.psel <= 0;
vif.penable <= 0;
$display("[%0t] DRV: %s", $time, tr.to_string());
end
endtask
endclass
// ── Test top — composition: env has-a driver, has-a generator ──
module tb;
// Interfaces stay in module-world; classes hold handles to them
apb_if u_apb(.clk);
apb_slave u_dut(.apb(u_apb));
initial begin
// Build the testbench: instantiate the driver and the inbox
apb_driver drv = new;
mailbox #(apb_xact) bx = new;
drv.inbox = bx;
drv.vif = u_apb;
fork drv.run(); join_none
// Generate 10 random transactions
repeat (10) begin
apb_xact tr = new;
assert (tr.randomize());
bx.put(tr);
end
#1us; $finish;
end
endmoduleSimulation Behavior — Object Lifecycle in the Simulator
A class object lives in the simulator's heap memory. The simulator allocates it when new() runs, tracks every handle that points to it, and frees its memory when the last handle drops the reference. Understanding when allocation, reference changes, and garbage collection happen is the difference between a clean testbench and one that mysteriously consumes 20 GB of RAM mid-regression.
class packet;
static int alive_count;
int id;
function new(int i);
id = i;
alive_count++;
$display("[%0t] packet#%0d created (alive=%0d)",
$time, id, alive_count);
endfunction
// SystemVerilog doesn't have destructors, but you can model
// "release" via a dedicated drop method that nulls the handle
function void drop();
alive_count--;
$display("[%0t] packet#%0d dropped (alive=%0d)",
$time, id, alive_count);
endfunction
endclass
initial begin
packet p1, p2;
p1 = new(1); // [0] packet#1 created (alive=1)
p2 = new(2); // [0] packet#2 created (alive=2)
// Handle reassignment — same object, two pointers
p1 = p2; // packet#1 now has zero handles → eligible for GC
// But alive_count still says 2 because no drop() was called
p2 = null; // One handle dropped; packet#2 still has p1 pointing to it
p1 = null; // Last handle dropped; packet#2 now garbage
// SV's GC is implementation-defined; the simulator may free immediately
// or batch up frees. alive_count is a manual indicator, not GC truth.
endWaveform Analysis — Why Classes Don't Appear in the Viewer
Verdi and DVE show signals — wires and variables that have a hierarchical path in the elaborated design. Classes don't have a hierarchical path. An object is created at runtime, lives in the heap, and has no fixed location in the design tree. You cannot browse to tb.drv.pending_tr.paddr in a waveform viewer the way you can to tb.u_apb.paddr — the first path goes through dynamic class memory; the second through static module signals. This trips up every engineer transitioning from RTL debug to verification debug.
// ── Pattern: snapshot class state into a module-level signal ───
module tb;
// Static signals — these DO appear in waveform
logic [31:0] last_paddr;
logic [31:0] last_pwdata;
int xact_count;
initial begin
apb_driver drv = new;
// As the driver processes each transaction, mirror its data into signals
forever begin
apb_xact tr;
drv.inbox.get(tr);
last_paddr = tr.paddr; // snapshot to signal — visible in Verdi
last_pwdata = tr.pwdata;
xact_count++;
drv.process(tr);
end
end
endmodule
// In Verdi, browse to:
// tb.last_paddr ← shows the most recent transaction's address
// tb.last_pwdata ← shows the most recent transaction's data
// tb.xact_count ← shows total transactions processed
//
// You CANNOT directly browse drv.* — it's a class handle pointing to
// dynamic memory with no hierarchical path.Industry Insights — How Senior Teams Use OOP in Verification
Debugging Academy — 5 Real Class-Based Testbench Bugs
Each lab is a real failure mode from production verification environments. Buggy code, symptom, root cause, fix.
class scoreboard;
int hits, misses;
function void record(bit hit);
if (hit) hits++; else misses++;
endfunction
endclass
module tb;
scoreboard scb; // ← declared, NOT instantiated
initial begin
scb.record(1); // crash: null reference
end
endmoduleclass packet;
int data[4];
endclass
initial begin
packet original = new;
packet copy = original; // ← NOT a copy — same handle, same object
original.data[0] = 42;
copy.data[0] = 99; // overwrites original.data[0]
$display("original=%0d copy=%0d", original.data[0], copy.data[0]);
// Prints: original=99 copy=99 — both see the same memory
endmodule tb_top;
// Class declared inside the module — not reusable elsewhere
class packet;
int data;
endclass
initial begin
packet p = new;
end
endmodule
// Another file:
module monitor;
initial begin
packet p = new; // ← compile error: packet undefined
end
endmodule// Generator sends 10 transactions through a mailbox
initial begin
packet tr = new; // ← single object reused for every send
repeat (10) begin
assert (tr.randomize());
bx.put(tr); // puts the SAME handle 10 times
end
end
// Receiver gets all 10 — but they're all the same object,
// holding only the last randomized values
initial begin
repeat (10) begin
packet rx;
bx.get(rx);
$display("Got: data=%h", rx.data); // all 10 print the same data
end
end// memory_controller.sv — meant to be synthesisable RTL
module memory_controller(...);
logic [31:0] addr_reg;
// Helper "class" added by an engineer who thought it would be clean
class addr_decoder;
function bit is_io(bit [31:0] a);
return (a[31:24] == 8'hFE);
endfunction
endclass
addr_decoder decoder = new;
always_ff @(posedge clk)
if (decoder.is_io(addr_reg)) ... // never reaches netlist
endmoduleInterview Q&A — 12 Questions on OOP Foundations
Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.
Verilog was designed for hardware modelling — static, gate-level structures. As chip complexity grew (from thousands of gates in the 1980s to billions today), the verification environment that exercised the design became its own large software project. Verilog's static module hierarchy is too rigid for dynamic test stimulus, reusable testbench components, and inheritance-based VIP. OOP — added in IEEE 1800-2005 — gave verification engineers a proper toolkit: dynamic objects, encapsulation, inheritance, polymorphism. The RTL part of SystemVerilog stayed Verilog-compatible; OOP lives strictly in the verification side.
Best Practices — OOP Rules to Walk Away With
- Always declare classes inside a package. Module-internal class declarations don't scale beyond the prototype stage.
- Pair handle declaration with construction.
my_class h = new();, or explicitlynew()in the next line. Never declare a handle and use it before initialising. - Allocate a fresh object for every queued send. Never put the same handle in a mailbox/queue twice with mutations in between.
- Default fields to
local. Promote toprotectedonly when a subclass genuinely needs the access; expose through methods otherwise. - Use inheritance only when subclass is-a superclass. If the only reason to extend is "to reuse this helper method," use composition or a package function instead.
- Mirror class-internal state into module signals for waveform visibility. Verdi can't browse class memory; module-level signals can.
- Log every important class state change with
$displayor ``uvm_info`. The transcript is your primary class-debug tool. - Add explicit cleanup methods for non-memory resources. File handles, mailbox slots, semaphore tokens — call
close()or release before dropping the last reference. - Lint for "class in RTL file." CI rule rejects any class declaration in files under
rtl/. Catches misclassified code at commit, not synthesis. - Master plain SV classes before reaching for UVM. If you can't write a working three-class testbench (transaction, driver, monitor) without UVM, you'll struggle to debug a UVM env that uses the same patterns underneath.