最新如何编写一个高效的Testbench?_testbench编写,2024火爆全网系列

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

在FPGA的开发过程中,验证是一个必不可少的环节,其所占时间有时候甚至还要大于RTL级的开发时间。对于大型设计,设计公司通常都有一整套完整且规范的设计流程。而对于小型设计,我们设计者则可以自己编写一个testbench对设计进行一个简单的功能验证。本文将告诉你:对于小型设计,要如何编写一个高效率的testbench。

本文主要翻译自Xilinx《XAPP199,Writing Efficient Testbenches》,浅蓝色字体为本人理解。


概述

本文是为刚接触HDL验证流程和没有丰富的Testbenches编写经验的逻辑设计师编写的。Testbenches是验证HDL设计的主要手段。本文提供了布局和构建高效Testbenches的指导大纲。它还提供了一种算法来为设计开发一个自检的Testbenches。


介绍

由于设计规模和复杂性的增加,数字化设计验证已经成为一项越来越困难和艰巨的任务。为了解决这一挑战,验证工程师使用了一些验证工具和方法。对于大型的、数百万门电路级的设计,工程师通常使用一套正式的验证工具。然而,对于较小的设计,设计工程师通常发现带有testbenches的HDL仿真工具是一种最有效的测试模式。

Testbenches已经成为验证HLL(High-Level Language,高级语言)设计的标准方法。通常,Testbenches会实现以下任务:

  • 实例化被测模块(DUT)
  • 通过对模型应用测试向量来激励DUT
  • 输出结果到终端或波形窗口进行目视检查
  • 可以选择将实际结果与预期结果进行比较

通常,testbenches是用行业标准的VHDL或Verilog硬件描述语言编写的。Testbenches调用被测模块,然后编写测试激励来激励它以观察期输出。复杂的testbenches会实现一些额外的功能----例如,它们包含用于确定设计的适当设计刺激或将实际结果与预期结果进行比较的逻辑。

本文描述了一个高效的testbenches架构,并提供了一个自检testbenches的示例----它自动地比较实际输出的结果和预期输出的结果。

图1显示了遵循上述步骤的标准HDL验证流程。

由于testbenches是用VHDL或Verilog编写的,因此testbenches验证过程可以跨平台和工具进行移植。此外,由于VHDL和Verilog是标准的非专有语言,所以在未来的设计中,用VHDL或Verilog编写的验证套件可以轻松地被复用。

testbenches通常包含一下部分:被测模块DUT,测试激励,测试结果比较。DUT是我们编写的RTL代码,也就是我们的功能模块;测试激励是要对DUT进行的测试输入,比如你的功能模块是一个加法器,那么你总得写个测试激励----要给2个加数赋值吧?结果比较模块的实现比较多样化:你可以直接观察波形,看看是不是自己想要的结果;也可以将结果与预期正确的结果自动做比较,如果无误就输出低电平,如果有误就输出高电平;也可以把所有的结果都保存下来,再用脚本自动比对,等等。


构建testbenches

testbenches可以用VHDL或Verilog编写。由于testbenches仅用于仿真,因此它们不受可综合过程中使用的RTL语言子集的语义约束的限制。因此,可以更通用地编写测试testbenches,这会使得它们更具可维护性。

所有的testbenches都包含表1中所示的基本部分。如上所述,testbenches通常还包含额外的功能,例如在终端上显示结果和错误检测功能。

下面的例子展示了一些在testbenches中经常使用的结构。

上面的结构是一个testbenches一定会具备的基本结构,testbenches的编写比较灵活,因为其内容不需要综合,也就是说不需要被映射到具体的电路实现,说白了,就是设计者随心所欲的写激励来作为输入,观察被测的功能模块能否在对应测试激励下,实现符合预期条件的输出。


产生时钟信号

使用系统时钟来实现时序逻辑的设计必须生成一个时钟。时钟可以很容易地在Verilog源代码中实现。以下是时钟生成的例子:

// Declare a clock period constant.
Parameter ClockPeriod = 10;

// Clock Generation method 1:
initial begin
    forever Clock = #(ClockPeriod / 2) ~ Clock;
end

// Clock Generation method 2:
initial begin
    always #(ClockPeriod / 2) Clock = ~Clock;
end

时钟信号一般使用always和Foever这两个循环来构建,方法很简单:每半个时钟周期,时钟信号翻转一次,这样就能实现周期性的占空比为50%的方波。


提供激励

为了获得testbenches的验证结果,必须向被测模块提供测试刺激。在testbenches上使用并行块来提供必要的测试激励。可以采用两种方法:绝对时间激励和相对时间激励。在第一种方法中,仿真数值相对于仿真时间零指定。相比之下,相对时间激励提供初始值,然后等待事件再重新触发激励。根据设计人员的需要,这两种方法可以在一个testbenches中实现。

下面分别提供了绝对时间激励和相对时间激励的例子:

(1)绝对时间激励

initial begin
Reset = 1;
Load = 0;
Count_UpDn = 0;
#100 Reset = 0;
#20 Load = 1;
#20 Count_UpDn = 1;
end

绝对时间激励使用#+时间来定义,比如#20就代表从仿真开始20个时间单位。

(2)相对时间激励

always @ (posedge clock)
    TB_Count <= TB_Count + 1;

initial begin
if (TB_Count <= 5) 
 begin 
 Reset = 1;
 Load = 0;
 Count _UpDn = 0;
 end
else
 begin
 Reset = 0;
 Load = 1;
 Count_UpDn = 1;
 end
end

initial begin
 if (Count == 1100) begin
 Count_UpDn <= 0;
 $display("Terminal Count 
Reached, now counting down.");
 end 
end

相对时间激励则使用某些时间来进行控制,比如时钟的上升沿啦,某个信号条变成具体的指定的值的时间啦,等等。

Verilog中的所有initial块是一起并发执行的。但是,在每个initial块中,事件是按照写入的顺序执行的。这意味着测试激励在每个并发块中会从仿真的零时刻开始执行。设计者应该使用多个initial块将复杂的测试激励分解成多个简单的部分,从而使代码更具可读性和可维护性。


显示结果

在Verilog中,通过 d i s p l a y 和 display和 displaymonitor关键字可以方便地显示结果。

以下是在终端屏幕上显示仿真结果的例子:

// pipes the ASCII results to the terminal or text editor

initial begin
 $timeformat(-9,1,"ns",12);
 $display(" Time Clk Rst Ld SftRg Data Sel");
 $monitor("%t %b %b %b %b %b %b", $realtime,
 clock, reset, load, shiftreg, data, sel);
end

t i m e f o r m a t 关键字指定时间变量的格式。 timeformat关键字指定时间变量的格式。 timeformat关键字指定时间变量的格式。display关键字将带引号的文本(“…”)输出到终端窗口。 m o n i t o r 关键字的工作方式不同,因为它的输出是事件驱动的。在本例中, monitor关键字的工作方式不同,因为它的输出是事件驱动的。在本例中, monitor关键字的工作方式不同,因为它的输出是事件驱动的。在本例中,realtime变量(由用户分配给当前仿真时间)用于触发信号列表中值的显示。信号列表以 r e a l t i m e 变量开始,后面跟着要显示其值的其他信号的名称 ( 时钟、重置、加载等 ) 。开头的“ realtime变量开始,后面跟着要显示其值的其他信号的名称(时钟、重置、加载等)。开头的“%”关键字包含一个格式说明符列表,用于控制如何格式化信号列表中的每个信号值以便显示。格式列表是位置对应的——每个格式说明符顺序地与信号列表中的一个连续的信号名称相关联。例如,%t指示符将显示的 realtime变量开始,后面跟着要显示其值的其他信号的名称(时钟、重置、加载等)。开头的realtime值格式化为时间格式,而第一个%指定符将时钟值格式化为二进制格式。Verilog提供了额外的格式说明符,例如,%h用于十六进制格式,%d用于十进制格式,%o用于八进制格式。

格式化的显示结果如图2所示。

d i s p l a y 是直接显示括号里的一句话,而 display是直接显示括号里的一句话,而 display是直接显示括号里的一句话,而monitor则是对括号内的变量进行监控,每当监控变量中的任意一个发生改变,就会在终端打印所有的变量值。


简单的testbenches

简单的testbenches例化被测模块,然后为其提供测试激励。Testbench输出以图形方式显示在仿真工具的波形窗口上,或者作为文本发送到用户的终端或文件。下面是一个表示移位寄存器的简单Verilog设计:

module shift_reg (clock, reset, load, sel, data, shiftreg);
input clock;
input reset;
input load;
input [1:0] sel;
input [4:0] data;
output [4:0] shiftreg;
reg [4:0] shiftreg;
always @ (posedge clock)
begin
 if (reset)
 shiftreg = 0;
 else if (load)
 shiftreg = data;
 else 
 case (sel)
 2'b00 : shiftreg = shiftreg;
 2'b01 : shiftreg = shiftreg << 1;
 2'b10 : shiftreg = shiftreg >> 1;
 default : shiftreg = shiftreg;
 endcase
end
endmodule

以下的testbenches例化了上面的移位寄存器模块:

module testbench; // declare testbench name

 reg clock;
 reg load;
 reg reset; // declaration of signals
 wire [4:0] shiftreg;
 reg [4:0] data;
 reg [1:0] sel;

 // instantiation of the shift_reg design below
 shift_reg dut(.clock (clock), 
    .load (load), 
    .reset (reset), 
    .shiftreg (shiftreg),
    .data (data), 
    .sel (sel));

 //this process block sets up the free running clock
 initial begin
     clock = 0;
     forever #50 clock = ~clock;
 end

 initial begin// this process block specifies the stimulus. 
     reset = 1;
     data = 5'b00000;
     load = 0;
     sel = 2'b00;
     #200
     reset = 0;
     load = 1;
     #200
     data = 5'b00001;
     #100
     sel = 2'b01;
     load = 0;
     #200
     sel = 2'b10;
     #1000 $stop;
 end

 initial begin// this process block pipes the ASCII results to the 
//terminal or text editor
     $timeformat(-9,1,"ns",12);
     $display(" Time Clk Rst Ld SftRg Data Sel");
     $monitor("%t %b %b %b %b %b %b", $realtime,
     clock, reset, load, shiftreg, data, sel);
 end

 endmodule

上面的testbenches例化了被测设计,设置了时钟,并提供了测试刺激。所有过程块从仿真的零时开始,并且是并发的。井号(#)指定应用下一个刺激之前的仿真时间延迟。 s t o p 命令指示仿真工具停止 t e s t b e n c h e s 仿真 ( 所有 t e s t b e n c h e s 都应该包含一个停止命令 ) 。最后, stop命令指示仿真工具停止testbenches仿真(所有testbenches都应该包含一个停止命令)。最后, stop命令指示仿真工具停止testbenches仿真(所有testbenches都应该包含一个停止命令)。最后,monitor语句将ASCII格式的结果回显给屏幕或文本文件。

上面这个testbench算是一个比较经典且简单的测试结构,被测模块、测试激励、时钟和结果监控都有了,可以作为一个testbench的一般模板来使用。


自动化验证

建议自动化验证测试结果,特别是对于较大型的设计。自动化验证减少了检查设计正确性所需的时间,并将人为出错的可能性降至最低。自动化testbenches验证常用的方法有:

  • 数据库比较。首先,创建一个包含预期输出的数据库文件。然后,获取仿真输出,并与文件中的参考值与实际结果进行比较(可借助脚本工具)。但是,由于没有提供从输出到输入文件的指针,这种方法的缺点是很难跟踪错误来源。
  • 波形的比较。波形比较可以自动进行,也可以手动进行。该方法使用testbenches比较器将预期波形与testbenches的输出波形进行比较。Xilinx HDL bench工具可以实现这一自动化功能。
  • 自检Testbenches。自检Testbenches在运行时,将实际结果和预期结果进行实时比对。因为可以在测试台中构建有用的错误跟踪信息来显示设计失败的地方,所以调试时间被显著缩短。

自检Testbenches是通过在测试台文件中放置一系列期望的向量来实现的。这些向量在定义的运行时间隔与实际模拟结果进行比较。如果实际结果与预期结果匹配,则仿真成功。如果结果没有匹配预期,则Testbenches会报告差异的地方。

实现自检Testbenches对于同步设计来说更简单,因为预期和实际结果可以在一个时钟边缘或每“n”个时钟周期之后进行比较。比较方法也取决于设计的性质。例如,内存I/O的Testbenches应该在每次向内存位置写入新数据或从内存位置读取新数据时检查结果。如果一个设计使用了大量的组合块,在指定预期结果时则必须考虑组合延迟。

在自检Testbenches中,预期输出将与定期运行时的实际输出进行比较,以提供自动错误检查。这种技术适用于中小型设计。然而,由于可能的输出组合随着设计的复杂性呈指数级增长,为大型设计编写一个自检Testbenches就变得更加困难和耗时。

下面是用Verilog编写的简单的自检Testbenches的例子:

在例化被测模块后,将设定预期的结果。在后面的代码中,将预期结果和实际结果进行比较,并将结果反馈给终端。如果每一次结构都匹配,就会显示“end of good simulation”消息。如果发生不匹配,则报告一个错误以及不匹配的期望值和实际值。

`timescale 1 ns / 1 ps
module test_sc;
 reg tbreset, tbstrtstop;
 reg tbclk;
 wire [6:0] onesout, tensout;
 wire [9:0] tbtenthsout;
parameter cycles = 25;
reg [9:0] Data_in_t [0:cycles];
// /
// Instantiation of the Design
// /
stopwatch UUT (.CLK (tbclk), .RESET (tbreset), .STRTSTOP (tbstrtstop),
 .ONESOUT (onesout), .TENSOUT (tensout), .TENTHSOUT (tbtenthsout));
 wire [4:0] tbonesout, tbtensout;
 assign tbtensout = led2hex(tensout);
 assign tbonesout = led2hex(onesout);
///
//EXPECTED RESULTS
///
initial begin
 Data_in_t[1] =10'b1111111110; 
 Data_in_t[2] =10'b1111111101; 
 Data_in_t[3] =10'b1111111011; 
 Data_in_t[4] =10'b1111110111;
 Data_in_t[5] =10'b1111101111; 
 Data_in_t[6] =10'b1111011111;
 Data_in_t[7] =10'b1110111111; 
 Data_in_t[8] =10'b1101111111; 
 Data_in_t[9] =10'b1011111111; 
 Data_in_t[10]=10'b0111111111;
 Data_in_t[11]=10'b1111111110; 
 Data_in_t[12]=10'b1111111110; 
 Data_in_t[13]=10'b1111111101; 
 Data_in_t[14]=10'b1111111011;
 Data_in_t[15]=10'b1111110111; 
 Data_in_t[16]=10'b1111101111;
 Data_in_t[17]=10'b1111011111; 
 Data_in_t[18]=10'b1110111111; 
 Data_in_t[19]=10'b1101111111; 
 Data_in_t[20]=10'b1011111111; 
 Data_in_t[21]=10'b0111111111; 
 Data_in_t[22]=10'b1111111110; 
 Data_in_t[23]=10'b1111111110; 
 Data_in_t[24]=10'b1111111101;
 Data_in_t[25]=10'b1111111011;
end
reg GSR;
assign glbl.GSR = GSR;
initial begin
 GSR = 1;
 // ///
 // Wait till Global Reset Finished
 // ///
 #100 GSR = 0;
end
 
// 
// Create the clock
// 
initial begin
 tbclk = 0;
 // Wait till Global Reset Finished, then cycle clock
 #100 forever #60 tbclk = ~tbclk;
end
initial begin
 // //
 // Initialize All Input Ports
 // //
 tbreset = 1;
 tbstrtstop = 1;
 // /
 // Apply Design Stimulus
 // /
 #240 tbreset = 0;
 tbstrtstop = 0;
 #5000 tbstrtstop = 1;
 #8125 tbstrtstop = 0;
 #500 tbstrtstop = 1;
 #875 tbreset = 1;
 #375 tbreset = 0;
 #700 tbstrtstop = 0;
 #550 tbstrtstop = 1;
 // /
 // simulation must be halted inside an initial statement
 // /
// #100000 $stop;
end
integer i,errors;
///
///
// Block below compares the expected vs. actual results
// at every negative clock edge.
///
///
always @ (posedge tbclk)
begin
 if (tbstrtstop)
 begin
 i = 0;
 errors = 0;
 end
 else 
 begin
 for (i = 1; i <= cycles; i = i + 1)
 begin
 @(negedge tbclk) 
 // check result at negedge
 $display("Time%d ns; TBSTRTSTOP=%b; Reset=%h; Expected 
TenthsOut=%b; Actual TenthsOut=%b", $stime, tbstrtstop, tbreset, 
Data_in_t[i], tbtenthsout);
 if ( tbtenthsout !== Data_in_t[i] )
 begin
 $display(" ------ERROR. A mismatch has occurred-----");
 errors = errors + 1;
 end
 end
 if (errors == 0)
 $display("Simulation finished Successfully.");
 else if (errors > 1)
 $display("%0d ERROR! See log above for details.",errors);
 else
 $display("ERROR! See log above for details."); 
#100 $stop; 
 end
end
endmodule

这种简单的自检Testbenches设计可以移植到任何测试用例中——当然,预期输出值和信号名称必须修改才能重用。如果不需要在每个时钟边缘进行检查,则可以根据需要修改for循环。

如果仿真成功,终端屏幕会显示如下信息:

自动化验证听起来挺唬人的,实际上简单的很:就是我先把所有预期的结果穷举出来(如果有必要的话,比如其没有规律性),然后再把得到的测试结果与其一一进行比较,如果比较结果是对的,那么我就输出高电平或者低电平;如果不对,那么我就输出相反的电平。这样,我们只需要监控这个信号,就可以知道我们的设计是否是正确的。


编写testbenches的指导方针

本节提供了编写测试工作台的指导方针。正如规划一个电路设计的布局有助于实现更好的电路性能一样,规划一个testbenches的布局同样可以改善仿真验证过程。

  • (1)在编写testbenches前多了解仿真工具

尽管常用的仿真工具符合HDL行业标准,但这些标准没有解决几个重要的特定仿真问题。不同的仿真工具具有不同的特性、性能和性能特征,则会导致其产生不同的仿真结果。

····基于事件VS基于循环的仿真

基于事件的仿真在输入、信号或门电路改变值时调度一个仿真事件。在基于事件的仿真中,延迟值可以与门电路和线网相关联,以实现最佳的定时仿真。

基于循环的仿真以同步设计为目标。它们优化组合逻辑,并在时钟周期分析结果。这个特性使得基于循环的仿真比基于事件的仿真更快,内存效率更高。

----调度事件

基于事件的仿真工具供应商使用不同的算法来调度仿真事件。因此,根据仿真工具使用的调度算法,在相同的仿真时间发生的事件可以以不同的顺序被调度(在每个事件之间插入                  delta延迟)。为了避免算法依赖和确保正确的结果,事件驱动的testbenches应该指定一个显式的刺激序列。

(2)避免使用无限循环

当事件添加到基于事件的仿真工具时,CPU和内存使用量会增加,处理会变慢。除非对testbenches至关重要,否则不应该使用无限循环来提供测试刺激。通常,时钟是在一个无限循环中指定的(例如,Verilog中的“forever”循环),而不是其他信号事件。

(3)将测试激励分解成不同的逻辑块

所有初始块(Verilog)都并行运行。如果不相关的激励被划分成单独的块,testbenches激励序列将变得更容易执行和检查。由于每个并发块都是相对于零仿真时间运行的,因此使用单独的块传递激励更容易。使用单独的测试激励块会产生更容易创建、维护和升级的testbenches。

(4)不要显示不重要的数据

大型设计的testbenches可能包含超过100,000个事件和大量信号。显示大量的仿真数据会大大降低仿真速度。最好每“n”个时钟周期只采样相关信号,以保证足够的仿真速度。


高级testbenches技术

用任务(Tasks)分解测试激励

当创建大型testbenches时,测试激励应该被划分成不同的子模块,以使得代码清晰和方便修改。任务(task)可用于对信号进行划分。在下面的例子中,测试台模拟了SDRAM控制器的设计。该设计包括重复激励块,因此testbench通过声明单独的任务对激励进行划分,这些任务稍后在testbench中调用,以实现单独的设计功能。

task addr_wr;
 input [31 : 0] address;
 begin
 data_addr_n = 0;
 we_rn = 1;
 ad = address;
 end
 endtask
task data_wr;
 input [31 : 0] data_in;
 begin
 data_addr_n = 1;


![img](https://img-blog.csdnimg.cn/img_convert/1d39e02f50eb1c35e80befef91a760dc.png)
![img](https://img-blog.csdnimg.cn/img_convert/5e9e05d74ef44c812fddeaac2d7f0a69.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

。该设计包括重复激励块,因此testbench通过声明单独的任务对激励进行划分,这些任务稍后在testbench中调用,以实现单独的设计功能。



task addr_wr;
input [31 : 0] address;
begin
data_addr_n = 0;
we_rn = 1;
ad = address;
end
endtask
task data_wr;
input [31 : 0] data_in;
begin
data_addr_n = 1;

[外链图片转存中…(img-QW5otASJ-1715821243338)]
[外链图片转存中…(img-U8gaFgE2-1715821243338)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值