doc: added wiki
This commit is contained in:
1 parent
f2f3030ee7
commit
780ffbbad0
1 file changed
+299
+299
@@ -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"
|
||||
```
|
||||
Reference in new issue
Block a user