1. 为什么复位设计值得单独写一篇文章
做芯片设计的人,尤其是刚入行做数字前端的朋友,通常会把大部分精力放在功能逻辑、流水线效率、低功耗策略这些“看得见算得着”的地方。复位电路这种基础得不能再基础的东西,往往被放到“先把功能跑通后面再说”的优先级里。但实际项目中,我见过太多模块调不通、芯片回来跑飞、甚至流片后靠改金属层才救回来的案例,根因都出在复位上。
复位的本质是什么?一句话:让芯片里所有时序单元从不确定状态进入确定状态。芯片上电瞬间,寄存器、SRAM、触发器里的值都是随机的,如果这些随机值被功能逻辑当成有效数据去处理,轻则状态机跑进非法状态,重则总线冲突、漏电、芯片直接不工作。复位电路就是给整个芯片一个“出厂设置”,让所有模块从一个已知且安全的起点开始跑。
但“给一个起点”这件事,在实际工程里远没有听起来那么简单。你的复位信号应该同步于时钟还是异步于时钟?复位释放的瞬间,如果刚好撞上时钟沿,寄存器采集到的“复位已撤销”信号是0还是1?一个大的SoC里几百万个触发器,靠一根线直接连过去,到达时间能差出几个周期?这些不是教科书里那种“接一个按键加一个电容”就能糊弄过去的,而是会直接决定芯片能不能在全部工艺角、全温度范围下稳定启动的硬问题。
我最早意识到复位问题严重性,是在做一个MCU项目的时候。功能仿真怎么跑都过,前仿、后仿、网表仿真都绿油油,结果芯片回来之后,十个芯片里有三个在低温下概率性启动失败。后来一追,发现是复位释放路径上没有做时序收敛,复位撤销信号到达各个触发器的时刻和时钟沿产生了竞争。那段排查经历让我花了不少时间,也让我下定决心把复位设计从头到尾系统梳理一遍。这篇文章就围绕“异步复位同步释放”和“复位树构建”这两条主线展开,适合数字前端工程师、芯片验证工程师,也适合对复位设计原理感兴趣的学生和刚入行的新人。内容不会太学术,但该讲清楚的原理一点都不会省。
2. 三种复位策略的取舍
2.1 同步复位:省心但没那么省资源
同步复位,顾名思义,复位信号必须配合时钟沿一起生效。也就是说,复位的置位和撤销都是相对于时钟沿来说的,复位信号本身不直接驱动触发器的异步端,而是作为数据路径上的一个逻辑条件。
RTL写法很直接:
always @(posedge clk) begin if (!rst_n) begin q <= 1'b0; end else begin q <= d; end end这段代码综合出来之后,复位信号会进入触发器数据输入端的组合逻辑,跟D端数据做一个二选一。这样做的优点很突出:复位行为天然和时钟对齐,不存在复位释放导致亚稳态的问题。因为释放的时候,复位信号作为数据路径的一部分,受建立时间和保持时间约束的保护。后端做时序约束的时候,也比较简单,把它当作普通数据路径来约束和处理就行了。
但它有几个绕不开的缺点。第一个是面积和功耗代价。因为复位进入了数据路径,每个触发器前面都得多一组逻辑门,对面积敏感的设计来说,白白浪费资源。功耗上,因为数据路径翻转率本身高,复位作为数据路径的一部分,也会增加这部分翻转的功率。第二个问题是异步信号进同步逻辑需要额外处理。如果你的复位源本身是异步的(比如外部按键复位、看门狗超时复位),必须在模块内部先做两级同步器,否则就是一个标准的CDC违例。第三个问题是所谓“复位传播”慢。同步复位需要时钟存在才能完成复位,如果时钟停了,复位永远也进不去。在有些需要“时钟还没起来就要把关键寄存器钉死在安全值”的场景下,同步复位根本做不到。
2.2 异步复位:直截了当但释放有坑
异步复位就是直接把复位信号接到触发器的异步复位端(通常是CDN端,低有效)。RTL写法:
always @(posedge clk or negedge rst_n) begin if (!rst_n) begin q <= 1'b0; end else begin q <= d; end end综合工具这时候会把rst_n映射到触发器的异步复位引脚,不需要额外组合逻辑,面积和延迟都比同步复位有优势。而且复位不依赖时钟,只要复位有效,不管时钟有没有、时钟跑多快,触发器一律被清零。这个特性在芯片刚上电、时钟PLL还没锁定的阶段是保命的。
但是异步复位最经典的坑就是复位释放(de-assertion)时的亚稳态问题。回想一下,异步复位只要保持有效,触发器就一直处于复位态,这时候复位什么时候撤除、撤除瞬间时钟在什么相位,都不影响复位状态本身。问题的关键在于释放的一瞬间:如果复位信号在时钟有效沿附近释放,触发器的异步端从“有效”变成“无效”,而这个变化又正好处在时钟沿的建立保持窗口里,触发器输出可能进入亚稳态——既不是0也不是1,也可能震荡,最后收敛到哪个值完全不可预测。
更麻烦的是,因为复位信号到每个触发器的物理距离不同,同一个复位源释放时,有的触发器已经看到“复位撤销”了,有的还认为“复位还在有效”。这种复位偏移(reset skew)会导致整个芯片在退出复位的那个周期里,状态是混乱的。功能上表现为什么都可能:状态机跳到未定义状态,总线进入高阻与驱动冲突的竞争态,甚至直接死机。
所以异步复位用得好是利器,用得糙就是定时炸弹。关键点就在于怎么处理“释放”这个动作,这就是异步复位同步释放要解决的问题。
2.3 异步复位同步释放:业界主流做法的核心原理
异步复位同步释放,英文叫 Asynchronous Assert, Synchronous De-assert,或者常见的缩写 Async Reset Synchronized Release。它的核心思想用一个词概括就是“各取所长”:
- 复位的建立(assertion)走异步路径:复位信号一旦有效,立即通过触发器的异步端生效,不管时钟有没有、时钟是否稳定。这样保证了最快响应和最小电路开销。
- 复位的释放(de-assertion)走同步路径:在释放之前,先让复位信号经过两级同步触发器,让它和时钟域对齐,再统一释放到各个功能触发器。
标准电路结构如下:
module reset_sync #( parameter NUM_STAGES = 2 ) ( input wire clk, input wire arst_n, // 异步复位输入(原始复位源) output wire rst_n_out // 同步释放后的复位输出 ); reg [NUM_STAGES-1:0] rst_n_sync; always @(posedge clk or negedge arst_n) begin if (!arst_n) begin rst_n_sync <= 'b0; // 异步置位 end else begin rst_n_sync <= {rst_n_sync[NUM_STAGES-2:0], ~arst_n}; end end assign rst_n_out = rst_n_sync[NUM_STAGES-1]; endmodule实际上,标准的异步复位同步释放结构是“异步复位,同步释放”或者更严格的“异步置位,同步移出”。以下是更经典的双触发器结构:
reg rst_n_r1, rst_n_r2; always @(posedge clk or negedge arst_n) begin if (!arst_n) begin rst_n_r1 <= 1'b0; end else begin rst_n_r1 <= 1'b1; end end always @(posedge clk or negedge arst_n) begin if (!arst_n) begin rst_n_r2 <= 1'b0; end else begin rst_n_r2 <= rst_n_r1; end end assign rst_n_out = rst_n_r2;注意这两级触发器的复位端依然连的是原始异步复位信号,所以复位有效时它们会立即被拉低,输出也是低,整个功能模块被复位。当异步复位释放时,第一级触发器进入“等待下一个时钟沿置1”的状态,第二级同样,两级链保证释放动作只在时钟沿之后才传播出去,从而杜绝了复位信号释放沿和时钟沿竞争的问题。
为什么必须两级而不是一级?这是经典的CDC同步器思路。异步信号进入同步时钟域,第一级触发器存在亚稳态风险,第二级的作用就是给第一级的亚稳态足够长的收敛时间。一级同步器在复位随机释放面前,失效率仍然大到不可接受。两级之后,只要第一级的亚稳态在一个周期内收敛,第二级就能采到稳定值,MTBF(平均无故障时间)就非常高了。三级在部分对安全要求极高的场景会使用,常规设计里两级足够。
从行为上理解,这个电路相当于一个“异步置位、同步撤除”的边沿触发器。它被置位不需要时钟,但撤销必须等时钟。这样的设计同时满足了“复位必须立刻生效”和“释放必须避开竞争”这两个看似矛盾的需求。
2.4 三种方案关键指标对比
| 指标 | 同步复位 | 异步复位 | 异步复位同步释放 |
|---|---|---|---|
| 复位生效是否依赖时钟 | 依赖,时钟必须存在且稳定 | 不依赖,立刻生效 | 不依赖,立刻生效 |
| 资源开销(面积/功耗) | 较高,复位逻辑进入数据路径 | 较低,使用DFF原生异步端 | 较低,功能DFF同样用异步端,仅额外两级同步器 |
| 释放风险(亚稳态/复位偏移) | 低,受时序约束保护 | 高,释放与时钟竞争 | 低,释放通过两级同步器对齐 |
| 时序约束复杂度 | 简单,当作数据路径 | 复杂,需要reset recovery/removal约束 | 适中,也需要recovery/removal约束,但释放路径被同步 |
| 抗毛刺能力 | 较好,不直接驱动异步端 | 较差,毛刺直接导致误复位 | 较差但可缓解,毛刺仍可能误触发,需要额外滤波 |
| 典型应用场景 | 模块级弱复位、无时钟域问题的小设计 | 简单模块复位、对面积敏感且复位释放风险可控的设计 | SoC主流选择,几乎覆盖所有数字模块复位 |
实践里我基本只在两种情况下用纯同步复位:一种是模块内部自己产生的、明确与时钟域对齐的复位信号;另一种是极小的模块,几十个触发器,没有异步复位源进入,同步复位写得顺。一旦设计规模上来,复位源是外部引脚或跨域信号,基本都采用异步复位同步释放。
3. 复位释放的时序本质:recovery 与 removal 检查
3.1 为什么释放路径也是一条寄存器到寄存器的路径
很多前端工程师对复位信号的处理有误解,以为复位信号不是常规数据信号,不需要做时序检查。在用异步复位同步释放之后,释放路径上其实已经产生了一条从“同步器内部触发器”到“功能触发器异步端”的路径。虽然这些寄存器的异步端不参与计算,但复位释放信号到达时刻如果与时钟沿距离过近,就会引发前面提到的亚稳态问题。
这里有两个关键时序参数必须理解:
- 恢复时间(Recovery Time):类比于建立时间。它定义的是:在时钟有效沿到来之前,异步复位信号必须从无效状态转变(撤销)为有效状态的最小提前量。也就是说,释放动作不能紧贴着时钟沿来,必须提前一段时间稳定下来。
- 移除时间(Removal Time):类比于保持时间。它定义的是:在时钟有效沿之后,异步复位信号必须保持有效状态的最小时间。换句话说,时钟沿到了之后,复位信号不能立刻撤掉,得稳定保持一段时间,否则沿后面采样到的状态无法保证。
这两个时序参数在标准单元库里都有定义,通常以触发器的lib文件约束存在。异步复位同步释放电路做的事情,本质上是把“复位释放这个异步事件”重新同步到了时钟域,使得它和时钟沿之间的关系可以满足recovery和removal检查。
3.2 约束这样写才不留坑
设计里有专门的复位同步器模块之后,对释放路径的约束通常有两种做法。我在不同项目里都试过,简单说说各自的用法。
一种做法是把复位同步器输出的复位信号与第一级时钟沿之间的路径,用set_false_path约束掉。理由是:同步器输出的复位信号是异步释放的,时序工具并没有必要去修一个实际不存在的功能路径。但这种约束有一个前提,就是你必须极度确信同步器逻辑没有其他路径能遇到真正的时序问题。比如,如果你把复位同步器的第二级触发器输出接到另一个触发器的数据端,那条路径就不该被false path误伤。
另一种更严谨的做法是对复位释放路径使用专用的reset约束。SDC里对应的命令是:
set_reset_signal -type sync -active low -domain reset_domain -hierarchy ...不过这个命令主要用于指导综合布局布线工具把复位信号当作复位类信号处理,并非所有工具都友好。实战中我更推荐在复位同步器模块上加set_false_path -from约束:
# 复位同步器输出的释放路径,锁定到功能触发器的异步端 set_false_path -from [get_pins reset_sync_inst/rst_n_r2_reg/C] -to [get_pins .../CDN]但这条约束之后需要单独对复位同步器内部做recovery和removal检查。更准确的做法是直接把释放路径上需要的recovery/removal时序约束加在同步器寄存器上,让工具去优化同步器输出到各功能触发器异步端的树形布线延迟。
实际项目里我习惯同时做两件事:
- 对复位同步器内部的寄存器,设置完整的
set_clock_groups -asynchronous划分,避免工具把同步器和功能逻辑混在同一个时钟域里乱优化。 - 同步器到功能触发器异步端的路径,用
set_false_path屏蔽数据时序检查,同时额外报告这些引脚的recovery/removal检查结果,人工确认余量。
芯片设计工具的约束写法各家有不少细节差异,但原理是一样的:释放路径要保证“复位释放”这个信号到达所有功能触发器的时刻,离各自时钟沿都有足够距离。
3.3 多时钟域场景下的处理
一个大的SoC不可能只有一个时钟域。CPU核、总线、外设、内存控制器,各自跑在不同频率甚至完全异步的时钟下。复位信号要怎么分配?
常见的方案是全局复位源先做一次异步复位同步释放,输出一个“全局复位已释放”信号,然后各时钟域分别对该信号再做一次本域的同步释放。也就是说,复位同步器本质上要为每个时钟域做一份副本。
时序上需要注意:如果全局复位释放信号在时钟域A的同步器里已经变为“已释放”状态,但时钟域B的同步器还没有看到这个变化,这个时候两个时钟域模块的复位释放时刻就是不同的。这个偏移有没有关系?要分情况。如果两个域之间在复位释放后的第一个周期就已经有通信,那就要小心了。比如域A释放后立刻去访问域B的寄存器,而域B还在复位态,那么从域A视角读到的全是0,可能会被当成有效数据。
解决这个问题有两种思路:
- 按依赖顺序释放:先释放给所有模块供时钟/电源管理使用的“基础设施复位域”,再释放功能逻辑域,最后释放与外界通信的IO域。每一个域的释放,都等待其依赖的下游域已经稳定释放。
- 在跨域路径上加同步器:对于存在跨域通信的接口,在接口上正常做异步FIFO或握手同步,这样即使两个域复位释放有先后,也不会把复位态误判为有效数据。
实际项目中,我倾向于把“复位顺序”做成可控的配置项。复位管理器(Reset Manager)用状态机控制不同域的释放时序,每个域释放之间的间隔是固定周期数,这个间隔要覆盖路径上最长同步器的稳定时间,一般加上个10~20个周期的余量就非常稳了。同时,跨域接口按常规CDC设计,这样无论复位顺序怎么配置,数据都不会错乱。
4. 复位树构建:从单点同步到全芯片分配
4.1 复位信号为什么也需要“树”
做过后端物理实现的朋友都知道时钟树。时钟信号从时钟源出发,经过一系列缓冲器,分发给所有触发器时钟端,保证时钟到达各个触发器的偏差(skew)在可控范围内。复位信号面临的问题,和时钟信号有相似之处,但也有本质区别。
相似之处在于:复位信号也是一个单源、多负载的信号。一个SoC里可能有几十万甚至上百万个触发器需要复位,一个复位源的驱动能力不可能直接拉到这么多负载上,必须通过缓冲器逐级扇出。就像一条水管要给全城供水,中间得有泵站和管道网络。
区别在于:时钟树需要满足严格的skew要求,因为所有触发器共享同一个时钟沿,skew过大会直接造成建立/保持违例。而复位树呢?如果使用的是异步复位同步释放方案,实际上“各个触发器何时进入复位态”并不要求完全一致——异步复位只要有效就会立刻生效,早一个周期晚一个周期触发复位,在复位期间没有功能性影响。真正关键的是释放时刻的一致性。如果有的触发器已经释放,有的还在复位,那么在这段不一致的窗口内,已释放的触发器开始用“复位后初始值”参与逻辑,而未释放的触发器还在复位态,两个状态就会互相打架。这个窗口就是“释放偏移”。
所以复位的异步建立允许复位树有较大的插入延迟差异,但释放的一致性要求所有触发器从“复位态”到“释放态”的时间尽量对齐。这种不对称的要求,决定了复位树的结构不能完全照抄时钟树。
4.2 复位缓冲器选型,这个细节最容易翻车
复位树里使用的缓冲器,有一个非常容易踩的坑:普通缓冲器在复位阶段自己也需要复位吗?实际上,复位树上的缓冲器只是逻辑门,不需要复位。但如果你用的缓冲器是带复位的寄存器型单元(比如做流水线用的flop),那就会出问题:这些寄存器自己也需要复位信号,而它们的复位信号又来自于这棵复位树——这会导致先有鸡还是先有蛋的死循环。
因此构建复位树时,必须选用纯组合逻辑的缓冲器(buffer)或反相器对(inverter pair),绝不能选带时序逻辑的单元。哪怕只是很小的局部复位分支,这个原则都不能破。另外,缓冲器的驱动强度和树的层级结构,要根据负载数量、布线长度、目标频率来计算。我的一般做法是:先用综合工具报告复位网络的扇出情况,对于扇出超过工具建议值的节点,手动插入buffer tree,或者用综合工具自带的set_clock_tree_options类似的命令对复位树单独做树形优化。
4.3 全局复位树与局部复位树的分层设计
现在主流的SoC复位架构,基本是三层结构:
- 第一层:芯片级复位源。外部复位引脚、上电复位(POR)、看门狗复位、软件复位等,统一进入复位管理器。
- 第二层:复位管理器(Reset Manager)。它负责做异步复位同步释放,并按设计好的顺序生成各域复位信号,比如
rst_cpu_n、rst_bus_n、rst_periph_n、rst_io_n。 - 第三层:各模块内部的局部复位树。每个模块拿到本域复位信号后,通常在模块入口再做一次本地同步释放,然后生成模块内部使用的
rst_n,再通过buffer树驱动到模块内各个触发器。
为什么一定要在模块入口再做一次同步释放?因为全局复位信号在到达模块入口时,经过了漫长的全局复位树,插入延迟较大,信号边沿也可能变慢。如果再直接驱动模块内部的触发器,释放沿的质量很难保证。模块入口的本地同步器,相当于把“长距离传输的信号”重新对齐到本模块时钟域,这样从模块内部的视角看,复位释放就像一个本地信号,后端约束容易收敛得多。
还有一种设计是把第一层和第二层合并,即全局复位源直接同步释放,不再经过复位管理器。但从可维护性和可控性角度,我还是强烈推荐做一个独立的复位管理器。调度顺序、可配置延迟、状态监控这些功能,等到芯片调试阶段你就会发现有多重要。一次流片回来,发现某个外设复位时序不对,如果复位管理器支持寄存器配置释放间隔,一条软件命令就解决了,而不是再改版。
4.4 复位树上的毛刺、倾斜和电源域问题
复位树有一个时钟树不太容易遇到的问题:复位信号上的毛刺(glitch)。异步复位只要毛刺幅度够深、宽度够宽,触发器的异步端就可能被误触发,导致寄存器随机复位。毛刺来源通常是跨电源域信号,或者信号走线太长、相邻信号翻转耦合。处理毛刺有几种常见手段:
- 在复位同步器前面加一个毛刺滤波器(glitch filter),通常是一个RC滤波器配合施密特触发器,或者数字上做连续N拍采样,连续N拍都是有效电平才认为复位有效。
- 对进入芯片的异步复位引脚,约束输入延迟,并在IO处加同步器逻辑。
- 物理上让复位树走线远离高速翻转的总线,降低耦合。
关于复位倾斜(reset skew),前面已经讲了释放一致性是最主要的约束。后端实现时,复位树虽然不像时钟树那样要严格balance,但对于释放路径上的关键节点,还是应该让工具对复位树的末端做balance处理。在Place & Route阶段,我会对全局复位树的末端buffer设置set_dont_touch,防止工具为了修时序把它挪走或删除,然后在signoff阶段专门报一下各模块入口复位信号的到达时间差,目标是把最差释放偏移控制在几个时钟周期内。对大多数设计来说,这个量级的偏移完全无害,因为模块内触发器再次通过本地同步器对齐了。
4.5 功耗和低功耗模式下的复位处理
现代芯片几乎都有低功耗模式,复位设计和低功耗的交互是一个容易被忽略的点。典型场景:在休眠模式下,模块时钟被关闭(CLKGATE),甚至电源域被关断(Power Gating),此时复位信号怎么办?
如果是时钟关闭但电源不关的模块,复位信号释放时模块内触发器没有时钟,复位释放的同步就无从谈起。这种情况下需要确保:模块在时钟恢复之前保持复位有效,时钟恢复并稳定之后,再释放复位。这个顺序通常由电源管理单元(PMU)配合时钟管理单元(CCU)通过状态机来保证。
如果是电源关断的模块,复位信号在电源域关断期间应该保持有效,重新上电后,等电源稳定再释放。这里需要特别注意隔离单元(isolation cell)和电平转换器(level shifter)的配置:关断域的复位信号如果要输出到常开域,必须经过隔离单元,否则浮动电压会漏电或影响常开域逻辑。
低功耗模式下的复位释放顺序,我通常设计成和上电复位释放顺序一样的机制,只是触发源从POR变成了“唤醒事件”。这段逻辑放在PMU里,和功能逻辑彻底分开,用常开域的独立小状态机实现,防止功能逻辑在休眠时把PMU带乱。
5. 实际项目中踩过的复位坑和排查链路
怎么描述“排查复位移除时序问题”的过程呢?最好的方式是把完整的排查链路写清楚,让大家在自己的项目里遇到类似现象时,知道从哪下手。
5.1 现象:芯片概率性启动失败,低温更明显
那次MCU项目的现象是:某些芯片在低温(低于0°C)下,约30%的概率无法正常启动。示波器抓外部晶振波形和复位引脚波形,看起来都正常。串口没有任何输出,JTAG链能连上但CPU核停在未知状态。
第一反应是查时钟,因为晶振在低温下起振困难是常见问题。但示波器显示晶振振幅正常,PLL锁定信号也正常。接着怀疑电源,用高带宽示波器测内核电压的纹波,没发现明显异常。
然后转向检查复位释放。在这款芯片里,全局复位源经过一个外部复位引脚和内部POR,然后进复位管理器,生成rst_cpu_n等域复位信号。我们把复位信号用逻辑分析仪长时间抓取,同时配合一个GPIO输出指示“CPU已经完成初始化”。结果发现:复位释放后,CPU初始化代码执行到一半,某些外设寄存器读取的值和预期不符,导致初始化流程跑飞。
5.2 排查链路:从前仿环境补测试用例
在功能仿真环境里,通常的复位测试只是简单地拉低复位、拉高复位、跑代码,很少会对“复位释放沿相对时钟沿相位”做穷举。但真实芯片上,每次上电复位释放的相位都是随机的,和时钟沿的关系不可控。
我后来做的一件事,是在验证环境中加入了对复位释放相位的扫描测试:让复位释放时刻相对时钟沿在一个周期内以1/20周期步进遍历,然后跑一遍完整的启动序列,检查初始化结果是否一致。这一扫,果然找到了多个模块存在对复位释放相位敏感的问题。修复方法是调整这些模块内部的复位同步器,以及修正某些模块入口复位信号的约束,让释放路径的时序余量更充足。
这个排查经历给我的最大启发是:功能验证通过不等于复位没问题,复位释放相位的鲁棒性必须通过针对性测试来覆盖。验证工程师看到这篇内容,建议检查一下自己的验证环境中是否有这样的扫描用例,没有的话可以补一个。
5.3 现象:看门狗复位后系统状态不确定
另一个常见坑是看门狗复位。小型MCU项目里,看门狗超时后会产生复位信号。如果这个复位信号直接接入复位管理器,那么看门狗复位的释放路径和上电复位的释放路径是一样的,理论上应该没问题。但实际项目中,看门狗复位往往还需要保留某些调试信息(比如复位原因寄存器),这些信息是用普通寄存器存储的,如果它们也被看门狗复位“一把清掉”,调试数据就没了。
解决办法是把“复位原因寄存器”放在一个独立的小模块里,使用不同的复位信号,只受POR复位,而不受看门狗复位影响。这个模块的复位移除路径也要单独约束,防止它在其他模块还在复位时被错误访问。
这类问题不是RC电路能解决的,是芯片架构层面的决策:哪些逻辑应该被哪些复位信号复位。我的经验是,在设计初期就把复位域定义清楚,和时钟域一样重视。一个寄存器只能归属于一个复位域,如果它需要受多个复位源控制,通常的做法是在软件层面管理,而不是在硬件上叠加多个复位信号。
5.4 现象:复位信号毛刺导致的间歇性误复位
还有一个项目,功耗不高,但芯片在强电磁干扰环境下会偶发复位。这个现象排查起来最难,因为它不是每次复现,频率也极低,可能跑几小时才出现一次。我们用片上调试接口在复位发生前记录关键信号到RAM,然后通过JTAG读出。最终定位到是外部复位引脚上的一个毛刺。毛刺本身只有几百皮秒宽,但恰好超过了触发器的异步端最小脉冲宽度要求,触发了异步复位。
解决手段包括:在外部复位引脚输入路径上增加毛刺滤波器,以及重新规划PCB上复位引脚的走线,让它远离大电流开关节点。这个案例说明,即使是“外部简单RC复位电路”这种看起来很成熟的设计,放到复杂电磁环境里也可能出问题。
6. 一些值得带走的实操经验
这篇文章聊了异步复位同步释放的原理,也聊了复位树的物理设计考量。最后分享几个我实际项目中沉淀下来的、常规文档里不会写的小经验。
第一,复位信号命名一定要带后缀。rst_n是低有效,rst是高有效,如果用reset这种模棱两可的名字,综合工具默认的映射行为可能会让你在检查网表时浪费大量时间。我习惯在模块端口命名时就明确标注:arst_n表示异步复位低有效,srst_n表示同步复位低有效,rst_n表示模块统一复位。看代码的人一眼就能知道这个复位是异步同步释放出来的还是普通同步复位。
第二,复位同步器模块不要被综合工具优化掉。因为异步复位同步释放电路的结构很固定,工具有时会“聪明”地把它和功能逻辑合并,导致等效电路不符合预期。我通常会对复位同步器模块设置set_dont_touch,确保它的结构在综合、布局布线全流程中都保持不变。代价是这部分电路无法被工具优化,面积略有增加,但换来的确定性和可调性远远值得。
第三,复位释放顺序要在设计文档里画清楚。做大型SoC时,复位域之间的关系、释放时序、依赖关系,一定要有明确的文档,最好配一张时序图。芯片调试阶段,几个模块的复位顺序搞反,问题会以非常奇怪的方式出现,排查起来特别痛苦。这些文档可能在芯片回来之前看起来没什么用,但真到了调试阶段,你会感谢当时的自己。
第四,不要轻易在RTL里手动插入buffer树。很多前端工程师看到复位扇出太大,就手动在RTL里加buffer,这其实是后端工具的活。你手动加的buffer在后端布局时可能被移动甚至删除,而且可读性很差。正确做法是在综合约束里约束复位信号的max_fanout,或者在后端专门针对复位信号做树形优化。
第五,复位信号和时钟信号一样,需要一个“clean”的开始。如果芯片没有POR(Power-On Reset)电路,外部复位引脚必须保证在上电后维持足够长的低电平时间,直到电源电压稳定。很多“芯片上电后概率性死机”的问题,最后查出来的根因都是POR时间不够,复位提前释放。电源管理芯片(PMU)的复位输出延时可以解决这个问题,或者用外部复位IC的电源监控功能。
说实话,复位设计在芯片设计的教科书里往往只有一两页,但它牵扯到的时序、物理实现、低功耗、验证策略,每一项都值得认真对待。尤其对做SoC和MCU的工程师来说,把复位架构想清楚,比多做几个功能模块更能决定芯片的稳定性和可调试性。希望这篇文章能把一些实践经验传递给正在做相关工作的读者,少走几步我走过的弯路。