9. Testbenches

9.1. Introduction

In previous chapters, we generated the simulation waveforms using modelsim, by providing the input signal values manually; if the number of input signals are very large and/or we have to perform simulation several times, then this process can be quite complex, time consuming and irritating. Suppose input is of 10 bit, and we want to test all the possible values of input i.e. \(2^{10}-1\), then it is impossible to do it manually. In such cases, testbenches are very useful; also, the tested designs are more reliable and prefer by the clients as well. Further, with the help of testbenches, we can generate results in the form of csv (comma separated file), which can be used by other softwares for further analysis e.g. Python, Excel and Matlab etc.

Since testbenches are used for simulation purpose only (not for synthesis), therefore full range of Verilog constructs can be used e.g. keywords ‘for’, ‘display’ and ‘monitor’ etc. can be used for writing testbenches.

Important

Modelsim-project is created in this chapter for simulations, which allows the relative path to the files with respect to project directory as shown in Section 9.3.1. Simulation can be run without creating the project, but we need to provide the full path of the files as shown in Line 25 of Listing 9.4.

Lastly, mixed modeling is not supported by Altera-Modelsim-starter version, i.e. Verilog designs with VHDL and vice-versa can not be compiled in this version of Modelsim. For mixed modeling, we can use Active-HDL software as discussed in Chapter Section 2.

9.2. Testbench for combinational circuits

In this section, various testbenches for combinational circuits are shown, whereas testbenches for sequential circuits are discussed in next section. For simplicity of the codes and better understanding, a simple half adder circuit is tested using various simulation methods.

9.2.1. Half adder

Listing 9.1 shows the Verilog code for the half adder which is tested using different methods,

Listing 9.1 Half adder
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// half_adder.v

module half_adder
(
    input wire a, b,
    output wire sum, carry
);

assign sum = a ^ b;
assign carry = a & b;

endmodule

9.3. Testbench with ‘initial block’

Note that, testbenches are written in separate Verilog files as shown in Listing 9.2. Simplest way to write a testbench, is to invoke the ‘design for testing’ in the testbench and provide all the input values inside the ‘initial block’, as explained below,

Explanation Listing 9.2

In this listing, a testbench with name ‘half_adder_tb’ is defined at Line 5. Note that, ports of the testbench is always empty i.e. no inputs or outputs are defined in the definition (see Line 5). Then 4 signals are defined i.e. a, b, sum and carry (Lines 7-8); these signals are then connected to actual half adder design using structural modeling (see Line 13). Lastly, different values are assigned to input signals e.g. ‘a’ and ‘b’ at lines 18 and 19 respectively.

Note

‘Initial block’ is used at Line 15, which is executed only once, and terminated when the last line of the block executed i.e. Line 32. Hence, when we press run-all button in Fig. 9.1, then simulation terminated after 60 ns (i.e. does not run forever).

In Line 19, value of ‘b’ is 0, then it changes to ‘1’ at Line 23, after a delay of ‘period’ defined at Line 20. The value of period is ‘20 (Line 11) * timescale (Line 3) = 20 ns’. In this listing all the combinations of inputs are defined manually i.e. 00, 01, 10 and 11; and the results are shown in Fig. 9.1, also corresponding outputs i.e. sum and carry are shown in the figure.

Note

To generate the waveform, first compile the ‘half_adder.v and then ‘half_adder_tb.v’ (or compile both the file simultaneously.). Then simulate the half_adder_tb.v file. Finally, click on ‘run all’ button (which will run the simulation to maximum time i.e. 80 ns)and then click then ‘zoom full’ button (to fit the waveform on the screen), as shown in Fig. 9.1.

Listing 9.2 Simple testbench for half adder
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// half_adder_tb.v

`timescale 1 ns/10 ps  // time-unit = 1 ns, precision = 10 ps

module half_adder_tb;

    reg a, b;
    wire sum, carry;

    // duration for each bit = 20 * timescale = 20 * 1 ns  = 20ns
    localparam period = 20;  

    half_adder UUT (.a(a), .b(b), .sum(sum), .carry(carry));
    
    initial // initial block executes only once
        begin
            // values for a and b
            a = 0;
            b = 0;
            #period; // wait for period 

            a = 0;
            b = 1;
            #period;

            a = 1;
            b = 0;
            #period;

            a = 1;
            b = 1;
            #period;
        end
endmodule
../_images/half_adder_tb.jpg

Fig. 9.1 Simulation results for Listing 9.2

Testbench with ‘always’ block

In Listing 9.3, ‘always’ statement is used in the testbench; which includes the input values along with the corresponding output values. If the specified outputs are not matched with the output generated by half-adder, then errors will be displayed.

Note

  • In the testbenches, the ‘always’ statement can be written with or without the sensitivity list as shown in Listing 9.3.
  • Unlike ‘initial’ block, the ‘always’ block executes forever (if not terminated using ‘stop’ keyword). The statements inside the ‘always’ block execute sequentially; and after the execution of last statement, the execution begins again from the first statement of the ‘always’ block.

Explanation Listing 9.3

The listing is same as previous Listing 9.2, but ‘always block’ is used instead of ‘initial block’, therefore we can provide the sensitive list to the design (Line 28) and gain more control over the testing. A continuous clock is generated in Lines 19-26 by not defining the sensitive list to always-block (Line 19). This clock is used by Line 28. Also, some messages are also displayed if the outcome of the design does not match with the desire outcomes (Lines 35-36). In this way, we can find errors just by reading the terminal (see Fig. 9.2), instead of visualizing the whole waveform, which can be very difficult if the results are too long (see Fig. 9.3). Also, Lines 35-36 can be added in ‘initial-block’ of :numref:`verilog_half_adder_tb_v` as well.

Listing 9.3 Testbench with procedural statement
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// half_adder_procedural_tb.v

`timescale 1 ns/10 ps  // time-unit = 1 ns, precision = 10 ps

module half_adder_procedural_tb;

    reg a, b;
    wire sum, carry;

    // duration for each bit = 20 * timescale = 20 * 1 ns  = 20ns
    localparam period = 20;  

    half_adder UUT (.a(a), .b(b), .sum(sum), .carry(carry));
reg clk;

// note that sensitive list is omitted in always block
// therefore always-block run forever
// clock period = 2 ns
always 
begin
    clk = 1'b1; 
    #20; // high for 20 * timescale = 20 ns

    clk = 1'b0;
    #20; // low for 20 * timescale = 20 ns
end

always @(posedge clk)
begin
    // values for a and b
    a = 0;
    b = 0;
    #period; // wait for period
    // display message if output not matched
    if(sum != 0 || carry != 0)  
        $display("test failed for input combination 00");

    a = 0;
    b = 1;
    #period; // wait for period 
    if(sum != 1 || carry != 0)
        $display("test failed for input combination 01");

    a = 1;
    b = 0;
    #period; // wait for period 
    if(sum != 1 || carry != 0)
        $display("test failed for input combination 10");

    a = 1;
    b = 1;
    #period; // wait for period 
    if(sum != 0 || carry != 1)
        $display("test failed for input combination 11");

    a = 0;
    b = 1;
    #period; // wait for period 
    if(sum != 1 || carry != 1)
        $display("test failed for input combination 01");

    $stop;   // end of simulation
end
endmodule
../_images/half_adder_process_error_tb.jpg

Fig. 9.2 Error generated by Listing 9.3

../_images/half_adder_process_tb.jpg

Fig. 9.3 Simulation results for Listing 9.3

9.3.1. Read data from file

In this section, data is read from file ‘read_file_ex.txt’ and displayed in simulation results. Date stored in the file is shown in Fig. 9.4.

../_images/read_file_table_ex.jpg

Fig. 9.4 Data in file ‘read_file_ex.txt’

Explanation Listing 9.4

In the listing, ‘integer i (Line 16)’ is used in the ‘for loop (Line 28)’ to read all the lines of file ‘read_file_ex.txt’. Data can be saved in ‘binary’ or ‘hexadecimal format’. Since data is saved in ‘binary format’, therefor ‘readmemb’ (Line 23) is used to read it. For ‘hexadecimal format’, we need to use keyword ‘readmemh’. Read comments for further details of the listing. Data read by the listing is displayed in Fig. 9.5.

Listing 9.4 Read data from file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// read_file_ex.v
// note that, we need to create Modelsim project to run this file,
// or provide full path to the input-file i.e. adder_data.txt  

`timescale 1 ns/10 ps  // time-unit = 1 ns, precision = 10 ps

module read_file_ex;
    
    reg a, b;
    // sum_expected, carry_expected are merged together for understanding
    reg[1:0] sum_carry_expected; 

    // [3:0] = 4 bit data
    // [0:5] = 6 rows  in the file adder_data.txt
    reg[3:0] read_data [0:5];
    integer i;

    initial
    begin 
        // readmemb = read the binary values from the file
        // other option is 'readmemh' for reading hex values
        // create Modelsim project to use relative path with respect to project directory
        $readmemb("input_output_files/adder_data.txt", read_data);
        // or provide the compelete path as below
        // $readmemb("D:/Testbences/input_output_files/adder_data.txt", read_data);

        // total number of lines in adder_data.txt = 6
        for (i=0; i<6; i=i+1)
        begin
            // 0_1_0_1 and 0101 are read in the same way, i.e.
            //a=0, b=1, sum_expected=0, carry_expected=0 for above line;
            // but use of underscore makes the values more readable.
            {a, b, sum_carry_expected} = read_data[i]; // use this or below
            // {a, b, sum_carry_expected[0], sum_carry_expected[1]} = read_data[i];
            #20;  // wait for 20 clock cycle
        end
    end
endmodule
../_images/read_file_ex.jpg

Fig. 9.5 Simulation results of Listing 9.4

9.3.2. Write data to file

In this part, different types of values are defined in Listing 9.5 and then stored in the file.

Explanation Listing 9.5

To write the data to the file, first we need to define an ‘integer’ as shown in Line 14, which will work as buffer for open-file (see Line 28). Then data is written in the files using ‘fdisplay’ command, and rest of the code is same as Listing 9.4.

Listing 9.5 Write data to file
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// write_file_ex.v
// note that, we need to create Modelsim project to run this file,
// or provide full path to the input-file i.e. adder_data.txt  

`timescale 1 ns/10 ps  // time-unit = 1 ns, precision = 10 ps

module write_file_ex;
    
    reg a, b, sum_expected, carry_expected;
    // [3:0] = 4 bit data
    // [0:5] = 6 rows  in the file adder_data.txt
    reg[3:0] read_data [0:5];

    integer write_data;
    integer i;

    initial
    begin 

        // readmemb = read the binary values from the file
        // other option is 'readmemh' for reading hex values
        // create Modelsim project to use relative path with respect to project directory
        $readmemb("input_output_files/adder_data.txt", read_data);
        // or provide the compelete path as below
        // $readmemb("D:/Testbences/input_output_files/adder_data.txt", read_data);

        // write data : provide full path or create project as above
        write_data = $fopen("input_output_files/write_file_ex.txt");
        
        for (i=0; i<6; i=i+1)
        begin
            {a, b, sum_expected, carry_expected} = read_data[i];
            #20;

            // write data to file using 'fdisplay'
            $fdisplay(write_data, "%b_%b_%b_%b", a, b, sum_expected, carry_expected);
        end

        $fclose(write_data);  // close the file
    end


endmodule
../_images/write_file_table_ex.jpg

Fig. 9.6 Data in file ‘write_file_ex.txt’

9.4. Testbench for sequential designs

In previous sections, we read the lines from one file and then saved those line in other file using ‘for loop’. Also, we saw use of ‘initial’ and ‘always’ blocks. In this section, we will combine all the techniques together to save the results of Mod-M counter, which is an example of ‘sequential design’. The Mod-m counter is discussed in Listing 6.4. Testbench for this listing is shown in Listing 9.6 and the waveforms are illustrated in Fig. 9.7.

Explanation Listing 9.6

In the testbench following operations are performed,

  • Simulation data is saved on the terminal and saved in a file. Note that, ‘monitor/fmonitor’ keyword can be used in the ‘initial’ block, which can track the changes in the signals as shown in Lines 98-108 and the output is shown in Fig. 9.8. In the figure we can see that the ‘error’ message is displayed two time (but the error is at one place only), as ‘monitor’ command checks for the transition in the signal as well.
  • If we want to track only final results, then ‘display/fdisplay’ command can be used inside the always block as shown in Lines 110-124 (Uncomment these lines and comment Lines 98-108). Output is show in Fig. 9.9. Note that, it is very easy to look for errors in the terminal/csv file as compare to finding it in the waveforms.
  • Simulation is stopped after a fixed number of clock-cycle using ‘if statement’ at Line 76-84. This method can be extended to stop the simulation after certain condition is reached.

Note

Notice the use of ‘negedge’ in the code at Lines 89 and 117, to compare the result saved in file ‘mod_m_counter_desired.txt’ (Fig. 9.10) with result obtained by ‘modMCounter.v’ (Line 32). For more details, please read the comments in the code.

../_images/monitor_waveform.jpg

Fig. 9.7 Waveform for Listing 9.6

../_images/monitor_error.jpg

Fig. 9.8 Data displayed using ‘initial block’ and ‘monitor’ (Lines 98-108 of Listing 9.6)

../_images/error_neg_edge_csv.jpg

Fig. 9.9 Data saved in .csv file using ‘always block’ and ‘fdisplay’ (Lines 110-124 of Listing 9.6)

../_images/content_mod_m_desire.jpg

Fig. 9.10 Content of file ‘mod_m_counter_desired.txt’

Listing 9.6 Save data of simulation
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// modMCounter_tb.v

// note that the counter starts the count from 1 after reset (not from 0),
// therefore file "mod_m_counter_desired.txt" starts with 1 (not from 0), 
// also one entry in the file is incorrect i.e. Line 10, where '1' is written
// instead of 'a'. 

`timescale 1 ns/10 ps  // time-unit = 1 ns, precision = 10 ps

module modMCounter_tb;

localparam M = 12, N=4, period=20;

reg clk, reset;
wire complete_tick; 

// desired_count is read from file
// count is count provided by modMCounter.v
wire[N-1:0] count;
reg[N-1:0] desired_count;

reg[39:0] error_msg; // message = error

// [3:0] = 4 bit data
// [0:M-1] = 12 rows  in the file mod_m_counter_desired.txt
reg[3:0] read_data [0:M-1];

integer counter_data; // for saving counter-data on file
integer i  = 0, j = 0, total_cycle = M; // used for ending the simulation after M cycle

// unit under test
modMCounter #(.M(M), .N(N)) UUT (.clk(clk), .reset(reset), .complete_tick(complete_tick), .count(count));

// read the data from file
always @(posedge clk)
begin 
    $readmemh("input_output_files/mod_m_counter_desired.txt", read_data);
    if (reset)
        desired_count = 0;
    else 
    begin     
            desired_count = read_data[j];
            j = j+1;
    end    
end

// open csv-file for writing
initial
begin
	counter_data = $fopen("input_output_files/counter_output.csv"); // open file
end

// note that sensitive list is omitted in always block
// therefore always-block run forever
// clock period = 2 ns
always 
begin
    clk = 1'b1; 
    #20; // high for 20 * timescale = 20 ns

    clk = 1'b0;
    #20; // low for 20 * timescale = 20 ns
end

// reset
initial 
begin 
	reset = 1;
	#(period);
	reset = 0;
end


// stop the simulation total_cycle and close the file
// i.e. store only total_cycle values in file
always @(posedge clk)
begin
    if (total_cycle == i) 
    begin
    	$stop;
    	$fclose(counter_data);  // close the file
	end	
    i = i+1;
end

// note that, the comparison is made at negative edge, 
// when all the transition are settled. 
// if we use 'posedge', then result will not be in correct form
always @(negedge clk)
begin
    if (desired_count == count)
        error_msg = "    ";
    else 
        error_msg = "error"; 
end

// print the values on terminal and file
initial
begin
    // write on terminal
    $display("    time, desired_count, count, complete_tick, error_msg");
    // monitors checks and print the transitions
    $monitor("%6d, %10b, %7x, %5b,  %15s", $time, desired_count, count, complete_tick,  error_msg);
    
    // write on the file
    $fdisplay(counter_data, "time, desired_count, count, complete_tick, error_msg");
    $fmonitor(counter_data, "%d,%b,%x,%b,%s", $time, desired_count, count, complete_tick,  error_msg);
end

// // print the values on terminal and file
// // header line
// initial
// begin
//     $fdisplay(counter_data,"time,desired_count,count,complete_tick,error_msg");
// end
// // negative edge is used here, as error values are updated on negedge
// always @(negedge clk)
// begin
//     // write on terminal
//     $display("%6d, %10b, %7x, %5b,  %15s", $time, desired_count, count, complete_tick,  error_msg);
    
//     // write on the file
//     $fdisplay(counter_data, "%d, %d, %x, %b,  %s", $time, desired_count, count, complete_tick,  error_msg);
// end

endmodule

9.5. Conclusion

In this chapter, we learn to write testbenches with different styles for combinational circuits and sequential circuits. We saw the methods by which inputs can be read from the file and the outputs can be written in the file. Simulation results and expected results are compared and saved in the csv file and displayed as simulation waveforms; which demonstrated that locating the errors in csv files is easier than the simulation waveforms.