FPGA 设计Verilog基础(三)

一个完整的设计,除了良好的功能描述代码,对程序的仿真验证时不可少的。学会如何去验证自己所写的程序,即如何调试自己的程序是一件非常重要的事情。而RTL逻辑设计中,学会根据硬件逻辑来写测试程序。即Testbench是尤其重要的。Verillog测试平台是一个例化的待测(MUT)模块,重要的是给他施加激励并观测器输出。逻辑模块与对应的测试平台共同组成仿真模型,应用这个模型可以测试该模块是否符合自己设计的要求。

编写Testbench的目的是为了对使用硬件描述语言设计的电路进行仿真验证、测试设计电路功能、性能与设计的是否相符。通常,编写测试文件的过程如下:

  • 产生模拟激励
  • 将产生的激励加入到被测模块中并观察其响应
  • 将输出响应与期望值比较

1.1Testbench 文件结构

通常一个完整的测试文件结构为

module Test_bench(); //通常无输入无输出
信号或变量定义声明
逻辑设计中输入对用reg型
逻辑设计中输出对应wire型
使用initial 或 always 语句产生激励
例化待测试模块
监控和比较输出响应
endmodule

1.2 时钟激励设计

下面里举出一些常用的封装子程序,这些是常用的写法,在很多应用中都能应用到。

/*-----------------------------------------------
时钟激励产生方法一:50%占空比时钟
-----------------------------------------------*/

parameter clockperiod = 10;

initial
    begin
        clk_i = 0;
        forever
            #(clock_period/2) clk_i = ~clk_i;
    end


/*-----------------------------------------------
时钟激励产生方法二:50%占空比时钟
-----------------------------------------------*/

initial
    begin
        clk_i = 0;
        always #(clock_period/2) clk_i = ~clk_i;
    end

/*-----------------------------------------------
时钟激励产生方法三:产生固定数量的时钟脉冲
-----------------------------------------------*/

initial
    begin
        clk_i = 0;
        repeat(6)
            #(clock_period) clk_i= ~clk_i;
    ebd

/*-----------------------------------------------
时钟激励产生方法四:产生非占空比为50%的时钟
-----------------------------------------------*/

initial
    begin
        clk_i = 0;
        forever
            begin
                #((clock_period/2)-2) clk_i = 0;
                #((clock_period/2)+2) clk_i = 1;
            end
    end

1.3 复位信号设计

/*----------------复位信号产生方法一:异步复位 -------------*/ 

initial
    begin
        rst_n_i = 1;
        #100;
        rst_n_i = 0;
        #100;
        rst_n_i = 1;
        #100;
    end

/*----------------复位信号产生方法二:同步复位 -------------*/ 
initial
    begin
        rst_n_i = 1;
        @(begedge clk_i)
        rst_n_i = 0;
        #100;  //复位固定时长
        repeat(10) @(negedge clk_i)//固定周期复位
        rst_n_i = 1;

/*----------------复位信号产生方法三:复位任务封装 -------------*/    

task reset;
    input [31:0] reset_time; //复位时间可调,输入复位时间
    RST_ING= 0;//复位方式可调,低电平或者高电平
    begin
        rst_n =RST_ING; //复位中
        #reset_time;    //复位时间
        rst_n_i = ~RST_ING;
    end
endtask
     

1.4 特殊信号设计

/*----------------------- 特殊激励信号产生描述一:输入信号任务封装--------------------*/
 
task i_data;
input [7:0] dut_data;
begin
    @(posedge data_en);send_data = 0;
    @(posedge data_en);send_data = dut_data[0];
    @(posedge data_en);send_data = dut_data[1];
    @(posedge data_en);send_data = dut_data[2];
    @(posedge data_en);send_data = dut_data[3];
    @(posedge data_en);send_data = dut_data[4];
    @(posedge data_en);send_data = dut_data[5];
    @(posedge data_en);send_data = dut_data[6];
    @(posedge data_en);send_data = dut_data[7];
    @(posedge data_en);send_data = 0;
    #100;
end

endtask

//调用方法:i_data(8'hXX);

/*----------------------- 特殊激励信号产生描述一:多输入信号任务封装--------------------*/

task more_input;
input [7:0] a;
input [7:0] b;
input [31:0] times;
output [8:0] c;

begin
    repeat(times)    //等待times个时钟上升沿
        @(posedge clk_i)
            c = a+b;
end
endtask

//调用方法:more_input(x,y,t,z);
//按声明顺序

/*--------------- 双向信号描述一:inout在testbench中定义为wire型变量----------------*/

    //为双向端口设置中间变量inout_reg作为inout的输出寄存,
    //inout变量定义为wire型,使用输出使能控制输出方向

//inout bir_port;
wire  bir_port;
reg    bir_port_reg;
reg    bi_port_oe;
assign bi_port = bi_port_oe?bir_port_reg:1'bz;


/*--------------- 双向信号描述二:强制force----------------*/
    //当双向端口作为输出口时,不需要对其初始化,而只需要开通三态门
    //当双像端口作为输入时,只需要对其初始化并开通三态门,初始化赋值需要使用wire型数据,通过            
    //force命令来对双向端口进行输入赋值
    //assign dinout = (!en) din:16'hz; 完成双向赋值

initial
    begin
        force dinout = 20;
        #200;
        force dinout = dinout-1;
    end
/*---------- 特殊激励信号产生描述三:(输入信号产生)一次 SRAM 写信号产生 ------------*/
initial
    begin
        cs_n = 1;
        cs_n=1;      //片选无效   
        wr_n=1;      //写使能无效   
        rd_n=1;      //读使能无效   
        addr=8'hxx;     //地址无效   
        data=8'hzz;     //数据无效   
        #100;   
        cs_n=0;      //片选有效   
        wr_n=0;      //写使能有效   
        addr=8'hF1;     //写入地址 
        data=8'h2C;     //写入数据   
        #100;   
        cs_n=1;         
        wr_n=1;   
        #10;   
        addr=8'hxx;   
        data=8'hzz;
    end

/*----------------Testbench中@与wait-------------------------*/

//@使用沿触发
//wait语句都是使用电平触发

initial
    begin
        start = 1'b1;
        wait(en=1'b1);
        #10;
        start = 1'b0;
    end

1.5 仿真控制语句即系统任务描述

/*-------------------仿真控制语句及系统任务描述-----------------*/

$stop    //停止运行仿真,modelsim中可继续仿真
$stop(n) //带参数系统任务,根据参数0,1,2不同,输出仿真信息
$finish  //结束运行仿真,不可继续仿真
$finish(n) //带参数系统任务,根据参数0,1,2不同,输出仿真信息

           //0:不输出任何信息
           //1:输出当前仿真时刻和位置
           //2:输出当前方针时刻、位置和仿真过程中用到的memory以及CPU时间的统计

$random    //产生随机数
$random %n //产生范围-n 到 n 之间的随机数
{$random}%n //产生范围0到n之间的随机数



/*-------------------仿真终端显示描述-----------------*/
$monitor        //仿真打印输出,打印出仿真过程中的变量
例如:$monitor($time,,,"clk=%d reset=%d out=%d",clk,reset,out);

$display        //终端打印字符串,显示仿真结果等
例如:$display("simulation start!");
     $display("At time %t,input is %b%b%b,output is %b",$time,a,b,en,z);

$time    //返回64位整型时间
$stime    //返回32位整型时间
$realtime //实型模拟时间


/*-------------------文本输入方式:$readmemb/$readmemh-----------------*/

//激励具有复杂的数据结构
//Verilog    提供了读入读入文本的系统函数

$readmemb/$readmemh("<数据文件名>",<存储器名>);

$readmemb/$readmemh("<数据文件名>",<存储器名>,<起始地址>);

$readmemb/$readmemh("<数据文件名>",<存储器名>,<起始地址>,<结束地址>);

$readmemb: 读取二进制数据,读取文件内容只能包含:空白位置、注释行、二进制数。数据中不能包含位宽说明和格式说明,每个数字必须时二进制数字。

$readmemh: 读取十六进制数据,读物文件内容只能包含:空白位置、注释行、十六进制数。数据中不能包含位宽说明和格式说明,每个数字必须时十六进制数字。

//当地址出现在数据文件中,格式位@hh……h,地址与数据之间不能有空白位置,可出现多个地址。


module
    reg [7:0] memory [0:3];//声明8个8位存储单元
    integer i;

    initial
        begin
            $readmemh("mem.dat",memory);//读取系统文件到存储器中的给定地址
            //显示存储器内容
            for(i=0;i<4;i=i+1)
                $display("Memoey[%d]=%h",i,memory[i]);
        end
endmodule

/*mem.dat文件内容

@001
AB CD
@003
A1
*/

//仿真输出为
Memory[0] = xx;
Memory[1] = AB;
Memory[2] = CD;
Memory[3] = A1;

1.6 加法器的仿真与测试文件编写

上面只举例了常用的testbench写法,在工程应用中基本能满足我们的需求,至于其他更复杂的testbench写法,大家可以草靠其他书籍或者资料。

这里提出几点建议供大家参考:

  • 封装有用且常用的testbench,testbench中可以使用task或者function对代码进行封装,下次利用时灵活调用即可。
  • 如果待测文件中存在双向的信号需要注意,使用reg变量来表示输入,一个wire变量表示输出。
  • 单个initial 语句不要太复杂,可分开写成多个initial语句,便于阅读和修改。
  • Testbench 说到底是依赖PC软件平台的,必须与自身设计的硬件功能相搭配。

下面看一段程序。

module add(a,b,c,d,e);//模块接口

input [5:0] a;
input [5:0] b;
input [5:0] c;
input [5:0] d;
output [7:0] e;

wire [6:0] outa1,outa2;

assign e = outa2+outa1;

adder u1(
.ina(a),
.inb(b),
.outa(outa1)
);

adder u2(
.ina(c),
.inb(d),
.outa(outa2)
);

endmodule

//adder子模块

module adder(ina,inb,outa);

input [5:0] ina;
input [5:0] inb;
output [6:0] outa;

assign outa = ina + inb;

endmodule;

//仿真文件
`timescale 1ns/1ps

module add_tb();

reg    [5:0] a;
reg    [5:0] b;
reg    [5:0] c;
reg    [5:0] d;

wire    [7:0] e;

reg    [5:0] i; //中间变量


add uut(
.a(a),
.b(b),
.c(c),
.d(d),
.e(e)
);

initial
    begin
        a = 0;
        b = 0 ;
        c = 0;
        d = 0;
        for(i=1;i<31;i=i+1)
            begin
                #10;
                a = i;
                b = i;
                c = i;
                d = i;
            end
    end


initial
    begin
        $monitor($time,,,"%d+%d+%d+%d = {%d}",a,b,c,d,e);
        #500;
        $finish;
    end

endmodule

猜你喜欢

转载自blog.csdn.net/MaoChuangAn/article/details/83273654