doc: added wiki

This commit is contained in:
aZholtikov committed 2026-10-18 10:29:59 +03:00
1 parent f2f3030ee7
commit 780ffbbad0
1 file changed
+299
+299
View File
@@ -0,0 +1,299 @@
# Описание компонента 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` | Конфигурация инициализации: 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`, в которую компонент постит все сообщения. |
## Потокобезопасность
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 ? "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");
}
}
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"
```