按键为什么会乱跳:一次按下为什么变成很多次
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(),它把这个数的低三位分别写到 PC13、PC14、PC15 上;按键这边只负责决定什么时候该调它:
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 的二进制数只改变一次。这个版本把触点刚碰到时的抖动过滤掉了,也把按住期间的重复读取压成了一个事件。
按键没有“按了一次”这个电平。它只给出一根线的高低变化;一次、两次和很多次,是程序在这些变化里整理出来的事件。
最后橘猫说
按住按键时,输入电平会在一段时间内持续有效;如果主循环每转一圈就把有效电平当成一次操作,计数自然会飞快增加。触点抖动又会在刚按下和刚松开时制造额外的高低变化。等待一小段时间再确认、再等待松手,是最容易看懂的阻塞式处理,能把一次按压压成一次返回值,但也会占住主循环。更精确的等待和非阻塞消抖,要在稳定的时间基准建立后再做。
最后蜘蛛说
原来不止我的手会抖,按钮也会啊。

手指只按了一下,三颗 LED 拼出的数却往前跳了一长串。最容易看懂的那种写法,凭什么能把一段连续的电平,稳稳地压成一次返回值?
浙公网安备 33010602011771号