3.5.5 · D4HDL & Digital Design Flow

Exercises — Testbenches and simulation

3,501 words16 min readBack to topic

Before we start, one tiny picture to fix vocabulary in your eyes:

The testbench (TB) is the box on the outside. It has no ports — nothing connects to it from further out. Inside it sits the DUT (Device Under Test), your real design. The TB drives signals in (chalk-blue arrows) and observes signals out (chalk-pink arrows). Keep this picture in mind for every exercise.

Two ideas will haunt every exercise below — the clock edge and simulation time. Let's see them before we use them:

The chalk-blue trace is clk. A rising edge (posedge) is the instant it jumps 0→1 (yellow dashed lines). The pink trace is rst: it is held high for exactly one period, then released. Simulation time runs left to right along the x-axis, measured in time units — it is virtual, not wall-clock. When you write #5 you advance this axis by 5 units. Everything about "sampling after the edge" lives on this picture.


Level 1 — Recognition

Can you name the pieces and read the rules?

Recall Solution 1.1

Rule: signals the TB drivesreg; signals the DUT drives (TB only observes) → wire.

  • clk — TB generates it → reg.
  • rst — TB drives it → reg.
  • q — the DUT's output, driven by the DUT → wire.

Why? A reg holds a value until a procedural block (initial/always) changes it; a wire simply reflects whatever is continuously driving it. You cannot procedurally assign a wire, and a reg driven by two sources would conflict.

Recall Solution 1.2

Here the toggle delay is ns (recall: is the half-period we defined above). Step 1 — build the period. One full period is the low half plus the high half. Each half lasts , so the period is ns. Step 2 — why frequency is the reciprocal. Frequency means "how many full periods fit in one second." If one period takes seconds, then of them fit in a second — that is the definition . So MHz.

Recall Solution 1.3

The TB is the top of the design hierarchy. Ports exist so a module can be connected to something above it. There is nothing above the TB, so there is nothing to connect ports to. All stimulus and checking happens inside the TB, using its own internal reg/wire signals.


Level 2 — Application

Can you write the mechanics correctly?

Recall Solution 2.1

Step 1 — link period to frequency. From Exercise 1.2 the period is , and frequency is the reciprocal of the period, . Substitute : this gives . Step 2 — solve for the delay. Rearranging for gives . Step 3 — plug in, converting MHz to Hz. "Mega" means , so MHz Hz. Then seconds ns. Statement: always #12.5 clk = ~clk; (a fractional delay is legal in simulation if the timescale precision allows it — e.g. `timescale 1ns/100ps).

Recall Solution 2.2
initial begin
  rst = 1;            // assert reset immediately (blocking =)
  @(posedge clk);     // wait through one rising edge...
  #1;                 // ...settle past the edge
  rst = 0;            // release reset
end

Why blocking =? TB stimulus should change immediately at the intended time; non-blocking would defer it inside the delta and can cause off-by-one confusion with the DUT sampling on the same edge.

What the "hair" actually is — #1 vs #0. A "hair after the edge" is a genuine advance of one time unit (#1): simulation time moves from to , so all edge-triggered updates that were scheduled at have already committed. That is different from #0, which advances zero time — it only pushes execution into a later delta cycle at the same time , where old and new values may still be racing. So #1 is the safe choice; #0 does not escape the racing instant.

What a delta cycle is (see the figure below). At a single simulation time , the simulator does not do everything at once. It runs in ordered sub-steps called delta cycles () — all at the same time — so that combinational ripples and non-blocking commits settle in a definite order before time advances.

Reading the strip: at the posedge time , non-blocking updates are scheduled in but only committed at the end of the delta region. Your initial block resuming in could read the old value — a race. Advancing #1 to (yellow) reads the committed value, safely.

Recall Solution 2.3

Two 1-bit inputs → combinations: 00, 01, 10, 11. Exhaustive is cheap because the input space is tiny. In general an -input combinational block has vectors, so exhaustive is only feasible for small (e.g. is fine; is not).


Level 3 — Analysis

Can you predict what a subtle piece of code does?

Recall Solution 3.1

(A) Blocking = executes top-to-bottom, immediately:

  • a = b;a becomes 0 right now.
  • b = a;a is already 0, so b becomes 0.
  • Result: a=0, b=0 (the value was destroyed — no swap).

(B) Non-blocking <= reads all right-hand sides first, then schedules updates at the end of the delta:

  • RHS read: a's new value ← old b = 0; b's new value ← old a = 1.
  • Commit: a=0, b=1.
  • Result: a=0, b=1 (a clean swap).

This is exactly why sequential logic uses <=: it models flip-flops sampling their inputs simultaneously on the edge. Trace it against the delta-cycle strip above: the RHS reads happen in , the commit at the end of the delta region.

Recall Solution 3.2

Unreliable — it's a race. With no #1, at each @(posedge clk) the block resumes in the same simulation time as the flip-flop's update. Looking at the delta strip (figure s03): the block may resume in an early delta before the DUT's non-blocking q <= q+1 has committed, so it reads the pre-edge value of q. It then compares that stale q against the freshly incremented expected, producing spurious $errors (or masking real ones). Fix: insert #1 after @(posedge clk) so time advances to and q has committed to its new value before the comparison. With the fix, on the first counted edge q=1 and expected=1 — they match.

Recall Solution 3.3

A 4-bit register counts modulo : values cycle 1,2,...,15,0,1,.... After 16 counted edges (edges 1..16) expected has been incremented to 16, but q wraps: on edge 15 → q=15, edge 16 → q=0. So . The comparison uses expected[3:0] = the low 4 bits = , which correctly matches q=0. Comparing against the full 32-bit expected (=16) would falsely fail, because q physically cannot store 16.


Level 4 — Synthesis

Can you assemble a complete, correct component?

Recall Solution 4.1
`timescale 1ns/1ps
module tb;
  reg clk, rst;          // TB drives → reg
  wire [3:0] q;          // DUT drives → wire
  integer expected;
 
  counter dut(.clk(clk), .rst(rst), .q(q));   // named ports
 
  // Step: clock — d=5ns → T=10ns → 100 MHz
  initial clk = 0;
  always #5 clk = ~clk;
 
  // Step: stimulus + self-check
  initial begin
    rst = 1; expected = 0;
    @(posedge clk); #1;        // hold reset one cycle
    rst = 0;
    repeat (20) begin
      @(posedge clk); #1;      // settle past edge, 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 forever clock
  end
 
  // Step: waveform dump
  initial begin
    $dumpfile("wave.vcd");
    $dumpvars(0, tb);
  end
endmodule

Every line earns its place: no ports on tb; reg for driven, wire for observed; named port connection; d=5 for 100 MHz; #1 to dodge the edge race; !== to catch x/z; $finish to end the otherwise-infinite sim.

Recall Solution 4.2

Step 1 — start from the linked formula. We showed , so (the half-period is one over twice the frequency). Step 2 — put in MHz and in ns. One MHz is Hz and one ns is s, and these two factors of and combine so that if is measured in MHz then in ns is .

parameter real F_MHZ = 250;
localparam real HALF = 500.0 / F_MHZ;   // ns
always #(HALF) clk = ~clk;

Step 3 — evaluate. For F_MHZ = 250: ns → period ns → frequency MHz. ✓


Level 5 — Mastery

Can you reason about verification strategy and coverage?

Recall Solution 5.1

(a) Inputs total bits → vectors. (b) s ≈ 33 ms — trivial. (c) bits → vectors. At 2M vec/s that's about seconds ≈ 292{,}000 years. Not feasible. This is exactly why we switch to constrained-random stimulus and functional coverage — we can't hit every input, so we measure which meaningful scenarios we did hit and stop when coverage goals are met.

Recall Solution 5.2
  • (a) Assertion-based (SVA) — a continuous temporal rule ("never more than 3 cycles") is exactly what an assert property checks over time. See SystemVerilog Assertions (SVA).
  • (b) Directed / deterministic — hand-written vectors for known corner cases.
  • (c) Constrained-random — generate random-but-legal inputs to hit unanticipated cases.
  • (d) Coverage-driven — measure which state transitions were exercised; tells you when you're "done." See Functional Coverage and Verification. All of these are usually layered on top of a self-checking backbone so failures are caught automatically.
Recall Solution 5.3

Step 1 — set the target. 95% of 144 is transitions, so we need . Step 2 — plug in and isolate the exponential. . Divide both sides by 144: , hence . Step 3 — undo the exponential with the natural log. We use here because it is the exact inverse of — it answers "what exponent gives this value?". Taking of both sides: . Step 4 — solve for . , and since we can only run whole cycles we round up to cycles.

Reading the curve: coverage rises fast then saturates — the last few percent cost the most cycles. That is the whole point of coverage-driven verification: it tells you when extra random cycles stop paying off.