ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

异步复位跨时钟域处理:同步释放原理、RTL模板与约束实践

异步复位跨时钟域处理:同步释放原理、RTL模板与约束实践 做过数字IC设计或FPGA开发的同学应该对“异步复位”又爱又恨。复位用起来简单一个低电平就能把寄存器拉到已知状态但一旦和跨时钟域CDC纠缠在一起它就成了整个系统里最容易被低估的雷。我在项目中见过不止一次功能仿真全绿上板跑几小时才偶发一次状态错乱最后定位到复位释放沿和时钟沿“擦肩而过”导致跨时钟域逻辑中的寄存器进入亚稳态。今天想和你聊聊异步复位信号在跨时钟域CDC中的处理原则聊聊我踩过坑之后的总结以及可以直接抄走的RTL方案和约束写法。这篇内容适合两类人一类是刚接触CDC同步、对复位同步器只有模糊概念的前端工程师另一类是已经写完RTL但被时序约束或FPGA上板问题困扰的朋友。核心就一句话异步复位可以异步拉低但释放一定要同步。围绕这句话我会拆解电路原理、参数计算、RTL模板、约束配置和常见问题尽量让你看完就能在自己的项目里落地。1. 先搞懂为什么异步复位是CDC里的“定时炸弹”很多人觉得复位信号很简单拉低清零拉高运行。但在跨时钟域设计中复位信号的“异步”属性天然地和触发器的建立/保持时间对着干。理解这一点后面所有处理原则才立得住。1.1 异步复位的本质与隐患异步复位最典型的特征就是不受时钟控制它可以在任意时刻拉低或拉高。触发器内部有一个异步清零端只要复位有效不管时钟沿在哪输出立刻被清零。这个特性非常方便尤其在上电复位、应急复位场景里能让整个设计立刻进入确定状态。问题出在复位释放的那一刻。假设rst_n从低电平跳回高电平恰好这个跳变发生在目标时钟的采样沿附近并且复位释放沿与时钟沿之间的间隔小于触发器的恢复时间recovery time触发器就可能出现亚稳态。简单说此时寄存器不知道该采样D端的值还是继续维持复位态输出会在一段时间里是不确定的0/1振荡最终稳定到哪个值也无法预测。在一个单时钟域设计里这个问题相对好处理把复位释放沿约束到时钟沿之后足够远或者用同步释放电路把它“打两拍”让所有触发器在同一边沿退出复位即可。但在CDC场景里麻烦成倍增加。原因有二一是外部复位信号往往来自片外芯片、按键或上电监测电路相对内部所有时钟都是异步的二是系统内部有些模块会产生异步复位信号给其它模块而这个模块和目标模块可能运行在不同的时钟域。复位信号一旦跨了时钟域就已经是CDC问题不只是复位问题了。1.2 跨时钟域中复位信号的两种典型场景我平常会把跨时钟域的复位场景分成两类处理套路不太一样。第一类是外部异步复位输入。典型例子是复位芯片输出的全局复位信号或者边沿缓慢的RC复位电路输出。这类信号通常直接接到所有模块的异步复位端看似“全局统一”但实际上每个触发器的复位端收到信号的时间可能都很接近可复位释放后各时钟域的时钟沿并不对齐于是每个触发器退出复位的时刻相对各自的时钟沿位置完全随机。如果不做任何处理会出现同一个模块里一部分寄存器已经在采集新数据另一部分还在复位态整个状态机直接乱掉。第二类是内部模块间产生的异步复位信号。比如模块A检测到温度超限立刻把复位信号拉低而模块B用了另一个异步时钟。这个复位信号对模块B来说天然是异步的而且很可能在模块B时钟沿附近到达。这种情况更隐蔽因为仿真时如果激励给得比较“温柔”很难暴露问题只有在真实电路里才会随机闪现故障。不管是哪种场景处理原则都是一样的不要直接用原始异步复位信号去释放寄存器而是先把它同步到目标时钟域再作为释放信号。这就是常说的“异步复位、同步释放”。2. 处理原则异步复位同步释放是基本盘处理异步复位跨时钟域业界最成熟的做法就是“异步复位、同步释放”。我在多个项目里验证过这个原则能覆盖绝大多数场景。下面把原理和电路拆开讲。2.1 核心原则先同步再释放“异步复位、同步释放”听起来像一个矛盾的短语其实它描述的是两个动作复位生效时走异步路径复位失效释放时走同步路径。复位生效时rst_n直接连接触发器的异步复位端。只要rst_n拉低寄存器立刻清零不需要等时钟沿。这样做的好处是响应快、安全性高即使系统时钟还没起来复位也能生效。比如上电瞬间时钟可能还不稳定异步复位能确保所有寄存器进入已知状态。复位释放时rst_n不能直接拉高而是要先经过一个由目标时钟驱动的两级同步器让释放信号和目标时钟沿对齐。具体电路就是复位同步器第一级触发器在外部复位拉低时被清零外部复位拉高后第一级在第一个时钟沿采样到1第二级在下一个时钟沿采样到第一级的1最终输出一个“延迟了1到2个时钟周期”的复位释放信号。为什么非要同步释放因为如果直接释放复位沿可能落在时钟沿附近触发器会进入亚稳态。但如果让释放沿经过两级触发器它就变成了一个和时钟同步的普通信号时序分析可以像检查普通路径一样检查它到各个寄存器的路径。两级触发器的存在还有一个作用把第一级可能出现的亚稳态“消化”掉等第二级采样时第一级的输出已经稳定从而大概率消除亚稳态向整个复位树的传播。2.2 复位同步器电路与参数计算标准复位同步器电路不多核心就是两级触发器但每级的异步复位端都要接到原始异步复位信号上。我常用的Verilog模板长这样module reset_sync ( input wire clk, input wire rst_n, // 原始异步复位低有效 output wire rst_n_sync // 同步后的复位释放信号低有效 ); reg rst_n_meta; reg rst_n_sync_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_n_meta 1b0; rst_n_sync_reg 1b0; end else begin rst_n_meta 1b1; rst_n_sync_reg rst_n_meta; end end assign rst_n_sync rst_n_sync_reg; endmodule这个电路的工作过程值得细看。当原始rst_n拉低时两个寄存器立刻清零rst_n_sync输出为0相当于复位还在生效。当原始rst_n拉高后rst_n_meta并不会立刻变成1而是要等目标时钟的第一个上升沿。第一个沿到来时rst_n_meta从0变为1第二个沿到来时rst_n_sync_reg跟着变为1此时复位才真正释放。所以从原始复位释放到同步复位释放大约会经过1到2个目标时钟周期。如果是FPGA综合工具会自动推断出这个结构但我建议手写并保持风格统一方便综合器识别。如果是ASIC这个模块会由标准单元库里的复位同步器单元完成工具会做等价检查。关于参数计算很多朋友会问“打两拍够不够”。两级触发器能把亚稳态的概率降到极低但理论上不是零。在复位释放场景里我们关注的是“MTBF”平均无故障时间这取决于触发器的亚稳态时间常数、时钟频率和信号翻转率。工程上标准单元库里的触发器通常能满足要求。如果设计用于极端环境或者时钟频率很高可以考虑三级同步器代价是多一个周期复位释放延迟但继续增加级数的收益会迅速递减。实际项目里两级是默认起点三级留给高可靠性场景。2.3 同步释放在不同时钟域中的变体如果系统里有多个时钟域比如CPU域、总线域、外设域每个时钟域都要单独例化一个复位同步器不能指望用一个同步后的复位信号去给所有域释放。原因很简单每个时钟域的时钟沿时间点不同同步器输出也会在不同时刻释放如果共用一个释放信号那对一部分时钟域来说它仍然是异步的。我做过的多时钟域SoC里每个时钟域都会有自己的reset_sync实例。比如reset_sync u_rst_cpu ( .clk(clk_cpu), .rst_n(rst_n_ext), .rst_n_sync(rst_n_cpu_sync) ); reset_sync u_rst_bus ( .clk(clk_bus), .rst_n(rst_n_ext), .rst_n_sync(rst_n_bus_sync) ); reset_sync u_rst_peri ( .clk(clk_peri), .rst_n(rst_n_ext), .rst_n_sync(rst_n_peri_sync) );这样每个时钟域都在自己的时钟沿释放复位保持域内同步。某几个时钟域之间如果需要有序启动还要额外加“复位释放握手”逻辑确保所有域释放完成后再开始互操作。这里有一个容易忽略的点复位同步器自身触发器的异步复位端应该直接接原始异步复位信号而不是接同步后的信号。一旦把它们接成同步复位就失去了“异步复位”的快速响应能力而且上电时钟不稳定时可能没法可靠复位。这一点我见过很多刚从软件转硬件的同事搞混务必注意。3. 实操细节从RTL到约束的完整落地有了基本原理和电路模板接下来就是工程落地。RTL好写但真正让系统稳定运行的往往是约束和检查方法。这部分我分三块RTL组织、时序约束、复位释放时序分析。3.1 RTL实现与代码模板在真实项目里我习惯把复位同步器单独成一个模块并放在顶层或模块边界而不是到处内联复制。这样方便统一约束和验证。一个稍微完整的版本可以支持“上电后不复位直到外部复位有效”的场景也可以增加一个高电平有效版本。我常用的是下面的带注释版本// 异步复位同步释放模板 module rst_sync_async_release ( input wire clk, input wire rst_n, // 异步复位低有效来自外部 output wire rst_n_sync // 同步释放后的复位低有效 ); wire rst_n_async; // 原始复位作为异步复位端 reg rst_n_ff1; reg rst_n_ff2; assign rst_n_async rst_n; always (posedge clk or negedge rst_n_async) begin if (!rst_n_async) begin rst_n_ff1 1b0; rst_n_ff2 1b0; end else begin rst_n_ff1 1b1; rst_n_ff2 rst_n_ff1; end end assign rst_n_sync rst_n_ff2; endmodule用这个模块时注意顶层时钟域划分。每个需要复位信号的模块应该接收自己时钟域的rst_n_sync而不是原始rst_n。比如rst_sync_async_release u_rst_cpu ( .clk(clk_cpu), .rst_n(ext_rst_n), .rst_n_sync(sys_rst_n_cpu) ); my_module u_my_module ( .clk(clk_cpu), .rst_n(sys_rst_n_cpu), ... );还有一个常见的补充设计复位同步器输出可以直接接到后续模块的异步复位端也可以接到一个“复位网络”后再分发。如果扇出特别大建议在同步器后面加一级“复位缓冲树”让复位信号到每个寄存器的延迟尽量一致。RTL里不需要体现这些但在综合/布局布线时需要注意。3.2 复位同步器的时序约束与CDC检查很多初次接触CDC的人以为写了复位同步器就万事大吉结果在STA里看到一堆从外部复位输入到第一级寄存器的时序违例手足无措。这是因为原始异步复位信号和时钟之间没有定时关系工具默认会做建立/保持检查但检查结果没有意义。我们应该把它设置为异步路径。对于外部复位输入我一般会这样约束# 复位输入到复位同步器第一级的路径不需要建立保持检查 set_false_path -from [get_ports ext_rst_n] -to [get_pins u_rst_cpu/rst_n_ff1_reg/C] set_false_path -from [get_ports ext_rst_n] -to [get_pins u_rst_cpu/rst_n_ff1_reg/R]实际上工具对复位端复位管脚的处理和对数据端不同但很多流程还是建议显式声明。对于复位同步器内部的路径情况不一样第一级触发器的输出到第二级触发器的D端是一条同步路径必须正常检查。不过在很多综合流程中复位同步器被识别为特殊单元工具会直接标记为同步路径或由CDC工具处理。如果是ASIC设计库通常提供专用的复位同步器单元并为第一级到第二级的路径做了优化。如果你用FPGA做项目更常见的方法是把外部复位信号在约束里加set_false_path或者用set_clock_groups -asynchronous来定义外部异步时钟关系。但千万注意一旦设置了false path所有这条路径上的时序检查都会被忽略所以别把同步器的第一级到第二级路径也set_false_path了那会让真正的同步路径失守。这里我推荐的做法是在项目初期就建立“复位信号CDC检查清单”把复位同步器的输入和输出路径列清楚。比如路径是否需要检查约束方法原始复位到第一级复位端否set_false_path 或异步约束原始复位到第一级数据端否set_false_path第一级到第二级是正常时钟约束第二级输出到下游寄存器复位端是正常时钟约束第二级输出到下游寄存器数据端否下游复位期间数据无效这个表能让你的约束有据可依不会漏掉关键路径。在实际项目中我还喜欢在CDC工具报告里专门看“reset sync”类别有的工具会直接报出复位同步器的位置和状态排查起来很方便。3.3 复位释放时序分析Recovery/Removal复位释放不能违反恢复时间recovery time和移除时间removal time。这两个概念分别对应常见的setup和hold恢复时间是指复位释放沿必须早于时钟采样沿一段时间保证触发器能从复位状态切回功能态移除时间是指复位释放沿必须晚于时钟采样沿一段时间保证触发器不会错误地再次采样到复位态。用生活化的比喻你按下“恢复出厂设置”后要等系统完全回到待机界面再发送新的指令。如果指令发太早系统还没准备好如果界面还没消失你就又去按可能进入一种混乱状态。触发器的复位释放也类似它需要一条“安全走廊”释放沿不能太靠近时钟沿否则复位端和时钟端同时“争抢”输出触发器就不知道听谁的。在先进工艺下recovery/removal时间通常由标准单元库给出范围从几十皮秒到几百皮秒不等。我们的复位同步器天然可以满足这个要求因为同步器把释放沿固定在时钟沿附近而下游寄存器的时钟沿就是同一个时钟域两者之间的偏斜只要在单元库允许范围内即可。然而如果复位同步器的输出到某些寄存器的延迟过长或者复位树的偏斜太大下游寄存器的复位释放沿相对它自己的时钟沿就会提前或滞后。提前太多可能违反recovery滞后太多可能违反removal。所以复位树的偏斜控制很重要。在布局布线阶段我习惯对复位网络做单独约束比如用set_propagated_clock处理时钟偏斜以及尽量让复位树和时钟树做“同时收敛”。一句话让所有触发器的复位端在同一个时钟沿附近收到释放信号。4. 常见问题与排查实录理论讲再多不如真实问题来一遍。下面这几个问题都是我在项目中遇到或评审过的每个都能让整个系统“随机抽风”。4.1 复位毛刺会导致什么后果异步复位最怕的是毛刺。因为复位是异步的毛刺只要短暂的脉冲就能让寄存器立刻复位哪怕它只有几百皮秒。如果毛刺出现在复位释放阶段可能让刚复位的寄存器又被“再复位”一次或者进入一个非预期的中间状态。我见过一个设备偶发死机的案例复位源来自外部RC电路RC时间常数不够电源波动时产生了一个亚稳态毛刺直接驱动复位同步器同步器里的寄存器被异步清零后释放沿又落在时钟沿附近后续模块状态完全错乱。后来在复位源和同步器之间加了施密特触发器滤波问题才消失。所以处理异步复位第一步是保证复位源干净。如果无法保证建议在复位输入管脚附近加滤波电路或者在芯片内部做一个“复位毛刺滤波器”只有持续一定时间的低电平才认为复位有效。同步器可以消除亚稳态但对毛刺无能为力。4.2 多时钟域复位释放不一致怎么办多个时钟域都有各自的复位同步器释放沿不会完全对齐。这个“不一致”本身不是灾难真正的问题在于一个时钟域已经退出复位开始跑数据而另一个时钟域还在复位状态两者之间的握手信号可能被错误采样。比如总线域已经退出复位并发出请求而外设域还处于复位状态总线域看到外设域的信号可能是高阻态或残留旧值导致总线上出现无效请求。更糟的情况是外设域复位释放瞬间总线域刚好采样到外设域复位前的旧状态造成假握手。我建议的做法是所有跨时钟域接口的握手信号都必须由每个时钟域自己的复位同步器产生的复位信号控制同时在复位释放后加一个“复位完成等待”状态。具体操作上可以在复位同步器后加一个复位状态寄存器每一个域释放完成后向上层报告前级域收到所有后级域的 release 完成信号后再开始对接口发送数据。这样能避免跨时钟域握手时的复位不一致问题。4.3 复位与异步FIFO的交互异步FIFO是跨时钟域数据传输的常用结构它的读写指针分布在不同时钟域需要分别同步。复位信号在这里有个特殊坑写侧的复位同步器清零写指针读侧的复位同步器清零读指针但两侧复位释放的时刻不同如果一侧比另一侧早释放可能立刻开始写入数据而另一侧的指针还没清完导致FIFO的full/empty状态判断异常。解决办法是异步FIFO两侧的复位同步器要共用原始异步复位源并且要确保在两侧都完成复位释放之前禁止任何一侧发起读写。通常FIFO IP会自带这个逻辑。如果是自己写的FIFO务必检查复位释放路径和读写指针清零的时序关系。我在自研异步FIFO时吃过亏当时只给读写指针各自做了同步器没管复位释放的先后关系仿真里测不出来上板后数据偶尔多拍或少拍定位了几天才找到。后来把FIFO的复位方案改成“两侧复位释放后等待两个周期再开放读写”问题才消失。4.4 多余复位分支导致复位树偏斜复位树如果做得不好哪怕复位同步器输出很干净下游寄存器也可能在复位释放时收到“有先有后”的复位信号。这个问题常出现在后端物理实现阶段前端容易忽略。我常用的经验是在所有寄存器都使用同一个同步后的复位信号不要在设计中某些地方用原始复位某些地方用同步复位那样会产生额外的复位域。另外复位网络要尽量做成树形结构避免长走线直连到大量触发器。在ASIC流程中复位树综合选项里可以设置“reset buffering”和“reset skew limit”布局前就把偏斜约束好。5. 聊点实际的我现在的复位处理习惯文章快写完了最后还是分享几个我在项目中固定下来的习惯也许对你直接用。第一每个时钟域只允许有一个“真正的异步复位源”。所有内部产生的复位要么是同步复位要么先经过复位同步器再分发。原始异步复位不要到处拉线避免做后端的时候头大。第二复位同步器的触发器必须保持“异步复位”属性。我见过有人为了省事把复位同步器改成了同步复位结果上电时钟未稳时整个系统无法可靠复位。在FPGA上这个差异尤其明显因为FPGA的配置寄存器通常都有专用的全局复位网络不建议你用普通逻辑去覆盖它。第三CDC工具不是万能的。很多CDC检查工具能报告复位同步器位置但不会帮你想清楚复位释放和跨时钟域握手的时序关系。我习惯在验证环境里加断言复位释放后检查跨时钟域接口没有错误请求复位释放沿附近握手信号保持稳定至少一个周期。这种断言能自动跑上几万次随机测试比自己盲看波形靠谱得多。异步复位和跨时钟域这个组合说难也难说简单也简单。难在它一旦出问题就非常随机复现概率可能只有千分之一简单在一套成熟的“异步复位、同步释放”流程配合正确的约束和验证方法能覆盖绝大多数场景。希望这篇内容能帮你在项目设计阶段就把这个坑填平而不是等芯片回来再被它折磨一次。
返回列表