Classes and Objects for Verification
Section 9.1 stayed on the design side. Section 9.2 crosses over to verification, and the first thing to get used to is that verification code does not have to look like hardware.
RTL describes hardware, so RTL constructs (modules, always blocks, wires) make sense for RTL. A testbench does not describe hardware at all. It describes test data and test behaviour, runs only in simulation, and has no synthesis target. SystemVerilog recognised this and gave verification its own toolkit: classes and objects, borrowed from object-oriented software languages, used on the testbench side and essentially never in synthesizable RTL.
Why not keep using always blocks for testbenches?
Recall the FIFO testbench from Section 7.4: a scoreboard array, some loop variables, initial blocks driving stimulus. That works, and you have used it for six modules’ worth of testbenches. It starts to strain as verification grows: every new kind of test transaction needs its own set of variables and helper tasks, with no natural way to bundle “everything about one transaction” into a single thing you can create, copy and pass around.
A class is that bundling mechanism:
class fifo_transaction;
rand bit [7:0] data; // 'rand' is explained in the next article
bit wr_en;
function void display();
$display("Transaction: data=%h wr_en=%b", data, wr_en);
endfunction
endclass
This declares a type, a template for “one FIFO transaction”, bundling the data that describes a transaction with a method that operates on it. Nothing here is hardware: no clock, no port, nothing that could be synthesized. It exists purely to organise the verification environment.
Creating and using objects
A class declaration is a template. You create instances, objects, with new():
module tb_fifo_oop;
fifo_transaction txn; // a handle: a reference to an object, not the object itself yet
initial begin
txn = new(); // NOW an object exists, and txn refers to it
txn.data = 8'hAB;
txn.wr_en = 1'b1;
txn.display(); // prints: Transaction: data=ab wr_en=1
end
endmodule
Note the distinction between txn, a handle that points to an object, and the object itself created by new(). This is a different memory model from anything in classic Verilog. A reg or wire is fixed storage that exists for the whole simulation; a handle can point to different objects at different times, or to none (null). That flexibility is what makes classes useful for generating a stream of different transactions rather than reusing one fixed set of testbench variables.
Several objects, cleanly separated
fifo_transaction txn_array[10];
initial begin
for (int i = 0; i < 10; i++) begin
txn_array[i] = new();
txn_array[i].data = i * 8'h11;
txn_array[i].wr_en = 1'b1;
end
foreach (txn_array[i])
txn_array[i].display();
end
Ten independent objects, each with its own data and wr_en, created and handled with ordinary array and loop syntax. The classic-Verilog equivalent, parallel reg arrays related only by index, is possible but has none of the class’s bundling: everything about one transaction travels together as one thing you can create, pass to a function or put in a queue.
Passing objects to tasks and functions
Because an object is a self-contained bundle, passing a whole transaction to a task is one argument, not a growing list of parameters:
task automatic drive_transaction(fifo_transaction t);
wdata = t.data;
wr_en = t.wr_en;
@(posedge clk);
wr_en = 1'b0;
endtask
initial begin
fifo_transaction t = new();
t.data = 8'h42; t.wr_en = 1'b1;
drive_transaction(t);
end
As the number of fields describing a transaction grows, which happens quickly when modelling a real protocol, this scales far better than a task signature listing every field separately. It is the same motivation as struct bundling in Section 9.1, applied on the verification side.
This class-based, transaction-oriented style is the foundation of UVM (Universal Verification Methodology), the industry-standard framework built almost entirely from SystemVerilog classes: transactions, drivers, monitors and scoreboards are all classes, connected through a standard set of base classes. You are not learning UVM here, but every class concept in this section is a prerequisite for reading it.
Practice Exercise
Define a class alu_transaction bundling two 8-bit operands, a 3-bit opcode and an 8-bit expected result. Write a short block that creates one object, assigns all four fields and displays them.
Solution
class alu_transaction;
bit [7:0] operand_a, operand_b;
bit [2:0] opcode;
bit [7:0] expected_result;
function void display();
$display("ALU txn: a=%h b=%h op=%b expected=%h",
operand_a, operand_b, opcode, expected_result);
endfunction
endclass
module tb_alu_class;
alu_transaction t;
initial begin
t = new();
t.operand_a = 8'h0A;
t.operand_b = 8'h05;
t.opcode = 3'b000; // ADD, per the ALU opcodes from Modules 2 and 7
t.expected_result = 8'h0F;
t.display();
end
endmodule
This is a small-scale preview of the transaction class a real ALU verification environment would build: every piece of information about one test case in a single object, ready for randomization in the next article.
Classes exist for verification code, not RTL. They bundle test data and behaviour into objects created with new() and referenced through handles rather than fixed storage, which is the flexibility a testbench generating many test cases needs and an RTL module describing fixed hardware never does. In the next article we make these transaction objects generate their own random data with rand and randc.