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 0000000..6e7f985 Binary files /dev/null and b/tools/service_tui/assets/macos_icon.png differ