Files

302 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Описание компонента 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"
```