按键为什么会乱跳:一次按下为什么变成很多次

LED 亮起来之后,我又插上了两个轻触按键。A 按键接 PB11,B 按键接 PB1;两个按键的另一端都接 GND。A 按一下,数值加一,B 按一下,数值减一,三颗外接 LED 把这个数值显示成三位二进制。

第一次测试,问题立刻出现了:手指只按了一下,数字却往前跳了一长串。松开按键,数字才停住。

我先问自己:“是不是按键坏了?”然后回头检查代码。代码看起来很规矩:检测到按下,就一直等到松手,松手以后再给出一个结果。按下、等待、输出,顺序没有少任何一步:

if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET)
{
    while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET)
    {
    }

    value++;
}

按下时进入 if,按住时卡在 while,松手后才执行 value++。乍一看,一次按键应该只加一次,逻辑几乎挑不出毛病。

但实际运行还是会多次触发,究竟是为什么呢?

按住一次,程序为什么能数很多次

接线是:

  • A 按键一端接 PB11,B 按键一端接 PB1;
  • 两个按键的另一端接 GND;
  • PB11、PB1 都配成内部上拉输入;
  • PC13、PC14、PC15 接三颗外接 LED,分别显示 bit0、bit1、bit2。

(接线沿用这三颗灯。手册第 20 页注明:PC13~PC15 作输出时不能作为电流源驱动 LED,而且同一时间只能有一个作输出。按手册看,这个接法是出规格的——这里不改接线,只把限制写出来。)

先把输入初始化好:

void Key_Init(void)
{
    GPIO_InitTypeDef gpio;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    gpio.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_1;
    gpio.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &gpio);
}

GPIO_Mode_IPU 里的 IPU 是上拉输入。松开按键时,PB11 通过内部上拉保持高电平;按下按键时,PB11 被按键接到 GND,读到低电平。

uint8_t Key_A_IsPressed(void)
{
    return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET;
}

uint8_t Key_B_IsPressed(void)
{
    return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET;
}

这些函数只回答一个问题:“现在是不是按下状态?”按住不动,它们会连续返回 1。如果主循环这样写:

while (1)
{
    if (Key_A_IsPressed())
    {
        count++;
    }
}

手指按住几十毫秒,主循环已经转了成千上万圈,count 也就增加了那么多。

为什么一定要上拉

如果 PB11 既没有接到高电平,也没有接到低电平,松开按键后它就悬在空中,读到的电平没有依据。

这就是浮空输入。把模式临时改成:

gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;

然后断开按键,连续读取 PB11。LED 可能自己闪,计数可能自己变,手指靠近时读数还会变化。你没有按它,芯片却收到了一个没有固定答案的输入。

内部上拉的作用很直接:没人按键时,给 PB11 一个默认高电平。按键按下后,GND 的连接比内部上拉更强,把它拉成低电平。

也可以使用外部上拉电阻。内部上拉省了一颗电阻,接线更简单;外部电阻的阻值和电气特性更容易控制。对于这个入门按键,内部上拉已经够用。

一次按下和当前按着不是一回事

边沿检测可以把“松开到按下”抓出来。A、B 各自保存一份上一次状态:

uint8_t Key_A_DetectPressEdge(void)
{
    static uint8_t was_pressed = 0;
    uint8_t is_pressed = Key_A_IsPressed();
    uint8_t event;

    event = (uint8_t)(is_pressed && !was_pressed);
    was_pressed = is_pressed;

    return event;
}

uint8_t Key_B_DetectPressEdge(void)
{
    static uint8_t was_pressed = 0;
    uint8_t is_pressed = Key_B_IsPressed();
    uint8_t event;

    event = (uint8_t)(is_pressed && !was_pressed);
    was_pressed = is_pressed;

    return event;
}

主循环不再对每个低电平都计数,而是只在状态从 0 变成 1 的那一轮得到事件。三颗灯这一侧用一个 LED_ShowBinary(),它把这个数的低三位分别写到 PC13PC14PC15 上;按键这边只负责决定什么时候该调它:

while (1)
{
    if (Key_A_DetectPressEdge())
    {
        value = (uint8_t)((value + 1U) & 0x07U);
        LED_ShowBinary(value);
    }

    if (Key_B_DetectPressEdge())
    {
        value = (uint8_t)((value + 7U) & 0x07U);
        LED_ShowBinary(value);
    }
}

三颗灯那边,一位对一根引脚:

void LED_ShowBinary(uint8_t value)
{
    if (value & 0x01U) { GPIO_SetBits(GPIOC, GPIO_Pin_13); } else { GPIO_ResetBits(GPIOC, GPIO_Pin_13); }
    if (value & 0x02U) { GPIO_SetBits(GPIOC, GPIO_Pin_14); } else { GPIO_ResetBits(GPIOC, GPIO_Pin_14); }
    if (value & 0x04U) { GPIO_SetBits(GPIOC, GPIO_Pin_15); } else { GPIO_ResetBits(GPIOC, GPIO_Pin_15); }
}

但这还没有解决轻触按键本身的另一个问题。金属触点碰到一起时,不会像理想开关那样干净地从高变低。它可能在几毫秒内弹来弹去,高、低、高、低,最后才稳定下来。边沿检测会把这些抖动也当成好几次按下。

先用最容易看懂的办法确认一次按下

发现按下后,先等一小段时间,再读一次;确认仍然按着,就等到松手,最后只返回一次事件:

static void Key_WaitShort(void)
{
    volatile uint32_t i;

    for (i = 0; i < 72000U; i++)
    {
    }
}

uint8_t Key_A_GetPressEvent(void)
{
    if (Key_A_IsPressed())
    {
        Key_WaitShort();

        if (Key_A_IsPressed())
        {
            while (Key_A_IsPressed())
            {
            }

            Key_WaitShort();
            return 1U;
        }
    }

    return 0U;
}

下载后按一次 A 或 B,三颗 LED 的二进制数只改变一次。这个版本把触点刚碰到时的抖动过滤掉了,也把按住期间的重复读取压成了一个事件。

按键没有“按了一次”这个电平。它只给出一根线的高低变化;一次、两次和很多次,是程序在这些变化里整理出来的事件。

最后橘猫说

按住按键时,输入电平会在一段时间内持续有效;如果主循环每转一圈就把有效电平当成一次操作,计数自然会飞快增加。触点抖动又会在刚按下和刚松开时制造额外的高低变化。等待一小段时间再确认、再等待松手,是最容易看懂的阻塞式处理,能把一次按压压成一次返回值,但也会占住主循环。更精确的等待和非阻塞消抖,要在稳定的时间基准建立后再做。

最后蜘蛛说

原来不止我的手会抖,按钮也会啊。

posted @ 2026-09-23 10:34  Zw-awa  阅读(37)  评论(0)    收藏  举报