style: added docs

This commit is contained in:
aZholtikov committed 2026-11-04 09:12:31 +03:00
1 parent b8aa3a594d
commit b9fc3a6f41
17 files changed
+824 -244

No files matched your search

+337
View File
@@ -0,0 +1,337 @@
# Описание компонента 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 деинициализируется.
## Примеры использования
### Инициализация угломера канала
```c
#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 под мьютексом
```c
#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
```c
#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.
## Установка
```bash
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` при первой сборке. Для подключения в собственном коде используется единый заголовок конфигурации:
```c
#include "ate0005.h"
```