Compare commits
No commits in common. "main" and "feature-tui-monolith" have entirely different histories.
main
...
feature-tu
1089 changed files with 1284369 additions and 13700 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: '90'
|
value: '60'
|
||||||
- key: readability-function-size.StatementThreshold
|
- key: readability-function-size.StatementThreshold
|
||||||
value: '30'
|
value: '30'
|
||||||
- key: readability-magic-numbers.IgnoredIntegerValues
|
- key: readability-magic-numbers.IgnoredIntegerValues
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ FLASHLOADER_VID=15a2
|
||||||
FLASHLOADER_PID=0073
|
FLASHLOADER_PID=0073
|
||||||
SERVICE_CDC_VID=1996
|
SERVICE_CDC_VID=1996
|
||||||
SERVICE_CDC_PID=00ad
|
SERVICE_CDC_PID=00ad
|
||||||
SERVICE_M5_VID=303a
|
|
||||||
SERVICE_M5_PID=4001
|
|
||||||
# --- Paths ---
|
# --- Paths ---
|
||||||
# BUILD_DIR и TOOLS_DIR задаются абсолютно в корневом justfile
|
# BUILD_DIR и TOOLS_DIR задаются абсолютно в корневом justfile
|
||||||
# через justfile_directory(), здесь можно переопределить если нужно
|
# через justfile_directory(), здесь можно переопределить если нужно
|
||||||
|
|
@ -62,4 +61,4 @@ HIL_USB_CDC_BAUD=115200
|
||||||
HIL_USB_CDC_TIMEOUT=5.0
|
HIL_USB_CDC_TIMEOUT=5.0
|
||||||
|
|
||||||
# Тип сборки firmware_test для TUI (Debug | Release)
|
# Тип сборки firmware_test для TUI (Debug | Release)
|
||||||
FIRMWARE_BUILD_TYPE=Debug
|
FIRMWARE_BUILD_TYPE=Release
|
||||||
338
.github/workflows/release.yml
vendored
338
.github/workflows/release.yml
vendored
|
|
@ -1,338 +0,0 @@
|
||||||
name: Release
|
|
||||||
|
|
||||||
# Раздельные теги (bootloader — Фаза 5, firmware/bootloader/PLAN.md):
|
|
||||||
# firmware_test, bootloader и service-tui версионируются и релизятся
|
|
||||||
# независимо друг от друга.
|
|
||||||
# firmware-vX.Y.Z → publish-firmware (только firmware_test_hab.bin, Debug)
|
|
||||||
# bootloader-vX.Y.Z → publish-bootloader (только bootloader_hab.bin, Release,
|
|
||||||
# подписан ТЕСТОВЫМ HAB-ключом — см.
|
|
||||||
# firmware/bootloader/SIGNING_CEREMONY.md)
|
|
||||||
# tui-vX.Y.Z → publish-tui (service-tui-vX.Y.Z-{macos,windows}.zip)
|
|
||||||
# HAB firmware_test/bootloader, вшиваемые в TUI-бандл, всегда собираются заново
|
|
||||||
# из текущего HEAD тега tui-v*, а не берутся из отдельных релизов — так проще
|
|
||||||
# и не тянет зависимость на чужой GitHub Release.
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "tui-v*"
|
|
||||||
- "firmware-v*"
|
|
||||||
- "bootloader-v*"
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
release_type:
|
|
||||||
description: "Тестовый прогон без публикации (job publish-* не запускается вне push тега)"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- tui
|
|
||||||
- firmware
|
|
||||||
- bootloader
|
|
||||||
default: tui
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: release-${{ github.workflow }}-${{ github.ref }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# firmware — собирает HAB Debug firmware_test И HAB Release bootloader.
|
|
||||||
# Нужна как для standalone firmware-/bootloader-релизов, так и для вшивания
|
|
||||||
# обоих образов в TUI-бандл (package-tui, Фаза 5) — выполняется всегда.
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
firmware:
|
|
||||||
name: Build firmware_test HAB (Debug) + bootloader HAB (Release)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 90
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Build devcontainer image with cache
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: .devcontainer/Dockerfile
|
|
||||||
tags: tft-devcontainer-ci:latest
|
|
||||||
load: true
|
|
||||||
cache-from: type=gha,scope=tft-devcontainer
|
|
||||||
cache-to: type=gha,mode=max,scope=tft-devcontainer
|
|
||||||
|
|
||||||
- name: Sync host Python tools inside container
|
|
||||||
run: |
|
|
||||||
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \
|
|
||||||
tft-devcontainer-ci:latest bash -lc 'cd tools/host && uv sync'
|
|
||||||
|
|
||||||
- name: Проверить, что тег совпадает с версией в CMakeLists.txt
|
|
||||||
if: startsWith(github.ref, 'refs/tags/firmware-v')
|
|
||||||
run: |
|
|
||||||
tag_version="${GITHUB_REF_NAME#firmware-v}"
|
|
||||||
file_version=$(grep -m1 -oE 'VERSION [0-9]+\.[0-9]+\.[0-9]+' firmware/test/CMakeLists.txt | awk '{print $2}')
|
|
||||||
if [[ "$tag_version" != "$file_version" ]]; then
|
|
||||||
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с VERSION в firmware/test/CMakeLists.txt (${file_version})"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✅ Версия тега совпадает с CMakeLists.txt: ${file_version}"
|
|
||||||
|
|
||||||
- name: Собрать firmware_test HAB (Debug)
|
|
||||||
run: |
|
|
||||||
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \
|
|
||||||
tft-devcontainer-ci:latest bash -lc 'just build::hab-firmware-test-debug'
|
|
||||||
|
|
||||||
- name: Upload firmware_test_hab.bin
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: firmware-hab-debug
|
|
||||||
path: build/Debug/firmware_test_hab.bin
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
- name: Проверить, что тег совпадает с версией в CMakeLists.txt (bootloader)
|
|
||||||
if: startsWith(github.ref, 'refs/tags/bootloader-v')
|
|
||||||
run: |
|
|
||||||
tag_version="${GITHUB_REF_NAME#bootloader-v}"
|
|
||||||
file_version=$(grep -m1 -oE 'VERSION [0-9]+\.[0-9]+\.[0-9]+' firmware/bootloader/CMakeLists.txt | awk '{print $2}')
|
|
||||||
if [[ "$tag_version" != "$file_version" ]]; then
|
|
||||||
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с VERSION в firmware/bootloader/CMakeLists.txt (${file_version})"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✅ Версия тега совпадает с CMakeLists.txt: ${file_version}"
|
|
||||||
|
|
||||||
- name: Собрать bootloader HAB (Release)
|
|
||||||
run: |
|
|
||||||
docker run --rm --user root -v "$GITHUB_WORKSPACE":/workspace -w /workspace \
|
|
||||||
tft-devcontainer-ci:latest bash -lc 'just build::hab-bootloader-release'
|
|
||||||
|
|
||||||
- name: Upload bootloader_hab.bin
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: bootloader-hab-release
|
|
||||||
path: build/Release/bootloader_hab.bin
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# publish-firmware — только на тег firmware-v*, отдельный standalone-релиз
|
|
||||||
# HAB-образа (для tools/host/flash_usb.py и ручной прошивки, не через TUI).
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
publish-firmware:
|
|
||||||
name: Publish firmware release
|
|
||||||
needs: firmware
|
|
||||||
if: startsWith(github.ref, 'refs/tags/firmware-v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download firmware_test_hab.bin
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: firmware-hab-debug
|
|
||||||
path: release-assets/
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
gh release create "${GITHUB_REF_NAME}" \
|
|
||||||
release-assets/firmware_test_hab.bin \
|
|
||||||
--title "firmware_test ${GITHUB_REF_NAME#firmware-v}" \
|
|
||||||
--generate-notes
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# publish-bootloader — только на тег bootloader-v*, отдельный standalone-
|
|
||||||
# релиз HAB-образа (для tools/host/flash_usb.py / SWD, не через TUI).
|
|
||||||
# ⚠️ Подписан ТЕСТОВЫМ HAB-ключом (dev/pre-series, HAB Open чип всё равно
|
|
||||||
# загрузит и unsigned) — реальная SRK-церемония не проведена, см.
|
|
||||||
# firmware/bootloader/SIGNING_CEREMONY.md. Явно проговорено в release notes,
|
|
||||||
# чтобы этот asset не приняли за production-подписанный постфактум.
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
publish-bootloader:
|
|
||||||
name: Publish bootloader release
|
|
||||||
needs: firmware
|
|
||||||
if: startsWith(github.ref, 'refs/tags/bootloader-v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download bootloader_hab.bin
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: bootloader-hab-release
|
|
||||||
path: release-assets/
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
gh release create "${GITHUB_REF_NAME}" \
|
|
||||||
release-assets/bootloader_hab.bin \
|
|
||||||
--title "bootloader ${GITHUB_REF_NAME#bootloader-v}" \
|
|
||||||
--notes "⚠️ HAB-подпись — ТЕСТОВЫЙ ключ (dev/pre-series, HAB Open). Не production. Реальная SRK-церемония — firmware/bootloader/SIGNING_CEREMONY.md."
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# service-tui-{macos,windows} — упаковка PyInstaller-бандла с вшитым HAB
|
|
||||||
# из job firmware. Запускается на тег tui-v* и на workflow_dispatch с
|
|
||||||
# release_type=tui (для тестового прогона без публикации).
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
service-tui-macos:
|
|
||||||
name: Package service-tui (macOS)
|
|
||||||
needs: firmware
|
|
||||||
if: startsWith(github.ref, 'refs/tags/tui-v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'tui')
|
|
||||||
runs-on: macos-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v8.3.2 # без плавающего major-тега начиная с v8 (upstream security policy) — пин точной версии, обновлять периодически
|
|
||||||
|
|
||||||
- name: Install just
|
|
||||||
uses: extractions/setup-just@v4
|
|
||||||
|
|
||||||
- name: Download firmware_test_hab.bin
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: firmware-hab-debug
|
|
||||||
path: build/Debug/
|
|
||||||
|
|
||||||
- name: Download bootloader_hab.bin
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: bootloader-hab-release
|
|
||||||
path: build/Release/
|
|
||||||
|
|
||||||
- name: Проверить, что тег совпадает с версией в pyproject.toml
|
|
||||||
if: startsWith(github.ref, 'refs/tags/tui-v')
|
|
||||||
run: |
|
|
||||||
tag_version="${GITHUB_REF_NAME#tui-v}"
|
|
||||||
file_version=$(grep -m1 '^version' tools/service_tui/pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
|
|
||||||
if [[ "$tag_version" != "$file_version" ]]; then
|
|
||||||
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с version в tools/service_tui/pyproject.toml (${file_version})"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✅ Версия тега совпадает с pyproject.toml: ${file_version}"
|
|
||||||
|
|
||||||
- name: Собрать зависимости и упаковать бандл
|
|
||||||
run: |
|
|
||||||
just host::service-setup
|
|
||||||
just host::package-tui
|
|
||||||
|
|
||||||
- name: Заархивировать бандл
|
|
||||||
working-directory: tools/service_tui/dist
|
|
||||||
run: |
|
|
||||||
name=$(ls -d service-tui-v*-macos)
|
|
||||||
zip -r "${name}.zip" "${name}"
|
|
||||||
|
|
||||||
- name: Upload bundle
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: service-tui-macos
|
|
||||||
path: tools/service_tui/dist/service-tui-v*-macos.zip
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
service-tui-windows:
|
|
||||||
name: Package service-tui (Windows)
|
|
||||||
needs: firmware
|
|
||||||
if: startsWith(github.ref, 'refs/tags/tui-v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'tui')
|
|
||||||
runs-on: windows-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v8.3.2 # без плавающего major-тега начиная с v8 (upstream security policy) — пин точной версии, обновлять периодически
|
|
||||||
|
|
||||||
- name: Install just
|
|
||||||
uses: extractions/setup-just@v4
|
|
||||||
|
|
||||||
- name: Download firmware_test_hab.bin
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: firmware-hab-debug
|
|
||||||
path: build/Debug/
|
|
||||||
|
|
||||||
- name: Download bootloader_hab.bin
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: bootloader-hab-release
|
|
||||||
path: build/Release/
|
|
||||||
|
|
||||||
- name: Проверить, что тег совпадает с версией в pyproject.toml
|
|
||||||
if: startsWith(github.ref, 'refs/tags/tui-v')
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
tag_version="${GITHUB_REF_NAME#tui-v}"
|
|
||||||
file_version=$(grep -m1 '^version' tools/service_tui/pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
|
|
||||||
if [[ "$tag_version" != "$file_version" ]]; then
|
|
||||||
echo "::error::Тег ${GITHUB_REF_NAME} (версия ${tag_version}) не совпадает с version в tools/service_tui/pyproject.toml (${file_version})"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✅ Версия тега совпадает с pyproject.toml: ${file_version}"
|
|
||||||
|
|
||||||
- name: Собрать зависимости и упаковать бандл
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
just host::service-setup
|
|
||||||
just host::package-tui
|
|
||||||
|
|
||||||
- name: Заархивировать бандл
|
|
||||||
working-directory: tools/service_tui/dist
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$name = (Get-ChildItem -Directory -Filter "service-tui-v*-windows").Name
|
|
||||||
Compress-Archive -Path $name -DestinationPath "$name.zip"
|
|
||||||
|
|
||||||
- name: Upload bundle
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: service-tui-windows
|
|
||||||
path: tools/service_tui/dist/service-tui-v*-windows.zip
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 14
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# publish-tui — только на тег tui-v*, релиз с обоими zip-бандлами.
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
publish-tui:
|
|
||||||
name: Publish service-tui release
|
|
||||||
needs: [service-tui-macos, service-tui-windows]
|
|
||||||
if: startsWith(github.ref, 'refs/tags/tui-v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download bundles
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
pattern: service-tui-*
|
|
||||||
path: release-assets/
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
gh release create "${GITHUB_REF_NAME}" \
|
|
||||||
release-assets/*.zip \
|
|
||||||
--title "service-tui ${GITHUB_REF_NAME#tui-v}" \
|
|
||||||
--generate-notes
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -77,4 +77,3 @@ tools/host/.venv-host/
|
||||||
tools/host/.venv-host-win/
|
tools/host/.venv-host-win/
|
||||||
.zed/
|
.zed/
|
||||||
project_tree.txt
|
project_tree.txt
|
||||||
tools/service_tui/dist/
|
|
||||||
|
|
|
||||||
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:bootloader-debug",
|
"preLaunchTask": "build-and-rtt:firmware-test-debug",
|
||||||
},
|
},
|
||||||
// =============================================================
|
// =============================================================
|
||||||
// firmware/tft_app — FreeRTOS task view
|
// firmware/tft_app — FreeRTOS task view
|
||||||
|
|
|
||||||
195
CHANGELOG.md
195
CHANGELOG.md
|
|
@ -52,195 +52,19 @@
|
||||||
|
|
||||||
### Что отслеживать
|
### Что отслеживать
|
||||||
|
|
||||||
- ~~Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow~~ — решено: job `test` уже запускает host-тесты (`just ci::test` → `just build::test-host-release`).
|
- Проверять, остаётся ли `.github/workflows/ci.yml` только сборочным workflow через `just ci::build` или начинает запускать host-тесты.
|
||||||
- Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL.
|
- Отслеживать появление self-hosted runner, регулярных аппаратных прогонов или отчётов по HIL.
|
||||||
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
|
- Проверять новые firmware-test модули в `firmware/test/src/tests/` и синхронные обновления протокола/документации.
|
||||||
- Следить за развитием BSP: RGB (частично закрыто display-тестом); `tft_app` — директория всё ещё не заведена (bootloader — реализован, Фазы 0–6, см. запись ниже).
|
- Следить за развитием BSP: RGB (частично закрыто display-тестом), bootloader или `tft_app` — обе директории всё ещё не заведены.
|
||||||
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log (пример — точечный патч `fault_injection_hardening.c` под `#if defined(__arm__)`, bootloader Фаза 3, см. запись ниже).
|
- Отслеживать локальные патчи поверх vendor SDK, которые нужно вести отдельным patch log.
|
||||||
- ~~Отслеживать мерж ветки `feature-tui-monolith` в `dev`~~ — смёржено (`b4c664f`), запись закрыта датой ниже.
|
- Отслеживать мерж ветки `feature-tui-monolith` в `dev` — после мержа эту запись нужно закрыть датой и финальным диапазоном SHA.
|
||||||
- Отслеживать тег `bootloader-v*` — после первого релиза закрыть запись «bootloader: полная реализация» датой и финальным SHA (сейчас часть диапазона — незакоммиченные изменения рабочего дерева).
|
|
||||||
|
|
||||||
## [Не выпущено] — bootloader: полная реализация, Фазы 0–6 (MCUboot Direct-XIP, HAB, service-tui интеграция)
|
## [Не выпущено] — service-tui: монолитный spsdk-бэкенд, устойчивость к обрыву USB, упаковка PyInstaller
|
||||||
|
|
||||||
Диапазон: `4644f21507d3..6c564f16388a` + незакоммиченные изменения рабочего дерева (HAB-подпись
|
Диапазон: `1801f1beb959d610d31ee3dcd1f91046953117d4..22c40779ef0ec9911031d7a5272c4611b596d3e8` + незакоммиченные изменения рабочего дерева (документация)
|
||||||
тестовым ключом, Тир-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`). Смёржено в `dev` и
|
> Ветка `feature-tui-monolith` (от `dev`, поверх мержа `feature-tui-python`).
|
||||||
> выпущено как часть первого релиза (`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/раннего мержа и с тех пор
|
||||||
|
|
@ -367,6 +191,11 @@
|
||||||
|
|
||||||
### Известные ограничения
|
### Известные ограничения
|
||||||
|
|
||||||
|
- `service_tui.spec` не включает `datas` для `spsdk`/`dcd/*.bin`/
|
||||||
|
`pyproject.toml`, хотя уже собранные alpha-бандлы их содержат — спек
|
||||||
|
нужно синхронизировать перед следующей сборкой релиза.
|
||||||
|
- `pyusb` в `pyproject.toml` — мёртвая зависимость (Р7 перевёл детект на
|
||||||
|
`spsdk`/`serial.tools.list_ports`), кандидат на удаление.
|
||||||
- Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решили
|
- Auto-config Flashloader для W25Q256/512 не проверялся напрямую — решили
|
||||||
не полагаться на него вообще, FCB для кастомных бинарей всегда пишется
|
не полагаться на него вообще, FCB для кастомных бинарей всегда пишется
|
||||||
явно.
|
явно.
|
||||||
|
|
|
||||||
|
|
@ -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,10 +46,8 @@ 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/bootloader) - add_subdirectory(firmware/app) #
|
||||||
# Заглушка tft_app для аппаратной верификации bootutil (Фаза 2) — удалить,
|
# Загрузчик + Основное приложение
|
||||||
# когда появится реальный firmware/tft_app. См. firmware/bootloader/PLAN.md.
|
|
||||||
add_subdirectory(firmware/bootloader/test_stub)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -126,19 +126,6 @@
|
||||||
"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",
|
||||||
|
|
@ -163,11 +150,7 @@
|
||||||
"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"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -186,11 +169,7 @@
|
||||||
"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"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
202
FIRST_RELEASE_PLAN.md
Normal file
202
FIRST_RELEASE_PLAN.md
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
# План первого релиза в GitHub
|
||||||
|
|
||||||
|
> Цель: в GitHub Releases появляется тег с прикреплёнными бинарниками —
|
||||||
|
> `firmware_test` (HAB-образ) и `service-tui` (standalone-бандлы под macOS и
|
||||||
|
> Windows). Этот документ — практический чек-лист «как довести до кнопки
|
||||||
|
> Publish», а не повторение инженерных фаз `RELEASE_ROADMAP.md`.
|
||||||
|
|
||||||
|
## Как связаны два артефакта
|
||||||
|
|
||||||
|
`firmware_test` (C, i.MX RT1052, версия `0.1.2` из
|
||||||
|
`firmware/test/CMakeLists.txt`) и `service-tui` (Python/Textual, версия
|
||||||
|
`0.2.0` из `tools/production/pyproject.toml`) — независимо версионируемые
|
||||||
|
проекты, но релиз одного без другого бесполезен сервисному инженеру:
|
||||||
|
`service-tui` — это инструмент, которым он *прошивает* плату диагностической
|
||||||
|
прошивкой, и HAB-образ `firmware_test` кладётся внутрь бандла TUI как
|
||||||
|
`firmware/<Type>/firmware_test_hab.bin` (см. `just host::package-tui`,
|
||||||
|
`tools/production/docs/DEV_ARCH.md` §14). Поэтому релиз собирается как один
|
||||||
|
комплект, даже если версии независимые.
|
||||||
|
|
||||||
|
Известное ограничение (задокументировано в `README.md`/`DEV_ARCH.md`):
|
||||||
|
Release-сборка `firmware_test` нестабильна (проблема с FCB/clock), поэтому
|
||||||
|
в бандл TUI кладётся **Debug**-образ (`FIRMWARE_BUILD_TYPE=Debug`). В релиз
|
||||||
|
GitHub имеет смысл положить оба HAB-образа отдельными assets (Debug — как
|
||||||
|
основной для TUI, Release — с пометкой «experimental», для тех, кто прошивает
|
||||||
|
через `tools/host/flash_usb.py` вручную), либо только Debug — см. открытый
|
||||||
|
вопрос в шаге 1.5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текущее состояние (снимок на момент написания плана)
|
||||||
|
|
||||||
|
| Область | Состояние |
|
||||||
|
| --- | --- |
|
||||||
|
| `firmware_test` | Собирается, HAB-образ генерируется (`just build::hab-firmware-test-{debug,release}`), Release нестабилен |
|
||||||
|
| `service-tui` | v0.2.0, PyInstaller onedir, alpha-бандлы уже вручную собраны и прогнаны на живом железе macOS+Windows (коммиты `c694258`/`bfe4dd6`) |
|
||||||
|
| `service_tui.spec` | **Устарел относительно того, чем реально собраны протестированные alpha-бандлы** — не содержит `datas` для `spsdk`, `dcd/*.bin`, `pyproject.toml` (задокументировано в `DEV_ARCH.md` §14 и `CHANGELOG.md`「Известные ограничения」) |
|
||||||
|
| `tools/production/dist/service-tui-v0.2.0-{macos,windows}/` | Закоммичены в git (906 файлов, ~96 МБ суммарно) и **устарели относительно HEAD** — собраны до коммитов `2dbe3e6`/`22c4077`/`31e3237` (фиксы моков тестов, рефакторинг докстрингов) |
|
||||||
|
| CI (`.github/workflows/ci.yml`) | Только `build`+`test` в devcontainer на `ubuntu-latest`; не собирает `service-tui`, нет macOS/Windows раннеров, нет release-пайплайна, нет тегов в репозитории |
|
||||||
|
| `just/ci.just` | Есть рецепт `release` (→ `just build::hab-all-release`) — только firmware, ничего про упаковку TUI или публикацию на GitHub |
|
||||||
|
| Ветки | `feature-tui-monolith` на 15 коммитов впереди `dev`, ещё не смёржена; в репозитории также есть `main` — политика, какая ветка режет релизы, явно не зафиксирована |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 1 — Закрыть блокирующие долги перед тегом
|
||||||
|
|
||||||
|
Без этого CI-сборка (шаг 3) не будет соответствовать тому, что уже
|
||||||
|
провалидировано на железе, — а «релиз, который не воспроизводим из
|
||||||
|
исходников» хуже отсутствия релиза.
|
||||||
|
|
||||||
|
1. **Актуализировать `tools/production/service_tui.spec`** — добавить
|
||||||
|
`collect_data_files("spsdk")` (+ `SPSDK_DATA_FOLDER` если понадобится),
|
||||||
|
`collect_dynamic_libs("libusbsio")`, `datas` для
|
||||||
|
`tools/host/dcd/{dcd.bin,w25q128_fdcb.bin,w25q512_fdcb.bin,ivt_flashloader.bin}`
|
||||||
|
и `pyproject.toml`. Ориентир — реальное содержимое уже собранных
|
||||||
|
alpha-бандлов в `dist/` (их можно инспектировать перед удалением из git,
|
||||||
|
см. следующий пункт).
|
||||||
|
2. **Убрать `tools/production/dist/` из git**: `git rm -r --cached
|
||||||
|
tools/production/dist` + добавить `tools/production/dist/` в
|
||||||
|
`.gitignore`. Собранные бандлы — это build-артефакты, их место в GitHub
|
||||||
|
Release assets или CI-артефактах, не в истории репозитория.
|
||||||
|
3. **(Дёшево, но не блокирует)** Убрать мёртвую зависимость `pyusb` из
|
||||||
|
`tools/production/pyproject.toml` — детект давно переведён на
|
||||||
|
`spsdk`/`serial.tools.list_ports` (Р7), ни один модуль `app/` её не
|
||||||
|
импортирует.
|
||||||
|
4. **Пересобрать бандлы локально** с исправленным spec из актуального HEAD
|
||||||
|
(`just host::package-tui` на macOS и на Windows) и повторить хотя бы
|
||||||
|
дымовой прогон чек-листа Гейта 5 из `RELEASE_ROADMAP.md` (детект SDP →
|
||||||
|
прошивка `firmware_test` → диагностика → выход). Полный деструктивный
|
||||||
|
чек-лист (обрыв USB и т.п.) уже пройден на предыдущей сборке — здесь
|
||||||
|
цель убедиться, что исправленный spec не сломал состав бандла, а не
|
||||||
|
повторять всё с нуля.
|
||||||
|
5. **Открытый вопрос:** класть ли в релиз Release-сборку `firmware_test`
|
||||||
|
вообще (сейчас нестабильна) — по умолчанию план предполагает **только
|
||||||
|
Debug**-образ как основной asset, Release не публикуется до починки
|
||||||
|
FCB/clock-проблемы. Требует подтверждения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 2 — Версия и тег
|
||||||
|
|
||||||
|
1. **Схема тега** — `firmware_test` (0.1.2) и `service-tui` (0.2.0)
|
||||||
|
версионируются независимо. Предлагается: тег вида `vX.Y.Z` = версия
|
||||||
|
`service-tui` (это главный продукт релиза для сервисного инженера),
|
||||||
|
версия `firmware_test` указывается в описании релиза отдельной строкой.
|
||||||
|
Альтернатива — раздельные теги (`tui-v0.2.0` + `firmware-v0.1.2`), если
|
||||||
|
в будущем оба проекта должны релизиться независимо друг от друга.
|
||||||
|
**Требует подтверждения**, план ниже считает первый вариант.
|
||||||
|
2. **Ветка релиза** — в репозитории есть и `dev`, и `main`, при этом
|
||||||
|
`main` в `git log` не встречается в истории `feature-tui-monolith`/`dev`
|
||||||
|
(нужно свериться отдельно, если `main` уже используется под что-то
|
||||||
|
другое). Рекомендация: смёржить `feature-tui-monolith → dev`, затем
|
||||||
|
`dev → main`, тег ставить на `main` — так `main` остаётся точкой,
|
||||||
|
соответствующей опубликованным релизам, а `dev` — интеграционной веткой.
|
||||||
|
**Требует подтверждения**, если у проекта другая договорённость про
|
||||||
|
`main`.
|
||||||
|
3. **`CHANGELOG.md`** — закрыть секцию `[Не выпущено] — service-tui: ...`
|
||||||
|
→ `[YYYY-MM-DD] — v0.2.0`, вычеркнуть из «Известные ограничения» то, что
|
||||||
|
закрывается шагом 1 (spec-расхождение, `pyusb`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 3 — CI: собрать релизные бинарники автоматически
|
||||||
|
|
||||||
|
Текущий `.github/workflows/ci.yml` собирает только `firmware_test` на
|
||||||
|
`ubuntu-latest` внутри devcontainer — этого недостаточно для
|
||||||
|
кросс-платформенной упаковки `service-tui`. Нужен отдельный workflow,
|
||||||
|
не смешанный с обычным PR-циклом (см. `just/ci_workflow.md`, «Шаг 3 —
|
||||||
|
выделить release workflow»).
|
||||||
|
|
||||||
|
Новый `.github/workflows/release.yml`, триггер — тег `v*` (плюс
|
||||||
|
`workflow_dispatch` для тестового прогона без публикации):
|
||||||
|
|
||||||
|
| Job | Раннер | Что делает |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `firmware` | `ubuntu-latest` (тот же devcontainer-подход, что в `ci.yml`) | `just ci::release` → `hab-all-release` (по факту нужен только `firmware_test`, Debug+Release); выгрузить `firmware_test_hab.bin` (оба типа) как артефакт |
|
||||||
|
| `service-tui-macos` | `macos-latest` | скачать firmware-артефакт из job `firmware`; `uv sync` в `tools/production`; `just host::package-tui`; заархивировать `dist/service-tui-vX.Y.Z-macos/` |
|
||||||
|
| `service-tui-windows` | `windows-latest` | то же самое, PowerShell-совместимые команды (`just`/`uv` доступны на Windows) |
|
||||||
|
| `publish-release` | `ubuntu-latest`, `needs: [firmware, service-tui-macos, service-tui-windows]` | скачать все артефакты, создать GitHub Release через `gh release create` / `softprops/action-gh-release@v2`, прикрепить `firmware_test_hab.bin` (Debug, + Release с пометкой experimental, если решение по шагу 1.5 — «класть оба»), `service-tui-vX.Y.Z-macos.zip`, `service-tui-vX.Y.Z-windows.zip` |
|
||||||
|
|
||||||
|
Важные нюансы:
|
||||||
|
|
||||||
|
- Firmware для бандла TUI собирается **один раз** в job `firmware` и
|
||||||
|
передаётся в macOS/Windows job'ы артефактом — пересобирать ARM-прошивку
|
||||||
|
на каждом раннере отдельно избыточно (и на macOS/Windows раннерах нет
|
||||||
|
подготовленного devcontainer/toolchain).
|
||||||
|
- Аппаратные гейты (детект SDP на живой плате, деструктивные сценарии
|
||||||
|
обрыва USB) **CI выполнить не может** — GitHub-hosted раннеры не видят
|
||||||
|
реальное USB-устройство. Это ручной шаг, который уже пройден один раз
|
||||||
|
вручную (коммиты «MacOS tested»/«Windows tested») и должен повторяться
|
||||||
|
вручную перед каждым релизом, пока не поднят self-hosted HIL-раннер
|
||||||
|
(см. `just/ci_workflow.md`, «Шаг 5»). План релиза это не блокирует, но
|
||||||
|
release notes должны явно фиксировать, что сборка прошла ручную проверку
|
||||||
|
на железе, а не только CI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 4 — Ручные шаги перед Publish
|
||||||
|
|
||||||
|
1. Скачать `service-tui-vX.Y.Z-{macos,windows}.zip`, собранные именно CI
|
||||||
|
(не локальную сборку из шага 1.4) — прогнать сокращённый чек-лист Гейта
|
||||||
|
5: детект SDP → прошивка `firmware_test` → диагностика на обеих ОС.
|
||||||
|
Цель — убедиться, что CI-сборка не разошлась с уже провалидированной
|
||||||
|
локальной.
|
||||||
|
2. Обновить `tools/production/README.md`/корневой `README.md` — ссылка на
|
||||||
|
релиз/инструкция «откуда скачать сервисному инженеру».
|
||||||
|
|
||||||
|
## Шаг 5 — Публикация
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag vX.Y.Z
|
||||||
|
git push origin vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
— триггерит `release.yml`. Проверить, что все 3 asset'а прикрепились и
|
||||||
|
release notes корректны (описание можно сгенерировать из секции
|
||||||
|
`CHANGELOG.md` за этот релиз + `gh release create --generate-notes` как
|
||||||
|
дополнение). Если Гейт 6 (`RELEASE_ROADMAP.md`) закрыт не полностью
|
||||||
|
(например, POST-1 сознательно отложен — это нормально, он и заявлен как
|
||||||
|
пост-релизный) — релиз всё равно можно публиковать как обычный, а не
|
||||||
|
pre-release, POST-1 не блокирует v1 по замыслу roadmap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 6 — Развитие CI после первого релиза (не блокирует, но логично заложить сразу)
|
||||||
|
|
||||||
|
Эти пункты уже зафиксированы в `just/ci_workflow.md` («Рекомендуемые
|
||||||
|
следующие шаги»), возвращаемся сюда после первого релиза:
|
||||||
|
|
||||||
|
- `lint` job (`just ci::lint` сейчас заглушка) — `clang-format --dry-run
|
||||||
|
--Werror` + `clang-tidy`.
|
||||||
|
- Coverage (`just ci::_coverage` уже есть, но не подключён в workflow).
|
||||||
|
- Публикация devcontainer image в GHCR — сократит время `firmware` job в
|
||||||
|
`release.yml` и обычном `ci.yml` (сейчас образ пересобирается в каждой
|
||||||
|
job, даже с layer-кэшем).
|
||||||
|
- Self-hosted HIL lane — единственный способ когда-нибудь автоматизировать
|
||||||
|
то, что сейчас в шаге 4 делается руками.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сводная последовательность
|
||||||
|
|
||||||
|
```
|
||||||
|
Шаг 1 (spec + dist из git + пересборка)
|
||||||
|
│
|
||||||
|
Шаг 2 (тег/ветка/CHANGELOG — решения по открытым вопросам)
|
||||||
|
│
|
||||||
|
Шаг 3 (release.yml: firmware → macOS/Windows package → publish)
|
||||||
|
│
|
||||||
|
Шаг 4 (ручная проверка CI-бинарников на железе)
|
||||||
|
│
|
||||||
|
Шаг 5 (git tag → publish)
|
||||||
|
│
|
||||||
|
Шаг 6 (lint/coverage/GHCR/HIL — после релиза, не блокирует)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Открытые вопросы, требующие решения пользователя
|
||||||
|
|
||||||
|
| # | Вопрос | Где всплывает |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1 | Класть ли Release-сборку `firmware_test` в релиз (сейчас нестабильна) | Шаг 1.5 |
|
||||||
|
| 2 | Схема тега — один `vX.Y.Z` (=версия TUI) или раздельные теги firmware/TUI | Шаг 2.1 |
|
||||||
|
| 3 | Тег ставится на `main` (после `dev → main`) или сразу на `dev`/на самой feature-ветке | Шаг 2.2 |
|
||||||
35
README.md
35
README.md
|
|
@ -9,21 +9,21 @@
|
||||||
|
|
||||||
## Firmware-проекты
|
## Firmware-проекты
|
||||||
|
|
||||||
| Проект | Путь | Описание |
|
| Проект | Путь | Описание |
|
||||||
| ----------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------- |
|
||||||
| Тестовая прошивка | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS [README](firmware/test/README.md) |
|
| Тестовая прошивка (✅ реализована) | `firmware/test/` | Входной контроль платы: CAN, UART, SDRAM, QSPI, SDIO, RGB, оптовходы, LED, кнопки, MQS |
|
||||||
| Загрузчик | `firmware/bootloader/` | A/Б обновление через uSD (MCUboot, Direct-XIP), recovery при зависании образа. Обновляется только через USB ROM + blhost / SWD [README](firmware/bootloader/README.md) |
|
| Загрузчик (⏳ запланирован) | `firmware/bootloader/` | A/B обновление через uSD. Обновляется только через USB ROM + blhost / SWD |
|
||||||
| Production прошивка (запланирована) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
|
| Production прошивка (⏳ запланирован) | `firmware/tft_app/` | Приложение с реализацией логики лифтового индикатора. Обновляется загрузчиком |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Инструменты (`tools/`)
|
## Инструменты (`tools/`)
|
||||||
|
|
||||||
| Инструмент | Путь | Назначение |
|
| Инструмент | Путь | Назначение |
|
||||||
| ------------------ | -------------------- | ---------------------------------------------------------------------------------------------- |
|
| ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||||
| Сервисный TUI | `tools/service_tui/` | Диагностика и прошивка готовых плат сервисным инженером [README](tools/service_tui/README.md) |
|
| Сервисный TUI | `tools/production/` | Диагностика и прошивка готовых плат сервисным инженером (Textual, standalone-бинарь). [README](tools/production/README.md) |
|
||||||
| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке [README](tools/host/README.md) |
|
| Прошивка (dev-CLI) | `tools/host/` | USB SDP / SWD прошивка при разработке (`sdphost`/`blhost`/`nxpimage`/`pyOCD`). [README](tools/host/README.md) |
|
||||||
| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов [README](tools/hil/README.md) |
|
| HIL-тесты | `tools/hil/` | pytest-окружение аппаратных тестов (pyOCD + M5StampPLC). [README](tools/hil/README.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -68,6 +68,21 @@ just host::debug-server # GDB-сервер для отладки
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Зависимости
|
||||||
|
|
||||||
|
| | Подход |
|
||||||
|
| --------------------------------------------- | -------------------- |
|
||||||
|
| NXP MCUXpresso SDK, FreeRTOS, FatFS, LittleFS | vendored |
|
||||||
|
| Unity, fff, SEGGER RTT | vendored |
|
||||||
|
| pyOCD, pyserial, pytest, mpremote | `tools/hil/uv.lock` |
|
||||||
|
| spsdk (nxpimage, blhost, sdphost — dev-CLI) | `tools/host/uv.lock` |
|
||||||
|
| spsdk (McuBoot/SDP/HabImage — прямой Python API), Textual | `tools/production/uv.lock` |
|
||||||
|
|
||||||
|
Всё что не меняется — vendored. Сборка работает после `git clone` без интернета
|
||||||
|
(кроме Python-зависимостей).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ 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)
|
||||||
|
|
@ -12,8 +15,6 @@ 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)
|
||||||
|
|
@ -22,6 +23,7 @@ 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 подавляет предупреждения для всех внешних потребителей
|
||||||
|
|
@ -64,8 +66,3 @@ 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)
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
# 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` |
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 */
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,6 +1,24 @@
|
||||||
/*
|
/*
|
||||||
* 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,6 +2,8 @@
|
||||||
* @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,16 +91,6 @@ 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,27 +28,9 @@ 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)
|
|
||||||
{
|
{
|
||||||
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) == BOARD_SDMMC_SD_CD_INSERT_LEVEL;
|
||||||
return (USDHC_GetPresentStatusFlags(BOARD_SDMMC_SD_HOST_BASEADDR) &
|
|
||||||
(uint32_t) kUSDHC_CardInsertedFlag) != 0U;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
|
|
@ -67,8 +49,7 @@ static uint32_t get_usdhc1_src_clock_hz(void)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Управление питанием карты: GPIO1[19] (SdPwr). Регистрируется как
|
* Управление питанием карты: GPIO1[19] (SdPwr), active-high.
|
||||||
* usrParam.pwr — SDK дёргает её из SD_SetCardPower().
|
|
||||||
*/
|
*/
|
||||||
static void sd_power_control(bool enable)
|
static void sd_power_control(bool enable)
|
||||||
{
|
{
|
||||||
|
|
@ -142,12 +123,10 @@ 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 для стабильного уровня. */
|
||||||
* CD (D13) здесь НЕ конфигурируем: пин на USDHC1_CD_B (см. BOARD_SD_Config),
|
IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_12_GPIO2_IO28,
|
||||||
* pad задан в BOARD_InitPins() и на железе даёт корректный CINST. Прежняя
|
IOMUXC_SW_PAD_CTL_PAD_PKE_MASK | IOMUXC_SW_PAD_CTL_PAD_PUE_MASK |
|
||||||
* настройка pad'а GPIO2_IO28 убрана вместе с GPIO-детектом (item 1,
|
IOMUXC_SW_PAD_CTL_PAD_HYS_MASK | IOMUXC_SW_PAD_CTL_PAD_PUS(1));
|
||||||
* DEBUG_LOG_PHASE3_SD.md).
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
|
|
@ -172,10 +151,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: USDHC PRES_STATE.CINST через callback (polling, без IRQ) --- */
|
/* --- card detect: GPIO CD (active-low) --- */
|
||||||
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; /* kSD_DetectCardByGpioCD → callback ниже */
|
s_cd.type = BOARD_SDMMC_SD_CD_TYPE;
|
||||||
s_cd.cardDetected = sd_card_detect_prsstat;
|
s_cd.cardDetected = sd_card_detect_gpio;
|
||||||
s_cd.callback = cd; /* обычно NULL из bsp_sd */
|
s_cd.callback = cd; /* обычно NULL из bsp_sd */
|
||||||
s_cd.userData = user_data;
|
s_cd.userData = user_data;
|
||||||
|
|
||||||
|
|
@ -187,20 +166,15 @@ 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 и настраиваем вход */
|
||||||
* CD_B (GPIO_B1_12 / physical D13) остаётся на USDHC1_CD_B постоянно —
|
IOMUXC_SetPinMux(IOMUXC_GPIO_B1_12_GPIO2_IO28, 0U);
|
||||||
* единый механизм детекта через PRES_STATE.CINST (sd_card_detect_prsstat
|
const gpio_pin_config_t cd_cfg = {
|
||||||
* выше + гейт bsp_sd_is_inserted). Прежней двойной маршрутизации
|
.direction = kGPIO_DigitalInput,
|
||||||
* (remux на GPIO2_IO28 для GPIO-чтения внутри f_mount) больше нет —
|
.outputLogic = 0U,
|
||||||
* см. DEBUG_LOG_PHASE3_SD.md, раунд 3 «консолидация детекта» (item 1);
|
.interruptMode = kGPIO_NoIntmode,
|
||||||
* она убирала латентную хрупкость: после первого bsp_sd_init() пин уходил
|
};
|
||||||
* на GPIO2_IO28 и повторный PRSSTAT-скан ослеп бы. Явно переустанавливаем
|
GPIO_PinInit(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN, &cd_cfg);
|
||||||
* альт-функцию (BOARD_InitPins() её тоже ставит — так модуль не зависит от
|
/* Важно: применяем pad-конфиг сразу для ранних CMD (CMD0/CMD8/CMD55/ACMD41). */
|
||||||
* порядка инициализации). 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,6 +8,8 @@
|
||||||
* LED_HEARTBEAT — системный, мигает как признак жизни прошивки
|
* LED_HEARTBEAT — системный, мигает как признак жизни прошивки
|
||||||
* LED_APP — прикладной, управляется из firmware по ситуации
|
* LED_APP — прикладной, управляется из firmware по ситуации
|
||||||
*
|
*
|
||||||
|
* Пины сконфигурированы в generated/pin_mux.h. Этот хедер не знает
|
||||||
|
* ни про GPIO-порты, ни про NXP SDK.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ target_link_libraries(firmware_test PRIVATE bsp_provisioning)
|
||||||
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
|
||||||
| `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers |
|
| `bsp_board` | PRIVATE | Транзитивно: clock, SDK headers |
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Особенности
|
## Особенности
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,8 @@ 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() */
|
||||||
|
|
||||||
/* Стирание */
|
/* Стирание */
|
||||||
|
|
@ -118,13 +117,9 @@ 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,29 +90,9 @@ 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 байта. Читаем 4 байта,
|
* FlexSPI FIFO работает минимальными единицами в 4 байта (RXWMRK=0, 1 FILL
|
||||||
* используем только byte[0].
|
* unit = 4 bytes). Читаем 4 байта, используем только 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_UNITS =
|
const uint32_t FILL =
|
||||||
(QSPI_BASE->IPRXFSTS & FLEXSPI_IPRXFSTS_FILL_MASK) >> FLEXSPI_IPRXFSTS_FILL_SHIFT;
|
(QSPI_BASE->IPRXFSTS & FLEXSPI_IPRXFSTS_FILL_MASK) >> FLEXSPI_IPRXFSTS_FILL_SHIFT;
|
||||||
if ((FILL_UNITS * QSPI_WM_UNIT_WORDS) >= WORDS_NEEDED)
|
if (FILL >= WORDS_NEEDED)
|
||||||
{
|
{
|
||||||
done = true;
|
done = true;
|
||||||
}
|
}
|
||||||
|
|
@ -508,11 +508,7 @@ 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))
|
||||||
{
|
{
|
||||||
/* IDATSZ округляем вверх до кратного QSPI_RFDR_WORD_BYTES (4) */
|
qspi_ip_setup(seq_idx, addr, data_len);
|
||||||
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;
|
||||||
|
|
@ -657,42 +653,6 @@ 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,10 +26,9 @@ bsp_status_t bsp_sd_init(void);
|
||||||
bsp_status_t bsp_sd_deinit(void);
|
bsp_status_t bsp_sd_deinit(void);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Проверить физическое наличие карты через USDHC PRES_STATE.CINST
|
* Проверить физическое наличие карты через регистр USDHC PRSSTAT.
|
||||||
* (USDHC_GetPresentStatusFlags). Включает тактирование USDHC1 на время
|
* Не требует предварительного вызова bsp_sd_init().
|
||||||
* чтения; не требует предварительного вызова bsp_sd_init(). Корректно
|
* Включает тактирование USDHC1 на время чтения регистра.
|
||||||
* пока пин D13 замаплен на USDHC1_CD_B.
|
|
||||||
*/
|
*/
|
||||||
bool bsp_sd_is_inserted(void);
|
bool bsp_sd_is_inserted(void);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,15 @@
|
||||||
#include "bsp/sd.h"
|
#include "bsp/sd.h"
|
||||||
|
|
||||||
#include "fsl_sd.h"
|
#include "fsl_sd.h"
|
||||||
#include "fsl_usdhc.h"
|
#include "fsl_usdhc.h" /* USDHC_Reset — аппаратный сброс FIFO/state machine */
|
||||||
#include "sdmmc_config.h"
|
#include "sdmmc_config.h" /* BOARD_SD_Config, BOARD_SDMMC_SD_HOST_BASEADDR */
|
||||||
|
|
||||||
#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;
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
|
|
@ -33,8 +37,7 @@ static void ensure_host_configured(void)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* cd=NULL, userData=NULL: детект — GPIO-callback внутри BOARD_SD_Config(),
|
/* cd=NULL, userData=NULL: CD управляется хостом через PRSSTAT */
|
||||||
* не внешний 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;
|
||||||
}
|
}
|
||||||
|
|
@ -51,8 +54,19 @@ 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);
|
||||||
|
|
@ -66,20 +80,12 @@ 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, включая usrParam.pwr */
|
ensure_host_configured(); /* только BOARD_SD_Config — заполняет g_sd */
|
||||||
|
|
||||||
if (SD_HostInit(&g_sd) != kStatus_Success)
|
/*
|
||||||
{
|
* Полный init (host + card) происходит в sd_disk_initialize → SD_Init,
|
||||||
return BSP_ERR_HW;
|
* который вызывается из f_mount → disk_initialize.
|
||||||
}
|
*/
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -95,6 +101,12 @@ 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;
|
||||||
|
|
@ -104,8 +116,6 @@ bsp_status_t bsp_sd_deinit(void)
|
||||||
|
|
||||||
bool bsp_sd_is_inserted(void)
|
bool bsp_sd_is_inserted(void)
|
||||||
{
|
{
|
||||||
CLOCK_EnableClock(kCLOCK_Usdhc1);
|
return GPIO_PinRead(BOARD_SDMMC_SD_CD_GPIO_BASE, BOARD_SDMMC_SD_CD_GPIO_PIN) ==
|
||||||
|
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 МБ)
|
||||||
|
|
||||||
Подъём SEMC (для прошивок без DCD) и минимальная верификация доступности
|
Минимальная верификация доступности внешней SDRAM, подключённой к SEMC.
|
||||||
внешней SDRAM, подключённой к SEMC. Подробное тестирование (паттерны, шина
|
Подробное тестирование (паттерны, шина адреса/данных, retention) выполняется
|
||||||
адреса/данных, retention) выполняется в тест-модуле `firmware_test/test_sdram.c`,
|
в тест-модуле `firmware_test/test_sdram.c`, который использует константы
|
||||||
который использует API этого модуля (путь с DCD, см. ниже).
|
и API этого модуля.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -29,45 +29,16 @@
|
||||||
Тестовая база смещена на 2 МБ от начала — гарантированно выше `.data`/`.bss`
|
Тестовая база смещена на 2 МБ от начала — гарантированно выше `.data`/`.bss`
|
||||||
прошивки и ниже non-cacheable региона.
|
прошивки и ниже non-cacheable региона.
|
||||||
|
|
||||||
---
|
**Важно:** SEMC инициализируется через DCD **до вызова `main()`**. Этот модуль
|
||||||
|
не настраивает SEMC и не трогает его регистры. Если DCD не отработал —
|
||||||
## Контракт: кто поднимает SEMC
|
`bsp_sdram_init()` вернёт ошибку, но исправить ситуацию из модуля нельзя.
|
||||||
|
|
||||||
Два независимых пути, в зависимости от того, есть ли у прошивки 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_configure(void); /* поднять SEMC: тактирование → пины → контроллер → init-команды SDRAM */
|
bsp_status_t bsp_sdram_init(void);
|
||||||
bsp_status_t bsp_sdram_init(void); /* верифицировать SDRAM (SEMC уже поднят — DCD либо bsp_sdram_configure()) */
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Публичные константы:**
|
**Публичные константы:**
|
||||||
|
|
@ -84,37 +55,6 @@ bsp_status_t bsp_sdram_init(void); /* верифицировать SDRAM (S
|
||||||
Константы размеров — для потребителей; `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`),
|
||||||
|
|
@ -126,13 +66,11 @@ ROM пишет регистры до включения MPU. Уже добавл
|
||||||
|
|
||||||
**Коды возврата:**
|
**Коды возврата:**
|
||||||
|
|
||||||
| Функция | Код | Условие |
|
| Код | Условие |
|
||||||
| -------------------------- | ----------------- | -------------------------------------------- |
|
| ----------------- | ------------------------------------------- |
|
||||||
| `bsp_sdram_configure()` | `BSP_OK` | Тактирование/пины/регистры/команды прошли |
|
| `BSP_OK` | SDRAM доступна, оба паттерна совпали |
|
||||||
| `bsp_sdram_configure()` | `BSP_ERR_INIT` | IP-команда SEMC вернула ошибку (precharge/refresh/mode-set) |
|
| `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
|
||||||
| `bsp_sdram_init()` | `BSP_OK` | SDRAM доступна, оба паттерна совпали |
|
| `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
|
||||||
| `bsp_sdram_init()` | `BSP_ERR_TIMEOUT` | SEMC не перешёл в IDLE за 10 мс |
|
|
||||||
| `bsp_sdram_init()` | `BSP_ERR_INIT` | Хотя бы один паттерн не совпал при readback |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -141,19 +79,10 @@ ROM пишет регистры до включения MPU. Уже добавл
|
||||||
```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) */
|
||||||
```
|
```
|
||||||
|
|
@ -162,19 +91,15 @@ 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->`, `SEMC_SendIPCommand()` |
|
| `sdk_semc` | PRIVATE | `fsl_semc.h` — `SEMC->STS0`, `SEMC_STS0_IDLE_MASK` |
|
||||||
| `bsp_board` | PRIVATE | Транзитивно даёт `sdk_clock` (`CLOCK_Init*()`) и `sdk_device` (`IOMUXC`) — отдельных PRIVATE-строк на них не заводили |
|
| `bsp_board` | PRIVATE | Общие board-уровневые символы |
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,9 @@
|
||||||
* @file sdram.h
|
* @file sdram.h
|
||||||
* @brief BSP для внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
|
* @brief BSP для внешней SDRAM MT48LC16M16A2 (32 МБ, шина 16 бит).
|
||||||
*
|
*
|
||||||
* Два пути в зависимости от того, кто использует SEMC:
|
* Архитектурное ограничение [DECISION]:
|
||||||
*
|
* SEMC инициализируется DCD до вызова main(). Этот модуль не трогает
|
||||||
* - Прошивки С DCD (firmware_test): SEMC поднят DCD до main().
|
* регистры SEMC — только верифицирует работоспособность памяти.
|
||||||
* 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)
|
||||||
|
|
@ -20,10 +14,16 @@
|
||||||
* Тестовый регион (не пересекается с .data/.bss и non-cacheable):
|
* Тестовый регион (не пересекается с .data/.bss и non-cacheable):
|
||||||
* 0x80200000 — начало (2 MB offset от базы)
|
* 0x80200000 — начало (2 MB offset от базы)
|
||||||
*
|
*
|
||||||
* Использование (путь без DCD):
|
* Использование:
|
||||||
* @code
|
* @code
|
||||||
* if (bsp_sdram_configure() != BSP_OK) { handle_semc_bringup_error(); }
|
* bsp_sdram_result_t result;
|
||||||
* 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,53 +65,19 @@
|
||||||
|
|
||||||
/* ── 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 или bsp_sdram_configure())
|
* Проверяет что SEMC контроллер инициализирован DCD и SDRAM отвечает —
|
||||||
* и SDRAM отвечает — выполняет минимальный write/read/verify на первых 4 байтах
|
* выполняет минимальный write/read/verify на первых 4 байтах тестового
|
||||||
* тестового региона. Не затрагивает .data/.bss прошивки.
|
* региона. Не затрагивает .data/.bss прошивки.
|
||||||
*
|
*
|
||||||
* @note Не (ре)инициализирует SEMC — предполагает, что DCD либо
|
* @note Не реинициализирует SEMC — DCD уже сделал это до main().
|
||||||
* bsp_sdram_configure() уже это сделали.
|
|
||||||
*
|
*
|
||||||
* @retval BSP_OK SDRAM доступна и отвечает корректно.
|
* @retval BSP_OK SDRAM доступна и отвечает корректно.
|
||||||
* @retval BSP_ERR_INIT SEMC не готов (инициализация не отработала).
|
* @retval BSP_ERR_INIT SEMC не готов (DCD не отработал).
|
||||||
* @retval BSP_ERR_TIMEOUT SEMC занят дольше ожидаемого.
|
* @retval BSP_ERR_TIMEOUT SEMC занят дольше ожидаемого.
|
||||||
*/
|
*/
|
||||||
bsp_status_t bsp_sdram_init(void);
|
bsp_status_t bsp_sdram_init(void);
|
||||||
|
|
||||||
#endif /* BSP_SDRAM_H_ */
|
#endif /* BSP_SDRAM_H_ */
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* @file sdram.c
|
* @file sdram.c
|
||||||
* @brief Подъём SEMC и верификация внешней SDRAM MT48LC16M16A2 (32 МБ, 16 бит).
|
* @brief Верификация внешней 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 — иначе
|
||||||
|
|
@ -18,13 +13,12 @@
|
||||||
#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
|
||||||
|
|
@ -44,202 +38,11 @@
|
||||||
/** @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 g_s_initialised = false;
|
static bool 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.
|
||||||
|
|
@ -269,10 +72,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,33 +87,16 @@ 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();
|
||||||
|
|
@ -331,6 +117,6 @@ bsp_status_t bsp_sdram_init(void)
|
||||||
return BSP_ERR_INIT;
|
return BSP_ERR_INIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_s_initialised = true;
|
s_initialised = true;
|
||||||
return BSP_OK;
|
return BSP_OK;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ COM-порт (`/dev/ttyACM*` на Linux/macOS, `COMx` на Windows). Испол
|
||||||
PHY калибровка: `D_CAL=0x0C`, `TXCAL45DP=0x06`, `TXCAL45DM=0x06`.
|
PHY калибровка: `D_CAL=0x0C`, `TXCAL45DP=0x06`, `TXCAL45DM=0x06`.
|
||||||
|
|
||||||
**VID/PID**: `0x1996` / `0x00AD` (`usb_device_descriptor.h`) — тот же
|
**VID/PID**: `0x1996` / `0x00AD` (`usb_device_descriptor.h`) — тот же
|
||||||
идентификатор, что `tools/service_tui/` (service-tui) использует для
|
идентификатор, что `tools/production/` (service-tui) использует для
|
||||||
детекта CDC-порта firmware_test (`SERVICE_CDC_VID`/`SERVICE_CDC_PID`).
|
детекта CDC-порта firmware_test (`SERVICE_CDC_VID`/`SERVICE_CDC_PID`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
# 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()` |
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 */
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,271 +0,0 @@
|
||||||
/*
|
|
||||||
** ###################################################################
|
|
||||||
** 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")
|
|
||||||
}
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
/*
|
|
||||||
** ###################################################################
|
|
||||||
** 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,198 +0,0 @@
|
||||||
# CI/CD: устройство GitHub Actions workflow
|
|
||||||
|
|
||||||
> Проект: TFT Firmware (MIMXRT1052CVJ5B)
|
|
||||||
> Документ описывает схему и принцип работы двух GitHub Actions workflow в
|
|
||||||
> репозитории: `ci.yml` (обычный PR/push-цикл) и `release.yml` (публикация
|
|
||||||
> релизных бинарников по тегу). Это техническая справка «как оно работает
|
|
||||||
> сейчас», а не хронология решений.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Два workflow, два разных назначения
|
|
||||||
|
|
||||||
| | `ci.yml` | `release.yml` |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Когда запускается | `push` в `dev`/`main`, любой `pull_request`, `workflow_dispatch` | `push` тега `tui-v*` / `firmware-v*` / `bootloader-v*`, `workflow_dispatch` |
|
|
||||||
| Что проверяет | Собирается ли проект и проходят ли host-тесты | Собираются ли и публикуются ли релизные бинарники |
|
|
||||||
| Публикует что-то наружу? | Нет — только артефакты прогона (для отладки) | Да — GitHub Release с реальными asset'ами (только по тегу) |
|
|
||||||
| Раннеры | `ubuntu-latest` (оба job'а) | `ubuntu-latest` + `macos-latest` + `windows-latest` |
|
|
||||||
|
|
||||||
Они намеренно не смешаны в один файл: PR-цикл должен оставаться быстрым и не
|
|
||||||
зависеть от кросс-платформенной упаковки `service-tui`, а релизная
|
|
||||||
публикация не должна гонять host-тесты повторно на каждый push в PR.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. `ci.yml` — обычный PR/push-цикл
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
trigger["push (dev/main)\nPR\nworkflow_dispatch"] --> build["build\nсборка в devcontainer"]
|
|
||||||
build -->|"artifact: build-tree"| test["test\nhost-тесты в devcontainer"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Оба job'а выполняются на `ubuntu-latest`, внутри одного и того же
|
|
||||||
devcontainer-образа (`.devcontainer/Dockerfile`) — то же окружение, что и у
|
|
||||||
разработчика локально (ARM toolchain, cmake, ninja, `just`, `uv`), не
|
|
||||||
отдельно собранное под раннер. Образ пересобирается в каждом job'е, но
|
|
||||||
кэшируется через `docker/build-push-action@v7` (`cache-from`/`cache-to:
|
|
||||||
type=gha, scope=tft-devcontainer`) — повторные прогоны переиспользуют слои,
|
|
||||||
не пересобирают с нуля.
|
|
||||||
|
|
||||||
- **`build`** — checkout, поднять devcontainer, `tools/host && uv sync`,
|
|
||||||
`just ci::build` (→ `just build::build-all-release`, все три firmware-
|
|
||||||
проекта), выгрузить `build/` как артефакт `build-tree`.
|
|
||||||
- **`test`** — зависит от `build` (`needs: build`), скачивает `build-tree`,
|
|
||||||
поднимает тот же образ (тот же кэш), `just ci::test` (→
|
|
||||||
`just build::test-host-release`, Unity/fff host-тесты), выгружает логи +
|
|
||||||
`build/` как `test-artifacts`.
|
|
||||||
|
|
||||||
Команды внутри контейнера запускаются `docker run --user root -v
|
|
||||||
"$GITHUB_WORKSPACE":/workspace -w /workspace tft-devcontainer-ci:latest
|
|
||||||
bash -lc '...'` — `--user root` обязателен, иначе non-root пользователь в
|
|
||||||
контейнере не может писать в bind-mounted `$GITHUB_WORKSPACE` (ломало
|
|
||||||
`uv sync`/создание `.venv`).
|
|
||||||
|
|
||||||
Чего `ci.yml` **не делает**: lint (заглушка в `just ci::lint`), coverage,
|
|
||||||
сборку/упаковку `service-tui`, HIL-тесты (нужно физическое железо —
|
|
||||||
самостоятельная задача для self-hosted раннера). См. §5 ниже.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. `release.yml` — публикация релиза
|
|
||||||
|
|
||||||
### 3.1 Схема тегов
|
|
||||||
|
|
||||||
Firmware (`firmware_test`), bootloader и `service-tui` версионируются и
|
|
||||||
релизятся **независимо** — три разных паттерна тега запускают три разных
|
|
||||||
сценария внутри одного workflow-файла:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
push_fw["push tag\nfirmware-vX.Y.Z"] --> firmware
|
|
||||||
push_bl["push tag\nbootloader-vX.Y.Z"] --> firmware
|
|
||||||
push_tui["push tag\ntui-vX.Y.Z"] --> firmware
|
|
||||||
dispatch["workflow_dispatch\n(release_type: tui | firmware | bootloader)"] --> firmware
|
|
||||||
|
|
||||||
firmware["firmware\n(ubuntu-latest, devcontainer)\nhab-firmware-test-debug +\nhab-bootloader-release"]
|
|
||||||
|
|
||||||
firmware -->|"тег firmware-v*"| publishFw["publish-firmware\ngh release create\n(HAB Debug)"]
|
|
||||||
firmware -->|"тег bootloader-v*"| publishBl["publish-bootloader\ngh release create\n(HAB Release, тестовый ключ)"]
|
|
||||||
|
|
||||||
firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| macos["service-tui-macos\njust host::package-tui"]
|
|
||||||
firmware -->|"тег tui-v* ИЛИ\ndispatch(tui)"| windows["service-tui-windows\njust host::package-tui"]
|
|
||||||
|
|
||||||
macos -->|"тег tui-v*"| publishTui["publish-tui\ngh release create\n(2× .zip)"]
|
|
||||||
windows -->|"тег tui-v*"| publishTui
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключевое архитектурное решение: **HAB-образы `firmware_test` и `bootloader`,
|
|
||||||
которые вшиваются внутрь TUI-бандла, всегда собираются заново из текущего
|
|
||||||
HEAD** джобой `firmware` — а не скачиваются из последних опубликованных
|
|
||||||
`firmware-v*`/`bootloader-v*` релизов. Поэтому job `firmware` выполняется
|
|
||||||
**при любом триггере**, без условия — она нужна и для отдельных релизов
|
|
||||||
firmware_test/bootloader, и как общая зависимость для упаковки TUI (см.
|
|
||||||
Фазу 5, `firmware/bootloader/PLAN.md` — до неё `firmware` собирала только
|
|
||||||
firmware_test, и TUI-бандл молча уходил без образа bootloader).
|
|
||||||
|
|
||||||
### 3.2 Триггеры
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: ["tui-v*", "firmware-v*", "bootloader-v*"]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
release_type: {type: choice, options: [tui, firmware, bootloader], default: tui}
|
|
||||||
```
|
|
||||||
|
|
||||||
`workflow_dispatch` — «сухой прогон» без публикации: собирает всё
|
|
||||||
(включая упаковку TUI под выбранный `release_type`), выгружает скачиваемые
|
|
||||||
артефакты, но **не** создаёт GitHub Release — джобы `publish-*` гейтятся
|
|
||||||
условием `if: startsWith(github.ref, 'refs/tags/...')`, которое на ручном
|
|
||||||
запуске всегда ложно (`github.ref` в этом случае — ветка, не тег).
|
|
||||||
|
|
||||||
Важный нюанс реализации: условие для `service-tui-macos`/`-windows`
|
|
||||||
проверяет `release_type` через `github.event.inputs.release_type`, не
|
|
||||||
через голый контекст `inputs.release_type` — последний рассчитан прежде
|
|
||||||
всего на reusable workflows (`workflow_call`) и не даёт предсказуемого
|
|
||||||
результата в job-level `if:` для прямого `workflow_dispatch`. На первом
|
|
||||||
реальном прогоне (`inputs.release_type`) джобы `service-tui-*` молча
|
|
||||||
скипались даже при выбранном `release_type=tui` — потребовалась замена на
|
|
||||||
`github.event.inputs.*`, если снова понадобится ссылаться на inputs в
|
|
||||||
job-level `if:`, использовать именно эту форму.
|
|
||||||
|
|
||||||
### 3.3 Джобы
|
|
||||||
|
|
||||||
| Job | Раннер | Когда выполняется | Что делает |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `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-bootloader` | `ubuntu-latest` | только push тега `bootloader-v*` | скачивает `bootloader-hab-release`; `gh release create bootloader-vX.Y.Z bootloader_hab.bin` — standalone-релиз (USB ROM/SWD, без TUI); `--notes` явно предупреждает, что HAB-подпись тестовым ключом, не production (см. `firmware/bootloader/SIGNING_CEREMONY.md`) |
|
|
||||||
| `service-tui-macos` / `service-tui-windows` | `macos-latest` / `windows-latest` | push тега `tui-v*` ИЛИ `workflow_dispatch` с `release_type=tui` | `astral-sh/setup-uv` + `extractions/setup-just` (на раннерах нет `uv`/`just` из коробки); скачивает `firmware-hab-debug` в `build/Debug/` и `bootloader-hab-release` в `build/Release/`; сверяет тег `tui-v*` с `version` в `pyproject.toml` (если применимо); `just host::service-setup` + `just host::package-tui` (падает явно, если `build/Release/bootloader_hab.bin` не найден — production-функция TUI жёстко требует именно этот файл); архивирует `dist/service-tui-vX.Y.Z-<os>/` в zip (`zip -r` на macOS, `Compress-Archive` на Windows); артефакт `service-tui-macos`/`service-tui-windows` |
|
|
||||||
| `publish-tui` | `ubuntu-latest`, `needs: [service-tui-macos, service-tui-windows]` | только push тега `tui-v*` | скачивает оба zip; `gh release create tui-vX.Y.Z *.zip` |
|
|
||||||
|
|
||||||
**Debug HAB для firmware_test, Release HAB для bootloader** — не единое
|
|
||||||
правило «всегда Debug». Release-сборка `firmware_test` нестабильна
|
|
||||||
(FCB/clock), поэтому `firmware`-джоба собирает `hab-firmware-test-debug`,
|
|
||||||
не полный `hab-all-release`. Bootloader — наоборот: production-путь (Фаза 5,
|
|
||||||
`firmware/bootloader/PLAN.md`) жёстко требует **Release**, подписанный
|
|
||||||
(`flags=0x08`) — Debug-конфиг bootloader остаётся unsigned и используется
|
|
||||||
только для локальной отладки, в релиз/TUI-бандл не попадает.
|
|
||||||
|
|
||||||
**Сверка версии тег↔файл** — маленький, но важный guard в обеих ветках
|
|
||||||
(`firmware`/`service-tui-*`): если версия в теге не совпадает с версией в
|
|
||||||
`CMakeLists.txt`/`pyproject.toml`, job падает с понятной ошибкой вместо
|
|
||||||
того, чтобы молча опубликовать релиз с несовпадающим номером версии внутри
|
|
||||||
файлов (забытый version bump перед тегом).
|
|
||||||
|
|
||||||
**Windows-раннер и bash** — корневой `Justfile` требует `bash` (`set shell
|
|
||||||
:= ["bash", ...]`); `windows-latest` образ GitHub Actions включает Git for
|
|
||||||
Windows (даёт `bash.exe` в PATH из коробки) — дополнительной настройки
|
|
||||||
shell не требуется, `just`-рецепты выполняются так же, как и локально под
|
|
||||||
Git Bash на Windows.
|
|
||||||
|
|
||||||
### 3.4 Как проверить без публикации
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh workflow run release.yml --ref dev -f release_type=tui
|
|
||||||
gh workflow run release.yml --ref dev -f release_type=firmware
|
|
||||||
gh workflow run release.yml --ref dev -f release_type=bootloader
|
|
||||||
```
|
|
||||||
|
|
||||||
или через веб-интерфейс: Actions → **Release** → **Run workflow** → выбрать
|
|
||||||
branch и `release_type`. Джобы `publish-*` в этом сценарии показываются как
|
|
||||||
**Skipped**, не **Failed** — это ожидаемое поведение, не баг.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Общее между `ci.yml` и `release.yml`
|
|
||||||
|
|
||||||
- **Один и тот же devcontainer-подход** для всего, что требует ARM
|
|
||||||
toolchain (сборка firmware) — job `firmware` в `release.yml` использует
|
|
||||||
дословно тот же паттерн `docker/build-push-action@v7` +
|
|
||||||
`docker run --user root ...`, что и `build`/`test` в `ci.yml`.
|
|
||||||
- **Общий GHA layer-кэш** — оба workflow используют `scope:
|
|
||||||
tft-devcontainer` в `cache-from`/`cache-to`, поэтому кэш переиспользуется
|
|
||||||
между обычными PR-прогонами и релизными сборками, а не живёт отдельно.
|
|
||||||
- **`workflow_dispatch` есть у обоих** — в `ci.yml` это просто способ
|
|
||||||
перезапустить pipeline вручную без нового коммита; в `release.yml` — это
|
|
||||||
единственный способ протестировать сборку без реальной публикации.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Чего пока нет
|
|
||||||
|
|
||||||
- **Self-hosted HIL-раннер.** Ни один из двух workflow не может
|
|
||||||
задетектировать SDP на живой плате или прогнать деструктивные сценарии
|
|
||||||
(обрыв USB) — GitHub-hosted раннеры не видят реальное железо. Это ручной
|
|
||||||
шаг перед каждым релизом, пока не поднят self-hosted lane.
|
|
||||||
- **`lint`/`coverage`** не подключены ни в `ci.yml`, ни в `release.yml`.
|
|
||||||
- **Публикация devcontainer image в GHCR** — образ пересобирается в каждой
|
|
||||||
job'е каждого workflow (пусть и с layer-кэшем); заранее опубликованный
|
|
||||||
образ сократил бы время старта ещё сильнее.
|
|
||||||
- **`bootloader-v*`/`tui-v*` релизы несут HAB-образ, подписанный ТЕСТОВЫМ
|
|
||||||
ключом** (`tools/host/hab/keys/`, HAB Open, схема NOCAK) — не production.
|
|
||||||
Реальная SRK-церемония описана в
|
|
||||||
`firmware/bootloader/SIGNING_CEREMONY.md`, в CI пока не встроена (сама
|
|
||||||
церемония — не автоматизируемый процесс, см. документ).
|
|
||||||
|
|
@ -68,7 +68,7 @@ graph TB
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Что устанавливается и где
|
## 3. Что устанавливается и где
|
||||||
|
|
||||||
|
|
@ -198,9 +198,9 @@ flowchart LR
|
||||||
│ │ │ dcd.bin, ivt_flashloader.bin
|
│ │ │ dcd.bin, ivt_flashloader.bin
|
||||||
│ │ └── uv.lock
|
│ │ └── uv.lock
|
||||||
│ │
|
│ │
|
||||||
│ ├── service_tui/ ← service-tui: TUI сервисного инженера (Textual)
|
│ ├── production/ ← service-tui: TUI сервисного инженера (Textual)
|
||||||
│ │ прошивка/диагностика готовых плат, см.
|
│ │ прошивка/диагностика готовых плат, см.
|
||||||
│ │ tools/service_tui/README.md
|
│ │ tools/production/README.md + DEV_ARCH.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` | XIP из Flash, без ITCM/DCD (не трогает SDRAM) — выбирает и запускает `tft_app` из слота (MCUboot Direct-XIP) | SPSDK → Flash |
|
| `bootloader` | Копирование в ITCM | SPSDK → Flash |
|
||||||
| `tft_app` | XIP из своего слота (Direct-XIP, два слота A/Б) + буферы в SDRAM (SEMC поднимает сама) | SPSDK → Flash |
|
| `tft_app` | XIP + буферы в SDRAM | 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 не используется — прошивка исчезает при отключении питания.
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ just host::flash-production # bootloader release + app release (с подт
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A["Плата в SDP-режиме\n1FC9:0130"] --> B["sdphost\nзагрузить ivt_flashloader.bin\nв RAM 0x20001C00"]
|
A["Плата в SDP-режиме\n1FC9:0130"] --> B["sdphost\nзагрузить ivt_flashloader.bin\nв RAM 0x20001C00"]
|
||||||
B --> C["sdphost jump-address\nFlashloader поднимается\nкак 15A2:0073"]
|
B --> C["sdphost jump-address\nFlashloader поднимается\nкак 15A2:0073"]
|
||||||
C --> D["configure-memory 0xC0000207\nинициализация FlexSPI NOR + QE-бит"]
|
C --> D["configure-memory 0xC0000007\nинициализация FlexSPI NOR"]
|
||||||
D --> E["flash-erase-region 0x60000000"]
|
D --> E["flash-erase-region 0x60000000"]
|
||||||
E --> F["configure-memory 0xF000000F\nзапись FCB в 0x60000000"]
|
E --> F["configure-memory 0xF000000F\nзапись FCB в 0x60000000"]
|
||||||
F --> G["write-memory 0x60001000\nHAB-образ"]
|
F --> G["write-memory 0x60001000\nHAB-образ"]
|
||||||
|
|
@ -79,7 +79,7 @@ auto-config не подтверждена — см. 1.5.
|
||||||
|
|
||||||
### 1.5 Нестандартная память (W25Q256/512) и сторонние бинарники
|
### 1.5 Нестандартная память (W25Q256/512) и сторонние бинарники
|
||||||
|
|
||||||
`service-tui` (`tools/service_tui/`) умеет прошивать бинарники, собранные не
|
`service-tui` (`tools/production/`) умеет прошивать бинарники, собранные не
|
||||||
в этом репозитории (например, старые платы с W25Q512), тем же способом
|
в этом репозитории (например, старые платы с W25Q512), тем же способом
|
||||||
(USB SDP), но с двумя отличиями от штатного пути. Это **отдельная
|
(USB SDP), но с двумя отличиями от штатного пути. Это **отдельная
|
||||||
реализация**, не связанная с `flash_usb.py`/`nxpimage` CLI — TUI прошивает
|
реализация**, не связанная с `flash_usb.py`/`nxpimage` CLI — TUI прошивает
|
||||||
|
|
@ -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/ARCHITECTURE.md](../tools/service_tui/docs/ARCHITECTURE.md),
|
Подробности конвейера — в [tools/production/docs/DEV_ARCH.md](../tools/production/docs/DEV_ARCH.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.
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
# Карта 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()`.
|
|
||||||
|
|
@ -1,302 +0,0 @@
|
||||||
# Загрузчик 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` | Счётчик обнулён, зависший слот пробуется заново |
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# 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) |
|
|
||||||
|
|
@ -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,42 +150,6 @@ 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-контейнер
|
||||||
|
|
@ -206,7 +170,7 @@ export`, `flags`, `AuthenticateData`) не меняется, меняются т
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -217,7 +181,7 @@ BootROM в режиме SDP умеет только писать в RAM и пр
|
||||||
│
|
│
|
||||||
Flashloader запущен
|
Flashloader запущен
|
||||||
│ blhost -u 0x15A2,0x0073
|
│ blhost -u 0x15A2,0x0073
|
||||||
├─ fill-memory 0x2000 4 0xC0000207 ← option word для FlexSPI NOR (+QE-бит)
|
├─ fill-memory 0x2000 4 0xC0000007 ← option word для FlexSPI NOR
|
||||||
├─ configure-memory 9 0x2000 ← Flashloader пишет FCB в Flash
|
├─ configure-memory 9 0x2000 ← Flashloader пишет FCB в Flash
|
||||||
├─ flash-erase-region 0x60000000 …
|
├─ flash-erase-region 0x60000000 …
|
||||||
├─ write-memory 0x60002000 firmware_hab.bin ← HAB образ
|
├─ write-memory 0x60002000 firmware_hab.bin ← HAB образ
|
||||||
|
|
@ -228,12 +192,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 необходимо убедиться, что подписанный образ успешно проходит верификацию на реальном железе.
|
||||||
|
|
||||||
|
|
@ -241,13 +205,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 устройств |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
# 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")
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
# 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` генерируется, вручную не редактируется.
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
# 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)
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
* 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_ */
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
# 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}")
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,280 +0,0 @@
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
/**
|
|
||||||
* @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); /* при успехе не возвращается */
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
/**
|
|
||||||
* @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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,282 +0,0 @@
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,313 +0,0 @@
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
/**
|
|
||||||
* @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 */
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
/**
|
|
||||||
* @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 };
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,273 +0,0 @@
|
||||||
/**
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
/**
|
|
||||||
* @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 };
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/**
|
|
||||||
* @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_ */
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
# 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)
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
/**
|
|
||||||
* @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,6 +29,7 @@
|
||||||
- [Как добавить новый тест](#как-добавить-новый-тест)
|
- [Как добавить новый тест](#как-добавить-новый-тест)
|
||||||
- [Host unit-тесты](#host-unit-тесты)
|
- [Host unit-тесты](#host-unit-тесты)
|
||||||
- [Версионирование](#версионирование)
|
- [Версионирование](#версионирование)
|
||||||
|
- [Архитектурные решения (закрыты)](#архитектурные-решения-закрыты)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -198,16 +199,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`).
|
||||||
|
|
@ -270,7 +271,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, нет подтверждений доставки.
|
||||||
|
|
@ -550,16 +551,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:**
|
||||||
|
|
||||||
|
|
@ -822,4 +823,4 @@ session_start / version_response: "fw":"0.1.2"
|
||||||
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
|
с ожидаемой. При несовместимых изменениях протокола (новое обязательное поле,
|
||||||
смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`.
|
смена семантики) — bump версии + обновление этого документа и `README_TESTING.md`.
|
||||||
|
|
||||||
|
##
|
||||||
|
|
|
||||||
|
|
@ -232,58 +232,10 @@ 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 -f mimxrt1050 -b "${BIN}" -o "${OUT}"
|
cd "{{ TOOLS_DIR }}" && uv run nxpimage hab parse -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
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
84
just/ci_workflow.md
Normal file
84
just/ci_workflow.md
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Текущее состояние 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 и аппаратной валидации на стенде.
|
||||||
113
just/host.just
113
just/host.just
|
|
@ -269,44 +269,44 @@ _flash_swd := TOOLS_DIR / "flash_swd.py"
|
||||||
[doc('Прошить firmware_test Debug через SWD. Нужен power cycle после')]
|
[doc('Прошить firmware_test Debug через SWD. Нужен power cycle после')]
|
||||||
[group('flash-swd')]
|
[group('flash-swd')]
|
||||||
flash-swd-test-debug:
|
flash-swd-test-debug:
|
||||||
uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \
|
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
|
||||||
--firmware firmware_test --build-type Debug
|
--firmware firmware_test --build-type Debug
|
||||||
|
|
||||||
[doc('Прошить firmware_test Release через SWD. Нужен power cycle после')]
|
[doc('Прошить firmware_test Release через SWD. Нужен power cycle после')]
|
||||||
[group('flash-swd')]
|
[group('flash-swd')]
|
||||||
flash-swd-test-release:
|
flash-swd-test-release:
|
||||||
uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \
|
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
|
||||||
--firmware firmware_test --build-type Release
|
--firmware firmware_test --build-type Release
|
||||||
|
|
||||||
[doc('Прошить bootloader Debug через SWD. Нужен power cycle после')]
|
[doc('Прошить bootloader Debug через SWD. Нужен power cycle после')]
|
||||||
[group('flash-swd')]
|
[group('flash-swd')]
|
||||||
flash-swd-bootloader-debug:
|
flash-swd-bootloader-debug:
|
||||||
uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \
|
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
|
||||||
--firmware bootloader --build-type Debug
|
--firmware bootloader --build-type Debug
|
||||||
|
|
||||||
[doc('Прошить bootloader Release через SWD. Нужен power cycle после')]
|
[doc('Прошить bootloader Release через SWD. Нужен power cycle после')]
|
||||||
[group('flash-swd')]
|
[group('flash-swd')]
|
||||||
flash-swd-bootloader-release:
|
flash-swd-bootloader-release:
|
||||||
uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \
|
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
|
||||||
--firmware bootloader --build-type Release
|
--firmware bootloader --build-type Release
|
||||||
|
|
||||||
[doc('Прошить tft_app Debug через SWD. Нужен power cycle после')]
|
[doc('Прошить tft_app Debug через SWD. Нужен power cycle после')]
|
||||||
[group('flash-swd')]
|
[group('flash-swd')]
|
||||||
flash-swd-app-debug:
|
flash-swd-app-debug:
|
||||||
uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \
|
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
|
||||||
--firmware app --build-type Debug
|
--firmware app --build-type Debug
|
||||||
|
|
||||||
[doc('Прошить tft_app Release через SWD. Нужен power cycle после')]
|
[doc('Прошить tft_app Release через SWD. Нужен power cycle после')]
|
||||||
[group('flash-swd')]
|
[group('flash-swd')]
|
||||||
flash-swd-app-release:
|
flash-swd-app-release:
|
||||||
uv run --directory "{{ HIL_DIR }}" python "{{ _flash_swd }}" \
|
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
|
||||||
--firmware app --build-type Release
|
--firmware app --build-type Release
|
||||||
|
|
||||||
[confirm("Стереть всю Flash (W25Q128)? Все прошивки будут удалены.")]
|
[confirm("Стереть всю Flash (W25Q128)? Все прошивки будут удалены.")]
|
||||||
[doc('Стереть всю Flash (W25Q128) через SWD (chip erase). Нужен power cycle после')]
|
[doc('Стереть всю Flash (W25Q128) через SWD (chip erase). Нужен power cycle после')]
|
||||||
[group('flash-swd')]
|
[group('flash-swd')]
|
||||||
flash-swd-erase:
|
flash-swd-erase:
|
||||||
uv run --directory "{{ HIL_DIR }}" pyocd erase \
|
uv run --directory {{ HIL_DIR }} pyocd erase \
|
||||||
--target {{ env('PYOCD_TARGET', 'mimxrt1050_quadspi') }} \
|
--target {{ env('PYOCD_TARGET', 'mimxrt1050_quadspi') }} \
|
||||||
--frequency {{ env('PYOCD_FREQUENCY', '4000000') }} \
|
--frequency {{ env('PYOCD_FREQUENCY', '4000000') }} \
|
||||||
--chip -O resume_on_disconnect=False
|
--chip -O resume_on_disconnect=False
|
||||||
|
|
@ -321,59 +321,59 @@ _hil_build := BUILD_DIR / "target-debug"
|
||||||
[doc('Запустить все не интерактивные HIL-тесты')]
|
[doc('Запустить все не интерактивные HIL-тесты')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-run:
|
hil-run:
|
||||||
HIL_BUILD_DIR="{{ _hil_build }}" \
|
HIL_BUILD_DIR={{ _hil_build }} \
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest -m "not interactive and not usb_vcom" -v
|
uv run --directory {{ HIL_DIR }} pytest -m "not interactive and not usb_vcom" -v
|
||||||
|
|
||||||
[doc('Запустить все интерактивные HIL-тесты: кнопки, дисплей')]
|
[doc('Запустить все интерактивные HIL-тесты: кнопки, дисплей')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-run-interactive:
|
hil-run-interactive:
|
||||||
HIL_BUILD_DIR="{{ _hil_build }}" \
|
HIL_BUILD_DIR={{ _hil_build }} \
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest -m interactive -s -v
|
uv run --directory {{ HIL_DIR }} pytest -m interactive -s -v
|
||||||
|
|
||||||
[doc('Запустить HIL-тест UART')]
|
[doc('Запустить HIL-тест UART')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-uart:
|
hil-uart:
|
||||||
HIL_BUILD_DIR="{{ _hil_build }}" \
|
HIL_BUILD_DIR={{ _hil_build }} \
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest 01_test_uart.py -v
|
uv run --directory {{ HIL_DIR }} pytest 01_test_uart.py -v
|
||||||
|
|
||||||
[doc('Запустить HIL-тест оптоизолированных входов')]
|
[doc('Запустить HIL-тест оптоизолированных входов')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-opto:
|
hil-opto:
|
||||||
HIL_BUILD_DIR="{{ _hil_build }}" \
|
HIL_BUILD_DIR={{ _hil_build }} \
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest 02_test_opto.py -v
|
uv run --directory {{ HIL_DIR }} pytest 02_test_opto.py -v
|
||||||
|
|
||||||
[doc('Запустить HIL-тест кнопок')]
|
[doc('Запустить HIL-тест кнопок')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-button:
|
hil-button:
|
||||||
HIL_BUILD_DIR="{{ _hil_build }}" \
|
HIL_BUILD_DIR={{ _hil_build }} \
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest 04_test_button.py -s -v
|
uv run --directory {{ HIL_DIR }} pytest 04_test_button.py -s -v
|
||||||
|
|
||||||
[doc('Запустить HIL тест bsp_can')]
|
[doc('Запустить HIL тест bsp_can')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-can:
|
hil-can:
|
||||||
HIL_BUILD_DIR="{{ _hil_build }}" \
|
HIL_BUILD_DIR={{ _hil_build }} \
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest 03_test_can.py -v
|
uv run --directory {{ HIL_DIR }} pytest 03_test_can.py -v
|
||||||
|
|
||||||
[doc('Запустить HIL-тест USB CDC')]
|
[doc('Запустить HIL-тест USB CDC')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-usb-cdc:
|
hil-usb-cdc:
|
||||||
HIL_BUILD_DIR="{{ _hil_build }}" \
|
HIL_BUILD_DIR={{ _hil_build }} \
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest 05_test_usb_cdc.py -v
|
uv run --directory {{ HIL_DIR }} pytest 05_test_usb_cdc.py -v
|
||||||
|
|
||||||
[doc('Запустить HIL-тест оптовходов через firmware_test CDC (протокол v2)')]
|
[doc('Запустить HIL-тест оптовходов через firmware_test CDC (протокол v2)')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-firmware-opto:
|
hil-firmware-opto:
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest 06_test_firmware_opto.py -v
|
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_opto.py -v
|
||||||
|
|
||||||
[doc('Запустить HIL-тест CAN через firmware_test CDC (протокол v2)')]
|
[doc('Запустить HIL-тест CAN через firmware_test CDC (протокол v2)')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-firmware-can:
|
hil-firmware-can:
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest 06_test_firmware_can.py -v
|
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_can.py -v
|
||||||
|
|
||||||
[doc('Запустить все firmware_test CDC HIL-тесты (opto + can)')]
|
[doc('Запустить все firmware_test CDC HIL-тесты (opto + can)')]
|
||||||
[group('hil')]
|
[group('hil')]
|
||||||
hil-firmware:
|
hil-firmware:
|
||||||
uv run --directory "{{ HIL_DIR }}" pytest 06_test_firmware_opto.py 06_test_firmware_can.py -v
|
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_opto.py 06_test_firmware_can.py -v
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ГРУППА: debug — GDB-сервер для отладки из VSCode (devcontainer)
|
# ГРУППА: debug — GDB-сервер для отладки из VSCode (devcontainer)
|
||||||
|
|
@ -394,8 +394,8 @@ _hil_dir := "tools/hil"
|
||||||
[doc('Запустить GDB-сервер. Оставить запущенным, подключаться из VSCode')]
|
[doc('Запустить GDB-сервер. Оставить запущенным, подключаться из VSCode')]
|
||||||
[group('debug')]
|
[group('debug')]
|
||||||
debug-server:
|
debug-server:
|
||||||
uv run --directory "{{ _hil_dir }}" pyocd gdbserver \
|
uv run --directory {{ _hil_dir }} pyocd gdbserver \
|
||||||
--config "{{ justfile_directory() }}/pyocd_debug.yaml" \
|
--config {{ justfile_directory() }}/pyocd_debug.yaml \
|
||||||
--port {{ env('GDB_PORT', '3333') }} \
|
--port {{ env('GDB_PORT', '3333') }} \
|
||||||
--persist \
|
--persist \
|
||||||
--semihosting
|
--semihosting
|
||||||
|
|
@ -403,7 +403,7 @@ debug-server:
|
||||||
[doc('Показать доступные builtin-таргеты pyOCD для MIMXRT')]
|
[doc('Показать доступные builtin-таргеты pyOCD для MIMXRT')]
|
||||||
[group('debug')]
|
[group('debug')]
|
||||||
debug-list-targets:
|
debug-list-targets:
|
||||||
uv run --directory "{{ _hil_dir }}" pyocd list --targets | grep -i mimx
|
uv run --directory {{ _hil_dir }} pyocd list --targets | grep -i mimx
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ГРУППА: m5 — работа с M5Stack StamPLC
|
# ГРУППА: m5 — работа с M5Stack StamPLC
|
||||||
|
|
@ -448,7 +448,7 @@ m5-scan:
|
||||||
[doc('Открыть MicroPython REPL на M5Stack (Ctrl+X для выхода)')]
|
[doc('Открыть MicroPython REPL на M5Stack (Ctrl+X для выхода)')]
|
||||||
[group('m5')]
|
[group('m5')]
|
||||||
m5-repl:
|
m5-repl:
|
||||||
uv run --directory "{{ HIL_DIR }}" mpremote connect "{{ M5_PORT }}" repl
|
uv run --directory {{ HIL_DIR }} mpremote connect {{ M5_PORT }} repl
|
||||||
|
|
||||||
[doc('Проверить версию MicroPython на M5Stack')]
|
[doc('Проверить версию MicroPython на M5Stack')]
|
||||||
[group('m5')]
|
[group('m5')]
|
||||||
|
|
@ -470,8 +470,8 @@ m5-start:
|
||||||
[doc('Управление питанием таргета через M5 RLY1: m5-power <on|off>')]
|
[doc('Управление питанием таргета через M5 RLY1: m5-power <on|off>')]
|
||||||
[group('m5')]
|
[group('m5')]
|
||||||
m5-power state:
|
m5-power state:
|
||||||
uv run --directory "{{ HIL_DIR }}" python m5/power.py \
|
uv run --directory {{ HIL_DIR }} python m5/power.py \
|
||||||
--port "{{ M5_PORT }}" --state "{{ state }}"
|
--port {{ M5_PORT }} --state {{ state }}
|
||||||
|
|
||||||
[doc('Скопировать агент на M5Stack (tools/hil/m5_agent.py → /main.py)')]
|
[doc('Скопировать агент на M5Stack (tools/hil/m5_agent.py → /main.py)')]
|
||||||
[group('m5')]
|
[group('m5')]
|
||||||
|
|
@ -488,7 +488,7 @@ m5-deploy:
|
||||||
[doc('Запустить интерактивный CLI для ручного тестирования агента')]
|
[doc('Запустить интерактивный CLI для ручного тестирования агента')]
|
||||||
[group('m5')]
|
[group('m5')]
|
||||||
m5-cli:
|
m5-cli:
|
||||||
uv run --directory "{{ HIL_DIR }}" python m5/cli.py --port "{{ M5_PORT }}"
|
uv run --directory {{ HIL_DIR }} python m5/cli.py --port {{ M5_PORT }}
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ГРУППА: util
|
# ГРУППА: util
|
||||||
|
|
@ -513,8 +513,8 @@ flashloader-status:
|
||||||
[doc('Открыть UART монитор на MCU-Link VCOM (кроссплатформенный)')]
|
[doc('Открыть UART монитор на MCU-Link VCOM (кроссплатформенный)')]
|
||||||
[group('util')]
|
[group('util')]
|
||||||
uart-monitor:
|
uart-monitor:
|
||||||
uv run --directory "{{ HIL_DIR }}" python3 -m serial.tools.miniterm \
|
uv run --directory {{ HIL_DIR }} python3 -m serial.tools.miniterm \
|
||||||
"{{ env('HIL_VCOM_PORT') }}" "{{ env('HIL_VCOM_BAUD', '115200') }}" \
|
{{ env('HIL_VCOM_PORT') }} {{ env('HIL_VCOM_BAUD', '115200') }} \
|
||||||
--raw
|
--raw
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -538,21 +538,21 @@ production: flash-production
|
||||||
# ГРУППА: service — TUI сервисного инженера
|
# ГРУППА: service — TUI сервисного инженера
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
SERVICE_TUI_DIR := justfile_directory() / 'tools/service_tui'
|
PRODUCTION_DIR := justfile_directory() / 'tools/production'
|
||||||
|
|
||||||
[doc('Установить/обновить зависимости TUI сервисного инженера')]
|
[doc('Установить/обновить зависимости TUI сервисного инженера')]
|
||||||
[group('service')]
|
[group('service')]
|
||||||
service-setup:
|
service-setup:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
echo " 📦 Syncing tools/service_tui deps..."
|
echo " 📦 Syncing tools/production deps..."
|
||||||
cd "{{ SERVICE_TUI_DIR }}" && uv sync
|
cd "{{ PRODUCTION_DIR }}" && uv sync
|
||||||
echo " ✅ tools/service_tui deps installed"
|
echo " ✅ tools/production deps installed"
|
||||||
|
|
||||||
[doc('Запустить TUI сервисного инженера')]
|
[doc('Запустить TUI сервисного инженера')]
|
||||||
[group('service')]
|
[group('service')]
|
||||||
service-tui:
|
service-tui:
|
||||||
uv run --directory "{{ SERVICE_TUI_DIR }}" python main.py
|
uv run --directory {{ PRODUCTION_DIR }} python main.py
|
||||||
|
|
||||||
[doc('Собрать standalone-бинарь TUI (PyInstaller → dist/service_tui)')]
|
[doc('Собрать standalone-бинарь TUI (PyInstaller → dist/service_tui)')]
|
||||||
[group('service')]
|
[group('service')]
|
||||||
|
|
@ -560,7 +560,7 @@ service-build:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
echo " 🔨 Building standalone service-tui..."
|
echo " 🔨 Building standalone service-tui..."
|
||||||
cd "{{ SERVICE_TUI_DIR }}" && uv run pyinstaller \
|
cd "{{ PRODUCTION_DIR }}" && uv run pyinstaller \
|
||||||
--onefile \
|
--onefile \
|
||||||
--name service_tui \
|
--name service_tui \
|
||||||
--add-data "../shared:shared" \
|
--add-data "../shared:shared" \
|
||||||
|
|
@ -571,7 +571,7 @@ service-build:
|
||||||
# ГРУППА: package — сборка service-tui в исполняемый бандл (PyInstaller)
|
# ГРУППА: package — сборка service-tui в исполняемый бандл (PyInstaller)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
_service_dir := justfile_directory() / 'tools/service_tui'
|
_prod_dir := justfile_directory() / 'tools/production'
|
||||||
_prod_build_dir := env('BUILD_DIR', justfile_directory() / 'build')
|
_prod_build_dir := env('BUILD_DIR', justfile_directory() / 'build')
|
||||||
|
|
||||||
[doc('Собрать service-tui в PyInstaller-бандл + скопировать firmware/*_hab.bin')]
|
[doc('Собрать service-tui в PyInstaller-бандл + скопировать firmware/*_hab.bin')]
|
||||||
|
|
@ -580,14 +580,14 @@ package-tui:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "{{ _service_dir }}"
|
cd "{{ _prod_dir }}"
|
||||||
echo " 📦 Running PyInstaller..."
|
echo " 📦 Running PyInstaller..."
|
||||||
uv run pyinstaller service_tui.spec --noconfirm
|
uv run pyinstaller service_tui.spec --noconfirm
|
||||||
|
|
||||||
DIST="dist/service_tui"
|
DIST="dist/service_tui"
|
||||||
|
|
||||||
echo " 📦 Copying firmware HAB images (build/<Type>/*_hab.bin)..."
|
echo " 📦 Copying firmware HAB images (build/<Type>/*_hab.bin)..."
|
||||||
found_debug=0
|
found=0
|
||||||
for type_dir in "{{ _prod_build_dir }}"/Debug "{{ _prod_build_dir }}"/Release; do
|
for type_dir in "{{ _prod_build_dir }}"/Debug "{{ _prod_build_dir }}"/Release; do
|
||||||
[[ -d "$type_dir" ]] || continue
|
[[ -d "$type_dir" ]] || continue
|
||||||
type_name=$(basename "$type_dir")
|
type_name=$(basename "$type_dir")
|
||||||
|
|
@ -595,24 +595,12 @@ package-tui:
|
||||||
[[ -f "$hab" ]] || continue
|
[[ -f "$hab" ]] || continue
|
||||||
mkdir -p "$DIST/firmware/$type_name"
|
mkdir -p "$DIST/firmware/$type_name"
|
||||||
cp "$hab" "$DIST/firmware/$type_name/"
|
cp "$hab" "$DIST/firmware/$type_name/"
|
||||||
[[ "$type_name" == "Debug" ]] && found_debug=1
|
found=1
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
if [[ "$found_debug" -eq 0 ]]; then
|
if [[ "$found" -eq 0 ]]; then
|
||||||
echo " ❌ Не найден Debug *_hab.bin в {{ _prod_build_dir }}/Debug — соберите"
|
echo " ⚠️ Ни один *_hab.bin не найден в {{ _prod_build_dir }} — соберите"
|
||||||
echo " прошивку заранее (just build::hab-firmware-test-debug)."
|
echo " прошивки заранее (just build::hab-all-release / hab-all-debug)."
|
||||||
echo " Бандл TUI без Debug-образа нерабочий (см. README/DEV_ARCH.md)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Flasher.PRODUCTION (Фаза 5) резолвит bootloader ЖЁСТКО из Release, без
|
|
||||||
# фоллбэка на Debug (см. flasher.py) — в отличие от firmware_test выше,
|
|
||||||
# здесь недостаточно "хоть какой-то hab нашёлся", нужен именно этот файл,
|
|
||||||
# иначе «Серийная прошивка» в готовом бандле молча падает в рантайме.
|
|
||||||
if [[ ! -f "{{ _prod_build_dir }}/Release/bootloader_hab.bin" ]]; then
|
|
||||||
echo " ❌ Не найден {{ _prod_build_dir }}/Release/bootloader_hab.bin — соберите"
|
|
||||||
echo " заранее (just build::hab-bootloader-release)."
|
|
||||||
echo " Серийная прошивка в бандле TUI без него не работает."
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VERSION=$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
|
VERSION=$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
|
||||||
|
|
@ -625,11 +613,6 @@ package-tui:
|
||||||
RELEASE_NAME="service-tui-v${VERSION}-${OS_TAG}"
|
RELEASE_NAME="service-tui-v${VERSION}-${OS_TAG}"
|
||||||
|
|
||||||
rm -rf "dist/${RELEASE_NAME}"
|
rm -rf "dist/${RELEASE_NAME}"
|
||||||
mkdir -p "dist/${RELEASE_NAME}"
|
mkdir -p "dist/${RELEASE_NAME}/custom_binaries"
|
||||||
mv "$DIST" "dist/${RELEASE_NAME}/"
|
mv "$DIST" "dist/${RELEASE_NAME}"
|
||||||
|
|
||||||
echo " 📦 Seeding custom_binaries/ from tools/service_tui/custom_binaries/..."
|
|
||||||
mkdir -p "dist/${RELEASE_NAME}/service_tui/custom_binaries"
|
|
||||||
cp "{{ _service_dir }}/custom_binaries/"*.bin "dist/${RELEASE_NAME}/service_tui/custom_binaries/"
|
|
||||||
|
|
||||||
echo " ✅ dist/${RELEASE_NAME}"
|
echo " ✅ dist/${RELEASE_NAME}"
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,6 @@ 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,7 +65,6 @@ __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");
|
||||||
|
|
@ -75,17 +74,5 @@ 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 */
|
||||||
|
|
|
||||||
1738
sdk/sdk_tree.txt
Normal file
1738
sdk/sdk_tree.txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -218,99 +218,3 @@ 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()
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
# 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`.
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
/**
|
|
||||||
* @file fake_flash_map_backend.h
|
|
||||||
* @brief Test-only реализация контракта flash_map.h поверх обычной памяти
|
|
||||||
* хоста — заменяет bsp_qspi_flash для host-тестов bootutil (Direct-XIP
|
|
||||||
* выбор слота, см. firmware/bootloader/PLAN.md, Фаза 2).
|
|
||||||
*
|
|
||||||
* Слот A (fa_id=0) и слот Б (fa_id=1) — смежные регионы одного статического
|
|
||||||
* буфера FAKE_SLOT_SIZE байт каждый, имитируют один flash-девайс с двумя
|
|
||||||
* областями (как и на реальном железе — см. BOOTLOADER_FLASH_MAP.md).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef FAKE_FLASH_MAP_BACKEND_H_
|
|
||||||
#define FAKE_FLASH_MAP_BACKEND_H_
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#define FAKE_FLASH_SECTOR_SIZE 0x1000U /* 4 KB — как реальный W25Qxx */
|
|
||||||
#define FAKE_FLASH_SLOT_SIZE 0x8000U /* 32 KB — уменьшенный тестовый слот */
|
|
||||||
|
|
||||||
/** @brief Заполнить всю fake-флеш 0xFF (оба слота — "стёрты"). */
|
|
||||||
void fake_flash_reset(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Записать содержимое (например, imgtool-подписанный фикстур-образ)
|
|
||||||
* в начало указанного слота.
|
|
||||||
*
|
|
||||||
* @param[in] slot_idx 0 — Slot A, 1 — Slot Б.
|
|
||||||
* @param[in] p_data Данные образа.
|
|
||||||
* @param[in] len Длина данных, <= FAKE_FLASH_SLOT_SIZE.
|
|
||||||
*/
|
|
||||||
void fake_flash_write_slot(int slot_idx, const uint8_t *p_data, size_t len);
|
|
||||||
|
|
||||||
#endif /* FAKE_FLASH_MAP_BACKEND_H_ */
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue