# bootloader: phase 0-1 + docs

This commit is contained in:
Dmitry Akimov 2026-07-08 20:33:45 +03:00
parent 49ae714955
commit 4644f21f07
14 changed files with 1308 additions and 9 deletions

2
.vscode/launch.json vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

266
firmware/bootloader/PLAN.md Normal file
View file

@ -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) — только после того как логика зелёная на хосте, ручная проверка на плате по чек-листу каждой
фазы. Не начинать следующую фазу, пока не пройдена верификация текущей.

View file

@ -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` генерируется, не редактируется вручную.

View file

@ -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 <stdbool.h>
#include <stdint.h>
#include <string.h>
/* ── Ключи полей 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++;
}
}

View file

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

View file

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

View file

@ -0,0 +1,38 @@
/**
* @file protocol.c
* @brief Протокол bootloader реализация сериализации.
*/
#include "protocol.h"
#include "cli.h"
#include <stdio.h>
/* ── Константы ─────────────────────────────────────────────────────────── */
/** @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);
}

View file

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

View file

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