7.5 仿真测试文件(Testbench)的设计方法
7.5.1 测试文件的用途
随着设计量和复杂度的不断增加,数字设计验证变得越来越难,所消耗的成本也越来越高。面对这种挑战,验证工程师必须依靠相应的验证工具和方法才行。对于大型的设计,比如上百万门的设计验证,工程师必须使用一整套规范的验证工具;而对于较小的设计,使用具有HDL testbench的仿真器是一个不错的选择。
Testbench已经成为了HLL(High-Level-Language,高级语言)设计标准验证方法。一般来说,它能够完成下面一些任务。
· 实例化DUT(Design Under Test,被测设计)。
· 通过为模块添加测试向量对DUT进行仿真。
· 通过终端或波形窗口提供仿真结果。
· 比较实际输出与期望输出的差异。
一般来说,Testbench使用工业标准VHDL或者Verilog HDL语言来描述。简单的Testbench通过调用用户设计的功能模块,然后进行仿真。较为复杂的Testbench还包括一些其他的功能,比如包含特定的激励向量或者进行实际输出与期望输出的比较等。
如图7.32所示是一个标准的HDL验证流程,图中表达了上述的Testbench功能。
图7.32 HDL Testbench仿真流程
由于Testbench是使用VHDL或者Verilog HDL编写的,因此Testbench的验证流程是跨平台的。同时由于这两种语言是标准的非私有语言,所以使用它们进行验证的流程可以在未来的设计中继续使用。
下面讲解Testbench的设计方法。
7.5.2 测试文件设计方法
由于测试文件只是在仿真时使用,因此在综合中对RTL语言的语法限制在测试文件中并不存在。相反地,测试文件可以编写得更加通俗,所有的行为结构语法都可以使用,使之更加容易理解。所有的测试文件都包含了如表7.1所示的结构。
表7.1 测试文件的共同结构
VHDL |
Verilog HDL |
实体和结构声明(Entity,Architecture) |
模块声明(Module) |
信号声明(Signal) |
信号生命(Signal) |
顶层模块实例化 |
顶层模块实例化 |
激励向量 |
激励向量 |
正如所述,测试文件除了包含这些共有的结构意外,还完成一些额外的工作。例如显示结果、嵌入的误码检测等。下面通过一些例子来展现测试文件中常用的结构。
1.产生测试时钟
仿真必须产生时钟才能进行,只有在推进的仿真时钟中才可以判定输出的结果是否符合设计的要求。交互时钟是硬件设计语言里面最容易实现的时钟,下面通过Verilog语言的例子来说明如何生成测试时钟。
Parameter ClockPeriod = 10; // 声明时钟周期常量
//时钟生成方法1:
initial begin
forever Clock = #(ClockPeriod / 2) ~ Clock;
end
//时钟生成方法2:
initial begin
always #(ClockPeriod / 2) Clock = ~Clock;
end
在两种实现方法中,都是用了initial块语句。在方法1中,使用了forever语句,是最常用的产生时钟的方法。在方法2中,使用过了always语句,同样也实现了时钟的生成。两种方法都产生了周期为10 的时钟波形。
2.提供激励源
为了得到Testbench的验证结果,必须为DUT提供激励向量。并行激励模块常常被用来为测试文件提供必要的激励。有两种不同的方法来实现并行的激励:一种是绝对时间激励,另一种是相对时间激励。
在绝对时间激励中,所有的仿真时间值都是相对仿真时间零点定义的。而在相对时间激励中,一般会提供初始化值,然后等待事件来触发激励向量。两种方法都可以根据设计者的需要在同一个测试文件中使用。
下面分别是两种方法产生激励的例子。
(1)绝对时间。
initial begin
Reset = 1; //仿真时间零点激励
Load = 0; //仿真时间零点激励
Count_UpDn = 0; //仿真时间零点激励
#100 Reset = 0; //绝对时间100激励
#20 Load = 1; //绝对时间120激励,相对上一个时间点20
#20 Count_UpDn = 1; //绝对时间140激励,相对上一个时间点20
end
(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 HDL语言的initial模块之间是并行执行的,但是initial模块内部是顺序执行的。也就是说,测试文件的激励顺序在仿真时间零点同时启动并行模块,然后根据各个模块的内部激励顺序产生激励向量。
3.结果输出
测试文件通过关键词$display和$monitor来实现结果的输出。下面是使用Verilog HDL语言实现在终端上显示结果的例子:
// 在终端中打印信号的ASCII值
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则不同,因此它的输出是事件驱动的。在例子中,$monitor信号列表中的$realtime信号变化会触发终端显示事件的发生,该信号被设计者对应到仿真时间中,每次$monitor的触发将会把信号列表中的信号值显示在终端中。
$monitor语句中的“%”用于定义信号列表中信号的输出格式。例如,%t将信号按照时间格式输出,%b将信号按照二进制格式输出。另外,Verilog HDL语言还提供了其他的输出格式,比如%h为十六进制输出,%d为十进制输出,%o为八进制输出等。更为详细的格式输出定义可以参看Verilog参考手册。
7.5.3 测试常用语句
常用的Verilog测试用结构语句,比如$monitor、$display和$time在上面已经介绍过了。下面再来介绍一些其他的常用语句。
1.force/release
force和release语句可以用来强制对执行过程中的寄存器或网络型信号量赋值。这两条语句共同完成一个强制赋值的过程。当一个被force的信号被release以后,这个信号将会保持当时的状态直到下一个赋值语句产生为止。
下面举个例子来说明这两条语句的使用。
module testbench;
...
initial begin
reset = 1; //在仿真时间零点将reset激励为1
force DataOut = 101; //在仿真时间零点强制使DataOut为101,并保持
#25 reset = 0; //在仿真绝对时间25将reset激励为0
#25 release DataOut; //在仿真绝对时间50释放
// DataOut值将保持直至下一个对它的赋值语句
...
end
endmodule
2.assign/deassign
assign/deassign语句与force/release语句相类似,不过assign/deassign语句只能对设计中的寄存器型信号赋值。它们常常被用来设置输入值。
下面是这两个语句的例子。
module testbench;
...
initial begin
reset = 1; //绝对时间零点对reset赋值1
DataOut = 101;
#25 reset = 0; //绝对时间25对reset赋值0
release DataOut;
...
end
initial begin
#20 assign reset = 1; //此条语句覆盖之前的赋值语句(即绝对时间零点的赋值)
#25 reset = 0; //绝对时间45对reset赋值0
#50 release reset; //绝对时间95释放reset信号
endmodule
3.timescales
timescale语句用于定义测试文件的单位时间,同时也对仿真的精度有影响。它的语法定义如下:
‘timescale reference_time/precision
其中,reference_time是单位时间的度量,precision决定了仿真的推进延迟精度,同时也设置了仿真的推进步进单位。下面是timescale语句的使用范例:
‘timescale 1 ns / 1 ps //度量参考为1ns,精度为1ps
module testbench;
...
initial begin
#5 reset = 1; // 5个仿真时间延迟,相当于5×1ns = 5ns 的仿真时间
#10 reset = 0;
...
end
initial begin
//display语句将在每一个仿真推进步进中执行,也就是1ps执行一次
$display (“%d , Reset = %b”, $time, reset);
end
endmodule
应该注意的是,如果仿真中使用了时间延迟值,那么仿真的精度应大于最小的延迟值。例如仿真中使用了9ps的仿真时间延迟,那么仿真的推进步进精度必须为1ps来保证9ps的延迟。
4.Reading Memory Initialization Files
Verilog HDL提供了$readmemb和$readmemh命令来读取ASCII文件,用于初始化memory的内容。这两个语句可以用于初始化FPGA中由IP Core生成的存储器宏模块,例如RAM、ROM等。
下面是利用这个语句对Xilinx的实例(design_instance)进行初始化的例子。
$readmemb (“<design.mif>”, design_instance);
其中,mif文件是Xilinx的Core Generator建立的对存储器进行初始化的文件。用户也可以自己编写这个文件的内容。