1. Тема дня
Как правильно мыслить embedded-прошивку: слои, события, драйверы и сервисы. Главная идея: прошивка для ESP32 или STM32 - это не один большой файл с GPIO, UART и таймерами, а маленькая система, где каждый блок отвечает за свою область. Типичный набор подсистем в реальном проекте:
- входы светофора через оптопары, ADC или GPIO-расширители;
- ESP32 с ESP-IDF;
- EC25-модем по UART и AT-командам;
- MQTT/TCP/UDP;
- логирование;
- watchdog;
- питание камеры, радара или Jetson;
- HIL-тесты через Raspberry Pi;
- возможный перенос части логики на STM32.
2. Зачем это нужно
Многие проблемы вроде задержек, watchdog reset, зависаний UART, ложных событий, сложного отключения мигания и непонятных состояний появляются не потому, что разработчик “плохо написал один if”, а потому что в проекте смешаны:
- чтение железа;
- фильтрация;
- бизнес-логика;
- транспорт;
- логирование;
- диагностика;
- тайминги;
- восстановление после ошибок.
Плохая логика:
while (1) {
read_adc();
filter_ema();
detect_phase();
if (changed) {
send_tcp();
log_everything();
}
check_modem();
vTaskDelay(pdMS_TO_TICKS(10));
}Поначалу это работает. Потом появляются разные типы входов, UDP-подтверждения, timestamp, watchdog, HIL, CLI, EC25 URC, восстановление связи - и один цикл превращается в клубок. Правильная мысль: Драйвер входов только измеряет сигнал. Детектор фаз только принимает очищенные события. Транспорт только доставляет событие. Модем только поддерживает связь. CLI только управляет и диагностирует.
3. Теория
Удобное деление прошивки на уровни:
Application layer
логика устройства: фаза изменилась, нужно отправить событие,
режим диагностики, аварийная логика
Service layer
modem_service, phase_service, transport_service,
health_service, cli_service
Driver layer
adc_driver, gpio_expander_driver, uart_driver,
opto_input_driver, timer_driver
Hardware / BSP layer
номера GPIO, UART-пины, частоты, особенности платыПравило: Верхний слой может знать о нижнем, но нижний не должен знать о верхнем. Примеры:
- adc_driver не знает, что такое красный/жёлтый/зелёный;
- phase_detector не знает, пришёл сигнал через ADS1115, MCP23017 или GPIO;
- transport_service не знает, как устроена оптопара;
- modem_service не знает, что событие относится к светофору;
- cli_service не ковыряет GPIO напрямую, а вызывает публичные API сервисов.
4. Пример архитектуры для светофора
Плохой путь:
Считать ADS1115 -> отфильтровать -> понять фазу -> отправить по TCP -> залогироватьХороший путь:
input_driver
читает физические каналы
signal_filter
превращает грязный сигнал в стабильный уровень
phase_detector
понимает RED/YELLOW/GREEN/OFF/UNKNOWN
event_bus
рассылает событие "phase changed"
transport_service
отправляет событие по UDP/TCP/MQTT
health_service
следит за ошибками и задержками
cli_service
позволяет посмотреть состояние5. Событийный подход
Polling-подход:
while (1) {
check_inputs();
check_modem();
check_uart();
check_timeout();
}Event-driven подход:
INPUT_LEVEL_CHANGED
PHASE_CHANGED
MODEM_CONNECTED
MODEM_LOST
UDP_ACK_TIMEOUT
WATCHDOG_WARNINGМинимальная структура события:
typedef enum {
APP_EVENT_PHASE_CHANGED,
APP_EVENT_MODEM_READY,
APP_EVENT_MODEM_LOST,
APP_EVENT_UDP_ACK_TIMEOUT,
APP_EVENT_INPUT_FAULT,
} app_event_type_t;
typedef struct {
app_event_type_t type;
int64_t timestamp_us;
union {
struct {
uint8_t channel;
uint8_t old_phase;
uint8_t new_phase;
} phase;
struct {
int error_code;
} fault;
} data;
} app_event_t;6. Быстрый и медленный путь
Для задач со светофором и камерой главный принцип: Момент изменения фазы должен фиксироваться максимально близко к входу, а всё медленное - модем, TCP, MQTT, логи и CLI - должно происходить после этого и не задерживать timestamp. Плохая цепочка:
прочитал вход
-> долго фильтровал
-> долго логировал
-> полез в модем
-> потом записал timestampПравильная цепочка:
прочитал вход
-> быстро зафиксировал timestamp
-> положил событие в очередь
-> транспорт отправляет, повторяет, ждёт ACK и логирует отдельно7. Типичные ошибки
2.7.1 Ошибка 1. Драйвер знает бизнес-логику
Плохо:
if (adc_value > threshold) {
send_red_phase_to_camera();
}Лучше:
input_level_t level = input_driver_get_level(CH_RED);
phase_detector_process(CH_RED, level, timestamp);2.7.2 Ошибка 2. Одна большая task на всё
Одна задача читает ADC, парсит UART, отправляет MQTT, печатает логи и обслуживает CLI. Это почти гарантированный путь к задержкам и watchdog.
2.7.3 Ошибка 3. Логи внутри быстрого пути
Логи через UART могут сильно влиять на тайминги. В быстром пути лучше считать счётчики, а печатать агрегированно раз в секунду или по CLI-команде.
2.7.4 Ошибка 4. Событие хранит мало информации
Плохо:
phase = GREEN;Лучше:
phase_event = {
.old_phase = RED,
.new_phase = GREEN,
.timestamp_us = esp_timer_get_time(),
.source = INPUT_SOURCE_OPTO,
.confidence = 95,
.raw_mask = 0b0010,
};2.7.5 Ошибка 5. Логика зависит от ESP-IDF/STM32 HAL напрямую
Если phase_detector.c включает driver/gpio.h, esp_log.h или stm32_hal_gpio.h, его сложно тестировать на ПК и переносить. Лучше чистая функция:
phase_state_t phase_detector_update(
phase_detector_t *det,
input_snapshot_t input,
int64_t timestamp_us
);8. Практическое задание
Создай файл ARCHITECTURE.md. Минимальная структура:
# Firmware architecture
## Layers
### BSP
Pins, UART numbers, board-specific constants.
### Drivers
ADC, GPIO expander, UART, timers.
### Services
Phase detector, modem, transport, CLI, health monitor.
### Application
Starts services and connects events.
## Fast path
Input sample -> timestamp -> phase detector -> event queue.
## Slow path
Transport, logging, diagnostics, retries.
## Rules
1. Drivers do not know business logic.
2. Services communicate through events.
3. Timestamp is captured before network sending.
4. Logs must not block fast path.Затем найди в текущем проекте 3 нарушения архитектуры:
- драйвер вызывает сетевую отправку;
- логика фазы лежит внутри ADC-кода;
- transport читает глобальные переменные;
- CLI напрямую меняет внутренние поля сервисов;
- timestamp ставится слишком поздно.
9. Что попробовать дальше
- Посмотреть официальную документацию ESP-IDF по components и build system.
- Посмотреть esp_event как основу событийного подхода.
- Начать отделять phase_detector от ESP-IDF, чтобы его можно было тестировать на ПК.
10. Короткий итог
Хорошая embedded-прошивка строится не вокруг GPIO, UART и ADC, а вокруг ответственности модулей и событий между ними. Правильный быстрый путь:
вход изменился
-> timestamp зафиксирован
-> новое состояние определено
-> событие положено в очередь
-> сеть, модем и логи работают отдельноЗадание
Разложите чтение входа, timestamp, определение фазы, постановку события в очередь, AT-команды, повтор сетевой отправки и печать диагностических логов по быстрому и медленному пути. Назовите владельца каждой операции и одно условие, без которого сама очередь не гарантирует быстрый путь.
Критерии самопроверки: Назовите все семь операций, сохраните timestamp перед медленной работой, не поручайте драйверу транспорт и объясните обработку полной очереди.
Показать ответ автора
Быстрый путь: драйвер читает вход, фиксируется timestamp, детектор обновляет фазу, событие ставится в очередь. Медленный путь: modem_service ведёт AT-команды, transport_service отправляет и повторяет, диагностика агрегирует логи. Быстрый путь не должен неограниченно блокироваться на полной очереди; нужна явная политика переполнения и ограничение ожидания.