赞
踩
那么学会了计数器的使用,我们可以做很多的事情。可以毫不夸张的讲,在 FPGA 的设计当中,一切与时间有关的设计都会用到我们的计数器。
那么本小节的主要内容分为两个部分:第一部分是理论部分的学习,第二部分是实战部分的学习。
在理论部分的学习当中,我们会对计数器的相关知识做一个系统性的讲解。那么在实战部分,我们会通过实验工程设计并实现一个具有计数器功能的电路。
首先是理论学习
那么计数它是一种最为简单的也是最基本的一种运算,我们的计数器就是实现这种运算的逻辑电路。那么计数器在数字系统中主要是对脉冲的个数进行计数,比如说在 FPGA 当中,就是对时钟的脉冲进行计数;那么它可以实现测量、计数、控制的功能,同时兼有分频的功能。
计数器在数字系统中应用十分广泛,比如说计算机控制器中的指令地址需要进行计数,那么以便于顺序的取出下一条指令;在我们的运算器当中做乘法和除法的运算,同样需要计数器;那么在数字仪器当中对脉冲的计数也需要使用到我们的计数器。
同时,计数器也是在 FPGA 设计当中最常用的一种时序逻辑电路。那么根据计数器的计数值,我们可以精确的计算出 FPGA 内部各信号之间的时间关系,每个信号什么时候拉高、什么时候拉低,那么高电平要保持多久、低电平要保持多久,都可以由计数器实现精确的控制。而让计数器计数的是由外部晶振产生的时钟,所以说,我们可以比较精确的控制具体需要计数的时间。
我们的计数器一般是从 0 开始计数,计数到我们需要的计数值就可以进行清零,或者说计数器计满溢出清零。并且计数器可以进行不断的循环计数。
那么以上就是理论部分,对计数器相关知识的讲解。那么下面开始实战演练
在实战演练部分,我们会通过实验工程设计并实现一个具有计数器功能的电路。
它计数的时间间隔是 1s,因为我们的计数器是模块内部产生的,要对它进行验证,我们同样要使用我们板载的 LED 灯。在一个 1s 的时间间隔内,让我们的灯在前 0.5s 处于一个点亮的状态,在后 0.5s 处于一个熄灭的状态。这样就可以通过我们的 LED 灯来检测我们的计数器。
那么实验目标和检测方法了解了之后,下面开始程序的设计。
那么第一步就是需要建立一个文件体系
那么文件体系建立完成之后,我们打开 doc 文件夹。建立一个 Visio 文件,用来绘制模块框图和我们的模块波形图
那么下面开始模块框图的绘制。那么首先是模块的主体,然后给它取一个名字;接下来是端口信号,首先是输入信号,因为我们的计数器是对时钟信号进行计数,所以说一定有时钟信号;那么时钟信号绘制完成之后,下面是我们的复位信号,那么输入信号,除了我们的时钟信号和复位信号之外,再没有其他的输入信号。那么下面就是输出信号,为了方便验证,我们要引出一路输出信号到我们的 LED 灯
那么这样模块框图已经绘制完成,下面可以开始波形图的绘制。
在开始波形图的绘制之前,有一点要告诉大家就是:从本章节开始,我们后面将要接触到的实验工程都是时序电路的设计。那么波形设计在时序电路的设计当中是最有价值、也是最好用的。在前面的组合逻辑的设计当中,虽然我们也进行了波形图的绘制,但是大家可以感觉到,在前面组合逻辑的设计当中,波形图的绘制它的作用并不是很大,因为我们波形图的绘制,目的有两个:第一是帮助我们对模块功能加以理解,第二是有助于我们代码的编写。在前面组合逻辑的设计当中,我们的模块功能比较简单,而且我们可以使用真值表代替我们的波形图实现代码的编写。所以说,在前面组合逻辑的设计当中并没有涉及到波形图绘制的精髓之处。而在时序电路的设计当中,我们的模块功能相对比较复杂,需要波形图来帮助我们理解模块的功能,需要波形图来帮助我们进行代码的编写。所以说,大家一定要掌握波形图绘制的方法。那么下面开始波形图的绘制。
首先是我们的输入信号。那么先来绘制我们时钟信号的波形,我们时钟信号是由外部晶振传入的,它的频率是 50MHz;那么下面就是我们复位信号的波形绘制,我们让复位信号,在系统上电后保持一段时间有效的低电平进行复位,那么复位完成之后让它一直保持为高电平
那么这样,输入信号——时钟信号和复位信号的波形绘制完成。
我们本章节的目的就是教会大家如何使用好我们的计数器,那么计数器的使用有两点你需要掌握:第一点就是,控制好我们的计数器什么时候开始计数;第二点就是,控制好我们的计数器什么时候进行清零的问题。
首先,先考虑第一点:计数器什么时候开始计数。
在我们这个模块当中只有两路输入信号:一个是时钟信号,一个是复位信号。那么只要我们的复位信号已撤销,时钟沿到来就可以立刻进行计数。
那么接下来考虑第二点:我们的计数器什么时候进行清零。
那么计数器的清零有两种情况:第一种情况是,计数器记满了它会自动的清零;第二种情况就是,计数器计数到我们需要的计数值,然后进行清零。这样就引入了一个新的问题,哪一个计数值才是我们需要的计数值。
就拿本实验工程来讲,我们计数的时间间隔是 1s。那么我们就会考虑:计数 1s 的时间,需要计数器计数多少个个数,那么这儿就需要一个计算。
那么在前面我们已经提到了:我们的系统时钟,它的频率是
50
MHz
50\text{MHz}
50MHz。那么
50
MHz
50\text{MHz}
50MHz 经过单位换算,它也等于
5
×
1
0
4
kHz
5\times10^4\text{kHz}
5×104kHz,它也等于
5
×
1
0
7
Hz
5\times10^7\text{Hz}
5×107Hz。那么频率我们一般使用
f
\text{f}
f 来表示:
f
=
5
×
1
0
7
Hz
\text{f} = 5\times10^7\text{Hz}
f=5×107Hz
那么频率的概念它是什么意思呢?它表示的是:单位时间内,信号进行周期性变化的一个次数。我们的系统时钟它的频率是
5
×
1
0
7
Hz
5\times10^7\text{Hz}
5×107Hz 就表示它单位时间内进行了
5
×
1
0
7
5\times10^7
5×107 次周期性的一个变化。
那么每次变化用的时间,我们用 t \text{t} t 来表示。知道了系统时钟的频率之后,就可以计算我们的 t \text{t} t 了。那么怎么计算呢?
我们的频率表示的是,单位时间内系统进行周期性变化的一个次数,那么这个单位时间就是 1s,那么一秒之内进行了 5 × 1 0 7 5\times10^7 5×107 次周期性的一个变化。这样就可以得出:每个周期性变化所用的时间就是我们频率的倒数。那么针对我们的系统时钟 就应该这样写: t = 1 f = 1 5 × 1 0 7 s \text{t} = \frac{1}{\text{f}} = \frac{1}{5\times10^7}\text{s} t=f1=5×1071s 那么经过计算我们得到 t \text{t} t 它等于 5 × 1 0 − 8 s 5\times10^{-8}\text{s} 5×10−8s。那么这样经过单位换算就可以得到它等于 20 ns 20\text{ns} 20ns
那么通过这样的计算,就得到了我们的系统时钟,它的时钟周期是
20
ns
20\text{ns}
20ns。这就表示我们的计数器每进行一次计数,就计数时间
20
ns
20\text{ns}
20ns。那么下面就要确定一下,我们的计数器要完成一秒的计数,需要计数的最大值是多少。也就是我们要求出 1s 之内有多少个
20
ns
20\text{ns}
20ns,那么经过计算我们求出了这个数值就是
5
×
1
0
7
5\times10^7
5×107,那么这个数值我们使用
M
\text{M}
M 来表示。这也就表示:我们的计数器需要在
50
MHz
50\text{MHz}
50MHz 的时钟下计数
5
×
1
0
7
5\times10^7
5×107 个数才可以实现
1
s
1\text{s}
1s 的计数。但是还有一点要考虑:我们的计数器是从
0
0
0 开始计数的,所以说在
50
MHz
50\text{MHz}
50MHz 的时钟频率下计数一秒的时间,最终的计数值应该是
5
×
1
0
7
−
1
5\times10^7-1
5×107−1 也就是
M
−
1
\text{M}-1
M−1
f
=
50
MHz
=
5
×
1
0
4
kHz
=
5
×
1
0
7
Hz
t
=
1
f
=
1
5
×
1
0
7
s
=
20
ns
M
=
1
s
20
ns
=
5
×
1
0
7
M
−
1
=
5
×
1
0
7
−
1
那么了解了这个之后,下面可以开始波形图的绘制。
首先我们要声明一个变量就是计数器变量,我们把它命名为 cnt
,我们填充为黄色表示我们的中间变量
那么下面开始它的波形绘制。
首先是复位信号有效时给它赋一个初值,它的初值我们赋为 0。复位信号一旦撤销就开始计数,那么每个时钟周期它自加一,比如说到下一个时钟,到下一个时钟周期它这儿应该是计数为 1 了,那么再下一个时钟它应该就是 2 了,那么每个时钟周期自加一,依次往后计数。因为它计数的值比较大,我们这儿采取一个省略的画法。那么到这儿,它应该是从 3 计数到我们的 M/2-1。那么从 0 计到这儿,就相当于完成了 0.5s 的计数
那么下面这个位置就应该是 M/2 然后计数到我们 M-2 就是最大值的前一个值,那么最后一个值应该是最大值,就是计数到我们的 M-1。那么计数到最大值之后后面就应该是清零了
然后后面就是新的一个周期的计数
那么第二个周期计数到最大值 M-1 那么归零,开始第三个周期的一个计数,后面的波形我们就不再绘制了,大家理解一下就可以了。
那么这样,中间变量计数器 cnt 它的波形大概就这个样子。从 0 开始计数,计数到最大值 M-1 然后归零,开始下一个周期的计数,这样循环计数。
那么计数器的波形绘制完成之后,下面就是我们输出信号的波形。我们前面已经提到了:在一个一秒的时间间隔内,前 0.5s 点亮我们的 LED 灯,后 0.5s 让它保持一个熄灭状态。而我们板载的 LED 灯,它是低电平点亮,高电平是处于熄灭状态。所以说,我们的输出信号在计数器的前 0.5s 的计数范围内应该是保持一个低电平,那么在后 0.5s 的范围内应该是保持一个高电平;这样就可以在前 0.5s 我们的 LED 灯处于点亮状态,后 0.5s 处于熄灭状态,这样就能实现我们 LED 灯一个闪烁的一个效果。
那么下面开始输出信号波形的绘制。首先在我们的复位信号有效时,我们让它保持一个低电平,就初值是 0。然后,在计数器计数的前 0.5s 也让它保持一个低电平,那么这样就是点亮我们的 LED 灯,就到这个位置。那么后 0.5s,让我们的 LED 灯处于一个熄灭状态,那么输出信号就应该是保持高电平(因为我们的 LED 灯是低电平点亮、高电平熄灭)
那么计数到最大值表示一秒的时间间隔计数完成,那么就开始归零,也就是下一个周期的计数。那么在下一个周期的一秒计数之内,前 0.5s 还是点亮我们的 LED 灯,所以说我们的输出信号由高电平又变为了低电平,然后,在下一个周期的后 0.5s 又由低电平变为了高电平,我们的 LED 灯又由点亮变为了熄灭状态。然后到第二个周期,我们计数器计数到最大值那么又归零了,又要将我们的输出信号拉低
那么这样,我们的输出信号的波形也绘制完成,那这样我们模块的波形图已经绘制完成。那么这样我们的波形图绘制完成之后,其实就可以开始进行代码的编写了,但是我们这儿,有一个需要改进的地方。什么地方呢?
我们在想:在一个 1s 的时间间隔内,前 0.5s 是输出低电平,那么后 0.5s 输出高电平,而且这一个周期内,高电平和低电平它保持的时间都是 0.5s。那么我们再想:为什么我们的计数器的最大值要计数到 M-1 呢?它直接计数到 M/2-1 不就好了吗?计数到最大值 M/2-1 的时候对我们的输出信号进行一个反转,那么同样可以实现我们的 LED 灯 0.5s 点亮 0.5s 熄灭。那么什么意思呢?我们来绘制一下波形图
比如说这个样子,我们的计数器计数到最大值 M/2-1 就直接进行清零。也就是说,它的波形应该是这个样子
那么这样,我们计数器的波形修改完成。我们的计数器从 0 开始计数,它的最大值是 M/2-1,计数到最大值之后归零,重新下一个周期的计数。我们的输出信号初值为 0,然后第一个计数周期,保持它的低电平;当计数器计数到最大值对它进行取反,那么就是高电平,下一个周期计数到最大值再进行取反,就是低电平;这样不断的取反,就可以实现高电平、低电平它们之间的切换,而且高电平和低电平保持的时间,都是一个完整的计数周期,就是 0.5s。
这样同样可以实现与上面波形图相同的一个效果。
那么有的朋友肯定要问了:既然第一种方法能够实现,我们为什么要使用第二种方法呢?
这儿我们考虑到资源量的问题。我们第一种方法,计数到最大值是 M-1,那么我们的计数器需要多少位宽呢?我们来算一下。那么在十进制状态下,输入 M-1 对应的数值,然后切换成二进制
我们可以看到,它需要 26 个比特位,也就是说我们的计数器它的位宽要设置为 26 位宽,也就是 cnt[25:0]
而如果计数的最大值计数到 M/2-1 呢?我们来看一下
它只需要 25 个比特位,也就是说它的位宽可以是 25 位宽,也就是 cnt[24:0] 那么这样就可以节省我们寄存器的资源
那么有的朋友可能还会说:那我们的 FPGA 资源够哇!可以实现。是的,在这个实验当中,我们 FPGA 的资源是相当够用的,但是我们要养成这样一个习惯:一个精简设计的一个习惯。如果说在很大的工程的设计当中,我们节省的资源可以用到其他地方,这样使我们的设计更精简,花最小的资源量使整个系统的性能达到最优。
所以说我们这儿采用第二种方法。
那么下面就开始代码的编写。那么首先是模块开始,然后是模块名称,然后是端口列表、模块结束
输入信号有我们的时钟信号,还有我们的复位信号。然后是输出信号 led_out
接下来声明一个变量,就是我们的计数器变量,它的位宽是 25 位宽。因为计数器这个变量我们使用 always 进行赋值,所以说这儿使用 reg 型
然后我们还要进行一个参数的定义。那么参数的定义,在前面 Verilog 基础语法当中已经讲到了,但是这段时间我们一直没有使用,这儿就进行一个参数的定义,大家看一下用法。它的关键词是 parameter 然后我们定义的参数是 计数器的计数最大值,它的名称一般使用的是大写,这样有利于区分,我们这儿计数的最大值是 M/2-1 也就是 24_999_999 使用下划线是增强可读性,这儿加上它的位宽,然后是十进制
那么这儿就进行了一个参数的定义。
那么参数的定义,除了可以使用 parameter 还可以使用 localparam
localparam CNT_MAX = 25'd24_999_999;
那么这两个都可以进行参数的定义,但是它俩是有区别的。
那么第一点是:它们两个都可以在模块内部作为一个参数的定义,我们的 localparam 只能使用在模块内部进行参数的定义。那么 parameter 它既可以使用在模块内部进行参数定义,同时它还可以写在这个位置
module counter
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
output wire led_out
);
如果 parameter 写在这个位置
module counter
(
input wire sys_clk ,
input wire sys_rst_n ,
output wire led_out
);
parameter CNT_MAX = 25'd24_999_999;
reg [24:0] cnt;
endmodule
是在模块内部进行一个普通的参数定义。
那么写在这个位置
module counter
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
output wire led_out
);
reg [24:0] cnt;
endmodule
它可以作为在实例化中参数传递的一个接口,什么意思呢?我们这儿来做个示范。
比如说,在一个顶层文件当中我们要实例化这个计数器。首先是实例化名称,那么这儿就用一个点来连接,后面是括号;然后这儿,也是用点来连接,后面是括号;然后进行信号的传递。如果是在实例化当中,我们在这儿可以进行参数的一个修改。比如说,我们在这儿设置为一个 100;如果说在模块内部,这个参数定义的是 M/2-1,但是如果在模块的实例化当中,我们在这个位置进行了修改,那么实际在运行过程中,我们的模块是按照这个 100 的参数来进行计数的,与这个参数就没有关系了
那么这样的好处是什么呢?
就是说如果说对这个模块实例化两次
counter counter_inst1 #( .CNT_MAX (100) ) ( .sys_clk (sys_clk), .sys_rst_n(sys_rst_n), .led_out (led_out) ); counter counter_inst2 #( .CNT_MAX (500) ) ( .sys_clk (sys_clk), .sys_rst_n(sys_rst_n), .led_out (led_out) );
虽然是实例化的同一个模块,但是我实例化两次,可以让这个模块实现两个不同的计数器功能,计数的时间间隔可以是不同的。
同样的,在仿真的时候如果在实例化时对 parameter 参数进行一个修改(比如说如果不进行修改,还是 M/2-1。但是如果我们把 parameter 参数修改的小一些),我们可以缩短我们的仿真时间,但是对我们的仿真结果并没有影响。
那么以上,就是对 parameter 参数传递的一个介绍。
那么这儿还有一点需要注意,就是带有参数接口的模块实例化,实例化的名称应该写在端口列表的前面,而不是写在模块名的后面
counter #( .CNT_MAX (100) ) counter_inst1 ( .sys_clk (sys_clk), .sys_rst_n(sys_rst_n), .led_out (led_out) ); counter #( .CNT_MAX (500) ) counter_inst2 ( .sys_clk (sys_clk), .sys_rst_n(sys_rst_n), .led_out (led_out) );
这儿大家要注意一下。
我们这儿使用一个参数传递的一个方法,来定义它的参数
那么下面开始计数器变量的赋值。我们使用 always 语句,使用我们的异步复位,然后给我们的计数器变量赋一个初值;当我们复位信号有效时,我们的计数器变量赋一个初值赋为 0,初值赋为 0 和我们波形图是一致的。当它计数到最大值 M/2-1 的时候让它归零,这是一个 else if 语句。那么这儿如果没有进行参数的定义,我们这儿就需要直接写这个数。当然了这样也是可以的,但是我们这儿为什么要进行参数的定义呢?我们可以直接调用这个参数。如果说在一个模块当中
有多个 always 语句,都使用这个数值作为一个条件判断,那么我们把它定义为参数就可以直接很好的调用;第二点就是,如果说这个参数需要修改,假如有多个 always 语句都使用了这个参数,参数要修改的话,就需要对每个 always 语句它的条件进行一个逐一的修改,这样太麻烦了;而我们如果进行一个参数的定义,直接对这个参数的定义进行一个修改,那么就可以实现模块内部所有调用的这个参数,统一的一个修改
当计数到最大值让它归零;那么如果我们的复位条件无效而且没有计数到最大值,每个时钟周期让它自加一
那么这样参照着它的波形图,我们就完成了计数器 cnt 的一个赋值
那么下面开始输出信号的赋值。那么输出信号,我们同样打算使用 always 的语句,它的变量类型就应该是 reg 型。当我们的复位信号有效时给它一个低电平,然后当我们的计数信号计数到最大值 M/2-1 这个位置,对它一个数据进行一个取反,那么这儿用到了一个取反符号:~
前面已经讲过了。那么如果我们的复位信号无效,而且没有计数到最大值,那么就 0 到 M/2-2 这个范围内,它就保持它原来的电平
那么这样,模块代码就编写完成,我们保存。
counter.v
module counter #( parameter CNT_MAX = 25'd24_999_999 ) ( input wire sys_clk , input wire sys_rst_n , output reg led_out ); reg [24:0] cnt; always@(posedge sys_clk or negedge sys_rst_n) if (sys_rst_n == 1'b0) cnt <= 25'd0; else if (cnt == CNT_MAX) cnt <= 25'd0; else cnt <= cnt + 25'd1; 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; else led_out <= led_out; endmodule
那么下面就进行代码的编译,来检测我们的语法错误
回到我们的桌面,建立一个新的工程
然后加载我们的 .v 文件,就是刚才编写的代码;然后点击 Start Compilation 这个位置进行全编译,查找我们的语法错误
那么编译完成 7 个警告信息,我们选择忽略。
代码通过编译之后,我们来查看一下 RTL 视图
那么这个图形,就是根据我们的 RTL 代码综合出来的 RTL 视图。它有一个加法器、一个选择器,然后两个寄存器分别对应我们的计数器和我们的输出信号。这儿就是一个 always 语句生成一个寄存器,那么这儿生成了两个。另一个是比较器。
我们来看一下它是怎么运行的。首先,我们的加法器它有两个加数,一个加数就是 1,25 位宽的 1:25'h0000001
,对于代码当中就是我们定义的 25'd1
;然后另一个加数是我们的寄存器反馈的一个值。然后两个数的加和会传到多路选择器当中,然后它的选择条件是由比较器传入的,那么比较器两个比较值,一个是我们寄存器传入的一个值,就是我们的计数器寄存器传入的一个值,另一个值 25'h17D783F
是我们设定的一个参数 CNT_MAX
;我们计数器的输出值与这个最大值参数相比较,如果说计数器的计数值等于这个最大值,它就输出高电平;如果是高电平输入给多路选择器 MXU21,MUX21 它就将 DATAB 这个值由 OUT0 输出,就是将它归零了,计数到最大值进行归零,对应的就是这条语句
else if (cnt == CNT_MAX)
cnt <= 25'd0;
将它归零了
如果说 MUX21 的选择信号是低电平,就将加法器 Add0 传入的 DATAA 通过 OUT0 输出,到达我们的寄存器 cnt[24:0];那么在时钟的上升沿寄存器 cnt[24:0] 就将它传出,那么同样到了 Equal0 的输出这个位置,如果比较器的信号是高电平,就传入输出寄存器 led_out~reg0 的使能信号,那么它为高电平,那么就将 Q 的输出信号取反给 D 然后再重新赋值给 Q 这个位置对应的是这句话
else if (cnt == CNT_MAX)
led_out <= ~led_out;
如果说这个使能信号无效,也就是说比较器传入的是低电平,那么输出寄存器 led_out~reg0 的输出一直保持原来的 Q 值,对应的就是这句话
else
led_out <= led_out;
那么以上就是对 RTL 视图的一个讲解,那么下面就要对我们的代码进行一个仿真验证。
首先需要编写仿真代码,那么仿真代码的编写大家也应该很熟悉了
首先是时间参数,然后是模块开始、模块名称,然后是端口列表(这儿是空),然后是模块结束
接下来声明两个变量,就是模拟时钟信号和模拟复位信号;后面将使用 initial 语句对它们进行赋值,所以说这儿应该是 reg 型,然后要引出模块的输出信号,用 wire 型;那么接下来就开始初始化我们的输入信号,使用 initial 语句
那么这儿是时钟信号赋初值为 1;复位信号是初值为 0 然后过 20ns 将我们的复位信号拉高。
那么下面是产生我们的时钟信号,给它定义一个频率;这儿的时间间隔设置为 10ns,那么每 10ns 让我们的时钟信号进行一次反转,那么产生一个 50MHz 的时钟信号
下面就开始实例化。那么为了方便编写,我们可以通过这种方式:点中仿真文件名进行拖拽,拖拽到右侧移动到另一视图;这样在一个界面当中就可以查看两个代码,这样方便我们的实例化
那么这个位置也加一个点,就相当于连接;等号不需要了,在这儿直接加一个括号;那么这儿就可以写入我们想要传递的一个参数,比如说我们写入 24,那么在模块仿真的时候,CNT_MAX 这个数值就没有任何意义了,那么它的值就变为了 24 我们这儿定义了 24 这儿也可以加上位宽;然后是连接我们的输入、输出端口
那么这样就完成了仿真代码的编写,保存
tb_counter.v
`timescale 1ns/1ns module tb_counter(); reg sys_clk; reg sys_rst_n; wire led_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; counter #( .CNT_MAX (25'd24) ) counter_inst ( .sys_clk (sys_clk), .sys_rst_n(sys_rst_n), .led_out (led_out) ); endmodule
回到我们的实验工程,然后添加我们的仿真代码;然后进行仿真的设置
然后开始仿真
我们打开 sim 窗口,那么添加我们的模块波形,也可以使用快捷键 Ctrl+W;打开波形界面窗口 Ctrl+A 全选,Ctrl+G 进行分组,那么去除波形名称前缀;那么点击 Restart 清除已存在波形,然后时间参数设置为 10us 运行一次;查看全局视图
那么这个波形看似和我们绘制的差不多,但是我们要局部放大,针对我们的绘制波形图来进行一下对比。
那么时钟信号和复位信号,波形是一致的
然后我们添加三个参考线
我们的复位信号它前面低电平是保持了 20ns 是正确的;那么第二条和第三条这两条参考线之间是一个时钟周期是 20ns,那么我们的时钟信号频率是 50MHz,那么是正确的。那么输入信号俩波形是正确的
接下来看我们的计数器
我们的计数器初值是为 0 然后每个时钟周期自加一,加到最大值 24,那么在 24 它归零了;归零了之后又继续进行计数,每个时钟周期自加一,到了 24 它又归零了。那么一个周期是 0 到 24 与我们的波形也是一致的;计数到最大值归零,再进行下一个周期的计数,与波形图是对应的,没有问题。
下面看一下输出信号
那么输出信号初值为 0 那么在计数器的第一个周期保持低电平,当计数到最大值 24 然后对它进行取反,那么取反之后到下一个计数周期的最大值,又对它进行取反,那么它的波形变化也是正确的,和我们的绘制波形是一致的。
那么下面我们修改一下我们的仿真代码,增加两个系统函数
那么系统函数添加完成之后,保存
tb_counter.v
`timescale 1ns/1ns module tb_counter(); reg sys_clk; reg sys_rst_n; wire led_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; initial begin $timeformat(-9,0,"ns",6); $monitor("@time %t:led_out = %b", $time, led_out); end counter #( .CNT_MAX (25'd24) ) counter_inst ( .sys_clk (sys_clk), .sys_rst_n(sys_rst_n), .led_out (led_out) ); endmodule
回到我们的 ModelSim
那么刚才仿真波形与我们绘制波形图已经对比完成,是相同的。我们来看一下打印信息,这儿我们增加一下仿真时间,首先清除全部波形,仿真时间设置为 10us 然后运行一次,查看我们的打印信息
这儿我们可以看到:输出信号的高低电平呈周期性的变化,而且变化周期,看这个时间参数,周期是相同的,都是 500ns
那么我们的系统上电之后,前 20ns 保持低电平,而且第一个时钟周期也保持低电平,所以说前 520ns 一直是低电平;到了第 520ns 之后变为高电平,那么到了 1020ns 之后又回到了低电平。是呈周期性变化的,每个周期是 500ns。这儿为什么是 500ns 呢?
因为我们实例化 counter 模块时对 CNT_MAX 参数定义的是 24,计数器的最大值是 24,那么 0~24 是 25 个周期,一个周期是 20ns,这儿就是 500ns 与上面打印信息的 500ns 周期是对应的。
仿真波形图与我们绘制的波形图是正确的,那么打印的参数也是正确的,那么仿真验证通过,回到我们的实验工程。
下面就绑定我们的管脚。那么输出信号是传递到 LED 灯,我们使用板载的 D6 LED 灯,它的引脚对应 L7;我们的时钟信号是由晶振传入,引脚是 E1;复位信号是由复位按键传入,引脚对应是 M15
管脚绑定完成之后,重新编译;那么全编译完成,点击 OK
那么按照我们下图所示连接我们的下载器,然后和我们的电源;那么下载器的另一端连接到电脑,然后给开发板上电
然后回到实验工程,点击 Programmer 这个位置打开下载界面,然后添加我们的 SOF 文件,点击开始进行下载
那么程序下载完成
我们可以看到 (D6)LED 灯进行一个闪烁,时间间隔大概为一秒,那么这样上板验证成功。
那么上板验证通过之后,我们回到波形图界面。这里我们再采取另外一种看上去多此一举的方法,实现我们的计数器。这里我们定义一个新的变量:脉冲信号 cnt_flag,当我们的复位信号有效时让它保持一个低电平,当我们的计数器计数到最大值 M/2-1 时,先不对输出信号进行取反,而是让我们的 cnt_flag 信号,产生一个时钟周期的高脉冲,那么其他时刻让它保持一个低电平。这样,脉冲信号的波形就是这样
然后我们的输出信号,那么输出信号初值还是低电平,当它检测到我们的脉冲信号为高电平时,对它进行一个取反,那么这样输出信号的波形就是
我们将这个波形与上面一个波形做对比
那么通过对比可以发现,如果使用原来的方法可以发现,我们的输出信号是与我们的计数周期完整对应的;但是我们新的方法,就是使用脉冲信号的方法,它错开了一个时钟周期;虽然这种情况,在我们现在的实验工程中不会对结果造成多大的影响,但是在一些对精确度要求比较高的工程中,这样的方法是不合适的,所以说我们对它进行一下改进。
我们的脉冲信号初值仍为 0,当我们的计数器计数到 M/2-2 的时候让它保持一个时钟周期的高电平;然后我们的输出信号初值仍为 0,那么检测到脉冲信号的高电平让它进行取反,那么这样,使用脉冲信号的最终的波形图已经绘制完成
通过对比可以发现:输出信号与计数器变量对应关系,两幅波形图之内是一样的。
可能有的朋友会问:为什么一定要使用这个脉冲标志信号呢?我们使用第一种方法实现我们的计数器不好吗?
当然不是。其实在这里我们是想引出一个非常有用的信号,就是我们刚刚使用的脉冲标志信号 flag。这种信号在后面会经常使用,它可以使我们代码中 if 括号内的条件更加的清晰简洁,而且当需要在多处使用脉冲标志信号的地方,要比全部写出的方式更节省我们的逻辑资源;脉冲标志信号在指示某些状态时是非常有用的,让大家以后在实现相对复杂的逻辑功能时,注意想到使用我们的脉冲标志信号。那么除了脉冲标志信号,后面我们还会介绍另一个有用的信号,叫做使能信号。
那么接下来参照着这个波形图,对我们的代码进行修改
首先需要声明一个新的变量,就是我们的脉冲信号,我们同样使用 always 语句赋值,所以说使用 reg 型。那么当复位信号有效时,我们的脉冲信号初值给它一个低电平,那么与波形图当中是对应的,给它一个低电平;然后我们计数器计数到最大值减二的时候,拉高一个时钟周期的高电平;那么其他时刻让它处于一个低电平
那么这样脉冲信号的代码,我们参照波形图已经编写完成,下面开始修改我们的输出信号的代码
那么修改完成之后,保存
counter.v
module counter #( parameter CNT_MAX = 25'd24_999_999 ) ( input wire sys_clk , input wire sys_rst_n , output reg led_out ); reg [24:0] cnt; reg cnt_flag; always@(posedge sys_clk or negedge sys_rst_n) if (sys_rst_n == 1'b0) cnt <= 25'd0; else if (cnt == CNT_MAX) cnt <= 25'd0; else cnt <= cnt + 25'd1; always@(posedge sys_clk or negedge sys_rst_n) if (sys_rst_n == 1'b0) cnt_flag <= 1'b0; else if (cnt == (CNT_MAX-1)) cnt_flag <= 1'b1; else cnt_flag <= 1'b0; 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; else led_out <= led_out; endmodule
打开我们的实验工程,重新进行编译,那么编译完成,点击 OK
然后,查看一下 RTL 视图。我们来看一下 RTL 视图
那么 RTL 视图与之前的 RTL 视图相比,多了一个寄存器,也多了一个比较器。
那么下面对我们的代码进行一个仿真验证。打开我们的 ModelSim,找到 Library,然后对我们的文件进行一个重编译,因为我们刚才修改了,那么这儿显示编译完成
然后使用 Restart 然后全选、删除,回到我们的 sim 重新加载我们的波形,因为我们刚才添加了一个新的信号;然后全选、分组,这样我们的 cnt_flag 信号也已经加入了;然后运行 10us 全局视图
我们这儿主要查看一下我们的 cnt_flag 信号,还有我们的输出信号的变化
首先,我们的脉冲信号初值为 0;当我们的计数器计数到最大值减一的时候,拉高一个时钟周期的高电平,因为寄存器有延迟一拍的一个效果,所以说它拉高的时钟周期刚好与计数器最大值是吻合的。然后,我们使用 cnt_flag 信号作为条件对我们的输出信号进行取反,我们的输出信号同样使用的是寄存器,有一个延迟一拍的一个效果,所以说我们输出信号的电平保持,刚好与我们计数器的一个完整的周期是对应的。
如果只针对输出信号来看,与我们之前的仿真波形是一致的,与我们绘制的波形图也是一致的,那么仿真验证通过。
回到我们的实验工程,然后再进行一次全编译,编译完成,然后下载到我们的开发板
我们开发板上的实验效果与之前的实验效果是相同的
那么验证通过
那么以上就是我们本章节的全部内容
参考资料:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。