Testbench也是能够做到可重用化的设计。下面用模块做一个结构化可重用的示例。
这是假设的待验证模块的顶层:
module prj_top(clk,rst_n,dsp_addr,dsp_data,dsp_rw···);
input clk;
input rst_n;
input [23:0] dsp_addr;
input dsp_rw;
inout [15:0] dsp_data;
···
···
endmodule
这是Testbench的顶层:
module tf_prj_top;
//这个例化适用于被例化文件(这里是print_task.v)不对待验证模块接口进行控制
//print_task.v里包含常用信息打印任务封装
print_task print();
//这个例化适用于被例化文件需要对待验证模块接口进行控制,和通常RTL设计中例化方法是一样的
//sys_ctrl_task.v里包含系统时钟产生的单元和系统复位任务
sys_ctrl_task sys_ctrl(
.clk(clk),
.rst_n(rst_n)
);
//dsp_ctrl_task.v包含DSP读/写控制模拟
dsp_ctrl_task dsp_ctrl(
.dsp_rw(dsp_rw),
.dsp_addr(dsp_addr),
.dsp_data(dsp_data).
···
);
//这里的端口例化需要注意的是,原来被测试模块的output为reg,如果被底层的例化模块控制,
//那么这个reg要改为wire类型进行定义,而底层模块要将其定义为reg
wire clk;
wire rst_n;
wire [23:0] dsp_addr;
wire dsp_rw;
wire [15:0] dsp_data;
···
//例化待验证工程顶层
prj_top uut(
.clk(clk),
.rst_n(rst_n),
.dsp_addr(dsp_addr),
.dsp_data(dsp_data),
.dsp_rw(dsp_rw),
···
);
//注意下面调用底层模块的任务的方式,例如sys_ctrl表示上面例化的sys_ctrl_task.v,sys_reset
//是例化文件中的一个任务,用"."做分割
initial begin
sys_ctrl.sys_reset(32'd1000); //系统复位1000ns
#1000;
dsp_ctrl.task_dsp_write(SELECT_STRB0,24'000001,16'h00ff); //DSP写任务调用
#1000;
dsp_ctrl.task_dsp_read(SELECT_STRB0,24'h000008,dsp_rd_data); //DSP读任务调用
...
print.terminate;
end
endmodule
调用层1代码如下:
//调用层1
module print_task;
//--------------------------------------------//
//常用信息打印任务封装
//-------------------------------------------//
//警告信息打印任务
task warning:
input [80*8:1] msg;
begin
$ write("WARNING at %t: %s", $time,msg);
end
endtask
//错误信息打印任务
task error;
input [80*8:1] msg;
begin
$ write("ERROR at %t :%s", $time,msg);
end
endtask
//致命错误打印并停止仿真任务
task fatal;
input [80*8:1] msg;
begin
$write("FATAL at %t :%s", $time,msg);
$write ("Simulation false\n");
$stop;
end
endtask
//完成仿真任务
task terminate:
begin
$write("Simulation Successful\n");
$ stop;
end
endtask
endmodule
调用层2代码如下:
//调用层2
module sys_ctrl_task(
clk,rst_n
);
output reg clk; //时钟信号
outpu reg rst_n; //复位信号
parameter PERIOD = 20; //时钟周期,单位ns
parameter RST_ING = 1'b0; //有效复位值,默认低电平复位
//---------------------------------------------------------//
//系统时钟信号产生
//--------------------------------------------------------//
initial begin
clk = 0;
forever
#(PERIOD/2) clk=~clk;
end
//------------------------------------------------//
//系统复位任务封装
//---------------------------------------------------//
task sys_reset;
input [31:0] reset_time; //复位时间输入,单位ns
begin
rst_n = RST_ING; //复位中
#reset_time; //复位时间
rst_n = ~ RST_ING; //撤销复位
end
endtask
endmodule
调用层3任务如下:
module dsp_ctrl_task(
dsp_rw,dsp_strb0,dsp_strb1,dsp_iostrb,dsp_addr,dsp_data
);
output reg dsp_rw; //DSP读写信号,低--写,高----读
output reg dsp_strb0; //DSP存储空间STRB0选通信号
output reg dsp_strb1; //DSP存储空间STRB1选通信号
output reg dsp_iostrb; //DSP存储空间IOSTRB选通信号
output reg[23:0] dsp_addr; //DSP地址总线
inout wire [15:0] dsp_data; //DSP数据总线
//print_task.v 里包含常用信息打印任务封装
print_task print();
//------------------------------------------------------------------------------------------------------------//
//模拟DSP读写任务封装
//------------------------------------------------------------------------------------------------------------//
//DSP地址空间选择
parameter SELECT_STRB0 = 2'd1,
SELECT_STRB1 = 2'd2,
SELECT_IOSTRB = 2'd3;
reg[15:0] dsp_data_reg; //DSP数据总线寄存器
assigin dsp_data = dsp_rw ? 16'hzz : dsp_data_reg;
reg rd_flag; //任务忙标志位,用于防止同时调用该任务
reg wr_flag; //任务忙标志位,用于防止同时调用该任务
initial begin
rd_flag = 0; //DSP 读任务不忙
wr_flag= 0; //DSP写任务不忙
//DSP信号接口初始化
dsp_rw=1;
dsp_data_reg = 16'hzzzz;
dsp_addr = 24'hzzzzzz;
dsp_strb0 = 1;
dsp_strb1 = 1;
dsp_iostrb = 1;
end
reg h1; //DSP时钟模拟,h1为DSP指令周期
initial begin
h1 =1'b0;
forever
#20 h1 =~ h1;
end
//模拟DSP读FPGA任务
task task_dsp_read;
input[1:0] tcs; //片选输入
Input[23:0] taddr; //地址输入
output[15:0] tdata; //数据读出
begin
...
end
endtask
//模拟DSP写FPGA任务
task task_dsp_write;
input[1:0] tcs; //片选输入
input[23:0] taddr; //地址输入
input[15:0] tdata; //数据写入
begin
...
end
endtask
endmodule
在同一时刻对同一个寄存器进行读/写容易发生紊乱状态。如以下的例子,第1个always块对count操作(写),第2个always却要显示它,那么会出现什么状态呢?
module rw_race(clk);
input clk;
integer count;
always @ (posedge clk)
begin
count = count + 1;
end
always @ (posedge clk)
begin
$write ("Count is equal to %0d\n", count);
end
endmodule
由于Testbench的运行是基于PC机的,处理的时候也是分时服用的,所以这两个always块也会先后执行。也就是说,会出现两种情况。这里假设count在执行前为10,若先执行第1个块,那么第2个块执行后的结果显示为count=11;若先执行第2个块在执行第1个块,显示的结果为count=10。
这样紊乱状态往往不是我们希望看到的,这可能会给测试工作带来许多不必要的麻烦。那么,有什么什么解决方法呢?可以先看看下面这段代码。
module rw_race(clk);
input clk;
integer count;
always @ (posedge clk)
begin
count <= count +1;
end
always @ (posedge clk)
begin
$write("Count is equal to %0d\n", count);
end
endmodule
采用非阻塞赋值语句后,这个紊乱的状态就会得到解决。在第1个always块count增加的同时第2个always块也在执行,那么最后显示的count值是count增1之前的数值。
再看下面的例子。
module rw_race;
wire [7:0] out;
assign out = count + 1;
integer count;
initial
begin
count = 0;
$write( " Out = %b\n",out);
end
endmodule
以上代码执行后会得到什么结果呢?这取决于测试者使用的仿真器和命令行。一般来说,Verilog—XL会输出“xxxxxxxx", 而VCS则会认为是”00000001“。 那么如何改进呢?看下面的代码。
module rw_race;
wire [7:0] out,tmp;
assign #1 out = tmp -1;
assign #3 tmp = count +1;
integer count;
initial
begin
count = 0;
#4; //out的值为0
$ write("Out= %b\n", out);
end
endmodule
这些都是编写一个好的Testbench代码应该注意的细节。
Testbench使用的是硬件语言,而其依赖的环境却是基于PC的软件平台,这也就决定了其独特的代码风格。有时的的确确是以一个软件式的顺序方式在给待测试硬件代码做测试,但是写出来的Testbench代码中却时常布满了并行执行的陷阱。这给硬件测试者带来了不少麻烦,既然选择了Verilog,那么就得领会它在硬件测试环境下的特殊性。或者说,应该掌握一些常用的技巧来避免这些问题,让Testbench更高效的执行。
下面给出使用task的一个常见冲突以及解决方法。
task write;
input [7:0] wadd;
input [7:0] wdat;
begin
ad_dt <= wadd;
ale <= 1'b1;
rw <= 1'b1;
@ (posedge rdy);
ad_dt <= wdat;
ale <= 1'b0;
@ (negedge rdy);
end
endtask
initial write(8'h5A, 8'h00);
initial write(8'hAD, 8'h34);
上面的task实现了向存储器的指定地址写入指定数据的功能。由于Verilog中always 和initial在实际执行时都是并行工作的,这就很有可能出现上面两个initial同时进行task调用、同时需要写存储器的情况,冲突的结果无法预料。
那么如何解决这样的问题呢?看下面改进后的代码:
task write;
input [7:0] wadd;
input [7:0] wdat;
reg in_use;
begin
if (in_use === 1'b1) $stop;
in_use = 1'b1;
ad_dt <= wadd;
ale <= 1'b1;
rw <= 1'b1;
@ (posedge rdy)
ad_dt <= wdat;
ale <= 1'b0;
@ (posedge rdy)
in_use = 1'b0;
end
endtask