在FPGA或嵌入式系统中,按键作为人机接口的重要部件被广泛使用。在作为标准的输入接口,按键可以提供状态变换,多键组合以及按照不同时间顺序的组合,可以表达各种不同的需求。比如计算机键盘,在信息输入方面几乎涵盖了各种需要。由于目前应用最多的还是机械式按键,机械式按键在每次按下时簧片都会抖动,因此在按键按下瞬间会出现按键的多次闭合与断开,效果如同该按键被多次按下,因此不处理好按键抖动,就会得到重复输入的结果。

图1 机械式按键内部结构示意图
1.按键的抖动原理
通常按键所用的都是机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定的接通,在断开时也不会一下子彻底断开,而是在闭合和断开的瞬间伴随了一连串的抖动,抖动又分为前沿抖动和后沿,图 2 所示。
按键稳定闭合时间长短是由操作人员决定的,通常都会在 100ms 以上,刻意快速按的话能达到 40-50ms 左右,再低就比较难了。抖动时间是由按键的机械特性决定的,一般都会在几毫秒到几十毫秒之间,为了确保程序对按键的一次操作只捕获一次闭合或者一次断开,必须进行按键的消抖处理。当检测到按键状态变化时,不是立即去做按键响应,而是先等待一段时间重新检测按键闭合或断开状态,确保按键的状态稳定后再进行处理。按键消抖一般有硬件消抖和软件消抖两种处理方式。

图2 按键抖动原理
- 硬件消抖
硬件消抖一般采用阻容滤波加回滞比较器的方式实现,阻容滤波一般采用无源积分电路实现,带有回滞特性的比较器可以选用施密特触发器实现,目前有许多的MCU接口都具备施密特特性的输入接口可以使用。
- 软件消抖
在绝大多数情况下,我们是用软件即程序来实现消抖的。最简单的消抖原理,就是当检测到按键状态变化后,先等待一个 10ms–50ms 左右的延时时间,等待抖动消失后再进行一次按键状态检测,如果与刚才检测到的状态相同,就可以确认按键已经稳定的动作了。这种检测在传统的软件设计中使用比较广泛。但是随着按键使用次数增加,按键逐渐老化,或质量较差的按键抖动的时间会加长,抖动的频次也会增加。这种算法非常简单,在早期的单片机时代应用比较广泛,但这种算法的缺点也很明显,很难在消除抖动和按键灵敏度之间平衡,即如果延时过短抖动不能滤除干净,延时过长又影响按键的灵敏度。后来逐渐被自适应算法所替代。
- 自适应消抖算法
由于不同的按键在按键被按下时的抖动效果不同,前沿和后沿抖动时间也不一样,如果仅仅靠提供一段时间的延迟来滤掉抖动的话,那么延迟时间应尽可能的长,如果前沿总延迟超过100ms,那么总延迟将会超过200ms 以上,这样可能会严重影响用户体验。自适应延时的策略是将延时计数器不是用来控制整个前沿抖动的过程,而是利用每次的抖动对延迟计数器进行复位(清零),直到按键稳定,计数器计在没有复位的情况下计到特定的数值之后,进行按键捕获,如图2。按键释放的过程也要用同样的过程。图2中的delay_count为毫秒计数器。

图2
2.按键抖动程序设计
- 按键抖动测试
利用按键作为十进制计数器的输入,观察每次按键按下,计数器计数的次数,为了测试方便,将计数值译码后显示在数码管上。测试条件利用开发板FII-PRA006或FII-PRA010(Intel FPGA),按键资源如下:在开发板上共四个按键如图3,4所示,KEY0~3分别接到FPGA的四个管脚上,其映射关系如表1,
表1
| 按键名称 | 网络名称 | FPGA对应管脚 |
| UP | KEY3(PB0) | 11 |
| DOWN | KEY2(PB3) | 10 |
| LEFT | KEY0(PB1) | 3 |
| RIGHT | KEY1(PB2) | 7 |

图3

图4
从图4中可以看出,当没有按键按下时,由于KEY1~3经过1K电阻接地,因此FPGA管脚对应的是低电平,当按键按下时应该为高电平。本测试用到的资源如表2,
表2
| 程序信号名 | 网络标号 | FPGA管脚 | 端口说明 |
| clk | C10_50MCLK | 91 | 输入时钟 |
| rst | KEY0 | 3 | 复位按键 |
| bn | KEY3 | 11 | 按键输入 |
| SEL[5] | SEG_3V3_D5 | 124 | 位选信号第5位 |
| SEL[4] | SEG_3V3_D4 | 127 | 位选信号第4位 |
| SEL[3] | SEG_3V3_D3 | 129 | 位选信号第3位 |
| SEL[2] | SEG_3V3_D2 | 141 | 位选信号第2位 |
| SEL[1] | SEG_3V3_D1 | 142 | 位选信号第1位 |
| SEL[0] | SEG_3V3_D0 | 136 | 位选信号第0位 |
| SEVEN_SEG[6] | SEG_PG | 135 | 段选信号第7位 |
| SEVEN_SEG[5] | SEG_PF | 138 | 段选信号第6位 |
| SEVEN_SEG[4] | SEG_PE | 126 | 段选信号第5位 |
| SEVEN_SEG[3] | SEG_PD | 125 | 段选信号第4位 |
| SEVEN_SEG[2] | SEG_PC | 133 | 段选信号第3位 |
| SEVEN_SEG[1] | SEG_PB | 137 | 段选信号第2位 |
| SEVEN_SEG[0] | SEG_PA | 132 | 段选信号第1位 |
| DP | SEG_DP | 128 | 段选信号第0位 |
例1:利用消抖后的按键输入实现加1计数
/*
mapping :
SEVEN_SEG[0] SEVEN_SEG[1] SEVEN_SEG[2] SEVEN_SEG[3] SEVEN_SEG[4] SEVEN_SEG[5] SEVEN_SEG[6]
A B C D E F G
*/
module debouncing
(
input inclk,
input rst,
input bn,
output reg [6:0] SEVEN_SEG,
output DP,
output reg [5:0] SEL
);
reg [3:0] counta, countb;
reg [2:0] bn_r; //按键输入锁存,消除亚稳态
reg bn_hp; //按键高脉冲提取
reg [15:0] div_count;
reg f_1ms;
reg [3:0] count_sel;
//assign SEL = 6'b111110;
assign DP = 1'b1;
//======= 提取按键上升沿 ============
always@(posedge inclk or posedge rst )
if(rst)
begin
bn_r <= 0;
bn_hp <= 0;
end
else
begin
bn_r <= {bn_r[1:0], bn};
bn_hp <= ~bn_r[2] & bn_r[1];
end
//=========== 按键加一 =================
always@(posedge inclk or posedge rst )
if(rst)
begin
counta <= 0;
countb <= 0;
end
else
begin
if(counta == 9)
begin
counta <= 0;
if(countb == 9)
begin
countb <= 0;
end
else
countb <= countb + 1;
end
else if(bn_hp)
counta <= counta + 1;
end
//=========== 时钟分频 ============
always@(posedge inclk or posedge rst )
if(rst)
begin
div_count <= 0;
f_1ms <= 0;
end
else
begin
f_1ms <= 0;
if(div_count == 49999)
begin
div_count <= 0;
f_1ms <= 1'b1;
end
else
begin
div_count <= div_count + 1;
end
end
//========= 数码管扫描 ==========
always@(posedge inclk or posedge rst )
if(rst)
begin
SEL <= 6'b11_1111;
count_sel <= 0;
end
else
begin
if(f_1ms)
begin
if(SEL == 6'b11_1110)
begin
SEL <= 6'b11_1101;
count_sel <= countb;
end
else
begin
SEL <= 6'b11_1110;
count_sel <= counta;
end
end
end
//====== 7段数码管译码 ==============
always@(*)
case(count_sel)
0: SEVEN_SEG = 7'b100_0000;
1: SEVEN_SEG = 7'b111_1001;
2: SEVEN_SEG = 7'b010_0100;
3: SEVEN_SEG = 7'b011_0000;
4: SEVEN_SEG = 7'b001_1001;
5: SEVEN_SEG = 7'b001_0010;
6: SEVEN_SEG = 7'b000_0010;
7: SEVEN_SEG = 7'b111_1000;
8: SEVEN_SEG = 7'b000_0000;
9: SEVEN_SEG = 7'b001_0000;
4'ha: SEVEN_SEG = 7'b000_1000;
4'hb: SEVEN_SEG = 7'b000_0011;
4'hC: SEVEN_SEG = 7'b100_0110;
4'hd: SEVEN_SEG = 7'b010_0001;
4'hE: SEVEN_SEG = 7'b000_0110;
4'hF: SEVEN_SEG = 7'b000_1110;
default: SEVEN_SEG = 7'b111_1111;
endcase
endmodule
根据表2列出的FPGA管脚映射进行管脚锁定。重新编译,下载到开发板FII-PRA006进行测试,按动4个按钮中右边的按钮(PB2),观察计数情况,看看有没有抖动。按下PB3可以清除已有的计数,测试可以重新开始。
思考题: 在上面的代码中,分析下面的代码原理及作用
else
begin
bn_r = {bn_r[1:0], bn};
bn_hp = ~bn_r[2] & bn_r[1];
end
例2:修改例1中的代码,实现自适应去抖
/*
mapping :
SEVEN_SEG[0] SEVEN_SEG[1] SEVEN_SEG[2] SEVEN_SEG[3] SEVEN_SEG[4] SEVEN_SEG[5] SEVEN_SEG[6]
A B C D E F G
*/
module debouncing
(
input inclk,
input rst,
input bn,
output reg [6:0] SEVEN_SEG,
output DP,
output reg [5:0] SEL
);
reg [3:0] counta, countb;
reg [2:0] bn_r;
//reg bn_hp;
reg bn_hp_debounce;//hp --> high pulse
reg [3:0] deb_count;
reg [1:0] deb_st;
reg [15:0] div_count;
reg f_1ms;
reg [3:0] count_sel;
//assign SEL=6'b111110;
assign DP=1'b1;
//
always@(posedge inclk or posedge rst )
if(rst)
begin
bn_r <= 0;
//bn_hp <= 0;
deb_st <= 0;
deb_count <= 0;
bn_hp_debounce <= 0;
end
else
begin
bn_r <= {bn_r[1:0], bn };
//bn_hp <= ~bn_r[2] & bn_r[1];
case(deb_st)
0:
begin
deb_count <= 0;
bn_hp_debounce <= 0;
if(bn_r[2])
deb_st <= 1;
end
1:
begin
if(~bn_r[2])
deb_st <= 0;
else if(deb_count == 10)
deb_st <= 2;
else if(f_1ms)
deb_count <= deb_count + 1;
end
2:
begin
deb_count <= 0;
if(~bn_r[2])
deb_st <= 3;
end
3:
begin
if(bn_r[2])
deb_st <= 2;
else if(deb_count == 10)
begin
deb_st <= 0;
bn_hp_debounce <= 1'b1;
end
else if(f_1ms)
deb_count <= deb_count + 1;
end
default: deb_st <= 0;
endcase
end
always@(posedge inclk or posedge rst )
if(rst)
begin
counta <= 0;
countb <= 0;
end
else
begin
if(bn_hp_debounce)
begin
if(counta == 9)
begin
counta <= 0;
countb <= countb + 1;
end
else
counta <= counta + 1;
end
end
//=====
always@(posedge inclk or posedge rst )
if(rst)
begin
div_count <= 0;
f_1ms <= 0;
end
else
begin
f_1ms <= 0;
if(div_count == 49999)
begin
div_count <= 0;
f_1ms <= 1'b1;
end
else
begin
div_count <= div_count + 1;
end
end
//====================
always@(posedge inclk or posedge rst )
if(rst)
begin
SEL <= 6'b11_1111;
count_sel <= 0;
end
else
begin
if(f_1ms)
begin
if(SEL == 6'b11_1110)
begin
SEL <= 6'b11_1101;
count_sel <= countb;
end
else
begin
SEL <= 6'b11_1110;
count_sel <= counta;
end
end
end
always@(*)
case(count_sel)
0: SEVEN_SEG = 7'b100_0000;
1: SEVEN_SEG = 7'b111_1001;
2: SEVEN_SEG = 7'b010_0100;
3: SEVEN_SEG = 7'b011_0000;
4: SEVEN_SEG = 7'b001_1001;
5: SEVEN_SEG = 7'b001_0010;
6: SEVEN_SEG = 7'b000_0010;
7: SEVEN_SEG = 7'b111_1000;
8: SEVEN_SEG = 7'b000_0000;
9: SEVEN_SEG = 7'b001_0000;
4'ha: SEVEN_SEG = 7'b000_1000;
4'hb: SEVEN_SEG = 7'b000_0011;
4'hC: SEVEN_SEG = 7'b100_0110;
4'hd: SEVEN_SEG = 7'b010_0001;
4'hE: SEVEN_SEG = 7'b000_0110;
4'hF: SEVEN_SEG = 7'b000_1110;
default: SEVEN_SEG = 7'b111_1111;
endcase
endmodule
重新编译,下载到FII-PRA006开发板中进行测试,按下PB2观察数码管计数情况,看看还有没有一次按下,多次计数的情况。
练习:
- 分析按键自适应去抖原理及程序实现
- 将上面的代码移植到Vivado下编译,并用FII-PRX100-D 开发板进行测试。
- 增减一个按键如key2, 实现按键的加减计数。