Skip to main content

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 to mailbox's constructor means it never fills up, so put() never blocks — this is exactly why the top-level initial block 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 a put() 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_fork as a more disciplined alternative to #100. The page's own #100 is called out as a rough stand-in for real end-of-test synchronization. wait_fork is one concrete step up: it suspends the calling process until every child process spawned by an enclosing fork (including ones started join_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 #100 risks entirely.

What this example ties together​

ConceptWhere 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.