Compare commits
13 commits
service-tu
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6120e71416 | |||
| 6c564f1638 | |||
| acb1fed1f2 | |||
| 12bb949834 | |||
| 8cb1cb143c | |||
| cc6f29b8f4 | |||
| 894d0d56c0 | |||
| 1def117e44 | |||
| 17698ce109 | |||
| 580e5f50d9 | |||
| db2c573642 | |||
| 6a643ae790 | |||
| 4644f21f07 |
146 changed files with 11275 additions and 1095 deletions
|
|
@ -80,7 +80,7 @@ CheckOptions:
|
||||||
- key: readability-identifier-naming.PointerParameterPrefix
|
- key: readability-identifier-naming.PointerParameterPrefix
|
||||||
value: "p_" # uint8_t *p_buffer
|
value: "p_" # uint8_t *p_buffer
|
||||||
- key: readability-function-size.LineThreshold
|
- key: readability-function-size.LineThreshold
|
||||||
value: '60'
|
value: '90'
|
||||||
- key: readability-function-size.StatementThreshold
|
- key: readability-function-size.StatementThreshold
|
||||||
value: '30'
|
value: '30'
|
||||||
- key: readability-magic-numbers.IgnoredIntegerValues
|
- key: readability-magic-numbers.IgnoredIntegerValues
|
||||||
|
|
|
||||||
97
.github/workflows/release.yml
vendored
97
.github/workflows/release.yml
vendored
|
|
@ -1,17 +1,22 @@
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
# Раздельные теги (FIRST_RELEASE_PLAN.md, Шаг 2.1): firmware_test и
|
# Раздельные теги (bootloader — Фаза 5, firmware/bootloader/PLAN.md):
|
||||||
# service-tui версионируются и релизятся независимо друг от друга.
|
# firmware_test, bootloader и 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)
|
# firmware-vX.Y.Z → publish-firmware (только firmware_test_hab.bin, Debug)
|
||||||
# HAB firmware_test, вшиваемый в TUI-бандл, всегда собирается заново из
|
# bootloader-vX.Y.Z → publish-bootloader (только bootloader_hab.bin, Release,
|
||||||
# текущего HEAD тега tui-v*, а не берётся из отдельного firmware-релиза —
|
# подписан ТЕСТОВЫМ HAB-ключом — см.
|
||||||
# так проще и не тянет зависимость на чужой GitHub Release.
|
# 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:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "tui-v*"
|
- "tui-v*"
|
||||||
- "firmware-v*"
|
- "firmware-v*"
|
||||||
|
- "bootloader-v*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
release_type:
|
release_type:
|
||||||
|
|
@ -20,6 +25,7 @@ on:
|
||||||
options:
|
options:
|
||||||
- tui
|
- tui
|
||||||
- firmware
|
- firmware
|
||||||
|
- bootloader
|
||||||
default: tui
|
default: tui
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|
@ -30,11 +36,12 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
# firmware — собирает HAB Debug firmware_test. Нужна как для standalone
|
# firmware — собирает HAB Debug firmware_test И HAB Release bootloader.
|
||||||
# firmware-релиза, так и для вшивания в TUI-бандл — выполняется всегда.
|
# Нужна как для standalone firmware-/bootloader-релизов, так и для вшивания
|
||||||
|
# обоих образов в TUI-бандл (package-tui, Фаза 5) — выполняется всегда.
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
firmware:
|
firmware:
|
||||||
name: Build firmware_test HAB (Debug)
|
name: Build firmware_test HAB (Debug) + bootloader HAB (Release)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
|
||||||
|
|
@ -86,6 +93,30 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
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-релиз
|
# publish-firmware — только на тег firmware-v*, отдельный standalone-релиз
|
||||||
# HAB-образа (для tools/host/flash_usb.py и ручной прошивки, не через TUI).
|
# HAB-образа (для tools/host/flash_usb.py и ручной прошивки, не через TUI).
|
||||||
|
|
@ -116,6 +147,40 @@ jobs:
|
||||||
--title "firmware_test ${GITHUB_REF_NAME#firmware-v}" \
|
--title "firmware_test ${GITHUB_REF_NAME#firmware-v}" \
|
||||||
--generate-notes
|
--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
|
# service-tui-{macos,windows} — упаковка PyInstaller-бандла с вшитым HAB
|
||||||
# из job firmware. Запускается на тег tui-v* и на workflow_dispatch с
|
# из job firmware. Запускается на тег tui-v* и на workflow_dispatch с
|
||||||
|
|
@ -144,6 +209,12 @@ jobs:
|
||||||
name: firmware-hab-debug
|
name: firmware-hab-debug
|
||||||
path: build/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
|
- name: Проверить, что тег совпадает с версией в pyproject.toml
|
||||||
if: startsWith(github.ref, 'refs/tags/tui-v')
|
if: startsWith(github.ref, 'refs/tags/tui-v')
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -197,6 +268,12 @@ jobs:
|
||||||
name: firmware-hab-debug
|
name: firmware-hab-debug
|
||||||
path: build/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
|
- name: Проверить, что тег совпадает с версией в pyproject.toml
|
||||||
if: startsWith(github.ref, 'refs/tags/tui-v')
|
if: startsWith(github.ref, 'refs/tags/tui-v')
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -38,7 +38,7 @@
|
||||||
"interface": "swd",
|
"interface": "swd",
|
||||||
"loadFiles": [],
|
"loadFiles": [],
|
||||||
"runToEntryPoint": "main",
|
"runToEntryPoint": "main",
|
||||||
"preLaunchTask": "build-and-rtt:firmware-test-debug",
|
"preLaunchTask": "build:bootloader-debug",
|
||||||
},
|
},
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// firmware/tft_app — FreeRTOS task view
|
// firmware/tft_app — FreeRTOS task view
|
||||||
|
|
|
||||||
190
CHANGELOG.md
190
CHANGELOG.md
|
|
@ -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.
|
- Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL.
|
||||||
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
|
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
|
||||||
- Следить за развитием BSP: RGB (частично закрыто display-тестом), bootloader или `tft_app` — обе директории всё ещё не заведены.
|
- Следить за развитием BSP: RGB (частично закрыто display-тестом); `tft_app` — директория всё ещё не заведена (bootloader — реализован, Фазы 0–6, см. запись ниже).
|
||||||
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log.
|
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log (пример — точечный патч `fault_injection_hardening.c` под `#if defined(__arm__)`, bootloader Фаза 3, см. запись ниже).
|
||||||
- Отслеживать мерж ветки `feature-tui-monolith` в `dev` — после мержа эту запись нужно закрыть датой и финальным диапазоном SHA.
|
- ~~Отслеживать мерж ветки `feature-tui-monolith` в `dev`~~ — смёржено (`b4c664f`), запись закрыта датой ниже.
|
||||||
|
- Отслеживать тег `bootloader-v*` — после первого релиза закрыть запись «bootloader: полная реализация» датой и финальным SHA (сейчас часть диапазона — незакоммиченные изменения рабочего дерева).
|
||||||
|
|
||||||
## [Не выпущено] — service-tui: монолитный spsdk-бэкенд, устойчивость к обрыву USB, упаковка PyInstaller
|
## [Не выпущено] — bootloader: полная реализация, Фазы 0–6 (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()`
|
||||||
|
(таксономия отказов A–D); 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`.
|
||||||
|
- Полный аппаратный чек-лист пройден на каждой фазе (Фазы 2–6); детали были в удалённых
|
||||||
|
`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>
|
Сравнение: <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` +
|
> бинарников через subprocess (`tools/host/flash_usb.py --fcb-path` +
|
||||||
> `nxpimage`) была реализацией на момент Фазы 0/раннего мержа и с тех пор
|
> `nxpimage`) была реализацией на момент Фазы 0/раннего мержа и с тех пор
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,14 @@ if(NOT BUILD_TESTS_HOST)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_C_EXTENSIONS OFF)
|
set(CMAKE_C_EXTENSIONS OFF)
|
||||||
set(BSP_SYSCALLS_FILE
|
set(BSP_SYSCALLS_FILE
|
||||||
"${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c"
|
"${CMAKE_SOURCE_DIR}/bsp/generated/syscalls.c"
|
||||||
CACHE FILEPATH "Заглушки системных вызовов newlib")
|
CACHE FILEPATH "Заглушки системных вызовов newlib")
|
||||||
set(BSP_GENERATED
|
set(BSP_GENERATED
|
||||||
"${CMAKE_SOURCE_DIR}/bsp/generated"
|
"${CMAKE_SOURCE_DIR}/bsp/generated"
|
||||||
CACHE PATH "Путь до сгенерированных ConfigTools файлов")
|
CACHE PATH "Путь до сгенерированных ConfigTools файлов")
|
||||||
set(BSP_STARTUP_FILE
|
set(BSP_STARTUP_FILE
|
||||||
"${BSP_GENERATED}/startup/startup_MIMXRT1052.S"
|
"${BSP_GENERATED}/startup/startup_MIMXRT1052.S"
|
||||||
CACHE FILEPATH "Путь до стартап файла")
|
CACHE FILEPATH "Путь до стартап файла")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
@ -46,8 +46,10 @@ add_subdirectory(lib)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if(NOT BUILD_TESTS_HOST)
|
if(NOT BUILD_TESTS_HOST)
|
||||||
add_subdirectory(firmware/test)
|
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()
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,19 @@
|
||||||
"app"
|
"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",
|
"name": "app-release",
|
||||||
"displayName": "app — Release",
|
"displayName": "app — Release",
|
||||||
|
|
@ -150,7 +163,11 @@
|
||||||
"test_prio_queue",
|
"test_prio_queue",
|
||||||
"uart_host_mock_example",
|
"uart_host_mock_example",
|
||||||
"test_ring_buffer",
|
"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",
|
"test_prio_queue",
|
||||||
"uart_host_mock_example",
|
"uart_host_mock_example",
|
||||||
"test_ring_buffer",
|
"test_ring_buffer",
|
||||||
"test_timeout_pattern"
|
"test_timeout_pattern",
|
||||||
|
"test_mcuboot_boot_select",
|
||||||
|
"test_slot_version",
|
||||||
|
"test_update_policy",
|
||||||
|
"test_recovery"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -9,21 +9,21 @@
|
||||||
|
|
||||||
## Firmware-проекты
|
## Firmware-проекты
|
||||||
|
|
||||||
| Проект | Путь | Описание |
|
| Проект | Путь | Описание |
|
||||||
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
|
| ----------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Тестовая прошивка (реализована) | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
|
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS [README](firmware/test/README.md) |
|
||||||
| Загрузчик (запланирован) | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD |
|
| Загрузчик | `firmware/bootloader/` | A/Б обновление через uSD (MCUboot, Direct-XIP), recovery при зависании образа. Обновляется только через USB ROM + blhost / SWD [README](firmware/bootloader/README.md) |
|
||||||
| Production прошивка (запланирован) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
|
| Production прошивка (запланирована) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Инструменты (`tools/`)
|
## Инструменты (`tools/`)
|
||||||
|
|
||||||
| Инструмент | Путь | Назначение |
|
| Инструмент | Путь | Назначение |
|
||||||
| ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------- |
|
| ------------------ | -------------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
| Сервисный TUI | `tools/service_tui/` | Диагностика и прошивка готовых плат сервисным инженером [README](tools/service_tui/README.md) |
|
| Сервисный TUI | `tools/service_tui/` | Диагностика и прошивка готовых плат сервисным инженером [README](tools/service_tui/README.md) |
|
||||||
| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке [README](tools/host/README.md) |
|
| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке [README](tools/host/README.md) |
|
||||||
| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов [README](tools/hil/README.md) |
|
| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов [README](tools/hil/README.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ if(BUILD_TESTS_HOST)
|
||||||
return()
|
return()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# bsp_board — cгенерированные Config Tools файлы, инициализация платы
|
||||||
# bsp_board — генерированные файлы Config Tools, инициализация платы
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
add_library(bsp_board STATIC generated/board.c generated/pin_mux.c)
|
add_library(bsp_board STATIC generated/board.c generated/pin_mux.c)
|
||||||
|
|
||||||
add_subdirectory(common)
|
add_subdirectory(common)
|
||||||
|
|
@ -15,6 +12,8 @@ add_subdirectory(uart_host)
|
||||||
add_subdirectory(opto)
|
add_subdirectory(opto)
|
||||||
add_subdirectory(can)
|
add_subdirectory(can)
|
||||||
add_subdirectory(button)
|
add_subdirectory(button)
|
||||||
|
add_subdirectory(wdog)
|
||||||
|
add_subdirectory(boot_state)
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(usb_cdc)
|
add_subdirectory(usb_cdc)
|
||||||
add_subdirectory(sdram)
|
add_subdirectory(sdram)
|
||||||
|
|
@ -23,7 +22,6 @@ add_subdirectory(sd)
|
||||||
add_subdirectory(mqs)
|
add_subdirectory(mqs)
|
||||||
add_subdirectory(provisioning)
|
add_subdirectory(provisioning)
|
||||||
|
|
||||||
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
|
|
||||||
target_compile_options(bsp_board PRIVATE -w)
|
target_compile_options(bsp_board PRIVATE -w)
|
||||||
|
|
||||||
# SYSTEM подавляет предупреждения для всех внешних потребителей
|
# SYSTEM подавляет предупреждения для всех внешних потребителей
|
||||||
|
|
@ -66,3 +64,8 @@ target_compile_definitions(
|
||||||
|
|
||||||
add_library(bsp_boot_ram INTERFACE)
|
add_library(bsp_boot_ram INTERFACE)
|
||||||
target_compile_definitions(bsp_boot_ram INTERFACE SKIP_SYSCLK_INIT)
|
target_compile_definitions(bsp_boot_ram INTERFACE SKIP_SYSCLK_INIT)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
||||||
14
bsp/boot_state/CMakeLists.txt
Normal file
14
bsp/boot_state/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
if(BUILD_TESTS_HOST)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(bsp_boot_state STATIC src/boot_state.c)
|
||||||
|
|
||||||
|
target_include_directories(
|
||||||
|
bsp_boot_state
|
||||||
|
PUBLIC include/
|
||||||
|
PRIVATE src/)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
bsp_boot_state
|
||||||
|
PRIVATE sdk_src)
|
||||||
79
bsp/boot_state/README.md
Normal file
79
bsp/boot_state/README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# bsp_boot_state — счётчик попыток загрузки (SRC_GPR)
|
||||||
|
|
||||||
|
Счётчик попыток загрузки поверх `SRC` General Purpose Register — переживает тёплый/watchdog-сброс,
|
||||||
|
обнуляется только на POR. Даёт коду восстановления прожить несколько сбросов подряд без
|
||||||
|
персистентного хранилища во flash.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Аппаратура
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
| ------------------------ | -------------------------------------------------------------- |
|
||||||
|
| Периферия | SRC (System Reset Controller) |
|
||||||
|
| Регистр счётчика | `SRC_GPR[2]` (GPR3, 0-based индекс `fsl_src` API) |
|
||||||
|
| Переживает | тёплый сброс, watchdog-сброс |
|
||||||
|
| Обнуляется | только POR (детект — `SRC->SRSR`, бит `IPP_RESET_B`) |
|
||||||
|
| Занято ROM (не трогать) | GPR1/2 (warm-boot entry/arg), GPR6/7/8/9 (ROM, explicit note в RM), GPR10 (альт. SBMR1) |
|
||||||
|
| Не занято ROM, но занято конвенцией | GPR5 — RM рекомендует под различение SYSRESETREQ/CPU lockup, не наша задача |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контракт: разделение с `bsp_wdog`
|
||||||
|
|
||||||
|
`SRC->SRSR` и `WDOG1->WRSR` — разные регистры с разной семантикой очистки. `WRSR` самоочищается на
|
||||||
|
каждый сброс (не требует явной очистки — см. `bsp_wdog_caused_last_reset()`). `SRSR` —
|
||||||
|
write-1-to-clear и **копит биты между тёплыми сбросами**, если их не чистить софтом:
|
||||||
|
`bsp_boot_state_init()` чистит `SRSR` при каждом вызове, поэтому вопрос «был ли сброс по watchdog»
|
||||||
|
остаётся за `bsp_wdog`, а не за этим модулем — этот модуль отвечает только за «был ли сброс POR».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```c
|
||||||
|
void bsp_boot_state_init(void); /* взвести — читает/чистит SRC->SRSR, детектит POR */
|
||||||
|
bool bsp_boot_state_was_por(void); /* true, если последний сброс — POR */
|
||||||
|
uint32_t bsp_boot_attempt_count(void); /* текущее значение счётчика, 0 сразу после POR */
|
||||||
|
void bsp_boot_attempt_inc(void); /* +1 — звать перед попыткой прыжка в образ */
|
||||||
|
void bsp_boot_attempt_reset(void); /* обнулить — новый образ/фолбэк-стирание */
|
||||||
|
void bsp_boot_health_mark(void); /* = bsp_boot_attempt_reset(), для вызова из приложения */
|
||||||
|
```
|
||||||
|
|
||||||
|
`bsp_boot_state_init()` **не идемпотентна** — повторный вызов в той же сессии увидит уже очищенный
|
||||||
|
`SRSR` как «не POR». Звать ровно один раз, как можно раньше в `main()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "bsp/boot_state.h"
|
||||||
|
|
||||||
|
/* main.c — после board_hw_init()/bsp_wdog_init(): */
|
||||||
|
bsp_boot_state_init();
|
||||||
|
|
||||||
|
if (bsp_boot_attempt_count() >= THRESHOLD)
|
||||||
|
{
|
||||||
|
/* серия сбросов подряд без здорового образа — решение о фолбэке/recovery
|
||||||
|
* принимает вызывающий код, не этот модуль */
|
||||||
|
}
|
||||||
|
|
||||||
|
bsp_boot_attempt_inc(); /* перед каждой попыткой прыжка */
|
||||||
|
/* ... */
|
||||||
|
bsp_boot_health_mark(); /* приложение подтвердило собственное здоровье */
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CMake
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
target_link_libraries(firmware_bootloader PRIVATE bsp_boot_state)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Зависимости модуля:**
|
||||||
|
|
||||||
|
| Зависимость | Тип | Описание |
|
||||||
|
| ----------- | ------- | -------------------------------------------------------------- |
|
||||||
|
| `sdk_src` | PRIVATE | `fsl_src.h` — `SRC_Get/SetGeneralPurposeRegister`, `SRC_Get/ClearResetStatusFlags` |
|
||||||
59
bsp/boot_state/include/bsp/boot_state.h
Normal file
59
bsp/boot_state/include/bsp/boot_state.h
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* bsp_boot_state — счётчик попыток загрузки поверх SRC General Purpose
|
||||||
|
* Register (MIMXRT1052).
|
||||||
|
*
|
||||||
|
* Назначение: пережить несколько сбросов подряд без реального
|
||||||
|
* персистентного хранилища во flash. SRC_GPR — retained-регистр: сохраняет
|
||||||
|
* значение через тёплый/watchdog-сброс, теряет его только на POR.
|
||||||
|
*
|
||||||
|
* Контракт: bsp_boot_state_init() сама детектирует POR по SRC->SRSR и, если
|
||||||
|
* это POR, обнуляет счётчик — вызывающему коду не нужно проверять причину
|
||||||
|
* сброса самостоятельно для этой цели. Причина «сброс по watchdog?» — не
|
||||||
|
* этот модуль, см. bsp_wdog_caused_last_reset() (bsp/wdog.h): SRC->SRSR и
|
||||||
|
* WDOG1->WRSR — разные регистры с разной семантикой очистки (WRSR
|
||||||
|
* самоочищается на каждый сброс, SRSR — write-1-to-clear, накапливает биты
|
||||||
|
* между сбросами без явной очистки).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BSP_BOOT_STATE_H
|
||||||
|
#define BSP_BOOT_STATE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Инициализация. Вызывать один раз, как можно раньше в main() (после
|
||||||
|
* board_hw_init()/bsp_wdog_init()). Читает SRC->SRSR, определяет POR (бит
|
||||||
|
* IPP_RESET_B — на MIMXRT1052 отдельного бита POR нет, эту роль играет он),
|
||||||
|
* чистит SRSR (write-1-to-clear, иначе биты копятся между тёплыми сбросами)
|
||||||
|
* и, если это POR, обнуляет счётчик попыток.
|
||||||
|
*
|
||||||
|
* Повторный вызов в той же сессии — не идемпотентен (снова прочитает и
|
||||||
|
* очистит уже очищенный SRSR, увидит "не POR"): звать ровно один раз.
|
||||||
|
*/
|
||||||
|
void bsp_boot_state_init(void);
|
||||||
|
|
||||||
|
/* true, если последний сброс МК (на момент bsp_boot_state_init()) был POR. */
|
||||||
|
bool bsp_boot_state_was_por(void);
|
||||||
|
|
||||||
|
/* Текущее значение счётчика попыток. 0 сразу после POR. */
|
||||||
|
uint32_t bsp_boot_attempt_count(void);
|
||||||
|
|
||||||
|
/* Инкремент счётчика попыток. Звать перед каждой попыткой прыжка в образ. */
|
||||||
|
void bsp_boot_attempt_inc(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Обнулить счётчик попыток. Звать при: успешной установке нового образа
|
||||||
|
* (свежему образу — полный бюджет попыток), фолбэк-стирании зависшего слота
|
||||||
|
* (ситуация изменилась).
|
||||||
|
*/
|
||||||
|
void bsp_boot_attempt_reset(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Отметить образ здоровым — семантический алиас bsp_boot_attempt_reset() для
|
||||||
|
* вызова из приложения (tft_app), а не из загрузчика: тот же эффект (счётчик
|
||||||
|
* обнуляется), но имя называет намерение вызывающей стороны.
|
||||||
|
*/
|
||||||
|
void bsp_boot_health_mark(void);
|
||||||
|
|
||||||
|
#endif /* BSP_BOOT_STATE_H */
|
||||||
67
bsp/boot_state/src/boot_state.c
Normal file
67
bsp/boot_state/src/boot_state.c
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* bsp_boot_state — реализация поверх fsl_src (SRC_GPR5 + SRC->SRSR).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "bsp/boot_state.h"
|
||||||
|
|
||||||
|
#include "fsl_src.h"
|
||||||
|
|
||||||
|
#define BSP_BOOT_STATE_SRC_BASE SRC
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GPR-индекс счётчика попыток (0-based, index=0 -> GPR1). Сверено с i.MX RT1050
|
||||||
|
* RM (SRC, гл. 21.8.5-21.8.13): GPR1/2 — ROM (entry/arg пробуждения из
|
||||||
|
* low-power), GPR6/7/8/9 — ТОЖЕ explicit "used by the ROM code, should not be
|
||||||
|
* used by application software" (несмотря на первоначальное предположение,
|
||||||
|
* что свободны только GPR1/2/10), GPR10 — ROM (альтернативный SBMR1 через бит
|
||||||
|
* [28]). GPR5 формально не ROM, но RM рекомендует именно его под отдельную
|
||||||
|
* задачу (различение SYSRESETREQ/CPU lockup) — не занимаем во избежание
|
||||||
|
* конфликта с этой конвенцией. GPR3 (index 2) — единственный не подписан
|
||||||
|
* НИКАКИМ примечанием в RM, описан просто как "arbitrary value".
|
||||||
|
*/
|
||||||
|
#define BSP_BOOT_STATE_GPR_INDEX 2U
|
||||||
|
|
||||||
|
static bool g_s_was_por = false;
|
||||||
|
|
||||||
|
void bsp_boot_state_init(void)
|
||||||
|
{
|
||||||
|
uint32_t flags = SRC_GetResetStatusFlags(BSP_BOOT_STATE_SRC_BASE);
|
||||||
|
|
||||||
|
g_s_was_por = (flags & (uint32_t) kSRC_IppResetPinFlag) != 0U;
|
||||||
|
|
||||||
|
/* SRSR — write-1-to-clear, копит биты между тёплыми сбросами без явной
|
||||||
|
* очистки (в отличие от WDOG1->WRSR, который самоочищается). Чистим всё,
|
||||||
|
* что доступно, чтобы следующая загрузка увидела только свою причину. */
|
||||||
|
SRC_ClearResetStatusFlags(BSP_BOOT_STATE_SRC_BASE, ~0U);
|
||||||
|
|
||||||
|
if (g_s_was_por)
|
||||||
|
{
|
||||||
|
bsp_boot_attempt_reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bsp_boot_state_was_por(void)
|
||||||
|
{
|
||||||
|
return g_s_was_por;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t bsp_boot_attempt_count(void)
|
||||||
|
{
|
||||||
|
return SRC_GetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bsp_boot_attempt_inc(void)
|
||||||
|
{
|
||||||
|
uint32_t count = bsp_boot_attempt_count();
|
||||||
|
SRC_SetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX, count + 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bsp_boot_attempt_reset(void)
|
||||||
|
{
|
||||||
|
SRC_SetGeneralPurposeRegister(BSP_BOOT_STATE_SRC_BASE, BSP_BOOT_STATE_GPR_INDEX, 0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bsp_boot_health_mark(void)
|
||||||
|
{
|
||||||
|
bsp_boot_attempt_reset();
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* bsp_button — тактовые кнопки TactBut1 / TactBut2
|
* 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
|
#ifndef BSP_BUTTON_H
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
* @file display.h
|
* @file display.h
|
||||||
* @brief BSP: ELCDIF display driver — TFT4 / TFT7 / TFT8 / TFT10.
|
* @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_
|
#ifndef BSP_DISPLAY_DISPLAY_H_
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,16 @@ static void board_mpu_init(void)
|
||||||
MPU->RBAR = ARM_MPU_RBAR(10U, 0x40000000U);
|
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);
|
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);
|
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
|
||||||
|
|
||||||
SCB_EnableDCache();
|
SCB_EnableDCache();
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,27 @@ static sd_io_voltage_t s_io_voltage = {
|
||||||
volatile uint32_t g_sdmmc_dbg_dma_buf_addr = 0U;
|
volatile uint32_t g_sdmmc_dbg_dma_buf_addr = 0U;
|
||||||
volatile uint32_t g_sdmmc_dbg_usdhc1_src_clock_hz = 0U;
|
volatile uint32_t g_sdmmc_dbg_usdhc1_src_clock_hz = 0U;
|
||||||
|
|
||||||
static bool sd_card_detect_gpio(void)
|
/*
|
||||||
|
* Детект карты через USDHC PRES_STATE.CINST — ЕДИНЫЙ механизм и для нашего
|
||||||
|
* гейта bsp_sd_is_inserted() (bsp/sd/src/sd.c), и для внутреннего
|
||||||
|
* SD_PollingCardInsert() SDK (тот зовёт этот callback при kSD_DetectCardByGpioCD).
|
||||||
|
* Не GPIO_PinRead — исторически детект через GPIO2/28 давал ложный "card
|
||||||
|
* present" на пустом слоте (Фаза 3, симптом 1; DEBUG_LOG_PHASE3_SD.md, раунд 3).
|
||||||
|
* Работает, пока пин D13 замаплен на USDHC1_CD_B (см. BOARD_SD_Config ниже:
|
||||||
|
* прежний remux на GPIO2_IO28 убран, пин остаётся на USDHC1_CD_B постоянно —
|
||||||
|
* консолидация, item 1). Тактирование USDHC1 включается идемпотентно на случай
|
||||||
|
* вызова до полного SD_HostInit().
|
||||||
|
*
|
||||||
|
* Тот же PRSSTAT-бит читает и штатный host-CD путь SDK
|
||||||
|
* (SDMMCHOST_CardDetectStatus), но kSD_DetectCardByHostCD дополнительно взводит
|
||||||
|
* USDHC card-detect ПРЕРЫВАНИЯ — не нужны загрузчику (лишний источник IRQ перед
|
||||||
|
* прыжком), поэтому оставляем polling через callback (kSD_DetectCardByGpioCD).
|
||||||
|
*/
|
||||||
|
static bool sd_card_detect_prsstat(void)
|
||||||
{
|
{
|
||||||
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) == BOARD_SDMMC_SD_CD_INSERT_LEVEL;
|
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
||||||
|
return (USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR) &
|
||||||
|
(uint32_t) kUSDHC_CardInsertedFlag) != 0U;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
|
|
@ -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)
|
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_03_USDHC1_DATA1, pad);
|
||||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
|
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_04_USDHC1_DATA2, pad);
|
||||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
|
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_05_USDHC1_DATA3, pad);
|
||||||
/* CD_B в GPIO-режиме: подтяжка вверх + hysteresis для стабильного уровня. */
|
/*
|
||||||
IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_12_GPIO2_IO28,
|
* CD (D13) здесь НЕ конфигурируем: пин на USDHC1_CD_B (см. BOARD_SD_Config),
|
||||||
IOMUXC_SW_PAD_CTL_PAD_PKE_MASK | IOMUXC_SW_PAD_CTL_PAD_PUE_MASK |
|
* pad задан в BOARD_InitPins() и на железе даёт корректный CINST. Прежняя
|
||||||
IOMUXC_SW_PAD_CTL_PAD_HYS_MASK | IOMUXC_SW_PAD_CTL_PAD_PUS(1));
|
* настройка 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_dma_buf_addr = (uint32_t)(uintptr_t)s_dma_buf;
|
||||||
g_sdmmc_dbg_usdhc1_src_clock_hz = sd->host->hostController.sourceClock_Hz;
|
g_sdmmc_dbg_usdhc1_src_clock_hz = sd->host->hostController.sourceClock_Hz;
|
||||||
|
|
||||||
/* --- card detect: GPIO CD (active-low) --- */
|
/* --- card detect: USDHC PRES_STATE.CINST через callback (polling, без IRQ) --- */
|
||||||
s_cd.cdDebounce_ms = BOARD_SDMMC_SD_CD_DEBOUNCE_MS;
|
s_cd.cdDebounce_ms = BOARD_SDMMC_SD_CD_DEBOUNCE_MS;
|
||||||
s_cd.type = BOARD_SDMMC_SD_CD_TYPE;
|
s_cd.type = BOARD_SDMMC_SD_CD_TYPE; /* kSD_DetectCardByGpioCD → callback ниже */
|
||||||
s_cd.cardDetected = sd_card_detect_gpio;
|
s_cd.cardDetected = sd_card_detect_prsstat;
|
||||||
s_cd.callback = cd; /* обычно NULL из bsp_sd */
|
s_cd.callback = cd; /* обычно NULL из bsp_sd */
|
||||||
s_cd.userData = user_data;
|
s_cd.userData = user_data;
|
||||||
|
|
||||||
|
|
@ -166,15 +187,20 @@ void BOARD_SD_Config(void *card, sd_cd_t cd, uint32_t host_irq_priority, void *u
|
||||||
|
|
||||||
/* --- GPIO питания --- */
|
/* --- GPIO питания --- */
|
||||||
sd_power_init();
|
sd_power_init();
|
||||||
/* CD_B: переводим в GPIO2_IO28 и настраиваем вход */
|
/*
|
||||||
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);
|
* CD_B (GPIO_B1_12 / physical D13) остаётся на USDHC1_CD_B постоянно —
|
||||||
const gpio_pin_config_t cd_cfg = {
|
* единый механизм детекта через PRES_STATE.CINST (sd_card_detect_prsstat
|
||||||
.direction = kGPIO_DigitalInput,
|
* выше + гейт bsp_sd_is_inserted). Прежней двойной маршрутизации
|
||||||
.outputLogic = 0U,
|
* (remux на GPIO2_IO28 для GPIO-чтения внутри f_mount) больше нет —
|
||||||
.interruptMode = kGPIO_NoIntmode,
|
* см. DEBUG_LOG_PHASE3_SD.md, раунд 3 «консолидация детекта» (item 1);
|
||||||
};
|
* она убирала латентную хрупкость: после первого bsp_sd_init() пин уходил
|
||||||
GPIO_PinInit(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN, &cd_cfg);
|
* на GPIO2_IO28 и повторный PRSSTAT-скан ослеп бы. Явно переустанавливаем
|
||||||
/* Важно: применяем pad-конфиг сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
|
* альт-функцию (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);
|
sd_pin_config(400000U);
|
||||||
|
|
||||||
/* --- приоритет прерывания хоста --- */
|
/* --- приоритет прерывания хоста --- */
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
* LED_HEARTBEAT — системный, мигает как признак жизни прошивки
|
* LED_HEARTBEAT — системный, мигает как признак жизни прошивки
|
||||||
* LED_APP — прикладной, управляется из firmware по ситуации
|
* LED_APP — прикладной, управляется из firmware по ситуации
|
||||||
*
|
*
|
||||||
* Пины сконфигурированы в generated/pin_mux.h. Этот хедер не знает
|
|
||||||
* ни про GPIO-порты, ни про NXP SDK.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,9 @@ target_compile_definitions(firmware_test PRIVATE
|
||||||
/* Инициализация — вызвать до bsp_tick_init() */
|
/* Инициализация — вызвать до bsp_tick_init() */
|
||||||
bsp_status_t bsp_qspi_init(void);
|
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);
|
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() */
|
uint32_t bsp_qspi_flash_size(void); /* доступно после init() */
|
||||||
|
|
||||||
/* Стирание */
|
/* Стирание */
|
||||||
|
|
@ -117,9 +118,13 @@ board_hw_init();
|
||||||
bsp_qspi_init(); /* ← до bsp_tick_init() */
|
bsp_qspi_init(); /* ← до bsp_tick_init() */
|
||||||
bsp_tick_init();
|
bsp_tick_init();
|
||||||
|
|
||||||
/* Идентификация чипа */
|
/* Идентификация чипа — работает даже если bsp_qspi_init() выше вернула
|
||||||
|
ошибку (LUT для JEDEC грузится безусловным первым шагом внутри неё) */
|
||||||
bsp_qspi_jedec_t jedec;
|
bsp_qspi_jedec_t jedec;
|
||||||
bsp_qspi_read_jedec_id(&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);
|
bsp_qspi_erase_sector(0x00010000);
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,29 @@ bsp_status_t bsp_qspi_init(void);
|
||||||
*
|
*
|
||||||
* @param[out] p_jedec Результат. Не NULL.
|
* @param[out] p_jedec Результат. Не NULL.
|
||||||
* @retval BSP_OK / BSP_ERR.
|
* @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);
|
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.
|
* @brief Стирание сектора 4 KB.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,8 @@
|
||||||
/**
|
/**
|
||||||
* @brief Размер читаемого буфера для однобайтных SR-команд.
|
* @brief Размер читаемого буфера для однобайтных SR-команд.
|
||||||
*
|
*
|
||||||
* FlexSPI FIFO работает минимальными единицами в 4 байта (RXWMRK=0, 1 FILL
|
* FlexSPI FIFO работает минимальными единицами в 4 байта. Читаем 4 байта,
|
||||||
* unit = 4 bytes). Читаем 4 байта, используем только byte[0].
|
* используем только byte[0].
|
||||||
*/
|
*/
|
||||||
#define SR_READ_LEN 4U
|
#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)
|
while (!done)
|
||||||
{
|
{
|
||||||
const uint32_t FILL =
|
const uint32_t FILL_UNITS =
|
||||||
(QSPI_BASE->IPRXFSTS & FLEXSPI_IPRXFSTS_FILL_MASK) >> FLEXSPI_IPRXFSTS_FILL_SHIFT;
|
(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;
|
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,
|
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))
|
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);
|
const status_t RESULT = qspi_read_fifo(p_rx, data_len);
|
||||||
qspi_wait_idle();
|
qspi_wait_idle();
|
||||||
return RESULT;
|
return RESULT;
|
||||||
|
|
@ -653,6 +657,42 @@ AT_QUICKACCESS_SECTION_CODE(static bsp_status_t qspi_detect_chip(uint8_t cap_byt
|
||||||
return BSP_OK;
|
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-операций.
|
* @brief Общая реализация erase-операций.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,10 @@ bsp_status_t bsp_sd_init(void);
|
||||||
bsp_status_t bsp_sd_deinit(void);
|
bsp_status_t bsp_sd_deinit(void);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Проверить физическое наличие карты через регистр USDHC PRSSTAT.
|
* Проверить физическое наличие карты через USDHC PRES_STATE.CINST
|
||||||
* Не требует предварительного вызова bsp_sd_init().
|
* (USDHC_GetPresentStatusFlags). Включает тактирование USDHC1 на время
|
||||||
* Включает тактирование USDHC1 на время чтения регистра.
|
* чтения; не требует предварительного вызова bsp_sd_init(). Корректно
|
||||||
|
* пока пин D13 замаплен на USDHC1_CD_B.
|
||||||
*/
|
*/
|
||||||
bool bsp_sd_is_inserted(void);
|
bool bsp_sd_is_inserted(void);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,11 @@
|
||||||
#include "bsp/sd.h"
|
#include "bsp/sd.h"
|
||||||
|
|
||||||
#include "fsl_sd.h"
|
#include "fsl_sd.h"
|
||||||
#include "fsl_usdhc.h" /* USDHC_Reset — аппаратный сброс FIFO/state machine */
|
#include "fsl_usdhc.h"
|
||||||
#include "sdmmc_config.h" /* BOARD_SD_Config, BOARD_SDMMC_SD_HOST_BASEADDR */
|
#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;
|
extern sd_card_t g_sd;
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
|
|
@ -37,7 +33,8 @@ static void ensure_host_configured(void)
|
||||||
{
|
{
|
||||||
return;
|
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);
|
BOARD_SD_Config(&g_sd, NULL, BOARD_SDMMC_SD_HOST_IRQ_PRIORITY, NULL);
|
||||||
g_s_host_configured = true;
|
g_s_host_configured = true;
|
||||||
}
|
}
|
||||||
|
|
@ -54,19 +51,8 @@ bsp_status_t bsp_sd_init(void)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Аппаратный сброс USDHC FIFO + command/data state machine ПЕРЕД
|
* Аппаратный сброс 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 на этот момент должен быть включён, см. ниже).
|
|
||||||
*/
|
*/
|
||||||
CLOCK_EnableClock(kCLOCK_Usdhc1); /* тактирование нужно ДО сброса регистров */
|
CLOCK_EnableClock(kCLOCK_Usdhc1); /* тактирование нужно ДО сброса регистров */
|
||||||
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
|
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));
|
(void) memset(&g_sd, 0, sizeof(g_sd));
|
||||||
g_s_host_configured = false; /* форсируем повторный BOARD_SD_Config ниже */
|
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 */
|
||||||
|
|
||||||
/*
|
if (SD_HostInit(&g_sd) != kStatus_Success)
|
||||||
* Полный init (host + card) происходит в sd_disk_initialize → SD_Init,
|
{
|
||||||
* который вызывается из f_mount → disk_initialize.
|
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;
|
g_s_initialized = true;
|
||||||
return BSP_OK;
|
return BSP_OK;
|
||||||
|
|
@ -101,12 +95,6 @@ bsp_status_t bsp_sd_deinit(void)
|
||||||
SD_HostDeinit(&g_sd);
|
SD_HostDeinit(&g_sd);
|
||||||
SD_SetCardPower(&g_sd, false);
|
SD_SetCardPower(&g_sd, false);
|
||||||
|
|
||||||
/*
|
|
||||||
* Дополнительный аппаратный сброс сразу после deinit — гарантирует,
|
|
||||||
* что FIFO и state machine USDHC не останутся в промежуточном
|
|
||||||
* состоянии независимо от того, что делает (или не делает)
|
|
||||||
* SD_HostDeinit() из SDK на уровне регистров.
|
|
||||||
*/
|
|
||||||
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
|
USDHC_Reset(BOARD_SDMMC_SD_HOST_BASEADDR, kUSDHC_ResetAll, 100U);
|
||||||
|
|
||||||
g_s_initialized = false;
|
g_s_initialized = false;
|
||||||
|
|
@ -116,6 +104,8 @@ bsp_status_t bsp_sd_deinit(void)
|
||||||
|
|
||||||
bool bsp_sd_is_inserted(void)
|
bool bsp_sd_is_inserted(void)
|
||||||
{
|
{
|
||||||
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) ==
|
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
||||||
BOARD_SDMMC_SD_CD_INSERT_LEVEL;
|
|
||||||
|
uint32_t ps = USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR);
|
||||||
|
return (ps & kUSDHC_CardInsertedFlag) != 0U;
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# bsp_sdram — внешняя SDRAM MT48LC16M16A2 (32 МБ)
|
# bsp_sdram — внешняя SDRAM MT48LC16M16A2 (32 МБ)
|
||||||
|
|
||||||
Минимальная верификация доступности внешней SDRAM, подключённой к SEMC.
|
Подъём SEMC (для прошивок без DCD) и минимальная верификация доступности
|
||||||
Подробное тестирование (паттерны, шина адреса/данных, retention) выполняется
|
внешней SDRAM, подключённой к SEMC. Подробное тестирование (паттерны, шина
|
||||||
в тест-модуле `firmware_test/test_sdram.c`, который использует константы
|
адреса/данных, retention) выполняется в тест-модуле `firmware_test/test_sdram.c`,
|
||||||
и API этого модуля.
|
который использует API этого модуля (путь с DCD, см. ниже).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -29,16 +29,45 @@
|
||||||
Тестовая база смещена на 2 МБ от начала — гарантированно выше `.data`/`.bss`
|
Тестовая база смещена на 2 МБ от начала — гарантированно выше `.data`/`.bss`
|
||||||
прошивки и ниже non-cacheable региона.
|
прошивки и ниже 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
|
## API
|
||||||
|
|
||||||
```c
|
```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` не запускает по ним
|
||||||
внутренних проходов.
|
внутренних проходов.
|
||||||
|
|
||||||
|
**Поведение `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()`:**
|
**Поведение `bsp_sdram_init()`:**
|
||||||
|
|
||||||
1. Ждёт перехода SEMC в IDLE (`SEMC->STS0 & SEMC_STS0_IDLE_MASK`),
|
1. Ждёт перехода SEMC в IDLE (`SEMC->STS0 & SEMC_STS0_IDLE_MASK`),
|
||||||
|
|
@ -66,11 +126,13 @@ bsp_status_t bsp_sdram_init(void);
|
||||||
|
|
||||||
**Коды возврата:**
|
**Коды возврата:**
|
||||||
|
|
||||||
| Код | Условие |
|
| Функция | Код | Условие |
|
||||||
| ----------------- | ------------------------------------------- |
|
| -------------------------- | ----------------- | -------------------------------------------- |
|
||||||
| `BSP_OK` | SDRAM доступна, оба паттерна совпали |
|
| `bsp_sdram_configure()` | `BSP_OK` | Тактирование/пины/регистры/команды прошли |
|
||||||
| `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
|
| `bsp_sdram_configure()` | `BSP_ERR_INIT` | IP-команда SEMC вернула ошибку (precharge/refresh/mode-set) |
|
||||||
| `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
|
| `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
|
```c
|
||||||
#include "bsp/sdram.h"
|
#include "bsp/sdram.h"
|
||||||
|
|
||||||
|
/* Путь С DCD (firmware_test) — SEMC уже поднят до main(): */
|
||||||
if (bsp_sdram_init() != BSP_OK) {
|
if (bsp_sdram_init() != BSP_OK) {
|
||||||
handle_critical_error();
|
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) */
|
[BSP_SDRAM_BASE_ADDR, BSP_SDRAM_BASE_ADDR + BSP_SDRAM_SIZE_BYTES) */
|
||||||
```
|
```
|
||||||
|
|
@ -91,15 +162,19 @@ if (bsp_sdram_init() != BSP_OK) {
|
||||||
|
|
||||||
## CMake
|
## CMake
|
||||||
|
|
||||||
|
Сегодня линкует только `firmware_test` (путь с DCD). Для пути без DCD
|
||||||
|
потребитель (например, bootloader) добавляет зависимость сам — модуль
|
||||||
|
никого за собой не тянет:
|
||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
target_link_libraries(firmware_test PRIVATE bsp_sdram)
|
target_link_libraries(firmware_test PRIVATE bsp_sdram)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Зависимости модуля:**
|
**Зависимости модуля:**
|
||||||
|
|
||||||
| Зависимость | Тип | Описание |
|
| Зависимость | Тип | Описание |
|
||||||
| ------------ | ------- | -------------------------------------------------- |
|
| ------------ | ------- | ----------------------------------------------------------------|
|
||||||
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||||
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаута SEMC IDLE |
|
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаута SEMC IDLE |
|
||||||
| `sdk_semc` | PRIVATE | `fsl_semc.h` — `SEMC->STS0`, `SEMC_STS0_IDLE_MASK` |
|
| `sdk_semc` | PRIVATE | `fsl_semc.h` — регистры `SEMC->`, `SEMC_SendIPCommand()` |
|
||||||
| `bsp_board` | PRIVATE | Общие board-уровневые символы |
|
| `bsp_board` | PRIVATE | Транзитивно даёт `sdk_clock` (`CLOCK_Init*()`) и `sdk_device` (`IOMUXC`) — отдельных PRIVATE-строк на них не заводили |
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,15 @@
|
||||||
* @file sdram.h
|
* @file sdram.h
|
||||||
* @brief BSP для внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
|
* @brief BSP для внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
|
||||||
*
|
*
|
||||||
* Архитектурное ограничение [DECISION]:
|
* Два пути в зависимости от того, кто использует SEMC:
|
||||||
* SEMC инициализируется DCD до вызова main(). Этот модуль не трогает
|
*
|
||||||
* регистры 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)
|
* 0x80000000 — начало SDRAM (SEMC BR0)
|
||||||
|
|
@ -14,16 +20,10 @@
|
||||||
* Тестовый регион (не пересекается с .data/.bss и non-cacheable):
|
* Тестовый регион (не пересекается с .data/.bss и non-cacheable):
|
||||||
* 0x80200000 — начало (2 MB offset от базы)
|
* 0x80200000 — начало (2 MB offset от базы)
|
||||||
*
|
*
|
||||||
* Использование:
|
* Использование (путь без DCD):
|
||||||
* @code
|
* @code
|
||||||
* bsp_sdram_result_t result;
|
* if (bsp_sdram_configure() != BSP_OK) { handle_semc_bringup_error(); }
|
||||||
*
|
* if (bsp_sdram_init() != BSP_OK) { handle_sdram_error(); }
|
||||||
* if (bsp_sdram_init() != BSP_OK) { // DCD не отработал
|
|
||||||
* handle_critical_error();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* bsp_sdram_test_fast(&result); // ~50 мс, 64 KB
|
|
||||||
* bsp_sdram_test_full(&result); // ~2–5 с, 1 MB
|
|
||||||
* @endcode
|
* @endcode
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -65,17 +65,51 @@
|
||||||
|
|
||||||
/* ── Public API ────────────────────────────────────────────────────────── */
|
/* ── 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.
|
* @brief Верифицировать доступность SDRAM.
|
||||||
*
|
*
|
||||||
* Проверяет что SEMC контроллер инициализирован DCD и SDRAM отвечает —
|
* Проверяет что SEMC контроллер инициализирован (DCD или bsp_sdram_configure())
|
||||||
* выполняет минимальный write/read/verify на первых 4 байтах тестового
|
* и SDRAM отвечает — выполняет минимальный write/read/verify на первых 4 байтах
|
||||||
* региона. Не затрагивает .data/.bss прошивки.
|
* тестового региона. Не затрагивает .data/.bss прошивки.
|
||||||
*
|
*
|
||||||
* @note Не реинициализирует SEMC — DCD уже сделал это до main().
|
* @note Не (ре)инициализирует SEMC — предполагает, что DCD либо
|
||||||
|
* bsp_sdram_configure() уже это сделали.
|
||||||
*
|
*
|
||||||
* @retval BSP_OK SDRAM доступна и отвечает корректно.
|
* @retval BSP_OK SDRAM доступна и отвечает корректно.
|
||||||
* @retval BSP_ERR_INIT SEMC не готов (DCD не отработал).
|
* @retval BSP_ERR_INIT SEMC не готов (инициализация не отработала).
|
||||||
* @retval BSP_ERR_TIMEOUT SEMC занят дольше ожидаемого.
|
* @retval BSP_ERR_TIMEOUT SEMC занят дольше ожидаемого.
|
||||||
*/
|
*/
|
||||||
bsp_status_t bsp_sdram_init(void);
|
bsp_status_t bsp_sdram_init(void);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
/**
|
/**
|
||||||
* @file sdram.c
|
* @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).
|
* Кэш: SDRAM настроена как Normal Write-Back cacheable (MPU Region 8).
|
||||||
* Верификация требует явного cache maintenance перед readback — иначе
|
* Верификация требует явного cache maintenance перед readback — иначе
|
||||||
|
|
@ -13,12 +18,13 @@
|
||||||
#include "bsp/sdram.h"
|
#include "bsp/sdram.h"
|
||||||
|
|
||||||
#include "bsp/tick.h"
|
#include "bsp/tick.h"
|
||||||
|
#include "fsl_clock.h"
|
||||||
#include "fsl_semc.h"
|
#include "fsl_semc.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/* ── Константы ─────────────────────────────────────────────────────────── */
|
/* ── Константы верификации ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
/** @brief Таймаут ожидания готовности SEMC контроллера, мс. */
|
/** @brief Таймаут ожидания готовности SEMC контроллера, мс. */
|
||||||
#define SDRAM_SEMC_IDLE_TIMEOUT_MS 10U
|
#define SDRAM_SEMC_IDLE_TIMEOUT_MS 10U
|
||||||
|
|
@ -38,11 +44,202 @@
|
||||||
/** @brief Инверсия эталонного паттерна. */
|
/** @brief Инверсия эталонного паттерна. */
|
||||||
#define SDRAM_VERIFY_PATTERN_B 0x5A5A5A5AUL
|
#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.
|
* @brief Дождаться перехода SEMC в состояние IDLE.
|
||||||
|
|
@ -72,10 +269,10 @@ static bsp_status_t wait_semc_idle(void)
|
||||||
*/
|
*/
|
||||||
static void flush_cache_at_test_base(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_CleanDCache_by_Addr(P_ADDR, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
||||||
SCB_InvalidateDCache_by_Addr(p_addr, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
SCB_InvalidateDCache_by_Addr(P_ADDR, (int32_t) SDRAM_CACHE_LINE_BYTES);
|
||||||
__DSB();
|
__DSB();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,16 +284,33 @@ static void flush_cache_at_test_base(void)
|
||||||
*/
|
*/
|
||||||
static bsp_status_t verify_word(uint32_t pattern)
|
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();
|
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 ────────────────────────────────────────────────────────── */
|
/* ── 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 bsp_sdram_init(void)
|
||||||
{
|
{
|
||||||
bsp_status_t status = wait_semc_idle();
|
bsp_status_t status = wait_semc_idle();
|
||||||
|
|
@ -117,6 +331,6 @@ bsp_status_t bsp_sdram_init(void)
|
||||||
return BSP_ERR_INIT;
|
return BSP_ERR_INIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_initialised = true;
|
g_s_initialised = true;
|
||||||
return BSP_OK;
|
return BSP_OK;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
bsp/wdog/CMakeLists.txt
Normal file
15
bsp/wdog/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
if(BUILD_TESTS_HOST)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(bsp_wdog STATIC src/wdog.c)
|
||||||
|
|
||||||
|
target_include_directories(
|
||||||
|
bsp_wdog
|
||||||
|
PUBLIC include/
|
||||||
|
PRIVATE src/)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
bsp_wdog
|
||||||
|
PUBLIC bsp_status
|
||||||
|
PRIVATE sdk_wdog)
|
||||||
80
bsp/wdog/README.md
Normal file
80
bsp/wdog/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# bsp_wdog — аппаратный watchdog (WDOG1)
|
||||||
|
|
||||||
|
Аппаратный сброс МК по таймауту. Защищает от зависаний в блокирующих вызовах,
|
||||||
|
которые не возвращают управление в код приложения — там, где программный
|
||||||
|
watchdog (флаг + проверка в основном цикле) бессилен, поскольку сам цикл не
|
||||||
|
выполняется.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Аппаратура
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
| ---------------------- | ---------------------------------------------- |
|
||||||
|
| Периферия | WDOG1 |
|
||||||
|
| Шаг таймаута | 0.5 c |
|
||||||
|
| Диапазон таймаута | 1..128 c |
|
||||||
|
| Причина сброса | `WDOG1->WRSR.TOUT` (1 — сброс был по watchdog) |
|
||||||
|
| Поведение под SWD-halt | Приостановлен (`enableDebug = false`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контракт: WDE — write-once
|
||||||
|
|
||||||
|
`WDOG_WCR.WDE` (enable) — бит однократной записи: после `bsp_wdog_init()`
|
||||||
|
watchdog нельзя выключить программно до следующего POR. Он остаётся взведённым
|
||||||
|
и после любого перехода управления внутри той же сессии питания (переход в
|
||||||
|
другой образ прыжком, а не через ресет). Любой код, к которому управление
|
||||||
|
переходит после инициализации watchdog в этой же сессии, обязан периодически
|
||||||
|
вызывать `bsp_wdog_refresh()` не реже периода таймаута — иначе неизбежен
|
||||||
|
reset-loop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```c
|
||||||
|
bsp_status_t bsp_wdog_init(uint32_t timeout_s); /* взвести, once; захватывает причину предыдущего сброса */
|
||||||
|
void bsp_wdog_refresh(void); /* сбросить счётчик таймаута */
|
||||||
|
bool bsp_wdog_caused_last_reset(void); /* true, если последний сброс МК — по таймауту WDOG */
|
||||||
|
bool bsp_wdog_is_armed(void); /* true после успешного init() */
|
||||||
|
uint32_t bsp_wdog_timeout_s(void); /* сконфигурированный таймаут (0 до init) */
|
||||||
|
```
|
||||||
|
|
||||||
|
`bsp_wdog_refresh()` безопасно звать даже до `bsp_wdog_init()` — no-op.
|
||||||
|
Звать только в точках подтверждённого прогресса, не непосредственно перед
|
||||||
|
вызовом, от зависания в котором watchdog и защищает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "bsp/wdog.h"
|
||||||
|
|
||||||
|
/* main.c — как можно раньше после board_hw_init(): */
|
||||||
|
bsp_wdog_init(10U); /* c запасом над самой длинной легитимной операцией */
|
||||||
|
|
||||||
|
if (bsp_wdog_caused_last_reset())
|
||||||
|
{
|
||||||
|
/* предыдущая сессия закончилась таймаутом — восстановились после зависания */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* в основном цикле и в точках подтверждённого прогресса: */
|
||||||
|
bsp_wdog_refresh();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CMake
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
target_link_libraries(firmware_test PRIVATE bsp_wdog)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Зависимости модуля:**
|
||||||
|
|
||||||
|
| Зависимость | Тип | Описание |
|
||||||
|
| ------------ | ------- | ------------------------------------ |
|
||||||
|
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||||
|
| `sdk_wdog` | PRIVATE | `fsl_wdog.h` — `WDOG_Init/Refresh()` |
|
||||||
60
bsp/wdog/include/bsp/wdog.h
Normal file
60
bsp/wdog/include/bsp/wdog.h
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* bsp_wdog — аппаратный watchdog (WDOG1, MIMXRT1052).
|
||||||
|
*
|
||||||
|
* Назначение: аппаратный сброс МК по таймауту. Защищает от зависаний в
|
||||||
|
* блокирующих вызовах, которые не возвращают управление в код приложения —
|
||||||
|
* там, где программный watchdog (флаг + проверка в основном цикле) бессилен,
|
||||||
|
* поскольку сам цикл не выполняется.
|
||||||
|
*
|
||||||
|
* Контракт: WDOG Enable (WDE) — write-once бит. После bsp_wdog_init() watchdog
|
||||||
|
* нельзя выключить программно до следующего POR; он остаётся взведённым в
|
||||||
|
* течение всей сессии питания, включая любую передачу управления внутри неё
|
||||||
|
* (не только через ресет). Любой код, к которому управление переходит после
|
||||||
|
* инициализации watchdog в этой же сессии, обязан периодически звать
|
||||||
|
* bsp_wdog_refresh() не реже периода таймаута — иначе reset-loop. Под
|
||||||
|
* отладчиком (SWD halt) watchdog приостанавливается (enableDebug=false), так
|
||||||
|
* что пошаговая отладка не сбивается сбросами.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BSP_WDOG_H
|
||||||
|
#define BSP_WDOG_H
|
||||||
|
|
||||||
|
#include "bsp/status.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Взвести WDOG1 с таймаутом timeout_s секунд (аппаратно округляется до шага
|
||||||
|
* 0.5 с: реальный таймаут = (2*timeout_s) * 0.5 c). Разумный диапазон 1..128.
|
||||||
|
*
|
||||||
|
* Побочно захватывает причину ПРЕДЫДУЩЕГО сброса (WDOG-таймаут vs прочее) для
|
||||||
|
* bsp_wdog_caused_last_reset() — читать до/после безразлично, но делается тут.
|
||||||
|
*
|
||||||
|
* Вызывать один раз, как можно раньше в main() (после board_hw_init()).
|
||||||
|
* Повторный вызов — no-op (WDE уже взведён).
|
||||||
|
*/
|
||||||
|
bsp_status_t bsp_wdog_init(uint32_t timeout_s);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* "Погладить" watchdog — сбросить счётчик таймаута. Дёшево; безопасно звать
|
||||||
|
* даже если WDOG не взведён (запись refresh-последовательности безвредна).
|
||||||
|
* Звать только в точках РЕАЛЬНОГО прогресса, НЕ перед блокирующими вызовами,
|
||||||
|
* от зависания в которых watchdog и защищает.
|
||||||
|
*/
|
||||||
|
void bsp_wdog_refresh(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* true, если ПОСЛЕДНИЙ сброс МК был вызван таймаутом WDOG (а не power-on /
|
||||||
|
* software / прочим). Валидно после bsp_wdog_init(). Для диагностики: показать
|
||||||
|
* технологу через USB-CDC, что плата восстановилась после зависания.
|
||||||
|
*/
|
||||||
|
bool bsp_wdog_caused_last_reset(void);
|
||||||
|
|
||||||
|
/* true после успешного bsp_wdog_init(). */
|
||||||
|
bool bsp_wdog_is_armed(void);
|
||||||
|
|
||||||
|
/* Сконфигурированный таймаут в секундах (0, если ещё не взведён). */
|
||||||
|
uint32_t bsp_wdog_timeout_s(void);
|
||||||
|
|
||||||
|
#endif /* BSP_WDOG_H */
|
||||||
79
bsp/wdog/src/wdog.c
Normal file
79
bsp/wdog/src/wdog.c
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* bsp_wdog — реализация поверх fsl_wdog (WDOG1).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "bsp/wdog.h"
|
||||||
|
|
||||||
|
#include "fsl_wdog.h"
|
||||||
|
|
||||||
|
#define BSP_WDOG_BASE WDOG1
|
||||||
|
|
||||||
|
/* Границы поля WCR.WT (8 бит): таймаут = (WT+1) * 0.5 c, максимум 128 c. */
|
||||||
|
#define BSP_WDOG_TIMEOUT_S_MIN 1U
|
||||||
|
#define BSP_WDOG_TIMEOUT_S_MAX 128U
|
||||||
|
|
||||||
|
static bool g_s_armed = false;
|
||||||
|
static bool g_s_last_reset_was_wdog = false;
|
||||||
|
static uint32_t g_s_timeout_s = 0U;
|
||||||
|
|
||||||
|
bsp_status_t bsp_wdog_init(uint32_t timeout_s)
|
||||||
|
{
|
||||||
|
if (g_s_armed)
|
||||||
|
{
|
||||||
|
return BSP_OK; /* WDE — write-once; повторно не взводим */
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((timeout_s < BSP_WDOG_TIMEOUT_S_MIN) || (timeout_s > BSP_WDOG_TIMEOUT_S_MAX))
|
||||||
|
{
|
||||||
|
return BSP_ERR_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Причина предыдущего сброса: читаем WDOG1->WRSR до настройки. WRSR
|
||||||
|
* read-only, отражает последний сброс (TOUT=WDOG-таймаут, POR=power-on),
|
||||||
|
* стабилен до следующего сброса.
|
||||||
|
*/
|
||||||
|
g_s_last_reset_was_wdog = (BSP_WDOG_BASE->WRSR & WDOG_WRSR_TOUT_MASK) != 0U;
|
||||||
|
|
||||||
|
wdog_config_t cfg;
|
||||||
|
WDOG_GetDefaultConfig(&cfg);
|
||||||
|
|
||||||
|
/* WT = 2*timeout_s - 1 → таймаут = (WT+1)*0.5 c = timeout_s c. */
|
||||||
|
cfg.timeoutValue = (uint16_t) ((timeout_s * 2U) - 1U);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* КРИТИЧНО для рабочего процесса: не сбрасывать плату, когда ядро
|
||||||
|
* остановлено отладчиком (SWD halt) — иначе пошаговая отладка загрузчика
|
||||||
|
* невозможна. enableWait/enableStop оставляем как в дефолте: загрузчик и
|
||||||
|
* приложение в эти режимы не входят, но если войдут — пусть watchdog
|
||||||
|
* продолжает считать (безопаснее по умолчанию).
|
||||||
|
*/
|
||||||
|
cfg.workMode.enableDebug = false;
|
||||||
|
|
||||||
|
cfg.enableWdog = true;
|
||||||
|
WDOG_Init(BSP_WDOG_BASE, &cfg); /* с этого момента WDE взведён навсегда */
|
||||||
|
|
||||||
|
g_s_timeout_s = timeout_s;
|
||||||
|
g_s_armed = true;
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bsp_wdog_refresh(void)
|
||||||
|
{
|
||||||
|
WDOG_Refresh(BSP_WDOG_BASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bsp_wdog_caused_last_reset(void)
|
||||||
|
{
|
||||||
|
return g_s_last_reset_was_wdog;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bsp_wdog_is_armed(void)
|
||||||
|
{
|
||||||
|
return g_s_armed;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t bsp_wdog_timeout_s(void)
|
||||||
|
{
|
||||||
|
return g_s_timeout_s;
|
||||||
|
}
|
||||||
271
cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld
Normal file
271
cmake/linker/MIMXRT1052xxxxx_bootloader_flexspi_nor.ld
Normal 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")
|
||||||
|
}
|
||||||
130
cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld
Normal file
130
cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld
Normal 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")
|
||||||
|
}
|
||||||
|
|
@ -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 и аппаратной валидации на стенде.
|
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
> Проект: TFT Firmware (MIMXRT1052CVJ5B)
|
> Проект: TFT Firmware (MIMXRT1052CVJ5B)
|
||||||
> Документ описывает схему и принцип работы двух GitHub Actions workflow в
|
> Документ описывает схему и принцип работы двух GitHub Actions workflow в
|
||||||
> репозитории: `ci.yml` (обычный PR/push-цикл) и `release.yml` (публикация
|
> репозитории: `ci.yml` (обычный PR/push-цикл) и `release.yml` (публикация
|
||||||
> релизных бинарников по тегу). Для истории решений и roadmap развития
|
> релизных бинарников по тегу). Это техническая справка «как оно работает
|
||||||
> `ci.yml` — см. `just/ci_workflow.md`; этот документ — техническая справка
|
> сейчас», а не хронология решений.
|
||||||
> «как оно работает сейчас», а не хронология.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -13,15 +12,14 @@
|
||||||
|
|
||||||
| | `ci.yml` | `release.yml` |
|
| | `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-тесты | Собираются ли и публикуются ли релизные бинарники |
|
| Что проверяет | Собирается ли проект и проходят ли host-тесты | Собираются ли и публикуются ли релизные бинарники |
|
||||||
| Публикует что-то наружу? | Нет — только артефакты прогона (для отладки) | Да — GitHub Release с реальными asset'ами (только по тегу) |
|
| Публикует что-то наружу? | Нет — только артефакты прогона (для отладки) | Да — GitHub Release с реальными asset'ами (только по тегу) |
|
||||||
| Раннеры | `ubuntu-latest` (оба job'а) | `ubuntu-latest` + `macos-latest` + `windows-latest` |
|
| Раннеры | `ubuntu-latest` (оба job'а) | `ubuntu-latest` + `macos-latest` + `windows-latest` |
|
||||||
|
|
||||||
Они намеренно не смешаны в один файл (см. `just/ci_workflow.md`, «Шаг 3 —
|
Они намеренно не смешаны в один файл: PR-цикл должен оставаться быстрым и не
|
||||||
выделить release workflow»): PR-цикл должен оставаться быстрым и не зависеть
|
зависеть от кросс-платформенной упаковки `service-tui`, а релизная
|
||||||
от кросс-платформенной упаковки `service-tui`, а релизная публикация не
|
публикация не должна гонять host-тесты повторно на каждый push в PR.
|
||||||
должна гонять host-тесты повторно на каждый push в PR.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -57,8 +55,7 @@ bash -lc '...'` — `--user root` обязателен, иначе non-root по
|
||||||
|
|
||||||
Чего `ci.yml` **не делает**: lint (заглушка в `just ci::lint`), coverage,
|
Чего `ci.yml` **не делает**: lint (заглушка в `just ci::lint`), coverage,
|
||||||
сборку/упаковку `service-tui`, HIL-тесты (нужно физическое железо —
|
сборку/упаковку `service-tui`, HIL-тесты (нужно физическое железо —
|
||||||
самостоятельная задача для self-hosted раннера). См. `just/ci_workflow.md`
|
самостоятельная задача для self-hosted раннера). См. §5 ниже.
|
||||||
для планов по каждому из этих пунктов.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -66,19 +63,21 @@ bash -lc '...'` — `--user root` обязателен, иначе non-root по
|
||||||
|
|
||||||
### 3.1 Схема тегов
|
### 3.1 Схема тегов
|
||||||
|
|
||||||
Firmware (`firmware_test`) и `service-tui` версионируются и релизятся
|
Firmware (`firmware_test`), bootloader и `service-tui` версионируются и
|
||||||
**независимо** (`FIRST_RELEASE_PLAN.md`, Шаг 2.1) — два разных паттерна
|
релизятся **независимо** — три разных паттерна тега запускают три разных
|
||||||
тега запускают два разных сценария внутри одного workflow-файла:
|
сценария внутри одного workflow-файла:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
push_fw["push tag\nfirmware-vX.Y.Z"] --> firmware
|
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
|
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 -->|"тег 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)"| macos["service-tui-macos\njust host::package-tui"]
|
||||||
firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| windows["service-tui-windows\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
|
windows -->|"тег tui-v*"| publishTui
|
||||||
```
|
```
|
||||||
|
|
||||||
Ключевое архитектурное решение: **HAB-образ `firmware_test`, который
|
Ключевое архитектурное решение: **HAB-образы `firmware_test` и `bootloader`,
|
||||||
вшивается внутрь TUI-бандла, всегда собирается заново из текущего HEAD**
|
которые вшиваются внутрь TUI-бандла, всегда собираются заново из текущего
|
||||||
джобой `firmware` — а не скачивается из последнего опубликованного
|
HEAD** джобой `firmware` — а не скачиваются из последних опубликованных
|
||||||
`firmware-v*` релиза. Поэтому job `firmware` выполняется **при любом
|
`firmware-v*`/`bootloader-v*` релизов. Поэтому job `firmware` выполняется
|
||||||
триггере**, без условия — она нужна и для отдельного firmware-релиза, и
|
**при любом триггере**, без условия — она нужна и для отдельных релизов
|
||||||
как зависимость для упаковки TUI.
|
firmware_test/bootloader, и как общая зависимость для упаковки TUI (см.
|
||||||
|
Фазу 5, `firmware/bootloader/PLAN.md` — до неё `firmware` собирала только
|
||||||
|
firmware_test, и TUI-бандл молча уходил без образа bootloader).
|
||||||
|
|
||||||
### 3.2 Триггеры
|
### 3.2 Триггеры
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ["tui-v*", "firmware-v*"]
|
tags: ["tui-v*", "firmware-v*", "bootloader-v*"]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
release_type: {type: choice, options: [tui, firmware], default: tui}
|
release_type: {type: choice, options: [tui, firmware, bootloader], default: tui}
|
||||||
```
|
```
|
||||||
|
|
||||||
`workflow_dispatch` — «сухой прогон» без публикации: собирает всё
|
`workflow_dispatch` — «сухой прогон» без публикации: собирает всё
|
||||||
|
|
@ -125,15 +126,19 @@ job-level `if:`, использовать именно эту форму.
|
||||||
|
|
||||||
| Job | Раннер | Когда выполняется | Что делает |
|
| 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 |
|
| `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` |
|
| `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) —
|
**Debug HAB для firmware_test, Release HAB для bootloader** — не единое
|
||||||
Release-сборка `firmware_test` нестабильна (FCB/clock), поэтому `firmware`
|
правило «всегда Debug». Release-сборка `firmware_test` нестабильна
|
||||||
джоба собирает только `hab-firmware-test-debug`, не полный
|
(FCB/clock), поэтому `firmware`-джоба собирает `hab-firmware-test-debug`,
|
||||||
`hab-all-release`.
|
не полный `hab-all-release`. Bootloader — наоборот: production-путь (Фаза 5,
|
||||||
|
`firmware/bootloader/PLAN.md`) жёстко требует **Release**, подписанный
|
||||||
|
(`flags=0x08`) — Debug-конфиг bootloader остаётся unsigned и используется
|
||||||
|
только для локальной отладки, в релиз/TUI-бандл не попадает.
|
||||||
|
|
||||||
**Сверка версии тег↔файл** — маленький, но важный guard в обеих ветках
|
**Сверка версии тег↔файл** — маленький, но важный guard в обеих ветках
|
||||||
(`firmware`/`service-tui-*`): если версия в теге не совпадает с версией в
|
(`firmware`/`service-tui-*`): если версия в теге не совпадает с версией в
|
||||||
|
|
@ -152,6 +157,7 @@ Git Bash на Windows.
|
||||||
```bash
|
```bash
|
||||||
gh workflow run release.yml --ref dev -f release_type=tui
|
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=firmware
|
||||||
|
gh workflow run release.yml --ref dev -f release_type=bootloader
|
||||||
```
|
```
|
||||||
|
|
||||||
или через веб-интерфейс: Actions → **Release** → **Run workflow** → выбрать
|
или через веб-интерфейс: Actions → **Release** → **Run workflow** → выбрать
|
||||||
|
|
@ -180,10 +186,13 @@ branch и `release_type`. Джобы `publish-*` в этом сценарии п
|
||||||
- **Self-hosted HIL-раннер.** Ни один из двух workflow не может
|
- **Self-hosted HIL-раннер.** Ни один из двух workflow не может
|
||||||
задетектировать SDP на живой плате или прогнать деструктивные сценарии
|
задетектировать SDP на живой плате или прогнать деструктивные сценарии
|
||||||
(обрыв USB) — GitHub-hosted раннеры не видят реальное железо. Это ручной
|
(обрыв USB) — GitHub-hosted раннеры не видят реальное железо. Это ручной
|
||||||
шаг перед каждым релизом (`FIRST_RELEASE_PLAN.md`, Шаг 4), пока не
|
шаг перед каждым релизом, пока не поднят self-hosted lane.
|
||||||
поднят self-hosted lane (`just/ci_workflow.md`, «Шаг 5»).
|
- **`lint`/`coverage`** не подключены ни в `ci.yml`, ни в `release.yml`.
|
||||||
- **`lint`/`coverage`** не подключены ни в `ci.yml`, ни в `release.yml` —
|
|
||||||
см. `just/ci_workflow.md`, «Рекомендуемые следующие шаги» (Шаги 1–2).
|
|
||||||
- **Публикация devcontainer image в GHCR** — образ пересобирается в каждой
|
- **Публикация devcontainer image в GHCR** — образ пересобирается в каждой
|
||||||
job'е каждого workflow (пусть и с layer-кэшем); заранее опубликованный
|
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 пока не встроена (сама
|
||||||
|
церемония — не автоматизируемый процесс, см. документ).
|
||||||
|
|
|
||||||
|
|
@ -198,9 +198,9 @@ flowchart LR
|
||||||
│ │ │ dcd.bin, ivt_flashloader.bin
|
│ │ │ dcd.bin, ivt_flashloader.bin
|
||||||
│ │ └── uv.lock
|
│ │ └── 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-окружение
|
│ └── hil/ ← HIL pytest-окружение
|
||||||
│ ├── conftest.py ← фикстуры: m5, loaded_<n>, uart_<n>
|
│ ├── conftest.py ← фикстуры: m5, loaded_<n>, uart_<n>
|
||||||
|
|
@ -356,8 +356,8 @@ buildPresets (HIL):
|
||||||
| Прошивка | Стратегия | Инструмент загрузки |
|
| Прошивка | Стратегия | Инструмент загрузки |
|
||||||
| ---------------------------- | ---------------------------------- | ------------------- |
|
| ---------------------------- | ---------------------------------- | ------------------- |
|
||||||
| `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash |
|
| `firmware_test` | XIP из Flash (`flexspi_nor.ld`) | SPSDK → Flash |
|
||||||
| `bootloader` | Копирование в ITCM | SPSDK → Flash |
|
| `bootloader` | XIP из Flash, без ITCM/DCD (не трогает SDRAM) — выбирает и запускает `tft_app` из слота (MCUboot Direct-XIP) | SPSDK → Flash |
|
||||||
| `tft_app` | XIP + буферы в SDRAM | SPSDK → Flash |
|
| `tft_app` | XIP из своего слота (Direct-XIP, два слота A/Б) + буферы в SDRAM (SEMC поднимает сама) | SPSDK → Flash |
|
||||||
| HIL target (`tests/target/`) | Исполнение из ITCM/DTCM (`ram.ld`) | pyOCD → RAM |
|
| 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 не используется — прошивка исчезает при отключении питания.
|
**HIL boot-стратегия:** pyOCD настраивает FLEXRAM (128 KB ITCM + 128 KB DTCM + 256 KB OCRAM), записывает PT_LOAD сегменты ELF по физическим адресам, устанавливает SP/PC из таблицы векторов и запускает выполнение. Flash не используется — прошивка исчезает при отключении питания.
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ in-process через Python API `spsdk` (`app/flash_backend.py`: `HabImage`,
|
||||||
`configure-memory 0xF000000F`) — auto-config для 4-байтной адресации не
|
`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`, три сборки этого репозитория, что через
|
§8. Штатный путь (`--firmware`, три сборки этого репозитория, что через
|
||||||
`just host::flash`, что через `service-tui`) не меняется и по-прежнему
|
`just host::flash`, что через `service-tui`) не меняется и по-прежнему
|
||||||
использует auto-config Flashloader, как описано в 1.4.
|
использует auto-config Flashloader, как описано в 1.4.
|
||||||
|
|
|
||||||
125
docs/bootloader/BOOTLOADER_FLASH_MAP.md
Normal file
125
docs/bootloader/BOOTLOADER_FLASH_MAP.md
Normal 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()`.
|
||||||
302
docs/bootloader/BOOT_FLOW.md
Normal file
302
docs/bootloader/BOOT_FLOW.md
Normal 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` | Счётчик обнулён, зависший слот пробуется заново |
|
||||||
|
|
@ -84,9 +84,9 @@ typedef struct {
|
||||||
|
|
||||||
DCD содержит последовательность команд двух типов:
|
DCD содержит последовательность команд двух типов:
|
||||||
|
|
||||||
| Команда | Тег | Назначение |
|
| Команда | Тег | Назначение |
|
||||||
|---|---|---|
|
| ---------------- | ------ | -------------------------------- |
|
||||||
| `Write Data` | `0xCC` | Записать значение по адресу |
|
| `Write Data` | `0xCC` | Записать значение по адресу |
|
||||||
| `Check Bits Set` | `0xCF` | Ждать пока бит станет `1` (poll) |
|
| `Check Bits Set` | `0xCF` | Ждать пока бит станет `1` (poll) |
|
||||||
|
|
||||||
Формат бинарника: `Tag(1) | Length(2 BE) | Parameter(1) | данные`.
|
Формат бинарника: `Tag(1) | Length(2 BE) | Parameter(1) | данные`.
|
||||||
|
|
@ -105,11 +105,11 @@ DCD содержит последовательность команд двух
|
||||||
|
|
||||||
### 4.1 По типу образа
|
### 4.1 По типу образа
|
||||||
|
|
||||||
| Режим | `flags` | CSF | Применение |
|
| Режим | `flags` | CSF | Применение |
|
||||||
|---|---|---|---|
|
| ------------------ | ------- | ------------------------- | ------------------- |
|
||||||
| Unsigned | `0x00` | отсутствует | разработка, отладка |
|
| Unsigned | `0x00` | отсутствует | разработка, отладка |
|
||||||
| Signed | `0x08` | RSA/ECDSA подпись | производство |
|
| Signed | `0x08` | RSA/ECDSA подпись | производство |
|
||||||
| Signed + Encrypted | `0x0C` | подпись + шифрование кода | максимальная защита |
|
| Signed + Encrypted | `0x0C` | подпись + шифрование кода | максимальная защита |
|
||||||
|
|
||||||
### 4.2 По состоянию чипа (OTP fuse)
|
### 4.2 По состоянию чипа (OTP fuse)
|
||||||
|
|
||||||
|
|
@ -150,6 +150,42 @@ BootROM проверяет подпись, но **игнорирует ошиб
|
||||||
→ прыгает на entry point
|
→ прыгает на 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-контейнер
|
## 6. Почему Unsigned-образ требует HAB-контейнер
|
||||||
|
|
@ -170,7 +206,7 @@ BootROM проверяет подпись, но **игнорирует ошиб
|
||||||
|
|
||||||
BootROM в режиме SDP умеет только писать в RAM и прыгать. Для записи во Flash необходим **Flashloader** — специальная программа от NXP.
|
BootROM в режиме SDP умеет только писать в RAM и прыгать. Для записи во Flash необходим **Flashloader** — специальная программа от NXP.
|
||||||
|
|
||||||
```
|
```bash
|
||||||
Плата в SDP режиме (BOOT_MOD_1 = 3V3)
|
Плата в SDP режиме (BOOT_MOD_1 = 3V3)
|
||||||
│
|
│
|
||||||
│ sdphost -u 0x1FC9,0x0130
|
│ sdphost -u 0x1FC9,0x0130
|
||||||
|
|
@ -192,12 +228,12 @@ BootROM в режиме SDP умеет только писать в RAM и пр
|
||||||
|
|
||||||
## 8. Жизненный цикл для проекта TFT
|
## 8. Жизненный цикл для проекта TFT
|
||||||
|
|
||||||
| Стадия | Режим HAB | Подпись | Fuse |
|
| Стадия | Режим HAB | Подпись | Fuse |
|
||||||
|---|---|---|---|
|
| --------------------- | ---------- | ----------------- | ---------------- |
|
||||||
| Разработка | Open | Unsigned (`0x00`) | не трогаем |
|
| Разработка | Open | Unsigned (`0x00`) | не трогаем |
|
||||||
| Входной контроль | Open | Unsigned (`0x00`) | не трогаем |
|
| Входной контроль | Open | Unsigned (`0x00`) | не трогаем |
|
||||||
| Предсерийные образцы | Open | Signed (`0x08`) | не трогаем |
|
| Предсерийные образцы | Open | Signed (`0x08`) | не трогаем |
|
||||||
| Серийное производство | **Closed** | Signed (`0x08`) | `SEC_CONFIG = 1` |
|
| Серийное производство | **Closed** | Signed (`0x08`) | `SEC_CONFIG = 1` |
|
||||||
|
|
||||||
> ⚠️ Запись `SEC_CONFIG = 1` необратима. Перед закрытием HAB необходимо убедиться, что подписанный образ успешно проходит верификацию на реальном железе.
|
> ⚠️ Запись `SEC_CONFIG = 1` необратима. Перед закрытием HAB необходимо убедиться, что подписанный образ успешно проходит верификацию на реальном железе.
|
||||||
|
|
||||||
|
|
@ -205,13 +241,13 @@ BootROM в режиме SDP умеет только писать в RAM и пр
|
||||||
|
|
||||||
## 9. Инструменты
|
## 9. Инструменты
|
||||||
|
|
||||||
| Инструмент | Назначение |
|
| Инструмент | Назначение |
|
||||||
|---|---|
|
| --------------------- | ---------------------------------------------------------- |
|
||||||
| `nxpimage hab export` | Сборка HAB-образа (IVT + BDT + DCD + опционально CSF) |
|
| `nxpimage hab export` | Сборка HAB-образа (IVT + BDT + DCD + опционально CSF) |
|
||||||
| `nxpimage hab parse` | Разбор готового образа для проверки |
|
| `nxpimage hab parse` | Разбор готового образа для проверки |
|
||||||
| `sdphost` | Связь с BootROM в SDP режиме (запись в RAM, прыжок) |
|
| `sdphost` | Связь с BootROM в SDP режиме (запись в RAM, прыжок) |
|
||||||
| `blhost` | Связь с Flashloader (запись во Flash, конфигурация памяти) |
|
| `blhost` | Связь с Flashloader (запись во Flash, конфигурация памяти) |
|
||||||
| `nxpdevscan` | Обнаружение подключённых NXP устройств |
|
| `nxpdevscan` | Обнаружение подключённых NXP устройств |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
11
docs/bootloader/LED_PATTERNS.md
Normal file
11
docs/bootloader/LED_PATTERNS.md
Normal 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) |
|
||||||
138
docs/mimxrt1052/UPDATE_FLOW.md
Normal file
138
docs/mimxrt1052/UPDATE_FLOW.md
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
# UPDATE_FLOW.md — производственная заливка и полевое обновление tft_app
|
||||||
|
|
||||||
|
Разбор «что и куда заливать» для связки **bootloader → tft_app** (MCUboot Direct-XIP, два слота A/Б).
|
||||||
|
Дополняет [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md) (адреса) и
|
||||||
|
[../../firmware/bootloader/PLAN.md](../../firmware/bootloader/PLAN.md) (фазы, recovery).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR (главное, что вызывает непонимание)
|
||||||
|
|
||||||
|
**Direct-XIP: образ исполняется прямо из адреса своего слота, код обычно НЕ позиционно-независим →
|
||||||
|
под каждый слот нужна СВОЯ линковка.** Обновление, которое встанет в Slot Б, должно быть слинковано
|
||||||
|
под адрес Slot Б (`0x60240000`). Поэтому:
|
||||||
|
|
||||||
|
> **Релиз одной версии tft_app = ДВА подписанных бинаря** (линковка под A + линковка под Б), из
|
||||||
|
> ОДНОГО исходника, параметризованным линкер-скриптом — ровно как уже устроен `test_stub`
|
||||||
|
> (`--defsym=__slot_base__=…`). Версия (`-v X.Y.Z`) одинаковая в обоих.
|
||||||
|
|
||||||
|
Да, «собрать новую версию со своим линкер-скриптом под слот Б» — так и есть. Но не вручную по одному:
|
||||||
|
это один параметризованный `.ld` и один рецепт сборки, дающий оба бинаря.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Почему так — это свойство Direct-XIP, не наша прихоть
|
||||||
|
|
||||||
|
- **XIP** = eXecute In Place: CPU фетчит инструкции напрямую из флеша по адресу слота, образ никуда не
|
||||||
|
копируется (в отличие от swap/scratch-режимов MCUboot, которые мы сознательно НЕ используем).
|
||||||
|
- Абсолютные адреса — vector table, указатели на функции, литеральные пулы, адрес инициализации
|
||||||
|
`.data` — фиксируются на этапе **линковки** под конкретный базовый адрес. Образ, слинкованный под
|
||||||
|
Slot A (`0x60040000`), в Slot Б (`0x60240000`) поедет по чужим адресам и не запустится корректно.
|
||||||
|
- Это прямо зафиксированный «открытый вопрос §4» в [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md):
|
||||||
|
*«код обычно не позиционно-независим — вероятно потребуется два варианта линковки под Slot A и
|
||||||
|
Slot Б, либо PIC»*. `test_stub` (Фаза 2) уже подтвердил two-slot-two-linkage на реальном железе.
|
||||||
|
|
||||||
|
> ⚠️ **Почему на `test_stub` «одинаковый» бинарь как будто работал в обоих слотах.** Заглушка
|
||||||
|
> крошечная (только мигание LED): почти весь её код PC-relative, `.data` минимальна, а vector table
|
||||||
|
> релоцируется и `boot_select` (ставит `VTOR`), и самим образом в старте — поэтому она позиционно
|
||||||
|
> **терпима** по случайности. Реальный tft_app (большой, с `.data`, абсолютными указателями,
|
||||||
|
> шрифтами-C-массивами, framebuffer) терпимым **не будет** — ему нужны обе линковки по-настоящему.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Карта слотов (напоминание)
|
||||||
|
|
||||||
|
| Слот | База | Размер | ORIGIN образа (после imgtool-заголовка `0x200`) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Slot A | `0x60040000` | 2 МБ | `0x60040200` |
|
||||||
|
| Slot Б | `0x60240000` | 2 МБ | `0x60240200` |
|
||||||
|
|
||||||
|
Линкер tft_app: `ORIGIN = __slot_base__ + 0x200` (образец — `cmake/linker/MIMXRT1052xxxxx_mcuboot_slot.ld`
|
||||||
|
у `test_stub`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Релизные артефакты (на каждую версию)
|
||||||
|
|
||||||
|
Из одного исходника tft_app — **два подписанных бинаря** production-ключом:
|
||||||
|
|
||||||
|
| Файл | Линковка (`__slot_base__`) | Назначение |
|
||||||
|
|---|---|---|
|
||||||
|
| `tft_app_slotA_vX.Y.Z.signed.bin` | `0x60040000` | ставится в Slot A |
|
||||||
|
| `tft_app_slotB_vX.Y.Z.signed.bin` | `0x60240000` | ставится в Slot Б |
|
||||||
|
|
||||||
|
- Оба — **одна версия** `-v X.Y.Z` (version-gate сравнивает версию образа, а не его линковку).
|
||||||
|
- Оба подписаны production-ключом (не тестовым sample-ключом MCUboot; см. HAB SRK-церемонию как
|
||||||
|
аналог для bootloader).
|
||||||
|
- Рецепт — по образцу `just build::build-mcuboot-stub` (собирает оба слота + подписывает imgtool'ом),
|
||||||
|
но с production-ключом и реальным tft_app вместо заглушки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Кейсы
|
||||||
|
|
||||||
|
| # | Ситуация | Куда/что | Как |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **Начальная production-заливка** | A-линковка → Slot A; Slot Б пуст | SWD / USB ROM (blhost), `just host::flash-production`-путь |
|
||||||
|
| 2 | **Первое полевое обновление** v1→v2 | A активен → target = Slot Б → ставится **Б-линковка** v2 | SD, авто по version-gate; после — A=v1 (fallback), Б=v2 (active) |
|
||||||
|
| 3 | **Следующее обновление** v2→v3 | Б активен → target = Slot A → ставится **A-линковка** v3 | SD; после — A=v3 (active), Б=v2 (fallback) |
|
||||||
|
| 4 | **Даунгрейд** (BTN_1 при старте) | старая версия в inactive + стирание прежнего активного | SD + удержание BTN_1 (см. Фаза 3) |
|
||||||
|
| 5 | **Recovery** (BTN_2 / class C) | оба слота стёрты → чистый борт → A-линковка в Slot A | SD, ослабленный version-gate (см. Фаза 6) |
|
||||||
|
| 6 | **Образ завис после обновления** | авто-откат (class A) или счётчик+фолбэк (class B) | см. Фаза 6, taxonomy A/B |
|
||||||
|
|
||||||
|
**Ключевой инвариант alternation A/Б:** обновление всегда идёт в НЕактивный слот, прежний рабочий слот
|
||||||
|
остаётся нетронутым как fallback (см. `update_policy` — «целевой слот всегда НЕ активный»). Поэтому в
|
||||||
|
поле почти всегда есть куда откатиться, если новая версия окажется плохой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Как обновление выбирает нужную линковку — ТЕКУЩИЙ GAP
|
||||||
|
|
||||||
|
**Сейчас** ([sd_update.c](../../firmware/bootloader/src/sd_update.c)): ищется ОДИН файл
|
||||||
|
`2:/TFT_APP.BIN` и ставится в неактивный слот (`decision.target_slot`). Для позиционно-терпимого
|
||||||
|
`test_stub` это прошло все 5 сценариев Фазы 3 — но для реального tft_app **сломается**: если активен
|
||||||
|
Slot A, обновление идёт в Slot Б, а `TFT_APP.BIN` мог быть слинкован под A → в Slot Б не запустится.
|
||||||
|
|
||||||
|
**Нужное расширение (часть включения реального tft_app):**
|
||||||
|
- SD несёт **оба** линкованных бинаря по соглашению имён, напр. `TFT_APP_A.BIN` / `TFT_APP_B.BIN`.
|
||||||
|
- `sd_update` открывает файл **по целевому слоту**: `TFT_APP_A.BIN`, если target = Slot A, иначе
|
||||||
|
`TFT_APP_B.BIN`.
|
||||||
|
- Оператор кладёт на SD **оба** и не думает, какой слот сейчас активен — bootloader сам берёт нужный
|
||||||
|
под инактивный слот.
|
||||||
|
- Version-gate: bootloader читает версию из выбранного файла (в обоих одна) — сравнение корректно.
|
||||||
|
|
||||||
|
Это простое изменение (одна развилка имени файла по `target_slot`), но его **надо сделать до первого
|
||||||
|
реального полевого обновления tft_app**. Пока стоит `test_stub` — не мешает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Альтернатива — PIC (почему не сейчас)
|
||||||
|
|
||||||
|
Позиционно-независимый образ (один бинарь на оба слота) — теоретически убирает дублирование линковки.
|
||||||
|
Но на bare-metal Cortex-M это **ROPI/RWPI**: флаги компилятора, PI-совместимый startup, `r9` как
|
||||||
|
static base для RW-данных, и **все** библиотеки (FreeRTOS, SDK-драйверы, шрифты) собранные в PI-режиме,
|
||||||
|
плюс runtime-оверхед. Объём работы и риск большие; для Direct-XIP индустрия стандартно выбирает
|
||||||
|
dual-link. Оставляем PIC на «если поддержка двух сборок станет реальной обузой» — не сейчас.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Конкретные TODO для включения реального tft_app (сводка)
|
||||||
|
|
||||||
|
1. **Параметризованный `.ld`** tft_app под два адреса слота (образец — `test_stub`
|
||||||
|
`MIMXRT1052xxxxx_mcuboot_slot.ld` + `--defsym=__slot_base__=`).
|
||||||
|
2. **Рецепт сборки+подписи** двух бинарей на релиз (по образцу `build-mcuboot-stub`, но production-ключ).
|
||||||
|
3. **Расширить `sd_update`**: `TFT_APP_<A|B>.BIN` по `decision.target_slot` (§5).
|
||||||
|
4. **Контракт tft_app** (Фаза 6, 6c): обслуживание WDOG (alive-flag), отложенный `boot_set_confirmed()`,
|
||||||
|
`bsp_boot_health_mark()`.
|
||||||
|
5. **Решить**: production заливать только Slot A или A+Б? Достаточно A — первое обновление наполнит Б
|
||||||
|
и даст fallback *другой* версии (A+Б одной версией от class-B не спасает — тот же баг в обоих).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Связанные документы
|
||||||
|
|
||||||
|
- [BOOTLOADER_FLASH_MAP.md](BOOTLOADER_FLASH_MAP.md) — адреса слотов, обоснование размеров.
|
||||||
|
- [../../firmware/bootloader/PLAN.md](../../firmware/bootloader/PLAN.md) — фазы; Фаза 6 (recovery,
|
||||||
|
taxonomy зависаний), Фаза 5 (HAB/production/service-tui).
|
||||||
|
- [HAB_GUIDE.md](HAB_GUIDE.md) — подпись самого bootloader (HAB), не путать с imgtool-подписью образов
|
||||||
|
tft_app.
|
||||||
111
firmware/bootloader/CMakeLists.txt
Normal file
111
firmware/bootloader/CMakeLists.txt
Normal 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")
|
||||||
168
firmware/bootloader/README.md
Normal file
168
firmware/bootloader/README.md
Normal 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` генерируется, вручную не редактируется.
|
||||||
35
firmware/bootloader/fatfs/CMakeLists.txt
Normal file
35
firmware/bootloader/fatfs/CMakeLists.txt
Normal 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)
|
||||||
86
firmware/bootloader/fatfs/include/ffconf.h
Normal file
86
firmware/bootloader/fatfs/include/ffconf.h
Normal 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_ */
|
||||||
53
firmware/bootloader/fatfs/src/diskio.c
Normal file
53
firmware/bootloader/fatfs/src/diskio.c
Normal 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;
|
||||||
|
}
|
||||||
43
firmware/bootloader/mcuboot_port/bootutil_sources.cmake
Normal file
43
firmware/bootloader/mcuboot_port/bootutil_sources.cmake
Normal 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}")
|
||||||
62
firmware/bootloader/mcuboot_port/flash_map.h
Normal file
62
firmware/bootloader/mcuboot_port/flash_map.h
Normal 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_ */
|
||||||
280
firmware/bootloader/mcuboot_port/flash_map_backend.c
Normal file
280
firmware/bootloader/mcuboot_port/flash_map_backend.c
Normal 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 может только сбрасывать биты 1→0,
|
||||||
|
* запись 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;
|
||||||
|
}
|
||||||
|
|
@ -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_ */
|
||||||
27
firmware/bootloader/mcuboot_port/keys.c
Normal file
27
firmware/bootloader/mcuboot_port/keys.c
Normal 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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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_ */
|
||||||
|
|
@ -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_ */
|
||||||
26
firmware/bootloader/mcuboot_port/sysflash/sysflash.h
Normal file
26
firmware/bootloader/mcuboot_port/sysflash/sysflash.h
Normal 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_ */
|
||||||
109
firmware/bootloader/src/boot_select.c
Normal file
109
firmware/bootloader/src/boot_select.c
Normal 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); /* при успехе не возвращается */
|
||||||
|
}
|
||||||
21
firmware/bootloader/src/boot_select.h
Normal file
21
firmware/bootloader/src/boot_select.h
Normal 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_ */
|
||||||
257
firmware/bootloader/src/cli.c
Normal file
257
firmware/bootloader/src/cli.c
Normal 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
firmware/bootloader/src/cli.h
Normal file
50
firmware/bootloader/src/cli.h
Normal 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_ */
|
||||||
282
firmware/bootloader/src/dev_sdram_test.c
Normal file
282
firmware/bootloader/src/dev_sdram_test.c
Normal 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);
|
||||||
|
}
|
||||||
45
firmware/bootloader/src/dev_sdram_test.h
Normal file
45
firmware/bootloader/src/dev_sdram_test.h
Normal 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_ */
|
||||||
113
firmware/bootloader/src/led_status.c
Normal file
113
firmware/bootloader/src/led_status.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
firmware/bootloader/src/led_status.h
Normal file
76
firmware/bootloader/src/led_status.h
Normal 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_ */
|
||||||
313
firmware/bootloader/src/main.c
Normal file
313
firmware/bootloader/src/main.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
firmware/bootloader/src/protocol.c
Normal file
138
firmware/bootloader/src/protocol.c
Normal 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 */
|
||||||
177
firmware/bootloader/src/protocol.h
Normal file
177
firmware/bootloader/src/protocol.h
Normal 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_ */
|
||||||
75
firmware/bootloader/src/recovery.c
Normal file
75
firmware/bootloader/src/recovery.c
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* @file recovery.c
|
||||||
|
* @brief Реализация — см. recovery.h.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "recovery.h"
|
||||||
|
|
||||||
|
/* ── Определение активного слота ──────────────────────────────────────────
|
||||||
|
* Тот же приём, что find_active_slot() в update_policy.c (private там) —
|
||||||
|
* не шарим напрямую, копия минимальна и независимо тестируется здесь же,
|
||||||
|
* как уже принято в проекте для мелкой логики между модулями. */
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
bool have_active;
|
||||||
|
update_policy_slot_t active_slot;
|
||||||
|
struct image_version active_ver;
|
||||||
|
} active_slot_info_t;
|
||||||
|
|
||||||
|
static active_slot_info_t find_active_slot(const update_policy_slot_state_t *p_slot_a,
|
||||||
|
const update_policy_slot_state_t *p_slot_b)
|
||||||
|
{
|
||||||
|
active_slot_info_t info = { .have_active = false };
|
||||||
|
|
||||||
|
if (p_slot_a->valid)
|
||||||
|
{
|
||||||
|
info.have_active = true;
|
||||||
|
info.active_slot = UPDATE_POLICY_SLOT_A;
|
||||||
|
info.active_ver = p_slot_a->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p_slot_b->valid &&
|
||||||
|
(!info.have_active || image_version_compare(&p_slot_b->version, &info.active_ver) > 0))
|
||||||
|
{
|
||||||
|
info.have_active = true;
|
||||||
|
info.active_slot = UPDATE_POLICY_SLOT_B;
|
||||||
|
info.active_ver = p_slot_b->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool other_slot_valid(update_policy_slot_t active_slot,
|
||||||
|
const update_policy_slot_state_t *p_slot_a,
|
||||||
|
const update_policy_slot_state_t *p_slot_b)
|
||||||
|
{
|
||||||
|
return (active_slot == UPDATE_POLICY_SLOT_A) ? p_slot_b->valid : p_slot_a->valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Публичный API ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
recovery_decision_t recovery_decide(uint32_t attempt_count, uint32_t threshold,
|
||||||
|
const update_policy_slot_state_t *p_slot_a,
|
||||||
|
const update_policy_slot_state_t *p_slot_b, bool btn2_held)
|
||||||
|
{
|
||||||
|
if (btn2_held)
|
||||||
|
{
|
||||||
|
return (recovery_decision_t) { .action = RECOVERY_ENTER_RECOVERY_MODE };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt_count < threshold)
|
||||||
|
{
|
||||||
|
return (recovery_decision_t) { .action = RECOVERY_NORMAL_BOOT };
|
||||||
|
}
|
||||||
|
|
||||||
|
active_slot_info_t active = find_active_slot(p_slot_a, p_slot_b);
|
||||||
|
|
||||||
|
if (active.have_active && other_slot_valid(active.active_slot, p_slot_a, p_slot_b))
|
||||||
|
{
|
||||||
|
return (recovery_decision_t) { .action = RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER,
|
||||||
|
.active_slot = active.active_slot };
|
||||||
|
}
|
||||||
|
|
||||||
|
return (recovery_decision_t) { .action = RECOVERY_ENTER_RECOVERY_MODE };
|
||||||
|
}
|
||||||
72
firmware/bootloader/src/recovery.h
Normal file
72
firmware/bootloader/src/recovery.h
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* @file recovery.h
|
||||||
|
* @brief Чистая логика решения "что делать с этой попыткой загрузки" —
|
||||||
|
* без аппаратных зависимостей (flash/SRC_GPR), полностью
|
||||||
|
* host-тестируема.
|
||||||
|
*
|
||||||
|
* См. firmware/bootloader/PLAN.md, Фаза 6, разбивка 6a.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef RECOVERY_H_
|
||||||
|
#define RECOVERY_H_
|
||||||
|
|
||||||
|
#include "update_policy.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
RECOVERY_NORMAL_BOOT = 0,
|
||||||
|
RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER,
|
||||||
|
RECOVERY_ENTER_RECOVERY_MODE,
|
||||||
|
} recovery_action_t;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
recovery_action_t action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Слот, который нужно стереть перед прыжком. Значим только при
|
||||||
|
* action == RECOVERY_ERASE_ACTIVE_THEN_BOOT_OTHER.
|
||||||
|
*/
|
||||||
|
update_policy_slot_t active_slot;
|
||||||
|
} recovery_decision_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Решить, что делать с текущей попыткой загрузки — таксономия
|
||||||
|
* отказов Фазы 6 (классы B/C/D; класс A закрывается штатным revert
|
||||||
|
* MCUboot без участия этой функции).
|
||||||
|
*
|
||||||
|
* Правила (приоритет сверху вниз):
|
||||||
|
* - btn2_held → RECOVERY_ENTER_RECOVERY_MODE. Ручной триггер главнее
|
||||||
|
* счётчика — оператор может войти в recovery в любой момент, независимо
|
||||||
|
* от истории попыток.
|
||||||
|
* - attempt_count < threshold → RECOVERY_NORMAL_BOOT. Обычная загрузка,
|
||||||
|
* ничего не предпринимаем (счётчик инкрементирует вызывающий код перед
|
||||||
|
* прыжком).
|
||||||
|
* - attempt_count >= threshold и есть "активный" слот (валидный, более
|
||||||
|
* высокой версии — тот же приём, что update_policy_decide()) — он и
|
||||||
|
* есть подозреваемый в зависании:
|
||||||
|
* - другой слот валиден (есть фолбэк) → RECOVERY_ERASE_ACTIVE_THEN_
|
||||||
|
* BOOT_OTHER(active_slot). Вызывающий код стирает active_slot и
|
||||||
|
* обнуляет счётчик — boot_go() сам выберет оставшийся слот.
|
||||||
|
* - другого валидного слота нет (стирать нечего — иначе ноль рабочих
|
||||||
|
* слотов) → RECOVERY_ENTER_RECOVERY_MODE.
|
||||||
|
* - attempt_count >= threshold и активного слота нет вообще →
|
||||||
|
* RECOVERY_ENTER_RECOVERY_MODE (нечего анализировать, нечего стирать).
|
||||||
|
*
|
||||||
|
* @param[in] attempt_count Текущее значение счётчика попыток (bsp_boot_attempt_count()).
|
||||||
|
* @param[in] threshold Порог срабатывания фолбэка/recovery (см. RECOVERY_DEFAULT_THRESHOLD).
|
||||||
|
* @param[in] p_slot_a Состояние Slot A.
|
||||||
|
* @param[in] p_slot_b Состояние Slot Б.
|
||||||
|
* @param[in] btn2_held BSP_BUTTON_2 удержана на старте — ручной вход в recovery.
|
||||||
|
*/
|
||||||
|
recovery_decision_t recovery_decide(uint32_t attempt_count, uint32_t threshold,
|
||||||
|
const update_policy_slot_state_t *p_slot_a,
|
||||||
|
const update_policy_slot_state_t *p_slot_b, bool btn2_held);
|
||||||
|
|
||||||
|
/** @brief Дефолтный порог — 3 сброса подряд до фолбэка/recovery. */
|
||||||
|
#define RECOVERY_DEFAULT_THRESHOLD 3U
|
||||||
|
|
||||||
|
#endif /* RECOVERY_H_ */
|
||||||
273
firmware/bootloader/src/sd_update.c
Normal file
273
firmware/bootloader/src/sd_update.c
Normal 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);
|
||||||
|
}
|
||||||
50
firmware/bootloader/src/sd_update.h
Normal file
50
firmware/bootloader/src/sd_update.h
Normal 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_ */
|
||||||
65
firmware/bootloader/src/slot_version.c
Normal file
65
firmware/bootloader/src/slot_version.c
Normal 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;
|
||||||
|
}
|
||||||
37
firmware/bootloader/src/slot_version.h
Normal file
37
firmware/bootloader/src/slot_version.h
Normal 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_ */
|
||||||
110
firmware/bootloader/src/update_policy.c
Normal file
110
firmware/bootloader/src/update_policy.c
Normal 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 };
|
||||||
|
}
|
||||||
116
firmware/bootloader/src/update_policy.h
Normal file
116
firmware/bootloader/src/update_policy.h
Normal 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_ */
|
||||||
25
firmware/bootloader/src/version.h.in
Normal file
25
firmware/bootloader/src/version.h.in
Normal 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_ */
|
||||||
186
firmware/bootloader/test_stub/CMakeLists.txt
Normal file
186
firmware/bootloader/test_stub/CMakeLists.txt
Normal 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)
|
||||||
161
firmware/bootloader/test_stub/main.c
Normal file
161
firmware/bootloader/test_stub/main.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,6 @@
|
||||||
- [Как добавить новый тест](#как-добавить-новый-тест)
|
- [Как добавить новый тест](#как-добавить-новый-тест)
|
||||||
- [Host unit-тесты](#host-unit-тесты)
|
- [Host unit-тесты](#host-unit-тесты)
|
||||||
- [Версионирование](#версионирование)
|
- [Версионирование](#версионирование)
|
||||||
- [Архитектурные решения (закрыты)](#архитектурные-решения-закрыты)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -199,16 +198,16 @@ main.c
|
||||||
|
|
||||||
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
|
**BSP-зависимости тест-модулей** (по `target_link_libraries` в `CMakeLists.txt`):
|
||||||
|
|
||||||
| Тест | BSP модуль |
|
| Тест | BSP модуль |
|
||||||
| -------------- | ------------------------------ |
|
| -------------- | ---------------------------------- |
|
||||||
| `test_sdram` | `bsp_sdram` |
|
| `test_sdram` | `bsp_sdram` |
|
||||||
| `test_qspi` | `bsp_qspi_flash` |
|
| `test_qspi` | `bsp_qspi_flash` |
|
||||||
| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) |
|
| `test_usd` | `bsp_sd` (+ `firmware_test_fatfs`) |
|
||||||
| `test_display` | `bsp_display` |
|
| `test_display` | `bsp_display` |
|
||||||
| `test_buttons` | `bsp_button` |
|
| `test_buttons` | `bsp_button` |
|
||||||
| `test_opto` | `bsp_opto` (rs_as_gpio=true) |
|
| `test_opto` | `bsp_opto` (rs_as_gpio=true) |
|
||||||
| `test_can` | `bsp_can` |
|
| `test_can` | `bsp_can` |
|
||||||
| `test_mqs` | `bsp_mqs` |
|
| `test_mqs` | `bsp_mqs` |
|
||||||
|
|
||||||
> `bsp_uart_host` также линкуется (используется вне тест-реестра); отдельного
|
> `bsp_uart_host` также линкуется (используется вне тест-реестра); отдельного
|
||||||
> UART-тест-модуля в текущем реестре нет (тестируется в `tests/target`).
|
> UART-тест-модуля в текущем реестре нет (тестируется в `tests/target`).
|
||||||
|
|
@ -271,7 +270,7 @@ main.c
|
||||||
| Интерфейс | USB CDC ACM, разъём J2 |
|
| Интерфейс | USB CDC ACM, разъём J2 |
|
||||||
| Кодировка | UTF-8 |
|
| Кодировка | UTF-8 |
|
||||||
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
|
| Фреймирование | JSON-lines: одна строка = одно сообщение, завершается `\n` |
|
||||||
| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) |
|
| Максимальная длина строки | 128 байт включая `\n` (`CLI_LINE_BUF_SIZE`) |
|
||||||
| CR+LF | Принимается (таргет отбрасывает `\r`) |
|
| CR+LF | Принимается (таргет отбрасывает `\r`) |
|
||||||
|
|
||||||
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
|
Нет хэндшейка, нет sequence number, нет подтверждений доставки.
|
||||||
|
|
@ -551,16 +550,16 @@ confirm id для pre-confirm равен id теста (`usd`) — механи
|
||||||
|
|
||||||
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
|
Порядок — как в реестре `k_registry[]` (`test_runner.c`).
|
||||||
|
|
||||||
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
|
| № | ID | Название | Тип | Critical | M5 HIL | Confirm |
|
||||||
| --- | --------- | ------------------ | ---------------------------- | -------- | ------ | --------------- |
|
| --- | --------- | ------------------ | ----------- | -------- | ------ | ------------- |
|
||||||
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
| 1 | `sdram` | SDRAM 32 MB | self | ✅ | ❌ | ❌ |
|
||||||
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
| 2 | `qspi` | QSPI Flash W25Qxx | self | ✅ | ❌ | ❌ |
|
||||||
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
|
| 3 | `usd` | microSD (SDIO) | interactive | ❌ | ❌ | ✅ pre_confirm |
|
||||||
| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() |
|
| 4 | `display` | TFT Display RGB888 | interactive | ❌ | ❌ | ✅ 6× в run() |
|
||||||
| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only |
|
| 5 | `buttons` | Test Buttons | interactive | ❌ | ❌ | prompt only |
|
||||||
| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) |
|
| 6 | `opto` | Opto Inputs | HIL | ❌ | ✅ | ✅ 6× (авто) |
|
||||||
| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) |
|
| 7 | `can` | CAN loopback | HIL | ❌ | ✅ | ✅ 2× (авто) |
|
||||||
| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() |
|
| 8 | `mqs` | MQS Audio Out | interactive | ❌ | ❌ | ✅ 1× в run() |
|
||||||
|
|
||||||
**Типы confirm:**
|
**Типы confirm:**
|
||||||
|
|
||||||
|
|
@ -823,4 +822,4 @@ session_start / version_response: "fw":"0.1.2"
|
||||||
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
|
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
|
||||||
смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`.
|
смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`.
|
||||||
|
|
||||||
##
|
|
||||||
|
|
|
||||||
|
|
@ -232,10 +232,58 @@ hab-verify project="firmware_test" type="release":
|
||||||
exit 1 ;;
|
exit 1 ;;
|
||||||
esac
|
esac
|
||||||
OUT="/tmp/hab_parse_{{ project }}_{{ type }}.yaml"
|
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}"
|
echo " ✅ ${OUT}"
|
||||||
grep -E "(entry|csf|tag)" "${OUT}" || true
|
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
|
# ГРУППА: quality
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -604,6 +604,16 @@ package-tui:
|
||||||
echo " Бандл TUI без Debug-образа нерабочий (см. README/DEV_ARCH.md)."
|
echo " Бандл TUI без Debug-образа нерабочий (см. README/DEV_ARCH.md)."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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/')
|
VERSION=$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ add_sdk_driver(sai_edma fsl_sai_edma.c)
|
||||||
add_sdk_driver(edma fsl_edma.c)
|
add_sdk_driver(edma fsl_edma.c)
|
||||||
add_sdk_driver(dmamux fsl_dmamux.c)
|
add_sdk_driver(dmamux fsl_dmamux.c)
|
||||||
add_sdk_driver(xbara fsl_xbara.c)
|
add_sdk_driver(xbara fsl_xbara.c)
|
||||||
|
add_sdk_driver(wdog fsl_wdog.c)
|
||||||
|
add_sdk_driver(src fsl_src.c)
|
||||||
|
|
||||||
# Драйверы которые зависят от clock
|
# Драйверы которые зависят от clock
|
||||||
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)
|
target_link_libraries(sdk_lpuart PUBLIC sdk_clock)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ __attribute__((used))
|
||||||
__attribute__((noinline))
|
__attribute__((noinline))
|
||||||
void fih_panic_loop(void)
|
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");
|
__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");
|
__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 */
|
#endif /* FIH_ENABLE_GLOBAL_FAIL */
|
||||||
|
|
|
||||||
|
|
@ -218,3 +218,99 @@ add_host_test(
|
||||||
${BSP_MOCKS_DIR})
|
${BSP_MOCKS_DIR})
|
||||||
|
|
||||||
target_compile_definitions(test_firmware_runner PRIVATE UNIT_TEST)
|
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()
|
||||||
|
|
|
||||||
56
tests/host/button/README.md
Normal file
56
tests/host/button/README.md
Normal 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
66
tests/host/can/README.md
Normal 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
55
tests/host/cli/README.md
Normal 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
42
tests/host/led/README.md
Normal 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
|
||||||
|
```
|
||||||
|
|
@ -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
|
||||||
|
```
|
||||||
60
tests/host/mcuboot_port/README.md
Normal file
60
tests/host/mcuboot_port/README.md
Normal 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`.
|
||||||
189
tests/host/mcuboot_port/fake_flash_map_backend.c
Normal file
189
tests/host/mcuboot_port/fake_flash_map_backend.c
Normal 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;
|
||||||
|
}
|
||||||
34
tests/host/mcuboot_port/fake_flash_map_backend.h
Normal file
34
tests/host/mcuboot_port/fake_flash_map_backend.h
Normal 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_ */
|
||||||
BIN
tests/host/mcuboot_port/fixtures/corrupt_v1.bin
Normal file
BIN
tests/host/mcuboot_port/fixtures/corrupt_v1.bin
Normal file
Binary file not shown.
BIN
tests/host/mcuboot_port/fixtures/valid_v1.bin
Normal file
BIN
tests/host/mcuboot_port/fixtures/valid_v1.bin
Normal file
Binary file not shown.
BIN
tests/host/mcuboot_port/fixtures/valid_v2.bin
Normal file
BIN
tests/host/mcuboot_port/fixtures/valid_v2.bin
Normal file
Binary file not shown.
BIN
tests/host/mcuboot_port/fixtures/valid_v2_unconfirmed.bin
Normal file
BIN
tests/host/mcuboot_port/fixtures/valid_v2_unconfirmed.bin
Normal file
Binary file not shown.
47
tests/host/mcuboot_port/host_link_shims.c
Normal file
47
tests/host/mcuboot_port/host_link_shims.c
Normal 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 не вызывается */
|
||||||
|
}
|
||||||
149
tests/host/mcuboot_port/test_boot_select.c
Normal file
149
tests/host/mcuboot_port/test_boot_select.c
Normal 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
68
tests/host/opto/README.md
Normal 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
|
||||||
|
```
|
||||||
56
tests/host/prio_queue/README.md
Normal file
56
tests/host/prio_queue/README.md
Normal 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
|
||||||
|
```
|
||||||
43
tests/host/protocol/README.md
Normal file
43
tests/host/protocol/README.md
Normal 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
Loading…
Reference in a new issue