FPGA学习之按键去抖

按键仿真的例程,仿照黑金开发板03_key_detect_1例程,分为两个模块,分别是detect模块、delay模块,detect模块检测输入key_in信号的变化,delay模块负责延时去抖;

两个模块并不复杂,但是在test bench上仿真花费相当多的时间,波形图一直不是我想要的输出类型,折腾了一天,决定先下载到开发板上看看效果,结果效果却是我想要的输出类型,这就更加让我郁闷了,怎么仿真都不行呢?

按说当时应该可以明确的一点就是程序本省应该没有多大的问题,但是当时并没有意识到这里,这点以后还是要加强啊。随后我又多次查看的程序本身,并单独对detect模块进行仿真测试,波形是正确的,delay模块的波形始终有问题,经过分析,我将原来的test bench文件又看了看,并对应波形比较了比较,发现问题出现仿真的周期上。看下test bench文件,

`timescale 10ns / 1ps

module key_tf;

 

    // Inputs

    reg clk;

    reg rst;

    reg key_in;

 

    parameter T = 5'd20;

    parameter TMS = 21'd1500_000;        //15ms

    // Outputs

    wire LED;

 

    // Instantiate the Unit Under Test (UUT)

    key uut (

        .clk(clk),

        .rst(rst),

        .key_in(key_in),

        .LED(LED)

    );

    

    always begin

            clk = 1;

            # (T/2);

            clk = 0;

            # (T/2);

        end

    

    always begin

            key_in = 1;

            #(TMS);

            key_in = 0;

            #(TMS*10);

        end

        

    initial begin

        // Initialize Inputs

        clk = 0;

        rst = 1;

        key_in = 0;

 

        // Wait 100 ns for global reset to finish

        #100;

     rst = 0;

        // Add stimulus here

 

    end

 

endmodule

 

其实这里没什么东西,很简单的语句,就是自己在clk这里犯了浑,搞错了,而detect模块中基本上算是没有用到clk,所以delay模块出现问题时没有及时的找到问题的根源,我们知道test bench文件中已经设定clk的周期,这是我们通过always文件自己定义的,现在仿真的周期是10ns,clk的周期是我们设定的T = 20*10 = 200ns,而我却把10ns作为了我delay模块的时钟周期,所以造成了波形时钟延后很长一段时间,造成逻辑因此出现错误,在这个仿真周期上我算是栽了一个跟头,今后一定要引以为戒。把周期调整之后,波形就正确了,但是有一定的延时,估计是布线或者其他原因引起的问题,我暂时还没有学习到这一块,加油,继续FPGA之旅。

 

 

 

看了许久的按键消除抖动的例程,特权的、黑金的,对于按键的消抖有了一些了解,首先先说说我们单片机通常的消抖是怎样做的,read按键值,如果按键被按下,延时10ms,再次读取按键值,如果按键确实被按下,则确认按键动作执行。这种按键去抖的方式有时候并不是特别管用,可以采用按键按下,然后抬起作为一次按键操作,执行流程是如下程序,

uint8_t keydata = 0;

static uint8_t keyup = 0;    

if(keyup && (keydata ^ 0xf0))

    {

        keyup = 0;

        //延时10ms

        Delay(10);

        //读取按键值

        keydata = GPIO_ReadInputPin(GPIOG, (GPIO_Pin_TypeDef)(HR1|HR2|HR3));

        if(keydata ^ 0xf0)

        {

            return keydata;

        }    

    }

//无按键按下时至keyup标志位    

else if((keydata ^ 0xf0) == 0)

    {

        keyup = 1;

        return 0;

    }

这个程序是我用的最多的按键去抖程序,他的优点很明显就是能够防止按键误动作,不会出现按一次按键出现多次按键值的情况,缺点就是按键按一次只能有一次按键值,不能做连续的按键值输出,比如说有需要按键按下按键值能够连续相加的情况。

FPGA的按键去抖与单片机的去抖就会有些不同,这里也需要对按键进行去抖,当然消抖必然会进行延时,毕竟这些抖动信号是我们不需要的,黑金的例程中按键消抖处理感觉有点小问题,这里先看按键去抖的例程:

去抖程序分为两个模块,detect_module.v为电平检查模块

module detect_module

(

CLK, RSTn, Pin_In, H2L_Sig, L2H_Sig

);

 

input CLK;

     input RSTn;

     input Pin_In;

     output H2L_Sig;

     output L2H_Sig;

      

     /**********************************/

      

     parameter T100US = 11'd4_999;//DB4CE15开发板使用的晶振为50MHz,50M*0.0001-1=4_999

      

     /**********************************/

      

     reg [10:0]Count1;

     reg isEn;

      

     always @ ( posedge CLK or negedge RSTn )

     if( !RSTn )

         begin

         Count1 <= 11'd0;

         isEn <= 1'b0;

                end

     else if( Count1 == T100US )

                isEn <= 1'b1;

         else

         Count1 <= Count1 + 1'b1;

                

/********************************************/

      

     reg H2L_F1;

     reg H2L_F2;

     reg L2H_F1;

     reg L2H_F2;

      

     always @ ( posedge CLK or negedge RSTn )

     if( !RSTn )

         begin

                 H2L_F1 <= 1'b1;

                     H2L_F2 <= 1'b1;

                     L2H_F1 <= 1'b0;

                     L2H_F2 <= 1'b0;

             end

         else

         begin

                     H2L_F1 <= Pin_In;

                     H2L_F2 <= H2L_F1;

                     L2H_F1 <= Pin_In;

                     L2H_F2 <= L2H_F1;

                end

                

/***********************************/

      

     assign H2L_Sig = isEn ? ( H2L_F2 & !H2L_F1 ) : 1'b0;

     assign L2H_Sig = isEn ? ( !L2H_F2 & L2H_F1 ) : 1'b0;

      

     /***********************************/

    

Endmodule

这里先说这个isEn,因为电平检测模块是非常敏感,在复位的一瞬间,电平容易处于不稳定的状态,我们需要延迟 100us。 isEn = 1 寄存器是表示 100us 的延迟已经完成。H2L_F1, H2L_F2, L2H_F1, L2H_F2四个reg变量主要是用来检测电平变化的H2L_F2 & !H2L_F1这里是检测高电平到低电平的变化,也就是按键按下时电平会由高电平下降到低电平,这时信号为输出为高,一个时钟节拍之后信号输出为低电平(信号持续一个时钟周期),!L2H_F2 & L2H_F1这里是检测低电平到高电平的变化,也就是按键抬起时电平会由低电平上升至高电平,电平变化的信号有了,后面的delay_module.v模块就能够进行延时输出信号了。

module delay_module

(

CLK, RSTn, H2L_Sig, L2H_Sig, Pin_Out

);

 

input CLK;

     input RSTn;

     input H2L_Sig;

     input L2H_Sig;

     output Pin_Out;

      

     /****************************************/

      

     parameter T1MS = 16'd49_999;//DB4CE15开发板使用的晶振为50MHz,50M*0.001-1=49_999

      

     /***************************************/

      

     reg [15:0]Count1;

 

     always @ ( posedge CLK or negedge RSTn )

     if( !RSTn )

         Count1 <= 16'd0;

         else if( isCount && Count1 == T1MS )

         Count1 <= 16'd0;

         else if( isCount )

         Count1 <= Count1 + 1'b1;

         else if( !isCount )

         Count1 <= 16'd0;

      

/****************************************/    

                

reg [3:0]Count_MS;

      

     always @ ( posedge CLK or negedge RSTn )

if( !RSTn )

         Count_MS <= 4'd0;

         else if( isCount && Count1 == T1MS )

         Count_MS <= Count_MS + 1'b1;

         else if( !isCount )

         Count_MS <= 4'd0;

    

    /******************************************/

    reg isCount;

    reg rPin_Out;

    reg [1:0]i;

    

    always @ ( posedge CLK or negedge RSTn )

     if( !RSTn )

         begin

         isCount <= 1'b0;

                    rPin_Out <= 1'b0;

                    i <= 2'd0;

             end

         else

         case ( i )

                 2'd0 :

                     if( H2L_Sig ) i <= 2'd1;

                     else if( L2H_Sig ) i <= 2'd2;

                 2'd1 :

                     if( Count_MS == 4'd10 ) begin isCount <= 1'b0; rPin_Out <= 1'b1;

i <= 2'd0; end

                 else    

isCount <= 1'b1;

                     2'd2 :

                     if( Count_MS == 4'd10 ) begin isCount <= 1'b0; rPin_Out <= 1'b0;

i <= 2'd0; end

                 else    isCount <= 1'b1;

                    

                endcase

/********************************************/

     assign Pin_Out = rPin_Out;

     /********************************************/

 

Endmodule

延时程序在接收到H2L_Sig和L2H_Sig时会跳转到相应的case条件,如果是H2L_Sig高到低信号产生,首先延时10ms,然后Pin_Out输出高电平,当接收到L2H_Sig低到高信号产生,同样首先延时10ms,然后Pin_Out输出低电平,这样程序就会在按键按下时点亮LED灯,按键抬起后熄灭LED灯,延时10ms达到消除抖动的目的。

这样的延时消抖是不是存在弊端?在延时之后并没有查看按键的当前值,如果出现误触发的情况,时间小于10ms,这样就会本来是误动作却引起输出为高电平的情况(L2H_Sig由于延时并不能响应),当然这是理论上的情况,实际中很难发生。

黑金的按键去抖程序总体而言是非常清晰,容易理解,也比较实用,接下来看看特权的按键消抖程序。

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

// 再次按下后,LED熄灭,按键控制LED亮灭

 

module sw_debounce(

clk,rst_n,

sw1_n,sw2_n,sw3_n,

led_d1,led_d2,led_d3

);

input clk;                 //主时钟信号,50MHz

input rst_n;                  //复位信号,低有效

input sw1_n,sw2_n,sw3_n; //三个独立按键,低表示按下

output led_d1,led_d2,led_d3; //发光二极管,分别由按键控制

//---------------------------------------------------------------------------

reg[2:0] key_rst;

always @(posedge clk or negedge rst_n)

if (!rst_n) key_rst <= 3'b111;

else key_rst <= {sw3_n,sw2_n,sw1_n};

 

reg[2:0] key_rst_r;

always @ ( posedge clk or negedge rst_n )

if (!rst_n) key_rst_r <= 3'b111;

else key_rst_r <= key_rst;

 

//当寄存器key_rst由1变为0时,led_an的值变为高,维持一个时钟周期

wire[2:0] key_an = key_rst_r & ( ~key_rst);

//wire [2:0]key_an = key_rst_r ^ key_rst; // 注:也可以这样写

//---------------------------------------------------------------------------

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由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

其实这段程序并没有什么比较难理解的逻辑,与黑金的程序有所不同,这里的特权的程序是确定按键按下之后产生一个信号,LED输出一个状态(黑金是两个状态),程序的执行顺序是,按键扫描到按键值由1变位0时,通过移位比较的方式得到电平变化的信号,信号触发计数器重新计数,当计数值为20ms时,读取按键值,再次进行移位比较,如果按键值由1变位0,则确实为按键按下,执行led信号输出(信号反转)。这段程序中我自己认为比较难以理解的地方,主要是两次移位比较,像黑金那样进行高到低,低到高的移位比较比较容易理解,这里只是进行了高到低的移位比较,并且进行了两次,因此理解起来可能有点不太习惯(我自己就是这样的)。

wire[2:0] key_an = key_rst_r & ( ~key_rst);

这里明显是1到0时key_an为高,这里与单个信号是操作不同的是key_rst用的是取反而不是取非操作,状态由1到0是按键刚按下的时刻,key_an输出为高才能进行下面的操作。

wire[2:0] led_ctrl = low_sw_r[2:0] & ( ~low_sw[2:0]);

这里与黑金的有所不同,黑金是对两种状态1到0和0到1,产生两种信号,而这里与上次产生的key_an信号都是由1到0产生,也上个信号存在一个20ms的时间差,这样就起到了延时去抖的目的,这里说明一点是,延时过程中low_sw信号是没有更新的,也就是说一旦key_an信号产生,low_sw信号20ms之后才能更新,这样就能够保证low_sw_r是20ms之前的,也就是未发生改变的按键值,而延时之后跟新的low_sw则是最新的值,这样就能够判断是否是1到0的信号变化了。led_ctrl信号为高就会产生led信号反转,也就是LED的亮或灭。也黑金比较的话这里能够真正起到延时去抖的作用,按键按下会产生一个触发信号。另外一点必须要说的的是这个延时else if (cnt == 20'hfffff)这里,20ms的延时时间不算长,在key_an为低电平时,这里的计数值也是在增加的,也是达到20'hfffff,这样low_sw也是在更新之中,这也是通常会忽视的地方,这里的更新能够很好的记录下按键未触发之前的状态,虽然不能保证很实时吧(毕竟20ms的更新速度),相对于按键触发来说很短暂的,如果将led_ctrl修改为由0到1的信号触发其实也能达到按下一次按键led点亮或者熄灭,理论与实践我都测试了,通过信号波形也看到了结果,确实可行,只不过是两个产生的原因是不同的,不是key_an激发的,是按键本身触发的,毕竟有20ms延时的采样。

按键去抖是一个简单的小程序,但是有时候一点改变影响的效果确实是不一样的,就像用C语言写的按键去抖,一个抬起有效的按键去抖方式就能很好解决按键抖动的问题,FPGA也一样。

 

原文地址:https://www.cnblogs.com/longbiao831/p/5626280.html