From 2662111e1b7b4dade4c85632026a335f7fbad4cd Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Wed, 22 Jul 2026 14:49:22 +0300 Subject: [PATCH] # tft_app: Phase 3.2 - menu & indicator --- cmake/linker/MIMXRT1052xxxxx_app_slot.ld | 25 +- docs/tft_app/MENU.md | 107 +++++- firmware/tft_app/CMakeLists.txt | 13 +- firmware/tft_app/PLAN.md | 228 +++++++++++- firmware/tft_app/src/app/app_tasks.h | 127 +++++++ firmware/tft_app/src/app/main.c | 341 ++---------------- firmware/tft_app/src/app/task_bringup.c | 186 ++++++++++ firmware/tft_app/src/app/task_menu.c | 114 ++++++ firmware/tft_app/src/app/task_render.c | 86 +++++ firmware/tft_app/src/app/task_sul_rx.c | 135 +++++++ .../can/include/domain/sul/transport/can.h | 22 +- .../sul/transport/can/src/can_transport.c | 77 +++- firmware/tft_app/src/menu/include/menu/menu.h | 2 +- .../tft_app/src/services/gfx/CMakeLists.txt | 7 +- .../src/services/gfx/include/services/gfx.h | 36 +- firmware/tft_app/src/services/gfx/src/gfx.c | 199 ++++++++-- .../settings_store/src/settings_store.c | 8 +- .../tft_app/src/ui/fallback/src/fallback.c | 2 +- .../src/ui/menu/include/ui/menu_view.h | 10 +- firmware/tft_app/src/ui/menu/src/menu_view.c | 47 ++- port/log/src/log_mutex.c | 12 +- sdk/CMakeLists.txt | 2 + 22 files changed, 1355 insertions(+), 431 deletions(-) create mode 100644 firmware/tft_app/src/app/app_tasks.h create mode 100644 firmware/tft_app/src/app/task_bringup.c create mode 100644 firmware/tft_app/src/app/task_menu.c create mode 100644 firmware/tft_app/src/app/task_render.c create mode 100644 firmware/tft_app/src/app/task_sul_rx.c diff --git a/cmake/linker/MIMXRT1052xxxxx_app_slot.ld b/cmake/linker/MIMXRT1052xxxxx_app_slot.ld index d53beab..3fd896e 100644 --- a/cmake/linker/MIMXRT1052xxxxx_app_slot.ld +++ b/cmake/linker/MIMXRT1052xxxxx_app_slot.ld @@ -46,21 +46,24 @@ MEMORY m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128 КБ — ramfunc */ m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128 КБ — .data/.bss/heap/stack */ m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256 КБ */ - /* SDRAM 32 МБ, разбита на 2: некэшируемый framebuffer (ELCDIF DMA читает - * его напрямую — MPU Region 9 в board_mpu_init() параметризован ЭТИМИ - * __NCACHE_REGION_* символами, C-код не трогаем) + остаток под кэшируемые - * буферы будущих фаз (ассеты, аудио). Framebuffer TFT8 800×600×4 ≈ 1.83 МБ - * — 2 МБ (степень двойки, MPU-регион обязан быть степенью двойки и - * выровнен по своему размеру) с запасом. См. bsp/sdram/README.md про NXP- - * рекомендацию некэшируемых DMA-буферов в SDRAM. */ - m_sdram_ncache (RW) : ORIGIN = 0x80000000, LENGTH = 0x00200000 /* 2 МБ — framebuffer (non-cacheable) */ - m_sdram (RW) : ORIGIN = 0x80200000, LENGTH = 0x01E00000 /* ~30 МБ — кэшируемый остаток, Фаза 1 не использует */ + /* SDRAM 32 МБ, разбита на 2: некэшируемые DMA-буферы компоновщика (ELCDIF + * читает framebuffer, PXP — AS/PS напрямую; MPU Region 9 в board_mpu_init() + * параметризован ЭТИМИ __NCACHE_REGION_* символами, C-код не трогаем) + + * остаток под кэшируемые буферы будущих фаз (ассеты, аудио). + * + * Фаза 3.2.4 (double-buffer + PXP): 4 буфера TFT8 800×600×4 ≈ 1.92 МБ каждый + * — AS (alpha_buffer) + PS (processing_buffer) + framebuffer[2] = 7.68 МБ + * → 8 МБ (степень двойки; MPU-регион обязан быть степенью двойки и выровнен + * по своему размеру — 8 МБ @ 0x80000000 удовлетворяет). См. bsp/sdram/ + * README.md про NXP-рекомендацию некэшируемых DMA-буферов в SDRAM. */ + m_sdram_ncache (RW) : ORIGIN = 0x80000000, LENGTH = 0x00800000 /* 8 МБ — AS+PS+2×FB (non-cacheable) */ + m_sdram (RW) : ORIGIN = 0x80800000, LENGTH = 0x01800000 /* 24 МБ — кэшируемый остаток, пока не используется */ } SECTIONS { __NCACHE_REGION_START = ORIGIN(m_sdram_ncache); - __NCACHE_REGION_SIZE = LENGTH(m_sdram_ncache); /* 2 МБ non-cacheable — framebuffer */ + __NCACHE_REGION_SIZE = LENGTH(m_sdram_ncache); /* 8 МБ non-cacheable — AS+PS+2×FB */ /* Vector table (imgtool header зарезервирован до неё в MEMORY). */ .interrupts : @@ -208,7 +211,7 @@ SECTIONS ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data") ASSERT(text_end <= SLOT_BASE + SLOT_SIZE, "app вышел за пределы слота (2 МБ)") ASSERT((__noncachedata_end__ - ORIGIN(m_sdram_ncache)) <= LENGTH(m_sdram_ncache), - "m_sdram_ncache overflow — framebuffer(ы) не влезли в 2 МБ") + "m_sdram_ncache overflow — AS+PS+2×FB не влезли в 8 МБ") .bss : { diff --git a/docs/tft_app/MENU.md b/docs/tft_app/MENU.md index 35f9581..7f113fd 100644 --- a/docs/tft_app/MENU.md +++ b/docs/tft_app/MENU.md @@ -1,8 +1,9 @@ # tft-app — движок меню и связь с настройками -Документ описывает **реализованный** движок меню (Фаза 3.2.1: чистая модель) и его -связь с модулем настроек `settings_store`. Проектная основа — [ARCH.md §8](../../firmware/tft_app/ARCH.md); -реализация — [menu.c](../../firmware/tft_app/src/menu/src/menu.c), +Документ описывает **реализованный** движок меню (Фаза 3.2.1: чистая модель; 3.2.2–3.2.4: рендер, +wiring, разделение на задачи) и его связь с модулем настроек `settings_store`. Проектная основа — +[ARCH.md §8](../../firmware/tft_app/ARCH.md); реализация — +[menu.c](../../firmware/tft_app/src/menu/src/menu.c), [settings_store](../../firmware/tft_app/src/services/settings_store/). Движущее требование (§8): клиент приносит уникальные настройки, и добавление их **не должно @@ -17,7 +18,7 @@ ```mermaid flowchart TB - BTN["bsp_button
долгое BUTTON_2 / next / action"] + BTN["bsp_button
короткое BUTTON_1 (вход/next) / BUTTON_2 (action)"] subgraph MODEL["menu (чистая модель, host-тест)"] TREE["дерево-данные
menu_item_desc_t[]"] NAV["навигация + редактор
menu_next / menu_action"] @@ -68,8 +69,10 @@ typedef struct { ## 3. Навигация -Две кнопки: BUTTON_1 → `menu_next` (следующий пункт уровня, с заворотом); короткое BUTTON_2 → -`menu_action` (по типу пункта). Вход в меню — долгое BUTTON_2 (app-слой). +Две кнопки, обе коротким нажатием (без удержания — раскладка `OLD_PROJECT_TFT8_UKL`): BUTTON_1 → +`menu_next` (следующий пункт уровня, с заворотом) когда меню открыто, **вход в меню** (app-слой, +`menu_open`) когда закрыто; BUTTON_2 → `menu_action` (по типу пункта) когда открыто, намеренный +no-op когда закрыто. ```mermaid flowchart TD @@ -101,32 +104,100 @@ flowchart TD на дескриптор протокола `sul_settings_desc_t` (§8) — меню строит раздел «Настройки протокола» из дескриптора активного протокола, не хардкодом. -**Поток сохранения** (app-слой связывает модель и flash): +**Поток сохранения** (`menu_task` связывает модель и flash — см. §5): ```mermaid sequenceDiagram participant U as Пользователь (кнопки) participant M as menu (модель) participant S as settings_t (RAM) - participant A as app + participant MT as menu_task participant St as settings_store U->>M: menu_action (правка / выход) M->>S: запись поля по offset (dirty=true) - M-->>A: open=false, save_requested=true - A->>St: settings_store_save() + M-->>MT: open=false, save_requested=true + MT->>St: settings_store_save() St->>St: serialize + CRC → QSPI 0x450000 - A->>A: пере-применить настройки (напр. адрес → декодер) ``` -После сохранения app пере-применяет изменившиеся настройки к рантайму (например, новый адрес — -в `nku_can_set_address()`), т.к. декодер держит свою копию адреса. +Адрес станции (`nku_can_set_address()`) `sul_rx_task` пере-применяет из настроек сам, на следующей +итерации — не требует отдельного сигнала (декодер читает `settings_store_get()` каждую итерацию). --- -## 5. Рендер (Фаза 3.2.2) +## 5. Рендер и wiring (Фаза 3.2.2–3.2.4) -Рендер — отдельный слой, читает модель запросами (`menu_current`, `menu_level_range`, -`menu_read_value`) и рисует в **фиксированном окне 480×272 в логических (0,0)** — одинаково на всех -панелях (на больших — левый-верхний угол, остальное чёрное). Меню модально: пока `menu_is_open()`, -индикация под ним не рисуется. Детали — [PLAN.md, Фаза 3.2](../../firmware/tft_app/PLAN.md). +Рендер ([menu_view.c](../../firmware/tft_app/src/ui/menu/src/menu_view.c)) — отдельный слой, читает +модель запросами (`menu_current`, `menu_level_range`, `menu_read_value`) и рисует в **фиксированном +окне 480×272 @ логич.(0,0)** — одинаково на всех панелях (на больших — левый-верхний угол, остальное +чёрное). Логики навигации не содержит. + +**Раскладка** (под реальные шрифты: `SystemFont`/JBMono24 h=31, `SystemFontSmall`/JBMono12 h=16): +заголовок 36 + 6 строк × 36 + футер 20 = **272**, обрамление — тонкая серая рамка 1 px по периметру. +Заголовок = подпись текущего уровня; строки «подпись слева / значение справа»; **курсор — сплошная +полоса-заливка** (`gfx_fill_rect`) + белый текст; футер — легенда кнопок + «N/M». + +**Цвет — тинтингом** (`gfx_draw_string(..., color)` с альфа-блендингом): один белый шрифт рисуется +любым цветом со сглаживанием, и оно корректно ложится на полосу-курсор. Значения SELECT/BOOL — из +`options[]` дескриптора, BYTE — числом. + +**Полнокадровый рендер** (double-buffer + PXP, Фаза 3.2.4): любое изменение — `menu_view_render()` +рисует **весь кадр** off-screen в альфа-поверхность AS (обнуление + окно), затем владелец дисплея +зовёт `gfx_present()` (PXP-композит AS над чёрным PS → задний framebuffer + атомарный свап). Рисуем +вне экрана, показываем атомарно → tear-free. Компоновщик — `services/gfx` (эталон +`OLD_PROJECT_TFT8_UKL/source/display/`). + +**Меню и рендер — РАЗНЫЕ задачи** ([task_menu.c](../../firmware/tft_app/src/app/task_menu.c) / +[task_render.c](../../firmware/tft_app/src/app/task_render.c)). Найдено на HW-верификации Фазы +3.2.4: в объединённой задаче (Фазы 3.2.1–3.2.3, один framebuffer, без ожиданий) блокировок не было, +разделение было безвредным упущением — но `gfx_present()` (double-buffer + PXP) внёс блокирующее +ожидание кадра, и в объединённой задаче это ожидание попутно блокировало вход в меню (ноль реакции +на кнопки). Эталон разделения — `OLD_PROJECT_TFT8_UKL`: `BUTTONS_TASK`/`menu_task` отдельно от +`REFRESH_TASK`/`tft_refresh_task`. + +```mermaid +flowchart LR + BTN["bsp_button
софт-таймер 5 мс
(debounce, независимо от обеих задач)"] + MT["menu_task
модель + мгновенный вход/навигация + save
НЕ рисует"] + RX["sul_rx_task
CAN → decode → controller
WDOG безусловно"] + RT["render_task
ЕДИНСТВЕННЫЙ вызывающий gfx_present()
event-driven"] + + BTN --> MT + MT -->|"xTaskNotifyGive
(любое изменение)"| RT + RX -->|"xQueueOverwrite (данные)
+ xTaskNotifyGive (сигнал)"| RT + MT -.g_menu_active.-> RX +``` + +**Связь — MPSC.** Два продюсера (`sul_rx_task`, `menu_task`), один консюмер (`render_task`). +Данные (какой этаж/диф) идут только по плечу `sul_rx_task`→`render_task` — однослотовая +`xQueueOverwrite`-очередь (важно только последнее). Пробуждение — `xTaskNotifyGive()`/ +`ulTaskNotifyTake(pdTRUE, portMAX_DELAY)` от ОБОИХ продюсеров: `render_task` не поллит, спит между +изменениями; несколько notify схлопываются в одно пробуждение (та же семантика «важно только +последнее»). Приоритет «меню важнее индикации» не кодируется в уведомлении — `render_task`, +проснувшись, всегда СНАЧАЛА проверяет `menu_is_open()`. + +**Мягкая пауза `sul_rx_task` на время меню.** Пока меню открыто, `menu_task` держит +`g_menu_active=true`; `sul_rx_task` под этим флагом пропускает decode/controller/запись в очередь — +но WDOG/heartbeat кормятся БЕЗУСЛОВНО (вне флага), задача не suspend'ится. При выходе из меню +`render_task` (по признаку «меню только что закрылось») сразу перерисовывает последнее известное +состояние индикации, не дожидаясь свежего CAN-кадра. + +**Ввод.** Опрос кнопок — **софт-таймер** (`input_poll_cb`, 5 мс; демон таймеров на высшем приоритете +в системе → нажатия не теряются, пока заняты остальные задачи; в Фазе 3.4 туда же +`bsp_opto_process()`). Раскладка — как в `OLD_PROJECT_TFT8_UKL`, оба нажатия короткие, без +удержания: BUTTON_1 = вход в меню (закрыто) / следующий пункт (открыто); BUTTON_2 = выбор/инкремент +(открыто), намеренный no-op (закрыто). + +**Приоритеты задач** (`app_tasks.h` — единая точка правды, `tskIDLE_PRIORITY`-относительно): +`bringup_task` (одноразовая, самый высокий из четырёх) → `menu_task` → `render_task` → `sul_rx_task` +(самый низкий). **Важно:** `render_task` НАМЕРЕННО выше `sul_rx_task`, не наоборот — `bsp_can_receive()` +busy-spin без yield (`bsp/can/src/can.c`) занимает CPU весь `CAN_RX_TIMEOUT_MS` (100 мс) при +отсутствии трафика, и `xTaskDelayUntil()` в этом случае не блокирует вовсе (дедлайн уже в прошлом — +см. `sdk/rtos/freertos/freertos-kernel/tasks.c`), т.е. `sul_rx_task` не отдаёт CPU добровольно. +Если `sul_rx_task` окажется выше `render_task`, последняя будет голодать всё время отсутствия +CAN-трафика (найдено на HW-верификации — экран не обновлялся при старте без связи и при обрыве +связи; см. PLAN.md, Фаза 3.2.4). `menu_task` по-прежнему выше `render_task` — её PXP busy-wait +(~60-100 мс) не должен придерживать ввод. Логгер (`utils/log`) под FreeRTOS — с мьютексом +(`port/log/src/log_mutex.c`, до Фазы 3.2.4 был `#if 0` и не собирался — гонка на общем static-буфере +логгера между несколькими пишущими задачами). diff --git a/firmware/tft_app/CMakeLists.txt b/firmware/tft_app/CMakeLists.txt index 985e867..219ec44 100644 --- a/firmware/tft_app/CMakeLists.txt +++ b/firmware/tft_app/CMakeLists.txt @@ -58,7 +58,11 @@ include(${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cm add_executable( app - src/app/main.c + src/app/main.c # main() + wiring + FreeRTOS-хуки + src/app/task_bringup.c # одноразовый bring-up (QSPI/settings/self-confirm/SDRAM+gfx+CAN) → создаёт остальные задачи + src/app/task_sul_rx.c # приём CAN → decode → controller (WDOG/heartbeat безусловно, CAN-работа под !g_menu_active) + src/app/task_menu.c # модель меню: кнопки/hold-to-enter/nav/edit/save — НЕ рисует + src/app/task_render.c # презентация: единственный вызывающий gfx_present(), event-driven ${BSP_GENERATED}/clock_config.c # BOARD_BootClockRUN (зовётся board_hw_init) ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE} @@ -70,6 +74,12 @@ add_executable( # ниже (BSP_TICK_FREERTOS_MODE), чтобы не тянуть SysTick_Handler, # конфликтующий с FreeRTOS-портом. Приходит транзитивно через bsp_uart_host. ${CMAKE_SOURCE_DIR}/bsp/tick/src/tick.c + # log_mutex.c — FreeRTOS strong-override весов log_mutex_{init,lock,unlock} + # (weak NOP в utils/log/log.c). Компилируется ПРЯМО в app (не отдельной + # либой) — как tick.c: FreeRTOS-специфика только этой цели, нужна во ВСЕХ + # сборках tft_app (без неё — гонка на общем static-буфере логгера между + # bringup_task/sul_rx_task/menu_task/render_task, см. PLAN.md). + ${CMAKE_SOURCE_DIR}/port/log/src/log_mutex.c ${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c ${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c) @@ -106,6 +116,7 @@ target_link_libraries( app PRIVATE bsp_board bsp_led + bsp_button bsp_wdog bsp_boot_state bsp_qspi_flash diff --git a/firmware/tft_app/PLAN.md b/firmware/tft_app/PLAN.md index 20cf114..1f639ee 100644 --- a/firmware/tft_app/PLAN.md +++ b/firmware/tft_app/PLAN.md @@ -210,6 +210,27 @@ marks_both_pending`, аппаратно на реальном обрыве св > подстановка «-»); недостаёт выделенной чистой функции проверки покрытия и её теста — > требует небольшого выноса из `gfx.c` (зависит от `bsp_display`). > +> 🐛 **Найден и починен на реальной станции (не симулятор, 2026-07-22) — транспорт, +> не декодер.** `domain/sul/nku_can.c` (decode) корректно считает адресный сдвиг +> `group4=addr<<4`/`group6=addr<<6` на каждый вызов — декодер никогда не был +> виноват. Баг — в `domain/sul/transport/can/` (HW, host не тестируется, отдельный +> слой): (1) HW RX-фильтры FlexCAN выставлялись ОДИН РАЗ при bring-up на +> захардкоженные ID адреса 0 (0x506/0x508) — правка адреса в меню обновляла только +> декодер, физически кадры с другим адресом отбрасывались CAN-контроллером до +> всякого софта; воспроизводилось на реальной станции (адрес 1) — heartbeat живой, +> экран пуст; на симуляторе (адрес 0) работало, поэтому не всплывало раньше. (2) +> Фильтровались только PACKET1/PACKET3 — PACKET2/4/5 (перегруз, сейсмоопасность, +> следующий этаж) не принимались НИКОГДА, вне зависимости от адреса. Т.е. критерий +> выхода Фазы 2 «все режимы НКУ-CAN корректно доходят» ни разу не выполнялся для +> этих трёх пакетов на железе. **Фикс** (по образцу `OLD_PROJECT/source/main_programm.c` +> `apply_nku_can_filters()`/`msg_receiver_task` — та же bsp_can-прослойка, дословно +> портирован паттерн): новая `sul_transport_can_set_address(addr)` переконфигурирует +> все 5 MB-фильтров под текущий адрес (diff+сентинел `0xFFU`, как в референсе — +> дёшево звать каждую итерацию); `sul_transport_can_init()` фильтры больше не +> трогает. `task_sul_rx.c` зовёт её рядом с `nku_can_set_address()`. Домен +> (`domain/sul.h`, `nku_can.h`/`.c`) не тронут — фикс целиком внутри NKU-CAN- +> транспорта; для других протоколов (Фаза 8: УИМ/SD7/УЭЛ/УКЛ, в основном UART) своя +> адресация — не переиспользуется автоматически. > Побочная находка: `SystemFont.c` (сгенерирован lcd-image-converter) экспортирует tFont > под именем `JBMono24` (из .xml), а `gfx.h` обещал `SystemFont` — рассинхрон был > латентным (Фаза 1 линковала только `FloorFontFallback`), вскрылся при первом @@ -231,10 +252,44 @@ marks_both_pending`, аппаратно на реальном обрыве св --- -## Фаза 3 — Настройки и меню ⬜ +## Фаза 3 — Настройки и меню 🟨 **Цель.** Персистентные настройки на QSPI, меню на двух кнопках, локальные входы (opto). +> ## ⏸️ ТОЧКА ОСТАНОВКИ (переход в новый тред) — 2026-07-21 +> +> **Сделано, на железе подтверждено:** +> - **3.1 `services/settings_store`** ✅ — персист ядра (magic/version/CRC32 в `0x450000` через +> `bsp_qspi_flash`), `proto_slice[0]`→`nku_address` в декодер. HW: load/save, адрес переживает +> power-cycle. _Латентный баг починен:_ 4-КБ `settings_page_t` на ~3-КБ стеке задачи → stack +> overflow → bootloader recovery; сделал буфер `static`, стеки задач x6→x8. +> - **3.2.1 модель меню** ✅ — чистая (`src/menu/menu.c`: дерево-данные + навигация + edit + +> offset-привязка, `save_requested`), host-тест `tests/host/tft_app_menu` (10). +> - **3.2.2 рендер + gfx** ✅ — тинтинг `gfx_draw_string(...,color)` (альфа-блендинг: белый шрифт +> любым цветом со сглаживанием, корректно на цветной полосе), `gfx_fill_rect`/`gfx_draw_rect`, +> шрифт `JBMono12`→алиас `SystemFontSmall`; окно 480×272 @ (0,0); боевое дерево `menu_tree.c` +> (Протокол/Адрес/Логи/Выход, метки `options[]`). +> - **3.2.3 wiring** ✅ — **софт-таймер опроса ввода** (`input_poll_cb`, 5 мс; демон таймеров на +> высшем приоритете → нажатия не теряются во время рендера; opto добавится сюда в 3.4); +> `ui_task` — потребитель событий + владелец дисплея; вход долгим BUTTON_2; save на выходе; +> адрес `sul_rx_task` пере-применяет из настроек. **Макет/шрифты/тинтинг на железе — как в +> макете; адрес персистится.** +> +> **ПОЧЕМУ ОСТАНОВИЛИСЬ — один framebuffer не годится.** Единственный буфер, который ELCDIF +> сканирует по DMA пока CPU в него пишет → **tearing / частичная отрисовка**; полный `gfx_clear` +> 800×600 в некэшируемую SDRAM (~40 мс) на кадр → **тормоза**; вход нестабилен. Инкрементальная +> перерисовка (только изменившиеся строки) — костыль, дала баг (пропал футер). **Костыль убрать.** +> +> **РЕШЕНИЕ (согласовано): перенести фундамент рендера (double-buffer + PXP) из Фазы 4 в конец +> Фазы 3.** Эталон — `OLD_PROJECT_TFT8_UKL/source/display/{pxp_config.c,display.c}` + +> `main_programm.c` (см. ниже «3.2.4»). Ассеты/спрайты/layout остаются в Фазе 4. Дальше — под-шаг +> **3.2.4** (R1→R4), затем 3.3–3.6. +> +> _Флеш-грабли (в памяти):_ `host::flash-swd-app-slot-debug` шьёт **подписанный** образ +> `build/Debug/signed/app_slot_a.bin` → после правок обязательно `build::sign-app-debug`, иначе +> прошивается старый код. Вся работа Фаз 2–3 — в рабочем дереве (ветка `tft-app-dev`), не +> закоммичена (пользователь коммитит сам). + > **Согласованный дизайн (решено с пользователем; референсы — все старые проекты: > settings_manager ×3, menu ×7 вкл. ALPACA, main_programm/menu_task).** > @@ -273,13 +328,180 @@ marks_both_pending`, аппаратно на реальном обрыве св | Под-шаг | Содержание | | --- | --- | -| **3.1 `services/settings_store`** | Персист ядра (magic/version/CRC, load/save/get/defaults) через `bsp_qspi_flash`, фикс-сектор `0x450000` (§10, размер-независимо). Сразу: `proto_slice[0]` → `nku_address` в декодер (замена хардкода `=0`). | -| **3.2 движок меню + рендер** | **Чистая модель** (дерево-данные, навигация, edit, offset-привязка) — оперирует переданным `settings_t*`, **сама не сохраняет** (выставляет флаг save, app зовёт `settings_store_save()`) → host-тест без QSPI. **Рендер** (примитивы `gfx`: список/курсор/значение) в **переносимом окне 480×272 @ логич.(0,0)** (одинаково на всех панелях; на больших — левый-верхний угол, остальное чёрное). Вход — **долгое нажатие BUTTON_2** (~1.5–2 с); BUTTON_1=следующий, короткое BUTTON_2=выбор/инкремент. Опрос кнопок — задача 5 мс. Модальный экран. | +| **3.1 `services/settings_store`** ✅ | Персист ядра (magic/version/CRC, load/save/get/defaults) через `bsp_qspi_flash`, фикс-сектор `0x450000` (§10, размер-независимо). Сразу: `proto_slice[0]` → `nku_address` в декодер (замена хардкода `=0`). | +| **3.2 движок меню + рендер** ✅ (рендер — на одном буфере, см. 3.2.4) | **Чистая модель** (дерево-данные, навигация, edit, offset-привязка) — оперирует переданным `settings_t*`, **сама не сохраняет** (выставляет флаг save, app зовёт `settings_store_save()`) → host-тест без QSPI. **Рендер** (примитивы `gfx`: список/курсор/значение) в **переносимом окне 480×272 @ логич.(0,0)** (одинаково на всех панелях; на больших — левый-верхний угол, остальное чёрное). Вход — **долгое нажатие BUTTON_2** (~1.5–2 с); BUTTON_1=следующий, короткое BUTTON_2=выбор/инкремент. Опрос ввода — **софт-таймер 5 мс** (не задача; масштабируется на opto). Модальный экран. | +| **3.2.4 фундамент рендера (double-buffer + PXP)** 🟨 код-комплит, ожидает железо | Перенос из Фазы 4. Убирает tearing/тормоза, снимает костыль частичной отрисовки. Эталон — TFT8_UKL. R1→R4 реализованы (ниже), Debug/Release + host зелёные; ✅ после проверки на стенде. | | **3.3 per-protocol дескрипторы** | `sul_settings_desc_t`: протокол регистрирует параметры (NKU-CAN: адрес 0..15); секция меню строится из дескриптора. | | **3.4 opto-входы** | IN1/IN2 → вызов/ответ диспетчера → презентация (в fallback — примитив/текст; иконки-спрайты — Фаза 4/5). | | **3.5 удалённая адресация NKU-CAN** | `0x4X1`/`0x5XB` → запись `nku_address` в настройки (долг Фазы 2). | | **3.6 тумблер логов** | `log_enabled` в настройках + пункт меню + рантайм-гейт `log_set_enabled()` поверх компайл-тайм `LOG_LEVEL`. NB: продакшн-сборка — с `LOG_LEVEL >= INFO`, иначе гейтить нечего (макросы вырезаны). | +### 3.2.4 — Фундамент рендера: double-buffer + PXP (🟨 код-комплит, ожидает железо) + +> **Статус: R1→R4 реализованы, Debug/Release + host (21/21) зелёные; ожидает +> подтверждения на стенде.** По прецеденту Фаз 1/2 ✅ ставится только после +> проверки на железе (меню/индикация без tearing, вход в меню стабилен). +> +> Что сделано (решения — как согласовано): +> - **R1.** `add_sdk_driver(pxp fsl_pxp.c)` (+`sdk_clock`), gfx линкует `sdk_pxp`; +> `m_sdram_ncache` 2→8 МБ (`m_sdram` origin/len сдвинут). `fsl_memory.h` не нужен +> — `FSL_FEATURE_MEMORY_HAS_ADDRESS_OFFSET` для RT1052 не определён (identity). +> - **R2.** 4 поверхности в ncache (AS+PS+2×FB, 800×600×4 ≈ 1.92 МБ, итого 7500 КБ); +> порт `init_pxp`/`pxp_start_operation` в `services/gfx`; примитивы пишут в AS +> (alpha 0xFF), `gfx_clear()`→memset 0 (прозрачно); `gfx_present()` = PXP-композит +> AS над PS → задний FB + свап по семафору FRAME_DONE (ISR-колбэк ELCDIF, priority 2 +> == max-syscall → `xSemaphoreGiveFromISR` допустим). PS-формат — `kPXP_PsPixel +> FormatARGB8888` (расширенная таблица RT1052). Владение present — согласовано: +> **явный `gfx_present()`, оркеструет `ui_task`** (menu_view/fallback только рисуют). +> - **R3.** Костыль снят: `menu_view_render_row` + full/row-ветвление удалены, +> `menu_view_render()` рисует полный кадр; `ui_task` зовёт `gfx_present()` на +> изменение. Fallback уже полнокадровый. +> - **R4 (первая версия).** `main.c` разбит: `app/task_sul_rx.c` + `app/task_ui.c` — +> `ui_task` совмещал меню и рендер в одной задаче. +> +> **Первая HW-проверка (2026-07-22) вскрыла регресс от R1→R4:** меню не реагировало +> на кнопки вообще (ноль реакции), индикация запаздывала на 1-2 с относительно +> реального CAN-сигнала, при обрыве связи экран не сбрасывался на «--» (только +> после восстановления). Root-cause — связка меню+рендер в одной задаче: до R1→R4 +> (3.2.1–3.2.3, один framebuffer) блокировок в этой задаче не было, разделение было +> безвредным упущением; `gfx_present()` внёс блокирующее ожидание кадра +> (PXP busy-wait + semaphore-wait на FRAME_DONE), и в объединённой задаче это +> ожидание попутно блокировало проверку кнопок — `menu_task`-логика (hold-детект) +> физически не успевала выполняться между вызовами `gfx_present()`. Побочно +> вскрылась гонка: FreeRTOS-мьютекс логгера (`port/log/src/log_mutex.c`) был под +> `#if 0` и не собирался ни в один таргет — `LOG_*` из разных задач писали в общий +> `static` буфер (`utils/log/log.c`) без синхронизации. +> +> **R4 (исправлено) — 4 задачи вместо 2**, эталон разделения — +> `OLD_PROJECT_TFT8_UKL` (`BUTTONS_TASK`/`menu_task` отдельно от +> `REFRESH_TASK`/`tft_refresh_task`): +> - `bringup_task` — одноразовая инициализация (лог-мьютекс → UART/лог → QSPI/ +> settings/self-confirm → SDRAM+gfx+CAN), создаёт три нижеследующие задачи, +> удаляет себя (`vTaskDelete(NULL)`). +> - `sul_rx_task` (`task_sul_rx.c`) — CAN→decode→controller. WDOG/heartbeat — +> **безусловно**; сама CAN-работа — под `!g_menu_active` (**мягкая пауза**, не +> `vTaskSuspend` — WDOG остаётся в безопасности по конструкции, не завязан на +> suspend-состояние). +> - `menu_task` (новый, `task_menu.c`) — модель меню, потребление кнопок, +> hold-to-enter, save. НЕ рисует. Владеет `g_menu`, пишет `g_menu_active`. +> - `render_task` (был `ui_task`, `task_render.c`) — единственный вызывающий +> `gfx_present()`. **Event-driven**: `ulTaskNotifyTake(pdTRUE, portMAX_DELAY)`, +> будят `sul_rx_task` и `menu_task` (MPSC) через `xTaskNotifyGive` — не поллит. +> На «меню только что закрылось» — восстанавливает индикацию из последнего +> известного состояния сразу, не дожидаясь свежего CAN-кадра. +> - Приоритеты (`app_tasks.h`, единая точка правды, менялись — см. вторую HW- +> проверку ниже): изначально `bringup` > `menu` > `sul_rx` > `render`. Демон +> программных таймеров (`input_poll_cb`, debounce) — отдельно, +> `configTIMER_TASK_PRIORITY` (наивысший в системе), не задет. +> - `port/log/src/log_mutex.c` — снят `#if 0`, включён в сборку `app` (как +> `tick.c` — компилируется прямо в таргет, FreeRTOS-специфика); `log_mutex_init()` +> первой строкой `bringup_task`, до первого `LOG_*` где-либо в системе. +> +> **Временная диагностика** (не убрана, ждёт решения по 66 мс ниже): +> `gfx_present()` логирует раздельно длительность PXP busy-wait и +> semaphore-wait на FRAME_DONE (`LOG_I("gfx", "present: pxp=%u ms vsync=%u ms")`). +> Замерено на стенде: **pxp≈66 мс, vsync≈0 мс** — стабильно, независимо от +> содержимого кадра (похоже на фиксированную стоимость полнокадрового PXP- +> композита на этом железе/тактировании, не на баг в коде отрисовки — тактовые +> частоты (ARM PLL/AHB/IPG) идентичны `OLD_PROJECT_TFT8_UKL`, который использует +> тот же PXP+vsync паттерн). Причина ещё не тормознее ELCDIF (65 Гц по расчёту +> из `clock_config.c` — не бутылочное горлышко, vsync≈0 подтверждает). Открытый +> вопрос — см. вторую HW-проверку. +> +> --- +> +> **Вторая HW-проверка (2026-07-22, тот же день) — три новых находки:** +> +> 1. **Обрыв связи: экран не сбрасывается на «--», залипает на старом этаже** +> (по логу — НИ ОДНОГО `[gfx] present:` за 8+ с после обрыва, хотя WDOG/ +> heartbeat продолжают штатно). **Root-cause найден и подтверждён кодом:** +> `bsp_can_receive()` (`bsp/can/src/can.c`) — busy-spin БЕЗ единого +> блокирующего FreeRTOS-вызова внутри (`for(;;){poll_rx_mailboxes(); ...}`). +> Без CAN-трафика `sul_rx_task` занимает весь `CAN_RX_TIMEOUT_MS` (100 мс) +> КАЖДУЮ итерацию. Проверено по вендоренному `xTaskDelayUntil()` +> (`sdk/rtos/freertos/freertos-kernel/tasks.c`): если дедлайн уже в прошлом +> (а он в прошлом, когда спин съедает весь период) — `xShouldDelay` остаётся +> `false`, задача в delayed-list НЕ добавляется, т.е. **не блокирует вовсе**. +> Более низкоприоритетная задача никогда не выполнится, пока такая задача +> непрерывно READY — `render_task` (был ниже `sul_rx_task`) физически не +> получал CPU ни для первого рендера при старте без связи, ни для обработки +> diff'а на «--» при обрыве. При живом трафике невидимо (`receive` почти +> всегда быстрый, `sul_rx_task` реально блокируется) — поэтому не всплывало +> раньше ни разу (PLAN.md, Фаза 1, прямо отмечал: обрыв связи на реальном +> железе отдельно не гонялся). +> **Фикс:** переставлены приоритеты — `menu > render > sul_rx` +> (было `menu > sul_rx > render`). `render_task` больше не может быть +> голодом заморожен `sul_rx_task`; `menu_task` по-прежнему выше `render_task` +> (её PXP busy-wait не должен придерживать ввод). Корень (busy-spin в +> `bsp_can_receive`) НЕ тронут — общий bare-metal+FreeRTOS модуль, добавление +> yield потребовало бы условной компиляции; обсуждается отдельно. +> 2. **Меню: вход теперь работает** (подтверждает фикс R4). Остальное: +> навигация медленная (см. pxp≈66 мс выше — при открытом меню `sul_rx_task` +> не спинит, `g_menu_active` гасит её блок целиком, так что 66 мс — это +> чистая стоимость одного `gfx_present()`, не голодание); выход иногда не +> срабатывает (могло быть тем же голоданием, что и находка 1 — при отсутствии +> связи `sul_rx_task` возобновляет спин сразу по выходу из меню и мог не +> пускать `render_task` дорисовать «выход»; должно закрыться тем же фиксом +> приоритетов, перепроверить на стенде); пожелание — другая кнопка входа, +> мгновенно (без удержания), как в `OLD_PROJECT_TFT8_UKL`. +> **Согласовано и сделано:** раскладка `OLD_PROJECT_TFT8_UKL` дословно — +> короткое BUTTON_1 = вход в меню (закрыто) / следующий пункт (открыто); +> короткое BUTTON_2 = выбор/действие (открыто), намеренный no-op (закрыто). +> `MENU_ENTER_HOLD_MS`/`hold_start` убраны из `task_menu.c` целиком. +> 3. **Пустой экран при старте без связи со станцией** (прочерки появляются +> только после захода в меню) — объясняется тем же голоданием (находка 1): +> `render_task` не получал CPU для самого первого рендера, пока `sul_rx_task` +> непрерывно спинила. Должно закрыться тем же фиксом приоритетов. +> +> _Осталось для ✅ (третий цикл проверки):_ прошить подписанный +> `build/Debug/signed/app_slot_a.bin` и на TFT8: (а) старт без связи со +> станцией — прочерки видны сразу, без захода в меню; (б) обрыв связи +> мид-сессии → «--» сразу, без залипания; восстановление — сразу; (в) вход и +> выход из меню — короткими BUTTON_1/действием, срабатывают стабильно, в т.ч. +> без связи со станцией. Плюс субъективно оценить навигацию в меню (66 мс/шаг +> — приемлемо или нужно отдельно разбираться со снижением стоимости +> `gfx_present()` для модального окна меню) — открытый вопрос, решить по +> итогам этой проверки. + + + +> **Эталон — `OLD_PROJECT_TFT8_UKL/source/display/`** (`pxp_config.c`, `display.c`) + +> `main_programm.c` (refresh-цикл ~строки 1052–1095). Наш `bsp_display` уже даёт нужный API: +> `bsp_display_init(type, fb, on_frame_done)` (ISR-колбэк конца кадра) + `bsp_display_set_next_buffer(addr)`. +> `fsl_pxp.{c,h}` есть в SDK (`sdk/devices/MIMXRT1052/drivers/`), но **не собран** как target. + +**Модель (как в TFT8_UKL):** CPU рисует **весь кадр** в **AS** (`alpha_buffer`, ARGB8888, alpha +значим: opaque=0xFF, «пусто»=0). **PS** (`processing_buffer`) — фон (сейчас сплошной чёрный, раз +заливается; style-картинка — Фаза 5). PXP блендит **AS+PS → задний framebuffer** (`pxp_start_operation`, +busy-wait Complete), затем свап: `xSemaphoreTake(frame_done)` (синх с ELCDIF) → `bsp_display_set_next_buffer(back)` → +`current_buffer_index ^= 1`. Double-buffer = tear-free (рисуем off-screen, свап атомарный) → **костыль +инкрементальной отрисовки удаляем, рисуем полный кадр.** + +**Буферы** (некэшируемая SDRAM, `AT_NONCACHEABLE_SECTION_ALIGN`): `alpha_buffer` (AS) + +`processing_buffer` (PS) + `framebuffer[2]` — каждый 800×600×4 = 1.92 МБ → **растим `m_sdram_ncache` +2→8 МБ** в `cmake/linker/MIMXRT1052xxxxx_app_slot.ld` (степень 2, MPU-выравнивание через +`__NCACHE_REGION_SIZE`). + +**Sub-шаги (собирать после каждого; сборка/host-тесты в devcontainer, флеш — пользователь):** +- **R1.** SDK: добавить `fsl_pxp` в сборку (новый target либо в gfx). Линкер: `m_sdram_ncache` 2→8 МБ. +- **R2. gfx → компоновщик:** AS/PS/2×FB в ncache; порт `init_pxp`/`pxp_start_operation`; gfx рисует + в **AS** (примитивы пишут alpha 0xFF; `gfx_clear` → AS прозрачный, memset 0); `gfx_present()` = + PXP-композит AS+PS → задний FB + свап по `frame_done`-семафору; `gfx_init` регистрирует колбэк + (даёт семафор), заливает PS чёрным, стартует ELCDIF на FB[0]. +- **R3. Презентация:** рисуем полный кадр в AS → `gfx_present()`. **Удалить костыль** `menu_view_render_row` + / логику «full/row» в `ui_task`; `menu_view` рисует полный кадр. Fallback — так же в AS. +- **R4. Разбить `main.c`.** Первая версия — `app/task_sul_rx.c` + `app/task_ui.c` (презентация+ + меню+таймер ввода в одной задаче) — HW-проверка вскрыла регресс (меню не реагирует на кнопки, + см. статус выше). Исправлено на 4 задачи: `bringup_task`/`sul_rx_task`/`menu_task`/`render_task` + (`app/task_{bringup,sul_rx,menu,render}.c`), контракт — `app/app_tasks.h`. `main.c` = только + `main()` (board init, объекты, ОДИН `bringup_task`, scheduler) + FreeRTOS-хуки. (ARCH §4: app = + задачи + wiring + main.) + +**Решения (согласованы):** PXP-код — внутри `services/gfx` (gfx владеет компоновщиком, как `display/` +в TFT8_UKL); PS сейчас — сплошной чёрный; ассеты/спрайты/PNG/layout-компоновка остаются в Фазе 4 +(там же §PLAN Фаза 4 «Double buffering в gfx» — теперь закрывается здесь, в Фазе 4 остаётся только +PXP-блит спрайтов и layout). + **Тесты (host).** Сериализация/дефолты/CRC ядра настроек; логика навигации меню; связывание дескрипторов; распознавание команды удалённой адресации; рантайм-гейт логов. diff --git a/firmware/tft_app/src/app/app_tasks.h b/firmware/tft_app/src/app/app_tasks.h new file mode 100644 index 0000000..0bad698 --- /dev/null +++ b/firmware/tft_app/src/app/app_tasks.h @@ -0,0 +1,127 @@ +/** + * @file app_tasks.h + * @brief Контракт между задачами app-слоя (wiring, ARCH §4). + * + * Четыре задачи (найдено на HW-верификации Фазы 3.2.4 — меню и render в одной + * задаче оказались взаимно неотзывчивы, см. PLAN.md): + * - bringup_task — одноразовая инициализация (UART/QSPI/settings/self-confirm/ + * SDRAM+gfx+CAN), затем создаёт три нижеследующие задачи и удаляет себя. + * - sul_rx_task — приём CAN → decode → controller. WDOG/heartbeat кормятся + * БЕЗУСЛОВНО; сама CAN-работа — под !g_menu_active (мягкая пауза на время + * меню — WDOG остаётся в безопасности по конструкции). + * - menu_task — модель меню: потребление кнопок, hold-to-enter, + * навигация/edit/exit+save. НЕ рисует. + * - render_task — единственный владелец дисплея/вызывающий gfx_present(). + * Event-driven (будится xTaskNotifyGive от sul_rx_task И menu_task — + * MPSC), не поллит, не содержит кнопочной логики. + * + * main.c создаёt только очередь/софт-таймер ввода/bringup_task — сами задачи + * друг друга создают/не создают по схеме выше, main.c про это не знает. + */ + +#ifndef APP_TASKS_H_ +#define APP_TASKS_H_ + +#include "FreeRTOS.h" +#include "domain/controller.h" /* indication_task_t */ +#include "domain/elevator_model.h" /* sul_result_t */ +#include "menu/menu.h" /* menu_ctx_t */ +#include "queue.h" +#include "task.h" +#include "timers.h" + +#include + +/* ── Приоритеты (единая точка правды, tskIDLE_PRIORITY-относительно) ──────── + * + * menu_task > render_task > sul_rx_task. Найдено на HW-верификации (см. + * PLAN.md): `bsp_can_receive()` — busy-spin БЕЗ yield (bsp/can/src/can.c, + * никаких блокирующих FreeRTOS-вызовов внутри `for(;;){poll;...}`); пока нет + * CAN-трафика, sul_rx_task занимает весь CAN_RX_TIMEOUT_MS каждую итерацию, и + * xTaskDelayUntil() в этом случае НЕ блокирует вовсе (дедлайн уже в прошлом — + * см. tasks.c, xShouldDelay остаётся false). Более низкоприоритетная задача + * никогда не получает CPU, пока такая задача продолжает быть READY — + * render_task, будучи ниже sul_rx_task, банально не мог выполниться (ни + * первый рендер при старте без связи, ни обработка «--» при обрыве связи). + * При живом трафике это было невидимо (receive почти всегда быстрый, + * sul_rx_task реально блокируется), поэтому не всплывало раньше. + * + * menu_task остаётся ВЫШЕ render_task: её собственный busy-wait внутри PXP + * (gfx_pxp_run, ~60-100 мс по замерам) не должен придерживать ввод — то же + * свойство, ради которого меню и рендер разведены по разным задачам. + * bringup_task — выше всех троих (монополизирует CPU на время одноразовой + * инициализации, пока остальные задачи ещё не созданы). Демон программных + * таймеров (bsp_button debounce, input_poll_cb) — отдельно, на + * configTIMER_TASK_PRIORITY (см. FreeRTOSConfig.h — наивысший в системе), не + * отсюда. + * + * Корень (busy-spin в bsp_can_receive без yield) НЕ тронут — это отдельный, + * более рискованный шаг (bsp_can — общий модуль, используется и bare-metal + * таргетами; добавить FreeRTOS-yield внутрь потребует условной компиляции, + * как bsp_tick/log_mutex). Обсудить отдельно, если приоритетов недостаточно. */ +#define APP_PRIORITY_BRINGUP (tskIDLE_PRIORITY + 4U) +#define APP_PRIORITY_MENU (tskIDLE_PRIORITY + 3U) +#define APP_PRIORITY_RENDER (tskIDLE_PRIORITY + 2U) +#define APP_PRIORITY_SUL_RX (tskIDLE_PRIORITY + 1U) + +/** Общий стек-бюджет app-тасков (см. PLAN.md, Фаза 0 — тонкий стек уже + * маскировался под похожий на зависание симптом; x8 — с запасом, + * проверено на всех четырёх ролях). */ +#define APP_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 8U) + +/** + * @brief Самое свежее состояние индикации (не история): sul_rx_task → render_task. + * + * Очередь глубины 1 с xQueueOverwrite() — важно только ПОСЛЕДНЕЕ состояние, + * не промежуточные кадры (рендер не обязан успевать за каждым). Пробуждение + * render_task — отдельно, через xTaskNotifyGive (см. g_render_task_handle). + */ +typedef struct +{ + indication_task_t task; + sul_result_t result; +} render_msg_t; + +/** Очередь глубины 1 (создаётся в main). Продюсер — sul_rx_task, консюмер — render_task. */ +extern QueueHandle_t g_render_queue; + +/** Дисплей+CAN подняты (bring-up в bringup_task) — sul_rx_task/render_task ждут этого. */ +extern volatile bool g_display_ready; + +/** true, пока меню открыто. Мягкая пауза sul_rx_task (CAN-decode/controller/ + * очередь пропускаются, WDOG/heartbeat — нет, см. task_sul_rx.c). Единственный + * писатель — menu_task, единственный читатель — sul_rx_task. */ +extern volatile bool g_menu_active; + +/** Модель меню. Владеет и мутирует ТОЛЬКО menu_task; render_task только читает + * для отрисовки (после xTaskNotifyGive — happens-before через нотификацию, + * как у g_render_queue). */ +extern menu_ctx_t g_menu; + +/** Хэндл render_task — sul_rx_task и menu_task (MPSC-продюсеры) будят его + * xTaskNotifyGive() на любое изменение состояния. Устанавливается + * bringup_task ДО создания sul_rx_task/menu_task. */ +extern TaskHandle_t g_render_task_handle; + +/** Одноразовая инициализация (UART/QSPI/settings/confirm_self/SDRAM+gfx+CAN), + * затем создаёт sul_rx_task/menu_task/render_task и удаляет себя. */ +void bringup_task(void *p_arg); + +/** Приём CAN + WDOG/heartbeat (безусловно) + decode/controller (под !g_menu_active). */ +void sul_rx_task(void *p_arg); + +/** Модель меню: потребление кнопок, hold-to-enter, навигация/edit/exit+save. */ +void menu_task(void *p_arg); + +/** Презентация: владелец дисплея, единственный вызывающий gfx_present(). */ +void render_task(void *p_arg); + +/** Колбэк софт-таймера debounce (bsp_button_poll) — main создаёт таймер, каденция в main. */ +void input_poll_cb(TimerHandle_t x_timer); + +/** Диагностика трейлера слота (read-only, безопасно звать многократно) — + * общая для bringup_task (before/after-confirm) и sul_rx_task (периодический + * re-log). Определена в task_bringup.c. */ +void log_slot_status(const char *p_when); + +#endif /* APP_TASKS_H_ */ diff --git a/firmware/tft_app/src/app/main.c b/firmware/tft_app/src/app/main.c index 45bfa74..f7ee900 100644 --- a/firmware/tft_app/src/app/main.c +++ b/firmware/tft_app/src/app/main.c @@ -1,336 +1,59 @@ /** * @file main.c - * @brief tft_app — точка входа. + * @brief tft_app — точка входа: board init, объекты, bringup_task, планировщик, хуки. + * + * ARCH §4: app-слой = задачи + wiring + main. main() создаёт только очередь, + * софт-таймер ввода и ОДНУ задачу (bringup_task) — она сама создаёт + * sul_rx_task/menu_task/render_task после одноразовой инициализации и + * удаляет себя (см. app_tasks.h — полный контракт между задачами). * * Фаза 0 (каркас, подтверждено на железе): образ линкуется как валидный * MCUboot-слот (Direct-XIP), bootloader в него прыгает, FreeRTOS стартует, - * WDOG кормится, образ ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной - * загрузке — anti-brick сохранён). Контракт с bootloader и обоснование - * self-confirm/QSPI-ramfunc/UART-диагностики — см. PLAN.md, Фаза 0. - * - * Фаза 1 (walking skeleton): реальный CAN-кадр НКУ-CAN → чистый декодер → - * контроллер → fallback-рендер (позиция + стрелка). Две задачи: - * - sul_rx_task — WDOG/heartbeat (унаследовано от Фазы 0) + приём CAN + - * decode + controller_process(); на изменение — в очередь render_task. - * - render_task — потребляет очередь, зовёт ui_fallback_render(). - * Очередь глубиной 1 с xQueueOverwrite() — важно только ПОСЛЕДНЕЕ состояние, - * не история промежуточных кадров (render не обязан успевать за каждым). + * WDOG кормится, образ подтверждает себя в рантайме — см. PLAN.md, Фаза 0. */ #include "FreeRTOS.h" +#include "app_tasks.h" #include "board.h" -#include "bootutil/bootutil_public.h" -#include "bsp/boot_state.h" -#include "bsp/display.h" +#include "bsp/button.h" #include "bsp/led.h" -#include "bsp/qspi_flash.h" -#include "bsp/sdram.h" -#include "bsp/uart_host.h" -#include "bsp/wdog.h" -#include "domain/controller.h" -#include "domain/elevator_model.h" -#include "domain/sul.h" -#include "domain/sul/nku_can.h" -#include "domain/sul/transport/can.h" -#include "flash_map.h" -#include "log/log.h" -#include "port/log_uart.h" #include "queue.h" -#include "services/gfx.h" -#include "services/settings_store.h" #include "task.h" -#include "ui/fallback.h" +#include "timers.h" #include -#define LOG_TAG "app" +#define INPUT_POLL_PERIOD_MS 5U /* софт-таймер опроса ввода (bsp_button_poll; opto — Фаза 3.4) */ -/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */ -#ifndef APP_OWN_SLOT_ID -#define APP_OWN_SLOT_ID 0 -#endif +/* ── Разделяемое состояние задач (объявления — app_tasks.h) ──────────────── */ -/* Панель текущего стенда — Фаза 1 хардкод (Фаза 9: provisioning выбирает из - * settings, см. ARCH §9 — TFT7/8/10 рантайм-выбор внутри профиля app-big). */ -#define PANEL_TYPE BSP_DISPLAY_TFT8 - -#define HEARTBEAT_PERIOD_MS 500U -#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */ -#define STATUS_LOG_PERIOD_MS \ - 2000U /* периодический re-log трейлера — виден независимо - * от момента подключения терминала */ -#define CAN_RX_TIMEOUT_MS 100U /* держит цикл отзывчивым к WDOG/heartbeat-каденции */ -#define CONNECTION_TIMEOUT_MS \ - 3000U /* «пропадание трафика» — см. ARCH, поток данных: - * poll + timeout→default. Порядок величины — как - * в OLD_PROJECT (там ~3 с на отметку потери связи) */ - -/* TEMP (Фаза 3.1): двухзагрузочная HW-проверка пути save/персист. УБРАТЬ в 3.2, - * когда save начнёт вызываться из меню. Сентинел в ИНЕРТНОМ поле max_load_kg - * (рендер грузоподъёмности — Фаза 5), proto_slice/адрес НЕ трогаем — индикатор - * продолжает работать с адресом 0. */ -#define SETTINGS_SELFTEST_SENTINEL 4242U - -/* Передача render_task самого свежего состояния — не истории. */ -typedef struct -{ - indication_task_t task; - sul_result_t result; -} render_msg_t; - -static QueueHandle_t g_s_render_queue; - -/* ── Диагностика трейлера слота (read-only, безопасно звать многократно) ── */ - -static void log_slot_status(const char *p_when) -{ - const struct flash_area *p_fap; - const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap); - if (RC_OPEN != 0) - { - LOG_E(LOG_TAG, "%s: flash_area_open(slot%d) rc=%d", p_when, APP_OWN_SLOT_ID, RC_OPEN); - return; - } - - struct boot_swap_state st = { 0 }; - const int RC_RD = boot_read_swap_state(p_fap, &st); - LOG_I(LOG_TAG, "%s: slot%d magic=%d copy_done=%d image_ok=%d (rd=%d)", p_when, APP_OWN_SLOT_ID, - st.magic, st.copy_done, st.image_ok, RC_RD); - - flash_area_close(p_fap); -} - -/** - * @brief Подтвердить СОБСТВЕННЫЙ слот (APP_OWN_SLOT_ID). - * - * boot_set_next(fap, active=true, confirm=true), НЕ boot_set_confirmed(): та - * жёстко пишет в FLASH_AREA_IMAGE_PRIMARY (Slot A) независимо от исполняемого - * слота — для Direct-XIP с двумя слотами это подтвердило бы не тот при - * исполнении из Slot Б. - */ -static void confirm_self(void) -{ - log_slot_status("before-confirm"); /* ожидаем magic=1(GOOD) image_ok=3(UNSET) */ - - const struct flash_area *p_fap; - const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap); - if (RC_OPEN != 0) - { - LOG_E(LOG_TAG, "confirm: flash_area_open(slot%d) rc=%d", APP_OWN_SLOT_ID, RC_OPEN); - return; - } - - const int RC_SET = boot_set_next(p_fap, true, true); - LOG_I(LOG_TAG, "confirm: boot_set_next rc=%d", RC_SET); - - flash_area_close(p_fap); - - log_slot_status("after-confirm"); /* ожидаем image_ok=1(SET) */ -} - -/** - * @brief SDRAM (SEMC, без DCD) + framebuffer + CAN — до создания render_task. - * - * bsp_sdram_configure()+init() — та же документированная в bsp/sdram.h связка - * для «прошивок без DCD», которую использует и bootloader-smoke-test. - */ -static bool bring_up_display_and_can(void) -{ - if (bsp_sdram_configure() != BSP_OK) - { - LOG_E(LOG_TAG, "bsp_sdram_configure FAILED"); - return false; - } - if (bsp_sdram_init() != BSP_OK) - { - LOG_E(LOG_TAG, "bsp_sdram_init FAILED"); - return false; - } - if (gfx_init(PANEL_TYPE) != BSP_OK) - { - LOG_E(LOG_TAG, "gfx_init FAILED"); - return false; - } - if (sul_transport_can_init() != BSP_OK) - { - LOG_E(LOG_TAG, "sul_transport_can_init FAILED"); - return false; - } - - LOG_I(LOG_TAG, "display+CAN bring-up OK"); - return true; -} - -static void render_task(void *p_arg) -{ - (void) p_arg; - - /* Безусловная первая отрисовка — не ждём первого diff (см. ui/fallback.h). */ - const sul_result_t INITIAL = sul_default_state(); - ui_fallback_render_initial(&INITIAL); - - render_msg_t msg; - for (;;) - { - if (xQueueReceive(g_s_render_queue, &msg, portMAX_DELAY) == pdTRUE) - { - ui_fallback_render(&msg.task, &msg.result); - } - } -} - -/* TEMP (Фаза 3.1): проверка save/персист через power cycle. Первый запуск — - * пишет сентинел и просит перезагрузку; после перезагрузки load() читает его с - * флеша → «PERSIST OK». Сентинел остаётся (инертен), затирается первым - * сохранением из меню (3.2) или factory-reset. УБРАТЬ вместе с #define в 3.2. */ -static void settings_selftest(void) -{ - settings_t *p_s = settings_store_get_mutable(); - if (p_s->user.max_load_kg == SETTINGS_SELFTEST_SENTINEL) - { - LOG_I(LOG_TAG, "settings self-test: PERSIST OK (сентинел пережил перезагрузку)"); - return; - } - - p_s->user.max_load_kg = SETTINGS_SELFTEST_SENTINEL; - const bsp_status_t RC = settings_store_save(); - LOG_I(LOG_TAG, - "settings self-test: записан сентинел rc=%d — СДЕЛАЙТЕ POWER-CYCLE для проверки персиста", - RC); -} - -static void sul_rx_task(void *p_arg) -{ - (void) p_arg; - - /* LPUART1/MCU-Link VCOM — доступен сразу, без enumeration/wait (в отличие - * от target-side USB CDC). log_mutex не нужен — единственный писатель. */ - (void) bsp_uart_host_init(115200U); - log_uart_init(); - - /* flash_map_backend требует bsp_qspi_init() ДО любой flash_area_*. */ - const bool QSPI_OK = (bsp_qspi_init() == BSP_OK); - LOG_I(LOG_TAG, "tft_app phase1 boot: qspi=%s", QSPI_OK ? "OK" : "FAIL"); - - /* Настройки ядра (§8, §10): с флеша если QSPI поднялся, иначе дефолты. */ - if (QSPI_OK) - { - const bsp_status_t S_RC = settings_store_load(); - LOG_I(LOG_TAG, "settings: load rc=%d proto_addr=%u", S_RC, - settings_store_get()->user.proto_slice[0]); - settings_selftest(); /* TEMP (3.1): HW-проверка save/персист, убрать в 3.2 */ - } - else - { - settings_store_init_defaults(); - } - - /* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки - * (recovery загрузчика). SRC GPR, без flash. Безусловно, до потенциально - * рискованного bring-up дисплея/CAN ниже. */ - bsp_boot_health_mark(); - - if (QSPI_OK) - { - confirm_self(); - } - else - { - LOG_E(LOG_TAG, "qspi_init FAILED — self-confirm skipped, slot will revert"); - } - - const bool DISPLAY_CAN_OK = bring_up_display_and_can(); - - nku_can_ctx_t nku_ctx; - nku_can_init(&nku_ctx); - /* Адрес станции из настроек (§8, proto_slice[0]) → сдвиг ID пакетов. */ - nku_can_set_address(&nku_ctx, settings_store_get()->user.proto_slice[0]); - - controller_ctx_t ctrl_ctx; - controller_init(&ctrl_ctx); - - const TickType_t FEED_PERIOD = pdMS_TO_TICKS(WDOG_FEED_PERIOD_MS); - uint32_t elapsed_ms = 0U; - uint32_t since_status_ms = 0U; - TickType_t last_wake = xTaskGetTickCount(); - TickType_t last_frame_tick = xTaskGetTickCount(); - - for (;;) - { - bsp_wdog_refresh(); - - elapsed_ms += WDOG_FEED_PERIOD_MS; - if (elapsed_ms >= HEARTBEAT_PERIOD_MS) - { - elapsed_ms = 0U; - bsp_led_toggle(LED_APP); - } - - since_status_ms += WDOG_FEED_PERIOD_MS; - if (since_status_ms >= STATUS_LOG_PERIOD_MS) - { - since_status_ms = 0U; - log_slot_status("periodic"); - } - - if (DISPLAY_CAN_OK) - { - sul_result_t decoded; - bool have_update = false; - - sul_frame_t frame; - if (sul_transport_can_receive(CAN_RX_TIMEOUT_MS, &frame) == BSP_OK) - { - const sul_driver_t *p_driver = sul_registry_active(); - if (p_driver->decode(&nku_ctx, &frame, &decoded) == SUL_STATUS_OK) - { - last_frame_tick = xTaskGetTickCount(); - have_update = true; - } - /* IGNORED/ERR — Фаза 1 их отдельно не обрабатывает, следующая итерация. */ - } - - const uint32_t SINCE_FRAME_MS = - (uint32_t) (xTaskGetTickCount() - last_frame_tick) * portTICK_PERIOD_MS; - if (SINCE_FRAME_MS >= CONNECTION_TIMEOUT_MS) - { - /* poll + timeout→default (ARCH, поток данных) — controller сам - * определит, реальное ли это изменение (не сработает повторно - * на каждой итерации после первого перехода в default). */ - decoded = sul_default_state(); - have_update = true; - } - - if (have_update) - { - const indication_task_t DIFF = controller_process(&ctrl_ctx, &decoded); - if (DIFF.pos_pending || DIFF.direction_pending) - { - const render_msg_t MSG = { .task = DIFF, .result = decoded }; - (void) xQueueOverwrite(g_s_render_queue, &MSG); - } - } - } - - vTaskDelayUntil(&last_wake, FEED_PERIOD); - } -} +volatile bool g_display_ready = false; +volatile bool g_menu_active = false; +QueueHandle_t g_render_queue = NULL; +TaskHandle_t g_render_task_handle = NULL; int main(void) { board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */ bsp_led_init(); + (void) bsp_button_init(); /* GPIO настроен в BOARD_InitPins; сброс debounce */ - g_s_render_queue = xQueueCreate(1, sizeof(render_msg_t)); - configASSERT(g_s_render_queue != NULL); + g_render_queue = xQueueCreate(1, sizeof(render_msg_t)); + configASSERT(g_render_queue != NULL); - /* sul_rx выше приоритетом render — приём CAN/WDOG важнее своевременности - * перерисовки. Стеки x6 (3 КБ) с запасом (см. PLAN.md, Фаза 0 — тонкий - * стек уже маскировался под похожий на зависание симптом). */ - (void) xTaskCreate(sul_rx_task, "sul_rx", configMINIMAL_STACK_SIZE * 6U, NULL, - tskIDLE_PRIORITY + 2U, NULL); - (void) xTaskCreate(render_task, "render", configMINIMAL_STACK_SIZE * 6U, NULL, - tskIDLE_PRIORITY + 1U, NULL); + /* Опрос ввода — софт-таймер (демон на высшем приоритете в системе вытесняет + * всё остальное — нажатия не теряются). Масштабируется на opto (Фаза 3.4) + * тем же колбэком. */ + TimerHandle_t input_timer = + xTimerCreate("input", pdMS_TO_TICKS(INPUT_POLL_PERIOD_MS), pdTRUE, NULL, input_poll_cb); + configASSERT(input_timer != NULL); + (void) xTimerStart(input_timer, 0); + + /* Единственная задача, которую создаёт main — bringup_task сама создаст + * sul_rx_task/menu_task/render_task после инициализации и удалит себя + * (см. app_tasks.h про приоритеты/порядок). */ + (void) xTaskCreate(bringup_task, "bringup", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_BRINGUP, + NULL); vTaskStartScheduler(); diff --git a/firmware/tft_app/src/app/task_bringup.c b/firmware/tft_app/src/app/task_bringup.c new file mode 100644 index 0000000..b47409b --- /dev/null +++ b/firmware/tft_app/src/app/task_bringup.c @@ -0,0 +1,186 @@ +/** + * @file task_bringup.c + * @brief Одноразовое bring-up: лог-мьютекс, UART/лог, QSPI+settings+self-confirm, + * SDRAM+gfx+CAN. Затем создаёт steady-state задачи и удаляет себя. + * + * Слот образа ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной загрузке — + * anti-brick). Контракт с bootloader и обоснование self-confirm/QSPI-ramfunc/ + * UART-диагностики — см. PLAN.md, Фаза 0. + * + * Выделено в отдельную задачу на Фазе 3.2.4 (было частью sul_rx_task) — по + * итогам HW-верификации: одноразовая init-последовательность архитектурно не + * то же самое, что вечный CAN-цикл, и разделение делает обе задачи проще + * контролировать по отдельности. + */ + +#include "app_tasks.h" + +#include "FreeRTOS.h" +#include "bootutil/bootutil_public.h" +#include "bsp/boot_state.h" +#include "bsp/display.h" +#include "bsp/qspi_flash.h" +#include "bsp/sdram.h" +#include "bsp/uart_host.h" +#include "domain/sul/transport/can.h" +#include "flash_map.h" +#include "log/log.h" +#include "port/log_uart.h" +#include "services/gfx.h" +#include "services/settings_store.h" +#include "task.h" + +#include + +#define LOG_TAG "bringup" + +/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */ +#ifndef APP_OWN_SLOT_ID +#define APP_OWN_SLOT_ID 0 +#endif + +/* Панель текущего стенда — Фаза 1 хардкод (Фаза 9: provisioning выбирает из + * settings, см. ARCH §9 — TFT7/8/10 рантайм-выбор внутри профиля app-big). */ +#define PANEL_TYPE BSP_DISPLAY_TFT8 + +/* ── Диагностика трейлера слота (read-only, безопасно звать многократно). + * Общая для bringup_task (before/after-confirm) и sul_rx_task (periodic) — + * прототип в app_tasks.h. ──────────────────────────────────────────────── */ +void log_slot_status(const char *p_when) +{ + const struct flash_area *p_fap; + const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap); + if (RC_OPEN != 0) + { + LOG_E(LOG_TAG, "%s: flash_area_open(slot%d) rc=%d", p_when, APP_OWN_SLOT_ID, RC_OPEN); + return; + } + + struct boot_swap_state st = { 0 }; + const int RC_RD = boot_read_swap_state(p_fap, &st); + LOG_I(LOG_TAG, "%s: slot%d magic=%d copy_done=%d image_ok=%d (rd=%d)", p_when, APP_OWN_SLOT_ID, + st.magic, st.copy_done, st.image_ok, RC_RD); + + flash_area_close(p_fap); +} + +/** + * @brief Подтвердить СОБСТВЕННЫЙ слот (APP_OWN_SLOT_ID). + * + * boot_set_next(fap, active=true, confirm=true), НЕ boot_set_confirmed(): та + * жёстко пишет в FLASH_AREA_IMAGE_PRIMARY (Slot A) независимо от исполняемого + * слота — для Direct-XIP с двумя слотами это подтвердило бы не тот при + * исполнении из Slot Б. + */ +static void confirm_self(void) +{ + log_slot_status("before-confirm"); /* ожидаем magic=1(GOOD) image_ok=3(UNSET) */ + + const struct flash_area *p_fap; + const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap); + if (RC_OPEN != 0) + { + LOG_E(LOG_TAG, "confirm: flash_area_open(slot%d) rc=%d", APP_OWN_SLOT_ID, RC_OPEN); + return; + } + + const int RC_SET = boot_set_next(p_fap, true, true); + LOG_I(LOG_TAG, "confirm: boot_set_next rc=%d", RC_SET); + + flash_area_close(p_fap); + + log_slot_status("after-confirm"); /* ожидаем image_ok=1(SET) */ +} + +/** + * @brief SDRAM (SEMC, без DCD) + компоновщик gfx + CAN — перед steady-state задачами. + * + * bsp_sdram_configure()+init() — та же документированная в bsp/sdram.h связка + * для «прошивок без DCD», которую использует и bootloader-smoke-test. + */ +static bool bring_up_display_and_can(void) +{ + if (bsp_sdram_configure() != BSP_OK) + { + LOG_E(LOG_TAG, "bsp_sdram_configure FAILED"); + return false; + } + if (bsp_sdram_init() != BSP_OK) + { + LOG_E(LOG_TAG, "bsp_sdram_init FAILED"); + return false; + } + if (gfx_init(PANEL_TYPE) != BSP_OK) + { + LOG_E(LOG_TAG, "gfx_init FAILED"); + return false; + } + if (sul_transport_can_init() != BSP_OK) + { + LOG_E(LOG_TAG, "sul_transport_can_init FAILED"); + return false; + } + + LOG_I(LOG_TAG, "display+CAN bring-up OK"); + return true; +} + +void bringup_task(void *p_arg) +{ + (void) p_arg; + + /* ДО первого LOG_* где-либо в системе — log_write() берёт мьютекс + * безусловно (port/log/src/log_mutex.c, strong-override weak-NOP). + * bringup_task — первая и пока единственная запущенная задача, так что + * порядок гарантирован конструкцией (main создаёт только её). */ + log_mutex_init(); + + /* LPUART1/MCU-Link VCOM — доступен сразу, без enumeration/wait (в отличие + * от target-side USB CDC). */ + (void) bsp_uart_host_init(115200U); + log_uart_init(); + + /* flash_map_backend требует bsp_qspi_init() ДО любой flash_area_*. */ + const bool QSPI_OK = (bsp_qspi_init() == BSP_OK); + LOG_I(LOG_TAG, "tft_app boot: qspi=%s", QSPI_OK ? "OK" : "FAIL"); + + /* Настройки ядра (§8, §10): с флеша если QSPI поднялся, иначе дефолты. */ + if (QSPI_OK) + { + const bsp_status_t S_RC = settings_store_load(); + LOG_I(LOG_TAG, "settings: load rc=%d proto_addr=%u", S_RC, + settings_store_get()->user.proto_slice[0]); + } + else + { + settings_store_init_defaults(); + } + + /* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки + * (recovery загрузчика). SRC GPR, без flash. Безусловно, до потенциально + * рискованного bring-up дисплея/CAN ниже. */ + bsp_boot_health_mark(); + + if (QSPI_OK) + { + confirm_self(); + } + else + { + LOG_E(LOG_TAG, "qspi_init FAILED — self-confirm skipped, slot will revert"); + } + + g_display_ready = bring_up_display_and_can(); /* sul_rx_task/render_task ждут этого */ + + /* render_task — первым: его хэндл нужен sul_rx_task/menu_task для + * xTaskNotifyGive. Порядок формально некритичен (bringup_task — + * наивысший приоритет из четырёх и монополизирует CPU до своего + * удаления — никто из троих не может выполниться раньше, чем все три + * xTaskCreate() ниже отработают), но документирует зависимость явно. */ + (void) xTaskCreate(render_task, "render", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_RENDER, + &g_render_task_handle); + (void) xTaskCreate(sul_rx_task, "sul_rx", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_SUL_RX, NULL); + (void) xTaskCreate(menu_task, "menu", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_MENU, NULL); + + vTaskDelete(NULL); /* одноразовая задача — дальше нечего делать */ +} diff --git a/firmware/tft_app/src/app/task_menu.c b/firmware/tft_app/src/app/task_menu.c new file mode 100644 index 0000000..a8ae835 --- /dev/null +++ b/firmware/tft_app/src/app/task_menu.c @@ -0,0 +1,114 @@ +/** + * @file task_menu.c + * @brief Модель меню: потребление кнопок (debounce — bsp_button/софт-таймер, + * независимо от этой задачи), мгновенные вход/навигация/edit/exit+save. + * + * Раскладка — как в OLD_PROJECT_TFT8_UKL (без удержания, короткие нажатия): + * короткое BUTTON_1 = вход в меню (когда закрыто) / следующий пункт (когда + * открыто); короткое BUTTON_2 = выбор/действие (когда открыто), намеренный + * no-op вне меню. + * + * НЕ рисует — мутирует `g_menu` и будит render_task (xTaskNotifyGive) на любое + * изменение состояния. Разделено от render_task на Фазе 3.2.4 (HW-находка): + * раньше (3.2.1–3.2.3, один framebuffer, без ожиданий) потребление кнопок и + * рендер жили в одной задаче безвредно — блокировок не было. gfx_present() + * (double-buffer + PXP) внёс блокирующее ожидание кадра; в объединённой + * задаче это ожидание попутно блокировало вход в меню (ноль реакции на + * кнопки — см. PLAN.md). Эталон разделения — OLD_PROJECT_TFT8_UKL: + * BUTTONS_TASK/menu_task отдельно от REFRESH_TASK/tft_refresh_task. + */ + +#include "app_tasks.h" + +#include "FreeRTOS.h" +#include "bsp/button.h" +#include "log/log.h" +#include "menu/menu.h" +#include "menu/menu_tree.h" +#include "services/settings_store.h" +#include "task.h" +#include "timers.h" + +#include + +#define LOG_TAG "menu" + +#define MENU_TICK_MS 5U /* каденция потребления кнопок */ + +menu_ctx_t g_menu; + +/* Софт-таймер (высший приоритет демона) опрашивает debounce независимо от + * menu_task/render_task — нажатия не теряются, пока кто-то из них занят. + * Колбэк короткий, без блокировок. */ +void input_poll_cb(TimerHandle_t x_timer) +{ + (void) x_timer; + bsp_button_poll(); + /* Фаза 3.4: bsp_opto_process(); (диспетчерские вход/ответ) */ +} + +void menu_task(void *p_arg) +{ + (void) p_arg; + + menu_init(&g_menu, menu_tree_items(), menu_tree_count(), settings_store_get_mutable()); + + TickType_t last_wake = xTaskGetTickCount(); + + for (;;) + { + bool changed = false; + + /* Порядок и структура — как в OLD_PROJECT_TFT8_UKL menu_task(): + * BUTTON_2 читается и обрабатывается только "внутри", BUTTON_1 — + * читается всегда (вход ИЛИ навигация в зависимости от open). Оба + * события дренируются безусловно (один get_event_pressed на кнопку + * за итерацию) — не копится устаревшее состояние. */ + if (bsp_button_get_event_pressed(BSP_BUTTON_2)) + { + if (menu_is_open(&g_menu)) + { + menu_action(&g_menu); + + /* Обновить ДО settings_store_save() (флеш-запись, не + * мгновенная) — иначе sul_rx_task ещё несколько мс видел бы + * устаревший g_menu_active=true и держал бы мягкую паузу + * дольше нужного. */ + g_menu_active = menu_is_open(&g_menu); + + if (!g_menu_active && g_menu.save_requested) + { + /* Выход: сохранить (если менялось). Адрес подхватит + * sul_rx_task из настроек на следующей итерации. */ + const bsp_status_t RC = settings_store_save(); + LOG_I(LOG_TAG, "settings saved rc=%d", RC); + } + changed = true; + } + /* иначе — намеренный no-op вне меню (как в референсе) */ + } + else if (bsp_button_get_event_pressed(BSP_BUTTON_1)) + { + if (menu_is_open(&g_menu)) + { + menu_next(&g_menu); + } + else + { + menu_open(&g_menu); /* мгновенный вход, без удержания */ + } + changed = true; + } + + /* Финальная синхронизация флага мягкой паузы с моделью — покрывает + * вход/навигацию (выход уже обновил его выше, до save()). */ + g_menu_active = menu_is_open(&g_menu); + + if (changed && (g_render_task_handle != NULL)) + { + (void) xTaskNotifyGive(g_render_task_handle); + } + + vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(MENU_TICK_MS)); + } +} diff --git a/firmware/tft_app/src/app/task_render.c b/firmware/tft_app/src/app/task_render.c new file mode 100644 index 0000000..1feec4d --- /dev/null +++ b/firmware/tft_app/src/app/task_render.c @@ -0,0 +1,86 @@ +/** + * @file task_render.c + * @brief Презентация: единственный владелец дисплея и вызывающий gfx_present(). + * + * Event-driven (xTaskNotifyGive от sul_rx_task И menu_task — MPSC, будят оба + * продюсера, ulTaskNotifyTake(pdTRUE,...) схлопывает несколько notify в одно + * пробуждение — важно только «есть свежее состояние», не сколько раз оно + * менялось). НЕ содержит кнопочной логики — разделено от menu_task на Фазе + * 3.2.4 (см. task_menu.c про причину). + * + * На каждое пробуждение: меню открыто → рисует меню; иначе — если меню ТОЛЬКО + * ЧТО закрылось, сразу восстанавливает последнее известное состояние индикации + * (не дожидаясь свежего сообщения — sul_rx_task мог простаивать под + * g_menu_active), и дренирует g_render_queue, если там свежий diff. + */ + +#include "app_tasks.h" + +#include "FreeRTOS.h" +#include "domain/elevator_model.h" +#include "queue.h" +#include "services/gfx.h" +#include "task.h" +#include "ui/fallback.h" +#include "ui/menu_view.h" + +#include + +#define DISPLAY_WAIT_POLL_MS 5U /* пока g_display_ready не выставлен bringup_task'ом */ + +void render_task(void *p_arg) +{ + (void) p_arg; + + while (!g_display_ready) + { + vTaskDelay(pdMS_TO_TICKS(DISPLAY_WAIT_POLL_MS)); + } + + sul_result_t last = sul_default_state(); + ui_fallback_render_initial(&last); + gfx_present(); + + bool was_menu_open = false; + + for (;;) + { + (void) ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + const bool MENU_OPEN_NOW = menu_is_open(&g_menu); + bool present_needed = false; + + if (MENU_OPEN_NOW) + { + menu_view_render(&g_menu); + present_needed = true; + } + else + { + if (was_menu_open) + { + /* Меню только что закрылось — восстановить индикацию + * последним известным состоянием немедленно, не дожидаясь + * свежего сообщения (sul_rx_task мог простаивать под + * g_menu_active — очередь пока пуста). */ + ui_fallback_render_initial(&last); + present_needed = true; + } + + render_msg_t msg; + if (xQueueReceive(g_render_queue, &msg, 0) == pdTRUE) + { + last = msg.result; + ui_fallback_render(&msg.task, &msg.result); + present_needed = true; + } + } + + if (present_needed) + { + gfx_present(); /* PXP-композит AS+PS → задний FB + свап (tear-free) */ + } + + was_menu_open = MENU_OPEN_NOW; + } +} diff --git a/firmware/tft_app/src/app/task_sul_rx.c b/firmware/tft_app/src/app/task_sul_rx.c new file mode 100644 index 0000000..156d5d8 --- /dev/null +++ b/firmware/tft_app/src/app/task_sul_rx.c @@ -0,0 +1,135 @@ +/** + * @file task_sul_rx.c + * @brief Приём CAN → decode → controller → уведомление render_task. + * + * WDOG/heartbeat/периодический re-log трейлера — БЕЗУСЛОВНО (housekeeping, + * не CAN-специфика). Сама CAN-работа (decode/controller/очередь) — под + * `!g_menu_active`: мягкая пауза на время меню (см. app_tasks.h) — задача НЕ + * suspend'ится, поэтому WDOG остаётся в безопасности по конструкции. + * + * Bring-up (QSPI/settings/self-confirm/SDRAM+gfx+CAN) — bringup_task + * (task_bringup.c); эта задача стартует уже после него, `g_display_ready` + * решает, делать ли CAN-работу вообще. + */ + +#include "app_tasks.h" + +#include "FreeRTOS.h" +#include "bsp/led.h" +#include "bsp/wdog.h" +#include "domain/controller.h" +#include "domain/elevator_model.h" +#include "domain/sul.h" +#include "domain/sul/nku_can.h" +#include "domain/sul/transport/can.h" +#include "queue.h" +#include "services/settings_store.h" +#include "task.h" + +#include + +#define HEARTBEAT_PERIOD_MS 500U +#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */ +#define STATUS_LOG_PERIOD_MS \ + 2000U /* периодический re-log трейлера — виден независимо + * от момента подключения терминала */ +#define CAN_RX_TIMEOUT_MS 100U /* держит цикл отзывчивым к WDOG/heartbeat-каденции */ +#define CONNECTION_TIMEOUT_MS \ + 3000U /* «пропадание трафика» — см. ARCH, поток данных: + * poll + timeout→default. Порядок величины — как + * в OLD_PROJECT (там ~3 с на отметку потери связи). + * Меряется от last_frame_tick — пауза меню не портит + * логику: если трафик реально стоял, «--» появится + * сразу по возврату из меню; если шёл — SINCE_FRAME_MS + * обнулится первым же принятым кадром. */ + +void sul_rx_task(void *p_arg) +{ + (void) p_arg; + + nku_can_ctx_t nku_ctx; + nku_can_init(&nku_ctx); + + controller_ctx_t ctrl_ctx; + controller_init(&ctrl_ctx); + + const TickType_t FEED_PERIOD = pdMS_TO_TICKS(WDOG_FEED_PERIOD_MS); + uint32_t elapsed_ms = 0U; + uint32_t since_status_ms = 0U; + TickType_t last_wake = xTaskGetTickCount(); + TickType_t last_frame_tick = xTaskGetTickCount(); + + for (;;) + { + bsp_wdog_refresh(); + + elapsed_ms += WDOG_FEED_PERIOD_MS; + if (elapsed_ms >= HEARTBEAT_PERIOD_MS) + { + elapsed_ms = 0U; + bsp_led_toggle(LED_APP); + } + + since_status_ms += WDOG_FEED_PERIOD_MS; + if (since_status_ms >= STATUS_LOG_PERIOD_MS) + { + since_status_ms = 0U; + log_slot_status("periodic"); + } + + if (g_display_ready && !g_menu_active) + { + /* Адрес станции из настроек (proto_slice[0]) — подхватывает правку из + * меню без межзадачного сигнала (запись/чтение uint8 атомарны). + * Обе стороны: decode (nku_ctx) И HW RX-фильтры FlexCAN — вторая + * без переприменения фильтров под реальный адрес станции кадры с + * адресом != 0 отбрасывались бы на уровне CAN-контроллера, ещё до + * decode (см. PLAN.md — найдено на реальной станции, адрес 1). */ + const uint8_t NKU_ADDR = settings_store_get()->user.proto_slice[0]; + nku_can_set_address(&nku_ctx, NKU_ADDR); + (void) sul_transport_can_set_address(NKU_ADDR); /* no-op, если адрес не менялся */ + + sul_result_t decoded; + bool have_update = false; + + sul_frame_t frame; + if (sul_transport_can_receive(CAN_RX_TIMEOUT_MS, &frame) == BSP_OK) + { + const sul_driver_t *p_driver = sul_registry_active(); + if (p_driver->decode(&nku_ctx, &frame, &decoded) == SUL_STATUS_OK) + { + last_frame_tick = xTaskGetTickCount(); + have_update = true; + } + /* IGNORED/ERR — Фаза 1 их отдельно не обрабатывает, следующая итерация. */ + } + + const uint32_t SINCE_FRAME_MS = + (uint32_t) (xTaskGetTickCount() - last_frame_tick) * portTICK_PERIOD_MS; + if (SINCE_FRAME_MS >= CONNECTION_TIMEOUT_MS) + { + /* poll + timeout→default (ARCH, поток данных) — controller сам + * определит, реальное ли это изменение (не сработает повторно + * на каждой итерации после первого перехода в default). */ + decoded = sul_default_state(); + have_update = true; + } + + if (have_update) + { + const indication_task_t DIFF = controller_process(&ctrl_ctx, &decoded); + if (DIFF.pos_pending || DIFF.direction_pending || DIFF.mode_pending) + { + const render_msg_t MSG = { .task = DIFF, .result = decoded }; + (void) xQueueOverwrite(g_render_queue, &MSG); + if (g_render_task_handle != NULL) + { + (void) xTaskNotifyGive(g_render_task_handle); + } + } + } + } + + vTaskDelayUntil(&last_wake, FEED_PERIOD); + } +} diff --git a/firmware/tft_app/src/domain/sul/transport/can/include/domain/sul/transport/can.h b/firmware/tft_app/src/domain/sul/transport/can/include/domain/sul/transport/can.h index 96982ae..4a1ad4f 100644 --- a/firmware/tft_app/src/domain/sul/transport/can/include/domain/sul/transport/can.h +++ b/firmware/tft_app/src/domain/sul/transport/can/include/domain/sul/transport/can.h @@ -20,14 +20,28 @@ extern "C" #endif /** - * @brief bsp_can_init() + RX-фильтры под активные ID НКУ-CAN. + * @brief bsp_can_init(). RX-фильтры НЕ настраивает. * - * Фаза 1: фильтры на PACKET1(0x506)/PACKET3(0x508), адрес станции 0 - * (хардкод — совпадает с decode-стороной nku_can.c). Фаза 3 параметризует - * оба конца из настроек одновременно. + * Вызывающий обязан сразу после этого позвать sul_transport_can_set_address() + * — внутренний сентинел форсирует первое применение фильтров независимо от + * переданного адреса (как в OLD_PROJECT msg_receiver_task). */ bsp_status_t sul_transport_can_init(void); +/** + * @brief (Пере)настроить RX-фильтры (PACKET1..5) под адрес станции. + * + * НКУ-CAN кодирует адрес станции в ID: PACKET1..4 — биты [7:4] (group4 = + * addr<<4), PACKET5 — биты [8:6] (group6 = addr<<6, протокол отводит под + * него только 3 бита). Дёшево звать на каждой итерации приёма — реальная + * переконфигурация Message Buffer'ов FlexCAN происходит только при + * фактическом изменении адреса (внутренний diff, сентинел на первый вызов). + * Эталон — OLD_PROJECT msg_receiver_task/apply_nku_can_filters(). + * + * @param nku_address 0..15; вне диапазона — приводится к 15. + */ +bsp_status_t sul_transport_can_set_address(uint8_t nku_address); + /** * @brief Принять один кадр и перевести в sul_frame_t. * diff --git a/firmware/tft_app/src/domain/sul/transport/can/src/can_transport.c b/firmware/tft_app/src/domain/sul/transport/can/src/can_transport.c index e95f35b..0f9acc8 100644 --- a/firmware/tft_app/src/domain/sul/transport/can/src/can_transport.c +++ b/firmware/tft_app/src/domain/sul/transport/can/src/can_transport.c @@ -2,35 +2,82 @@ #include "bsp/can.h" -/* Фаза 1: адрес станции захардкожен в 0 — совпадает с базовыми ID в - * domain/sul/nku_can/src/nku_can.c (0x506|group4, group4=address<<4=0). - * Фаза 3 параметризует оба конца одновременно из настроек. */ -#define PACKET1_ID 0x506U -#define PACKET3_ID 0x508U -#define STD_ID_MASK 0x7FFU /* 11-bit STD — проверять все биты */ +/* Базовые ID НКУ-CAN (адрес станции 0) — та же протокольная раскладка, что и + * decode-сторона (domain/sul/nku_can/src/nku_can.c): PACKET1..4 кодируют + * адрес в битах [7:4] ID (group4 = addr<<4), PACKET5 — в битах [8:6] + * (group6 = addr<<6, протокол отводит под него только 3 бита). Decode и + * transport намеренно НЕ шарят общий заголовок с этими константами — каждый + * владеет своей копией протокольного факта (декодер ничего не знает про + * транспорт и наоборот, ARCH). */ +#define PACKET1_BASE 0x506U +#define PACKET2_BASE 0x408U +#define PACKET3_BASE 0x508U +#define PACKET4_BASE 0x50BU +#define PACKET5_BASE 0x606U +#define STD_ID_MASK 0x7FFU /* 11-bit STD — проверять все биты */ + +#define NKU_ADDRESS_MAX 15U /* 4-битный адрес, group4 = addr<<4 */ + +/* MB index 0..4 — PACKET1..5 соответственно (см. sul_transport_can_set_address). */ /* Хранилище последнего принятого кадра — см. предупреждение в can.h про * время жизни p_out->p_data, возвращаемого sul_transport_can_receive(). */ static bsp_can_frame_t s_last_frame; +/* Сентинел вне диапазона 0..15 — форсирует применение фильтров на первый + * вызов sul_transport_can_set_address(), независимо от переданного адреса + * (порт OLD_PROJECT msg_receiver_task: last_nku_address = 0xFFU). */ +static uint8_t s_last_applied_address = 0xFFU; + bsp_status_t sul_transport_can_init(void) { const bsp_can_config_t cfg = {.bitrate = 125000U}; /* см. OLD_PROJECT msg_receiver_task */ - bsp_status_t st = bsp_can_init(&cfg); + return bsp_can_init(&cfg); + /* Фильтры не настраиваем здесь — вызывающий (sul_rx_task) обязан сразу + * позвать sul_transport_can_set_address(), см. can.h. */ +} + +bsp_status_t sul_transport_can_set_address(uint8_t nku_address) +{ + const uint8_t ADDR = (nku_address <= NKU_ADDRESS_MAX) ? nku_address : NKU_ADDRESS_MAX; + + if (ADDR == s_last_applied_address) + { + return BSP_OK; /* не менялось — переконфигурация MB не нужна */ + } + + const uint32_t GROUP4 = (uint32_t) ADDR << 4U; + const uint32_t GROUP6 = (uint32_t) ADDR << 6U; + + bsp_status_t st = bsp_can_set_filter(0U, PACKET1_BASE | GROUP4, STD_ID_MASK, false); + if (st != BSP_OK) + { + return st; + } + st = bsp_can_set_filter(1U, PACKET2_BASE | GROUP4, STD_ID_MASK, false); + if (st != BSP_OK) + { + return st; + } + st = bsp_can_set_filter(2U, PACKET3_BASE | GROUP4, STD_ID_MASK, false); + if (st != BSP_OK) + { + return st; + } + st = bsp_can_set_filter(3U, PACKET4_BASE | GROUP4, STD_ID_MASK, false); + if (st != BSP_OK) + { + return st; + } + st = bsp_can_set_filter(4U, PACKET5_BASE | GROUP6, STD_ID_MASK, false); if (st != BSP_OK) { return st; } - st = bsp_can_set_filter(0U, PACKET1_ID, STD_ID_MASK, false); - if (st != BSP_OK) - { - return st; - } - - /* Остальные MB (2..15) свободны под Фазу 2 (PACKET2/4/5, remote-address). */ - return bsp_can_set_filter(1U, PACKET3_ID, STD_ID_MASK, false); + s_last_applied_address = ADDR; + return BSP_OK; } bsp_status_t sul_transport_can_receive(uint32_t timeout_ms, sul_frame_t *p_out) diff --git a/firmware/tft_app/src/menu/include/menu/menu.h b/firmware/tft_app/src/menu/include/menu/menu.h index 6ac1eb3..e4666f6 100644 --- a/firmware/tft_app/src/menu/include/menu/menu.h +++ b/firmware/tft_app/src/menu/include/menu/menu.h @@ -14,7 +14,7 @@ * * Навигация: BUTTON_1 → menu_next() (следующий пункт уровня, с заворотом); * короткое BUTTON_2 → menu_action() (вход в подменю / инкремент значения / - * выход-с-сохранением). Вход в меню (долгое BUTTON_2) и модальность — уровень app. + * выход-с-сохранением). Вход в меню (короткое BUTTON_1, вне меню) и модальность — уровень app. * * Соглашение: items[MENU_ROOT_INDEX] — корневое SUBMENU; его дети — верхний * уровень (их parent == MENU_ROOT_INDEX). Дети одного уровня — непрерывный diff --git a/firmware/tft_app/src/services/gfx/CMakeLists.txt b/firmware/tft_app/src/services/gfx/CMakeLists.txt index cd4f323..33d41a1 100644 --- a/firmware/tft_app/src/services/gfx/CMakeLists.txt +++ b/firmware/tft_app/src/services/gfx/CMakeLists.txt @@ -23,4 +23,9 @@ set_source_files_properties( PROPERTIES COMPILE_OPTIONS "-include;${CMAKE_CURRENT_SOURCE_DIR}/fonts/include/fonts.h;-w") -target_link_libraries(tft_app_gfx PUBLIC bsp_display bsp_sdram sdk_common) # fsl_common.h — AT_NONCACHEABLE_SECTION_ALIGN +# sdk_common — fsl_common.h (AT_NONCACHEABLE_SECTION_ALIGN); sdk_pxp — PXP-компоновщик; +# freertos_kernel — семафор FRAME_DONE в gfx_present/ISR-колбэке (Фаза 3.2.4); +# port_log_uart — ВРЕМЕННО, тайминг-диагностика в gfx_present() (см. её тело) — +# убрать вместе с диагностикой, когда причина лага индикации найдена. +target_link_libraries(tft_app_gfx PUBLIC bsp_display bsp_sdram sdk_common PRIVATE sdk_pxp + freertos_kernel port_log_uart) diff --git a/firmware/tft_app/src/services/gfx/include/services/gfx.h b/firmware/tft_app/src/services/gfx/include/services/gfx.h index 7ce2fff..4cc775e 100644 --- a/firmware/tft_app/src/services/gfx/include/services/gfx.h +++ b/firmware/tft_app/src/services/gfx/include/services/gfx.h @@ -1,9 +1,15 @@ /** * @file gfx.h - * @brief Фаза 1 — минимальный gfx: framebuffer (SDRAM, non-cacheable) + - * RLE-шрифты (lcd-image-converter) + примитив стрелки. Без PXP/ - * компоновщика/альфа-слоёв — прямая запись в единственный framebuffer, - * который ELCDIF сканирует по DMA (см. gfx.c про non-cacheable SDRAM). + * @brief Фаза 3.2.4 — gfx как компоновщик: double-buffer + PXP. + * + * Модель (эталон OLD_PROJECT_TFT8_UKL/source/display/): CPU рисует ВЕСЬ кадр в + * альфа-поверхность **AS** (`alpha_buffer`, ARGB8888; примитивы пишут alpha + * 0xFF, `gfx_clear` обнуляет → прозрачно). PXP блендит AS над фоновой + * поверхностью **PS** (`processing_buffer`, сейчас сплошной чёрный) в один из + * двух задних framebuffer'ов, затем свап синхронно с ELCDIF (семафор FRAME_DONE). + * Рисуем off-screen, показываем атомарным свапом → tear-free. Стиль-картинка в + * PS и спрайты в AS — Фаза 4/5; сейчас PS чёрный, композиция = чёрный фон + + * нарисованное в AS. * * Формат tImage/tChar/tFont и RLE-декодирование — порт проверенного в проде * алгоритма из OLD_PROJECT (source/fonts/fonts.c), тот же формат данных, что @@ -73,18 +79,32 @@ typedef uint32_t gfx_color_t; #define GFX_COLOR_WHITE 0x00FFFFFFU /** - * @brief framebuffer (SDRAM non-cacheable) + bsp_display. + * @brief Поднять компоновщик: AS/PS/2×FB (SDRAM non-cacheable) + PXP + ELCDIF. + * + * Создаёт семафор FRAME_DONE и регистрирует ISR-колбэк ELCDIF (даёт семафор), + * заливает PS сплошным чёрным, инициализирует PXP (AS над PS → выходной FB) и + * стартует ELCDIF на FB[0]. До первого gfx_present() экран чёрный. * * SDRAM (SEMC) должна быть уже поднята вызывающим (bsp_sdram_configure() + - * bsp_sdram_init()) — gfx не владеет SEMC-инициализацией, только framebuffer + * bsp_sdram_init()) — gfx не владеет SEMC-инициализацией, только буферами * внутри уже готовой SDRAM. * * @param type тип панели (Фаза 1 — хардкод из app; Фаза 9 — provisioning) */ bsp_status_t gfx_init(bsp_display_type_t type); -/** Залить весь кадр цветом (обычно GFX_COLOR_BLACK перед перерисовкой). */ -void gfx_clear(gfx_color_t color); +/** Обнулить AS (весь кадр становится прозрачным). Вызывать перед отрисовкой + * нового полного кадра; непрорисованные области покажут фон PS (чёрный). */ +void gfx_clear(void); + +/** + * @brief Показать нарисованный в AS кадр: PXP-композит AS над PS → задний FB, + * затем свап синхронно с ELCDIF (tear-free). + * + * Блокирующий (busy-wait завершения PXP + ожидание FRAME_DONE) — звать из + * задачи-владельца дисплея после того, как полный кадр нарисован в AS. + */ +void gfx_present(void); /** * @brief Нарисовать строку заданным цветом (тинтинг с альфа-сглаживанием). diff --git a/firmware/tft_app/src/services/gfx/src/gfx.c b/firmware/tft_app/src/services/gfx/src/gfx.c index 1062894..d186cd3 100644 --- a/firmware/tft_app/src/services/gfx/src/gfx.c +++ b/firmware/tft_app/src/services/gfx/src/gfx.c @@ -1,47 +1,69 @@ #include "services/gfx.h" +#include "FreeRTOS.h" #include "fsl_common.h" /* AT_NONCACHEABLE_SECTION_ALIGN */ +#include "fsl_pxp.h" +#include "log/log.h" /* ВРЕМЕННО (Фаза 3.2.4 HW-расследование лага) — см. gfx_present() */ +#include "semphr.h" +#include "task.h" /* xTaskGetTickCount — тайминг-инструментация ниже */ #include +#include -/* ── Framebuffer (SDRAM, non-cacheable) ──────────────────────────────────── +#define LOG_TAG "gfx" + +/* ── Поверхности компоновщика (SDRAM, non-cacheable) ──────────────────────── * * AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции * NonCacheable/.ncache → cmake/linker/..._app_slot.ld отображает её на - * m_sdram_ncache (2 МБ в начале SDRAM), а board_mpu_init() (bsp/generated/ + * m_sdram_ncache (8 МБ в начале SDRAM), а board_mpu_init() (bsp/generated/ * board.c, Region 9) конфигурирует ЭТОТ ЖЕ диапазон как non-cacheable через * линкер-символы __NCACHE_REGION_START/SIZE — рекомендация NXP для буферов, - * которые ELCDIF читает по DMA (см. OLD_PROJECT source/display/image_cache.c, + * которые ELCDIF/PXP читают по DMA (см. OLD_PROJECT source/display/image_cache.c, * тот же макрос, framebuffer/alpha_buffer/processing_buffer). Без этого CPU - * писал бы через Write-Back D-Cache (Region 8: SDRAM WB Cacheable), и ELCDIF + * писал бы через Write-Back D-Cache (Region 8: SDRAM WB Cacheable), и DMA * читал бы устаревшие данные, пока кэш-линия не вытеснится сама. * - * Размер — под ТЕКУЩУЮ панель стенда (TFT8, 800×600, известна на Фазе 1), не - * под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9) станет рантайм-выбирать - * между TFT7/8/10 в одном бинарнике, размер и m_sdram_ncache (сейчас 2 МБ) - * придётся поднять до максимума (1024×600×4 ≈ 2.34 МБ → 4 МБ регион). */ -#define FRAMEBUFFER_ALIGN 64U /* см. OLD_PROJECT FRAME_BUFFER_ALIGN — типичное ELCDIF/AXI выравнивание */ -#define FRAMEBUFFER_PIXELS (800U * 600U) + * Четыре поверхности (эталон TFT8_UKL): AS (рисует CPU), PS (фон, чёрный), + * FB[2] (выход PXP = вход ELCDIF, double buffer). Размер — под ТЕКУЩУЮ панель + * стенда (TFT8, 800×600), не под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9) + * станет рантайм-выбирать между TFT7/8/10 в одном бинарнике, размер поверхностей + * и m_sdram_ncache придётся поднять до максимума панели. */ +#define FRAMEBUFFER_ALIGN 64U /* см. OLD_PROJECT FRAME_BUFFER_ALIGN — типичное ELCDIF/PXP/AXI выравнивание */ +#define SURFACE_PIXELS (800U * 600U) +#define BYTES_PER_PIXEL 4U +#define ALPHA_OPAQUE 0xFF000000U /* AS: alpha=0xFF → пиксель непрозрачен для PXP-блендинга */ #define FALLBACK_CHAR '-' -AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_framebuffer[FRAMEBUFFER_PIXELS], FRAMEBUFFER_ALIGN); +/* AS — CPU рисует сюда (ARGB8888, alpha значим). */ +AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_alpha_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN); +/* PS — фон под AS (сейчас сплошной чёрный, заливается однократно в gfx_init). */ +AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_processing_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN); +/* Выходные буферы PXP = сканируемые ELCDIF (double buffer, свап в gfx_present). */ +AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_framebuffer[2][SURFACE_PIXELS], FRAMEBUFFER_ALIGN); static uint16_t s_fb_width; static uint16_t s_fb_height; +static uint8_t s_back_index; /* индекс FB, в который PXP компонует следующий кадр */ +static SemaphoreHandle_t s_frame_done; /* даётся из ELCDIF ISR по завершении кадра */ +static pxp_output_buffer_config_t s_output_cfg; /* хранится: gfx_present меняет buffer0Addr */ + static inline void set_pixel(uint16_t x, uint16_t y, gfx_color_t color) { if ((x >= s_fb_width) || (y >= s_fb_height)) { return; /* примитивы могут частично выходить за экран — не UB, просто обрезка */ } - s_framebuffer[(uint32_t) y * s_fb_width + x] = color; + /* AS: пишем непрозрачно (alpha 0xFF), RGB из color (X-байт игнорируем). */ + s_alpha_buffer[(uint32_t) y * s_fb_width + x] = (color & 0x00FFFFFFU) | ALPHA_OPAQUE; } -/* Альфа-блендинг: наложить @p color с покрытием @p a (0..255) на фон - * framebuffer'а. a=0 — фон нетронут (прозрачный край глифа), a=255 — полная - * замена. Так сглаживание глифа корректно ложится на любой фон (в т.ч. - * полосу-курсор), а не штампует чёрный бокс. */ +/* Альфа-блендинг: наложить @p color с покрытием @p a (0..255) на текущее + * содержимое AS. a=0 — пиксель нетронут (прозрачный край глифа), a=255 — + * полная замена. Так сглаживание глифа корректно ложится на уже нарисованный + * фон (в т.ч. полосу-курсор), а не штампует чёрный бокс. Результат всегда + * непрозрачен (alpha 0xFF): PXP покажет его поверх PS. */ static inline void blend_pixel(uint16_t x, uint16_t y, gfx_color_t color, uint8_t a) { if ((x >= s_fb_width) || (y >= s_fb_height) || (a == 0U)) @@ -52,16 +74,16 @@ static inline void blend_pixel(uint16_t x, uint16_t y, gfx_color_t color, uint8_ const uint32_t idx = (uint32_t) y * s_fb_width + x; if (a == 0xFFU) { - s_framebuffer[idx] = color; + s_alpha_buffer[idx] = (color & 0x00FFFFFFU) | ALPHA_OPAQUE; return; } - const uint32_t bg = s_framebuffer[idx]; + const uint32_t bg = s_alpha_buffer[idx]; const uint32_t inv = 255U - a; const uint32_t r = (((color >> 16) & 0xFFU) * a + ((bg >> 16) & 0xFFU) * inv) / 255U; const uint32_t g = (((color >> 8) & 0xFFU) * a + ((bg >> 8) & 0xFFU) * inv) / 255U; const uint32_t b = (((color) & 0xFFU) * a + ((bg) & 0xFFU) * inv) / 255U; - s_framebuffer[idx] = (r << 16) | (g << 8) | b; + s_alpha_buffer[idx] = (r << 16) | (g << 8) | b | ALPHA_OPAQUE; } /* ── Поиск глифа (бинарный — chars[] отсортирован по code, гарантия формата @@ -302,13 +324,101 @@ void gfx_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, gfx_color_t c } } -/* ── Framebuffer / init ──────────────────────────────────────────────────── */ +/* ── PXP-компоновщик (порт OLD_PROJECT_TFT8_UKL/source/display/pxp_config.c) ── */ + +/* PS-формат: для RT1052 (расширенная таблица форматов, FSL_FEATURE_PXP_HAS_NO_ + * EXTEND_PIXEL_FORMAT не определён) 32-битный формат без реального альфа-канала + * называется kPXP_PsPixelFormatARGB8888 (0x4) — фон, альфа PS в блендинге не + * участвует (значима альфа AS, kPXP_AlphaEmbedded). */ +static void gfx_pxp_init(void) +{ + PXP_Init(PXP); + + const uint16_t PITCH = (uint16_t) (s_fb_width * BYTES_PER_PIXEL); + + const pxp_ps_buffer_config_t ps_cfg = { + .pixelFormat = kPXP_PsPixelFormatARGB8888, + .swapByte = false, + .bufferAddr = (uint32_t) s_processing_buffer, + .bufferAddrU = 0U, + .bufferAddrV = 0U, + .pitchBytes = PITCH, + }; + PXP_SetProcessSurfaceBufferConfig(PXP, &ps_cfg); + + const pxp_as_buffer_config_t as_cfg = { + .pixelFormat = kPXP_AsPixelFormatARGB8888, + .bufferAddr = (uint32_t) s_alpha_buffer, + .pitchBytes = PITCH, + }; + PXP_SetAlphaSurfaceBufferConfig(PXP, &as_cfg); + + /* Embedded alpha: доля AS-пикселя над PS берётся из его альфа-байта. */ + const pxp_as_blend_config_t blend_cfg = { + .alpha = 0xFFU, + .invertAlpha = false, + .alphaMode = kPXP_AlphaEmbedded, + .ropMode = kPXP_RopMaskAs, + }; + PXP_SetAlphaSurfaceBlendConfig(PXP, &blend_cfg); + + s_output_cfg.pixelFormat = kPXP_OutputPixelFormatARGB8888; + s_output_cfg.interlacedMode = kPXP_OutputProgressive; + s_output_cfg.buffer0Addr = (uint32_t) s_framebuffer[0]; + s_output_cfg.buffer1Addr = 0U; + s_output_cfg.pitchBytes = PITCH; + s_output_cfg.width = s_fb_width; + s_output_cfg.height = s_fb_height; + PXP_SetOutputBufferConfig(PXP, &s_output_cfg); + + PXP_EnableCsc1(PXP, false); /* включён по умолчанию — фон RGB, конверсия не нужна */ + + PXP_SetProcessSurfacePosition(PXP, 0U, 0U, s_fb_width, s_fb_height); + PXP_SetAlphaSurfacePosition(PXP, 0U, 0U, s_fb_width, s_fb_height); +} + +/* Запустить PXP и дождаться завершения композиции (busy-wait, как в эталоне). */ +static void gfx_pxp_run(void) +{ + PXP_ClearStatusFlags(PXP, kPXP_CommandLoadFlag); + PXP_ClearStatusFlags(PXP, kPXP_Axi0ReadErrorFlag); + PXP_ClearStatusFlags(PXP, kPXP_Axi0WriteErrorFlag); + PXP_ClearStatusFlags(PXP, kPXP_CompleteFlag); + + PXP_Start(PXP); + while ((kPXP_CompleteFlag & PXP_GetStatusFlags(PXP)) == 0U) + { + } +} + +/* ISR-safe: конец кадра ELCDIF → отпустить семафор (синхронизация свапа). */ +static void on_frame_done(void) +{ + BaseType_t hp_task_woken = pdFALSE; + (void) xSemaphoreGiveFromISR(s_frame_done, &hp_task_woken); + portYIELD_FROM_ISR(hp_task_woken); +} + +/* ── Компоновщик / init ──────────────────────────────────────────────────── */ bsp_status_t gfx_init(bsp_display_type_t type) { /* SDRAM (SEMC) — забота вызывающего (bsp_sdram_configure()+init()), gfx - * владеет только framebuffer'ом внутри уже готовой SDRAM. */ - const bsp_status_t st = bsp_display_init(type, (uint32_t) s_framebuffer, NULL); + * владеет только поверхностями внутри уже готовой SDRAM. */ + s_frame_done = xSemaphoreCreateBinary(); + if (s_frame_done == NULL) + { + return BSP_ERR_INIT; + } + + /* PS — чёрный фон; AS — прозрачно; оба выходных FB — чёрные (ELCDIF стартует + * на FB[0] ещё до первого gfx_present, иначе на экране был бы мусор). */ + (void) memset(s_processing_buffer, 0, sizeof(s_processing_buffer)); + (void) memset(s_alpha_buffer, 0, sizeof(s_alpha_buffer)); + (void) memset(s_framebuffer, 0, sizeof(s_framebuffer)); + + /* Семафор создан и колбэк готов ДО включения IRQ внутри bsp_display_init. */ + const bsp_status_t st = bsp_display_init(type, (uint32_t) s_framebuffer[0], on_frame_done); if (st != BSP_OK) { return st; @@ -317,16 +427,47 @@ bsp_status_t gfx_init(bsp_display_type_t type) const bsp_display_size_t *p_size = bsp_display_get_size(); s_fb_width = p_size->width; s_fb_height = p_size->height; + s_back_index = 0U; /* FB[0] показывается; первый present уйдёт в FB[1] */ + + gfx_pxp_init(); return BSP_OK; } -void gfx_clear(gfx_color_t color) +void gfx_clear(void) { - const uint32_t count = (uint32_t) s_fb_width * s_fb_height; - - for (uint32_t i = 0U; i < count; i++) - { - s_framebuffer[i] = color; - } + /* Прозрачно (alpha 0) → непрорисованные области покажут фон PS (чёрный). */ + (void) memset(s_alpha_buffer, 0, (size_t) s_fb_width * s_fb_height * BYTES_PER_PIXEL); +} + +void gfx_present(void) +{ + /* Компонуем в НЕ показываемый сейчас буфер, показываем атомарным свапом. */ + s_back_index ^= 1U; + + s_output_cfg.buffer0Addr = (uint32_t) s_framebuffer[s_back_index]; + PXP_SetOutputBufferConfig(PXP, &s_output_cfg); + + /* ВРЕМЕННО (Фаза 3.2.4, HW-расследование лага индикации ~1-2 c — + * PLAN.md): раздельный замер PXP busy-wait и ожидания FRAME_DONE. ELCDIF + * по clock_config.c должен давать ~65 Гц (528 МГц PLL2 / 12 / кадр) → + * ожидаем pxp единицы мс, vsync до ~15 мс. Если на железе один из них + * систематически большой — вот прямой ответ, что именно тормозит. Убрать + * после диагностики (см. include log/log.h и task.h выше — тоже под снос + * вместе с этим). */ + const TickType_t T0 = xTaskGetTickCount(); + + gfx_pxp_run(); /* AS над PS → s_framebuffer[s_back_index] */ + + const TickType_t T1 = xTaskGetTickCount(); + + /* Синхронизация с развёрткой: дождаться конца кадра, затем отдать ELCDIF + * новый буфер — он переключится аппаратно на границе кадра (tear-free). */ + (void) xSemaphoreTake(s_frame_done, portMAX_DELAY); + + const TickType_t T2 = xTaskGetTickCount(); + + bsp_display_set_next_buffer((uint32_t) s_framebuffer[s_back_index]); + + LOG_I(LOG_TAG, "present: pxp=%u ms vsync=%u ms", (unsigned) (T1 - T0), (unsigned) (T2 - T1)); } diff --git a/firmware/tft_app/src/services/settings_store/src/settings_store.c b/firmware/tft_app/src/services/settings_store/src/settings_store.c index 4369052..b7c085f 100644 --- a/firmware/tft_app/src/services/settings_store/src/settings_store.c +++ b/firmware/tft_app/src/services/settings_store/src/settings_store.c @@ -14,15 +14,17 @@ void settings_store_init_defaults(void) bsp_status_t settings_store_load(void) { - settings_page_t page; + /* static: страница = сектор (4 КБ) — на стеке задачи (~3 КБ) переполнит его. + * Однократный вызов при старте, не реентерабельно (как s_page в save()). */ + static settings_page_t s_page; - if (bsp_qspi_read(TFT_APP_QSPI_SETTINGS_OFFSET, (uint8_t *) &page, sizeof(page)) != BSP_OK) + if (bsp_qspi_read(TFT_APP_QSPI_SETTINGS_OFFSET, (uint8_t *) &s_page, sizeof(s_page)) != BSP_OK) { g_settings = settings_defaults(); return BSP_ERR_HW; } - if (!settings_deserialize(&page, &g_settings)) + if (!settings_deserialize(&s_page, &g_settings)) { g_settings = settings_defaults(); return BSP_ERR_INVALID; diff --git a/firmware/tft_app/src/ui/fallback/src/fallback.c b/firmware/tft_app/src/ui/fallback/src/fallback.c index bd552f5..8af0ac6 100644 --- a/firmware/tft_app/src/ui/fallback/src/fallback.c +++ b/firmware/tft_app/src/ui/fallback/src/fallback.c @@ -60,7 +60,7 @@ static void render_normal(const sul_result_t *p_result) static void render(sul_mode_t mode, const sul_result_t *p_result) { - gfx_clear(GFX_COLOR_BLACK); + gfx_clear(); const char *label = mode_label(mode); if (label == NULL) diff --git a/firmware/tft_app/src/ui/menu/include/ui/menu_view.h b/firmware/tft_app/src/ui/menu/include/ui/menu_view.h index 3f3b497..2479689 100644 --- a/firmware/tft_app/src/ui/menu/include/ui/menu_view.h +++ b/firmware/tft_app/src/ui/menu/include/ui/menu_view.h @@ -6,6 +6,10 @@ * заголовок уровня, список пунктов с полосой-курсором, значения, футер-подсказка. * Читает только модель (menu_ctx_t) через её запросы; логики навигации не содержит. * HIL — проверяется на железе. + * + * Рисует ПОЛНЫЙ кадр в AS (double-buffer + PXP, Фаза 3.2.4) — инкрементальной + * построчной отрисовки больше нет: off-screen перерисовка полного кадра + + * атомарный свап дёшевы и tear-free. Показ — gfx_present() у владельца дисплея. */ #ifndef UI_MENU_VIEW_H_ @@ -18,7 +22,11 @@ extern "C" { #endif -/** Отрисовать текущий кадр меню по состоянию модели. Меню должно быть открыто. */ +/** + * @brief Отрисовать полный кадр меню в AS (очистка + заголовок + строки + футер). + * + * Рисует off-screen (AS); на экран попадёт после gfx_present() владельца дисплея. + */ void menu_view_render(const menu_ctx_t *p_ctx); #ifdef __cplusplus diff --git a/firmware/tft_app/src/ui/menu/src/menu_view.c b/firmware/tft_app/src/ui/menu/src/menu_view.c index 8fcf057..af69e09 100644 --- a/firmware/tft_app/src/ui/menu/src/menu_view.c +++ b/firmware/tft_app/src/ui/menu/src/menu_view.c @@ -7,17 +7,23 @@ /* Окно 480×272 @ (0,0). Метрики под реальные шрифты: JBMono24 h=31 (заголовок/ * строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */ #define WIN_W 480U +#define WIN_H 272U #define TITLE_H 36U #define ROW_H 36U #define FOOTER_H 20U #define ROWS_Y0 TITLE_H -#define FOOTER_Y (272U - FOOTER_H) /* 252 */ -#define TEXT_DY24 2U /* (36-31)/2 — центрирование JBMono24 в полосе 36 */ -#define TEXT_DY12 2U /* (20-16)/2 — JBMono12 в футере 20 */ +#define FOOTER_Y (WIN_H - FOOTER_H) /* 252 */ +#define TEXT_DY24 2U /* центрирование JBMono24 (h31) в полосе 36 */ +#define TEXT_DY12 2U /* JBMono12 (h16) в футере 20 */ #define LEFT_MARGIN 16U #define RIGHT_MARGIN 16U #define EDGE_MARGIN 8U +/* Рамка 1 px по периметру — строки/футер вписаны внутрь неё (не затирают). */ +#define BORDER 1U +#define CONTENT_X BORDER +#define CONTENT_W (WIN_W - 2U * BORDER) + /* Палитра (XRGB). Собирается тинтингом из белых шрифтов. */ #define COL_BG 0x000000U #define COL_TITLE 0x00CFE0FFU @@ -26,7 +32,7 @@ #define COL_SEL_BG 0x0017406BU #define COL_SEL_TEXT 0x00FFFFFFU #define COL_FOOTER 0x006F6F6FU -#define COL_SEP 0x00333333U +#define COL_SEP 0x00333333U /* разделители + рамка */ /* Строка значения пункта. @return false — у пункта нет значения (BACK). */ static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size_t buf_len) @@ -39,7 +45,7 @@ static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size (void) snprintf(p_buf, buf_len, ">"); return true; case MENU_BYTE: - (void) snprintf(p_buf, buf_len, "%u", menu_read_value(p_ctx, idx)); + (void) snprintf(p_buf, buf_len, "%u", (unsigned) menu_read_value(p_ctx, idx)); return true; case MENU_SELECT: case MENU_BOOL: @@ -51,7 +57,7 @@ static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size } else { - (void) snprintf(p_buf, buf_len, "%u", V); + (void) snprintf(p_buf, buf_len, "%u", (unsigned) V); } return true; } @@ -61,14 +67,14 @@ static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size } } +/* Отрисовать одну строку по её экранному y (fill фона + подпись + значение). + * Заливает CONTENT_X..CONTENT_W — рамку (x=0, x=WIN_W-1) не трогает. */ static void draw_row(const menu_ctx_t *p_ctx, uint8_t idx, uint16_t row_y, bool selected) { - if (selected) - { - gfx_fill_rect(0U, row_y, WIN_W, ROW_H, COL_SEL_BG); - } - + const gfx_color_t BG_COL = selected ? COL_SEL_BG : COL_BG; const gfx_color_t TEXT_COL = selected ? COL_SEL_TEXT : COL_LABEL; + gfx_fill_rect(CONTENT_X, row_y, CONTENT_W, ROW_H, BG_COL); + (void) gfx_draw_string(&SystemFont, p_ctx->items[idx].label, LEFT_MARGIN, (uint16_t) (row_y + TEXT_DY24), TEXT_COL); @@ -84,9 +90,9 @@ static void draw_row(const menu_ctx_t *p_ctx, uint8_t idx, uint16_t row_y, bool static void draw_footer(const menu_ctx_t *p_ctx, uint8_t level_first, uint8_t level_last) { - gfx_fill_rect(0U, (uint16_t) (FOOTER_Y - 1U), WIN_W, 1U, COL_SEP); + gfx_fill_rect(CONTENT_X, (uint16_t) (FOOTER_Y - 1U), CONTENT_W, 1U, COL_SEP); - (void) gfx_draw_string(&SystemFontSmall, "Кн.1 - далее Кн.2 - выбор", EDGE_MARGIN, + (void) gfx_draw_string(&SystemFontSmall, "Кн.1 - далее Кн.2 - выбор", LEFT_MARGIN, (uint16_t) (FOOTER_Y + TEXT_DY12), COL_FOOTER); const uint8_t TOTAL = (uint8_t) (level_last - level_first + 1U); @@ -100,8 +106,10 @@ static void draw_footer(const menu_ctx_t *p_ctx, uint8_t level_first, uint8_t le void menu_view_render(const menu_ctx_t *p_ctx) { - /* Меню модально и переносимо: чёрный весь экран, окно — верхний-левый 480×272. */ - gfx_clear(COL_BG); + /* Полный кадр off-screen: обнулить AS (вне окна 480×272 → чёрный фон PS), + * затем нарисовать окно. Свап — gfx_present() у владельца дисплея. */ + gfx_clear(); + if (!p_ctx->open) { return; @@ -111,9 +119,8 @@ void menu_view_render(const menu_ctx_t *p_ctx) const char *p_title = p_ctx->items[p_ctx->items[p_ctx->cur].parent].label; const uint16_t TW = gfx_string_width(&SystemFont, p_title); (void) gfx_draw_string(&SystemFont, p_title, (uint16_t) ((WIN_W - TW) / 2U), TEXT_DY24, COL_TITLE); - gfx_fill_rect(0U, (uint16_t) (TITLE_H - 1U), WIN_W, 1U, COL_SEP); + gfx_fill_rect(CONTENT_X, (uint16_t) (TITLE_H - 1U), CONTENT_W, 1U, COL_SEP); - /* Список пунктов текущего уровня, окно страницы. */ uint8_t first; uint8_t last; menu_level_range(p_ctx, &first, &last); @@ -126,9 +133,11 @@ void menu_view_render(const menu_ctx_t *p_ctx) { break; } - const uint16_t ROW_Y = (uint16_t) (ROWS_Y0 + slot * ROW_H); - draw_row(p_ctx, IDX, ROW_Y, (IDX == p_ctx->cur)); + draw_row(p_ctx, IDX, (uint16_t) (ROWS_Y0 + slot * ROW_H), (IDX == p_ctx->cur)); } draw_footer(p_ctx, first, last); + + /* Рамка — последней, поверх содержимого. */ + gfx_draw_rect(0U, 0U, WIN_W, WIN_H, COL_SEP); } diff --git a/port/log/src/log_mutex.c b/port/log/src/log_mutex.c index cccdbc5..cd3637b 100644 --- a/port/log/src/log_mutex.c +++ b/port/log/src/log_mutex.c @@ -15,26 +15,24 @@ * @note Вызов LOG_* из ISR запрещён: xSemaphoreTake с portMAX_DELAY * недопустим в ISR-контексте. */ -#if 0 #include "FreeRTOS.h" #include "log/log.h" #include "semphr.h" - + static SemaphoreHandle_t s_log_mutex; - + void log_mutex_init(void) { s_log_mutex = xSemaphoreCreateMutex(); configASSERT(s_log_mutex != NULL); } - + void log_mutex_lock(void) { xSemaphoreTake(s_log_mutex, portMAX_DELAY); } - + void log_mutex_unlock(void) { xSemaphoreGive(s_log_mutex); -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt index 9661186..f8d7bd1 100644 --- a/sdk/CMakeLists.txt +++ b/sdk/CMakeLists.txt @@ -56,6 +56,7 @@ add_sdk_driver(lpi2c fsl_lpi2c.c) add_sdk_driver(pwm fsl_pwm.c) add_sdk_driver(adc fsl_adc.c) add_sdk_driver(elcdif fsl_elcdif.c) +add_sdk_driver(pxp fsl_pxp.c) add_sdk_driver(cache fsl_cache.c) add_sdk_driver(sai fsl_sai.c) add_sdk_driver(sai_edma fsl_sai_edma.c) @@ -73,6 +74,7 @@ target_link_libraries(sdk_lpi2c PUBLIC sdk_clock) target_link_libraries(sdk_pwm PUBLIC sdk_clock) target_link_libraries(sdk_adc PUBLIC sdk_clock) target_link_libraries(sdk_elcdif PUBLIC sdk_clock) +target_link_libraries(sdk_pxp PUBLIC sdk_clock) # PXP_Init → CLOCK_EnableClock(kCLOCK_Pxp) target_link_libraries(sdk_sai PUBLIC sdk_clock) target_link_libraries(sdk_sai_edma PUBLIC sdk_sai sdk_edma) target_link_libraries(sdk_edma PUBLIC sdk_common)