这是 FPGAs中关于复位(resets)系列的第三页,也是最后一页。建议在此之前阅读前两篇。
概述
花时间设置适当的逻辑以创建复位总是一个好主意。即使设计没有明显的问题,在设计中跳过这个阶段也可能会造成损失: 稍后,您可能会花费数天时间尝试解决一些不稳定问题,但没有意识到这是因为逻辑没有正确初始化。随着时间的推移,在尝试解决此类问题的过程中,设计积累了在不了解问题根源的情况下做出的丑陋变通办法。此页面上有关奇怪的不稳定性的更多信息。
从项目一开始就仔细考虑和设计复位信号(reset signals)也很重要,以确保它在添加新功能时能够正常发展。当从头开始启动 FPGA 项目时,通常会先实现其核心功能,然后随着时间的推移添加功能。由于不同的模块通常需要单独的时钟和复位,因此很容易陷入为每个部分分别快速记下某些内容的陷阱。这通常会使项目在进展过程中变得越来越混乱。
通过使用从第一天开始就正确编写的用于包含复位和时钟的中央模块可以更容易地避免混乱的项目。如下文进一步解释,复位控制器(reset controller)和时钟资源(clock resources)(锁相环(PLLs)和时钟缓冲器(clock buffers)是必要的)相互影响,因此将它们放在同一个模块中是值得的。我通常给这个模块取名为 clkrst.v。
但是,通常不可能将所有这些都集中在一个模块中,因为某些 IP cores、子系统或设计 blocks (design blocks)可能会生成自己的复位和时钟组。在这种情况下,需要仔细考虑什么依赖于什么,以及整个系统应该如何响应来自不同来源的复位请求。例如, PCIe block 的复位几乎总是来自总线本身,而 block 会生成复位信号供连接到此 block的逻辑使用。在这种情况下,需要考虑,例如,如果复位来自 PCIe 总线(包括完全不响应的可能性),系统通常应该如何响应。
由于每个项目都有自己的故事,因此没有适用于所有情况的单一解决方案。此页面描述概念并提出想法和代码片段,它们可用作您自己的复位控制器的构建块。但是,这些代码片段并非用于直接剪切和粘贴到项目中,而应被视为演示。
为了简单起见,我假设系统中没有其他 block 既不生成时钟也不生成复位,但是将控制器(controller)扩展到一般情况是相当简单的。
复位状态机(reset state machine)
在大多数设计中,主要有两种情况需要重置:
- FPGA之后紧接着上电(powerup)和比特流 configuration(bitstream configuration),以确保干净启动。
- 当明确请求复位时,通过按下按钮或来自处理器或计算机的命令。
此外,当 watchdog timers 到期或通过其他方式检测到重大系统故障时,可能需要重置。
对所有这些情况的预期反应是从根本上重新启动,以确保无论出现什么问题,它都会得到纠正。这通常最好使用某种状态机(state machine)来完成,以确保复位顺序(reset sequence)是一致且可重复的,无论它是出于什么原因启动的。
话虽如此,在某些情况下,本地、可能重复的重置是有意义的。例如,处理 video image 帧的逻辑可以在每个帧(frame)开始之前重置。这通常需要一种轻量级机制,归根结底就是在激活本地同步复位(synchronous reset)时为多个寄存器(registers)分配初始值。这是一种像其他任何重置机制一样的重置机制,通常是一种确保稳健运行的简洁方法。
但由于关于这种可能性没有太多可补充的内容,本页的其余部分将重点介绍涵盖整 FPGA的基本复位。
不稳定的时钟和对复位的需求
使用 FPGA自己的锁相环来生成时钟是很常见的(通常推荐)。但是这些锁相环通常在 FPGA 上的所有其他逻辑激活的同时开始运行。结果,依赖于这些时钟的逻辑被喂给了不稳定的时钟,其频率可能明显高于正常值。
出现这种情况时,相关逻辑路径(logic paths)的时序(timing)不保。因此,任何依赖于由锁相环生成的时钟的逻辑都应被视为在锁相环被锁定之前未能实现时序约束(timing constraints)。
适当且常见的解决方案是将所有此类逻辑保存在复位中,直到相关的锁相环被锁定。或者,可以将此逻辑设置为忽略时钟,直到锁相环被锁定,例如通过确保触发器(flip-flops)的时钟使能输入(Clock Enable input)(CE) 处于非活动状态。
如果外部时钟直接从 FPGA的引脚连接到其逻辑单元(logic elements),那么问题是时钟 generator (clock generator)(通常是锁相环)是否比 FPGA 加载比特流(bitstream)更快。确定这一点并不总是那么容易。
因此,大多数设计中的许多同步逻辑单元(synchronous elements)都需要重置。或者更准确地说,在时钟不稳定是否需要复位的问题上,可以将这些 elements 分为三组:
- 那些可能包含 garbage 数据的,这不是问题。
- 只要时钟不稳定(例如通过时钟使能(clock enable)),就可以保证忽略它。
- 那些需要重置的。
值得一提的是,部分 FPGAs 允许设置 configuration 进程(即加载比特流和初始化 FPGA的进程),使得 FPGA的唤醒延迟到所有锁相环都被锁定。这可能是解决此问题的一种方法。这种方案的缺点是,如果外接时钟出现问题, FPGA 根本不会启动。像这样的情况可能非常令人困惑。
一个简单的状态机
由于基本启动或重新启动是非常罕见的事件,因此它是否需要多于几微秒并不重要。在许多情况下,甚至高达 100 ms 左右都可以,并且可以利用这一点。因此,普通的计数器是实现一系列事件的简单方法。
以最简单的形式,它可以归结为:
reg [4:0] reset_count;
reg rst_src_pll, rst_src_shreg, rst_src_debounce;
reg clear_counter;
reg master_reset;
initial reset_count = 0;
initial master_reset = 1;
initial clear_counter = 1;
always @(posedge wakeup_clk)
begin
clear_counter <= rst_src_pll || rst_src_shreg || rst_src_debounce;
master_reset <= (reset_count != 31);
if (clear_counter)
reset_count <= 0;
else if (reset_count != 31)
reset_count <= reset_count + 1;
end
@rst_src_pll、 @rst_src_shreg 和 @rst_src_debounce 代表不同的复位系统原因。这些寄存器(registers)由其他一些逻辑给定值。我将介绍一些这样的逻辑示例,但现在重要的是这些寄存器与 @wakeup_clk 同步(因此不需要跨时钟域(clock domain crossing))。
@clear_counter 是复位的这些原因中的逻辑或(logic OR)。这个寄存器将 @reset_count 更改为零。否则,这个计数器从 0 变为 31(在此示例中),然后停止。
最后,只要 @reset_count 还没有完成计数, @master_reset 就处于活动状态。这是复位状态机的最简单形式,数到 31 也相当适中。
因此,只要任何一个 @rst_src_N 信号处于活动状态,即使只有一个时钟周期(clock cycle),同步复位也会对 31个时钟周期处于活动状态。
长复位脉冲(reset pulse)有两个优点: 首先,如果相关的 @rst_src_N 随机开启和关闭(例如,摆动锁相环 lock detectors(PLL lock detectors)、按钮、多次请求复位的软件),这些多次激活不会传播到可见的同步复位。虽然这样的多次激活通常是无害的,但它们可能会导致输出引脚(output pins)的不必要活动。此类活动可能会产生负面影响,例如使测试电子设备的人误认为有问题。
从这个意义上说,31时钟周期(clock cycles)的例子非常简约。如果这样的延迟是可以接受的,最好算到对应于 10-100 ms的值。通过这样做,复位控制器隐藏了任何比此更短的摆动。
长复位脉冲的第二个原因是将原始同步复位分发到本地副本(如前所述)涉及一个时钟周期的延迟。如果复位在整个逻辑中的分布需要多次复制信号,那么总延迟不仅会变长,而且可能还会不均匀。长复位脉冲确保在某个时间点,所有逻辑都暴露于活动同步复位。在复位路径(reset path)中出现不均匀延迟仍然是一个坏主意,因为复位的停用也会变得不均匀,但有时这没有问题。对于这个问题,复位脉冲的 31时钟周期比可能需要的要长得多,但它不会受到伤害。
复位用于其他时钟域(clock domains)
@master_reset 是普通的同步复位,但它与时钟同步,这可能与应用逻辑使用的时钟不同。要为其他时钟生产同步复位,应该对每个时钟执行以下操作:
reg reset_clk_pre1, reset_clk_pre2;
reg reset reset_clk;
always @(posedge clk)
begin
reset_clk <= reset_clk_pre2;
reset_clk_pre2 <= reset_clk_pre1;
reset_clk_pre1 <= master_reset;
end
这只是一个普通的三级跨时钟域,生产 @reset_clk。该信号是与 @clk配套的同步复位。两个阶段实际上就足够了,但因为它是一个重要的信号,所以我用额外的寄存器放纵了它,以更加安全。
复位控制器的时钟
时钟原则上可以作为 @wakeup_clk使用的三种,即复位状态机:
- 在 FPGA外部生成的时钟,已知在 FPGA 唤醒时是稳定的。
- 由 FPGA上的锁相环生成的时钟,因此预计不会在 FPGA 唤醒时准备好。
- 在某些 FPGAs上,可以使用 configuration 时钟,它是由 FPGA 本身上的 ring oscillator 生成的。
第一种选择最容易使用。由于几乎总是有一个参考时钟(reference clock)驱动 FPGA的锁相环,因此这个参考时钟通常可以直接用作 wakeup 时钟。然而,重要的是要验证当 FPGA 唤醒时时钟确实稳定,即 FPGA 加载比特流所需的时间比外部 oscillator 生成有效时钟所需的时间长。查看数据手册(datasheets)时,通常会出现这种情况,并且会有很大的余量。但如果电路板的上电顺序(powerup sequence)没有得到适当的规划,那么很可能在 oscillator 的 supply 电压达到其正确水平之前, FPGA 就被允许读取其比特流。
第二种选择是使用由锁相环在 FPGA 本身上生成的时钟。明显的优势是这个时钟也可能用于应用逻辑,因此它与时钟资源和 power一起使用时效率更高。此选择需要将复位状态机保持在初始状态,直到时钟稳定,这可以通过以下方式完成:
reg rst_src_pll;
reg rst_src_pll_pre;
initial rst_src_pll = 1;
initial rst_src_pll_pre = 1;
always @(posedge wakeup_clk)
begin
rst_src_pll <= rst_src_pll_pre;
rst_src_pll_pre <= !pll_locked;
end
@pll_locked 是生成 @wakeup_clk (active 高) 的锁相环的 lock detector 输出。这个信号是异步的,所以先和 @wakeup_clk同步,然后作为激活 @clear_counter的原因之一,如上图(即 @rst_src_pll)。
此选项的另一个优点是,如果参考时钟(reference clock)暂时不稳定或缺失(特别是在打开电路板电源后), lock detector 也很有可能不稳定。因此,如果 @reset_count 在停用 @master_reset之前计数到很大的数字(肯定远高于 31),则 FPGA 有可能一直保持在复位中,直到参考时钟可以正常工作。但是,这不能依赖。
无论如何, @wakeup_clk 是锁相环的输出(output)的事实必然意味着它在时钟稳定之前喂给逻辑。因此,尚不清楚状态机在这段时间内是否正常工作。可以说这无关紧要,因为在某些时候时钟会变得足够好,然后会生成适当的复位。如果在复位之后一切都成为过去,谁在乎之前发生了什么?
更严格的方法是复位必须保持稳定活动,直到 wakeup 时钟被锁定,因此 FPGA 在比特流 configuration之后不久就不会出现奇怪的行为。这需要注意仅将非常简单的逻辑与此时钟一起使用。换句话说,如果时钟的频率比预期的高,逻辑应该不会严重失败。
要分析如果 @wakeup_clk 临时频率过高会发生什么情况,请注意 @reset_count 是复位状态机中唯一一 vector的寄存器。这意味着所有其他触发器可能在最坏的情况下采样他们计算的“next value”一个时钟为时已晚,因为违反了时序。特别是因为 @pll_locked 低而锁相环未锁定, @rst_src_pll 很快就稳定高,因此 @clear_counter 也稳定高。当触发器的 D 输入(下一 value)没有改变时,时钟的速度有多快并不重要。
因此,唯一可能的问题在于 @reset_count,它可能会计数错误,直到其计算出的 next value 由于 @clear_counter而稳定地保持在零。例如,如果其当前值为 3 (binary 011),则其下一个计算值为 4 (binary 100)。但是,如果由于时序而未对两 LSBs 进行采样,并且仍然对第三个比特(bit)进行采样,则计数器的值可能会跳转到 7 (111 binary)。
为了防止这种情况发生,从 @pll_locked 到 @clear_counter 的寄存器链的初始值都分配有利于保持 @reset_count 稳定为零。因此,如果 @pll_locked 保持稳定低(应该如此)直到 @wakeup_clk 稳定, @reset_count 不会远离零,并且 @master_reset 保持稳定活动。
至于最后一个选项,将 FPGA的 ring oscillator 用于复位控制器: 我自己没有尝试过,所以我不确定它有多好。但是,如果它可以拯救那些没有其他选择的人,这里或多或少是如何使用 Xilinx的 FPGAs做到这一点的: 在 FPGA 的 Configuration User Guide 中查找名为 STARTUPE2 (或类似名称)的 primitive 以获取 .它应该有一个名为 CFGMCLK的输出,它是来自 FPGA 本身的不准确 ring oscillator 的时钟。它的频率在 50-65 MHz左右。当 FPGA 唤醒时,这个时钟保证是稳定的,我会将时序约束设置为相当高的频率(比如 100 MHz)。
但如果真的没有其他选择,我会这样做。例如,如果 FPGA 唤醒时外部参考时钟不稳定,则需要在逻辑中实现额外的延迟。
重置锁相环
将锁相环作为复位顺序的一部分进行重置通常是个好主意。这可确保在已知参考时钟有效时重置它们。此外,如果复位是由用户启动的(例如按下复位按钮(reset pushbutton)),这可能是由于锁相环锁定不良引起的问题。不应该发生,但以防万一。
@clear_counter 不能用于重置锁相环,因为一旦锁相环停止锁定,它就会立即激活。如果使用它,锁相环将保留在复位状态(reset state)中,永远不会被锁定,复位也永远不会被释放。出于同样的原因,锁相环的复位不能从 @reset_count派生: 除了所有锁相环都被锁定时,它保持为零。
解决方案是为锁相环单独创建一个复位寄存器(reset register),与 @clear_counter类似。因此,使用上面的符号,未锁定的锁相环由 @rst_src_pll反映,它归结为如下所示:
reg clear_counter;
reg reset_plls;
initial clear_counter = 1;
initial reset_plls = 1;
assign reset_sources = rst_src_shreg || rst_src_debounce;
always @(posedge wakeup_clk)
begin
clear_counter <= rst_src_pll || reset_sources;
reset_plls <= reset_sources;
[ ... ]
在此代码片段中, @rst_src_pll 已从 @reset_sources中排除,仅用于 @clear_counter。结果,锁相环与整 FPGA一起被重置,除非它们本身没有被锁定。
请注意,如果 @reset_sources 随机变低和变高,那么 @reset_plls 也会根据此处显示的代码。这通常是无害的,因为当他们的复位疯狂地开关时,锁相环不会发生任何不好的事情。尽管如此,也有一种方法可以避免这种情况,如下所述。
更复杂的启动序列
使用普通的计数器(@reset_count) 作为状态变量(state variable)可以轻松实现更复杂的 startup 顺序。例如,生成正确的异步复位(asynchronous resets)(在时钟关闭时处于活动状态)非常简单: 它通过简单的逻辑 expressions (logic expressions)完成,定义了时钟何时关闭和复位何时处于活动状态的时间段。
因此,这种简单的计数器方法是一个很好的起点,即使对于在项目开始时似乎对复位状态机有简单需求的设计也是如此。如果后来发现需要复杂的 startup 顺序,很容易扩展现有的逻辑以实现这一目标。
无论如何,规划 startup 顺序归结为定义顺序(sequence)中每个阶段的有效时间段。每个这样的周期都被转换为 @reset_count 应该具有的值范围。
例如,如果设计有两个或更多不相关的时钟,则可以为每个时钟域(clock domain)创建复位信号,如上所示(请参阅“其他时钟域的复位”)。但如果采用该方法,则每个时钟域都会以随机顺序从复位中出来。这通常不是问题,但如果有问题,则每个时钟域都可以根据 @reset_count的进度在定义的时间停用其复位。
也可以实现多个计数器。当复位顺序需要等待某些条件满足后才能继续时,这很有用。例如,如果复位顺序涉及重置锁相环,等待它们锁定,然后继续复位顺序,那么让一个计数器保持在零直到所有锁相环都锁定是有意义的。第二个计数器保持在零,直到第一个计数器完成计数。
请注意,如果锁相环丢失了 lock,锁相环本身不会被重置,但依赖于它们的一切都会被重置。这可能不是所期望的行为,因为丢失其 lock 的锁相环在大多数设计中是一个严重故障。要在 lock丢失的情况下请求完整的复位,请将下面定义的 @pll_restart 添加到与或(OR)合并以获得 @reset_sources的信号中:
assign pll_restart = rst_src_pll && !master_reset;
这简单地说: 如果在 master 复位处于非活动状态后锁相环解锁,请再次重置所有内容,包括锁相环。为此,复位 count (reset count)必须足够长以承受 lock detectors可能的摆动。换句话说,复位 count 必须比锁相环的 lock detector 可以高的时间更长,即使锁相环还没有被锁定。不要将这与锁相环获取 lock需要多长时间相混淆。一些 lock detectors 根本不摆动,而且这些摆动很可能比锁相环的 lock time 短得多(如数据手册中所指定)。
wakeup shift 寄存器
它可能有点像 overkill,但我通常会在我的设计中添加这样的 wakeup shift 寄存器:
reg [15:0] wakeup_shift;
reg rst_src_shreg;
initial rst_src_shreg = 1;
initial wakeup_shift = 0;
always @(posedge wakeup_clk)
begin
rst_src_shreg <= !wakeup_shift[15];
wakeup_shift <= { wakeup_shift, 1'b1 };
end
由于大多数 FPGAs 将 @wakeup_shift 实现为 shift register primitive,其消耗的资源与 LUT相当,因此资源成本低,并提供了另一种机制来确保复位在上电期间发生。这在没有锁相环的设计中是必需的,因为没有其他任何东西可以激活 @clear_counter。但即使有锁相环,也有可能在 FPGA 唤醒时它们已经被锁定,因为这是某些 FPGAs上比特流 configuration 的一个可能选项。
所以无论如何,这是一个推荐的附加组件。安全总比后悔好。
外接复位按钮
复位按钮很常见。他们所做的与一台设计完全不同。一种可能性是用户认为“复位”的按钮连接到 FPGA 引脚,从而启动了将比特流加载到 FPGA的过程。它也可以连接到一个处理器的复位引脚(reset pin),在 FPGAs 上有一个 embedded 处理器。
并且这个复位按钮(reset button)可以在 FPGA上连接一个通用的 I/O 引脚,用于对 FPGA 逻辑进行复位。在这种情况下,这是激活 @clear_counter的另一个原因。
如果 @reset_count 计数到对应于 10 ms 或更多的数字,则不需要 debounce 来自输入引脚的信号,因为摆动将被计数器本身吸收。在这种情况下,这就足够了:
reg rst_src_debounce;
reg rst_src_debounce_pre;
initial rst_src_debounce = 1;
initial rst_src_debounce_pre = 1;
always @(posedge wakeup_clk)
begin
rst_src_debounce <= rst_src_debounce_pre;
rst_src_debounce_pre <= reset_button_pin;
end
但是,如果计数器快速完成(如上例所示,仅达到 31),则按钮需要 debouncing。有几种方法可以做到这一点,例如:
reg [17:0] debounce_count = 0;
reg reset_button_d, reset_button_d2, reset_button_d3;
wire debounce_reached = (debounce_count == 250000);
initial debounce_count = 0;
initial rst_src_debounce = 1;
always @(posedge wakeup_clk)
begin
reset_button_d3 <= reset_button_d2;
reset_button_d2 <= reset_button_d;
reset_button_d <= reset_button_pin;
if (reset_button_d2 != reset_button_d3)
debounce_count <= 0;
else if (!debounce_reached)
debounce_count <= debounce_count + 1;
if (debounce_reached)
rst_src_debounce <= reset_button_d3;
end
这是相对严格的 debouncer,与嘈杂的输入信号配合不好。这是优点还是缺点,取决于您的需求。
此代码示例是为 25 MHz 时钟编写的。如果 @reset_button_pin 在 10 ms期间具有相同的值,则 @reset_button_pin 的值被复制到 @rst_src_debounce 。请注意,当 @reset_button_d3 更改值时, @debounce_count 在同一时钟周期上更改为零。因此,当 @reset_button_d3 改变值时,它不会立即复制到 @rst_src_debounce 中,而是在长时间保持相同的值之后。
概括
在设计 FPGA的初始化时需要考虑很多事情。本页介绍了一些概念和想法,但重要的是要记住,真正的任务是正确识别需要启动操作的事件。为了使逻辑可靠地进入工作状态,定义对每个此类事件的正确响应也很重要。