Testbench file writing minutes (Verilog)

Follow, star public account, wonderful content delivered every day
Source: Internet material

Before using Verilog for FPGA projects and other occasions, I wrote some testbench files for hands-on practice. When I first wrote a few times, I didn’t remember some basic things every time. Not proficient, I was a little proficient when I wrote it later, but the overall writing is fragmented and unsystematic, so here is a brief record of the main points of writing the testbench file that needs to be used when testing small verilog modules under normal circumstances.

This article mainly refers to the relevant content in the “A Verilog HDL Test Bench Primer” manual of Lattice Company found on the Internet. Thanks!

Module instantiation, reg & amp; wire declaration, use of initial and always blocks

The module (Verilog-module) that needs to be tested is called DUT (Design Under Test), and one or more DUTs need to be instantiated in the testbench.

Top-level modules in Testbench do not need to define inputs and outputs.

In Testbench, the input connected to the DUT instance is the reg type, and the output connected to the DUT instance is the wire type.

For the inout type variable of DUT, it is necessary to use reg and wire type variables to call in testbench respectively.

For example, for the following module to be tested:

744cfc97fe9494ef7008bca92339ee21.jpeg

module bidir_infer (DATA, READ_WRITE);
input READ_WRITE;
inout [1:0] DATA;
reg[1:0] LATCH_OUT;

always @ (READ_WRITE or DATA) begin
    if (READ_WRITE == 1)
        LATCH_OUT <= DATA;
end

assign DATA = (READ_WRITE == 1) ? 2'bZ : LATCH_OUT;

endmodule

aa9f84333815cf8e86ccf0f677bd4f2c.jpeg

The testbench file designed for it can be:

d73744fb3e4a4e9356ebdcbd1f1bdbbc.jpeg

module test_bidir_ver;
reg read_writet;
reg [1:0] data_in;
wire [1:0] datat, data_out;
bidir_infer uut(datat, read_writet);

assign datat = (read_writet == 1) ? data_in : 2'bZ;
assign data_out = (read_writet == 0) ? datat : 2'bZ;

initial begin
read_write = 1;
data_in = 11;
#50 read_writet = 0;
end

endmodule

2b7da743e17faa62b419dea73c62b7d4.jpeg

As in the normal Verilog module, use assign to assign values to variables of type wire.

One thing to note is that for variables that are not assigned an initial value in the code, the wire type variable is initialized to Z, and the reg type variable is initialized to X.

always and initial are two serial control blocks that operate on reg variables. Each initial and always block will start running at the same time when the simulation starts.

Commonly, they can be used to generate the clock and reset signals required by the module, as follows:

7255925d81b97b9c9e2f58511dfa4406.jpeg

'timescale 1 ns / 100 ps

reg clk_50, rst_l;

initial
begin
$display($time, " << Starting the Simulation >>");
clk_50 = 1'b0; // at time 0
rst_l = 0; // reset is active
#20 rst_l = 1’b1; // at time 20 release reset
end

always
#10 clk_50 = ~clk_50; // every ten nanoseconds invert

db47477beca41d59d090869efbca0c30.jpeg

The first line defines the time unit/time precision. The time unit is 1ns, so the generated clk_50 clock cycle is 20ns, that is, the frequency is 50MHz.

The reset signal rst_l is in the reset state of 0 initially, and is 1 after 20 ns to release the reset.

Stop, variable monitoring and output in simulation

There are two simulation control functions: $finish and $stop. Among them, the $finish task is used to terminate the simulation and jump out of the emulator; the $stop task is used to terminate the simulation. In Modelsim, the $stop task is to return to the interactive mode.

If you need to monitor the change of a variable in the simulation, you can use the $monitor function:

$monitor($time, " clk_50=%b, rst_l=%b, enable_l=%b, load_l=%b, count_in=%h, cnt_out=%h, oe_l=%b, count_tri=%h" , clk_50, rst_l, enable_l, load_l, count_in, cnt_out, oe_l, count_tri);

Produces output whenever any variable in the variable list changes.

If you need to print the output on the emulated console screen, you can use the $display function:

$display($time, "<< count = %d - Turning OFF count enable >>",cnt_out);

Usage of task

A group of repetitive or related commands can be grouped together to form a task.

Tasks can usually be called in initial or always blocks.

A task can have inputs, outputs, and inouts, and can also contain timing or delay elements.

Take, for example, a simple SPI interface implemented on an FPGA. The external device is the master and the FPGA is the slave. The command is 32 bits in total, which is composed of “1-bit read and write command word (1 read and 0 write) + 14-bit address + 1-bit NO CARE + 16-bit data”, and communicate after the chip select signal is pulled low At the beginning, the timing is as follows:

bc20994f1ebfa703918c87d4f2b85ccb.png

When the data flow is from the peripheral to the FPGA (FPGA is receiving), the peripheral updates MOSI on the falling edge of SCLK; FPGA captures the value on MOSI to the shift register on the rising edge of SCLK.

When the FPGA is the sender, the FPGA updates the output on the MISO line on the falling edge of SCLK, and the peripheral device captures the value on MISO on the rising edge of SCLK.

Peripherals can access registers generated inside the FPGA through this SPI interface.

When performing a read test on the spi module on the FPGA, the read command sent by the peripheral to the FPGA is:

{1’b1, address, 1’b0, data (read 16bit data)}

A task spi_read written for this could be:

5facf3ab9640e908bf3f1b7896783d6e.jpeg

77271f5093173e1ab56dee3540f3e08c.jpeg

task spi_read;
input[13:0] address;
output[15:0]data;
reg [31:0] output_register;
reg [15:0] input_register;
integer i;
    begin
       $display("time:%t----------------task spi_read", $time );
            #100;
            spi_clk = 1'b0;
            spi_csn = 1'b1;
            spi_mosi = 1'b0;
            output_register = {1'b1,address,1'b0,16'd0};
            
            $display("time:%t,testbench read output_register: %h,",$time,output_register );
            $display("time:%t,testbench read address: %h",$time,address );
            
            spi_csn = 1'b1;
            for(i = 0 ; i < 16 ; i=i + 1)
                begin
                    spi_csn = 1'b0;
                    spi_clk = 1'b0;
                    spi_mosi = output_register[31-i];
                    #100;
                    spi_clk = 1'b1;
                    #100;
                end
                
            for(i = 0 ; i < 16 ; i=i + 1)
                begin
                    spi_csn = 1'b0;
                    spi_clk = 1'b0;
                    #100;
                    spi_clk = 1'b1;
                    input_register[15-i] = spi_miso;
                    #100;
                end
            spi_csn = 1'b1;
            
            
            data = input_register;
            $display("time:%t,testbench spi_read read data: %h,",$time,input_register );
            
            $display("time:%t----------------",$time );
            #100;
    end

endtask

a4ff3175a482ece39ad854df0356121a.jpeg

(The time unit of the simulation is 1ns, and the spi clock frequency is 10MHz)

Example and summary

According to the foregoing, self-summary generally simple testbench file structure can be as follows:

f7ba438eeca06cac34f36e892c922d34.jpeg

`timescale 1 ns / 1 ns

module testbench_module_top;
reg
reg
...
wire
wire
...

//reset and clock definition
initial begin … end
initial begin … end

//actual testing flows
initial
begin
//variables initialization
a =
b =
…

task_1(var_1...var_N)
…
task_N(var_1... var_N)
$stop;
…end


//dut module instance
module_top U1
(
.var1(),
.var2(),
…
.varN()
)

//necessary control logic for testbench module test flow
always@(...)

//tasks definition
task task_1;
input...;
output...;
...
//action flow
...
end task

...

task task_N;
...
end task

endmodule