FPGA学习——按键消抖的多种实现方法

文章目录

  • 一、按键消抖简介
    • 1.1、为什么要按键消抖
  • 二、C4开发板原理图
  • 三、按键消抖源码
    • 3.1、方案一(每当检测到下降沿便开始重新计数)
    • 3.2、方案二(检测到第一次下降沿后便开始计数)
  • 四、仿真代码及仿真波形图
  • 五、拓展:
    • 5.1、按键消抖版按键控制LED状态(参数模块化版)
    • 5.2、状态机实现按键消抖(参数模块化版)

一、按键消抖简介

1.1、为什么要按键消抖

生活中常用的按键为机械按键,而机械按键在按下后就会产生按键抖动

按键抖动:按键抖动通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动。当按下一次按键,可能在A点检测到一次低电平,在B点检测到一次高电平,在C点又检测到一次低电平。同时抖动是随机,不可测的。那么按下一次按键,抖动可能会误以为按下多次按键。
FPGA学习——按键消抖的多种实现方法_第1张图片
一般我们认为抖动存在时间保持在5-10ms左右,而在5-10ms后才会保持稳定,
因此我们很容易意识到可以通过延时采样稳定的按键信号进行消抖。而本次设计中博主在消抖后输出的是一个脉冲信号

本文博主提出两种按键消抖的解决方案:

  • 第一种:每当检测到下降沿便开始重复进行20ms计数
  • 第二种:检测到第一次下降沿后,进行一次并只进行这一次20ms计数

第一种方案为博主自行编写,代码略显繁琐。第二种方案为博主老师的方法,较容易理解(但博主认为从第一次下降沿开始便检测有些许不太严谨),大家可以依据自己的喜好进行选择。

二、C4开发板原理图

FPGA学习——按键消抖的多种实现方法_第2张图片
由原理图可以看出,博主的开发板按键按下后低电平有效,博主接下来提供的代码请各位根据自己实际的开发板进行适当的修改

三、按键消抖源码

3.1、方案一(每当检测到下降沿便开始重新计数)

源码分析:

  • 在方案一中,博主设计了key_r0、key_r1、key_r2三个中间寄存器,分别用于同步按键信号key_in并进行打两拍。大家可以认为同步打拍就是在时序电路中,分别将前一个信号的值寄存下来,第一次为同步,后续即为打拍(这里打一拍还是打两拍取决于各位,甚至只同步不打拍也可以进行下降沿检测)。

  • 由于是在时序电路中进行非阻塞赋值,因此三个中间信号分别相差了一个时钟周期,而博主就是利用后两个信号进行下降沿检测,具体如图:
    FPGA学习——按键消抖的多种实现方法_第3张图片
    由波形图可以看出,我们可以通过key_r1取反逻辑与上key_r2的运算,检测出下降沿就,即:assign nedge = ~key_r1 && key_r2

  • 在本方案中,博主设计的20ms计数器在不断的计数,但是通过前面的分析我们得知,要想按键消抖,应该在检测到下降沿的时候才开始计数,因此博主在计时器中添加了检测nedge的条件,一旦检测到nedge就会自动将进行计数器清零,以此确保计数器检测到下降沿后是从0开始计数。

  • 但大家又会想到,按键按下的时间如果远远大于20ms,计数器就会不止一次地计数到20ms,而我们也将采样多次,前文博主说道要求输出脉冲信号,因此在按键按下的低电平持续时间内会产生多个脉冲信号,并不符合要求。对此博主的解决方案是:在计数器计数到最大值-1(代码中是MAX - 2 因为计数是从0开始)时便输出脉冲信号,而一旦计数器计数到最大值(MAX - 1)便将cnt保持在最大值,从而避免产生多个脉冲。(最大值-1与最大值只相差一个时钟周期20ns,因此影响可以忽略不计)。

  • 同时注意在一切条件都满足后,输出的key_out的值不应该置1。如果只是单纯置1,假设抖动后按键仍保持在高电平,或者松开按键时产生的释放抖动也会输出一个脉冲信号,很明显在这两种情况下是不成立的,因此key_out的值应该赋为key_in(或者三个中间信号均可)并取反(按键低电平有效需要取反),并且在一个时钟周期后置0(初始值为0)才是输出脉冲信号。

  • 本方案每当检测到下降沿便会重新开始20ms计数

module key_filter(
    input       wire    clk     ,
    input       wire    rst_n   ,
    input       wire    key_in  ,//按键输入信号

    output      reg     key_out  //输出稳定的脉冲信号
);

parameter MAX = 20'd1_000_000;

reg     [19:0]  cnt_delay       ; //20ms延时计数寄存器
wire            add_cnt_delay   ; //开始计数的标志
wire            end_cnt_delay   ; //结束计数的标志

reg             key_r0          ; //同步
reg             key_r1          ; //打一拍
reg             key_r2          ; //打两拍

wire            nedge           ; //下降沿寄存器


//同步打拍
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        key_r0 <= 1'b1;
        key_r1 <= 1'b1;
        key_r2 <= 1'b1;
    end
    else begin
        key_r0 <= key_in; //同步
        key_r1 <= key_r0; //寄存一拍
        key_r2 <= key_r1; //寄存两拍
    end
end

//20ms计数器
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cnt_delay <= 1'b0;
    end
    else if(add_cnt_delay )begin
        if(nedge)begin //检测到下降沿从0开始计数
            cnt_delay <= 1'b0;
        end
        else if(cnt_delay == MAX - 1'b1)begin
            cnt_delay <= cnt_delay; //计数计满结束后保持,避免产生多个输出脉冲
        end
        else begin
            cnt_delay <= cnt_delay + 1'b1;
        end
    end
    else begin
        cnt_delay <= 1'b0;
    end
end

assign nedge = ~key_r1 && key_r2; //下降沿检测
assign add_cnt_delay = 1'b1; 
assign end_cnt_delay = add_cnt_delay && cnt_delay == MAX - 1'b1;


//key_out脉冲信号赋值
always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        key_out <= 1'b0;
    end
    else if(cnt_delay == MAX - 2'd2)begin //计数到最大值减1时产生按键脉冲
        key_out <= ~key_in;//不可直接置为1!!!!!!!!!
    end
    else begin
        key_out <= 1'b0;
    end
end

endmodule

3.2、方案二(检测到第一次下降沿后便开始计数)

源码分析:

  • 原理与第一种方案类似,但是此方案引入了中间变量flag,初始值为0,检测到下降沿后便将flag置为1,此时再将flag作为开始计数的条件,同时计满20ms后再将flag置为0,也就省去了在最大值-1时赋值key_out并保持计数器最大值的繁琐。
  • 此方法与方案一的区别就在于,方案一相当于是在抖动过程中最后一次下降沿才开始计数,而此方案是在抖动过程中的第一次下降沿便开始计数。
/**************************************功能介绍***********************************
Date	: 2023-07-26 14:43:33
Author	: majiko
Version	: 1.0
Description: 按键消抖模块(1位按键)
*********************************************************************************/
    
//---------<模块及端口声名>------------------------------------------------------
module key_filter( 
    input				clk		,
    input				rst_n	,
    input				key_in  ,
    output  reg         key_down //输出脉冲信号(按键按下1次) 
);								 
//---------<参数定义>--------------------------------------------------------- 
    parameter TIME_20MS = 1000_000;//20ms

//---------<内部信号定义>-----------------------------------------------------
    reg                 key_r0          ;//同步
    reg                 key_r1          ;//打两拍
    reg                 key_r2          ;
    wire                n_edge          ;//下降沿
    reg                 flag            ;//计数器计数的标志信号(按键按下抖动标志)

    reg			[19:0]	cnt_20ms	   	;//20ms计数器
    wire				add_cnt_20ms	;
    wire				end_cnt_20ms	;

//****************************************************************
//--同步打拍
//****************************************************************
    //同步,将key_in信号,同步到clk时钟域下面
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            key_r0 <= 1'b1;
        end 
        else begin 
            key_r0 <= key_in;
        end 
    end
    
    //打两拍
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            key_r1 <= 1'b1;
            key_r2 <= 1'b1;
        end 
        else begin 
            key_r1 <= key_r0;
            key_r2 <= key_r1;
        end 
    end
    
//****************************************************************
//--n_edge 
//****************************************************************
    assign n_edge = ~key_r1 & key_r2;//下降沿检测
    // assign p_edge = key_r1 & ~key_r2;//上升沿检测

//****************************************************************
//--cnt_20ms
//****************************************************************
    always @(posedge clk or negedge rst_n)begin 
       if(!rst_n)begin
            cnt_20ms <= 'd0;
        end 
        else if(add_cnt_20ms)begin 
            if(end_cnt_20ms)begin 
                cnt_20ms <= 'd0;
            end
            else begin 
                cnt_20ms <= cnt_20ms + 1'b1;
            end 
        end
    end 
    
    assign add_cnt_20ms = flag;
    assign end_cnt_20ms = add_cnt_20ms && cnt_20ms == TIME_20MS - 1;
    
//****************************************************************
//--flag
//****************************************************************
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            flag <= 'd0;
        end 
        else if(n_edge)begin 
            flag <= 1'b1;
        end 
        else if(end_cnt_20ms)begin
            flag <= 1'b0;
        end
    end

//****************************************************************
//--key_down
//****************************************************************
    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            key_down <= 1'd0;
        end 
        else if(end_cnt_20ms)begin 
            key_down <= ~key_in;
        end 
        else begin 
            key_down <= 1'b0;
        end 
    end

endmodule

四、仿真代码及仿真波形图

本次仿真较为随意,只是为了展示逻辑功能

源码:

`timescale 1ns/1ns
module tb_key_filter();

reg    clk     ;
reg    rst_n   ;
reg    key_in  ;
 
wire    key_out; 

parameter CYCLE = 20;
always #(CYCLE / 2) clk = ~clk;

initial begin
    clk = 1'b0;
    rst_n = 1'b1;
    key_in = 1'b1;
    #20
    rst_n = 1'b0;
    #20
    rst_n = 1'b1;
    #60
    key_in = 1'b0;
    #(30*CYCLE);
    $stop;
end


key_filter#(.MAX(10)) u_key_filter(
    .clk    (clk    ) ,
    .rst_n  (rst_n  ) ,
    .key_in (key_in ) ,

    .key_out(key_out)  
);

endmodule

仿真波形图:
FPGA学习——按键消抖的多种实现方法_第4张图片

可以看出,成功输出了一次脉冲信号key_out

五、拓展:

5.1、按键消抖版按键控制LED状态(参数模块化版)

按键消抖模块:

module key_filter(
    input       wire            clk     ,
    input       wire            rst_n   ,
    input       wire  [3:0]     key_in  ,//按键输入信号

    output      reg   [3:0]     key_out  //输出稳定的脉冲信号
);

parameter MAX = 20'd1_000_000;

reg     [19:0]          cnt_delay       ; //20ms延时计数寄存器
wire                    add_cnt_delay   ; //开始计数的标志
wire                    end_cnt_delay   ; //结束计数的标志

reg     [3:0]           key_r0          ; //同步
reg     [3:0]           key_r1          ; //打一拍
reg     [3:0]           key_r2          ; //打两拍

wire                    nedge           ; //下降沿寄存器


//同步打拍
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        key_r0 <= 4'b1111;
        key_r1 <= 4'b1111;
        key_r2 <= 4'b1111;
    end
    else begin
        key_r0 <= key_in; //同步
        key_r1 <= key_r0; //寄存一拍
        key_r2 <= key_r1; //寄存两拍
    end
end

//20ms计数器
always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cnt_delay <= 1'b0;
    end
    else if(add_cnt_delay )begin
        if(nedge)begin //检测到下降沿从0开始计数
            cnt_delay <= 1'b0;
        end
        else if(cnt_delay == MAX - 1'b1)begin
            cnt_delay <= cnt_delay; //计数计满结束后保持,避免产生多个输出脉冲
        end
        else begin
            cnt_delay <= cnt_delay + 1'b1;
        end
    end
    else begin
        cnt_delay <= 1'b0;
    end
end

assign nedge = (~key_r1[0] && key_r2[0]) || (~key_r1[1] && key_r2[1]) || (~key_r1[2] && key_r2[2]) || (~key_r1[3] && key_r2[3]); //下降沿检测
assign add_cnt_delay = 1'b1; 
assign end_cnt_delay = add_cnt_delay && cnt_delay == MAX - 1'b1;


//key_out脉冲信号赋值
always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        key_out <= 4'b0000;
    end
    else if(cnt_delay == MAX - 2'd2)begin //计数到最大值减1时产生按键脉冲
        key_out <= ~key_in;
    end
    else begin
        key_out <= 4'b0000;
    end
end

endmodule

LED控制模块:

module led (
    input   wire                clk     ,
    input   wire                rst_n   ,
    input   wire    [3:0]       key_in  ,

    output  reg     [3:0]       led
);



parameter MAX = 15_000_000 ;
reg     [25:0]      cnt ;
reg     [3:0]       flag;


always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        cnt <= 1'b0;
    end
    else if(cnt == MAX - 1'b1)begin
        cnt <= 1'b0;
    end
    else begin
        cnt <= cnt + 1'b1;
    end
end

reg [2:0]   state   ;

always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        state <= 1'b0;
    end
    else if(cnt == MAX - 1'b1)begin
        state <= state + 1'b1;
    end
    else begin
        state <= state;
    end
end

always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        flag <= 4'b0000;
    end
    else if(key_in[0] == 1'b1)begin
        flag <= 4'b0001;
    end
    else if(key_in[1] == 1'b1)begin
        flag <= 4'b0010;
    end
    else if(key_in[2] == 1'b1)begin
        flag <= 4'b0100;
    end
    else if(key_in[3] == 1'b1)begin
        flag <= 4'b1000;
    end
    else begin
        flag <= flag;
    end
end

always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        led <= 4'b0001;
    end
    else if(flag == 4'b0001)begin
        case(state)
           3'd0 : led <= 4'b0000;
           3'd1 : led <= 4'b0001;
           3'd2 : led <= 4'b0011;
           3'd3 : led <= 4'b0111;
           3'd4 : led <= 4'b1111;
           3'd5 : led <= 4'b0111;
           3'd6 : led <= 4'b0011;
           3'd7 : led <= 4'b0001;
        default:;
        endcase
    end
    else if(flag == 4'b0010 && cnt == MAX - 1'b1)begin
        led <= {led[2:0],led[3]};
    end
    else if(flag == 4'b0100 && cnt == MAX - 1'b1)begin
        led <= {led[2:0],~led[3]};
    end
    else if(flag == 4'b1000 && cnt == MAX - 1'b1)begin
        led <= 4'b1111;
    end
end

endmodule

顶层模块:

module top_key_led(
    input   wire                clk     ,
    input   wire                rst_n   ,
    input   wire    [3:0]       key_in  ,
    output  wire    [3:0]       led
);

wire [3:0]      key_out;
 
key_filter u_key_filter(
    .clk     (clk   ),
    .rst_n   (rst_n ),
    .key_in  (key_in),

    .key_out (key_out) 
);

led u_led(
   .clk     (clk    ),
   .rst_n   (rst_n  ),
   .key_in  (key_out),

   .led     (led    ) 
);

endmodule

注意:led模块有一点问题,在key0跑马灯开始后再按下key1,key2时,呈现效果也许不是流水灯,因为跑马灯开始后led状态不再是4’b0001

5.2、状态机实现按键消抖(参数模块化版)

代码分析:

博主一共设计了四种状态:

  • 按键未按下时的空闲状态:IDLE
  • 按键按下抖动状态:FILTER_DOWN
  • 按键按下后稳定状态:HOLD_DOWN
  • 按键释放抖动状态:FILTER_UP

状态跳转条件可以结合下列时序图以及代码进行分析:
FPGA学习——按键消抖的多种实现方法_第5张图片
FPGA学习——按键消抖的多种实现方法_第6张图片

由时序图我们可以分析得出:

  • 从IDLE跳转到FILTER_DOWN的条件为:当前状态为IDLE,且检测到下降沿产生(并且此时20ms延时计数器开始计数)
  • 从FILTER_DOWN跳转到HOLD_DOWN的条件为:当前状态为FILTER_DOWN,且20ms计数完毕(end_cnt_20ms)
  • 从HOLD_DOWN跳转到FILTER_UP的条件为:当前状态为HOLD_DOWN且检测到上升沿产生(并且此时20ms计数器开始计数)
  • 从FILTER_UP跳转到IDLE的条件为:当前状态为FILTER_UP,且20ms计数完毕(end_cnt_20ms)
  • 同时我们还可以看出,计数器开始计数(add_cnt_20ms)的条件为:FILTER_DOWN || FILTER_UP
module fsm_key#(parameter WIDTH = 4) (
    input       wire                    clk     ,
    input       wire                    rst_n   ,
    input       wire   [WIDTH - 1:0]    key_in  ,//按键信号输入

    output      reg    [WIDTH - 1:0]    key_out  //消抖后稳定的脉冲信号输出
);

//参数定义

parameter       IDLE        = 4'b0001;//空闲状态
parameter       FILTER_DOWN = 4'b0010;//按键按下抖动状态
parameter       HOLD_DOWN   = 4'b0100;//按键稳定状态
parameter       FILTER_UP   = 4'b1000;//按键释放抖动状态


parameter MAX = 20'd1_000_000 ;//延时20ms采样

//内部信号定义

reg         [19:0]              cnt_20ms    ;//20ms计数寄存器
reg         [WIDTH - 1:0]       key_r0      ;//同步
reg         [WIDTH - 1:0]       key_r1      ;//打一拍
reg         [WIDTH - 1:0]       key_r2      ;//打两拍

reg         [3:0]               cstate      ;//现态寄存器
reg         [3:0]               nstate      ;//次态寄存器


//状态转移条件定义
wire                             idle2down   ;
wire                             down2hold   ;
wire                             hold2up     ;
wire                             up2idle     ;  


wire                             add_cnt_20ms;//开始计时的标志
wire                             end_cnt_20ms;//结束计时的标志

wire        [WIDTH - 1:0]        nedge       ;//下降沿寄存器
wire        [WIDTH - 1:0]        pedge       ;//上升沿寄存器


//同步打拍
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        key_r0 <= {WIDTH{1'b1}};
        key_r1 <= {WIDTH{1'b1}};
        key_r2 <= {WIDTH{1'b1}};
    end
    else begin
        key_r0 <= key_in;
        key_r1 <= key_r0;
        key_r2 <= key_r1;
    end
end

//20ms计数器
always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cnt_20ms <= 1'b0;
    end
    else if(add_cnt_20ms)begin
        if(end_cnt_20ms)begin
            cnt_20ms <= 1'b0;
        end
        else begin
            cnt_20ms <= cnt_20ms + 1'b1;
        end
    end
    else begin
        cnt_20ms <= 1'b0;
    end
end

assign add_cnt_20ms = cstate == FILTER_DOWN || cstate == FILTER_UP;
assign end_cnt_20ms = add_cnt_20ms && cnt_20ms == MAX - 1'b1;


//下降沿 上升沿检测
assign nedge = ~key_r1 & key_r2;
assign pedge = key_r1 & ~key_r2;


//三段式状态机

//第一段,时序逻辑,描述状态转移
always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        cstate <= IDLE;
    end
    else begin
        cstate <= nstate;
    end
end

//第二段,组合逻辑,状态转移规律及状态转移条件
always @(*)begin
    case (cstate)
        IDLE         : begin
            if(idle2down)begin
                nstate <= FILTER_DOWN;
            end
            else begin
                nstate <= cstate;
            end
        end
        FILTER_DOWN  : begin
            if(down2hold)begin
                nstate <= HOLD_DOWN;
            end
            else begin
                nstate <= cstate;
            end
        end
        HOLD_DOWN    : begin
            if(hold2up)begin
                nstate <= FILTER_UP;
            end
            else begin
                nstate <= cstate;
            end
        end
        FILTER_UP    : begin
            if(up2idle)begin
                nstate <= IDLE;
            end
            else begin
                nstate <= cstate;
            end
        end
        default     : nstate <= IDLE;
    endcase
end

assign idle2down   = cstate == IDLE         && nedge        ;
assign down2hold   = cstate == FILTER_DOWN  && end_cnt_20ms ;
assign hold2up     = cstate == HOLD_DOWN    && pedge        ;
assign up2idle     = cstate == FILTER_UP    && end_cnt_20ms ;

//第三段 时序逻辑或组合逻辑 描述输出
always@(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        key_out <= {WIDTH{1'b0}};
    end
    else if(down2hold)begin
        key_out <= ~key_in;
    end
    else begin
        key_out <= {WIDTH{1'b0}};
    end
end
            
endmodule

六、参考资料
https://blog.csdn.net/weixin_43828944/article/details/122360794

你可能感兴趣的:(fpga开发,学习)