EE_FPGA基础教程系列 -- NO.4--按键消抖

chenzhufly   2011-5-10 14:26 楼主
!!!!!!!!!!!!!! 原来代码有误,感谢6楼7楼的认真解读,文中整体程序部分已经改正。对出现的错误表示道歉!

11.jpg

 

22.jpg

 

Table of Contents

 

1. 回顾...............................................4
2. 按键消抖........................................4
  2.1 按键输入原理.......................4
  2.2 何为按键消抖.......................4
  2.3 按键消抖思路.......................5
  2.4 按键消抖程序.......................5
  2.5 程序分析...............................6
  2.6 整体程序...............................8
3. 实验结果.......................................10
4. 总结...............................................11

 

  1. 回顾

  这次我们继续给玩转LED加入些新元素,使用按键控制LED。点亮LED是利用了FPGA输出电平,这次对按键进行操作则是对FPGA进行输入了。

  2. 按键消抖

  2.1 按键输入原理

  首先,我们得打开EE_FPGA的硬件手册,找到按键部分的原理图。

  如下图所示,这会大家就可以利用在LED中学到的知识进行分析了,当按键没有被按下的时候,管脚连接的是VDD3.3V的高电平;当按键被按下时,管脚接地。

  所以我们只要检测这几个管脚是否是低电平,就可以判断是否有按键被按下啦。

33.jpg
 
  2.2 何为按键消抖

  如果仅仅是按上面所说,那这个是否太简单了一点呢?是滴,你一定会想到按键消抖的问题。似乎不管是学单片机还是DSP的时候,凡是涉及到按键的都会提到按键消抖。正好,网上找到一张关于按键抖动的图。
44.jpg

 
  抖动时间t1、t3一般在20ms左右。从理论上讲,在抖动时间内,会产生多个脉冲信号,如果不进行任何处理,则按一次按键,程序会认为按了多次,从而产生错误。

  那不消抖可不可以的呢,也许有些地方是没什么问题的。上次还在论坛上看到一位朋友一定要找出一种能说明按键不消抖有问题的例子。我想,这样没必要,设计的时候根据实际情况自然就知道需不需要消抖了。这里,我们是学习这个知识点。

  2.3 按键消抖思路

  关于FPGA的按键消抖,我在网上找了一个经典的程序,稍加修改,便于大家学习和理解。

  程序设计的基本思路是:
   1、 检测管脚电平是否拉低
   2、 若检测到低电平,启动计数器,延时20ms左右的时间
   3、 再次检测管脚是否低电平
   4、 若还是低电平,确定按键被按下。输出控制信号

  2.4 按键消抖程序

  1. input   clk; //主时钟信号,50MHz
    input   rst_n; //复位信号,低有效
    input   key1;  //按键1
    //---------------------------------------------------------------------------
    reg reg0_key; 
    reg reg1_key; 
    always @(posedge clk  or negedge rst_n) begin
        if(!rst_n) begin
      reg0_key <= 1'b1;
      reg1_key <= 1'b1;
     end
        else begin
      reg0_key <= key1;
      reg1_key <= reg0_key;
        end
    end

  2. //当寄存器key1由1变为0时,led_an的值变为高,维持一个时钟周期
    wire key_an;
    assign key_an = reg1_key & ( ~reg0_key);
    //-------------------------------启动延时--------------------------------------------
    reg[19:0]  cnt_key; //计数寄存器
    always @ (posedge clk  or negedge rst_n) begin
        if (!rst_n) cnt_key <= 20'd0; //复位
     else if(key_an) cnt_key <=20'd0;
        else cnt_key <= cnt_key + 1'b1;
    end

  3. reg reg_low;
    reg reg1_low;
    always @(posedge clk  or negedge rst_n) begin
        if (!rst_n) begin
      reg_low <= 1'b1;
      reg1_low <= 1'b1;
     end
        else if(cnt_key == 20'hfffff) begin
      reg_low <= key1;          // cnt == 20'hfffff  约20ms
      reg1_low <= reg_low;
     end
    end
    //---------------------------------------------------------------------------

  4. //当寄存器reg_low由1变为0时,key_low的值变为高,维持一个时钟周期
    wire key_low = reg1_low & ( ~reg_low);



  2.5 程序分析

  这段短短的程序,其实有着两个非常重要的知识点值得我们学习。

  首先,大家了解下复位语句,if (!rst_n) begin  *** end起到异步复位作用,就是对程序设置一个初始值,这样的语句大家只要了解初始值是多少就可以。
对于蓝色标注的两段程序,有个重要的知识点。在介绍这个知识点之前,大家还必须对非阻塞赋值和阻塞赋值有个清楚的了解。这里就利用了非阻塞赋值的原理。在看似同一个时钟的操作下,但寄存器reg1_key的值要比reg0_key的值滞后一个时钟周期。
  1. always @(posedge clk  or negedge rst_n) begin
        **
        else begin
      reg0_key <= key1;
      reg1_key <= reg0_key;
    end


  原因,我们看非阻塞赋值的原理来分析。非阻塞赋值的操作过程可以看作两个步骤:
   (1)在赋值开始时刻,计算 <= 右边的表达式
   (2)在赋值结束时刻,更新 <= 左边的表达式

回到程序中,reg0_key,reg1_key最先的初始值都是1’b1。当第一个时钟的上升沿(posedge clk)来临。非阻塞赋值开始,先是计算 <= 右边的表达式,kye1的值就是按键的值;由于这个时候还没更新 <= 左边的表达式,所以reg0_key这时还是初始值1’b1 。下面进入赋值结束时刻,更新 <= 左边的表达式,结果就是reg0_key等于了当前的键值key1,而reg1_key等于上一时刻reg0_key的值1’b1。

  最后的结论就是,reg1_key要比reg0_key延时一个时钟周期。类似于移位寄存器的效果。

  这个结论需要大家在实践中好好体会一下。阻塞赋值和非阻塞赋值是Verilog设计中一个常聊的话题。大家在以后的设计中也会不断的遇到。

  第二个知识点,是脉冲边缘检测问题。代码如下:
  1. //当寄存器key1由1变为0时,led_an的值变为高,维持一个时钟周期
    wire key_an;
    assign key_an = reg1_key & ( ~reg0_key);

55.jpg
 
  我们画一张时序图来解释这个问题就非常好理解了。我们假设按键key1输入上图这样一段时序序列。经过reg0_key和reg1_key的移位操作,以及reg0_key的取反。最后寄存器key_an被拉高一个时钟周期,清楚地显示了下降沿的位置。

  这段程序是用来检测下降沿的典型程序。这里,我提醒下,我们只要把取反的寄存器换一下,改成assign key_an = reg0_key & ( ~reg1_key);就变成了一段检测上升沿的典型语句。不信你画个时序图看看。顺便多说一句,做数字电路,画时序图是解决问题的一个很好的方法哦。

  理解了以上两个知识点,那这个按键消抖的程序就很好懂了。如果管脚检测到下降沿,我们用key_an作为标志信号启动计数器,当计数器计到20’hfffff的时候,(即约10万个clk周期,20ms)。再次存入键值,
 

  1.    else if(cnt_key == 20'hfffff) begin
      reg_low <= key1;          // cnt == 20'hfffff  约20ms
      reg1_low <= reg_low;
     end


  如果这个时候键值还是低电平,那么在这个语句wire key_low = reg1_low & ( ~reg_low);就把key_low拉高一个时钟周期。这个问题,我一度进入一个死胡同,想不清楚,但告诉你一个最简单明了的办法,跟前面一样,画个时序图就能分析明了。现在我感觉这是一段绝妙的程序,key_low保持一个时钟周期的高电平之后,又会变回到低电平,也就是说,一次按键只执行一次操作。大家一定要认真体会体会。

  2.6 整体程序
  1. module led (
    clk,rst_n,key1,
    led
    );

  2. input clk;
    input rst_n;
    input key1;
    output[3:0] led;
    //------------------------键盘消抖程序---------------------------------------------------
    reg reg0_key; 
    reg reg1_key; 
    always @(posedge clk  or negedge rst_n)
    begin
        if(!rst_n) begin
      reg0_key <= 1'b1;
      reg1_key <= 1'b1;
     end
        else begin
      reg0_key <= key1;
      reg1_key <= reg0_key;  //根据非阻塞赋值的原理,reg1_key存储的值是reg0_key上一个时钟的值
        end
    end

  3. //当寄存器key1由1变为0时,led_an的值变为高,维持一个时钟周期
    wire key_an;
    assign key_an = reg1_key & ( ~reg0_key);
    //---------------------------------------------------------------------------
    reg[19:0]  cnt_key; //计数寄存器
    always @ (posedge clk  or negedge rst_n)
    begin
    if (!rst_n)
    cnt_key <= 20'd0; //异步复位
     else if(key_an)
    cnt_key <=20'd0;
    else
    cnt_key <= cnt_key + 1'b1;
    end

  4. reg reg_low;

    always @(posedge clk  or negedge rst_n)
    begin
    if (!rst_n)
    begin
      reg_low <= 1'b1;
     
     end
    else if(cnt_key == 20'hfffff)
    begin
      reg_low <= key1;           //cnt == 20'hfffff,20ms
      
     end
    end
    //-------------------------------------------------------------------

  5. reg reg1_low;

    always @(posedge clk  or negedge rst_n)
    begin
    if (!rst_n)
    begin
      reg1_low <= 1'b1; 
     end
    else
    begin
      reg1_low <= reg_low;  
     end
    end

  6. //当寄存器reg_low由1变为0时,key_low的值变为高,维持一个时钟周期
    wire key_low = reg1_low & ( ~reg_low);

  7. //===============LED控制==================================
    reg[21:0] cnt;   //
    always @(posedge clk or negedge rst_n)
    begin
    if(!rst_n)
    cnt <= 22'b0;
    else
    cnt <= cnt + 1'b1;
    end

  8. reg enable_r;
    always @(posedge clk or negedge rst_n)
    begin
    if(!rst_n)
    enable_r <= 1'b0;
    else
    if (cnt == 22'h3fffff) enable_r <= 1'b1;
    else
    enable_r <= 1'b0;
    end

  9. wire enable;
    assign enable = enable_r;

  10. reg[3:0] led_r;
    always @(posedge clk or negedge rst_n)
    begin
    if(!rst_n)
    led_r <= 4'b0111;

    else if(key_low)
    led_r <= 4'b0;
    else if(enable && !key_low)
    led_r <= {led_r[0],led_r[3:1]};
    else ;
    end

  11. wire[3:0] led;
    assign led = led_r;

  12. endmodule


  3. 实验结果

  我们把按键消抖的程序结合到之前点亮LED的程序中。另外我们分配管脚的时候把按键Key2连接到rst_n信号,key1连接到key1信号。最终的结果是:当按下key2键的时候,系统复位,只有一个LED点亮。松开key2,没有键按下的时候,四个LED交替两灭,流水灯操作。当按下key1键时,执行下面语句else if(key_low) led_r <= 4'b0;四个灯全亮。这时,如果不按复位按键,系统会一直停留在这个状态。

  是不是迫不及待想试试了呢?

  4. 总结

  这次不仅学习了按键消抖的程序,更重要的是理解了非阻塞赋值,脉冲边沿检测这两个重要的概念。

 

附学习文档PDF版本:

EE_FPGA基础教程系列 -- 按键消抖.pdf (304.1 KB)
(下载次数: 1941, 2011-6-27 22:16 上传)

 

[ 本帖最后由 xieqiang 于 2011-6-27 22:16 编辑 ]
生活就是油盐酱醋再加一点糖,快活就是一天到晚乐呵呵的忙 =================================== 做一个简单的人,踏实而务实,不沉溺幻想,不庸人自扰

回复评论 (29)

That's what I want! perfect
点赞  2011-5-11 10:33
谢谢了
点赞  2011-5-19 09:53
点赞  2011-5-25 09:59

谢谢楼主分享

点赞  2011-5-25 14:46
确实蛮不错的,以前学过但没有搞懂,现在搞懂了,谢谢诶
点赞  2011-5-28 09:01
有些人能写出好程序,也有些人能够很好理解程序,但不是很多人能够花那么大心思帮别人理解程序
点赞  2011-6-6 16:33

楼上说的在理,楼主确实是好人!

一个为理想不懈前进的人,一个永不言败人! http://shop57496282.taobao.com/ 欢迎光临网上店铺!
点赞  2011-6-6 18:51
有一个疑问:“。。。我们用key_an作为标志信号启动计数器,当计数器计到20’hfffff的时候,(即约10万个clk周期,20ms)。。。”,此解释对应代码如下
reg[19:0]  cnt_key; //计数寄存器
always @ (posedge clk  or negedge rst_n) begin
    if (!rst_n) cnt_key <= 20'd0; //复位
else if(key_an) cnt_key <=20'd0;
    else cnt_key <= cnt_key + 1'b1;
end
我对代码的理解是,key_an是一个寄存器cnt_key的清零信号,在key_an=0(按键没按下)时,cnt_key也同样会不断累加,若按键抖动刚好发生在cnt == 22'h3fffff时,这样将同样会引发误触发。。。即此程序在按键消抖中存在一个很小概率的bug。
个人建议是:多设置一个寄存器cnt_key的使能信号cnt_enable,按键按下(下降沿),cnt_enable=1,cnt_key计时,按键松开(上升沿),cnt_enable=0,cnt_key停止计时;然后判断cnt_key是否大于设定时间,如20ms,若小于,即判断为按键抖动。
当然,这样的程序效果是,按键松开时才起作用

如上为个人见解,请大伙给出意见。。。
点赞  2011-6-7 23:50

回复 9楼 Alren 的帖子

好像是存在这个问题
而且这个
reg reg_low;
reg reg1_low;
always @(posedge clk  or negedge rst_n)
begin
if (!rst_n)
begin
  reg_low <= 1'b1;
  reg1_low <= 1'b1;
end
else if(cnt_key == 20'hfffff)
begin
  reg_low <= key1;           //cnt == 20'hfffff,20ms
  reg1_low <= reg_low;
end
end
//---------------------------------------------------------------------------

//当寄存器reg_low由1变为0时,key_low的值变为高,维持一个时钟周期
wire[2:0] key_low = reg1_low & ( ~reg_low);
key_low其实维持的是20ms吧
点赞  2011-6-14 09:58
LZ的几位同学说的对,这里有误
reg1_low <= reg_low; 这个语句应该用正常的clk触发
应改成
always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
                reg1_low <= 1'b1;
        else
                reg1_low <= reg_low;
end
//---------------------------------------------------------------------------
//当寄存器reg_low由1变为0时,key_low的值变为高,维持一个时钟周期
wire key_low = reg1_low & ( ~reg_low);
点赞  2011-6-27 21:57
谢谢楼主
点赞  2011-7-12 12:38
点赞  2011-7-27 16:22

为什么要改成正常的CLK触发?

请问:reg1_low <= reg_low为什么要改成用正常的clk触发
【always @(posedge clk or negedge rst_n) begin
   if (!rst_n)
   reg1_low <= 1'b1;
   else
   reg1_low <= reg_low;
   end】?在同一时钟下的效果不是一样的吗?
点赞  2011-8-3 11:00
谢谢楼主分享,楼主真是好人
点赞  2011-10-1 22:00
点赞  2011-10-4 13:10
谢谢分享,楼主辛苦
点赞  2011-10-10 21:49
:rose:
点赞  2011-10-10 22:56
楼注好人啊
点赞  2011-10-11 11:07

顶!

一个为理想不懈前进的人,一个永不言败人! http://shop57496282.taobao.com/ 欢迎光临网上店铺!
点赞  2011-10-11 15:23
12下一页
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复