From 6c564f16388ba5a67750362062ad7f696b63b411 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Mon, 20 Jul 2026 11:40:36 +0300 Subject: [PATCH] # bootloader: Phase 4 - done --- firmware/bootloader/PLAN.md | 97 +++++++++++- firmware/bootloader/README.md | 50 ++++-- .../mcuboot_port/flash_map_backend.c | 11 +- firmware/bootloader/src/led_status.h | 17 +- firmware/bootloader/test_stub/CMakeLists.txt | 9 +- .../HARDWARE_VERIFICATION_LED_PATTERNS.md | 149 ++++++++++++++++++ 6 files changed, 309 insertions(+), 24 deletions(-) create mode 100644 firmware/bootloader/test_stub/HARDWARE_VERIFICATION_LED_PATTERNS.md diff --git a/firmware/bootloader/PLAN.md b/firmware/bootloader/PLAN.md index 397dcee..82cf90f 100644 --- a/firmware/bootloader/PLAN.md +++ b/firmware/bootloader/PLAN.md @@ -8,7 +8,7 @@ | 1 — Скелет (CDC + LED) | ✅ завершена | сборка/HAB/SWD-прошивка/ping-pong/debug — все пункты верификации пройдены на реальной плате, детали ниже | | 2 — bootutil (Direct-XIP) | ✅ завершена | host-тесты 5/5, аппаратная верификация — все 5 сценариев пройдены на реальной плате (детали и 3 найденных/исправленных бага — [DEBUG_LOG_PHASE2.md](DEBUG_LOG_PHASE2.md)) | | 3 — SD-путь установки | ✅ полностью верифицирована (2026-07-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-паттерны | ✅ полностью верифицирована (2026-07-20) | `bsp_sdram_configure()` (C-порт DCD) + `qspi_info` (JEDEC → чип/ёмкость) + словарь LED-паттернов. Host-тесты 16/16 (без изменений — вся новая логика аппаратная, host-тестировать нечего). Аппаратная верификация: SDRAM 4-фазный dev-тест (`sdram_test`) все фазы green; сценарии LED 1-5 из чек-листа пройдены (6 сознательно не гонялся — реальная проверка smoke-тестов идёт через service-tui сразу после прошивки, не через синтетический чек-лист). По пути найдено/исправлено 3 бага — детали ниже. Чек-лист — [test_stub/HARDWARE_VERIFICATION_LED_PATTERNS.md](test_stub/HARDWARE_VERIFICATION_LED_PATTERNS.md), словарь для сервисных инженеров — [docs/bootloader/LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md) | | 5 — HAB Release + service-tui | не начата | | | 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) | @@ -699,6 +699,101 @@ while (true)` без выхода по времени, если callback `cardDe Полноценной аппаратной инъекции неисправностей не делаем (в проекте и для остальных HIL-тестов это не принято) — ограничиваемся проверкой на реальной плате в штатном состоянии. +### ✅ Реализовано (2026-07-20): SDRAM/QSPI smoke-test + словарь LED-паттернов + +**`bsp_sdram_configure()` — C-порт DCD.** DCD (`tools/host/dcd/dcd.bin`) — источник истины, 4 года в +производстве. Порт побитовый, не пересчитанный из формул SDK: значения регистров SEMC — дословно из +DCD, тактовая цепочка — через типизированный CCM API (лосслесс перенос значений), init-последовательность +SDRAM — через `SEMC_SendIPCommand()` (командные слова DCD `0xA55A000F/000C/000A` decode ровно в +`kSEMC_SDRAMCM_Prechargeall/AutoRefresh/Modeset`, подтверждено). Первый SEMC-блок DCD — мёртвый код +(полностью перезаписывается вторым до какого-либо использования) — не портирован. Секция «Clock Init» +DCD — наоборот НЕ дублирование: `SKIP_SYSCLK_INIT` (`bsp/CMakeLists.txt`) гасит PLL2+PFD2+SEMC-mux в +штатном `BOARD_BootClockRUN()` для всех таргетов, включая bootloader — без DCD и без своего кода эту +цепочку не поднимает никто; `bsp_sdram_configure()` обязана делать это сама (SEMC ≈135.77 МГц, PLL2 +528 → PFD2 `FRAC=35` ≈271.54 → `SEMC_PODF` ÷2). Детали и вся математика — [bsp/sdram/README.md](../../bsp/sdram/README.md). + +**Найденный и исправленный баг: AXI-QoS регистры валили C-код фолтом (не DCD).** Хвост DCD пишет +NIC-301 GPV-регистры приоритета шины (`0x41044100+`, `0x41442100+`). Первая гипотеза («такой периферии +на этом кристалле нет») оказалась неверной — пользователь предоставил выжимку из RM (гл. 29 "Network +Interconnect Bus System"), адреса совпали день-в-день. Настоящая причина — +[bsp/generated/board.c](../../bsp/generated/board.c) `board_mpu_init()`: Region 0 (deny-all, errata +1013783-B воркэраунд) перехватывает весь диапазон 0x0..0xFFFFFFFF, а Region 10 (периферия) покрывает +только 4 МБ от `0x40000000` — GPV-регистры на `0x41xxxxxx` мимо. DCD это переживает (пишет до включения +MPU), C-код — нет. Исправлено: Region 11 (`0x41000000`, 8 МБ, Device/AP_FULL) добавлен в +`board_mpu_init()` — общий для всех прошивок код, задет один раз, не только для bootloader. + +**Dev-only глубокий тест (`{"type":"cmd","cmd":"sdram_test"}`, только Debug, +`BOOTLOADER_DEV_DIAGNOSTICS`).** [dev_sdram_test.c](dev_sdram_test.c) — 4 фазы 1:1 портированы из +`firmware_test/src/tests/test_sdram.c` (address_bus/data_bus/sequential/retention), но против нового +C-порта, не против DCD напрямую. **Все фазы зелёные на железе** — реальный прогон ~4.2 с (комментарий +в оригинале про «~30 с» оказался завышен раза в 6 — запись садится в D-Cache почти мгновенно, реальная +задержка SDRAM только на `flush_dcache()` и чтение при верификации; поправлено в коде). + +**`qspi_info` — идентификация чипа QSPI.** `bsp_qspi_decode_chip()` — новая функция в +[bsp/qspi_flash](../../bsp/qspi_flash/README.md) (capacity byte JEDEC → `"W25Q64/128/256/512"` + МБ, +работает и без успешного `bsp_qspi_init()` — LUT для JEDEC грузится безусловным первым шагом внутри +неё). Ёмкость сверяется с минимумом `BOOTLOADER_FLASH_MAP.md` (16 МБ, W25Q128) — не только +идентификация, а реальный BOM/QC-гейт: W25Q64 драйвер поддерживает технически, но для карты флеша этой +платы это неверная деталь, а не "чуть меньше запас". + +**Smoke-test в `main.c`** (после `bsp_qspi_init()`/`bsp_usb_cdc_init()`, до `attempt_boot()`, +неблокирующий): `bsp_sdram_configure()`+`bsp_sdram_init()` и чтение JEDEC. **Найдена и исправлена +проблема видимости**: результаты (`smoke_pass`/`smoke_fail`, `qspi_info`) изначально шлются один раз +сразу после соответствующего `init()` — хост почти никогда не успевает открыть порт к этому моменту +(USB enumeration), `cli_send()` неблокирующий и теряет запись. Решение — тот же паттерн, что уже был у +`wdog`: `protocol_set_smoke_result()`/`protocol_set_qspi_info()` кэширует исход, +`protocol_send_smoke_status()`/`protocol_send_qspi_info()` переспрашивает в любой момент сессии по +командам `"smoke_status"`/`"qspi_info"`. + +**Словарь LED-паттернов.** Новый модуль [led_status.{c,h}](src/led_status.c) — единственный владелец +всех паттернов. Состояния: норма (ждём SD, APP выкл) / установка (APP соло 250/250) / железо не в +порядке (объединяет `!qspi_ok`, недостаточную ёмкость QSPI и провал SDRAM smoke-test в один паттерн +APP соло 100/100 — деталь причины только по CDC) / recovery (оба LED синхронно 100/100, без изменений +с Фазы 6) / разовая вспышка 4×(80/80) на отклонённый SD-образ. Полное описание для сервисных +инженеров, без ссылок и деталей реализации — [docs/bootloader/LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md). + +Паттерн «установка» рисуется изнутри самой (блокирующей) установки — окно `led_status_install_begin/end()` +в `sd_update.c`, `led_status_tick_install()` (no-op вне окна) вызывается и из цикла копирования чанков, +и из поблочного стирания слота в `flash_map_backend.c` (общий файл — понадобилась правка +`test_stub/CMakeLists.txt`, добавить include-path и сам `led_status.c` в `add_mcuboot_stub()`, иначе +`flash_map_backend.c` там не собирался). + +**Найденное и задокументированное (не баг, ограничение архитектуры XIP-safety): APP подвисает во время +стирания блока.** Аппаратно проверено — во время ~150 мс стирания каждого 64 КБ блока +(`bsp_qspi_erase_block_64k()`) паттерн «установка» не гладкий 250/250, а дёрганый/подвисающий. +Причина: `qspi_irq_lock()` держит `__disable_irq()` на всю длительность IP-команды (обязательно — +иначе прерывание может зафетчить код из flash прямо во время стирания → HardFault, см. +[bsp/qspi_flash/README.md](../../bsp/qspi_flash/README.md), «XIP-безопасность»), а под этим глушится и +SysTick, на котором целиком держится `bsp_tick_get_ms()` (`bsp/tick/src/tick.c`) — часы паттерна на +это время фактически стоят. Во время копирования (страницы по ~3 мс) то же самое явление на порядок +короче — там мигание заметно ровнее. Осознанно не чинится — маскировать IRQ на время IP-команды нужно +по конструкции. Задокументировано в `@note` у `led_status_tick_install()` и в чек-листе. + +**Найденное и подтверждённое штатным (не баг): повтор отклонения при вставленной плохой SD-карте.** +Раз файл на карте не меняется, каждый периодический пере-скан (`SD_RETRY_PERIOD_MS`, Фаза 3) заново +получает `SD_CANDIDATE_INVALID` — LED-вспышка и CDC-ошибка повторяются каждые ~1.5 с, пока карта не +вынута. Это взаимодействие с уже существующей с Фазы 3 логикой периодического пере-скана, не регрессия +этой фазы — оставлено как есть. + +**Аппаратная верификация** — +[test_stub/HARDWARE_VERIFICATION_LED_PATTERNS.md](test_stub/HARDWARE_VERIFICATION_LED_PATTERNS.md): +сценарии 1-5 (норма / битый файл / неверная подпись / успешная установка / recovery) пройдены на +плате. Сценарий 6 (искусственная неисправность через временный порог в коде) сознательно не прогонялся +— реальная проверка smoke-тестов на производстве идёт через service-tui сразу после прошивки +загрузчика, не через синтетический чек-лист. + +**Host-тесты**: `just build::test-host` — по-прежнему **16/16** (новых нет: вся логика этой фазы — +тонкая аппаратная оркестрация без доступа к железу не протестируешь, толстой host-тестируемой логики +здесь не появилось — соответствует принципу проекта, см. низ файла). + +**ARM-сборка**: `just build::build-bootloader-debug`/`hab-bootloader-debug` и +`build-bootloader-release`/`hab-bootloader-release` — все зелёные. `m_text`: Debug 96512 Б из 247 КБ +(38.16%, было 90576 Б / 35.69% после Фазы 6), Release 47232 Б (18.67%) — запас по-прежнему большой. + +**Не сделано, сознательно отложено**: событие `"booting"` на CDC (упоминалось в исходном плане) — не +реализовано; ценность низкая (переход к прыжку — микросекунды, отдельным CDC-событием почти нечего +добавить к уже наблюдаемому факту прыжка), добавим по факту реальной необходимости, не заранее. + --- ## Фаза 5 — HAB, релизные пресеты, интеграция в service-tui diff --git a/firmware/bootloader/README.md b/firmware/bootloader/README.md index 8dea510..47c85d6 100644 --- a/firmware/bootloader/README.md +++ b/firmware/bootloader/README.md @@ -61,8 +61,10 @@ watchdog приостановлен, пошаговая отладка сбро ## Архитектура -Загрузчик работает **без SDRAM** (SEMC поднимает само приложение в своём раннем startup) и без -дисплея/RTOS: инициализация, доступ к QSPI-flash, чтение FatFS с SD, проверка и выбор образа, прыжок. +Загрузчик **не зависит от SDRAM** для своей работы (XIP только из W25Q, без DCD) и без дисплея/RTOS: +инициализация, доступ к QSPI-flash, чтение FatFS с SD, проверка и выбор образа, прыжок. SEMC/SDRAM +трогаются только диагностически (`bsp_sdram_configure()`, boot-time smoke-test) — реально их поднимает +для себя уже само приложение в своём раннем startup. Линкер жёстко ограничивает код бюджетом области загрузчика (256 КБ) с `ASSERT` на границу Slot A — превышение становится ошибкой сборки, а не тихим заездом в чужую область. @@ -80,6 +82,8 @@ firmware/bootloader/ │ │ установить в целевой слот с потоковой verify-записью │ ├── cli.* — построчный IO + диспетчеризация команд │ ├── protocol.* — сериализация исходящих событий +│ ├── led_status.* — словарь LED-паттернов (см. ../../docs/bootloader/LED_PATTERNS.md) +│ ├── dev_sdram_test.* — [DEV-ONLY, Debug] глубокий тест SDRAM по команде "sdram_test" │ └── version.h.in — шаблон версии (CMake → generated/version.h) │ ├── mcuboot_port/ — интеграция bootutil (MCUboot) поверх bsp_qspi_flash: @@ -96,30 +100,44 @@ firmware/bootloader/ ## Протокол -USB CDC ACM, JSON-строки, максимум 128 байт на строку. +USB CDC ACM, JSON-строки. Входящая строка — до `CLI_LINE_BUF_SIZE` (128) байт, исходящее сообщение — +до `PROTO_BUF_SIZE` (192) байт (несимметрично: `qspi_info`/`sdram_test` длиннее старых сообщений). **Команды хоста:** -| Команда | Ответ | -| ------------------------------------ | ------------------------------------------------------------------------------------- | -| `{"type":"cmd","cmd":"ping"}` | `{"type":"pong"}` | -| `{"type":"cmd","cmd":"get_version"}` | `{"type":"version_response","fw":"X.Y.Z"}` | -| `{"type":"cmd","cmd":"wdog"}` | `{"type":"wdog","armed":…,"timeout_s":…,"recovered":…,"reset_count":…,"threshold":…}` | +| Команда | Ответ | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `{"type":"cmd","cmd":"ping"}` | `{"type":"pong"}` | +| `{"type":"cmd","cmd":"get_version"}` | `{"type":"version_response","fw":"X.Y.Z"}` | +| `{"type":"cmd","cmd":"wdog"}` | `{"type":"wdog","armed":…,"timeout_s":…,"recovered":…,"reset_count":…,"threshold":…}` | +| `{"type":"cmd","cmd":"smoke_status"}` | `{"type":"status","state":"smoke_pass"}` / `"smoke_fail"` — результат boot-time smoke-теста SDRAM/SEMC (переспрос, см. ниже) | +| `{"type":"cmd","cmd":"qspi_info"}` | `{"type":"qspi_info","chip":"W25Q128","mfr":"0xEF","cap_byte":"0x18","size_mb":16,"pass":true}` (переспрос, см. ниже) | +| `{"type":"cmd","cmd":"sdram_test"}`
**[DEV-ONLY, Debug-сборка]** | серия из 6 `{"type":"sdram_test","phase":"…","pass":…,"duration_ms":…,"fail_addr":"…","expected":"…","got":"…"}` (`configure`/`address_bus`/`data_bus`/`sequential`/`retention`/`summary`) — блокирует главный цикл на ~4 с. Нет в Release/HAB (`BOOTLOADER_DEV_DIAGNOSTICS`) | + +`smoke_status`/`qspi_info` ничего не отвечают, если соответствующий boot-time чек ещё не отработал — +в штатной последовательности `main.c` такого не бывает. **Исходящие статусы** `{"type":"status","state":"…"}`: -| Состояние | Когда | -| ---------------- | ------------------------------- | -| `waiting_for_sd` | нет валидного слота, ждём карту | -| `installing` | идёт запись образа в слот | -| `update_skipped` | кандидат отклонён по версии | -| `recovery_mode` | плата в режиме восстановления | +| Состояние | Когда | +| ---------------- | ------------------------------------------ | +| `waiting_for_sd` | нет валидного слота, ждём карту | +| `installing` | идёт запись образа в слот | +| `update_skipped` | кандидат отклонён по версии | +| `recovery_mode` | плата в режиме восстановления | +| `smoke_pass` | boot-time smoke-тест SDRAM/SEMC прошёл | +| `smoke_fail` | boot-time smoke-тест SDRAM/SEMC провалился | **Ошибки** `{"ok":false,"error":"…"}`: `SD_CANDIDATE_INVALID`, `SD_INSTALL_WRITE_FAILED`, `SD_INSTALL_REJECTED`, `SD_DOWNGRADE_ERASE_FAILED`, `PARSE_ERR`, `UNKNOWN_CMD`, `LINE_TOO_LONG`. -Событие `wdog` эмитится также автоматически один раз на старте, если предыдущий сброс был по -watchdog (`recovered:true`). +**Автоматически на старте, без команды:** `wdog` — если предыдущий сброс был по watchdog +(`recovered:true`); `qspi_info` и `smoke_pass`/`smoke_fail` — сразу после соответствующей проверки. +Все три — best-effort: хост почти никогда не успевает открыть порт к этому моменту (USB enumeration), +поэтому у `qspi_info`/`smoke_status` (но не у одноразового boot-time `wdog`) есть команда-переспрос +в таблице выше. + +LED-индикация, соответствующая этим состояниям, — [LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md). --- diff --git a/firmware/bootloader/mcuboot_port/flash_map_backend.c b/firmware/bootloader/mcuboot_port/flash_map_backend.c index ea51f1d..6f92b53 100644 --- a/firmware/bootloader/mcuboot_port/flash_map_backend.c +++ b/firmware/bootloader/mcuboot_port/flash_map_backend.c @@ -153,10 +153,13 @@ int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len) * Зависание самого стирания флеша всё равно ловится: refresh — по * ЗАВЕРШЕНИИ блока, а не перед ним. */ bsp_wdog_refresh(); - /* Прогресс-хук индикации — no-op вне окна установки (см. - * led_status.h). Нужен, чтобы APP не замирал на ~5 c стирания слота - * при установке; на revert/recovery-стирании (тоже зовут эту - * функцию) ничего не рисует. */ + /* Прогресс-хук индикации — no-op вне окна установки, ничего не + * рисует на revert/recovery-стирании (та же функция). Вызывается + * исправно каждый блок, но САМ блок (~150 мс) идёт под + * qspi_irq_lock() — глушит SysTick, от которого тикает + * bsp_tick_get_ms(); на глаз это видно как подвисание/дёрганое + * мигание, не гладкие 250/250 (известное ограничение, см. @note + * у led_status_tick_install() в led_status.h). */ led_status_tick_install(); if (bsp_qspi_erase_block_64k(block_addr) != BSP_OK) { diff --git a/firmware/bootloader/src/led_status.h b/firmware/bootloader/src/led_status.h index 3ec4d84..8785cea 100644 --- a/firmware/bootloader/src/led_status.h +++ b/firmware/bootloader/src/led_status.h @@ -47,8 +47,21 @@ void led_status_install_end(void); * APP 250/250). No-op вне окна install. * * Звать из ВСЕХ блокирующих циклов установки — и поблочного стирания слота - * (flash_area_erase, ~5 c на 2 МБ), и копирования чанков (sd_update), иначе - * APP замирал бы на время стирания. + * (flash_area_erase, ~5 c на 2 МБ), и копирования чанков (sd_update). + * + * @note [ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ, подтверждено на железе] Во время самого + * стирания блока (~150 мс на 64 КБ, bsp_qspi_erase_block_64k()) + * qspi_irq_lock() держит __disable_irq() на всю длительность busy-wait + * — это глушит и SysTick, на котором держится bsp_tick_get_ms() + * (см. bsp/tick/src/tick.c). Часы, от которых считается фаза мигания, + * не идут внутри каждого такого окна — тик вызывается исправно между + * блоками, но «сейчас» между вызовами почти не меняется, поэтому + * глазом видно подвисание/дёрганое мигание, а не плавные 250/250. + * Во время копирования чанков (страницы по ~3 мс) окна намного короче + * — там мигание заметно более гладкое. Осознанно не чиним (означало бы + * не маскировать IRQ на время IP-команды — прямой путь к HardFault, + * см. bsp/qspi_flash/README.md, «XIP-безопасность»); фиксируем как + * факт в HARDWARE_VERIFICATION_LED_PATTERNS.md. */ void led_status_tick_install(void); diff --git a/firmware/bootloader/test_stub/CMakeLists.txt b/firmware/bootloader/test_stub/CMakeLists.txt index df95fa3..9a63a1c 100644 --- a/firmware/bootloader/test_stub/CMakeLists.txt +++ b/firmware/bootloader/test_stub/CMakeLists.txt @@ -34,9 +34,16 @@ function(add_mcuboot_stub) ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE} ${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c + ${CMAKE_SOURCE_DIR}/firmware/bootloader/src/led_status.c ${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c) - target_include_directories(${ARG_NAME} PRIVATE ${MCUBOOT_BOOTUTIL_INCLUDES}) + # led_status.h — flash_map_backend.c (общий с bootloader) зовёт + # led_status_tick_install() из поблочного erase (см. led_status.h) — не + # добавляет стабу нового поведения (окно установки здесь никогда не + # открыто, tick — no-op), просто нужен для компиляции общего файла. + target_include_directories( + ${ARG_NAME} PRIVATE ${MCUBOOT_BOOTUTIL_INCLUDES} + ${CMAKE_SOURCE_DIR}/firmware/bootloader/src) target_compile_definitions( ${ARG_NAME} diff --git a/firmware/bootloader/test_stub/HARDWARE_VERIFICATION_LED_PATTERNS.md b/firmware/bootloader/test_stub/HARDWARE_VERIFICATION_LED_PATTERNS.md new file mode 100644 index 0000000..c0d7ffe --- /dev/null +++ b/firmware/bootloader/test_stub/HARDWARE_VERIFICATION_LED_PATTERNS.md @@ -0,0 +1,149 @@ +# Аппаратная верификация — словарь LED-паттернов (Фаза 4) + +Чек-лист проверяет `led_status.c` на реальной плате: 5 сценариев из +[docs/bootloader/LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md) + опционально «неисправность +железа» (требует временной правки кода — раздел 6). Между сценариями 1-3 SWD-перепрошивка/power cycle +не нужны — бутлоадер сам пере-сканирует SD раз в ~1.5 с, доставать/менять файл на карте можно на ходу. + +--- + +## 0. Подготовка (один раз) + +```bash +# Собрать и прошить bootloader +just build::build-bootloader-debug +just host::flash-swd-bootloader-debug +# ОБЯЗАТЕЛЕН power cycle платы после прошивки — SWD не ресетит автоматически + +# Собрать подписанные образы-заглушки (test_stub) — понадобится stub_a_v1_confirmed.bin +just build::build-mcuboot-stub +ls build/Debug/signed/ # ожидаем stub_a_v1_confirmed.bin, stub_b_v2_confirmed.bin, ... + +# Стереть оба слота — чистая плата для сценариев 1-4 +uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 \ + --sector 0x60040000+0x200000 +uv run --directory tools/hil pyocd erase --target mimxrt1050_quadspi --frequency 4000000 \ + --sector 0x60240000+0x200000 + +# Подготовить 2 тестовых файла для SD-карты (кладём рядом, на карту переносим по одному под именем TFT_APP.BIN) +head -c 512 /dev/urandom > /tmp/TFT_APP_garbage.bin + +cp build/Debug/signed/stub_a_v1_confirmed.bin /tmp/TFT_APP_badsig.bin +python3 -c " +with open('/tmp/TFT_APP_badsig.bin', 'r+b') as f: + f.seek(0x1000) # далеко за 32-байтным заголовком — magic/версия целы + b = f.read(1) + f.seek(0x1000) + f.write(bytes([b[0] ^ 0xFF])) # один перевёрнутый байт ломает hash/подпись +" + +# Открыть монитор порта — держать открытым все сценарии, смотреть и на CDC, и на LED +just host::uart-monitor +``` + +Power cycle после прошивки → плата должна войти в сценарий 1 сама. + +--- + +## 1. Норма — ждём SD + +**Подготовка:** SD-карта НЕ вставлена (или без файла `TFT_APP.BIN`). + +**Смотреть:** HEARTBEAT мигает 50 мс горит / 450 мс не горит. APP не горит вообще. + +**В мониторе:** каждые ~1.5 с — `{"type":"status","state":"waiting_for_sd"}`. + +--- + +## 2. Образ отклонён — битый файл (`SD_CANDIDATE_INVALID`) + +**Подготовка:** на SD-карту, под именем `TFT_APP.BIN`, положить `/tmp/TFT_APP_garbage.bin`. Вставить +карту (если уже была вставлена — просто заменить файл, ждать до ~1.5 с). + +**Смотреть:** APP делает ровно 4 быстрых вспышки (80 мс горит / 80 мс не горит), затем гаснет — +плата возвращается к паттерну «Норма» (пункт 1). HEARTBEAT всё это время не меняется. + +**В мониторе:** `{"ok":false,"error":"SD_CANDIDATE_INVALID"}`, следом снова `waiting_for_sd`. + +--- + +## 3. Образ отклонён — неверная подпись (`SD_INSTALL_REJECTED`) + +**Подготовка:** заменить файл на SD-карте на `/tmp/TFT_APP_badsig.bin` (переименовать в `TFT_APP.BIN`). + +**Смотреть:** сначала на секунду-две APP переходит в паттерн «Установка» (250/250, см. пункт 4 ниже — +файл пишется в Slot A, только потом проверка подписи проваливается), затем — те же 4 быстрых вспышки, +затем возврат к «Норме». + +**В мониторе:** `{"type":"status","state":"installing"}`, затем `{"ok":false,"error":"SD_INSTALL_REJECTED"}`, +следом снова `waiting_for_sd`. + +--- + +## 4. Установка — успешная (включая стирание слота) + +**Подготовка:** заменить файл на SD-карте на `build/Debug/signed/stub_a_v1_confirmed.bin` (переименовать +в `TFT_APP.BIN`). Это единственный образ, который можно ставить на чистую плату (см. §5.1 +`PLAN.md` — цель установки всегда Slot A, `stub_a_*` слинкован под её адрес; `stub_b_*` здесь +использовать нельзя — прыгнет в нерабочий код). + +**Смотреть:** +1. APP переходит в 250 мс вкл / 250 мс выкл. +2. Пока идёт стирание слота (~5 с) — мигание подвисает/дёргается, ровным 250/250 не выглядит. + **Это ожидаемо, не баг**: `bsp_qspi_erase_block_64k()` держит `__disable_irq()` на все ~150 мс + каждого блока — глушится и SysTick, от которого тикает `bsp_tick_get_ms()`, часы паттерна на это + время фактически стоят. Известное ограничение, см. `@note` у `led_status_tick_install()` + (`led_status.h`) — сознательно не чиним (означало бы не маскировать IRQ на время IP-команды + стирания, прямой путь к HardFault на XIP). +3. Во время копирования (после стирания, несколько секунд) — мигание заметно более гладкое, чем на + шаге 2: там каждая запись страницы держит IRQ всего ~3 мс, а не ~150 мс. +4. По завершении — оба LED гаснут (бутлоадер передал управление), затем APP начинает мигать в паттерне + самого `test_stub` (500 мс вкл / 250 мс выкл) — это уже не бутлоадер, а другая прошивка. + +**В мониторе:** `{"type":"status","state":"installing"}`, дальше тишина по протоколу бутлоадера (CDC +переходит под управление `test_stub`, если он вообще что-то шлёт). + +--- + +## 5. Recovery Mode + +**Подготовка:** удержать `BSP_BUTTON_2` в момент подачи питания (после сценария 4 в Slot A уже валидный +образ — это не мешает, `BTN 2` проверяется раньше выбора слота). + +**Смотреть:** оба светодиода горят и гаснут ОДНОВРЕМЕННО: 100 мс горят / 100 мс не горят. + +**В мониторе:** `{"type":"status","state":"recovery_mode"}`. + +**Дополнительно (не обязательно):** если в этот момент вставить SD с валидным образом — на несколько +секунд паттерн переключится на «Установка» (250/250, как в пункте 4), затем плата стирает оба слота и +прыгает. Это ожидаемо, не баг: пока установка идёт, экран рисует именно она, recovery-паттерн +возвращается только если установка не состоялась. + +--- + +## 6. (Опционально) Неисправность железа + +Настоящую неисправность QSPI/SDRAM на живой плате не устроить без пайки — этот пункт использует +временную правку одной строки, чтобы искусственно провалить проверку ёмкости QSPI. **Не забыть +откатить после проверки.** + +**Подготовка:** +1. В `firmware/bootloader/src/main.c` найти строку: + ```c + const uint32_t QSPI_MIN_FLASH_SIZE_MB = 16U; + ``` + Временно поднять порог выше реальной ёмкости чипа на плате (если на плате W25Q128 — 16 МБ, + поставить, например, `32U`). +2. Пересобрать и прошить: `just build::build-bootloader-debug && just host::flash-swd-bootloader-debug`. +3. Power cycle. + +**Смотреть:** HEARTBEAT мигает как обычно (50/450). APP мигает быстро без остановки: 100 мс горит / +100 мс не горит — и не останавливается (устойчиво до следующего POR). + +**В мониторе:** `{"type":"qspi_info","chip":"W25Q128","...","pass":false}`. + +**Откатить обязательно:** +1. Вернуть строку в `main.c` к `16U`. +2. Пересобрать и прошить заново: `just build::build-bootloader-debug && just host::flash-swd-bootloader-debug`. +3. Power cycle, убедиться что плата снова показывает «Норма» (пункт 1) — не оставлять плату/дерево + с искусственно применённым порогом.