07 FPGA按钮去抖实验

发布时间 2023-12-29 11:09:48作者: 米联客(milianke)

软件版本:VIVADO2021.1

操作系统:WIN10 64bit

硬件平台:适用XILINX A7/K7/Z7/ZU/KU系列FPGA

登录米联客(MILIANKE)FPGA社区-www.uisrc.com观看免费视频课程、在线答疑解惑!

1 概述

按键的消抖,是指按键在闭合或松开的瞬间伴随着一连串的抖动,这样的抖动将直接影响设计系统的稳定性,降低响应灵敏度。因此,必须对抖动进行处理,即消除抖动的影响。实际工程中,有很多消抖方案,如RS触发器消抖,电容充放电消抖,软件消抖。本章利用FPGA内部来设计消抖,即采取软件消抖。

按键的机械特性,决定着按键的抖动时间,一般抖动时间在5ms~10ms。消抖,也意味着,每次在按键闭合或松开期间,跳过这段抖动时间,再检测按键的状态。只要通过简单的延时就可实现按键的消抖动。

2 硬件电路分析

硬件接口和子卡模块请阅读"附录 1"

配套工程的 FPGA PIN 脚定义路径为 fpga_prj/uisrc/04_pin/ fpga_pin.xdc。

3 系统框图

10ms定时器模块用于触发去抖状态机每间隔10ms判断一次按键状态。

寄存去亚稳态模块是对key进行异步多次寄存,消除亚稳态

去抖状态机每间隔10ms判断一次按键状态,分别判断按键的按下,按键的弹开

之后根据去抖状态机的状态切换判断按键是有效按下和有效弹开。

4 状态机流程图

状态机每间隔10ms完成一次按键检测,首先判断按键是否按下,如果按下再次判断按键是否按下,如果按下,检测按键是否松开,也是判断两次。如果按键按下后第二次判断按键没有按下,那么就代表按键按下的不稳定,继续回到开始状态机。

5 key模块的设计

由于按键滤波是比较比较通用的一个程序,因此我们可以把一个通用的程序设置为一个模块,方便后面重复使用。

`timescale 1ns / 1ns

 

module key #

(

parameter REF_CLK = 32'd50_000_000        //设置时钟为参数,方便上层调用修改

)

(

input  I_sysclk,

input  I_rstn,

input  I_key,

output O_key_down,

output O_key_up

);

 

parameter  T10MS = (REF_CLK/50 - 1'b1);                      //设置10MS的时钟分频计数

parameter  KEY_S0 = 2'd0;                                      //设置按键状态机的状态

parameter  KEY_S1 = 2'd1;

parameter  KEY_S2 = 2'd2;

parameter  KEY_S3 = 2'd3;

 

reg [32:0] t10ms_cnt = 25'd0;

reg [3:0] key_r = 4'd0;

//reg [1:0] key_s = 2'b0;

//reg [1:0] key_s_r = 2'b0;

//wire t10ms_done ;

assign t10ms_done = (t10ms_cnt == T10MS);

assign O_key_down   = (key_s == KEY_S2)&&( key_s_r == KEY_S1);    //设置判断按键按下时的条件

assign O_key_up     = (key_s == KEY_S0)&&( key_s_r == KEY_S3);     //设置判断按键松开时的条件

 

(*mark_debug = "true"*) reg [1:0] key_s = 2'b0;                   //mark_debug 语句可以观察信号波形

(*mark_debug = "true"*) reg [1:0] key_s_r = 2'b0;

(*mark_debug = "true"*) wire t10ms_done ;

 

//10ms timer counter

always @(posedge I_sysclk or negedge I_rstn)begin                 //系统时钟的上升沿以及复位的下降沿触发

    if(I_rstn == 1'b0)begin

        t10ms_cnt <= 25'd0;                                          //系统复位

    end

    else if(t10ms_cnt < T10MS)                                        //10ms计数,目标值是 T10MS

        t10ms_cnt <= t10ms_cnt + 1'b1;                                //未达到目标t10ms_cnt+1

    else

        t10ms_cnt <= 25'd0;                                             //达到目标值复位

end

always @(posedge I_sysclk)begin                                     //将key_s的状态缓存一拍

    key_s_r <= key_s;

end

always @(posedge I_sysclk)begin                                     //将I_key的状态缓存一拍

    key_r <= {key_r[2:0],I_key};

end

always @(posedge I_sysclk or negedge I_rstn)begin                //设置状态机,设定按键的4种状态

    if(I_rstn == 1'b0)begin

        key_s <= KEY_S0;

    end

    else if(t10ms_done)begin     //触发条件为t10ms_done,说明下列所有的状态转移都是每10ms

        case(key_s)                                                     //触发一次

        KEY_S0:begin

           if(!key_r[3])                          //收到第一个按键的低电平信号,不能判断是否为毛刺

               key_s <= KEY_S1;                                        //转到状态S1

        end  

        KEY_S1:begin//recheck key done                               //第二次判断按键是否按下

           if(!key_r[3])

               key_s <= KEY_S2;                                        //按下转入S2状态

            else

               key_s <= KEY_S0;                                  //没按下,判断为毛刺,转入S0状态,等待触发

        end

        KEY_S2:begin//wait key up                                     //确定按键按下后

           if(key_r[3])                                            //等待按键松开,接收到按键的高电平信号,

               key_s <= KEY_S3;                                        //不能确定是否为毛刺

        end                                                              //转入状态S3

        KEY_S3:begin//recheck key up                                  

           if(key_r[3])                                                 //第二次判断案件是否松开

              key_s <= KEY_S0;                        //依然检测到按键是松开的状态,转入S0状态,等待触发

        end

        endcase                  

    end

   

以上代码中,首先把系统时钟做分频,产生10ms 的分配时钟使能信号。在设计的状态机中,分4个状态

KEY_S0:判断按键是否按下,如果是,转移到状态KEY_S1;

KEY_S1:10ms后再次判断按键是否按下,如果是,转移状态到KEY_S2,否则继续回到KEY_S0;

KEY_S2:判断按键是否抬起,如果是,转移状态到KEY_S3;

KEY_S3:10ms后再次判断按键是否抬起,如果是,转移状态到KEY_S0,否则继续回到KEY_S2;

当状态从KEY_S1 转到KEY_S2代表依次按钮按下key_cap输出一次高电平。

6 调用key模块

以下代码中调用了key模块,并且通过两个按钮来分别控制LED等的向左跟向右偏移。

`timescale 1ns / 1ns

 

module key_top #

(

parameter REF_CLK = 64'd50_000_000                                     //设置时间参数,方便上层调用

)

(

input  I_sysclk,

input  I_rstn,

input  I_key1,

input  I_key2,

output [3:0]O_led

);

 

reg [3:0] led_r;                                                       //存储LED灯的状态

 

wire key1_down,key2_down;

 

assign O_led = led_r;                                                 //将存储的LED灯的状态输出

 

always @(posedge I_sysclk or negedge I_rstn)begin

    if(I_rstn == 1'b0)

       led_r <= 4'b1110;                                           //设置LED的初始状态

    else if(key1_down)

       led_r<={led_r[2:0],led_r[3]};                              //将LED灯左移动

    else if(key2_down)

       led_r<={led_r[0],led_r[3:1]};                                 //将LED灯右移动

end

 

key#(                                                //例化了两个KEY按键,一个负责左移一个负责右移

.REF_CLK(REF_CLK)

)

key_u1

(

.I_sysclk(I_sysclk),

.I_rstn(I_rstn),

.I_key(I_key1),

.O_key_down(key1_down),

.O_key_up()

);

 

key#(

.REF_CLK(REF_CLK)

)

key_u2

(

.I_sysclk(I_sysclk),

.I_rstn(I_rstn),

.I_key(I_key2),

.O_key_down(key2_down),

.O_key_up()

);

 

endmodule

7 综合布线前仿真时序

1、新建仿真文件,仿真文件源码如下所示。

`timescale 1ns / 1ns

 

module tb_key();

 

reg I_sysclk,I_rstn,I_key1,I_key2;

wire  [3:0]O_led;

 

key_top#

(

.REF_CLK(100_000)

)

key_top_inst

(

.I_sysclk(I_sysclk),

.I_rstn(I_rstn),

.I_key1(I_key1),

.I_key2(I_key2),

.O_led(O_led)

);

 

initial

   begin

      // Initialize Inputs

      I_sysclk_p = 0;

      I_rstn = 0;

      #100;

      I_rstn =1;

      I_key1 = 1;

      I_key2 = 1;

      #10000;

      forever

         begin

            I_key1 = 0;

            // Wait 100 ns for global reset to finish

            #100;

            I_key1=1; #1000;                       //100个时钟周期,频繁的翻转,模拟的是毛刺状态

            I_key1=0; #1000;

            I_key1=1; #2000;

            I_key1=0; #5000;

            #20000000;                             //20000000个时钟周期的KEY变化,确认为真实的按键操作

            I_key1=1;

            I_key1=0; #1000;

                I_key1=1; #2000;

                I_key1=0; #1000;

                I_key1=1; #2000;      

            #20000000;                         //20000000个时钟周期的KEY变化,确认为真实的按键操作

            I_key2=1; #1000;

            I_key2=0; #1000;

            I_key2=1; #2000;

            I_key2=0; #5000;

            #20000000;                            //20000000个时钟周期的KEY变化,确认为真实的按键操作

            I_key2=1;

            I_key2=0; #1000;

                I_key2=1; #2000;

                I_key2=0; #1000;

                I_key2=1; #2000;

            I_key2=0; #1000;  

            #20000000;                                  //20000000个时钟周期的KEY变化,确认为真实的按键操作

            I_key2=1; #1000;  

         end

   end

always #10 I_sysclk_p=~I_sysclk_p;

 

endmodule


2、进入仿真界面:SIMULATION->单击 Run Simulation->单击Run Behavioral Simulation。

Setp1:设置断点

之后再点击下图箭头所指

读者可以以这种方法去观察自己想看的内部信号

Setp2:取消断点,添加想观察的信号

Setp3:观察仿真波形

可以看到每次key1的按下,key1_down信号都会拉高表示捕捉到key1的按下,并且led灯进行偏移给我们反馈。

Setp4:放大信号观察毛刺

可以看到我们仿真模拟的按键抖动并不会触发我们的led灯的偏移,同时我们的key2_down信号也不会拉高反馈,说明按键抖动被很好的滤除。

8 Chipscope在线逻辑分析仪仿真

很多时候软件仿真后的代码也不一定完全执行正确,这个时候我们可以通过XILINX 自带的在线逻辑分析,在板子上运行并且查看关键信号。

1、将(*mark_debug = "true"*) 添加到需要观察的信号前面。

(*mark_debug = "true"*) reg [1:0] key_s = 2'b0;

(*mark_debug = "true"*) reg [1:0] key_s_r = 2'b0;

(*mark_debug = "true"*) wire t10ms_done ;

2、为了观察到信号,先点击Run Synthesis

点击ok

3、单击Set Up Debug 设置需要观察的信号

以下是我们要观察的信号

 

以下是这只在线逻辑分析仪的采样深度,使用的是FPGA的 BRAM,以及设置Captrue control,对于这种超慢信号,XILINX 的在线逻辑分析低于20M采样速度的,波形窗口就不会显示波形,这个XILINX也没有特别说明过,但是通过设置Captrue control,可以用我们这里的t10ms_done来作为扑捉控制,而采样时钟依然用系统时钟。

单击Finish后会出来下面的原理图设计,可以看到FPGA编程的本质还是回归电路设计。现在我们使用代码去设计电路。记得保存,否则无法观察到调试信号。下面的小蚂蚁,就是已经添加调试标记的信号。

4、编译程序

5、下载程序

6、设置触发,以及设置Capture 信号

Captrue mode 一定要设置为BASIC

Window data depth 为采样深度设置为2048最大

Trigger position inwindow 设置为1024

以上参数都可以根据需要用户自行设置

 

以信号t10ms_done作为Capture信号

以信号key_s以及key_s_r作为触发信号,条件是key_s == KEY_S2以及 key_s_r == KEY_S1,也就是key_s =2且key_s_r=1。因为我们代码中定义:

assign key_down   = (key_s == KEY_S2)&&( key_s_r == KEY_S1);

7、启动采集并且按下按键,可以连续多次按下按键

可以看到正确观察到了按键程序的状态机信号。

 

9 输出结果

(该教程为通用型教程,教程中仅展示一款示例开发板的连接方式,具体连接方式以所购买的开发板型号以及结合配套代码管脚约束为准。)

请确保下载器和开发板已经正确连接,并且开发板已经上电。(注意JTAG端子不支持热插拔,而USB接口支持,所以在不通电的情况下接通好JTAG后,再插入USB到电脑,之后再上电,以免造成JTAG IO损坏)

 

将程序下载。按键KEY1每按一次,LED灯进行向右偏移;按键KEY2每按一次,LED灯进行向左偏移;键KEY3每按一次,LED灯复位,LED灯响应无差错。为清晰的表示消抖的效果,可将延时参数设置很小,可以发现,按键有时候明明已经按下去了,LED却无响应。