27 KiB
Описание компонента zh_encoder
Общие сведения
zh_encoder — это компонент для ESP-IDF, реализующий драйвер поворотного энкодера на аппаратном периферийном блоке PCNT (Pulse Counter) с поддержкой кнопки и асинхронными событиями через esp_event.
Компонент обрабатывает сигналы квадрантурного энкодера на уровне прерываний, накапливает сырые счётчики PCNT в общей задаче, преобразует их в шаги настраиваемой величины и рассылает события поворота и нажатия кнопки.
Основные возможности
- Аппаратное квадрантурное декодирование — сигнал каналов A/B обрабатывается периферией PCNT без участия CPU.
- Дробная точность позиции — диапазон и шаг задаются числами с плавающей точкой (
float). - Настраиваемая цена шага — количество импульсов PCNT на один фиксатор энкодера (
pcnt_counts_per_step). - Поддержка кнопки — программное подавление дребезга через одноразовый таймер FreeRTOS.
- Множественные энкодеры — несколько энкодеров используют одну общую задачу обработки.
- Событийная модель — события поворота и кнопки рассылаются через
esp_eventиз контекста задачи. - Встроенная статистика ошибок — счётчики ошибок отправки событий, переполнения очереди и минимальный остаток стека.
Внутреннее устройство
Структура 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, в которую компонент постит все сообщения |
Потокобезопасность
- Спинлок (
portMUX_TYPE) — защищает позицию, аккумулятор и параметры диапазона от одновременного доступа из задачи обработки, обработчиков публичного API и ISR PCNT. В прерываниях используютсяtaskENTER_CRITICAL_ISR/taskEXIT_CRITICAL_ISR. - Очередь FreeRTOS — ISR PCNT передаёт сырой счётчик в общую задачу через
xQueueSendFromISR(), без блокировок и без работы с позицией в прерывании. - Критическая секция в ISR — атомарная пара операций «чтение + сброс счётчика PCNT» внутри обработчика точки наблюдения.
- Таймер дребезга кнопки — состояние кнопки читается и пишется только в колбэке таймера (контекст задачи таймеров FreeRTOS), поэтому конкуренции за это поле нет.
- События
esp_event— публикуются из контекста задачи (общая задача обработки и колбэк таймера), а не из ISR, с таймаутом блокировки 10 мс.
Публичные функции zh_encoder_init() / zh_encoder_deinit() не защищены спинлоком между собой: их следует вызывать из одной задачи (например, из app_main).
Алгоритмы
Преобразование импульсов PCNT в шаги
Общая задача обработки для каждого сообщения из очереди выполняет:
total = count_accumulator + count— суммирование остатка с новым значением счётчика.full_steps = total / pcnt_counts_per_step— целочисленное деление со знаком; остаток сохраняется вcount_accumulator.- Если
full_steps != 0, вычисляется кандидат новой позицииcandidate = position + full_steps * encoder_step. - Кандидат ограничивается диапазоном:
candidate > max → max,candidate < min → min. - Если
|candidate| < encoder_step / 2, значение «прилипает» к нулю (candidate = 0). Это исключает «хвосты» при шагах с плавающей точкой. - Событие поворота публикуется только при фактическом изменении позиции.
Точки наблюдения PCNT
- Прерывание генерируется при достижении счётчиком значений
+pcnt_counts_per_stepи-pcnt_counts_per_step, то есть ровно на один зафиксированный шаг. - После чтения счётчик сбрасывается, поэтому накопление идёт «от события к событию», а пределы единицы (±100) служат лишь страховкой от переполнения при переполнении очереди.
Подавление дребезга кнопки
- Прерывание по любому front'у только перезапускает одноразовый таймер (
xTimerResetFromISR) — перезагрузка таймера «задвигает» окончание дребезга. - По истечении задержки колбэк таймера читает уровень пина и постит событие только при изменении состояния относительно предыдущего.
- Задержка
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 |
Ограничения
- Уникальный номер энкодера —
encoder_numberдолжен быть больше 0 и не повторяться среди всех инициализированных энкодеров (проверяется по вектору зарегистрированных номеров). - Общие задача и очередь —
task_priority,stack_sizeиqueue_sizeпервого инициализированного энкодера используются всей системой; для последующих энкодеров они игнорируются. - Ёмкость очереди по умолчанию равна 1 — при быстрой ротации или медленной задаче обработки возможны потери сообщений; факт потери фиксируется счётчиком
queue_overflow_error. - Число энкодеров ограничено железом — количество доступных единиц PCNT и GPIO конкретной модели ESP32.
- Позиция инициализируется серединой диапазона —
(min + max) / 2, а не нулём; приmin = -10, max = 10стартовая позиция равна 0. - Ограничение и «прилипание» позиции — значение за краями диапазона отбрасывается, а значение ближе половины шага к нулю заменяется нулём.
- Параметры не могут меняться на лету — GPIO, номер энкодера и
pcnt_counts_per_stepфиксируются при инициализации;zh_encoder_reinit()меняет только диапазон и шаг. - Зависимость от
zh_vector— компонент использует сторонний вектор для реестра энкодеров; он подтягивается автоматически как зависимость компонента. - Требуется цикл событий
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"