# bootloader: Phase 4 - preparing
This commit is contained in:
parent
1def117e44
commit
894d0d56c0
18 changed files with 789 additions and 41 deletions
|
|
@ -162,7 +162,8 @@
|
|||
"test_timeout_pattern",
|
||||
"test_mcuboot_boot_select",
|
||||
"test_slot_version",
|
||||
"test_update_policy"
|
||||
"test_update_policy",
|
||||
"test_recovery"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -184,7 +185,8 @@
|
|||
"test_timeout_pattern",
|
||||
"test_mcuboot_boot_select",
|
||||
"test_slot_version",
|
||||
"test_update_policy"
|
||||
"test_update_policy",
|
||||
"test_recovery"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ add_subdirectory(opto)
|
|||
add_subdirectory(can)
|
||||
add_subdirectory(button)
|
||||
add_subdirectory(wdog)
|
||||
add_subdirectory(boot_state)
|
||||
add_subdirectory(display)
|
||||
add_subdirectory(usb_cdc)
|
||||
add_subdirectory(sdram)
|
||||
|
|
|
|||
14
bsp/boot_state/CMakeLists.txt
Normal file
14
bsp/boot_state/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
if(BUILD_TESTS_HOST)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_library(bsp_boot_state STATIC src/boot_state.c)
|
||||
|
||||
target_include_directories(
|
||||
bsp_boot_state
|
||||
PUBLIC include/
|
||||
PRIVATE src/)
|
||||
|
||||
target_link_libraries(
|
||||
bsp_boot_state
|
||||
PRIVATE sdk_src)
|
||||
79
bsp/boot_state/README.md
Normal file
79
bsp/boot_state/README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# bsp_boot_state — счётчик попыток загрузки (SRC_GPR)
|
||||
|
||||
Счётчик попыток загрузки поверх `SRC` General Purpose Register — переживает тёплый/watchdog-сброс,
|
||||
обнуляется только на POR. Даёт коду восстановления прожить несколько сбросов подряд без
|
||||
персистентного хранилища во flash.
|
||||
|
||||
---
|
||||
|
||||
## Аппаратура
|
||||
|
||||
| Параметр | Значение |
|
||||
| ------------------------ | -------------------------------------------------------------- |
|
||||
| Периферия | SRC (System Reset Controller) |
|
||||
| Регистр счётчика | `SRC_GPR[2]` (GPR3, 0-based индекс `fsl_src` API) |
|
||||
| Переживает | тёплый сброс, watchdog-сброс |
|
||||
| Обнуляется | только POR (детект — `SRC->SRSR`, бит `IPP_RESET_B`) |
|
||||
| Занято ROM (не трогать) | GPR1/2 (warm-boot entry/arg), GPR6/7/8/9 (ROM, explicit note в RM), GPR10 (альт. SBMR1) |
|
||||
| Не занято ROM, но занято конвенцией | GPR5 — RM рекомендует под различение SYSRESETREQ/CPU lockup, не наша задача |
|
||||
|
||||
---
|
||||
|
||||
## Контракт: разделение с `bsp_wdog`
|
||||
|
||||
`SRC->SRSR` и `WDOG1->WRSR` — разные регистры с разной семантикой очистки. `WRSR` самоочищается на
|
||||
каждый сброс (не требует явной очистки — см. `bsp_wdog_caused_last_reset()`). `SRSR` —
|
||||
write-1-to-clear и **копит биты между тёплыми сбросами**, если их не чистить софтом:
|
||||
`bsp_boot_state_init()` чистит `SRSR` при каждом вызове, поэтому вопрос «был ли сброс по watchdog»
|
||||
остаётся за `bsp_wdog`, а не за этим модулем — этот модуль отвечает только за «был ли сброс POR».
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```c
|
||||
void bsp_boot_state_init(void); /* взвести — читает/чистит SRC->SRSR, детектит POR */
|
||||
bool bsp_boot_state_was_por(void); /* true, если последний сброс — POR */
|
||||
uint32_t bsp_boot_attempt_count(void); /* текущее значение счётчика, 0 сразу после POR */
|
||||
void bsp_boot_attempt_inc(void); /* +1 — звать перед попыткой прыжка в образ */
|
||||
void bsp_boot_attempt_reset(void); /* обнулить — новый образ/фолбэк-стирание */
|
||||
void bsp_boot_health_mark(void); /* = bsp_boot_attempt_reset(), для вызова из приложения */
|
||||
```
|
||||
|
||||
`bsp_boot_state_init()` **не идемпотентна** — повторный вызов в той же сессии увидит уже очищенный
|
||||
`SRSR` как «не POR». Звать ровно один раз, как можно раньше в `main()`.
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```c
|
||||
#include "bsp/boot_state.h"
|
||||
|
||||
/* main.c — после board_hw_init()/bsp_wdog_init(): */
|
||||
bsp_boot_state_init();
|
||||
|
||||
if (bsp_boot_attempt_count() >= THRESHOLD)
|
||||
{
|
||||
/* серия сбросов подряд без здорового образа — решение о фолбэке/recovery
|
||||
* принимает вызывающий код, не этот модуль */
|
||||
}
|
||||
|
||||
bsp_boot_attempt_inc(); /* перед каждой попыткой прыжка */
|
||||
/* ... */
|
||||
bsp_boot_health_mark(); /* приложение подтвердило собственное здоровье */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CMake
|
||||
|
||||
```cmake
|
||||
target_link_libraries(firmware_bootloader PRIVATE bsp_boot_state)
|
||||
```
|
||||
|
||||
**Зависимости модуля:**
|
||||
|
||||
| Зависимость | Тип | Описание |
|
||||
| ----------- | ------- | -------------------------------------------------------------- |
|
||||
| `sdk_src` | PRIVATE | `fsl_src.h` — `SRC_Get/SetGeneralPurposeRegister`, `SRC_Get/ClearResetStatusFlags` |
|
||||
59
bsp/boot_state/include/bsp/boot_state.h
Normal file
59
bsp/boot_state/include/bsp/boot_state.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* bsp_boot_state — счётчик попыток загрузки поверх SRC General Purpose
|
||||
* Register (MIMXRT1052).
|
||||
*
|
||||
* Назначение: пережить несколько сбросов подряд без реального
|
||||
* персистентного хранилища во flash. SRC_GPR — retained-регистр: сохраняет
|
||||
* значение через тёплый/watchdog-сброс, теряет его только на POR.
|
||||
*
|
||||
* Контракт: bsp_boot_state_init() сама детектирует POR по SRC->SRSR и, если
|
||||
* это POR, обнуляет счётчик — вызывающему коду не нужно проверять причину
|
||||
* сброса самостоятельно для этой цели. Причина «сброс по watchdog?» — не
|
||||
* этот модуль, см. bsp_wdog_caused_last_reset() (bsp/wdog.h): SRC->SRSR и
|
||||
* WDOG1->WRSR — разные регистры с разной семантикой очистки (WRSR
|
||||
* самоочищается на каждый сброс, SRSR — write-1-to-clear, накапливает биты
|
||||
* между сбросами без явной очистки).
|
||||
*/
|
||||
|
||||
#ifndef BSP_BOOT_STATE_H
|
||||
#define BSP_BOOT_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Инициализация. Вызывать один раз, как можно раньше в main() (после
|
||||
* board_hw_init()/bsp_wdog_init()). Читает SRC->SRSR, определяет POR (бит
|
||||
* IPP_RESET_B — на MIMXRT1052 отдельного бита POR нет, эту роль играет он),
|
||||
* чистит SRSR (write-1-to-clear, иначе биты копятся между тёплыми сбросами)
|
||||
* и, если это POR, обнуляет счётчик попыток.
|
||||
*
|
||||
* Повторный вызов в той же сессии — не идемпотентен (снова прочитает и
|
||||
* очистит уже очищенный SRSR, увидит "не POR"): звать ровно один раз.
|
||||
*/
|
||||
void bsp_boot_state_init(void);
|
||||
|
||||
/* true, если последний сброс МК (на момент bsp_boot_state_init()) был POR. */
|
||||
bool bsp_boot_state_was_por(void);
|
||||
|
||||
/* Текущее значение счётчика попыток. 0 сразу после POR. */
|
||||
uint32_t bsp_boot_attempt_count(void);
|
||||
|
||||
/* Инкремент счётчика попыток. Звать перед каждой попыткой прыжка в образ. */
|
||||
void bsp_boot_attempt_inc(void);
|
||||
|
||||
/*
|
||||
* Обнулить счётчик попыток. Звать при: успешной установке нового образа
|
||||
* (свежему образу — полный бюджет попыток), фолбэк-стирании зависшего слота
|
||||
* (ситуация изменилась).
|
||||
*/
|
||||
void bsp_boot_attempt_reset(void);
|
||||
|
||||
/*
|
||||
* Отметить образ здоровым — семантический алиас bsp_boot_attempt_reset() для
|
||||
* вызова из приложения (tft_app), а не из загрузчика: тот же эффект (счётчик
|
||||
* обнуляется), но имя называет намерение вызывающей стороны.
|
||||
*/
|
||||
void bsp_boot_health_mark(void);
|
||||
|
||||
#endif /* BSP_BOOT_STATE_H */
|
||||
67
bsp/boot_state/src/boot_state.c
Normal file
67
bsp/boot_state/src/boot_state.c
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* bsp_boot_state — реализация поверх fsl_src (SRC_GPR5 + SRC->SRSR).
|
||||
*/
|
||||
|
||||
#include "bsp/boot_state.h"
|
||||
|
||||
#include "fsl_src.h"
|
||||
|
||||
#define BSP_BOOT_STATE_SRC_BASE SRC
|
||||
|
||||
/*
|
||||
* GPR-индекс счётчика попыток (0-based, index=0 -> GPR1). Сверено с i.MX RT1050
|
||||
* RM (SRC, гл. 21.8.5-21.8.13): GPR1/2 — ROM (entry/arg пробуждения из
|
||||
* low-power), GPR6/7/8/9 — ТОЖЕ explicit "used by the ROM code, should not be
|
||||
* used by application software" (несмотря на первоначальное предположение,
|
||||
* что свободны только GPR1/2/10), GPR10 — ROM (альтернативный SBMR1 через бит
|
||||
* [28]). GPR5 формально не ROM, но RM рекомендует именно его под отдельную
|
||||
* задачу (различение SYSRESETREQ/CPU lockup) — не занимаем во избежание
|
||||
* конфликта с этой конвенцией. GPR3 (index 2) — единственный не подписан
|
||||
* НИКАКИМ примечанием в RM, описан просто как "arbitrary value".
|
||||
*/
|
||||
#define BSP_BOOT_STATE_GPR_INDEX 2U
|
||||
|
||||
static bool g_s_was_por = false;
|
||||
|
||||
void bsp_boot_state_init(void)
|
||||
{
|
||||
uint32_t flags = SRC_GetResetStatusFlags(BSP_BOOT_STATE_SRC_BASE);
|
||||
|
||||
g_s_was_por = (flags & (uint32_t) kSRC_IppResetPinFlag) != 0U;
|
||||
|
||||
/* SRSR — write-1-to-clear, копит биты между тёплыми сбросами без явной
|
||||
* очистки (в отличие от WDOG1->WRSR, который самоочищается). Чистим всё,
|
||||
* что доступно, чтобы следующая загрузка увидела только свою причину. */
|
||||
SRC_ClearResetStatusFlags(BSP_BOOT_STATE_SRC_BASE, ~0U);
|
||||
|
||||
if (g_s_was_por)
|
||||
{
|
||||
bsp_boot_attempt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool bsp_boot_state_was_por(void)
|
||||
{
|
||||
return g_s_was_por;
|
||||
}
|
||||
|
||||
uint32_t bsp_boot_attempt_count(void)
|
||||
{
|
||||
return SRC_GetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX);
|
||||
}
|
||||
|
||||
void bsp_boot_attempt_inc(void)
|
||||
{
|
||||
uint32_t count = bsp_boot_attempt_count();
|
||||
SRC_SetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX, count + 1U);
|
||||
}
|
||||
|
||||
void bsp_boot_attempt_reset(void)
|
||||
{
|
||||
SRC_SetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX, 0U);
|
||||
}
|
||||
|
||||
void bsp_boot_health_mark(void)
|
||||
{
|
||||
bsp_boot_attempt_reset();
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ add_executable(
|
|||
src/update_policy.c
|
||||
src/slot_version.c
|
||||
src/sd_update.c
|
||||
src/recovery.c
|
||||
mcuboot_port/flash_map_backend.c
|
||||
mcuboot_port/keys.c
|
||||
${MCUBOOT_BOOTUTIL_SOURCES}
|
||||
|
|
@ -52,7 +53,8 @@ target_compile_definitions(
|
|||
target_link_libraries(${TARGET_NAME} PRIVATE bsp_board bsp_led bsp_tick
|
||||
bsp_usb_cdc bsp_qspi_flash
|
||||
bsp_boot_xip_no_dcd bsp_button
|
||||
bsp_wdog bootloader_fatfs)
|
||||
bsp_wdog bsp_boot_state
|
||||
bootloader_fatfs)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
| 0 — Карта Flash | ✅ завершена | [docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md](../../docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md) |
|
||||
| 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже |
|
||||
| 2 — bootutil (Direct-XIP) | ✅ завершена | host-тесты 5/5, аппаратная верификация — все 5 сценариев пройдены на реальной плате (детали и 3 найденных/исправленных бага — [DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md)) |
|
||||
| 3 — SD-путь установки | ✅ верифицирована (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) |
|
||||
| 3 — SD-путь установки | ✅ полностью верифицирована (2026-07-13) | все 5 сценариев + раунд 4 (консолидация детекта на единый PRSSTAT + ранний сэмпл кнопки) + **аппаратный watchdog** (`bsp/wdog`, см. ниже) пройдены на реальной плате. По пути найден и исправлен баг чек-листа сценария 1 (стабы не PIC, см. ниже) — не регресс кода. См. [DEBUG_LOG_PHASE3_SD.md](DEBUG_LOG_PHASE3_SD.md), чек-лист — [test_stub/HARDWARE_VERIFICATION_PHASE3.md](test_stub/HARDWARE_VERIFICATION_PHASE3.md) |
|
||||
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | рекомендуется ПОСЛЕ Фазы 6 (см. ниже) |
|
||||
| 5 — HAB Release + service-tui | не начата | |
|
||||
| 6 — Устойчивость и восстановление (recovery) | спроектирована, код не начат | **рекомендованный следующий шаг** (раньше 4/5): закрывает зависание уже установленного образа и даёт ручной аварийный вход. Дизайн зафиксирован в обсуждении — см. раздел «Фаза 6» ниже |
|
||||
|
|
@ -491,7 +491,7 @@ host-тестом Фазы 2 (`test_boot_go_reverts_unconfirmed_image` в `tests
|
|||
плате). Единственное отличие от `firmware_test_fatfs`: **`FF_FS_READONLY=1`** — bootloader только
|
||||
читает `TFT_APP.BIN`, никогда не пишет на SD; убирает весь write-путь FatFS из сборки.
|
||||
|
||||
### Итог (выполнено, аппаратно ещё не проверено)
|
||||
### Итог (выполнено, аппаратно верифицировано)
|
||||
|
||||
- `firmware/bootloader/src/update_policy.{c,h}` — чистая логика install/skip + выбор целевого слота
|
||||
(см. решения выше). `image_version_compare()` — свой аналог `boot_version_cmp()` (`static` в
|
||||
|
|
@ -588,12 +588,18 @@ CDC. Сценарии 2/3 эту грань не задевали случайн
|
|||
`HARDWARE_VERIFICATION_PHASE3.md` §5 дополнен явным предупреждением о требовании
|
||||
линковка-под-слот-установки для любого файла на SD.
|
||||
|
||||
### ✅ Реализовано (2026-07-10): аппаратный watchdog
|
||||
### ✅ Реализовано и аппаратно верифицировано (2026-07-13): аппаратный watchdog
|
||||
|
||||
**Статус:** реализован, собран (ARM + host зелёные), **аппаратно ещё не проверен** — провокацию
|
||||
реального зависания и подтверждение восстановления см. в
|
||||
[test_stub/HARDWARE_VERIFICATION_PHASE3.md](test_stub/HARDWARE_VERIFICATION_PHASE3.md), раздел
|
||||
«Watchdog». Реализация против плана ниже:
|
||||
**Статус:** реализован, собран (ARM + host зелёные), **аппаратно верифицирован** (2026-07-13) —
|
||||
все пункты 9.1–9.5 чек-листа
|
||||
[test_stub/HARDWARE_VERIFICATION_PHASE3.md](test_stub/HARDWARE_VERIFICATION_PHASE3.md#9-watchdog--провокация-зависания-и-подтверждение-восстановления)
|
||||
пройдены: синтетическое зависание (механизм WDOG1 напрямую и в реалистичном SD-контексте) →
|
||||
сброс ≈10 c → авто-восстановление в валидный слот без второго вмешательства; регрессия (чек-лист §8
|
||||
без патчей, 2+ мин в цикле ожидания) — ни одного ложного сброса; SWD-halt дольше 10 c не сбрасывает
|
||||
плату. Дополнительно подтверждено прямым чтением регистров причины сброса через pyOCD (п. 9.3,
|
||||
опционально) — `WDOG1->WRSR=0x0010` (бит `POR`, не `TOUT`) и `SRC->SRSR=0x00000001` (бит
|
||||
`IPP_RESET_B`, не `WDOG_RST_B`) на момент чтения (после регрессии §9.4 — ожидаемо обычный POR, не
|
||||
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**
|
||||
|
|
@ -651,7 +657,8 @@ while (true)` без выхода по времени, если callback `cardDe
|
|||
|
||||
</details>
|
||||
|
||||
**Верификация (первая часть Фазы 3, требующая реального железа — ещё не проведена)**:
|
||||
**Верификация (первая часть Фазы 3, требующая реального железа — пройдена, см. чек-лист выше и
|
||||
[test_stub/HARDWARE_VERIFICATION_PHASE3.md](test_stub/HARDWARE_VERIFICATION_PHASE3.md))**:
|
||||
1. Чистая плата (только bootloader, оба слота пустые) + SD с валидным подписанным образом →
|
||||
автоустановка, переход к загрузке (проверить по CDC-статусам и LED).
|
||||
2. То же SD с образом версии ниже уже установленной, кнопка не нажата → отклонён
|
||||
|
|
@ -808,6 +815,34 @@ recovery-LED/CDC это показывают), лечится SD-фиксом и
|
|||
- Расширить CDC `wdog`-статус: `{"type":"wdog",...,"reset_count":N,"threshold":M}` — диагностика в
|
||||
поле («сбрасывалась N из M»).
|
||||
|
||||
### ✅ Реализовано (2026-07-13): 6a — счётчик + фолбэк, host-тесты зелёные, ARM собирается
|
||||
|
||||
- **`bsp/boot_state`** (не расширение `bsp_wdog` — разные периферии, см. решения выше): `SRC_GPR[2]`
|
||||
(GPR3, исправлено после сверки с официальной RM — см. ниже) — счётчик, `SRC->SRSR` бит
|
||||
`IPP_RESET_B` — POR-детект с обязательной
|
||||
`SRC_ClearResetStatusFlags()` после чтения (иначе биты копятся между тёплыми сбросами — см. находку
|
||||
про разделение `WRSR`/`SRSR` выше). API: `bsp_boot_state_init()`, `bsp_boot_state_was_por()`,
|
||||
`bsp_boot_attempt_count/inc/reset()`, `bsp_boot_health_mark()`. Переиспользует вендоренный
|
||||
`fsl_src.h` (новый `sdk_src` таргет в `sdk/CMakeLists.txt`, тот же `add_sdk_driver()` макрос, что и
|
||||
у `sdk_wdog`) — регистры руками не трогаем. Документация — по строгому стандарту остальных `bsp/*`.
|
||||
- **`firmware/bootloader/src/recovery.{c,h}`** — `recovery_decide(attempt_count, threshold, slot_a,
|
||||
slot_b, btn2_held)`. `is_por` из исходной сигнатуры плана убран — POR-обнуление счётчика происходит
|
||||
как побочный эффект `bsp_boot_state_init()`, чистой функции этот параметр не нужен. 9 host-тестов
|
||||
(`tests/host/recovery/`) — все ветки таксономии (BTN_2 главнее счётчика, ниже/на/выше порога, есть
|
||||
фолбэк/нет, оба слота невалидны).
|
||||
- **`main.c`** — обе точки прыжка (до цикла ожидания и внутри пере-скана) объединены в один
|
||||
`attempt_boot()`: `sd_update_check()` → `peek_slot()` обоих слотов → `recovery_decide()` → ветка.
|
||||
`bsp_boot_state_init()` — сразу после `bsp_wdog_init()`. `BSP_BUTTON_2` сэмплируется тем же ранним
|
||||
приёмом, что и `BSP_BUTTON_1`. Ветка `RECOVERY_ENTER_RECOVERY_MODE` в 6a — заглушка (не прыгаем,
|
||||
падаем в существующий цикл ожидания); полноценное recovery-состояние — 6b.
|
||||
- **CDC**: `protocol_send_wdog_status()` расширен полями `reset_count`/`threshold`.
|
||||
- **Host-тесты**: `just build::test-host` — **16/16 зелёных** (было 15, +9 новых `test_recovery`,
|
||||
один тест из старых 15 групп — не путать с количеством отдельных assert'ов).
|
||||
- **ARM-сборка**: `just build::build-bootloader-debug` и `hab-bootloader-debug` — оба зелёные.
|
||||
`m_text` — 90280 Б из 247 КБ бюджета (35.69%, было 87928 Б / 34.76% после Фазы 3 watchdog).
|
||||
- **Аппаратно не проверено** — как и остальная Фаза 6, ждёт стенда `test_stub` (6c) и чек-листа §
|
||||
«Верификация» выше.
|
||||
|
||||
**6b — Recovery-режим + ручной BTN_2** (тонкий аппаратный оркестратор)
|
||||
- Единое состояние recovery: подавить `boot_select_and_jump()`, отдельный LED-паттерн (узнаваемый
|
||||
«шиммер»/SOS обоими LED, явно отличный от heartbeat 50/450 и app 500/250 — сведётся в словарь
|
||||
|
|
@ -849,12 +884,45 @@ recovery-LED/CDC это показывают), лечится SD-фиксом и
|
|||
→ счётчик обнулён (POR), слот пробуется снова (подтверждает per-session-семантику).
|
||||
6. **Не мешает норме** — прогнать сценарии Фазы 3 как есть: без ложных recovery/сбросов.
|
||||
|
||||
### Открытые вопросы к реализации
|
||||
### Решения, принятые в обсуждении реализации Фазы 6 (2026-07-13, до кода — не пересматриваются)
|
||||
|
||||
- `SRC_GPR[n]`: индекс (свободный от ROM) + подтвердить переживание watchdog-сброса и обнуление на
|
||||
POR (RM + железо).
|
||||
- `SRC->SRSR` — корректно читать/чистить (w1c) причину сброса, различать POR vs WDOG (биты
|
||||
`SRC_SRSR_WDOG_RST_B` / POR).
|
||||
- **Новый модуль `bsp/boot_state`**, не расширение `bsp_wdog` — разные периферии (WDOG1 vs SRC),
|
||||
разная ответственность. Документация — по тому же строгому стандарту, что и остальные `bsp/*`
|
||||
(см. недавний рефактор `bsp/wdog/README.md`): `Аппаратура` → `Контракт`/`Архитектура` → `API` →
|
||||
`Быстрый старт` → `CMake`, без ссылок на конкретных потребителей.
|
||||
- **Переиспользуем вендоренный `sdk/devices/MIMXRT1052/drivers/fsl_src.h`**, не хардкодим регистры
|
||||
`SRC` руками: `SRC_GetGeneralPurposeRegister`/`SRC_SetGeneralPurposeRegister` (индекс 0-based,
|
||||
`index=0` → GPR1) для счётчика; `SRC_GetResetStatusFlags`/`SRC_ClearResetStatusFlags` для `SRSR`
|
||||
(уже корректно реализует w1c через `SRC_SRSR_W1C_BITS_MASK`, ручной код не нужен).
|
||||
- **`SRC_GPR[2]` (GPR3)** — счётчик попыток. **Исправлено после сверки с официальной i.MX RT1050 RM**
|
||||
(гл. 21.8.5–21.8.13, PDF от пользователя) — первоначальная догадка по одним заголовкам
|
||||
(`GPR5..8` "спокойные", заняты только `GPR1/2/10`) оказалась **неверной**: RM явно помечает
|
||||
`SRC_GPR6`, `GPR7`, `GPR8`, `GPR9` тем же примечанием **"used by the ROM code and should not be
|
||||
used by application software"**, что и `GPR1`/`GPR2` (entry/arg тёплой загрузки) — заголовки
|
||||
`PERI_SRC.h` этого не показывают (там только имена битовых полей, без ROM-пометок из текста RM).
|
||||
`GPR10` — тоже ROM (бит `[28]` включает трактовку `GPR9` как альтернативного `SBMR1`).
|
||||
**`GPR5`** формально не ROM-зарезервирован, но RM прямо рекомендует именно его под свою задачу
|
||||
(различение `SYSRESETREQ` vs CPU lockup перед программным сбросом) — заняли бы чужую конвенцию.
|
||||
Единственные регистры **без единой пометки** в RM — `GPR3` и `GPR4` ("Read/write general purpose
|
||||
bits used to store an arbitrary value"). Выбран `GPR3`.
|
||||
**Всё ещё стоит подтвердить на железе** (RM говорит про GPR1/2 явно "SRC ensures that the register
|
||||
value will persist across system resets" — вся физическая шина GPR та же, но не для каждого
|
||||
регистра дословно повторено): реально ли значение переживает watchdog-сброс — первый шаг 6a.
|
||||
- **Разделение ответственности WRSR vs SRSR** (важный нюанс, не был очевиден из плана): `SRSR`
|
||||
накапливает биты между resets, если не чистить софтом (w1c) — эмпирически подтверждено (§9.3):
|
||||
чистый POR обнулил `SRSR` целиком аппаратно, но между двумя warm-резетами подряд (класс B — серия
|
||||
watchdog-сбросов без POR между ними) биты будут копиться без явной чистки. `WDOG1->WRSR` (уже
|
||||
использует `bsp_wdog_caused_last_reset()`) — self-clean, накопления не даёт. Поэтому:
|
||||
- **«Это был сброс по watchdog?»** → как и раньше, `bsp_wdog_caused_last_reset()` (WRSR) — код уже
|
||||
есть, не трогаем.
|
||||
- **«Это был POR (обнулить счётчик) или тёплый резет (не трогать)?»** → новый код в
|
||||
`bsp/boot_state`, бит `IPP_RESET_B` в `SRC->SRSR` (для MIMXRT1052 `FSL_FEATURE_SRC_HAS_SRSR_POR=0`
|
||||
— отдельного бита POR нет, роль POR играет `IPP_RESET_B`) — и **обязательная
|
||||
`SRC_ClearResetStatusFlags()` сразу после чтения**, каждую загрузку.
|
||||
- **LED-паттерн recovery** (не дожидаясь словаря Фазы 4): **оба LED вместе, синхронно, 100 мс
|
||||
вкл/100 мс выкл** — чётко отличается от heartbeat (50/450 мс, один LED) и app (500/250 мс, один
|
||||
LED).
|
||||
- **Порог класса B — подтверждён: 3** (дефолт из исходного плана остаётся).
|
||||
- Word choice recovery-LED — согласовать со словарём Фазы 4, чтобы не переделывать.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@
|
|||
* ожидания, где SD периодически пере-сканируется (см. sd_update.h о том,
|
||||
* почему boot_go() нельзя звать без новой попытки установки между вызовами).
|
||||
*
|
||||
* Фаза 6 (recovery, см. recovery.h): каждая попытка обёрнута в attempt_boot()
|
||||
* — после SD-скана, но перед прыжком, recovery_decide() решает, обычная ли
|
||||
* это загрузка, нужно ли стереть подозреваемый в зависании слот (счётчик
|
||||
* bsp_boot_attempt_count() дошёл до порога, но есть валидный фолбэк), или
|
||||
* входить в recovery (порог без фолбэка, или удержан BSP_BUTTON_2). Класс A
|
||||
* таксономии (незавершённая установка) закрывается штатным revert MCUboot
|
||||
* без участия этой логики.
|
||||
*
|
||||
* USB CDC поднимается ДО SD-логики (не дожидаясь подключения хоста —
|
||||
* bsp_usb_cdc_write() не блокируется без хоста, см. bsp/usb_cdc/src/usb_cdc.c)
|
||||
* — чтобы статусы ("installing" и т.п.) были видны, если технолог уже
|
||||
|
|
@ -18,15 +26,16 @@
|
|||
* Последовательность старта:
|
||||
* 1. board_hw_init() — тактирование, MPU, кэш, пины
|
||||
* 2. bsp_wdog_init() — аппаратный watchdog как можно раньше (см. ниже)
|
||||
* 3. bsp_led_init() — оба LED выключены
|
||||
* 4. bsp_tick_init() — SysTick 1 мс
|
||||
* 5. bsp_button_init() — для проверки удержания BSP_BUTTON_1
|
||||
* 6. bsp_qspi_init() — доступ к Slot A/Б
|
||||
* 7. bsp_usb_cdc_init() — не блокирует, см. выше
|
||||
* 8. sd_update_check() — no-op быстро, если SD не вставлена
|
||||
* 9. boot_select_and_jump() — при успехе не возвращается
|
||||
* 3. bsp_boot_state_init() — POR-детект + счётчик попыток (Фаза 6)
|
||||
* 4. bsp_led_init() — оба LED выключены
|
||||
* 5. bsp_tick_init() — SysTick 1 мс
|
||||
* 6. bsp_button_init() — для проверки удержания BSP_BUTTON_1/2
|
||||
* 7. bsp_qspi_init() — доступ к Slot A/Б
|
||||
* 8. bsp_usb_cdc_init() — не блокирует, см. выше
|
||||
* 9. attempt_boot() — SD-скан + recovery-гейт + прыжок; при
|
||||
* успехе не возвращается
|
||||
* 10. Цикл ожидания — CDC ping/pong + LED + периодический
|
||||
* пере-скан SD (шаги 8-9 повторно)
|
||||
* пере-скан SD (шаг 9 повторно)
|
||||
*
|
||||
* Watchdog (bsp_wdog): единственная защита от бесконечных зависаний в
|
||||
* блокирующих вызовах SDMMC-стека, не возвращающих управление в наш код
|
||||
|
|
@ -42,6 +51,7 @@
|
|||
*/
|
||||
#include "board.h"
|
||||
#include "boot_select.h"
|
||||
#include "bsp/boot_state.h"
|
||||
#include "bsp/button.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/qspi_flash.h"
|
||||
|
|
@ -49,12 +59,73 @@
|
|||
#include "bsp/usb_cdc.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "cli.h"
|
||||
#include "flash_map.h"
|
||||
#include "protocol.h"
|
||||
#include "recovery.h"
|
||||
#include "sd_update.h"
|
||||
#include "slot_version.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ── Одна попытка загрузки: SD-скан + recovery-гейт (Фаза 6) + прыжок ─────
|
||||
*
|
||||
* sd_update_check() — до чтения счётчика/состояния слотов: новый образ с SD
|
||||
* заслуживает полный бюджет попыток независимо от текущего счётчика (см.
|
||||
* "Правила обнуления счётчика" в PLAN.md), а не только после гейта.
|
||||
*
|
||||
* peek_slot() — та же логика, что private peek_slot() в sd_update.c: не
|
||||
* шарим напрямую между модулями (см. update_policy.h), копия минимальна. */
|
||||
static update_policy_slot_state_t peek_slot(uint8_t fa_id)
|
||||
{
|
||||
update_policy_slot_state_t state;
|
||||
state.valid = slot_version_get(fa_id, &state.version);
|
||||
return state;
|
||||
}
|
||||
|
||||
static void attempt_boot(bool downgrade_held, bool recovery_held)
|
||||
{
|
||||
sd_update_check(downgrade_held); /* no-op быстро, если SD не вставлена */
|
||||
|
||||
update_policy_slot_state_t slot_a = peek_slot(0U);
|
||||
update_policy_slot_state_t slot_b = peek_slot(1U);
|
||||
|
||||
recovery_decision_t decision = recovery_decide(
|
||||
bsp_boot_attempt_count(), RECOVERY_DEFAULT_THRESHOLD, &slot_a, &slot_b, recovery_held);
|
||||
|
||||
switch (decision.action)
|
||||
{
|
||||
case RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER:
|
||||
{
|
||||
/* Подозреваемый в зависании слот — стереть, есть подтверждённый
|
||||
* фолбэк (recovery_decide() это уже проверила). boot_go() внутри
|
||||
* boot_select_and_jump() ниже сам выберет оставшийся. */
|
||||
const struct flash_area *p_fap;
|
||||
if (flash_area_open((uint8_t) decision.active_slot, &p_fap) == 0)
|
||||
{
|
||||
(void) flash_area_erase(p_fap, 0U, p_fap->fa_size);
|
||||
flash_area_close(p_fap);
|
||||
}
|
||||
bsp_boot_attempt_reset(); /* ситуация изменилась — новый полный бюджет */
|
||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||
boot_select_and_jump(); /* при успехе не возвращается */
|
||||
break;
|
||||
}
|
||||
case RECOVERY_ENTER_RECOVERY_MODE:
|
||||
/* TODO(Фаза 6b): отдельное recovery-состояние — LED-паттерн, CDC
|
||||
* status:recovery_mode, ослабленный version-gate на SD-установке.
|
||||
* Пока просто не прыгаем — падаем в обычный цикл ожидания main(),
|
||||
* как и при отсутствии валидного образа. */
|
||||
break;
|
||||
case RECOVERY_NORMAL_BOOT:
|
||||
default:
|
||||
bsp_boot_attempt_inc(); /* перед попыткой — см. bsp/boot_state.h */
|
||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||
boot_select_and_jump(); /* при успехе не возвращается */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const uint32_t ERROR_BLINK_MS = 250U;
|
||||
|
|
@ -72,6 +143,10 @@ int main(void)
|
|||
/* Как можно раньше — до первой же SD-логики, которая может зависнуть. */
|
||||
(void) bsp_wdog_init(WDOG_TIMEOUT_S);
|
||||
|
||||
/* Сразу после watchdog — сама операция дешёвая (пара регистров SRC), а
|
||||
* решение recovery_decide() ниже нужно уже на первой попытке. */
|
||||
bsp_boot_state_init();
|
||||
|
||||
bsp_led_init();
|
||||
bsp_tick_init();
|
||||
bsp_button_init();
|
||||
|
|
@ -84,14 +159,16 @@ int main(void)
|
|||
* Значение переиспользуется и первой попыткой, и пере-сканами в цикле. */
|
||||
const bool DOWNGRADE_HELD = bsp_button_read(BSP_BUTTON_1);
|
||||
|
||||
/* Жест recovery (Фаза 6) — тот же приём, удержание BSP_BUTTON_2. Приоритет
|
||||
* над BTN_1 разрешается внутри recovery_decide() (проверяется первым). */
|
||||
const bool RECOVERY_HELD = bsp_button_read(BSP_BUTTON_2);
|
||||
|
||||
bool qspi_ok = (bsp_qspi_init() == BSP_OK);
|
||||
bool cdc_ok = (bsp_usb_cdc_init() == BSP_OK);
|
||||
|
||||
if (qspi_ok)
|
||||
{
|
||||
sd_update_check(DOWNGRADE_HELD); /* no-op быстро, если SD не вставлена */
|
||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||
boot_select_and_jump(); /* при успехе не возвращается */
|
||||
attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
|
||||
}
|
||||
|
||||
/* Нет валидного образа ни в одном слоте (или сбой QSPI) —
|
||||
|
|
@ -139,9 +216,7 @@ int main(void)
|
|||
|
||||
protocol_send_status("waiting_for_sd");
|
||||
|
||||
sd_update_check(DOWNGRADE_HELD);
|
||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||
boot_select_and_jump(); /* при успехе не возвращается */
|
||||
attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
|
||||
#include "protocol.h"
|
||||
|
||||
#include "bsp/boot_state.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "cli.h"
|
||||
#include "recovery.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
|
|
@ -49,9 +51,12 @@ 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",
|
||||
"{\"type\":\"wdog\",\"armed\":%s,\"timeout_s\":%u,\"recovered\":%s,"
|
||||
"\"reset_count\":%u,\"threshold\":%u}\n",
|
||||
bsp_wdog_is_armed() ? "true" : "false",
|
||||
(unsigned) bsp_wdog_timeout_s(),
|
||||
bsp_wdog_caused_last_reset() ? "true" : "false");
|
||||
bsp_wdog_caused_last_reset() ? "true" : "false",
|
||||
(unsigned) bsp_boot_attempt_count(),
|
||||
(unsigned) RECOVERY_DEFAULT_THRESHOLD);
|
||||
cli_send(buf);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,13 +63,19 @@ void protocol_send_error(const char *p_code);
|
|||
void protocol_send_status(const char *p_state);
|
||||
|
||||
/**
|
||||
* @brief Отправить статус аппаратного watchdog.
|
||||
* @brief Отправить статус аппаратного watchdog и счётчика попыток загрузки
|
||||
* (Фаза 6).
|
||||
*
|
||||
* Формат: {"type":"wdog","armed":true,"timeout_s":10,"recovered":false}
|
||||
* Формат: {"type":"wdog","armed":true,"timeout_s":10,"recovered":false,
|
||||
* "reset_count":0,"threshold":3}
|
||||
* - armed — watchdog взведён (bsp_wdog_init выполнен);
|
||||
* - timeout_s — сконфигурированный таймаут в секундах;
|
||||
* - recovered — ПОСЛЕДНИЙ сброс МК был по таймауту watchdog (плата
|
||||
* восстановилась после зависания).
|
||||
* восстановилась после зависания);
|
||||
* - reset_count — bsp_boot_attempt_count(): сколько попыток подряд без
|
||||
* подтверждения здоровья (health-mark/новая установка), 0
|
||||
* сразу после POR;
|
||||
* - threshold — RECOVERY_DEFAULT_THRESHOLD: порог фолбэка/recovery.
|
||||
*
|
||||
* Эмитится один раз на старте, если recovered, и по команде "wdog".
|
||||
*/
|
||||
|
|
|
|||
75
firmware/bootloader/src/recovery.c
Normal file
75
firmware/bootloader/src/recovery.c
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @file recovery.c
|
||||
* @brief Реализация — см. recovery.h.
|
||||
*/
|
||||
|
||||
#include "recovery.h"
|
||||
|
||||
/* ── Определение активного слота ──────────────────────────────────────────
|
||||
* Тот же приём, что find_active_slot() в update_policy.c (private там) —
|
||||
* не шарим напрямую, копия минимальна и независимо тестируется здесь же,
|
||||
* как уже принято в проекте для мелкой логики между модулями. */
|
||||
|
||||
typedef struct
|
||||
{
|
||||
bool have_active;
|
||||
update_policy_slot_t active_slot;
|
||||
struct image_version active_ver;
|
||||
} active_slot_info_t;
|
||||
|
||||
static active_slot_info_t find_active_slot(const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b)
|
||||
{
|
||||
active_slot_info_t info = { .have_active = false };
|
||||
|
||||
if (p_slot_a->valid)
|
||||
{
|
||||
info.have_active = true;
|
||||
info.active_slot = UPDATE_POLICY_SLOT_A;
|
||||
info.active_ver = p_slot_a->version;
|
||||
}
|
||||
|
||||
if (p_slot_b->valid &&
|
||||
(!info.have_active || image_version_compare(&p_slot_b->version, &info.active_ver) > 0))
|
||||
{
|
||||
info.have_active = true;
|
||||
info.active_slot = UPDATE_POLICY_SLOT_B;
|
||||
info.active_ver = p_slot_b->version;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
static bool other_slot_valid(update_policy_slot_t active_slot,
|
||||
const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b)
|
||||
{
|
||||
return (active_slot == UPDATE_POLICY_SLOT_A) ? p_slot_b->valid : p_slot_a->valid;
|
||||
}
|
||||
|
||||
/* ── Публичный API ─────────────────────────────────────────────────────── */
|
||||
|
||||
recovery_decision_t recovery_decide(uint32_t attempt_count, uint32_t threshold,
|
||||
const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b, bool btn2_held)
|
||||
{
|
||||
if (btn2_held)
|
||||
{
|
||||
return (recovery_decision_t) { .action = RECOVERY_ENTER_RECOVERY_MODE };
|
||||
}
|
||||
|
||||
if (attempt_count < threshold)
|
||||
{
|
||||
return (recovery_decision_t) { .action = RECOVERY_NORMAL_BOOT };
|
||||
}
|
||||
|
||||
active_slot_info_t active = find_active_slot(p_slot_a, p_slot_b);
|
||||
|
||||
if (active.have_active && other_slot_valid(active.active_slot, p_slot_a, p_slot_b))
|
||||
{
|
||||
return (recovery_decision_t) { .action = RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER,
|
||||
.active_slot = active.active_slot };
|
||||
}
|
||||
|
||||
return (recovery_decision_t) { .action = RECOVERY_ENTER_RECOVERY_MODE };
|
||||
}
|
||||
72
firmware/bootloader/src/recovery.h
Normal file
72
firmware/bootloader/src/recovery.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* @file recovery.h
|
||||
* @brief Чистая логика решения "что делать с этой попыткой загрузки" —
|
||||
* без аппаратных зависимостей (flash/SRC_GPR), полностью
|
||||
* host-тестируема.
|
||||
*
|
||||
* См. firmware/bootloader/PLAN.md, Фаза 6, разбивка 6a.
|
||||
*/
|
||||
|
||||
#ifndef RECOVERY_H_
|
||||
#define RECOVERY_H_
|
||||
|
||||
#include "update_policy.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum
|
||||
{
|
||||
RECOVERY_NORMAL_BOOT = 0,
|
||||
RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER,
|
||||
RECOVERY_ENTER_RECOVERY_MODE,
|
||||
} recovery_action_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
recovery_action_t action;
|
||||
|
||||
/**
|
||||
* Слот, который нужно стереть перед прыжком. Значим только при
|
||||
* action == RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER.
|
||||
*/
|
||||
update_policy_slot_t active_slot;
|
||||
} recovery_decision_t;
|
||||
|
||||
/**
|
||||
* @brief Решить, что делать с текущей попыткой загрузки — таксономия
|
||||
* отказов Фазы 6 (классы B/C/D; класс A закрывается штатным revert
|
||||
* MCUboot без участия этой функции).
|
||||
*
|
||||
* Правила (приоритет сверху вниз):
|
||||
* - btn2_held → RECOVERY_ENTER_RECOVERY_MODE. Ручной триггер главнее
|
||||
* счётчика — оператор может войти в recovery в любой момент, независимо
|
||||
* от истории попыток.
|
||||
* - attempt_count < threshold → RECOVERY_NORMAL_BOOT. Обычная загрузка,
|
||||
* ничего не предпринимаем (счётчик инкрементирует вызывающий код перед
|
||||
* прыжком).
|
||||
* - attempt_count >= threshold и есть "активный" слот (валидный, более
|
||||
* высокой версии — тот же приём, что update_policy_decide()) — он и
|
||||
* есть подозреваемый в зависании:
|
||||
* - другой слот валиден (есть фолбэк) → RECOVERY_ERASE_ACTIVE_THEN_
|
||||
* BOOT_OTHER(active_slot). Вызывающий код стирает active_slot и
|
||||
* обнуляет счётчик — boot_go() сам выберет оставшийся слот.
|
||||
* - другого валидного слота нет (стирать нечего — иначе ноль рабочих
|
||||
* слотов) → RECOVERY_ENTER_RECOVERY_MODE.
|
||||
* - attempt_count >= threshold и активного слота нет вообще →
|
||||
* RECOVERY_ENTER_RECOVERY_MODE (нечего анализировать, нечего стирать).
|
||||
*
|
||||
* @param[in] attempt_count Текущее значение счётчика попыток (bsp_boot_attempt_count()).
|
||||
* @param[in] threshold Порог срабатывания фолбэка/recovery (см. RECOVERY_DEFAULT_THRESHOLD).
|
||||
* @param[in] p_slot_a Состояние Slot A.
|
||||
* @param[in] p_slot_b Состояние Slot Б.
|
||||
* @param[in] btn2_held BSP_BUTTON_2 удержана на старте — ручной вход в recovery.
|
||||
*/
|
||||
recovery_decision_t recovery_decide(uint32_t attempt_count, uint32_t threshold,
|
||||
const update_policy_slot_state_t *p_slot_a,
|
||||
const update_policy_slot_state_t *p_slot_b, bool btn2_held);
|
||||
|
||||
/** @brief Дефолтный порог — 3 сброса подряд до фолбэка/recovery. */
|
||||
#define RECOVERY_DEFAULT_THRESHOLD 3U
|
||||
|
||||
#endif /* RECOVERY_H_ */
|
||||
|
|
@ -7,9 +7,11 @@ bootloader и заглушки слотов (`test_stub`) уже знакомы
|
|||
Все команды — из корня репозитория (`tft_manufacture_test/`). Сборка и host-тесты — в devcontainer;
|
||||
прошивка/отладка/проверки на железе — на стороне пользователя.
|
||||
|
||||
**Статус: ✅ все 5 сценариев пройдены на реальной плате (2026-07-10).** Историю бага
|
||||
card-detect и его разрешение (чтение USDHC `PRES_STATE.CINST`) см. в
|
||||
[../DEBUG_LOG_PHASE3_SD.md](../DEBUG_LOG_PHASE3_SD.md).
|
||||
**Статус: ✅ все 5 сценариев §8 пройдены на реальной плате (2026-07-10, повторно — с раундом 4 и
|
||||
watchdog — 2026-07-13); watchdog §9 (9.1–9.5) пройден 2026-07-13.** Историю бага card-detect и его
|
||||
разрешение (чтение USDHC `PRES_STATE.CINST`) см. в [../DEBUG_LOG_PHASE3_SD.md](../DEBUG_LOG_PHASE3_SD.md).
|
||||
По пути найден и исправлен баг самого чек-листа (не кода) — сценарий 1 использовал не тот линкованный
|
||||
стаб, см. [../PLAN.md](../PLAN.md), раздел «Найден и исправлен баг чек-листа».
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ add_sdk_driver(edma fsl_edma.c)
|
|||
add_sdk_driver(dmamux fsl_dmamux.c)
|
||||
add_sdk_driver(xbara fsl_xbara.c)
|
||||
add_sdk_driver(wdog fsl_wdog.c)
|
||||
add_sdk_driver(src fsl_src.c)
|
||||
|
||||
# Драйверы которые зависят от clock
|
||||
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)
|
||||
|
|
|
|||
|
|
@ -263,6 +263,22 @@ add_host_test(
|
|||
${PROJECT_SOURCE_DIR}/firmware/bootloader/src
|
||||
${MCUBOOT_BOOTUTIL_INCLUDES})
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# recovery — чистая логика решения "что делать с этой попыткой загрузки"
|
||||
# (Фаза 6, разбивка 6a). Переиспользует update_policy_slot_state_t/
|
||||
# image_version_compare() из update_policy.c — тот же набор INCLUDES.
|
||||
# -----------------------------------------------------------------------------
|
||||
add_host_test(
|
||||
NAME
|
||||
test_recovery
|
||||
SOURCES
|
||||
recovery/test_recovery.c
|
||||
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/recovery.c
|
||||
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/update_policy.c
|
||||
INCLUDES
|
||||
${PROJECT_SOURCE_DIR}/firmware/bootloader/src
|
||||
${MCUBOOT_BOOTUTIL_INCLUDES})
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# slot_version — read-only пик версии слота через bootutil_img_validate()
|
||||
# (Фаза 3). Реальный bootutil + TinyCrypt поверх fake_flash_map_backend.c —
|
||||
|
|
|
|||
48
tests/host/recovery/README.md
Normal file
48
tests/host/recovery/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# test_recovery
|
||||
|
||||
## Модуль под тестом
|
||||
|
||||
`firmware/bootloader/src/recovery.c` (`recovery.h`) — чистая логика решения
|
||||
«что делать с этой попыткой загрузки» (таксономия отказов Фазы 6, классы
|
||||
B/C/D), без аппаратных зависимостей (`SRC_GPR`/flash). См.
|
||||
`firmware/bootloader/PLAN.md`.
|
||||
|
||||
## Категория
|
||||
|
||||
A — платформонезависимый модуль. Переиспользует `update_policy_slot_state_t`
|
||||
и `image_version_compare()` из `update_policy.c` (линкуется вместе с тестом),
|
||||
сам `bootutil` не линкуется.
|
||||
|
||||
## Моки
|
||||
|
||||
Нет — тестируется напрямую, без фейков/стабов.
|
||||
|
||||
## Что проверяется
|
||||
|
||||
- **BSP_BUTTON_2 удержана** — `RECOVERY_ENTER_RECOVERY_MODE` независимо от
|
||||
счётчика попыток и состояния слотов (ручной триггер главнее счётчика).
|
||||
- **Счётчик ниже порога** — `RECOVERY_NORMAL_BOOT`, независимо от слотов.
|
||||
- **Счётчик на пороге, есть фолбэк** (второй слот валиден) —
|
||||
`RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER` с правильным `active_slot`
|
||||
(активный — слот с более высокой версией, тот же приём, что в
|
||||
`update_policy_decide()`), проверено для обеих сторон (А активен/Б
|
||||
активен).
|
||||
- **Счётчик выше порога** — фолбэк продолжает срабатывать (не только ровно
|
||||
на пороге).
|
||||
- **Счётчик на пороге, фолбэка нет** (единственный валидный слот, или ни
|
||||
одного валидного) — `RECOVERY_ENTER_RECOVERY_MODE` (стирать единственный
|
||||
рабочий слот нельзя).
|
||||
|
||||
## Гарантии
|
||||
|
||||
- Класс A (незавершённая установка, `copy_done!=image_ok`) этой функцией не
|
||||
закрывается — целиком авто-откат штатным revert MCUboot, без участия
|
||||
загрузчика.
|
||||
- Фолбэк никогда не оставляет ноль валидных слотов — стирается только
|
||||
активный, и только когда другой уже подтверждён валидным.
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
ctest --preset host-debug-test -R test_recovery -V
|
||||
```
|
||||
156
tests/host/recovery/test_recovery.c
Normal file
156
tests/host/recovery/test_recovery.c
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* @file test_recovery.c
|
||||
* @brief Host-тесты чистой логики recovery_decide().
|
||||
*
|
||||
* Без аппаратных зависимостей (SRC_GPR/flash) — сценарии зеркалят таксономию
|
||||
* отказов Фазы 6 (firmware/bootloader/PLAN.md).
|
||||
*/
|
||||
|
||||
#include "recovery.h"
|
||||
#include "unity.h"
|
||||
|
||||
/* ── Вспомогательные конструкторы ─────────────────────────────────────── */
|
||||
|
||||
static struct image_version make_version(uint8_t major, uint8_t minor, uint16_t revision)
|
||||
{
|
||||
struct image_version ver = {
|
||||
.iv_major = major,
|
||||
.iv_minor = minor,
|
||||
.iv_revision = revision,
|
||||
.iv_build_num = 0,
|
||||
};
|
||||
return ver;
|
||||
}
|
||||
|
||||
static update_policy_slot_state_t make_valid_slot(struct image_version ver)
|
||||
{
|
||||
update_policy_slot_state_t slot = { .valid = true, .version = ver };
|
||||
return slot;
|
||||
}
|
||||
|
||||
static const update_policy_slot_state_t K_INVALID_SLOT = { .valid = false };
|
||||
static const uint32_t K_THRESHOLD = 3U;
|
||||
|
||||
void setUp(void)
|
||||
{
|
||||
}
|
||||
|
||||
void tearDown(void)
|
||||
{
|
||||
}
|
||||
|
||||
/* ── BTN_2 — главнее счётчика ─────────────────────────────────────────── */
|
||||
|
||||
void test_btn2_held_enters_recovery_even_with_zero_attempts(void)
|
||||
{
|
||||
recovery_decision_t d =
|
||||
recovery_decide(0U, K_THRESHOLD, &K_INVALID_SLOT, &K_INVALID_SLOT, true);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_ENTER_RECOVERY_MODE, d.action);
|
||||
}
|
||||
|
||||
void test_btn2_held_enters_recovery_even_with_healthy_slots(void)
|
||||
{
|
||||
update_policy_slot_state_t slot_a = make_valid_slot(make_version(1, 0, 0));
|
||||
update_policy_slot_state_t slot_b = make_valid_slot(make_version(2, 0, 0));
|
||||
|
||||
recovery_decision_t d = recovery_decide(0U, K_THRESHOLD, &slot_a, &slot_b, true);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_ENTER_RECOVERY_MODE, d.action);
|
||||
}
|
||||
|
||||
/* ── Ниже порога — обычная загрузка ──────────────────────────────────── */
|
||||
|
||||
void test_below_threshold_is_normal_boot(void)
|
||||
{
|
||||
recovery_decision_t d = recovery_decide(K_THRESHOLD - 1U, K_THRESHOLD, &K_INVALID_SLOT,
|
||||
&K_INVALID_SLOT, false);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_NORMAL_BOOT, d.action);
|
||||
}
|
||||
|
||||
void test_zero_attempts_is_normal_boot(void)
|
||||
{
|
||||
update_policy_slot_state_t slot_a = make_valid_slot(make_version(1, 0, 0));
|
||||
|
||||
recovery_decision_t d = recovery_decide(0U, K_THRESHOLD, &slot_a, &K_INVALID_SLOT, false);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_NORMAL_BOOT, d.action);
|
||||
}
|
||||
|
||||
/* ── На пороге, класс B, есть фолбэк ─────────────────────────────────── */
|
||||
|
||||
void test_at_threshold_with_fallback_erases_active_slot_a(void)
|
||||
{
|
||||
update_policy_slot_state_t slot_a = make_valid_slot(make_version(2, 0, 0)); /* активный */
|
||||
update_policy_slot_state_t slot_b = make_valid_slot(make_version(1, 0, 0)); /* фолбэк */
|
||||
|
||||
recovery_decision_t d = recovery_decide(K_THRESHOLD, K_THRESHOLD, &slot_a, &slot_b, false);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER, d.action);
|
||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, d.active_slot);
|
||||
}
|
||||
|
||||
void test_at_threshold_with_fallback_erases_active_slot_b(void)
|
||||
{
|
||||
update_policy_slot_state_t slot_a = make_valid_slot(make_version(1, 0, 0)); /* фолбэк */
|
||||
update_policy_slot_state_t slot_b = make_valid_slot(make_version(2, 0, 0)); /* активный */
|
||||
|
||||
recovery_decision_t d = recovery_decide(K_THRESHOLD, K_THRESHOLD, &slot_a, &slot_b, false);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER, d.action);
|
||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_B, d.active_slot);
|
||||
}
|
||||
|
||||
void test_above_threshold_still_triggers_fallback(void)
|
||||
{
|
||||
update_policy_slot_state_t slot_a = make_valid_slot(make_version(2, 0, 0));
|
||||
update_policy_slot_state_t slot_b = make_valid_slot(make_version(1, 0, 0));
|
||||
|
||||
recovery_decision_t d = recovery_decide(K_THRESHOLD + 5U, K_THRESHOLD, &slot_a, &slot_b, false);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER, d.action);
|
||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, d.active_slot);
|
||||
}
|
||||
|
||||
/* ── На пороге, класс C — откатываться некуда ────────────────────────── */
|
||||
|
||||
void test_at_threshold_without_fallback_enters_recovery(void)
|
||||
{
|
||||
update_policy_slot_state_t slot_a = make_valid_slot(make_version(1, 0, 0)); /* активный, одинокий */
|
||||
|
||||
recovery_decision_t d =
|
||||
recovery_decide(K_THRESHOLD, K_THRESHOLD, &slot_a, &K_INVALID_SLOT, false);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_ENTER_RECOVERY_MODE, d.action);
|
||||
}
|
||||
|
||||
void test_at_threshold_with_no_valid_slots_enters_recovery(void)
|
||||
{
|
||||
recovery_decision_t d =
|
||||
recovery_decide(K_THRESHOLD, K_THRESHOLD, &K_INVALID_SLOT, &K_INVALID_SLOT, false);
|
||||
|
||||
TEST_ASSERT_EQUAL(RECOVERY_ENTER_RECOVERY_MODE, d.action);
|
||||
}
|
||||
|
||||
/* ── Точка входа ───────────────────────────────────────────────────────── */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
||||
RUN_TEST(test_btn2_held_enters_recovery_even_with_zero_attempts);
|
||||
RUN_TEST(test_btn2_held_enters_recovery_even_with_healthy_slots);
|
||||
|
||||
RUN_TEST(test_below_threshold_is_normal_boot);
|
||||
RUN_TEST(test_zero_attempts_is_normal_boot);
|
||||
|
||||
RUN_TEST(test_at_threshold_with_fallback_erases_active_slot_a);
|
||||
RUN_TEST(test_at_threshold_with_fallback_erases_active_slot_b);
|
||||
RUN_TEST(test_above_threshold_still_triggers_fallback);
|
||||
|
||||
RUN_TEST(test_at_threshold_without_fallback_enters_recovery);
|
||||
RUN_TEST(test_at_threshold_with_no_valid_slots_enters_recovery);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Reference in a new issue