Files
zh_encoder/wiki_ru.md
T

27 KiB
Raw Blame History

Описание компонента 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, и освобождаются, когда он становится пустым.

Примеры использования

Базовый пример: подписка на события

#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);
}

Работа с позицией и диапазоном

#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);             // освобождение ресурсов
}

Мониторинг статистики ошибок

#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". Сообщения об ошибках включают имя файла, номер строки и имя кода ошибки.

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.

Установка

cd ваш_проект/components
git clone https://github.com/aZholtikov/zh_encoder
#include "zh_encoder.h"