Files
ate0005/wiki_ru.md
T
2026-11-04 09:12:31 +03:00

26 KiB
Raw Blame History

Описание компонента ate0005

Общие сведения

ATE0005 — это прошивка ESP32 для ESP-IDF, реализующая универсальный оптический двухканальный угломер/тахометр.

Приложение измеряет угол поворота оси и/или скорость её вращения по двум независимым каналам A и B, подключённым к оптическим энкодерам, и выводит результат на LCD-экран 16x2. Управление приборами осуществляется поворотным энкодером MODE с кнопкой.

Основные возможности

  1. Два независимых канала — каналы A и B работают с собственными оптическими энкодерами.
  2. Четыре режима работы — ANGLE/ANGLE, TACHO/TACHO, ANGLE/TACHO, REMOTE.
  3. Измерение угла — текущий угол с разрешением 0.1°, сброс длительным нажатием кнопки.
  4. Измерение скорости — обороты в минуту (RPM) с обновлением каждые 100 мс.
  5. LCD 16x2 через PCF8574 — заставка, анимированный прогресс-бар загрузки, живой вывод значений.
  6. Меню на поворотном энкодере — выбор режима поворотом, вход/выход коротким нажатием.
  7. WiFi AP+STA — точка доступа для локального доступа и подключение к внешней сети.
  8. OTA-обновления — встроенный веб-сервер с самопроверкой образа и откатом (rollback).
  9. Удалённый syslog — периодическая отправка статистики задач и счётчиков ошибок драйверов.
  10. FreeRTOS-многозадачность — отдельные задачи на каждый канал, энкодер, расширитель и статистику.

Архитектура прошивки

Приложение состоит из единственного исходного файла main/ate0005.c и заголовка конфигурации main/ate0005.h. Вся прикладная логика построена вокруг компонентов проекта (zh_*), которые выполняют непосредственную работу с железом; ate0005.c отвечает за инициализацию, коммутацию режимов и вывод.

Точка входа app_main() выполняет последовательную инициализацию подсистем:

  1. nvs_flash_init() с очисткой и повторной инициализацией при пустом разделе или несовпадении версии.
  2. esp_event_loop_create_default() — общая петля событий.
  3. _zh_pcf8574_init() — шина I2C, расширитель PCF8574, драйвер LCD, мьютекс экрана.
  4. _zh_system_load() — заставка и прогресс-бар загрузки.
  5. _zh_encoder_init() — энкодер выбора режима с обработчиком событий.
  6. _zh_timer_init() — программный таймер сброса угла.
  7. _zh_a_inclinometer_init(), _zh_b_inclinometer_init() — угломеры каналов A и B.
  8. _zh_wifi_init() — WiFi в режиме AP+STA.
  9. _zh_webserver_init() — HTTP-сервер с OTA-страницей.
  10. _zh_espnow_init() — заготовка под ESP-NOW (пока пуста).

После инициализации выполняется самопроверка OTA: если запущенный образ находится в состоянии ESP_OTA_IMG_PENDING_VERIFY, задача ждёт 60 секунд и помечает образ валидным через esp_ota_mark_app_valid_cancel_rollback().

Задачи FreeRTOS

Задача Приоритет Стек, слов Назначение
_zh_a_inclinometer_processing_task tskIDLE_PRIORITY + 5 configMINIMAL_STACK_SIZE + 512 Вывод угла канала A
_zh_b_inclinometer_processing_task tskIDLE_PRIORITY + 5 configMINIMAL_STACK_SIZE + 512 Вывод угла канала B
_zh_a_tachometer_processing_task tskIDLE_PRIORITY + 5 configMINIMAL_STACK_SIZE + 512 Вывод RPM канала A
_zh_b_tachometer_processing_task tskIDLE_PRIORITY + 5 configMINIMAL_STACK_SIZE + 512 Вывод RPM канала B
_zh_encoder_event_handler обрабатывается в задаче zh_encoder — События энкодера и кнопки
_zh_pcf8574_* (внутри zh_pcf8574) tskIDLE_PRIORITY + 4 configMINIMAL_STACK_SIZE Обмен с расширителем LCD
_zh_stats_processing_task tskIDLE_PRIORITY configMINIMAL_STACK_SIZE + 1280 Отправка статистики в syslog

Все задачи создаются через xTaskCreatePinnedToCore() с tskNO_AFFINITY, то есть планировщик сам выбирает ядро.

Внутреннее устройство

Структура _task_stats_t

Хранит минимальный свободный объём стека (high water mark) каждой измерительной задачи. Статистическая задача читает эти значения для отправки в syslog.

Поле Тип Описание
a_inclinometer_task_min_stack_size uint32_t Минимальный свободный стек задачи угломера A
b_inclinometer_task_min_stack_size uint32_t Минимальный свободный стек задачи угломера B
a_tachometer_task_min_stack_size uint32_t Минимальный свободный стек задачи тахометра A
b_tachometer_task_min_stack_size uint32_t Минимальный свободный стек задачи тахометра B

Статический экземпляр _task_stats инициализируется полными размерами стеков, чтобы до первого замера статистика не давала ложных предупреждений.

Глобальные дескрипторы и флаги

Переменная Тип Описание
_http_server_handle httpd_handle_t Дескриптор HTTP-сервера
_lcd_handle zh_pcf8574_handle_t * Расширитель PCF8574
_lcd_160x_handle zh_160x_i2c_handle_t * Драйвер LCD 16x2
_mode_encoder_handle zh_encoder_handle_t * Энкодер выбора режима
_a_inclinometer_handle, _b_inclinometer_handle zh_inclinometer_handle_t * Угломеры каналов A/B
_a_tachometer_handle, _b_tachometer_handle zh_tachometer_handle_t * Тахометры каналов A/B
_inclinometer_reset_timer_handle esp_timer_handle_t Таймер сброса угла
_lcd_mutex SemaphoreHandle_t Мьютекс доступа к LCD
_is_menu_mode volatile bool Флаг нахождения в меню выбора режима
_is_reset_mode volatile bool Флаг нажатия кнопки (идёт отсчёт долгого нажатия)
_is_reset_occurred volatile bool Признак того, что сброс уже выполнен, чтобы отпустить кнопку «вхолостую»
_current_menu_value volatile uint8_t Индекс выбранного режима
_wifi_reconnect_retry_num volatile uint8_t Счётчик попыток переподключения STA
_is_wifi_connected volatile bool Флаг подключения STA

Все разделяемые между задачами и обработчиками прерываний флаги объявлены volatile, поскольку изменяются из разных контекстов без блокировок.

Константы конфигурации

Режимы работы заданы константами и подписями меню:

Константа Значение Подпись меню
MODE_ANGLE_ANGLE 0 "ANGLE/ANGLE"
MODE_TACHO_TACHO 1 "TACHO/TACHO"
MODE_ANGLE_TACHO 2 "ANGLE/TACHO"
MODE_REMOTE 3 "REMOTE"

Массив _main_menu[] хранит подписи в порядке их индексов; энкодер режима ограничен диапазоном 0 .. sizeof(_main_menu)/sizeof(_main_menu[0]) - 1.

Распиновка GPIO

Периферия Вывод A Вывод B Вывод S Примечание
I2C-шина SDA = GPIO_NUM_21 SCL = GPIO_NUM_22 — Внутренние подтяжки включены
Расширитель LCD адрес 0x20 — — PCF8574 (U2)
Энкодер MODE GPIO_NUM_27 GPIO_NUM_26 GPIO_NUM_25 Разъём EN1, экземпляр 1
Оптический энкодер A GPIO_NUM_14 GPIO_NUM_13 — Разъём EN2
Оптический энкодер B GPIO_NUM_18 GPIO_NUM_19 — Разъём EN3

Оптическое устройство каждого канала общее для угломера и тахометра этого канала: они не могут работать одновременно на одном канале.

API (интерфейс)

Прикладной код ate0005.c не экспортирует публичных функций — это законченное приложение, а не библиотека. Все функции модуля объявлены static и вызываются только из app_main() и обработчиков событий. Ниже приведена их роль как внутренняя справка.

Инициализация подсистем

Функция Описание
app_main() Точка входа: NVS, событийная петля, LCD, энкодер, таймер, угломеры, WiFi, веб-сервер, ESP-NOW, самопроверка OTA.
_zh_pcf8574_init() Создаёт шину I2C, инициализирует PCF8574 и LCD 16x2, создаёт мьютекс экрана.
_zh_encoder_init() Регистрирует обработчик энкодера, задаёт GPIO, диапазон и шаг, сбрасывает позицию в 0.
_zh_timer_init() Создаёт одноразовый программный таймер сброса угла.
_zh_a_inclinometer_init() Инициализирует угломер канала A и создаёт его задачу.
_zh_b_inclinometer_init() Инициализирует угломер канала B и создаёт его задачу.
_zh_a_tachometer_init() Инициализирует тахометр канала A и создаёт его задачу.
_zh_b_tachometer_init() Инициализирует тахометр канала B и создаёт его задачу.
_zh_wifi_init() Настраивает AP и STA, регистрирует обработчики событий WiFi/IP, запускает WiFi.
_zh_webserver_init() Запускает HTTP-сервер и регистрирует OTA-страницу на "/".
_zh_espnow_init() Заготовка под ESP-NOW, тело пусто.

Обработчики событий

Функция Описание
_zh_encoder_event_handler() Обрабатывает нажатие/отпускание кнопки и поворот энкодера: переключение меню, сброс угла.
_zh_wifi_event_handler() Подключение STA, переподключение с ограничением попыток, инициализация syslog и создание задачи статистики при получении IP.
_zh_espnow_event_handler() Заготовка под ESP-NOW, тело пусто.
_zh_inclinometer_reset_timer_on_alarm_cb() Колбэк долгого нажатия: сбрасывает угол активных угломеров текущего режима.

Задачи обработки и вывода

Функция Описание
_zh_a_inclinometer_processing_task() Выводит угол канала A на 2-ю строку LCD с 1 знаком после запятой.
_zh_b_inclinometer_processing_task() Выводит угол канала B на 2-ю строку LCD с 1 знаком после запятой.
_zh_a_tachometer_processing_task() Выводит RPM канала A на 2-ю строку LCD.
_zh_b_tachometer_processing_task() Выводит RPM канала B на 2-ю строку LCD.
_zh_stats_processing_task() Периодически отправляет свободный стек задач и счётчики ошибок в syslog.

Служебные функции вывода

Функция Описание
_zh_system_load() Показывает заставку с названием и версией, затем прогресс-бар загрузки.

Потокобезопасность

  1. Мьютекс _lcd_mutex — защищает последовательности обращений к LCD (set_cursor → print_*) от одновременного доступа нескольких задач. Захватывается на время вывода значения и освобождается сразу после.
  2. volatile флаги состояния — _is_menu_mode, _is_reset_mode, _is_reset_occurred, _current_menu_value изменяются из обработчика событий энкодера и читаются из задач и колбэка таймера; прямой доступ без блокировки допустим для однобайтовых флагов.
  3. _task_stats — каждая измерительная задача пишет только своё поле, читатель (статистическая задача) только читает; гонка исключена за счёт разделения полей.
  4. Событийная модель zh_encoder — прерывания энкодера постят события в очередь, обработка идёт в задаче компонента, а не в ISR.

Алгоритмы

Переключение режима работы

  1. Короткое нажатие кнопки энкодера инвертирует _is_menu_mode.
  2. При входе в меню текущие активные устройства режима останавливаются: удаляются соответствующие задачи через vTaskDelete(), затем вызываются zh_inclinometer_deinit() / zh_tachometer_deinit().
  3. Включается курсор LCD, очищается 2-я строка, на 1-й отображается текущая позиция меню.
  4. При выходе из меню курсор выключается, инициализируются устройства выбранного режима и запускаются их задачи.
  5. Поворот энкодера в меню обновляет _current_menu_value и подпись режима; вне меню позиция энкодера принудительно возвращается к _current_menu_value через zh_encoder_set().

Сброс угла долгим нажатием

  1. При нажатии кнопки запускается одноразовый таймер на 2 секунды и выставляется _is_reset_mode.
  2. Если кнопка отпущена раньше 2 секунд, таймер останавливается, событие считается коротким нажатием (переключение меню).
  3. Если таймер сработал, колбэк выставляет _is_reset_occurred и вызывает zh_inclinometer_reset() для активных угломеров режима (в ANGLE/ANGLE — оба, в ANGLE/TACHO — только A).
  4. Последующее отпускание кнопки игнорируется, если сброс уже произошёл (_is_reset_occurred == true).

Обновление значения на LCD

  1. Задача кэширует предыдущее показание (prev_angle / prev_rpm).
  2. Каждые 100 мс читается новое значение через zh_inclinometer_get() / zh_tachometer_get().
  3. Значение перерисовывается только при изменении: сначала область значения затишается пробелами, затем печатается новое число. Это снижает нагрузку на I2C и мигание экрана.

Статистика и syslog

  1. Задача статистики создаётся только после получения STA-адреса (IP_EVENT_STA_GOT_IP).
  2. Каждые ~60 секунд (с промежуточными задержками по 500 мс) отправляются сообщения о свободном стеке каждой задачи и о счётчиках ошибок zh_encoder и zh_pcf8574.
  3. Уровень ZH_WARNING ставится, если свободный стек задачи меньше 100 слов или счётчик ошибок ненулевой; иначе ZH_INFO.
  4. При разрыве WiFi задача статистики удаляется, syslog деинициализируется.

Примеры использования

Инициализация угломера канала

#include "zh_inclinometer.h"

static zh_inclinometer_handle_t *_a_inclinometer_handle = NULL;

void app_main(void)
{
    zh_inclinometer_init_config_t config = ZH_INCLINOMETER_INIT_CONFIG_DEFAULT();
    config.a_gpio_number = A_OPTICAL_DEVICE_A_GPIO; // GPIO_NUM_14
    config.b_gpio_number = A_OPTICAL_DEVICE_B_GPIO; // GPIO_NUM_13
    config.encoder_pulses = 3600;                   // импульсов на оборот
    zh_inclinometer_init(&config, &_a_inclinometer_handle);
    // ... использование ...
    zh_inclinometer_deinit(&_a_inclinometer_handle); // освобождение ресурсов
}

Вывод угла на LCD под мьютексом

#include "zh_160x_i2c.h"

static zh_160x_i2c_handle_t *_lcd_160x_handle = NULL;
static SemaphoreHandle_t _lcd_mutex = NULL;

void app_main(void)
{
    _lcd_mutex = xSemaphoreCreateMutex();
    float current_angle = 0.0;
    for (;;)
    {
        // читаем угол и выводим только при изменении
        zh_inclinometer_get(&_a_inclinometer_handle, &current_angle);
        xSemaphoreTake(_lcd_mutex, portMAX_DELAY);
        zh_160x_set_cursor(&_lcd_160x_handle, 1, 2);
        zh_160x_print_float(&_lcd_160x_handle, current_angle, 1);
        xSemaphoreGive(_lcd_mutex);
        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
    vSemaphoreDelete(_lcd_mutex);
}

Инициализация удалённого syslog

#include "zh_syslog.h"

void app_main(void)
{
    zh_syslog_init_config_t config = ZH_SYSLOG_INIT_CONFIG_DEFAULT();
    strcpy(config.syslog_ip, SYSLOG_IP); // адрес сервера из ate0005.h
    esp_err_t err = zh_syslog_init(&config);
    if (err != ESP_OK)
    {
        printf("Syslog initialization error %s\n", esp_err_to_name(err));
        return;
    }
    zh_syslog_send(ZH_KERN, ZH_INFO, "ATE0005", "zh_syslog", "Stats task started.\n");
    zh_syslog_deinit(); // освобождение ресурсов
}

Конфигурация сборки

Прошивка опирается на следующие параметры sdkconfig.defaults:

Параметр Значение Назначение
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE y Разрешает OTA-откат при непройденной самопроверке
CONFIG_BOOTLOADER_LOG_LEVEL_NONE y Отключает лог загрузчика
CONFIG_ESPTOOLPY_FLASHSIZE_8MB 8MB Размер флеш под две OTA-партиции
CONFIG_PARTITION_TABLE_CUSTOM y Используется partitions.csv
CONFIG_COMPILER_OPTIMIZATION_SIZE y Оптимизация по размеру
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240 240 Максимальная частота CPU
CONFIG_FREERTOS_HZ 1000 Тик 1 мс для точных задержек
CONFIG_LOG_DEFAULT_LEVEL_NONE y Отключает ESP-IDF лог по умолчанию

Партиции (partitions.csv)

Имя Тип Подтип Размер Назначение
nvs data nvs 0x6000 Хранилище настроек
otadata data ota 0x2000 Состояние OTA
phy_init data phy 0x1000 Калибровка PHY
ota_0 app ota_0 3500K Образ прошивки A
ota_1 app ota_1 3500K Образ прошивки B
storage data fat 1M Файловое хранилище

Ошибки

Прикладной код не возвращает пользовательских кодов ошибок и полагается на коды ESP-IDF, транслируемые компонентами zh_*. Ниже типовые коды, встречающиеся в путях инициализации.

Код Описание
ESP_OK Операция выполнена успешно
ESP_ERR_INVALID_ARG Неверный аргумент функции (например, нулевой дескриптор)
ESP_ERR_NO_MEM Недостаточно памяти
ESP_ERR_NOT_FOUND Устройство или партиция не найдены
ESP_ERR_TIMEOUT Таймаут операции (I2C, WiFi)
ESP_ERR_NVS_NO_FREE_PAGES Раздел NVS переполнен (требует очистки)
ESP_ERR_NVS_NEW_VERSION_FOUND Версия формата NVS не совпадает (требует очистки)

Ограничения

  1. Режим REMOTE не реализован — функции _zh_espnow_init() и _zh_espnow_event_handler() имеют пустое тело, поэтому в этом режиме на экране выводится NOT CONNECTED.
  2. Общее оптическое устройство на канал — угломер и тахометр одного канала используют одни GPIO, поэтому не могут работать одновременно; при переключении режима предыдущие устройства деинициализируются.
  3. WiFi-кредиты зашиты в прошивку — WIFI_STA_SSID и WIFI_STA_PASS заданы константами в ate0005.h и требуют пересборки для смены сети.
  4. Жёсткая распиновка GPIO — номера выводов соответствуют разъёмам платы ATE0005 и не должны меняться без аппаратной ревизии.
  5. Самопроверка OTA блокирует на 60 секунд — при первом запуске после OTA-обновления app_main() выдерживает задержку перед подтверждением образа.
  6. Порог предупреждения стека — 100 слов — статистика выдаёт ZH_WARNING при свободном стеке ниже 100 слов; значение подобрано под конкретные размеры стеков задач.

Требования для сборки

  • ESP-IDF версии 5.0 или выше.
  • FreeRTOS (входит в ESP-IDF).
  • Компоненты проекта: zh_160x_i2c, zh_encoder, zh_inclinometer, zh_tachometer, zh_espnow, zh_ota_server, zh_syslog.
  • Встроенные компоненты ESP-IDF: esp_wifi, nvs_flash, esp_timer, esp_app_format.
  • Флеш-память 8 МБ под две OTA-партиции по 3500K.

Установка

cd your_projects_folder
git clone https://github.com/aZholtikov/ate0005
cd ate0005
idf.py set-target esp32
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

Зависимости-компоненты подтягиваются автоматически из main/idf_component.yml при первой сборке. Для подключения в собственном коде используется единый заголовок конфигурации:

#include "ate0005.h"