From a9e668196a1f8e95e00aa3d2633658904218bff5 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Tue, 21 Jul 2026 12:04:49 +0300 Subject: [PATCH] # tft_app: Phase 0 --- .vscode/launch.json | 2 +- CMakeLists.txt | 7 +- CMakePresets.json | 6 +- cmake/linker/MIMXRT1052xxxxx_app_slot.ld | 237 ++++++++++++++++++ firmware/tft_app/ARCH.md | 41 +-- firmware/tft_app/CMakeLists.txt | 122 +++++++++ firmware/tft_app/PLAN.md | 61 ++++- firmware/tft_app/README.md | 65 +++++ firmware/tft_app/src/app/FreeRTOSConfig.h | 122 +++++++++ firmware/tft_app/src/app/boot_shims.c | 15 ++ firmware/tft_app/src/app/main.c | 206 +++++++++++++++ firmware/tft_app/src/domain/README.md | 3 + firmware/tft_app/src/menu/README.md | 3 + firmware/tft_app/src/services/README.md | 3 + firmware/tft_app/src/ui/README.md | 3 + just/build.just | 33 +++ just/host.just | 24 ++ port/log/CMakeLists.txt | 22 ++ port/log/include/port/log_cdc.h | 46 ++++ port/log/src/log_cdc.c | 48 ++++ tests/host/CMakeLists.txt | 9 + tests/host/tft_app_smoke/README.md | 8 + tests/host/tft_app_smoke/test_tft_app_smoke.c | 26 ++ tools/service_tui/assets/macos_icon.png | Bin 0 -> 9503 bytes 24 files changed, 1084 insertions(+), 28 deletions(-) create mode 100644 cmake/linker/MIMXRT1052xxxxx_app_slot.ld create mode 100644 firmware/tft_app/CMakeLists.txt create mode 100644 firmware/tft_app/README.md create mode 100644 firmware/tft_app/src/app/FreeRTOSConfig.h create mode 100644 firmware/tft_app/src/app/boot_shims.c create mode 100644 firmware/tft_app/src/app/main.c create mode 100644 firmware/tft_app/src/domain/README.md create mode 100644 firmware/tft_app/src/menu/README.md create mode 100644 firmware/tft_app/src/services/README.md create mode 100644 firmware/tft_app/src/ui/README.md create mode 100644 port/log/include/port/log_cdc.h create mode 100644 port/log/src/log_cdc.c create mode 100644 tests/host/tft_app_smoke/README.md create mode 100644 tests/host/tft_app_smoke/test_tft_app_smoke.c create mode 100644 tools/service_tui/assets/macos_icon.png diff --git a/.vscode/launch.json b/.vscode/launch.json index 5f2075b..606cbc0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -57,7 +57,7 @@ "loadFiles": [], "runToEntryPoint": "main", "rtos": "FreeRTOS", - "preLaunchTask": "build:firmware-test-debug", + "preLaunchTask": "build:app-debug", } ] } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 3cd41cc..a99062a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,8 +47,11 @@ add_subdirectory(lib) if(NOT BUILD_TESTS_HOST) add_subdirectory(firmware/test) add_subdirectory(firmware/bootloader) - # Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) — удалить, - # когда появится реальный firmware/tft_app. См. firmware/bootloader/PLAN.md. + add_subdirectory(firmware/tft_app) + # Заглушка tft_app для аппаратной верификации bootutil (Фаза 2). Пока + # оставляем рядом с реальным firmware/tft_app как независимую A/B-регрессию + # загрузчика — удалить, когда app сам сможет служить стендом. См. + # firmware/bootloader/PLAN.md и firmware/tft_app/PLAN.md. add_subdirectory(firmware/bootloader/test_stub) endif() diff --git a/CMakePresets.json b/CMakePresets.json index 675efc3..160a408 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -167,7 +167,8 @@ "test_mcuboot_boot_select", "test_slot_version", "test_update_policy", - "test_recovery" + "test_recovery", + "test_tft_app_smoke" ] }, { @@ -190,7 +191,8 @@ "test_mcuboot_boot_select", "test_slot_version", "test_update_policy", - "test_recovery" + "test_recovery", + "test_tft_app_smoke" ] }, { diff --git a/cmake/linker/MIMXRT1052xxxxx_app_slot.ld b/cmake/linker/MIMXRT1052xxxxx_app_slot.ld new file mode 100644 index 0000000..07a8c70 --- /dev/null +++ b/cmake/linker/MIMXRT1052xxxxx_app_slot.ld @@ -0,0 +1,237 @@ +/* +** ################################################################### +** Linker file — tft_app как MCUboot-слот-образ (Direct-XIP). +** +** Полный набор секций как в MIMXRT1052xxxxx_bootloader_flexspi_nor.ld +** (включая .ram_function → ITCM для AT_QUICKACCESS_SECTION_CODE, .ncache, +** DataQuickAccess), но адаптирован под слот: +** - НЕТ .flash_config / .ivt (образ грузит не BootROM, а bootloader +** software-jump'ом — FCB/IVT/DCD не нужны). +** - Перед vector table зарезервирован imgtool-заголовок (0x200): +** boot_select.c прыгает по flash_base + fa_off + ih_hdr_size. +** - m_text — на весь слот (2 МБ − header − vectors). +** +** КРИТИЧНО: .ram_function размещает код bsp_qspi_flash +** (AT_QUICKACCESS_SECTION_CODE, входная секция CodeQuickAccess) в ITCM и +** копируется startup'ом. Без неё этот код исполняется из flash, а любая +** IP-команда FlexSPI (bsp_qspi_init/erase/write) блокирует AHB-путь → +** HardFault на fetch следующей инструкции из той же flash (XIP). См. +** bsp/qspi_flash/README.md "XIP-безопасность" и +** docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md §5. +** +** Базовый адрес слота — через -Wl,--defsym=__slot_base__=0x... +** Slot A: 0x60040000 Slot Б: 0x60240000 +** +** Copyright 2016 Freescale Semiconductor, Inc. +** Copyright 2016-2024 NXP +** SPDX-License-Identifier: BSD-3-Clause +** ################################################################### +*/ + +ENTRY(Reset_Handler) + +HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x0400; /* newlib sbrk-heap; FreeRTOS heap_4 — свой массив в .bss */ +STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x1000; +VECTOR_RAM_SIZE = DEFINED(__ram_vector_table__) ? 0x00000400 : 0; + +SLOT_BASE = DEFINED(__slot_base__) ? __slot_base__ : 0x60040000; +SLOT_SIZE = 0x00200000; /* 2 МБ — = imgtool -S 0x200000 и flash_map */ +IMGTOOL_HDR_SZ = 0x200; /* imgtool sign -H 0x200 */ +VECTOR_SIZE = 0x400; + +MEMORY +{ + m_interrupts (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ, LENGTH = VECTOR_SIZE + m_text (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ + VECTOR_SIZE, LENGTH = SLOT_SIZE - IMGTOOL_HDR_SZ - VECTOR_SIZE + m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128 КБ — ramfunc */ + m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128 КБ — .data/.bss/heap/stack */ + m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256 КБ — ncache */ + m_sdram (RW) : ORIGIN = 0x80000000, LENGTH = 0x02000000 /* SDRAM 32 МБ — Фаза 0 не используется */ +} + +SECTIONS +{ + __NCACHE_REGION_START = ORIGIN(m_data2); + __NCACHE_REGION_SIZE = 0x2000; /* 8 КБ non-cacheable */ + + /* Vector table (imgtool header зарезервирован до неё в MEMORY). */ + .interrupts : + { + __VECTOR_TABLE = .; + __Vectors = .; + . = ALIGN(4); + KEEP(*(.isr_vector)) + . = ALIGN(4); + } > m_interrupts + + .text : + { + . = ALIGN(4); + *(.text) + *(.text*) + *(.rodata) + *(.rodata*) + *(.glue_7) + *(.glue_7t) + *(.eh_frame) + KEEP (*(.init)) + KEEP (*(.fini)) + . = ALIGN(4); + } > m_text + + .ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } > m_text + + .ARM : + { + __exidx_start = .; + *(.ARM.exidx*) + __exidx_end = .; + } > m_text + + .ctors : + { + __CTOR_LIST__ = .; + KEEP (*crtbegin.o(.ctors)) + KEEP (*crtbegin?.o(.ctors)) + KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)) + KEEP (*(SORT(.ctors.*))) + KEEP (*(.ctors)) + __CTOR_END__ = .; + } > m_text + + .dtors : + { + __DTOR_LIST__ = .; + KEEP (*crtbegin.o(.dtors)) + KEEP (*crtbegin?.o(.dtors)) + KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)) + KEEP (*(SORT(.dtors.*))) + KEEP (*(.dtors)) + __DTOR_END__ = .; + } > m_text + + .preinit_array : + { + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP (*(.preinit_array*)) + PROVIDE_HIDDEN (__preinit_array_end = .); + } > m_text + + .init_array : + { + PROVIDE_HIDDEN (__init_array_start = .); + KEEP (*(SORT(.init_array.*))) + KEEP (*(.init_array*)) + PROVIDE_HIDDEN (__init_array_end = .); + } > m_text + + .fini_array : + { + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP (*(SORT(.fini_array.*))) + KEEP (*(.fini_array*)) + PROVIDE_HIDDEN (__fini_array_end = .); + } > m_text + + __etext = .; + __DATA_ROM = .; + + .interrupts_ram : + { + . = ALIGN(4); + __VECTOR_RAM__ = .; + __interrupts_ram_start__ = .; + *(.m_interrupts_ram) + . += VECTOR_RAM_SIZE; + . = ALIGN(4); + __interrupts_ram_end__ = .; + } > m_data + + __VECTOR_RAM = DEFINED(__ram_vector_table__) ? __VECTOR_RAM__ : ORIGIN(m_interrupts); + __RAM_VECTOR_TABLE_SIZE_BYTES = DEFINED(__ram_vector_table__) ? (__interrupts_ram_end__ - __interrupts_ram_start__) : 0x0; + + .data : AT(__DATA_ROM) + { + . = ALIGN(4); + __DATA_RAM = .; + __data_start__ = .; + *(.data) + *(.data*) + *(DataQuickAccess) /* quick access data section */ + KEEP(*(.jcr*)) + . = ALIGN(4); + __data_end__ = .; + } > m_data + + __ram_function_flash_start = __DATA_ROM + (__data_end__ - __data_start__); /* startup копирует ramfunc из flash в ITCM */ + + .ram_function : AT(__ram_function_flash_start) + { + . = ALIGN(32); + __ram_function_start__ = .; + *(CodeQuickAccess) /* AT_QUICKACCESS_SECTION_CODE — bsp_qspi FlexSPI IP-код */ + . = ALIGN(128); + __ram_function_end__ = .; + } > m_qacode + + __NDATA_ROM = __ram_function_flash_start + (__ram_function_end__ - __ram_function_start__); + .ncache.init : + { + . = ALIGN(32); + __noncachedata_start__ = .; + *(NonCacheable.init) + . = ALIGN(4); + __noncachedata_init_end__ = .; + } > m_data2 + . = __noncachedata_init_end__; + .ncache : + { + *(NonCacheable) + . = ALIGN(4); + __noncachedata_end__ = .; + } > m_data2 + + __DATA_END = __NDATA_ROM; + text_end = ORIGIN(m_text) + LENGTH(m_text); + ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data") + ASSERT(text_end <= SLOT_BASE + SLOT_SIZE, "app вышел за пределы слота (2 МБ)") + ASSERT((__noncachedata_end__ - ORIGIN(m_data2)) <= LENGTH(m_data2), "m_data2 ncache overflow") + + .bss : + { + . = ALIGN(4); + __START_BSS = .; + __bss_start__ = .; + *(.bss) + *(.bss*) + *(COMMON) + . = ALIGN(4); + __bss_end__ = .; + __END_BSS = .; + } > m_data + + .heap : + { + . = ALIGN(8); + __end__ = .; + PROVIDE(end = .); + __HeapBase = .; + . += HEAP_SIZE; + __HeapLimit = .; + __heap_limit = .; + } > m_data + + .stack : + { + . = ALIGN(8); + . += STACK_SIZE; + } > m_data + + __StackTop = ORIGIN(m_data) + LENGTH(m_data); + __StackLimit = __StackTop - STACK_SIZE; + PROVIDE(__stack = __StackTop); + + .ARM.attributes 0 : { *(.ARM.attributes) } + + ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap") +} diff --git a/firmware/tft_app/ARCH.md b/firmware/tft_app/ARCH.md index 9dbee7b..437d89e 100644 --- a/firmware/tft_app/ARCH.md +++ b/firmware/tft_app/ARCH.md @@ -12,6 +12,7 @@ **1.1 Индикация в реальном времени.** Местоположение кабины, спецрежимы, музыкальное сопровождение при движении, озвучка сигналов. + - Сигналы от СУЛ приходят по последовательному интерфейсу (+24 В UART, кастомный бинарный протокол) либо по CAN. - Отображение состояния двух диспетчерских оптовходов: «Вызов подан» / «Вызов принят» / пусто. @@ -56,7 +57,7 @@ A/B Direct-XIP; ассеты/layout — задача tft-app, см. §12). ### 4. Слои -``` +```bash ┌──────────────────────────────────────────────────────────────────────┐ │ app/ FreeRTOS-задачи, wiring, main (firmware/tft_app)│ L4 ├──────────────────────────────────────────────────────────────────────┤ @@ -77,7 +78,7 @@ A/B Direct-XIP; ассеты/layout — задача tft-app, см. §12). **Поток данных** (адаптирован из проекта `special`, поверх FreeRTOS): -``` +```bash транспорт (CAN/UART) ─► sul_driver.transport ─► sul_driver.decode(frame) ─► sul_result_t │ poll + timeout→default ▼ @@ -92,12 +93,12 @@ A/B Direct-XIP; ассеты/layout — задача tft-app, см. §12). ### 5. Размещение и тестируемость -| Слой | Где живёт | Тип | Host-тест | -|---|---|---|---| -| `elevator_model`, `sul` (декодеры), `controller`, `audio_policy`, layout-солвер | `firmware/tft_app/src/domain/*`, `.../ui/layout/*` | чистый C | **да** (golden-векторы) | -| `sul` транспорт-адаптеры | `firmware/tft_app/src/domain/sul/transport/*` | HW | HIL | -| `gfx`, `audio_engine`, `assets`, `settings_store`, `fs` | `firmware/tft_app/src/services/*` | HW/адаптеры | частично (парсеры/TLV — да) | -| `ui`, `menu`, `app` | `firmware/tft_app/src/{ui,menu,app}/*` | app | вид/меню — HIL | +| Слой | Где живёт | Тип | Host-тест | +| ------------------------------------------------------------------------------- | -------------------------------------------------- | ----------- | --------------------------- | +| `elevator_model`, `sul` (декодеры), `controller`, `audio_policy`, layout-солвер | `firmware/tft_app/src/domain/*`, `.../ui/layout/*` | чистый C | **да** (golden-векторы) | +| `sul` транспорт-адаптеры | `firmware/tft_app/src/domain/sul/transport/*` | HW | HIL | +| `gfx`, `audio_engine`, `assets`, `settings_store`, `fs` | `firmware/tft_app/src/services/*` | HW/адаптеры | частично (парсеры/TLV — да) | +| `ui`, `menu`, `app` | `firmware/tft_app/src/{ui,menu,app}/*` | app | вид/меню — HIL | Host-тесты — `tests/host/tft_app_*` (Unity + fff, компилируем `.c` домена против моков bsp, по образцу `tests/host/mcuboot_port`, `tests/host/protocol`). @@ -190,10 +191,10 @@ typedef struct { — 50pin с U/D·L/R). SDRAM, SEMC, периферия — одинаковы. Панели TFT7/8/10 уже разведены рантаймом в `bsp_display` (таблица `panel_config[]`: тайминги, клок, `has_orientation_pins`). -| Профиль сборки | LCDIF | Панель | Клок | -|---|---|---|---| -| `app-tft4` | 40pin, без ориентации | TFT4 (фикс) | Video PLL | -| `app-big` | 50pin, с U/D·L/R | 7 / 8 / 10 — **рантайм** из provisioning | PLL2 | +| Профиль сборки | LCDIF | Панель | Клок | +| -------------- | --------------------- | ---------------------------------------- | --------- | +| `app-tft4` | 40pin, без ориентации | TFT4 (фикс) | Video PLL | +| `app-big` | 50pin, с U/D·L/R | 7 / 8 / 10 — **рантайм** из provisioning | PLL2 | Различие изолировано в одном board-файле пин-мукса LCDIF. **Тип панели — provisioning-параметр** (пишется service_tui), не пользовательская настройка. Матрица сборки — два buildPreset @@ -201,14 +202,14 @@ typedef struct { ### 10. Карта QSPI (W25Q128, 16 МБ) -| Регион | Смещение | Размер | Владелец | -|---|---|---|---| -| bootloader | `0x000000` | 256 КБ | bootloader | -| slot A (tft_app) | `0x040000` | 2 МБ | MCUboot | -| slot Б (tft_app) | `0x240000` | 2 МБ | MCUboot | -| **layout-регион** | `0x440000` | 64 КБ | tft_app | -| **assets-регион** | `0x450000` | ≈ 11 МБ | tft_app | -| settings | `0xFFE000` | 4 КБ | tft_app | +| Регион | Смещение | Размер | Владелец | +| ----------------- | ---------- | ------- | ---------- | +| bootloader | `0x000000` | 256 КБ | bootloader | +| slot A (tft_app) | `0x040000` | 2 МБ | MCUboot | +| slot Б (tft_app) | `0x240000` | 2 МБ | MCUboot | +| **layout-регион** | `0x440000` | 64 КБ | tft_app | +| **assets-регион** | `0x450000` | ≈ 11 МБ | tft_app | +| settings | `0xFFE000` | 4 КБ | tft_app | layout- и assets-регионы — **вне** flash-area загрузчика (bootutil про них не знает). Разметка фиксируется в **едином partition-заголовке**, из которого читают app и генератор для service_tui. diff --git a/firmware/tft_app/CMakeLists.txt b/firmware/tft_app/CMakeLists.txt new file mode 100644 index 0000000..6c1a1de --- /dev/null +++ b/firmware/tft_app/CMakeLists.txt @@ -0,0 +1,122 @@ +# firmware/tft_app/CMakeLists.txt +# +# tft_app — прошивка лифтового индикатора. Собирается как MCUboot-слот-образ +# (Direct-XIP), подписывается imgtool (см. just build::sign-app). Архитектура — +# ARCH.md, план фаз — PLAN.md. +# +# Фаза 0 (каркас): FreeRTOS-мигалка + кормление WDOG + рантайм self-confirm +# слота + UART диагностика (LPUART1/MCU-Link VCOM — см. src/app/main.c про +# отказ от USB CDC под FreeRTOS для этой цели). + +if(BUILD_TESTS_HOST) + return() +endif() + +# ----------------------------------------------------------------------------- +# FreeRTOS ядро — первый RTOS-потребитель в репозитории. Собираем свой STATIC +# таргет из вендоренных исходников SDK (порт GCC/ARM_CM4F пригоден для cm7f, +# heap_4). Конфиг — src/app/FreeRTOSConfig.h. +# ----------------------------------------------------------------------------- +set(FREERTOS_DIR ${CMAKE_SOURCE_DIR}/sdk/rtos/freertos/freertos-kernel) + +add_library( + freertos_kernel STATIC + ${FREERTOS_DIR}/tasks.c + ${FREERTOS_DIR}/list.c + ${FREERTOS_DIR}/queue.c + ${FREERTOS_DIR}/timers.c + ${FREERTOS_DIR}/event_groups.c + ${FREERTOS_DIR}/stream_buffer.c + ${FREERTOS_DIR}/portable/GCC/ARM_CM4F/port.c + ${FREERTOS_DIR}/portable/MemMang/heap_4.c) + +target_include_directories( + freertos_kernel + PUBLIC ${FREERTOS_DIR}/include + ${FREERTOS_DIR}/portable/GCC/ARM_CM4F + ${CMAKE_CURRENT_SOURCE_DIR}/src/app) # FreeRTOSConfig.h + +# core_cm7.h, MIMXRT1052.h, SystemCoreClock +target_link_libraries(freertos_kernel PUBLIC sdk_device) + +# Вендоренное ядро — не наш стиль/warnings. +target_compile_options(freertos_kernel PRIVATE -w) + +# ----------------------------------------------------------------------------- +# app — исполняемый образ слота +# ----------------------------------------------------------------------------- +include(${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake) + +add_executable( + app + src/app/main.c + ${BSP_GENERATED}/clock_config.c # BOARD_BootClockRUN (зовётся board_hw_init) + ${BSP_STARTUP_FILE} + ${BSP_SYSCALLS_FILE} + # confirm_self(): flash_area_* + boot_set_next. Переиспользуем backend и + # bootutil_public (общий с bootloader/test_stub). flash_map_backend.c зовёт + # led_status_tick_install() — даём локальную no-op заглушку (boot_shims.c). + src/app/boot_shims.c + # bsp_tick, скомпилирован ПРЯМО в app (не через libbsp_tick.a) — режим задан + # ниже (BSP_TICK_FREERTOS_MODE), чтобы не тянуть SysTick_Handler, + # конфликтующий с FreeRTOS-портом. Приходит транзитивно через bsp_uart_host. + ${CMAKE_SOURCE_DIR}/bsp/tick/src/tick.c + ${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c + ${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c) + +target_include_directories( + app PRIVATE src/app ${MCUBOOT_BOOTUTIL_INCLUDES} + ${CMAKE_SOURCE_DIR}/firmware/bootloader/src) # led_status.h + +# __STARTUP_INITIALIZE_RAMFUNCTION — startup копирует .ram_function из flash в +# ITCM. БЕЗ него ramfunc-код bsp_qspi (AT_QUICKACCESS_SECTION_CODE) в ITCM не +# попадает → запись QSPI виснет (см. bsp/qspi_flash/README.md). Без USB CDC +# __STARTUP_INITIALIZE_NONCACHEDATA не нужен — ни один линкуемый модуль не +# кладёт данные в NonCacheable/NonCacheable.init (проверено). +target_compile_definitions( + app PRIVATE APP_OWN_SLOT_ID=0 __STARTUP_CLEAR_BSS __STARTUP_INITIALIZE_RAMFUNCTION) + +# Вендоренный bootutil_public.c — под своими warnings (как в bootloader/stub). +set_source_files_properties( + ${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c + PROPERTIES COMPILE_OPTIONS "${MCUBOOT_VENDORED_COMPILE_OPTIONS}") + +# FreeRTOS-режим bsp_tick: без него tick.c определяет SysTick_Handler +# (bare-metal), конфликтующий с обработчиком FreeRTOS-порта. Компилируется прямо +# в app (link-line раньше архивов) → bare-metal member из libbsp_tick.a не +# извлекается. +set_source_files_properties( + ${CMAKE_SOURCE_DIR}/bsp/tick/src/tick.c PROPERTIES COMPILE_DEFINITIONS + BSP_TICK_FREERTOS_MODE) + +target_link_libraries( + app + PRIVATE bsp_board + bsp_led + bsp_wdog + bsp_boot_state + bsp_qspi_flash + bsp_uart_host + bsp_boot_xip_no_dcd + freertos_kernel + port_log_uart) # UART диагностика (utils_log + bsp_uart_host, LPUART1/MCU-Link VCOM) + +target_link_options( + app + PRIVATE + -Wl,--gc-sections + -Wl,--print-memory-usage + -Wl,-Map=${CMAKE_BINARY_DIR}/app.map + -Wl,--defsym=__slot_base__=0x60040000 # Slot A; Slot Б — на фазе обновления + -Wl,--defsym=__stack_size__=0x1000 + -Wl,--defsym=__heap_size__=0x400 + -T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_app_slot.ld) + +set_target_properties(app PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +add_custom_command( + TARGET app + POST_BUILD + COMMAND ${CMAKE_OBJCOPY} -O binary $ ${CMAKE_BINARY_DIR}/app.bin + COMMAND ${CMAKE_SIZE} $ + COMMENT "Generating app.bin") diff --git a/firmware/tft_app/PLAN.md b/firmware/tft_app/PLAN.md index 9160480..400fa44 100644 --- a/firmware/tft_app/PLAN.md +++ b/firmware/tft_app/PLAN.md @@ -15,7 +15,7 @@ | # | Фаза | Статус | |---|---|---| -| 0 | Каркас проекта и сборка | ⬜ | +| 0 | Каркас проекта и сборка | ✅ | | 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ⬜ | | 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | ⬜ | | 3 | Настройки и меню, локальные входы (opto) | ⬜ | @@ -28,7 +28,62 @@ --- -## Фаза 0 — Каркас проекта и сборка ⬜ +## Фаза 0 — Каркас проекта и сборка ✅ + +**Итог.** Подтверждено на железе дважды (первый запуск + power cycle через +переустановку с SD): `image_ok` персистит через рестарт (`before-confirm` на +втором запуске читает `1`, записанный на первом), загрузчик не откатывает +образ. Закрывает оба исходных симптома отчёта: (1) SWD-прошивка работала один +раз, но не переживала перезапуск; (2) обновление через SD зацикливалось +(запись в слот A + рестарт). Причина обоих — один и тот же баг (ниже). + +**Root-cause (главная находка Фазы 0) — забытый дефайн `__STARTUP_ +INITIALIZE_RAMFUNCTION`.** Запись QSPI из `app` виснет в `qspi_write_fifo` +(FlexSPI не дренажит IP TX FIFO), хотя весь write-путь `bsp_qspi` помечен +`AT_QUICKACCESS_SECTION_CODE` (ramfunc). Причина: startup копирует секцию +`.ram_function` из flash в ITCM только под `#ifdef __STARTUP_ +INITIALIZE_RAMFUNCTION` — `app` его не задавал (только `__STARTUP_CLEAR_BSS`). +Оба рабочих потребителя `bsp_qspi` (firmware_test, bootloader) его задают. +Без него в ITCM оставался leftover ramfunc **загрузчика** (исполнялся первым) +→ чтения «случайно работали» на совместимом коде, запись — падала. +`bsp/qspi_flash/README.md` прямо предупреждает («без него ITCM содержит нули +→ HardFault»). Подтверждено изолированным QSPI self-test (erase+write+readback) +до возврата полной диагностики. + +**Прочие находки bring-up (для истории и будущих фаз — Фаза 3/settings и +Фаза 7/assets тоже пишут QSPI, наступят на смежное):** +- `confirm_self()`/любая `flash_area_*` требует `bsp_qspi_init()` заранее — + иначе запись молча падает (независимо от ramfunc-бага). +- Слот-линкер обязан иметь секцию `.ram_function` (ITCM) — `app_slot.ld` + переписан по образцу боотлоадерного (все RAM-секции). +- `bsp_usb_cdc_init()`/аналогичные bsp-вызовы, дёргающие `bsp_delay()`, нельзя + звать до `vTaskStartScheduler()` — в FreeRTOS-режиме `bsp_tick` это + `xTaskGetTickCount()`, который не идёт до старта планировщика → зависает + навсегда. Любая tick-зависимая bsp-инициализация — только из задачи. +- Первый потребитель `BSP_TICK_FREERTOS_MODE` в репозитории вскрыл 2 + латентных бага: (1) общий `bsp_tick` — bare-metal STATIC-либа, + `SysTick_Handler` конфликтует с FreeRTOS-портом — решение: компилировать + `tick.c` прямо в таргет с `BSP_TICK_FREERTOS_MODE`, не через `libbsp_tick.a`; + (2) `configTICK_RATE_HZ` нельзя как `((TickType_t)1000)` — `#if` в `tick.c` + не разбирает cast, только `1000`. +- **Диагностика — UART (LPUART1/MCU-Link VCOM), не USB CDC.** CDC под FreeRTOS + (первый потребитель target-side USB device stack в RTOS-контексте) не + заработал стабильно за несколько заходов на этом пути. UART — самый + проверенный канал в репозитории (все `tests/target/*` HIL-образы), доступен + сразу через уже подключённый для SWD кабель, без enumeration/wait. Если + понадобится CDC под FreeRTOS позже (Фаза 7: provisioning-канал) — отдельная + сфокусированная задача. `port_log_cdc` (переиспользуемый адаптер) оставлен + в дереве, из `app` сейчас не линкуется. +- `.vscode/launch.json`: у `🐛 Debug: tft_app` был `preLaunchTask` на + `firmware_test` (copy-paste) — исправлено на `build:app-debug`. + +**Осознанное отклонение от исходного критерия выхода:** `firmware/bootloader/ +test_stub` НЕ удалён (план ниже это предполагал) — оставлен как независимая +A/B-регрессия загрузчика, раз `app` пока не покрывает те же сценарии +(hang-классы, revert без self-confirm и т.д.). Переоценить, когда/если `app` +сможет их replicate. + +--- **Цель.** `firmware/tft_app` собирается как ARM-таргет, грузится через bootloader, мигает heartbeat; host-тест-харнесс на месте. Пустой, но живой каркас всех слоёв. @@ -45,7 +100,7 @@ heartbeat; host-тест-харнесс на месте. Пустой, но жи **Тесты.** `just build::build-app-big-debug` собирает ELF; host smoke-тест зелёный. **Критерий выхода.** ELF грузится bootloader'ом из слота, heartbeat мигает (проверка — пользователь -на железе). Удалён `test_stub` из корневого CMake. +на железе). ✅ Выполнено; `test_stub` сохранён (см. отклонение выше). --- diff --git a/firmware/tft_app/README.md b/firmware/tft_app/README.md new file mode 100644 index 0000000..2cd3251 --- /dev/null +++ b/firmware/tft_app/README.md @@ -0,0 +1,65 @@ +# tft_app + +Прошивка лифтового индикатора (MIMXRT1052, TFT 4/7/8/10). Собирается как +MCUboot-слот-образ (Direct-XIP), которым управляет [bootloader](../bootloader/). + +- **Архитектура** — [ARCH.md](ARCH.md). +- **План разработки (статус фаз)** — [PLAN.md](PLAN.md). + +> `OLD_PROJECT*` и `special/` — референсные дампы старых серийных проектов +> (в `.gitignore`), источник бизнес-логики и доменного дизайна. Не собираются. + +--- + +## Статус: Фаза 0 (каркас) + +Минимальный живой каркас: FreeRTOS + heartbeat + кормление унаследованного от +загрузчика WDOG + само-подтверждение слота MCUboot. Доменные/презентационные +слои — со следующих фаз. + +## Сборка и подпись + +```bash +# devcontainer: +just build::build-app-debug # app.elf/.bin (Slot A, XIP из слота) +just build::sign-app-debug # + imgtool EC256 → build/Debug/signed/app_slot_a.bin +just build::test-host # host-тесты (вкл. test_tft_app_smoke) зелёные +``` + +Подпись — imgtool ECDSA P-256 (как stub загрузчика), **без** `--confirm`: образ +подтверждает свой слот в рантайме (`boot_set_next`). HAB для app не нужен — его +грузит не BootROM, а загрузчик после проверки imgtool-подписи. + +## Прошивка (хост) + +```bash +# итеративно — SWD подписанного слот-образа в Slot A (0x60040000), power cycle после: +just host::flash-swd-app-slot-debug +``` + +## Диагностика (Фаза 0) + +USB CDC ACM (VCOM), 115200. Логирует boot-последовательность и **периодически +(раз в 2 с) состояние трейлера слота** (magic/copy_done/image_ok) — видно +независимо от момента подключения терминала: + +```bash +screen /dev/cu.usbmodemXXXX 115200 # macOS; порт свой на каждое подключение +``` + +Транспорт — `port_log_cdc` (переиспользуемый адаптер `utils/log` над +`bsp_usb_cdc`, симметричный `port_log_uart`), best-effort (строки без хоста +молча теряются). + +## Раскладка + +```bash +src/ +├── app/ точка входа, FreeRTOS-задачи, wiring, FreeRTOSConfig.h +├── domain/ чистый C, host-тесты: elevator_model, sul (декодеры), controller +├── services/ адаптеры к железу: gfx, audio_engine, assets, settings, fs +├── ui/ layout-движок, темы, fallback-рендер +└── menu/ экран настроек, навигация двумя кнопками +``` + +Слот-линкер — [cmake/linker/MIMXRT1052xxxxx_app_slot.ld](../../cmake/linker/MIMXRT1052xxxxx_app_slot.ld). diff --git a/firmware/tft_app/src/app/FreeRTOSConfig.h b/firmware/tft_app/src/app/FreeRTOSConfig.h new file mode 100644 index 0000000..3fe9c34 --- /dev/null +++ b/firmware/tft_app/src/app/FreeRTOSConfig.h @@ -0,0 +1,122 @@ +/* + * FreeRTOSConfig.h — конфигурация ядра для tft_app (MIMXRT1052, Cortex-M7F). + * + * Порт: GCC/ARM_CM4F (используется и для cm7f, см. + * sdk/rtos/freertos/freertos-kernel/CMakeLists.txt). Схема кучи — heap_4. + * + * Фаза 0: configTOTAL_HEAP_SIZE держим скромным во внутренней RAM (heap_4 + * размещает свой массив в .bss → DTCM). На фазе дисплея куча (и фреймбуферы) + * переедут в SDRAM — тогда пересмотреть размер и размещение. + */ + +#ifndef FREERTOS_CONFIG_H +#define FREERTOS_CONFIG_H + +#if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__) +#include +extern uint32_t SystemCoreClock; /* задаётся менеджером тактирования */ +#endif + +/* ── Планировщик ─────────────────────────────────────────────────────────── */ +#define configUSE_PREEMPTION 1 +#define configUSE_TICKLESS_IDLE 0 +#define configCPU_CLOCK_HZ (SystemCoreClock) +/* Без cast — bsp/tick.c (BSP_TICK_FREERTOS_MODE) сравнивает это значение в + * `#if configTICK_RATE_HZ != 1000`; препроцессор не умеет разбирать cast + * внутри #if ("missing binary operator"). Для C-кода бит-в-бит то же самое — + * TickType_t получается неявным преобразованием на месте использования. */ +#define configTICK_RATE_HZ 1000 +#define configMAX_PRIORITIES 8 +#define configMINIMAL_STACK_SIZE ((unsigned short)128) +#define configMAX_TASK_NAME_LEN 20 +#define configUSE_16_BIT_TICKS 0 +#define configIDLE_SHOULD_YIELD 1 +#define configUSE_TASK_NOTIFICATIONS 1 +#define configUSE_MUTEXES 1 +#define configUSE_RECURSIVE_MUTEXES 1 +#define configUSE_COUNTING_SEMAPHORES 1 +#define configQUEUE_REGISTRY_SIZE 8 +#define configUSE_QUEUE_SETS 0 +#define configUSE_TIME_SLICING 1 +#define configUSE_NEWLIB_REENTRANT 0 +#define configENABLE_BACKWARD_COMPATIBILITY 0 +#define configSTACK_DEPTH_TYPE uint32_t + +/* ── Память ──────────────────────────────────────────────────────────────── */ +#define configFRTOS_MEMORY_SCHEME 4 /* heap_4.c */ +#define configSUPPORT_STATIC_ALLOCATION 0 +#define configSUPPORT_DYNAMIC_ALLOCATION 1 +#define configTOTAL_HEAP_SIZE ((size_t)(0x8000)) /* 32 КБ (Фаза 0, DTCM) */ +#define configAPPLICATION_ALLOCATED_HEAP 0 + +/* ── Hooks (строгая диагностика на этапе разработки) ─────────────────────── */ +#define configUSE_IDLE_HOOK 0 +#define configUSE_TICK_HOOK 0 +#define configCHECK_FOR_STACK_OVERFLOW 2 /* → vApplicationStackOverflowHook */ +#define configUSE_MALLOC_FAILED_HOOK 1 /* → vApplicationMallocFailedHook */ +#define configUSE_DAEMON_TASK_STARTUP_HOOK 0 + +/* ── Статистика/трассировка ──────────────────────────────────────────────── */ +#define configGENERATE_RUN_TIME_STATS 0 +#define configUSE_TRACE_FACILITY 1 +#define configUSE_STATS_FORMATTING_FUNCTIONS 0 + +/* ── Со-рутины (не используем) ───────────────────────────────────────────── */ +#define configUSE_CO_ROUTINES 0 +#define configMAX_CO_ROUTINE_PRIORITIES 2 + +/* ── Программные таймеры ─────────────────────────────────────────────────── */ +#define configUSE_TIMERS 1 +#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1) +#define configTIMER_QUEUE_LENGTH 10 +#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 2) + +/* ── Ловим ошибки на разработке ──────────────────────────────────────────── */ +#define configASSERT(x) \ + if ((x) == 0) \ + { \ + taskDISABLE_INTERRUPTS(); \ + for (;;) \ + { \ + } \ + } + +/* ── Опциональные API ────────────────────────────────────────────────────── */ +#define INCLUDE_vTaskPrioritySet 1 +#define INCLUDE_uxTaskPriorityGet 1 +#define INCLUDE_vTaskDelete 1 +#define INCLUDE_vTaskSuspend 1 +#define INCLUDE_xResumeFromISR 1 +#define INCLUDE_vTaskDelayUntil 1 +#define INCLUDE_vTaskDelay 1 +#define INCLUDE_xTaskGetSchedulerState 1 +#define INCLUDE_xTaskGetCurrentTaskHandle 1 +#define INCLUDE_uxTaskGetStackHighWaterMark 1 +#define INCLUDE_xTaskGetIdleTaskHandle 0 +#define INCLUDE_eTaskGetState 0 +#define INCLUDE_xTimerPendFunctionCall 1 +#define INCLUDE_xTaskAbortDelay 0 +#define INCLUDE_xTaskGetHandle 0 +#define INCLUDE_xTaskResumeFromISR 1 + +/* ── Прерывания Cortex-M (RT1052 NVIC = 4 бита приоритета) ───────────────── */ +#ifdef __NVIC_PRIO_BITS +#define configPRIO_BITS __NVIC_PRIO_BITS +#else +#define configPRIO_BITS 4 +#endif + +#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY ((1U << configPRIO_BITS) - 1) +#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 2 + +#define configKERNEL_INTERRUPT_PRIORITY \ + (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS)) +#define configMAX_SYSCALL_INTERRUPT_PRIORITY \ + (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS)) + +/* ── Маппинг обработчиков порта на CMSIS-имена (vector table) ────────────── */ +#define vPortSVCHandler SVC_Handler +#define xPortPendSVHandler PendSV_Handler +#define xPortSysTickHandler SysTick_Handler + +#endif /* FREERTOS_CONFIG_H */ diff --git a/firmware/tft_app/src/app/boot_shims.c b/firmware/tft_app/src/app/boot_shims.c new file mode 100644 index 0000000..30ae795 --- /dev/null +++ b/firmware/tft_app/src/app/boot_shims.c @@ -0,0 +1,15 @@ +/* + * boot_shims.c — заглушки символов, которые дёргает общий с bootloader + * flash_map_backend.c, но которые не имеют смысла в контексте app. + * + * flash_map_backend.c зовёт led_status_tick_install() из цикла поблочного + * erase, чтобы двигать LED-анимацию прогресса в bootloader. В app окно + * установки не открыто (LED-индикацией управляет прикладная логика), поэтому + * здесь это no-op — ровно как у test_stub, но без утаскивания led_status.c + * (и его зависимости от bare-metal bsp_tick, конфликтующего с FreeRTOS-SysTick). + */ + +void led_status_tick_install(void) +{ + /* no-op: у app нет окна LED-установки */ +} diff --git a/firmware/tft_app/src/app/main.c b/firmware/tft_app/src/app/main.c new file mode 100644 index 0000000..12609e3 --- /dev/null +++ b/firmware/tft_app/src/app/main.c @@ -0,0 +1,206 @@ +/** + * @file main.c + * @brief tft_app — точка входа. Фаза 0 (каркас): FreeRTOS + heartbeat + WDOG + + * рантайм self-confirm слота MCUboot + UART диагностика. + * + * Каркас walking-skeleton: образ линкуется как валидный MCUboot-слот + * (Direct-XIP), bootloader в него прыгает, FreeRTOS стартует, LED мигает, + * WDOG кормится, образ ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной + * загрузке — anti-brick сохранён). Бизнес-логика — со следующих фаз (PLAN.md). + * + * Контракт со стороны bootloader (см. firmware/bootloader/test_stub/main.c): + * - WDOG взведён загрузчиком, WDE write-once — образ ОБЯЗАН кормить его. + * - Образ подписан imgtool БЕЗ --confirm → обязан подтвердить себя рантаймом + * (boot_set_next на своём слоте), иначе MCUboot откатит его. + * + * Записью QSPI из app под Direct-XIP можно пользоваться только потому, что + * ramfunc-код bsp_qspi реально копируется в ITCM — для этого потребитель + * ОБЯЗАН задать __STARTUP_INITIALIZE_RAMFUNCTION (см. CMakeLists и + * bsp/qspi_flash/README.md). Без него запись виснет в qspi_write_fifo. + * + * ── Диагностика: UART (LPUART1 / MCU-Link VCOM), НЕ USB CDC ────────────── + * USB CDC (target-side EHCI/PHY) под FreeRTOS не заработал стабильно — + * несколько заходов, последний раз плата падала в recovery загрузчика сразу + * после добавления wait-for-host цикла (вероятная причина — не сама USB, а + * инкрементально узкий стек boot_task, но целый класс неопределённости + * "первый в репозитории FreeRTOS-потребитель USB device stack" остаётся). + * LPUART1 — самый проверенный канал в репозитории (используется во ВСЕХ + * tests/target/* HIL-образах), идёт через уже подключённый для SWD кабель + * MCU-Link, не требует enumeration/wait, пины уже смуксены в BOARD_InitPins(). + * Если понадобится USB CDC под FreeRTOS (например, Фаза 7 — provisioning) — + * отдельная сфокусированная задача, не диагностика Фазы 0. + * + * main() — минимальный bare-metal (board bring-up + запуск планировщика). Всё + * остальное — в boot_task, после старта планировщика (унификация с QSPI/ + * confirm-логикой; UART сам по себе не требует тика для init/write). + */ + +#include "FreeRTOS.h" +#include "board.h" +#include "bootutil/bootutil_public.h" +#include "bsp/boot_state.h" +#include "bsp/led.h" +#include "bsp/qspi_flash.h" +#include "bsp/uart_host.h" +#include "bsp/wdog.h" +#include "flash_map.h" +#include "log/log.h" +#include "port/log_uart.h" +#include "task.h" + +#include + +/* Фаза 0: диагностика self-confirm по UART (LPUART1 / MCU-Link VCOM, + * 115200). Сократить/убрать, когда подтверждено на железе многократно. */ +#define LOG_TAG "app" + +/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */ +#ifndef APP_OWN_SLOT_ID +#define APP_OWN_SLOT_ID 0 +#endif + +#define HEARTBEAT_PERIOD_MS 500U +#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */ +#define STATUS_LOG_PERIOD_MS 2000U /* периодический re-log трейлера — виден независимо + * от момента подключения терминала (UART без + * handshake — писать можно сразу, слушают или нет) */ + +/* ── Диагностика трейлера слота (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) */ +} + +static void boot_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 phase0 boot: qspi=%s", QSPI_OK ? "OK" : "FAIL"); + + /* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки + * (recovery загрузчика). SRC GPR, без flash. */ + bsp_boot_health_mark(); + + if (QSPI_OK) + { + confirm_self(); + } + else + { + LOG_E(LOG_TAG, "qspi_init FAILED — self-confirm skipped, slot will revert"); + } + + 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(); + + 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"); + } + + vTaskDelayUntil(&last_wake, FEED_PERIOD); + } +} + +int main(void) +{ + board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */ + bsp_led_init(); + + /* x4 (2 КБ) с запасом: логирование (vsnprintf) + чтение трейлера. */ + (void) xTaskCreate(boot_task, "boot", configMINIMAL_STACK_SIZE * 4U, NULL, + tskIDLE_PRIORITY + 1U, NULL); + + vTaskStartScheduler(); + + /* Сюда планировщик не возвращается. Если вернулся — не хватило кучи под + * idle/timer задачу. WDOG сбросит плату. */ + for (;;) + { + } +} + +/* ── FreeRTOS hooks (строгая диагностика) ────────────────────────────────── */ + +void vApplicationStackOverflowHook(TaskHandle_t task, char *name) +{ + (void) task; + (void) name; + taskDISABLE_INTERRUPTS(); + for (;;) + { + /* WDOG сбросит плату — детерминированный отказ вместо тихой порчи. */ + } +} + +void vApplicationMallocFailedHook(void) +{ + taskDISABLE_INTERRUPTS(); + for (;;) + { + } +} diff --git a/firmware/tft_app/src/domain/README.md b/firmware/tft_app/src/domain/README.md new file mode 100644 index 0000000..c62138f --- /dev/null +++ b/firmware/tft_app/src/domain/README.md @@ -0,0 +1,3 @@ +# domain + +Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md. diff --git a/firmware/tft_app/src/menu/README.md b/firmware/tft_app/src/menu/README.md new file mode 100644 index 0000000..65cd4a1 --- /dev/null +++ b/firmware/tft_app/src/menu/README.md @@ -0,0 +1,3 @@ +# menu + +Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md. diff --git a/firmware/tft_app/src/services/README.md b/firmware/tft_app/src/services/README.md new file mode 100644 index 0000000..a46b7c8 --- /dev/null +++ b/firmware/tft_app/src/services/README.md @@ -0,0 +1,3 @@ +# services + +Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md. diff --git a/firmware/tft_app/src/ui/README.md b/firmware/tft_app/src/ui/README.md new file mode 100644 index 0000000..7950ce6 --- /dev/null +++ b/firmware/tft_app/src/ui/README.md @@ -0,0 +1,3 @@ +# ui + +Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md. diff --git a/just/build.just b/just/build.just index 0707da0..62c369a 100644 --- a/just/build.just +++ b/just/build.just @@ -66,6 +66,39 @@ build-app-debug: _configure-debug build-app-release: _configure-release cmake --build --preset app-release +# ── подпись app (imgtool EC256, как stub; БЕЗ --confirm — self-confirm в рантайме) ── +# Загрузчик валидирует подпись слота (MCUBOOT_VALIDATE_PRIMARY_SLOT + +# MCUBOOT_SIGN_EC256), поэтому образ ОБЯЗАН быть подписан тестовым ключом +# root-ec-p256.pem. Прошивка результата — через SWD в слот A (0x60040000). +# HAB-контейнер (hab-app-*/SDP-путь) на Фазе 0 не используется, см. PLAN.md. +# +# БЕЗ --confirm: image_ok в трейлере НЕ выставлен на этапе подписи — образ +# подтверждает себя рантаймом (boot_set_next в confirm_self, src/app/main.c). +# Так сохраняется anti-brick revert: образ, который загрузился, но не смог +# подтвердиться (завис до confirm), MCUboot откатит на следующей загрузке. + +[doc('Собрать и подписать tft_app для Slot A (Debug)')] +[group('build')] +sign-app-debug: build-app-debug + #!/usr/bin/env bash + set -euo pipefail + mkdir -p "{{ BUILD_DIR }}/Debug/signed" + {{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \ + -k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 0.1.0 --align 1 --pad-header --pad \ + "{{ BUILD_DIR }}/Debug/app.bin" "{{ BUILD_DIR }}/Debug/signed/app_slot_a.bin" + echo " ✅ build/Debug/signed/app_slot_a.bin (Slot A 0x60040000) — прошивка через just host::flash-swd-app-slot-debug" + +[doc('Собрать и подписать tft_app для Slot A (Release)')] +[group('build')] +sign-app-release: build-app-release + #!/usr/bin/env bash + set -euo pipefail + mkdir -p "{{ BUILD_DIR }}/Release/signed" + {{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \ + -k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 0.1.0 --align 1 --pad-header --pad \ + "{{ BUILD_DIR }}/Release/app.bin" "{{ BUILD_DIR }}/Release/signed/app_slot_a.bin" + echo " ✅ build/Release/signed/app_slot_a.bin (Slot A 0x60040000)" + # ── все проекты сразу ───────────────────────────────────────────────────────── [doc('Собрать все проекты (Debug)')] diff --git a/just/host.just b/just/host.just index 2e851ca..5675093 100644 --- a/just/host.just +++ b/just/host.just @@ -302,6 +302,30 @@ flash-swd-app-release: uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \ --firmware app --build-type Release +# ── слот-образ (подписанный imgtool, без HAB) — пишем сырой .bin по адресу ── +# слота A напрямую через pyOCD (не через flash_swd.py: тот собирает FCB+IVT+HAB +# под 0x60000000; слот — это данные, которые читает boot_go(), не boot-образ +# BootROM). Тот же приём, что для stub'ов загрузчика. Требует собранного и +# подписанного образа: just build::sign-app-. + +[doc('Прошить подписанный tft_app в Slot A (0x60040000) через SWD (Debug). Power cycle после')] +[group('flash-swd')] +flash-swd-app-slot-debug: + uv run --directory "{{ HIL_DIR }}" pyocd flash \ + --target {{ env('PYOCD_TARGET', 'mimxrt1050_quadspi') }} \ + --frequency {{ env('PYOCD_FREQUENCY', '4000000') }} \ + --base-address 0x60040000 --erase sector \ + "{{ BUILD_DIR }}/Debug/signed/app_slot_a.bin" + +[doc('Прошить подписанный tft_app в Slot A (0x60040000) через SWD (Release). Power cycle после')] +[group('flash-swd')] +flash-swd-app-slot-release: + uv run --directory "{{ HIL_DIR }}" pyocd flash \ + --target {{ env('PYOCD_TARGET', 'mimxrt1050_quadspi') }} \ + --frequency {{ env('PYOCD_FREQUENCY', '4000000') }} \ + --base-address 0x60040000 --erase sector \ + "{{ BUILD_DIR }}/Release/signed/app_slot_a.bin" + [confirm("Стереть всю Flash (W25Q128)? Все прошивки будут удалены.")] [doc('Стереть всю Flash (W25Q128) через SWD (chip erase). Нужен power cycle после')] [group('flash-swd')] diff --git a/port/log/CMakeLists.txt b/port/log/CMakeLists.txt index 03b141e..b215680 100644 --- a/port/log/CMakeLists.txt +++ b/port/log/CMakeLists.txt @@ -22,3 +22,25 @@ target_link_libraries( # Явный include на случай если utils обрабатывается после port в корневом CMake. # Дублирует транзитивный путь из utils — безвредно. target_include_directories(port_log_uart PUBLIC ${CMAKE_SOURCE_DIR}/utils) + +# ============================================================================= +# port_log_cdc — USB CDC ACM адаптер для utils_log. +# +# Связывает utils_log с bsp_usb_cdc: - регистрирует bsp_usb_cdc_write() как +# write callback (best-effort — см. port/log_cdc.h) - предоставляет +# strong-реализацию log_get_timestamp_ms() → bsp_tick_get_ms() +# +# ВАЖНО: bsp_usb_cdc_init() вызывает bsp_delay() внутри себя — под FreeRTOS +# звать ТОЛЬКО после vTaskStartScheduler(), из задачи (см. port/log_cdc.h). +# +# Не компилируется для host-тестов (bsp_usb_cdc там недоступен). +# ============================================================================= + +add_library(port_log_cdc STATIC src/log_cdc.c) + +target_include_directories(port_log_cdc PUBLIC include/) + +target_link_libraries( + port_log_cdc PUBLIC utils # несёт log.h и LOG_LEVEL транзитивно + bsp_usb_cdc bsp_tick) +target_include_directories(port_log_cdc PUBLIC ${CMAKE_SOURCE_DIR}/utils) diff --git a/port/log/include/port/log_cdc.h b/port/log/include/port/log_cdc.h new file mode 100644 index 0000000..ed4e71d --- /dev/null +++ b/port/log/include/port/log_cdc.h @@ -0,0 +1,46 @@ +/** + * @file port/log_cdc.h + * @brief USB CDC ACM адаптер логгера: регистрирует bsp_usb_cdc как транспорт. + * + * @warning Инициализация — ТОЛЬКО после vTaskStartScheduler() (из задачи + * FreeRTOS). bsp_usb_cdc_init() внутри себя вызывает bsp_delay(), которая в + * FreeRTOS-режиме bsp_tick реализована через xTaskGetTickCount() — до старта + * планировщика тик не идёт (счётчик стоит на 0), вызов до этого момента + * зависает навсегда. + * + * @code + * // внутри задачи, ПОСЛЕ vTaskStartScheduler(): + * bsp_usb_cdc_init(); + * log_cdc_init(); // регистрирует транспорт + timestamp + * @endcode + * + * Транспорт best-effort: пока хост не открыл VCOM (или предыдущая передача + * ещё не завершена), записи молча отбрасываются — см. bsp_usb_cdc_write(). + * Не подходит для сообщений, которые обязаны быть доставлены — для этого + * нужен ack-протокол поверх (как JSON-протокол bootloader'а). + */ + +#ifndef PORT_LOG_CDC_H +#define PORT_LOG_CDC_H + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Зарегистрировать USB CDC как транспорт логгера. + * + * Вызывает log_init() с cdc-callback и регистрирует + * log_get_timestamp_ms() → bsp_tick_get_ms(). + * + * Предусловие: bsp_usb_cdc_init() уже вызван — после старта планировщика, + * см. предупреждение в докстроке файла. + */ + void log_cdc_init(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PORT_LOG_CDC_H */ diff --git a/port/log/src/log_cdc.c b/port/log/src/log_cdc.c new file mode 100644 index 0000000..00aecd9 --- /dev/null +++ b/port/log/src/log_cdc.c @@ -0,0 +1,48 @@ +/** + * @file log_cdc.c + * @brief USB CDC ACM адаптер логгера. + * + * Подключает utils_log к bsp_usb_cdc: + * - write callback → bsp_usb_cdc_write() (best-effort, см. port/log_cdc.h) + * - timestamp hook → bsp_tick_get_ms() + */ + +#include "port/log_cdc.h" + +#include "bsp/tick.h" +#include "bsp/usb_cdc.h" +#include "log/log.h" + +/* -------------------------------------------------------------------------- */ +/* Strong-реализация weak-хука timestamp */ +/* -------------------------------------------------------------------------- */ + +uint32_t log_get_timestamp_ms(void) +{ + return bsp_tick_get_ms(); +} + +/* -------------------------------------------------------------------------- */ +/* Write callback */ +/* -------------------------------------------------------------------------- */ + +static void cdc_write(const char *p_buf, size_t len, void *p_ctx) +{ + (void) p_ctx; + if (!bsp_usb_cdc_is_ready()) + { + return; /* хост не подключён — молча отбрасываем (best-effort) */ + } + /* Не блокируемся на BSP_ERR_BUSY (предыдущая передача не завершена) — + * это диагностический канал, потерянная строка не критична. */ + (void) bsp_usb_cdc_write((const uint8_t *) p_buf, len); +} + +/* -------------------------------------------------------------------------- */ +/* Публичный API */ +/* -------------------------------------------------------------------------- */ + +void log_cdc_init(void) +{ + log_init(cdc_write, NULL); +} diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index 332a3a9..a5bcf64 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -314,3 +314,12 @@ if(CMAKE_C_COMPILER_ID MATCHES "Clang") ${PROJECT_SOURCE_DIR}/firmware/bootloader/src/slot_version.c PROPERTIES COMPILE_OPTIONS "-fno-sanitize=address,undefined") endif() + +# ----------------------------------------------------------------------------- +# tft_app — smoke (Фаза 0: проверка харнесса; доменные тесты — Фаза 1) +# ----------------------------------------------------------------------------- +add_host_test( + NAME + test_tft_app_smoke + SOURCES + tft_app_smoke/test_tft_app_smoke.c) diff --git a/tests/host/tft_app_smoke/README.md b/tests/host/tft_app_smoke/README.md new file mode 100644 index 0000000..ebafbc3 --- /dev/null +++ b/tests/host/tft_app_smoke/README.md @@ -0,0 +1,8 @@ +# test_tft_app_smoke + +Фаза 0: проверка работоспособности host-харнесса tft_app. + +На Фазе 0 в `firmware/tft_app` ещё нет чистой доменной логики — тест лишь +подтверждает, что таргет собирается и запускается. На Фазе 1 заменяется +реальными доменными тестами (декодер НКУ-CAN → `sul_result_t`, `controller` +diff). См. [firmware/tft_app/PLAN.md](../../../firmware/tft_app/PLAN.md). diff --git a/tests/host/tft_app_smoke/test_tft_app_smoke.c b/tests/host/tft_app_smoke/test_tft_app_smoke.c new file mode 100644 index 0000000..1306210 --- /dev/null +++ b/tests/host/tft_app_smoke/test_tft_app_smoke.c @@ -0,0 +1,26 @@ +/* + * test_tft_app_smoke.c — Фаза 0: проверка живости host-харнесса tft_app. + * + * На Фазе 0 в tft_app ещё нет чистой доменной логики. Этот тест лишь + * подтверждает, что таргет собирается clang'ом, линкуется с Unity и + * запускается под CTest в пресетах host-*. На Фазе 1 заменяется реальными + * доменными тестами (sul-декодер НКУ → sul_result_t, controller diff) — + * см. firmware/tft_app/PLAN.md. + */ + +#include "unity.h" + +void setUp(void) {} +void tearDown(void) {} + +static void test_harness_alive(void) +{ + TEST_ASSERT_EQUAL_INT(2, 1 + 1); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_harness_alive); + return UNITY_END(); +} diff --git a/tools/service_tui/assets/macos_icon.png b/tools/service_tui/assets/macos_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6e7f985155d57139a2af29a94212b93dcee6d872 GIT binary patch literal 9503 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