
异步复位信号在跨时钟域CDC中的处理原则做了十几年数字IC和FPGA设计我最早明白异步复位有风险是在一个多时钟域项目里翻车之后。仿真全绿上板之后偶尔抓拍数据错几个bit查了半天才发现是复位释放瞬间和时钟上升沿撞在了一起某一片寄存器的输出端直接出现了在0和1之间抖动的电平。当时第一反应是电源噪声后来用示波器加逻辑分析仪定位最终确定是异步复位信号的释放沿没有和目的时钟对齐。这类问题在CDC跨时钟域场景里特别隐蔽因为复位信号的扇出面极大一个点出问题整条数据链路的寄存器都可能被污染。这篇文章就围绕异步复位信号在CDC中的处理原则展开把标准做法、架构选型、验证手段和实际踩坑都梳理一遍希望对正在做多时钟域设计的同行有帮助。1. 异步复位为什么是CDC里的“隐形杀手”1.1 从一次X态故障说起那次故障让我对“复位”这两个字有了新的认识。项目里的系统有四个时钟域复位源是片外的上电复位信号直接连到了各个时钟域的触发器异步复位端。板子运行起来偶发性地出现寄存器输出为X态随后数据链路错误。最初怀疑跨时钟域的数据同步有漏洞把SpyGlass CDC的report翻了一遍数据路径的检查都是干净的。后来定位到出错的寄存器发现它们都有一个共同点复位释放沿和各自的时钟上升沿距离太近违反了库单元的recovery时间。这类问题在仿真里很难抓因为仿真激励里的复位信号通常离时钟沿有固定的相位关系比如复位释放都安排在时钟沿之后几纳秒大家习惯性地认为“这样就安全了”。可实际场景里外部复位信号和片内时钟的相位关系是任意的。复位信号本身异步于每个时钟域它在任意时刻释放都可能在某个时钟域里撞击到关键沿。当复位释放沿落在触发器的建立/保持窗口内触发器输出不是干净的复位值也不是正常功能值而是亚稳态或者不确定状态。1.2 recovery/removal时间——两个被低估的时序窗口很多人对setup/hold time很熟但对recovery/removal time就模糊了。其实这两组参数是一一对应的关系。setup/hold描述的是数据信号相对于时钟沿的约束而recovery/removal描述的是异步控制信号比如异步复位、异步置位相对于时钟沿的约束。recovery time恢复时间在时钟有效沿到来之前异步复位信号必须已经稳定释放的最短时间。如果复位释放太晚时钟沿到达时复位还没完全释放触发器可能看不到正确的D端数据。removal time移除时间在时钟有效沿之后异步复位信号必须继续保持有效状态的最短时间。如果复位释放得太早复位脉冲的高电平宽度不足可能导致触发器来不及可靠退出复位状态。这两者合起来构成了一个对异步复位信号释放沿的“禁忌窗口”。异步复位信号一旦被触发它可以在任意时刻生效并快速把输出拉回复位值这一点不受时钟约束但释放时刻如果落进这个禁忌窗口触发器的输出状态就无法预测。在实际电路里这个窗口通常是几个百皮秒到几个纳秒看起来很小但只要释放沿和时钟沿的相位差恰好落进去故障就是真实存在的。更麻烦的是复位信号通常接在成千上万个触发器上扇出路径长短不一不同寄存器的释放沿到达时间也不同只要有一个寄存器掉进禁忌窗口整条链路就可能被污染。1.3 复位信号也要当成CDC信号来对待很多工程师做CDC检查时只关注数据路径比如一个模块的输出直接送到另一个时钟域中间有没有同步器、有没有握手协议。但复位信号本质上也是一种跨时钟域信号。复位源所在的时间域和目标时钟域之间没有确定的相位关系它就符合CDC信号的定义。只是我们通常不把它叫“数据CDC”而叫“复位CDC”。这带来一个思维转变你不能因为复位信号是“全局的”“异步的”就觉得它天然安全。恰恰相反正因为它是异步的才必须用专门的机制把它“驯服”。数据信号的CDC处理常用两级同步器、异步FIFO、握手协议解决的问题是“数据内容不能丢、不能错”复位信号的CDC处理解决的是“释放沿必须对齐到目的时钟且不能落入时序禁忌窗口”。两者都是同步概念但手法不同不能混用。还有一种情况是需要特别小心的复位信号从另一个时钟域产生比如软件控制下的软复位信号它本身在某个时钟域里是同步信号但到了目标时钟域之后又变成了异步信号。这种信号不能直接当作复位源使用应该先按照CDC数据通道的方式把软复位信号安全传递到目标时钟域再在目标时钟域内做异步复位的同步释放。换句话说复位信号的源头到目标寄存器之间每一个跨域边界都要专门处理。2. 标准解法异步置位、同步释放2.1 两級寄存器的电路结构与工作时序业界对异步复位信号的标准处理原则四个字就能概括异步置位、同步释放。英文常写作Asynchronous Assert, Synchronous Deassert。意思是复位信号有效的那个沿进入复位状态可以完全异步地发生不需要任何同步但复位信号释放的那个沿退出复位状态必须与目标时钟对齐。典型的实现是一个两级复位同步器结构不复杂第一级触发器的D端接高电平针对低有效复位接1。第二级触发器的D端接第一级的Q端。两个触发器的异步复位端都直接接原始异步复位信号。两个触发器使用同一个目标时钟域时钟。第二级触发器的Q端输出就是同步释放后的复位信号。时序上可以这样理解原始复位信号拉低的那一刻两级触发器都立即进入复位状态输出为0整个时钟域被复位。原始复位信号释放拉高后第一级触发器要等到目标时钟的第一个有效沿到来因为它的D端是1所以输出变1第二级触发器要再等一个时钟周期才能看到第一级的1并输出变1。这样一来复位释放沿就从“任意时刻”变成了“目标时钟沿加clk-to-q延时”的确定性事件下游所有触发器的recovery/removal时序都有了保障。实际代码大概是这个思路module reset_synchronizer ( input wire clk, input wire rst_n, // 原始异步复位低有效 output wire rst_sync_n ); reg rst_n_sync1; reg rst_n_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_n_sync1 1b0; end else begin rst_n_sync1 1b1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_n_sync2 1b0; end else begin rst_n_sync2 rst_n_sync1; end end assign rst_sync_n rst_n_sync2; endmodule需要注意这个模块输出的是一根“内部时钟域的复位信号”它的有效沿拉低仍然是异步的释放沿拉高已经被同步到了clk。下游的目标寄存器它们的异步复位端接到的就应该是这根信号而不是原始复位信号。未同步的原始复位信号只能连接到各时钟域自己的复位同步器输入端不要再直接接到其他寄存器上。2.2 为什么两级够用什么情况下要上三级有不少人问为什么一定是两级一级不行吗一级同步器的问题在于原始异步复位信号释放沿落到第一级触发器的recovery/removal窗口时它输出状态可能进入亚稳态。如果这个亚稳态信号直接作为整个时钟域的复位使用亚稳态可能传播到所有寄存器。两级结构里第一级即使亚稳态也有整整一个时钟周期来收敛第二级看到的输入通常已经稳定到0或1它自己的输出就能可靠地对齐到时钟沿。这是典型的MTBF平均无故障时间考量。两级是绝大多数场景下的起步配置。如果复位信号需要驱动的寄存器特别多、复位网络特别长或者目标时钟频率很高、时钟抖动较大有些团队会加一级变成三级同步器。第三级的作用是额外提供一个时钟周期的稳定余量代价是多一个时钟周期的复位释放延迟。我在高性能项目里见过三级但大多数SoC设计用两级就够了。需要注意的是增加级数并不会改善“扇出过大”的问题。同步器只是把释放沿对齐到时钟它不能替代复位树设计。如果复位扇出成百上千个触发器你必须考虑复位缓冲树的平衡否则每个触发器的释放沿到达时间会有差异部分寄存器照样可能落在禁忌窗口里。这个问题后面专门展开。2.3 低有效复位和高有效复位的统一处理标准单元库里的异步复位端可能低有效也可能高有效。低有效的情况最常见代码里通常叫rst_n同步器里的逻辑用上面那套写法即可。如果复位端是高有效对应rst同步器也能做对称处理第一级D端接0。两级触发器的异步置位端接原始高有效复位信号。释放时第一级在时钟沿后变0第二级跟随后变0。库单元如果只提供低有效异步复位端而系统复位源是高有效可以在同步器输出端加一个反相器或者把高有效复位信号反相后再进同步器。原则是把“异步有效沿”和“同步释放沿”对应到正确的电平极性上不要让信号经过组合逻辑后再大范围扇出。还有一点容易忽略同步器输出的复位信号必须保持它作为“异步复位端驱动”的属性。也就是说下游寄存器看到的是一个电平型复位它的有效沿是即时的释放沿是对齐过的。你不能因为这个信号逻辑上像“普通电平”就把它又当成使能信号去接D端逻辑。这种误用会导致复位行为在时序上出现不可控的延迟。3. 集中式还是分布式复位树的架构权衡3.1 集中式同步器适合什么场景复位同步器放哪里是一个架构决策。最简单的方式是全芯片只放一个复位同步器它的输出通过全局复位树分发到所有寄存器。这个方案的优点是资源少、逻辑简单只要一个同步器工作正确全局复位逻辑看上去就是统一的。但它有两个很明显的隐患。第一个是复位树延迟。同步器输出到不同位置寄存器的路径长度差异很大如果复位树没有仔细平衡同一个释放沿到达近端寄存器和远端寄存器的时刻可能差好几个纳秒。对于高速时钟这个差异完全可能超过时钟偏斜的容忍度导致一部分寄存器已经退出复位另一部分还在复位态甚至有一小部分寄存器的释放沿和本地时钟沿卡在禁忌窗口里。第二个隐患是时钟和复位的偏斜耦合。集中式同步器距离各个目标寄存器远等效于把“复位偏斜”叠加到“时钟偏斜”上时序收敛难度呈指数上升。所以集中式复位同步器只适合寄存器规模小、时钟频率低、复位树延迟远小于时钟周期的设计比如一些低速控制模块。多时钟域大型SoC上直接用一个全局同步器后段物理实现会很痛苦。3.2 分布式同步器每个时钟域各放一个更稳妥的做法是分布式复位同步器。每个时钟域都放一个独立的同步器原始异步复位信号同时接到所有同步器的异步复位端每个同步器的输出只驱动自己所在时钟域内的复位树。这样做的好处有三个层次。第一每个时钟域的释放沿都对齐到本域时钟和别的时钟域没有相位关系天然避开了“多域释放沿必须一致”的伪需求。第二同步器输出到域内寄存器的距离被压缩得很短复位树延迟可控时钟和复位的偏斜可以并做一体收敛。第三某个域的复位逻辑出了问题影响范围被限制在该域内不会扩散成全局事故。代价是原始异步复位信号需要广播到每一个同步器。有人会担心原始复位信号的到达时间不一致会不会造成各域复位释放时间不同这个问题其实不用太纠结。因为每个同步器都做了异步置位、同步释放原始复位信号到达时间的那点差异只会影响第一级同步器的“异步置位时刻”而释放时刻永远对齐到各自时钟。你能确定的只是所有域都会在一个相对晚的时间点之后完成释放但每个域具体哪个周期释放由各自时钟决定。这在多数系统里是允许的。如果业务上要求多个时钟域在同一个时刻一起退出复位那就要做多域复位释放对齐电路属于特殊需求不能用分布式同步器天然实现。3.3 “释放沿对齐”背后的时钟树代价把复位同步器分布在各个时钟域之后很多人忽略了另一件事复位释放沿对齐到时钟沿这个“对齐”隐含了时钟树和复位树必须联合作战。说得直白一点同步器输出到目标寄存器的复位路径和时钟到目标寄存器的时钟路径最终都在同一个触发器的异步复位端汇合。复位释放沿必须在时钟沿的recovery/removal窗口之外这就要求复位树和时钟树的偏差被压缩到很小。后段实现里复位树往往会被综合工具当成普通的high-fanout信号自动插入很多buffer。如果没有额外的约束这些buffer的位置和延迟是不受控的。我的建议是在做物理实现时把复位同步器的输出节点设置成balance属性让工具把复位树当成高优先级网络处理并且让复位树和时钟树共用类似的布局密度约束。另外复位信号在布局阶段如果和时钟路径绕了远路释放沿的偏斜会很难收敛。时序分析时也要对复位释放路径做相应的检查。有些团队用SDC约束里create_generated_clock或者set_false_path来处理复位这是不严谨的。复位释放路径不是false path它在recovery/removal分析里是真实存在的路径。要做的是在这个路径上设置max_delay约束复位树从同步器输出到目的寄存器的延迟上限帮助工具收敛。4. 多时钟域与低功耗场景下的额外讲究4.1 全局复位与局部复位的配合多时钟域系统里除了上电时的全局复位往往还有各种局部复位比如某个子系统的软复位、调试复位、低功耗唤醒复位。这些局部复位不能随意“扔”到异步复位端上。我的处理习惯是先把全局复位和局部复位合并成一棵复位树再把它接入该时钟域的复位同步器。合并时要非常小心不要用组合逻辑把多个复位信号直接或起来就完事。复位信号和普通控制信号不同它的有效沿要求接近零延迟传播组合逻辑引入的毛刺和延迟都可能让部分触发器误复位。更安全的做法是用一个专门的复位控制器模块把全局复位、软复位请求、低功耗状态机的退出复位等统一处理成干净的同步复位请求信号然后再进入复位同步器。软复位请求跨时钟域的部分要按标准CDC流程走。比如软复位寄存器在CPU时钟域目标模块在高速接口时钟域你就需要做一次跨时钟域的脉冲或电平同步防止请求信号在目标域里被当成毛刺。这样做的成本不大但能省掉很多事后排查X态的精力。4.2 时钟门控期间复位还能不能同步释放低功耗设计里目标时钟域可能被门控关闭。此时如果原始复位信号释放复位同步器没有时钟沿可以采样第一级和第二级的输出会保持复位状态等时钟重新开启后第一个有效时钟沿才开始释放过程。这个行为其实是安全的但它带来一个时序上的不确定性时钟恢复的时刻和复位释放之间可能存在一段“等待窗口”。在等待窗口内域内寄存器保持复位态域外看到的是不确定的中间状态。很多人踩过的坑是时钟门控打开后的第一个时钟沿正好和复位释放沿非常接近。虽然两级同步器能吸收这个风险但物理实现和工具分析上它往往被当成复杂的边界条件。我建议在UPF低功耗流程里把时钟开启和复位释放的顺序约束起来比如要求时钟稳定运行至少一个周期后复位同步器才允许开始释放。这样既符合时序收敛的直觉也能减少后段优化时的麻烦。另外不要在复位释放临界点同时关断时钟。复位释放本来已经对齐到某个时钟沿如果这个沿被门控吃掉释放动作被拖延到下一个不确定的沿下游模块的行为就完全难以预测。所以时钟门控单元的控制信号和复位同步器输出之间我总是会加严格的时序约束宁可让复位晚一点释放也不让时钟在释放窗口里抖。4.3 复位脉冲宽度太窄等于没复位异步复位信号的有效宽度必须足够宽这是设计中容易糊弄的地方。复位有效沿可以异步发生但是它必须持续足够时间让所有目标触发器都进入确定的复位状态。对于两级复位同步器来说原始复位信号的有效宽度至少要保证两级触发器都完成异步置位并且在释放前有稳定的时间。更严格的经验值是复位有效宽度至少覆盖目标时钟域最慢时钟的2到3个周期。如果复位控制器发出的复位脉冲太窄比如只有几个纳秒就可能出现这样一种尴尬情况靠近复位源的同步器已经复位远端寄存器还没来得及收到有效的复位电平脉冲就消失了。结果整个复位操作形同虚设。为了避免这个问题复位控制器里可以加一个最小脉宽扩展电路用计数器把复位有效时间延长到几百甚至几千个时钟周期。反正复位期间逻辑不工作宁长勿短。同理对外部输入的异步复位信号片内最好用专门的复位输入的毛刺滤波器先滤除窄毛刺再做复位同步。不要把外部复位直接接到同步器甚至直接接到寄存器外部输入信号的边沿质量和脉宽你是无法保证的。5. 用工具和断言把复位问题拦在流片前5.1 CDC工具查什么结构检查与恢复时间违例现在主流CDC工具比如Cadence SpyGlass CDC、Synopsys Questa CDC、Siemens Meridian CDC都对复位信号有专门的检查。你不要只看数据路径的CDC报告复位相关的检查项要单独过一遍。工具一般会做两类检查。第一类是结构检查确认每个时钟域的复位信号是否经过了标准的复位同步器结构。如果你把原始异步复位信号直接接到某个普通触发器的异步复位端又没有同步释放工具会报出类似“async reset deassert unsynchronized”的violation。第二类是恢复时间检查工具会分析复位释放沿和各时钟沿的相对相位评估是否违反recovery/removal时间。有个容易忽略的点CDC工具默认的复位同步器识别规则可能要求你按照特定模板编写代码。如果你用了“两个独立always块分别描述两级触发器”这种常见写法工具大概率能识别如果你把两级触发器写在一个always块里或者把D端逻辑搞得很复杂工具可能就认不出来最后要么误报要么漏报。我的经验是写复位同步器模块时保持极简只做同步功能不要混杂其他逻辑。这样工具识别率最高后续审查也最省事。5.2 仿真断言怎么写、X态怎么追仿真阶段复位问题最容易伪装成“偶发现象”。因为复位释放沿和时钟沿的相位关系在普通行为仿真里可能永远撞不上除非你用随机延迟或者后仿真的真实时序。我有两个习惯。第一在顶层测试平台里给复位释放加随机抖动。每次测试用例开始前用系统函数把复位释放时刻在一定范围内随机化多跑上百个种子能有效暴露复位释放和时钟沿竞争的问题。第二在复位相关信号上做断言检查。可以在复位同步器输出端加一条SVA断言要求复位释放沿和时钟上升沿之间的间隔不小于recovery time。如果后端时序仿真里跑出这条断言失败几乎可以直接定位到复位路径的物理实现问题。追查X态时也有技巧。当仿真出现X态不要只盯着数据路径找先检查复位信号的波形。看看复位释放沿的位置和所有相关时钟沿的位置把它们放在同一个时间坐标下对比。如果复位释放沿距离某个时钟沿非常近而X态恰好出现在那一拍附近八成就是复位时序问题。再结合CDC工具报告基本能确认根因。5.3 收敛流程从回归到signoff把复位CDC问题纳入signoff流程是我个人觉得最重要的一步。很多团队的CDC signoff checklist里只写了数据同步器检查复位检查是附带的甚至有人根本没意识到要查。实际上复位网络的高扇出特性决定了它比普通数据路径更值得花时间做形式化检查。我的收敛流程大致是这样RTL阶段先用SpyGlass跑一遍复位CDC检查把所有异步复位释放未同步的violation清零然后综合后拿网表再跑一次重点看recovery/removal违例最后后端布局完成后用STA和CDC工具交叉验证复位树的延迟和释放沿对齐情况。任何一步有违例都要回到RTL或约束层面解决不允许带着未关闭的复位CDC违例进signoff。这里尤其要提醒不要在复位释放路径上盲目设set_false_path来“消除”违例。你是把这个约束加上了报告干净了但芯片里的物理时序它不认约束它只认实际的边沿关系。很多芯片流片后偶发复位问题回头看代码往往就是一根set_false_path把复位路径的检查给屏蔽了。这个教训我在多个项目里反复遇到真的不值得再踩。6. 几个容易翻车的边缘场景6.1 复位信号被组合逻辑“加工”后接异步端有人会把复位同步器输出和某个控制信号做逻辑与比如“在上电初始化完成之前模块保持复位”然后把这个组合信号接到寄存器的异步复位端。这种做法的风险在于组合逻辑的延迟和毛刺会破坏复位释放沿的对齐甚至可能产生一个非常窄的“复位毛刺”导致某个寄存器在正常运行中突然被异步复位一拍。异步复位端对毛刺极其敏感你必须保证驱动它的信号是干净的电平信号而不是组合逻辑瞬态的结果。如果确实需要根据某个条件延长复位应该把这个条件也放进复位控制器由控制器重新生成一个干净的复位请求再走一遍同步器。所有寄存器只允许看到同步器输出或者复位树上的缓冲信号中间不能再有任何组合逻辑。6.2 同步器输出又被当数据用复位同步器的输出是一根经过同步的复位信号它本身也连接到很多寄存器的异步复位端。但有些工程师会顺便把它当作一个普通状态信号比如“复位释放完成标志”送给另一个时钟域的逻辑或者软件。这个用法的问题在于它的释放沿虽然和本时钟域对齐了但到了另一个时钟域它又成了异步信号直接使用依然存在CDC风险。如果软件或者其他硬件需要知道“复位已经释放”正确的做法是用一个独立的跨时钟域握手信号把释放状态安全地传递过去。更隐蔽的变体是有人把复位同步器输出的释放沿当作一个事件用来触发某些寄存器的初始化赋值。这样做会把复位释放对齐的时序和功能逻辑的时序搅在一起任何时序微小的偏差都会变成功能错误。复位释放是全局行为功能事件的触发要用功能信号不要复用复位路径。6.3 复位源本身带毛刺或来自另一个时钟域最后一个常见翻车点是复位源本身不干净。外部引脚进来的复位信号先经过输入缓冲如果不加滤波外部环境的抖动可能产生几个窄毛刺。这些毛刺被当成异步复位有效沿就会导致寄存器复位状态被随机修改。处理办法是在复位输入路径上放置专门的毛刺滤波器或者用低频采样逻辑把外部复位先采几拍确认其稳定有效后再往内部复位树送。对于来自另一个时钟域的复位源也一定要先做跨时钟域数据同步再进入复位同步器不能想当然地认为异步复位端能“容忍”一切异步输入。扫描测试模式下也要额外注意。扫描使能期间异步复位信号如果仍然有效会导致扫描移位数据被破坏或者产生不可预测的行为。多数设计里会用测试复位信号在调试模式下接管异步复位或者通过测试控制逻辑屏蔽功能复位。这一点在后端DFT阶段通常会被查出来但如果你的RTL里没有预留测试复位接口后期改起来会很痛苦。我自己的习惯是在每个项目开始时就把复位CDC检查写进RTL评审的必查清单里并且在代码规范中强制所有异步复位必须通过复位同步器模块接入。一开始大家觉得多此一举后来遇到几个因为复位释放没对齐导致的偶发bug所有人都默认了这条规矩。异步复位信号看起来简单但在跨时钟域场景里它往往是仿真最看不到、上板最折磨人的那类问题。先把原则定下来把电路结构写对再靠工具和断言兜底整个系统才算真正稳定。