基于 FPGA 的 PLL 调试记录
开始时间:2025.7.17
结束时间:2025.7.19
撰写者: Keruone
代码主要编写者:Keruone、Friday
特别鸣谢: 徐浩俊、方睿、金垄晟
摘要:本博客记录了基于 FPGA 的锁相环(PLL)调试过程,包括锁相环的基本原理、模块设计与实现、代码实现以及调试经验。博客详细介绍了锁相环的核心模块,如鉴相器(PD)、数字振荡器(NCO)和 FIR 低通滤波器,并提供了 Verilog 代码实现。通过 FPGA 开发板(Altera Cyclone IV)实现了完整的锁相环系统,支持固定频率和动态调整的锁相功能。
关键词:FPGA、锁相环(PLL)、鉴相器(PD)、数字振荡器(NCO)、DDS、FIR 低通滤波器、Verilog、Quartus Prime、频率控制字、相位控制字、调试记录
目录:
1. 锁相环原理概述
锁相环的原理其实网上有很多资料,这里重复的就不多赘述了,在这里我可能提及更多的是自己的理解。
首先,我们先简单介绍一下锁相环,它的最直接的用途,就是让输出信号“紧紧跟随”输入信号。
- (你应该知道,不同的设备即使输出同样的频率,那也仍然会有几hz的频率偏差。反映在示波器上,就是同时显示输入信号和输出信号时,会发现两个信号间会微微移动)
- 注意:你应该注意到,我这里所提及的锁相环,它的输入输出频率都是非常接近的。那你应该注意到,本文所提及及讨论的锁相环,也是频率非常接近的。
> 为此,如果你想要实现频率可以大范围变化的锁相环,那你应该使用串口(如SPI、UART)来通信,告诉 fpga 频率控制字大致要为多少,再进行微调;或者 FFT 来检测出输入频率,再调整 fpga 频率控制字。最后再进行锁相。
> 或者你可以网上再搜寻搜寻看看,有没有什么更好的法子。
那我们该如何实现锁相呢?
在回答这个问题之前,我们先想想,为什么叫做“锁相环”而不是“锁频环”。
锁相锁相,顾名思义,它锁定的是相位,将输出和输入的信号的相位锁在一起,而频率只是“顺带”的。通过电路的反馈,控制相位跟随。
1.1 鉴相(PD)
根据上面的描述,我们可以“近似”假设,在添加反馈回路前,输入和输出的信号只有相位差,没有频率差,即
\begin{aligned}
s_{in} &= \sin(\omega_0 t + \theta_{in}) \
s_{out} &= \sin(\omega_0 t + \theta_{out})
\end{aligned}
那么,我们该如何提取其中的相位信号呢?这个时候,最直接的办法就是相积化和差+低通滤波器。
s_{in} \cdot s_{out} = \frac{1}{2} \left[ \cos\left( \theta_{in} – \theta_{out} \right) – \cos\left( 2\omega_0 t + \theta_{in} + \theta_{out} \right) \right]
如果忘记了什么是积化和差,可以参考百度百科
我们来观察一下,可以发现,这里有两个分量:
– 1. 与相位相关的直流分量
– 2. 高频分量。
那这个时候理论上将输出的波形相乘,再通过低通滤波器,我们就得到了与相位差相关的电压信号\frac{1}{2}\cos\left( \theta_{in} – \theta_{out} \right)。
这个信号很关键,我们可以利用这个信号来实现反馈,从而实现锁相。
综上,本文 PD 部分在 fpga 中将会用到两个模块:
– 1. 乘法器
– 2. FIR II
上述内容详见代码部分
附,为什么使用sin而不使用cos?有人会问,这两个信号相乘的结果不是不一样吗?
我的回答是:理论上计算的表象是会不一样,但是你需要注意到,两个信号的相位差是不确定的,你每次使用这个锁相环时,随着你的参考点不同、使用的时间不同、开启的时间不同,相位都是不同的。
那sin和cos有什么区别?不就一个90度相位差呗。这放在不确定的初始相位中,不就加减90的关系,其实是没有任何影响的。
所以,不要纠结乘sin还是cos,等价的。
1.2 数字振荡器(NCO)
前文一直在提及输出信号,但是输出信号是哪里来的?答,来自数字振荡器。
数字振荡器(NCO)这个词可能有点陌生,但也没关系。在这篇博客里,我们实现的方式其实就是一个有频率控制字和相位控制字的 DDS 罢了。
如果你不知道什么是 DDS ,那我只能建议你网上查一查了,比如这里,
那这里,我们可以很明显的观察到两个可以用来反馈的参数:
– 1. 频率控制字
– 2. 相位控制字
首先先说结论,我们通过调整相位控制字实现锁相。
为什么?首先,我们先假设将从DDS输出到乘法器的信号存储在一张有1024个不同数据的rom表中。接着让我们来简单回顾一下这两的区别:
– 1. 频率控制字:最通俗的理解,即在rom表中,每个clk进来时,rom表中指向输出的指针移动多少个索引,DDS输出该索引的数值
– 2. 相位控制字:直接在索引上加加减减
那这个时候肯定会有人问,这不都是修改地址(索引)吗?为什么不能使用频率控制字反馈呢?
我们再来回顾一下,这是”锁相环“,不是”锁频环“。所以对此,频率控制字是间接(积分) 控制相位,而相位控制字是直接控制相位。自然而然,相位控制字的反馈会迅速很多。
另一种解释是:作用于频率控制字上的变化,在修改之后的每一个时钟周期,这个 Δω 都会持续产生作用,也就是说,它会通过时间积分 最终形成相位的变化。
\theta(t) = \int \omega(t) \, dt
不过说反馈频率能不能反馈吧,其实也是能反馈的,只不过效果不太行,会左右晃荡。
1.3 框图

1.4 补充与倍频
*对于类似时钟信号的上下上下的方波脉冲,可以参考这篇博客
对此如果你有倍频的需求,其实只要在反馈输出信号的回路上添加一个分频器就行了(这也是像单片机这些设备如何获得高频的由来)
对此,框图可以参考这里:

**上图来自这篇博客*
1.5 用途
- 普通的锁频:
- 锁定微小信号:
- 倍频
- 调制解调
2. 代码实现
注意,我本身并不是专门写 verilog 的,所以代码可能有些不规范,请原谅。
2.1 设备参数介绍
本项目基于以下硬件和开发环境实现:
| 参数类别 | 具体信息 |
|---|---|
| 开发软件 | Quartus Prime 18.0 |
| 芯片型号 | Altera Cyclone IV EP4CE10E22C8N |
| 开发板型号 | EP4CE10V1 |
| 实际选用器件 | EP4CE10E22C8(在 Quartus 中选择) |
*注意,我们社团有两块板子,一块是黑的,一块蓝的,分别是Cyclone IV和II的,不要拿错了。
2.2 鉴相器
根据前文的介绍,鉴相器由两部分组成:乘法器 和 FIR II数字低通滤波器。
2.2.1 乘法器
这个就不多介绍了,考虑到我们的板子 adc 采集和 dac 输出都是 10bit 的信号,所以用IP核配置乘法器时,只需要配置输入两个通道分别10bit,输出20bit即可。
2.2.2 FIR II 数字低通滤波器
在此,我们设置clk频率为50MHz,采样率为500kHz,通带频率约为5kHz,截止频率为15kHz,使用matlab来设计参数,再导入到quartus中。
我们的板子clk频率为50MHz,但这不意味着我们采样频率就是50MHz。如果只是adc采集信号直接输出到dac的话,那确实可以50MHz,但是FIR设计完毕后,阶数会达到惊人的14000+,这显然不现实,所以采样频率肯定得降低。
在IP核中,还有这几个参数需要关注一下:
1. 参数位宽。也就影响影响精度,你在Cofficient标签栏中是可以直观的看到不同位宽对滤波器的影响。这里选择了12bit。
2. 输出的MSB截断(高位开始多少位不要)和LSB截断(低位开始多少位不要)。因为通过FIR后的信号会根据输入位数和参数宽度影响输出的实际宽度。像我这实际输出是31bit,因为如果直接从高位截10bit作为最终输出的话信号会很小,不利于反馈。所以我在MSB截断了5bit,LSB截断了16位。这样相较于直接从高位截10bit,数值是放大了的,FIR的输出变化会更加明显。(这里参数如果自己设计FIR的话就自己实验中看看结果再来调整吧)
其余的IP核参数就放张截图吧:





调用这IP核的代码如下所示
点击展开:FIR滤波器模块代码(FIR_II_FILTER)
//==============================================================================
// Company: Ningbo University
// Engineer: Keruone
// Create Date: 2025/07/19
// Module Name: FIR_II_FILTER
// Description: FIR Filter with 500kHz sampling rate and signed data processing
//==============================================================================
module FIR_II_FILTER (
// Clock Interface
input clk, // 50MHz 系统时钟
// Data Interface
input signed [9:0] fir_signed_input, // ADC 输入数据 (有符号)
output reg signed [9:0] fir_signed_output, // DAC 输出数据 (有符号)
// Clock Output Interface
output clk_adc, // ADC 时钟输出 (500kHz)
output clk_dac // DAC 时钟输出 (500kHz)
);
//==============================================================================
// Parameter Definitions
//==============================================================================
// Clock Divider Parameters
parameter CLK_DIV_COUNT = 49; // 分频计数值 (50MHz/500kHz/2 - 1)
parameter RESET_CYCLES = 15; // 复位周期数
//==============================================================================
// Internal Signal Declarations
//==============================================================================
// Reset Generation
reg [3:0] reset_counter; // 复位计数器
reg reset_n; // 内部复位信号
// Clock Generation
reg [6:0] clk_div_counter; // 分频计数器 (0-49)
reg clk_500k; // 500kHz 时钟
reg clk_500k_prev; // 500kHz 时钟的前一状态
wire clk_500k_posedge; // 500kHz 时钟上升沿检测
// Data Processing
reg signed [9:0] adc_data_reg; // ADC 数据寄存器 (有符号)
reg adc_valid; // ADC 数据有效信号
// FIR Filter Interface
wire signed [9:0] fir_output; // FIR 有符号输出
wire fir_output_valid; // FIR 输出有效信号
//==============================================================================
// Reset Generation Circuit
//==============================================================================
// 复位计数器初始化
initial begin
reset_counter = 4'd0;
end
// 内部复位信号生成器
always @(posedge clk) begin
if (reset_counter < RESET_CYCLES) begin
reset_counter <= reset_counter + 1'b1;
reset_n <= 1'b0;
end else begin
reset_n <= 1'b1;
end
end
//==============================================================================
// Clock Divider (50MHz -> 500kHz)
//==============================================================================
// 50MHz -> 500kHz 分频器
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
clk_div_counter <= 7'd0;
clk_500k <= 1'b0;
end else begin
if (clk_div_counter >= CLK_DIV_COUNT) begin
clk_div_counter <= 7'd0;
clk_500k <= ~clk_500k; // 翻转生成500kHz
end else begin
clk_div_counter <= clk_div_counter + 1'b1;
end
end
end
//==============================================================================
// Edge Detection for 500kHz Clock
//==============================================================================
// 500kHz 时钟上升沿检测
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
clk_500k_prev <= 1'b0;
end else begin
clk_500k_prev <= clk_500k;
end
end
assign clk_500k_posedge = clk_500k & ~clk_500k_prev;
//==============================================================================
// ADC Data Sampling
//==============================================================================
// ADC 数据采样 (直接使用有符号数据)
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
adc_data_reg <= 10'sd0; // 有符号零值
adc_valid <= 1'b0;
end else begin
if (clk_500k_posedge) begin
adc_data_reg <= fir_signed_input; // 采样有符号ADC数据
adc_valid <= 1'b1; // 数据有效
end else begin
adc_valid <= 1'b0; // 其他时候数据无效
end
end
end
//==============================================================================
// FIR Filter Instantiation
//==============================================================================
// 实例化 FIR 滤波器 (直接使用有符号数据)
FIR_FILTER fir_filter_inst (
.clk (clk), // 50MHz 时钟
.reset_n (reset_n), // 复位信号
.ast_sink_data (adc_data_reg), // 有符号输入数据
.ast_sink_valid (adc_valid), // 数据有效信号
.ast_sink_error (2'b00), // 无错误
.ast_source_data (fir_output), // 有符号输出数据
.ast_source_valid (fir_output_valid), // 输出有效信号
.ast_source_error () // 错误信号(未使用)
);
//==============================================================================
// DAC Output Data Processing
//==============================================================================
// DAC 输出数据锁存 (直接使用有符号数据)
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
fir_signed_output <= 10'sd0; // 复位时输出有符号零值
end else begin
if (fir_output_valid) begin
fir_signed_output <= fir_output; // 更新DAC输出
end
end
end
//==============================================================================
// Output Clock Assignment
//==============================================================================
// 输出时钟分配
assign clk_adc = clk_500k; // ADC 时钟 500kHz
assign clk_dac = clk_500k; // DAC 时钟 500kHz
endmodule
- 功能:实现了一个基于500kHz采样率的FIR滤波器模块,用于对ADC输入的有符号10位数据进行滤波处理,并将结果输出给DAC。
- 核心功能点:
- 从50MHz系统时钟分频出500kHz的ADC/DAC时钟;
- 对ADC输入数据进行采样并送入FIR滤波器;
- 滤波结果输出至DAC;
- 支持有符号数据处理(signed [9:0]);
- 使用方式:
- 在顶层模块中实例化该模块;
- 将需要滤波的信号输入连接到
fir_signed_input; - 将运滤波后的信号输出连接到
fir_signed_output; - 使用
clk_adc和clk_dac驱动外部ADC/DAC芯片,不过这里是500kHz的信号,如果不想要以这个频率驱动的,请不要连接; - 内部调用的
FIR_FILTER模块需提前生成(如使用Quartus的IP核工具);
2.3 数字振荡器
本设计中使用的数字振荡器(NCO)是通过自己编写的模块实现的,而不是使用 Quartus 提供的 IP 核。
为什么不用 IP 核?个人观点如下:
– IP 核支持的频率控制字最多只有 24 位(多了我编译就失败了);
– 使用 IP 核资源占用大、灵活性差;
– 修改波形数据不够方便,调试起来也不直观;
– 自己写的话,频率控制字可以做到 32 位,精度更高;
– 还能方便地添加多路输出、任意波形、相位控制等功能。
模块功能简介
- 使用 32 位相位累加器,实现高精度频率控制;
- 通过查找 ROM 表输出正弦波形;
- 支持双路输出:
- 第一路用于反馈控制;
- 第二路可以设置任意相位差(如 90°、180°);
- 使用 MIF 文件加载 ROM 数据,方便修改波形;
- 输出使用流水线寄存器,提高时序性能;
- 时钟频率为 50MHz,支持高速输出。
关键公式说明
- 频率控制字计算:
Fword = \frac{F_{out} \times 2^{32}}{F_{clk}}
例如:要在 50MHz 系统时钟下输出 1kHz 信号:
Fword = \frac{1000 \times 2^{32}}{50 \times 10^6} \approx 85899
- 相位差与 phase_diff 的关系:
phase_diff = \frac{期望相位差}{360^\circ} \times 1024
例如:设置 90° 相位差:
phase_diff = \frac{90}{360} \times 1024 = 256
常用配置示例
| 功能需求 | phase_diff | 说明 |
|---|---|---|
| 同相输出 | 0 | 两路信号完全一致 |
| 正交输出 | 256 | I/Q 调制,第二路超前 90° |
| 反相输出 | 512 | 输出反相信号 |
| 三相系统 | 0, 341, 683 | 三相相差 120° |
*注,其实这里设计的初衷是考虑到随着频率的不同,锁定后的相位差也会不同,实际输出的波形需要调整
点击展开:DDS 模块代码(DDS)
//==============================================================================
// Company: Ningbo University
// Engineer: Friday
// Create Date: 2025/07/03
// Modified: 2025/07/19 - Added dual output with configurable phase difference
//
// Module Name: DDS (Direct Digital Synthesis)
// Description: 双路输出DDS模块,支持独立相位控制
// 第一路用于反馈控制,第二路可设置相位差
//==============================================================================
module DDS (
// ========== 时钟和复位信号 ==========
input wire clk, // 系统时钟 (50MHz)
input wire reset_n, // 异步复位,低电平有效
// ========== 输出信号 ==========
output wire dac_clk, // DAC时钟输出 (与系统时钟同频)
output wire [9:0] sin_out, // 第一路正弦波输出 (反馈信号)
output wire [9:0] sin_out2, // 第二路正弦波输出 (可设相位差)
// ========== 控制参数 ==========
input wire [31:0] Fword, // 频率控制字
input wire [9:0] Pword, // 第一路相位控制字
input wire [9:0] phase_diff // 第二路相位差控制字
);
//==========================================================================
// 内部信号定义
//==========================================================================
// 相位累加器
reg [31:0] phase_accumulator; // 32位相位累加器
// ROM接口信号
wire [9:0] rom_addr1; // 第一路ROM地址
wire [9:0] rom_addr2; // 第二路ROM地址
wire [9:0] wave_data1; // 第一路ROM输出数据
wire [9:0] wave_data2; // 第二路ROM输出数据
// 输出寄存器
reg [9:0] sin_out_reg1; // 第一路输出寄存器
reg [9:0] sin_out_reg2; // 第二路输出寄存器
//==========================================================================
// 输出信号连接
//==========================================================================
assign dac_clk = clk; // DAC时钟直接连接系统时钟
assign sin_out = sin_out_reg1; // 第一路输出
assign sin_out2 = sin_out_reg2; // 第二路输出
//==========================================================================
// 相位累加器 - 实现频率控制
//==========================================================================
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
phase_accumulator <= 32'h0000_0000;
end
else begin
phase_accumulator <= phase_accumulator + Fword;
end
end
//==========================================================================
// ROM地址生成 - 实现相位控制
//==========================================================================
// 第一路:基准相位 = 累加器高10位 + 相位偏移
assign rom_addr1 = phase_accumulator[31:22] + Pword;
// 第二路:在第一路基础上增加相位差
assign rom_addr2 = phase_accumulator[31:22] + Pword + phase_diff;
//==========================================================================
// 正弦波ROM实例化
//==========================================================================
// 第一路正弦波查找表
rom u_sin_rom1 (
.address ( rom_addr1 ),
.clock ( dac_clk ),
.q ( wave_data1 )
);
// 第二路正弦波查找表
rom u_sin_rom2 (
.address ( rom_addr2 ),
.clock ( dac_clk ),
.q ( wave_data2 )
);
//==========================================================================
// 输出寄存器 - 时序对齐和流水线
//==========================================================================
always @(posedge dac_clk) begin
sin_out_reg1 <= wave_data1; // 第一路输出寄存
sin_out_reg2 <= wave_data2; // 第二路输出寄存
end
endmodule
2.4 顶层PLL封装
根据前文介绍的各个模块,我们需要将鉴相器、数字振荡器、PID控制器等组件整合成一个完整的锁相环系统。
2.4.1 PLL_MODULE核心模块设计
PLL_MODULE 是锁相环的核心实现模块,包含了完整的信号处理链路和控制逻辑。
主要功能特点:
- 双路DDS输出:
- 第一路用于反馈控制(
nco_sin1) - 第二路作为实际输出(
nco_sin2),可设置相位差
- 第一路用于反馈控制(
- PID控制器:
- 采用定点数运算(Q16.16格式)
- 支持比例(P)、积分(I)、微分(D)三项控制
- 在250kHz频率下更新控制参数
- 信号处理链:
ADC输入 → 符号转换 → 乘法器 → FIR滤波 → PID控制 → 相位调整 → DDS输出
关键参数说明:
| 参数类别 | 参数名称 | 数值 | 说明 |
|---|---|---|---|
| 时钟分频 | DIVIDER_COUNT | 99 | 50MHz→250kHz分频比 |
| PID增益 | KP | 16384 | 比例增益(Q16.16格式下的0.25) |
| PID增益 | KI | 0 | 积分增益(一阶系统置零) |
| PID增益 | KD | 0 | 微分增益(一阶系统置零) |
注意:对于一阶锁相环来说,调整 KP 就够了,KI 和 KD 全部置零即可。这样可以避免系统震荡,提高稳定性。
信号转换处理:
// ADC输入:无符号 → 有符号
assign signed_adc = adc - 10'sd512;
// DAC输出:有符号 → 无符号
assign dac_output = signed_dac + 10'sd512;
// DDS反馈:无符号 → 有符号
assign dds_feedback = nco_sin1 - 10'd512;
为什么要做这些转换?
– ADC和DAC芯片通常使用无符号数据格式(0-1023)
– 内部信号处理需要有符号运算(-512到+511)
– 通过加减512实现两种格式的转换
PID控制器实现细节:
控制器采用250kHz更新频率,避免过快响应导致系统不稳定:
// 仅在250kHz上升沿更新PID参数
if (clk_250khz_posedge) begin
// P项:kp * error
p_term <= (KP * signed_firout_32) >>> 16;
// 更新相位控制字
phaseCtlWord <= phaseCtlWord + p_term[31:0];
end
频率控制字计算:
对于10kHz输出频率在50MHz系统时钟下:
Fword = \frac{F_{out} \times 2^{32}}{F_{clk}} = \frac{10000 \times 2^{32}}{50 \times 10^6} = 8589933
2.4.2 模块调用示例
为了方便使用,我们提供了一个简化的顶层封装示例。这个封装隐藏了内部复杂的参数配置,让用户可以直接调用:
设计思路:
- 接口简化:外部调用时不需要关心频率控制字、相位差等内部参数
- 参数固化:将常用的配置参数定义为常量,减少出错概率
- 兼容性:保持简洁的外部接口
常量参数配置:
// 频率控制字常量(10kHz@50MHz)
localparam [31:0] FREQ_CTRL_WORD_CONST = 32'd8589933;
// 相位差控制常量(约-18度相位差)
localparam signed [9:0] PHASE_DIFF_CONST = -10'sd50;
使用方式:
- 直接调用PLL_MODULE:如果需要动态调整频率和相位差
PLL_MODULE u_pll ( .FREQ_CTRL_WORD(your_freq_word), .PHASE_DIFF(your_phase_diff), // ... 其他信号 ); - 使用封装的PLL模块:如果使用固定参数
PLL u_pll_simple ( .clk(clk), .rst_n(rst_n), .adc(adc_data), .dac_output(dac_out), // ... 其他信号 );
建议:初学者建议使用封装后的PLL模块,参数已经调试好了。如果需要不同频率的锁相环,可以修改封装模块中的常量参数。
调试接口说明:
key2:控制DAC1输出选择key2=0:输出乘法器结果(调试用)key2=1:输出DDS信号(正常工作模式)
dac2_output:始终输出FIR滤波器结果,用于观察误差信号
2.4.3 参考代码
点击展开:PLL模块封装
//==============================================================================
// Company: Ningbo University
// Engineer: Keruone、Friday
// Create Date: 2025/07/19
// Module Name: PLL_MODULE
// Description: Phase-Locked Loop with dual DDS output and PID control
//==============================================================================
module PLL_MODULE(
// Clock and Reset
input logic clk,
input logic rst_n,
// ADC Interface
input logic [9:0] adc,
output logic clk_adc,
// DAC Interface - Channel 1
output logic [9:0] dac_output,
output logic clk_dac,
// DAC Interface - Channel 2
output logic [9:0] dac2_output,
output logic clk_dac2,
// DDS Output Phase Diff
input logic [31:0] FREQ_CTRL_WORD,
input logic signed [9:0] PHASE_DIFF,
// Control Interface
input logic key2
);
//==============================================================================
// Parameter Definitions
//==============================================================================
// Clock Divider Parameters
localparam int DIVIDER_COUNT = 99; // 50MHz/250kHz = 200分频
// PID Controller Parameters (Q16.16 format)
// 对于一阶锁相环来说调整 kp 就够了, ki 和 kd 全部置零即可
localparam signed [31:0] KP = 32'sd16384; // 比例增益 (0.25 in Q16.16)
localparam signed [31:0] KI = 32'sd0; // 积分增益 (0.05 in Q16.16)
localparam signed [31:0] KD = 32'sd0; // 微分增益 (0.025 in Q16.16)
// DDS Parameters
// localparam [31:0] FREQ_CTRL_WORD = 32'd8589933; // 固定频率控制字(例如:10kHz@50MHz)
// localparam signed[9:0] PHASE_DIFF = -10'd50; // 两路DDS输出相位差
//==============================================================================
// Internal Signal Declarations
//==============================================================================
// DDS Signals
logic [9:0] nco_sin1; // DDS第一路输出(用于反馈)
logic [9:0] nco_sin2; // DDS第二路输出(实际输出)
logic [9:0] dds_sin_out; // 处理后的DDS输出信号
logic [9:0] dds_feedback; // 用于反馈的DDS信号
// Signal Processing
logic signed [9:0] signed_adc; // 有符号ADC输入
logic signed [9:0] signed_dac; // 有符号DAC输出
logic [19:0] mult_result1; // 乘法器结果
logic signed [9:0] mult_signed1; // 截取后的乘法器输出
logic signed [9:0] signed_firout; // FIR滤波器输出
// Control Signals
logic [31:0] freqCtlWord; // 32位频率控制字(固定值)
logic signed [31:0] phaseCtlWord; // 32位有符号相位控制字
logic [9:0] phaseCtlWord_10; // 10位相位控制字给DDS
// PID Controller Variables
logic signed [31:0] error_previous; // 前一次误差
logic signed [63:0] integral_sum; // 积分累加(扩展位宽防止溢出)
logic signed [63:0] p_term; // P项计算结果
logic signed [63:0] i_term; // I项计算结果
logic signed [63:0] d_term; // D项计算结果
logic signed [31:0] error_diff; // 误差差值
logic signed [31:0] signed_firout_32; // 扩展的误差信号
// Clock Generation
logic [7:0] clk_divider_cnt; // 时钟分频计数器
logic clk_250khz; // 250kHz时钟
logic clk_250khz_posedge; // 250kHz时钟上升沿
logic clk_250khz_prev; // 250kHz时钟前一状态
//==============================================================================
// Clock Divider (250kHz Generation)
//==============================================================================
// 250kHz时钟分频器
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_divider_cnt <= 8'd0;
clk_250khz <= 1'b0;
end else begin
if (clk_divider_cnt >= DIVIDER_COUNT) begin
clk_divider_cnt <= 8'd0;
clk_250khz <= ~clk_250khz;
end else begin
clk_divider_cnt <= clk_divider_cnt + 1'b1;
end
end
end
// 250kHz时钟上升沿检测
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_250khz_prev <= 1'b0;
end else begin
clk_250khz_prev <= clk_250khz;
end
end
assign clk_250khz_posedge = clk_250khz & ~clk_250khz_prev;
//==============================================================================
// Control Word Generation
//==============================================================================
// 固定频率控制字
assign freqCtlWord = FREQ_CTRL_WORD;
// 取相位控制字的高10位给DDS(限制在0-1023范围内)
assign phaseCtlWord_10 = phaseCtlWord[19:10];
// 将10位误差信号扩展到32位进行计算
assign signed_firout_32 = {{22{signed_firout[9]}}, signed_firout};
assign error_diff = signed_firout_32 - error_previous;
//==============================================================================
// PID Controller (250kHz Update Rate)
//==============================================================================
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
phaseCtlWord <= 32'sd0; // 初始相位控制字为0
error_previous <= 32'sd0;
integral_sum <= 64'sd0;
end else if (clk_250khz_posedge) begin // 仅在250kHz上升沿更新
// 定点数PID计算 (Q16.16格式)
// P项:kp * error
p_term <= (KP * signed_firout_32) >>> 16;
// I项:累加误差并乘以ki
integral_sum <= integral_sum + signed_firout_32;
i_term <= (KI * integral_sum[31:0]) >>> 16; // 取积分和的低32位
// D项:kd * (error - error_previous)
d_term <= (KD * error_diff) >>> 16;
// 更新相位控制字
phaseCtlWord <= phaseCtlWord + p_term[31:0] + i_term[31:0] + d_term[31:0];
// 保存当前误差用于下次D项计算
error_previous <= signed_firout_32;
end
end
//==============================================================================
// Signal Processing Chain
//==============================================================================
// 乘法器实例化
mult mult1 (
.clock (clk),
.dataa (signed_adc),
.datab (dds_feedback),
.result (mult_result1)
);
// FIR滤波器实例化
FIR_II_FILTER fir_inst1 (
.clk (clk),
.fir_signed_input (mult_signed1),
.clk_adc (clk_adc),
.fir_signed_output (signed_firout),
.clk_dac () // 未使用,避免阶跃变化
);
/* Do not use `clk_dac`.
If this CLK is used to drive the dac,
the output signal will exhibit a significant step-like variation.
Unless you are using it for debugging the FIR effect.
*/
// DDS模块实例化
DDS u2 (
.clk (clk),
.reset_n (rst_n),
.dac_clk (clk_dac), // 驱动DAC实际使用这里输出的DAC时钟
.sin_out (nco_sin1), // 用于反馈的信号
.sin_out2 (nco_sin2), // 实际输出信号
.Fword (freqCtlWord), // 固定频率控制字
.Pword (phaseCtlWord_10), // 相位控制字(10位)
.phase_diff (PHASE_DIFF) // 两路输出相位差
);
//==============================================================================
// Output Signal Processing
//==============================================================================
// ADC输入信号处理:无符号转有符号
assign signed_adc = adc - 10'sd512; // 输入无符号转有符号 (Necessary)
// DAC时钟分配
assign clk_dac2 = clk_dac; // 使用DDS输出的DAC时钟来驱动 (Necessary)
// DAC1输出信号选择
assign signed_dac = key2 ? dds_sin_out : mult_signed1;
assign dac_output = signed_dac + 10'sd512; // 有符号转无符号输出 (Optional)
// DAC2输出信号
assign dac2_output = signed_firout + 10'sd512; // 直接输出滤波器输出 (Optional)
// 乘法器输出截取
assign mult_signed1 = mult_result1[19:10]; // 用于FIR滤波 (Necessary)
// DDS输出信号处理
assign dds_feedback = nco_sin1 - 10'd512; // 用于乘法器反馈 (Necessary)
assign dds_sin_out = nco_sin2 - 10'd512; // 实际输出信号
endmodule
//==============================================================================
// End of Module
//==============================================================================
点击展开:调用示例
//==============================================================================
// Company: Ningbo University
// Engineer: Keurone、Friday
// Create Date: 2025/07/19
// Module Name: PLL (Top Level Wrapper)
// Description: Top-level wrapper for PLL_MODULE to maintain pin compatibility
//
// 特别鸣谢:徐浩俊、金垄晟
//==============================================================================
module PLL (
// ========== 原始接口保持不变 ==========
// 时钟和复位
input wire clk,
input wire rst_n,
// ADC接口
input wire [9:0] adc,
output wire clk_adc,
// DAC接口 - 通道1
output wire [9:0] dac_output,
output wire clk_dac,
// DAC接口 - 通道2
output wire [9:0] dac2_output,
output wire clk_dac2,
// 控制接口
input wire key2
);
//==========================================================================
// DDS控制常量定义
//==========================================================================
// 频率控制字常量(10kHz@50MHz)
localparam [31:0] FREQ_CTRL_WORD_CONST = 32'd8589933;
// 相位差控制常量
localparam signed [9:0] PHASE_DIFF_CONST = -10'sd50;
//==========================================================================
// PLL核心模块实例化
//==========================================================================
PLL_MODULE u_pll_core (
// 时钟和复位
.clk ( clk ),
.rst_n ( rst_n ),
// ADC接口
.adc ( adc ),
.clk_adc ( clk_adc ),
// DAC接口 - 通道1
.dac_output ( dac_output ),
.clk_dac ( clk_dac ),
// DAC接口 - 通道2
.dac2_output ( dac2_output ),
.clk_dac2 ( clk_dac2 ),
// DDS控制接口(使用常量)
.PHASE_DIFF ( PHASE_DIFF_CONST ),
.FREQ_CTRL_WORD ( FREQ_CTRL_WORD_CONST ),
// 控制接口
.key2 ( key2 )
);
endmodule
3. 完整 quartus 工程分享
通过网盘分享的文件:2025PLL_In_FPGA.zip
链接: https://pan.baidu.com/s/1VqRUN3udlJt8LI9_PqVSvg?pwd=snkr 提取码: snkr
4. 参考
-
原理参考:
https://blog.csdn.net/DBLLLLLLLL/article/details/84395583
https://blog.csdn.net/weixin_43824941/article/details/118739186 -
FIR参考:
https://blog.csdn.net/qq_42839007/article/details/104354810 -
Matlab FIR设计:
https://blog.csdn.net/bleauchat/article/details/85136485
https://www.cnblogs.com/cofin/p/10220648.html -
书籍参考:
《锁相环技术原理及FPGA实现》 杜勇
《数字滤波器的MATLAB与FPGA实现-Altera/Verilog版(第二版)》 杜勇 -
社团资料:
新版 FPGA 开发板使用说明书


太有操作了xh学长