Skip to content
code0.HDL

Testbench techniques

Write a testbench with reg inputs, wire outputs and a dut instance, make time pass with # delays, a clock and event controls, then package stimulus in a task, compare against a model with !==, and end a failing or hanging run with $fatal.

In this chapter

  • Write a testbench module with reg inputs, wire outputs, a dut instance, an initial sequence and $finish
  • Generate a clock with always #5 and wait with @(posedge), @(negedge), repeat and wait
  • Package one cycle of stimulus in a task and compare the DUT with a reference model using !==
  • End a failing or hanging run with $fatal so that the exit code, not only the text, reports it
  • List the conditions a passing testbench never applies, add them, and find the first mismatch time in the waveform
Concepts in this chapter
  • Testbench techniques
Prerequisite concepts

1. The shape of a testbench

A testbench is a module without ports that you write to exercise a design. It instantiates the design under test (DUT), changes its inputs over time, and watches its outputs or compares them with expected values. In a clocked design many values change on the same edge, so you have to know exactly when to change an input and when to read an output before your testbench can be trusted.

Delays such as #5, system tasks such as $display, and an initial block that steps through a test sequence exist for simulation; synthesis tools ignore or reject them, and which ones they accept depends on the tool. @(posedge clock) is different: as the sensitivity of an always block it is the standard way to describe a synthesizable register, which is exactly how the DUTs of this course use it. It is only in a testbench, as a wait in the middle of a sequence, that it is a simulation device. Keep the DUT to the register and combinational idioms, and let the testbench use any simulation construct it needs. This chapter runs everything in simulation with Icarus 11; nothing here is synthesized.

A testbench is a top-level module with no ports. The signals connected to the DUT's inputs are assigned by the testbench, so they are declared reg. The signals connected to its outputs are driven by the DUT, so they are wires.

Syntax

module tb;
  reg  input_signal;    // assigned by the testbench
  wire output_signal;   // driven by the DUT
  dut_module dut (.in(input_signal), .out(output_signal));
  initial begin
    // change inputs, wait, check outputs
    $finish;
  end
endmodule
module tb;
  reg  a, b;
  wire y;
  and_gate dut (.a(a), .b(b), .y(y));
  initial begin
    a = 1; b = 0;
    #1 $display("y=%b", y);
    b = 1;
    #1 $display("y=%b", y);
    $finish;
  end
endmodule
PartRole
module tb;No port list. Nothing instantiates it, so it becomes the top
reg a, b;Variables that drive the DUT's inputs
wire y;A net driven by the DUT's output
and_gate dut (...)The instance under test, conventionally named dut
initialDescribes the sequence of input changes and checks
$finishEnds the simulation

2. Time and procedural blocks

2.1 The # delay

# followed by a number lets that much simulation time pass before the next statement runs. Delays in this form are procedural delay controls. Verilog also supports delays on continuous assignments and gates; those forms are not used here.

Syntax

#delay statement;
#delay;
initial begin
  a = 0;          // time 0
  #10 a = 1;      // time 10
  #5  a = 0;      // time 15
end

A delay counts from the moment the previous statement ran. The second assignment above runs at time 10, and the third 5 units later, at time 15. What a unit means physically is set with `timescale, in Register stages, observation and waveforms.

2.2 initial and always

BlockStarts atHow it runs
initialTime 0Runs its statements once, in order, then ends
alwaysTime 0When it reaches the end, it starts over. It repeats forever

Syntax

initial statement;
always  statement;

An always block must contain a point where time passes or where it waits for an event. Without one it would loop forever at time 0, so Icarus stops compilation with always process does not have any delay.

2.3 Generating a clock

A clock is an always block that inverts a signal at a fixed interval.

Syntax

reg clock = 0;
always #half_period clock = ~clock;
reg clock = 0;
always #5 clock = ~clock;   // period 10; rising edges at 5, 15, 25, ...
PartMeaning
reg clock = 0;Declares the clock with an initial value of 0. Without it, ~x is still x and the clock never starts
#5Half a period. Inverting every 5 units gives a period of 10
clock = ~clockInverts the current value

Tip. The same clock can be written as initial begin clock = 0; forever #5 clock = ~clock; end. forever repeats its statement endlessly. Either way, the first rising edge is at 5 and the first falling edge at 10.

2.4 Event controls and waiting

@(...) suspends a block until the event in the parentheses happens.

Syntax

@(posedge signal) statement;
@(negedge signal) statement;
repeat (count) @(posedge signal);
wait (condition) statement;
initial begin
  @(posedge clock);              // next rising edge: time 5
  @(negedge clock);              // next falling edge: time 10
  repeat (3) @(posedge clock);   // three more rising edges: time 35
  $display("t=%0t", $time);
end
ConstructBehavior
@(posedge clock)Waits until clock goes from 0 to 1
@(negedge clock)Waits until clock goes from 1 to 0
repeat (n) statementRuns the statement n times; with an event control, it waits for n edges
wait (condition)Waits until the condition is true, or continues at once if it already is
$timeThe current simulation time. %0t prints it without leading spaces

What a testbench sees when it reads a register at the edge, after #1, or with $strobe is the observation table in Register stages, observation and waveforms; the scheduling regions behind it, and why inputs are changed on the falling edge, are in Registers and nonblocking assignment.

3. Tasks and self-checking

3.1 Tasks

A task groups several statements under a name so you can call them repeatedly. Unlike a function, a task may contain delays and event controls, which makes it the natural way to package one cycle of stimulus.

Syntax

task task_name(input [width] arg, input [width] arg);
  begin
    // statements; delays and event controls are allowed
  end
endtask
reg [7:0] a, b;
task show(input [7:0] x, input [7:0] y);
  begin
    a = x; b = y;
    #1 $display("sum=%0d", a + b);
  end
endtask

initial begin
  show(8'd3, 8'd4);     // sum=7
  show(8'd10, 8'd20);   // sum=30; time is now 2
end

The #1 inside the task lets time pass on every call, so after two calls the time is 2.

3.2 Comparing with !==

OperatorWhen x or z is involvedResult inside if
!=May return x if unknown bits prevent a decision; a known differing bit still gives 1An x condition counts as false, so a mismatch can be missed
!==Compares all four states bit by bit and returns 0 or 1An x output differs from a known expected value
reg [7:0] acc, model;   // acc has never been assigned, so it is x
initial begin
  model = 8'd5;
  if (acc != model)  $display("mismatch");   // does not run
  if (acc !== model) $display("mismatch");   // runs
end

Always use !== for the comparisons in a self-checking testbench.

3.3 $fatal and the watchdog

$fatal prints a message, ends the simulation immediately, and makes vvp exit with a nonzero code. The first argument is a number that selects how much diagnostic information is printed at exit; this course uses 1.

Syntax

$fatal(1, "format", value, ...);

A watchdog is a separate initial block that guards against a run that never ends.

initial begin
  #1000 $fatal(1, "TIMEOUT");
end

If the main initial block reaches $finish normally, the watchdog's $fatal never runs. If the main block hangs waiting for something that never happens, the watchdog ends the run at time 1000.

Note. $fatal is a SystemVerilog system task (IEEE 1800); Verilog-2005 does not define it. Icarus 11 accepts it with -g2001 and -g2005 as well, but other Verilog-2005 tools may not. Every other construct in this chapter's testbenches is Verilog-2001. The course compiles with -g2012, which also reserves SystemVerilog keywords: a signal named before, logic or bit that compiles under -g2005 is a syntax error under -g2012.

3.4 Example: a self-checking accumulator test

A self-checking testbench computes the expected result itself and compares it with the DUT every cycle. Its parts are nearly always the same.

PartRole
Reference modelA simple description of the intended behavior, written in the testbench
Stimulus taskApplies one cycle of inputs at a safe moment and waits for the edge
ComparisonCompares DUT output and model with !== and counts mismatches
SummaryOne final line that says pass or fail
WatchdogA separate initial block that ends a run that hangs

Accumulator.v adds in to acc at each edge where add is 1. clear sets acc to 0, and because the if/else if chain tests clear first, clear wins when both inputs are 1, the priority rule from Reset, enable and priority.

Accumulator.v
// An 8-bit accumulator: clear wins over add.
module accumulator (
  input            clock,
  input            clear,
  input            add,
  input      [7:0] in,
  output reg [7:0] acc
);
  always @(posedge clock) begin
    if (clear)    acc <= 8'd0;
    else if (add) acc <= acc + in;
  end
endmodule

Tb.v keeps a model register called model. The task step works in four steps:

  1. Set the inputs on a falling edge, half a period away from the sampling edge.
  2. Wait for the rising edge.
  3. Update model with the same priority rule as the DUT.
  4. Wait one unit for the DUT's update to settle, then compare.

The seven step calls cover clearing, adding, holding with add low, clear and add together, and an addition that wraps past 255. The second initial block is a watchdog that ends the run at time 1000.

Tb.v
module tb;
  reg        clock = 0;
  reg        clear = 0, add = 0;
  reg  [7:0] in = 0;
  wire [7:0] acc;
  reg  [7:0] model;
  integer    checks = 0, errors = 0;

  accumulator dut (.clock(clock), .clear(clear), .add(add), .in(in), .acc(acc));

  always #5 clock = ~clock;

  // Apply one cycle of inputs, then compare the DUT with the model.
  task step(input c, input a, input [7:0] value);
    begin
      @(negedge clock);
      clear = c; add = a; in = value;
      @(posedge clock);
      if (c)      model = 8'd0;
      else if (a) model = model + value;
      #1;
      checks = checks + 1;
      if (acc !== model) begin
        errors = errors + 1;
        $display("MISMATCH check %0d: acc=%0d model=%0d", checks, acc, model);
      end
    end
  endtask

  // A watchdog ends a run that would otherwise never finish.
  initial begin
    #1000 $fatal(1, "TIMEOUT: no result after 1000 time units");
  end

  initial begin
    step(1, 0, 0);
    step(0, 1, 8'd10);
    step(0, 1, 8'd20);
    step(0, 0, 8'd99);
    step(1, 1, 8'd5);
    step(0, 1, 8'd200);
    step(0, 1, 8'd100);
    $display("SELF-CHECK checks=%0d errors=%0d acc=%0d", checks, errors, acc);
    if (errors == 0) $display("RESULT PASS");
    else             $display("RESULT FAIL");
    $finish;
  end
endmodule

Practice this concept

Use this concept in a short Verilog problem as your next step.

Predict before you run.

  1. What is acc after all seven step calls?
  2. How many lines does a correct DUT produce?

Check

What is acc after the seventh step call, 200 + 100 added into an 8-bit register?

Your answer

Check the result.

ItemContent
Final acc10 + 20 = 30, held at 30, clear with add gives 0, then 200, then 200 + 100 = 300, which is 44 in eight bits
SELF-CHECK lineSeven checks, zero errors, final value 44
RESULT lineRESULT PASS

A correct DUT prints nothing except these two lines. A passing self-checking testbench is quiet, and a failing one tells you which check broke.

Try it yourself.

ChangeWhat happens
Reorder the conditions in Accumulator.v so add is tested before clearOnly check 5 drives both inputs high, so MISMATCH check 5: acc=35 model=0 appears first. The error carries into later values, two more mismatches follow, and the run ends with errors=3 and RESULT FAIL. The exit code is still 0
Keep the bug and replace the $display in the mismatch branch with $fatal(1, ...) and the same messageThe run stops at the first mismatch and prints a FATAL: line with the file, line, time, and scope. vvp exits with code 1
Restore the DUT and add wait (acc == 8'd255); after the last stepacc never reaches 255, so the main block waits forever. At time 1000 the watchdog prints TIMEOUT: no result after 1000 time units and exits with code 1

Tip. Icarus writes the FATAL: report to standard output, not standard error.

3.5 A passing testbench that misses a case

A self-checking testbench can only report mismatches in the cases it applies. WrapCounter.v is meant to follow the counter contract of Reset, enable and priority: reset first, then a wrap from 5 to 0 while enabled, then increment, otherwise hold. Read Tb.v first and leave the design closed for now.

Tb.v
module tb;
  reg        clock = 0;
  reg        reset = 0, enable = 0;
  wire [2:0] count;
  reg  [2:0] model;
  integer    checks = 0, errors = 0, edge_time, first_bad = -1;

  wrap_counter dut (.clock(clock), .reset(reset), .enable(enable), .count(count));

  always #5 clock = ~clock;

  initial begin
    $dumpfile("dump.vcd");
    $dumpvars(0, tb);
  end

  // One cycle: drive on the falling edge, update the model at the rising edge, compare.
  task step(input r, input e);
    begin
      @(negedge clock);
      reset = r; enable = e;
      @(posedge clock);
      edge_time = $time;
      if (r)      model = 3'd0;
      else if (e) model = (model == 3'd5) ? 3'd0 : model + 3'd1;
      #1;
      checks = checks + 1;
      if (count !== model) begin
        errors = errors + 1;
        if (first_bad < 0) first_bad = edge_time;
        $display("MISMATCH edge t=%0d reset=%b enable=%b count=%0d model=%0d",
                 edge_time, r, e, count, model);
      end
    end
  endtask

  initial begin
    // Only normal cases so far.
    step(1, 0);
    step(0, 1); step(0, 1);
    step(0, 0);
    step(0, 1);
    $display("SELF-CHECK checks=%0d errors=%0d", checks, errors);
    if (errors == 0) $display("RESULT PASS");
    else             $display("RESULT FAIL first mismatch at t=%0d", first_bad);
    $finish;
  end
endmodule

step(r, e) drives reset = r and enable = e on a falling edge, updates model at the rising edge with the contract, and compares one unit later with !==. On the first mismatch it stores the rising-edge time in first_bad; the MISMATCH line carries the same time. The run also records dump.vcd.

Check

Tb.v applies step(1,0), step(0,1) twice, step(0,0) and step(0,1). Which conditions of the contract does it never apply?

Your answer

Press Run. The result is SELF-CHECK checks=5 errors=0 and RESULT PASS. That pass says the five cases it checked behave; it says nothing about the two conditions it never applied.

Add the missing cases. After the line step(0, 1); that ends the normal cases, add

step(0, 1); step(0, 1); step(0, 1);
step(1, 1);

The first three steps count 4, 5 and then wrap to 0; the last raises reset while enable is 1. The first step was driven at time 10 and checked at the rising edge at 15, and every step takes one 10-unit period.

Check

At which rising-edge time is the added step(1, 1), the reset with enable 1, checked?

Your answer

Run again. A MISMATCH line names the edge time of the failing step, so you can tell which added case failed from the time alone:

LineWhat it tells you
MISMATCH edge t=95 reset=1 enable=1 count=1 model=0The first failing case, its inputs, and both values
SELF-CHECK checks=9 errors=1The wrap at 85 passed; one check failed
RESULT FAIL first mismatch at t=95The time to look up in the waveform

Open the waveform and go to time 95. reset rose at the falling edge at 90 while enable stayed 1; at the rising edge at 95, count steps from 0 to 1 instead of staying 0. Rows before 95 are the ones that passed, so the first mismatch time is where to start reading. Now open the design:

WrapCounter.v
// The same counting contract, but enable is tested before reset.
module wrap_counter (
  input            clock,
  input            reset,
  input            enable,
  output reg [2:0] count
);
  always @(posedge clock) begin
    if (enable) begin
      if (count == 3'd5) count <= 3'd0;
      else               count <= count + 3'd1;
    end
    else if (reset)
      count <= 3'd0;
  end
endmodule

It tests enable before reset, so a reset during an enabled cycle is ignored. Move reset to the front of the chain, keeping the wrap inside the enable branch, and run again: checks=9 errors=0 and RESULT PASS. This is the opposite bug from the Practice problem reset-priority, where reset is ignored while enable is 0; between them, a testbench needs reset both with and without enable.

4. Common mistakes

4.1 An always block with no delay

// Wrong: time never advances, so this loops forever
always clock = ~clock;

// Right
always #5 clock = ~clock;

An always block without a delay or event control would repeat forever at time 0. Icarus prints always process does not have any delay. and A runtime infinite loop will occur., and stops compiling.

4.2 Forgetting $finish

always #5 clock = ~clock;

// Wrong: the clock keeps running, so the simulation never ends
initial begin
  repeat (10) @(posedge clock);
  $display("done");
end

// Right
initial begin
  repeat (10) @(posedge clock);
  $display("done");
  $finish;
end

The always block that makes the clock never ends, so the simulation keeps going after the initial block is done. In this lab it runs until it hits the execution time limit. Put $finish where your checks are complete.

4.3 Missing x with !=

// Wrong: if acc is x, the condition is x and counts as false
if (acc != model) errors = errors + 1;

// Right
if (acc !== model) errors = errors + 1;

A defect that leaves the output at x, such as a missing reset, passes a != check. !== reports x as a mismatch.

4.4 Printing a failure but exiting with 0

// Wrong: a person reads FAIL, but a script sees success
if (errors != 0) $display("RESULT FAIL");
$finish;

// Right: a failure ends the run with a nonzero exit code
if (errors != 0) $fatal(1, "RESULT FAIL errors=%0d", errors);
$finish;

A script that runs your test looks at the exit code, not at the words you printed. In any testbench meant to run automatically, end a failure with $fatal.

Changing an input on the sampling edge, the race that step avoids by driving on the falling edge, is the mistake covered in Registers and nonblocking assignment.

5. Summary

TopicKey point
Testbench shapeA module without ports. DUT inputs connect to regs, outputs to wires
Time#n runs the next statement n units later. Every always needs a delay or event control
Clockreg clock = 0; with always #5 clock = ~clock; gives a period-10 clock
Event controlsWait with @(posedge ...), @(negedge ...), repeat (n), and wait (condition)
Self-checkingCompare against a model with !==, guard against hangs with a watchdog, end failures with $fatal
Coverage of casesA pass covers only the cases applied: list the contract's conditions and apply each one, then find the first mismatch time in the waveform

The Practice problem reset-priority below this chapter is a counter that ignores reset while enable is low: trace which branch runs at each edge and make reset win every time. Its testbench prints at the rising edge, so read each line as the value from before that edge, the first row of the observation table in Register stages, observation and waveforms.

6. Check your understanding

  1. Why does the self-checking testbench compare with !== rather than !=?

    Show answer

    If unknown bits prevent != from deciding equality, it returns x, which if treats as false. In the example, the entirely unknown acc therefore escapes the check. A known differing bit can still make != return 1. !== always returns 0 or 1 and detects an unknown actual value when the expected value is known.

  2. Which change turned a mismatch into a nonzero exit code, and why does automated grading need an exit code?

    Show answer

    Replacing the $display in the mismatch branch with $fatal(1, ...). The watchdog uses the same $fatal to end a hung run with exit code 1. An automated grader does not read the meaning of printed lines; it decides pass or fail from the exit code, so a failure has to show up there.

Checkpoint

Run the example and compare it with your prediction

Sources

Licences and notices for the sources this site follows