Skip to main content

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:

  1. GPIO interrupt fires on falling edge (press)
  2. ISR sends the GPIO number to a raw event queue
  3. A FreeRTOS task reads from the raw queue, arms a 50 ms one-shot timer
  4. When the timer fires, read the current pin state
  5. If still LOW (still held down): emit a confirmed BTN_EVENT_PRESS
  6. 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_PRESS instead of BTN_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.