Configurable Counter Waveform

Test Bench for Verilog Behavioral Simulation

I will confess that I’ve been withholding some important information from you. I’ve touched upon the subject in several previous posts like the Verilog Introduction, or D-type Flip Flop. And if you’ve been trying to follow along using the source code from github you’ve surely caught me. I am surprised that no one has commented on it so far!

The important information that I’ve withheld has to do with how I setup and run the Behavioral Simulations in Vivado. I’ve shown pictures of the waveform output. But I haven’t yet talked about how you go about it. The key to running a simulation is to create a special kind of Verilog file called a test bench.

Why use a test bench?

There are a few reasons why using a test bench is a good idea. First, running a simulation is faster than a complete synthesis and deployment to a device. So it is more productive to iterate on a design using a test bench. The second reason is that you can be sure to test, and more importantly re-rest, for every input and output combination. Test benches help automate comprehensive testing. And finally, a test bench helps you test even if you do not have a peripheral or even an FPGA board.

So what does a test bench look like?

Below I’ve pasted the test bench code that I used with the last project, the configurable counter. The first part of the code has something new, the localparam keyword. A localparam defines a named constant that you can use throughout your module. The next new thing you will notice is where we instantiate a counter circuit for testing. See line 16 below. This is called a structural description, and also sometimes called module instantiation. The format of a module instantiation is as follows:

module_name #(parameters) instance_name(input_parameters);

Let’s break it down. The module name is the name we chose for our configurable counter module, which is counter_n. The parameters are optional, if you use them you may specify any of the configurable parameters that counter_n defines. In our case counter_n defines only one parameter, the number of bits in the counter and we called that parameter BITS. Note that when you specify the parameter you define the parameter you are providing, the .BITS() part, and the value you are passing for that parameter, which we defined as BITS in the localparam. Taken together this looks like .BITS(BITS).

Next, I named the instance we created mycounter. And then we connect input and output signals from the device to signals in our test bench. For consistency I named the test bench signals to be the same as the signal names used by counter_n. Thus the input parameters look like .clk(clk) and so on, which says that the test bench signal clk is connected to the .clk() input. Note that you are not required to make connections to every input.

// simple test bench for the configurable counter
module counter_n_test();

    // define our constants
    // the clock period is 20ns
    localparam T = 20;

    // our counter will have 3 bits
    localparam BITS = 3;
    
    reg clk, rst;
    wire [BITS - 1:0] q;
    wire tick;
    
    // instantiate our counter for testing
    counter_n #(.BITS(BITS)) mycounter(.clk(clk), .rst(rst), .q(q), .tick(tick));
    
    // generate clock signal    
    always
    begin
        clk = 1'b1;
        #(T/2);
        clk = 1'b0;
        #(T/2);
    end

    // reset the circuit for first half clock cycle
    initial
    begin
        rst = 1'b1;
        #(T/2);
        rst = 1'b0;
    end
    
    initial
    begin
        // wait for reset to end
        @(negedge rst);

        repeat(3) @(negedge clk);

        // wait for next clock
        @(negedge clk);
        rst = 1'b1;
        #(T/2);
        rst = 1'b0;

        repeat(2 ** BITS) @(negedge clk);
                
        $stop; 
    end
endmodule

The next interesting thins is the always block that generates the clk signal. This code starts at line 19. In this always block we raise the clk signal by setting it to 1’b1. Then we wait half of a clock cycle #(T/2), set the clk signal to 1’b0 and then wait for another half clock cycle. This then repeats forever.

After the always block, starting at line 28 is an initial block which is another new keyword. The initial block defines code that starts running at the start of simulation. If you looked ahead you may have already noted that I have two initial blocks, which is not uncommon. The first initial block performs a reset on the counter at the start by setting rst to 1, waiting half a clock cycle, then setting rst to 0.

The second initial block, starting at line 35 waits for the reset to complete by looking for the negative edge (via the negedge sensitivity) of the rst signal. Then the code at line 40 waits for 3 full clock cycles to complete. It does this using the repeat(3) statement which waits until it has seen the negative edge of the clk signal three times. This allows our counter time to count upwards.

Next, the code starting at line 43 performs another reset of the counter to clear the count. And then finally the last repeat statement at line 48 waits for 2 ^ BITS (8) clock cycles to complete so that we can observe the counter raise the tick signal at 2 ^ BITS – 1 (7). I’ll show the resulting simulation waveform again below. And at that point the $stop command causes the simulation to end.

configurable counter waveform

I’ve only scratched the surface of what can be done. There is a lot more that you can do with a test bench. But that’s enough for now. The source code for the test bench is included with the counter_n project and is available on github. If you have comments, questions or suggestions, please leave me a comment!

Discover more from FPGA Coding

Subscribe now to keep reading and get access to the full archive.

Continue reading