#include "encoder.h" #include "driver/pulse_cnt.h" #include "esp_check.h" #include "esp_log.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "pcf8574.h" static const char *TAG = "encoder"; /* * Full quadrature decode counts 4 edges per mechanical detent on an EC28A. * PCNT counts in raw edges; we divide. */ #define COUNTS_PER_DETENT 4 #define PCNT_HIGH_LIMIT 1000 #define PCNT_LOW_LIMIT (-1000) #define GLITCH_FILTER_NS 1000 #define BUTTON_POLL_MS 20 #define BUTTON_DEBOUNCE_N 3 #define LONG_PRESS_MS 800 static pcnt_unit_handle_t s_unit; static int s_residual; /* raw counts not yet worth a detent */ static volatile encoder_button_event_t s_btn_event; static volatile bool s_btn_down; static void button_task(void *arg) { (void)arg; int stable = 0; bool debounced = false; /* true = pressed */ int64_t pressed_at = 0; bool long_fired = false; while (1) { bool level = true; /* P5 is INPUT_PULLUP: low means pressed. */ bool pressed = (pcf8574_read(PCF_PIN_ENCODER_SW, &level) == ESP_OK) && !level; if (pressed == debounced) { stable = 0; } else if (++stable >= BUTTON_DEBOUNCE_N) { stable = 0; debounced = pressed; s_btn_down = pressed; if (pressed) { pressed_at = esp_timer_get_time(); long_fired = false; } else if (!long_fired) { /* Click reported on release, so a long press never also * produces a click. */ s_btn_event = ENCODER_BTN_CLICK; } } if (debounced && !long_fired && (esp_timer_get_time() - pressed_at) > (LONG_PRESS_MS * 1000)) { long_fired = true; s_btn_event = ENCODER_BTN_LONG_PRESS; } vTaskDelay(pdMS_TO_TICKS(BUTTON_POLL_MS)); } } esp_err_t encoder_init(void) { pcnt_unit_config_t unit_cfg = { .high_limit = PCNT_HIGH_LIMIT, .low_limit = PCNT_LOW_LIMIT, }; ESP_RETURN_ON_ERROR(pcnt_new_unit(&unit_cfg, &s_unit), TAG, "pcnt unit"); pcnt_glitch_filter_config_t filter = { .max_glitch_ns = GLITCH_FILTER_NS, }; ESP_RETURN_ON_ERROR(pcnt_unit_set_glitch_filter(s_unit, &filter), TAG, "glitch filter"); /* Two channels, each watching one edge and using the other line as the * direction control — the standard full-quadrature setup. */ pcnt_chan_config_t chan_a_cfg = { .edge_gpio_num = ENCODER_PIN_A, .level_gpio_num = ENCODER_PIN_B, }; pcnt_channel_handle_t chan_a = NULL; ESP_RETURN_ON_ERROR(pcnt_new_channel(s_unit, &chan_a_cfg, &chan_a), TAG, "chan a"); pcnt_chan_config_t chan_b_cfg = { .edge_gpio_num = ENCODER_PIN_B, .level_gpio_num = ENCODER_PIN_A, }; pcnt_channel_handle_t chan_b = NULL; ESP_RETURN_ON_ERROR(pcnt_new_channel(s_unit, &chan_b_cfg, &chan_b), TAG, "chan b"); ESP_RETURN_ON_ERROR( pcnt_channel_set_edge_action(chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE), TAG, "chan a edge"); ESP_RETURN_ON_ERROR( pcnt_channel_set_level_action(chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE), TAG, "chan a level"); ESP_RETURN_ON_ERROR( pcnt_channel_set_edge_action(chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE), TAG, "chan b edge"); ESP_RETURN_ON_ERROR( pcnt_channel_set_level_action(chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE), TAG, "chan b level"); ESP_RETURN_ON_ERROR(pcnt_unit_enable(s_unit), TAG, "enable"); ESP_RETURN_ON_ERROR(pcnt_unit_clear_count(s_unit), TAG, "clear"); ESP_RETURN_ON_ERROR(pcnt_unit_start(s_unit), TAG, "start"); if (xTaskCreate(button_task, "enc_btn", 3072, NULL, 4, NULL) != pdPASS) { ESP_LOGE(TAG, "button task create failed"); return ESP_ERR_NO_MEM; } ESP_LOGI(TAG, "ready: A=%d B=%d, button on expander P%d", ENCODER_PIN_A, ENCODER_PIN_B, PCF_PIN_ENCODER_SW); return ESP_OK; } int encoder_take_delta(void) { if (!s_unit) { return 0; } int raw = 0; if (pcnt_unit_get_count(s_unit, &raw) != ESP_OK) { return 0; } pcnt_unit_clear_count(s_unit); /* * Carry the remainder rather than discarding it. Dropping it would lose a * detent whenever a turn straddles two polls, which shows up as a knob * that occasionally ignores you. */ s_residual += raw; int detents = s_residual / COUNTS_PER_DETENT; s_residual -= detents * COUNTS_PER_DETENT; return detents; } encoder_button_event_t encoder_take_button_event(void) { encoder_button_event_t e = s_btn_event; s_btn_event = ENCODER_BTN_NONE; return e; } bool encoder_button_is_down(void) { return s_btn_down; }