编写Testbench的主要目的是为了对使用硬件描述语言(HDL)设计的电路进行仿真验证,测试设计电路的功能、部分性能是否与预期的目标相符。
博主在刚开始入门FPGA时把写RTL代码当成重点,不愿写Testbench,仅仅使用Quartus II自带的仿真产生几个激励,然后观察一下最后输出的波形就完事了。有段时间甚至直接忽视仿真,拿单片机在线调试那一套来对付FPGA,直接把代码下载到板子里看效果,若与预期不符,再修改代码,再次下载到板子,如此反复,效率、效果都很低。
后来随着深入地了解,发现FPGA水很深,而且和单片机等有很大的不同。因此劝各位要管住自己、不去使用Quartus II自带的仿真。如果你立志从事FPGA行业,那么会写Testbench才你的敲门砖,才是去大厂的门票,别想着去公司写RTL代码了,公司一定是让你来写Testbench测试文件的。
Testbench作为Modelsim的激励文件,也是自成体系的,同样具有规范化的设计,需要完美的风格及苛刻的结构
本篇只讲Testbench测试代码的推荐编写规范,Modelsim仿真步骤请移步另一篇博客https://blog.csdn.net/zhaogoudan/article/details/111663758。
一个最基本的Testbench包含三个部分,信号定义、模块接口和功能代码。借用一下特权同学总结的编写Testbench的三个基本步骤:
对被测试设计的顶层接口进行例化;
给被测试设计的输入接口添加激励;
判断被测试设计的输出相应是否满足设计要求。
逐步解决编写Testbench的这三点:
首先“对被测试设计的顶层接口进行例化”,这一步相对比较简单,就是简单的例化,但端口多时,也比较困难,而且要分wire、reg,有时会弄错。这里我推荐大家可以偷个懒,通过Quartus II自动生成一个Testbench的模板,里面会自动例化好顶层的接口。
步骤:选择Processing -> Start -> Start Test Bench Template Writer,等待完成后打开刚才生成的Testbench,默认是保存在simulation\Modelsim文件夹下的.vt格式文件。这一步就不多讲了,偷懒就挺好。
其次“给被测试设计的输入接口添加激励”,一般时序设计必然涉及到最基本的两个信号——clk、rst_n(时钟、复位)。肯定有朋友会讲可以没有rst_n,是可以没有,但让代码更丰富、更严谨没有错。下面来看看激励clk、rst_n的写法:
首先先讲一下timescale,因为想要进行仿真首先要规定时间单位,而且最好在Testbench里面统一规定时间单位,而不要在工程代码里定义,因为不同的模块如果时间单位不同可能会为仿真带来一些问题,而timescale本身对综合也就是实际电路没有影响。 `timescale 1ns/ 1ps表示仿真的单位时间为1ns,精度为1ps。
`timescale 1ns/1ps
initial
clk = 0;
always
#10 clk <= ~clk;
`timescale 1ns/1ps
initial begin
clk = 0;
forever
#10 clk = ~clk;
end
`timescale 1ns/1ps
always begin
#10 clk <= 0;
#10 clk <= 1;
end
上述三种代码的目的就是产生系统时钟,给clk一个初值后,不断重复执行:每10ns翻转一次clk,从而生成一个周期为20ns,频率50MHz的方波信号。第一、二种基本类似,第三种比较简单,少了一个initial,放在了always里初始化。
三种方法都无一例外地给clk赋了初值,因为信号的缺省值为Z,如果不赋初值,则反相后还是Z,时钟就一直处于高阻Z状态。
根据复位方式的不同,rst_n一般有两种写法:
`timescale 1ns/1ps
initial begin
rst_n = 1;
#100 rst_n = 0;
#500 rst_n = 1;
end
//同步复位
`timescale 1ns/1ps
initial begin
rst_n = 1;
@(negedge clk) //等待时钟下降沿
repeat(3) @(negedge clk);
rst_n = 1;
end
上述两种代码的目的基本都是延时复位,但一个异步复位,一个同步复位,用途不同。
推荐!
//clock generate moudle
initial
begin
clk = 0;
forever #(PERIOD/2)
clk = ~clk;
end
//将复位信号封装在Task(任务)当中
task task_reset;
begin
rst_n = 0;
repeat(2) @(negedge clk); //等待clk出现两个下降沿之后,才继续执行后续语句
rst_n = 1;
end
endtask
最后“判断被测试设计的输出相应是否满足设计要求”。首先介绍最常用的两个系统任务函数 $ stop和$ finish。$ stop代表暂停仿真后返回软件操作主窗口,将控制权交给user;$ finish代表终止仿真后关闭软件操作主窗口。其他任务函数如$ monitor、$ display 、$ time、$ fwrite等也比较重要,用到的时候再一一介绍。
RTL代码:
* All rights Reserved, Designed By https://blog.csdn.net/zhaogoudan
* @projectName :led
* @title :
* @description :LED自加
* @author :zhaodongliang
* @date :21/01/07
* @version :V1.0.0
* @copyright ${YEAR} https://blog.csdn.net/zhaogoudan
*/
module led(
input clk,
input rst_n,
output reg [3:0] led_data
);
//---------------------------
//Delay for 0.3s
localparam DELAY_TOP = 24'd15_000_000;
//localparam DELAY_TOP = 24'd4; //just for test
reg [23:0] delay_cnt;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
delay_cnt <= 24'd0;
else if(delay_cnt < DELAY_TOP)
delay_cnt <= delay_cnt + 1'b1;
else
delay_cnt <= 24'd0;
end
wire delay_done = (delay_cnt == DELAY_TOP) ? 1'b1 : 1'b0;
//----------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
led_data <= 4'd0;
else if(delay_done) begin
if(led_data < 4'd15)
led_data <= led_data + 1'b1;
else
led_data <= 4'd0;
end
end
endmodule
testbench代码
`timescale 1 ns/ 1 ns
module led_vlg_tst();
localparam PERIOD = 20;
reg clk;
reg rst_n;
// wires
wire [3:0] led_data;
// assign statements (if any)
led i1 (
// port map - connection between master ports and signals/registers
.clk(clk),
.led_data(led_data),
.rst_n(rst_n)
);
initial
begin
clk = 0;
forever #(PERIOD/2)
clk = ~clk;
end
task task_reset;
begin
rst_n = 1'b0;
repeat(2) @(negedge clk)
rst_n = 1'b1;
end
endtask
//-------------------
//systerm initialization
task task_sysinit;
begin
end
endtask
//--------------------
//testbench of the RTL
initial
begin
task_sysinit; //必须先进行系统初始化
task_reset; //再进行复位
//然后才能进行其他的激励
end
endmodule
Testbench可以通过设计Task功能来实现任务的模块化编写及调用,而这些部分的目的就是为了在最后生成待测试的测试激励。
在测试激励生成的代码编写规范中,要遵守以下几点:
//testbench of the RTL
initial
begin
task_sysinit; //必须先进行系统初始化
task_reset; //再进行复位
//然后才能进行其他的激励
end
输入
必须为reg,因为对于testbench来说输入其实是要输出给测试模块的,也就是相当于需要一个东西来存储,所以是register类型
输出
必须为wire,因为对于testbench来说输出其实是测试模块的输入,也就是需要通过wire来进行连接