FPGA:用状态机点亮一个LED灯(结合讲清楚按键消抖!!!)

实现功能:按键控制LED,按一下亮,按一下灭,就这么简单;

其实有很多方法都能实现这个功能,最简单的

如下:

module led_ctrl(
    input    wire    sys_clk,
    input    wire    sys_rstn,
    input    wire    key,
    
    output    reg        led
);

always@(posedge sys_clk or negedge sys_rstn)
    if(sys_rstn==1'b0)
        led <= 1'b1;
    else if(key==1'b0)
        led <= ~led;
    else    
        led <= led;

endmodule

 

 

下面这个图是上面代码综合出的RTL视图,

但是实际上这里我在做的时候出现了一个问题,就是有时候按一下就灭了,再按下的时候不亮了,我再按久一点,它又亮了,这是为什么呢,这里存在一个硬件上的问题,就是机械按键在按下去的时候信号会有抖动,什么意思呢,见下图:

 

 

我本来是按照这样一个波形图去设计的,但是实际上的输入是下面这样的波形:

 

 

这就是机械按键自带的抖动,按下之后会有,弹回之前也会有,也就是说,我无法保证中间状态抖了多少次,所以最后的led输出状态也不确定。

怎么去除这两个状态呢,有两个方法:硬件和软件,硬件的话就是加一个RS触发器如下图,这样在跳变期间会保持状态而不至于抖动。

 

 

但是这样会增加电路元件,在寸土寸金的电路板上,我们大概率不会考虑这样的方法,所以软件消抖比较实用也比较好实现,软件消抖的原理是什么呢,就是延时检测,抖动时间一般小于10ms,把那段跳动避过去,就OK了,具体怎么实现呢,通过一个计数器计时,按键按下为低电平,一般一次按下那段稳定的低电平信号会持续20ms左右,当电平为低时,开始计数,当电平为高,计数归0,只要计时到20ms了,那都稳定这么久了,那信号肯定稳了,给个信号输出来就行。就像这样:

 

 

你们可能会奇怪为什么要计数要到999_999,是因为我的开发板晶振频率f是50MHz,1/f换成时间就是20ns一周期,要计时20ms就是要10_000_000*20ns,所以从0计到999_999,所以计数多少看你们自己情况,计时时长15ms~20ms,写成代码:

module  key_filter
#(
    parameter CNT_MAX = 20'd999_999 //计数器计数最大值
)
(
    input   wire    sys_clk     ,   //系统时钟50Mhz
    input   wire    sys_rst_n   ,   //全局复位
    input   wire    key_in      ,   //按键输入信号

    output  reg     key_flag        //key_flag为1时表示消抖后检测到按键被按下
                                    //key_flag为0时表示没有检测到按键被按下
);


//reg   define
reg     [19:0]  cnt_20ms    ;   //计数器

//cnt_20ms:如果时钟的上升沿检测到外部按键输入的值为低电平时,计数器开始计数
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_20ms <= 20'b0;
    else    if(key_in == 1'b1)
        cnt_20ms <= 20'b0;
    else    if(cnt_20ms == CNT_MAX && key_in == 1'b0)
        cnt_20ms <= cnt_20ms;
    else
        cnt_20ms <= cnt_20ms + 1'b1;

//key_flag:当计数满20ms后产生按键有效标志位
//且key_flag在999_999时拉高,维持一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        key_flag <= 1'b0;
    else    if(cnt_20ms == CNT_MAX - 1'b1)
        key_flag <= 1'b1;
    else
        key_flag <= 1'b0;

endmodule

把这个结合到刚刚那个里面去再来一次:代码如下:

 1 module led_ctrl(
 2     input    wire    sys_clk,
 3     input    wire    sys_rstn,
 4     input    wire    key,
 5     
 6     output    reg        led
 7 );
 8 
 9 wire key_flag;
10 
11 always@(posedge sys_clk or negedge sys_rstn)
12     if(sys_rstn==1'b0)
13         led <= 1'b1;
14     else if(key_flag==1'b1)
15         led <= ~led;
16     else    
17         led <= led;
18         
19 key_filter
20 #(
21     .CNT_MAX(20'd999_999)         //计数器计数最大值
22 )
23 key_filter_inst
24 (
25     .sys_clk     (sys_clk    ),   //系统时钟50Mhz
26     .sys_rst_n   (sys_rstn    ),   //全局复位
27     .key_in      (key        ),   //按键输入信号
28 
29     .key_flag    (key_flag    )    //key_flag为1时表示消抖后检测到按键被按下
30                                  //key_flag为0时表示没有检测到按键被按下
31 );
32 
33 endmodule

 

 这是综合出来的RTL视图,相比于之前那个,前面多了一个按键消抖模块。

OK,讲到这里,才把按键消抖模块讲清楚,今天的主角还没上呢,好了,主角登场:状态机,一般说状态机指有限状态机啊,就是FSM(Finite State Machine)。

状态机的按照类型分呢,主要分类两种:Moore型状态机和Mealy型状态机,Moore型状态机的输出只与当前状态有关,Mealy型状态机的输出既与当前状态还和输入有关系;由于今天讲的东西呢,只有两个状态,所以我们以Moore型状态机来举例哈,以为讲完了吗?还没有,

另外一种分类方式呢,就是代码风格分类,分为一段式、二段式和三段式,

一段式就是把输入,状态变化条件和状态变化,输出全写在一起,比较精简吧,但是后期难以维护;

二段式就是把时序逻辑和组合逻辑分开,时序逻辑内进行当前状态和下一状态的切换,组合逻辑内实现各个输入、输出以及状态判断,二段式相较于一段式好维护,但是组合逻辑输出易出现毛刺等常见问题;

三段式就是一段时序逻辑内进行当前状态和下一状态的切换,一段组合逻辑实现状态判断,再来一段时序逻辑输出;解决了毛刺问题,但是耗费的资源比二段式要多。

好了,状态机概念先讲到这,下面实现目的,点灯!!!

状态机,首先要明白状态怎么变,所以要画状态转换图,如下;

 

看图写代码,如下(加上消抖模块!!!不然会出现和之前一样的情况)

module led_ctrl(
    input    wire    sys_clk,
    input    wire    sys_rstn,
    input    wire    key,
    
    output    wire    led
);

parameter    OFF=1'b0,ON=1'b1;    //定义状态
reg            state,nstate    ;    //定义状态变量
wire         key_flag        ;    

always@(posedge sys_clk or negedge sys_rstn)
    begin
        if(sys_rstn==1'b0)
            state <= OFF;
        else
            state <= nstate;
    end

always@(*)
    begin
        case(state)
            OFF:    nstate = key_flag ? ON:OFF;
            ON :    nstate = key_flag ? OFF:ON;
            default:nstate = OFF;
        endcase
    end
    
assign led=(state==OFF);
        
key_filter
#(
    .CNT_MAX(20'd999_999)         //计数器计数最大值
)
key_filter_inst
(
    .sys_clk     (sys_clk    ),   //系统时钟50Mhz
    .sys_rst_n   (sys_rstn    ),   //全局复位
    .key_in      (key        ),   //按键输入信号

    .key_flag    (key_flag    )    //key_flag为1时表示消抖后检测到按键被按下
                                 //key_flag为0时表示没有检测到按键被按下
);

endmodule

 

 比之前要复杂,同样实现功能,但是用的是不一样的方法,还有个方法,叫边缘检测,大家可以试一下;

点个灯可能很简单,但是要用学到的新东西去实现功能,活学活用才是硬道理;

 注:用的硬件平台是野火FPGA征途系列开发板,软件平台是quartus II + notepad++

posted @ 2022-02-15 09:28  Alpha521  阅读(657)  评论(1编辑  收藏  举报