Up to Day 6, you've built the structural testbench: interfaces, clocking blocks, and synchronous drive/sample patterns. Now you step into the software side of verification — object-oriented programming with SystemVerilog classes. Classes let you create reusable, encapsulated verification components (drivers, monitors, scoreboards) that are dynamic, flexible, and easy to extend.
A class is a template for creating objects. It defines:
new() function that initializes an object when it is created.Unlike Verilog modules which are structural and instantiated statically in the module hierarchy, classes are software objects created dynamically at runtime. This is essential for testbenches where you may create thousands of transactions, drivers, or monitors on the fly.
Use modules for structural hardware: top-level testbench, clock generation, module instantiation, interface creation. Use classes for behavioral testbench components: transaction generators, drivers, monitors, scoreboards, any object that encapsulates data and algorithms.
class transaction;
// Properties
logic [7:0] data;
logic valid;
string addr;
// Constructor
function new();
data = 0;
valid = 0;
addr = "";
endfunction
// Methods
function display();
$display("data=%0h valid=%b addr=%s", data, valid, addr);
endfunction
endclass
Properties (member variables) are declared inside the class, before any methods. They can be logic, int, string, arrays, queues, or even handles to other objects. Each object instance has its own copy of these properties — if two objects have a data property, changing one doesn't affect the other.
Methods are functions declared inside the class. They can read and write the object's properties. A method can have return types, arguments, and local variables just like a normal function. Inside a method, you access properties directly by name — no prefix needed (though you can use this.property for clarity).
The new() function is called automatically when you create an object with new. It typically initializes properties and can accept arguments. For example:
class fifo;
int depth;
int width;
function new(int d = 16, int w = 8);
depth = d;
width = w;
endfunction
endclass
// Create a 32-deep, 16-bit wide FIFO
fifo f = new(32, 16);
A handle is a reference (pointer) to an object. When you declare transaction tr; you are declaring a handle named tr. Initially it is null (points to nothing). When you execute tr = new() you allocate a new object and tr points to it.
| Code | What happens |
|---|---|
| transaction tr; | Declare a handle; tr is null initially |
| tr = new(); | Create new object; tr now points to it |
| tr.data = 8'hAA; | Write property of the object tr points to |
| val = tr.data; | Read property of the object tr points to |
| tr = null; | tr no longer points to anything (object deleted) |
Handles are passed by reference: if you pass a handle to a function and that function modifies the object, the original caller sees the changes. This is fundamentally different from simple data types (which are passed by value).
The new() operator:
new() constructor (if one is defined).transaction tr = new(); or after: tr = new();Two different handles can point to the same object:
transaction tr1 = new(); transaction tr2 = tr1; // tr2 points to the SAME object as tr1 tr1.data = 8'hAA; $display(tr2.data); // Prints 8'hAA — they share the object tr2 = new(); // Create new object; tr2 now points to it tr1.data = 8'hBB; $display(tr2.data); // Still 0 — tr2 points to a different object
// ============================================================
// uart_transaction.sv — UART transaction class example
// EcrioniX · SV Verification Course · Day 7
// ============================================================
class uart_transaction;
// ---- Properties ----
rand logic [7:0] data; // payload byte
rand logic parity; // parity bit
logic parity_error; // error flag
string operation; // "tx" or "rx"
int unsigned timestamp; // when this transaction occurred
// ---- Constructor ----
function new(string op = "tx");
data = 0;
parity = 0;
parity_error = 0;
operation = op;
timestamp = 0;
endfunction
// ---- Methods ----
// Display the transaction
function void display();
$display("[%0t] UART %s: data=0x%02h parity=%b parity_error=%b timestamp=%0d",
$time, operation, data, parity, parity_error, timestamp);
endfunction
// Calculate parity (even parity: count of 1's should be even)
function logic calc_parity();
logic p = 0;
for (int i = 0; i < 8; i++)
p = p ^ data[i];
return p; // Returns 1 if odd number of 1's
endfunction
// Check if parity is correct
function logic is_parity_ok();
logic calculated_parity = calc_parity();
return (calculated_parity == parity);
endfunction
// Randomize with constraints
function void randomize_payload();
if (!randomize())
$fatal(1, "Randomization failed");
parity = calc_parity();
endfunction
// Compare two transactions
function logic compare(uart_transaction other);
if (other == null)
return 0;
return (data == other.data) && (parity == other.parity);
endfunction
// Copy all fields from another transaction
function void copy_from(uart_transaction source);
if (source == null) begin
$warning("Attempted to copy from null transaction");
return;
end
this.data = source.data;
this.parity = source.parity;
this.parity_error = source.parity_error;
this.operation = source.operation;
this.timestamp = source.timestamp;
endfunction
endclass : uart_transaction
// ---- Example usage ----
module uart_transaction_demo;
initial begin
// Create transaction objects
uart_transaction tr1 = new("tx");
uart_transaction tr2 = new("rx");
// Set properties
tr1.data = 8'hA5;
tr1.parity = 1'b1; // odd number of 1's in 0xA5: parity=1
tr1.timestamp = 100;
tr1.display();
// Check parity
if (tr1.is_parity_ok())
$display("tr1 parity is correct");
else
$display("tr1 parity ERROR");
// Create and randomize another
tr2.data = 8'h3C;
tr2.parity = tr2.calc_parity();
tr2.operation = "rx";
tr2.display();
// Copy transaction
uart_transaction tr3 = new();
tr3.copy_from(tr1);
tr3.operation = "copied";
tr3.display();
// Compare
if (tr1.compare(tr3))
$display("tr1 and tr3 have same data/parity");
$finish;
end
endmodule : uart_transaction_demoAlways check if a handle is null before using it. If you pass a null handle to a function that expects to access its properties, you'll get a fatal error. Use if (handle != null) before dereferencing.
Inside a method, this is an implicit reference to the current object. It is rarely necessary — you can access properties directly — but it is useful for clarity or when you have a local variable with the same name as a property:
function void set_data(logic [7:0] data); this.data = data; // 'this.data' is the property, 'data' is the argument endfunction
| Aspect | Module | Class |
|---|---|---|
| Instantiation | Static, at compile time | Dynamic, at runtime with new() |
| Port connections | Explicit ports/interfaces | No ports — access via handle |
| Multiple instances | Fixed count in hierarchy | Unlimited; create as many as needed |
| Lifetime | Entire simulation | Exists until handle set to null |
| Use case | Structural — clocks, resets, DUT | Behavioral — drivers, monitors, TX |
UVM (Universal Verification Methodology) is built entirely on classes because:
transaction tr; declares a handle; tr = new(); creates the object.new() constructor initializes an object when it is created.this.property.Modules are structural and static — instantiated at compile time, they form the hardware hierarchy. Classes are software objects created dynamically at runtime. Use modules for testbench infrastructure (top level, clocks, interfaces); use classes for testbench behavior (drivers, monitors, transactions, scoreboards).
A handle is a reference (pointer) to an object. Declaring transaction tr; creates a null handle. Executing tr = new(); allocates an object and makes tr point to it. Multiple handles can point to the same object. If you assign tr to another handle, both point to the same object until one is reassigned.
The new() operator allocates memory for a new object, calls the class's new() constructor to initialize it, and returns a handle to it. A class can have only one constructor (the new() function). If you define a custom new() with arguments, you can initialize the object when creating it: uart_transaction tr = new("rx");
You can copy the handle (both point to the same object), but to copy the object's contents you must write a method that copies each field. The example's copy_from() method demonstrates this. Assigning one handle to another (tr2 = tr1) makes both point to the same object — changes to one affect the other.