# bootloader: Phase 3 - wdog testing
This commit is contained in:
parent
17698ce109
commit
1def117e44
23 changed files with 1054 additions and 175 deletions
|
|
@ -2,10 +2,7 @@ if(BUILD_TESTS_HOST)
|
|||
return()
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# bsp_board — генерированные файлы Config Tools, инициализация платы
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# bsp_board — cгенерированные Config Tools файлы, инициализация платы
|
||||
add_library(bsp_board STATIC generated/board.c generated/pin_mux.c)
|
||||
|
||||
add_subdirectory(common)
|
||||
|
|
@ -15,6 +12,7 @@ add_subdirectory(uart_host)
|
|||
add_subdirectory(opto)
|
||||
add_subdirectory(can)
|
||||
add_subdirectory(button)
|
||||
add_subdirectory(wdog)
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(usb_cdc)
|
||||
add_subdirectory(sdram)
|
||||
|
|
@ -23,7 +21,6 @@ add_subdirectory(sd)
|
|||
add_subdirectory(mqs)
|
||||
add_subdirectory(provisioning)
|
||||
|
||||
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
|
||||
target_compile_options(bsp_board PRIVATE -w)
|
||||
|
||||
# SYSTEM подавляет предупреждения для всех внешних потребителей
|
||||
|
|
@ -67,10 +64,6 @@ target_compile_definitions(
|
|||
add_library(bsp_boot_ram INTERFACE)
|
||||
target_compile_definitions(bsp_boot_ram INTERFACE SKIP_SYSCLK_INIT)
|
||||
|
||||
# bsp_boot_xip без DCD — для firmware/bootloader: XIP из Flash, но без
|
||||
# инициализации SDRAM (bootloader SDRAM не использует, см.
|
||||
# docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). В отличие от bsp_boot_xip не
|
||||
# определяет XIP_BOOT_HEADER_DCD_ENABLE.
|
||||
add_library(bsp_boot_xip_no_dcd INTERFACE)
|
||||
target_compile_definitions(
|
||||
bsp_boot_xip_no_dcd INTERFACE XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1
|
||||
|
|
|
|||
|
|
@ -28,9 +28,27 @@ static sd_io_voltage_t s_io_voltage = {
|
|||
volatile uint32_t g_sdmmc_dbg_dma_buf_addr = 0U;
|
||||
volatile uint32_t g_sdmmc_dbg_usdhc1_src_clock_hz = 0U;
|
||||
|
||||
static bool sd_card_detect_gpio(void)
|
||||
/*
|
||||
* Детект карты через USDHC PRES_STATE.CINST — ЕДИНЫЙ механизм и для нашего
|
||||
* гейта bsp_sd_is_inserted() (bsp/sd/src/sd.c), и для внутреннего
|
||||
* SD_PollingCardInsert() SDK (тот зовёт этот callback при kSD_DetectCardByGpioCD).
|
||||
* Не GPIO_PinRead — исторически детект через GPIO2/28 давал ложный "card
|
||||
* present" на пустом слоте (Фаза 3, симптом 1; DEBUG_LOG_PHASE3_SD.md, раунд 3).
|
||||
* Работает, пока пин D13 замаплен на USDHC1_CD_B (см. BOARD_SD_Config ниже:
|
||||
* прежний remux на GPIO2_IO28 убран, пин остаётся на USDHC1_CD_B постоянно —
|
||||
* консолидация, item 1). Тактирование USDHC1 включается идемпотентно на случай
|
||||
* вызова до полного SD_HostInit().
|
||||
*
|
||||
* Тот же PRSSTAT-бит читает и штатный host-CD путь SDK
|
||||
* (SDMMCHOST_CardDetectStatus), но kSD_DetectCardByHostCD дополнительно взводит
|
||||
* USDHC card-detect ПРЕРЫВАНИЯ — не нужны загрузчику (лишний источник IRQ перед
|
||||
* прыжком), поэтому оставляем polling через callback (kSD_DetectCardByGpioCD).
|
||||
*/
|
||||
static bool sd_card_detect_prsstat(void)
|
||||
{
|
||||
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) == BOARD_SDMMC_SD_CD_INSERT_LEVEL;
|
||||
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
||||
return (USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR) &
|
||||
(uint32_t) kUSDHC_CardInsertedFlag) != 0U;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
|
|
@ -125,19 +143,11 @@ static void sd_pin_config(uint32_t freq)
|
|||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
|
||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
|
||||
/*
|
||||
* CD_B pad config — байт-в-байт как TFT_BOOTLOADER::BOARD_SD_Pin_Config()
|
||||
* (board/sdmmc_config.c: IOMUXC_SetPinConfig(..., 0x10B0U)), не "разумная
|
||||
* по умолчанию" альтернатива. Отличие от прежней версии здесь: PKE=1 но
|
||||
* PUE=0 — это KEEPER, не активная подтяжка (PUS игнорируется в этом
|
||||
* режиме); HYS выключен. Прежняя версия (активная 47к подтяжка вверх +
|
||||
* hysteresis) не была проверена на этой плате и не объяснила устойчивый
|
||||
* ложный "card present" без карты на реальном железе (Фаза 3, симптом 1,
|
||||
* см. DEBUG_LOG_PHASE3_SD.md) — pinmux сам по себе (GPIO2_IO28) это не
|
||||
* лечит, раз симптом воспроизводится и после его отката.
|
||||
* CD (D13) здесь НЕ конфигурируем: пин на USDHC1_CD_B (см. BOARD_SD_Config),
|
||||
* pad задан в BOARD_InitPins() и на железе даёт корректный CINST. Прежняя
|
||||
* настройка pad'а GPIO2_IO28 убрана вместе с GPIO-детектом (item 1,
|
||||
* DEBUG_LOG_PHASE3_SD.md).
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
|
|
@ -162,10 +172,10 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
|
|||
g_sdmmc_dbg_dma_buf_addr = (uint32_t)(uintptr_t)s_dma_buf;
|
||||
g_sdmmc_dbg_usdhc1_src_clock_hz = sd->host->hostController.sourceClock_Hz;
|
||||
|
||||
/* --- card detect: GPIO CD (active-low) --- */
|
||||
/* --- card detect: USDHC PRES_STATE.CINST через callback (polling, без IRQ) --- */
|
||||
s_cd.cdDebounce_ms = BOARD_SDMMC_SD_CD_DEBOUNCE_MS;
|
||||
s_cd.type = BOARD_SDMMC_SD_CD_TYPE;
|
||||
s_cd.cardDetected = sd_card_detect_gpio;
|
||||
s_cd.type = BOARD_SDMMC_SD_CD_TYPE; /* kSD_DetectCardByGpioCD → callback ниже */
|
||||
s_cd.cardDetected = sd_card_detect_prsstat;
|
||||
s_cd.callback = cd; /* обычно NULL из bsp_sd */
|
||||
s_cd.userData = user_data;
|
||||
|
||||
|
|
@ -178,37 +188,19 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
|
|||
/* --- GPIO питания --- */
|
||||
sd_power_init();
|
||||
/*
|
||||
* 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 везде).
|
||||
* CD_B (GPIO_B1_12 / physical D13) остаётся на USDHC1_CD_B постоянно —
|
||||
* единый механизм детекта через PRES_STATE.CINST (sd_card_detect_prsstat
|
||||
* выше + гейт bsp_sd_is_inserted). Прежней двойной маршрутизации
|
||||
* (remux на GPIO2_IO28 для GPIO-чтения внутри f_mount) больше нет —
|
||||
* см. DEBUG_LOG_PHASE3_SD.md, раунд 3 «консолидация детекта» (item 1);
|
||||
* она убирала латентную хрупкость: после первого bsp_sd_init() пин уходил
|
||||
* на GPIO2_IO28 и повторный PRSSTAT-скан ослеп бы. Явно переустанавливаем
|
||||
* альт-функцию (BOARD_InitPins() её тоже ставит — так модуль не зависит от
|
||||
* порядка инициализации). Pad этого пина оставляем как задал BOARD_InitPins:
|
||||
* на железе CINST на нём читается корректно (Фаза 3, все сценарии).
|
||||
*/
|
||||
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);
|
||||
const gpio_pin_config_t cd_cfg = {
|
||||
.direction = kGPIO_DigitalInput,
|
||||
.outputLogic = 0U,
|
||||
.interruptMode = kGPIO_NoIntmode,
|
||||
};
|
||||
GPIO_PinInit(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN, &cd_cfg);
|
||||
/* Важно: применяем pad-конфиг сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
|
||||
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U);
|
||||
/* Pad-конфиг линий SD (CMD/CLK/DATA) сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
|
||||
sd_pin_config(400000U);
|
||||
|
||||
/* --- приоритет прерывания хоста --- */
|
||||
|
|
|
|||
15
bsp/wdog/CMakeLists.txt
Normal file
15
bsp/wdog/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
if(BUILD_TESTS_HOST)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_library(bsp_wdog STATIC src/wdog.c)
|
||||
|
||||
target_include_directories(
|
||||
bsp_wdog
|
||||
PUBLIC include/
|
||||
PRIVATE src/)
|
||||
|
||||
target_link_libraries(
|
||||
bsp_wdog
|
||||
PUBLIC bsp_status
|
||||
PRIVATE sdk_wdog)
|
||||
80
bsp/wdog/README.md
Normal file
80
bsp/wdog/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# bsp_wdog — аппаратный watchdog (WDOG1)
|
||||
|
||||
Аппаратный сброс МК по таймауту. Защищает от зависаний в блокирующих вызовах,
|
||||
которые не возвращают управление в код приложения — там, где программный
|
||||
watchdog (флаг + проверка в основном цикле) бессилен, поскольку сам цикл не
|
||||
выполняется.
|
||||
|
||||
---
|
||||
|
||||
## Аппаратура
|
||||
|
||||
| Параметр | Значение |
|
||||
| ---------------------- | ---------------------------------------------- |
|
||||
| Периферия | WDOG1 |
|
||||
| Шаг таймаута | 0.5 c |
|
||||
| Диапазон таймаута | 1..128 c |
|
||||
| Причина сброса | `WDOG1->WRSR.TOUT` (1 — сброс был по watchdog) |
|
||||
| Поведение под SWD-halt | Приостановлен (`enableDebug = false`) |
|
||||
|
||||
---
|
||||
|
||||
## Контракт: WDE — write-once
|
||||
|
||||
`WDOG_WCR.WDE` (enable) — бит однократной записи: после `bsp_wdog_init()`
|
||||
watchdog нельзя выключить программно до следующего POR. Он остаётся взведённым
|
||||
и после любого перехода управления внутри той же сессии питания (переход в
|
||||
другой образ прыжком, а не через ресет). Любой код, к которому управление
|
||||
переходит после инициализации watchdog в этой же сессии, обязан периодически
|
||||
вызывать `bsp_wdog_refresh()` не реже периода таймаута — иначе неизбежен
|
||||
reset-loop.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```c
|
||||
bsp_status_t bsp_wdog_init(uint32_t timeout_s); /* взвести, once; захватывает причину предыдущего сброса */
|
||||
void bsp_wdog_refresh(void); /* сбросить счётчик таймаута */
|
||||
bool bsp_wdog_caused_last_reset(void); /* true, если последний сброс МК — по таймауту WDOG */
|
||||
bool bsp_wdog_is_armed(void); /* true после успешного init() */
|
||||
uint32_t bsp_wdog_timeout_s(void); /* сконфигурированный таймаут (0 до init) */
|
||||
```
|
||||
|
||||
`bsp_wdog_refresh()` безопасно звать даже до `bsp_wdog_init()` — no-op.
|
||||
Звать только в точках подтверждённого прогресса, не непосредственно перед
|
||||
вызовом, от зависания в котором watchdog и защищает.
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```c
|
||||
#include "bsp/wdog.h"
|
||||
|
||||
/* main.c — как можно раньше после board_hw_init(): */
|
||||
bsp_wdog_init(10U); /* c запасом над самой длинной легитимной операцией */
|
||||
|
||||
if (bsp_wdog_caused_last_reset())
|
||||
{
|
||||
/* предыдущая сессия закончилась таймаутом — восстановились после зависания */
|
||||
}
|
||||
|
||||
/* в основном цикле и в точках подтверждённого прогресса: */
|
||||
bsp_wdog_refresh();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CMake
|
||||
|
||||
```cmake
|
||||
target_link_libraries(firmware_test PRIVATE bsp_wdog)
|
||||
```
|
||||
|
||||
**Зависимости модуля:**
|
||||
|
||||
| Зависимость | Тип | Описание |
|
||||
| ------------ | ------- | ------------------------------------ |
|
||||
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||
| `sdk_wdog` | PRIVATE | `fsl_wdog.h` — `WDOG_Init/Refresh()` |
|
||||
60
bsp/wdog/include/bsp/wdog.h
Normal file
60
bsp/wdog/include/bsp/wdog.h
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* bsp_wdog — аппаратный watchdog (WDOG1, MIMXRT1052).
|
||||
*
|
||||
* Назначение: аппаратный сброс МК по таймауту. Защищает от зависаний в
|
||||
* блокирующих вызовах, которые не возвращают управление в код приложения —
|
||||
* там, где программный watchdog (флаг + проверка в основном цикле) бессилен,
|
||||
* поскольку сам цикл не выполняется.
|
||||
*
|
||||
* Контракт: WDOG Enable (WDE) — write-once бит. После bsp_wdog_init() watchdog
|
||||
* нельзя выключить программно до следующего POR; он остаётся взведённым в
|
||||
* течение всей сессии питания, включая любую передачу управления внутри неё
|
||||
* (не только через ресет). Любой код, к которому управление переходит после
|
||||
* инициализации watchdog в этой же сессии, обязан периодически звать
|
||||
* bsp_wdog_refresh() не реже периода таймаута — иначе reset-loop. Под
|
||||
* отладчиком (SWD halt) watchdog приостанавливается (enableDebug=false), так
|
||||
* что пошаговая отладка не сбивается сбросами.
|
||||
*/
|
||||
|
||||
#ifndef BSP_WDOG_H
|
||||
#define BSP_WDOG_H
|
||||
|
||||
#include "bsp/status.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Взвести WDOG1 с таймаутом timeout_s секунд (аппаратно округляется до шага
|
||||
* 0.5 с: реальный таймаут = (2*timeout_s) * 0.5 c). Разумный диапазон 1..128.
|
||||
*
|
||||
* Побочно захватывает причину ПРЕДЫДУЩЕГО сброса (WDOG-таймаут vs прочее) для
|
||||
* bsp_wdog_caused_last_reset() — читать до/после безразлично, но делается тут.
|
||||
*
|
||||
* Вызывать один раз, как можно раньше в main() (после board_hw_init()).
|
||||
* Повторный вызов — no-op (WDE уже взведён).
|
||||
*/
|
||||
bsp_status_t bsp_wdog_init(uint32_t timeout_s);
|
||||
|
||||
/*
|
||||
* "Погладить" watchdog — сбросить счётчик таймаута. Дёшево; безопасно звать
|
||||
* даже если WDOG не взведён (запись refresh-последовательности безвредна).
|
||||
* Звать только в точках РЕАЛЬНОГО прогресса, НЕ перед блокирующими вызовами,
|
||||
* от зависания в которых watchdog и защищает.
|
||||
*/
|
||||
void bsp_wdog_refresh(void);
|
||||
|
||||
/*
|
||||
* true, если ПОСЛЕДНИЙ сброс МК был вызван таймаутом WDOG (а не power-on /
|
||||
* software / прочим). Валидно после bsp_wdog_init(). Для диагностики: показать
|
||||
* технологу через USB-CDC, что плата восстановилась после зависания.
|
||||
*/
|
||||
bool bsp_wdog_caused_last_reset(void);
|
||||
|
||||
/* true после успешного bsp_wdog_init(). */
|
||||
bool bsp_wdog_is_armed(void);
|
||||
|
||||
/* Сконфигурированный таймаут в секундах (0, если ещё не взведён). */
|
||||
uint32_t bsp_wdog_timeout_s(void);
|
||||
|
||||
#endif /* BSP_WDOG_H */
|
||||
79
bsp/wdog/src/wdog.c
Normal file
79
bsp/wdog/src/wdog.c
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* bsp_wdog — реализация поверх fsl_wdog (WDOG1).
|
||||
*/
|
||||
|
||||
#include "bsp/wdog.h"
|
||||
|
||||
#include "fsl_wdog.h"
|
||||
|
||||
#define BSP_WDOG_BASE WDOG1
|
||||
|
||||
/* Границы поля WCR.WT (8 бит): таймаут = (WT+1) * 0.5 c, максимум 128 c. */
|
||||
#define BSP_WDOG_TIMEOUT_S_MIN 1U
|
||||
#define BSP_WDOG_TIMEOUT_S_MAX 128U
|
||||
|
||||
static bool g_s_armed = false;
|
||||
static bool g_s_last_reset_was_wdog = false;
|
||||
static uint32_t g_s_timeout_s = 0U;
|
||||
|
||||
bsp_status_t bsp_wdog_init(uint32_t timeout_s)
|
||||
{
|
||||
if (g_s_armed)
|
||||
{
|
||||
return BSP_OK; /* WDE — write-once; повторно не взводим */
|
||||
}
|
||||
|
||||
if ((timeout_s < BSP_WDOG_TIMEOUT_S_MIN) || (timeout_s > BSP_WDOG_TIMEOUT_S_MAX))
|
||||
{
|
||||
return BSP_ERR_PARAM;
|
||||
}
|
||||
|
||||
/*
|
||||
* Причина предыдущего сброса: читаем WDOG1->WRSR до настройки. WRSR
|
||||
* read-only, отражает последний сброс (TOUT=WDOG-таймаут, POR=power-on),
|
||||
* стабилен до следующего сброса.
|
||||
*/
|
||||
g_s_last_reset_was_wdog = (BSP_WDOG_BASE->WRSR & WDOG_WRSR_TOUT_MASK) != 0U;
|
||||
|
||||
wdog_config_t cfg;
|
||||
WDOG_GetDefaultConfig(&cfg);
|
||||
|
||||
/* WT = 2*timeout_s - 1 → таймаут = (WT+1)*0.5 c = timeout_s c. */
|
||||
cfg.timeoutValue = (uint16_t) ((timeout_s * 2U) - 1U);
|
||||
|
||||
/*
|
||||
* КРИТИЧНО для рабочего процесса: не сбрасывать плату, когда ядро
|
||||
* остановлено отладчиком (SWD halt) — иначе пошаговая отладка загрузчика
|
||||
* невозможна. enableWait/enableStop оставляем как в дефолте: загрузчик и
|
||||
* приложение в эти режимы не входят, но если войдут — пусть watchdog
|
||||
* продолжает считать (безопаснее по умолчанию).
|
||||
*/
|
||||
cfg.workMode.enableDebug = false;
|
||||
|
||||
cfg.enableWdog = true;
|
||||
WDOG_Init(BSP_WDOG_BASE, &cfg); /* с этого момента WDE взведён навсегда */
|
||||
|
||||
g_s_timeout_s = timeout_s;
|
||||
g_s_armed = true;
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
void bsp_wdog_refresh(void)
|
||||
{
|
||||
WDOG_Refresh(BSP_WDOG_BASE);
|
||||
}
|
||||
|
||||
bool bsp_wdog_caused_last_reset(void)
|
||||
{
|
||||
return g_s_last_reset_was_wdog;
|
||||
}
|
||||
|
||||
bool bsp_wdog_is_armed(void)
|
||||
{
|
||||
return g_s_armed;
|
||||
}
|
||||
|
||||
uint32_t bsp_wdog_timeout_s(void)
|
||||
{
|
||||
return g_s_timeout_s;
|
||||
}
|
||||
|
|
@ -69,6 +69,8 @@ tft_app.
|
|||
как обновляется (та же SD-логика, что и Slot A/Б, или отдельный механизм).
|
||||
- Точный layout `.ld`-скрипта tft_app для двух адресов слотов (Direct-XIP: код обычно не
|
||||
позиционно-независим — вероятно потребуется два варианта линковки под Slot A и Slot Б, либо PIC).
|
||||
**Разобрано** в [UPDATE_FLOW.md](UPDATE_FLOW.md): рекомендован dual-link (два подписанных бинаря на
|
||||
версию, как `test_stub`), с выбором линковки по целевому слоту при SD-обновлении; PIC — не сейчас.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
138
docs/mimxrt1052/UPDATE_FLOW.md
Normal file
138
docs/mimxrt1052/UPDATE_FLOW.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# UPDATE_FLOW.md — производственная заливка и полевое обновление tft_app
|
||||
|
||||
Разбор «что и куда заливать» для связки **bootloader → tft_app** (MCUboot Direct-XIP, два слота A/Б).
|
||||
Дополняет [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md) (адреса) и
|
||||
[../../firmware/bootloader/PLAN.md](../../firmware/bootloader/PLAN.md) (фазы, recovery).
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (главное, что вызывает непонимание)
|
||||
|
||||
**Direct-XIP: образ исполняется прямо из адреса своего слота, код обычно НЕ позиционно-независим →
|
||||
под каждый слот нужна СВОЯ линковка.** Обновление, которое встанет в Slot Б, должно быть слинковано
|
||||
под адрес Slot Б (`0x60240000`). Поэтому:
|
||||
|
||||
> **Релиз одной версии tft_app = ДВА подписанных бинаря** (линковка под A + линковка под Б), из
|
||||
> ОДНОГО исходника, параметризованным линкер-скриптом — ровно как уже устроен `test_stub`
|
||||
> (`--defsym=__slot_base__=…`). Версия (`-v X.Y.Z`) одинаковая в обоих.
|
||||
|
||||
Да, «собрать новую версию со своим линкер-скриптом под слот Б» — так и есть. Но не вручную по одному:
|
||||
это один параметризованный `.ld` и один рецепт сборки, дающий оба бинаря.
|
||||
|
||||
---
|
||||
|
||||
## 1. Почему так — это свойство Direct-XIP, не наша прихоть
|
||||
|
||||
- **XIP** = eXecute In Place: CPU фетчит инструкции напрямую из флеша по адресу слота, образ никуда не
|
||||
копируется (в отличие от swap/scratch-режимов MCUboot, которые мы сознательно НЕ используем).
|
||||
- Абсолютные адреса — vector table, указатели на функции, литеральные пулы, адрес инициализации
|
||||
`.data` — фиксируются на этапе **линковки** под конкретный базовый адрес. Образ, слинкованный под
|
||||
Slot A (`0x60040000`), в Slot Б (`0x60240000`) поедет по чужим адресам и не запустится корректно.
|
||||
- Это прямо зафиксированный «открытый вопрос §4» в [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md):
|
||||
*«код обычно не позиционно-независим — вероятно потребуется два варианта линковки под Slot A и
|
||||
Slot Б, либо PIC»*. `test_stub` (Фаза 2) уже подтвердил two-slot-two-linkage на реальном железе.
|
||||
|
||||
> ⚠️ **Почему на `test_stub` «одинаковый» бинарь как будто работал в обоих слотах.** Заглушка
|
||||
> крошечная (только мигание LED): почти весь её код PC-relative, `.data` минимальна, а vector table
|
||||
> релоцируется и `boot_select` (ставит `VTOR`), и самим образом в старте — поэтому она позиционно
|
||||
> **терпима** по случайности. Реальный tft_app (большой, с `.data`, абсолютными указателями,
|
||||
> шрифтами-C-массивами, framebuffer) терпимым **не будет** — ему нужны обе линковки по-настоящему.
|
||||
|
||||
---
|
||||
|
||||
## 2. Карта слотов (напоминание)
|
||||
|
||||
| Слот | База | Размер | ORIGIN образа (после imgtool-заголовка `0x200`) |
|
||||
|---|---|---|---|
|
||||
| Slot A | `0x60040000` | 2 МБ | `0x60040200` |
|
||||
| Slot Б | `0x60240000` | 2 МБ | `0x60240200` |
|
||||
|
||||
Линкер tft_app: `ORIGIN = __slot_base__ + 0x200` (образец — `cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld`
|
||||
у `test_stub`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Релизные артефакты (на каждую версию)
|
||||
|
||||
Из одного исходника tft_app — **два подписанных бинаря** production-ключом:
|
||||
|
||||
| Файл | Линковка (`__slot_base__`) | Назначение |
|
||||
|---|---|---|
|
||||
| `tft_app_slotA_vX.Y.Z.signed.bin` | `0x60040000` | ставится в Slot A |
|
||||
| `tft_app_slotB_vX.Y.Z.signed.bin` | `0x60240000` | ставится в Slot Б |
|
||||
|
||||
- Оба — **одна версия** `-v X.Y.Z` (version-gate сравнивает версию образа, а не его линковку).
|
||||
- Оба подписаны production-ключом (не тестовым sample-ключом MCUboot; см. HAB SRK-церемонию как
|
||||
аналог для bootloader).
|
||||
- Рецепт — по образцу `just build::build-mcuboot-stub` (собирает оба слота + подписывает imgtool'ом),
|
||||
но с production-ключом и реальным tft_app вместо заглушки.
|
||||
|
||||
---
|
||||
|
||||
## 4. Кейсы
|
||||
|
||||
| # | Ситуация | Куда/что | Как |
|
||||
|---|---|---|---|
|
||||
| 1 | **Начальная production-заливка** | A-линковка → Slot A; Slot Б пуст | SWD / USB ROM (blhost), `just host::flash-production`-путь |
|
||||
| 2 | **Первое полевое обновление** v1→v2 | A активен → target = Slot Б → ставится **Б-линковка** v2 | SD, авто по version-gate; после — A=v1 (fallback), Б=v2 (active) |
|
||||
| 3 | **Следующее обновление** v2→v3 | Б активен → target = Slot A → ставится **A-линковка** v3 | SD; после — A=v3 (active), Б=v2 (fallback) |
|
||||
| 4 | **Даунгрейд** (BTN_1 при старте) | старая версия в inactive + стирание прежнего активного | SD + удержание BTN_1 (см. Фаза 3) |
|
||||
| 5 | **Recovery** (BTN_2 / class C) | оба слота стёрты → чистый борт → A-линковка в Slot A | SD, ослабленный version-gate (см. Фаза 6) |
|
||||
| 6 | **Образ завис после обновления** | авто-откат (class A) или счётчик+фолбэк (class B) | см. Фаза 6, taxonomy A/B |
|
||||
|
||||
**Ключевой инвариант alternation A/Б:** обновление всегда идёт в НЕактивный слот, прежний рабочий слот
|
||||
остаётся нетронутым как fallback (см. `update_policy` — «целевой слот всегда НЕ активный»). Поэтому в
|
||||
поле почти всегда есть куда откатиться, если новая версия окажется плохой.
|
||||
|
||||
---
|
||||
|
||||
## 5. Как обновление выбирает нужную линковку — ТЕКУЩИЙ GAP
|
||||
|
||||
**Сейчас** ([sd_update.c](../../firmware/bootloader/src/sd_update.c)): ищется ОДИН файл
|
||||
`2:/TFT_APP.BIN` и ставится в неактивный слот (`decision.target_slot`). Для позиционно-терпимого
|
||||
`test_stub` это прошло все 5 сценариев Фазы 3 — но для реального tft_app **сломается**: если активен
|
||||
Slot A, обновление идёт в Slot Б, а `TFT_APP.BIN` мог быть слинкован под A → в Slot Б не запустится.
|
||||
|
||||
**Нужное расширение (часть включения реального tft_app):**
|
||||
- SD несёт **оба** линкованных бинаря по соглашению имён, напр. `TFT_APP_A.BIN` / `TFT_APP_B.BIN`.
|
||||
- `sd_update` открывает файл **по целевому слоту**: `TFT_APP_A.BIN`, если target = Slot A, иначе
|
||||
`TFT_APP_B.BIN`.
|
||||
- Оператор кладёт на SD **оба** и не думает, какой слот сейчас активен — bootloader сам берёт нужный
|
||||
под инактивный слот.
|
||||
- Version-gate: bootloader читает версию из выбранного файла (в обоих одна) — сравнение корректно.
|
||||
|
||||
Это простое изменение (одна развилка имени файла по `target_slot`), но его **надо сделать до первого
|
||||
реального полевого обновления tft_app**. Пока стоит `test_stub` — не мешает.
|
||||
|
||||
---
|
||||
|
||||
## 6. Альтернатива — PIC (почему не сейчас)
|
||||
|
||||
Позиционно-независимый образ (один бинарь на оба слота) — теоретически убирает дублирование линковки.
|
||||
Но на bare-metal Cortex-M это **ROPI/RWPI**: флаги компилятора, PI-совместимый startup, `r9` как
|
||||
static base для RW-данных, и **все** библиотеки (FreeRTOS, SDK-драйверы, шрифты) собранные в PI-режиме,
|
||||
плюс runtime-оверхед. Объём работы и риск большие; для Direct-XIP индустрия стандартно выбирает
|
||||
dual-link. Оставляем PIC на «если поддержка двух сборок станет реальной обузой» — не сейчас.
|
||||
|
||||
---
|
||||
|
||||
## 7. Конкретные TODO для включения реального tft_app (сводка)
|
||||
|
||||
1. **Параметризованный `.ld`** tft_app под два адреса слота (образец — `test_stub`
|
||||
`MIMXRT1052xxxxx_mcuboot_slot.ld` + `--defsym=__slot_base__=`).
|
||||
2. **Рецепт сборки+подписи** двух бинарей на релиз (по образцу `build-mcuboot-stub`, но production-ключ).
|
||||
3. **Расширить `sd_update`**: `TFT_APP_<A|B>.BIN` по `decision.target_slot` (§5).
|
||||
4. **Контракт tft_app** (Фаза 6, 6c): обслуживание WDOG (alive-flag), отложенный `boot_set_confirmed()`,
|
||||
`bsp_boot_health_mark()`.
|
||||
5. **Решить**: production заливать только Slot A или A+Б? Достаточно A — первое обновление наполнит Б
|
||||
и даст fallback *другой* версии (A+Б одной версией от class-B не спасает — тот же баг в обоих).
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md) — адреса слотов, обоснование размеров.
|
||||
- [../../firmware/bootloader/PLAN.md](../../firmware/bootloader/PLAN.md) — фазы; Фаза 6 (recovery,
|
||||
taxonomy зависаний), Фаза 5 (HAB/production/service-tui).
|
||||
- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись самого bootloader (HAB), не путать с imgtool-подписью образов
|
||||
tft_app.
|
||||
|
|
@ -52,7 +52,7 @@ target_compile_definitions(
|
|||
target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick
|
||||
bsp_usb_cdc bsp_qspi_flash
|
||||
bsp_boot_xip_no_dcd bsp_button
|
||||
bootloader_fatfs)
|
||||
bsp_wdog bootloader_fatfs)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом
|
||||
|
|
|
|||
|
|
@ -150,8 +150,11 @@ IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U);
|
|||
> **РАЗРЕШЕНО 2026-07-10 (раунд 3).** Основной баг детекта закрыт PRSSTAT-чтением (см. секцию
|
||||
> «Обновление 2026-07-10 (раунд 3)» выше). Пункты 1–2 ниже сохранены как история; пункт 3 —
|
||||
> опровергнут; пункты 4–5 (devcontainer, watchdog) остаются актуальными как отдельные задачи.
|
||||
> Плюс два новых нереализованных пункта-рекомендации из раунда 3: (6) консолидация детекта на
|
||||
> единый механизм PRSSTAT (убрать латентную хрупкость re-scan), (7) ранний сэмпл кнопки даунгрейда.
|
||||
> Рекомендации (6) консолидация детекта на единый PRSSTAT и (7) ранний сэмпл кнопки даунгрейда —
|
||||
> **✅ реализованы в раунде 4** (см. секции «✅ Латентная хрупкость re-scan — УСТРАНЕНА» и «Тайминг
|
||||
> удержания кнопки» выше); аппаратно ещё не проверены. Пункт (5) аппаратный watchdog —
|
||||
> **✅ реализован** (`bsp/wdog`, см. PLAN.md «Реализовано: аппаратный watchdog» и чек-лист в
|
||||
> test_stub/HARDWARE_VERIFICATION_PHASE3.md §9); аппаратно ещё не проверен. Открытых пунктов нет.
|
||||
|
||||
1. ✅ **Применено 2026-07-10 (раунд 1).** Откатить pinmux CD на `GPIO2_IO28` (без `USDHC1_CD_B`) —
|
||||
самый доверенный референс (`TFT_BOOTLOADER`) использует именно так. **Аппаратно проверено —
|
||||
|
|
@ -447,26 +450,28 @@ bool bsp_sd_is_inserted(void)
|
|||
- `sd_power_control` возвращён в `static` (публичное имя `BOARD_SDCardPowerControl` из раунда 2
|
||||
больше не нужно — side-effect, ради которого оно вводилось, заменён PRSSTAT-чтением).
|
||||
|
||||
### ⚠️ Латентная хрупкость re-scan (не мешает 5 сценариям, но стоит знать)
|
||||
### ✅ Латентная хрупкость re-scan — УСТРАНЕНА (раунд 4, item 1)
|
||||
|
||||
Рабочая конфигурация опирается на то, что пин на `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`). Реальный, но узкий крайний случай.
|
||||
**Была** (до раунда 4): рабочая конфигурация опиралась на то, что пин на `USDHC1_CD_B` в момент
|
||||
гейта, но `BOARD_SD_Config()` перемапливал его на `GPIO2_IO28` при первом же `bsp_sd_init()` и
|
||||
обратно НЕ возвращал. Если `run_update()` был вызван (карта была на гейте), но завершался без прыжка
|
||||
(например, на карте нет `TFT_APP.BIN`), то на СЛЕДУЮЩЕМ периодическом скане `bsp_sd_is_inserted()`
|
||||
читал PRSSTAT с пином уже на `GPIO2_IO28` → ложный ноль → карта «исчезала» до перезагрузки.
|
||||
|
||||
**Рекомендованная консолидация (не реализована — требует повторной проверки на железе, а рабочее
|
||||
состояние трогать без запроса не стал):** свести детект к ЕДИНОМУ механизму — 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. Оба варианта убирают двойную
|
||||
маршрутизацию и латентную хрупкость. Оба меняют путь, проверенный сейчас только в текущей
|
||||
двухмеханизменной форме, — отсюда обязательная повторная аппаратная проверка перед принятием.
|
||||
**Реализовано (раунд 4, вариант B — минимальный):** детект сведён к ЕДИНОМУ механизму, PRSSTAT
|
||||
везде. Callback `sd_card_detect_gpio` → `sd_card_detect_prsstat` (читает `USDHC_GetPresentStatusFlags
|
||||
& kUSDHC_CardInsertedFlag`); remux на `GPIO2_IO28` из `BOARD_SD_Config()` убран — пин остаётся на
|
||||
`USDHC1_CD_B` постоянно (там же явно переустанавливается); мёртвый pad-конфиг GPIO2_IO28 из
|
||||
`sd_pin_config()` удалён. `s_cd.type` оставлен `kSD_DetectCardByGpioCD` — это не «GPIO» в смысле
|
||||
чтения, а «детект через callback» (polling); альтернатива `kSD_DetectCardByHostCD` дополнительно
|
||||
взвела бы USDHC card-detect ПРЕРЫВАНИЯ (тот же PRSSTAT-бит, но лишний источник IRQ перед прыжком —
|
||||
см. `SDMMCHOST_CardDetectInit` в `sdk/.../usdhc/non_blocking/fsl_sdmmc_host.c`), поэтому выбран
|
||||
вариант B. Теперь и гейт, и внутренний `SD_PollingCardInsert` читают один регистр на одной
|
||||
маршрутизации пина.
|
||||
|
||||
**Требует аппаратной ре-проверки:** все 5 сценариев, плюс явно кейс, который раньше был сломан —
|
||||
«карта вставлена, но без `TFT_APP.BIN`; ждём; кладём файл на карту БЕЗ извлечения/перезагрузки —
|
||||
пере-скан должен его подхватить».
|
||||
|
||||
### Тайминг удержания кнопки даунгрейда (симптом/вопрос пользователя)
|
||||
|
||||
|
|
@ -490,15 +495,19 @@ bool bsp_sd_is_inserted(void)
|
|||
(кода на отпускание нет); это совпадение: решение уже защёлкнулось на сэмпле (пока держал), а
|
||||
видимый результат (стирание+копирование+прыжок в старший образ) проявляется как раз к ~15 c.
|
||||
|
||||
**Практическая инструкция технологу** (в HARDWARE_VERIFICATION_PHASE3.md): держать `BSP_BUTTON_1` с
|
||||
момента подачи питания и не отпускать, пока по CDC не придёт `status: installing` (или пока частота
|
||||
мигания `LED_APP` не сменится на «старший» образ). Ориентир ~15 c — держать с запасом.
|
||||
**✅ Реализовано (раунд 4, item 2):** кнопка теперь сэмплится РАНО и защёлкивается. `main.c` читает
|
||||
`bsp_button_read(BSP_BUTTON_1)` один раз сразу после `bsp_button_init()` (до `bsp_qspi_init()`/
|
||||
SD-логики) в `const bool downgrade_held` и передаёт это значение в `sd_update_check(downgrade_held)`
|
||||
на обеих точках вызова (первая попытка + пере-сканы цикла). `run_update()` больше не читает кнопку
|
||||
сам (`bsp/button.h` из `sd_update.c` убран) — `update_policy_decide()` получает уже защёлкнутое
|
||||
значение. Жест «удержание при включении» теперь ловится в детерминированный ранний момент, а не
|
||||
через секунды слепого окна.
|
||||
|
||||
**Рекомендация по коду (не реализовано — поведение бы изменилось, требует запроса и ре-теста):**
|
||||
сэмплить кнопку РАНО — при `bsp_button_init()`/сразу в `board_hw_init()`-фазе, до медленной
|
||||
SD-инициализации и двойной крипто-валидации — и защёлкивать результат, вместо чтения в глубине
|
||||
`run_update()`. Тогда «удержание при старте» стало бы предсказуемым (нажал в момент включения —
|
||||
сработало), без многосекундного слепого окна. Сейчас это самый user-hostile момент SD-пути.
|
||||
**Новая инструкция технологу** (обновлено в HARDWARE_VERIFICATION_PHASE3.md): достаточно держать
|
||||
`BSP_BUTTON_1` нажатой **в момент подачи питания** (до/во время включения) — состояние считывается в
|
||||
первые миллисекунды после `board_hw_init()`, ещё до обращения к SD. Долгое «15 c с запасом» больше
|
||||
не требуется. **Требует аппаратной ре-проверки** сценария 3 (даунгрейд): удержать кнопку при
|
||||
включении → даунгрейд срабатывает; не удерживать → штатная загрузка более нового слота.
|
||||
|
||||
## Побочная находка (не блокирует, но стоит знать)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@
|
|||
| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) |
|
||||
| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже |
|
||||
| 2 — bootutil (Direct-XIP) | ✅ завершена | host-тесты 5/5, аппаратная верификация — все 5 сценариев пройдены на реальной плате (детали и 3 найденных/исправленных бага — [DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md)) |
|
||||
| 3 — SD-путь установки | ✅ аппаратно верифицирована (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-паттерны | не начата | |
|
||||
| 3 — SD-путь установки | ✅ верифицирована (2026-07-10); раунд 4 + watchdog ждут ре-проверки | все 5 сценариев пройдены на плате; баг card-detect закрыт чтением USDHC PRES_STATE.CINST (раунд 3). Раунд 4 (консолидация детекта на единый PRSSTAT + ранний сэмпл кнопки) и **аппаратный watchdog** (`bsp/wdog`, см. ниже) реализованы, сборка зелёная, **аппаратно ещё не проверены**. См. [DEBUG_LOG_PHASE3_SD.md](DEBUG_LOG_PHASE3_SD.md), чек-лист — [test_stub/HARDWARE_VERIFICATION_PHASE3.md](test_stub/HARDWARE_VERIFICATION_PHASE3.md) |
|
||||
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | рекомендуется ПОСЛЕ Фазы 6 (см. ниже) |
|
||||
| 5 — HAB Release + service-tui | не начата | |
|
||||
| 6 — Устойчивость и восстановление (recovery) | спроектирована, код не начат | **рекомендованный следующий шаг** (раньше 4/5): закрывает зависание уже установленного образа и даёт ручной аварийный вход. Дизайн зафиксирован в обсуждении — см. раздел «Фаза 6» ниже |
|
||||
|
||||
## Контекст
|
||||
|
||||
|
|
@ -29,7 +30,10 @@
|
|||
- **Схема обновления**: MCUboot **Direct-XIP**, два слота (A/Б) с полностью валидными образами каждый,
|
||||
без swap/scratch. Единственный полевой канал обновления — microSD. USB как канал заливки *образа*
|
||||
сознательно не делаем (SDP/blhost на производстве — это отдельный, не зависящий от кода bootloader,
|
||||
канал через NXP BootROM).
|
||||
канал через NXP BootROM). **Direct-XIP → образ линкуется под адрес своего слота**: релиз tft_app =
|
||||
два подписанных бинаря (линковка под A и под Б) на версию. Полный разбор производственной заливки и
|
||||
полевого обновления, включая текущий gap в `sd_update` (один `TFT_APP.BIN`) —
|
||||
[docs/mimxrt1052/UPDATE_FLOW.md](../../docs/mimxrt1052/UPDATE_FLOW.md).
|
||||
- **Верификация образов tft_app** — через **bootutil** (MCUboot), не самодельный верификатор
|
||||
(см. исследование ниже). Подпись — `imgtool` (не HAB; HAB — только для самого bootloader).
|
||||
- **Обратная связь** — USB CDC ACM с тем же JSON-lines протоколом (`ping`/`pong`, `get_version`), что уже
|
||||
|
|
@ -554,7 +558,64 @@ ARM-таргет — без изменений), иначе (host, любая а
|
|||
кросс-компиляцией под `-target x86_64-unknown-linux-gnu` (0 ошибок после фикса, была именно эта ошибка
|
||||
до него).
|
||||
|
||||
### Запланировано (обязательно к реализации): аппаратный watchdog
|
||||
### Найден и исправлен баг чек-листа (не бутлоадера): сценарий 1 не запускал установленный образ
|
||||
|
||||
При аппаратной ре-верификации раунда 4 + watchdog (2026-07-13) сценарий 1 чек-листа
|
||||
(HARDWARE_VERIFICATION_PHASE3.md) перестал проходить: консоль показывала `waiting_for_sd` →
|
||||
`installing`, прыжок доходил до `p_vt->reset()` (подтверждено в отладчике), но установленный образ
|
||||
не запускался — ни сразу, ни после power cycle; erase Slot A возвращал bootloader в обычный
|
||||
ping/pong-цикл.
|
||||
|
||||
**Причина — не регресс кода.** Стабы `test_stub` не PIC: `stub_a_*` линкован под адрес Slot A
|
||||
(`0x60040000`), `stub_b_*` — под Slot Б (`0x60240000`), абсолютные адреса зашиты в бинарь при
|
||||
линковке (тот же открытый вопрос, что и в Фазе 2 — "разный адрес — намеренно... открытый вопрос из
|
||||
BOOTLOADER_FLASH_MAP.md §4"). Чек-лист сценария 1 предписывал класть на SD `stub_b_v2_confirmed.bin`,
|
||||
а целевой слот на чистой плате (нет активного слота) по `update_policy_decide()` всегда Slot A —
|
||||
несовпадение линковки и адреса установки. `slot_version_get()` (крипто-гейт) это не ловит: подпись
|
||||
валидна для собственного содержимого файла независимо от того, где физически лежит слот; `boot_go()`
|
||||
корректно выбирает и прыгает — исполняется код, рассчитанный на чужой адрес, без какой-либо ошибки на
|
||||
CDC. Сценарии 2/3 эту грань не задевали случайно: там кандидат на понижение — всегда `stub_a`, а
|
||||
целевой слот в этих сценариях (Slot Б уже активен) тоже всегда А — адрес и линковка совпадают
|
||||
по совпадению постановки, не по общему принципу.
|
||||
|
||||
Тот же класс несовпадения — уже задокументированный в проде gap одного `TFT_APP.BIN`
|
||||
([docs/mimxrt1052/UPDATE_FLOW.md](../../docs/mimxrt1052/UPDATE_FLOW.md)): production-релиз собирается
|
||||
как два линкованных под разные слоты бинаря на версию, а текущий `sd_update` полагается на единый
|
||||
файл. Раунд 4 просто нащупал эту границу руками на стенде, а не создал новую.
|
||||
|
||||
**Исправлено** — только чек-лист (не код): сценарий 1 теперь кладёт на SD `stub_a_v1_confirmed.bin`
|
||||
(совпадает с гарантированным целевым слотом А), ожидаемая частота `LED_APP` — ~1/сек (v1), не ~2/сек.
|
||||
`HARDWARE_VERIFICATION_PHASE3.md` §5 дополнен явным предупреждением о требовании
|
||||
линковка-под-слот-установки для любого файла на SD.
|
||||
|
||||
### ✅ Реализовано (2026-07-10): аппаратный watchdog
|
||||
|
||||
**Статус:** реализован, собран (ARM + host зелёные), **аппаратно ещё не проверен** — провокацию
|
||||
реального зависания и подтверждение восстановления см. в
|
||||
[test_stub/HARDWARE_VERIFICATION_PHASE3.md](test_stub/HARDWARE_VERIFICATION_PHASE3.md), раздел
|
||||
«Watchdog». Реализация против плана ниже:
|
||||
|
||||
- **Модуль `bsp/wdog`** (WDOG1): `bsp_wdog_init(timeout_s)` / `bsp_wdog_refresh()` /
|
||||
`bsp_wdog_caused_last_reset()` / `bsp_wdog_is_armed()` / `bsp_wdog_timeout_s()`. Таймаут **10 c**
|
||||
(не 5-8 — самый долгий накормленный участок оказался блочным стиранием 2 МБ ~4.8 c плюс запас;
|
||||
refresh теперь и внутри цикла стирания). `enableDebug=false` — под SWD-отладкой watchdog
|
||||
приостановлен (иначе пошаговая отладка невозможна).
|
||||
- **Взвод** — в `main.c` сразу после `board_hw_init()`.
|
||||
- **Кормление** — верх главного цикла; поблочно/посекторно внутри `flash_area_erase()`
|
||||
(`mcuboot_port/flash_map_backend.c`); на каждый чанк в `erase_and_copy_candidate()`
|
||||
(`sd_update.c`); один раз прямо перед `boot_select_and_jump()`. **Не** кормим перед
|
||||
`f_mount`/`SD_Init` — защита именно от них сохранена.
|
||||
- **⚠️ Контракт handoff:** `WDE` (enable) — **write-once**, watchdog нельзя выключить и он переживает
|
||||
прыжок. Поэтому **каждый образ после прыжка обязан кормить watchdog** (`tft_app` в проде;
|
||||
`test_stub` уже кормит — иначе reset-loop и регрессия сценариев Фаз 2/3). Программный watchdog тут
|
||||
невозможен: зависания — внутри вендоренного `do{}while(true)`, управление к нам не возвращается.
|
||||
- **Статус по USB-CDC:** событие `{"type":"wdog","armed":..,"timeout_s":..,"recovered":..}` —
|
||||
эмитится на старте, если предыдущий сброс был по watchdog, и по команде `{"type":"cmd","cmd":"wdog"}`.
|
||||
`recovered:true` = плата восстановилась после зависания.
|
||||
- Ресет по watchdog безопасен по построению (ни один слот не стирается до подтверждения валидности
|
||||
нового образа — `erase_previous_active`): в худшем случае откат к «начать сессию заново».
|
||||
|
||||
<details><summary>Исходный план (до реализации)</summary>
|
||||
|
||||
Найдено при аппаратной верификации (см. [DEBUG_LOG_PHASE3_SD.md](DEBUG_LOG_PHASE3_SD.md)): у
|
||||
`SD_PollingCardInsert()` (SDK, вызывается изнутри `SD_Init()`, который вызывается изнутри
|
||||
|
|
@ -588,6 +649,8 @@ while (true)` без выхода по времени, если callback `cardDe
|
|||
следующий шаг после того, как будет закрыт текущий SD/card-detect баг (иначе watchdog будет
|
||||
маскировать/сбрасывать симптом на каждом цикле, не давая найти первопричину).
|
||||
|
||||
</details>
|
||||
|
||||
**Верификация (первая часть Фазы 3, требующая реального железа — ещё не проведена)**:
|
||||
1. Чистая плата (только bootloader, оба слота пустые) + SD с валидным подписанным образом →
|
||||
автоустановка, переход к загрузке (проверить по CDC-статусам и LED).
|
||||
|
|
@ -652,6 +715,150 @@ while (true)` без выхода по времени, если callback `cardDe
|
|||
|
||||
---
|
||||
|
||||
## Фаза 6 — Устойчивость и восстановление (recovery)
|
||||
|
||||
**Рекомендованный порядок исполнения: раньше Фаз 4/5** (см. статус-таблицу). Ядро (boot_select,
|
||||
sd_update, update_policy, watchdog) свежее в контексте; закрывает уже случившуюся на практике боль —
|
||||
окирпичивание платы зависшим образом. Полностью реализуема и железно проверяема уже сейчас через
|
||||
`test_stub` (сторона tft_app — контракт, документируется в 6c под будущий план tft_app).
|
||||
|
||||
**Цель**: гарантировать, что ни зависший в рантайме образ, ни кривая установка не превращают полевую
|
||||
плату в кирпич, и дать оператору ручной аварийный вход. Аппаратный watchdog (Фаза 3) уже ловит
|
||||
зависание — Фаза 6 надстраивает над ним **логику восстановления**: что делать ПОСЛЕ того, как
|
||||
watchdog сбросил плату.
|
||||
|
||||
### Таксономия отказов (что чем закрывается)
|
||||
|
||||
| # | Сценарий | Механизм | Статус |
|
||||
|---|---|---|---|
|
||||
| A | Новый образ завис ДО подтверждения себя | watchdog-сброс + штатный revert MCUboot (`boot_select_or_erase`, [loader.c](../../sdk/middleware/mcuboot_opensource/boot/bootutil/src/loader.c) — стирает `copy_done==SET && image_ok!=SET`) → откат на прежний слот | **авто, кода в загрузчике не требует** (нужен лишь отложенный confirm в tft_app, 6c) |
|
||||
| B | **Подтверждённый** образ завис в рантайме | счётчик watchdog-сбросов подряд + фолбэк (6a) | новый код 6a |
|
||||
| C | Откатываться некуда (единственный/оба слота зависают) | recovery-режим: подавить загрузку, ждать SD (6b) | новый код 6b |
|
||||
| D | Оператор хочет чистый старт вручную | recovery-режим по BTN_2 (6b) | новый код 6b |
|
||||
|
||||
Ключ к классу A — **отложенное подтверждение**: если tft_app зовёт `boot_set_confirmed()` только
|
||||
доказав здоровье (проработал T c, self-check пройден), любой битый новый образ, зависший в этом
|
||||
окне, остаётся `image_ok!=SET` и откатывается MCUboot'ом автоматически. Самый частый класс закрыт
|
||||
без единой строки в загрузчике.
|
||||
|
||||
### Решения, принятые в обсуждении Фазы 6 (не пересматриваются)
|
||||
|
||||
- **Отложенное подтверждение** (`boot_set_confirmed()` после доказанного здоровья) — контракт
|
||||
tft_app. Переводит класс A в авто-откат.
|
||||
- **WDOG переживает прыжок** (Фаза 3, `WDE` write-once) + **alive-flag идиом** в tft_app: главный
|
||||
цикл ставит `alive=true`, таймерный ISR раз в ~1 c кормит watchdog только если `alive` был
|
||||
выставлен (иначе главный поток завис → watchdog сработает). Одна строка в цикле, ловит зависания
|
||||
главного потока, а не только полный локап. Таймаута 10 c хватает любому нормальному циклу с
|
||||
запасом ×1000; отдельного внимания требуют только единичные блокирующие операции > таймаута.
|
||||
- **Класс B — счётчик watchdog-сбросов подряд**, хранится в **retained-регистре `SRC_GPR`**
|
||||
(переживает тёплый/watchdog-сброс, обнуляется на POR). Порог **настраиваемый `#define`, дефолт 3**.
|
||||
- **Фолбэк класса B — Вариант A (стереть зависший слот)**: при достижении порога, ЕСЛИ второй слот
|
||||
валиден (`slot_version_get`) → стереть активный (зависший) слот → `boot_go()` сам выберет второй →
|
||||
прыжок. Если валидного второго слота НЕТ → **не стирать единственный образ** (иначе ноль рабочих
|
||||
слотов) → recovery-режим.
|
||||
- **Бан зависшего слота — per-session (Вариант 1)**: отдельного флеш-маркера НЕТ; «забанен» = «счётчик
|
||||
≥ порога». На POR счётчик обнуляется → на холодном старте зависший слот пробуется снова (даёт шанс,
|
||||
если завис был транзиентным). Детерминированный завис одинокого слота → оператор жмёт **BTN_2**
|
||||
(немедленный recovery), не дожидаясь порог×10 c. Флеш-маркер сознательно не делаем — если позже в
|
||||
поле детерминированные зависы одинокого слота окажутся частыми, добавим как отдельный кусок (место
|
||||
в дизайне оставлено).
|
||||
- **Recovery-режим — единое состояние, два триггера**: авто (класс C — порог без валидного фолбэка)
|
||||
и ручной (BTN_2 удержан при старте). Поведение: **прыжок подавлен** (это само рвёт loop), отдельный
|
||||
LED-паттерн, CDC `recovery_mode`, ждём SD, **version-gate ослаблен** (принимаем любую подписанную
|
||||
версию без кнопки — подпись проверяется всегда).
|
||||
- **Стирание в recovery — отложенное** (на момент установки, не на входе): вход в recovery ничего не
|
||||
стирает → случайное удержание BTN_2 безопасно (без SD-образа ничего не теряется); при найденном
|
||||
валидном подписанном образе — стереть оба слота и поставить свежий в Slot A (sd_update и так стирает
|
||||
целевой слот). «Чистый борт» достигается ровно тогда, когда есть чем заменить.
|
||||
- **После recovery-установки — авто-прыжок** в свежепровалидированный образ (если и он зависнет —
|
||||
отработает обычный класс A/B).
|
||||
- **Приоритет триггеров при старте**: BTN_2 (recovery) проверяется ПЕРВЫМ — если удержан, в обычный
|
||||
boot-путь не идём вообще. Затем BTN_1 (даунгрейд) в обычном пути. Recovery «главнее».
|
||||
- **Правила обнуления счётчика**: (1) POR; (2) успешная установка нового образа (SD/recovery — свежему
|
||||
образу полный бюджет попыток); (3) фолбэк-стирание (ситуация изменилась); (4) health-mark работающим
|
||||
образом.
|
||||
|
||||
### Честная граница (что НЕ восстанавливается автоматически — задокументировать)
|
||||
|
||||
Если образ стабильно работает, **обнуляет счётчик** (health-mark), и лишь ПОТОМ ловит редкий баг
|
||||
(конкретный файл на SD, конкретное CAN-сообщение) — счётчик каждый раз обнуляется до зависания,
|
||||
автопорог не копится, loop автоматически не ловится. Это фундаментально (по таймеру не отличить
|
||||
«здоров» от «здоров, но потом словил редкое»). Для такого класса: плата видимо циклится (watchdog +
|
||||
recovery-LED/CDC это показывают), лечится SD-фиксом или BTN_2. Watchdog как минимум не даёт «намертво»
|
||||
зависнуть. Принимаем и пишем в доке, не делаем вид, что решаем всё.
|
||||
|
||||
### Разбивка
|
||||
|
||||
**6a — Класс B: счётчик watchdog-сбросов + фолбэк A** (сторона загрузчика, host-тестируема)
|
||||
- Расширить `bsp/wdog` (или новый `bsp/boot_state`): счётчик в `SRC_GPR[n]` (выбрать свободный —
|
||||
`GPR1/2` заняты ROM под warm-boot entry/arg, `GPR10` под redundant/secondary boot; кандидаты
|
||||
`GPR5..8`, **проверить по RM/на железе**, что переживает watchdog-сброс и что мы корректно обнуляем
|
||||
на POR через `SRC->SRSR`). API: `bsp_boot_attempt_count()`, `bsp_boot_attempt_inc()`,
|
||||
`bsp_boot_attempt_reset()`, `bsp_boot_health_mark()` (= reset, зовётся приложением).
|
||||
- **Чистая host-тестируемая функция решения** (по образцу `update_policy_decide`), напр.
|
||||
`recovery_decide(is_por, attempt_count, threshold, slot_a_state, slot_b_state, btn2_held)` →
|
||||
`{NORMAL_BOOT | ERASE_ACTIVE_THEN_BOOT_OTHER(active_id) | RECOVERY}`. Активный слот вычисляется из
|
||||
`slot_version_get()` обоих слотов (тот же приём, что уже в `update_policy`). Тесты в
|
||||
`tests/host/recovery/` — все ветки таблицы таксономии.
|
||||
- `main.c`: до `boot_select_and_jump()` — прочитать причину сброса (POR→reset счётчика), применить
|
||||
`recovery_decide`. Ветки: NORMAL → `attempt_inc()` + прыжок; ERASE_OTHER → стереть активный слот
|
||||
(`flash_area_erase`) + reset счётчика + прыжок (boot_go выберет оставшийся); RECOVERY → в состояние
|
||||
6b. **Инкремент — перед прыжком** (если образ зависнет, следующий старт увидит инкремент; здоровый
|
||||
образ обнулит через health-mark).
|
||||
- Расширить CDC `wdog`-статус: `{"type":"wdog",...,"reset_count":N,"threshold":M}` — диагностика в
|
||||
поле («сбрасывалась N из M»).
|
||||
|
||||
**6b — Recovery-режим + ручной BTN_2** (тонкий аппаратный оркестратор)
|
||||
- Единое состояние recovery: подавить `boot_select_and_jump()`, отдельный LED-паттерн (узнаваемый
|
||||
«шиммер»/SOS обоими LED, явно отличный от heartbeat 50/450 и app 500/250 — сведётся в словарь
|
||||
Фазы 4), CDC `status: recovery_mode`.
|
||||
- BTN_2 (`BSP_BUTTON_2`, свободен): ранний сэмпл + защёлкивание (как BTN_1 в Фазе 3, item 2),
|
||||
приоритет над обычным boot-путём и над BTN_1.
|
||||
- SD-скан с **ослабленным version-gate** (в `update_policy` — флаг `recovery`/`force_install`:
|
||||
принять любую подписанную версию без кнопки; подпись/крипто-гейт остаются). Отложенное стирание:
|
||||
на валидном образе — стереть оба слота, поставить в Slot A, авто-прыжок.
|
||||
- Кормление watchdog в recovery — как везде (верх цикла + per-block при стирании; 2×2 МБ ~10 c
|
||||
накрыты поблочным refresh).
|
||||
|
||||
**6c — Контракт tft_app** (документация под будущий план tft_app, кода в загрузчике нет)
|
||||
- tft_app ОБЯЗАН: (1) обслуживать watchdog (alive-flag идиом); (2) `boot_set_confirmed()` —
|
||||
отложенно, после доказанного здоровья; (3) `bsp_boot_health_mark()` — дойдя до устойчивого
|
||||
состояния (можно раньше confirm). (2) и (3) можно разнести: health-mark пораньше (дошёл до главного
|
||||
цикла), confirm позже (полный self-test).
|
||||
- Опционально: tft_app может переконфигурировать таймаут WDOG (`WCR.WT` переписываем, `WDE` — нет)
|
||||
под свои длинные операции.
|
||||
|
||||
### Стенд (доработки `test_stub`)
|
||||
|
||||
Опции компиляции/поведения заглушки, чтобы прогнать все классы на железе:
|
||||
- confirm: сразу / отложенно (через N c) / никогда;
|
||||
- зависнуть: никогда / до health-mark / после health-mark / после confirm;
|
||||
- обслуживать watchdog: да / нет.
|
||||
|
||||
### Верификация (реальное железо — чек-лист, детали в отдельном HARDWARE_VERIFICATION при реализации)
|
||||
|
||||
1. **Класс A** — свежий образ, `confirm=никогда`, `hang=после jump`: watchdog-сброс → MCUboot
|
||||
стирает → откат на прежний слот (или wait_for_sd, если прежнего нет).
|
||||
2. **Класс B, есть фолбэк** — `confirm=сразу`, `hang=после confirm`, второй слот валиден: после
|
||||
порога — зависший слот стёрт, загрузка второго; CDC `reset_count` растёт до порога.
|
||||
3. **Класс B, нет фолбэка** — то же, но второй слот пуст: после порога — recovery-режим (LED-шиммер,
|
||||
CDC `recovery_mode`), плата не циклится дальше.
|
||||
4. **Recovery по BTN_2** — валидный образ в слоте, удержать BTN_2 при старте → recovery-режим без
|
||||
загрузки; вставить SD с образом → стирание обоих + установка + авто-прыжок.
|
||||
5. **Транзиент (per-session)** — после класса B без фолбэка (recovery) сделать power cycle БЕЗ BTN_2
|
||||
→ счётчик обнулён (POR), слот пробуется снова (подтверждает per-session-семантику).
|
||||
6. **Не мешает норме** — прогнать сценарии Фазы 3 как есть: без ложных recovery/сбросов.
|
||||
|
||||
### Открытые вопросы к реализации
|
||||
|
||||
- `SRC_GPR[n]`: индекс (свободный от ROM) + подтвердить переживание watchdog-сброса и обнуление на
|
||||
POR (RM + железо).
|
||||
- `SRC->SRSR` — корректно читать/чистить (w1c) причину сброса, различать POR vs WDOG (биты
|
||||
`SRC_SRSR_WDOG_RST_B` / POR).
|
||||
- Word choice recovery-LED — согласовать со словарём Фазы 4, чтобы не переделывать.
|
||||
|
||||
---
|
||||
|
||||
## Ключевые файлы для переиспользования (не изобретать заново)
|
||||
|
||||
| Что нужно | Где смотреть образец |
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
#include "bsp/qspi_flash.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "flash_map.h"
|
||||
#include "sysflash/sysflash.h"
|
||||
|
||||
|
|
@ -146,6 +147,11 @@ int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len)
|
|||
uint32_t block_addr = area->fa_off;
|
||||
for (; len > 0U; len -= BSP_QSPI_BLOCK_64K_SIZE)
|
||||
{
|
||||
/* Кормим watchdog поблочно: стирание 2 МБ ~4.8 c — это реальный
|
||||
* прогресс, но один блочный вызов не должен упереться в таймаут.
|
||||
* Зависание самого стирания флеша всё равно ловится: refresh — по
|
||||
* ЗАВЕРШЕНИИ блока, а не перед ним. */
|
||||
bsp_wdog_refresh();
|
||||
if (bsp_qspi_erase_block_64k(block_addr) != BSP_OK)
|
||||
{
|
||||
return -1;
|
||||
|
|
@ -158,6 +164,7 @@ int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len)
|
|||
uint32_t addr = area->fa_off + off;
|
||||
for (; len > 0U; len -= BSP_QSPI_SECTOR_SIZE)
|
||||
{
|
||||
bsp_wdog_refresh(); /* см. выше — посекторный путь тоже длинный */
|
||||
if (bsp_qspi_erase_sector(addr) != BSP_OK)
|
||||
{
|
||||
return -1;
|
||||
|
|
|
|||
|
|
@ -135,6 +135,12 @@ static void handle_cmd(const char *p_line)
|
|||
return;
|
||||
}
|
||||
|
||||
if (strcmp(cmd_name, "wdog") == 0)
|
||||
{
|
||||
protocol_send_wdog_status();
|
||||
return;
|
||||
}
|
||||
|
||||
protocol_send_error("UNKNOWN_CMD");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,25 @@
|
|||
*
|
||||
* Последовательность старта:
|
||||
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
||||
* 2. bsp_led_init() — оба LED выключены
|
||||
* 3. bsp_tick_init() — SysTick 1 мс
|
||||
* 4. bsp_button_init() — для проверки удержания BSP_BUTTON_1
|
||||
* 5. bsp_qspi_init() — доступ к Slot A/Б
|
||||
* 6. bsp_usb_cdc_init() — не блокирует, см. выше
|
||||
* 7. sd_update_check() — no-op быстро, если SD не вставлена
|
||||
* 8. boot_select_and_jump() — при успехе не возвращается
|
||||
* 9. Цикл ожидания — CDC ping/pong + LED + периодический
|
||||
* пере-скан SD (шаги 7-8 повторно)
|
||||
* 2. bsp_wdog_init() — аппаратный watchdog как можно раньше (см. ниже)
|
||||
* 3. bsp_led_init() — оба LED выключены
|
||||
* 4. bsp_tick_init() — SysTick 1 мс
|
||||
* 5. bsp_button_init() — для проверки удержания BSP_BUTTON_1
|
||||
* 6. bsp_qspi_init() — доступ к Slot A/Б
|
||||
* 7. bsp_usb_cdc_init() — не блокирует, см. выше
|
||||
* 8. sd_update_check() — no-op быстро, если SD не вставлена
|
||||
* 9. boot_select_and_jump() — при успехе не возвращается
|
||||
* 10. Цикл ожидания — CDC ping/pong + LED + периодический
|
||||
* пере-скан SD (шаги 8-9 повторно)
|
||||
*
|
||||
* Watchdog (bsp_wdog): единственная защита от бесконечных зависаний в
|
||||
* блокирующих вызовах SDMMC-стека, не возвращающих управление в наш код
|
||||
* (SD_PollingCardInsert / OSA_SemaphoreWait — см. DEBUG_LOG_PHASE3_SD.md).
|
||||
* ⚠️ WDE — write-once: после взвода watchdog не выключить, он переживает прыжок,
|
||||
* поэтому целевой образ (tft_app / test_stub) ОБЯЗАН его кормить (см. bsp/wdog).
|
||||
* Кормим только в точках реального прогресса (верх цикла, циклы стирания/
|
||||
* копирования, перед прыжком) — НЕ перед f_mount/SD_Init, иначе watchdog
|
||||
* перестаёт защищать именно от них.
|
||||
*
|
||||
* Bootloader без SDRAM (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) — DCD
|
||||
* не используется (bsp_boot_xip_no_dcd).
|
||||
|
|
@ -37,6 +47,7 @@
|
|||
#include "bsp/qspi_flash.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "cli.h"
|
||||
#include "protocol.h"
|
||||
#include "sd_update.h"
|
||||
|
|
@ -50,19 +61,36 @@ int main(void)
|
|||
const uint32_t SD_RETRY_PERIOD_MS = 1500U;
|
||||
const uint32_t HEARTBEAT_ON_MS = 50U;
|
||||
const uint32_t HEARTBEAT_PERIOD_MS = 500U;
|
||||
/* Таймаут WDOG. С запасом над самым долгим НАКОРМЛЕННЫМ участком: между
|
||||
* соседними refresh худший легитимный интервал — одиночное стирание 64 КБ
|
||||
* блока (~0.15..2 c по даташиту W25Q) либо цепочка bsp_sd_init+f_mount+пик
|
||||
* слотов (~2-2.5 c). 10 c даёт кратный запас; зависание ловится ≤10 c. */
|
||||
const uint32_t WDOG_TIMEOUT_S = 10U;
|
||||
|
||||
board_hw_init();
|
||||
|
||||
/* Как можно раньше — до первой же SD-логики, которая может зависнуть. */
|
||||
(void) bsp_wdog_init(WDOG_TIMEOUT_S);
|
||||
|
||||
bsp_led_init();
|
||||
bsp_tick_init();
|
||||
bsp_button_init();
|
||||
|
||||
/* Жест форс. даунгрейда — удержание BSP_BUTTON_1 при подаче питания.
|
||||
* Сэмплируем РОВНО ЗДЕСЬ, до медленной SD-инициализации, и защёлкиваем на
|
||||
* всю сессию: сама установка читает кнопку глубоко внутри run_update()
|
||||
* (после mount + двух крипто-валидаций слотов, секунды спустя), поэтому
|
||||
* читать её там — неинтуитивно (см. DEBUG_LOG_PHASE3_SD.md, тайминг кнопки).
|
||||
* Значение переиспользуется и первой попыткой, и пере-сканами в цикле. */
|
||||
const bool DOWNGRADE_HELD = bsp_button_read(BSP_BUTTON_1);
|
||||
|
||||
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 не вставлена */
|
||||
sd_update_check(DOWNGRADE_HELD); /* no-op быстро, если SD не вставлена */
|
||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||
boot_select_and_jump(); /* при успехе не возвращается */
|
||||
}
|
||||
|
||||
|
|
@ -76,21 +104,25 @@ int main(void)
|
|||
|
||||
cli_init();
|
||||
|
||||
/* Если предыдущий сброс — по таймауту watchdog, известим (best-effort:
|
||||
* если хост ещё не подключён, сообщение потеряется — состояние всегда
|
||||
* доступно по команде "wdog", см. cli.c). */
|
||||
if (bsp_wdog_caused_last_reset())
|
||||
{
|
||||
protocol_send_wdog_status();
|
||||
}
|
||||
|
||||
/* Готово немедленно — первая попытка сразу извещает "жду SD", не ждёт
|
||||
* SD_RETRY_PERIOD_MS. Дальнейшие попытки уже дросселируются периодом. */
|
||||
uint32_t next_sd_retry_ms = bsp_tick_get_ms();
|
||||
|
||||
while (1)
|
||||
{
|
||||
bsp_wdog_refresh(); /* начало итерации — точка реального прогресса */
|
||||
|
||||
bsp_usb_cdc_poll();
|
||||
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)
|
||||
{
|
||||
|
|
@ -107,7 +139,8 @@ int main(void)
|
|||
|
||||
protocol_send_status("waiting_for_sd");
|
||||
|
||||
sd_update_check(); /* no-op быстро, если SD не вставлена */
|
||||
sd_update_check(DOWNGRADE_HELD);
|
||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||
boot_select_and_jump(); /* при успехе не возвращается */
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include "protocol.h"
|
||||
|
||||
#include "bsp/wdog.h"
|
||||
#include "cli.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
|
@ -43,3 +44,14 @@ void protocol_send_status(const char *p_state)
|
|||
(void) snprintf(buf, sizeof(buf), "{\"type\":\"status\",\"state\":\"%s\"}\n", p_state);
|
||||
cli_send(buf);
|
||||
}
|
||||
|
||||
void protocol_send_wdog_status(void)
|
||||
{
|
||||
char buf[PROTO_BUF_SIZE];
|
||||
(void) snprintf(buf, sizeof(buf),
|
||||
"{\"type\":\"wdog\",\"armed\":%s,\"timeout_s\":%u,\"recovered\":%s}\n",
|
||||
bsp_wdog_is_armed() ? "true" : "false",
|
||||
(unsigned) bsp_wdog_timeout_s(),
|
||||
bsp_wdog_caused_last_reset() ? "true" : "false");
|
||||
cli_send(buf);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
* Типы исходящих событий (Фаза 3):
|
||||
* status — top-level состояние bootloader (waiting_for_sd и т.д.,
|
||||
* см. protocol_send_status())
|
||||
* wdog — статус аппаратного watchdog (armed/timeout/recovered,
|
||||
* см. protocol_send_wdog_status())
|
||||
*
|
||||
* Полный словарь состояний status (smoke_pass/smoke_fail/booting/...)
|
||||
* появится в Фазе 4.
|
||||
|
|
@ -60,4 +62,17 @@ void protocol_send_error(const char *p_code);
|
|||
*/
|
||||
void protocol_send_status(const char *p_state);
|
||||
|
||||
/**
|
||||
* @brief Отправить статус аппаратного watchdog.
|
||||
*
|
||||
* Формат: {"type":"wdog","armed":true,"timeout_s":10,"recovered":false}
|
||||
* - armed — watchdog взведён (bsp_wdog_init выполнен);
|
||||
* - timeout_s — сконфигурированный таймаут в секундах;
|
||||
* - recovered — ПОСЛЕДНИЙ сброс МК был по таймауту watchdog (плата
|
||||
* восстановилась после зависания).
|
||||
*
|
||||
* Эмитится один раз на старте, если recovered, и по команде "wdog".
|
||||
*/
|
||||
void protocol_send_wdog_status(void);
|
||||
|
||||
#endif /* PROTOCOL_H_ */
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@
|
|||
#include "sd_update.h"
|
||||
|
||||
#include "bootutil/image.h"
|
||||
#include "bsp/button.h"
|
||||
#include "bsp/sd.h"
|
||||
#include "bsp/usb_cdc.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "ff.h"
|
||||
#include "flash_map.h"
|
||||
#include "protocol.h"
|
||||
|
|
@ -105,6 +105,7 @@ static bool erase_and_copy_candidate(const struct flash_area *p_fap, uint32_t fi
|
|||
while (offset < file_size)
|
||||
{
|
||||
bsp_usb_cdc_poll();
|
||||
bsp_wdog_refresh(); /* потоковое копирование — реальный прогресс на чанк */
|
||||
|
||||
uint32_t want = file_size - offset;
|
||||
if (want > SD_UPDATE_CHUNK_SIZE)
|
||||
|
|
@ -138,7 +139,7 @@ static bool erase_and_copy_candidate(const struct flash_area *p_fap, uint32_t fi
|
|||
|
||||
/* ── Основной сценарий ────────────────────────────────────────────────── */
|
||||
|
||||
static void run_update(void)
|
||||
static void run_update(bool button_held)
|
||||
{
|
||||
struct image_version candidate_ver;
|
||||
struct image_version installed_ver;
|
||||
|
|
@ -147,7 +148,6 @@ static void run_update(void)
|
|||
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)
|
||||
|
|
@ -177,7 +177,7 @@ static void run_update(void)
|
|||
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);
|
||||
/* button_held — сэмплирован при старте в main.c и передан сюда (см. sd_update.h). */
|
||||
decision = update_policy_decide(&slot_a, &slot_b, &candidate_ver, button_held);
|
||||
|
||||
if (decision.action == UPDATE_POLICY_SKIP)
|
||||
|
|
@ -242,12 +242,12 @@ cleanup:
|
|||
|
||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||
|
||||
void sd_update_check(void)
|
||||
void sd_update_check(bool downgrade_button_held)
|
||||
{
|
||||
if (!bsp_sd_is_inserted())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
run_update();
|
||||
run_update(downgrade_button_held);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
#ifndef SD_UPDATE_H_
|
||||
#define SD_UPDATE_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* @brief Одна попытка: смонтировать SD, найти TFT_APP.BIN, при необходимости
|
||||
* установить его в неактивный слот.
|
||||
|
|
@ -21,8 +23,15 @@
|
|||
* Ничего не делает, если SD не вставлена (bsp_sd_is_inserted() == false) —
|
||||
* безопасно вызывать многократно, в т.ч. из цикла ожидания в main.c.
|
||||
*
|
||||
* @param downgrade_button_held Состояние BSP_BUTTON_1, сэмплированное ОДИН РАЗ
|
||||
* при старте (main.c, до медленной SD-инициализации) и защёлкнутое.
|
||||
* true → разрешён форс. даунгрейд более старого подписанного образа
|
||||
* (см. update_policy.h). Передаётся, а не читается здесь, чтобы жест
|
||||
* "удержание при включении" ловился в предсказуемый ранний момент, а не
|
||||
* через несколько секунд внутри run_update() (DEBUG_LOG_PHASE3_SD.md).
|
||||
*
|
||||
* @pre bsp_qspi_init() уже вызван.
|
||||
*/
|
||||
void sd_update_check(void);
|
||||
void sd_update_check(bool downgrade_button_held);
|
||||
|
||||
#endif /* SD_UPDATE_H_ */
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
# firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2". Не часть
|
||||
# продукта — удалить, когда появится реальный tft_app.
|
||||
#
|
||||
# Два таргета из одного main.c: разный адрес слота (--defsym __slot_base__)
|
||||
# и разная частота мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой
|
||||
# слот выбрал bootloader. Постройка .bin, дальше подписывается вручную
|
||||
# imgtool'ом (см. PLAN.md) — .bin сюда не должен попасть без подписи.
|
||||
# Два таргета из одного main.c: разный адрес слота (--defsym __slot_base__) и
|
||||
# разная частота мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой слот
|
||||
# выбрал bootloader. Постройка .bin, дальше подписывается вручную imgtool'ом
|
||||
# (см. PLAN.md) — .bin сюда не должен попасть без подписи.
|
||||
|
||||
function(add_mcuboot_stub NAME SLOT_BASE BLINK_MS)
|
||||
add_executable(${NAME} main.c ${BSP_GENERATED}/clock_config.c
|
||||
|
|
@ -16,8 +16,11 @@ function(add_mcuboot_stub NAME SLOT_BASE BLINK_MS)
|
|||
target_compile_definitions(${NAME} PRIVATE STUB_BLINK_MS=${BLINK_MS}
|
||||
__STARTUP_CLEAR_BSS)
|
||||
|
||||
# bsp_wdog: загрузчик взводит WDOG перед прыжком, WDE — write-once, поэтому
|
||||
# образ ОБЯЗАН его кормить (иначе reset-loop). Заглушка — стенд-ин tft_app,
|
||||
# тоже кормит. См. bsp/wdog/README.md.
|
||||
target_link_libraries(${NAME} PRIVATE bsp_board bsp_led bsp_tick
|
||||
bsp_boot_xip_no_dcd)
|
||||
bsp_boot_xip_no_dcd bsp_wdog)
|
||||
|
||||
target_link_options(
|
||||
${NAME}
|
||||
|
|
@ -39,7 +42,7 @@ function(add_mcuboot_stub NAME SLOT_BASE BLINK_MS)
|
|||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${NAME}>
|
||||
${CMAKE_BINARY_DIR}/${NAME}.bin
|
||||
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${NAME}>
|
||||
COMMENT "Generating ${NAME}.bin (не подписан — imgtool sign вручную, см. PLAN.md)")
|
||||
COMMENT "Generating ${NAME}.bin")
|
||||
endfunction()
|
||||
|
||||
# Slot A: 0x60040000, мигает раз в 500 мс ("версия 1")
|
||||
|
|
|
|||
|
|
@ -103,31 +103,46 @@ uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency
|
|||
|
||||
Карта — FAT32/FAT16. Bootloader ищет в корне файл **`TFT_APP.BIN`** (путь `2:/TFT_APP.BIN`, диск `2:`
|
||||
— внутренняя нумерация FatFS, физически это корень карты). Файл — подписанный образ-заглушка,
|
||||
просто переименованный:
|
||||
просто переименованный.
|
||||
|
||||
> ⚠️ **Стабы не PIC — файл на SD должен быть линкован под адрес слота, в который он реально попадёт.**
|
||||
> `stub_a_*` линкован под `0x60040000` (Slot A), `stub_b_*` — под `0x60240000` (Slot Б); абсолютные
|
||||
> адреса (таблица векторов, литералы) зашиты в бинарь при линковке. Установка "не того" файла в слот
|
||||
> проходит crypto-гейт (подпись валидна для своего же содержимого) и доходит до прыжка, но
|
||||
> исполняется код, рассчитанный на другой адрес — эффективно ничего не запускается, без ошибки на
|
||||
> CDC. Тот же открытый вопрос, что и в Фазе 2 ([../PLAN.md](../PLAN.md), "разный адрес — намеренно...
|
||||
> открытый вопрос из BOOTLOADER_FLASH_MAP.md §4") и уже задокументированный в проде как gap одного
|
||||
> `TFT_APP.BIN` в [../../../docs/mimxrt1052/UPDATE_FLOW.md](../../../docs/mimxrt1052/UPDATE_FLOW.md).
|
||||
> **Целевой слот определяет `update_policy_decide()`** (всегда НЕ активный слот; на пустой плате
|
||||
> активного слота нет → по умолчанию Slot A) — файл на SD должен быть собран под этот же адрес.
|
||||
|
||||
```bash
|
||||
# Кандидат-"новее" (v2) — для авто-установки/сравнения версий
|
||||
cp build/Debug/signed/stub_b_v2_confirmed.bin /Volumes/<SD>/TFT_APP.BIN
|
||||
|
||||
# Кандидат-"старше" (v1) — для теста даунгрейда (сценарий 3)
|
||||
# Сценарий 1 (чистая плата — целевой слот всегда А, т.к. активного слота нет) —
|
||||
# ОБЯЗАТЕЛЬНО stub_a, не stub_b: stub_b линкован под Slot Б и не запустится в Slot A.
|
||||
cp build/Debug/signed/stub_a_v1_confirmed.bin /Volumes/<SD>/TFT_APP.BIN
|
||||
|
||||
# Кандидат с битой ПОДПИСЬЮ (magic цел, TLV/хэш испорчен) — сценарий 4b (SD_INSTALL_REJECTED)
|
||||
# Сценарий 2/3 (Slot Б уже занят под v2 — целевой слот снова А, т.к. Slot Б активен) —
|
||||
# тот же stub_a, сценарии отличаются только удержанием кнопки, см. §7 и таблицу §8.
|
||||
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)
|
||||
# Кандидат с битым ЗАГОЛОВКОМ (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»).
|
||||
> ℹ️ **Про повторный скан карты без ресета (раунд 4).** Детект сведён к единому механизму — пин
|
||||
> постоянно на `USDHC1_CD_B`, и гейт, и внутренний детект SDK читают USDHC `PRES_STATE.CINST` (item
|
||||
> 1, см. DEBUG_LOG). Прежняя хрупкость (после первого `bsp_sd_init()` пин уходил на `GPIO2_IO28` и
|
||||
> пере-скан ослеп бы) устранена. **Стоит отдельно проверить** кейс, который раньше был сломан: карта
|
||||
> вставлена, но `TFT_APP.BIN` появляется/кладётся на неё БЕЗ извлечения и перезагрузки — пере-скан в
|
||||
> цикле ожидания (~1.5 c) должен его подхватить. Между полноценными сценариями power cycle всё равно
|
||||
> обязателен (см. ниже).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -153,27 +168,29 @@ screen /dev/cu.usbmodemXXXX # macOS, порт свой у каждого по
|
|||
| `SD_INSTALL_WRITE_FAILED` | сбой записи/verify чанка во flash |
|
||||
| `SD_INSTALL_REJECTED` | образ записан, но пост-проверка (hash+ECDSA) не прошла |
|
||||
| `SD_DOWNGRADE_ERASE_FAILED` | не удалось стереть прежний активный слот при форс. даунгрейде |
|
||||
- Watchdog (`{"type":"wdog",...}`): запросить `{"type":"cmd","cmd":"wdog"}` →
|
||||
`{"type":"wdog","armed":true,"timeout_s":10,"recovered":false}`. `recovered:true` означает, что
|
||||
предыдущий сброс был по таймауту watchdog (плата восстановилась после зависания) — то же событие
|
||||
эмитится автоматически один раз при старте, если восстановление произошло.
|
||||
|
||||
---
|
||||
|
||||
## 7. Тайминг удержания кнопки даунгрейда (BSP_BUTTON_1)
|
||||
## 7. Удержание кнопки даунгрейда (BSP_BUTTON_1)
|
||||
|
||||
**Держать кнопку с момента подачи питания и не отпускать, пока по CDC не придёт
|
||||
`status: installing`** (или пока `LED_APP` не замигает с частотой более старого образа). Ориентир —
|
||||
**~15 секунд, с запасом**; отпускание на 2 c даунгрейд НЕ вызывает.
|
||||
**Держать `BSP_BUTTON_1` нажатой в момент подачи питания** (при включении). Состояние считывается
|
||||
один раз в первые миллисекунды после `board_hw_init()` — сразу после `bsp_button_init()`, ДО
|
||||
обращения к SD — и защёлкивается на всю сессию. Долгое удержание не требуется: важно, чтобы кнопка
|
||||
была нажата на момент включения.
|
||||
|
||||
Почему так (разбор по коду — [../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 идёт
|
||||
только в главном цикле, до которого управление ещё не дошло). Отпустишь раньше сэмпла — прочитается
|
||||
"не нажата" → штатная загрузка нового слота. «Срабатывает сразу после отпускания» — это не триггер
|
||||
по отпусканию (кода на отпускание нет), а совпадение: решение защёлкивается на сэмпле, пока кнопка
|
||||
ещё нажата.
|
||||
> **Изменение поведения (раунд 4, требует ре-проверки):** раньше кнопка читалась глубоко внутри
|
||||
> `run_update()` — уже после `f_mount()` и двух крипто-валидаций слотов, то есть через несколько
|
||||
> секунд слепого окна (приходилось держать ~15 c, пока по CDC не придёт `installing`). Теперь сэмпл
|
||||
> ранний и защёлкнутый (`main.c` → `downgrade_held` → `sd_update_check(...)`), см.
|
||||
> [../DEBUG_LOG_PHASE3_SD.md](../DEBUG_LOG_PHASE3_SD.md), раунд 4 / item 2. При проверке сценария 3
|
||||
> достаточно удерживать кнопку при включении.
|
||||
|
||||
> Известное неудобство (в очереди на доработку, не реализовано): сэмплить кнопку РАНО, при старте,
|
||||
> до медленной SD-инициализации — тогда «удержание при включении» стало бы предсказуемым.
|
||||
`bsp_button_read()` — мгновенное сырое чтение, без debounce и latch; жест — именно «нажато в момент
|
||||
старта», не «нажать после».
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -184,9 +201,9 @@ screen /dev/cu.usbmodemXXXX # macOS, порт свой у каждого по
|
|||
|
||||
| № | Сценарий | Подготовка | Ожидаемый результат |
|
||||
| --- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| 1 | Чистая плата + валидный образ на SD | Erase Slot A и Slot Б (§4); SD с `TFT_APP.BIN` = `stub_a_v1_confirmed.bin` (§5, **не** stub_b — см. предупреждение о PIC там); вставить карту | CDC: `installing`; авто-установка в Slot A → прыжок; `LED_APP` мигает ~1/сек (v1) |
|
||||
| 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/сек |
|
||||
| 3 | Форс. даунгрейд по кнопке | Как сценарий 2, но **удерживать `BSP_BUTTON_1` нажатой в момент подачи питания** (см. §7) | 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` при желании |
|
||||
|
|
@ -194,4 +211,197 @@ screen /dev/cu.usbmodemXXXX # macOS, порт свой у каждого по
|
|||
**Инструмент подтверждения «прыжок реально произошёл»** — тот же, что в Фазе 2: частота мигания
|
||||
`LED_APP` (500 мс vs 250 мс) однозначно указывает на выбранный слот, верифицируемо глазами.
|
||||
|
||||
> ⚠️ **Watchdog и заглушка.** Начиная с этого раунда загрузчик взводит аппаратный WDOG (10 c) перед
|
||||
> прыжком, а `WDE` — write-once (выключить нельзя). Заглушка `test_stub` его кормит, поэтому мигает
|
||||
> как раньше. Если прыгнуть в образ, который WDOG НЕ кормит, плата будет ресетиться каждые ~10 c —
|
||||
> для будущего `tft_app` это обязательный контракт (см. [../../../bsp/wdog/README.md](../../../bsp/wdog/README.md)).
|
||||
|
||||
---
|
||||
|
||||
## 9. Watchdog — провокация зависания и подтверждение восстановления
|
||||
|
||||
Цель: убедиться, что аппаратный WDOG1 (a) реально сбрасывает плату при зависании в блокирующем коде,
|
||||
(b) даёт восстановление (прыжок в валидный слот после сброса) без второго вмешательства оператора,
|
||||
(c) корректно сообщает об этом по CDC, (d) не мешает нормальной работе и отладке.
|
||||
|
||||
Раунд 3 закрыл конкретный баг card-detect (§0), поэтому исходный сценарий «пустой слот CD, физическое
|
||||
выдёргивание карты» больше не воспроизводится детерминированно тем же путём. Вместо охоты за реальной
|
||||
гонкой — **синтетическое зависание, включаемое временным патчем и гарантированно однократное**: код
|
||||
проверяет `bsp_wdog_caused_last_reset()` и виснет только в том прогоне, где предыдущий сброс не был
|
||||
watchdog-сбросом (т.е. только на первом POR-старте). Второй (WDOG-инициированный) сброс уже не
|
||||
попадает в ветку зависания — плата продолжает штатный boot без ручного вмешательства между двумя
|
||||
шагами. Один power cycle — весь тест, включая наблюдение восстановления.
|
||||
|
||||
**Общие предпосылки для 9.1 и 9.2:**
|
||||
- В Slot A — валидный подтверждённый `stub_a_v1_confirmed.bin` (см. §3), чтобы после WDOG-сброса
|
||||
плате было куда восстанавливаться.
|
||||
- Патчи в этом разделе — **временные, не коммитить**. После каждого теста — `git diff` / `git
|
||||
checkout -- firmware/bootloader/src/...` перед переходом к следующему шагу.
|
||||
- Секундомер (или просто ощущение "около 10 c") достаточен — таймаут не настолько короткий, чтобы
|
||||
нужен был осциллограф.
|
||||
|
||||
---
|
||||
|
||||
### 9.1 Тест A — механизм WDOG1 напрямую (без SD, без карты)
|
||||
|
||||
Изолирует сам аппаратный таймер от всей SD/USDHC-логики: гарантированный ноль `bsp_wdog_refresh()`
|
||||
после старта.
|
||||
|
||||
**Патч** (`firmware/bootloader/src/main.c`, сразу после `bsp_wdog_init()`, до `bsp_led_init()`):
|
||||
|
||||
```c
|
||||
(void) bsp_wdog_init(WDOG_TIMEOUT_S);
|
||||
|
||||
/* ВРЕМЕННО — тест 9.1, не коммитить */
|
||||
if (!bsp_wdog_caused_last_reset())
|
||||
{
|
||||
for (;;) { }
|
||||
}
|
||||
|
||||
bsp_led_init();
|
||||
```
|
||||
|
||||
**Сборка и прошивка:**
|
||||
|
||||
```bash
|
||||
just build::build-bootloader-debug
|
||||
just host::flash-swd-bootloader-debug
|
||||
```
|
||||
|
||||
**Прогон (один power cycle):**
|
||||
|
||||
1. Подать питание (или физический power cycle, не ресет через отладчик — нужен настоящий POR, чтобы
|
||||
`WDOG1->WRSR`/`SRC->SRSR` были в чистом состоянии).
|
||||
2. Наблюдение: ничего не мигает, ничего не инициализируется (зависание — до `bsp_led_init()`),
|
||||
CDC-порт не появляется.
|
||||
3. Через **~10 c** — аппаратный сброс. На этот раз `bsp_wdog_caused_last_reset()` вернёт `true` (сброс
|
||||
был по WDOG), условие ложно — зависания не будет, выполнение идёт дальше штатно:
|
||||
`bsp_led_init()` → ... → `boot_select_and_jump()` → прыжок в Slot A. `LED_APP` начинает мигать
|
||||
~1/сек (частота `stub_a`) — это и есть подтверждение восстановления, без второго power cycle.
|
||||
4. Подключиться по CDC и запросить статус:
|
||||
|
||||
```bash
|
||||
screen /dev/cu.usbmodemXXXX
|
||||
{"type":"cmd","cmd":"wdog"}
|
||||
```
|
||||
|
||||
Ожидаемый ответ: `{"type":"wdog","armed":true,"timeout_s":10,"recovered":true}`.
|
||||
|
||||
**Итог теста A:** таймаут ≈10 c подтверждён по секундомеру, `recovered:true` подтверждён по CDC,
|
||||
восстановление в валидный слот подтверждено по LED — без ручного вмешательства между зависанием и
|
||||
восстановлением.
|
||||
|
||||
---
|
||||
|
||||
### 9.2 Тест B — реалистичный сценарий (внутри SD-пути, требует вставленной карты)
|
||||
|
||||
Тот же приём gate-инга по `bsp_wdog_caused_last_reset()`, но зависание — в точке, где исторически уже
|
||||
дважды зависал реальный SD/USDHC-стек (см. [../DEBUG_LOG_PHASE3_SD.md](../DEBUG_LOG_PHASE3_SD.md)):
|
||||
сразу после успешного `bsp_sd_init()`, перед `f_mount()`.
|
||||
|
||||
**Патч** (`firmware/bootloader/src/sd_update.c`, в `run_update()`, сразу после проверки
|
||||
`bsp_sd_init()`):
|
||||
|
||||
```c
|
||||
if (bsp_sd_init() != BSP_OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* ВРЕМЕННО — тест 9.2, не коммитить */
|
||||
if (!bsp_wdog_caused_last_reset())
|
||||
{
|
||||
for (;;) { }
|
||||
}
|
||||
|
||||
if (f_mount(&g_s_fs, SD_UPDATE_MOUNT_POINT, 1) != FR_OK)
|
||||
```
|
||||
|
||||
**Предпосылка, отличная от 9.1:** SD-карта должна быть **физически вставлена** при подаче питания —
|
||||
иначе `sd_update_check()` вернётся по `bsp_sd_is_inserted() == false` ещё до `run_update()`, и
|
||||
патч не сработает вообще. Содержимое карты не важно (можно пустую/неотформатированную — до чтения
|
||||
файловой системы код не доходит).
|
||||
|
||||
**Сборка, прошивка, прогон** — идентично 9.1 (`just build::build-bootloader-debug`,
|
||||
`just host::flash-swd-bootloader-debug`, один power cycle с картой в слоте). Ожидаемая картина: LED
|
||||
успевает пройти обычную раннюю инициализацию (heartbeat может пару раз моргнуть, пока не дошло до
|
||||
`sd_update_check()`), дальше зависание на ~10 c, автоматический WDOG-сброс, на втором прогоне (тот же
|
||||
power cycle, вмешательство не требуется) — штатное продолжение и прыжок в Slot A. CDC-проверка —
|
||||
как в 9.1, п. 4.
|
||||
|
||||
**Итог теста B:** подтверждает защиту именно того класса зависаний, ради которого watchdog и
|
||||
проектировался — блокирующий вызов в SD-пути, не возвращающий управление.
|
||||
|
||||
---
|
||||
|
||||
### 9.3 (опционально) Прямое чтение регистров через pyOCD
|
||||
|
||||
Дополнительное, не обязательное подтверждение на уровне регистров — если хочется убедиться в причине
|
||||
сброса, не полагаясь только на прошивку/CDC. **Осторожно:** подключение отладчика само по себе может
|
||||
повлиять на состояние ядра в зависимости от режима коннекта — трактовать как вспомогательную, а не
|
||||
основную проверку; основные критерии успеха — секундомер + LED + CDC-ответ из 9.1/9.2.
|
||||
|
||||
```bash
|
||||
# WDOG1->WRSR (0x400B8004, 16-бит) — бит1 (0x2) = TOUT, сброс был по watchdog
|
||||
uv run --directory tools/hil pyocd commander --target mimxrt1050_quadspi --frequency 4000000 \
|
||||
-c "read16 0x400B8004"
|
||||
|
||||
# SRC->SRSR (0x400F8008, 32-бит) — бит4 (0x10) = WDOG_RST_B, та же причина на уровне SRC
|
||||
uv run --directory tools/hil pyocd commander --target mimxrt1050_quadspi --frequency 4000000 \
|
||||
-c "read32 0x400F8008"
|
||||
```
|
||||
|
||||
Оба регистра read-only снимки последнего сброса — валидны сразу после WDOG-сброса, до следующего
|
||||
любого сброса (в т.ч. до ресета самим отладчиком при коннекте — читать значение сразу первой командой
|
||||
сессии, ничего не делать до этого).
|
||||
|
||||
---
|
||||
|
||||
### 9.4 Регрессия — «не мешает нормальной работе»
|
||||
|
||||
Обязательно после 9.1/9.2, перед закрытием пункта:
|
||||
|
||||
1. Откатить оба временных патча (`git diff firmware/bootloader/src/main.c
|
||||
firmware/bootloader/src/sd_update.c` должен быть пустым), пересобрать и перепрошить:
|
||||
|
||||
```bash
|
||||
just build::build-bootloader-debug
|
||||
just host::flash-swd-bootloader-debug
|
||||
```
|
||||
|
||||
2. Power cycle, подключиться по CDC, запросить `{"type":"cmd","cmd":"wdog"}` — ожидается
|
||||
`"recovered":false` (последний сброс — обычный power-on, не watchdog).
|
||||
3. Прогнать чек-лист §8 (сценарии 1–5) как есть — все должны проходить без единого неожиданного
|
||||
сброса. WDOG кормится во всех легитимных долгих операциях (стирание слота, потоковое копирование,
|
||||
цикл ожидания SD) — сценарии с самой длинной легитимной работой (установка образа, сценарии 1 и 3)
|
||||
— лучший регрессионный индикатор: если бы кормление где-то пропустили, именно они словили бы
|
||||
ложный сброс посреди операции.
|
||||
4. Оставить плату в цикле ожидания (без SD, оба слота с валидными образами не трогать) на 2+ минуты,
|
||||
убедиться, что `LED_HEARTBEAT`/`LED_APP` продолжают штатный паттерн без сбросов — таймаут 10 c
|
||||
означает, что пропуск хотя бы одного кормления в цикле проявился бы в пределах первой минуты.
|
||||
|
||||
---
|
||||
|
||||
### 9.5 Поведение под отладчиком (SWD halt)
|
||||
|
||||
Подтвердить, что `enableDebug=false` действительно приостанавливает WDOG под отладкой — иначе
|
||||
пошаговая отладка любого будущего кода после `bsp_wdog_init()` была бы невозможна:
|
||||
|
||||
1. Запустить `🐛 Debug: bootloader` (см. Фаза 1), поставить брейкпоинт после `bsp_wdog_init()`.
|
||||
2. Остановиться на нём и держать паузу **дольше 10 c** (просто подождать, не резюмировать).
|
||||
3. Ожидаемо: сброса не происходит — после `Continue` выполнение продолжается с той же точки, как будто
|
||||
таймер не тикал во время halt.
|
||||
|
||||
---
|
||||
|
||||
### 9.6 Итоговый чек-лист
|
||||
|
||||
| № | Тест | Ожидаемый результат |
|
||||
|---|---|---|
|
||||
| 9.1 | Синтетическое зависание сразу после `bsp_wdog_init()`, без SD | Сброс ≈10 c после POR; авто-восстановление в Slot A без второго power cycle; CDC `wdog` → `recovered:true` |
|
||||
| 9.2 | Синтетическое зависание в `run_update()` после `bsp_sd_init()`, карта вставлена | То же поведение, но на пути, воспроизводящем исторический класс SD/USDHC-зависаний |
|
||||
| 9.3 (опц.) | Чтение `WDOG1->WRSR`/`SRC->SRSR` через pyOCD | Бит причины сброса (`TOUT`/`WDOG_RST_B`) установлен сразу после WDOG-сброса |
|
||||
| 9.4 | Регрессия: чек-лист §8 без патчей, ожидание 2+ мин в цикле | Ни одного ложного сброса; `wdog` → `recovered:false` |
|
||||
| 9.5 | SWD-halt дольше 10 c | Сброса нет, отладка не сбивается |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -11,10 +11,18 @@
|
|||
* Фазы 2".
|
||||
*
|
||||
* Без USB/CDC — визуальной индикации достаточно, минимальный код.
|
||||
*
|
||||
* Watchdog: загрузчик взводит аппаратный WDOG перед прыжком сюда, а WDE —
|
||||
* write-once (выключить нельзя). Поэтому заглушка ОБЯЗАНА его кормить, иначе
|
||||
* WDOG сбросит плату через таймаут и получится reset-loop — заглушка тут
|
||||
* играет роль tft_app, которая в проде тоже будет кормить watchdog
|
||||
* (см. bsp/wdog/README.md). Период мигания (250/500 мс) << таймаута (~10 c),
|
||||
* так что refresh на каждой итерации — с огромным запасом.
|
||||
*/
|
||||
#include "board.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/tick.h"
|
||||
#include "bsp/wdog.h"
|
||||
|
||||
#ifndef STUB_BLINK_MS
|
||||
#error "STUB_BLINK_MS must be defined (see firmware/bootloader/test_stub/CMakeLists.txt)"
|
||||
|
|
@ -28,6 +36,7 @@ int main(void)
|
|||
|
||||
while (1)
|
||||
{
|
||||
bsp_wdog_refresh(); /* обслуживаем унаследованный от загрузчика WDOG */
|
||||
bsp_led_toggle(LED_APP);
|
||||
bsp_delay(STUB_BLINK_MS);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@
|
|||
- [Как добавить новый тест](#как-добавить-новый-тест)
|
||||
- [Host unit-тесты](#host-unit-тесты)
|
||||
- [Версионирование](#версионирование)
|
||||
- [Архитектурные решения (закрыты)](#архитектурные-решения-закрыты)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -200,7 +199,7 @@ main.c
|
|||
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
|
||||
|
||||
| Тест | BSP модуль |
|
||||
| -------------- | ------------------------------ |
|
||||
| -------------- | ---------------------------------- |
|
||||
| `test_sdram` | `bsp_sdram` |
|
||||
| `test_qspi` | `bsp_qspi_flash` |
|
||||
| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) |
|
||||
|
|
@ -552,7 +551,7 @@ confirm id для pre-confirm равен id теста (`usd`) — механи
|
|||
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
|
||||
|
||||
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
|
||||
| --- | --------- | ------------------ | ---------------------------- | -------- | ------ | --------------- |
|
||||
| --- | --------- | ------------------ | ----------- | -------- | ------ | ------------- |
|
||||
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
||||
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
|
||||
|
|
@ -823,4 +822,4 @@ session_start / version_response: "fw":"0.1.2"
|
|||
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
|
||||
смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`.
|
||||
|
||||
##
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ add_sdk_driver(sai_edma fsl_sai_edma.c)
|
|||
add_sdk_driver(edma fsl_edma.c)
|
||||
add_sdk_driver(dmamux fsl_dmamux.c)
|
||||
add_sdk_driver(xbara fsl_xbara.c)
|
||||
add_sdk_driver(wdog fsl_wdog.c)
|
||||
|
||||
# Драйверы которые зависят от clock
|
||||
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)
|
||||
|
|
|
|||
Loading…
Reference in a new issue