目录
做仿真时需要编写testbench,作为完全没学过verilog的小白,第一次碰到需要手写的verilog是testbench文件。那就正好从testbench入手,逐句学习verilog和testbench的语法。
什么是testbench及其作用:
Verilog 代码设计完成后,还需要进行重要的步骤,即逻辑功能仿真。仿真激励文件称之为 testbench,放在各设计模块的顶层,以便对模块进行系统性的例化调用进行仿真。
毫不夸张的说,对于稍微复杂的 Verilog 设计,如果不进行仿真,即便是经验丰富的老手,99.9999% 以上的设计都不会正常的工作。不能说仿真比设计更加的重要,但是一般来说,仿真花费的时间会比设计花费的时间要多。有时候,考虑到各种应用场景,testbench 的编写也会比 Verilog 设计更加的复杂。
Testbench测试机制:
任何一个设计好的模块,都有输入和输出,此模块是否满足要求就是看给定满足要求的输入,是否能够得到满足要求的输出。所以testbench的测试机制就是:用各种verilog或者VHDL语法,产生满足条件的激励信号(也就是对被模块的输入),同时对模块的输出进行捕捉,测试输出是否满足要求。如下图,产生激励输出验证模块两个模块都属于testbench,最好的输出验证模块最终只需要给一个pass和fail的答案出来就可以了。不管是用一个信号表示pass和fail还是用$display()函数打印,最终简单明了的给出过或者不过的信息就好了。请大家写仿真文件的时候尽量做到这点。
完整的testbench结构:
module Test_bench();//创建verilog模块
信号或变量声明定义
逻辑设计中输入信号在这里对应reg型变量
逻辑设计中的输出信号在这里对应wire型
例化测试模块DUT
使用initial或always语句块产生激励
监控和比较输出响应
endmodule
1、创建verilog模块:
编写testbench的第一步是创建一个 verilog 模块作为测试的顶层。
与verilog module不同,在这种情况下,要创建的是一个没有输入和输出的模块。因为设计人员希望testbench模块是完全独立的(self contained)。
下面的代码片段展示了一个空模块的语法,这可以被用作testbench。
module ();
//在这里写testbench
endmodule :
2、声明变量并例化被测设计:
创建了一个testbench之后,必须例化被测设计,这可以将信号连接到被测设计以激励代码运行。下面的代码片段展示了如何例化一个被测模块。
<模块本身的名字> <例化的模块名字> (
//例化参数
.<parameter_name1> (<parameter_value1>);
.<parameter_name2> (<parameter_value2>)
)
下面举一个简单的例子:
wire in_a = 1'b0; //声明连线in_a
wire in_b = 1'b1; //声明连线in_b
wire out_q; //声明连线out_q
//例化设计
example_design dut (
.a (in_a),
.b (in_b),
.q (out_q)
);
在这里首先声明了三条连线,并将原模块example_design中的端口(.<name>表示端口)与连线(括号里的,也是前面声明好的)连接起来。
数据类型种类:
除了上面用到的wire型外,还有其他数据类型。其中,wire与reg是最常用的两种数据类型,其他数据类型可以认为是这两种的扩展或辅助。
wire:wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。如果没有驱动元件连接到 wire 型变量,缺省值一般为 "Z"。
wire interrupt ; wire flag1, flag2 ; wire gnd = 1'b0 ;
reg:寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。
reg clk_temp; reg flag1, flag2 ;
在仿真时,寄存器的值可在任意时刻通过赋值操作进行改写。例如:
reg rstn ; initial begin rstn = 1'b0 ; #100 ; rstn = 1'b1 ; end
向量:当位宽大于 1 时,wire 或 reg 即可声明为向量的形式。例如:整数,实数,时间寄存器变量
reg [3:0] counter ; //声明4bit位宽的寄存器counter wire [32-1:0] gpio_data; //声明32bit位宽的线型变量gpio_data wire [8:2] addr ; //声明7bit位宽的线型变量addr,位宽范围为8:2 reg [0:31] data ; //声明32bit位宽的寄存器变量data, 最高有效位为0
整数,实数,时间寄存器变量:
integer j ; //整型变量 real temp ; //实数型变量 time current_time ; //时间型变量
3、使用initial或always语句产生激励
和功能文件的书写上有些区别,测试模块则一般习惯性用initial模块去产生一个信号的输入激励或对一个变量进行初始化赋值操作。在initial 块中编写的任何代码都会在仿真开始时执行一次且仅执行一次。与 always 块不同,在 initial 块中编写的 verilog 代码几乎是不可综合的,因此其几乎只被用于仿真。但是,在verilog RTL 中也可以使用initial块来初始化信号(几乎很少用)。
语法格式:
initial begin
语句1;
...
语句n;
end
为了更好地理解如何使用initial块在 verilog 中编写激励,请来看一个基本示例----假设现在想要测试一个基本的两输入与门,为此需要编写代码来生成所有可能的四种输入。此外还需要使用延时运算符以在生成不同的输入之间延迟一段时间。这很重要,因为这可以允许信号有时间来传播。
initial begin
// 每隔10个时间单位就生成一个输入
and_in = 2b'00;
#10 and_in = 2b'01;
#10 and_in = 2b'10;
#10 and_in = 2b'11;
end
#+数字:表示verilog中的建模时间(类似于delay(num))。
testbench代码和设计代码之间的主要区别是testbench并不需要被综合成实际电路,为此可以使用时间控制语句这种特殊结构。事实上,这对于创建测试激励至关重要。
在 Verilog 中有一个可用的结构----它能够对仿真进行延时。在 verilog 中使用 # 字符后跟多个时间单位来模拟延时。
例如,下面的 verilog 代码展示使用延时运算符等待 10 个时间单位。
#10
将延时语句写在与赋值相同的代码行中也很常见,这可以有效地行使调度功能,将信号的变化安排在延迟时间之后。下面的代码片段是此种情况的一个示例。
#10 a = 1'b1; // 在10个时间单位后,a将被赋值为1
注意,除非#10作为单独一行,否则其后没有分号。
————————————————
版权声明:本文为CSDN博主「孤独的单刀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wuzhikaidetb/article/details/129396009
4、任务函数task,function
task和function说明语句分别用来定义测试模块当中的任务和函数,利用任务和函数可以把一个很大规模的程序分解成很多较小的任务和函数,非常便于Testbench的编写、理解以及调试,类似于C语言中的函数接口,输入、输出、总线信号的值可以传入、传出任务和函数,所以说学会灵活使用task和function可以极大程度上简练Testbench的程序结构,使得程序整体上通俗易懂,直观明了。
任务格式:task <任务名>
<端口和数据类型声明语句>
语句1;
……
语句n;
endtask
如上图所示,是按键消抖测试模块的task任务,这里我们模拟按键闭合和按下可能存在的机械抖动情况,并将其做成了task任务,方便在Testbench的编写时候去调用它,从而更加简化测试模块的程序结构。
函数格式:function <返回值的类型或范围>(函数名)
<端口说明语句>
<变量类型说明语句>
begin
语句1;
……
语句n;
end
endfunction
如下图所示,我们用阶乘函数的定义和调用来举例说明function的具体用法,在这个例子当中也用嵌套使用了for循环语句和$display,代码层面整体上并不难理解,所以就不再赘述了。
task和function说明语句也存在一些不同点,请大家在编程时需要去注意:1.函数只能和本模块共用同一个仿真时间单位,而任务则可以定义自己的仿真时间单位;2.函数不能启动任务,而任务里能启动其他任务或函数;3.函数至少要有一个输入变量,而任务可以没有或者有多个任意类型的变量;4.函数返回一个值,而任务则不返回值。
因为task的书写格式总得来说要比function更加宽泛和灵活,所以在实际应用操作中,大家一般都习惯性定义task来分割任务,这其实类似于上面的while、repeat和for语句都可以表示循环,但是repeat和for语句因为其书写上的灵活性更容易被人们所接受。
————————————————
版权声明:本文为CSDN博主「青青豌豆」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wandou0511/article/details/122953707
可以看到在任务/函数中间,用begin...end语句做了任务块的分割,这是常用的方法。除此之外,还有一种fork...join的任务块分割语法:
fork...join与begin...end的区别就是,前者中的语句是并行执行的,后者中的语句是顺序执行的。一个小例子:
第一个代码中顺序块的执行时间是d+d+d+d,这里d=50,那么这个块完成的时间就是250;第二个代码中每条语句都是同时执行的,这个块完成的时间是最慢的那条语句完成的时间,这里是250。
parameter d=50;
reg [7:0] r;
begin
#d r='h35;
#d r='hE2;
#d r='h00;
#d r='hF7;
#d ->end_wave; //->表示触发事件end_wave使其翻转
end
fork
#250 ->end_wave;
#200 r='hF7;
#150 r='h00;
#100 r='hE2;
#50 r='h35;
join
5、verilog系统任务
在 verilog 中编写testbench时,有一些内置的任务和函数可以提供帮助。这些被统称为系统任务或系统函数,它们很容易被识别----总是以美元符号($)开头。
虽然有很多这样的系统任务可用,但是这三个是最常用的 :$display、$monitor 和 $time。
————————————————
版权声明:本文为CSDN博主「孤独的单刀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wuzhikaidetb/article/details/129396009
这个写的挺好,收藏一下: