这一篇在干嘛?

复位(reset)电路是 FPGA 设计中最常被忽视的部分之一。很多工程师以为”复位同步是 ASIC 才要操心的事,FPGA 的全局复位资源会搞定一切”——这是误解。设计不当的复位会表现为不可复现的逻辑错误,而不可复现的错误恰恰是最可怕的错误。本章讲清楚:异步复位与同步复位各自的坑,以及业界推荐的”异步断言、同步释放”方案。

大多数 FPGA 厂商的库元件都同时提供同步和异步复位端口,两种拓扑都能实现——所以选择权在你手上,责任也在你手上。复位对面积的影响可参见第 2 章。本章内容包括:

  • 异步复位与同步复位的对比(全异步的问题、全同步的优缺点、异步断言+同步释放的优势);
  • 混合复位类型的问题(不可复位触发器、内部生成的复位);
  • 多时钟域下的复位管理。

10.1 异步复位 vs 同步复位

10.1.1 全异步复位的问题

全异步复位指:复位信号的断言(assert,进入复位)和释放(deassert,退出复位)都异步完成——复位网络直接接到触发器的异步复位引脚,且断言/释放的时机完全不关心时钟。典型电路如图 10.1:

图 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;
endmodule

always @(posedge iClk or negedge iRst) 这个敏感列表就是异步复位的标志——复位信号下降沿可以不看时钟、随时打断数据通路。

为什么说它危险?先看复位控制器(reset controller)在干什么:它只监视电压电平。上电时,电压达到阈值之前一直保持复位(此时假定逻辑已有足够电力正常工作),然后释放复位;掉电或电压跌落(brownout,电压暂降)时,电压低于阈值就重新拉复位。这一切都发生在不考虑被复位器件系统时钟的情况下。

上面这个电路的问题在于:它大多数时候都能正常工作,但偶尔会出事——当复位释放的边沿恰好落在离下一个时钟沿太近的位置,就会违反复位恢复时间(reset recovery time)。这是一种建立时间类的时序约束,规定复位释放到下一个时钟上升沿之间必须有最小间隔:

图 10.2 复位恢复时间:复位释放须与下一个时钟沿保持最小间隔

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

图 10.3 复位恢复时间违例:导致亚稳态和不可预测行为

关键结论:复位恢复时间违例只发生在复位释放(deassertion)时,不会发生在复位断言(assertion)时。 因此全异步复位不被推荐。后文所有解决方案都聚焦于”从复位态切换到工作态”这个转换过程。

10.1.2 全同步复位

解决上一节问题最直观的办法:像同步其他异步信号一样,把复位信号同步化(打两拍),如图 10.4:

图 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.5 全同步复位遇到慢时钟/门控时钟的情形

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

图 10.6 复位同步失败:断言期间没有时钟沿,复位根本没被捕获

当时钟跑得足够慢(或被门控关掉)时,复位断言期间没有出现时钟上升沿,同步触发器根本没机会采样到复位——结果是这个域内的触发器从未被复位。这就是”断言失败(failure of assertion)”。

因此,全同步复位同样不被推荐——除非能通过设计保证复位断言一定能被时钟捕获。把前两种复位方式结合起来:断言异步、释放同步,就是最可靠的方案,见下一节。

10.1.3 异步断言、同步释放(推荐方案)

第三种方案兼得两者之长:所有复位异步地断言,但同步地释放

看图 10.7 的结构:复位电路内部的寄存器本身被外部信号异步复位,于是所有功能寄存器在同一时刻被拉入复位——这一步与时钟无关,时钟跑不跑都行。当外部复位释放后,该时钟域的本地时钟必须翻转两次,功能寄存器才会被放出来——而且是以与时钟同步的方式放出来的:

图 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:

图 10.8 混合复位类型同块的综合结果:oDat 被迫使用带使能的大触发器

这需要更大的时序单元和额外的布线资源。把两个触发器拆成两个 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:

图 10.9 混合复位类型的最优实现:oDat 是干净的无复位触发器

规则:同一个 always 块里不要混用不同复位类型。 需要复位的逻辑写一个块,不需要复位的流水寄存器写另一个块。

10.2.2 内部生成的复位

有些设计中,内部事件会触发对芯片一部分逻辑的复位。这时有两个选择:

  1. 用异步复位:把产生复位的逻辑设计成无任何静态冒险(static hazard)
  2. 用同步复位。

异步复位上静态冒险的主要问题:由于传播延迟的差异,即使逻辑是从一个无效态切换到另一个无效态,复位线上也可能冒出一个毛刺脉冲。假设复位低有效,无效态(复位未生效)上的毛刺就是静态-1 冒险(static-1 hazard)。看图 10.10 的电路:

图 10.10 复位引脚上潜在的冒险

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

图 10.11 复位线冒险的示例波形:无关态切换期间冒出毛刺

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

图 10.12 用卡诺图定位静态-1 冒险:相邻乘积项之间没有桥接

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

图 10.13 增加冗余素蕴涵项桥接相邻乘积项,消除冒险

加上这个新的乘积项后,在两个圈之间的过渡期始终有至少一项保持有效,无效态上的毛刺就不可能出现了。

不过实践中,与其费劲做无毛刺异步复位,更推荐使用全同步复位同步器——既保持了全同步设计风格,又省掉了维持无毛刺复位信号所需的冗余逻辑。

10.3 多时钟域的复位

前面已经确立两条铁律:复位释放必须同步;异步复位信号必须做同步化释放。把这个原则延伸一步:每个异步时钟域都要独立地同步各自的复位

图 10.14 多时钟域的复位同步:每个时钟域配一个独立的复位同步器

如图 10.14,每个时钟域使用一个独立的复位同步电路。为什么不能共用一个同步后的复位?因为在一个时钟域上同步释放的复位,相对于另一个异步时钟域而言依然是异步的——A 域同步好了的释放沿,在 B 域看来照样可能贴着 B 的时钟沿,照样违反恢复时间。换句话说:一个同步化后的复位信号,相对于来自另一个独立时钟域的同步复位来说,仍然是异步的。

规则:每个独立时钟域必须配一个独立的复位同步器。

10.4 要点总结

  • 复位恢复时间违例只发生在复位释放时。
  • 全同步复位可能因时钟特性(慢时钟/门控/不跑)而根本捕获不到复位断言
  • 异步断言、同步释放的复位电路通常比全同步或全异步都更可靠。
  • 不同复位类型不要混用在同一个 always 块中。

常见坑:全芯片共用一根异步复位线、复位释放不做任何同步

这是最经典的翻车组合:外部复位控制器按电压阈值拉放复位,FPGA 内部 always @(posedge clk or negedge rst) 直接全网使用。平时没事,偶尔上电就出现”每次现象都不一样、无法复现”的怪 bug——正是复位释放边沿贴着时钟沿造成亚稳态。修法:在每个时钟域入口放一个”异步断言、同步释放”同步器(两个触发器),功能逻辑统一使用同步化后的复位。

通关标准:

学完本篇你应该能做到:

  • 说清异步复位和同步复位各自的致命缺陷:前者在释放时可能违反复位恢复时间导致亚稳态,后者在慢时钟/门控时钟下可能根本捕获不到断言;
  • 默写”异步断言、同步释放”同步器的 Verilog(两个触发器、两个分支),并解释为什么断言不需要时钟、释放必须翻转两次时钟;
  • 识别 always 块中混用复位类型导致的带使能触发器问题,并会拆块优化;
  • 解释静态-1 冒险的成因和卡诺图上加冗余素蕴涵项的修法;
  • 为多时钟域设计复位架构:每个时钟域一个独立同步器。