From 17698ce10925b8c648187f38cc823fce5e805ebb Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Fri, 10 Jul 2026 15:02:18 +0300 Subject: [PATCH] # bootloader: Phase 3 - checked and done 580e5f5 --- CMakePresets.json | 8 +- bsp/generated/sdmmc_config.c | 46 +- bsp/qspi_flash/src/qspi_flash.c | 24 +- bsp/sd/include/bsp/sd.h | 7 +- bsp/sd/src/sd.c | 62 +-- docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md | 46 +- firmware/bootloader/CMakeLists.txt | 12 +- firmware/bootloader/DEBUG_LOG_PHASE3_SD.md | 507 ++++++++++++++++++ firmware/bootloader/PLAN.md | 205 ++++++- firmware/bootloader/README.md | 92 ++-- firmware/bootloader/fatfs/CMakeLists.txt | 35 ++ firmware/bootloader/fatfs/include/ffconf.h | 86 +++ firmware/bootloader/fatfs/src/diskio.c | 53 ++ .../mcuboot_port/flash_map_backend.c | 39 +- firmware/bootloader/src/boot_select.c | 74 ++- firmware/bootloader/src/main.c | 95 +++- firmware/bootloader/src/protocol.c | 7 + firmware/bootloader/src/protocol.h | 17 +- firmware/bootloader/src/sd_update.c | 253 +++++++++ firmware/bootloader/src/sd_update.h | 28 + firmware/bootloader/src/slot_version.c | 65 +++ firmware/bootloader/src/slot_version.h | 37 ++ firmware/bootloader/src/update_policy.c | 100 ++++ firmware/bootloader/src/update_policy.h | 95 ++++ .../test_stub/HARDWARE_VERIFICATION_PHASE3.md | 197 +++++++ .../bootutil/src/fault_injection_hardening.c | 13 + tests/host/CMakeLists.txt | 54 ++ tests/host/slot_version/README.md | 62 +++ tests/host/slot_version/test_slot_version.c | 138 +++++ tests/host/update_policy/README.md | 55 ++ tests/host/update_policy/test_update_policy.c | 222 ++++++++ tools/service_tui/spike/output_macos.md | 90 ---- tools/service_tui/spike/output_windows.md | 80 --- tools/service_tui/spike/spike_flash.py | 190 ------- tools/service_tui/spike/spike_hab.py | 216 -------- tools/service_tui/spike/spike_readback.py | 70 --- 36 files changed, 2562 insertions(+), 818 deletions(-) create mode 100644 firmware/bootloader/DEBUG_LOG_PHASE3_SD.md create mode 100644 firmware/bootloader/fatfs/CMakeLists.txt create mode 100644 firmware/bootloader/fatfs/include/ffconf.h create mode 100644 firmware/bootloader/fatfs/src/diskio.c create mode 100644 firmware/bootloader/src/sd_update.c create mode 100644 firmware/bootloader/src/sd_update.h create mode 100644 firmware/bootloader/src/slot_version.c create mode 100644 firmware/bootloader/src/slot_version.h create mode 100644 firmware/bootloader/src/update_policy.c create mode 100644 firmware/bootloader/src/update_policy.h create mode 100644 firmware/bootloader/test_stub/HARDWARE_VERIFICATION_PHASE3.md create mode 100644 tests/host/slot_version/README.md create mode 100644 tests/host/slot_version/test_slot_version.c create mode 100644 tests/host/update_policy/README.md create mode 100644 tests/host/update_policy/test_update_policy.c delete mode 100644 tools/service_tui/spike/output_macos.md delete mode 100644 tools/service_tui/spike/output_windows.md delete mode 100644 tools/service_tui/spike/spike_flash.py delete mode 100644 tools/service_tui/spike/spike_hab.py delete mode 100644 tools/service_tui/spike/spike_readback.py diff --git a/CMakePresets.json b/CMakePresets.json index b9e8c0b..b4b7ad1 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -160,7 +160,9 @@ "uart_host_mock_example", "test_ring_buffer", "test_timeout_pattern", - "test_mcuboot_boot_select" + "test_mcuboot_boot_select", + "test_slot_version", + "test_update_policy" ] }, { @@ -180,7 +182,9 @@ "uart_host_mock_example", "test_ring_buffer", "test_timeout_pattern", - "test_mcuboot_boot_select" + "test_mcuboot_boot_select", + "test_slot_version", + "test_update_policy" ] }, { diff --git a/bsp/generated/sdmmc_config.c b/bsp/generated/sdmmc_config.c index fb4dfa2..14c2df1 100644 --- a/bsp/generated/sdmmc_config.c +++ b/bsp/generated/sdmmc_config.c @@ -49,7 +49,8 @@ static uint32_t get_usdhc1_src_clock_hz(void) } /* - * Управление питанием карты: GPIO1[19] (SdPwr), active-high. + * Управление питанием карты: GPIO1[19] (SdPwr). Регистрируется как + * usrParam.pwr — SDK дёргает её из SD_SetCardPower(). */ static void sd_power_control(bool enable) { @@ -123,10 +124,20 @@ static void sd_pin_config(uint32_t freq) IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_03_USDHC1_DATA1, pad); IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad); IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad); - /* CD_B в GPIO-режиме: подтяжка вверх + hysteresis для стабильного уровня. */ - IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_12_GPIO2_IO28, - IOMUXC_SW_PAD_CTL_PAD_PKE_MASK | IOMUXC_SW_PAD_CTL_PAD_PUE_MASK | - IOMUXC_SW_PAD_CTL_PAD_HYS_MASK | IOMUXC_SW_PAD_CTL_PAD_PUS(1)); + /* + * CD_B pad config — байт-в-байт как TFT_BOOTLOADER::BOARD_SD_Pin_Config() + * (board/sdmmc_config.c: IOMUXC_SetPinConfig(..., 0x10B0U)), не "разумная + * по умолчанию" альтернатива. Отличие от прежней версии здесь: PKE=1 но + * PUE=0 — это KEEPER, не активная подтяжка (PUS игнорируется в этом + * режиме); HYS выключен. Прежняя версия (активная 47к подтяжка вверх + + * hysteresis) не была проверена на этой плате и не объяснила устойчивый + * ложный "card present" без карты на реальном железе (Фаза 3, симптом 1, + * см. DEBUG_LOG_PHASE3_SD.md) — pinmux сам по себе (GPIO2_IO28) это не + * лечит, раз симптом воспроизводится и после его отката. + */ + IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_12_GPIO2_IO28, IOMUXC_SW_PAD_CTL_PAD_PKE_MASK | + IOMUXC_SW_PAD_CTL_PAD_SPEED(2U) | + IOMUXC_SW_PAD_CTL_PAD_DSE(6U)); } /* --------------------------------------------------------------------------- @@ -166,7 +177,30 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u /* --- GPIO питания --- */ sd_power_init(); - /* CD_B: переводим в GPIO2_IO28 и настраиваем вход */ + /* + * CD_B (GPIO_B1_12 / physical D13) — на этой плате детект работает через + * DVA механизма в разные моменты (см. DEBUG_LOG_PHASE3_SD.md, разрешение + * от 2026-07-10): + * + * 1. НАШ гейт bsp_sd_is_inserted() (bsp/sd/src/sd.c) — читает USDHC + * PRES_STATE.CINST (USDHC_GetPresentStatusFlags). Это и есть настоящий + * фикс симптома 1: на пустом слоте BOARD_SD_Config() ещё не вызывался, + * пин остаётся на USDHC1_CD_B (замаплен в BOARD_InitPins()), и PRSSTAT + * отражает реальность корректно — гейт стабильно возвращает false, и + * блокирующий SD_PollingCardInsert() без карты просто не достигается. + * 2. Внутренний детект SDK (SD_PollingCardInsert внутри f_mount) — через + * GPIO-callback sd_card_detect_gpio() (GPIO_PinRead(GPIO2, 28)). Он + * достигается только ПОСЛЕ того, как гейт уже подтвердил карту, то + * есть уже после этого IOMUXC_SetPinMux ниже — тогда пин на GPIO2_IO28 + * и GPIO-чтение корректно. + * + * Отсюда remux ниже на GPIO2_IO28: он нужен именно для (2). Пин остаётся + * эксклюзивным (одна альт-функция разом), поэтому (1) и (2) физически + * работают в разные моменты на разной маршрутизации одного пина — это + * подтверждено на железе (все 5 сценариев Фазы 3 пройдены), но хрупко: + * см. "латентная хрупкость re-scan" в DEBUG_LOG_PHASE3_SD.md и там же — + * рекомендованная консолидация на единый механизм (PRSSTAT везде). + */ IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U); const gpio_pin_config_t cd_cfg = { .direction = kGPIO_DigitalInput, diff --git a/bsp/qspi_flash/src/qspi_flash.c b/bsp/qspi_flash/src/qspi_flash.c index 07747c7..dff23b0 100644 --- a/bsp/qspi_flash/src/qspi_flash.c +++ b/bsp/qspi_flash/src/qspi_flash.c @@ -116,8 +116,7 @@ * @brief Размер читаемого буфера для однобайтных SR-команд. * * FlexSPI FIFO работает минимальными единицами в 4 байта. Читаем 4 байта, - * используем только byte[0]. (Про размер watermark-юнита IPRXFSTS.FILL — - * см. QSPI_WM_UNIT_BYTES и комментарий в qspi_read_tail().) + * используем только byte[0]. */ #define SR_READ_LEN 4U @@ -339,13 +338,6 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_tail(uint8_t *p_dst, uint3 while (!done) { - /* 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; if ((FILL_UNITS * QSPI_WM_UNIT_WORDS) >= WORDS_NEEDED) @@ -516,19 +508,7 @@ 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, uint8_t *p_rx, uint32_t 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-байт молча дренируется вместе со словом и отбрасывается. */ + /* IDATSZ округляем вверх до кратного QSPI_RFDR_WORD_BYTES (4) */ const uint32_t IDATSZ_ALIGNED = (data_len + (QSPI_RFDR_WORD_BYTES - 1U)) & ~(QSPI_RFDR_WORD_BYTES - 1U); diff --git a/bsp/sd/include/bsp/sd.h b/bsp/sd/include/bsp/sd.h index 2fc8ac4..fce443e 100644 --- a/bsp/sd/include/bsp/sd.h +++ b/bsp/sd/include/bsp/sd.h @@ -26,9 +26,10 @@ bsp_status_t bsp_sd_init(void); bsp_status_t bsp_sd_deinit(void); /* - * Проверить физическое наличие карты через регистр USDHC PRSSTAT. - * Не требует предварительного вызова bsp_sd_init(). - * Включает тактирование USDHC1 на время чтения регистра. + * Проверить физическое наличие карты через USDHC PRES_STATE.CINST + * (USDHC_GetPresentStatusFlags). Включает тактирование USDHC1 на время + * чтения; не требует предварительного вызова bsp_sd_init(). Корректно + * пока пин D13 замаплен на USDHC1_CD_B. */ bool bsp_sd_is_inserted(void); diff --git a/bsp/sd/src/sd.c b/bsp/sd/src/sd.c index 05c5df2..d1718d7 100644 --- a/bsp/sd/src/sd.c +++ b/bsp/sd/src/sd.c @@ -5,15 +5,11 @@ #include "bsp/sd.h" #include "fsl_sd.h" -#include "fsl_usdhc.h" /* USDHC_Reset — аппаратный сброс FIFO/state machine */ -#include "sdmmc_config.h" /* BOARD_SD_Config, BOARD_SDMMC_SD_HOST_BASEADDR */ +#include "fsl_usdhc.h" +#include "sdmmc_config.h" + +#include -#include /* memset */ -/* --------------------------------------------------------------------------- - * Глобальный дескриптор карты — нужен SDK-стеку (передаётся по указателю - * в BOARD_SD_Config и sd_disk_initialize через g_sd). - * Объявлен без static — fsl_sd_disk.c ссылается на него как extern sd_card_t g_sd. - * ------------------------------------------------------------------------- */ extern sd_card_t g_sd; /* --------------------------------------------------------------------------- @@ -37,7 +33,8 @@ static void ensure_host_configured(void) { return; } - /* cd=NULL, userData=NULL: CD управляется хостом через PRSSTAT */ + /* cd=NULL, userData=NULL: детект — GPIO-callback внутри BOARD_SD_Config(), + * не внешний callback сюда (см. bsp_sd_is_inserted() — тот же механизм). */ BOARD_SD_Config(&g_sd, NULL, BOARD_SDMMC_SD_HOST_IRQ_PRIORITY, NULL); g_s_host_configured = true; } @@ -54,19 +51,8 @@ bsp_status_t bsp_sd_init(void) } /* - * Аппаратный сброс USDHC FIFO + command/data state machine ПЕРЕД - * повторной инициализацией. Без этого non-blocking host driver SDK - * (fsl_sdmmc_host.c) может остаться в состоянии "ожидание завершения - * предыдущей транзакции" после SD_HostDeinit() на прошлом прогоне — - * физическая транзакция уже умерла вместе с deinit, но внутренний - * флаг ожидания interrupt остаётся выставленным, и следующий f_mount() - * блокируется навсегда в ожидании события, которое никогда не придёт. - * - * USDHC_Reset с маской kUSDHC_ResetAll сбрасывает контроллер на - * регистровом уровне, не полагаясь на состояние, оставленное - * предыдущей сессией. Безопасно вызывать даже при первом запуске — - * базовый адрес уже доступен через BOARD_SDMMC_SD_HOST_BASEADDR - * (clock на этот момент должен быть включён, см. ниже). + * Аппаратный сброс USDHC FIFO + command/data state machine перед + * повторной инициализацией. */ CLOCK_EnableClock(kCLOCK_Usdhc1); /* тактирование нужно ДО сброса регистров */ USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U); @@ -80,12 +66,20 @@ bsp_status_t bsp_sd_init(void) (void) memset(&g_sd, 0, sizeof(g_sd)); g_s_host_configured = false; /* форсируем повторный BOARD_SD_Config ниже */ - ensure_host_configured(); /* только BOARD_SD_Config — заполняет g_sd */ + ensure_host_configured(); /* BOARD_SD_Config — заполняет g_sd, включая usrParam.pwr */ - /* - * Полный init (host + card) происходит в sd_disk_initialize → SD_Init, - * который вызывается из f_mount → disk_initialize. - */ + if (SD_HostInit(&g_sd) != kStatus_Success) + { + return BSP_ERR_HW; + } + + if (SD_PollingCardInsert(&g_sd, kSD_Inserted) != kStatus_Success) + { + return BSP_ERR_HW; + } + + SD_SetCardPower(&g_sd, false); + SD_SetCardPower(&g_sd, true); g_s_initialized = true; return BSP_OK; @@ -101,12 +95,6 @@ bsp_status_t bsp_sd_deinit(void) SD_HostDeinit(&g_sd); SD_SetCardPower(&g_sd, false); - /* - * Дополнительный аппаратный сброс сразу после deinit — гарантирует, - * что FIFO и state machine USDHC не останутся в промежуточном - * состоянии независимо от того, что делает (или не делает) - * SD_HostDeinit() из SDK на уровне регистров. - */ USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U); g_s_initialized = false; @@ -116,6 +104,8 @@ bsp_status_t bsp_sd_deinit(void) bool bsp_sd_is_inserted(void) { - return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) == - BOARD_SDMMC_SD_CD_INSERT_LEVEL; -} + CLOCK_EnableClock(kCLOCK_Usdhc1); + + uint32_t ps = USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR); + return (ps & kUSDHC_CardInsertedFlag) != 0U; +} \ No newline at end of file diff --git a/docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md b/docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md index 6e98cea..d594c50 100644 --- a/docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md +++ b/docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md @@ -72,7 +72,51 @@ tft_app. --- -## 5. Ссылки +## 5. Ограничения на runtime-доступ к flash из tft_app (Direct-XIP) — решено + +Boot-стратегия (Direct-XIP и для bootloader, и для tft_app — см. `firmware/bootloader/PLAN.md`) +пересмотрена и подтверждена в обсуждении Фазы 3, с учётом требований tft_app: (1) кеширование +спрайтов в SDRAM, (2) хранение и изменение настроек во flash, (3) проигрывание WAV с flash. + +**Найденный механизм риска** — [bsp/qspi_flash/README.md](../../bsp/qspi_flash/README.md), раздел +"XIP-безопасность": любая IP-команда FlexSPI блокирует AHB-путь — если в этот момент CPU фетчит +инструкцию из Flash, происходит HardFault. Защита в `bsp_qspi_flash` — IRQ lock на всё время операции +(стирание сектора ~45 мс, блока 64 КБ ~150 мс). Для однопоточного блокирующего bootloader'а это не +проблема; для tft_app (FreeRTOS, конкурентные задачи) любая такая операция глушит **все** прерывания +в системе на своё время, включая аудио DMA-колбэк. + +**Почему не перешли на `MCUBOOT_RAM_LOAD`** (альтернатива, устраняющая конфликт полностью — код +перестаёт исполняться через flash-AHB вообще): в вендоренном bootutil этот режим **не имеет аналога +`MCUBOOT_DIRECT_XIP_REVERT`** — `boot_select_or_erase()` (`copy_done`/`image_ok`, автоматический откат +неподтверждённого образа) гейтится `#if defined(MCUBOOT_DIRECT_XIP) && defined(MCUBOOT_DIRECT_XIP_REVERT)` +в `loader.c` и не вызывается в ветке `MCUBOOT_RAM_LOAD`. Переход потерял бы anti-brick гарантию, +аппаратно проверенную в Фазе 2 (сценарий 5 её чек-листа), без готовой замены в самом bootutil — +пришлось бы реализовывать такой механизм самостоятельно, без прецедента. + +**Референс для калибровки** — легаси-реализация (`TFT8_RX_wOS`, исполняется из SDRAM): +её `audio_player.c` стримит блоками `MONO_READ_SIZE=256` Б через кольцевой буфer `BUFFER_NUM=3` — на +руках держится всего ~8.7 мс аудио (128 сэмплов / 44100 Гц × 3 блока). Этого достаточно при исполнении +из RAM (конкуренции за flash-AHB нет вообще), но недостаточно при Direct-XIP. + +**Решение — Direct-XIP остаётся, при двух обязательных ограничениях для tft_app** (переносятся в его +будущий план, не в план bootloader'а): + +1. **Аудио — глубоко буферизировать в SDRAM**, не стримить малыми порциями, как в легаси-версии. + Целевая глубина — заведомо больше худшей flash-операции (например, ≥300 мс — это ~26 КБ моно PCM16 + 44.1 кГц, ничто относительно 32 МБ SDRAM). При такой глубине редкая конкурентная запись настроек не + создаёт слышимого дропаута. +2. **Любое чтение/запись flash, способное совпасть по времени с другой flash-операцией, обязано идти + через защищённый IP-command драйвер** (аналог `bsp_qspi_read()`: ITCM + IRQ lock, как уже сделано в + `bsp_qspi_flash`), а не через сырой XIP `memcpy`, как в легаси `settings_manager.c` (там это было + безопасно только потому что код исполнялся из RAM). Актуально для будущей FatFS-прослойки над + областью ассетов (см. п.4 выше) — она должна использовать тот же паттерн, что `port/fatfs/sd` + использует поверх `bsp_sd`. +3. Запись настроек остаётся редкой, явной, инициированной пользователем (не периодический автосейв) — + короткий блокирующий фриз (десятки мс) на сохранение ожидаем и допустим в UI. + +--- + +## 6. Ссылки - [BOOT_FLAGS.md](BOOT_FLAGS.md) — XIP/DCD/сценарии исполнения кода. - [HAB_GUIDE.md](HAB_GUIDE.md) — подпись bootloader (HAB) vs подпись образов tft_app (`imgtool`). diff --git a/firmware/bootloader/CMakeLists.txt b/firmware/bootloader/CMakeLists.txt index 854ee9f..0667a74 100644 --- a/firmware/bootloader/CMakeLists.txt +++ b/firmware/bootloader/CMakeLists.txt @@ -17,12 +17,18 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in" # Фаза 2. include(${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/bootutil_sources.cmake) +# bootloader_fatfs — bare-metal FatFS для чтения TFT_APP.BIN с SD (Фаза 3). +add_subdirectory(fatfs) + add_executable( ${TARGET_NAME} src/main.c src/cli.c src/protocol.c src/boot_select.c + src/update_policy.c + src/slot_version.c + src/sd_update.c mcuboot_port/flash_map_backend.c mcuboot_port/keys.c ${MCUBOOT_BOOTUTIL_SOURCES} @@ -40,11 +46,13 @@ target_compile_definitions( __STARTUP_INITIALIZE_NONCACHEDATA) # ----------------------------------------------------------------------------- -# Зависимости. bsp_button (downgrade-override) добавится в Фазе 3. +# Зависимости. bsp_button — downgrade-override (удержание BSP_BUTTON_1). +# bootloader_fatfs — чтение TFT_APP.BIN с SD (Фаза 3, firmware/bootloader/fatfs/). # ----------------------------------------------------------------------------- target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick bsp_usb_cdc bsp_qspi_flash - bsp_boot_xip_no_dcd) + bsp_boot_xip_no_dcd bsp_button + bootloader_fatfs) # ----------------------------------------------------------------------------- # Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом diff --git a/firmware/bootloader/DEBUG_LOG_PHASE3_SD.md b/firmware/bootloader/DEBUG_LOG_PHASE3_SD.md new file mode 100644 index 0000000..15e054b --- /dev/null +++ b/firmware/bootloader/DEBUG_LOG_PHASE3_SD.md @@ -0,0 +1,507 @@ +# Фаза 3 — аппаратная верификация SD-пути: лог (РАЗРЕШЁН 2026-07-10) + +**✅ РАЗРЕШЕНО (2026-07-10, раунд 3).** Настоящий фикс детекта — чтение **USDHC PRES_STATE.CINST** +(`USDHC_GetPresentStatusFlags` + `kUSDHC_CardInsertedFlag`) в `bsp_sd_is_inserted()`, а не GPIO. Это +подход (a) из самого первого лога, который тогда откатили как «CINST=0 даже со вставленной картой» — +но откатили ошибочно: CINST=0 читался потому, что в тот момент пин ещё не был на `USDHC1_CD_B`. +Все 5 сценариев Фазы 3 пройдены на железе. Детали — «Обновление 2026-07-10 (раунд 3)» ниже. Разделы +раундов 1–2 сохранены как история расследования (их выводы частично опровергнуты раундом 3 — см. +явные пометки). + +
История расследования (раунды 1–2, выводы частично опровергнуты) + +**2026-07-10, раунд 1**: pinmux CD откачен на `GPIO2_IO28` + явный power-cycle карты. Аппаратно +проверено — симптом 1 (SD не вставлена) **не устранён**, симптом 3→4 похоже устранён. См. +«Обновление 2026-07-10» ниже. **⚠️ Вывод «GPIO2_IO28 — верный pinmux» опровергнут раундом 3: для +работающего PRSSTAT-чтения пин нужен на `USDHC1_CD_B`.** + +**2026-07-10, раунд 2**: по прямому запросу пользователя — полное выравнивание SD-стека на +`TFT_BOOTLOADER`, не только pinmux. Три дополнительных расхождения найдены и закрыты (CD pad-config +байт-в-байт, SD_PWR side-effect в детекте, explicit HostInit/PollingCardInsert/power-cycle). См. +«Обновление 2026-07-10 (раунд 2)» ниже. **⚠️ CD pad-config и explicit-init остались в дереве и не +вредят; SD_PWR side-effect в `bsp_sd_is_inserted()` заменён финальным PRSSTAT-чтением раунда 3.** + +
+ +Этот файл — снимок состояния расследования на момент передачи в отдельный тред. Код Фазы 3 +(`update_policy`, `slot_version`, `sd_update`) спроектирован и host-протестирован отдельно (см. +[PLAN.md](PLAN.md), раздел «Фаза 3») и сам по себе не пересматривается. Проблема — в аппаратном +bring-up SD/USDHC на конкретной плате, всплывшая только на реальном железе. + +## Симптомы (в порядке обнаружения) + +1. **Чистая плата, оба слота пусты, SD карта отсутствует** → плата зависала внутри + `run_update → ... → micro_sd_disk_initialize → SD_PollingCardInsert`. +2. После правок ниже (текущее состояние pinmux, см. «Файлы» ниже) то же исходное условие (нет SD, + пустые слоты) стало вместо зависания давать **падение в `DefaultISR`**. Стек вызовов (VSCode + Cortex-Debug): + ``` + main → sd_update_check → run_update → bsp_sd_deinit → SD_HostDeinit → + SDMMCHOST_Deinit → SDMMCHOST_Reset → USDHC_SelectVoltage → → DefaultISR + ``` +3. **SD вставлена при старте** → доходит до `jump_to_image` и успешно стартует (под отладчиком). + После этого: выключить питание, вынуть SD, включить снова → та же прошивка (slot-заглушка) **не + стартует** (возврат к симптому 1/2). +4. **SD оставлена в слоте**, повторный power cycle сразу после успешного случая (3) (то есть SD + физически всё ещё вставлена) → **зависание в `OSA_SemaphoreWait`**, отдельный стек: + ``` + main → sd_update_check → run_update → f_mount → mount_volume → disk_initialize → + microsd_disk_initialize → sd_disk_initialize → SD_CardInit → sdcard_init → SD_ReadStatus → + SD_Transfer → SDMMCHOST_TransferFunction → SDMMC_OSAEventWait → OSA_SemaphoreWait + ``` + Это происходит **до** попытки прыжка — на самой первой инициализации карты в этой сессии + питания. Прерывание, которое должно отпустить семафор (сигнал завершения транзакции), судя по + всему, не приходит. + +Симптомы 2 и 4 — из одного и того же похода тестирования, оба неразобраны на момент передачи. + +## Что уже пробовали (в хронологическом порядке) + +| # | Правка | Результат | +|---|---|---| +| a | `bsp_sd_is_inserted()` / внутренний callback переведены на USDHC `PRES_STATE.CINST` вместо GPIO | Неверно: `CINST` читался как 0 даже при физически вставленной карте (вероятно, пин на тот момент не был замаплен на альт-функцию USDHC). Откачено. | +| b | `cleanup_before_jump()` в `boot_select.c` — отключение всех NVIC IRQ + `SysTick->CTRL=0` + `SCB->VTOR` перед прыжком | Пользователь подтвердил на железе: **«Никаких изменений. Всё также»**. Оставлено в коде (не вредит, независимо подтверждено паттерном `jump_to_application()` в референсе `TFT_BOOTLOADER`), но не является фиксом текущего бага. | +| c | Детект возвращён на GPIO-чтение; pinmux CD-пина переключён с `GPIO2_IO28` на `USDHC1_CD_B` (основание — референс `TFT7_RX_wOS`) | Дало симптом 2 (падение в `DefaultISR` вместо зависания) — то есть что-то изменилось, но проблема не ушла. Появилась **третья**, ещё более авторитетная референсная кодовая база (`TFT_BOOTLOADER`), которая прямо противоречит этому выбору pinmux (см. ниже) — **не проверено на железе**, стоит откатить в первую очередь. | + +## Три референсных проекта — что каждый показывает про SD/CD + +Все три, со слов пользователя, работали на **той же физической плате**. Расхождения между ними и +есть ядро проблемы. + +1. **`TFT8_RX_wOS`** (`source/hal/sd.c/h`) — `bsp_sd_is_inserted()` через + `USDHC_GetPresentStatusFlags()` + `kUSDHC_CardInsertedFlag` (аппаратный регистр USDHC, не GPIO). +2. **`TFT7_RX_wOS`** (полный board-код) — CD-пин замаплен на `USDHC1_CD_B` (`board/pin_mux.c`), но + **читается** через обычный `GPIO_PinRead()` (`BOARD_SDCardGetDetectStatus()` в `board/sdmmc_config.c`) — + то есть USDHC-альтфункция выбрана, а не используется для самого чтения детекта. +3. **`TFT_BOOTLOADER`** (собственный, предыдущий, явно описанный пользователем как «без нареканий + работал на этой плате») — CD-пин замаплен на **простой `GPIO2_IO28`** (никакой USDHC-альтфункции + вообще), читается тем же `GPIO_PinRead`. Дополнительно, в `is_sdcard_present()`/`init_sd()`, есть + явное управление питанием карты: `SD_PWR_PIN` (GPIO1/19) выключается/включается в зависимости от + результата детекта, и явный **power-cycle карты** — `SD_SetCardPower(&g_sd, false)` → + `SD_SetCardPower(&g_sd, true)` — перед тем, как что-либо ещё делается с картой. Собственный + `jump_to_application()` тоже независимо переставляет `SCB->VTOR` перед прыжком (подтверждает + правку (b) выше, но никак не связано с текущим багом). + +**Проблема**: (2) и (3) прямо противоречат друг другу по pinmux того же физического пина +(`GPIO_B1_12` / физический пин D13) — мультиплексирование эксклюзивно, физически не может быть верно +одновременно и то, и другое. (3) — самый доверенный источник (собственный, явно проверенный продакшн +на этой же плате, не просто «легаси-референс»), и именно его выбор (`GPIO2_IO28`, без USDHC-альтфункции) +**сейчас не применён** в дереве — стоит откатить в первую очередь. + +## Текущее состояние кода (на момент передачи) + +`bsp/sd/src/sd.c::bsp_sd_is_inserted()` — GPIO-чтение: +```c +return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) == + BOARD_SDMMC_SD_CD_INSERT_LEVEL; +``` + +`bsp/generated/sdmmc_config.c::BOARD_SD_Config()` — pinmux **на `USDHC1_CD_B`** (см. выше — под +вопросом): +```c +IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U); +``` +внутренний детект-callback — `sd_card_detect_gpio()` (GPIO, не USDHC-регистр) — сам механизм чтения +не менялся, менялся только pinmux. + +`bsp/sd/src/sd.c::bsp_sd_init()`/`bsp_sd_deinit()` — **не делают explicit power-cycle карты** +(`SD_SetCardPower`) — в отличие от `TFT_BOOTLOADER::init_sd()`. `bsp_sd_init()` выставляет +`g_s_initialized = true` сразу после `BOARD_SD_Config()` (только конфигурация дескриптора хоста) — +**до** того, как реальный `SD_HostInit()` вообще вызывается (тот вызывается неявно позже, изнутри +`f_mount() → sd_disk_initialize() → SD_Init()`). Отсюда гипотеза по симптому 2 ниже. + +`firmware/bootloader/src/sd_update.c::run_update()` — если `f_mount()` возвращает ошибку (не +зависает, а именно быстро проваливается), путь `(void) bsp_sd_deinit(); return;` (строка ~160) +вызывается напрямую, не через `cleanup:` — это ровно та точка, где произошло падение в симптоме 2. + +## Рабочие гипотезы (не проверены на железе) + +1. **Симптом 2 (DefaultISR, SD физически отсутствует)**: при текущем pinmux (`USDHC1_CD_B`) + `GPIO_PinRead()` того же физического пина может отдавать некорректное/неопределённое значение + (частый эффект на i.MX-подобных SoC — когда пин отдан альтернативной функции, вход GPIO-регистра + не гарантированно отражает реальный внешний уровень). Это дало бы **ложное срабатывание + "карта вставлена"** даже без карты → `sd_update_check()` пропускает наш собственный gate и + заходит в `run_update()` → `f_mount()` внутри тоже видит ложный "card present" (тот же callback) + → пытается реальный протокол инициализации без карты → команды таймаутятся (не висят вечно, в + отличие от `SD_PollingCardInsert`) → `f_mount()` возвращает ошибку → `bsp_sd_deinit()` вызывается, + но `SD_HostInit()` либо не был вызван, либо card/voltage-состояние осталось в промежуточном виде + → `SDMMCHOST_Reset()/USDHC_SelectVoltage()` работает с невалидным состоянием → падение. + **Проверка**: откатить pinmux на `GPIO2_IO28` (референс `TFT_BOOTLOADER`) и повторить сценарий + "нет SD, пустые слоты" — если ложное срабатывание уйдёт, `run_update()` вообще не должен + вызываться, и падение исчезнет как следствие. +2. **Симптом 4 (OSA_SemaphoreWait, SD физически присутствует)** — не про card-detect вообще, а про + реальный обмен данными (`SD_ReadStatus`/`SD_Transfer`) — прерывание завершения транзакции не + приходит. Кандидат на причину: отсутствие power-cycle карты перед стартом обмена (в отличие от + `TFT_BOOTLOADER::init_sd()`) — карта могла остаться в состоянии, унаследованном от **предыдущей** + успешной сессии (та же карта, тот же физический power rail, если он не сбрасывается при + `bsp_sd_deinit()`/между сессиями), и не отвечать на новый протокол инициализации так, как ожидает + SDK-стек. **Не проверялось**, требует добавления explicit `SD_SetCardPower(false)` → + задержка → `SD_SetCardPower(true)` в `bsp_sd_init()` и повторного теста именно сценария «второй + power cycle подряд с той же картой». +3. Не проверено: действительно ли `bsp/qspi_flash`, `firmware_test`'ный стек SD (уже работающий в + продакшене через `test_usd.c`) отличается от bootloader-сценария именно временем между + power-on/детектом и первым обращением к карте — `firmware_test` ждёт explicit подтверждения + человека перед `usd_init()`, что даёт карте много времени "устояться" электрически; bootloader + делает это заметно быстрее после включения питания. Если гипотеза 1 не полностью объясняет + симптом 2, стоит проверить добавление небольшой выдержки после детекта перед `f_mount()`. + +## Открытые вопросы для нового треда + +> **РАЗРЕШЕНО 2026-07-10 (раунд 3).** Основной баг детекта закрыт PRSSTAT-чтением (см. секцию +> «Обновление 2026-07-10 (раунд 3)» выше). Пункты 1–2 ниже сохранены как история; пункт 3 — +> опровергнут; пункты 4–5 (devcontainer, watchdog) остаются актуальными как отдельные задачи. +> Плюс два новых нереализованных пункта-рекомендации из раунда 3: (6) консолидация детекта на +> единый механизм PRSSTAT (убрать латентную хрупкость re-scan), (7) ранний сэмпл кнопки даунгрейда. + +1. ✅ **Применено 2026-07-10 (раунд 1).** Откатить pinmux CD на `GPIO2_IO28` (без `USDHC1_CD_B`) — + самый доверенный референс (`TFT_BOOTLOADER`) использует именно так. **Аппаратно проверено — + недостаточно само по себе**: симптом 1 (SD не вставлена) воспроизводится и с верным pinmux. См. + «Обновление 2026-07-10 (раунд 2)» — потребовалось довыровнять ещё и pad-config CD-пина. +2. ✅ **Применено 2026-07-10 (раунд 1).** Добавить explicit power-cycle SD-карты в `bsp_sd_init()` + (`SD_SetCardPower(false)` → задержка → `SD_SetCardPower(true)`), по образцу + `TFT_BOOTLOADER::init_sd()`. **Аппаратно похоже подтверждено** — симптом 3→4 (повторный power + cycle с картой в слоте) больше не зависает (~5 c вместо hang), хотя раунд 1 не изолировал, какой + именно из двух фиксов это дал. +3. Развести флаг `g_s_initialized` в `bsp_sd_init()`/`bsp_sd_deinit()` на два состояния — "хост + сконфигурирован" (`BOARD_SD_Config` вызван) vs "хост реально поднят" (`SD_HostInit` реально + отработал) — чтобы `bsp_sd_deinit()` не звал `SD_HostDeinit()`/`USDHC_Reset` на состоянии, + которое никогда полноценно не поднималось. **Не реализовано** — см. «Обновление 2026-07-10»: + source-level разбор показал, что в наблюдавшемся симптоме 2 это не изменило бы поведение + (`SD_HostInit()` к моменту падения уже отработал успешно). Понижено до фоновой гигиены. +4. Прогонять сборку/прошивку через devcontainer (стандартный путь проекта), не только через + локальный host-toolchain — держать в уме при следующей проверке (сама эта сессия строила и + прошивала с хоста напрямую). +5. После того как оба симптома закрыты и подтверждены на железе — реализовать аппаратный watchdog + (см. [PLAN.md](PLAN.md), раздел «Запланировано: аппаратный watchdog») **как отдельный шаг после**, + не вместо, диагностики первопричины — иначе watchdog будет просто маскировать/сбрасывать симптом + на каждом цикле. + +## Обновление 2026-07-10 — четвёртый референс, source-level трассировка, два фикса применены + +Пользователь предоставил **полный код** `TFT_BOOTLOADER` (ранее в этом логе упоминался только по +описанию) — `board/pin_mux.c`, `board/sdmmc_config.c`, `source/sd.c`, `source/gpio_setup.c`, +`source/main.c` и др. Это подтверждает и уточняет раздел «Три референсных проекта» выше, а не +меняет его выводы. + +### Пинмукс CD — подтверждено, откачено + +`TFT_BOOTLOADER/board/pin_mux.c`: +```c +IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);//SD check pin +``` +Плоский `GPIO2_IO28`, без какой-либо альт-функции USDHC. + +**Четвёртый источник**, найденный при подготовке фикса: NXP-овские +`sdk/boards/evkbimxrt1050/sdmmc_examples/*/pin_mux.c` (`sdcard_polling`, `sdcard_interrupt`, +`sdcard_freertos`, `sdcard_fatfs`, `mmccard_freertos` — уже вендорены в этом дереве) — **все** +используют `IOMUXC_GPIO_B1_12_GPIO2_IO28` для этого же физического пина (D13) на том же чипе +(MIMXRT1052/EVKB-семейство). Официальный NXP-референс для этой платы и этого пина ни разу не +использует `USDHC1_CD_B`. + +Итого — четыре независимых указания в одну сторону (`TFT_BOOTLOADER`; `TFT7_RX_wOS`, который хоть и +маплит на `USDHC1_CD_B`, но ЧИТАЕТ через GPIO, то есть де-факто не использует USDHC-функцию пина; +NXP `sdmmc_examples`; и само нестабильное поведение на железе под `USDHC1_CD_B`, из-за которого +этот лог вообще начался). + +**Применено**: `bsp/generated/sdmmc_config.c::BOARD_SD_Config()` — pinmux возвращён на +`IOMUXC_GPIO_B1_12_GPIO2_IO28`. Комментарии в этом файле и в `bsp/sd/src/sd.c::bsp_sd_is_inserted()` +(который раньше аргументировал ЗА `USDHC1_CD_B` задним числом, по результатам ещё не проверенного +на железе эксперимента (c)) переписаны под текущее состояние. Доктстринг `bsp_sd_is_inserted()` в +`bsp/sd/include/bsp/sd.h` тоже был стал устаревшим ещё раньше — утверждал чтение через регистр +USDHC PRSSTAT, оставшееся от давно откаченного эксперимента (a) — тоже исправлен. + +**Не тронуто намеренно** — `bsp/generated/board/pin_mux.c` (генерируется MCUXpresso Config Tools, +в этой сессии не редактировался) по-прежнему содержит +`IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U)` внутри `BOARD_InitPins()`, а сам +`TFT_Board.mex` (строка ~134) содержит `` для +этого пина — маршрутизация на уровне самого Config Tools проекта всё ещё «неправильная». +Функционально это не баг сейчас: `board_hw_init() → BOARD_InitPins()` отрабатывает раньше, чем +`bsp_sd_init() → BOARD_SD_Config()`, и именно вызов из `sdmmc_config.c` (уже исправленный) — самый +последний перед любой операцией с картой, то есть он и определяет реальное состояние железа. Но +это мина для будущего: перегенерация пинов через Config Tools перезапишет `pin_mux.c` обратно на +`USDHC1_CD_B`, а конфликт останется незаметным (`sdmmc_config.c` продолжит его перекрывать) — пока +кто-то не тронет порядок инициализации или не сочтёт повторный вызов в `sdmmc_config.c` +дублирующим и не уберёт его. **Рекомендация на будущее**: при следующем открытии `TFT_Board.mex` в +Config Tools — перемаршрутизировать D13/`GPIO_B1_12` на `GPIO2.gpio_io[28]` вместо +`USDHC1.usdhc_cd_b`. Не блокирует текущую аппаратную проверку. + +### Симптом 2 — механизм подтверждён трассировкой по исходникам SDK, не только гипотезой + +Прочитан `sdk/middleware/sdmmc/sd/fsl_sd.c`: + +- `SD_PollingCardInsert()` (используется для типа детекта `kSD_DetectCardByGpioCD`) — буквально + `do { ... } while (true)`, **без единого условия выхода по времени**. Если `cardDetected()` + (== `sd_card_detect_gpio()`, тот же нестабильный GPIO-read под `USDHC1_CD_B`) ни разу не даёт + связку значений, удовлетворяющую циклу, — зависание гарантированное и постоянное, не «иногда». + Ровно то же самое SDK-поведение отдельно задокументировано в `PLAN.md` под «Запланировано: + аппаратный watchdog» — тот же вывод, независимо подтверждён прямым чтением функции. +- `SD_Init()` **всегда** вызывает `SD_HostInit()` первым (`if (!card->isHostReady) { SD_HostInit(...); }`, + а `isHostReady` после `memset(&g_sd, 0, ...)` в `bsp_sd_init()` гарантированно `false`) — то есть + к моменту, когда `SD_Init()` доходит до `SD_PollingCardInsert()`/`SD_CardInit()` и потенциально + проваливается, хост уже реально поднят. Значит: наблюдаемый стек симптома 2 + (`SD_HostDeinit → SDMMCHOST_Deinit → SDMMCHOST_Reset → USDHC_SelectVoltage → DefaultISR`) + происходит **после** успешного `SD_HostInit()`, а не из-за его отсутствия — гипотеза 1 в + исходном логе («либо не был вызван `SD_HostInit()`...») технически неточна в части причины, но + итоговый вывод (откатить pinmux) от этого не меняется: без ложного детекта `run_update()` вообще + не входит в `f_mount()`, и весь этот путь (включая `SD_CardInit()` → протокольные команды к + несуществующей карте → `USDHC` остаётся в промежуточном по voltage-switch состоянии → падение при + последующем `SDMMCHOST_Reset()`) просто не выполняется. +- `SD_CardInit()` вызывает только `SD_SetCardPower(card, true)` — **не** делает `false→true` сама. + Явного power-cycle в штатном пути SDK нет вообще, если его не сделать самостоятельно (важно для + следующего раздела). + +### Симптом 4 — явный power-cycle добавлен (открытый вопрос 2, теперь применён) + +`bsp/sd/src/sd.c::bsp_sd_init()` — добавлен `SD_SetCardPower(&g_sd, false)` → +`SD_SetCardPower(&g_sd, true)` сразу после `ensure_host_configured()` (нужен `usrParam.pwr`, +который выставляет `BOARD_SD_Config()`), по образцу `TFT_BOOTLOADER::init_sd()`. Задержки — +встроенные в сам `SD_SetCardPower()` (`fsl_sd.c`): `SD_POWER_OFF_DELAY=100` мс, +`SD_POWER_ON_DELAY=400` мс, итого ~500 мс добавляется к каждой попытке `bsp_sd_init()` (не к +каждому проходу главного цикла — только когда карта физически детектится и `run_update()` реально +стартует). + +Механизм здесь остаётся гипотезой (в отличие от симптома 2 — прямого прочтения бага в исходниках +для этого случая нет): `SD_CardInit()` при обычном включении питания отработал бы одинаково что с +предварительным power-cycle, что без. Разница имела бы значение, только если карта физически не +успевает разрядиться между быстрыми power cycle платы (правдоподобно электрически — SD-рейл может +иметь свою постоянную разряда, отличную от MCU — но не подтверждено осциллографом). Аппаратный тест +покажет. + +### Открытый вопрос 3 (разделение флага `g_s_initialized`) — понижен в приоритете + +Source-level разбор показал: к моменту падения в симптоме 2 `SD_HostInit()` уже отработал успешно +(`isHostReady == true`) — разделение флага на «хост сконфигурирован» vs «хост реально поднят» **не +изменило бы поведение в этом конкретном наблюдавшемся случае**: `bsp_sd_deinit()` вызвал бы +`SD_HostDeinit()` в обоих вариантах флага одинаково. Ценность этого пункта — на пока не +пронаблюдавшемся крайнем случае (`SD_HostInit()` возвращает ошибку сам по себе, например +`SDMMCHOST_Init()` фейлится внутри) — не реализовано в этой сессии, чтобы не размывать набор +изменений перед проверкой на железе. Остаётся в очереди как фоновая гигиена, не как фикс под +конкретный симптом. + +### Сборка + +`just build::build-bootloader-debug` — зелёно (`m_text` 87304 Б / 247 КБ, 34.52% — чуть меньше, чем +87928 Б, отмеченные в PLAN.md до этой сессии). `just build::test-host` — 15/15. Оба прогнаны в +devcontainer (`docker exec -w /workspace tft-devcontainer ...`), не с хоста напрямую — см. открытый +вопрос 4 выше. + +### Чек-лист для аппаратной проверки (следующий шаг — на стороне пользователя) + +Оба фикса (pinmux + power-cycle) применены вместе — они независимы по механизму и по разным +симптомам, так что сигнатуры (зависание в `SD_PollingCardInsert`/`OSA_SemaphoreWait` vs падение в +`DefaultISR`) при повторном проявлении однозначно укажут, какой из двух фиксов не сработал (или +сработал не полностью), даже тестируя одним прогоном: + +1. **Симптом 1/2** (чистая плата, оба слота пусты, SD карта отсутствует) — ожидание: ни зависания, + ни падения; `sd_update_check()` должен быстро выйти (`bsp_sd_is_inserted()` теперь стабильно + `false`), дойти до `boot_select_and_jump()` → нет валидного образа → штатный ping/pong-цикл. +2. **Симптом 3** (SD вставлена при старте) — должен по-прежнему успешно доходить до + `jump_to_image`, как и раньше. +3. **Симптом 3→1** (после (2): выключить питание, вынуть SD, включить снова) — то же самое, что + симптом 1, теперь с картой, которая только что использовалась — ожидание: то же корректное + поведение, что в (1). +4. **Симптом 4 буквально** (SD оставлена в слоте, повторный power cycle сразу после успешного (2), + карта физически всё ещё вставлена) — тест именно на power-cycle-фикс; ожидание: не зависает в + `OSA_SemaphoreWait`, обновление/загрузка проходит штатно. + +Если что-то из этого всё ещё не проходит — сначала зафиксировать точную сигнатуру (стек в +отладчике, как и раньше в этом логе) до следующей правки; при необходимости фикс power-cycle и +фикс pinmux можно тестировать раздельно, закомментировав добавленный блок `SD_SetCardPower` в +`bsp_sd_init()` (единственное новое, легко изолируемое от pinmux-фикса изменение). + +## Обновление 2026-07-10 (раунд 2) — пинмукс-фикса недостаточно, полное выравнивание на TFT_BOOTLOADER + +**Аппаратный результат раунда 1** (пинмукс `GPIO2_IO28` + explicit power-cycle в `bsp_sd_init()`, +без изменения pad-config и без power-toggle side-effect): + +- **Симптом 1 (чистая плата, SD не вставлена) — не устранён.** Всё ещё висим внутри того же + `SD_PollingCardInsert()` `do{}while(true)`. Это означает: `bsp_sd_is_inserted()` всё ещё + возвращает ложный `true` без карты, ДАЖЕ с верным pinmux — вывод раунда 1 («нестабильное чтение + из-за неверной альт-функции») опровергнут этим результатом. Раз чтение теперь стабильно неверное + (не «шумит», а последовательно врёт), причина — не альт-функция сама по себе, а что-то ещё в + электрической конфигурации пина (см. ниже) либо в физической полярности детект-переключателя на + этой плате. +- **Симптом 3→4 (SD оставлена в слоте, повторный power cycle) — похоже, устранён.** Раньше — + зависание в `OSA_SemaphoreWait`. Теперь: загрузка (переиспользование уже установленного stub-а) + занимает ~5 секунд вместо зависания. Не финальное подтверждение (не изолировано, какой именно + из фиксов раунда 1 это дал — pinmux или power-cycle), но однозначно прогресс. + +**Важное уточнение механизма симптома 1**, важное для дальнейшей диагностики: `SD_PollingCardInsert(card, kSD_Inserted)` +в SDK — это `do{}while(true)` без ЕДИНОГО условия выхода, если карта не появляется. Это не +«нестабильность», это **ожидаемое поведение при вызове с картой, которой реально нет** — функция +единственная блокируется до появления карты, всегда, у обоих бутлоадеров (текущий и +`TFT_BOOTLOADER::init_sd()` вызывают её одинаково). Значит, единственное, что может предотвратить +зависание — гейт (`bsp_sd_is_inserted()`) ДОЛЖЕН быть на 100% верным при пустом слоте. Раз он +по-прежнему неверный — дело в самом детекте, не в том, как его результат используется. + +### Инструкция пользователя: полное выравнивание на TFT_BOOTLOADER, не только pinmux + +Пройден весь `TFT_BOOTLOADER` построчно на предмет расхождений с текущим SD-стеком, помимо +pinmux. Найдено и закрыто три конкретных расхождения: + +**1. Pad-config CD-пина — было НЕ то, что в референсе.** Раунд 1 откатил только pinmux +(`IOMUXC_SetPinMux`), но НЕ pad-config (`IOMUXC_SetPinConfig`) — тот со времён исходной правки +оставался «активная 47к подтяжка вверх + hysteresis» (`PKE|PUE|HYS|PUS(1)`). Побитовый разбор +`TFT_BOOTLOADER::BOARD_SD_Pin_Config()` (`0x10B0U`, с помощью распечатанных масок из +`PERI_IOMUXC.h`) даёt: `PKE=1, PUE=0 (KEEPER, не активная подтяжка — PUS в этом режиме +игнорируется), HYS=0 (выключен), SPEED=2, DSE=6`. Это **не** совпадает с тем, что было в дереве — +принципиальная разница (keeper vs. активный pull-up, hysteresis выкл vs. вкл). Применено побитово +точно как в референсе (`bsp/generated/sdmmc_config.c::sd_pin_config()`). **Это — наиболее +вероятный настоящий фикс** для устойчивого (не шумового) ложного детекта: если реальная физическая +конфигурация детект-цепи на этой плате рассчитана на keeper (или просто не рассчитана на активную +подтяжку в эту сторону), активная 47к подтяжка вверх могла систематически перетягивать/конфликтовать +с реальным сигналом. +- **Не проверено на железе.** + +**2. `bsp_sd_is_inserted()` не имела side-effect на SD_PWR.** `TFT_BOOTLOADER::is_sdcard_present()` +на каждый вызов не только читает CD, но и включает/выключает `SD_PWR` по результату — до этого +раунда `bsp_sd_is_inserted()` была чистым чтением без побочных эффектов. Добавлено: `sd_power_control()` +(`sdmmc_config.c`) сделана публичной под именем `BOARD_SDCardPowerControl()` (то же имя, что и в +стоковом `TFT_BOOTLOADER/board/sdmmc_config.c` — там это отдельная, не связанная с +`is_sdcard_present()` функция, но семантически та же роль) и вызывается из +`bsp_sd_is_inserted()` на каждый скан. +- **Механизм не подтверждён** — детект (механический контакт) физически не должен зависеть от + питания карты, но раз референс это делает, а референс проверенно работал на этой плате, — реплицировано + добросовестно, не только «на всякий случай», а по прямому запросу пользователя выровнять именно + настройку SD-стека на референс. + +**3. `bsp_sd_init()` теперь явно вызывает `SD_HostInit()` → `SD_PollingCardInsert()` → power-cycle**, +байт-в-байт порядок `TFT_BOOTLOADER::init_sd()`, вместо implicit-инициализации внутри +`f_mount() → SD_Init()`. Само по себе это НЕ предотвращает зависание при реальном отсутствии карты +(см. выше — блокировка без тайм-аута заложена в SDK и одинакова что в implicit, что в explicit +пути) — ценность изменения в точном соответствии референсу и в том, что `f_mount()` внутри теперь +дополнительно делает `SD_HostDoReset()` + повторный `SD_PollingCardInsert()`/`SD_CardInit()` уже +поверх реально поднятого хоста (`isHostReady == true`), тем же паттерном двойного вызова +`SD_PollingCardInsert`, что и в референсе. + +### Сборка + +Оба фикса раунда 2 собраны и проверены в devcontainer: `just build::build-bootloader-debug` — +зелёно (`m_text` 87352 Б / 34.54%), `just build::test-host` — 15/15. + +### Если симптом 1 всё ещё не уйдёт после раунда 2 + +Тогда pad-config — тоже не объяснение, и дальше гадать конфигурацией регистров вслепую +контрпродуктивно (уже два раунда фиксов на одних догадках без прямого замера). Следующий шаг в +этом случае — не третья попытка pin-config, а **прямой замер** физического пина `GPIO_B1_12` / +D13 (мультиметром или логическим анализатором через MCU-Link): уровень при вставленной карте vs. +без карты, и полярность самого механического CD-переключателя в слоте этой платы (нормально-открыт +или нормально-закрыт — `BOARD_SDMMC_SD_CD_INSERT_LEVEL=0U` предполагает нормально-открытый, +замыкающий на GND при вставке; если физически наоборот — весь код читает верно регистр, но с +неверным заранее предположением о полярности, и фикс — просто `BOARD_SDMMC_SD_CD_INSERT_LEVEL=1U`, +а не что-либо из pinmux/pad-config). + +## Обновление 2026-07-10 (раунд 3) — РАЗРЕШЕНО: детект через USDHC PRES_STATE.CINST + +Пользователь заменил `bsp_sd_is_inserted()` на чтение регистра USDHC вместо GPIO: + +```c +bool bsp_sd_is_inserted(void) +{ + CLOCK_EnableClock(kCLOCK_Usdhc1); + uint32_t ps = USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR); + return (ps & kUSDHC_CardInsertedFlag) != 0U; +} +``` + +**Все 5 сценариев Фазы 3 пройдены на железе.** + +### Почему это сработало (и почему раунды 1–2 искали не там) + +Это ровно подход **(a)** из таблицы «Что уже пробовали» в самом верху лога — тогда откачен как +«CINST читался как 0 даже при вставленной карте». Причина того провала теперь понятна: `PRSSTAT.CINST` +отражает реальное состояние CD **только когда физический пин D13 замаплен на `USDHC1_CD_B`** (сигнал +идёт в периферию USDHC, а не в GPIO). В момент пробы (a) пин был на `GPIO2_IO28` — поэтому CINST и +читался нулём. То есть (a) и (c) по отдельности не работают, а **вместе** — работают: `USDHC1_CD_B` +маршрутизация (c) + чтение через PRSSTAT (a). + +Ключевой поворот: **мой откат pinmux на `GPIO2_IO28` в раунде 1 был направлен не в ту сторону.** +Исходная правка (c) (`USDHC1_CD_B`) была верной — ей не хватало парного чтения через регистр, а не +через `GPIO_PinRead`. Вся линия рассуждений раундов 1–2 («три-четыре референса за GPIO2_IO28») +верна лишь для того механизма чтения, который эти референсы используют (GPIO), но не была +единственно возможной: PRSSTAT — четвёртый, не рассматривавшийся всерьёз в раундах 1–2 путь, +который и оказался рабочим на этой плате. + +### Почему это чинит симптом 1 конкретно + +`SD_PollingCardInsert(kSD_Inserted)` в SDK — `do{}while(true)` без тайм-аута (разобрано в раунде 1). +Единственная защита — гейт `bsp_sd_is_inserted()` обязан быть верным на пустом слоте. На старте с +пустым слотом `BOARD_SD_Config()` ещё не вызывался (его зовёт `bsp_sd_init()` уже внутри +`run_update()`, за гейтом) → пин остаётся на `USDHC1_CD_B`, как его поставил `BOARD_InitPins()` в +`board_hw_init()` → PRSSTAT.CINST корректно читает «карты нет» → гейт возвращает false → блокирующий +`SD_PollingCardInsert()` вообще не достигается. Симптом 1 закрыт по построению. + +### Текущее итоговое состояние кода (что осталось от раундов 1–3) + +- `bsp_sd_is_inserted()` — PRSSTAT-чтение (раунд 3, рабочее). **Это единственное, что реально + закрыло симптом 1.** +- `bsp_sd_init()` — explicit `SD_HostInit()` → `SD_PollingCardInsert()` → power-cycle (раунд 2). + Не вредит; power-cycle правдоподобно закрыл симптом 3→4 (не изолировано отдельно). +- CD pad-config `0x10B0` на `GPIO2_IO28` (раунд 2) — остаётся; применяется к пину в моменты, когда + он на `GPIO2_IO28` (внутренний GPIO-детект SDK, см. ниже). Не вредит. +- `BOARD_SD_Config()` маплит пин на `GPIO2_IO28` (раунд 1) — **остаётся**, потому что внутренний + детект SDK (`sd_card_detect_gpio`, `kSD_DetectCardByGpioCD`) внутри `f_mount()→SD_Init()` читает + GPIO, и к тому моменту гейт уже подтвердил карту. Комментарии в `sdmmc_config.c`/`sd.c`/`sd.h` + переписаны под это фактическое состояние (два механизма детекта на разной маршрутизации одного + пина в разные моменты). +- `sd_power_control` возвращён в `static` (публичное имя `BOARD_SDCardPowerControl` из раунда 2 + больше не нужно — side-effect, ради которого оно вводилось, заменён PRSSTAT-чтением). + +### ⚠️ Латентная хрупкость re-scan (не мешает 5 сценариям, но стоит знать) + +Рабочая конфигурация опирается на то, что пин на `USDHC1_CD_B` в момент гейта. Но `BOARD_SD_Config()` +перемапливает его на `GPIO2_IO28` при первом же `bsp_sd_init()` и обратно НЕ возвращает (ни +`bsp_sd_deinit()`, ни что-либо ещё). Значит: если `run_update()` был вызван (карта была на гейте), +но завершился без прыжка (например, на карте нет `TFT_APP.BIN` → `bsp_sd_deinit()` → return), то +на СЛЕДУЮЩЕМ периодическом скане `bsp_sd_is_inserted()` прочитает PRSSTAT с пином уже на +`GPIO2_IO28` → снова получит ложный ноль → карта «исчезнет» до перезагрузки. В 5 сценариях это не +всплывает (везде, где карта валидна, происходит прыжок и сканы прекращаются; где карты нет — +`BOARD_SD_Config` не вызывался и пин остаётся на `USDHC1_CD_B`). Реальный, но узкий крайний случай. + +**Рекомендованная консолидация (не реализована — требует повторной проверки на железе, а рабочее +состояние трогать без запроса не стал):** свести детект к ЕДИНОМУ механизму — PRSSTAT везде. Либо +(вариант A) убрать remux на `GPIO2_IO28` из `BOARD_SD_Config()` (оставить `USDHC1_CD_B` как ставит +`BOARD_InitPins()`) и переключить `s_cd.type` на `kSD_DetectCardByHostCD` (тогда внутренний +`SD_PollingCardInsert` пойдёт через `SDMMCHOST_PollingCardDetectStatus` → тот же PRSSTAT); либо +(вариант B, минимальнее) оставить `kSD_DetectCardByGpioCD`, но заменить сам callback +`sd_card_detect_gpio` на PRSSTAT-чтение и тоже убрать remux. Оба варианта убирают двойную +маршрутизацию и латентную хрупкость. Оба меняют путь, проверенный сейчас только в текущей +двухмеханизменной форме, — отсюда обязательная повторная аппаратная проверка перед принятием. + +### Тайминг удержания кнопки даунгрейда (симптом/вопрос пользователя) + +Наблюдение: удержание `BSP_BUTTON_1` 2 c при подаче питания → даунгрейд НЕ происходит; 15 c → +происходит (визуально — сразу после отпускания). Разобрано по коду: + +- `bsp_button_read()` (`bsp/button/src/button.c`) — **мгновенное сырое чтение GPIO**, без debounce, + без накопления, без latch. `bsp_button_poll()`/события в загрузчике **не используются вообще** + (единственное обращение к кнопке — `bsp_button_read(BSP_BUTTON_1)` в `sd_update.c:180`). +- Значит важен ровно один момент: физически ли нажата кнопка в ту единственную点, когда + `run_update()` её сэмплит. А сэмпл этот — **после** `bsp_sd_init()` (power-cycle-задержки ~0.5 c) + + `f_mount()` (полная инициализация карты: CMD0/CMD8/ACMD41-поллинг и т.д.) + `f_open()` + чтения + заголовка + **двух полных крипто-валидаций слотов** (`peek_slot(0)`/`peek_slot(1)` → + `slot_version_get` → `bootutil_img_validate`: SHA-256 + ECDSA-P256 по каждому слоту). Это секунды, + и всё это — ДО главного цикла, без LED-фидбэка (heartbeat мигает только в главном цикле, куда + управление ещё не дошло). +- Отсюда: кнопку надо держать НЕПРЕРЫВНО от подачи питания до момента сэмпла. Отпустил в 2 c → + сэмпл (несколькими секундами позже) поймал уже отпущенную → `update_policy_decide` видит + `button_held=false` → SKIP → штатная загрузка более нового слота (не даунгрейд). Держал 15 c → + на момент сэмпла ещё нажата → даунгрейд. «Сразу после отпускания» — это не триггер по отпусканию + (кода на отпускание нет); это совпадение: решение уже защёлкнулось на сэмпле (пока держал), а + видимый результат (стирание+копирование+прыжок в старший образ) проявляется как раз к ~15 c. + +**Практическая инструкция технологу** (в HARDWARE_VERIFICATION_PHASE3.md): держать `BSP_BUTTON_1` с +момента подачи питания и не отпускать, пока по CDC не придёт `status: installing` (или пока частота +мигания `LED_APP` не сменится на «старший» образ). Ориентир ~15 c — держать с запасом. + +**Рекомендация по коду (не реализовано — поведение бы изменилось, требует запроса и ре-теста):** +сэмплить кнопку РАНО — при `bsp_button_init()`/сразу в `board_hw_init()`-фазе, до медленной +SD-инициализации и двойной крипто-валидации — и защёлкивать результат, вместо чтения в глубине +`run_update()`. Тогда «удержание при старте» стало бы предсказуемым (нажал в момент включения — +сработало), без многосекундного слепого окна. Сейчас это самый user-hostile момент SD-пути. + +## Побочная находка (не блокирует, но стоит знать) + +`PLAN.md` в двух местах ссылается на `DEBUG_LOG_PHASE2.md`, которого фактически нет в дереве (не +найден ни в рабочей директории, ни в истории git) — судя по всему, файл планировался, но не был +создан. Не тема этого лога, но стоит либо создать его отдельно, либо убрать ссылки, когда дойдут руки. diff --git a/firmware/bootloader/PLAN.md b/firmware/bootloader/PLAN.md index 8f8ecd1..8c11bac 100644 --- a/firmware/bootloader/PLAN.md +++ b/firmware/bootloader/PLAN.md @@ -7,7 +7,7 @@ | 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) | ✅ завершена | host-тесты 5/5, аппаратная верификация — все 5 сценариев пройдены на реальной плате (детали и 3 найденных/исправленных бага — [DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md)) | -| 3 — SD-путь установки | не начата | | +| 3 — SD-путь установки | ✅ аппаратно верифицирована (2026-07-10) | все 5 сценариев пройдены на реальной плате; баг SD/card-detect закрыт чтением USDHC PRES_STATE.CINST в `bsp_sd_is_inserted()` (см. [DEBUG_LOG_PHASE3_SD.md](DEBUG_LOG_PHASE3_SD.md), раунд 3; чек-лист — [test_stub/HARDWARE_VERIFICATION_PHASE3.md](test_stub/HARDWARE_VERIFICATION_PHASE3.md)). Осталось нереализованным (отдельными задачами): аппаратный watchdog (см. ниже), консолидация детекта на единый PRSSTAT, ранний сэмпл кнопки даунгрейда | | 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | | | 5 — HAB Release + service-tui | не начата | | @@ -23,7 +23,9 @@ - **Boot-стратегия**: и bootloader, и tft_app исполняются XIP из W25Q. Bootloader без ITCM-копирования, без DCD — он не трогает SDRAM. tft_app сама поднимает SEMC в своём раннем startup (SDRAM — под её XIP, - см. отдельный будущий план на tft_app). + см. отдельный будущий план на tft_app). Пересмотрена и переподтверждена в обсуждении Фазы 3 (риск + конфликта flash-AHB с runtime-записью настроек/проигрыванием аудио в tft_app vs `MCUBOOT_RAM_LOAD` — + см. [BOOTLOADER_FLASH_MAP.md §5](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md#5-ограничения-на-runtime-доступ-к-flash-из-tft_app-direct-xip--решено)). - **Схема обновления**: MCUboot **Direct-XIP**, два слота (A/Б) с полностью валидными образами каждый, без swap/scratch. Единственный полевой канал обновления — microSD. USB как канал заливки *образа* сознательно не делаем (SDP/blhost на производстве — это отдельный, не зависящий от кода bootloader, @@ -418,31 +420,192 @@ bsp_qspi_read → qspi_ip_read → qspi_read_fifo → qspi_read_tail`) видн **Цель**: сканирование 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 уточняет полный словарь паттернов) — - выхода из цикла нет, пока установка не пройдёт успешно. +### Ключевая находка (определила архитектуру, до всякого кода) -**Верификация (первая фаза, требующая реального железа)**: +**`boot_go()` нельзя вызывать дважды за одну сессию питания.** В `loader.c::boot_select_or_erase()` +(Direct-XIP-Revert) при выборе ещё не подтверждённого образа функция сразу пишет `copy_done=SET` **в +трейлер во flash** и продолжает грузить его. Если в той же сессии вызвать `boot_go()` повторно (напр. +чтобы сначала "подсмотреть" активную версию, а потом прыгнуть) — второй вызов увидит `copy_done=SET`, +`image_ok` ещё не выставлен (приложение не успело подтвердиться) — и **сотрёт только что установленный +образ**, посчитав это неудавшимся revert'ом. Это же поведение уже неявно доказано существующим +host-тестом Фазы 2 (`test_boot_go_reverts_unconfirmed_image` в `tests/host/mcuboot_port/`): второй +`boot_go()` на неподтверждённом образе стирает его. + +Следствие: версию уже активного слота для сравнения с SD-кандидатом нужно узнавать **не через +`boot_go()`**, а отдельным read-only способом (`slot_version.c`, ниже) — сам `boot_go()` (через +`boot_select_and_jump()`) вызывается в `main.c` ровно один раз за попытку, в самом конце, уже после +того как вся SD-логика отработала и решение об установке принято. + +### Решения, принятые в обсуждении Фазы 3 (не пересматриваются) + +- **Пик версии активного слота — полная криптографическая проверка**, не пик только заголовка. + `slot_version_get()` (`firmware/bootloader/src/slot_version.c`) читает заголовок слота и вызывает + `bootutil_img_validate()` напрямую (тот же вызов, что `loader.c::boot_image_check()` использует + внутри `boot_go()`, вне state-машины `context_boot_go()`) — hash+ECDSA проверяются по-настоящему, + не только magic. `bootutil_img_validate()` не пишет в flash (проверено по `image_validate.c`) и + безопасна вызывать сколько угодно раз, включая до первого `boot_go()` и на ещё не подтверждённых + образах — в отличие от `boot_go()`. `FIH_CALL`-обёртка (CFI-счётчик, `FIH_ENABLE_CFI` под профилем + LOW) самобалансируется на каждый вызов независимо (save/increment перед вызовом, decrement/verify + после — см. `fault_injection_hardening.h`), поэтому несколько вызовов подряд (Slot A, Slot Б, + повторно после установки) и последующий отдельный `boot_go()` не влияют друг на друга. +- **Гейт принятия SD-кандидата — двухступенчатый**, чтобы не строить отдельный flash_area-шim над + SD-файлом: (1) лёгкий пик заголовка (magic + версия) прямо с SD, до касания flash — этого достаточно + для решения install/skip; (2) после записи в целевой слот — тот же `slot_version_get()` на этом + слоте как финальный крипто-гейт. Если подпись кандидата битая — `slot_version_get()` вернёт false + (событие `SD_INSTALL_REJECTED`), а последующий единственный `boot_go()` просто не выберет этот слот + и останется на прежнем валидном — сама проверка подписи не дублируется нигде отдельно. +- **Стирание целого слота — fast-path на 64 КБ блоках** внутри самого `flash_area_erase()` + (`mcuboot_port/flash_map_backend.c`): если стирается вся область целиком (`off=0`, `len=fa_size`, + кратно 64 КБ) — `bsp_qspi_erase_block_64k()` (~4.8 с на 2 МБ) вместо посекторного пути (~23 с). + Частичное/невыровненное стирание — прежний посекторный путь. Выигрыш получает и `sd_update`, и + штатный revert-erase внутри `boot_go()` (`boot_select_or_erase()`) — оба уже зовут + `flash_area_erase(fap, 0, flash_area_get_size(fap))` без каких-либо изменений в bootutil-коде. +- **Верификация записи — немедленный `memcmp` на каждый чанк**, не хэш всего образа. Сразу после + `flash_area_write()` очередного чанка (4 КБ) — `flash_area_read()` того же диапазона и побайтовое + сравнение. Дёшево (буфер уже есть), ловит битую страницу сразу, без второго прохода по SD/flash и + без дублирования того, что `bootutil_img_validate()` и так проверит через TLV-хэш на шаге 2 гейта + выше. +- **Цикл ожидания SD — периодический пере-скан**, не одноразовая проверка при холодном старте. + Существенно для производственного сценария "bootloader-only → потом массовая SD-установка" — SD-путь + не должен требовать, чтобы карта уже стояла в момент включения питания. Дросселирован + `SD_RETRY_PERIOD_MS = 1500` мс через `bsp_tick_get_ms()`, не завязан на период мигания LED. +- **Целевой слот установки — всегда НЕ активный.** Активный слот (валидный, с более высокой версией; + если валиден только один — он активный; если ни одного — активного слота нет) этой логикой никогда + не стирается и не перезаписывается, независимо от исхода сравнения версий и от кнопки. Если + активного слота нет вообще — по умолчанию Slot A. Инвариант живёт в чистой функции + `update_policy_decide()` (`firmware/bootloader/src/update_policy.c`), полностью host-тестируемой (без + флеша/SD). +- **USB CDC поднимается до SD-логики**, не дожидаясь подключения хоста — `bsp_usb_cdc_write()` не + блокируется без хоста (см. `bsp/usb_cdc/src/usb_cdc.c:585-617`, безопасно проверено по коду), поэтому + статусы (`status: installing`, `waiting_for_sd`) видны технологу, если он уже подключён, в т.ч. на + самой первой попытке (чек-лист, сценарий 1). Небольшой сопутствующий эффект: `bsp_usb_cdc_init()` + теперь вызывается на каждой загрузке (раньше — только если `boot_select_and_jump()` уже провалился); + сама инициализация нерегистрозатратна и не ждёт хоста, так что "быстрая загрузка без кабеля" не + нарушена. +- **`bootloader_fatfs`** — bare-metal FatFS-таргет по образцу `firmware/test/fatfs/` (свой `ffconf.h`, + не шарить `firmware_test_fatfs` — bootloader и firmware_test взаимоисключающие прошивки на одной + плате). Единственное отличие от `firmware_test_fatfs`: **`FF_FS_READONLY=1`** — bootloader только + читает `TFT_APP.BIN`, никогда не пишет на SD; убирает весь write-путь FatFS из сборки. + +### Итог (выполнено, аппаратно ещё не проверено) + +- `firmware/bootloader/src/update_policy.{c,h}` — чистая логика install/skip + выбор целевого слота + (см. решения выше). `image_version_compare()` — свой аналог `boot_version_cmp()` (`static` в + `loader.c`, не экспортируется): major.minor.revision, без build_num, то же соглашение по умолчанию. + 12 host-тестов (`tests/host/update_policy/`) — все сценарии чек-листа ниже плюс сравнение версий. +- `firmware/bootloader/src/slot_version.{c,h}` — read-only пик версии слота (см. находку выше). + 4 host-теста (`tests/host/slot_version/`), переиспользуют `fake_flash_map_backend.c` и фикстуры + `tests/host/mcuboot_port/fixtures/` из Фазы 2 (`valid_v1/v2`, `corrupt_v1`, + `valid_v2_unconfirmed` — последний доказывает, что пик не задет revert-состоянием, в отличие от + `boot_go()`). +- `firmware/bootloader/mcuboot_port/flash_map_backend.c` — fast-path 64К в `flash_area_erase()`. +- `firmware/bootloader/fatfs/` — `bootloader_fatfs` (CMakeLists.txt, `include/ffconf.h`, `src/diskio.c`), + подключена через `add_subdirectory(fatfs)` в `firmware/bootloader/CMakeLists.txt`. +- `firmware/bootloader/src/sd_update.{c,h}` — оркестрация: `bsp_sd_is_inserted()` → `bsp_sd_init()` → + `f_mount("2:/")` → `f_open("2:/TFT_APP.BIN")` → лёгкий пик заголовка → `slot_version_get()` на + обоих слотах → `update_policy_decide()` → при INSTALL: `protocol_send_status("installing")` → + `flash_area_erase` (fast-path) + потоковое копирование чанками 4 КБ (`flash_area_write` + + немедленный `memcmp`-verify) → финальный `slot_version_get()` на целевом слоте как крипто-гейт. + Коды ошибок на CDC: `SD_CANDIDATE_INVALID`, `SD_INSTALL_WRITE_FAILED`, `SD_INSTALL_REJECTED`. Не + host-тестируется напрямую (тонкий ARM-only оркестратор над уже протестированными + `update_policy`/`slot_version`+ реальной FatFS/SD) — соответствует установленному в проекте принципу + "толстый слой логики тестируем на хосте, тонкий аппаратный оркестратор — только на плате". +- `firmware/bootloader/src/protocol.{c,h}` — новое событие `status` + (`{"type":"status","state":"..."}`) — `protocol_send_status()`. Пока используется для `installing` и + `waiting_for_sd`; полный словарь состояний — Фаза 4. +- `firmware/bootloader/src/main.c` — переупорядочен: `bsp_button_init()` добавлен; + `bsp_usb_cdc_init()` поднимается до `sd_update_check()`/`boot_select_and_jump()` (см. решение о CDC + выше); один комбинированный цикл ожидания вместо прежних двух (ожидание CDC / серв-цикл) — совмещает + CDC ping/pong, LED (`LED_APP`, если CDC готов, иначе мигание `LED_HEARTBEAT`) и периодический + пере-скан SD с повторным `boot_select_and_jump()`, если что-то установилось. +- `firmware/bootloader/CMakeLists.txt` — добавлены `bsp_button`, `bootloader_fatfs`, + `src/update_policy.c`, `src/slot_version.c`, `src/sd_update.c`. +- **Host-тесты**: `just build::test-host` — **15/15 таргетов зелёных** (13 существовавших до Фазы 3 + + новые `test_update_policy` [12 тестов] и `test_slot_version` [4 теста]). +- **ARM-сборка**: `just build::build-bootloader-debug` и `hab-bootloader-debug` — оба зелёные. + `m_text` — 87928 Б из 247 КБ бюджета (34.76%, был 20.05% после Фазы 2 — рост за счёт FatFS+SDMMC, + запас всё ещё больше половины). + +### Найденный и исправленный пробел: форс. даунгрейд не имел бы эффекта + +При проектировании чек-листа ниже (сценарий 3) обнаружилось: `update_policy_decide()` изначально +только писала более старый образ в неактивный слот, но **не трогала прежний активный** — а `boot_go()` +всегда выбирает более высокую версию среди валидных слотов. Значит, форс. даунгрейд физически +записался бы на flash, но реально не загрузился бы, пока прежний (более новый) активный слот не станет +невалидным сам по себе — не то поведение, которое ожидает технолог, держащий кнопку. + +**Исправлено**: `update_policy_result_t` получила поле `erase_previous_active` (`true` только для +форс. даунгрейда — не для обычного "кандидат новее", там прежний активный и так проиграет сравнение +версий естественным путём). `sd_update.c` стирает прежний активный слот **только после** того, как +`slot_version_get()` подтвердил валидность только что установленного образа — на диске никогда не +бывает нуля рабочих слотов даже на середине операции. Регрессионный тест +`test_forced_downgrade_targets_and_erases_the_higher_version_slot` — зелёный. + +### Найден и исправлен баг CI (не связан с Фазой 3 по сути, но всплыл при её работе) + +`test-host-release` в GitHub Actions падал на компиляции вендоренного +`sdk/middleware/mcuboot_opensource/boot/bootutil/src/fault_injection_hardening.c`: +`fih_panic_loop()` использует inline-asm `__asm volatile("b fih_panic_loop")` — валидная мнемоника +только для ARM/Thumb. На x86_64-раннере GitHub Actions ассемблер падает +(`invalid instruction mnemonic 'b'`); в devcontainer на arm64 та же мнемоника случайно ассемблируется +(AArch64 тоже использует `b