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:
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
The testbench file designed for it can be:
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
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:
'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
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:
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:
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
(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:
`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