前言
提到FPGA逻辑的仿真,一般指的是行为仿真或者功能仿真,还有人会称为前仿,不包含时间延迟信息,只验证逻辑功能。对于小模块的仿真,需要写一个测试文件,英文是testbench,即测试平台。在testbench里面,我们给输入信号的激励,给时钟信号等等,然后观察输出,或者处理输出,观察符合不符合预期,达到测试功能的目的。
但是对于大型项目的整体仿真,也可以这样做,但是为了遍历更多的情况,验证更多的功能,对设计给予更充分的验证,我们需要在仿真工具,仿真脚本,以及仿真文件上的编写上给予更多的关注,考虑通用性设计,让验证更有效率,这就要求我们搭建一个仿真平台,最好是更多自动化的。
说到仿真软件,常听说的是Modelsim,也确实如此,这个软件的优点就是效率高,速度快,但是也有一点小小的遗憾是波形不太好看,它的另一个版本增加了更多的功能,叫QuestaSIM,其实就是增加了对system Verilog的支持,对于使用Verilog以及VHDL的用户Modelsim就已经足够了。
上面说到了一个仿真工具,其实对于大厂的FPGA编译软件,是自带仿真功能的,或者说集成了仿真功能,例如Xilinx的工具Vivado,这个工具的缺点很明显,就是速度慢,但是优点也是有的,例如仿真波形确实好看点。
关于二者的仿真软件的差异,我也没有做出过多的比较,遇到过的问题也做过总结,例如:关于仿真的一点观点,写的不好,但也不想改了,等有机会再更新吧。
这篇文章呢?准备谈一下最基础的仿真平台的搭建,从体系结构到testbench的一些关键要素等,最后给出一个完整的Verilog testbench示例,这其实在我的很多篇博文中,都给出过,我的每一个设计几乎都会附带一个简单的tb文件,也就是testbench文件。
基本测试平台的架构
测试平台由不可综合的Verilog代码组成(任何Verilog代码都可以,只要符合语法),该代码可生成设计的输入并检查输出是否正确。
下图显示了一个简单测试平台的典型架构。
测试平台的框图,其中显示了一个激励模块,该激励模块生成设计的输入,而输出检查器则检查正确的输出。
激励模块为我们的FPGA设计生成输入,输出检查器对输出进行测试,以确保它们具有正确的值。
对于较大的设计,激励和输出检查器将在单独的文件中。也可以将所有这些不同元素包含在一个文件中。
下面详细介绍:
实例化DUT
编写测试平台的第一步是创建一个Verilog模块,该模块充当测试的顶层。
与到目前为止我们讨论过的verilog模块不同,在这种情况下,我们要创建一个没有输入或输出的模块。这是因为我们希望testbench模块完全独立。
下面的代码段显示了一个空模块的语法,我们可以将其用作测试平台。
module <module_name> (); // Our testbench code goes here endmodule : <module_name>创建测试平台模块之后,必须实例化正在测试的设计。这使我们可以将信号连接到设计上以为我们的代码提供激励。
下面提供例化格式:
<module_name> # ( // If the module uses parameters they are connected here .<parameter_name> (<parameter_value>) ) <instance_name> ( // Connection to the module ports .<port_name> (<signal_name>), .<port_name> (signal_name>) );完成此操作后,就可以开始将激励信号写入FPGA了。这包括生成时钟和复位,以及创建测试数据以发送到FPGA。
为此,我们需要使用一些Verilog语法,例如initial块,forever循环和延迟的语句。
在通过完整的Verilog Testbench示例之前,我们将更详细地研究这些内容。
Verilog中的延迟建模
测试平台代码与设计代码之间的主要区别之一是,我们不需要综合测试平台。
结果,我们可以使用消耗时间的特殊构造。实际上,这对于创建测试激励至关重要。
我们在Verilog中提供了一个可用于构造延迟的模型。在verilog中,我们使用#字符后跟多个时间单位来建模延迟。
例如,下面的verilog代码显示了使用延迟运算符等待10个时间单位的示例。
#10这里要注意的重要一件事是,代码末尾没有分号。当我们编写代码以对Verilog中的延迟进行建模时,这实际上会导致编译错误。
通常在与分配相同的代码行中写入延迟。这有效地充当了调度程序,这意味着信号的变化被调度为在延迟时间之后发生。
下面的代码段显示了此类代码的示例。
// A is set to 1 after 10 time units #10 a = 1b1;或
// A is set to 1 after 10 time units a = #10 1b1;Timescale编译器指令
到目前为止,我们已经讨论了十个时间单位的延迟。在我们实际定义应该使用的时间单位之前,这是毫无意义的。
为了指定在仿真过程中使用的时间单位,我们使用了verilog编译器指令,该指令指定了时间单位和分辨率。我们只需要在测试平台中执行一次此操作,就应该在模块外部执行此操作。
下面的代码段显示了我们用来在verilog中指定时间单位的编译器指令。
`timescale <unit_time> / <resolution>我们使用unit_time字段指定测试平台的主要时间单位,并使用resolution字段定义模拟中时间单位的分辨率。
resolution字段很重要,因为我们可以使用非整数来指定Verilog代码中的延迟。
例如,如果我们希望有10.5ns的延迟,我们可以简单地写#10.5作为延迟。
因此,编译器指令中的resolution字段确定了我们可以在Verilog代码中实际建模的最小时间步长。
此编译器伪指令中的两个字段都采用时间类型,例如1ps或1ns。
Verilog initial块
在Verilog中使用的另一种程序块称为initial块。
我们在初始块中编写的任何代码在仿真开始时都会执行一次,并且只能执行一次。
下面的verilog代码显示了我们用于初始块的语法。
initial begin // Our code goes here end与Always块不同,在initial中编写的Verilog代码不可综合。结果,我们几乎仅将它们用于仿真目的。
但是,我们也可以在Verilog RTL中使用initial块来初始化信号。
当我们在Verilog测试平台中编写激励代码时,我们几乎总是使用initial块。
为了更好地理解我们如何使用初始块在verilog中编写激励,我们考虑一个基本示例。
对于此示例,假设我们要测试基本的两个输入and门。
为此,我们需要生成四个可能输入组合中的每一个的代码。
另外,我们还需要使用延迟运算符,以便在生成输入之间等待一段时间。
这一点很重要,因为它为信号在我们的设计中传播留出了时间。
下面的Verilog代码显示了我们将用于在initial块中编写此测试的方法。
initial begin // Generate each input to an AND gate // Waiting 10 time units between each and_in = 2b00; #10 and_in = 2b01; #10 and_in = 2b10; #10 and_in = 2b11; endVerilog forever循环
我们在文章:FPGA的设计艺术(15)逻辑设计及仿真利器之各式各样的循环中,已经讨论过循环。
仿真中最常用的是forever循环,当我们使用这种构造时,实际上是在创建一个无限循环。这意味着我们创建了一段代码,该段代码在仿真过程中会连续运行。
下面的Verilog代码显示了我们用于编写forever循环的语法。
forever begin // our code goes here end当用其他编程语言编写代码时,我们可能会认为无限循环是一个严重的错误,应该避免。
但是,我们必须记住,verilog与其他编程语言不同。当我们编写Verilog代码时,我们是在描述硬件,而不是在编写软件。
因此,在至少一种情况下,我们可以使用forever循环-在Verilog测试台中生成时钟信号。
为此,我们需要一种以规则间隔连续反转信号的方法。永远的循环为我们提供了一个简单的方法来实现这一目标。
下面的Verilog代码显示了如何使用永远循环在测试平台中生成时钟。重要的是要注意,我们编写的任何循环都必须包含在过程块或generate块中。
initial begin clk = 1b0; forever begin #1 clk = ~clk; end endVerilog系统任务
在verilog中编写测试平台时,我们具有一些内置的任务和功能,可以用来帮助我们。
这些被统称为系统任务或系统功能,我们可以轻松识别它们,因为它们始终以美元符号开头。
实际上,这些任务中有几个可用。但是,我们仅查看三个最常用的Verilog系统任务-$ display,$ monitor和$ time。
$display$ display函数是Verilog中最常用的系统任务之一。我们使用它来输出一条消息,该消息在模拟过程中显示在控制台上。
我们使用$ display宏的方式与C中的printf函数非常相似。
这意味着我们可以轻松地在测试台中创建文本语句,并使用它们来显示有关仿真状态的信息。
我们还可以在字符串中使用特殊字符(%)来显示设计中的信号。当我们这样做时,我们还必须包括一个格式字母,告诉任务以什么格式显示变量。
最常用的格式代码是b(二进制),d(十进制)和h(十六进制)。我们还可以在此格式代码前面加上一个数字,以确定要显示的位数。
下面的Verilog代码显示$ display系统任务的常规语法。此代码段还包括一个示例用例。
// General syntax $display(<string_to_display>, <variables_to_display); // Example – display value of x as a binary, hex and decimal number $display(“x (bin) = %b, x (hex) = %h, x (decimal) = %d”, x, x, x);下表显示了我们可以与$ display系统任务一起使用的各种格式的完整列表。
格式代码 描述
%b或%B 显示为二进制
%d或%D 显示为小数
%h或%H 显示为十六进制
%o或%O 显示为八进制格式
%c或%C 显示为ASCII字符
%m或%M 显示模块的层次结构名称
%s或%S 显示为字符串
%t或%T 显示为时间
$monitor$ monitor函数与$ display函数非常相似,不同之处在于它的行为更为智能。
我们使用此功能来监视测试台中信号的值,并在这些信号之一更改状态时显示一条消息。
综合器实际上忽略了所有系统任务,因此我们甚至可以在 verilog RTL代码中包含$ monitor语句,尽管这种情况并不常见。
下面的代码段显示了此系统任务的常规语法。此代码段还包括一个示例用例。
// General syntax $monitor(<message_to_display>, <variables_to_display>); // Example – monitor the values of the in_a and in_b signals $monitor(“in_a=%b, in_b=%b\n”, in_a, in_b); $time我们通常在测试平台中使用的最终系统任务是$ time函数。我们使用此系统任务来获取当前的仿真时间。
在我们的Verilog测试平台中,我们通常将$ time函数与$ display或$ monitor任务一起使用,以在消息中显示时间。
下面的Verilog代码显示了我们如何一起使用$ time和$ display任务来创建消息。
$display(“Current simulation time = %t”, $time);Verilog Testbench示例
现在,我们已经讨论了测试平台设计的最重要主题,让我们考虑一个完整示例。
为此,我们将使用一个非常简单的电路,并构建一个生成所有可能的输入组合的测试平台。
下面显示的电路是我们用于此示例的电路。它由一个简单的两输入与门以及一个触发器组成。
电路图显示了两个输入与门,而该与门的输出是D型触发器的输入
1.创建一个测试平台模块我们在测试平台中要做的第一件事是声明一个空模块以写入我们的测试平台代码。
下面的代码片段显示了此测试平台的模块声明。
注意,优良作法是保持被测试设计的名称与测试台相似。通常,只需在设计名称的末尾附加_tb或_test即可完成此操作。
module example_tb (); // Our testbench code goes here endmodule : example_tb 2.实例化DUT现在我们有一个空白的测试平台模块可以使用,我们需要实例化要测试的设计。
由于命名实例化通常比位置实例化更易于维护,并且更易于理解,因此,这就是我们使用的方法。
下面的代码片段显示了我们如何实例化DUT,假设信号clk,in_1,in_b和out_q事先已声明。
example_design dut ( .clock (clk), .reset (reset), .a (in_a), .b (in_b), .q (out_q) ); 3.生成时钟并复位接下来要做的是在Verilog测试台中生成时钟并复位信号。
在这两种情况下,我们都可以在初始块中编写此代码。然后,我们使用verilog延迟运算符来安排状态更改。
对于时钟信号,我们在测试过程中使用forever关键字连续运行时钟信号。
使用这种结构,我们每1 ns安排一次反转,从而获得1GHz的时钟频率。
选择该频率纯粹是为了提供快速的仿真时间。实际上,FPGA中无法达到1GHz的时钟速率,并且测试台时钟频率应与硬件时钟的频率匹配。
下面的Verilog代码显示了如何在我们的测试台中生成时钟和复位信号。
// generate the clock initial begin clk = 1b0; forever #1 clk = ~clk; end // Generate the reset initial begin reset = 1b1; #10 reset = 1b0; end 4.编写激励我们需要编写的测试平台的最后一部分是测试激励。
为了测试电路,我们需要依次生成四个可能的输入组合中的每一个。然后,在信号通过我们的代码块传播时,我们需要等待一小段时间。
为此,我们为输入分配一个值,然后使用verilog延迟运算符允许通过FPGA传播。
我们还希望监视输入和输出的值,我们可以使用$ monitor verilog系统任务来执行此操作。
下面的代码段显示了此代码。
initial begin // Use the monitor task to display the FPGA IO $monitor(“time=%3d, in_a=%b, in_b=%b, q=%2b \n”, $time, in_a, in_b, q); // Generate each input with a 20 ns delay between them in_a = 1b0; in_b = 1b0; #20 in_a = 1b1; #20 in_a = 1b0; in_b = 1b1; #20 in_a = 1b1; end下面的verilog代码完整显示了testbench示例。
`timescale 1ns / 1ps module example_tb (); // Clock and reset signals reg clk; reg reset; // Design Inputs and outputs reg in_a; reg in_b; wire out_q; // DUT instantiation example_design dut ( .clock (clk), .reset (reset), .a (in_a), .b (in_b), .q (out_q) ); // generate the clock initial begin clk = 1b0; forever #1 clk = ~clk; end // Generate the reset initial begin reset = 1b1; #10 reset = 1b0; end // Test stimulus initial begin // Use the monitor task to display the FPGA IO $monitor(“time=%3d, in_a=%b, in_b=%b, q=%2b \n”, $time, in_a, in_b, q); // Generate each input with a 20 ns delay between them in_a = 1b0; in_b = 1b0; #20 in_a = 1b1; #20 in_a = 1b0; in_b = 1b1; #20 in_a = 1b1; end endmodule : example_tb免责声明:文章内容来自互联网,本站不对其真实性负责,也不承担任何法律责任,如有侵权等情况,请与本站联系删除。
转载请注明出处:FPGA的设计艺术(17)如何搭建一个简易的逻辑测试平台? https://www.yhzz.com.cn/a/12832.html