# bootloader: Phase 2 - checked and done
This commit is contained in:
parent
db2c573642
commit
580e5f50d9
16 changed files with 1049 additions and 70 deletions
|
|
@ -47,6 +47,9 @@ add_subdirectory(lib)
|
||||||
if(NOT BUILD_TESTS_HOST)
|
if(NOT BUILD_TESTS_HOST)
|
||||||
add_subdirectory(firmware/test)
|
add_subdirectory(firmware/test)
|
||||||
add_subdirectory(firmware/bootloader)
|
add_subdirectory(firmware/bootloader)
|
||||||
|
# Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) — удалить,
|
||||||
|
# когда появится реальный firmware/tft_app. См. firmware/bootloader/PLAN.md.
|
||||||
|
add_subdirectory(firmware/bootloader/test_stub)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,15 @@
|
||||||
"app"
|
"app"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mcuboot-stub-debug",
|
||||||
|
"displayName": "mcuboot slot stub (Фаза 2 верификация) — Debug",
|
||||||
|
"configurePreset": "Debug",
|
||||||
|
"targets": [
|
||||||
|
"test_slot_stub_a",
|
||||||
|
"test_slot_stub_b"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "app-release",
|
"name": "app-release",
|
||||||
"displayName": "app — Release",
|
"displayName": "app — Release",
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,9 @@
|
||||||
/**
|
/**
|
||||||
* @brief Размер читаемого буфера для однобайтных SR-команд.
|
* @brief Размер читаемого буфера для однобайтных SR-команд.
|
||||||
*
|
*
|
||||||
* FlexSPI FIFO работает минимальными единицами в 4 байта (RXWMRK=0, 1 FILL
|
* FlexSPI FIFO работает минимальными единицами в 4 байта. Читаем 4 байта,
|
||||||
* unit = 4 bytes). Читаем 4 байта, используем только byte[0].
|
* используем только byte[0]. (Про размер watermark-юнита IPRXFSTS.FILL —
|
||||||
|
* см. QSPI_WM_UNIT_BYTES и комментарий в qspi_read_tail().)
|
||||||
*/
|
*/
|
||||||
#define SR_READ_LEN 4U
|
#define SR_READ_LEN 4U
|
||||||
|
|
||||||
|
|
@ -338,9 +339,16 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_tail(uint8_t *p_dst, uint3
|
||||||
|
|
||||||
while (!done)
|
while (!done)
|
||||||
{
|
{
|
||||||
const uint32_t FILL =
|
/* IPRXFSTS.FILL считает в watermark-юнитах (1 unit = QSPI_WM_UNIT_WORDS
|
||||||
|
* слов = QSPI_WM_UNIT_BYTES байт), а не в словах напрямую — см.
|
||||||
|
* FLEXSPI_GetFifoCounts() в sdk/.../drivers/fsl_flexspi.h, которая
|
||||||
|
* домножает это же поле на 8 при переводе в байты. Без домножения
|
||||||
|
* ниже FILL=1 (один готовый юнит, 2 слова уже в RFDR[0..1]) никогда
|
||||||
|
* не проходил сравнение с WORDS_NEEDED=2 — busy-wait висел вечно,
|
||||||
|
* хотя нужные данные уже лежали в FIFO. */
|
||||||
|
const uint32_t FILL_UNITS =
|
||||||
(QSPI_BASE->IPRXFSTS & FLEXSPI_IPRXFSTS_FILL_MASK) >> FLEXSPI_IPRXFSTS_FILL_SHIFT;
|
(QSPI_BASE->IPRXFSTS & FLEXSPI_IPRXFSTS_FILL_MASK) >> FLEXSPI_IPRXFSTS_FILL_SHIFT;
|
||||||
if (FILL >= WORDS_NEEDED)
|
if ((FILL_UNITS * QSPI_WM_UNIT_WORDS) >= WORDS_NEEDED)
|
||||||
{
|
{
|
||||||
done = true;
|
done = true;
|
||||||
}
|
}
|
||||||
|
|
@ -508,7 +516,23 @@ AT_QUICKACCESS_SECTION_CODE(static void qspi_ip_setup(uint32_t seq_idx, uint32_t
|
||||||
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_ip_read(uint32_t seq_idx, uint32_t addr,
|
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_ip_read(uint32_t seq_idx, uint32_t addr,
|
||||||
uint8_t *p_rx, uint32_t data_len))
|
uint8_t *p_rx, uint32_t data_len))
|
||||||
{
|
{
|
||||||
qspi_ip_setup(seq_idx, addr, data_len);
|
/* IDATSZ округляем вверх до кратного QSPI_RFDR_WORD_BYTES (4).
|
||||||
|
*
|
||||||
|
* Наблюдение: при data_len, не кратном 4 (напр. 7 — хвост hash-цикла
|
||||||
|
* bootutil), контроллер отдавал ровно один "бит" LUT-инструкции
|
||||||
|
* READ_SDR (operand=4 байта, см. LSEQ_IP_READ) и выставлял IPCMDDONE,
|
||||||
|
* не дотягивая до второго (частичного) слова — IPRXFSTS.FILL зависал
|
||||||
|
* на 1 навсегда. JEDEC/status-регистры (SR_READ_LEN=4) этой границы
|
||||||
|
* никогда не касались — отсюда и не было заметно раньше.
|
||||||
|
*
|
||||||
|
* qspi_read_fifo()/qspi_read_tail() ниже вызываются с ИСХОДНЫМ
|
||||||
|
* data_len — из задренированного (возможно на слово большего) FIFO
|
||||||
|
* извлекается по-прежнему ровно запрошенное число байт, лишний
|
||||||
|
* padding-байт молча дренируется вместе со словом и отбрасывается. */
|
||||||
|
const uint32_t IDATSZ_ALIGNED =
|
||||||
|
(data_len + (QSPI_RFDR_WORD_BYTES - 1U)) & ~(QSPI_RFDR_WORD_BYTES - 1U);
|
||||||
|
|
||||||
|
qspi_ip_setup(seq_idx, addr, IDATSZ_ALIGNED);
|
||||||
const status_t RESULT = qspi_read_fifo(p_rx, data_len);
|
const status_t RESULT = qspi_read_fifo(p_rx, data_len);
|
||||||
qspi_wait_idle();
|
qspi_wait_idle();
|
||||||
return RESULT;
|
return RESULT;
|
||||||
|
|
|
||||||
130
cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld
Normal file
130
cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
** ###################################################################
|
||||||
|
** Linker file for the GNU C Compiler
|
||||||
|
**
|
||||||
|
** Abstract:
|
||||||
|
** Тестовая заглушка для аппаратной верификации Фазы 2 bootutil
|
||||||
|
** (firmware/bootloader/PLAN.md) — минимальный XIP-образ, который
|
||||||
|
** boot_go() выбирает и в который bootloader реально прыгает.
|
||||||
|
** НЕ boot-образ для BootROM (нет FCB/IVT/DCD секций — Slot A/Б не
|
||||||
|
** видны BootROM напрямую, только software jump из bootloader).
|
||||||
|
**
|
||||||
|
** Базовый адрес слота передаётся через -Wl,--defsym=__slot_base__=0x...
|
||||||
|
** (Slot A: 0x60040000, Slot Б: 0x60240000, см.
|
||||||
|
** docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). imgtool header (0x200)
|
||||||
|
** зарезервирован перед vector table — boot_select.c вычисляет адрес
|
||||||
|
** прыжка как flash_base + fa_off + ih_hdr_size.
|
||||||
|
** ###################################################################
|
||||||
|
*/
|
||||||
|
|
||||||
|
ENTRY(Reset_Handler)
|
||||||
|
|
||||||
|
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x400;
|
||||||
|
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x400;
|
||||||
|
|
||||||
|
SLOT_BASE = DEFINED(__slot_base__) ? __slot_base__ : 0x60040000;
|
||||||
|
IMGTOOL_HDR_SZ = 0x200; /* -H 0x200 при imgtool sign, см. PLAN.md */
|
||||||
|
|
||||||
|
MEMORY
|
||||||
|
{
|
||||||
|
m_interrupts (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ, LENGTH = 0x00000400
|
||||||
|
m_text (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ + 0x400, LENGTH = 0x00008000 /* 32 KB — с большим запасом для мигалки */
|
||||||
|
m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128KB */
|
||||||
|
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256KB — не используется стабом, нужна board_mpu_init() */
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTIONS
|
||||||
|
{
|
||||||
|
__NCACHE_REGION_START = ORIGIN(m_data2);
|
||||||
|
__NCACHE_REGION_SIZE = 0x2000; /* 8 KB non-cacheable, как в остальных линкер-скриптах */
|
||||||
|
|
||||||
|
.interrupts :
|
||||||
|
{
|
||||||
|
__VECTOR_TABLE = .;
|
||||||
|
__Vectors = .;
|
||||||
|
. = ALIGN(4);
|
||||||
|
KEEP(*(.isr_vector))
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > m_interrupts
|
||||||
|
|
||||||
|
.text :
|
||||||
|
{
|
||||||
|
. = ALIGN(4);
|
||||||
|
*(.text)
|
||||||
|
*(.text*)
|
||||||
|
*(.rodata)
|
||||||
|
*(.rodata*)
|
||||||
|
*(.glue_7)
|
||||||
|
*(.glue_7t)
|
||||||
|
*(.eh_frame)
|
||||||
|
KEEP (*(.init))
|
||||||
|
KEEP (*(.fini))
|
||||||
|
. = ALIGN(4);
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
.ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } > m_text
|
||||||
|
|
||||||
|
.ARM :
|
||||||
|
{
|
||||||
|
__exidx_start = .;
|
||||||
|
*(.ARM.exidx*)
|
||||||
|
__exidx_end = .;
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
.init_array :
|
||||||
|
{
|
||||||
|
PROVIDE_HIDDEN (__init_array_start = .);
|
||||||
|
KEEP (*(SORT(.init_array.*)))
|
||||||
|
KEEP (*(.init_array*))
|
||||||
|
PROVIDE_HIDDEN (__init_array_end = .);
|
||||||
|
} > m_text
|
||||||
|
|
||||||
|
__etext = .;
|
||||||
|
__DATA_ROM = .;
|
||||||
|
|
||||||
|
.data : AT(__DATA_ROM)
|
||||||
|
{
|
||||||
|
. = ALIGN(4);
|
||||||
|
__data_start__ = .;
|
||||||
|
*(.data)
|
||||||
|
*(.data*)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__data_end__ = .;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
.bss :
|
||||||
|
{
|
||||||
|
. = ALIGN(4);
|
||||||
|
__bss_start__ = .;
|
||||||
|
*(.bss)
|
||||||
|
*(.bss*)
|
||||||
|
*(COMMON)
|
||||||
|
. = ALIGN(4);
|
||||||
|
__bss_end__ = .;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
.heap :
|
||||||
|
{
|
||||||
|
. = ALIGN(8);
|
||||||
|
__end__ = .;
|
||||||
|
PROVIDE(end = .);
|
||||||
|
__HeapBase = .;
|
||||||
|
. += HEAP_SIZE;
|
||||||
|
__HeapLimit = .;
|
||||||
|
__heap_limit = .;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
.stack :
|
||||||
|
{
|
||||||
|
. = ALIGN(8);
|
||||||
|
. += STACK_SIZE;
|
||||||
|
} > m_data
|
||||||
|
|
||||||
|
__StackTop = ORIGIN(m_data) + LENGTH(m_data);
|
||||||
|
__StackLimit = __StackTop - STACK_SIZE;
|
||||||
|
PROVIDE(__stack = __StackTop);
|
||||||
|
|
||||||
|
.ARM.attributes 0 : { *(.ARM.attributes) }
|
||||||
|
|
||||||
|
ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap")
|
||||||
|
}
|
||||||
|
|
@ -12,16 +12,25 @@ set(TARGET_NAME bootloader)
|
||||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in"
|
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in"
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/version.h" @ONLY)
|
"${CMAKE_CURRENT_BINARY_DIR}/generated/version.h" @ONLY)
|
||||||
|
|
||||||
|
# bootutil (MCUboot Direct-XIP) + TinyCrypt + ASN.1 — общий список с
|
||||||
|
# host-тестами (tests/host/mcuboot_port/), см. firmware/bootloader/PLAN.md,
|
||||||
|
# Фаза 2.
|
||||||
|
include(${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/bootutil_sources.cmake)
|
||||||
|
|
||||||
add_executable(
|
add_executable(
|
||||||
${TARGET_NAME}
|
${TARGET_NAME}
|
||||||
src/main.c
|
src/main.c
|
||||||
src/cli.c
|
src/cli.c
|
||||||
src/protocol.c
|
src/protocol.c
|
||||||
|
src/boot_select.c
|
||||||
|
mcuboot_port/flash_map_backend.c
|
||||||
|
mcuboot_port/keys.c
|
||||||
|
${MCUBOOT_BOOTUTIL_SOURCES}
|
||||||
${BSP_GENERATED}/clock_config.c
|
${BSP_GENERATED}/clock_config.c
|
||||||
${BSP_STARTUP_FILE}
|
${BSP_STARTUP_FILE}
|
||||||
${BSP_SYSCALLS_FILE})
|
${BSP_SYSCALLS_FILE})
|
||||||
|
|
||||||
target_include_directories(${TARGET_NAME} PRIVATE src/)
|
target_include_directories(${TARGET_NAME} PRIVATE src/ ${MCUBOOT_BOOTUTIL_INCLUDES})
|
||||||
|
|
||||||
target_include_directories(${TARGET_NAME}
|
target_include_directories(${TARGET_NAME}
|
||||||
PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
|
||||||
|
|
@ -31,11 +40,11 @@ target_compile_definitions(
|
||||||
__STARTUP_INITIALIZE_NONCACHEDATA)
|
__STARTUP_INITIALIZE_NONCACHEDATA)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Зависимости — минимум для Фазы 1 (bring-up + CDC). bsp_qspi_flash (доступ к
|
# Зависимости. bsp_button (downgrade-override) добавится в Фазе 3.
|
||||||
# слотам) и bsp_button (downgrade-override) добавятся в Фазах 2-3.
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick
|
target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick
|
||||||
bsp_usb_cdc bsp_boot_xip_no_dcd)
|
bsp_usb_cdc bsp_qspi_flash
|
||||||
|
bsp_boot_xip_no_dcd)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом
|
# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) |
|
| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) |
|
||||||
| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже |
|
| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже |
|
||||||
| 2 — bootutil (Direct-XIP) | 🟡 host-тесты завершены | реальный bootutil+TinyCrypt+imgtool-фикстуры, 5/5 тестов зелёные; ARM-сторона (flash_map_backend над bsp_qspi_flash, boot_select.c/jump, сборка/железо) — впереди |
|
| 2 — bootutil (Direct-XIP) | ✅ завершена | host-тесты 5/5, аппаратная верификация — все 5 сценариев пройдены на реальной плате (детали и 3 найденных/исправленных бага — [DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md)) |
|
||||||
| 3 — SD-путь установки | не начата | |
|
| 3 — SD-путь установки | не начата | |
|
||||||
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | |
|
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | |
|
||||||
| 5 — HAB Release + service-tui | не начата | |
|
| 5 — HAB Release + service-tui | не начата | |
|
||||||
|
|
@ -209,18 +209,197 @@
|
||||||
`test_boot_go_ignores_corrupted_slot`, `test_boot_go_no_valid_image`,
|
`test_boot_go_ignores_corrupted_slot`, `test_boot_go_no_valid_image`,
|
||||||
`test_boot_go_reverts_unconfirmed_image`).
|
`test_boot_go_reverts_unconfirmed_image`).
|
||||||
|
|
||||||
**Осталось для Фазы 2 (ARM-сторона, ещё не сделано):**
|
**ARM-сторона (выполнено):**
|
||||||
- `firmware/bootloader/mcuboot_port/flash_map_backend.c` — реальный шим над `bsp_qspi_flash`
|
- `firmware/bootloader/mcuboot_port/flash_map_backend.c` — реальный шим над `bsp_qspi_flash`.
|
||||||
(`bsp_qspi_read`/`bsp_qspi_write_page`/`bsp_qspi_erase_sector`), по образцу
|
`bsp_qspi_read/write_page/erase_sector()` принимают flash-relative адрес (0-based, `IPCR0` FlexSPI
|
||||||
`sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/flashapi/flash_api.c`.
|
IP-команд), НЕ XIP-адрес — `flash_area.fa_off` тоже flash-relative (Slot A `0x00040000`, Slot Б
|
||||||
- `firmware/bootloader/src/boot_select.c` — `boot_go()` + прыжок в выбранный образ. Референс —
|
`0x00240000`). `flash_device_base()` — единственное место с XIP-адресом `0x60000000`, нужен только
|
||||||
|
`boot_select.c` для вычисления адреса прыжка. Постраничная запись (`write_page_chunked`) — порт
|
||||||
|
логики `flash_area_write_internal()` из NXP-референса: 0xFF поверх уже запрограммированных байт не
|
||||||
|
меняет их (NOR program только сбрасывает биты 1→0), поэтому безопасно перезатирать буфером с
|
||||||
|
ERASED_VAL в нетронутой части страницы.
|
||||||
|
- `firmware/bootloader/src/boot_select.{c,h}` — `boot_go()` + `jump_to_image()`. Портировано с
|
||||||
`sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot()`: `flash_device_base()` →
|
`sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot()`: `flash_device_base()` →
|
||||||
`vt = flash_base + rsp->br_image_off + rsp->br_hdr->ih_hdr_size` → `__set_MSP(vt->msp)` →
|
`vt = flash_base + rsp->br_image_off + rsp->br_hdr->ih_hdr_size` → `__set_MSP(vt->msp)` →
|
||||||
`((void(*)(void))vt->reset)()`. CMSIS-интринсики, ассемблер не нужен.
|
`((void(*)(void))vt->reset)()`. CMSIS-интринсики (`fsl_common.h`), ассемблер не понадобился.
|
||||||
- Подключить `bsp_qspi_flash` в `firmware/bootloader/CMakeLists.txt`, собрать под ARM (проверить что
|
- `firmware/bootloader/CMakeLists.txt` — `bsp_qspi_flash` в зависимостях,
|
||||||
`m_text` укладывается в 247 КБ бюджет Фазы 1).
|
`include(mcuboot_port/bootutil_sources.cmake)` (тот же список bootutil+TinyCrypt+ASN.1, что и у
|
||||||
- Аппаратная проверка: образ-заглушка (просто зажигает LED), подписанный тем же тестовым ключом, залит
|
host-тестов — не дублируется). `-w`/`-fno-sanitize` для вендоренного кода перенесены в сам
|
||||||
вручную через SWD в Slot A (`0x60040000`) — подтвердить что bootloader реально в него прыгает.
|
`bootutil_sources.cmake` (`set_source_files_properties`), не блэнкетом на весь таргет — наш код
|
||||||
|
(`main.c`, `boot_select.c`, `flash_map_backend.c`) остаётся под обычными warnings.
|
||||||
|
- `main.c`: после `board_hw_init()`/`bsp_led_init()`/`bsp_tick_init()`/`bsp_qspi_init()` — сразу
|
||||||
|
`boot_select_and_jump()`, **до** поднятия USB CDC. Так плата грузится в tft_app и без подключённого
|
||||||
|
кабеля (нормальный полевой сценарий) — ждать хоста для проверки образа было бы неправильно. Провал
|
||||||
|
→ **не падать**, продолжить в уже существующий ping/pong-цикл Фазы 1 — прообраз будущего состояния
|
||||||
|
"жду SD" из Фазы 3, без самого SD-сканирования. Сценарий "оба слота пусты" на железе проверяется тем
|
||||||
|
же CDC-ping, что и в Фазе 1.
|
||||||
|
- Собрано под ARM: `m_text` — 50712 Б из 247 КБ бюджета (20.05%, был 12.85% в Фазе 1 — рост за счёт
|
||||||
|
bootutil+TinyCrypt+ASN.1, запас всё ещё большой). `just build::build-bootloader-debug` и
|
||||||
|
`hab-bootloader-debug` — оба зелёные.
|
||||||
|
|
||||||
|
### Аппаратная верификация Фазы 2 — все 5 сценариев пройдены на железе
|
||||||
|
|
||||||
|
**Зачем отдельная заглушка.** `tft_app` не существует — нечего класть в слоты для проверки прыжка.
|
||||||
|
Нужен минимальный, независимо собираемый "образ", который bootloader может реально выбрать и в
|
||||||
|
который может реально прыгнуть — не файл с мусором, а настоящий imgtool-подписанный образ с корректным
|
||||||
|
vector table по адресу слота.
|
||||||
|
|
||||||
|
**Реализовано в `firmware/bootloader/test_stub/`** (не `tests/target/` — тот масштабируется под
|
||||||
|
RAM-загрузку через pyOCD для HIL, наш стаб — XIP из Flash, ближе по духу к самому bootloader; удалить
|
||||||
|
директорию целиком, когда появится реальный `firmware/tft_app`):
|
||||||
|
- `main.c` — `board_hw_init()` → `bsp_led_init()` → `bsp_tick_init()` → бесконечный цикл
|
||||||
|
`bsp_led_toggle(LED_APP)` с периодом `STUB_BLINK_MS` (компилируется в двух вариантах). Без USB/CDC.
|
||||||
|
- `cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld` — параметризован через `-Wl,--defsym=__slot_base__=`,
|
||||||
|
один файл на оба таргета вместо двух копий. `ORIGIN = SLOT_BASE + 0x200` (после imgtool header),
|
||||||
|
`m_text` 32 КБ (с большим запасом для мигалки в 2-МБ слоте). Пришлось добавить `m_data2`/
|
||||||
|
`__NCACHE_REGION_START/SIZE` — `board_mpu_init()` (общий для всех прошивок, включён транзитивно
|
||||||
|
через `bsp_board`) их безусловно требует, даже когда некэшируемый регион не используется.
|
||||||
|
- Два CMake-таргета из одного `main.c` (`add_mcuboot_stub()` в `test_stub/CMakeLists.txt`):
|
||||||
|
`test_slot_stub_a` (`__slot_base__=0x60040000`, `STUB_BLINK_MS=500` — мигает ~1 раз/сек, "версия 1"),
|
||||||
|
`test_slot_stub_b` (`__slot_base__=0x60240000`, `STUB_BLINK_MS=250` — ~2 раза/сек, "версия 2").
|
||||||
|
Разный адрес — намеренно: MCUboot Direct-XIP код обычно не позиционно-независим (открытый вопрос из
|
||||||
|
BOOTLOADER_FLASH_MAP.md §4), это первая реальная проверка two-slot-two-linkage подхода.
|
||||||
|
- Оба собраны и подписаны тем же тестовым ключом (`sdk/middleware/mcuboot_opensource/root-ec-p256.pem`),
|
||||||
|
реальные параметры слота (`-H 0x200 -S 0x200000 --align 1 --pad-header --pad`) — **одной командой**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just build::build-mcuboot-stub
|
||||||
|
```
|
||||||
|
|
||||||
|
Рецепт (`just/build.just`, группа `mcuboot-stub`) сам собирает `test_slot_stub_a`/`test_slot_stub_b`
|
||||||
|
(пресет `mcuboot-stub-debug`, targets в `CMakePresets.json`, т.к. в `bootloader-debug` их нет — иначе
|
||||||
|
собирались бы всегда вместе с bootloader) и подписывает imgtool'ом (эфемерный venv через
|
||||||
|
`uv run --with cryptography --with intelhex --with click --with cbor2 --with pyyaml` — эти зависимости
|
||||||
|
не в `tools/host` (spsdk), туда их специально не добавляли, чтобы не раздувать основной venv ради
|
||||||
|
временной тестовой оснастки). **Важно**: сначала это было проделано вручную в чате и `signed/` не
|
||||||
|
появлялась при обычной пересборке — это и вскрылось при попытке воспроизвести. Плюс была реальная
|
||||||
|
ошибка в первой версии рецепта: потерян `--pad` у `unconfirmed`-варианта (файл получался 16 КБ вместо
|
||||||
|
2 МБ — без trailer'а в конце слота revert-сценарий не работал бы). Исправлено, проверено размерами
|
||||||
|
файлов (все три — по 2 МБ).
|
||||||
|
- `build/Debug/signed/stub_a_v1_confirmed.bin` — `-v 1.0.0 --confirm` (сценарии 1, 3, 4)
|
||||||
|
- `build/Debug/signed/stub_b_v2_confirmed.bin` — `-v 2.0.0 --confirm` (сценарий 2)
|
||||||
|
- `build/Debug/signed/stub_a_v1_unconfirmed.bin` — `-v 1.0.0`, без `--confirm` (сценарий 5, revert)
|
||||||
|
|
||||||
|
**Прошивка в слот напрямую по адресу** — не через `flash_swd.py` (тот собирает FCB+IVT+HAB под
|
||||||
|
`0x60000000`, слотам это не нужно — они не самостоятельный boot-образ для BootROM, а данные, которые
|
||||||
|
читает `boot_go()`). Пишем сырой подписанный `.bin` напрямую по адресу слота через pyOCD (тот же
|
||||||
|
`tools/hil` venv, что уже использует `flash_swd.py` — см. `run_pyocd_flash()`):
|
||||||
|
|
||||||
|
**Важно**: `uv run --directory tools/hil` меняет рабочую директорию у самого `pyocd`, а не только у
|
||||||
|
`uv` — относительный путь к `.bin` резолвится от `tools/hil/`, не от корня репозитория. Путь к образу
|
||||||
|
должен быть абсолютным (`"$(pwd)/build/..."`, запускать из корня репо).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Slot A — валидный, confirmed (сценарии 1, 3, 4)
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60040000 --erase sector "$(pwd)/build/Debug/signed/stub_a_v1_confirmed.bin"
|
||||||
|
|
||||||
|
# Slot Б — валидный, confirmed, новее (сценарий 2)
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60240000 --erase sector "$(pwd)/build/Debug/signed/stub_b_v2_confirmed.bin"
|
||||||
|
|
||||||
|
# Slot A — неподтверждённый, для проверки revert (сценарий 5, вместо confirmed-варианта выше)
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60040000 --erase sector "$(pwd)/build/Debug/signed/stub_a_v1_unconfirmed.bin"
|
||||||
|
|
||||||
|
# Стереть слот (для сценария 4 — "оба слота пусты")
|
||||||
|
# Адрес диапазона — позиционный аргумент, не через -a; start+length, не @.
|
||||||
|
uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--sector 0x60040000+0x200000
|
||||||
|
uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--sector 0x60240000+0x200000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Чек-лист (зеркалит 5 host-тестов, но на реальном железе и с реальным `bsp_qspi_flash`) — все 5
|
||||||
|
сценариев пройдены на плате (2026-07-09):**
|
||||||
|
|
||||||
|
| № | Сценарий | Подготовка | Ожидаемый результат |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Валиден только Slot A | Erase Slot Б, `stub_a_v1_confirmed.bin` → Slot A | LED_APP мигает ~1/сек (частота stub_a) |
|
||||||
|
| 2 | Оба валидны, побеждает версия Б | + `stub_b_v2_confirmed.bin` → Slot Б | LED_APP мигает ~2/сек (частота stub_b) |
|
||||||
|
| 3 | Slot Б повреждён | В Slot Б — испорченный файл (например, скопировать `stub_b_v2_confirmed.bin`, поменять байт в payload, прошить) | LED_APP возвращается к ~1/сек (Slot A) |
|
||||||
|
| 4 | Оба слота пусты | Erase Slot A и Slot Б целиком | LED_APP не мигает по образцу заглушки; `ping` по CDC отвечает `pong` — bootloader не прыгнул, остался в своём цикле |
|
||||||
|
| 5 | Revert неподтверждённого образа | `stub_a_v1_unconfirmed.bin` → Slot A; power cycle (1) → LED мигает (выбран впервые, `copy_done` выставляется); power cycle (2) БЕЗ вмешательства → Slot A должен быть стёрт bootutil'ом | После второго ресета — как сценарий 4 (LED не мигает, CDC ping жив) |
|
||||||
|
|
||||||
|
Между сценариями — обязательный power cycle (SWD-запись не ресетит автоматически, как и в Фазе 1).
|
||||||
|
|
||||||
|
**Инструмент подтверждения "прыжок реально произошёл", а не просто "LED мигает случайно":** частота
|
||||||
|
мигания однозначно указывает на конкретный слот (500 мс vs 250 мс визуально различимы), так что
|
||||||
|
чек-лист верифицируем глазами без дополнительной телеметрии. Если нужна более строгая проверка —
|
||||||
|
можно снять частоту осциллографом/логическим анализатором через MCU-Link, но для Фазы 2 визуального
|
||||||
|
контроля достаточно.
|
||||||
|
|
||||||
|
### Найденный баг: сценарий 1 не проходил — LED мигал один раз и замирал
|
||||||
|
|
||||||
|
`jump_to_image()` (`boot_select.c`) вызывал `__disable_irq()` перед прыжком — "для чистоты", не было
|
||||||
|
в референсном `do_boot()` NXP. `bsp_delay()` (`bsp/tick/src/tick.c`) — busy-wait на `g_s_tick_ms`,
|
||||||
|
инкрементируемом только внутри `SysTick_Handler` (ISR). Обычный `Reset_Handler` целевого образа не
|
||||||
|
трогает PRIMASK — рассчитывает, что прерывания уже разрешены, как после настоящего аппаратного
|
||||||
|
ресета. Замаскировав IRQ прыжком и не восстановив их нигде, мы гарантированно вешали `bsp_delay()` в
|
||||||
|
любом образе, куда прыгает bootloader: `LED_APP` успевал toggle'нуться один раз (до первого
|
||||||
|
`bsp_delay()` внутри цикла стаба) и застывал — неотличимо на глаз от "не мигает вообще".
|
||||||
|
|
||||||
|
**Исправлено**: убрали `__disable_irq()` из `jump_to_image()` — как и в референсе, трогаем только
|
||||||
|
`__set_CONTROL(0)`/`__set_MSP`/`__ISB`. Пересобрано (`m_text` уменьшился на 272 Б), HAB
|
||||||
|
пересгенерирован. Стаб-образы (`build/Debug/signed/*.bin`) пересборки не требуют — баг был чисто на
|
||||||
|
стороне bootloader, не заглушки.
|
||||||
|
|
||||||
|
### Второй найденный баг: сценарий 1 всё ещё не проходил — реальный завис в `bsp_qspi_flash`
|
||||||
|
|
||||||
|
После фикса `jump_to_image()` завис уже сам bootloader, **до** прыжка — в отладчике (стек
|
||||||
|
`boot_select_and_jump → boot_go → ... → bootutil_img_validate → bootutil_img_hash → flash_area_read →
|
||||||
|
bsp_qspi_read → qspi_ip_read → qspi_read_fifo → qspi_read_tail`) видно бесконечный busy-wait на
|
||||||
|
`IPRXFSTS.FILL`. Значения на момент зависания: `remain=7` (внутри `qspi_read_tail`, т.е. запрошено
|
||||||
|
`WORDS_NEEDED=2` слова), `FILL` стабильно `1`, `QSPI_BASE->INTR=0x61` — расшифровка битов
|
||||||
|
(`PERI_FLEXSPI.h`): bit0 `IPCMDDONE` **уже установлен**. Контроллер считает IP-команду завершённой,
|
||||||
|
реально доставив только одно слово (4 байта) из требуемых двух.
|
||||||
|
|
||||||
|
`bootutil_img_hash()` (`image_validate.c:125-129`) читает образ кусками до `BOOT_TMPBUF_SZ=256` байт;
|
||||||
|
последняя итерация цикла — остаток `size - off`, в данном случае 7 байт (последние байты хэшируемой
|
||||||
|
области `header+img_size+protected_tlv`). Эта короткая, **не кратная 4** длина ни разу не встречалась
|
||||||
|
раньше: JEDEC ID и чтения статус-регистров всегда используют `SR_READ_LEN=4` (уже word-aligned), а
|
||||||
|
`firmware_test`/`test_qspi.c` тоже, судя по всему, ни разу не запрашивал не кратный 4 размер. LUT
|
||||||
|
`LSEQ_IP_READ` (`qspi_flash.c`) использует `READ_SDR` с operand `0x04` — похоже, что при `IDATSZ`, не
|
||||||
|
кратном 4, контроллер отдаёт ровно один "бит" этой инструкции и не дотягивает до второго (частичного)
|
||||||
|
слова.
|
||||||
|
|
||||||
|
**Исправлено** в `bsp/qspi_flash/src/qspi_flash.c::qspi_ip_read()` — `IDATSZ`, который уходит в
|
||||||
|
железо (`qspi_ip_setup`), теперь округляется вверх до кратного `QSPI_RFDR_WORD_BYTES` (4); из FIFO
|
||||||
|
`qspi_read_fifo()`/`qspi_read_tail()` по-прежнему извлекают ровно исходное (не округлённое) число
|
||||||
|
байт — лишний padding-байт молча дренируется вместе со словом и отбрасывается существующей логикой
|
||||||
|
извлечения, менять её не понадобилось. Затрагивает **все** IP-чтения произвольной длины через
|
||||||
|
`bsp_qspi_read()`, не только bootutil — потенциально тот же баг мог бы всплыть и в `firmware_test`,
|
||||||
|
если бы там когда-нибудь понадобилось прочитать не кратное 4 число байт.
|
||||||
|
|
||||||
|
**Важно**: после этого фикса на сценарии 1 всплыл ещё один, третий баг (ниже) — оба фикса стоят в
|
||||||
|
дереве вместе, изолированно друг от друга на железе не перепроверялись.
|
||||||
|
|
||||||
|
**Урок**: любая ручная "гигиена" вокруг прыжка (маскирование прерываний, сброс периферии и т.п.),
|
||||||
|
не присутствующая в проверенном референсе, — повод для отдельного вопроса "а точно ли это
|
||||||
|
симметрично восстанавливается на другой стороне", а не молчаливого добавления "на всякий случай".
|
||||||
|
|
||||||
|
### Третий найденный баг: зависание переехало на чтение подписи — `qspi_read_tail` сравнивал FILL в юнитах напрямую со счётчиком слов
|
||||||
|
|
||||||
|
После фикса второго бага зависание не пропало, а переехало дальше по стеку — тот же паттерн
|
||||||
|
(`qspi_read_tail`, `FILL` намертво на `1`), но уже на чтении TLV с ECDSA-P256 подписью в
|
||||||
|
`bootutil_img_validate` (не в `bootutil_img_hash`). Ключевой момент: для этого чтения `IDATSZ`,
|
||||||
|
уходящий в железо, уже был кратен 4 (72 байта) — гипотеза про word-alignment из второго бага оказалась
|
||||||
|
неполной.
|
||||||
|
|
||||||
|
Настоящая причина: `IPRXFSTS.FILL` считает не слова, а watermark-юниты по 8 байт (2 слова) — как и
|
||||||
|
документирует `FLEXSPI_GetFifoCounts()` в SDK-драйвере (`fsl_flexspi.h`, домножает то же поле на 8
|
||||||
|
при переводе в байты). `qspi_read_tail()` сравнивал `FILL` напрямую со счётчиком нужных слов, без
|
||||||
|
перевода единиц — зависал именно тогда, когда хвост требовал ровно 2 слова (невидимо для всех чтений,
|
||||||
|
которым достаточно 1 слова, включая все SR/JEDEC-чтения — отсюда и не проявлялось раньше).
|
||||||
|
|
||||||
|
**Исправлено** в той же функции `qspi_read_tail()` — сравнение переведено в слова
|
||||||
|
(`FILL_UNITS * QSPI_WM_UNIT_WORDS >= WORDS_NEEDED`), тем же паттерном, что уже использовался в
|
||||||
|
`qspi_read_fifo()`. Полный разбор, включая то, как гипотеза была подтверждена без доступа к живому
|
||||||
|
регистру через отладчик (упёрлись в ограничение карты памяти pyOCD-таргета `mimxrt1050_quadspi`) —
|
||||||
|
[DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md).
|
||||||
|
|
||||||
|
**Подтверждено на железе**: со всеми тремя фиксами сценарий 1, а следом и оставшиеся четыре сценария
|
||||||
|
чек-листа прошли (2026-07-09). Фаза 2 аппаратно верифицирована полностью.
|
||||||
|
|
||||||
**Верификация — до всякого железа** (план, для истории):
|
**Верификация — до всякого железа** (план, для истории):
|
||||||
- Host-юнит-тесты (`tests/host/`, Unity + fff, по образцу существующих `tests/host/protocol/`,
|
- Host-юнит-тесты (`tests/host/`, Unity + fff, по образцу существующих `tests/host/protocol/`,
|
||||||
|
|
|
||||||
|
|
@ -52,16 +52,22 @@ set(MCUBOOT_BOOTUTIL_INCLUDES
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
# Обход бага clang 22.1.8 (Homebrew): -fsanitize=address,undefined ломает
|
# Опции компиляции только для вендоренных исходников (не для нашего кода —
|
||||||
# генерацию CFI-директив на некоторых больших функциях bootutil (напр.
|
# main.c/cli.c/boot_select.c и т.д. компилируются с обычными warnings/ASan
|
||||||
# loader.c::context_boot_go) — "invalid CFI advance_loc expression" на
|
# консьюмера):
|
||||||
# этапе ассемблирования. Без санитайзеров те же файлы собираются чисто —
|
# -w — вендоренный код, не наш стиль/lint
|
||||||
# похоже на баг конкретной версии тулчейна, а не проблему в bootutil или
|
# -fno-sanitize=address,undefined — обход бага clang 22.1.8 (Homebrew):
|
||||||
# нашем коде. Отключаем санитайзеры только для вендоренных исходников
|
# -fsanitize=address,undefined ломает генерацию CFI-директив на
|
||||||
# bootutil/TinyCrypt/ASN.1 (не для нашего кода — там ASan/UBSan остаются).
|
# некоторых больших функциях bootutil (напр. loader.c::context_boot_go)
|
||||||
|
# — "invalid CFI advance_loc expression" на этапе ассемблирования. Без
|
||||||
|
# санитайзеров те же файлы собираются чисто — похоже на баг конкретной
|
||||||
|
# версии тулчейна. Не применимо к arm-none-eabi-gcc (ARM-таргет не
|
||||||
|
# использует ASan) — гейтим по Clang.
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
|
set(MCUBOOT_VENDORED_COMPILE_OPTIONS -w)
|
||||||
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
|
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||||
|
list(APPEND MCUBOOT_VENDORED_COMPILE_OPTIONS -fno-sanitize=address,undefined)
|
||||||
|
endif()
|
||||||
set_source_files_properties(${MCUBOOT_BOOTUTIL_SOURCES}
|
set_source_files_properties(${MCUBOOT_BOOTUTIL_SOURCES}
|
||||||
PROPERTIES COMPILE_OPTIONS
|
PROPERTIES COMPILE_OPTIONS
|
||||||
"-fno-sanitize=address,undefined")
|
"${MCUBOOT_VENDORED_COMPILE_OPTIONS}")
|
||||||
endif()
|
|
||||||
|
|
|
||||||
237
firmware/bootloader/mcuboot_port/flash_map_backend.c
Normal file
237
firmware/bootloader/mcuboot_port/flash_map_backend.c
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
/**
|
||||||
|
* @file flash_map_backend.c
|
||||||
|
* @brief Реализация flash_map.h поверх bsp_qspi_flash — Slot A/Б из
|
||||||
|
* docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md.
|
||||||
|
*
|
||||||
|
* bsp_qspi_read/write_page/erase_sector() принимают flash-relative адрес
|
||||||
|
* (0-based от начала чипа, IPCR0 FlexSPI IP-команд) — НЕ XIP-адрес
|
||||||
|
* (0x60000000+). fa_off здесь — то же самое flash-relative смещение.
|
||||||
|
* flash_device_base() — единственное место, где встречается XIP-адрес
|
||||||
|
* 0x60000000: он нужен boot_select.c для вычисления адреса прыжка
|
||||||
|
* (flash_base + fa_off + hdr_size), но не самим read/write/erase.
|
||||||
|
*
|
||||||
|
* @pre bsp_qspi_init() должен быть вызван до любой flash_area_* функции
|
||||||
|
* (main.c, до boot_go()).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "flash_map.h"
|
||||||
|
#include "sysflash/sysflash.h"
|
||||||
|
|
||||||
|
#include "bsp/qspi_flash.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define ERASED_VAL 0xFFU
|
||||||
|
|
||||||
|
/* Slot A/Б — см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md. Смещения —
|
||||||
|
* flash-relative (от начала чипа), не XIP-адрес. */
|
||||||
|
static const struct flash_area g_s_areas[2] = {
|
||||||
|
{ .fa_id = 0U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U,
|
||||||
|
.fa_off = 0x00040000UL, .fa_size = 0x00200000UL }, /* Slot A: 0x60040000, 2 МБ */
|
||||||
|
{ .fa_id = 1U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U,
|
||||||
|
.fa_off = 0x00240000UL, .fa_size = 0x00200000UL }, /* Slot Б: 0x60240000, 2 МБ */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── Постраничная запись (аналог NXP flash_area_write_internal) ─────────
|
||||||
|
*
|
||||||
|
* bsp_qspi_write_page() пишет ровно BSP_QSPI_PAGE_SIZE (256) байт по
|
||||||
|
* странично-выровненному адресу. Запись 0xFF поверх уже запрограммированных
|
||||||
|
* байт — не изменяет их (NOR program может только сбрасывать биты 1→0,
|
||||||
|
* запись 0xFF не запрашивает сброс ни одного бита) — поэтому безопасно
|
||||||
|
* "перезатирать" уже записанную часть страницы буфером, где нетронутая
|
||||||
|
* часть заполнена ERASED_VAL: bootutil пишет монотонно возрастающими
|
||||||
|
* смещениями, повторно данные не перезаписывает.
|
||||||
|
*/
|
||||||
|
static int write_page_chunked(uint32_t dst_addr, const uint8_t *p_src, uint32_t len)
|
||||||
|
{
|
||||||
|
uint8_t page_buf[BSP_QSPI_PAGE_SIZE];
|
||||||
|
|
||||||
|
uint32_t chunk_ofs = dst_addr % BSP_QSPI_PAGE_SIZE;
|
||||||
|
uint32_t page_addr = dst_addr - chunk_ofs;
|
||||||
|
uint32_t chunk_size = BSP_QSPI_PAGE_SIZE - chunk_ofs;
|
||||||
|
|
||||||
|
while (len > 0U)
|
||||||
|
{
|
||||||
|
if (chunk_size > len)
|
||||||
|
{
|
||||||
|
chunk_size = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(page_buf, ERASED_VAL, BSP_QSPI_PAGE_SIZE);
|
||||||
|
memcpy(page_buf + chunk_ofs, p_src, chunk_size);
|
||||||
|
|
||||||
|
if (bsp_qspi_write_page(page_addr, page_buf) != BSP_OK)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
p_src += chunk_size;
|
||||||
|
len -= chunk_size;
|
||||||
|
chunk_ofs = 0U;
|
||||||
|
chunk_size = BSP_QSPI_PAGE_SIZE;
|
||||||
|
page_addr += BSP_QSPI_PAGE_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── flash_map.h contract ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
int flash_device_base(uint8_t fd_id, uintptr_t *ret)
|
||||||
|
{
|
||||||
|
if (fd_id != FLASH_DEVICE_ID)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*ret = 0x60000000UL; /* XIP-mapped база — только для вычисления адреса прыжка */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_open(uint8_t id, const struct flash_area **area)
|
||||||
|
{
|
||||||
|
if (id >= 2U)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*area = &g_s_areas[id];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void flash_area_close(const struct flash_area *area)
|
||||||
|
{
|
||||||
|
(void) area;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
|
||||||
|
{
|
||||||
|
if (off + len > area->fa_size)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (bsp_qspi_read(area->fa_off + off, (uint8_t *) dst, len) == BSP_OK) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len)
|
||||||
|
{
|
||||||
|
if (off + len > area->fa_size)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return write_page_chunked(area->fa_off + off, (const uint8_t *) src, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len)
|
||||||
|
{
|
||||||
|
if ((off + len > area->fa_size) || ((off % BSP_QSPI_SECTOR_SIZE) != 0U) ||
|
||||||
|
((len % BSP_QSPI_SECTOR_SIZE) != 0U))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t addr = area->fa_off + off;
|
||||||
|
for (; len > 0U; len -= BSP_QSPI_SECTOR_SIZE)
|
||||||
|
{
|
||||||
|
if (bsp_qspi_erase_sector(addr) != BSP_OK)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
addr += BSP_QSPI_SECTOR_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t flash_area_align(const struct flash_area *area)
|
||||||
|
{
|
||||||
|
(void) area;
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t flash_area_erased_val(const struct flash_area *area)
|
||||||
|
{
|
||||||
|
(void) area;
|
||||||
|
return ERASED_VAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
|
||||||
|
{
|
||||||
|
if (flash_area_read(area, off, dst, len) != 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *p_buf = (const uint8_t *) dst;
|
||||||
|
for (uint32_t i = 0U; i < len; i++)
|
||||||
|
{
|
||||||
|
if (p_buf[i] != ERASED_VAL)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector)
|
||||||
|
{
|
||||||
|
if (off >= fa->fa_size)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sector->fs_off = (off / BSP_QSPI_SECTOR_SIZE) * BSP_QSPI_SECTOR_SIZE;
|
||||||
|
sector->fs_size = BSP_QSPI_SECTOR_SIZE;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors)
|
||||||
|
{
|
||||||
|
const struct flash_area *fa;
|
||||||
|
uint32_t max_cnt = *count;
|
||||||
|
|
||||||
|
if (flash_area_open((uint8_t) fa_id, &fa) != 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t rem_len = fa->fa_size;
|
||||||
|
*count = 0U;
|
||||||
|
while ((rem_len > 0U) && (*count < max_cnt))
|
||||||
|
{
|
||||||
|
sectors[*count].fs_off = BSP_QSPI_SECTOR_SIZE * (*count);
|
||||||
|
sectors[*count].fs_size = BSP_QSPI_SECTOR_SIZE;
|
||||||
|
(*count)++;
|
||||||
|
rem_len -= BSP_QSPI_SECTOR_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_id_from_multi_image_slot(int image_index, int slot)
|
||||||
|
{
|
||||||
|
switch (slot)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return FLASH_AREA_IMAGE_PRIMARY(image_index);
|
||||||
|
case 1:
|
||||||
|
return FLASH_AREA_IMAGE_SECONDARY(image_index);
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_id_from_image_slot(int slot)
|
||||||
|
{
|
||||||
|
return flash_area_id_from_multi_image_slot(0, slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
int flash_area_id_to_multi_image_slot(int image_index, int area_id)
|
||||||
|
{
|
||||||
|
if (area_id == FLASH_AREA_IMAGE_PRIMARY(image_index))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (area_id == FLASH_AREA_IMAGE_SECONDARY(image_index))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
61
firmware/bootloader/src/boot_select.c
Normal file
61
firmware/bootloader/src/boot_select.c
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* @file boot_select.c
|
||||||
|
* @brief bootutil boot_go() + прыжок в выбранный образ.
|
||||||
|
*
|
||||||
|
* Референс — sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot():
|
||||||
|
* та же последовательность (flash_device_base → вычислить адрес vector table →
|
||||||
|
* __set_MSP → __ISB → прыжок на Reset_Handler), CMSIS-интринсики, без ассемблера.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "boot_select.h"
|
||||||
|
|
||||||
|
#include "bootutil/bootutil.h"
|
||||||
|
#include "bootutil/fault_injection_hardening.h"
|
||||||
|
#include "flash_map.h"
|
||||||
|
|
||||||
|
#include "fsl_common.h"
|
||||||
|
|
||||||
|
struct arm_vector_table
|
||||||
|
{
|
||||||
|
uint32_t msp;
|
||||||
|
uint32_t reset;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void jump_to_image(const struct boot_rsp *p_rsp)
|
||||||
|
{
|
||||||
|
uintptr_t flash_base;
|
||||||
|
if (flash_device_base(p_rsp->br_flash_dev_id, &flash_base) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct arm_vector_table *p_vt = (const struct arm_vector_table *) (flash_base +
|
||||||
|
p_rsp->br_image_off +
|
||||||
|
p_rsp->br_hdr->ih_hdr_size);
|
||||||
|
|
||||||
|
/* Намеренно НЕ __disable_irq() здесь (в отличие от более ранней версии).
|
||||||
|
* bsp_delay() в целевом образе — busy-wait на счётчике, инкрементируемом
|
||||||
|
* из SysTick_Handler (см. bsp/tick/src/tick.c); обычный Reset_Handler не
|
||||||
|
* трогает PRIMASK, рассчитывая, что прерывания уже разрешены (как после
|
||||||
|
* реального аппаратного ресета). Если замаскировать IRQ здесь и не
|
||||||
|
* восстановить в целевом образе — SysTick не сработает ни разу, и любой
|
||||||
|
* bsp_delay() внутри целевого образа зависнет навсегда. NXP-референс
|
||||||
|
* (do_boot()) тоже не трогает PRIMASK — только __set_CONTROL/__set_MSP. */
|
||||||
|
__set_CONTROL(0U);
|
||||||
|
__set_MSP(p_vt->msp);
|
||||||
|
__ISB();
|
||||||
|
((void (*)(void)) p_vt->reset)();
|
||||||
|
}
|
||||||
|
|
||||||
|
void boot_select_and_jump(void)
|
||||||
|
{
|
||||||
|
struct boot_rsp rsp;
|
||||||
|
fih_ret fih_rc = boot_go(&rsp);
|
||||||
|
|
||||||
|
if (!FIH_EQ(fih_rc, FIH_SUCCESS))
|
||||||
|
{
|
||||||
|
return; /* нет валидного образа — main.c продолжит ping/pong-цикл */
|
||||||
|
}
|
||||||
|
|
||||||
|
jump_to_image(&rsp); /* при успехе не возвращается */
|
||||||
|
}
|
||||||
21
firmware/bootloader/src/boot_select.h
Normal file
21
firmware/bootloader/src/boot_select.h
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* @file boot_select.h
|
||||||
|
* @brief Выбор и запуск образа tft_app через bootutil (Direct-XIP).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BOOT_SELECT_H_
|
||||||
|
#define BOOT_SELECT_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Выбрать образ (bootutil boot_go, Direct-XIP) и прыгнуть в него.
|
||||||
|
*
|
||||||
|
* При успехе не возвращается — управление переходит в выбранный образ.
|
||||||
|
* При провале (нет валидного образа ни в одном слоте, или оба слота стёрты
|
||||||
|
* bootutil'ом из-за незавершённого revert) — возвращается, чтобы main.c
|
||||||
|
* мог продолжить в ping/pong-цикл (задел на состояние "жду SD" Фазы 3).
|
||||||
|
*
|
||||||
|
* @pre bsp_qspi_init() уже вызван.
|
||||||
|
*/
|
||||||
|
void boot_select_and_jump(void);
|
||||||
|
|
||||||
|
#endif /* BOOT_SELECT_H_ */
|
||||||
|
|
@ -2,26 +2,32 @@
|
||||||
* @file main.c
|
* @file main.c
|
||||||
* @brief bootloader — точка входа.
|
* @brief bootloader — точка входа.
|
||||||
*
|
*
|
||||||
* Фаза 1 (скелет): bring-up + USB CDC ACM + ping/get_version. Без доступа
|
* Фаза 2: минимальный bring-up → сразу попытка boot_go() (bootutil,
|
||||||
* к Flash-слотам, без bootutil, без SD — это добавится в Фазах 2-3.
|
* Direct-XIP) — так плата грузится в tft_app и без подключённого USB
|
||||||
|
* (нормальный полевой сценарий). Только если валидного образа нет ни в
|
||||||
|
* одном слоте — поднимаем USB CDC ACM для диагностики (JSON-lines, cli.c,
|
||||||
|
* урезанное подмножество протокола firmware_test) и остаёмся в ping/pong
|
||||||
|
* цикле. Это прообраз будущего состояния "жду SD" из Фазы 3 — сама
|
||||||
|
* SD-логика ещё не добавлена.
|
||||||
*
|
*
|
||||||
* Единственный канал хост↔плата: USB CDC ACM. Протокол: JSON-lines через
|
* Последовательность старта:
|
||||||
* cli.c (урезанное подмножество протокола firmware_test).
|
|
||||||
*
|
|
||||||
* Последовательность старта (зеркалит firmware/test/src/main.c):
|
|
||||||
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
||||||
* 2. bsp_led_init() — оба LED выключены
|
* 2. bsp_led_init() — оба LED выключены
|
||||||
* 3. bsp_tick_init() — SysTick 1 мс
|
* 3. bsp_tick_init() — SysTick 1 мс
|
||||||
* 4. bsp_usb_cdc_init() — PHY + стек + NVIC
|
* 4. bsp_qspi_init() — доступ к Slot A/Б
|
||||||
* 5. Ожидание CDC ready — LED_HEARTBEAT мигает
|
* 5. boot_select_and_jump() — при успехе не возвращается
|
||||||
* 6. cli_init() — сброс буфера
|
* 6. bsp_usb_cdc_init() — только если п.5 не сработал
|
||||||
* 7. Главный цикл — poll + cli_process
|
* 7. Ожидание CDC ready — LED_HEARTBEAT мигает
|
||||||
|
* 8. cli_init() — сброс буфера
|
||||||
|
* 9. Главный цикл — poll + cli_process
|
||||||
*
|
*
|
||||||
* Bootloader без SDRAM (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) — DCD
|
* Bootloader без SDRAM (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) — DCD
|
||||||
* не используется (bsp_boot_xip_no_dcd).
|
* не используется (bsp_boot_xip_no_dcd).
|
||||||
*/
|
*/
|
||||||
#include "board.h"
|
#include "board.h"
|
||||||
|
#include "boot_select.h"
|
||||||
#include "bsp/led.h"
|
#include "bsp/led.h"
|
||||||
|
#include "bsp/qspi_flash.h"
|
||||||
#include "bsp/tick.h"
|
#include "bsp/tick.h"
|
||||||
#include "bsp/usb_cdc.h"
|
#include "bsp/usb_cdc.h"
|
||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
|
|
@ -38,6 +44,12 @@ int main(void)
|
||||||
bsp_led_init();
|
bsp_led_init();
|
||||||
bsp_tick_init();
|
bsp_tick_init();
|
||||||
|
|
||||||
|
if (bsp_qspi_init() == BSP_OK)
|
||||||
|
{
|
||||||
|
boot_select_and_jump(); /* при успехе не возвращается */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Нет валидного образа ни в одном слоте — диагностический режим. */
|
||||||
if (bsp_usb_cdc_init() != BSP_OK)
|
if (bsp_usb_cdc_init() != BSP_OK)
|
||||||
{
|
{
|
||||||
bsp_led_toggle(LED_HEARTBEAT);
|
bsp_led_toggle(LED_HEARTBEAT);
|
||||||
|
|
|
||||||
49
firmware/bootloader/test_stub/CMakeLists.txt
Normal file
49
firmware/bootloader/test_stub/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# firmware/bootloader/test_stub/CMakeLists.txt
|
||||||
|
#
|
||||||
|
# Заглушка tft_app для аппаратной верификации Фазы 2 bootutil — см.
|
||||||
|
# firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2". Не часть
|
||||||
|
# продукта — удалить, когда появится реальный tft_app.
|
||||||
|
#
|
||||||
|
# Два таргета из одного main.c: разный адрес слота (--defsym __slot_base__)
|
||||||
|
# и разная частота мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой
|
||||||
|
# слот выбрал bootloader. Постройка .bin, дальше подписывается вручную
|
||||||
|
# imgtool'ом (см. PLAN.md) — .bin сюда не должен попасть без подписи.
|
||||||
|
|
||||||
|
function(add_mcuboot_stub NAME SLOT_BASE BLINK_MS)
|
||||||
|
add_executable(${NAME} main.c ${BSP_GENERATED}/clock_config.c
|
||||||
|
${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
|
||||||
|
|
||||||
|
target_compile_definitions(${NAME} PRIVATE STUB_BLINK_MS=${BLINK_MS}
|
||||||
|
__STARTUP_CLEAR_BSS)
|
||||||
|
|
||||||
|
target_link_libraries(${NAME} PRIVATE bsp_board bsp_led bsp_tick
|
||||||
|
bsp_boot_xip_no_dcd)
|
||||||
|
|
||||||
|
target_link_options(
|
||||||
|
${NAME}
|
||||||
|
PRIVATE
|
||||||
|
-Wl,--gc-sections
|
||||||
|
-Wl,--print-memory-usage
|
||||||
|
-Wl,-Map=${CMAKE_BINARY_DIR}/${NAME}.map
|
||||||
|
-Wl,--defsym=__slot_base__=${SLOT_BASE}
|
||||||
|
-Wl,--defsym=__stack_size__=0x400
|
||||||
|
-Wl,--defsym=__heap_size__=0x400
|
||||||
|
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld)
|
||||||
|
|
||||||
|
set_target_properties(${NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
|
||||||
|
${CMAKE_BINARY_DIR})
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET ${NAME}
|
||||||
|
POST_BUILD
|
||||||
|
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${NAME}>
|
||||||
|
${CMAKE_BINARY_DIR}/${NAME}.bin
|
||||||
|
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${NAME}>
|
||||||
|
COMMENT "Generating ${NAME}.bin (не подписан — imgtool sign вручную, см. PLAN.md)")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Slot A: 0x60040000, мигает раз в 500 мс ("версия 1")
|
||||||
|
add_mcuboot_stub(test_slot_stub_a 0x60040000 500)
|
||||||
|
|
||||||
|
# Slot Б: 0x60240000, мигает раз в 250 мс ("версия 2")
|
||||||
|
add_mcuboot_stub(test_slot_stub_b 0x60240000 250)
|
||||||
120
firmware/bootloader/test_stub/HARDWARE_VERIFICATION_PHASE2.md
Normal file
120
firmware/bootloader/test_stub/HARDWARE_VERIFICATION_PHASE2.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Фаза 2 (bootutil / MCUboot Direct-XIP) — аппаратная верификация
|
||||||
|
|
||||||
|
Памятка с готовыми командами: сборка, прошивка, стирание, чек-лист.
|
||||||
|
|
||||||
|
Все команды — из корня репозитория (`tft_manufacture_test/`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Предпосылки
|
||||||
|
|
||||||
|
- `firmware/bootloader` собран с ARM-стороной Фазы 2 (bootutil + `flash_map_backend.c` над
|
||||||
|
`bsp_qspi_flash` + `boot_select.c`). `main.c` пытается `boot_go()` сразу после минимального
|
||||||
|
bring-up, до подъёма USB CDC — так что плата грузит tft_app и без подключённого кабеля.
|
||||||
|
- Заглушка вместо ещё не существующего `tft_app` — `firmware/bootloader/test_stub/`: два образа,
|
||||||
|
различающиеся частотой мигания `LED_APP` (по частоте видно, какой слот реально выбрал bootloader).
|
||||||
|
- Оба образа подписаны тестовым sample-ключом MCUboot (`sdk/middleware/mcuboot_opensource/root-ec-p256.pem`)
|
||||||
|
— публичный, не для продакшена, годится только для этой проверки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Сборка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bootloader (Debug) + HAB-контейнер
|
||||||
|
just build::build-bootloader-debug
|
||||||
|
just build::hab-bootloader-debug
|
||||||
|
|
||||||
|
# Заглушки Slot A/Б — собрать И подписать imgtool'ом одной командой
|
||||||
|
just build::build-mcuboot-stub
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого в `build/Debug/` должны появиться:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bootloader_hab.bin
|
||||||
|
signed/stub_a_v1_confirmed.bin (2 МБ, v1.0.0, --confirm)
|
||||||
|
signed/stub_b_v2_confirmed.bin (2 МБ, v2.0.0, --confirm)
|
||||||
|
signed/stub_a_v1_unconfirmed.bin (2 МБ, v1.0.0, без --confirm — для revert)
|
||||||
|
```
|
||||||
|
|
||||||
|
Если файлов нет или размер не 2 МБ — пересобрать:
|
||||||
|
`rm -rf build/Debug && just build::build-bootloader-debug && just build::hab-bootloader-debug && just build::build-mcuboot-stub`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Прошивка bootloader
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just host::flash-swd-bootloader-debug
|
||||||
|
# обязателен power cycle платы после прошивки — SWD-запись не ресетит автоматически
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Прошивка образов в слоты (pyOCD)
|
||||||
|
|
||||||
|
Не через `flash_swd.py` — тот собирает FCB+IVT+HAB под `0x60000000`, слотам это не нужно (не
|
||||||
|
самостоятельный boot-образ для BootROM, а данные, которые читает `boot_go()`). Пишем сырой
|
||||||
|
подписанный `.bin` напрямую по адресу слота.
|
||||||
|
|
||||||
|
**Важно:** `uv run --directory tools/hil` меняет рабочую директорию у самого `pyocd`, не только у
|
||||||
|
`uv` — путь к `.bin` должен быть абсолютным (`"$(pwd)/..."`), иначе резолвится от `tools/hil/` и
|
||||||
|
получите `No such file`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Slot A (0x60040000) — валидный, confirmed
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60040000 --erase sector "$(pwd)/build/Debug/signed/stub_a_v1_confirmed.bin"
|
||||||
|
|
||||||
|
# Slot Б (0x60240000) — валидный, confirmed, версия новее
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60240000 --erase sector "$(pwd)/build/Debug/signed/stub_b_v2_confirmed.bin"
|
||||||
|
|
||||||
|
# Slot A — неподтверждённый вариант, для проверки revert (использовать ВМЕСТО confirmed-варианта)
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60040000 --erase sector "$(pwd)/build/Debug/signed/stub_a_v1_unconfirmed.bin"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Стирание слота
|
||||||
|
|
||||||
|
Адрес — позиционный аргумент (не `-a`), формат диапазона `start+length` (не `start@length`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--sector 0x60040000+0x200000 # Slot A, 2 МБ
|
||||||
|
|
||||||
|
uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--sector 0x60240000+0x200000 # Slot Б, 2 МБ
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Проверка "bootloader жив" (CDC)
|
||||||
|
|
||||||
|
Актуально для сценария 4 (оба слота пусты — bootloader не прыгает, остаётся в диагностическом цикле).
|
||||||
|
Подключиться к USB CDC ACM платы (см. [README.md](README.md#3-подключение)) любым терминалом:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
screen /dev/cu.usbmodemXXXX # macOS, порт свой у каждого подключения
|
||||||
|
```
|
||||||
|
|
||||||
|
Отправить `{"type":"cmd","cmd":"ping"}` — в ответ должно прийти `{"type":"pong"}`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Чек-лист сценариев
|
||||||
|
|
||||||
|
Между КАЖДЫМ сценарием — обязательный power cycle платы (SWD-запись не ресетит автоматически).
|
||||||
|
|
||||||
|
| № | Сценарий | Подготовка | Ожидаемый результат |
|
||||||
|
| --- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 1 | Валиден только Slot A | Erase Slot Б (шаг 4), `stub_a_v1_confirmed.bin` → Slot A (шаг 3) | `LED_APP` мигает ~1 раз/сек |
|
||||||
|
| 2 | Оба валидны, побеждает версия Б | + `stub_b_v2_confirmed.bin` → Slot Б | `LED_APP` мигает ~2 раза/сек |
|
||||||
|
| 3 | Slot Б повреждён | В Slot Б — испорченный файл (скопировать `stub_b_v2_confirmed.bin`, поменять один байт в payload, прошить тем же способом что и в шаге 3) | `LED_APP` возвращается к ~1 разу/сек (снова Slot A) |
|
||||||
|
| 4 | Оба слота пусты | Erase Slot A и Slot Б целиком (шаг 4, оба вызова) | `LED_APP` не мигает по образцу заглушки; `ping` по CDC (шаг 5) отвечает `pong` — bootloader не прыгнул, остался в своём цикле |
|
||||||
|
| 5 | Revert неподтверждённого образа | `stub_a_v1_unconfirmed.bin` → Slot A; power cycle (1) → LED мигает (образ выбран впервые, `copy_done` выставляется); power cycle (2) БЕЗ каких-либо действий между ними → Slot A должен быть стёрт bootutil'ом | После второго ресета — как сценарий 4 (LED не мигает, CDC ping жив) |
|
||||||
|
|
||||||
|
---
|
||||||
34
firmware/bootloader/test_stub/main.c
Normal file
34
firmware/bootloader/test_stub/main.c
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* @file main.c
|
||||||
|
* @brief Заглушка tft_app для аппаратной верификации Фазы 2 bootutil.
|
||||||
|
*
|
||||||
|
* tft_app ещё не реализована — bootloader'у некуда прыгать. Этот образ —
|
||||||
|
* минимальный, но настоящий imgtool-подписанный XIP-образ с корректным
|
||||||
|
* vector table по адресу слота: единственная задача — мигать LED_APP с
|
||||||
|
* периодом, зависящим от STUB_BLINK_MS (задаётся компилятору), чтобы по
|
||||||
|
* частоте мигания визуально отличить, какой слот реально выбрал
|
||||||
|
* bootloader. См. firmware/bootloader/PLAN.md, "Аппаратная верификация
|
||||||
|
* Фазы 2".
|
||||||
|
*
|
||||||
|
* Без USB/CDC — визуальной индикации достаточно, минимальный код.
|
||||||
|
*/
|
||||||
|
#include "board.h"
|
||||||
|
#include "bsp/led.h"
|
||||||
|
#include "bsp/tick.h"
|
||||||
|
|
||||||
|
#ifndef STUB_BLINK_MS
|
||||||
|
#error "STUB_BLINK_MS must be defined (see firmware/bootloader/test_stub/CMakeLists.txt)"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
board_hw_init();
|
||||||
|
bsp_led_init();
|
||||||
|
bsp_tick_init();
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
bsp_led_toggle(LED_APP);
|
||||||
|
bsp_delay(STUB_BLINK_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -236,6 +236,35 @@ hab-verify project="firmware_test" type="release":
|
||||||
echo " ✅ ${OUT}"
|
echo " ✅ ${OUT}"
|
||||||
grep -E "(entry|csf|tag)" "${OUT}" || true
|
grep -E "(entry|csf|tag)" "${OUT}" || true
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ГРУППА: mcuboot-stub — заглушка tft_app для аппаратной верификации bootutil
|
||||||
|
# (Фаза 2, firmware/bootloader/PLAN.md). Временное — удалить вместе с
|
||||||
|
# firmware/bootloader/test_stub/, когда появится реальный firmware/tft_app.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
MCUBOOT_ROOT := justfile_directory() / 'sdk/middleware/mcuboot_opensource'
|
||||||
|
MCUBOOT_KEY := MCUBOOT_ROOT / 'root-ec-p256.pem'
|
||||||
|
MCUBOOT_IMGTOOL := MCUBOOT_ROOT / 'scripts/imgtool.py'
|
||||||
|
# imgtool не входит в зависимости tools/host (spsdk) — ставится эфемерно через uv --with,
|
||||||
|
# не трогая основной venv (см. firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2").
|
||||||
|
MCUBOOT_UV := 'uv run --with cryptography --with intelhex --with click --with cbor2 --with pyyaml python3'
|
||||||
|
|
||||||
|
[doc('Собрать и подписать заглушки Slot A/Б для аппаратной верификации bootutil (Debug)')]
|
||||||
|
[group('mcuboot-stub')]
|
||||||
|
build-mcuboot-stub: _configure-debug
|
||||||
|
cmake --build --preset mcuboot-stub-debug
|
||||||
|
mkdir -p "{{ BUILD_DIR }}/Debug/signed"
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 1.0.0 --align 1 --pad-header --pad --confirm \
|
||||||
|
"{{ BUILD_DIR }}/Debug/test_slot_stub_a.bin" "{{ BUILD_DIR }}/Debug/signed/stub_a_v1_confirmed.bin"
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 2.0.0 --align 1 --pad-header --pad --confirm \
|
||||||
|
"{{ BUILD_DIR }}/Debug/test_slot_stub_b.bin" "{{ BUILD_DIR }}/Debug/signed/stub_b_v2_confirmed.bin"
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 1.0.0 --align 1 --pad-header --pad \
|
||||||
|
"{{ BUILD_DIR }}/Debug/test_slot_stub_a.bin" "{{ BUILD_DIR }}/Debug/signed/stub_a_v1_unconfirmed.bin"
|
||||||
|
@echo " ✅ build/Debug/signed/stub_{a,b}_*.bin готовы — прошивка через just host::flash-swd (см. PLAN.md)"
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ГРУППА: quality
|
# ГРУППА: quality
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# tools/hil
|
# tools/hil
|
||||||
|
|
||||||
Python-окружение на базе [uv](https://docs.astral.sh/uv/) для HIL-тестов,
|
Python-окружение на базе [uv](https://docs.astral.sh/uv/) для HIL-тестов и
|
||||||
GDB-сервера отладки и SWD-прошивки через `flash_swd.py`.
|
отладки MIMXRT1052 через pyOCD (SWD: загрузка ELF в RAM, GDB-сервер).
|
||||||
|
|
||||||
Запускается на **хост-машине** — не внутри devcontainer.
|
Запускается на **хост-машине** — не внутри devcontainer.
|
||||||
|
|
||||||
|
|
@ -11,26 +11,57 @@ GDB-сервера отладки и SWD-прошивки через `flash_swd.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tools/hil/
|
tools/hil/
|
||||||
├── conftest.py — pytest-фикстуры (m5, loaded_<n>, uart_<n>)
|
├── conftest.py — pytest-фикстуры (m5, loaded_<n>, uart_<n>, usb_cdc_port, firmware_cdc)
|
||||||
├── env_config.py — конфигурация из os.environ / .env
|
├── env_config.py — конфигурация из os.environ / .env
|
||||||
├── pyocd_utils.py — FLEXRAM init, ELF loader, run_from_vectors
|
├── pyocd_utils.py — FLEXRAM init, ELF loader, run_from_vectors (использует conftest.py и load_and_run.py)
|
||||||
├── load_and_run.py — CLI-утилита: загрузить ELF в RAM вручную
|
├── load_and_run.py — CLI-утилита: загрузить ELF в RAM вручную
|
||||||
├── 01_test_uart.py — HIL тест bsp_uart_host (без стенда)
|
├── 01_test_uart.py — bsp_uart_host, без внешнего стенда
|
||||||
├── 02_test_opto.py — HIL тест bsp_opto (через M5StampPLC)
|
├── 02_test_opto.py — bsp_opto через реле M5StampPLC
|
||||||
|
├── 03_test_can.py — bsp_can, M5StampPLC как второй узел CAN-шины
|
||||||
|
├── 04_test_button.py — bsp_button, интерактивный (кнопки нажимает оператор)
|
||||||
|
├── 05_test_usb_cdc.py — bsp_usb_cdc, требует физический USB-порт таргета
|
||||||
|
├── 06_test_firmware_can.py — test_can внутри firmware_test (протокол v2 через CDC)
|
||||||
|
├── 06_test_firmware_opto.py — test_opto внутри firmware_test (протокол v2 через CDC)
|
||||||
├── m5/
|
├── m5/
|
||||||
│ ├── agent.py — MicroPython агент на M5StampPLC (реле, входы, CAN)
|
│ ├── agent.py — MicroPython-агент на M5StampPLC (реле, входы, CAN)
|
||||||
│ ├── cli.py — интерактивный CLI для ручного тестирования стенда и таргета
|
│ ├── cli.py — интерактивный CLI для ручного тестирования стенда и таргета
|
||||||
│ ├── power.py — управление питанием таргета (RLY1) из командной строки
|
│ ├── power.py — управление питанием таргета (RLY1) из командной строки
|
||||||
│ └── firmware/ — прошивки MicroPython для M5StampPLC
|
│ └── firmware/ — прошивки MicroPython для M5StampPLC
|
||||||
│ ├── v1.25/ — MicroPython 1.25 — используется (поддерживает CAN)
|
│ ├── esp32s3_bl-v1.25.0_twai.bin — используется (поддерживает CAN)
|
||||||
│ └── v1.27/ — MicroPython 1.27 — CAN не поддерживается, не использовать
|
│ └── esp32s3_bl-v1.27.0.bin — CAN не поддерживается, не использовать
|
||||||
├── pyproject.toml
|
├── pyproject.toml
|
||||||
└── uv.lock
|
└── uv.lock
|
||||||
```
|
```
|
||||||
|
|
||||||
> **MicroPython на M5StampPLC:** использовать прошивку из `m5/firmware/v1.25/`.
|
> Числовой префикс в имени файла (`NN_test_*.py`, паттерн задан в
|
||||||
> В v1.27 модуль CAN недоступен — `agent.py` инициализирует CAN при старте,
|
> `pyproject.toml` → `python_files`) фиксирует порядок диагностики стенда —
|
||||||
> тесты с CAN не пройдут. Загрузить прошивку можно с помощью [утилиты](https://github.com/esp-rs/espflash)
|
> от простого автономного теста к более сложным и зависящим от внешнего
|
||||||
|
> оборудования.
|
||||||
|
|
||||||
|
> **MicroPython на M5StampPLC:** использовать
|
||||||
|
> `m5/firmware/esp32s3_bl-v1.25.0_twai.bin`. В прошивке 1.27 модуль CAN
|
||||||
|
> недоступен — `agent.py` инициализирует CAN при старте, тесты с CAN не
|
||||||
|
> пройдут. Загрузить прошивку можно с помощью
|
||||||
|
> [espflash](https://github.com/esp-rs/espflash).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Тестовые файлы
|
||||||
|
|
||||||
|
| Файл | Что проверяет | Внешний стенд | Маркер |
|
||||||
|
| --------------------------- | ------------------------------------------------- | ---------------------------------- | ------------- |
|
||||||
|
| `01_test_uart.py` | `bsp_uart_host` | нет (M5 только включает питание) | — |
|
||||||
|
| `02_test_opto.py` | `bsp_opto` | M5StampPLC (реле → оптопары) | — |
|
||||||
|
| `03_test_can.py` | `bsp_can` | M5StampPLC (второй узел CAN-шины) | — |
|
||||||
|
| `04_test_button.py` | `bsp_button` | нет, кнопки нажимает оператор | `interactive` |
|
||||||
|
| `05_test_usb_cdc.py` | `bsp_usb_cdc` | физический USB-порт таргета | `usb_vcom` |
|
||||||
|
| `06_test_firmware_can.py` | `test_can` внутри `firmware_test` (протокол v2) | M5StampPLC | `usb_vcom` |
|
||||||
|
| `06_test_firmware_opto.py` | `test_opto` внутри `firmware_test` (протокол v2) | M5StampPLC | `usb_vcom` |
|
||||||
|
|
||||||
|
Подробности о том, что именно проверяется и какие гарантии даёт каждый
|
||||||
|
тест — в `tests/target/*/README.md` рядом с соответствующей HIL-прошивкой
|
||||||
|
(`06_test_firmware_*.py` — исключение: они гоняют `test_can`/`test_opto`
|
||||||
|
внутри `firmware_test`, а не прошивки из `tests/target`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -39,10 +70,11 @@ tools/hil/
|
||||||
Полное описание стека HIL-тестирования — в `docs/testing/hil/`:
|
Полное описание стека HIL-тестирования — в `docs/testing/hil/`:
|
||||||
|
|
||||||
| Документ | Содержимое |
|
| Документ | Содержимое |
|
||||||
| --------------------------------------------------------------- | ---------------------------------------------- |
|
| ------------------------------------------------------------------ | ----------------------------------------------- |
|
||||||
| [HIL_HOW_TO.md](../../docs/testing/hil/HIL_HOW_TO.md) | Как запускать HIL-тесты (пошагово) |
|
| [HIL_HOW_TO.md](../../docs/testing/hil/HIL_HOW_TO.md) | Как запускать HIL-тесты (пошагово) |
|
||||||
| [HIL_BENCH.md](../../docs/testing/hil/HIL_BENCH.md) | Стенд: оборудование, подключение, маппинг реле |
|
| [HIL_BENCH.md](../../docs/testing/hil/HIL_BENCH.md) | Стенд: оборудование, подключение, маппинг реле |
|
||||||
| [HIL_CREATE_TEST.md](../../docs/testing/hil/HIL_CREATE_TEST.md) | Как добавить новый HIL-тест |
|
| [HIL_CREATE_TEST.md](../../docs/testing/hil/HIL_CREATE_TEST.md) | Как добавить новый HIL-тест |
|
||||||
|
| [HIL_FIXTURES.md](../../docs/testing/hil/HIL_FIXTURES.md) | Памятка по pytest-фикстурам применительно к HIL |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -52,15 +84,39 @@ tools/hil/
|
||||||
# Первый раз: установить зависимости
|
# Первый раз: установить зависимости
|
||||||
cd tools/hil && uv sync
|
cd tools/hil && uv sync
|
||||||
|
|
||||||
# Заполнить .env в корне репозитория (порты MCU-Link и M5)
|
# Заполнить .env в корне репозитория (см. .env.example — порты MCU-Link и M5)
|
||||||
|
|
||||||
# Задеплоить агент на M5 (после изменений agent.py — повторить)
|
# Задеплоить агент на M5 (после изменений agent.py — повторить)
|
||||||
just host::m5-deploy
|
just host::m5-deploy
|
||||||
|
|
||||||
# devcontainer: собрать HIL ELF
|
# devcontainer: собрать HIL-прошивки
|
||||||
just build::build-hil
|
just build::build-hil
|
||||||
|
|
||||||
# хост: запустить тесты
|
# хост: запустить тесты
|
||||||
just host::hil-run
|
just host::hil-run # все не интерактивные и не usb_vcom тесты
|
||||||
just host::hil-uart # только UART тесты
|
just host::hil-run-interactive # интерактивные (кнопки) — нужен оператор у стенда
|
||||||
just host::hil-opto # только opto тесты
|
just host::hil-uart
|
||||||
|
just host::hil-opto
|
||||||
|
just host::hil-can
|
||||||
|
just host::hil-button
|
||||||
|
just host::hil-usb-cdc
|
||||||
|
just host::hil-firmware-opto # test_opto через firmware_test (протокол v2)
|
||||||
|
just host::hil-firmware-can # test_can через firmware_test (протокол v2)
|
||||||
|
just host::hil-firmware # оба сразу
|
||||||
|
```
|
||||||
|
|
||||||
|
### Отладка без pytest
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just host::debug-server # GDB-сервер pyOCD — оставить запущенным, подключаться из VSCode
|
||||||
|
just host::debug-list-targets # доступные builtin-таргеты pyOCD для MIMXRT
|
||||||
|
uv run python load_and_run.py firmware_test.elf # загрузить ELF в RAM вручную
|
||||||
|
```
|
||||||
|
|
||||||
|
### Работа со стендом M5StampPLC
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just host::m5-scan # показать подключённые M5Stack-устройства
|
||||||
|
just host::m5-cli # интерактивный CLI для ручного тестирования агента
|
||||||
|
just host::m5-power on # управление питанием таргета через RLY1 (on|off)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue