简介:面向FPGA与Verilog初学者及需要夯实复位设计基础的工程师,这份文档围绕同步复位、异步复位与同步释放三种机制展开对比,剖析各自优缺点,并延伸到PLL配置场景下的复位设计思路。内容结合综合后的RTL视图,讲解异步复位专用CLRN端口的使用、同步复位带来的额外资源消耗,以及两级寄存器异步复位中复位撤销落在建立保持时间窗口内引发的亚稳态与系统不同步隐患,进而说明同步释放如何借助同一时钟域寄存器消除该问题,最后给出PLL时钟有效输出前保持系统复位的设计方法。资源包共1个docx文件,约217KB,篇幅精炼、图文结合,便于对照代码与电路理解。已有2374人学习,适合作为复位机制选型与PLL复位电路设计的参考笔记,帮助读者理清三种方案的取舍依据与工程落地要点。
1. 三种复位方式,我为什么最后只留一种
前几年做某图像处理 Demo 的 FPGA 前端,板子跑起来十次有两次上电后图像错位,重新烧录又正常。查了三天,最后定位到复位信号撤销的时机撞上了时钟沿——典型的异步复位亚稳态。那次之后我把项目里所有复位逻辑翻了一遍,也把同步复位、异步复位、异步复位同步释放这三种写法挨个综合出来对比了 RTL 视图和资源占用。这篇就把我比过的东西摊开讲:三种复位各自的真实代价在哪、两级寄存器为什么能救场、PLL 场景下复位链该怎么串。刚接触 FPGA 的能照着改代码,做过几个项目的能对上自己踩过的坑。
2. 三种复位方式逐项对比:从 RTL 视图看真实代价
选复位方式这件事,光看代码写法没意义,得看综合器把它变成了什么。我一般会拿同一个两级寄存器模块,分别用三种写法综合,然后打开 RTL 视图逐个看端口连接和资源消耗。下面这张表是我在几个项目里反复验证后整理的判断依据,不是教科书抄来的。
| 对比维度 | 同步复位 | 异步复位 | 异步复位同步释放 |
|---|---|---|---|
| 敏感列表写法 | always @(posedge clk) | always @(posedge clk or negedge rst_n) | 复位同步器 + 异步复位寄存器 |
| 综合后复位端口 | 无 CLRN,复位信号进数据逻辑 | 接寄存器专用 CLRN 端 | 同步器输出接 CLRN |
| 额外资源消耗 | 每个寄存器多一级输入逻辑 | 几乎为零 | 每个时钟域多两级触发器 |
| 复位生效时机 | 等时钟沿 | 立即生效 | 立即生效 |
| 复位撤销风险 | 无亚稳态,但可能漏采 | 撤销落在建立/保持窗口内会亚稳态 | 撤销被同步到时钟域,风险消除 |
| 对时钟的要求 | 复位期间时钟必须翻转 | 复位期间时钟可停 | 复位期间时钟必须翻转 |
| 适用场景 | 小规模、复位源单一 | 上电复位、需要立即停机的场合 | 多时钟域、大项目默认选择 |
2.1 同步复位:省了 CLRN 端口,多了逻辑级数
同步复位的写法很干净,敏感列表里只有时钟:
// 同步复位:复位信号只在时钟上升沿被采样 always @(posedge clk) begin if (!rst_n) begin q <= 1'b0; // 复位有效时清零 end else begin q <= d; // 正常工作时更新 end end综合之后打开 RTL 视图会发现,寄存器根本没有用到 CLRN 清零端,复位信号 rst_n 被综合成了数据输入端的使能逻辑——相当于在 D 和寄存器之间插了一个与门/多路选择器。这意味着每个被复位的寄存器都要多消耗一点组合逻辑资源。单个寄存器看不出来,几千个寄存器同时同步复位,LUT 占用会明显上去。
同步复位的另一个隐性约束是:复位期间时钟必须一直在翻转。如果系统进了低功耗状态把时钟关了,同步复位就永远等不到那个上升沿,复位信号挂在那里也没用。这一点在带时钟门控的设计里特别容易翻车,我见过有项目在调试模式下关时钟,结果复位按下去没反应,查了半天以为是按键坏了。
它也不是没有优势。同步复位天然没有撤销时亚稳态的问题,因为复位信号只在时钟沿被采样,采样到的值要么是 0 要么是 1,不存在中间态。所以小规模设计、复位源单一、时钟常开的场景,同步复位写起来最省心。
2.2 异步复位:CLRN 端白用,撤销时机是黑匣子
异步复位的写法多一个敏感项:
// 异步复位:复位信号有效立即清零,不等时钟沿 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin q <= 1'b0; // 复位有效立即生效 end else begin q <= d; end end综合后 RTL 视图里,每个寄存器都会连到专用的异步清零端 CLRN。这个端口是 FPGA 寄存器自带的硬件资源,不占额外 LUT,所以从资源角度看异步复位几乎零成本。即使代码里写的是高电平有效复位,综合器也会自动反向后接到低有效的 CLRN 端,这一点不用手动处理。
问题出在复位撤销的那一刻。复位信号从低变高,如果这个跳变正好落在时钟的建立时间和保持时间窗口内,寄存器采样到的 rst_n 就是不确定的——可能是 0 也可能是 1,这就是亚稳态。原文里那个两级寄存器的例子说得很清楚:b 和 c 两个寄存器,复位撤销时可能都跳出复位、都保持复位,也可能一个跳出一个没跳,最后一种情况直接导致系统状态不同步。
单个两级寄存器出问题还能靠重新上电救回来,大项目里几百个寄存器同时面对一个异步撤销的复位信号,每个寄存器的时钟偏斜、布线延迟都不一样,撤销信号到达各寄存器的时间有先有后,亚稳态出现的组合就更多了。这就是为什么异步复位单独用在大项目里是给自己埋雷。
2.3 异步复位同步释放:两级触发器买一份后悔药
异步复位同步释放的思路不复杂:复位有效时仍然走异步路径立即生效,但复位撤销时不让原始 rst_n 直接接到寄存器的 CLRN,而是先经过同一个时钟域的两级触发器打拍,用同步后的信号去撤销复位。
// 复位同步器:两级触发器,时钟域与目标寄存器一致 reg rst_sync1, rst_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin rst_sync1 <= 1'b0; // 异步复位立即拉低 rst_sync2 <= 1'b0; end else begin rst_sync1 <= 1'b1; // 复位撤销后第一拍同步 rst_sync2 <= rst_sync1; // 第二拍输出稳定的释放信号 end end wire rst_n_sync = rst_sync2; // 这个信号送给目标寄存器的复位端关键点在敏感列表:同步器本身仍然是异步复位(negedge rst_n),所以复位有效时立刻生效,不用等时钟;但复位撤销后,rst_sync1 和 rst_sync2 要经过两个时钟沿才依次变高,最终输出的 rst_n_sync 是一个与 clk 同步的干净信号。目标寄存器用这个信号做异步复位,撤销时机就被约束在时钟沿之后,避开了建立/保持窗口。
代价是每个时钟域多两个触发器,以及复位撤销会延迟两个时钟周期。对于绝大多数系统来说,两个周期的复位释放延迟完全可以接受,换来的是整个时钟域内复位撤销的一致性。我现在的习惯是:只要设计里时钟域超过一个,复位链一律走同步释放,不再纠结。
3. PLL 场景的复位链:先锁时钟,再放模块
带 PLL 的项目里,复位设计多了一层麻烦:PLL 输出时钟在锁定之前是不稳定的,而系统里大部分模块用的恰恰是 PLL 输出的时钟。如果复位释放得比 PLL 锁定早,模块就在一个频率乱跳的时钟下开始工作,行为不可预测。原文给出的思路是对的——用外部输入时钟先把复位做同步释放,再送给 PLL 和后续模块。我把它拆成可执行的步骤。
3.1 复位链的时序关系
整个上电过程我一般按这个顺序排:
- 外部晶振时钟 clk_in 起振,这是唯一在 PLL 锁定前就稳定的时钟。
- 外部复位 rst_n 有效,系统所有部分保持复位。
- 用 clk_in 对 rst_n 做异步复位同步释放,得到 rst_n_sync。
- rst_n_sync 送入 PLL 的复位端,同时 clk_in 送入 PLL 参考输入端。
- PLL 开始锁定,locked 信号拉高表示输出时钟稳定。
- 用 locked 参与后续模块的复位释放条件,确保模块在时钟稳定后才开始工作。
这里容易忽略的是第 6 步。很多设计只做了第 3 步就把 rst_n_sync 直接当全局复位用,但 PLL 从复位释放到 locked 拉高之间还有一段时间,这期间 PLL 输出时钟可能在爬频率。稳妥的做法是把 locked 和 rst_n_sync 做与逻辑,或者用 locked 再对复位做一次同步释放,保证模块看到的复位释放一定晚于时钟稳定。
3.2 可复用的复位同步模块写法
我把复位同步器写成一个独立模块,项目里每个时钟域例化一份,端口只有时钟、原始复位和同步后复位:
module reset_sync ( input wire clk, // 该时钟域的时钟,PLL 场景下用 clk_in input wire rst_n_in, // 原始异步复位,低有效 output wire rst_n_out // 同步释放后的复位,低有效 ); reg sync1, sync2; // 异步复位、同步释放的标准两级结构 always @(posedge clk or negedge rst_n_in) begin if (!rst_n_in) begin sync1 <= 1'b0; sync2 <= 1'b0; end else begin sync1 <= 1'b1; sync2 <= sync1; end end assign rst_n_out = sync2; endmodule参数说明:clk必须是与目标寄存器同源的时钟,PLL 场景下第一级同步器用外部输入时钟 clk_in,PLL 输出时钟域如果需要独立复位,再在对应时钟域例化第二份。rst_n_in接板级复位或上电复位电路输出。rst_n_out送给该时钟域内所有寄存器的异步复位端。
例化时注意一点:如果 PLL 输出多个频率的时钟,每个时钟域各例化一份 reset_sync,不要用一个同步器的输出跨时钟域去复位别的域,那样同步释放的意义就没了。
3.3 综合后怎么验证复位链没接错
代码写完不算完,我一般会做两个检查。第一,综合后打开 RTL 视图,确认目标寄存器的 CLRN 端连的是同步器的 sync2 输出,而不是原始 rst_n。第二,看时序报告里复位路径的约束有没有被工具正确识别,如果复位信号被当成普通数据路径分析,说明同步器没起作用。
仿真层面,我会在 testbench 里故意把 rst_n 的撤销时刻设在时钟沿附近,跑异步复位版本能看到寄存器输出出现 X 态,跑同步释放版本则输出干净。这个对比仿真做一次,比看十遍文档都管用。
4. 复位设计避坑:五条踩出来的排查记录
4.1 复位按下去没反应,以为按键坏了
现象:调试时按复位键,系统状态不变,示波器量按键信号正常。
原因:设计用了同步复位,但调试模式下时钟被门控关掉了,复位信号等不到上升沿,永远采不进去。
解决:确认复位路径上的时钟在复位期间必须翻转。如果设计有低功耗关时钟模式,复位逻辑要么改异步,要么保证复位期间时钟强制开启。我现在的做法是在时钟门控模块里加一条旁路,复位有效时强制放行时钟。
4.2 上电偶尔图像错位,重新烧录就好
现象:某图像处理 Demo 十次上电有两次数据错位,重新配置 FPGA 后正常。
原因:异步复位撤销时刻随机,撞上某个寄存器的建立/保持窗口,导致部分寄存器提前跳出复位。
解决:把该时钟域的复位全部换成异步复位同步释放,复位撤销延迟两个周期但一致性有保证。改完后连续上电五十次没再复现。
4.3 PLL 没锁就放复位,模块跑飞
现象:系统上电后偶尔不工作,测量发现 PLL 输出时钟频率不对,但过几秒又自己好了。
原因:复位释放早于 PLL 锁定,模块在时钟爬频阶段就开始跑状态机,进了非法状态。
解决:把 PLL 的 locked 信号纳入复位释放条件,模块复位释放必须同时满足 rst_n_sync 有效和 locked 为高。改完后上电时序稳定。
4.4 同步器例化了但没起作用
现象:明明加了 reset_sync 模块,综合后看 RTL 视图,目标寄存器 CLRN 还是接的原始 rst_n。
原因:例化时输出端口连错了,或者综合工具把同步器优化掉了——两级触发器如果被识别为冗余逻辑,某些优化选项下会被合并。
解决:检查例化连接,确认 rst_n_out 确实接到了目标模块的复位端口;在综合设置里对同步器寄存器加 keep 属性防止被优化。我一般会在同步器模块上加(* keep = "true" *)到两级寄存器上。
4.5 跨时钟域共用一个同步器
现象:两个不同频率时钟域的模块共用一个 reset_sync 输出,其中一个域偶尔复位不干净。
原因:同步器的释放时机只对它的源时钟域成立,跨到另一个时钟域后同步关系失效,等于又变回了异步撤销。
解决:每个时钟域独立例化 reset_sync,源复位可以共用,但同步器必须各用各的时钟。多例化两个触发器而已,别省这个。
5. 我的默认选择:异步复位同步释放加 PLL 锁定门控
回到开头那个图像错位的项目,最后的改法就是把整个前端复位链重构成:板级复位进第一级 reset_sync(时钟用外部晶振),输出送 PLL 复位端,PLL locked 与同步后复位做门控,再分发给各时钟域的模块。改完之后那个偶发错位再没出现过,综合资源多了不到二十个触发器,相对于整个设计的占用可以忽略。
如果只能记一条结论:单时钟域小设计,同步复位够用;只要涉及 PLL 或多时钟域,异步复位同步释放是默认答案,别在异步复位单独用的路上反复试。PLL 场景多一步——复位释放必须等 locked,这一步漏了,前面同步器做得再标准也白搭。
我现在的习惯是新建工程先把 reset_sync 模块和 PLL 复位门控逻辑搭好,再往里填业务模块,复位链不后补。这个顺序反过来做过一次,后面查复位问题花的时间够把同步器写二十遍。希望帮到你。
本文还有配套的精品资源,点击获取