[FPGA 学习记录] 计数器

计数器


在前面的章节当中我们讲解了:在时序逻辑电路当中,最基本的单元就是我们的寄存器。那么在本小节当中我们就使用寄存器来实现一个具有计数器功能的电路。

那么学会了计数器的使用,我们可以做很多的事情。可以毫不夸张的讲,在 FPGA 的设计当中,一切与时间有关的设计都会用到我们的计数器。

那么本小节的主要内容分为两个部分:第一部分是理论部分的学习,第二部分是实战部分的学习。

在理论部分的学习当中,我们会对计数器的相关知识做一个系统性的讲解。那么在实战部分,我们会通过实验工程设计并实现一个具有计数器功能的电路。

首先是理论学习

1 理论学习

那么计数它是一种最为简单的也是最基本的一种运算,我们的计数器就是实现这种运算的逻辑电路。那么计数器在数字系统中主要是对脉冲的个数进行计数,比如说在 FPGA 当中,就是对时钟的脉冲进行计数;那么它可以实现测量、计数、控制的功能,同时兼有分频的功能。

计数器在数字系统中应用十分广泛,比如说计算机控制器中的指令地址需要进行计数,那么以便于顺序的取出下一条指令;在我们的运算器当中做乘法和除法的运算,同样需要计数器;那么在数字仪器当中对脉冲的计数也需要使用到我们的计数器。

同时,计数器也是在 FPGA 设计当中最常用的一种时序逻辑电路。那么根据计数器的计数值,我们可以精确的计算出 FPGA 内部各信号之间的时间关系,每个信号什么时候拉高、什么时候拉低,那么高电平要保持多久、低电平要保持多久,都可以由计数器实现精确的控制。而让计数器计数的是由外部晶振产生的时钟,所以说,我们可以比较精确的控制具体需要计数的时间。

我们的计数器一般是从 0 开始计数,计数到我们需要的计数值就可以进行清零,或者说计数器计满溢出清零。并且计数器可以进行不断的循环计数。

那么以上就是理论部分,对计数器相关知识的讲解。那么下面开始实战演练

2 实战演练

2.1 实验目标

在实战演练部分,我们会通过实验工程设计并实现一个具有计数器功能的电路。

2.2 硬件资源

它计数的时间间隔是 1s,因为我们的计数器是模块内部产生的,要对它进行验证,我们同样要使用我们板载的 LED 灯。在一个 1s 的时间间隔内,让我们的灯在前 0.5s 处于一个点亮的状态,在后 0.5s 处于一个熄灭的状态。这样就可以通过我们的 LED 灯来检测我们的计数器。

image-20231031160450103

那么实验目标和检测方法了解了之后,下面开始程序的设计。

2.3 程序设计

那么第一步就是需要建立一个文件体系

20231030183526_KpSWJogX8B

2.3.1 模块框图

那么文件体系建立完成之后,我们打开 doc 文件夹。建立一个 Visio 文件,用来绘制模块框图和我们的模块波形图

sNyCnWVhLk

那么下面开始模块框图的绘制。那么首先是模块的主体,然后给它取一个名字;接下来是端口信号,首先是输入信号,因为我们的计数器是对时钟信号进行计数,所以说一定有时钟信号;那么时钟信号绘制完成之后,下面是我们的复位信号,那么输入信号,除了我们的时钟信号和复位信号之外,再没有其他的输入信号。那么下面就是输出信号,为了方便验证,我们要引出一路输出信号到我们的 LED 灯

20231030184207_ZxxqcUmR2x

那么这样模块框图已经绘制完成,下面可以开始波形图的绘制。

2.3.2 波形绘制

在开始波形图的绘制之前,有一点要告诉大家就是:从本章节开始,我们后面将要接触到的实验工程都是时序电路的设计。那么波形设计在时序电路的设计当中是最有价值、也是最好用的。在前面的组合逻辑的设计当中,虽然我们也进行了波形图的绘制,但是大家可以感觉到,在前面组合逻辑的设计当中,波形图的绘制它的作用并不是很大,因为我们波形图的绘制,目的有两个:第一是帮助我们对模块功能加以理解,第二是有助于我们代码的编写。在前面组合逻辑的设计当中,我们的模块功能比较简单,而且我们可以使用真值表代替我们的波形图实现代码的编写。所以说,在前面组合逻辑的设计当中并没有涉及到波形图绘制的精髓之处。而在时序电路的设计当中,我们的模块功能相对比较复杂,需要波形图来帮助我们理解模块的功能,需要波形图来帮助我们进行代码的编写。所以说,大家一定要掌握波形图绘制的方法。那么下面开始波形图的绘制。

首先是我们的输入信号。那么先来绘制我们时钟信号的波形,我们时钟信号是由外部晶振传入的,它的频率是 50MHz;那么下面就是我们复位信号的波形绘制,我们让复位信号,在系统上电后保持一段时间有效的低电平进行复位,那么复位完成之后让它一直保持为高电平

20231030184843_G9vz0BQRNA

那么这样,输入信号——时钟信号和复位信号的波形绘制完成。

我们本章节的目的就是教会大家如何使用好我们的计数器,那么计数器的使用有两点你需要掌握:第一点就是,控制好我们的计数器什么时候开始计数;第二点就是,控制好我们的计数器什么时候进行清零的问题。

首先,先考虑第一点:计数器什么时候开始计数。

在我们这个模块当中只有两路输入信号:一个是时钟信号,一个是复位信号。那么只要我们的复位信号已撤销,时钟沿到来就可以立刻进行计数。

那么接下来考虑第二点:我们的计数器什么时候进行清零。

那么计数器的清零有两种情况:第一种情况是,计数器记满了它会自动的清零;第二种情况就是,计数器计数到我们需要的计数值,然后进行清零。这样就引入了一个新的问题,哪一个计数值才是我们需要的计数值。

就拿本实验工程来讲,我们计数的时间间隔是 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×108s。那么这样经过单位换算就可以得到它等于 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×1071 也就是 M − 1 \text{M}-1 M1
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 \begin{align*} \text{f} &= 50\text{MHz}\\ &= 5\times10^4\text{kHz}\\ &= 5\times10^7\text{Hz}\\ \\ \text{t} &= \frac{1}{\text{f}}\\ &= \frac{1}{5\times10^7}\text{s}\\ &= 20\text{ns}\\ \\ \text{M} &= \frac{1\text{s}}{20\text{ns}}\\ &= 5\times10^7\\ \text{M}-1 &= 5\times10^7-1 \end{align*} ftMM1=50MHz=5×104kHz=5×107Hz=f1=5×1071s=20ns=20ns1s=5×107=5×1071
那么了解了这个之后,下面可以开始波形图的绘制。

首先我们要声明一个变量就是计数器变量,我们把它命名为 cnt,我们填充为黄色表示我们的中间变量

20231030201414_zuBf1Ngm6k

那么下面开始它的波形绘制。

首先是复位信号有效时给它赋一个初值,它的初值我们赋为 0。复位信号一旦撤销就开始计数,那么每个时钟周期它自加一,比如说到下一个时钟,到下一个时钟周期它这儿应该是计数为 1 了,那么再下一个时钟它应该就是 2 了,那么每个时钟周期自加一,依次往后计数。因为它计数的值比较大,我们这儿采取一个省略的画法。那么到这儿,它应该是从 3 计数到我们的 M/2-1。那么从 0 计到这儿,就相当于完成了 0.5s 的计数

20231030202243_tduCsGrx8G

那么下面这个位置就应该是 M/2 然后计数到我们 M-2 就是最大值的前一个值,那么最后一个值应该是最大值,就是计数到我们的 M-1。那么计数到最大值之后后面就应该是清零了

20231030202840_KkCznh3HGr

然后后面就是新的一个周期的计数

20231030203150_M9zzBSz1kq

那么第二个周期计数到最大值 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 灯是低电平点亮、高电平熄灭)

20231030205024_QFygHNr9Re

那么计数到最大值表示一秒的时间间隔计数完成,那么就开始归零,也就是下一个周期的计数。那么在下一个周期的一秒计数之内,前 0.5s 还是点亮我们的 LED 灯,所以说我们的输出信号由高电平又变为了低电平,然后,在下一个周期的后 0.5s 又由低电平变为了高电平,我们的 LED 灯又由点亮变为了熄灭状态。然后到第二个周期,我们计数器计数到最大值那么又归零了,又要将我们的输出信号拉低

20231030205316_vIGhIarZt9

那么这样,我们的输出信号的波形也绘制完成,那这样我们模块的波形图已经绘制完成。那么这样我们的波形图绘制完成之后,其实就可以开始进行代码的编写了,但是我们这儿,有一个需要改进的地方。什么地方呢?
我们在想:在一个 1s 的时间间隔内,前 0.5s 是输出低电平,那么后 0.5s 输出高电平,而且这一个周期内,高电平和低电平它保持的时间都是 0.5s。那么我们再想:为什么我们的计数器的最大值要计数到 M-1 呢?它直接计数到 M/2-1 不就好了吗?计数到最大值 M/2-1 的时候对我们的输出信号进行一个反转,那么同样可以实现我们的 LED 灯 0.5s 点亮 0.5s 熄灭。那么什么意思呢?我们来绘制一下波形图

比如说这个样子,我们的计数器计数到最大值 M/2-1 就直接进行清零。也就是说,它的波形应该是这个样子

20231030205825_lP12xTGyxR

那么这样,我们计数器的波形修改完成。我们的计数器从 0 开始计数,它的最大值是 M/2-1,计数到最大值之后归零,重新下一个周期的计数。我们的输出信号初值为 0,然后第一个计数周期,保持它的低电平;当计数器计数到最大值对它进行取反,那么就是高电平,下一个周期计数到最大值再进行取反,就是低电平;这样不断的取反,就可以实现高电平、低电平它们之间的切换,而且高电平和低电平保持的时间,都是一个完整的计数周期,就是 0.5s。

这样同样可以实现与上面波形图相同的一个效果。

那么有的朋友肯定要问了:既然第一种方法能够实现,我们为什么要使用第二种方法呢?

这儿我们考虑到资源量的问题。我们第一种方法,计数到最大值是 M-1,那么我们的计数器需要多少位宽呢?我们来算一下。那么在十进制状态下,输入 M-1 对应的数值,然后切换成二进制

20231030210422_Jo6tA6B3Wr

我们可以看到,它需要 26 个比特位,也就是说我们的计数器它的位宽要设置为 26 位宽,也就是 cnt[25:0]
而如果计数的最大值计数到 M/2-1 呢?我们来看一下

20231030210536_l6xcVXWfy3

它只需要 25 个比特位,也就是说它的位宽可以是 25 位宽,也就是 cnt[24:0] 那么这样就可以节省我们寄存器的资源

image-20231030211421718

那么有的朋友可能还会说:那我们的 FPGA 资源够哇!可以实现。是的,在这个实验当中,我们 FPGA 的资源是相当够用的,但是我们要养成这样一个习惯:一个精简设计的一个习惯。如果说在很大的工程的设计当中,我们节省的资源可以用到其他地方,这样使我们的设计更精简,花最小的资源量使整个系统的性能达到最优。

所以说我们这儿采用第二种方法。

2.3.3 编写代码

那么下面就开始代码的编写。那么首先是模块开始,然后是模块名称,然后是端口列表、模块结束

XExu8AgndS

输入信号有我们的时钟信号,还有我们的复位信号。然后是输出信号 led_out

DA1D3tu4aJ

接下来声明一个变量,就是我们的计数器变量,它的位宽是 25 位宽。因为计数器这个变量我们使用 always 进行赋值,所以说这儿使用 reg 型

y1o9DnRLfv

然后我们还要进行一个参数的定义。那么参数的定义,在前面 Verilog 基础语法当中已经讲到了,但是这段时间我们一直没有使用,这儿就进行一个参数的定义,大家看一下用法。它的关键词是 parameter 然后我们定义的参数是 计数器的计数最大值,它的名称一般使用的是大写,这样有利于区分,我们这儿计数的最大值是 M/2-1 也就是 24_999_999 使用下划线是增强可读性,这儿加上它的位宽,然后是十进制

WZIyuMXTwZ

那么这儿就进行了一个参数的定义。

那么参数的定义,除了可以使用 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 的参数来进行计数的,与这个参数就没有关系了

20231030213837_7ArVpPSeSQ

那么这样的好处是什么呢?

就是说如果说对这个模块实例化两次

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)
);

这儿大家要注意一下。

我们这儿使用一个参数传递的一个方法,来定义它的参数

20231030214559_1O6qNPFCFQ

那么下面开始计数器变量的赋值。我们使用 always 语句,使用我们的异步复位,然后给我们的计数器变量赋一个初值;当我们复位信号有效时,我们的计数器变量赋一个初值赋为 0,初值赋为 0 和我们波形图是一致的。当它计数到最大值 M/2-1 的时候让它归零,这是一个 else if 语句。那么这儿如果没有进行参数的定义,我们这儿就需要直接写这个数。当然了这样也是可以的,但是我们这儿为什么要进行参数的定义呢?我们可以直接调用这个参数。如果说在一个模块当中
有多个 always 语句,都使用这个数值作为一个条件判断,那么我们把它定义为参数就可以直接很好的调用;第二点就是,如果说这个参数需要修改,假如有多个 always 语句都使用了这个参数,参数要修改的话,就需要对每个 always 语句它的条件进行一个逐一的修改,这样太麻烦了;而我们如果进行一个参数的定义,直接对这个参数的定义进行一个修改,那么就可以实现模块内部所有调用的这个参数,统一的一个修改

20231030215141_r6sxhKGXxg

当计数到最大值让它归零;那么如果我们的复位条件无效而且没有计数到最大值,每个时钟周期让它自加一

nZwsQHPTkc

那么这样参照着它的波形图,我们就完成了计数器 cnt 的一个赋值
那么下面开始输出信号的赋值。那么输出信号,我们同样打算使用 always 的语句,它的变量类型就应该是 reg 型。当我们的复位信号有效时给它一个低电平,然后当我们的计数信号计数到最大值 M/2-1 这个位置,对它一个数据进行一个取反,那么这儿用到了一个取反符号:~ 前面已经讲过了。那么如果我们的复位信号无效,而且没有计数到最大值,那么就 0 到 M/2-2 这个范围内,它就保持它原来的电平

Hgkn52gQ9k

那么这样,模块代码就编写完成,我们保存。

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

2.3.4 编译代码

那么下面就进行代码的编译,来检测我们的语法错误

回到我们的桌面,建立一个新的工程

6Hu6q0VDLE

然后加载我们的 .v 文件,就是刚才编写的代码;然后点击 Start Compilation 这个位置进行全编译,查找我们的语法错误

20231030223449_kjJk7QOTfU

那么编译完成 7 个警告信息,我们选择忽略。

代码通过编译之后,我们来查看一下 RTL 视图

cover

那么这个图形,就是根据我们的 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 视图的一个讲解,那么下面就要对我们的代码进行一个仿真验证。

2.3.5 逻辑仿真

首先需要编写仿真代码,那么仿真代码的编写大家也应该很熟悉了

20231030225537_5DnajI6kB6

首先是时间参数,然后是模块开始、模块名称,然后是端口列表(这儿是空),然后是模块结束

20231030225723_uyGQu11HH5

接下来声明两个变量,就是模拟时钟信号和模拟复位信号;后面将使用 initial 语句对它们进行赋值,所以说这儿应该是 reg 型,然后要引出模块的输出信号,用 wire 型;那么接下来就开始初始化我们的输入信号,使用 initial 语句

20231030230004_N5PvmSGSSz

那么这儿是时钟信号赋初值为 1;复位信号是初值为 0 然后过 20ns 将我们的复位信号拉高。

那么下面是产生我们的时钟信号,给它定义一个频率;这儿的时间间隔设置为 10ns,那么每 10ns 让我们的时钟信号进行一次反转,那么产生一个 50MHz 的时钟信号

20231030230137_IwEvumqsNE

下面就开始实例化。那么为了方便编写,我们可以通过这种方式:点中仿真文件名进行拖拽,拖拽到右侧移动到另一视图;这样在一个界面当中就可以查看两个代码,这样方便我们的实例化

20231030230346_VOOMIc21vP

那么这个位置也加一个点,就相当于连接;等号不需要了,在这儿直接加一个括号;那么这儿就可以写入我们想要传递的一个参数,比如说我们写入 24,那么在模块仿真的时候,CNT_MAX 这个数值就没有任何意义了,那么它的值就变为了 24 我们这儿定义了 24 这儿也可以加上位宽;然后是连接我们的输入、输出端口

20231030230713_ysZu26PpCN

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

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

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

20231030231402_QJaLbdFgdI

然后开始仿真

20231030231545_cfZpibddCd

我们打开 sim 窗口,那么添加我们的模块波形,也可以使用快捷键 Ctrl+W;打开波形界面窗口 Ctrl+A 全选,Ctrl+G 进行分组,那么去除波形名称前缀;那么点击 Restart 清除已存在波形,然后时间参数设置为 10us 运行一次;查看全局视图

20231030231814_wPxX5sxoyW

那么这个波形看似和我们绘制的差不多,但是我们要局部放大,针对我们的绘制波形图来进行一下对比。

那么时钟信号和复位信号,波形是一致的

image-20231030232104727

然后我们添加三个参考线

image-20231030232656537

我们的复位信号它前面低电平是保持了 20ns 是正确的;那么第二条和第三条这两条参考线之间是一个时钟周期是 20ns,那么我们的时钟信号频率是 50MHz,那么是正确的。那么输入信号俩波形是正确的
接下来看我们的计数器

image-20231030234333658

我们的计数器初值是为 0 然后每个时钟周期自加一,加到最大值 24,那么在 24 它归零了;归零了之后又继续进行计数,每个时钟周期自加一,到了 24 它又归零了。那么一个周期是 0 到 24 与我们的波形也是一致的;计数到最大值归零,再进行下一个周期的计数,与波形图是对应的,没有问题。

下面看一下输出信号

image-20231030234853096

那么输出信号初值为 0 那么在计数器的第一个周期保持低电平,当计数到最大值 24 然后对它进行取反,那么取反之后到下一个计数周期的最大值,又对它进行取反,那么它的波形变化也是正确的,和我们的绘制波形是一致的。

那么下面我们修改一下我们的仿真代码,增加两个系统函数

20231030235234_2MyxWXgSuw

那么系统函数添加完成之后,保存

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

20231030235439_wK36hy9Pz5

那么刚才仿真波形与我们绘制波形图已经对比完成,是相同的。我们来看一下打印信息,这儿我们增加一下仿真时间,首先清除全部波形,仿真时间设置为 10us 然后运行一次,查看我们的打印信息

20231030235612_QBkvAlT3FU

这儿我们可以看到:输出信号的高低电平呈周期性的变化,而且变化周期,看这个时间参数,周期是相同的,都是 500ns

image-20231030235747739

那么我们的系统上电之后,前 20ns 保持低电平,而且第一个时钟周期也保持低电平,所以说前 520ns 一直是低电平;到了第 520ns 之后变为高电平,那么到了 1020ns 之后又回到了低电平。是呈周期性变化的,每个周期是 500ns。这儿为什么是 500ns 呢?
因为我们实例化 counter 模块时对 CNT_MAX 参数定义的是 24,计数器的最大值是 24,那么 0~24 是 25 个周期,一个周期是 20ns,这儿就是 500ns 与上面打印信息的 500ns 周期是对应的。

仿真波形图与我们绘制的波形图是正确的,那么打印的参数也是正确的,那么仿真验证通过,回到我们的实验工程。

2.4 上板验证

2.4.1 绑定管脚

下面就绑定我们的管脚。那么输出信号是传递到 LED 灯,我们使用板载的 D6 LED 灯,它的引脚对应 L7;我们的时钟信号是由晶振传入,引脚是 E1;复位信号是由复位按键传入,引脚对应是 M15

20231031000144_sRV2pd2rFY

管脚绑定完成之后,重新编译;那么全编译完成,点击 OK

20231031000250_3G8OKilxvZ

2.4.2 结果验证

那么按照我们下图所示连接我们的下载器,然后和我们的电源;那么下载器的另一端连接到电脑,然后给开发板上电

上板验证前的硬件连线

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

20231031000808_SCLsektWwk

那么程序下载完成

20231031155531_5ejusQH4FR

我们可以看到 (D6)LED 灯进行一个闪烁,时间间隔大概为一秒,那么这样上板验证成功。

2.5 添加脉冲信号实现 LED 闪烁

2.5.1 波形绘制

那么上板验证通过之后,我们回到波形图界面。这里我们再采取另外一种看上去多此一举的方法,实现我们的计数器。这里我们定义一个新的变量:脉冲信号 cnt_flag,当我们的复位信号有效时让它保持一个低电平,当我们的计数器计数到最大值 M/2-1 时,先不对输出信号进行取反,而是让我们的 cnt_flag 信号,产生一个时钟周期的高脉冲,那么其他时刻让它保持一个低电平。这样,脉冲信号的波形就是这样

image-20231031142952426

然后我们的输出信号,那么输出信号初值还是低电平,当它检测到我们的脉冲信号为高电平时,对它进行一个取反,那么这样输出信号的波形就是

image-20231031144021915

我们将这个波形与上面一个波形做对比

image-20231031144240378

那么通过对比可以发现,如果使用原来的方法可以发现,我们的输出信号是与我们的计数周期完整对应的;但是我们新的方法,就是使用脉冲信号的方法,它错开了一个时钟周期;虽然这种情况,在我们现在的实验工程中不会对结果造成多大的影响,但是在一些对精确度要求比较高的工程中,这样的方法是不合适的,所以说我们对它进行一下改进。

我们的脉冲信号初值仍为 0,当我们的计数器计数到 M/2-2 的时候让它保持一个时钟周期的高电平;然后我们的输出信号初值仍为 0,那么检测到脉冲信号的高电平让它进行取反,那么这样,使用脉冲信号的最终的波形图已经绘制完成

image-20231031145322217

通过对比可以发现:输出信号与计数器变量对应关系,两幅波形图之内是一样的。

可能有的朋友会问:为什么一定要使用这个脉冲标志信号呢?我们使用第一种方法实现我们的计数器不好吗?

当然不是。其实在这里我们是想引出一个非常有用的信号,就是我们刚刚使用的脉冲标志信号 flag。这种信号在后面会经常使用,它可以使我们代码中 if 括号内的条件更加的清晰简洁,而且当需要在多处使用脉冲标志信号的地方,要比全部写出的方式更节省我们的逻辑资源;脉冲标志信号在指示某些状态时是非常有用的,让大家以后在实现相对复杂的逻辑功能时,注意想到使用我们的脉冲标志信号。那么除了脉冲标志信号,后面我们还会介绍另一个有用的信号,叫做使能信号。

那么接下来参照着这个波形图,对我们的代码进行修改

2.5.2 修改代码

首先需要声明一个新的变量,就是我们的脉冲信号,我们同样使用 always 语句赋值,所以说使用 reg 型。那么当复位信号有效时,我们的脉冲信号初值给它一个低电平,那么与波形图当中是对应的,给它一个低电平;然后我们计数器计数到最大值减二的时候,拉高一个时钟周期的高电平;那么其他时刻让它处于一个低电平

X6UVIZ3e5B

那么这样脉冲信号的代码,我们参照波形图已经编写完成,下面开始修改我们的输出信号的代码

qgLBxvhCMy

那么修改完成之后,保存

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

2.5.3 编译代码

打开我们的实验工程,重新进行编译,那么编译完成,点击 OK

20231031150951_5vmhCzDxEg

然后,查看一下 RTL 视图。我们来看一下 RTL 视图

image-20231031151256766

那么 RTL 视图与之前的 RTL 视图相比,多了一个寄存器,也多了一个比较器。

2.5.4 逻辑仿真

那么下面对我们的代码进行一个仿真验证。打开我们的 ModelSim,找到 Library,然后对我们的文件进行一个重编译,因为我们刚才修改了,那么这儿显示编译完成

20231031151525_e7G3TPAqyJ

然后使用 Restart 然后全选、删除,回到我们的 sim 重新加载我们的波形,因为我们刚才添加了一个新的信号;然后全选、分组,这样我们的 cnt_flag 信号也已经加入了;然后运行 10us 全局视图

20231031151815_3P4Nktc2xR

我们这儿主要查看一下我们的 cnt_flag 信号,还有我们的输出信号的变化

image-20231031153007112

首先,我们的脉冲信号初值为 0;当我们的计数器计数到最大值减一的时候,拉高一个时钟周期的高电平,因为寄存器有延迟一拍的一个效果,所以说它拉高的时钟周期刚好与计数器最大值是吻合的。然后,我们使用 cnt_flag 信号作为条件对我们的输出信号进行取反,我们的输出信号同样使用的是寄存器,有一个延迟一拍的一个效果,所以说我们输出信号的电平保持,刚好与我们计数器的一个完整的周期是对应的。

如果只针对输出信号来看,与我们之前的仿真波形是一致的,与我们绘制的波形图也是一致的,那么仿真验证通过。

2.5.5 上板验证

回到我们的实验工程,然后再进行一次全编译,编译完成,然后下载到我们的开发板

20231031154643_bp1WvmCVoP

我们开发板上的实验效果与之前的实验效果是相同的

20231031155531_5ejusQH4FR

那么验证通过

那么以上就是我们本章节的全部内容


参考资料:

11. 计数器

第十三讲-计数器(一)

第十三讲-计数器(二)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值