ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA数字锁相环DPLL实现:从Verilog代码到Vivado仿真验证

FPGA数字锁相环DPLL实现:从Verilog代码到Vivado仿真验证 数字锁相环DPLL在FPGA里算不上什么高深项目但它的地位很特殊通信系统里的时钟恢复、电机驱动里的转速同步、并网装置里的相位跟踪到处都有它的身影。我当年刚接触FPGA时以为锁相环就是调一个PLL IP核点一下就能用直到开始自己写代码才发现真正要理解的是环路本身怎么收敛、带宽怎么折中、仿真怎么验证。这篇文章我用一个最经典的DPLL架构——鉴相器加环路滤波器加数控振荡器从原理到Verilog实现再到Vivado仿真验证完完整整走一遍最后给出可直接运行的仿真代码。适合想把锁相吃透而不只是调用IP核的FPGA开发也适合通信、电力电子方向的在校生。1. 数字锁相环的整体设计思路1.1 数字锁相环到底在锁什么锁相环本质是一个相位自动跟踪系统你可以把它想成两个人在打节拍一个人负责给基准节拍另一个人必须跟着这个节拍走一旦一方节奏乱了另一方要能自动调整回去。电子系统里没有“耳朵”于是就用鉴相器来比较两路信号的相位差用环路滤波器来处理这个误差再用振荡器去修正输出频率和相位形成一个闭环反馈系统。数字锁相环和模拟锁相环最大的区别在于模拟PLL用RC充放电、压控振荡器VCO来实现元器件值会随温度、电压漂移数字锁相环的信号通路完全是寄存器、加法器、乘法器不涉及电容电阻的模拟充放电。换句话说同样一个环路参数在FPGA里今天跑和明年跑结果完全一致这是数字实现的确定性优势。从应用角度数字锁相环主要干三类事。第一类是频率合成在某个基准频率的基础上生成另一个频率要求跟基准有严格的整数倍关系第二类是时钟恢复从接收到的串行数据流里“提取”出时钟信号用DSP做解调时经常碰到第三类是相位同步比如逆变器并网时要让本地正弦波与电网电压同频同相或者伺服系统里让电机转速精确跟踪指令值。不管哪种场景锁相环的核心都是一个反馈控制问题只不过被控对象从模拟电压换成了数字相位累加值。1.2 为什么在FPGA里做而不是用DSP或现成PLL有人可能会问DSP里也能用软件写锁相环为什么偏要在FPGA里用逻辑搭我的看法是这两条路不是替代关系而是不同场景下的选择。DSP跑锁相环优点是灵活改个环路系数就是改个变量缺点是串行执行采样率一旦高起来CPU时间就被锁相环吃掉了尤其当系统里还要同时做滤波、解调、协议解析时实时性很容易成为瓶颈。FPGA则是把所有处理摊成并行流水线鉴相器、滤波器、NCO每个周期都在工作数据通路的延迟几乎恒定很适合高采样率的实时控制。也有人说Xilinx不是有PLL和MMCM硬核吗直接用不就行了。确实如果你只需要把一个时钟从50MHz倍频到200MHz那么直接用Vivado里的Clocking Wizard生成PLL IP核就行了功耗低、抖动小、几乎不用调。但硬核PLL的问题在于它只能生成跟输入频率成固定倍数关系的时钟没法动态跟踪一个未知频率的信号。数字锁相环则是自己用逻辑搭出来的环路输入信号可以是一个ADC采回来的正弦波、方波、甚至带噪声的基带信号锁相环输出的是一个与输入同步的数控振荡器信号。简单说硬核PLL是给时钟用的数字锁相环是给信号用的两者定位完全不同。如果你只是想快速交一个Demo当然可以去找别人写好的DPLL模块改一改端口就用。但如果想真正掌握环路结构、能够在系统异常时定位问题、能够针对不同业务场景调整捕获带宽和锁定速度那么从鉴相器、环路滤波器到NCO每一级都亲手写一遍这笔时间花得很值。1.3 系统的整体架构与模块划分我这次实现的是经典的一阶锁相环结构整体信号链路如下参考正弦信号和NCO输出的余弦信号同时送入鉴相器鉴相器用乘法器把两路信号相乘输出一个包含相位差信息的高频分量叠加低频分量的误差值误差值进入环路滤波器滤掉高频分量留下平滑的相位差信号滤波器输出作为NCO的频率控制字NCO根据这个控制字不断地调整本地正弦和余弦输出的频率与相位最终实现与参考信号同步。这里面最核心的反馈思想是如果NCO的频率低于参考信号那么鉴相器输出就会有一个正的直流分量经过环路滤波后抬高频率控制字让NCO加快反之如果NCO频率高于参考信号误差信号为负频率控制字被压低NCO减慢。经过若干次调整环路稳定后本地信号和参考信号之间的相位差趋近于一个固定值频率完全对齐这就是“锁定”。模块划分上我把它分成四个Verilog文件分别是NCO模块、鉴相器模块、环路滤波器模块和顶层连接模块。这样划分的好处是每个模块都可以独立仿真、独立测试排查问题时不用对整个工程翻来翻去。2. 核心模块设计与参数计算2.1 鉴相器乘法器为什么是最简单方案鉴相器的任务是比较两路信号的相位差。常见的实现方式有异或门鉴相器、边沿触发器鉴相器和乘法器鉴相器。异或门鉴相器适合方波信号两路50%占空比方波经过异或后输出脉冲宽度正比于相位差但它要求两个信号都是方波不能直接处理正弦信号。乘法器鉴相器则不同它天然适合正弦波而且实现起来极其简单就是一个有符号数乘法器。乘法器鉴相的原理来自积化和差公式。假设参考信号是S A*sin(wt θ1)NCO输出是C B*cos(wt θ2)两者相乘得到S × C (A*B/2) * [sin(θ1 - θ2) sin(2wt θ1 θ2)]其中一项是相位差的正弦频率很低另一项是两倍频分量频率是参考信号的两倍。只要用环路滤波器把二倍频分量滤掉剩下就是一个正比于相位差正弦的误差信号。当相位差不大时sin(θ1 - θ2)近似等于θ1 - θ2这就是一个天然的线性鉴相特性。用Verilog实现时要注意端口都定义成有符号数。在FPGA里乘法器一般直接调用*运算符综合工具能映射到DSP48硬核不额外消耗太多逻辑资源。代码如下module phase_detector #( parameter DATA_WIDTH 16 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] ref_signal, input wire signed [DATA_WIDTH-1:0] nco_cos, output reg signed [DATA_WIDTH*2-1:0] error ); always (posedge clk or negedge rst_n) begin if (!rst_n) error {(DATA_WIDTH*2){1b0}}; else error ref_signal * nco_cos; end endmodule这里我故意没有做截位把完整的乘法结果32位传给了环路滤波器。为什么保留全精度因为在FPGA里中间计算多保留几位能有效减少后续截位引起的量化误差。锁定状态时相位差本来就很小如果在这里就截掉太多位相位差可能被量化为零环路就失去调整能力了。资源上多出来的那几位寄存器相比于锁相性能的提升完全值得。2.2 环路滤波器比例积分结构的数字实现环路滤波器是整个锁相环的灵魂。它同时承担两个任务一是滤除鉴相器输出的高频分量和噪声二是提供足够的环路增益让闭环系统能够收敛。我采用的是比例加积分的结构类似控制系统里的PI控制器。用一个生活里的类比来理解这两个分量你在淋浴时调节水温发现水凉了立刻把旋钮往热水方向拧了一个大角度这是比例项它让系统快速响应误差但拧完大角度后水温可能还是差一点才合适这时候需要一小步一小步地微调直到水温刚刚好这是积分项它负责消除最终那一点点稳态误差。比例项管速度和稳定性积分项管精度。数字实现时比例项就是把鉴相器误差右移若干位后直接加到输出上右移的位数就是比例增益积分项则是把误差不断地累加到一个寄存器里累加的系数决定了积分速度。代码如下module loop_filter #( parameter DATA_WIDTH 32, parameter PD_WIDTH 32, parameter KP_SHIFT 10, parameter KI_SHIFT 14 )( input wire clk, input wire rst_n, input wire signed [PD_WIDTH-1:0] error, output reg signed [DATA_WIDTH-1:0] freq_ctrl ); reg signed [DATA_WIDTH-1:0] integral; wire signed [PD_WIDTH-1:0] kp_term error KP_SHIFT; wire signed [PD_WIDTH-1:0] ki_term error KI_SHIFT; always (posedge clk or negedge rst_n) begin if (!rst_n) begin integral 32d0; freq_ctrl 32d0; end else begin integral integral ki_term; freq_ctrl integral kp_term; end end endmodule强调一点在Verilog中对有符号数做右移要用算术右移运算符而不是逻辑右移。算术右移会保留符号位负数右移后仍然是负数这样才能保证环路在误差为负时能把频率控制字压低。这个细节如果写错了环路会出现正反馈永远锁不住。环路滤波器的系数选择没有万能公式但有一个经验方向KP_SHIFT和KI_SHIFT越大增益越小环路越稳定但收敛越慢增益越大锁定越快但容易出现振荡。我通常会先用较大的KP_SHIFT值保证仿真不会炸掉再逐步减小观察锁定时间的变化。常规的调试路径是先让积分项保留一定作用但不让它主导否则锁定后相位差会一直摆动。2.3 数控振荡器NCO的Verilog实现细节NCO就是数字锁相环里的“压控振荡器”只是它控制的不是电压而是频率控制字。NCO的核心是一个相位累加器每个时钟周期把频率控制字加到相位累加值上相位累加值的高位作为查找表的地址查找表里存着正弦和余弦波形输出的就是本地振荡信号。为什么相位累加器能够产生频率看图更容易理解把相位累加器想象成一个循环前进的进度条从0走到2^32每走完一圈就在正弦查找表里对应一个完整的周期。频率控制字越大每步前进的相位越多走完一圈需要的时间就越短输出频率就越高。输出频率的计算公式是f_out FCW / 2^PHASE_WIDTH * f_clk其中FCW是频率控制字PHASE_WIDTH是相位累加器位宽f_clk是系统时钟频率。位宽越大频率分辨率越高因为2^PHASE_WIDTH越大同样的整数变化对应的频率步进越小。查找表用$readmemh初始化把正弦波形写进一个十六进制文件里。考虑到完整32位相位不可能全部作为地址我截取了高10位作为查找表地址。截位会带来一定程度的相位截断杂散但10位地址对应1024点一个周期幅度精度16位对于大多数通信和电机控制应用已经够用。代码如下module nco #( parameter PHASE_WIDTH 32, parameter ADDR_WIDTH 10, parameter LUT_WIDTH 16 )( input wire clk, input wire rst_n, input wire [PHASE_WIDTH-1:0] freq_ctrl, output reg signed [LUT_WIDTH-1:0] sin_out, output reg signed [LUT_WIDTH-1:0] cos_out ); reg [PHASE_WIDTH-1:0] phase_acc; wire [ADDR_WIDTH-1:0] addr_sin phase_acc[PHASE_WIDTH-1:PHASE_WIDTH-ADDR_WIDTH]; wire [ADDR_WIDTH-1:0] addr_cos addr_sin (1 (ADDR_WIDTH - 2)); reg [LUT_WIDTH-1:0] sin_table [0:(1ADDR_WIDTH)-1]; initial $readmemh(sin_table.hex, sin_table); always (posedge clk or negedge rst_n) begin if (!rst_n) begin phase_acc {PHASE_WIDTH{1b0}}; sin_out 16sd0; cos_out 16sd0; end else begin phase_acc phase_acc freq_ctrl; sin_out $signed(sin_table[addr_sin]); cos_out $signed(sin_table[addr_cos]); end end endmodule余弦的输出其实就是在正弦相位上偏移90度也就是四分之一个周期对应地址偏移1 (ADDR_WIDTH - 2)。这样省掉了一张余弦表BRAM资源减半。我刚开始写NCO时没想到这一点直接存了两张完整波形表后来才改成加法偏移资源占用立刻少了一大块。2.4 参数计算实例从参考频率到FCW参数计算是很多新手卡壳的地方。这里我给出一个完整实例照着算就不会出错。假设系统时钟f_clk为100MHz参考信号频率f_ref为1MHz相位累加器位宽PHASE_WIDTH为32位。先算参考信号对应的频率控制字FCW_ref f_ref / f_clk * 2^32 1e6 / 100e6 * 4294967296 42949672.96 ≈ 42949673换算成十六进制是0x028F5C29。这个值就是testbench里参考信号源NCO和数字锁相环NCO的初始频率控制字。因为两个NCO用同样的系统时钟同样的位宽当锁相环收敛后频率控制字会停在FCW_ref附近很小的波动是相位调整导致的正常现象。再看环路滤波器的初始参数。带上典型数据鉴相器输出是16位乘16位得到32位有符号数如果参考信号和NCO输出幅度都是满量程的约±8192相乘后幅度约±2^27也就是误差信号的峰值可达1.3亿左右。这么大的值直接进累加器一次累加就可能让积分项溢出。所以第一步要对误差做缩放。我通常在环路滤波器前面把乘法结果右移8位再做后续处理让误差的动态范围落到几百万以内这样KP_SHIFT取12、KI_SHIFT取16时单次调整量在原信号幅度的千分之一到百分之一之间环路能够稳定收敛。参数汇总如下表参数取值说明系统时钟频率100 MHz所有模块共用一个时钟参考信号频率1 MHz仿真时从testbench注入相位累加器位宽32 bit频率分辨率约0.023 Hz查找表地址位宽10 bit波形点数1024查找表数据位宽16 bit带符号参考FCW0x028F5C291MHz对应频率控制字误差缩放位右移8位防止积分溢出KP_SHIFT12比例项增益KI_SHIFT16积分项增益这套初始参数在我的仿真里表现不错大约几十微秒内可以完成锁定。但请注意参数必须配合你的具体场景来调换了采样率、换了信号幅度环路参数都要重新推一遍。用表格把这几个数记下来后面做回归测试时就能快速定位是参数问题还是逻辑问题。3. Vivado工程搭建与仿真验证全流程3.1 建工程与添加文件的注意事项用Vivado做数字锁相环仿真步骤上并不复杂我建议的顺序是新建工程、添加设计文件、添加testbench文件、设置testbench为顶层、运行行为仿真。新建工程时选择器件型号我一般选和自己手里的开发板一致的器件这样以后上板调试不用重新综合。如果只是想做仿真验证随便选一个Kintex-7或Artix-7的器件都行仿真结果不受影响。文件组织上我强烈建议在工程目录里单独建一个sim文件夹放testbench设计文件放src文件夹波形文件放mem文件夹。因为Vivado会把所有文件都加入工程索引工程文件一多目录混乱会让你后期维护很痛苦。特别是$readmemh的路径问题如果sin_table.hex没有放在仿真器当前工作目录下仿真时LUT会全部变成X态这是最常见的错误之一。添加文件后需要确认文件类型testbench文件要标记为Simulation Sources否则综合时会报错。在Sources面板里右键文件选择Add to Simulation Sources即可。然后在Hierarchy面板里右键testbench文件选择Set as TopVivado在仿真时才能正确找到入口。运行仿真只需要点击Flow Navigator中的Run Simulation选Behavioral Simulation。第一次仿真会比较慢因为Vivado要编译完整的仿真库之后每次修改再跑增量编译会快很多。3.2 手写testbench的要点testbench是整个仿真验证里容易被低估的部分。数字锁相环的testbench要有两个关键功能一是能产生一个频率和相位可控的参考信号二是能观察环路的内部状态比如频率控制字的收敛过程。参考信号最简单可靠的方式是在testbench例化一个NCO给它一个固定的频率控制字。但这样做有个小问题testbench里的NCO和被测锁相环里的NCO查找表初始化文件路径可能指向同一个文件如果文件路径写得不一致会带来额外的头文件管理麻烦。所以我更推荐用直接赋值的方法把参考正弦波按查找表的数据格式放进一个数组用$readmemh初始化后每周期查表输出一次。跳频测试是验证锁相环跟踪能力的核心场景。我习惯让参考信号在工作一段时间后改变频率控制字模拟频率突变然后观察锁相环能否在几百个周期内重新锁定。比如先发1MHz5000个时钟周期后跳到1.01MHz再5000个周期后跳回1MHz。这种动态测试能直接暴露环路带宽设置是否合理。testbench里时钟和复位的写法也比较固定具体如下timescale 1ns / 1ps module tb_dpll; reg clk; reg rst_n; wire signed [15:0] ref_signal; wire signed [15:0] nco_sin; wire signed [15:0] nco_cos; wire signed [31:0] freq_ctrl_out; // 100MHz时钟 initial clk 0; always #5 clk ~clk; // 复位 initial begin rst_n 0; #100; rst_n 1; end // 参考信号NCO reg [31:0] ref_fcw; initial begin ref_fcw 32h028F5C29; // 1MHz #50000; ref_fcw 32h0291F2AA; // 跳到约1.01MHz #50000; ref_fcw 32h028F5C29; // 跳回1MHz end nco #( .PHASE_WIDTH(32), .ADDR_WIDTH (10), .LUT_WIDTH (16) ) u_ref_nco ( .clk (clk), .rst_n (rst_n), .freq_ctrl (ref_fcw), .sin_out (ref_signal), .cos_out () ); // 被测锁相环 dpll_top u_dpll ( .clk (clk), .rst_n (rst_n), .ref_signal (ref_signal), .nco_sin (nco_sin), .nco_cos (nco_cos), .freq_ctrl (freq_ctrl_out) ); endmodule用initial块动态改变ref_fcw时要注意时序。#50000表示50微秒按100MHz时钟算就是5000个周期。这个时间足够一阶锁相环完成约50次调整通常能够锁定。如果你修改了环路增益参数跳频时间可能要相应加长否则上一次还没锁住下一次又来了波形看起来会非常混乱。3.3 仿真波形与锁定过程的观察方法运行仿真后Vivado会自动弹出波形窗口。第一次跑完建议把顶层接口和关键内部信号全部添加到波形窗口包括ref_signal、nco_sin、nco_cos、freq_ctrl_out还有环路滤波器里的integral信号。观察锁相是否成功有一个非常直观的方法直接看freq_ctrl_out的数值。如果参考信号频率是1MHzfreq_ctrl_out最终稳定在0x028F5C29附近就说明频率锁定了。跳频后freq_ctrl_out也会跟着变化在几百个周期内平移到新的目标值附近。另一种方法是看波形对齐。锁定时ref_signal与nco_sin之间的相位差固定但要注意不是让两个正弦波完全重合才算锁住。由于鉴相器设计是让参考正弦乘NCO余弦锁定后参考正弦与NCO正弦之间会有一个固定相位差大约为90度。这是因为只有相位差为90度时参考正弦与NCO余弦乘积的直流分量为零环路才进入平衡状态。如果在波形上看到两个正弦波峰值位置始终相差固定的几个采样点并且这个差值不随时间变化那也说明锁定成功。鉴相器输出信号也值得一看。刚启动时鉴相器输出是一个明显的正弦包络叠加二倍频分量幅度比较大随着环路收敛这个信号的直流分量逐渐变小接近零。这就像两个人打节拍刚开始节奏很乱拍子重而且歪慢慢调整后拍子轻了、正了最后稳定在一个很小的范围内。仿真时间长度上我刚才给的testbench总共15万纳秒也就是150微秒。跑完整个仿真Vivado波形窗口里能完整看到第一次锁定、跳频失锁再锁定、跳回原始频率再锁定这三个过程。如果你发现第一次锁定花了太长时间甚至跑完仿真还没稳定那就按第4节的排错方法去调参数。4. 常见问题与排错经验实录4.1 环路锁定不了或锁定太慢怎么查我在调试数字锁相环时最常遇到的问题就是环路不收敛。现象多种多样频率控制字像过山车一样来回摆动或者一直匀速爬升根本停不下来又或者始终保持在一个偏离目标很远的值上。排错的第一步不应该是调参数而是确认每个单独模块工作正常。NCO模块单独给一个固定频率控制字看输出波形频率是不是预期的值鉴相器用两个固定频率的输入信号看输出是不是包含两个频率分量环路滤波器给一个固定的阶跃误差看积分项是不是线性增长。三层排查下来大多数“环路不收敛”问题其实在底层就暴露了。如果确认模块没问题再把视线放到环路增益上。增益太大时误差信号的单次调整量过大环路会在目标频率附近来回穿越形成振荡反映在freq_ctrl_out上就是持续的锯齿波或正弦摆动。增益太小时调整量微乎其微锁定时间指数级增长看起来就像锁不住。这两种情况我都遇到过解决的套路是先设一组偏保守的增益调大shift位确认环路能锁定且稳定再逐步降低shift位观察锁定时间变化。还有一类情况是参考信号初始相位与NCO初始相位相差太远。鉴相器输出的是相位差的正弦而不是相位差本身如果初始相位差接近180度误差信号接近零环路会误以为已经锁定导致“假锁”。实际工程里上电时给NCO的初相和参考信号相位很难保证一致所以要给环路设置一个初始频率偏置让VCO的起振频率接近参考频率把捕获范围覆盖住。仿真的testbench里可以通过复位后立即给一个接近目标的频率控制字来实现。4.2 Vivado仿真中的时序与初始化坑仿真阶段最容易踩的坑是X态传播。典型场景是LUT输出全是X然后鉴相器输出X接着环路滤波器也全变X一眼望去波形窗口里全是红叉。这个问题的根源九成是$readmemh没有找到文件。Vivado行为仿真时工作目录默认在工程目录下如果你的sin_table.hex放在子目录里要么用绝对路径要么用$readmemh(../../mem/sin_table.hex, sin_table)这种方式。我通常直接把hex文件放在工程根目录省心。第二个坑是异步复位释放引起的亚稳态。testbench里复位信号是纯initial驱动和时钟完全没有关系如果复位释放的瞬间正好赶上时钟上升沿被测模块里的状态寄存器可能进入亚稳态。虽然行为仿真不一定会真的表现成错误但这个隐患一旦带到上板阶段就会很棘手。我现在的习惯是在所有模块里都用异步复位同步释放的方式复位是异步置位释放时至少经过两级同步后再使用。第三个坑是仿真速度慢。DPLL需要几十万纳秒的仿真时间才能真正看得清锁定过程而Vivado的xsim仿真器在跑这种长仿真时速度会明显下降。解决办法是把波形刷新率降低或者只在需要观察的时间窗口里打开信号采集更高效的方法是用$monitor或$display定时打印freq_ctrl_out用文本就能判断环路收敛趋势不需要全程盯着波形图看。4.3 从仿真到上板要注意的实际问题仿真跑通了不代表上板就一定没问题这是FPGA开发最容易被现实教育的一课。仿真环境下的时钟是理想时钟上板后的时钟会有抖动参考信号在仿真里是一个完美的正弦波而实际从ADC采进来的参考信号可能叠加噪声、带直流偏置、甚至有毛刺。鉴相器对这些非理想因素非常敏感。如果输入信号带有较大的直流偏置乘法器输出会混入一个与偏置成正比的误差分量导致锁定点偏移。解决办法是在关键输入信号路径上加一阶高通滤波器把直流分量去掉或者使用时域平均的方法压低噪声。针对毛刺可以在鉴相器前加一个简单的滑动平均滤波代价是环路多了几个周期的延迟但稳定性显著提升。内部信号观测也是上板后的难点。仿真时可以直接把信号拖到波形窗口但上板后芯片内部信号是看不见的。我的做法是在顶层模块里给关键信号添加(* mark_debug true *)约束综合后在硬件管理器里用ILA核去抓取。调试时要抓的信号主要是freq_ctrl_out和鉴相器输出通过观察这两个信号的实时值来判断环路是否锁定。需要注意ILA接入会消耗一定的BRAM资源而且会占用JTAG链路调试完之后要记得把mark_debug去掉重新综合。还有一点是时序约束。DPLL里NCO的相位累加器是一个典型的超长组合逻辑链如果时序约束没做好工作频率上不去环路增益在实际运行时会因为时序收敛问题而打折。我建议在综合前为所有时钟加上create_clock约束并且把NCO输出路径设为伪路径告诉工具这部分不需要严格的STA检查否则Vivado可能会在布局布线阶段给你报一堆时序违例。加约束本身不复杂但对后期上板成功率影响很大。最后再说一个数字锁相环调试时不容易注意但实际非常致命的细节频率控制字的赋值时机。NCO在每个时钟上升沿使用freq_ctrl环路滤波器的输出也在时钟上升沿更新如果你把环路滤波器和NCO在同一个模块里用非阻塞赋值驱动可能出现一个周期的时间错位。虽然这个错位在仿真里看起来只是慢了一个时钟周期但在高速系统里这一拍可能让环路相位裕度变差导致本来稳定的环路突然变成振荡。解决方法是把环路滤波器和NCO放在两个独立模块里让它们在相邻时钟沿更新或者用流水线寄存器把环路滤波结果打一拍再送往NCO。我个人在实际操作中的体会是数字锁相环这种项目代码本身写出来并不难难的是参数标定和调试。你可以把第2节里那张参数表跑一遍然后再故意把环路增益调得极大或极小亲眼看看波形会变成什么样这种“故意犯错”的方式比看十遍理论都管用。这个项目后续还可以扩展的方向不少比如把一阶环路改成二阶加入捕获检测与辅助锁定逻辑或者把NCO的查找表换成CORDIC算法来节省BRAM这些我在实际项目中也都试过。如果后面有时间我会再把二阶环路的实现和调试过程整理出来继续分享给大家。
返回列表