11. 计数器

11.1. 章节导读

再前文中我们讲解了时序逻辑电路中最基本的单元——寄存器,本章我们就用寄存器做点事情,用它来实现计数器的设计,有了计数器我们能做的事情就太多了太多了,可以毫不夸张的说一切和时间有关的设计都会用到它。

11.2. 理论学习

计数是一种最简单基本的运算,计数器就是实现这种运算的逻辑电路,计数器在数字系统中主要是对脉冲的个数进行计数,以实现测量、计数和控制的功能,同时兼有分频功能。计数器在数字系统中应用广泛,如在电子计算机的控制器中对指令地址进行计数,以便顺序取出下一条指令,在运算器中作乘法、除法运算时记下加法、减法次数,又如在数字仪器中对脉冲的计数等等。

计数器也是在FPGA设计中最常用的一种时序逻辑电路,根据计数器的计数值我们可以精确的计算出FPGA内部各种信号之间的时间关系,每个信号何时拉高、何时拉低、拉高多久、拉低多久都可以由计数器实现精确的控制。而让计数器计数的是由外部晶振产生的时钟,所以可以比较精准的控制具体需要计数的时间。计数器一般都是从 0开始计数,计数到我们需要的值或者计数满溢出后清零,并可以进行不断的循环,3位数的十进制计数器最大可以计数到999,4位数的最大可以计数到9999;3位数的二进制计数器最大可以计数到111(7),4位数的最大可以计数到1111(15)。

11.3. 实战演练

11.3.1. 实验目标

本例我们让计数器计数1s时间间隔,来实现led灯每隔1s闪烁一次的效果。

11.3.2. 硬件资源

如图 17‑1所示,使用开发板板载LED灯来展示计数器。

count002

图 17‑1 硬件资源

由原理图可知,征途Pro开发板LED灯为低电平点亮。如图 17‑2所示。

count003

图 17‑2 LED灯原理图

11.3.3. 程序设计

11.3.3.1. 模块框图

因为本设计功能单一,主要是通过设计一个1s计数器来实现led灯闪烁的效果,所以给模块取名为counter。计数器肯定需要时钟和复位信号,因为计数器的计数就是靠时钟的脉冲来提供的,所以没有其他额外的输入信号了,而输出我们则使用一个led灯来观察计数器计数后的效果,所以需要又一个输出信号名为led_ou t。根据上面的分析设计出的Visio框图如图 17‑3所示。

count004

图 17‑3 模块框图

端口列表与功能总结如表格 17‑1所示。

表格 17‑1 输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1Bit

Input

工作时钟,频率50MHz

sys_rst_n

1Bit

Input

复位信号,低电平有效

led_out

1Bit

Output

输出控制led灯

11.3.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灯实现闪烁的效果。

count005

图 17‑4 计数器波形图(一)

方法2实现:带标志信号的计数器

当然我们还可以采用看上去“多此一举”的方法,那就是再添加一个用于指示cnt计数器计数到N的脉冲信号cnt_flag,当计数器计数到N时led_out信号先不取反,而是让cnt_flag脉冲信号产生一个时钟周期的高脉冲,led_out信号每当检测到cnt_flag脉冲信号为高时取反,也能够控制外部le d灯实现闪烁的效果。

为了更严谨,这里还有一个细节需要注意,如图 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脉冲标志信号产生其他的信号时间就是严格准确的了。

count006

图 17‑5 计数器波形图(二)

count007

图 17‑6 计数器波形图(三)

可能有人会问为什么一定要使用这个脉冲标志信号呢,方法1实现的计数器不好吗?当然不是,其实在这里我们是想引出一个非常有用的信号——脉冲标志信号(flag),这种信号会在后面用到很多,它可以减少代码中if括号内的条件让代码更加清晰简洁,而且当需要在多处使用脉冲标志信号的地方要比全部写出的方式更节约逻辑资 源,脉冲标志信号在指示某些状态时是非常有用的,当大家以后在实现相对复杂的逻辑功能时注意想到使用脉冲标志信号,后面我们还会介绍到另一个有用的信后——使能信号。

11.3.3.3. 代码编写

方法1实现:不带标志信号的计数器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
module counter
#(
parameter CNT_MAX = 25'd24_999_999/*这是我们第一次使用参数的方式定义常量
使用参数的方式定义常量有很多好处,如:我们在RTL代码中实例化该模块时,如果需要两个
不同计数值的计数器我们不必设计两个模块,而是直接修改参数的值即可;另一个好处是在编
写Testbench进行仿真时我们也需要实例化该模块,但是我们需要仿真至少0.5s的时间才
能够看出到led_out效果,这会让仿真时间很长,也会导致产生的仿真文件很大,所以我们
可以通过直接修改参数的方式来缩短仿真的时间而看到相同的效果,且不会影响到RTL代码模
块中的实际值,因为parameter定义的是局部参数,所以只在本模块中有效。为了更好的区
 分,参数名我们习惯上都要大写*/
 )
 (
 input wire sys_clk , //系统时钟50MHz
 input wire sys_rst_n , //全局复位

 output reg led_out //输出控制led灯
 );

 reg [24:0] cnt; //经计算得需要25位宽的寄存器才够500ms

 //cnt:计数器计数,当计数到CNT_MAX的值时清零
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt <= 25'b0;
 else if(cnt == CNT_MAX)
 cnt <= 25'b0;
 else
 cnt <= cnt + 1'b1;

 //led_out:输出控制一个LED灯,每当计数满标志信号有效时取反
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 led_out <= 1'b0;
 else if(cnt == CNT_MAX)
 led_out <= ~led_out;

 endmodule

根据上面RTL代码综合出的RTL视图如图 17‑7所示,我们可以看到其结构已经比之前的设计复杂很多了,初学者乍一看到此图会有些懵,不过仔细分析还是可以看明白的。首先最左边的“ADDER”是一个加法器,加法器的一个输入端是寄存器反馈回来的值,另一个输入端是加数1,用于计数器自加1,加和后的值传给下一级 “MUX21”选择器,这个选择器的选择端“SEL”是比较器“EQUAL”用于比较计数器是否计数到24_999_999这个值的,如果计数器计数到了24_999_999就让选择器的选择端“SEL”置为1,使选择器“MUX21”选通“DATAB”端,此时25位的寄存器清零;如果计数器还没有计数到24_99 9_999这个值就让选择器的选择端“SEL”置为0,使选择器“MUX21”选通“DATAA”让计数器继续计数。当“EQUAL”比较器的输出为1时(表示计数器已经计数到了24_999_999这个值),会将该信号作用于最后一级寄存器的使能端,使能最后一级寄存器输出信号至外部管脚,最后一级寄存器的输出端反 馈回其输入端并取反等待下一次“EQUAL”比较器的输出为1时再变化。

count008

图 17‑7 RTL视图(一)

方法2实现:带标志信号的计数器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
module counter
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk , //系统时钟50Mh
input wire sys_rst_n , //全局复位

output reg led_out //输出控制led灯
 );

 //reg define
 reg [24:0] cnt ; //经计算得需要25位宽的寄存器才够500ms
 reg cnt_flag;

 //cnt:计数器计数,当计数到CNT_MAX的值时清零
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt <= 25'b0;
 else if(cnt == CNT_MAX)
 cnt <= 25'b0;
 else
 cnt <= cnt + 1'b1;

 //cnt_flag:计数到最大值产生的标志信号,每当计数满标志信号有效时取反
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_flag <= 1'b0;
 else if(cnt == CNT_MAX – 25'b1)
 cnt_flag <= 1'b1;
 else
 cnt_flag <= 1'b0;

 //led_out:输出控制一个LED灯
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 led_out <= 1'b0;
 else if(cnt_flag == 1'b1)
 led_out <= ~led_out;

 endmodule

根据上面RTL代码综合出的RTL视图如图 17‑8所示,前面的分析都是一样的,不同是“EQUAL”比较器输出后不是直接接到最后一级寄存器上,而是在中间又加一个用于产生计数器计数到24_999_999这个值时的脉冲信号寄存器,该脉冲信号同时会作用于最后一级寄存器的使能端,使能最后一级寄存器输出信号至外 部管脚,最后一级寄存器的输出端反馈回其输入端并取反等待下一次“EQUAL”比较器的输出为1时再变化。

count009

图 17‑8 RTL视图(二)

通过对比我们可以发现第一种实现方式用了2个always块,其RTL视图分别对应两组寄存器,而第二种实现方式用了3个always块,其RTL视图分别对应了3组寄存器,这是我们分析时需要特别注意的。

11.3.3.4. 仿真验证

仿真文件编写

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
\`timescale 1ns/1ns
module tb_counter();

//reg define
reg sys_clk;
reg sys_rst_n;

//wire define
wire led_out;

 //初始化输入信号
 initial begin
 sys_clk = 1'b1;
 sys_rst_n <= 1'b0;
 #20
 sys_rst_n <= 1'b1;
 end

 //sys_clk:每10ns电平翻转一次,产生一个50MHz的时钟信号
 always #10 sys_clk = ~sys_clk;

 //---------------------flip_flop_inst----------------------
 counter
 #(
 .CNT_MAX (25'd24 ) //实例化带参数的模块时要注意格式,当我们想要修改常数在
 //当前模块的值时,直接在实例化参数名后面的括号内修改即可
 )
 counter_inst(
 .sys_clk (sys_clk ), //input sys_clk
 .sys_rst_n (sys_rst_n ), //input sys_rst_n

 .led_out (led_out ) //output led_out
 );

 endmodule

仿真波形分析

方法1实现:不带标志信号的计数器

打开ModelSim执行仿真,仿真出来的波形如图 17‑9所示,我们让仿真运行了10us,已经可以发现led_out信号产生了等间隔的脉冲高低变化。如图 17‑10所示,我们把led_out信号变化的地方放大观察,可以看到cnt计数器计数到24就清零了,和我们在Testbench中修改的参数结果一致,同时led_out信号的电平发生了反转。从而验证了我们设计的是正确的。

count010

图 17‑9 方法1仿真波形图(一)

count011

图 17‑10 方法1仿真波形图(二)

我们观察“Transcript”界面(如图 17‑11所示)中打印的结果,也可以发现led_out信号是在相同的时间间隔下高低电平交替变化的。

count012

图 17‑11 方法1打印结果(一)

方法2实现:带标志信号的计数器

如图 17‑12所示,我们同样让仿真运行了10us,也发现led_out信号产生了等间隔的脉冲高低变化。不同的是我们多加了一个cnt_flag脉冲标志信号,从图中也的确可以发现一个个小的脉冲。如图 17‑13所以,我们也把led_out信号变化的地方放大观察,可以看到cnt计数器也是计数到24就清零 了,和我们在Testbench中修改的参数结果一致,在cnt计数器计数到23的同时,cnt_flag脉冲信号拉高一个时钟的高电平,led_out信号的检测到cnt_flag脉冲信号为高电平发生了反转。也验证了我们的设计是正确的。

count013

图 17‑12 方法2仿真波形图(一)

count014

图 17‑13 方法2仿真波形图(二)

我们观察“Transcript”界面(如图 17‑14所示)中打印的结果,和图 17‑11相同。

count015

图 17‑14 方法2打印结果

11.3.4. 上板验证

11.3.4.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 17‑2所示。引脚配置如图 17‑15所示。

表格 17‑2 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

Input

E1

时钟

sys_rst_n

Input

M15

复位按键

led_out

Output

L7

LED灯

count016

图 17‑15 引脚配置图

11.3.4.2. 结果验证

如图 17‑16所示,开发板连接12V直流电源和USB-Blaster下载器JTAG端口。线路正确连接后,打开开关为板卡上电。

count017

图 17‑16 程序下载连线图

如图 17‑17所示,使用“Programmer”为开发板下载程序。

count018

图 17‑17 程序下载窗口

程序下载完毕后,会看到板卡LED灯D6不断闪烁,时间间隔为1秒。

11.4. 章末总结

本章主要讲解了时序逻辑电路中最常用的计数器,并详细讲解了如何根据计数时钟来精确计算计数的时间和个数,并讲解了两种控制led灯输出的方式,引出了重要的脉冲标志信号的以及其用法,同时还分析了稍微复杂的时序逻辑电路的RTL视图,一定要有硬件思想。通过本章的例子,我们应该继续加大根据分析绘制波形图技能的训练 ,这也仅仅是一个开始,真正的开始,希望大家能够通过后面章节的继续训练彻底掌握这种好的设计方法。

新语法总结

重点掌握

1、paramter的用法(现在模块内部的局部定义)

知识点总结

1、能够通过自己慢慢的分析绘制出时序逻辑电路的波形;

2、学会根据计数器的计数时钟来精确计算我们要想计数的时间和个数,熟练的控制计数器;

3、能够了解flag脉冲标志信号的意义,如何精确产生,以及应用场景;

4、学会分析简单时序逻辑的RTL视图,理解设计的RTL代码就是硬件的思想。

11.5. 拓展训练

1、如果把计数器的计数条件和清零条件的优先级互换会有什么不一样的效果?

2、如果我们只想让led灯闪烁10次该如何去实现呢?请按照本例的设计方法——画波形图、编写代码、仿真、上板验证尝试一下。