Example Walkthrough: A Class-Based Testbench for the Full Adder
Verilog's own Example Walkthrough closed that topic with a full RTL implementation of a serial frame receiver, driven by a plain-Verilog, directed, initial-block testbench. This page deliberately returns to a much smaller DUT — the full adder, threaded through this entire curriculum's sibling topic since Verilog's Introduction — so that every SystemVerilog language mechanic from this topic (interfaces, classes, randomization, concurrency, assertions, coverage, clocking blocks) can be shown working together clearly, without a large DUT's own complexity competing for attention. This is intentionally not a UVM environment — no uvm_component, no factory, no phasing — just the raw SystemVerilog language pieces, since UVM already has its own capstone built from its own base classes.
The DUT
module full_adder (
input logic a, b, cin,
output logic sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (b & cin) | (a & cin);
endmodule
Unchanged from SystemVerilog's Data Types page — plain combinational logic, no clock needed for its own function. The testbench below still uses a clock purely to pace stimulus application at a steady cadence, exactly as real testbenches do even for combinational DUTs.
The interface, with a clocking block
interface adder_if (input logic clk);
logic a, b, cin;
logic sum, cout;
clocking cb @(posedge clk);
output a, b, cin;
input sum, cout;
endclocking
modport tb (clocking cb);
endinterface
One adder_if bundles all five DUT signals (per Interfaces), and its clocking block (per Clocking Blocks and Program Blocks) gives the testbench a race-free way to drive a/b/cin and sample sum/cout relative to clk.
The transaction class
class add_transaction;
rand bit a, b, cin;
function void display();
$display("a=%0b b=%0b cin=%0b", a, b, cin);
endfunction
endclass
A minimal class (per Classes Basics) — three randomizable bits are all a full adder's stimulus needs. No constraints are required here (every combination of three bits is legal input), which is itself worth noting: not every randomized class needs a constraint block — Randomization and Constraints covers constraints as a tool for when some combinations are illegal, not a mandatory part of every rand class.
Driver and monitor: concurrent processes over a mailbox
class driver;
virtual adder_if.tb vif;
mailbox #(add_transaction) gen2drv;
function new(virtual adder_if.tb vif, mailbox #(add_transaction) gen2drv);
this.vif = vif;
this.gen2drv = gen2drv;
endfunction
task run();
add_transaction tr;
forever begin
gen2drv.get(tr);
vif.cb.a <= tr.a;
vif.cb.b <= tr.b;
vif.cb.cin <= tr.cin;
@(vif.cb);
end
endtask
endclass
virtual adder_if.tb vif is a virtual interface handle — a class-based object's way of reaching a real interface instance that only actually exists at the module level, exactly the pattern Interfaces flagged as mattering most for verification. this.vif = vif; (and this.gen2drv = ...) uses this to distinguish the class property from the same-named constructor argument — standard practice once a constructor's parameter names match its properties, which they very often do. The driver get()s each transaction from a mailbox and drives it through the clocking block.
class scoreboard;
virtual adder_if.tb vif;
int pass_count = 0, fail_count = 0;
covergroup adder_cg;
coverpoint vif.a;
coverpoint vif.b;
coverpoint vif.cin;
cross vif.a, vif.b, vif.cin; // all 8 input combinations
endgroup
function new(virtual adder_if.tb vif);
this.vif = vif;
adder_cg = new();
endfunction
task run();
forever begin
@(vif.cb);
adder_cg.sample();
assert (vif.cb.sum == (vif.cb.a ^ vif.cb.b ^ vif.cb.cin))
else begin
fail_count++;
$error("sum mismatch: a=%0b b=%0b cin=%0b sum=%0b", vif.cb.a, vif.cb.b, vif.cb.cin, vif.cb.sum);
end
if (vif.cb.cout == ((vif.cb.a & vif.cb.b) | (vif.cb.b & vif.cb.cin) | (vif.cb.a & vif.cb.cin)))
pass_count++;
end
endtask
endclass
The scoreboard combines three of this topic's tools in one class: a covergroup tracking whether all 8 input combinations have actually been exercised, an immediate assertion checking sum against an independently-computed expected value every single cycle, and a plain pass_count/fail_count tally for cout. This is deliberately not one single mechanism — real verification environments lean on assertions, coverage, and scoreboard tallies together, each catching a different kind of gap, exactly as Functional Coverage closed by explaining.
Top-level: wiring it together with fork/join_none
module tb_top;
logic clk = 0;
always #5 clk = ~clk;
adder_if dut_if (.clk(clk));
full_adder dut (.a(dut_if.a), .b(dut_if.b), .cin(dut_if.cin), .sum(dut_if.sum), .cout(dut_if.cout));
mailbox #(add_transaction) gen2drv = new();
driver drv = new(dut_if.tb, gen2drv);
scoreboard sb = new(dut_if.tb);
initial begin
fork
drv.run();
sb.run();
join_none
repeat (20) begin
add_transaction tr = new();
tr.randomize();
gen2drv.put(tr);
end
#100;
$display("PASS=%0d FAIL=%0d, coverage=%0.1f%%", sb.pass_count, sb.fail_count, sb.adder_cg.get_coverage());
$finish;
end
endmodule
fork ... join_none (per Processes and Fork-Join) launches the driver and scoreboard as independent background processes, then the main initial block generates 20 randomized transactions and put()s them into the mailbox without waiting on the driver/scoreboard to keep pace one at a time. #100 at the end gives the last few transactions time to work through the pipeline before $finish — a simple stand-in for the more rigorous end-of-test synchronization (UVM's objection mechanism, for instance) that a production testbench would use instead. .get_coverage() reports the covergroup's percentage directly, closing the loop with a printed pass/fail/coverage summary in one line.
Two details worth naming explicitly
gen2drv = new()is an unbounded mailbox. Passing no argument tomailbox's constructor means it never fills up, soput()never blocks — this is exactly why the top-levelinitialblock can hand off all 20 transactions in a tight loop without waiting on the driver to keep pace one at a time. A bounded mailbox (e.g.new(4)) would instead suspend aput()call once four items were queued and unconsumed, until the driver caught up — useful for deliberately capping how far a producer can get ahead of a slower consumer, which this testbench doesn't need since draining 20 items is cheap either way.wait_forkas a more disciplined alternative to#100. The page's own#100is called out as a rough stand-in for real end-of-test synchronization.wait_forkis one concrete step up: it suspends the calling process until every child process spawned by an enclosingfork(including ones startedjoin_none) has completed, rather than guessing a fixed delay long enough to cover them — still simpler than UVM's objection mechanism, but immune to the "guessed too short a delay" failure mode a fixed#100risks entirely.
What this example ties together
| Concept | Where it's used here |
|---|---|
interface + modport (Interfaces) | adder_if, bundling all 5 DUT signals |
| Clocking block (Clocking Blocks) | Race-free drive/sample of a/b/cin/sum/cout |
class/new() (Classes Basics) | add_transaction, driver, scoreboard |
| Virtual interface (Interfaces) | virtual adder_if.tb vif inside driver/scoreboard |
rand/randomize() (Randomization) | add_transaction's a/b/cin |
mailbox (Interprocess Communication) | gen2drv, handing transactions to the driver |
fork/join_none (Processes and Fork-Join) | Launching driver/scoreboard concurrently |
| Immediate assertion (Immediate Assertions) | sum checked every cycle inside the scoreboard |
covergroup/cross (Functional Coverage) | All 8 input combinations tracked |
What's next
This capstone closes the SystemVerilog curriculum — every construct introduced across Sections A–F contributed something to this one example. Testbench picks up from here to cover full testbench architecture at production scale (layered environments, reusable driver/monitor/agent components, configuration), and UVM (already complete) shows how that architecture gets standardized into a reusable class library and methodology — both assuming everything covered in this topic as a starting point.