如何搭建SoC项目的基本Testbench(eetop)
- 格式:pdf
- 大小:227.06 KB
- 文档页数:8
空间管理您的位置: 中国电子顶级开发网(EETOP)-电子设计论坛、博客、超人气的电子工程师资料分享平台 »多云转晴 »日志欢迎大家和我讨论Xilinx FPGA方面的问题如何编写testbench的总结(非常实用的总结)(转)上一篇 / 下一篇 2011-09-19 21:08:27 / 个人分类:FPGA知识查看( 23717 ) / 评论( 0 ) / 评分( 0/ 0) 1.激励的设置相应于被测试模块的输入激励设置为reg型,输出相应设置为wire类型,双向端口inout在测试中需要进行处理。
方法1:为双向端口设置中间变量inout_reg作为该inout的输出寄存,inout口在testbench中要定义为w ire型变量,然后用输出使能控制传输方向。
eg:inout [0:0] bi_dir_port;wire [0:0] bi_dir_port;reg [0:0] bi_dir_port_reg;reg bi_dir_port_oe;assign bi_dir_port=bi_dir_port_oe?bi_dir_port_reg:1'bz;用bi_dir_port_oe控制端口数据方向,并利用中间变量寄存器改变其值。
等于两个模块之间用inout双向口互连。
往端口写(就是往模块里面输入)方法2:使用force和release语句,这种方法不能准确反映双向端口的信号变化,但这种方法可以反映块内信号的变化。
具体如示:module test();wire data_inout;reg data_reg;reg link;#xx; //延时force data_inout=1'bx; //强制作为输入端口...............#xx;release data_inout; //释放输入端口endmodule从文本文件中读取和写入向量1)读取文本文件:用$readmemb系统任务从文本文件中读取二进制向量(可以包含输入激励和输出期望值)。
关于使用ModelSim中创建testbench方法
ModelSim中自动创建TestBench的方法
1. 创建工程,将需要仿真的模块加入工程,编译
2. File -> Open,使用ModelSim自带的文本编辑器打开被仿真模块
3. Source -> Show Language Templates
4. 在显示的Language Templates栏目中选择“Create Testbench”
5. 软件自动弹出的"Create Testbench Wizzard”窗口中,在"work"下选择待仿真模块,按照提示走完,即
自动生成
对于初学者来说写Testbench测试文件还是比较困难的,但Modelsim和quartus ii都提供了模板,下面就如何使用Modelsim提供的模板进行操作。
Modelsim提供了很多Testbench模板,我们直接拿过来用可以减少工作量。
对源文件编译完后,鼠标光标移到代码编辑窗后才会在菜单栏看到source选项,点Source->Show Language Templates然后会出现一个加载工程,接着你会发现在刚才的文档编辑窗口左边出现了一个Language Templates窗口,展开Verilog项,双击Creat Testbench会出现一个创建向导,展开工作目录添加目标文件,点击next,弹出testbench配置窗口,默认就行了,然后点击finish。
一个Testbench模板就诞生了,我们就可以在此编辑窗中添加激励代码了。
本次操作软件版本为modelsim alter starter edition 6.6d.。
Testbench入门1 编写testbench目的编写testbench的主要目的是为了对使用硬件描述语言(HDL)设计的电路进行仿真验证,测试设计电路的功能、部分性能是否与预期的目标相符。
编写testbench进行测试的过程如下:1)产生模拟激励(波形);2)将产生的激励加入到被测试模块并观察其输出响应;3)将输出响应与期望进行比较,从而判断设计的正确性。
2 基本的testbench结构module test_bench;// 通常testbench没有输入与输出端口信号或变量定义声明使用initial或always语句来产生激励波形例化设计模块监控和比较输出响应endmodule简单的testbench的结构通常需要建立一个顶层文件,顶层文件没有输入和输出端口。
在顶层文件里,把被测模块和激励产生模块实例化进来,并且把被测模块的端口与激励模块的端口进行对应连接,使得激励可以输入到被测模块。
端口连接的方式有名称和位置关联两种方式,我们常常使用“名称关联”方式。
3 产生激励的一些描写方式3.1 产生时钟的几种方式1)使用initial方式产生占空比50﹪的时钟initialbeginCLK = 0;#delay;forever#(period/2) CLK = ~CLK;end注意:一定要给时钟赋初始值,因为信号的缺省值为z,如果不赋初值,则反相后还是z,时钟就一直处于高阻z状态。
产生的时钟信号如下图所示:2)使用always方式initialCLK = 0;always#(period/2) CLK = ~CLK;3)使用repeat产生确定数目的时钟脉冲initialbeginCLK = 0;repeat(6)#(period/2) CLK = ~CLK;end该例使用repeat产生3个时钟脉冲,产生的波形如下:4)产生占空比非50﹪的时钟initialCLK = 0;alwaysbegin#3 CLK = ~CLK;#2 CLK = ~CLK;end3.2 产生复位信号的几种形式1)异步复位initialbeginRst = 1;#100;Rst = 0;#500;Rst = 1;end2)同步复位1initialbeginRst = 1;@(negedge CLK); // 等待时钟下降沿Rst = 0;#30;@(negedge CLK); // 等待时钟下降沿Rst = 1;end2)同步复位2initialbeginRst = 1;@(negedge CLK); // 等待时钟下降沿repeat (3) @(negedge CLK); // 经过3个时钟下降沿Rst = 1;end4 testbench实例4.1 2-4解码器实例module dec2x4(A, B, Enable, Z);input A, B, Enable;output[3:0] Z;reg [3:0] Z_o;assign Z = Z_o;always@(A or B or Enable)beginif(Enable == 1'b0)Z_o = 4'b1111;elsecase({A, B})2'b00: Z_o = 4'b1110;2'b01: Z_o = 4'b1101;2'b10: Z_o = 4'b1011;2'b11: Z_o = 4'b0111;default: Z_o = 4'b1111;endcaseendendmodule测试模块测试模块::`timescale 1ns/100psmodule testbench;reg a, b, en;wire [3:0] z;//例化被测试模块dec2x4 DUT(.A(a),.B(b),.Enable(en),.Z(z));//产生输入激励initialbeginen = 0;a = 0;b = 0;#10 en = 1;#10 b = 1;#10 a = 1;#10 b = 0;#10 a = 0;#10 $stop;end//显示输出结果always@(en or a or b or z)begin$display("At time %t, input is %b%b%b, output is %b", $time, a, b, en, z);endendmodule下面是测试模块执行时产生的输出和功能仿真波形:4.2 时序检测器下面是一个时序检测器的验证实例。
如何编写testbench今天,我来带领大家写一个简单的testbench ,顺便讲解如何写好一个testbench 以及写testbench 时应该注意的地方。
在讲解testbench 之前,我们先看一下前面的那个AND_2程序的仿真图,如下:如上图中所标,在1处,B 已经为低电平了,但是输出C 仍然为高电平,这样求与运算就会出错。
在2处,A 和B 都是低电平了,C 仍然为高电平,直到下次出现时钟的上升沿为止,为什么会这样呢?编译的时候并没有报错,呵呵,出了怪事了啊!其实编译器只能检查处语法错误,无法检测到逻辑错误,这个图给出的结果和我们程序所表达的结果一样,但是这并不是我们所要的求与运算,我们想要的是A 和B 同时为高电平时,C 才输出高电平。
我们把程序的敏感列表改为:always@(posedge clk or negedge rst or A or B)就可以了,把A 的电平改变和B的电平改变都加进敏感列表,激励不变,所得到的仿真图:这才是我们所要的求与运算!好了,现在开始讲如何写testbench 。
Testbench 不像RTL 级代码,可以用高级行为语句,不用考虑其可综合性,这样就能写出高效的检测代码。
在语法上,testbench 和可综合代码一样,都是类C 结构。
好了,我们开始吧!1,建立工程等,与之前的一样,但是在创建文件的时候,我们一次创建两个。
取名分别为ParallelSerial_Mult 和ParallelSerial_Mult_test。
创建完成后,如下图:这两个代码分别如下:moduleParallelSerial_Mult(Clk,Rst,MultiplicandIn,MultiplierIn,Load,Product,Out_en);parameter N=8;parameter CYCLES=3;input Clk,Rst,Load;input[N-1:0]MultiplicandIn,MultiplierIn;output[2*N-1:0]Product;output Out_en;reg[2*N-1:0]Product;wire Out_en;reg[N-1:0]Multiplicand;reg[2*N-1:0]NextProduct;reg[CYCLES:0]Count;reg Busy;wire[N-1:0]Sum;wire Carry;assign{Carry,Sum}=Multiplicand+Product[2*N-1:N];assign Out_en=Count[CYCLES];always@(posedge Clk or negedge Rst)if(!Rst)beginMultiplicand<=0;Count<=0;Product<=0;Busy<=0;endelsebeginProduct<=NextProduct;if(Load)beginMultiplicand<=MultiplicandIn;Count<=0;Busy<=1'b1;endelsebeginif(Busy)Count<=Count+1'b1;if(Count[CYCLES])beginCount<=0;Busy<=1'b0;endendendalways@(Load or MultiplierIn or Product or Count[CYCLES]or Carry or Sum) casex({Product[0],Count[CYCLES],Load})3'bxx1:NextProduct={{N{1'b0}},MultiplierIn[N-1:0]};3'b100:NextProduct={Carry,Sum,Product[N-1:1]};3'b000:NextProduct={1'b0,Product[2*N-1:1]};default:NextProduct=Product;endcaseendmodule这个代码是书中第120页的8位乘法器。
在 Vivado中,编写 Testbench 是进行数字电路仿真和验证的重要步骤。
以下是 Vivado Testbench 的一些语法要点:1. 语言选择:Vivado支持使用 SystemVerilog 或 Verilog 作为 Testbench 的语言。
你可以根据需要选择其中之一进行编写。
2. 模块实例化:Testbench 通常包含一个顶层模块来实例化待测试的模块。
你需要创建一个模块,并使用待测试模块的端口信号进行实例化。
3. 时钟和复位:在 Testbench 中,你通常需要生成时钟信号和复位信号,并将其应用于待测试模块的输入端口。
你可以使用 `fork...join` 结构和 `repeat` 或 `forever` 循环来生成时钟信号。
4. 输入模拟:在 Testbench 中,你需要为待测试模块的输入端口提供合适的模拟数据。
你可以使用 `#` 操作符来延迟信号的更新,以模拟不同的输入情况。
5. 断言和检查:在 Testbench 中,你可以使用断言语句来验证待测试模块的行为是否符合预期。
Vivado 支持使用 `assert` 和 `assume` 等关键字来定义断言。
6. 输出比较:在仿真结束后,你可以比较待测试模块的输出信号与预期结果进行验证。
你可以使用 `$display` 或 `$monitor` 等系统任务来显示输出信号的值。
7. 仿真控制:你可以使用 `initial` 块或 `always` 块来控制 Testbench 的仿真行为。
你可以使用 `#` 操作符来延迟仿真时间或 `disable` 关键字来停止仿真。
8. 仿真时长:在 Vivado 中,你可以使用 `run` 或 `run XXns` 命令来指定仿真运行的时长。
默认情况下,仿真会一直运行直到遇到 `$finish` 或 `$stop` 系统任务。
以上是 Vivado Testbench 的一些语法要点。
编写高效Vivado HLS工程testbench的三个要素Xilinx DSP specialist FAE: Harvest Guo在C程序的设计中,任何一个C程序的顶层都是main()函数。
而在vivado HLS的设计中,只要函数的层次在main()函数以下,都可以被综合。
但是每个vivado HLS工程只能指定一个top层函数作为输出RTL模块的顶层,其它和这个函数层次平行,不需要被综合的函数都可以作为testbench来使用。
这样就带来一个问题,如何编写vivado HLS工程的testbench更高效,或者说能更好的让HLS工具自动重用C testbench验证产生的RTL代码就变得非常重要。
通常,在Vivado HLS中,好的C testbench设计原则是testbench设计和需要实现的算法函数分别保存在不同的文件中,并且充分利用头文件。
Testbench常常包含了一些HLS综合不支持的操作,比如通过文件的读写取得仿真数据并保存结果,或者打印一些测试结果进行分析。
在头文件中,完成对testbench中所有的数据类型和函数的定义,以及包含共享的设计文件和函数库。
Vivado HLS中,只能指定一个top层函数用于综合,top层函数可以包含多个子函数。
当需要综合多个并行层次的函数时,可以编写一个wrapper函数作为top层函数,将需要综合的多个并行函数封装起来。
C testbench的目不仅是要验证需要综合的top函数功能正确(C编译器验证环境),同时重用C testbench作为综合产生RTL代码的仿真激励,HLS工具自动调用C testbench来验证RTL功能的一致性(C编译器和RTL仿真器的协同仿真环境)。
这样,编写一个好的风格testbench可以很好的提高设计的验证效率,如果在HLS综合前和综合过程中,需要修改综合函数的代码,可以用testbench验证,确保需要综合的C算法功能正确。
(verilog和vhdl)Testbench编程指南TestBench编程指南如今数字设计的规模变得越来越庞大,设计的复杂程度也越来越高,这就使得设计的验证变得越来越困难,而且费时费力。
为了应对这种挑战,验证工程师依靠各种验证工具和方法。
对于大型设计,如几百万门的设计,通常采用一整套正式的验证工具。
然而,对于小一些的设计,设计工程师发现往往采用带TestBench的HDL仿真工具是最好的途径。
TestBench已经变成验证高级语言设计的一种标准的方法。
通常,TestBench执行以下任务:z例化设计,使其可测试(DUT-design under test);z通过将测试向量应用到模型来仿真例化后的可测试的设计;z将结果输出到终端,或者输出波形窗口;z将真实的结果和期望的结果进行比较;一般,TestBench采用工业标准的VHDL或者Verilog硬件描述语言来编写。
TestBench调用功能设计,然后仿真。
复杂的测试文件执行附加功能――例如,他们包含逻辑以决定合适的设计激励或者比较真实的结果和期望的结果。
以下章节将讨论一个组织良好的测试文件的组成,以及例举了一个带有自检的测试文件(自动将真实的结果和预期的结果进行比较)。
下图是一个标准的HDL验证的流程。
自从测试文件可以用VHDL或者Verilog编写以来,测试验证流程就可以在平台和供应商的工具交叉进行。
同时,由于VHDL和Verilog都是标准的公用的语言,所以用VHDL或者是Verilog描述的验证可以很简单的被再使用。
图1. HDL验证流程测试文件构成:测试文件可以采用VHDL或者Verilog语言编写。
由于测试文件只是用来仿真的,他们就不被用于综合的RTL语言子集的语法所约束。
相反,所有行为结构都可以被使用。
这样,测试文件可以被写的更通用,更易于维护。
所有的测试文件都包含以下基本内容,如表1。
如上所属,测试文件经常同时包含附加功能,如结果的可视化显示和内建错误检测。
先啰嗦几句。
其实老早就想写这个帖子,自己犯懒一直木有写。
前阵子写了一个初版,然后发给了几个做验证的朋友看了看,普遍反映没看明白. 说是我写的东西和我搭的环境结合的太过紧密了,不结合代码,理解的不透彻。
可惜代码是公司的,我不能把代码发出来。
我后来写了一个带很多代码截屏的版本,但是很抱歉没法发到论坛上来。
我个人觉得下面的文字已经能表达我的想法和思想了,希望能对帖的有一点帮助吧。
---------------------------------------------写这个文档的目的是让大家对搭建SoC项目的Testbench有一个比较清晰的认识,可以根据这个文档来一步一步的搭建起一个SoC项目的基本的testbench。
本文档重点是指导大家搭建基本环境,以及能解决搭建Testbench过程中容易遗漏的问题或者容易遇到的“地雷”。
我搭的SoC项目的testbench会有一些相对特殊的点:1)要有嵌入式的软件。
这里包括两部分,一是初始化的bootloader(一般是固化在rom或者存放在外部的flash里),一是boot起来以后放在外部易失性存储介质上的应用层的程序。
2)正常启动起来(一级boot可以切到应用程序了)以后,为了简化流程,我们要使用ISS的环境。
--- 这是比较特殊的一个点3)环境主要脚本的维护和修改。
主要是单个仿真和批量仿真(regression)核心脚本4)为了优化仿真和编译速度,我们要能把不用的模块dummy掉。
5)文件列表的处理6)SoC软件与Testbench都能访问的“共享空间”的处理7)公用函数的准备,比如根据CPU看到的地址空间直接访问外部DRAM的数组,进行初始化写、数据写和数据读操作。
8)环境变量的维护。
9)Define文件的维护10)DDRC的替换(一个是AXI_SLV_VIP的替换,一个是简单AXI_SLV模型的替换)磨刀不误砍柴工,把需要的东西提前准备好,搭建Testbench就像搭积木一样简单快速了。
环境变量维护使用module工具来维护整个项目的环境变量。
目的是为了让项目上的工程师都使用统一的环境(主要是工具版本和环境变量)。
核心脚本的维护两个脚本:run_sim 和regress。
run_sim负责提交单个仿真任务,regress负责提交批量仿真任务。
两个脚本已经使用了很多项目了,脚本的具体说明我以后专门开专题讲。
在这里只提醒一下,run_sim脚本通常需要根据不同的项目做微小的改变。
run_sim和regress都是比较大的perl脚本程序,大致描述一下功能。
run_sim脚本功能1)为每个仿真产生仿真目录。
仿真的目录里应该包括文件列表(硬件和软件)、编译和仿真命令(注意包括嵌入式软件的MakeFile)、提前建立需要的子目录、和单个仿真对应的文件链接(比如维护的C的测试主函数、扩展的随机类的SV文件、一级bootloader文件的链接)、define文件、本仿真的重构命令(这是一个容易忽略的,一旦你跑regression的时候某个仿真失败,你又不想在出错的目录下重新仿真,用这个重构命令文件就可以直接提交)。
2)各种option的维护。
比如不同仿真需要不同的define、编译和运行option、dump波形的scope以及层次regress脚本功能regress脚本比较简单,要吃一个由很多run_sim仿真命令组成的命令集文件。
用regress 脚本把这些仿真命令提交到工作站上去。
需要注意的是:有时候可能会有一些公共的option 或者define,比如打开coverage收集、某个define要应用到整个regression里。
所以regress脚本要能支持对所有run_sim命令添加option的功能。
产生dummy文件使用gen_dummy_file脚本来产生dummy文件。
设计工程师可能也要维护一个module_dummy.v的文件用于做integeration,验证工程师产生的dummy文件记得名字不要和设计自己维护的文件重复了。
为什么不使用设计维护的文件?因为一个是设计维护的文件在integration以后很可能就不再维护了;另一个是设计维护的文件可能output全是assign成0的,但是对于模块输出的pready\CEN等信号最好assign成1,否则可能导致问题(例如:sram使能信号CEN赋成0,可能导致后面的sram模型认为有读写行为;pready信号赋成0,可能导致SoC软件跑起来的时候对该模块寄存器操作的时候挂死apb总线)。
这个脚本并不好写。
因为verilog语法支持的模块声明实在是太多了,导致脚本很容易顾此失彼。
举例来说几个复杂的地方:module声明后面可以跟parameter的就很复杂Module test #(parameter a = 1,Parameter b = 2,C = 3,D = 4 );这些parameter很可能要用在端口位宽的声明里。
更为麻烦的是parameter里可能会有function的使用。
而function有可能是以define的形式写到代码中。
这样就很难用parse RTL的方法来解决。
再比如:端口声明里出现ifdef else endif这种编译宏的处理也比较麻烦。
也可以使用simulator或者debug工具提供的用户接口来编写tcl程序来获取各个端口的name、width信息。
但是不同仿真(define不同)可能导致端口宽度和端口不一致,结果要针对不同define来维护不同的dummy也比较麻烦。
总之,产生dummy文件以后一定要记得检查一下。
Dummy文件可以有效缩短编译的时间。
文件列表处理的维护上述几个事情是应该提前准备好的,接下来我们要开始编译RTL了。
Integration好的文件列表,首先要先编译该文件列表。
有可能遇到的问题是加密文件的种类,有可能文件列表里的加密文件和你用的仿真器不一致。
然后结合前面产生好的dummy文件,我们要处理出一个简化设计的mini-文件列表,一般里面只包括初始化必须的模块(Clkrst、PAD、CPU、总线拓扑、内存控制器),也就是把video系统、外围接口、存储系统这些模块统统dummy掉。
产生mini-文件列表可以用脚本来维护一个配置文件,在该配置文件中指明如何删改原有文件列表。
注意最终使用的文件列表里的文件路径应该是绝对路径。
使用绝对路径的好处在于可以让run_sim脚本指定仿真在任何目录下进行(比如regression 要提交到别的硬盘上去跑,那么就必须使用绝对路径了)。
注意绝对路径里不要用$macro的结构,别人有可能用你的文件列表跑仿真或者debug,而别人的$macro很可能与你的不同,导致出问题。
文件列表的产生有一个地方需要注意:通常来说一个文件(比如a.v)在一个文件列表里只允许出现一次。
否则可能会有重复module 的编译错误。
但是有时候集成比较特殊(比如FPGA版本),为了改动的时候少调整code,会使用ifdef-elsif-else-endif这种结构来对同一个文件的module定义不同的module-name。
比如文件a.v的内容如下:`ifdef FPGA1module v_fpga1 (`elsif FPGA2module v_fpga2 (`elsif FPGA3module v_fpga3 (`elsemodule v (`endif那么在文件列表里就会是下面这种结构:fpga1_def.va.vfpga1_undef.vfpga2_def.va.vfpga2_undef.v……请注意文件列表处理脚本,有可能会有“去重”的处理。
这个时候要去掉文件列表处理脚本的“去重”功能。
编译过mini-文件列表以后就可以开始真正的准备写testbench顶层模块了。
Define文件的维护我们在搭建testbench过程中的interface、env、svtb等可能需要xmr(cross module reference)访问信号,这时候维护一个公共的define文件很重要。
该define文件中应该包括1)各个主要模块的xmr路径define ,记得按照ASIC/ FPGA/模块级来分别区分define2)地址空间上模型数组的路径。
比如dram模型里数组的xmr路径、sram里memory数组的xmr 路径3)共享空间的部分地址的define,比如我们的软件打印的实现所用到的共享空间的define 4)Dram基本define共享空间SoC项目Testbench中的“共享空间”,是指的软件(嵌入式C程序)和Testbench(SV程序)都可以看到的空间。
一般来说Testbench可以看到所有的内容,而软件只能看到CPU 地址空间(寄存器、SRAM、ROM、Dram、外部IO空间等)。
共享空间需要的地址范围不算小(可能需要几十KB,所以一般是放在CPU可见的SRAM和Dram里),对于ISS会有所不同(后面会说明)。
公共函数的维护项目上大家都可能使用到的函数即为“公共函数”。
我个人认为最重要的是对CPU地址空间的访问(我们是xmr_read_mem和xmr_write_mem)。
以及基于这几个函数(task)实现的文件存取等函数。
在实现xmr_read_mem和xmr_write_mem task(或function)的时候,主要模型数组的宽度会导致根据模型数组下标访问的地址的不同。
比如,加入位宽是128bit,那么一个memory就对应着4个32bit的word。
----- 各个项目会有所不同。
另外,对于Dram的处理是最复杂的,尤其是Dram是支持bank地址和Row地址 remap的,所以要特别注意remap时候的地址和bank信号、row地址信号的对应关系。
-----这个工作是可以继承前面项目的。
Xmr函数需要考虑“用SRAMC或者AXI_SLV_VIP替换DDRC”情况下的实现。
简单说一下vip_slave_write32函数的实现。
这个函数调的底层函数是:env.axi_slave_subenv.do_write32(addr, data);但是该函数在tb其他组件可能看不到,但是program可以看到。
所以在program里做一个函数来调底层的env.axi_slave_subenv.do_write32然后把program的这个do_write32用DPI export出去。
在tb上维护一个xmr.c的c程序,里面实现vip_slave_write32。
在xmr_wr_mem32里调用的就是这个vip_slave_write32.使用xmr_wr_mem32 和 xmr_rd_mem32可以比较容易的实现:1)装入初始化程序 --- 一级bootloader要装入到rom中,应用程序要装入到dram中。