testbench

1. Generation of stimulus

For the testbench, the port should correspond one-to-one with the module to be tested. When the ports are divided into input, output and inout types to generate excitation signals, the port corresponding to input should be declared as reg, the port corresponding to output should be declared as wire, and the inout port is special, which will be explained below.

1) Direct assignment.

Generally, the initial block is used to assign an initial value to the signal. The initial block is executed once, and always or forever means that it is repeatedly executed by the event.

For example, a module

module exam();

reg rst_n;
reg clk;
reg data;

initial
begin
       clk=1'b0;
       rst=1'b1;
       #10
       rst=1'b0;
       #500
       rst=1'b1;
end

always
begin
       #10
            clk=~clk;
end 

You should notice that there is a # symbol, which means to delay the corresponding time unit. The time unit is determined by timescale. Generally, the time unit and simulation precision are defined at the beginning of the testbench, such as `timescale 1ns/1ps, the former one represents the time unit, and the latter one represents the simulation time precision. In the above example, one clock cycle is 20 units, which is 20ns. The concept of simulation time accuracy is that you can see the corresponding signal value at 1.001ns, but if the timescale is 1ns/1ns, the value at 1.001ns cannot be seen. For a design, the time scale should be unified. If the time scale in the design file and the testbench are inconsistent, the simulator defaults to the testbench. A better way is to write a global.v file and then use the include method to prevent this problem.

For repeated operations, it can be written as task and then called, such as

task load_count;
       input [3:0] load_value;
       begin
            @(negedge clk_50);
                     $display($time, " << Loading the counter with %h >>" , load_value);
            load_l = 1'b0;
            count_in = load_value;
            @(negedge clk_50);
            load_l = 1'b1;
       end
endtask //of load_count

initial 
begin
   load_count(4'hA); // call task
end


and other statements like forever, for, function, etc. are similar in usage. Although they may not be integrated, they are very convenient to use in testbench. Check the reference documents by yourself


2) File input
Sometimes , a large amount of data input is required, and it is cumbersome to directly assign values. You can generate data first, then read the data into the register, and take it out when needed. Use the $readmemb system task to read binary vectors (which can contain input stimulus and output expectations) from a text file. $readmemh is used to read hex files. For example:

reg [7:0] mem[1:256] // a 8-bit, 256-word define memory mem

initial $readmemh ( "E:/readhex/mem.dat", mem ) // convert the .dat file Read into the register mem

initial $readmemh ( "E:/readhex/mem.dat", mem, 128, 1 ) // The parameter is the address where the register is loaded with data is always

2. View the simulation results

For a simple module, you need to To see the waveform in the simulation window of modelsim, use the add wave .. command
For example, the top-level module of testbench is named tb. To see the clock signal, use add wave tb.clk.
To view all signals, use add wave /*
Of course, you can also right-click instance in the sim window under workspace To add waveforms

For  complex simulations, it is unavoidable to record waveforms and data to a file.

1) Wave file record There are generally two kinds of
common waveform files, vcd and fsdb, debussy is a good tool and supports fsdb, so it is best to use the combination of modelsim+debussy
By default, modelsim does not know fsdb, so it needs to be installed first debussy, and then generate the fsdb file. 


$dumpfile and $dumpvar are two system tasks in the verilog language, which can be called to create and import specified information into VCD files.
For fsdb files, the corresponding commands are fsdbDumpfile, dumpfsdbvars
(what is a VCD file Answer: The VCD file is an information record file that records the value changes of various signals during the simulation process of the design. The EDA tool displays the graphical simulation waveform by reading the VCD format file, so the VCD can be The file is simply regarded as a waveform record file.) The following describes their usage and exemplifies them respectively.

$dumpfile system task: Specify the file name for the VCD file to be created.
Example (the content after the "//" symbol is the comment text):
initial
$dumpfile ("myfile.dump"); //Specify the name of the VCD file as myfile.dump, the simulation information will be recorded to this file


$dumpvar system task: specify the signal to be recorded in the VCD file, you can specify a module level All the signals above, or a single signal can be specified individually.
The typical syntax is $dumpvar(level, module_name); the parameter level is an integer that specifies the number of layers, and the parameter module specifies the module to be recorded. The meaning of the whole sentence is that for the specified module, including the signals of each level below it (the number of levels is specified by level), it needs to be recorded in the VCD file.

Example:
initial
$dumpvar (0, top); //The specified level is 0, then all signals of the top module and the levels below it will be recorded

initial
$dumpvar (1, top); //Record the module instance top to the next The signal of the layer
// The number of layers is 1, that is, the signal at this level of the top module is recorded.
// For the deeper module instance called in the top module, its signal change is not recorded.

Initial
$dumpvar (2, top); // Record the signals of the two layers below the module instance top
//that is, the signals of the top module and its next layer will be recorded.

Assuming that module top contains the submodule module1, and we want to record the signals of the two layers below the top.module1 module, the syntax An example is as follows:
initial
$dumpvar (2, top.module1); //The signal of the module instance top.module1 and its next layer will be recorded.

Suppose module top contains the signals signal1 and signal2 (note that it is a variable instead of a submodule), if we want to record only For these two signals, the syntax example is as follows:
initial
$dumpvar (0, top.signal1, top.signal2); //Although the number of layers is specified, the number of layers does not affect the signal specified separately
//The number of layers is specified It has nothing to do with individually specified signals.

We can even specify all signals at some level and a single signal in the same $dumpvar call, assuming that the module top contains the signal signal1, and also contains the submodule module1. If we Not only want to record the independent signal signal1, but also want to record all the signals of the three layers below the sub-module module1, the syntax example is as follows:
initial
$dumpvar (3, top.signal1, top.module1); //Specify the number of layers and separate The specified signal is irrelevant
//so the level number 3 only affects the module top.module1, and has nothing to do with the signal

top.signal1 The

above example is equivalent to the following statement:
initial
begin
$dumpvar (0, top.signal1);
$ dumpvar (3, top.module1);
end

Special usage of $dumpvar (without any parameters):
initial
$dumpvar; //No parameters, indicating that all signals in the design will be recorded


Finally , we will use the two system tasks of $dumpfile and $dumpvar below In the comprehensive description of the example, suppose we have a design example named i_design, this design contains module module1, and there are many levels below module module1, we want to simulate this design, and simulate the module module1 and below in the simulation process The changes of all signals in all levels are recorded and stored in the VCD file named mydesign.dump, and the example is as follows:
initial
begin
$dumpfile ("mydesign.dump"); //Specify the VCD file name as mydesign.dump
$ dumpvar (0, i_design.module1); //Record all signals of the i_design.module1 module and all modules in the lower layers.
End


is similar to the generation of fsdb files.

Initial
      begin
         $fsdbDumpfile("tb_xxx.fsdb");
         $ fsdbDumpvars(0,tb_xxx);    
      end
2)文件输出结果

integer out_file;   // out_file 是一个文件描述,需要定义为 integer类型

out_file = $fopen ( " cpu.data " ); // cpu.data 是需要打开的文件,也就是最终的输出文本

设计中的信号值可以通过$fmonitor, $fdisplay,$fwrite

其中$fmonitor只要有变化就一直记录,$fdisplay和$fwrite需要触发条件才记录


例子:
initial begin
    $fmonitor(file_id, "%m: %t in1=%d o1=%h", $time, in1, o1);
end 
  

always@(a or b)
begin
    $fwrite(file_id,"At time%t a=%b b=%b",$realtime,a,b);
end



3 testbench的技巧

1).如果激励中有一些重复的项目,可以考虑将这些语句编写成一个task,这样会给书写和仿真带来很大方便。例如,一个存储器的testbench的激励可以包含write,read等task。

2).如果DUT中包含双向信号(inout),在编写testbench时要注意。需要一个reg变量来表示其输入,还需要一个wire变量表示其输出。

3).如果initial块语句过于复杂,可以考虑将其分为互补相干的几个部分,用数个initial块来描述。在仿真时,这些initial块会并发运行。这样方便阅读和修改。

4).每个testbench都最好包含$stop语句,用以指明仿真何时结束。
5).加载测试向量时,避免在时钟的上下沿变化,比如数据最好在时钟上升沿之前变化,这也符合建立时间的要求。


4.一个简单的例子

module counter (clk, reset, enable, count);

input clk, reset, enable;
output [3:0] count;
reg [3:0] count;                                  
  
always @ (posedge clk)
if (reset == 1'b1) begin
count <= 0;
end else if ( enable == 1'b1) begin
count <= count + 1;
end
  
endmodule 



testbench

module counter_tb; 
reg clk, reset, enable; 
wire [3:0] count; 
   
counter U0 ( 
.clk (clk), 
.reset   (reset), 
.enable (enable), 
.count   (count) 
); 
   
initial begin
   clk = 0; 
   reset = 0; 
   enable = 0; 
end 
   
always  
   #5   clk =   ! clk; 
   
initial   begin
   $dumpfile ("counter.vcd"); 
   $dumpvars; 
end 
   
initial   begin
   $display("\t\ttime,\tclk,\treset,\tenable,\tcount"); 
   $monitor("‰d,\t‰b,\t‰b,\t‰b,\t‰d",$time, clk,reset,enable,count); 
end 
   
initial 
    #100   $finish; 
   
//Rest of testbench code after this line 
   
   endmodule
5   双向端口
这个我没用过,完全是从网上google的,如果有问题,大家再讨论吧


芯片外部引脚很多都使用inout类型的,为的是节省管腿。一般信号线用做总线等双向数据传输的时候就要用到INOUT类型了。就是一个端口同时做输入和 输出。 inout在具体实现上一般用三态门来实现。三态门的第三个状态就是高阻'Z'。当inout端口不输出时,将三态门置高阻。这样信号就不会因为两端同时 输出而出错了,更详细的内容可以搜索一下三态门tri-state的资料.
1 使用inout类型数据,可以用如下写法:
inout data_inout;
input data_in;
reg data_reg;//data_inout的映象寄存器
reg link_data;
assign data_inout=link_data?data_reg:1’bz;//link_data控制三态门
//对于data_reg,可以通过组合逻辑或者时序逻辑根据data_in对其赋值.通过控制link_data的高低电平,从而设置data_inout是输出数据还是处于高阻态,如果处于高阻态,则此时当作输入端口使用.link_data可以通过相关电路来控制.
2 编写测试模块时,对于inout类型的端口,需要定义成wire类型变量,而其它输入端口都定义成reg类型,这两者是有区别的.
当上面例子中的data_inout用作输入时,需要赋值给data_inout,其余情况可以断开.此时可以用assign语句实现:assign data_inout=link?data_in_t:1’bz;其中的link ,data_in_t是reg类型变量,在测试模块中赋值.
另外,可以设置一个输出端口观察data_inout用作输出的情况:
Wire data_out;
Assign data_out_t=(!link)?data_inout:1’bz;

else,in RTL
inout use in top module(PAD)
dont use inout(tri) in sub module
也就是说,在内部模块最好不要出现inout,如果确实需要,那么用两个port实现,到顶层的时候再用三态实现。理由是:在非顶层模块用双向口的话,该 双向口必然有它的上层跟它相连。既然是双向口,则上层至少有一个输入口和一个输出口联到该双向口上,则发生两个内部输出单元连接到一起的情况出现,这样在 综合时往往会出错。

对双向口,我们可以将其理解为2个分量:一个输入分量,一个输出分量。另外还需要一个控制信号控制输出分量何时输出。此时,我们就可以很容易地对双向端口建模。

例子:
CODE:
module dual_port (
....
inout_pin,
....
);

inout inout_pin;

wire inout_pin;

wire input_of_inout;
wire output_of_inout;
wire out_en;

assign input_of_inout = inout_pin;

assign inout_pin = out_en ? output_of_inout : 高阻;

endmodule

可见,此时input_of_inout和output_of_inout就可以当作普通信号使用了。

在仿真的时候,需要注意双向口的处理。如果是直接与另外一个模块的双向口连接,那么只要保证一个模块在输出的时候,另外一个模块没有输出(处于高阻态)就可以了。
如果是在ModelSim中作为单独的模块仿真,那么在模块输出的时候,不能使用force命令将其设为高阻态,而是使用release命令将总线释放掉

很多初学者在写testbench进行仿真和验证的时候,被inout双向口难住了。仿真器老是提示错误不能进行。下面是我个人对inout端口写 testbench仿真的一些总结,并举例进行说明。在这里先要说明一下inout口在testbench中要定义为wire型变量。

先假设有一源代码为:

module xx(data_inout , ........);

inout data_inout;

........................

assign data_inout=(! link)?datareg:1'bz;

endmodule

方法一:使用相反控制信号inout口,等于两个模块之间用inout双向口互连。这种方法要注意assign 语句只能放在initial和always块内。

module test();

wire data_inout;

reg data_reg;

reg link;

initial begin

..........

end

assign data_inout=link?data_reg:1'bz;

endmodule

方法二:使用force和release语句,但这种方法不能准确反映双向端口的信号变化,但这种方法可以反在块内。

module test();

wire data_inout;

reg data_reg;

reg link;

#xx;        //延时

force data_inout=1'bx;           //强制作为输入端口

...............

#xx;

release data_inout;       //释放输入端口

endmodule

很多读者反映仿真双向端口的时候遇到困难,这里介绍一下双向端口的仿真方法。一个典型的双向端口如图1所示。

其中inner_port与芯片内部其他逻辑相连,outer_port为芯片外部管脚,out_en用于控制双向端口的方向,out_en为1时,端口为输出方向,out_en为0时,端口为输入方向。

用Verilog语言描述如下:
module bidirection_io(inner_port,out_en,outer_port);
input out_en;
inout[7:0] inner_port;
inout[7:0] outer_port;
assign outer_port=(out_en==1)?inner_port:8'hzz;
assign inner_port=(out_en==0)?outer_port:8'hzz;
endmodule

用VHDL语言描述双向端口如下:
library ieee;
use IEEE.STD_LOGIC_1164.ALL;
entity bidirection_io is
port ( inner_port : inout std_logic_vector(7 downto 0);
out_en : in std_logic;
outer_port : inout std_logic_vector(7 downto 0) );
end bidirection_io;
architecture behavioral of bidirection_io is
begin
outer_port<=inner_port when out_en='1' else (OTHERS=>'Z');
inner_port<=outer_port when out_en='0' else (OTHERS=>'Z');
end behavioral;

仿真时需要验证双向端口能正确输出数据,以及正确读入数据,因此需要驱动out_en端口,当out_en端口为1时,testbench驱动 inner_port端口,然后检查outer_port端口输出的数据是否正确;当out_en端口为0时,testbench驱动 outer_port端口,然后检查inner_port端口读入的数据是否正确。由于inner_port和outer_port端口都是双向端口(在 VHDL和Verilog语言中都用inout定义),因此驱动方法与单向端口有所不同。
验证该双向端口的testbench结构如图2所示。

这是一个self-checking testbench,可以自动检查仿真结果是否正确,并在Modelsim控制台上打印出提示信息。图中Monitor完成信号采样、结果自动比较的功能。
testbench的工作过程为
1)out_en=1时,双向端口处于输出状态,testbench给inner_port_tb_reg信号赋值,然后读取outer_port_tb_wire的值,如果两者一致,双向端口工作正常。
2)out_en=0时,双向端口处于输如状态,testbench给outer_port_tb_reg信号赋值,然后读取inner_port_tb_wire的值,如果两者一致,双向端口工作正常。

用Verilog代码编写的testbench如下,其中使用了自动结果比较,随机化激励产生等技术。

`timescale 1ns/10ps
module tb();
reg[7:0] inner_port_tb_reg;
wire[7:0] inner_port_tb_wire;
reg[7:0] outer_port_tb_reg;
wire[7:0] outer_port_tb_wire;
reg out_en_tb;
integer i;

initial
begin
out_en_tb=0;
inner_port_tb_reg=0;
outer_port_tb_reg=0;
i=0;
repeat(20)
begin
#50
i=$random;
out_en_tb=i[0]; //randomize out_en_tb
inner_port_tb_reg=$random; //randomize data
outer_port_tb_reg=$random;
end
end

//**** drive the ports connecting to bidirction_io
assign inner_port_tb_wire=(out_en_tb==1)?inner_port_tb_reg:8'hzz;
assign outer_port_tb_wire=(out_en_tb==0)?outer_port_tb_reg:8'hzz;

//instatiate the bidirction_io module
bidirection_io bidirection_io_inst(.inner_port(inner_port_tb_wire),
.out_en(out_en_tb),
.outer_port(outer_port_tb_wire));

//***** monitor ******
always@(out_en_tb,inner_port_tb_wire,outer_port_tb_wire)
begin
#1;
if(outer_port_tb_wire===inner_port_tb_wire)
begin
$display("\n **** time=%t ****",$time);
$display("OK! out_en=%d",out_en_tb);
$display("OK! outer_port_tb_wire=%d,inner_port_tb_wire=%d",
outer_port_tb_wire,inner_port_tb_wire);
end
else
begin
$display("\n **** time=%t ****",$time);
$display("ERROR! out_en=%d",out_en_tb);
$display("ERROR! outer_port_tb_wire != inner_port_tb_wire" );
$display("ERROR! outer_port_tb_wire=%d, inner_port_tb_wire=%d",
outer_port_tb_wire,inner_port_tb_wire);
end
end
endmodule


6. 高级用法
比如pli之类的东西,我也没用过。。。有需要的,大家再讨论

   总体感觉,testbench是个很难的事情,这里讨论的只是一些最基本的东西。真正有技术含量的是testcase的设计,设计阶段合理层次设计以及模 块划分等等,我没有做过很大的项目,所以这方面也没有办法提供更多的帮助。经验丰富的大牛不妨出来讲讲经验,^_^

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325393294&siteId=291194637