全数字锁相环ADPLL的Verilog实现与Modelsim仿真全流程解析
简介一套基于Verilog实现的全数字锁相环完整工程包面向毕业设计、课程设计与项目开发主要解决从输入低频时钟到输出高频时钟的相位锁定、倍频及50%占空比问题。设计用Verilog代码完成第一阶段实现数字锁相环锁定第二阶段完成倍频工程基于QuartusII搭建借助ModelSim做行为仿真通过波形验证功能正确性。压缩包共194个文件、仅3.64MB以HDL源码与工程设计文件.v/.vhd/.tdf、Quartus工程配置.qpf/.qsf/.sof、ModelSim仿真脚本与波形.do/.wlf、说明文档.md/.txt/readme为主并含工程中间文件与目录结构便于逐模块检索。源码经过严格测试含有鉴相器、分频器、随机游走滤波器等子模块可直接在QuartusII与ModelSim中复现锁定和倍频效果结合源码解析、项目文档与仿真实验可快速理解锁相环实现思路并进行二次扩展。目前已有203人学习/浏览适合FPGA初学者、电类本科生及有项目开发需求的工程师参考。1. 全数字锁相环为什么要用Verilog自己写一个ADPLL全数字锁相环ADPLL并不是模拟锁相环的简单Verilog复刻。在FPGA和数字IC项目里当你需要从带抖动的串行数据中恢复出干净时钟、在无外部压控振荡器的条件下合成精确频率、或者做突发模式下的频率跟踪时模拟PLL往往没有可用的片上参考这时候一套纯Verilog实现的ADPLL就成了唯一现实方案。它不依赖任何模拟器件用触发器、加法器和比较器构造出完整环路行为级仿真与FPGA实测行为高度一致。本文面向正在做课程设计、毕业设计以及准备把锁相环代码用于实际工程的开发者从环路原理推导到可综合源码再到Modelsim仿真实验和项目文档组织提供一套拿起来就能用的完整路径。2. 全数字锁相环的工作原理与Verilog模块划分2.1 ADPLL的环路结构为什么是PFD、DLF、DCO三段式模拟锁相环由鉴相器、环路滤波器、压控振荡器和分频器组成。全数字锁相环的对应关系如下模拟PLL全数字ADPLLVerilog实现载体鉴相器数字鉴频鉴相器PFD寄存器 组合逻辑环路滤波器数字环路滤波器DLF加法器 移位寄存器压控振荡器数控振荡器DCO累加器 比较器分频器数字分频器计数器环路的核心矛盾在于DCO的输出频率必须能随时根据相位差做调校而数字信号的频率调节是离散的。ADPLL最常见的做法是用一个高分辨率相位累加器把控制字映射到输出翻转时刻。控制字每变化1输出周期变化一个单位时间间隔这个单位时间决定了锁定精度也是整个环路相位噪声的底限。值得注意的是很多人把ADPLL等同于DDS加一个比较器实际上DCO只是其中的执行单元真正决定环路行为的是PFD的鉴相方式和DLF的系数设定。2.2 顶层模块接口定义与时序规划我一般会把ADPLL顶层设计成以下几个端口module adpll #( parameter DW 20, // 相位累加器位宽决定频率分辨率 parameter FDW 10, // 分频器位宽 parameter REF_DIV 100 // 参考时钟分频比 )( input wire clk, // 系统主时钟例如50MHz input wire rst_n, // 异步复位低有效 input wire ref_clk, // 参考时钟 output wire dco_out, // 数控振荡器输出 output wire lock_ind // 锁定指示 );端口设计有三个注意点。第一ref_clk不一定是标准方波在时钟数据恢复场景里它可能是恢复出的边沿脉冲因此PFD模块内部需要做同步处理避免亚稳态。第二DW位宽直接影响频率分辨率和锁定时间位宽越大频率越精细但环路响应越慢这个矛盾到仿真阶段会直观体现。第三lock_ind是毕设里的加分项它不能简单地把相位误差归零就拉高需要滞回逻辑否则锁定指示会在临界点来回抖动。2.3 环路带宽与稳定性数字滤波器的系数关系数字环路滤波器的系数选择是ADPLL设计中区分能跑和跑得好的分水岭。PFD输出的相位差是一个有符号数DLF的作用不是简单累加而是对相位差做比例-积分运算这直接对应模拟环路的带宽和阻尼系数。离散域传递函数可以写成H(z) KP KI/(1 - z^-1)对应Verilog实现就是两个支路比例支路把相位差右移KP_SHIFT位积分支路先累加再右移KI_SHIFT位最后相加。右移位数越大对应增益越小环路带宽越窄对高频抖动的抑制越好但捕获时间会变长。这里有一个工程口诀先通过右移减小增益让环路稳定再逐步减小右移位数直到锁定时间满足需求。3. 核心模块的Verilog源码实现与逐段解析3.1 数字鉴频鉴相器PFD的可综合实现PFD的核心逻辑是比较参考时钟和反馈时钟的边沿先后。边沿先到者把对应输出拉高两个输出都高时复位。用Verilog实现时常见做法是状态机加计数器组合module pfd #( parameter W 16 )( input wire clk, input wire rst_n, input wire ref_edge, // 参考时钟边沿脉冲 input wire fb_edge, // 反馈时钟边沿脉冲 output wire signed [W-1:0] phase_err ); reg signed [W-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; end else if (ref_edge !fb_edge) begin cnt cnt 1; // 参考领先相位差增大 end else if (fb_edge !ref_edge) begin cnt cnt - 1; // 反馈领先相位差减小 end // ref_edge 与 fb_edge 同时到达时保持当前值 end assign phase_err cnt; endmodule这段代码把相位差累积到一个带符号计数器里原理上与经典三态PFD等价但更适合Verilog综合。关键点在于当参考边沿和反馈边沿同拍到达时计数器保持不变这实现了死区消除避免输出在锁定点附近产生毛刺。ref_edge和fb_edge在顶层由边沿检测模块产生边沿检测用两级寄存器同步后取异或这是所有跨时钟域信号的标准处理方式。计数器位宽W的选择需要与后级DLF匹配。如果W是16位而DLF的移位参数设置过大相位误差的信息会被右移运算丢弃导致环路无法收敛。我通常把W设为与DLF输入位宽一致这样整个误差传递路径上没有信息损失。3.2 数字环路滤波器DLF比例积分参数怎么设DLF的Verilog实现核心运算很少但参数设错会导致环路振荡。最常见的错误是比例支路和积分支路的位宽没有对齐导致累加器溢出。我会将PFD输出的相位差扩展位宽后再送入DLF给积分累加器留足余量module dlf #( parameter DW 20, parameter SHIFT_P 4, // 比例增益右移4位相当于×1/16 parameter SHIFT_I 8 // 积分增益右移8位相当于×1/256 )( input wire clk, input wire rst_n, input wire signed [DW7:0] phase_err, output reg signed [DW7:0] ctrl_word ); reg signed [DW15:0] acc; always (posedge clk or negedge rst_n) begin if (!rst_n) begin acc 0; ctrl_word 0; end else begin acc acc phase_err; // 积分支路 ctrl_word (phase_err SHIFT_P) (acc SHIFT_I); // 比例积分 end end endmodule三个参数调整的实验结论值得记下来。SHIFT_P决定锁定速度调小则增益大、锁定快但容易过冲SHIFT_I决定稳态精度调大则增益小、输出稳但锁定时间成倍增长。两者的右移运算本质上是除以2的幂次避免了乘法器资源消耗这是在FPGA上实现数字滤波器的常见做法。有一个容易被忽略的细节acc的位宽必须比phase_err多至少8位因为积分项是持续累加的位数不够时累加器会周期性溢出表现为ctrl_word突然跳变环路在锁定后周期性失锁。如果在仿真波形里看到控制字打顶先查位宽而不是调系数。值得一提的优化方向是把滑动窗口滤波Verilog实现放在DLF前端做去毛刺。原理是取最近N个相位差的平均值再进入比例积分控制器好处是锁定后ctrl_word抖动显著减小代价是环路增加N拍延迟。对抖动敏感而对锁定时间不敏感的场景比如时钟恢复值得引入。3.3 DCO数控振荡器与数字分频器的Verilog实现DCO在ADPLL里负责把控制字变成输出频率。最实用的实现是相位累加器结构也就是常说的直接数字频率合成方式module dco #( parameter DW 20 )( input wire clk, input wire rst_n, input wire signed [DW7:0] ctrl_word, output reg dco_out ); reg [DW-1:0] phase_acc; wire [DW-1:0] delta ctrl_word[DW-1:0]; // 取低20位作为频率控制增量 always (posedge clk or negedge rst_n) begin if (!rst_n) begin phase_acc 0; dco_out 0; end else begin phase_acc phase_acc delta; dco_out phase_acc[DW-1]; // 最高位翻转作为输出 end end endmodule这个结构的物理含义是每个时钟周期相位累加器增加delta累加器最高位翻转频率约等于delta × fclk / 2^DW。输出是占空比接近50%的方波如果项目要求严格50%占空比后端接一个二分频器即可。需要注意ctrl_word是有符号数而相位累加器是无符号数二者位宽不同。截断处理的原则是控制字应为正数可以通过在DLF输出端叠加一个固定偏置实现。这样设计的好处是DCO可以在中心频率上下对称调节频率捕捉范围不再是一边倒。反馈分频器用计数器实现关键是分频系数要覆盖设计需求范围module feedback_div #( parameter FDW 10 )( input wire clk, // DCO输出 input wire rst_n, input wire [FDW-1:0] div_ratio, output reg fb_edge_pulse ); reg [FDW-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; fb_edge_pulse 0; end else if (cnt div_ratio - 1) begin cnt 0; fb_edge_pulse 1; // 输出单周期脉冲作为反馈边沿 end else begin cnt cnt 1; fb_edge_pulse 0; end end endmodule这里输出的是单周期脉冲而不是占空比50%的方波因为PFD只需要边沿信息脉冲反馈比方波反馈更直接也避免了方波上升沿检测的额外逻辑。如果后续要用分频后的方波驱动其他模块可以在这个脉冲输出后面再接一个T触发器。4. 仿真实验用Modelsim把全数字锁相环完整跑通4.1 Testbench设计与激励构造的关键代码仿真阶段不是把顶层模块连上就结束难点在于制造一个带频率偏差的参考时钟。如果参考频率固定不变环路表现得再好也无法验证捕获能力。我常用的做法是生成频率可编程的参考时钟仿真过程中逐步改变周期timescale 1ns/1ps module tb_adpll; reg sys_clk 0; reg rst_n 0; reg ref_clk 0; wire dco_out; wire lock_ind; real ref_period 20.0; // 初始50MHz参考 integer i; always #10 sys_clk ~sys_clk; // 50MHz系统时钟 initial begin #100 rst_n 1; // 参考频率从50MHz渐变到52MHz模拟输入频偏 for (i 0; i 200; i i 1) begin #(ref_period/2) ref_clk 1; #(ref_period/2) ref_clk 0; ref_period ref_period - 0.01; // 每周期周期减0.01ns end // 频率阶跃跳变验证重捕获能力 ref_period 18.0; repeat (500) begin #(ref_period/2) ref_clk 1; #(ref_period/2) ref_clk 0; end end adpll_top #( .DW(20), .FDW(10) ) u_adpll ( .clk(sys_clk), .rst_n(rst_n), .ref_clk(ref_clk), .dco_out(dco_out), .lock_ind(lock_ind) ); endmodule这段激励的重要设计在于把仿真分成两个阶段第一阶段参考频率连续渐变考验环路的跟踪能力第二阶段频率阶跃到18ns周期约55.6MHz比初始50MHz高出11%这能逼出PFD和DLF的极限响应。如果环路在这种跳变下还能重新锁定说明设计余量充足。4.2 Modelsim仿真脚本与波形验证的完整流程用命令行方式跑仿真比图形界面更可复现也方便写进项目文档。我一般准备一个run.do脚本vlib work vlog -work work adpll_top.v pfd.v dlf.v dco.v feedback_div.v tb_adpll.v vsim -novopt work.tb_adpll add wave -hex /tb_adpll/sys_clk add wave -hex /tb_adpll/ref_clk add wave -hex /tb_adpll/dco_out add wave -hex /tb_adpll/u_adpll/u_dlf/ctrl_word add wave -hex /tb_adpll/u_adpll/u_pfd/phase_err add wave -hex /tb_adpll/u_adpll/u_dlf/acc run 2ms先vlib work建库再vlog编译全部源文件。这里使用-novopt虽然在新版本Modelsim中已默认关闭优化但在旧版本里不关会造成内部信号被优化掉波形窗口看不到acc和ctrl_word调参时无从下手。add wave要把内部寄存器信号一并加入环路滤波器累加器是观察锁定过程最重要的信号。跑完仿真后判断是否锁定的三个波形依据dco_out与ref_clk的上升沿基本对齐phase_err在一个恒定值附近做正负1抖动ctrl_word收敛到某一稳定值且不再单调变化。如果phase_err一直在同方向单边累积说明DCO频率覆盖范围没有包含参考频率要么增大DCO位宽要么调整控制字初始偏置。4.3 锁定时间与输出抖动的参数调整实验针对锁定太慢和锁定但抖动大两种典型问题参数调整实验可以按下面表格进行症状调整项调法预期结果锁定时间超过200μsSHIFT_P从4改到2环路增益变大锁定时间缩短约一半锁定后phase_err抖动±3SHIFT_I从8改到10积分作用减弱抖动降至±1dco_out出现周期性抖动DW从16位改到20位频率分辨率提高输出抖动明显改善输入频率超出锁定范围div_ratio确认分频比匹配输出能覆盖输入频率这几个实验的关联性很强调整SHIFT_P改善锁定时间后通常需要紧接着检查抖动是否超标而抖动超标时优先调SHIFT_I而不是继续调比例支路。背后的原因是比例支路控制瞬时响应积分支路控制稳态误差。在毕设答辩时把这个实验矩阵讲清楚比单纯背诵环路理论更有说服力。Modelsim仿真文件本身也是项目文档的一部分脚本加上波形截图构成完整的可回归验证环境。5. 项目文档组织与源码解析的高级方法5.1 按系统集成类项目过程文档标准组织毕设文档系统集成类项目过程文档主要包括需求分析、设计说明、测试报告、使用说明四个层面。应用到ADPLL项目上详细设计文档不要按文件逐个讲而是按数据流组织从ref_clk边沿进入PFD开始到控制字生成再到DCO输出每个模块在路径上解决什么问题。读者跟着信号走一遍比按文件列表读更直观。5.2 源码解析文档中的三个关键波形源码解析文档里PFD的鉴相过程、DLF控制字收敛过程、锁定后的稳态波形这三个图必须画出来且要标注关键时间点。时序图可以用文本方式描述边沿关系注明参考时钟与反馈时钟的对齐过程。很多同学把PFD的参考领先和反馈领先画反答辩时被老师一眼看穿。画图的要点是注意边沿先后关系不需要精确到每个时钟周期。5.3 答辩演示中必须现场跑通的三个验证动作答辩或评审演示时不要只放录好的波形截图而是准备三个能现场复现的实验。第一个是锁定过程实时演示把lock_ind拉高时刻标注出来第二个是输入频率阶跃跳变后重新锁定的过渡过程第三个是调整SHIFT_I后观察抖动改善。这三个实验覆盖了锁相环最核心的指标捕获时间、跟踪能力和稳态精度。现场运行时先把run.do里的仿真时间设得足够长比如run 2ms并提前在报告里剪好波形区间避免现场等待仿真结束的尴尬。这套文档和验证习惯可以直接迁移到工程项目中本质上就是一个可回归的验证环境加完整的变更记录。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →