# Описание компонента zh_ttp229 ## Общие сведения **zh_ttp229** — это компонент ESP-IDF для работы с контроллером ёмкостных сенсорных кнопок TTP229 по интерфейсу SCL/SDO. Чтение состояния кнопок выполняется через периферию RMT вместо программного переключения GPIO (bit-banging). Компонент регистрирует прерывание на выводе SDO, по фронтальному фронту генерирует тактовые импульсы SCL через RMT TX, принимает импульс с линии SDO через RMT RX и публикует событие `ZH_TTP229` с номером нажатой кнопки. ## Основные возможности 1. **Два режима работы** — конфигурации на 8 и на 16 кнопок. 2. **RMT-обмен** — тактирование и приём без bit-banging, с IRAM-safe обработчиками. 3. **Многоустройственность** — несколько плат TTP229 с уникальными номерами устройств. 4. **Событийный API** — уведомления через фреймворк `esp_event`. 5. **Debounce по тикам** — отбрасывание дребезга без задержек в задаче обработки. 6. **Восстановление RX-канала** — автоматический сброс RMT RX при ошибках драйвера. 7. **Сбор статистики** — счётчики ошибок RMT, постинга событий, переполнения очереди, остаток стека. 8. **Самоочистка ресурсов** — общая очередь и задача удаляются при деинициализации последнего устройства. ## Внутреннее устройство ### Структура `zh_ttp229_handle_t` Непрозрачный дескриптор одного устройства, определяется в `zh_ttp229.c`. | Поле | Тип | Описание | |------|-----|----------| | `sdo_gpio` | `gpio_num_t` | Вывод данных SDO (прерывание и приём) | | `scl_gpio` | `gpio_num_t` | Вывод тактирования SCL (RMT TX) | | `device_number` | `uint8_t` | Уникальный номер устройства, заданный при инициализации | | `rmt_tx_start_delay` | `uint8_t` | Задержка (мкс) между импульсом на SDO и стартом генератора RMT | | `scl_symbols_count` | `uint8_t` | Число тактовых символов SCL (8 или 16) | | `debounce_time` | `uint16_t` | Время debounce в миллисекундах | | `last_event_tick` | `TickType_t` | Тик последнего отправленного события, `-1` до первого события | | `rx_needs_reset` | `volatile bool` | Флаг запроса сброса RMT RX канала, устанавливается из ISR | | `scl_symbols` | `rmt_symbol_word_t [16]` | Предвычисленные символы тактового сигнала SCL | | `rx_symbols` | `rmt_symbol_word_t` | Принятый символ с линии SDO | | `tx_channel` | `rmt_channel_handle_t` | Дескриптор RMT TX канала | | `rx_channel` | `rmt_channel_handle_t` | Дескриптор RMT RX канала | | `copy_encoder` | `rmt_encoder_handle_t` | Copy-энкодер для RMT TX | Поле `rx_channel` обнуляется при деинициализации — обработчик прерывания проверяет его и завершается, не обращаясь к удалённому каналу. ### Структура `zh_ttp229_queue_t` Элемент очереди, передаваемый из RMT RX callback в задачу обработки. | Поле | Тип | Описание | |------|-----|----------| | `handle` | `zh_ttp229_handle_t *` | Указатель на дескриптор устройства-инициатора | | `rx_symbol` | `rmt_symbol_word_t` | Снимок принятого символа SDO | ### Модульные статические данные | Переменная | Тип | Описание | |------------|-----|----------| | `zh_ttp229` | `TaskHandle_t` | Задача обработки прерываний; создаётся для первого устройства | | `_queue_handle` | `QueueHandle_t` | Общая очередь уведомлений от RMT RX callback | | `_stats` | `zh_ttp229_stats_t` | Накопленная статистика ошибок | | `_vector` | `zh_vector_t *` | Вектор номеров всех инициализированных устройств (компонент `zh_vector`) | ### Управление памятью Дескриптор выделяется через `heap_caps_calloc()` с флагом `MALLOC_CAP_8BIT` при инициализации и освобождается в `zh_ttp229_deinit()`. Общая память (очередь, задача, вектор) принадлежит первому устройству: при ошибке инициализации первого устройства выполняется откат (`_zh_ttp229_rollback_task_and_resources()`), при удалении последнего — полное освобождение. ## API (интерфейс) ### Инициализация и освобождение | Функция | Описание | |---------|----------| | `zh_ttp229_init()` | Инициализация устройства: валидация конфигурации, создание общих ресурсов (для первого устройства), настройка GPIO с прерыванием по фронтальному фронту, создание RMT TX/RX каналов. Требует `*handle == NULL` на входе. | | `zh_ttp229_deinit()` | Деинициализация устройства: удаление из вектора, снятие ISR handler, удаление RMT каналов и энкодера, сброс GPIO. Общие ресурсы удаляются только для последнего устройства. | ### Получение информации и сброс | Функция | Описание | |---------|----------| | `zh_ttp229_get_stats()` | Возвращает указатель на статическую структуру `zh_ttp229_stats_t`, действительную всё время работы приложения. | | `zh_ttp229_reset_stats()` | Сбрасывает все счётчики статистики в ноль. | ### Типы и константы | Элемент | Описание | |---------|----------| | `ZH_TTP229_INIT_CONFIG_DEFAULT()` | Макрос конфигурации по умолчанию (приоритет 1, очередь 5, 8 кнопок, debounce 100 мс, задержка 15 мкс). | | `ZH_TTP229` | База событий `esp_event` для уведомлений о нажатиях. | | `zh_ttp229_work_mode_t` | Режим работы: `ZH_TTP229_8_PAD` (8) или `ZH_TTP229_16_PAD` (16). | | `zh_ttp229_init_config_t` | Конфигурация инициализации (GPIO, режим, debounce, параметры задачи и очереди). | | `zh_ttp229_stats_t` | Статистика ошибок и остатка стека. | | `zh_ttp229_event_on_isr_t` | Полезная нагрузка события: `pad_number` (1-based) и `device_number`. | ## Потокобезопасность 1. **Очередь FreeRTOS** — единственной синхронизирующей примитивом является общая очередь `_queue_handle`: ISR handler и RMT RX callback кладут в неё записи через `xQueueSendFromISR()`, задача обработки забирает через `xQueueReceive()`. Взаимные исключения между задачами пользователя не предусмотрены. 2. **Флаг `rx_needs_reset`** — объявлен как `volatile bool`, устанавливается из ISR и сбрасывается в задаче обработки; используется для отложенного сброса RMT RX канала вне контекста прерывания. 3. **Отключение прерываний GPIO** — на время цикла чтения прерывание SDO отключается (`gpio_intr_disable()`) и включается обратно в задаче обработки после постинга события, что исключает повторный вход в ISR во время обмена. 4. **Удаление** — при переполнении очереди или ошибке RMT прерывания GPIO включаются сразу из ISR, чтобы устройство не осталось «зависшим». ## Алгоритмы ### Опрос кнопок (протокол SCL/SDO) 1. Фронтальный фронт на SDO (контроллер готов передать данные) вызывает `_zh_ttp229_isr_handler()`. 2. ISR вооружает приём одного символа `rmt_receive()` на RX канале (окно сигнала 100 нс – 2 мкс). 3. Кратковременный импульс на SDO (OUTPUT low → high → INPUT) синхронизирует контроллер. 4. Через RMT TX передаются `scl_symbols_count` тактовых символов SCL (каждый: 2 мкс low + 2 мкс high, `init_level = 1`). 5. По завершении приёма RMT RX callback кладёт измеренный символ в очередь. ### Декодирование номера кнопки Номер кнопки определяется по длительности `duration1` принятого символа (в мкс, разрешение RMT 1 МГц). Обозначим `base = rmt_tx_start_delay`. Для кнопки N (1..16) ожидаемое окно: - N = 1: `duration1 ∈ {base-1, base, base+1}` - N > 1: `duration1 ∈ {base + 4·(N-1) - 1, base + 4·(N-1), base + 4·(N-1) + 1}` Тolerance ±1 мкс компенсирует джиттер. Распознанный номер больше `scl_symbols_count` отбрасывается. ### Debounce - Событие публикуется, если с предыдущего события прошло не менее `pdMS_TO_TICKS(debounce_time)` тиков, либо событие первое (`last_event_tick == -1`). - Окно сравнивается по арифметике тиков без вычитания переполнения (`now - last_event_tick`), что корректно при переполнении счётчика. - подавленные события не обновляют `last_event_tick`, поэтому кнопка «срабатывает» сразу после окончания окна. ### Восстановление RX канала - При ошибке `rmt_receive()`/`rmt_transmit()` устанавливается `rx_needs_reset` и в очередь ставится пустая запись-уведомление. - Задача обработки выполняет `rmt_disable()` + `rmt_enable()` для RX канала вне ISR. ## Примеры использования ### Базовый пример ```c #include "zh_ttp229.h" static zh_ttp229_handle_t *ttp229_handle = NULL; void touch_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { zh_ttp229_event_on_isr_t *event = event_data; printf("Устройство %d: нажата кнопка %d\n", event->device_number, event->pad_number); } void app_main(void) { esp_event_loop_create_default(); esp_event_handler_instance_register(ZH_TTP229, ESP_EVENT_ANY_ID, &touch_handler, NULL, NULL); zh_ttp229_init_config_t config = ZH_TTP229_INIT_CONFIG_DEFAULT(); config.scl_gpio = GPIO_NUM_25; config.sdo_gpio = GPIO_NUM_27; config.work_mode = ZH_TTP229_16_PAD; config.rmt_tx_start_delay = 23; // подбирается экспериментально if (zh_ttp229_init(&config, &ttp229_handle) != ESP_OK) { return; // обработка ошибки инициализации } // ... работа ... zh_ttp229_deinit(&ttp229_handle); // очистка ресурсов } ``` ### Работа с несколькими устройствами ```c #include "zh_ttp229.h" #define DEV1 0x01 #define DEV2 0x02 static zh_ttp229_handle_t *dev1 = NULL; static zh_ttp229_handle_t *dev2 = NULL; void app_main(void) { esp_event_loop_create_default(); zh_ttp229_init_config_t config1 = ZH_TTP229_INIT_CONFIG_DEFAULT(); config1.scl_gpio = GPIO_NUM_25; config1.sdo_gpio = GPIO_NUM_26; config1.device_number = DEV1; zh_ttp229_init_config_t config2 = ZH_TTP229_INIT_CONFIG_DEFAULT(); config2.scl_gpio = GPIO_NUM_27; config2.sdo_gpio = GPIO_NUM_22; config2.device_number = DEV2; zh_ttp229_init(&config1, &dev1); zh_ttp229_init(&config2, &dev2); // общее ресурсы не пересоздаются // ... работа ... zh_ttp229_deinit(&dev2); zh_ttp229_deinit(&dev1); // последний объект удаляет задачу и очередь } ``` ### Мониторинг статистики ```c #include "zh_ttp229.h" void stats_task(void *arg) { for (;;) { const zh_ttp229_stats_t *stats = zh_ttp229_get_stats(); if (stats->rmt_driver_error > 0 || stats->queue_overflow_error > 0) { printf("Ошибки RMT: %lu, переполнения очереди: %lu\n", stats->rmt_driver_error, stats->queue_overflow_error); zh_ttp229_reset_stats(); // сброс счётчиков } vTaskDelay(pdMS_TO_TICKS(60000)); } } ``` ## Логирование Компонент использует ESP-IDF логирование с тегом `"zh_ttp229"`. ```c esp_log_level_set("zh_ttp229", ESP_LOG_ERROR); ``` Также компонент зависит от `zh_vector` с тегом `"zh_vector"`, который стоит приглушить отдельно. ## Ошибки | Код | Описание | |-----|----------| | `ESP_OK` | Успех | | `ESP_ERR_INVALID_ARG` | NULL-аргумент, значение вне допустимого диапазона, дублирующийся `device_number` | | `ESP_ERR_INVALID_STATE` | `*handle` не равен `NULL` при вызове `zh_ttp229_init()` | | `ESP_ERR_NO_MEM` | Не удалось выделить память под дескриптор | | `ESP_FAIL` | Ошибка создания очереди/задачи, инициализации GPIO, ISR-сервиса или RMT; устройство не найдено в векторе при деинициализации | ## Ограничения 1. **Первое нажатие после старта/сброса не работает** — особенность контроллера TTP229. 2. **Мультитач не поддерживается** — за один цикл декодируется только одна кнопка. 3. **`device_number` должен быть уникальным и > 0** — используется как ключ в векторе устройств. 4. **Общая очередь и задача используют параметры первого устройства** — `queue_size`, `stack_size` и `task_priority` последующих устройств игнорируются. 5. **`rmt_tx_start_delay` подбирается индивидуально** — минимум 15 мкс, точность зависит от платы; определяется экспериментально или логическим анализатором. 6. **Первое нажатие после инициализации** может быть потеряно из-за синхронизации контроллера. 7. **Потокобезопасность ограничена** — функции `init`/`deinit` не защищены от одновременного вызова из нескольких задач. 8. **`min_stack_size` обновляется только при событиях** — при отсутствии нажатий значение остаётся равным размеру стека из конфигурации. ## Требования для сборки - ESP-IDF >= 5.0 (в `CMakeLists.txt` учтены различия `driver` и `esp_driver_rmt`/`esp_driver_gpio` для IDF 6.x) - FreeRTOS (входит в состав ESP-IDF) - `esp_event` (входит в состав ESP-IDF) - `zh_vector` — динамический вектор (подтягивается автоматически через `idf_component.yml`) ## Установка ```bash cd ваш_проект/components git clone https://github.com/aZholtikov/zh_ttp229 ``` ```c #include "zh_ttp229.h" ```