简介:这份资源围绕Xilinx FPGA中PLL与MMCM的动态配置展开,面向需要处理复杂时钟需求的FPGA设计者与学习者,重点解决运行时调整时钟参数、相位对齐与抖动优化等实际问题。包内共55个文件,以Verilog源码、Tcl脚本、约束文件与C头文件为主,另含PDF说明文档、仿真波形与工程数据库,压缩包约1.02MB,覆盖PLLE2至PLLE4、MMCME2至MMCME4多个DRP模块的参考设计与仿真脚本。内容源自xapp888应用笔记,包含各时钟管理单元的DRP控制器、Testbench、综合与仿真流程脚本,以及QuestaSim仿真环境配置,便于读者对照官方文档理解动态重配置的完整实现路径。目前已有2070人学习下载,适合希望掌握Xilinx MMCM/PLL动态配置技术、提升时钟灵活性与系统稳定性的中高级开发者参考。
1. xapp888 里的 MMCM 动态重配:为什么一线工程师绕不开 DRP
做 XILINX FPGA 的同行大概率遇到过这种场景:板子已经贴片出货,客户突然要求把某路时钟从 100 MHz 改成 156.25 MHz,或者要在运行中切换视频像素时钟。重新综合、布局布线、出比特流、现场升级,一套流程下来少说半天,现场还不一定有条件。这时候 XAPP888 里那套 MMCM 动态重配置(DRP)方案就成了救命稻草——它让你在 FPGA 运行过程中,通过 DRP 接口直接改写 MMCM 的分频、倍频寄存器,不用重新烧录比特流就能把输出频率换掉。标题里的 xapp888_pll配置_mmcm 说的就是这件事:用 Xilinx 官方应用笔记 XAPP888 的思路,把 PLL/MMCM 的配置参数在运行时改掉。适合谁?适合已经跑通基本时钟、现在被“运行中切时钟”需求卡住的 FPGA 工程师,尤其是做视频、通信、测试仪器的朋友。下面我把这套方案从原理到落地拆开讲,包括寄存器怎么算、DRP 怎么接、哪些参数一动就翻车。
2. MMCM 动态重配的底层逻辑与 XAPP888 的寄存器模型
2.1 MMCM 内部到底有哪些可改的寄存器
MMCM 不是一块黑匣子,它内部有一组配置寄存器,上电时由比特流写入初值,之后可以通过 DRP(Dynamic Reconfiguration Port)端口读写。XAPP888 的核心价值,就是把这组寄存器的地址、位域、约束条件整理成了一张可查的表。关键寄存器包括:
| 寄存器名 | 作用 | 典型地址偏移 |
|---|---|---|
| CLKOUT0_REG | 控制 CLKOUT0 的 divide 和 duty cycle | 0x08 |
| CLKOUT1_REG | 控制 CLKOUT1 | 0x09 |
| CLKOUT2_REG | 控制 CLKOUT2 | 0x0A |
| CLKOUT3_REG | 控制 CLKOUT3 | 0x0B |
| CLKOUT4_REG | 控制 CLKOUT4 | 0x0C |
| CLKOUT5_REG | 控制 CLKOUT5 | 0x0D |
| DIVCLK_REG | 输入分频 D | 0x14 |
| CLKFBOUT_REG | 反馈分频 M | 0x15 |
| LOCK_REG1 | 锁定相关配置 | 0x18 |
| FILTER_REG1 | 环路滤波器 | 0x4E |
| POWER_REG | 电源相关 | 0x28 |
这些地址是 7 位 DRP 地址,实际访问时还要看你的 MMCM 是哪个系列。7 系列和 UltraScale 的地址布局不完全一样,XAPP888 主要覆盖 7 系列,UltraScale 需要参考对应的 UG 文档做微调。我一般会先把 XAPP888 的表格打印出来贴在工位上,改一个参数就对着表查一遍,比在 Vivado 里反复试快得多。
2.2 输出频率是怎么算出来的
MMCM 的输出频率公式不复杂,但很容易在 VCO 范围上踩坑:
Fvco = Fclkin / D * M Fout = Fvco / O其中 D 是输入分频(DIVCLK),M 是反馈分频(CLKFBOUT),O 是各输出通道的分频。约束条件有三个:Fvco 必须落在器件规定的 VCO 范围内(7 系列一般是 600 MHz 到 1200 MHz,具体看速度等级),Fout 不能超过输出缓冲的最大频率,D 和 M 的取值有合法范围(D 通常 1 到 106,M 通常 2 到 64,但实际可用值受 VCO 约束限制)。
举个例子,输入 100 MHz,想要 156.25 MHz 输出。先选 D=1,M=10,Fvco=1000 MHz,在范围内。然后 O = 1000/156.25 = 6.4,不是整数。这时候要么调 M,要么调 D。试 D=2,M=25,Fvco=1250 MHz,超了。试 D=1,M=12.5,不行。试 D=4,M=25,Fvco=625 MHz,O=4,正好。所以 D=4,M=25,O=4 是一组可行解。实际写寄存器时,D 和 O 的编码不是直接写数值,而是有特定的编码规则,XAPP888 里给了查找表。
2.3 DRP 接口的时序要求
DRP 是一个同步接口,信号包括 DADDR、DI、DO、DEN、DWE、DCLK、DRDY。写操作流程是:在 DCLK 上升沿把 DADDR 和 DI 准备好,拉高 DEN,DWE 拉高表示写,等 DRDY 拉高表示完成。读操作类似,DWE 拉低。注意 DRDY 不是每个周期都拉高,它只在操作完成后给一个脉冲。我见过有人用组合逻辑去等 DRDY,结果时序不收敛,DRP 读写全乱。正确做法是用状态机,在 DRDY 上升沿跳转状态。
提示:DRP 的 DCLK 可以用 MMCM 的输入时钟分频得到,但不要用 MMCM 自己的输出时钟去驱动 DRP,因为你在改 MMCM 的过程中输出时钟可能会失锁,导致 DRP 时钟也挂了。用独立的时钟源最稳。
3. 用 DRP 改 MMCM 频率的完整步骤与代码实现
3.1 从 Vivado 工程里提取初始配置
第一步不是写代码,而是先把你当前的 MMCM 配置从 Vivado 里导出来。打开综合后的设计,找到 MMCME2_ADV 或 MMCME4_ADV 原语,看它的属性:CLKIN1_PERIOD、DIVCLK_DIVIDE、CLKFBOUT_MULT_F、CLKOUT0_DIVIDE_F 等等。这些属性决定了上电时的初始寄存器值。你可以用 Tcl 命令直接导出:
# 在 Vivado Tcl Console 里执行,导出 MMCM 配置 set mmcm [get_cells -hier -filter {REF_NAME =~ MMCM*}] report_property $mmcm把输出保存下来,后面算新频率时要以这些值为基准。注意 CLKOUT0_DIVIDE_F 可以是小数,比如 6.5,但 DRP 寄存器里存的是整数编码,小数分频是通过 EDGE 和 PHASE 位组合实现的,XAPP888 里有专门的换算表。
3.2 计算新频率对应的寄存器值
假设初始配置是输入 100 MHz,D=1,M=10,CLKOUT0 分频 O=10,输出 100 MHz。现在要改成 156.25 MHz。按 2.2 节的算法,选 D=4,M=25,O=4。接下来要把这些数值转成寄存器编码。
DIVCLK_REG 的编码:D 值直接写二进制,但要注意位宽。7 系列的 DIVCLK_REG 是 8 位,低 6 位存 D 值,高 2 位保留。所以 D=4 写成 0x04。
CLKFBOUT_REG 的编码:M 值乘以 8 后取整,因为寄存器里存的是 M 的 1/8 精度。M=25,25*8=200,写成 0xC8。但 CLKFBOUT_REG 是 16 位寄存器,分两个字节,低字节 0xC8,高字节 0x00。
CLKOUT0_REG 的编码:O 值乘以 8,O=4,4*8=32,0x20。同样 16 位,低字节 0x20,高字节 0x00。但还要设置 EDGE 和 PHASE,默认情况下 EDGE=0,PHASE=0,所以高字节就是 0x00。
这些编码规则在 XAPP888 的 Table 3 到 Table 7 里有详细说明。我建议写一个 Python 脚本自动算,避免手算出错:
def calc_mmcm_regs(fclkin, fout, d, m, o): """ 计算 MMCM 寄存器值 fclkin: 输入频率 MHz fout: 目标输出频率 MHz d: 输入分频 m: 反馈分频 o: 输出分频 """ fvco = fclkin / d * m fout_actual = fvco / o print(f"VCO = {fvco} MHz, 实际输出 = {fout_actual} MHz") # 检查 VCO 范围 if fvco < 600 or fvco > 1200: print("警告:VCO 超出 7 系列范围 600-1200 MHz") # DIVCLK_REG divclk_reg = d & 0x3F print(f"DIVCLK_REG = 0x{divclk_reg:02X}") # CLKFBOUT_REG clkfbout_reg = int(m * 8) & 0xFFFF print(f"CLKFBOUT_REG = 0x{clkfbout_reg:04X}") # CLKOUT0_REG clkout0_reg = int(o * 8) & 0xFFFF print(f"CLKOUT0_REG = 0x{clkout0_reg:04X}") return divclk_reg, clkfbout_reg, clkout0_reg # 示例:100 MHz 输入,156.25 MHz 输出 calc_mmcm_regs(100, 156.25, 4, 25, 4)跑一下输出:
VCO = 625.0 MHz, 实际输出 = 156.25 MHz DIVCLK_REG = 0x04 CLKFBOUT_REG = 0x00C8 CLKOUT0_REG = 0x00203.3 写一个 DRP 状态机
DRP 读写不能靠组合逻辑硬等,必须用状态机。下面是一个简化的 Verilog 状态机,实现单次写操作:
module drp_writer ( input wire clk, input wire rst_n, input wire start, input wire [6:0] daddr, input wire [15:0] di, output reg [15:0] do, output reg done, output reg [6:0] daddr_out, output reg [15:0] di_out, output reg den, output reg dwe, input wire drdy ); localparam IDLE = 2'd0; localparam WRITE = 2'd1; localparam WAIT = 2'd2; reg [1:0] state; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; den <= 0; dwe <= 0; done <= 0; end else begin case (state) IDLE: begin done <= 0; if (start) begin daddr_out <= daddr; di_out <= di; den <= 1; dwe <= 1; state <= WRITE; end end WRITE: begin den <= 0; dwe <= 0; state <= WAIT; end WAIT: begin if (drdy) begin done <= 1; state <= IDLE; end end endcase end end endmodule逻辑说明:IDLE 状态等待 start 信号,收到后把地址和数据放到总线上,拉高 DEN 和 DWE。下一个周期进入 WRITE,拉低 DEN 和 DWE,因为 DRP 要求 DEN 只在一个周期内有效。然后进入 WAIT,等 DRDY 拉高后置 done 并回到 IDLE。参数说明:daddr 是 7 位地址,di 是 16 位数据,drdy 来自 MMCM 的 DRDY 输出。注意这个状态机没有处理读操作,读操作只需要把 DWE 拉低,其他一样。
3.4 上板验证:怎么确认频率真的变了
改完寄存器后,怎么知道输出频率对了?最直接的办法是用示波器或频率计测。如果没有,可以用 FPGA 内部的计数器测频:用一个已知的参考时钟去计数待测时钟的周期数。下面是一个简单的测频模块:
module freq_counter ( input wire ref_clk, // 已知参考时钟,比如 100 MHz input wire test_clk, // 待测时钟 input wire rst_n, output reg [31:0] freq_result // 单位:Hz ); reg [31:0] count_ref; reg [31:0] count_test; reg test_clk_d1, test_clk_d2; wire test_clk_rise; always @(posedge ref_clk or negedge rst_n) begin if (!rst_n) begin count_ref <= 0; count_test <= 0; test_clk_d1 <= 0; test_clk_d2 <= 0; end else begin test_clk_d1 <= test_clk; test_clk_d2 <= test_clk_d1; if (count_ref < 100_000_000) begin // 计数 1 秒 count_ref <= count_ref + 1; if (test_clk_rise) count_test <= count_test + 1; end else begin freq_result <= count_test; count_ref <= 0; count_test <= 0; end end end assign test_clk_rise = test_clk_d1 & ~test_clk_d2; endmodule这个模块用 100 MHz 参考时钟计数 1 秒,同时数待测时钟的上升沿,结果就是频率值。注意待测时钟要先做两级同步再检测上升沿,否则亚稳态会导致计数错误。实际使用时,参考时钟的精度决定了测量精度,如果参考时钟本身有偏差,结果也会偏。
4. 避坑与排查:MMCM 动态重配的 5 个血泪教训
4.1 改完寄存器后 MMCM 失锁,输出时钟直接没了
现象:写完 DIVCLK_REG 或 CLKFBOUT_REG 后,LOCKED 信号拉低,输出时钟停振。原因:MMCM 在改变 M 或 D 的瞬间,VCO 频率会跳变,如果跳变幅度超过 VCO 的锁定范围,环路就失锁了。解决:不要一次性改所有寄存器,先改输出分频 O,再改 M,最后改 D。每次改完等 LOCKED 重新拉高再改下一个。如果必须同时改,可以先复位 MMCM,但复位会导致输出中断,不适合运行中切换。
4.2 DRP 写进去的值读出来不对
现象:写了一个寄存器,再读回来发现值不一样。原因:有些寄存器是只读的,或者写入的值会被硬件截断。比如 CLKOUT0_REG 的高字节里有些位是保留的,写 1 会被忽略。解决:写之前先读一次,把保留位保持原值,只改需要改的位。XAPP888 的表格里标注了哪些位是 R/W,哪些是 RO。
4.3 频率算出来对,但实际输出差很多
现象:按公式算的 Fout 是 156.25 MHz,实测只有 150 MHz 左右。原因:输入时钟的频率不准,或者 MMCM 的输入分频 D 没有生效。检查 CLKIN1_PERIOD 属性是否和实际输入频率一致,如果不一致,Vivado 综合时会按错误的周期算,导致实际输出偏差。解决:在约束文件里用 create_clock 明确指定输入时钟频率,不要依赖默认值。
4.4 DRDY 一直不拉高,状态机卡死
现象:状态机停在 WAIT 状态,DRDY 始终为低。原因:DRP 的 DCLK 频率太高,超过了 MMCM 的 DRP 时钟上限。7 系列的 DRP 时钟最高 100 MHz,UltraScale 可以到 200 MHz,但实际用 50 MHz 左右最稳。解决:给 DRP 单独分一个低频时钟,不要直接用系统时钟。另外检查 DEN 的脉冲宽度,太窄可能被漏采。
4.5 动态切换时输出时钟有毛刺
现象:切换频率的瞬间,输出时钟上出现窄脉冲,下游逻辑误触发。原因:MMCM 在改分频比时,输出时钟的相位会突变,产生毛刺。解决:如果下游对毛刺敏感,可以在切换前先把输出时钟切换到备用时钟(比如 BUFGMUX),切完再切回来。或者用 MMCM 的 HOLD 功能,在改寄存器时保持输出不变,改完再释放。HOLD 的具体用法参考 XAPP888 的 Section 4。
5. 进阶技巧:用 DRP 做多频率扫描与在线校准
前面讲的都是单次切换,实际项目中更常见的是需要在一组频率之间来回切,比如测试仪器要扫频。这时候可以把所有目标频率的寄存器值预先算好,存到 ROM 里,用状态机依次写入。下面是一个多频率切换的状态机片段:
// 预存 4 组频率的寄存器值 reg [15:0] divclk_rom [0:3]; reg [15:0] clkfbout_rom [0:3]; reg [15:0] clkout0_rom [0:3]; initial begin // 100 MHz divclk_rom[0] = 16'h0001; clkfbout_rom[0] = 16'h0050; // M=10, 10*8=80=0x50 clkout0_rom[0] = 16'h0050; // O=10, 10*8=80=0x50 // 156.25 MHz divclk_rom[1] = 16'h0004; clkfbout_rom[1] = 16'h00C8; // M=25, 25*8=200=0xC8 clkout0_rom[1] = 16'h0020; // O=4, 4*8=32=0x20 // 200 MHz divclk_rom[2] = 16'h0001; clkfbout_rom[2] = 16'h0060; // M=12, 12*8=96=0x60 clkout0_rom[2] = 16'h0030; // O=6, 6*8=48=0x30 // 50 MHz divclk_rom[3] = 16'h0001; clkfbout_rom[3] = 16'h0060; // M=12 clkout0_rom[3] = 16'h00C0; // O=24, 24*8=192=0xC0 end切换时按顺序写 DIVCLK、CLKFBOUT、CLKOUT0,每写一个等 DRDY,然后等 LOCKED 拉高再写下一个。注意 200 MHz 那组,VCO=1200 MHz,正好在 7 系列的上限,如果速度等级不够可能会失锁,实际用的时候留点余量,比如降到 190 MHz。
在线校准是另一个实用技巧:如果发现输出频率有偏差,可以读回当前寄存器值,和理论值对比,算出误差后微调 M 或 O。比如实测 155 MHz,目标 156.25 MHz,误差 0.8%,可以把 M 从 25 调到 25.2,但 M 只能是整数或半整数(通过 CLKFBOUT_FRAC 实现),所以更实际的做法是调 O 的小数分频。7 系列的 CLKOUT0_DIVIDE_F 支持 1/8 精度,O=4.032 对应 156.25/1554=4.032,编码时 4.0328=32.256,取整 32,实际输出 156.25 MHz,误差在可接受范围内。
我自己的习惯是:每次改完 MMCM 配置,先用 freq_counter 测一遍,确认频率对了再往下走。如果不对,先读回寄存器看写入是否成功,再检查 VCO 范围,最后才怀疑时钟源。这套流程帮我省了很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取