按键去抖

由于机械按键的特性(例如轻触开关),在开关的瞬间,不会稳定地闭合或断开,一连串的抖动信号可以轻易被单片机捕获,因此单片机可能会误判按键状态。为了能够让单片机正确识别按键状态,必须对输入的按键信号进行去抖,分为硬件去抖软件去抖。实际项目中最常用的去抖方案就是软件定时器 + 硬件电容滤波

1.抖动波形

正常人类操作按键的时间约几百毫秒到几秒不等,而按键的抖动信号的持续时间约5ms到20ms。按键的闭合以及松开的瞬间均会产生抖动信号。

按键抖动波形

按键在空闲时(稳定松开状态)默认高电平,以下分析上升沿:按键在按下状态,松开瞬间的波形,也就是从低电平变为高电平的瞬间。

2.创建工程

STM32CubeMX创建工程,设置PA2GPIO_EXTI2(配置为外部中断触发),同时设置PC13作为GPIO输出。

初始化工程

配置触发中断条件

生成工程并导入到EIDE中,在/* USER CODE BEGIN 4 */代码块之间插入代码,意思是在GPIO的输入中断里切换PC13的电平(切换LED灯显示),编译烧录到STM32开发板中

1
2
3
4
5
6
7
8
9
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
// 判断产生中断的引脚
if (GPIO_Pin == GPIO_PIN_2)
{
// 切换LED灯状态
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}

3.波形分析

将一个轻触开关的一端接入PA2,另一端接入GND,多次按下按键,观察LED状态,发现不能每次都稳定地切换LED灯状态。将示波器接入PA2,参数设置如下,开启单次触发,观察波形不难看出在按键松开的过程中发生了多次抖动

触发类型 触发方式 时基 幅值
上升沿 常规 100uS 1V

按键实际抖动波形

4.软件去抖

4.1 延时

延时的方式相当直观简单,在主循环中,只要检测到PA2为低电平,也就是按键按下,延迟10ms再次检测是否为低电平,如果是说明按键按下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 如果检测到低电平,即按键按下
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2) == GPIO_PIN_RESET)
{
// 延时10ms
HAL_Delay(10);
// 再判断一次是否为低电平
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2) == GPIO_PIN_RESET)
{
// 切换LED显示状态
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
// 等待按键松开
while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2) == GPIO_PIN_RESET);
}
}

延时判断代码

虽然延时判断的方式简单有效,但是会极大影响系统的实时性(系统在延时期间无法处理其他任务)。正常情况下不会应用到实际项目中,除非是简单验证功能或者项目没有实时性的要求。

4.2 定时器

STM32CubeMX添加定时器TIM1,参数如下。定时器中断去抖的本质上就是检测在某一段时间内是否有连续相同的电平状态。
定时器TIM1配置
启用TIM1中断

以下函数均在main.c文件中实现

1
2
3
#define PRESS_STATE 0
#define RELEASE_STATE 1
#define DEBOUNCE_TIME 25
1
2
int old_btn_state = 0;
int new_btn_state = 0;
1
2
3
4
HAL_TIM_Base_Start_IT(&htim1);
// 按键状态 初始化
old_btn_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2);
new_btn_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (old_btn_state != new_btn_state)
{
// ** 此处可以执行耗时操作
// 如果按键按下
if (new_btn_state == PRESS_STATE)
{
// TODO VSCode 添加日志点
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
else // 按键松开
{
// TODO VSCode 添加日志点
}
// 更新按键状态
old_btn_state = new_btn_state;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void button_scan(int *state)
{
// 按下时长
static int press_time = 0;
// 松开时长
static int release_time = 0;
// 如果读取到低电平
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2) == GPIO_PIN_RESET)
{
// 清零松开时长
release_time = 0;
// 累计按下时长
press_time++;
// 如果定时器检测到连续25ms都是低电平,说明按键按下状态有效
if (press_time >= DEBOUNCE_TIME)
{
*state = PRESS_STATE;
}
}
else // 如果读取到高电平
{
// 清零按下时长
press_time = 0;
// 累计松开时长
release_time++;
// 如果定时器检测到连续25ms都是高电平,说明按键松开状态有效
if (release_time >= DEBOUNCE_TIME)
{
*state = RELEASE_STATE;
}
}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim1)
{
// 判断并更新按键状态
button_scan(&new_btn_state);
}
}

5.硬件去抖

5.1 电容滤波

最常见的硬件去抖方式是电容滤波,给按键并联一个电容和串联一个电阻,利用电容的充放电特性实现一定程度的滤波。

以电容取值100nF,电阻取值10KΩ为例,原理图如下,将示波器探头接入PA2,观察按键松开时的波形

电容滤波原理图

电容滤波结果

底部的测量参数上升沿1.524ms,实际从低电平爬升至高电平需要3-4ms左右,因此电容的取值不能过大,否则爬升时间太长可能会影响按键的有效判断。

5.2 RS触发器

RS触发器就是使用两个与非门的级联,其中一个与非门的输出作为另一个与非门的输入,描述方法是现态$Q^{n}$和次态$Q^{n+1}$,通常用于没有MCU的场合,使用较少。

单击此处可以在线模拟RS触发器按键效果,单击切换开关,观察底部的示波器波形,示意图如下

模拟RS触发器

根据与非门的先与后非的运算规则,只要输入0,一定输出1,真值表如下

R输入 S输入 Q输出
0 0 1
0 1 1
1 0 1
1 1 0
  • 当R1稳定连接R1 = 0,则S2一定稳定断开(单刀双掷开关不能可能同时导通), S2 = 1;根据与非门规则:Q1 = 1;根据图中R2与Q1直连,因此R2 = Q1 = 1, 因此Q2 = 0;S1与Q2直连,因此S1 = Q2 = 0
  • 当R1开始断开, R1状态不稳定,随机1或0,但是因为且S2状态不变,S1 = 0也不变,因此Q1状态不变Q1 = 1
  • 当R1完全断开R1 = 1,Q1保持
  • 当S2开始连接,S2状态不稳定,随机1或0;当S2第一次等于0,则Q2 = 1S1 = Q2 = 1,因为R1稳定断开R1 = 1,因此Q1 = 0R2 = Q1 = 0,后续就算是S2再变成1,因为R2已经稳定,因此Q1稳定
  • 当S2稳定连接S2 = 0,Q1不变Q1 = 0

按照目前按键的生产工艺,抖动的情况已经有比较好的改善,甚至有些品质较好的按键都已经不会产生抖动信号。
另外,有一些单片机在其GPIO功能上就内置了按键去抖功能,在初始化GPIO输入的时候,设置结构体的deboune成员变量为10ms即可。相对与自行实现的按键去抖方案,不仅实现起来简单,而且还节省一个定时器资源。

参考


按键去抖
https://blog.gogo.uno/2024/03/16/debounce/
作者
Orionxer
发布于
2024年3月16日
更新于
2024年8月3日
许可协议