Button Driver
The button driver converts raw GPIO transitions into debounced logical button events and delivers them to the main task via a FreeRTOS queue.
Events
// components/buttons/include/buttons.h
#pragma once
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
typedef enum {
BTN_UP = 0,
BTN_DOWN,
BTN_SELECT,
BTN_BACK,
BTN_SNOOZE,
BTN_DISMISS,
BTN_COUNT
} button_id_t;
typedef enum {
BTN_EVENT_PRESS,
BTN_EVENT_LONG_PRESS, // held for > 2 seconds
} button_event_type_t;
typedef struct {
button_id_t id;
button_event_type_t type;
} button_event_t;
extern QueueHandle_t button_event_queue;
esp_err_t buttons_init(void);
GPIO Mapping
static const gpio_num_t button_gpio[BTN_COUNT] = {
[BTN_UP] = GPIO_NUM_6,
[BTN_DOWN] = GPIO_NUM_7,
[BTN_SELECT] = GPIO_NUM_8,
[BTN_BACK] = GPIO_NUM_9,
[BTN_SNOOZE] = GPIO_NUM_38,
[BTN_DISMISS] = GPIO_NUM_39,
};
Debounce Strategy
Mechanical buttons bounce for 5–50 ms when pressed. The debounce strategy:
- GPIO interrupt fires on falling edge (press)
- ISR sends the GPIO number to a raw event queue
- A FreeRTOS task reads from the raw queue, arms a 50 ms one-shot timer
- When the timer fires, read the current pin state
- If still LOW (still held down): emit a confirmed
BTN_EVENT_PRESS - If HIGH (noise / very fast release): discard
Long-press detection:
- At confirmed press time, note
esp_timer_get_time() - On rising edge (release), compute elapsed time
- If > 2000 ms: emit
BTN_EVENT_LONG_PRESSinstead ofBTN_EVENT_PRESS
Implementation
#include "driver/gpio.h"
#include "freertos/timers.h"
#include "esp_timer.h"
QueueHandle_t button_event_queue;
static QueueHandle_t raw_queue;
static int64_t press_time_us[BTN_COUNT];
static void IRAM_ATTR gpio_isr(void *arg) {
uint32_t gpio = (uint32_t)arg;
xQueueSendFromISR(raw_queue, &gpio, NULL);
}
static void debounce_task(void *arg) {
uint32_t gpio;
for (;;) {
if (!xQueueReceive(raw_queue, &gpio, portMAX_DELAY)) continue;
vTaskDelay(pdMS_TO_TICKS(50)); // debounce window
// Find which button this GPIO belongs to
button_id_t id = BTN_COUNT;
for (int i = 0; i < BTN_COUNT; i++) {
if (button_gpio[i] == (gpio_num_t)gpio) { id = i; break; }
}
if (id == BTN_COUNT) continue;
if (gpio_get_level((gpio_num_t)gpio) == 0) {
// Still pressed after debounce — confirmed press
press_time_us[id] = esp_timer_get_time();
} else {
// Released — determine press vs long-press
int64_t duration_us = esp_timer_get_time() - press_time_us[id];
button_event_t evt = {
.id = id,
.type = (duration_us > 2000000)
? BTN_EVENT_LONG_PRESS
: BTN_EVENT_PRESS,
};
xQueueSend(button_event_queue, &evt, 0);
}
}
}
esp_err_t buttons_init(void) {
button_event_queue = xQueueCreate(10, sizeof(button_event_t));
raw_queue = xQueueCreate(20, sizeof(uint32_t));
gpio_install_isr_service(0);
for (int i = 0; i < BTN_COUNT; i++) {
gpio_config_t cfg = {
.pin_bit_mask = (1ULL << button_gpio[i]),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_ANYEDGE, // both press and release
};
gpio_config(&cfg);
gpio_isr_handler_add(button_gpio[i], gpio_isr,
(void *)(uint32_t)button_gpio[i]);
}
xTaskCreate(debounce_task, "btn_debounce", 2048, NULL, 5, NULL);
return ESP_OK;
}
Consuming Events in Main
button_event_t evt;
if (xQueueReceive(button_event_queue, &evt, 0) == pdTRUE) {
state_machine_handle_button(current_state, evt);
}
The state machine receives the event and decides what to do based on the current state. See the main loop doc for the full dispatch table.