FPGA实战:全数字锁相环DPLL的Verilog实现与参数调优
2026/9/17 3:07:30 网站建设 项目流程

简介:全数字锁相环(DP-DPLL)设计工程包,面向通信工程、FPGA开发及数字信号处理学习者,完整呈现10MHz~100MHz输出范围的数字锁相环实现方案。工程涵盖数字鉴相器、数字低通滤波器、数字压控振荡器及分频器核心模块,详尽描述了参考信号与内部振荡的相位比较、误差平滑与频率调节过程,并给出环路锁定条件与参数优化思路,可帮助读者理解DP-DPLL从原理到落地的完整链路。压缩包共104个文件,以tdf逻辑设计、bdf原理图、vhd硬件描述、bsf符号定义及rpt综合报告等为主,辅助qsf工程配置与vwf仿真激励文件,便于在Quartus环境中直接打开工程进行学习与验证。包体仅427KB,结构精简。目前已有162人学习浏览,适合有一定数字电路基础、希望掌握全数字锁相环工程实现细节的开发者参考。

1. 全数字锁相环 DPLL:为什么说它是数字时代的频率同步基石

打开一个标着dpll.zip的工程包时,你可能以为里面只是某个 FPGA 例程。但真正值得关心的是DPDPLL这两个缩写背后的东西:一个完全用数字电路实现的锁相环。它不接模拟 VCO,不用环路滤波器上的电阻电容,所有鉴相、滤波、压控振荡器的职能全部转换成加法器、乘法器和查找表。对通信接收机、时钟恢复、电力电子并网这类场景,全数字锁相环意味着环路参数可以软件配置、温度漂移更小、在 ASIC 里可以直接综合。这篇博文会把 DPLL 从工作原理、Verilog 实现到 FPGA 调参技巧完整梳理一遍,全程围绕可复现的代码和参数表格,新手能照着搭,老手可以对比边界条件。

2. 从模拟 PLL 到全数字 DPLL:四个值得记住的环路差异

2.1 模拟 PLL 的架构与局限

传统模拟锁相环由鉴相器(PD)、环路滤波器(LF)和压控振荡器(VCO)构成。鉴相器比较输入信号与 VCO 反馈信号的相位差,输出一个与相位差成正比的电压,经过环路滤波器滤除高频分量后,控制 VCO 的振荡频率。这是一个典型的负反馈闭环,最终让输出频率与输入频率一致,相位差保持恒定。

模拟 PLL 的局限主要在环路滤波器:电阻电容的值一旦焊上去就固定在板上,调试时需要改换器件;电容容值随温度变化会影响环路带宽;VCO 需要外接电感或晶体,在数字芯片内部难以直接集成。数字接收机里如果使用模拟 PLL,还需要额外的高速 ADC 和 DAC,把模拟的相位误差信号数字化,大大增加面积与功耗。

2.2 DPLL 的数字域拆解:NCO、鉴相器、环路滤波器

全数字锁相环把所有环节搬到数字域。常见做法是用数控振荡器(NCO)替代 VCO,NCO 本质上是一个相位累加器加一个查找表,每来一个时钟,累加器加上频率控制字(FCW),高几位查正弦表输出数字波形。鉴相器不再是模拟乘法器,而是对输入采样序列做乘法或坐标旋转计算(CORDIC),得到相位差。

环路滤波器变成数字滤波器,典型是一阶比例积分(PI)结构,输出经过限幅后作为 NCO 的频率修正量。整个环路可以形式化表示为:相位误差e[n]→ 滤波器输出y[n]→ 更新频率控制字fcw[n]→ 下一拍相位累加。相比模拟 PLL,DPLL 最大的优势是可以随时通过寄存器改写环路带宽、阻尼因子和中心频率,不需要改硬件。

2.3 DPLL 设计中的 4 个关键参数与选型表

设计 DPLL 时,下面四个参数决定环路的收敛速度和稳定性,需要先定下来再写 RTL。

参数典型值影响数字域实现
环路带宽 ( f_n )信号带宽的 1/10 ~ 1/20带宽越窄滤噪越强,锁定越慢决定 PI 增益系数
阻尼因子 ( \zeta )0.707 ~ 1.0决定超调量和相位裕度与 ( K_p )、( K_i ) 比值相关
NCO 位宽 ( N )32 或 48频率分辨率 ( f_{clk}/2^N )相位累加器位宽
采样率 ( f_s )输入信号频率的 4~8 倍过高浪费资源,过低混叠确定环路滤波器时钟频率

提示:环路带宽并非越高越好。如果输入信号带外噪声大,过高的带宽会把噪声引入 NCO 控制字,表现为输出时钟抖动增大。一般从信号标称频率的 1/20 开始试,逐步往上调。

3. 用 Verilog 写一个最小 DPLL 模块及仿真要点

3.1 DPLL 顶层模块与端口定义

下面是一个面向 FPGA 的最小 DPLL 模块,省略了复位同步和时钟管理,聚焦于环路核心。代码风格是常见的一进程状态机加纯组合逻辑,便于移植到不同厂家的器件上。

module dpll_core #( parameter integer N = 32, // NCO 相位累加器位宽 parameter integer M = 12, // 查找表地址位宽 parameter signed [15:0] KP = 16'd100, // 比例增益 parameter signed [15:0] KI = 16'd5 // 积分增益 )( input wire clk, // 工作时钟(采样时钟) input wire rst_n, // 异步复位,低有效 input wire signed [15:0] din, // 输入信号样本,定点数 output wire signed [15:0] dout, // NCO 输出正弦波 output wire [N-1:0] phase_out, // 当前相位累加值 output wire signed [31:0] fcw_out // 频率控制字(含环路修正) ); // 相位累加器 reg [N-1:0] phase_acc; wire [N-1:0] phase_next = phase_acc + fcw_reg; // 频率控制字寄存器(中心频率 + 环路滤波输出) reg signed [31:0] fcw_reg; // 相位误差输出经 PI 环路滤波器 wire signed [31:0] phase_error; reg signed [31:0] pi_integ; // NCO 正弦查找表实例 wire [M-1:0] addr = phase_acc[N-1 -: M]; reg signed [15:0] sin_table [0:(1<<M)-1]; // 实际工程中由脚本生成或使用 ROM IP // 鉴相器:用输入与 NCO 输出的乘积做相位检测 // 这里简化为乘法器输出,实际还需低通滤波 assign phase_error = din * dout; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin phase_acc <= {N{1'b0}}; fcw_reg <= 32'd286331153; // 约 42MHz@100MHz,可调 pi_integ <= 32'd0; end else begin // 环路滤波:比例 + 积分 pi_integ <= pi_integ + $signed(KI * phase_error); fcw_reg <= fcw_reg + $signed(KP * phase_error) + pi_integ; // 更新相位 phase_acc <= phase_next; end end // 查表输出正弦,幅值 16 位 assign dout = sin_table[addr]; assign phase_out = phase_acc; assign fcw_out = fcw_reg; endmodule

这段代码把三个核心模块放进同一个 always 块:相位累加、环路滤波、NCO 查表。逻辑说明如下:

  • phase_error是输入样本与 NCO 输出的乘积。当两者频率接近时,乘积包含一个直流分量和一个两倍频分量,环路滤波器负责把二倍频分量滤掉,只保留直流分量作为相位差指示。
  • pi_integ是积分项,用来消除稳态相位误差。只有比例项时,环路会存在固定相差,积分项可以把它压到接近零。
  • fcw_reg是频率控制字,默认值对应中心频率,环路修正量直接叠加在上面。注意这里用了有符号乘法和加法,K参数必须是 signed 类型,否则仿真会出现符号错误。

3.2 数字鉴相器与环路滤波器实现

上面代码中的鉴相器是乘法器方案,适合正弦输入。如果输入不是正弦波,比如方波或 BPSK 调制信号,单纯乘法会出现大量谐波干扰。常见的替代做法是 CORDIC 鉴相器,输入两个正交信号得到反正切值。这里为了演示最小实现,先用乘法器,并在其后补一个平均值滤波器。

环路滤波器的参数KPKI在 RTL 里是常量,实际调优时会映射成寄存器。如果想让环路带宽可配置,可以把KPKI定义成输入端口,每次更新数值前需要做毛刺消除或同步处理,否则动态调参会引起环路跳变。经验做法是:先用固定值完成锁定,再在锁定指示信号 valid 拉起后缓慢修改KI,例如每隔 1024 个时钟加 1,避免突变。

3.3 用 Python 脚本生成 NCO 查找表并验证初始相位

NCO 查找表通常由脚本生成,而不是手写 4096 个数据。下面是一段生成.mem文件的 Python 脚本,同时输出相位累加器的初始值验证结果。

import math import numpy as np M = 12 depth = 1 << M scale = 32767 # 16 位有符号最大值 with open("sin_table.mem", "w") as f: for i in range(depth): val = int(round(scale * math.sin(2 * math.pi * i / depth))) f.write(f"{val:04x}\n") # 验证:当 fcw 对应的频率为 42MHz,采样率 100MHz 时 fs = 100e6 f_target = 42e6 N = 32 fcw = int(round(f_target / fs * (1 << N))) phase = 0 # 累加 1000 个周期,观察相位是否均匀增加 phases = [] for _ in range(1000): phase = (phase + fcw) & 0xffffffff phases.append(phase) print(f"FCW = {fcw}, 相位增量 = {(phases[1] - phases[0]) & 0xffffffff}") print(f"预期每个时钟相位增量 = {fcw}")

参数说明:

  • M=12表示查找表地址为 12 位,输出 4096 个点,对应相位量化误差约 ( 2\pi/4096 ),折合信噪比约 72 dB,大多数数字通信设计足够。
  • scale=32767表示输出正弦波满幅为 16 位有符号数,避免溢出。
  • FCW 计算方法f_target / fs * 2^N是 DPLL 频率合成的核心公式。频率分辨率等于 ( f_{clk}/2^{32} ),在 100 MHz 采样率下约为 0.023 Hz,足够精细。

4. 在 FPGA 上把 DPLL 跑起来:时钟域处理与参数调优

4.1 跨时钟域采样与亚稳态处理

在真实系统中,输入信号往往来自另一个时钟域,例如高速 ADC 的数据输出。直接在clk域采样外部数据,一旦外部数据在时钟上升沿附近变化,触发器就会进入亚稳态。DPLL 的鉴相器对亚稳态异常敏感,因为相位误差的微小错误会被积分项持续累积,最终表现为输出频率周期性漂移。

最常见的处理是打两拍同步。但如果输入数据是多位并行总线,只打两拍不够,因为每位信号的延迟可能不同,导致采样到错误组合。更稳妥的方案是使用异步 FIFO 或 DPRAM 做跨时钟域缓冲。对于 DPLL 应用,我一般会先用一个简单的两级寄存器链同步高位的有效标志,数据本身使用格雷码传递,或者把数据送入块 RAM 后再读出。这样付出的代价是多一个时钟周期延迟,对环路影响很小。

4.2 环路带宽与阻尼因子的调参实验

环路带宽和阻尼因子的数字实现之间存在直接映射。连续时间域中,PI 控制器的传递函数为 ( K_p + K_i/s ),离散化后得到:

[ y[n] = y[n-1] + K_p(e[n] - e[n-1]) + K_i e[n] ]

但在上面的 Verilog 代码里,我们使用了位置式 PI 的近似形式:fcw_new = fcw_old + KP*phase_error + pi_integ,其中pi_integ是误差的累积和。这种形式的KPKI对环路稳定性的影响可以通过表格直观理解。

KP 增大KI 增大
快速拉入锁定,超调增加消除稳态误差,但可能引起低频振荡
对输入噪声敏感相位噪声性能变好,但锁定时间变长
可能造成环路不稳定积分饱和时需要限幅处理

实际调参步骤可以这样走:

  1. KI设为 0,只保留比例项,从小到大增加KP直到环路能锁定且超调可接受。
  2. 锁定后观察phase_out与输入相位之间的差值,逐步增加KI让差值减小。
  3. 如果出现持续振荡,说明KI过大或是环路滤波器引入了过大的相位延迟,降低KI并检查采样率是否足够高。

注意:在 FPGA 中,PI 运算的结果一定要做饱和截断。如果fcw_reg是无符号数,而环路修正量为负,会在累加器中产生巨大跳变。应当把fcw_reg声明为有符号数,并加限幅逻辑将最终值限制在合法频率范围内。

4.3 锁定的判定指标:频率误差、相位误差与锁定时间

判断 DPLL 是否可靠锁定,不能只看波形。工程上至少需要三个指标:频率误差、剩余相位误差和锁定时间。频率误差可以通过统计fcw_out在锁定状态下的方差得到,方差越小说明环路对噪声的抑制越好。剩余相位误差则反映鉴相器的直流分量大小,理论锁定到零,但受量化噪声影响会在几个 LSB 内抖动。

锁定时间的测量方法是在仿真环境中给输入信号一个频率阶跃,观察fcw_out从初始值上升到目标值 90% 所需的时间。下面给出一个在 Verilog 测试平台中测量锁定时间的简化过程:用fcw_ref寄存期望值,当fcw_outfcw_ref的差值连续 1024 个时钟周期小于阈值时,拉高locked信号,并记录当前仿真时间。

// 测试平台片段:锁定检测逻辑 reg signed [31:0] fcw_ref = 32'd300000000; parameter THRESHOLD = 32'd1000; integer sample_cnt = 0; reg locked = 0; always @(posedge clk) begin if (!rst_n) begin sample_cnt <= 0; locked <= 0; end else begin if (fcw_out inside {[(fcw_ref - THRESHOLD) : (fcw_ref + THRESHOLD)]}) begin if (sample_cnt >= 1023) begin locked <= 1; end else begin sample_cnt <= sample_cnt + 1; end end else begin sample_cnt <= 0; locked <= 0; end end end

这个检测逻辑用了一个连续条件判断,实际设计中由于fcw_out是有符号数,需要做绝对值比较。更工程化的方法是把差值取绝对值后与阈值比较,避免区间运算带来的综合问题。locked信号可以用于后级电路的时钟门控或数据使能。

5. 进阶技巧:用 DPLL 恢复时钟的三种验证方法

这里给出三个不依赖厂家专用验证工具的验证方法,分别对应仿真、板卡实测和自动化回归,能覆盖大多数 DPLL 开发场景。

仿真级验证:注入频偏与相噪

在 Testbench 中用$signed控制输入信号的频率,不直接生成理想正弦波,而是每隔一定周期修改输入数据的相位步长。例如模拟输入频率从 42 MHz 跳变到 42.1 MHz,观察phase_out是否跟随。要加入相噪时,可以使用$dist_normal给输入样本叠加高斯噪声,但需要先确定环路带宽能否滤除该噪声。仿真时重点看fcw_out是否冻结在一个稳定值附近,而不是发散。

板卡实测:示波器观测 NCO 输出与输入信号的关系

dout引到 FPGA 的 DAC 引脚,输入信号由信号源给出。示波器用外部触发模式,把触发源设为输入信号的上升沿,观察dout的波形是否稳定。如果波形左右晃动,说明环路没有锁住,优先检查采样时钟是否满足奈奎斯特条件,以及KP是否过小。锁定状态下可以测量输出波形与输入之间的相位差,该值应在几个毫弧度内。

自动化回归:脚本批量扫描参数组合

由于 DPLL 的锁定行为高度依赖KPKI和初始频率误差,人工逐个调参效率很低。把 Verilog 测试平台编译成 VPI 接口,用 Python 脚本通过共享库控制参数值并读取lock_status,然后批量运行不同参数组合。每次运行记录锁定时间、超调量和稳态误差,最后用表格汇总。这样可以快速找到满足设计条范围的参数区间,再针对边界点做细化仿真。这个方法在需要兼容不同采样率的多模接收机中特别有用。

最后一个技巧是:如果 DPLL 在板卡上出现时好时坏的锁定状态,先不要怀疑算法,大概率是复位释放时机不对。确保复位信号在所有时钟稳定后至少延迟 10 个周期再释放,并且释放时fcw_reg已加载初值,否则 NCO 会从随机相位开始追赶,导致锁定时间大幅延长。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询