FPGA设计中PLL locked信号与复位逻辑的可靠实现

发布时间:2026/10/6 18:00:55
FPGA设计中PLL locked信号与复位逻辑的可靠实现
1. 为什么PLL的locked信号值得单独拿出来讲做过FPGA项目的人都有一个共识系统上电之后最怕的不是逻辑写错而是时序没稳住就开始跑逻辑。PLL锁相环输出的时钟在锁定之前频率和相位都在漂移如果这时候复位已经释放、状态机已经开始跳转轻则数据错乱重则整个系统跑飞而且这种问题往往不是每次上电都出现调试起来非常头疼。locked信号就是解决这个问题的关键。它是PLL的一个输出端口用来指示内部压控振荡器是否已经和参考时钟完成频率和相位的锁定。说白了locked拉高的那一刻才意味着PLL输出的时钟是“干净”的、可以信赖的。把locked信号纳入复位逻辑让系统在PLL稳定之前一直保持复位状态稳定之后再释放复位这是FPGA设计里一个非常经典但又容易被忽视的细节。这篇文章面向的是有一定Verilog基础、正在做FPGA项目的开发者尤其是那些用Xilinx、Intel/Altera、高云等平台做实际项目的朋友。不管你是刚入门的新手还是已经做过几个项目的老手只要你的设计里用到了PLLlocked信号的处理方式就值得认真对待。我会从原理讲到代码从常见坑讲到排查方法把这件事彻底说透。2. PLL locked信号的底层逻辑与复位设计思路2.1 locked信号到底在指示什么PLL锁相环的基本结构包括鉴频鉴相器PFD、电荷泵、环路滤波器、压控振荡器VCO和分频器。参考时钟和反馈时钟同时送入PFDPFD比较两者的频率和相位差输出上下脉冲去控制电荷泵电荷泵再通过环路滤波器产生控制电压驱动VCO调整输出频率。当反馈时钟和参考时钟的频率、相位达到一致时环路进入锁定状态。locked信号就是PLL内部锁定检测电路的输出。不同厂商的PLL IP核实现方式略有差异但核心逻辑大同小异当相位误差持续小于某个阈值、并且持续了一定时间之后锁定检测电路判定PLL已锁定将locked拉高。如果参考时钟丢失、频率突变或者电源波动导致失锁locked会被拉低。这里有一个很重要的细节locked拉高并不代表PLL的输出时钟在那一刻就完美无缺了。它只是说明环路已经进入锁定状态但输出时钟仍然可能存在较小的抖动。对于绝大多数数字逻辑设计来说这个抖动是可以接受的。但如果你的设计对时钟抖动极其敏感比如高速SerDes、射频采样还需要额外关注PLL的相噪指标。注意locked信号本身是异步的它相对于PLL输出时钟来说是异步信号。直接拿它去做复位或者同步逻辑会引入亚稳态风险。这一点后面会详细讲怎么处理。2.2 为什么不能直接用外部复位很多初学者写复位逻辑是这样的外部按键复位直接接到所有寄存器的复位端或者用一个简单的RC电路产生上电复位。这种方案在低频、小规模设计里勉强能用但在有PLL的系统中问题很大。外部复位释放的时刻和PLL锁定的时刻没有任何关联。如果外部复位释放得早PLL还没锁定系统就开始跑逻辑时钟频率还在漂移建立时间和保持时间都无法保证寄存器可能进入亚稳态。如果外部复位释放得晚虽然PLL已经锁定但系统白白浪费了一段启动时间。更糟糕的是如果外部复位信号本身有抖动或者毛刺可能导致系统反复复位。所以正确的做法是外部复位只作为“请求复位”的输入真正的系统复位需要等PLL锁定之后再释放。这样既保证了系统在稳定时钟下启动又避免了外部复位信号的抖动影响。2.3 复位方案的整体架构一个可靠的复位架构通常包含以下几个部分外部复位输入按键、电源监控芯片或者MCU给出的复位信号作为复位的初始请求。PLL locked信号指示PLL是否锁定作为复位释放的条件之一。复位同步器将异步的复位信号同步到PLL输出时钟域避免亚稳态。复位计数器在PLL锁定之后再保持一段时间的复位确保时钟稳定。复位分发网络将同步后的复位信号分发到各个时钟域。这个架构的核心思想是复位释放必须满足两个条件——外部复位请求已经撤销并且PLL已经锁定。两个条件都满足之后再经过若干个时钟周期的同步和延时才真正释放系统复位。3. Verilog实现从locked信号到可靠复位3.1 基础版复位同步器代码先来看一个最基础的复位同步器实现。这个模块的功能是将异步复位信号同步到目标时钟域并在时钟稳定后释放复位。module reset_sync ( input wire clk, // PLL输出时钟 input wire ext_rst_n, // 外部复位输入低有效 input wire pll_locked, // PLL锁定信号 output wire sys_rst_n // 系统复位输出低有效 ); reg [2:0] rst_sync_cnt; reg rst_sync_n; // 复位条件外部复位有效 或 PLL未锁定 wire rst_assert ~ext_rst_n | ~pll_locked; always (posedge clk or negedge ext_rst_n) begin if (!ext_rst_n) begin rst_sync_cnt 3b000; rst_sync_n 1b0; end else begin if (rst_assert) begin rst_sync_cnt 3b000; rst_sync_n 1b0; end else begin if (rst_sync_cnt 3b111) begin rst_sync_cnt rst_sync_cnt 1b1; rst_sync_n 1b0; end else begin rst_sync_n 1b1; end end end end assign sys_rst_n rst_sync_n; endmodule这段代码的逻辑很直接只要外部复位有效或者PLL没锁定复位就一直保持。两个条件都满足之后计数器开始计数计满7个时钟周期后才释放复位。为什么要等7个周期因为复位同步器本身需要2到3个周期来消除亚稳态再加上PLL锁定后时钟可能还有轻微抖动多等几个周期更保险。3.2 加入上电延时和滤波的增强版基础版能解决大部分问题但在实际项目中我遇到过两个额外的坑一是locked信号在某些PLL配置下会有毛刺二是上电瞬间电源不稳可能导致PLL反复锁定/失锁。针对这两个问题增强版加入了输入滤波和更长的复位保持时间。module reset_sync_enhanced ( input wire clk, input wire ext_rst_n, input wire pll_locked, output wire sys_rst_n ); // 对locked信号做滤波连续16个周期为高才认为是真锁定 reg [15:0] locked_filter; always (posedge clk or negedge ext_rst_n) begin if (!ext_rst_n) locked_filter 16h0000; else locked_filter {locked_filter[14:0], pll_locked}; end wire locked_stable locked_filter; // 复位计数器锁定后额外保持255个周期 reg [7:0] rst_cnt; reg rst_n_reg; wire rst_assert ~ext_rst_n | ~locked_stable; always (posedge clk or negedge ext_rst_n) begin if (!ext_rst_n) begin rst_cnt 8h00; rst_n_reg 1b0; end else if (rst_assert) begin rst_cnt 8h00; rst_n_reg 1b0; end else if (rst_cnt 8hFF) begin rst_cnt rst_cnt 1b1; rst_n_reg 1b0; end else begin rst_n_reg 1b1; end end assign sys_rst_n rst_n_reg; endmodule这个版本里locked_filter是一个16位的移位寄存器只有连续16个时钟周期pll_locked都为高才认为锁定是稳定的。这个滤波窗口的大小需要根据你的时钟频率来定。假设PLL输出时钟是100MHz16个周期就是160ns足够滤掉大部分毛刺。如果时钟频率更低比如10MHz16个周期就是1.6微秒也够用。复位计数器从8位扩展到255个周期在100MHz下大约是2.55微秒的额外保持时间。这个时间看起来不长但对于确保时钟完全稳定、电源纹波衰减到位来说是很有必要的。3.3 多时钟域下的复位处理实际项目中一个PLL往往输出多个时钟比如100MHz给系统逻辑、200MHz给DDR接口、50MHz给低速外设。这些时钟域都需要复位但复位释放的时刻需要各自同步。module reset_sync_multi ( input wire clk_a, // 时钟域A input wire clk_b, // 时钟域B input wire ext_rst_n, input wire pll_locked, output wire rst_a_n, output wire rst_b_n ); // 先在主时钟域生成一个统一的复位请求 reg [3:0] main_cnt; reg main_rst_n; wire main_rst_assert ~ext_rst_n | ~pll_locked; always (posedge clk_a or negedge ext_rst_n) begin if (!ext_rst_n) begin main_cnt 4h0; main_rst_n 1b0; end else if (main_rst_assert) begin main_cnt 4h0; main_rst_n 1b0; end else if (main_cnt 4hF) begin main_cnt main_cnt 1b1; main_rst_n 1b0; end else begin main_rst_n 1b1; end end // 将复位同步到时钟域A reg [1:0] sync_a; always (posedge clk_a or negedge main_rst_n) begin if (!main_rst_n) sync_a 2b00; else sync_a {sync_a[0], 1b1}; end assign rst_a_n sync_a[1]; // 将复位同步到时钟域B reg [1:0] sync_b; always (posedge clk_b or negedge main_rst_n) begin if (!main_rst_n) sync_b 2b00; else sync_b {sync_b[0], 1b1}; end assign rst_b_n sync_b[1]; endmodule这个模块的思路是先在主时钟域生成一个“全局复位释放”信号main_rst_n然后将这个信号分别同步到各个时钟域。每个时钟域用两级触发器做同步输出本时钟域的复位信号。这样做的好处是每个时钟域的复位释放都对齐到了自己的时钟边沿避免了跨时钟域的亚稳态问题。实操心得多时钟域复位同步时千万不要用一个时钟域的复位直接去复位另一个时钟域的寄存器。即使两个时钟频率相同但相位不同也可能导致亚稳态。老老实实每个时钟域做一次同步多花两级触发器而已。4. 常见问题与排查技巧实录4.1 locked信号一直不拉高怎么办这是最常见的问题。PLL的locked信号迟迟不拉高系统一直处于复位状态看起来像是“死机”了。排查思路如下排查项可能原因解决方法参考时钟晶振未起振、时钟引脚约束错误用示波器测晶振输出检查引脚分配PLL配置倍频/分频参数超出VCO范围重新计算参数确保VCO频率在手册范围内电源电源电压不稳或纹波过大测量PLL供电引脚增加去耦电容复位极性PLL IP核的复位极性搞反检查IP核配置确认areset是高有效还是低有效锁定阈值锁定检测阈值设置过严调整PLL配置中的锁定窗口参数我遇到过一次很典型的情况PLL配置里参考时钟频率写错了实际晶振是50MHz配置里写的是25MHz导致VCO频率超出范围locked永远不拉高。这种问题看代码看不出来必须对照原理图和IP核配置逐项核对。4.2 locked信号有毛刺导致系统反复复位有些PLL在锁定边缘会输出毛刺locked信号在高低之间快速跳变。如果复位逻辑直接使用locked系统就会反复复位。解决方法就是前面代码里提到的滤波用移位寄存器对locked做连续采样只有连续N个周期为高才认为是真锁定。滤波窗口的大小需要权衡。窗口太小滤不掉毛刺窗口太大系统启动时间变长。我的经验是对于100MHz左右的时钟16到32个周期比较合适对于低频时钟可以适当减小窗口但至少保证8个周期。4.3 复位释放后系统仍然不稳定有时候locked已经稳定拉高复位也正常释放了但系统跑起来还是有问题。这种情况往往不是复位逻辑本身的问题而是复位释放的时机和系统其他初始化过程没有配合好。比如DDR3控制器需要在复位释放后执行一段初始化序列如果复位释放太快DDR3还没完成校准后续读写就会出错。再比如某些外设芯片需要在上电后等待一段时间才能接受配置命令如果FPGA的复位释放太早配置数据发过去外设还没准备好。这类问题的解决方法是在复位逻辑里加入一个可配置的延时计数器根据系统需求调整复位保持时间。或者设计一个分级的复位释放机制核心逻辑先释放外设接口后释放。4.4 仿真时locked信号怎么处理在仿真环境中PLL的行为模型通常会在一段时间后自动拉高locked。但有些仿真模型需要手动激励或者locked的拉高时间与真实硬件差异很大。如果仿真时发现系统一直不复位释放先检查PLL仿真模型是否正确加载再检查locked信号的初始值。我的习惯是在testbench里对locked信号做一个简单的行为模拟上电后等待100个时钟周期然后拉高。这样既能验证复位逻辑的正确性又不会因为PLL模型的问题卡住仿真。// testbench中模拟PLL locked信号 initial begin pll_locked 1b0; #1000; // 等待1us pll_locked 1b1; end注意仿真时locked的时序和真实硬件不同复位逻辑的验证重点应该放在“locked拉高后复位是否正确释放”以及“locked拉低后复位是否立即生效”这两个行为上而不是纠结于locked拉高的具体时刻。4.5 不同厂商PLL IP的locked信号差异Xilinx的MMCM/PLL、Intel的ALTPLL、高云的PLL它们的locked信号行为有一些细微差异。Xilinx的MMCM在锁定后如果参考时钟丢失locked会拉低但重新锁定需要的时间较长。Intel的ALTPLL有一个locked端口但某些模式下还有pll_locked和pll_cal_busy等额外信号需要关注。高云的PLL IP相对简单locked信号的行为也比较直接。不管用哪家的IP核心原则是一样的locked信号必须经过同步和滤波之后才能用于复位逻辑。不要因为某家IP的文档说locked可以直接用就跳过同步步骤亚稳态的风险是实实在在的。5. 几个容易踩坑的细节和我的实操建议5.1 复位信号的极性统一FPGA设计里复位有高有效和低有效两种。我的建议是整个系统统一用低有效复位rst_n因为大多数IP核和硬核的复位都是低有效统一极性可以减少转换逻辑降低出错概率。如果外部复位是高有效在顶层做一次反相即可。5.2 复位同步器的位置复位同步器应该放在靠近PLL输出的位置而不是分散在各个模块内部。最好的做法是在顶层或者一个专门的复位管理模块里集中处理然后通过复位分发网络送到各个子模块。这样做的原因是复位同步器需要用到PLL输出时钟如果放在子模块里每个子模块都要做一次同步浪费资源不说还容易出现不一致。5.3 异步复位同步释放的标准写法前面代码里用的都是“异步复位、同步释放”的经典结构。这种结构的优点是复位生效是异步的不需要时钟参与保证即使时钟停了也能复位复位释放是同步的避免了释放时刻的亚稳态。这个结构在Xilinx的XAPP523等文档里有详细说明是经过验证的可靠方案。5.4 不要忽略PLL的复位PLL本身也需要复位。很多PLL IP核有一个areset输入用来复位PLL内部逻辑。这个复位应该在外部复位有效时保持有效在外部复位释放后、PLL开始锁定之前释放。如果PLL的复位没有正确释放locked永远不会拉高。5.5 上电顺序的考虑在一些复杂系统里FPGA的供电和外部芯片的供电可能有先后顺序。如果FPGA先上电、外部芯片后上电FPGA的复位释放后可能会向未上电的外部芯片输出信号导致电流倒灌。这种情况下复位逻辑还需要考虑外部芯片的电源就绪信号等所有电源都稳定后再释放复位。6. 一个完整的复位管理模块示例把前面讲的内容整合起来给出一个可以直接在项目中使用的复位管理模块。这个模块包含了locked滤波、复位同步、多时钟域分发和可配置延时。module reset_manager #( parameter LOCKED_FILTER_LEN 16, // locked滤波窗口 parameter RST_HOLD_CYCLES 255 // 复位保持周期数 )( input wire clk, // 主时钟PLL输出 input wire ext_rst_n, // 外部复位低有效 input wire pll_locked, // PLL锁定信号 output wire sys_rst_n, // 系统复位输出低有效 output wire rst_released // 复位释放指示 ); // locked信号滤波 reg [LOCKED_FILTER_LEN-1:0] locked_filter; always (posedge clk or negedge ext_rst_n) begin if (!ext_rst_n) locked_filter {LOCKED_FILTER_LEN{1b0}}; else locked_filter {locked_filter[LOCKED_FILTER_LEN-2:0], pll_locked}; end wire locked_stable locked_filter; // 复位状态机 localparam S_RST_ASSERT 2b00; localparam S_RST_HOLD 2b01; localparam S_RST_RELEASE 2b10; reg [1:0] state, next_state; reg [15:0] hold_cnt; reg rst_n_reg; always (posedge clk or negedge ext_rst_n) begin if (!ext_rst_n) state S_RST_ASSERT; else state next_state; end always (*) begin next_state state; case (state) S_RST_ASSERT: if (locked_stable ext_rst_n) next_state S_RST_HOLD; S_RST_HOLD: if (hold_cnt RST_HOLD_CYCLES) next_state S_RST_RELEASE; S_RST_RELEASE: if (!locked_stable || !ext_rst_n) next_state S_RST_ASSERT; default: next_state S_RST_ASSERT; endcase end always (posedge clk or negedge ext_rst_n) begin if (!ext_rst_n) begin hold_cnt 16h0000; rst_n_reg 1b0; end else begin case (next_state) S_RST_ASSERT: begin hold_cnt 16h0000; rst_n_reg 1b0; end S_RST_HOLD: begin hold_cnt hold_cnt 1b1; rst_n_reg 1b0; end S_RST_RELEASE: begin hold_cnt hold_cnt; rst_n_reg 1b1; end default: begin hold_cnt 16h0000; rst_n_reg 1b0; end endcase end end assign sys_rst_n rst_n_reg; assign rst_released rst_n_reg; endmodule这个模块用了一个简单的三段式状态机来管理复位过程。S_RST_ASSERT状态下复位保持有效等locked_stable和ext_rst_n都满足后进入S_RST_HOLDS_RST_HOLD状态下继续保持复位同时计数器开始计数计满RST_HOLD_CYCLES后进入S_RST_RELEASE释放复位。如果释放后locked丢失或者外部复位再次有效状态机立即回到S_RST_ASSERT重新拉低复位。LOCKED_FILTER_LEN和RST_HOLD_CYCLES两个参数可以根据项目需求调整。对于100MHz时钟、普通数字逻辑默认值就够用。对于高速接口或者对启动时间敏感的场景可以适当减小RST_HOLD_CYCLES。7. 仿真验证要点与上板调试经验7.1 仿真时重点看什么复位逻辑的仿真不需要太复杂重点验证三个场景第一个场景是正常上电外部复位先有效然后释放PLL在若干周期后锁定观察sys_rst_n是否在locked稳定后正确释放。第二个场景是PLL失锁在系统正常运行过程中强制拉低pll_locked观察sys_rst_n是否立即拉低。第三个场景是外部复位抖动在复位释放过程中给ext_rst_n加几个毛刺观察系统是否会被误复位。这三个场景覆盖了复位逻辑的核心行为。仿真时可以用Force/Release或者直接修改testbench里的信号来模拟。7.2 上板调试时怎么观察上板之后最直接的观察方式是用ILA集成逻辑分析仪抓取pll_locked、ext_rst_n和sys_rst_n三个信号。触发条件设在sys_rst_n的上升沿观察复位释放的时刻和locked的关系。如果发现复位释放后系统行为异常可以进一步抓取关键状态机的状态寄存器和数据总线看是否有时序违例。Xilinx的Vivado和Intel的Quartus都支持时序报告复位释放后的几个周期内如果有建立时间或保持时间违例时序报告里会有明确提示。7.3 一个容易被忽视的问题复位释放时的时钟切换有些设计里系统在启动阶段用低速时钟PLL锁定后切换到高速时钟。这种时钟切换如果和复位释放配合不好会导致系统在切换瞬间跑飞。正确的做法是先在低速时钟下完成PLL配置和锁定检测锁定后再执行时钟切换切换完成后才释放系统复位。时钟切换本身也需要用专门的时钟切换电路如BUFGMUX来避免毛刺。8. 写在最后locked信号看起来简单就是一个指示PLL是否锁定的标志位但围绕它构建一套可靠的复位逻辑需要考虑的东西不少同步、滤波、多时钟域分发、复位保持时间、失锁恢复每一个环节都有坑。我在实际项目中见过太多因为复位处理不当导致的“玄学问题”——有时候上电能跑有时候上电跑不了实验室能跑现场跑不了。追根溯源往往就是locked信号没有正确处理。把复位管理单独做成一个模块用状态机来管理复位过程是我个人比较推荐的做法。代码不复杂但逻辑清晰调试的时候也容易定位问题。上面给出的reset_manager模块可以直接拿去用参数根据项目调整就行。最后分享一个小技巧在复位管理模块里加一个复位原因寄存器记录最近一次复位是因为外部复位、PLL失锁还是上电复位。这个寄存器在调试阶段非常有用能帮你快速判断系统重启的原因。等产品稳定了这个寄存器可以保留也可以去掉成本很低但价值很高。