Testbenches and scheduling
Apply clean stimulus, check outputs automatically, and understand when registers change.
A testbench is not RTL
A testbench has no ports. It instantiates the circuit, creates the clock, drives inputs, and decides whether the result is correct.
`timescale 1ns/1ps
module edge_counter (
input wire i_clk,
input wire i_reset,
input wire i_enable,
output reg [3:0] o_count
);
always @(posedge i_clk) begin
if (i_reset)
o_count <= 4'd0;
else if (i_enable)
o_count <= o_count + 1'b1;
end
endmodule
module tb_edge_counter;
reg i_clk;
reg i_reset;
reg i_enable;
wire [3:0] o_count;
edge_counter dut (
.i_clk (i_clk),
.i_reset (i_reset),
.i_enable (i_enable),
.o_count (o_count)
);
initial i_clk = 1'b0;
always #5 i_clk = ~i_clk;
task expect_count;
input [3:0] expected;
begin
if (o_count !== expected) begin
$display("FAIL at %0t: expected %0d, got %0d",
$time, expected, o_count);
$finish;
end
end
endtask
initial begin
i_reset = 1'b1;
i_enable = 1'b0;
repeat (2) @(posedge i_clk);
@(negedge i_clk);
expect_count(0);
i_reset = 1'b0;
i_enable = 1'b1;
repeat (3) @(posedge i_clk);
@(negedge i_clk);
expect_count(3);
i_enable = 1'b0;
repeat (2) @(posedge i_clk);
@(negedge i_clk);
expect_count(3);
$display("PASS");
$finish;
end
endmoduleThe test changes control signals on a falling edge and checks on a falling edge. They are stable before the next active edge, and nonblocking assignments have been applied before the read.
Understanding one simulation time step
Several events can share one simulation time. On a rising edge:
- blocks sensitive to that edge are activated;
- the right-hand side of each nonblocking assignment is evaluated;
- new values are applied later in the same time step.
A test that reads an output in the same region as the edge can therefore see the old value. Scattering arbitrary delays through the test hides the issue without establishing a sound method. Choose stimulus and checking edges deliberately.
Assignments in the testbench
Stimulus signals are usually driven with blocking assignments. Registers in the DUT remain described with nonblocking assignments. This separation reduces races between the test and circuit.
To compare signals that may contain x or z, === and !== make those values visible. With ==, the comparison result can itself become unknown.
Waiting for a useful event
@(posedge i_clk) waits for an edge. wait(o_done) waits until a condition becomes true. repeat (N) repeats an action a known number of times. A delay such as #10 advances by an absolute duration defined by timescale.
Protocol-based waits are often more robust than fixed delays. Waiting for o_valid, for example, survives a latency change better than waiting exactly four periods.
Making the test self-checking
A useful test knows the expected result and fails on its own. It should report at least the scenario, expected value, and received value.
Waveforms remain useful for understanding a failure, but they should not be the only way to know whether one hundred cases passed.
Covering boundaries
For a counter or FIFO, testing only a few normal values is not enough. Also cover reset, wraparound, full, empty, simultaneous requests, and controls held high for several cycles.
Key points
- The testbench creates time and stimulus; it is not synthesized.
- A nonblocking assignment updates its target later in the same simulation step.
- Deliberate stimulus and checking edges prevent races.
===and!==explicitly detectxandz.- A test should report its own pass or failure result.
📝 Test your knowledge - Chapter quiz