> 技术文档 > 利用Vivado中的Clocking Wizard实现动态调整输出时钟的频率和相位以及相关应用

利用Vivado中的Clocking Wizard实现动态调整输出时钟的频率和相位以及相关应用


开发背景

  • 资源连接:Clocking Wizard v6.0手册pg065-clk-wiz

  • 物理硬件平台:Xilinx Artix-7 FPGA A704

  • 开发工具:VsCode编译器,Vivado综合布局,Modelsim仿真,Verilog开发语言

        博主为了准备电赛,实现其中一个扫频的功能,便利用Vivado中现成的IP核实现动态调整锁相环输出的频率和相位。过程中发现现有的资料较少遇到不少问题,便以此写一篇文章为记录,也可以供大家参考。有问题的地方劳烦大家指出,也可以进行交流。

开发思路

        由于我需要从88MHz-108MHz,并且以0.1MHz(100K)的频率进行扫频,再阅读手册15-19页其相关寄存器,发现Clocking Wizard其中有一个公共寄存器控制所有时钟输出频率和相位后,各自时钟还有相关寄存器自行控制。也就是说输入频率,先经过公共的整数+分数倍频(如倍频8.125),然后经过整数分频后,最后经过每个时钟各自的分频才会输出。(注意这里特别提到了倍频和分频是整数还是包含分数,分数部分只能为”125“的倍数,倍频只有唯一一次在公共寄存器,普通时钟自身没有倍频只有整数分频,特别的:第一个时钟有分数分频)由于我只需要一路时钟输出,所有我选择了第一个时钟。

        所有我们为了输出时钟更加的精准,我们需要计算每个频率的相关参数,但是由于硬件描述语言不便于计算相关参数,并且目标频率是有步进的(虽然是一个范围),由此我决定利用Python脚本生成相关参数,再作为COE文件导入工程,用ROM存放直接读取,这样子更加便捷并且时许上更简单。

        最后利用Vivado现有的Clocking,用AXI4Lite协议(握手协议)来读写其内部寄存器,从而达到改变频率和相位的效果。

        (以下截出了部分的寄存器,其它寄存器描述大差不差,英语不好可以翻译一下。)

        这里的C_BASEADDR是基准地址,是内部自己控制的,不需要我们控制,我们只需要控制0x200这个公共寄存器,由于相位我们单独控制就不需要控制0x204了,另外时钟0控制频率的寄存器是0x208,往下翻发现时钟1是0x214,时钟2是0x220……所以我们在写代码的时候可以以时钟0的频率寄存器为准,地址可以是0x208+时钟号×0x00C(就是按12递增),同样的道理,相位寄存器也可以这样写。

        写完寄存器后,要记得写重载寄存器0x25C,Bit[0]位表示要重新加载,Bit[1]在控制重新加载默认(也就是在Vivado的UI界面配置的初始化状态),还是加载你动态写入的寄存器。加载完成后,也就是Locked拉高,内部会重新将Bit[0]进行复位成0;

        这个是要用的Clocking的初始化界面,相信大家经常会用到。

具体步骤

参数计算

        这里是使用Python以及输出时钟1,我的系统输入时钟为50MHz,所以我算的五个参数分别是:(有点个人口语描述)

  • 整数倍频(对所有时钟)
  • 分数倍频(对所有时钟)
  • 整数分频(对所有时钟)
  • 整数分频(对第一个时钟)
  • 分数分频(对第一个时钟)

        要注意,这里凡是分数,都得是”125“的倍数,例如8.125倍频,4.250分频。

脚本一

# -*- coding: utf-8 -*-import mathdef find_best_params(target_freq, clk_in=50.0): \"\"\" 计算最接近目标频率的倍频和分频参数。 :param target_freq: 目标频率 (MHz) :param clk_in: 输入时钟频率 (MHz) :return: (整数倍频, 小数倍频, 一级整数分频, 二级整数分频, 二级小数分频) \"\"\" frac_unit = 0.125 best = None min_err = float(\'inf\') # 预先生成所有可能的小数部分,避免重复计算 frac_list = [round(i * frac_unit, 3) for i in range(0, 8)] for mult_int in range(1, 50): # 适当减小上限,加速 for mult_frac in frac_list: mult = mult_int + mult_frac for div1 in range(1, 5): for div2 in range(1, 15): # 适当减小上限,加速  for div_frac in frac_list: div = div2 + div_frac freq = clk_in * mult / div1 / div err = abs(freq - target_freq) if err < min_err: min_err = err best = ( mult_int, mult_frac, div1, div2, div_frac ) if min_err < 1e-6: return best # 直接返回,极大加速 return bestdef format_params(freq, params): \"\"\" 按指定格式输出参数 \"\"\" mult_int, mult_frac, div1, div2, div_frac = params return f\"\"\"{freq}:frq_mult_int = {mult_int}frq_mult_float = {mult_frac}frq_div_int_all = {div1}frq_div_int = {div2}frq_div_float = {div_frac}\"\"\"if __name__ == \"__main__\": # 目标频率列表,可自行修改 target_freqs = [88, 88.1, 88.2, 88.3, 88.4, 88.5, 88.6, 88.7, 88.8, 88.9, 89, 89.1, 89.2, 89.3, 89.4, 89.5, 89.6, 89.7, 89.8, 89.9, 90, 90.1, 90.2, 90.3, 90.4, 90.5, 90.6, 90.7, 90.8, 90.9, 91, 91.1, 91.2, 91.3, 91.4, 91.5, 91.6, 91.7, 91.8, 91.9, 92, 92.1, 92.2, 92.3, 92.4, 92.5, 92.6, 92.7, 92.8, 92.9, 93, 93.1, 93.2, 93.3, 93.4, 93.5, 93.6, 93.7, 93.8, 93.9, 94, 94.1, 94.2, 94.3, 94.4, 94.5, 94.6, 94.7, 94.8, 94.9, 95, 95.1, 95.2, 95.3, 95.4, 95.5, 95.6, 95.7, 95.8, 95.9, 96, 96.1, 96.2, 96.3, 96.4, 96.5, 96.6, 96.7, 96.8, 96.9, 97, 97.1, 97.2, 97.3, 97.4, 97.5, 97.6, 97.7, 97.8, 97.9, 98, 98.1, 98.2, 98.3, 98.4, 98.5, 98.6, 98.7, 98.8, 98.9, 99, 99.1, 99.2, 99.3, 99.4, 99.5, 99.6, 99.7, 99.8, 99.9, 100, 100.1, 100.2, 100.3, 100.4, 100.5, 100.6, 100.7, 100.8, 100.9, 101, 101.1, 101.2, 101.3, 101.4, 101.5, 101.6, 101.7, 101.8, 101.9, 102, 102.1, 102.2, 102.3, 102.4, 102.5, 102.6, 102.7, 102.8, 102.9, 103, 103.1, 103.2, 103.3, 103.4, 103.5, 103.6, 103.7, 103.8, 103.9, 104, 104.1, 104.2, 104.3, 104.4, 104.5, 104.6, 104.7, 104.8, 104.9, 105, 105.1, 105.2, 105.3, 105.4, 105.5, 105.6, 105.7, 105.8, 105.9, 106, 106.1, 106.2, 106.3, 106.4, 106.5, 106.6, 106.7, 106.8, 106.9, 107, 107.1, 107.2, 107.3, 107.4, 107.5, 107.6, 107.7, 107.8, 107.9, 108] with open(\"freq_params.txt\", \"w\", encoding=\"utf-8\") as f: for freq in target_freqs: params = find_best_params(freq) f.write(format_params(freq, params))

这个脚本就是生成相关参数,并且将其以如下格式输出到freq_params.txt文件里面,经过验证它的精度可以去到小数点后三位,所以已经满足了我比赛的要求,看官也可以自己计算一下(50MHz作为输入)。接下来,要将他们按照不同参数分到不同的ROM里面,就需要就行归类。并且在开头加上COE文件的两行,这就要利用脚本二了。

  • MEMORY_INITIALIZATION_RADIX=10;
  • MEMORY_INITIALIZATION_VECTOR=
88.1:
frq_mult_int = 4
frq_mult_float = 0.625
frq_div_int_all = 1
frq_div_int = 2
frq_div_float = 0.625

脚本二

# -*- coding: utf-8 -*-import mathdef find_best_params(target_freq, clk_in=50.0): \"\"\" 计算最接近目标频率的倍频和分频参数。 :param target_freq: 目标频率 (MHz) :param clk_in: 输入时钟频率 (MHz) :return: (整数倍频, 小数倍频, 一级整数分频, 二级整数分频, 二级小数分频) \"\"\" frac_unit = 0.125 best = None min_err = float(\'inf\') # 预先生成所有可能的小数部分,避免重复计算 frac_list = [round(i * frac_unit, 3) for i in range(0, 8)] for mult_int in range(1, 50): # 适当减小上限,加速 for mult_frac in frac_list: mult = mult_int + mult_frac for div1 in range(1, 5): for div2 in range(1, 15): # 适当减小上限,加速  for div_frac in frac_list: div = div2 + div_frac freq = clk_in * mult / div1 / div err = abs(freq - target_freq) if err < min_err: min_err = err best = ( mult_int, mult_frac, div1, div2, div_frac ) if min_err < 1e-6: return best # 直接返回,极大加速 return bestdef format_params(freq, params): \"\"\" 按指定格式输出参数 \"\"\" mult_int, mult_frac, div1, div2, div_frac = params return f\"\"\"{freq}:frq_mult_int = {mult_int}frq_mult_float = {mult_frac}frq_div_int_all = {div1}frq_div_int = {div2}frq_div_float = {div_frac}\"\"\"if __name__ == \"__main__\": # 目标频率列表,可自行修改 target_freqs = [88, 88.1, 88.2, 88.3, 88.4, 88.5, 88.6, 88.7, 88.8, 88.9, 89, 89.1, 89.2, 89.3, 89.4, 89.5, 89.6, 89.7, 89.8, 89.9, 90, 90.1, 90.2, 90.3, 90.4, 90.5, 90.6, 90.7, 90.8, 90.9, 91, 91.1, 91.2, 91.3, 91.4, 91.5, 91.6, 91.7, 91.8, 91.9, 92, 92.1, 92.2, 92.3, 92.4, 92.5, 92.6, 92.7, 92.8, 92.9, 93, 93.1, 93.2, 93.3, 93.4, 93.5, 93.6, 93.7, 93.8, 93.9, 94, 94.1, 94.2, 94.3, 94.4, 94.5, 94.6, 94.7, 94.8, 94.9, 95, 95.1, 95.2, 95.3, 95.4, 95.5, 95.6, 95.7, 95.8, 95.9, 96, 96.1, 96.2, 96.3, 96.4, 96.5, 96.6, 96.7, 96.8, 96.9, 97, 97.1, 97.2, 97.3, 97.4, 97.5, 97.6, 97.7, 97.8, 97.9, 98, 98.1, 98.2, 98.3, 98.4, 98.5, 98.6, 98.7, 98.8, 98.9, 99, 99.1, 99.2, 99.3, 99.4, 99.5, 99.6, 99.7, 99.8, 99.9, 100, 100.1, 100.2, 100.3, 100.4, 100.5, 100.6, 100.7, 100.8, 100.9, 101, 101.1, 101.2, 101.3, 101.4, 101.5, 101.6, 101.7, 101.8, 101.9, 102, 102.1, 102.2, 102.3, 102.4, 102.5, 102.6, 102.7, 102.8, 102.9, 103, 103.1, 103.2, 103.3, 103.4, 103.5, 103.6, 103.7, 103.8, 103.9, 104, 104.1, 104.2, 104.3, 104.4, 104.5, 104.6, 104.7, 104.8, 104.9, 105, 105.1, 105.2, 105.3, 105.4, 105.5, 105.6, 105.7, 105.8, 105.9, 106, 106.1, 106.2, 106.3, 106.4, 106.5, 106.6, 106.7, 106.8, 106.9, 107, 107.1, 107.2, 107.3, 107.4, 107.5, 107.6, 107.7, 107.8, 107.9, 108] with open(\"freq_params.txt\", \"w\", encoding=\"utf-8\") as f: for freq in target_freqs: params = find_best_params(freq) f.write(format_params(freq, params))

        这个脚本就是帮我们将不同参数进行分类成五个COE文件,再按照频率从88-108从小到大的顺序进行排列,方便我们按照顺序的地址进行读取写入,扫频。这里根据手册要求,小数部分要乘以1000,因为其实小数部分和整数部分是写在同一组32位寄存器的不同位置,所以我在这个脚本顺便进行了。得到了五个COE文件的其中一个文件如下:

MEMORY_INITIALIZATION_RADIX=10;MEMORY_INITIALIZATION_VECTOR=500,625,625,375,875,125,125,250,625,0,125,375,500,875,125,375,625,0,125,250,250,750,375,125,375,625,500,750,125,500,375,875,375,250,250,875,250,750,375,625,750,375,500,0,125,625,375,625,625,625,625,750,250,625,625,375,375,875,250,625,875,875,250,875,625,625,625,625,875,625,375,125,125,875,750,875,0,250,500,625,0,875,125,500,750,125,500,625,625,250,125,750,0,0,750,875,625,250,875,125,125,375,250,125,375,625,375,0,500,500,375,500,625,625,625,875,375,375,875,0,0,0,625,625,625,125,875,875,375,0,625,0,0,500,125,375,625,375,250,125,375,375,625,250,375,125,750,500,500,750,875,250,375,375,0,875,750,0,375,625,500,875,0,250,500,125,750,625,375,375,625,875,250,375,375,375,375,125,250,125,625,875,250,625,625,625,375,875,250,750,375,375,375,875,125,375,0,500,0,125,750;

IP核配置

        这里使用的实现方式是MMCM,可以说MMCM是增强版的PLL,其对输出时钟的控制灵活性和精度更大。我的输入时钟是单端的50MHz系统时钟,输出选择了时钟1,初始化88MHz。

  • Frequency Synthesis:表示可以生成不同频率的时钟
  • Phase Alignment:已知输入时钟的频率和相位
  • Dynamic Reconfig:表示可以创新改写寄存器
  • AXI4Lite:是一种接口协议,其实就是握手信号,也可以选择DRP不过相关的寄存器地址就会发生改变,并且文档还是另一个,当时最开始是使用这个协议,一直没效果便改成了AXI4Lite,其实这个是对DRP的再一次封装,另一个文档连接xapp888_7Series_DynamicRecon
  • Phase Duty Cycle Config:如果要修改相位,得勾上这个

        可以看到,配置完后,左边IP Symbol界面展示了现有的端口,其中展开可以看到AXI4Lite协议的相关端口。其中各个端口的含义可以查看一下手册里面的说明,简单来说其实就是写地址+写数据+写有效位标识以及他们各自的握手信号,写完一次后,还有应答信号以及应答信号的握手信号。(特别的读写是分开的)

驱动Clocking Wizard模块

        可以发现,我们可以将修改寄存器作为一个module模块,然后用一个上级模块控制写入的参数和顺序。

time_tree_dy.v

        这个模块是用来控制读写的,大致的思路是使用二段式的状态机,会先计算总共要写入的次数,然后先写公共寄存器,然后依次写各自时钟的频率寄存器,在依次写各自时钟的相位寄存器,最后写重加载寄存器。(我这里预留了俩路信号,是到处验证的时候为了做对比,可以通过端口定义的clk_control_num配合Vivado的UI界面勾选时钟的数量,保证整个代码的通用性)

        不过,为了代码写的方便,所以这里我的端口有点多,显得有点唐,不过在上级控制模块会有5个ROM读取上述的五个参数,可以直接传递给对应端口,所以怎么方便怎么来吧。

        另外一些特别的地方我都有注释。

/*==============================================* Function Name : time_tree_dy.v* Description : 该模块是为了动态设计时钟树,*  主要听从于STM32F4的需求,*  通过动态配置实现不同频率的时钟输出* input port : 详细接口见下方注释* output port : 详细接口见下方注释* Author : ADBD//==============================================*/module time_tree_dy( input wire sys_clk, // 系统时钟 input wire rst_n, // 低电平复位 input wire enable, // 模块启动信号 input wire valid, // 输入有效信号 output reg ready, // 输出就绪信号,表示模块已准备好接收数据 input wire [2:0] clk_control_num,// 需要控制的时钟总数量 input wire [2:0] clk_choise, // 时钟选择信号,0表示全部时钟,  // 1表示时钟1,2表示时钟2,3表示时钟3 input wire  phase_enable, // 相位使能信号,1-使能相位配置,0-不使能相位配置 input wire [7:0] frq_mult_int, // 频率倍频系数,正数部分,对于全部时钟 input wire [9:0] frq_mult_float, // 频率倍频系数,小数部分,对于全部时钟 input wire [7:0] frq_div_int, // 频率分频系数,整数部分,复用,可对所有时钟,也可以对某个时钟 input wire [9:0] frq_div_float, // 频率分频系数,小数部分,对于时钟1才有小数分频 input wire [31:0] frq_phase_value,// 频率相位值,对各自的时钟进行设置 output wire  accomplish, // 表示写寄存器全部完成 output wire  error_sign, // 错误信号 output wire  clk_out1, // 输出时钟1 // output wire  clk_dac, // 输出时钟2 // output wire  clk_fir, // 输出时钟3 output wire  locked // 锁定信号);reg enable_r; // 输入有效信号寄存器wire [3:0] cnt_goal;reg [3:0] cnt; // 计数器,用于控制时钟选择reg [10 : 0] s_axi_awaddr; // AXI写地址reg s_axi_awvalid; // AXI写地址有效信号wire s_axi_awready;  // AXI写地址就绪信号reg [31 : 0] s_axi_wdata; // AXI写数据reg [3 : 0] s_axi_wstrb; // AXI写数据掩码,控制写入哪几个字节reg s_axi_wvalid; // AXI写数据有效wire s_axi_wready; // AXI写数据就绪信号wire [1 : 0] s_axi_bresp; // AXI写响应信号,00表示成功,01表示写地址错误,10表示写数据错误,11表示其他错误reg [1 : 0] s_axi_bresp_r; // AXI写响应信号的寄存器wire s_axi_bvalid; // AXI写响应有效信号reg s_axi_bready; // AXI写响应就绪信号// 初始化AXI接口读取相关的寄存器reg [10 : 0] s_axi_araddr = 11\'d0; // AXI读地址reg s_axi_arvalid = 1\'b0; // AXI读地址有效信号wire s_axi_arready;  // AXI读地址就绪信号wire [31 : 0] s_axi_rdata;  // AXI读数据wire [1 : 0] s_axi_rresp; // AXI读响应信号,00表示成功,01表示读地址错误,10表示读数据错误,11表示其他错误wire s_axi_rvalid;  // AXI读响应有效信号reg s_axi_rready = 1\'b0; // AXI读响应就绪信号// 状态机状态定义 // 注意:第一个一定要写共同的倍频和分频,然后依次写n个时钟的分频,再依次写n个时钟的相位// 顺序:IDLE(空闲等待请求)-> START(收到请求,并且将对所有时钟控制的数据进行加载) -> WRITE(将有效信号拉高) -> DATA_Accept(等待AXI写地址和数据有效信号)// -> WRITE_OVER(等待响应信号)-> ERROR(判断是否错误) -> WRITE(在enable还在的情况下,等待接收新的数据继续读写) // -> LOAD(检查错误后使能加载寄存器)localparam IDLE = 4\'d0;// 空闲状态localparam START = 4\'d1;// 数据加载localparam WRITE = 4\'d2;// 有效拉高localparam DATA_Accept = 4\'d3;// 等待就绪信号localparam Response = 4\'d4;// 等待响应信号localparam ERROR = 4\'d5;// 判断是否错误localparam LOAD = 4\'d6;// 写加载寄存器reg [3:0] axi_cstate,axi_nstate; // 控制AXI读写的状态机// 状态机状态定义wire IDLE_START = enable_r; // IDLE状态下的有效信号wire START_WRITE = (axi_cstate == START) && valid; // START状态下,并且接收到有效信号wire WAIT_Accept = (axi_cstate == WRITE);  // 等待AXI写地址和数据有效信号wire Accept_Response = (axi_cstate == DATA_Accept) && (!s_axi_awvalid && !s_axi_wvalid);  // 等待握手协议完成,当地址和数据都传输完成wire Response_ERROR = (axi_cstate == Response) && (s_axi_bvalid && s_axi_bready);  // 等待响应信号握手协议完成wire ERROR_LOAD = (axi_cstate == ERROR) && (s_axi_bresp_r == 2\'d0) && cnt == cnt_goal; // 判断完无错误,并且要写的寄存器都已经写完进行数据加载wire ERROR_START = (axi_cstate == ERROR) && s_axi_bresp_r == 2\'d0 && cnt < cnt_goal;  // 判断无错误,并且计数器小于目标计数器,表示还有时钟需要配置wire ERROR_IDLE = (axi_cstate == ERROR) && (s_axi_bresp_r != 2\'d0 || s_axi_awaddr == 11\'h25c); // 判断完有错误直接回到IDLE状态,加载数据直接回到IDLE状态wire LOAD_WRITE = (axi_cstate == LOAD);assign error_sign = (axi_cstate == ERROR) && (s_axi_bresp_r != 2\'d0); // 错误信号,当状态机处于错误状态时为高assign cnt_goal = enable_r ? phase_enable ? (clk_control_num << 1) + 1 : clk_control_num + 1: 4\'d0; // 计数器目标值,根据使能信号和相位使能信号计算assign accomplish = axi_cstate == LOAD;// 使能信号寄存器always@(posedge sys_clk or negedge rst_n)begin if (!rst_n) begin enable_r <= 1\'b0; // 复位时清零 end else if (enable) begin enable_r <= 1\'d1; // 保持输入有效信号 end else begin enable_r <= 1\'b0; // 输入无效时清零 endend// 二段式状态机always@(posedge sys_clk or negedge rst_n)begin if (!rst_n)begin axi_cstate <= IDLE; end else begin axi_cstate <= axi_nstate; endend// 计数处理always @(posedge sys_clk or negedge rst_n) begin if (!rst_n) begin cnt <= 4\'d0; // 复位时清零 end else if (axi_cstate == START && valid) begin cnt <= cnt + 1\'b1; // 计数器自增 end else if (axi_cstate == LOAD) begin cnt <= 4\'d0; // 重置计数器 end else begin cnt <= cnt; // 保持计数器不变 endend// 读数据有效回馈always @(posedge sys_clk or negedge rst_n) begin if (!rst_n)begin ready <= 1\'b0; // 复位时清零 end else if (axi_cstate == IDLE) begin ready <= 1\'b0; // 空闲状态下清零 end else if (Accept_Response)begin ready <= 1\'b1; // 有效信号拉高,表示模块准备好接收数据 end else begin ready <= 1\'b0; // 如果没有有效信号,保持就绪状态 endend// 数据缓存并处理always@(posedge sys_clk or negedge rst_n)begin if(!rst_n)begin s_axi_awaddr <= 11\'d0; s_axi_wdata <= 32\'d0; s_axi_wstrb  (cnt_goal - 1)>>1 && phase_enable)begin// 写频率 s_axi_awaddr <= 11\'h20C + ((clk_choise - 1) * 4\'d12); s_axi_wdata <= (frq_phase_value << 10) - (frq_phase_value << 4) - (frq_phase_value << 3);// ×1000 s_axi_wstrb <= 4\'b1111; end else begin if (clk_choise == 3\'b000)begin  s_axi_awaddr <= 11\'h200;  s_axi_wdata <= {6\'d0,frq_mult_float,frq_mult_int,frq_div_int};  s_axi_wstrb <= 4\'b0111; end else if (clk_choise == 3\'b001) begin  s_axi_awaddr <= 11\'h208;// 确定时钟分频的寄存器地址  s_axi_wdata <= {14\'d0,frq_div_float,frq_div_int}; // 将整数和小数部分拼接成32位数据  s_axi_wstrb <= 4\'b0011; end else begin  s_axi_awaddr <= 11\'h208 + ((clk_choise - 1) * 4\'d12);// 确定时钟分频的寄存器地址  s_axi_wdata <= {24\'d0,frq_div_int}; // 只有整数分频  s_axi_wstrb <= 4\'b0001; end end end else begin s_axi_awaddr <= s_axi_awaddr; // 保持地址不变 s_axi_wdata <= s_axi_wdata; // 保持数据不变 s_axi_wstrb <= s_axi_wstrb; // 保持掩码不变 end end else if (axi_cstate == LOAD)begin s_axi_awaddr <= 11\'h25C; s_axi_wdata <= 32\'d3; s_axi_wstrb <= 4\'d1; end else begin s_axi_awaddr <= s_axi_awaddr; s_axi_wdata <= s_axi_wdata; s_axi_wstrb <= s_axi_wstrb; endend// 控制握手协议always@(posedge sys_clk or negedge rst_n)begin if (!rst_n) begin s_axi_awvalid <= \'b0; s_axi_wvalid <= \'b0; end else if (axi_cstate == WRITE) begin s_axi_awvalid <= \'b1; s_axi_wvalid <= \'b1; end else if (axi_cstate == DATA_Accept)begin if (s_axi_awready)begin s_axi_awvalid <= \'b0; end else begin s_axi_awvalid <= s_axi_awvalid; end if (s_axi_wready)begin s_axi_wvalid <= \'b0; end else begin s_axi_wvalid <= s_axi_wvalid; end end else begin s_axi_awvalid <= s_axi_awvalid; s_axi_wvalid <= s_axi_wvalid; endend// 响应信号处理always@(posedge sys_clk or negedge rst_n)begin if (!rst_n) begin s_axi_bready <= \'b0; s_axi_bresp_r <= 2\'b00; end else if (axi_cstate == START || axi_cstate == LOAD) begin s_axi_bready <= \'b0; s_axi_bresp_r <= 2\'b00; // 响应信号寄存器初始化 end else if (axi_cstate == Response) begin s_axi_bready <= \'b1; // 响应信号就绪 if(s_axi_bvalid)begin s_axi_bresp_r <= s_axi_bresp; end else begin s_axi_bresp_r <= s_axi_bresp_r; end end else begin s_axi_bready <= s_axi_bready; s_axi_bresp_r <= s_axi_bresp_r; endend// 第二级状态机控制always@(*)begin case(axi_cstate) IDLE:if (IDLE_START) begin axi_nstate = START; end else begin axi_nstate = axi_cstate; end START:if(START_WRITE) begin axi_nstate = WRITE; end else begin axi_nstate = axi_cstate; end WRITE:if(WAIT_Accept) begin axi_nstate = DATA_Accept; end else begin axi_nstate = axi_cstate; end DATA_Accept:if(Accept_Response) begin axi_nstate = Response; end else begin axi_nstate = axi_cstate; end Response:if (Response_ERROR)begin axi_nstate = ERROR; end else begin axi_nstate = axi_cstate; end ERROR:if(ERROR_IDLE)begin axi_nstate = IDLE; end else if(ERROR_LOAD) begin axi_nstate = LOAD; end else if (ERROR_START)begin axi_nstate = START; end else begin axi_nstate = axi_cstate; end LOAD:if (LOAD_WRITE)begin axi_nstate = WRITE; // 进入WRITE状态,控制寄存器重载 end else begin axi_nstate = axi_cstate; end default: begin axi_nstate = IDLE; // 默认状态,防止状态机进入未知状态 end endcaseendclk_module clk_module_inst ( .s_axi_aclk(sys_clk), .s_axi_aresetn(rst_n), .s_axi_awaddr(s_axi_awaddr), .s_axi_awvalid(s_axi_awvalid), .s_axi_awready(s_axi_awready), .s_axi_wdata(s_axi_wdata), .s_axi_wstrb(s_axi_wstrb), .s_axi_wvalid(s_axi_wvalid), .s_axi_wready(s_axi_wready), .s_axi_bresp(s_axi_bresp), .s_axi_bvalid(s_axi_bvalid), .s_axi_bready(s_axi_bready), .s_axi_araddr(s_axi_araddr), .s_axi_arvalid(s_axi_arvalid), .s_axi_arready(s_axi_arready), .s_axi_rdata(s_axi_rdata), .s_axi_rresp(s_axi_rresp), .s_axi_rvalid(s_axi_rvalid), .s_axi_rready(s_axi_rready), .clk_out1(clk_out1), // .clk_out2(clk_adc), // .clk_out3(clk_fir), .locked(locked), .clk_in1(sys_clk) );endmodule 

MMCM_Control.v

        这个模块控制通过握手信号进行参数的传输,和ROM的按顺序读取。也是用状态机写的。

module MMCM_Control( input wire sys_clk, input wire rst_n, output wire clk_out1, output reg [7:0] frq, input wire frq_search_over, input wire clc_accompish, output wire locked);localparam IDLE = 0, START = 1, WRITE = 2, WAIT = 3, Once_OVER = 4, OVER = 5;wire ready;wire accomplish;reg [7:0] addra;reg [2:0] clk_choise;wire [7:0] frq_mult_int_all;wire [9:0] frq_mult_float_all;wire [7:0] frq_div_int_all;wire [7:0] frq_div_int;wire [9:0] frq_div_float;reg [2:0] cstate,nstate;wire IDLE_START = (cstate == IDLE) && (clc_accompish) && locked;wire START_WRITE = (cstate == START);wire WRITE_WAIT = (cstate == WRITE) && (clk_choise == 3\'d1) && ready;wire WAIT_Once_OVER = (cstate == WAIT) && locked;wire Once_OVER_IDLE = (cstate == Once_OVER) && (addra != 8\'d200);wire Once_OVER_OVER = (cstate == Once_OVER) && (addra == 8\'d200);wire OVER_IDLE = (cstate == OVER) && frq_search_over;wire [7:0] frq_div_int_w = clk_choise ? frq_div_int : frq_div_int_all;wire valid = cstate == WRITE;// clk_choise ,0/1always @(posedge sys_clk or negedge rst_n)begin if (!rst_n)begin clk_choise <= 3\'d0; end else if (cstate == WRITE && ready)begin clk_choise <= 3\'d1; end else if (cstate == Once_OVER_OVER)begin clk_choise <= 3\'d0; end else begin clk_choise <= clk_choise; endend// addra 自增always @(posedge sys_clk or negedge rst_n) begin if (!rst_n)begin addra <= 8\'d0; frq <= 8\'d88; end else if (WAIT_Once_OVER) begin addra <= addra + 1\'b1; frq <= frq + 1\'b1; end else if (Once_OVER_OVER)begin addra <= 8\'d0; frq <= 8\'d88; end else begin addra <= addra; frq <= frq; endend// 状态机2always @(*) begin case (cstate) IDLE: begin if (IDLE_START) begin nstate = START; end else begin nstate = IDLE; end end START: begin if (START_WRITE) begin nstate = WRITE; end else begin nstate = START; end end WRITE: begin if (WRITE_WAIT) begin nstate = WAIT; end else begin nstate = WRITE; end end WAIT: begin if (WAIT_Once_OVER) begin nstate = Once_OVER; end else begin nstate = WAIT; end end Once_OVER: begin if (Once_OVER_OVER) begin nstate = OVER; end else if (Once_OVER_IDLE)begin nstate = IDLE; end else begin nstate = Once_OVER; end end OVER: begin if (OVER_IDLE) begin nstate = IDLE; end else begin nstate = OVER; end end default: begin nstate = IDLE; // 默认状态 end endcaseend// 状态机1always @(posedge sys_clk or negedge rst_n)begin if (!rst_n)begin cstate <= IDLE; end else begin cstate <= nstate; endendfrq_mult_int_rom frq_mult_int_rom_inst( .addra(addra),//8 .clka(sys_clk), .douta(frq_mult_int_all)//8 );frq_mult_float_rom frq_mult_float_rom_inst( .addra(addra),//8 .clka(sys_clk), .douta(frq_mult_float_all)//10 );frq_div_int_all_rom frq_div_int_all_rom_inst( .addra(addra),//8 .clka(sys_clk), .douta(frq_div_int_all)//8 );frq_div_int_rom frq_div_int_rom_inst( .addra(addra),//8 .clka(sys_clk), .douta(frq_div_int)//8 );frq_div_float_rom frq_div_float_rom_inst( .addra(addra),//8 .clka(sys_clk), .douta(frq_div_float)//10 );time_tree_dy time_tree_dy_inst ( .sys_clk(sys_clk), .rst_n(rst_n), .enable(1\'b1), .valid(valid), .ready(ready), .clk_control_num(3\'b1), .clk_choise(clk_choise), .phase_enable(1\'b0), // 相位使能信号,暂时不使用 .frq_mult_int(frq_mult_int_all), .frq_mult_float(frq_mult_float_all), .frq_div_int(frq_div_int_w), .frq_div_float(frq_div_float), .frq_phase_value(32\'d0), .accomplish(accomplish), .error_sign(error_sign), .clk_out1(clk_out1), // .clk_dac(clk_dac), // .clk_fir(clk_fir), .locked(locked) );endmodule

验证

用Vivado+Modelsim联合仿真。

这里要注意,要等待ip核的locked信号拉高后,也就是信号稳定后,才能进行后续。

这个是写入ip核的端口时许

这个是写一次后,locked信号拉高后,输出信号周期为11351ps,计算得为88.097MHz)接近88.1MHz。