FPGA开发。该篇包括按键消抖。
按键消抖
按键的一般示意图如下图所示:
原理图如下:
由原理图可以看出,按键未按下时 IO 口为高电平,按键按下时则变为低电平,
因此系统即可通过检测 IO 的电平来判断按键的状态。
按键结构示意图中可以看到按键存在一个反作用弹簧,因此当按下或者松开时均会产生
额外的物理抖动,物理抖动便会产生电平的抖动。在按键从按下再到松开的过程中,其电平
变化如下图所示,上为理想波形输出,下为实际波形输出。 如下图所示:
上图中,产生的抖动次数以及间隔时间均是不可预期的,这就需要通过滤波来消除抖动
可能对外部其他设备造成的影响。一般情况下抖动的总时间会持续 20ms 以内。这种抖动,
可以通过硬件电路或者逻辑设计的方式来消除,也可以通过软件的方式完成。其中硬件电路
消除抖动适用于按键数目较少的场合。
硬件消抖此处不展开解释。
按键状态
按键一般有一下几种状态:
- 没有被按下,按键处于空闲状态,高电平;
- 按下,按键抖动,高低电平来回切换多次;
- 抖动结束,按键处于静止状态,输出低电平;
- 释放,按键抖动,高低电平来回切换,最终输出低电平。
一般情况下抖动的总时间会持续 20ms 以内。
在单片机中,我们对于按键抖动的问题常常是加入一个延时,但是20ms只是一个大概的时间,我们无法保证在20ms之后按键不再抖动。因此,在FPGA中我们常常使用状态机来解决这个问题。
不用状态机的程序
module Key_debounce(
input Clk,
input Reset_n,
input Key, //外部输入的按键值
output Key_vlaue, //消抖后的按键值
output key_flag //消抖后的按键值的有效标志
);
reg [19:0] cnt; //延时计数器,设定的有效阈值
reg key_reg;
always @(posedge Clk or negedge Reset_n)begin
if(!Reset_n)begin
cnt <= 20'd0;
key_reg <= 1'b1; //低电平有效
end
else begin
key_reg <= key; //延迟一拍
if(key_reg != key)begin //按键此时已经按下
cnt <= 20'd100_0000; //一旦按下,开始计时
end
else begin
if(cnt > 20'd0) //按下,因此检测到当前的key_reg与key不同
cnt <= cnt - 1'b1; //倒计时
else //没有按下
cnt <= 20'd0; //保持0
end
end
end
always @(posedge Clk or negedge Reset_n)begin
if(!Reset_n)begin
key_value <= 1'b1; //低位有效
key_flag <= 1'b0;
end
else if(cnt == 20'd1)begin
key_value <= key; //倒计时结束,消抖,认定key_value值是当前值
key_flag <= 1'b1; //消抖后,key_flag置1
end
else begin
key_vlaue <= key_value; //没有消抖,保持当前值
key_flag <= 1'b0; //有效标志位仍为0
end
end
endmodule
状态机消抖
核心思想:在20ms内如果检测到按键按下或松开对应的信号则认定为抖动,恢复到上一状态继续消抖,直到检测到按键按下或松开对应的信号超过20ms则认定为消抖完成。状态转移过程如下图所示:
综上,本实验主要分为以下几点:
检测边缘、计数(计数20ms)、状态机的设计。
按键边缘检测
这和之前串口接收时相同,具体代码如下:
//使用D触发器存储两个相邻时钟上升沿时外部输入信号(已经同步到系统时钟域中)的电平状态
always@(posedge clk or negedge Reset_n)
if(!Reset_n)begin
key_in_reg1 <= 1'b0;
key_in_reg2 <= 1'b0;
end
else begin
key_in_reg1 <= key_in;
key_in_reg2 <= key_in_reg1;
end
//产生跳变沿信号
assign key_in_nedge = !key_in_reg1 & key_in_reg2;
assign key_in_pedge = key_in_reg1 & (!key_in_reg2)
计数器设计
always@(posedge clk or negedge Reset_n)
if(!Reset_n)
cnt <= 20'd0;
else if(en_cnt)
cnt <= cnt + 1'b1; //en_cnt=1,开始计时;不满20ms,或者未按下都不会计时(看状态机)
else
cnt <= 20'd0;
always@(posedge clk or negedge Reset_n)
if(reset)begin
cnt_full <= 1'b0;
else if(cnt == 20'd999_999) //只有满20ms才跳转下个状态,否则不满20ms,归零
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
状态机设计
现在开始状态机设计,首先用本地参数化对状态机的状态进行定义。
localparam
IDLE= 4'b0001,
FILTER0= 4'b0010,
DOWN= 4'b0100,
FILTER1= 4'b1000;
由于状态以及判断条件较少,此处先用一段式状态机来进行描述。当复位时候将计数器清零,状态回到 IDLE,key_flag 与 key_state 也回到初始态。
always@(posedge clk or negedge Reset_n)
if(!Reset_n)begin
en_cnt <= 1'b0;
state <= IDLE;
key_flag <= 1'b0;
key_state <= 1'b1;
end
else begin
case(state)
default:begin
state <= IDLE;
en_cnt <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
endcase
end
在未按下时状态为IDLE时,如果检测到下降沿则状态进入按下抖动滤除状态FILTER0,并使能计数器,否则继续保持 IDLE 状态。
IDLE :begin
key_flag <= 1'b0;
if(key_in_nedge)begin
state <= FILTER0;
en_cnt <= 1'b1;
end
else
state <= IDLE;
end
当在 FILTER0 状态时,如果 20ms 尚未计时结束就有上升沿到来,则认为此时还是按键按下抖动过程,状态回到 IDLE 并清 0 计数器。按下过程中当最后一次抖动后,不会存在上升沿,计数器则可以一直计数,计数满后则将 key_flag 置 1、key_state 置 0,状态进入按下稳定状态 DOWN 并将计数器清 0。这样就可以通过判断 key_flag && !key_state 来确定按键的状态,为 1 则按下。
FILTER0:begin
if(cnt_full)begin
key_flag <= 1'b1;
key_state <= 1'b0;
en_cnt <= 1'b0;
state <= DOWN;
end
else if(key_in_pedge)begin
state <= IDLE;
en_cnt <= 1'b0;
end
else
state <= FILTER0;
end
进入按键稳定状态 DOWM 后,将 key_flag 清 0。如果检测到上升沿则进入释放抖动滤除状态 FILTER1,否则保持当前态。
DOWN:begin
key_flag <= 1'b0;
if(key_in_pedge)begin
state <= FILTER1;
en_cnt <= 1'b1;
end
else
state <= DOWN;
end
进入 FILTER1 状态后,如果 20ms 计数尚未结束就检测到下降沿,则认为此时还是按键释放抖动过程,状态回到 DOWN 并清 0 计数器。释放过程中当最后一次抖动后,不会存在下降沿,计数器则可以一直计数,计数满后则将 key_flag 与 key_state 均置 1,状态进入 IDLE并将计数器清 0,等待下一次按键被按下。
FILTER1:begin
if(cnt_full)begin
key_flag <= 1'b1;
key_state <= 1'b1;
state <= IDLE;
en_cnt <= 1'b0;
end
else if(key_in_nedge)begin
en_cnt <= 1'b0;
state <= DOWN;
end
else
state <= FILTER1;
end
练习:按键控制蜂鸣器
按键消抖(这里使用不使用状态机的)
module key_ctrl(
input Clk,
input Reset_n,
input key, //外部输入的按键值
output reg key_value, //消抖后的按键值
output reg key_flag //消抖后的按键值的有效标志
);
reg [19:0] cnt; //延时计数器,设定的有效阈值
reg key_reg;
always @(posedge Clk or negedge Reset_n)begin
if(!Reset_n)begin
cnt <= 20'd0;
key_reg <= 1'b1; //低电平有效
end
else begin
key_reg <= key; //延迟一拍
if(key_reg != key)begin //按键此时已经按下
cnt <= 20'd100_0000; //一旦按下,开始计时
end
else begin
if(cnt > 20'd0) //按下,因此检测到当前的key_reg与key不同
cnt <= cnt - 1'b1; //倒计时
else //没有按下
cnt <= 20'd0; //保持0
end
end
end
always @(posedge Clk or negedge Reset_n)begin
if(!Reset_n)begin
key_value <= 1'b1; //低位有效
key_flag <= 1'b0;
end
else if(cnt == 20'd1)begin
key_value <= key; //倒计时结束,消抖,认定key_value值是当前值
key_flag <= 1'b1; //消抖后,key_flag置1
end
else begin
key_value <= key_value; //没有消抖,保持当前值
key_flag <= 1'b0; //有效标志位仍为0
end
end
endmodule
蜂鸣器控制
module beep_ctrl(
input Clk,
input Reset_n,
input key_value,
input key_flag,
output reg beep
);
always @(posedge Clk or negedge Reset_n)begin
if(!Reset_n)
beep <= 1'b1;
else if(key_flag && (key_value == 1'b0))
beep <= ~beep;
end
endmodule
顶层程序
module key_beep_ctrl_top(
input Clk,
input Reset_n,
input key,
output beep
);
wire key_value;
wire key_flag;
key_ctrl key_ctrl(
.Clk(Clk),
.Reset_n(Reset_n),
.key(key),
.key_value(key_value),
.key_flag(key_flag)
);
beep_ctrl beep_ctrl(
.Clk(Clk),
.Reset_n(Reset_n),
.key_value(key_value),
.key_flag(key_flag),
.beep(beep)
);
endmodule