/* * MTG RFID Companion - EC11 rotary encoder driver * * Implements a true Gray Code quadrature state machine. * Both CLK and DT pins generate interrupts on ANYEDGE. * Contact bounce is inherently rejected by valid transition filtering. * Each mechanical detent ("tick") advances through a fixed number of valid * quadrature transitions (default 4 for standard full-step EC11, or 2 for half-step). */ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" #include "driver/gpio.h" #include "hw_pins.h" #include "encoder.h" static const char *TAG = "encoder"; static volatile int32_t s_delta = 0; static volatile int8_t s_accum = 0; static volatile uint8_t s_prev_state = 0x03; static volatile int8_t s_steps_per_detent = 4; /* 4 = full-step, 2 = half-step */ /* * Quadrature Gray-Code Transition Table * Index: (old_state << 2) | new_state (4-bit, 0..15) * State bits: bit 1 = CLK (GPIO 32), bit 0 = DT (GPIO 33) * * Clockwise sequence: 11 (3) -> 01 (1) -> 00 (0) -> 10 (2) -> 11 (3) * 11->01 (13): +1 * 01->00 (4): +1 * 00->10 (2): +1 * 10->11 (11): +1 * * Counter-Clockwise sequence: 11 (3) -> 10 (2) -> 00 (0) -> 01 (1) -> 11 (3) * 11->10 (14): -1 * 10->00 (8): -1 * 00->01 (1): -1 * 01->11 (7): -1 * * All bounces (e.g. 11 <-> 01) cancel out: +1 + (-1) = 0. * Invalid diagonal skips (00 <-> 11, 01 <-> 10): 0. */ static const int8_t s_quad_table[16] = { [0] = 0, [1] = -1, // 00 -> 01 (CCW) [2] = 1, // 00 -> 10 (CW) [3] = 0, // 00 -> 11 (invalid) [4] = 1, // 01 -> 00 (CW) [5] = 0, [6] = 0, // 01 -> 10 (invalid) [7] = -1, // 01 -> 11 (CCW) [8] = -1, // 10 -> 00 (CCW) [9] = 0, // 10 -> 01 (invalid) [10] = 0, [11] = 1, // 10 -> 11 (CW) [12] = 0, // 11 -> 00 (invalid) [13] = 1, // 11 -> 01 (CW) [14] = -1, // 11 -> 10 (CCW) [15] = 0, }; static void IRAM_ATTR encoder_isr(void *arg) { (void)arg; /* Read current levels of both encoder channels */ uint8_t clk = (uint8_t)gpio_get_level(HW_ENC_CLK_GPIO); uint8_t dt = (uint8_t)gpio_get_level(HW_ENC_DT_GPIO); uint8_t curr = (clk << 1) | dt; if (curr != s_prev_state) { uint8_t idx = ((s_prev_state & 0x03) << 2) | (curr & 0x03); int8_t step = s_quad_table[idx]; s_prev_state = curr; if (step != 0) { s_accum += step; if (s_accum >= s_steps_per_detent) { s_delta += 1; s_accum = 0; } else if (s_accum <= -s_steps_per_detent) { s_delta -= 1; s_accum = 0; } } } } static esp_err_t config_pin(gpio_num_t pin, gpio_int_type_t intr_type) { gpio_config_t cfg = { .pin_bit_mask = 1ULL << pin, .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = intr_type, }; return gpio_config(&cfg); } esp_err_t encoder_init(void) { esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_ERROR_CHECK(err); } ESP_ERROR_CHECK(config_pin(HW_ENC_CLK_GPIO, GPIO_INTR_ANYEDGE)); ESP_ERROR_CHECK(config_pin(HW_ENC_DT_GPIO, GPIO_INTR_ANYEDGE)); ESP_ERROR_CHECK(config_pin(HW_ENC_SW_GPIO, GPIO_INTR_DISABLE)); /* Initialize initial state */ uint8_t clk = (uint8_t)gpio_get_level(HW_ENC_CLK_GPIO); uint8_t dt = (uint8_t)gpio_get_level(HW_ENC_DT_GPIO); s_prev_state = (clk << 1) | dt; s_accum = 0; s_delta = 0; ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_CLK_GPIO, encoder_isr, NULL)); ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_DT_GPIO, encoder_isr, NULL)); ESP_LOGI(TAG, "EC11 quadrature encoder ready (CLK=%d DT=%d SW=%d steps/tick=%d)", HW_ENC_CLK_GPIO, HW_ENC_DT_GPIO, HW_ENC_SW_GPIO, s_steps_per_detent); return ESP_OK; } int8_t encoder_get_steps_per_detent(void) { return s_steps_per_detent; } void encoder_set_steps_per_detent(int8_t steps) { if (steps != 2 && steps != 4) { steps = 4; } s_steps_per_detent = steps; s_accum = 0; ESP_LOGI(TAG, "Encoder resolution set to %d steps/detent", steps); } bool encoder_has_delta(void) { return s_delta != 0; } int32_t encoder_get_delta_and_reset(void) { int32_t v = (int32_t)s_delta; s_delta = 0; return v; }