# bootloader: Phase 6 - done
This commit is contained in:
parent
cc6f29b8f4
commit
8cb1cb143c
16 changed files with 1333 additions and 633 deletions
|
|
@ -128,11 +128,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mcuboot-stub-debug",
|
"name": "mcuboot-stub-debug",
|
||||||
"displayName": "mcuboot slot stub (Фаза 2 верификация) — Debug",
|
"displayName": "mcuboot slot stub (Фаза 2/6 верификация) — Debug",
|
||||||
"configurePreset": "Debug",
|
"configurePreset": "Debug",
|
||||||
"targets": [
|
"targets": [
|
||||||
"test_slot_stub_a",
|
"test_slot_stub_a",
|
||||||
"test_slot_stub_b"
|
"test_slot_stub_b",
|
||||||
|
"test_slot_stub_a_hang",
|
||||||
|
"test_slot_stub_b_hang",
|
||||||
|
"test_slot_stub_a_confirm_hang",
|
||||||
|
"test_slot_stub_b_confirm_hang"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
299
firmware/bootloader/BOOT_FLOW.md
Normal file
299
firmware/bootloader/BOOT_FLOW.md
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
# Загрузчик TFT — принцип работы
|
||||||
|
|
||||||
|
Документ описывает поведение загрузчика: как устроена память, как выбирается и обновляется образ
|
||||||
|
приложения, правила версий, даунгрейд и режим восстановления.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Карта памяти
|
||||||
|
|
||||||
|
Приложение хранится в QSPI NOR Flash в **двух слотах** — A и Б. Каждый слот содержит **полную,
|
||||||
|
самостоятельно валидную** копию приложения. Загрузчик занимает начало flash, за слотами идёт
|
||||||
|
отдельная область под ассеты (спрайты, звук), которая к процессу загрузки отношения не имеет.
|
||||||
|
|
||||||
|
| Область | Начало | Размер |
|
||||||
|
| ------------------------ | ------------ | ------------- |
|
||||||
|
| Загрузчик | `0x60000000` | 256 КБ |
|
||||||
|
| Slot A | `0x60040000` | 2 МБ |
|
||||||
|
| Slot Б | `0x60240000` | 2 МБ |
|
||||||
|
| Файловая система ассетов | `0x60440000` | до конца чипа |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
BL["Загрузчик — 256 КБ<br/>0x60000000"]
|
||||||
|
SA["Slot A — 2 МБ<br/>0x60040000"]
|
||||||
|
SB["Slot Б — 2 МБ<br/>0x60240000"]
|
||||||
|
FS["Файловая система ассетов<br/>0x60440000 … конец чипа"]
|
||||||
|
BL --- SA --- SB --- FS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Приложение исполняется прямо из flash** (XIP) — из того слота, который выбрал
|
||||||
|
загрузчик, без копирования в ОЗУ. Из этого следует ключевое свойство: образ жёстко привязан к адресу
|
||||||
|
своего слота при сборке. Поэтому **релиз приложения — это два бинарника на одну версию**: один собран
|
||||||
|
под адрес Slot A, второй — под адрес Slot Б. Образ, физически положенный не в «свой» слот, пройдёт
|
||||||
|
проверку подписи, но не запустится.
|
||||||
|
|
||||||
|
Два слота нужны для безопасного обновления: пока приложение работает из одного слота, новый образ
|
||||||
|
пишется в другой. Рабочая копия никогда не затирается — на диске всегда есть чем загрузиться, даже
|
||||||
|
если обновление прервётся на середине.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Выбор образа при старте
|
||||||
|
|
||||||
|
На каждой подаче питания загрузчик решает, из какого слота запускать приложение.
|
||||||
|
|
||||||
|
**Слот считается кандидатом, только если он валиден целиком**: корректная сигнатура формата, целый
|
||||||
|
хэш содержимого и верная криптографическая подпись (ECDSA-P256). Битый или неподписанный слот
|
||||||
|
игнорируется.
|
||||||
|
|
||||||
|
Правила выбора:
|
||||||
|
- Оба слота валидны → активным становится слот с **большей версией**.
|
||||||
|
- Валиден только один → он и активен.
|
||||||
|
- Ни одного валидного → активного слота нет, загрузчик переходит в ожидание microSD.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
START([Подача питания]) --> CHECK["Проверить оба слота:<br/>сигнатура + хэш + подпись"]
|
||||||
|
CHECK --> CMP{Сколько валидных?}
|
||||||
|
CMP -->|Оба| HIGHER["Активный = слот<br/>с большей версией"]
|
||||||
|
CMP -->|Один| ONE["Активный = он"]
|
||||||
|
CMP -->|Ни одного| NONE["Ожидание microSD"]
|
||||||
|
HIGHER --> JUMP([Запуск приложения])
|
||||||
|
ONE --> JUMP
|
||||||
|
```
|
||||||
|
|
||||||
|
**Защитная сеть от битого обновления.** Свежеустановленный образ считается «непроверенным», пока сам
|
||||||
|
не подтвердит своё здоровье в рантайме. Если непроверенный образ запустился, но так и не подтвердился
|
||||||
|
(например, завис на старте) — при следующей загрузке он трактуется как неудавшееся обновление и
|
||||||
|
**автоматически стирается**, а загрузчик откатывается на прежний слот. Приложение обязано подтвердить
|
||||||
|
себя один раз, доказав работоспособность.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Обновление через microSD
|
||||||
|
|
||||||
|
**Единственный полевой канал обновления — карта microSD.** Загрузчик ищет в корне карты файл
|
||||||
|
`TFT_APP.BIN` — подписанный образ приложения. Карта проверяется при старте и периодически (примерно
|
||||||
|
раз в 1.5 с), пока загрузчик находится в ожидании, — карту можно вставить уже после включения.
|
||||||
|
|
||||||
|
Приёмка кандидата — **двухступенчатая**:
|
||||||
|
1. **Проверка заголовка** (сигнатура формата + версия) прямо с карты, до касания flash — этого
|
||||||
|
достаточно, чтобы решить «ставить или пропустить».
|
||||||
|
2. **Полная криптографическая проверка** — уже после записи в целевой слот. Если подпись битая,
|
||||||
|
только что записанный слот просто не будет выбран при загрузке, и загрузчик останется на прежнем
|
||||||
|
валидном образе.
|
||||||
|
|
||||||
|
Запись идёт **потоком по частям**, каждая записанная часть немедленно вычитывается обратно и
|
||||||
|
сверяется — битая страница ловится сразу.
|
||||||
|
|
||||||
|
**Целевой слот установки — всегда НЕ активный.** Работающий/загружаемый слот не перезаписывается
|
||||||
|
никогда. Если активного слота нет вообще (чистая плата) — по умолчанию Slot A.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
CARD([microSD + TFT_APP.BIN]) --> HDR["Пик заголовка:<br/>сигнатура + версия"]
|
||||||
|
HDR -->|Сигнатура битая| REJ1["Отклонить<br/>(candidate invalid)"]
|
||||||
|
HDR -->|OK| DEC{Решение по версии}
|
||||||
|
DEC -->|Ставить| WRITE["Записать в НЕактивный слот:<br/>стереть → поток + verify"]
|
||||||
|
DEC -->|Пропустить| SKIP["Пропустить<br/>(update skipped)"]
|
||||||
|
WRITE --> CRYPTO{"Крипто-проверка<br/>записанного слота"}
|
||||||
|
CRYPTO -->|OK| DONE([Установлено → загрузка])
|
||||||
|
CRYPTO -->|Подпись битая| REJ2["Отклонить<br/>(install rejected)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Заводской сценарий** — частный случай этой же логики: чистая плата с одним загрузчиком, оба слота
|
||||||
|
пусты. Загрузчик ждёт SD, при появлении `TFT_APP.BIN` ставит его в Slot A и загружается. Никакого
|
||||||
|
отдельного механизма первичной заливки нет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Правила версий
|
||||||
|
|
||||||
|
Версия образа — **major.minor.revision**. Номер сборки (build) в сравнении **не участвует**: два
|
||||||
|
образа, отличающиеся только номером сборки, считаются равными.
|
||||||
|
|
||||||
|
Версия используется дважды:
|
||||||
|
- при **выборе** активного слота — побеждает бо́льшая версия;
|
||||||
|
- при **решении об установке** кандидата с SD.
|
||||||
|
|
||||||
|
Решение по кандидату (без удержания кнопки):
|
||||||
|
|
||||||
|
| Кандидат относительно активного | Действие |
|
||||||
|
| ------------------------------- | ---------------------------- |
|
||||||
|
| Строго новее | Установить в неактивный слот |
|
||||||
|
| Активного слота нет вообще | Установить в Slot A |
|
||||||
|
| Старше или равен | Пропустить |
|
||||||
|
|
||||||
|
При штатном обновлении (кандидат новее) прежний активный слот **не стирается** — он естественным
|
||||||
|
образом проиграет сравнение версий при следующей загрузке, новый образ победит сам.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Даунгрейд
|
||||||
|
|
||||||
|
Установить образ **старее** уже стоящего можно только с помощью оператора: **удержать `BTN 1` в
|
||||||
|
момент подачи питания**. Кнопка считывается один раз на старте и действует всю сессию.
|
||||||
|
|
||||||
|
Ключевой момент: при обычном даунгрейде записать старый образ в свободный слот **недостаточно** —
|
||||||
|
прежний (более новый) активный слот остался бы валиден и снова победил бы по версии, и даунгрейд
|
||||||
|
физически лёг бы на flash, но не загрузился. Поэтому при форсированном даунгрейде прежний активный
|
||||||
|
слот **стирается** — но только **после** того, как новый образ уже записан и подтверждён валидным.
|
||||||
|
На диске никогда не бывает нуля рабочих слотов даже на середине операции.
|
||||||
|
|
||||||
|
| Условие | Действие |
|
||||||
|
| ---------------------------------- | ----------------------------------------------------- |
|
||||||
|
| Кандидат старше + `BTN 1` удержана | Установить в свободный слот, стереть прежний активный |
|
||||||
|
| Кандидат равен активному + `BTN 1` | Пропустить (переустановку той же версии не форсируем) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Режим восстановления (Recovery Mode)
|
||||||
|
|
||||||
|
Назначение — не дать полевой плате превратиться в «кирпич», если уже установленный образ зависает в
|
||||||
|
рантайме, и дать оператору ручной аварийный вход.
|
||||||
|
|
||||||
|
### Аппаратный сторож
|
||||||
|
|
||||||
|
Плата защищена аппаратным watchdog с таймаутом **10 секунд**. Если управление зависает где-либо
|
||||||
|
(включая рантайм приложения), через 10 с происходит аппаратный сброс. Watchdog взводится один раз и
|
||||||
|
до перезагрузки по питанию не выключается — он «переживает» переход в приложение, поэтому приложение
|
||||||
|
обязано периодически его «кормить». Зависание → гарантированный сброс, а не вечный локап.
|
||||||
|
|
||||||
|
### Счётчик и порог
|
||||||
|
|
||||||
|
Число **подряд идущих** watchdog-сбросов хранится в регистре, который переживает тёплый/watchdog-сброс
|
||||||
|
и обнуляется только при настоящей подаче питания (POR). Счётчик обнуляется также при успешной
|
||||||
|
установке нового образа и при откате на фолбэк. Порог срабатывания — **3** сброса подряд.
|
||||||
|
|
||||||
|
### Классификация отказов и их обработка
|
||||||
|
|
||||||
|
| Класс | Ситуация | Что срабатывает |
|
||||||
|
| ----- | ----------------------------------------------- | ----------------------------------------------------- |
|
||||||
|
| **A** | Новый образ завис, ещё не подтвердив себя | Watchdog-сброс + автоматический откат на прежний слот |
|
||||||
|
| **B** | Уже подтверждённый образ завис в рантайме | Счётчик сбросов достиг порога → фолбэк на второй слот |
|
||||||
|
| **C** | Откатываться некуда (единственный/оба зависают) | Recovery Mode |
|
||||||
|
| **D** | Оператор хочет чистый старт вручную | Recovery Mode по `BTN 2` |
|
||||||
|
|
||||||
|
Класс A — самый частый — закрыт полностью автоматически: откат непроверенного образа не требует ни
|
||||||
|
счётчика, ни вмешательства. Класс B ловит то, что откат не покрывает (образ-то подтверждён): после
|
||||||
|
порога зависший слот стирается, и загружается второй, если он валиден.
|
||||||
|
|
||||||
|
### Решение при старте
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
S([Начало попытки]) --> BTN{BTN 2 удержана?}
|
||||||
|
BTN -->|Да| REC[Recovery Mode]
|
||||||
|
BTN -->|Нет| CNT{"Счётчик сбросов<br/>≥ порога (3)?"}
|
||||||
|
CNT -->|Нет| NORM[Обычная загрузка]
|
||||||
|
CNT -->|Да| FB{Второй слот валиден?}
|
||||||
|
FB -->|Да| ERASE["Стереть зависший слот →<br/>обнулить счётчик →<br/>загрузить второй"]
|
||||||
|
FB -->|Нет| REC
|
||||||
|
```
|
||||||
|
|
||||||
|
`BTN 2` проверяется **первым** — приоритет ручного входа выше и счётчика, и обычной загрузки, и
|
||||||
|
кнопки даунгрейда. Если `BTN 2` удержана, обычный путь загрузки не выполняется вообще, даже при
|
||||||
|
наличии валидного образа.
|
||||||
|
|
||||||
|
### Поведение в Recovery Mode
|
||||||
|
|
||||||
|
- **Прыжок в приложение подавлен** — это само по себе разрывает цикл зависаний.
|
||||||
|
- **Отдельная LED-индикация**: оба светодиода мигают синхронно, 100 мс включено / 100 мс выключено —
|
||||||
|
явно отличается от heartbeat и рабочих паттернов приложения.
|
||||||
|
- **Статус по USB**: `recovery_mode`.
|
||||||
|
- **Ослабленный контроль версий**: принимается **любой** подписанный образ с SD — без сравнения
|
||||||
|
версий и без кнопки. Проверка подписи при этом сохраняется всегда.
|
||||||
|
- При найденном валидном образе — **оба слота стираются**, образ ставится в Slot A, происходит
|
||||||
|
автоматический прыжок. «Чистый борт» достигается ровно тогда, когда есть чем заменить.
|
||||||
|
|
||||||
|
### Семантика «на одну сессию»
|
||||||
|
|
||||||
|
Счётчик не сохраняется во flash. На подаче питания он обнуляется, поэтому зависший слот **пробуется
|
||||||
|
заново** — если зависание было случайным (транзиентным), плата получает новый шанс. Если зависание
|
||||||
|
детерминированное, оператор жмёт `BTN 2` и входит в recovery немедленно, не дожидаясь порога.
|
||||||
|
|
||||||
|
### Честная граница
|
||||||
|
|
||||||
|
Если образ стабильно работает, обнуляет счётчик (доказав здоровье), и лишь **потом** ловит редкий баг
|
||||||
|
(конкретный файл на SD, конкретное входное сообщение) — счётчик каждый раз обнуляется до зависания,
|
||||||
|
автопорог не накапливается, и цикл автоматически не ловится. Это принципиально: по таймеру не отличить
|
||||||
|
«здоров» от «здоров, но потом словил редкое». В таком случае плата видимо циклится (watchdog +
|
||||||
|
recovery-индикация это показывают), лечится SD-фиксом или `BTN 2`. Watchdog как минимум не даёт плате
|
||||||
|
зависнуть намертво.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Индикация и обратная связь
|
||||||
|
|
||||||
|
### Светодиоды
|
||||||
|
|
||||||
|
| Состояние | Паттерн |
|
||||||
|
| ---------------------- | ------------------------------------------------ |
|
||||||
|
| Загрузчик жив, ждёт SD | Один LED: короткий импульс ~50 мс, пауза ~450 мс |
|
||||||
|
| Приложение работает | Задаётся приложением |
|
||||||
|
| Recovery Mode | Оба LED синхронно: 100 мс вкл / 100 мс выкл |
|
||||||
|
|
||||||
|
### USB (виртуальный COM-порт)
|
||||||
|
|
||||||
|
Загрузчик поднимает USB-порт до обращения к SD, поэтому статусы видны, даже если оператор подключился
|
||||||
|
заранее. Обмен — текстовые JSON-строки.
|
||||||
|
|
||||||
|
Состояния (`status`):
|
||||||
|
|
||||||
|
| Значение | Когда |
|
||||||
|
| ---------------- | ------------------------------------------------- |
|
||||||
|
| `waiting_for_sd` | Нет валидного слота, ждём карту |
|
||||||
|
| `installing` | Принято решение установить кандидата, идёт запись |
|
||||||
|
| `update_skipped` | Кандидат отклонён по версии |
|
||||||
|
| `recovery_mode` | Плата в режиме восстановления |
|
||||||
|
|
||||||
|
Ошибки установки: `SD_CANDIDATE_INVALID` (битый заголовок), `SD_INSTALL_WRITE_FAILED` (сбой записи),
|
||||||
|
`SD_INSTALL_REJECTED` (записан, но подпись не прошла), `SD_DOWNGRADE_ERASE_FAILED`.
|
||||||
|
|
||||||
|
Статус сторожа (по запросу `wdog`): взведён ли watchdog, таймаут, был ли последний сброс по watchdog,
|
||||||
|
текущее значение счётчика сбросов и порог.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Полный жизненный цикл
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
state "Загрузка" as BOOT
|
||||||
|
state "Приложение" as APP
|
||||||
|
state "Ожидание SD" as WAIT
|
||||||
|
state "Установка" as INST
|
||||||
|
state "Recovery Mode" as REC
|
||||||
|
|
||||||
|
[*] --> BOOT: питание
|
||||||
|
BOOT --> APP: валидный образ выбран
|
||||||
|
BOOT --> WAIT: нет валидного слота
|
||||||
|
BOOT --> REC: порог сбросов / BTN 2
|
||||||
|
WAIT --> INST: TFT_APP.BIN найден
|
||||||
|
INST --> APP: установлено + прыжок
|
||||||
|
INST --> WAIT: отклонено по версии/подписи
|
||||||
|
APP --> BOOT: watchdog-сброс при зависании
|
||||||
|
REC --> INST: образ с SD (любая подписанная версия)
|
||||||
|
REC --> REC: ждём SD
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Сводка сценариев
|
||||||
|
|
||||||
|
| Ситуация | Поведение загрузчика |
|
||||||
|
| ------------------------------------------------------ | --------------------------------------------------------------------- |
|
||||||
|
| Оба слота валидны | Загрузка слота с бо́льшей версией |
|
||||||
|
| Валиден один слот | Загрузка его |
|
||||||
|
| Чистая плата, оба слота пусты | Ожидание SD (бессрочно), heartbeat |
|
||||||
|
| SD с образом новее активного | Установка в свободный слот → загрузка |
|
||||||
|
| SD с образом старше/равным, кнопка не нажата | Пропуск, загрузка прежнего |
|
||||||
|
| SD с образом старше, `BTN 1` удержана | Даунгрейд: установка + стирание прежнего активного → загрузка старого |
|
||||||
|
| SD с битым заголовком / битой подписью | Отклонение, прежний валидный слот не тронут |
|
||||||
|
| Новый образ завис, не подтвердившись (Класс A) | Watchdog-сброс → автоматический откат на прежний слот |
|
||||||
|
| Подтверждённый образ завис, есть второй слот (Класс B) | 3 сброса → стирание зависшего слота → загрузка второго |
|
||||||
|
| Зависает, откатываться некуда (Класс C) | Recovery Mode |
|
||||||
|
| Оператор удержал `BTN 2` при старте (Класс D) | Recovery Mode немедленно, обычная загрузка подавлена |
|
||||||
|
| В recovery вставлена SD с подписанным образом | Оба слота стёрты, образ в Slot A, автоматический прыжок |
|
||||||
|
| POR после recovery без `BTN 2` | Счётчик обнулён, зависший слот пробуется заново |
|
||||||
|
|
@ -1,516 +0,0 @@
|
||||||
# Фаза 3 — аппаратная верификация SD-пути: лог (РАЗРЕШЁН 2026-07-10)
|
|
||||||
|
|
||||||
**✅ РАЗРЕШЕНО (2026-07-10, раунд 3).** Настоящий фикс детекта — чтение **USDHC PRES_STATE.CINST**
|
|
||||||
(`USDHC_GetPresentStatusFlags` + `kUSDHC_CardInsertedFlag`) в `bsp_sd_is_inserted()`, а не GPIO. Это
|
|
||||||
подход (a) из самого первого лога, который тогда откатили как «CINST=0 даже со вставленной картой» —
|
|
||||||
но откатили ошибочно: CINST=0 читался потому, что в тот момент пин ещё не был на `USDHC1_CD_B`.
|
|
||||||
Все 5 сценариев Фазы 3 пройдены на железе. Детали — «Обновление 2026-07-10 (раунд 3)» ниже. Разделы
|
|
||||||
раундов 1–2 сохранены как история расследования (их выводы частично опровергнуты раундом 3 — см.
|
|
||||||
явные пометки).
|
|
||||||
|
|
||||||
<details><summary>История расследования (раунды 1–2, выводы частично опровергнуты)</summary>
|
|
||||||
|
|
||||||
**2026-07-10, раунд 1**: pinmux CD откачен на `GPIO2_IO28` + явный power-cycle карты. Аппаратно
|
|
||||||
проверено — симптом 1 (SD не вставлена) **не устранён**, симптом 3→4 похоже устранён. См.
|
|
||||||
«Обновление 2026-07-10» ниже. **⚠️ Вывод «GPIO2_IO28 — верный pinmux» опровергнут раундом 3: для
|
|
||||||
работающего PRSSTAT-чтения пин нужен на `USDHC1_CD_B`.**
|
|
||||||
|
|
||||||
**2026-07-10, раунд 2**: по прямому запросу пользователя — полное выравнивание SD-стека на
|
|
||||||
`TFT_BOOTLOADER`, не только pinmux. Три дополнительных расхождения найдены и закрыты (CD pad-config
|
|
||||||
байт-в-байт, SD_PWR side-effect в детекте, explicit HostInit/PollingCardInsert/power-cycle). См.
|
|
||||||
«Обновление 2026-07-10 (раунд 2)» ниже. **⚠️ CD pad-config и explicit-init остались в дереве и не
|
|
||||||
вредят; SD_PWR side-effect в `bsp_sd_is_inserted()` заменён финальным PRSSTAT-чтением раунда 3.**
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
Этот файл — снимок состояния расследования на момент передачи в отдельный тред. Код Фазы 3
|
|
||||||
(`update_policy`, `slot_version`, `sd_update`) спроектирован и host-протестирован отдельно (см.
|
|
||||||
[PLAN.md](PLAN.md), раздел «Фаза 3») и сам по себе не пересматривается. Проблема — в аппаратном
|
|
||||||
bring-up SD/USDHC на конкретной плате, всплывшая только на реальном железе.
|
|
||||||
|
|
||||||
## Симптомы (в порядке обнаружения)
|
|
||||||
|
|
||||||
1. **Чистая плата, оба слота пусты, SD карта отсутствует** → плата зависала внутри
|
|
||||||
`run_update → ... → micro_sd_disk_initialize → SD_PollingCardInsert`.
|
|
||||||
2. После правок ниже (текущее состояние pinmux, см. «Файлы» ниже) то же исходное условие (нет SD,
|
|
||||||
пустые слоты) стало вместо зависания давать **падение в `DefaultISR`**. Стек вызовов (VSCode
|
|
||||||
Cortex-Debug):
|
|
||||||
```
|
|
||||||
main → sd_update_check → run_update → bsp_sd_deinit → SD_HostDeinit →
|
|
||||||
SDMMCHOST_Deinit → SDMMCHOST_Reset → USDHC_SelectVoltage → <signal handler called> → DefaultISR
|
|
||||||
```
|
|
||||||
3. **SD вставлена при старте** → доходит до `jump_to_image` и успешно стартует (под отладчиком).
|
|
||||||
После этого: выключить питание, вынуть SD, включить снова → та же прошивка (slot-заглушка) **не
|
|
||||||
стартует** (возврат к симптому 1/2).
|
|
||||||
4. **SD оставлена в слоте**, повторный power cycle сразу после успешного случая (3) (то есть SD
|
|
||||||
физически всё ещё вставлена) → **зависание в `OSA_SemaphoreWait`**, отдельный стек:
|
|
||||||
```
|
|
||||||
main → sd_update_check → run_update → f_mount → mount_volume → disk_initialize →
|
|
||||||
microsd_disk_initialize → sd_disk_initialize → SD_CardInit → sdcard_init → SD_ReadStatus →
|
|
||||||
SD_Transfer → SDMMCHOST_TransferFunction → SDMMC_OSAEventWait → OSA_SemaphoreWait
|
|
||||||
```
|
|
||||||
Это происходит **до** попытки прыжка — на самой первой инициализации карты в этой сессии
|
|
||||||
питания. Прерывание, которое должно отпустить семафор (сигнал завершения транзакции), судя по
|
|
||||||
всему, не приходит.
|
|
||||||
|
|
||||||
Симптомы 2 и 4 — из одного и того же похода тестирования, оба неразобраны на момент передачи.
|
|
||||||
|
|
||||||
## Что уже пробовали (в хронологическом порядке)
|
|
||||||
|
|
||||||
| # | Правка | Результат |
|
|
||||||
|---|---|---|
|
|
||||||
| a | `bsp_sd_is_inserted()` / внутренний callback переведены на USDHC `PRES_STATE.CINST` вместо GPIO | Неверно: `CINST` читался как 0 даже при физически вставленной карте (вероятно, пин на тот момент не был замаплен на альт-функцию USDHC). Откачено. |
|
|
||||||
| b | `cleanup_before_jump()` в `boot_select.c` — отключение всех NVIC IRQ + `SysTick->CTRL=0` + `SCB->VTOR` перед прыжком | Пользователь подтвердил на железе: **«Никаких изменений. Всё также»**. Оставлено в коде (не вредит, независимо подтверждено паттерном `jump_to_application()` в референсе `TFT_BOOTLOADER`), но не является фиксом текущего бага. |
|
|
||||||
| c | Детект возвращён на GPIO-чтение; pinmux CD-пина переключён с `GPIO2_IO28` на `USDHC1_CD_B` (основание — референс `TFT7_RX_wOS`) | Дало симптом 2 (падение в `DefaultISR` вместо зависания) — то есть что-то изменилось, но проблема не ушла. Появилась **третья**, ещё более авторитетная референсная кодовая база (`TFT_BOOTLOADER`), которая прямо противоречит этому выбору pinmux (см. ниже) — **не проверено на железе**, стоит откатить в первую очередь. |
|
|
||||||
|
|
||||||
## Три референсных проекта — что каждый показывает про SD/CD
|
|
||||||
|
|
||||||
Все три, со слов пользователя, работали на **той же физической плате**. Расхождения между ними и
|
|
||||||
есть ядро проблемы.
|
|
||||||
|
|
||||||
1. **`TFT8_RX_wOS`** (`source/hal/sd.c/h`) — `bsp_sd_is_inserted()` через
|
|
||||||
`USDHC_GetPresentStatusFlags()` + `kUSDHC_CardInsertedFlag` (аппаратный регистр USDHC, не GPIO).
|
|
||||||
2. **`TFT7_RX_wOS`** (полный board-код) — CD-пин замаплен на `USDHC1_CD_B` (`board/pin_mux.c`), но
|
|
||||||
**читается** через обычный `GPIO_PinRead()` (`BOARD_SDCardGetDetectStatus()` в `board/sdmmc_config.c`) —
|
|
||||||
то есть USDHC-альтфункция выбрана, а не используется для самого чтения детекта.
|
|
||||||
3. **`TFT_BOOTLOADER`** (собственный, предыдущий, явно описанный пользователем как «без нареканий
|
|
||||||
работал на этой плате») — CD-пин замаплен на **простой `GPIO2_IO28`** (никакой USDHC-альтфункции
|
|
||||||
вообще), читается тем же `GPIO_PinRead`. Дополнительно, в `is_sdcard_present()`/`init_sd()`, есть
|
|
||||||
явное управление питанием карты: `SD_PWR_PIN` (GPIO1/19) выключается/включается в зависимости от
|
|
||||||
результата детекта, и явный **power-cycle карты** — `SD_SetCardPower(&g_sd, false)` →
|
|
||||||
`SD_SetCardPower(&g_sd, true)` — перед тем, как что-либо ещё делается с картой. Собственный
|
|
||||||
`jump_to_application()` тоже независимо переставляет `SCB->VTOR` перед прыжком (подтверждает
|
|
||||||
правку (b) выше, но никак не связано с текущим багом).
|
|
||||||
|
|
||||||
**Проблема**: (2) и (3) прямо противоречат друг другу по pinmux того же физического пина
|
|
||||||
(`GPIO_B1_12` / физический пин D13) — мультиплексирование эксклюзивно, физически не может быть верно
|
|
||||||
одновременно и то, и другое. (3) — самый доверенный источник (собственный, явно проверенный продакшн
|
|
||||||
на этой же плате, не просто «легаси-референс»), и именно его выбор (`GPIO2_IO28`, без USDHC-альтфункции)
|
|
||||||
**сейчас не применён** в дереве — стоит откатить в первую очередь.
|
|
||||||
|
|
||||||
## Текущее состояние кода (на момент передачи)
|
|
||||||
|
|
||||||
`bsp/sd/src/sd.c::bsp_sd_is_inserted()` — GPIO-чтение:
|
|
||||||
```c
|
|
||||||
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) ==
|
|
||||||
BOARD_SDMMC_SD_CD_INSERT_LEVEL;
|
|
||||||
```
|
|
||||||
|
|
||||||
`bsp/generated/sdmmc_config.c::BOARD_SD_Config()` — pinmux **на `USDHC1_CD_B`** (см. выше — под
|
|
||||||
вопросом):
|
|
||||||
```c
|
|
||||||
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U);
|
|
||||||
```
|
|
||||||
внутренний детект-callback — `sd_card_detect_gpio()` (GPIO, не USDHC-регистр) — сам механизм чтения
|
|
||||||
не менялся, менялся только pinmux.
|
|
||||||
|
|
||||||
`bsp/sd/src/sd.c::bsp_sd_init()`/`bsp_sd_deinit()` — **не делают explicit power-cycle карты**
|
|
||||||
(`SD_SetCardPower`) — в отличие от `TFT_BOOTLOADER::init_sd()`. `bsp_sd_init()` выставляет
|
|
||||||
`g_s_initialized = true` сразу после `BOARD_SD_Config()` (только конфигурация дескриптора хоста) —
|
|
||||||
**до** того, как реальный `SD_HostInit()` вообще вызывается (тот вызывается неявно позже, изнутри
|
|
||||||
`f_mount() → sd_disk_initialize() → SD_Init()`). Отсюда гипотеза по симптому 2 ниже.
|
|
||||||
|
|
||||||
`firmware/bootloader/src/sd_update.c::run_update()` — если `f_mount()` возвращает ошибку (не
|
|
||||||
зависает, а именно быстро проваливается), путь `(void) bsp_sd_deinit(); return;` (строка ~160)
|
|
||||||
вызывается напрямую, не через `cleanup:` — это ровно та точка, где произошло падение в симптоме 2.
|
|
||||||
|
|
||||||
## Рабочие гипотезы (не проверены на железе)
|
|
||||||
|
|
||||||
1. **Симптом 2 (DefaultISR, SD физически отсутствует)**: при текущем pinmux (`USDHC1_CD_B`)
|
|
||||||
`GPIO_PinRead()` того же физического пина может отдавать некорректное/неопределённое значение
|
|
||||||
(частый эффект на i.MX-подобных SoC — когда пин отдан альтернативной функции, вход GPIO-регистра
|
|
||||||
не гарантированно отражает реальный внешний уровень). Это дало бы **ложное срабатывание
|
|
||||||
"карта вставлена"** даже без карты → `sd_update_check()` пропускает наш собственный gate и
|
|
||||||
заходит в `run_update()` → `f_mount()` внутри тоже видит ложный "card present" (тот же callback)
|
|
||||||
→ пытается реальный протокол инициализации без карты → команды таймаутятся (не висят вечно, в
|
|
||||||
отличие от `SD_PollingCardInsert`) → `f_mount()` возвращает ошибку → `bsp_sd_deinit()` вызывается,
|
|
||||||
но `SD_HostInit()` либо не был вызван, либо card/voltage-состояние осталось в промежуточном виде
|
|
||||||
→ `SDMMCHOST_Reset()/USDHC_SelectVoltage()` работает с невалидным состоянием → падение.
|
|
||||||
**Проверка**: откатить pinmux на `GPIO2_IO28` (референс `TFT_BOOTLOADER`) и повторить сценарий
|
|
||||||
"нет SD, пустые слоты" — если ложное срабатывание уйдёт, `run_update()` вообще не должен
|
|
||||||
вызываться, и падение исчезнет как следствие.
|
|
||||||
2. **Симптом 4 (OSA_SemaphoreWait, SD физически присутствует)** — не про card-detect вообще, а про
|
|
||||||
реальный обмен данными (`SD_ReadStatus`/`SD_Transfer`) — прерывание завершения транзакции не
|
|
||||||
приходит. Кандидат на причину: отсутствие power-cycle карты перед стартом обмена (в отличие от
|
|
||||||
`TFT_BOOTLOADER::init_sd()`) — карта могла остаться в состоянии, унаследованном от **предыдущей**
|
|
||||||
успешной сессии (та же карта, тот же физический power rail, если он не сбрасывается при
|
|
||||||
`bsp_sd_deinit()`/между сессиями), и не отвечать на новый протокол инициализации так, как ожидает
|
|
||||||
SDK-стек. **Не проверялось**, требует добавления explicit `SD_SetCardPower(false)` →
|
|
||||||
задержка → `SD_SetCardPower(true)` в `bsp_sd_init()` и повторного теста именно сценария «второй
|
|
||||||
power cycle подряд с той же картой».
|
|
||||||
3. Не проверено: действительно ли `bsp/qspi_flash`, `firmware_test`'ный стек SD (уже работающий в
|
|
||||||
продакшене через `test_usd.c`) отличается от bootloader-сценария именно временем между
|
|
||||||
power-on/детектом и первым обращением к карте — `firmware_test` ждёт explicit подтверждения
|
|
||||||
человека перед `usd_init()`, что даёт карте много времени "устояться" электрически; bootloader
|
|
||||||
делает это заметно быстрее после включения питания. Если гипотеза 1 не полностью объясняет
|
|
||||||
симптом 2, стоит проверить добавление небольшой выдержки после детекта перед `f_mount()`.
|
|
||||||
|
|
||||||
## Открытые вопросы для нового треда
|
|
||||||
|
|
||||||
> **РАЗРЕШЕНО 2026-07-10 (раунд 3).** Основной баг детекта закрыт PRSSTAT-чтением (см. секцию
|
|
||||||
> «Обновление 2026-07-10 (раунд 3)» выше). Пункты 1–2 ниже сохранены как история; пункт 3 —
|
|
||||||
> опровергнут; пункты 4–5 (devcontainer, watchdog) остаются актуальными как отдельные задачи.
|
|
||||||
> Рекомендации (6) консолидация детекта на единый PRSSTAT и (7) ранний сэмпл кнопки даунгрейда —
|
|
||||||
> **✅ реализованы в раунде 4** (см. секции «✅ Латентная хрупкость re-scan — УСТРАНЕНА» и «Тайминг
|
|
||||||
> удержания кнопки» выше); аппаратно ещё не проверены. Пункт (5) аппаратный watchdog —
|
|
||||||
> **✅ реализован** (`bsp/wdog`, см. PLAN.md «Реализовано: аппаратный watchdog» и чек-лист в
|
|
||||||
> test_stub/HARDWARE_VERIFICATION_PHASE3.md §9); аппаратно ещё не проверен. Открытых пунктов нет.
|
|
||||||
|
|
||||||
1. ✅ **Применено 2026-07-10 (раунд 1).** Откатить pinmux CD на `GPIO2_IO28` (без `USDHC1_CD_B`) —
|
|
||||||
самый доверенный референс (`TFT_BOOTLOADER`) использует именно так. **Аппаратно проверено —
|
|
||||||
недостаточно само по себе**: симптом 1 (SD не вставлена) воспроизводится и с верным pinmux. См.
|
|
||||||
«Обновление 2026-07-10 (раунд 2)» — потребовалось довыровнять ещё и pad-config CD-пина.
|
|
||||||
2. ✅ **Применено 2026-07-10 (раунд 1).** Добавить explicit power-cycle SD-карты в `bsp_sd_init()`
|
|
||||||
(`SD_SetCardPower(false)` → задержка → `SD_SetCardPower(true)`), по образцу
|
|
||||||
`TFT_BOOTLOADER::init_sd()`. **Аппаратно похоже подтверждено** — симптом 3→4 (повторный power
|
|
||||||
cycle с картой в слоте) больше не зависает (~5 c вместо hang), хотя раунд 1 не изолировал, какой
|
|
||||||
именно из двух фиксов это дал.
|
|
||||||
3. Развести флаг `g_s_initialized` в `bsp_sd_init()`/`bsp_sd_deinit()` на два состояния — "хост
|
|
||||||
сконфигурирован" (`BOARD_SD_Config` вызван) vs "хост реально поднят" (`SD_HostInit` реально
|
|
||||||
отработал) — чтобы `bsp_sd_deinit()` не звал `SD_HostDeinit()`/`USDHC_Reset` на состоянии,
|
|
||||||
которое никогда полноценно не поднималось. **Не реализовано** — см. «Обновление 2026-07-10»:
|
|
||||||
source-level разбор показал, что в наблюдавшемся симптоме 2 это не изменило бы поведение
|
|
||||||
(`SD_HostInit()` к моменту падения уже отработал успешно). Понижено до фоновой гигиены.
|
|
||||||
4. Прогонять сборку/прошивку через devcontainer (стандартный путь проекта), не только через
|
|
||||||
локальный host-toolchain — держать в уме при следующей проверке (сама эта сессия строила и
|
|
||||||
прошивала с хоста напрямую).
|
|
||||||
5. После того как оба симптома закрыты и подтверждены на железе — реализовать аппаратный watchdog
|
|
||||||
(см. [PLAN.md](PLAN.md), раздел «Запланировано: аппаратный watchdog») **как отдельный шаг после**,
|
|
||||||
не вместо, диагностики первопричины — иначе watchdog будет просто маскировать/сбрасывать симптом
|
|
||||||
на каждом цикле.
|
|
||||||
|
|
||||||
## Обновление 2026-07-10 — четвёртый референс, source-level трассировка, два фикса применены
|
|
||||||
|
|
||||||
Пользователь предоставил **полный код** `TFT_BOOTLOADER` (ранее в этом логе упоминался только по
|
|
||||||
описанию) — `board/pin_mux.c`, `board/sdmmc_config.c`, `source/sd.c`, `source/gpio_setup.c`,
|
|
||||||
`source/main.c` и др. Это подтверждает и уточняет раздел «Три референсных проекта» выше, а не
|
|
||||||
меняет его выводы.
|
|
||||||
|
|
||||||
### Пинмукс CD — подтверждено, откачено
|
|
||||||
|
|
||||||
`TFT_BOOTLOADER/board/pin_mux.c`:
|
|
||||||
```c
|
|
||||||
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);//SD check pin
|
|
||||||
```
|
|
||||||
Плоский `GPIO2_IO28`, без какой-либо альт-функции USDHC.
|
|
||||||
|
|
||||||
**Четвёртый источник**, найденный при подготовке фикса: NXP-овские
|
|
||||||
`sdk/boards/evkbimxrt1050/sdmmc_examples/*/pin_mux.c` (`sdcard_polling`, `sdcard_interrupt`,
|
|
||||||
`sdcard_freertos`, `sdcard_fatfs`, `mmccard_freertos` — уже вендорены в этом дереве) — **все**
|
|
||||||
используют `IOMUXC_GPIO_B1_12_GPIO2_IO28` для этого же физического пина (D13) на том же чипе
|
|
||||||
(MIMXRT1052/EVKB-семейство). Официальный NXP-референс для этой платы и этого пина ни разу не
|
|
||||||
использует `USDHC1_CD_B`.
|
|
||||||
|
|
||||||
Итого — четыре независимых указания в одну сторону (`TFT_BOOTLOADER`; `TFT7_RX_wOS`, который хоть и
|
|
||||||
маплит на `USDHC1_CD_B`, но ЧИТАЕТ через GPIO, то есть де-факто не использует USDHC-функцию пина;
|
|
||||||
NXP `sdmmc_examples`; и само нестабильное поведение на железе под `USDHC1_CD_B`, из-за которого
|
|
||||||
этот лог вообще начался).
|
|
||||||
|
|
||||||
**Применено**: `bsp/generated/sdmmc_config.c::BOARD_SD_Config()` — pinmux возвращён на
|
|
||||||
`IOMUXC_GPIO_B1_12_GPIO2_IO28`. Комментарии в этом файле и в `bsp/sd/src/sd.c::bsp_sd_is_inserted()`
|
|
||||||
(который раньше аргументировал ЗА `USDHC1_CD_B` задним числом, по результатам ещё не проверенного
|
|
||||||
на железе эксперимента (c)) переписаны под текущее состояние. Доктстринг `bsp_sd_is_inserted()` в
|
|
||||||
`bsp/sd/include/bsp/sd.h` тоже был стал устаревшим ещё раньше — утверждал чтение через регистр
|
|
||||||
USDHC PRSSTAT, оставшееся от давно откаченного эксперимента (a) — тоже исправлен.
|
|
||||||
|
|
||||||
**Не тронуто намеренно** — `bsp/generated/board/pin_mux.c` (генерируется MCUXpresso Config Tools,
|
|
||||||
в этой сессии не редактировался) по-прежнему содержит
|
|
||||||
`IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U)` внутри `BOARD_InitPins()`, а сам
|
|
||||||
`TFT_Board.mex` (строка ~134) содержит `<pin peripheral="USDHC1" signal="usdhc_cd_b" .../>` для
|
|
||||||
этого пина — маршрутизация на уровне самого Config Tools проекта всё ещё «неправильная».
|
|
||||||
Функционально это не баг сейчас: `board_hw_init() → BOARD_InitPins()` отрабатывает раньше, чем
|
|
||||||
`bsp_sd_init() → BOARD_SD_Config()`, и именно вызов из `sdmmc_config.c` (уже исправленный) — самый
|
|
||||||
последний перед любой операцией с картой, то есть он и определяет реальное состояние железа. Но
|
|
||||||
это мина для будущего: перегенерация пинов через Config Tools перезапишет `pin_mux.c` обратно на
|
|
||||||
`USDHC1_CD_B`, а конфликт останется незаметным (`sdmmc_config.c` продолжит его перекрывать) — пока
|
|
||||||
кто-то не тронет порядок инициализации или не сочтёт повторный вызов в `sdmmc_config.c`
|
|
||||||
дублирующим и не уберёт его. **Рекомендация на будущее**: при следующем открытии `TFT_Board.mex` в
|
|
||||||
Config Tools — перемаршрутизировать D13/`GPIO_B1_12` на `GPIO2.gpio_io[28]` вместо
|
|
||||||
`USDHC1.usdhc_cd_b`. Не блокирует текущую аппаратную проверку.
|
|
||||||
|
|
||||||
### Симптом 2 — механизм подтверждён трассировкой по исходникам SDK, не только гипотезой
|
|
||||||
|
|
||||||
Прочитан `sdk/middleware/sdmmc/sd/fsl_sd.c`:
|
|
||||||
|
|
||||||
- `SD_PollingCardInsert()` (используется для типа детекта `kSD_DetectCardByGpioCD`) — буквально
|
|
||||||
`do { ... } while (true)`, **без единого условия выхода по времени**. Если `cardDetected()`
|
|
||||||
(== `sd_card_detect_gpio()`, тот же нестабильный GPIO-read под `USDHC1_CD_B`) ни разу не даёт
|
|
||||||
связку значений, удовлетворяющую циклу, — зависание гарантированное и постоянное, не «иногда».
|
|
||||||
Ровно то же самое SDK-поведение отдельно задокументировано в `PLAN.md` под «Запланировано:
|
|
||||||
аппаратный watchdog» — тот же вывод, независимо подтверждён прямым чтением функции.
|
|
||||||
- `SD_Init()` **всегда** вызывает `SD_HostInit()` первым (`if (!card->isHostReady) { SD_HostInit(...); }`,
|
|
||||||
а `isHostReady` после `memset(&g_sd, 0, ...)` в `bsp_sd_init()` гарантированно `false`) — то есть
|
|
||||||
к моменту, когда `SD_Init()` доходит до `SD_PollingCardInsert()`/`SD_CardInit()` и потенциально
|
|
||||||
проваливается, хост уже реально поднят. Значит: наблюдаемый стек симптома 2
|
|
||||||
(`SD_HostDeinit → SDMMCHOST_Deinit → SDMMCHOST_Reset → USDHC_SelectVoltage → DefaultISR`)
|
|
||||||
происходит **после** успешного `SD_HostInit()`, а не из-за его отсутствия — гипотеза 1 в
|
|
||||||
исходном логе («либо не был вызван `SD_HostInit()`...») технически неточна в части причины, но
|
|
||||||
итоговый вывод (откатить pinmux) от этого не меняется: без ложного детекта `run_update()` вообще
|
|
||||||
не входит в `f_mount()`, и весь этот путь (включая `SD_CardInit()` → протокольные команды к
|
|
||||||
несуществующей карте → `USDHC` остаётся в промежуточном по voltage-switch состоянии → падение при
|
|
||||||
последующем `SDMMCHOST_Reset()`) просто не выполняется.
|
|
||||||
- `SD_CardInit()` вызывает только `SD_SetCardPower(card, true)` — **не** делает `false→true` сама.
|
|
||||||
Явного power-cycle в штатном пути SDK нет вообще, если его не сделать самостоятельно (важно для
|
|
||||||
следующего раздела).
|
|
||||||
|
|
||||||
### Симптом 4 — явный power-cycle добавлен (открытый вопрос 2, теперь применён)
|
|
||||||
|
|
||||||
`bsp/sd/src/sd.c::bsp_sd_init()` — добавлен `SD_SetCardPower(&g_sd, false)` →
|
|
||||||
`SD_SetCardPower(&g_sd, true)` сразу после `ensure_host_configured()` (нужен `usrParam.pwr`,
|
|
||||||
который выставляет `BOARD_SD_Config()`), по образцу `TFT_BOOTLOADER::init_sd()`. Задержки —
|
|
||||||
встроенные в сам `SD_SetCardPower()` (`fsl_sd.c`): `SD_POWER_OFF_DELAY=100` мс,
|
|
||||||
`SD_POWER_ON_DELAY=400` мс, итого ~500 мс добавляется к каждой попытке `bsp_sd_init()` (не к
|
|
||||||
каждому проходу главного цикла — только когда карта физически детектится и `run_update()` реально
|
|
||||||
стартует).
|
|
||||||
|
|
||||||
Механизм здесь остаётся гипотезой (в отличие от симптома 2 — прямого прочтения бага в исходниках
|
|
||||||
для этого случая нет): `SD_CardInit()` при обычном включении питания отработал бы одинаково что с
|
|
||||||
предварительным power-cycle, что без. Разница имела бы значение, только если карта физически не
|
|
||||||
успевает разрядиться между быстрыми power cycle платы (правдоподобно электрически — SD-рейл может
|
|
||||||
иметь свою постоянную разряда, отличную от MCU — но не подтверждено осциллографом). Аппаратный тест
|
|
||||||
покажет.
|
|
||||||
|
|
||||||
### Открытый вопрос 3 (разделение флага `g_s_initialized`) — понижен в приоритете
|
|
||||||
|
|
||||||
Source-level разбор показал: к моменту падения в симптоме 2 `SD_HostInit()` уже отработал успешно
|
|
||||||
(`isHostReady == true`) — разделение флага на «хост сконфигурирован» vs «хост реально поднят» **не
|
|
||||||
изменило бы поведение в этом конкретном наблюдавшемся случае**: `bsp_sd_deinit()` вызвал бы
|
|
||||||
`SD_HostDeinit()` в обоих вариантах флага одинаково. Ценность этого пункта — на пока не
|
|
||||||
пронаблюдавшемся крайнем случае (`SD_HostInit()` возвращает ошибку сам по себе, например
|
|
||||||
`SDMMCHOST_Init()` фейлится внутри) — не реализовано в этой сессии, чтобы не размывать набор
|
|
||||||
изменений перед проверкой на железе. Остаётся в очереди как фоновая гигиена, не как фикс под
|
|
||||||
конкретный симптом.
|
|
||||||
|
|
||||||
### Сборка
|
|
||||||
|
|
||||||
`just build::build-bootloader-debug` — зелёно (`m_text` 87304 Б / 247 КБ, 34.52% — чуть меньше, чем
|
|
||||||
87928 Б, отмеченные в PLAN.md до этой сессии). `just build::test-host` — 15/15. Оба прогнаны в
|
|
||||||
devcontainer (`docker exec -w /workspace tft-devcontainer ...`), не с хоста напрямую — см. открытый
|
|
||||||
вопрос 4 выше.
|
|
||||||
|
|
||||||
### Чек-лист для аппаратной проверки (следующий шаг — на стороне пользователя)
|
|
||||||
|
|
||||||
Оба фикса (pinmux + power-cycle) применены вместе — они независимы по механизму и по разным
|
|
||||||
симптомам, так что сигнатуры (зависание в `SD_PollingCardInsert`/`OSA_SemaphoreWait` vs падение в
|
|
||||||
`DefaultISR`) при повторном проявлении однозначно укажут, какой из двух фиксов не сработал (или
|
|
||||||
сработал не полностью), даже тестируя одним прогоном:
|
|
||||||
|
|
||||||
1. **Симптом 1/2** (чистая плата, оба слота пусты, SD карта отсутствует) — ожидание: ни зависания,
|
|
||||||
ни падения; `sd_update_check()` должен быстро выйти (`bsp_sd_is_inserted()` теперь стабильно
|
|
||||||
`false`), дойти до `boot_select_and_jump()` → нет валидного образа → штатный ping/pong-цикл.
|
|
||||||
2. **Симптом 3** (SD вставлена при старте) — должен по-прежнему успешно доходить до
|
|
||||||
`jump_to_image`, как и раньше.
|
|
||||||
3. **Симптом 3→1** (после (2): выключить питание, вынуть SD, включить снова) — то же самое, что
|
|
||||||
симптом 1, теперь с картой, которая только что использовалась — ожидание: то же корректное
|
|
||||||
поведение, что в (1).
|
|
||||||
4. **Симптом 4 буквально** (SD оставлена в слоте, повторный power cycle сразу после успешного (2),
|
|
||||||
карта физически всё ещё вставлена) — тест именно на power-cycle-фикс; ожидание: не зависает в
|
|
||||||
`OSA_SemaphoreWait`, обновление/загрузка проходит штатно.
|
|
||||||
|
|
||||||
Если что-то из этого всё ещё не проходит — сначала зафиксировать точную сигнатуру (стек в
|
|
||||||
отладчике, как и раньше в этом логе) до следующей правки; при необходимости фикс power-cycle и
|
|
||||||
фикс pinmux можно тестировать раздельно, закомментировав добавленный блок `SD_SetCardPower` в
|
|
||||||
`bsp_sd_init()` (единственное новое, легко изолируемое от pinmux-фикса изменение).
|
|
||||||
|
|
||||||
## Обновление 2026-07-10 (раунд 2) — пинмукс-фикса недостаточно, полное выравнивание на TFT_BOOTLOADER
|
|
||||||
|
|
||||||
**Аппаратный результат раунда 1** (пинмукс `GPIO2_IO28` + explicit power-cycle в `bsp_sd_init()`,
|
|
||||||
без изменения pad-config и без power-toggle side-effect):
|
|
||||||
|
|
||||||
- **Симптом 1 (чистая плата, SD не вставлена) — не устранён.** Всё ещё висим внутри того же
|
|
||||||
`SD_PollingCardInsert()` `do{}while(true)`. Это означает: `bsp_sd_is_inserted()` всё ещё
|
|
||||||
возвращает ложный `true` без карты, ДАЖЕ с верным pinmux — вывод раунда 1 («нестабильное чтение
|
|
||||||
из-за неверной альт-функции») опровергнут этим результатом. Раз чтение теперь стабильно неверное
|
|
||||||
(не «шумит», а последовательно врёт), причина — не альт-функция сама по себе, а что-то ещё в
|
|
||||||
электрической конфигурации пина (см. ниже) либо в физической полярности детект-переключателя на
|
|
||||||
этой плате.
|
|
||||||
- **Симптом 3→4 (SD оставлена в слоте, повторный power cycle) — похоже, устранён.** Раньше —
|
|
||||||
зависание в `OSA_SemaphoreWait`. Теперь: загрузка (переиспользование уже установленного stub-а)
|
|
||||||
занимает ~5 секунд вместо зависания. Не финальное подтверждение (не изолировано, какой именно
|
|
||||||
из фиксов раунда 1 это дал — pinmux или power-cycle), но однозначно прогресс.
|
|
||||||
|
|
||||||
**Важное уточнение механизма симптома 1**, важное для дальнейшей диагностики: `SD_PollingCardInsert(card, kSD_Inserted)`
|
|
||||||
в SDK — это `do{}while(true)` без ЕДИНОГО условия выхода, если карта не появляется. Это не
|
|
||||||
«нестабильность», это **ожидаемое поведение при вызове с картой, которой реально нет** — функция
|
|
||||||
единственная блокируется до появления карты, всегда, у обоих бутлоадеров (текущий и
|
|
||||||
`TFT_BOOTLOADER::init_sd()` вызывают её одинаково). Значит, единственное, что может предотвратить
|
|
||||||
зависание — гейт (`bsp_sd_is_inserted()`) ДОЛЖЕН быть на 100% верным при пустом слоте. Раз он
|
|
||||||
по-прежнему неверный — дело в самом детекте, не в том, как его результат используется.
|
|
||||||
|
|
||||||
### Инструкция пользователя: полное выравнивание на TFT_BOOTLOADER, не только pinmux
|
|
||||||
|
|
||||||
Пройден весь `TFT_BOOTLOADER` построчно на предмет расхождений с текущим SD-стеком, помимо
|
|
||||||
pinmux. Найдено и закрыто три конкретных расхождения:
|
|
||||||
|
|
||||||
**1. Pad-config CD-пина — было НЕ то, что в референсе.** Раунд 1 откатил только pinmux
|
|
||||||
(`IOMUXC_SetPinMux`), но НЕ pad-config (`IOMUXC_SetPinConfig`) — тот со времён исходной правки
|
|
||||||
оставался «активная 47к подтяжка вверх + hysteresis» (`PKE|PUE|HYS|PUS(1)`). Побитовый разбор
|
|
||||||
`TFT_BOOTLOADER::BOARD_SD_Pin_Config()` (`0x10B0U`, с помощью распечатанных масок из
|
|
||||||
`PERI_IOMUXC.h`) даёt: `PKE=1, PUE=0 (KEEPER, не активная подтяжка — PUS в этом режиме
|
|
||||||
игнорируется), HYS=0 (выключен), SPEED=2, DSE=6`. Это **не** совпадает с тем, что было в дереве —
|
|
||||||
принципиальная разница (keeper vs. активный pull-up, hysteresis выкл vs. вкл). Применено побитово
|
|
||||||
точно как в референсе (`bsp/generated/sdmmc_config.c::sd_pin_config()`). **Это — наиболее
|
|
||||||
вероятный настоящий фикс** для устойчивого (не шумового) ложного детекта: если реальная физическая
|
|
||||||
конфигурация детект-цепи на этой плате рассчитана на keeper (или просто не рассчитана на активную
|
|
||||||
подтяжку в эту сторону), активная 47к подтяжка вверх могла систематически перетягивать/конфликтовать
|
|
||||||
с реальным сигналом.
|
|
||||||
- **Не проверено на железе.**
|
|
||||||
|
|
||||||
**2. `bsp_sd_is_inserted()` не имела side-effect на SD_PWR.** `TFT_BOOTLOADER::is_sdcard_present()`
|
|
||||||
на каждый вызов не только читает CD, но и включает/выключает `SD_PWR` по результату — до этого
|
|
||||||
раунда `bsp_sd_is_inserted()` была чистым чтением без побочных эффектов. Добавлено: `sd_power_control()`
|
|
||||||
(`sdmmc_config.c`) сделана публичной под именем `BOARD_SDCardPowerControl()` (то же имя, что и в
|
|
||||||
стоковом `TFT_BOOTLOADER/board/sdmmc_config.c` — там это отдельная, не связанная с
|
|
||||||
`is_sdcard_present()` функция, но семантически та же роль) и вызывается из
|
|
||||||
`bsp_sd_is_inserted()` на каждый скан.
|
|
||||||
- **Механизм не подтверждён** — детект (механический контакт) физически не должен зависеть от
|
|
||||||
питания карты, но раз референс это делает, а референс проверенно работал на этой плате, — реплицировано
|
|
||||||
добросовестно, не только «на всякий случай», а по прямому запросу пользователя выровнять именно
|
|
||||||
настройку SD-стека на референс.
|
|
||||||
|
|
||||||
**3. `bsp_sd_init()` теперь явно вызывает `SD_HostInit()` → `SD_PollingCardInsert()` → power-cycle**,
|
|
||||||
байт-в-байт порядок `TFT_BOOTLOADER::init_sd()`, вместо implicit-инициализации внутри
|
|
||||||
`f_mount() → SD_Init()`. Само по себе это НЕ предотвращает зависание при реальном отсутствии карты
|
|
||||||
(см. выше — блокировка без тайм-аута заложена в SDK и одинакова что в implicit, что в explicit
|
|
||||||
пути) — ценность изменения в точном соответствии референсу и в том, что `f_mount()` внутри теперь
|
|
||||||
дополнительно делает `SD_HostDoReset()` + повторный `SD_PollingCardInsert()`/`SD_CardInit()` уже
|
|
||||||
поверх реально поднятого хоста (`isHostReady == true`), тем же паттерном двойного вызова
|
|
||||||
`SD_PollingCardInsert`, что и в референсе.
|
|
||||||
|
|
||||||
### Сборка
|
|
||||||
|
|
||||||
Оба фикса раунда 2 собраны и проверены в devcontainer: `just build::build-bootloader-debug` —
|
|
||||||
зелёно (`m_text` 87352 Б / 34.54%), `just build::test-host` — 15/15.
|
|
||||||
|
|
||||||
### Если симптом 1 всё ещё не уйдёт после раунда 2
|
|
||||||
|
|
||||||
Тогда pad-config — тоже не объяснение, и дальше гадать конфигурацией регистров вслепую
|
|
||||||
контрпродуктивно (уже два раунда фиксов на одних догадках без прямого замера). Следующий шаг в
|
|
||||||
этом случае — не третья попытка pin-config, а **прямой замер** физического пина `GPIO_B1_12` /
|
|
||||||
D13 (мультиметром или логическим анализатором через MCU-Link): уровень при вставленной карте vs.
|
|
||||||
без карты, и полярность самого механического CD-переключателя в слоте этой платы (нормально-открыт
|
|
||||||
или нормально-закрыт — `BOARD_SDMMC_SD_CD_INSERT_LEVEL=0U` предполагает нормально-открытый,
|
|
||||||
замыкающий на GND при вставке; если физически наоборот — весь код читает верно регистр, но с
|
|
||||||
неверным заранее предположением о полярности, и фикс — просто `BOARD_SDMMC_SD_CD_INSERT_LEVEL=1U`,
|
|
||||||
а не что-либо из pinmux/pad-config).
|
|
||||||
|
|
||||||
## Обновление 2026-07-10 (раунд 3) — РАЗРЕШЕНО: детект через USDHC PRES_STATE.CINST
|
|
||||||
|
|
||||||
Пользователь заменил `bsp_sd_is_inserted()` на чтение регистра USDHC вместо GPIO:
|
|
||||||
|
|
||||||
```c
|
|
||||||
bool bsp_sd_is_inserted(void)
|
|
||||||
{
|
|
||||||
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
|
||||||
uint32_t ps = USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR);
|
|
||||||
return (ps & kUSDHC_CardInsertedFlag) != 0U;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Все 5 сценариев Фазы 3 пройдены на железе.**
|
|
||||||
|
|
||||||
### Почему это сработало (и почему раунды 1–2 искали не там)
|
|
||||||
|
|
||||||
Это ровно подход **(a)** из таблицы «Что уже пробовали» в самом верху лога — тогда откачен как
|
|
||||||
«CINST читался как 0 даже при вставленной карте». Причина того провала теперь понятна: `PRSSTAT.CINST`
|
|
||||||
отражает реальное состояние CD **только когда физический пин D13 замаплен на `USDHC1_CD_B`** (сигнал
|
|
||||||
идёт в периферию USDHC, а не в GPIO). В момент пробы (a) пин был на `GPIO2_IO28` — поэтому CINST и
|
|
||||||
читался нулём. То есть (a) и (c) по отдельности не работают, а **вместе** — работают: `USDHC1_CD_B`
|
|
||||||
маршрутизация (c) + чтение через PRSSTAT (a).
|
|
||||||
|
|
||||||
Ключевой поворот: **мой откат pinmux на `GPIO2_IO28` в раунде 1 был направлен не в ту сторону.**
|
|
||||||
Исходная правка (c) (`USDHC1_CD_B`) была верной — ей не хватало парного чтения через регистр, а не
|
|
||||||
через `GPIO_PinRead`. Вся линия рассуждений раундов 1–2 («три-четыре референса за GPIO2_IO28»)
|
|
||||||
верна лишь для того механизма чтения, который эти референсы используют (GPIO), но не была
|
|
||||||
единственно возможной: PRSSTAT — четвёртый, не рассматривавшийся всерьёз в раундах 1–2 путь,
|
|
||||||
который и оказался рабочим на этой плате.
|
|
||||||
|
|
||||||
### Почему это чинит симптом 1 конкретно
|
|
||||||
|
|
||||||
`SD_PollingCardInsert(kSD_Inserted)` в SDK — `do{}while(true)` без тайм-аута (разобрано в раунде 1).
|
|
||||||
Единственная защита — гейт `bsp_sd_is_inserted()` обязан быть верным на пустом слоте. На старте с
|
|
||||||
пустым слотом `BOARD_SD_Config()` ещё не вызывался (его зовёт `bsp_sd_init()` уже внутри
|
|
||||||
`run_update()`, за гейтом) → пин остаётся на `USDHC1_CD_B`, как его поставил `BOARD_InitPins()` в
|
|
||||||
`board_hw_init()` → PRSSTAT.CINST корректно читает «карты нет» → гейт возвращает false → блокирующий
|
|
||||||
`SD_PollingCardInsert()` вообще не достигается. Симптом 1 закрыт по построению.
|
|
||||||
|
|
||||||
### Текущее итоговое состояние кода (что осталось от раундов 1–3)
|
|
||||||
|
|
||||||
- `bsp_sd_is_inserted()` — PRSSTAT-чтение (раунд 3, рабочее). **Это единственное, что реально
|
|
||||||
закрыло симптом 1.**
|
|
||||||
- `bsp_sd_init()` — explicit `SD_HostInit()` → `SD_PollingCardInsert()` → power-cycle (раунд 2).
|
|
||||||
Не вредит; power-cycle правдоподобно закрыл симптом 3→4 (не изолировано отдельно).
|
|
||||||
- CD pad-config `0x10B0` на `GPIO2_IO28` (раунд 2) — остаётся; применяется к пину в моменты, когда
|
|
||||||
он на `GPIO2_IO28` (внутренний GPIO-детект SDK, см. ниже). Не вредит.
|
|
||||||
- `BOARD_SD_Config()` маплит пин на `GPIO2_IO28` (раунд 1) — **остаётся**, потому что внутренний
|
|
||||||
детект SDK (`sd_card_detect_gpio`, `kSD_DetectCardByGpioCD`) внутри `f_mount()→SD_Init()` читает
|
|
||||||
GPIO, и к тому моменту гейт уже подтвердил карту. Комментарии в `sdmmc_config.c`/`sd.c`/`sd.h`
|
|
||||||
переписаны под это фактическое состояние (два механизма детекта на разной маршрутизации одного
|
|
||||||
пина в разные моменты).
|
|
||||||
- `sd_power_control` возвращён в `static` (публичное имя `BOARD_SDCardPowerControl` из раунда 2
|
|
||||||
больше не нужно — side-effect, ради которого оно вводилось, заменён PRSSTAT-чтением).
|
|
||||||
|
|
||||||
### ✅ Латентная хрупкость re-scan — УСТРАНЕНА (раунд 4, item 1)
|
|
||||||
|
|
||||||
**Была** (до раунда 4): рабочая конфигурация опиралась на то, что пин на `USDHC1_CD_B` в момент
|
|
||||||
гейта, но `BOARD_SD_Config()` перемапливал его на `GPIO2_IO28` при первом же `bsp_sd_init()` и
|
|
||||||
обратно НЕ возвращал. Если `run_update()` был вызван (карта была на гейте), но завершался без прыжка
|
|
||||||
(например, на карте нет `TFT_APP.BIN`), то на СЛЕДУЮЩЕМ периодическом скане `bsp_sd_is_inserted()`
|
|
||||||
читал PRSSTAT с пином уже на `GPIO2_IO28` → ложный ноль → карта «исчезала» до перезагрузки.
|
|
||||||
|
|
||||||
**Реализовано (раунд 4, вариант B — минимальный):** детект сведён к ЕДИНОМУ механизму, PRSSTAT
|
|
||||||
везде. Callback `sd_card_detect_gpio` → `sd_card_detect_prsstat` (читает `USDHC_GetPresentStatusFlags
|
|
||||||
& kUSDHC_CardInsertedFlag`); remux на `GPIO2_IO28` из `BOARD_SD_Config()` убран — пин остаётся на
|
|
||||||
`USDHC1_CD_B` постоянно (там же явно переустанавливается); мёртвый pad-конфиг GPIO2_IO28 из
|
|
||||||
`sd_pin_config()` удалён. `s_cd.type` оставлен `kSD_DetectCardByGpioCD` — это не «GPIO» в смысле
|
|
||||||
чтения, а «детект через callback» (polling); альтернатива `kSD_DetectCardByHostCD` дополнительно
|
|
||||||
взвела бы USDHC card-detect ПРЕРЫВАНИЯ (тот же PRSSTAT-бит, но лишний источник IRQ перед прыжком —
|
|
||||||
см. `SDMMCHOST_CardDetectInit` в `sdk/.../usdhc/non_blocking/fsl_sdmmc_host.c`), поэтому выбран
|
|
||||||
вариант B. Теперь и гейт, и внутренний `SD_PollingCardInsert` читают один регистр на одной
|
|
||||||
маршрутизации пина.
|
|
||||||
|
|
||||||
**Требует аппаратной ре-проверки:** все 5 сценариев, плюс явно кейс, который раньше был сломан —
|
|
||||||
«карта вставлена, но без `TFT_APP.BIN`; ждём; кладём файл на карту БЕЗ извлечения/перезагрузки —
|
|
||||||
пере-скан должен его подхватить».
|
|
||||||
|
|
||||||
### Тайминг удержания кнопки даунгрейда (симптом/вопрос пользователя)
|
|
||||||
|
|
||||||
Наблюдение: удержание `BSP_BUTTON_1` 2 c при подаче питания → даунгрейд НЕ происходит; 15 c →
|
|
||||||
происходит (визуально — сразу после отпускания). Разобрано по коду:
|
|
||||||
|
|
||||||
- `bsp_button_read()` (`bsp/button/src/button.c`) — **мгновенное сырое чтение GPIO**, без debounce,
|
|
||||||
без накопления, без latch. `bsp_button_poll()`/события в загрузчике **не используются вообще**
|
|
||||||
(единственное обращение к кнопке — `bsp_button_read(BSP_BUTTON_1)` в `sd_update.c:180`).
|
|
||||||
- Значит важен ровно один момент: физически ли нажата кнопка в ту единственную点, когда
|
|
||||||
`run_update()` её сэмплит. А сэмпл этот — **после** `bsp_sd_init()` (power-cycle-задержки ~0.5 c) +
|
|
||||||
`f_mount()` (полная инициализация карты: CMD0/CMD8/ACMD41-поллинг и т.д.) + `f_open()` + чтения
|
|
||||||
заголовка + **двух полных крипто-валидаций слотов** (`peek_slot(0)`/`peek_slot(1)` →
|
|
||||||
`slot_version_get` → `bootutil_img_validate`: SHA-256 + ECDSA-P256 по каждому слоту). Это секунды,
|
|
||||||
и всё это — ДО главного цикла, без LED-фидбэка (heartbeat мигает только в главном цикле, куда
|
|
||||||
управление ещё не дошло).
|
|
||||||
- Отсюда: кнопку надо держать НЕПРЕРЫВНО от подачи питания до момента сэмпла. Отпустил в 2 c →
|
|
||||||
сэмпл (несколькими секундами позже) поймал уже отпущенную → `update_policy_decide` видит
|
|
||||||
`button_held=false` → SKIP → штатная загрузка более нового слота (не даунгрейд). Держал 15 c →
|
|
||||||
на момент сэмпла ещё нажата → даунгрейд. «Сразу после отпускания» — это не триггер по отпусканию
|
|
||||||
(кода на отпускание нет); это совпадение: решение уже защёлкнулось на сэмпле (пока держал), а
|
|
||||||
видимый результат (стирание+копирование+прыжок в старший образ) проявляется как раз к ~15 c.
|
|
||||||
|
|
||||||
**✅ Реализовано (раунд 4, item 2):** кнопка теперь сэмплится РАНО и защёлкивается. `main.c` читает
|
|
||||||
`bsp_button_read(BSP_BUTTON_1)` один раз сразу после `bsp_button_init()` (до `bsp_qspi_init()`/
|
|
||||||
SD-логики) в `const bool downgrade_held` и передаёт это значение в `sd_update_check(downgrade_held)`
|
|
||||||
на обеих точках вызова (первая попытка + пере-сканы цикла). `run_update()` больше не читает кнопку
|
|
||||||
сам (`bsp/button.h` из `sd_update.c` убран) — `update_policy_decide()` получает уже защёлкнутое
|
|
||||||
значение. Жест «удержание при включении» теперь ловится в детерминированный ранний момент, а не
|
|
||||||
через секунды слепого окна.
|
|
||||||
|
|
||||||
**Новая инструкция технологу** (обновлено в HARDWARE_VERIFICATION_PHASE3.md): достаточно держать
|
|
||||||
`BSP_BUTTON_1` нажатой **в момент подачи питания** (до/во время включения) — состояние считывается в
|
|
||||||
первые миллисекунды после `board_hw_init()`, ещё до обращения к SD. Долгое «15 c с запасом» больше
|
|
||||||
не требуется. **Требует аппаратной ре-проверки** сценария 3 (даунгрейд): удержать кнопку при
|
|
||||||
включении → даунгрейд срабатывает; не удерживать → штатная загрузка более нового слота.
|
|
||||||
|
|
||||||
## Побочная находка (не блокирует, но стоит знать)
|
|
||||||
|
|
||||||
`PLAN.md` в двух местах ссылается на `DEBUG_LOG_PHASE2.md`, которого фактически нет в дереве (не
|
|
||||||
найден ни в рабочей директории, ни в истории git) — судя по всему, файл планировался, но не был
|
|
||||||
создан. Не тема этого лога, но стоит либо создать его отдельно, либо убрать ссылки, когда дойдут руки.
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
| 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) |
|
| 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 (см. ниже) |
|
| 4 — SDRAM/W25Q smoke-test + LED-паттерны | не начата | рекомендуется ПОСЛЕ Фазы 6 (см. ниже) |
|
||||||
| 5 — HAB Release + service-tui | не начата | |
|
| 5 — HAB Release + service-tui | не начата | |
|
||||||
| 6 — Устойчивость и восстановление (recovery) | спроектирована, код не начат | **рекомендованный следующий шаг** (раньше 4/5): закрывает зависание уже установленного образа и даёт ручной аварийный вход. Дизайн зафиксирован в обсуждении — см. раздел «Фаза 6» ниже |
|
| 6 — Устойчивость и восстановление (recovery) | ✅ полностью верифицирована (2026-07-14) | 6a (счётчик `SRC_GPR3` + фолбэк) + 6b (recovery-режим, BTN_2, ослабленный gate) + стенд `test_stub` реализованы; host-тесты 16/16, ARM+HAB зелёные. Все 6 сценариев чек-листа пройдены на реальной плате; по пути найдено/исправлено 4 бага (2 в коде, 2 в стенде/чек-листе — детали ниже). 6c (контракт tft_app) — документация под будущий план. Чек-лист — [test_stub/HARDWARE_VERIFICATION_PHASE6.md](test_stub/HARDWARE_VERIFICATION_PHASE6.md) |
|
||||||
|
|
||||||
## Контекст
|
## Контекст
|
||||||
|
|
||||||
|
|
@ -855,6 +855,125 @@ recovery-LED/CDC это показывают), лечится SD-фиксом и
|
||||||
- Кормление watchdog в recovery — как везде (верх цикла + per-block при стирании; 2×2 МБ ~10 c
|
- Кормление watchdog в recovery — как везде (верх цикла + per-block при стирании; 2×2 МБ ~10 c
|
||||||
накрыты поблочным refresh).
|
накрыты поблочным refresh).
|
||||||
|
|
||||||
|
### ✅ Реализовано (2026-07-13): 6b — recovery_mode протянут, LED-паттерн, CDC-статус
|
||||||
|
|
||||||
|
- **`update_policy_decide()`** — параметр `recovery_mode`: при `true` версия/кнопка игнорируются,
|
||||||
|
всегда `target_slot = SLOT_A`, `erase_previous_active = true` (стереть Slot Б) — независимо от
|
||||||
|
того, какой слот был активен (в т.ч. если активной была именно Slot A). 3 новых host-теста
|
||||||
|
(`tests/host/update_policy/`), включая регрессионный на «recovery целится в A, даже если A
|
||||||
|
активна».
|
||||||
|
- **`sd_update_check()`/`run_update()`** — сигнатура: добавлен `recovery_mode`, возврат сменён с
|
||||||
|
`void` на `bool` (`true` ⟺ `target_slot` подтверждён `slot_version_get()` после установки).
|
||||||
|
Порядок в `attempt_boot()` (`main.c`) специально **recovery_decide() → SD-скан**, не наоборот:
|
||||||
|
ослабленный gate должен быть известен ДО скана — иначе строгий gate никогда не пустит образ
|
||||||
|
поверх «активного» слота, и recovery-режим не имел бы смысла даже при найденном валидном
|
||||||
|
кандидате.
|
||||||
|
- **`main.c`** — `attempt_boot()` возвращает `bool` (recovery-состояние); при
|
||||||
|
`RECOVERY_ENTER_RECOVERY_MODE` + успешной установке — немедленный `jump_now()` (не дожидаясь
|
||||||
|
следующего пере-скана), иначе остаёмся в recovery. Новый `jump_now()` — общий `bsp_wdog_refresh()`
|
||||||
|
+ `boot_select_and_jump()`, убрал дублирование (было 3 места). `in_recovery` — состояние между
|
||||||
|
попытками, читается каждую итерацию главного цикла (LED должен обновляться чаще, чем раз в
|
||||||
|
`SD_RETRY_PERIOD_MS`).
|
||||||
|
- **LED-паттерн recovery** — оба LED синхронно, 100/100 мс (решение зафиксировано в обсуждении
|
||||||
|
реализации выше). CDC — `status: recovery_mode` вместо `waiting_for_sd`, пока `in_recovery`.
|
||||||
|
- **Host-тесты**: `just build::test-host` — **16/16 зелёных** (15 в `test_update_policy` + 9 в
|
||||||
|
`test_recovery`).
|
||||||
|
- **ARM-сборка**: `build-bootloader-debug`/`hab-bootloader-debug` — зелёные, 0 warnings на чистой
|
||||||
|
пересборке. `m_text` — 90552 Б из 247 КБ (35.80%, было 90320 Б / 35.71% до этого шага).
|
||||||
|
- **Не реализовано в этом шаге**: `BSP_BUTTON_2` уже сэмплируется (готово с 6a) и передаётся как
|
||||||
|
`recovery_held` в `recovery_decide()` — ручной вход через удержание кнопки уже работает
|
||||||
|
архитектурно. Что осталось из 6b: явный чек-лист аппаратной верификации (см. общий чек-лист Фазы 6
|
||||||
|
выше) — **аппаратно ещё не проверено**.
|
||||||
|
|
||||||
|
### Найден и исправлен баг (до железа, разбором кода): счётчик рос и на пустой плате
|
||||||
|
|
||||||
|
При подготовке стенда 6c (до запуска на железе) обнаружилось: в `attempt_boot()` (`main.c`)
|
||||||
|
`bsp_boot_attempt_inc()` вызывался **безусловно** в ветке `RECOVERY_NORMAL_BOOT`, даже когда `boot_go()`
|
||||||
|
заведомо нечего выбирать (оба слота пусты, SD не вставлена — штатное состояние "bootloader-only,
|
||||||
|
ждём первую установку", уже верифицированное в Фазе 3, сценарий 4). Счётчик рос на каждом пере-скане
|
||||||
|
(раз в 1.5 c) НЕЗАВИСИМО от того, было ли реальное зависание — через `threshold` (3) попыток
|
||||||
|
(~4.5 c) `recovery_decide()` видел `attempt_count >= threshold` и **ошибочно уводил бы плату в
|
||||||
|
recovery-режим** (LED-паттерн + CDC `recovery_mode`) при полностью нормальном, ожидаемом состоянии —
|
||||||
|
прямая регрессия к уже пройденному сценарию 4 Фазы 3 ("оба слота пусты" должен оставаться в обычном
|
||||||
|
ожидании SD бесконечно, без всякого recovery).
|
||||||
|
|
||||||
|
**Исправлено**: инкремент теперь условный — `if (installed || slot_a.valid || slot_b.valid)`. Слоты
|
||||||
|
уже пикнуты (`peek_slot()`) для вызова `recovery_decide()` в начале функции, переиспользуются без
|
||||||
|
дополнительных чтений flash. Корректно обрабатывает все случаи:
|
||||||
|
- Оба слота пусты, SD не вставлена — счётчик НЕ растёт, `recovery_decide()` навсегда видит `0 <
|
||||||
|
threshold` → `NORMAL_BOOT`, плата остаётся в обычном ожидании (регрессия закрыта).
|
||||||
|
- Класс A/B (валидный по крипте слот, но виснет в рантайме) — `slot_a.valid`/`slot_b.valid` истинны
|
||||||
|
на момент пика (до того как `boot_go()` внутри `jump_now()` может стереть слот по штатному
|
||||||
|
revert'у Класса A) → инкремент происходит корректно, механизм таксономии не задет.
|
||||||
|
- Свежая установка с SD в этом же вызове — `installed=true` → инкремент (0→1) после уже случившегося
|
||||||
|
`bsp_boot_attempt_reset()` — то же самое поведение, что и раньше, просто явное условие вместо
|
||||||
|
неявного "всегда".
|
||||||
|
|
||||||
|
Пойман до единого прогона на железе — чисто рассуждением о сценарии перед написанием чек-листа
|
||||||
|
верификации, см. ниже. ARM-сборка зелёная после фикса (`m_text` 90568 Б, было 90552 Б).
|
||||||
|
|
||||||
|
### Найден и исправлен баг (на железе, сценарий 2): стенд обнулял счётчик раньше, чем зависание успевало засчитаться
|
||||||
|
|
||||||
|
**Аппаратная верификация (2026-07-14).** Сценарий 1 (Класс A) прошёл штатно. Сценарий 2 (Класс B,
|
||||||
|
есть фолбэк) — **не сработал**: Slot Б (стаб `confirm_hang`) резетился по WDOG раз 5 подряд без
|
||||||
|
остановки (ожидалось — ровно 3, затем автоматический фолбэк на Slot A); оператору пришлось стереть
|
||||||
|
Slot Б вручную, после чего Slot A подхватился штатно (уже без участия recovery-логики — простой
|
||||||
|
`boot_go()` с единственным валидным слотом).
|
||||||
|
|
||||||
|
**Причина — не в `recovery.c`/`main.c` (та логика верна), а в самом стенде `test_stub`**:
|
||||||
|
`bsp_boot_health_mark()` — это буквально `bsp_boot_attempt_reset()` (см. `bsp/boot_state.h`) —
|
||||||
|
вызывался в `main.c` стаба **безусловно**, сразу на входе, до всякой проверки `STUB_HANG_MODE`. Для
|
||||||
|
варианта `confirm_hang` (`HANG_MODE=3`) это означало: bootloader инкрементирует счётчик (0→1) перед
|
||||||
|
прыжком → стаб тут же обнуляет его обратно в 0 (health-mark) → мигает ~6 c → виснет → WDOG-сброс —
|
||||||
|
и по новой, **счётчик никогда не накапливался выше 1**, порог 3 не достигался никогда, resets шли
|
||||||
|
бесконечно.
|
||||||
|
|
||||||
|
Это частный, детерминированный случай уже задокументированной в этом плане «честной границы»
|
||||||
|
(раздел «Честная граница») — только там речь про РЕДКИЙ баг, а тут health-mark и зависание
|
||||||
|
гарантированно чередуются на КАЖДОМ цикле, так что граница проявляется не «иногда», а всегда.
|
||||||
|
|
||||||
|
**Исправлено**: в `test_stub/main.c` `bsp_boot_health_mark()` теперь не вызывается для
|
||||||
|
`HANG_MODE ∈ {2, 3}` (обе конфигурации гарантированно виснут — раз так, заявлять "здоров" не имеет
|
||||||
|
смысла, счётчик должен копиться). `HANG_MODE=0` (никогда не виснет) и `HANG_MODE=1` (виснет ДО этой
|
||||||
|
строки, недостижима) — не затронуты. Пересобрано (`just build::build-mcuboot-stub`), все 7 файлов
|
||||||
|
подписаны заново.
|
||||||
|
|
||||||
|
**Важный урок для будущего 6c (контракт tft_app)**: если реальный tft_app зовёт `boot_set_confirmed`/
|
||||||
|
health-mark слишком рано (сразу по входу в главный цикл, не дождавшись хоть какого-то подтверждения
|
||||||
|
стабильности) и затем детерминированно (не редко!) виснет на каждой сессии — recovery-фолбэк работать
|
||||||
|
не будет, счётчик будет обнуляться быстрее, чем успевает накопиться. Стоит явно прописать в 6c:
|
||||||
|
health-mark — не "дошёл до main()", а как минимум "пережил разумный интервал без явных проблем".
|
||||||
|
|
||||||
|
### Найден и исправлен баг (на железе, сценарий 4): чек-лист сам предписывал не тот файл под recovery-установку
|
||||||
|
|
||||||
|
**Аппаратная верификация (2026-07-14), продолжение.** Сценарии 1-3 пройдены. При проверке ручного
|
||||||
|
входа в recovery по BTN_2 с последующей установкой с SD: CDC показал `installing`, затем **USB CDC
|
||||||
|
пропал**, ничего не запустилось; после извлечения SD и перезагрузки плата **сама, без кнопки**,
|
||||||
|
через несколько циклов ушла в `recovery_mode`.
|
||||||
|
|
||||||
|
**Причина — не новый баг кода, а ошибка в самом чек-листе** (`HARDWARE_VERIFICATION_PHASE6.md`,
|
||||||
|
сценарий 4, шаг 2): в качестве `TFT_APP.BIN` был явно предписан `stub_b_v2_confirmed.bin` — а
|
||||||
|
recovery-режим (`update_policy_decide(recovery_mode=true)`) **всегда** целится в Slot A. Файл
|
||||||
|
`stub_b_v2_confirmed.bin` линкован под адрес Slot Б (`0x60240000`); физически положенный в Slot A,
|
||||||
|
он проходит крипто-гейт (подпись валидна для содержимого файла независимо от адреса — тот же класс
|
||||||
|
находки, что уже был пойман в Фазе 3 §5, только теперь всплыл в recovery-пути) и "успешно
|
||||||
|
устанавливается", но исполняется с чужими абсолютными адресами → бутлоадер прыгает в нерабочий код →
|
||||||
|
CDC (обслуживаемый только бутлоадером) пропадает → WDOG циклически резетит → счётчик копится через
|
||||||
|
`NORMAL_BOOT`-путь (слот всё ещё крипто-валиден) → по достижении порога плата САМА уходит в recovery,
|
||||||
|
уже без кнопки, что и наблюдалось.
|
||||||
|
|
||||||
|
**Исправлено** — только документация: `HARDWARE_VERIFICATION_PHASE6.md`, сценарий 4 дополнен явным
|
||||||
|
предупреждением (по образцу уже существующего для сценария 2) — `TFT_APP.BIN` для recovery-теста
|
||||||
|
обязан быть `stub_a_v1_confirmed.bin` (Slot A), не `stub_b_v2_confirmed.bin`.
|
||||||
|
|
||||||
|
**Заодно, разбирая путь, нашёл и исправил смежный, реальный пробел в коде**: `attempt_boot()` в
|
||||||
|
`main.c` не инкрементировал счётчик попыток перед первым прыжком в СВЕЖЕустановленный
|
||||||
|
recovery-образ (в отличие от обычного пути установки, где `RECOVERY_NORMAL_BOOT`-ветка это уже
|
||||||
|
делала). Не меняет исход (тот же образ всё равно докопится до порога через обычные попытки), но
|
||||||
|
восстанавливает симметрию с обычным путём и сокращает число циклов до автоматического отката на
|
||||||
|
один. Добавлен `bsp_boot_attempt_inc()` перед `jump_now()` в recovery-ветке. ARM-сборка зелёная
|
||||||
|
(`m_text` 90576 Б).
|
||||||
|
|
||||||
**6c — Контракт tft_app** (документация под будущий план tft_app, кода в загрузчике нет)
|
**6c — Контракт tft_app** (документация под будущий план tft_app, кода в загрузчике нет)
|
||||||
- tft_app ОБЯЗАН: (1) обслуживать watchdog (alive-flag идиом); (2) `boot_set_confirmed()` —
|
- tft_app ОБЯЗАН: (1) обслуживать watchdog (alive-flag идиом); (2) `boot_set_confirmed()` —
|
||||||
отложенно, после доказанного здоровья; (3) `bsp_boot_health_mark()` — дойдя до устойчивого
|
отложенно, после доказанного здоровья; (3) `bsp_boot_health_mark()` — дойдя до устойчивого
|
||||||
|
|
@ -863,14 +982,36 @@ recovery-LED/CDC это показывают), лечится SD-фиксом и
|
||||||
- Опционально: tft_app может переконфигурировать таймаут WDOG (`WCR.WT` переписываем, `WDE` — нет)
|
- Опционально: tft_app может переконфигурировать таймаут WDOG (`WCR.WT` переписываем, `WDE` — нет)
|
||||||
под свои длинные операции.
|
под свои длинные операции.
|
||||||
|
|
||||||
### Стенд (доработки `test_stub`)
|
### ✅ Реализовано (2026-07-13): Стенд (доработки `test_stub`)
|
||||||
|
|
||||||
Опции компиляции/поведения заглушки, чтобы прогнать все классы на железе:
|
Три независимые оси, задаются компилятору (`add_mcuboot_stub()` в `test_stub/CMakeLists.txt`,
|
||||||
- confirm: сразу / отложенно (через N c) / никогда;
|
именованные параметры `CONFIRM_MODE`/`HANG_MODE`/`FEED_WDOG`, см. `main.c`):
|
||||||
- зависнуть: никогда / до health-mark / после health-mark / после confirm;
|
- `STUB_CONFIRM_MODE` — 0 сразу / 1 отложенно (`STUB_CONFIRM_DELAY_MS`) / 2 никогда.
|
||||||
- обслуживать watchdog: да / нет.
|
- `STUB_HANG_MODE` — 0 никогда / 1 до health-mark / 2 после health-mark, до confirm / 3 после confirm.
|
||||||
|
- `STUB_FEED_WDOG` — 1 кормить (дефолт) / 0 не кормить.
|
||||||
|
|
||||||
### Верификация (реальное железо — чек-лист, детали в отдельном HARDWARE_VERIFICATION при реализации)
|
Подтверждение — `boot_set_next(fap, true, true)` на **собственном** слоте (`STUB_OWN_SLOT_ID`), **не**
|
||||||
|
`boot_set_confirmed()`: та жёстко пишет в `FLASH_AREA_IMAGE_PRIMARY` (Slot A) независимо от того, откуда
|
||||||
|
реально исполняется код — для стаба в Slot Б это подтвердило бы чужой слот. Находка сделана чтением
|
||||||
|
самой реализации `boot_set_confirmed_multi()` в `bootutil_public.c`, до написания кода.
|
||||||
|
|
||||||
|
Линкуется только `bootutil_public.c` (+ `flash_map_backend.c`, `bsp_qspi_flash`) — не весь
|
||||||
|
`MCUBOOT_BOOTUTIL_SOURCES` (loader.c/image_validate.c/TinyCrypt/ASN.1): стаб не валидирует образы,
|
||||||
|
только пишет свой трейлер, а бюджет `m_text` стаба всего 32 КБ — вся связка bootutil+крипто туда не
|
||||||
|
влезла бы. `bootutil_public.c` не зависит от FIH/крипто-путей (`boot_set_next()` возвращает простой
|
||||||
|
`int`, не `fih_ret`) — собралось и слинковалось с первой попытки, без единого missing symbol.
|
||||||
|
|
||||||
|
Новые таргеты (`just build::build-mcuboot-stub`, добавлены в `mcuboot-stub-debug`): `test_slot_stub_
|
||||||
|
{a,b}_hang` (Класс A — `CONFIRM_MODE=2, HANG_MODE=1`, подписан БЕЗ `--confirm`) и `test_slot_stub_
|
||||||
|
{a,b}_confirm_hang` (Класс B — `CONFIRM_MODE=0, HANG_MODE=3`, подписан С `--confirm`). Оба варианта на
|
||||||
|
каждый физический слот — в разных сценариях чек-листа "подозреваемый" слот то A, то Б. `m_text`:
|
||||||
|
здоровые (a/b) — 20172-20196 Б (61.56-61.63%), hang-варианты — 13788 Б (42.08%, они короче — не доходят
|
||||||
|
до кода конфига встроенного подтверждения при `HANG_MODE=1`). Все 7 подписанных `.bin` — ровно по 2 МБ
|
||||||
|
(паддинг корректный, не повторилась находка Фазы 2 про потерянный `--pad`).
|
||||||
|
|
||||||
|
Подробный чек-лист команд — [test_stub/HARDWARE_VERIFICATION_PHASE6.md](test_stub/HARDWARE_VERIFICATION_PHASE6.md).
|
||||||
|
|
||||||
|
### Верификация (реальное железо — чек-лист, детали в [test_stub/HARDWARE_VERIFICATION_PHASE6.md](test_stub/HARDWARE_VERIFICATION_PHASE6.md))
|
||||||
|
|
||||||
1. **Класс A** — свежий образ, `confirm=никогда`, `hang=после jump`: watchdog-сброс → MCUboot
|
1. **Класс A** — свежий образ, `confirm=никогда`, `hang=после jump`: watchdog-сброс → MCUboot
|
||||||
стирает → откат на прежний слот (или wait_for_sd, если прежнего нет).
|
стирает → откат на прежний слот (или wait_for_sd, если прежнего нет).
|
||||||
|
|
|
||||||
|
|
@ -68,14 +68,19 @@
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/* ── Одна попытка загрузки: SD-скан + recovery-гейт (Фаза 6) + прыжок ─────
|
/* ── Одна попытка загрузки: recovery-гейт (Фаза 6) → SD-скан → прыжок ─────
|
||||||
*
|
*
|
||||||
* sd_update_check() — до чтения счётчика/состояния слотов: новый образ с SD
|
* Порядок важен: recovery_decide() должна знать, входим ли мы в recovery, ДО
|
||||||
* заслуживает полный бюджет попыток независимо от текущего счётчика (см.
|
* SD-скана — от этого зависит, каким gate'ом сканировать SD (обычным строгим
|
||||||
* "Правила обнуления счётчика" в PLAN.md), а не только после гейта.
|
* или ослабленным, update_policy_decide(recovery_mode), см. update_policy.h).
|
||||||
|
* Обратный порядок (сначала SD, потом решение) не дал бы recovery-режиму
|
||||||
|
* смысла: строгий gate никогда не поставит образ поверх "активного", даже
|
||||||
|
* если тот активный и есть подозреваемый в зависании слот.
|
||||||
*
|
*
|
||||||
* peek_slot() — та же логика, что private peek_slot() в sd_update.c: не
|
* peek_slot() — та же логика, что private peek_slot() в sd_update.c: не
|
||||||
* шарим напрямую между модулями (см. update_policy.h), копия минимальна. */
|
* шарим напрямую между модулями (см. update_policy.h), копия минимальна.
|
||||||
|
* Здесь — только для recovery_decide(); sd_update_check() независимо
|
||||||
|
* повторно пикает слоты внутри себя для update_policy_decide(). */
|
||||||
static update_policy_slot_state_t peek_slot(uint8_t fa_id)
|
static update_policy_slot_state_t peek_slot(uint8_t fa_id)
|
||||||
{
|
{
|
||||||
update_policy_slot_state_t state;
|
update_policy_slot_state_t state;
|
||||||
|
|
@ -83,23 +88,55 @@ static update_policy_slot_state_t peek_slot(uint8_t fa_id)
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void attempt_boot(bool downgrade_held, bool recovery_held)
|
static void jump_now(void)
|
||||||
{
|
{
|
||||||
sd_update_check(downgrade_held); /* no-op быстро, если SD не вставлена */
|
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
||||||
|
boot_select_and_jump(); /* при успехе не возвращается */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true, если по итогам этой попытки мы (остаёмся) в recovery-режиме
|
||||||
|
* — main() использует это для LED-паттерна/CDC-статуса (Фаза 6b).
|
||||||
|
*/
|
||||||
|
static bool attempt_boot(bool downgrade_held, bool recovery_held)
|
||||||
|
{
|
||||||
update_policy_slot_state_t slot_a = peek_slot(0U);
|
update_policy_slot_state_t slot_a = peek_slot(0U);
|
||||||
update_policy_slot_state_t slot_b = peek_slot(1U);
|
update_policy_slot_state_t slot_b = peek_slot(1U);
|
||||||
|
|
||||||
recovery_decision_t decision = recovery_decide(
|
recovery_decision_t decision = recovery_decide(
|
||||||
bsp_boot_attempt_count(), RECOVERY_DEFAULT_THRESHOLD, &slot_a, &slot_b, recovery_held);
|
bsp_boot_attempt_count(), RECOVERY_DEFAULT_THRESHOLD, &slot_a, &slot_b, recovery_held);
|
||||||
|
|
||||||
|
bool enter_recovery = (decision.action == RECOVERY_ENTER_RECOVERY_MODE);
|
||||||
|
|
||||||
|
bool installed = sd_update_check(downgrade_held, enter_recovery);
|
||||||
|
if (installed)
|
||||||
|
{
|
||||||
|
bsp_boot_attempt_reset(); /* новый образ — новый полный бюджет попыток */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enter_recovery)
|
||||||
|
{
|
||||||
|
if (installed)
|
||||||
|
{
|
||||||
|
/* Recovery только что поставил валидный образ в Slot A —
|
||||||
|
* прыгаем немедленно, не дожидаясь следующего пере-скана.
|
||||||
|
* Инкремент — как и в обычном пути (см. RECOVERY_NORMAL_BOOT
|
||||||
|
* ниже): первая попытка прыжка в свежий образ тоже расходует
|
||||||
|
* бюджет попыток, симметрично обычной установке. */
|
||||||
|
bsp_boot_attempt_inc();
|
||||||
|
jump_now();
|
||||||
|
}
|
||||||
|
/* Кандидата не нашлось/не прошёл гейт — остаёмся в recovery. */
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
switch (decision.action)
|
switch (decision.action)
|
||||||
{
|
{
|
||||||
case RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER:
|
case RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER:
|
||||||
{
|
{
|
||||||
/* Подозреваемый в зависании слот — стереть, есть подтверждённый
|
/* Подозреваемый в зависании слот — стереть, есть подтверждённый
|
||||||
* фолбэк (recovery_decide() это уже проверила). boot_go() внутри
|
* фолбэк (recovery_decide() это уже проверила). boot_go() внутри
|
||||||
* boot_select_and_jump() ниже сам выберет оставшийся. */
|
* jump_now() сам выберет оставшийся. */
|
||||||
const struct flash_area *p_fap;
|
const struct flash_area *p_fap;
|
||||||
if (flash_area_open((uint8_t) decision.active_slot, &p_fap) == 0)
|
if (flash_area_open((uint8_t) decision.active_slot, &p_fap) == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -107,23 +144,26 @@ static void attempt_boot(bool downgrade_held, bool recovery_held)
|
||||||
flash_area_close(p_fap);
|
flash_area_close(p_fap);
|
||||||
}
|
}
|
||||||
bsp_boot_attempt_reset(); /* ситуация изменилась — новый полный бюджет */
|
bsp_boot_attempt_reset(); /* ситуация изменилась — новый полный бюджет */
|
||||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
jump_now();
|
||||||
boot_select_and_jump(); /* при успехе не возвращается */
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case RECOVERY_ENTER_RECOVERY_MODE:
|
|
||||||
/* TODO(Фаза 6b): отдельное recovery-состояние — LED-паттерн, CDC
|
|
||||||
* status:recovery_mode, ослабленный version-gate на SD-установке.
|
|
||||||
* Пока просто не прыгаем — падаем в обычный цикл ожидания main(),
|
|
||||||
* как и при отсутствии валидного образа. */
|
|
||||||
break;
|
|
||||||
case RECOVERY_NORMAL_BOOT:
|
case RECOVERY_NORMAL_BOOT:
|
||||||
default:
|
default:
|
||||||
bsp_boot_attempt_inc(); /* перед попыткой — см. bsp/boot_state.h */
|
/* Инкремент только если действительно ЕСТЬ что пытаться загрузить
|
||||||
bsp_wdog_refresh(); /* образ унаследует полное окно таймаута */
|
* (слот уже валиден, либо только что установлен этим же вызовом) —
|
||||||
boot_select_and_jump(); /* при успехе не возвращается */
|
* иначе на чисто пустой плате без SD счётчик рос бы и на пустом
|
||||||
|
* месте, и через threshold попыток (несколько секунд) recovery_decide()
|
||||||
|
* ошибочно увела бы в recovery-режим при отсутствии какого-либо
|
||||||
|
* реального зависания (регрессия к сценарию 4 Фазы 3 — "оба слота
|
||||||
|
* пусты" должен оставаться в обычном ожидании SD бесконечно). */
|
||||||
|
if (installed || slot_a.valid || slot_b.valid)
|
||||||
|
{
|
||||||
|
bsp_boot_attempt_inc(); /* перед попыткой — см. bsp/boot_state.h */
|
||||||
|
}
|
||||||
|
jump_now();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
|
|
@ -132,6 +172,11 @@ 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;
|
||||||
|
/* Recovery-паттерн (Фаза 6b): оба LED синхронно, 100 мс вкл/100 мс выкл —
|
||||||
|
* чётко отличается от heartbeat (50/450, один LED) и app (500/250,
|
||||||
|
* один LED, см. test_stub) визуально, без дополнительной телеметрии. */
|
||||||
|
const uint32_t RECOVERY_BLINK_ON_MS = 100U;
|
||||||
|
const uint32_t RECOVERY_BLINK_PERIOD_MS = 200U;
|
||||||
/* Таймаут WDOG. С запасом над самым долгим НАКОРМЛЕННЫМ участком: между
|
/* Таймаут WDOG. С запасом над самым долгим НАКОРМЛЕННЫМ участком: между
|
||||||
* соседними refresh худший легитимный интервал — одиночное стирание 64 КБ
|
* соседними refresh худший легитимный интервал — одиночное стирание 64 КБ
|
||||||
* блока (~0.15..2 c по даташиту W25Q) либо цепочка bsp_sd_init+f_mount+пик
|
* блока (~0.15..2 c по даташиту W25Q) либо цепочка bsp_sd_init+f_mount+пик
|
||||||
|
|
@ -166,9 +211,15 @@ int main(void)
|
||||||
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);
|
||||||
|
|
||||||
|
/* Отслеживает recovery-состояние между попытками — main-loop использует
|
||||||
|
* его для LED-паттерна каждую итерацию, не только на попытках прыжка
|
||||||
|
* (attempt_boot() зовётся раз в SD_RETRY_PERIOD_MS, LED должен обновляться
|
||||||
|
* значительно чаще). */
|
||||||
|
bool in_recovery = false;
|
||||||
|
|
||||||
if (qspi_ok)
|
if (qspi_ok)
|
||||||
{
|
{
|
||||||
attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
|
in_recovery = attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Нет валидного образа ни в одном слоте (или сбой QSPI) —
|
/* Нет валидного образа ни в одном слоте (или сбой QSPI) —
|
||||||
|
|
@ -200,23 +251,32 @@ int main(void)
|
||||||
bsp_usb_cdc_poll();
|
bsp_usb_cdc_poll();
|
||||||
cli_process();
|
cli_process();
|
||||||
|
|
||||||
uint32_t heartbeat_phase_ms = bsp_tick_get_ms() % HEARTBEAT_PERIOD_MS;
|
if (in_recovery)
|
||||||
if (heartbeat_phase_ms < HEARTBEAT_ON_MS)
|
|
||||||
{
|
{
|
||||||
bsp_led_on(LED_HEARTBEAT);
|
bool recovery_led_on = (bsp_tick_get_ms() % RECOVERY_BLINK_PERIOD_MS) < RECOVERY_BLINK_ON_MS;
|
||||||
|
bsp_led_set(LED_HEARTBEAT, recovery_led_on);
|
||||||
|
bsp_led_set(LED_APP, recovery_led_on);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bsp_led_off(LED_HEARTBEAT);
|
uint32_t heartbeat_phase_ms = bsp_tick_get_ms() % HEARTBEAT_PERIOD_MS;
|
||||||
|
if (heartbeat_phase_ms < HEARTBEAT_ON_MS)
|
||||||
|
{
|
||||||
|
bsp_led_on(LED_HEARTBEAT);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bsp_led_off(LED_HEARTBEAT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (qspi_ok && ((int32_t) (bsp_tick_get_ms() - next_sd_retry_ms) >= 0))
|
if (qspi_ok && ((int32_t) (bsp_tick_get_ms() - next_sd_retry_ms) >= 0))
|
||||||
{
|
{
|
||||||
next_sd_retry_ms = bsp_tick_get_ms() + SD_RETRY_PERIOD_MS;
|
next_sd_retry_ms = bsp_tick_get_ms() + SD_RETRY_PERIOD_MS;
|
||||||
|
|
||||||
protocol_send_status("waiting_for_sd");
|
protocol_send_status(in_recovery ? "recovery_mode" : "waiting_for_sd");
|
||||||
|
|
||||||
attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
|
in_recovery = attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@
|
||||||
* error — ошибка протокола или парсинга
|
* error — ошибка протокола или парсинга
|
||||||
*
|
*
|
||||||
* Типы исходящих событий (Фаза 3):
|
* Типы исходящих событий (Фаза 3):
|
||||||
* status — top-level состояние bootloader (waiting_for_sd и т.д.,
|
* status — top-level состояние bootloader (waiting_for_sd,
|
||||||
* см. protocol_send_status())
|
* installing, update_skipped, recovery_mode — см.
|
||||||
|
* protocol_send_status())
|
||||||
* wdog — статус аппаратного watchdog (armed/timeout/recovered,
|
* wdog — статус аппаратного watchdog (armed/timeout/recovered,
|
||||||
* см. protocol_send_wdog_status())
|
* см. protocol_send_wdog_status())
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*
|
*
|
||||||
* Целевой слот при установке — всегда НЕ активный (см. update_policy.h):
|
* Целевой слот при установке — всегда НЕ активный (см. update_policy.h):
|
||||||
* уже выбранный на этот момент слот этой функцией никогда не стирается.
|
* уже выбранный на этот момент слот этой функцией никогда не стирается.
|
||||||
|
* Исключение — recovery-режим (Фаза 6b, update_policy_decide(recovery_mode)):
|
||||||
|
* там целевой слот всегда Slot A, независимо от того, что было активно.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "sd_update.h"
|
#include "sd_update.h"
|
||||||
|
|
@ -39,9 +41,7 @@
|
||||||
/** @brief Размер чанка потокового копирования SD → flash. */
|
/** @brief Размер чанка потокового копирования SD → flash. */
|
||||||
#define SD_UPDATE_CHUNK_SIZE 4096U
|
#define SD_UPDATE_CHUNK_SIZE 4096U
|
||||||
|
|
||||||
/* ── Состояние модуля ──────────────────────────────────────────────────────
|
/* ── Состояние модуля ────────────────────────────────────────────────────── */
|
||||||
* Статические буферы — не на стеке (см. firmware/test/src/tests/test_usd.c
|
|
||||||
* про ограниченный стек bare-metal прошивок). */
|
|
||||||
|
|
||||||
static FATFS g_s_fs;
|
static FATFS g_s_fs;
|
||||||
static FIL g_s_file;
|
static FIL g_s_file;
|
||||||
|
|
@ -139,7 +139,11 @@ static bool erase_and_copy_candidate(const struct flash_area *p_fap, uint32_t fi
|
||||||
|
|
||||||
/* ── Основной сценарий ────────────────────────────────────────────────── */
|
/* ── Основной сценарий ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
static void run_update(bool button_held)
|
/**
|
||||||
|
* @return true, если target_slot после этого вызова содержит новый,
|
||||||
|
* подтверждённый (slot_version_get()) образ — см. sd_update.h.
|
||||||
|
*/
|
||||||
|
static bool run_update(bool button_held, bool recovery_mode)
|
||||||
{
|
{
|
||||||
struct image_version candidate_ver;
|
struct image_version candidate_ver;
|
||||||
struct image_version installed_ver;
|
struct image_version installed_ver;
|
||||||
|
|
@ -149,23 +153,24 @@ static void run_update(bool button_held)
|
||||||
const struct flash_area *p_fap = NULL;
|
const struct flash_area *p_fap = NULL;
|
||||||
uint32_t file_size;
|
uint32_t file_size;
|
||||||
bool copy_ok;
|
bool copy_ok;
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
if (bsp_sd_init() != BSP_OK)
|
if (bsp_sd_init() != BSP_OK)
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f_mount(&g_s_fs, SD_UPDATE_MOUNT_POINT, 1) != FR_OK)
|
if (f_mount(&g_s_fs, SD_UPDATE_MOUNT_POINT, 1) != FR_OK)
|
||||||
{
|
{
|
||||||
(void) bsp_sd_deinit();
|
(void) bsp_sd_deinit();
|
||||||
return; /* нет карты/файловой системы — штатно, не ошибка */
|
return false; /* нет карты/файловой системы — штатно, не ошибка */
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f_open(&g_s_file, SD_UPDATE_FILE_PATH, FA_READ) != FR_OK)
|
if (f_open(&g_s_file, SD_UPDATE_FILE_PATH, FA_READ) != FR_OK)
|
||||||
{
|
{
|
||||||
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
|
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
|
||||||
(void) bsp_sd_deinit();
|
(void) bsp_sd_deinit();
|
||||||
return; /* TFT_APP.BIN отсутствует — тоже штатно */
|
return false; /* TFT_APP.BIN отсутствует — тоже штатно */
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!read_candidate_header(&candidate_ver))
|
if (!read_candidate_header(&candidate_ver))
|
||||||
|
|
@ -177,8 +182,10 @@ static void run_update(bool button_held)
|
||||||
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 — сэмплирован при старте в main.c и передан сюда (см. sd_update.h). */
|
/* button_held — сэмплирован при старте в main.c и передан сюда (см.
|
||||||
decision = update_policy_decide(&slot_a, &slot_b, &candidate_ver, button_held);
|
* sd_update.h). recovery_mode — ослабленный gate Фазы 6b, см.
|
||||||
|
* update_policy.h; решение "входить ли в recovery" — не здесь. */
|
||||||
|
decision = update_policy_decide(&slot_a, &slot_b, &candidate_ver, button_held, recovery_mode);
|
||||||
|
|
||||||
if (decision.action == UPDATE_POLICY_SKIP)
|
if (decision.action == UPDATE_POLICY_SKIP)
|
||||||
{
|
{
|
||||||
|
|
@ -209,11 +216,17 @@ static void run_update(bool button_held)
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Форс. даунгрейд (см. update_policy.h): без стирания прежнего активного
|
/* target_slot подтверждён валидным — установка состоялась независимо от
|
||||||
* слота он остался бы валиден и новее только что установленного, и
|
* исхода стирания "второго" слота ниже (оно диагностируется отдельным
|
||||||
* снова выиграл бы в boot_go() — даунгрейд физически записался бы, но
|
* protocol_send_error, но не отменяет уже подтверждённый результат). */
|
||||||
* не загрузился. Стираем ТОЛЬКО теперь, когда новый образ уже подтверждён
|
result = true;
|
||||||
* валидным — на диске никогда не бывает нуля рабочих слотов. */
|
|
||||||
|
/* Форс. даунгрейд и recovery (см. update_policy.h): без стирания
|
||||||
|
* прежнего активного/Slot Б он остался бы валиден (и новее в обычном
|
||||||
|
* режиме) и снова выиграл бы в boot_go() — даунгрейд/recovery физически
|
||||||
|
* записались бы, но не загрузились. Стираем ТОЛЬКО теперь, когда новый
|
||||||
|
* образ уже подтверждён валидным — на диске никогда не бывает нуля
|
||||||
|
* рабочих слотов. */
|
||||||
if (decision.erase_previous_active)
|
if (decision.erase_previous_active)
|
||||||
{
|
{
|
||||||
const struct flash_area *p_peer_fap;
|
const struct flash_area *p_peer_fap;
|
||||||
|
|
@ -238,16 +251,17 @@ cleanup:
|
||||||
(void) f_close(&g_s_file);
|
(void) f_close(&g_s_file);
|
||||||
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
|
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
|
||||||
(void) bsp_sd_deinit();
|
(void) bsp_sd_deinit();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
/* ── Public API ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
void sd_update_check(bool downgrade_button_held)
|
bool sd_update_check(bool downgrade_button_held, bool recovery_mode)
|
||||||
{
|
{
|
||||||
if (!bsp_sd_is_inserted())
|
if (!bsp_sd_is_inserted())
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
run_update(downgrade_button_held);
|
return run_update(downgrade_button_held, recovery_mode);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,12 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Одна попытка: смонтировать SD, найти TFT_APP.BIN, при необходимости
|
* @brief Одна попытка: смонтировать SD, найти TFT_APP.BIN, при необходимости
|
||||||
* установить его в неактивный слот.
|
* установить его в целевой слот (см. update_policy.h).
|
||||||
*
|
*
|
||||||
* Пишет только в неактивный слот (см. update_policy.h) — уже выбранный/
|
* Обычный режим (recovery_mode == false) пишет только в неактивный слот —
|
||||||
* загружаемый слот никогда не трогается. Сам не вызывает boot_go()/
|
* уже выбранный/загружаемый слот никогда не трогается. Recovery-режим
|
||||||
|
* (Фаза 6b) — ослабленный gate: любой подписанный кандидат ставится в Slot A
|
||||||
|
* безусловно, Slot Б стирается. Сам не вызывает boot_go()/
|
||||||
* boot_select_and_jump() — решение "когда прыгать" остаётся за main.c,
|
* boot_select_and_jump() — решение "когда прыгать" остаётся за main.c,
|
||||||
* которое обязано вызвать его ровно один раз за сессию питания (см.
|
* которое обязано вызвать его ровно один раз за сессию питания (см.
|
||||||
* slot_version.h о том, почему boot_go() нельзя звать повторно).
|
* slot_version.h о том, почему boot_go() нельзя звать повторно).
|
||||||
|
|
@ -29,9 +31,20 @@
|
||||||
* (см. update_policy.h). Передаётся, а не читается здесь, чтобы жест
|
* (см. update_policy.h). Передаётся, а не читается здесь, чтобы жест
|
||||||
* "удержание при включении" ловился в предсказуемый ранний момент, а не
|
* "удержание при включении" ловился в предсказуемый ранний момент, а не
|
||||||
* через несколько секунд внутри run_update() (DEBUG_LOG_PHASE3_SD.md).
|
* через несколько секунд внутри run_update() (DEBUG_LOG_PHASE3_SD.md).
|
||||||
|
* Игнорируется при recovery_mode == true.
|
||||||
|
* @param recovery_mode Ослабленный version-gate (Фаза 6b) — см.
|
||||||
|
* update_policy_decide(). Решение "входить ли в recovery" принимает
|
||||||
|
* вызывающий код (recovery_decide(), recovery.h), не эта функция.
|
||||||
|
*
|
||||||
|
* @retval true Кандидат успешно установлен и прошёл финальный крипто-гейт
|
||||||
|
* (slot_version_get() на целевом слоте) — в целевом слоте
|
||||||
|
* теперь новый валидный образ.
|
||||||
|
* @retval false Ничего не установлено (SD не вставлена/нет файла/skip) либо
|
||||||
|
* установка не удалась/кандидат отклонён — целевой слот не
|
||||||
|
* изменился относительно состояния до вызова.
|
||||||
*
|
*
|
||||||
* @pre bsp_qspi_init() уже вызван.
|
* @pre bsp_qspi_init() уже вызван.
|
||||||
*/
|
*/
|
||||||
void sd_update_check(bool downgrade_button_held);
|
bool sd_update_check(bool downgrade_button_held, bool recovery_mode);
|
||||||
|
|
||||||
#endif /* SD_UPDATE_H_ */
|
#endif /* SD_UPDATE_H_ */
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,18 @@ static update_policy_slot_t other_slot(update_policy_slot_t slot)
|
||||||
update_policy_result_t update_policy_decide(const update_policy_slot_state_t *p_slot_a,
|
update_policy_result_t update_policy_decide(const update_policy_slot_state_t *p_slot_a,
|
||||||
const update_policy_slot_state_t *p_slot_b,
|
const update_policy_slot_state_t *p_slot_b,
|
||||||
const struct image_version *p_candidate_ver,
|
const struct image_version *p_candidate_ver,
|
||||||
bool button_held)
|
bool button_held, bool recovery_mode)
|
||||||
{
|
{
|
||||||
|
if (recovery_mode)
|
||||||
|
{
|
||||||
|
/* Ослабленный гейт: версия/кнопка не участвуют, целевой слот всегда
|
||||||
|
* A, Slot Б обязан быть стёрт (см. recovery.h — этот флаг не связан
|
||||||
|
* с recovery_decide() там). */
|
||||||
|
return (update_policy_result_t) { .action = UPDATE_POLICY_INSTALL,
|
||||||
|
.target_slot = UPDATE_POLICY_SLOT_A,
|
||||||
|
.erase_previous_active = true };
|
||||||
|
}
|
||||||
|
|
||||||
active_slot_info_t active = find_active_slot(p_slot_a, p_slot_b);
|
active_slot_info_t active = find_active_slot(p_slot_a, p_slot_b);
|
||||||
|
|
||||||
if (!active.have_active)
|
if (!active.have_active)
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ typedef struct
|
||||||
/**
|
/**
|
||||||
* @brief Решить, устанавливать ли SD-кандидат, и в какой слот.
|
* @brief Решить, устанавливать ли SD-кандидат, и в какой слот.
|
||||||
*
|
*
|
||||||
* Правила:
|
* Правила (обычный режим, recovery_mode == false):
|
||||||
* - "Активный" слот — валидный слот с более высокой версией; если валиден
|
* - "Активный" слот — валидный слот с более высокой версией; если валиден
|
||||||
* только один — он активный; если ни одного — активного слота нет.
|
* только один — он активный; если ни одного — активного слота нет.
|
||||||
* - Целевой слот установки — всегда НЕ активный (активный не перезаписываем
|
* - Целевой слот установки — всегда НЕ активный (активный не перезаписываем
|
||||||
|
|
@ -71,15 +71,36 @@ typedef struct
|
||||||
* - Кандидат равен активному, кнопка удержана → SKIP (не форсируем
|
* - Кандидат равен активному, кнопка удержана → SKIP (не форсируем
|
||||||
* переустановку той же версии).
|
* переустановку той же версии).
|
||||||
*
|
*
|
||||||
|
* Recovery-режим (recovery_mode == true, Фаза 6b) — ослабленный version-gate:
|
||||||
|
* - Версия кандидата и button_held игнорируются целиком — ЛЮБОЙ кандидат,
|
||||||
|
* прошедший последующие (внешние по отношению к этой функции) проверки
|
||||||
|
* заголовка и крипто-гейта, принимается безусловно.
|
||||||
|
* - target_slot — всегда Slot A, независимо от того, какой слот был активен
|
||||||
|
* до входа в recovery (в отличие от обычного режима, где целевой слот
|
||||||
|
* вычисляется как "не активный").
|
||||||
|
* - erase_previous_active — всегда true: вызывающий код обязан стереть
|
||||||
|
* Slot Б перед/после установки в Slot A (см. rationale выше про порядок
|
||||||
|
* "стереть только после подтверждения валидности нового образа") — вместе
|
||||||
|
* с тем, что erase_and_copy_candidate() и так стирает сам target_slot
|
||||||
|
* перед записью, это и даёт "чистый борт": оба слота гарантированно
|
||||||
|
* стёрты, в Slot A — только что установленный и провалидированный образ.
|
||||||
|
*
|
||||||
* @param[in] p_slot_a Состояние Slot A.
|
* @param[in] p_slot_a Состояние Slot A.
|
||||||
* @param[in] p_slot_b Состояние Slot Б.
|
* @param[in] p_slot_b Состояние Slot Б.
|
||||||
* @param[in] p_candidate_ver Версия образа-кандидата на SD.
|
* @param[in] p_candidate_ver Версия образа-кандидата на SD. Не читается при
|
||||||
|
* recovery_mode == true.
|
||||||
* @param[in] button_held Кнопка даунгрейда (BSP_BUTTON_1) удержана на старте.
|
* @param[in] button_held Кнопка даунгрейда (BSP_BUTTON_1) удержана на старте.
|
||||||
|
* Не читается при recovery_mode == true.
|
||||||
|
* @param[in] recovery_mode Ослабленный version-gate (см. выше). Не путать с
|
||||||
|
* recovery_decide() (recovery.h) — та решает,
|
||||||
|
* входить ли в recovery-режим вообще (счётчик
|
||||||
|
* watchdog-сбросов), эта функция — что делать с
|
||||||
|
* SD-кандидатом, уже находясь в нём.
|
||||||
*/
|
*/
|
||||||
update_policy_result_t update_policy_decide(const update_policy_slot_state_t *p_slot_a,
|
update_policy_result_t update_policy_decide(const update_policy_slot_state_t *p_slot_a,
|
||||||
const update_policy_slot_state_t *p_slot_b,
|
const update_policy_slot_state_t *p_slot_b,
|
||||||
const struct image_version *p_candidate_ver,
|
const struct image_version *p_candidate_ver,
|
||||||
bool button_held);
|
bool button_held, bool recovery_mode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Сравнить версии образов: major.minor.revision, без build_num — то
|
* @brief Сравнить версии образов: major.minor.revision, без build_num — то
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,185 @@
|
||||||
# firmware/bootloader/test_stub/CMakeLists.txt
|
# firmware/bootloader/test_stub/CMakeLists.txt
|
||||||
#
|
#
|
||||||
# Заглушка tft_app для аппаратной верификации Фазы 2 bootutil — см.
|
# Заглушка tft_app для аппаратной верификации корректной работы загрузчика
|
||||||
# firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2". Не часть
|
|
||||||
# продукта — удалить, когда появится реальный tft_app.
|
|
||||||
#
|
#
|
||||||
# Два таргета из одного main.c: разный адрес слота (--defsym __slot_base__) и
|
# Один main.c, много таргетов: адрес слота (--defsym __slot_base__) + частота
|
||||||
# разная частота мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой слот
|
# мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой слот выбрал bootloader
|
||||||
# выбрал bootloader. Постройка .bin, дальше подписывается вручную imgtool'ом
|
# плюс три независимые оси confirm/hang/watchdog — чтобы прогнать классы
|
||||||
# (см. PLAN.md) — .bin сюда не должен попасть без подписи.
|
# отказов A/B на реальном железе. Подписывается вручную imgtool.
|
||||||
|
#
|
||||||
|
include(
|
||||||
|
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake)
|
||||||
|
|
||||||
function(add_mcuboot_stub NAME SLOT_BASE BLINK_MS)
|
function(add_mcuboot_stub)
|
||||||
add_executable(${NAME} main.c ${BSP_GENERATED}/clock_config.c
|
cmake_parse_arguments(
|
||||||
${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
|
ARG ""
|
||||||
|
"NAME;SLOT_BASE;BLINK_MS;OWN_SLOT_ID;CONFIRM_MODE;HANG_MODE;FEED_WDOG" ""
|
||||||
|
${ARGN})
|
||||||
|
|
||||||
target_compile_definitions(${NAME} PRIVATE STUB_BLINK_MS=${BLINK_MS}
|
if(NOT DEFINED ARG_CONFIRM_MODE)
|
||||||
__STARTUP_CLEAR_BSS)
|
set(ARG_CONFIRM_MODE 0) # 0 сразу / 1 отложенно / 2 никогда — см. main.c
|
||||||
|
endif()
|
||||||
|
if(NOT DEFINED ARG_HANG_MODE)
|
||||||
|
set(ARG_HANG_MODE 0) # 0 никогда / 1 до health-mark / 2 после health-mark /
|
||||||
|
# 3 после confirm
|
||||||
|
endif()
|
||||||
|
if(NOT DEFINED ARG_FEED_WDOG)
|
||||||
|
set(ARG_FEED_WDOG 1) # 1 кормить (дефолт, как в проде) / 0 не кормить
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(
|
||||||
|
${ARG_NAME}
|
||||||
|
main.c
|
||||||
|
${BSP_GENERATED}/clock_config.c
|
||||||
|
${BSP_STARTUP_FILE}
|
||||||
|
${BSP_SYSCALLS_FILE}
|
||||||
|
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c)
|
||||||
|
|
||||||
|
target_include_directories(${ARG_NAME} PRIVATE ${MCUBOOT_BOOTUTIL_INCLUDES})
|
||||||
|
|
||||||
|
target_compile_definitions(
|
||||||
|
${ARG_NAME}
|
||||||
|
PRIVATE STUB_BLINK_MS=${ARG_BLINK_MS}
|
||||||
|
STUB_OWN_SLOT_ID=${ARG_OWN_SLOT_ID}
|
||||||
|
STUB_CONFIRM_MODE=${ARG_CONFIRM_MODE}
|
||||||
|
STUB_HANG_MODE=${ARG_HANG_MODE}
|
||||||
|
STUB_FEED_WDOG=${ARG_FEED_WDOG}
|
||||||
|
__STARTUP_CLEAR_BSS)
|
||||||
|
|
||||||
|
# Вендоренный bootutil_public.c — не наш стиль/warnings, тот же обход, что и
|
||||||
|
# для остального bootutil (см. bootutil_sources.cmake). Наш код (main.c,
|
||||||
|
# flash_map_backend.c) остаётся под обычными warnings.
|
||||||
|
set_source_files_properties(
|
||||||
|
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c
|
||||||
|
PROPERTIES COMPILE_OPTIONS "${MCUBOOT_VENDORED_COMPILE_OPTIONS}")
|
||||||
|
|
||||||
# bsp_wdog: загрузчик взводит WDOG перед прыжком, WDE — write-once, поэтому
|
# bsp_wdog: загрузчик взводит WDOG перед прыжком, WDE — write-once, поэтому
|
||||||
# образ ОБЯЗАН его кормить (иначе reset-loop). Заглушка — стенд-ин tft_app,
|
# образ ОБЯЗАН его кормить, если STUB_FEED_WDOG=1 (иначе reset-loop — либо
|
||||||
# тоже кормит. См. bsp/wdog/README.md.
|
# намеренно, для теста самого механизма). bsp_boot_state —
|
||||||
target_link_libraries(${NAME} PRIVATE bsp_board bsp_led bsp_tick
|
# bsp_boot_health_mark(). bsp_qspi_flash — нужен flash_map_backend.c для
|
||||||
bsp_boot_xip_no_dcd bsp_wdog)
|
# confirm_self().
|
||||||
|
target_link_libraries(
|
||||||
|
${ARG_NAME}
|
||||||
|
PRIVATE bsp_board
|
||||||
|
bsp_led
|
||||||
|
bsp_tick
|
||||||
|
bsp_boot_xip_no_dcd
|
||||||
|
bsp_wdog
|
||||||
|
bsp_boot_state
|
||||||
|
bsp_qspi_flash)
|
||||||
|
|
||||||
target_link_options(
|
target_link_options(
|
||||||
${NAME}
|
${ARG_NAME}
|
||||||
PRIVATE
|
PRIVATE
|
||||||
-Wl,--gc-sections
|
-Wl,--gc-sections
|
||||||
-Wl,--print-memory-usage
|
-Wl,--print-memory-usage
|
||||||
-Wl,-Map=${CMAKE_BINARY_DIR}/${NAME}.map
|
-Wl,-Map=${CMAKE_BINARY_DIR}/${ARG_NAME}.map
|
||||||
-Wl,--defsym=__slot_base__=${SLOT_BASE}
|
-Wl,--defsym=__slot_base__=${ARG_SLOT_BASE}
|
||||||
-Wl,--defsym=__stack_size__=0x400
|
-Wl,--defsym=__stack_size__=0x400
|
||||||
-Wl,--defsym=__heap_size__=0x400
|
-Wl,--defsym=__heap_size__=0x400
|
||||||
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld)
|
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld)
|
||||||
|
|
||||||
set_target_properties(${NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
|
set_target_properties(${ARG_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
|
||||||
${CMAKE_BINARY_DIR})
|
${CMAKE_BINARY_DIR})
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
TARGET ${NAME}
|
TARGET ${ARG_NAME}
|
||||||
POST_BUILD
|
POST_BUILD
|
||||||
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${NAME}>
|
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${ARG_NAME}>
|
||||||
${CMAKE_BINARY_DIR}/${NAME}.bin
|
${CMAKE_BINARY_DIR}/${ARG_NAME}.bin
|
||||||
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${NAME}>
|
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${ARG_NAME}>
|
||||||
COMMENT "Generating ${NAME}.bin")
|
COMMENT "Generating ${ARG_NAME}.bin")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
# Slot A: 0x60040000, мигает раз в 500 мс ("версия 1")
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
add_mcuboot_stub(test_slot_stub_a 0x60040000 500)
|
# Фаза 2/3 — базовые "здоровые" заглушки: подтверждаются сразу, никогда не
|
||||||
|
# виснут. Slot A мигает раз в 500 мс ("версия 1"), Slot Б — раз в 250 мс
|
||||||
|
# ("версия 2"). CONFIRM_MODE/HANG_MODE/FEED_WDOG — дефолты (0/0/1).
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
add_mcuboot_stub(
|
||||||
|
NAME
|
||||||
|
test_slot_stub_a
|
||||||
|
SLOT_BASE
|
||||||
|
0x60040000
|
||||||
|
BLINK_MS
|
||||||
|
500
|
||||||
|
OWN_SLOT_ID
|
||||||
|
0)
|
||||||
|
add_mcuboot_stub(
|
||||||
|
NAME
|
||||||
|
test_slot_stub_b
|
||||||
|
SLOT_BASE
|
||||||
|
0x60240000
|
||||||
|
BLINK_MS
|
||||||
|
250
|
||||||
|
OWN_SLOT_ID
|
||||||
|
1)
|
||||||
|
|
||||||
# Slot Б: 0x60240000, мигает раз в 250 мс ("версия 2")
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
add_mcuboot_stub(test_slot_stub_b 0x60240000 250)
|
# Фаза 6 — стенд для чек-листа recovery (см. PLAN.md, "Стенд"). Оба варианта
|
||||||
|
# (Slot A / Slot Б), т.к. в разных сценариях чек-листа "подозреваемый" слот — то
|
||||||
|
# один, то другой.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Класс A: никогда не подтверждается, виснет СРАЗУ (до health-mark) — свежий
|
||||||
|
# неподтверждённый образ виснет до всякого прогресса. WDOG-сброс → штатный
|
||||||
|
# MCUboot revert (copy_done без image_ok) → откат, без участия recovery.c.
|
||||||
|
add_mcuboot_stub(
|
||||||
|
NAME
|
||||||
|
test_slot_stub_a_hang
|
||||||
|
SLOT_BASE
|
||||||
|
0x60040000
|
||||||
|
BLINK_MS
|
||||||
|
500
|
||||||
|
OWN_SLOT_ID
|
||||||
|
0
|
||||||
|
CONFIRM_MODE
|
||||||
|
2
|
||||||
|
HANG_MODE
|
||||||
|
1)
|
||||||
|
add_mcuboot_stub(
|
||||||
|
NAME
|
||||||
|
test_slot_stub_b_hang
|
||||||
|
SLOT_BASE
|
||||||
|
0x60240000
|
||||||
|
BLINK_MS
|
||||||
|
250
|
||||||
|
OWN_SLOT_ID
|
||||||
|
1
|
||||||
|
CONFIRM_MODE
|
||||||
|
2
|
||||||
|
HANG_MODE
|
||||||
|
1)
|
||||||
|
|
||||||
|
# Класс B: подтверждается СРАЗУ (успевает помигать наблюдаемо — видно, что образ
|
||||||
|
# живой и подтверждён), виснет через STUB_CONFIRM_DELAY_MS + STUB_HANG_DELAY_MS
|
||||||
|
# (~6 c) после старта — уже ПОСЛЕ confirm. Штатный revert MCUboot тут не
|
||||||
|
# сработает (image_ok уже SET) — ловит именно recovery.c (счётчик
|
||||||
|
# watchdog-сбросов + фолбэк/recovery).
|
||||||
|
add_mcuboot_stub(
|
||||||
|
NAME
|
||||||
|
test_slot_stub_a_confirm_hang
|
||||||
|
SLOT_BASE
|
||||||
|
0x60040000
|
||||||
|
BLINK_MS
|
||||||
|
500
|
||||||
|
OWN_SLOT_ID
|
||||||
|
0
|
||||||
|
CONFIRM_MODE
|
||||||
|
0
|
||||||
|
HANG_MODE
|
||||||
|
3)
|
||||||
|
add_mcuboot_stub(
|
||||||
|
NAME
|
||||||
|
test_slot_stub_b_confirm_hang
|
||||||
|
SLOT_BASE
|
||||||
|
0x60240000
|
||||||
|
BLINK_MS
|
||||||
|
250
|
||||||
|
OWN_SLOT_ID
|
||||||
|
1
|
||||||
|
CONFIRM_MODE
|
||||||
|
0
|
||||||
|
HANG_MODE
|
||||||
|
3)
|
||||||
|
|
|
||||||
330
firmware/bootloader/test_stub/HARDWARE_VERIFICATION_PHASE6.md
Normal file
330
firmware/bootloader/test_stub/HARDWARE_VERIFICATION_PHASE6.md
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
# Фаза 6 (recovery) — аппаратная верификация
|
||||||
|
|
||||||
|
Памятка с готовыми командами: сборка, прошивка, подготовка стенда, чек-лист. Продолжение
|
||||||
|
[HARDWARE_VERIFICATION_PHASE3.md](HARDWARE_VERIFICATION_PHASE3.md) — предполагает, что прошивка
|
||||||
|
bootloader и базовые заглушки слотов (`test_slot_stub_a`/`_b`) уже знакомы по Фазам 2/3.
|
||||||
|
|
||||||
|
Все команды — из корня репозитория (`tft_manufacture_test/`). Сборка и host-тесты — в devcontainer;
|
||||||
|
прошивка/отладка/проверки на железе — на стороне пользователя.
|
||||||
|
|
||||||
|
**Статус (2026-07-14): сценарии 1-4 пройдены.** По пути два найденных и исправленных пробела (см.
|
||||||
|
PLAN.md, разделы "Найден и исправлен баг (на железе, сценарий 2/4)"):
|
||||||
|
- Сценарий 2 сначала не проходил — стенд (`test_stub`) обнулял счётчик попыток раньше, чем
|
||||||
|
зависание успевало засчитаться; после фикса и переподписи (`just build::build-mcuboot-stub`) —
|
||||||
|
подтверждён.
|
||||||
|
- Сценарий 4 сначала не проходил — сам чек-лист (этот файл) предписывал не тот `TFT_APP.BIN`
|
||||||
|
(линкован под чужой слот); исправлен текст сценария 4 ниже + попутно найден и исправлен реальный
|
||||||
|
пробел в `main.c` (счётчик не инкрементировался на первом прыжке в свежий recovery-образ). После
|
||||||
|
фикса — подтверждён.
|
||||||
|
|
||||||
|
Сценарий 5 (транзиент/per-session) — подтверждён: POR без BTN_2 обнуляет счётчик, зависший слот
|
||||||
|
пробуется заново, детерминированное зависание повторяет цикл 1→2→3 и снова приходит в
|
||||||
|
`recovery_mode` — ровно ожидаемая per-session семантика (не отдельный флеш-маркер).
|
||||||
|
|
||||||
|
Сценарий 6 (регрессия) — подтверждён: чек-лист Фазы 3 + пустая плата 2+ мин без единого ложного
|
||||||
|
recovery/сброса.
|
||||||
|
|
||||||
|
**✅ Все 6 сценариев Фазы 6 пройдены на реальной плате (2026-07-14).**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Предпосылки
|
||||||
|
|
||||||
|
- `firmware/bootloader` собран с кодом Фазы 6: `bsp/boot_state` (счётчик попыток в `SRC_GPR3`,
|
||||||
|
POR-детект), `recovery.c` (`recovery_decide()` — таксономия классов B/C/D), `update_policy.c`
|
||||||
|
(флаг `recovery_mode` — ослабленный gate), `main.c` (`attempt_boot()` — recovery-гейт → SD-скан →
|
||||||
|
прыжок, LED-паттерн, CDC-статус).
|
||||||
|
- Класс A (незавершённая установка) закрывается штатным revert MCUboot автоматически — код
|
||||||
|
загрузчика в этом не участвует, проверяется тем не менее (сценарий 1 ниже), т.к. счётчик всё равно
|
||||||
|
инкрементируется на каждой такой попытке (см. находку про фикс счётчика в PLAN.md).
|
||||||
|
- Watchdog (Фаза 3) уже аппаратно верифицирован — таймаут 10 c, см.
|
||||||
|
[HARDWARE_VERIFICATION_PHASE3.md](HARDWARE_VERIFICATION_PHASE3.md), раздел 9. Здесь ту же
|
||||||
|
10-секундную защиту используем как механизм провокации, не проверяем заново.
|
||||||
|
- Порог фолбэка/recovery — `RECOVERY_DEFAULT_THRESHOLD = 3` (сбросов подряд).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Сборка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bootloader (Debug) + HAB-контейнер
|
||||||
|
just build::build-bootloader-debug
|
||||||
|
just build::hab-bootloader-debug
|
||||||
|
|
||||||
|
# Стаб-образы (базовые Фазы 2/3 + новые Фазы 6) — одной командой
|
||||||
|
just build::build-mcuboot-stub
|
||||||
|
```
|
||||||
|
|
||||||
|
После этого в `build/Debug/signed/` — 7 подписанных файлов, все ровно по 2 МБ:
|
||||||
|
|
||||||
|
| Файл | Версия | Слот (адрес линковки) | Подтверждён при прошивке | Поведение в рантайме |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `stub_a_v1_confirmed.bin` | 1.0.0 | Slot A (`0x60040000`) | да (`--confirm`) | здоров, никогда не виснет, мигает ~1/сек |
|
||||||
|
| `stub_b_v2_confirmed.bin` | 2.0.0 | Slot Б (`0x60240000`) | да | здоров, никогда не виснет, мигает ~2/сек |
|
||||||
|
| `stub_a_v1_unconfirmed.bin` | 1.0.0 | Slot A | нет | здоров, но revert-тест Фазы 2 (не используется здесь) |
|
||||||
|
| `stub_a_hang_class_a.bin` | 1.0.0 | Slot A | нет | **Класс A**: никогда не подтверждается, виснет СРАЗУ |
|
||||||
|
| `stub_b_hang_class_a.bin` | 2.0.0 | Slot Б | нет | то же, для Slot Б |
|
||||||
|
| `stub_a_confirm_hang_class_b.bin` | 1.0.0 | Slot A | да | **Класс B**: подтверждается сразу, мигает ~6 c, виснет |
|
||||||
|
| `stub_b_confirm_hang_class_b.bin` | 2.0.0 | Slot Б | да | то же, для Slot Б |
|
||||||
|
|
||||||
|
> ⚠️ **Версии фиксированы по слоту линковки (1.0.0 = A, 2.0.0 = Б) — это ограничивает выбор файлов
|
||||||
|
> для сценария "класс B, есть фолбэк".** `boot_go()` всегда выбирает более высокую версию как
|
||||||
|
> активную. Если положить `stub_a_confirm_hang_class_b.bin` (v1.0.0) в Slot A вместе со
|
||||||
|
> `stub_b_v2_confirmed.bin` (v2.0.0) в Slot Б — активным станет Slot Б (здоровый!), а зависающий
|
||||||
|
> Slot A вообще не будет выбран, и тест ничего не покажет. **Правильная комбинация для "есть
|
||||||
|
> фолбэк"**: `stub_b_confirm_hang_class_b.bin` (v2.0.0, виснет) в Slot Б + `stub_a_v1_confirmed.bin`
|
||||||
|
> (v1.0.0, здоров) в Slot A — тогда активный (Slot Б, выше версия) реально виснет, а фолбэк (Slot A,
|
||||||
|
> валиден) ждёт своей очереди. См. сценарий 2 ниже — именно эта комбинация.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Прошивка bootloader
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just host::flash-swd-bootloader-debug
|
||||||
|
# обязателен power cycle платы после прошивки
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Прошивка образов в слоты напрямую (pyOCD)
|
||||||
|
|
||||||
|
Тот же способ, что в Фазах 2/3 — сырой подписанный `.bin` напрямую по адресу слота, путь абсолютный
|
||||||
|
(`"$(pwd)/..."`, команда — из корня репозитория).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Slot A (0x60040000) — любой из файлов таблицы выше с адресом Slot A
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60040000 --erase sector "$(pwd)/build/Debug/signed/<файл>.bin"
|
||||||
|
|
||||||
|
# Slot Б (0x60240000) — любой из файлов таблицы выше с адресом Slot Б
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60240000 --erase sector "$(pwd)/build/Debug/signed/<файл>.bin"
|
||||||
|
|
||||||
|
# Стереть слот целиком (адрес — позиционный аргумент, start+length)
|
||||||
|
uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--sector 0x60040000+0x200000 # Slot A
|
||||||
|
uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--sector 0x60240000+0x200000 # Slot Б
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. CDC — статусы и команды
|
||||||
|
|
||||||
|
Подключиться (см. [../README.md](../README.md)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
screen /dev/cu.usbmodemXXXX
|
||||||
|
```
|
||||||
|
|
||||||
|
- Живость: `{"type":"cmd","cmd":"ping"}` → `{"type":"pong"}`.
|
||||||
|
- Статус watchdog + счётчика: `{"type":"cmd","cmd":"wdog"}` →
|
||||||
|
`{"type":"wdog","armed":true,"timeout_s":10,"recovered":<bool>,"reset_count":<N>,"threshold":3}`.
|
||||||
|
`reset_count` — растущий индикатор прогресса к порогу; главный источник диагностики для
|
||||||
|
сценариев 2/3 (растёт после каждого WDOG-сброса подряд, обнуляется на POR/новой установке/фолбэке).
|
||||||
|
- Статусы (`{"type":"status","state":"..."}`): `waiting_for_sd`, `installing`, `update_skipped`
|
||||||
|
(уже знакомы по Фазе 3) + новый **`recovery_mode`** — эмитится вместо `waiting_for_sd`, пока плата
|
||||||
|
в recovery-состоянии.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Жест BTN_2 (ручной вход в recovery)
|
||||||
|
|
||||||
|
**Держать `BSP_BUTTON_2` нажатой в момент подачи питания** — считывается один раз, сразу после
|
||||||
|
`bsp_button_init()` (`main.c`), до всей SD-логики, и защёлкивается на сессию — тот же приём, что
|
||||||
|
`BSP_BUTTON_1` в Фазе 3. Приоритет над `BSP_BUTTON_1` и над обычным boot-путём разрешается внутри
|
||||||
|
`recovery_decide()` (проверяется первым, независимо от счётчика попыток и состояния слотов).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Индикация recovery-режима
|
||||||
|
|
||||||
|
- **LED**: `LED_HEARTBEAT` и `LED_APP` мигают **синхронно, 100 мс вкл / 100 мс выкл** — чётко
|
||||||
|
отличается на глаз от обычного heartbeat (50/450 мс, только `LED_HEARTBEAT`) и от app-паттернов
|
||||||
|
(500/250 мс, только `LED_APP`, см. `stub_a`/`stub_b`).
|
||||||
|
- **CDC**: `status: recovery_mode` вместо `waiting_for_sd`, повторяется каждые ~1.5 c (тот же период,
|
||||||
|
что и обычный пере-скан SD).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Чек-лист сценариев
|
||||||
|
|
||||||
|
Между КАЖДЫМ сценарием — обязательный power cycle платы (SWD-запись/установка не ресетят
|
||||||
|
автоматически). Все тайминги — ориентировочные (наблюдать глазами/секундомером, точная синхронизация
|
||||||
|
не нужна).
|
||||||
|
|
||||||
|
### Сценарий 1 — Класс A: свежий образ никогда не подтверждается, виснет сразу
|
||||||
|
|
||||||
|
**Подготовка:**
|
||||||
|
```bash
|
||||||
|
uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 --sector 0x60240000+0x200000 # Slot Б пуст
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60040000 --erase sector "$(pwd)/build/Debug/signed/stub_a_hang_class_a.bin"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ожидаемо:**
|
||||||
|
1. Power on → bootloader выбирает Slot A (единственный валидный) → прыжок → `LED_APP` **не
|
||||||
|
загорается вообще** (зависание — до `bsp_led_init()`'s первого toggle в цикле, LED остаётся в
|
||||||
|
выключенном состоянии init'а).
|
||||||
|
2. Через ~10 c — аппаратный WDOG-сброс.
|
||||||
|
3. Bootloader стартует заново; `boot_go()` видит `copy_done=SET` (уже выставлен предыдущим выбором),
|
||||||
|
`image_ok` НЕ установлен (никогда не подтверждался) → штатный MCUboot-revert → **Slot A стирается
|
||||||
|
автоматически**.
|
||||||
|
4. Плата остаётся в обычном цикле ожидания: `LED_HEARTBEAT` — обычный heartbeat (50/450 мс), **не**
|
||||||
|
recovery-паттерн; CDC `ping`→`pong` живой; `status: waiting_for_sd`.
|
||||||
|
5. Запросить `{"type":"cmd","cmd":"wdog"}` — `"recovered":true` (последний сброс был по watchdog).
|
||||||
|
|
||||||
|
**Это и есть регрессионный тест находки** (см. PLAN.md, "Найден и исправлен баг: счётчик рос и на
|
||||||
|
пустой плате") — без фикса плата вместо шага 4 ушла бы в `recovery_mode` через несколько секунд
|
||||||
|
(накопленный `reset_count`), хотя ей совершенно не о чем сигнализировать: обе полки пусты, это
|
||||||
|
штатное ожидание первой установки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Сценарий 2 — Класс B, есть фолбэк: подтверждённый образ виснет в рантайме
|
||||||
|
|
||||||
|
**Подготовка** (см. предупреждение о версиях в §1 — порядок важен):
|
||||||
|
```bash
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60040000 --erase sector "$(pwd)/build/Debug/signed/stub_a_v1_confirmed.bin"
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60240000 --erase sector "$(pwd)/build/Debug/signed/stub_b_confirm_hang_class_b.bin"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ожидаемо** (один длинный power on, ничего трогать не нужно):
|
||||||
|
1. Bootloader выбирает Slot Б (v2.0.0 > v1.0.0 — активный). `LED_APP` мигает ~2/сек первые ~6 c
|
||||||
|
(видимое подтверждение "образ жив и уже подтверждён" — `CONFIRM_MODE=0` отработал почти сразу).
|
||||||
|
2. `LED_APP` застывает (после ~6 c) — зависание после confirm. Через ~10 c от последнего кормления —
|
||||||
|
WDOG-сброс.
|
||||||
|
3. Bootloader стартует заново. `image_ok` УЖЕ был `SET` до зависания (confirm состоялся) — штатный
|
||||||
|
MCUboot-revert НЕ срабатывает (это не Класс A). Slot Б снова валиден и снова активен (та же
|
||||||
|
версия) → снова выбирается → снова виснет → снова WDOG-сброс. Счётчик (`SRC_GPR3`, переживает
|
||||||
|
тёплые/watchdog-сбросы) растёт: 1, 2, 3.
|
||||||
|
4. На **третьем** сбросе подряд (`reset_count == threshold == 3`) — `recovery_decide()` находит
|
||||||
|
фолбэк (Slot A, валиден) → **Slot Б стирается**, счётчик обнуляется, `boot_go()` выбирает Slot A →
|
||||||
|
`LED_APP` начинает мигать ~1/сек (Slot A, здоров, никогда не виснет) **и остаётся так постоянно**.
|
||||||
|
5. Наблюдать по CDC (подключиться в любой момент до/во время цикла): `{"cmd":"wdog"}` →
|
||||||
|
`reset_count` растёт 1→2→3, затем после фолбэка возвращается к состоянию нормальной работы
|
||||||
|
(счётчик обнулён — следующий запрос покажет `reset_count:0`, если плата уже перезагрузилась в
|
||||||
|
рамках здорового Slot A и что-то там его обнулило явно — иначе останется как есть до следующего
|
||||||
|
события; для стаба, в отличие от tft_app, health-mark/confirm уже случились в Slot Б, Slot A —
|
||||||
|
собственный образ, его confirm/health-mark произойдут в СВОЁМ прогоне при следующем прыжке в него).
|
||||||
|
6. Общее время до фолбэка — ориентировочно 3 × (~6 c мигания + ~10 c до сброса) ≈ **45-50 c**. Не
|
||||||
|
вмешиваться, просто наблюдать.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Сценарий 3 — Класс B, нет фолбэка: откатываться некуда
|
||||||
|
|
||||||
|
**Подготовка:**
|
||||||
|
```bash
|
||||||
|
uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 --sector 0x60040000+0x200000 # Slot A пуст
|
||||||
|
uv run --directory tools/hil pyocd flash --target mimxrt1050_quadspi --frequency 4000000 \
|
||||||
|
--base-address 0x60240000 --erase sector "$(pwd)/build/Debug/signed/stub_b_confirm_hang_class_b.bin"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ожидаемо:** то же, что сценарий 2, шаги 1-3 (Slot Б виснет, WDOG-сбросы, счётчик растёт до 3) — но
|
||||||
|
на третьем сбросе `recovery_decide()` НЕ находит валидного фолбэка (Slot A пуст) →
|
||||||
|
**`RECOVERY_ENTER_RECOVERY_MODE`**:
|
||||||
|
- Прыжок подавлен (Slot Б НЕ стирается — «на диске никогда не бывает нуля рабочих слотов», хотя он
|
||||||
|
и зависал).
|
||||||
|
- `LED_HEARTBEAT`+`LED_APP` — recovery-паттерн (синхронно, 100/100 мс).
|
||||||
|
- CDC: `status: recovery_mode`, повторяется каждые ~1.5 c.
|
||||||
|
- Плата **не циклится дальше** — остаётся в этом состоянии бессрочно, ждёт SD (ослабленный gate)
|
||||||
|
или BTN_2 (см. сценарий 4).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
5:55.694 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
14:35:57.932 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
14:35:59.128 -> `{"type":"cmd","cmd":"wdog"}`
|
||||||
|
|
||||||
|
14:36:00.170 -> {"type":"wdog","armed":true,"timeout_s":10,"recovered":true,"reset_count":3,"threshold":3}
|
||||||
|
14:36:00.170 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
14:36:02.408 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
14:36:04.646 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
|
||||||
|
# Перезапуск питания
|
||||||
|
5:55.694 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
14:35:57.932 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
14:35:59.128 -> `{"type":"cmd","cmd":"wdog"}`
|
||||||
|
|
||||||
|
14:36:00.170 -> {"type":"wdog","armed":true,"timeout_s":10,"recovered":true,"reset_count":3,"threshold":3}
|
||||||
|
14:36:00.170 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
14:36:02.408 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
14:36:04.646 -> {"type":"status","state":"recovery_mode"}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Сценарий 4 — Recovery по BTN_2 (ручной вход)
|
||||||
|
|
||||||
|
**Подготовка:** валидный здоровый образ в любом слоте (например, `stub_a_v1_confirmed.bin` → Slot A,
|
||||||
|
Slot Б пуст) — специально не пустая плата, чтобы подтвердить: BTN_2 подавляет загрузку ДАЖЕ
|
||||||
|
валидного образа.
|
||||||
|
|
||||||
|
> ⚠️ **Recovery всегда ставит в Slot A (см. update_policy.h) — `TFT_APP.BIN` ОБЯЗАН быть слинкован
|
||||||
|
> под адрес Slot A.** Использовать **`stub_a_v1_confirmed.bin`**, переименованный — **НЕ**
|
||||||
|
> `stub_b_v2_confirmed.bin` (тот линкован под Slot Б, `0x60240000`; физически положенный в Slot A он
|
||||||
|
> пройдёт крипто-гейт — подпись валидна для содержимого файла независимо от адреса — но исполняться
|
||||||
|
> будет с чужими абсолютными адресами: тот же класс проблемы, что и в Фазе 3 §5, только теперь для
|
||||||
|
> recovery-пути). Симптом при ошибке: CDC покажет `installing`, затем USB CDC-порт пропадёт (прыжок
|
||||||
|
> в нерабочий код), и через несколько автоматических WDOG-циклов плата САМА уйдёт в `recovery_mode`
|
||||||
|
> — без удержания кнопки, только потому что "успешно установленный" Slot A на самом деле не работает
|
||||||
|
> и продолжает и продолжает выбираться, копя `reset_count`.
|
||||||
|
|
||||||
|
**Ожидаемо:**
|
||||||
|
1. **Удерживая `BSP_BUTTON_2`**, подать питание → recovery-режим немедленно (LED-паттерн, CDC
|
||||||
|
`recovery_mode`), **без попытки прыжка** в валидный Slot A — приоритет BTN_2 подтверждён.
|
||||||
|
2. Не отпуская последствия (в любой момент, кнопку уже можно отпустить — она сэмплируется один раз
|
||||||
|
на старте) вставить SD-карту с `TFT_APP.BIN` = **`stub_a_v1_confirmed.bin`**, переименованный (см.
|
||||||
|
предупреждение выше — не `stub_b_v2_confirmed.bin`).
|
||||||
|
3. В течение ~1.5 c (следующий пере-скан) — CDC: `installing` → оба слота стираются, новый образ
|
||||||
|
ставится в Slot A → **авто-прыжок**, `LED_APP` начинает мигать ~1/сек (паттерн `stub_a`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Сценарий 5 — Транзиент (per-session семантика счётчика)
|
||||||
|
|
||||||
|
**Подготовка:** сразу после сценария 3 (плата в recovery, `reset_count` было 3 на момент входа).
|
||||||
|
|
||||||
|
**Действие:** power cycle БЕЗ удержания BTN_2 (обычное выключение/включение).
|
||||||
|
|
||||||
|
**Ожидаемо:** POR обнуляет счётчик (`bsp_boot_state_init()` детектит `IPP_RESET_B`, чистит
|
||||||
|
`SRC->SRSR`, зовёт `bsp_boot_attempt_reset()`) → Slot Б (всё ещё физически там же, всё ещё виснет —
|
||||||
|
ничего не стёрлось в сценарии 3) **пробуется заново** — bootloader НЕ помнит о прошлой серии сбросов
|
||||||
|
как о чём-то, что сразу требует recovery. Наблюдать: `LED_APP` снова мигает ~2/сек первые ~6 c (Slot
|
||||||
|
Б снова выбран и снова "жив"), затем весь цикл сценария 2/3 повторяется с нуля (счётчик 1, 2, 3...).
|
||||||
|
Это подтверждает **per-session** семантику (Вариант 1 из решений Фазы 6: «забанен» = «счётчик ≥
|
||||||
|
порога», не отдельный флеш-маркер) — если зависание было транзиентным, POR всегда даёт новый шанс.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Сценарий 6 — Регрессия: не мешает норме
|
||||||
|
|
||||||
|
Прогнать чек-лист Фазы 3 (§8 в [HARDWARE_VERIFICATION_PHASE3.md](HARDWARE_VERIFICATION_PHASE3.md))
|
||||||
|
как есть, **и дополнительно**:
|
||||||
|
- Сценарий 1 этого документа (выше) — сам по себе уже прямой регрессионный тест на находку
|
||||||
|
«счётчик рос и на пустой плате».
|
||||||
|
- Оставить чистую плату (оба слота пусты, без SD) в цикле ожидания на 2+ минуты — `reset_count`
|
||||||
|
должен оставаться `0` бессрочно (запросить `{"cmd":"wdog"}` в начале и в конце интервала, сверить).
|
||||||
|
- Прогнать сценарий 1 Фазы 3 (чистая плата + валидный образ на SD, авто-установка) — убедиться, что
|
||||||
|
установка проходит и прыгает штатно, `reset_count` после первого успешного прыжка — `0` или `1`
|
||||||
|
(инкремент на первую же попытку прыжка в свежеустановленный образ — ожидаемо, не баг).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Итоговая таблица
|
||||||
|
|
||||||
|
| № | Сценарий | Ожидаемый результат |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Класс A: никогда не confirm, виснет сразу | WDOG-сброс → штатный MCUboot revert → стирание → обычное ожидание (НЕ recovery) |
|
||||||
|
| 2 | Класс B, есть фолбэк | 3 WDOG-сброса → зависший слот стёрт → фолбэк выбран и стабилен |
|
||||||
|
| 3 | Класс B, нет фолбэка | 3 WDOG-сброса → recovery-режим (LED-паттерн, CDC `recovery_mode`), не циклится дальше |
|
||||||
|
| 4 | Recovery по BTN_2 | Подавленный прыжок → recovery → SD найдена → оба слота стёрты → установка → авто-прыжок |
|
||||||
|
| 5 | Транзиент (per-session) | POR после recovery без BTN_2 → счётчик обнулён → зависший слот пробуется заново |
|
||||||
|
| 6 | Регрессия | Чек-лист Фазы 3 + пустая плата 2+ мин без ложного recovery |
|
||||||
|
|
@ -1,42 +1,160 @@
|
||||||
/**
|
/**
|
||||||
* @file main.c
|
* @file main.c
|
||||||
* @brief Заглушка tft_app для аппаратной верификации Фазы 2 bootutil.
|
* @brief Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) и
|
||||||
|
* recovery-логики (Фаза 6).
|
||||||
*
|
*
|
||||||
* tft_app ещё не реализована — bootloader'у некуда прыгать. Этот образ —
|
* tft_app ещё не реализована — bootloader'у некуда прыгать. Этот образ —
|
||||||
* минимальный, но настоящий imgtool-подписанный XIP-образ с корректным
|
* минимальный, но настоящий imgtool-подписанный XIP-образ с корректным
|
||||||
* vector table по адресу слота: единственная задача — мигать LED_APP с
|
* vector table по адресу слота: базово мигает LED_APP с периодом,
|
||||||
* периодом, зависящим от STUB_BLINK_MS (задаётся компилятору), чтобы по
|
* зависящим от STUB_BLINK_MS (задаётся компилятору), чтобы по частоте
|
||||||
* частоте мигания визуально отличить, какой слот реально выбрал
|
* мигания визуально отличить, какой слот реально выбрал bootloader. См.
|
||||||
* bootloader. См. firmware/bootloader/PLAN.md, "Аппаратная верификация
|
* firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2".
|
||||||
* Фазы 2".
|
|
||||||
*
|
*
|
||||||
* Без USB/CDC — визуальной индикации достаточно, минимальный код.
|
* Фаза 6 добавила три независимые, настраиваемые компилятором оси —
|
||||||
|
* стенд-контракт tft_app (см. PLAN.md, 6c) минимально, но по-настоящему:
|
||||||
|
* - STUB_CONFIRM_MODE — 0 сразу / 1 отложенно (STUB_CONFIRM_DELAY_MS) /
|
||||||
|
* 2 никогда. Подтверждение — boot_set_next(fap, true, true) на
|
||||||
|
* СОБСТВЕННОМ слоте (STUB_OWN_SLOT_ID), НЕ boot_set_confirmed() — та
|
||||||
|
* жёстко пишет в FLASH_AREA_IMAGE_PRIMARY (Slot A) независимо от того,
|
||||||
|
* откуда реально исполняется код: для стаба в Slot Б это подтвердило бы
|
||||||
|
* чужой слот, а не себя.
|
||||||
|
* - STUB_HANG_MODE — 0 никогда / 1 до health-mark (сразу на входе) /
|
||||||
|
* 2 после health-mark, до confirm / 3 после confirm (через
|
||||||
|
* STUB_HANG_DELAY_MS после факта подтверждения — чтобы успеть увидеть
|
||||||
|
* мигание глазами перед тем как оно застынет).
|
||||||
|
* - STUB_FEED_WDOG — 1 (дефолт, как в проде) / 0 — не кормить watchdog,
|
||||||
|
* детерминированно проверить сам механизм сброса.
|
||||||
|
*
|
||||||
|
* Без USB/CDC — визуальной индикации (частота/застывание LED_APP) достаточно
|
||||||
|
* для чек-листа Фазы 6, минимальный код.
|
||||||
*
|
*
|
||||||
* Watchdog: загрузчик взводит аппаратный WDOG перед прыжком сюда, а WDE —
|
* Watchdog: загрузчик взводит аппаратный WDOG перед прыжком сюда, а WDE —
|
||||||
* write-once (выключить нельзя). Поэтому заглушка ОБЯЗАНА его кормить, иначе
|
* write-once (выключить нельзя). Поэтому заглушка, если сконфигурирована
|
||||||
* WDOG сбросит плату через таймаут и получится reset-loop — заглушка тут
|
* его кормить (STUB_FEED_WDOG=1, дефолт), обязана делать это в главном цикле
|
||||||
* играет роль tft_app, которая в проде тоже будет кормить watchdog
|
* — иначе WDOG сбросит плату через таймаут (см. bsp/wdog/README.md).
|
||||||
* (см. bsp/wdog/README.md). Период мигания (250/500 мс) << таймаута (~10 c),
|
|
||||||
* так что refresh на каждой итерации — с огромным запасом.
|
|
||||||
*/
|
*/
|
||||||
#include "board.h"
|
#include "board.h"
|
||||||
|
#include "bootutil/bootutil_public.h"
|
||||||
|
#include "bsp/boot_state.h"
|
||||||
#include "bsp/led.h"
|
#include "bsp/led.h"
|
||||||
#include "bsp/tick.h"
|
#include "bsp/tick.h"
|
||||||
#include "bsp/wdog.h"
|
#include "bsp/wdog.h"
|
||||||
|
#include "flash_map.h"
|
||||||
|
|
||||||
|
#include <stdbool.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)"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef STUB_OWN_SLOT_ID
|
||||||
|
#error "STUB_OWN_SLOT_ID must be defined (0 = Slot A, 1 = Slot Б)"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Confirm mode: 0 = сразу, 1 = отложенно, 2 = никогда. */
|
||||||
|
#ifndef STUB_CONFIRM_MODE
|
||||||
|
#define STUB_CONFIRM_MODE 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef STUB_CONFIRM_DELAY_MS
|
||||||
|
#define STUB_CONFIRM_DELAY_MS 3000U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Hang mode: 0 = никогда, 1 = до health-mark, 2 = после health-mark (до
|
||||||
|
* confirm), 3 = после confirm. */
|
||||||
|
#ifndef STUB_HANG_MODE
|
||||||
|
#define STUB_HANG_MODE 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef STUB_HANG_DELAY_MS
|
||||||
|
#define STUB_HANG_DELAY_MS 3000U
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef STUB_FEED_WDOG
|
||||||
|
#define STUB_FEED_WDOG 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Подтвердить СОБСТВЕННЫЙ слот (STUB_OWN_SLOT_ID).
|
||||||
|
*
|
||||||
|
* boot_set_next(fap, active=true, confirm=true) — не boot_set_confirmed():
|
||||||
|
* та жёстко работает с FLASH_AREA_IMAGE_PRIMARY (Slot A) вне зависимости от
|
||||||
|
* того, какой слот реально исполняется; для Direct-XIP с двумя равноправными
|
||||||
|
* слотами это подтвердило бы не тот слот при исполнении из Slot Б.
|
||||||
|
*/
|
||||||
|
static void confirm_self(void)
|
||||||
|
{
|
||||||
|
const struct flash_area *p_fap;
|
||||||
|
if (flash_area_open((uint8_t) STUB_OWN_SLOT_ID, &p_fap) == 0)
|
||||||
|
{
|
||||||
|
(void) boot_set_next(p_fap, true, true);
|
||||||
|
flash_area_close(p_fap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
board_hw_init();
|
board_hw_init();
|
||||||
bsp_led_init();
|
bsp_led_init();
|
||||||
bsp_tick_init();
|
bsp_tick_init();
|
||||||
|
|
||||||
|
#if STUB_HANG_MODE == 1
|
||||||
|
for (;;) { } /* до health-mark — Класс A: свежий образ виснет сразу */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* health-mark == bsp_boot_attempt_reset() (см. bsp/boot_state.h) — вызывать
|
||||||
|
* его безусловно перед потенциальным зависанием НЕЛЬЗЯ: он обнулял бы
|
||||||
|
* счётчик попыток на КАЖДОМ цикле ДО того, как зависание успевает
|
||||||
|
* засчитаться, и recovery-фолбэк/recovery-режим не сработали бы никогда
|
||||||
|
* (найдено на железе — HANG_MODE=3 резетился бесконечно вместо остановки
|
||||||
|
* на пороге). Это тот же класс "честной границы", что уже описан в
|
||||||
|
* PLAN.md (образ обнуляет счётчик, ПОТОМ виснет — таймером не отличить
|
||||||
|
* "здоров" от "здоров, но детерминированно виснет"): здесь HANG_MODE
|
||||||
|
* снимает эту неоднозначность на этапе компиляции — если конфигурация
|
||||||
|
* гарантированно виснет (2 — после health-mark, до confirm; 3 — после
|
||||||
|
* confirm), health-mark не зовём вообще, счётчик копится корректно.
|
||||||
|
* Здоровый стаб (HANG_MODE 0) и "виснет до health-mark" (1, сюда и не
|
||||||
|
* доходит) — не затронуты. */
|
||||||
|
#if (STUB_HANG_MODE != 2) && (STUB_HANG_MODE != 3)
|
||||||
|
bsp_boot_health_mark(); /* "дошёл до устойчивого состояния" (Фаза 6, 6c) */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if STUB_HANG_MODE == 2
|
||||||
|
for (;;) { } /* после (несостоявшегося) health-mark, до confirm */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool confirmed = false;
|
||||||
|
|
||||||
|
#if STUB_CONFIRM_MODE == 0
|
||||||
|
confirm_self();
|
||||||
|
confirmed = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uint32_t start_ms = bsp_tick_get_ms();
|
||||||
|
(void) start_ms; /* не используется, если ни один из режимов ниже её не читает */
|
||||||
|
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
|
#if STUB_FEED_WDOG
|
||||||
bsp_wdog_refresh(); /* обслуживаем унаследованный от загрузчика WDOG */
|
bsp_wdog_refresh(); /* обслуживаем унаследованный от загрузчика WDOG */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if STUB_CONFIRM_MODE == 1
|
||||||
|
if (!confirmed && ((bsp_tick_get_ms() - start_ms) >= STUB_CONFIRM_DELAY_MS))
|
||||||
|
{
|
||||||
|
confirm_self();
|
||||||
|
confirmed = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if STUB_HANG_MODE == 3
|
||||||
|
if (confirmed &&
|
||||||
|
((bsp_tick_get_ms() - start_ms) >= (STUB_CONFIRM_DELAY_MS + STUB_HANG_DELAY_MS)))
|
||||||
|
{
|
||||||
|
for (;;) { } /* после confirm — Класс B: подтверждённый образ виснет в рантайме */
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
bsp_led_toggle(LED_APP);
|
bsp_led_toggle(LED_APP);
|
||||||
bsp_delay(STUB_BLINK_MS);
|
bsp_delay(STUB_BLINK_MS);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -238,8 +238,9 @@ hab-verify project="firmware_test" type="release":
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ГРУППА: mcuboot-stub — заглушка tft_app для аппаратной верификации bootutil
|
# ГРУППА: mcuboot-stub — заглушка tft_app для аппаратной верификации bootutil
|
||||||
# (Фаза 2, firmware/bootloader/PLAN.md). Временное — удалить вместе с
|
# (Фаза 2) и recovery-логики (Фаза 6, firmware/bootloader/PLAN.md). Временное —
|
||||||
# firmware/bootloader/test_stub/, когда появится реальный firmware/tft_app.
|
# удалить вместе с firmware/bootloader/test_stub/, когда появится реальный
|
||||||
|
# firmware/tft_app.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
MCUBOOT_ROOT := justfile_directory() / 'sdk/middleware/mcuboot_opensource'
|
MCUBOOT_ROOT := justfile_directory() / 'sdk/middleware/mcuboot_opensource'
|
||||||
|
|
@ -249,7 +250,7 @@ MCUBOOT_IMGTOOL := MCUBOOT_ROOT / 'scripts/imgtool.py'
|
||||||
# не трогая основной venv (см. firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2").
|
# не трогая основной venv (см. firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2").
|
||||||
MCUBOOT_UV := 'uv run --with cryptography --with intelhex --with click --with cbor2 --with pyyaml python3'
|
MCUBOOT_UV := 'uv run --with cryptography --with intelhex --with click --with cbor2 --with pyyaml python3'
|
||||||
|
|
||||||
[doc('Собрать и подписать заглушки Slot A/Б для аппаратной верификации bootutil (Debug)')]
|
[doc('Собрать и подписать заглушки Slot A/Б для аппаратной верификации bootutil и recovery (Debug)')]
|
||||||
[group('mcuboot-stub')]
|
[group('mcuboot-stub')]
|
||||||
build-mcuboot-stub: _configure-debug
|
build-mcuboot-stub: _configure-debug
|
||||||
cmake --build --preset mcuboot-stub-debug
|
cmake --build --preset mcuboot-stub-debug
|
||||||
|
|
@ -263,7 +264,25 @@ build-mcuboot-stub: _configure-debug
|
||||||
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 1.0.0 --align 1 --pad-header --pad \
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 1.0.0 --align 1 --pad-header --pad \
|
||||||
"{{ BUILD_DIR }}/Debug/test_slot_stub_a.bin" "{{ BUILD_DIR }}/Debug/signed/stub_a_v1_unconfirmed.bin"
|
"{{ BUILD_DIR }}/Debug/test_slot_stub_a.bin" "{{ BUILD_DIR }}/Debug/signed/stub_a_v1_unconfirmed.bin"
|
||||||
@echo " ✅ build/Debug/signed/stub_{a,b}_*.bin готовы — прошивка через just host::flash-swd (см. PLAN.md)"
|
# Фаза 6, Класс A — БЕЗ --confirm: проверяем именно штатный MCUboot revert
|
||||||
|
# (copy_done без image_ok → стирание на втором boot_go()), стаб сам тоже
|
||||||
|
# никогда не подтверждается (CONFIRM_MODE=2).
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 1.0.0 --align 1 --pad-header --pad \
|
||||||
|
"{{ BUILD_DIR }}/Debug/test_slot_stub_a_hang.bin" "{{ BUILD_DIR }}/Debug/signed/stub_a_hang_class_a.bin"
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 2.0.0 --align 1 --pad-header --pad \
|
||||||
|
"{{ BUILD_DIR }}/Debug/test_slot_stub_b_hang.bin" "{{ BUILD_DIR }}/Debug/signed/stub_b_hang_class_a.bin"
|
||||||
|
# Фаза 6, Класс B — С --confirm: образ уже "здоров" с момента прошивки
|
||||||
|
# (стаб и сам подтвердит себя рантаймом почти сразу, CONFIRM_MODE=0, но
|
||||||
|
# подпись с --confirm исключает даже стартовое окно до этого момента).
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 1.0.0 --align 1 --pad-header --pad --confirm \
|
||||||
|
"{{ BUILD_DIR }}/Debug/test_slot_stub_a_confirm_hang.bin" "{{ BUILD_DIR }}/Debug/signed/stub_a_confirm_hang_class_b.bin"
|
||||||
|
{{ MCUBOOT_UV }} "{{ MCUBOOT_IMGTOOL }}" sign \
|
||||||
|
-k "{{ MCUBOOT_KEY }}" -H 0x200 -S 0x200000 -v 2.0.0 --align 1 --pad-header --pad --confirm \
|
||||||
|
"{{ BUILD_DIR }}/Debug/test_slot_stub_b_confirm_hang.bin" "{{ BUILD_DIR }}/Debug/signed/stub_b_confirm_hang_class_b.bin"
|
||||||
|
@echo " ✅ build/Debug/signed/stub_*.bin готовы — прошивка через just host::flash-swd (см. PLAN.md)"
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ГРУППА: quality
|
# ГРУППА: quality
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ void test_no_valid_slots_installs_to_slot_a(void)
|
||||||
struct image_version candidate = make_version(1, 0, 0, 0);
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
update_policy_result_t result =
|
update_policy_result_t result =
|
||||||
update_policy_decide(&K_INVALID_SLOT, &K_INVALID_SLOT, &candidate, false);
|
update_policy_decide(&K_INVALID_SLOT, &K_INVALID_SLOT, &candidate, false, false);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
|
@ -95,7 +95,7 @@ void test_only_slot_b_valid_targets_slot_a(void)
|
||||||
struct image_version candidate = make_version(2, 0, 0, 0);
|
struct image_version candidate = make_version(2, 0, 0, 0);
|
||||||
|
|
||||||
update_policy_result_t result =
|
update_policy_result_t result =
|
||||||
update_policy_decide(&K_INVALID_SLOT, &slot_b, &candidate, false);
|
update_policy_decide(&K_INVALID_SLOT, &slot_b, &candidate, false, false);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
|
@ -109,7 +109,7 @@ void test_candidate_newer_installs_to_inactive_slot(void)
|
||||||
struct image_version candidate = make_version(2, 0, 0, 0);
|
struct image_version candidate = make_version(2, 0, 0, 0);
|
||||||
|
|
||||||
update_policy_result_t result =
|
update_policy_result_t result =
|
||||||
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, false);
|
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, false, false);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_B, result.target_slot);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_B, result.target_slot);
|
||||||
|
|
@ -126,7 +126,7 @@ void test_candidate_older_without_button_skips(void)
|
||||||
struct image_version candidate = make_version(1, 0, 0, 0);
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
update_policy_result_t result =
|
update_policy_result_t result =
|
||||||
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, false);
|
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, false, false);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SKIP, result.action);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SKIP, result.action);
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +137,7 @@ void test_candidate_older_with_button_forces_install(void)
|
||||||
struct image_version candidate = make_version(1, 0, 0, 0);
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
update_policy_result_t result =
|
update_policy_result_t result =
|
||||||
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, true);
|
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, true, false);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_B, result.target_slot);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_B, result.target_slot);
|
||||||
|
|
@ -153,7 +153,7 @@ void test_candidate_equal_with_button_still_skips(void)
|
||||||
struct image_version candidate = make_version(1, 0, 0, 0);
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
update_policy_result_t result =
|
update_policy_result_t result =
|
||||||
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, true);
|
update_policy_decide(&slot_a, &K_INVALID_SLOT, &candidate, true, false);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SKIP, result.action);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SKIP, result.action);
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +167,7 @@ void test_active_slot_is_the_higher_version_when_both_valid(void)
|
||||||
struct image_version candidate = make_version(3, 0, 0, 0);
|
struct image_version candidate = make_version(3, 0, 0, 0);
|
||||||
|
|
||||||
/* Активный — Б (выше версия), значит целевой слот установки — А. */
|
/* Активный — Б (выше версия), значит целевой слот установки — А. */
|
||||||
update_policy_result_t result = update_policy_decide(&slot_a, &slot_b, &candidate, false);
|
update_policy_result_t result = update_policy_decide(&slot_a, &slot_b, &candidate, false, false);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
|
@ -191,7 +191,56 @@ void test_forced_downgrade_targets_and_erases_the_higher_version_slot(void)
|
||||||
/* Активный — Б (v2, выше версия). Кандидат v1 старше активного, кнопка
|
/* Активный — Б (v2, выше версия). Кандидат v1 старше активного, кнопка
|
||||||
* удержана → форс. установка в Slot A (неактивный) + Slot Б обязан быть
|
* удержана → форс. установка в Slot A (неактивный) + Slot Б обязан быть
|
||||||
* стёрт, иначе Slot Б (всё ещё валидный v2) снова выиграет. */
|
* стёрт, иначе Slot Б (всё ещё валидный v2) снова выиграет. */
|
||||||
update_policy_result_t result = update_policy_decide(&slot_a, &slot_b, &candidate, true);
|
update_policy_result_t result = update_policy_decide(&slot_a, &slot_b, &candidate, true, false);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
TEST_ASSERT_TRUE(result.erase_previous_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── update_policy_decide — recovery_mode (Фаза 6b, ослабленный gate) ────
|
||||||
|
*
|
||||||
|
* recovery_mode == true игнорирует версию/кнопку целиком и всегда целится в
|
||||||
|
* Slot A с erase_previous_active == true (Slot Б стирается вызывающим
|
||||||
|
* кодом) — независимо от того, какой слот "активен" по обычным правилам.
|
||||||
|
*/
|
||||||
|
|
||||||
|
void test_recovery_mode_installs_to_slot_a_ignoring_candidate_version(void)
|
||||||
|
{
|
||||||
|
update_policy_slot_state_t slot_a = make_valid_slot(make_version(5, 0, 0, 0));
|
||||||
|
update_policy_slot_state_t slot_b = make_valid_slot(make_version(9, 0, 0, 0));
|
||||||
|
/* Кандидат СТАРШЕ обоих слотов — в обычном режиме это был бы SKIP. */
|
||||||
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result = update_policy_decide(&slot_a, &slot_b, &candidate, false, true);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
TEST_ASSERT_TRUE(result.erase_previous_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_recovery_mode_targets_slot_a_even_when_slot_a_is_the_active_one(void)
|
||||||
|
{
|
||||||
|
/* Slot A — активный (выше версия). Обычный режим целил бы в Slot Б —
|
||||||
|
* recovery всё равно должен ставить в Slot A (см. rationale в
|
||||||
|
* update_policy.h: "прежде чем что-то заменить, нужно на что менять"). */
|
||||||
|
update_policy_slot_state_t slot_a = make_valid_slot(make_version(9, 0, 0, 0));
|
||||||
|
update_policy_slot_state_t slot_b = make_valid_slot(make_version(1, 0, 0, 0));
|
||||||
|
struct image_version candidate = make_version(2, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result = update_policy_decide(&slot_a, &slot_b, &candidate, false, true);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
TEST_ASSERT_TRUE(result.erase_previous_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_recovery_mode_installs_even_with_no_valid_slots(void)
|
||||||
|
{
|
||||||
|
struct image_version candidate = make_version(1, 0, 0, 0);
|
||||||
|
|
||||||
|
update_policy_result_t result =
|
||||||
|
update_policy_decide(&K_INVALID_SLOT, &K_INVALID_SLOT, &candidate, false, true);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_INSTALL, result.action);
|
||||||
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
TEST_ASSERT_EQUAL(UPDATE_POLICY_SLOT_A, result.target_slot);
|
||||||
|
|
@ -218,5 +267,9 @@ int main(void)
|
||||||
RUN_TEST(test_active_slot_is_the_higher_version_when_both_valid);
|
RUN_TEST(test_active_slot_is_the_higher_version_when_both_valid);
|
||||||
RUN_TEST(test_forced_downgrade_targets_and_erases_the_higher_version_slot);
|
RUN_TEST(test_forced_downgrade_targets_and_erases_the_higher_version_slot);
|
||||||
|
|
||||||
|
RUN_TEST(test_recovery_mode_installs_to_slot_a_ignoring_candidate_version);
|
||||||
|
RUN_TEST(test_recovery_mode_targets_slot_a_even_when_slot_a_is_the_active_one);
|
||||||
|
RUN_TEST(test_recovery_mode_installs_even_with_no_valid_slots);
|
||||||
|
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1126,7 +1126,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "service-tui"
|
name = "service-tui"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pyinstaller" },
|
{ name = "pyinstaller" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue