这一篇在干嘛?
复位(reset)电路是 FPGA 设计中最常被忽视的部分之一。很多工程师以为”复位同步是 ASIC 才要操心的事,FPGA 的全局复位资源会搞定一切”——这是误解。设计不当的复位会表现为不可复现的逻辑错误,而不可复现的错误恰恰是最可怕的错误。本章讲清楚:异步复位与同步复位各自的坑,以及业界推荐的”异步断言、同步释放”方案。
大多数 FPGA 厂商的库元件都同时提供同步和异步复位端口,两种拓扑都能实现——所以选择权在你手上,责任也在你手上。复位对面积的影响可参见第 2 章。本章内容包括:
- 异步复位与同步复位的对比(全异步的问题、全同步的优缺点、异步断言+同步释放的优势);
- 混合复位类型的问题(不可复位触发器、内部生成的复位);
- 多时钟域下的复位管理。
10.1 异步复位 vs 同步复位
10.1.1 全异步复位的问题
全异步复位指:复位信号的断言(assert,进入复位)和释放(deassert,退出复位)都异步完成——复位网络直接接到触发器的异步复位引脚,且断言/释放的时机完全不关心时钟。典型电路如图 10.1:

对应的 Verilog 代码非常常见(你可能天天在写):
module resetff(
output reg oData,
input iClk, iRst,
input iData);
always @(posedge iClk or negedge iRst)
if (!iRst)
oData <= 0;
else
oData <= iData;
endmodulealways @(posedge iClk or negedge iRst) 这个敏感列表就是异步复位的标志——复位信号下降沿可以不看时钟、随时打断数据通路。
为什么说它危险?先看复位控制器(reset controller)在干什么:它只监视电压电平。上电时,电压达到阈值之前一直保持复位(此时假定逻辑已有足够电力正常工作),然后释放复位;掉电或电压跌落(brownout,电压暂降)时,电压低于阈值就重新拉复位。这一切都发生在不考虑被复位器件系统时钟的情况下。
上面这个电路的问题在于:它大多数时候都能正常工作,但偶尔会出事——当复位释放的边沿恰好落在离下一个时钟沿太近的位置,就会违反复位恢复时间(reset recovery time)。这是一种建立时间类的时序约束,规定复位释放到下一个时钟上升沿之间必须有最小间隔:

图 10.2 中复位在时钟沿之前留有足够裕量释放,条件满足。而图 10.3 展示了违反的情形——复位释放太贴近时钟沿,触发器输出进入亚稳态(metastability),后续行为不可预测:

关键结论:复位恢复时间违例只发生在复位释放(deassertion)时,不会发生在复位断言(assertion)时。 因此全异步复位不被推荐。后文所有解决方案都聚焦于”从复位态切换到工作态”这个转换过程。
10.1.2 全同步复位
解决上一节问题最直观的办法:像同步其他异步信号一样,把复位信号同步化(打两拍),如图 10.4:

实现代码与异步数据信号的双触发器同步(double-flopping)技术类似:
module resetsync(
output reg oRstSync,
input iClk, iRst);
reg R1;
always @(posedge iClk) begin
R1 <= iRst;
oRstSync <= R1;
end
endmodule两级触发器 R1 → oRstSync,用纯时钟域打拍,杜绝了复位释放时的恢复时间违例。这种拓扑的优点是:送给所有功能触发器的复位与时钟完全同步,只要给高扇出的同步复位信号配好缓冲,就总能满足复位恢复时间。
但是全同步复位有它自己独特的坑——不在释放端,而在断言端(更具体说是复位的持续时间)。回想上一节:断言时机不重要这个结论只对异步复位成立,对同步复位不成立。考虑图 10.5 的场景:

同一个复位,一个同步到快速时钟域,另一个同步到很慢的时钟域(也可能是一个非周期性的门控使能时钟)。如果时钟根本没在跑,会发生什么?看图 10.6 的波形:

当时钟跑得足够慢(或被门控关掉)时,复位断言期间没有出现时钟上升沿,同步触发器根本没机会采样到复位——结果是这个域内的触发器从未被复位。这就是”断言失败(failure of assertion)”。
因此,全同步复位同样不被推荐——除非能通过设计保证复位断言一定能被时钟捕获。把前两种复位方式结合起来:断言异步、释放同步,就是最可靠的方案,见下一节。
10.1.3 异步断言、同步释放(推荐方案)
第三种方案兼得两者之长:所有复位异步地断言,但同步地释放。
看图 10.7 的结构:复位电路内部的寄存器本身被外部信号异步复位,于是所有功能寄存器在同一时刻被拉入复位——这一步与时钟无关,时钟跑不跑都行。当外部复位释放后,该时钟域的本地时钟必须翻转两次,功能寄存器才会被放出来——而且是以与时钟同步的方式放出来的:

同步器代码如下:
module resetsync(
output reg oRstSync,
input iClk, iRst);
reg R1;
always @(posedge iClk or negedge iRst)
if (!iRst) begin
R1 <= 0;
oRstSync <= 0;
end
else begin
R1 <= 1;
oRstSync <= R1;
end
endmodule逐行看懂这段代码的巧妙之处:
always @(posedge iClk or negedge iRst)+if (!iRst):断言路径是异步的——iRst 一变低,R1 和 oRstSync 立即清 0,不依赖时钟(解决了全同步方案”断言失败”的问题);else分支:释放路径是同步的——iRst 变高后,需要 oRstSync 先经过 R1 一拍、再一拍输出,共两个时钟沿,输出才变 1。释放边沿永远与时钟对齐(解决了全异步方案”恢复时间违例”的问题);- 输出 oRstSync 作为同步化后的复位分发给本时钟域的所有功能寄存器(功能寄存器用同步复位风格编码,或直接用 oRstSync 做使能清零)。
这种实现允许一组触发器不看时钟就进入复位,但必须跟着时钟、同步地退出复位。作为设计实践准则:系统复位推荐采用异步断言—同步释放方式。
10.2 混合复位类型
10.2.1 不可复位触发器
好的设计实践:不同复位类型的触发器不要混在同一个 always 块里。看这个反面例子——一个带复位的触发器驱动一个不带复位的触发器,两者挤在一个 always 块里:
module resetckt (
output reg oDat,
input iReset, iClk,
input iDat);
reg datareg;
always @(posedge iClk)
if (!iReset)
datareg <= 0;
else begin
datareg <= iDat;
oDat <= datareg;
end
endmodule问题出在第二个触发器(oDat):它本身不需要复位,但它被写在 else 分支里,综合器只能把它实现为带加载(load)或时钟使能(clock enable)输入的触发器,用第一个触发器的复位来驱动这个使能,如图 10.8:

这需要更大的时序单元和额外的布线资源。把两个触发器拆成两个 always 块:
module resetckt(
output reg oDat,
input iReset, iClk,
input iDat);
reg datareg;
always @(posedge iClk)
if (!iReset)
datareg <= 0;
else
datareg <= iDat;
always @(posedge iClk)
oDat <= datareg;
endmodule这样第二个触发器不再有任何多余的电路,综合成最朴素、最小的触发器,如图 10.9:

规则:同一个 always 块里不要混用不同复位类型。 需要复位的逻辑写一个块,不需要复位的流水寄存器写另一个块。
10.2.2 内部生成的复位
有些设计中,内部事件会触发对芯片一部分逻辑的复位。这时有两个选择:
- 用异步复位:把产生复位的逻辑设计成无任何静态冒险(static hazard);
- 用同步复位。
异步复位上静态冒险的主要问题:由于传播延迟的差异,即使逻辑是从一个无效态切换到另一个无效态,复位线上也可能冒出一个毛刺脉冲。假设复位低有效,无效态(复位未生效)上的毛刺就是静态-1 冒险(static-1 hazard)。看图 10.10 的电路:

当输入从 (a, b, c) = (1, 0, 1) 切换到 (0, 0, 1) 时,就可能发生静态-1 冒险,波形如图 10.11:

从波形能看到:当”把复位拉成无效(逻辑 1)“的乘积项之一先失效、而下一个乘积项还没来得及接管输出时,复位线上就会瞬时掉出静态-1 毛刺。回到数字逻辑课的基础,用卡诺图(K-map)表示如图 10.12:

图 10.12 中每个圈是一个”把复位置为无效”的乘积项。当输入状态从一个乘积项圈跳到相邻的另一个圈时,如果第一个项先失效、第二个项后生效,就会出现毛刺。修复办法:增加一个冗余素蕴涵项(redundant prime implicant)把两个相邻的圈桥接起来,如图 10.13:

加上这个新的乘积项后,在两个圈之间的过渡期始终有至少一项保持有效,无效态上的毛刺就不可能出现了。
不过实践中,与其费劲做无毛刺异步复位,更推荐使用全同步复位同步器——既保持了全同步设计风格,又省掉了维持无毛刺复位信号所需的冗余逻辑。
10.3 多时钟域的复位
前面已经确立两条铁律:复位释放必须同步;异步复位信号必须做同步化释放。把这个原则延伸一步:每个异步时钟域都要独立地同步各自的复位。

如图 10.14,每个时钟域使用一个独立的复位同步电路。为什么不能共用一个同步后的复位?因为在一个时钟域上同步释放的复位,相对于另一个异步时钟域而言依然是异步的——A 域同步好了的释放沿,在 B 域看来照样可能贴着 B 的时钟沿,照样违反恢复时间。换句话说:一个同步化后的复位信号,相对于来自另一个独立时钟域的同步复位来说,仍然是异步的。
规则:每个独立时钟域必须配一个独立的复位同步器。
10.4 要点总结
- 复位恢复时间违例只发生在复位释放时。
- 全同步复位可能因时钟特性(慢时钟/门控/不跑)而根本捕获不到复位断言。
- 异步断言、同步释放的复位电路通常比全同步或全异步都更可靠。
- 不同复位类型不要混用在同一个 always 块中。
常见坑:全芯片共用一根异步复位线、复位释放不做任何同步
这是最经典的翻车组合:外部复位控制器按电压阈值拉放复位,FPGA 内部
always @(posedge clk or negedge rst)直接全网使用。平时没事,偶尔上电就出现”每次现象都不一样、无法复现”的怪 bug——正是复位释放边沿贴着时钟沿造成亚稳态。修法:在每个时钟域入口放一个”异步断言、同步释放”同步器(两个触发器),功能逻辑统一使用同步化后的复位。
通关标准:
学完本篇你应该能做到:
- 说清异步复位和同步复位各自的致命缺陷:前者在释放时可能违反复位恢复时间导致亚稳态,后者在慢时钟/门控时钟下可能根本捕获不到断言;
- 默写”异步断言、同步释放”同步器的 Verilog(两个触发器、两个分支),并解释为什么断言不需要时钟、释放必须翻转两次时钟;
- 识别 always 块中混用复位类型导致的带使能触发器问题,并会拆块优化;
- 解释静态-1 冒险的成因和卡诺图上加冗余素蕴涵项的修法;
- 为多时钟域设计复位架构:每个时钟域一个独立同步器。
自测:为什么复位恢复时间违例只发生在释放时,断言时却不用担心?
复位断言是”强制把触发器打回已知状态”,异步复位引脚的动作本身不依赖时钟,任何时刻断言都能生效。而释放是”把控制权交还给时钟”——如果释放边沿太贴近时钟沿,触发器刚离开复位就赶上采样窗口,输出可能亚稳。所以约束只在释放沿与时钟沿之间。
自测:全同步复位在什么场景下会"复位失败"?如何理解?
当时钟跑得太慢、被门控关掉、或复位断言期间没有出现时钟上升沿时,同步触发器无法采样到复位断言,复位根本传不到功能寄存器——这个域的触发器从未被复位,上电后状态未知。根源:同步复位的生效依赖时钟沿,而”需要复位”的场景恰恰可能包括时钟未就绪。
自测:异步断言、同步释放的同步器里,为什么外部复位释放后本地时钟要翻转两次功能寄存器才出复位?
同步器内部有两个触发器串联:R1 和 oRstSync。复位释放后,R1 在第一个上升沿捕获到 1,oRstSync 在第二个上升沿把 1 传出去。两拍保证了释放沿经过完整同步、输出变化与时钟沿对齐,功能寄存器看到的释放时刻永远满足恢复时间。
自测:把带复位和不带复位的触发器写进同一个 always 块,综合结果会差在哪?
不带复位的那个触发器会被综合成带 load/时钟使能输入的更大时序单元,其使能端由复位逻辑驱动,还消耗额外布线。拆成两个 always 块后,无复位触发器综合成最小、最干净的普通触发器,省面积也利于时序。
自测:多时钟域设计中,为什么不能在一个时钟域里同步好复位就全芯片共用?
复位释放与时钟 A 对齐了,对异步的时钟 B 来说这个释放沿依然是随机的——在 B 域的触发器看来仍可能违反恢复时间。同步只对”自己的时钟”有效,所以每个独立时钟域都需要一个专属的复位同步器。