【深入浅出玩转FPGA学习12----Testbench书写技巧2】

深入浅出玩转FPGA学习11----Testbench书写技巧2

结构化Testbench

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代码应该注意的细节。

防止同时调用task

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
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周猿猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值