- T" N9 C9 S( ^. e2 E9 V写在前面:
( c/ ~: K& ^1 ^# l6 i3 _
5 m. D. J3 }4 X) ]
# G$ B; o* a& N |
按键去抖:由上图可以看出理想波形与实际波形之间是有区别的,实际波形在按下和释放的瞬间都有抖动的现象,抖动时间的长短和按键的机械特性有关,一般为5~10ms。通常我们手动按键然后释放,这个动作中稳定闭合的时间超过了20ms。因此单片机在检测键盘是否按下时都要加上去抖动操作,有专用的去抖动电路,也有专门的去抖动芯片,但通常我们采用软件延时的方法就可以解决抖动问题。9 [$ _2 q( [) U" L1 ~$ w4 B7 L4 @
1、单片机中按键消抖程序
1 v3 W' m8 Y; s, s. S/ H# @1.1 单片机中,比如STM32中,一般的方法(最简单的方法)$ B* {- L) T9 D S% s
软件消抖程序:: q+ A8 F" ^* K; q( {
- E( i% H/ s+ n7 z8 y- if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_14)==1)
6 K D0 D( ~: s$ z5 U - { ' c5 T9 A" f6 Z9 i4 u a
- delay_ms(20);//延时20ms再去检测按键值. d+ ^. n+ A, D9 u/ E
- if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_14)==0) // 相当于下降沿
+ R* y3 m4 t' f2 L1 B- F1 S - {
# ~7 h7 S/ `, I/ C. Q - KEY1 = 1; //表示KEY1被按下# K; t: u! `0 C! \5 h' t; Z. q3 S
- }
, ~/ |. _( Q9 [; K; o - }
复制代码 ; x7 }7 P$ U( ^7 L
1.2 比较全面的按键消抖程序及按键状态检测程序
0 }( y7 x- s8 l7 Z; e F第一步:初始化全局时间戳的定时器,一般采用SysTick定时器来产生,每ms一次tick即可。
( B5 g* W! y9 ], k; s+ r: c第二步:初始化按键对应的IO,复用为边沿触发的外部中断。7 a8 X1 X" T' a0 @0 Y
第三步:在外部中断函数中添加按键事件处理函数。' G$ V# I" d5 N% @4 f) f+ |
代码部分:- $ w0 ]+ Y; l, x: P0 H
- typedef struct _Key_t ' F3 ~, B9 d5 U/ ^0 S
- { 2 p6 o4 R! b, @) f! X0 N9 g
- u32 last_time;
9 ]$ W3 `! C, N, }6 ?+ h% P - enum
; k* ?, B- ^; x* Y& ]: ]2 } - {
J6 O2 L, b, G# {- y7 ] - May_Press, . g+ e9 z# T( k2 {
- Release, / z% A7 ?9 n( W, I9 J; _. p: @: a" E
- }private_state; 7 a1 g9 Q& P5 F+ X4 ~
- enum
* b/ u: n( v$ h- x - {
. C; h2 q& M+ ]% c - No_Press,
' j" F# ~6 P9 K. e - Short_Press,
8 O& {- d- J# b$ J- L - Long_Press,
" t) J% p l/ z% ^/ V- l8 q* m; ~ - }state;
$ O4 [! B. o# m" Q7 V" b+ b+ A4 F - }Key_t;( M+ I) Z8 G5 G7 V. Y7 b, ]' d- H
- 3 q q, x% g8 e: q |2 D
- #define Is_ShortPress_Threshold 1500
复制代码 ; v0 }, W9 }8 y
简单定义一个按键状态的结构体,用于管理每个按键的状态。顺便再定义一个长短按的识别阈值,用于区分按键的长短按。* s# K& }1 @+ x, i/ a4 H, P
- 7 b* R% Y2 \3 c' s( n3 `
- if(key_state.private_state==Release) 0 W1 N0 P. t( h3 L/ `: W
- {
; ~7 o V. q3 ]7 a; @' R, ` - if(KEY==0) ( A v; h: S) `9 X: c. d
- { * K! N: Y/ f A5 b8 C
- key_state.private_state=May_Press; 5 n @8 y* o2 o+ a1 B. g# [* e6 t2 [4 }
- key_state.last_time=course_ms();
- N( @$ r3 t$ | - }
& R2 U4 }2 K; T6 f0 A - }
4 V( b. [* k# o6 L( B* _0 k - else if(key_state.private_state==May_Press)
3 `$ t! y6 V5 o* q$ D2 ? - { + }' c7 m7 `% U% Q U9 c) @$ ?
- if(KEY==1) 5 R* g- J u# T. G% w6 K" [- A2 @. w
- { : U2 O+ m4 ]1 P6 x7 \, V0 h( ?4 I
- if((course_ms()-key_state.last_time>10)&&(course_ms()-key_state.last_time
# G. H; q; u4 w9 j - {
+ U( Z6 }, w3 V8 {+ p - key_state.state=Short_Press; 0 Y; k O# [1 D- P; ]
- key_state.private_state=Release; 0 h) C4 y0 S# O
- } % z0 L5 F. I+ v: ]( M" h
- else if(course_ms()-key_state.last_time>Is_ShortPress_Threshold) * x% p0 V3 f s4 f% |
- {
1 ?, ?- N1 y W( N9 p6 n* X. | ?/ ` - key_state.state=Long_Press;
, C7 \' f0 o- Q( A4 Q' @" O+ G2 F - key_state.private_state=Release;
9 E- M- E& d5 M- U* m8 F& t - }
! J/ y0 x+ F$ D, A4 [" S - else & f- ?8 J: h0 {& x0 O
- key_state.private_state=Release; . ?# r o, e( K1 B- w3 |) H O& E
- }
5 }: R" b: B( O3 I3 @" K - }
复制代码 5 d( Y7 W! f, M8 s4 F5 t+ n6 K
( Q/ h# ^3 }: s1 z. }1 K
6 B9 `) p5 I; q ^. n; P& r; s
, i- M4 Q7 y H: ?' |
以上为需要添加到中断处理函数的按键事件处理函数,算法的核心是一个状态机。在本例中,按键被默认上拉,按下接地。course_ms()为获取全局时间戳的函数。
' T" h9 v7 _( G( T0 @' R7 T1 D8 E
0 e# J" d- o. r( a* n思路解释如下:按键状态结构体有一个用于识别的状态位,默认处于Release,也就是释放的状态。一旦按键被按下,中断触发,此时检查是否是Relase状态,如果是就检查按键是否被拉低,如果是,此时进入May_Press状态,也就是可能是按下的,并且记录此时的时间戳,这一步是消抖的关键。当按键被释放,由于是边沿触发,会再次进行处理,此时检查和上一次触发之间的时间戳之差,如果小于10ms我们就认为是抖动,此时不会对按键输出状态进行修改,而是直接将按键状态置回Relase状态,反之检查差值和长短按阈值之间的关系,将state置位为对应的状态。消抖的核心在于记录时间戳,而这只是一个简单的赋值操作,并不耗费时间。
9 q' u8 h1 b5 w1 [! m效率上来说,延时消抖花费时间在无意义延时上,而相对较好的定时轮询还是不可避免的在轮询,而现在这种方式完全是中断性质的。唯一多出的开销(全局时间戳)并不是只可以用于按键消抖,另外在HAL库中存在直接获取tick的函数,这样实现就更方便了。经实际测试,消抖效果可以达到其他两种消抖算法的水平。
# P, {# J2 p0 {/ Y; ]2、FPGA按键消抖程序/ |, J' \" Q( S K% \. n* _- Y* x. {
首先,做两个假定,以方便后面的描述:6 _6 i1 d) i# ~& |: C: ~
& M( N" S' H1 ^0 Y- l+ B9 H% L
- 假定按键的默认状态为0,被按下后为1
- 假定按键抖动时长小于20ms,也即使用20ms的消抖时间& k3 Z( F2 ]5 \5 C6 F% L$ g' |$ b
9 ~+ U: t: T$ N- x# U核心:方案8 ?" M I- H! a( W0 X
# \3 i) }+ M1 R( g! g& d4 l
- 最容易想到的方案
! t- V/ w/ E' m# @- X6 e
' Q& ?6 [; U( w$ a在按键电平稳定的情况下,当第一次检测到键位电平变化,开始20ms计时,计时时间到后将按键电平更新为当前电平。% I9 O z# Q; T2 Q) @; z& e
9 Z- q, n9 L k6 Q- N- 或许这才是最容易想的方案5 |/ Q) f9 | c
$ P8 G: L+ k6 m [" x$ W: A
在20ms计时的过程中,有任何的电平变化都立即复位计时
& J. c3 L E, f: f9 U0 W& Q8 x+ H: M9 `( t: B; u6 O1 p6 m# J
- 消除按键反应延时抖方案
( p( }& c- |0 |* p+ k' T7 j' X 6 t R* f3 L% Q% C6 _. G
在有电平变化时立即改变按键输出电平,并开始20ms计时,忽略这其中抖动测试平台设计(修改代码以仿真的1us代替实际1ms)
( e( Q$ W$ Y l8 F1 _ Z9 [" n: \$ \, D7 z
- 无抖动 上升沿抖动5毫秒
- 下降沿抖动15毫秒
- 上升和下降沿均抖动19毫秒2 F* H, E/ u' @$ v+ W# V5 u
附加测试(可以不通过)+ v! K8 S, X$ S8 P- F+ T
代码
8 A8 f q& H2 C0 y2 m% H方案1
, r0 B- y. c& U2 ?) ^- 4 f1 ]- g8 Z, S4 I
- module debounce( input wire clk, nrst, input wire key_in, output reg key_out
# B) g' {; _& ^1 h9 f - ); // 20ms parameter// localparam TIME_20MS = 1_000_000;
! Y: H9 |: ^: d# M. i - localparam TIME_20MS = 1_000; // just for test // variable
6 L3 X- _6 k& p6 H - reg [20:0] cnt; reg key_cnt; s' U6 R3 I& e) d" `3 u" c
- // debounce time passed, refresh key state- U/ K: Y3 A# Z2 W6 ^1 E
- always @(posedge clk or negedge nrst) begin
7 d% }0 b. y# c5 z+ h$ N$ U - if(nrst == 0)
- B' ^% L* Y6 D/ j) c) H- f! @ - key_out <= 0; else if(cnt == TIME_20MS - 1)
7 z+ g7 a2 e: t- n& {7 K' O) b+ v - key_out <= key_in; end
: Q/ t- l9 j- I) Q4 Z
8 W- w% H. z' \% I- q- // while in debounce state, count, otherwise 0
% Y, B$ p, W7 G k9 S( H! F - always @(posedge clk or negedge nrst) begin1 r4 Q; z: {" `# n8 L8 S
- if(nrst == 0)
$ b3 f5 T+ W0 d& P) J7 b - cnt <= 0; else if(key_cnt)2 n/ f: D6 T. ^1 c
- cnt <= cnt + 1'b1;
4 [. o7 k) m- I M - else
- T4 x* G) @7 Q& P - cnt <= 0;
1 J2 g" K* o8 L) U0 V - end& x0 i, w: ^6 ^
A# ~, j# E( e+ @- //
2 v* C4 {& c" W. x - always @(posedge clk or negedge nrst) begin
1 G0 O. g/ z* ?* n- f" ^* P( [3 j - if(nrst == 0): V2 g' t3 b- C
- key_cnt <= 0; else if(key_cnt == 0 && key_in != key_out): i! h% Z/ \- _5 |. n- W. z
- key_cnt <= 1; else if(cnt == TIME_20MS - 1)
6 r' ]% M8 i1 ~, r: o& Z: p - key_cnt <= 0; endendmodule
复制代码 4 |" A$ g6 v* Y4 j1 w6 z/ u
方案2
! H3 B9 r# Q$ t( W1 Q- 1 M( T, S% H0 h
- module debounce( input wire clk, nrst, input wire key_in, output reg key_out! h! Q& a O( K, H" Q0 E( m
- );// localparam TIME_20MS = 1_000_000;
+ a: v( K% z5 T* F4 {% t4 x - localparam TIME_20MS = 1_000; reg key_cnt; reg [20:0] cnt; always @(posedge clk or negedge nrst) begin" |' F( t' f1 J: k1 h6 p
- if(nrst == 0)" S% W) P. _, x
- key_cnt <= 0; else if(cnt == TIME_20MS - 1)
8 b& N3 ]6 l" [ - key_cnt <= 0; else if(key_cnt == 0 && key_out != key_in)' F& E$ M1 }% c, s
- key_cnt <= 1; end
% o8 G5 [3 ?- F8 E: R2 ^* i: [0 [ - 0 g6 D z! m; Y8 t& p1 ]% O
- always @(posedge clk or negedge nrst) begin% \/ d5 \8 Z3 L' S/ U) \; z5 b" k
- if(nrst == 0)
% i: l$ R% J" k$ l* r- `+ I- }/ ?3 v - cnt <= 0; else if(key_cnt) begin' i# t9 n/ \. g; T; \3 {
- if(key_out == key_in)
! m9 [% D, i" V1 O0 U9 Q: A - cnt <= 0; else
7 K/ Z' B1 B8 E1 M4 l - cnt <= cnt + 1'b1;
! G7 j1 E9 i3 ^! i0 G/ O - end
% ~" K; H4 Y, _: f5 ? - else2 p" c6 ]4 }8 {& v* s. H$ t( X
- cnt <= 0; end
( ?6 I! X' Q6 @) k( O
. k @/ B9 h* x3 S0 a" o% V# P5 u- always @(posedge clk or negedge nrst) begin7 `# Y8 c: T0 V
- if(nrst == 0)
$ R. ]6 L3 x! q0 k7 [8 ?# E - key_out <= 0; else if(cnt == TIME_20MS - 1)( W0 T, `3 @1 T3 W8 v1 o
- key_out <= key_in; endendmodule
复制代码
. c0 C' k1 f) L( I方案3; I2 Z0 C9 w- e) n K: V
- 4 ]% \) z, }- [
- module debounce( input wire clk, nrst, input wire key_in, output reg key_out
6 F6 o, V/ T' c4 N9 j) V g - );// localparam TIME_20MS = 1_000_000;
0 f0 m; E. U6 _) {0 _7 r/ m - localparam TIME_20MS = 1_000; // just for test
+ h6 B; y* S+ K h; M9 \& Z
7 V' r: W* C* N9 Q/ e9 N- reg key_cnt; reg [20:0] cnt; always @(posedge clk or negedge nrst) begin
( P+ V- E/ [7 h3 Y% }; |5 S% Q - if(nrst == 0)7 b4 b8 m- l' Z- s; Q
- key_cnt <= 0; else if(key_cnt == 0 && key_out != key_in)) T$ j1 t: c- a) `9 }
- key_cnt <= 1; else if(cnt == TIME_20MS - 1)
: X, Q# Y! I, `& i - key_cnt <= 0; end
7 s! y5 q1 k1 U
$ X! [: G1 }3 K. e) G! ?9 L- always @(posedge clk or negedge nrst) begin6 W3 }- U( m- @
- if(nrst == 0)
$ @6 z1 R. S% @7 { - cnt <= 0; else if(key_cnt)
8 w5 @7 h' D9 Q9 `9 ?# U# N2 w - cnt <= cnt + 1'b1;2 C9 S' r1 U9 q, a ~
- else
3 x- y6 R( @2 g L - cnt <= 0; end: _% e) e9 P$ K) E# @/ y9 U. J
& s) X* D: C5 |2 R3 T: k- always @(posedge clk or negedge nrst) begin
* W1 F3 J0 @0 D; V0 u* d. m - if(nrst == 0)
: b% g4 \ w# N+ ^ - key_out <= 0; else if(key_cnt == 0 && key_out != key_in)0 q( [8 v N& a8 j# J, K" ?- g
- key_out <= key_in; endendmodule
复制代码 测试代码
! _ i! F2 w/ X h% d4 n" h- // 按键消抖测试电路// 时间单位`timescale 1ns/10ps// modulemodule debounce_tb; // time period parameter
+ I8 _/ n7 m6 I5 I! m - localparam T = 20; // variable
1 l: q' i) u( m. v - reg clk, nrst; reg key_in; wire key_out; // instantiate debounce uut(
+ A4 U) h% U1 b) }' M - .clk (clk ),/ \ X/ g3 K+ s
- .nrst (nrst ),1 D- X9 E0 \8 ~+ x* ]% q# z$ m2 z
- .key_in (key_in ),$ k8 N r; {8 l/ a' y4 N: `
- .key_out(key_out)
# L8 ]; H- j- w6 t( k$ Y - ); // clock
. p `" y% `) H - initial begin6 H0 s! M; ? b( @1 O. N% m F
- clk = 1; forever #(T/2) clk = ~clk; end
2 v, R T' J' G* Q; L
+ {0 r9 c- a% ~( w. C- // reset
$ g* x$ r! C& ? - initial begin
3 j L5 W* A2 K% ~. D. y - nrst = 1;+ a) C- V' W9 G/ B
- @(negedge clk) nrst = 0;
. p; G) r) \1 W; m - @(negedge clk) nrst = 1; end/ m3 V1 r0 F5 |, `0 i7 j4 z) S
- x5 @5 L- Y* B* t* d- // key_in
8 c. i. s2 Z& J' \' y8 w& E$ j - initial begin3 Z3 M% s/ l" W+ H
- // initial value/ C3 {: o, q& D
- key_in = 0;
: i/ [ b5 {9 |9 M! F - // wait reset* B0 ?4 u4 Q8 t. |& V
- repeat(3) @(negedge clk);
0 p( ]0 n5 _7 M/ p - // no bounce // key down4 W. o2 G- V1 b- D! {/ p, i8 x2 S$ s
- key_in = 1; // last 60ms' g; [1 f0 X1 x# V! Z& P
- repeat(3000) @(negedge clk); // key up
7 c* U: d8 \ h3 c) P! j. K1 s - key_in = 0; // wait 50ms
- ^/ [7 R+ c- ^' j - repeat(2500) @(negedge clk); // down 5ms, up 15ms // key down, bounce 5ms
4 t- ^3 d& S) V* [( p; L& G7 F1 x/ H - repeat(251) @(negedge clk) key_in = ~key_in; // last 60ms
: e2 t0 ~" Y9 W - repeat(3000) @(negedge clk); // key up, bounce 15ms
- o- [5 k0 }6 g" t( R, M2 ~& M5 ]% [ - repeat(751) @(negedge clk) key_in = ~key_in; // wait 50ms
# d, Y( A! ^7 M6 @" | - repeat(2500) @(negedge clk); // down 19ms, up 19ms // key down, bounce 19ms
0 X# [! a& d* y5 [ - repeat(951) @(negedge clk) key_in = ~key_in; // last 60ms, ^0 m" U; i/ v8 X
- repeat(3000) @(negedge clk); // key up, bounce 19ms
4 S9 i% i$ T( R* H' E, a, P - repeat(951) @(negedge clk) key_in = ~key_in; // wait 50ms; B/ w$ Q2 ?4 f( }1 @
- repeat(2500) @(negedge clk);
0 P+ d+ G/ H- [) i- Q8 K- C - // additional, this situation shoud not ever happen // down 25ms, up 25ms // key down, bounce 25ms
! I3 D4 ~& @& W# o4 }1 ` - repeat(1251) @(negedge clk) key_in = ~key_in; // last 60ms
( t- F; k% t6 S# P+ \ - repeat(3000) @(negedge clk); // key up, bounce 25ms8 J6 L7 f% S6 B- Z8 y
- repeat(1251) @(negedge clk) key_in = ~key_in; // wait 50ms
* l, [7 y W C - repeat(2500) @(negedge clk); // stop $stop; endendmodule
复制代码
& T# \- B! C1 p. U' K4 K放在最后的,并不一定是最不重要的. H# O6 o- B& l5 ~" a8 p- n. c% b
" y" i/ [! e1 M. R/ N对于上面的三种方案,我比较喜欢第三种方案,它更贴合实际的按键状态,以上的代码我都做过modelsim仿真,但还没有在实际的项目中验证。在整理准备这个博客的时候,我又想到了一个感觉是更巧妙的方案,具体是这样的:在第三个方案的基础上,因为按键输入有变化的第一时刻,输出就已经改变了,在这种情况下,我可以把计时的时长改为一个很小的值,该值只要比抖动中的最长高低电平变化时间长即可。但想想也没这个必要,且这个抖动的高低电平变化时长我也很难去给它界定一个值。 |