编写高效的Testbench

本文转载自:FPGA技术实战

引言:Testbench是验证HDL设计的主要手段,本文提供了布局和构建高效Testbench的指南以及示例。另外,本文还提供了一种示例,可以为任何设计开发自检Testbench。

1. 概述
由于FPGA设计规模和复杂性的增加,数字设计验证已成为一项越来越困难和费力的任务。为了应对这一挑战,验证工程师依靠多种验证工具和方法。对于大型、数百万门的设计,工程师通常使用一套正式的验证工具。然而,对于较小的设计,设计工程师通常使用带有Testbench的HDL仿真器效果最好,如Modelsim、Vivado Simulator等。

Testbench已成为验证HLL(高级语言)设计的标准方法。通常,Testbench执行以下任务:

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

通常,Testbench是用行业标准VHDL或Verilog硬件描述语言编写的,Testbench调用功能设计,然后对其进行仿真。复杂的Testbench还执行其他功能,例如,将实际结果与预期结果进行比较的逻辑。图1显示了遵循上述步骤的标准HDL验证流程。

图1:使用Testbench的HDL验证流程

由于Testbench是用VHDL或Verilog编写的,因此Testbench验证流程可以跨平台和供应商工具移植。此外,由于VHDL和Verilog是标准的非专有语言,因此用VHDL或Verilog编写的验证套件可以在未来的设计中毫无困难地重用。

2. Testbench构建
由于Testbench仅用于仿真,因此它们不受适用于综合中使用的RTL语言子集的语义约束的限制。相反,可以使用所有行为构造。因此,Testbench可以更通用地编写,使其更容易维护。所有Testbench均包含表1所示的基本部分。

表1:试验台通用截面

2.1 生成时钟信号
激励时钟可以很容易地在VHDL和Verilog源代码中实现。以下是Testbench中经常使用的时钟生成VHDL和Verilog示例:

VHDL:

Verilog:

2.2 提供激励
为了获得Testbench验证结果,须向DUT提供激励。在Testbench上使用并发激励块来提供,通常采用两种方法:绝对时间激励和相对时间激励。在第一种方法中,相对于仿真时间零点指定模拟值。相比之下,相对时间激励提供初始值,然后在重新触发激励之前等待事件。根据设计师的需求,这两种方法可以在Testbench上结合使用。

表2和表3分别提供了VHDL和Verilog源代码中的绝对时间和相对时间激励的示例。

表2:绝对时间激励示例

表3:相对时间激励示例

VHDL过程块和Verilog初始块与文件中的其他过程和初始块同时执行。然而,在每个(进程或初始)块中,事件是按照写入的顺序执行。这意味着激励序列在模拟时间零点开始于每个并发块。推荐使用多个块将复杂的激励序列分解为更可读和可维护的代码块。

2.3 结果显示
Verilog中的$display和$monitor关键字用于显示结果。虽然VHDL没有等效的显示特定命令,但它提供了std_textio包,该包允许将文件I/O重定向到显示终端窗口(有关此技术的示例,请参阅下面的自检Testbench)。

以下是Verilog示例,其中值显示在终端屏幕上:

$display关键字将带引号的括号文本(“…”)输出到终端窗口。$monitor关键字的工作方式不同,因为它的输出是事件驱动的。在该示例中,$realtime变量(由用户分配给当前模拟时间)用于触发信号列表中值的显示。Verilog提供了额外的格式说明符,例如,%h用于十六进制,%d用于十进制,%o用于八进制格式。格式化的显示结果如图2所示。

图2:仿真结果输出到终端

2.4 其他语句结构
(1)force/release
force/release语句可用于覆盖对寄存器或网络进行的程序分配。这些构造通常用于强制特定的设计行为。一旦强制值被释放,信号将保持其状态,直到新值通过过程赋值传递。以下是强制和释放语句使用的示例:

(2)assign/deassign
赋值和取消赋值语句类似于强制和释放语句,但赋值和取消指派仅适用于设计中的寄存器。它们通常用于设置输入值。与强制语句一样,assign语句会覆盖过程语句传递的值。以下是赋值和取消赋值语句用法的示例。

(3)timescales
时间刻度指令用于指定Testbench的单位时间步长。它也会影响仿真器的精度。此指令的语法为:

`timescale reference_time/precision

reference_time是测量的单位时间。精度决定延迟四舍五入的精度,并设置仿真的单位时间步长。以下是“时间刻度用法”的示例:

如果仿真使用定时延迟值,则模拟必须以大于最小延迟的精度运行(以便包含延迟)。例如,如果在仿真库中使用9ps延迟,则仿真的精度必须为1ps才能适应9ps延迟。

(4)读取内存初始化文件
Verilog提供$readmemb和$readmemh命令来读取ASCII文件以初始化内存内容。此命令可用于在模拟中初始化Xilinx BlockRAM或SelectRAM组件。语法如下:

$readmemb(“”,design_instance);

3. 简单Testbench
简单的Testbench实例化用户设计,然后为其提供激励。Testbench输出以图形方式显示在仿真器的波形窗口上,或作为文本发送到用户终端或文本文件。

下面是一个代表移位寄存器的简单Verilog设计:

以下简单的Testbench示例实例化了移位寄存器设计。

上面的Testbench实例化设计,设置时钟,然后提供激励。所有过程块都从模拟时间零点开始,并且是并发的。符号(#)指定应用下一个激励之前的延迟。$stop命令指示仿真器停止Testbench仿真(所有Testbench都应包含stop命令)。最后,$monitor语句将ASCII格式的结果返回到屏幕或本编辑器。

下面是一个VHDL Testbench,它实例化并为上述Verilog移位寄存器设计提供激励。

4. 自动验证

建议自动化Testbench结果验证,特别是对于较大的设计。自动化减少了检查设计正确性所需的时间,并最大限度地减少了人为错误。通常使用几种方法来自动化Testbench验证:
(1)数据库比较:首先,创建一个包含预期输出的数据库文件(“黄金向量”文件)。然后,捕获仿真输出并将其与黄金向量文件中的参考向量进行比较。这种方法的缺点是由于没有提供从输出到输入文件的指针,难以将不正确的输出追踪到错误的来源。

(2)波形比较:波形比较可以自动或手动执行。自动方法采用Testbench比较器将黄金波形与Testbench输出波形进行比较。Xilinx HDL Bencher工具可用于执行自动波形比较。

(3)Self-Checking Testbench。自检Testbench在运行时,而不是在仿真结束时,根据实际结果检查预期结果。由于可以在Testbench上构建有用的错误跟踪信息来显示设计失败的地方,因此调试时间大大缩短。

5. Self-Checking Testbench
自检Testbench是通过在Testbench文件中放置一系列预期向量来实现的。这些向量在定义的运行时间间隔与实际仿真结果进行比较。如果实际结果与预期结果匹配,则仿真成功。如果结果与预期不符,Testbench会报告差异。

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

在自检自检Testbench上,以规则的运行时间间隔将预期输出与实际输出进行比较,以提供自动错误检查。这种技术在中小型设计中效果很好。然而,由于可能的输出组合随着设计复杂性呈指数级增长,为大型设计编写自检Testbench变得更加困难和耗时。

以下是用Verilog和VHDL编写的简单自检Testbench的示例:

Verilog示例:
这种简单的自检Testbench设计可以移植到任何测试用例中。如果不需要在每个时钟沿进行检查,则可以根据需要修改for循环。如果仿真成功,终端屏幕上会显示以下信息:

VHDL示例:

如果检测到错误,则会在仿真器提示下显示:

6. 编写Testbench指南
本节提供了编写Testbench的指南,规划Testbench布局可以提高仿真验证结果。

(1)在编写Testbench之前,了解仿真器。
尽管常用的仿真工具符合HDL行业标准,但这些标准并没有解决几个重要的仿真特定问题。不同的模拟器具有不同的特性、能力和性能特征,并产生不同的仿真结果。

(2)基于事件的仿真与基于周期的仿真
仿真器器使用基于事件或基于周期的仿真方法。基于事件的仿真器在输入、信号或门改变值时安排仿真器事件。在基于事件的仿真器中,延迟值可以与门和网络相关联,以实现最佳的时序模拟。基于循环的仿真器以同步设计为目标。它们优化组合逻辑,并在时钟周期内分析结果。此功能使基于循环的仿真器比基于事件的仿真器更快、更节省内存。然而,由于基于循环的仿真器不允许详细的时序特异性,因此它们的准确性不高。

(3)避免使用无限循环
当一个事件被添加到基于事件的仿真器中时,CPU和内存的使用率会增加,仿真处理速度也会减慢。除非对Testbench至关重要,否则不应使用无限循环来提供设计激励。通常,时钟是在无限循环内指定的(例如,Verilog中的“永远”循环),而不是其他信号事件。

(4)将激励分解为逻辑块
在Testbench中,所有初始(Verilog)和过程(VHDL)块同时运行。如果将无关的激励分为单独的块,则Testbench激励序列更容易实施和审查。由于每个并发块都相对于模拟时间零点运行,因此使用单独的块更容易传递激励。使用单独的激励块可以使Testbench更容易创建、维护和升级。

(5)避免显示不重要的数据
大型设计的Testbench可能包含超过100000个事件和大量信号。显示大量仿真数据会大大减慢仿真速度。最好每“n”个时钟周期只对相关信号进行采样,以确保足够的仿真速度。

7.高级Testbench技术
(1)用task任务和process过程模块化激励
在创建更大的Testbench时,应该对激励进行分区,以帮助代码清晰并便于修改,并使代码更具可读性。以下示例中,Testbench模拟了SDRAM控制器的设计。该设计包括重复激励块,因此Testbench通过声明单独的任务来划分激励,这些任务稍后在Testbench中调用以执行单独的设计功能。

Verilog示例:

这些任务指定了设计功能的单独元素,包括读写、数据读写或nop(无操作)。一旦指定,这些任务可以在激励过程中调用,如下所示:

VHDL示例:

(2)仿真中双向信号的控制
大多数设计使用双向信号,在Testbench上必须与单向信号区别对待。

VHDL示例:

要访问上述示例中的双向DATA信号,可以按如下方式设置Testbench:

双向总线由Testbench控制,双向总线的值通过data_top信号访问。

Verilog示例:

Verilog Testbench可以按如下方式设置:

8.编码风格指南
(1)缩进
始终缩进代码以使其更具可读性。建议缩进宽度为三到四个空格。缩进宽度为五个或更多空格通常会在右边距留下很少的空间,而缩进宽度小于三个空格则会导致缩进太小。

(2)文件命名
在源文件名中始终保持“.v”(Verilog)或“.vhd”(VHDL)文件扩展名。如果这些标准扩展名被更改,一些编辑器和过滤器将无法识别源文件。

(3)信号命名
对所有用户信号使用相同的大小写(建议使用小写)。Verilog是区分大小写的,错误的大写可能会导致设计在综合和仿真中失败。此外,使用一致的信号名称格式样式使信号名称更容易在源文件中定位。使用简短的描述性信号名称。短名称更容易输入,描述性名称有助于记录信号功能。

(4)注释
自由地注释Testbench代码。注释对于继承和重用代码的其他人来说是无价的,注释代码填充了重要的细节,大大提高了源代码的清晰度和可重用性

(5)设计结构
为每个模块或实体保留一个物理文件。单独的模块和实体的单独文件使设计更容易维护。