# tft_app: Phase 0

This commit is contained in:
Dmitry Akimov 2026-07-21 12:04:49 +03:00
parent e2e3f8aff8
commit a9e668196a
24 changed files with 1084 additions and 28 deletions

2
.vscode/launch.json vendored
View file

@ -57,7 +57,7 @@
"loadFiles": [],
"runToEntryPoint": "main",
"rtos": "FreeRTOS",
"preLaunchTask": "build:firmware-test-debug",
"preLaunchTask": "build:app-debug",
}
]
}

View file

@ -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()

View file

@ -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"
]
},
{

View file

@ -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")
}

View file

@ -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
@ -93,7 +94,7 @@ 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 — да) |
@ -191,7 +192,7 @@ typedef struct {
в `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 |
@ -202,7 +203,7 @@ typedef struct {
### 10. Карта QSPI (W25Q128, 16 МБ)
| Регион | Смещение | Размер | Владелец |
|---|---|---|---|
| ----------------- | ---------- | ------- | ---------- |
| bootloader | `0x000000` | 256 КБ | bootloader |
| slot A (tft_app) | `0x040000` | 2 МБ | MCUboot |
| slot Б (tft_app) | `0x240000` | 2 МБ | MCUboot |

View file

@ -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 $<TARGET_FILE:app> ${CMAKE_BINARY_DIR}/app.bin
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:app>
COMMENT "Generating app.bin")

View file

@ -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` сохранён (см. отклонение выше).
---

View file

@ -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).

View file

@ -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 <stdint.h>
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 */

View file

@ -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-установки */
}

View file

@ -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 <stdbool.h>
/* Фаза 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 (;;)
{
}
}

View file

@ -0,0 +1,3 @@
# domain
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.

View file

@ -0,0 +1,3 @@
# menu
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.

View file

@ -0,0 +1,3 @@
# services
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.

View file

@ -0,0 +1,3 @@
# ui
Слой tft_app. Наполняется по фазам — см. ../../PLAN.md и ../../ARCH.md.

View file

@ -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)')]

View file

@ -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-<debug|release>.
[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')]

View file

@ -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)

View file

@ -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 */

48
port/log/src/log_cdc.c Normal file
View file

@ -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);
}

View file

@ -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)

View file

@ -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).

View file

@ -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();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB