首页 > 代码库 > 【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二:按键模块① - 消抖

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二:按键模块① - 消抖

实验二:按键模块① - 消抖

按键消抖实验可谓是经典中的经典,按键消抖实验虽曾在《建模篇》出现过,而且还惹来一堆麻烦。事实上,笔者这是在刁难各位同学,好让对方的惯性思维短路一下,但是惨遭口水攻击 ... 面对它,笔者宛如被甩的男人,对它又爱又恨。不管怎么样,如今 I’ll be back,笔者再也不会重复一样的悲剧。

按键消抖说傻不傻说难不难。所谓傻,它因为原理不仅简单(就是延迟几下下而已),而且顺序语言(C语言)也有无数不尽的例子。所谓难,那是因为人们很难从单片机的思维跳出来 ... 此外,按键消抖也有许多细节未曾被人重视,真是让人伤心。按键消抖一般有3段操作:

l 检测电平变化;

l 过滤抖动(延迟);

l 产生有效按键。

假设C语言与单片机的组合想要检测电平变化,它们一般是利用if查询或者外部中断。事后,如果这对组合想要过滤抖动,那么可以借用for延迟的力量,又或者依赖定时中断产生精明的延迟效果。反观有效案件的产生,这对组合视乎而外钟情“按下有效”似的 ... 不管怎么样,C语言与单片机这对组合在处理按键的时候,它们往往会错过一些黄金。

“黄金?”,读者震撼道。

所谓黄金时间就是电平发生变化那一瞬间,还有消抖(延迟)以后那一瞬间。按键按下期间,按键的输入电平故会发生变化,如果使用if查询去检测,结果很容易浪费单片机的处理资源,因为单片机必须一直等待 ... 换之,如果反用外部中断,中断寻址也会耽误诺干时间。

假设C语言与单片机这对组合挨过电平检测这起难关,余下的困难却是消抖动作。如果利用for循环实现去消抖,例如 Delay_ms(10) 之类的函数。For循环不仅计数不紧密,而且还会白白浪费单片机的处理资源。定时中断虽然计数紧密,但是中断触发依然也会产生诺干的寻址延迟。补上,所谓寻址延迟是处理器处理中断触发的时候,它要事先保护现场之余,也要寻址中断处理入口,然后执行中断函数,完后回复现场,最后再返回当前的工作。

感觉上,笔者好似在欺负C语言以及单片机,死劲说它们的不是。亲爱的读者,千万别误会,笔者只是在陈述事实而已。单片机本来就是比较粗野的爷们,它很难做到紧凑又毫无空隙的操作,反观FPGA却异常在行。所以说,单片机的思路很难沿用在FPGA身上,否则会出现许多笑话。如今,这是描述语言以及FPGA的新时代,所谓后浪推前浪正是新旧时代的替换。

FPGA不仅没有隐性处理,而且描述语言也是自由自身。我们只要方法得当,手段有效,“黄金”要多少就有多少 ... 哇哈哈!在此,笔者说了那么多废话只是告知读者,千万别用单片机的思维去猜摸FPGA如何处理按键校抖,不然问号会没完没了。好了,废话差不多说完了,让我们切回主题吧。

clip_image002

图2.1 按键活动的时序示意图。

如图2.1 所示,那是按键活动的时序图。高电为平按键默认状态,按键一旦按下,“按下事件”就发生了,电平随之发生抖动,抖动周期大约为10ms。事后,如果按键依然按着不放,那么电平便会处于低电平。换之,如果按键此刻被释放,那么“释放事件”发生了,电平随之由低变高,然后发生抖动,抖动周期大约为10ms。笔者曾在前面说过,按键消抖组一般有3个工作要做,亦即检测电平变化,过滤抖动,还有产生有效按键。

检测电平变化:

clip_image004

图2.2 按键电平变化,按下事件与释放事件。

顾名思义,检测电平变化就是用来察觉“按下事件”还有“释放事件”,或者监控电平的状态变化。如图2.2所示,笔者建立一组寄存器F1~F2,F1暂存当前的电平状态,F2则暂存上一个时钟的电平状态。Verilog语言可以这样表示,如代码2.1所示:

reg F2,F1;
always @ ( posedge CLOCK )
    { F2,F1 } <= { F1,KEY };
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

代码2.1所示

根据图2.2的显示,按下事件是 F2 为1值,F1为0值;释放事件则是 F2为0值,F1为1值。为了不要错过电平变化的黄金时间,“按下事件”还有“释放事件”必须作为“即时“,为此 Verilog语言可以这样表示:

wire isH2L = ( F2 == 1 && F1 == 0 );
wire isL2H = ( F2 == 0 && F1 == 1 );
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

过滤抖动:

clip_image006

图2.3 过滤抖动。

过滤抖动就是也可以称为延迟抖动,常规又廉价的机械按键,抖动时间大约是10ms之内,如图2.3.所示。抖动一般都发生在“按下事件”或者“释放事件”以后,过滤抖动就是延迟诺干时间即可。Verilog语言则可以这样表示,如代码2.2所示:

3: 
if( C1 == T10MS -1 ) begin C1 <= 19‘d0; i <= i + 1‘b1; end
else C1 <= C1 + 1‘b1;
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

代码2.2

产生有效按键:

clip_image008

图2.4 产生有效按键。

产生有效按键亦即立旗有效的按键事件,如图2.4所示,按下有效isPress信号,还有释放有效isRelease信号,两个信号分别都是拉高一个时钟。除了常见的按下有效或者释放有效以外,根据设计要求,有效按键按也有其它,例如:按键按下两下有效(双击),按键按下一段时间有效(长击)。至于Verilog语言则可以这样表示,如代码2.3所示:

1: begin isPress <= 1‘b1; i <= i + 1‘b1; end               
2: begin isPress <= 1‘b0; i <= i + 1‘b1; end
...
1: begin isRelease <= 1‘b1; i <= i + 1‘b1; end               
2: begin isRelease <= 1‘b0; i <= i + 1‘b1; end
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

代码2.3

热身完毕后,我们就可以进入实验主题了。

clip_image010

图2.5 实验二建模图。

如图2.5所示,哪里有一块名为 key_funcmod 的功能模块,输入端为 KEY信号,输出端却为 LED信号。KEY信号连接按键资源,LED信号分别驱动两位LED资源。按键功能模块的工作主要是过滤 KEY信号变化以后所发生的抖动,接着产生“按下有效”还有“释放有效”两个有效按键,然后点亮LED资源。

key_funcmod.v
1.    module key_funcmod
2.    (
3.         input CLOCK, RESET,
4.         input KEY,
5.         output [1:0]LED
6.    );
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

以上内容为出入端声明。

7.         parameter T10MS = 19‘d500_000;
8.         
9.         /***************************/ //sub
10.         
11.         reg F2,F1; 
12.             
13.         always @ ( posedge CLOCK or negedge RESET ) 
14.             if( !RESET ) 
15.                  { F2, F1 } <= 2‘b11;
16.              else 
17.                  { F2, F1 } <= { F1, KEY };
18.                    
19.         /***************************/ //core            
20.        
21.        wire isH2L = ( F2 == 1 && F1 == 0 );
22.        wire isL2H = ( F2 == 0 && F1 == 1 );
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

以上内容为常量声明以及电平检测的周边操作。第7行则是10ms的常量声明,第11~17则是电平状态检测的周边操作。至于第21~22行则是“按下事件”还有“释放事件”的即时声明。

23.         reg [3:0]i;         
24.         reg isPress, isRelease;
25.         reg [18:0]C1;
26.         
27.         always @ ( posedge CLOCK or negedge RESET )
28.             if( !RESET )
29.                   begin
30.                          i <= 4‘d0;
31.                         { isPress,isRelease } <= 2‘b00;
32.                         C1 <= 19‘d0;
33.                     end
34.              else
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

以上内容为相关的寄存器声明以及复位操作。i用来指向步骤,isPress与 isRelease则标示按下有效亦即释放有效,C1则用来计数。

35.                  case(i)
36.                         
37.                         0: // H2L check 
38.                         if( isH2L ) i <= i + 1‘b1;
39.                         
40.                         1: // Key trigger prees up
41.                         begin isPress <= 1‘b1; i <= i + 1‘b1; end
42.                       
43.                          2: // Key trigger prees down
44.                         begin isPress <= 1‘b0; i <= i + 1‘b1;    end     
45.                     
46.                         3: // H2L debounce
47.                         if( C1 == T10MS -1 ) begin C1 <= 19‘d0; i <= i + 1‘b1; end
48.                         else C1 <= C1 + 1‘b1;
49.                         
50.                         4: // L2H check 
51.                         if( isL2H ) i <= i + 1‘b1;
52.                         
53.                         5: // Key trigger prees up
54.                         begin isRelease <= 1‘b1; i <= i + 1‘b1; end
55.                       
56.                          6: // Key trigger prees down
57.                         begin isRelease <= 1‘b0; i <= i + 1‘b1; end
58.                    
59.                         7: // L2H debounce
60.                         if( C1 == T10MS -1 ) begin C1 <= 19‘d0; i <= 4‘d0; end
61.                         else C1 <= C1 + 1‘b1;        
62.                    
63.                    endcase
 
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

以上内容为核心操作。具体的核心操作过程如下:

步骤0等待电平由高变低;

步骤1~2用来产生按下有效的高脉冲;

步骤3用来过滤由高变低所引发的抖动;

步骤4等待电平由低变高;

步骤5~6用来产生释放有效的高脉冲;

步骤7用来过滤由低变高所引发的抖动,然后返回步骤0。

 
64.        
65.        /***********************/ // sub-demo
66.        
67.        reg [1:0]D1;
68.        
69.        always @ ( posedge CLOCK or negedge RESET )
70.             if( !RESET )
71.                 D1 <= 2‘b00;
72.             else if( isPress )
73.                 D1[1] <= ~D[1];
74.             else if( isRelease )
75.                 D1[0] <= ~D[0];
76.                    
77.        assign LED = D1;
78.    
79.    endmodule
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

以上内容为演示用的周边操作,它根据 isPress 还有 isRelease 的高脉冲,分别翻转 D1[1] 还有 D1[0]的内容。至于第77行则是输出驱动的声明,D1驱动LED输出端。编译完后便下载程序。

我们会发现第一次按下 <KEY2> 会点亮 LED[1],释放<KEY2>会点亮 LED[0]。第二次按下 <KEY2> 会消灭 LED[1],释放 <KEY2> 则会消灭 LED[0]。如此一来,实验二已经成功。实验二未结束之前,让笔者分析一下实验二的诺干细节:

细节一:过分消抖

clip_image012

图2.6 过分消抖(过分延迟)。

结果如图2.6所示,假设笔者手痒将消抖时间拉长至1s,Verilog语言则可以这样表示,如代码2.4所示:

    3: 
    if( C1 == T1S -1 ) begin C1 <= 19‘d0; i <= i + 1‘b1; end
    else C1 <= C1 + 1‘b1;
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

代码2.4

一般消抖期间,按键功能模块的核心操作就会停留在消抖(延迟)步骤,如果消抖期间笔者释放按键,那么释放事件会被无视,然后核心操作会被打乱,最后整个功能模块会跑飞。反之,如果笔者等待消抖完毕再释放按键,那么释放事件会照常发生,整个功能模块也会照常运作。

细节二:精密控时

    1: // Key trigger prees up
    begin isPress <= 1‘b1; i <= i + 1‘b1; end                   
    2: // Key trigger prees down
    begin isPress <= 1‘b0; i <= i + 1‘b1;    end     
    3: // H2L debounce
    if( C1 == T10MS -1 ) begin C1 <= 19‘d0; i <= i + 1‘b1; end
    else C1 <= C1 + 1‘b1;
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

代码2.5

根据按键功能模块,核心操作执行消抖之前都会事先产生有效按键,亦即为立旗寄存器(isPress 或者 isRelease)拉高又拉低一个时钟,如代码2.5所示。如果笔者是一位精密控时的狂人,这段拉高又拉低的时钟消耗,笔者也会将其考虑进去消抖时间。为此,笔者可以这样修改,如代码2.6所示:

    1: // Key trigger prees up
    begin isPress <= 1‘b1; i <= i + 1‘b1; end                   
    2: // Key trigger prees down
    begin isPress <= 1‘b0; i <= i + 1‘b1;    end     
    3: // H2L debounce
    if( C1 == T10MS -1 -2 ) begin C1 <= 19‘d0; i <= i + 1‘b1; end
    else C1 <= C1 + 1‘b1;
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

代码2.6

代码2.6相较代码2.6,消抖步骤部分的if判断内多了 -2,其中 -2表示产生按键有效所消耗的时钟。

细节三:完整的个体模块

clip_image014

图2.7 完整的按键功能模块。

图2.6是演示用的建模图,然而图2.7则是完整的建模图,其中按键功能模块有一个 KEY输入端,主要连接按键资源。此外,按键功能模块也有一组两位的沟通信号Trig,亦即按下Trig[1]产生一个高脉冲,释放Trig[0]产生一个高脉冲。

key_funcmod.v
1.    module key_funcmod
2.    (
3.         input CLOCK, RESET,
4.         input KEY,
5.         output [1:0]oTrig
6.    );
7.         parameter T10MS = 19‘d500_000;
8.         
9.         /***************************/ //sub
10.         
11.         reg F2,F1; 
12.             
13.         always @ ( posedge CLOCK or negedge RESET ) 
14.             if( !RESET ) 
15.                  { F2, F1 } <= 2‘b11;
16.              else 
17.                  { F2, F1 } <= { F1, KEY };
18.                    
19.         /***************************/ //core            
20.        
21.         wire isH2L = ( F2 == 1 && F1 == 0 );
22.         wire isL2H = ( F2 == 0 && F1 == 1 );
23.         reg [3:0]i;         
24.         reg isPress, isRelease;
25.         reg [18:0]C1;
26.         
27.         always @ ( posedge CLOCK or negedge RESET )
28.             if( !RESET )
29.                   begin
30.                        i <= 4‘d0;
31.                         { isPress,isRelease } <= 2‘b00;
32.                         C1 <= 19‘d0;
33.                     end
34.              else
35.                  case(i)
36.                         
37.                         0: // H2L check 
38.                         if( isH2L ) i <= i + 1‘b1;
39.                         
40.                         1: // Key trigger prees up
41.                         begin isPress <= 1‘b1; i <= i + 1‘b1; end
42.                       
43.                          2: // Key trigger prees down
44.                         begin isPress <= 1‘b0; i <= i + 1‘b1;    end     
45.                     
46.                         3: // H2L debounce
47.                         if( C1 == T10MS -1 ) begin C1 <= 19‘d0; i <= i + 1‘b1; end
48.                         else C1 <= C1 + 1‘b1;
49.                         
50.                         4: // L2H check 
51.                         if( isL2H ) i <= i + 1‘b1;
52.                         
53.                         5: // Key trigger prees up
54.                         begin isRelease <= 1‘b1; i <= i + 1‘b1; end
55.                       
56.                         6: // Key trigger prees down
57.                         begin isRelease <= 1‘b0; i <= i + 1‘b1; end
58.                    
59.                         7: // L2H debounce
60.                         if( C1 == T10MS -1 ) begin C1 <= 19‘d0; i <= 4‘d0; end
61.                         else C1 <= C1 + 1‘b1;        
62.                    
63.                    endcase
64.                    
65.        /********************************/
66.                    
67.        assign oTrig = { isPress, isRelease };
68.    
69.    endmodule
<style type="text/css">.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; } </style>

最后别向笔者要仿真了,因为按键消抖没有仿真的意义,单是代码已经足够脑补时序了。