本文转载自:单刀FPGA
1、时序逻辑电路落后一拍?
FPGA初学者可能经常听到一句话:“时序逻辑电路,或者说用 <= 输出的电路会延迟(落后)一个时钟周期。”但在仿真过程中经常会发现不符合这一“定律”的现象–明明是在仿真时序逻辑,怎么输出不会落后一拍?
先来看一个简单的例子:把输入信号用时序逻辑电路寄存两次,即俗称的“打两拍”。Verilog代码如下:
module test(
input clk, //系统时钟;
input rst, //系统复位,高电平有效;
input [1:0] in,
output [1:0] out
);
reg [1:0] in_r,in_rr; //分别打一拍、打两拍
assign out = in_rr;
always@(posedge clk or posedge rst)begin
if(rst)begin
in_r <= 2'd0; //复位初始值
in_rr <= 2'd0; //复位初始值
end
else begin
in_r <= in; //输入打一拍
in_rr <= in_r; //输入打两拍
end
end
endmodule
然后再写个TB文件来仿真一下:
`timescale 1 ns/1 ns
module tb_test();
//输入输出端口
reg clk;
reg rst;
reg [1:0] in;
wire [1:0] out;
//例化被测试模块
test u_test (
.clk (clk),
.rst (rst),
.in (in ),
.out (out)
);
//生成系统时钟,周期10ns;
initial begin
clk = 1;
forever #5 clk = ~clk;
end
//生成复位信号
initial begin
rst = 1;
#25;
rst = 0;
end
//生成输入信号(测试激励)
initial begin
in = 0;
#30;
repeat(8)begin //循环8次;
#10 in = in + 1; //输入递增1
end
$stop; //停止仿真
end
endmodule
这段测试代码的测试逻辑是:在复位完成后,每10ns依次对输入信号in执行+1操作,观察打一拍信号in_r和打两拍信号in_rr的变化。来看下仿真结果:
. 输入信号in在每个时钟上升沿递增1,符合预期
. 信号in_r原本应该落后信号in一个时钟周期,但上图中二者却完全同步,不符合预期!
. 信号in_rr落后信号in_r一个时钟周期,符合预期
那么,是什么导致仿真结果与预期目的不符?
2、建立时间、保持时间和数据输出延迟
在FPGA设计中所用的底层时序逻辑单元是D触发器(DFF,D Flip-Flop),在理想状况下,可以认为DFF的变化是瞬态的,即输出从0到1或者从1到0,都是在一瞬间完成。但在实际使用中,这种瞬态变化显然不可能存在,所以寄存器的输出必定需要一些时间,而这个时间就是Tco。
上图是一张DFF的非理想状态下的数据传输示意图,为此需要明确3个概念:
. Tsu:D端的数据必须在时钟上升沿到来之前的一定时间内就已经保持稳定,该时间被称为D触发器的建立时间(Tsu)。
. Th:D端的数据必须在时钟上升沿到来之后的一定时间内继续保持稳定,该时间被称为D触发器的保持时间(Th)。
. Tco:D端的数据不可能会在时钟上升沿出现的那一刻就立即更新到Q端,从时钟的上升沿到D端的数据稳定出现在Q端,也有一个时间,该时间称为寄存器的时钟到输出延迟(Tco)。
上面的概念理解两点即可:
1.如果不满足建立时间和保持时间要求,则DFF的输出可能会出现亚稳态,简单理解就是输出容易不正常,会出问题。
2.每个时钟上升沿(或下降沿)DFF都会从输入端采集数据并将其更新到输出端,但这个过程需要时间(Tco),所以数据的输出实际上会落后时钟上升沿一些时间。
3、时序逻辑电路落后一个周期的原因
接下来继续分析上面的仿真结果。
. 在①处,信号in_r在发生变化,由于Tco的存在,所以in_r从0到1的时间是不会和上升沿同步的,会落后一点点。仿真波形表示的不是很明显,但也用了一个小小的斜坡来表示这一过程。
. 在①处,信号in_r的变化会落后于上升沿,信号in_rr在①处采集到的值则仍是信号in_r未变化的值,即0。
. 在②处,信号in_r的变化同上一个时钟①处类似。信号in_r的变化会落后于上升沿,信号in_rr在②处采集到的值则仍是信号in_r未变化的值,即1。
这样理解起来可能还是不够直观,没事,我们把代码做一些小小的改变:
in_r <= #1 in; //输入打一拍
in_rr <= #1 in_r; //输入打两拍
只是把输出语句加一个 “#1”,即输出会延迟1ns。聪明的你应该已经看出来了,这就是用来模拟Tco这个概念的。
继续看仿真结果,是不是一目了然?
. 在①处,由于Tco的存在,所以in_rr采集到的in_r的值是0,所以in_rr输出也是0,而且输出会落后一个Tco时间(但是由于值相同,所以看不出来)
. 在②处,由于Tco的存在,所以in_rr采集到的in_r的值是1,所以in_rr输出也是1,而且输出也会落后一个Tco时间
所以现在我们清楚了,时序逻辑电路的输出根本就不会落后一个时钟周期,而只会落后一个Tco时间。二者之所以看上去会落后一个周期,完全是由于前级输出的Tco时间存在,导致后级电路在当前时钟上升沿无法采集到最新值,而只能采集到前级未变化的值!
4、为什么把时序逻辑仿成了组合逻辑?
上面的仿真还有个问题悬而未决,那就是in_r是in被寄存后的信号,为啥没有落后in一个时钟周期?问题出在仿真机制和TB文件中对in的赋值方式上。
在TB中,我们是这么对in赋值的:
#10 in = in + 1; //输入递增1
注意看,用的是阻塞赋值“ = ”,阻塞赋值“ = ”一般用来描述组合逻辑,而非阻塞赋值“ <= "则一般用来描述时序逻辑。
虽然输入信号in是我们构建的一个虚拟向量,但对于被测试模块来说,这个激励仍然被视作是来自于上级模块的输出,所以需要指明它到底是一个组合逻辑的输出值还是一个时序逻辑的输出值。
如果它是用“ = ”来描述的,那它就是来自组合逻辑,而组合逻辑的输出是不与时钟上升沿有关的,它的输出几乎就是瞬时完成的。如果它是用“ <= ”来描述的,那它就是来自时序逻辑,而时序逻辑的输出则会落后时钟上升沿一个Tco时间。
回到上面的仿真结果,由于信号in使用“ = ”来赋值,所以它的每一次更新都几乎与时钟上升沿同步,并不会有Tco时间的存在,每一个上升沿后级的in_r信号都能采集到最新的in值,所以二者并不会有一个周期的延迟。
假如我们把信号in改成“ <= ”这种赋值方式:
#10 in <= in + 1; //输入递增1
那么仿真结果就是这样了:
这与我们最初料想的一致:打一拍信号in_r落后输入信号in一个时钟周期,打两拍信号in_rr后输入信号两个时钟周期。
如果说还有问题的话,就是输入信号in的变化没有很好的体现Tco时间,所以再修改一下:
#10 in <= #1 in + 1; //输入递增1
嗯,这样就没问题了。由于采用了“#1”这种赋值方式来模拟Tco的存在,所以你应该再也不会搞错信号在哪个时钟沿采样和哪个时钟沿变化了。
5、总结
. 时序逻辑电路的输出不是瞬时发生的,而是需要一定的时间,这个时间就是Tco
. 时序逻辑电路并没有真正意义上的落后一拍,落后一拍的原因是因为Tco的存在,导致在当前时钟上升沿无法采集到最新的值,而只能采集到未变化的值
. 在仿真时,输入信号尽量用非阻塞赋值“<=”来模拟其来自寄存器的输出,这样的仿真结果更接近实际电路
. 可以采用“#1”这种赋值方式来模拟Tco的存在,这可以在仿真时带来很大的便利