1. Тема дня
Как проектировать задачи FreeRTOS в ESP-IDF: сколько задач нужно, какие у них должны быть ответственности, как выбирать приоритеты, где использовать очереди, а где не плодить лишние потоки. Главная мысль: FreeRTOS task - это не “просто поток”. Это владелец конкретной ответственности во времени.
2. Зачем это нужно
ESP32 должен одновременно:
- читать входы фаз светофора;
- фильтровать шумные сигналы после оптопар;
- фиксировать timestamp изменения фазы;
- общаться с EC25 по UART;
- принимать URC от модема;
- отправлять MQTT/TCP/UDP;
- отвечать на CLI;
- управлять питанием;
- не ловить watchdog;
- быть тестируемым через HIL.
Одна задача на всё даст задержки. Слишком много задач даст хаос, гонки и переполнения стеков.
3. Что такое task на практике
Типовая задача:
void my_task(void *arg)
{
while (1) {
do_work();
vTaskDelay(pdMS_TO_TICKS(10));
}
}Но хорошая задача отвечает не за “кусок кода”, а за процесс во времени:
input_task - регулярно снимает входы
phase_task - принимает снимки и определяет фазу
modem_task - обслуживает EC25 и state machine
transport_task - отправляет события наружу
cli_task - обрабатывает команды пользователя
health_task - следит за состоянием системыПлохие названия:
task1
main_loop_task
process_task
logic_taskХорошие названия:
input_scan_task
phase_detector_task
modem_service_task
udp_tx_task
cli_rx_task
health_monitor_task4. Много задач не равно хорошая архитектура
Плохая идея:
red_input_task
yellow_input_task
green_input_task
mqtt_task
tcp_task
udp_task
modem_rx_task
modem_tx_task
cli_rx_task
cli_tx_task
log_task
watchdog_task
adc_task
gpio_task
filter_taskПроблемы:
- много очередей;
- много блокировок;
- сложные зависимости;
- непонятно, кто владеет данными;
- расход RAM на стеки;
- сложнее понять задержку от входа до события.
Правило: Новая task нужна только тогда, когда у блока есть независимая временная жизнь.
5. Базовая схема задач
Для проекта “детектирование фазы + отправка события”:
input_task
-> input_snapshot_queue
phase_task
-> phase_event_queue
transport_task
-> modem_service / udp / tcp / mqtt
modem_task
cli_task
health_taskКлючевой принцип: Изменение фазы не должно ждать модем, MQTT, TCP, логи или CLI.
6. Где ставить timestamp
Плохо:
read_inputs();
filter_inputs();
send_udp();
event.timestamp_us = esp_timer_get_time();Лучше:
input_snapshot_t snapshot = {
.timestamp_us = esp_timer_get_time(),
.raw_mask = read_input_mask(),
};Дальше timestamp проходит через цепочку:
input_snapshot.timestamp_us
-> phase_event.timestamp_us
-> transport_event.timestamp_us
-> UDP/MQTT/TCP payload7. Приоритеты задач
Примерная шкала:
Высокий:
input_task
modem_uart_rx_task или modem_task с быстрой обработкой RX
Средний:
phase_task
transport_task
cli_task
Низкий:
health_task
periodic_log_task
telemetry_taskНе ставь всем высокий приоритет “на всякий случай”. Если задача высокого приоритета крутится без блокировки/yield, она может мешать idle task и привести к watchdog. Плохо:
while (1) {
poll_uart();
parse_everything();
}Лучше:
while (1) {
int len = uart_read_bytes(
MODEM_UART,
buf,
sizeof(buf),
pdMS_TO_TICKS(100)
);
if (len > 0) {
modem_parser_feed(buf, len);
}
}8. Очереди
Очередь нужна, когда один контекст производит данные, а другой потребляет.
typedef struct {
uint32_t raw_mask;
int64_t timestamp_us;
} input_snapshot_t;
static QueueHandle_t input_queue;Отправитель:
void input_task(void *arg)
{
while (1) {
input_snapshot_t s = {
.raw_mask = input_driver_read_mask(),
.timestamp_us = esp_timer_get_time(),
};
xQueueSend(input_queue, &s, 0);
vTaskDelay(pdMS_TO_TICKS(5));
}
}Получатель:
void phase_task(void *arg)
{
input_snapshot_t s;
while (1) {
if (xQueueReceive(input_queue, &s, portMAX_DELAY) == pdTRUE) {
phase_event_t ev;
if (phase_detector_update(&s, &ev)) {
xQueueSend(phase_event_queue, &ev, 0);
}
}
}
}9. Переполнение очередей
Плохой быстрый путь:
xQueueSend(queue, &event, portMAX_DELAY);Если очередь полна, задача зависнет. Лучше:
if (xQueueSend(phase_event_queue, &ev, 0) != pdTRUE) {
diagnostics.phase_event_queue_drops++;
}Переполнение очереди не должно быть молчаливым.
10. EC25 и задачи
Модему нужна отдельная задача, потому что он живёт асинхронно: AT-команды, таймауты, URC, переподключения, PDP, MQTT-состояния. Правильная схема:
transport_task
-> modem_request_queue
modem_task
-> UART2 / EC25
<- URC / AT responsesUART2 принадлежит modem_task.
11. Типичные ошибки
- task без vTaskDelay() или блокирующего ожидания;
- высокий приоритет всем задачам;
- CLI напрямую меняет внутренности сервисов;
- логи в быстром цикле;
- блокировка быстрого пути на сети;
- не измеряется uxTaskGetStackHighWaterMark().
12. Практическое задание
Создай TASKS.md:
# FreeRTOS task map
| Task | Responsibility | Priority | Period / wake source | Owns | Must not do |
|---|---|---:|---|---|---|
| input_task | Read physical inputs and timestamp snapshots | 12 | every 5 ms | input driver | send network packets |
| phase_task | Convert input snapshots to phase events | 10 | input_snapshot_queue | phase detector state | read GPIO directly |
| modem_task | Own EC25 UART and modem state machine | 11 | UART / request queue / timers | UART2, AT parser | know traffic-light logic |
| transport_task | Send events over UDP/TCP/MQTT | 9 | phase_event_queue | network send queue | read ADC/GPIO |
| cli_task | Handle UART0 CLI commands | 7 | UART0 input | CLI parser | modify globals directly |
| health_task | Monitor heap, stacks, queue drops, watchdog counters | 5 | every 1 s | diagnostics | block fast path |Дополнительно опиши:
- чем владеет каждая задача;
- быстрый путь;
- что запрещено в быстром пути;
- диагностические счётчики.
13. Короткий итог
Task должна существовать не потому, что “так красивее”, а потому что у неё есть отдельная ответственность во времени. Базовое разбиение:
input_task - быстро читает входы и ставит timestamp
phase_task - определяет изменение фазы
modem_task - владеет EC25 и UART2
transport_task - отправляет события наружу
cli_task - диагностика и управление
health_task - стеки, heap, очереди, watchdogЗадание
input_task читает вход каждые 5 мс. Модем может ждать ответ до 60 секунд. Составьте схему владения, timestamp и передачи данных, позволяющую входам продолжать работу. Добавьте поведение при перегрузке.
Критерии самопроверки: Сохраните разделение задач и timestamp; исключите ожидание сети из input_task; укажите обработку полной очереди.
Показать ответ автора
input_task владеет чтением, фиксирует timestamp при получении снимка и передаёт снимок phase_task. phase_task владеет состоянием детектора и передаёт событие transport_task. modem_task владеет UART2 и автоматом AT с ограниченным ожиданием/таймаутом; сеть не блокирует чтение. Для каждой очереди задаются ёмкость, ограничение ожидания, обработка отказа и счётчик. Числа 5 мс/60 секунд здесь условия задания, а не гарантированные показатели платы.