3.5.5HDL & Digital Design Flow

Testbenches and simulation

2,756 words13 min readdifficulty · medium

WHY does this exist?


WHAT are the pieces?


HOW do you build one — derived from scratch

Let's build a TB step by step for a DUT, reasoning about why each line must exist.

Suppose our DUT is a 4-bit synchronous counter:

module counter(input clk, input rst, output reg [3:0] q);
  always @(posedge clk)
    if (rst) q <= 0; else q <= q + 1;
endmodule

Step 1 — a module with no ports.

module tb;

Why? A TB is the top of the hierarchy; there is nothing above it to wire ports to.

Step 2 — declare TB-side signals.

  reg clk, rst;      // we DRIVE these → reg
  wire [3:0] q;      // DUT drives this → wire

Why? clk and rst are outputs of the TB into the DUT, so the TB must hold/assign them → reg. q is driven by the DUT → wire.

Step 3 — instantiate the DUT (connect the rig to the thing under test).

  counter dut(.clk(clk), .rst(rst), .q(q));

Why? Named port connection (.port(signal)) is safer than positional — order changes won't silently mis-wire.

Step 4 — generate a clock.

  initial clk = 0;
  always #5 clk = ~clk;   // period = 10 time units, 100 MHz if 1 unit = 1 ns

Why? Sequential logic needs a posedge. Toggling every 5 units gives a 10-unit period. The always runs forever, which is what a clock must do.

Step 5 — stimulus + self-checking.

  integer expected;
  initial begin
    rst = 1; expected = 0;
    @(posedge clk); #1;          // apply reset for one cycle
    rst = 0;
    repeat (20) begin
      @(posedge clk); #1;        // wait for the flip-flop to update, then check
      expected = expected + 1;
      if (q !== expected[3:0])
        $error("t=\%0t: q=\%0d expected=\%0d",$time, q, expected[3:0]);
    end
    $display("TEST DONE");
    $finish;                     // stop the (otherwise infinite) simulation
  end

Why the #1 after @(posedge clk)? At the exact clock edge the flip-flop's new value hasn't settled in the same delta step — sampling a hair after the edge avoids a race condition and reads the stable value. Why !== not !=? !== also catches x/z (unknown/high-Z), which != treats as ambiguous.

Step 6 — waveform dump (Dual Coding for your eyes).

  initial begin
    $dumpfile("wave.vcd");
    $dumpvars(0, tb);   // record everything under tb
  end

Why? When the self-check fails, the VCD waveform lets you see the timing relationship that broke.

Figure — Testbenches and simulation

The event-driven simulation model (the real engine)


Blocking (=) vs non-blocking (<=) — a testbench trap


Types of testbenches (know these names)


Worked example 2 — combinational DUT (a 2-input AND gate), exhaustive TB

DUT: module and2(input a,b, output y); assign y = a & b; endmodule

module tb;
  reg a, b; wire y;
  and2 dut(.a(a), .b(b), .y(y));
  integer i;
  initial begin
    for (i = 0; i < 4; i = i + 1) begin
      {a, b} = i[1:0];   // Why? concatenation drives both inputs from a counter
      #10;               // Why? let the combinational output settle before checking
      if (y !== (a & b))
        $error("a=%b b=%b y=%b", a, b, y);
    end
    $display("AND2 OK"); $finish;
  end
endmodule

Why exhaustive? A 2-input gate has only 22=42^2 = 4 input combinations — cheap to test all of them. In general an nn-input combinational block has 2n2^n cases; exhaustive testing is only feasible for small nn, which is why random/coverage methods exist for large designs.


Worked example 3 — using a golden reference model

To test an adder, compute the expected sum in the TB itself:

reg [7:0] a, b; wire [8:0] sum;
adder8 dut(.a(a), .b(b), .sum(sum));
initial begin
  repeat (1000) begin
    a = $random; b = $random;   // Why? constrained-random-ish coverage
    #1;
    if (sum !== (a + b))        // Why? {a+b} in TB is the golden model
      $error("%0d + %0d = %0d", a, b, sum);
  end
  $finish;
end

Why this works: the simulator's own integer arithmetic is our trusted reference; the DUT's gate-level logic is what we distrust. Comparing the two automatically finds mismatches over 1000 random cases.


Recall Feynman: explain to a 12-year-old

Imagine you built a toy vending machine. Before you sell candy with it, you make a test kit: a hand that pushes the buttons in order, and a checklist that says "if I push this, I should get that." The test kit is not part of the machine — it just pokes it and watches. That poking-and-checking done inside a computer (before building anything real) is simulation, and the test kit is the testbench. The clever part: instead of you watching every time, the checklist screams automatically when the machine gives the wrong candy.


Active recall

What is a testbench?
A non-synthesizable HDL module that instantiates the DUT, drives stimulus, and checks outputs; it has no ports.
Why is a testbench non-synthesizable?
It uses simulation-only constructs (initial, #delay, $display, $random) that don't map to hardware; its job is to test, not to be built.
In a Verilog TB, why must DUT-input signals be reg?
They are driven by procedural blocks (initial/always) which require a variable that holds its value; a wire can't be procedurally assigned.
In a TB, why must DUT-output signals be wire?
The DUT continuously drives them, and a wire reflects its driver's value.
How do you generate a clock in a testbench?
initial clk=0; always #5 clk=~clk; — toggling every 5 units gives a period of 10 units.
Given a toggle delay d, what is the clock period and frequency?
T = 2d and f = 1/(2d).
Why sample outputs a small time after @(posedge clk)?
At the exact edge old and new values coexist across delta cycles (a race); sampling after the edge reads the settled value.
Difference between != and !== in Verilog?
!== (case inequality) distinguishes x/z exactly; != returns x when operands contain x/z, so it can miss unknown-value bugs.
What is an event-driven simulator?
An engine that keeps a time-ordered event queue, re-evaluates signals only when an input event occurs, and advances time only when no events remain at the current instant.
What are delta cycles?
Zero-time ordered sub-steps within a single simulation time used to propagate combinational updates and resolve non-blocking assignments.
What is a self-checking testbench?
One that compares DUT outputs to a golden reference in code and reports errors automatically ($error) instead of manual waveform inspection.
What is a golden reference model?
A trusted "correct answer" (e.g. a+b computed by the simulator) the TB compares the DUT against.
Why use named port connections in DUT instantiation?
They bind signals to ports by name, so port-order changes can't silently mis-wire the design.
Why must a testbench call $finish?
The clock always block runs forever, so without $finish the simulation never terminates.
When is exhaustive testing feasible?
For small combinational blocks with n inputs (2^n cases); for large n use constrained-random and coverage-driven methods.
What is constrained-random verification?
Generating random-but-legal stimulus to hit corner cases the engineer didn't anticipate, measured by coverage.
What does $dumpvars(0, tb) do?
Records all signals under tb (level 0 = all hierarchy) into a VCD waveform file for viewing.
Why prefer blocking = for TB stimulus?
Stimulus should change immediately at the specified time; non-blocking would defer the change and create off-by-one-cycle confusion.

Connections

Concept Map

wraps + instantiates

generates

drives inputs into

produces

compares outputs vs

checked against

enables

reports

runs

advances

driven signals must be

observed signals must be

catches bugs before

Testbench non-synthesizable

Device Under Test

Stimulus over time

Outputs

Golden reference model

Self-checking $error

Pass or Fail

Event-driven simulator

Simulation time #10

reg / logic

wire / logic

Synthesis + silicon

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, testbench ek aisa HDL module hai jise hum hardware banane ke liye nahi likhte — iska kaam sirf tumhare design (jise DUT, Device Under Test bolte hain) ko test karna hai. Socho tumne ek circuit banaya; ab usko silicon pe daalne se pehle tum ek "test rig" banate ho jo DUT ko inputs deta hai, clock chalata hai, aur outputs ko check karta hai — sab kuch computer ke andar. Isi chalane ko simulation kehte hain. Faayda? Bugs pehle hi pakde jaate hain, warna FPGA/ASIC pe debug karna bahut mehenga aur slow hota hai.

TB module ke koi ports nahi hote kyunki ye sabse upar hota hai, iske upar kuch connect nahi hota. Jo signals tum DUT ko drive karte ho (jaise clk, rst) wo reg hote hain, aur jo DUT se aate hain (jaise q) wo wire. Clock banane ke liye always #5 clk = ~clk; — matlab har 5 unit pe toggle, toh period ban jaata hai 2×5=102\times5 = 10 units. Sabse important trick: output ko clock edge ke thoda baad check karo (@(posedge clk); #1;), warna edge pe purani aur nayi value dono confuse kar deti hain (race condition).

Ek self-checking TB banao — matlab code khud if (q !== expected) $error(...) kar ke bataye, taaki tumhe har baar waveform dekh ke aankhen kharaab na karni pade. !== use karo != ki jagah, kyunki !== x/z (unknown/high-Z) values ko bhi pakadta hai. Aur simulator event-driven hota hai — ye har nanosecond calculate nahi karta, sirf tab kaam karta hai jab koi signal change (event) hota hai, phir agle event pe time jump kar deta hai. Yahi reason hai ki simulation itni fast aur smart hoti hai.

Yaad rakhna 80/20 rule: asli value achhe stimulus + automatic checking se aati hai, waveform ghoorna sabse slow tareeka hai. Aur $finish lagana mat bhoolna, warna always clock hamesha chalta rahega aur sim kabhi rukega hi nahi!

Go deeper — visual, from zero

Test yourself — HDL & Digital Design Flow

Connections