lift_indicator_suite/firmware/tft_app/REPORT.md
2026-07-21 15:38:59 +03:00

18 KiB
Raw Permalink Blame History

Готово. Полная повторная верификация:

Проверка Результат
Host-тесты 19/19 зелёные
Debug сборка+подпись чисто, image_ok UNSET
Release сборка+подпись чисто, image_ok UNSET
Оба бинарника 2 097 152 байт (ровно размер слота)
Аппаратно (ваш лог) self-confirm пережил перезагрузку, CAN→рендер работает

PLAN.md обновлён: Фаза 1 → , известное ограничение (single-buffer) зафиксировано с честной формулировкой механизма, явно вписано в объём Фазы 4 вместе с PXP. Также отметил, что критерий «timeout→--» проверен только логически (host-тестом), не на реальном обрыве связи — не стал завышать степень подтверждённости.

Отчёт — tft_app_phase_0_1_report.md. Полный текст ниже — можно скопировать целиком в новый тред:


# tft_app — отчёт по Фазам 01

> Канонические источники — `firmware/tft_app/ARCH.md` (архитектура) и `firmware/tft_app/PLAN.md`
> (статус фаз, живой документ, обновляется каждой фазой). Этот файл — снимок на момент завершения
> Фазы 1, для быстрого ввода в контекст новой сессии. При расхождении — верить PLAN.md/ARCH.md, не
> этому файлу.

**Статус:** Фаза 0 ✅ и Фаза 1 ✅ — обе аппаратно подтверждены. Работа Фазы 1 **не закоммичена**
(в рабочем дереве поверх коммита `# tft_app: Phase 0`, `a9e6681`).

---

## Фаза 0 — Каркас (FreeRTOS + self-confirm слота MCUboot)

**Итог.** `firmware/tft_app` собирается как MCUboot-слот-образ (Direct-XIP), bootloader в него
прыгает, FreeRTOS стартует, образ подтверждает себя в рантайме (`boot_set_next`) и переживает
power cycle без отката — anti-brick сохранён. Подтверждено дважды на реальном железе (в т.ч.
через переустановку с microSD).

**Путь до рабочего состояния был не прямым — три независимых бага, все найдены и закрыты:**

1. **`bsp_qspi_init()` обязателен до любого `flash_area_*`.** Без него запись `image_ok` в трейлер
   слота молча падала — `boot_set_next()` возвращал успех, но ничего не персистило.
2. **Корневая причина (самая коварная): забытый дефайн `__STARTUP_INITIALIZE_RAMFUNCTION`.**
   Запись QSPI из app виснет в `qspi_write_fifo` — FlexSPI не дренирует IP TX FIFO. Причина:
   startup копирует секцию `.ram_function` (код `bsp_qspi`, помеченный
   `AT_QUICKACCESS_SECTION_CODE`) из flash в ITCM только под этим дефайном; `app` его не задавал
   — в ITCM оставался leftover ramfunc **загрузчика** (тот исполнялся первым), поэтому чтения
   «случайно работали», а запись — нет. Оба рабочих потребителя `bsp_qspi` (firmware_test,
   bootloader) этот дефайн задают; `app` был первым, кто его пропустил. Диагностировано под
   отладчиком (зависание конкретно в `qspi_write_fifo`), подтверждено изолированным
   erase+write+readback self-test (частота мигания LED = индикатор результата) до восстановления
   полной диагностики.
3. **UART, не USB CDC, для диагностики/логов.** USB CDC (target-side EHCI/PHY) под FreeRTOS не
   заработал стабильно за несколько заходов — первый в репозитории FreeRTOS-потребитель target
   USB device stack, класс неопределённости остался непроверенным. LPUART1/MCU-Link VCOM — самый
   проверенный канал в репозитории (все `tests/target/*` HIL-образы), доступен сразу через уже
   подключённый для SWD кабель, без enumeration/wait. Решение принято явно с пользователем.

**Побочные, но важные для будущих фаз находки:**
- Любая bsp-инициализация, зависящая от тика (`bsp_delay()` внутри `bsp_usb_cdc_init()` и т.п.),
  нельзя звать **до** `vTaskStartScheduler()` — в FreeRTOS-режиме `bsp_tick` это
  `xTaskGetTickCount()`, который не идёт до старта планировщика → вызов зависает навсегда молча.
  `main()` в итоге — минимальный bare-metal (board bring-up + запуск планировщика), вся
  инициализация — из задачи.
- Первый в репозитории потребитель `BSP_TICK_FREERTOS_MODE` вскрыл 2 латентных бага: (1) общий
  `bsp_tick` — bare-metal STATIC-либа → `SysTick_Handler` конфликтует с FreeRTOS-портом (решение:
  компилировать `tick.c` прямо в `app`, не через `libbsp_tick.a`); (2) `configTICK_RATE_HZ`
  нельзя как `((TickType_t)1000)``#if` в `tick.c` не разбирает cast, только `1000`.
- `.vscode/launch.json`: у `🐛 Debug: tft_app` был `preLaunchTask` на `firmware_test`
  (copy-paste) — исправлено на `build:app-debug`.
- Тонкий стек задачи (изначально x2=1 КБ) маскировался под тот же класс симптома, что и другие
  баги (зависание/reset-loop) — увеличен с запасом (x4-x6, 2-3 КБ) везде, где заводятся новые
  задачи.

**Осознанное отклонение от исходного плана:** `firmware/bootloader/test_stub` НЕ удалён (план
предполагал) — оставлен как независимая A/B-регрессия загрузчика, `app` пока не покрывает те же
сценарии (hang-классы, revert без self-confirm).

---

## Фаза 1 — Walking skeleton (CAN → декодер → контроллер → fallback-рендер)

**Итог.** Полный вертикальный срез: реальный CAN-кадр НКУ-CAN → чистый декодер → контроллер
(diff) → fallback-рендер (позиция + стрелка) на экране. Подтверждено на TFT8 с реальным
CAN-трафиком станции.

### Архитектура (по слоям, каждый — своя CMake-либа, конвенция `bsp/<module>`)

domain/elevator_model sul_result_t (pos: UTF-8 строка, direction: none/up/down/double), sul_default_state() domain/sul sul_frame_t, sul_driver_t, sul_status_t, реестр драйверов (заголовок и реализация реестра — РАЗНЫЕ CMake-таргеты, иначе цикл: декодеру нужны только типы sul.h, реестру — decode() декодера) domain/sul/nku_can чистый декодер PACKET1 (направление) / PACKET3 (позиция) — порт OLD_PROJECT msg_receiver_task, БЕЗ единого HAL-вызова domain/sul/transport/can тонкий bsp_can → sul_frame_t адаптер (HW, не host-тестируется) domain/controller process(sul_result_t) → indication_task_t (diff), кэш services/gfx framebuffer (SDRAM non-cacheable) + RLE-декодер шрифтов (lcd-image-converter формат) + примитив стрелки ui/fallback рендер pos+стрелка, asset-free/FS-free (ARCH §11) app задачи sul_rx (WDOG+CAN+decode+controller) и render, связаны очередью глубины 1 (xQueueOverwrite — важно только последнее состояние, не история)


**Host-тесты (все зелёные, домен полностью протестирован до касания железа):**
- `tests/host/tft_app_sul_nku` — 16 тестов, golden-векторы CAN-кадров (все direction, позиции,
  легаси-квирк `left==0`≈пробел, кириллица, IGNORED/ERR на чужой ID/DLC/вне таблицы символов,
  накопление состояния между PACKET1 и PACKET3).
- `tests/host/tft_app_controller` — 6 тестов (diff-логика, timeout→default тем же путём, что и
  обычный кадр).
- Полный пакет: **19/19** (включая существующие тесты bootloader/bsp).

### Ключевые находки/решения по ходу

1. **Framebuffer в SDRAM обязан идти через `AT_NONCACHEABLE_SECTION_ALIGN`** (прямая рекомендация
   NXP, порт паттерна из `OLD_PROJECT/source/display/image_cache.c`). MPU держит SDRAM как
   `Normal Write-Back Cacheable` (`board_mpu_init()`, Region 8); ELCDIF читает framebuffer
   напрямую по DMA — без non-cacheable региона CPU писал бы через кэш, DMA видел бы устаревшие
   данные (мусор/частичные кадры на экране, выглядело бы как загадочный баг железа). Механизм уже
   был параметризован в `board_mpu_init()` через линкер-символы `__NCACHE_REGION_START/SIZE`
   (Region 9, унаследовано из bootloader) — переопределены в `app_slot.ld` на новый
   `m_sdram_ncache` (2 МБ в начале SDRAM) вместо OCRAM (256 КБ, для framebuffer ~1.83 МБ не
   влезло бы). На первой попытке framebuffer был сырым указателем на константный адрес (работало
   случайно, линкер честно репортил `0 B` в секции) — исправлено на настоящее объявление
   переменной через макрос.
2. **RLE-декодер шрифтов — порт проверенного в проде алгоритма** из `OLD_PROJECT/source/fonts/
   fonts.c` (UNIQUE/REPEATABLE блоки, бинарный поиск по отсортированному коду символа), не
   переизобретён с нуля. Формат — то, что реально экспортирует lcd-image-converter (`tImage`/
   `tChar`/`tFont`, ARGB8888, "Color A8R8G8B8"). Пользователь сгенерировал `FloorFontFallback.c`
   (Inter 215pt, символы 0-9 и "-") и `SystemFont.c` (JetBrains Mono 24pt, ASCII+кириллица)
   инструментом напрямую.
3. **По-символьный fallback** (ARCH §11): символ вне таблицы шрифта → подстановка `'-'`
   (согласованная политика — "-" общий заменитель во `FloorFontFallback`).
4. Сгенерированные шрифтовые файлы ожидают типы из bare-name `fonts.h` по-разному
   (`SystemFont.c` — явный `#include`, `FloorFontFallback.c` — вообще без include, разные версии
   конвертера) — закрыто через `-include`/`-I` в CMake; сами сгенерированные файлы не
   редактировались (конвертер их перезапишет при регенерации).
5. Файлы шрифтов перенесены из `domain/` (где их положил пользователь) в `services/gfx/fonts/` —
   по ARCH §4 шрифты относятся к gfx, не к домену (домен — чистый C без рендеринга).

### ⚠️ Известное ограничение (зафиксировано, чинится в Фазе 4)

Один framebuffer, без double buffering — `ui_fallback_render()` пишет напрямую в буфер, который
ELCDIF в этот момент сканирует по DMA. Если перерисовка (`gfx_clear` + RLE-decode глифов +
стрелка) не укладывается в один кадр развёртки, видно сам процесс закраски (глиф «набирается» по
частям за несколько кадров) — **не tearing и не порча пикселей** (каждая отдельная запись всегда
валидна), просто заметен сам процесс. Подтверждено на железе пользователем ("рвано, но без
артефактов" — совпадает с механизмом).

**Фикс — double buffering**, НЕ требует PXP отдельно (это вопрос буферизации, не композиции):
второй framebuffer (`m_sdram_ncache` 2→4 МБ), swap через `bsp_display_set_next_buffer()` +
семафор на `FRAME_DONE` (сейчас `gfx_init()` передаёт `NULL` вместо колбэка). Явно вписано в
объём Фазы 4 (PLAN.md) вместе с PXP-компоновщиком (для многослойной композиции реальных
ассетов — фон+спрайты+текст, та же фаза, естественно смежная работа).

### Прочее

- Критерий выхода выполнен на TFT8: реальный CAN-трафик → корректный номер этажа и стрелка.
  Таймаут→"--" логически проверен host-тестом, аппаратно на реальном обрыве связи отдельно не
  гонялся (низкий риск — тот же код-путь, что у обычного кадра).
- Debug и Release оба собираются и подписываются чисто (2 097 152 байта — ровно размер слота,
  `image_ok` UNSET в обоих — рантайм self-confirm, не sign-time `--confirm`).

---

## Состояние репозитория на конец Фазы 1

- Ветка: `tft-app-dev`.
- Коммит `a9e6681` (# tft_app: Phase 0) — последний закоммиченный чекпоинт.
- Вся Фаза 1 — в рабочем дереве, не закоммичена: `firmware/tft_app/src/{domain,services/gfx,
  ui/fallback}/`, `firmware/tft_app/src/app/main.c` (переписан), `tests/host/tft_app_sul_nku/`,
  `tests/host/tft_app_controller/`, изменения в `CMakePresets.json`,
  `cmake/linker/MIMXRT1052xxxxx_app_slot.ld`, `firmware/tft_app/CMakeLists.txt`,
  `tests/host/CMakeLists.txt`, `firmware/tft_app/PLAN.md`.
- Стоит рассмотреть коммит перед длительным перерывом/переключением контекста — на усмотрение
  пользователя (не коммичу без явной просьбы).

## Дальше — Фаза 2

Полный контракт `sul_result_t` (next, сигналы, `lading_secs`, `floor_num`), полный декодер
НКУ-CAN (PACKET1..5, режимы, удалённая установка адреса), таблица приоритетов режимов
(данные, потенциально клиентские), валидация рендеримости позиции (renderable-множество шрифта +
по-символьный fallback — механизм уже есть в `gfx`, п.3 выше). Подробности — PLAN.md, раздел
«Фаза 2».

Одно практическое замечание, раз вы переключаетесь в новый тред: вся Фаза 1 сейчас не закоммичена — сидит в рабочем дереве поверх коммита Фазы 0. Хотите, чтобы я закоммитил перед переключением (стандартный git add + сообщение по Фазе 1), или оставляете как есть?