FPGA设计中的隐式依赖:组合逻辑与时序逻辑互相赋值的竞争与冒险

一、 写在前面

二、 问题引入

三、 逻辑分析

3.1 核心逻辑设计(情景一、情景二)test1a

3.2 控制模块设计(仅情景二)test1b

 3.3 情景一  测试代码

 3.4 情景二  测试代码

四、 仿真分析

4.1 仿真结果

 4.2 问题阐述

4.3  问题分析

 五、 总结

六、 写在后面


一、 写在前面

文章讲述了关于在FPGA设计当中遇到了仿真时序与实际时序不一致的问题,通过仿真分析解决了问题。

二、 问题引入

在进行FPGA设计过程中,在进行实际测试以及仿真时,发现设计代码在运行过程中可以正常运行,但是在仿真过程中,仿真结果却出现了些许的问题。经过分析验证,发现是代码中存在组合逻辑与时序逻辑,并且在这两种逻辑中,存在变量的相互赋值。并且在仿真过程中,发现了变量赋值的竞争关系。通过简化逻辑,抽象出了一个简单的案例并进行分析。

三、 逻辑分析

由于源代码较为复杂,因此我对Verilog代码进行了抽象处理,设计了一个类似的逻辑模块,对问题进行复现,以记录本次代码设计出现的仿真时序紊乱情况。

为了完成比对,在这里设计两份仿真代码(情景一、情景二),其代码实现功能相同。

3.1 核心逻辑设计(情景一、情景二)test1a

我们需要设计代码完成一下逻辑任务

首先输入数据input_data,输出数据out_data并设计组合逻辑变量wire_data, 令wire_data等于输入的数据input_data,输出的数据out_data等于上一时刻的wire_data。为此设计:

定义输入数据[7:0] input_data;

定义wire类型的变量[7:0] wire_data;

定义输出数据[7:0] out_data;

设计组合逻辑:assign 变量(wire_data)等于input_data  + out_data;

设计时序逻辑:always@(posedge clk) out_data <= wire_data;

代码如下所示:

module test1a(
	input 				sys_clk,
	input 		[7:0]	input_data,
	input 				sys_rst_n,
	input 				en,
	output	reg [7:0]	out_data
);
wire [7:0] wire_data;
assign 	wire_data = input_data+out_data;

always @(posedge sys_clk or negedge sys_rst_n)begin
	
	if(!sys_rst_n)begin
		out_data <= 1'b1;
	end else if(en)
		out_data <= wire_data;
	else;
end
endmodule

我主要想验证组合逻辑变量与时序逻辑变量在 输入数据以及时钟 发生变化时,其是如何进行变化的。

根据代码可分析得到,当输入数据input_data,发生改变,wire类型的wire_data会立即发生改变,等待时钟上升沿,out_data会被赋值上一时刻的wire_data的值。但实际情况是,in_data变化时,会伴随着时钟上升沿(这也是作者在进行仿真时发生设计错误的原因)。对于此类情况我们接下来进行分析。

3.2 控制模块设计(仅情景二)test1b

下面我们先来设计另一段代码,该代码是控制test1a模块的输入数据input_data以及使能信号en发生变化的模块。我们将其命名位test1b。我们简单设计该模块,代码如下:

module test1b(
	input 				sys_clk		,	
	input 				sys_rst_n	,
	input 				en_1b		,
	output	reg [7:0]	data_in1a	,
	output	reg			en_1a
);
reg [7:0] cnt;
always @(posedge sys_clk or negedge sys_rst_n)begin
	
	if(!sys_rst_n)begin
		en_1a <= 1'b0;
		data_in1a<=8'b0;
		cnt <=1'b0;
	end else begin
		if(en_1b)begin
			en_1a<=1'b0;
			case(cnt)
				8'd0: begin
					en_1a<=1'b1;
					cnt <= cnt+1'b1;
					data_in1a <=  8'd2;
					end
				8'd1: begin
					en_1a<=1'b1;
					cnt <= cnt+1'b1;
					data_in1a <= 8'd1;
					end
				8'd2: begin
					en_1a<=1'b1;
					cnt <= cnt+1'b1;
					data_in1a <= 8'd2;
					end
				8'd3: begin
					en_1a<=1'b1;
					cnt <= cnt+1'b1;
					data_in1a <= 8'd3;
					end
				default:;
			endcase
		end else ;
	end
end
endmodule

输入使能信号en_1b,当en_1b为高电平时,开始对test1a模块进行使能,并输出数据data_in1a,连接至test1a模块的input_data,并在赋值期间对en_1a信号进行拉高,其余时间进行拉低。当然,其仅我们为了简单,仅对data_in1a数据赋值了4次。

我们在开始时设计en_1a<=1'b0; 但是在每个case的情况下又设计了en_1a<=1'b1;这里虽然verilog的赋值语言是并行的,但是编译器会自动整合代码,使用下面的赋值语句。也就是说,在满足case的情况下,en_1a会执行en_1a<=1'b1;而不会执行en_1a<=1'b0;

 3.3 情景一  测试代码

下面我们编写两份测试代码(一份时序存在隐藏错误,另一份不存在时序错误)。

 首先看存在错误的一版(情景一)

`timescale 1ns / 1ps
module temp_tb1();

reg				sys_clk;
reg				sys_rst_n;
reg				en;
reg	[7:0]		input_data;
wire [7:0]		out_data;


// 仿真时钟周期定义,可根据实际情况修改
parameter CLK_PERIOD = 10;

test1a u_test1a(

	.sys_clk	(sys_clk	),
	.input_data	(input_data	),
	.sys_rst_n	(sys_rst_n	),
	.en			(en			),
	.out_data   (out_data 	)
);

initial begin
	sys_clk 	= 0;
	sys_rst_n 	= 0;
	en			= 0;
	input_data  = 8'd0;
	#(CLK_PERIOD/2-1);
	sys_rst_n	= 1'b1;		// 设置使能信号
	#1
	#(CLK_PERIOD);
	#(CLK_PERIOD) 	en =1;
					input_data = 8'd2;
	#(CLK_PERIOD) 	input_data = 8'd1;
	#(CLK_PERIOD) 	input_data = 8'd2;
	#(CLK_PERIOD) 	input_data = 8'd3;
	#(CLK_PERIOD) 	en = 8'd0;
end

always # (CLK_PERIOD/2) sys_clk = ~sys_clk;


endmodule

该仿真代码不通过test1b模块对其进行控制,直接对test1a模块的输入数据进行赋值以及使能。

为了与test1b的控制输入数据以及使能信号一致,我们先对模块做复位处理。

然后对test1a模块进行使能的同时,对input_data赋值第一个值input_data = 8'd2;

此后赋值三个另外的值(与test1b控制输出的值相对应)。

在赋值完最后一个值之后,等待下一时钟周期的上升沿对使能信号拉低。

 3.4 情景二  测试代码

之后,我们编写另一份测试代码,通过控制test1b的使能来控制test1a的工作:

`timescale 1ns / 1ps
module temp_tb2();

reg				sys_clk;
reg				sys_rst_n;


// 仿真时钟周期定义,可根据实际情况修改
parameter CLK_PERIOD = 10;


reg 		en_1b;
wire [7:0] 	data_in1a;
wire [7:0] 	out_data;
wire 		en_1a;

test1a u_test1a(

	.sys_clk	(sys_clk	),
	.input_data	(data_in1a	),
	.sys_rst_n	(sys_rst_n	),
	.en			(en_1a		),
	.out_data   (out_data 	)
);
test1b u_test1b(

	.sys_clk	(sys_clk	),	
	.sys_rst_n	(sys_rst_n	),
	.en_1b		(en_1b		),
	.data_in1a	(data_in1a	),
	.en_1a      (en_1a     	)
);

initial begin
	sys_clk 	= 1'b0;
	sys_rst_n 	= 1'b0;
	en_1b		= 1'b0;
	#(CLK_PERIOD/2-1);
	sys_rst_n	= 1'b1;		// 设置使能信号
	en_1b		= 1'b1;
		
end

always # (CLK_PERIOD/2) sys_clk = ~sys_clk;


endmodule

在这里,我们将test1a与test1b的使能/数据信号进行连接。在开始时,对各个模块进行复位,并失能test1b模块。

经过半个周期后,对复位信号拉高(低电平复位),并使能test1b模块,以此来完成对test1a模块的input_data信号的赋值以及对en信号的拉高。

四、 仿真分析

4.1 仿真结果

首先看情景一代码的仿真结果(数据为test1a模块的数据):

情景一 仿真结果

下面是情景二代码的仿真结果(数据依然为test1a模块的数据):

情景二 仿真结果

我们发现,第一份代码的仿真结果与第二份代码不相同。

第一份代码中,在时钟上升沿来临时,out_data的赋值并不等于上一时刻的wire_data。(实际依然是上一时刻的wire_data,只是时序紊乱,待会进行分析)

第二份代码的out_data的赋值为上一个wire_data的值。

 4.2 问题阐述

问题出现在哪里那?

继续观察,发现第一份代码的out_data总是领先与第二份代码的wire_data一个周期。

正常的时序逻辑应为:

开始时,wire_data=1,在下一个时钟周期来临时,输入数据为2,则out_data的值为上一周期的wire_data=1,然后wire_data的值:assign wire_data = out_data + input_data;值为3。

第二份仿真代码确实如此,但是第一份发生了什么那? 

情景一 仿真结果

开始时,wire_data=1,在下一个时钟周期来临时,输入数据为2,out_data值直接变成了1+2=3;

下一时刻输入input_data=1,out_data的值变成了1+3=4;

再下一时刻,输入input_data=2,out_data的值变成了4+2=6;

再下一时刻,输入input_data=3,out_data的值变成了6+3=9;

发现了吗?第一份代码的仿真时序逻辑是这样的:

开始时,wire_data=1。

在下一个时钟周期来临时,输入数据为2,wire_data的值率被赋值 assign wire_data = input_data + out_data;  wire_data变为了3,此后,out_data被赋值为wire_data,out_data变为了3,由于wire_data为组合逻辑,瞬间变为了input_data + out_data = 2 + 3 = 5;

下一时刻输入input_data=1,wire_data的值率被赋值 assign wire_data = input_data + out_data;  wire_data变为了4,此后,out_data被赋值为wire_data,out_data变为了4,由于wire_data为组合逻辑,瞬间变为了input_data + out_data = 4 + 1 = 5;

下面的时刻分析相同。

4.3  问题分析

为什么会发生这种情况那?两份代码为什么会出现不同的结果?

我们先来看第一份仿真文件:

initial begin
	sys_clk 	= 0;
	sys_rst_n 	= 0;
	en			= 0;
	input_data  = 8'd0;
	#(CLK_PERIOD/2-1);
	sys_rst_n	= 1'b1;		// 设置使能信号
	#1
	#(CLK_PERIOD);
	#(CLK_PERIOD) 	en =1;
					input_data = 8'd2;
	#(CLK_PERIOD) 	input_data = 8'd1;
	#(CLK_PERIOD) 	input_data = 8'd2;
	#(CLK_PERIOD) 	input_data = 8'd3;
	#(CLK_PERIOD) 	en = 8'd0;
end

在将en拉高时,input_data也紧跟被赋值。

此时,test1a中的assign语句会直接将wire_data进行赋值。同时,alwyas语句块就会检测到en信号为高,之后启动对out_data的赋值。但此时,wire_data已经被赋值为input_data+out_data=3。所以out_data被赋值的上一时刻的wire_data的值为3,因此out_data被赋值为3。

之后组合逻辑wire_data会被立刻赋值为3+2 = 5。此后逻辑类似。

接下来我们看第二份仿真文件:

initial begin
	sys_clk 	= 1'b0;
	sys_rst_n 	= 1'b0;
	en_1b		= 1'b0;
	#(CLK_PERIOD/2-1);
	sys_rst_n	= 1'b1;		// 设置使能信号
	en_1b		= 1'b1;
		
end

在sys_rst_n复位结束后,sys_clk的上升沿会同时进入test1a与test1b两个模块。

test1b检测到sys_clk上升沿,判断en信号为高电平,对en_1a信号拉高,同时对data_in1a信号进行赋值。在test1b检测到clk上升沿时,test1a同时检测到clk上升沿,但是此时,test1b的en_1a还未被赋值为高,也就是test1a模块并未检测到en信号,不进行对out_data的赋值操作。

这时,test1b模块也完成了对en_1a信号的拉高以及对data_in1a信号的赋值,在test1a模块中,组合逻辑的wire_data由于data_in1a(input_data)的改变而被重新赋值为1+2=3;

在下一个时钟的上升沿时,test1a模块与test1b模块同时检测到上升沿,test1a开始对data_in1a(input_data)信号赋值为1。同时,test1b模块并未检测到data_in1a(input_data)变化,wire_data还是原始值。而test1b的alway语句块检测到了en信号为高电平,将wire_data信号赋值给了out_data,out_data = 3。

此后,data_in1a(input_data)的值被test1b模块改变为1,test1a模块检测到变化,将wire_data的值改变为input_data+out_data = 1+3 = 4。

此后时刻的逻辑也相同。

 五、 总结

总结来说,第一份代码的 input_data的赋值与clk信号同时发生,test1a信号在检测到clk上升沿的过程中也检测到了input_data信号的变化,因此assign语句时wire_data发生赋值,之后out_data赋值为了变化后的wire_data,这是wire_data由由于为组合逻辑发生了input_data+out_data的赋值变化。

而第二份代码的clk同时输入到test1a与test1b两个模块,而idata_in1a(input_data)的赋值是在检测到上升沿之后,因此out_data会先被赋值为为变化的wire_data,此后wire_data检测到data_in1a(input_data)的变化以及out_data的变化被重新赋值。

六、 写在后面

作者为FPGA初学者,上述笔记仅记录在学习中遇到的问题,观点仅个人分析得出的结论,不确保100%正确。若存在错误欢迎留言批评指正。若转载请标明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值