1. Тема дня

Разбираем синхронизацию:

text
задача ↔ задача
ISR ↔ задача
ядро ESP32 ↔ другое ядро ESP32
DMA ↔ CPU
драйвер ↔ прикладная логика

Основные проблемы:

text
race condition
deadlock
priority inversion
starvation

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

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

EC25

Если modem_task, mqtt_task, sms_task и CLI напрямую пишут в UART, даже mutex вокруг uart_write() не спасёт: ответы и URC перемешаются. Нужно сериализовать всю AT transaction или передать владение UART одной modem_task.

Детектор фаз

Если input_task обновляет red/yellow/green/timestamp, а MQTT/CAN/health читают одновременно, они могут увидеть “torn state”: часть полей уже новые, часть старые.

DMA

DMA не берёт mutex. Если CPU читает ту же область, которую DMA пишет, будет смешанный пакет.

3. Теория

Race condition

counter++ - это read-modify-write. Две задачи могут прочитать одно и то же значение и потерять increment.

volatile не mutex

volatile не гарантирует атомарность, согласованность структуры, порядок между ядрами или protection от ISR. Для синхронизации нужны mutex, queue, notification, critical section или atomics.

Mutex vs binary semaphore

Mutex имеет владельца и priority inheritance. Binary semaphore - это сигнал. Для mutual exclusion между задачами используй mutex, для ISR→task - semaphore/notification. Priority inversion

LOW держит mutex, HIGH ждёт mutex, MEDIUM вытесняет LOW. HIGH косвенно ждёт MEDIUM. Mutex с priority inheritance временно повышает LOW до HIGH, чтобы он отпустил ресурс. Но inheritance не делает длинную блокировку безопасной. Под mutex нельзя ждать сеть, AT- response, Flash, callback или логировать большие строки.

Deadlock

text
Task A: lock(CONFIG) → lock(I2C)
Task B: lock(I2C) → lock(CONFIG)

Решение: глобальный lock order.

text
1. CONFIG
2. STATE
3. I2C
4. MODEM
5. LOG

Лучше вообще не брать два mutex одновременно.

Owner-task pattern

Вместо:

text
Task A → mutex → UART
Task B → mutex → UART
Task C → mutex → UART

делаем:

text
Task A ─┐
Task B ─┼→ command queue → UART owner task → peripheral
Task C ─┘

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

  • Считать volatile mutex-ом.
  • Использовать binary semaphore для mutual exclusion.
  • Использовать mutex из ISR.
  • Держать mutex во время I/O.
  • Вложенные mutex без порядка.
  • Вызывать callback под lock.
  • Использовать vTaskSuspendAll() как lock на ESP32 SMP.
  • Защищать каждый UART write, но не всю AT transaction.
  • Использовать event bit там, где нужен счётчик.

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

Создай RTOS_SYNC_POLICY.md.

markdown
# RTOS synchronization policy
1. Every peripheral has one explicit owner.
2. ISR never waits for mutex or performs recovery.
3. Mutex protects only short task-context resource access.
4. No network, delay, logging or callback while holding mutex.
5. Multiple locks are acquired only in documented order.
6. Every system mutex has finite timeout and diagnostics.
7. Fast ISR-to-task signalling uses notifications/thread flags.
8. Structured events are passed through queues.
9. Shared structures are read only through atomic snapshot API.
10. volatile is not used as synchronization mechanism.

Добавь таблицу ресурсов: EC25 UART, I2C, phase state, logger UART, CAN TX, config.

6. Что попробовать дальше

  • Запретить прямой доступ к EC25 UART из всех файлов, кроме modem_transport.c.
  • Добавить modem_command_submit().
  • Добавить lock diagnostics: owner, max_hold_us, timeout_count.
  • Провести HIL-тест: MQTT publish + SMS + CLI AT одновременно.

Задание

Две задачи блокируют UART вокруг каждого write, но AT transactions всё равно перекрываются. Объясните отсутствующую границу синхронизации и перепроектируйте интерфейс.

Критерии самопроверки: Указать владельца UART и parser state; объяснить, почему write-level mutual exclusion не исключает mixed responses.

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

AT transaction включает command, response matching, timeout и asynchronous URC handling, а не только один write. Передавать команды одной modem owner task, сериализующей полные transactions и распределяющей результаты.

Задание

Задача читает phase snapshot, пока input_task изменяет поля. Определите наблюдаемое требование согласованности и способ синхронизации.

Критерии самопроверки: Указать поля одного snapshot и обеспечение ownership/lifetime. Одного volatile недостаточно.

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

Каждый reader должен получить coherent snapshot одного update, а не смесь старых и новых полей. Передавать copied snapshot через подходящую queue либо защищать короткую copy выбранным механизмом синхронизации; избегать long I/O под защитой.