# tft_app: Phase 3.2 - menu & indicator

This commit is contained in:
Dmitry Akimov 2026-07-22 14:49:22 +03:00
parent b9b46154cc
commit 2662111e1b
22 changed files with 1355 additions and 431 deletions

View file

@ -46,21 +46,24 @@ MEMORY
m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128 КБ — ramfunc */ 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_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128 КБ — .data/.bss/heap/stack */
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256 КБ */ m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256 КБ */
/* SDRAM 32 МБ, разбита на 2: некэшируемый framebuffer (ELCDIF DMA читает /* SDRAM 32 МБ, разбита на 2: некэшируемые DMA-буферы компоновщика (ELCDIF
* его напрямую — MPU Region 9 в board_mpu_init() параметризован ЭТИМИ * читает framebuffer, PXP — AS/PS напрямую; MPU Region 9 в board_mpu_init()
* __NCACHE_REGION_* символами, C-код не трогаем) + остаток под кэшируемые * параметризован ЭТИМИ __NCACHE_REGION_* символами, C-код не трогаем) +
* буферы будущих фаз (ассеты, аудио). Framebuffer TFT8 800×600×4 ≈ 1.83 МБ * остаток под кэшируемые буферы будущих фаз (ассеты, аудио).
* — 2 МБ (степень двойки, MPU-регион обязан быть степенью двойки и *
* выровнен по своему размеру) с запасом. См. bsp/sdram/README.md про NXP- * Фаза 3.2.4 (double-buffer + PXP): 4 буфера TFT8 800×600×4 ≈ 1.92 МБ каждый
* рекомендацию некэшируемых DMA-буферов в SDRAM. */ * — AS (alpha_buffer) + PS (processing_buffer) + framebuffer[2] = 7.68 МБ
m_sdram_ncache (RW) : ORIGIN = 0x80000000, LENGTH = 0x00200000 /* 2 МБ — framebuffer (non-cacheable) */ * → 8 МБ (степень двойки; MPU-регион обязан быть степенью двойки и выровнен
m_sdram (RW) : ORIGIN = 0x80200000, LENGTH = 0x01E00000 /* ~30 МБ — кэшируемый остаток, Фаза 1 не использует */ * по своему размеру — 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 SECTIONS
{ {
__NCACHE_REGION_START = ORIGIN(m_sdram_ncache); __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). */ /* Vector table (imgtool header зарезервирован до неё в MEMORY). */
.interrupts : .interrupts :
@ -208,7 +211,7 @@ SECTIONS
ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data") ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")
ASSERT(text_end <= SLOT_BASE + SLOT_SIZE, "app вышел за пределы слота (2 МБ)") ASSERT(text_end <= SLOT_BASE + SLOT_SIZE, "app вышел за пределы слота (2 МБ)")
ASSERT((__noncachedata_end__ - ORIGIN(m_sdram_ncache)) <= LENGTH(m_sdram_ncache), 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 : .bss :
{ {

View file

@ -1,8 +1,9 @@
# tft-app — движок меню и связь с настройками # tft-app — движок меню и связь с настройками
Документ описывает **реализованный** движок меню (Фаза 3.2.1: чистая модель) и его Документ описывает **реализованный** движок меню (Фаза 3.2.1: чистая модель; 3.2.23.2.4: рендер,
связь с модулем настроек `settings_store`. Проектная основа — [ARCH.md §8](../../firmware/tft_app/ARCH.md); wiring, разделение на задачи) и его связь с модулем настроек `settings_store`. Проектная основа —
реализация — [menu.c](../../firmware/tft_app/src/menu/src/menu.c), [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/). [settings_store](../../firmware/tft_app/src/services/settings_store/).
Движущее требование (§8): клиент приносит уникальные настройки, и добавление их **не должно Движущее требование (§8): клиент приносит уникальные настройки, и добавление их **не должно
@ -17,7 +18,7 @@
```mermaid ```mermaid
flowchart TB flowchart TB
BTN["bsp_button<br/>долгое BUTTON_2 / next / action"] BTN["bsp_button<br/>короткое BUTTON_1 (вход/next) / BUTTON_2 (action)"]
subgraph MODEL["menu (чистая модель, host-тест)"] subgraph MODEL["menu (чистая модель, host-тест)"]
TREE["дерево-данные<br/>menu_item_desc_t[]"] TREE["дерево-данные<br/>menu_item_desc_t[]"]
NAV["навигация + редактор<br/>menu_next / menu_action"] NAV["навигация + редактор<br/>menu_next / menu_action"]
@ -68,8 +69,10 @@ typedef struct {
## 3. Навигация ## 3. Навигация
Две кнопки: BUTTON_1 → `menu_next` (следующий пункт уровня, с заворотом); короткое BUTTON_2 → Две кнопки, обе коротким нажатием (без удержания — раскладка `OLD_PROJECT_TFT8_UKL`): BUTTON_1 →
`menu_action` (по типу пункта). Вход в меню — долгое BUTTON_2 (app-слой). `menu_next` (следующий пункт уровня, с заворотом) когда меню открыто, **вход в меню** (app-слой,
`menu_open`) когда закрыто; BUTTON_2 → `menu_action` (по типу пункта) когда открыто, намеренный
no-op когда закрыто.
```mermaid ```mermaid
flowchart TD flowchart TD
@ -101,32 +104,100 @@ flowchart TD
на дескриптор протокола `sul_settings_desc_t` (§8) — меню строит раздел «Настройки протокола» из на дескриптор протокола `sul_settings_desc_t` (§8) — меню строит раздел «Настройки протокола» из
дескриптора активного протокола, не хардкодом. дескриптора активного протокола, не хардкодом.
**Поток сохранения** (app-слой связывает модель и flash): **Поток сохранения** (`menu_task` связывает модель и flash — см. §5):
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant U as Пользователь (кнопки) participant U as Пользователь (кнопки)
participant M as menu (модель) participant M as menu (модель)
participant S as settings_t (RAM) participant S as settings_t (RAM)
participant A as app participant MT as menu_task
participant St as settings_store participant St as settings_store
U->>M: menu_action (правка / выход) U->>M: menu_action (правка / выход)
M->>S: запись поля по offset (dirty=true) M->>S: запись поля по offset (dirty=true)
M-->>A: open=false, save_requested=true M-->>MT: open=false, save_requested=true
A->>St: settings_store_save() MT->>St: settings_store_save()
St->>St: serialize + CRC → QSPI 0x450000 St->>St: serialize + CRC → QSPI 0x450000
A->>A: пере-применить настройки (напр. адрес → декодер)
``` ```
После сохранения app пере-применяет изменившиеся настройки к рантайму (например, новый адрес — Адрес станции (`nku_can_set_address()`) `sul_rx_task` пере-применяет из настроек сам, на следующей
в `nku_can_set_address()`), т.к. декодер держит свою копию адреса. итерации — не требует отдельного сигнала (декодер читает `settings_store_get()` каждую итерацию).
--- ---
## 5. Рендер (Фаза 3.2.2) ## 5. Рендер и wiring (Фаза 3.2.23.2.4)
Рендер — отдельный слой, читает модель запросами (`menu_current`, `menu_level_range`, Рендер ([menu_view.c](../../firmware/tft_app/src/ui/menu/src/menu_view.c)) — отдельный слой, читает
`menu_read_value`) и рисует в **фиксированном окне 480×272 в логических (0,0)** — одинаково на всех модель запросами (`menu_current`, `menu_level_range`, `menu_read_value`) и рисует в **фиксированном
панелях (на больших — левый-верхний угол, остальное чёрное). Меню модально: пока `menu_is_open()`, окне 480×272 @ логич.(0,0)** — одинаково на всех панелях (на больших — левый-верхний угол, остальное
индикация под ним не рисуется. Детали — [PLAN.md, Фаза 3.2](../../firmware/tft_app/PLAN.md). чёрное). Логики навигации не содержит.
**Раскладка** (под реальные шрифты: `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.13.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<br/>софт-таймер 5 мс<br/>(debounce, независимо от обеих задач)"]
MT["menu_task<br/>модель + мгновенный вход/навигация + save<br/>НЕ рисует"]
RX["sul_rx_task<br/>CAN → decode → controller<br/>WDOG безусловно"]
RT["render_task<br/>ЕДИНСТВЕННЫЙ вызывающий gfx_present()<br/>event-driven"]
BTN --> MT
MT -->|"xTaskNotifyGive<br/>(любое изменение)"| RT
RX -->|"xQueueOverwrite (данные)<br/>+ 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-буфере
логгера между несколькими пишущими задачами).

View file

@ -58,7 +58,11 @@ include(${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cm
add_executable( add_executable(
app 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_GENERATED}/clock_config.c # BOARD_BootClockRUN (зовётся board_hw_init)
${BSP_STARTUP_FILE} ${BSP_STARTUP_FILE}
${BSP_SYSCALLS_FILE} ${BSP_SYSCALLS_FILE}
@ -70,6 +74,12 @@ add_executable(
# ниже (BSP_TICK_FREERTOS_MODE), чтобы не тянуть SysTick_Handler, # ниже (BSP_TICK_FREERTOS_MODE), чтобы не тянуть SysTick_Handler,
# конфликтующий с FreeRTOS-портом. Приходит транзитивно через bsp_uart_host. # конфликтующий с FreeRTOS-портом. Приходит транзитивно через bsp_uart_host.
${CMAKE_SOURCE_DIR}/bsp/tick/src/tick.c ${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 ${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c) ${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c)
@ -106,6 +116,7 @@ target_link_libraries(
app app
PRIVATE bsp_board PRIVATE bsp_board
bsp_led bsp_led
bsp_button
bsp_wdog bsp_wdog
bsp_boot_state bsp_boot_state
bsp_qspi_flash bsp_qspi_flash

View file

@ -210,6 +210,27 @@ marks_both_pending`, аппаратно на реальном обрыве св
> подстановка «-»); недостаёт выделенной чистой функции проверки покрытия и её теста — > подстановка «-»); недостаёт выделенной чистой функции проверки покрытия и её теста —
> требует небольшого выноса из `gfx.c` (зависит от `bsp_display`). > требует небольшого выноса из `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 > Побочная находка: `SystemFont.c` (сгенерирован lcd-image-converter) экспортирует tFont
> под именем `JBMono24` (из .xml), а `gfx.h` обещал `SystemFont` — рассинхрон был > под именем `JBMono24` (из .xml), а `gfx.h` обещал `SystemFont` — рассинхрон был
> латентным (Фаза 1 линковала только `FloorFontFallback`), вскрылся при первом > латентным (Фаза 1 линковала только `FloorFontFallback`), вскрылся при первом
@ -231,10 +252,44 @@ marks_both_pending`, аппаратно на реальном обрыве св
--- ---
## Фаза 3 — Настройки и меню ## Фаза 3 — Настройки и меню 🟨
**Цель.** Персистентные настройки на QSPI, меню на двух кнопках, локальные входы (opto). **Цель.** Персистентные настройки на 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.33.6.
>
> _Флеш-грабли (в памяти):_ `host::flash-swd-app-slot-debug` шьёт **подписанный** образ
> `build/Debug/signed/app_slot_a.bin` → после правок обязательно `build::sign-app-debug`, иначе
> прошивается старый код. Вся работа Фаз 23 — в рабочем дереве (ветка `tft-app-dev`), не
> закоммичена (пользователь коммитит сам).
> **Согласованный дизайн (решено с пользователем; референсы — все старые проекты: > **Согласованный дизайн (решено с пользователем; референсы — все старые проекты:
> settings_manager ×3, menu ×7 вкл. ALPACA, main_programm/menu_task).** > 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.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.52 с); BUTTON_1=следующий, короткое BUTTON_2=выбор/инкремент. Опрос кнопок — задача 5 мс. Модальный экран. | | **3.2 движок меню + рендер** ✅ (рендер — на одном буфере, см. 3.2.4) | **Чистая модель** (дерево-данные, навигация, edit, offset-привязка) — оперирует переданным `settings_t*`, **сама не сохраняет** (выставляет флаг save, app зовёт `settings_store_save()`) → host-тест без QSPI. **Рендер** (примитивы `gfx`: список/курсор/значение) в **переносимом окне 480×272 @ логич.(0,0)** (одинаково на всех панелях; на больших — левый-верхний угол, остальное чёрное). Вход — **долгое нажатие BUTTON_2** (~1.52 с); 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.3 per-protocol дескрипторы** | `sul_settings_desc_t`: протокол регистрирует параметры (NKU-CAN: адрес 0..15); секция меню строится из дескриптора. |
| **3.4 opto-входы** | IN1/IN2 → вызов/ответ диспетчера → презентация (в fallback — примитив/текст; иконки-спрайты — Фаза 4/5). | | **3.4 opto-входы** | IN1/IN2 → вызов/ответ диспетчера → презентация (в fallback — примитив/текст; иконки-спрайты — Фаза 4/5). |
| **3.5 удалённая адресация NKU-CAN** | `0x4X1`/`0x5XB` → запись `nku_address` в настройки (долг Фазы 2). | | **3.5 удалённая адресация NKU-CAN** | `0x4X1`/`0x5XB` → запись `nku_address` в настройки (долг Фазы 2). |
| **3.6 тумблер логов** | `log_enabled` в настройках + пункт меню + рантайм-гейт `log_set_enabled()` поверх компайл-тайм `LOG_LEVEL`. NB: продакшн-сборка — с `LOG_LEVEL >= INFO`, иначе гейтить нечего (макросы вырезаны). | | **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.13.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-цикл ~строки 10521095). Наш `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 ядра настроек; логика навигации меню; связывание **Тесты (host).** Сериализация/дефолты/CRC ядра настроек; логика навигации меню; связывание
дескрипторов; распознавание команды удалённой адресации; рантайм-гейт логов. дескрипторов; распознавание команды удалённой адресации; рантайм-гейт логов.

View file

@ -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 <stdbool.h>
/* ── Приоритеты (единая точка правды, 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_ */

View file

@ -1,336 +1,59 @@
/** /**
* @file main.c * @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 (каркас, подтверждено на железе): образ линкуется как валидный * Фаза 0 (каркас, подтверждено на железе): образ линкуется как валидный
* MCUboot-слот (Direct-XIP), bootloader в него прыгает, FreeRTOS стартует, * MCUboot-слот (Direct-XIP), bootloader в него прыгает, FreeRTOS стартует,
* WDOG кормится, образ ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной * WDOG кормится, образ подтверждает себя в рантайме см. PLAN.md, Фаза 0.
* загрузке 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 не обязан успевать за каждым).
*/ */
#include "FreeRTOS.h" #include "FreeRTOS.h"
#include "app_tasks.h"
#include "board.h" #include "board.h"
#include "bootutil/bootutil_public.h" #include "bsp/button.h"
#include "bsp/boot_state.h"
#include "bsp/display.h"
#include "bsp/led.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 "queue.h"
#include "services/gfx.h"
#include "services/settings_store.h"
#include "task.h" #include "task.h"
#include "ui/fallback.h" #include "timers.h"
#include <stdbool.h> #include <stdbool.h>
#define LOG_TAG "app" #define INPUT_POLL_PERIOD_MS 5U /* софт-таймер опроса ввода (bsp_button_poll; opto — Фаза 3.4) */
/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */ /* ── Разделяемое состояние задач (объявления — app_tasks.h) ──────────────── */
#ifndef APP_OWN_SLOT_ID
#define APP_OWN_SLOT_ID 0
#endif
/* Панель текущего стенда — Фаза 1 хардкод (Фаза 9: provisioning выбирает из volatile bool g_display_ready = false;
* settings, см. ARCH §9 TFT7/8/10 рантайм-выбор внутри профиля app-big). */ volatile bool g_menu_active = false;
#define PANEL_TYPE BSP_DISPLAY_TFT8 QueueHandle_t g_render_queue = NULL;
TaskHandle_t g_render_task_handle = NULL;
#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 + timeoutdefault. Порядок величины как
* в 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);
}
}
int main(void) int main(void)
{ {
board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */ board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */
bsp_led_init(); bsp_led_init();
(void) bsp_button_init(); /* GPIO настроен в BOARD_InitPins; сброс debounce */
g_s_render_queue = xQueueCreate(1, sizeof(render_msg_t)); g_render_queue = xQueueCreate(1, sizeof(render_msg_t));
configASSERT(g_s_render_queue != NULL); configASSERT(g_render_queue != NULL);
/* sul_rx выше приоритетом render — приём CAN/WDOG важнее своевременности /* Опрос ввода — софт-таймер (демон на высшем приоритете в системе вытесняет
* перерисовки. Стеки x6 (3 КБ) с запасом (см. PLAN.md, Фаза 0 тонкий * всё остальное нажатия не теряются). Масштабируется на opto (Фаза 3.4)
* стек уже маскировался под похожий на зависание симптом). */ * тем же колбэком. */
(void) xTaskCreate(sul_rx_task, "sul_rx", configMINIMAL_STACK_SIZE * 6U, NULL, TimerHandle_t input_timer =
tskIDLE_PRIORITY + 2U, NULL); xTimerCreate("input", pdMS_TO_TICKS(INPUT_POLL_PERIOD_MS), pdTRUE, NULL, input_poll_cb);
(void) xTaskCreate(render_task, "render", configMINIMAL_STACK_SIZE * 6U, NULL, configASSERT(input_timer != NULL);
tskIDLE_PRIORITY + 1U, 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(); vTaskStartScheduler();

View file

@ -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 <stdbool.h>
#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); /* одноразовая задача — дальше нечего делать */
}

View file

@ -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.13.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 <stdbool.h>
#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));
}
}

View file

@ -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 <stdbool.h>
#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;
}
}

View file

@ -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 <stdbool.h>
#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 + timeoutdefault. Порядок величины как
* в 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);
}
}

View file

@ -20,14 +20,28 @@ extern "C"
#endif #endif
/** /**
* @brief bsp_can_init() + RX-фильтры под активные ID НКУ-CAN. * @brief bsp_can_init(). RX-фильтры НЕ настраивает.
* *
* Фаза 1: фильтры на PACKET1(0x506)/PACKET3(0x508), адрес станции 0 * Вызывающий обязан сразу после этого позвать sul_transport_can_set_address()
* (хардкод совпадает с decode-стороной nku_can.c). Фаза 3 параметризует * внутренний сентинел форсирует первое применение фильтров независимо от
* оба конца из настроек одновременно. * переданного адреса (как в OLD_PROJECT msg_receiver_task).
*/ */
bsp_status_t sul_transport_can_init(void); 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. * @brief Принять один кадр и перевести в sul_frame_t.
* *

View file

@ -2,35 +2,82 @@
#include "bsp/can.h" #include "bsp/can.h"
/* Фаза 1: адрес станции захардкожен в 0 — совпадает с базовыми ID в /* Базовые ID НКУ-CAN (адрес станции 0) — та же протокольная раскладка, что и
* domain/sul/nku_can/src/nku_can.c (0x506|group4, group4=address<<4=0). * decode-сторона (domain/sul/nku_can/src/nku_can.c): PACKET1..4 кодируют
* Фаза 3 параметризует оба конца одновременно из настроек. */ * адрес в битах [7:4] ID (group4 = addr<<4), PACKET5 в битах [8:6]
#define PACKET1_ID 0x506U * (group6 = addr<<6, протокол отводит под него только 3 бита). Decode и
#define PACKET3_ID 0x508U * transport намеренно НЕ шарят общий заголовок с этими константами каждый
#define STD_ID_MASK 0x7FFU /* 11-bit STD — проверять все биты */ * владеет своей копией протокольного факта (декодер ничего не знает про
* транспорт и наоборот, 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 про /* Хранилище последнего принятого кадра — см. предупреждение в can.h про
* время жизни p_out->p_data, возвращаемого sul_transport_can_receive(). */ * время жизни p_out->p_data, возвращаемого sul_transport_can_receive(). */
static bsp_can_frame_t s_last_frame; 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) bsp_status_t sul_transport_can_init(void)
{ {
const bsp_can_config_t cfg = {.bitrate = 125000U}; /* см. OLD_PROJECT msg_receiver_task */ 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) if (st != BSP_OK)
{ {
return st; return st;
} }
st = bsp_can_set_filter(0U, PACKET1_ID, STD_ID_MASK, false); s_last_applied_address = ADDR;
if (st != BSP_OK) return 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);
} }
bsp_status_t sul_transport_can_receive(uint32_t timeout_ms, sul_frame_t *p_out) bsp_status_t sul_transport_can_receive(uint32_t timeout_ms, sul_frame_t *p_out)

View file

@ -14,7 +14,7 @@
* *
* Навигация: BUTTON_1 menu_next() (следующий пункт уровня, с заворотом); * Навигация: BUTTON_1 menu_next() (следующий пункт уровня, с заворотом);
* короткое BUTTON_2 menu_action() (вход в подменю / инкремент значения / * короткое BUTTON_2 menu_action() (вход в подменю / инкремент значения /
* выход-с-сохранением). Вход в меню (долгое BUTTON_2) и модальность уровень app. * выход-с-сохранением). Вход в меню (короткое BUTTON_1, вне меню) и модальность уровень app.
* *
* Соглашение: items[MENU_ROOT_INDEX] корневое SUBMENU; его дети верхний * Соглашение: items[MENU_ROOT_INDEX] корневое SUBMENU; его дети верхний
* уровень (их parent == MENU_ROOT_INDEX). Дети одного уровня непрерывный * уровень (их parent == MENU_ROOT_INDEX). Дети одного уровня непрерывный

View file

@ -23,4 +23,9 @@ set_source_files_properties(
PROPERTIES PROPERTIES
COMPILE_OPTIONS "-include;${CMAKE_CURRENT_SOURCE_DIR}/fonts/include/fonts.h;-w") 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)

View file

@ -1,9 +1,15 @@
/** /**
* @file gfx.h * @file gfx.h
* @brief Фаза 1 минимальный gfx: framebuffer (SDRAM, non-cacheable) + * @brief Фаза 3.2.4 gfx как компоновщик: double-buffer + PXP.
* RLE-шрифты (lcd-image-converter) + примитив стрелки. Без PXP/ *
* компоновщика/альфа-слоёв прямая запись в единственный framebuffer, * Модель (эталон OLD_PROJECT_TFT8_UKL/source/display/): CPU рисует ВЕСЬ кадр в
* который ELCDIF сканирует по DMA (см. gfx.c про non-cacheable SDRAM). * альфа-поверхность **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-декодирование порт проверенного в проде * Формат tImage/tChar/tFont и RLE-декодирование порт проверенного в проде
* алгоритма из OLD_PROJECT (source/fonts/fonts.c), тот же формат данных, что * алгоритма из OLD_PROJECT (source/fonts/fonts.c), тот же формат данных, что
@ -73,18 +79,32 @@ typedef uint32_t gfx_color_t;
#define GFX_COLOR_WHITE 0x00FFFFFFU #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() + * SDRAM (SEMC) должна быть уже поднята вызывающим (bsp_sdram_configure() +
* bsp_sdram_init()) gfx не владеет SEMC-инициализацией, только framebuffer * bsp_sdram_init()) gfx не владеет SEMC-инициализацией, только буферами
* внутри уже готовой SDRAM. * внутри уже готовой SDRAM.
* *
* @param type тип панели (Фаза 1 хардкод из app; Фаза 9 provisioning) * @param type тип панели (Фаза 1 хардкод из app; Фаза 9 provisioning)
*/ */
bsp_status_t gfx_init(bsp_display_type_t type); bsp_status_t gfx_init(bsp_display_type_t type);
/** Залить весь кадр цветом (обычно GFX_COLOR_BLACK перед перерисовкой). */ /** Обнулить AS (весь кадр становится прозрачным). Вызывать перед отрисовкой
void gfx_clear(gfx_color_t color); * нового полного кадра; непрорисованные области покажут фон 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 Нарисовать строку заданным цветом (тинтинг с альфа-сглаживанием). * @brief Нарисовать строку заданным цветом (тинтинг с альфа-сглаживанием).

View file

@ -1,47 +1,69 @@
#include "services/gfx.h" #include "services/gfx.h"
#include "FreeRTOS.h"
#include "fsl_common.h" /* AT_NONCACHEABLE_SECTION_ALIGN */ #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 <stddef.h> #include <stddef.h>
#include <string.h>
/* ── Framebuffer (SDRAM, non-cacheable) ──────────────────────────────────── #define LOG_TAG "gfx"
/* ── Поверхности компоновщика (SDRAM, non-cacheable) ────────────────────────
* *
* AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции * AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции
* NonCacheable/.ncache cmake/linker/..._app_slot.ld отображает её на * 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 через * board.c, Region 9) конфигурирует ЭТОТ ЖЕ диапазон как non-cacheable через
* линкер-символы __NCACHE_REGION_START/SIZE рекомендация NXP для буферов, * линкер-символы __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 * тот же макрос, 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), не * Четыре поверхности (эталон TFT8_UKL): AS (рисует CPU), PS (фон, чёрный),
* под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9) станет рантайм-выбирать * FB[2] (выход PXP = вход ELCDIF, double buffer). Размер под ТЕКУЩУЮ панель
* между TFT7/8/10 в одном бинарнике, размер и m_sdram_ncache (сейчас 2 МБ) * стенда (TFT8, 800×600), не под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9)
* придётся поднять до максимума (1024×600×4 2.34 МБ 4 МБ регион). */ * станет рантайм-выбирать между TFT7/8/10 в одном бинарнике, размер поверхностей
#define FRAMEBUFFER_ALIGN 64U /* см. OLD_PROJECT FRAME_BUFFER_ALIGN — типичное ELCDIF/AXI выравнивание */ * и m_sdram_ncache придётся поднять до максимума панели. */
#define FRAMEBUFFER_PIXELS (800U * 600U) #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 '-' #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_width;
static uint16_t s_fb_height; 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) static inline void set_pixel(uint16_t x, uint16_t y, gfx_color_t color)
{ {
if ((x >= s_fb_width) || (y >= s_fb_height)) if ((x >= s_fb_width) || (y >= s_fb_height))
{ {
return; /* примитивы могут частично выходить за экран — не UB, просто обрезка */ 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) на фон /* Альфа-блендинг: наложить @p color с покрытием @p a (0..255) на текущее
* framebuffer'а. a=0 фон нетронут (прозрачный край глифа), a=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) 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)) 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; const uint32_t idx = (uint32_t) y * s_fb_width + x;
if (a == 0xFFU) if (a == 0xFFU)
{ {
s_framebuffer[idx] = color; s_alpha_buffer[idx] = (color & 0x00FFFFFFU) | ALPHA_OPAQUE;
return; 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 inv = 255U - a;
const uint32_t r = (((color >> 16) & 0xFFU) * a + ((bg >> 16) & 0xFFU) * inv) / 255U; 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 g = (((color >> 8) & 0xFFU) * a + ((bg >> 8) & 0xFFU) * inv) / 255U;
const uint32_t b = (((color) & 0xFFU) * a + ((bg) & 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, гарантия формата /* ── Поиск глифа (бинарный — 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) bsp_status_t gfx_init(bsp_display_type_t type)
{ {
/* SDRAM (SEMC) — забота вызывающего (bsp_sdram_configure()+init()), gfx /* SDRAM (SEMC) — забота вызывающего (bsp_sdram_configure()+init()), gfx
* владеет только framebuffer'ом внутри уже готовой SDRAM. */ * владеет только поверхностями внутри уже готовой SDRAM. */
const bsp_status_t st = bsp_display_init(type, (uint32_t) s_framebuffer, NULL); 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) if (st != BSP_OK)
{ {
return st; 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(); const bsp_display_size_t *p_size = bsp_display_get_size();
s_fb_width = p_size->width; s_fb_width = p_size->width;
s_fb_height = p_size->height; s_fb_height = p_size->height;
s_back_index = 0U; /* FB[0] показывается; первый present уйдёт в FB[1] */
gfx_pxp_init();
return BSP_OK; 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; /* Прозрачно (alpha 0) → непрорисованные области покажут фон PS (чёрный). */
(void) memset(s_alpha_buffer, 0, (size_t) s_fb_width * s_fb_height * BYTES_PER_PIXEL);
for (uint32_t i = 0U; i < count; i++) }
{
s_framebuffer[i] = color; 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));
} }

View file

@ -14,15 +14,17 @@ void settings_store_init_defaults(void)
bsp_status_t settings_store_load(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(); g_settings = settings_defaults();
return BSP_ERR_HW; return BSP_ERR_HW;
} }
if (!settings_deserialize(&page, &g_settings)) if (!settings_deserialize(&s_page, &g_settings))
{ {
g_settings = settings_defaults(); g_settings = settings_defaults();
return BSP_ERR_INVALID; return BSP_ERR_INVALID;

View file

@ -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) 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); const char *label = mode_label(mode);
if (label == NULL) if (label == NULL)

View file

@ -6,6 +6,10 @@
* заголовок уровня, список пунктов с полосой-курсором, значения, футер-подсказка. * заголовок уровня, список пунктов с полосой-курсором, значения, футер-подсказка.
* Читает только модель (menu_ctx_t) через её запросы; логики навигации не содержит. * Читает только модель (menu_ctx_t) через её запросы; логики навигации не содержит.
* HIL проверяется на железе. * HIL проверяется на железе.
*
* Рисует ПОЛНЫЙ кадр в AS (double-buffer + PXP, Фаза 3.2.4) инкрементальной
* построчной отрисовки больше нет: off-screen перерисовка полного кадра +
* атомарный свап дёшевы и tear-free. Показ gfx_present() у владельца дисплея.
*/ */
#ifndef UI_MENU_VIEW_H_ #ifndef UI_MENU_VIEW_H_
@ -18,7 +22,11 @@ extern "C"
{ {
#endif #endif
/** Отрисовать текущий кадр меню по состоянию модели. Меню должно быть открыто. */ /**
* @brief Отрисовать полный кадр меню в AS (очистка + заголовок + строки + футер).
*
* Рисует off-screen (AS); на экран попадёт после gfx_present() владельца дисплея.
*/
void menu_view_render(const menu_ctx_t *p_ctx); void menu_view_render(const menu_ctx_t *p_ctx);
#ifdef __cplusplus #ifdef __cplusplus

View file

@ -7,17 +7,23 @@
/* Окно 480×272 @ (0,0). Метрики под реальные шрифты: JBMono24 h=31 (заголовок/ /* Окно 480×272 @ (0,0). Метрики под реальные шрифты: JBMono24 h=31 (заголовок/
* строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */ * строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */
#define WIN_W 480U #define WIN_W 480U
#define WIN_H 272U
#define TITLE_H 36U #define TITLE_H 36U
#define ROW_H 36U #define ROW_H 36U
#define FOOTER_H 20U #define FOOTER_H 20U
#define ROWS_Y0 TITLE_H #define ROWS_Y0 TITLE_H
#define FOOTER_Y (272U - FOOTER_H) /* 252 */ #define FOOTER_Y (WIN_H - FOOTER_H) /* 252 */
#define TEXT_DY24 2U /* (36-31)/2 — центрирование JBMono24 в полосе 36 */ #define TEXT_DY24 2U /* центрирование JBMono24 (h31) в полосе 36 */
#define TEXT_DY12 2U /* (20-16)/2 — JBMono12 в футере 20 */ #define TEXT_DY12 2U /* JBMono12 (h16) в футере 20 */
#define LEFT_MARGIN 16U #define LEFT_MARGIN 16U
#define RIGHT_MARGIN 16U #define RIGHT_MARGIN 16U
#define EDGE_MARGIN 8U #define EDGE_MARGIN 8U
/* Рамка 1 px по периметру — строки/футер вписаны внутрь неё (не затирают). */
#define BORDER 1U
#define CONTENT_X BORDER
#define CONTENT_W (WIN_W - 2U * BORDER)
/* Палитра (XRGB). Собирается тинтингом из белых шрифтов. */ /* Палитра (XRGB). Собирается тинтингом из белых шрифтов. */
#define COL_BG 0x000000U #define COL_BG 0x000000U
#define COL_TITLE 0x00CFE0FFU #define COL_TITLE 0x00CFE0FFU
@ -26,7 +32,7 @@
#define COL_SEL_BG 0x0017406BU #define COL_SEL_BG 0x0017406BU
#define COL_SEL_TEXT 0x00FFFFFFU #define COL_SEL_TEXT 0x00FFFFFFU
#define COL_FOOTER 0x006F6F6FU #define COL_FOOTER 0x006F6F6FU
#define COL_SEP 0x00333333U #define COL_SEP 0x00333333U /* разделители + рамка */
/* Строка значения пункта. @return false — у пункта нет значения (BACK). */ /* Строка значения пункта. @return false — у пункта нет значения (BACK). */
static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size_t buf_len) 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, ">"); (void) snprintf(p_buf, buf_len, ">");
return true; return true;
case MENU_BYTE: 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; return true;
case MENU_SELECT: case MENU_SELECT:
case MENU_BOOL: 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 else
{ {
(void) snprintf(p_buf, buf_len, "%u", V); (void) snprintf(p_buf, buf_len, "%u", (unsigned) V);
} }
return true; 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) static void draw_row(const menu_ctx_t *p_ctx, uint8_t idx, uint16_t row_y, bool selected)
{ {
if (selected) const gfx_color_t BG_COL = selected ? COL_SEL_BG : COL_BG;
{
gfx_fill_rect(0U, row_y, WIN_W, ROW_H, COL_SEL_BG);
}
const gfx_color_t TEXT_COL = selected ? COL_SEL_TEXT : COL_LABEL; 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, (void) gfx_draw_string(&SystemFont, p_ctx->items[idx].label, LEFT_MARGIN,
(uint16_t) (row_y + TEXT_DY24), TEXT_COL); (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) 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); (uint16_t) (FOOTER_Y + TEXT_DY12), COL_FOOTER);
const uint8_t TOTAL = (uint8_t) (level_last - level_first + 1U); 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) void menu_view_render(const menu_ctx_t *p_ctx)
{ {
/* Меню модально и переносимо: чёрный весь экран, окно — верхний-левый 480×272. */ /* Полный кадр off-screen: обнулить AS (вне окна 480×272 → чёрный фон PS),
gfx_clear(COL_BG); * затем нарисовать окно. Свап gfx_present() у владельца дисплея. */
gfx_clear();
if (!p_ctx->open) if (!p_ctx->open)
{ {
return; 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 char *p_title = p_ctx->items[p_ctx->items[p_ctx->cur].parent].label;
const uint16_t TW = gfx_string_width(&SystemFont, p_title); 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); (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 first;
uint8_t last; uint8_t last;
menu_level_range(p_ctx, &first, &last); menu_level_range(p_ctx, &first, &last);
@ -126,9 +133,11 @@ void menu_view_render(const menu_ctx_t *p_ctx)
{ {
break; break;
} }
const uint16_t ROW_Y = (uint16_t) (ROWS_Y0 + slot * ROW_H); draw_row(p_ctx, IDX, (uint16_t) (ROWS_Y0 + slot * ROW_H), (IDX == p_ctx->cur));
draw_row(p_ctx, IDX, ROW_Y, (IDX == p_ctx->cur));
} }
draw_footer(p_ctx, first, last); draw_footer(p_ctx, first, last);
/* Рамка — последней, поверх содержимого. */
gfx_draw_rect(0U, 0U, WIN_W, WIN_H, COL_SEP);
} }

View file

@ -15,7 +15,6 @@
* @note Вызов LOG_* из ISR запрещён: xSemaphoreTake с portMAX_DELAY * @note Вызов LOG_* из ISR запрещён: xSemaphoreTake с portMAX_DELAY
* недопустим в ISR-контексте. * недопустим в ISR-контексте.
*/ */
#if 0
#include "FreeRTOS.h" #include "FreeRTOS.h"
#include "log/log.h" #include "log/log.h"
#include "semphr.h" #include "semphr.h"
@ -37,4 +36,3 @@ void log_mutex_unlock(void)
{ {
xSemaphoreGive(s_log_mutex); xSemaphoreGive(s_log_mutex);
} }
#endif

View file

@ -56,6 +56,7 @@ add_sdk_driver(lpi2c fsl_lpi2c.c)
add_sdk_driver(pwm fsl_pwm.c) add_sdk_driver(pwm fsl_pwm.c)
add_sdk_driver(adc fsl_adc.c) add_sdk_driver(adc fsl_adc.c)
add_sdk_driver(elcdif fsl_elcdif.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(cache fsl_cache.c)
add_sdk_driver(sai fsl_sai.c) add_sdk_driver(sai fsl_sai.c)
add_sdk_driver(sai_edma fsl_sai_edma.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_pwm PUBLIC sdk_clock)
target_link_libraries(sdk_adc PUBLIC sdk_clock) target_link_libraries(sdk_adc PUBLIC sdk_clock)
target_link_libraries(sdk_elcdif 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 PUBLIC sdk_clock)
target_link_libraries(sdk_sai_edma PUBLIC sdk_sai sdk_edma) target_link_libraries(sdk_sai_edma PUBLIC sdk_sai sdk_edma)
target_link_libraries(sdk_edma PUBLIC sdk_common) target_link_libraries(sdk_edma PUBLIC sdk_common)