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
endmodulemodule 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| Part | Role |
|---|---|
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 |
initial | Describes the sequence of input changes and checks |
$finish | Ends 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
endA 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
| Block | Starts at | How it runs |
|---|---|---|
initial | Time 0 | Runs its statements once, in order, then ends |
always | Time 0 | When 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, ...| Part | Meaning |
|---|---|
reg clock = 0; | Declares the clock with an initial value of 0. Without it, ~x is still x and the clock never starts |
#5 | Half a period. Inverting every 5 units gives a period of 10 |
clock = ~clock | Inverts 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| Construct | Behavior |
|---|---|
@(posedge clock) | Waits until clock goes from 0 to 1 |
@(negedge clock) | Waits until clock goes from 1 to 0 |
repeat (n) statement | Runs 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 |
$time | The 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
endtaskreg [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
endThe #1 inside the task lets time pass on every call, so after two calls the time is 2.
3.2 Comparing with !==
| Operator | When x or z is involved | Result inside if |
|---|---|---|
!= | May return x if unknown bits prevent a decision; a known differing bit still gives 1 | An x condition counts as false, so a mismatch can be missed |
!== | Compares all four states bit by bit and returns 0 or 1 | An 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
endAlways 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");
endIf 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.
| Part | Role |
|---|---|
| Reference model | A simple description of the intended behavior, written in the testbench |
| Stimulus task | Applies one cycle of inputs at a safe moment and waits for the edge |
| Comparison | Compares DUT output and model with !== and counts mismatches |
| Summary | One final line that says pass or fail |
| Watchdog | A 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.
// 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
endmoduleTb.v keeps a model register called model. The task step works in four steps:
- Set the inputs on a falling edge, half a period away from the sampling edge.
- Wait for the rising edge.
- Update
modelwith the same priority rule as the DUT. - 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.
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
endmodulePractice this concept
Use this concept in a short Verilog problem as your next step.
Predict before you run.
- What is
accafter all sevenstepcalls? - 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?
Check the result.
| Item | Content |
|---|---|
Final acc | 10 + 20 = 30, held at 30, clear with add gives 0, then 200, then 200 + 100 = 300, which is 44 in eight bits |
SELF-CHECK line | Seven checks, zero errors, final value 44 |
RESULT line | RESULT 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.
| Change | What happens |
|---|---|
Reorder the conditions in Accumulator.v so add is tested before clear | Only 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 message | The 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 step | acc 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.
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
endmodulestep(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?
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?
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:
| Line | What it tells you |
|---|---|
MISMATCH edge t=95 reset=1 enable=1 count=1 model=0 | The first failing case, its inputs, and both values |
SELF-CHECK checks=9 errors=1 | The wrap at 85 passed; one check failed |
RESULT FAIL first mismatch at t=95 | The 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:
// 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
endmoduleIt 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;
endThe 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
| Topic | Key point |
|---|---|
| Testbench shape | A 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 |
| Clock | reg clock = 0; with always #5 clock = ~clock; gives a period-10 clock |
| Event controls | Wait with @(posedge ...), @(negedge ...), repeat (n), and wait (condition) |
| Self-checking | Compare against a model with !==, guard against hangs with a watchdog, end failures with $fatal |
| Coverage of cases | A 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
-
Why does the self-checking testbench compare with
!==rather than!=?Show answer
If unknown bits prevent
!=from deciding equality, it returnsx, whichiftreats as false. In the example, the entirely unknownacctherefore 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. -
Which change turned a mismatch into a nonzero exit code, and why does automated grading need an exit code?
Show answer
Replacing the
$displayin the mismatch branch with$fatal(1, ...). The watchdog uses the same$fatalto 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.