[FPGA 学习记录] 分频器---偶分频

分频器---偶分频


在前面的章节当中,我们使用寄存器实现了计数器。那么计数器它是对我们的时钟信号进行计数,时钟对于 FPGA 来说是非常重要的,但是板载晶振提供的时钟信号它的频率是固定的,不一定能够满足我们工程的需求;所以说,使用分频或倍频产生需要的时钟是非常有必要的。

本章节当中,我们将带领各位学员开始分频器的学习,首先是偶分频。

本章节的全部内容分为两个部分:第一部分是理论学习,在这一部分,我们会对分频器的相关知识做一个系统性的讲解;第二部分是实战演练部分,在这一部分,我们会通过实验工程,设计并实现一个对系统时钟进行六分频的偶数分频电路。首先是理论学习

1 理论学习

在数字电路当中,时钟占有很重要的地位,时间的计算都要以时钟作为基本的单元。一般来说,我们使用的开发板上只有一个晶振,这就表示只有一种频率的时钟;但是在数字系统设计当中,经常需要对基准时钟进行不同倍数的分频,得到各模块所需的时钟频率。若是想要得到比固定的时钟频率更慢的时钟,可以将固定时钟进行分频;若是想要得到比固定时钟频率更快的时钟,只要在固定时钟的频率基础上进行倍频。那么无论是分频和倍频,我们都有两种方式可以选择:那么第一种就是我们厂商提供的锁相环,那么简称是 PLL——锁相环,它的知识在后面的章节我们会了解到;那么另一种方法,就是自己编写 Verilog 代码来实现。而我们使用 Verilog 代码描述的往往是分频电路,就是分频器

分频器

那么分频器在数字系统设计中是最常见的基本电路之一。所谓的分频,就是把输入信号的频率变成成倍数低于输入频率的输出信号,它的原理是:把输入的信号作为计数脉冲,由于计数器的输出端口是按一定规律输出脉冲的,所以说对不同的端口输出的信号脉冲就可以看作是对输入信号的分频。

那么分频器可以分为偶数分频器和奇数分频器,那么偶数分频器这个倍数就是偶数,比如说:2、4、6、8;那么奇数分频器这个倍数就是奇数,比如说:3、5、7、9。分频器和计数器是非常类似的,有的时候甚至可以说就是同一种东西;比如说,我们在上一章节讲到的计数器,我们绘制了它的波形;如果说,我们单单只看输出信号与输入时钟

image-20231031165625984

我们可以看出:输出信号就好像是输入信号的分频,输出信号的一个时钟周期对应输入信号的若干个周期。

那么以上就是对分频器相关知识的讲解,接下来开始实战演练

2 实战演练

2.1 实验目标

在实战演练部分,我们将通过实验工程设计并实现一个对系统时钟进行六分频的偶数分频电路。

2.2 硬件资源

我们的输出信号,将会输出到我们开发板的扩展 I/O 口,在那儿我们可以使用逻辑分析仪来测量它的频率,检验我们的实验结果。

image-20231101014746905

2.3 程序设计

首先,先建立文件体系

20231031165924_WZrzcrBrpW

然后打开 doc 文件夹,新建一个 Visio 文件

NVQtyLXWWX

2.3.1 模块框图

先来绘制模块框图,然后是输入、输出信号;那么输入信号有我们的系统时钟和复位信号,那么接下来是我们的输出信号

20231031170635_czZND4o02w

2.3.2 波形绘制

那么接下来就开始波形图的绘制,首先是输入信号

20231031171009_HhmKQwJRro

那么输入信号的波形绘制完成。

我们实验工程要实现的是对系统时钟的六分频,那么既然是分频,肯定需要一个计数器,我们就定义一个名为 cnt 的计数器

20231031171252_65F4Z606b8

那么到了这里大家应该感觉到,这和之前计数器的波形绘制差不多。

我们要实现系统时钟的六分频,那么根据上一章的讲解我们得知:计数器要精确的控制它何时计数、何时清零。那么这里的计数我们没有特殊的要求,只要时钟正常工作,复位释放后就可以立刻进行计数。那么计数器何时清零呢?我们需要对输入的系统时钟进行六分频,那么是否需要我们的计数器计数 0~5 这六个数呢?当然是不需要的。和上一章计数器的思考过程是一样的,我们只需要让计数器从 0 计数到 2,计数三个数就可以了;然后,每当计数器计数到 2 的时候,就让我们的输出信号取反就可以了。那么参照这个思想我们绘制波形图

20231031172153_zNojHuZB4v

首先初值是 0,然后每个时钟周期加一;那么前面已经说到了,计数到最大值 2 就进行清零。

那么这样,计数器的波形绘制完成,下面开始输出信号的波形绘制

20231031172504_mH4UcK3k2H

那么输出信号初值赋为 0 就是一直处于低电平,当计数到最大值对它进行取反,那么这样输出信号的波形绘制完成。

我们可以看一下,那么从 ❶ 到 ❷ 可以看作输出信号的一个时钟周期,那么对应的我们系统时钟是六个时钟周期,这样就表示六分频

image-20231031172813725

2.3.3 代码编写

那么接下来可以进行代码的编写,我们参照我们的波形图编写代码

VB7ToqT708

首先是模块开始,然后是模块名称、端口列表,然后是模块结束;那么输入信号有系统时钟、复位信号,然后是我们的输出信号

pSOKwlxFyd

端口列表编写完成之后,开始信号的赋值。首先要声明一个计数器变量,它的位宽是 2 位宽,因为后面使用 always 语句进行赋值,所以说它的变量类型是 reg 型。我们使用异步复位,当我们的复位信号有效时给它赋一个初值,初值是 0 和波形图对应;然后,当它计数到最大值让它归零,最大值是 2 当然了这个最大值你也可以定义一个参数,我们这儿就不定义了;如果说我们的复位信号无效且没有计数到最大值,让它不断加一

3vueOcbrgl

那么这样计数器变量的代码编写完成,接下来是我们的输出信号。

那么输出信号同样使用 always 语句进行赋值,它的初值也是 0;当我们的计数器计数到最大值对输出信号进行取反;当我们的复位信号无效且没有计数到最大值,让它保持原来的电平

W1VDA7JyRh

那么这样代码编写完成,我们保存

divider_six.v

module divider_six
(
    input   wire        sys_clk     ,
    input   wire        sys_rst_n   ,
    
    output  reg         clk_out
);

reg [1:0]   cnt;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt <= 2'd0;
    else if (cnt == 2'd2)
        cnt <= 2'd0;
    else
        cnt <= cnt + 2'd1;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        clk_out <= 1'b0;
    else if (cnt == 2'd2)
        clk_out <= ~clk_out;
    else
        clk_out <= clk_out;

endmodule

2.3.4 逻辑仿真

然后回到桌面,建立一个新的工程

mBEaZhzBFQ

然后加载我们的代码,进行一次全编译,查找代码的语法错误;那么编译完成,有 7 个警告,我们点击 OK

20231031174524_pj61g7L4rl

我们看一下 RTL 视图

image-20231031174653696

那么看到这个 RTL 视图,和我们之前计数器的 RTL 视图是基本上完全一样的,只是计数器的位宽可能不一样。这也印证了我们分频器与计数器之间的关系。

那么下面编写仿真代码,对我们的代码进行仿真验证

20231031174906_N2ugSFinsg

那么首先是时间参数,然后是模块开始、模块名称,然后是端口列表,端口列表是空的,然后是模块结束;然后是声明我们的时钟信号和复位信号,然后是引出我们的输出信号

20231031175145_SYPKDePan5

那么初始化我们的系统时钟和全局复位;然后是我们的时钟频率,延迟 10ns 对我们的时钟进行反转,这样就可以得到频率为 50MHz 的时钟信号

20231031175345_a6ITlkQazn

那么下面就是模块的实例化。然后接入我们的时钟信号,接入我们的复位信号,然后引出我们的输出信号

20231031175549_0luySCJYka

那么这样仿真代码编写完成,保存

tb_divider_six.v

module tb_divider_six();

reg     sys_clk;
reg     sys_rst_n;

wire    clk_out;

initial
    begin
        sys_clk = 1'b1;
        sys_rst_n <= 1'b0;
        #20
        sys_rst_n <= 1'b1;
    end

always #10 sys_clk = ~sys_clk;

divider_six divider_six_inst
(
    .sys_clk  (sys_clk),
    .sys_rst_n(sys_rst_n),
    
    .clk_out  (clk_out)
);

endmodule

回到我们的实验工程加载仿真代码,然后进行仿真设置

20231031175745_BEZUPO2fub

然后开始仿真

20231031180408_L3klu3Ibgv

那么编译完成之后我们打开 sim 窗口,添加我们的模块波形;然后打开波形窗口,全选、分组、消除路径;然后点击 Restart,清除之前存在的波形;运行时间设置为 500ns 然后运行一次,全局视图

20231031180958_JxVQcDveqC

我们添加两条参考线,一条在输出信号的上升沿,另一条在下一个上升沿;那么输出信号的时钟周期是 120ns 我们换算一下单位,在这个位置 点击右键,选择这个选项,然后在这儿勾选 展示我们的频率,然后点击 OK 那么这儿就显示的是频率

20231031181300_SMeISyCnjo

我们输出信号的频率就是 8.333MHz 也就是我们系统时钟 50MHz 的六分频,而且各信号的波形变化与我们绘制的波形图是完全一致的,所以说我们仿真验证通过。

2.4 上板验证

2.4.1 管脚绑定

那么回到我们的实验工程,绑定我们的管脚;那么分频后的时钟,我们输出到开发板的扩展 I/O 口,在那儿可以使用逻辑分析仪对它的频率进行测量,我们输出到 F15 端口;我们的系统时钟是 E1;那么复位信号是 M15

image-20231031181859606

那么端口绑定完成之后,重新进行全编译,那么编译完成,点击 OK

20231031181955_quhvUwCVLd

2.4.2 结果验证

那么按照下图所示连接我们的板卡,首先连接下载器,下载器的另一端连接我们的电脑,然后连接电源,为我们的开发板进行上电

上板验证前的硬件连线

回到我们的实验工程,点击 Programmer 这个位置打开下载界面,然后添加我们的 SOF 文件,点击 Start 进行程序的下载

20231031182209_iIeSSZ5Mrz

那么程序下载完成之后,将扩展IO口F15连接到逻辑分析仪的通道0——CH0接口

image-20231031183524033

它的频率是 8.333MHz,那么测量结果与仿真结果相同,都是我们时钟信号的六分频,那么上板验证通过。

2.5 使用脉冲信号实现六分频

那么接下来我们回到波形图的绘制,来修改一下我们的波形图。为什么要进行波形图的修改呢?

那么使用刚才的方法我们输出了分频后的时钟,很多朋友就直接把这个信号当做新的低频时钟来使用,实现自己想要的功能。那么各位朋友肯定会觉得:能够实现功能就一切 OK 了,但是往往忽略了一些隐患的存在,如果你对 FPGA 的了解多一些,就会理解这个做法是不严谨的。这种做法衍生的潜在问题在低速系统中不易察觉,而在高速系统中就很容易出现问题。这是因为,我们通过这种方式分频得到的时钟,表面上是对系统时钟进行了分频,产生了一个新的低频时钟;但实际上与真正的系统时钟还是有很大的区别的。因为在 FPGA 当中,凡是时钟信号都要连接到全局时钟网络,那么全局时钟网络也叫做全局时钟树,它是 FPGA 厂商专为时钟路径而特殊设计的,它能够使时钟信号到达每个寄存器的时间都尽可能地相同,那么减少时序问题的产生。而我们这种分频的方式产生的低频信号,并没有连接到全局时钟网络上;但是我们外部晶振传入的时钟信号,通过管脚连接到了 FPGA 的专用时钟引脚上,自然就连接到了全局时钟网络上;所以说,在系统时钟工作下的信号
要比刚刚我们分频产生的时钟工作下的信号,更容易在高速系统中保持稳定。

那么既然发现了这个问题,我们就需要进行改进,这个问题怎么改进呢?这时我们要想到上一章刚学到的脉冲标志信号——flag 这里就可以用上了。

2.5.1 波形修改

那么这里,我们可以产生一个用于标记六分频的 flag 标志信号;那么这样,每两个 flag 脉冲之间的频率就是对系统时钟的六分频。

但是计数器计数的个数我们需要增加一些,那么在波形图上怎么表示呢?我们来看一下

20231031184745_603nNkYWWZ

然后产生一个标志信号,同时这个标志信号也是作为输出信号,就是产生的分频时钟。那么标志信号初值为 0 当我们的计数器计数到最大值减一,也就是 4 的时候,在下一个时钟周期让它保持一个时钟周期的高电平

20231031185147_Sn4WRgot72

这个用法和我们之前计数器当中的用法是相同的。

那么这样,输出信号的两个上升沿之间也是对应的我们系统时钟的六个时钟周期,那么这个输出信号就是我们系统时钟的六分频,虽然它的占空比 50%

2.5.2 RTL 代码修改

那么接下来参照这个波形图来改进我们的代码。那么输出信号重新定义;我们计数器的最大值改为了 5,位宽也要改一下 3 位宽;那么下面是输出信号的赋值,仍然使用 always 语句,初值一样是低电平;当我们的计数器计数到最大值减一的时候,下一个时钟周期让它保持一个时钟周期的高电平,那么代码就这样编写;那么计数最大值是 5,减 1 就是 4,然后让它保持一个时钟周期的高电平,其他时刻都让它保持为低电平。那么这样代码就修改完成

pctlyN1GKA

divider_six.v

module divider_six
(
    input   wire        sys_clk     ,
    input   wire        sys_rst_n   ,
    
    // output  reg         clk_out
    output  reg         clk_flag
);

reg [2:0]   cnt;

/* always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt <= 2'd0;
    else if (cnt == 2'd2)
        cnt <= 2'd0;
    else
        cnt <= cnt + 2'd1;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        clk_out <= 1'b0;
    else if (cnt == 2'd2)
        clk_out <= ~clk_out;
    else
        clk_out <= clk_out;
 */

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt <= 3'd0;
    else if (cnt == 3'd5)
        cnt <= 3'd0;
    else
        cnt <= cnt + 3'd1;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        clk_flag <= 1'b0;
    else if (cnt == 3'd4)
        clk_flag <= 1'b1;
    else
        clk_flag <= 1'b0;

endmodule

有的同学肯定存在疑问:这个方法与刚才那个方法类似,只不过产生的分频时钟的占空比不同。其实并不是这样,比如说:如果我们使用第一种方法对一个变量进行赋值,应该怎么办?我们这儿声明一个变量,比如说:声明一个变量 a。如果我们使用第一种方法生成的时钟对它进行赋值,应该怎么编写呢?

reg     a;

always@(posedge clk_out or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        a <= 1'b0;
    else
        a <= a + 1'b1;

这儿的时钟就要换成我们刚刚生成的分频时钟 clk_out;给它一个初值 0 然后让它不断的加一。

但是如果我们使用第二种方法生成的时钟信号对它进行赋值,应该怎么使用呢?应该是这样

reg     a;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        a <= 1'b0;
    else if (cnt_flag == 1'b1)
        a <= a + 1'b1;

这个地方仍然是系统时钟 sys_clk;那这儿需要加一个条件,当 cnt_flag 它为高电平时。

那么这两种方法最终的效果都是相同的,而第二种方法的信号 a 是在我们系统时钟的控制下产生的,所以说它和所有的在系统时钟下产生的信号都保持几乎相同的时钟关系,方法是更好的。我们推荐这种写法。

第一种方法在低频系统下也能够使用,但是在高频系统中容易存在问题,第一种方法是分频的方法;第二种方法是降频的方法,大家要记住哈。

2.5.3 RTL 代码编译

然后回到我们的实验工程,进行全编译,那么点击 OK

20231101005639_YdGLzpsEqi

然后查看一下 RTL 视图

image-20231101005811796

那么这个 RTL 视图与我们前面计数器当中使用脉冲信号的 RTL 视图是差不多的,只是少了一个输出的寄存器。

2.5.4 仿真代码修改

下面我们修改仿真代码。把 clk_out 这个信号全部换为 clk_flag 信号,我们使用 Ctrl+H 快捷键,在这儿输入 clk_flag,然后点击全部替换

20231101010111_k2EakjnILF

这样修改完成,点击保存

tb_divider_six.v

module tb_divider_six();

reg     sys_clk;
reg     sys_rst_n;

wire    clk_flag;

initial
    begin
        sys_clk = 1'b1;
        sys_rst_n <= 1'b0;
        #20
        sys_rst_n <= 1'b1;
    end

always #10 sys_clk = ~sys_clk;

divider_six divider_six_inst
(
    .sys_clk  (sys_clk),
    .sys_rst_n(sys_rst_n),
    
    .clk_flag  (clk_flag)
);

endmodule

2.5.5 逻辑仿真

然后回到我们的 ModelSim 打开我们的 Library 窗口。因为两个模块都进行了修改,所以说对两个模块都进行重新的编译;这儿显示编译完成,回到波形窗口,全选我们的信号,然后删除;再回到 sim 重新添加模块波形;然后点击 Restart 重新运行 500ns

20231101010746_5yBwAtdXoa

那么接下来将仿真波形与我们绘制的波形图进行对比

image-20231101012403313

我们的计数器计数最大值是 5,cnt 波形对应位置是 5 没有问题;计数到最大值 5 然后归零,进行下一个周期的计数,也没有问题。我们的 clk_flag 信号,在 cnt 等于 5 这个位置保持一个时钟周期的高电平,没有问题。

仿真波形与我们绘制的波形图是完全一致的,查看一下输出信号的频率

image-20231101012943946

它的频率是 8.333GHz,这里有问题,我们查看一下仿真代码,发现忘记编写时间参数了,修改后重新仿真

20231101013203_V5UFjsk9yB

那么就是我们系统时钟的六分频,那么仿真验证是正确的。

2.5.6 上板验证

下面进行上板验证。回到我们的实验工程,然后修改我们的端口,将 clk_out 这个端口删除:选中它点击 Delete,点击 OK,点击 Yes 就可以删除;然后将 clk_flag 信号与我们的 F15 端口绑定,然后进行重新编译,点击 OK

20231101013529_q84RUtMI04

然后下载我们的程序

20231101013644_V5cqiPe6F8

它的频率仍然是 8.333MHz

image-20231101013923567

那么上板验证也通过。

在本章节当中,我们讲解了六分频实现的两种方法,以及它们之间的区别。我们推荐使用第二种方法:降频的方法来实现分频。


参考资料:

12. 分频器

17-第十四讲-分频器—偶分频

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值