# bootloader: Phase 3 - checked and done 580e5f5
This commit is contained in:
parent
580e5f50d9
commit
17698ce109
36 changed files with 2562 additions and 818 deletions
|
|
@ -160,7 +160,9 @@
|
||||||
"uart_host_mock_example",
|
"uart_host_mock_example",
|
||||||
"test_ring_buffer",
|
"test_ring_buffer",
|
||||||
"test_timeout_pattern",
|
"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",
|
"uart_host_mock_example",
|
||||||
"test_ring_buffer",
|
"test_ring_buffer",
|
||||||
"test_timeout_pattern",
|
"test_timeout_pattern",
|
||||||
"test_mcuboot_boot_select"
|
"test_mcuboot_boot_select",
|
||||||
|
"test_slot_version",
|
||||||
|
"test_update_policy"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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_03_USDHC1_DATA1, pad);
|
||||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
|
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
|
||||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
|
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
|
||||||
/* CD_B в GPIO-режиме: подтяжка вверх + hysteresis для стабильного уровня. */
|
/*
|
||||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_12_GPIO2_IO28,
|
* CD_B pad config — байт-в-байт как TFT_BOOTLOADER::BOARD_SD_Pin_Config()
|
||||||
IOMUXC_SW_PAD_CTL_PAD_PKE_MASK | IOMUXC_SW_PAD_CTL_PAD_PUE_MASK |
|
* (board/sdmmc_config.c: IOMUXC_SetPinConfig(..., 0x10B0U)), не "разумная
|
||||||
IOMUXC_SW_PAD_CTL_PAD_HYS_MASK | IOMUXC_SW_PAD_CTL_PAD_PUS(1));
|
* по умолчанию" альтернатива. Отличие от прежней версии здесь: 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 питания --- */
|
/* --- GPIO питания --- */
|
||||||
sd_power_init();
|
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);
|
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);
|
||||||
const gpio_pin_config_t cd_cfg = {
|
const gpio_pin_config_t cd_cfg = {
|
||||||
.direction = kGPIO_DigitalInput,
|
.direction = kGPIO_DigitalInput,
|
||||||
|
|
|
||||||
|
|
@ -116,8 +116,7 @@
|
||||||
* @brief Размер читаемого буфера для однобайтных SR-команд.
|
* @brief Размер читаемого буфера для однобайтных SR-команд.
|
||||||
*
|
*
|
||||||
* FlexSPI FIFO работает минимальными единицами в 4 байта. Читаем 4 байта,
|
* FlexSPI FIFO работает минимальными единицами в 4 байта. Читаем 4 байта,
|
||||||
* используем только byte[0]. (Про размер watermark-юнита IPRXFSTS.FILL —
|
* используем только byte[0].
|
||||||
* см. QSPI_WM_UNIT_BYTES и комментарий в qspi_read_tail().)
|
|
||||||
*/
|
*/
|
||||||
#define SR_READ_LEN 4U
|
#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)
|
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 =
|
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_UNITS * QSPI_WM_UNIT_WORDS) >= WORDS_NEEDED)
|
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,
|
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))
|
||||||
{
|
{
|
||||||
/* IDATSZ округляем вверх до кратного QSPI_RFDR_WORD_BYTES (4).
|
/* 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 =
|
const uint32_t IDATSZ_ALIGNED =
|
||||||
(data_len + (QSPI_RFDR_WORD_BYTES - 1U)) & ~(QSPI_RFDR_WORD_BYTES - 1U);
|
(data_len + (QSPI_RFDR_WORD_BYTES - 1U)) & ~(QSPI_RFDR_WORD_BYTES - 1U);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,10 @@ bsp_status_t bsp_sd_init(void);
|
||||||
bsp_status_t bsp_sd_deinit(void);
|
bsp_status_t bsp_sd_deinit(void);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Проверить физическое наличие карты через регистр USDHC PRSSTAT.
|
* Проверить физическое наличие карты через USDHC PRES_STATE.CINST
|
||||||
* Не требует предварительного вызова bsp_sd_init().
|
* (USDHC_GetPresentStatusFlags). Включает тактирование USDHC1 на время
|
||||||
* Включает тактирование USDHC1 на время чтения регистра.
|
* чтения; не требует предварительного вызова bsp_sd_init(). Корректно
|
||||||
|
* пока пин D13 замаплен на USDHC1_CD_B.
|
||||||
*/
|
*/
|
||||||
bool bsp_sd_is_inserted(void);
|
bool bsp_sd_is_inserted(void);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,11 @@
|
||||||
#include "bsp/sd.h"
|
#include "bsp/sd.h"
|
||||||
|
|
||||||
#include "fsl_sd.h"
|
#include "fsl_sd.h"
|
||||||
#include "fsl_usdhc.h" /* USDHC_Reset — аппаратный сброс FIFO/state machine */
|
#include "fsl_usdhc.h"
|
||||||
#include "sdmmc_config.h" /* BOARD_SD_Config, BOARD_SDMMC_SD_HOST_BASEADDR */
|
#include "sdmmc_config.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#include <string.h> /* 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;
|
extern sd_card_t g_sd;
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
|
|
@ -37,7 +33,8 @@ static void ensure_host_configured(void)
|
||||||
{
|
{
|
||||||
return;
|
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);
|
BOARD_SD_Config(&g_sd, NULL, BOARD_SDMMC_SD_HOST_IRQ_PRIORITY, NULL);
|
||||||
g_s_host_configured = true;
|
g_s_host_configured = true;
|
||||||
}
|
}
|
||||||
|
|
@ -54,19 +51,8 @@ bsp_status_t bsp_sd_init(void)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Аппаратный сброс USDHC FIFO + command/data state machine ПЕРЕД
|
* Аппаратный сброс 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 на этот момент должен быть включён, см. ниже).
|
|
||||||
*/
|
*/
|
||||||
CLOCK_EnableClock(kCLOCK_Usdhc1); /* тактирование нужно ДО сброса регистров */
|
CLOCK_EnableClock(kCLOCK_Usdhc1); /* тактирование нужно ДО сброса регистров */
|
||||||
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
|
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));
|
(void) memset(&g_sd, 0, sizeof(g_sd));
|
||||||
g_s_host_configured = false; /* форсируем повторный BOARD_SD_Config ниже */
|
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 */
|
||||||
|
|
||||||
/*
|
if (SD_HostInit(&g_sd) != kStatus_Success)
|
||||||
* Полный init (host + card) происходит в sd_disk_initialize → SD_Init,
|
{
|
||||||
* который вызывается из f_mount → disk_initialize.
|
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;
|
g_s_initialized = true;
|
||||||
return BSP_OK;
|
return BSP_OK;
|
||||||
|
|
@ -101,12 +95,6 @@ bsp_status_t bsp_sd_deinit(void)
|
||||||
SD_HostDeinit(&g_sd);
|
SD_HostDeinit(&g_sd);
|
||||||
SD_SetCardPower(&g_sd, false);
|
SD_SetCardPower(&g_sd, false);
|
||||||
|
|
||||||
/*
|
|
||||||
* Дополнительный аппаратный сброс сразу после deinit — гарантирует,
|
|
||||||
* что FIFO и state machine USDHC не останутся в промежуточном
|
|
||||||
* состоянии независимо от того, что делает (или не делает)
|
|
||||||
* SD_HostDeinit() из SDK на уровне регистров.
|
|
||||||
*/
|
|
||||||
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
|
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
|
||||||
|
|
||||||
g_s_initialized = false;
|
g_s_initialized = false;
|
||||||
|
|
@ -116,6 +104,8 @@ bsp_status_t bsp_sd_deinit(void)
|
||||||
|
|
||||||
bool bsp_sd_is_inserted(void)
|
bool bsp_sd_is_inserted(void)
|
||||||
{
|
{
|
||||||
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) ==
|
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
||||||
BOARD_SDMMC_SD_CD_INSERT_LEVEL;
|
|
||||||
|
uint32_t ps = USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR);
|
||||||
|
return (ps & kUSDHC_CardInsertedFlag) != 0U;
|
||||||
}
|
}
|
||||||
|
|
@ -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/сценарии исполнения кода.
|
- [BOOT_FLAGS.md](BOOT_FLAGS.md) — XIP/DCD/сценарии исполнения кода.
|
||||||
- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись bootloader (HAB) vs подпись образов tft_app (`imgtool`).
|
- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись bootloader (HAB) vs подпись образов tft_app (`imgtool`).
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,18 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in"
|
||||||
# Фаза 2.
|
# Фаза 2.
|
||||||
include(${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/bootutil_sources.cmake)
|
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(
|
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
|
src/boot_select.c
|
||||||
|
src/update_policy.c
|
||||||
|
src/slot_version.c
|
||||||
|
src/sd_update.c
|
||||||
mcuboot_port/flash_map_backend.c
|
mcuboot_port/flash_map_backend.c
|
||||||
mcuboot_port/keys.c
|
mcuboot_port/keys.c
|
||||||
${MCUBOOT_BOOTUTIL_SOURCES}
|
${MCUBOOT_BOOTUTIL_SOURCES}
|
||||||
|
|
@ -40,11 +46,13 @@ target_compile_definitions(
|
||||||
__STARTUP_INITIALIZE_NONCACHEDATA)
|
__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
|
target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick
|
||||||
bsp_usb_cdc bsp_qspi_flash
|
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, ограниченным бюджетом
|
# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом
|
||||||
|
|
|
||||||
507
firmware/bootloader/DEBUG_LOG_PHASE3_SD.md
Normal file
507
firmware/bootloader/DEBUG_LOG_PHASE3_SD.md
Normal file
|
|
@ -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 — см.
|
||||||
|
явные пометки).
|
||||||
|
|
||||||
|
<details><summary>История расследования (раунды 1–2, выводы частично опровергнуты)</summary>
|
||||||
|
|
||||||
|
**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.**
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Этот файл — снимок состояния расследования на момент передачи в отдельный тред. Код Фазы 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 → <signal handler called> → 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) содержит `<pin peripheral="USDHC1" signal="usdhc_cd_b" .../>` для
|
||||||
|
этого пина — маршрутизация на уровне самого 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) — судя по всему, файл планировался, но не был
|
||||||
|
создан. Не тема этого лога, но стоит либо создать его отдельно, либо убрать ссылки, когда дойдут руки.
|
||||||
|
|
@ -7,7 +7,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-тесты 5/5, аппаратная верификация — все 5 сценариев пройдены на реальной плате (детали и 3 найденных/исправленных бага — [DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md)) |
|
| 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-паттерны | не начата | |
|
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | |
|
||||||
| 5 — HAB Release + service-tui | не начата | |
|
| 5 — HAB Release + service-tui | не начата | |
|
||||||
|
|
||||||
|
|
@ -23,7 +23,9 @@
|
||||||
|
|
||||||
- **Boot-стратегия**: и bootloader, и tft_app исполняются XIP из W25Q. Bootloader без ITCM-копирования,
|
- **Boot-стратегия**: и bootloader, и tft_app исполняются XIP из W25Q. Bootloader без ITCM-копирования,
|
||||||
без DCD — он не трогает SDRAM. tft_app сама поднимает SEMC в своём раннем startup (SDRAM — под её XIP,
|
без 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/Б) с полностью валидными образами каждый,
|
- **Схема обновления**: MCUboot **Direct-XIP**, два слота (A/Б) с полностью валидными образами каждый,
|
||||||
без swap/scratch. Единственный полевой канал обновления — microSD. USB как канал заливки *образа*
|
без swap/scratch. Единственный полевой канал обновления — microSD. USB как канал заливки *образа*
|
||||||
сознательно не делаем (SDP/blhost на производстве — это отдельный, не зависящий от кода bootloader,
|
сознательно не делаем (SDP/blhost на производстве — это отдельный, не зависящий от кода bootloader,
|
||||||
|
|
@ -418,31 +420,192 @@ bsp_qspi_read → qspi_ip_read → qspi_read_fifo → qspi_read_tail`) видн
|
||||||
|
|
||||||
**Цель**: сканирование microSD, установка образа в слот, top-level состояние "нет валидного образа".
|
**Цель**: сканирование 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 <label>`), маскируя проблему — отсюда расхождение "в devcontainer всё
|
||||||
|
хорошо". Исправлено точечным патчем вендоренного файла: `__asm` под `#if defined(__arm__)` (реальный
|
||||||
|
ARM-таргет — без изменений), иначе (host, любая архитектура) — портируемый `for (;;) {}`. Подтверждено
|
||||||
|
кросс-компиляцией под `-target x86_64-unknown-linux-gnu` (0 ошибок после фикса, была именно эта ошибка
|
||||||
|
до него).
|
||||||
|
|
||||||
|
### Запланировано (обязательно к реализации): аппаратный watchdog
|
||||||
|
|
||||||
|
Найдено при аппаратной верификации (см. [DEBUG_LOG_PHASE3_SD.md](DEBUG_LOG_PHASE3_SD.md)): у
|
||||||
|
`SD_PollingCardInsert()` (SDK, вызывается изнутри `SD_Init()`, который вызывается изнутри
|
||||||
|
`f_mount()`) при типе детекта `kSD_DetectCardByGpioCD` **нет тайм-аута вообще** — это `do {...}
|
||||||
|
while (true)` без выхода по времени, если callback `cardDetected()` ни разу не вернёт ожидаемое
|
||||||
|
для текущего условия ожидания значение. На реальном железе уже дважды пойман сценарий, когда
|
||||||
|
исполнение зависает внутри вендоренного SD/USDHC-стека (не только в этой функции — второй найденный
|
||||||
|
зависон, в `SD_ReadStatus`/`OSA_SemaphoreWait`, тоже блокирующий и без видимого тайм-аута) — то есть
|
||||||
|
риск не ограничивается одной конкретной причиной, которую можно точечно пропатчить, а системный:
|
||||||
|
любой blocking-вызов в SD/USDHC-стеке потенциально способен подвесить весь bootloader навсегда.
|
||||||
|
|
||||||
|
Для загрузчика это неприемлемо: зависание до `boot_select_and_jump()` означает, что даже валидный
|
||||||
|
образ в Slot A никогда не загрузится без внешнего вмешательства (SWD-сброс/перезагрузка вручную) —
|
||||||
|
на производстве это означает бракованную единицу без возможности восстановления штатными средствами.
|
||||||
|
|
||||||
|
**Решение — аппаратный WDOG (WDOG1 или WDOG2, MIMXRT1052)**:
|
||||||
|
- Инициализация с разумным тайм-аутом (ориентир 5-8 с — с запасом над самой длинной легитимной
|
||||||
|
операцией, потоковым копированием чанка 4 КБ SD→flash) как можно раньше в `main.c`, после
|
||||||
|
`board_hw_init()`.
|
||||||
|
- "Кормление" (`WDOG_Refresh`/эквивалент) — только в точках, где мы **уверены**, что прогресс
|
||||||
|
реален: начало каждой итерации главного цикла `main.c`, и внутри `erase_and_copy_candidate()`
|
||||||
|
(`sd_update.c`) на каждой итерации цикла копирования чанков (там уже есть `bsp_usb_cdc_poll()` —
|
||||||
|
рядом). **Не кормить** непосредственно перед вызовом функций SDK, которые могут заблокироваться
|
||||||
|
бесконечно (`f_mount`, `SD_Init` и всё, что внутри) — иначе watchdog перестаёт быть защитой именно
|
||||||
|
от них.
|
||||||
|
- Ресет по watchdog безопасен по построению: ни один слот не стирается до подтверждения валидности
|
||||||
|
только что записанного образа (см. `erase_previous_active` выше) — WDOG-ресет в худшем случае
|
||||||
|
просто откатывает к состоянию "начать сессию заново", как обычный power cycle.
|
||||||
|
- Не реализовано в рамках текущей сессии — статус явно "запланировано", реализация и аппаратная
|
||||||
|
проверка (спровоцировать реальное зависание и убедиться, что WDOG восстанавливает работу) —
|
||||||
|
следующий шаг после того, как будет закрыт текущий SD/card-detect баг (иначе watchdog будет
|
||||||
|
маскировать/сбрасывать симптом на каждом цикле, не давая найти первопричину).
|
||||||
|
|
||||||
|
**Верификация (первая часть Фазы 3, требующая реального железа — ещё не проведена)**:
|
||||||
1. Чистая плата (только bootloader, оба слота пустые) + SD с валидным подписанным образом →
|
1. Чистая плата (только bootloader, оба слота пустые) + SD с валидным подписанным образом →
|
||||||
автоустановка, переход к загрузке (проверить по CDC-статусам и LED).
|
автоустановка, переход к загрузке (проверить по CDC-статусам и LED).
|
||||||
2. То же SD с образом версии ниже уже установленной, кнопка не нажата → отклонён, лог на CDC.
|
2. То же SD с образом версии ниже уже установленной, кнопка не нажата → отклонён
|
||||||
3. То же, кнопка `BSP_BUTTON_1` удержана при старте → установлен.
|
(`status: update_skipped` на CDC).
|
||||||
4. SD с образом без подписи/битым TLV → отклонён, плата остаётся на последнем валидном слоте (если был)
|
3. То же, кнопка `BSP_BUTTON_1` удержана при старте → установлен И загружен (прежний активный слот
|
||||||
или продолжает ждать (если не было), характерный error-паттерн LED.
|
стирается автоматически — см. находку выше).
|
||||||
|
4. SD с образом без подписи/битым TLV → отклонён (`error: SD_CANDIDATE_INVALID` или
|
||||||
|
`SD_INSTALL_REJECTED`, в зависимости от того, где именно битый образ — в заголовке или в TLV/подписи),
|
||||||
|
плата остаётся на последнем валидном слоте (если был) или продолжает ждать (если не было).
|
||||||
5. Сценарий 2 (бандл залит сразу в Slot A через SWD/SDP на этапе тестирования): SD не участвует,
|
5. Сценарий 2 (бандл залит сразу в Slot A через SWD/SDP на этапе тестирования): SD не участвует,
|
||||||
bootloader должен просто загрузить Slot A без обращения к SD-логике вообще.
|
bootloader должен просто загрузить Slot A без обращения к SD-логике вообще.
|
||||||
|
|
||||||
|
Понадобится тестовый образ на SD — переиспользовать `test_stub` из Фазы 2 (`stub_a_v1_confirmed.bin`
|
||||||
|
и т.п., уже собираются `just build::build-mcuboot-stub`), переименованный/скопированный в
|
||||||
|
`TFT_APP.BIN`, а не дожидаться реального `tft_app`. Точный порядок команд — см. чат/раздел ниже
|
||||||
|
(зависит от того, что именно уже стоит на конкретной тестовой плате).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 4 — SDRAM/W25Q smoke-test + словарь LED-паттернов
|
## Фаза 4 — SDRAM/W25Q smoke-test + словарь LED-паттернов
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,24 @@
|
||||||
> SWD — не в поле. Единственный канал диагностики: USB CDC ACM (JSON-lines,
|
> SWD — не в поле. Единственный канал диагностики: USB CDC ACM (JSON-lines,
|
||||||
> тот же стиль протокола, что у `firmware_test`).
|
> тот же стиль протокола, что у `firmware_test`).
|
||||||
>
|
>
|
||||||
> Версия: `0.1.0` | Статус: Фаза 1 (скелет) завершена — см. [PLAN.md](PLAN.md)
|
> Версия: `0.1.0` | Статус: Фаза 2 (bootutil / MCUboot Direct-XIP) завершена — см. [PLAN.md](PLAN.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Содержание
|
## Содержание
|
||||||
|
|
||||||
- [Быстрый старт](#быстрый-старт)
|
- [bootloader](#bootloader)
|
||||||
- [Архитектура](#архитектура)
|
- [Содержание](#содержание)
|
||||||
- [Карта Flash](#карта-flash)
|
- [Быстрый старт](#быстрый-старт)
|
||||||
- [Протокол](#протокол)
|
- [1. Сборка](#1-сборка)
|
||||||
- [Roadmap](#roadmap)
|
- [2. Прошивка](#2-прошивка)
|
||||||
- [Версионирование](#версионирование)
|
- [3. Подключение](#3-подключение)
|
||||||
|
- [4. Отладка](#4-отладка)
|
||||||
|
- [Архитектура](#архитектура)
|
||||||
|
- [Карта Flash](#карта-flash)
|
||||||
|
- [Протокол](#протокол)
|
||||||
|
- [Roadmap](#roadmap)
|
||||||
|
- [Версионирование](#версионирование)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -46,6 +52,11 @@ just host::flash bootloader debug
|
||||||
|
|
||||||
### 3. Подключение
|
### 3. Подключение
|
||||||
|
|
||||||
|
CDC поднимается, только если в обоих слотах (A/Б) нет валидного образа — иначе
|
||||||
|
bootloader сразу выбирает слот и прыгает в `tft_app` (Direct-XIP), не поднимая
|
||||||
|
USB вообще. Для проверки диагностического режима слоты должны быть пустыми
|
||||||
|
или содержать только невалидные образы.
|
||||||
|
|
||||||
USB CDC ACM (тот же порядок, что у `firmware_test`):
|
USB CDC ACM (тот же порядок, что у `firmware_test`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -73,17 +84,31 @@ VSCode → `🐛 Debug: bootloader` (`.vscode/launch.json`) — пересоби
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|
||||||
Bring-up идентичен `firmware_test` (`firmware/test/src/main.c`), но без
|
Bring-up идентичен `firmware_test` (`firmware/test/src/main.c`), но вместо
|
||||||
тестового раннера — bootloader не запускает тесты, только диагностический
|
тестового раннера — сразу попытка выбрать и запустить `tft_app` из одного из
|
||||||
CLI:
|
двух слотов (MCUboot Direct-XIP, `bootutil`), и только если валидного образа
|
||||||
|
нет ни в одном слоте — диагностический CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
firmware/bootloader/
|
firmware/bootloader/
|
||||||
├── CMakeLists.txt
|
├── CMakeLists.txt
|
||||||
|
├── mcuboot_port/ — интеграция bootutil (MCUboot) с bsp_qspi_flash
|
||||||
|
│ ├── flash_map_backend.c — flash_area_* поверх bsp_qspi_flash (2 слота)
|
||||||
|
│ ├── keys.c — публичный ключ ECDSA-P256 для imgtool-подписи
|
||||||
|
│ ├── bootutil_sources.cmake — общий список исходников bootutil+TinyCrypt+
|
||||||
|
│ │ ASN.1 (используется и host-тестами)
|
||||||
|
│ └── mcuboot_config/, sysflash/, flash_map_backend/, keys/ — конфиг-заголовки
|
||||||
|
│
|
||||||
└── src/
|
└── src/
|
||||||
├── main.c — board_hw_init → led/tick/usb_cdc init →
|
├── main.c — board_hw_init → led/tick/qspi init →
|
||||||
│ ожидание CDC (LED_HEARTBEAT мигает) →
|
│ boot_select_and_jump() (при успехе не
|
||||||
│ LED_APP on → главный цикл (poll + cli_process)
|
│ возвращается) → иначе usb_cdc init → ожидание
|
||||||
|
│ CDC (LED_HEARTBEAT мигает) → LED_APP on →
|
||||||
|
│ главный цикл (poll + cli_process)
|
||||||
|
│
|
||||||
|
├── boot_select.c/.h — обёртка над bootutil: `boot_go()` (выбор
|
||||||
|
│ валидного слота по подписи/версии) + прыжок в
|
||||||
|
│ выбранный образ (`jump_to_image`)
|
||||||
│
|
│
|
||||||
├── version.h.in — шаблон версии (CMake → generated/version.h)
|
├── version.h.in — шаблон версии (CMake → generated/version.h)
|
||||||
│
|
│
|
||||||
|
|
@ -98,8 +123,13 @@ Bootloader **без SDRAM** — DCD не используется (`bsp_boot_xip
|
||||||
(см. [BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md)).
|
(см. [BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md)).
|
||||||
|
|
||||||
Линкер: `cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld` — `m_text`
|
Линкер: `cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld` — `m_text`
|
||||||
жёстко ограничен 247 КБ, с `ASSERT` на границу Slot A. Превышение бюджета —
|
жёстко ограничен 247 КБ, с `ASSERT` на границу Slot A.
|
||||||
ошибка линковки, а не тихий выход кода за пределы своей области.
|
|
||||||
|
Верификация образов — подпись ECDSA-P256 (bootutil/TinyCrypt), два независимых
|
||||||
|
слота (A/Б) по 2 МБ каждый, без swap/scratch (Direct-XIP). Аппаратно
|
||||||
|
верифицировано все 5 сценариев чек-листа (единственный валидный слот, выбор
|
||||||
|
более новой версии, откат при повреждении, оба слота пусты, revert
|
||||||
|
неподтверждённого образа)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -109,11 +139,11 @@ Bootloader **без SDRAM** — DCD не используется (`bsp_boot_xip
|
||||||
ёмкость чипа" — в
|
ёмкость чипа" — в
|
||||||
[docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md).
|
[docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md).
|
||||||
|
|
||||||
| Область | Смещение | Размер |
|
| Область | Смещение | Размер |
|
||||||
|---|---|---|
|
| --------------------------- | ------------ | ------------------- |
|
||||||
| Bootloader | `0x60000000` | 256 КБ |
|
| Bootloader | `0x60000000` | 256 КБ |
|
||||||
| Slot A (tft_app) | `0x60040000` | 2 МБ |
|
| Slot A (tft_app) | `0x60040000` | 2 МБ |
|
||||||
| Slot Б (tft_app) | `0x60240000` | 2 МБ |
|
| Slot Б (tft_app) | `0x60240000` | 2 МБ |
|
||||||
| ФС ассетов (спрайты/музыка) | `0x60440000` | остальное (рантайм) |
|
| ФС ассетов (спрайты/музыка) | `0x60440000` | остальное (рантайм) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -126,9 +156,9 @@ JSON-lines, максимум 128 байт на строку.
|
||||||
|
|
||||||
**Реализовано (Фаза 1):**
|
**Реализовано (Фаза 1):**
|
||||||
|
|
||||||
| Команда хоста | Ответ |
|
| Команда хоста | Ответ |
|
||||||
|---|---|
|
| ------------------------------------ | ------------------------------------------ |
|
||||||
| `{"type":"cmd","cmd":"ping"}` | `{"type":"pong"}` |
|
| `{"type":"cmd","cmd":"ping"}` | `{"type":"pong"}` |
|
||||||
| `{"type":"cmd","cmd":"get_version"}` | `{"type":"version_response","fw":"X.Y.Z"}` |
|
| `{"type":"cmd","cmd":"get_version"}` | `{"type":"version_response","fw":"X.Y.Z"}` |
|
||||||
|
|
||||||
Ошибки: `{"ok":false,"error":"PARSE_ERR"}` / `"UNKNOWN_CMD"` / `"LINE_TOO_LONG"`.
|
Ошибки: `{"ok":false,"error":"PARSE_ERR"}` / `"UNKNOWN_CMD"` / `"LINE_TOO_LONG"`.
|
||||||
|
|
@ -147,14 +177,14 @@ JSON-lines, максимум 128 байт на строку.
|
||||||
|
|
||||||
Полный план по фазам с целями и критериями верификации — [PLAN.md](PLAN.md).
|
Полный план по фазам с целями и критериями верификации — [PLAN.md](PLAN.md).
|
||||||
|
|
||||||
| Фаза | Что делает |
|
| Фаза | Что делает |
|
||||||
|---|---|
|
| ---- | ---------------------------------------------------------------- |
|
||||||
| 0 ✅ | Карта Flash, регистрация в сборке |
|
| 0 ✅ | Карта Flash, регистрация в сборке |
|
||||||
| 1 ✅ | Скелет: bring-up, USB CDC, ping/get_version, LED heartbeat |
|
| 1 ✅ | Скелет: bring-up, USB CDC, ping/get_version, LED heartbeat |
|
||||||
| 2 | bootutil (MCUboot Direct-XIP) — выбор слота, верификация подписи |
|
| 2 ✅ | bootutil (MCUboot Direct-XIP) — выбор слота, верификация подписи |
|
||||||
| 3 | Установка образа с microSD, состояние "нет валидного слота" |
|
| 3 | Установка образа с microSD, состояние "нет валидного слота" |
|
||||||
| 4 | SDRAM/W25Q smoke-test, словарь LED-паттернов |
|
| 4 | SDRAM/W25Q smoke-test, словарь LED-паттернов |
|
||||||
| 5 | HAB Release, интеграция в service-tui |
|
| 5 | HAB Release, интеграция в service-tui |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
35
firmware/bootloader/fatfs/CMakeLists.txt
Normal file
35
firmware/bootloader/fatfs/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# bootloader_fatfs — FatFS, скомпилированный с bare-metal ffconf.h для
|
||||||
|
# bootloader.
|
||||||
|
#
|
||||||
|
# ff.c, fsl_sd_disk.c, diskio_sd.c включают ff.h → ffconf.h. Все три
|
||||||
|
# компилируются здесь, чтобы видели один и тот же ffconf.h из
|
||||||
|
# ${CMAKE_CURRENT_SOURCE_DIR}/include.
|
||||||
|
#
|
||||||
|
# Не шарить с firmware_test_fatfs: bootloader и firmware_test —
|
||||||
|
# взаимоисключающие прошивки одной платы (см. firmware/bootloader/PLAN.md,
|
||||||
|
# Фаза 3) — зависимость от таргета с именем "firmware_test" была бы неверной
|
||||||
|
# связью. tft_app заведёт свой аналогичный таргет с FreeRTOS ffconf.h.
|
||||||
|
#
|
||||||
|
# Отличие от firmware_test_fatfs: FF_FS_READONLY=1 — bootloader только читает
|
||||||
|
# TFT_APP.BIN, никогда не пишет на SD (см. include/ffconf.h).
|
||||||
|
|
||||||
|
add_library(
|
||||||
|
bootloader_fatfs STATIC
|
||||||
|
${SDK_FATFS_FF_SRC} # sdk/middleware/fatfs/source/ff.c
|
||||||
|
${SDK_FATFS_SD_DISK_SRC} # sdk/middleware/fatfs/source/fsl_sd_disk/fsl_sd_disk.c
|
||||||
|
${PORT_FATFS_SD_SRC} # port/fatfs/sd/src/diskio_sd.c
|
||||||
|
src/diskio.c)
|
||||||
|
|
||||||
|
# include/ первым — ffconf.h отсюда должен перекрыть любой шаблонный
|
||||||
|
target_include_directories(
|
||||||
|
bootloader_fatfs
|
||||||
|
PUBLIC include # ffconf.h, виден потребителям (sd_update.c)
|
||||||
|
PRIVATE src)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
bootloader_fatfs
|
||||||
|
PUBLIC port_fatfs_sd # diskio_sd.h, ff.h, fsl_sd_disk.h, bsp_sd
|
||||||
|
PRIVATE bsp_sdmmc_config) # SD_ENABLED транзитивно из bsp_sd, но явно для
|
||||||
|
# ясности
|
||||||
|
|
||||||
|
target_compile_options(bootloader_fatfs PRIVATE -w)
|
||||||
86
firmware/bootloader/fatfs/include/ffconf.h
Normal file
86
firmware/bootloader/fatfs/include/ffconf.h
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* ffconf.h — конфигурация FatFS для bootloader (bare-metal, только SD).
|
||||||
|
*
|
||||||
|
* Копия паттерна firmware/test/fatfs/include/ffconf.h — не шарить: bootloader
|
||||||
|
* и firmware_test взаимоисключающие прошивки одной платы (см.
|
||||||
|
* firmware/bootloader/PLAN.md, Фаза 3). tft_app заведёт свой аналогичный
|
||||||
|
* таргет с FreeRTOS ffconf.h.
|
||||||
|
*
|
||||||
|
* Ключевое отличие от firmware_test:
|
||||||
|
* FF_FS_READONLY = 1 — bootloader только читает TFT_APP.BIN с SD, никогда
|
||||||
|
* не пишет на карту; убирает f_write и весь путь
|
||||||
|
* записи FatFS из сборки.
|
||||||
|
* FF_FS_REENTRANT = 0 — нет RTOS, нет мьютексов
|
||||||
|
* FF_VOLUMES = 3 — 0: зарезервирован, 1: зарезервирован, 2: SD
|
||||||
|
* FF_MAX_SS = 512 — SD всегда 512 байт/сектор, ioctl не нужен
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _FFCONF_H_
|
||||||
|
#define _FFCONF_H_
|
||||||
|
|
||||||
|
#define FFCONF_DEF 80286
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------------------/
|
||||||
|
/ MSDK adaptation
|
||||||
|
/---------------------------------------------------------------------------*/
|
||||||
|
#define SD_DISK_ENABLE 1
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------------------/
|
||||||
|
/ Function Configurations
|
||||||
|
/---------------------------------------------------------------------------*/
|
||||||
|
#define FF_FS_READONLY 1 /* bootloader никогда не пишет на SD */
|
||||||
|
#define FF_FS_MINIMIZE 0
|
||||||
|
#define FF_USE_FIND 0
|
||||||
|
#define FF_USE_MKFS 0 /* f_mkfs не нужна — карта уже отформатирована */
|
||||||
|
#define FF_USE_FASTSEEK 0
|
||||||
|
#define FF_USE_EXPAND 0
|
||||||
|
#define FF_USE_CHMOD 0
|
||||||
|
#define FF_USE_LABEL 0
|
||||||
|
#define FF_USE_FORWARD 0
|
||||||
|
#define FF_USE_STRFUNC 0
|
||||||
|
#define FF_PRINT_LLI 0
|
||||||
|
#define FF_PRINT_FLOAT 0
|
||||||
|
#define FF_STRF_ENCODE 3
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------------------/
|
||||||
|
/ Locale
|
||||||
|
/---------------------------------------------------------------------------*/
|
||||||
|
#define FF_CODE_PAGE 437 /* U.S. — минимальный, имена файлов ASCII */
|
||||||
|
|
||||||
|
#define FF_USE_LFN 0 /* только 8.3 — достаточно для TFT_APP.BIN */
|
||||||
|
#define FF_MAX_LFN 255
|
||||||
|
#define FF_LFN_UNICODE 0
|
||||||
|
#define FF_LFN_BUF 255
|
||||||
|
#define FF_SFN_BUF 12
|
||||||
|
#define FF_FS_RPATH 0 /* относительные пути не нужны */
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------------------/
|
||||||
|
/ Drive/Volume Configurations
|
||||||
|
/---------------------------------------------------------------------------*/
|
||||||
|
#define FF_VOLUMES 3 /* 0: зарезервирован, 1: зарезервирован, 2: SD */
|
||||||
|
#define FF_STR_VOLUME_ID 0
|
||||||
|
#define FF_MULTI_PARTITION 0
|
||||||
|
|
||||||
|
#define FF_MIN_SS 512
|
||||||
|
#define FF_MAX_SS 512 /* SD: всегда 512, GET_SECTOR_SIZE не нужен */
|
||||||
|
|
||||||
|
#define FF_LBA64 0
|
||||||
|
#define FF_MIN_GPT 0x10000000
|
||||||
|
#define FF_USE_TRIM 0
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------------------/
|
||||||
|
/ System Configurations
|
||||||
|
/---------------------------------------------------------------------------*/
|
||||||
|
#define FF_FS_TINY 0
|
||||||
|
#define FF_FS_EXFAT 0 /* exFAT требует LFN — оба отключены */
|
||||||
|
#define FF_FS_NORTC 1
|
||||||
|
#define FF_NORTC_MON 1
|
||||||
|
#define FF_NORTC_MDAY 1
|
||||||
|
#define FF_NORTC_YEAR 2024
|
||||||
|
#define FF_FS_NOFSINFO 0
|
||||||
|
#define FF_FS_LOCK 0
|
||||||
|
|
||||||
|
#define FF_FS_REENTRANT 0 /* bare-metal: нет RTOS, нет мьютексов */
|
||||||
|
/* FF_FS_TIMEOUT и FF_SYNC_t не нужны */
|
||||||
|
|
||||||
|
#endif /* _FFCONF_H_ */
|
||||||
53
firmware/bootloader/fatfs/src/diskio.c
Normal file
53
firmware/bootloader/fatfs/src/diskio.c
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* diskio.c — FatFS diskio диспетчер для bootloader.
|
||||||
|
*
|
||||||
|
* FF_VOLUMES=3: диски 0 и 1 — заглушки, диск 2 = SDDISK (microSD).
|
||||||
|
* W25Q и RAM-диск отсутствуют — нет зависимости на bsp_qspi_flash (слоты A/Б
|
||||||
|
* читаются/пишутся напрямую через flash_area_*, в обход FatFS).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "diskio.h"
|
||||||
|
|
||||||
|
#include "port/fatfs/diskio_sd.h"
|
||||||
|
|
||||||
|
#define SDDISK 2U
|
||||||
|
|
||||||
|
DSTATUS disk_initialize(BYTE pdrv)
|
||||||
|
{
|
||||||
|
if (pdrv == SDDISK)
|
||||||
|
{
|
||||||
|
return microsd_disk_initialize(pdrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return STA_NOINIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
DSTATUS disk_status(BYTE pdrv)
|
||||||
|
{
|
||||||
|
if (pdrv == SDDISK)
|
||||||
|
{
|
||||||
|
return microsd_disk_status(pdrv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return STA_NOINIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
|
||||||
|
{
|
||||||
|
if (pdrv == SDDISK)
|
||||||
|
{
|
||||||
|
return microsd_disk_read(pdrv, buff, sector, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RES_PARERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
|
||||||
|
{
|
||||||
|
if (pdrv == SDDISK)
|
||||||
|
{
|
||||||
|
return microsd_disk_ioctl(pdrv, cmd, buff);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RES_PARERR;
|
||||||
|
}
|
||||||
|
|
@ -14,11 +14,10 @@
|
||||||
* (main.c, до boot_go()).
|
* (main.c, до boot_go()).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "bsp/qspi_flash.h"
|
||||||
#include "flash_map.h"
|
#include "flash_map.h"
|
||||||
#include "sysflash/sysflash.h"
|
#include "sysflash/sysflash.h"
|
||||||
|
|
||||||
#include "bsp/qspi_flash.h"
|
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define ERASED_VAL 0xFFU
|
#define ERASED_VAL 0xFFU
|
||||||
|
|
@ -26,10 +25,16 @@
|
||||||
/* Slot A/Б — см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md. Смещения —
|
/* Slot A/Б — см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md. Смещения —
|
||||||
* flash-relative (от начала чипа), не XIP-адрес. */
|
* flash-relative (от начала чипа), не XIP-адрес. */
|
||||||
static const struct flash_area g_s_areas[2] = {
|
static const struct flash_area g_s_areas[2] = {
|
||||||
{ .fa_id = 0U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U,
|
{ .fa_id = 0U,
|
||||||
.fa_off = 0x00040000UL, .fa_size = 0x00200000UL }, /* Slot A: 0x60040000, 2 МБ */
|
.fa_device_id = FLASH_DEVICE_ID,
|
||||||
{ .fa_id = 1U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U,
|
.pad16 = 0U,
|
||||||
.fa_off = 0x00240000UL, .fa_size = 0x00200000UL }, /* Slot Б: 0x60240000, 2 МБ */
|
.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) ─────────
|
/* ── Постраничная запись (аналог NXP flash_area_write_internal) ─────────
|
||||||
|
|
@ -128,6 +133,28 @@ int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fast-path: стирание всей области целиком (off=0, len=fa_size), кратно
|
||||||
|
* 64 КБ — блочное стирание ~5x быстрее посекторного (2 МБ: ~4.8 с против
|
||||||
|
* ~23 с, см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md, "Обоснование
|
||||||
|
* размеров"). Ускоряет и sd_update (Фаза 3), и штатный revert-erase
|
||||||
|
* bootutil (boot_select_or_erase() в loader.c) — они уже зовут
|
||||||
|
* flash_area_erase(fap, 0, flash_area_get_size(fap)) без изменений.
|
||||||
|
* Частичное/невыровненное стирание (напр. один трейлер) — прежний
|
||||||
|
* посекторный путь ниже. */
|
||||||
|
if ((off == 0U) && (len == area->fa_size) && ((len % BSP_QSPI_BLOCK_64K_SIZE) == 0U))
|
||||||
|
{
|
||||||
|
uint32_t block_addr = area->fa_off;
|
||||||
|
for (; len > 0U; len -= BSP_QSPI_BLOCK_64K_SIZE)
|
||||||
|
{
|
||||||
|
if (bsp_qspi_erase_block_64k(block_addr) != BSP_OK)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
block_addr += BSP_QSPI_BLOCK_64K_SIZE;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t addr = area->fa_off + off;
|
uint32_t addr = area->fa_off + off;
|
||||||
for (; len > 0U; len -= BSP_QSPI_SECTOR_SIZE)
|
for (; len > 0U; len -= BSP_QSPI_SECTOR_SIZE)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
*
|
*
|
||||||
* Референс — sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot():
|
* Референс — sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot():
|
||||||
* та же последовательность (flash_device_base → вычислить адрес vector table →
|
* та же последовательность (flash_device_base → вычислить адрес vector table →
|
||||||
* __set_MSP → __ISB → прыжок на Reset_Handler), CMSIS-интринсики, без ассемблера.
|
* cleanup → __set_MSP → __ISB → прыжок на Reset_Handler), CMSIS-интринсики, без
|
||||||
|
* ассемблера. cleanup_before_jump()/VTOR добавлены в Фазе 3 — см. её docstring
|
||||||
|
* про найденный на железе баг (прыжок сразу после bsp_usb_cdc_init()).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "boot_select.h"
|
#include "boot_select.h"
|
||||||
|
|
@ -12,7 +14,6 @@
|
||||||
#include "bootutil/bootutil.h"
|
#include "bootutil/bootutil.h"
|
||||||
#include "bootutil/fault_injection_hardening.h"
|
#include "bootutil/fault_injection_hardening.h"
|
||||||
#include "flash_map.h"
|
#include "flash_map.h"
|
||||||
|
|
||||||
#include "fsl_common.h"
|
#include "fsl_common.h"
|
||||||
|
|
||||||
struct arm_vector_table
|
struct arm_vector_table
|
||||||
|
|
@ -21,6 +22,53 @@ struct arm_vector_table
|
||||||
uint32_t reset;
|
uint32_t reset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Вернуть NVIC/SysTick в состояние "как после аппаратного сброса"
|
||||||
|
* перед прыжком — целевой образ не должен унаследовать прерывания,
|
||||||
|
* включённые bootloader'ом.
|
||||||
|
*
|
||||||
|
* Найдено на реальном железе (Фаза 3): Slot A с уже валидным подтверждённым
|
||||||
|
* образом не загружался, когда прыжок происходил сразу после
|
||||||
|
* bsp_usb_cdc_init() (USB ещё в процессе enumeration — прерывания частые), но
|
||||||
|
* загружался, когда прыжок происходил позже (после цикла ожидания SD — USB
|
||||||
|
* уже в устоявшемся состоянии). Причина: bootloader (в отличие от Фазы 1/2,
|
||||||
|
* где до прыжка включался только bsp_qspi_init() — без единого постоянно
|
||||||
|
* включённого NVIC IRQ) теперь включает USB CDC и, при вставленной SD,
|
||||||
|
* USDHC — оба взводят свои NVIC IRQ. jump_to_image() не переключал VTOR —
|
||||||
|
* прерывание, сработавшее в узком окне между прыжком и тем, как целевой
|
||||||
|
* образ успеет настроить свою таблицу векторов в Reset_Handler/SystemInit(),
|
||||||
|
* уходило по ещё активной (bootloader'овской) таблице векторов с чужим
|
||||||
|
* стеком/контекстом.
|
||||||
|
*
|
||||||
|
* Портировано по мотивам cleanup()/SBL_DisablePeripherals() в референсном
|
||||||
|
* sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot() — не
|
||||||
|
* скопировано напрямую (SBL_DisablePeripherals — extern, платформенно-
|
||||||
|
* специфичная функция, не вендоренная в нашем дереве); здесь общий,
|
||||||
|
* не завязанный на конкретную периферию эквивалент через NVIC/SysTick.
|
||||||
|
*
|
||||||
|
* НЕ трогает PRIMASK (в отличие от более ранней, откаченной в Фазе 2
|
||||||
|
* версии) — SysTick_Handler целевого образа должен сработать после того как
|
||||||
|
* образ сам вызовет bsp_tick_init(), а PRIMASK обычный Reset_Handler не
|
||||||
|
* восстанавливает (см. bsp_delay()-зависание, найденное в Фазе 2).
|
||||||
|
* Отключение конкретных источников (NVIC ICER/ICPR, SysTick->CTRL) — не
|
||||||
|
* то же самое, что глобальная маскировка: раз выключенный SysTick просто не
|
||||||
|
* тикает, пока образ не включит его сам, и не блокирует его же будущий
|
||||||
|
* запуск.
|
||||||
|
*/
|
||||||
|
static void cleanup_before_jump(void)
|
||||||
|
{
|
||||||
|
for (uint32_t i = 0U; i < (sizeof(NVIC->ICER) / sizeof(NVIC->ICER[0])); i++)
|
||||||
|
{
|
||||||
|
NVIC->ICER[i] = 0xFFFFFFFFU; /* запретить все внешние IRQ */
|
||||||
|
NVIC->ICPR[i] = 0xFFFFFFFFU; /* сбросить pending — не унаследовать флаг */
|
||||||
|
}
|
||||||
|
|
||||||
|
SysTick->CTRL = 0U; /* SysTick — не в NVIC->ICER, отдельный системный таймер */
|
||||||
|
|
||||||
|
__DSB();
|
||||||
|
__ISB();
|
||||||
|
}
|
||||||
|
|
||||||
static void jump_to_image(const struct boot_rsp *p_rsp)
|
static void jump_to_image(const struct boot_rsp *p_rsp)
|
||||||
{
|
{
|
||||||
uintptr_t flash_base;
|
uintptr_t flash_base;
|
||||||
|
|
@ -29,18 +77,18 @@ static void jump_to_image(const struct boot_rsp *p_rsp)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const struct arm_vector_table *p_vt = (const struct arm_vector_table *) (flash_base +
|
const struct arm_vector_table *p_vt =
|
||||||
p_rsp->br_image_off +
|
(const struct arm_vector_table *) (flash_base + p_rsp->br_image_off +
|
||||||
p_rsp->br_hdr->ih_hdr_size);
|
p_rsp->br_hdr->ih_hdr_size);
|
||||||
|
|
||||||
/* Намеренно НЕ __disable_irq() здесь (в отличие от более ранней версии).
|
cleanup_before_jump();
|
||||||
* bsp_delay() в целевом образе — busy-wait на счётчике, инкрементируемом
|
|
||||||
* из SysTick_Handler (см. bsp/tick/src/tick.c); обычный Reset_Handler не
|
/* Образ сам переставит VTOR в своём Reset_Handler/SystemInit() — но до
|
||||||
* трогает PRIMASK, рассчитывая, что прерывания уже разрешены (как после
|
* этого момента (первые же инструкции после прыжка) он уже должен быть
|
||||||
* реального аппаратного ресета). Если замаскировать IRQ здесь и не
|
* валиден, на случай если что-то прервёт выполнение раньше. */
|
||||||
* восстановить в целевом образе — SysTick не сработает ни разу, и любой
|
SCB->VTOR = (uint32_t) p_vt;
|
||||||
* bsp_delay() внутри целевого образа зависнет навсегда. NXP-референс
|
|
||||||
* (do_boot()) тоже не трогает PRIMASK — только __set_CONTROL/__set_MSP. */
|
/* Намеренно НЕ __disable_irq()/PRIMASK здесь — см. cleanup_before_jump(). */
|
||||||
__set_CONTROL(0U);
|
__set_CONTROL(0U);
|
||||||
__set_MSP(p_vt->msp);
|
__set_MSP(p_vt->msp);
|
||||||
__ISB();
|
__ISB();
|
||||||
|
|
|
||||||
|
|
@ -2,74 +2,113 @@
|
||||||
* @file main.c
|
* @file main.c
|
||||||
* @brief bootloader — точка входа.
|
* @brief bootloader — точка входа.
|
||||||
*
|
*
|
||||||
* Фаза 2: минимальный bring-up → сразу попытка boot_go() (bootutil,
|
* Фаза 3: перед выбором образа (bootutil, Direct-XIP) проверяется microSD —
|
||||||
* Direct-XIP) — так плата грузится в tft_app и без подключённого USB
|
* если вставлена, sd_update_check() при необходимости ставит более новый
|
||||||
* (нормальный полевой сценарий). Только если валидного образа нет ни в
|
* (или, при удержании BSP_BUTTON_1, принудительно более старый) подписанный
|
||||||
* одном слоте — поднимаем USB CDC ACM для диагностики (JSON-lines, cli.c,
|
* образ в неактивный слот. boot_select_and_jump() вызывается РОВНО ОДИН РАЗ
|
||||||
* урезанное подмножество протокола firmware_test) и остаёмся в ping/pong
|
* за попытку — если валидного образа нет, main() возвращается в цикл
|
||||||
* цикле. Это прообраз будущего состояния "жду SD" из Фазы 3 — сама
|
* ожидания, где SD периодически пере-сканируется (см. sd_update.h о том,
|
||||||
* SD-логика ещё не добавлена.
|
* почему boot_go() нельзя звать без новой попытки установки между вызовами).
|
||||||
|
*
|
||||||
|
* USB CDC поднимается ДО SD-логики (не дожидаясь подключения хоста —
|
||||||
|
* bsp_usb_cdc_write() не блокируется без хоста, см. bsp/usb_cdc/src/usb_cdc.c)
|
||||||
|
* — чтобы статусы ("installing" и т.п.) были видны, если технолог уже
|
||||||
|
* подключён, в т.ч. на самой первой попытке (чек-лист Фазы 3, сценарий 1).
|
||||||
*
|
*
|
||||||
* Последовательность старта:
|
* Последовательность старта:
|
||||||
* 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_qspi_init() — доступ к Slot A/Б
|
* 4. bsp_button_init() — для проверки удержания BSP_BUTTON_1
|
||||||
* 5. boot_select_and_jump() — при успехе не возвращается
|
* 5. bsp_qspi_init() — доступ к Slot A/Б
|
||||||
* 6. bsp_usb_cdc_init() — только если п.5 не сработал
|
* 6. bsp_usb_cdc_init() — не блокирует, см. выше
|
||||||
* 7. Ожидание CDC ready — LED_HEARTBEAT мигает
|
* 7. sd_update_check() — no-op быстро, если SD не вставлена
|
||||||
* 8. cli_init() — сброс буфера
|
* 8. boot_select_and_jump() — при успехе не возвращается
|
||||||
* 9. Главный цикл — poll + cli_process
|
* 9. Цикл ожидания — CDC ping/pong + LED + периодический
|
||||||
|
* пере-скан SD (шаги 7-8 повторно)
|
||||||
*
|
*
|
||||||
* 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 "boot_select.h"
|
||||||
|
#include "bsp/button.h"
|
||||||
#include "bsp/led.h"
|
#include "bsp/led.h"
|
||||||
#include "bsp/qspi_flash.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"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "sd_update.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
const uint32_t CONNECT_BLINK_MS = 200U;
|
const uint32_t ERROR_BLINK_MS = 250U;
|
||||||
const uint32_t ERROR_BLINK_MS = 250U;
|
const uint32_t SD_RETRY_PERIOD_MS = 1500U;
|
||||||
|
const uint32_t HEARTBEAT_ON_MS = 50U;
|
||||||
|
const uint32_t HEARTBEAT_PERIOD_MS = 500U;
|
||||||
|
|
||||||
board_hw_init();
|
board_hw_init();
|
||||||
|
|
||||||
bsp_led_init();
|
bsp_led_init();
|
||||||
bsp_tick_init();
|
bsp_tick_init();
|
||||||
|
bsp_button_init();
|
||||||
|
|
||||||
if (bsp_qspi_init() == BSP_OK)
|
bool qspi_ok = (bsp_qspi_init() == BSP_OK);
|
||||||
|
bool cdc_ok = (bsp_usb_cdc_init() == BSP_OK);
|
||||||
|
|
||||||
|
if (qspi_ok)
|
||||||
{
|
{
|
||||||
|
sd_update_check(); /* no-op быстро, если SD не вставлена */
|
||||||
boot_select_and_jump(); /* при успехе не возвращается */
|
boot_select_and_jump(); /* при успехе не возвращается */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Нет валидного образа ни в одном слоте — диагностический режим. */
|
/* Нет валидного образа ни в одном слоте (или сбой QSPI) —
|
||||||
if (bsp_usb_cdc_init() != BSP_OK)
|
* диагностический режим. */
|
||||||
|
if (!cdc_ok)
|
||||||
{
|
{
|
||||||
bsp_led_toggle(LED_HEARTBEAT);
|
bsp_led_toggle(LED_HEARTBEAT);
|
||||||
bsp_delay(ERROR_BLINK_MS);
|
bsp_delay(ERROR_BLINK_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ожидать подключения хоста. LED_HEARTBEAT мигает — bootloader жив. */
|
|
||||||
while (!bsp_usb_cdc_is_ready())
|
|
||||||
{
|
|
||||||
bsp_usb_cdc_poll();
|
|
||||||
bsp_led_toggle(LED_HEARTBEAT);
|
|
||||||
bsp_delay(CONNECT_BLINK_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
bsp_led_on(LED_APP);
|
|
||||||
|
|
||||||
cli_init();
|
cli_init();
|
||||||
|
|
||||||
|
/* Готово немедленно — первая попытка сразу извещает "жду SD", не ждёт
|
||||||
|
* SD_RETRY_PERIOD_MS. Дальнейшие попытки уже дросселируются периодом. */
|
||||||
|
uint32_t next_sd_retry_ms = bsp_tick_get_ms();
|
||||||
|
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
bsp_usb_cdc_poll();
|
bsp_usb_cdc_poll();
|
||||||
cli_process();
|
cli_process();
|
||||||
|
|
||||||
|
/* "Загрузчик жив" — короткий импульс (50 мс) + долгая пауза (450 мс),
|
||||||
|
* безусловно, вне зависимости от CDC. Специально отличим от ровного
|
||||||
|
* 50/50 мигания образа в слоте (LED_APP, 500/250 мс) — иначе на глаз
|
||||||
|
* не отличить "жив загрузчик" от "прыгнули в образ". Не blocking —
|
||||||
|
* bsp_delay() здесь не используется, иначе не успевали бы poll'ить
|
||||||
|
* CDC/cli с достаточной частотой. */
|
||||||
|
uint32_t heartbeat_phase_ms = bsp_tick_get_ms() % HEARTBEAT_PERIOD_MS;
|
||||||
|
if (heartbeat_phase_ms < HEARTBEAT_ON_MS)
|
||||||
|
{
|
||||||
|
bsp_led_on(LED_HEARTBEAT);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bsp_led_off(LED_HEARTBEAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qspi_ok && ((int32_t) (bsp_tick_get_ms() - next_sd_retry_ms) >= 0))
|
||||||
|
{
|
||||||
|
next_sd_retry_ms = bsp_tick_get_ms() + SD_RETRY_PERIOD_MS;
|
||||||
|
|
||||||
|
protocol_send_status("waiting_for_sd");
|
||||||
|
|
||||||
|
sd_update_check(); /* no-op быстро, если SD не вставлена */
|
||||||
|
boot_select_and_jump(); /* при успехе не возвращается */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,10 @@ void protocol_send_error(const char *p_code)
|
||||||
(void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code);
|
(void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code);
|
||||||
cli_send(buf);
|
cli_send(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void protocol_send_status(const char *p_state)
|
||||||
|
{
|
||||||
|
char buf[PROTO_BUF_SIZE];
|
||||||
|
(void) snprintf(buf, sizeof(buf), "{\"type\":\"status\",\"state\":\"%s\"}\n", p_state);
|
||||||
|
cli_send(buf);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,12 @@
|
||||||
* version_response — ответ на {"type":"cmd","cmd":"get_version"}
|
* version_response — ответ на {"type":"cmd","cmd":"get_version"}
|
||||||
* error — ошибка протокола или парсинга
|
* error — ошибка протокола или парсинга
|
||||||
*
|
*
|
||||||
* status-события (waiting_for_sd/installing/smoke_pass/...) добавятся в Фазах 3-4.
|
* Типы исходящих событий (Фаза 3):
|
||||||
|
* status — top-level состояние bootloader (waiting_for_sd и т.д.,
|
||||||
|
* см. protocol_send_status())
|
||||||
|
*
|
||||||
|
* Полный словарь состояний status (smoke_pass/smoke_fail/booting/...)
|
||||||
|
* появится в Фазе 4.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef PROTOCOL_H_
|
#ifndef PROTOCOL_H_
|
||||||
|
|
@ -45,4 +50,14 @@ void protocol_send_version_response(void);
|
||||||
*/
|
*/
|
||||||
void protocol_send_error(const char *p_code);
|
void protocol_send_error(const char *p_code);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Отправить событие status — top-level состояние bootloader.
|
||||||
|
*
|
||||||
|
* Формат: {"type":"status","state":"waiting_for_sd"}
|
||||||
|
*
|
||||||
|
* @param[in] p_state Короткий ASCII-идентификатор состояния,
|
||||||
|
* напр. "waiting_for_sd", "installing".
|
||||||
|
*/
|
||||||
|
void protocol_send_status(const char *p_state);
|
||||||
|
|
||||||
#endif /* PROTOCOL_H_ */
|
#endif /* PROTOCOL_H_ */
|
||||||
|
|
|
||||||
253
firmware/bootloader/src/sd_update.c
Normal file
253
firmware/bootloader/src/sd_update.c
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
/**
|
||||||
|
* @file sd_update.c
|
||||||
|
* @brief Реализация — см. sd_update.h.
|
||||||
|
*
|
||||||
|
* Гейт принятия кандидата — двухступенчатый:
|
||||||
|
* 1. Лёгкий пик заголовка (magic + версия) сразу с SD, до касания flash —
|
||||||
|
* достаточно для решения install/skip (update_policy_decide()).
|
||||||
|
* 2. Полная криптографическая проверка (hash+ECDSA) — уже ПОСЛЕ записи в
|
||||||
|
* целевой слот, переиспользованием slot_version_get() (та же функция,
|
||||||
|
* что и для пика активного слота). Если кандидат подписан неверно —
|
||||||
|
* slot_version_get() на целевом слоте вернёт false, а последующий
|
||||||
|
* (единственный) boot_go() в main.c просто не выберет этот слот и
|
||||||
|
* останется на прежнем валидном — отдельный shim "flash_area поверх SD-
|
||||||
|
* файла" не нужен.
|
||||||
|
*
|
||||||
|
* Целевой слот при установке — всегда НЕ активный (см. update_policy.h):
|
||||||
|
* уже выбранный на этот момент слот этой функцией никогда не стирается.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "sd_update.h"
|
||||||
|
|
||||||
|
#include "bootutil/image.h"
|
||||||
|
#include "bsp/button.h"
|
||||||
|
#include "bsp/sd.h"
|
||||||
|
#include "bsp/usb_cdc.h"
|
||||||
|
#include "ff.h"
|
||||||
|
#include "flash_map.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "slot_version.h"
|
||||||
|
#include "update_policy.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* ── Константы ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#define SD_UPDATE_MOUNT_POINT "2:/"
|
||||||
|
#define SD_UPDATE_FILE_PATH "2:/TFT_APP.BIN"
|
||||||
|
|
||||||
|
/** @brief Размер чанка потокового копирования SD → flash. */
|
||||||
|
#define SD_UPDATE_CHUNK_SIZE 4096U
|
||||||
|
|
||||||
|
/* ── Состояние модуля ──────────────────────────────────────────────────────
|
||||||
|
* Статические буферы — не на стеке (см. firmware/test/src/tests/test_usd.c
|
||||||
|
* про ограниченный стек bare-metal прошивок). */
|
||||||
|
|
||||||
|
static FATFS g_s_fs;
|
||||||
|
static FIL g_s_file;
|
||||||
|
static uint8_t g_s_chunk_buf[SD_UPDATE_CHUNK_SIZE];
|
||||||
|
static uint8_t g_s_verify_buf[SD_UPDATE_CHUNK_SIZE];
|
||||||
|
|
||||||
|
/* ── Вспомогательные функции ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static update_policy_slot_state_t peek_slot(uint8_t fa_id)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t state;
|
||||||
|
state.valid = slot_version_get(fa_id, &state.version);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Прочитать заголовок кандидата с начала уже открытого файла.
|
||||||
|
* @retval true magic верный — версия в *p_out_ver, позиция файла = sizeof(header).
|
||||||
|
* @retval false Ошибка чтения либо неверный magic.
|
||||||
|
*/
|
||||||
|
static bool read_candidate_header(struct image_version *p_out_ver)
|
||||||
|
{
|
||||||
|
struct image_header hdr;
|
||||||
|
UINT br = 0U;
|
||||||
|
|
||||||
|
FRESULT fr = f_read(&g_s_file, &hdr, sizeof(hdr), &br);
|
||||||
|
if ((fr != FR_OK) || (br != sizeof(hdr)) || (hdr.ih_magic != IMAGE_MAGIC))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*p_out_ver = hdr.ih_ver;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Стереть целевой слот и потоково скопировать в него файл-кандидат
|
||||||
|
* (с начала файла — заголовок читается заново), сверяя каждый
|
||||||
|
* записанный чанк немедленным обратным чтением.
|
||||||
|
*
|
||||||
|
* @return true при успехе (весь файл скопирован и каждый чанк совпал).
|
||||||
|
*/
|
||||||
|
static bool erase_and_copy_candidate(const struct flash_area *p_fap, uint32_t file_size)
|
||||||
|
{
|
||||||
|
if (file_size > p_fap->fa_size)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flash_area_erase(p_fap, 0U, p_fap->fa_size) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f_lseek(&g_s_file, 0) != FR_OK)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t offset = 0U;
|
||||||
|
while (offset < file_size)
|
||||||
|
{
|
||||||
|
bsp_usb_cdc_poll();
|
||||||
|
|
||||||
|
uint32_t want = file_size - offset;
|
||||||
|
if (want > SD_UPDATE_CHUNK_SIZE)
|
||||||
|
{
|
||||||
|
want = SD_UPDATE_CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
UINT br = 0U;
|
||||||
|
if ((f_read(&g_s_file, g_s_chunk_buf, want, &br) != FR_OK) || (br != want))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flash_area_write(p_fap, offset, g_s_chunk_buf, want) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flash_area_read(p_fap, offset, g_s_verify_buf, want) != 0) ||
|
||||||
|
(memcmp(g_s_chunk_buf, g_s_verify_buf, want) != 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += want;
|
||||||
|
}
|
||||||
|
|
||||||
|
bsp_usb_cdc_poll();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Основной сценарий ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void run_update(void)
|
||||||
|
{
|
||||||
|
struct image_version candidate_ver;
|
||||||
|
struct image_version installed_ver;
|
||||||
|
update_policy_slot_state_t slot_a;
|
||||||
|
update_policy_slot_state_t slot_b;
|
||||||
|
update_policy_result_t decision;
|
||||||
|
const struct flash_area *p_fap = NULL;
|
||||||
|
uint32_t file_size;
|
||||||
|
bool button_held;
|
||||||
|
bool copy_ok;
|
||||||
|
|
||||||
|
if (bsp_sd_init() != BSP_OK)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f_mount(&g_s_fs, SD_UPDATE_MOUNT_POINT, 1) != FR_OK)
|
||||||
|
{
|
||||||
|
(void) bsp_sd_deinit();
|
||||||
|
return; /* нет карты/файловой системы — штатно, не ошибка */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f_open(&g_s_file, SD_UPDATE_FILE_PATH, FA_READ) != FR_OK)
|
||||||
|
{
|
||||||
|
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
|
||||||
|
(void) bsp_sd_deinit();
|
||||||
|
return; /* TFT_APP.BIN отсутствует — тоже штатно */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!read_candidate_header(&candidate_ver))
|
||||||
|
{
|
||||||
|
protocol_send_error("SD_CANDIDATE_INVALID");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_size = (uint32_t) f_size(&g_s_file);
|
||||||
|
slot_a = peek_slot(0U);
|
||||||
|
slot_b = peek_slot(1U);
|
||||||
|
button_held = bsp_button_read(BSP_BUTTON_1);
|
||||||
|
decision = update_policy_decide(&slot_a, &slot_b, &candidate_ver, button_held);
|
||||||
|
|
||||||
|
if (decision.action == UPDATE_POLICY_SKIP)
|
||||||
|
{
|
||||||
|
protocol_send_status("update_skipped");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol_send_status("installing");
|
||||||
|
|
||||||
|
if (flash_area_open((uint8_t) decision.target_slot, &p_fap) != 0)
|
||||||
|
{
|
||||||
|
protocol_send_error("SD_INSTALL_WRITE_FAILED");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_ok = erase_and_copy_candidate(p_fap, file_size);
|
||||||
|
flash_area_close(p_fap);
|
||||||
|
|
||||||
|
if (!copy_ok)
|
||||||
|
{
|
||||||
|
protocol_send_error("SD_INSTALL_WRITE_FAILED");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slot_version_get((uint8_t) decision.target_slot, &installed_ver))
|
||||||
|
{
|
||||||
|
protocol_send_error("SD_INSTALL_REJECTED");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Форс. даунгрейд (см. update_policy.h): без стирания прежнего активного
|
||||||
|
* слота он остался бы валиден и новее только что установленного, и
|
||||||
|
* снова выиграл бы в boot_go() — даунгрейд физически записался бы, но
|
||||||
|
* не загрузился. Стираем ТОЛЬКО теперь, когда новый образ уже подтверждён
|
||||||
|
* валидным — на диске никогда не бывает нуля рабочих слотов. */
|
||||||
|
if (decision.erase_previous_active)
|
||||||
|
{
|
||||||
|
const struct flash_area *p_peer_fap;
|
||||||
|
uint8_t peer_slot = (decision.target_slot == UPDATE_POLICY_SLOT_A) ? UPDATE_POLICY_SLOT_B
|
||||||
|
: UPDATE_POLICY_SLOT_A;
|
||||||
|
|
||||||
|
if (flash_area_open(peer_slot, &p_peer_fap) != 0)
|
||||||
|
{
|
||||||
|
protocol_send_error("SD_DOWNGRADE_ERASE_FAILED");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (flash_area_erase(p_peer_fap, 0U, p_peer_fap->fa_size) != 0)
|
||||||
|
{
|
||||||
|
protocol_send_error("SD_DOWNGRADE_ERASE_FAILED");
|
||||||
|
}
|
||||||
|
flash_area_close(p_peer_fap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
(void) f_close(&g_s_file);
|
||||||
|
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
|
||||||
|
(void) bsp_sd_deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void sd_update_check(void)
|
||||||
|
{
|
||||||
|
if (!bsp_sd_is_inserted())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
run_update();
|
||||||
|
}
|
||||||
28
firmware/bootloader/src/sd_update.h
Normal file
28
firmware/bootloader/src/sd_update.h
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* @file sd_update.h
|
||||||
|
* @brief Оркестрация установки образа tft_app с microSD в неактивный слот.
|
||||||
|
*
|
||||||
|
* См. firmware/bootloader/PLAN.md, Фаза 3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SD_UPDATE_H_
|
||||||
|
#define SD_UPDATE_H_
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Одна попытка: смонтировать SD, найти TFT_APP.BIN, при необходимости
|
||||||
|
* установить его в неактивный слот.
|
||||||
|
*
|
||||||
|
* Пишет только в неактивный слот (см. update_policy.h) — уже выбранный/
|
||||||
|
* загружаемый слот никогда не трогается. Сам не вызывает boot_go()/
|
||||||
|
* boot_select_and_jump() — решение "когда прыгать" остаётся за main.c,
|
||||||
|
* которое обязано вызвать его ровно один раз за сессию питания (см.
|
||||||
|
* slot_version.h о том, почему boot_go() нельзя звать повторно).
|
||||||
|
*
|
||||||
|
* Ничего не делает, если SD не вставлена (bsp_sd_is_inserted() == false) —
|
||||||
|
* безопасно вызывать многократно, в т.ч. из цикла ожидания в main.c.
|
||||||
|
*
|
||||||
|
* @pre bsp_qspi_init() уже вызван.
|
||||||
|
*/
|
||||||
|
void sd_update_check(void);
|
||||||
|
|
||||||
|
#endif /* SD_UPDATE_H_ */
|
||||||
65
firmware/bootloader/src/slot_version.c
Normal file
65
firmware/bootloader/src/slot_version.c
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* @file slot_version.c
|
||||||
|
* @brief Реализация — см. slot_version.h.
|
||||||
|
*
|
||||||
|
* Вызов bootutil_img_validate() зеркалит loader.c::boot_image_check()
|
||||||
|
* (enc_state=NULL — шифрование образов не используется, image_index=0 —
|
||||||
|
* MCUBOOT_IMAGE_NUMBER=1, seed=NULL/0 — FIH_PROFILE_LOW не использует RNG-
|
||||||
|
* задержку, out_hash=NULL — хэш нам не нужен, только факт валидности+версия).
|
||||||
|
*
|
||||||
|
* FIH_CALL безопасен вне boot_go(): CFI-счётчик (FIH_ENABLE_CFI под LOW
|
||||||
|
* профилем) сохраняется/инкрементируется в FIH_CFI_PRECALL_BLOCK и
|
||||||
|
* проверяется/возвращается к сохранённому значению в FIH_CFI_POSTCALL_BLOCK —
|
||||||
|
* пара самобалансирующаяся на каждый вызов, не накапливающееся состояние
|
||||||
|
* между вызовами (см. fault_injection_hardening.h). Несколько вызовов подряд
|
||||||
|
* (Slot A, Slot Б, кандидат) и последующий отдельный boot_go() не влияют друг
|
||||||
|
* на друга через этот счётчик.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "slot_version.h"
|
||||||
|
|
||||||
|
#include "bootutil/fault_injection_hardening.h"
|
||||||
|
#include "flash_map.h"
|
||||||
|
|
||||||
|
#include <stddef.h> /* NULL */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Совпадает с BOOT_TMPBUF_SZ в sdk/middleware/mcuboot_opensource/boot/bootutil/
|
||||||
|
* src/bootutil_priv.h — приватный заголовок bootutil (в src/, не в include/),
|
||||||
|
* поэтому не включаем его напрямую. Тот же размер, что loader.c использует
|
||||||
|
* для этого же вызова.
|
||||||
|
*/
|
||||||
|
#define SLOT_VERSION_TMPBUF_SIZE 256U
|
||||||
|
|
||||||
|
bool slot_version_get(uint8_t fa_id, struct image_version *p_out_ver)
|
||||||
|
{
|
||||||
|
const struct flash_area *p_fap;
|
||||||
|
if (flash_area_open(fa_id, &p_fap) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct image_header hdr;
|
||||||
|
bool read_ok = (flash_area_read(p_fap, 0U, &hdr, sizeof(hdr)) == 0);
|
||||||
|
|
||||||
|
if (!read_ok || (hdr.ih_magic != IMAGE_MAGIC))
|
||||||
|
{
|
||||||
|
flash_area_close(p_fap);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t s_tmpbuf[SLOT_VERSION_TMPBUF_SIZE];
|
||||||
|
fih_ret fih_rc;
|
||||||
|
FIH_CALL(bootutil_img_validate, fih_rc, NULL, 0, &hdr, p_fap, s_tmpbuf, sizeof(s_tmpbuf), NULL,
|
||||||
|
0, NULL);
|
||||||
|
|
||||||
|
flash_area_close(p_fap);
|
||||||
|
|
||||||
|
if (!FIH_EQ(fih_rc, FIH_SUCCESS))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*p_out_ver = hdr.ih_ver;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
37
firmware/bootloader/src/slot_version.h
Normal file
37
firmware/bootloader/src/slot_version.h
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* @file slot_version.h
|
||||||
|
* @brief Read-only "пик" версии образа в слоте bootutil — без побочных
|
||||||
|
* эффектов на flash.
|
||||||
|
*
|
||||||
|
* В отличие от boot_go(): для образа, ни разу не подтверждённого приложением,
|
||||||
|
* bootutil (Direct-XIP-Revert) пишет copy_done в трейлер слота уже на этапе
|
||||||
|
* выбора — вызов boot_go() второй раз за одну сессию питания принял бы этот
|
||||||
|
* флаг за "образ уже грузился и не подтвердился" и стёр бы его (см.
|
||||||
|
* firmware/bootloader/PLAN.md, Фаза 3). slot_version_get() читает и валидирует
|
||||||
|
* слот через flash_area_read()/bootutil_img_validate() напрямую — оба
|
||||||
|
* read-only (image_validate.c не пишет в flash), в обход boot_go().
|
||||||
|
*
|
||||||
|
* Валидация — полная (hash + ECDSA-подпись через bootutil_img_validate()),
|
||||||
|
* тот же путь, что loader.c::boot_image_check() использует для каждого слота
|
||||||
|
* при штатной загрузке, а не самодельная проверка одного заголовка.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SLOT_VERSION_H_
|
||||||
|
#define SLOT_VERSION_H_
|
||||||
|
|
||||||
|
#include "bootutil/image.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Прочитать и провалидировать образ в слоте fa_id.
|
||||||
|
*
|
||||||
|
* @param[in] fa_id ID области (см. sysflash.h) — 0 = Slot A, 1 = Slot Б.
|
||||||
|
* @param[out] p_out_ver Версия образа при успехе. Не тронут при false.
|
||||||
|
* @retval true Валидный образ (magic, hash и подпись прошли) — версия в *p_out_ver.
|
||||||
|
* @retval false Слот пуст/повреждён/подпись не прошла, либо ошибка чтения/открытия.
|
||||||
|
*/
|
||||||
|
bool slot_version_get(uint8_t fa_id, struct image_version *p_out_ver);
|
||||||
|
|
||||||
|
#endif /* SLOT_VERSION_H_ */
|
||||||
100
firmware/bootloader/src/update_policy.c
Normal file
100
firmware/bootloader/src/update_policy.c
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* @file update_policy.c
|
||||||
|
* @brief Реализация — см. update_policy.h.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "update_policy.h"
|
||||||
|
|
||||||
|
/* ── Сравнение версий ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
int image_version_compare(const struct image_version *p_ver1, const struct image_version *p_ver2)
|
||||||
|
{
|
||||||
|
if (p_ver1->iv_major != p_ver2->iv_major)
|
||||||
|
{
|
||||||
|
return (p_ver1->iv_major > p_ver2->iv_major) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p_ver1->iv_minor != p_ver2->iv_minor)
|
||||||
|
{
|
||||||
|
return (p_ver1->iv_minor > p_ver2->iv_minor) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p_ver1->iv_revision != p_ver2->iv_revision)
|
||||||
|
{
|
||||||
|
return (p_ver1->iv_revision > p_ver2->iv_revision) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Определение активного слота ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
bool have_active;
|
||||||
|
update_policy_slot_t active_slot;
|
||||||
|
struct image_version active_ver;
|
||||||
|
} active_slot_info_t;
|
||||||
|
|
||||||
|
static active_slot_info_t find_active_slot(const update_policy_slot_state_t *p_slot_a,
|
||||||
|
const update_policy_slot_state_t *p_slot_b)
|
||||||
|
{
|
||||||
|
active_slot_info_t info = { .have_active = false };
|
||||||
|
|
||||||
|
if (p_slot_a->valid)
|
||||||
|
{
|
||||||
|
info.have_active = true;
|
||||||
|
info.active_slot = UPDATE_POLICY_SLOT_A;
|
||||||
|
info.active_ver = p_slot_a->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p_slot_b->valid &&
|
||||||
|
(!info.have_active || image_version_compare(&p_slot_b->version, &info.active_ver) > 0))
|
||||||
|
{
|
||||||
|
info.have_active = true;
|
||||||
|
info.active_slot = UPDATE_POLICY_SLOT_B;
|
||||||
|
info.active_ver = p_slot_b->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
static update_policy_slot_t other_slot(update_policy_slot_t slot)
|
||||||
|
{
|
||||||
|
return (slot == UPDATE_POLICY_SLOT_A) ? UPDATE_POLICY_SLOT_B : UPDATE_POLICY_SLOT_A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Публичный API ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
update_policy_result_t update_policy_decide(const update_policy_slot_state_t *p_slot_a,
|
||||||
|
const update_policy_slot_state_t *p_slot_b,
|
||||||
|
const struct image_version *p_candidate_ver,
|
||||||
|
bool button_held)
|
||||||
|
{
|
||||||
|
active_slot_info_t active = find_active_slot(p_slot_a, p_slot_b);
|
||||||
|
|
||||||
|
if (!active.have_active)
|
||||||
|
{
|
||||||
|
return (update_policy_result_t) { .action = UPDATE_POLICY_INSTALL,
|
||||||
|
.target_slot = UPDATE_POLICY_SLOT_A,
|
||||||
|
.erase_previous_active = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
int cmp = image_version_compare(p_candidate_ver, &active.active_ver);
|
||||||
|
|
||||||
|
bool candidate_newer = (cmp > 0);
|
||||||
|
bool forced_downgrade = (cmp < 0) && button_held;
|
||||||
|
|
||||||
|
if (!candidate_newer && !forced_downgrade)
|
||||||
|
{
|
||||||
|
return (update_policy_result_t) { .action = UPDATE_POLICY_SKIP };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Форс. даунгрейд без стирания прежнего активного слота не имел бы
|
||||||
|
* эффекта — он остаётся валиден и новее, и снова выиграет в boot_go().
|
||||||
|
* "Новее" не требует стирания — прежний активный сам проиграет
|
||||||
|
* сравнение версий естественным путём. */
|
||||||
|
return (update_policy_result_t) { .action = UPDATE_POLICY_INSTALL,
|
||||||
|
.target_slot = other_slot(active.active_slot),
|
||||||
|
.erase_previous_active = forced_downgrade };
|
||||||
|
}
|
||||||
95
firmware/bootloader/src/update_policy.h
Normal file
95
firmware/bootloader/src/update_policy.h
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* @file update_policy.h
|
||||||
|
* @brief Чистая логика решения "устанавливать ли SD-кандидат" — без
|
||||||
|
* аппаратных зависимостей (flash/FatFS), полностью host-тестируема.
|
||||||
|
*
|
||||||
|
* См. firmware/bootloader/PLAN.md, Фаза 3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef UPDATE_POLICY_H_
|
||||||
|
#define UPDATE_POLICY_H_
|
||||||
|
|
||||||
|
#include "bootutil/image.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/** @brief Логический слот (индекс сисфлеша, не физический адрес). */
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
UPDATE_POLICY_SLOT_A = 0,
|
||||||
|
UPDATE_POLICY_SLOT_B = 1,
|
||||||
|
} update_policy_slot_t;
|
||||||
|
|
||||||
|
/** @brief Состояние одного слота глазами вызывающего (см. slot_version.h). */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
bool valid; /**< Есть валидный (прошедший bootutil_img_validate) образ. */
|
||||||
|
struct image_version version; /**< Значимо только если valid == true. */
|
||||||
|
} update_policy_slot_state_t;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
UPDATE_POLICY_SKIP = 0,
|
||||||
|
UPDATE_POLICY_INSTALL,
|
||||||
|
} update_policy_action_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
update_policy_action_t action;
|
||||||
|
update_policy_slot_t target_slot; /**< Значим только если action == UPDATE_POLICY_INSTALL. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значим только при action == UPDATE_POLICY_INSTALL. Если true —
|
||||||
|
* вызывающий код обязан, ПОСЛЕ успешной установки и пост-записи
|
||||||
|
* валидации target_slot, стереть слот, который был активным ДО
|
||||||
|
* установки (см. rationale ниже про форс. даунгрейд).
|
||||||
|
*/
|
||||||
|
bool erase_previous_active;
|
||||||
|
} update_policy_result_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Решить, устанавливать ли SD-кандидат, и в какой слот.
|
||||||
|
*
|
||||||
|
* Правила:
|
||||||
|
* - "Активный" слот — валидный слот с более высокой версией; если валиден
|
||||||
|
* только один — он активный; если ни одного — активного слота нет.
|
||||||
|
* - Целевой слот установки — всегда НЕ активный (активный не перезаписываем
|
||||||
|
* никогда, независимо от исхода сравнения версий). Если активного слота
|
||||||
|
* нет — по умолчанию Slot A.
|
||||||
|
* - Кандидат новее активного (или активного слота нет вообще) → INSTALL,
|
||||||
|
* erase_previous_active = false. Прежний активный слот сам проиграет
|
||||||
|
* сравнение версий в boot_go() — стирать его не нужно.
|
||||||
|
* - Кандидат старше или равен активному, кнопка не удержана → SKIP.
|
||||||
|
* - Кандидат старше активного, кнопка удержана → INSTALL,
|
||||||
|
* erase_previous_active = true. Без этого форс. даунгрейд не имел бы
|
||||||
|
* эффекта: прежний (более новый) активный слот остался бы валиден и
|
||||||
|
* снова выиграл бы сравнение версий в boot_go(), несмотря на успешную
|
||||||
|
* запись более старого образа в другой слот. Стирание — обязанность
|
||||||
|
* вызывающего кода и только ПОСЛЕ подтверждения, что только что
|
||||||
|
* установленный образ валиден (иначе на короткое время не осталось бы
|
||||||
|
* ни одного рабочего слота).
|
||||||
|
* - Кандидат равен активному, кнопка удержана → SKIP (не форсируем
|
||||||
|
* переустановку той же версии).
|
||||||
|
*
|
||||||
|
* @param[in] p_slot_a Состояние Slot A.
|
||||||
|
* @param[in] p_slot_b Состояние Slot Б.
|
||||||
|
* @param[in] p_candidate_ver Версия образа-кандидата на SD.
|
||||||
|
* @param[in] button_held Кнопка даунгрейда (BSP_BUTTON_1) удержана на старте.
|
||||||
|
*/
|
||||||
|
update_policy_result_t update_policy_decide(const update_policy_slot_state_t *p_slot_a,
|
||||||
|
const update_policy_slot_state_t *p_slot_b,
|
||||||
|
const struct image_version *p_candidate_ver,
|
||||||
|
bool button_held);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сравнить версии образов: major.minor.revision, без build_num — то
|
||||||
|
* же соглашение, что boot_version_cmp() в sdk/.../bootutil/loader.c
|
||||||
|
* (static там, не экспортируется — здесь свой аналог).
|
||||||
|
*
|
||||||
|
* @retval <0 p_ver1 < p_ver2
|
||||||
|
* @retval 0 p_ver1 == p_ver2
|
||||||
|
* @retval >0 p_ver1 > p_ver2
|
||||||
|
*/
|
||||||
|
int image_version_compare(const struct image_version *p_ver1, const struct image_version *p_ver2);
|
||||||
|
|
||||||
|
#endif /* UPDATE_POLICY_H_ */
|
||||||
197
firmware/bootloader/test_stub/HARDWARE_VERIFICATION_PHASE3.md
Normal file
197
firmware/bootloader/test_stub/HARDWARE_VERIFICATION_PHASE3.md
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
# Фаза 3 (SD-путь установки) — аппаратная верификация
|
||||||
|
|
||||||
|
Памятка с готовыми командами: сборка, прошивка, подготовка SD-карты, чек-лист. Продолжение
|
||||||
|
[HARDWARE_VERIFICATION_PHASE2.md](HARDWARE_VERIFICATION_PHASE2.md) — предполагает, что прошивка
|
||||||
|
bootloader и заглушки слотов (`test_stub`) уже знакомы по Фазе 2.
|
||||||
|
|
||||||
|
Все команды — из корня репозитория (`tft_manufacture_test/`). Сборка и host-тесты — в devcontainer;
|
||||||
|
прошивка/отладка/проверки на железе — на стороне пользователя.
|
||||||
|
|
||||||
|
**Статус: ✅ все 5 сценариев пройдены на реальной плате (2026-07-10).** Историю бага
|
||||||
|
card-detect и его разрешение (чтение USDHC `PRES_STATE.CINST`) см. в
|
||||||
|
[../DEBUG_LOG_PHASE3_SD.md](../DEBUG_LOG_PHASE3_SD.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Предпосылки
|
||||||
|
|
||||||
|
- `firmware/bootloader` собран с ARM-стороной Фазы 3 (`update_policy` + `slot_version` + `sd_update` +
|
||||||
|
`bootloader_fatfs`). `main.c` перед выбором слота сканирует microSD: если вставлена и на ней лежит
|
||||||
|
подписанный `TFT_APP.BIN` — по политике версий (или по удержанию кнопки) ставит его в НЕактивный
|
||||||
|
слот, затем `boot_select_and_jump()`.
|
||||||
|
- Заглушка вместо ещё не существующего `tft_app` — та же `firmware/bootloader/test_stub/`, что и в
|
||||||
|
Фазе 2: два образа, различающиеся частотой мигания `LED_APP` (по частоте видно, какой слот реально
|
||||||
|
выбран). Оба подписаны тестовым sample-ключом MCUboot.
|
||||||
|
- **Card detect** — через регистр USDHC `PRES_STATE.CINST`, не GPIO (см. DEBUG_LOG). На пустом слоте
|
||||||
|
гейт `bsp_sd_is_inserted()` обязан быть верным, иначе блокирующий `SD_PollingCardInsert()` без
|
||||||
|
тайм-аута подвесит загрузчик; PRSSTAT это обеспечивает.
|
||||||
|
- **Обратная связь** — USB CDC ACM, тот же JSON-lines протокол, что в Фазе 1/2, плюс события
|
||||||
|
`status` (см. §6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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/` — те же файлы, что в Фазе 2:
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
Частоты мигания `LED_APP` (по ним отличаем слот): **stub_a (v1) ≈ 1 раз/сек**, **stub_b (v2) ≈ 2
|
||||||
|
раза/сек**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Прошивка bootloader
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just host::flash-swd-bootloader-debug
|
||||||
|
# обязателен power cycle платы после прошивки — SWD-запись не ресетит автоматически
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Прошивка образов в слоты напрямую (pyOCD)
|
||||||
|
|
||||||
|
Нужно только для сценариев, где слот должен быть заполнен ДО теста (2, 3, 5) — установка с SD слоты
|
||||||
|
заполняет сама. Способ идентичен Фазе 2 (сырой подписанный `.bin` по адресу слота, не через
|
||||||
|
`flash_swd.py`).
|
||||||
|
|
||||||
|
**Важно:** `uv run --directory tools/hil` меняет рабочую директорию у самого `pyocd` — путь к `.bin`
|
||||||
|
должен быть абсолютным (`"$(pwd)/..."`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Slot A (0x60040000)
|
||||||
|
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)
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Стирание слота
|
||||||
|
|
||||||
|
Адрес — позиционный аргумент, формат диапазона `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. Подготовка SD-карты
|
||||||
|
|
||||||
|
Карта — FAT32/FAT16. Bootloader ищет в корне файл **`TFT_APP.BIN`** (путь `2:/TFT_APP.BIN`, диск `2:`
|
||||||
|
— внутренняя нумерация FatFS, физически это корень карты). Файл — подписанный образ-заглушка,
|
||||||
|
просто переименованный:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Кандидат-"новее" (v2) — для авто-установки/сравнения версий
|
||||||
|
cp build/Debug/signed/stub_b_v2_confirmed.bin /Volumes/<SD>/TFT_APP.BIN
|
||||||
|
|
||||||
|
# Кандидат-"старше" (v1) — для теста даунгрейда (сценарий 3)
|
||||||
|
cp build/Debug/signed/stub_a_v1_confirmed.bin /Volumes/<SD>/TFT_APP.BIN
|
||||||
|
|
||||||
|
# Кандидат с битой ПОДПИСЬЮ (magic цел, TLV/хэш испорчен) — сценарий 4b (SD_INSTALL_REJECTED)
|
||||||
|
cp build/Debug/signed/stub_b_v2_confirmed.bin /Volumes/<SD>/TFT_APP.BIN
|
||||||
|
# затем поменять один байт в СЕРЕДИНЕ файла (payload), напр. в hex-редакторе — magic в начале не трогать
|
||||||
|
# Кандидат с битым ЗАГОЛОВКОМ (magic) — сценарий 4a (SD_CANDIDATE_INVALID)
|
||||||
|
# поменять один из первых 4 байт файла (ih_magic)
|
||||||
|
```
|
||||||
|
|
||||||
|
Берём `--confirm`-варианты: иначе установленный с SD образ на следующем power cycle
|
||||||
|
откатится (Direct-XIP Revert, см. Фаза 2 / сценарий 5 там).
|
||||||
|
|
||||||
|
> ⚠️ **Про извлечение/повторный скан карты без ресета.** Детект на пустом слоте читает PRSSTAT при
|
||||||
|
> пине на `USDHC1_CD_B`; после первого же `bsp_sd_init()` пин перемапливается на `GPIO2_IO28` и
|
||||||
|
> обратно не возвращается до перезагрузки. Практическое следствие для теста: **после каждого
|
||||||
|
> сценария делай power cycle** (и так обязателен, см. ниже), не рассчитывай на «вынул/вставил карту
|
||||||
|
> на живом загрузчике». Подробности и рекомендованная консолидация — в DEBUG_LOG (раунд 3,
|
||||||
|
> «латентная хрупкость re-scan»).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. CDC-статусы и проверка "bootloader жив"
|
||||||
|
|
||||||
|
Подключиться к USB CDC ACM платы любым терминалом (см. [../README.md](../README.md)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
screen /dev/cu.usbmodemXXXX # macOS, порт свой у каждого подключения
|
||||||
|
```
|
||||||
|
|
||||||
|
- Живость: отправить `{"type":"cmd","cmd":"ping"}` → ответ `{"type":"pong"}`.
|
||||||
|
- События SD-пути (`{"type":"status","state":"..."}`):
|
||||||
|
| state | когда |
|
||||||
|
| ----------------- | ------------------------------------------------------------------ |
|
||||||
|
| `waiting_for_sd` | периодически в цикле ожидания (нет валидного слота, ждём SD) |
|
||||||
|
| `installing` | принято решение установить кандидата, начинается запись в слот |
|
||||||
|
| `update_skipped` | кандидат отклонён по версии (старше/равен, кнопка не удержана) |
|
||||||
|
- Ошибки (`{"ok":false,"error":"..."}`):
|
||||||
|
| error | когда |
|
||||||
|
| --------------------------- | -------------------------------------------------------------- |
|
||||||
|
| `SD_CANDIDATE_INVALID` | битый magic заголовка кандидата (до записи во flash) |
|
||||||
|
| `SD_INSTALL_WRITE_FAILED` | сбой записи/verify чанка во flash |
|
||||||
|
| `SD_INSTALL_REJECTED` | образ записан, но пост-проверка (hash+ECDSA) не прошла |
|
||||||
|
| `SD_DOWNGRADE_ERASE_FAILED` | не удалось стереть прежний активный слот при форс. даунгрейде |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Тайминг удержания кнопки даунгрейда (BSP_BUTTON_1)
|
||||||
|
|
||||||
|
**Держать кнопку с момента подачи питания и не отпускать, пока по CDC не придёт
|
||||||
|
`status: installing`** (или пока `LED_APP` не замигает с частотой более старого образа). Ориентир —
|
||||||
|
**~15 секунд, с запасом**; отпускание на 2 c даунгрейд НЕ вызывает.
|
||||||
|
|
||||||
|
Почему так (разбор по коду — [../DEBUG_LOG_PHASE3_SD.md](../DEBUG_LOG_PHASE3_SD.md), раунд 3):
|
||||||
|
`bsp_button_read()` — мгновенное сырое чтение, без debounce и без latch. Кнопка сэмплится ровно
|
||||||
|
один раз, в `sd_update.c` (`run_update()`), и этот момент наступает **после** `bsp_sd_init()`,
|
||||||
|
`f_mount()` (полная инициализация карты), `f_open()` и **двух полных крипто-валидаций слотов**
|
||||||
|
(SHA-256 + ECDSA по каждому). Это несколько секунд слепого окна без LED-фидбэка (heartbeat идёт
|
||||||
|
только в главном цикле, до которого управление ещё не дошло). Отпустишь раньше сэмпла — прочитается
|
||||||
|
"не нажата" → штатная загрузка нового слота. «Срабатывает сразу после отпускания» — это не триггер
|
||||||
|
по отпусканию (кода на отпускание нет), а совпадение: решение защёлкивается на сэмпле, пока кнопка
|
||||||
|
ещё нажата.
|
||||||
|
|
||||||
|
> Известное неудобство (в очереди на доработку, не реализовано): сэмплить кнопку РАНО, при старте,
|
||||||
|
> до медленной SD-инициализации — тогда «удержание при включении» стало бы предсказуемым.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Чек-лист сценариев
|
||||||
|
|
||||||
|
Между КАЖДЫМ сценарием — обязательный power cycle платы (SWD-запись/установка не ресетят
|
||||||
|
автоматически). Зеркалит план верификации Фазы 3 (см. [../PLAN.md](../PLAN.md)).
|
||||||
|
|
||||||
|
| № | Сценарий | Подготовка | Ожидаемый результат |
|
||||||
|
| --- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 1 | Чистая плата + валидный образ на SD | Erase Slot A и Slot Б (§4); SD с `TFT_APP.BIN` = `stub_b_v2_confirmed.bin` (§5); вставить карту | CDC: `installing`; авто-установка в Slot A → прыжок; `LED_APP` мигает ~2/сек (v2) |
|
||||||
|
| 2 | Кандидат старше установленного, без кнопки | Slot Б = `stub_b_v2_confirmed.bin` (§3); SD `TFT_APP.BIN` = `stub_a_v1_confirmed.bin` (v1); кнопку НЕ трогать | CDC: `update_skipped`; прыжок в существующий Slot Б; `LED_APP` ~2/сек (v2 не тронут) |
|
||||||
|
| 3 | Форс. даунгрейд по кнопке | Как сценарий 2, но **удерживать `BSP_BUTTON_1`** при включении (см. §7 — до `installing`, ~15 c) | CDC: `installing`; v1 ставится в Slot A, прежний активный Slot Б стирается; прыжок в v1; `LED_APP` ~1/сек |
|
||||||
|
| 4a | Битый заголовок кандидата | SD `TFT_APP.BIN` с испорченным magic (§5); слоты — как удобно (напр. Slot A = v1) | CDC: `error: SD_CANDIDATE_INVALID`; плата остаётся на прежнем валидном слоте (или ждёт SD, если слотов нет) |
|
||||||
|
| 4b | Битая подпись/TLV кандидата | SD `TFT_APP.BIN` с целым magic, но испорченным payload (§5) | CDC: `installing` затем `error: SD_INSTALL_REJECTED`; прежний валидный слот не тронут (записанный кандидат не выбирается) |
|
||||||
|
| 5 | Бандл залит в слот заранее (SD не участвует) | `stub_a_v1_confirmed.bin` → Slot A (§3); карту НЕ вставлять | SD-логика не входит; прыжок в Slot A; `LED_APP` ~1/сек. `ping`→`pong` при желании |
|
||||||
|
|
||||||
|
**Инструмент подтверждения «прыжок реально произошёл»** — тот же, что в Фазе 2: частота мигания
|
||||||
|
`LED_APP` (500 мс vs 250 мс) однозначно указывает на выбранный слот, верифицируемо глазами.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -65,6 +65,7 @@ __attribute__((used))
|
||||||
__attribute__((noinline))
|
__attribute__((noinline))
|
||||||
void fih_panic_loop(void)
|
void fih_panic_loop(void)
|
||||||
{
|
{
|
||||||
|
#if defined(__arm__)
|
||||||
__asm volatile ("b fih_panic_loop");
|
__asm volatile ("b fih_panic_loop");
|
||||||
__asm volatile ("b fih_panic_loop");
|
__asm volatile ("b fih_panic_loop");
|
||||||
__asm volatile ("b fih_panic_loop");
|
__asm volatile ("b fih_panic_loop");
|
||||||
|
|
@ -74,5 +75,17 @@ void fih_panic_loop(void)
|
||||||
__asm volatile ("b fih_panic_loop");
|
__asm volatile ("b fih_panic_loop");
|
||||||
__asm volatile ("b fih_panic_loop");
|
__asm volatile ("b fih_panic_loop");
|
||||||
__asm volatile ("b fih_panic_loop");
|
__asm volatile ("b fih_panic_loop");
|
||||||
|
#else
|
||||||
|
/* Host-порт (см. tests/host/mcuboot_port/): "b fih_panic_loop" — валидная
|
||||||
|
* мнемоника только для ARM/Thumb. На x86_64 ассемблер падает ("invalid
|
||||||
|
* instruction mnemonic 'b'") — отсюда падение test-host-release в CI
|
||||||
|
* (x86_64-раннер), которого нет в devcontainer на arm64 (там та же
|
||||||
|
* мнемоника случайно ассемблируется, но не несёт смысла — это не
|
||||||
|
* реальный fault-injection путь, а хостовая сборка). Реальный ARM-таргет
|
||||||
|
* (__arm__ определён) использует оригинальный код выше без изменений. */
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif /* FIH_ENABLE_GLOBAL_FAIL */
|
#endif /* FIH_ENABLE_GLOBAL_FAIL */
|
||||||
|
|
|
||||||
|
|
@ -244,3 +244,57 @@ target_compile_definitions(
|
||||||
PRIVATE FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/fixtures")
|
PRIVATE FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/fixtures")
|
||||||
|
|
||||||
target_compile_options(test_mcuboot_boot_select PRIVATE -w)
|
target_compile_options(test_mcuboot_boot_select PRIVATE -w)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# update_policy — чистая логика решения install/skip + целевой слот (Фаза 3,
|
||||||
|
# SD-путь установки). Нужен только заголовок bootutil/image.h (struct
|
||||||
|
# image_version) — без линковки самого bootutil, но fault_injection_hardening.h
|
||||||
|
# (включается из image.h) требует mcuboot_config/mcuboot_config.h в include-
|
||||||
|
# путях, поэтому переиспользуем MCUBOOT_BOOTUTIL_INCLUDES целиком (уже задан
|
||||||
|
# выше через bootutil_sources.cmake).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_update_policy
|
||||||
|
SOURCES
|
||||||
|
update_policy/test_update_policy.c
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/update_policy.c
|
||||||
|
INCLUDES
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/bootloader/src
|
||||||
|
${MCUBOOT_BOOTUTIL_INCLUDES})
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# slot_version — read-only пик версии слота через bootutil_img_validate()
|
||||||
|
# (Фаза 3). Реальный bootutil + TinyCrypt поверх fake_flash_map_backend.c —
|
||||||
|
# переиспользует те же фикстуры, что и test_mcuboot_boot_select (Фаза 2).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_slot_version
|
||||||
|
SOURCES
|
||||||
|
slot_version/test_slot_version.c
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/slot_version.c
|
||||||
|
mcuboot_port/fake_flash_map_backend.c
|
||||||
|
mcuboot_port/host_link_shims.c
|
||||||
|
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/keys.c
|
||||||
|
${MCUBOOT_BOOTUTIL_SOURCES}
|
||||||
|
INCLUDES
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/bootloader/src
|
||||||
|
${MCUBOOT_BOOTUTIL_INCLUDES}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port)
|
||||||
|
|
||||||
|
target_compile_definitions(
|
||||||
|
test_slot_version PRIVATE FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/fixtures")
|
||||||
|
|
||||||
|
target_compile_options(test_slot_version PRIVATE -w)
|
||||||
|
|
||||||
|
# slot_version.c зовёт FIH_CALL(bootutil_img_validate, ...) напрямую — тот же
|
||||||
|
# баг clang 22.1.8 (Homebrew) с генерацией CFI-директив под ASan/UBSan, что
|
||||||
|
# уже обойдён для вендоренных файлов bootutil в bootutil_sources.cmake
|
||||||
|
# ("invalid CFI advance_loc expression"); здесь тот же обход для нашего
|
||||||
|
# файла, единственного, где эта макро-развёртка встречается вне bootutil.
|
||||||
|
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||||
|
set_source_files_properties(
|
||||||
|
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/slot_version.c
|
||||||
|
PROPERTIES COMPILE_OPTIONS "-fno-sanitize=address,undefined")
|
||||||
|
endif()
|
||||||
|
|
|
||||||
62
tests/host/slot_version/README.md
Normal file
62
tests/host/slot_version/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# test_slot_version
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`firmware/bootloader/src/slot_version.c` (`slot_version.h`) — read-only
|
||||||
|
«пик» версии образа в слоте bootutil (Фаза 3), без побочных эффектов на
|
||||||
|
flash: в отличие от `boot_go()`, не проверяет и не изменяет статус
|
||||||
|
confirm/copy_done.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
B — интеграционный host-тест: реальный `bootutil`
|
||||||
|
(`bootutil_img_validate()`, полная проверка hash + ECDSA-подписи)
|
||||||
|
линкуется поверх фейкового flash-бэкенда вместо `bsp_qspi_flash`.
|
||||||
|
Переиспользует фейковый backend и фикстуры из `tests/host/mcuboot_port/` —
|
||||||
|
те же, что и `test_mcuboot_boot_select`.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
- `fake_flash_map_backend.c/.h` (переиспользуется из `mcuboot_port/`) —
|
||||||
|
тестовая реализация контракта `flash_map.h` поверх памяти хоста вместо
|
||||||
|
`bsp_qspi_flash`. Не fff-мок — полноценная тестовая реализация backend'а.
|
||||||
|
- `host_link_shims.c` (переиспользуется из `mcuboot_port/`) — заглушки
|
||||||
|
символов, недостающих только при линковке `bootutil` на хосте; не
|
||||||
|
участвуют в тестируемой логике.
|
||||||
|
- `fixtures/*.bin` (общие с `test_mcuboot_boot_select`) — реальные,
|
||||||
|
подписанные `imgtool` тестовым ключом MCUboot образы (`valid_v1`,
|
||||||
|
`corrupt_v1`, `valid_v2_unconfirmed`).
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **Валидный слот** — `slot_version_get()` возвращает `true` и версию,
|
||||||
|
совпадающую с версией в заголовке фикстуры (major/minor/revision).
|
||||||
|
- **Повреждённый слот** (битый хэш/подпись) — возвращает `false`.
|
||||||
|
- **Пустой слот** (стёрт в `0xFF`, magic не совпадает с `IMAGE_MAGIC`) —
|
||||||
|
возвращает `false`.
|
||||||
|
- **Неподтверждённый, но валидный образ** (`valid_v2_unconfirmed.bin`) —
|
||||||
|
версия всё равно читается, и повторный вызов `slot_version_get()` на том
|
||||||
|
же слоте по-прежнему успешен — слот не «стирается», как это сделал бы
|
||||||
|
повторный `boot_go()`.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- `slot_version_get()` не имеет побочных эффектов на flash — не читает и
|
||||||
|
не изменяет статус confirm/copy_done, поэтому безопасен для
|
||||||
|
многократного вызова и вызова до первого `boot_go()`.
|
||||||
|
- Валидация полная — тот же путь `bootutil_img_validate()`, что
|
||||||
|
использует `loader.c` для каждого слота при штатной загрузке, а не
|
||||||
|
упрощённая проверка одного заголовка.
|
||||||
|
- Идемпотентность: повторные вызовы для одного и того же
|
||||||
|
неподтверждённого образа дают одинаковый результат — в отличие от
|
||||||
|
`boot_go()`, который стирает неподтверждённый образ при повторном
|
||||||
|
вызове (см. `test_mcuboot_boot_select::test_boot_go_reverts_unconfirmed_image`).
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_slot_version -V
|
||||||
|
```
|
||||||
|
|
||||||
|
Путь к фикстурам (`FIXTURES_DIR`) общий с `test_mcuboot_boot_select` и
|
||||||
|
прокидывается автоматически через `tests/host/CMakeLists.txt`.
|
||||||
138
tests/host/slot_version/test_slot_version.c
Normal file
138
tests/host/slot_version/test_slot_version.c
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
/**
|
||||||
|
* @file test_slot_version.c
|
||||||
|
* @brief Host-тесты slot_version_get() — read-only пик версии слота.
|
||||||
|
*
|
||||||
|
* Переиспользует фейковый flash-бэкенд и фикстуры
|
||||||
|
* tests/host/mcuboot_port/ (Фаза 2) — реальный bootutil (hash+ECDSA-подпись)
|
||||||
|
* поверх in-memory буфера вместо bsp_qspi_flash, те же подписанные imgtool
|
||||||
|
* образы.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#include "fake_flash_map_backend.h"
|
||||||
|
#include "slot_version.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifndef FIXTURES_DIR
|
||||||
|
#error "FIXTURES_DIR must be defined by CMake (see tests/host/CMakeLists.txt)"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static uint8_t g_s_fixture_buf[FAKE_FLASH_SLOT_SIZE];
|
||||||
|
|
||||||
|
static size_t load_fixture(const char *p_name)
|
||||||
|
{
|
||||||
|
char path[256];
|
||||||
|
(void) snprintf(path, sizeof(path), "%s/%s", FIXTURES_DIR, p_name);
|
||||||
|
|
||||||
|
FILE *p_file = fopen(path, "rb");
|
||||||
|
TEST_ASSERT_NOT_NULL_MESSAGE(p_file, path);
|
||||||
|
|
||||||
|
size_t n = fread(g_s_fixture_buf, 1U, sizeof(g_s_fixture_buf), p_file);
|
||||||
|
(void) fclose(p_file);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(FAKE_FLASH_SLOT_SIZE, n);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Версия зашита в первых байтах фикстуры (image_header) — читаем её
|
||||||
|
* напрямую из буфера вместо того чтобы угадывать/дублировать число в тесте. */
|
||||||
|
static struct image_version fixture_header_version(void)
|
||||||
|
{
|
||||||
|
struct image_header hdr;
|
||||||
|
memcpy(&hdr, g_s_fixture_buf, sizeof(hdr));
|
||||||
|
return hdr.ih_ver;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
fake_flash_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Валидный слот ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void test_valid_slot_returns_its_header_version(void)
|
||||||
|
{
|
||||||
|
size_t len = load_fixture("valid_v1.bin");
|
||||||
|
struct image_version expected_ver = fixture_header_version();
|
||||||
|
|
||||||
|
fake_flash_write_slot(0, g_s_fixture_buf, len);
|
||||||
|
|
||||||
|
struct image_version actual_ver;
|
||||||
|
bool ok = slot_version_get(0U, &actual_ver);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(ok);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(expected_ver.iv_major, actual_ver.iv_major);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(expected_ver.iv_minor, actual_ver.iv_minor);
|
||||||
|
TEST_ASSERT_EQUAL_UINT16(expected_ver.iv_revision, actual_ver.iv_revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Повреждённый слот ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void test_corrupt_slot_returns_false(void)
|
||||||
|
{
|
||||||
|
size_t len = load_fixture("corrupt_v1.bin");
|
||||||
|
fake_flash_write_slot(1, g_s_fixture_buf, len);
|
||||||
|
|
||||||
|
struct image_version ver;
|
||||||
|
bool ok = slot_version_get(1U, &ver);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Пустой слот ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void test_empty_slot_returns_false(void)
|
||||||
|
{
|
||||||
|
/* fake_flash_reset() в setUp уже оставил слот стёртым (0xFF) — magic
|
||||||
|
* не совпадёт с IMAGE_MAGIC. */
|
||||||
|
struct image_version ver;
|
||||||
|
bool ok = slot_version_get(0U, &ver);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Неподтверждённый, но валидный образ ──────────────────────────────────
|
||||||
|
*
|
||||||
|
* Ключевая гарантия slot_version_get(): в отличие от boot_go(), статус
|
||||||
|
* confirm/copy_done не проверяется и не изменяется — вызов идемпотентен,
|
||||||
|
* безопасен сколько угодно раз и до первого boot_go() (см. slot_version.h).
|
||||||
|
*/
|
||||||
|
|
||||||
|
void test_unconfirmed_valid_image_still_reports_version(void)
|
||||||
|
{
|
||||||
|
size_t len = load_fixture("valid_v2_unconfirmed.bin");
|
||||||
|
struct image_version expected_ver = fixture_header_version();
|
||||||
|
|
||||||
|
fake_flash_write_slot(0, g_s_fixture_buf, len);
|
||||||
|
|
||||||
|
struct image_version actual_ver;
|
||||||
|
TEST_ASSERT_TRUE(slot_version_get(0U, &actual_ver));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(expected_ver.iv_major, actual_ver.iv_major);
|
||||||
|
|
||||||
|
/* Повторный вызов — по-прежнему успешен, слот не "стёрт", в отличие от
|
||||||
|
* повторного boot_go() (см. test_mcuboot_boot_select::
|
||||||
|
* test_boot_go_reverts_unconfirmed_image, где второй boot_go() как раз
|
||||||
|
* стирает такой же образ). */
|
||||||
|
TEST_ASSERT_TRUE(slot_version_get(0U, &actual_ver));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Точка входа ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_valid_slot_returns_its_header_version);
|
||||||
|
RUN_TEST(test_corrupt_slot_returns_false);
|
||||||
|
RUN_TEST(test_empty_slot_returns_false);
|
||||||
|
RUN_TEST(test_unconfirmed_valid_image_still_reports_version);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
55
tests/host/update_policy/README.md
Normal file
55
tests/host/update_policy/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# test_update_policy
|
||||||
|
|
||||||
|
## Модуль под тестом
|
||||||
|
|
||||||
|
`firmware/bootloader/src/update_policy.c` (`update_policy.h`) — чистая
|
||||||
|
логика решения «устанавливать ли SD-кандидат и в какой слот», без
|
||||||
|
аппаратных зависимостей (flash/FatFS). Часть Фазы 3 (SD update path), см.
|
||||||
|
`firmware/bootloader/PLAN.md`.
|
||||||
|
|
||||||
|
## Категория
|
||||||
|
|
||||||
|
A — платформонезависимый модуль. Зависит только от типа
|
||||||
|
`struct image_version` из `bootutil/image.h`, сам `bootutil` не линкуется.
|
||||||
|
Только Unity.
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
Нет — тестируется напрямую, без фейков/стабов. Для сборки нужны только
|
||||||
|
include-пути до `bootutil/image.h` (`mcuboot_config`), реальный `bootutil`
|
||||||
|
в тест не линкуется.
|
||||||
|
|
||||||
|
## Что проверяется
|
||||||
|
|
||||||
|
- **image_version_compare()** — старшинство `major` побеждает `minor`
|
||||||
|
побеждает `revision`; `build_num` игнорируется (версии, отличающиеся
|
||||||
|
только номером сборки, считаются равными).
|
||||||
|
- **update_policy_decide() — нет валидных слотов** — установка по
|
||||||
|
умолчанию в Slot A.
|
||||||
|
- **Валиден только Slot Б** — установка всё равно в Slot A, т.к. активный
|
||||||
|
слот — валидный Slot Б, а целевой слот установки всегда не активный.
|
||||||
|
- **Кандидат новее активного** — установка в неактивный слот.
|
||||||
|
- **Кандидат старше/равен активному без удержания кнопки** — `SKIP`.
|
||||||
|
- **Кандидат старше активного с удержанием кнопки** (форсированный
|
||||||
|
даунгрейд) — `INSTALL`.
|
||||||
|
- **Кандидат равен активному с удержанием кнопки** — всё равно `SKIP`
|
||||||
|
(переустановка той же версии не форсируется).
|
||||||
|
- **Оба слота валидны** — активным считается слот с более высокой версией,
|
||||||
|
установка идёт в другой.
|
||||||
|
|
||||||
|
## Гарантии
|
||||||
|
|
||||||
|
- Целевой слот установки всегда противоположен активному — активный слот
|
||||||
|
решением никогда не затрагивается.
|
||||||
|
- При отсутствии валидных слотов по умолчанию выбирается Slot A.
|
||||||
|
- Форсированный даунгрейд возможен только явным удержанием кнопки и только
|
||||||
|
если кандидат строго старше активного — при равенстве версий
|
||||||
|
переустановка не форсируется.
|
||||||
|
- Сравнение версий игнорирует `build_num` — тот же контракт, что у
|
||||||
|
`boot_version_cmp()` в `bootutil/loader.c`.
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctest --preset host-debug-test -R test_update_policy -V
|
||||||
|
```
|
||||||
222
tests/host/update_policy/test_update_policy.c
Normal file
222
tests/host/update_policy/test_update_policy.c
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
/**
|
||||||
|
* @file test_update_policy.c
|
||||||
|
* @brief Host-тесты чистой логики update_policy_decide()/image_version_compare().
|
||||||
|
*
|
||||||
|
* Без аппаратных зависимостей — сценарии зеркалят чек-лист аппаратной
|
||||||
|
* верификации Фазы 3 (firmware/bootloader/PLAN.md), но проверяются здесь
|
||||||
|
* без реального SD/flash.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "unity.h"
|
||||||
|
#include "update_policy.h"
|
||||||
|
|
||||||
|
/* ── Вспомогательные конструкторы ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
static struct image_version make_version(uint8_t major, uint8_t minor, uint16_t revision,
|
||||||
|
uint32_t build_num)
|
||||||
|
{
|
||||||
|
struct image_version ver = {
|
||||||
|
.iv_major = major,
|
||||||
|
.iv_minor = minor,
|
||||||
|
.iv_revision = revision,
|
||||||
|
.iv_build_num = build_num,
|
||||||
|
};
|
||||||
|
return ver;
|
||||||
|
}
|
||||||
|
|
||||||
|
static update_policy_slot_state_t make_valid_slot(struct image_version ver)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot = { .valid = true, .version = ver };
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const update_policy_slot_state_t K_INVALID_SLOT = { .valid = false };
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── image_version_compare ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void test_compare_major_wins(void)
|
||||||
|
{
|
||||||
|
struct image_version v1 = make_version(2, 0, 0, 0);
|
||||||
|
struct image_version v2 = make_version(1, 9, 9, 9);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(image_version_compare(&v1, &v2) > 0);
|
||||||
|
TEST_ASSERT_TRUE(image_version_compare(&v2, &v1) < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_compare_minor_wins_when_major_equal(void)
|
||||||
|
{
|
||||||
|
struct image_version v1 = make_version(1, 2, 0, 0);
|
||||||
|
struct image_version v2 = make_version(1, 1, 9, 9);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(image_version_compare(&v1, &v2) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_compare_revision_wins_when_major_minor_equal(void)
|
||||||
|
{
|
||||||
|
struct image_version v1 = make_version(1, 0, 5, 0);
|
||||||
|
struct image_version v2 = make_version(1, 0, 3, 100);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(image_version_compare(&v1, &v2) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_compare_ignores_build_num(void)
|
||||||
|
{
|
||||||
|
struct image_version v1 = make_version(1, 0, 0, 1);
|
||||||
|
struct image_version v2 = make_version(1, 0, 0, 999);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, image_version_compare(&v1, &v2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── update_policy_decide — нет валидных слотов ──────────────────────── */
|
||||||
|
|
||||||
|
void test_no_valid_slots_installs_to_slot_a(void)
|
||||||
|
{
|
||||||
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result =
|
||||||
|
update_policy_decide(&K_INVALID_SLOT, &K_INVALID_SLOT, &candidate, false);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
TEST_ASSERT_FALSE(result.erase_previous_active); /* нечего стирать */
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_only_slot_b_valid_targets_slot_a(void)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot_b = make_valid_slot(make_version(1, 0, 0, 0));
|
||||||
|
struct image_version candidate = make_version(2, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result =
|
||||||
|
update_policy_decide(&K_INVALID_SLOT, &slot_b, &candidate, false);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── update_policy_decide — кандидат новее активного ─────────────────── */
|
||||||
|
|
||||||
|
void test_candidate_newer_installs_to_inactive_slot(void)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot_a = make_valid_slot(make_version(1, 0, 0, 0));
|
||||||
|
struct image_version candidate = make_version(2, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result =
|
||||||
|
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, false);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_B, result.target_slot);
|
||||||
|
/* "Новее" не требует стирания прежнего активного — он сам проиграет
|
||||||
|
* сравнение версий в boot_go(). */
|
||||||
|
TEST_ASSERT_FALSE(result.erase_previous_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── update_policy_decide — кандидат старше/равен активному ──────────── */
|
||||||
|
|
||||||
|
void test_candidate_older_without_button_skips(void)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot_a = make_valid_slot(make_version(2, 0, 0, 0));
|
||||||
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result =
|
||||||
|
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, false);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SKIP, result.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_candidate_older_with_button_forces_install(void)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot_a = make_valid_slot(make_version(2, 0, 0, 0));
|
||||||
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result =
|
||||||
|
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, true);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_B, result.target_slot);
|
||||||
|
/* Без стирания прежнего активного (Slot A, v2) даунгрейд не имел бы
|
||||||
|
* эффекта — Slot A остался бы валиден и новее, снова выиграл бы в
|
||||||
|
* boot_go(), несмотря на успешную установку v1 в Slot Б. */
|
||||||
|
TEST_ASSERT_TRUE(result.erase_previous_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_candidate_equal_with_button_still_skips(void)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot_a = make_valid_slot(make_version(1, 0, 0, 0));
|
||||||
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result =
|
||||||
|
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, true);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SKIP, result.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── update_policy_decide — оба слота валидны ─────────────────────────── */
|
||||||
|
|
||||||
|
void test_active_slot_is_the_higher_version_when_both_valid(void)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot_a = make_valid_slot(make_version(1, 0, 0, 0));
|
||||||
|
update_policy_slot_state_t slot_b = make_valid_slot(make_version(2, 0, 0, 0));
|
||||||
|
struct image_version candidate = make_version(3, 0, 0, 0);
|
||||||
|
|
||||||
|
/* Активный — Б (выше версия), значит целевой слот установки — А. */
|
||||||
|
update_policy_result_t result = update_policy_decide(&slot_a, &slot_b, &candidate, false);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
TEST_ASSERT_FALSE(result.erase_previous_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── update_policy_decide — форс. даунгрейд должен реально загрузиться ───
|
||||||
|
*
|
||||||
|
* Регрессионный тест: без erase_previous_active даунгрейд был бы запятан на
|
||||||
|
* flash, но никогда не загружался бы — активный слот Б (v2) остаётся
|
||||||
|
* валиден и новее, снова выигрывает в boot_go(). Проверяем инвариант на
|
||||||
|
* уровне решения, не дожидаясь аппаратной верификации.
|
||||||
|
*/
|
||||||
|
|
||||||
|
void test_forced_downgrade_targets_and_erases_the_higher_version_slot(void)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot_a = make_valid_slot(make_version(1, 0, 0, 0));
|
||||||
|
update_policy_slot_state_t slot_b = make_valid_slot(make_version(2, 0, 0, 0));
|
||||||
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
|
/* Активный — Б (v2, выше версия). Кандидат v1 старше активного, кнопка
|
||||||
|
* удержана → форс. установка в Slot A (неактивный) + Slot Б обязан быть
|
||||||
|
* стёрт, иначе Slot Б (всё ещё валидный v2) снова выиграет. */
|
||||||
|
update_policy_result_t result = update_policy_decide(&slot_a, &slot_b, &candidate, true);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
TEST_ASSERT_TRUE(result.erase_previous_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Точка входа ───────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_compare_major_wins);
|
||||||
|
RUN_TEST(test_compare_minor_wins_when_major_equal);
|
||||||
|
RUN_TEST(test_compare_revision_wins_when_major_minor_equal);
|
||||||
|
RUN_TEST(test_compare_ignores_build_num);
|
||||||
|
|
||||||
|
RUN_TEST(test_no_valid_slots_installs_to_slot_a);
|
||||||
|
RUN_TEST(test_only_slot_b_valid_targets_slot_a);
|
||||||
|
RUN_TEST(test_candidate_newer_installs_to_inactive_slot);
|
||||||
|
RUN_TEST(test_candidate_older_without_button_skips);
|
||||||
|
RUN_TEST(test_candidate_older_with_button_forces_install);
|
||||||
|
RUN_TEST(test_candidate_equal_with_button_still_skips);
|
||||||
|
RUN_TEST(test_active_slot_is_the_higher_version_when_both_valid);
|
||||||
|
RUN_TEST(test_forced_downgrade_targets_and_erases_the_higher_version_slot);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
# Windows Host Results
|
|
||||||
|
|
||||||
## Step 1: spike_hab.py
|
|
||||||
|
|
||||||
```bash
|
|
||||||
production git:(feature-tui-monolith) uv run pytest spike/spike_hab.py -v -s
|
|
||||||
================================================================================================================ test session starts ================================================================================================================
|
|
||||||
platform darwin -- Python 3.14.6, pytest-9.1.1, pluggy-1.6.0 -- /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production/.venv/bin/python3
|
|
||||||
cachedir: .pytest_cache
|
|
||||||
rootdir: /Users/von_akimow/Desktop/TFT_ENV/tft_manufacture_test/tools/production
|
|
||||||
configfile: pyproject.toml
|
|
||||||
collected 2 items
|
|
||||||
|
|
||||||
spike/spike_hab.py::test_golden_hab_no_dcd PASSED
|
|
||||||
spike/spike_hab.py::test_golden_hab_with_dcd PASSED
|
|
||||||
|
|
||||||
================================================================================================================= 2 passed in 1.07s =================================================================================================================
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 2: spike_flash.py --ports-only
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# подключены M5StamPLC + плата с firmware_test
|
|
||||||
|
|
||||||
➜ production git:(feature-tui-monolith) ✗ uv run python spike/spike_flash.py --ports-only
|
|
||||||
=== Serial-порты (serial.tools.list_ports.comports) ===
|
|
||||||
/dev/cu.debug-console VID:PID=----:---- 'n/a' serial=None
|
|
||||||
/dev/cu.Bluetooth-Incoming-Port VID:PID=----:---- 'n/a' serial=None
|
|
||||||
/dev/cu.usbmodem11101 VID:PID=303A:4001 'Espressif Device' serial='3cdc75edcf6c0000'
|
|
||||||
/dev/cu.usbmodemGUXFBWDJBWTGQ3 VID:PID=1FC9:0143 'MCU-LINK (r0FB) CMSIS-DAP V3.172' serial='GUXFBWDJBWTGQ'
|
|
||||||
/dev/cu.usbmodem11301 VID:PID=1996:00AD 'MU LLC' serial=None
|
|
||||||
|
|
||||||
Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5).
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 3: spike_flash.py
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# подключены M5StamPLC + плата в режиме SDP
|
|
||||||
➜ production git:(feature-tui-monolith) ✗ uv run python spike/spike_flash.py
|
|
||||||
=== Serial-порты (serial.tools.list_ports.comports) ===
|
|
||||||
/dev/cu.debug-console VID:PID=----:---- 'n/a' serial=None
|
|
||||||
/dev/cu.Bluetooth-Incoming-Port VID:PID=----:---- 'n/a' serial=None
|
|
||||||
/dev/cu.usbmodem11101 VID:PID=303A:4001 'Espressif Device' serial='3cdc75edcf6c0000'
|
|
||||||
/dev/cu.usbmodemGUXFBWDJBWTGQ3 VID:PID=1FC9:0143 'MCU-LINK (r0FB) CMSIS-DAP V3.172' serial='GUXFBWDJBWTGQ'
|
|
||||||
|
|
||||||
Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5).
|
|
||||||
|
|
||||||
=== SDP/Flashloader (SDP=0x1fc9:0x0130, Flashloader=0x15a2:0x0073) ===
|
|
||||||
Загрузка Flashloader через SDP (0x1fc9:0x0130)
|
|
||||||
Ожидание Flashloader (до 10с).... OK
|
|
||||||
|
|
||||||
✅ get_property(CURRENT_VERSION) = [1258424320]
|
|
||||||
|
|
||||||
=== configure_memory (FlexSPI NOR) ===
|
|
||||||
configure_memory: OK
|
|
||||||
|
|
||||||
✅ Гейт 0 (SDP/Flashloader): пройден
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 4: spike_flash.py --erase-all
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Подключены M5StamPLC + плата в режиме SDP
|
|
||||||
|
|
||||||
➜ production git:(feature-tui-monolith) ✗ uv run python spike/spike_flash.py --erase-all
|
|
||||||
=== Serial-порты (serial.tools.list_ports.comports) ===
|
|
||||||
/dev/cu.debug-console VID:PID=----:---- 'n/a' serial=None
|
|
||||||
/dev/cu.Bluetooth-Incoming-Port VID:PID=----:---- 'n/a' serial=None
|
|
||||||
/dev/cu.usbmodem11101 VID:PID=303A:4001 'Espressif Device' serial='3cdc75edcf6c0000'
|
|
||||||
/dev/cu.usbmodemGUXFBWDJBWTGQ3 VID:PID=1FC9:0143 'MCU-LINK (r0FB) CMSIS-DAP V3.172' serial='GUXFBWDJBWTGQ'
|
|
||||||
|
|
||||||
Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5).
|
|
||||||
|
|
||||||
=== SDP/Flashloader (SDP=0x1fc9:0x0130, Flashloader=0x15a2:0x0073) ===
|
|
||||||
Flashloader уже запущен — пропускаем загрузку
|
|
||||||
|
|
||||||
✅ get_property(CURRENT_VERSION) = [1258424320]
|
|
||||||
|
|
||||||
=== configure_memory (FlexSPI NOR) ===
|
|
||||||
configure_memory: OK
|
|
||||||
|
|
||||||
⚠️ Chip erase сотрёт FCB — плата не загрузится до повторной прошивки.
|
|
||||||
Наберите ERASE для подтверждения: ERASE
|
|
||||||
Выставляю timeout=200000мс (эквивалент blhost -t 200000)
|
|
||||||
flash_erase_all: OK
|
|
||||||
|
|
||||||
✅ Гейт 0 (SDP/Flashloader): пройден
|
|
||||||
```
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
# Windows Host Results
|
|
||||||
|
|
||||||
## Step 1: spike_hab.py
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PS C:\Development\IMXRT\tft_manufacture_test\tools\production> uv run pytest .\spike\spike_hab.py -v -s
|
|
||||||
============================================================ test session starts =============================================================
|
|
||||||
platform win32 -- Python 3.13.14, pytest-9.1.1, pluggy-1.6.0 -- C:\Development\IMXRT\tft_manufacture_test\tools\production\.venv\Scripts\python.exe
|
|
||||||
cachedir: .pytest_cache
|
|
||||||
rootdir: C:\Development\IMXRT\tft_manufacture_test\tools\production
|
|
||||||
configfile: pyproject.toml
|
|
||||||
collected 2 items
|
|
||||||
|
|
||||||
spike/spike_hab.py::test_golden_hab_no_dcd PASSED
|
|
||||||
spike/spike_hab.py::test_golden_hab_with_dcd PASSED
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 2: spike_flash.py --ports-only
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# подключены M5StamPLC + плата с firmware_test
|
|
||||||
|
|
||||||
PS C:\Development\IMXRT\tft_manufacture_test\tools\production> uv run python .\spike\spike_flash.py --ports-only
|
|
||||||
=== Serial-порты (serial.tools.list_ports.comports) ===
|
|
||||||
COM10 VID:PID=1996:00AD 'Устройство с последовательным интерфейсом USB (COM10)' serial=''
|
|
||||||
COM8 VID:PID=303A:4001 'Устройство с последовательным интерфейсом USB (COM8)' serial='3CDC75EDCF6C0000'
|
|
||||||
|
|
||||||
Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5).
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 3: spike_flash.py
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# подключены M5StamPLC + плата в режиме SDP
|
|
||||||
PS C:\Development\IMXRT\tft_manufacture_test\tools\production> uv run python .\spike\spike_flash.py
|
|
||||||
=== Serial-порты (serial.tools.list_ports.comports) ===
|
|
||||||
COM8 VID:PID=303A:4001 'Устройство с последовательным интерфейсом USB (COM8)' serial='3CDC75EDCF6C0000'
|
|
||||||
|
|
||||||
Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5).
|
|
||||||
|
|
||||||
=== SDP/Flashloader (SDP=0x1fc9:0x0130, Flashloader=0x15a2:0x0073) ===
|
|
||||||
Загрузка Flashloader через SDP (0x1fc9:0x0130)
|
|
||||||
Ожидание Flashloader (до 10с).... OK
|
|
||||||
|
|
||||||
✅ get_property(CURRENT_VERSION) = [1258424320]
|
|
||||||
|
|
||||||
=== configure_memory (FlexSPI NOR) ===
|
|
||||||
configure_memory: OK
|
|
||||||
|
|
||||||
✅ Гейт 0 (SDP/Flashloader): пройден
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 4: spike_flash.py --erase-all
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Подключены M5StamPLC + плата в режиме SDP
|
|
||||||
|
|
||||||
PS C:\Development\IMXRT\tft_manufacture_test\tools\production> uv run python .\spike\spike_flash.py --erase-all
|
|
||||||
=== Serial-порты (serial.tools.list_ports.comports) ===
|
|
||||||
COM8 VID:PID=303A:4001 'Устройство с последовательным интерфейсом USB (COM8)' serial='3CDC75EDCF6C0000'
|
|
||||||
|
|
||||||
Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC запиши VID:PID из вывода выше — это и есть измерение О1 (см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5).
|
|
||||||
|
|
||||||
=== SDP/Flashloader (SDP=0x1fc9:0x0130, Flashloader=0x15a2:0x0073) ===
|
|
||||||
Загрузка Flashloader через SDP (0x1fc9:0x0130)
|
|
||||||
Ожидание Flashloader (до 10с).... OK
|
|
||||||
|
|
||||||
✅ get_property(CURRENT_VERSION) = [1258424320]
|
|
||||||
|
|
||||||
=== configure_memory (FlexSPI NOR) ===
|
|
||||||
configure_memory: OK
|
|
||||||
|
|
||||||
⚠️ Chip erase сотрёт FCB — плата не загрузится до повторной прошивки.
|
|
||||||
Наберите ERASE для подтверждения: ERASE
|
|
||||||
Выставляю timeout=200000мс (эквивалент blhost -t 200000)
|
|
||||||
flash_erase_all: OK
|
|
||||||
|
|
||||||
✅ Гейт 0 (SDP/Flashloader): пройден
|
|
||||||
```
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
spike_flash.py — Фаза 0 (де-риск): SDP/Flashloader/McuBoot через spsdk
|
|
||||||
Python API вместо subprocess sdphost/blhost.
|
|
||||||
|
|
||||||
Закрывает:
|
|
||||||
⚠В2 — способ задать таймаут ≥200с для flash_erase_all (эквивалент
|
|
||||||
`blhost -t 200000`): interface.device.timeout — МИЛЛИСЕКУНДЫ
|
|
||||||
(проверено чтением spsdk/utils/interfaces/device/usb_device.py;
|
|
||||||
докстрока UsbDevice.scan() говорит "секунды" — по факту мс,
|
|
||||||
default=2000, что явно мс, а не секунды).
|
|
||||||
Р7 — сигнатуры SdpUSBInterface.scan()/MbootUSBInterface.scan()
|
|
||||||
(device_id="0xVVVV:0xPPPP", HID-транспорт через libusbsio,
|
|
||||||
pyusb в детекте не участвует).
|
|
||||||
О1 — вывод list_ports.comports() для CDC-устройств (firmware_test,
|
|
||||||
M5StampPLC) с явными VID:PID — для ручной сверки на Windows.
|
|
||||||
|
|
||||||
Прогнать на Windows-машине БЕЗ Zadig — это и есть проверка Р7.
|
|
||||||
|
|
||||||
По умолчанию скрипт НЕ деструктивен: детект SDP → загрузка Flashloader →
|
|
||||||
get_property → configure_memory. Chip erase — только с --erase-all и
|
|
||||||
интерактивным подтверждением (как в flash_usb.py).
|
|
||||||
|
|
||||||
Запуск:
|
|
||||||
uv run python spike_flash.py # детект + get_property
|
|
||||||
uv run python spike_flash.py --ports-only # только serial-порты (О1)
|
|
||||||
uv run python spike_flash.py --erase-all # + chip erase (ДЕСТРУКТИВНО)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from spsdk.mboot import McuBoot, MbootUSBInterface
|
|
||||||
from spsdk.mboot.properties import PropertyTag
|
|
||||||
from spsdk.sdp import SDP, SdpUSBInterface
|
|
||||||
|
|
||||||
# tools/service_tui/spike/spike_flash.py → корень репозитория
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
||||||
FLASHLOADER_BIN = REPO_ROOT / "tools" / "host" / "dcd" / "ivt_flashloader.bin"
|
|
||||||
|
|
||||||
FLASHLOADER_LOAD_ADDR = 0x20001C00
|
|
||||||
FLEXSPI_OPTION_ADDR = 0x2000
|
|
||||||
FLEXSPI_OPTION_VALUE = 0xC0000007
|
|
||||||
FLEXSPI_MEMORY_ID = 9
|
|
||||||
ERASE_ALL_TIMEOUT_MS = 200_000 # эквивалент blhost -t 200000, см. docstring
|
|
||||||
|
|
||||||
|
|
||||||
def _device_id(vid_env: str, vid_default: str, pid_env: str, pid_default: str) -> str:
|
|
||||||
"""Тот же паттерн, что _usb() в tools/host/flash_usb.py, но в формате
|
|
||||||
spsdk USBDeviceFilter: "0xVVVV:0xPPPP"."""
|
|
||||||
vid = os.environ.get(vid_env, vid_default).strip().lower().removeprefix("0x")
|
|
||||||
pid = os.environ.get(pid_env, pid_default).strip().lower().removeprefix("0x")
|
|
||||||
return f"0x{vid}:0x{pid}"
|
|
||||||
|
|
||||||
|
|
||||||
SDP_ID = _device_id("BOOTROM_VID", "1fc9", "BOOTROM_PID", "0130")
|
|
||||||
FLASHLOADER_ID = _device_id("FLASHLOADER_VID", "15a2", "FLASHLOADER_PID", "0073")
|
|
||||||
|
|
||||||
|
|
||||||
def enumerate_serial_ports() -> None:
|
|
||||||
"""О1 + вопрос про Windows CDC: печатает VID:PID/описание всех serial-
|
|
||||||
портов. firmware_test (1996:00AD) — class-compliant CDC ACM, драйвер
|
|
||||||
не нужен ни на одной ОС (Windows 10+ грузит usbser.sys по классу
|
|
||||||
интерфейса, не по VID:PID). M5StampPLC — открытый вопрос О1: нативный
|
|
||||||
ESP32-S3 CDC (VID Espressif 303A, драйвер тоже не нужен) или мост
|
|
||||||
CH9102/CP210x (на изолированной Windows потребует один вендорский
|
|
||||||
драйвер — честная ОС-необходимость, не Zadig-костыль)."""
|
|
||||||
import serial.tools.list_ports as list_ports
|
|
||||||
|
|
||||||
print("=== Serial-порты (serial.tools.list_ports.comports) ===")
|
|
||||||
ports = list(list_ports.comports())
|
|
||||||
if not ports:
|
|
||||||
print(" (ничего не найдено)")
|
|
||||||
return
|
|
||||||
for info in ports:
|
|
||||||
vidpid = (
|
|
||||||
f"{info.vid:04X}:{info.pid:04X}" if info.vid is not None else "----:----"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f" {info.device:20s} VID:PID={vidpid} "
|
|
||||||
f"{info.description!r} serial={info.serial_number!r}"
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"\n Сверь: firmware_test ожидается как 1996:00AD. Для M5StampPLC "
|
|
||||||
"запиши VID:PID из вывода выше — это и есть измерение О1 "
|
|
||||||
"(см. MONOLITH_APP_PLAN.md §О1 и DEV_ARCH.md §5)."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_flashloader(timeout_s: float = 10.0) -> Optional[MbootUSBInterface]:
|
|
||||||
print(f" Ожидание Flashloader (до {timeout_s:.0f}с)...", end="", flush=True)
|
|
||||||
deadline = time.monotonic() + timeout_s
|
|
||||||
while time.monotonic() < deadline:
|
|
||||||
found = MbootUSBInterface.scan(device_id=FLASHLOADER_ID)
|
|
||||||
if found:
|
|
||||||
print(" OK")
|
|
||||||
return found[0]
|
|
||||||
time.sleep(0.5)
|
|
||||||
print(".", end="", flush=True)
|
|
||||||
print(" TIMEOUT")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def load_flashloader() -> Optional[MbootUSBInterface]:
|
|
||||||
"""SDP.write_file + jump_and_run — прямой аналог load_flashloader()
|
|
||||||
из tools/host/flash_usb.py, но через spsdk API вместо sdphost."""
|
|
||||||
already = MbootUSBInterface.scan(device_id=FLASHLOADER_ID)
|
|
||||||
if already:
|
|
||||||
print(" Flashloader уже запущен — пропускаем загрузку")
|
|
||||||
return already[0]
|
|
||||||
|
|
||||||
sdp_devices = SdpUSBInterface.scan(device_id=SDP_ID)
|
|
||||||
if not sdp_devices:
|
|
||||||
print(f" SDP-устройство не найдено ({SDP_ID}). Плата в BootROM-режиме?")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not FLASHLOADER_BIN.exists():
|
|
||||||
print(f" Не найден: {FLASHLOADER_BIN}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
print(f" Загрузка Flashloader через SDP ({SDP_ID})")
|
|
||||||
data = FLASHLOADER_BIN.read_bytes()
|
|
||||||
with SDP(sdp_devices[0]) as sdp:
|
|
||||||
sdp.write_file(FLASHLOADER_LOAD_ADDR, data)
|
|
||||||
sdp.jump_and_run(FLASHLOADER_LOAD_ADDR)
|
|
||||||
|
|
||||||
return wait_for_flashloader()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--ports-only",
|
|
||||||
action="store_true",
|
|
||||||
help="Только serial-порты (О1), без SDP/Flashloader",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--erase-all", action="store_true", help="ДЕСТРУКТИВНО: chip erase после detect"
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
enumerate_serial_ports()
|
|
||||||
if args.ports_only:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
print(f"\n=== SDP/Flashloader (SDP={SDP_ID}, Flashloader={FLASHLOADER_ID}) ===")
|
|
||||||
iface = load_flashloader()
|
|
||||||
if iface is None:
|
|
||||||
print("\n❌ Flashloader не поднялся — дальнейшие шаги пропущены")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
with McuBoot(iface) as mboot:
|
|
||||||
version = mboot.get_property(PropertyTag.CURRENT_VERSION)
|
|
||||||
print(f"\n✅ get_property(CURRENT_VERSION) = {version}")
|
|
||||||
|
|
||||||
print("\n=== configure_memory (FlexSPI NOR) ===")
|
|
||||||
mboot.fill_memory(FLEXSPI_OPTION_ADDR, 4, FLEXSPI_OPTION_VALUE)
|
|
||||||
ok = mboot.configure_memory(FLEXSPI_OPTION_ADDR, FLEXSPI_MEMORY_ID)
|
|
||||||
print(f"configure_memory: {'OK' if ok else 'FAILED'}")
|
|
||||||
|
|
||||||
if args.erase_all:
|
|
||||||
print(
|
|
||||||
"\n⚠️ Chip erase сотрёт FCB — плата не загрузится до повторной прошивки."
|
|
||||||
)
|
|
||||||
confirm = input("Наберите ERASE для подтверждения: ")
|
|
||||||
if confirm != "ERASE":
|
|
||||||
print("Отменено.")
|
|
||||||
return 0
|
|
||||||
print(
|
|
||||||
f" Выставляю timeout={ERASE_ALL_TIMEOUT_MS}мс (эквивалент blhost -t 200000)"
|
|
||||||
)
|
|
||||||
iface.device.timeout = ERASE_ALL_TIMEOUT_MS
|
|
||||||
ok = mboot.flash_erase_all(mem_id=FLEXSPI_MEMORY_ID)
|
|
||||||
print(f"flash_erase_all: {'OK' if ok else 'FAILED'}")
|
|
||||||
mboot.reset(reopen=False)
|
|
||||||
|
|
||||||
print("\n✅ Гейт 0 (SDP/Flashloader): пройден")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
spike_hab.py — Фаза 0 (де-риск): HAB через HabImage (spsdk Python API)
|
|
||||||
вместо subprocess `nxpimage hab export`.
|
|
||||||
|
|
||||||
Закрывает ⚠В1: подтверждает, что HabImage.load_from_config(...).export()
|
|
||||||
даёт побайтно идентичный `nxpimage hab export` результат с тем же
|
|
||||||
конфигом, для DCD on/off.
|
|
||||||
|
|
||||||
Реальный вызов подсмотрен в исходнике самого nxpimage —
|
|
||||||
spsdk/apps/nxpimage_apps/nxpimage_hab.py::hab_export() делает ровно
|
|
||||||
следующее (это не внутренний вызов apps-модуля, а повтор того же кода
|
|
||||||
на публичных классах Config/HabImage):
|
|
||||||
|
|
||||||
cfg = Config.create_from_file(yaml_path)
|
|
||||||
schemas = HabImage.get_validation_schemas_from_cfg(cfg)
|
|
||||||
cfg.check(schemas, check_unknown_props=True)
|
|
||||||
hab = HabImage.load_from_config(cfg)
|
|
||||||
hab.post_export(cfg.config_dir)
|
|
||||||
data = hab.export()
|
|
||||||
|
|
||||||
Временный YAML пишется в системный tmp (Р6) с АБСОЛЮТНЫМИ путями
|
|
||||||
inputImageFile/DCDFilePath — в отличие от текущего flasher.py, который
|
|
||||||
полагается на cwd=tools/host/hab/ + относительный "../dcd/dcd.bin".
|
|
||||||
Config.get_input_file_name() резолвит путь через find_file(search_paths=
|
|
||||||
[cfg_dir]), а абсолютный путь find_file отдаёт как есть независимо от
|
|
||||||
search_paths (проверено чтением spsdk/utils/config.py) — значит схема
|
|
||||||
Р6 (temp где угодно, не обязательно рядом с hab/) технически безопасна.
|
|
||||||
|
|
||||||
Содержимое "прошивки" для golden-теста не имеет значения — сравнивается
|
|
||||||
результат УПАКОВКИ (IVT/BDT/[DCD]/APP), а не семантика кода, поэтому
|
|
||||||
используется детерминированный dummy-бинарь. DCD, наоборот, должен быть
|
|
||||||
настоящей HAB DCD-командной последовательностью (SegDCD.parse иначе
|
|
||||||
падает) — поэтому DCD-кейс использует штатный tools/host/dcd/dcd.bin;
|
|
||||||
если его нет рядом — тест скипается, а не подделывается фиктивным DCD.
|
|
||||||
|
|
||||||
Запуск:
|
|
||||||
uv run pytest spike_hab.py -v -s # golden-тест (без железа)
|
|
||||||
uv run python spike_hab.py # то же + подробный вывод в консоль
|
|
||||||
uv run python spike_hab.py --dcd-bin /path/to/other_dcd.bin
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import hashlib
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from spsdk.image.hab.hab_image import HabImage
|
|
||||||
from spsdk.utils.config import Config
|
|
||||||
|
|
||||||
# tools/service_tui/spike/spike_hab.py → корень репозитория
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
||||||
REAL_DCD_BIN = REPO_ROOT / "tools" / "host" / "dcd" / "dcd.bin"
|
|
||||||
|
|
||||||
_HAB_OPTIONS = [
|
|
||||||
"options:",
|
|
||||||
" flags: 0x00",
|
|
||||||
" startAddress: 0x60000000",
|
|
||||||
" ivtOffset: 0x1000",
|
|
||||||
" initialLoadSize: 0x2000",
|
|
||||||
" family: mimxrt1050",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _make_dummy_app(size: int = 512) -> bytes:
|
|
||||||
"""Детерминированные псевдослучайные байты фиксированного размера.
|
|
||||||
|
|
||||||
Не настоящая прошивка — см. docstring модуля: для golden-теста важна
|
|
||||||
побайтная идентичность УПАКОВКИ, а не валидность кода приложения.
|
|
||||||
"""
|
|
||||||
seed = hashlib.sha256(b"tft-monolith-phase0-golden-hab").digest()
|
|
||||||
return (seed * (size // len(seed) + 1))[:size]
|
|
||||||
|
|
||||||
|
|
||||||
def _write_config(input_bin: Path, dcd_bin: Optional[Path], work_dir: Path) -> Path:
|
|
||||||
"""Собрать YAML-конфиг nxpimage hab (абсолютные пути, см. docstring модуля)."""
|
|
||||||
lines = list(_HAB_OPTIONS)
|
|
||||||
if dcd_bin is not None:
|
|
||||||
lines.append(f" DCDFilePath: {dcd_bin.resolve().as_posix()}")
|
|
||||||
lines.append(f'inputImageFile: "{input_bin.resolve().as_posix()}"')
|
|
||||||
lines.append("sections: []")
|
|
||||||
|
|
||||||
suffix = "dcd" if dcd_bin is not None else "nodcd"
|
|
||||||
yaml_path = work_dir / f"hab_golden_{suffix}.yaml"
|
|
||||||
yaml_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
||||||
return yaml_path
|
|
||||||
|
|
||||||
|
|
||||||
def build_via_api(yaml_path: Path) -> bytes:
|
|
||||||
"""Собрать HAB-образ через spsdk Python API — то же самое, что делает
|
|
||||||
`nxpimage hab export` изнутри (см. docstring модуля)."""
|
|
||||||
cfg = Config.create_from_file(str(yaml_path))
|
|
||||||
schemas = HabImage.get_validation_schemas_from_cfg(cfg)
|
|
||||||
cfg.check(schemas, check_unknown_props=True)
|
|
||||||
hab = HabImage.load_from_config(cfg)
|
|
||||||
hab.post_export(cfg.config_dir)
|
|
||||||
return hab.export()
|
|
||||||
|
|
||||||
|
|
||||||
def _require_nxpimage() -> str:
|
|
||||||
path = shutil.which("nxpimage")
|
|
||||||
if path is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"nxpimage не найден в PATH — активирован ли venv tools/service_tui "
|
|
||||||
"(spsdk кладёт nxpimage как console_script)?"
|
|
||||||
)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def build_via_cli(yaml_path: Path, out_bin: Path) -> bytes:
|
|
||||||
"""Собрать тот же образ через `nxpimage hab export` (эталон сравнения)."""
|
|
||||||
nxpimage = _require_nxpimage()
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
nxpimage,
|
|
||||||
"hab",
|
|
||||||
"export",
|
|
||||||
"--force",
|
|
||||||
"-c",
|
|
||||||
str(yaml_path),
|
|
||||||
"-o",
|
|
||||||
str(out_bin),
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
return out_bin.read_bytes()
|
|
||||||
|
|
||||||
|
|
||||||
def _run_golden(work_dir: Path, dcd_bin: Optional[Path]) -> tuple[bytes, bytes]:
|
|
||||||
input_bin = work_dir / "dummy_app.bin"
|
|
||||||
input_bin.write_bytes(_make_dummy_app())
|
|
||||||
yaml_path = _write_config(input_bin, dcd_bin, work_dir)
|
|
||||||
api_bytes = build_via_api(yaml_path)
|
|
||||||
cli_bytes = build_via_cli(yaml_path, work_dir / "out_cli.bin")
|
|
||||||
return api_bytes, cli_bytes
|
|
||||||
|
|
||||||
|
|
||||||
# ── pytest: остаётся навсегда как регрессия на апгрейды spsdk (Гейт 0) ──
|
|
||||||
# Примечание: сейчас лежит в spike/ (временная директория по плану Фазы 0);
|
|
||||||
# в Фазе 1 у tools/service_tui/ появляется tests/ — тогда этот файл стоит
|
|
||||||
# туда перенести (или вынести тесты в отдельный test_hab_golden.py).
|
|
||||||
|
|
||||||
|
|
||||||
def test_golden_hab_no_dcd(tmp_path: Path) -> None:
|
|
||||||
api_bytes, cli_bytes = _run_golden(tmp_path, dcd_bin=None)
|
|
||||||
assert api_bytes == cli_bytes, (
|
|
||||||
f"HabImage API разошёлся с nxpimage CLI (DCD off): "
|
|
||||||
f"{len(api_bytes)} vs {len(cli_bytes)} байт"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_golden_hab_with_dcd(tmp_path: Path) -> None:
|
|
||||||
if not REAL_DCD_BIN.exists():
|
|
||||||
pytest.skip(f"Реальный DCD не найден: {REAL_DCD_BIN}")
|
|
||||||
api_bytes, cli_bytes = _run_golden(tmp_path, dcd_bin=REAL_DCD_BIN)
|
|
||||||
assert api_bytes == cli_bytes, (
|
|
||||||
f"HabImage API разошёлся с nxpimage CLI (DCD on): "
|
|
||||||
f"{len(api_bytes)} vs {len(cli_bytes)} байт"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ── standalone запуск: то же самое, но с подробным выводом в консоль ────
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dcd-bin", type=Path, default=REAL_DCD_BIN, help="Путь к реальному dcd.bin"
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
_require_nxpimage()
|
|
||||||
except RuntimeError as exc:
|
|
||||||
print(f"❌ {exc}")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
work_dir = Path(tempfile.mkdtemp(prefix="spike_hab_"))
|
|
||||||
print(f"Рабочая директория: {work_dir}")
|
|
||||||
|
|
||||||
ok = True
|
|
||||||
for dcd_bin, label in [(None, "DCD off"), (args.dcd_bin, "DCD on")]:
|
|
||||||
print(f"\n--- {label} ---")
|
|
||||||
if dcd_bin is not None and not dcd_bin.exists():
|
|
||||||
print(f" ПРОПУЩЕНО: DCD-файл не найден: {dcd_bin}")
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
api_bytes, cli_bytes = _run_golden(work_dir, dcd_bin)
|
|
||||||
match = api_bytes == cli_bytes
|
|
||||||
print(f" API: {len(api_bytes)} байт, CLI: {len(cli_bytes)} байт")
|
|
||||||
print(f" {'✅ ПОБАЙТНО СОВПАДАЕТ' if match else '❌ РАСХОЖДЕНИЕ'}")
|
|
||||||
ok = ok and match
|
|
||||||
except subprocess.CalledProcessError as exc:
|
|
||||||
print(f" ❌ nxpimage CLI упал: {exc.stderr}")
|
|
||||||
ok = False
|
|
||||||
except Exception as exc: # noqa: BLE001 — спайк, важен весь контекст ошибки
|
|
||||||
print(f" ❌ {type(exc).__name__}: {exc}")
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
print(f"\nРезультат: {'✅ Гейт 0 (HAB) пройден' if ok else '❌ Стоп-условие В1'}")
|
|
||||||
return 0 if ok else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
spike_readback.py — диагностика Гейта 3: читает Flash обратно и сравнивает
|
|
||||||
с ожидаемыми файлами (FCB-блоб @0x60000000, HAB-образ @0x60001000).
|
|
||||||
|
|
||||||
Запуск (плата в SDP или с уже поднятым Flashloader):
|
|
||||||
uv run python spike/spike_readback.py \
|
|
||||||
--expect-fcb ../host/dcd/w25q128_fdcb.bin \
|
|
||||||
--expect-image /путь/к/собранному.hab.bin
|
|
||||||
Дампы кладёт рядом: readback_fcb.bin, readback_image.bin.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
||||||
|
|
||||||
from app import flash_backend as fb # noqa: E402
|
|
||||||
from spsdk.mboot import McuBoot # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def _cmp(label: str, actual: bytes, expected: bytes) -> bool:
|
|
||||||
if actual == expected:
|
|
||||||
print(f" {label}: ✅ ПОБАЙТНО СОВПАДАЕТ ({len(actual)} байт)")
|
|
||||||
return True
|
|
||||||
n = min(len(actual), len(expected))
|
|
||||||
diff_at = next((i for i in range(n) if actual[i] != expected[i]), n)
|
|
||||||
print(
|
|
||||||
f" {label}: ❌ РАСХОЖДЕНИЕ с офсета 0x{diff_at:X} "
|
|
||||||
f"(len actual={len(actual)}, expected={len(expected)})"
|
|
||||||
)
|
|
||||||
print(f" actual [{diff_at:#x}]: {actual[diff_at : diff_at + 16].hex()}")
|
|
||||||
print(f" expected[{diff_at:#x}]: {expected[diff_at : diff_at + 16].hex()}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
ap = argparse.ArgumentParser()
|
|
||||||
ap.add_argument("--expect-fcb", type=Path, required=True)
|
|
||||||
ap.add_argument("--expect-image", type=Path, required=True)
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
exp_fcb = args.expect_fcb.read_bytes()
|
|
||||||
exp_img = args.expect_image.read_bytes()
|
|
||||||
|
|
||||||
iface = fb.load_flashloader(progress_cb=lambda p: print(f" {p.message}"))
|
|
||||||
ok = True
|
|
||||||
with McuBoot(iface) as mboot:
|
|
||||||
fb.configure_flexspi(mboot)
|
|
||||||
|
|
||||||
fcb = mboot.read_memory(fb.FLASH_BASE, max(len(exp_fcb), 512), mem_id=0)
|
|
||||||
img = mboot.read_memory(fb.FLASH_BASE + fb.HAB_OFFSET, len(exp_img), mem_id=0)
|
|
||||||
if fcb is None or img is None:
|
|
||||||
print("❌ read_memory вернул None")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
Path("readback_fcb.bin").write_bytes(fcb)
|
|
||||||
Path("readback_image.bin").write_bytes(img)
|
|
||||||
|
|
||||||
ok = _cmp("FCB @0x60000000", fcb[: len(exp_fcb)], exp_fcb) and ok
|
|
||||||
ok = _cmp("Image @0x60001000", img, exp_img) and ok
|
|
||||||
|
|
||||||
return 0 if ok else 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
Loading…
Reference in a new issue