编写高效率的testbench
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
编写高效率的testbench
简介:
由于设计的规模越来越大也越来越复杂,数字设计的验证已经成为一个日益困难和繁琐的任务。
验证工程师们依靠一些验证工具和方法来应付这个挑战。
对于几百万门的大型设计,工程师们一般使用一套形式验证(formal verification)工具。
然而对于一些小型的设计,设计工程师常常发现用带有testbench的HDL仿真器就可以很好地进行验证。
Testbench已经成为一个验证高级语言(HLL --High-Level Language) 设计的标准方法。
通常testbench完成如下的任务:
1.实例化需要测试的设计(DUT);
2.通过对DUT模型加载测试向量来仿真设计;
3.将输出结果到终端或波形窗口中加以视觉检视;
4.另外,将实际结果和预期结果进行比较。
通常testbench用工业标准的VHDL或Verilog硬件描述语言来编写。
Testbench调用功能设计,然后进行仿真。
复杂的testbench完成一些附加的功能—例如它们包含一些逻辑来选择产生合适的设计激励或比较实际结果和预期结果。
后续的章节描述了一个仔细构建的testbench的结构,并且提供了一个自动比较实际结果与预期结果的进行自我检查的testbench例子。
图1给出了一个如上所描述步骤的标准HDL验证流程。
由于testbench使用VHDL或Verilog来描述,testbench的验证过程可以根据不同的平台或不同的软件工具实现。
由于VHDL或Verilog是公开的通用标准,使用VHDL或Verilog编写的testbench以后也可以毫无困难地重用(reuse)。
图1使用Testbench的HDL验证流程
构建Testbench
Testbench用VHDL或Verilog来编写。
由于testbench只用来进行仿真,它们没有那些适用于综合的RTL语言子集的语法约束限制,而是所有的行为结构都可以使用。
因而testbench可以编写的更为通用,使得它们可以更容易维护。
所有testbench包含了如表1的基本程序段。
正如上面所提到的,testbench通常包含附加功能,如在终端上可视的结果和内建的错误检测。
表1 testbench的基本程序段
下面的例子介绍testbench中经常使用的结构。
产生时钟信号
使用系统时钟的时序逻辑设计必须产生时钟。
时钟信号在VHDL或Verilog中可以很容易地实现。
以下是VHDL和Verilog的时钟发生示例。
VHDL:
-- Declare a clock period constant.
Constant ClockPeriod : TIME := 10 ns;
-- Clock Generation method 1:
Clock <= not Clock after ClockPeriod / 2;
-- Clock Generation method 2:
GENERATE CLOCK: process
begin
wait for (ClockPeriod / 2)
Clock <= ’1’;
wait for (ClockPeriod / 2)
Clock <= ’0’;
end process;
Verilog:
// Declare a clock period constant.
Parameter ClockPeriod = 10;
// Clock Generation method 1:
initial begin
Clock = 0;
forever Clock = #(ClockPeriod / 2) ~ Clock;
end
// Clock Generation method 2:
always #(ClockPeriod / 2) Clock = ~Clock;
提供激励信号
为了获得testbench的验证结果,激励必须作用于DUT。
在testbench中使用的并行激励块提供必要的激励。
激励的产生可以采用两个方法:绝对时间激励和相对时间激励。
在第一个方法里,仿真变量相对于仿真时间零点进行详细描述。
相对而言,相对时间激励提供初始值,然后等待一个事件来重新触发激励。
根据设计者的需要,两种方法可以在testbench中同时使用。
下面的程序段是绝对时间激励的例子。
initial begin
reset = 1;
load = 0;
cout_updn = 0;
#100 reset = 0;
#20 load = 1;
#20 count_updn = 1;
end
下面的程序段是相对时间激励的例子。
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中可以非常方便地使用系统函数$display()和$monitor()显示结果。
VHDL 没有等效的显示指令,它提供了std_textio标准文本输入输出程序包。
它允许文件的i/o重定向到显示终端窗口(作为这个技术的示例,参看下面的自较验查验证设计)下面是verilog示例,它将在终端屏幕上显示一些值。
// 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
系统函数$display---在终端屏幕上输出引用的附加说明文字(“。
”)。
系统函数$monitor 操作不同。
因为它的输出是事件驱动的。
例中的变量$realtime(由用户赋值到当前的仿真时间)用于触发信号列表中值的显示。
信号表由变量$realtime开始,跟随其他将要显示的信号名(clock, reset, load等)。
以%开始的关键字包含一个格式描述的表,用来控制如何格式化显示信号列表中的每个信号的值。
格式列表是位置确定的。
每个格式说明有序地与信号列表中的信号顺序相关。
比如%t说明规定了$realtime的值是时间格式。
并且第一个%b说明符格式化clock的值是二进制形式。
Verilog提供附加的格式说明,比如%h用于说明十六进制,%d 说明十进制,%c说明显示为八进制。
图2说明格式显示结果
图2仿真结果
简单的testbench
简单的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 = shift reg >> 1;
default : shiftreg = shiftreg;
endcase
end
endmodule
以下是简单的testbench,示例移位寄存器设计的例子verilog描述。
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
以上的testbench实例化设计,设置时钟,提供激励信号。
所有的进程块在仿真时间零点开始。
英镑标记(#)说明下一个激励作用前的延迟。
$stop命令使仿真器停止测试仿真(所有测试设计中都应该包含一个停止命令)。
最后,$monitor语句返回ascII格式的结果到屏幕或者管道输出到一个文本编辑器。
自动验证
推荐自动实现测试结果的验证,尤其是对于较大的设计来说。
自动化减少了检查设计是否正确所要求的时间,也使人可能的犯错最少。
一般有以下几种常用的自动测试验证的方法:
1、数据库比较(database comparison)。
首先,要创建一个包含预期输出的数据库文件。
然后,仿真输出被捕获并与预期输出数据库文件中参考的向量比较。
然而,因为从输出到输入文件指针没有提供,是这种方法的一个缺点,使得跟踪一个导致错误输出的原因比较困难。
2、波形比较(waveform comparison)。
波形比较可以自动或是手动的运行。
自动的方法使用一个测试比较器来比较预期输出波形与测试输出波形。
3、自我检查测试平台(self-checking testbenches)。
一个自我检查testbench检查预期的结果与运行时间的实际结果,并不是在仿真结束以后。
因为有用的错误跟踪信息可以内建在一个测试设计中,用来说明哪些地方设计有误,调试时间可以非常明显地缩短。
自我检查testbenches
自我检查testbench通过在一个测试文档中放置一系列的预期向量表来实现。
运行时按定义好的时间间隔将这些向量与实际仿真结果进行比较。
如果实际结果与预期结果匹配,仿真成功。
如果结果不匹配,则报告两者的差异。
对于同步设计,实现自我检查testbench会更简单一些,因为与实现的结果相比较可以在时钟沿或每个几个时钟进行。
比较的方法基于设计本身的特性。
比如一个存储器读写的testbench在每次数据写入后者读出时进行检查。
在自我检查的testbench中,预期输出与实际输出在一定的时间间隔比较以便提供自动
的错误检查。
这个技术在小到中型的设计中非常好。
但是,因为当设计复杂后,可能的输出组合成指数倍的增长,为一个大型设计编写一个自我检查的testbench是非常困难和非常费时的。
以下是一个用Verilog描述的简单的自我检查的testbench例子:
下述的设计实例中,预期的结果被详细说明。
后面的代码,两种结果被比较,比较的结果被返回终端。
如果没有错误,一个“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;
Dat a_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;
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
如果仿真成功,下图的信息就会在显示终端上显示:
图3 verilog示例验证
编写testbench的准则
本节罗列一些编写testbench的准则。
正如规划一个电路设计可以帮助得到更好的电路性能,规划好的testbench可以提高仿真验证的效率。
在编写testbench前要了解仿真器
虽然通用仿真工具兼容HDL工业标准,但标准并没有重点强调跟仿真相关的一些主题。
不同的仿真器有不同的特点、功能和执行效率。
对我们而言要全面了解Active-HDL这个工具。
--基于事件vs基于周期的仿真
仿真器使用基于事件或基于周期的仿真方法。
基于事件的仿真器,当输入,信号,或是门改变了值,来确定仿真器事件的时间。
在一个基于事件的仿真器中,一个延时值可以附加
在门电路或是电路网络上来构建最优的时序仿真。
基于周期的仿真器面向同步设计。
这类工具优化组合逻辑,在时钟沿分析结果。
这个功能使得基于周期的仿真器比基于事件的仿真器更快更有效。
--确定事件时间
基于事件的仿真器提供商使用不同的运算法则来确定仿真事件。
所以,根据仿真器用来确定的运算法则不同,同一个仿真时间的事件被确定为不同的次序(根据在每个事件之间插入的delta延时)。
为避免对运算法则的依赖和确保正确的结果,一个事件驱动测试应该详细描述明确的激励顺序。
--避免使用无限循环
当一个事件添加到基于事件的仿真器,cpu和内存的使用就增加了,仿真过程就会变慢。
除非是评价testbench,无限循环不应该使用来作为设计的激励。
一般地,只有时钟被描述成一个无限循环(如'forever'循环)。
--细分激励到逻辑模块
在测试中,所有initial块(verilog)并行执行。
如果无关的激励被分离到独立的块中,测试激励的顺序会变得更容易实现和检查。
因为每个并行的块相关于仿真时间的零点开始执行,对于分离的块传递激励更容易。
使用分离激励块使得testbench的建立,维护和升级更加容易。
--避免显示并不重要的数据
大型设计的测试可能包含10万以上的事件或匿名信号。
显示大量的仿真数据会相当地降低仿真的速度。
高级测试技术
根据任务和过程细分激励模块
在创建一个大的testbench时,激励将会被分割使得代码清晰而易于修改。
Task块可以被用来分割信号。
在下面例子中的testbench用于一个SDRAM控制器的测试。
设计包括重复的激励模块,testbench通过不同的task来划分测试激励。
这些task稍后被调用来进行独立块的功能的测试仿真。
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;
we_rn = 1;
ad = data_in;
end
endtask
task addr_rd;
input [31 : 0] address;
begin
data_addr_n = 0;
we_rn = 0;
ad = address;
end
endtask
task data_rd;
input [31 : 0] data_in;
begin
data_addr_n = 1;
we_rn = 0;
ad = data_in;
end
endtask
task nop;
begin
data_addr_n = 1;
we_rn = 0;
ad = hi_z;
end
endtask
这些任务描述设计功能的独立单元:地址的读写,数据的读写,或者空操作。
当这些task 描述完成后,这些task可以在testbench中被调用。
如下所示:
Initial begin
nop ; // Nop
#( 86* ‘CYCLE +1); addr_wr (32’h20340400); // Precharge, l oad
Controller MR
#(‘CYCLE); data_wr (32’h0704a076); // value for Controller MR
#(‘CYCLE); nop ; // Nop
#(5 * ‘CYCLE); addr_wr (32’h38000000); // Auto Refresh
#(‘CYCLE); data_wr (32’h00000000); //
#(‘CYCLE); nop ; // Nop
…
…
end
细分激励到独立的任务使得激励很容易实现,也使得代码的可读性更好。
在仿真时控制双向信号
多数设计使用双向信号,在testbench中必须区别对待双向信号和单向信号。
双向总线由testbench控制,双向总线的值可以通过数据顶层信号来访问。
以下是一个双向总线示例。
module bidir_infer (DATA, READ_WRITE);
input READ_WRITE ;
inout [1:0] DATA ;
reg [1:0] LATCH_OUT ;
always @ (READ_WRITE or DA TA)
begin
if (READ_WRITE == 1)
LATCH_OUT <= DATA;
end
assign DATA = (READ_WRITE == 1) ? 2’bZ : LATCH_OUT;
endmodule
Verilog 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);
ass ign 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
在这些测试设计中,data_in信号提供激励到设计中的双向DA TA数据信号,data_out 信号读取该DATA数据信号.
Verilog中有用的语法结构
其他有用的Verilog语法结构,如$monitor, $display, 及$time,在前面的verilog测试示例中论述过,这一节说明另外的可以在测试设计中使用的verilog语句结构。
force/release
force/release语句可以用来跨越进程对一个寄存器或一个wire网络的赋值。
这个结构一般用于强制特定的设计行为。
一旦一个强制值释放,这个信号保持它的状态直到新的值被进程赋值。
以下是force/release语句的用法。
module testbench;
..
initial begin
reset = 1;
force DataOut = 101;
#25 reset = 0;
#25 release DataOut;
..
..
end
endmodule
assign/deassign
assign/deassign语句与force/release相类似,但是assign/deassign只用于设计中的寄存器。
他们一般用于设置输入值。
就象一个force语句,assign语句覆盖进程语句的赋值。
以下是一个assign/deassign语句的用法。
module testbench;
..
..
initial begin
reset = 1;
DataOut = 101;
#25 reset = 0;
release DataOut;
..
..
end
initial begin
#20 assign reset = 1;// this assign statement overrides the earlier
statement #25 reset = 0;
#50 release reset;
endmodule
timescales
timescale编译指令用于为testbench指定时间单位时间以及时间精度。
它也影响仿真器的精确度。
表示符号为:‘timescale reference_time/precision
Reference_time是单位时间。
Precision决定延时应该达到的精度。
以下是‘timescale 的使用方法。
‘timescale 1 ns / 1 ps
// this sets the reference time to 1 ns and precision to 1 ps.
module testbench;
..
..
initial begin
#5 reset = 1; // 5 unit time steps correspond to 5 * 1ns = 5ns in
simulation time
#10 reset = 0;
..
end
initial begin
$display (“%d , Reset = %b”, $time, reset); // this display
// statement will get executed
// on every simulator step, ie, 1 ps.
end
endmodule
如果仿真使用时延值,仿真就必须运行在一个比最小时延还好的精确度以内。
例如,如果9ps延时在仿真库中使用,相应仿真的精确度就必须是在1ps到9ps之间可调的范围。
只读储器初始化文件
verilog提供$readmemb和$readmemh命令来读取ascii文件来初始化存储器的内容。
这个命令可以在仿真中用来初始化存储器。
符号表达如下:
$readmemb (“<design.mif>”, design_instance);
编码风格准则
编码风格参考部门的编码规范,以下稍作罗列:
缩进
缩进使代码易读。
文件名
源文件以".v"做文件扩展名。
信号命令
有含义的名称会帮助表明信号的功能。
注释
可以自由地注释testbench文件代码。
注释能描述代码重要的设计细节,极大地增加了源代码的清晰性和可重用性
结语
Testbenches给工程师提供了一个可移动,可升级的验证流程。
好的testbench可以模拟实际的硬件环境,并模拟硬件环境对DUT进行更有实际意义的测试。
当设计出现bug时可以更加真实更加方便在仿真环境中将问题定位和解决。
Testbench实际是逻辑设计的一个重要组成部分。