1. Тема дня

Три главных механизма связи между задачами FreeRTOS:

text
Queue             - передать данные
Event Group       - сообщить набор флагов состояния
Task Notification - быстро разбудить конкретную task или передать 32-битный сигнал

Главный вопрос: как задачам общаться без глобальных переменных, гонок, зависаний и потерь событий.

2. Зачем это нужно

Типовые потоки данных:

text
1. Входы светофора
   input_task -> phase_task -> transport_task
2. Модем EC25
   UART RX -> modem_task -> transport_task / health_task / CLI
3. Сетевые события
   transport_task -> modem_task -> retry/ACK state
4. Диагностика
   services -> health_task -> CLI
5. HIL-тестирование
   Raspberry Pi -> UART CLI -> services -> ответы/логи

Глобальные переменные:

c
extern bool g_modem_ready;
extern int g_current_phase;
extern bool g_send_now;
extern uint32_t g_error_flags;

Поначалу удобны, но потом приводят к гонкам, потере событий и нестабильным HIL-тестам.

3. Шпаргалка выбора механизма

text
Нужно передать структуру данных?    -> Queue
Нужно сообщить флаги состояния?     -> Event Group
Нужно быстро разбудить одну task?   -> Task Notification
Нужно защитить общий ресурс?        -> Mutex
Нужно передавать поток байтов UART? -> Stream/ring buffer/UART driver buffer

4. Queue

Queue передаёт сообщения. Примеры:

text
input_task -> input_snapshot_t -> phase_task
phase_task -> phase_event_t -> transport_task
transport_task -> modem_request_t -> modem_task

Структуры:

c
typedef struct {
    uint32_t raw_mask;
    int64_t timestamp_us;
} input_snapshot_t;
typedef struct {
    uint32_t seq;
    uint8_t old_phase;
    uint8_t new_phase;
    uint32_t raw_mask;
    int64_t timestamp_us;
} phase_event_t;

Очередь отвечает на вопрос: “какие именно данные произошли?”

5. Выбор длины queue

Не ставь длину “на глаз” слишком большой. Примерные стартовые значения:

text
input_queue length        = 4..16
phase_event_queue length  = 8..32
modem_request_queue       = 8..32

Маленькая очередь быстрее показывает перегрузку. Большая может скрыть проблему.

6. Что делать при переполнении

Для разных данных - разные политики:

text
Событие переключения фазы:
  терять плохо, нужен counter drops и диагностика.
Периодическая телеметрия:
  можно потерять старую, важнее свежая.
Текущее состояние модема:
  можно хранить последнее значение.
CLI-ответ:
  лучше вернуть BUSY или QUEUE_FULL.
UART RX от модема:
  терять плохо, нужен нормальный RX buffer/parser.

Код:

c
if (xQueueSend(queue, &event, 0) != pdTRUE) {
    diag.queue_drops++;
}

7. Event Group

Event Group - набор битов состояния:

text
bit 0: MODEM_AT_READY
bit 1: MODEM_REGISTERED
bit 2: PDP_ACTIVE
bit 3: MQTT_CONNECTED
bit 4: UDP_READY
bit 5: TIME_SYNCED
bit 6: INPUT_FAULT
bit 7: TRANSPORT_DEGRADED

Пример:

c
#define MODEM_BIT_AT_READY        (1 << 0)
#define MODEM_BIT_REGISTERED      (1 << 1)
#define MODEM_BIT_PDP_ACTIVE      (1 << 2)
#define MODEM_BIT_MQTT_CONNECTED  (1 << 3)
#define MODEM_BIT_TIME_SYNCED     (1 << 4)
static EventGroupHandle_t modem_event_group;

Когда модем готов:

c
xEventGroupSetBits(modem_event_group, MODEM_BIT_AT_READY);

Когда сеть потеряна:

c
xEventGroupClearBits(modem_event_group,
                     MODEM_BIT_REGISTERED |
                     MODEM_BIT_PDP_ACTIVE |
                     MODEM_BIT_MQTT_CONNECTED);

Event group хорош для состояния, но плох для истории событий.

Биты и маски

Число: 15 · 00001111 · 0x0F
Маска: 14 · 00001110 · 0x0E

00001111 AND 00001110 = 00001110 (14)
Шестнадцатерично: 0x0F AND 0x0E = 0x0E

Кнопки переключаются клавишами Enter и пробел. AND оставляет общие биты, OR объединяет, XOR оставляет различающиеся.

Сверить рассуждение о готовности флагов

Раскрыть решение

Исходно 00001111 AND 00001110 = 00001110 (14): результат совпадает с маской. После выключения бита 2 число становится 11; 11 AND 14 = 10. Флага MODEM_BIT_PDP_ACTIVE нет, поэтому готовности всех трёх флагов нет, хотя результат ненулевой. После выключения только бита 0 число равно 14; 14 AND 14 = 14. Бит MODEM_BIT_AT_READY не входит в выбранную маску и на этот критерий не влияет. Маска 14 — конкретный критерий этой практики, а не универсальная политика готовности модема.

8. Task Notification

Task Notification - прямой сигнал конкретной задаче.

c
static TaskHandle_t transport_task_handle;
xTaskNotifyGive(transport_task_handle);

Получатель:

c
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

Полезно, если данные уже лежат где-то ещё, а notification нужен только как “звонок в дверь”. Можно использовать биты notification:

c
#define TRANSPORT_NOTIFY_NEW_EVENT   (1 << 0)
#define TRANSPORT_NOTIFY_MODEM_UP    (1 << 1)
#define TRANSPORT_NOTIFY_MODEM_DOWN  (1 << 2)
#define TRANSPORT_NOTIFY_ACK_TIMEOUT (1 << 3)
xTaskNotify(transport_task_handle,
            TRANSPORT_NOTIFY_NEW_EVENT,
            eSetBits);

9. Ограничения notification

Task notification удобны, но не заменяют queue.

text
Передать phase_event_t со всеми полями?    -> Queue
Разбудить задачу?                         -> Task Notification
Передать битовые флаги одной задаче?       -> Task Notification bits
Передать состояние, которое читают многие? -> Event Group

10. Типичные ошибки

  • использовать global variable вместо queue;
  • использовать event group для событий с данными;
  • игнорировать результат xQueueSend();
  • блокировать быстрый путь через portMAX_DELAY;
  • путать mutex и semaphore;
  • делать одну queue для всего;
  • заставлять одну task ждать слишком много разных объектов.

11. Практическое задание

Создай RTOS_COMMUNICATION.md:

markdown
# RTOS communication map
| From | To | Mechanism | Data | Overflow policy |
|---|---|---|---|---|
| input_task | phase_task | Queue | input_snapshot_t | drop newest + counter |
| phase_task | transport_task | Queue | phase_event_t | drop newest + critical counter |
| transport_task | modem_task | Queue | modem_request_t | reject low-priority telemetry first |
| modem_task | transport_task | Event Group / Notification | modem ready/lost flags | state only, no history |
| cli_task | services | direct API / Queue | command-specific | return BUSY if unavailable |
| services | health_task | counters / Event Group | fault bits | persistent until cleared |

Опиши структуры сообщений:

c
typedef enum {
    MODEM_REQ_MQTT_PUBLISH,
    MODEM_REQ_UDP_SEND,
    MODEM_REQ_TCP_SEND,
    MODEM_REQ_RECONNECT,
    MODEM_REQ_STATUS
} modem_request_type_t;
typedef struct {
    modem_request_type_t type;
    uint8_t priority;
    uint32_t seq;
    uint8_t payload[128];
    uint16_t payload_len;
    int64_t created_at_us;
} modem_request_t;

Добавь counters переполнений.

12. Короткий итог

text
Queue             -> данные
Event Group       -> состояние
Task Notification -> быстрый wakeup
Mutex             -> защита ресурса

Базовая схема:

text
input_task
  -> input_snapshot_queue
  -> phase_task
  -> phase_event_queue
  -> transport_task
-> modem_request_queue
-> modem_task / EC25
Нужно передать phase_event_t с timestamp, old_phase и new_phase и сохранить порядок нескольких переходов. Что выбрать?

Задание

Выберите механизм для четырёх случаев: история переключений фазы с timestamp; текущая готовность модема для нескольких читателей; пробуждение единственного потребителя уже заполненного буфера; защита общего ресурса. Укажите, что нельзя терять молча.

Критерии самопроверки: Назовите четыре разных механизма, место хранения payload уведомления и последствия переполнения.

Показать ответ автора

История фаз — Queue с payload и учётом переполнения. Общая готовность — Event Group. Пробуждение одного потребителя — Task Notification, а данные остаются в буфере с отдельным контрактом владения. Общий ресурс — Mutex в допустимом task-контексте. Переходы фаз и UART-байты нельзя терять молча: нужны политика, диагностика и обработка отказа.