在自己准备写一些简单的verilog教程之前,参考了许多资料----Asic-World网站的这套verilog教程即是其一。这套教程写得极好,奈何没有中文,在下只好斗胆翻译过来(加了自己的理解)分享给大家。
这是网站原文:Verilog Tutorial
这是系列导航:Verilog教程系列文章导航
编写Testbench(测试平台/测试脚本)和编写 RTL 代码一样复杂。随着如今ASIC 变得越来越复杂,验证ASIC的功能和性能已成为一项艰巨的挑战。通常情况下,ASIC项目开发所需的时间有 60~70% 都花在验证/确认/测试上。尽管上述事实为大多数 ASIC 工程师所熟知,但仍有工程师认为验证没有什么技术含量。
在你开始编写Testbench之前,最重要的是要编写“被测设计(design under test,DUT)”的设计规范(design specification)。你需要清楚地理解设计规范并制定与之匹配的测试计划,该计划要详细规划测试平台的架构和测试场景(测试用例)。
接下来,我们以一个简单的 4 位递增计数器的验证作为例子。该计数器会在使能信号(enable)为高电平时递增,并在同步复位信号(reset)为高电平时重置为零。
module counter (clk, reset, enable, count);
input clk, reset, enable;
output [3:0] count;
reg [3:0] count;
always @ (posedge clk)
if (reset == 1'b1) begin
count <= 0;
end else if ( enable == 1'b1) begin
count <= count + 1;
end
endmodule
接下来,我们将编写一个自检式的测试脚本。为了帮助您更好地理解自动化测试脚本的概念,我们将分步骤进行。测试脚本的框架类似下图。
被测设计DUT (计数器)将在测试脚本中被例化,测试脚本包含一个时钟生成器(clock gen)、复位信号生成逻辑(reset logic)、使能信号逻辑生成逻辑(enable logic)和监控/比较逻辑(monitor/checker)。该测试脚本将计算计数器的预期计数值并将其与计数器的实际输出进行比较,以验证被测设计的功能是否满足设计预期。
我们可以添加更多的测试用例,但我们的重心不是来测试计数器,而是来学习如何编写测试脚本的。
任何测试脚本创建的第一步都是构建一个伪模板(dummy template),该模板基本上将被测设计DUT 的输入声明为 reg,输出声明为 wire,然后再实例化 DUT,如下面的代码所示。请注意,测试脚本没有端口列表。
module counter_tb;
reg clk, reset, enable;
wire [3:0] count;
counter U0 (
.clk (clk),
.reset (reset),
.enable (enable),
.count (count)
);
endmodule
下一步是添加时钟生成器逻辑:这很好简单,因为我们知道如何生成时钟。在我们添加时钟发生器之前,我们需要将 DUT 的所有输入驱动到某个已知状态,如下面的代码所示。
module counter_tb;
reg clk, reset, enable;
wire [3:0] count;
counter U0 (
.clk (clk),
.reset (reset),
.enable (enable),
.count (count)
);
initial
begin
clk = 0;
reset = 0;
enable = 0;
end
always
#5 clk = !clk;
endmodule
Verilog 中的initial block(初始块)只会执行一次,因此模拟工具会将 clk、reset 和 enable 的值置为 0;通过查看计数器代码可以发现驱动 0 会使这些信号失效。
生成时钟的方法有很多种:可以在initial 块内使用foever循环作为上述代码的替代方法。您还可以添加参数或使用 `define 来控制时钟频率。你可能会写一个复杂的时钟发生器,我们可以在其中引入 PPM(百万分之一,时钟宽度漂移),然后控制占空比。
一旦我们有了基本的逻辑后,即可添加复位逻辑。如果我们查看测试用例,就会发现我们添加了一个约束条件 -- 在仿真期间应该可以随时激活复位。为实现这一目标,我们有很多方法。Verilog 中有一种叫做“事件(events)”的东西:事件可以被触发,也可以被监控,以查看事件是否发生。
让我们用等待触发事件“reset_trigger”的方式编写我们的复位逻辑:当此事件发生时,复位逻辑在时钟的下降沿有效复位信号并在下一个下降沿无效复位信号,如下面的代码所示。此外,在无效复位后,复位逻辑会触发另一个名为“reset_done_trigger”的事件。然后可以在测试台的其他地方使用此触发事件进行同步。
event reset_trigger;
event reset_done_trigger;
initial begin
forever begin
@ (reset_trigger);
@ (negedge clk);
reset = 1;
@ (negedge clk);
reset = 0;
-> reset_done_trigger;
end
end
接下来,让我们添加逻辑来生成测试用例。
重复一遍:编写测试用例“有很多种方法”,这完全取决于设计者的创造力。让我们采用一种简单的方法,然后慢慢地构建它。
在这个测试用例中,我们将在 10 个仿真时间单位后触发事件 reset_trigger(即使复位信号有效再无效)。
initial
begin: TEST_CASE
#10 -> reset_trigger;
end
在这个测试用例中,我们将触发复位逻辑并等待复位逻辑完成,然后将使能信号有效,并在10个时钟下降沿后将使能信号无效。
initial
begin: TEST_CASE
#10 -> reset_trigger;
@ (reset_done_trigger);
@ (negedge clk);
enable = 1;
repeat (10) begin
@ (negedge clk);
end
enable = 0;
end
在这个测试用例中,我们将触发复位逻辑并等待复位逻辑完成,然后随机地有效/无效复位信号和使能信号。
initial
begin : TEST_CASE
#10 -> reset_trigger;
@ (reset_done_trigger);
fork
repeat (10) begin
@ (negedge clk);
enable = $random;
end
repeat (10) begin
@ (negedge clk);
reset = $random;
end
join
end
好吧,你可能会问,这三个测试用例是否都存在于同一个文件中?答案是否定的。如果我们将这三个测试用例放在通一个文件中,那么由于三个initial块都会驱动复位信号和使能信号,就会导致出现竞争条件。所以通常情况下,测试用例会单独编写,然后使用“include”指令将其包含在测试脚本中。
如果仔细观察这三个测试用例,你就会发现即使测试用例执行未完成,仿真也会终止。为了更好地控制,我们可以做的是添加一个“terminate_sim”的事件,只有在触发该事件时才执行$finish(结束仿真)。我们可以在测试用例执行结束时触发此事件。$finish 的代码现在可能如下所示。
event terminate_sim;
initial begin
@ (terminate_sim);
#5 $finish;
end
要使任何测试脚本自检/自动化,首先需要设计一个在功能上模仿 DUT 的模型。在我们的示例中很容易,因为我们的DUT很简单。但如果 DUT 很复杂,那么模仿它就会非常复杂,并且需要大量创新技术才能进行自检。
reg [3:0] count_compare;
always @ (posedge clk)
if (reset == 1'b1) begin
count_compare <= 0;
end else if ( enable == 1'b1) begin
count_compare <= count_compare + 1;
end
一旦我们有了模拟 DUT 功能的逻辑,就需要添加检查逻辑,它会不断检查预期值与实际值的偏差。每当出现错误时,它都会打印出预期值和实际值,并通过触发事件“terminate_sim”来终止模拟。
现在我们已经准备好了所有逻辑,我们也可以删除 $display 和 $monitor,因为测试脚本已经实现了自检化,并不需要手动验证输入和输出。你尝试更改 count_compare = count_compare +2,看看比较逻辑是如何工作的。这只是查看我们的测试平台是否稳定的另一种方式。
always @ (posedge clk)
if (count_compare != count) begin
$display ("DUT Error at time %d", $time);
$display (" Expected value %d, Got Value %d", count_compare, count);
#5 -> terminate_sim;
end