14 KiB
tft-app — задачи FreeRTOS и их взаимодействие
Документ описывает реализованную (Фаза 3.2.4) многозадачную структуру tft_app: какие
задачи существуют, кто кого создаёт, как они обмениваются данными и почему приоритеты именно
такие. Контракт между задачами — app_tasks.h
(единственный источник истины по приоритетам/разделяемому состоянию); тела задач —
firmware/tft_app/src/app/task_*.c.
Структура выстрадана на HW-верификации (два боевых регресса — см. PLAN.md, Фаза 3.2.4):
объединение меню и рендера в одну задачу блокировало ввод, а неверный относительный приоритет
sul_rx/render морозил экран при отсутствии CAN-трафика. Эталон разделения —
OLD_PROJECT_TFT8_UKL (BUTTONS_TASK отдельно от REFRESH_TASK).
1. Состав
| Задача | Файл | Приоритет (app_tasks.h) |
Роль | Блокировки |
|---|---|---|---|---|
bringup_task |
task_bringup.c | +4 (высший, недолгоживущая) |
одноразовая инициализация → создаёт три остальные → vTaskDelete(NULL) |
QSPI/flash-операции |
menu_task |
task_menu.c | +3 |
модель меню: кнопки, вход/навигация/правка/сохранение. НЕ рисует | settings_store_save() (flash) на выходе из меню |
render_task |
task_render.c | +2 |
единственный владелец дисплея и вызывающий gfx_present*() |
PXP busy-wait + ожидание FRAME_DONE |
sul_rx_task |
task_sul_rx.c | +1 (низший) |
приём CAN → decode → controller; WDOG/heartbeat | busy-spin в bsp_can_receive() до 100 мс без трафика |
| — демон таймеров | (FreeRTOS) | configTIMER_TASK_PRIORITY (высший в системе) |
input_poll_cb каждые 5 мс: bsp_button_poll() (debounce) → bsp_opto_process() (debounce IN1/IN2) → dispatcher_poll() (§3.4, app/dispatcher.c — безусловный опрос bsp_opto_read(), НЕ колбэк, см. PLAN.md про баг реактивной версии; пишет g_dispatcher_indication и будит render_task прямо отсюда при изменении) |
нет (все три коротких) |
main() создаёт только очередь, софт-таймер ввода и bringup_task — остальное wiring делает
сам bringup_task.
2. Старт системы
sequenceDiagram
participant M as main()
participant B as bringup_task (+4)
participant R as render_task (+2)
participant S as sul_rx_task (+1)
participant U as menu_task (+3)
M->>M: board_hw_init, очередь, таймер ввода
M->>B: xTaskCreate + vTaskStartScheduler
B->>B: log_mutex → UART/лог → QSPI → settings → self-confirm
B->>B: SDRAM → gfx (PXP+ELCDIF) → CAN
B->>B: g_display_ready = true
B->>R: xTaskCreate (хэндл → g_render_task_handle)
B->>S: xTaskCreate
B->>U: xTaskCreate
B->>B: vTaskDelete(NULL)
R->>R: первый кадр («--») + gfx_present()
Порядок создания (render первым) документирует зависимость: его хэндл нужен продюсерам для
xTaskNotifyGive. Формально гонки нет — bringup_task выше всех по приоритету и монополизирует
CPU, пока не создаст всех троих.
3. Взаимодействие (MPSC)
Три продюсера, один консюмер (третий — dispatcher opto, §3.4). Данные и сигнал пробуждения разделены:
flowchart LR
TMR["демон таймеров<br/>input_poll_cb 5 мс<br/>bsp_button_poll + bsp_opto_process"]
MT["menu_task (+3)<br/>модель меню g_menu"]
RX["sul_rx_task (+1)<br/>CAN→decode→controller<br/>WDOG безусловно"]
RT["render_task (+2)<br/>gfx_present*()"]
DSP["dispatcher_poll()<br/>(app/dispatcher.c)<br/>g_dispatcher_indication"]
TMR -."залатанные события кнопок".-> MT
TMR -."безусловный опрос, каждый тик".-> DSP
MT -->|"xTaskNotifyGive<br/>(любое изменение)"| RT
RX -->|"xQueueOverwrite (render_msg_t)<br/>+ xTaskNotifyGive"| RT
DSP -->|"xTaskNotifyGive<br/>(изменение вызов/ответ)"| RT
MT -."g_menu_active (мягкая пауза)".-> RX
- Очередь
g_render_queue(глубина 1,xQueueOverwrite) — только плечоsul_rx→render, несётrender_msg_t(diff + результат). Семантика «важно только последнее состояние»: рендер не обязан успевать за каждым кадром CAN. У dispatcher СВОЕЙ очереди нет — состояние (g_dispatcher_indication) не история, читается напрямую (какg_menu), очередь тут не нужна (сравнение с прошлым значением — внутриrender_task). ulTaskNotifyTake(pdTRUE, portMAX_DELAY)вrender_task— event-driven, без поллинга; несколько notify от ЛЮБОГО из трёх продюсеров схлопываются в одно пробуждение (та же семантика «важно только последнее»).render_taskне различает, КТО его разбудил — каждую итерацию просто проверяет все три источника состояния заново.- Приоритет «меню важнее индикации» не кодируется в уведомлении: проснувшись,
render_taskпервым делом проверяетmenu_is_open(&g_menu)— если меню открыто, очередь индикации и dispatcher даже не читаются (см. §3.1 ниже — dispatcher копится, не теряется). - Модель меню
g_menuмутирует толькоmenu_task;render_taskчитает её для отрисовки после notify (happens-before через нотификацию — как у очереди). g_dispatcher_indicationмутирует толькоdispatcher_poll()(app/dispatcher.c), вызываемый БЕЗУСЛОВНО каждый тик из контекста демона таймеров — приоритет ВЫШЕmenu_task(не реактивно на колбэк bsp_opto — см. PLAN.md §3.4 про баг первой, реактивной версии).render_taskснимает копию в локальную переменную ОДИН раз за итерацию, а не перечитывает несколько раз (иначе возможна гонка того же класса, что чинили для курсора меню при быстрой навигации, см. PLAN.md, Фаза 3.2.4).
3.1 Диспетчерский вход и пауза меню (§3.4)
Приоритет диспетчерского сигнала («вызов»/«ответ») — высший из всех режимов индикации,
безусловно перекрывает и обычную позицию, и любой режим СУЛ; работает независимо от связи со
станцией (ARCH §8 п.3 — это «локальный вход», не данные СУЛ). Тем не менее, пока меню открыто,
render_task НЕ применяет его к экрану — как в OLD_PROJECT_TFT8_UKL (tft_refresh_task
целиком пропускает кадр, пока in_menu_screen), тот же принцип, что уже даёт мягкая пауза
sul_rx_task:
- Debounce и опрос не паузятся —
bsp_opto_process()иdispatcher_poll()вinput_poll_cbработают независимо отg_menu_active, дёшево (как и button). - Применение к экрану — только на закрытии меню. Пока меню открыто, оконный композит
перерисовывает ТОЛЬКО окно 480×272 (Фаза 3.2.4) — полный кадр индикации вне окна заморожен;
применить смену диспетчера немедленно значило бы либо сломать оконную оптимизацию (полный
редрав ради маленькой иконки), либо рисовать поверх окна меню.
g_dispatcher_indicationтем временем просто держит ПОСЛЕДНЕЕ значение — ничего не теряется, отражается сразу по закрытии меню тем же путём, что уже восстанавливает индикацию (ui_fallback_render_initial()).
Мягкая пауза sul_rx_task на время меню
menu_task держит g_menu_active=true, пока меню открыто; sul_rx_task под этим флагом
пропускает CAN-работу (decode/controller/очередь), но WDOG/heartbeat кормит безусловно —
задача не suspend'ится, поэтому сторожевой таймер в безопасности по конструкции, что бы ни
происходило с меню. Флаг обновляется до settings_store_save() (flash-запись небыстрая —
иначе пауза держалась бы дольше нужного). На закрытие меню render_task немедленно
восстанавливает индикацию из последнего известного состояния, не дожидаясь свежего CAN-кадра.
4. Почему приоритеты именно такие
menu (+3) > render (+2) > sul_rx (+1) — оба соотношения выстраданы на железе:
renderВЫШЕsul_rx.bsp_can_receive()— busy-spin без единого блокирующего FreeRTOS-вызова: без CAN-трафикаsul_rx_taskзанимает CPU весь таймаут (100 мс) каждую итерацию, иxTaskDelayUntil()при просроченном дедлайне не блокирует вовсе — задача непрерывно READY. Более низкоприоритетныйrender_taskв этой ситуации голодал: пустой экран при старте без связи, залипание индикации при обрыве. Обратный порядок приоритетов чинит это, не трогая общий bare-metal модульbsp_can.menuВЫШЕrender. Блокировки рендера (PXP busy-wait + ожидание кадра) не должны придерживать обработку кнопок — то самое свойство, ради которого меню и рендер разведены по задачам (в объединённой задаче ввод был мёртв).bringupвыше всех — монополизирует CPU на время одноразовой инициализации.- Демон таймеров — наивысший в системе (
configTIMER_TASK_PRIORITY): debounce-сэмплы не теряются, чем бы ни были заняты остальные.
5. Разделяемое состояние (app_tasks.h)
| Объект | Пишет | Читает | Синхронизация |
|---|---|---|---|
g_render_queue |
sul_rx | render | FreeRTOS queue |
g_render_task_handle |
bringup (до создания продюсеров) | sul_rx, menu | создание-до-использования |
g_display_ready |
bringup | render, sul_rx | volatile bool, одно-writer |
g_menu_active |
menu | sul_rx | volatile bool, одно-writer |
g_menu |
menu | render | notify (happens-before) |
g_dispatcher_indication |
dispatcher_poll() (демон таймеров, каждый тик) | render | volatile enum, один writer; render снимает копию один раз за итерацию (см. §3) |
лог-буфер utils/log |
все задачи | — | мьютекс port/log/src/log_mutex.c (FreeRTOS strong-override; включён в сборку app — до Фазы 3.2.4 был #if 0, гонка) |
6. Кадр на экран (сводка)
Рендер-пайплайн (детали — FALLBACK.md §5, MENU.md §5): CPU рисует в
AS (ARGB8888) → gfx_present*() в render_task = PXP-композит AS над чёрным PS → задний
framebuffer RGB565 → tear-free свап по FRAME_DONE. Гибрид bpp держит сканаут ELCDIF на
половинной полосе SDRAM (63 МБ/с вместо 126). Индикация — всегда полный кадр (~31 мс PXP);
меню после открытия — только окно 480×272 (~8 мс расчётно).