按键消抖 #
背景介绍:[[https://docs.geeksman.com/esp32/MicroPython/08.esp32-micropython-button.html][控制 GPIO 输入 - 按键实验]]
http://www.labbookpages.co.uk/electronics/debounce.html#soft
https://www.ganssle.com/debouncing-pt2.htm
Rust 软件解耦库:https://docs.rs/debouncr/latest/debouncr/
- 示例代码: https://dev.to/theembeddedrustacean/esp32-embedded-rust-at-the-hal-uart-serial-communication-1ig4 https://github.com/apollolabsdev/ESP32C3/tree/41efd9d1bbdf8d2e071332af610bae0515407d07/no_std_examples/uart/src
使用按键的时候,通常情况下需要进行消抖。
什么是按键消抖?该实验中所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个 按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的 抖动,为了不产生这种现象而作的措施就是按键消抖。按键的抖 动对于人类来说是感觉不到的,但对单片机来说, 则是完全可以感应到的,而且还是一个很漫长的过程,因为单片机处理的速度在微秒级, =而按键抖动的时间至少 在毫秒级= 。
一次按键动作的电平波形如下图。存在抖动现象, =其前后沿抖动时间一般在 5ms~10ms 之间= 。由于单片机运行速 度非常快,刚按下的时候会检测到低电平判断按键被按下。但是由于按键存在抖动,单片机在此时也会检测到高电 平,误以为松开按键,紧接着又检测到低电 平,判断到按键被按下。周而复始,在 5-10ms 内可能会出现很多次按 下的动作,每一次按键的动作判断的次数都不相同。
这种抖动可能会影响程序误判,造成严重后果,一般我们采用两种方式对按键进行消抖: 1. 硬件消抖,硬件消抖的典型做法是:采用 R-S 触发器或 RC 积分电路。 2. 软件消抖,通常我们会使用软件延时 10ms 来消抖。
例如,当按键按下后,引脚为低电平;所以首先读取引脚电平,若引脚为低电平,则延时 10ms 后再次读取引脚电 平,若为低电平,则证明按键已按下。硬件方法一般用在对按键操作过程比较严格,且按键数量较少的场合,而按 键数量较多时,通常采用软件消抖。
值得一提的是,对于复杂且多任务的单片机系统来说,若简单地采用 =循环指令来实现软件延时= ,则会浪费CPU宝 贵的时间资源,大大降低系统的实时性,所以,更好的做法是 =利用定时中断服务程序= 或利用标志位的方法来实现 软件消抖。
与输出不同的是,设置输入引脚时,我们需要配置上拉或下拉电阻,目的是确定某个状态电路中的高电平或低电平。 上、下拉电阻的作用是提高电路稳定性,避免引起误动作。按键如果不通过电阻上拉到高电平,那么在上电瞬间可 能就发生误动作,因为在上电瞬间单片机的引脚电平是不确定的,上拉电阻的存在保证了其引脚处于高电平状态, 而不会发生误动作。
import time
from machine import Pin
# 创建按键输入引脚类,如果引脚的一端接 Vcc,则设置下拉电阻;如果一端接的是 GND,则配置上拉电阻。
pin_button = Pin(14, Pin.IN, Pin.PULL_DOWN)
# 定义 LED 输出引脚
pin_led = Pin(2, Pin.OUT)
# 判断 LED 的状态是否改变过
status = 0
while True:
# 按键消抖
if pin_button.value() == 1:
# 睡眠 10ms,如果依然为高电平,说明抖动已消失。
time.sleep_ms(10) # 高效的实现方式是使用定时器中断!这样,CPU 可以干其他事
# 延时 10ms 后,如果依然为高电平,并且 LED 的状态没有改变
if pin_button.value() == 1 and status == 0:
pin_led.value(not pin_led.value())
# led 的状态发生了变化,即使我持续按着按键,LED 的状态也不应该改变。
status = 1
# 按键松开,记录 LED 状态的变量也需要响应的改变。
elif pin_button.value() == 0:
status = 0
硬件中断例子:
# https://docs.geeksman.com/esp32/MicroPython/15.esp32-micropython-interrupt.html
import time
from machine import Pin
button = Pin(14, Pin.IN, Pin.PULL_DOWN)
led = Pin(2, Pin.OUT)
# 定义 button 的外部中断函数
def button_irq(button):
time.sleep_ms(80)
if button.value() == 1:
led.value(not led.value())
button.irq(button_irq, Pin.IRQ_RISING)
定时器中断:
# https://docs.geeksman.com/esp32/MicroPython/16.esp32-micropython-timer.html
import time
from machine import Pin, Timer
# 定义 Pin 控制引脚
led_1 = Pin(2, Pin.OUT)
led_2 = Pin(4, Pin.OUT)
# 定义定时器中断的回调函数
def timer_irq(timer_pin):
led_1.value(not led_1.value())
# 定义定时器
timer = Timer(0)
# 初始化定时器
timer.init(period=500, mode=Timer.PERIODIC, callback=timer_irq)
while True:
led_2.value(not led_2.value())
time.sleep(1)
Being a hardware sort of bloke I would simply put a capacitor across the push button. It’s not too critical but a 0.1uF usually does the trick. That way you solve the problem at source.
- How to de-bounce a switch using CMOS & TTL :http://www.all-electric.com/schematic/debounce.htm
I think this solution would depend on what your entire circuit looks like. Without a pre-specified R part of the =RC filter=, the timescale of your capacitor’s discharge (e.g. microseconds) could potentially be much shorter than the timescale over which the button bounces (e.g. milliseconds). Just something to watch out for.
#define BOUNCE_DURATION 20 // define an appropriate bounce time in ms for your switches
volatile unsigned long bounceTime=0; // variable to hold ms count to debounce a pressed switch
void intHandler(){
// this is the interrupt handler for button presses
// it ignores presses that occur in intervals less then the bounce time
if (abs(millis() - bounceTime) > BOUNCE_DURATION)
{
// Your code here to handle new button press ?
bounceTime = millis(); // set whatever bounce time in ms is appropriate
}
}
中断处理按键:https://docs.esp-rs.org/std-training/04_4_1_interrupts.html
In this exercise we are using =notifications=, which only give =the latest value=, so if the interrupt is triggered multiple times before the value of the notification is read, you will only be able to read the latest one. Queues, on the other hand, allow receiving multiple values. See =esp_idf_hal::task::queue::Queue= for more details.
使用 queue 的例子:https://blog.csdn.net/xh870189248/article/details/80524714
esp-iot-solution 提供的 button componet: https://github.com/espressif/esp-iot-solution/tree/master/components/button
- 对应的文档:https://docs.espressif.com/projects/espressif-esp-iot-solution/zh_CN/latest/input_device/button.html
使用 5ms 扫描周期的定时器任务实现的:
- 5ms 定时器任务的 callback 函数中执行 debound 和按键判断;
iot_button_create:
- button_create_com
- esp_timer_start_periodic
// https://github.com/espressif/esp-iot-solution/blob/master/components/button/iot_button.c#L397
button_handle_t iot_button_create(const button_config_t *config)
{
ESP_LOGI(TAG, "IoT Button Version: %d.%d.%d", BUTTON_VER_MAJOR, BUTTON_VER_MINOR, BUTTON_VER_PATCH);
BTN_CHECK(config, "Invalid button config", NULL);
esp_err_t ret = ESP_OK;
button_dev_t *btn = NULL;
uint16_t long_press_time = 0;
uint16_t short_press_time = 0;
long_press_time = TIME_TO_TICKS(config->long_press_time, LONG_TICKS);
short_press_time = TIME_TO_TICKS(config->short_press_time, SHORT_TICKS);
switch (config->type) {
case BUTTON_TYPE_GPIO: {
const button_gpio_config_t *cfg = &(config->gpio_button_config);
ret = button_gpio_init(cfg);
BTN_CHECK(ESP_OK == ret, "gpio button init failed", NULL);
// 创建一个 Button,底层是创建一个周期触发的 esp_timer task 并指定了 callback 函数 button_cb
btn = button_create_com(cfg->active_level, button_gpio_get_key_level, (void *)cfg->gpio_num, long_press_time, short_press_time);
#if CONFIG_GPIO_BUTTON_SUPPORT_POWER_SAVE
if (cfg->enable_power_save) {
btn->enable_power_save = cfg->enable_power_save;
// 如果是 Power Save 模式,则为 GPIO 引脚启用中断 和 handler:button_power_save_isr_handler
button_gpio_set_intr(cfg->gpio_num, cfg->active_level == 0 ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL, button_power_save_isr_handler, (void *)cfg->gpio_num);
}
#endif
} break;
// ...
btn->type = config->type;
if (!btn->enable_power_save) {
// 启用 esp_itmer 周期定时器,(扫描)周期为 TICKS_INTERVAL,他来自于配置项 BUTTON_PERIOD_TIME_MS
// https://github.com/espressif/esp-iot-solution/blob/master/components/button/Kconfig
// 默认 5ms,这个值要小于硬件抖动的周期,因为后续按它的倍数来进行去抖计数。
esp_timer_start_periodic(g_button_timer_handle, TICKS_INTERVAL * 1000U);
g_is_timer_running = true;
}
return (button_handle_t)btn;
}
// Power Save ISR 也是通过启动周期 esp_timer 定时器任务
#if CONFIG_GPIO_BUTTON_SUPPORT_POWER_SAVE
static void IRAM_ATTR button_power_save_isr_handler(void* arg)
{
if (!g_is_timer_running) {
esp_timer_start_periodic(g_button_timer_handle, TICKS_INTERVAL * 1000U);
g_is_timer_running = true;
}
button_gpio_intr_control((int)arg, false);
}
#endif
button_create_com: 创建一个 定时器任务 button_timer, 传入 callback button_cb:
- esp-idf 通过 FreeRTOS 运行一个 high-priority esp_timer task 线程,它被 ISR 周期唤醒,然后执行 callback;
- 另外,也可以定义 esp_timer 使用 ISR 中断来执行 callback;参考: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html#callback-dispatch-methods
static button_dev_t *button_create_com(uint8_t active_level, uint8_t (*hal_get_key_state)(void *hardware_data), void *hardware_data, uint16_t long_press_ticks, uint16_t short_press_ticks)
{
BTN_CHECK(NULL != hal_get_key_state, "Function pointer is invalid", NULL);
button_dev_t *btn = (button_dev_t *) calloc(1, sizeof(button_dev_t));
// ...
if (!g_button_timer_handle) {
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html#structures
esp_timer_create_args_t button_timer = {0};
button_timer.arg = NULL;
button_timer.callback = button_cb; // 回调函数
button_timer.dispatch_method = ESP_TIMER_TASK; // Dispatch callback from task or ISR,这里使用 task
button_timer.name = "button_timer";
esp_timer_create(&button_timer, &g_button_timer_handle); // 只是创建,并没有指定时间,后续 esp_timer_start_* 来指定
}
return btn;
}
esp_timer 定时器任务执行的 callback 函数:button_cb -> button_handler
// https://github.com/espressif/esp-iot-solution/blob/master/components/button/iot_button.c#L293C1-L320C2
static void button_cb(void *args)
{
button_dev_t *target;
/*!< When all buttons enter the BUTTON_NONE_PRESS state, the system enters low-power mode */
#if CONFIG_GPIO_BUTTON_SUPPORT_POWER_SAVE
bool enter_power_save_flag = true;
#endif
for (target = g_head_handle; target; target = target->next) {
button_handler(target); // 核心:执行 button_handler 函数,进行按键处理
#if CONFIG_GPIO_BUTTON_SUPPORT_POWER_SAVE
if (!(target->enable_power_save && target->debounce_cnt == 0 && target->event == BUTTON_NONE_PRESS)) {
enter_power_save_flag = false;
}
#endif
}
#if CONFIG_GPIO_BUTTON_SUPPORT_POWER_SAVE
if (enter_power_save_flag) {
/*!< Stop esp timer for power save */
esp_timer_stop(g_button_timer_handle);
g_is_timer_running = false;
for (target = g_head_handle; target; target = target->next) {
if (target->type == BUTTON_TYPE_GPIO && target->enable_power_save) {
button_gpio_intr_control((int)(target->hardware_data), true);
}
}
}
#endif
}
button_handler:Button driver core function, driver state machine.
- 按键驱动的核心函数,使用状态机来进行按键去抖、用户既按键事件生成和调用用户注册的按键事件回调函数。
- DEBOUNCE_TICKS 来源于配置 CONFIG_BUTTON_DEBOUNCE_TICKS,表示 BUTTON_PERIOD_TIME_MS(默认 5ms) 的次 数,默认为 2 即去抖时间为 10ms。
// https://github.com/espressif/esp-iot-solution/blob/master/components/button/iot_button.c#L293C1-L320C2
/**
,* @brief Button driver core function, driver state machine.
,*/
static void button_handler(button_dev_t *btn)
{
// 读取 GPIO 引脚的电平
uint8_t read_gpio_level = btn->hal_button_Level(btn->hardware_data);
/** ticks counter working.. */
if ((btn->state) > 0) { // btn->state 初始值为 3
btn->ticks++;
}
/**< button debounce handle */
// 记录引脚电平发生变化的 持续 次数,默认为 2,对应连续 10ms 处于按下的电平
if (read_gpio_level != btn->button_level) {
if (++(btn->debounce_cnt) >= DEBOUNCE_TICKS) {
btn->button_level = read_gpio_level;
btn->debounce_cnt = 0;
}
} else {
// 如果引脚电平没有发生变化,或者正好位于抖动的高电平,则去抖计数归零,
// 总的效果是:
// 1. 引脚电平发生变化,连续两次检测都是低电平,则使用该引脚电平;
// 2. 第一次引脚电平发生变化,但是第二次检测到没有变化(如正好处于抖动峰值),这时抖动计数归零,从头开始计数。
// 3. 按键抖动结束后,持续稳定的低电平持续时间一般是 180ms 左右,所以只要按下肯定有机会被识别。
btn->debounce_cnt = 0;
}
/** State machine */
switch (btn->state) {
case 0:
// 按键按下
if (btn->button_level == btn->active_level) {
btn->event = (uint8_t)BUTTON_PRESS_DOWN;
CALL_EVENT_CB(BUTTON_PRESS_DOWN);
btn->ticks = 0;
btn->repeat = 1;
btn->state = 1;
} else {
btn->event = (uint8_t)BUTTON_NONE_PRESS;
}
break;
case 1:
// 按键松开
if (btn->button_level != btn->active_level) {
btn->event = (uint8_t)BUTTON_PRESS_UP;
CALL_EVENT_CB(BUTTON_PRESS_UP);
btn->ticks = 0;
btn->state = 2;
} else if (btn->ticks > btn->long_press_ticks) {
// 按键按下,且 ticks 大于 long_press_ticks,表示长按。
btn->event = (uint8_t)BUTTON_LONG_PRESS_START;
btn->state = 4;
/** Calling callbacks for BUTTON_LONG_PRESS_START */
uint16_t ticks_time = iot_button_get_ticks_time(btn);
if (btn->cb_info[btn->event] && btn->count[0] == 0) {
if (abs(ticks_time - (btn->long_press_ticks * TICKS_INTERVAL)) <= TOLERANCE && btn->cb_info[btn->event][btn->count[0]].event_data.long_press.press_time == (btn->long_press_ticks * TICKS_INTERVAL)) {
do {
btn->cb_info[btn->event][btn->count[0]].cb(btn, btn->cb_info[btn->event][btn->count[0]].usr_data);
btn->count[0]++;
if (btn->count[0] >= btn->size[btn->event]) {
break;
}
} while (btn->cb_info[btn->event][btn->count[0]].event_data.long_press.press_time == btn->long_press_ticks * TICKS_INTERVAL);
}
}
}
break;
case 2:
// 按键被按下,状态 state 切换到 3,ticks 清零
if (btn->button_level == btn->active_level) {
btn->event = (uint8_t)BUTTON_PRESS_DOWN;
CALL_EVENT_CB(BUTTON_PRESS_DOWN);
btn->event = (uint8_t)BUTTON_PRESS_REPEAT;
btn->repeat++;
CALL_EVENT_CB(BUTTON_PRESS_REPEAT); // repeat hit
btn->ticks = 0;
btn->state = 3;
} else if (btn->ticks > btn->short_press_ticks) {
// 按键松开且 ticks 大于 press_ticks, 则状态 state 切换到 0,
// 否则 state 一直处于 3
if (btn->repeat == 1) {
btn->event = (uint8_t)BUTTON_SINGLE_CLICK;
CALL_EVENT_CB(BUTTON_SINGLE_CLICK);
} else if (btn->repeat == 2) {
btn->event = (uint8_t)BUTTON_DOUBLE_CLICK;
CALL_EVENT_CB(BUTTON_DOUBLE_CLICK); // repeat hit
}
btn->event = (uint8_t)BUTTON_MULTIPLE_CLICK;
/** Calling the callbacks for MULTIPLE BUTTON CLICKS */
for (int i = 0; i < btn->size[btn->event]; i++) {
if (btn->repeat == btn->cb_info[btn->event][i].event_data.multiple_clicks.clicks) {
do {
btn->cb_info[btn->event][i].cb(btn, btn->cb_info[btn->event][i].usr_data);
i++;
if (i >= btn->size[btn->event]) {
break;
}
} while (btn->cb_info[btn->event][i].event_data.multiple_clicks.clicks == btn->repeat);
}
}
btn->event = (uint8_t)BUTTON_PRESS_REPEAT_DONE;
CALL_EVENT_CB(BUTTON_PRESS_REPEAT_DONE); // repeat hit
btn->repeat = 0;
btn->state = 0;
}
break;
case 3:
// btn->button_level 是从引脚读取的电平值,和有效电平不一致时,表示按键处于 release 状态
// 故调用 BUTTON_PRESS_UP 回调。
if (btn->button_level != btn->active_level) {
btn->event = (uint8_t)BUTTON_PRESS_UP;
CALL_EVENT_CB(BUTTON_PRESS_UP);
// SHORT_TICKS 的值是 CONFIG_BUTTON_SHORT_PRESS_TIME_MS /TICKS_INTERVAL,180/5 = 36
if (btn->ticks < SHORT_TICKS) {
btn->ticks = 0;
btn->state = 2; //repeat press
} else {
btn->state = 0;
}
}
break;
case 4:
// 一直按下
if (btn->button_level == btn->active_level) {
//continue hold trigger
if (btn->ticks >= (btn->long_press_hold_cnt + 1) * SERIAL_TICKS + btn->long_press_ticks) {
btn->event = (uint8_t)BUTTON_LONG_PRESS_HOLD;
btn->long_press_hold_cnt++;
CALL_EVENT_CB(BUTTON_LONG_PRESS_HOLD);
/** Calling callbacks for BUTTON_LONG_PRESS_START based on press_time */
uint16_t ticks_time = iot_button_get_ticks_time(btn);
if (btn->cb_info[BUTTON_LONG_PRESS_START]) {
button_cb_info_t *cb_info = btn->cb_info[BUTTON_LONG_PRESS_START];
uint16_t time = cb_info[btn->count[0]].event_data.long_press.press_time;
if (btn->long_press_ticks * TICKS_INTERVAL > time) {
for (int i = btn->count[0] + 1; i < btn->size[BUTTON_LONG_PRESS_START]; i++) {
time = cb_info[i].event_data.long_press.press_time;
if (btn->long_press_ticks * TICKS_INTERVAL <= time) {
btn->count[0] = i;
break;
}
}
}
if (btn->count[0] < btn->size[BUTTON_LONG_PRESS_START] && abs(ticks_time - time) <= TOLERANCE) {
do {
cb_info[btn->count[0]].cb(btn, cb_info[btn->count[0]].usr_data);
btn->count[0]++;
if (btn->count[0] >= btn->size[BUTTON_LONG_PRESS_START]) {
break;
}
} while (time == cb_info[btn->count[0]].event_data.long_press.press_time);
}
}
/** Updating counter for BUTTON_LONG_PRESS_UP press_time */
if (btn->cb_info[BUTTON_LONG_PRESS_UP]) {
button_cb_info_t *cb_info = btn->cb_info[BUTTON_LONG_PRESS_UP];
uint16_t time = cb_info[btn->count[1] + 1].event_data.long_press.press_time;
if (btn->long_press_ticks * TICKS_INTERVAL > time) {
for (int i = btn->count[1] + 1; i < btn->size[BUTTON_LONG_PRESS_UP]; i++) {
time = cb_info[i].event_data.long_press.press_time;
if (btn->long_press_ticks * TICKS_INTERVAL <= time) {
btn->count[1] = i;
break;
}
}
}
if (btn->count[1] + 1 < btn->size[BUTTON_LONG_PRESS_UP] && abs(ticks_time - time) <= TOLERANCE) {
do {
btn->count[1]++;
if (btn->count[1] + 1 >= btn->size[BUTTON_LONG_PRESS_UP]) {
break;
}
} while (time == cb_info[btn->count[1] + 1].event_data.long_press.press_time);
}
}
}
} else { //releasd
btn->event = BUTTON_LONG_PRESS_UP;
/** calling callbacks for BUTTON_LONG_PRESS_UP press_time */
if (btn->cb_info[btn->event] && btn->count[1] >= 0) {
button_cb_info_t *cb_info = btn->cb_info[btn->event];
do {
cb_info[btn->count[1]].cb(btn, cb_info[btn->count[1]].usr_data);
if (!btn->count[1]) {
break;
}
btn->count[1]--;
} while (cb_info[btn->count[1]].event_data.long_press.press_time == cb_info[btn->count[1] + 1].event_data.long_press.press_time);
/** Reset the counter */
btn->count[1] = -1;
}
/** Reset counter */
if (btn->cb_info[BUTTON_LONG_PRESS_START]) {
btn->count[0] = 0;
}
btn->event = (uint8_t)BUTTON_PRESS_UP;
CALL_EVENT_CB(BUTTON_PRESS_UP);
btn->state = 0; //reset
btn->long_press_hold_cnt = 0;
}
break;
}
}