手把手教你如何按键消抖

我们经常能使用到按键,然而由于按键的机械结构,导致了按键并不会完全像我们想象的那样工作。

理想的波形是上面那个,当我们一按下按钮,马上就把按键两端的电路连接起来,松开按键的时候两端电路再断开。

然而实际情况是下面那个波形,当我们按下按钮的时候,会有一段时间的不稳定状态,就是处于连接和断开之间的状态,也就是按键的抖动。

而我们一般是拿按键来用作外部中断的,外部中断条件常用的就是上升沿和下降沿(后续文章我们默认用的是下降沿触发中断),当按键在抖动的时候,很有可能触发多次外部中断,所以按键消抖是我们不得不面对的一个问题。

接下来的示例代码我用的STC库函数,也就是我最近开的一个系列,正好前天讲的是外部中断,昨天讲的定时器。

只要知道了原理,那么到其他芯片上也是一样的,用STC库函数来作为示例代码是因为最近正好在用。

第一个方法就是延时,假设我们遇到了下降沿,那么我们马上进行延时,一般来说按键的抖动时间不会超过10ms,越好(贵)的按键抖动时间越短。

经过我实测,延时7个ms就基本没什么问题了,延时过后直接就把按下的抖动给忽略掉了。

不过小伙伴们最好还是自己测试一下延时时间,因为不同的按钮抖动时长是不一样的。

这样就解决了我们按下按键的抖动问题,还有个问题就是我们松开按键的时候也是有抖动的,不过一般来说松开抖动的持续时间会比按下抖动的持续时间更短。

当我们松开按键的时候也有可能触发下降沿中断,所以在我们延时过后还需要再判断一下连接按键的引脚的电平,如果这时候还是低电平,那么才真正执行应该执行的处理代码。

#include "STC32G_GPIO.h"
#include "STC32G_Delay.h"
#include "STC32G_NVIC.h"
#include "STC32G_Exti.h"

void GPIO_Init(void){
    P3_MODE_IN_HIZ(GPIO_Pin_2);     // 将外部中断引脚配置为高阻输入
    P3_PULL_UP_ENABLE(GPIO_Pin_2);  // 将外部中断引脚使能上拉电阻
    P2_MODE_OUT_PP(GPIO_Pin_7);     // 将点灯引脚初始化为推挽输出.
}

void Exti_Init(void){
    EXTI_InitTypeDef initer;
    initer.EXTI_Mode = EXT_MODE_Fall;       // 下降沿触发中断
    Ext_Inilize(EXT_INT0, &initer);
    NVIC_INT0_Init(ENABLE, Priority_2);
}

void main(void){
    WTST = 0;		//设置程序指令延时参数,赋值为0可将CPU执行指令的速度设置为最快
    EAXSFR();		//扩展SFR(XFR)访问使能 
    CKCON = 0;      //提高访问XRAM速度

    GPIO_Init();
    Exti_Init();
    EA = 1;         // 开启中断

    while(1){
        if(WakeUpSource == 1){  // 当下降沿中断触发了
            WakeUpSource = 0;   // 清除中断标志
            delay_ms(7);        // 延时
            // 再次判断外部中断引脚电平,确认无误之后再翻转电平
            if(P32 == 0)    P27 = !P27; 
        }
    }
    return ;
}

虽然延时的方法简单易用,但是并不适用大多数应用场景,因为延时是会阻塞我们的程序的,因此我们可以用定时器的方式代替延时。

当我们第一次收到下降沿之后,我们开启定时器,定时个5ms,等到5ms过后,我们再次判断外部中断引脚的电平。

这边还有个要注意的是只有第一次收到下降沿的时候才开启定时器,也就是说当我们定时器正在运行的时候,我们忽略掉其他下降沿,因为那些大概率是抖动。

#include "STC32G_Delay.h"
#include "STC32G_Exti.h"
#include "STC32G_GPIO.h"
#include "STC32G_Timer.h"
#include "STC32G_NVIC.h"

uint8 timer0_open_flag = 0;
uint8 timer0_over_flag = 0;

// 定时器0中断函数
void Timer0_ISR_Handler(void) interrupt TMR0_VECTOR { 
    ET0 = 0;
    timer0_over_flag = 1;
    timer0_open_flag = 0;
}

void GPIO_Init(void) {
    P3_MODE_IN_HIZ(GPIO_Pin_2);     // 将外部中断引脚配置为高阻输入
    P3_PULL_UP_ENABLE(GPIO_Pin_2);  // 将外部中断引脚使能上拉电阻
    P2_MODE_OUT_PP(GPIO_Pin_7);     // 将点灯引脚初始化为推挽输出. 
}

void Exti_Init(void) {
    EXTI_InitTypeDef initer;
    initer.EXTI_Mode = EXT_MODE_Fall;  // 下降沿触发中断
    Ext_Inilize(EXT_INT0, &initer);
    NVIC_INT0_Init(ENABLE, Priority_1);
}

void Timer_Init(void) {
    TIM_InitTypeDef initer;
    initer.TIM_Mode = TIM_16BitAutoReload;          // 模式0,16位自动重装载寄存器.
    initer.TIM_ClkOut = DISABLE;                    // 失能可编程时钟输出
    initer.TIM_ClkSource = TIM_CLOCK_1T;            // 1T工作模式
    initer.TIM_Value = 55536;   // 计数寄存器和重装载寄存器的值设为55536
    initer.TIM_PS = 11;         // 预分频器的值设为11,实际上是12分频
    initer.TIM_Run = ENABLE;  
    Timer_Inilize(Timer0, &initer);
    NVIC_Timer0_Init(ENABLE, Priority_2);
    ET0 = 0;
}

void Open_Timer(void) {
    T0_Load(55536);
    ET0 = 1;
}

void main(void) {
    WTST = 0;       // 设置程序指令延时参数,赋值为0可将CPU执行指令的速度设置为最快
    EAXSFR();       // 扩展SFR(XFR)访问使能
    CKCON = 0;      // 提高访问XRAM速度

    GPIO_Init();
    Timer_Init();
    Exti_Init();
    EA = 1;         // 开启中断

    while (1) {
        if (WakeUpSource == 1) {  // 当下降沿中断触发了
            WakeUpSource = 0;
            // 若是定时器没开启,那么开启
            if (timer0_open_flag == 0){
                timer0_open_flag = 1;
                Open_Timer();
            }
        }
        if(timer0_over_flag == 1){
            // 定时器结束,若外部中断引脚仍为低电平,那么翻转电平
            timer0_over_flag = 0;
            if(P32 == 0)    P27 = !P27;
        }
    }
    return;
}

我上面代码中定时器0的中断函数是从STC32G_Timer_Isr.c里拿出来的。

我用了两个标记变量,当我们下降沿中断触发的时候,如果定时器没有开启,那么我们开启,同时把定时器开启标志置位,当定时器结束之后把定时器开启标志的那个变量置回0,同时把定时器结束标志置位。

我定时器定的是5ms,各位小伙伴要根据自己的按钮去设置合适的延时时间。

以上是软件层面的消抖方式,接下来就是硬件层面的了。

延时会阻塞程序,定时会消耗掉一个定时器的资源。

所以硬件层面的消抖也是我们经常会做的。

最简单的就是给按钮并联一个电容就行。

我常用的就是像上面那样加个100nf的电容,因为电容两端的电压不能突变,所以它可以让波形变得更平滑,也就是把抖动抹平了。

当然了,电容也不能百分百消除抖动,因为每个按键的结构都不太一样。

并且电容的取值也有讲究的,太小的话无法消除抖动,太大的话会导致我们按键按下还是没法拉低外部中断硬件的电平(一般也不会用那么大的电容)。

所以一般情况下我们无脑选个100nf就行了,然后在软件层面延时个1ms这样意思意思,或者不延时也行,效果也还是可以接受的。

如果我就是想搞清楚电容的容值放多少合适呢?

那我们就回顾一下电容的充放电公式,一个时间单位 = RC,一般来说三个时间单位我们就可以当电容充放电完成。

所以我们可以给电容再串联一个电阻,通过计算3个RC的持续长度大于抖动时间(不要大太多)来确定电容和电阻的值。

不过我们像上图那样并联个100nf就行了。

还有个不太常用的硬件解决方案,那就是加个RS触发器。

这个方案用的比较少,因为成本高,所以我们就简单提一下。

RS触发器实际上就是俩与非门,但是注意它们的连线方式。

接下来我们给AB加个上拉电阻,再加上按键控制

假设我们现在的开关接到A,那么由于A是0,所以C一定是1(与非门),因为B是1,所以D是0。

当按键进行抖动的时候,A的电平是在0和1之间飘忽不定的,重点来了,因为是与非门,A和D一起控制的C,由于D是0,所以不管A如何波动,都不会影响C和D的输出,这也就是RS消抖的原理,不过我认为实际上不是消抖,而是直接忽略了抖动。

以上就是今天分享的消抖方面的内容。

简单总结一下,软件方面是延时和定时,硬件方面是电容和RS触发器。

一般来说常用的就是并联个电容再加个小延时。

其他还有个方法是状态机,但是有点复杂,我们还是并联电容吧。

### RS触发器按键的实现方法 #### 硬件设计方案 RS触发器是一种双稳态电路,能够有效按键动的影响。具体来说,当按键未按下时,输出为0;当按键按下时,输出变为1。由于RS触发器具有记忆功能,即使按键因为机械弹性产生短暂的断开(即动跳开B),只要按键没有完全回到初始状态A,触发器的状态就不会改变,从而避免了动信号传递到后续电路中[^3]。 以下是基于RS触发器的硬件电路设计思路: - 使用两个“与非”门构建一个基本的RS触发器。 - 将按键的一端连接至电源,另一端通过电阻接地,并将该节点作为输入信号接入RS触发器的一个输入端。 - 利用电阻和电容组成的RC网络进一步平滑可能存在的微弱噪声。 ```circuitikz \begin{circuitikz} \draw (0,0) node[and port](AND1){}; \draw (2,0) node[and port](AND2){}; % Inputs and outputs of the RS flip-flop \draw (-1,1) -- (AND1.in 1); \draw (-1,-1) -- (AND1.in 2); \draw (AND1.out) |- (AND2.in 2); \draw (AND2.out) |- (AND1.in 1); % Key switch connection to input S \draw (-1.5,1) to[R,l=$R$,*-*] (-2,1) to[battery1,l_=$V_{cc}$](-2,-1)--(-1.5,-1); \draw (-2,0)node[left]{Key Switch}to[squarepole,o-o](-1.5,0); \end{circuitikz} ``` 这种硬件方案的优点在于无需额外编程支持即可完成处理,适合对实时性和可靠性要求较高的场景。 #### 软件设计方案 尽管硬件方法可以彻底解决按键动问题,但在许多实际应用场合下,采用软件方式进行按键更为经济高效。通常的做法是在检测到一次按键动作后加入适当延时再确认当前状态是否稳定不变。例如: ```c #include <util/delay.h> #define BUTTON_PIN PINB & (1 << PB0) void debounce_button() { uint8_t previous_state = BUTTON_PIN; while(1){ if(BUTTON_PIN != previous_state){ _delay_ms(20); // Delay for debouncing. if(BUTTON_PIN != previous_state){ // Process button press/release here... previous_state = BUTTON_PIN; } } } } ``` 上述代码片段展示了如何利用简单的循环等待配合短时间延迟来判断按键的真实变化情况[^1]。这种方法适用于资源有限的小型嵌入式系统中。 --- #### 综合考虑软硬结合的设计策略 对于某些复杂项目而言,单独依靠纯硬件或者仅靠软件都可能存在局限性。因此有时会采取两者相结合的方式——先由初步简易版硬件滤除大部分高频干扰成分后再交予处理器做最终精确判定。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值