diff --git a/README.md b/README.md index a388700..36a8f77 100644 --- a/README.md +++ b/README.md @@ -192,12 +192,12 @@ void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t ev if (event_id == ZH_BUTTON_EVENT) { zh_encoder_button_event_on_isr_t *button_event = (zh_encoder_button_event_on_isr_t *)event_data; - printf("Encoder %d button %s\n", button_event->encoder_number, button_event->button_status ? "released" : "pressed"); + printf("Encoder %d button %s\n", button_event->encoder_number, button_event->button_status == ZH_ENCODER_BUTTON_STATUS_RELEASED ? "released" : "pressed"); } else if (event_id == ZH_ENCODER_EVENT) { zh_encoder_event_on_isr_t *encoder_event = (zh_encoder_event_on_isr_t *)event_data; - printf("Encoder %d position %0.3f %s\n", encoder_event->encoder_number, encoder_event->encoder_position, encoder_event->encoder_status ? "increased" : "reduced"); + printf("Encoder %d position %0.3f %s\n", encoder_event->encoder_number, encoder_event->encoder_position, encoder_event->encoder_status == ZH_ENCODER_STATUS_INCREASED ? "increased" : "reduced"); } } diff --git a/idf_component.yml b/idf_component.yml index eebf589..483cc40 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,6 +1,6 @@ description: ESP32 ESP-IDF component for rotary encoder url: https://github.com/aZholtikov/zh_encoder -version: "3.1.0" +version: "3.2.0" license: "Apache-2.0" maintainers: - "Alexey Zholtikov " diff --git a/include/zh_encoder.h b/include/zh_encoder.h index 6df92b1..28c7591 100644 --- a/include/zh_encoder.h +++ b/include/zh_encoder.h @@ -20,6 +20,8 @@ * * @note All public functions require a valid handle pointer obtained through zh_encoder_init(). * @note Rotation and button events are posted from task context (shared processing task and timer callback). + * @note Button state semantics: with pull-up enabled (default), the pressed level is read as 0 + * (ZH_ENCODER_BUTTON_STATUS_PRESSED) and the released level as 1 (ZH_ENCODER_BUTTON_STATUS_RELEASED). */ #pragma once @@ -121,6 +123,15 @@ extern "C" ESP_EVENT_DECLARE_BASE(ZH_ENCODER); + /** + * @brief Encoder rotation direction reported in rotation events. + */ + typedef enum + { + ZH_ENCODER_STATUS_DECREASED, /*!< Encoder position decreased */ + ZH_ENCODER_STATUS_INCREASED /*!< Encoder position increased */ + } zh_encoder_status_t; + /** * @brief Event data structure for encoder rotation events. * @@ -130,11 +141,20 @@ extern "C" */ typedef struct { - float encoder_position; /*!< Encoder current position */ - uint8_t encoder_number; /*!< Encoder unique number */ - bool encoder_status; /*!< true if position increased, false if reduced */ + float encoder_position; /*!< Encoder current position */ + uint8_t encoder_number; /*!< Encoder unique number */ + zh_encoder_status_t encoder_status; /*!< Rotation direction */ } zh_encoder_event_on_isr_t; + /** + * @brief Encoder push-button state reported in button events. + */ + typedef enum + { + ZH_ENCODER_BUTTON_STATUS_PRESSED, /*!< Button is pressed */ + ZH_ENCODER_BUTTON_STATUS_RELEASED /*!< Button is released */ + } zh_encoder_button_status_t; + /** * @brief Event data structure for encoder button events. * @@ -145,8 +165,8 @@ extern "C" */ typedef struct { - uint8_t encoder_number; /*!< Encoder unique number */ - bool button_status; /*!< Encoder button status */ + uint8_t encoder_number; /*!< Encoder unique number */ + zh_encoder_button_status_t button_status; /*!< Debounced button state */ } zh_encoder_button_event_on_isr_t; /** diff --git a/version.txt b/version.txt index a0cd9f0..a4f52a5 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.1.0 \ No newline at end of file +3.2.0 \ No newline at end of file diff --git a/wiki_ru.md b/wiki_ru.md index 7371cbe..cbb5756 100644 --- a/wiki_ru.md +++ b/wiki_ru.md @@ -22,21 +22,21 @@ Внутренняя структура энкодера (opaque-хэндл, доступен только через API компонента): -| Поле | Тип | Описание | -| ------ | ----- | ---------- | -| `s_gpio_status` | `bool` | Текущее состояние кнопки (обновляется только колбэком таймера дребезга) | -| `encoder_number` | `uint8_t` | Уникальный номер энкодера, передаётся в событиях | -| `s_gpio_number` | `gpio_num_t` | GPIO кнопки (`GPIO_NUM_MAX`, если кнопка отключена) | -| `encoder_step` | `float` | Приращение позиции на один фиксатор энкодера | -| `encoder_position` | `float` | Текущая позиция энкодера | -| `encoder_min_value` | `float` | Минимальное значение позиции | -| `encoder_max_value` | `float` | Максимальное значение позиции | -| `count_accumulator` | `int32_t` | Остаток импульсов PCNT, не образующий целого шага | -| `pcnt_counts_per_step` | `uint8_t` | Количество импульсов PCNT на один шаг | -| `pcnt_unit_handle` | `pcnt_unit_handle_t` | Дескриптор единицы PCNT | -| `pcnt_channel_a_handle` | `pcnt_channel_handle_t` | Дескриптор канала A (edge A, level B) | -| `pcnt_channel_b_handle` | `pcnt_channel_handle_t` | Дескриптор канала B (edge B, level A) | -| `button_timer` | `TimerHandle_t` | Одноразовый таймер подавления дребезга кнопки | +| Поле | Тип | Описание | +| ----------------------- | ----------------------- | ----------------------------------------------------------------------- | +| `s_gpio_status` | `bool` | Текущее состояние кнопки (обновляется только колбэком таймера дребезга) | +| `encoder_number` | `uint8_t` | Уникальный номер энкодера, передаётся в событиях | +| `s_gpio_number` | `gpio_num_t` | GPIO кнопки (`GPIO_NUM_MAX`, если кнопка отключена) | +| `encoder_step` | `float` | Приращение позиции на один фиксатор энкодера | +| `encoder_position` | `float` | Текущая позиция энкодера | +| `encoder_min_value` | `float` | Минимальное значение позиции | +| `encoder_max_value` | `float` | Максимальное значение позиции | +| `count_accumulator` | `int32_t` | Остаток импульсов PCNT, не образующий целого шага | +| `pcnt_counts_per_step` | `uint8_t` | Количество импульсов PCNT на один шаг | +| `pcnt_unit_handle` | `pcnt_unit_handle_t` | Дескриптор единицы PCNT | +| `pcnt_channel_a_handle` | `pcnt_channel_handle_t` | Дескриптор канала A (edge A, level B) | +| `pcnt_channel_b_handle` | `pcnt_channel_handle_t` | Дескриптор канала B (edge B, level A) | +| `button_timer` | `TimerHandle_t` | Одноразовый таймер подавления дребезга кнопки | Поля, к которым обращаются из прерываний и задачи обработки, защищены критической секцией на спинлоке. @@ -44,22 +44,22 @@ Передаётся из ISR точки наблюдения PCNT в общую задачу обработки: -| Поле | Тип | Описание | -|------|-----|----------| -| `count` | `int32_t` | Знаковое значение счётчика PCNT, прочитанное перед сбросом | -| `handle` | `zh_encoder_handle_t *` | Указатель на хэндл энкодера-источника | +| Поле | Тип | Описание | +| -------- | ----------------------- | ---------------------------------------------------------- | +| `count` | `int32_t` | Знаковое значение счётчика PCNT, прочитанное перед сбросом | +| `handle` | `zh_encoder_handle_t *` | Указатель на хэндл энкодера-источника | Указатель на хэндл читается без спинлока: это безопасно, так как память хэндла освобождается в `zh_encoder_deinit()` только после удаления общей задачи обработки (при удалении последнего энкодера). ### Общие (статические) ресурсы -| Ресурс | Тип | Описание | -| -------- | ----- | ---------- | -| `zh_encoder` | `TaskHandle_t` | Дескриптор общей задачи обработки, создаётся для первого энкодера | -| `_queue_handle` | `QueueHandle_t` | Общая очередь сообщений от ISR PCNT к задаче обработки | -| `_spinlock` | `portMUX_TYPE` | Спинлок для защиты общих данных энкодеров | -| `_stats` | `zh_encoder_stats_t` | Глобальная статистика ошибок (счётчики и watermark стека) | -| `_vector` | `zh_vector_t *` | Вектор `zh_vector` с номерами зарегистрированных энкодеров | +| Ресурс | Тип | Описание | +| --------------- | -------------------- | ----------------------------------------------------------------- | +| `zh_encoder` | `TaskHandle_t` | Дескриптор общей задачи обработки, создаётся для первого энкодера | +| `_queue_handle` | `QueueHandle_t` | Общая очередь сообщений от ISR PCNT к задаче обработки | +| `_spinlock` | `portMUX_TYPE` | Спинлок для защиты общих данных энкодеров | +| `_stats` | `zh_encoder_stats_t` | Глобальная статистика ошибок (счётчики и watermark стека) | +| `_vector` | `zh_vector_t *` | Вектор `zh_vector` с номерами зарегистрированных энкодеров | Общая задача, очередь и вектор создаются при инициализации **первого** энкодера и удаляются при деинициализации **последнего**. Параметры `task_priority`, `stack_size` и `queue_size` первого энкодера действуют для всей системы. @@ -75,37 +75,39 @@ ### Инициализация и освобождение -| Функция | Описание | -| --------- | ---------- | -| `zh_encoder_init()` | Создаёт энкодер по конфигурации: проверяет параметры, регистрирует номер в векторе, при необходимости создаёт общую очередь и задачу, инициализирует PCNT и GPIO кнопки. Позиция устанавливается в середину диапазона. Требует, чтобы входной `*handle` был равен `NULL`. | -| `zh_encoder_deinit()` | Останавливает PCNT, удаляет каналы и единицу, снимает обработчик GPIO прерываний и таймер кнопки, удаляет номер из вектора. При удалении последнего энкодера удаляет общую задачу, очередь и вектор. Устанавливает входной указатель в `NULL`. | -| `zh_encoder_reinit()` | Меняет диапазон (`min`, `max`) и шаг (`step`) без перераспределения ресурсов, сбрасывает позицию в середину нового диапазона и обнуляет счётчик PCNT. | +| Функция | Описание | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `zh_encoder_init()` | Создаёт энкодер по конфигурации: проверяет параметры, регистрирует номер в векторе, при необходимости создаёт общую очередь и задачу, инициализирует PCNT и GPIO кнопки. Позиция устанавливается в середину диапазона. Требует, чтобы входной `*handle` был равен `NULL`. | +| `zh_encoder_deinit()` | Останавливает PCNT, удаляет каналы и единицу, снимает обработчик GPIO прерываний и таймер кнопки, удаляет номер из вектора. При удалении последнего энкодера удаляет общую задачу, очередь и вектор. Устанавливает входной указатель в `NULL`. | +| `zh_encoder_reinit()` | Меняет диапазон (`min`, `max`) и шаг (`step`) без перераспределения ресурсов, сбрасывает позицию в середину нового диапазона и обнуляет счётчик PCNT. | ### Управление позицией -| Функция | Описание | -|---------|----------| -| `zh_encoder_set()` | Устанавливает позицию в заданное значение (в пределах настроенного диапазона), обнуляет аккумулятор и счётчик PCNT. | -| `zh_encoder_get()` | Читает текущую позицию энкодера. | -| `zh_encoder_reset()` | Сбрасывает позицию в середину диапазона `(min + max) / 2` и обнуляет счётчик PCNT. | +| Функция | Описание | +| -------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `zh_encoder_set()` | Устанавливает позицию в заданное значение (в пределах настроенного диапазона), обнуляет аккумулятор и счётчик PCNT. | +| `zh_encoder_get()` | Читает текущую позицию энкодера. | +| `zh_encoder_reset()` | Сбрасывает позицию в середину диапазона `(min + max) / 2` и обнуляет счётчик PCNT. | ### Статистика -| Функция | Описание | -|---------|----------| -| `zh_encoder_get_stats()` | Возвращает read-only указатель на глобальную структуру статистики `zh_encoder_stats_t`. Указатель действителен до вызова `zh_encoder_reset_stats()`. | -| `zh_encoder_reset_stats()` | Обнуляет все счётчики статистики. | +| Функция | Описание | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `zh_encoder_get_stats()` | Возвращает read-only указатель на глобальную структуру статистики `zh_encoder_stats_t`. Указатель действителен до вызова `zh_encoder_reset_stats()`. | +| `zh_encoder_reset_stats()` | Обнуляет все счётчики статистики. | ### Типы и события -| Элемент | Описание | -| --------- | ---------- | -| `zh_encoder_init_config_t` | Конфигурация инициализации: GPIO, диапазон, шаг, параметры задачи и уникальный номер. Значения по умолчанию задаёт `ZH_ENCODER_INIT_CONFIG_DEFAULT()`. | -| `zh_encoder_stats_t` | Статистика: `event_post_error`, `queue_overflow_error`, `min_stack_size`. | -| `zh_encoder_event_on_isr_t` | Данные события поворота: позиция, номер энкодера, направление (`true` — увеличение). | -| `zh_encoder_button_event_on_isr_t` | Данные события кнопки: номер энкодера, состояние. | -| `zh_encoder_event_id_t` | Идентификаторы событий: `ZH_BUTTON_EVENT`, `ZH_ENCODER_EVENT`. | -| `ZH_ENCODER` | База событий `esp_event`, в которую компонент постит все сообщения. | +| Тип | Конструктор значений | Значения | +| ---------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `zh_encoder_init_config_t` | `ZH_ENCODER_INIT_CONFIG_DEFAULT()` | Конфигурация инициализации: GPIO, диапазон, шаг, параметры задачи и уникальный номер | +| `zh_encoder_stats_t` | — | Статистика: `event_post_error`, `queue_overflow_error`, `min_stack_size` | +| `zh_encoder_event_id_t` | — | Идентификаторы событий: `ZH_BUTTON_EVENT`, `ZH_ENCODER_EVENT` | +| `zh_encoder_status_t` | — | Направление поворота: `ZH_ENCODER_STATUS_DECREASED`, `ZH_ENCODER_STATUS_INCREASED` | +| `zh_encoder_button_status_t` | — | Состояние кнопки: `ZH_ENCODER_BUTTON_STATUS_PRESSED` (уровень 0), `ZH_ENCODER_BUTTON_STATUS_RELEASED` (уровень 1) | +| `zh_encoder_event_on_isr_t` | — | Данные события поворота: позиция, номер энкодера, направление (`zh_encoder_status_t`) | +| `zh_encoder_button_event_on_isr_t` | — | Данные события кнопки: номер энкодера, состояние (`zh_encoder_button_status_t`) | +| `ZH_ENCODER` | `ESP_EVENT_DEFINE_BASE` | База событий `esp_event`, в которую компонент постит все сообщения | ## Потокобезопасность @@ -161,11 +163,11 @@ void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t ev if (event_id == ZH_ENCODER_EVENT) { zh_encoder_event_on_isr_t *rot = (zh_encoder_event_on_isr_t *)event_data; printf("Encoder %d: position %0.3f (%s)\n", rot->encoder_number, - rot->encoder_position, rot->encoder_status ? "up" : "down"); + rot->encoder_position, rot->encoder_status == ZH_ENCODER_STATUS_INCREASED ? "up" : "down"); } else if (event_id == ZH_BUTTON_EVENT) { zh_encoder_button_event_on_isr_t *btn = (zh_encoder_button_event_on_isr_t *)event_data; printf("Encoder %d: button %s\n", btn->encoder_number, - btn->button_status ? "released" : "pressed"); + btn->button_status == ZH_ENCODER_BUTTON_STATUS_RELEASED ? "released" : "pressed"); } } @@ -259,13 +261,13 @@ esp_log_level_set("zh_encoder", ESP_LOG_ERROR); ## Ошибки -| Код | Описание | -| ----- | ---------- | -| `ESP_OK` | Операция выполнена успешно | -| `ESP_ERR_INVALID_ARG` | NULL-аргументы, неверные параметры конфигурации (диапазон, шаг, номер, GPIO), дублирование номера энкодера, позиция вне диапазона | -| `ESP_ERR_INVALID_STATE` | Энкодер по входному хэндлу уже инициализирован (`*handle != NULL`) | -| `ESP_ERR_NO_MEM` | Не удалось выделить память под хэндл энкодера | -| `ESP_FAIL` | Ошибка инициализации GPIO/PCNT, создания задачи, очереди или вектора, сброса счётчика PCNT | +| Код | Описание | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `ESP_OK` | Операция выполнена успешно | +| `ESP_ERR_INVALID_ARG` | NULL-аргументы, неверные параметры конфигурации (диапазон, шаг, номер, GPIO), дублирование номера энкодера, позиция вне диапазона | +| `ESP_ERR_INVALID_STATE` | Энкодер по входному хэндлу уже инициализирован (`*handle != NULL`) | +| `ESP_ERR_NO_MEM` | Не удалось выделить память под хэндл энкодера | +| `ESP_FAIL` | Ошибка инициализации GPIO/PCNT, создания задачи, очереди или вектора, сброса счётчика PCNT | ## Ограничения diff --git a/zh_encoder.c b/zh_encoder.c index 5c1b9b8..aa93809 100644 --- a/zh_encoder.c +++ b/zh_encoder.c @@ -618,7 +618,7 @@ static void _zh_encoder_isr_processing_task(void *pvParameter) { zh_encoder_handle_t *handle = encoder_queue.handle; bool position_changed = false; - bool encoder_status = false; + zh_encoder_status_t encoder_status = ZH_ENCODER_STATUS_DECREASED; float encoder_position = 0.0; uint8_t encoder_number = 0; taskENTER_CRITICAL(&handle->lock); @@ -644,7 +644,7 @@ static void _zh_encoder_isr_processing_task(void *pvParameter) { handle->encoder_position = candidate; position_changed = true; - encoder_status = full_steps > 0 ? true : false; + encoder_status = full_steps > 0 ? ZH_ENCODER_STATUS_INCREASED : ZH_ENCODER_STATUS_DECREASED; encoder_position = candidate; encoder_number = handle->encoder_number; } @@ -672,7 +672,7 @@ static void _zh_encoder_button_timer_cb(TimerHandle_t xTimer) encoder_handle->s_gpio_status = s_gpio_status; zh_encoder_button_event_on_isr_t encoder_data = {0}; encoder_data.encoder_number = encoder_handle->encoder_number; - encoder_data.button_status = s_gpio_status; + encoder_data.button_status = s_gpio_status == true ? ZH_ENCODER_BUTTON_STATUS_RELEASED : ZH_ENCODER_BUTTON_STATUS_PRESSED; ZH_ERROR_CHECK_CONT(esp_event_post(ZH_ENCODER, ZH_BUTTON_EVENT, &encoder_data, sizeof(encoder_data), pdMS_TO_TICKS(10)) == ESP_OK, ++_stats.event_post_error, "Encoder button isr processing failed. Failed to post interrupt event."); } }