button: HW timer-based driver for GPIO buttons

The driver debounces the contact, detects short and long presses and can repeat a click while the button is held. Every button is an opaque handle with its own timer, so several buttons work independently.

Events

Event

When

BUTTON_PRESSED

The button is pressed

BUTTON_RELEASED

The button is released

BUTTON_CLICKED

A short press ends, or an autorepeat step

BUTTON_PRESSED_LONG

The button is held longer than long_press_time_us

Without autorepeat a short press produces PRESSED, RELEASED, CLICKED, and a long press produces PRESSED, PRESSED_LONG, RELEASED. With autorepeat the long press is disabled and holding the button produces PRESSED, a series of CLICKED, then RELEASED.

Modes

BUTTON_MODE_POLL starts a periodic timer and reads the pin every poll_interval_us microseconds. It needs no interrupt service and works with any pin, but it wakes the CPU on every period even when nobody touches the button.

BUTTON_MODE_INTERRUPT waits for an edge on the pin; the interrupt then disables itself and starts the same timer, which confirms the press and follows it until the release. The interrupt service must be installed with gpio_install_isr_service() before the first such button is created. A button that is already held at boot is not detected until it is released and pressed again.

A pin can serve one button at a time, in either mode: the GPIO driver keeps a single interrupt handler per pin, so a second button on the same pin would take the handler over and the first one would stop seeing presses. The second button_create() fails with ESP_ERR_INVALID_STATE, and the pin becomes free again after button_delete().

Debouncing

The samples taken during dead_time_us after a press are ignored, so the contact bounce of the press does not produce extra events. The bounce of the release is covered by the same window when it is shorter than the dead time; otherwise increase dead_time_us.

Callback

The event callback runs in the ESP timer task while the button mutex is held. It must be short: a delay in it delays every other ESP timer in the system. It also must not call the driver back, because the mutex is not recursive: deleting the button from the callback deadlocks.

Reference

Defines

BUTTON_DEFAULT_CONFIG()

Typedefs

typedef struct button *button_handle_t

Opaque button handle.

typedef void (*button_event_cb_t)(const button_event_t *event, void *ctx)

Callback prototype.

The callback is invoked from the ESP timer task context, while the internal mutex of the button is held.

It must not block, and it must not call button_delete() or button_create(): the mutex is not recursive, so such a call deadlocks. Any delay in the callback also delays every other ESP timer in the system, because they share one task.

Param event:

Button event

Param ctx:

User context

Enums

enum button_mode_t

Button detection mode.

Values:

enumerator BUTTON_MODE_POLL

Periodic timer polling (default)

enumerator BUTTON_MODE_INTERRUPT

GPIO interrupt with debounce timer.

enum button_state_t

Button states/events.

When autorepeat is disabled, the event sequence for a short press is: PRESSED -> RELEASED -> CLICKED and for a long press: PRESSED -> PRESSED_LONG -> RELEASED

When autorepeat is enabled, long press detection is disabled. Instead, holding the button generates repeated CLICKED events: PRESSED -> CLICKED -> CLICKED -> … -> RELEASED

Values:

enumerator BUTTON_PRESSED
enumerator BUTTON_RELEASED
enumerator BUTTON_CLICKED
enumerator BUTTON_PRESSED_LONG

Functions

esp_err_t button_create(const button_config_t *config, button_handle_t *handle)

Create a new button.

The configuration is validated before the GPIO is touched. On failure handle is set to NULL and the pin is left as it was found.

One pin can serve one button at a time: the GPIO driver stores a single interrupt handler per pin, so a second button on the same pin would take the handler over and the first one would stop seeing presses. A second attempt on a pin that is already taken fails with ESP_ERR_INVALID_STATE; the pin becomes free again after button_delete().

When using BUTTON_MODE_INTERRUPT, the GPIO ISR service must be installed by the caller before creating any interrupt-mode buttons (via gpio_install_isr_service()), otherwise the call fails with ESP_ERR_INVALID_STATE.

Parameters:
  • config – Button configuration

  • handle – [out] Button handle, set to NULL on failure

Returns:

  • ESP_OK on success

  • ESP_ERR_INVALID_ARG if the configuration is invalid

  • ESP_ERR_NO_MEM if the handle or the timer cannot be allocated

  • ESP_ERR_INVALID_STATE if the GPIO ISR service is not installed or the pin already serves another button

esp_err_t button_delete(button_handle_t handle)

Delete a button.

The interrupt handler is removed, the timer is stopped and the pin is released back to a floating input, so the button no longer consumes the GPIO and another button may use the pin.

On ESP-IDF 6.1 and later the function waits for a timer callback that has already been dispatched. On older versions esp_timer_stop() cannot do that: the callback may still be running while the handle is freed, so the button must not be deleted concurrently with its own events. Either delete the button from a task that is not the callback, or make sure that no event can arrive at that moment (for example, by disabling the button beforehand).

Parameters:

handle – Button handle

Returns:

  • ESP_OK on success

  • ESP_ERR_INVALID_ARG if handle is NULL

struct button_event_t
#include <button.h>

Button event.

Public Members

button_state_t type

Event type.

button_handle_t sender

Button handle.

struct button_config_t
#include <button.h>

Button configuration.

button_create() validates the fields: the GPIO must exist, the mode must be one of BUTTON_MODE_POLL or BUTTON_MODE_INTERRUPT, pressed_level must be 0 or 1, poll_interval_us must be non-zero and, when autorepeat is enabled, autorepeat_interval_us must be non-zero as well.

Public Members

gpio_num_t gpio

GPIO pin number.

button_mode_t mode

Detection mode (poll or interrupt)

uint8_t pressed_level

Logic level when button is pressed (0 or 1)

bool enable_internal_pull

Enable internal pull-up/pull-down resistor.

bool autorepeat

Enable autorepeat (mutually exclusive with long press)

uint32_t dead_time_us

Debounce time: samples taken right after a press are ignored (microseconds)

uint32_t long_press_time_us

Long press threshold in microseconds (ignored when autorepeat is enabled)

uint32_t autorepeat_timeout_us

Time before autorepeat starts, in microseconds.

uint32_t autorepeat_interval_us

Autorepeat interval in microseconds.

uint32_t poll_interval_us

Period of the internal timer and the unit of the timings, in microseconds.

button_event_cb_t callback

Event callback (required)

void *callback_ctx

User context passed to callback.