1. 从一个被反复触发的诡异Bug说起
如果你在SoC前端设计这行干过几年,大概率遇到过这样一种现象:芯片回来之后,大部分功能都正常,偏偏在某些特定场景下——比如上电初始化、低功耗唤醒、或者某个外设突然被复位——系统会随机性地跑飞,而且复现概率极低,可能几千次才出一次。你拿波形去抓,发现某个寄存器的值莫名其妙变成了一个既不是0也不是1的中间态,或者状态机跳到了一个根本不存在的状态。更让人头疼的是,这种问题在仿真阶段几乎抓不到,因为仿真器默认是理想模型,不会模拟复位释放时刻的亚稳态传播。
这个问题的根源,十有八九出在RDC上。RDC是Reset Domain Crossing的缩写,中文一般叫“复位域交叉”。它和大家熟悉的CDC(Clock Domain Crossing,时钟域交叉)是一对孪生兄弟,但RDC的受重视程度远远不如CDC。很多团队在项目初期会花大量精力做CDC检查、加同步器、写约束,但RDC往往被忽略,直到流片回来出了问题才开始排查。而RDC引发的问题,恰恰是那种“平时不显山露水,一出事就是大事”的类型。
这篇文章想聊的,就是SoC设计中异步复位导致的亚稳态问题,以及如何通过合理的RDC设计来规避这些陷阱。我会从复位域的基本概念讲起,拆解异步复位为什么会产生亚稳态,然后给出几种经过实际项目验证的复位同步方案,最后分享一些在RDC检查工具使用和约束编写上的实操经验。不管你是刚接触SoC前端设计的新人,还是已经做过几个项目的老手,希望这些内容都能帮你少踩几个坑。
2. 复位域交叉到底在“交叉”什么
2.1 复位域和时钟域不是一回事
很多人第一次听到RDC这个词,会下意识地把它和CDC混为一谈。确实,两者都涉及“跨域”的问题,但它们的本质区别在于:CDC关心的是信号从一个时钟域传到另一个时钟域时,由于时钟相位和频率的差异导致的采样不确定;而RDC关心的是信号从一个复位域传到另一个复位域时,由于复位释放时刻的差异导致的采样不确定。
打个比方。CDC就像两个人用不同的节奏打拍子,一个人要把球扔给另一个人,如果扔的时机不对,对方可能接不住。RDC则是两个人本来在同一个节奏下打球,但其中一个人突然被叫停又重新开始,而另一个人不知道他什么时候重新开始,结果球传过去的时候对方还没准备好。
在SoC里,复位域通常和时钟域是绑定的,但并不是一一对应。一个时钟域里可能有多个复位域,比如一个模块有全局复位和局部软复位;一个复位域也可能跨越多个时钟域,比如一个全局复位信号同时复位了APB总线和AXI总线上的模块。这种复杂的对应关系,就是RDC问题的温床。
2.2 异步复位释放时刻的“灰色地带”
要理解RDC为什么会导致亚稳态,得先搞清楚异步复位的工作机制。异步复位的特点是:复位有效(通常为低电平或高电平)时,不管时钟是什么状态,触发器立刻被强制到复位值;复位释放时,触发器需要在下一个时钟有效沿才能恢复正常工作。
问题就出在“复位释放”这个动作上。假设复位信号在时钟上升沿附近释放,触发器的复位端从有效变为无效,此时触发器的内部状态处于一个不确定的区域——它既不是完全复位状态,也不是完全正常工作状态。如果复位释放时刻离时钟有效沿太近,触发器的输出就可能进入亚稳态,也就是输出既不是0也不是1,而是在两者之间振荡一段时间,最终随机稳定到某个值。
这个“太近”的范围,就是触发器的复位恢复时间(Recovery Time)和复位移除时间(Removal Time)。Recovery Time指的是复位释放到下一个时钟有效沿之间必须保持的最小时间,Removal Time指的是时钟有效沿到复位释放之间必须保持的最小时间。如果这两个时间不满足,触发器就可能进入亚稳态。
在同一个复位域内,所有触发器同时复位、同时释放,只要复位释放满足时序要求,就不会有问题。但当信号从一个复位域传到另一个复位域时,两个域的复位释放时刻可能不同步,接收端的触发器可能在发送端还没完全退出复位状态时就开始采样,采到的就是一个不确定的值。
2.3 一个典型的RDC故障场景
举个具体的例子。假设一个SoC里有两个模块:模块A在复位域RST_A下,模块B在复位域RST_B下。模块A产生一个valid信号给模块B,模块B用这个valid信号来使能某个数据通路。RST_A和RST_B来自不同的复位源,释放时刻相差几个时钟周期。
在RST_A释放后、RST_B还没释放的这段时间里,模块A已经开始正常工作,valid信号可能已经拉高。但模块B还处于复位状态,它的触发器被强制复位,valid信号在模块B内部被忽略。当RST_B释放时,如果释放时刻恰好靠近时钟有效沿,模块B的触发器可能进入亚稳态,采到的valid值不确定。如果采到1,模块B会误以为数据有效,开始处理一个可能还没准备好的数据;如果采到0,模块B会漏掉一个本来应该处理的请求。无论哪种情况,都是功能性Bug。
更隐蔽的情况是,valid信号本身经过了一个多级触发器同步器。很多人以为加了同步器就万事大吉了,但如果同步器的复位域和发送端不同,同步器本身也可能在复位释放时进入亚稳态,导致同步失败。
3. 异步复位同步释放:最常用的解法及其局限
3.1 基本电路结构
说到解决异步复位亚稳态问题,最经典的方案就是“异步复位同步释放”(Asynchronous Reset Synchronous Release)。这个电路的核心思想是:复位有效时异步拉低,保证复位立即生效;复位释放时通过两级触发器同步到目标时钟域,保证释放时刻与时钟对齐。
典型的电路结构是这样的:复位输入信号先经过一个两级触发器链,触发器的时钟是目标时钟域的时钟,复位端接复位输入信号本身。第一级触发器的输入接常数1(或VDD),第二级触发器的输入接第一级输出,第二级输出就是同步后的复位释放信号。同时,复位输入信号直接接到触发器的异步复位端,保证复位有效时立即拉低输出。
这个电路的工作逻辑是:当复位输入有效时,两个触发器被异步复位,输出为0;当复位输入释放时,第一个触发器在下一个时钟上升沿采样到1,第二个触发器再下一个时钟上升沿采样到1,输出在第二个时钟沿之后释放。这样,复位释放就与时钟对齐了,避免了亚稳态。
3.2 为什么两级触发器就够了
有人会问,为什么是两级而不是三级或更多?这涉及到MTBF(Mean Time Between Failures)的计算。同步器的级数越多,亚稳态传播到输出端的概率越低,但面积和延迟也越大。对于大多数SoC设计来说,两级触发器已经能把MTBF做到足够高,通常几十年甚至几百年才出一次问题。
MTBF的计算公式大致是:MTBF = e^(t/τ) / (T0 × fclk × fdata),其中t是留给亚稳态稳定的时间(一个时钟周期减去建立时间),τ和T0是触发器的工艺参数,fclk是时钟频率,fdata是异步信号的变化频率。从公式可以看出,时钟频率越高、异步信号变化越频繁,MTBF越低。在先进工艺下,τ值变小,MTBF也会降低。所以到了5nm、3nm节点,有些团队会考虑用三级同步器。
3.3 这个方案不是万能的
异步复位同步释放虽然经典,但它有一个重要的前提:同步器本身的复位必须和发送端的复位是同一个复位域,或者至少是同步释放的。如果同步器的复位域和发送端不同,同步器在复位释放时同样可能进入亚稳态,那这个同步器就形同虚设了。
另一个局限是,这个方案只解决了复位释放的同步问题,没有解决复位域之间的信号传输问题。如果模块A在复位释放后立刻给模块B发信号,而模块B的复位还没释放,那模块B仍然可能采到不确定的值。所以,除了加同步器,还需要在协议层面做握手,确保接收端已经退出复位状态后再接收数据。
还有一个容易被忽略的点:同步器链上的触发器本身也需要复位。如果同步器没有复位,上电时触发器的初始状态不确定,可能导致复位释放信号出现毛刺。所以同步器的复位端必须接一个可靠的复位源。
4. RDC检查工具能帮你做什么,不能帮你做什么
4.1 主流RDC检查工具的能力边界
现在主流的EDA工具都支持RDC检查,比如Synopsys的SpyGlass RDC、Cadence的Conformal RDC、Mentor的Questa RDC等。这些工具的基本原理是:识别设计中的复位域,分析跨复位域的信号路径,检查这些路径上是否有同步器或握手逻辑,如果没有就报warning或error。
工具能帮你做的主要是结构性检查:哪些信号跨了复位域、跨域路径上有没有同步器、同步器的结构是否正确、复位域的定义是否完整。这些检查对于大型SoC设计来说非常有必要,因为靠人工review很难覆盖所有跨域路径。
但工具不能帮你做的是:判断某个跨域路径是否真的需要同步。有些跨域信号是静态配置信号,复位释放后不会变化,或者变化时接收端已经退出复位状态,这些路径可能不需要同步器。工具会一律报出来,你需要根据实际场景判断哪些是真正的风险,哪些可以waive掉。
4.2 复位域定义的准确性决定检查质量
RDC检查的第一步是定义复位域。工具需要知道每个触发器的复位端接的是哪个复位信号,以及这个复位信号的释放是否与某个时钟同步。如果复位域定义不准确,检查结果要么漏报要么误报。
在实际项目中,复位域定义通常通过SDC或专门的RDC约束文件来描述。你需要明确指定:每个复位信号的源、每个复位域的时钟、复位释放的同步方式、跨域路径的同步策略等。这些约束的编写质量直接决定了RDC检查的有效性。
我见过一些项目,RDC约束写得很粗糙,只定义了顶层复位,没有细分模块级的复位域,结果工具报了一大堆跨域路径,但大部分都是误报。后来花了两周时间重新梳理复位域,把约束写细,误报率从70%降到了10%以下。所以,RDC检查不是跑一下工具就完事,前期的复位域梳理和约束编写才是重头戏。
4.3 工具报出来的问题怎么分类处理
工具报出来的RDC问题,大致可以分为三类:
第一类是真正的风险路径,跨域信号在接收端复位释放前后可能变化,且没有同步器。这类问题必须修,要么加同步器,要么改协议做握手,要么调整复位释放顺序。
第二类是伪风险路径,跨域信号在接收端复位释放时已经稳定,或者接收端在复位释放后不会立即采样该信号。这类问题可以通过加约束或waive来处理,但需要设计者给出充分的理由。
第三类是工具误报,比如工具没有识别出某个同步器结构,或者复位域定义有误导致路径被错误分类。这类问题需要修正约束或工具配置。
处理这三类问题的顺序很重要。先修第一类,再评估第二类,最后处理第三类。不要一上来就waive,那样可能漏掉真正的风险。
5. 复位域规划的几个实战原则
5.1 复位域数量不是越多越好
有些设计者喜欢把复位域切得很细,每个模块一个复位域,觉得这样控制灵活。但复位域越多,RDC路径就越多,同步器就越多,面积和验证复杂度都上去了。而且复位域之间的释放顺序很难保证,容易引入新的RDC问题。
我的经验是:复位域的数量应该和电源域、时钟域的数量相匹配,不要为了灵活而过度细分。一般来说,一个SoC里复位域的数量控制在时钟域数量的1.5倍以内比较合理。如果发现复位域太多,可以考虑合并一些释放顺序相同、没有跨域交互的复位域。
5.2 复位释放顺序要有明确规划
当SoC里有多个复位域时,复位释放顺序必须有明确的规划。通常的原则是:先释放核心和总线,再释放外设;先释放发送端,再释放接收端。这样可以保证接收端在退出复位状态时,发送端已经稳定,不会采到不确定的值。
但有时候这个顺序很难保证,比如两个模块互相发送数据,谁先释放都不对。这时候就需要在协议层面做握手,或者用一个全局的复位释放信号统一控制。
复位释放顺序的规划要在架构阶段就确定,不能等到RTL写完再补。因为一旦RTL结构定了,再调整释放顺序可能需要改很多模块。
5.3 软复位和硬复位的RDC处理差异
SoC里通常有硬复位和软复位两种。硬复位是上电复位或外部复位,释放时刻由外部条件决定;软复位是寄存器控制的复位,释放时刻由软件写入决定。
软复位的RDC处理和硬复位不同。硬复位的释放时刻是随机的,必须加同步器;软复位的释放时刻是软件控制的,通常与时钟同步,但如果软复位信号跨了时钟域,仍然需要同步。而且软复位释放后,软件需要等待一段时间才能访问被复位的模块,这个等待时间要写进软件驱动里。
我见过一个项目,软复位释放后软件立刻去读被复位模块的寄存器,结果读到了不确定的值。后来在驱动里加了几十个时钟周期的延时才解决。所以,软复位的RDC问题不仅影响硬件,还影响软件。
6. 从RTL到约束:一个完整的RDC排查流程
6.1 第一步:梳理复位域清单
在跑RDC工具之前,先手工梳理一份复位域清单。清单里要包含:复位信号名、复位源、复位类型(硬复位/软复位)、所属时钟域、释放同步方式、影响的模块列表。
这份清单不需要很精确,但要有,因为它能帮你快速定位问题。当工具报出某个跨域路径时,你可以对照清单判断这个路径是否真的跨了复位域,以及跨域的方式是否合理。
梳理清单的过程也是发现问题的过程。有时候你会发现某个复位信号的源不明确,或者某个模块的复位域定义有歧义,这些问题在跑工具之前解决掉,能省很多时间。
6.2 第二步:编写RDC约束
RDC约束的核心是告诉工具:哪些信号是复位信号、每个复位域的时钟是什么、跨域路径的同步策略是什么。
以SpyGlass为例,RDC约束通常包括:
# 定义复位信号 reset -name RST_A -active low reset -name RST_B -active low # 定义复位域 reset_domain -name DOMAIN_A -reset RST_A -clock CLK_A reset_domain -name DOMAIN_B -reset RST_B -clock CLK_B # 定义跨域同步策略 rdc_sync -from DOMAIN_A -to DOMAIN_B -type async_reset_sync约束写完后,先跑一遍检查,看看有没有语法错误和明显的漏定义。然后根据报错逐步完善。
6.3 第三步:分析报告并分类
工具跑完后会生成一份报告,列出所有跨域路径和检查结果。你需要逐条分析,把问题分类。
分析时重点关注:跨域信号的功能是什么、在接收端复位释放时是否会变化、接收端是否有同步器、同步器的结构是否正确、同步器的复位域是否匹配。
对于每条路径,都要给出明确的处理意见:修、waive、还是需要进一步确认。不要留模糊地带,否则问题会一直挂着。
6.4 第四步:修复和验证
修复RDC问题通常有几种方式:加同步器、改协议做握手、调整复位释放顺序、加约束waive。
加同步器是最直接的方式,但要注意同步器的复位域必须和发送端匹配,否则同步器本身会成为新的RDC问题。改协议做握手更彻底,但改动量大,适合在架构阶段做。调整复位释放顺序需要改复位控制逻辑,可能影响多个模块。加约束waive适合伪风险路径,但要有充分理由。
修复后要重新跑RDC检查,确认问题真的解决了,没有引入新的问题。然后跑一遍功能仿真,确认修复没有影响正常功能。
7. 几个容易踩的坑和我的应对经验
7.1 同步器被综合工具优化掉
这是一个非常隐蔽的坑。你写了一个两级同步器,综合工具在优化时发现第一级触发器的输出没有被其他逻辑使用,只驱动了第二级触发器,于是把第一级优化掉了,同步器变成了一级。一级同步器的MTBF远低于两级,亚稳态概率大大增加。
避免这个坑的方法是在同步器上加dont_touch或preserve属性,告诉综合工具不要优化。或者在代码里加一个(* keep = "true" *)的注释。不同工具的属性名不同,需要查手册确认。
7.2 复位释放时的毛刺
异步复位同步释放电路在复位释放时,如果复位输入信号本身有毛刺,可能导致同步器输出出现毛刺。虽然同步器能滤掉一些毛刺,但如果毛刺出现在时钟有效沿附近,仍然可能被采到。
解决方法是在复位输入信号上加滤波电路,或者用施密特触发器整形。对于片内复位,通常问题不大;对于片外复位,建议加RC滤波和施密特触发器。
7.3 仿真抓不到RDC问题
前面提到过,仿真器默认是理想模型,不会模拟复位释放时的亚稳态。所以即使RTL有RDC问题,功能仿真也可能全过。要抓RDC问题,需要用带时序信息的仿真,或者在仿真中注入亚稳态模型。
有些仿真器支持+define或+plusarg来开启亚稳态模拟,但配置比较复杂。更实际的做法是依赖RDC静态检查工具,在RTL阶段就把问题找出来。
7.4 跨复位域的总线协议信号
总线协议信号(如AXI的valid/ready)跨复位域时,处理起来特别麻烦。因为valid和ready是握手信号,如果两个信号跨域的方式不一致,可能导致死锁或数据丢失。
我的经验是:跨复位域的总线信号,最好在协议层做隔离。比如在复位域边界加一个桥接模块,桥接模块的复位域和接收端一致,发送端的信号先同步到桥接模块的时钟域,再由桥接模块转发给接收端。这样可以把RDC问题集中到一个模块里处理,避免分散在各处。
7.5 低功耗唤醒时的RDC问题
低功耗设计里,电源域关断再唤醒时,复位释放的时序和正常上电不同。电源域关断时,域内的触发器状态丢失;唤醒时,复位释放和电源稳定的时序关系可能不满足要求,导致RDC问题。
处理低功耗唤醒的RDC问题,需要在电源管理单元里做专门的复位释放控制,确保电源稳定后再释放复位,且释放时刻与时钟同步。这部分通常需要和低功耗验证团队紧密配合。
8. 写在最后的一些个人体会
RDC这个问题,说大不大,说小不小。它不像CDC那样每个项目都会重点检查,但一旦出问题,排查起来非常痛苦。因为RDC问题往往是概率性的,复现困难,定位困难,修复后验证也困难。
我的建议是:在项目初期就把RDC检查纳入流程,不要等到流片前才想起来。复位域规划要在架构阶段做,RDC约束要在RTL阶段写,检查要在综合前跑。这样即使有问题,也有足够的时间修。
另外,RDC检查工具只是辅助,不能完全依赖。工具能帮你找结构性问题,但判断某个路径是否真的需要同步,还需要设计者对电路功能有深入理解。所以,花时间理解复位域的工作原理,比学会用工具更重要。
最后分享一个小技巧:在RTL里给每个跨复位域的信号加一个命名前缀,比如rdc_,这样在代码review和工具检查时都能快速识别。这个习惯我坚持了好几年,确实能减少遗漏。