编写Testbench的目的是把RTL代码在Modsim中进行仿真验证,通过查看仿真波形和打印信息验证代码逻辑是否正确。下面以3-8译码器说明Testbench代码结构。
Testbench代码的本质是通过模拟输入信号的变化来观察输出信号是否符合设计要求!因此,Testbench的核心在于如何模拟输入信号,并把模拟的输入信号输入到功能模块中产生输出信号,如上图所示。解决方案为:
- 通过随机数产生输入信号
- 通过实例化模块把模拟输入信号传入功能模块中
Testbench代码可自定义,也可自动生成!
2 自定义3-8译码器Testbench代码
2.1 现有的功能模块为3-8译码器,其功能模块的RTL代码为:
module decoder3_8(
input wire [2:0] in,
output reg [7:0] out
);
// always/initial 模块中只能用 reg型变量
always @(*) begin
case(in)
3'h0: out = 8'h01;
3'h1: out = 8'h02;
3'h2: out = 8'h04;
3'h3: out = 8'h08;
3'h4: out = 8'h10;
3'h5: out = 8'h20;
3'h6: out = 8'h40;
3'h7: out = 8'h80;
// 避免lanth
default: out = 8'h00;
endcase
end
endmodule
2.2 对应Testbench代码为:
`timescale 1ns/1ns // 时间单位及=精度设置
module tb_decoder3_8();
reg [2:0] in;
wire [7:0] out;
// 初始化
initial begin
in <= 3'h0;
end
// 实现输入信号电平自动变化
always #10 in <= {$random} % 8;
initial begin
$timeformat(-9, 0, "ns", 6);
$monitor("time:%t in:%b out:%b",$time,in,out);
end
// 通过实例化模块把模拟输入信号传入功能模块中
decoder3_8 decoder3_8_inist(
.in(in),
.out(out)
);
endmodule
2.3 代码说明
(1) 时间单位:时间尺度预编译指令 时间单位 / 时间精度
- 定义时间单位: `timescale 1ns/1ns 表示时间单位为1ns,时间精度为1ns
- 时间单位和时间精度由值 1、10、和 100 以及单位 s、ms、us、ns、ps 和 fs 组成
- 时间单位不能比时间精度小
- 仿真过程所有与时间相关量的单位(即1单位的时间)
- 时间精度:决定时间相关量的精度及仿真显示的最小刻度
(2) 延时:#数字
- #10 表示延时10个单位的时间,时间单位为1ns时,该语句表示延时10ns
(3) 测试模块的命名:tb_<功能模块名>
- 功能模块名为:decoder3_8, 则对应测试模块名为:tb_decoder3_8
(4) 需要定义模拟的输入/输出信号:
- 输入/输出信号与功能模块中定义的输入/输出信号保持一致
- 输入信号一般定义为 reg 型信号,因为后面需要在always/initial语句块中被赋值
- 输出信号一般为 wire型即可
(5) 输入信号初始化
- 用initial 语句进行初始化,该语句中的代码块只执行一次
- 根据需要初始化为0/1都可
(6) 用always 语句实现信号在仿真过程中的电平变化
- always在仿真过程中将被多次执行
- always #10 in <= {$random} % 8; 表示每隔10个时间单位in的电平变化一次:
{$random}%8 表示随机选取[0,7]之间的数 - in <= {$random} % 8; 在赋值时会自动进行数据类型转换
- always后面最好只有一条语句
(7) 通过实例化模块把模拟输入信号传入功能模块中
- 即把模拟的输入信号传入到功能模块中即可
(8) 通过 $monitor实现变量实时监测
- 实时打印输入输出信号的数值,以便于监测
- $monitor等系统函数要在initial语句块中
3 常用系统函数
testbench文件中编写的系统函数要在initial语句块中!
3.1 $timeformat 设置显示时间的格式
使用格式:
$timeformat(time_unit, decimal_number, suffix_string, minimum_field_wdith);
- time_unit:时间单位,为一个数字。0(s)、-3(ms)、-6(us)、-9(ns)、-12(ps)、
-15(ps),也可使用中间值:-10表示100ps为单位 - decimal_number:打印时间值的小数位
- suffix_string:跟在时间值后的字符串(后缀字符串),一般写对应的时间单位ns、ps等
- minimum_field_wdith:是时间值字符串与后缀字符串合起来的这部分字符串的最小长度
- 主要用途:更改$write、$display、$strobe、$monitor、$fwrite、$fdisplay、$fstrobe、$fmonitor等任务在%t格式下显示时间的方式
3.2 其他常用系统函数
$display //打印信息,自动换行
$write //打印信息
$strobe //打印信息,自动换行,最后执行
$monitor //监测变量
$stop //暂停仿真
$finish //结束仿真
$time //时间函数
$random //随机函数
$readmemb //读文件函数
用法:
$<系统函数名>("格式控制语句", 变量1, 变量2, 变量3....);
- 格式控制语句中的数据类型顺序与变量的顺序一一对应
相应的格式控制符有:
(1) 常用转义字符
转义字符 | 含义 |
\n | 换行符 |
\t | 横向制表符 |
\v | 纵向制表符 |
\\ | 反斜杠 / |
\'' | 引号 '' |
\a | 响铃 |
%% | 百分号 % |
(2) 常用数据格式
格式 | 说明 |
%b / %B | 二进制 |
%d / %D | 十进制 |
%o / %O | 八进制 |
%h / %H | 十六进制 |
%e / %E | 科学计数法显示十进制数 |
%c / %C | ASCII码 |
%t / %T | 时间 |
%s / %S | 字符串 |
%v / %V | 线网型信号强度 |
%m / %M | 层次名 |
3.2.1 以 $display 用于输出、打印信息为例
用法:
$display("Add:%b+%b=%d",a, b, c); //格式“%b+%b=%d” 格式控制,未指定时默认十进制
- %b:表示对应位置显示二进制数
- %d:表示对应位置显示十进制数
- a, b, c 为与格式控制语句中格式控制符顺序对应的需要打印的变量
4 自动生成Testbench代码
根据仿真需求对相应的变量进行初始化,或对输入信号进行模拟即可!可减少代码量,避免不必要的错误!
按下图依次点击即可:
Processing -> Start -> Start Test Bench Template Witer
在下方会显示生成的testbench文件路径:
此时生成的文件为.vt文件,需要在生成的文件夹中改成.v文件,再根据需要进行改写即可!
5 testbench中调用RTL代码中寄存器变量的方法
基本语法:实例化的模块名.变量名
如:在RTL代码中定义了变量 state
module rtl_module(
port
);
// 定义状态寄存器
reg [2:0]state;
endmodule
在testbench中访问的方式为:
`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型!
6 在testbench中对输入信号进行模拟要用always块进行封装,使其与系统时钟相关联
如:在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
注意事项:有严格时序要求时不能采用直接复制的方式,否则仿真图中会出现逻辑混乱的问题!
错误演示:
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
push_money <= 1'b0;
#30
sys_rst_n <= 1'b1;
#28
push_money <= 1'b1;
#43
push_money <= 1'b0;
#59
push_money <= 1'b1;
#68
push_money <= 1'b0;
end
7 仿真参数重定义
在实际仿真中,我们没有必要按照实际的计数器值进行仿真,这将给仿真调试带来不便,此时我们只需改仿真参数为较小的数,能方便的看出输入输出的关系即可:
7.1 方式1参数传递的方式
即在仿真文件的顶层模块中给每个参数传入新的值!如下所示:
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
);
这种写法要求参数要从最顶层模块传到最底层模块,每一级都需写参数列表,当参数过多时会造成不便!
7.2 使用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的子模块实例化名 = 值;