# 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()
|
return()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# bsp_board — cгенерированные Config Tools файлы, инициализация платы
|
||||||
# bsp_board — генерированные файлы Config Tools, инициализация платы
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
add_library(bsp_board STATIC generated/board.c generated/pin_mux.c)
|
add_library(bsp_board STATIC generated/board.c generated/pin_mux.c)
|
||||||
|
|
||||||
add_subdirectory(common)
|
add_subdirectory(common)
|
||||||
|
|
@ -15,6 +12,7 @@ add_subdirectory(uart_host)
|
||||||
add_subdirectory(opto)
|
add_subdirectory(opto)
|
||||||
add_subdirectory(can)
|
add_subdirectory(can)
|
||||||
add_subdirectory(button)
|
add_subdirectory(button)
|
||||||
|
add_subdirectory(wdog)
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(usb_cdc)
|
add_subdirectory(usb_cdc)
|
||||||
add_subdirectory(sdram)
|
add_subdirectory(sdram)
|
||||||
|
|
@ -23,7 +21,6 @@ add_subdirectory(sd)
|
||||||
add_subdirectory(mqs)
|
add_subdirectory(mqs)
|
||||||
add_subdirectory(provisioning)
|
add_subdirectory(provisioning)
|
||||||
|
|
||||||
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
|
|
||||||
target_compile_options(bsp_board PRIVATE -w)
|
target_compile_options(bsp_board PRIVATE -w)
|
||||||
|
|
||||||
# SYSTEM подавляет предупреждения для всех внешних потребителей
|
# SYSTEM подавляет предупреждения для всех внешних потребителей
|
||||||
|
|
@ -67,10 +64,6 @@ target_compile_definitions(
|
||||||
add_library(bsp_boot_ram INTERFACE)
|
add_library(bsp_boot_ram INTERFACE)
|
||||||
target_compile_definitions(bsp_boot_ram INTERFACE SKIP_SYSCLK_INIT)
|
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)
|
add_library(bsp_boot_xip_no_dcd INTERFACE)
|
||||||
target_compile_definitions(
|
target_compile_definitions(
|
||||||
bsp_boot_xip_no_dcd INTERFACE XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1
|
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_dma_buf_addr = 0U;
|
||||||
volatile uint32_t g_sdmmc_dbg_usdhc1_src_clock_hz = 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_04_USDHC1_DATA2, pad);
|
||||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
|
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
|
||||||
/*
|
/*
|
||||||
* CD_B pad config — байт-в-байт как TFT_BOOTLOADER::BOARD_SD_Pin_Config()
|
* CD (D13) здесь НЕ конфигурируем: пин на USDHC1_CD_B (см. BOARD_SD_Config),
|
||||||
* (board/sdmmc_config.c: IOMUXC_SetPinConfig(..., 0x10B0U)), не "разумная
|
* pad задан в BOARD_InitPins() и на железе даёт корректный CINST. Прежняя
|
||||||
* по умолчанию" альтернатива. Отличие от прежней версии здесь: PKE=1 но
|
* настройка pad'а GPIO2_IO28 убрана вместе с GPIO-детектом (item 1,
|
||||||
* PUE=0 — это KEEPER, не активная подтяжка (PUS игнорируется в этом
|
* DEBUG_LOG_PHASE3_SD.md).
|
||||||
* режиме); HYS выключен. Прежняя версия (активная 47к подтяжка вверх +
|
|
||||||
* hysteresis) не была проверена на этой плате и не объяснила устойчивый
|
|
||||||
* ложный "card present" без карты на реальном железе (Фаза 3, симптом 1,
|
|
||||||
* см. DEBUG_LOG_PHASE3_SD.md) — pinmux сам по себе (GPIO2_IO28) это не
|
|
||||||
* лечит, раз симптом воспроизводится и после его отката.
|
|
||||||
*/
|
*/
|
||||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_12_GPIO2_IO28, IOMUXC_SW_PAD_CTL_PAD_PKE_MASK |
|
|
||||||
IOMUXC_SW_PAD_CTL_PAD_SPEED(2U) |
|
|
||||||
IOMUXC_SW_PAD_CTL_PAD_DSE(6U));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
|
|
@ -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_dma_buf_addr = (uint32_t)(uintptr_t)s_dma_buf;
|
||||||
g_sdmmc_dbg_usdhc1_src_clock_hz = sd->host->hostController.sourceClock_Hz;
|
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.cdDebounce_ms = BOARD_SDMMC_SD_CD_DEBOUNCE_MS;
|
||||||
s_cd.type = BOARD_SDMMC_SD_CD_TYPE;
|
s_cd.type = BOARD_SDMMC_SD_CD_TYPE; /* kSD_DetectCardByGpioCD → callback ниже */
|
||||||
s_cd.cardDetected = sd_card_detect_gpio;
|
s_cd.cardDetected = sd_card_detect_prsstat;
|
||||||
s_cd.callback = cd; /* обычно NULL из bsp_sd */
|
s_cd.callback = cd; /* обычно NULL из bsp_sd */
|
||||||
s_cd.userData = user_data;
|
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 питания --- */
|
/* --- GPIO питания --- */
|
||||||
sd_power_init();
|
sd_power_init();
|
||||||
/*
|
/*
|
||||||
* CD_B (GPIO_B1_12 / physical D13) — на этой плате детект работает через
|
* CD_B (GPIO_B1_12 / physical D13) остаётся на USDHC1_CD_B постоянно —
|
||||||
* DVA механизма в разные моменты (см. DEBUG_LOG_PHASE3_SD.md, разрешение
|
* единый механизм детекта через PRES_STATE.CINST (sd_card_detect_prsstat
|
||||||
* от 2026-07-10):
|
* выше + гейт bsp_sd_is_inserted). Прежней двойной маршрутизации
|
||||||
*
|
* (remux на GPIO2_IO28 для GPIO-чтения внутри f_mount) больше нет —
|
||||||
* 1. НАШ гейт bsp_sd_is_inserted() (bsp/sd/src/sd.c) — читает USDHC
|
* см. DEBUG_LOG_PHASE3_SD.md, раунд 3 «консолидация детекта» (item 1);
|
||||||
* PRES_STATE.CINST (USDHC_GetPresentStatusFlags). Это и есть настоящий
|
* она убирала латентную хрупкость: после первого bsp_sd_init() пин уходил
|
||||||
* фикс симптома 1: на пустом слоте BOARD_SD_Config() ещё не вызывался,
|
* на GPIO2_IO28 и повторный PRSSTAT-скан ослеп бы. Явно переустанавливаем
|
||||||
* пин остаётся на USDHC1_CD_B (замаплен в BOARD_InitPins()), и PRSSTAT
|
* альт-функцию (BOARD_InitPins() её тоже ставит — так модуль не зависит от
|
||||||
* отражает реальность корректно — гейт стабильно возвращает false, и
|
* порядка инициализации). Pad этого пина оставляем как задал BOARD_InitPins:
|
||||||
* блокирующий SD_PollingCardInsert() без карты просто не достигается.
|
* на железе CINST на нём читается корректно (Фаза 3, все сценарии).
|
||||||
* 2. Внутренний детект SDK (SD_PollingCardInsert внутри f_mount) — через
|
|
||||||
* GPIO-callback sd_card_detect_gpio() (GPIO_PinRead(GPIO2, 28)). Он
|
|
||||||
* достигается только ПОСЛЕ того, как гейт уже подтвердил карту, то
|
|
||||||
* есть уже после этого IOMUXC_SetPinMux ниже — тогда пин на GPIO2_IO28
|
|
||||||
* и GPIO-чтение корректно.
|
|
||||||
*
|
|
||||||
* Отсюда remux ниже на GPIO2_IO28: он нужен именно для (2). Пин остаётся
|
|
||||||
* эксклюзивным (одна альт-функция разом), поэтому (1) и (2) физически
|
|
||||||
* работают в разные моменты на разной маршрутизации одного пина — это
|
|
||||||
* подтверждено на железе (все 5 сценариев Фазы 3 пройдены), но хрупко:
|
|
||||||
* см. "латентная хрупкость re-scan" в DEBUG_LOG_PHASE3_SD.md и там же —
|
|
||||||
* рекомендованная консолидация на единый механизм (PRSSTAT везде).
|
|
||||||
*/
|
*/
|
||||||
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);
|
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U);
|
||||||
const gpio_pin_config_t cd_cfg = {
|
/* Pad-конфиг линий SD (CMD/CLK/DATA) сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
|
||||||
.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). */
|
|
||||||
sd_pin_config(400000U);
|
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/Б, или отдельный механизм).
|
как обновляется (та же SD-логика, что и Slot A/Б, или отдельный механизм).
|
||||||
- Точный layout `.ld`-скрипта tft_app для двух адресов слотов (Direct-XIP: код обычно не
|
- Точный layout `.ld`-скрипта tft_app для двух адресов слотов (Direct-XIP: код обычно не
|
||||||
позиционно-независим — вероятно потребуется два варианта линковки под Slot A и Slot Б, либо PIC).
|
позиционно-независим — вероятно потребуется два варианта линковки под 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
|
target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick
|
||||||
bsp_usb_cdc bsp_qspi_flash
|
bsp_usb_cdc bsp_qspi_flash
|
||||||
bsp_boot_xip_no_dcd bsp_button
|
bsp_boot_xip_no_dcd bsp_button
|
||||||
bootloader_fatfs)
|
bsp_wdog bootloader_fatfs)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом
|
# 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).** Основной баг детекта закрыт PRSSTAT-чтением (см. секцию
|
||||||
> «Обновление 2026-07-10 (раунд 3)» выше). Пункты 1–2 ниже сохранены как история; пункт 3 —
|
> «Обновление 2026-07-10 (раунд 3)» выше). Пункты 1–2 ниже сохранены как история; пункт 3 —
|
||||||
> опровергнут; пункты 4–5 (devcontainer, watchdog) остаются актуальными как отдельные задачи.
|
> опровергнут; пункты 4–5 (devcontainer, watchdog) остаются актуальными как отдельные задачи.
|
||||||
> Плюс два новых нереализованных пункта-рекомендации из раунда 3: (6) консолидация детекта на
|
> Рекомендации (6) консолидация детекта на единый PRSSTAT и (7) ранний сэмпл кнопки даунгрейда —
|
||||||
> единый механизм PRSSTAT (убрать латентную хрупкость re-scan), (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`) —
|
1. ✅ **Применено 2026-07-10 (раунд 1).** Откатить pinmux CD на `GPIO2_IO28` (без `USDHC1_CD_B`) —
|
||||||
самый доверенный референс (`TFT_BOOTLOADER`) использует именно так. **Аппаратно проверено —
|
самый доверенный референс (`TFT_BOOTLOADER`) использует именно так. **Аппаратно проверено —
|
||||||
|
|
@ -447,26 +450,28 @@ bool bsp_sd_is_inserted(void)
|
||||||
- `sd_power_control` возвращён в `static` (публичное имя `BOARD_SDCardPowerControl` из раунда 2
|
- `sd_power_control` возвращён в `static` (публичное имя `BOARD_SDCardPowerControl` из раунда 2
|
||||||
больше не нужно — side-effect, ради которого оно вводилось, заменён PRSSTAT-чтением).
|
больше не нужно — side-effect, ради которого оно вводилось, заменён PRSSTAT-чтением).
|
||||||
|
|
||||||
### ⚠️ Латентная хрупкость re-scan (не мешает 5 сценариям, но стоит знать)
|
### ✅ Латентная хрупкость re-scan — УСТРАНЕНА (раунд 4, item 1)
|
||||||
|
|
||||||
Рабочая конфигурация опирается на то, что пин на `USDHC1_CD_B` в момент гейта. Но `BOARD_SD_Config()`
|
**Была** (до раунда 4): рабочая конфигурация опиралась на то, что пин на `USDHC1_CD_B` в момент
|
||||||
перемапливает его на `GPIO2_IO28` при первом же `bsp_sd_init()` и обратно НЕ возвращает (ни
|
гейта, но `BOARD_SD_Config()` перемапливал его на `GPIO2_IO28` при первом же `bsp_sd_init()` и
|
||||||
`bsp_sd_deinit()`, ни что-либо ещё). Значит: если `run_update()` был вызван (карта была на гейте),
|
обратно НЕ возвращал. Если `run_update()` был вызван (карта была на гейте), но завершался без прыжка
|
||||||
но завершился без прыжка (например, на карте нет `TFT_APP.BIN` → `bsp_sd_deinit()` → return), то
|
(например, на карте нет `TFT_APP.BIN`), то на СЛЕДУЮЩЕМ периодическом скане `bsp_sd_is_inserted()`
|
||||||
на СЛЕДУЮЩЕМ периодическом скане `bsp_sd_is_inserted()` прочитает PRSSTAT с пином уже на
|
читал PRSSTAT с пином уже на `GPIO2_IO28` → ложный ноль → карта «исчезала» до перезагрузки.
|
||||||
`GPIO2_IO28` → снова получит ложный ноль → карта «исчезнет» до перезагрузки. В 5 сценариях это не
|
|
||||||
всплывает (везде, где карта валидна, происходит прыжок и сканы прекращаются; где карты нет —
|
|
||||||
`BOARD_SD_Config` не вызывался и пин остаётся на `USDHC1_CD_B`). Реальный, но узкий крайний случай.
|
|
||||||
|
|
||||||
**Рекомендованная консолидация (не реализована — требует повторной проверки на железе, а рабочее
|
**Реализовано (раунд 4, вариант B — минимальный):** детект сведён к ЕДИНОМУ механизму, PRSSTAT
|
||||||
состояние трогать без запроса не стал):** свести детект к ЕДИНОМУ механизму — PRSSTAT везде. Либо
|
везде. Callback `sd_card_detect_gpio` → `sd_card_detect_prsstat` (читает `USDHC_GetPresentStatusFlags
|
||||||
(вариант A) убрать remux на `GPIO2_IO28` из `BOARD_SD_Config()` (оставить `USDHC1_CD_B` как ставит
|
& kUSDHC_CardInsertedFlag`); remux на `GPIO2_IO28` из `BOARD_SD_Config()` убран — пин остаётся на
|
||||||
`BOARD_InitPins()`) и переключить `s_cd.type` на `kSD_DetectCardByHostCD` (тогда внутренний
|
`USDHC1_CD_B` постоянно (там же явно переустанавливается); мёртвый pad-конфиг GPIO2_IO28 из
|
||||||
`SD_PollingCardInsert` пойдёт через `SDMMCHOST_PollingCardDetectStatus` → тот же PRSSTAT); либо
|
`sd_pin_config()` удалён. `s_cd.type` оставлен `kSD_DetectCardByGpioCD` — это не «GPIO» в смысле
|
||||||
(вариант B, минимальнее) оставить `kSD_DetectCardByGpioCD`, но заменить сам callback
|
чтения, а «детект через callback» (polling); альтернатива `kSD_DetectCardByHostCD` дополнительно
|
||||||
`sd_card_detect_gpio` на PRSSTAT-чтение и тоже убрать remux. Оба варианта убирают двойную
|
взвела бы 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.
|
видимый результат (стирание+копирование+прыжок в старший образ) проявляется как раз к ~15 c.
|
||||||
|
|
||||||
**Практическая инструкция технологу** (в HARDWARE_VERIFICATION_PHASE3.md): держать `BSP_BUTTON_1` с
|
**✅ Реализовано (раунд 4, item 2):** кнопка теперь сэмплится РАНО и защёлкивается. `main.c` читает
|
||||||
момента подачи питания и не отпускать, пока по CDC не придёт `status: installing` (или пока частота
|
`bsp_button_read(BSP_BUTTON_1)` один раз сразу после `bsp_button_init()` (до `bsp_qspi_init()`/
|
||||||
мигания `LED_APP` не сменится на «старший» образ). Ориентир ~15 c — держать с запасом.
|
SD-логики) в `const bool downgrade_held` и передаёт это значение в `sd_update_check(downgrade_held)`
|
||||||
|
на обеих точках вызова (первая попытка + пере-сканы цикла). `run_update()` больше не читает кнопку
|
||||||
|
сам (`bsp/button.h` из `sd_update.c` убран) — `update_policy_decide()` получает уже защёлкнутое
|
||||||
|
значение. Жест «удержание при включении» теперь ловится в детерминированный ранний момент, а не
|
||||||
|
через секунды слепого окна.
|
||||||
|
|
||||||
**Рекомендация по коду (не реализовано — поведение бы изменилось, требует запроса и ре-теста):**
|
**Новая инструкция технологу** (обновлено в HARDWARE_VERIFICATION_PHASE3.md): достаточно держать
|
||||||
сэмплить кнопку РАНО — при `bsp_button_init()`/сразу в `board_hw_init()`-фазе, до медленной
|
`BSP_BUTTON_1` нажатой **в момент подачи питания** (до/во время включения) — состояние считывается в
|
||||||
SD-инициализации и двойной крипто-валидации — и защёлкивать результат, вместо чтения в глубине
|
первые миллисекунды после `board_hw_init()`, ещё до обращения к SD. Долгое «15 c с запасом» больше
|
||||||
`run_update()`. Тогда «удержание при старте» стало бы предсказуемым (нажал в момент включения —
|
не требуется. **Требует аппаратной ре-проверки** сценария 3 (даунгрейд): удержать кнопку при
|
||||||
сработало), без многосекундного слепого окна. Сейчас это самый user-hostile момент SD-пути.
|
включении → даунгрейд срабатывает; не удерживать → штатная загрузка более нового слота.
|
||||||
|
|
||||||
## Побочная находка (не блокирует, но стоит знать)
|
## Побочная находка (не блокирует, но стоит знать)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@
|
||||||
| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) |
|
| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) |
|
||||||
| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже |
|
| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже |
|
||||||
| 2 — bootutil (Direct-XIP) | ✅ завершена | host-тесты 5/5, аппаратная верификация — все 5 сценариев пройдены на реальной плате (детали и 3 найденных/исправленных бага — [DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md)) |
|
| 2 — bootutil (Direct-XIP) | ✅ завершена | host-тесты 5/5, аппаратная верификация — все 5 сценариев пройдены на реальной плате (детали и 3 найденных/исправленных бага — [DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md)) |
|
||||||
| 3 — SD-путь установки | ✅ аппаратно верифицирована (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, ранний сэмпл кнопки даунгрейда |
|
| 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-паттерны | не начата | |
|
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | рекомендуется ПОСЛЕ Фазы 6 (см. ниже) |
|
||||||
| 5 — HAB Release + service-tui | не начата | |
|
| 5 — HAB Release + service-tui | не начата | |
|
||||||
|
| 6 — Устойчивость и восстановление (recovery) | спроектирована, код не начат | **рекомендованный следующий шаг** (раньше 4/5): закрывает зависание уже установленного образа и даёт ручной аварийный вход. Дизайн зафиксирован в обсуждении — см. раздел «Фаза 6» ниже |
|
||||||
|
|
||||||
## Контекст
|
## Контекст
|
||||||
|
|
||||||
|
|
@ -29,7 +30,10 @@
|
||||||
- **Схема обновления**: MCUboot **Direct-XIP**, два слота (A/Б) с полностью валидными образами каждый,
|
- **Схема обновления**: MCUboot **Direct-XIP**, два слота (A/Б) с полностью валидными образами каждый,
|
||||||
без swap/scratch. Единственный полевой канал обновления — microSD. USB как канал заливки *образа*
|
без swap/scratch. Единственный полевой канал обновления — microSD. USB как канал заливки *образа*
|
||||||
сознательно не делаем (SDP/blhost на производстве — это отдельный, не зависящий от кода bootloader,
|
сознательно не делаем (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), не самодельный верификатор
|
- **Верификация образов tft_app** — через **bootutil** (MCUboot), не самодельный верификатор
|
||||||
(см. исследование ниже). Подпись — `imgtool` (не HAB; HAB — только для самого bootloader).
|
(см. исследование ниже). Подпись — `imgtool` (не HAB; HAB — только для самого bootloader).
|
||||||
- **Обратная связь** — USB CDC ACM с тем же JSON-lines протоколом (`ping`/`pong`, `get_version`), что уже
|
- **Обратная связь** — USB CDC ACM с тем же JSON-lines протоколом (`ping`/`pong`, `get_version`), что уже
|
||||||
|
|
@ -554,7 +558,64 @@ ARM-таргет — без изменений), иначе (host, любая а
|
||||||
кросс-компиляцией под `-target x86_64-unknown-linux-gnu` (0 ошибок после фикса, была именно эта ошибка
|
кросс-компиляцией под `-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)): у
|
Найдено при аппаратной верификации (см. [DEBUG_LOG_PHASE3_SD.md](DEBUG_LOG_PHASE3_SD.md)): у
|
||||||
`SD_PollingCardInsert()` (SDK, вызывается изнутри `SD_Init()`, который вызывается изнутри
|
`SD_PollingCardInsert()` (SDK, вызывается изнутри `SD_Init()`, который вызывается изнутри
|
||||||
|
|
@ -588,6 +649,8 @@ while (true)` без выхода по времени, если callback `cardDe
|
||||||
следующий шаг после того, как будет закрыт текущий SD/card-detect баг (иначе watchdog будет
|
следующий шаг после того, как будет закрыт текущий SD/card-detect баг (иначе watchdog будет
|
||||||
маскировать/сбрасывать симптом на каждом цикле, не давая найти первопричину).
|
маскировать/сбрасывать симптом на каждом цикле, не давая найти первопричину).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
**Верификация (первая часть Фазы 3, требующая реального железа — ещё не проведена)**:
|
**Верификация (первая часть Фазы 3, требующая реального железа — ещё не проведена)**:
|
||||||
1. Чистая плата (только bootloader, оба слота пустые) + SD с валидным подписанным образом →
|
1. Чистая плата (только bootloader, оба слота пустые) + SD с валидным подписанным образом →
|
||||||
автоустановка, переход к загрузке (проверить по CDC-статусам и LED).
|
автоустановка, переход к загрузке (проверить по 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/qspi_flash.h"
|
||||||
|
#include "bsp/wdog.h"
|
||||||
#include "flash_map.h"
|
#include "flash_map.h"
|
||||||
#include "sysflash/sysflash.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;
|
uint32_t block_addr = area->fa_off;
|
||||||
for (; len > 0U; len -= BSP_QSPI_BLOCK_64K_SIZE)
|
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)
|
if (bsp_qspi_erase_block_64k(block_addr) != BSP_OK)
|
||||||
{
|
{
|
||||||
return -1;
|
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;
|
uint32_t addr = area->fa_off + off;
|
||||||
for (; len > 0U; len -= BSP_QSPI_SECTOR_SIZE)
|
for (; len > 0U; len -= BSP_QSPI_SECTOR_SIZE)
|
||||||
{
|
{
|
||||||
|
bsp_wdog_refresh(); /* см. выше — посекторный путь тоже длинный */
|
||||||
if (bsp_qspi_erase_sector(addr) != BSP_OK)
|
if (bsp_qspi_erase_sector(addr) != BSP_OK)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,12 @@ static void handle_cmd(const char *p_line)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strcmp(cmd_name, "wdog") == 0)
|
||||||
|
{
|
||||||
|
protocol_send_wdog_status();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
protocol_send_error("UNKNOWN_CMD");
|
protocol_send_error("UNKNOWN_CMD");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,25 @@
|
||||||
*
|
*
|
||||||
* Последовательность старта:
|
* Последовательность старта:
|
||||||
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
||||||
* 2. bsp_led_init() — оба LED выключены
|
* 2. bsp_wdog_init() — аппаратный watchdog как можно раньше (см. ниже)
|
||||||
* 3. bsp_tick_init() — SysTick 1 мс
|
* 3. bsp_led_init() — оба LED выключены
|
||||||
* 4. bsp_button_init() — для проверки удержания BSP_BUTTON_1
|
* 4. bsp_tick_init() — SysTick 1 мс
|
||||||
* 5. bsp_qspi_init() — доступ к Slot A/Б
|
* 5. bsp_button_init() — для проверки удержания BSP_BUTTON_1
|
||||||
* 6. bsp_usb_cdc_init() — не блокирует, см. выше
|
* 6. bsp_qspi_init() — доступ к Slot A/Б
|
||||||
* 7. sd_update_check() — no-op быстро, если SD не вставлена
|
* 7. bsp_usb_cdc_init() — не блокирует, см. выше
|
||||||
* 8. boot_select_and_jump() — при успехе не возвращается
|
* 8. sd_update_check() — no-op быстро, если SD не вставлена
|
||||||
* 9. Цикл ожидания — CDC ping/pong + LED + периодический
|
* 9. boot_select_and_jump() — при успехе не возвращается
|
||||||
* пере-скан SD (шаги 7-8 повторно)
|
* 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
|
* Bootloader без SDRAM (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) — DCD
|
||||||
* не используется (bsp_boot_xip_no_dcd).
|
* не используется (bsp_boot_xip_no_dcd).
|
||||||
|
|
@ -37,6 +47,7 @@
|
||||||
#include "bsp/qspi_flash.h"
|
#include "bsp/qspi_flash.h"
|
||||||
#include "bsp/tick.h"
|
#include "bsp/tick.h"
|
||||||
#include "bsp/usb_cdc.h"
|
#include "bsp/usb_cdc.h"
|
||||||
|
#include "bsp/wdog.h"
|
||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "sd_update.h"
|
#include "sd_update.h"
|
||||||
|
|
@ -50,19 +61,36 @@ int main(void)
|
||||||
const uint32_t SD_RETRY_PERIOD_MS = 1500U;
|
const uint32_t SD_RETRY_PERIOD_MS = 1500U;
|
||||||
const uint32_t HEARTBEAT_ON_MS = 50U;
|
const uint32_t HEARTBEAT_ON_MS = 50U;
|
||||||
const uint32_t HEARTBEAT_PERIOD_MS = 500U;
|
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();
|
board_hw_init();
|
||||||
|
|
||||||
|
/* Как можно раньше — до первой же SD-логики, которая может зависнуть. */
|
||||||
|
(void) bsp_wdog_init(WDOG_TIMEOUT_S);
|
||||||
|
|
||||||
bsp_led_init();
|
bsp_led_init();
|
||||||
bsp_tick_init();
|
bsp_tick_init();
|
||||||
bsp_button_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 qspi_ok = (bsp_qspi_init() == BSP_OK);
|
||||||
bool cdc_ok = (bsp_usb_cdc_init() == BSP_OK);
|
bool cdc_ok = (bsp_usb_cdc_init() == BSP_OK);
|
||||||
|
|
||||||
if (qspi_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(); /* при успехе не возвращается */
|
boot_select_and_jump(); /* при успехе не возвращается */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,21 +104,25 @@ int main(void)
|
||||||
|
|
||||||
cli_init();
|
cli_init();
|
||||||
|
|
||||||
|
/* Если предыдущий сброс — по таймауту watchdog, известим (best-effort:
|
||||||
|
* если хост ещё не подключён, сообщение потеряется — состояние всегда
|
||||||
|
* доступно по команде "wdog", см. cli.c). */
|
||||||
|
if (bsp_wdog_caused_last_reset())
|
||||||
|
{
|
||||||
|
protocol_send_wdog_status();
|
||||||
|
}
|
||||||
|
|
||||||
/* Готово немедленно — первая попытка сразу извещает "жду SD", не ждёт
|
/* Готово немедленно — первая попытка сразу извещает "жду SD", не ждёт
|
||||||
* SD_RETRY_PERIOD_MS. Дальнейшие попытки уже дросселируются периодом. */
|
* SD_RETRY_PERIOD_MS. Дальнейшие попытки уже дросселируются периодом. */
|
||||||
uint32_t next_sd_retry_ms = bsp_tick_get_ms();
|
uint32_t next_sd_retry_ms = bsp_tick_get_ms();
|
||||||
|
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
|
bsp_wdog_refresh(); /* начало итерации — точка реального прогресса */
|
||||||
|
|
||||||
bsp_usb_cdc_poll();
|
bsp_usb_cdc_poll();
|
||||||
cli_process();
|
cli_process();
|
||||||
|
|
||||||
/* "Загрузчик жив" — короткий импульс (50 мс) + долгая пауза (450 мс),
|
|
||||||
* безусловно, вне зависимости от CDC. Специально отличим от ровного
|
|
||||||
* 50/50 мигания образа в слоте (LED_APP, 500/250 мс) — иначе на глаз
|
|
||||||
* не отличить "жив загрузчик" от "прыгнули в образ". Не blocking —
|
|
||||||
* bsp_delay() здесь не используется, иначе не успевали бы poll'ить
|
|
||||||
* CDC/cli с достаточной частотой. */
|
|
||||||
uint32_t heartbeat_phase_ms = bsp_tick_get_ms() % HEARTBEAT_PERIOD_MS;
|
uint32_t heartbeat_phase_ms = bsp_tick_get_ms() % HEARTBEAT_PERIOD_MS;
|
||||||
if (heartbeat_phase_ms < HEARTBEAT_ON_MS)
|
if (heartbeat_phase_ms < HEARTBEAT_ON_MS)
|
||||||
{
|
{
|
||||||
|
|
@ -107,7 +139,8 @@ int main(void)
|
||||||
|
|
||||||
protocol_send_status("waiting_for_sd");
|
protocol_send_status("waiting_for_sd");
|
||||||
|
|
||||||
sd_update_check(); /* no-op быстро, если SD не вставлена */
|
sd_update_check(DOWNGRADE_HELD);
|
||||||
|
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||||
boot_select_and_jump(); /* при успехе не возвращается */
|
boot_select_and_jump(); /* при успехе не возвращается */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
|
|
||||||
|
#include "bsp/wdog.h"
|
||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
|
|
||||||
#include <stdio.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);
|
(void) snprintf(buf, sizeof(buf), "{\"type\":\"status\",\"state\":\"%s\"}\n", p_state);
|
||||||
cli_send(buf);
|
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):
|
* Типы исходящих событий (Фаза 3):
|
||||||
* status — top-level состояние bootloader (waiting_for_sd и т.д.,
|
* status — top-level состояние bootloader (waiting_for_sd и т.д.,
|
||||||
* см. protocol_send_status())
|
* см. protocol_send_status())
|
||||||
|
* wdog — статус аппаратного watchdog (armed/timeout/recovered,
|
||||||
|
* см. protocol_send_wdog_status())
|
||||||
*
|
*
|
||||||
* Полный словарь состояний status (smoke_pass/smoke_fail/booting/...)
|
* Полный словарь состояний status (smoke_pass/smoke_fail/booting/...)
|
||||||
* появится в Фазе 4.
|
* появится в Фазе 4.
|
||||||
|
|
@ -60,4 +62,17 @@ void protocol_send_error(const char *p_code);
|
||||||
*/
|
*/
|
||||||
void protocol_send_status(const char *p_state);
|
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_ */
|
#endif /* PROTOCOL_H_ */
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@
|
||||||
#include "sd_update.h"
|
#include "sd_update.h"
|
||||||
|
|
||||||
#include "bootutil/image.h"
|
#include "bootutil/image.h"
|
||||||
#include "bsp/button.h"
|
|
||||||
#include "bsp/sd.h"
|
#include "bsp/sd.h"
|
||||||
#include "bsp/usb_cdc.h"
|
#include "bsp/usb_cdc.h"
|
||||||
|
#include "bsp/wdog.h"
|
||||||
#include "ff.h"
|
#include "ff.h"
|
||||||
#include "flash_map.h"
|
#include "flash_map.h"
|
||||||
#include "protocol.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)
|
while (offset < file_size)
|
||||||
{
|
{
|
||||||
bsp_usb_cdc_poll();
|
bsp_usb_cdc_poll();
|
||||||
|
bsp_wdog_refresh(); /* потоковое копирование — реальный прогресс на чанк */
|
||||||
|
|
||||||
uint32_t want = file_size - offset;
|
uint32_t want = file_size - offset;
|
||||||
if (want > SD_UPDATE_CHUNK_SIZE)
|
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 candidate_ver;
|
||||||
struct image_version installed_ver;
|
struct image_version installed_ver;
|
||||||
|
|
@ -147,7 +148,6 @@ static void run_update(void)
|
||||||
update_policy_result_t decision;
|
update_policy_result_t decision;
|
||||||
const struct flash_area *p_fap = NULL;
|
const struct flash_area *p_fap = NULL;
|
||||||
uint32_t file_size;
|
uint32_t file_size;
|
||||||
bool button_held;
|
|
||||||
bool copy_ok;
|
bool copy_ok;
|
||||||
|
|
||||||
if (bsp_sd_init() != BSP_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);
|
file_size = (uint32_t) f_size(&g_s_file);
|
||||||
slot_a = peek_slot(0U);
|
slot_a = peek_slot(0U);
|
||||||
slot_b = peek_slot(1U);
|
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);
|
decision = update_policy_decide(&slot_a, &slot_b, &candidate_ver, button_held);
|
||||||
|
|
||||||
if (decision.action == UPDATE_POLICY_SKIP)
|
if (decision.action == UPDATE_POLICY_SKIP)
|
||||||
|
|
@ -242,12 +242,12 @@ cleanup:
|
||||||
|
|
||||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
void sd_update_check(void)
|
void sd_update_check(bool downgrade_button_held)
|
||||||
{
|
{
|
||||||
if (!bsp_sd_is_inserted())
|
if (!bsp_sd_is_inserted())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
run_update();
|
run_update(downgrade_button_held);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
#ifndef SD_UPDATE_H_
|
#ifndef SD_UPDATE_H_
|
||||||
#define SD_UPDATE_H_
|
#define SD_UPDATE_H_
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Одна попытка: смонтировать SD, найти TFT_APP.BIN, при необходимости
|
* @brief Одна попытка: смонтировать SD, найти TFT_APP.BIN, при необходимости
|
||||||
* установить его в неактивный слот.
|
* установить его в неактивный слот.
|
||||||
|
|
@ -21,8 +23,15 @@
|
||||||
* Ничего не делает, если SD не вставлена (bsp_sd_is_inserted() == false) —
|
* Ничего не делает, если SD не вставлена (bsp_sd_is_inserted() == false) —
|
||||||
* безопасно вызывать многократно, в т.ч. из цикла ожидания в main.c.
|
* безопасно вызывать многократно, в т.ч. из цикла ожидания в 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() уже вызван.
|
* @pre bsp_qspi_init() уже вызван.
|
||||||
*/
|
*/
|
||||||
void sd_update_check(void);
|
void sd_update_check(bool downgrade_button_held);
|
||||||
|
|
||||||
#endif /* SD_UPDATE_H_ */
|
#endif /* SD_UPDATE_H_ */
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
# firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2". Не часть
|
# firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2". Не часть
|
||||||
# продукта — удалить, когда появится реальный tft_app.
|
# продукта — удалить, когда появится реальный tft_app.
|
||||||
#
|
#
|
||||||
# Два таргета из одного main.c: разный адрес слота (--defsym __slot_base__)
|
# Два таргета из одного main.c: разный адрес слота (--defsym __slot_base__) и
|
||||||
# и разная частота мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой
|
# разная частота мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой слот
|
||||||
# слот выбрал bootloader. Постройка .bin, дальше подписывается вручную
|
# выбрал bootloader. Постройка .bin, дальше подписывается вручную imgtool'ом
|
||||||
# imgtool'ом (см. PLAN.md) — .bin сюда не должен попасть без подписи.
|
# (см. PLAN.md) — .bin сюда не должен попасть без подписи.
|
||||||
|
|
||||||
function(add_mcuboot_stub NAME SLOT_BASE BLINK_MS)
|
function(add_mcuboot_stub NAME SLOT_BASE BLINK_MS)
|
||||||
add_executable(${NAME} main.c ${BSP_GENERATED}/clock_config.c
|
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}
|
target_compile_definitions(${NAME} PRIVATE STUB_BLINK_MS=${BLINK_MS}
|
||||||
__STARTUP_CLEAR_BSS)
|
__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
|
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(
|
target_link_options(
|
||||||
${NAME}
|
${NAME}
|
||||||
|
|
@ -39,7 +42,7 @@ function(add_mcuboot_stub NAME SLOT_BASE BLINK_MS)
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${NAME}>
|
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${NAME}>
|
||||||
${CMAKE_BINARY_DIR}/${NAME}.bin
|
${CMAKE_BINARY_DIR}/${NAME}.bin
|
||||||
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${NAME}>
|
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${NAME}>
|
||||||
COMMENT "Generating ${NAME}.bin (не подписан — imgtool sign вручную, см. PLAN.md)")
|
COMMENT "Generating ${NAME}.bin")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# Slot A: 0x60040000, мигает раз в 500 мс ("версия 1")
|
# 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:`
|
Карта — FAT32/FAT16. Bootloader ищет в корне файл **`TFT_APP.BIN`** (путь `2:/TFT_APP.BIN`, диск `2:`
|
||||||
— внутренняя нумерация FatFS, физически это корень карты). Файл — подписанный образ-заглушка,
|
— внутренняя нумерация 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
|
```bash
|
||||||
# Кандидат-"новее" (v2) — для авто-установки/сравнения версий
|
# Сценарий 1 (чистая плата — целевой слот всегда А, т.к. активного слота нет) —
|
||||||
cp build/Debug/signed/stub_b_v2_confirmed.bin /Volumes/<SD>/TFT_APP.BIN
|
# ОБЯЗАТЕЛЬНО stub_a, не stub_b: stub_b линкован под Slot Б и не запустится в Slot A.
|
||||||
|
|
||||||
# Кандидат-"старше" (v1) — для теста даунгрейда (сценарий 3)
|
|
||||||
cp build/Debug/signed/stub_a_v1_confirmed.bin /Volumes/<SD>/TFT_APP.BIN
|
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
|
cp build/Debug/signed/stub_b_v2_confirmed.bin /Volumes/<SD>/TFT_APP.BIN
|
||||||
# затем поменять один байт в СЕРЕДИНЕ файла (payload), напр. в hex-редакторе — magic в начале не трогать
|
# затем поменять один байт в СЕРЕДИНЕ файла (payload), напр. в hex-редакторе — magic в начале не трогать
|
||||||
# Кандидат с битым ЗАГОЛОВКОМ (magic) — сценарий 4a (SD_CANDIDATE_INVALID)
|
# Кандидат с битым ЗАГОЛОВКОМ (magic) — сценарий 4a (SD_CANDIDATE_INVALID), линковка тоже неважна
|
||||||
# поменять один из первых 4 байт файла (ih_magic)
|
# поменять один из первых 4 байт файла (ih_magic)
|
||||||
```
|
```
|
||||||
|
|
||||||
Берём `--confirm`-варианты: иначе установленный с SD образ на следующем power cycle
|
Берём `--confirm`-варианты: иначе установленный с SD образ на следующем power cycle
|
||||||
откатится (Direct-XIP Revert, см. Фаза 2 / сценарий 5 там).
|
откатится (Direct-XIP Revert, см. Фаза 2 / сценарий 5 там).
|
||||||
|
|
||||||
> ⚠️ **Про извлечение/повторный скан карты без ресета.** Детект на пустом слоте читает PRSSTAT при
|
> ℹ️ **Про повторный скан карты без ресета (раунд 4).** Детект сведён к единому механизму — пин
|
||||||
> пине на `USDHC1_CD_B`; после первого же `bsp_sd_init()` пин перемапливается на `GPIO2_IO28` и
|
> постоянно на `USDHC1_CD_B`, и гейт, и внутренний детект SDK читают USDHC `PRES_STATE.CINST` (item
|
||||||
> обратно не возвращается до перезагрузки. Практическое следствие для теста: **после каждого
|
> 1, см. DEBUG_LOG). Прежняя хрупкость (после первого `bsp_sd_init()` пин уходил на `GPIO2_IO28` и
|
||||||
> сценария делай power cycle** (и так обязателен, см. ниже), не рассчитывай на «вынул/вставил карту
|
> пере-скан ослеп бы) устранена. **Стоит отдельно проверить** кейс, который раньше был сломан: карта
|
||||||
> на живом загрузчике». Подробности и рекомендованная консолидация — в DEBUG_LOG (раунд 3,
|
> вставлена, но `TFT_APP.BIN` появляется/кладётся на неё БЕЗ извлечения и перезагрузки — пере-скан в
|
||||||
> «латентная хрупкость re-scan»).
|
> цикле ожидания (~1.5 c) должен его подхватить. Между полноценными сценариями power cycle всё равно
|
||||||
|
> обязателен (см. ниже).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -153,27 +168,29 @@ screen /dev/cu.usbmodemXXXX # macOS, порт свой у каждого по
|
||||||
| `SD_INSTALL_WRITE_FAILED` | сбой записи/verify чанка во flash |
|
| `SD_INSTALL_WRITE_FAILED` | сбой записи/verify чанка во flash |
|
||||||
| `SD_INSTALL_REJECTED` | образ записан, но пост-проверка (hash+ECDSA) не прошла |
|
| `SD_INSTALL_REJECTED` | образ записан, но пост-проверка (hash+ECDSA) не прошла |
|
||||||
| `SD_DOWNGRADE_ERASE_FAILED` | не удалось стереть прежний активный слот при форс. даунгрейде |
|
| `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 не придёт
|
**Держать `BSP_BUTTON_1` нажатой в момент подачи питания** (при включении). Состояние считывается
|
||||||
`status: installing`** (или пока `LED_APP` не замигает с частотой более старого образа). Ориентир —
|
один раз в первые миллисекунды после `board_hw_init()` — сразу после `bsp_button_init()`, ДО
|
||||||
**~15 секунд, с запасом**; отпускание на 2 c даунгрейд НЕ вызывает.
|
обращения к SD — и защёлкивается на всю сессию. Долгое удержание не требуется: важно, чтобы кнопка
|
||||||
|
была нажата на момент включения.
|
||||||
|
|
||||||
Почему так (разбор по коду — [../DEBUG_LOG_PHASE3_SD.md](../DEBUG_LOG_PHASE3_SD.md), раунд 3):
|
> **Изменение поведения (раунд 4, требует ре-проверки):** раньше кнопка читалась глубоко внутри
|
||||||
`bsp_button_read()` — мгновенное сырое чтение, без debounce и без latch. Кнопка сэмплится ровно
|
> `run_update()` — уже после `f_mount()` и двух крипто-валидаций слотов, то есть через несколько
|
||||||
один раз, в `sd_update.c` (`run_update()`), и этот момент наступает **после** `bsp_sd_init()`,
|
> секунд слепого окна (приходилось держать ~15 c, пока по CDC не придёт `installing`). Теперь сэмпл
|
||||||
`f_mount()` (полная инициализация карты), `f_open()` и **двух полных крипто-валидаций слотов**
|
> ранний и защёлкнутый (`main.c` → `downgrade_held` → `sd_update_check(...)`), см.
|
||||||
(SHA-256 + ECDSA по каждому). Это несколько секунд слепого окна без LED-фидбэка (heartbeat идёт
|
> [../DEBUG_LOG_PHASE3_SD.md](../DEBUG_LOG_PHASE3_SD.md), раунд 4 / item 2. При проверке сценария 3
|
||||||
только в главном цикле, до которого управление ещё не дошло). Отпустишь раньше сэмпла — прочитается
|
> достаточно удерживать кнопку при включении.
|
||||||
"не нажата" → штатная загрузка нового слота. «Срабатывает сразу после отпускания» — это не триггер
|
|
||||||
по отпусканию (кода на отпускание нет), а совпадение: решение защёлкивается на сэмпле, пока кнопка
|
|
||||||
ещё нажата.
|
|
||||||
|
|
||||||
> Известное неудобство (в очереди на доработку, не реализовано): сэмплить кнопку РАНО, при старте,
|
`bsp_button_read()` — мгновенное сырое чтение, без debounce и latch; жест — именно «нажато в момент
|
||||||
> до медленной SD-инициализации — тогда «удержание при включении» стало бы предсказуемым.
|
старта», не «нажать после».
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -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 не тронут) |
|
| 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, если слотов нет) |
|
| 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`; прежний валидный слот не тронут (записанный кандидат не выбирается) |
|
| 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` при желании |
|
| 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: частота мигания
|
**Инструмент подтверждения «прыжок реально произошёл»** — тот же, что в Фазе 2: частота мигания
|
||||||
`LED_APP` (500 мс vs 250 мс) однозначно указывает на выбранный слот, верифицируемо глазами.
|
`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".
|
* Фазы 2".
|
||||||
*
|
*
|
||||||
* Без USB/CDC — визуальной индикации достаточно, минимальный код.
|
* Без 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 "board.h"
|
||||||
#include "bsp/led.h"
|
#include "bsp/led.h"
|
||||||
#include "bsp/tick.h"
|
#include "bsp/tick.h"
|
||||||
|
#include "bsp/wdog.h"
|
||||||
|
|
||||||
#ifndef STUB_BLINK_MS
|
#ifndef STUB_BLINK_MS
|
||||||
#error "STUB_BLINK_MS must be defined (see firmware/bootloader/test_stub/CMakeLists.txt)"
|
#error "STUB_BLINK_MS must be defined (see firmware/bootloader/test_stub/CMakeLists.txt)"
|
||||||
|
|
@ -28,6 +36,7 @@ int main(void)
|
||||||
|
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
|
bsp_wdog_refresh(); /* обслуживаем унаследованный от загрузчика WDOG */
|
||||||
bsp_led_toggle(LED_APP);
|
bsp_led_toggle(LED_APP);
|
||||||
bsp_delay(STUB_BLINK_MS);
|
bsp_delay(STUB_BLINK_MS);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@
|
||||||
- [Как добавить новый тест](#как-добавить-новый-тест)
|
- [Как добавить новый тест](#как-добавить-новый-тест)
|
||||||
- [Host unit-тесты](#host-unit-тесты)
|
- [Host unit-тесты](#host-unit-тесты)
|
||||||
- [Версионирование](#версионирование)
|
- [Версионирование](#версионирование)
|
||||||
- [Архитектурные решения (закрыты)](#архитектурные-решения-закрыты)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -200,7 +199,7 @@ main.c
|
||||||
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
|
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
|
||||||
|
|
||||||
| Тест | BSP модуль |
|
| Тест | BSP модуль |
|
||||||
| -------------- | ------------------------------ |
|
| -------------- | ---------------------------------- |
|
||||||
| `test_sdram` | `bsp_sdram` |
|
| `test_sdram` | `bsp_sdram` |
|
||||||
| `test_qspi` | `bsp_qspi_flash` |
|
| `test_qspi` | `bsp_qspi_flash` |
|
||||||
| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) |
|
| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) |
|
||||||
|
|
@ -552,7 +551,7 @@ confirm id для pre-confirm равен id теста (`usd`) — механи
|
||||||
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
|
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
|
||||||
|
|
||||||
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
|
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
|
||||||
| --- | --------- | ------------------ | ---------------------------- | -------- | ------ | --------------- |
|
| --- | --------- | ------------------ | ----------- | -------- | ------ | ------------- |
|
||||||
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||||
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
||||||
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
|
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
|
||||||
|
|
@ -823,4 +822,4 @@ session_start / version_response: "fw":"0.1.2"
|
||||||
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
|
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
|
||||||
смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`.
|
смена семантики) — 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(edma fsl_edma.c)
|
||||||
add_sdk_driver(dmamux fsl_dmamux.c)
|
add_sdk_driver(dmamux fsl_dmamux.c)
|
||||||
add_sdk_driver(xbara fsl_xbara.c)
|
add_sdk_driver(xbara fsl_xbara.c)
|
||||||
|
add_sdk_driver(wdog fsl_wdog.c)
|
||||||
|
|
||||||
# Драйверы которые зависят от clock
|
# Драйверы которые зависят от clock
|
||||||
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)
|
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue