《按键消抖与LED控制》实验的个人思考与总结

2019/01/08,第一个判断是否有按键按下的操作好像有问题,有空在修改!

红色为修改部分:

问题描述:

当三个独立按键的某一个被按下后,相应的LED被点亮;再次按下后,LED熄灭,按键控制LED亮灭


下面是LED灯的原理图:

《按键消抖与LED控制》实验的个人思考与总结_第1张图片

可见,LED是低电平亮,高电平灭。

事实上,控制LED等的亮灭很简单,不是问题,对应的代码段如下:

reg d1;
reg d2;
reg d3;
  
always @ (posedge clk or negedge rst_n)
    if (!rst_n) begin
        d1 <= 1'b0;
        d2 <= 1'b0;
        d3 <= 1'b0;
      end
    else begin		//某个按键值变化时,LED将做亮灭翻转
        if ( led_ctrl[0] ) d1 <= ~d1;	
        if ( led_ctrl[1] ) d2 <= ~d2;
        if ( led_ctrl[2] ) d3 <= ~d3;
      end

assign led_d3 = d1 ? 1'b1 : 1'b0;		//LED翻转输出
assign led_d2 = d2 ? 1'b1 : 1'b0;
assign led_d1 = d3 ? 1'b1 : 1'b0;

我来做出解释:

首先,系统复位时候,先把寄存器变量d1、d2、d3清零,由下面这三天语句可知,这三个寄存器变量清零后,对应的led灯就是低电平,也就是亮。

assign led_d3 = d1 ? 1'b1 : 1'b0;        //LED翻转输出
assign led_d2 = d2 ? 1'b1 : 1'b0;
assign led_d1 = d3 ? 1'b1 : 1'b0;

然后如果某个按键按下,对应的led_ctrl[?]就会变成高电平,此时对应的d?就会翻转电平值,也就是说原来led?是低电平,按键按下后,d?就会翻转为高电平,此时,对应的led灯自然也会由亮变灭了。继续按下按键,led的亮灭又会翻转。

(这些分析都是本人通过代码以及实验结果证明的,确实如此。)


不得不说,如果仅仅是上面的分析那么简单,就省事了,同时这个实验也没啥意思了。

事实的情况我根据理解做如下的描述:

按键按下时候,按键值有一段时间的低电平,我们必须在低电平这段时间内的某一个瞬间采样按键值来控制led灯的亮灭。

可能不能采样对是一个值得研究的问题。

先给出按键的大概电路图;

《按键消抖与LED控制》实验的个人思考与总结_第2张图片

对这个电路图的一些解释:

独立按键一般有2组管脚,这2组管脚在按键未被按下时是断开的,在按键被按下时则是导通的

基于此原理,我们一般会把按键的一个管脚接地,另一个管脚上拉到VCC,并且也连接到GPIO。这样,在按键未被按下时,GPIO的连接状态为上拉到VCC,则键值为1;按键被按下时,GPIO虽然还是上拉到VCC,但同时被导通的另一个管脚拉到地了,所以它的键值实际上是0

再给出按键的波形图:

如下图是按键按下时候的波形图,一个是理想情况下的波形,另一个是事实情况的模拟图:

《按键消抖与LED控制》实验的个人思考与总结_第3张图片

可见,按下以及释放的瞬间都有一个抖动。

按键按下或者释放的时候都会出现一个不稳定的抖动时间,如果不处理好这个抖动时间,我们就无法正确采集到正确有效的按键值,所以我们的设计中必须有效消除按键抖动

好了,按键消抖的问题摆在面前了,我们怎么来消除抖动呢?这是一个有意思的问题。


看了网上好多解释,通过画图来解释这个消除抖动的原理,可是总是越看越糊涂,感觉这种不伦不类的图已经对我的理解产生一些误导了,于是找到了特权同学的视频看了一遍,说实话,看的似乎明白了,但是不是太清晰,学习这东西必须花时间,自己动手动脑分析才能够理清楚。

我就通过分析代码的方式来理解这个消抖的过程。

`timescale 1ns / 1ps

//说明:当三个独立按键的某一个被按下后,相应的LED被点亮;
//		再次按下后,LED熄灭,按键控制LED亮灭

module sw_debounce(
    		clk,rst_n,
			sw1_n,sw2_n,sw3_n,
	   		led_d1,led_d2,led_d3
    		);

input   clk;	//主时钟信号,25MHz
input   rst_n;	//复位信号,低有效
input   sw1_n,sw2_n,sw3_n; 	//三个独立按键,低表示按下
output  led_d1,led_d2,led_d3;	//发光二极管,分别由按键控制

//---------------------------------------------------------------------------
reg key_rst;  

always @(posedge clk  or negedge rst_n)
    if (!rst_n) key_rst <= 1'b1;
    else key_rst <= sw3_n&sw2_n&sw1_n;

reg key_rst_r;       //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中

always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) key_rst_r <= 1'b1;
    else key_rst_r <= key_rst;
   
//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期 
wire key_an = key_rst_r & (~key_rst);
/*
key_rst     1 1 1 0 0 1
~key_rst    0 0 0 1 1 0
key_rst_r     1 1 1 0 0 1
key_an        0 0 1 0 0
*/
//---------------------------------------------------------------------------
reg[19:0]  cnt;	//计数寄存器

always @ (posedge clk  or negedge rst_n)
    if (!rst_n) cnt <= 20'd0;	//异步复位
	else if(key_an) cnt <=20'd0;
    else cnt <= cnt + 1'b1;
  
reg[2:0] low_sw;

always @(posedge clk  or negedge rst_n)
    if (!rst_n) low_sw <= 3'b111;
    else if (cnt == 20'hfffff) 	//满20ms,将按键值锁存到寄存器low_sw中	 cnt == 20'hfffff
      low_sw <= {sw3_n,sw2_n,sw1_n};
      
//---------------------------------------------------------------------------
reg  [2:0] low_sw_r;       //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中

always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) low_sw_r <= 3'b111;
    else low_sw_r <= low_sw;
/*
low_sw		111 111 111 110 110 110  
~low_sw     000 000 000 001 001 001
low_sw_r        111 111 111 110 110 110

led_ctrl	000 000 000 001 000 000 
   */
//当寄存器low_sw由1变为0时,led_ctrl的值变为高,维持一个时钟周期 
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);

reg d1;
reg d2;
reg d3;
  
always @ (posedge clk or negedge rst_n)
    if (!rst_n) begin
        d1 <= 1'b0;
        d2 <= 1'b0;
        d3 <= 1'b0;
      end
    else begin		//某个按键值变化时,LED将做亮灭翻转
        if ( led_ctrl[0] ) d1 <= ~d1;	
        if ( led_ctrl[1] ) d2 <= ~d2;
        if ( led_ctrl[2] ) d3 <= ~d3;
      end

assign led_d3 = d1 ? 1'b1 : 1'b0;		//LED翻转输出
assign led_d2 = d2 ? 1'b1 : 1'b0;
assign led_d1 = d3 ? 1'b1 : 1'b0;
  
endmodule

首先点名输入输出:

input   clk;    //主时钟信号,25MHz
input   rst_n;    //复位信号,低有效
input   sw1_n,sw2_n,sw3_n;     //三个独立按键,低表示按下
output  led_d1,led_d2,led_d3;    //发光二极管,分别由按键控制

这里需要提出一点的是,输入输出在开头都声明好,至于中间需要用到的一些网线变量(wire)以及寄存器变量(reg),我们在用到的位置声明,不失为一种便于理解的代码格式。


reg key_rst;  

always @(posedge clk  or negedge rst_n)
    if (!rst_n) key_rst <= 1'b1;
    else key_rst <= sw3_n&sw2_n&sw1_n;

reg key_rst_r;       //每个时钟周期的上升沿将key_rst信号锁存到key_rst_r中

always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) key_rst_r <= 1'b1;
    else key_rst_r <= key_rst;
   
//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期 
wire key_an = key_rst_r & (~key_rst);
/*
key_rst     1 1 1 0 0 1
~key_rst   0 0 0 1 1 0
key_rst_r     1 1 1 0 0 1
key_an        0 0 1 0 0
*/
//---------------------------------------------------------------------------

这里定义了一个1位的寄存器变量(key_rst),用来存放三个按键状态值的与值,由于按键值在没有按下的时候,就是拉高的状态,所以当复位信号有效时,就把key_rst赋值为1,也就是每一位都是高电平。

没有复位的时候,就在每个时钟的上升沿到来时,把三个按键值的与状态赋值给key_rst;

这两句的解释对应的代码为:

reg key_rst;  

always @(posedge clk  or negedge rst_n)
    if (!rst_n) key_rst <= 1'b1;
    else key_rst <= sw3_n&sw2_n&sw1_n};


紧接着,我们讲下面这一小块代码:

reg key_rst_r;     

always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) key_rst_r <= 1'b1;
    else key_rst_r <= key_rst;

又定义了一个1位的寄存器变量key_rst_r,这个寄存器变量在复位信号有效时,也都赋值为1;

否则,在每个时钟上升沿都会将key_rst的值赋值给key_rst_r;

这就意味这什么呢?

意味着这两个变量之间增加了一个触发器或寄存器,key_rst_r延时一拍(延迟了一个时钟周期)

也就是相当于:

key_rst     1 1 1 0 0 1
key_rst_r     1 1 1 0 0 1

为什么要这么处理呢?

继续看下去,就会知道这有多么的巧妙?


//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期 
wire key_an = key_rst_r & (~key_rst);

我不太喜欢这么写这句语法,更喜欢如下写法:

wire key_an;

assign key_an = key_rst_r & (~key_rst);

定了一个线网型变量key_an,它的值是上面的寄存器key_rst取反后和key_rst_r的与。

为什么相与呢?

key_rst     1 1 1 0 0 1
~key_rst    0 0 0 1 1 0
key_rst_r      1 1 1 0 0 1
key_an         0 0 1 0 0

这样处理的目的是边沿检测也就是key_rst当中如果某一位由1变为0之后,这种处理就会在跳变时产生一个高电平脉冲,持续一个时钟周期,如果这样看还不算直观的话,我就勉为其难,画个图给你看看:

《按键消抖与LED控制》实验的个人思考与总结_第4张图片

看到了吗?检测到了key_rst的一个下降沿,这个下降沿到来的时候key_an就会出现一个高电平脉冲,持续一个时钟周期。

(如果有小伙伴想知道我这个波形图如何画的,就得看我的这篇博文喽:对如何使用WaveDrom画波形图的研究(案例分解分析),也就是一个工具,可以画波形图,用来写文章使用,我只开发了一点功能,其他功能有兴趣的可以自己开发哦。)

 


接着对这段代码分解分析:

reg[19:0]  cnt;    //计数寄存器

always @ (posedge clk  or negedge rst_n)
    if (!rst_n) cnt <= 20'd0;    //异步复位
    else if(key_an) cnt <=20'd0;
    else cnt <= cnt + 1'b1;
  
reg[2:0] low_sw;

always @(posedge clk  or negedge rst_n)
    if (!rst_n) low_sw <= 3'b111;
    else if (cnt == 20'hfffff)     //满20ms,将按键值锁存到寄存器low_sw中     cnt == 20'hfffff
      low_sw <= {sw3_n,sw2_n,sw1_n};
      
//---------------------------------------------------------------------------
reg  [2:0] low_sw_r;       //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中

always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) low_sw_r <= 3'b111;
    else low_sw_r <= low_sw;
/*
low_sw        111 111 111 110 110 110  
~low_sw      000 000 000 001 001 001
low_sw_r        111 111 111 110 110 110

led_ctrl    000 000 000 001 000 000 
   */
//当寄存器low_sw由1变为0时,led_ctrl的值变为高,维持一个时钟周期 
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);

这里说明一下,就是按键抖动的时间,公认的大约是20ms左右,而按键按下的那段时间,至少也是几百ms。


reg[19:0]  cnt;    //计数寄存器

always @ (posedge clk  or negedge rst_n)
    if (!rst_n) cnt <= 20'd0;    //异步复位
    else if(key_an) cnt <=20'd0;
    else cnt <= cnt + 1'b1;

又定义了一个20位计时计数器寄存器变量cnt,最大计数1048575次,大约1000_000次。

时钟频率是50MHz,那么时钟周期为20ns,那么计数1000_000次,大概是20ms。

好了,继续分析上面一小段代码,系统复位时,计数器清零,如果检测到按键值跳变(有可能是抖动引起),也就是key_an有高电平脉冲,计数器也清零,否则就一直计数。


reg[2:0] low_sw;

always @(posedge clk  or negedge rst_n)
    if (!rst_n) low_sw <= 3'b111;
    else if (cnt == 20'hfffff)     //满20ms,将按键值锁存到寄存器low_sw中     cnt == 20'hfffff
      low_sw <= {sw3_n,sw2_n,sw1_n};

这里定义了一个3位的寄存器变量low_sw,如果系统复位,则low_sw赋值为111,否则如果计数器计数到最大,也就是大概计数到20ms时,将按键值赋值给low_sw;

上面的意思就是20ms锁存一次按键值。


reg  [2:0] low_sw_r;       //每个时钟周期的上升沿将low_sw信号锁存到low_sw_r中

always @ ( posedge clk  or negedge rst_n )
    if (!rst_n) low_sw_r <= 3'b111;
    else low_sw_r <= low_sw;

再次定义一个寄存器变量low_sw_r,作用是存放延迟一拍的low_sw。

复位时候,low_sw_r赋值为111;

否则将low_sw的值赋值为low_sw_r,这不就相当于延迟了一拍吗?

意义呢?

继续看呗:

//当寄存器low_sw由1变为0时,led_ctrl的值变为高,维持一个时钟周期 
wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);

这和上面的分析一样,也是一个边沿检测的做法;

同样做出形象解释:

low_sw 1 1 1 1 0 1
~low_sw 0 0 0 0 1 0
low_sw_r       1 1 1
led_ctrl        0 1 0

 

这说明在led_ctrl的第二位出现了一个高电平值,说明了它代表的按键按下了。

low_sw        111 111 111 110 110 110                //这一行的意思是某一个按键按下时的情况
~low_sw      000 000 000 001 001 001              //取反
low_sw_r            111 111 111 110 110 110            //延迟一拍

led_ctrl                000 000 001 000 000 000                 //相与,在相应的位置出现一个高电平脉冲


上面这段代码讲完了,之后就到了控制led灯亮灭的模块了,这部分内容我在开头就讲了,所以不再重复。接下来,我就是要总览全局的时刻了,从总体来认识下,第一次边沿检测的意义在哪里?计数20ms锁存一下键值的意义?第二次边沿检测的意义?

第一次边沿检测,检测到了按键状态的跳变,如果有跳变,则key_an出现一个高电平脉冲,这个跳变是什么引起的呢?有可能是抖动引起的。

如果是抖动引起的话,那么就到达解释计数器的意义了。

从代码可以看出,如果key_an出现高电平,则计数器清零,重新计数,由于抖动也只有20ms左右而已,在抖动器件,计数器肯定计数不到20ms就会被清零,直到抖动期过了,才能计数到20ms,这样锁定按键值,得到的才是非抖动的按键值,根据按键值的状态控制led灯的亮灭就可以了。

岂不完美!

这样就到达了滤除抖动的效果。

至于第二次边沿检测,当然就是检验按键是否按下,如果某个按键按下了,就会出现一个高电平脉冲,led_ctrl的某一位是高电平,并且持续一个周期的时间,通过此来控制led灯的亮灭。


感觉如果仔细看的话,应该会很明白了,很开心,知识是需要动手动脑才能够理解的。

这种代码的设计形式是最简单的,也很有效,实践证明,是可以的。

你可能感兴趣的:(Verilog/FPGA,实用总结区)