SystemVerilog
Classes & Object-Oriented Programming
Why OOP in Verification?
Verilog testbenches quickly become spaghetti — hundreds of tasks, signals passed everywhere, no reuse. SystemVerilog classes solve this by letting you model a transaction (e.g., an AXI write) as an object with its own address, data, and behavior — then pass it by handle between components (driver, monitor, scoreboard) without copying data.
- Encapsulation: data + methods in one unit
- Inheritance: extend a base class to add fields without rewriting common code
- Polymorphism: a base class handle can call the correct overridden method at runtime
- Reuse: the same driver class works for write_txn and read_txn via a base handle
1. Basic Class: Properties, Constructor, Methods
class Packet; // Properties (class variables) bit [31:0] addr; bit [31:0] data; bit write; int id; // Static property: shared across all Packet objects static int pkt_count = 0; // Constructor — called by new() function new(bit [31:0] a = 0, bit [31:0] d = 0, bit w = 1); addr = a; data = d; write = w; id = ++pkt_count; endfunction // Method function void display(); $display("[PKT#%0d] %s addr=%08h data=%08h", id, write ? "WR" : "RD", addr, data); endfunction endclass // Usage initial begin Packet p1, p2; p1 = new(32'hDEAD_0000, 32'hCAFE_BEEF, 1); p2 = new(32'hABCD_1234, 0, 0); p1.display(); // [PKT#1] WR addr=DEAD0000 data=CAFEBEEF p2.display(); // [PKT#2] RD addr=ABCD1234 data=00000000 $display("Total packets: %0d", Packet::pkt_count); end
2. Handles — References, Not Values
A handle is a pointer to an object on the heap. Assigning one handle to another does NOT copy the object — both handles point to the same data:
Packet p1, p2, p3; p1 = new(32'h1000, 32'hAABB, 1); p2 = p1; // p2 points to SAME object as p1 p2.data = 32'hFFFF; $display(p1.data); // prints FFFF — same object! // Shallow copy: new fields, same handle values inside p3 = new p1; p3.addr = 32'h2000; $display(p1.addr); // still 1000 — p3 is a separate object // Null check before use Packet p_null; // defaults to null if (p_null == null) $display("Handle is null — object not yet created");
3. Inheritance — extends
Use extends to build a specialized class on top of a base class. The derived class inherits all properties and methods, and can add new ones or override existing ones:
// Base transaction class BaseTxn; bit [31:0] addr; bit [31:0] data; virtual function void display(); $display("BASE: addr=%0h data=%0h", addr, data); endfunction endclass // Write transaction: adds strobe class WriteTxn extends BaseTxn; bit [3:0] strobe; function new(bit[31:0] a, d, bit[3:0] s = 4'hF); super.addr = a; // access parent via super super.data = d; strobe = s; endfunction virtual function void display(); // override $display("WRITE: addr=%0h data=%0h strobe=%b", addr, data, strobe); endfunction endclass // Read transaction: adds expected data class ReadTxn extends BaseTxn; bit [31:0] exp_data; virtual function void display(); $display("READ: addr=%0h exp=%0h got=%0h %s", addr, exp_data, data, (data===exp_data) ? "PASS" : "FAIL"); endfunction endclass
4. Polymorphism — One Handle, Many Types
A base class handle can hold a derived object. When a virtual method is called through the base handle, SystemVerilog dispatches to the actual derived class's method at runtime:
initial begin BaseTxn txn_q[$]; // queue of base handles WriteTxn wr; ReadTxn rd; wr = new(32'h1000, 32'hDEAD); rd = new(); rd.addr = 32'h2000; rd.exp_data = 32'hBEEF; txn_q.push_back(wr); txn_q.push_back(rd); // Calls the correct display() for each type — polymorphism foreach (txn_q[i]) txn_q[i].display(); // Output: WRITE: addr=1000 data=dead strobe=1111 // READ: addr=2000 exp=beef got=0 FAIL // Type check / cast WriteTxn tmp_wr; if ($cast(tmp_wr, txn_q[0])) $display("First txn is WriteTxn, strobe=%b", tmp_wr.strobe); end
$cast(derived_h, base_h) — it returns 1 if the type matches, 0 otherwise. Never force-cast without checking.5. Abstract Classes — Virtual-Only Interface
A virtual class cannot be instantiated directly — it defines a contract that all subclasses must implement:
virtual class BaseDriver; // Pure virtual method: subclasses MUST override this pure virtual task drive(BaseTxn txn); // Concrete method shared by all subclasses function void pre_drive_check(BaseTxn txn); if (txn == null) $fatal("Null transaction passed to driver"); endfunction endclass class APBDriver extends BaseDriver; virtual interface apb_if vif; task drive(BaseTxn txn); // implements pure virtual pre_drive_check(txn); @(posedge vif.clk); vif.PSEL = 1; vif.PADDR = txn.addr; vif.PWDATA = txn.data; // ... APB handshake endtask endclass
6. Complete Example — AXI Scoreboard Class
A real verification pattern: the scoreboard stores expected results and compares them against actual DUT output using a class-based approach:
class Scoreboard; // Associative array: addr → expected data bit [31:0] exp_mem [bit [31:0]]; int pass_cnt, fail_cnt; function void write(bit[31:0] addr, data); exp_mem[addr] = data; endfunction function void check(bit[31:0] addr, actual); if (!exp_mem.exists(addr)) begin $warning("Scoreboard: unexpected read from addr %0h", addr); return; end if (actual === exp_mem[addr]) begin pass_cnt++; $display("PASS addr=%0h data=%0h", addr, actual); end else begin fail_cnt++; $error("FAIL addr=%0h expected=%0h got=%0h", addr, exp_mem[addr], actual); end exp_mem.delete(addr); endfunction function void report(); $display("=== Scoreboard: PASS=%0d FAIL=%0d ===", pass_cnt, fail_cnt); if (exp_mem.size() > 0) $error("%0d expected reads never checked!", exp_mem.size()); endfunction endclass
Key OOP Concepts — Quick Reference
| Concept | Syntax | Purpose |
|---|---|---|
| class | class MyClass; ... endclass | Define a blueprint |
| new() | obj = new(args) | Allocate object on heap |
| extends | class Child extends Parent | Inheritance |
| super | super.method() | Call parent's method |
| virtual | virtual function display() | Enable polymorphism |
| virtual class | virtual class Base | Abstract class — no direct instantiation |
| pure virtual | pure virtual task drive() | Force subclass to implement |
| static | static int count | Shared across all instances |
| $cast | $cast(derived_h, base_h) | Downcast with type check |
| this | this.field | Reference current object's field |
rand fields to your classes and use constraint blocks to auto-generate legal stimulus.