3.5.5 · D3HDL & Digital Design Flow

Worked examples — Testbenches and simulation

3,881 words18 min readBack to topic

Prerequisites we lean on (open them if a word feels new): HDL Basics — Verilog and VHDL, Blocking vs Non-blocking Assignments, Clocking and Sequential Logic, Synthesizable vs Non-synthesizable Constructs.


The scenario matrix

Think of every possible testbench situation as a cell in a grid. If we work one example per cell, the reader is covered everywhere. Here is the grid for this topic.

# Case class What makes it tricky Covered by
C1 Combinational DUT, exhaustive no clock, must let output settle Ex 1
C2 Sequential DUT, one edge must sample after the edge Ex 2
C3 Sequential DUT, full sweep counter wraps around (limiting value) Ex 3
C4 Degenerate input: reset / all-zero flip-flops start as x (unknown) Ex 4
C5 Sign / two's-complement edge overflow at the sign boundary Ex 5
C6 Timing: clock period ↔ frequency unit conversion, the trap Ex 6
C7 Race at the exact edge old vs new value coexist (delta cycles) Ex 7
C8 Blocking vs non-blocking swap = vs <= give different results Ex 8
C9 Real-world word problem "how long will this sim take?" Ex 9
C10 High-Z + exam twist: != vs !== with x/z unknowns silently pass a bad check Ex 10

Every row below is tagged with the cell it fills. Together they touch every cell.


Delta cycles — the on-page definition (used in Ex 2, Ex 7, Ex 8)

Several examples below hinge on how a Verilog simulator orders events inside a single instant of time. Rather than gloss it, here is the concrete rule.

Keep this box in mind; every "delta cycle" reference below points back to it.


Ex 1 — Combinational DUT, exhaustive (cell C1)

Forecast: guess — how many input rows, and what is for ?

  1. Count the input space. Two 1-bit inputs → combinations. Why this step? Exhaustive testing is only sane when the input count is small; is cheap, so we test all of them (no case escapes).

  2. Enumerate. Drive = binary :

    0 0 0
    0 1 1
    1 0 1
    1 1 0

    Why this step? XOR means "different → 1, same → 0". This is the golden reference the TB compares to.

  3. Count delay waits. The loop runs once per row, each with one #10. So waits time units total. Why this step? For a combinational DUT there is no clock — we insert a delay so the output can settle before we read it.

Verify (concrete): the simulator would print no $error lines and end with a single XOR OK message. The self-check numbers: the column contains exactly two 1s (rows 01, 10), and the loop consumes time units. Both are machine-checked below.


Ex 2 — Sequential DUT, one edge (cell C2)

Forecast: does become at the edge, or just after?

  1. State the flip-flop rule. On each rising clock edge, copies . Why this step? This is the whole behaviour — the DUT has no other logic.
  2. Look at the instant of the edge.
    Figure — Testbenches and simulation
    (Figure s01: three stacked waveforms — blue clk, green d=1, orange q. The red dashed line marks the rising edge where old q=0 and new q=1 momentarily coexist; the gray dotted line just after it shows the #1 sample point where q has stably become 1.) At the edge, the old and the new both exist — the new value is still sitting in the NBA region (see the delta-cycle box above) and has not been committed. Why this step? A non-blocking <= computes its result in the active region but writes it only in the later NBA region, so an on-edge read may catch the stale value.
  3. Sample after #1. @(posedge clk); #1; read q; reads the committed . Why this step? The #1 advances simulation time past all delta cycles at the edge, so the NBA write is done — a race-free read.

Verify (concrete): a simulator sampling with #1 prints q=1, matching d=1; sampling on the edge might print q=0 (see the waveform figure s01). The committed value is checked below.


Ex 3 — Sequential DUT, full sweep with wrap (cell C3)

Forecast: a 4-bit register — what is its largest value, and what happens when you add 1 to it?

  1. Range of a 4-bit register. 4 bits store to . Why this step? The limiting value (the top of the range) is where wrap-around bugs live.
  2. Count 15 edges. Starting at , after 15 increments (binary 1111). Why this step? Straightforward accumulation before the interesting event.
  3. The 16th edge — wrap. , but only 4 bits are kept: . So after 16 edges again. Why this step? Hardware silently drops the carry-out; the modulo captures that. Your TB's expected must also wrap, or it will falsely flag an error.
  4. The 17th edge. . Why this step? Confirms the counter resumes normally past the wrap.

Verify (concrete): the printed sequence would be 0,1,2,...,15,0,1. Specifically after 16 edges ; after 17 edges ; top value . All checked below.


Ex 4 — Degenerate input: unknown start / reset (cell C4)

Forecast: x means "I don't know". What is "unknown "?

  1. Meaning of x. x means the simulator cannot decide the bit's value. Why this step? Un-reset flip-flops power up as x — this is the degenerate, all-unknown case.
  2. Propagate through +1. Any arithmetic with an x bit yields x: x + 1 = x. Why this step? Unknown poisons everything downstream; the counter stays x forever.
  3. Consequence. Every self-check q !== expected fires (or, worse, silently passes when written with != — see Ex 10). The run tells you nothing. Why this step? This is why the parent note says always assert reset first.

Verify (concrete): the waveform would show q = xxxx for all cycles (never a clean number). We can't put a literal x into ordinary integer arithmetic, so the check below models the rule "unknown-plus-one is still unknown" as a small function add1('x') == 'x', and confirms it never collapses to a definite number.


Ex 5 — Sign boundary / two's-complement overflow (cell C5)

Forecast: what is "one more than the most positive number" in fixed-width signed hardware?

  1. Two's-complement range for 4 bits. With 4 bits, signed values run up to . Why this step? The sign boundary is the classic overflow edge — a case class of its own.
  2. Raw bits of . = 0111. Add 1 → 1000. Why this step? We track the literal bits because hardware only knows bits, not signs.
  3. Reinterpret 1000 as signed. In two's complement, 1000 . Why this step? The top bit is the sign; 1000 decodes to , so "wraps" to . A TB comparing against a signed golden model must expect , not .

Verify (concrete): a simulator printing %d (signed) shows -8; printing %b shows 1000; printing %u (unsigned) shows 8. 0111+1 = 1000, unsigned reads , signed reads — all checked below.


Ex 6 — Clock period ↔ frequency (cell C6)

Forecast: is the period ns or ns?

  1. One toggle is only half a period. The clock is low for then high for , so a full cycle is . Why this step? The single commonest timing mistake is forgetting the factor of — see the parent's clock derivation. Note is just the delay after the #.
  2. Plug in ns. ns. Why this step? Direct substitution.
  3. Frequency is the reciprocal. MHz. Why this step? Frequency answers "how many cycles per second" — the inverse of "seconds per cycle".
  4. Solve for the target 250 MHz. ns, so ns. Why this step? Invert the whole chain: pick , get , halve for .

Figure — Testbenches and simulation
(Figure s02: a blue square-wave clk. An orange double-arrow spans one half-period labelled d = 3 ns; a green double-arrow spans the full cycle labelled T = 2d = 6 ns with the resulting frequency. It makes the relationship literally visible: the toggle delay is only half the period.)

Verify (concrete): with #3 the printed period between two rising edges is ns → MHz; to hit 250 MHz you code #2. ns, MHz, ns — all checked numerically below.


Ex 7 — The race at the exact edge (cell C7)

Forecast: at the edge, is the old or the new ?

  1. Order of events at one time-step.
    Figure — Testbenches and simulation
    (Figure s03: a horizontal "delta cycles →" timeline through one clock instant, with four coloured dots in order: (1) edge fires, q still 4; (2) TB reads q — may still see 4; (3) q<=q+1 commits in the NBA region, q becomes 5; (4) #1 advances time, safe read gives 5. It shows the read landing before the commit — the race.) At the edge, both the DUT's always and the TB's @(posedge) wake up in the same time step, but the non-blocking update commits in the later NBA region (see the delta-cycle box). Why this step? Delta cycles are zero-time sub-steps; the read may happen in the delta before the write commits.
  2. Read window. If the TB reads in the pre-update delta, is still → the q==5 test fails spuriously. Why this step? This is a race: the outcome depends on scheduler ordering, not on your design.
  3. The fix. Insert #1 (advance real sim time) so all deltas at the edge finish first. Now is committed. Why this step? Advancing time guarantees the NBA write is done before you sample.

Verify (concrete): with #1 the print shows q=5; the committed post-edge value is checked below.


Ex 8 — Blocking vs non-blocking swap (cell C8)

Forecast: which one actually swaps the values?

  1. Blocking = runs top-to-bottom, immediately. a = b sets now; then b = a sees the new , so . Result: . Why this step? Blocking updates take effect before the next line executes — so the second line uses the just-changed value.
  2. Non-blocking <= samples RHS first, updates at end of delta. Both right-hand sides are read (in the active region) with the old values (), then applied together in the NBA region: , . Result: — a clean swap. Why this step? This deferred update (delta-cycle box) is exactly why flip-flops in real designs use <= (see Blocking vs Non-blocking Assignments).
  3. Testbench lesson. For DUT sequential logic use <=; for TB stimulus use = so inputs change immediately at a chosen time. Why this step? Mixing them is the trap the parent note warns about.

Verify (concrete): simulator prints A: a=0 b=0 and B: a=0 b=1. Both checked below.


Ex 9 — Real-world word problem: how long is my sim? (cell C9)

Forecast: roughly 200 ns, or 210 ns?

  1. Cycles counted. 1 reset cycle + 20 test cycles = 21 cycles. Why this step? We must include the reset cycle — it also waits for a posedge.
  2. Time per cycle. Period ns (from always #5). Why this step? Reuse the rule from Ex 6.
  3. Total. ns of simulated time. Why this step? Multiply cycles by seconds-per-cycle to get total elapsed virtual time.
  4. Sanity on the meaning. This ns is simulated time (what $time reports at $finish), not how long your CPU spends running the sim — that wall-clock runtime depends on your machine and can be far shorter or longer (parent note: sim time ≠ real time). Why this step? Beginners conflate the two; making the distinction explicit prevents "why did my 210 ns sim take 3 seconds?" confusion.

Answer: the stimulus block consumes 210 ns of simulated time.

Verify (concrete): $time at $finish reads 210 (plus the tiny #1 nudges we ignored). ns checked below.


Ex 10 — High-Z and the != vs !== trap (cell C10)

Forecast: does a bad (unknown or floating) output get caught, or does it slip through silently?

  1. Evaluate q != 3 when q = xxxx. Because q has x bits, != returns x (unknown), not 1. Why this step? != is blind to x; it cannot commit to "not equal", so it hands back x.
  2. How if treats that x. An if condition treats x (and 0 and z) as false. Why this step? This is the silent killer: if (x) does not run the body, so $error never fires — a broken DUT passes.
  3. Evaluate q !== 3 when q = xxxx. !== compares bit-for-bit: xxxx versus 0011 clearly differ (the x bits are not 0/1), so it returns a firm 1 (true). The $error fires — bug caught. Why this step? !== never returns x; it always gives 0/1, so it never goes blind.
  4. Now the floating case q = zzzz (high-Z). Same logic: q != 3 returns x → treated false → misses the floating bug. q !== 3 sees the z bits differ from 0011 → returns 1catches it. Why this step? A floating output is often worse than a wrong one (it can read as random on real silicon), so you absolutely need !== to catch it. This is the high-impedance edge case the matrix demanded.

Verify (concrete): for q = xxxx: !== vs 3 is true (fires), != is unknown → if treats false (does not fire). For q = zzzz: identical outcome — !== fires, != slips. All four outcomes checked below.


Recall

Recall Why insert

#1 after @(posedge clk) before checking? To advance past the delta cycles at the edge so the NBA (non-blocking) write has committed, avoiding a race. ::: Sample a hair after the edge so the non-blocking update has landed.

Recall 4-bit counter: value after 16 edges from 0?

— it wraps back to 0. ::: 0

Recall Signed 4-bit:

Bits 01111000 = . :::

Recall

always #d clk = ~clk; gives what period and frequency? , . ::: Period is twice the toggle delay .

Recall Why

!== and not != in a self-check, and what are the four logic values? The values are 0,1,x (unknown),z (high-Z). != returns x on any x/z input, which if treats as false → bug slips through; !== compares bit-for-bit and returns a firm 1/0, catching x/z. ::: The triple/two-bang exact operator sees unknowns and floating.


Back to Testbenches and simulation · deeper on unknowns and coverage: Functional Coverage and Verification, SystemVerilog Assertions (SVA).