Compare commits

...

13 commits

146 changed files with 11275 additions and 1095 deletions

View file

@ -80,7 +80,7 @@ CheckOptions:
- key: readability-identifier-naming.PointerParameterPrefix
value: "p_" # uint8_t *p_buffer
- key: readability-function-size.LineThreshold
value: '60'
value: '90'
- key: readability-function-size.StatementThreshold
value: '30'
- key: readability-magic-numbers.IgnoredIntegerValues

View file

@ -1,17 +1,22 @@
name: Release
# Раздельные теги (FIRST_RELEASE_PLAN.md, Шаг 2.1): firmware_test и
# service-tui версионируются и релизятся независимо друг от друга.
# firmware-vX.Y.Z → publish-firmware (только firmware_test_hab.bin, Debug)
# tui-vX.Y.Z → publish-tui (service-tui-vX.Y.Z-{macos,windows}.zip)
# HAB firmware_test, вшиваемый в TUI-бандл, всегда собирается заново из
# текущего HEAD тега tui-v*, а не берётся из отдельного firmware-релиза —
# так проще и не тянет зависимость на чужой GitHub Release.
# Раздельные теги (bootloader — Фаза 5, firmware/bootloader/PLAN.md):
# firmware_test, bootloader и service-tui версионируются и релизятся
# независимо друг от друга.
# firmware-vX.Y.Z → publish-firmware (только firmware_test_hab.bin, Debug)
# bootloader-vX.Y.Z → publish-bootloader (только bootloader_hab.bin, Release,
# подписан ТЕСТОВЫМ HAB-ключом — см.
# firmware/bootloader/SIGNING_CEREMONY.md)
# tui-vX.Y.Z → publish-tui (service-tui-vX.Y.Z-{macos,windows}.zip)
# HAB firmware_test/bootloader, вшиваемые в TUI-бандл, всегда собираются заново
# из текущего HEAD тега tui-v*, а не берутся из отдельных релизов — так проще
# и не тянет зависимость на чужой GitHub Release.
on:
push:
tags:
- "tui-v*"
- "firmware-v*"
- "bootloader-v*"
workflow_dispatch:
inputs:
release_type:
@ -20,6 +25,7 @@ on:
options:
- tui
- firmware
- bootloader
default: tui
concurrency:
@ -30,11 +36,12 @@ permissions:
jobs:
# ─────────────────────────────────────────────────────────────────────────
# firmware — собирает HAB Debug firmware_test. Нужна как для standalone
# firmware-релиза, так и для вшивания в TUI-бандл — выполняется всегда.
# firmware — собирает HAB Debug firmware_test И HAB Release bootloader.
# Нужна как для standalone firmware-/bootloader-релизов, так и для вшивания
# обоих образов в TUI-бандл (package-tui, Фаза 5) — выполняется всегда.
# ─────────────────────────────────────────────────────────────────────────
firmware:
name: Build firmware_test HAB (Debug)
name: Build firmware_test HAB (Debug) + bootloader HAB (Release)
runs-on: ubuntu-latest
timeout-minutes: 90
@ -86,6 +93,30 @@ jobs:
if-no-files-found: error
retention-days: 14
- name: Проверить, что тег совпадает с версией в CMakeLists.txt (bootloader)
if: startsWith(github.ref, 'refs/tags/bootloader-v')
run: |
tag_version="${GITHUB_REF_NAME#bootloader-v}"
file_version=$(grep -m1 -oE 'VERSION [0-9]+\.[0-9]+\.[0-9]+' firmware/bootloader/CMakeLists.txt | awk '{print $2}')
if [[ "$tag_version" != "$file_version" ]]; then
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с VERSION в firmware/bootloader/CMakeLists.txt (${file_version})"
exit 1
fi
echo " ✅ Версия тега совпадает с CMakeLists.txt: ${file_version}"
- name: Собрать bootloader HAB (Release)
run: |
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \
tft-devcontainer-ci:latest bash -lc 'just build::hab-bootloader-release'
- name: Upload bootloader_hab.bin
uses: actions/upload-artifact@v4
with:
name: bootloader-hab-release
path: build/Release/bootloader_hab.bin
if-no-files-found: error
retention-days: 14
# ─────────────────────────────────────────────────────────────────────────
# publish-firmware — только на тег firmware-v*, отдельный standalone-релиз
# HAB-образа (для tools/host/flash_usb.py и ручной прошивки, не через TUI).
@ -116,6 +147,40 @@ jobs:
--title "firmware_test ${GITHUB_REF_NAME#firmware-v}" \
--generate-notes
# ─────────────────────────────────────────────────────────────────────────
# publish-bootloader — только на тег bootloader-v*, отдельный standalone-
# релиз HAB-образа (для tools/host/flash_usb.py / SWD, не через TUI).
# ⚠️ Подписан ТЕСТОВЫМ HAB-ключом (dev/pre-series, HAB Open чип всё равно
# загрузит и unsigned) — реальная SRK-церемония не проведена, см.
# firmware/bootloader/SIGNING_CEREMONY.md. Явно проговорено в release notes,
# чтобы этот asset не приняли за production-подписанный постфактум.
# ─────────────────────────────────────────────────────────────────────────
publish-bootloader:
name: Publish bootloader release
needs: firmware
if: startsWith(github.ref, 'refs/tags/bootloader-v')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download bootloader_hab.bin
uses: actions/download-artifact@v4
with:
name: bootloader-hab-release
path: release-assets/
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${GITHUB_REF_NAME}" \
release-assets/bootloader_hab.bin \
--title "bootloader ${GITHUB_REF_NAME#bootloader-v}" \
--notes "⚠️ HAB-подпись — ТЕСТОВЫЙ ключ (dev/pre-series, HAB Open). Не production. Реальная SRK-церемония — firmware/bootloader/SIGNING_CEREMONY.md."
# ─────────────────────────────────────────────────────────────────────────
# service-tui-{macos,windows} — упаковка PyInstaller-бандла с вшитым HAB
# из job firmware. Запускается на тег tui-v* и на workflow_dispatch с
@ -144,6 +209,12 @@ jobs:
name: firmware-hab-debug
path: build/Debug/
- name: Download bootloader_hab.bin
uses: actions/download-artifact@v4
with:
name: bootloader-hab-release
path: build/Release/
- name: Проверить, что тег совпадает с версией в pyproject.toml
if: startsWith(github.ref, 'refs/tags/tui-v')
run: |
@ -197,6 +268,12 @@ jobs:
name: firmware-hab-debug
path: build/Debug/
- name: Download bootloader_hab.bin
uses: actions/download-artifact@v4
with:
name: bootloader-hab-release
path: build/Release/
- name: Проверить, что тег совпадает с версией в pyproject.toml
if: startsWith(github.ref, 'refs/tags/tui-v')
shell: bash

2
.vscode/launch.json vendored
View file

@ -38,7 +38,7 @@
"interface": "swd",
"loadFiles": [],
"runToEntryPoint": "main",
"preLaunchTask": "build-and-rtt:firmware-test-debug",
"preLaunchTask": "build:bootloader-debug",
},
// =============================================================
// firmware/tft_app FreeRTOS task view

View file

@ -52,19 +52,195 @@
### Что отслеживать
- Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow через `just ci::build` или начинает запускать host-тесты.
- ~~Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow~~ — решено: job `test` уже запускает host-тесты (`just ci::test` → `just build::test-host-release`).
- Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL.
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
- Следить за развитием BSP: RGB (частично закрыто display-тестом), bootloader или `tft_app`обе директории всё ещё не заведены.
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log.
- Отслеживать мерж ветки `feature-tui-monolith` в `dev` — после мержа эту запись нужно закрыть датой и финальным диапазоном SHA.
- Следить за развитием BSP: RGB (частично закрыто display-тестом); `tft_app` — директория всё ещё не заведена (bootloader — реализован, Фазы 06, см. запись ниже).
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log (пример — точечный патч `fault_injection_hardening.c` под `#if defined(__arm__)`, bootloader Фаза 3, см. запись ниже).
- ~~Отслеживать мерж ветки `feature-tui-monolith` в `dev`~~ — смёржено (`b4c664f`), запись закрыта датой ниже.
- Отслеживать тег `bootloader-v*` — после первого релиза закрыть запись «bootloader: полная реализация» датой и финальным SHA (сейчас часть диапазона — незакоммиченные изменения рабочего дерева).
## [Не выпущено] — service-tui: монолитный spsdk-бэкенд, устойчивость к обрыву USB, упаковка PyInstaller
## [Не выпущено] — bootloader: полная реализация, Фазы 06 (MCUboot Direct-XIP, HAB, service-tui интеграция)
Диапазон: `1801f1beb959d610d31ee3dcd1f91046953117d4..22c40779ef0ec9911031d7a5272c4611b596d3e8` + незакоммиченные изменения рабочего дерева (документация)
Диапазон: `4644f21507d3..6c564f16388a` + незакоммиченные изменения рабочего дерева (HAB-подпись
тестовым ключом, Тир-0/Тир-1 верификация в service-tui, CI `bootloader-v*`, документация)
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/4644f21...6c564f1>
> `firmware/bootloader/` была пустой директорией на момент базового среза (см. "Что отслеживать"
> выше — теперь снята с наблюдения, кроме тега релиза). Диапазон охватывает всю реализацию с нуля до
> готовности к первому релизу (`VERSION 1.0.0`), шесть фаз согласно (уже удалённому после завершения,
> см. "Удалено" ниже) `firmware/bootloader/PLAN.md`.
### Кратко
- Загрузчик MIMXRT1052 реализован целиком: XIP из Flash, выбор и запуск `tft_app` из одного из двух
слотов (MCUboot Direct-XIP), обновление с microSD, устойчивость к зависшим образам (watchdog +
recovery), HAB-подпись Release-сборки, интеграция с `service-tui` для контроля производственной
прошивки.
- По пути на реальном железе найдено и исправлено более десятка багов — от неверной трактовки
регистров FlexSPI/SRC до архитектурных пробелов в чек-листах верификации; детали по фазам ниже.
- `firmware/bootloader/CMakeLists.txt``VERSION 1.0.0`; все 6 фаз аппаратно верифицированы.
### Добавлено
- **Фаза 0 — карта Flash.** `docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md` — смещения
`BOOTLOADER`/`SLOT_A`/`SLOT_B`, зафиксированы до написания кода.
- **Фаза 1 — скелет.** `firmware/bootloader/{CMakeLists.txt,src/main.c,src/cli.c,src/protocol.{c,h}}`
bring-up (LED/tick/USB CDC), урезанный протокол (`ping`/`get_version`), HAB unsigned Debug-конфиг.
- **Фаза 2 — bootutil (Direct-XIP).** `mcuboot_port/` — шим `flash_area_*` над `bsp_qspi_flash`,
`sysflash.h`, `mcuboot_config.h` (TinyCrypt ECDSA-P256, `MCUBOOT_DIRECT_XIP_REVERT`),
`src/boot_select.{c,h}`. `test_stub/` — заглушка `tft_app` (два слота, разная линковка) для
аппаратной проверки выбора слота.
- **Фаза 3 — SD-путь установки.** `src/{update_policy,slot_version,sd_update}.{c,h}` — сканирование
microSD, установка в неактивный слот, top-level состояние «нет валидного образа»;
`bootloader_fatfs` (read-only FatFS); аппаратный watchdog (`bsp/wdog`).
- **Фаза 4 — SDRAM/QSPI smoke-test.** `bsp/sdram::bsp_sdram_configure()` — C-порт DCD (SEMC/CCM);
`bsp_qspi_decode_chip()` — идентификация чипа по JEDEC; `src/led_status.{c,h}` — единый словарь
LED-паттернов; `dev_sdram_test.c` (dev-only, `BOOTLOADER_DEV_DIAGNOSTICS`).
- **Фаза 6 — recovery.** `bsp/boot_state` — счётчик попыток загрузки в `SRC_GPR3` (переживает
watchdog-сброс, обнуляется на POR); `src/recovery.{c,h}` — чистая функция `recovery_decide()`
(таксономия отказов AD); recovery-режим по `BSP_BUTTON_2` с ослабленным version-gate.
- **Фаза 5 — HAB Release + service-tui.** Тестовый HAB-ключ (`tools/host/hab/keys/`, схема NOCAK) —
`hab_bootloader_release.yaml` реально подписывает Release-образ (`flags=0x08`);
`tools/service_tui/app/bootloader_client.py` — CDC-клиент bootloader
(`get_smoke_status`/`get_qspi_info`); `tools/service_tui/app/screens/verify.py` — экран живой
проверки загрузчика после серийной прошивки (Тир-1); Тир-0 (readback-верификация записи,
`flash_backend._verify_written()`) — включена по умолчанию для любой прошивки через `service-tui`;
`firmware/bootloader/SIGNING_CEREMONY.md` — план настоящей production-подписи (HAB SRK + MCUboot
production-ключ) на будущее.
### Изменено
- **Фаза 3**: детект SD консолидирован на единый `PRSSTAT`; ранний сэмпл кнопки даунгрейда.
- **Фаза 4**: `board_mpu_init()` (общий для всех прошивок код) — добавлен Region 11 под NIC-301
GPV-регистры (`0x41000000`, 8 МБ).
- **Фаза 5**: `Flasher.PRODUCTION` (`tools/service_tui/app/flasher.py`) сужен до сценария A (только
загрузчик) — бандл с `tft_app` (сценарий B) отложен до реализации `tft_app`; production жёстко
резолвит Release, независимо от переменной `FIRMWARE_BUILD_TYPE`. `just host::package-tui`
новый явный гвард на `build/Release/bootloader_hab.bin`. `.github/workflows/release.yml` — тег
`bootloader-v*` (симметрично `firmware-v*`), общий job `firmware` теперь собирает и подписывает
Release-образ bootloader.
- Корневой `README.md` — статус bootloader `запланирован``реализован, v1.0.0`.
### Исправлено
- **Фаза 2** (3 бага, аппаратная верификация): `jump_to_image()` маскировал IRQ перед прыжком (не по
референсу NXP) — вешал `bsp_delay()` в любом целевом образе; `bsp_qspi_read()` не округлял
`IDATSZ` до кратного 4 при IP-чтении — контроллер недодавал слово на хвостах не кратной длины
(впервые проявилось на чтении хэша образа bootutil); `qspi_read_tail()` сравнивал
`IPRXFSTS.FILL` (watermark-юниты по 8 байт) напрямую со счётчиком слов — зависал на хвостах ровно
в 2 слова (чтение подписи ECDSA).
- **Фаза 3**: форсированный даунгрейд физически записывался, но не загружался бы (`boot_go()` всегда
выбирает более высокую версию) — добавлено поле `erase_previous_active`; `fih_panic_loop()`
(вендоренный bootutil) ронял `test-host-release` в CI на x86_64-раннере (`invalid instruction
mnemonic 'b'` — ARM/Thumb-only мнемоника, на arm64 devcontainer случайно ассемблировалась,
маскируя проблему); стабы `test_stub` не позиционно-независимы — линковка под конкретный слот
обязательна и для реального `tft_app`, не только для заглушки.
- **Фаза 6** (4 бага): счётчик попыток загрузки рос и на пустой плате без SD (без реального
зависания) — ошибочно уводил бы в recovery через ~4.5 с в штатном ожидании; в стенде `test_stub`
health-mark вызывался безусловно до проверки `HANG_MODE`, из-за чего счётчик никогда не
накапливался выше 1 (фолбэк не срабатывал); в самом чек-листе Фазы 6 предписывался файл для
чужого слота при проверке recovery-установки; `attempt_boot()` не инкрементировал счётчик перед
первым прыжком в свежеустановленный recovery-образ (симметрия с обычным путём).
- **Фаза 4** (3 бага): AXI-QoS регистры (NIC-301 GPV) валили C-код фолтом — не покрыты
`board_mpu_init()`, DCD успевал их записать до включения MPU, C-порт — нет; результаты
smoke-теста терялись (шлются один раз сразу после `init()`, хост не успевает открыть порт) —
кэширование + переспрос по команде; оценка длительности SDRAM-теста в комментарии оригинала
завышена ~в 6 раз (реальный прогон ~4.2 с, не ~30 с).
- **Фаза 5** (2 бага в `service-tui`, до публикации): production шил bootloader и (будущий) app по
одному адресу `FLASH_BASE` — второй шаг затёр бы первый (наследие до-bootloader архитектуры, для
Direct-XIP неверно); production мог тихо взять unsigned Debug-образ bootloader через
`FIRMWARE_BUILD_TYPE` (переменная предназначена только для firmware_test, дефолт `Debug`) — теперь
Release резолвится жёстко.
### Тесты
- Host-тесты выросли с 13 (Фаза 2) до 16 (Фазы 3/6: `update_policy`, `slot_version`, `recovery`) —
зелёные, Debug и Release, обе платформы (macOS + devcontainer Linux).
- `tools/service_tui`: 68 → 76 тестов (Фаза 5) — Тир-0 readback (`test_flash_backend.py`) + новый
`test_bootloader_client.py`.
- Полный аппаратный чек-лист пройден на каждой фазе (Фазы 26); детали были в удалённых
`HARDWARE_VERIFICATION_*.md`/`DEBUG_LOG_*.md` (см. git-история, "Удалено" ниже).
### CI
- `.github/workflows/release.yml`: новый job `publish-bootloader` (тег `bootloader-v*`), общий job
`firmware` расширен на сборку Release HAB bootloader; `service-tui-{macos,windows}` теперь
докачивают `bootloader_hab.bin` в `build/Release/` перед упаковкой — без этого `just
host::package-tui` падал бы с новым гвардом (см. "Изменено").
- CI-баг `fih_panic_loop`/x86_64 (Фаза 3, см. "Исправлено") — точечный патч вендоренного
`fault_injection_hardening.c` под `#if defined(__arm__)`.
### Документация
- `docs/bootloader/HAB_GUIDE.md` — новый §5.1 (разбор команд CSF-секции `nxpimage`, NOCAK vs полная
SRK-иерархия).
- `firmware/bootloader/SIGNING_CEREMONY.md` — новый план настоящей production-подписи (HAB
SRK-церемония + MCUboot production-ключ), на будущее.
- `docs/DEV_ARCH.md` (корневой) — исправлена фактическая ошибка (bootloader описывался как
«копирование в ITCM», реально XIP без ITCM/DCD) и устаревший путь `tools/production/`.
- `tools/service_tui/docs/DEV_ARCH.md` → переименован в `tools/service_tui/docs/ARCHITECTURE.md`
(коллизия имени с корневым `docs/DEV_ARCH.md`); новый §16 (Тир-0/Тир-1); синхронизирован со всеми
изменениями Фазы 5; починены 10 битых/несогласованных ссылок на файл в трёх других README.
- `docs/CI_WORKFLOW.md` — синхронизирован с `bootloader-v*` (диаграмма, триггеры, таблица job'ов).
### Удалено
- `firmware/bootloader/PLAN.md`, `DEBUG_LOG_PHASE2.md`, `DEBUG_LOG_PHASE3_SD.md`,
`test_stub/HARDWARE_VERIFICATION_{PHASE2,PHASE3,PHASE6,LED_PATTERNS}.md`, `docs/CI_PLAN.md`
планирующие/трекинговые документы и чек-листы, отработавшие своё после завершения всех фаз;
фактическое содержание либо перенесено в постоянные документы (`README.md`,
`docs/bootloader/HAB_GUIDE.md`, `docs/CI_WORKFLOW.md`), либо остаётся доступным в git-истории. Тот
же паттерн, что уже применялся к `FIRST_RELEASE_PLAN.md`/`RELEASE_ROADMAP.md`/`just/ci_workflow.md`
при предыдущем релизе (см. запись ниже).
## [2026-07-07 .. 2026-07-13] — Первый релиз: `tui-v0.2.0`/`firmware-v0.1.2`, точечный `tui-v0.2.1`
Диапазон: `8869b3c8..d9fb813b`
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/8869b3c...d9fb813>
### Кратко
- Первый тег-релиз проекта: `firmware-v0.1.2` (firmware_test HAB Debug) и `tui-v0.2.0` (service-tui
PyInstaller-бандл, macOS + Windows) — итог ветки `feature-tui-monolith` (см. запись ниже, закрыта
этим же релизом).
- Директория service-tui переименована `tools/production/``tools/service_tui/`.
- Точечный релиз `tui-v0.2.1` (отдельная ветка `service-tui-fixes`) — найден и исправлен полевой баг
записи во Flash, воспроизводившийся на случайном подмножестве плат.
### Изменено
- `tools/production/``tools/service_tui/` (директория и все внутренние пути/ссылки).
- Из репозитория убран ранее случайно закоммиченный `dist/` (собранные PyInstaller-бандлы) —
добавлен `.gitignore`.
### Исправлено
- **QE-бит (Winbond) не выставлялся при auto-config Flashloader — ~50/500 плат в поле падали на
ЛЮБОЙ flash-операции.** Option word `0xC0000007` (со старта проекта, унаследован
`flash_backend.py`/`flash_usb.py`) не включает Quad Enable; часть партий W25Q128 приходит с завода
с QE=0, из-за чего чип остаётся в SPI-режиме при LUT, настроенных на quad-команды →
`status 20106 FlexSPINOR: Command Failure` на любой команде. Две промежуточные гипотезы (порядок
commit-FCB/erase; маргинальный электрический контакт) проверены на живом железе и опровергнуты.
Причина найдена пересчётом (не «на глаз») десятичного option word из логов NXP MCUBootUtility:
`0xC0000207`. QE энергонезависимый — после одной корректной установки (в т.ч. случайно, через
сторонний инструмент) плата «чинится» навсегда, что и маскировало баг как нестабильный.
- M5StampPLC: два раунда фиксов детекта порта и CAN-обмена в `service-tui`.
### Удалено
- Планирующие документы, отработавшие своё к моменту релиза — `FIRST_RELEASE_PLAN.md`,
`RELEASE_ROADMAP.md`, `just/ci_workflow.md`, `tools/production/docs/MONOLITH_APP_PLAN.md`.
Содержание перенесено в постоянные `README.md`/`docs/DEV_ARCH.md` (тогда ещё под именем
`tools/production/`).
## [2026-07-07] — service-tui: монолитный spsdk-бэкенд, устойчивость к обрыву USB, упаковка PyInstaller
Диапазон: `1801f1beb959d610d31ee3dcd1f91046953117d4..22c40779ef0ec9911031d7a5272c4611b596d3e8` (мёрж в `dev``b4c664fe121226c4231675a150fba809e73b21d6`)
Сравнение: <https://github.com/OSabuser/tft_manufacture_test/compare/1801f1beb959d610d31ee3dcd1f91046953117d4...22c40779ef0ec9911031d7a5272c4611b596d3e8>
> Ветка `feature-tui-monolith` (от `dev`, поверх мержа `feature-tui-python`).
> Ветка `feature-tui-monolith` (от `dev`, поверх мержа `feature-tui-python`). Смёржено в `dev` и
> выпущено как часть первого релиза (`tui-v0.2.0`/`firmware-v0.1.2`) — см. запись выше.
> **Полностью заменяет предыдущую версию этой записи**: прошивка сторонних
> бинарников через subprocess (`tools/host/flash_usb.py --fcb-path` +
> `nxpimage`) была реализацией на момент Фазы 0/раннего мержа и с тех пор

View file

@ -18,14 +18,14 @@ if(NOT BUILD_TESTS_HOST)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
set(BSP_SYSCALLS_FILE
"${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c"
CACHE FILEPATH "Заглушки системных вызовов newlib")
"${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c"
CACHE FILEPATH "Заглушки системных вызовов newlib")
set(BSP_GENERATED
"${CMAKE_SOURCE_DIR}/bsp/generated"
CACHE PATH "Путь до сгенерированных ConfigTools файлов")
"${CMAKE_SOURCE_DIR}/bsp/generated"
CACHE PATH "Путь до сгенерированных ConfigTools файлов")
set(BSP_STARTUP_FILE
"${BSP_GENERATED}/startup/startup_MIMXRT1052.S"
CACHE FILEPATH "Путь до стартап файла")
"${BSP_GENERATED}/startup/startup_MIMXRT1052.S"
CACHE FILEPATH "Путь до стартап файла")
endif()
# -----------------------------------------------------------------------------
@ -46,8 +46,10 @@ add_subdirectory(lib)
# -----------------------------------------------------------------------------
if(NOT BUILD_TESTS_HOST)
add_subdirectory(firmware/test)
# add_subdirectory(firmware/bootloader) - add_subdirectory(firmware/app) #
# Загрузчик + Основное приложение
add_subdirectory(firmware/bootloader)
# Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) — удалить,
# когда появится реальный firmware/tft_app. См. firmware/bootloader/PLAN.md.
add_subdirectory(firmware/bootloader/test_stub)
endif()
# -----------------------------------------------------------------------------

View file

@ -126,6 +126,19 @@
"app"
]
},
{
"name": "mcuboot-stub-debug",
"displayName": "mcuboot slot stub (Фаза 2/6 верификация) — Debug",
"configurePreset": "Debug",
"targets": [
"test_slot_stub_a",
"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"
]
},
{
"name": "app-release",
"displayName": "app — Release",
@ -150,7 +163,11 @@
"test_prio_queue",
"uart_host_mock_example",
"test_ring_buffer",
"test_timeout_pattern"
"test_timeout_pattern",
"test_mcuboot_boot_select",
"test_slot_version",
"test_update_policy",
"test_recovery"
]
},
{
@ -169,7 +186,11 @@
"test_prio_queue",
"uart_host_mock_example",
"test_ring_buffer",
"test_timeout_pattern"
"test_timeout_pattern",
"test_mcuboot_boot_select",
"test_slot_version",
"test_update_policy",
"test_recovery"
]
},
{

View file

@ -9,21 +9,21 @@
## Firmware-проекты
| Проект | Путь | Описание |
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
| Тестовая прошивка (реализована) | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
| Загрузчик (запланирован) | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD |
| Production прошивка (запланирован) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
| Проект | Путь | Описание |
| ----------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS [README](firmware/test/README.md) |
| Загрузчик | `firmware/bootloader/` | A/Б обновление через uSD (MCUboot, Direct-XIP), recovery при зависании образа. Обновляется только через USB ROM + blhost / SWD [README](firmware/bootloader/README.md) |
| Production прошивка (запланирована) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
---
## Инструменты (`tools/`)
| Инструмент | Путь | Назначение |
| ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------- |
| Инструмент | Путь | Назначение |
| ------------------ | -------------------- | ---------------------------------------------------------------------------------------------- |
| Сервисный TUI | `tools/service_tui/` | Диагностика и прошивка готовых плат сервисным инженером [README](tools/service_tui/README.md) |
| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке [README](tools/host/README.md) |
| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов [README](tools/hil/README.md) |
| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке [README](tools/host/README.md) |
| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов [README](tools/hil/README.md) |
---

View file

@ -2,10 +2,7 @@ if(BUILD_TESTS_HOST)
return()
endif()
# -----------------------------------------------------------------------------
# bsp_board — генерированные файлы Config Tools, инициализация платы
# -----------------------------------------------------------------------------
# bsp_board — cгенерированные Config Tools файлы, инициализация платы
add_library(bsp_board STATIC generated/board.c generated/pin_mux.c)
add_subdirectory(common)
@ -15,6 +12,8 @@ add_subdirectory(uart_host)
add_subdirectory(opto)
add_subdirectory(can)
add_subdirectory(button)
add_subdirectory(wdog)
add_subdirectory(boot_state)
add_subdirectory(display)
add_subdirectory(usb_cdc)
add_subdirectory(sdram)
@ -23,7 +22,6 @@ add_subdirectory(sd)
add_subdirectory(mqs)
add_subdirectory(provisioning)
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
target_compile_options(bsp_board PRIVATE -w)
# SYSTEM подавляет предупреждения для всех внешних потребителей
@ -66,3 +64,8 @@ target_compile_definitions(
add_library(bsp_boot_ram INTERFACE)
target_compile_definitions(bsp_boot_ram INTERFACE SKIP_SYSCLK_INIT)
add_library(bsp_boot_xip_no_dcd INTERFACE)
target_compile_definitions(
bsp_boot_xip_no_dcd INTERFACE XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1
SKIP_SYSCLK_INIT)

View file

@ -0,0 +1,14 @@
if(BUILD_TESTS_HOST)
return()
endif()
add_library(bsp_boot_state STATIC src/boot_state.c)
target_include_directories(
bsp_boot_state
PUBLIC include/
PRIVATE src/)
target_link_libraries(
bsp_boot_state
PRIVATE sdk_src)

79
bsp/boot_state/README.md Normal file
View file

@ -0,0 +1,79 @@
# bsp_boot_state — счётчик попыток загрузки (SRC_GPR)
Счётчик попыток загрузки поверх `SRC` General Purpose Register — переживает тёплый/watchdog-сброс,
обнуляется только на POR. Даёт коду восстановления прожить несколько сбросов подряд без
персистентного хранилища во flash.
---
## Аппаратура
| Параметр | Значение |
| ------------------------ | -------------------------------------------------------------- |
| Периферия | SRC (System Reset Controller) |
| Регистр счётчика | `SRC_GPR[2]` (GPR3, 0-based индекс `fsl_src` API) |
| Переживает | тёплый сброс, watchdog-сброс |
| Обнуляется | только POR (детект — `SRC->SRSR`, бит `IPP_RESET_B`) |
| Занято ROM (не трогать) | GPR1/2 (warm-boot entry/arg), GPR6/7/8/9 (ROM, explicit note в RM), GPR10 (альт. SBMR1) |
| Не занято ROM, но занято конвенцией | GPR5 — RM рекомендует под различение SYSRESETREQ/CPU lockup, не наша задача |
---
## Контракт: разделение с `bsp_wdog`
`SRC->SRSR` и `WDOG1->WRSR` — разные регистры с разной семантикой очистки. `WRSR` самоочищается на
каждый сброс (не требует явной очистки — см. `bsp_wdog_caused_last_reset()`). `SRSR`
write-1-to-clear и **копит биты между тёплыми сбросами**, если их не чистить софтом:
`bsp_boot_state_init()` чистит `SRSR` при каждом вызове, поэтому вопрос «был ли сброс по watchdog»
остаётся за `bsp_wdog`, а не за этим модулем — этот модуль отвечает только за «был ли сброс POR».
---
## API
```c
void bsp_boot_state_init(void); /* взвести — читает/чистит SRC->SRSR, детектит POR */
bool bsp_boot_state_was_por(void); /* true, если последний сброс — POR */
uint32_t bsp_boot_attempt_count(void); /* текущее значение счётчика, 0 сразу после POR */
void bsp_boot_attempt_inc(void); /* +1 — звать перед попыткой прыжка в образ */
void bsp_boot_attempt_reset(void); /* обнулить — новый образ/фолбэк-стирание */
void bsp_boot_health_mark(void); /* = bsp_boot_attempt_reset(), для вызова из приложения */
```
`bsp_boot_state_init()` **не идемпотентна** — повторный вызов в той же сессии увидит уже очищенный
`SRSR` как «не POR». Звать ровно один раз, как можно раньше в `main()`.
---
## Быстрый старт
```c
#include "bsp/boot_state.h"
/* main.c — после board_hw_init()/bsp_wdog_init(): */
bsp_boot_state_init();
if (bsp_boot_attempt_count() >= THRESHOLD)
{
/* серия сбросов подряд без здорового образа — решение о фолбэке/recovery
* принимает вызывающий код, не этот модуль */
}
bsp_boot_attempt_inc(); /* перед каждой попыткой прыжка */
/* ... */
bsp_boot_health_mark(); /* приложение подтвердило собственное здоровье */
```
---
## CMake
```cmake
target_link_libraries(firmware_bootloader PRIVATE bsp_boot_state)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| ----------- | ------- | -------------------------------------------------------------- |
| `sdk_src` | PRIVATE | `fsl_src.h``SRC_Get/SetGeneralPurposeRegister`, `SRC_Get/ClearResetStatusFlags` |

View file

@ -0,0 +1,59 @@
/*
* bsp_boot_state счётчик попыток загрузки поверх SRC General Purpose
* Register (MIMXRT1052).
*
* Назначение: пережить несколько сбросов подряд без реального
* персистентного хранилища во flash. SRC_GPR retained-регистр: сохраняет
* значение через тёплый/watchdog-сброс, теряет его только на POR.
*
* Контракт: bsp_boot_state_init() сама детектирует POR по SRC->SRSR и, если
* это POR, обнуляет счётчик вызывающему коду не нужно проверять причину
* сброса самостоятельно для этой цели. Причина «сброс по watchdog?» не
* этот модуль, см. bsp_wdog_caused_last_reset() (bsp/wdog.h): SRC->SRSR и
* WDOG1->WRSR разные регистры с разной семантикой очистки (WRSR
* самоочищается на каждый сброс, SRSR write-1-to-clear, накапливает биты
* между сбросами без явной очистки).
*/
#ifndef BSP_BOOT_STATE_H
#define BSP_BOOT_STATE_H
#include <stdbool.h>
#include <stdint.h>
/*
* Инициализация. Вызывать один раз, как можно раньше в main() (после
* board_hw_init()/bsp_wdog_init()). Читает SRC->SRSR, определяет POR (бит
* IPP_RESET_B на MIMXRT1052 отдельного бита POR нет, эту роль играет он),
* чистит SRSR (write-1-to-clear, иначе биты копятся между тёплыми сбросами)
* и, если это POR, обнуляет счётчик попыток.
*
* Повторный вызов в той же сессии не идемпотентен (снова прочитает и
* очистит уже очищенный SRSR, увидит "не POR"): звать ровно один раз.
*/
void bsp_boot_state_init(void);
/* true, если последний сброс МК (на момент bsp_boot_state_init()) был POR. */
bool bsp_boot_state_was_por(void);
/* Текущее значение счётчика попыток. 0 сразу после POR. */
uint32_t bsp_boot_attempt_count(void);
/* Инкремент счётчика попыток. Звать перед каждой попыткой прыжка в образ. */
void bsp_boot_attempt_inc(void);
/*
* Обнулить счётчик попыток. Звать при: успешной установке нового образа
* (свежему образу полный бюджет попыток), фолбэк-стирании зависшего слота
* (ситуация изменилась).
*/
void bsp_boot_attempt_reset(void);
/*
* Отметить образ здоровым семантический алиас bsp_boot_attempt_reset() для
* вызова из приложения (tft_app), а не из загрузчика: тот же эффект (счётчик
* обнуляется), но имя называет намерение вызывающей стороны.
*/
void bsp_boot_health_mark(void);
#endif /* BSP_BOOT_STATE_H */

View file

@ -0,0 +1,67 @@
/*
* bsp_boot_state реализация поверх fsl_src (SRC_GPR5 + SRC->SRSR).
*/
#include "bsp/boot_state.h"
#include "fsl_src.h"
#define BSP_BOOT_STATE_SRC_BASE SRC
/*
* GPR-индекс счётчика попыток (0-based, index=0 -> GPR1). Сверено с i.MX RT1050
* RM (SRC, гл. 21.8.5-21.8.13): GPR1/2 ROM (entry/arg пробуждения из
* low-power), GPR6/7/8/9 ТОЖЕ explicit "used by the ROM code, should not be
* used by application software" (несмотря на первоначальное предположение,
* что свободны только GPR1/2/10), GPR10 ROM (альтернативный SBMR1 через бит
* [28]). GPR5 формально не ROM, но RM рекомендует именно его под отдельную
* задачу (различение SYSRESETREQ/CPU lockup) не занимаем во избежание
* конфликта с этой конвенцией. GPR3 (index 2) единственный не подписан
* НИКАКИМ примечанием в RM, описан просто как "arbitrary value".
*/
#define BSP_BOOT_STATE_GPR_INDEX 2U
static bool g_s_was_por = false;
void bsp_boot_state_init(void)
{
uint32_t flags = SRC_GetResetStatusFlags(BSP_BOOT_STATE_SRC_BASE);
g_s_was_por = (flags & (uint32_t) kSRC_IppResetPinFlag) != 0U;
/* SRSR — write-1-to-clear, копит биты между тёплыми сбросами без явной
* очистки (в отличие от WDOG1->WRSR, который самоочищается). Чистим всё,
* что доступно, чтобы следующая загрузка увидела только свою причину. */
SRC_ClearResetStatusFlags(BSP_BOOT_STATE_SRC_BASE, ~0U);
if (g_s_was_por)
{
bsp_boot_attempt_reset();
}
}
bool bsp_boot_state_was_por(void)
{
return g_s_was_por;
}
uint32_t bsp_boot_attempt_count(void)
{
return SRC_GetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX);
}
void bsp_boot_attempt_inc(void)
{
uint32_t count = bsp_boot_attempt_count();
SRC_SetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX, count + 1U);
}
void bsp_boot_attempt_reset(void)
{
SRC_SetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX, 0U);
}
void bsp_boot_health_mark(void)
{
bsp_boot_attempt_reset();
}

View file

@ -1,24 +1,6 @@
/*
* bsp_button тактовые кнопки TactBut1 / TactBut2
*
* Аппаратура:
* TactBut1 GPIO2 pin 30 (GPIO_B1_14), подтяжка к 3V3 внешняя, нажатие = LOW
* TactBut2 GPIO2 pin 31 (GPIO_B1_15), подтяжка к 3V3 внешняя, нажатие = LOW
*
* Пины инициализированы в BOARD_InitPins() (generated/pin_mux.c).
* bsp_button_init() не трогает GPIO только сбрасывает внутреннее состояние.
*
* Использование (bare-metal):
* bsp_button_init();
* // в tick-коллбэке каждые 5 мс:
* bsp_button_poll();
* // в основном цикле:
* if (bsp_button_get_event_pressed(BSP_BUTTON_1)) { ... }
*
* Использование (FreeRTOS):
* // в таске с vTaskDelay(5):
* bsp_button_poll();
* if (bsp_button_get_event_pressed(BSP_BUTTON_1)) { xQueueSend(...); }
*/
#ifndef BSP_BUTTON_H

View file

@ -2,8 +2,6 @@
* @file display.h
* @brief BSP: ELCDIF display driver TFT4 / TFT7 / TFT8 / TFT10.
*
* Bare-metal compatible: callback-based FRAME_DONE notification.
* No FreeRTOS dependency in this layer.
*/
#ifndef BSP_DISPLAY_DISPLAY_H_

View file

@ -91,6 +91,16 @@ static void board_mpu_init(void)
MPU->RBAR = ARM_MPU_RBAR(10U, 0x40000000U);
MPU->RASR = ARM_MPU_RASR(0U, ARM_MPU_AP_FULL, 2U, 0U, 0U, 0U, 0U, ARM_MPU_REGION_SIZE_4MB);
/* Region 11: Device, NIC-301 GPV (bus-arbitration QoS) 0x41000000, 8 MB.
* Region 10 не покрывает GPV0/SIM_MAIN (0x41000000) и GPV4/SIM_M7
* (0x41400000, "Cortex-M7 read/write_qos") лежат за пределами его 4 MB
* от 0x40000000, попадают только под Region 0 (deny-all, errata-воркэраунд
* выше) запись фолтит, хотя регистры реальные и документированы (i.MX
* RT1050 RM, гл. 29 "Network Interconnect Bus System (NIC-301)"). Нужен
* bsp_sdram_configure() для read_qos/write_qos регионов SDRAM/LCD/M7. */
MPU->RBAR = ARM_MPU_RBAR(11U, 0x41000000U);
MPU->RASR = ARM_MPU_RASR(0U, ARM_MPU_AP_FULL, 2U, 0U, 0U, 0U, 0U, ARM_MPU_REGION_SIZE_8MB);
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
SCB_EnableDCache();

View file

@ -28,9 +28,27 @@ static sd_io_voltage_t s_io_voltage = {
volatile uint32_t g_sdmmc_dbg_dma_buf_addr = 0U;
volatile uint32_t g_sdmmc_dbg_usdhc1_src_clock_hz = 0U;
static bool sd_card_detect_gpio(void)
/*
* Детект карты через USDHC PRES_STATE.CINST ЕДИНЫЙ механизм и для нашего
* гейта bsp_sd_is_inserted() (bsp/sd/src/sd.c), и для внутреннего
* SD_PollingCardInsert() SDK (тот зовёт этот callback при kSD_DetectCardByGpioCD).
* Не GPIO_PinRead исторически детект через GPIO2/28 давал ложный "card
* present" на пустом слоте (Фаза 3, симптом 1; DEBUG_LOG_PHASE3_SD.md, раунд 3).
* Работает, пока пин D13 замаплен на USDHC1_CD_B (см. BOARD_SD_Config ниже:
* прежний remux на GPIO2_IO28 убран, пин остаётся на USDHC1_CD_B постоянно
* консолидация, item 1). Тактирование USDHC1 включается идемпотентно на случай
* вызова до полного SD_HostInit().
*
* Тот же PRSSTAT-бит читает и штатный host-CD путь SDK
* (SDMMCHOST_CardDetectStatus), но kSD_DetectCardByHostCD дополнительно взводит
* USDHC card-detect ПРЕРЫВАНИЯ не нужны загрузчику (лишний источник IRQ перед
* прыжком), поэтому оставляем polling через callback (kSD_DetectCardByGpioCD).
*/
static bool sd_card_detect_prsstat(void)
{
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) == BOARD_SDMMC_SD_CD_INSERT_LEVEL;
CLOCK_EnableClock(kCLOCK_Usdhc1);
return (USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR) &
(uint32_t) kUSDHC_CardInsertedFlag) != 0U;
}
/* ---------------------------------------------------------------------------
@ -49,7 +67,8 @@ static uint32_t get_usdhc1_src_clock_hz(void)
}
/*
* Управление питанием карты: GPIO1[19] (SdPwr), active-high.
* Управление питанием карты: GPIO1[19] (SdPwr). Регистрируется как
* usrParam.pwr SDK дёргает её из SD_SetCardPower().
*/
static void sd_power_control(bool enable)
{
@ -123,10 +142,12 @@ static void sd_pin_config(uint32_t freq)
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_03_USDHC1_DATA1, pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
/* CD_B в GPIO-режиме: подтяжка вверх + hysteresis для стабильного уровня. */
IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_12_GPIO2_IO28,
IOMUXC_SW_PAD_CTL_PAD_PKE_MASK | IOMUXC_SW_PAD_CTL_PAD_PUE_MASK |
IOMUXC_SW_PAD_CTL_PAD_HYS_MASK | IOMUXC_SW_PAD_CTL_PAD_PUS(1));
/*
* CD (D13) здесь НЕ конфигурируем: пин на USDHC1_CD_B (см. BOARD_SD_Config),
* pad задан в BOARD_InitPins() и на железе даёт корректный CINST. Прежняя
* настройка pad'а GPIO2_IO28 убрана вместе с GPIO-детектом (item 1,
* DEBUG_LOG_PHASE3_SD.md).
*/
}
/* ---------------------------------------------------------------------------
@ -151,10 +172,10 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
g_sdmmc_dbg_dma_buf_addr = (uint32_t)(uintptr_t)s_dma_buf;
g_sdmmc_dbg_usdhc1_src_clock_hz = sd->host->hostController.sourceClock_Hz;
/* --- card detect: GPIO CD (active-low) --- */
/* --- card detect: USDHC PRES_STATE.CINST через callback (polling, без IRQ) --- */
s_cd.cdDebounce_ms = BOARD_SDMMC_SD_CD_DEBOUNCE_MS;
s_cd.type = BOARD_SDMMC_SD_CD_TYPE;
s_cd.cardDetected = sd_card_detect_gpio;
s_cd.type = BOARD_SDMMC_SD_CD_TYPE; /* kSD_DetectCardByGpioCD → callback ниже */
s_cd.cardDetected = sd_card_detect_prsstat;
s_cd.callback = cd; /* обычно NULL из bsp_sd */
s_cd.userData = user_data;
@ -166,15 +187,20 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
/* --- GPIO питания --- */
sd_power_init();
/* CD_B: переводим в GPIO2_IO28 и настраиваем вход */
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);
const gpio_pin_config_t cd_cfg = {
.direction = kGPIO_DigitalInput,
.outputLogic = 0U,
.interruptMode = kGPIO_NoIntmode,
};
GPIO_PinInit(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN, &cd_cfg);
/* Важно: применяем pad-конфиг сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
/*
* CD_B (GPIO_B1_12 / physical D13) остаётся на USDHC1_CD_B постоянно
* единый механизм детекта через PRES_STATE.CINST (sd_card_detect_prsstat
* выше + гейт bsp_sd_is_inserted). Прежней двойной маршрутизации
* (remux на GPIO2_IO28 для GPIO-чтения внутри f_mount) больше нет
* см. DEBUG_LOG_PHASE3_SD.md, раунд 3 «консолидация детекта» (item 1);
* она убирала латентную хрупкость: после первого bsp_sd_init() пин уходил
* на GPIO2_IO28 и повторный PRSSTAT-скан ослеп бы. Явно переустанавливаем
* альт-функцию (BOARD_InitPins() её тоже ставит так модуль не зависит от
* порядка инициализации). Pad этого пина оставляем как задал BOARD_InitPins:
* на железе CINST на нём читается корректно (Фаза 3, все сценарии).
*/
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_USDHC1_CD_B, 0U);
/* Pad-конфиг линий SD (CMD/CLK/DATA) сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
sd_pin_config(400000U);
/* --- приоритет прерывания хоста --- */

View file

@ -8,8 +8,6 @@
* LED_HEARTBEAT системный, мигает как признак жизни прошивки
* LED_APP прикладной, управляется из firmware по ситуации
*
* Пины сконфигурированы в generated/pin_mux.h. Этот хедер не знает
* ни про GPIO-порты, ни про NXP SDK.
*/
#include <stdbool.h>

View file

@ -77,8 +77,9 @@ target_compile_definitions(firmware_test PRIVATE
/* Инициализация — вызвать до bsp_tick_init() */
bsp_status_t bsp_qspi_init(void);
/* Идентификация */
/* Идентификация — bsp_qspi_read_jedec_id() работает и без успешного init() */
bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec);
const char *bsp_qspi_decode_chip(uint8_t cap_byte, uint32_t *p_size_mb); /* device_id & 0xFF → "W25Q128" + МБ */
uint32_t bsp_qspi_flash_size(void); /* доступно после init() */
/* Стирание */
@ -117,9 +118,13 @@ board_hw_init();
bsp_qspi_init(); /* ← до bsp_tick_init() */
bsp_tick_init();
/* Идентификация чипа */
/* Идентификация чипа — работает даже если bsp_qspi_init() выше вернула
ошибку (LUT для JEDEC грузится безусловным первым шагом внутри неё) */
bsp_qspi_jedec_t jedec;
bsp_qspi_read_jedec_id(&jedec);
uint32_t size_mb;
const char *chip_name = bsp_qspi_decode_chip((uint8_t) (jedec.device_id & 0xFF), &size_mb);
/* chip_name = "W25Q128", size_mb = 16 — либо "UNKNOWN"/0, если чип не опознан */
/* Стереть сектор и записать страницу */
bsp_qspi_erase_sector(0x00010000);

View file

@ -90,9 +90,29 @@ bsp_status_t bsp_qspi_init(void);
*
* @param[out] p_jedec Результат. Не NULL.
* @retval BSP_OK / BSP_ERR.
*
* @note Работает и после проваленного bsp_qspi_init() LUT-слот для чтения
* JEDEC ID грузится безусловным первым шагом внутри него, до любой из
* проверок, на которых init() мог отвалиться. Полезно для диагностики
* "что именно распаяно", когда чип не опознан/не тот.
*/
bsp_status_t bsp_qspi_read_jedec_id(bsp_qspi_jedec_t *p_jedec);
/**
* @brief Человекочитаемое имя и ёмкость чипа по capacity byte JEDEC ID.
*
* Тот же байт, что различает поддерживаемые чипы в bsp_qspi_init() вынесен
* отдельно, чтобы потребитель мог опознать чип из уже прочитанного
* bsp_qspi_jedec_t.device_id, не завися от успеха bsp_qspi_init().
*
* @param[in] cap_byte Байт ёмкости (device_id & 0xFF).
* @param[out] p_size_mb Ёмкость чипа, МБ. 0, если байт не распознан.
* Может быть NULL, если размер не нужен.
* @return "W25Q64"/"W25Q128"/"W25Q256"/"W25Q512", либо "UNKNOWN" для
* нераспознанного байта.
*/
const char *bsp_qspi_decode_chip(uint8_t cap_byte, uint32_t *p_size_mb);
/**
* @brief Стирание сектора 4 KB.
*

View file

@ -115,8 +115,8 @@
/**
* @brief Размер читаемого буфера для однобайтных SR-команд.
*
* FlexSPI FIFO работает минимальными единицами в 4 байта (RXWMRK=0, 1 FILL
* unit = 4 bytes). Читаем 4 байта, используем только byte[0].
* FlexSPI FIFO работает минимальными единицами в 4 байта. Читаем 4 байта,
* используем только byte[0].
*/
#define SR_READ_LEN 4U
@ -338,9 +338,9 @@ AT_QUICKACCESS_SECTION_CODE(static status_t qspi_read_tail(uint8_t *p_dst, uint3
while (!done)
{
const uint32_t FILL =
const uint32_t FILL_UNITS =
(QSPI_BASE->IPRXFSTS & FLEXSPI_IPRXFSTS_FILL_MASK) >> FLEXSPI_IPRXFSTS_FILL_SHIFT;
if (FILL >= WORDS_NEEDED)
if ((FILL_UNITS * QSPI_WM_UNIT_WORDS) >= WORDS_NEEDED)
{
done = true;
}
@ -508,7 +508,11 @@ AT_QUICKACCESS_SECTION_CODE(static void qspi_ip_setup(uint32_t seq_idx, uint32_t
AT_QUICKACCESS_SECTION_CODE(static status_t qspi_ip_read(uint32_t seq_idx, uint32_t addr,
uint8_t *p_rx, uint32_t data_len))
{
qspi_ip_setup(seq_idx, addr, data_len);
/* IDATSZ округляем вверх до кратного QSPI_RFDR_WORD_BYTES (4) */
const uint32_t IDATSZ_ALIGNED =
(data_len + (QSPI_RFDR_WORD_BYTES - 1U)) & ~(QSPI_RFDR_WORD_BYTES - 1U);
qspi_ip_setup(seq_idx, addr, IDATSZ_ALIGNED);
const status_t RESULT = qspi_read_fifo(p_rx, data_len);
qspi_wait_idle();
return RESULT;
@ -653,6 +657,42 @@ AT_QUICKACCESS_SECTION_CODE(static bsp_status_t qspi_detect_chip(uint8_t cap_byt
return BSP_OK;
}
const char *bsp_qspi_decode_chip(uint8_t cap_byte, uint32_t *p_size_mb)
{
const char *p_name;
uint32_t size_mb;
switch (cap_byte)
{
case BSP_QSPI_CAP_64MBIT:
p_name = "W25Q64";
size_mb = 8U;
break;
case BSP_QSPI_CAP_128MBIT:
p_name = "W25Q128";
size_mb = 16U;
break;
case BSP_QSPI_CAP_256MBIT:
p_name = "W25Q256";
size_mb = 32U;
break;
case BSP_QSPI_CAP_512MBIT:
p_name = "W25Q512";
size_mb = 64U;
break;
default:
p_name = "UNKNOWN";
size_mb = 0U;
break;
}
if (p_size_mb != NULL)
{
*p_size_mb = size_mb;
}
return p_name;
}
/**
* @brief Общая реализация erase-операций.
*

View file

@ -26,9 +26,10 @@ bsp_status_t bsp_sd_init(void);
bsp_status_t bsp_sd_deinit(void);
/*
* Проверить физическое наличие карты через регистр USDHC PRSSTAT.
* Не требует предварительного вызова bsp_sd_init().
* Включает тактирование USDHC1 на время чтения регистра.
* Проверить физическое наличие карты через USDHC PRES_STATE.CINST
* (USDHC_GetPresentStatusFlags). Включает тактирование USDHC1 на время
* чтения; не требует предварительного вызова bsp_sd_init(). Корректно
* пока пин D13 замаплен на USDHC1_CD_B.
*/
bool bsp_sd_is_inserted(void);

View file

@ -5,15 +5,11 @@
#include "bsp/sd.h"
#include "fsl_sd.h"
#include "fsl_usdhc.h" /* USDHC_Reset — аппаратный сброс FIFO/state machine */
#include "sdmmc_config.h" /* BOARD_SD_Config, BOARD_SDMMC_SD_HOST_BASEADDR */
#include "fsl_usdhc.h"
#include "sdmmc_config.h"
#include <string.h>
#include <string.h> /* memset */
/* ---------------------------------------------------------------------------
* Глобальный дескриптор карты нужен SDK-стеку (передаётся по указателю
* в BOARD_SD_Config и sd_disk_initialize через g_sd).
* Объявлен без static fsl_sd_disk.c ссылается на него как extern sd_card_t g_sd.
* ------------------------------------------------------------------------- */
extern sd_card_t g_sd;
/* ---------------------------------------------------------------------------
@ -37,7 +33,8 @@ static void ensure_host_configured(void)
{
return;
}
/* cd=NULL, userData=NULL: CD управляется хостом через PRSSTAT */
/* cd=NULL, userData=NULL: детект — GPIO-callback внутри BOARD_SD_Config(),
* не внешний callback сюда (см. bsp_sd_is_inserted() тот же механизм). */
BOARD_SD_Config(&g_sd, NULL, BOARD_SDMMC_SD_HOST_IRQ_PRIORITY, NULL);
g_s_host_configured = true;
}
@ -54,19 +51,8 @@ bsp_status_t bsp_sd_init(void)
}
/*
* Аппаратный сброс USDHC FIFO + command/data state machine ПЕРЕД
* повторной инициализацией. Без этого non-blocking host driver SDK
* (fsl_sdmmc_host.c) может остаться в состоянии "ожидание завершения
* предыдущей транзакции" после SD_HostDeinit() на прошлом прогоне —
* физическая транзакция уже умерла вместе с deinit, но внутренний
* флаг ожидания interrupt остаётся выставленным, и следующий f_mount()
* блокируется навсегда в ожидании события, которое никогда не придёт.
*
* USDHC_Reset с маской kUSDHC_ResetAll сбрасывает контроллер на
* регистровом уровне, не полагаясь на состояние, оставленное
* предыдущей сессией. Безопасно вызывать даже при первом запуске
* базовый адрес уже доступен через BOARD_SDMMC_SD_HOST_BASEADDR
* (clock на этот момент должен быть включён, см. ниже).
* Аппаратный сброс USDHC FIFO + command/data state machine перед
* повторной инициализацией.
*/
CLOCK_EnableClock(kCLOCK_Usdhc1); /* тактирование нужно ДО сброса регистров */
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
@ -80,12 +66,20 @@ bsp_status_t bsp_sd_init(void)
(void) memset(&g_sd, 0, sizeof(g_sd));
g_s_host_configured = false; /* форсируем повторный BOARD_SD_Config ниже */
ensure_host_configured(); /* только BOARD_SD_Config — заполняет g_sd */
ensure_host_configured(); /* BOARD_SD_Config — заполняет g_sd, включая usrParam.pwr */
/*
* Полный init (host + card) происходит в sd_disk_initialize SD_Init,
* который вызывается из f_mount disk_initialize.
*/
if (SD_HostInit(&g_sd) != kStatus_Success)
{
return BSP_ERR_HW;
}
if (SD_PollingCardInsert(&g_sd, kSD_Inserted) != kStatus_Success)
{
return BSP_ERR_HW;
}
SD_SetCardPower(&g_sd, false);
SD_SetCardPower(&g_sd, true);
g_s_initialized = true;
return BSP_OK;
@ -101,12 +95,6 @@ bsp_status_t bsp_sd_deinit(void)
SD_HostDeinit(&g_sd);
SD_SetCardPower(&g_sd, false);
/*
* Дополнительный аппаратный сброс сразу после deinit гарантирует,
* что FIFO и state machine USDHC не останутся в промежуточном
* состоянии независимо от того, что делает (или не делает)
* SD_HostDeinit() из SDK на уровне регистров.
*/
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
g_s_initialized = false;
@ -116,6 +104,8 @@ bsp_status_t bsp_sd_deinit(void)
bool bsp_sd_is_inserted(void)
{
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) ==
BOARD_SDMMC_SD_CD_INSERT_LEVEL;
}
CLOCK_EnableClock(kCLOCK_Usdhc1);
uint32_t ps = USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR);
return (ps & kUSDHC_CardInsertedFlag) != 0U;
}

View file

@ -1,9 +1,9 @@
# bsp_sdram — внешняя SDRAM MT48LC16M16A2 (32 МБ)
Минимальная верификация доступности внешней SDRAM, подключённой к SEMC.
Подробное тестирование (паттерны, шина адреса/данных, retention) выполняется
в тест-модуле `firmware_test/test_sdram.c`, который использует константы
и API этого модуля.
Подъём SEMC (для прошивок без DCD) и минимальная верификация доступности
внешней SDRAM, подключённой к SEMC. Подробное тестирование (паттерны, шина
адреса/данных, retention) выполняется в тест-модуле `firmware_test/test_sdram.c`,
который использует API этого модуля (путь с DCD, см. ниже).
---
@ -29,16 +29,45 @@
Тестовая база смещена на 2 МБ от начала — гарантированно выше `.data`/`.bss`
прошивки и ниже non-cacheable региона.
**Важно:** SEMC инициализируется через DCD **до вызова `main()`**. Этот модуль
не настраивает SEMC и не трогает его регистры. Если DCD не отработал —
`bsp_sdram_init()` вернёт ошибку, но исправить ситуацию из модуля нельзя.
---
## Контракт: кто поднимает SEMC
Два независимых пути, в зависимости от того, есть ли у прошивки DCD:
- **С DCD** (`firmware_test`, `bsp_boot_xip`): SEMC поднят DCD **до вызова
`main()`**. Этот модуль в этом случае регистры SEMC не трогает — только
`bsp_sdram_init()` для верификации.
- **Без DCD** (bootloader, `bsp_boot_xip_no_dcd`; в будущем `tft_app`): SEMC
не поднимает никто, пока не будет явно вызван `bsp_sdram_configure()`
побитовый порт проверенной в производстве DCD-последовательности
(`tools/host/dcd/dcd.bin`, «блок 2» — первый блок там мёртвый код,
полностью перезаписывается вторым до какого-либо использования, поэтому
не переносился). Источник истины — сам DCD, а не пересчёт по формулам SDK
из наносекунд: значения регистров контроллера SEMC скопированы дословно.
**Почему `bsp_sdram_configure()` сама поднимает тактирование.** Штатный
`BOARD_BootClockRUN()` (его вызывает `board_hw_init()` в каждой прошивке) НЕ
настраивает PLL2 → PFD2 → делитель SEMC — этот блок в `clock_config.c`
выключен макросом `SKIP_SYSCLK_INIT`, который определён для **всех** таргетов
сборки (`bsp/CMakeLists.txt`), включая `bsp_boot_xip_no_dcd`. Смысл макроса —
не глитчить PLL, пока на нём уже висит поднятая DCD SDRAM (случай
`firmware_test`); но для пути без DCD это побочно означает, что тактирование
SEMC не настраивает вообще никто, кроме `bsp_sdram_configure()`. Результат —
SEMC ≈135.77 МГц (PLL2 528 МГц → PFD2 `FRAC=35` ≈271.54 МГц → `SEMC_PODF` ÷2),
под эту частоту тюнингованы все timing-регистры DCD.
Вызывать `bsp_sdram_configure()` в пути с DCD не нужно и не имеет смысла —
DCD уже сделала эту работу раньше, а `bsp_sdram_configure()` дублировала бы
её же на живой памяти.
---
## API
```c
bsp_status_t bsp_sdram_init(void);
bsp_status_t bsp_sdram_configure(void); /* поднять SEMC: тактирование → пины → контроллер → init-команды SDRAM */
bsp_status_t bsp_sdram_init(void); /* верифицировать SDRAM (SEMC уже поднят — DCD либо bsp_sdram_configure()) */
```
**Публичные константы:**
@ -55,6 +84,37 @@ bsp_status_t bsp_sdram_init(void);
Константы размеров — для потребителей; `bsp_sdram` не запускает по ним
внутренних проходов.
**Поведение `bsp_sdram_configure()`:**
1. Тактирование: `CLOCK_InitSysPll()` (PLL2, 528 МГц) → `CLOCK_InitSysPfd()`
(PFD2, `FRAC=35`) → `CLOCK_SetMux`/`CLOCK_SetDiv` (SEMC ← alt ← PFD2, ÷2).
2. IOMUX: пины `GPIO_EMC_00..41` → ALT0 (функция SEMC), `SW_PAD_CTL_PAD` =
`0x000110F9`; `GPIO_EMC_39` (`SEMC_DQS`) дополнительно получает `SION`
(иначе SEMC не сможет читать собственный read-strobe).
3. Регистры контроллера SEMC (`MCR`, `BMCR0/1`, `BR[0..8]`, `IOCR`,
`SDRAMCR0..3`, `DBICR0/1`, `IPCR1/2`) — побитово из DCD.
4. Командная последовательность SDRAM через `SEMC_SendIPCommand()`:
precharge-all → 2×auto-refresh → mode-set (`0x33` = BL8/sequential/CL3,
согласуется с `SDRAMCR0` и даташитом MT48LC16M16A2) → включение
авто-refresh (`SDRAMCR3 = 0x50210A09`).
5. AXI-QoS приоритеты SDRAM-мастеров (`0x41044100/104` LCD, `0x41442100/104`
Cortex-M7 read/write_qos) — хвост DCD. Это не SEMC и не в заголовках
`sdk/devices/MIMXRT1052` (NXP не заворачивает ARM'овский NIC-301 IP в
CMSIS-структуру), но регистры реальные и документированы (i.MX RT1050 RM,
гл. 29 "Network Interconnect Bus System (NIC-301)" — адреса сверены день-в-
день: `0x41044100` = `GPV0_BASE(0x41000000)+0x44000+0x100` =
`SIM_MAIN.LCD.read_qos`). Для bootloader инертны (нет конкуренции LCD/DMA
vs CPU за шину), но входят в проверенную последовательность и понадобятся
`tft_app`.
**Требует MPU Region 11** (`board_mpu_init()`, `bsp/generated/board.c`,
`0x41000000`, 8 МБ) — штатный Region 10 (периферия, только 4 МБ от
`0x40000000` = 4 домена AIPSTZ) NIC-301 GPV не покрывает; без Region 11 шаг 5
фолтит (deny-all errata-регион 0 перехватывает всё, что не покрыто более
специфичным регионом — `PRIVDEFENA` тут не спасает, т.к. регион 0 покрывает
весь диапазон 0x0..0xFFFFFFFF и потому всегда matched). DCD это переживает —
ROM пишет регистры до включения MPU. Уже добавлен в `board_mpu_init()`.
**Поведение `bsp_sdram_init()`:**
1. Ждёт перехода SEMC в IDLE (`SEMC->STS0 & SEMC_STS0_IDLE_MASK`),
@ -66,11 +126,13 @@ bsp_status_t bsp_sdram_init(void);
**Коды возврата:**
| Код | Условие |
| ----------------- | ------------------------------------------- |
| `BSP_OK` | SDRAM доступна, оба паттерна совпали |
| `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
| `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
| Функция | Код | Условие |
| -------------------------- | ----------------- | -------------------------------------------- |
| `bsp_sdram_configure()` | `BSP_OK` | Тактирование/пины/регистры/команды прошли |
| `bsp_sdram_configure()` | `BSP_ERR_INIT` | IP-команда SEMC вернула ошибку (precharge/refresh/mode-set) |
| `bsp_sdram_init()` | `BSP_OK` | SDRAM доступна, оба паттерна совпали |
| `bsp_sdram_init()` | `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
| `bsp_sdram_init()` | `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
---
@ -79,10 +141,19 @@ bsp_status_t bsp_sdram_init(void);
```c
#include "bsp/sdram.h"
/* Путь С DCD (firmware_test) — SEMC уже поднят до main(): */
if (bsp_sdram_init() != BSP_OK) {
handle_critical_error();
}
/* Путь БЕЗ DCD (bootloader smoke-test и т.п.) — сначала поднять SEMC сами: */
if (bsp_sdram_configure() != BSP_OK) {
handle_semc_bringup_error();
}
if (bsp_sdram_init() != BSP_OK) {
handle_sdram_error();
}
/* Работа с памятью по адресам внутри
[BSP_SDRAM_BASE_ADDR, BSP_SDRAM_BASE_ADDR + BSP_SDRAM_SIZE_BYTES) */
```
@ -91,15 +162,19 @@ if (bsp_sdram_init() != BSP_OK) {
## CMake
Сегодня линкует только `firmware_test` (путь с DCD). Для пути без DCD
потребитель (например, bootloader) добавляет зависимость сам — модуль
никого за собой не тянет:
```cmake
target_link_libraries(firmware_test PRIVATE bsp_sdram)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| ------------ | ------- | -------------------------------------------------- |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаута SEMC IDLE |
| `sdk_semc` | PRIVATE | `fsl_semc.h``SEMC->STS0`, `SEMC_STS0_IDLE_MASK` |
| `bsp_board` | PRIVATE | Общие board-уровневые символы |
| Зависимость | Тип | Описание |
| ------------ | ------- | ----------------------------------------------------------------|
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаута SEMC IDLE |
| `sdk_semc` | PRIVATE | `fsl_semc.h`регистры `SEMC->`, `SEMC_SendIPCommand()` |
| `bsp_board` | PRIVATE | Транзитивно даёт `sdk_clock` (`CLOCK_Init*()`) и `sdk_device` (`IOMUXC`) — отдельных PRIVATE-строк на них не заводили |

View file

@ -2,9 +2,15 @@
* @file sdram.h
* @brief BSP для внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
*
* Архитектурное ограничение [DECISION]:
* SEMC инициализируется DCD до вызова main(). Этот модуль не трогает
* регистры SEMC только верифицирует работоспособность памяти.
* Два пути в зависимости от того, кто использует SEMC:
*
* - Прошивки С DCD (firmware_test): SEMC поднят DCD до main().
* bsp_sdram_init() только верифицирует доступность памяти.
*
* - Прошивки БЕЗ DCD (bootloader smoke-test; в будущем tft_app под свой
* XIP): bsp_sdram_configure() сам поднимает SEMC (порт проверенной
* DCD-последовательности в C, см. tools/host/dcd/dcd.bin), затем
* bsp_sdram_init() верифицирует, как и в первом случае.
*
* Карта памяти:
* 0x80000000 начало SDRAM (SEMC BR0)
@ -14,16 +20,10 @@
* Тестовый регион (не пересекается с .data/.bss и non-cacheable):
* 0x80200000 начало (2 MB offset от базы)
*
* Использование:
* Использование (путь без DCD):
* @code
* bsp_sdram_result_t result;
*
* if (bsp_sdram_init() != BSP_OK) { // DCD не отработал
* handle_critical_error();
* }
*
* bsp_sdram_test_fast(&result); // ~50 мс, 64 KB
* bsp_sdram_test_full(&result); // ~25 с, 1 MB
* if (bsp_sdram_configure() != BSP_OK) { handle_semc_bringup_error(); }
* if (bsp_sdram_init() != BSP_OK) { handle_sdram_error(); }
* @endcode
*/
@ -65,19 +65,53 @@
/* ── Public API ────────────────────────────────────────────────────────── */
/**
* @brief Поднять SEMC и инициализировать внешнюю SDRAM (для вызывателей без DCD).
*
* Побитово-точный порт проверенной в производстве DCD-последовательности
* (tools/host/dcd/dcd.bin, «блок 2») в вызываемый C-код:
* 1. тактирование SEMC (PLL2 528 МГц PFD2 271.54 МГц ÷2 = 135.77 МГц);
* 2. IOMUX/PAD пинов GPIO_EMC (функция SEMC, DQS с SION);
* 3. регистры контроллера SEMC (MCR/BR/IOCR/SDRAMCR0..3/), значения из DCD;
* 4. командная последовательность SDRAM: precharge-all 2×auto-refresh
* mode-set включение авто-refresh;
* 5. AXI-QoS приоритеты SDRAM-мастеров (LCD/Cortex-M7) NIC-301 GPV,
* отдельный IP вне карты SEMC (i.MX RT1050 RM, гл. 29).
*
* Прошивки С DCD (firmware_test) получают то же самое до main() и эту функцию
* НЕ вызывают только bsp_sdram_init().
*
* @note Безопасно вызывать в рантайме после board_hw_init(): CPU тактируется от
* ARM PLL, FlexSPI-XIP от USB1 PLL; PLL2/PFD2 поднимаются с нуля и не
* задевают ни то, ни другое (под SKIP_SYSCLK_INIT штатный
* BOARD_BootClockRUN() эту цепочку не трогает).
*
* @note Требует MPU Region 11 (board_mpu_init(), bsp/generated/board.c)
* шаг 5 пишет NIC-301 GPV (0x41000000+), который Region 10
* (периферия, только 4 МБ от 0x40000000) не покрывает; без Region 11
* запись фолтит (deny-all errata-регион 0 перехватывает всё
* непокрытое). Уже добавлен заметка для будущих правок MPU-таблицы.
*
* @retval BSP_OK SEMC поднят, SDRAM инициализирована.
* @retval BSP_ERR_INIT Командная последовательность SDRAM не завершилась
* (IP-команда SEMC вернула ошибку).
*/
bsp_status_t bsp_sdram_configure(void);
/**
* @brief Верифицировать доступность SDRAM.
*
* Проверяет что SEMC контроллер инициализирован DCD и SDRAM отвечает
* выполняет минимальный write/read/verify на первых 4 байтах тестового
* региона. Не затрагивает .data/.bss прошивки.
* Проверяет что SEMC контроллер инициализирован (DCD или bsp_sdram_configure())
* и SDRAM отвечает выполняет минимальный write/read/verify на первых 4 байтах
* тестового региона. Не затрагивает .data/.bss прошивки.
*
* @note Не реинициализирует SEMC DCD уже сделал это до main().
* @note Не (ре)инициализирует SEMC предполагает, что DCD либо
* bsp_sdram_configure() уже это сделали.
*
* @retval BSP_OK SDRAM доступна и отвечает корректно.
* @retval BSP_ERR_INIT SEMC не готов (DCD не отработал).
* @retval BSP_ERR_INIT SEMC не готов (инициализация не отработала).
* @retval BSP_ERR_TIMEOUT SEMC занят дольше ожидаемого.
*/
bsp_status_t bsp_sdram_init(void);
#endif /* BSP_SDRAM_H_ */
#endif /* BSP_SDRAM_H_ */

View file

@ -1,9 +1,14 @@
/**
* @file sdram.c
* @brief Верификация внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
* @brief Подъём SEMC и верификация внешней SDRAM MT48LC16M16A2 (32 МБ, 16 бит).
*
* SEMC инициализируется DCD до main() этот модуль только проверяет
* доступность памяти. Регистры SEMC не модифицируются.
* Два независимых куска:
*
* bsp_sdram_configure() поднимает SEMC. Побитовый порт проверенной DCD-последовательности
* (tools/host/dcd/dcd.bin)
*
* bsp_sdram_init() верификация уже поднятой памяти (DCD или configure).
* Регистры SEMC не модифицирует.
*
* Кэш: SDRAM настроена как Normal Write-Back cacheable (MPU Region 8).
* Верификация требует явного cache maintenance перед readback иначе
@ -13,12 +18,13 @@
#include "bsp/sdram.h"
#include "bsp/tick.h"
#include "fsl_clock.h"
#include "fsl_semc.h"
#include <stdbool.h>
#include <stdint.h>
/* ── Константы ─────────────────────────────────────────────────────────── */
/* ── Константы верификации ─────────────────────────────────────────────── */
/** @brief Таймаут ожидания готовности SEMC контроллера, мс. */
#define SDRAM_SEMC_IDLE_TIMEOUT_MS 10U
@ -38,11 +44,202 @@
/** @brief Инверсия эталонного паттерна. */
#define SDRAM_VERIFY_PATTERN_B 0x5A5A5A5AUL
/* ── Константы SEMC (значения из DCD, см. tools/host/dcd/dcd.bin) */
/** @brief PFD2 FRAC из DCD (PFD_528=0x00230000): 528 МГц ×18/35 ≈ 271.54 МГц. */
#define SDRAM_SEMC_PFD2_FRAC 35U
/**
* @brief Индекс пина GPIO_EMC_39 (SEMC_DQS) в массивах IOMUXC.
*
* SW_MUX_CTL_PAD[]/SW_PAD_CTL_PAD[] начинаются с GPIO_EMC_00 (индекс 0), шаг 1.
* DQS единственный пин, которому DCD ставит SION (вход strobe должен быть
* принудительно включён, чтобы SEMC читал собственный строб).
*/
#define SDRAM_EMC_PAD_FIRST 0U
#define SDRAM_EMC_PAD_LAST 41U /* GPIO_EMC_41 — последний EMC-пин */
#define SDRAM_DQS_PAD_INDEX 39U /* GPIO_EMC_39 = SEMC_DQS */
/** @brief SION (Software Input On), бит 4 SW_MUX_CTL_PAD — только для DQS. */
#define SDRAM_MUX_SION 0x00000010UL
/** @brief SW_PAD_CTL для всех EMC-пинов (DCD: 0x000110F9 на каждый). */
#define SDRAM_PAD_CTL_VALUE 0x000110F9UL
/** @brief Адрес SDRAM для IP-команд SEMC (совпадает с BR0 = базой SDRAM). */
#define SDRAM_SEMC_IPCMD_ADDR 0x80000000UL
/**
* @brief Значение mode-register SDRAM (DCD IPTXDAT=0x33).
*
* M[2:0]=011 (burst length 8), M3=0 (sequential), M[6:4]=011 (CAS latency 3)
* согласуется с SDRAMCR0 (BL8/CL3) и даташитом MT48LC16M16A2.
*/
#define SDRAM_SEMC_MODE_REG 0x00000033UL
/*
* AXI-QoS регистры арбитража доступа к SDRAM NIC-301 GPV (Global
* Programmer's View). Не влияют на корректность самой SDRAM только на арбитраж при
* конкуренции за шину (LCD/CPU vs SDRAM). */
#define SDRAM_QOS_LCD_READ (*(volatile uint32_t *) 0x41044100UL)
#define SDRAM_QOS_LCD_WRITE (*(volatile uint32_t *) 0x41044104UL)
#define SDRAM_QOS_M7_READ (*(volatile uint32_t *) 0x41442100UL)
#define SDRAM_QOS_M7_WRITE (*(volatile uint32_t *) 0x41442104UL)
/* ── Состояние модуля ──────────────────────────────────────────────────── */
static bool s_initialised = false;
static bool g_s_initialised = false;
/* ── Внутренние функции ────────────────────────────────────────────────── */
/**
* @brief Поднять тактовую цепочку SEMC (порт секции «Clock Init» DCD).
*
*/
static void sdram_configure_clock(void)
{
/* DCD PLL_SYS=0x00002001 → Fout = 24 МГц × (20 + 2×loopDivider + num/denom)
* = 24 × (20 + 2 + 0) = 528 МГц. */
static const clock_sys_pll_config_t SYS_PLL = {
.loopDivider = 1, .numerator = 0, .denominator = 1, .src = 0, /* 24 МГц OSC */
};
/* Гейтим SEMC на время переключения его источника (идиома clock_config.c). */
CLOCK_DisableClock(kCLOCK_Semc);
CLOCK_InitSysPll(&SYS_PLL);
CLOCK_InitSysPfd(kCLOCK_Pfd2, SDRAM_SEMC_PFD2_FRAC);
/* DCD CBCDR=0x00010D40 → SEMC_CLK_SEL=1 (alt), SEMC_ALT_CLK_SEL=0 (→PFD2),
* SEMC_PODF=1 (÷2). */
CLOCK_SetMux(kCLOCK_SemcAltMux, 0); /* alt = PLL2 PFD2 */
CLOCK_SetMux(kCLOCK_SemcMux, 1); /* SEMC clock = alt (PFD2), не periph_clk */
CLOCK_SetDiv(kCLOCK_SemcDiv, kCLOCK_SemcDivBy2);
CLOCK_EnableClock(kCLOCK_Semc);
}
/* ── SEMC: пины ─────────────────────────────────────────────────── */
/**
* @brief Замуксить пины GPIO_EMC на функцию SEMC + PAD-настройки (порт DCD).
*
*/
static void sdram_configure_pins(void)
{
for (uint32_t i = SDRAM_EMC_PAD_FIRST; i <= SDRAM_EMC_PAD_LAST; i++)
{
IOMUXC->SW_MUX_CTL_PAD[i] = 0UL; /* ALT0 = функция SEMC */
IOMUXC->SW_PAD_CTL_PAD[i] = SDRAM_PAD_CTL_VALUE;
}
/* DQS: ALT0 + SION (DCD пишет сюда 0x10 вместо 0x00). */
IOMUXC->SW_MUX_CTL_PAD[SDRAM_DQS_PAD_INDEX] = SDRAM_MUX_SION;
}
/* ── SEMC: регистры контроллера ─────────────────────────────────── */
/**
* @brief Записать регистры контроллера SEMC (дословно «блок 2» DCD).
*
*/
static void sdram_configure_controller(void)
{
SEMC->MCR = 0x10000004UL; /* модуль вкл (MDIS=0), DQSMD=1 (DQS с пина), BTO=16 */
SEMC->BMCR0 = 0x00000081UL; /* веса AXI-очереди A */
SEMC->BMCR1 = 0x00000081UL; /* веса AXI-очереди B */
/* Базовые регистры регионов SEMC. BR0 — сама SDRAM (0x80000000, 32 МБ,
* VLD=1). BR1..BR8 прочие регионы из проверенного DCD, переносятся как
* есть (bootloader их не использует, но конфиг источника истины не режем). */
SEMC->BR[0] = 0x8000001BUL;
SEMC->BR[1] = 0x8200001BUL;
SEMC->BR[2] = 0x8400001BUL;
SEMC->BR[3] = 0x8600001BUL;
SEMC->BR[4] = 0x90000021UL;
SEMC->BR[5] = 0xA0000019UL;
SEMC->BR[6] = 0xA8000017UL;
SEMC->BR[7] = 0xA900001BUL;
SEMC->BR[8] = 0x00000021UL;
SEMC->IOCR = 0x000079A8UL; /* внутренний pinmux SEMC */
/* Геометрия и тайминги SDRAM. SDRAMCR0: PS=16бит, BL=8, COL=9бит, CL=3 —
* MT48LC16M16A2 (даташит: 512 колонок = 9 адресных бит). */
SEMC->SDRAMCR0 = 0x00000F31UL;
SEMC->SDRAMCR1 = 0x00652922UL;
SEMC->SDRAMCR2 = 0x00020201UL;
SEMC->SDRAMCR3 = 0x08193D0FUL; /* тайминги refresh; REN включим в конце */
/* DBICR0/DBICR1 — DCD их пишет, хотя DBI-устройства на плате нет и
* SEMC_ConfigureSDRAM() их не трогает. Инертны (регион DBI не включён),
* но переносятся дословно ради полного соответствия проверенному DCD. */
SEMC->DBICR0 = 0x00000021UL;
SEMC->DBICR1 = 0x00888888UL;
/* Параметры IP-команд: DATSZ=2 байта (запись mode-register 16-бит шиной). */
SEMC->IPCR1 = 0x00000002UL;
SEMC->IPCR2 = 0x00000000UL;
}
/* ── SEMC: командная последовательность инициализации SDRAM ──────── */
/**
* @brief Прогнать init-последовательность SDRAM через IP-команды SEMC.
*
* precharge-all 2×auto-refresh mode-set включение авто-refresh.
*
* @retval BSP_OK все команды завершились успешно.
* @retval BSP_ERR_INIT IP-команда SEMC вернула ошибку.
*/
static bsp_status_t sdram_issue_init_sequence(void)
{
const uint32_t ADDR = SDRAM_SEMC_IPCMD_ADDR;
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR, (uint32_t) kSEMC_SDRAMCM_Prechargeall,
0, NULL) != kStatus_Success)
{
return BSP_ERR_INIT;
}
for (uint32_t i = 0U; i < 2U; i++)
{
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR,
(uint32_t) kSEMC_SDRAMCM_AutoRefresh, 0, NULL) != kStatus_Success)
{
return BSP_ERR_INIT;
}
}
if (SEMC_SendIPCommand(SEMC, kSEMC_MemType_SDRAM, ADDR, (uint32_t) kSEMC_SDRAMCM_Modeset,
SDRAM_SEMC_MODE_REG, NULL) != kStatus_Success)
{
return BSP_ERR_INIT;
}
/* Включить авто-refresh + перейти на рабочие параметры refresh. DCD пишет
* SDRAMCR3 повторно другим значением (не только бит REN) переносим как
* есть; это финальный «operational» refresh-конфиг после инициализации. */
SEMC->SDRAMCR3 = 0x50210A09UL;
return BSP_OK;
}
/* ── SEMC: AXI-QoS арбитраж ─ */
/**
* @brief Настроить приоритеты доступа мастеров к SDRAM
*
* Требуют board_mpu_init() Region 11
*/
static void sdram_configure_axi_qos(void)
{
SDRAM_QOS_LCD_READ = 6UL;
SDRAM_QOS_LCD_WRITE = 6UL;
SDRAM_QOS_M7_READ = 7UL;
SDRAM_QOS_M7_WRITE = 7UL;
}
/* ── Внутренние функции верификации ────────────────────────────────────── */
/**
* @brief Дождаться перехода SEMC в состояние IDLE.
@ -72,10 +269,10 @@ static bsp_status_t wait_semc_idle(void)
*/
static void flush_cache_at_test_base(void)
{
uint32_t *const p_addr = (uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
uint32_t *const P_ADDR = (uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
SCB_CleanDCache_by_Addr(p_addr, (int32_t) SDRAM_CACHE_LINE_BYTES);
SCB_InvalidateDCache_by_Addr(p_addr, (int32_t) SDRAM_CACHE_LINE_BYTES);
SCB_CleanDCache_by_Addr(P_ADDR, (int32_t) SDRAM_CACHE_LINE_BYTES);
SCB_InvalidateDCache_by_Addr(P_ADDR, (int32_t) SDRAM_CACHE_LINE_BYTES);
__DSB();
}
@ -87,16 +284,33 @@ static void flush_cache_at_test_base(void)
*/
static bsp_status_t verify_word(uint32_t pattern)
{
volatile uint32_t *const p_test = (volatile uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
volatile uint32_t *const P_TEST = (volatile uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
*p_test = pattern;
*P_TEST = pattern;
flush_cache_at_test_base();
return (*p_test == pattern) ? BSP_OK : BSP_ERR_INIT;
return (*P_TEST == pattern) ? BSP_OK : BSP_ERR_INIT;
}
/* ── Public API ────────────────────────────────────────────────────────── */
bsp_status_t bsp_sdram_configure(void)
{
sdram_configure_clock();
sdram_configure_pins();
sdram_configure_controller();
bsp_status_t status = sdram_issue_init_sequence();
if (status != BSP_OK)
{
return status;
}
#if defined(__NIC301_EXPERIMENTS_)
sdram_configure_axi_qos();
#endif
return BSP_OK;
}
bsp_status_t bsp_sdram_init(void)
{
bsp_status_t status = wait_semc_idle();
@ -117,6 +331,6 @@ bsp_status_t bsp_sdram_init(void)
return BSP_ERR_INIT;
}
s_initialised = true;
g_s_initialised = true;
return BSP_OK;
}

15
bsp/wdog/CMakeLists.txt Normal file
View file

@ -0,0 +1,15 @@
if(BUILD_TESTS_HOST)
return()
endif()
add_library(bsp_wdog STATIC src/wdog.c)
target_include_directories(
bsp_wdog
PUBLIC include/
PRIVATE src/)
target_link_libraries(
bsp_wdog
PUBLIC bsp_status
PRIVATE sdk_wdog)

80
bsp/wdog/README.md Normal file
View file

@ -0,0 +1,80 @@
# bsp_wdog — аппаратный watchdog (WDOG1)
Аппаратный сброс МК по таймауту. Защищает от зависаний в блокирующих вызовах,
которые не возвращают управление в код приложения — там, где программный
watchdog (флаг + проверка в основном цикле) бессилен, поскольку сам цикл не
выполняется.
---
## Аппаратура
| Параметр | Значение |
| ---------------------- | ---------------------------------------------- |
| Периферия | WDOG1 |
| Шаг таймаута | 0.5 c |
| Диапазон таймаута | 1..128 c |
| Причина сброса | `WDOG1->WRSR.TOUT` (1 — сброс был по watchdog) |
| Поведение под SWD-halt | Приостановлен (`enableDebug = false`) |
---
## Контракт: WDE — write-once
`WDOG_WCR.WDE` (enable) — бит однократной записи: после `bsp_wdog_init()`
watchdog нельзя выключить программно до следующего POR. Он остаётся взведённым
и после любого перехода управления внутри той же сессии питания (переход в
другой образ прыжком, а не через ресет). Любой код, к которому управление
переходит после инициализации watchdog в этой же сессии, обязан периодически
вызывать `bsp_wdog_refresh()` не реже периода таймаута — иначе неизбежен
reset-loop.
---
## API
```c
bsp_status_t bsp_wdog_init(uint32_t timeout_s); /* взвести, once; захватывает причину предыдущего сброса */
void bsp_wdog_refresh(void); /* сбросить счётчик таймаута */
bool bsp_wdog_caused_last_reset(void); /* true, если последний сброс МК — по таймауту WDOG */
bool bsp_wdog_is_armed(void); /* true после успешного init() */
uint32_t bsp_wdog_timeout_s(void); /* сконфигурированный таймаут (0 до init) */
```
`bsp_wdog_refresh()` безопасно звать даже до `bsp_wdog_init()` — no-op.
Звать только в точках подтверждённого прогресса, не непосредственно перед
вызовом, от зависания в котором watchdog и защищает.
---
## Быстрый старт
```c
#include "bsp/wdog.h"
/* main.c — как можно раньше после board_hw_init(): */
bsp_wdog_init(10U); /* c запасом над самой длинной легитимной операцией */
if (bsp_wdog_caused_last_reset())
{
/* предыдущая сессия закончилась таймаутом — восстановились после зависания */
}
/* в основном цикле и в точках подтверждённого прогресса: */
bsp_wdog_refresh();
```
---
## CMake
```cmake
target_link_libraries(firmware_test PRIVATE bsp_wdog)
```
**Зависимости модуля:**
| Зависимость | Тип | Описание |
| ------------ | ------- | ------------------------------------ |
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `sdk_wdog` | PRIVATE | `fsl_wdog.h``WDOG_Init/Refresh()` |

View file

@ -0,0 +1,60 @@
/*
* bsp_wdog аппаратный watchdog (WDOG1, MIMXRT1052).
*
* Назначение: аппаратный сброс МК по таймауту. Защищает от зависаний в
* блокирующих вызовах, которые не возвращают управление в код приложения
* там, где программный watchdog (флаг + проверка в основном цикле) бессилен,
* поскольку сам цикл не выполняется.
*
* Контракт: WDOG Enable (WDE) write-once бит. После bsp_wdog_init() watchdog
* нельзя выключить программно до следующего POR; он остаётся взведённым в
* течение всей сессии питания, включая любую передачу управления внутри неё
* (не только через ресет). Любой код, к которому управление переходит после
* инициализации watchdog в этой же сессии, обязан периодически звать
* bsp_wdog_refresh() не реже периода таймаута иначе reset-loop. Под
* отладчиком (SWD halt) watchdog приостанавливается (enableDebug=false), так
* что пошаговая отладка не сбивается сбросами.
*/
#ifndef BSP_WDOG_H
#define BSP_WDOG_H
#include "bsp/status.h"
#include <stdbool.h>
#include <stdint.h>
/*
* Взвести WDOG1 с таймаутом timeout_s секунд (аппаратно округляется до шага
* 0.5 с: реальный таймаут = (2*timeout_s) * 0.5 c). Разумный диапазон 1..128.
*
* Побочно захватывает причину ПРЕДЫДУЩЕГО сброса (WDOG-таймаут vs прочее) для
* bsp_wdog_caused_last_reset() читать до/после безразлично, но делается тут.
*
* Вызывать один раз, как можно раньше в main() (после board_hw_init()).
* Повторный вызов no-op (WDE уже взведён).
*/
bsp_status_t bsp_wdog_init(uint32_t timeout_s);
/*
* "Погладить" watchdog сбросить счётчик таймаута. Дёшево; безопасно звать
* даже если WDOG не взведён (запись refresh-последовательности безвредна).
* Звать только в точках РЕАЛЬНОГО прогресса, НЕ перед блокирующими вызовами,
* от зависания в которых watchdog и защищает.
*/
void bsp_wdog_refresh(void);
/*
* true, если ПОСЛЕДНИЙ сброс МК был вызван таймаутом WDOG (а не power-on /
* software / прочим). Валидно после bsp_wdog_init(). Для диагностики: показать
* технологу через USB-CDC, что плата восстановилась после зависания.
*/
bool bsp_wdog_caused_last_reset(void);
/* true после успешного bsp_wdog_init(). */
bool bsp_wdog_is_armed(void);
/* Сконфигурированный таймаут в секундах (0, если ещё не взведён). */
uint32_t bsp_wdog_timeout_s(void);
#endif /* BSP_WDOG_H */

79
bsp/wdog/src/wdog.c Normal file
View file

@ -0,0 +1,79 @@
/*
* bsp_wdog реализация поверх fsl_wdog (WDOG1).
*/
#include "bsp/wdog.h"
#include "fsl_wdog.h"
#define BSP_WDOG_BASE WDOG1
/* Границы поля WCR.WT (8 бит): таймаут = (WT+1) * 0.5 c, максимум 128 c. */
#define BSP_WDOG_TIMEOUT_S_MIN 1U
#define BSP_WDOG_TIMEOUT_S_MAX 128U
static bool g_s_armed = false;
static bool g_s_last_reset_was_wdog = false;
static uint32_t g_s_timeout_s = 0U;
bsp_status_t bsp_wdog_init(uint32_t timeout_s)
{
if (g_s_armed)
{
return BSP_OK; /* WDE — write-once; повторно не взводим */
}
if ((timeout_s < BSP_WDOG_TIMEOUT_S_MIN) || (timeout_s > BSP_WDOG_TIMEOUT_S_MAX))
{
return BSP_ERR_PARAM;
}
/*
* Причина предыдущего сброса: читаем WDOG1->WRSR до настройки. WRSR
* read-only, отражает последний сброс (TOUT=WDOG-таймаут, POR=power-on),
* стабилен до следующего сброса.
*/
g_s_last_reset_was_wdog = (BSP_WDOG_BASE->WRSR & WDOG_WRSR_TOUT_MASK) != 0U;
wdog_config_t cfg;
WDOG_GetDefaultConfig(&cfg);
/* WT = 2*timeout_s - 1 → таймаут = (WT+1)*0.5 c = timeout_s c. */
cfg.timeoutValue = (uint16_t) ((timeout_s * 2U) - 1U);
/*
* КРИТИЧНО для рабочего процесса: не сбрасывать плату, когда ядро
* остановлено отладчиком (SWD halt) иначе пошаговая отладка загрузчика
* невозможна. enableWait/enableStop оставляем как в дефолте: загрузчик и
* приложение в эти режимы не входят, но если войдут пусть watchdog
* продолжает считать (безопаснее по умолчанию).
*/
cfg.workMode.enableDebug = false;
cfg.enableWdog = true;
WDOG_Init(BSP_WDOG_BASE, &cfg); /* с этого момента WDE взведён навсегда */
g_s_timeout_s = timeout_s;
g_s_armed = true;
return BSP_OK;
}
void bsp_wdog_refresh(void)
{
WDOG_Refresh(BSP_WDOG_BASE);
}
bool bsp_wdog_caused_last_reset(void)
{
return g_s_last_reset_was_wdog;
}
bool bsp_wdog_is_armed(void)
{
return g_s_armed;
}
uint32_t bsp_wdog_timeout_s(void)
{
return g_s_timeout_s;
}

View file

@ -0,0 +1,271 @@
/*
** ###################################################################
** Processors: MIMXRT1052CVJ5B
** MIMXRT1052CVL5B
** MIMXRT1052DVJ6B
** MIMXRT1052DVL6B
**
** Compiler: GNU C Compiler
** Reference manual: IMXRT1050RM Rev.5, 07/2021 | IMXRT1050SRM Rev.2
**
** Abstract:
** Linker file for firmware/bootloader.
**
** Вариант MIMXRT1052xxxxx_flexspi_nor.ld с m_text, ограниченным
** бюджетом bootloader из docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md —
** 256 KB от 0x60000000 (0x60000000..0x60040000). Slot A (tft_app)
** начинается на 0x60040000 сразу за границей m_text. Превышение
** бюджета — ошибка линковки (ASSERT ниже), а не тихий выход
** кода bootloader за пределы своей области.
**
** Copyright 2016 Freescale Semiconductor, Inc.
** Copyright 2016-2024 NXP
** SPDX-License-Identifier: BSD-3-Clause
** ###################################################################
*/
/* Entry Point */
ENTRY(Reset_Handler)
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x2000;
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x1000;
VECTOR_RAM_SIZE = DEFINED(__ram_vector_table__) ? 0x00000400 : 0;
/* Specify the memory areas */
MEMORY
{
m_flash_config (RX) : ORIGIN = 0x60000000, LENGTH = 0x00001000
m_ivt (RX) : ORIGIN = 0x60001000, LENGTH = 0x00001000
m_interrupts (RX) : ORIGIN = 0x60002000, LENGTH = 0x00000400
m_text (RX) : ORIGIN = 0x60002400, LENGTH = 0x0003DC00 /* до 0x60040000 — граница Slot A */
m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128KB */
m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128KB */
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256KB */
}
/* Define output sections */
SECTIONS
{
__NCACHE_REGION_START = ORIGIN(m_data2);
__NCACHE_REGION_SIZE = 0x2000; /* 8 KB non-cacheable for USB DMA */
.flash_config :
{
. = ALIGN(4);
__FLASH_BASE = .;
KEEP(* (.boot_hdr.conf)) /* flash config section */
. = ALIGN(4);
} > m_flash_config
ivt_begin = ORIGIN(m_flash_config) + LENGTH(m_flash_config);
.ivt : AT(ivt_begin)
{
. = ALIGN(4);
KEEP(* (.boot_hdr.ivt)) /* ivt section */
KEEP(* (.boot_hdr.boot_data)) /* boot section */
KEEP(* (.boot_hdr.dcd_data)) /* dcd section (не используется bootloader — без DCD) */
. = ALIGN(4);
} > m_ivt
/* The startup code goes first into internal RAM */
.interrupts :
{
__VECTOR_TABLE = .;
__Vectors = .;
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} > m_interrupts
/* The program code and other data goes into internal RAM */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.rodata) /* .rodata sections (constants, strings, etc.) */
*(.rodata*) /* .rodata* sections (constants, strings, etc.) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
} > m_text
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} > m_text
.ARM :
{
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} > m_text
.ctors :
{
__CTOR_LIST__ = .;
/* gcc uses crtbegin.o to find the start of
the constructors, so we make sure it is
first. Because this is a wildcard, it
doesn't matter if the user does not
actually link against crtbegin.o; the
linker won't look for a file to match a
wildcard. The wildcard also means that it
doesn't matter which directory crtbegin.o
is in. */
KEEP (*crtbegin.o(.ctors))
KEEP (*crtbegin?.o(.ctors))
/* We don't want to include the .ctor section from
from the crtend.o file until after the sorted ctors.
The .ctor section from the crtend file contains the
end of ctors marker and it must be last */
KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors))
KEEP (*(SORT(.ctors.*)))
KEEP (*(.ctors))
__CTOR_END__ = .;
} > m_text
.dtors :
{
__DTOR_LIST__ = .;
KEEP (*crtbegin.o(.dtors))
KEEP (*crtbegin?.o(.dtors))
KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors))
KEEP (*(SORT(.dtors.*)))
KEEP (*(.dtors))
__DTOR_END__ = .;
} > m_text
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
} > m_text
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} > m_text
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
} > m_text
__etext = .; /* define a global symbol at end of code */
__DATA_ROM = .; /* Symbol is used by startup for data initialization */
.interrupts_ram :
{
. = ALIGN(4);
__VECTOR_RAM__ = .;
__interrupts_ram_start__ = .; /* Create a global symbol at data start */
*(.m_interrupts_ram) /* This is a user defined section */
. += VECTOR_RAM_SIZE;
. = ALIGN(4);
__interrupts_ram_end__ = .; /* Define a global symbol at data end */
} > m_data
__VECTOR_RAM = DEFINED(__ram_vector_table__) ? __VECTOR_RAM__ : ORIGIN(m_interrupts);
__RAM_VECTOR_TABLE_SIZE_BYTES = DEFINED(__ram_vector_table__) ? (__interrupts_ram_end__ - __interrupts_ram_start__) : 0x0;
.data : AT(__DATA_ROM)
{
. = ALIGN(4);
__DATA_RAM = .;
__data_start__ = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */
*(DataQuickAccess) /* quick access data section */
KEEP(*(.jcr*))
. = ALIGN(4);
__data_end__ = .; /* define a global symbol at data end */
} > m_data
__ram_function_flash_start = __DATA_ROM + (__data_end__ - __data_start__); /* Symbol is used by startup for TCM data initialization */
.ram_function : AT(__ram_function_flash_start)
{
. = ALIGN(32);
__ram_function_start__ = .;
*(CodeQuickAccess)
. = ALIGN(128);
__ram_function_end__ = .;
} > m_qacode
__NDATA_ROM = __ram_function_flash_start + (__ram_function_end__ - __ram_function_start__);
.ncache.init :
{
. = ALIGN(32);
__noncachedata_start__ = .;
*(NonCacheable.init)
. = ALIGN(4);
__noncachedata_init_end__ = .;
} > m_data2
. = __noncachedata_init_end__;
.ncache :
{
*(NonCacheable)
. = ALIGN(4);
__noncachedata_end__ = .;
} > m_data2
__DATA_END = __NDATA_ROM;
text_end = ORIGIN(m_text) + LENGTH(m_text);
ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")
ASSERT(text_end <= 0x60040000, "bootloader вышел за пределы бюджета 256 KB (граница Slot A, см. BOOTLOADER_FLASH_MAP.md)")
ASSERT((__noncachedata_end__ - ORIGIN(m_data2)) <= LENGTH(m_data2), "m_data2 ncache overflow")
/* Uninitialized data section */
.bss :
{
/* This is used by the startup in order to initialize the .bss section */
. = ALIGN(4);
__START_BSS = .;
__bss_start__ = .;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
__bss_end__ = .;
__END_BSS = .;
} > m_data
.heap :
{
. = ALIGN(8);
__end__ = .;
PROVIDE(end = .);
__HeapBase = .;
. += HEAP_SIZE;
__HeapLimit = .;
__heap_limit = .; /* Add for _sbrk */
} > m_data
.stack :
{
. = ALIGN(8);
. += STACK_SIZE;
} > m_data
/* Initializes stack on the end of block */
__StackTop = ORIGIN(m_data) + LENGTH(m_data);
__StackLimit = __StackTop - STACK_SIZE;
PROVIDE(__stack = __StackTop);
.ARM.attributes 0 : { *(.ARM.attributes) }
ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap")
}

View file

@ -0,0 +1,130 @@
/*
** ###################################################################
** Linker file for the GNU C Compiler
**
** Abstract:
** Тестовая заглушка для аппаратной верификации Фазы 2 bootutil
** (firmware/bootloader/PLAN.md) — минимальный XIP-образ, который
** boot_go() выбирает и в который bootloader реально прыгает.
** НЕ boot-образ для BootROM (нет FCB/IVT/DCD секций — Slot A/Б не
** видны BootROM напрямую, только software jump из bootloader).
**
** Базовый адрес слота передаётся через -Wl,--defsym=__slot_base__=0x...
** (Slot A: 0x60040000, Slot Б: 0x60240000, см.
** docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). imgtool header (0x200)
** зарезервирован перед vector table — boot_select.c вычисляет адрес
** прыжка как flash_base + fa_off + ih_hdr_size.
** ###################################################################
*/
ENTRY(Reset_Handler)
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x400;
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x400;
SLOT_BASE = DEFINED(__slot_base__) ? __slot_base__ : 0x60040000;
IMGTOOL_HDR_SZ = 0x200; /* -H 0x200 при imgtool sign, см. PLAN.md */
MEMORY
{
m_interrupts (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ, LENGTH = 0x00000400
m_text (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ + 0x400, LENGTH = 0x00008000 /* 32 KB — с большим запасом для мигалки */
m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128KB */
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256KB — не используется стабом, нужна board_mpu_init() */
}
SECTIONS
{
__NCACHE_REGION_START = ORIGIN(m_data2);
__NCACHE_REGION_SIZE = 0x2000; /* 8 KB non-cacheable, как в остальных линкер-скриптах */
.interrupts :
{
__VECTOR_TABLE = .;
__Vectors = .;
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > m_interrupts
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.rodata)
*(.rodata*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
} > m_text
.ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } > m_text
.ARM :
{
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} > m_text
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} > m_text
__etext = .;
__DATA_ROM = .;
.data : AT(__DATA_ROM)
{
. = ALIGN(4);
__data_start__ = .;
*(.data)
*(.data*)
. = ALIGN(4);
__data_end__ = .;
} > m_data
.bss :
{
. = ALIGN(4);
__bss_start__ = .;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
__bss_end__ = .;
} > m_data
.heap :
{
. = ALIGN(8);
__end__ = .;
PROVIDE(end = .);
__HeapBase = .;
. += HEAP_SIZE;
__HeapLimit = .;
__heap_limit = .;
} > m_data
.stack :
{
. = ALIGN(8);
. += STACK_SIZE;
} > m_data
__StackTop = ORIGIN(m_data) + LENGTH(m_data);
__StackLimit = __StackTop - STACK_SIZE;
PROVIDE(__stack = __StackTop);
.ARM.attributes 0 : { *(.ARM.attributes) }
ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap")
}

View file

@ -1,84 +0,0 @@
# Текущее состояние CI/CD workflow для `tft_manufacture_test`
## Обзор
В репозитории настроен рабочий GitHub Actions pipeline, который успешно запускается на событиях `push`, `pull_request` и при ручном запуске через `workflow_dispatch`. Пайплайн специально привязан к реальному окружению разработки проекта: сборка и тесты выполняются внутри того же devcontainer-образа, который описан в `.devcontainer/Dockerfile`, а не в вручную собранной среде на `ubuntu-latest`.
Такой подход уже устранил основные проблемы, которые проявились при первичном поднятии CI: отсутствие ARM toolchain, отсутствие `ninja`, установка неправильного `just` и несовместимость прав доступа при работе с bind-mounted workspace внутри Docker.
## Текущая архитектура
Workflow разделён на две jobы: `build` и `test`.[cite:156] Такое разделение делает пайплайн проще для сопровождения, позволяет отдельно анализировать результаты стадии сборки и создаёт хороший фундамент для следующих этапов — `lint`, coverage, release packaging и аппаратных проверок.
Среда выполнения строится из Dockerfile devcontainer-а проекта, в котором уже определены ARM GCC toolchain в `/opt/arm-toolchain`, обновлённый `PATH`, а также установлены `cmake`, `ninja-build`, `clang-17`, `uv` и `just`.[cite:125] Поскольку все ключевые зависимости уже зафиксированы именно там, использование этого же образа в CI делает поведение раннера максимально близким к локальной разработке.[cite:125]
## Как работает workflow
Workflow реагирует на три типа событий: `push`, `pull_request` и `workflow_dispatch`. Это даёт удобный баланс между автоматической проверкой обычных коммитов и возможностью вручную перезапускать pipeline для отладки инфраструктурных или нестабильных падений без обязательного нового изменения в коде.
Внутри job используется Docker Buildx и `docker/build-push-action`, а кэширование слоёв контейнера подключено через backend GitHub Actions cache с помощью `cache-from: type=gha` и `cache-to: type=gha`. За счёт этого повторные прогоны не пересобирают devcontainer с нуля, а переиспользуют уже собранные Docker-слои, что заметно ускоряет пайплайн после первого успешного заполнения кэша.
## Что делает job `build`
Job `build` выполняет checkout репозитория, инициализирует Buildx, собирает devcontainer image с поддержкой кэша, проверяет версии инструментов внутри контейнера, синхронизирует Python tooling в `tools/host` через `uv sync`, а затем запускает `just ci::build` внутри контейнера. После успешной сборки workflow выгружает директорию `build/` как GitHub artifact, чтобы результаты можно было сохранить и использовать на следующих стадиях.
Важная техническая деталь — команды внутри контейнера запускаются с `--user root`.Это требуется из-за того, что `GITHUB_WORKSPACE` подключается в контейнер как bind mount, а в GitHub Actions non-root пользователь внутри Docker часто не получает права на запись в такую директорию; ранее это как раз ломало создание `.venv` во время `uv sync`.
## Что делает job `test`
Job `test` зависит от `build`, скачивает artifact с директорией `build/`, заново поднимает тот же devcontainer image с использованием cached layers, синхронизирует `tools/host` и запускает `just ci::test` внутри контейнера. На практике это означает, что host unit-тесты работают в той же программной среде, что и стадия сборки, но при этом выделены в отдельный CI-этап.
Так как GitHub-hosted runnerы эфемерны, сам Docker image не передаётся напрямую между jobами.[cite:143] Поэтому обмен между `build` и `test` организован двумя способами: ускорение повторной сборки образа идёт через Docker layer cache, а результаты проекта передаются через GitHub artifacts.
## Почему эта схема хорошо подходит проекту
Этот репозиторий нельзя считать обычным desktop C-проектом: он завязан на фиксированное расположение embedded toolchain и на специально подготовленный devcontainer.[cite:125] Ранние попытки выполнять pipeline прямо на runnerе падали, потому что проект ожидал наличие `/opt/arm-toolchain`, установленный `Ninja` и современный бинарник `just`, который понимает атрибуты вроде `[doc(...)]`.
Перенос CI внутрь devcontainer image устраняет этот класс расхождений и делает Dockerfile единым источником истины для окружения, версий и путей.[cite:125] Это упрощает дальнейшее сопровождение: при изменении инструментария достаточно обновить Dockerfile, и эти же изменения автоматически начнут действовать как локально, так и в CI.[cite:125]
## Чего workflow пока не делает
Текущий pipeline пока не включает обязательную стадию `lint` и статический анализ, потому что в `just/ci.just` для `lint` пока ещё оставлена заглушка, а не полноценный вызов `clang-format` и `clang-tidy`. Он также пока не формирует release/HAB artifacts в CI, хотя в репозитории уже есть соответствующие рецепты `just ci::release` и связанные сборочные шаги.
Также pipeline пока не запускает HIL-сценарии. Это ожидаемо и правильно для текущего этапа: hardware-in-the-loop проверки требуют физического оборудования и в дальнейшем должны выполняться отдельно на self-hosted runner рядом с bench-стендом, а не на GitHub-hosted машинах.
## Сильные стороны текущего решения
У текущей реализации уже есть несколько сильных сторон:
- Она воспроизводима, потому что сборка и тесты выполняются в том же образе, что и локальная разработка.
- Она ускоряется на повторных прогонах за счёт Docker layer caching через GitHub Actions cache backend.
- Она модульна, потому что `build` и `test` вынесены в отдельные jobы, связанные артефактами.
- Она удобна для отладки, потому что build outputs сохраняются как artifacts, а workflow можно запускать вручную через `workflow_dispatch`.
- Она хорошо вписана в структуру проекта, потому что использует уже существующие `just`-точки входа, а не дублирует build-логику в YAML.
## Текущие ограничения
Главное ограничение сейчас состоит в том, что pipeline проверяет собираемость и host unit-тесты, но ещё не закрывает style gate, static analysis, coverage и release packaging. Второе ограничение — Docker image пересобирается в каждой job, поэтому даже при наличии кэша остаётся неизбежный накладной расход по времени по сравнению с вариантом, где используется заранее опубликованный образ из registry.[cite:143]
Есть и архитектурное ограничение GitHub-hosted runnerов для аппаратной части.[cite:127] Прошивка через USB, pyOCD-сценарии и управление стендом должны в будущем быть вынесены в отдельную hardware lane на self-hosted runner.
## Рекомендуемые следующие шаги
### Шаг 1 — добавить `lint` job
Самое логичное следующее улучшение — реализовать полноценную стадию `lint` в `just/ci.just` и подключить отдельную job в workflow. В эту стадию стоит включить `clang-format --dry-run --Werror`, `clang-tidy` и необходимые исключения для generated-кода или vendor-зависимостей, чтобы избежать лишнего шума в CI.
### Шаг 2 — добавить coverage
После стабилизации `lint` полезно подключить экспорт coverage для host-тестов. В `ci.just` уже существует закрытый рецепт `_coverage`, и его можно развить до генерации XML-отчёта, выгрузки артефактов и последующей интеграции с внешним coverage-сервисом, если это будет нужно.
### Шаг 3 — выделить release workflow
Release packaging лучше оформлять отдельным workflow или отдельной gated job, запускаемой только по тегам, на `main` или вручную через `workflow_dispatch`. Это позволит не замедлять обычный PR-цикл, но при этом использовать `just ci::release` и публикацию HAB-артефактов тогда, когда это действительно нужно.
### Шаг 4 — публиковать devcontainer image в GHCR
Следующий сильный шаг по оптимизации — публиковать devcontainer image в GHCR и затем запускать CI уже на базе заранее собранного образа, а не пересобирать его в каждой job.Это ещё сильнее сократит время старта pipeline и сделает масштабирование на `lint`, `coverage` и `release` заметно проще.
### Шаг 5 — добавить self-hosted HIL lane
Финальное крупное направление развития — выделенный аппаратный workflow на self-hosted runner с доступом к MCU-Link, target board и M5StampPLC. Такую lane лучше запускать вручную, по расписанию или по label-триггеру, а не делать обязательной для каждого PR, поскольку аппаратные проверки медленнее, менее стабильны и по природе отличаются от быстрых software regression checks.
## Целевое состояние
Зрелая версия этого CI/CD контура, вероятно, будет состоять из четырёх независимых линий: быстрый PR-pipeline (`build`, `test`, `lint`), optional coverage reporting, отдельный release workflow и отдельный self-hosted HIL pipeline.[cite:156] Такая структура сохранит короткий feedback loop для обычной разработки и одновременно покроет полный жизненный цикл embedded-проекта: от изменений в исходниках до production artifacts и аппаратной валидации на стенде.

View file

@ -3,9 +3,8 @@
> Проект: TFT Firmware (MIMXRT1052CVJ5B)
> Документ описывает схему и принцип работы двух GitHub Actions workflow в
> репозитории: `ci.yml` (обычный PR/push-цикл) и `release.yml` (публикация
> релизных бинарников по тегу). Для истории решений и roadmap развития
> `ci.yml` — см. `just/ci_workflow.md`; этот документ — техническая справка
> «как оно работает сейчас», а не хронология.
> релизных бинарников по тегу). Это техническая справка «как оно работает
> сейчас», а не хронология решений.
---
@ -13,15 +12,14 @@
| | `ci.yml` | `release.yml` |
| --- | --- | --- |
| Когда запускается | `push` в `dev`/`main`, любой `pull_request`, `workflow_dispatch` | `push` тега `tui-v*` / `firmware-v*`, `workflow_dispatch` |
| Когда запускается | `push` в `dev`/`main`, любой `pull_request`, `workflow_dispatch` | `push` тега `tui-v*` / `firmware-v*` / `bootloader-v*`, `workflow_dispatch` |
| Что проверяет | Собирается ли проект и проходят ли host-тесты | Собираются ли и публикуются ли релизные бинарники |
| Публикует что-то наружу? | Нет — только артефакты прогона (для отладки) | Да — GitHub Release с реальными asset'ами (только по тегу) |
| Раннеры | `ubuntu-latest` (оба job'а) | `ubuntu-latest` + `macos-latest` + `windows-latest` |
Они намеренно не смешаны в один файл (см. `just/ci_workflow.md`, «Шаг 3 —
выделить release workflow»): PR-цикл должен оставаться быстрым и не зависеть
от кросс-платформенной упаковки `service-tui`, а релизная публикация не
должна гонять host-тесты повторно на каждый push в PR.
Они намеренно не смешаны в один файл: PR-цикл должен оставаться быстрым и не
зависеть от кросс-платформенной упаковки `service-tui`, а релизная
публикация не должна гонять host-тесты повторно на каждый push в PR.
---
@ -57,8 +55,7 @@ bash -lc '...'` — `--user root` обязателен, иначе non-root по
Чего `ci.yml` **не делает**: lint (заглушка в `just ci::lint`), coverage,
сборку/упаковку `service-tui`, HIL-тесты (нужно физическое железо —
самостоятельная задача для self-hosted раннера). См. `just/ci_workflow.md`
для планов по каждому из этих пунктов.
самостоятельная задача для self-hosted раннера). См. §5 ниже.
---
@ -66,19 +63,21 @@ bash -lc '...'` — `--user root` обязателен, иначе non-root по
### 3.1 Схема тегов
Firmware (`firmware_test`) и `service-tui` версионируются и релизятся
**независимо** (`FIRST_RELEASE_PLAN.md`, Шаг 2.1) — два разных паттерна
тега запускают два разных сценария внутри одного workflow-файла:
Firmware (`firmware_test`), bootloader и `service-tui` версионируются и
релизятся **независимо** — три разных паттерна тега запускают три разных
сценария внутри одного workflow-файла:
```mermaid
flowchart TD
push_fw["push tag\nfirmware-vX.Y.Z"] --> firmware
push_bl["push tag\nbootloader-vX.Y.Z"] --> firmware
push_tui["push tag\ntui-vX.Y.Z"] --> firmware
dispatch["workflow_dispatch\n(release_type: tui | firmware)"] --> firmware
dispatch["workflow_dispatch\n(release_type: tui | firmware | bootloader)"] --> firmware
firmware["firmware\n(ubuntu-latest, devcontainer)\njust build::hab-firmware-test-debug"]
firmware["firmware\n(ubuntu-latest, devcontainer)\nhab-firmware-test-debug +\nhab-bootloader-release"]
firmware -->|"тег firmware-v*"| publishFw["publish-firmware\ngh release create\n(HAB Debug)"]
firmware -->|"тег bootloader-v*"| publishBl["publish-bootloader\ngh release create\n(HAB Release, тестовый ключ)"]
firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| macos["service-tui-macos\njust host::package-tui"]
firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| windows["service-tui-windows\njust host::package-tui"]
@ -87,22 +86,24 @@ flowchart TD
windows -->|"тег tui-v*"| publishTui
```
Ключевое архитектурное решение: **HAB-образ `firmware_test`, который
вшивается внутрь TUI-бандла, всегда собирается заново из текущего HEAD**
джобой `firmware`а не скачивается из последнего опубликованного
`firmware-v*` релиза. Поэтому job `firmware` выполняется **при любом
триггере**, без условия — она нужна и для отдельного firmware-релиза, и
как зависимость для упаковки TUI.
Ключевое архитектурное решение: **HAB-образы `firmware_test` и `bootloader`,
которые вшиваются внутрь TUI-бандла, всегда собираются заново из текущего
HEAD** джобой `firmware`а не скачиваются из последних опубликованных
`firmware-v*`/`bootloader-v*` релизов. Поэтому job `firmware` выполняется
**при любом триггере**, без условия — она нужна и для отдельных релизов
firmware_test/bootloader, и как общая зависимость для упаковки TUI (см.
Фазу 5, `firmware/bootloader/PLAN.md` — до неё `firmware` собирала только
firmware_test, и TUI-бандл молча уходил без образа bootloader).
### 3.2 Триггеры
```yaml
on:
push:
tags: ["tui-v*", "firmware-v*"]
tags: ["tui-v*", "firmware-v*", "bootloader-v*"]
workflow_dispatch:
inputs:
release_type: {type: choice, options: [tui, firmware], default: tui}
release_type: {type: choice, options: [tui, firmware, bootloader], default: tui}
```
`workflow_dispatch` — «сухой прогон» без публикации: собирает всё
@ -125,15 +126,19 @@ job-level `if:`, использовать именно эту форму.
| Job | Раннер | Когда выполняется | Что делает |
| --- | --- | --- | --- |
| `firmware` | `ubuntu-latest`, devcontainer (тот же подход и кэш, что в `ci.yml`) | всегда | сверяет тег `firmware-v*` с `VERSION` в `firmware/test/CMakeLists.txt` (если применимо); `just build::hab-firmware-test-debug`; артефакт `firmware-hab-debug` |
| `firmware` | `ubuntu-latest`, devcontainer (тот же подход и кэш, что в `ci.yml`) | всегда | сверяет тег `firmware-v*` с `VERSION` в `firmware/test/CMakeLists.txt` (если применимо); `just build::hab-firmware-test-debug` → артефакт `firmware-hab-debug`; сверяет тег `bootloader-v*` с `VERSION` в `firmware/bootloader/CMakeLists.txt` (если применимо); `just build::hab-bootloader-release` → артефакт `bootloader-hab-release` |
| `publish-firmware` | `ubuntu-latest` | только push тега `firmware-v*` | скачивает `firmware-hab-debug`; `gh release create firmware-vX.Y.Z firmware_test_hab.bin` — standalone-релиз для `tools/host/flash_usb.py`, без TUI |
| `service-tui-macos` / `service-tui-windows` | `macos-latest` / `windows-latest` | push тега `tui-v*` ИЛИ `workflow_dispatch` с `release_type=tui` | `astral-sh/setup-uv` + `extractions/setup-just` (на раннерах нет `uv`/`just` из коробки); скачивает `firmware-hab-debug` в `build/Debug/`; сверяет тег `tui-v*` с `version` в `pyproject.toml` (если применимо); `just host::service-setup` + `just host::package-tui`; архивирует `dist/service-tui-vX.Y.Z-<os>/` в zip (`zip -r` на macOS, `Compress-Archive` на Windows); артефакт `service-tui-macos`/`service-tui-windows` |
| `publish-bootloader` | `ubuntu-latest` | только push тега `bootloader-v*` | скачивает `bootloader-hab-release`; `gh release create bootloader-vX.Y.Z bootloader_hab.bin` — standalone-релиз (USB ROM/SWD, без TUI); `--notes` явно предупреждает, что HAB-подпись тестовым ключом, не production (см. `firmware/bootloader/SIGNING_CEREMONY.md`) |
| `service-tui-macos` / `service-tui-windows` | `macos-latest` / `windows-latest` | push тега `tui-v*` ИЛИ `workflow_dispatch` с `release_type=tui` | `astral-sh/setup-uv` + `extractions/setup-just` (на раннерах нет `uv`/`just` из коробки); скачивает `firmware-hab-debug` в `build/Debug/` и `bootloader-hab-release` в `build/Release/`; сверяет тег `tui-v*` с `version` в `pyproject.toml` (если применимо); `just host::service-setup` + `just host::package-tui` (падает явно, если `build/Release/bootloader_hab.bin` не найден — production-функция TUI жёстко требует именно этот файл); архивирует `dist/service-tui-vX.Y.Z-<os>/` в zip (`zip -r` на macOS, `Compress-Archive` на Windows); артефакт `service-tui-macos`/`service-tui-windows` |
| `publish-tui` | `ubuntu-latest`, `needs: [service-tui-macos, service-tui-windows]` | только push тега `tui-v*` | скачивает оба zip; `gh release create tui-vX.Y.Z *.zip` |
**Только Debug HAB** идёт в релиз (`FIRST_RELEASE_PLAN.md`, Шаг 1.5) —
Release-сборка `firmware_test` нестабильна (FCB/clock), поэтому `firmware`
джоба собирает только `hab-firmware-test-debug`, не полный
`hab-all-release`.
**Debug HAB для firmware_test, Release HAB для bootloader** — не единое
правило «всегда Debug». Release-сборка `firmware_test` нестабильна
(FCB/clock), поэтому `firmware`-джоба собирает `hab-firmware-test-debug`,
не полный `hab-all-release`. Bootloader — наоборот: production-путь (Фаза 5,
`firmware/bootloader/PLAN.md`) жёстко требует **Release**, подписанный
(`flags=0x08`) — Debug-конфиг bootloader остаётся unsigned и используется
только для локальной отладки, в релиз/TUI-бандл не попадает.
**Сверка версии тег↔файл** — маленький, но важный guard в обеих ветках
(`firmware`/`service-tui-*`): если версия в теге не совпадает с версией в
@ -152,6 +157,7 @@ Git Bash на Windows.
```bash
gh workflow run release.yml --ref dev -f release_type=tui
gh workflow run release.yml --ref dev -f release_type=firmware
gh workflow run release.yml --ref dev -f release_type=bootloader
```
или через веб-интерфейс: Actions → **Release****Run workflow** → выбрать
@ -180,10 +186,13 @@ branch и `release_type`. Джобы `publish-*` в этом сценарии п
- **Self-hosted HIL-раннер.** Ни один из двух workflow не может
задетектировать SDP на живой плате или прогнать деструктивные сценарии
(обрыв USB) — GitHub-hosted раннеры не видят реальное железо. Это ручной
шаг перед каждым релизом (`FIRST_RELEASE_PLAN.md`, Шаг 4), пока не
поднят self-hosted lane (`just/ci_workflow.md`, «Шаг 5»).
- **`lint`/`coverage`** не подключены ни в `ci.yml`, ни в `release.yml`
см. `just/ci_workflow.md`, «Рекомендуемые следующие шаги» (Шаги 12).
шаг перед каждым релизом, пока не поднят self-hosted lane.
- **`lint`/`coverage`** не подключены ни в `ci.yml`, ни в `release.yml`.
- **Публикация devcontainer image в GHCR** — образ пересобирается в каждой
job'е каждого workflow (пусть и с layer-кэшем); заранее опубликованный
образ сократил бы время старта ещё сильнее (`just/ci_workflow.md`, «Шаг 4»).
образ сократил бы время старта ещё сильнее.
- **`bootloader-v*`/`tui-v*` релизы несут HAB-образ, подписанный ТЕСТОВЫМ
ключом** (`tools/host/hab/keys/`, HAB Open, схема NOCAK) — не production.
Реальная SRK-церемония описана в
`firmware/bootloader/SIGNING_CEREMONY.md`, в CI пока не встроена (сама
церемония — не автоматизируемый процесс, см. документ).

View file

@ -68,7 +68,7 @@ graph TB
end
```
---
---
## 3. Что устанавливается и где
@ -198,9 +198,9 @@ flowchart LR
│ │ │ dcd.bin, ivt_flashloader.bin
│ │ └── uv.lock
│ │
│ ├── production/ ← service-tui: TUI сервисного инженера (Textual)
│ ├── service_tui/ ← service-tui: TUI сервисного инженера (Textual)
│ │ прошивка/диагностика готовых плат, см.
│ │ tools/service_tui/README.md + DEV_ARCH.md
│ │ tools/service_tui/README.md
│ │
│ └── hil/ ← HIL pytest-окружение
│ ├── conftest.py ← фикстуры: m5, loaded_<n>, uart_<n>
@ -356,8 +356,8 @@ buildPresets (HIL):
| Прошивка | Стратегия | Инструмент загрузки |
| ---------------------------- | ---------------------------------- | ------------------- |
| `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash |
| `bootloader` | Копирование в ITCM | SPSDK → Flash |
| `tft_app` | XIP + буферы в SDRAM | SPSDK → Flash |
| `bootloader` | XIP из Flash, без ITCM/DCD (не трогает SDRAM) — выбирает и запускает `tft_app` из слота (MCUboot Direct-XIP) | SPSDK → Flash |
| `tft_app` | XIP из своего слота (Direct-XIP, два слота A/Б) + буферы в SDRAM (SEMC поднимает сама) | SPSDK → Flash |
| HIL target (`tests/target/`) | Исполнение из ITCM/DTCM (`ram.ld`) | pyOCD → RAM |
**HIL boot-стратегия:** pyOCD настраивает FLEXRAM (128 KB ITCM + 128 KB DTCM + 256 KB OCRAM), записывает PT_LOAD сегменты ELF по физическим адресам, устанавливает SP/PC из таблицы векторов и запускает выполнение. Flash не используется — прошивка исчезает при отключении питания.

View file

@ -93,7 +93,7 @@ in-process через Python API `spsdk` (`app/flash_backend.py`: `HabImage`,
`configure-memory 0xF000000F`) — auto-config для 4-байтной адресации не
проверялся, решили на него не полагаться
Подробности конвейера — в [tools/service_tui/docs/DEV_ARCH.md](../tools/service_tui/docs/DEV_ARCH.md),
Подробности конвейера — в [tools/service_tui/docs/ARCHITECTURE.md](../tools/service_tui/docs/ARCHITECTURE.md),
§8. Штатный путь (`--firmware`, три сборки этого репозитория, что через
`just host::flash`, что через `service-tui`) не меняется и по-прежнему
использует auto-config Flashloader, как описано в 1.4.

View file

@ -0,0 +1,125 @@
# Карта Flash — bootloader + tft_app (A/Б) + файловая система ассетов
Единая карта QSPI NOR Flash (W25Q64/128/256/512, см. [bsp/qspi_flash](../../bsp/qspi_flash/README.md))
для связки `firmware/bootloader` + `firmware/tft_app`. Дополняет
[BOOT_FLAGS.md](BOOT_FLAGS.md) (сценарии исполнения) и [HAB_GUIDE.md](HAB_GUIDE.md) (подпись).
---
## 1. Принцип: один bootloader на любую ёмкость чипа
`bsp_qspi_flash` определяет чип и его размер в рантайме (`bsp_qspi_init()` → JEDEC ID →
`bsp_qspi_flash_size()`), а не на этапе компиляции. На производстве возможен разброс чипов —
минимум W25Q128 (16 МБ), также встречается W25Q512 (64 МБ).
Карта построена так, чтобы **один и тот же бинарник bootloader** работал на любой ёмкости:
- **Bootloader, Slot A, Slot Б — фиксированные смещения и размеры**, одинаковые на всех платах
независимо от реальной ёмкости чипа. Именно это делает образ bootloader переносимым между платами
с разным флешем без пересборки.
- **Область файловой системы ассетов (спрайты/музыка) — не фиксирована.** Она занимает всё
пространство от конца Slot Б до фактического конца чипа, размер вычисляется в рантайме через
`bsp_qspi_flash_size()` при монтировании. На 16 МБ чипе это ~11.75 МБ, на 64 МБ — ~59.75 МБ.
**Формат и владелец этой области (что её монтирует, кто и как обновляет ассеты) — открытый вопрос,
вне рамок текущего плана bootloader'а.** Здесь фиксируется только адресный диапазон.
`bootutil` (MCUboot, Direct-XIP) знает только про Slot A и Slot Б через `sysflash.h` — про область
ФС ему знать не нужно, коллизий с его логикой нет.
---
## 2. Карта
| Область | Смещение от `0x60000000` | Размер | Абсолютный адрес (начало) |
| ---------------- | ------------------------- | -------------------------------------- | --------------------------- |
| Bootloader | `0x000000` | 256 КБ (`0x040000`) | `0x60000000` |
| Slot A (tft_app) | `0x040000` | 2 МБ (`0x200000`) | `0x60040000` |
| Slot Б (tft_app) | `0x240000` | 2 МБ (`0x200000`) | `0x60240000` |
| ФС ассетов | `0x440000` | `bsp_qspi_flash_size() - 0x440000` | `0x60440000` |
Все границы выровнены на 64 КБ блок (`BSP_QSPI_BLOCK_64K_SIZE`) — стирание region-ов через
`bsp_qspi_erase_block_64k()` без частичных секторов.
Direct-XIP не использует scratch-область — метаданные состояния/версии образа хранятся в trailer
самого образа в каждом слоте (стандартный механизм bootutil), отдельного региона под них не нужно.
---
## 3. Обоснование размеров
**Bootloader — 256 КБ.** Без SDRAM/дисплея/FreeRTOS: bring-up, USB CDC, FatFS, bootutil (Direct-XIP,
без swap/scratch-машинерии), крипто-бэкенд (mbedTLS/tinycrypt), драйвер QSPI. Реальный размер ожидается
существенно меньше — заложен запас на будущий рост (например RSA вместо ECDSA, расширенная диагностика).
**Slot A/Б — 2 МБ каждый.** Ассеты (спрайты, музыка) вынесены в отдельную область ФС и не входят в
подписанный образ — в слоте только код: FreeRTOS, логика индикатора, шрифты как вшитые C-массивы (8bpp
со сглаживанием). Ориентир: текущая production-прошивка (Debug-сборка, с шрифтами, FreeRTOS, FatFS)
занимает ~1 МБ — 2 МБ даёт двукратный запас.
**Почему размеры не пересматриваются "по факту" после первой сборки.** Карта Flash — контракт,
зашитый в уже прошитые на производстве bootloader'ы (обновляются только через SWD/USB ROM, не в поле).
Смещения слотов фиксированы заранее с запасом, а не подгоняются под фактический размер первой сборки
tft_app.
---
## 4. Открытые вопросы (не в рамках плана bootloader)
- Формат и владелец области ФС ассетов (спрайты/музыка): FAT/LittleFS/кастомный, подписывается ли,
как обновляется (та же SD-логика, что и Slot A/Б, или отдельный механизм).
- Точный layout `.ld`-скрипта tft_app для двух адресов слотов (Direct-XIP: код обычно не
позиционно-независим — вероятно потребуется два варианта линковки под Slot A и Slot Б, либо PIC).
**Разобрано** в [UPDATE_FLOW.md](UPDATE_FLOW.md): рекомендован dual-link (два подписанных бинаря на
версию, как `test_stub`), с выбором линковки по целевому слоту при SD-обновлении; PIC — не сейчас.
---
## 5. Ограничения на runtime-доступ к flash из tft_app (Direct-XIP) — решено
Boot-стратегия (Direct-XIP и для bootloader, и для tft_app — см. `firmware/bootloader/PLAN.md`)
пересмотрена и подтверждена в обсуждении Фазы 3, с учётом требований tft_app: (1) кеширование
спрайтов в SDRAM, (2) хранение и изменение настроек во flash, (3) проигрывание WAV с flash.
**Найденный механизм риска** — [bsp/qspi_flash/README.md](../../bsp/qspi_flash/README.md), раздел
"XIP-безопасность": любая IP-команда FlexSPI блокирует AHB-путь — если в этот момент CPU фетчит
инструкцию из Flash, происходит HardFault. Защита в `bsp_qspi_flash` — IRQ lock на всё время операции
(стирание сектора ~45 мс, блока 64 КБ ~150 мс). Для однопоточного блокирующего bootloader'а это не
проблема; для tft_app (FreeRTOS, конкурентные задачи) любая такая операция глушит **все** прерывания
в системе на своё время, включая аудио DMA-колбэк.
**Почему не перешли на `MCUBOOT_RAM_LOAD`** (альтернатива, устраняющая конфликт полностью — код
перестаёт исполняться через flash-AHB вообще): в вендоренном bootutil этот режим **не имеет аналога
`MCUBOOT_DIRECT_XIP_REVERT`** — `boot_select_or_erase()` (`copy_done`/`image_ok`, автоматический откат
неподтверждённого образа) гейтится `#if defined(MCUBOOT_DIRECT_XIP) && defined(MCUBOOT_DIRECT_XIP_REVERT)`
в `loader.c` и не вызывается в ветке `MCUBOOT_RAM_LOAD`. Переход потерял бы anti-brick гарантию,
аппаратно проверенную в Фазе 2 (сценарий 5 её чек-листа), без готовой замены в самом bootutil —
пришлось бы реализовывать такой механизм самостоятельно, без прецедента.
**Референс для калибровки** — легаси-реализация (`TFT8_RX_wOS`, исполняется из SDRAM):
её `audio_player.c` стримит блоками `MONO_READ_SIZE=256` Б через кольцевой буфer `BUFFER_NUM=3` — на
руках держится всего ~8.7 мс аудио (128 сэмплов / 44100 Гц × 3 блока). Этого достаточно при исполнении
из RAM (конкуренции за flash-AHB нет вообще), но недостаточно при Direct-XIP.
**Решение — Direct-XIP остаётся, при двух обязательных ограничениях для tft_app** (переносятся в его
будущий план, не в план bootloader'а):
1. **Аудио — глубоко буферизировать в SDRAM**, не стримить малыми порциями, как в легаси-версии.
Целевая глубина — заведомо больше худшей flash-операции (например, ≥300 мс — это ~26 КБ моно PCM16
44.1 кГц, ничто относительно 32 МБ SDRAM). При такой глубине редкая конкурентная запись настроек не
создаёт слышимого дропаута.
2. **Любое чтение/запись flash, способное совпасть по времени с другой flash-операцией, обязано идти
через защищённый IP-command драйвер** (аналог `bsp_qspi_read()`: ITCM + IRQ lock, как уже сделано в
`bsp_qspi_flash`), а не через сырой XIP `memcpy`, как в легаси `settings_manager.c` (там это было
безопасно только потому что код исполнялся из RAM). Актуально для будущей FatFS-прослойки над
областью ассетов (см. п.4 выше) — она должна использовать тот же паттерн, что `port/fatfs/sd`
использует поверх `bsp_sd`.
3. Запись настроек остаётся редкой, явной, инициированной пользователем (не периодический автосейв) —
короткий блокирующий фриз (десятки мс) на сохранение ожидаем и допустим в UI.
---
## 6. Ссылки
- [BOOT_FLAGS.md](BOOT_FLAGS.md) — XIP/DCD/сценарии исполнения кода.
- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись bootloader (HAB) vs подпись образов tft_app (`imgtool`).
- [bsp/qspi_flash/README.md](../../bsp/qspi_flash/README.md) — поддерживаемые чипы, `bsp_qspi_flash_size()`.

View file

@ -0,0 +1,302 @@
# Загрузчик 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. Индикация и обратная связь
### Светодиоды
Полный словарь (установка, «железо не в порядке», ошибка образа, приоритет между ними) —
[LED_PATTERNS.md](LED_PATTERNS.md). Кратко:
| Состояние | Паттерн |
| ---------------------- | ------------------------------------------------ |
| Загрузчик жив, ждёт 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` | Счётчик обнулён, зависший слот пробуется заново |

View file

@ -84,9 +84,9 @@ typedef struct {
DCD содержит последовательность команд двух типов:
| Команда | Тег | Назначение |
|---|---|---|
| `Write Data` | `0xCC` | Записать значение по адресу |
| Команда | Тег | Назначение |
| ---------------- | ------ | -------------------------------- |
| `Write Data` | `0xCC` | Записать значение по адресу |
| `Check Bits Set` | `0xCF` | Ждать пока бит станет `1` (poll) |
Формат бинарника: `Tag(1) | Length(2 BE) | Parameter(1) | данные`.
@ -105,11 +105,11 @@ DCD содержит последовательность команд двух
### 4.1 По типу образа
| Режим | `flags` | CSF | Применение |
|---|---|---|---|
| Unsigned | `0x00` | отсутствует | разработка, отладка |
| Signed | `0x08` | RSA/ECDSA подпись | производство |
| Signed + Encrypted | `0x0C` | подпись + шифрование кода | максимальная защита |
| Режим | `flags` | CSF | Применение |
| ------------------ | ------- | ------------------------- | ------------------- |
| Unsigned | `0x00` | отсутствует | разработка, отладка |
| Signed | `0x08` | RSA/ECDSA подпись | производство |
| Signed + Encrypted | `0x0C` | подпись + шифрование кода | максимальная защита |
### 4.2 По состоянию чипа (OTP fuse)
@ -150,6 +150,42 @@ BootROM проверяет подпись, но **игнорирует ошиб
→ прыгает на entry point
```
### 5.1 Как это выглядит в конфиге nxpimage (`sections:` в hab_*.yaml)
Схема выше — идеальная production-картина с SRK-таблицей на 4 ключа. На практике `nxpimage hab export`
собирает CSF из списка команд в `sections:` конфига — прямой аналог CSF-файла из NXP CST (Code Signing
Tool, см. AN12263 в §10), только в YAML вместо самодельного текстового формата CST.
В `firmware/bootloader` сейчас (тестовый ключ, до SRK-церемонии — см. §8) используется упрощённая
схема — **HAB4 NOCAK** («No CA Key», fast authentication): один ключ вместо иерархии SRK→CSFK/IMG.
Пример — [tools/host/hab/hab_bootloader_release.yaml](../../tools/host/hab/hab_bootloader_release.yaml):
```yaml
sections:
- Header: {...} # версия HAB, hash-алгоритм, формат сертификата/подписи — заголовок CSF
- InstallNOCAK: {...} # ставит ОДИН сертификат в слот 0 key store вместо SRK-таблицы
- AuthenticateCSF: {...} # подписывает сам CSF-контейнер (иначе BootROM не станет читать остальные команды)
- AuthenticateData: {...} # подписывает содержимое образа (IVT+BDT+код) — это и проверяет BootROM перед прыжком
```
| Команда | Что делает | Ключевые поля |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `Header` | Обязательна, идёт первой. Задаёт версию HAB (`4.2` для RT105x), алгоритм хэша (`sha256`), формат сертификата (`x509`) и формат подписи (`CMS`) — умолчания для всех следующих команд. | — |
| `InstallNOCAK` | Устанавливает публичный сертификат (`InstallNOCAK_File`) в слот 0 внутреннего key store HAB. В полной production-схеме (диаграмма выше) это делают `InstallSRK`+`InstallCSFK` — раздельные ключи с возможностью ревокации по отдельности. NOCAK — сознательное упрощение, годится только с тестовым ключом. | `InstallNOCAK_File` — путь к `.pem`-сертификату |
| `AuthenticateCSF` | Подписывает сам CSF-контейнер закрытым ключом — иначе ROM не станет доверять остальным командам после этой точки. Подпись создаётся на хосте во время сборки (`nxpimage`), не на чипе. | `Signer` — путь к приватному ключу |
| `AuthenticateData` | Подписывает реальные данные образа (IVT, BDT, код) — то, что BootROM хэширует и сверяет с подписью перед прыжком на `entry point`. `AuthenticateData_VerificationIndex` должен совпадать со слотом установленного ключа (`0` для NOCAK). | `AuthenticateData_VerificationIndex`, `Signer` |
Ключи для тестовой подписи — [tools/host/hab/keys/](../../tools/host/hab/keys/) (пояснение там же в
README). **Для реальной production-подписи** `flags=0x08` остаётся, но `InstallNOCAK` меняется на
полную `InstallSRK`+`InstallCSFK` (+`InstallKey` под отдельный IMG-ключ) цепочку с настоящими SRK
table/fuse-файлами, сгенерированными в рамках SRK-церемонии (§8) — сама YAML-механика (`nxpimage hab
export`, `flags`, `AuthenticateData`) не меняется, меняются только сертификаты и добавляются команды
установки промежуточных ключей.
Проверить, что CSF реально встроился в готовый образ: `just build::hab-verify <project> <debug|release>`
(обёртка над `nxpimage hab parse`) — для подписанного образа поле `csf` в IVT ненулевое и рядом
появляется отдельный `csf.bin`; для unsigned (`flags=0x00`) оба отсутствуют.
---
## 6. Почему Unsigned-образ требует HAB-контейнер
@ -170,7 +206,7 @@ BootROM проверяет подпись, но **игнорирует ошиб
BootROM в режиме SDP умеет только писать в RAM и прыгать. Для записи во Flash необходим **Flashloader** — специальная программа от NXP.
```
```bash
Плата в SDP режиме (BOOT_MOD_1 = 3V3)
│ sdphost -u 0x1FC9,0x0130
@ -192,12 +228,12 @@ BootROM в режиме SDP умеет только писать в RAM и пр
## 8. Жизненный цикл для проекта TFT
| Стадия | Режим HAB | Подпись | Fuse |
|---|---|---|---|
| Разработка | Open | Unsigned (`0x00`) | не трогаем |
| Входной контроль | Open | Unsigned (`0x00`) | не трогаем |
| Предсерийные образцы | Open | Signed (`0x08`) | не трогаем |
| Серийное производство | **Closed** | Signed (`0x08`) | `SEC_CONFIG = 1` |
| Стадия | Режим HAB | Подпись | Fuse |
| --------------------- | ---------- | ----------------- | ---------------- |
| Разработка | Open | Unsigned (`0x00`) | не трогаем |
| Входной контроль | Open | Unsigned (`0x00`) | не трогаем |
| Предсерийные образцы | Open | Signed (`0x08`) | не трогаем |
| Серийное производство | **Closed** | Signed (`0x08`) | `SEC_CONFIG = 1` |
> ⚠️ Запись `SEC_CONFIG = 1` необратима. Перед закрытием HAB необходимо убедиться, что подписанный образ успешно проходит верификацию на реальном железе.
@ -205,13 +241,13 @@ BootROM в режиме SDP умеет только писать в RAM и пр
## 9. Инструменты
| Инструмент | Назначение |
|---|---|
| `nxpimage hab export` | Сборка HAB-образа (IVT + BDT + DCD + опционально CSF) |
| `nxpimage hab parse` | Разбор готового образа для проверки |
| `sdphost` | Связь с BootROM в SDP режиме (запись в RAM, прыжок) |
| `blhost` | Связь с Flashloader (запись во Flash, конфигурация памяти) |
| `nxpdevscan` | Обнаружение подключённых NXP устройств |
| Инструмент | Назначение |
| --------------------- | ---------------------------------------------------------- |
| `nxpimage hab export` | Сборка HAB-образа (IVT + BDT + DCD + опционально CSF) |
| `nxpimage hab parse` | Разбор готового образа для проверки |
| `sdphost` | Связь с BootROM в SDP режиме (запись в RAM, прыжок) |
| `blhost` | Связь с Flashloader (запись во Flash, конфигурация памяти) |
| `nxpdevscan` | Обнаружение подключённых NXP устройств |
---

View file

@ -0,0 +1,11 @@
# LED-индикация bootloader
Два светодиода: **HEARTBEAT** (системный) и **APP** (прикладной).
| Индикация | Значение |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| HEARTBEAT мигает: 50 мс горит / 450 мс не горит. APP не горит вообще. | Загрузчик работает, ждёт карту microSD |
| HEARTBEAT мигает: 50 мс горит / 450 мс не горит. APP мигает: 250 мс горит / 250 мс не горит. | Идёт установка образа с SD-карты — питание не выключать |
| HEARTBEAT мигает: 50 мс горит / 450 мс не горит. APP часто мигает без остановки: 100 мс горит / 100 мс не горит. | Неисправность платы (QSPI flash или SDRAM) — плата не годна, отложить |
| APP быстро мигает 4 раза подряд (80 мс горит / 80 мс не горит), затем возвращается к тому виду, что был до этого. | Образ на SD-карте отклонён (битый файл или неверная подпись) — заменить файл на карте |
| Оба светодиода горят и гаснут одновременно: 100 мс горят / 100 мс не горят. | Режим восстановления (Recovery Mode) |

View file

@ -0,0 +1,138 @@
# UPDATE_FLOW.md — производственная заливка и полевое обновление tft_app
Разбор «что и куда заливать» для связки **bootloader → tft_app** (MCUboot Direct-XIP, два слота A/Б).
Дополняет [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md) (адреса) и
[../../firmware/bootloader/PLAN.md](../../firmware/bootloader/PLAN.md) (фазы, recovery).
---
## TL;DR (главное, что вызывает непонимание)
**Direct-XIP: образ исполняется прямо из адреса своего слота, код обычно НЕ позиционно-независим →
под каждый слот нужна СВОЯ линковка.** Обновление, которое встанет в Slot Б, должно быть слинковано
под адрес Slot Б (`0x60240000`). Поэтому:
> **Релиз одной версии tft_app = ДВА подписанных бинаря** (линковка под A + линковка под Б), из
> ОДНОГО исходника, параметризованным линкер-скриптом — ровно как уже устроен `test_stub`
> (`--defsym=__slot_base__=…`). Версия (`-v X.Y.Z`) одинаковая в обоих.
Да, «собрать новую версию со своим линкер-скриптом под слот Б» — так и есть. Но не вручную по одному:
это один параметризованный `.ld` и один рецепт сборки, дающий оба бинаря.
---
## 1. Почему так — это свойство Direct-XIP, не наша прихоть
- **XIP** = eXecute In Place: CPU фетчит инструкции напрямую из флеша по адресу слота, образ никуда не
копируется (в отличие от swap/scratch-режимов MCUboot, которые мы сознательно НЕ используем).
- Абсолютные адреса — vector table, указатели на функции, литеральные пулы, адрес инициализации
`.data` — фиксируются на этапе **линковки** под конкретный базовый адрес. Образ, слинкованный под
Slot A (`0x60040000`), в Slot Б (`0x60240000`) поедет по чужим адресам и не запустится корректно.
- Это прямо зафиксированный «открытый вопрос §4» в [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md):
*«код обычно не позиционно-независим — вероятно потребуется два варианта линковки под Slot A и
Slot Б, либо PIC»*. `test_stub` (Фаза 2) уже подтвердил two-slot-two-linkage на реальном железе.
> ⚠️ **Почему на `test_stub` «одинаковый» бинарь как будто работал в обоих слотах.** Заглушка
> крошечная (только мигание LED): почти весь её код PC-relative, `.data` минимальна, а vector table
> релоцируется и `boot_select` (ставит `VTOR`), и самим образом в старте — поэтому она позиционно
> **терпима** по случайности. Реальный tft_app (большой, с `.data`, абсолютными указателями,
> шрифтами-C-массивами, framebuffer) терпимым **не будет** — ему нужны обе линковки по-настоящему.
---
## 2. Карта слотов (напоминание)
| Слот | База | Размер | ORIGIN образа (после imgtool-заголовка `0x200`) |
|---|---|---|---|
| Slot A | `0x60040000` | 2 МБ | `0x60040200` |
| Slot Б | `0x60240000` | 2 МБ | `0x60240200` |
Линкер tft_app: `ORIGIN = __slot_base__ + 0x200` (образец — `cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld`
у `test_stub`).
---
## 3. Релизные артефакты (на каждую версию)
Из одного исходника tft_app — **два подписанных бинаря** production-ключом:
| Файл | Линковка (`__slot_base__`) | Назначение |
|---|---|---|
| `tft_app_slotA_vX.Y.Z.signed.bin` | `0x60040000` | ставится в Slot A |
| `tft_app_slotB_vX.Y.Z.signed.bin` | `0x60240000` | ставится в Slot Б |
- Оба**одна версия** `-v X.Y.Z` (version-gate сравнивает версию образа, а не его линковку).
- Оба подписаны production-ключом (не тестовым sample-ключом MCUboot; см. HAB SRK-церемонию как
аналог для bootloader).
- Рецепт — по образцу `just build::build-mcuboot-stub` (собирает оба слота + подписывает imgtool'ом),
но с production-ключом и реальным tft_app вместо заглушки.
---
## 4. Кейсы
| # | Ситуация | Куда/что | Как |
|---|---|---|---|
| 1 | **Начальная production-заливка** | A-линковка → Slot A; Slot Б пуст | SWD / USB ROM (blhost), `just host::flash-production`-путь |
| 2 | **Первое полевое обновление** v1→v2 | A активен → target = Slot Б → ставится **Б-линковка** v2 | SD, авто по version-gate; после — A=v1 (fallback), Б=v2 (active) |
| 3 | **Следующее обновление** v2→v3 | Б активен → target = Slot A → ставится **A-линковка** v3 | SD; после — A=v3 (active), Б=v2 (fallback) |
| 4 | **Даунгрейд** (BTN_1 при старте) | старая версия в inactive + стирание прежнего активного | SD + удержание BTN_1 (см. Фаза 3) |
| 5 | **Recovery** (BTN_2 / class C) | оба слота стёрты → чистый борт → A-линковка в Slot A | SD, ослабленный version-gate (см. Фаза 6) |
| 6 | **Образ завис после обновления** | авто-откат (class A) или счётчик+фолбэк (class B) | см. Фаза 6, taxonomy A/B |
**Ключевой инвариант alternation A/Б:** обновление всегда идёт в НЕактивный слот, прежний рабочий слот
остаётся нетронутым как fallback (см. `update_policy` — «целевой слот всегда НЕ активный»). Поэтому в
поле почти всегда есть куда откатиться, если новая версия окажется плохой.
---
## 5. Как обновление выбирает нужную линковку — ТЕКУЩИЙ GAP
**Сейчас** ([sd_update.c](../../firmware/bootloader/src/sd_update.c)): ищется ОДИН файл
`2:/TFT_APP.BIN` и ставится в неактивный слот (`decision.target_slot`). Для позиционно-терпимого
`test_stub` это прошло все 5 сценариев Фазы 3 — но для реального tft_app **сломается**: если активен
Slot A, обновление идёт в Slot Б, а `TFT_APP.BIN` мог быть слинкован под A → в Slot Б не запустится.
**Нужное расширение (часть включения реального tft_app):**
- SD несёт **оба** линкованных бинаря по соглашению имён, напр. `TFT_APP_A.BIN` / `TFT_APP_B.BIN`.
- `sd_update` открывает файл **по целевому слоту**: `TFT_APP_A.BIN`, если target = Slot A, иначе
`TFT_APP_B.BIN`.
- Оператор кладёт на SD **оба** и не думает, какой слот сейчас активен — bootloader сам берёт нужный
под инактивный слот.
- Version-gate: bootloader читает версию из выбранного файла (в обоих одна) — сравнение корректно.
Это простое изменение (одна развилка имени файла по `target_slot`), но его **надо сделать до первого
реального полевого обновления tft_app**. Пока стоит `test_stub` — не мешает.
---
## 6. Альтернатива — PIC (почему не сейчас)
Позиционно-независимый образ (один бинарь на оба слота) — теоретически убирает дублирование линковки.
Но на bare-metal Cortex-M это **ROPI/RWPI**: флаги компилятора, PI-совместимый startup, `r9` как
static base для RW-данных, и **все** библиотеки (FreeRTOS, SDK-драйверы, шрифты) собранные в PI-режиме,
плюс runtime-оверхед. Объём работы и риск большие; для Direct-XIP индустрия стандартно выбирает
dual-link. Оставляем PIC на «если поддержка двух сборок станет реальной обузой» — не сейчас.
---
## 7. Конкретные TODO для включения реального tft_app (сводка)
1. **Параметризованный `.ld`** tft_app под два адреса слота (образец — `test_stub`
`MIMXRT1052xxxxx_mcuboot_slot.ld` + `--defsym=__slot_base__=`).
2. **Рецепт сборки+подписи** двух бинарей на релиз (по образцу `build-mcuboot-stub`, но production-ключ).
3. **Расширить `sd_update`**: `TFT_APP_<A|B>.BIN` по `decision.target_slot` (§5).
4. **Контракт tft_app** (Фаза 6, 6c): обслуживание WDOG (alive-flag), отложенный `boot_set_confirmed()`,
`bsp_boot_health_mark()`.
5. **Решить**: production заливать только Slot A или A+Б? Достаточно A — первое обновление наполнит Б
и даст fallback *другой* версии (A+Б одной версией от class-B не спасает — тот же баг в обоих).
---
## Связанные документы
- [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md) — адреса слотов, обоснование размеров.
- [../../firmware/bootloader/PLAN.md](../../firmware/bootloader/PLAN.md) — фазы; Фаза 6 (recovery,
taxonomy зависаний), Фаза 5 (HAB/production/service-tui).
- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись самого bootloader (HAB), не путать с imgtool-подписью образов
tft_app.

View file

@ -0,0 +1,111 @@
# firmware/bootloader/CMakeLists.txt Загрузчик — A/Б обновление tft_app через
# microSD (Direct-XIP). Обновляется только через USB ROM + blhost / SWD.
cmake_minimum_required(VERSION 3.20)
project(
bootloader
VERSION 1.0.0
LANGUAGES C ASM)
set(TARGET_NAME bootloader)
# Генерация version.h из шаблона
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/generated/version.h" @ONLY)
# bootutil (MCUboot Direct-XIP) + TinyCrypt + ASN.1 — общий список с
# host-тестами (tests/host/mcuboot_port/), см. firmware/bootloader/PLAN.md, Фаза
# 2.
include(${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/bootutil_sources.cmake)
# bootloader_fatfs — bare-metal FatFS для чтения TFT_APP.BIN с SD (Фаза 3).
add_subdirectory(fatfs)
add_executable(
${TARGET_NAME}
src/main.c
src/cli.c
src/protocol.c
src/boot_select.c
src/update_policy.c
src/slot_version.c
src/sd_update.c
src/recovery.c
src/led_status.c
mcuboot_port/flash_map_backend.c
mcuboot_port/keys.c
${MCUBOOT_BOOTUTIL_SOURCES}
${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE}
${BSP_SYSCALLS_FILE})
target_include_directories(${TARGET_NAME} PRIVATE src/
${MCUBOOT_BOOTUTIL_INCLUDES})
target_include_directories(${TARGET_NAME}
PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated")
target_compile_definitions(
${TARGET_NAME} PRIVATE __STARTUP_INITIALIZE_RAMFUNCTION __STARTUP_CLEAR_BSS
__STARTUP_INITIALIZE_NONCACHEDATA)
# -----------------------------------------------------------------------------
# Зависимости. bsp_button — downgrade-override (удержание BSP_BUTTON_1).
# bootloader_fatfs — чтение TFT_APP.BIN с SD (Фаза 3,
# firmware/bootloader/fatfs/). bsp_sdram — smoke-test SDRAM/SEMC (Фаза 4,
# bsp_sdram_configure()+_init()) — bootloader без DCD, сам поднимает SEMC на
# время диагностики.
# -----------------------------------------------------------------------------
target_link_libraries(
${TARGET_NAME}
PRIVATE bsp_board
bsp_led
bsp_tick
bsp_usb_cdc
bsp_qspi_flash
bsp_boot_xip_no_dcd
bsp_button
bsp_wdog
bsp_boot_state
bsp_sdram
bootloader_fatfs)
# -----------------------------------------------------------------------------
# [DEV-ONLY] Диагностика Фазы 4 — CLI-команда "sdram_test" (dev_sdram_test.c):
# 4-фазный тест SDRAM (адресная шина/шина данных/sequential/retention),
# портирован из firmware_test/src/tests/test_sdram.c, прогоняется через
# bsp_sdram_configure() вместо DCD. НЕ для production: только Debug —
# Release/HAB-бинарь этот файл не содержит и о команде не знает.
# -----------------------------------------------------------------------------
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_sources(${TARGET_NAME} PRIVATE src/dev_sdram_test.c)
target_compile_definitions(${TARGET_NAME} PRIVATE BOOTLOADER_DEV_DIAGNOSTICS)
endif()
# -----------------------------------------------------------------------------
# Linker script — вариант flexspi_nor с m_text, ограниченным бюджетом bootloader
# (256 KB, см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md). Без SDRAM (в отличие от
# firmware_test) — bootloader SDRAM не использует.
# -----------------------------------------------------------------------------
target_link_options(
${TARGET_NAME}
PRIVATE
-Wl,--gc-sections
-Wl,--print-memory-usage
-Wl,-Map=${CMAKE_BINARY_DIR}/bootloader.map
-Wl,--defsym=__stack_size__=0x1000
-Wl,--defsym=__heap_size__=0x1000
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld)
set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR})
# -----------------------------------------------------------------------------
# Post-build: генерация .bin для прошивки через blhost
# -----------------------------------------------------------------------------
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${TARGET_NAME}>
${CMAKE_BINARY_DIR}/bootloader.bin
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${TARGET_NAME}>
COMMENT "Generating bootloader.bin")

View file

@ -0,0 +1,168 @@
# bootloader
Загрузчик MIMXRT1052: выбирает и запускает приложение `tft_app` из одного из двух слотов
(MCUboot, Direct-XIP), обновляет его с microSD, восстанавливает плату при зависании образа. Сам
загрузчик прошивается только по USB ROM (blhost) или SWD — в поле не обновляется. Канал диагностики —
USB CDC ACM (JSON-строки).
**Принцип работы** (карта памяти, выбор образа, обновление, версии, даунгрейд, recovery) —
[BOOT_FLOW.md](../../docs/bootloader/BOOT_FLOW.md). LED-индикация — [LED_PATTERNS.md](../../docs/bootloader/LED_PATTERNS.md).
---
## Быстрый старт
### Сборка
```bash
just build::build-bootloader-debug # bootloader.elf/.bin
just build::hab-bootloader-debug # HAB-контейнер bootloader_hab.bin
```
### Прошивка
SWD (для итеративной разработки, не требует смены boot-режима платы):
```bash
just host::flash-swd-bootloader-debug
# после прошивки обязателен power cycle платы
```
USB ROM (SDP, плата в режиме Serial Downloader):
```bash
just host::flash bootloader debug
```
### Подключение
```bash
screen /dev/cu.usbmodemXXXX # macOS; порт свой на каждое подключение
```
```json
→ {"type":"cmd","cmd":"ping"}
← {"type":"pong"}
→ {"type":"cmd","cmd":"get_version"}
← {"type":"version_response","fw":"0.1.0"}
```
USB поднимается на каждой загрузке до обращения к SD, поэтому статусы видны, даже если подключиться
заранее.
### Отладка
VSCode → `🐛 Debug: bootloader` — пересобирает, подключается к GDB-серверу
(`just host::debug-server` должен быть запущен), останавливается на `main`. Под отладчиком аппаратный
watchdog приостановлен, пошаговая отладка сбросами не сбивается.
---
## Архитектура
Загрузчик **не зависит от SDRAM** для своей работы (XIP только из W25Q, без DCD) и без дисплея/RTOS:
инициализация, доступ к QSPI-flash, чтение FatFS с SD, проверка и выбор образа, прыжок. SEMC/SDRAM
трогаются только диагностически (`bsp_sdram_configure()`, boot-time smoke-test) — реально их поднимает
для себя уже само приложение в своём раннем startup.
Линкер жёстко ограничивает код бюджетом области загрузчика (256 КБ) с `ASSERT` на границу Slot A —
превышение становится ошибкой сборки, а не тихим заездом в чужую область.
```text
firmware/bootloader/
├── src/
│ ├── main.c — точка входа: инициализация → одна попытка загрузки
│ │ (SD-скан + recovery-гейт + прыжок) → цикл ожидания
│ ├── boot_select.* — выбор валидного слота и прыжок в выбранный образ
│ ├── slot_version.* — read-only проверка и чтение версии слота (без побочных
│ │ эффектов на flash)
│ ├── update_policy.* — чистая логика «ставить/пропустить» + целевой слот
│ ├── recovery.* — чистая логика решения recovery (порог / фолбэк / режим)
│ ├── sd_update.* — оркестрация: смонтировать SD, найти TFT_APP.BIN,
│ │ установить в целевой слот с потоковой 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:
│ flash_area_* на 2 слота, конфиг, публичный ключ ECDSA-P256
├── fatfs/ — read-only FatFS для чтения TFT_APP.BIN с карты
└── test_stub/ — самостоятельный подписанный образ-заглушка вместо
tft_app для аппаратной проверки загрузчика
```
`update_policy` и `recovery` — чистые функции без доступа к железу, целиком покрыты host-тестами.
`boot_select`, `sd_update`, `flash_map_backend` — тонкий аппаратный слой поверх них.
---
## Протокол
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":"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"}` <br> **[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` | плата в режиме восстановления |
| `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`); `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).
---
## Тесты
**Host** — вся логика без железа (выбор слота, сравнение версий, политика установки, решение
recovery, протокол, CLI) на in-memory flash-фейках:
```bash
just build::test-host
```
**Аппаратный стенд** — сборка и подпись образов-заглушек, замещающих `tft_app` при ручной проверке
на плате (здоровые образы + варианты с зависанием на разных стадиях для проверки восстановления):
```bash
just build::build-mcuboot-stub
```
Чек-листы ручной проверки лежат рядом со стендом в `test_stub/`.
---
## Версионирование
Версия задаётся `project(bootloader VERSION X.Y.Z)` в `CMakeLists.txt` и прокидывается через
`configure_file(src/version.h.in → generated/version.h)` в строку, которую возвращает
`get_version`. `version.h` генерируется, вручную не редактируется.

View file

@ -0,0 +1,35 @@
# bootloader_fatfs — FatFS, скомпилированный с bare-metal ffconf.h для
# bootloader.
#
# ff.c, fsl_sd_disk.c, diskio_sd.c включают ff.h → ffconf.h. Все три
# компилируются здесь, чтобы видели один и тот же ffconf.h из
# ${CMAKE_CURRENT_SOURCE_DIR}/include.
#
# Не шарить с firmware_test_fatfs: bootloader и firmware_test —
# взаимоисключающие прошивки одной платы (см. firmware/bootloader/PLAN.md,
# Фаза 3) — зависимость от таргета с именем "firmware_test" была бы неверной
# связью. tft_app заведёт свой аналогичный таргет с FreeRTOS ffconf.h.
#
# Отличие от firmware_test_fatfs: FF_FS_READONLY=1 — bootloader только читает
# TFT_APP.BIN, никогда не пишет на SD (см. include/ffconf.h).
add_library(
bootloader_fatfs STATIC
${SDK_FATFS_FF_SRC} # sdk/middleware/fatfs/source/ff.c
${SDK_FATFS_SD_DISK_SRC} # sdk/middleware/fatfs/source/fsl_sd_disk/fsl_sd_disk.c
${PORT_FATFS_SD_SRC} # port/fatfs/sd/src/diskio_sd.c
src/diskio.c)
# include/ первым — ffconf.h отсюда должен перекрыть любой шаблонный
target_include_directories(
bootloader_fatfs
PUBLIC include # ffconf.h, виден потребителям (sd_update.c)
PRIVATE src)
target_link_libraries(
bootloader_fatfs
PUBLIC port_fatfs_sd # diskio_sd.h, ff.h, fsl_sd_disk.h, bsp_sd
PRIVATE bsp_sdmmc_config) # SD_ENABLED транзитивно из bsp_sd, но явно для
# ясности
target_compile_options(bootloader_fatfs PRIVATE -w)

View file

@ -0,0 +1,86 @@
/*
* ffconf.h конфигурация FatFS для bootloader (bare-metal, только SD).
*
* Копия паттерна firmware/test/fatfs/include/ffconf.h не шарить: bootloader
* и firmware_test взаимоисключающие прошивки одной платы (см.
* firmware/bootloader/PLAN.md, Фаза 3). tft_app заведёт свой аналогичный
* таргет с FreeRTOS ffconf.h.
*
* Ключевое отличие от firmware_test:
* FF_FS_READONLY = 1 bootloader только читает TFT_APP.BIN с SD, никогда
* не пишет на карту; убирает f_write и весь путь
* записи FatFS из сборки.
* FF_FS_REENTRANT = 0 нет RTOS, нет мьютексов
* FF_VOLUMES = 3 0: зарезервирован, 1: зарезервирован, 2: SD
* FF_MAX_SS = 512 SD всегда 512 байт/сектор, ioctl не нужен
*/
#ifndef _FFCONF_H_
#define _FFCONF_H_
#define FFCONF_DEF 80286
/*---------------------------------------------------------------------------/
/ MSDK adaptation
/---------------------------------------------------------------------------*/
#define SD_DISK_ENABLE 1
/*---------------------------------------------------------------------------/
/ Function Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_READONLY 1 /* bootloader никогда не пишет на SD */
#define FF_FS_MINIMIZE 0
#define FF_USE_FIND 0
#define FF_USE_MKFS 0 /* f_mkfs не нужна — карта уже отформатирована */
#define FF_USE_FASTSEEK 0
#define FF_USE_EXPAND 0
#define FF_USE_CHMOD 0
#define FF_USE_LABEL 0
#define FF_USE_FORWARD 0
#define FF_USE_STRFUNC 0
#define FF_PRINT_LLI 0
#define FF_PRINT_FLOAT 0
#define FF_STRF_ENCODE 3
/*---------------------------------------------------------------------------/
/ Locale
/---------------------------------------------------------------------------*/
#define FF_CODE_PAGE 437 /* U.S. — минимальный, имена файлов ASCII */
#define FF_USE_LFN 0 /* только 8.3 — достаточно для TFT_APP.BIN */
#define FF_MAX_LFN 255
#define FF_LFN_UNICODE 0
#define FF_LFN_BUF 255
#define FF_SFN_BUF 12
#define FF_FS_RPATH 0 /* относительные пути не нужны */
/*---------------------------------------------------------------------------/
/ Drive/Volume Configurations
/---------------------------------------------------------------------------*/
#define FF_VOLUMES 3 /* 0: зарезервирован, 1: зарезервирован, 2: SD */
#define FF_STR_VOLUME_ID 0
#define FF_MULTI_PARTITION 0
#define FF_MIN_SS 512
#define FF_MAX_SS 512 /* SD: всегда 512, GET_SECTOR_SIZE не нужен */
#define FF_LBA64 0
#define FF_MIN_GPT 0x10000000
#define FF_USE_TRIM 0
/*---------------------------------------------------------------------------/
/ System Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_TINY 0
#define FF_FS_EXFAT 0 /* exFAT требует LFN — оба отключены */
#define FF_FS_NORTC 1
#define FF_NORTC_MON 1
#define FF_NORTC_MDAY 1
#define FF_NORTC_YEAR 2024
#define FF_FS_NOFSINFO 0
#define FF_FS_LOCK 0
#define FF_FS_REENTRANT 0 /* bare-metal: нет RTOS, нет мьютексов */
/* FF_FS_TIMEOUT и FF_SYNC_t не нужны */
#endif /* _FFCONF_H_ */

View file

@ -0,0 +1,53 @@
/*
* diskio.c FatFS diskio диспетчер для bootloader.
*
* FF_VOLUMES=3: диски 0 и 1 заглушки, диск 2 = SDDISK (microSD).
* W25Q и RAM-диск отсутствуют нет зависимости на bsp_qspi_flash (слоты A/Б
* читаются/пишутся напрямую через flash_area_*, в обход FatFS).
*/
#include "diskio.h"
#include "port/fatfs/diskio_sd.h"
#define SDDISK 2U
DSTATUS disk_initialize(BYTE pdrv)
{
if (pdrv == SDDISK)
{
return microsd_disk_initialize(pdrv);
}
return STA_NOINIT;
}
DSTATUS disk_status(BYTE pdrv)
{
if (pdrv == SDDISK)
{
return microsd_disk_status(pdrv);
}
return STA_NOINIT;
}
DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
{
if (pdrv == SDDISK)
{
return microsd_disk_read(pdrv, buff, sector, count);
}
return RES_PARERR;
}
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff)
{
if (pdrv == SDDISK)
{
return microsd_disk_ioctl(pdrv, cmd, buff);
}
return RES_PARERR;
}

View file

@ -0,0 +1,43 @@
# mcuboot_port/bootutil_sources.cmake
#
# Общий список файлов bootutil (MCUboot) + TinyCrypt + ASN.1-парсер,
# используемых и реальным ARM-таргетом (firmware/bootloader), и host-тестами
# (tests/host/mcuboot_port/)
set(MCUBOOT_OPENSOURCE_DIR
${CMAKE_SOURCE_DIR}/sdk/middleware/mcuboot_opensource)
set(MCUBOOT_BOOTUTIL_DIR ${MCUBOOT_OPENSOURCE_DIR}/boot/bootutil)
set(MCUBOOT_EXT_DIR ${MCUBOOT_OPENSOURCE_DIR}/ext)
set(MCUBOOT_BOOTUTIL_SOURCES
${MCUBOOT_BOOTUTIL_DIR}/src/loader.c
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_misc.c
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c
${MCUBOOT_BOOTUTIL_DIR}/src/tlv.c
${MCUBOOT_BOOTUTIL_DIR}/src/image_validate.c
${MCUBOOT_BOOTUTIL_DIR}/src/image_ecdsa.c
${MCUBOOT_BOOTUTIL_DIR}/src/fault_injection_hardening.c
${MCUBOOT_BOOTUTIL_DIR}/src/swap_scratch.c
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/ecc.c
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/ecc_dsa.c
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/sha256.c
${MCUBOOT_EXT_DIR}/tinycrypt/lib/source/utils.c
${MCUBOOT_EXT_DIR}/mbedtls-asn1/src/asn1parse.c
${MCUBOOT_EXT_DIR}/mbedtls-asn1/src/platform_util.c)
set(MCUBOOT_BOOTUTIL_INCLUDES
${MCUBOOT_BOOTUTIL_DIR}/include
${MCUBOOT_BOOTUTIL_DIR}/src
${MCUBOOT_EXT_DIR}/tinycrypt/lib/include
${MCUBOOT_EXT_DIR}/mbedtls-asn1/include
${CMAKE_CURRENT_LIST_DIR} # sysflash.h, mcuboot_config.h, flash_map.h,
# flash_map_backend.h
)
set(MCUBOOT_VENDORED_COMPILE_OPTIONS -w)
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
list(APPEND MCUBOOT_VENDORED_COMPILE_OPTIONS -fno-sanitize=address,undefined)
endif()
set_source_files_properties(
${MCUBOOT_BOOTUTIL_SOURCES} PROPERTIES COMPILE_OPTIONS
"${MCUBOOT_VENDORED_COMPILE_OPTIONS}")

View file

@ -0,0 +1,62 @@
/**
* @file flash_map.h
* @brief Контракт bootutil на "область флеша" реализуется
* flash_map_backend.c (реальный, над bsp_qspi_flash) или
* fake_flash_map_backend.c (host-тесты, in-memory буфер).
*
*/
#ifndef FLASH_MAP_H_
#define FLASH_MAP_H_
#include <stdint.h>
/**
* @brief Область на flash-устройстве.
*
* Несколько устройств в системе не предполагается (см. FLASH_DEVICE_ID в
* sysflash.h) fa_device_id всегда равен FLASH_DEVICE_ID.
*/
struct flash_area
{
uint8_t fa_id; /**< ID области, уникален в системе. */
uint8_t fa_device_id; /**< ID flash-устройства. */
uint16_t pad16;
uint32_t fa_off; /**< Смещение области от начала устройства. */
uint32_t fa_size; /**< Размер области, байт. */
};
/** @brief Сектор внутри области (смещение относительно начала области). */
struct flash_sector
{
uint32_t fs_off;
uint32_t fs_size;
};
/** @brief Базовый адрес flash-устройства в адресном пространстве MCU (XIP). */
int flash_device_base(uint8_t fd_id, uintptr_t *ret);
int flash_area_open(uint8_t id, const struct flash_area **area);
void flash_area_close(const struct flash_area *area);
/* Read/write/erase — смещение относительно начала области. */
int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len);
int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len);
int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len);
/** @brief Минимальное выравнивание записи. */
uint8_t flash_area_align(const struct flash_area *area);
/** @brief Значение стёртого байта (0xFF для NOR). */
uint8_t flash_area_erased_val(const struct flash_area *area);
/** @brief Прочитать len байт с off и проверить что это стёртая область. */
int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len);
int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors);
int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector);
int flash_area_id_from_image_slot(int slot);
int flash_area_id_to_multi_image_slot(int image_index, int area_id);
#endif /* FLASH_MAP_H_ */

View file

@ -0,0 +1,280 @@
/**
* @file flash_map_backend.c
* @brief Реализация flash_map.h поверх bsp_qspi_flash Slot A/Б из
* docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md.
*
* bsp_qspi_read/write_page/erase_sector() принимают flash-relative адрес
* (0-based от начала чипа, IPCR0 FlexSPI IP-команд) НЕ XIP-адрес
* (0x60000000+). fa_off здесь то же самое flash-relative смещение.
* flash_device_base() единственное место, где встречается XIP-адрес
* 0x60000000: он нужен boot_select.c для вычисления адреса прыжка
* (flash_base + fa_off + hdr_size), но не самим read/write/erase.
*
* @pre bsp_qspi_init() должен быть вызван до любой flash_area_* функции
* (main.c, до boot_go()).
*/
#include "bsp/qspi_flash.h"
#include "bsp/wdog.h"
#include "flash_map.h"
#include "led_status.h"
#include "sysflash/sysflash.h"
#include <string.h>
#define ERASED_VAL 0xFFU
/* Slot A/Б — см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md. Смещения —
* flash-relative (от начала чипа), не XIP-адрес. */
static const struct flash_area g_s_areas[2] = {
{ .fa_id = 0U,
.fa_device_id = FLASH_DEVICE_ID,
.pad16 = 0U,
.fa_off = 0x00040000UL,
.fa_size = 0x00200000UL }, /* Slot A: 0x60040000, 2 МБ */
{ .fa_id = 1U,
.fa_device_id = FLASH_DEVICE_ID,
.pad16 = 0U,
.fa_off = 0x00240000UL,
.fa_size = 0x00200000UL }, /* Slot Б: 0x60240000, 2 МБ */
};
/* ── Постраничная запись (аналог NXP flash_area_write_internal) ─────────
*
* bsp_qspi_write_page() пишет ровно BSP_QSPI_PAGE_SIZE (256) байт по
* странично-выровненному адресу. Запись 0xFF поверх уже запрограммированных
* байт не изменяет их (NOR program может только сбрасывать биты 10,
* запись 0xFF не запрашивает сброс ни одного бита) поэтому безопасно
* "перезатирать" уже записанную часть страницы буфером, где нетронутая
* часть заполнена ERASED_VAL: bootutil пишет монотонно возрастающими
* смещениями, повторно данные не перезаписывает.
*/
static int write_page_chunked(uint32_t dst_addr, const uint8_t *p_src, uint32_t len)
{
uint8_t page_buf[BSP_QSPI_PAGE_SIZE];
uint32_t chunk_ofs = dst_addr % BSP_QSPI_PAGE_SIZE;
uint32_t page_addr = dst_addr - chunk_ofs;
uint32_t chunk_size = BSP_QSPI_PAGE_SIZE - chunk_ofs;
while (len > 0U)
{
if (chunk_size > len)
{
chunk_size = len;
}
memset(page_buf, ERASED_VAL, BSP_QSPI_PAGE_SIZE);
memcpy(page_buf + chunk_ofs, p_src, chunk_size);
if (bsp_qspi_write_page(page_addr, page_buf) != BSP_OK)
{
return -1;
}
p_src += chunk_size;
len -= chunk_size;
chunk_ofs = 0U;
chunk_size = BSP_QSPI_PAGE_SIZE;
page_addr += BSP_QSPI_PAGE_SIZE;
}
return 0;
}
/* ── flash_map.h contract ─────────────────────────────────────────────── */
int flash_device_base(uint8_t fd_id, uintptr_t *ret)
{
if (fd_id != FLASH_DEVICE_ID)
{
return -1;
}
*ret = 0x60000000UL; /* XIP-mapped база — только для вычисления адреса прыжка */
return 0;
}
int flash_area_open(uint8_t id, const struct flash_area **area)
{
if (id >= 2U)
{
return -1;
}
*area = &g_s_areas[id];
return 0;
}
void flash_area_close(const struct flash_area *area)
{
(void) area;
}
int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
{
if (off + len > area->fa_size)
{
return -1;
}
return (bsp_qspi_read(area->fa_off + off, (uint8_t *) dst, len) == BSP_OK) ? 0 : -1;
}
int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len)
{
if (off + len > area->fa_size)
{
return -1;
}
return write_page_chunked(area->fa_off + off, (const uint8_t *) src, len);
}
int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len)
{
if ((off + len > area->fa_size) || ((off % BSP_QSPI_SECTOR_SIZE) != 0U) ||
((len % BSP_QSPI_SECTOR_SIZE) != 0U))
{
return -1;
}
/* Fast-path: стирание всей области целиком (off=0, len=fa_size), кратно
* 64 КБ блочное стирание ~5x быстрее посекторного (2 МБ: ~4.8 с против
* ~23 с, см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md, "Обоснование
* размеров"). Ускоряет и sd_update (Фаза 3), и штатный revert-erase
* bootutil (boot_select_or_erase() в loader.c) они уже зовут
* flash_area_erase(fap, 0, flash_area_get_size(fap)) без изменений.
* Частичное/невыровненное стирание (напр. один трейлер) прежний
* посекторный путь ниже. */
if ((off == 0U) && (len == area->fa_size) && ((len % BSP_QSPI_BLOCK_64K_SIZE) == 0U))
{
uint32_t block_addr = area->fa_off;
for (; len > 0U; len -= BSP_QSPI_BLOCK_64K_SIZE)
{
/* Кормим watchdog поблочно: стирание 2 МБ ~4.8 c — это реальный
* прогресс, но один блочный вызов не должен упереться в таймаут.
* Зависание самого стирания флеша всё равно ловится: refresh по
* ЗАВЕРШЕНИИ блока, а не перед ним. */
bsp_wdog_refresh();
/* Прогресс-хук индикации — 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)
{
return -1;
}
block_addr += BSP_QSPI_BLOCK_64K_SIZE;
}
return 0;
}
uint32_t addr = area->fa_off + off;
for (; len > 0U; len -= BSP_QSPI_SECTOR_SIZE)
{
bsp_wdog_refresh(); /* см. выше — посекторный путь тоже длинный */
if (bsp_qspi_erase_sector(addr) != BSP_OK)
{
return -1;
}
addr += BSP_QSPI_SECTOR_SIZE;
}
return 0;
}
uint8_t flash_area_align(const struct flash_area *area)
{
(void) area;
return 1U;
}
uint8_t flash_area_erased_val(const struct flash_area *area)
{
(void) area;
return ERASED_VAL;
}
int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
{
if (flash_area_read(area, off, dst, len) != 0)
{
return -1;
}
const uint8_t *p_buf = (const uint8_t *) dst;
for (uint32_t i = 0U; i < len; i++)
{
if (p_buf[i] != ERASED_VAL)
{
return 0;
}
}
return 1;
}
int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector)
{
if (off >= fa->fa_size)
{
return -1;
}
sector->fs_off = (off / BSP_QSPI_SECTOR_SIZE) * BSP_QSPI_SECTOR_SIZE;
sector->fs_size = BSP_QSPI_SECTOR_SIZE;
return 0;
}
int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors)
{
const struct flash_area *fa;
uint32_t max_cnt = *count;
if (flash_area_open((uint8_t) fa_id, &fa) != 0)
{
return -1;
}
uint32_t rem_len = fa->fa_size;
*count = 0U;
while ((rem_len > 0U) && (*count < max_cnt))
{
sectors[*count].fs_off = BSP_QSPI_SECTOR_SIZE * (*count);
sectors[*count].fs_size = BSP_QSPI_SECTOR_SIZE;
(*count)++;
rem_len -= BSP_QSPI_SECTOR_SIZE;
}
return 0;
}
int flash_area_id_from_multi_image_slot(int image_index, int slot)
{
switch (slot)
{
case 0:
return FLASH_AREA_IMAGE_PRIMARY(image_index);
case 1:
return FLASH_AREA_IMAGE_SECONDARY(image_index);
default:
return -1;
}
}
int flash_area_id_from_image_slot(int slot)
{
return flash_area_id_from_multi_image_slot(0, slot);
}
int flash_area_id_to_multi_image_slot(int image_index, int area_id)
{
if (area_id == FLASH_AREA_IMAGE_PRIMARY(image_index))
{
return 0;
}
if (area_id == FLASH_AREA_IMAGE_SECONDARY(image_index))
{
return 1;
}
return -1;
}

View file

@ -0,0 +1,41 @@
/**
* @file flash_map_backend.h
* @brief Инлайн-аксессоры flash_area/flash_sector, требуемые bootutil.
*/
#ifndef FLASH_MAP_BACKEND_H_
#define FLASH_MAP_BACKEND_H_
#include "flash_map.h"
static inline uint8_t flash_area_get_id(const struct flash_area *fa)
{
return fa->fa_id;
}
static inline uint8_t flash_area_get_device_id(const struct flash_area *fa)
{
return fa->fa_device_id;
}
static inline uint32_t flash_area_get_off(const struct flash_area *fa)
{
return fa->fa_off;
}
static inline uint32_t flash_area_get_size(const struct flash_area *fa)
{
return fa->fa_size;
}
static inline uint32_t flash_sector_get_off(const struct flash_sector *fs)
{
return fs->fs_off;
}
static inline uint32_t flash_sector_get_size(const struct flash_sector *fs)
{
return fs->fs_size;
}
#endif /* FLASH_MAP_BACKEND_H_ */

View file

@ -0,0 +1,27 @@
/**
* @file keys.c
* @brief Таблица публичных ключей bootutil.
*
* MCUBOOT_HW_KEY / MCUBOOT_BUILTIN_KEY не определены (см. mcuboot_config.h)
* bootutil использует стандартный путь: TLV образа несёт хэш ключа,
* bootutil ищет совпадение в bootutil_keys[] и проверяет подпись найденным
* ключом. По образцу sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/keys.c.
*/
#include <bootutil/sign_key.h>
#include <mcuboot_config/mcuboot_config.h>
#if defined(MCUBOOT_SIGN_EC256)
#include "keys/bootloader_test_ecdsa_pub.c"
#else
#error "No public key available for given signing algorithm."
#endif
const struct bootutil_key bootutil_keys[] = {
{
.key = ecdsa_pub_key,
.len = &ecdsa_pub_key_len,
},
};
const int bootutil_key_cnt = 1;

View file

@ -0,0 +1,25 @@
/**
* @file bootloader_test_ecdsa_pub.c
* @brief Публичный ключ ECDSA P-256 сгенерирован из тестового
* sample-ключа MCUboot (sdk/middleware/mcuboot_opensource/root-ec-p256.pem)
* командой `imgtool.py getpub --lang c`.
*
* ВНИМАНИЕ: это публичный, широко известный sample-ключ проекта MCUboot,
* НЕ производственный секрет. Используется для Фазы 2 (host-тесты + первая
* проверка на железе). Перед серийным производством должен быть заменён
* на реальный production-ключ (приватная часть вне репозитория, см.
* жизненный цикл ключей HAB в docs/mimxrt1052/HAB_GUIDE.md аналогичная
* процедура нужна для ключа подписи tft_app-образов).
*/
/* Autogenerated by imgtool.py, do not edit. */
const unsigned char ecdsa_pub_key[] = {
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06,
0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x2a,
0xcb, 0x40, 0x3c, 0xe8, 0xfe, 0xed, 0x5b, 0xa4, 0x49, 0x95, 0xa1, 0xa9, 0x1d, 0xae,
0xe8, 0xdb, 0xbe, 0x19, 0x37, 0xcd, 0x14, 0xfb, 0x2f, 0x24, 0x57, 0x37, 0xe5, 0x95,
0x39, 0x88, 0xd9, 0x94, 0xb9, 0xd6, 0x5a, 0xeb, 0xd7, 0xcd, 0xd5, 0x30, 0x8a, 0xd6,
0xfe, 0x48, 0xb2, 0x4a, 0x6a, 0x81, 0x0e, 0xe5, 0xf0, 0x7d, 0x8b, 0x68, 0x34, 0xcc,
0x3a, 0x6a, 0xfc, 0x53, 0x8e, 0xfa, 0xc1,
};
const unsigned int ecdsa_pub_key_len = 91;

View file

@ -0,0 +1,61 @@
/**
* @file mcuboot_config.h
* @brief Конфигурация bootutil (MCUboot) для загрузчика TFT.
*
* В отличие от шаблона NXP (nxp_mcux_sdk/include/mcuboot_config/mcuboot_config.h)
* задаёт финальные макросы напрямую, без Kconfig-подобной прослойки
* решения зафиксированы в firmware/bootloader/PLAN.md, Фаза 2:
*
* - Direct-XIP с revert: два слота, оба могут содержать валидный образ,
* bootutil выбирает более новую валидную версию; если она ни разу не
* подтверждена (confirm) следующая загрузка откатится на предыдущую
* (см. MCUBOOT_DIRECT_XIP_REVERT).
* - ECDSA P-256 + TinyCrypt компактный, полностью вендорен в репозитории
* (sdk/middleware/mcuboot_opensource/ext/tinycrypt), в отличие от
* mbedTLS (не вендорен, потребовал бы ~8000+ новых строк).
* - FIH профиль LOW часть защиты bootutil (double-read сравнений) без
* RNG-задержки (та требует mbedTLS-энтропию, доступно только в профиле
* HIGH не наш случай).
* - Heap НЕ нужен: malloc/free в bootutil (loader.c) вызываются только в
* swap-режиме, недостижимы под MCUBOOT_DIRECT_XIP.
*/
#ifndef MCUBOOT_CONFIG_H_
#define MCUBOOT_CONFIG_H_
/* ── Схема подписи ─────────────────────────────────────────────────────── */
#define MCUBOOT_SIGN_EC256
/* ── Крипто-бэкенд ─────────────────────────────────────────────────────── */
#define MCUBOOT_USE_TINYCRYPT
/* ── Режим обновления ─────────────────────────────────────────────────── */
#define MCUBOOT_DIRECT_XIP
#define MCUBOOT_DIRECT_XIP_REVERT
/* Проверять подпись активного слота при каждой загрузке, не только при
* установке нового образа. */
#define MCUBOOT_VALIDATE_PRIMARY_SLOT
/* ── Образы ────────────────────────────────────────────────────────────── */
#define MCUBOOT_IMAGE_NUMBER 1
/* ── Flash-абстракция ─────────────────────────────────────────────────── */
#define MCUBOOT_USE_FLASH_AREA_GET_SECTORS
/* Slot A/Б = 2 МБ (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md), сектор
* W25Qxx = 4 КБ 2 МБ / 4 КБ = 512 секторов на слот. */
#define MCUBOOT_MAX_IMG_SECTORS 512
/* ── Fault injection hardening ────────────────────────────────────────── */
#define MCUBOOT_FIH_PROFILE_LOW
/* ── Логирование — отключено, BOOT_LOG_* становятся no-op (bootutil_log.h) */
/* ── Watchdog — не используется в bootloader ─────────────────────────── */
#define MCUBOOT_WATCHDOG_FEED() \
do \
{ \
} while (0)
#endif /* MCUBOOT_CONFIG_H_ */

View file

@ -0,0 +1,21 @@
/**
* @file mcuboot_logging.h
* @brief Логирование bootutil не используется (MCUBOOT_HAVE_LOGGING не
* определён в mcuboot_config.h, BOOT_LOG_* становятся no-op через
* bootutil_log.h). Этот заголовок обязателен к существованию
* часть заголовков bootutil (bootutil/crypto/sha.h) включает его
* безусловно, независимо от MCUBOOT_HAVE_LOGGING.
*/
#ifndef MCUBOOT_LOGGING_H_
#define MCUBOOT_LOGGING_H_
#define MCUBOOT_LOG_MODULE_DECLARE(domain)
#define MCUBOOT_LOG_MODULE_REGISTER(domain)
#define MCUBOOT_LOG_ERR(...)
#define MCUBOOT_LOG_WRN(...)
#define MCUBOOT_LOG_INF(...)
#define MCUBOOT_LOG_DBG(...)
#endif /* MCUBOOT_LOGGING_H_ */

View file

@ -0,0 +1,26 @@
/**
* @file sysflash.h
* @brief Отображение логических слотов bootutil на flash-area ID.
*
* MCUBOOT_IMAGE_NUMBER=1 два ID: Slot A (primary=0), Slot Б (secondary=1).
* Без scratch Direct-XIP не использует область подкачки. Смещения/размеры
* самих областей заданы в flash_map_backend.c (см.
* docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md).
*/
#ifndef SYSFLASH_H_
#define SYSFLASH_H_
#include "mcuboot_config/mcuboot_config.h"
#define FLASH_AREA_IMAGE_PRIMARY(x) (((x) == 0) ? 0 : 255)
#define FLASH_AREA_IMAGE_SECONDARY(x) (((x) == 0) ? 1 : 255)
#define MCUBOOT_IMAGE_SLOT_NUMBER (MCUBOOT_IMAGE_NUMBER * 2)
/** @brief Единственное flash-устройство в системе — W25Qxx через FlexSPI. */
#define FLASH_DEVICE_ID 1
int flash_area_id_from_multi_image_slot(int image_index, int slot);
#endif /* SYSFLASH_H_ */

View file

@ -0,0 +1,109 @@
/**
* @file boot_select.c
* @brief bootutil boot_go() + прыжок в выбранный образ.
*
* Референс sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot():
* та же последовательность (flash_device_base вычислить адрес vector table
* cleanup __set_MSP __ISB прыжок на Reset_Handler), CMSIS-интринсики, без
* ассемблера. cleanup_before_jump()/VTOR добавлены в Фазе 3 см. её docstring
* про найденный на железе баг (прыжок сразу после bsp_usb_cdc_init()).
*/
#include "boot_select.h"
#include "bootutil/bootutil.h"
#include "bootutil/fault_injection_hardening.h"
#include "flash_map.h"
#include "fsl_common.h"
struct arm_vector_table
{
uint32_t msp;
uint32_t reset;
};
/**
* @brief Вернуть NVIC/SysTick в состояние "как после аппаратного сброса"
* перед прыжком целевой образ не должен унаследовать прерывания,
* включённые bootloader'ом.
*
* Найдено на реальном железе (Фаза 3): Slot A с уже валидным подтверждённым
* образом не загружался, когда прыжок происходил сразу после
* bsp_usb_cdc_init() (USB ещё в процессе enumeration прерывания частые), но
* загружался, когда прыжок происходил позже (после цикла ожидания SD USB
* уже в устоявшемся состоянии). Причина: bootloader (в отличие от Фазы 1/2,
* где до прыжка включался только bsp_qspi_init() без единого постоянно
* включённого NVIC IRQ) теперь включает USB CDC и, при вставленной SD,
* USDHC оба взводят свои NVIC IRQ. jump_to_image() не переключал VTOR
* прерывание, сработавшее в узком окне между прыжком и тем, как целевой
* образ успеет настроить свою таблицу векторов в Reset_Handler/SystemInit(),
* уходило по ещё активной (bootloader'овской) таблице векторов с чужим
* стеком/контекстом.
*
* Портировано по мотивам cleanup()/SBL_DisablePeripherals() в референсном
* sdk/middleware/mcuboot_opensource/boot/nxp_mcux_sdk/boot.c::do_boot() не
* скопировано напрямую (SBL_DisablePeripherals extern, платформенно-
* специфичная функция, не вендоренная в нашем дереве); здесь общий,
* не завязанный на конкретную периферию эквивалент через NVIC/SysTick.
*
* НЕ трогает PRIMASK (в отличие от более ранней, откаченной в Фазе 2
* версии) SysTick_Handler целевого образа должен сработать после того как
* образ сам вызовет bsp_tick_init(), а PRIMASK обычный Reset_Handler не
* восстанавливает (см. bsp_delay()-зависание, найденное в Фазе 2).
* Отключение конкретных источников (NVIC ICER/ICPR, SysTick->CTRL) не
* то же самое, что глобальная маскировка: раз выключенный SysTick просто не
* тикает, пока образ не включит его сам, и не блокирует его же будущий
* запуск.
*/
static void cleanup_before_jump(void)
{
for (uint32_t i = 0U; i < (sizeof(NVIC->ICER) / sizeof(NVIC->ICER[0])); i++)
{
NVIC->ICER[i] = 0xFFFFFFFFU; /* запретить все внешние IRQ */
NVIC->ICPR[i] = 0xFFFFFFFFU; /* сбросить pending — не унаследовать флаг */
}
SysTick->CTRL = 0U; /* SysTick — не в NVIC->ICER, отдельный системный таймер */
__DSB();
__ISB();
}
static void jump_to_image(const struct boot_rsp *p_rsp)
{
uintptr_t flash_base;
if (flash_device_base(p_rsp->br_flash_dev_id, &flash_base) != 0)
{
return;
}
const struct arm_vector_table *p_vt =
(const struct arm_vector_table *) (flash_base + p_rsp->br_image_off +
p_rsp->br_hdr->ih_hdr_size);
cleanup_before_jump();
/* Образ сам переставит VTOR в своём Reset_Handler/SystemInit() — но до
* этого момента (первые же инструкции после прыжка) он уже должен быть
* валиден, на случай если что-то прервёт выполнение раньше. */
SCB->VTOR = (uint32_t) p_vt;
/* Намеренно НЕ __disable_irq()/PRIMASK здесь — см. cleanup_before_jump(). */
__set_CONTROL(0U);
__set_MSP(p_vt->msp);
__ISB();
((void (*)(void)) p_vt->reset)();
}
void boot_select_and_jump(void)
{
struct boot_rsp rsp;
fih_ret fih_rc = boot_go(&rsp);
if (!FIH_EQ(fih_rc, FIH_SUCCESS))
{
return; /* нет валидного образа — main.c продолжит ping/pong-цикл */
}
jump_to_image(&rsp); /* при успехе не возвращается */
}

View file

@ -0,0 +1,21 @@
/**
* @file boot_select.h
* @brief Выбор и запуск образа tft_app через bootutil (Direct-XIP).
*/
#ifndef BOOT_SELECT_H_
#define BOOT_SELECT_H_
/**
* @brief Выбрать образ (bootutil boot_go, Direct-XIP) и прыгнуть в него.
*
* При успехе не возвращается управление переходит в выбранный образ.
* При провале (нет валидного образа ни в одном слоте, или оба слота стёрты
* bootutil'ом из-за незавершённого revert) возвращается, чтобы main.c
* мог продолжить в ping/pong-цикл (задел на состояние "жду SD" Фазы 3).
*
* @pre bsp_qspi_init() уже вызван.
*/
void boot_select_and_jump(void);
#endif /* BOOT_SELECT_H_ */

View file

@ -0,0 +1,257 @@
/**
* @file cli.c
* @brief IO-слой и диспатчер сообщений для bootloader.
*
* Транспорт: USB CDC ACM через bsp_usb_cdc.
*
* Парсинг минималистичный: strstr по фиксированным полям (тот же подход,
* что и в firmware_test/src/cli.c) cJSON не используется намеренно, схема
* входящих сообщений фиксирована.
*
* Входящие типы (Фаза 1):
* "cmd" handle_cmd() protocol_send_pong() / protocol_send_version_response()
*
* [Фаза 4] Команды "smoke_status"/"qspi_info" переспросить результат
* boot-time smoke-теста SDRAM/SEMC и опознанный чип QSPI flash в любой
* момент сессии (сами события шлются один раз рано, до открытия хостом
* порта, и почти наверняка теряются см. protocol.h).
*
* [DEV-ONLY, Фаза 4] Команда "sdram_test" под BOOTLOADER_DEV_DIAGNOSTICS
* (только Debug, см. CMakeLists.txt): запускает dev_sdram_test_run(), которая
* блокирует главный цикл на ~4 с (пройденный прогон на железе: configure ~0,
* address_bus <1 мс, data_bus ~130 мс, sequential ~3.7 с, retention ~400 мс).
* Отсутствует в Release/HAB-бинаре.
*
* Добавление новой команды типа "cmd":
* 1. Добавить ветку if (strcmp(cmd_name, "FOO") == 0) в handle_cmd().
*/
#include "cli.h"
#include "bsp/usb_cdc.h"
#include "protocol.h"
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
#include "dev_sdram_test.h"
#endif
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
/* ── Ключи полей JSON ──────────────────────────────────────────────────── */
static const char K_FIELD_TYPE[] = "\"type\"";
static const char K_FIELD_CMD[] = "\"cmd\"";
/** @brief Буфер непрочитанного остатка chunk после вызова process_line(). */
static uint8_t g_s_chunk_buf[CLI_LINE_BUF_SIZE];
static size_t g_s_chunk_len = 0U;
static size_t g_s_chunk_pos = 0U;
/* ── RX line buffer ────────────────────────────────────────────────────── */
static uint8_t g_s_line_buf[CLI_LINE_BUF_SIZE];
static size_t g_s_line_len = 0U;
/* ── Парсинг полей ─────────────────────────────────────────────────────── */
/**
* @brief Извлечь строковое значение в кавычках после двоеточия.
*
* @param[in] p_after_key Позиция сразу после ключа в строке JSON.
* @param[out] p_out Буфер для результата.
* @param[in] out_size Размер p_out (включая место под '\0').
* @return true если значение найдено и помещается в p_out.
*/
static bool extract_string_value(const char *p_after_key, char *p_out, size_t out_size)
{
const char *colon = strchr(p_after_key, ':');
if (colon == NULL)
{
return false;
}
const char *open_q = strchr(colon + 1U, '"');
if (open_q == NULL)
{
return false;
}
open_q++;
const char *close_q = strchr(open_q, '"');
if (close_q == NULL)
{
return false;
}
size_t len = (size_t) (close_q - open_q);
if (len == 0U || len >= out_size)
{
return false;
}
memcpy(p_out, open_q, len);
p_out[len] = '\0';
return true;
}
/**
* @brief Извлечь значение поля "type".
*/
static bool parse_type_field(const char *p_line, char *p_out, size_t out_size)
{
const char *key = strstr(p_line, K_FIELD_TYPE);
if (key == NULL)
{
return false;
}
return extract_string_value(key + sizeof(K_FIELD_TYPE) - 1U, p_out, out_size);
}
/**
* @brief Извлечь значение поля "cmd".
*/
static bool parse_cmd_field(const char *p_line, char *p_out, size_t out_size)
{
const char *key = strstr(p_line, K_FIELD_CMD);
if (key == NULL)
{
return false;
}
return extract_string_value(key + sizeof(K_FIELD_CMD) - 1U, p_out, out_size);
}
/* ── Обработчики входящих сообщений ────────────────────────────────────── */
/**
* @brief Обработать сообщение {"type":"cmd",...}.
*/
static void handle_cmd(const char *p_line)
{
const uint8_t MAX_CMD_LEN = 32U;
char cmd_name[MAX_CMD_LEN];
if (!parse_cmd_field(p_line, cmd_name, sizeof(cmd_name)))
{
protocol_send_error("PARSE_ERR");
return;
}
if (strcmp(cmd_name, "ping") == 0)
{
protocol_send_pong();
return;
}
if (strcmp(cmd_name, "get_version") == 0)
{
protocol_send_version_response();
return;
}
if (strcmp(cmd_name, "wdog") == 0)
{
protocol_send_wdog_status();
return;
}
if (strcmp(cmd_name, "smoke_status") == 0)
{
protocol_send_smoke_status();
return;
}
if (strcmp(cmd_name, "qspi_info") == 0)
{
protocol_send_qspi_info();
return;
}
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
if (strcmp(cmd_name, "sdram_test") == 0)
{
dev_sdram_test_run();
return;
}
#endif
protocol_send_error("UNKNOWN_CMD");
}
/**
* @brief Диспатчить накопленную строку по полю "type".
*/
static void process_line(const char *p_line)
{
const uint8_t MAX_TYPE_LEN = 16U;
char msg_type[MAX_TYPE_LEN];
if (!parse_type_field(p_line, msg_type, sizeof(msg_type)))
{
protocol_send_error("PARSE_ERR");
return;
}
if (strcmp(msg_type, "cmd") == 0)
{
handle_cmd(p_line);
return;
}
protocol_send_error("UNKNOWN_CMD");
}
/* ── Public API ────────────────────────────────────────────────────────── */
void cli_init(void)
{
g_s_line_len = 0U;
g_s_chunk_len = 0U;
g_s_chunk_pos = 0U;
}
void cli_send(const char *p_resp)
{
bsp_usb_cdc_write((const uint8_t *) p_resp, strlen(p_resp));
}
void cli_process(void)
{
if (g_s_chunk_pos >= g_s_chunk_len)
{
g_s_chunk_len = bsp_usb_cdc_read(g_s_chunk_buf, sizeof(g_s_chunk_buf));
g_s_chunk_pos = 0U;
}
while (g_s_chunk_pos < g_s_chunk_len)
{
uint8_t byte = g_s_chunk_buf[g_s_chunk_pos];
g_s_chunk_pos++;
if (g_s_line_len >= (CLI_LINE_BUF_SIZE - 1U))
{
g_s_line_len = 0U;
protocol_send_error("LINE_TOO_LONG");
return;
}
if (byte == (uint8_t) '\n')
{
if (g_s_line_len > 0U && g_s_line_buf[g_s_line_len - 1U] == (uint8_t) '\r')
{
g_s_line_len--;
}
g_s_line_buf[g_s_line_len] = '\0';
size_t completed_len = g_s_line_len;
g_s_line_len = 0U; /* ← сбросить ДО process_line */
if (completed_len > 0U)
{
process_line((const char *) g_s_line_buf);
}
}
g_s_line_buf[g_s_line_len] = byte;
g_s_line_len++;
}
}

View file

@ -0,0 +1,50 @@
/**
* @file cli.h
* @brief IO-слой CLI для bootloader.
*
* Транспорт: USB CDC ACM (bsp_usb_cdc) единственный канал.
* Протокол: JSON-lines, каждая строка завершается '\n'. Урезанное
* подмножество протокола firmware_test (firmware/test/src/cli.h).
*
* Входящие типы (Фаза 1):
* {"type":"cmd", "cmd":"ping"}
* {"type":"cmd", "cmd":"get_version"}
*
* Исходящие события формируются через protocol.h, а не напрямую через cli_send().
* cli_send() остаётся публичным: его использует protocol.c как единственную
* точку вывода.
*/
#ifndef CLI_H_
#define CLI_H_
#include <stddef.h>
/** @brief Максимальная длина входящей JSON-строки включая '\n'. */
#define CLI_LINE_BUF_SIZE 128U
/**
* @brief Инициализировать CLI. Сбрасывает внутренний буфер строки.
*
* Вызывать после bsp_usb_cdc_init() и до первого cli_process().
*/
void cli_init(void);
/**
* @brief Отправить готовую JSON-строку через USB CDC.
*
* @param[in] p_resp NUL-terminated строка, завершённая '\n'.
*
* @note Неблокирующий. Если TX занят запись теряется.
*/
void cli_send(const char *p_resp);
/**
* @brief Обработать входящие байты, диспатчить сообщение при получении '\n'.
*
* Вызывать в главном цикле после bsp_usb_cdc_poll().
* Неблокирующий: если данных нет возвращается немедленно.
*/
void cli_process(void);
#endif /* CLI_H_ */

View file

@ -0,0 +1,282 @@
/**
* @file dev_sdram_test.c
* @brief [DEV-ONLY] Реализация глубокого теста SDRAM (см. dev_sdram_test.h).
*
* Фазы 1:1 портированы из firmware/test/src/tests/test_sdram.c (тот же
* алгоритм, то же покрытие) не переизобретаются, чтобы результат был
* сопоставим с уже доверенным тестом DCD-пути. Отличия от оригинала:
* - sdram_test_init() там предполагал DCD; здесь сама зовёт
* bsp_sdram_configure() это и есть предмет проверки.
* - test_module_t/test_result_t (инфраструктура тест-раннера firmware_test)
* не используются bootloader её не имеет; результат каждой фазы уходит
* отдельным CDC-событием по ходу прогона, не одним отчётом в конце.
* - добавлено кормление watchdog (bsp_wdog_refresh()) на той же частоте,
* что и опрос CDC без него более медленный прогон рисковал бы не
* пережить таймаут WDOG (10 с, main.c) и словить сброс посреди фазы
* sequential (по факту прогон ~4 с, запас большой см. dev_sdram_test.h).
*/
#include "dev_sdram_test.h"
#include "bsp/sdram.h"
#include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "bsp/wdog.h"
#include "fsl_common.h"
#include "protocol.h"
#include <stdbool.h>
#include <stdint.h>
/* ── Константы (те же значения, что в firmware_test/test_sdram.c) ───────── */
/** @brief Интервал вызова pump() при записи/чтении, байт. */
#define SDRAM_USB_POLL_INTERVAL_BYTES 0x1000U
/** @brief Ширина walking ones паттерна, бит. */
#define SDRAM_WALKING_ONES_BITS 8U
/** @brief Количество адресных бит теста address_bus (13 row+9 col+2 bank). */
#define SDRAM_ADDR_BUS_BITS 24U
/** @brief Размер кэш-линии Cortex-M7, байт. */
#define SDRAM_CACHE_LINE_BYTES 32U
/** @brief Размер фазы sequential, байт (2 MB). */
#define SDRAM_SEQUENTIAL_SIZE 0x00200000UL
/** @brief Размер фазы retention, байт (256 KB). */
#define SDRAM_RETENTION_SIZE 0x00040000UL
/** @brief Задержка фазы retention, мс (MT48LC16M16A2: авто-refresh 64 мс, ≈3 периода). */
#define SDRAM_RETENTION_DELAY_MS 200U
/** @brief Шаг ожидания в фазе retention (для pump()), мс. */
#define SDRAM_RETENTION_POLL_STEP_MS 10U
/* ── Локальные типы ────────────────────────────────────────────────────── */
/** @brief Информация о первой ошибке паттерн-прохода. fail_addr==0 — нет ошибки. */
typedef struct
{
uint32_t fail_addr;
uint8_t expected;
uint8_t got;
} sdram_fail_info_t;
typedef uint8_t (*pattern_fn_t)(uint32_t offset);
/* ── Паттерн-функции ───────────────────────────────────────────────────── */
static uint8_t pattern_walking_ones(uint32_t offset)
{
return (uint8_t) (1U << (offset % SDRAM_WALKING_ONES_BITS));
}
static uint8_t pattern_walking_ones_inv(uint32_t offset)
{
return (uint8_t) (~(1U << (offset % SDRAM_WALKING_ONES_BITS)) & 0xFFU);
}
static uint8_t pattern_addr(uint32_t offset)
{
return (uint8_t) (offset & 0xFFU);
}
static uint8_t pattern_addr_inv(uint32_t offset)
{
return (uint8_t) (~offset & 0xFFU);
}
/* ── Вспомогательные функции ───────────────────────────────────────────── */
/** @brief Опрос CDC + кормление watchdog. Вызывать не реже раза в ~4 KB. */
static void pump(void)
{
bsp_usb_cdc_poll();
bsp_wdog_refresh();
}
static void flush_dcache(uint32_t size)
{
uint32_t *const P_BASE = (uint32_t *) BSP_SDRAM_TEST_BASE_ADDR;
SCB_CleanDCache_by_Addr(P_BASE, (int32_t) size);
SCB_InvalidateDCache_by_Addr(P_BASE, (int32_t) size);
__DSB();
}
/** @brief Сброс одной кэш-линии по произвольному байтовому адресу (для address_bus). */
static void flush_dcache_line_at(uint32_t byte_addr)
{
uint32_t *const P_LINE = (uint32_t *) (byte_addr & ~(SDRAM_CACHE_LINE_BYTES - 1U));
SCB_CleanDCache_by_Addr(P_LINE, (int32_t) SDRAM_CACHE_LINE_BYTES);
SCB_InvalidateDCache_by_Addr(P_LINE, (int32_t) SDRAM_CACHE_LINE_BYTES);
__DSB();
}
static void delay_with_pump(uint32_t ms)
{
const uint32_t START_MS = bsp_tick_get_ms();
while ((bsp_tick_get_ms() - START_MS) < ms)
{
bsp_delay(SDRAM_RETENTION_POLL_STEP_MS);
pump();
}
}
static void write_pattern(uint32_t size, pattern_fn_t p_fn)
{
volatile uint8_t *const P_BASE = (volatile uint8_t *) BSP_SDRAM_TEST_BASE_ADDR;
for (uint32_t i = 0U; i < size; i++)
{
if ((i & (SDRAM_USB_POLL_INTERVAL_BYTES - 1U)) == 0U)
{
pump();
}
P_BASE[i] = p_fn(i);
}
}
/** @brief Верифицировать паттерн. Останавливается на первом несовпадении. */
static bool verify_pattern(uint32_t size, pattern_fn_t p_fn, sdram_fail_info_t *p_fail)
{
volatile const uint8_t *const P_BASE = (volatile const uint8_t *) BSP_SDRAM_TEST_BASE_ADDR;
for (uint32_t i = 0U; i < size; i++)
{
if ((i & (SDRAM_USB_POLL_INTERVAL_BYTES - 1U)) == 0U)
{
pump();
}
const uint8_t EXPECTED = p_fn(i);
const uint8_t GOT = P_BASE[i];
if (GOT != EXPECTED)
{
p_fail->fail_addr = BSP_SDRAM_TEST_BASE_ADDR + i;
p_fail->expected = EXPECTED;
p_fail->got = GOT;
return false;
}
}
return true;
}
/** @brief Один паттерн-проход: запись → flush → верификация. */
static bool run_pass(uint32_t size, pattern_fn_t p_fn, sdram_fail_info_t *p_fail)
{
write_pattern(size, p_fn);
flush_dcache(size);
return verify_pattern(size, p_fn, p_fail);
}
/* ── Фазы теста (см. dev_sdram_test.h) ───────────────────────────────────── */
/** @brief Фаза address_bus: 24 адресных бита, точечный флаш на каждую запись. */
static bool run_phase_address_bus(sdram_fail_info_t *p_fail)
{
volatile uint8_t *const P_BASE = (volatile uint8_t *) BSP_SDRAM_TEST_BASE_ADDR;
for (uint32_t bit = 0U; bit < SDRAM_ADDR_BUS_BITS; bit++)
{
const uint32_t OFFSET = (1UL << bit);
const uint8_t PATTERN = (uint8_t) (bit + 1U);
P_BASE[OFFSET] = PATTERN;
flush_dcache_line_at(BSP_SDRAM_TEST_BASE_ADDR + OFFSET);
const uint8_t GOT = P_BASE[OFFSET];
if (GOT != PATTERN)
{
p_fail->fail_addr = BSP_SDRAM_TEST_BASE_ADDR + OFFSET;
p_fail->expected = PATTERN;
p_fail->got = GOT;
return false;
}
}
return true;
}
/** @brief Фаза data_bus: walking ones + инверсия, 64 KB. */
static bool run_phase_data_bus(sdram_fail_info_t *p_fail)
{
return run_pass(BSP_SDRAM_TEST_FAST_SIZE, pattern_walking_ones, p_fail) &&
run_pass(BSP_SDRAM_TEST_FAST_SIZE, pattern_walking_ones_inv, p_fail);
}
/** @brief Фаза sequential: address pattern + инверсия, 2 MB. */
static bool run_phase_sequential(sdram_fail_info_t *p_fail)
{
return run_pass(SDRAM_SEQUENTIAL_SIZE, pattern_addr, p_fail) &&
run_pass(SDRAM_SEQUENTIAL_SIZE, pattern_addr_inv, p_fail);
}
/** @brief Фаза retention: запись → flush → 200 мс → verify, 256 KB. */
static bool run_phase_retention(sdram_fail_info_t *p_fail)
{
write_pattern(SDRAM_RETENTION_SIZE, pattern_addr);
flush_dcache(SDRAM_RETENTION_SIZE);
delay_with_pump(SDRAM_RETENTION_DELAY_MS);
return verify_pattern(SDRAM_RETENTION_SIZE, pattern_addr, p_fail);
}
/* ── Оркестрация ──────────────────────────────────────────────────────── */
static void report_phase(const char *p_name, bool pass, uint32_t start_ms, const sdram_fail_info_t *p_fail)
{
const uint32_t DURATION_MS = bsp_tick_get_ms() - start_ms;
const uint32_t FAIL_ADDR = (p_fail != NULL) ? p_fail->fail_addr : 0U;
const uint8_t EXPECTED = (p_fail != NULL) ? p_fail->expected : 0U;
const uint8_t GOT = (p_fail != NULL) ? p_fail->got : 0U;
protocol_send_sdram_test_phase(p_name, pass, DURATION_MS, FAIL_ADDR, EXPECTED, GOT);
}
void dev_sdram_test_run(void)
{
const uint32_t CONFIGURE_START_MS = bsp_tick_get_ms();
const bool CONFIGURED = (bsp_sdram_configure() == BSP_OK) && (bsp_sdram_init() == BSP_OK);
report_phase("configure", CONFIGURED, CONFIGURE_START_MS, NULL);
if (!CONFIGURED)
{
return;
}
const uint32_t OVERALL_START_MS = bsp_tick_get_ms();
sdram_fail_info_t fail = { 0U, 0U, 0U };
bool is_ok;
uint32_t phase_start_ms;
phase_start_ms = bsp_tick_get_ms();
is_ok = run_phase_address_bus(&fail);
report_phase("address_bus", is_ok, phase_start_ms, is_ok ? NULL : &fail);
if (is_ok)
{
phase_start_ms = bsp_tick_get_ms();
is_ok = run_phase_data_bus(&fail);
report_phase("data_bus", is_ok, phase_start_ms, is_ok ? NULL : &fail);
}
if (is_ok)
{
phase_start_ms = bsp_tick_get_ms();
is_ok = run_phase_sequential(&fail);
report_phase("sequential", is_ok, phase_start_ms, is_ok ? NULL : &fail);
}
if (is_ok)
{
phase_start_ms = bsp_tick_get_ms();
is_ok = run_phase_retention(&fail);
report_phase("retention", is_ok, phase_start_ms, is_ok ? NULL : &fail);
}
report_phase("summary", is_ok, OVERALL_START_MS, NULL);
}

View file

@ -0,0 +1,45 @@
/**
* @file dev_sdram_test.h
* @brief [DEV-ONLY] Глубокий тест SDRAM через bsp_sdram_configure() (Фаза 4).
*
* НЕ для production компилируется только в Debug
* (BOOTLOADER_DEV_DIAGNOSTICS, см. CMakeLists.txt); отсутствует в HAB
* Release-бинаре.
*
* Цель не заменить лёгкий boot-time smoke-test (main.c, "smoke_pass"/
* "smoke_fail"), а дать ту же глубину проверки, что уже доверена
* firmware_test/src/tests/test_sdram.c (4 фазы, покрывают адресную шину,
* шину данных, coupling между ячейками и refresh-timing), но против нового
* C-порта DCD (bsp_sdram_configure()), а не против DCD напрямую чтобы
* убедиться, что порт поднимает память так же надёжно, как проверенный
* годами в производстве DCD.
*
* Вызывается по CDC-команде {"type":"cmd","cmd":"sdram_test"} (см. cli.c).
* Блокирует главный цикл на время прогона (~4 с, см. таймингы ниже реально
* измерено на железе, а не оценка) сама кормит watchdog и опрашивает CDC по
* ходу, вызывающему коду ничего дополнительно делать не нужно. Каждая фаза и
* итог репортятся отдельным событием по CDC синхронно по ходу прогона (см.
* protocol_send_sdram_test_phase()).
*/
#ifndef DEV_SDRAM_TEST_H_
#define DEV_SDRAM_TEST_H_
/**
* @brief Поднять SEMC (bsp_sdram_configure()) и прогнать 4-фазный тест SDRAM.
*
* Фазы (портированы из firmware/test/src/tests/test_sdram.c без изменений
* логики меняется только то, кто поднимает SEMC до них). Тайминги
* реальный прогон на железе (не оценка из test_sdram.c, та оказалась
* консервативнее раз в 6 запись садится в D-Cache почти мгновенно, реальная
* задержка SDRAM только на flush_dcache() и на чтение при верификации):
* 1. address_bus 24 адресных бита (13 row + 9 col + 2 bank), <1 мс.
* 2. data_bus walking ones + инверсия, 64 KB, ~130 мс.
* 3. sequential address pattern + инверсия, 2 MB, ~3.7 с.
* 4. retention запись flush 200 мс verify, 256 KB, ~400 мс.
*
* Если bsp_sdram_configure()/bsp_sdram_init() не прошли репортится фаза
* "configure" с pass=false, дальнейшие фазы не запускаются.
*/
void dev_sdram_test_run(void);
#endif /* DEV_SDRAM_TEST_H_ */

View file

@ -0,0 +1,113 @@
/**
* @file led_status.c
* @brief Реализация словаря LED-паттернов см. led_status.h и
* docs/bootloader/LED_PATTERNS.md.
*/
#include "led_status.h"
#include "bsp/led.h"
#include "bsp/tick.h"
#include "bsp/wdog.h"
#include <stdbool.h>
#include <stdint.h>
/* ── Периоды паттернов, мс (СИНХРОНИЗИРОВАТЬ с LED_PATTERNS.md) ─────────── */
/** @brief Heartbeat «жив»: 50 мс горит / 450 мс не горит (период 500). */
#define LED_HEARTBEAT_ON_MS 50U
#define LED_HEARTBEAT_PERIOD_MS 500U
/** @brief Неисправность железа: APP 100/100 (период 200). */
#define LED_HW_FAULT_ON_MS 100U
#define LED_HW_FAULT_PERIOD_MS 200U
/** @brief Recovery: оба LED синхронно 100/100 (период 200). */
#define LED_RECOVERY_ON_MS 100U
#define LED_RECOVERY_PERIOD_MS 200U
/** @brief Установка: APP 250/250 (период 500). */
#define LED_INSTALL_ON_MS 250U
#define LED_INSTALL_PERIOD_MS 500U
/** @brief «Образ отклонён»: 4 вспышки по 80 мс вкл / 80 мс выкл. */
#define LED_REJECT_ON_MS 80U
#define LED_REJECT_COUNT 4U
/* ── Состояние модуля ──────────────────────────────────────────────────── */
/** @brief Окно установки открыто — см. led_status_tick_install(). */
static bool g_s_installing = false;
/* ── Внутренние помощники ──────────────────────────────────────────────── */
/** @brief true, если по текущему тику LED в фазе «горит» для период/on. */
static bool phase_on(uint32_t period_ms, uint32_t on_ms)
{
return (bsp_tick_get_ms() % period_ms) < on_ms;
}
/** @brief Отрисовать системный heartbeat (общий для waiting/hw_fault/install). */
static void draw_heartbeat(void)
{
bsp_led_set(LED_HEARTBEAT, phase_on(LED_HEARTBEAT_PERIOD_MS, LED_HEARTBEAT_ON_MS));
}
/* ── Public API ────────────────────────────────────────────────────────── */
void led_status_draw_background(led_bg_t bg)
{
switch (bg)
{
case LED_BG_RECOVERY:
{
/* Оба LED — один и тот же фазовый расчёт → строго синхронно. */
const bool ON = phase_on(LED_RECOVERY_PERIOD_MS, LED_RECOVERY_ON_MS);
bsp_led_set(LED_HEARTBEAT, ON);
bsp_led_set(LED_APP, ON);
break;
}
case LED_BG_HW_FAULT:
draw_heartbeat(); /* heartbeat в своём ритме 50/450 — не синхронен с APP */
bsp_led_set(LED_APP, phase_on(LED_HW_FAULT_PERIOD_MS, LED_HW_FAULT_ON_MS));
break;
case LED_BG_WAITING:
default:
draw_heartbeat();
bsp_led_off(LED_APP);
break;
}
}
void led_status_install_begin(void)
{
g_s_installing = true;
}
void led_status_install_end(void)
{
g_s_installing = false;
}
void led_status_tick_install(void)
{
if (!g_s_installing)
{
return; /* не установка (revert/recovery-стирание) — ничего не трогаем */
}
draw_heartbeat();
bsp_led_set(LED_APP, phase_on(LED_INSTALL_PERIOD_MS, LED_INSTALL_ON_MS));
}
void led_status_flash_image_rejected(void)
{
for (uint32_t i = 0U; i < LED_REJECT_COUNT; i++)
{
bsp_wdog_refresh(); /* ~640 мс блокирующей вспышки — держим watchdog сытым */
bsp_led_on(LED_APP);
bsp_delay(LED_REJECT_ON_MS);
bsp_led_off(LED_APP);
bsp_delay(LED_REJECT_ON_MS);
}
}

View file

@ -0,0 +1,76 @@
/**
* @file led_status.h
* @brief Индикация bootloader на двух LED единый словарь паттернов.
*
* Полное человекочитаемое описание (для сервисных инженеров)
* docs/bootloader/LED_PATTERNS.md. Здесь программный контракт; периоды
* в мс заданы в led_status.c и обязаны совпадать с тем документом.
*
* Разделение на два вида вызова не случайно:
* - Фоновые (устойчивые) состояния рисует главный цикл main.c каждую
* итерацию: led_status_draw_background().
* - Паттерн «идёт установка» рисуется ИЗНУТРИ блокирующей установки (главный
* цикл в это время не исполняется): led_status_tick_install(), обёрнутый в
* окно led_status_install_begin()/_end(). Вне окна tick no-op, поэтому
* его безопасно звать из flash_area_erase(), который дёргается и на
* revert/recovery-стирании, а не только на установке.
*/
#ifndef LED_STATUS_H_
#define LED_STATUS_H_
/** @brief Фоновое (устойчивое) состояние индикации. */
typedef enum
{
LED_BG_WAITING, /**< Норма: HEARTBEAT 50/450, APP выкл — ждём microSD. */
LED_BG_HW_FAULT, /**< Неисправность: HEARTBEAT 50/450, APP 100/100. */
LED_BG_RECOVERY, /**< Recovery (Фаза 6): оба LED синхронно 100/100. */
} led_bg_t;
/**
* @brief Нарисовать фоновый паттерн по текущему тику.
*
* Вызывать каждую итерацию главного цикла. Приоритет разрешается на стороне
* вызывателя (recovery > неисправность > норма) сюда приходит уже
* выбранное состояние.
*/
void led_status_draw_background(led_bg_t bg);
/** @brief Открыть окно «идёт установка» — с этого момента tick рисует паттерн. */
void led_status_install_begin(void);
/** @brief Закрыть окно «идёт установка» — tick снова становится no-op. */
void led_status_install_end(void);
/**
* @brief Обновить APP/HEARTBEAT под паттерн установки (HEARTBEAT 50/450,
* APP 250/250). No-op вне окна install.
*
* Звать из ВСЕХ блокирующих циклов установки и поблочного стирания слота
* (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);
/**
* @brief Разовая индикация «образ с SD отклонён»: APP мигает 4×(80/80).
*
* Блокирующая (~640 мс), кормит watchdog по ходу. Оставляет APP выключенным
* фоновый паттерн восстановит главный цикл на следующей итерации.
*/
void led_status_flash_image_rejected(void);
#endif /* LED_STATUS_H_ */

View file

@ -0,0 +1,313 @@
/**
* @file main.c
* @brief bootloader точка входа.
*
* Фаза 3: перед выбором образа (bootutil, Direct-XIP) проверяется microSD
* если вставлена, sd_update_check() при необходимости ставит более новый
* (или, при удержании BSP_BUTTON_1, принудительно более старый) подписанный
* образ в неактивный слот. boot_select_and_jump() вызывается РОВНО ОДИН РАЗ
* за попытку если валидного образа нет, main() возвращается в цикл
* ожидания, где SD периодически пере-сканируется (см. sd_update.h о том,
* почему boot_go() нельзя звать без новой попытки установки между вызовами).
*
* Фаза 6 (recovery, см. recovery.h): каждая попытка обёрнута в attempt_boot()
* после SD-скана, но перед прыжком, recovery_decide() решает, обычная ли
* это загрузка, нужно ли стереть подозреваемый в зависании слот (счётчик
* bsp_boot_attempt_count() дошёл до порога, но есть валидный фолбэк), или
* входить в recovery (порог без фолбэка, или удержан BSP_BUTTON_2). Класс A
* таксономии (незавершённая установка) закрывается штатным revert MCUboot
* без участия этой логики.
*
* USB CDC поднимается ДО SD-логики (не дожидаясь подключения хоста
* bsp_usb_cdc_write() не блокируется без хоста, см. bsp/usb_cdc/src/usb_cdc.c)
* чтобы статусы ("installing" и т.п.) были видны, если технолог уже
* подключён, в т.ч. на самой первой попытке (чек-лист Фазы 3, сценарий 1).
*
* Последовательность старта:
* 1. board_hw_init() тактирование, MPU, кэш, пины
* 2. bsp_wdog_init() аппаратный watchdog как можно раньше (см. ниже)
* 3. bsp_boot_state_init() POR-детект + счётчик попыток (Фаза 6)
* 4. bsp_led_init() оба LED выключены
* 5. bsp_tick_init() SysTick 1 мс
* 6. bsp_button_init() для проверки удержания BSP_BUTTON_1/2
* 7. bsp_qspi_init() доступ к Slot A/Б
* 8. bsp_usb_cdc_init() не блокирует, см. выше
* 9. qspi_info (Фаза 4) идентификация чипа QSPI (JEDEC имя +
* ёмкость), не зависит от qspi_ok, только CDC
* 10. bsp_sdram_configure()+ smoke-test SDRAM/SEMC (Фаза 4): диагностика,
* bsp_sdram_init() не блокирует, результат только на CDC
* 11. attempt_boot() SD-скан + recovery-гейт + прыжок; при
* успехе не возвращается
* 12. Цикл ожидания CDC ping/pong + LED + периодический
* пере-скан SD (шаг 11 повторно)
*
* Watchdog (bsp_wdog): единственная защита от бесконечных зависаний в
* блокирующих вызовах SDMMC-стека, не возвращающих управление в наш код
* (SD_PollingCardInsert / OSA_SemaphoreWait см. DEBUG_LOG_PHASE3_SD.md).
* WDE write-once: после взвода watchdog не выключить, он переживает прыжок,
* поэтому целевой образ (tft_app / test_stub) ОБЯЗАН его кормить (см. bsp/wdog).
* Кормим только в точках реального прогресса (верх цикла, циклы стирания/
* копирования, перед прыжком) НЕ перед f_mount/SD_Init, иначе watchdog
* перестаёт защищать именно от них.
*
* Bootloader не зависит от SDRAM (см. docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md)
* DCD не используется (bsp_boot_xip_no_dcd), XIP только из W25Q. SEMC/SDRAM
* трогаются только диагностически, шагом 9 (bsp_sdram_configure(), см.
* bsp/sdram/README.md) bootloader сам эту память ни для чего не использует.
*/
#include "board.h"
#include "boot_select.h"
#include "bsp/boot_state.h"
#include "bsp/button.h"
#include "bsp/led.h"
#include "bsp/qspi_flash.h"
#include "bsp/sdram.h"
#include "bsp/tick.h"
#include "bsp/usb_cdc.h"
#include "bsp/wdog.h"
#include "cli.h"
#include "flash_map.h"
#include "led_status.h"
#include "protocol.h"
#include "recovery.h"
#include "sd_update.h"
#include "slot_version.h"
#include <stdbool.h>
#include <stdint.h>
/* ── Одна попытка загрузки: recovery-гейт (Фаза 6) → SD-скан → прыжок ─────
*
* Порядок важен: recovery_decide() должна знать, входим ли мы в recovery, ДО
* SD-скана от этого зависит, каким gate'ом сканировать SD (обычным строгим
* или ослабленным, update_policy_decide(recovery_mode), см. update_policy.h).
* Обратный порядок (сначала SD, потом решение) не дал бы recovery-режиму
* смысла: строгий gate никогда не поставит образ поверх "активного", даже
* если тот активный и есть подозреваемый в зависании слот.
*
* peek_slot() та же логика, что private peek_slot() в sd_update.c: не
* шарим напрямую между модулями (см. update_policy.h), копия минимальна.
* Здесь только для recovery_decide(); sd_update_check() независимо
* повторно пикает слоты внутри себя для update_policy_decide(). */
static update_policy_slot_state_t peek_slot(uint8_t fa_id)
{
update_policy_slot_state_t state;
state.valid = slot_version_get(fa_id, &state.version);
return state;
}
static void jump_now(void)
{
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_b = peek_slot(1U);
recovery_decision_t decision = recovery_decide(
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)
{
case RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER:
{
/* Подозреваемый в зависании слот — стереть, есть подтверждённый
* фолбэк (recovery_decide() это уже проверила). boot_go() внутри
* jump_now() сам выберет оставшийся. */
const struct flash_area *p_fap;
if (flash_area_open((uint8_t) decision.active_slot, &p_fap) == 0)
{
(void) flash_area_erase(p_fap, 0U, p_fap->fa_size);
flash_area_close(p_fap);
}
bsp_boot_attempt_reset(); /* ситуация изменилась — новый полный бюджет */
jump_now();
break;
}
case RECOVERY_NORMAL_BOOT:
default:
/* Инкремент только если действительно ЕСТЬ что пытаться загрузить
* (слот уже валиден, либо только что установлен этим же вызовом)
* иначе на чисто пустой плате без 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;
}
return false;
}
int main(void)
{
const uint32_t ERROR_BLINK_MS = 250U;
const uint32_t SD_RETRY_PERIOD_MS = 1500U;
/* Таймаут WDOG. С запасом над самым долгим НАКОРМЛЕННЫМ участком: между
* соседними refresh худший легитимный интервал одиночное стирание 64 КБ
* блока (~0.15..2 c по даташиту W25Q) либо цепочка bsp_sd_init+f_mount+пик
* слотов (~2-2.5 c). 10 c даёт кратный запас; зависание ловится 10 c. */
const uint32_t WDOG_TIMEOUT_S = 10U;
/* Минимум по docs/mimxrt1052/BOOTLOADER_FLASH_MAP.md §1 — карта
* (bootloader+Slot A+Slot Б+запас под ФС ассетов) рассчитана на W25Q128
* (16 МБ) и выше; W25Q64 драйвер технически поддерживает, но для этой
* платы это неверный BOM, а не "чуть меньше запас". */
const uint32_t QSPI_MIN_FLASH_SIZE_MB = 16U;
board_hw_init();
/* Как можно раньше — до первой же SD-логики, которая может зависнуть. */
(void) bsp_wdog_init(WDOG_TIMEOUT_S);
/* Сразу после watchdog — сама операция дешёвая (пара регистров SRC), а
* решение recovery_decide() ниже нужно уже на первой попытке. */
bsp_boot_state_init();
bsp_led_init();
bsp_tick_init();
bsp_button_init();
/* Жест форс. даунгрейда — удержание BSP_BUTTON_1 при подаче питания.
* Сэмплируем РОВНО ЗДЕСЬ, до медленной SD-инициализации, и защёлкиваем на
* всю сессию: сама установка читает кнопку глубоко внутри run_update()
* (после mount + двух крипто-валидаций слотов, секунды спустя), поэтому
* читать её там неинтуитивно (см. DEBUG_LOG_PHASE3_SD.md, тайминг кнопки).
* Значение переиспользуется и первой попыткой, и пере-сканами в цикле. */
const bool DOWNGRADE_HELD = bsp_button_read(BSP_BUTTON_1);
/* Жест recovery (Фаза 6) — тот же приём, удержание BSP_BUTTON_2. Приоритет
* над BTN_1 разрешается внутри recovery_decide() (проверяется первым). */
const bool RECOVERY_HELD = bsp_button_read(BSP_BUTTON_2);
bool qspi_ok = (bsp_qspi_init() == BSP_OK);
bool cdc_ok = (bsp_usb_cdc_init() == BSP_OK);
/* Единый признак «плата не годна» (LED_BG_HW_FAULT, см. LED_PATTERNS.md) —
* накапливается по обоим boot-time чекам ниже (QSPI + SDRAM smoke).
* Детали, что именно не так, всегда есть по CDC (qspi_info/smoke_status);
* LED показывает лишь факт неисправности. */
bool hw_fault = false;
/* Идентификация QSPI-чипа (Фаза 4) — не зависит от qspi_ok:
* bsp_qspi_read_jedec_id() отрабатывает и после проваленного
* bsp_qspi_init() (см. её @note), так что "чип не тот"/"чип не опознан"
* репортится с деталями, а не просто as "не сработало". */
{
bsp_qspi_jedec_t jedec = { 0U, 0U };
uint32_t qspi_size_mb = 0U;
const char *p_qspi_chip = "UNKNOWN";
uint8_t qspi_cap_byte = 0U;
if (bsp_qspi_read_jedec_id(&jedec) == BSP_OK)
{
qspi_cap_byte = (uint8_t) (jedec.device_id & 0xFFU);
p_qspi_chip = bsp_qspi_decode_chip(qspi_cap_byte, &qspi_size_mb);
}
const bool QSPI_PASS = (qspi_size_mb >= QSPI_MIN_FLASH_SIZE_MB);
hw_fault = (!qspi_ok) || (!QSPI_PASS); /* чип не отвечает / не тот / мал */
protocol_set_qspi_info(jedec.manufacturer_id, p_qspi_chip, qspi_cap_byte, qspi_size_mb, QSPI_PASS);
protocol_send_qspi_info(); /* лучший случай — хост уже слушает; см. protocol.h */
}
/* Smoke-test SDRAM/SEMC (Фаза 4) — диагностический, неблокирующий: не
* влияет на attempt_boot() ниже (bootloader SDRAM ни для чего не
* использует, см. docstring файла), результат только репортится по CDC.
* Цель поймать неисправность SEMC/SDRAM на плате раньше, чем её
* унаследует tft_app (см. bsp/sdram/README.md). */
bool sdram_ok = (bsp_sdram_configure() == BSP_OK) && (bsp_sdram_init() == BSP_OK);
hw_fault = hw_fault || (!sdram_ok);
protocol_set_smoke_result(sdram_ok);
protocol_send_smoke_status(); /* лучший случай — хост уже слушает; см. protocol.h */
/* Отслеживает recovery-состояние между попытками — main-loop использует
* его для LED-паттерна каждую итерацию, не только на попытках прыжка
* (attempt_boot() зовётся раз в SD_RETRY_PERIOD_MS, LED должен обновляться
* значительно чаще). */
bool in_recovery = false;
if (qspi_ok)
{
in_recovery = attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
}
/* Нет валидного образа ни в одном слоте (или сбой QSPI) —
* диагностический режим. */
if (!cdc_ok)
{
bsp_led_toggle(LED_HEARTBEAT);
bsp_delay(ERROR_BLINK_MS);
}
cli_init();
/* Если предыдущий сброс — по таймауту watchdog, известим (best-effort:
* если хост ещё не подключён, сообщение потеряется состояние всегда
* доступно по команде "wdog", см. cli.c). */
if (bsp_wdog_caused_last_reset())
{
protocol_send_wdog_status();
}
/* Готово немедленно — первая попытка сразу извещает "жду SD", не ждёт
* SD_RETRY_PERIOD_MS. Дальнейшие попытки уже дросселируются периодом. */
uint32_t next_sd_retry_ms = bsp_tick_get_ms();
while (1)
{
bsp_wdog_refresh(); /* начало итерации — точка реального прогресса */
bsp_usb_cdc_poll();
cli_process();
/* Фоновый паттерн: recovery > неисправность железа > норма (ждём SD).
* Паттерн «установка» здесь не участвует он рисуется изнутри самой
* (блокирующей) установки, см. led_status_tick_install(). */
led_bg_t bg = in_recovery ? LED_BG_RECOVERY : (hw_fault ? LED_BG_HW_FAULT : LED_BG_WAITING);
led_status_draw_background(bg);
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;
protocol_send_status(in_recovery ? "recovery_mode" : "waiting_for_sd");
in_recovery = attempt_boot(DOWNGRADE_HELD, RECOVERY_HELD);
}
}
}

View file

@ -0,0 +1,138 @@
/**
* @file protocol.c
* @brief Протокол bootloader реализация сериализации.
*/
#include "protocol.h"
#include "bsp/boot_state.h"
#include "bsp/wdog.h"
#include "cli.h"
#include "recovery.h"
#include <stdio.h>
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
#include <inttypes.h>
#endif
/* ── Константы ─────────────────────────────────────────────────────────── */
/**
* @brief Размер внутреннего TX-буфера.
*
* 192, не 128: {"type":"sdram_test",...} с именем фазы "sequential" и
* fail_addr/expected/got самое длинное сообщение протокола, ~140 байт.
*/
#define PROTO_BUF_SIZE 192U
/* ── Состояние модуля ──────────────────────────────────────────────────── */
/** @brief Кэш результата smoke-теста SDRAM/SEMC — см. protocol_send_smoke_status(). */
static bool s_smoke_result_known = false;
static bool s_smoke_result_pass = false;
/** @brief Кэш информации о QSPI-чипе — см. protocol_send_qspi_info(). */
static bool s_qspi_info_known = false;
static bool s_qspi_pass = false;
static uint8_t s_qspi_mfr_id = 0U;
static uint8_t s_qspi_cap_byte = 0U;
static uint32_t s_qspi_size_mb = 0U;
static const char *s_p_qspi_chip = ""; /* строковый литерал из bsp_qspi_decode_chip() — статичен */
/* ── Public API ────────────────────────────────────────────────────────── */
void protocol_send_pong(void)
{
cli_send("{\"type\":\"pong\"}\n");
}
void protocol_send_version_response(void)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"version_response\","
"\"fw\":\"" BOOTLOADER_VERSION "\"}\n");
cli_send(buf);
}
void protocol_send_error(const char *p_code)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf), "{\"ok\":false,\"error\":\"%s\"}\n", p_code);
cli_send(buf);
}
void protocol_send_status(const char *p_state)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf), "{\"type\":\"status\",\"state\":\"%s\"}\n", p_state);
cli_send(buf);
}
void protocol_set_smoke_result(bool pass)
{
s_smoke_result_known = true;
s_smoke_result_pass = pass;
}
void protocol_send_smoke_status(void)
{
if (s_smoke_result_known)
{
protocol_send_status(s_smoke_result_pass ? "smoke_pass" : "smoke_fail");
}
}
void protocol_set_qspi_info(uint8_t mfr_id, const char *p_chip_name, uint8_t cap_byte, uint32_t size_mb, bool pass)
{
s_qspi_info_known = true;
s_qspi_mfr_id = mfr_id;
s_p_qspi_chip = p_chip_name;
s_qspi_cap_byte = cap_byte;
s_qspi_size_mb = size_mb;
s_qspi_pass = pass;
}
void protocol_send_qspi_info(void)
{
if (!s_qspi_info_known)
{
return;
}
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"qspi_info\",\"chip\":\"%s\",\"mfr\":\"0x%02X\","
"\"cap_byte\":\"0x%02X\",\"size_mb\":%u,\"pass\":%s}\n",
s_p_qspi_chip, (unsigned) s_qspi_mfr_id, (unsigned) s_qspi_cap_byte,
(unsigned) s_qspi_size_mb, s_qspi_pass ? "true" : "false");
cli_send(buf);
}
void protocol_send_wdog_status(void)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"wdog\",\"armed\":%s,\"timeout_s\":%u,\"recovered\":%s,"
"\"reset_count\":%u,\"threshold\":%u}\n",
bsp_wdog_is_armed() ? "true" : "false",
(unsigned) bsp_wdog_timeout_s(),
bsp_wdog_caused_last_reset() ? "true" : "false",
(unsigned) bsp_boot_attempt_count(),
(unsigned) RECOVERY_DEFAULT_THRESHOLD);
cli_send(buf);
}
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
void protocol_send_sdram_test_phase(
const char *p_phase, bool pass, uint32_t duration_ms, uint32_t fail_addr, uint8_t expected, uint8_t got)
{
char buf[PROTO_BUF_SIZE];
(void) snprintf(buf, sizeof(buf),
"{\"type\":\"sdram_test\",\"phase\":\"%s\",\"pass\":%s,\"duration_ms\":%" PRIu32
",\"fail_addr\":\"0x%08" PRIX32 "\",\"expected\":\"0x%02X\",\"got\":\"0x%02X\"}\n",
p_phase, pass ? "true" : "false", duration_ms, fail_addr, (unsigned) expected, (unsigned) got);
cli_send(buf);
}
#endif /* BOOTLOADER_DEV_DIAGNOSTICS */

View file

@ -0,0 +1,177 @@
/**
* @file protocol.h
* @brief Протокол bootloader сериализация исходящих событий.
*
* Урезанное подмножество протокола firmware_test (firmware/test/src/protocol.h):
* только то, что нужно для диагностики bootloader через service-tui. Без
* test_begin/test_result/confirm_request/session_start те специфичны для
* тестового раннера firmware_test.
*
* Все функции формируют JSON-строку и отправляют через cli_send().
* Без динамической памяти каждая функция пишет в стековый буфер.
*
* Типы исходящих событий (Фаза 1):
* pong ответ на {"type":"cmd","cmd":"ping"}
* version_response ответ на {"type":"cmd","cmd":"get_version"}
* error ошибка протокола или парсинга
*
* Типы исходящих событий (Фаза 3):
* status top-level состояние bootloader (waiting_for_sd,
* installing, update_skipped, recovery_mode,
* smoke_pass/smoke_fail (Фаза 4, SDRAM/SEMC smoke-test,
* см. main.c) см. protocol_send_status())
* wdog статус аппаратного watchdog (armed/timeout/recovered,
* см. protocol_send_wdog_status())
* qspi_info опознанный чип QSPI flash + вписывается ли в минимум
* карты флеша (Фаза 4, см. protocol_send_qspi_info())
*
* smoke_pass/smoke_fail и qspi_info шлются один раз сразу после
* bsp_usb_cdc_init()/bsp_qspi_init() хост почти наверняка не успевает
* открыть порт к этому моменту (USB enumeration), cli_send() неблокирующий и
* теряет запись, если TX ещё не готов. Решение для обоих одинаковое:
* protocol_set_smoke_result()/protocol_set_qspi_info() кэширует исход,
* protocol_send_smoke_status()/protocol_send_qspi_info() переспрашивает его в
* любой момент сессии по командам "smoke_status"/"qspi_info" (тот же приём,
* что уже был у wdog).
*
* Ещё не реализовано (Фаза 4): состояние "booting" и словарь LED-паттернов
* на LED_APP для всех перечисленных состояний см. firmware/bootloader/PLAN.md.
*
* [DEV-ONLY, Фаза 4] Тип sdram_test под BOOTLOADER_DEV_DIAGNOSTICS
* (компилируется только в Debug, см. CMakeLists.txt и dev_sdram_test.h):
* sdram_test результат одной фазы глубокого теста SDRAM, см.
* protocol_send_sdram_test_phase() и dev_sdram_test.h
*/
#ifndef PROTOCOL_H_
#define PROTOCOL_H_
#include "version.h"
#include <stdbool.h>
#include <stdint.h>
/** @brief Строка версии bootloader, вставляемая в version_response. */
#define BOOTLOADER_VERSION BOOTLOADER_VERSION_STR
/**
* @brief Отправить pong ответ на ping.
*/
void protocol_send_pong(void);
/**
* @brief Отправить ответ на команду get_version.
*
* Формат: {"type":"version_response","fw":"X.Y.Z"}
*/
void protocol_send_version_response(void);
/**
* @brief Отправить событие error.
*
* @param[in] p_code Короткий ASCII-код ошибки, напр. "PARSE_ERR".
*/
void protocol_send_error(const char *p_code);
/**
* @brief Отправить событие status top-level состояние bootloader.
*
* Формат: {"type":"status","state":"waiting_for_sd"}
*
* @param[in] p_state Короткий ASCII-идентификатор состояния,
* напр. "waiting_for_sd", "installing".
*/
void protocol_send_status(const char *p_state);
/**
* @brief Сохранить результат smoke-теста SDRAM/SEMC (Фаза 4) для последующих
* protocol_send_smoke_status().
*
* Вызывать один раз из main.c сразу после bsp_sdram_configure()+_init().
*
* @param[in] pass true, если оба вызова вернули BSP_OK.
*/
void protocol_set_smoke_result(bool pass);
/**
* @brief Отправить status с последним сохранённым результатом smoke-теста.
*
* Формат: {"type":"status","state":"smoke_pass"} / "smoke_fail". Ничего не
* делает, если protocol_set_smoke_result() ещё ни разу не вызывался (не
* должно происходить в штатной последовательности main.c, но на команду
* "smoke_status" в этом случае лучше промолчать, чем соврать результат).
*/
void protocol_send_smoke_status(void);
/**
* @brief Сохранить информацию об обнаруженном QSPI flash-чипе (Фаза 4) для
* последующих protocol_send_qspi_info().
*
* Вызывать один раз из main.c сразу после bsp_qspi_init(). Значения
* читаются напрямую по JEDEC ID (bsp_qspi_decode_chip()), не зависят от
* успеха bsp_qspi_init() так неопознанный/не тот чип тоже репортится с
* деталями, а не просто "не сработало".
*
* @param[in] mfr_id Сырой manufacturer byte JEDEC ID.
* @param[in] p_chip_name Имя чипа ("W25Q128", "UNKNOWN") см. bsp_qspi_decode_chip().
* @param[in] cap_byte Сырой capacity byte JEDEC ID.
* @param[in] size_mb Обнаруженная ёмкость, МБ (0, если чип не опознан).
* @param[in] pass true, если Winbond + известная ёмкость + ёмкость не
* меньше минимума карты флеша (см. main.c).
*/
void protocol_set_qspi_info(
uint8_t mfr_id, const char *p_chip_name, uint8_t cap_byte, uint32_t size_mb, bool pass);
/**
* @brief Отправить event с последней сохранённой информацией о QSPI-чипе.
*
* Формат: {"type":"qspi_info","chip":"W25Q128","mfr":"0xEF","cap_byte":"0x18",
* "size_mb":16,"pass":true}
*
* Ничего не делает, если protocol_set_qspi_info() ещё не вызывался.
*/
void protocol_send_qspi_info(void);
/**
* @brief Отправить статус аппаратного watchdog и счётчика попыток загрузки
* (Фаза 6).
*
* Формат: {"type":"wdog","armed":true,"timeout_s":10,"recovered":false,
* "reset_count":0,"threshold":3}
* - armed watchdog взведён (bsp_wdog_init выполнен);
* - timeout_s сконфигурированный таймаут в секундах;
* - recovered ПОСЛЕДНИЙ сброс МК был по таймауту watchdog (плата
* восстановилась после зависания);
* - reset_count bsp_boot_attempt_count(): сколько попыток подряд без
* подтверждения здоровья (health-mark/новая установка), 0
* сразу после POR;
* - threshold RECOVERY_DEFAULT_THRESHOLD: порог фолбэка/recovery.
*
* Эмитится один раз на старте, если recovered, и по команде "wdog".
*/
void protocol_send_wdog_status(void);
#ifdef BOOTLOADER_DEV_DIAGNOSTICS
/**
* @brief [DEV-ONLY] Отправить результат одной фазы dev_sdram_test (Фаза 4).
*
* Формат: {"type":"sdram_test","phase":"data_bus","pass":true,"duration_ms":812,
* "fail_addr":"0x00000000","expected":"0x00","got":"0x00"}
*
* fail_addr/expected/got осмысленны только при pass=false при pass=true
* передавать 0/0/0. phase="summary" итог всего прогона (все фазы пройдены
* И до summary дошло см. dev_sdram_test.c).
*
* @param[in] p_phase "configure"/"address_bus"/"data_bus"/"sequential"/
* "retention"/"summary".
* @param[in] pass Результат фазы.
* @param[in] duration_ms Длительность фазы, мс.
* @param[in] fail_addr Адрес первой ошибки (0, если pass=true).
* @param[in] expected Ожидаемый байт при ошибке.
* @param[in] got Прочитанный байт при ошибке.
*/
void protocol_send_sdram_test_phase(
const char *p_phase, bool pass, uint32_t duration_ms, uint32_t fail_addr, uint8_t expected, uint8_t got);
#endif /* BOOTLOADER_DEV_DIAGNOSTICS */
#endif /* PROTOCOL_H_ */

View file

@ -0,0 +1,75 @@
/**
* @file recovery.c
* @brief Реализация см. recovery.h.
*/
#include "recovery.h"
/* ── Определение активного слота ──────────────────────────────────────────
* Тот же приём, что find_active_slot() в update_policy.c (private там)
* не шарим напрямую, копия минимальна и независимо тестируется здесь же,
* как уже принято в проекте для мелкой логики между модулями. */
typedef struct
{
bool have_active;
update_policy_slot_t active_slot;
struct image_version active_ver;
} active_slot_info_t;
static active_slot_info_t find_active_slot(const update_policy_slot_state_t *p_slot_a,
const update_policy_slot_state_t *p_slot_b)
{
active_slot_info_t info = { .have_active = false };
if (p_slot_a->valid)
{
info.have_active = true;
info.active_slot = UPDATE_POLICY_SLOT_A;
info.active_ver = p_slot_a->version;
}
if (p_slot_b->valid &&
(!info.have_active || image_version_compare(&p_slot_b->version, &info.active_ver) > 0))
{
info.have_active = true;
info.active_slot = UPDATE_POLICY_SLOT_B;
info.active_ver = p_slot_b->version;
}
return info;
}
static bool other_slot_valid(update_policy_slot_t active_slot,
const update_policy_slot_state_t *p_slot_a,
const update_policy_slot_state_t *p_slot_b)
{
return (active_slot == UPDATE_POLICY_SLOT_A) ? p_slot_b->valid : p_slot_a->valid;
}
/* ── Публичный API ─────────────────────────────────────────────────────── */
recovery_decision_t recovery_decide(uint32_t attempt_count, uint32_t threshold,
const update_policy_slot_state_t *p_slot_a,
const update_policy_slot_state_t *p_slot_b, bool btn2_held)
{
if (btn2_held)
{
return (recovery_decision_t) { .action = RECOVERY_ENTER_RECOVERY_MODE };
}
if (attempt_count < threshold)
{
return (recovery_decision_t) { .action = RECOVERY_NORMAL_BOOT };
}
active_slot_info_t active = find_active_slot(p_slot_a, p_slot_b);
if (active.have_active && other_slot_valid(active.active_slot, p_slot_a, p_slot_b))
{
return (recovery_decision_t) { .action = RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER,
.active_slot = active.active_slot };
}
return (recovery_decision_t) { .action = RECOVERY_ENTER_RECOVERY_MODE };
}

View file

@ -0,0 +1,72 @@
/**
* @file recovery.h
* @brief Чистая логика решения "что делать с этой попыткой загрузки"
* без аппаратных зависимостей (flash/SRC_GPR), полностью
* host-тестируема.
*
* См. firmware/bootloader/PLAN.md, Фаза 6, разбивка 6a.
*/
#ifndef RECOVERY_H_
#define RECOVERY_H_
#include "update_policy.h"
#include <stdbool.h>
#include <stdint.h>
typedef enum
{
RECOVERY_NORMAL_BOOT = 0,
RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER,
RECOVERY_ENTER_RECOVERY_MODE,
} recovery_action_t;
typedef struct
{
recovery_action_t action;
/**
* Слот, который нужно стереть перед прыжком. Значим только при
* action == RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER.
*/
update_policy_slot_t active_slot;
} recovery_decision_t;
/**
* @brief Решить, что делать с текущей попыткой загрузки таксономия
* отказов Фазы 6 (классы B/C/D; класс A закрывается штатным revert
* MCUboot без участия этой функции).
*
* Правила (приоритет сверху вниз):
* - btn2_held RECOVERY_ENTER_RECOVERY_MODE. Ручной триггер главнее
* счётчика оператор может войти в recovery в любой момент, независимо
* от истории попыток.
* - attempt_count < threshold RECOVERY_NORMAL_BOOT. Обычная загрузка,
* ничего не предпринимаем (счётчик инкрементирует вызывающий код перед
* прыжком).
* - attempt_count >= threshold и есть "активный" слот (валидный, более
* высокой версии тот же приём, что update_policy_decide()) он и
* есть подозреваемый в зависании:
* - другой слот валиден (есть фолбэк) RECOVERY_ERASE_ACTIVE_THEN_
* BOOT_OTHER(active_slot). Вызывающий код стирает active_slot и
* обнуляет счётчик boot_go() сам выберет оставшийся слот.
* - другого валидного слота нет (стирать нечего иначе ноль рабочих
* слотов) RECOVERY_ENTER_RECOVERY_MODE.
* - attempt_count >= threshold и активного слота нет вообще
* RECOVERY_ENTER_RECOVERY_MODE (нечего анализировать, нечего стирать).
*
* @param[in] attempt_count Текущее значение счётчика попыток (bsp_boot_attempt_count()).
* @param[in] threshold Порог срабатывания фолбэка/recovery (см. RECOVERY_DEFAULT_THRESHOLD).
* @param[in] p_slot_a Состояние Slot A.
* @param[in] p_slot_b Состояние Slot Б.
* @param[in] btn2_held BSP_BUTTON_2 удержана на старте ручной вход в recovery.
*/
recovery_decision_t recovery_decide(uint32_t attempt_count, uint32_t threshold,
const update_policy_slot_state_t *p_slot_a,
const update_policy_slot_state_t *p_slot_b, bool btn2_held);
/** @brief Дефолтный порог — 3 сброса подряд до фолбэка/recovery. */
#define RECOVERY_DEFAULT_THRESHOLD 3U
#endif /* RECOVERY_H_ */

View file

@ -0,0 +1,273 @@
/**
* @file sd_update.c
* @brief Реализация см. sd_update.h.
*
* Гейт принятия кандидата двухступенчатый:
* 1. Лёгкий пик заголовка (magic + версия) сразу с SD, до касания flash
* достаточно для решения install/skip (update_policy_decide()).
* 2. Полная криптографическая проверка (hash+ECDSA) уже ПОСЛЕ записи в
* целевой слот, переиспользованием slot_version_get() (та же функция,
* что и для пика активного слота). Если кандидат подписан неверно
* slot_version_get() на целевом слоте вернёт false, а последующий
* (единственный) boot_go() в main.c просто не выберет этот слот и
* останется на прежнем валидном отдельный shim "flash_area поверх SD-
* файла" не нужен.
*
* Целевой слот при установке всегда НЕ активный (см. update_policy.h):
* уже выбранный на этот момент слот этой функцией никогда не стирается.
* Исключение recovery-режим (Фаза 6b, update_policy_decide(recovery_mode)):
* там целевой слот всегда Slot A, независимо от того, что было активно.
*/
#include "sd_update.h"
#include "bootutil/image.h"
#include "bsp/sd.h"
#include "bsp/usb_cdc.h"
#include "bsp/wdog.h"
#include "ff.h"
#include "flash_map.h"
#include "led_status.h"
#include "protocol.h"
#include "slot_version.h"
#include "update_policy.h"
#include <string.h>
/* ── Константы ─────────────────────────────────────────────────────────── */
#define SD_UPDATE_MOUNT_POINT "2:/"
#define SD_UPDATE_FILE_PATH "2:/TFT_APP.BIN"
/** @brief Размер чанка потокового копирования SD → flash. */
#define SD_UPDATE_CHUNK_SIZE 4096U
/* ── Состояние модуля ────────────────────────────────────────────────────── */
static FATFS g_s_fs;
static FIL g_s_file;
static uint8_t g_s_chunk_buf[SD_UPDATE_CHUNK_SIZE];
static uint8_t g_s_verify_buf[SD_UPDATE_CHUNK_SIZE];
/* ── Вспомогательные функции ───────────────────────────────────────────── */
static update_policy_slot_state_t peek_slot(uint8_t fa_id)
{
update_policy_slot_state_t state;
state.valid = slot_version_get(fa_id, &state.version);
return state;
}
/**
* @brief Прочитать заголовок кандидата с начала уже открытого файла.
* @retval true magic верный версия в *p_out_ver, позиция файла = sizeof(header).
* @retval false Ошибка чтения либо неверный magic.
*/
static bool read_candidate_header(struct image_version *p_out_ver)
{
struct image_header hdr;
UINT br = 0U;
FRESULT fr = f_read(&g_s_file, &hdr, sizeof(hdr), &br);
if ((fr != FR_OK) || (br != sizeof(hdr)) || (hdr.ih_magic != IMAGE_MAGIC))
{
return false;
}
*p_out_ver = hdr.ih_ver;
return true;
}
/**
* @brief Стереть целевой слот и потоково скопировать в него файл-кандидат
* (с начала файла заголовок читается заново), сверяя каждый
* записанный чанк немедленным обратным чтением.
*
* @return true при успехе (весь файл скопирован и каждый чанк совпал).
*/
static bool erase_and_copy_candidate(const struct flash_area *p_fap, uint32_t file_size)
{
if (file_size > p_fap->fa_size)
{
return false;
}
if (flash_area_erase(p_fap, 0U, p_fap->fa_size) != 0)
{
return false;
}
if (f_lseek(&g_s_file, 0) != FR_OK)
{
return false;
}
uint32_t offset = 0U;
while (offset < file_size)
{
bsp_usb_cdc_poll();
bsp_wdog_refresh(); /* потоковое копирование — реальный прогресс на чанк */
led_status_tick_install(); /* APP 250/250 всю установку, см. led_status.h */
uint32_t want = file_size - offset;
if (want > SD_UPDATE_CHUNK_SIZE)
{
want = SD_UPDATE_CHUNK_SIZE;
}
UINT br = 0U;
if ((f_read(&g_s_file, g_s_chunk_buf, want, &br) != FR_OK) || (br != want))
{
return false;
}
if (flash_area_write(p_fap, offset, g_s_chunk_buf, want) != 0)
{
return false;
}
if ((flash_area_read(p_fap, offset, g_s_verify_buf, want) != 0) ||
(memcmp(g_s_chunk_buf, g_s_verify_buf, want) != 0))
{
return false;
}
offset += want;
}
bsp_usb_cdc_poll();
return true;
}
/* ── Основной сценарий ────────────────────────────────────────────────── */
/**
* @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 installed_ver;
update_policy_slot_state_t slot_a;
update_policy_slot_state_t slot_b;
update_policy_result_t decision;
const struct flash_area *p_fap = NULL;
uint32_t file_size;
bool copy_ok;
bool result = false;
if (bsp_sd_init() != BSP_OK)
{
return false;
}
if (f_mount(&g_s_fs, SD_UPDATE_MOUNT_POINT, 1) != FR_OK)
{
(void) bsp_sd_deinit();
return false; /* нет карты/файловой системы — штатно, не ошибка */
}
if (f_open(&g_s_file, SD_UPDATE_FILE_PATH, FA_READ) != FR_OK)
{
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
(void) bsp_sd_deinit();
return false; /* TFT_APP.BIN отсутствует — тоже штатно */
}
if (!read_candidate_header(&candidate_ver))
{
protocol_send_error("SD_CANDIDATE_INVALID");
led_status_flash_image_rejected(); /* битый заголовок = негодный файл */
goto cleanup;
}
file_size = (uint32_t) f_size(&g_s_file);
slot_a = peek_slot(0U);
slot_b = peek_slot(1U);
/* button_held — сэмплирован при старте в main.c и передан сюда (см.
* 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)
{
protocol_send_status("update_skipped");
goto cleanup;
}
protocol_send_status("installing");
led_status_install_begin(); /* открыть окно: tick_install() ниже начинает рисовать */
if (flash_area_open((uint8_t) decision.target_slot, &p_fap) != 0)
{
protocol_send_error("SD_INSTALL_WRITE_FAILED");
goto cleanup;
}
copy_ok = erase_and_copy_candidate(p_fap, file_size);
flash_area_close(p_fap);
if (!copy_ok)
{
protocol_send_error("SD_INSTALL_WRITE_FAILED");
goto cleanup;
}
if (!slot_version_get((uint8_t) decision.target_slot, &installed_ver))
{
protocol_send_error("SD_INSTALL_REJECTED");
led_status_flash_image_rejected(); /* записан, но подпись не прошла = негодный файл */
goto cleanup;
}
/* target_slot подтверждён валидным — установка состоялась независимо от
* исхода стирания "второго" слота ниже (оно диагностируется отдельным
* protocol_send_error, но не отменяет уже подтверждённый результат). */
result = true;
/* Форс. даунгрейд и recovery (см. update_policy.h): без стирания
* прежнего активного/Slot Б он остался бы валиден (и новее в обычном
* режиме) и снова выиграл бы в boot_go() даунгрейд/recovery физически
* записались бы, но не загрузились. Стираем ТОЛЬКО теперь, когда новый
* образ уже подтверждён валидным на диске никогда не бывает нуля
* рабочих слотов. */
if (decision.erase_previous_active)
{
const struct flash_area *p_peer_fap;
uint8_t peer_slot = (decision.target_slot == UPDATE_POLICY_SLOT_A) ? UPDATE_POLICY_SLOT_B
: UPDATE_POLICY_SLOT_A;
if (flash_area_open(peer_slot, &p_peer_fap) != 0)
{
protocol_send_error("SD_DOWNGRADE_ERASE_FAILED");
}
else
{
if (flash_area_erase(p_peer_fap, 0U, p_peer_fap->fa_size) != 0)
{
protocol_send_error("SD_DOWNGRADE_ERASE_FAILED");
}
flash_area_close(p_peer_fap);
}
}
cleanup:
led_status_install_end(); /* закрыть окно (идемпотентно, если не открывали) */
(void) f_close(&g_s_file);
(void) f_unmount(SD_UPDATE_MOUNT_POINT);
(void) bsp_sd_deinit();
return result;
}
/* ── Public API ────────────────────────────────────────────────────────── */
bool sd_update_check(bool downgrade_button_held, bool recovery_mode)
{
if (!bsp_sd_is_inserted())
{
return false;
}
return run_update(downgrade_button_held, recovery_mode);
}

View file

@ -0,0 +1,50 @@
/**
* @file sd_update.h
* @brief Оркестрация установки образа tft_app с microSD в неактивный слот.
*
* См. firmware/bootloader/PLAN.md, Фаза 3.
*/
#ifndef SD_UPDATE_H_
#define SD_UPDATE_H_
#include <stdbool.h>
/**
* @brief Одна попытка: смонтировать SD, найти TFT_APP.BIN, при необходимости
* установить его в целевой слот (см. update_policy.h).
*
* Обычный режим (recovery_mode == false) пишет только в неактивный слот
* уже выбранный/загружаемый слот никогда не трогается. Recovery-режим
* (Фаза 6b) ослабленный gate: любой подписанный кандидат ставится в Slot A
* безусловно, Slot Б стирается. Сам не вызывает boot_go()/
* boot_select_and_jump() решение "когда прыгать" остаётся за main.c,
* которое обязано вызвать его ровно один раз за сессию питания (см.
* slot_version.h о том, почему boot_go() нельзя звать повторно).
*
* Ничего не делает, если SD не вставлена (bsp_sd_is_inserted() == false)
* безопасно вызывать многократно, в т.ч. из цикла ожидания в main.c.
*
* @param downgrade_button_held Состояние BSP_BUTTON_1, сэмплированное ОДИН РАЗ
* при старте (main.c, до медленной SD-инициализации) и защёлкнутое.
* true разрешён форс. даунгрейд более старого подписанного образа
* (см. update_policy.h). Передаётся, а не читается здесь, чтобы жест
* "удержание при включении" ловился в предсказуемый ранний момент, а не
* через несколько секунд внутри run_update() (DEBUG_LOG_PHASE3_SD.md).
* Игнорируется при 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() уже вызван.
*/
bool sd_update_check(bool downgrade_button_held, bool recovery_mode);
#endif /* SD_UPDATE_H_ */

View file

@ -0,0 +1,65 @@
/**
* @file slot_version.c
* @brief Реализация см. slot_version.h.
*
* Вызов bootutil_img_validate() зеркалит loader.c::boot_image_check()
* (enc_state=NULL шифрование образов не используется, image_index=0
* MCUBOOT_IMAGE_NUMBER=1, seed=NULL/0 FIH_PROFILE_LOW не использует RNG-
* задержку, out_hash=NULL хэш нам не нужен, только факт валидности+версия).
*
* FIH_CALL безопасен вне boot_go(): CFI-счётчик (FIH_ENABLE_CFI под LOW
* профилем) сохраняется/инкрементируется в FIH_CFI_PRECALL_BLOCK и
* проверяется/возвращается к сохранённому значению в FIH_CFI_POSTCALL_BLOCK
* пара самобалансирующаяся на каждый вызов, не накапливающееся состояние
* между вызовами (см. fault_injection_hardening.h). Несколько вызовов подряд
* (Slot A, Slot Б, кандидат) и последующий отдельный boot_go() не влияют друг
* на друга через этот счётчик.
*/
#include "slot_version.h"
#include "bootutil/fault_injection_hardening.h"
#include "flash_map.h"
#include <stddef.h> /* NULL */
/*
* Совпадает с BOOT_TMPBUF_SZ в sdk/middleware/mcuboot_opensource/boot/bootutil/
* src/bootutil_priv.h приватный заголовок bootutil (в src/, не в include/),
* поэтому не включаем его напрямую. Тот же размер, что loader.c использует
* для этого же вызова.
*/
#define SLOT_VERSION_TMPBUF_SIZE 256U
bool slot_version_get(uint8_t fa_id, struct image_version *p_out_ver)
{
const struct flash_area *p_fap;
if (flash_area_open(fa_id, &p_fap) != 0)
{
return false;
}
struct image_header hdr;
bool read_ok = (flash_area_read(p_fap, 0U, &hdr, sizeof(hdr)) == 0);
if (!read_ok || (hdr.ih_magic != IMAGE_MAGIC))
{
flash_area_close(p_fap);
return false;
}
static uint8_t s_tmpbuf[SLOT_VERSION_TMPBUF_SIZE];
fih_ret fih_rc;
FIH_CALL(bootutil_img_validate, fih_rc, NULL, 0, &hdr, p_fap, s_tmpbuf, sizeof(s_tmpbuf), NULL,
0, NULL);
flash_area_close(p_fap);
if (!FIH_EQ(fih_rc, FIH_SUCCESS))
{
return false;
}
*p_out_ver = hdr.ih_ver;
return true;
}

View file

@ -0,0 +1,37 @@
/**
* @file slot_version.h
* @brief Read-only "пик" версии образа в слоте bootutil без побочных
* эффектов на flash.
*
* В отличие от boot_go(): для образа, ни разу не подтверждённого приложением,
* bootutil (Direct-XIP-Revert) пишет copy_done в трейлер слота уже на этапе
* выбора вызов boot_go() второй раз за одну сессию питания принял бы этот
* флаг за "образ уже грузился и не подтвердился" и стёр бы его (см.
* firmware/bootloader/PLAN.md, Фаза 3). slot_version_get() читает и валидирует
* слот через flash_area_read()/bootutil_img_validate() напрямую оба
* read-only (image_validate.c не пишет в flash), в обход boot_go().
*
* Валидация полная (hash + ECDSA-подпись через bootutil_img_validate()),
* тот же путь, что loader.c::boot_image_check() использует для каждого слота
* при штатной загрузке, а не самодельная проверка одного заголовка.
*/
#ifndef SLOT_VERSION_H_
#define SLOT_VERSION_H_
#include "bootutil/image.h"
#include <stdbool.h>
#include <stdint.h>
/**
* @brief Прочитать и провалидировать образ в слоте fa_id.
*
* @param[in] fa_id ID области (см. sysflash.h) 0 = Slot A, 1 = Slot Б.
* @param[out] p_out_ver Версия образа при успехе. Не тронут при false.
* @retval true Валидный образ (magic, hash и подпись прошли) версия в *p_out_ver.
* @retval false Слот пуст/повреждён/подпись не прошла, либо ошибка чтения/открытия.
*/
bool slot_version_get(uint8_t fa_id, struct image_version *p_out_ver);
#endif /* SLOT_VERSION_H_ */

View file

@ -0,0 +1,110 @@
/**
* @file update_policy.c
* @brief Реализация см. update_policy.h.
*/
#include "update_policy.h"
/* ── Сравнение версий ──────────────────────────────────────────────────── */
int image_version_compare(const struct image_version *p_ver1, const struct image_version *p_ver2)
{
if (p_ver1->iv_major != p_ver2->iv_major)
{
return (p_ver1->iv_major > p_ver2->iv_major) ? 1 : -1;
}
if (p_ver1->iv_minor != p_ver2->iv_minor)
{
return (p_ver1->iv_minor > p_ver2->iv_minor) ? 1 : -1;
}
if (p_ver1->iv_revision != p_ver2->iv_revision)
{
return (p_ver1->iv_revision > p_ver2->iv_revision) ? 1 : -1;
}
return 0;
}
/* ── Определение активного слота ──────────────────────────────────────── */
typedef struct
{
bool have_active;
update_policy_slot_t active_slot;
struct image_version active_ver;
} active_slot_info_t;
static active_slot_info_t find_active_slot(const update_policy_slot_state_t *p_slot_a,
const update_policy_slot_state_t *p_slot_b)
{
active_slot_info_t info = { .have_active = false };
if (p_slot_a->valid)
{
info.have_active = true;
info.active_slot = UPDATE_POLICY_SLOT_A;
info.active_ver = p_slot_a->version;
}
if (p_slot_b->valid &&
(!info.have_active || image_version_compare(&p_slot_b->version, &info.active_ver) > 0))
{
info.have_active = true;
info.active_slot = UPDATE_POLICY_SLOT_B;
info.active_ver = p_slot_b->version;
}
return info;
}
static update_policy_slot_t other_slot(update_policy_slot_t slot)
{
return (slot == UPDATE_POLICY_SLOT_A) ? UPDATE_POLICY_SLOT_B : UPDATE_POLICY_SLOT_A;
}
/* ── Публичный API ─────────────────────────────────────────────────────── */
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 struct image_version *p_candidate_ver,
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);
if (!active.have_active)
{
return (update_policy_result_t) { .action = UPDATE_POLICY_INSTALL,
.target_slot = UPDATE_POLICY_SLOT_A,
.erase_previous_active = false };
}
int cmp = image_version_compare(p_candidate_ver, &active.active_ver);
bool candidate_newer = (cmp > 0);
bool forced_downgrade = (cmp < 0) && button_held;
if (!candidate_newer && !forced_downgrade)
{
return (update_policy_result_t) { .action = UPDATE_POLICY_SKIP };
}
/* Форс. даунгрейд без стирания прежнего активного слота не имел бы
* эффекта он остаётся валиден и новее, и снова выиграет в boot_go().
* "Новее" не требует стирания прежний активный сам проиграет
* сравнение версий естественным путём. */
return (update_policy_result_t) { .action = UPDATE_POLICY_INSTALL,
.target_slot = other_slot(active.active_slot),
.erase_previous_active = forced_downgrade };
}

View file

@ -0,0 +1,116 @@
/**
* @file update_policy.h
* @brief Чистая логика решения "устанавливать ли SD-кандидат" без
* аппаратных зависимостей (flash/FatFS), полностью host-тестируема.
*
* См. firmware/bootloader/PLAN.md, Фаза 3.
*/
#ifndef UPDATE_POLICY_H_
#define UPDATE_POLICY_H_
#include "bootutil/image.h"
#include <stdbool.h>
/** @brief Логический слот (индекс сисфлеша, не физический адрес). */
typedef enum
{
UPDATE_POLICY_SLOT_A = 0,
UPDATE_POLICY_SLOT_B = 1,
} update_policy_slot_t;
/** @brief Состояние одного слота глазами вызывающего (см. slot_version.h). */
typedef struct
{
bool valid; /**< Есть валидный (прошедший bootutil_img_validate) образ. */
struct image_version version; /**< Значимо только если valid == true. */
} update_policy_slot_state_t;
typedef enum
{
UPDATE_POLICY_SKIP = 0,
UPDATE_POLICY_INSTALL,
} update_policy_action_t;
typedef struct
{
update_policy_action_t action;
update_policy_slot_t target_slot; /**< Значим только если action == UPDATE_POLICY_INSTALL. */
/**
* Значим только при action == UPDATE_POLICY_INSTALL. Если true
* вызывающий код обязан, ПОСЛЕ успешной установки и пост-записи
* валидации target_slot, стереть слот, который был активным ДО
* установки (см. rationale ниже про форс. даунгрейд).
*/
bool erase_previous_active;
} update_policy_result_t;
/**
* @brief Решить, устанавливать ли SD-кандидат, и в какой слот.
*
* Правила (обычный режим, recovery_mode == false):
* - "Активный" слот валидный слот с более высокой версией; если валиден
* только один он активный; если ни одного активного слота нет.
* - Целевой слот установки всегда НЕ активный (активный не перезаписываем
* никогда, независимо от исхода сравнения версий). Если активного слота
* нет по умолчанию Slot A.
* - Кандидат новее активного (или активного слота нет вообще) INSTALL,
* erase_previous_active = false. Прежний активный слот сам проиграет
* сравнение версий в boot_go() стирать его не нужно.
* - Кандидат старше или равен активному, кнопка не удержана SKIP.
* - Кандидат старше активного, кнопка удержана INSTALL,
* erase_previous_active = true. Без этого форс. даунгрейд не имел бы
* эффекта: прежний (более новый) активный слот остался бы валиден и
* снова выиграл бы сравнение версий в boot_go(), несмотря на успешную
* запись более старого образа в другой слот. Стирание обязанность
* вызывающего кода и только ПОСЛЕ подтверждения, что только что
* установленный образ валиден (иначе на короткое время не осталось бы
* ни одного рабочего слота).
* - Кандидат равен активному, кнопка удержана 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_b Состояние Slot Б.
* @param[in] p_candidate_ver Версия образа-кандидата на SD. Не читается при
* recovery_mode == true.
* @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,
const update_policy_slot_state_t *p_slot_b,
const struct image_version *p_candidate_ver,
bool button_held, bool recovery_mode);
/**
* @brief Сравнить версии образов: major.minor.revision, без build_num то
* же соглашение, что boot_version_cmp() в sdk/.../bootutil/loader.c
* (static там, не экспортируется здесь свой аналог).
*
* @retval <0 p_ver1 < p_ver2
* @retval 0 p_ver1 == p_ver2
* @retval >0 p_ver1 > p_ver2
*/
int image_version_compare(const struct image_version *p_ver1, const struct image_version *p_ver2);
#endif /* UPDATE_POLICY_H_ */

View file

@ -0,0 +1,25 @@
/**
* @file version.h
* @brief Версия bootloader генерируется CMake из CMakeLists.txt.
*
* НЕ редактировать вручную. Версию менять в firmware/bootloader/CMakeLists.txt:
* project(bootloader VERSION X.Y.Z)
*/
#ifndef VERSION_H_
#define VERSION_H_
/** @brief Мажорная версия. */
#define BOOTLOADER_VERSION_MAJOR @bootloader_VERSION_MAJOR@
/** @brief Минорная версия. */
#define BOOTLOADER_VERSION_MINOR @bootloader_VERSION_MINOR@
/** @brief Патч-версия. */
#define BOOTLOADER_VERSION_PATCH @bootloader_VERSION_PATCH@
/** @brief Версия строкой для протокола: "X.Y.Z". */
#define BOOTLOADER_VERSION_STR \
"@bootloader_VERSION_MAJOR@.@bootloader_VERSION_MINOR@.@bootloader_VERSION_PATCH@"
#endif /* VERSION_H_ */

View file

@ -0,0 +1,186 @@
# firmware/bootloader/test_stub/CMakeLists.txt
#
# Заглушка tft_app для аппаратной верификации корректной работы загрузчика
#
# Один main.c, много таргетов: адрес слота (--defsym __slot_base__) + частота
# мигания (STUB_BLINK_MS) — чтобы на глаз отличить, какой слот выбрал bootloader
# плюс три независимые оси confirm/hang/watchdog — чтобы прогнать классы
# отказов A/B на реальном железе. Подписывается вручную imgtool.
#
include(
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake)
function(add_mcuboot_stub)
cmake_parse_arguments(
ARG ""
"NAME;SLOT_BASE;BLINK_MS;OWN_SLOT_ID;CONFIRM_MODE;HANG_MODE;FEED_WDOG" ""
${ARGN})
if(NOT DEFINED ARG_CONFIRM_MODE)
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
${CMAKE_SOURCE_DIR}/firmware/bootloader/src/led_status.c
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c)
# 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}
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, поэтому
# образ ОБЯЗАН его кормить, если STUB_FEED_WDOG=1 (иначе reset-loop — либо
# намеренно, для теста самого механизма). bsp_boot_state —
# bsp_boot_health_mark(). bsp_qspi_flash — нужен flash_map_backend.c для
# 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(
${ARG_NAME}
PRIVATE
-Wl,--gc-sections
-Wl,--print-memory-usage
-Wl,-Map=${CMAKE_BINARY_DIR}/${ARG_NAME}.map
-Wl,--defsym=__slot_base__=${ARG_SLOT_BASE}
-Wl,--defsym=__stack_size__=0x400
-Wl,--defsym=__heap_size__=0x400
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld)
set_target_properties(${ARG_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR})
add_custom_command(
TARGET ${ARG_NAME}
POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${ARG_NAME}>
${CMAKE_BINARY_DIR}/${ARG_NAME}.bin
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${ARG_NAME}>
COMMENT "Generating ${ARG_NAME}.bin")
endfunction()
# ─────────────────────────────────────────────────────────────────────────
# Базовые "здоровые" заглушки: подтверждаются сразу, никогда не виснут. 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)
# Класс 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)

View file

@ -0,0 +1,161 @@
/**
* @file main.c
* @brief Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) и
* recovery-логики (Фаза 6).
*
* tft_app ещё не реализована bootloader'у некуда прыгать. Этот образ
* минимальный, но настоящий imgtool-подписанный XIP-образ с корректным
* vector table по адресу слота: базово мигает LED_APP с периодом,
* зависящим от STUB_BLINK_MS (задаётся компилятору), чтобы по частоте
* мигания визуально отличить, какой слот реально выбрал bootloader. См.
* firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2".
*
* Фаза 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
* write-once (выключить нельзя). Поэтому заглушка, если сконфигурирована
* его кормить (STUB_FEED_WDOG=1, дефолт), обязана делать это в главном цикле
* иначе WDOG сбросит плату через таймаут (см. bsp/wdog/README.md).
*/
#include "board.h"
#include "bootutil/bootutil_public.h"
#include "bsp/boot_state.h"
#include "bsp/led.h"
#include "bsp/tick.h"
#include "bsp/wdog.h"
#include "flash_map.h"
#include <stdbool.h>
#ifndef STUB_BLINK_MS
#error "STUB_BLINK_MS must be defined (see firmware/bootloader/test_stub/CMakeLists.txt)"
#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)
{
board_hw_init();
bsp_led_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)
{
#if STUB_FEED_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_delay(STUB_BLINK_MS);
}
}

View file

@ -29,7 +29,6 @@
- [Как добавить новый тест](#как-добавить-новый-тест)
- [Host unit-тесты](#host-unit-тесты)
- [Версионирование](#версионирование)
- [Архитектурные решения (закрыты)](#архитектурные-решения-закрыты)
---
@ -199,16 +198,16 @@ main.c
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
| Тест | BSP модуль |
| -------------- | ------------------------------ |
| `test_sdram` | `bsp_sdram` |
| `test_qspi` | `bsp_qspi_flash` |
| Тест | BSP модуль |
| -------------- | ---------------------------------- |
| `test_sdram` | `bsp_sdram` |
| `test_qspi` | `bsp_qspi_flash` |
| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) |
| `test_display` | `bsp_display` |
| `test_buttons` | `bsp_button` |
| `test_opto` | `bsp_opto` (rs_as_gpio=true) |
| `test_can` | `bsp_can` |
| `test_mqs` | `bsp_mqs` |
| `test_display` | `bsp_display` |
| `test_buttons` | `bsp_button` |
| `test_opto` | `bsp_opto` (rs_as_gpio=true) |
| `test_can` | `bsp_can` |
| `test_mqs` | `bsp_mqs` |
> `bsp_uart_host` также линкуется (используется вне тест-реестра); отдельного
> UART-тест-модуля в текущем реестре нет (тестируется в `tests/target`).
@ -271,7 +270,7 @@ main.c
| Интерфейс | USB CDC ACM, разъём J2 |
| Кодировка | UTF-8 |
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) |
| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) |
| CR+LF | Принимается (таргет отбрасывает `\r`) |
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
@ -551,16 +550,16 @@ confirm id для pre-confirm равен id теста (`usd`) — механи
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
| --- | --------- | ------------------ | ---------------------------- | -------- | ------ | --------------- |
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() |
| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only |
| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) |
| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) |
| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() |
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
| --- | --------- | ------------------ | ----------- | -------- | ------ | ------------- |
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() |
| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only |
| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) |
| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) |
| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() |
**Типы confirm:**
@ -823,4 +822,4 @@ session_start / version_response: "fw":"0.1.2"
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`.
##

View file

@ -232,10 +232,58 @@ hab-verify project="firmware_test" type="release":
exit 1 ;;
esac
OUT="/tmp/hab_parse_{{ project }}_{{ type }}.yaml"
cd "{{ TOOLS_DIR }}" && uv run nxpimage hab parse -b "${BIN}" -o "${OUT}"
cd "{{ TOOLS_DIR }}" && uv run nxpimage hab parse -f mimxrt1050 -b "${BIN}" -o "${OUT}"
echo " ✅ ${OUT}"
grep -E "(entry|csf|tag)" "${OUT}" || true
# =============================================================================
# ГРУППА: mcuboot-stub — заглушка tft_app для аппаратной верификации bootutil
# (Фаза 2) и recovery-логики (Фаза 6, firmware/bootloader/PLAN.md). Временное —
# удалить вместе с firmware/bootloader/test_stub/, когда появится реальный
# firmware/tft_app.
# =============================================================================
MCUBOOT_ROOT := justfile_directory() / 'sdk/middleware/mcuboot_opensource'
MCUBOOT_KEY := MCUBOOT_ROOT / 'root-ec-p256.pem'
MCUBOOT_IMGTOOL := MCUBOOT_ROOT / 'scripts/imgtool.py'
# imgtool не входит в зависимости tools/host (spsdk) — ставится эфемерно через uv --with,
# не трогая основной venv (см. firmware/bootloader/PLAN.md, "Аппаратная верификация Фазы 2").
MCUBOOT_UV := 'uv run --with cryptography --with intelhex --with click --with cbor2 --with pyyaml python3'
[doc('Собрать и подписать заглушки Slot A/Б для аппаратной верификации bootutil и recovery (Debug)')]
[group('mcuboot-stub')]
build-mcuboot-stub: _configure-debug
cmake --build --preset mcuboot-stub-debug
mkdir -p "{{ BUILD_DIR }}/Debug/signed"
{{ 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.bin" "{{ BUILD_DIR }}/Debug/signed/stub_a_v1_confirmed.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.bin" "{{ BUILD_DIR }}/Debug/signed/stub_b_v2_confirmed.bin"
{{ 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.bin" "{{ BUILD_DIR }}/Debug/signed/stub_a_v1_unconfirmed.bin"
# Фаза 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
# =============================================================================

View file

@ -604,6 +604,16 @@ package-tui:
echo " Бандл TUI без Debug-образа нерабочий (см. README/DEV_ARCH.md)."
exit 1
fi
# Flasher.PRODUCTION (Фаза 5) резолвит bootloader ЖЁСТКО из Release, без
# фоллбэка на Debug (см. flasher.py) — в отличие от firmware_test выше,
# здесь недостаточно "хоть какой-то hab нашёлся", нужен именно этот файл,
# иначе «Серийная прошивка» в готовом бандле молча падает в рантайме.
if [[ ! -f "{{ _prod_build_dir }}/Release/bootloader_hab.bin" ]]; then
echo " ❌ Не найден {{ _prod_build_dir }}/Release/bootloader_hab.bin — соберите"
echo " заранее (just build::hab-bootloader-release)."
echo " Серийная прошивка в бандле TUI без него не работает."
exit 1
fi
VERSION=$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
case "$(uname -s)" in

View file

@ -62,6 +62,8 @@ add_sdk_driver(sai_edma fsl_sai_edma.c)
add_sdk_driver(edma fsl_edma.c)
add_sdk_driver(dmamux fsl_dmamux.c)
add_sdk_driver(xbara fsl_xbara.c)
add_sdk_driver(wdog fsl_wdog.c)
add_sdk_driver(src fsl_src.c)
# Драйверы которые зависят от clock
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)

View file

@ -65,6 +65,7 @@ __attribute__((used))
__attribute__((noinline))
void fih_panic_loop(void)
{
#if defined(__arm__)
__asm volatile ("b fih_panic_loop");
__asm volatile ("b fih_panic_loop");
__asm volatile ("b fih_panic_loop");
@ -74,5 +75,17 @@ void fih_panic_loop(void)
__asm volatile ("b fih_panic_loop");
__asm volatile ("b fih_panic_loop");
__asm volatile ("b fih_panic_loop");
#else
/* Host-порт (см. tests/host/mcuboot_port/): "b fih_panic_loop" — валидная
* мнемоника только для ARM/Thumb. На x86_64 ассемблер падает ("invalid
* instruction mnemonic 'b'") — отсюда падение test-host-release в CI
* (x86_64-раннер), которого нет в devcontainer на arm64 (там та же
* мнемоника случайно ассемблируется, но не несёт смысла это не
* реальный fault-injection путь, а хостовая сборка). Реальный ARM-таргет
* (__arm__ определён) использует оригинальный код выше без изменений. */
for (;;)
{
}
#endif
}
#endif /* FIH_ENABLE_GLOBAL_FAIL */

View file

@ -218,3 +218,99 @@ add_host_test(
${BSP_MOCKS_DIR})
target_compile_definitions(test_firmware_runner PRIVATE UNIT_TEST)
# -----------------------------------------------------------------------------
# bootutil (MCUboot Direct-XIP) — выбор слота. Реальный bootutil + TinyCrypt
# поверх fake_flash_map_backend.c (in-memory буфер вместо bsp_qspi_flash).
# См. firmware/bootloader/PLAN.md, Фаза 2.
# -----------------------------------------------------------------------------
include(${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cmake)
add_host_test(
NAME
test_mcuboot_boot_select
SOURCES
mcuboot_port/test_boot_select.c
mcuboot_port/fake_flash_map_backend.c
mcuboot_port/host_link_shims.c
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/keys.c
${MCUBOOT_BOOTUTIL_SOURCES}
INCLUDES
${MCUBOOT_BOOTUTIL_INCLUDES}
${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port)
target_compile_definitions(
test_mcuboot_boot_select
PRIVATE FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/fixtures")
target_compile_options(test_mcuboot_boot_select PRIVATE -w)
# -----------------------------------------------------------------------------
# update_policy — чистая логика решения install/skip + целевой слот (Фаза 3,
# SD-путь установки). Нужен только заголовок bootutil/image.h (struct
# image_version) — без линковки самого bootutil, но fault_injection_hardening.h
# (включается из image.h) требует mcuboot_config/mcuboot_config.h в include-
# путях, поэтому переиспользуем MCUBOOT_BOOTUTIL_INCLUDES целиком (уже задан
# выше через bootutil_sources.cmake).
# -----------------------------------------------------------------------------
add_host_test(
NAME
test_update_policy
SOURCES
update_policy/test_update_policy.c
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/update_policy.c
INCLUDES
${PROJECT_SOURCE_DIR}/firmware/bootloader/src
${MCUBOOT_BOOTUTIL_INCLUDES})
# -----------------------------------------------------------------------------
# recovery — чистая логика решения "что делать с этой попыткой загрузки"
# (Фаза 6, разбивка 6a). Переиспользует update_policy_slot_state_t/
# image_version_compare() из update_policy.c — тот же набор INCLUDES.
# -----------------------------------------------------------------------------
add_host_test(
NAME
test_recovery
SOURCES
recovery/test_recovery.c
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/recovery.c
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/update_policy.c
INCLUDES
${PROJECT_SOURCE_DIR}/firmware/bootloader/src
${MCUBOOT_BOOTUTIL_INCLUDES})
# -----------------------------------------------------------------------------
# slot_version — read-only пик версии слота через bootutil_img_validate()
# (Фаза 3). Реальный bootutil + TinyCrypt поверх fake_flash_map_backend.c —
# переиспользует те же фикстуры, что и test_mcuboot_boot_select (Фаза 2).
# -----------------------------------------------------------------------------
add_host_test(
NAME
test_slot_version
SOURCES
slot_version/test_slot_version.c
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/slot_version.c
mcuboot_port/fake_flash_map_backend.c
mcuboot_port/host_link_shims.c
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/keys.c
${MCUBOOT_BOOTUTIL_SOURCES}
INCLUDES
${PROJECT_SOURCE_DIR}/firmware/bootloader/src
${MCUBOOT_BOOTUTIL_INCLUDES}
${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port)
target_compile_definitions(
test_slot_version PRIVATE FIXTURES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/mcuboot_port/fixtures")
target_compile_options(test_slot_version PRIVATE -w)
# slot_version.c зовёт FIH_CALL(bootutil_img_validate, ...) напрямую — тот же
# баг clang 22.1.8 (Homebrew) с генерацией CFI-директив под ASan/UBSan, что
# уже обойдён для вендоренных файлов bootutil в bootutil_sources.cmake
# ("invalid CFI advance_loc expression"); здесь тот же обход для нашего
# файла, единственного, где эта макро-развёртка встречается вне bootutil.
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
set_source_files_properties(
${PROJECT_SOURCE_DIR}/firmware/bootloader/src/slot_version.c
PROPERTIES COMPILE_OPTIONS "-fno-sanitize=address,undefined")
endif()

View file

@ -0,0 +1,56 @@
# test_bsp_button
## Модуль под тестом
`bsp/button/src/button.c` (`bsp/button/include/bsp/button.h`)
## Категория
B — BSP-модуль, зависит от `fsl_gpio.h`. Unity + fff.
## Моки
- `GPIO_ReadPinInput` — единственная SDK-функция, вызываемая `button.c`,
fff value-фейк. Управляется напрямую (`return_val`) для одной кнопки или
через `SET_RETURN_SEQ` для чередования значений нескольких кнопок в одном
`bsp_button_poll()`.
- `GPIO_PinInit` не мокируется — `bsp_button_init()` не трогает GPIO
напрямую.
## Что проверяется
- **Инициализация**`bsp_button_init()` возвращает `BSP_OK`; повторный
вызов сбрасывает накопленное состояние (состояние нажатия и события).
- **Сырое чтение** (`bsp_button_read`) — LOW = нажата, HIGH = не нажата,
невалидный индекс безопасно возвращает `false`.
- **Debounce на нажатие** — событие не срабатывает до порога в 4 стабильных
сэмпла, срабатывает на 4-м, состояние остаётся стабильным при удержании,
повторных событий на удержании нет.
- **Потребление события** — флаг события сбрасывается после первого чтения
(`get_event_pressed`/`get_event_released`).
- **Debounce на отпускание** — аналогичная логика для перехода в
«отпущено».
- **Сброс счётчика при глитче** — единичный сэмпл противоположного уровня
посреди серии сбрасывает накопленный счётчик debounce.
- **Независимость кнопок**`BSP_BUTTON_1` и `BSP_BUTTON_2` не влияют друг
на друга при чередующихся значениях GPIO.
- **Граничные индексы** — некорректный `bsp_button_t` не приводит к падению
ни в одной публичной функции.
## Гарантии
- Debounce требует ровно 4 подряд идущих одинаковых сэмпла для регистрации
перехода состояния.
- Любой единичный «дребезг» (глитч) сбрасывает накопленный счётчик — не
засчитывается частично.
- События нажатия/отпускания одноразовые: второй подряд вызов `get_event_*`
без нового перехода возвращает `false`.
- Кнопки полностью независимы друг от друга.
- Невалидный индекс кнопки не вызывает падения — все геттеры возвращают
безопасное значение по умолчанию.
## Запуск
```bash
ctest --preset host-debug-test -R test_bsp_button -V
```

66
tests/host/can/README.md Normal file
View file

@ -0,0 +1,66 @@
# test_bsp_can
## Модуль под тестом
`bsp/can/src/can.c` + `utils/ring_buffer/ring_buffer.c` (реальный, не
мокается — используется как внутренняя зависимость `can.c`).
## Категория
B — BSP-модуль, зависит от FlexCAN SDK (`fsl_flexcan.h`, `fsl_clock.h`).
Unity + fff.
## Моки
Набор fff-фейков SDK FlexCAN: `FLEXCAN_GetDefaultConfig`,
`FLEXCAN_CalculateImprovedTimingValues`, `FLEXCAN_Init`, `FLEXCAN_Deinit`,
`FLEXCAN_SetTxMbConfig`, `FLEXCAN_SetRxMbConfig`,
`FLEXCAN_SetRxIndividualMask`, `FLEXCAN_WriteTxMb`, `FLEXCAN_ReadRxMb`,
`FLEXCAN_GetMbStatusFlags`, `FLEXCAN_ClearMbStatusFlags`, а также
`bsp_tick_get_ms` (управляемое время для тестов таймаутов) и
`CLOCK_EnableClock` (ERRATA 50235 workaround в `bsp_can_init()`).
Кастомные `custom_fake`: `read_rx_mb_inject` (подставляет заранее
подготовленный RX-фрейм), `get_mb_flags_once` (флаг готовности только на
первый вызов), `tick_advancing` (линейно растущее время для таймаутов),
`capture_tx_frame` (захват TX-фрейма для проверки конвертации ID/данных).
## Что проверяется
- **Init / Deinit** — успешная инициализация; отклонение `NULL`-конфига,
нулевого и слишком высокого bitrate, ошибки расчёта тайминга; повторная
инициализация вызывает `Deinit` перед новым `Init`; `deinit` без `init`
— no-op.
- **TX** (`bsp_can_send`) — успешная отправка, `NULL`-фрейм, `dlc > 8`,
отправка без инициализации, занятый MB (`BSP_ERR_BUSY`), таймаут
ожидания готовности (`BSP_ERR_TIMEOUT`).
- **Фильтрация** (`bsp_can_set_filter` / `bsp_can_accept_all`) — STD/EXT
ID, индекс фильтра вне диапазона, вызов без инициализации,
`accept_all` настраивает минимум 2 RX MB и деактивирует ранее
настроенные фильтры.
- **RX** (`bsp_can_receive`) — успешный приём STD/EXT фрейма, таймаут,
`NULL`-указатель, приём без инициализации, неблокирующий опрос при
`timeout_ms == 0`.
- **Callback-заглушка**`bsp_can_register_rx_callback` возвращает
`BSP_ERR_NOT_SUPPORTED`.
- **Конвертация фреймов** — корректность кодирования STD/EXT ID
(`FLEXCAN_ID_STD`/`FLEXCAN_ID_EXT`) и порядка байт данных при переводе
между `bsp_can_frame_t` и SDK `flexcan_frame_t`.
## Гарантии
- Все публичные функции проверяют, что модуль инициализирован, и
возвращают `BSP_ERR_PARAM` в противном случае.
- Параметры валидируются перед обращением к SDK: `dlc <= 8`, bitrate в
допустимом диапазоне, индекс фильтра в пределах `BSP_CAN_FILTER_MAX`.
- TX и RX корректно завершаются по таймауту (`BSP_ERR_TIMEOUT`), если MB не
становится готовым.
- STD/EXT кодирование идентификатора и порядок байт данных не искажаются
при конвертации между форматами приложения и SDK.
- `bsp_can_accept_all()` деактивирует ранее настроенные пользовательские
фильтры перед установкой приёма «всех кадров».
## Запуск
```bash
ctest --preset host-debug-test -R test_bsp_can -V
```

55
tests/host/cli/README.md Normal file
View file

@ -0,0 +1,55 @@
# test_cli
## Модуль под тестом
`firmware/test/src/cli.c` — построчный разбор JSON-протокола от USB CDC и
диспетчеризация команд.
## Категория
B — зависит от `bsp_usb_cdc`, `protocol_send_*`, `test_runner_*`,
`bsp_prov_read_uid`, подменяемых через fff.
## Моки
`bsp_usb_cdc_write`/`bsp_usb_cdc_read` (чтение эмулируется хелпером
`inject()`, который наполняет внутренний буфер «как будто USB» и вызывает
`cli_process()`), `protocol_send_pong`, `protocol_send_error`,
`protocol_send_uid_response`, `protocol_send_version_response`,
`test_runner_run_all`, `test_runner_run_single`, `test_runner_run_selected`,
`test_runner_on_confirm`, `test_runner_send_list`, `bsp_prov_read_uid`.
Кастомные `custom_fake` (`capture_run_single`, `capture_on_confirm`,
`capture_uid_response`, `capture_run_selected`) копируют строковые
аргументы по значению, т.к. `cli.c` передаёt указатели на собственные
локальные буферы, которые становятся dangling после возврата
`cli_process()`.
## Что проверяется
- **Команды** (`type: cmd`) — `ping`→pong, `run_all`→запуск всех,
`run`+`id`→запуск одного с передачей id, `get_uid` (успех и ошибка
чтения UID), `get_version`, `list_tests`, `run_selected` с массивом id.
- **Подтверждения** (`type: confirm`) — диспетчеризация `confirmed:
true/false` с корректным id.
- **Ошибки протокола** — отсутствует `type` или обязательное поле
(`id`/`confirmed`/`tests`) → `PARSE_ERR`; неизвестный `type`/`cmd` →
`UNKNOWN_CMD`; строка длиннее `CLI_LINE_BUF_SIZE``LINE_TOO_LONG`.
- **Построчный ввод** — пустая строка игнорируется, `\r\n` обрабатывается
как `\n`, две команды в одном чтении диспетчеризуются обе по отдельности.
## Гарантии
- Каждая валидная команда приводит ровно к одному вызову соответствующего
обработчика (`test_runner_*`/`protocol_send_*`).
- Любая ошибка парсинга или неизвестная команда отправляет ровно один
`protocol_send_error()` с точным, стабильным кодом ошибки.
- Построчный парсер корректно разделяет несколько команд в одном чтении и
не путает `\r\n` и `\n`.
- Переполнение буфера строки не приводит к падению — возвращается
контролируемая ошибка `LINE_TOO_LONG`.
## Запуск
```bash
ctest --preset host-debug-test -R test_cli -V
```

42
tests/host/led/README.md Normal file
View file

@ -0,0 +1,42 @@
# test_bsp_led
## Модуль под тестом
`bsp/led/src/led.c` (`bsp/led/include/bsp/led.h`)
## Категория
B — BSP-модуль, зависит от `fsl_gpio.h`. Unity + fff.
## Моки
- `GPIO_PinInit` — фейк, конфигурация захватывается через `custom_fake`
(`GPIO_PinInit_capture`) и копируется по значению, т.к. `gpio_pin_config_t`
живёт на стеке `bsp_led_init()` и становится dangling после возврата.
- `GPIO_PinWrite` — фейк, проверяется последний переданный уровень
(`arg2_val`).
## Что проверяется
- **Инициализация**`GPIO_PinInit` вызывается для каждого LED, пины
настраиваются как output, начальный уровень HIGH (LED выключен), оба LED
выключены сразу после `bsp_led_init()`.
- **on / off**`bsp_led_on()`/`bsp_led_off()` меняют логическое состояние и
пишут в GPIO инвертированный уровень.
- **toggle** — переключение из off→on, on→off, двойной toggle возвращает
исходное состояние.
- **set**`bsp_led_set(led, bool)` включает/выключает по явному флагу.
- **Независимость** — состояние `LED_HEARTBEAT` и `LED_APP` не пересекается.
## Гарантии
- Полярность active-LOW: `on` → GPIO=0, `off` → GPIO=1.
- После инициализации оба LED гарантированно выключены.
- `toggle`, применённый чётное число раз, возвращает исходное состояние.
- LED-каналы независимы друг от друга.
## Запуск
```bash
ctest --preset host-debug-test -R test_bsp_led -V
```

View file

@ -0,0 +1,58 @@
# test_log
## Модуль под тестом
`utils/log/log.c` (`utils/log/log.h`)
## Категория
A с fff-хуками — `log.c` не зависит от `fsl_*.h`, но имеет weak-хуки
(mutex, timestamp), которые в тесте переопределяются строгими fff-фейками;
линковщик выбирает их поверх слабых реализаций из `log.c` автоматически.
## Моки
- `log_mutex_init` / `log_mutex_lock` / `log_mutex_unlock` — fff void-фейки,
переопределяющие weak-реализации. Для проверки порядка вызовов lock/unlock
используют `custom_fake`, инкрементирующий общий монотонный счётчик.
- `log_get_timestamp_ms` — fff value-фейк, управляет значением метки времени
в выводе.
- `capture_cb` — тестовый callback вместо реального UART-транспорта:
сохраняет содержимое и длину каждого вызова в `g_s_capture` для проверки
через `strstr()`.
## Что проверяется
- **Init** — отсутствие вывода до `log_init()` и при `NULL`-callback;
callback вызывается один раз на каждый `LOG_*`; повторный `log_init()`
заменяет предыдущий callback.
- **Output** — наличие символа уровня (`[E]`/`[W]`/`[I]`/`[D]`/`[V]`), тега,
текста сообщения (включая форматирование с аргументами) и `\r\n` в конце
строки.
- **Timestamp**`log_get_timestamp_ms()` вызывается ровно один раз на
каждый `LOG_*`, возвращённое значение попадает в вывод.
- **Overflow** — строка длиннее `LOG_BUF_SIZE` (256 байт) обрезается,
callback не получает больше `LOG_BUF_SIZE` байт, `\r\n` сохраняется в
конце даже при обрезке.
- **Mutex** — порядок `lock → callback → unlock` проверяется через
монотонный счётчик последовательности; lock/unlock вызываются ровно один
раз на лог.
- **Context** — указатель контекста, переданный в `log_init()`, доходит до
callback без изменений (`NULL` и ненулевой указатель).
## Гарантии
- Без вызова `log_init()` (или с `NULL`-callback) логгер не производит
вывод.
- Вывод никогда не превышает `LOG_BUF_SIZE` байт и всегда заканчивается
`\r\n`, даже при переполнении входной строки.
- Каждый лог-вызов оборачивается ровно одной парой lock/unlock, callback
вызывается строго между ними.
- Контекст, переданный в `log_init()`, не искажается при передаче в
callback.
## Запуск
```bash
ctest --preset host-debug-test -R test_log -V
```

View file

@ -0,0 +1,60 @@
# test_mcuboot_boot_select
## Модуль под тестом
Выбор загрузочного слота реальным `bootutil` (MCUboot Direct-XIP + Revert,
см. `firmware/bootloader/PLAN.md`, Фаза 2). В тесте линкуется настоящий
`bootutil` (`loader.c`, `image_validate.c`, `tlv.c`, ...) и TinyCrypt —
тестируется реальная проверка подписи/версии/TLV, а не заглушка
криптографии.
## Категория
B — интеграционный host-тест: реальная библиотека `bootutil` собирается
поверх фейкового flash-бэкенда вместо `bsp_qspi_flash`.
## Моки
- `fake_flash_map_backend.c/.h` — полноценная тестовая реализация контракта
`flash_map.h` поверх статического буфера в памяти хоста (два смежных
слота по `FAKE_FLASH_SLOT_SIZE` = 32 KB) вместо `bsp_qspi_flash`. Это
фейк, а не fff-мок: `fake_flash_reset()` стирает оба слота в `0xFF`,
`fake_flash_write_slot()` записывает подготовленный образ в нужный слот.
- `host_link_shims.c` — заглушки символов, недостающих только при
линковке `bootutil` на хосте (`fih_panic_loop` под Mach-O ABI на macOS,
недостижимый в ECDSA-only сборке `mbedtls_mpi_read_binary`); не участвуют
в тестируемой логике выбора слота.
- `fixtures/*.bin` — реальные, подписанные `imgtool` тестовым ключом
MCUboot образы (`valid_v1`, `valid_v2`, `valid_v2_unconfirmed`,
`corrupt_v1`), загружаются в фейковый flash перед вызовом `boot_go()`.
## Что проверяется
- Валиден только Slot A → выбирается Slot A.
- Валидны оба слота, версия образа в Slot Б выше → выбирается Slot Б.
- Slot Б повреждён (битый хэш/подпись) → игнорируется, выбирается
валидный Slot A.
- Оба слота пусты/невалидны → `boot_go()` возвращает ошибку.
- Direct-XIP Revert: образ выбран, но ни разу не подтверждён
(`boot_set_confirmed()` не вызывался) → при следующей загрузке `bootutil`
стирает слот, повторный `boot_go()` не находит образ.
## Гарантии
- `bootutil` выбирает валидный образ с максимальной версией среди слотов.
- Образы с некорректной подписью/хэшем не выбираются и не приводят к
падению — они просто игнорируются в пользу валидного слота.
- Отсутствие валидного образа в обоих слотах даёт явную ошибку `boot_go()`,
а не неопределённое поведение.
- Неподтверждённый (unconfirmed) Direct-XIP образ откатывается (слот
стирается) при следующей загрузке — anti-brick гарантия механизма
Revert.
## Запуск
```bash
ctest --preset host-debug-test -R test_mcuboot_boot_select -V
```
Путь к фикстурам (`FIXTURES_DIR`) прокидывается автоматически через
`target_compile_definitions` в `tests/host/CMakeLists.txt`.

View file

@ -0,0 +1,189 @@
/**
* @file fake_flash_map_backend.c
* @brief Реализация flash_map.h поверх статического буфера в памяти хоста.
* См. fake_flash_map_backend.h.
*/
#include "fake_flash_map_backend.h"
#include "flash_map.h"
#include "sysflash/sysflash.h"
#include <string.h>
#define ERASED_VAL 0xFFU
/** @brief Slot A на [0, FAKE_FLASH_SLOT_SIZE), Slot Б сразу за ним. */
static uint8_t g_s_flash_buf[2U * FAKE_FLASH_SLOT_SIZE];
static const struct flash_area g_s_areas[2] = {
{ .fa_id = 0U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U,
.fa_off = 0U, .fa_size = FAKE_FLASH_SLOT_SIZE },
{ .fa_id = 1U, .fa_device_id = FLASH_DEVICE_ID, .pad16 = 0U,
.fa_off = FAKE_FLASH_SLOT_SIZE, .fa_size = FAKE_FLASH_SLOT_SIZE },
};
/* ── Test-only helpers ────────────────────────────────────────────────── */
void fake_flash_reset(void)
{
memset(g_s_flash_buf, ERASED_VAL, sizeof(g_s_flash_buf));
}
void fake_flash_write_slot(int slot_idx, const uint8_t *p_data, size_t len)
{
uint8_t *p_dst = g_s_flash_buf + ((size_t) slot_idx * FAKE_FLASH_SLOT_SIZE);
memcpy(p_dst, p_data, len);
}
/* ── flash_map.h contract ─────────────────────────────────────────────── */
int flash_device_base(uint8_t fd_id, uintptr_t *ret)
{
if (fd_id != FLASH_DEVICE_ID)
{
return -1;
}
*ret = (uintptr_t) g_s_flash_buf;
return 0;
}
int flash_area_open(uint8_t id, const struct flash_area **area)
{
if (id >= 2U)
{
return -1;
}
*area = &g_s_areas[id];
return 0;
}
void flash_area_close(const struct flash_area *area)
{
(void) area;
}
int flash_area_read(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
{
if (off + len > area->fa_size)
{
return -1;
}
memcpy(dst, g_s_flash_buf + area->fa_off + off, len);
return 0;
}
int flash_area_write(const struct flash_area *area, uint32_t off, const void *src, uint32_t len)
{
if (off + len > area->fa_size)
{
return -1;
}
memcpy(g_s_flash_buf + area->fa_off + off, src, len);
return 0;
}
int flash_area_erase(const struct flash_area *area, uint32_t off, uint32_t len)
{
if (off + len > area->fa_size)
{
return -1;
}
memset(g_s_flash_buf + area->fa_off + off, ERASED_VAL, len);
return 0;
}
uint8_t flash_area_align(const struct flash_area *area)
{
(void) area;
return 1U;
}
uint8_t flash_area_erased_val(const struct flash_area *area)
{
(void) area;
return ERASED_VAL;
}
int flash_area_read_is_empty(const struct flash_area *area, uint32_t off, void *dst, uint32_t len)
{
if (flash_area_read(area, off, dst, len) != 0)
{
return -1;
}
const uint8_t *p_buf = (const uint8_t *) dst;
for (uint32_t i = 0U; i < len; i++)
{
if (p_buf[i] != ERASED_VAL)
{
return 0;
}
}
return 1;
}
int flash_area_get_sector(const struct flash_area *fa, uint32_t off, struct flash_sector *sector)
{
if (off >= fa->fa_size)
{
return -1;
}
sector->fs_off = (off / FAKE_FLASH_SECTOR_SIZE) * FAKE_FLASH_SECTOR_SIZE;
sector->fs_size = FAKE_FLASH_SECTOR_SIZE;
return 0;
}
int flash_area_get_sectors(int fa_id, uint32_t *count, struct flash_sector *sectors)
{
const struct flash_area *fa;
uint32_t max_cnt = *count;
if (flash_area_open((uint8_t) fa_id, &fa) != 0)
{
return -1;
}
uint32_t rem_len = fa->fa_size;
*count = 0U;
while ((rem_len > 0U) && (*count < max_cnt))
{
sectors[*count].fs_off = FAKE_FLASH_SECTOR_SIZE * (*count);
sectors[*count].fs_size = FAKE_FLASH_SECTOR_SIZE;
(*count)++;
rem_len -= FAKE_FLASH_SECTOR_SIZE;
}
return 0;
}
int flash_area_id_from_multi_image_slot(int image_index, int slot)
{
switch (slot)
{
case 0:
return FLASH_AREA_IMAGE_PRIMARY(image_index);
case 1:
return FLASH_AREA_IMAGE_SECONDARY(image_index);
default:
return -1;
}
}
int flash_area_id_from_image_slot(int slot)
{
return flash_area_id_from_multi_image_slot(0, slot);
}
int flash_area_id_to_multi_image_slot(int image_index, int area_id)
{
if (area_id == FLASH_AREA_IMAGE_PRIMARY(image_index))
{
return 0;
}
if (area_id == FLASH_AREA_IMAGE_SECONDARY(image_index))
{
return 1;
}
return -1;
}

View file

@ -0,0 +1,34 @@
/**
* @file fake_flash_map_backend.h
* @brief Test-only реализация контракта flash_map.h поверх обычной памяти
* хоста заменяет bsp_qspi_flash для host-тестов bootutil (Direct-XIP
* выбор слота, см. firmware/bootloader/PLAN.md, Фаза 2).
*
* Слот A (fa_id=0) и слот Б (fa_id=1) смежные регионы одного статического
* буфера FAKE_SLOT_SIZE байт каждый, имитируют один flash-девайс с двумя
* областями (как и на реальном железе см. BOOTLOADER_FLASH_MAP.md).
*/
#ifndef FAKE_FLASH_MAP_BACKEND_H_
#define FAKE_FLASH_MAP_BACKEND_H_
#include <stddef.h>
#include <stdint.h>
#define FAKE_FLASH_SECTOR_SIZE 0x1000U /* 4 KB — как реальный W25Qxx */
#define FAKE_FLASH_SLOT_SIZE 0x8000U /* 32 KB — уменьшенный тестовый слот */
/** @brief Заполнить всю fake-флеш 0xFF (оба слота — "стёрты"). */
void fake_flash_reset(void);
/**
* @brief Записать содержимое (например, imgtool-подписанный фикстур-образ)
* в начало указанного слота.
*
* @param[in] slot_idx 0 Slot A, 1 Slot Б.
* @param[in] p_data Данные образа.
* @param[in] len Длина данных, <= FAKE_FLASH_SLOT_SIZE.
*/
void fake_flash_write_slot(int slot_idx, const uint8_t *p_data, size_t len);
#endif /* FAKE_FLASH_MAP_BACKEND_H_ */

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,47 @@
/**
* @file host_link_shims.c
* @brief Заглушки символов, недостающих только при линковке bootutil на
* хосте, но не на реальном ARM-таргете. Не часть mcuboot_port/
* (портируемого слоя) специфично для host-тестов.
*
* 1. fih_panic_loop() тело в bootutil/src/fault_injection_hardening.c
* написано как ARM inline asm, self-reference по имени без подчёркивания
* ("b fih_panic_loop"). На arm-none-eabi-gcc (реальный таргет) это
* резолвится нативно там C-символы не манглятся подчёркиванием.
*
* На хосте картина зависит от ABI, а не просто от "это host-тест":
* - Mach-O (macOS): C-функция "fih_panic_loop" компилируется в символ
* "_fih_panic_loop" inline asm ищет ровно "fih_panic_loop" без
* подчёркивания и не находит. Нужен явный символ через GNU asm-label
* (см. ниже, только под __APPLE__).
* - ELF (Linux, напр. devcontainer clang-17): C-символы НЕ манглятся
* подчёркиванием "fih_panic_loop" резолвится сам на себя нативно,
* как и на ARM. Наш шим здесь не нужен и создаёт konфликт
* ("multiple definition") с уже существующим определением в
* fault_injection_hardening.c поэтому строго под __APPLE__.
*
* 2. mbedtls_mpi_read_binary() используется только mbedtls_asn1_get_mpi()
* (RSA-путь ASN.1, ext/mbedtls-asn1/src/asn1parse.c), недостижимо в нашей
* ECDSA-only конфигурации. На ARM-таргете --gc-sections вырезает мёртвый
* вызов до того, как он потребует символ; host-линковка (Mach-O и ELF
* одинаково) без --gc-sections требует явного разрешения недостижимая
* по рантайму заглушка, платформенно-независима.
*/
#if defined(__APPLE__)
void fih_panic_loop_impl(void) asm("fih_panic_loop");
void fih_panic_loop_impl(void)
{
for (;;)
{
}
}
#endif /* __APPLE__ */
int mbedtls_mpi_read_binary(void *X, const unsigned char *buf, unsigned long len)
{
(void) X;
(void) buf;
(void) len;
return -1; /* недостижимо — ECDSA-only, RSA-путь ASN.1 не вызывается */
}

View file

@ -0,0 +1,149 @@
/**
* @file test_boot_select.c
* @brief Host-тесты выбора слота bootutil (MCUboot Direct-XIP + Revert).
*
* Реальный bootutil (loader.c, image_validate.c, tlv.c, ...) + TinyCrypt
* поверх fake_flash_map_backend.c (in-memory буфер вместо bsp_qspi_flash).
* Фикстуры в fixtures/ подписаны настоящим imgtool тестовым ключом
* MCUboot (root-ec-p256.pem, см. mcuboot_port/keys/bootloader_test_ecdsa_pub.c)
* тестируется реальная проверка подписи/версии/TLV, не мок.
*
* Сценарии firmware/bootloader/PLAN.md, Фаза 2:
* 1. Валиден только Slot A выбран A.
* 2. Оба валидны, версия Б выше выбран Б.
* 3. Slot Б повреждён (битый хэш/подпись) игнорируется, выбран A.
* 4. Оба слота пусты/невалидны boot_go() возвращает ошибку (триггер
* top-level состояния "нет образа" из Фазы 3).
* 5. Direct-XIP Revert: образ выбран, но ни разу не confirmed при
* следующей загрузке bootutil стирает слот и boot_go() проваливается.
*/
#include "unity.h"
#include "fake_flash_map_backend.h"
#include "bootutil/bootutil.h"
#include "bootutil/fault_injection_hardening.h"
#include <stdio.h>
#include <string.h>
#ifndef FIXTURES_DIR
#error "FIXTURES_DIR must be defined by CMake (see tests/host/CMakeLists.txt)"
#endif
static uint8_t g_s_fixture_buf[FAKE_FLASH_SLOT_SIZE];
/**
* @brief Загрузить фикстур-файл в g_s_fixture_buf.
* @return Число прочитанных байт.
*/
static size_t load_fixture(const char *p_name)
{
char path[256];
(void) snprintf(path, sizeof(path), "%s/%s", FIXTURES_DIR, p_name);
FILE *p_file = fopen(path, "rb");
TEST_ASSERT_NOT_NULL_MESSAGE(p_file, path);
size_t n = fread(g_s_fixture_buf, 1U, sizeof(g_s_fixture_buf), p_file);
(void) fclose(p_file);
TEST_ASSERT_EQUAL_UINT32(FAKE_FLASH_SLOT_SIZE, n);
return n;
}
void setUp(void)
{
fake_flash_reset();
}
void tearDown(void)
{
}
/* ── Сценарий 1 — валиден только Slot A ──────────────────────────────── */
void test_boot_go_slot_a_only_valid(void)
{
fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin"));
struct boot_rsp rsp;
fih_ret fih_rc = boot_go(&rsp);
TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS));
TEST_ASSERT_EQUAL_UINT32(0U, rsp.br_image_off);
}
/* ── Сценарий 2 — оба валидны, побеждает более новая версия ──────────── */
void test_boot_go_picks_higher_version(void)
{
fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin"));
fake_flash_write_slot(1, g_s_fixture_buf, load_fixture("valid_v2.bin"));
struct boot_rsp rsp;
fih_ret fih_rc = boot_go(&rsp);
TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS));
TEST_ASSERT_EQUAL_UINT32(FAKE_FLASH_SLOT_SIZE, rsp.br_image_off);
}
/* ── Сценарий 3 — повреждённый Slot Б игнорируется ───────────────────── */
void test_boot_go_ignores_corrupted_slot(void)
{
fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v1.bin"));
fake_flash_write_slot(1, g_s_fixture_buf, load_fixture("corrupt_v1.bin"));
struct boot_rsp rsp;
fih_ret fih_rc = boot_go(&rsp);
TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS));
TEST_ASSERT_EQUAL_UINT32(0U, rsp.br_image_off);
}
/* ── Сценарий 4 — оба слота пусты → нет загружаемого образа ──────────── */
void test_boot_go_no_valid_image(void)
{
/* fake_flash_reset() в setUp уже оставил оба слота стёртыми (0xFF) */
struct boot_rsp rsp;
fih_ret fih_rc = boot_go(&rsp);
TEST_ASSERT_FALSE(FIH_EQ(fih_rc, FIH_SUCCESS));
}
/* ── Сценарий 5 — Direct-XIP Revert: неподтверждённый образ стирается ── */
void test_boot_go_reverts_unconfirmed_image(void)
{
fake_flash_write_slot(0, g_s_fixture_buf, load_fixture("valid_v2_unconfirmed.bin"));
struct boot_rsp rsp;
/* Первая загрузка: образ валиден, выбран, но boot_set_confirmed() никто
* не вызвал (симулируем что tft_app не подтвердила себя). */
fih_ret fih_rc = boot_go(&rsp);
TEST_ASSERT_TRUE(FIH_EQ(fih_rc, FIH_SUCCESS));
/* Вторая загрузка "после перезагрузки" — bootutil видит copy_done=SET,
* image_ok не SET стирает слот и не находит образ. */
fih_rc = boot_go(&rsp);
TEST_ASSERT_FALSE(FIH_EQ(fih_rc, FIH_SUCCESS));
}
/* ── Точка входа ───────────────────────────────────────────────────────── */
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_boot_go_slot_a_only_valid);
RUN_TEST(test_boot_go_picks_higher_version);
RUN_TEST(test_boot_go_ignores_corrupted_slot);
RUN_TEST(test_boot_go_no_valid_image);
RUN_TEST(test_boot_go_reverts_unconfirmed_image);
return UNITY_END();
}

68
tests/host/opto/README.md Normal file
View file

@ -0,0 +1,68 @@
# test_bsp_opto
## Модуль под тестом
`bsp/opto/src/opto.c` (`bsp/opto/include/bsp/opto.h`)
## Категория
B — BSP-модуль, зависит от `fsl_gpio.h` и `bsp/tick.h`. Unity + fff.
## Моки
SDK GPIO/NVIC: `GPIO_PinInit`, `GPIO_PinWrite`, `GPIO_PinRead`,
`GPIO_SetPinInterruptConfig`, `GPIO_EnableInterrupts`,
`GPIO_DisableInterrupts`, `GPIO_GetPinsInterruptFlags`,
`GPIO_ClearPinsInterruptFlags`, `EnableIRQ`. Плюс BSP-зависимости:
`BOARD_InitRS_GPIO` (`board.h`-стаб), `bsp_tick_get_ms` (`bsp/tick.h`-стаб).
Кастомные `custom_fake`: захват конфигурации `GPIO_PinInit` и
`GPIO_SetPinInterruptConfig` по значению (пин + структура конфигурации).
ISR-обработчик `GPIO1_Combined_16_31_IRQHandler()` вызывается напрямую из
теста (`simulate_isr`) с заранее выставленными
`GPIO_GetPinsInterruptFlags`/`GPIO_PinRead` — реальное прерывание не
эмулируется, дёргается тот же код, что вызвало бы железо.
## Что проверяется
- **Инициализация** — число и параметры настраиваемых каналов в
зависимости от `rs_as_gpio` (2 канала IN1/IN2, либо 3 с RS), направление
всегда input, корректные номера пинов, IRQ включается на каждый канал и
глобально, `BOARD_InitRS_GPIO()` вызывается только при `rs_as_gpio ==
true`.
- **Начальный фронт для MODE_LEVEL** — если пин LOW (INACTIVE) при init,
выбирается RISING; если HIGH (ACTIVE) — FALLING.
- **Переключение фронта в ISR** (MODE_LEVEL) — после RISING фронта ISR
переключает ожидание на FALLING и обратно.
- **bsp_opto_read** — начальное состояние по уровню пина, отключённый
RS-канал и некорректный номер канала всегда дают INACTIVE.
- **bsp_opto_process — дебаунс (MODE_LEVEL)** — коллбэк не срабатывает до
истечения `debounce_ms`, срабатывает после с правильными
каналом/состоянием, не срабатывает повторно если состояние не изменилось
или уже обработано предыдущим `process()`.
- **Независимость каналов** — срабатывание одного канала не влияет на
состояние и коллбэки другого.
- **MODE_PROTO** — коллбэк вызывается синхронно прямо в ISR (не в
`process()`), после срабатывания IRQ канала отключается,
`bsp_opto_process()` не генерирует для него коллбэков,
`bsp_opto_proto_arm()` перевзводит прерывание (для PROTO) и является
no-op для LEVEL-канала, `bsp_opto_read()` для PROTO-канала всегда
INACTIVE.
## Гарантии
- Полярность active-HIGH: `raw=1``ACTIVE`, `raw=0``INACTIVE`.
- MODE_LEVEL: переходы состояния дебаунсятся (`debounce_ms`) и
подтверждаются только в `bsp_opto_process()`; ISR лишь фиксирует
«pending» и переключает направление ожидаемого фронта.
- MODE_PROTO: обрабатывается целиком в ISR, коллбэк синхронный,
`bsp_opto_process()` его не трогает.
- Каналы полностью независимы — событие на одном не искажает состояние
другого.
- Некорректный номер канала не приводит к падению, все геттеры
возвращают безопасное значение по умолчанию (`INACTIVE`).
## Запуск
```bash
ctest --preset host-debug-test -R test_bsp_opto -V
```

View file

@ -0,0 +1,56 @@
# test_prio_queue
## Модуль под тестом
`utils/prio_queue/prio_queue.c` (`utils/prio_queue/prio_queue.h`) — очередь
с приоритетами на отсортированном массиве фиксированной ёмкости, с
вытеснением наименее приоритетного элемента при переполнении.
## Категория
A — платформонезависимый модуль. Только Unity.
## Моки
Нет.
## Что проверяется
- **init** — размер 0, `peek`/`at` на пустой очереди возвращают `NULL`.
- **insert_order** — сортировка по приоритету не зависит от порядка
вставки (по возрастанию, по убыванию, вперемешку).
- **fifo** — при равных приоритетах порядок вставки сохраняется
(стабильная сортировка), включая перемешанные группы разных приоритетов.
- **full_eviction** — вставка в заполненную очередь: более приоритетный
элемент вытесняет худший (`PQ_EVICTED`), менее или равно приоритетный
отклоняется (`PQ_FULL`); размер очереди не меняется ни в одном из
случаев.
- **evict_correct** — вытесняется именно наименее приоритетный элемент,
верхушка (`peek`) остаётся корректной после вытеснения.
- **peek** — не удаляет элемент при повторных вызовах, указывает на
элемент во внутреннем хранилище очереди.
- **remove_at** — удаление первого/среднего/последнего элемента со сдвигом
остальных, no-op при индексе вне диапазона или на пустой очереди.
- **at** — доступ по индексу, `NULL` для индекса вне диапазона.
- **size** — инкремент при вставке, декремент при удалении, не меняется
при `PQ_FULL`.
- **stress** — полный цикл вставки всех элементов и последовательного
извлечения с проверкой сохранения порядка; чередование вставок с
одинаковыми и разными приоритетами.
## Гарантии
- `peek()` / `at(0)` всегда возвращает элемент с наивысшим приоритетом
(наименьшим значением согласно переданной `cmp`-функции).
- При равных приоритетах порядок извлечения строго FIFO.
- При переполнении: более приоритетная вставка вытесняет наименее
приоритетный существующий элемент (`PQ_EVICTED`), иначе отклоняется
(`PQ_FULL`) без изменения состояния очереди.
- `remove_at()` на некорректном индексе или пустой очереди — безопасный
no-op, не приводит к падению или порче данных.
## Запуск
```bash
ctest --preset host-debug-test -R test_prio_queue -V
```

View file

@ -0,0 +1,43 @@
# test_protocol
## Модуль под тестом
`firmware/test/src/protocol.c` — сериализация JSON-событий протокола
(`session_start`, `test_begin`, `test_result`, `summary`, `confirm_request`,
`pong`, `error`).
## Категория
B — зависит от `cli_send()` и `bsp_tick_get_ms()`, подменяемых через fff.
## Моки
- `cli_send` — fff void-фейк; итоговая строка захватывается через
`custom_fake` (`capture_cli_send`) в статический буфер, т.к. аргумент —
указатель на стековый буфер `protocol_send_*()`, живой только до возврата.
- `bsp_tick_get_ms` — fff value-фейк, управляет значением `uptime_ms` в
`session_start`.
- `bsp_delay` — fff void-фейк-заглушка (не используется в проверках).
## Что проверяется
Точный JSON-текст, передаваемый в `cli_send()`, для каждого типа сообщения:
`session_start` (нулевой и ненулевой uptime, версия прошивки), `test_begin`
(`critical: true/false`), `test_result` (`pass`, `fail` с деталями, `skip`),
`summary` (общий результат pass/fail), `confirm_request` (кастомный таймаут
и подстановка таймаута по умолчанию при `timeout_ms == 0`), `pong`, `error`
(разные коды ошибок).
## Гарантии
- Формат JSON точно соответствует протоколу — сравнивается побайтово с
эталонной строкой, а не только наличием отдельных полей.
- Каждый `protocol_send_*()` вызывает `cli_send()` ровно один раз.
- `confirm_request` с `timeout_ms == 0` всегда получает
`PROTOCOL_CONFIRM_TIMEOUT_MS` вместо нуля.
## Запуск
```bash
ctest --preset host-debug-test -R test_protocol -V
```

Some files were not shown because too many files have changed in this diff Show more