FPGA的计数器—LED灯闪烁试验

Posted 果果小师弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FPGA的计数器—LED灯闪烁试验相关的知识,希望对你有一定的参考价值。

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

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

3 位数的十进制计数器最大可以计数到 999;

4 位数的最大可以计数到 9999;

3 位数的二进制计数器最大可以计数到 111(7);

4 位数的最大可以计数到 1111(15)。

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

一、使用的开发板

正点原子新起点FPGA开发板

LED灯硬件原理图
流水灯实验管脚分配

二、功能分析

1、模块框图

模块框图
输入输出信号描述

2、波形图绘制

对于计数器来说只要控制好什么时候开始计数,什么时候清零的问题那么你就可以完全掌控计数器了。首先考虑什么时候开始计数的问题(也可以先考虑什么时候清零的问题),这个系统除了时钟和复位没有外界的其他输入了,所以只要复位一撤销,时钟沿来到就可以立刻进行计数,所以我们不需要太关系计数开始的条件,也可以默认为没有条件。

然后是考虑计数器什么时候清零的问题,有人可能会问,计数器不是会计数满自动清零吗?是的,但计数到多少后清零是不是需要我们考虑呢。这就引入了一个新的问题,计数 1s 的时间需要计数器计数多少个数。

有很多学习者对这一块是相当的迷糊,经常容易计算错,那样就会导致计数的个数不准,从而导致系统出现各种问题。我们计数的时钟就用系统时钟50MHz,换算成时间为[1/(50_000_000)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位宽的寄存器。

使用带标志的计数器

经过了简单的分析后我们可以开始波形的绘制,首先把输入sys_clksys_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 灯实现闪烁的效果。

3、脉冲标志信号(flag)

但是在实际应用中我们通常不采用这种方法,一般我们会添加一个用于指示cnt 计数器计数到N的脉冲信号cnt_flag,当计数器计数到Nled_out信号先不取反,而是让cnt_flag脉冲信号产生一个时钟周期的高脉冲,led_out信号每当检测到cnt_flag脉冲信号为高时取反,也能够控制外部led灯实现闪烁的效果。
 这里我们需要关心的是cnt_flag 脉冲标志信号是在计数到N时候拉高,还是计数到N-1时候拉高?

上图中的cnt_flag脉冲标志信号是在 N 有效时拉高,第一个 cnt_flag 脉冲标志信号是等待计数器计数到 N 这个值才拉高,时间上是刚刚好的,led_out 信号拉高的条件则是以cnt_flag为条件变化的,当时钟采集到cnt_flag脉冲标志信号为高电平时,其实 cnt 计数的个数已经为N+1了,也就是说此刻已经多计数了。

所以我们应该让cnt_flag脉冲标志信号在计数器计数到N-1时就拉高,这样子我们再利用cnt_flag脉冲标志信号产生其他的信号时间就是严格准确的了。

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

三、程序设计

1、RTL代码的编写

开始RTL代码的编写,RTL代码编写出的模块叫RTL模块(后文中也称功能模块、可综合模块)。之所以叫RTL代码是因为用Verilog HDL在Resistances Transistors Logic(寄存器传输级逻辑)来描述硬件电路,RTL代码能够综合出真实的电路以实现我们设计的功能,区别于不可综合的仿真代码。

`timescale  1ns/1ns

//带标志信号的计数器
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
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 <= cnt + 1'b1;
    else
       cnt <= 25'b0;

//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 - 1'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

2、代码的分析和综合

3、 查看RTL视图


4、Testbench代码的编写

`timescale  1ns/1ns

module  tb_counter();

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//wire  define
wire            led_out     ;

//reg   define
reg             sys_clk     ;
reg             sys_rst_n   ;

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//初始化系统时钟、全局复位
initial begin
    sys_clk    = 1'b1;
    sys_rst_n <= 1'b0;
    #20
    sys_rst_n <= 1'b1;
end

//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;

initial begin
    $timeformat(-9, 0, "ns", 6);
    $monitor("@time %t: led_out=%b", $time, led_out);
end

//********************************************************************//
//**************************** Instantiate ***************************//
//********************************************************************//
//------------- counter_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

5、ModelSim仿真波形

6、上板验证



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

以上是关于FPGA的计数器—LED灯闪烁试验的主要内容,如果未能解决你的问题,请参考以下文章

FPGA -- 实验一:闪烁灯

单片机c51,9个灯同时闪烁

FPGA开发随笔汇总

传感网灯闪烁代码

arduino两个led灯交替闪烁

汇编语言 编写 程序 LED 灯显示