一、 写在前面
文章讲述了关于在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%正确。若存在错误欢迎留言批评指正。若转载请标明出处。