01signal.com

FPGA上的异步复位(Asynchronous resets): 并不像许多人认为的那么容易

此页面是 FPGAs中有关复位(resets)的系列中的三个中的第一个。因为很多人在使用异步复位时并不知道它们并没有像他们期望的那样真正工作,所以第一页解释了为什么整个主题并不像看起来那么容易。

你的复位真的好用吗?

考虑这个重置状态机(state machine)的示例,这是错误的:

always @(posedge clk or negedge resetn)
     if (!resetn)
       state <= ST_START;
     else
       case (state)
	 ST_START:
	   begin
	      state <= ST_NEXT;
	      [  ... do some stuff maybe? ... ]
	   end

	 ST_NEXT:
	   begin
	      [ ... do something ... ]
	   end
       endcase

那么这里的复位有什么问题,您可能想知道?这与他们在教科书中显示的完全一样!一个 active low 异步复位,它将实现 @state 的寄存器(register)带到其初始状态。会出什么问题?

为了便于讨论,我们假设 @resetn 信号本身没有问题。换句话说,它没有直接连接到按钮或类似的东西。可能使用了旨在生成复位信号(reset signal)的芯片,或者复位由 FPGA内部生成。无论如何,我们假设复位以稳定的方式变为活动状态,并且保持活动状态足够长的时间,然后变为非活动状态。这仍然是错误的。

当我说错时,我的意思是那种让 FPGA 时不时地表现得很奇怪的错误,没有明显的原因。

所以有什么问题?好吧,因为复位的活动时间足够长,它肯定会将 @state 带入初始状态。但是当变为非活动状态(即在上面的示例中变回 '1' )时会发生什么?这时候相关的触发器(flip-flops)应该开始响应 @clk的上升沿(rising edges)。

但是,触发器需要一点时间才能从复位信号中恢复,并开始在上升沿上对数据输入(data input)进行采样。而且由于复位按照定义是异步的,因此它可能随时相对于 @clk停用。

如果 @clk 的第一个上升沿(rising edge)在复位停用后过早到达,则触发器会忽略此上升沿。如果连接到 @clk 的所有触发器都这样做,那就太好了。但并非所有触发器都完全相同,有些人比其他人更早地获得时钟边沿(clock edge),有些人比其他人更晚获得复位的停用。

说实话,这个解释有点过于简单化了。为了更准确地了解问题,请参阅解释时序(timing)基础知识的页面上关于 Recovery 和 Removal 的部分。

所以归结为: 如果运气不好,复位可能会变得不活跃,离时钟的上升沿足够近,因此一些触发器会响应第一个上升沿,而其他人会忽略它。事实上,一些触发器可能需要一些额外的时间来决定要做什么。怪触发器在同一个芯片上的区别还是怪时钟偏移(clock skew)和复位偏移(reset skew): 底线是一些触发器比其他时钟周期(clock cycle)领先。

要了解这有多糟糕,请考虑上面的示例。如果综合工具(synthesizer)识别出这是状态机,那么它很有可能会使用 one-hot encoding实现状态变量(state variable)。换句话说,它为每个状态分配一个 single-bit 寄存器。当状态机位于相关的状态中时,这些寄存器中的每一个都处于活动状态。假设综合工具为名为 ST_START的状态分配了一个名为 hot_state_0 的寄存器,为 ST_NEXT分配了 hot_state_1 。显然,复位使 hot_state_0 激活并禁用 hot_state_1。

现在请注意,状态机无条件地从 ST_START 移动到 ST_NEXT。因此, hot_state_0 在复位发布后的第一个时钟上变为非活动状态,而 hot_state_1 变为活动状态。

但是,如果复位在不走运的情况下变得不活跃,以至于一些触发器错过了第一个时钟边沿(clock edge)而另一些没有呢?一种可能是 hot_state_0 错过了第一个时钟,但 hot_state_1 响应它。结果,两者都变为活动状态,这是 one-hot encoding的非法条件。如果反过来,两个寄存器(registers)都处于非活动状态,所以实际上状态机的所有 one-hot 寄存器都处于非活动状态。无论哪种方式,状态机都可能永远无法恢复到合法状态。

在实际中情况会如何?当然,这取决于应用程序,但很可能直到将复位再次应用于 FPGA 之前,某些东西才会正常工作。找出这种行为的原因可能极其困难,因为这个问题会随机出现,而且不一定经常出现。它的行为也可能因编译(compilation)和 FPGA 设计的不同而不同,也可能因电路板的不同而不同。简而言之,这是一种会让你抓狂的错误。在这次讨论中,这听起来可能并不是那么糟糕,因为问题的根源就是这次讨论的主题。但是,当这种不稳定在现实生活中发生时,它可能是什么原因造成的,而且人们常常觉得 FPGA 闹鬼了

但我一直这样做,而且有效!

的确。在绝大多数情况下,如果某些触发器错过了复位之后的第一个时钟,这并不重要。

上面的状态机示例失败的主要原因是它在第一个时钟周期(clock cycle)上保留了初始状态。现实生活中的设计中的大多数状态机都有远离初始状态的规则,所以在最初的几个时钟周期中它们总是停留在这个状态。所以一个人逃脱了这个错误。

但这是另一个例子。一个简单的计数器:

reg [15:0] counter;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       counter <= 0;
     else
       counter <= counter + 1;

在这种情况下, @counter 由 16个触发器(flip-flops)组成。每个触发器在其数据输入处接收下一个时钟周期的计数器值,在其异步复位输入(asynchronous reset input)处接收 @resetn 的值。

当 @resetn 处于活动状态时, @counter 的值为 0,而下一个时钟周期的计数器的值为 1。因此,除 counter[0] 之外的所有触发器都保持为零,无论它们是否错过了第一个时钟边沿。所以无论哪种方式它都会开始正确计数。在编写此类代码的绝大多数情况下,计数器是否错过了第一个时钟周期并不重要。

然而,这是一个不同的故事:

reg [15:0] counter;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       counter <= 0;
     else
       counter <= counter - 1;

一个很小的差异,但很重要: 如果计数器从零开始倒计时,则下一个时钟周期处的计数器的值是 0xffff。换句话说,所有触发器都必须在复位之后的第一个时钟上更改其值。因此,如果一些复位之后的第一个时钟边沿响应,而其他时钟边沿不响应,则计数器可以从几乎任何随机值开始。

但是谁会将计数器重置为零值然后倒计时呢?

所以这里有一个更现实的例子: 时钟使能信号(clock enable signal)使逻辑的行为就像时钟的频率降低了一半一样(因此如果需要的话可以使用 multi-cycle 路径):

reg en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       en <= 0;
     else
       en <= !en;

   always @(posedge clk)
     if (en)
       [ ... do something ... ]

人们可能会认为这里没有任何问题: 时钟使能(clock enable)、 @en是单个寄存器,因此它何时开始切换其实并不重要,还是……?问题是时钟使能信号往往具有较高的扇出(fan-out),因此综合工具可能会复制它以避免超过扇出的限制。

我对 Vivado 综合工具的轶事实验表明,实现 @en 的每个复制寄存器都依赖于自己的输出信号。换句话说,没有一个单一的信号可以让所有触发器用来决定他们的下一个输出(output)应该是什么。相反,有许多独立的触发器总是在时钟的上升沿上改变它们的价值。因此,如果这些触发器没有开始在同一个时钟周期上切换,它们的输出(outputs)将无限期地保持不同。

如果发生此类事故,逻辑很可能会完全失灵。因此,如果您坚持使用异步复位,至少要确保所有时钟使能都依赖于单一来源,可能如

reg pre_en; // Apply some don't-touch synthesis directive on this
   reg en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       pre_en <= 0;
     else
       pre_en <= !pre_en;

   always @(posedge clk or negedge resetn)
     if (!resetn)
       en <= 0;
     else
       en <= pre_en;

   always @(posedge clk)
     if (en)
       [ ... do something ... ]

诀窍在于 @pre_en 是决定下一个值的寄存器。触发器具有较低的扇出,并且可能具有一些属性(attribute),它们会告诉综合工具不要篡改它。这样,它肯定是一个寄存器。所有实现 @en 的触发器都依赖于 @pre_en,因此它们都同意下一个时钟上的 @en 的值。至于复位之后的第一个时钟,即使某些触发器错过了也没关系,因为第一个时钟周期上的 @en 的值无论如何都是零。

所以底线是错误地使用异步复位通常会很好,主要是因为逻辑通常可以容忍复位相对于时钟何时变为非活动状态的不确定性。然而,不小心使用异步复位可能会导致偶发的错误行为,这可能是极难解决的。

在复位和时钟之间使用时序约束(timing constraint)

避免复位的停用和时钟的上升沿之间不确定的时序关系的看似显而易见的方法是在复位信号上使用时序约束。然而这样做会使复位信号变得同步。

但是复位信号与哪个时钟同步?通常,为整个逻辑设计创建一个全局异步复位会很方便。此复位是使用与一个特定时钟同步的逻辑创建的。如果将此复位与与另一个时钟同步的逻辑一起使用,则我们有一个跨时钟域(clock domain crossing)。这本身就是一个主题,但最重要的一点是工具可能会忽略相关路径(paths)上的时序。

所以在异步复位上使用时序约束的出发点是每个时钟必须有一个单独的异步复位。或者,如果您坚持,为每组 related 时钟分配一个单独的异步复位。否则时序约束就没有意义了。如果这听起来很奇怪,那是因为异步复位不再是异步的。

Verilog 代码使用异步复位的模式这一事实没有任何区别。是否使用触发器的异步复位输入以及触发器是否配置为将其复位输入(reset input)视为异步都无关紧要: 如果复位与时钟同步,并且使用时序约束,那么复位实际上是同步的。对于这种情况,您可以考虑直接使用 Verilog 模式:

always @(posedge clk)
     if (!resetn)
       state <= ST_START;
[ ... ]

话虽如此, Intel的时序收敛(timing closure)Youtube 视频提倡使用与时钟同步的异步复位,也应该使用时序约束。选择异步复位是为了在 FPGA中利用全局布线(global routing)的专用资源。我觉得这很奇怪,因为即使全局布线也可以有一个大的时延(delay),特别是在大的 FPGAs上。但肯定有一些场景是有意义的。

实际上,即使异步复位是有效同步的,仍然使用异步复位还有另一个原因,这将在下一页讨论: 它允许异步传播复位信号的激活,这在模拟(simulations)以及 ASICs测试中很有用。

如果使用具有同步异步复位的此方法,请务必确保时序约束在转到触发器的异步复位输入的所有信号路径(signal paths)上强制执行。复位由与目标触发器相同的时钟同步的触发器生成这一事实本身并不能确保它们之间的路径是定时的。默认情况下,某些时序工具会忽略以异步输入结尾的任何路径,因此可能需要明确启用此强制执行。务必在时序报告(timing reports)中验证这些路径确实被时序约束覆盖。很容易落入这个。

本系列关于复位的第一页到此结束。下一页讨论复位的不同选项和FPGA 的初始化

此页面由英文自动翻译。 如果有不清楚的地方,请参考原始页面
Copyright © 2021-2024. All rights reserved. (b4b9813f)