diff --git a/.vscode/launch.json b/.vscode/launch.json index 6f98b20..5f2075b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -38,7 +38,7 @@ "interface": "swd", "loadFiles": [], "runToEntryPoint": "main", - "preLaunchTask": "build-and-rtt:firmware-test-debug", + "preLaunchTask": "build:bootloader-debug", }, // ============================================================= // firmware/tft_app — FreeRTOS task view diff --git a/CMakeLists.txt b/CMakeLists.txt index ac5c7e8..84c4c8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,14 +18,14 @@ if(NOT BUILD_TESTS_HOST) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) set(BSP_SYSCALLS_FILE - "${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c" - CACHE FILEPATH "Заглушки системных вызовов newlib") + "${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c" + CACHE FILEPATH "Заглушки системных вызовов newlib") set(BSP_GENERATED - "${CMAKE_SOURCE_DIR}/bsp/generated" - CACHE PATH "Путь до сгенерированных ConfigTools файлов") + "${CMAKE_SOURCE_DIR}/bsp/generated" + CACHE PATH "Путь до сгенерированных ConfigTools файлов") set(BSP_STARTUP_FILE - "${BSP_GENERATED}/startup/startup_MIMXRT1052.S" - CACHE FILEPATH "Путь до стартап файла") + "${BSP_GENERATED}/startup/startup_MIMXRT1052.S" + CACHE FILEPATH "Путь до стартап файла") endif() # ----------------------------------------------------------------------------- @@ -46,8 +46,7 @@ add_subdirectory(lib) # ----------------------------------------------------------------------------- if(NOT BUILD_TESTS_HOST) add_subdirectory(firmware/test) - # add_subdirectory(firmware/bootloader) - add_subdirectory(firmware/app) # - # Загрузчик + Основное приложение + add_subdirectory(firmware/bootloader) endif() # ----------------------------------------------------------------------------- diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index 9b8aca7..e0af941 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -66,3 +66,12 @@ target_compile_definitions( add_library(bsp_boot_ram INTERFACE) target_compile_definitions(bsp_boot_ram INTERFACE SKIP_SYSCLK_INIT) + +# bsp_boot_xip без DCD — для firmware/bootloader: XIP из Flash, но без +# инициализации SDRAM (bootloader SDRAM не использует, см. +# docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). В отличие от bsp_boot_xip не +# определяет XIP_BOOT_HEADER_DCD_ENABLE. +add_library(bsp_boot_xip_no_dcd INTERFACE) +target_compile_definitions( + bsp_boot_xip_no_dcd INTERFACE XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1 + SKIP_SYSCLK_INIT) diff --git a/cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld b/cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld new file mode 100644 index 0000000..d9de077 --- /dev/null +++ b/cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld @@ -0,0 +1,271 @@ +/* +** ################################################################### +** Processors: MIMXRT1052CVJ5B +** MIMXRT1052CVL5B +** MIMXRT1052DVJ6B +** MIMXRT1052DVL6B +** +** Compiler: GNU C Compiler +** Reference manual: IMXRT1050RM Rev.5, 07/2021 | IMXRT1050SRM Rev.2 +** +** Abstract: +** Linker file for firmware/bootloader. +** +** Вариант MIMXRT1052xxxxx_flexspi_nor.ld с m_text, ограниченным +** бюджетом bootloader из docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md — +** 256 KB от 0x60000000 (0x60000000..0x60040000). Slot A (tft_app) +** начинается на 0x60040000 сразу за границей m_text. Превышение +** бюджета — ошибка линковки (ASSERT ниже), а не тихий выход +** кода bootloader за пределы своей области. +** +** Copyright 2016 Freescale Semiconductor, Inc. +** Copyright 2016-2024 NXP +** SPDX-License-Identifier: BSD-3-Clause +** ################################################################### +*/ + +/* Entry Point */ +ENTRY(Reset_Handler) + +HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x2000; +STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x1000; +VECTOR_RAM_SIZE = DEFINED(__ram_vector_table__) ? 0x00000400 : 0; + +/* Specify the memory areas */ +MEMORY +{ + m_flash_config (RX) : ORIGIN = 0x60000000, LENGTH = 0x00001000 + m_ivt (RX) : ORIGIN = 0x60001000, LENGTH = 0x00001000 + m_interrupts (RX) : ORIGIN = 0x60002000, LENGTH = 0x00000400 + m_text (RX) : ORIGIN = 0x60002400, LENGTH = 0x0003DC00 /* до 0x60040000 — граница Slot A */ + m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128KB */ + m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128KB */ + m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256KB */ +} + +/* Define output sections */ +SECTIONS +{ + __NCACHE_REGION_START = ORIGIN(m_data2); + __NCACHE_REGION_SIZE = 0x2000; /* 8 KB non-cacheable for USB DMA */ + + .flash_config : + { + . = ALIGN(4); + __FLASH_BASE = .; + KEEP(* (.boot_hdr.conf)) /* flash config section */ + . = ALIGN(4); + } > m_flash_config + + ivt_begin = ORIGIN(m_flash_config) + LENGTH(m_flash_config); + + .ivt : AT(ivt_begin) + { + . = ALIGN(4); + KEEP(* (.boot_hdr.ivt)) /* ivt section */ + KEEP(* (.boot_hdr.boot_data)) /* boot section */ + KEEP(* (.boot_hdr.dcd_data)) /* dcd section (не используется bootloader — без DCD) */ + . = ALIGN(4); + } > m_ivt + + /* The startup code goes first into internal RAM */ + .interrupts : + { + __VECTOR_TABLE = .; + __Vectors = .; + . = ALIGN(4); + KEEP(*(.isr_vector)) /* Startup code */ + . = ALIGN(4); + } > m_interrupts + + /* The program code and other data goes into internal RAM */ + .text : + { + . = ALIGN(4); + *(.text) /* .text sections (code) */ + *(.text*) /* .text* sections (code) */ + *(.rodata) /* .rodata sections (constants, strings, etc.) */ + *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ + *(.glue_7) /* glue arm to thumb code */ + *(.glue_7t) /* glue thumb to arm code */ + *(.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__ = .; + /* gcc uses crtbegin.o to find the start of + the constructors, so we make sure it is + first. Because this is a wildcard, it + doesn't matter if the user does not + actually link against crtbegin.o; the + linker won't look for a file to match a + wildcard. The wildcard also means that it + doesn't matter which directory crtbegin.o + is in. */ + KEEP (*crtbegin.o(.ctors)) + KEEP (*crtbegin?.o(.ctors)) + /* We don't want to include the .ctor section from + from the crtend.o file until after the sorted ctors. + The .ctor section from the crtend file contains the + end of ctors marker and it must be last */ + 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 = .; /* define a global symbol at end of code */ + __DATA_ROM = .; /* Symbol is used by startup for data initialization */ + + .interrupts_ram : + { + . = ALIGN(4); + __VECTOR_RAM__ = .; + __interrupts_ram_start__ = .; /* Create a global symbol at data start */ + *(.m_interrupts_ram) /* This is a user defined section */ + . += VECTOR_RAM_SIZE; + . = ALIGN(4); + __interrupts_ram_end__ = .; /* Define a global symbol at data 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__ = .; /* create a global symbol at data start */ + *(.data) /* .data sections */ + *(.data*) /* .data* sections */ + *(DataQuickAccess) /* quick access data section */ + KEEP(*(.jcr*)) + . = ALIGN(4); + __data_end__ = .; /* define a global symbol at data end */ + } > m_data + + __ram_function_flash_start = __DATA_ROM + (__data_end__ - __data_start__); /* Symbol is used by startup for TCM data initialization */ + + .ram_function : AT(__ram_function_flash_start) + { + . = ALIGN(32); + __ram_function_start__ = .; + *(CodeQuickAccess) + . = 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 <= 0x60040000, "bootloader вышел за пределы бюджета 256 KB (граница Slot A, см. BOOTLOADER_FLASH_MAP.md)") + ASSERT((__noncachedata_end__ - ORIGIN(m_data2)) <= LENGTH(m_data2), "m_data2 ncache overflow") + + /* Uninitialized data section */ + .bss : + { + /* This is used by the startup in order to initialize the .bss section */ + . = 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 = .; /* Add for _sbrk */ + } > m_data + + .stack : + { + . = ALIGN(8); + . += STACK_SIZE; + } > m_data + + /* Initializes stack on the end of block */ + __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/docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md b/docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md new file mode 100644 index 0000000..6e98cea --- /dev/null +++ b/docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md @@ -0,0 +1,79 @@ +# Карта Flash — bootloader + tft_app (A/Б) + файловая система ассетов + +Единая карта QSPI NOR Flash (W25Q64/128/256/512, см. [bsp/qspi_flash](../../bsp/qspi_flash/README.md)) +для связки `firmware/bootloader` + `firmware/tft_app`. Дополняет +[BOOT_FLAGS.md](BOOT_FLAGS.md) (сценарии исполнения) и [HAB_GUIDE.md](HAB_GUIDE.md) (подпись). + +--- + +## 1. Принцип: один bootloader на любую ёмкость чипа + +`bsp_qspi_flash` определяет чип и его размер в рантайме (`bsp_qspi_init()` → JEDEC ID → +`bsp_qspi_flash_size()`), а не на этапе компиляции. На производстве возможен разброс чипов — +минимум W25Q128 (16 МБ), также встречается W25Q512 (64 МБ). + +Карта построена так, чтобы **один и тот же бинарник bootloader** работал на любой ёмкости: + +- **Bootloader, Slot A, Slot Б — фиксированные смещения и размеры**, одинаковые на всех платах + независимо от реальной ёмкости чипа. Именно это делает образ bootloader переносимым между платами + с разным флешем без пересборки. +- **Область файловой системы ассетов (спрайты/музыка) — не фиксирована.** Она занимает всё + пространство от конца Slot Б до фактического конца чипа, размер вычисляется в рантайме через + `bsp_qspi_flash_size()` при монтировании. На 16 МБ чипе это ~11.75 МБ, на 64 МБ — ~59.75 МБ. + **Формат и владелец этой области (что её монтирует, кто и как обновляет ассеты) — открытый вопрос, + вне рамок текущего плана bootloader'а.** Здесь фиксируется только адресный диапазон. + +`bootutil` (MCUboot, Direct-XIP) знает только про Slot A и Slot Б через `sysflash.h` — про область +ФС ему знать не нужно, коллизий с его логикой нет. + +--- + +## 2. Карта + +| Область | Смещение от `0x60000000` | Размер | Абсолютный адрес (начало) | +| ---------------- | ------------------------- | -------------------------------------- | --------------------------- | +| Bootloader | `0x000000` | 256 КБ (`0x040000`) | `0x60000000` | +| Slot A (tft_app) | `0x040000` | 2 МБ (`0x200000`) | `0x60040000` | +| Slot Б (tft_app) | `0x240000` | 2 МБ (`0x200000`) | `0x60240000` | +| ФС ассетов | `0x440000` | `bsp_qspi_flash_size() - 0x440000` | `0x60440000` | + +Все границы выровнены на 64 КБ блок (`BSP_QSPI_BLOCK_64K_SIZE`) — стирание region-ов через +`bsp_qspi_erase_block_64k()` без частичных секторов. + +Direct-XIP не использует scratch-область — метаданные состояния/версии образа хранятся в trailer +самого образа в каждом слоте (стандартный механизм bootutil), отдельного региона под них не нужно. + +--- + +## 3. Обоснование размеров + +**Bootloader — 256 КБ.** Без SDRAM/дисплея/FreeRTOS: bring-up, USB CDC, FatFS, bootutil (Direct-XIP, +без swap/scratch-машинерии), крипто-бэкенд (mbedTLS/tinycrypt), драйвер QSPI. Реальный размер ожидается +существенно меньше — заложен запас на будущий рост (например RSA вместо ECDSA, расширенная диагностика). + +**Slot A/Б — 2 МБ каждый.** Ассеты (спрайты, музыка) вынесены в отдельную область ФС и не входят в +подписанный образ — в слоте только код: FreeRTOS, логика индикатора, шрифты как вшитые C-массивы (8bpp +со сглаживанием). Ориентир: текущая production-прошивка (Debug-сборка, с шрифтами, FreeRTOS, FatFS) +занимает ~1 МБ — 2 МБ даёт двукратный запас. + +**Почему размеры не пересматриваются "по факту" после первой сборки.** Карта Flash — контракт, +зашитый в уже прошитые на производстве bootloader'ы (обновляются только через SWD/USB ROM, не в поле). +Смещения слотов фиксированы заранее с запасом, а не подгоняются под фактический размер первой сборки +tft_app. + +--- + +## 4. Открытые вопросы (не в рамках плана bootloader) + +- Формат и владелец области ФС ассетов (спрайты/музыка): FAT/LittleFS/кастомный, подписывается ли, + как обновляется (та же SD-логика, что и Slot A/Б, или отдельный механизм). +- Точный layout `.ld`-скрипта tft_app для двух адресов слотов (Direct-XIP: код обычно не + позиционно-независим — вероятно потребуется два варианта линковки под Slot A и Slot Б, либо PIC). + +--- + +## 5. Ссылки + +- [BOOT_FLAGS.md](BOOT_FLAGS.md) — XIP/DCD/сценарии исполнения кода. +- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись bootloader (HAB) vs подпись образов tft_app (`imgtool`). +- [bsp/qspi_flash/README.md](../../bsp/qspi_flash/README.md) — поддерживаемые чипы, `bsp_qspi_flash_size()`. diff --git a/firmware/bootloader/CMakeLists.txt b/firmware/bootloader/CMakeLists.txt new file mode 100644 index 0000000..168d88e --- /dev/null +++ b/firmware/bootloader/CMakeLists.txt @@ -0,0 +1,67 @@ +# firmware/bootloader/CMakeLists.txt Загрузчик — A/Б обновление tft_app через +# microSD (Direct-XIP). Обновляется только через USB ROM + blhost / SWD. +cmake_minimum_required(VERSION 3.20) +project( + bootloader + VERSION 0.1.0 + LANGUAGES C ASM) + +set(TARGET_NAME bootloader) + +# Генерация version.h из шаблона +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/generated/version.h" @ONLY) + +add_executable( + ${TARGET_NAME} + src/main.c + src/cli.c + src/protocol.c + ${BSP_GENERATED}/clock_config.c + ${BSP_STARTUP_FILE} + ${BSP_SYSCALLS_FILE}) + +target_include_directories(${TARGET_NAME} PRIVATE src/) + +target_include_directories(${TARGET_NAME} + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated") + +target_compile_definitions( + ${TARGET_NAME} PRIVATE __STARTUP_INITIALIZE_RAMFUNCTION __STARTUP_CLEAR_BSS + __STARTUP_INITIALIZE_NONCACHEDATA) + +# ----------------------------------------------------------------------------- +# Зависимости — минимум для Фазы 1 (bring-up + CDC). bsp_qspi_flash (доступ к +# слотам) и bsp_button (downgrade-override) добавятся в Фазах 2-3. +# ----------------------------------------------------------------------------- +target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick + bsp_usb_cdc bsp_boot_xip_no_dcd) + +# ----------------------------------------------------------------------------- +# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом +# bootloader (256 KB, см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). Без SDRAM +# (в отличие от firmware_test) — bootloader SDRAM не использует. +# ----------------------------------------------------------------------------- +target_link_options( + ${TARGET_NAME} + PRIVATE + -Wl,--gc-sections + -Wl,--print-memory-usage + -Wl,-Map=${CMAKE_BINARY_DIR}/bootloader.map + -Wl,--defsym=__stack_size__=0x1000 + -Wl,--defsym=__heap_size__=0x1000 + -T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld) + +set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY + ${CMAKE_BINARY_DIR}) + +# ----------------------------------------------------------------------------- +# Post-build: генерация .bin для прошивки через blhost +# ----------------------------------------------------------------------------- +add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND ${CMAKE_OBJCOPY} -O binary $ + ${CMAKE_BINARY_DIR}/bootloader.bin + COMMAND ${CMAKE_SIZE} $ + COMMENT "Generating bootloader.bin") diff --git a/firmware/bootloader/PLAN.md b/firmware/bootloader/PLAN.md new file mode 100644 index 0000000..0035c62 --- /dev/null +++ b/firmware/bootloader/PLAN.md @@ -0,0 +1,266 @@ +# Загрузчик (firmware/bootloader) — план разработки по фазам + +## Статус + +| Фаза | Статус | Примечание | +|---|---|---| +| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) | +| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже | +| 2 — bootutil (Direct-XIP) | не начата | | +| 3 — SD-путь установки | не начата | | +| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | | +| 5 — HAB Release + service-tui | не начата | | + +## Контекст + +`service-tui` и `firmware_test` уже выпущены и работают на производстве. Следующий шаг — +`firmware/bootloader/` (сейчас пустая директория, но сборочная инфраструктура под неё уже +существует: пресеты `bootloader-debug/release` в CMakePresets.json, рецепты `just build::*`, +`.vscode/launch.json`, HAB-конвейер) и `firmware/tft_app/`. Начинаем с загрузчика — он определяет +контракт (flash-layout, формат образа, версия), под который потом пишется tft_app. + +Архитектурные решения, принятые в обсуждении (не пересматриваются в рамках этого плана): + +- **Boot-стратегия**: и bootloader, и tft_app исполняются XIP из W25Q. Bootloader без ITCM-копирования, + без DCD — он не трогает SDRAM. tft_app сама поднимает SEMC в своём раннем startup (SDRAM — под её XIP, + см. отдельный будущий план на tft_app). +- **Схема обновления**: MCUboot **Direct-XIP**, два слота (A/Б) с полностью валидными образами каждый, + без swap/scratch. Единственный полевой канал обновления — microSD. USB как канал заливки *образа* + сознательно не делаем (SDP/blhost на производстве — это отдельный, не зависящий от кода bootloader, + канал через NXP BootROM). +- **Верификация образов tft_app** — через **bootutil** (MCUboot), не самодельный верификатор + (см. исследование ниже). Подпись — `imgtool` (не HAB; HAB — только для самого bootloader). +- **Обратная связь** — USB CDC ACM с тем же JSON-lines протоколом (`ping`/`pong`, `get_version`), что уже + использует `firmware_test` и `tools/service_tui/app/firmware_client.py` — чтобы переиспользовать + клиентский код на стороне service-tui. Плюс `LED_HEARTBEAT`/`LED_APP` (`bsp/led`) с кодированием + состояний через паттерн мигания. +- **Даунгрейд**: строгий version-gate по умолчанию; обход — удержание кнопки (`BSP_BUTTON_1`, + `bsp/button`) при старте с валидным (но более старым) подписанным образом на SD. +- **Два сценария производства** обслуживаются одним и тем же механизмом установки с SD + (bootloader-only → потом массовая SD-установка, или сразу залитый бандл) — единственное отличие: + top-level состояние "нет ни одного валидного слота" (ожидание SD, retry-цикл, статус на CDC/LED), + которого нет в обычной работе. +- **Smoke-test** (SDRAM + W25Q) — встроен в bootloader как опциональный, неблокирующий шаг: переиспользует + `bsp_qspi_init()`/`bsp_qspi_read_jedec_id()` (bsp/qspi_flash, и так нужен для доступа к слотам) и + `bsp_sdram_init()` (bsp/sdram) поверх новой общей функции подъёма SEMC, которую позже переиспользует + и tft_app. + +### Что нашли по MCUboot bootutil (исследование Explore-агента) + +В `sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/` уже есть готовый порт bootutil под NXP MCUX SDK +(`flashapi/flash_api.c` — 425 строк, ровно тот шим над flash-драйвером, который нужен и нам; +`include/{sysflash,flash_map_backend,mcuboot_config}/*.h`; `boot.c`/`boot.h` — pattern main-loop +загрузчика). В `sdk/boards/evkbimxrt1050/ota_examples/ota_mcuboot_basic/armgcc/` есть рабочий линкер-скрипт +`MIMXRT1052xxxxx_flexspi_nor_mcuboot.ld` — тот же SoC, что и у нас. `bootutil_priv.h` требует ровно один +из `MCUBOOT_OVERWRITE_ONLY`/`MCUBOOT_SWAP_USING_MOVE`/`MCUBOOT_DIRECT_XIP`/`MCUBOOT_RAM_LOAD` — включение +`MCUBOOT_DIRECT_XIP` чисто вырезает всю swap/scratch-машинерию через `#if`. Итоговая оценка порт-кода: +~500-1100 строк (flash-шим над `bsp_qspi_flash`, `sysflash.h` на 2 слота, `mcuboot_config.h`, +статический `malloc`/`free`-пул — bootutil дважды дергает malloc даже без RTOS). Крипто-бэкенд +(mbedTLS/tinycrypt) уже вендорен в `ext/`. + +--- + +## Фаза 0 — Карта Flash и место в системе сборки + +**Цель**: зафиксировать бинарный контракт, прежде чем писать код. + +- Определить смещения: `BOOTLOADER` (фикс. `0x60000000`, бюджет размера — по факту размера + bootloader.bin + запас), `SLOT_A`, `SLOT_B` (равный размер, с запасом под рост tft_app; ориентир — + `bsp_qspi_flash_size()` минус бюджет bootloader, поделить пополам). Direct-XIP не требует scratch-области + — метаданные версии/статуса живут в самом image trailer каждого слота (стандарт bootutil). + Зафиксировать в `docs/mimxrt1052/` рядом с существующими `BOOT_FLAGS.md`/`HAB_GUIDE.md` (новый файл + `BOOTLOADER_FLASH_MAP.md` или раздел в `DEV_ARCH.md`) — единый источник истины для линкер-скриптов, + `sysflash.h` и будущего tft_app. +- Зарегистрировать `firmware/bootloader` в корневом `CMakeLists.txt` (раскомментировать + `add_subdirectory(firmware/bootloader)` — сейчас закомментировано вместе с `firmware/app`, добавить + раздельно). + +**Верификация**: ревью карты памяти (нет пересечений bootloader/Slot A/Slot B; согласуется с реальной +ёмкостью W25Q чипа на плате). Код ещё не пишем — это чисто согласованный документ. + +--- + +## Фаза 1 — Скелет: собирается, грузится, живой (CDC + LED) + +**Цель**: минимальный bootloader, который проходит тот же путь bring-up, что и `firmware_test` +(`firmware/test/src/main.c`), но без тестового раннера. + +Новые файлы, по образцу `firmware/test/`: +- `firmware/bootloader/CMakeLists.txt` — target `bootloader`, линкер `MIMXRT1052xxxxx_flexspi_nor.ld` + (без SDRAM-варианта — bootloader её не использует), `target_link_libraries`: `bsp_board`, `bsp_led`, + `bsp_tick`, `bsp_button`, `bsp_usb_cdc`, `bsp_qspi_flash`, `bsp_boot_xip` (если применимо, как в + firmware_test) + generated/startup/syscalls как в `firmware/test/CMakeLists.txt`. +- `firmware/bootloader/src/main.c` — bring-up: `board_hw_init()` → `bsp_led_init()` → `bsp_tick_init()` + → `bsp_usb_cdc_init()` → мигание `LED_HEARTBEAT` до готовности CDC (копия паттерна из + `firmware/test/src/main.c:30-65`). +- `firmware/bootloader/src/cli.c` + `protocol.c`/`.h` — урезанное подмножество протокола + `firmware/test/src/protocol.h`: `ping`→`pong`, `get_version`→`version_response`, плюс новый + `status`-эвент для состояний из Фазы 3/4 (без `test_begin`/`test_result`/`confirm_request` — те + специфичны для firmware_test). Не шарить код с firmware_test напрямую (разные жизненные циклы + сообщений) — копировать и урезать, как это уже сделано для `firmware_test_fatfs` vs будущего + tft_app fatfs (см. комментарий в `firmware/test/fatfs/CMakeLists.txt:7-8`). +- HAB unsigned yaml для Debug (по образцу существующих конфигов firmware_test в `tools/host/hab/`). + +**Верификация**: +1. ✅ `just build::build-bootloader-debug` — собирается (на хосте с `ARMGCC_DIR` вместо devcontainer — + тоже работает, toolchain найден локально). +2. ✅ `just host::flash-swd-bootloader-debug` (готовый рецепт, уже существовал в `just/host.just`) — + прошивается, `LED_HEARTBEAT` мигает до подключения CDC, после — `LED_APP` включается. +3. ✅ Ping/get_version по USB CDC ACM — `ping`→`pong`, `get_version`→`"0.1.0"`. +4. ✅ `🐛 Debug: bootloader` в `.vscode/launch.json` — подключается, останавливается на `main`. + +**Отличия от исходного плана (по факту реализации):** +- Линкер-скрипт — не переиспользован общий `MIMXRT1052xxxxx_flexspi_nor.ld` (он нигде не использовался + и не ограничивал `m_text` бюджетом bootloader), а сделана копия + `cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld` с `m_text` жёстко ограниченным 247 КБ + (`ASSERT` на границу Slot A `0x60040000`, см. BOOTLOADER_FLASH_MAP.md) — превышение бюджета теперь + ошибка линковки, а не тихий выход за пределы своей области. Фактически занято: 32.5 КБ (12.85%). +- `bsp_boot_xip` не переиспользован как есть (он тянет `XIP_BOOT_HEADER_DCD_ENABLE=1`, что противоречит + решению "bootloader SDRAM не трогает"). Добавлен `bsp_boot_xip_no_dcd` в `bsp/CMakeLists.txt`, третий + вариант рядом с существующими `bsp_boot_xip`/`bsp_boot_ram`. +- `status`-эвент из плана Фазы 1 отложен в Фазу 3/4 (там появится реальный смысл — состояния SD-установки + и smoke-теста). Фаза 1 ограничена `ping`/`pong`/`get_version`/`version_response`/`error`, без + `session_start` (не нужен — service-tui проверяет живость явным `ping`, как и HIL-фикстура + `firmware_cdc` для firmware_test). +- Найден и исправлен баг scaffold'а: `🐛 Debug: bootloader` в `.vscode/launch.json` ссылался на + несуществующую задачу `build-and-rtt:firmware-test-debug` (copy-paste). Исправлено на + `build:bootloader-debug` (уже существовала в `tasks.json`). +- HAB Debug-конфиг (`tools/host/hab/hab_bootloader_debug.yaml`) уже существовал в исходном scaffold + репозитория и уже был без DCD — менять не пришлось. +- `bsp_qspi_flash` и `bsp_button` из Фазы 1 в CMakeLists.txt пока не добавлены — они не используются + до Фазы 2 (доступ к слотам) и Фазы 3 (downgrade-override), добавятся вместе с кодом, который их + реально вызывает. + +--- + +## Фаза 2 — bootutil: порт + выбор слота (host-тестируемая логика) + +**Цель**: интегрировать bootutil в режиме Direct-XIP, с флеш-шимом над `bsp_qspi_flash`. + +- `firmware/bootloader/mcuboot_port/` (новая директория): `flash_map_backend.c/.h` (шим + `flash_area_open/read/write/erase` → `bsp_qspi_read`/`bsp_qspi_write_page`/`bsp_qspi_erase_sector`, + по образцу `sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/flashapi/flash_api.c`), `sysflash.h` + (2 flash-area ID под Slot A/Б, без scratch), `mcuboot_config.h` (`MCUBOOT_DIRECT_XIP`, + `MCUBOOT_IMAGE_NUMBER=1`, выбор сигнатурной схемы — рекомендация: ECDSA P-256, компактные ключи и + быстрая проверка на Cortex-M7 без аппаратного крипто-ускорителя), статический malloc/free-пул (bootutil + дважды вызывает malloc в `loader.c` даже без RTOS). +- Подключить `sdk/middleware/mcuboot_opensource/boot/bootutil` как источник в CMake (аналогично тому, как + `sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk` подключает bootutil через свой `.cmake`-файл — + использовать `middleware_mcuboot_nxp_bootutil_port.cmake` как референс, не копировать вслепую). +- `firmware/bootloader/src/boot_select.c` — вызов `boot_go` (Direct-XIP путь), получение адреса entry + point выбранного слота. + +**Верификация — до всякого железа**: +- Host-юнит-тесты (`tests/host/`, Unity + fff, по образцу существующих `tests/host/protocol/`, + `tests/host/cli/`) с фейковым flash-буфером в памяти вместо `bsp_qspi_flash`: валидный образ в + Slot A только → выбран A; оба слота валидны, версия Б выше → выбран Б; повреждённый TLV/подпись в + одном слоте → игнорируется, выбран валидный; оба слота пустые/невалидные → `boot_go` возвращает + "нет образа" (это и есть триггер top-level состояния Фазы 3). +- Только после зелёных host-тестов — сборка `target-debug`/реальная плата с образом, подписанным + `imgtool` (см. `sdk/middleware/mcuboot_opensource/scripts/imgtool.py`), прошитым вручную через SWD в + Slot A — подтвердить, что bootloader реально прыгает в него (минимальный тестовый "app"-заглушка, + которая просто включает LED, чтобы визуально подтвердить прыжок). + +--- + +## Фаза 3 — SD-путь установки (единый для обоих производственных сценариев) + +**Цель**: сканирование microSD, установка образа в слот, top-level состояние "нет валидного образа". + +- `firmware/bootloader/fatfs/` — bare-metal FatFS-таргет по образцу `firmware/test/fatfs/` + (свой `ffconf.h`, не шарить `firmware_test_fatfs` — bootloader и firmware_test взаимоисключающие + прошивки на одной плате, зависимость от таргета с именем "firmware_test" в trust-anchor была бы + неверной связью; переиспользуется общий `port/fatfs` уровнем ниже). +- `firmware/bootloader/src/sd_update.c` — на старте и опционально периодически: смонтировать SD, + найти `TFT_APP.BIN` (imgtool-подписанный) в корне, распарсить заголовок (через bootutil), сравнить + версию с активным слотом: + - новее → стереть неактивный (или любой, если оба невалидны) слот, записать постранично через + `bsp_qspi_write_page`, вычитать обратно и сверить хэш перед тем как считать установку завершённой; + - старше/равно и кнопка `BSP_BUTTON_1` не удержана при старте → пропустить; + - старше и кнопка удержана → тот же путь установки, что и "новее" (подпись всё равно проверяется). +- Top-level состояние "нет валидного слота": цикл ожидания SD с периодическим статусом на CDC + (`status: waiting_for_sd`) и характерным LED-паттерном (Фаза 4 уточняет полный словарь паттернов) — + выхода из цикла нет, пока установка не пройдёт успешно. + +**Верификация (первая фаза, требующая реального железа)**: +1. Чистая плата (только bootloader, оба слота пустые) + SD с валидным подписанным образом → + автоустановка, переход к загрузке (проверить по CDC-статусам и LED). +2. То же SD с образом версии ниже уже установленной, кнопка не нажата → отклонён, лог на CDC. +3. То же, кнопка `BSP_BUTTON_1` удержана при старте → установлен. +4. SD с образом без подписи/битым TLV → отклонён, плата остаётся на последнем валидном слоте (если был) + или продолжает ждать (если не было), характерный error-паттерн LED. +5. Сценарий 2 (бандл залит сразу в Slot A через SWD/SDP на этапе тестирования): SD не участвует, + bootloader должен просто загрузить Slot A без обращения к SD-логике вообще. + +--- + +## Фаза 4 — SDRAM/W25Q smoke-test + словарь LED-паттернов + +**Цель**: диагностический, неблокирующий проход по SDRAM/W25Q + финальный набор статусов. + +- Новая функция подъёма SEMC (например `bsp_sdram_configure()` рядом с существующим `bsp_sdram_init()` + в `bsp/sdram/`) — переносит регистровую последовательность, которая сейчас зашита в DCD, в вызываемый + C-код. Bootloader вызывает её только на время smoke-теста; tft_app (в своём будущем плане) будет + вызывать её по-настоящему в раннем startup. +- Smoke-test в `main.c`: `bsp_qspi_init()` (уже обязателен для доступа к слотам — статус даром), + `bsp_sdram_configure()` + `bsp_sdram_init()` (опционально, по времени — не блокирует переход к + загрузке приложения; результат только репортится). +- Финальный словарь состояний на `LED_APP` (поверх уже реализованных в Фазах 1/3): + ожидание SD / установка / ошибка образа / smoke-test fail / переход в app (гаснет). +- `status`-события на CDC для каждого состояния — `tools/service_tui` сможет опрашивать их так же, как + сейчас `FirmwareClient.ping()`/`get_version()`. + +**Верификация**: прогон на заведомо исправной плате — все статусы (`waiting_for_sd`, `installing`, +`smoke_pass`/`smoke_fail`, `booting`) наблюдаются в правильном порядке через CDC и глазами по LED. +Полноценной аппаратной инъекции неисправностей не делаем (в проекте и для остальных HIL-тестов это не +принято) — ограничиваемся проверкой на реальной плате в штатном состоянии. + +--- + +## Фаза 5 — HAB, релизные пресеты, интеграция в service-tui + +**Цель**: замкнуть производственный цикл для сценария "bootloader-only, потом SD". + +- HAB Release yaml для bootloader (подписанный, `flags=0x08`) — по образцу существующих + `just/build.just:154-176` (`hab-bootloader-debug/release` уже определены, нужен только рабочий + `bootloader_hab.bin`). +- `tools/service_tui`: обобщить/расширить `FirmwareClient` (или добавить параллельный тонкий клиент, + переиспользующий его внутренности) так, чтобы после прошивки bootloader через SDP service-tui сразу + проверял `ping`→`pong` и `get_version` — это и есть запрошенная "обратная связь, что загрузчик жив" + для сценария 1 (плата отложена в кучу без tft_app). +- Проверить, что уже существующие `just host::flash-production` / `just host::production` + (`just/host.just:532-535`) корректно находят `bootloader_hab.bin` — сборка `app_hab.bin` пока + недоступна (tft_app не реализован), так что сквозной прогон "production" целиком верифицируется только + после отдельного плана на tft_app; в рамках этой фазы проверяем только bootloader-плечо. + +**Верификация**: чистая плата → `just host::flash-production`-путь для bootloader (или его bootloader-only +подмножество) → service-tui показывает "bootloader alive" на основе реального ping/version с платы. + +--- + +## Ключевые файлы для переиспользования (не изобретать заново) + +| Что нужно | Где смотреть образец | +|---|---| +| Bring-up последовательность | `firmware/test/src/main.c` | +| JSON-lines протокол (ping/pong/version) | `firmware/test/src/protocol.h`, `.c` | +| CLI-диспетчер команд | `firmware/test/src/cli.c` | +| CMake-паттерн firmware-таргета + HAB post-build | `firmware/test/CMakeLists.txt` | +| Bare-metal FatFS-таргет | `firmware/test/fatfs/CMakeLists.txt` | +| Flash R/W/erase/JEDEC | `bsp/qspi_flash/include/bsp/qspi_flash.h` | +| SDRAM verify (паттерн read/write) | `bsp/sdram/src/sdram.c` | +| LED heartbeat/app конвенция | `bsp/led/include/bsp/led.h` | +| Кнопки с дебаунсом | `bsp/button/include/bsp/button.h` | +| bootutil NXP-порт (референс для шима) | `sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/` | +| Линкер-скрипт mcuboot для RT1052 | `sdk/boards/evkbimxrt1050/ota_examples/ota_mcuboot_basic/armgcc/MIMXRT1052xxxxx_flexspi_nor_mcuboot.ld` | +| imgtool (подпись образов) | `sdk/middleware/mcuboot_opensource/scripts/imgtool.py` | +| CDC-клиент на стороне service-tui | `tools/service_tui/app/firmware_client.py` | +| Host-юнит-тесты (Unity+fff паттерн) | `tests/host/protocol/`, `tests/host/cli/` | + +## Общий принцип верификации по фазам + +Логика без железа (выбор слота, парсинг TLV, версия-компар) — **host-тесты** (`just build::test-host`), +используя уже принятую в проекте связку Unity+fff с фейковым flash-буфером — так же, как уже тестируются +`cli`/`protocol`/`prio_queue`. Всё, что требует реального железа (SD, запись во flash, SEMC, LED, +USB CDC) — только после того как логика зелёная на хосте, ручная проверка на плате по чек-листу каждой +фазы. Не начинать следующую фазу, пока не пройдена верификация текущей. \ No newline at end of file diff --git a/firmware/bootloader/README.md b/firmware/bootloader/README.md new file mode 100644 index 0000000..932f68c --- /dev/null +++ b/firmware/bootloader/README.md @@ -0,0 +1,168 @@ +# bootloader + +> Загрузчик MIMXRT1052 — A/Б обновление `tft_app` через microSD (MCUboot, +> Direct-XIP). Сам bootloader обновляется только через USB ROM (blhost) или +> SWD — не в поле. Единственный канал диагностики: USB CDC ACM (JSON-lines, +> тот же стиль протокола, что у `firmware_test`). +> +> Версия: `0.1.0` | Статус: Фаза 1 (скелет) завершена — см. [PLAN.md](PLAN.md) + +--- + +## Содержание + +- [Быстрый старт](#быстрый-старт) +- [Архитектура](#архитектура) +- [Карта Flash](#карта-flash) +- [Протокол](#протокол) +- [Roadmap](#roadmap) +- [Версионирование](#версионирование) + +--- + +## Быстрый старт + +### 1. Сборка + +```bash +just build::build-bootloader-debug +just build::hab-bootloader-debug +``` + +### 2. Прошивка + +Для итеративной разработки — SWD (не требует смены boot-режима платы): + +```bash +just host::flash-swd-bootloader-debug +# обязателен power cycle платы после прошивки +``` + +Через USB ROM (SDP, плата в режиме Serial Downloader): + +```bash +just host::flash bootloader debug +``` + +### 3. Подключение + +USB CDC ACM (тот же порядок, что у `firmware_test`): + +```bash +# macOS +screen /dev/cu.usbmodemXXXX +``` + +Проверка связи: + +```json +→ {"type":"cmd","cmd":"ping"} +← {"type":"pong"} + +→ {"type":"cmd","cmd":"get_version"} +← {"type":"version_response","fw":"0.1.0"} +``` + +### 4. Отладка + +VSCode → `🐛 Debug: bootloader` (`.vscode/launch.json`) — пересобирает через +`build:bootloader-debug`, подключается к GDB-серверу (`just host::debug-server` +должен быть запущен), останавливается на `main`. + +--- + +## Архитектура + +Bring-up идентичен `firmware_test` (`firmware/test/src/main.c`), но без +тестового раннера — bootloader не запускает тесты, только диагностический +CLI: + +```bash +firmware/bootloader/ +├── CMakeLists.txt +└── src/ + ├── main.c — board_hw_init → led/tick/usb_cdc init → + │ ожидание CDC (LED_HEARTBEAT мигает) → + │ LED_APP on → главный цикл (poll + cli_process) + │ + ├── version.h.in — шаблон версии (CMake → generated/version.h) + │ + ├── cli.h / cli.c — IO-слой: буферизация строк, парсинг "type"/"cmd" + │ (урезанное подмножество firmware_test/src/cli.c) + │ + └── protocol.h / .c — сериализация исходящих событий → cli_send() +``` + +Bootloader **без SDRAM** — DCD не используется (`bsp_boot_xip_no_dcd` вместо +`bsp_boot_xip`). `tft_app` инициализирует SEMC сама в своём раннем startup +(см. [BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md)). + +Линкер: `cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld` — `m_text` +жёстко ограничен 247 КБ, с `ASSERT` на границу Slot A. Превышение бюджета — +ошибка линковки, а не тихий выход кода за пределы своей области. + +--- + +## Карта Flash + +Полная карта, обоснование размеров и принцип "один bootloader на любую +ёмкость чипа" — в +[docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). + +| Область | Смещение | Размер | +|---|---|---| +| Bootloader | `0x60000000` | 256 КБ | +| Slot A (tft_app) | `0x60040000` | 2 МБ | +| Slot Б (tft_app) | `0x60240000` | 2 МБ | +| ФС ассетов (спрайты/музыка) | `0x60440000` | остальное (рантайм) | + +--- + +## Протокол + +Транспорт и фреймирование — как у `firmware_test` +([firmware/test/README.md](../test/README.md#протокол-v2)): USB CDC ACM, +JSON-lines, максимум 128 байт на строку. + +**Реализовано (Фаза 1):** + +| Команда хоста | Ответ | +|---|---| +| `{"type":"cmd","cmd":"ping"}` | `{"type":"pong"}` | +| `{"type":"cmd","cmd":"get_version"}` | `{"type":"version_response","fw":"X.Y.Z"}` | + +Ошибки: `{"ok":false,"error":"PARSE_ERR"}` / `"UNKNOWN_CMD"` / `"LINE_TOO_LONG"`. + +Без `session_start` — в отличие от `firmware_test`, bootloader не шлёт +приветствие автоматически; живость проверяется явным `ping` (тот же паттерн, +что использует HIL-фикстура `firmware_cdc` для `firmware_test`). + +**Появится в следующих фазах** (см. [PLAN.md](PLAN.md)): `status`-события +(`waiting_for_sd`, `installing`, `smoke_pass`/`smoke_fail`, `booting`) — +Фазы 3-4. + +--- + +## Roadmap + +Полный план по фазам с целями и критериями верификации — [PLAN.md](PLAN.md). + +| Фаза | Что делает | +|---|---| +| 0 ✅ | Карта Flash, регистрация в сборке | +| 1 ✅ | Скелет: bring-up, USB CDC, ping/get_version, LED heartbeat | +| 2 | bootutil (MCUboot Direct-XIP) — выбор слота, верификация подписи | +| 3 | Установка образа с microSD, состояние "нет валидного слота" | +| 4 | SDRAM/W25Q smoke-test, словарь LED-паттернов | +| 5 | HAB Release, интеграция в service-tui | + +--- + +## Версионирование + +Версия задаётся `project(bootloader VERSION X.Y.Z)` в `CMakeLists.txt`, +прокидывается через `configure_file(src/version.h.in → generated/version.h)` +в `BOOTLOADER_VERSION_STR` — тот же механизм, что у `firmware_test` +(см. [firmware/test/README.md#версионирование](../test/README.md#версионирование)). + +`version.h` генерируется, не редактируется вручную. diff --git a/firmware/bootloader/src/cli.c b/firmware/bootloader/src/cli.c new file mode 100644 index 0000000..c159d60 --- /dev/null +++ b/firmware/bootloader/src/cli.c @@ -0,0 +1,216 @@ +/** + * @file cli.c + * @brief IO-слой и диспатчер сообщений для bootloader. + * + * Транспорт: USB CDC ACM через bsp_usb_cdc. + * + * Парсинг минималистичный: strstr по фиксированным полям (тот же подход, + * что и в firmware_test/src/cli.c) — cJSON не используется намеренно, схема + * входящих сообщений фиксирована. + * + * Входящие типы (Фаза 1): + * "cmd" → handle_cmd() → protocol_send_pong() / protocol_send_version_response() + * + * Добавление новой команды типа "cmd": + * 1. Добавить ветку if (strcmp(cmd_name, "FOO") == 0) в handle_cmd(). + */ + +#include "cli.h" + +#include "bsp/usb_cdc.h" +#include "protocol.h" + +#include +#include +#include + +/* ── Ключи полей JSON ──────────────────────────────────────────────────── */ + +static const char K_FIELD_TYPE[] = "\"type\""; +static const char K_FIELD_CMD[] = "\"cmd\""; + +/** @brief Буфер непрочитанного остатка chunk после вызова process_line(). */ +static uint8_t g_s_chunk_buf[CLI_LINE_BUF_SIZE]; +static size_t g_s_chunk_len = 0U; +static size_t g_s_chunk_pos = 0U; + +/* ── RX line buffer ────────────────────────────────────────────────────── */ + +static uint8_t g_s_line_buf[CLI_LINE_BUF_SIZE]; +static size_t g_s_line_len = 0U; + +/* ── Парсинг полей ─────────────────────────────────────────────────────── */ + +/** + * @brief Извлечь строковое значение в кавычках после двоеточия. + * + * @param[in] p_after_key Позиция сразу после ключа в строке JSON. + * @param[out] p_out Буфер для результата. + * @param[in] out_size Размер p_out (включая место под '\0'). + * @return true если значение найдено и помещается в p_out. + */ +static bool extract_string_value(const char *p_after_key, char *p_out, size_t out_size) +{ + const char *colon = strchr(p_after_key, ':'); + if (colon == NULL) + { + return false; + } + + const char *open_q = strchr(colon + 1U, '"'); + if (open_q == NULL) + { + return false; + } + open_q++; + + const char *close_q = strchr(open_q, '"'); + if (close_q == NULL) + { + return false; + } + + size_t len = (size_t) (close_q - open_q); + if (len == 0U || len >= out_size) + { + return false; + } + + memcpy(p_out, open_q, len); + p_out[len] = '\0'; + return true; +} + +/** + * @brief Извлечь значение поля "type". + */ +static bool parse_type_field(const char *p_line, char *p_out, size_t out_size) +{ + const char *key = strstr(p_line, K_FIELD_TYPE); + if (key == NULL) + { + return false; + } + return extract_string_value(key + sizeof(K_FIELD_TYPE) - 1U, p_out, out_size); +} + +/** + * @brief Извлечь значение поля "cmd". + */ +static bool parse_cmd_field(const char *p_line, char *p_out, size_t out_size) +{ + const char *key = strstr(p_line, K_FIELD_CMD); + if (key == NULL) + { + return false; + } + return extract_string_value(key + sizeof(K_FIELD_CMD) - 1U, p_out, out_size); +} + +/* ── Обработчики входящих сообщений ────────────────────────────────────── */ + +/** + * @brief Обработать сообщение {"type":"cmd",...}. + */ +static void handle_cmd(const char *p_line) +{ + const uint8_t MAX_CMD_LEN = 32U; + char cmd_name[MAX_CMD_LEN]; + + if (!parse_cmd_field(p_line, cmd_name, sizeof(cmd_name))) + { + protocol_send_error("PARSE_ERR"); + return; + } + + if (strcmp(cmd_name, "ping") == 0) + { + protocol_send_pong(); + return; + } + + if (strcmp(cmd_name, "get_version") == 0) + { + protocol_send_version_response(); + return; + } + + protocol_send_error("UNKNOWN_CMD"); +} + +/** + * @brief Диспатчить накопленную строку по полю "type". + */ +static void process_line(const char *p_line) +{ + const uint8_t MAX_TYPE_LEN = 16U; + char msg_type[MAX_TYPE_LEN]; + + if (!parse_type_field(p_line, msg_type, sizeof(msg_type))) + { + protocol_send_error("PARSE_ERR"); + return; + } + + if (strcmp(msg_type, "cmd") == 0) + { + handle_cmd(p_line); + return; + } + + protocol_send_error("UNKNOWN_CMD"); +} + +/* ── Public API ────────────────────────────────────────────────────────── */ + +void cli_init(void) +{ + g_s_line_len = 0U; + g_s_chunk_len = 0U; + g_s_chunk_pos = 0U; +} + +void cli_send(const char *p_resp) +{ + bsp_usb_cdc_write((const uint8_t *) p_resp, strlen(p_resp)); +} + +void cli_process(void) +{ + if (g_s_chunk_pos >= g_s_chunk_len) + { + g_s_chunk_len = bsp_usb_cdc_read(g_s_chunk_buf, sizeof(g_s_chunk_buf)); + g_s_chunk_pos = 0U; + } + + while (g_s_chunk_pos < g_s_chunk_len) + { + uint8_t byte = g_s_chunk_buf[g_s_chunk_pos]; + g_s_chunk_pos++; + + if (g_s_line_len >= (CLI_LINE_BUF_SIZE - 1U)) + { + g_s_line_len = 0U; + protocol_send_error("LINE_TOO_LONG"); + return; + } + + if (byte == (uint8_t) '\n') + { + if (g_s_line_len > 0U && g_s_line_buf[g_s_line_len - 1U] == (uint8_t) '\r') + { + g_s_line_len--; + } + g_s_line_buf[g_s_line_len] = '\0'; + size_t completed_len = g_s_line_len; + g_s_line_len = 0U; /* ← сбросить ДО process_line */ + if (completed_len > 0U) + { + process_line((const char *) g_s_line_buf); + } + } + + g_s_line_buf[g_s_line_len] = byte; + g_s_line_len++; + } +} diff --git a/firmware/bootloader/src/cli.h b/firmware/bootloader/src/cli.h new file mode 100644 index 0000000..a0b5ee9 --- /dev/null +++ b/firmware/bootloader/src/cli.h @@ -0,0 +1,50 @@ +/** + * @file cli.h + * @brief IO-слой CLI для bootloader. + * + * Транспорт: USB CDC ACM (bsp_usb_cdc) — единственный канал. + * Протокол: JSON-lines, каждая строка завершается '\n'. Урезанное + * подмножество протокола firmware_test (firmware/test/src/cli.h). + * + * Входящие типы (Фаза 1): + * {"type":"cmd", "cmd":"ping"} + * {"type":"cmd", "cmd":"get_version"} + * + * Исходящие события формируются через protocol.h, а не напрямую через cli_send(). + * cli_send() остаётся публичным: его использует protocol.c как единственную + * точку вывода. + */ + +#ifndef CLI_H_ +#define CLI_H_ + +#include + +/** @brief Максимальная длина входящей JSON-строки включая '\n'. */ +#define CLI_LINE_BUF_SIZE 128U + +/** + * @brief Инициализировать CLI. Сбрасывает внутренний буфер строки. + * + * Вызывать после bsp_usb_cdc_init() и до первого cli_process(). + */ +void cli_init(void); + +/** + * @brief Отправить готовую JSON-строку через USB CDC. + * + * @param[in] p_resp NUL-terminated строка, завершённая '\n'. + * + * @note Неблокирующий. Если TX занят — запись теряется. + */ +void cli_send(const char *p_resp); + +/** + * @brief Обработать входящие байты, диспатчить сообщение при получении '\n'. + * + * Вызывать в главном цикле после bsp_usb_cdc_poll(). + * Неблокирующий: если данных нет — возвращается немедленно. + */ +void cli_process(void); + +#endif /* CLI_H_ */ diff --git a/firmware/bootloader/src/main.c b/firmware/bootloader/src/main.c new file mode 100644 index 0000000..01b5ec6 --- /dev/null +++ b/firmware/bootloader/src/main.c @@ -0,0 +1,63 @@ +/** + * @file main.c + * @brief bootloader — точка входа. + * + * Фаза 1 (скелет): bring-up + USB CDC ACM + ping/get_version. Без доступа + * к Flash-слотам, без bootutil, без SD — это добавится в Фазах 2-3. + * + * Единственный канал хост↔плата: USB CDC ACM. Протокол: JSON-lines через + * cli.c (урезанное подмножество протокола firmware_test). + * + * Последовательность старта (зеркалит firmware/test/src/main.c): + * 1. board_hw_init() — тактирование, MPU, кэш, пины + * 2. bsp_led_init() — оба LED выключены + * 3. bsp_tick_init() — SysTick 1 мс + * 4. bsp_usb_cdc_init() — PHY + стек + NVIC + * 5. Ожидание CDC ready — LED_HEARTBEAT мигает + * 6. cli_init() — сброс буфера + * 7. Главный цикл — poll + cli_process + * + * Bootloader без SDRAM (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) — DCD + * не используется (bsp_boot_xip_no_dcd). + */ +#include "board.h" +#include "bsp/led.h" +#include "bsp/tick.h" +#include "bsp/usb_cdc.h" +#include "cli.h" + +#include + +int main(void) +{ + const uint32_t CONNECT_BLINK_MS = 200U; + const uint32_t ERROR_BLINK_MS = 250U; + + board_hw_init(); + + bsp_led_init(); + bsp_tick_init(); + + if (bsp_usb_cdc_init() != BSP_OK) + { + bsp_led_toggle(LED_HEARTBEAT); + bsp_delay(ERROR_BLINK_MS); + } + + /* Ожидать подключения хоста. LED_HEARTBEAT мигает — bootloader жив. */ + while (!bsp_usb_cdc_is_ready()) + { + bsp_usb_cdc_poll(); + bsp_led_toggle(LED_HEARTBEAT); + bsp_delay(CONNECT_BLINK_MS); + } + + bsp_led_on(LED_APP); + + cli_init(); + while (1) + { + bsp_usb_cdc_poll(); + cli_process(); + } +} diff --git a/firmware/bootloader/src/protocol.c b/firmware/bootloader/src/protocol.c new file mode 100644 index 0000000..ffb28ef --- /dev/null +++ b/firmware/bootloader/src/protocol.c @@ -0,0 +1,38 @@ +/** + * @file protocol.c + * @brief Протокол bootloader — реализация сериализации. + */ + +#include "protocol.h" + +#include "cli.h" + +#include + +/* ── Константы ─────────────────────────────────────────────────────────── */ + +/** @brief Размер внутреннего TX-буфера. */ +#define PROTO_BUF_SIZE 128U + +/* ── Public API ────────────────────────────────────────────────────────── */ + +void protocol_send_pong(void) +{ + cli_send("{\"type\":\"pong\"}\n"); +} + +void protocol_send_version_response(void) +{ + char buf[PROTO_BUF_SIZE]; + (void) snprintf(buf, sizeof(buf), + "{\"type\":\"version_response\"," + "\"fw\":\"" BOOTLOADER_VERSION "\"}\n"); + cli_send(buf); +} + +void protocol_send_error(const char *p_code) +{ + char buf[PROTO_BUF_SIZE]; + (void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code); + cli_send(buf); +} diff --git a/firmware/bootloader/src/protocol.h b/firmware/bootloader/src/protocol.h new file mode 100644 index 0000000..4711bbb --- /dev/null +++ b/firmware/bootloader/src/protocol.h @@ -0,0 +1,48 @@ +/** + * @file protocol.h + * @brief Протокол bootloader — сериализация исходящих событий. + * + * Урезанное подмножество протокола firmware_test (firmware/test/src/protocol.h): + * только то, что нужно для диагностики bootloader через service-tui. Без + * test_begin/test_result/confirm_request/session_start — те специфичны для + * тестового раннера firmware_test. + * + * Все функции формируют JSON-строку и отправляют через cli_send(). + * Без динамической памяти — каждая функция пишет в стековый буфер. + * + * Типы исходящих событий (Фаза 1): + * pong — ответ на {"type":"cmd","cmd":"ping"} + * version_response — ответ на {"type":"cmd","cmd":"get_version"} + * error — ошибка протокола или парсинга + * + * status-события (waiting_for_sd/installing/smoke_pass/...) добавятся в Фазах 3-4. + */ + +#ifndef PROTOCOL_H_ +#define PROTOCOL_H_ + +#include "version.h" + +/** @brief Строка версии bootloader, вставляемая в version_response. */ +#define BOOTLOADER_VERSION BOOTLOADER_VERSION_STR + +/** + * @brief Отправить pong — ответ на ping. + */ +void protocol_send_pong(void); + +/** + * @brief Отправить ответ на команду get_version. + * + * Формат: {"type":"version_response","fw":"X.Y.Z"} + */ +void protocol_send_version_response(void); + +/** + * @brief Отправить событие error. + * + * @param[in] p_code Короткий ASCII-код ошибки, напр. "PARSE_ERR". + */ +void protocol_send_error(const char *p_code); + +#endif /* PROTOCOL_H_ */ diff --git a/firmware/bootloader/src/version.h.in b/firmware/bootloader/src/version.h.in new file mode 100644 index 0000000..12f59f3 --- /dev/null +++ b/firmware/bootloader/src/version.h.in @@ -0,0 +1,25 @@ +/** + * @file version.h + * @brief Версия bootloader — генерируется CMake из CMakeLists.txt. + * + * НЕ редактировать вручную. Версию менять в firmware/bootloader/CMakeLists.txt: + * project(bootloader VERSION X.Y.Z) + */ + +#ifndef VERSION_H_ +#define VERSION_H_ + +/** @brief Мажорная версия. */ +#define BOOTLOADER_VERSION_MAJOR @bootloader_VERSION_MAJOR@ + +/** @brief Минорная версия. */ +#define BOOTLOADER_VERSION_MINOR @bootloader_VERSION_MINOR@ + +/** @brief Патч-версия. */ +#define BOOTLOADER_VERSION_PATCH @bootloader_VERSION_PATCH@ + +/** @brief Версия строкой для протокола: "X.Y.Z". */ +#define BOOTLOADER_VERSION_STR \ + "@bootloader_VERSION_MAJOR@.@bootloader_VERSION_MINOR@.@bootloader_VERSION_PATCH@" + +#endif /* VERSION_H_ */