This example verifies a purely combinational adder — no clock in the design itself — using the same modular driver/monitor/scoreboard/generator/environment/test structure from the previous two examples. Different designs need different driver, monitor, and scoreboard implementations depending on their specifics, but the overall architecture stays the same. This one also demonstrates something the earlier examples didn't: deliberately breaking the design to prove the scoreboard actually catches the resulting failure.
15 min read | Intermediate Level
What You'll Learn
- How to verify a clockless combinational design by giving the testbench its own synchronizing clock
- Why sampling slightly after a clock edge avoids racing against non-blocking-driven signals
- How a scoreboard's reference model mirrors the DUT's own logic to independently predict the expected result
- Why deliberately injecting a bug into a design is a genuinely useful way to validate that a checker works
Design
The adder itself has no clock — it's pure combinational logic:
// An adder is combinational logic and does not
// have a clock
module my_adder (adder_if _if);
always_comb begin
if (_if.rstn) begin
_if.sum <= 0;
_if.carry <= 0;
end else begin
{_if.carry, _if.sum} <= _if.a + _if.b;
end
end
endmodule
Note the polarity here: rstn being high (1) is what zeroes the outputs, and rstn being low (0) is what lets the adder actually add. That's the opposite of what the name rstn usually implies (active-low reset, where 0 means "reset applied"). This isn't a typo introduced here — it's exactly how the design is written — but it's worth double-checking a signal's actual behavior rather than assuming it from its name alone, especially when reusing this pattern elsewhere.
Transaction Object
Since verifying that the adder correctly does nothing when reset is just as important as verifying that it adds, rstn is randomized right alongside a and b:
// To verify that the adder adds, we also need to check that it
// does not add when rstn is 0, and hence rstn should also be
// randomized along with a and b.
class Packet;
rand bit rstn;
rand bit[7:0] a;
rand bit[7:0] b;
bit [7:0] sum;
bit carry;
// Print contents of the data packet
function void print(string tag="");
$display ("T=%0t %s a=0x%0h b=0x%0h sum=0x%0h carry=0x%0h", $time, tag, a, b, sum, carry);
endfunction
// This is a utility function to allow copying contents in
// one Packet variable to another.
function void copy(Packet tmp);
this.a = tmp.a;
this.b = tmp.b;
this.rstn = tmp.rstn;
this.sum = tmp.sum;
this.carry = tmp.carry;
endfunction
endclass
Driver
Since the DUT has no clock, the driver instead synchronizes to a separate, testbench-only clock (introduced in the Interface section below):
class driver;
virtual adder_if m_adder_vif;
virtual clk_if m_clk_vif;
event drv_done;
mailbox drv_mbx;
task run();
$display ("T=%0t [Driver] starting ...", $time);
// Try to get a new transaction every time and then assign
// packet contents to the interface. But do this only if the
// design is ready to accept new transactions
forever begin
Packet item;
$display ("T=%0t [Driver] waiting for item ...", $time);
drv_mbx.get(item);
@ (posedge m_clk_vif.tb_clk);
item.print("Driver");
m_adder_vif.rstn <= item.rstn;
m_adder_vif.a <= item.a;
m_adder_vif.b <= item.b;
->drv_done;
end
endtask
endclass
Monitor
The monitor samples the interface on every clock edge — unlike the previous two examples, this design has no valid-style signal to gate on, since the adder's outputs are always meaningful given whatever inputs currently sit on the bus:
// The monitor has a virtual interface handle with which it can monitor
// the events happening on the interface. It sees new transactions and then
// captures information into a packet and sends it to the scoreboard
// using another mailbox.
class monitor;
virtual adder_if m_adder_vif;
virtual clk_if m_clk_vif;
mailbox scb_mbx; // Mailbox connected to scoreboard
task run();
$display ("T=%0t [Monitor] starting ...", $time);
// Check forever at every clock edge to see if there is a
// valid transaction and if yes, capture info into a class
// object and send it to the scoreboard when the transaction
// is over.
forever begin
Packet m_pkt = new();
@(posedge m_clk_vif.tb_clk);
#1;
m_pkt.a = m_adder_vif.a;
m_pkt.b = m_adder_vif.b;
m_pkt.rstn = m_adder_vif.rstn;
m_pkt.sum = m_adder_vif.sum;
m_pkt.carry = m_adder_vif.carry;
m_pkt.print("Monitor");
scb_mbx.put(m_pkt);
end
endtask
endclass
The #1 delay right after the clock edge matters: the driver also drives new values right at that same edge, using non-blocking assignments. Sampling immediately, with no delay, risks racing against those updates and catching stale values — a small delay lets everything settle first. This is the same problem a clocking block's input/output skew is designed to solve more formally.
Because there's no gating condition here, the monitor keeps sampling and forwarding a packet every single clock cycle — including repeatedly re-reporting the same unchanged values once the driver has stopped feeding new transactions. You'll see this in the trailing entries of the output below.
Quick Check
Q: Why does the monitor need the #1 delay here, when the earlier two testbench examples' monitors didn't need one?
Show Answer
Examples 1 and 2's designs used explicit registered outputs with a one-cycle latency baked into their own clocked logic, so the monitor's second @(posedge) wait naturally landed after the new value had already settled. This adder's output is driven the moment the driver's own non-blocking assignment takes effect on the very same edge the monitor is also waiting on — with no delay, the monitor could sample before that update has actually propagated.
Scoreboard
The scoreboard builds its own independent reference model of what the adder should produce, using the exact same logic the design itself implements, then compares that prediction against what was actually observed:
// The scoreboard is responsible to check data integrity. Since the design
// simple adds inputs to give sum and carry, scoreboard helps to check if the
// output has changed for given set of inputs based on expected logic
class scoreboard;
mailbox scb_mbx;
task run();
forever begin
Packet item, ref_item;
scb_mbx.get(item);
item.print("Scoreboard");
// Copy contents from received packet into a new packet so
// just to get a and b.
ref_item = new();
ref_item.copy(item);
// Let us calculate the expected values in carry and sum
if (ref_item.rstn)
{ref_item.carry, ref_item.sum} = ref_item.a + ref_item.b;
else
{ref_item.carry, ref_item.sum} = 0;
// Now, carry and sum outputs in the reference variable can be compared
// with those in the received packet
if (ref_item.carry != item.carry) begin
$display("[%0t] Scoreboard Error! Carry mismatch ref_item=0x%0h item=0x%0h", $time, ref_item.carry, item.carry);
end else begin
$display("[%0t] Scoreboard Pass! Carry match ref_item=0x%0h item=0x%0h", $time, ref_item.carry, item.carry);
end
if (ref_item.sum != item.sum) begin
$display("[%0t] Scoreboard Error! Sum mismatch ref_item=0x%0h item=0x%0h", $time, ref_item.sum, item.sum);
end else begin
$display("[%0t] Scoreboard Pass! Sum match ref_item=0x%0h item=0x%0h", $time, ref_item.sum, item.sum);
end
end
endtask
endclass
Generator
// Sometimes we simply need to generate N random transactions to random
// locations so a generator would be useful to do just that. In this case
// loop determines how many transactions need to be sent
class generator;
int loop = 10;
event drv_done;
mailbox drv_mbx;
task run();
for (int i = 0; i < loop; i++) begin
Packet item = new;
item.randomize();
$display ("T=%0t [Generator] Loop:%0d/%0d create next item", $time, i+1, loop);
drv_mbx.put(item);
$display ("T=%0t [Generator] Wait for driver to be done", $time);
@(drv_done);
end
endtask
endclass
This declares loop = 10, but the captured output below only shows 5 items generated (Loop:1/5 through Loop:5/5) — the log was evidently produced with loop set to 5, not 10 as shown here. Worth verifying against the actual source before publishing, since the two don't currently agree.
Environment
The environment wires all four components together. As the comment notes, join_any is a deliberate choice here — the generator is the only component expected to actually finish, and that's exactly when the simulation should be allowed to move toward wrapping up:
// Lets say that the environment class was already there, and generator is
// a new component that needs to be included in the ENV.
class env;
generator g0; // Generate transactions
driver d0; // Driver to design
monitor m0; // Monitor from design
scoreboard s0; // Scoreboard connected to monitor
mailbox scb_mbx; // Top level mailbox for SCB <-> MON
virtual adder_if m_adder_vif; // Virtual interface handle
virtual clk_if m_clk_vif; // TB clk
event drv_done;
mailbox drv_mbx;
function new();
d0 = new;
m0 = new;
s0 = new;
scb_mbx = new();
g0 = new;
drv_mbx = new;
endfunction
virtual task run();
// Connect virtual interface handles
d0.m_adder_vif = m_adder_vif;
m0.m_adder_vif = m_adder_vif;
d0.m_clk_vif = m_clk_vif;
m0.m_clk_vif = m_clk_vif;
// Connect mailboxes between each component
d0.drv_mbx = drv_mbx;
g0.drv_mbx = drv_mbx;
m0.scb_mbx = scb_mbx;
s0.scb_mbx = scb_mbx;
// Connect event handles
d0.drv_done = drv_done;
g0.drv_done = drv_done;
// Start all components - a fork join_any is used because
// the stimulus is generated by the generator and we want the
// simulation to exit only when the generator has finished
// creating all transactions. Until then all other components
// have to run in the background.
fork
s0.run();
d0.run();
m0.run();
g0.run();
join_any
endtask
endclass
Test
// The test can instantiate any environment. In this test, we are using
// an environment without the generator and hence the stimulus should be
// written in the test.
class test;
env e0;
mailbox drv_mbx;
function new();
drv_mbx = new();
e0 = new();
endfunction
virtual task run();
e0.d0.drv_mbx = drv_mbx;
e0.run();
endtask
endclass
Interface
Since the adder itself has no clock, a separate interface provides one purely for the testbench's own use — deciding when values are driven and sampled, the way a clock would normally do for sequential logic between registers:
// Adder interface contains all signals that the adder requires
// to operate
interface adder_if();
logic rstn;
logic [7:0] a;
logic [7:0] b;
logic [7:0] sum;
logic carry;
endinterface
// Although an adder does not have a clock, let us create a mock clock
// used in the testbench to synchronize when value is driven and when
// value is sampled. Typically combinational logic is used between
// sequential elements like FF in a real circuit. So, let us assume
// that inputs to the adder is provided at some posedge clock. But because
// the design does not have clock in its input, we will keep this clock
// in a separate interface that is available only to testbench components
interface clk_if();
logic tb_clk;
initial tb_clk <= 0;
always #10 tb_clk = ~tb_clk;
endinterface
Testbench Top
module tb;
bit tb_clk;
clk_if m_clk_if ();
adder_if m_adder_if ();
my_adder u0 (m_adder_if);
initial begin
test t0;
t0 = new;
t0.e0.m_adder_vif = m_adder_if;
t0.e0.m_clk_vif = m_clk_if;
t0.run();
// Once the main stimulus is over, wait for some time
// until all transactions are finished and then end
// simulation. Note that $finish is required because
// there are components that are running forever in
// the background like clk, monitor, driver, etc
#50 $finish;
end
endmodule

Output
ncsim> run T=0 [Driver] starting ... T=0 [Driver] waiting for item ... T=0 [Monitor] starting ... T=0 [Generator] Loop:1/5 create next item T=0 [Generator] Wait for driver to be done T=10 Driver a=0x16 b=0x11 sum=0x0 carry=0x0 T=10 [Driver] waiting for item ... T=10 [Generator] Loop:2/5 create next item T=10 [Generator] Wait for driver to be done T=11 Monitor a=0x16 b=0x11 sum=0x0 carry=0x0 T=11 Scoreboard a=0x16 b=0x11 sum=0x0 carry=0x0 [11] Scoreboard Pass! Carry match ref_item=0x0 item=0x0 [11] Scoreboard Pass! Sum match ref_item=0x0 item=0x0 T=30 Driver a=0xde b=0x6 sum=0x0 carry=0x0 T=30 [Driver] waiting for item ... T=30 [Generator] Loop:3/5 create next item T=30 [Generator] Wait for driver to be done T=31 Monitor a=0xde b=0x6 sum=0x0 carry=0x0 T=31 Scoreboard a=0xde b=0x6 sum=0x0 carry=0x0 [31] Scoreboard Pass! Carry match ref_item=0x0 item=0x0 [31] Scoreboard Pass! Sum match ref_item=0x0 item=0x0 T=50 Driver a=0xb1 b=0xbd sum=0x0 carry=0x0 T=50 [Driver] waiting for item ... T=50 [Generator] Loop:4/5 create next item T=50 [Generator] Wait for driver to be done T=51 Monitor a=0xb1 b=0xbd sum=0x0 carry=0x0 T=51 Scoreboard a=0xb1 b=0xbd sum=0x0 carry=0x0 [51] Scoreboard Pass! Carry match ref_item=0x0 item=0x0 [51] Scoreboard Pass! Sum match ref_item=0x0 item=0x0 T=70 Driver a=0x63 b=0xfb sum=0x0 carry=0x0 T=70 [Driver] waiting for item ... T=70 [Generator] Loop:5/5 create next item T=70 [Generator] Wait for driver to be done T=71 Monitor a=0x63 b=0xfb sum=0x5e carry=0x1 T=71 Scoreboard a=0x63 b=0xfb sum=0x5e carry=0x1 [71] Scoreboard Pass! Carry match ref_item=0x1 item=0x1 [71] Scoreboard Pass! Sum match ref_item=0x5e item=0x5e T=90 Driver a=0x71 b=0xbc sum=0x0 carry=0x0 T=90 [Driver] waiting for item ... T=91 Monitor a=0x71 b=0xbc sum=0x0 carry=0x0 T=91 Scoreboard a=0x71 b=0xbc sum=0x0 carry=0x0 [91] Scoreboard Pass! Carry match ref_item=0x0 item=0x0 [91] Scoreboard Pass! Sum match ref_item=0x0 item=0x0 T=111 Monitor a=0x71 b=0xbc sum=0x0 carry=0x0 T=111 Scoreboard a=0x71 b=0xbc sum=0x0 carry=0x0 [111] Scoreboard Pass! Carry match ref_item=0x0 item=0x0 [111] Scoreboard Pass! Sum match ref_item=0x0 item=0x0 T=131 Monitor a=0x71 b=0xbc sum=0x0 carry=0x0 T=131 Scoreboard a=0x71 b=0xbc sum=0x0 carry=0x0 [131] Scoreboard Pass! Carry match ref_item=0x0 item=0x0 [131] Scoreboard Pass! Sum match ref_item=0x0 item=0x0 Simulation complete via $finish(1) at time 140 NS + 0 ./testbench.sv:265 #50 $finish;
Item 5 (a=0x63, b=0xfb) is worth checking by hand: 0x63 + 0xfb = 0x15E, a 9-bit result — carry (bit 8) is 1, and the remaining 8 bits, 0x5E, are the sum. That's exactly what both the monitor and the scoreboard report, confirming rstn was low (normal operation) for this particular item. The trailing entries at T=91, 111, and 131 are the monitor continuing to sample and re-report the same unchanged values after the driver stops feeding new transactions, exactly as the earlier note describes.
Buggy Design
Passing results alone don't prove a checker actually works — it might just never be exercised by a real failure. Deliberately introducing a bug and confirming the scoreboard flags it is how you validate the checker itself, not just the design:
module my_adder (adder_if _if);
always_comb begin
// Let sum and carry be reset when rstn is 1 instead of 0
// A simple but yet possible design bug
if (_if.rstn) begin
_if.sum <= 0;
_if.carry <= 0;
end else begin
{_if.carry, _if.sum} <= _if.a + _if.b;
end
end
endmodule
This code block is identical to the original design shown earlier — there's no visible change here at all, despite the comment describing an intended bug. Working backward from the actual output below: item 1 (a=0x16, b=0x11, known to have rstn high from the working run) now shows sum=0x27 (0x16 + 0x11, a real addition) instead of the expected reset-to-zero. That means the design that was actually simulated to produce this output most likely reads if (!_if.rstn) — an added negation flipping which branch runs for each value of rstn — rather than the code pasted here. Treat this section's code as unreliable and check the actual EDA Playground source below rather than this snippet if you want to reproduce the bug yourself.
The output confirms the checker does its job once the actual bug (whatever it precisely was) is in place:

Output
ncsim> run T=0 [Driver] starting ... T=0 [Driver] waiting for item ... T=0 [Monitor] starting ... T=0 [Generator] Loop:1/5 create next item T=0 [Generator] Wait for driver to be done T=10 Driver a=0x16 b=0x11 sum=0x0 carry=0x0 T=10 [Driver] waiting for item ... T=10 [Generator] Loop:2/5 create next item T=10 [Generator] Wait for driver to be done T=11 Monitor a=0x16 b=0x11 sum=0x27 carry=0x0 T=11 Scoreboard a=0x16 b=0x11 sum=0x27 carry=0x0 [11] Scoreboard Pass! Carry match ref_item=0x0 item=0x0 [11] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x27 T=30 Driver a=0xde b=0x6 sum=0x0 carry=0x0 T=30 [Driver] waiting for item ... T=30 [Generator] Loop:3/5 create next item T=30 [Generator] Wait for driver to be done T=31 Monitor a=0xde b=0x6 sum=0xe4 carry=0x0 T=31 Scoreboard a=0xde b=0x6 sum=0xe4 carry=0x0 [31] Scoreboard Pass! Carry match ref_item=0x0 item=0x0 [31] Scoreboard Error! Sum mismatch ref_item=0x0 item=0xe4 T=50 Driver a=0xb1 b=0xbd sum=0x0 carry=0x0 T=50 [Driver] waiting for item ... T=50 [Generator] Loop:4/5 create next item T=50 [Generator] Wait for driver to be done T=51 Monitor a=0xb1 b=0xbd sum=0x6e carry=0x1 T=51 Scoreboard a=0xb1 b=0xbd sum=0x6e carry=0x1 [51] Scoreboard Error! Carry mismatch ref_item=0x0 item=0x1 [51] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x6e T=70 Driver a=0x63 b=0xfb sum=0x0 carry=0x0 T=70 [Driver] waiting for item ... T=70 [Generator] Loop:5/5 create next item T=70 [Generator] Wait for driver to be done T=71 Monitor a=0x63 b=0xfb sum=0x0 carry=0x0 T=71 Scoreboard a=0x63 b=0xfb sum=0x0 carry=0x0 [71] Scoreboard Error! Carry mismatch ref_item=0x1 item=0x0 [71] Scoreboard Error! Sum mismatch ref_item=0x5e item=0x0 T=90 Driver a=0x71 b=0xbc sum=0x0 carry=0x0 T=90 [Driver] waiting for item ... T=91 Monitor a=0x71 b=0xbc sum=0x2d carry=0x1 T=91 Scoreboard a=0x71 b=0xbc sum=0x2d carry=0x1 [91] Scoreboard Error! Carry mismatch ref_item=0x0 item=0x1 [91] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x2d T=111 Monitor a=0x71 b=0xbc sum=0x2d carry=0x1 T=111 Scoreboard a=0x71 b=0xbc sum=0x2d carry=0x1 [111] Scoreboard Error! Carry mismatch ref_item=0x0 item=0x1 [111] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x2d T=131 Monitor a=0x71 b=0xbc sum=0x2d carry=0x1 T=131 Scoreboard a=0x71 b=0xbc sum=0x2d carry=0x1 [131] Scoreboard Error! Carry mismatch ref_item=0x0 item=0x1 [131] Scoreboard Error! Sum mismatch ref_item=0x0 item=0x2d Simulation complete via $finish(1) at time 140 NS + 0 ./testbench.sv:265 #50 $finish;
Every item that previously passed now mismatches on exactly the fields you'd expect once the reset condition is inverted — strong evidence the scoreboard's independent reference model is actually doing real work, not just rubber-stamping whatever the design happens to output.
Where Does This "Break It On Purpose" Technique Matter?
Before trusting any scoreboard in a real verification environment, it's standard practice to deliberately inject a known bug into the design (or a mutation of it) and confirm the checker actually flags it. A scoreboard that always passes, even against broken logic, isn't verifying anything — it's just consuming simulation time. This technique, sometimes called mutation testing, is exactly what this section demonstrates at a small scale.
Common Beginner Mistakes
Mistake #1: Assuming a signal's name tells you its actual polarity
As flagged above, this design's rstn behaves as active-high, despite the name conventionally implying active-low. Why? Always verify a reset signal's actual behavior in the code, rather than assuming it from the name — especially before reusing a driver or monitor pattern against a different design that might name things the same way but behave oppositely.
Mistake #2: Sampling exactly at a clock edge with no settling delay
As the Monitor section explains, sampling immediately at the same edge the driver uses to drive new values risks catching a stale, pre-update value. Why? A small delay (like the #1 used here), or a properly-skewed clocking block, gives non-blocking updates time to actually take effect before anything reads them.
Mistake #3: Trusting a scoreboard that has never actually caught anything
A scoreboard that only ever reports "Pass" hasn't necessarily been proven correct — it may simply never have been tested against a real failure. Why? Deliberately breaking the design once, as this article's "Buggy Design" section does, is a cheap and valuable way to confirm your checker logic actually works before relying on it for real bug-hunting.
Practice Exercise
Challenge: Predict the Reference Model's Output
Given a = 0x80, b = 0x80, and rstn = 0 (normal operation, using this design's actual polarity), what would the scoreboard's reference model compute for sum and carry?
Hint
0x80 + 0x80 needs more than 8 bits to represent — work out the 9-bit binary result first, then split off the top bit as carry and the remaining 8 as sum.
Solution
sum = 0x0, carry = 0x1. 0x80 + 0x80 = 0x100 in 9-bit binary is 1_0000_0000 — the carry bit (bit 8) is 1, and the remaining 8 bits (the sum) are all zero.
Summary
In this tutorial, you saw a third working testbench, applied to a clockless combinational design:
- A separate, testbench-only clock can synchronize verification components even when the DUT itself has none
- A small delay after a clock edge avoids racing against non-blocking updates happening on that same edge
- A scoreboard's reference model should implement the same logic the design is supposed to follow, entirely independently, so it can catch a real disagreement rather than just echoing the DUT's own output
- A signal's name doesn't guarantee its actual polarity — check the logic, not just the label
- Deliberately breaking a design once is a valuable way to confirm a scoreboard is actually checking something, not just always reporting success