# Описание компонента zh_pcf8574 ## Общие сведения **zh_pcf8574** — это драйвер для ESP-IDF, реализующий работу с 8-битным I2C GPIO-экспандером PCF8574. Компонент поддерживает чтение и запись состояний GPIO-пинов, обработку прерываний с определением уровня сигнала, а также предоставляет статистику для отладки и мониторинга. Драйвер создаёт внутренний FreeRTOS-задачу для обработки прерываний от всех подключённых устройств PCF8574 и использует событийную модель на базе `esp_event`. ## Основные возможности 1. **I2C-коммуникация** — работа через новый I2C master driver из ESP-IDF v5. 2. **Конфигурация направления GPIO** — каждая пин (P0–P7) может быть настроена как вход или выход. 3. **Обработка прерываний** — обнаружение изменения уровня на входных пинах с временной меткой. 4. **Событийная архитектура** — интеграция с `esp_event` для асинхронной обработки событий. 5. **Статистика ошибок** — подсчёт ошибок I2C, очереди, вектора и мониторинг стека задачи. 6. **Поддержка нескольких устройств** — работа с несколькими PCF8574 на одной I2C-шине с разными адресами. ## Внутреннее устройство ### Структура `zh_pcf8574_handle_t` Публичная непрозрачная структура-хендл, используемая всеми API-функциями для идентификации экземпляра устройства. | Поле | Тип | Описание | |------|-----|----------| | `i2c_address` | `uint8_t` | 7-битный I2C-адрес данного экземпляра устройства | ### Структура `_zh_pcf8574_vector_data_t` Внутренняя структура данных, хранимая в векторе `zh_vector` для каждого подключённого устройства. | Поле | Тип | Описание | |------|-----|----------| | `i2c_address` | `uint8_t` | I2C-адрес устройства | | `gpio_work_mode` | `uint8_t` | Маска направлений GPIO (1 = вход, 0 = выход) | | `gpio_status` | `uint8_t` | Текущее состояние GPIO-портов | | `dev_handle` | `i2c_master_dev_handle_t` | I2C-хендл устройства для коммуникации | ### Управление памятью - Хендл устройства (`zh_pcf8574_handle_t`) выделяется через `heap_caps_calloc()` при инициализации и освобождается через `heap_caps_free()` при деинициализации. - Состояние всех устройств хранится в глобальном векторе `_vector` типа `zh_vector_t`, реализованном в компоненте `zh_vector`. - Вектор инициализируется при первом вызове `zh_pcf8574_init()` и освобождается при удалении последнего устройства в `zh_pcf8574_deinit()`. - При удалении устройства данные удаляются из вектора через `zh_vector_delete_item()`. ### Глобальные ресурсы - `_vector` — глобальный вектор для хранения данных всех подключённых устройств. - `_interrupt_semaphore` — бинарный семафор FreeRTOS для синхронизации ISR и задачи обработки прерываний. - `_interrupt_gpio` — номер GPIO, используемого для прерываний (GPIO_NUM_MAX, если не используется). - `_gpio_matrix` — статический массив `{0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}` для побитовой маскировки пинов. - `_stats` — глобальная структура статистики ошибок. - `zh_pcf8574` — глобальный хендл FreeRTOS-задачи обработки прерываний. ## API (интерфейс) ### Инициализация и освобождение | Функция | Описание | |---------|----------| | `zh_pcf8574_init()` | Инициализирует устройство PCF8574: создаёт I2C-соединение, настраивает GPIO и при необходимости создаёт задачу обработки прерываний. | | `zh_pcf8574_deinit()` | Освобождает I2C-ресурсы и хендл устройства. Удаляет задачу обработки прерываний только при удалении последнего экземпляра. | ### Чтение и запись | Функция | Описание | |---------|----------| | `zh_pcf8574_read()` | Считывает байт с входных портов PCF8574 через I2C и обновляет внутреннее состояние устройства. | | `zh_pcf8574_write()` | Записывает байт в выходные порты PCF8574 через I2C и обновляет внутреннее состояние устройства. | | `zh_pcf8574_reset()` | Сбрасывает все выходные пины в низкий уровень, а входные — в высокий, записывая маску направлений в порт. | ### Доступ к отдельным пинам | Функция | Описание | |---------|----------| | `zh_pcf8574_read_gpio()` | Считывает текущий логический уровень указанного GPIO-пина. | | `zh_pcf8574_write_gpio()` | Устанавливает логический уровень указанного GPIO-пина, настроенного как выход. | ### Статистика | Функция | Описание | |---------|----------| | `zh_pcf8574_get_stats()` | Возвращает указатель на структуру со счётчиками ошибок и данными мониторинга. | | `zh_pcf8574_reset_stats()` | Сбрасывает все счётчики ошибок и данные мониторинга. | ## Потокобезопасность 1. **Семафор FreeRTOS** — бинарный семафор `_interrupt_semaphore` защищает синхронизацию между ISR (вызываемой по прерыванию) и задачей `_zh_pcf8574_isr_processing_task`. ISR отправляет сигнал через `xSemaphoreGiveFromISR()`, задача блокируется на `xSemaphoreTake()`. 2. **Глобальный вектор** — доступ к вектору `_vector` не защищён мьютексом. Все операции с вектором выполняются либо в контексте API-функций (синхронных), либо в контексте задачи обработки прерываний. Одновременный доступ к вектору из разных задач не предусмотрен. 3. **Глобальная статистика** — счётчики в `_stats` обновляются без блокировок. Чтение и запись происходят атомарно на уровне `uint32_t`, что безопасно для ESP32. ## Алгоритмы обработки прерываний ### Задача обработки прерываний 1. Задача блокируется на семафоре `xSemaphoreTake(_interrupt_semaphore, portMAX_DELAY)`. 2. При получении сигнала задача считывает размер вектора устройств. 3. Для каждого устройства в векторе: - Считываются старые данные из вектора. - Выполняется чтение регистра через I2C (`_zh_pcf8574_read_register()`). - Данные обновляются в векторе через `zh_vector_change_item()`. 4. Для каждого пина, настроенного как вход: - Сравниваются биты старого и нового значения регистра. - При обнаружении различий формируется событие `zh_pcf8574_event_on_isr_t` с временной меткой, адресом устройства, номером пина и текущим уровнем. - Событие отправляется через `esp_event_post()` с таймаутом 10 мс. 5. После обработки всех устройств обновляется статистика минимального размера стека задачи (`uxTaskGetStackHighWaterMark()`). ### Запись в регистр При записи в регистр (`zh_pcf8574_write()` и `zh_pcf8574_write_gpio()`) значение маскируется с маской направлений (`gpio_work_mode`), чтобы входные пины не были перезаписаны нулём. Это гарантирует, что только выходные пины изменяют состояние. ## Примеры использования ### Базовый пример: инициализация и чтение/запись ```c #include "zh_pcf8574.h" #define I2C_PORT (I2C_NUM_MAX - 1) zh_pcf8574_handle_t *pcf8574_handle = NULL; void app_main(void) { // Настройка уровня логирования esp_log_level_set("zh_pcf8574", ESP_LOG_ERROR); // Настройка I2C-шины i2c_master_bus_config_t i2c_bus_config = { .clk_source = I2C_CLK_SRC_DEFAULT, .i2c_port = I2C_PORT, .scl_io_num = GPIO_NUM_22, .sda_io_num = GPIO_NUM_21, .glitch_ignore_cnt = 7, .flags.enable_internal_pullup = true, }; i2c_master_bus_handle_t i2c_bus_handle = NULL; i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle); // Инициализация конфигурации PCF8574 zh_pcf8574_init_config_t config = ZH_PCF8574_INIT_CONFIG_DEFAULT(); config.i2c_handle = i2c_bus_handle; config.i2c_address = 0x38; config.p0_gpio_work_mode = ZH_PCF8574_GPIO_INPUT; config.p1_gpio_work_mode = ZH_PCF8574_GPIO_OUTPUT; // Инициализация устройства esp_err_t err = zh_pcf8574_init(&config, &pcf8574_handle); if (err != ESP_OK) { ESP_LOGE("APP", "Failed to initialize PCF8574: %s", esp_err_to_name(err)); return; } // Чтение текущего состояния портов uint8_t reg = 0; zh_pcf8574_read(&pcf8574_handle, ®); // Запись значения в порты (P7=1, P1=1, P0=0) zh_pcf8574_write(&pcf8574_handle, 0b10000010); // Деинициализация устройства zh_pcf8574_deinit(&pcf8574_handle); } ``` ### Работа с отдельными пинами ```c #include "zh_pcf8574.h" zh_pcf8574_handle_t *pcf8574_handle = NULL; void app_main(void) { // ... инициализация I2C и PCF8574 ... bool gpio_status = false; // Чтение состояния пина P0 zh_pcf8574_read_gpio(&pcf8574_handle, ZH_PCF8574_GPIO_NUM_P0, &gpio_status); printf("P0 status: %d\n", gpio_status); // Установка пина P7 в высокий уровень zh_pcf8574_write_gpio(&pcf8574_handle, ZH_PCF8574_GPIO_NUM_P7, ZH_PCF8574_GPIO_HIGH); // Сброс пина P4 в низкий уровень zh_pcf8574_write_gpio(&pcf8574_handle, ZH_PCF8574_GPIO_NUM_P4, ZH_PCF8574_GPIO_LOW); // Чтение состояния пина P4 после записи zh_pcf8574_read_gpio(&pcf8574_handle, ZH_PCF8574_GPIO_NUM_P4, &gpio_status); printf("P4 status: %d\n", gpio_status); // Сброс всех пинов zh_pcf8574_reset(&pcf8574_handle); } ``` ### Обработка прерываний ```c #include "zh_pcf8574.h" zh_pcf8574_handle_t *pcf8574_handle = NULL; // Регистратор обработчика событий void zh_pcf8574_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); void app_main(void) { // ... инициализация I2C ... // Создание событийной петли esp_event_loop_create_default(); // Регистрация обработчика событий PCF8574 esp_event_handler_instance_register(ZH_PCF8574, ESP_EVENT_ANY_ID, &zh_pcf8574_event_handler, NULL, NULL); // Инициализация с поддержкой прерываний zh_pcf8574_init_config_t config = ZH_PCF8574_INIT_CONFIG_DEFAULT(); config.i2c_handle = i2c_bus_handle; config.i2c_address = 0x38; config.p0_gpio_work_mode = ZH_PCF8574_GPIO_INPUT; // Вход для прерываний config.interrupt_gpio = GPIO_NUM_14; // GPIO для прерывания zh_pcf8574_init(&config, &pcf8574_handle); for (;;) { vTaskDelay(60000 / portTICK_PERIOD_MS); } } // Обработчик событий прерываний void zh_pcf8574_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { zh_pcf8574_event_on_isr_t *event = (zh_pcf8574_event_on_isr_t *)event_data; printf("Interrupt on 0x%02X, GPIO %d, level %d, time %lld us\n", event->i2c_address, event->gpio_number, event->gpio_level, event->interrupt_time); } ``` ## Логирование Компонент использует ESP-IDF логирование с тегом `"zh_pcf8574"`. ```c // Установка уровня логирования ERROR esp_log_level_set("zh_pcf8574", ESP_LOG_ERROR); // Установка уровня логирования INFO esp_log_level_set("zh_pcf8574", ESP_LOG_INFO); ``` Макросы логирования: - `ZH_LOGI` — информационные сообщения (через `ESP_LOGI`). - `ZH_LOGE` — сообщения об ошибках с кодом ошибки, файлом и строкой (через `ESP_LOGE`). ## Ошибки | Код | Описание | |-----|----------| | `ESP_OK` | Успешное выполнение | | `ESP_ERR_INVALID_ARG` | Неверный аргумент (NULL-указатели, недопустимые значения) | | `ESP_ERR_INVALID_STATE` | Устройство уже инициализировано | | `ESP_ERR_NOT_FOUND` | Хендл не соответствует валидному экземпляру устройства | | `ESP_ERR_NO_MEM` | Не удалось выделить память | | `ESP_FAIL` | Ошибка I2C-коммуникации, конфигурации или создания задачи | ## Ограничения 1. **Только I2C Fast Mode (100 кГц)** — частота ограничена максимальным значением 100 000 Гц. 2. **Ограниченный диапазон адресов** — поддерживаются только адреса `0x20–0x27` и `0x38–0x3F` (стандартные адреса PCF8574 с тремя адресными пинами). 3. **Одно GPIO для всех прерываний** — все устройства PCF8574 делят один GPIO-пин для прерываний. Все подключённые устройства должны иметь соединённые выводы INT. 4. **Задача обработки прерываний создаётся один раз** — задача создаётся только при инициализации первого устройства с прерываниями. Все последующие устройства используют ту же задачу. 5. **Не IRAM-safe** — обработка прерываний не выполняется в IRAM. ISR использует `ESP_INTR_FLAG_LOWMED` и не является безопасной для вызова из IRAM-контекста. 6. **Нет мьютекса для вектора** — вектор данных устройств не защищён мьютексом. Одновременный доступ из нескольких задач не поддерживается. 7. **Только отрицательный фронт прерывания** — ISR настроен на детектирование отрицательного фронта (`GPIO_INTR_NEGEDGE`). ## Требования для сборки - ESP-IDF v5.0+ (с новым I2C master driver) - FreeRTOS (входит в состав ESP-IDF) - Компонент `zh_vector` (встроенный компонент для работы с векторами) - `esp_timer` (для временных меток прерываний) - `esp_event` (событийная framework) ## Установка ```bash cd ваш_проект/components git clone https://github.com/aZholtikov/zh_pcf8574 ``` ```c #include "zh_pcf8574.h" ```