Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 41

Developing a Testbench with Input Sequences

Recommended resources

Welcome back! In our last lesson, we explored the "why" behind verification, establishing that a structured, planned approach is essential for ensuring a chip design is correct. We saw that the testbench is the environment where this verification plan is executed.

Today, we'll take our first practical step into building these environments. This lesson focuses on the most fundamental task of a testbench: applying a sequence of inputs, also known as stimulus, to a design under test (DUT). You will learn how to create a basic testbench module, instantiate your design within it, and write Verilog code to drive its inputs in a controlled sequence.

1. The Anatomy of a Testbench

At its core, a testbench is a Verilog module created solely for simulation. It is non-synthesizable, meaning it cannot be turned into physical hardware. This gives us the freedom to use the full range of Verilog constructs to create complex test scenarios.

A basic testbench has two main parts:

  1. The Testbench Module: A top-level Verilog module with no inputs or outputs. It contains all the logic for testing.
  2. The Design Under Test (DUT): An instance of the design you want to verify, placed inside the testbench module.

The testbench generates stimulus to drive the DUT's inputs and provides wires to observe the DUT's outputs.

Simple Testbench Form
This diagram shows the fundamental relationship in a simulation. The testbench module wraps around the synthesizable Design Under Test (DUT). It uses internal `reg` variables to drive the DUT's inputs and `wire`s to monitor the DUT's outputs.

A crucial concept to grasp is how signals are declared inside the testbench:

  • DUT Inputs are connected to variables of type reg in the testbench. This is because we need to actively assign and hold values onto these signals from within procedural blocks (like an initial block), which requires a reg type.
  • DUT Outputs are connected to variables of type wire in the testbench. The testbench doesn't drive these signals; it passively observes the values being driven out by the DUT.

The following video explains this important distinction and shows how to instantiate the DUT.

Vivado Simulator and Test Bench in Verilog | Xilinx FPGA Programming Tutorials

This video, "Vivado Simulator and Test Bench in Verilog," clearly explains the declaration of signals and the instantiation of a DUT inside a testbench.

Watch these two short clips: Signal declarations: Pay close attention to the explanation of why inputs to the DUT are declared as reg and outputs as wire within the testbench. DUT instantiation: Observe how the DUT is instantiated, connecting its ports to the regs and wires you just declared.


2. Applying Inputs with Delays

To apply a sequence of inputs, we need two key Verilog features: a procedural block to contain our stimulus sequence and a way to control the timing between input changes.

  • initial block: This is a procedural block where the code executes exactly once, starting at time 0 of the simulation. It's perfect for defining a finite sequence of test vectors.
  • # delay operator: This operator pauses the simulation for a specified number of time units. For example, #10; creates a delay of 10 time units.
  • timescale directive: This compiler directive, placed at the top of a Verilog file, defines what a time unit means. For example, `timescale 1ns / 1ps sets the main time unit to 1 nanosecond and the simulation precision to 1 picosecond.

The following reading provides a great summary of these concepts and puts them together in a complete example.

How to Write a Basic Verilog Testbench

This article from FPGA Tutorial clearly explains the essential constructs for writing a basic testbench.

Please read the following sections: Modelling Time in Verilog: Focus on the # delay operator and the timescale directive. Verilog initial block: Understand its purpose and syntax. Verilog Testbench Example: Read through the full example. Notice how it combines a module declaration, DUT instantiation, signal declarations, and an initial block with delays to apply a sequence of inputs.

Now, let's see this in action. The following video demonstrates how to write a simple testbench for a full adder, applying a few fixed input vectors one after another.

WRITING VERILOG TEST BENCHES

The video "WRITING VERILOG TEST BENCHES" provides a practical demonstration of applying a hard-coded stimulus sequence.

Watch the section showing a simple testbench for a full adder. Observe how the initial block is used to set the values of a, b, and c, with #5 delays in between each change.


3. Automating Stimulus Generation

Manually writing every single input combination, as we just saw, is manageable for a 3-input adder but quickly becomes impractical for designs with more inputs. A 16-bit input bus would require = 65,536 lines of code!

A more efficient method is to use a loop within the initial block to generate the stimulus programmatically. A for loop is perfect for iterating through all possible combinations of a set of inputs.

The same video you just watched demonstrates this improvement.

WRITING VERILOG TEST BENCHES

This next clip from "WRITING VERILOG TEST BENCHES" shows how to automate the stimulus generation for the same full adder.

Watch the section demonstrating the use of a for loop. Notice how it uses an integer i and concatenation {a,b,c} to efficiently apply all 8 possible input combinations. This is a much more scalable approach.


4. A Complete Example: Testbench for a 2:1 MUX

Let's consolidate everything we've learned by creating a testbench for a 2:1 multiplexer. In a previous module, you would have designed a MUX similar to the one below.

Design Under Test (DUT): mux_2_to_1.v

module mux_2_to_1 (
    input       a,
    input       b,
    input       sel,
    output      y
);

    assign y = sel ? b : a;

endmodule

Now, here is a complete testbench that instantiates this MUX and applies all possible input combinations using a for loop.

Testbench: tb_mux_2_to_1.v

`timescale 1ns / 1ps

module tb_mux_2_to_1();

    // 1. Declare signals to connect to the DUT
    // Inputs to DUT are 'reg'
    reg  tb_a;
    reg  tb_b;
    reg  tb_sel;

    // Outputs from DUT are 'wire'
    wire tb_y;

    // 2. Instantiate the Design Under Test (DUT)
    mux_2_to_1 dut (
        .a   (tb_a),
        .b   (tb_b),
        .sel (tb_sel),
        .y   (tb_y)
    );

    // 3. Apply stimulus sequence
    initial begin
        // Use $monitor to print values whenever a signal in the list changes
        $monitor("Time=%0t ns | sel=%b a=%b b=%b | y=%b", $time, tb_sel, tb_a, tb_b, tb_y);

        // Use a loop to generate all 8 input combinations
        // We can treat the inputs as a 3-bit vector {sel, b, a}
        for (integer i = 0; i < 8; i = i + 1) begin
            {tb_sel, tb_b, tb_a} = i;



            #10; // Wait 10 ns before applying the next input
        end




        #10; // Extra delay at the end
        $finish; // End the simulation
    end

endmodule

Analysis of the Testbench:

  • Signal Declaration: tb_a, tb_b, and tb_sel are reg types so the initial block can drive them. tb_y is a wire to observe the MUX's output.
  • Instantiation: The mux_2_to_1 module is instantiated with the name dut, and its ports are connected to our testbench signals.
  • Stimulus Block:
    • The initial block starts at time 0.
    • $monitor is a system task that prints the specified message whenever any of its arguments (tb_sel, tb_a, tb_b, tb_y) change value. This is extremely useful for debugging.
    • The for loop runs 8 times (for inputs).
    • In each iteration, the line {tb_sel, tb_b, tb_a} = i; uses concatenation to assign the 3 bits of the integer i directly to the three input signals.
    • The #10; delay ensures that each input combination is held for 10 ns before the next one is applied, allowing us to see the result clearly in a simulation waveform.
    • $finish is a system task that gracefully ends the simulation.

When you run this simulation, the $monitor output would look something like this:

Time=0 ns   | sel=0 a=0 b=0 | y=0
Time=10 ns  | sel=0 a=1 b=0 | y=1
Time=20 ns  | sel=0 a=0 b=1 | y=0
Time=30 ns  | sel=0 a=1 b=1 | y=1
Time=40 ns  | sel=1 a=0 b=0 | y=0
Time=50 ns  | sel=1 a=1 b=0 | y=0
Time=60 ns  | sel=1 a=0 b=1 | y=1
Time=70 ns  | sel=1 a=1 b=1 | y=1

Conclusion

In this lesson, you took the crucial first step from verification theory to practice. You now have the fundamental tools to create a test environment and drive your designs with controlled input sequences.

Key Takeaways:

  • A testbench is a non-synthesizable Verilog module used to simulate and verify a Design Under Test (DUT).
  • In a testbench, signals connected to DUT inputs are declared as reg, while signals connected to DUT outputs are declared as wire.
  • The initial block is used to define a sequence of actions that runs once at the beginning of a simulation.
  • The timescale directive and the # delay operator are used to control simulation timing, allowing you to apply inputs sequentially.
  • Using for loops within an initial block is an efficient and scalable way to generate a comprehensive set of input vectors.

So far, we have been manually checking the output on the console or in a waveform viewer. This is not efficient for large designs. In the next lesson, we will enhance our testbench to do this checking automatically. You will learn how to create a self-checking testbench that compares the DUT's output against a pre-defined expected result and reports a pass or fail status.