赞
踩
仿真测试是FPGA设计流程中必不可缺的流程,尤其在FPGA设计规模和复杂度与日俱增的今天,简简单单写两三个模块的Verilog代码,直接上板调试的工作,现实当中几乎已经不存在了。从笔者实际工作经验来看,如果一个项目中大家讨论到最后决定去起用FPGA,那么在设计当中基本或多或少都会有其他soc芯片如MCU、ARM、DSP不易实现的难点。一个落地转产的设计要在验证上花费的工作量往往可以占到整个项目开发流程的70%左右,仿真验证可以尽快发现设计当中存在的各种大大小小的逻辑bug,避免设计到最后一步的时候才发现结果和预期功能不一致,然后又来回定位问题,来回返工重做。当然我们在设计各个模块的时候,通过持续地项目积累、准确地前期分析、仔细地代码设计,也完全可以反过来减少不必要的仿真测试,提高代码的准确度少犯低级错误,从而大大提高项目进度。
提到仿真,就会聊起Testbench的概念,所谓Testbench即测试平台,或者通俗地说,就是给被验证的设计模块添加输入激励,再去观察模块的输出是否满足预期设想,初学fpga的朋友们,可能刚接触仿真的时候,会觉得写仿真文件只是单纯写几个输入信号,和测试模块一起代入Modelsim观察波形即可。
虽然这样做也没有问题,但是大家有没有想过如果是去验证大规模的设计,单纯靠穷举法去列举输入信号再通过肉眼一个个观察输出结果是非常难以实现的,举个例子,对于一个16位的输入信号,可能存在2的16次方即65536种组合,那么我们去对着输出波形逐一观察,可能看花眼都看不过来,因此对于FPGA仿真设计,直接去观察输出波形去验证测试结果是可以的,但绝对不是唯一的方法,如图1所示,是一般性的Testbench验证输出的方法,图中列举的三种验证输出的通用方法,笔者在这两节里都会举实例进行比较详细的说明,来帮助大家更好地消化理解,Testbench仿真文件的书写和应用是FPGA工程师的必备技能,笔者真诚地希望通过两篇博客去帮助朋友们掌握一般性的方法,也可以进一步地去灵活应用在实际工作当中。
图1 Testbench验证输出的方法
与功能模块在书写模式和语法规则上存在细微差别,测试模块有一些自己的特殊语法,用来更好地去支持对Testbench的编写,笔者在初学FPGA的时候也跌跌绊绊看过不少相关介绍Testbench编写的书籍或者博客,但看来看去、读来读去,得到更多是残缺的知识碎片,所以在这里想把测试模块中常用的语法规则归纳总结出来,并且简单举例介绍,帮助大家在学习中建立起整体的概念,方便在后续工作中,得心应手地应用实践。
命令格式:`timescale<时间单位>/<时间精度>
`timescale命令用来定义本模块下的仿真时间单位和时间精度,大家请注意在这条命令中,<时间单位>参量是用来定义模块中仿真时间和延迟时间的基准单位;<时间精度>参量是用来定义模块仿真时间的精度程度,当然这句话解释地很官方也很书面,所以可能字面上理解起来有点困难。
为了帮助大家更好地理解这两个参量的含义,在这里举个例子:`timescale 10 ns/1 ns,定义了整个模块的时间单位是10ns,时间精度是1ns,也就是说在本测试模块下,所有的时间值都是10ns的整数倍,并且以1ns为时间精度。如下图2所示,din作为输入的激励信号,首先定义了TIME为1.55,而这里的时间精度是1ns,那么参数TIME实际上被取整为1.6,其次因为时间单位是10ns,所以延迟时间#TIME作为时间单位的整数倍为16ns。整体上意味着din输入信号在仿真时刻是10ns被赋值为0;在仿真时刻是26ns时,din输入信号被赋值为1;在仿真时刻是42ns时,din输入信号被赋值为0。
图2 `timescale的举例说明
在`timescale的定义中,用于说明时间单位和时间精度参量值的数字必须是整数,且有效数字为1、10、100,单位可以是秒s、毫秒ms、微秒us、纳秒ns、皮秒ps、飞秒fs,并且参量时间精度至少要与时间单位一样精确,即时间精度的值要小于等于时间单位的值,一般情况下,大家都通常会习惯性定义为`timescale 1 ns/1 ns。
命令格式:{$random} % b ;
这是Testbench产生随机数据的一种有效方式,{$random} % b,其中b是大于0的整数,那么实际上给出了一个范围在(-b+1)到(b-1)之间的随机数,举个简单的例子,用如下图9-3所示的方法去产生一个随机数,因为rand被定义为16位位宽的reg型,那么在这里实际上rand会得到一个0-59之间的数。
图3 $random的举例说明
语法格式:initial begin
语句1;
……
语句n;
end
和功能文件的书写上有些区别,测试模块则一般习惯性用initial模块去产生一个信号的输入激励或对一个变量进行初始化赋值操作,这里也举两个例子说明,如下图4所示,在第一个例子当中,我们用initial模块在仿真开始时对变量值进行初始化,而这个初始化操作是不占用任何仿真时间的,即在仿真时刻为0时,已经完成了对变量memory的赋随机值的操作;在第二个例子当中,我们则用initial模块去产生din输入信号的激励波形。
图4 initial模块的举例说明
命令格式:$finish ; $finish(n) ; $ stop ; $ stop(n) ;
系统任务$finish的作用是退出仿真器,返回主操作系统,即结束整个仿真的过程,任务$finish可以带参量,也可以省略不写默认为1,一般大家习惯上省略该参量,对于不同参量值,系统输出的信息值为:0代表不输出任何信息;1代表输出当前仿真时刻和位置;2代表输出当前仿真时刻和位置,以及在仿真过程中所用memory一级CPU时间的统计。
系统任务$ stop的作用是把仿真器置成暂停模式,在仿真环境下给出一个交互式的命令提示符,并把控制权交给用户,同样的这个系统任务可以带参量,也可以习惯性的省略该参量,根据参量值(0、1、2)输出不同的信息,参量值越大输出的信息也就越多。
语法格式:forever 语句;
forever begin
语句1;
……
语句n;
end
forever循环语句常用在产生周期性的波形,用来作为仿真测试的信号,其本身不能独立出现在程序当中,必须嵌套在initial块内,如图5所示,这里用forever循环语句产生了一个频率为50M的标准时钟,作为时钟的输入激励代入功能模块当中。
图5 forever语句的举例说明
语法格式:repeat表达式
repeat begin
表达式1;
……
表达式n;
end
在repeat语句中,其表达式通常都是一个常量,如图6所示,我们用repeat语句去产生了测试模块的复位输入激励信号,这里就用了repeat语句,使得rst_n信号再等待两个时钟的上升沿后被置位为1,此外测试模块当中也可以使用repeat语句去循环移位或者做加减法等其他重复性的操作。
图6 repeat语句的举例说明
语法格式:while(表达式)语句;
while(表达式)begin
语句1;
……
语句n;
end
while语句在测试模块的编写当中,使用得比较少,一般大家都习惯性用repeat或者for语句去实现相同的功能,这里也举个直观的例子帮助大家更好地理解while语句在测试模块当中的应用,如图7所示,使用了while循环语句对位宽为8位的输出变量dout中为1的位数进行了计数。
图7 while语句的举例说明
语法格式:for(循环变量赋初值;循环结束条件;循环变量增值)
语句;
类似于C语言的语法,在测试模块当中for循环语句用得比较频繁,经常被用来初始化一些变量,对一些变量进行赋值或移位等等重复性的操作,也同样也会和$display、$write、$fscanf搭配使用,用来输出输入一些信息,使得整个Testbench的编写更加流畅,如下图8所示,这里就用了for循环语句去初始化memory变量。
图8 for语句的举例说明
task和function说明语句分别用来定义测试模块当中的任务和函数,利用任务和函数可以把一个很大规模的程序分解成很多较小的任务和函数,非常便于Testbench的编写、理解以及调试,类似于C语言中的函数接口,输入、输出、总线信号的值可以传入、传出任务和函数,所以说学会灵活使用task和function可以极大程度上简练Testbench的程序结构,使得程序整体上通俗易懂,直观明了。
任务格式:task <任务名>
<端口和数据类型声明语句>
语句1;
……
语句n;
endtask
图9 task任务的举例说明
如上图9所示,是按键消抖测试模块的task任务,这里我们模拟按键闭合和按下可能存在的机械抖动情况,并将其做成了task任务,方便在Testbench的编写时候去调用它,从而更加简化测试模块的程序结构。
函数格式:function <返回值的类型或范围>(函数名)
<端口说明语句>
<变量类型说明语句>
begin
语句1;
……
语句n;
end
endfunction
如图10所示,我们用阶乘函数的定义和调用来举例说明function的具体用法,在这个例子当中也用嵌套使用了for循环语句和$display,代码层面整体上并不难理解,所以就不再赘述了。
图10 function函数的举例说明
task和function说明语句也存在一些不同点,请大家在编程时需要去注意:1.函数只能和本模块共用同一个仿真时间单位,而任务则可以定义自己的仿真时间单位;2.函数不能启动任务,而任务里能启动其他任务或函数;3.函数至少要有一个输入变量,而任务可以没有或者有多个任意类型的变量;4.函数返回一个值,而任务则不返回值。
因为task的书写格式总得来说要比function更加宽泛和灵活,所以在实际应用操作中,大家一般都习惯性定义task来分割任务,这其实类似于上面的while、repeat和for语句都可以表示循环,但是repeat和for语句因为其书写上的灵活性更容易被人们所接受。
语法格式:文件句柄 = $fopen("文件句柄","读取格式");
使用 $fopen打开文件,当我们希望把测试文件中的数据通过如.txt外部文件导入,或者希望将Testbench的测试结果的数据通过如.txt外部文件导出的时候,我们则一般会使用$fopen去打开文件,并对文件中的数据进行读写操作,如图11所示是笔者工作的时候,在Vivado环境下测试Xilinx FFT IP核,编写的Testbench代码片段,为了更方便地在Modelsim下观察IP核的输出结果,笔者把其输出的实部和虚部结果保存到两个外部的.txt文件下,再代入Matlab仿真去验证FFT结果的正确性。这里我们需要注意在使用$fopen打开文件的时候,后面有一个参量,这个参量如果是w是以只写方式打开文件;如果是r是以只读方式打开文件;如果是+则是以读写方式去打开文件。
图11 $fopen的举例说明
语法格式:文件句柄 = $fclose ("文件句柄");
使用 $fclose关闭文件,请大家注意,这里的文件句柄就是在$fopen中获得的文件句柄,如下图12所示,我们用$fopen打开文件并获得其句柄,那么在使用$fclose的时候,需要去关闭其对应文件名的句柄。
图12 $fclose的举例说明
语法格式:$display (p1 ,p2 ,p3 , ……,pn);
语法格式:$write (p1 ,p2 ,p3 , ……,pn);
这两个系统任务的作用是输出信息,即将参数p2到pn按参数p1给定的格式输出,在这里,通常称参数p1为“格式控制“,通常称参数p2到pn为“输出列表”,这两个系统任务的作用基本是相同的,唯一区别在于$display会自动地在输出后进行换行操作,而$write则不会,所以在实际工作当中,可能大家会习惯性用$display多一些,如表9-1和表9-2所示,分别为输出格式和说明,换码格式和功能。$display、 $write其实非常类似于C语言中的printf函数,如图9-12所示,举个实际的例子,通过$display系统任务会输出:
\%
“S
输出格式 | 详细说明 |
%h或%H | 以十六进制数的形式输出 |
%d或%D | 以十进制数的形式输出 |
%o或%O | 以八进制数的形式输出 |
%b或%B | 以二进制数的形式输出 |
%c或%C | 以ASCII码字符的形式输出 |
%v或%V | 输出网络型数据信号强度 |
%s或%S | 以字符串的形式输出 |
%t或%T | 以当前的时间格式形式输出 |
%e或%E | 以指数的形式输出实型数 |
%f或%F | 以十进制数的形式输出实型数 |
表1 输出格式和说明
换码格式 | 具体功能 |
\n | 换行 |
\t | 跳到下一个输出区 |
\\ | 反斜杠字符\ |
\” | 双引号字符” |
\o | 1-3位八进制数代表的字符 |
%% | 百分符号% |
表2 换码格式和功能
图13 $display的举例说明
语法格式:$fscanf("文件句柄","读取格式","数组")
使用 $fscanf读取外部文件中的数据,当我们希望把测试文件中的数据通过如.txt外部文件导入,即从外部文件中有序读出有效数据用在本Testbench下,这时候一般使用$fscanf来实现,如图14所示,也是是笔者工作的时候,在Vivado环境下测试Xilinx FFT IP核,编写的Testbench代码片段,我们把Matlab生成的数据提前保存在外部.txt文件中,并代入FFT IP核作为输入激励,当然也有类似功能的系统任务比如$readmemh,但是$readmemh却只能读取16进制数,没有$fscanf用起来灵活,所以实际Testbench的编写中,多数人还是习惯性使用$fscanf去读取外部文件中的数据,当然如果在工作中见到别人用$readmemh,大家也需要明白什么意思即可。
图14 $fscanf的举例说明
在Verilog中$time可以返回一个64比特的整数来表示的当前仿真时刻值。该时刻是以模块的仿真时间尺度为基准的,如图15所示,是$time的应用举例,通过打印出警告信息,封装在warning任务里,方便仿真过程中定位到问题。
图15 $time的举例说明
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。