From 25de2de2b12c934a9bf4ef62d41ed87753ec7f62 Mon Sep 17 00:00:00 2001 From: Chris Leishman Date: Fri, 13 Mar 2026 14:15:24 -0700 Subject: [PATCH] Move to per-button opaque handles with individual timers API changed from static array + shared timer to per-button opaque handles (button_create/button_delete). Adds interrupt mode with GPIO ISR and debounce timer. Bumps version to 2.0.0. --- .eil.yml | 2 +- Kconfig | 14 +- button.c | 245 +++++++++++++++++++++++------------ button.h | 102 +++++++++++---- examples/default/main/main.c | 41 +++--- idf_component.yml | 2 +- 6 files changed, 269 insertions(+), 137 deletions(-) diff --git a/.eil.yml b/.eil.yml index 6614b17..08145e4 100644 --- a/.eil.yml +++ b/.eil.yml @@ -1,7 +1,7 @@ --- name: button description: HW timer-based driver for GPIO buttons -version: 1.1.0 +version: 2.0.0 groups: - input code_owners: diff --git a/Kconfig b/Kconfig index c3fc8b7..66441ec 100644 --- a/Kconfig +++ b/Kconfig @@ -1,15 +1,15 @@ menu "Button" - config BUTTON_MAX - int "Maximum number of buttons" - range 1 10 - default 5 - config BUTTON_POLL_TIMEOUT int "Poll timeout, ms" range 1 1000 default 10 + config BUTTON_DEAD_TIME + int "Dead time, ms" + range 10 1000 + default 50 + config BUTTON_LONG_PRESS_TIMEOUT int "Timeout of long press, ms" range 100 10000 @@ -24,5 +24,5 @@ menu "Button" int "Autorepeat interval, ms" range 100 10000 default 250 - -endmenu \ No newline at end of file + +endmenu diff --git a/button.c b/button.c index 9019266..12c6a98 100644 --- a/button.c +++ b/button.c @@ -33,151 +33,230 @@ * MIT Licensed as described in the file LICENSE */ #include "button.h" +#include #include +#include +#include -#define DEAD_TIME_US 50000 // 50ms -#define POLL_TIMEOUT_US (CONFIG_BUTTON_POLL_TIMEOUT * 1000) -#define AUTOREPEAT_TIMEOUT_US (CONFIG_BUTTON_AUTOREPEAT_TIMEOUT * 1000) -#define AUTOREPEAT_INTERVAL_US (CONFIG_BUTTON_AUTOREPEAT_INTERVAL * 1000) -#define LONG_PRESS_TIMEOUT_US (CONFIG_BUTTON_LONG_PRESS_TIMEOUT * 1000) +#define CHECK_ARG(VAL) do { if (!(VAL)) return ESP_ERR_INVALID_ARG; } while (0) -static button_t *buttons[CONFIG_BUTTON_MAX] = { NULL }; -static esp_timer_handle_t timer = NULL; +struct button +{ + button_config_t config; + esp_timer_handle_t timer; + SemaphoreHandle_t lock; + button_state_t state; + uint32_t pressed_time; + uint32_t repeating_time; + bool monitoring; // interrupt mode: true when esp_timer periodic is running +}; -#define CHECK(x) do { esp_err_t __; if ((__ = x) != ESP_OK) return __; } while (0) -#define CHECK_ARG(VAL) do { if (!(VAL)) return ESP_ERR_INVALID_ARG; } while (0) +static inline void emit_event(button_handle_t btn, button_state_t type) +{ + button_event_t ev = { .type = type, .sender = btn }; + btn->config.callback(&ev, btn->config.callback_ctx); +} -static void poll_button(button_t *btn) +static void poll_button(button_handle_t btn) { - if (btn->internal.state == BUTTON_PRESSED && btn->internal.pressed_time < DEAD_TIME_US) + if (btn->state == BUTTON_PRESSED && btn->pressed_time < btn->config.dead_time_us) { // Dead time, ignore all - btn->internal.pressed_time += POLL_TIMEOUT_US; + btn->pressed_time += btn->config.poll_interval_us; return; } - if (gpio_get_level(btn->gpio) == btn->pressed_level) + if (gpio_get_level(btn->config.gpio) == btn->config.pressed_level) { // button is pressed - if (btn->internal.state == BUTTON_RELEASED) + if (btn->state == BUTTON_RELEASED) { // pressing just started, reset pressing/repeating time and run callback - btn->internal.state = BUTTON_PRESSED; - btn->internal.pressed_time = 0; - btn->internal.repeating_time = 0; - btn->callback(btn, BUTTON_PRESSED); + btn->state = BUTTON_PRESSED; + btn->pressed_time = 0; + btn->repeating_time = 0; + emit_event(btn, BUTTON_PRESSED); return; } // increment pressing time - btn->internal.pressed_time += POLL_TIMEOUT_US; + btn->pressed_time += btn->config.poll_interval_us; // check autorepeat - if (btn->autorepeat) + if (btn->config.autorepeat) { // check autorepeat timeout - if (btn->internal.pressed_time < AUTOREPEAT_TIMEOUT_US) + if (btn->pressed_time < btn->config.autorepeat_timeout_us) return; // increment repeating time - btn->internal.repeating_time += POLL_TIMEOUT_US; + btn->repeating_time += btn->config.poll_interval_us; - if (btn->internal.repeating_time >= AUTOREPEAT_INTERVAL_US) + if (btn->repeating_time >= btn->config.autorepeat_interval_us) { // reset repeating time and run callback - btn->internal.repeating_time = 0; - btn->callback(btn, BUTTON_CLICKED); + btn->repeating_time = 0; + emit_event(btn, BUTTON_CLICKED); } return; } - if (btn->internal.state == BUTTON_PRESSED && btn->internal.pressed_time >= LONG_PRESS_TIMEOUT_US) + if (btn->state == BUTTON_PRESSED && btn->pressed_time >= btn->config.long_press_time_us) { - // button perssed long time, change state and run callback - btn->internal.state = BUTTON_PRESSED_LONG; - btn->callback(btn, BUTTON_PRESSED_LONG); + // button pressed long time, change state and run callback + btn->state = BUTTON_PRESSED_LONG; + emit_event(btn, BUTTON_PRESSED_LONG); } } - else if (btn->internal.state != BUTTON_RELEASED) + else if (btn->state != BUTTON_RELEASED) { // button released - bool clicked = btn->internal.state == BUTTON_PRESSED; - btn->internal.state = BUTTON_RELEASED; - btn->callback(btn, BUTTON_RELEASED); + bool clicked = btn->state == BUTTON_PRESSED + && !(btn->config.autorepeat && btn->pressed_time >= btn->config.autorepeat_timeout_us); + btn->state = BUTTON_RELEASED; + emit_event(btn, BUTTON_RELEASED); if (clicked) - btn->callback(btn, BUTTON_CLICKED); + { + emit_event(btn, BUTTON_CLICKED); + } } } -static void poll(void *arg) +static void button_timer_handler(void *arg) { - for (size_t i = 0; i < CONFIG_BUTTON_MAX; i++) - if (buttons[i] && buttons[i]->callback) - poll_button(buttons[i]); + button_handle_t btn = (button_handle_t)arg; + xSemaphoreTake(btn->lock, portMAX_DELAY); + + poll_button(btn); + + if (btn->config.mode == BUTTON_MODE_INTERRUPT) + { + if (btn->state == BUTTON_RELEASED) + { + // Button released (or false trigger): stop monitoring and re-enable interrupt + if (btn->monitoring) + { + esp_timer_stop(btn->timer); + btn->monitoring = false; + } + gpio_intr_enable(btn->config.gpio); + } + else if (!btn->monitoring) + { + // Debounce confirmed press: switch to periodic monitoring + esp_timer_start_periodic(btn->timer, btn->config.poll_interval_us); + btn->monitoring = true; + } + } + + xSemaphoreGive(btn->lock); } //////////////////////////////////////////////////////////////////////////////// +// Interrupt mode helpers -static const esp_timer_create_args_t timer_args = +static void IRAM_ATTR button_isr_handler(void *arg) { - .arg = NULL, - .name = "poll_buttons", - .dispatch_method = ESP_TIMER_TASK, - .callback = poll, -}; + button_handle_t btn = (button_handle_t)arg; + gpio_intr_disable(btn->config.gpio); + esp_timer_start_once(btn->timer, 0); +} + +//////////////////////////////////////////////////////////////////////////////// -esp_err_t button_init(button_t *btn) +esp_err_t button_create(const button_config_t *config, button_handle_t *handle) { - CHECK_ARG(btn); + CHECK_ARG(handle && config && config->callback); - if (!timer) - CHECK(esp_timer_create(&timer_args, &timer)); + esp_err_t err; - esp_timer_stop(timer); + button_handle_t btn = calloc(1, sizeof(struct button)); + if (!btn) + return ESP_ERR_NO_MEM; - esp_err_t res = ESP_ERR_NO_MEM; + btn->config = *config; + btn->state = BUTTON_RELEASED; - for (size_t i = 0; i < CONFIG_BUTTON_MAX; i++) + btn->lock = xSemaphoreCreateMutex(); + if (!btn->lock) { - if (buttons[i] == btn) - break; + err = ESP_ERR_NO_MEM; + goto fail; + } - if (!buttons[i]) - { - btn->internal.state = BUTTON_RELEASED; - btn->internal.pressed_time = 0; - btn->internal.repeating_time = 0; - res = gpio_set_direction(btn->gpio, GPIO_MODE_INPUT); - if (res != ESP_OK) break; - if (btn->internal_pull) - { - res = gpio_set_pull_mode(btn->gpio, btn->pressed_level ? GPIO_PULLDOWN_ONLY : GPIO_PULLUP_ONLY); - if (res != ESP_OK) break; - } - buttons[i] = btn; - break; - } + err = gpio_set_direction(btn->config.gpio, GPIO_MODE_INPUT); + if (err != ESP_OK) + goto fail_lock; + + if (btn->config.enable_internal_pull) + { + err = gpio_set_pull_mode(btn->config.gpio, btn->config.pressed_level ? GPIO_PULLDOWN_ONLY : GPIO_PULLUP_ONLY); + if (err != ESP_OK) + goto fail_lock; } - CHECK(esp_timer_start_periodic(timer, POLL_TIMEOUT_US)); - return res; + const esp_timer_create_args_t timer_args = + { + .name = "__button__", + .arg = btn, + .callback = button_timer_handler, + .dispatch_method = ESP_TIMER_TASK + }; + + err = esp_timer_create(&timer_args, &btn->timer); + if (err != ESP_OK) + goto fail_lock; + + if (btn->config.mode == BUTTON_MODE_INTERRUPT) + { + err = gpio_set_intr_type(btn->config.gpio, GPIO_INTR_ANYEDGE); + if (err != ESP_OK) + goto fail_timer; + + err = gpio_isr_handler_add(btn->config.gpio, button_isr_handler, btn); + if (err != ESP_OK) + goto fail_timer; + } + else + { + // Poll mode: start periodic timer immediately + err = esp_timer_start_periodic(btn->timer, btn->config.poll_interval_us); + if (err != ESP_OK) + goto fail_timer; + } + + *handle = btn; + + return ESP_OK; + +fail_timer: + esp_timer_delete(btn->timer); +fail_lock: + vSemaphoreDelete(btn->lock); +fail: + free(btn); + return err; } -esp_err_t button_done(button_t *btn) +esp_err_t button_delete(button_handle_t handle) { - CHECK_ARG(btn); + CHECK_ARG(handle); - esp_timer_stop(timer); + if (handle->config.mode == BUTTON_MODE_INTERRUPT) + { + gpio_intr_disable(handle->config.gpio); + gpio_isr_handler_remove(handle->config.gpio); + } - esp_err_t res = ESP_ERR_INVALID_ARG; + esp_timer_stop(handle->timer); - for (size_t i = 0; i < CONFIG_BUTTON_MAX; i++) - if (buttons[i] == btn) - { - buttons[i] = NULL; - res = ESP_OK; - break; - } + // Drain any in-flight callback + xSemaphoreTake(handle->lock, portMAX_DELAY); + handle->monitoring = false; + xSemaphoreGive(handle->lock); + + esp_timer_delete(handle->timer); + vSemaphoreDelete(handle->lock); - CHECK(esp_timer_start_periodic(timer, POLL_TIMEOUT_US)); - return res; + free(handle); + return ESP_OK; } diff --git a/button.h b/button.h index 68c8109..c1c8f82 100644 --- a/button.h +++ b/button.h @@ -47,12 +47,30 @@ extern "C" { #endif /** - * Typedef of button descriptor + * Opaque button handle */ -typedef struct button_s button_t; +typedef struct button *button_handle_t; + +/** + * Button detection mode + */ +typedef enum +{ + BUTTON_MODE_POLL = 0, //!< Periodic timer polling (default) + BUTTON_MODE_INTERRUPT, //!< GPIO interrupt with debounce timer +} button_mode_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 */ typedef enum { @@ -62,48 +80,80 @@ typedef enum BUTTON_PRESSED_LONG, } button_state_t; +/** + * Button event + */ +typedef struct +{ + button_state_t type; //!< Event type + button_handle_t sender; //!< Button handle +} button_event_t; + /** * Callback prototype * - * @param btn Pointer to button descriptor - * @param state Button action (new state) + * The callback is always invoked from the ESP timer task context. + * It must not block or call back into the library. + * + * @param event Button event + * @param ctx User context */ -typedef void (*button_event_cb_t)(button_t *btn, button_state_t state); +typedef void (*button_event_cb_t)(const button_event_t *event, void *ctx); /** - * Button descriptor struct + * Button configuration */ -struct button_s +typedef struct { - gpio_num_t gpio; //!< GPIO - bool internal_pull; //!< Enable internal pull-up/pull-down - uint8_t pressed_level; //!< Logic level of pressed button - bool autorepeat; //!< Enable autorepeat - button_event_cb_t callback; //!< Button callback - void *ctx; //!< User data - struct - { - button_state_t state; - uint32_t pressed_time; - uint32_t repeating_time; - } internal; //!< Internal button state -}; + 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; //!< Dead time after press / debounce delay (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; //!< Polling / monitoring interval (microseconds) + button_event_cb_t callback; //!< Event callback (required) + void *callback_ctx; //!< User context passed to callback +} button_config_t; + +#define BUTTON_DEFAULT_CONFIG() { \ + .gpio = GPIO_NUM_NC, \ + .mode = BUTTON_MODE_POLL, \ + .pressed_level = 0, \ + .enable_internal_pull = true, \ + .autorepeat = false, \ + .dead_time_us = CONFIG_BUTTON_DEAD_TIME * 1000, \ + .long_press_time_us = CONFIG_BUTTON_LONG_PRESS_TIMEOUT * 1000, \ + .autorepeat_timeout_us = CONFIG_BUTTON_AUTOREPEAT_TIMEOUT * 1000, \ + .autorepeat_interval_us = CONFIG_BUTTON_AUTOREPEAT_INTERVAL * 1000, \ + .poll_interval_us = CONFIG_BUTTON_POLL_TIMEOUT * 1000, \ + .callback = NULL, \ + .callback_ctx = NULL, \ +} /** - * @brief Init button + * @brief Create a new button + * + * 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()`). * - * @param btn Pointer to button descriptor + * @param config Button configuration + * @param[out] handle Button handle, populated on success * @return `ESP_OK` on success */ -esp_err_t button_init(button_t *btn); +esp_err_t button_create(const button_config_t *config, button_handle_t *handle); /** - * @brief Deinit button + * @brief Delete a button * - * @param btn Pointer to button descriptor + * @param handle Button handle * @return `ESP_OK` on success */ -esp_err_t button_done(button_t *btn); +esp_err_t button_delete(button_handle_t handle); #ifdef __cplusplus } diff --git a/examples/default/main/main.c b/examples/default/main/main.c index a2407da..df9a650 100644 --- a/examples/default/main/main.c +++ b/examples/default/main/main.c @@ -1,6 +1,6 @@ #include +#include #include -#include #include static const char *TAG = "button_example"; @@ -13,32 +13,35 @@ static const char *states[] = [BUTTON_PRESSED_LONG] = "pressed long", }; -static button_t btn1, btn2; +static button_handle_t btn1, btn2; -static void on_button(button_t *btn, button_state_t state) +static void on_button(const button_event_t *event, void *ctx) { - ESP_LOGI(TAG, "%s button %s", btn == &btn1 ? "First" : "Second", states[state]); + ESP_LOGI(TAG, "%s button %s", event->sender == btn1 ? "First" : "Second", states[event->type]); } void app_main() { // First button connected between GPIO and GND - // pressed logic level 0, no autorepeat - btn1.gpio = CONFIG_EXAMPLE_BUTTON1_GPIO; - btn1.pressed_level = 0; - btn1.internal_pull = true; - btn1.autorepeat = false; - btn1.callback = on_button; + // pressed logic level 0, no autorepeat, interrupt mode + button_config_t config1 = BUTTON_DEFAULT_CONFIG(); + config1.gpio = CONFIG_EXAMPLE_BUTTON1_GPIO; + config1.mode = BUTTON_MODE_INTERRUPT; + config1.pressed_level = 0; + config1.autorepeat = false; + config1.callback = on_button; // Second button connected between GPIO and +3.3V // pressed logic level 1, autorepeat enabled - btn2.gpio = CONFIG_EXAMPLE_BUTTON2_GPIO; - btn2.pressed_level = 1; - btn2.internal_pull = true; - btn2.autorepeat = true; - btn2.callback = on_button; - - ESP_ERROR_CHECK(button_init(&btn1)); - ESP_ERROR_CHECK(button_init(&btn2)); -} + button_config_t config2 = BUTTON_DEFAULT_CONFIG(); + config2.gpio = CONFIG_EXAMPLE_BUTTON2_GPIO; + config2.pressed_level = 1; + config2.autorepeat = true; + config2.callback = on_button; + + // Install GPIO ISR service (required before creating interrupt-mode buttons) + ESP_ERROR_CHECK(gpio_install_isr_service(0)); + ESP_ERROR_CHECK(button_create(&config1, &btn1)); + ESP_ERROR_CHECK(button_create(&config2, &btn2)); +} diff --git a/idf_component.yml b/idf_component.yml index ae8662a..872e589 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,5 +1,5 @@ --- -version: 1.1.0 +version: 2.0.0 description: HW timer-based driver for GPIO buttons license: MIT targets: