为什么要编写testbench?
编写Testbench的目的是把RTL代码在Modsim中进行仿真验证,通过查看仿真波形和打印信息验证代码逻辑是否正确。
testbench在这里做的就是给输入进行激励
具体逻辑为:
1.使用testbench给模块喂入输入信号(一般在testbench中定义为reg
型)
2.通过实例化模块(注意实例化模块时带partameter
的写法)把模拟输入信号传入功能模块中
testbench的基本模块
module写法
-
测试模块的命名:tb_<功能模块名> 或 <功能模块名>_tb
功能模块名为:delay_clap , 则对应测试模块名为:delay_clap_tb
需要定义模拟的输入/输出信号: -
输入/输出信号与功能模块中定义的输入/输出信号保持一致
输入信号一般定义为 reg 型信号,因为后面需要在always/initial语句块中被赋值
输出信号一般为 wire型即可
以边沿检测器为例
//原边沿检测器module部分
module delay_clap (
input sig,//异步信号
input clk1,//异步慢时钟
input rstn,//复位信号
input clk2,//目标快时钟
output sig_rise,//上升沿的信号
output sig_down,//下降沿的信号
output sig_out);//直接输出的信号
//具体内容
endmodule
//对应的testbench
module delay_clap_tb;
reg rstn;
reg clk1;
reg clk2;
reg sig;
wire sig_rise;
wire sig_down;
wire sig_out;
//具体内容
endmodule
显然:在定义端口时,testbench中对所有输入都定义为reg
型,所有输出都定义为wire
型。
生成时钟
特定频率的时钟
对于生成特定频率的时钟,一般会在testbench的最开始设定时间单位和时间精度,例如:
`timescale <时间单位>/<时间精度>
`timescale 1ns/1ps
通过规定timescale,来确定生成信号的时间单位,从而确定具体频率
- 时间单位:时间尺度预编译指令 时间单位 / 时间精度
- 定义时间单位: `timescale 1ns/1ns 表示时间单位为1ns,时间精度为1ns
- 时间单位和时间精度由值 1、10、和 100 以及单位 s、ms、us、ns、ps 和 fs 组成
- 时间单位不能比时间精度小
- 仿真过程所有与时间相关量的单位(即1单位的时间)
- 时间精度:决定时间相关量的精度及仿真显示的最小刻度 1ns/1ps 精度为.0.01ns
比如:要产生250MHZ的时钟信号的计算过程如下:
250MHZ的时钟信号一个周期的时长为:
1
/
(
250
×
1
0
6
)
=
4
n
s
1/(250\times10^6)=4ns
1/(250×106)=4ns
则说明该时钟生成的条件应该为声明1ns为时间单位,每2个时间单位翻转一次
生成时钟的写法
// Declare a clock period constant.
Parameter ClockPeriod = 10;
// Clock Generation method 1:
initial begin
forever Clock = #(ClockPeriod / 2) ~ Clock;
end
// Clock Generation method 2:
initial begin
always #(ClockPeriod / 2) Clock = ~Clock;
end
也可以不声明参数,直接写数字
//生成250MHZ的时钟写法
`timescale 1ns/1ps
initial clk='b0;
always #2 clk = ~clk;
生成测试激励
只有给设计激励输入,才能得到验证结果。
提供激励的方法有两种:绝对时间激励
和相对时间激励
- 激励信号初始化
用initial 语句进行初始化,该语句中的代码块只执行一次
根据需要初始化为0/1都可
绝对时间激励
绝对时间激励是指以仿真时刻0为基准,给信号赋值。
使用 #
用于指定等待的延迟时间,之后才会执行下一个激励
这里是指等待的延迟时间,而非绝对时间!
initial begin
rstn = 0;
sig = 0;
#200 //在绝对时间200时间单位时给rstn赋值
rstn = 1;
#800//
sig = 1;
#1100
sig = 0;
#1400
rstn = 0;
#1600
rstn = 1;
#2000
sig = 1;
#3000
sig = 0;
#20000
$stop;
end
相对时间激励
相对时间激励给信号一个初始值,直到某一事件发生后才触发激励赋值
输入信号与系统时钟关联
//sig是由clk1控制的信号,在上升沿发生改变
always @(posedge clk1) begin
sig <= sig;
end
//在RTL代码中定义的输入信号in
module rtl_module(
input wire in
);
//
endmodule
//在testbench中对输入信号进行模拟的方式为:
`timescale 1 ns/ 1 ps
module tb_rtl_module();
reg in;
always @(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
in<= 1'b0;
else
in<= {$random} % 2;
end
rtl_module rtl_module_int1 (
.in(in)
)
endmodule
always @ (posedge clk)
tb_cnt <= tb_cnt + 1;
initial begin
if (tb_cnt <= 5) begin
reset = 1;
load = 0;
count = 0;
end
else begin
reset = 0;
load = 1;
count = 1;
end
end
实例化模块
通过实例化模块把模拟输入信号传入功能模块中,即在测试中将原模块实例化,然后将输入信号与输出信号填入对应的端口
这里注意有参数的端口实例化
hand_pulse_syn #(.pulse_init(pulse_init)) hand_pulse_syn_inst(.clk_fast(clk_fast),
.clk_slow(clk_slow),
.rstn(rstn),
.pulse_fast(pulse_fast),
.pulse_slow(pulse_slow));
这里pulse_init
要记得声明一个parameter
和原模块中一致
仿真参数重定义
在实际仿真中,我们没有必要按照实际的计数器值进行仿真,这将给仿真调试带来不便,此时我们只需改仿真参数为较小的数,能方便的看出输入输出的关系即可
参数传递的方式
即在仿真文件的顶层模块中给每个参数传入新的值!如下所示:
rom_ip
#(
.CNT_200MS_MAX (199) ,
.CNT_256_MAX (9) ,
.CNT_KEYFILTER_MAX (9) ,
.CUNT_SCAN_MAX (99) ,
.CNT_SHIFT_MAX (21)
)
rom_ip_inst (
port
);
这种写法要求参数要从最顶层模块传到最底层模块,每一级都需写参数列表,当参数过多时会造成不便!
使用defparam命名
用defparam命令重定义每个子模块中的仿真参数,这样比较直观,且可以对任意子模块的参数进行设置,较为方便。
// 重定义仿真参数的方法
defparam rom_ip_inst.rom_rader_inst.CNT_200MS_MAX = 199;
defparam rom_ip_inst.rom_rader_inst.CNT_256_MAX = 9;
defparam rom_ip_inst.key1_filter_inst1.COUNTER_MAX = 9;
defparam rom_ip_inst.key1_filter_inst2.COUNTER_MAX = 9;
defparam rom_ip_inst.dynamic_seg_main_inst1.CUNT_SCAN_MAX = 99;
defparam rom_ip_inst.dynamic_seg_main_inst1.CNT_SHIFT_MAX = 99;
语法如下:
defparam 顶层模块实例化名.子模块1实例化名.子模块1的子模块实例化名 = 值;
用always语句实现信号在仿真过程中的电平变化
- always在仿真过程中将被多次执行
- always #10 in <= {$random} % 8; 表示每隔10个时间单位in的电平变化一次:
- {$random}%8 表示随机选取[0,7]之间的数
- in <= {$random} % 8; 在赋值时会自动进行数据类型转换
- always后面最好只有一条语句
testbench调用RTL代码中寄存器变量的方法
基本语法:实例化的模块名.变量名
如:在RTL代码中定义了变量 state
module rtl_module(
port
);
// 定义状态寄存器
reg [2:0]state;
endmodule
`timescale 1 ns/ 1 ps
module tb_rtl_module(
port
);
// 获取rtl代码中的变量
wire [2:0] state = rtl_module_int1.state;
// 实例化的模块
rtl_module rtl_module_int1 (
.port(port)
)
endmodule
注意:testbench中接收的变量要定义为wire型!
查看仿真结果
常见的波形文件类型
常见的波形文件类型一般有两种,VCD和fsdb
fsdb文件
如果使用modelsim,一般要安装debussy配合使用,因为modelsim不认识fsdb文件
因此需要安装debussy再生成fsdb文件
vcd文件
目前正在使用
VCD 文件是在对设计进行的仿真过程中,记录各种信号取值变化情况的信息记录文件。 EDA
工具通过读取 VCD 格式的文件,显示图形化的仿真波形,所以,可以把 VCD 文件简单地视为波形记录文件.)下面分别描述它们的用法并举例说明之。
目前使用$dumpfile
和$dumpvar
两个系统函数分别生成vcd文件名称和要记录的信号
$dumpfile 系统任务:为所要创建的 VCD 文件指定文件名。
举例( "//"符号后的内容为注释文字):
initial
$dumpfile ("myfile.dump"); //指定 VCD 文件的名字为 myfile.dump,仿真信息将记录到此文件
$dumpvar 系统任务:指定需要记录到 VCD 文件中的信号,可以指定某一模块层次上的所有信号,也可以单独指定某一
个信号。
典型语法为$dumpvar(level, module_name); 参数 level 为一个整数, 用于指定层次数, 参数 module 则指定要记录的模块。
整句的意思就是,对于指定的模块,包括其下各个层次(层次数由 level 指定)的信号,都需要记录到 VCD 文件中去。
举例:
initial
$dumpvar (0, top); //指定层次数为 0,则 top 模块及其下面各层次的所有信号将被记录
initial
$dumpvar (1, top); //记录模块实例 top 以下一层的信号
//层次数为 1,即记录 top 模块这一层次的信号
//对于 top 模块中调用的更深层次的模块实例,则不记录其信号变化
initial
$dumpvar (2, top); //记录模块实例 top 以下两层的信号
//即 top 模块及其下一层的信号将被记录
假设模块 top 中包含有子模块 module1,而我们希望记录 top.module1 模块以下两层的信号,则语法举例如下:
initial
$dumpvar (2, top.module1); //模块实例 top.module1 及其下一层的信号将被记录
假设模块 top 包含信号 signal1 和 signal2(注意是变量而不是子模块), 如我们希望只记录这两个信号,则语法举例如下:
initial
$dumpvar (0, top.signal1, top.signal2); //虽然指定了层次数,但层次数是不影响单独指定的信号的
- 对单个的.v文件中的module一般用法:
/*iverilog */
initial
begin
$dumpfile("delay_wave.vcd"); //生成的vcd文件名称
$dumpvars(0, delay_clap_tb); //tb模块名称
end
/*iverilog */
通过VScode使用iverilog和gtkwave联合仿真
- 在VScode中
新建终端
,进入相应文件夹 - 在命令行输入如下命令
/***************编译*************************/
iverilog -o wave led_demo_tb.v led_demo.v//对源文件和仿真文件进行语法规则检查和编译
//如果调用了多个.v文件
iverilog -o wave -y ./ led_demo.v led_demo_tb.v
/**************生成波形文件**************************/
vvp -n wave -lxt2//生成wave.vcd文件(波形文件)
//如果要改名字,wave可以改成想要的名字
/**************打开波形文件**************************/
gtkwave wave.vcd //wave.vcd是波形文件名称
制作批量文件
通过批处理文件,可以简化编译仿真的执行过程,直接一键执行编译和仿真。新建文本文档,输入以下内容:
echo "开始编译"
iverilog -o wave led_demo.v led_demo_tb.v
echo "编译完成"
vvp -n wave -lxt2
echo "生成波形文件"
cp wave.vcd wave.lxt
echo "打开波形文件"
gtkwave wave.lxt
pause
文件扩展名需要更改,Windows系统保存为.bat
格式,然后双击即可运行!
###testbench完整代码举例
原模块:delay_clap.v(边沿检测器)
//边沿检测器
module delay_clap (
input sig,//异步信号
input clk1,//异步慢时钟
input rstn,//复位信号
input clk2,//目标快时钟
output sig_rise,//上升沿的信号
output sig_down,//下降沿的信号
output sig_out);//直接输出的信号
reg [2:0] sig3_r;//三级缓存,前两级用于同步,后两节用于边沿检测
always @(posedge clk2 or negedge rstn) begin
if(!rstn)
begin
sig3_r <= 3'b000;
end
else
begin
sig3_r <= {sig3_r[1:0],sig};
end
end
assign sig_rise = sig3_r[1] && !sig3_r[2];//上升沿检测,第一个时刻是低,第二个时刻是高
assign sig_down = sig3_r[2] && !sig3_r[1];//下降沿检测
assign sig_out = sig3_r[1];//这里为什么是直接输出?==>使用的是电平同步:打两拍在第二拍输出,降低亚稳态概率
endmodule
testbench delay_clap_tb.v
//边沿检测器
module delay_clap_tb;
reg rstn;
reg clk1;
reg clk2;
reg sig;
wire sig_rise;
wire sig_down;
wire sig_out;
delay_clap delay_clap_inst(
.rstn(rstn),
.clk1(clk1),
.clk2(clk2),
.sig(sig),
.sig_rise(sig_rise),
.sig_down(sig_down),
.sig_out(sig_out)
);
/*iverilog */
initial
begin
$dumpfile("delay_wave.vcd"); //生成的vcd文件名称
$dumpvars(0, delay_clap_tb); //tb模块名称
end
/*iverilog */
initial clk1 = 0;
always #30 clk1 = ~clk1;
always @(posedge clk1) begin
sig <= sig;
end
initial clk2 = 0;
always #10 clk2 = ~clk2;
initial begin
rstn = 0;
sig = 0;
#200
rstn = 1;
#800
sig = 1;
#1100
sig = 0;
#1400
rstn = 0;
#1600
rstn = 1;
#2000
sig = 1;
#3000
sig = 0;
#20000
$stop;
end
endmodule
生成波形图
文章来源:https://www.toymoban.com/news/detail-614738.html
部分参考来源: Verilog十大基本功2(testbench的设计 文件读取和写入操作 源代码).文章来源地址https://www.toymoban.com/news/detail-614738.html
到了这里,关于verilog之testbench的写法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!