• Testbench文件编写纪要(Verilog)


    之前在使用Verilog做FPGA项目中、以及其他一些不同的场合下,零散的写过一些练手性质的testbench文件,开始几次写的时候,每次都会因为一些基本的东西没记住、写的很不熟练,后面写的时候稍微熟练了一点、但是整体编写下来比较零碎不成体系,所以在这里简要记录一下一般情况下、针对小型的verilog模块进行测试时所需要使用到的testbench文件的编写要点。

    本文主要参考了在网上找到的Lattice公司的“A Verilog HDL Test Bench Primer”手册中的有关内容。谢谢!

    模块实例化、reg&wire声明、initial和always块的使用

    需要测试的模块(Verilog-module)被称为DUT(Design Under Test),在testbench中需要对一个或者多个DUT进行实例化。

    Testbench中的顶层module不需要定义输入和输出。

    Testbench中连接到DUT instance的输入的为reg类型、连接到DUT instance的输出的为wire类型。

    对于DUT的inout类型变量,在testbench中需要分别使用reg、wire类型的变量进行调用。

    例如,对于下面这样一个待测试module:

    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

    为其设计的testbench文件可以是:

    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_writet = 1;
    data_in = 11;
    #50 read_writet = 0;
    end
    
    endmodule

    和普通的Verilog模块中一样、使用assign对wire类型的变量进行赋值。

    需要留意的一点是:对于没有在代码中赋初始值的变量,wire类型变量被初始化为Z、reg类型变量被初始化为X。

    always和initial是两种对reg变量进行操作的串行控制块。每个initial和always块都会在仿真开始时同时开始运行。

    常见的,可以利用它们生成模块所需的时钟和复位信号,如下:

    ‘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

    首行定义了时间单位/时间精度。时间单位为1ns,这样生成的clk_50时钟周期就是20ns、也就是频率为50MHz。

    复位信号rst_l在初始为0复位态、在20ns之后为1解除复位。

    仿真中的停止、变量监视和输出

    有两种仿真控制函数:$finish和$stop。其中,$finish任务用于终止仿真并跳出仿真器;$stop任务则用于中止仿真。在Modelsim中,$stop任务则是返回到交互模式。

    如果需要监视仿真中某个变量的变化情况,可以使用$monitor函数:

    $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);

    每当变量列表中的任一变量发生变化,就会产生输出。

    如果需要在仿真控制台屏幕打印输出,可以使用$display函数:

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

    任务Task的用法

    可以将一组重复性的或者相关的命令组合到一起构成一个任务。

    任务通常可以在initial或者always块中被调用。

    一个任务可以拥有输入、输出、以及inouts,也可以包含计时或延时元素。

    以一个在FPGA上实现的简单SPI接口为例。外部设备为主、FPGA为从,命令一共32bit,构成为“1位读写命令字(1读0写)+14位地址+1位NO CARE+16位数据”,片选信号拉低之后通信开始,时序如下图:

    数据流由外设到FPGA时(FPGA为接收),外设在SCLK的下降沿更新MOSI;FPGA在SCLK的上升沿将MOSI上的值抓取到移位寄存器。

    当FPGA为发送方时,FPGA在SCLK的下降沿更新MISO线上的输出,外设在SCLK的上升沿将MISO上的值抓取过来。

    外设可以通过该SPI接口访问FPGA内部生成的寄存器。

    当对FPGA上的spi模块进行读测试时,外设发给FPGA的读指令为:

    {1'b1,address,1'b0,data(读取到的16bit数据)}

    为此编写的任务spi_read可以是:

    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
    View Code

    (其中仿真的时间单位为1ns,spi时钟频率为10MHz)

    示例及汇总

    根据前述内容,自我总结一般简单的testbench文件的结构形式可以是如下:

    `timescale 1 ns / 1 ns
    
    module testbench_module_top;
    reg 
    reg
    ……
    wire
    wire
    ……
    
    //reset and clock definition
    initial beginend
    initial beginend
    
    //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
    ……
    endtask
    
    ……
    
    task task_N;
    ……
    endtask
    
    endmodule
  • 相关阅读:
    如何使用数据卷在宿主机和docker容器之间共享文件
    Debian 7 安装 Docker
    ajax简单封装
    GridView列的排序功能
    SqlHelper帮助类
    模板引擎小例子
    WCF练习小程序总结
    热线接口开发调试工作记录
    在Oracle中使用rank()over()排名的问题
    项目实施中出现的问题点汇总
  • 原文地址:https://www.cnblogs.com/lazypigwhy/p/10599074.html
Copyright © 2020-2023  润新知