基于 FPGA 的 PLL 调试记录

基于 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 用途

  1. 普通的锁频:
    锁频100kHz
  2. 锁定微小信号:
    锁定微笑信号
  3. 倍频
  4. 调制解调

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核参数就放张截图吧:

figure1

figure2

figure3

figure4

figure5

调用这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]);
  • 使用方式
    1. 在顶层模块中实例化该模块;
    2. 将需要滤波的信号输入连接到 fir_signed_input
    3. 将运滤波后的信号输出连接到 fir_signed_output
    4. 使用 clk_adcclk_dac 驱动外部ADC/DAC芯片,不过这里是500kHz的信号,如果不想要以这个频率驱动的,请不要连接;
    5. 内部调用的 FIR_FILTER 模块需提前生成(如使用Quartus的IP核工具);

2.3 数字振荡器

本设计中使用的数字振荡器(NCO)是通过自己编写的模块实现的,而不是使用 Quartus 提供的 IP 核。

为什么不用 IP 核?个人观点如下:
– IP 核支持的频率控制字最多只有 24 位(多了我编译就失败了);
– 使用 IP 核资源占用大、灵活性差;
– 修改波形数据不够方便,调试起来也不直观;
– 自己写的话,频率控制字可以做到 32 位,精度更高;
– 还能方便地添加多路输出、任意波形、相位控制等功能。

模块功能简介

  • 使用 32 位相位累加器,实现高精度频率控制;
  • 通过查找 ROM 表输出正弦波形;
  • 支持双路输出:
    • 第一路用于反馈控制;
    • 第二路可以设置任意相位差(如 90°、180°);
  • 使用 MIF 文件加载 ROM 数据,方便修改波形;
  • 输出使用流水线寄存器,提高时序性能;
  • 时钟频率为 50MHz,支持高速输出。

关键公式说明

  1. 频率控制字计算:

Fword = \frac{F_{out} \times 2^{32}}{F_{clk}}

例如:要在 50MHz 系统时钟下输出 1kHz 信号:
Fword = \frac{1000 \times 2^{32}}{50 \times 10^6} \approx 85899

  1. 相位差与 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 是锁相环的核心实现模块,包含了完整的信号处理链路和控制逻辑。

主要功能特点:

  1. 双路DDS输出
    • 第一路用于反馈控制(nco_sin1
    • 第二路作为实际输出(nco_sin2),可设置相位差
  2. PID控制器
    • 采用定点数运算(Q16.16格式)
    • 支持比例(P)、积分(I)、微分(D)三项控制
    • 在250kHz频率下更新控制参数
  3. 信号处理链
    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 模块调用示例

为了方便使用,我们提供了一个简化的顶层封装示例。这个封装隐藏了内部复杂的参数配置,让用户可以直接调用:

设计思路:

  1. 接口简化:外部调用时不需要关心频率控制字、相位差等内部参数
  2. 参数固化:将常用的配置参数定义为常量,减少出错概率
  3. 兼容性:保持简洁的外部接口

常量参数配置:

// 频率控制字常量(10kHz@50MHz)
localparam [31:0] FREQ_CTRL_WORD_CONST = 32'd8589933;

// 相位差控制常量(约-18度相位差)
localparam signed [9:0] PHASE_DIFF_CONST = -10'sd50;

使用方式:

  1. 直接调用PLL_MODULE:如果需要动态调整频率和相位差
    PLL_MODULE u_pll (
       .FREQ_CTRL_WORD(your_freq_word),
       .PHASE_DIFF(your_phase_diff),
       // ... 其他信号
    );
    
  2. 使用封装的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. 参考

  1. 原理参考:
    https://blog.csdn.net/DBLLLLLLLL/article/details/84395583
    https://blog.csdn.net/weixin_43824941/article/details/118739186

  2. FIR参考:
    https://blog.csdn.net/qq_42839007/article/details/104354810

  3. Matlab FIR设计:
    https://blog.csdn.net/bleauchat/article/details/85136485
    https://www.cnblogs.com/cofin/p/10220648.html

  4. 书籍参考:
    《锁相环技术原理及FPGA实现》 杜勇
    《数字滤波器的MATLAB与FPGA实现-Altera/Verilog版(第二版)》 杜勇

  5. 社团资料:
    新版 FPGA 开发板使用说明书

评论

  1. .
    5 月前
    2025-7-23 15:11:30

    太有操作了xh学长

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇