💡 特别鸣谢野火 FPGA 的教学与帮助!
本节相关的教程 link 📌
- 第 17 章 计数器:
- 17.0 章节导读:
- 17.1 理论学习:
- 17.2 实战演练:
- 17.2.1 实验目标:
- 17.2.2 硬件资源:
- 17.2.3 程序设计:
- 1. 模块框图:
- 2. 波形图绘制:
- 操作步骤:
- 方法 1 实现:不带标志信号的计数器:
- 方法 2 实现:带标志信号的计数器:
- 为什么一定要使用这个脉冲标志信号:
- 3. 代码编写:
- 方法 1 实现:不带标志信号的计数器:
- 方法 2 实现:带标志信号的计数器:
- 4. 仿真验证:
- 仿真文件编写:
- 仿真波形分析:
- 17.2.4 上板验证:
- 1. 引脚约束:
- 2. 结果验证:
- 17.3 章末总结:
- 17.4 拓展训练:
17.0 章节导读
前文中我们讲解了时序逻辑电路中最基本的单元——寄存器,本章我们就用寄存器做点事情,用它来实现计数器的设计,有了计数器我们能做的事情就太多了太多了,可以毫不夸张的说一切和时间有关的设计都会用到它。
17.1 理论学习
计数是一种最简单基本的运算,计数器就是实现这种运算的逻辑电路,计数器在数字系统中主要是对脉冲的个数进行计数,以实现测量、计数和控制的功能,同时兼有分频功能。计数器在数字系统中应用广泛,如在电子计算机的控制器中对指令地址进行计数,以便顺序取出下一条指令,在运算器中作乘法、除法运算时记下加法、减法次数,又如在数字仪器中对脉冲的计数等等。 计数器也是在 FPGA 设计中最常用的一种时序逻辑电路,根据计数器的计数值我们可以精确的计算出 FPGA 内部各种信号之间的时间关系,每个信号何时拉高、何时拉低、拉高多久、拉低多久都可以由计数器实现精确的控制。而让计数器计数的是由外部晶振产生的时钟,所以可以比较精准的控制具体需要计数的时间。计数器一般都是从 0 开始计数, 计数到我们需要的值或者计数满溢出后清零,并可以进行不断的循环,3 位数的十进制计数器最大可以计数到 999,4 位数的最大可以计数到 9999;3 位数的二进制计数器最大可以计数到 111(7),4 位数的最大可以计数到 1111(15)。
17.2 实战演练
17.2.1 实验目标
本例我们让计数器计数 1s 时间间隔,来实现 led 灯每隔 1s 闪烁一次的效果。
17.2.2 硬件资源
使用开发板板载 LED 灯来展示计数器。
17.2.3 程序设计
1. 模块框图
因为本设计功能单一,主要是通过设计一个 1s 计数器来实现 led 灯闪烁的效果,所以给模块取名为 counter。计数器肯定需要时钟和复位信号,因为计数器的计数就是靠时钟的脉冲来提供的,所以没有其他额外的输入信号了,而输出我们则使用一个 led 灯来观察计数器计数后的效果,所以需要又一个输出信号名为 led_out。根据上面的分析设计出的 Visio 框图如图 17-3 所示。
2. 波形图绘制
操作步骤
下面我们开始进行“真正”的波形设计,为什么说这是“真正”的呢?难道之前的波形设计都是假的吗?当然不是,因为波形设计在时序电路设计中最有价值,也最好用,组合逻辑的设计虽然我们也画波形了,主要是为了大家能够尽早接触这种方法,并不涉及画波形的精髓之处,对于组合逻辑的设计我们用真值表也可以设计出代码,前面的寄存器章节的时序逻辑电路又太简单,而我们在本章要画的计数器的波形则是以后设计中经常会用到的,也是非常重要的。 本章实例的重点就是如何控制好计数器,对于计数器来说只要控制好什么时候开始计数,什么时候清零的问题那么你就可以完全掌控计数器了。首先考虑什么时候开始计数的问题(也可以先考虑什么时候清零的问题),这个系统除了时钟和复位没有外界的其他输入了,所以只要复位一撤销,时钟沿来到就可以立刻进行计数,所以我们不需要太关系计数开始的条件,也可以默认为没有条件。 然后是考虑计数器什么时候清零的问题,有人可能会问,计数器不是会计数满自动清零吗?是的,但计数到多少后清零是不是需要我们考虑呢。这就引入了一个新的问题,计数 1s 的时间需要计数器计数多少个数。有很多学习者对这一块是相当的迷糊,经常容易计算错,那样就会导致计数的个数不准,从而导致系统出现各种问题。我们计数的时钟就用系统时钟 50MHz,换算成时间为[1/(50_10^3_10^3)Hz]s = 0.000_000_02s,也就是说 50MHz 频率的时钟一个周期的时间为 0.000_000_02s,计数 1s 的时间需要多少个 0.000_000_02s 呢,经计算得需要(1/0.000_000_02s)个 = 50_000_000 个,所以我们的计数器需要在 50MHz 的时钟下计 50_000_000 个数才可以。但是我们是从 0 开始计数的,所以在 50MHz 的时钟频率下计数 1s 的时间最终的计数值为 49_999_999。但是不要忘记了我们要实现的是在 1s 的时间内闪烁,就是说在 1s 的时间内,led 点亮 0.5s,熄灭 0.5s,这样的观赏效果最佳。我们真的需要让计数器的计数值到 49_999_999 这么多吗?首先这个想法当然是可以的,但是计数到 49_999_999 需要 26 位宽的寄存器,这显然需要使用很多的寄存器,会占用很多资源,虽然我们的资源足够,但更精简的设计可以让我们整个系统的性能达到最优,所以我们希望减少一些寄存器的使用,这样位宽就可以变小一些,从而节约一些寄存器资源。因为 led 灯实现 1s 内闪烁的效果,也就是 led 灯的电平为高和低电平交替进行,即每 0.5s 的时间将控制 led 灯的管脚取反就可以了。那么我们就可以让计数器减少一半的计数时间(个数),也就是计数 0.5s 的时间,计数器计数的值为 0~24_999_999,需要 25 位宽的寄存器。
方法 1 实现:不带标志信号的计数器
经过了简单的分析后我们可以开始波形的绘制,首先把输入 sys_clk 和 sys_rst_n 信号画好,然后我们添加一个用于计数 0.5s 时间的 cnt 计数器,当 sys_rst_n 信号有效时,cnt 计数器清零;当 sys_rst_n 信号撤销后,时钟的上升沿时刻 cnt 计数器开始自加 1。当 cnt 计数器计数到 N(这里 N = 24_999_999)时清零,只要 sys_rst_n 不复位,该计数器将一直循环计数下去。输出信号 led_out 就是直接控制 led 闪烁的信号,每当计数器计数到 N 时 led_out 信号取反,从而控制外部 led 灯实现闪烁的效果。
方法 2 实现:带标志信号的计数器
当然我们还可以采用看上去“多此一举”的方法,那就是再添加一个用于指示 cnt 计数器计数到 N 的脉冲信号 cnt_flag,当计数器计数到 N 时 led_out 信号先不取反,而是让 cnt_flag 脉冲信号产生一个时钟周期的高脉冲,led_out 信号每当检测到 cnt_flag 脉冲信号为高时取反,也能够控制外部 led 灯实现闪烁的效果。 为了更严谨,这里还有一个细节需要注意,如图 17-5 和图 17-6,这两组波形图不仔细看真的就是一模一样,但认真观察我们会发现不同,图 17-5 中的 cnt_flag 脉冲标志信号是在 N 有效时拉高,而图 17-6 中的 cnt_flag 脉冲标志信号是在 N-1 有效时拉高,为什么还要区分这一点细节呢?在本例中当然不需要区分,因为 led 灯闪烁的时间多一点、少一点对于观察不会有太大的影响,但如果我们做一个数字时钟的话,那情况就不一样了,需要一点不差,越准确越好。图 17-5 中的第一个 cnt_flag 脉冲标志信号是等待计数器计数到 N 这个值才拉高,时间上是刚刚好的,led_out 信号拉高的条件则是以 cnt_flag 为条件变化的, 当时钟采集到 cnt_flag 脉冲标志信号为高电平时,其实 cnt 计数的个数已经为 N+1 了,也就是说此刻已经多计数了,所以我们要采用图 17-6 的方式来拉高 cnt_flag 脉冲标志信号, 也就是让 cnt_flag 脉冲标志信号在计数器计数到 N-1 时就拉高,这样子我们再利用 cnt_flag 脉冲标志信号产生其他的信号时间就是严格准确的了。
经过了简单的分析后我们可以开始波形的绘制,首先把输入 sys_clk 和 sys_rst_n 信号画好,然后我们添加一个用于计数 0.5s 时间的 cnt 计数器,当 sys_rst_n 信号有效时,cnt 计数器清零;当 sys_rst_n 信号撤销后,时钟的上升沿时刻 cnt 计数器开始自加 1。当 cnt 计数器计数到 N(这里 N = 24_999_999)时清零,只要 sys_rst_n 不复位,该计数器将一直循环计数下去。输出信号 led_out 就是直接控制 led 闪烁的信号,每当计数器计数到 N 时 led_out 信号取反,从而控制外部 led 灯实现闪烁的效果。
为什么一定要使用这个脉冲标志信号
其实在这里我们是想引出一个非常有用的信号——脉冲标志信号(flag),这种信号会在后面用到很多,它可以减少代码中 if 括号内的条件让代码更加清晰简洁,而且当需要在多处使用脉冲标志信号的地方要比全部写出的方式更节约逻辑资源,脉冲标志信号在指示某些状态时是非常有用的,当大家以后在实现相对复杂的逻辑功能时注意想到使用脉冲标志信号,后面我们还会介绍到另一个有用的信后——使能信号。
3. 代码编写
方法 1 实现:不带标志信号的计数器
根据上面 RTL 代码综合出的 RTL 视图如图 17-7 所示,我们可以看到其结构已经比之前的设计复杂很多了,初学者乍一看到此图会有些懵,不过仔细分析还是可以看明白的。 首先最左边的“ADDER”是一个加法器,加法器的一个输入端是寄存器反馈回来的值,另一个输入端是加数 1,用于计数器自加 1,加和后的值传给下一级“MUX21”选择器,这个选择器的选择端“SEL”是比较器“EQUAL”用于比较计数器是否计数到 24_999_999 这个值的,如果计数器计数到了 24_999_999 就让选择器的选择端“SEL”置为 1,使选择器“MUX21”选通“DATAB”端,此时 25 位的寄存器清零;如果计数器还没有计数到 24_999_999 这个值就让选择器的选择端“SEL”置为 0,使选择器“MUX21”选通“DATAA”让计数器继续计数。当“EQUAL”比较器的输出为 1 时(表示计数器已经计数到了 24_999_999 这个值),会将该信号作用于最后一级寄存器的使能端,使能最后一级寄存器输出信号至外部管脚,最后一级寄存器的输出端反馈回其输入端并取反等待下一次“EQUAL”比较器的输出为 1 时再变化。
方法 2 实现:带标志信号的计数器
4. 仿真验证
仿真文件编写
仿真波形分析
17.2.4 上板验证
1. 引脚约束
仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 17-2 所示。引脚配置如图 17-15 所示。
2. 结果验证
程序下载完毕后,会看到板卡 LED 灯 D7 不断闪烁,时间间隔为 1 秒。
17.3 章末总结
本章主要讲解了时序逻辑电路中最常用的计数器,并详细讲解了如何根据计数时钟来精确计算计数的时间和个数,并讲解了两种控制 led 灯输出的方式,引出了重要的脉冲标志信号的以及其用法,同时还分析了稍微复杂的时序逻辑电路的 RTL 视图,一定要有硬件思想。通过本章的例子,我们应该继续加大根据分析绘制波形图技能的训练,这也仅仅是一个开始,真正的开始,希望大家能够通过后面章节的继续训练彻底掌握这种好的设计方法。
17.4 拓展训练
1、如果把计数器的计数条件和清零条件的优先级互换会有什么不一样的效果?
2、如果我们只想让 led 灯闪烁 10 次该如何去实现呢?请按照本例的设计方法——画波形图、编写代码、仿真、上板验证尝试一下。