# Описание компонента zh_encoder ## Общие сведения **zh_encoder** — это компонент для ESP-IDF, реализующий драйвер поворотного энкодера на аппаратном периферийном блоке PCNT (Pulse Counter) с поддержкой кнопки и асинхронными событиями через `esp_event`. Компонент обрабатывает сигналы квадрантурного энкодера на уровне прерываний, накапливает сырые счётчики PCNT в общей задаче, преобразует их в шаги настраиваемой величины и рассылает события поворота и нажатия кнопки. ## Основные возможности 1. **Аппаратное квадрантурное декодирование** — сигнал каналов A/B обрабатывается периферией PCNT без участия CPU. 2. **Дробная точность позиции** — диапазон и шаг задаются числами с плавающей точкой (`float`). 3. **Настраиваемая цена шага** — количество импульсов PCNT на один фиксатор энкодера (`pcnt_counts_per_step`). 4. **Поддержка кнопки** — программное подавление дребезга через одноразовый таймер FreeRTOS. 5. **Множественные энкодеры** — несколько энкодеров используют одну общую задачу обработки. 6. **Событийная модель** — события поворота и кнопки рассылаются через `esp_event` из контекста задачи. 7. **Встроенная статистика ошибок** — счётчики ошибок отправки событий, переполнения очереди и минимальный остаток стека. ## Внутреннее устройство ### Структура `zh_encoder_handle_t` Внутренняя структура энкодера (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` | Одноразовый таймер подавления дребезга кнопки | Поля, к которым обращаются из прерываний и задачи обработки, защищены критической секцией на спинлоке. ### Структура сообщения очереди `zh_encoder_queue_t` Передаётся из ISR точки наблюдения PCNT в общую задачу обработки: | Поле | Тип | Описание | | -------- | ----------------------- | ---------------------------------------------------------- | | `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` с номерами зарегистрированных энкодеров | Общая задача, очередь и вектор создаются при инициализации **первого** энкодера и удаляются при деинициализации **последнего**. Параметры `task_priority`, `stack_size` и `queue_size` первого энкодера действуют для всей системы. ### Конфигурация PCNT Каждый энкодер занимает одну единицу PCNT с двумя каналами: - Единица настраивается с пределами `high_limit = 100`, `low_limit = -100` и фильтром от помех `max_glitch_ns = 1000`. - Канал A: front A + уровень B, канал B: front B + уровень A; действия каналов задаются так, чтобы вращение по часовой стрелке увеличивало счётчик. - Регистрируются две точки наблюдения: `+pcnt_counts_per_step` и `-pcnt_counts_per_step`, что даёт прерывание на каждый полный шаг энкодера. ## API (интерфейс) ### Инициализация и освобождение | Функция | Описание | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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_get_stats()` | Возвращает read-only указатель на глобальную структуру статистики `zh_encoder_stats_t`. Указатель действителен до вызова `zh_encoder_reset_stats()`. | | `zh_encoder_reset_stats()` | Обнуляет все счётчики статистики. | ### Типы и события | Тип | Конструктор значений | Значения | | ---------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `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`, в которую компонент постит все сообщения | ## Потокобезопасность 1. **Спинлок (`portMUX_TYPE`)** — защищает позицию, аккумулятор и параметры диапазона от одновременного доступа из задачи обработки, обработчиков публичного API и ISR PCNT. В прерываниях используются `taskENTER_CRITICAL_ISR` / `taskEXIT_CRITICAL_ISR`. 2. **Очередь FreeRTOS** — ISR PCNT передаёт сырой счётчик в общую задачу через `xQueueSendFromISR()`, без блокировок и без работы с позицией в прерывании. 3. **Критическая секция в ISR** — атомарная пара операций «чтение + сброс счётчика PCNT» внутри обработчика точки наблюдения. 4. **Таймер дребезга кнопки** — состояние кнопки читается и пишется только в колбэке таймера (контекст задачи таймеров FreeRTOS), поэтому конкуренции за это поле нет. 5. **События `esp_event`** — публикуются из контекста задачи (общая задача обработки и колбэк таймера), а не из ISR, с таймаутом блокировки 10 мс. Публичные функции `zh_encoder_init()` / `zh_encoder_deinit()` не защищены спинлоком между собой: их следует вызывать из одной задачи (например, из `app_main`). ## Алгоритмы ### Преобразование импульсов PCNT в шаги Общая задача обработки для каждого сообщения из очереди выполняет: 1. `total = count_accumulator + count` — суммирование остатка с новым значением счётчика. 2. `full_steps = total / pcnt_counts_per_step` — целочисленное деление со знаком; остаток сохраняется в `count_accumulator`. 3. Если `full_steps != 0`, вычисляется кандидат новой позиции `candidate = position + full_steps * encoder_step`. 4. Кандидат ограничивается диапазоном: `candidate > max → max`, `candidate < min → min`. 5. Если `|candidate| < encoder_step / 2`, значение «прилипает» к нулю (`candidate = 0`). Это исключает «хвосты» при шагах с плавающей точкой. 6. Событие поворота публикуется только при фактическом изменении позиции. ### Точки наблюдения PCNT - Прерывание генерируется при достижении счётчиком значений `+pcnt_counts_per_step` и `-pcnt_counts_per_step`, то есть ровно на один зафиксированный шаг. - После чтения счётчик сбрасывается, поэтому накопление идёт «от события к событию», а пределы единицы (±100) служат лишь страховкой от переполнения при переполнении очереди. ### Подавление дребезга кнопки 1. Прерывание по любому front'у только перезапускает одноразовый таймер (`xTimerResetFromISR`) — перезагрузка таймера «задвигает» окончание дребезга. 2. По истечении задержки колбэк таймера читает уровень пина и постит событие **только при изменении** состояния относительно предыдущего. 3. Задержка `s_gpio_debounce_time` задаётся в микросекундах и округляется вверх до целых миллисекунд (минимум 1 мс). ### Регистрация энкодеров - Номера занятых энкодеров хранятся в векторе `zh_vector`; при инициализации проверяется уникальность номера, при деинициализации — он удаляется из вектора. - Общая очередь, задача и вектор создаются, когда размер вектора становится равен 1, и освобождаются, когда он становится пустым. ## Примеры использования ### Базовый пример: подписка на события ```c #include "zh_encoder.h" zh_encoder_handle_t *encoder_handle = NULL; // Обработчик событий энкодера void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { 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 == 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 == ZH_ENCODER_BUTTON_STATUS_RELEASED ? "released" : "pressed"); } } void app_main(void) { ESP_ERROR_CHECK(esp_event_loop_create_default()); ESP_ERROR_CHECK(esp_event_handler_instance_register(ZH_ENCODER, ESP_EVENT_ANY_ID, &zh_encoder_event_handler, NULL, NULL)); zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT(); config.a_gpio_number = GPIO_NUM_4; // канал A config.b_gpio_number = GPIO_NUM_16; // канал B config.s_gpio_number = GPIO_NUM_15; // кнопка config.encoder_number = 1; // уникальный номер if (zh_encoder_init(&config, &encoder_handle) != ESP_OK) { return; // ошибка инициализации } for (;;) { vTaskDelay(pdMS_TO_TICKS(1000)); } // По завершении работы: zh_encoder_deinit(&encoder_handle); } ``` ### Работа с позицией и диапазоном ```c #include "zh_encoder.h" zh_encoder_handle_t *encoder_handle = NULL; void app_main(void) { zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT(); config.a_gpio_number = GPIO_NUM_4; config.b_gpio_number = GPIO_NUM_16; config.encoder_min_value = 0; config.encoder_max_value = 100; config.encoder_step = 0.5f; // дробный шаг config.encoder_number = 1; if (zh_encoder_init(&config, &encoder_handle) != ESP_OK) { return; } zh_encoder_set(&encoder_handle, 42.5f); // установить позицию float position = 0; zh_encoder_get(&encoder_handle, &position); // прочитать позицию printf("Position: %0.3f\n", position); zh_encoder_reinit(&encoder_handle, -10, 10, 1); // сменить диапазон и шаг zh_encoder_reset(&encoder_handle); // сброс в середину диапазона zh_encoder_deinit(&encoder_handle); // освобождение ресурсов } ``` ### Мониторинг статистики ошибок ```c #include "zh_encoder.h" zh_encoder_handle_t *encoder_handle = NULL; void app_main(void) { zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT(); config.a_gpio_number = GPIO_NUM_4; config.b_gpio_number = GPIO_NUM_16; config.encoder_number = 1; if (zh_encoder_init(&config, &encoder_handle) != ESP_OK) { return; } for (;;) { const zh_encoder_stats_t *stats = zh_encoder_get_stats(); if (stats->queue_overflow_error > 0) { printf("Queue overflow: %lu\n", (unsigned long)stats->queue_overflow_error); } if (stats->min_stack_size < 512) { printf("Low stack watermark: %lu\n", (unsigned long)stats->min_stack_size); } zh_encoder_reset_stats(); // сброс счётчиков vTaskDelay(pdMS_TO_TICKS(60000)); } // По завершении работы: zh_encoder_deinit(&encoder_handle); } ``` ## Логирование Компонент использует ESP-IDF логирование с тегом `"zh_encoder"`. Сообщения об ошибках включают имя файла, номер строки и имя кода ошибки. ```c 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 | ## Ограничения 1. **Уникальный номер энкодера** — `encoder_number` должен быть больше 0 и не повторяться среди всех инициализированных энкодеров (проверяется по вектору зарегистрированных номеров). 2. **Общие задача и очередь** — `task_priority`, `stack_size` и `queue_size` первого инициализированного энкодера используются всей системой; для последующих энкодеров они игнорируются. 3. **Ёмкость очереди по умолчанию равна 1** — при быстрой ротации или медленной задаче обработки возможны потери сообщений; факт потери фиксируется счётчиком `queue_overflow_error`. 4. **Число энкодеров ограничено железом** — количество доступных единиц PCNT и GPIO конкретной модели ESP32. 5. **Позиция инициализируется серединой диапазона** — `(min + max) / 2`, а не нулём; при `min = -10, max = 10` стартовая позиция равна 0. 6. **Ограничение и «прилипание» позиции** — значение за краями диапазона отбрасывается, а значение ближе половины шага к нулю заменяется нулём. 7. **Параметры не могут меняться на лету** — GPIO, номер энкодера и `pcnt_counts_per_step` фиксируются при инициализации; `zh_encoder_reinit()` меняет только диапазон и шаг. 8. **Зависимость от `zh_vector`** — компонент использует сторонний вектор для реестра энкодеров; он подтягивается автоматически как зависимость компонента. 9. **Требуется цикл событий `esp_event`** — без созданного default loop (`esp_event_loop_create_default()`) события не доставляются. ## Требования для сборки - ESP-IDF версии **5.0 или выше** (используется PCNT driver из компонента `driver`). - FreeRTOS (входит в состав ESP-IDF) — задачи, очереди, таймеры. - `esp_event` (входит в состав ESP-IDF) — рассылка событий. - `esp_timer` (входит в состав ESP-IDF) — таймер дребезга кнопки. - Компонент `zh_vector` — автоматически устанавливается как зависимость через `idf_component.yml`. ## Установка ```bash cd ваш_проект/components git clone https://github.com/aZholtikov/zh_encoder ``` ```c #include "zh_encoder.h" ```